Skip to content

Canonical TS tokens, dark mode, Quarto export, contrast matrix CI#25

Merged
MaxGhenis merged 1 commit into
mainfrom
feat/canonical-tokens-dark-quarto-contrast
May 9, 2026
Merged

Canonical TS tokens, dark mode, Quarto export, contrast matrix CI#25
MaxGhenis merged 1 commit into
mainfrom
feat/canonical-tokens-dark-quarto-contrast

Conversation

@MaxGhenis
Copy link
Copy Markdown
Contributor

Summary

Structural pass on the design system: makes @policyengine/ui-kit fit the "real design system" bar — single source of truth, dark mode plumbed in, paper renders sharing the dashboard palette, accessibility guarantees enforced by tests instead of convention.

What changes

Canonical TS source

  • src/theme/tokens.ts is now the source of truth for every token.
  • scripts/generate-css.ts reads it and writes both src/theme/tokens.css (for @import "@policyengine/ui-kit/theme.css") and src/theme/quarto.scss (for paper renders). Both files are checked in; CI fails if they drift.
  • bun run generate-tokens regenerates the outputs; prebuild runs it automatically.

Runtime token exports

  • colors, palette, chartPalette, semanticFills, typography, radius, namedSpacing, breakpoints, contrastPairs, tokens — all available via @policyengine/ui-kit/tokens or the package root.
  • Consumers that need a JS hex string (Plotly, generated SVG, inline styles) no longer have to read the CSSOM. Consumers that need dark-mode-aware values keep using var(--color-…).

Dark mode

  • Real :root.dark / .dark { … } overrides for every shadcn semantic role plus accessible-on-dark text variants (warning / error / success picked at WCAG AA on #0B0E14).
  • Activate by adding class="dark" to any ancestor (typically <html>).

Quarto theme export

  • @policyengine/ui-kit/quarto.scss maps the same hex values to Bootstrap/Quarto variables ($primary, $body-color, $card-bg, link colors, accessible $pe-text-warning / $pe-text-error / $pe-text-success). Paper renders now share the dashboard's palette and contrast guarantees.

WCAG contrast matrix in CI

  • tests/theme/contrast.test.ts implements WCAG 2.x relative-luminance + contrast-ratio formulas and asserts every documented foreground/background pair clears AA in both light and dark mode.
  • tests/theme/generated-css.test.ts re-runs the generator and asserts the checked-in tokens.css and quarto.scss match — drift fails CI.
  • Two latent contrast bugs surfaced and were fixed:
    • --destructive #EF4444#DC2626 (red-500 → red-600). White foreground was 3.76:1, now 4.83:1.
    • --text-warning #d9480f#c2410c (Mantine orange.9 → Tailwind orange-700). Was 4.30:1 on white, now 5.18:1.

Focus + reduced motion at @layer base

  • :focus-visible outline rule on a, button, [role="button"], input, select, textarea, summary, [tabindex] using --ring.
  • @media (prefers-reduced-motion: reduce) snaps animations and transitions to instant.
  • Both inherited by every consumer just by importing theme.css.

Verification

  • bun run typecheck clean
  • bun run test 219/219 pass (including the 17 new contrast assertions and 2 generator-drift checks)
  • bun run build clean

Notes

  • chartColors (in src/charts/chartDefaults.ts) is unchanged. The new indexed-slot helper is exported as chartPalette.
  • Tiny visual change: the destructive red is slightly darker (red-600 vs red-500). Reviewers should sanity-check the demo once the preview deploy lands.

🤖 Generated with Claude Code

This is the structural pass that makes @policyengine/ui-kit fit the
"real design system" bar: a single source of truth, dark mode plumbed
in, paper renders sharing the dashboard palette, and accessibility
guarantees enforced by tests rather than convention.

src/theme/tokens.ts is now the canonical source for every PolicyEngine
design token. scripts/generate-css.ts reads it and writes:

  - src/theme/tokens.css   → @import "@policyengine/ui-kit/theme.css"
  - src/theme/quarto.scss  → @import "@policyengine/ui-kit/quarto.scss"

Both files are checked in and CI fails if they drift from the
generator output.

Runtime exports
- colors, palette, chartPalette, semanticFills, typography, radius,
  namedSpacing, breakpoints, contrastPairs, tokens — all available via
  @policyengine/ui-kit/tokens or the package root.
- Consumers that need a JS hex string (Plotly traces, generated SVG,
  inline styles) no longer have to read the CSSOM. Consumers that
  need dark-mode-aware values keep using var(--color-…).

Dark mode
- :root.dark / .dark overrides for every shadcn semantic role plus
  accessible-on-dark text variants. Picked at WCAG AA on #0B0E14.
- Activated by adding class="dark" to any ancestor (typically <html>);
  the @custom-variant dark declaration is unchanged.

Quarto theme export
- @policyengine/ui-kit/quarto.scss maps the same hex values to
  Bootstrap/Quarto SCSS ($primary, $body-color, $card-bg, link
  colors, accessible $pe-text-warning/error/success). Paper renders
  now share the dashboard's palette and contrast guarantees.

WCAG contrast matrix in CI
- tests/theme/contrast.test.ts implements the WCAG 2.x relative-
  luminance and contrast-ratio formulas and asserts every documented
  foreground/background pair clears AA in both light and dark mode.
- tests/theme/generated-css.test.ts re-runs the generator and asserts
  the checked-in tokens.css and quarto.scss match — drift fails CI.
- Two latent contrast bugs surfaced and were fixed:
  - --destructive #EF4444 → #DC2626 (red-500 → red-600); white
    foreground was 3.76:1, now 4.83:1.
  - --text-warning #d9480f → #c2410c (Mantine orange.9 → Tailwind
    orange-700); was 4.30:1 on white, now 5.18:1.

Built-in focus-visible + reduced-motion at @layer base
- :focus-visible outline rule on a, button, [role="button"], input,
  select, textarea, summary, [tabindex] using --ring.
- @media (prefers-reduced-motion: reduce) snaps animations and
  transitions to instant.
- Both are inherited by every consumer just by importing theme.css.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
policyengine-ui-kit Ready Ready Preview, Comment May 9, 2026 0:05am

Request Review

@policyengine policyengine Bot added the ⚙️ Engineering... PolicyEngine's GitHub agent is working on this label May 9, 2026
@MaxGhenis MaxGhenis merged commit cf853e8 into main May 9, 2026
4 checks passed
Copy link
Copy Markdown
Contributor

@policyengine policyengine Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work on this design system overhaul. The architecture is solid and achieves all stated goals:

Strengths:

  • Single source of truth pattern is properly implemented (tokens.ts → generate-css.ts → CSS/SCSS)
  • CI enforcement via generated-css.test.ts prevents drift
  • WCAG contrast validation with proper formula implementation
  • Clean separation: semanticFills (badges/UI) vs text-warning/error/success (accessible text)
  • Dark mode properly structured with :root.dark overrides
  • Runtime token exports for JS contexts (Plotly, etc.)
  • Quarto SCSS integration for paper renders

Code Quality:

  • WCAG luminance formula correctly implements spec
  • Generator script is well-structured and maintainable
  • Tests cover both contrast validation and generator drift
  • TypeScript types properly exported

Minor style note (non-blocking):
There's some inconsistency in hex color casing (mostly uppercase like #FFFFFF, but a few lowercase like #f5f9ff, #e2e8f0, #ffffff, #5a5a5a, #c2410c). Both work fine, but consistent casing would be slightly cleaner.

Verified:

  • The distinction between --destructive (#DC2626, meets WCAG AA for white text) and semanticFills.error (#EF4444, for badges) is intentional and documented
  • Quarto's `` mapping to semanticFills.error is appropriate for Bootstrap's use case
  • All contrast pairs properly validate against their declared minimums

The prebuild hook ensures files stay in sync. Ready to merge.

@policyengine policyengine Bot removed the ⚙️ Engineering... PolicyEngine's GitHub agent is working on this label May 9, 2026
MaxGhenis added a commit that referenced this pull request May 9, 2026
…ix (#26)

* Fix dark card visibility, Quarto $secondary, and expand contrast matrix

Follow-up to #25 addressing review findings:

- **Dark mode card surfaces visible.** `--card`/`--popover`/`--muted` bumped
  from #131820 (1.08:1 vs --background) to #1A2030 (1.19:1), and
  `--border`/`--input` bumped from #1E293B (1.32:1 / 1.11:1 on card) to
  #334155 (1.87:1 / 1.57:1). Cards and their borders are now distinguishable
  at glance instead of nearly invisible.
- **Quarto `$secondary` fixed.** Was resolving to `--secondary-foreground`
  (#101828, near-black). Bootstrap's `$secondary` is a fill color, so paper
  renders previously got a near-black .btn-secondary background. Now uses
  `--secondary` (#F2F4F7), the light gray surface.
- **Contrast matrix coverage.** Added pairs for: `--ring` on `--background`
  light + dark (WCAG SC 1.4.11 non-text 3:1 for focus indicators), dark
  `--primary-foreground` on `--primary`, dark `--destructive-foreground` on
  `--destructive`, and the default link color (teal-600) on background.
- **`pretest` hook.** `bun run test` now regenerates `tokens.css` /
  `quarto.scss` before vitest runs, so token edits without an explicit
  `bun run generate-tokens` no longer surface as a misleading drift failure.

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

* Address review: revert pretest hook, fix old --border ratio comment

The pretest hook (added in this PR) silently regenerates tokens.css before
vitest runs, masking the drift test it was added to make less annoying. The
test was specifically designed to catch hand-edits to the generated CSS;
under pretest, those edits get clobbered before the assertion runs and CI
goes green on drift. The error message ("run \`bun run generate-tokens\`")
is already directive, so revert the hook and trust the existing UX.

Also fix the inaccurate "1.11:1 on card" claim in the dark-mode --border
comment — the actual ratio of #1E293B on #131820 is 1.22:1, and add a note
next to the light-mode --ring documenting the SC 1.4.11 dependency
(currently 3.51:1 on white, only ~0.5 above the 3:1 floor).

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

* Fix changelog: 1.11 → 1.22 on card (matches tokens.ts comment)

Round-2 review caught that the dark-card-visibility changelog fragment
still claimed the old --border was "1.11:1 vs card" — the same number that
was wrong in tokens.ts and got fixed there last commit but missed here.
The actual #1E293B-on-#131820 ratio is 1.22:1.

Tracking issue for the related --input SC 1.4.11 fail filed as #27.

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

---------

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