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.
Pattern — Admin data table
*tatus* v0.1.0 — Draft. Ships alongside the ratification of
rfcs/design-RFC-008-pro-opinionated-wrappers.kmdOption C (20260523). 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 *ort + filter + select + bulk actions*ergonomics.
- The user is performing *dministrative 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 *ashboard with widgets*— use the dashboard
pattern (separate spec, future).
- The surface is a *ingle-record editor*— use a detail/form
layout instead.
- The surface is a *anding 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 *nly 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 *ntire 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
localStoragepertool + persaved-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 optout: products MAY hide individual export items viaconfig (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 *o behavioral overrides*to the primitive — what data-table.kmd specifies is what runs here. In particular:
- Bulk
action bar (datatable.kmd R3) appears in the *ame row*asthe toolbar (R4) when ≥ 1 row is selected — toolbar slides out, bulk
action bar slides in (200ms; prefersreduced-motion: instant). - Sticky header (data-table.kmd R4) pins below the toolbar (toolbar
itself is sticky-page if the admin shell wants it).
- Pagination/virtualization choice (data
table.kmd R5) is pertool;this pattern is agnostic.
- Inline edit (data
table.kmd R8) is pertool; this pattern neitherrequires 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 *nside 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 *ll 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: `datatable.kmd §R10` - Filter screen
reader: `indexfilters.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. |
Crosssurface parity is *wnercurated* 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 (transportagnostic). - 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 *hird Koder admin tool*adopts this pattern, open a followup ticket in `tools/designgenbacklog to re-open
rfcs/designRFC008proopinionated-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) | *dopted R4 + R4.1 |
drive#005 |
| Koder Hub publisher (DeveloperScreen) | *dopted R3 + R4.1 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 | — |