mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-10 05:53:05 +09:00
feat(frontend): sort select + direction toggle above stacked DataTable cards on phones
This commit is contained in:
1 parent
a44e3c2435
commit
a7ff0bf564
3 files changed
+62
-5
No files matched your search
@@ -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 && (
|
||||
<div className="sk-dtable__sortbar">
|
||||
<Select
|
||||
value={activeSort?.key ?? SORT_NONE}
|
||||
onValueChange={(key) => setSorts(key === SORT_NONE ? [] : [{ key, direction: activeSort?.direction ?? 'asc' }])}
|
||||
>
|
||||
<SelectTrigger aria-label={t('app.dataTable.sortBy', 'Sort by')}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={SORT_NONE}>{t('app.dataTable.defaultOrder', 'Default order')}</SelectItem>
|
||||
{sortColumns.map((column) => (
|
||||
<SelectItem key={column.key} value={column.key}>{translateLabel(t, column, 'header')}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SharedButton
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={!activeSort}
|
||||
aria-label={activeSort?.direction === 'desc'
|
||||
? t('app.dataTable.sortDescending', 'Sorted descending, switch to ascending')
|
||||
: t('app.dataTable.sortAscending', 'Sorted ascending, switch to descending')}
|
||||
onClick={() => setSorts([{ key: activeSort.key, direction: activeSort.direction === 'asc' ? 'desc' : 'asc' }])}
|
||||
>
|
||||
{activeSort?.direction === 'desc' ? <ArrowDownNarrowWide size={16} /> : <ArrowUpNarrowWide size={16} />}
|
||||
</SharedButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn('sk-dtable-wrap', className)} ref={wrapRef}>
|
||||
{sortBar}
|
||||
<Table className={cn('sk-dtable', responsive && 'sk-dtable--stack', tableClassName)}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user