Skip to content

feat(spa): responsive record-cards for the list on narrow viewports (#421)#528

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/responsive-record-cards-421
May 27, 2026
Merged

feat(spa): responsive record-cards for the list on narrow viewports (#421)#528
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/responsive-record-cards-421

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

Summary

On phones/tablets a wide changelist table is unreadable. Below Tailwind's md breakpoint (768px), ListPage now renders each object as a stacked, tappable record-card instead of the table — a long-standing mobile-parity gap (PM priorities §4 responsive UI / §7 mobile-tablet).

Implements #421. No backend or wire-contract change — Tier 4 (frontend only).

What

  • @dar/ui RecordCardList<Row> — a generic, model-agnostic card layout that consumes the same TableColumn<Row>[] descriptors, rows, selection, and navigation props as Table. A page defines its columns once and renders either layout. The first column is the card title (and carries the open-in-new-tab anchor, Navigation: Cmd/Ctrl+click (and middle-click) should open links in a new tab #253); the rest become a label/value list. Selection checkboxes, loading skeletons, and the empty-state mirror Table.
  • @dar/ui useMediaQuery(query) — a generic useSyncExternalStore-based hook: correct first-paint value (no flash), syncs across resizes, inert when matchMedia is unavailable.
  • ListPage swaps TableRecordCardList under (max-width: 767px), reusing the existing columns, selection state, and preserved-filter navigation. Switching in JS (not a CSS hidden/md:block pair) keeps a single layout in the DOM, so there are no duplicate inputs/checkboxes.

Design notes

  • One descriptor, two layouts — maximises reuse and keeps @dar/ui generic (CLAUDE.md §7); inline list_editable cells still work in cards because they're the same render functions.
  • Sort / column-resize are desktop affordances and aren't surfaced on the cards (the cards are a reading/nav surface); a possible follow-up, not a regression.
  • A modified click (Cmd/Ctrl/Shift/Alt) on the title opens a new tab only — the card's tap handler ignores modified clicks so it doesn't also navigate in-app.
  • No redundant chrome (CLAUDE.md §7): cards carry only field labels + values.

Verification (matches the new CI gate #506)

  • pnpm -r typecheck clean (13 pkgs) · pnpm lint clean (eslint --max-warnings 0 + stylelint + dark-mode coverage guard) · pnpm -r build ok.
  • pnpm test124 passed (20 files), incl. new RecordCardList (7) + useMediaQuery (4) suites: render, tap-to-open, new-tab anchor, modified-click guard, selection-without-nav, loading skeletons, empty-state; hook initial-match / change / unsubscribe / no-matchMedia.

Role / autonomy

Author / Architect. Tier 4 (frontend only, no contract/security surface) → agent-mergeable per autonomy-policy.md.

Closes #421

🤖 Generated with Claude Code

…421)

On phones/tablets a wide changelist table is unreadable. Below Tailwind's
`md` breakpoint the list now renders each object as a stacked, tappable
record-card instead of the table.

- @dar/ui `RecordCardList<Row>`: a generic, model-agnostic card layout
  that consumes the SAME `TableColumn<Row>[]` descriptors, rows,
  selection and navigation props as `Table` — a page defines its columns
  once and renders either layout. First column = card title (carries the
  open-in-new-tab anchor, #253); the rest become a label/value list.
  Selection checkboxes, loading skeletons, and the empty-state mirror
  `Table`.
- @dar/ui `useMediaQuery(query)`: a generic `useSyncExternalStore`-based
  hook (no first-paint flash, inert without `matchMedia`). Switching the
  layout in JS keeps a single layout in the DOM, so there are no
  duplicate inputs/checkboxes.
- ListPage swaps `Table` → `RecordCardList` under `(max-width: 767px)`,
  reusing the existing columns, selection and preserved-filter nav.

Unit tests: RecordCardList (render, tap-to-open, new-tab anchor,
modified-click guard, selection, loading, empty) + useMediaQuery
(initial match, change, unsubscribe, no-matchMedia). Full vitest 124
passed; typecheck + eslint + stylelint + dark-mode guard clean; build ok.

Closes #421

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MartinCastroAlvarez MartinCastroAlvarez merged commit 4afc63c into main May 27, 2026
5 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the feat/responsive-record-cards-421 branch May 28, 2026 13:10
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.

Responsive list: render objects as stacked record-cards on narrow viewports (extract detail card to @dar/ui)

2 participants