Skip to content

chore: shadcn DataTable proof-of-concept (2 consumers) - #132

Open
iacapuca wants to merge 2 commits into
chore/tanstack-table-migrationfrom
chore/shadcn-datatable-poc
Open

chore: shadcn DataTable proof-of-concept (2 consumers)#132
iacapuca wants to merge 2 commits into
chore/tanstack-table-migrationfrom
chore/shadcn-datatable-poc

Conversation

@iacapuca

@iacapuca iacapuca commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduces a shadcn/ui-style generic <DataTable> alongside the existing monolithic <Table>, and migrates two simple consumers to prove the pattern works end-to-end. The remaining six consumers stay on the monolithic component for now — this PR is a beachhead, not a full conversion.

Base: this PR stacks on chore/tanstack-table-migration (PR #131) — which adds @tanstack/react-table. Merge #131 first, then this one (or retarget to main after #131 lands).

Why

Answering the "why are there so many `any`s in Table.tsx" question: because the monolithic component is generic-shaped but not generic-parameterized. shadcn's recipe — typed primitives + consumer-owned useReactTable call — puts the generic parameter at the consumer level, where it actually has meaning. That's the pattern this POC sets up.

Infrastructure (new)

  • components.json — shadcn CLI config, root of repo. Aliases configured for ~/components, ~/lib, ~/components/ui.
  • app/lib/utils.ts — the standard cn() helper (clsx + tailwind-merge).
  • app/components/ui/table.tsx — unstyled Table primitives: Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption. Thin `div`/`table`/`tr`/`td`… wrappers with Tailwind classes. No CSS-variable theming — uses concrete Tailwind classes that match the existing project aesthetic. (Adopting shadcn's full HSL-based theming is a bigger migration we can tackle separately.)
  • app/components/ui/data-table.tsx — generic <DataTable<TData, TValue>> with sorting + pagination + optional rowClassName(row). ~150 lines total. Consumers pass typed ColumnDef<TData>[] and get type safety all the way down.

Deps added: class-variance-authority, clsx, tailwind-merge (3 packages).

Proof-of-concept consumer migrations

File Before After
app/components/Samu/SamuClientSide.tsx (cities list) <Table> with inline untyped columns={[…]} <DataTable columns={citiesTableColumns} data={…} /> with a typed CityRow interface
app/components/ExecucaoCicloviaria/CityContent.tsx (PDC structures) <Table> with inline untyped columns={[…]} + showFilters={true} <DataTable columns={pdcColumns} data={…} /> with a typed PdcRelation interface

Both consumers now:

  • Own their table's title/header as an <h3> above the DataTable (composition, not a prop).
  • Get real TypeScript autocomplete on row.original.* — no more any.
  • Lose the per-column filter toggle UI. For these two simple tables, that's an acceptable trade — neither's columns had custom filter components. If filter UX is wanted back, it'd be an <input> above the DataTable with table.getColumn("x").setFilterValue(...).

Explicitly not touched

  • The monolithic app/components/Commom/Table/Table.tsx stays in place. The other six consumers still use it.
  • CountsTable, CountingComparisionTable, IdecicloTable, ViasRankingTable, dados.viasinseguras.\$slug sinistros table, and dados.loa.index — each a separate follow-up PR. The LOA page will be the hardest (filterType pills, allColumns expansion, classifyAction row coloring, column-sum headers).
  • Existing Tailwind theme — no CSS variables introduced.
  • No Radix dependency added (we didn't add Select primitives yet).

Verification

  • npm install — 3 new packages
  • npm run typecheck — 301 pre-existing errors on base; zero new errors introduced
  • npm run build — clean in 8.4s

Test plan

  • /dados/samu — scroll to "Lista completa das cidades" table. Sort headers should work (click Ranking, click Município), pagination buttons should appear if > 10 rows. No filter UI (expected).
  • /dados/execucaocicloviaria → select a city → scroll to "Estruturas do PDC para {city}" table. Same checks.
  • Visually compare against previous styling: border/shadow, row alternation, header styling should look basically identical.

Follow-up roadmap

Once this proves out, the staged migration:

  1. Simpler tables: dados.viasinseguras.\$slug (inline sinistros), CountsTable, CountingComparisionTable — ~1-2 hours each
  2. Mid complexity: IdecicloTable, ViasRankingTable — feature-richer, custom filter components to re-home
  3. Hard: dados.loa.index — half a day on its own
  4. Delete Commom/Table/Table.tsx + TableFilters.tsx

Total remaining work: ~1.5-2 days spread across 3-4 PRs.

🤖 Generated with Claude Code

iacapuca and others added 2 commits April 23, 2026 13:44
Introduces a shadcn/ui-style generic DataTable alongside the existing
monolithic <Table>, and migrates two simple consumers to prove the
pattern. The rest continue using the monolithic component until a
follow-up PR.

Stacks on top of PR #131 (TanStack Table v8 migration) — which is
required for @tanstack/react-table to exist.

Infrastructure (all new files):
- components.json: shadcn CLI config at repo root
- app/lib/utils.ts: cn() helper (clsx + tailwind-merge)
- app/components/ui/table.tsx: unstyled Table primitives
  (Table/Header/Body/Row/Head/Cell/Caption) as thin Tailwind
  wrappers, no CSS-variable theming — uses concrete classes that
  match the existing project aesthetic
- app/components/ui/data-table.tsx: generic DataTable<TData, TValue>
  with sort + pagination + optional rowClassName. Consumers own
  the title/header UI above it and write typed ColumnDef<TData>
  lists.

Deps added: class-variance-authority, clsx, tailwind-merge (3
packages total).

Consumer migrations (proof of concept):
- app/components/Samu/SamuClientSide.tsx: cities list table now
  uses DataTable with a typed CityRow interface. Title moved out
  of the table into an <h3> above it.
- app/components/ExecucaoCicloviaria/CityContent.tsx: PDC
  structures table now uses DataTable with a typed PdcRelation
  interface. Same pattern — title moved outside.

Intentionally not touched:
- The monolithic app/components/Commom/Table/Table.tsx stays in
  place. The other 6 consumers still use it. Follow-up PRs can
  migrate them individually and delete the monolithic component
  once all consumers are off it.

Verification: typecheck count unchanged (301 pre-existing errors,
zero introduced); build succeeds in 8.4s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lish

Extends the POC DataTable with an optional render-prop toolbar that
exposes the TanStack table instance, then wires up a dropdown filter
on the PDC structures table (filter by Tipologia Prevista) as a real
example of the shadcn "consumer-owned filter UI" pattern.

DataTable additions:
- getFacetedRowModel + getFacetedUniqueValues row models registered
  so consumers can populate dropdowns from the actual data.
- ColumnFilters state wired into the useReactTable instance.
- New `toolbar?: (table) => ReactNode` prop rendered above the
  table's bordered container.
- Sort button styling: focus ring moved to focus-visible (mouse
  click no longer paints the blue ring), chevron icon wrapped in a
  fixed w-4/h-4 span to prevent sub-pixel layout shift on sort
  state changes.

Consumer (CityContent PDC structures table):
- Adds a typed dropdown filter on the pdc_typology column. Options
  come from column.getFacetedUniqueValues(), so the list is always
  in sync with whatever typologies the Atlas API returns — no
  hardcoded list.
- Replaces the `as string` assertion on getFilterValue() with a
  typeof narrow; drops `as PdcRelation[]` on the data prop (any
  is structurally assignable). The remaining `v is string` is a
  proper type predicate, not an assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant