# Table & Component Standardization Plan ## 0. Motivation The app currently has **5 table implementations**, each hand-writing `` boilerplate from scratch. The shadcn-svelte `Table.*` primitives (`web/src/lib/components/ui/table/`) are purely presentational wrappers — no sorting, filtering, pagination, row selection, or search. Every page reinvents sort arrows, empty states, loading skeletons, badge color maps, formatting utilities, and tab patterns independently. **Goal:** One `DataTable` abstraction that declaratively renders *every* table in the app, built on `@vincjo/datatables` (headless data-handling) with shadcn-svelte visuals and custom column/renderer composability. **Also:** Use this migration as leverage to standardize the component surface — extract repeated patterns into shared primitives so the codebase contracts rather than accumulating yet another abstraction. --- ## 1. Audit Summary ### 1.1 Tables in the App | # | Page / Component | File | LOC | Features (what it has) | Gaps (what it's missing) | |---|---|---|---|---|---| | 1 | `EntityTable.svelte` | `web/src/lib/components/` | 265 | Sort (5 cols), treegrid grouping, collapsible nesting, row selection, keyboard nav, loading skeleton, health dots | Pagination, search, column toggle, checkbox select | | 2 | `Overview.svelte` | `web/src/pages/` | 125 | Filter pills (all/running/input/done/failed), sticky header, responsive cols, animated status dots | Plain `` (no shadcn), no sort, no pagination | | 3 | `Ops.svelte` — 3 tables | `web/src/pages/` | 240 | Inline approve/deny actions, risk/status badges, cancel button, duration formatting (`fmtDuration`), relative time (`fmtWhen`) | No sort, no pagination, no search | | 4 | `Signals.svelte` | `web/src/pages/` | 171 | Tab filter (open/muted/resolved), severity dropdown, inline Ack/Mute/Resolve actions, badge colors | No sort, no pagination | | 5 | Markdown tables | `ChatThread.svelte`, `EntityDetailContent.svelte` | CSS-only | Prose-styled `
` for AI output | No interactive features (by design) | ### 1.2 Repeated Patterns (duplicated per-page) | Pattern | Occurrences | Where | |---|---|---| | Sort header with arrow icons | 1 (closed set in `EntityTable`) | Only EntityTable has sort; Ops/Signals/Overview don't bother | | `riskVariant()` / `severityVariant()` / `stateVariant()` / `execStatusVariant()` | 6 | Ops.svelte ×2, Signals.svelte ×1, EntityTable.svelte ×2, Knowledge.svelte ×1 | | `fmtWhen()` / `relTime()` inline relative-time formatting | 3 | Ops.svelte, Knowledge.svelte (both inline; utils.ts has `relativeTime` already) | | ` > > > ` boilerplate | 6 | Every table page | | Empty state `No ...` | 6 | Every table page | | ` > > ` with badge counts | 2 | Ops.svelte, Signals.svelte | | Loading skeleton | 2 | EntityTable.svelte (custom widths), EntityDetailContent.svelte | ### 1.3 Current Tech Stack | Layer | What | Version | |---|---|---| | Framework | Svelte 5 (runes mode) | ^5.0.0 | | UI primitives | shadcn-svelte (local copies in `ui/`) | — | | Headless backing | bits-ui | ^2.18.1 | | CSS | Tailwind v4 (CSS-first config, no PostCSS) | ^4.3.2 | | Variant system | tailwind-variants | ^3.2.2 | | Icons | @lucide/svelte | ^1.23.0 | | Table library | **none** | — | --- ## 2. `@vincjo/datatables` — Why This Library **Headless.** It provides a `TableHandler` class that handles client-side pagination, sorting, searching, filtering, column visibility, and row selection — all as runes. Rendering is entirely up to us. This pairs perfectly with shadcn-svelte visual styling. **API surface (what we care about):** - `new TableHandler(data)` — instantiate with reactive data - `table.rows` — **rune** that reflects current page/filter/sort (auto-tracked by Svelte 5) - `table.rowCount`, `table.pageCount`, `table.currentPage`, `table.pages`, `table.pagesWithEllipsis` - `table.setRows(data)`, `table.setRowsPerPage(n)`, `table.setPage('next'|'previous'|int)` - `table.createSort()`, `table.createSearch()`, `table.createFilter()`, `table.createView()` - `table.select(id)`, `table.selectAll()`, `table.selected`, `table.isAllSelected` - `table.createCSV()`, `table.createCalculation()`, `table.createRecordFilter()` **No dependencies.** Lightweight. TypeScript-native. SSR friendly (even though we're SPA). ### What it does NOT do (and that's fine) - No rendering. We build the UI ourselves — use shadcn-svelte primitives. - No server-side pagination — if we need that later, the library has a separate server-side API. - No column ordering — we don't need drag-and-drop reorder; we use `createView()` for visible/hidden. --- ## 3. Architecture Plan ### 3.1 New Core Component: `DataTable.svelte` ``` web/src/lib/components/data-table/ ├── DataTable.svelte # The main table component ├── DataTable.svelte.ts # TypeScript type definitions ├── columns.ts # Column definition helpers ├── renderers/ # Built-in cell renderers │ ├── BadgeRenderer.svelte │ ├── HealthDotRenderer.svelte │ ├── RelativeTimeRenderer.svelte │ └── DateRenderer.svelte ├── pagination/ # Pagination UI │ ├── Pagination.svelte │ ├── PageButton.svelte │ └── RowsPerPage.svelte ├── sort-header.svelte # Sortable column header with arrow icons ├── search-input.svelte # Text search input └── toolbar.svelte # Top toolbar (search + filter + page size) ``` ### 3.2 `DataTable` API (declarative, Svelte 5 runes) ```svelte ``` ### 3.3 Column System A `DataTableColumn` is: ```typescript type ColumnRenderer = | 'badge' // wraps value in | 'health-dot' // colored dot + relative time | 'relative-time' // relativeTime(val) | 'date' // new Date(val).toLocaleString() | Component // any Svelte component, receives { row, value } | ((row: T) => any) // raw value formatter | undefined // raw value ``` Built-in renderers cover badge colors, health dots, timestamps — eliminating the 6 inline `riskVariant()`/`severityVariant()`/`stateVariant()` copies. Custom components cover action buttons and complex cells. ### 3.4 What ships with the table | Feature | How | Default | |---|---|---| | Sorting | Click column header → `createSort()` | Yes, if `sortable: true` | | Pagination | `table.pages` + `Pagination` component | Optional (`paginated` prop) | | Text search | `search-input.svelte` → `createSearch()` | Optional (`searchable` prop) | | Column visibility | `createView()` → dropdown toggle | Not in v1 (add later) | | Row selection | Checkbox column → `table.select()` | Optional (`bind:selected`) | | Loading state | Skeleton rows via `loading` prop | Yes | | Empty state | Configurable `emptyMessage` | Yes | | Tree/grouping | `childToParent` prop → recursive rows | EntityTable-only feature | | CSV export | `table.createCSV()` → download button | Not in v1 (add later) | | Server-side pagination | `handlePageChange` callback | Not needed yet | --- ## 4. Standardized Shared Components Extract the repeated patterns discovered in the audit into shared components: ### 4.1 `StatusBadge.svelte` **Replaces:** 6 copies of `riskVariant()`, `severityVariant()`, `stateVariant()`, `execStatusVariant()` ```svelte ``` ### 4.2 `EmptyState.svelte` **Replaces:** 6 `No ...` blocks ```svelte ``` ### 4.3 `RelativeTime.svelte` **Replaces:** `Oks.svelte:58` (`fmtWhen`), `Knowledge.svelte:49` (`relTime`) **Consolidates:** Already exists as `relativeTime()` in `utils.ts` — wrap in a component that auto-updates. ### 4.4 `FilterTabs.svelte` **Replaces:** `Ops.svelte:114-120` and `Signals.svelte:153-159` (Tabs.Root boilerplate with badge counts) ```svelte ``` ### 4.5 `PageHeader.svelte` **Replaces:** Every page's `

...

` + optional actions row. --- ## 5. Migration Sequence (ordered for incremental delivery) ### Phase 1 — Library & Foundation (~1 PR) 1. **Install `@vincjo/datatables`** ``` npm install -D @vincjo/datatables ``` 2. **Build `DataTable.svelte` + `DataTable.svelte.ts` + `columns.ts`** - Core loop: `{#each table.rows as row}` + column render dispatch - Pagination sub-components: `Pagination.svelte`, `PageButton.svelte`, `RowsPerPage.svelte` - `SortHeader.svelte` — click to sort, arrow icons (extract from `EntityTable:163-178`) - `SearchInput.svelte` — debounced text search 3. **Build renderers:** `BadgeRenderer.svelte`, `HealthDotRenderer.svelte`, `RelativeTimeRenderer.svelte`, `DateRenderer.svelte` 4. **Build `EmptyState.svelte`** 5. **Unit tests** for `DataTable` column dispatch, sort, pagination, selection. ### Phase 2 — Simple Tables (no tree, no actions) (~1 PR) 6. **Migrate `Overview.svelte` (task board)** - Plain `
` → `DataTable` with `StatusBadge`, `RelativeTime`, filter pills external - Drop sticky-header CSS (`DataTable` handles it) - Verify: filter pills, status dots, responsive summary column, click-to-open 7. **Migrate `Signals.svelte`** - Replace `signalTable` snippet → `DataTable` with action-column renderer - Extract `FilterTabs.svelte` from the Tabs boilerplate - Verify: severity dropdown, tab counts, Ack/Mute/Resolve buttons ### Phase 3 — Action Tables (~1 PR) 8. **Migrate `Ops.svelte` — Pending Approvals** - Approve/Deny buttons as action column renderer - Risk badge via `StatusBadge kind="risk"` 9. **Migrate `Ops.svelte` — Decided Approvals** - Same columns, no actions 10. **Migrate `Ops.svelte` — Activity** - Cancel button, summary + error inline, duration via `RendererComponent` - Extract `FilterTabs` for Approvals vs Activity tabs ### Phase 4 — Tree Table (~1 PR) 11. **Migrate `EntityTable.svelte`** - Treegrid grouping is the hard part. Build a `TreeTable` variant or a `grouped` prop. - `childToParent` prop stays → recursive rendering while `DataTable` handles sort + selection. - **Alternative:** Ship `treegrid` as a separate `TreeDataTable.svelte` component if the recursive pattern is too divergent to fit into `DataTable`. ### Phase 5 — Cleanup & Standardization (~1 PR) 12. **Extract shared components everywhere:** - Audit every `.svelte` file for inline `riskVariant()` / `severityVariant()` / `fmtWhen()` — replace with `StatusBadge`, `RelativeTime` - Audit for inline `` boilerplate — replace with `FilterTabs` - Audit for `` with inline logic — consolidate 13. **Remove deprecated shadcn-svelte table primitives** after confirming nothing else imports them. 14. **Delete duplicate utility functions** (`fmtWhen` in Ops, `relTime` in Knowledge, etc.) ### Phase 6 — Polish (~1 PR) 15. **Column visibility toggle** (optional) 16. **CSV export** for entity tables (optional) 17. **Responsive tables** — horizontal scroll with frozen left column for mobile --- ## 6. Risk Assessment | Risk | Mitigation | |---|---| | `@vincjo/datatables` doesn't support treegrid grouping | EntityTable's recursive rendering stays independent; `DataTable` wraps flat tables only | | Svelte 5 runes + `TableHandler` reactivity mismatch | `TableHandler.rows` is a rune. Wrap in `$derived` or `$effect` to feed `data` prop → `table.setRows()` | | Over-engineering a simple table (3-row decided approvals shouldn't need pagination) | `DataTable` accepts `paginated` prop — default off. Small tables stay simple. | | Treegrid migration breaks KB browser | Phase 4 is isolated. Phases 1–3 deliver value before touching the critical KB table. | --- ## 7. Success Criteria 1. **Every ``** in the app routes through `DataTable.svelte` 2. **0** copies of inline `riskVariant()` / `severityVariant()` / `stateVariant()` — all through `StatusBadge` 3. **0** copies of inline `fmtWhen()` / `relTime()` — all through `RelativeTime` or `utils.relativeTime` 4. **0** copies of manual `No ...` — all through `EmptyState` 5. **`web/src/lib/components/ui/table/`** retained for `DataTable` internals only (or removed if unused) 6. **TypeScript compiles** with `--noEmit` and **tests pass** (`vitest run`) 7. **All existing features preserved**: sort, tree expand/collapse, tab filters, severity dropdown, approve/deny/cancel/ack/resolve buttons, sticky headers, loading skeletons, health dots, empty states --- ## 8. File Manifest (what gets created / modified / deleted) ### Created ``` plan/tables.md ← this file web/src/lib/components/data-table/DataTable.svelte web/src/lib/components/data-table/DataTable.svelte.ts web/src/lib/components/data-table/columns.ts web/src/lib/components/data-table/columns.test.ts web/src/lib/components/data-table/renderers/BadgeRenderer.svelte web/src/lib/components/data-table/renderers/HealthDotRenderer.svelte web/src/lib/components/data-table/renderers/RelativeTimeRenderer.svelte web/src/lib/components/data-table/renderers/DateRenderer.svelte web/src/lib/components/data-table/pagination/Pagination.svelte web/src/lib/components/data-table/pagination/PageButton.svelte web/src/lib/components/data-table/pagination/RowsPerPage.svelte web/src/lib/components/data-table/sort-header.svelte web/src/lib/components/data-table/search-input.svelte web/src/lib/components/data-table/toolbar.svelte web/src/lib/components/StatusBadge.svelte web/src/lib/components/EmptyState.svelte web/src/lib/components/RelativeTime.svelte web/src/lib/components/FilterTabs.svelte web/src/lib/components/PageHeader.svelte ``` ### Modified (in migration order) ``` web/package.json ← add @vincjo/datatables web/src/pages/Overview.svelte ← Phase 2 web/src/pages/Signals.svelte ← Phase 2 web/src/pages/Ops.svelte ← Phase 3 web/src/lib/components/EntityTable.svelte ← Phase 4 web/src/pages/KnowledgeBase.svelte ← Phase 4 (consumer of EntityTable) web/src/pages/Knowledge.svelte ← Phase 5 (remove relTime) ``` ### Potentially Removed (Phase 5) ``` web/src/lib/components/ui/table/* ← if DataTable is the sole consumer (These stay if DataTable still uses them internally for rendering) ``` --- ## 9. Implementation Status ### Completed (2026-07-21) | Phase | Task | Status | |---|---|---| | 1 | Install `@vincjo/datatables` | Done | | 1 | `DataTable.svelte` core component | Done | | 1 | Types (`DataTable.svelte.ts`, `columns.ts`) | Done | | 1 | Pagination (`Pagination`, `PageButton`, `RowsPerPage`) | Done | | 1 | Sort header, search input, toolbar | Done | | 1 | Built-in renderers: `BadgeRenderer`, `HealthDotRenderer`, `RelativeTimeRenderer`, `DateRenderer`, `RiskBadgeRenderer`, `ExecutionStatusRenderer`, `DurationRenderer`, `StatusDotRenderer` | Done | | 1 | `EmptyState.svelte` shared component | Done | | 2 | Migrate `Overview.svelte` to `DataTable` | Done | | 2 | Migrate `Signals.svelte` to `DataTable` | Done | | 3 | Migrate `Ops.svelte` (3 tables) to `DataTable` | Done | | 4 | Refactor `EntityTable.svelte` to use shared {SortHeader, EmptyState, HealthDotRenderer} | Done | | 5 | Create `StatusBadge.svelte` (consolidates risk/severity/execution-type variant maps) | Done | | 5 | Create `FilterTabs.svelte` component | Done | | 5 | Clean up `Knowledge.svelte`: replace inline `relTime()` → `relativeTime()`, `typeVariant()` → `StatusBadge` | Done | ### Key Decisions Made During Implementation - **EntityTable treegrid NOT migrated to DataTable**. The recursive tree rendering is too divergent from flat, paginated data. Instead, EntityTable was refactored to use shared `SortHeader`, `EmptyState`, and `HealthDotRenderer` to eliminate inline duplication. - **`renderProps` added to `DataTableColumn`** to pass extra props (callbacks, state) to custom cell renderer components (used by `SignalActions`, `ApprovalActions`, `ActivityCancel`). - **`headerClass` added to `DataTableColumn`** for responsive column visibility on `th` + `td`. - **`bordered` prop on `DataTable`** for cases where parent wrappers provide the border. - **`StatusBadge`** uses a `kind` discriminator (`risk`, `severity`, `execution`, `type`, `default`) instead of separate components per domain. - **`FilterTabs`** created but not yet wired into Ops/Signals — those pages still use inline `` for the approvals/activity and open/muted/resolved tabs. ### Remaining (Phase 6 — Future PR) - Wire `FilterTabs` into Ops.svelte and Signals.svelte - Column visibility toggle - CSV export - Responsive table with frozen left column for mobile