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.