Skip to content

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.

When this pattern applies

Primary triggers

All triggers

Specification body

Pattern — Admin data table

Status: v0.1.0 — Draft. Ships alongside the ratification of rfcs/design-RFC-008-pro-opinionated-wrappers.kmd Option 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-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 only original contribution — header (R3), table (R5), and footer (R6) delegate to primitives.

R3 — Page header

ElementRequiredNotes
BreadcrumbsRecommendedPer app shell convention; omit on root admin views
TitleYesResource collection name (e.g. "Repositories"); sentence case
Subtitle / descriptionOptionalOne line context; describes the dataset scope (e.g. "All repos visible to your account")
Primary action buttonRecommendedThe "create new" verb for this resource (e.g. "+ New repository"); top-right; primary tone per Verge
Secondary actionsOptionalOverflow 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:

SlotWidthContentSource spec
Search boxflex 1Per specs/components/index-filters.kmd §R1index-filters R1
Filter chip areaflex 2Active filter chips inlineindex-filters R2
Saved-views dropdownautoCaret + view nameindex-filters R4
"+ Add filter"autoOpens filter pickerindex-filters R1
Density toggleauto`comfortablecompact` icon group
Column visibilityautoIcon button → popover with column checkboxesthis spec §R4.2
ExportautoIcon 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-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 per-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 Material DataTable render 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.

Two-element row at the bottom of the table container:

SlotContent
Left"{N} of {total} rows shown" (counts respect filters)
RightPer-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 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 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:

SurfaceImplementation
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 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).
  • 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):

AdopterStatusTracking 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 consumeddrive#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 consumedhub#157 (done)
Koder Forge (Gitea fork — repo / issue / PR lists)not yet — fork-managed surface, separate strategy
(Future) Koder ID adminnot yet

References