Admin data table pattern
Canonical composition pattern for Koder admin tools that list typed records (Forge repo list, Kdrive folder browser, Hub publisher catalog, future Koder ID admin, etc.). Stacks the primitive `data-table` and `index-filters` together with a standard toolbar (density / column visibility / export) and page shell. Ratified by `rfcs/design-RFC-008-pro-opinionated-wrappers.kmd` as Option C (recipe pattern, not bundled Pro component) — every admin surface composes the primitives following this spec.
Status: v0.1.0 — Draft. Ships alongside the ratification of
rfcs/design-RFC-008-pro-opinionated-wrappers.kmdOption C (2026-05-23). Live URL once rendered:kds.koder.dev/<locale>/patterns/patterns-admin-data-table.html.
R1 — When to use
Use this pattern when:
- A Koder admin surface lists typed records (repos, files, users,
packages, certificates, …) with sort + filter + select + bulk actions ergonomics.
- The user is performing administrative tasks (managing the set),
not consuming individual records (which would call for a detail view).
- The surface lives in an admin shell with topbar + sidebar
navigation; this pattern fills the main content area.
Do NOT use this pattern when:
- The surface is a dashboard with widgets — use the dashboard
pattern (separate spec, future).
- The surface is a single-record editor — use a detail/form
layout instead.
- The surface is a landing page or marketing surface — landing
pages have their own specs under
specs/landing-pages/. - The dataset is < 50 stable rows and no filtering is needed — drop
the toolbar entirely; render
data-tableprimitive directly.
R2 — Composition
Vertical stack, top → bottom:
┌──────────────────────────────────────────────────────────┐
│ Page header │ ← R3
│ breadcrumbs · title · subtitle · primary action │
├──────────────────────────────────────────────────────────┤
│ Toolbar │ ← R4
│ search + filter chips · saved views · density · │
│ column menu · export · "+ Add filter" │
├──────────────────────────────────────────────────────────┤
│ │
│ Data table (primitive: specs/components/data-table.kmd) │ ← R5
│ sortable columns · multi-select · bulk-action bar │
│ sticky header · pagination/virtualization · rows │
│ │
├──────────────────────────────────────────────────────────┤
│ Footer (optional) │ ← R6
│ pagination summary · count · per-page selector │
└──────────────────────────────────────────────────────────┘The toolbar (R4) is this pattern's only original contribution — header (R3), table (R5), and footer (R6) delegate to primitives.
R3 — Page header
| Element | Required | Notes |
|---|---|---|
| Breadcrumbs | Recommended | Per app shell convention; omit on root admin views |
| Title | Yes | Resource collection name (e.g. "Repositories"); sentence case |
| Subtitle / description | Optional | One line context; describes the dataset scope (e.g. "All repos visible to your account") |
| Primary action button | Recommended | The "create new" verb for this resource (e.g. "+ New repository"); top-right; primary tone per Verge |
| Secondary actions | Optional | Overflow menu (︙) right of primary; "Import…", "Export all…", "Settings" |
Spacing: header pad = --kdr-spacing-6 top + bottom; separator rule between header and toolbar = 1px --kdr-border-muted.
R4 — Toolbar (the canonical bundle)
Single horizontal row, left → right:
| Slot | Width | Content | Source spec | |
|---|---|---|---|---|
| Search box | flex 1 | Per specs/components/index-filters.kmd §R1 |
index-filters R1 | |
| Filter chip area | flex 2 | Active filter chips inline | index-filters R2 | |
| Saved-views dropdown | auto | Caret + view name | index-filters R4 | |
| "+ Add filter" | auto | Opens filter picker | index-filters R1 | |
| Density toggle | auto | `comfortable | compact` icon group | this spec §R4.1 |
| Column visibility | auto | Icon button → popover with column checkboxes | this spec §R4.2 | |
| Export | auto | Icon button → menu (CSV / JSON / current view) | this spec §R4.3 |
The first four slots are the index-filters primitive composed in-place; the last three (density / columns / export) are this pattern's additions.
When the dataset has < 50 stable rows and no filters are wired, the entire toolbar is omitted and the data-table primitive renders flush with the page header.
R4.1 — Density toggle
- Two states:
comfortable(default) |compact. - Implementation: scales
--kdr-table-row-heightfrom48px→32px. - Icon group (segmented control) with two icons: standard rows / dense rows.
- Selection persisted to
localStorageper-tool (koder.<tool>.density). - Aria:
<div role="radiogroup" aria-label="Row density">with two<button role="radio" aria-checked>.
R4.2 — Column visibility menu
- Trigger: icon button (columns icon, aria-label "Show/hide columns").
- Popover content: vertical list of all columns with a checkbox each;
default-visible columns checked initially; "Reset to defaults" link at bottom.
- Hidden columns persist to
localStorageper-tool + per-saved-view(
koder.<tool>.<view>.columns-hidden). - Hidden columns also hidden from export (R4.3) — what you see is
what you export.
R4.3 — Export menu
- Trigger: icon button (download icon, aria-label "Export").
- Menu items (default):
Current view as CSV,Current view as JSON,separator,
All rows as CSV,All rows as JSON. - "Current view" honors active filters + sort + column visibility.
- "All rows" ignores filters; honors column visibility.
- Export triggered client-side via
Blob+ download; large exports(> 10k rows) confirm before generating ("This will export 47,329 rows. Continue?").
- Per-product opt-out: products MAY hide individual export items via
config (e.g. Kdrive admin hides JSON for security review).
R5 — Data table (delegated)
The table body delegates to specs/components/data-table.kmd in full. This pattern adds no behavioral overrides to the primitive — what data-table.kmd specifies is what runs here. In particular:
- Bulk-action bar (data-table.kmd R3) appears in the same row as
the toolbar (R4) when ≥ 1 row is selected — toolbar slides out, bulk-action bar slides in (200ms; prefers-reduced-motion: instant).
- Sticky header (data-table.kmd R4) pins below the toolbar (toolbar
itself is sticky-page if the admin shell wants it). Available since koder_kit 0.63.0 as opt-in
KoderDataTable.stickyHeader: true+tableHeight:(bounded viewport) — engaging it switches the primitive from the MaterialDataTablerender to the SDK-owned Sliver pipeline (data_table_sliver.dart); non-opt-in tables keep the Material render and its R10 web<table>semantics. a11y note: the Sliver path uses safe semantics (header/selected), not the strict ARIA table-role tree (koder_kit#073). - Expandable rows (data-table.kmd R7) available since koder_kit 0.63.0
via opt-in
KoderDataTable.expansionBuilder+expansionMode(accordionmultiple) +expandedonExpandedChanged. Same Sliver render foundation as R4. - Pagination/virtualization choice (data-table.kmd R5) is per-tool;
this pattern is agnostic.
- Inline edit (data-table.kmd R8) is per-tool; this pattern neither
requires nor forbids it.
R6 — Footer (optional)
Two-element row at the bottom of the table container:
| Slot | Content |
|---|---|
| Left | "{N} of {total} rows shown" (counts respect filters) |
| Right | Per-page selector (25 / 50 / 100) + pagination controls |
Omit the footer entirely when virtualization is on (no per-page concept) — the count moves into the page header subtitle in that case ("Repositories · 47,329 total").
R7 — Empty state
When the dataset (or current filter set) returns zero rows, render specs/patterns/empty-state.kmd inside the table container with appropriate copy:
- Dataset truly empty (no records exist for this account): "No
repositories yet" + illustration + primary action ("+ New repository").
- Filter set returned zero matches: "No repositories match these
filters" +
Clear filterssecondary action.
Empty state replaces table rows but does NOT replace the toolbar or header — they stay visible so the user can adjust filters or trigger the primary action.
R8 — Saved views integration
Saved views (index-filters.kmd §R4) capture all toolbar state: columns + sort + filters + density. Switching views updates all three regions in one navigation. URL serialization (index-filters R5) carries ?view=… plus any explicit overrides.
A view named Default is always present and not user-editable. New views are user-created via the "Save current as new view" CTA in the saved-views dropdown.
R9 — Keyboard and accessibility
Delegated to primitive specs:
- Table keyboard nav:
data-table.kmd §R9 - Filter / search keyboard nav:
index-filters.kmd §R6 - Table screen-reader semantics:
data-table.kmd §R10 - Filter screen-reader:
index-filters.kmd §R8
This pattern adds:
- Cmd/Ctrl + B → focus the column-visibility button.
- Cmd/Ctrl + E → open the export menu.
- Cmd/Ctrl + D → toggle density.
Per-tool customization MAY override these accelerators; defaults apply when no override is set.
R10 — Cross-surface guidance
The pattern lands on three surfaces with the same contract:
| Surface | Implementation |
|---|---|
| Flutter (mobiledesktopweb via koder_kit) | Compose KoderDataTable + KoderIndexFilters + this pattern's toolbar widgets in a Column / CustomScrollView per product layout. No KoderAdminTable widget ships — pattern is composition, not class. |
| Web (templ + HTMX) | Render the layout in a templ partial; toolbar widgets are templ components consuming the same Verge tokens. HTMX interactions (filter apply, export trigger) hit per-tool endpoints. |
| Web SDK (koderwebkit) | The <koder-data-table> and <koder-index-filters> web components compose; toolbar widgets are sibling <koder-density-toggle>, <koder-column-menu>, <koder-export-menu> web components — three new tickets when this pattern ships to web. |
Cross-surface parity is owner-curated, not structurally enforced. The pattern is the contract; per-surface implementations are audited against it (separate koder-spec-audit rule once a third adopter ships).
Não-escopo
- A bundled
KoderAdminTablewidget that wraps everything (wouldhave been Option B in RFC-008 — explicitly rejected by ratification).
- Per-product Pro variants (consumer concern; pattern sets contract only).
- Server-side data fetching, transport, caching (transport-agnostic).
- Auth / permissions / row-level access (separate spec; the pattern
assumes data already filtered by permission server-side).
- Cross-tab synchronization of saved views (out of scope; persistence
is local).
- Theming / branding overrides (delegated to Verge presets in
specs/themes/verge.kmd).
Re-evaluation trigger
When the third Koder admin tool adopts this pattern, open a follow-up ticket in tools/design-gen/backlog/ to re-open rfcs/design-RFC-008-pro-opinionated-wrappers.kmd and consider whether Option B (SDK helper bundle) or Option A (Pro spec set) now makes sense given concrete adopter evidence. Until then, this pattern is the contract.
Current adopters (update as products ship):
| Adopter | Status | Tracking ticket |
|---|---|---|
| Koder Drive (file browser) | adopted R3 + R4 + R4.1-R4.3 + R5 + R6 + R7 + R8 + R9 + R12 (R3 page header + R6 footer closed 2026-05-24) — first end-to-end pattern validation + KoderEmptyState + KoderSavedViewStore + KoderSkeletonTable; R3 = DrivePageHeader (folder title + item-count subtitle + "New folder" primary; breadcrumbs in PathBar); R6 = KoderDataTable built-in footer (page-size 50100200). Pattern fully consumed |
drive#005 (done) |
| Koder Hub publisher (DeveloperScreen) | adopted R3 + R4.1-R4.3 + R5 + R6 + R7 + R8 + R9 + R12 (2026-05-24) — view-switcher flavor (cards default + table mode) + R3 page header (title + subtitle "N apps · M downloads" + AppBar avatar-dropdown that opens profile dialog, replacing the prior full-width stats card) + KoderEmptyState + KoderSavedViewStore cross-widget restore + KoderSkeletonTable on loading + R6 pagination footer (auto-wired via koderkit's `PaginationFooter whenever rows.isNotEmpty + pageSize > 0; Hub passes pageSize: 25`). Pattern fully consumed |
hub#157 (done) |
| Koder Forge (Gitea fork — repo / issue / PR lists) | not yet — fork-managed surface, separate strategy | — |
| (Future) Koder ID admin | not yet | — |