From a7ff0bf564289254ca0b126c36ab2d317da33f72 Mon Sep 17 00:00:00 2001 From: Juan Denis Date: Tue, 3 Nov 2026 13:47:53 -0500 Subject: [PATCH] feat(frontend): sort select + direction toggle above stacked DataTable cards on phones --- frontend/src/components/ds/DataTable.jsx | 42 ++++++++++++++++++- frontend/src/i18n/locales/en.json | 6 ++- .../src/styles/components/_design-system.scss | 19 ++++++++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/ds/DataTable.jsx b/frontend/src/components/ds/DataTable.jsx index 5b1e3600..cac10d8f 100644 --- a/frontend/src/components/ds/DataTable.jsx +++ b/frontend/src/components/ds/DataTable.jsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { ChevronUp, ChevronDown, ChevronRight, Filter } from 'lucide-react'; +import { ArrowDownNarrowWide, ArrowUpNarrowWide, ChevronUp, ChevronDown, ChevronRight, Filter } from 'lucide-react'; import { Table, TableBody, @@ -9,6 +9,7 @@ import { TableRow, } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { cn } from '@/lib/utils'; import { applyTableSorts, nextSorts, useTableSort } from '@/hooks/useTableSort'; import { useColumnVisibility } from '@/hooks/useColumnVisibility'; @@ -19,6 +20,8 @@ import { useTranslation } from 'react-i18next'; import { translateLabel } from '../../i18n/labels'; import { Button as SharedButton } from '@/components/ui/button'; +const SORT_NONE = '__none__'; + /** * Declarative data table built on top of the shadcn/ui Table primitives. * @@ -46,7 +49,7 @@ import { Button as SharedButton } from '@/components/ui/button'; * The first visible column is the card title, the rest become "label · value" * rows using the column header as the label, a header-less trailing column * (actions) becomes a footer row of 40px targets, and the sortable headers - * collapse into a chip row above the cards. Pass `responsive={false}` for + * collapse into one sort select + direction toggle above the cards. Pass `responsive={false}` for * editor-style grids that must stay tabular on phones. * * Example: @@ -398,8 +401,43 @@ export function DataTable({ ); }; + // Phones hide the header, so column sort moves to a select + direction toggle. + const sortColumns = visibleColumns.filter((column) => sortable && column.sortable && typeof column.header === 'string'); + const activeSort = sorts[0]; + const sortBar = responsive && sortColumns.length > 0 && ( +
+ + setSorts([{ key: activeSort.key, direction: activeSort.direction === 'asc' ? 'desc' : 'asc' }])} + > + {activeSort?.direction === 'desc' ? : } + +
+ ); + return (
+ {sortBar} diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 6c2a3705..d38b188c 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1695,10 +1695,14 @@ }, "dataTable": { "clickToSortShiftClickTo": "Click to sort · Shift+click to add a sort level", + "defaultOrder": "Default order", "loading": "Loading", "noRowsMatchTheActiveColumn": "No rows match the active column filters.", "selectAllRows": "Select all rows", - "selectRow": "Select row" + "selectRow": "Select row", + "sortAscending": "Sorted ascending, switch to descending", + "sortBy": "Sort by", + "sortDescending": "Sorted descending, switch to ascending" }, "dataTableFooter": { "nextPage": "Next page", diff --git a/frontend/src/styles/components/_design-system.scss b/frontend/src/styles/components/_design-system.scss index c5c5d362..e4371f3e 100644 --- a/frontend/src/styles/components/_design-system.scss +++ b/frontend/src/styles/components/_design-system.scss @@ -502,6 +502,9 @@ $sk-tones: ('accent', 'green', 'cyan', 'amber', 'red', 'violet'); &__value { display: contents; } } +// Desktop has the header row for sorting. +.sk-dtable__sortbar { display: none; } + // ---- card mode: rows become stacked cards under 640px -------------------- // The markup is unchanged (DataTable `responsive`, on by default), so sorting, // selection, grouping and the keyboard cursor all keep working. Opt out with @@ -514,10 +517,23 @@ $sk-tones: ('accent', 'green', 'cyan', 'amber', 'red', 'violet'); border-radius: 0; } + .sk-dtable__sortbar { + display: flex; + align-items: center; + gap: $space-2; + margin-bottom: $space-3; + + > [role='combobox'] { flex: 1 1 auto; min-width: 0; height: $control-lg; } + > button { flex: 0 0 auto; width: $control-lg; height: $control-lg; } + } + .sk-dtable--stack { &, thead, tbody { display: block; } - // Sortable headers become a scrolling chip row above the cards. + // Sorting lives in the .sk-dtable__sortbar above; only the select-all + // checkbox stays up here, as a chip. + thead:not(:has(.sk-dtable__check)) { display: none; } + thead tr { display: flex; gap: $space-2; @@ -539,7 +555,6 @@ $sk-tones: ('accent', 'green', 'cyan', 'amber', 'red', 'violet'); border-radius: $radius-full; background: var(--surface); - &.is-sortable, &.sk-dtable__check { display: inline-flex; } // Header menus are hover affordances; there is no hover on a phone.