[C07] Data Table (component)

Summary

C07 is a component spec, not a screen — distinct from every other entry in this catalogue. It is the canonical generic Data Table for the admin portal: the single source of truth for tabular UI, designed so every list page (E02 Event Participants, future Members, Affiliates, Results, Audit log, T02–T05 Number Stock, etc.) mounts <DataTable> rather than re-implementing rows, selection, and bulk chrome.

The bulk-engaged shell — "variant E" from the bulk toolbar exploration — is locked: 1.5px indigo border + 4px outer glow on the wrapper, plus a bonded DTBulkBar welded to the table’s top edge. Operators can never miss the actions; the actions are spatially welded to the rows they act on. Page chrome above the table (page header, stats strip, filter chips, utilities row) is never modified by the table — no layout jitter on enter/exit.

This .adoc is the persistent contract; the design at :design-url: is the visual reference. Implementation lands as a shared component in the admin-portal SPA (eventual home: @ems/shared-ui per WS4 of the admin-portal greenfield plan).

Actor & Context

Actor: indirect — operators interact with C07 via every list-page consumer; the component itself has no actor of its own. Frequency: every list-page render. Precondition: the consumer has a row dataset + a column declaration; the backend provides server-side sort/paginate where applicable (C07 is display-only at the substrate level). Entry point: mounted by the consumer screen as <DataTable> (or whichever stack-appropriate name lands in the implementation — the prop contract is the canonical part).

API Surface

Column shape

Field Type Purpose

key

string

Unique column id.

label

string

Header text.

width

number

Pixels (no flex-grow yet).

sort

'asc' | 'desc' (optional)

Sort indicator only — display-only at the C07 level. Caller owns sort logic.

num

boolean (optional)

Right-align numeric content.

wrap

boolean (optional)

Allow text wrap.

stopRowClick

boolean (optional, default true for cells with render)

Cell suppresses onRowClick. Opt out per column with stopRowClick: false.

render

(row) ⇒ ReactNode (optional)

Cell renderer. When omitted, the cell renders row[key] verbatim.

DataTable props

Prop Type Purpose

columns

Column[]

Column declaration (above).

rows

{id, …}[]

Row data — each row must carry an id.

selected

Set<id> (optional)

Selected row ids. Omit to disable selection entirely.

onSelect

(id) ⇒ void

Row checkbox click.

onSelectAll

() ⇒ void

Header checkbox click.

focusedRowId

id (optional)

Dim siblings to focus on this row.

onRowClick

(row) ⇒ void (optional)

Row click handler. Pointer cursor when set.

isRowInactive

(row) ⇒ bool (optional)

Strikethrough + faint treatment.

bulkMode

boolean

When true, render bulk bar + indigo border + outer glow.

bulkContext

string (optional)

Free-form context label, e.g. "across page 1".

bulkTotal

number (optional)

Drives the Select all N pill when current page selection < total.

bulkActions

Action[]

[{label, icon, danger?}] per bulk-bar button.

onClearBulk

() ⇒ void

Bulk-bar Clear button.

emptyState

ReactNode (optional)

Rendered when rows.length === 0.

Click-to-act mechanism

Cell-level clicks override the row-level click via the dt-stop class and stopRowClick flag:

  • Cells with a render function default to stopRowClick: true (most common case — the renderer wires its own click handler).

  • Cells with NO render function default to stopRowClick: false (the row click bubbles).

  • A column may explicitly set stopRowClick: false to opt back into row-click bubbling even when it has a renderer.

Internally, cells emit a dt-stop className that the row’s click handler checks via e.target.closest('.dt-stop') before firing onRowClick.

Visual Treatment

Default mode (no bulk chrome)

  • Wrapper: 1px neutral border, no glow.

  • Header row: 36px tall, off-white background #fbfbfc, uppercase label, 10.5px font, indigo chevron on sortable columns.

  • Row: 38px min-height, single-pixel divider between rows.

  • Hover: pointer cursor when onRowClick is set; row tint on hover.

  • Focused row: indigo-soft background UI.accentSoft; siblings dimmed.

  • Selected row: light-indigo background #f8f7ff.

  • Inactive row (per isRowInactive): strikethrough text + faint colour; treatment applies across all cells.

Bulk-engaged mode

  • Wrapper: 1.5px indigo border + 4px outer glow 0 0 0 4px rgba(79,70,229,0.10).

  • DTBulkBar bonded to the table’s top edge: indigo background, white type, ~11px x 14px padding.

    • Indeterminate checkbox (white-stroked) on the left.

    • <count> selected label, plus optional context label (· across page 1).

    • Select all N pill (background rgba(255,255,255,0.10)) when count < bulkTotal.

    • Action buttons right-aligned: white text, rgba(255,255,255,0.10) background, 1px translucent-white border. Danger actions outlined in soft red rgba(255,180,180,0.55) instead.

    • Clear button (Clear ✕) at the far right, button-less, separated by a thin vertical divider.

  • Header row + body rows: unchanged from default mode.

  • Selected rows: tint stays #f8f7ff (lighter than the bar) — the bar provides the selection-mode signal; row tint is gentle.

Out of Scope (today)

  • Column resize.

  • Column hide/show.

  • Sticky headers.

  • Server-side sort + paginate adapter (C07 is display-only at the substrate level; caller owns sort logic + renders pagination chrome outside the wrapper).

  • Side panels and modals — these are summoned by the caller via onRowClick, never owned by C07.

  • Touch / mobile responsive treatment.

  • Keyboard interaction (Space/Enter to select, Shift+click for range, arrow-key navigation).

These are tracked as Future Use Cases below where they have a known v2 trigger; otherwise they defer until a real consumer surfaces the need.

Design Anchors

  • Portal Pattern

  • UI Design Principles

  • design-journal/2026-04/jhipster-filterable-data-table.adoc — table substrate pattern (filter / sort / pagination shared layer); C07 is the visual substrate, the headless adapter is the data substrate. Eventual composition.

  • design-journal/2026-04/admin-portal-screen-design-prompt-iteration.adoc — broader handoff workflow.

  • E02 Event Participants — first canonical consumer; the design prototype was driven by E02’s column set.

  • C04 Reassignment Dialog — modal pattern that calling screens summon via onRowClick.

  • _explore-bulk-toolbar/Bulk Toolbar Variants.html (in the design bundle) — the placement exploration that picked variant E.

Design Decisions

  • Component, not screen (2026-04-30). C07 is the only entry in this catalogue that is a component spec rather than a screen use-case. The C-prefix is allocated because the catalogue’s namespace covers "anything cross-cutting"; component contracts qualify. Distinct from every other Cxx in two ways: no actor flow, no :design-url: of its own when accessed via a consumer screen.

  • Locked bulk treatment — variant E (2026-04-30). 1.5px indigo border + 4px outer glow + bonded DTBulkBar. Five placements were compared (header swap, neutral bar above filters, indigo bar above filters, floating dock, table-attached glow); E was chosen for the strongest spatial link between actions and rows.

  • Mode toggle = bulkMode prop (2026-04-30). Caller-controlled, not derived from row count. Page chrome above the table is never modified by C07 — no layout jitter on enter/exit. Calling screens decide when to engage bulk mode based on their own selection-count rules (e.g. E02 engages at ≥2 selected; another consumer might engage at ≥1).

  • Click-to-act cells are caller-owned (2026-04-30). Cell renderers handle their own clicks; C07 provides the dt-stop mechanism so row-click bubbling is suppressed cleanly. E02's four click levels (row → side panel; Category → category dialog; Primary Number → C04; Order chip → C06 modal/popover) are all expressible without C07 coupling.

  • Sort + pagination are display-only (2026-04-30). C07 renders the sort indicator chevron; the consumer owns sort state + the data fetch. C07 renders no pagination chrome at all; the consumer renders pagination outside the wrapper. Server-side adapters are deferred until a consumer needs them.

  • Select all N pill is caller-driven (2026-04-30). C07 renders the pill when bulkTotal is provided AND count < bulkTotal. The action behind the pill (calling a "select-all-matching" backend endpoint, expanding the selection set, etc.) is the consumer’s job. C07 just exposes the affordance.

  • Selected row tint distinct from bar (2026-04-30). Selected rows tint to #f8f7ff — lighter than the bonded indigo BulkBar #4f46e5. The bar provides the unmistakable mode signal; row tint is gentle so the table stays scannable.

  • Inactive treatment is row-wide (2026-04-30). isRowInactive returning true strikes through and fades the entire row, every cell. Click handlers (row click, cell-render handlers) stay wired — operators may need to interact with cancelled-order chips on inactive EPs, etc.

Consumers

Consumer Status

E02 Event Participants

First canonical consumer. The C07 design used E02’s column set as its prototype. E02 implementation (US #610 under Feature #608) will mount <DataTable> rather than re-implementing rows + selection chrome.

E04 Event Results

Future — list of races + results per race.

M02 Membership Members

Future.

T02, T03, T04, T05

Future tenant number/tag inventory — natural C07 consumers.

Future Audit Log screen

Future.

Future Affiliations list

Future.

Future Use Cases

Forward-looking. Not in v1. Captured here so the design intent isn’t lost and so v1 stubs in the right places (or doesn’t, where the absence is the intent).

FU-1 — Sticky table header

Long lists (E02 with 10k+ participants, E04 with thousands of results) need a sticky header row so the column titles stay visible during scroll. Out of v1 scope per the design. Trigger: first consumer that hits a "I lost track of what column I’m looking at" UX problem on real production data.

FU-2 — Server-side sort + paginate adapter

Today C07 is display-only on sort + has no pagination chrome at all. Consumers wire their own. A v2 adapter — declarative sort prop with caller-provided (column, direction) ⇒ void callback, plus a pagination slot or built-in chrome — would reduce per-consumer plumbing.

The headless filter substrate at design-journal/2026-04/jhipster-filterable-data-table.adoc already prototypes the JhipsterQueryAdapter (toApiQueryParams / toUrlQueryParams / fromRoute) with 35 unit tests. C07 v2’s server-side adapter should compose with that adapter rather than reinvent it.

FU-3 — Operator-controlled column visibility settings

Mirrors E02's FU-7 (captured there because E02 is the first screen to surface the need). Implementation belongs at the C07 level so every consumer benefits — settings live with the component substrate, not per-screen.

A "Column settings" affordance (gear icon / table header utility) lets the operator hide/reveal columns from the canonical set. Persisted per-user, per-screen.

Distinct from the structural displayListField mechanic (which hides a column when, e.g., no event has team-based registrations) — that’s structure-driven; FU-3 is operator-driven.

FU-4 — Column resize + column reorder

Power-user affordance. Hide behind a settings flag if/when surfaced.

FU-5 — Keyboard interaction

Space/Enter to select a focused row; Shift+click for range select; arrow keys to navigate rows. Accessibility-grade UX — defer until a screen-reader pass is on the roadmap.

FU-6 — Touch / mobile responsive treatment

Today’s design assumes desktop. Mobile / tablet operators (event-day check-in scenarios per the field-ops use cases) need a different layout — likely card-per-row with affordance-on-tap. Trigger: mobile-first screen surfaces in the roadmap.

FU-7 — Bulk-action overflow menu

The current design shows 4 bulk actions inline; what happens at 6, 8, 10? An overflow ⋯ menu pattern is the obvious answer but hasn’t been designed. Trigger: first consumer with > 5 bulk actions.

Notes

Active design iteration in progress. v1 of C07 is locked at the component-and-bulk-treatment level. v2 changes are listed under Future Use Cases above (and partially overlap with E02 FU-7). See admin-portal Screen Design Prompt Iteration for the broader handoff workflow.

v2 prompt — what to change in the next Claude Design pass

Captured here so the next iteration on C07 can pull these forward without re-deriving from chat memory:

  1. E02 column-set in the canonical prototype is stale. The C07 design’s participantColumns example renders a Secondary Number column. E02 v2 (post-2026-04-30 review) explicitly drops Secondary Number until US #478 schema pivot lands. v2 prompt should either drop the column from C07’s canonical example OR note the discrepancy with a comment. The C07 component is unaffected; only the example data binding shifts.

  2. Sort indicator — needs both directions. Today the design shows chevronDown on sortable columns. To express the "currently sorted column + direction" state, we need a chevronUp/chevronDown pair (matching the column’s sort: 'asc' | 'desc' value) plus a way for the consumer to communicate which column is currently active. v2 prompt should specify the active-sort visual treatment.

  3. Select all N semantics need a backend coordination call-out. The design says clicking the pill "selects every row matching current filters". Today’s /api/event-participants is paginated; the bulk action has to operate on a much larger set. v2 prompt should call out the contract: the consumer must provide a "fetch all matching IDs" path (likely a new lightweight /api/…​?ids-only=true endpoint family or equivalent), AND the bulk action must accept either a Set of IDs OR a filter expression. C07 doesn’t need to know — but the spec needs to say so.

  4. Sticky header trigger (FU-1) — the design defers this. v2 prompt should pick a row-count threshold (e.g. > 50 rows visible) above which sticky activates by default, OR a stickyHeader prop the consumer sets. Whichever ships, document the rationale.

  5. Filter/sort/pagination composition with the headless substrate. The journal entry on jhipster-filterable-data-table.adoc defines the ColumnConfig<T> shape with declarative filter+sort+visibility facets. C07’s column shape is currently {key, label, width, sort?, num?, wrap?, stopRowClick?, render?} — no filter dimension. v2 prompt should propose either: (a) extend C07’s column shape to include filter facet, OR (b) introduce a higher-level <FilterableDataTable> wrapper that bridges the headless adapter into C07’s prop surface. Pick one and explain.

  6. Column visibility settings UI (FU-3) — gear-icon affordance design needed: where does the gear sit, what does the panel look like, how does the operator set "default visible vs hidden", etc. This was raised on E02 FU-7 but the proper home is C07.

  7. Bulk-action overflow (FU-7) — when > 5 bulk actions, the inline layout breaks. v2 prompt should design the ⋯ overflow menu treatment.

  8. Keyboard + accessibility pass (FU-5) — Space/Enter selection, Shift+click range, arrow-key navigation, ARIA roles on row+cell. v2 prompt should run a focused a11y review.

  9. Empty state variants — today the emptyState: ReactNode is a single slot. The consumer-side reality has multiple distinct empties: "no data yet" (e.g. event with zero participants), "filter result empty" (data exists but filters narrowed it to nothing), "permission-empty" (rows exist but the operator can see none). Different copy, different CTAs (Import / Clear filters / contact admin). v2 prompt should specify either a richer slot (emptyStateForReason) or document that callers handle the discrimination upstream.

  10. Touch / mobile treatment (FU-6) — defer or design? Trigger likely arrives when the operator-on-tablet event-day flow is real.

These items are captured here rather than in the design-journal entry because they’re component-spec concerns, not iteration-process concerns. The design-journal entry’s Open Questions list pulls a one-line summary of the most active items when the next session is kicked off.