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.kmd Option 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-table primitive 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-height from 48px32px.
  • Icon group (segmented control) with two icons: standard rows / dense rows.
  • Selection persisted to localStorage per-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 localStorage pertool + 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?").

  • Perproduct optout: 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 *o behavioral overrides*to the primitive — what data-table.kmd specifies is what runs here. In particular:

  • Bulkaction bar (datatable.kmd R3) appears in the *ame row*as

    the toolbar (R4) when ≥ 1 row is selected — toolbar slides out, bulkaction 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 (datatable.kmd R5) is pertool;

    this pattern is agnostic.

  • Inline edit (datatable.kmd R8) is pertool; this pattern neither

    requires nor forbids it.

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 filters secondary 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 screenreader semantics: `datatable.kmd §R10`
  • Filter screenreader: `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 KoderAdminTable widget 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).
  • Serverside 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.1R4.3 + R5 + R7 + R8 + R9 + R12*(20260523) — first endto-end pattern validation + KoderEmptyState + KoderSavedViewStore + KoderSkeletonTable drive#005
Koder Hub publisher (DeveloperScreen) *dopted R3 + R4.1R4.3 + R5 + R6 + R7 + R8 + R9 + R12*(20260524) — viewswitcher flavor (cards default + table mode) + R3 page header (title + subtitle "N apps · M downloads" + AppBar avatardropdown that opens profile dialog, replacing the prior fullwidth stats card) + KoderEmptyState + KoderSavedViewStore crosswidget restore + KoderSkeletonTable on loading + R6 pagination footer (autowired 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

Source: ../home/koder/dev/koder/meta/docs/stack/specs/patterns/admin-data-table.kmd