Problem
The shipped DataTable from #105 is a thin Mantine Table wrapper: striped rows, optional toolbar slot, no column sorting, no sticky header, no row selection, and no built-in search/sort/column-picker affordances.
List behaviour is inconsistent and external to the table:
| List route |
Filtering / sort |
Column visibility |
| Channels |
Band, mode, name, distance filters + sort modes in ChannelsListSectionNav via useChannelListQuery |
Optional columns in section nav via useChannelListColumns + localStorage |
| Zones, Contacts, Talk groups, RX group lists |
Name filter only (useListNameQuery + ?q=) applied in the route before passing rows |
Fixed columns — no picker |
| Detail pages |
— |
Embedded DataTable for reference lists (members, usage counts) — read-only |
Operators get a functional spreadsheet view but cannot sort by clicking headers, scroll long lists without losing column context, or use a consistent filter/column toolbar across entity lists. Channel-specific logic will not scale as lists grow (more optional columns, bulk actions, export selections).
Intended outcome
Replace the naive table with a standardised list datatable aligned with Mantine UI table patterns (sticky header, sortable columns, search, optional multi-select — adopt other recipes from that category where useful).
Core DataTable capabilities
| Feature |
Notes |
| Sticky header |
Header row stays visible inside scroll area on long lists |
| Column sort |
Click header to sort asc/desc; support custom comparators per column (e.g. MHz numeric, name locale, distance when present) |
| Search / filter slot |
Built-in toolbar region: global text search or wired external filter state (channels keep richer filters in section nav; simpler lists get in-table search) |
| Column show/hide |
Standardised optional-column picker (persist per list in localStorage); generalise channel-only CHANNEL_OPTIONAL_COLUMNS pattern |
| Row selection (optional) |
Multi-select checkbox column for future bulk actions; off by default on read-only embedded tables |
| Empty state |
Keep existing EmptyState; respect filtered-empty vs truly-empty copy |
| Mobile |
Implement or wire mobileColumnPolicy — coordinate with #68 (collapse name/band/mode, merge RX/TX) |
API shape (sketch — refine in PR)
Evolve DataTableColumn to carry sortability, default visibility, and comparator:
interface DataTableColumn<T> {
key: string;
header: string;
render: (row: T) => ReactNode;
sortable?: boolean;
sortValue?: (row: T) => string | number | null;
defaultVisible?: boolean;
hideable?: boolean;
}
Toolbar composition: DataTable owns layout (search input, column multi-select, result count); routes/section nav supply filter state via props or render props rather than reimplementing controls ad hoc.
Implementation options (pick in PR — not prescriptive):
Either way: one table abstraction in src/components/ui/; detail-page embedded tables use the same primitive with selectable={false} and fewer toolbar features.
Adoption — entity list pages
Migrate primary list routes to the new table + toolbar contract:
Follow-up (optional): UkRepeaterSearch uses raw Mantine Table — adopt kit table if selection/sort patterns align.
Affected
src/components/ui/DataTable.tsx (+ tests)
src/hooks/useChannelListColumns.ts, channelListQueryUtils.ts — generalise or delegate to table
src/components/SectionNav/sections/ChannelsListSectionNav.tsx — dedupe column picker / search if moved into table toolbar
src/hooks/useListNameQuery.ts — may merge into shared list-query hook or table-controlled search
docs/features/ui/README.md — document datatable contract
Related
Out of scope (initial slice)
- Server-side pagination (all data is in-memory LocalStorage today)
- Bulk delete/edit actions (selection column is groundwork only unless a small win is trivial)
- Virtualised rows for 10k+ channels (measure first; add follow-up if needed)
- Replacing
BandPlanTable / non-entity reference tables
Manual verify
- Each entity list: click column headers to sort; order toggles asc/desc; numeric columns sort numerically.
- Sticky header remains visible when scrolling 50+ rows.
- Channels: optional columns toggle from table toolbar; choice persists across reload.
- Contacts/zones/etc.: name search works via table toolbar or existing
?q= (document chosen behaviour).
- Styleguide demos sort + selection + column picker.
- Mobile / narrow viewport: #68 collapse behaviour or acceptable horizontal scroll documented.
npm run lint && npm run test && npm run build.
Workflow note
Phased PR(s) acceptable — e.g. (1) core DataTable + styleguide, (2) migrate simple lists, (3) channels + section-nav dedupe + #68 mobile. Branch from origin/main, atomic conventional commits per slice, link Closes #.
Problem
The shipped
DataTablefrom #105 is a thin MantineTablewrapper: striped rows, optionaltoolbarslot, no column sorting, no sticky header, no row selection, and no built-in search/sort/column-picker affordances.List behaviour is inconsistent and external to the table:
ChannelsListSectionNavviauseChannelListQueryuseChannelListColumns+ localStorageuseListNameQuery+?q=) applied in the route before passing rowsDataTablefor reference lists (members, usage counts) — read-onlyOperators get a functional spreadsheet view but cannot sort by clicking headers, scroll long lists without losing column context, or use a consistent filter/column toolbar across entity lists. Channel-specific logic will not scale as lists grow (more optional columns, bulk actions, export selections).
Intended outcome
Replace the naive table with a standardised list datatable aligned with Mantine UI table patterns (sticky header, sortable columns, search, optional multi-select — adopt other recipes from that category where useful).
Core
DataTablecapabilitiesCHANNEL_OPTIONAL_COLUMNSpatternEmptyState; respect filtered-empty vs truly-empty copymobileColumnPolicy— coordinate with #68 (collapse name/band/mode, merge RX/TX)API shape (sketch — refine in PR)
Evolve
DataTableColumnto carry sortability, default visibility, and comparator:Toolbar composition:
DataTableowns layout (search input, column multi-select, result count); routes/section nav supply filter state via props or render props rather than reimplementing controls ad hoc.Implementation options (pick in PR — not prescriptive):
Table+ScrollAreafollowing ui.mantine.dev table recipesmantine-datatableif it fits our column/slot needs without fighting HashRouter / link cellsEither way: one table abstraction in
src/components/ui/; detail-page embedded tables use the same primitive withselectable={false}and fewer toolbar features.Adoption — entity list pages
Migrate primary list routes to the new table + toolbar contract:
src/routes/channels/list.tsx— fold optional-column picker into table toolbar; keep distance/band/mode filters in section nav or consolidate if UX improvessrc/routes/zones/list.tsxsrc/routes/ContactsList.tsxsrc/routes/TalkGroupsList.tsxsrc/routes/RxGroupListsList.tsxsrc/routes/styleguide.tsx— demonstrate sort, sticky header, selection, column picker, empty/filtered-emptyFollow-up (optional):
UkRepeaterSearchuses raw MantineTable— adopt kit table if selection/sort patterns align.Affected
src/components/ui/DataTable.tsx(+ tests)src/hooks/useChannelListColumns.ts,channelListQueryUtils.ts— generalise or delegate to tablesrc/components/SectionNav/sections/ChannelsListSectionNav.tsx— dedupe column picker / search if moved into table toolbarsrc/hooks/useListNameQuery.ts— may merge into shared list-query hook or table-controlled searchdocs/features/ui/README.md— document datatable contractRelated
DataTable; this ticket is the datatable depth passmobileColumnPolicyor close togetherOut of scope (initial slice)
BandPlanTable/ non-entity reference tablesManual verify
?q=(document chosen behaviour).npm run lint && npm run test && npm run build.Workflow note
Phased PR(s) acceptable — e.g. (1) core
DataTable+ styleguide, (2) migrate simple lists, (3) channels + section-nav dedupe + #68 mobile. Branch fromorigin/main, atomic conventional commits per slice, linkCloses #.