Admin data table pattern
patterns specs/patterns/admin-data-table.kmd
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.
Quando este padrão se aplica
Triggers primários
- Author a new Koder admin list view
Todos os triggers
- Build a Koder admin surface that lists typed records with filters and bulk actions
- Audit an existing admin tool for parity with the canonical toolbar
- Decide how to wire data-table + index-filters + toolbar in a new product
Corpo da especificação
Pattern — Admin data table
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 |
| 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(accordion/multiple) +expanded/onExpandedChanged. 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 (mobile/desktop/web 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 (koder_web_kit) | 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 (would have 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 50/100/200). 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 koder_kit'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 | — |
Referências
specs/components/data-table.kmdspecs/components/index-filters.kmdspecs/patterns/empty-state.kmdspecs/themes/verge.kmdrfcs/design-RFC-008-pro-opinionated-wrappers.kmd