List page (bespoke DataTable)
Slice 1 of external-cli-and-collapse-shell retired the saved-views meta-schema and the generic <ViewRenderer> runtime. Lists are now per-module with bespoke <DataTable> pages — every page owns its columns, filters, query keys, and server endpoint. The five filter pills, three sort options, and two columns the page uses are checked into source as React; nothing is reconciled at boot, nothing is reflected through a generic table renderer.
When to apply
- Any new top-level list route (
/contacts, /messaging, /agents, /schedules, …).
- Replacing a card-list, ad-hoc grid, or
<ViewRenderer> callsite with a structured table.
Skip when the underlying data is genuinely small + static (≤20 rows, no filters) — a plain unordered list or <table> with RelativeTimeCard is fine.
Canonical example
packages/template/modules/contacts/pages/index.tsx is the reference implementation. Read that file first; this skill summarizes the load-bearing parts.
Stack
- DataTable + DataTableToolbar from DiceUI (
bunx shadcn@latest add "https://diceui.com/r/data-table.json")
useDataTable owns URL state via nuqs (?page=2&filters[segments]=qualified,vip&sort=createdAt.desc)
- TanStack Query keys are derived from the URL params, so URL change ⇒ query refetch
- Module-typed Hono RPC (
contactsClient, messagingClient, …) for row fetches; never raw fetch in src/** (banned by no-raw-fetch.grit)
<RelativeTimeCard date={...}> for any date column — auto-updating, i18n-safe, hover for full timestamp
Server side
A list endpoint receives the URL params, validates with Zod, runs filters through Drizzle, and returns { rows, pageCount }. Use filterColumns() from the data-table skill's reference if you need full server-side filtering; for simple lists, hand-roll the predicates.
Filter rails
Hardcoded React arrays — not metadata. Slice 1's <InboxFilterRail> precedent: Active | Later | Done baked into the component, no DB lookup, no saved-view round-trip. New filter? Edit the source.
What to avoid
- Don't introduce a generic
<ViewRenderer scope="…"> again. Pages own their tables.
- Don't reach for a YAML view definition or
defineViewable() — both are gone.
- Don't write
new Date(value).toLocaleString() — wrap with <RelativeTimeCard date={value} />.
- Don't bypass the typed RPC client; a raw
fetch('/api/…') will fail the check:bundle lint and produce untyped responses.
1---2name: list-page3description: Bespoke per-module list page pattern using DiceUI DataTable + useDataTable + nuqs URL state + RelativeTimeCard for dates + module-typed Hono RPC for rows. Use this skill when adding a new top-level list page (e.g., `/widgets`, `/orders`) inside a vobase module, or when refactoring an old free-form list view into the standard data-table layout. Also use when the user says "list page", "make a table for X", "data table for module Y", "filter pills for the list", or "URL-shareable filters".4---56# List page (bespoke DataTable)78Slice 1 of `external-cli-and-collapse-shell` retired the saved-views meta-schema and the generic `<ViewRenderer>` runtime. Lists are now per-module with bespoke `<DataTable>` pages — every page owns its columns, filters, query keys, and server endpoint. The five filter pills, three sort options, and two columns the page uses are checked into source as React; nothing is reconciled at boot, nothing is reflected through a generic table renderer.910## When to apply1112- Any new top-level list route (`/contacts`, `/messaging`, `/agents`, `/schedules`, …).13- Replacing a card-list, ad-hoc grid, or `<ViewRenderer>` callsite with a structured table.1415Skip when the underlying data is genuinely small + static (≤20 rows, no filters) — a plain unordered list or `<table>` with `RelativeTimeCard` is fine.1617## Canonical example1819`packages/template/modules/contacts/pages/index.tsx` is the reference implementation. Read that file first; this skill summarizes the load-bearing parts.2021## Stack2223- **DataTable + DataTableToolbar** from DiceUI (`bunx shadcn@latest add "https://diceui.com/r/data-table.json"`)24- **`useDataTable`** owns URL state via nuqs (`?page=2&filters[segments]=qualified,vip&sort=createdAt.desc`)25- **TanStack Query** keys are derived from the URL params, so URL change ⇒ query refetch26- **Module-typed Hono RPC** (`contactsClient`, `messagingClient`, …) for row fetches; **never** raw `fetch` in `src/**` (banned by `no-raw-fetch.grit`)27- **`<RelativeTimeCard date={...}>`** for any date column — auto-updating, i18n-safe, hover for full timestamp2829## Server side3031A list endpoint receives the URL params, validates with Zod, runs filters through Drizzle, and returns `{ rows, pageCount }`. Use `filterColumns()` from the `data-table` skill's reference if you need full server-side filtering; for simple lists, hand-roll the predicates.3233## Filter rails3435Hardcoded React arrays — not metadata. Slice 1's `<InboxFilterRail>` precedent: `Active | Later | Done` baked into the component, no DB lookup, no saved-view round-trip. New filter? Edit the source.3637## What to avoid3839- Don't introduce a generic `<ViewRenderer scope="…">` again. Pages own their tables.40- Don't reach for a YAML view definition or `defineViewable()` — both are gone.41- Don't write `new Date(value).toLocaleString()` — wrap with `<RelativeTimeCard date={value} />`.42- Don't bypass the typed RPC client; a raw `fetch('/api/…')` will fail the `check:bundle` lint and produce untyped responses.