Skip to content

feat: formalize design system on existing shadcn/Tailwind foundation - #326

Open
bmersereau wants to merge 3 commits into
Open-Legal-Products:mainfrom
bmersereau:feat/316-design-system
Open

feat: formalize design system on existing shadcn/Tailwind foundation#326
bmersereau wants to merge 3 commits into
Open-Legal-Products:mainfrom
bmersereau:feat/316-design-system

Conversation

@bmersereau

Copy link
Copy Markdown

Summary

Formalizes the design system that already exists rather than changing it: documents the current tokens and primitives, promotes three duplicated patterns into the shared layer, and fixes the accessibility gaps in the primitives. No visual redesign — the only pixel-level changes are the accessibility fixes listed below.

Closes #322

Scope note: catalog tool deferred

The component catalog (acceptance-criteria item 2, Storybook or Ladle) was intentionally deferred to a follow-up issue and is not in this PR. As a result this PR adds no new dependenciesfrontend/package.json and package-lock.json are untouched.

1. docs/design-system.md

Documents the existing foundations, measured off globals.css and all ~120 component files rather than assumed:

  • Colour — the three vocabularies actually in use, and when each applies: the Tailwind grey ramp (~930 usages, the real working palette), the app-* surface tokens, and the shadcn semantic tokens (~45 usages, effectively primitives-only). Plus the accent blue, and the five files that legitimately use raw hex.
  • Dark mode is documented as unshipped — a full .dark token set exists but nothing ever adds the class, and dark: variants appear only in the three vendored shadcn primitives. Worth writing down so nobody assumes it works.
  • Typography — Inter/EB Garamond split, when to reach for font-serif, and the de facto type scale (text-xs at ~200 uses is the workhorse; this is a dense, table-heavy app).
  • Spacing/radius — the de facto scale by measured frequency, plus the "bigger surface, bigger radius" convention.
  • The glass/"liquid" idiom — and the rule that layered shadow-[...] recipes are never retyped, since that is this codebase's main source of copy-paste drift.
  • Which layer to reach for — a 5-step ladder from existing primitive → shared constant → shadcn registry → feature-local one-off → new primitive, with an explicit promotion bar (same markup in 3+ places).

2. Consolidated primitives

Chosen from a grep-and-read pass over the feature components, not invented. Each was verified as genuinely the same markup before promoting it:

New primitive Replaces Call sites migrated
ui/glass-icon-button.tsx A character-for-character identical 300-char glass close button 4 — Modal, DocumentSidePanel, TRSidePanel, TREditColumnMenu
ui/empty-state.tsx The icon + serif title + description + action body repeated inside TableEmptyState 8 across 5 files — ProjectAssistantTable, ProjectReviewsTable, ProjectsOverview (×2), WorkflowList (×3), WorkflowDetailPage
ui/check-square.tsx The selection square, including its mixed and muted states 5 — AddProjectDocsModal, QuickActionsModal, FileDirectory (×3)

Deliberately not consolidated, to keep this honest:

  • Inline Loader2 spinners — 43 sites with real size/colour variation at each; a wrapper would trade one line for one line.
  • Tag pills — already shared via tabular/pillUtils.ts.
  • The hand-rolled "Actions" dropdown (5 copies of an outside-click effect + panel). Real duplication and the biggest win, but migrating it to Radix changes focus/Escape/portal behaviour, so it belongs in its own PR rather than riding along with a docs change.
  • ProjectReviewsTable's load-error state and the short "no results found" variants — superficially similar but a different heading size and a deliberately smaller state.

Two design decisions worth flagging: EmptyState renders no wrapper, so it drops straight into the existing TableEmptyState container; and CheckSquare is deliberately role-free and ARIA-free, because call sites disagree about whether the checkbox semantics belong on the square or on the row button wrapping it, and adding a role would double up on the three that already declare one.

3. Accessibility fixes

All applied, not just reported, and each covered by a test:

  • cite-button — was missing type="button" (so it submitted any surrounding form) and had no accessible name at all when rendered icon-only. Now labelled only when there is no visible text, so the visible "Cite" stays the accessible name (WCAG 2.5.3).
  • search-bar — the input had no accessible name (a placeholder is not a name, WCAG 4.1.2), and its only focus indicator was a white/70white/90 shift, which is imperceptible (WCAG 2.4.7).
  • tab-pill-button — the inactive state used text-gray-400 on a translucent white pill, ~2.5:1, failing WCAG 1.4.3. Now text-gray-500 at ~4.8:1.
  • toggle-switch — the off state was a white thumb on a gray-100 track, ~1.07:1, effectively invisible (WCAG 1.4.11). Fixed with ring-1 ring-gray-300 rather than a border, so the absolutely-positioned thumb doesn't shift by 1px.
  • dropdown-menu — menu items set outline-hidden and relied on focus:bg-accent, but the liquid-dropdown skin overrides that with a near-white hover colour (~1.02:1), leaving keyboard users no focus indicator. Added a background-independent ring. Also, a selected radio item was marked by bg-gray-100 on white (~1.03:1) and by colour alone — now also carries font-medium as a second, non-colour channel (WCAG 1.4.1).
  • pill-button — had no focus-visible ring.
  • Verified as already passing: the oklch token palette (--muted-foreground is ~4.6:1 on the light background) and button/input, which already ship upstream shadcn focus and aria-invalid handling.

4. CONTRIBUTING.md

One additive ## Frontend UI section on checking components/ui/ and the shadcn registry before hand-rolling a component. Placed in its own section and does not restructure the file, so it should not conflict with #324's separate addition.

Test plan

  • 39 new tests across 9 files in frontend/src/app/components/ui/ (new primitives + the ARIA/focus/contrast assertions), matching the existing house style: queried by role and accessible name, asserting only the classes that carry meaning.
  • Written first and confirmed failing for the right reasons before implementing.
  • Full frontend suite: 315 passed. The 4 failures in src/app/lib/mikeApi.test.ts (blob.text is not a function) are pre-existing and reproduce on an unmodified tree — a local Node 24 Response.blob() difference. CI and Code Quality are green on main at this branch's base commit (1af9231), and CI runs Node 22, so no issue was filed.
  • npm run test:coverage (the CI gate) — the src/app/lib/** ratchet floor is untouched by this change.
  • npm run lint with the cache cleared — 0 errors, and no warnings in any file this PR touches.
  • npm run build — passes with the same placeholder env vars CI uses.
  • tsc --noEmit clean.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

Formalize a design system on top of the existing shadcn/Tailwind foundation

2 participants