refactor(pwa): add semantic colour tokens + migrate primitives off inlined hex (#149) #174

Merged
james merged 1 commit from 149-semantic-color-tokens into main 2026-06-20 14:06:31 +00:00
Owner

Closes #149.

Summary

Adds the four DS-style semantic triplets — --success / --warning / --danger / --info, each with -subtle and -text siblings — to app/themes/light.css and app/themes/dark.css (and the auto-when-OS-prefers-dark branch). Values lifted verbatim from the hex literals the five primitive stylesheets used to inline.

Token shape

Mirrors accent (--accent / --accent-subtle / --accent-text):

  • --name: base — used for active borders, focus rings, icon fills.
  • --name-subtle: low-alpha background tint.
  • --name-text: readable foreground on -subtle.

Base + subtle stay in lockstep across themes; only --*-text shifts to keep contrast on the flipped surface.

Files migrated

  • Badge.module.css — every .tone-* rule reads var(--*-subtle) + var(--*-text). Removed all four per-tone [data-theme="dark"] + auto-media-query overrides.
  • Field.module.css.required and .error read var(--danger-text). Matching dark + auto overrides removed.
  • Input.module.css, Textarea.module.css, Select.module.cssaria-invalid border reads var(--danger); focus ring uses color-mix(in srgb, var(--danger) 35%, transparent) so its alpha tracks the base colour.

Visual parity

Captured /dev/components screenshots in both themes before and after.

  • Light-mode page: byte-identical.
  • Dark-mode page: differs in one band — the badge tones inside the LIGHT-theme panel that's nested inside the dark page. The old code's [data-theme="dark"] .tone-success { color: #4ade80; } selector matched any .tone-success descendant of the dark page, including badges inside the nested light panel, leaving them rendering dark text on a light surface. Tokens cascade through nested data-theme scopes correctly, so the light panel now shows light-theme badge colours. This is a strict improvement — no real-app surface relied on the old behaviour (nested mixed-theme scopes only exist in /dev/components).

Browser support note

color-mix(in srgb, ...) is used in the three input primitives' focus rings. Supported in Chrome 111+, Firefox 113+, Safari 16.2+ (all current). Targets line up with the rest of the codebase.

Test plan

  • npm run lint / npx tsc --noEmit / npm test / npm run build all clean (440 passed).
  • /dev/components light-mode: byte-identical before/after.
  • /dev/components dark-mode: only the nested light panel changed, in the way that's strictly correct.

🤖 Generated with Claude Code

Closes #149. ## Summary Adds the four DS-style semantic triplets — `--success` / `--warning` / `--danger` / `--info`, each with `-subtle` and `-text` siblings — to `app/themes/light.css` and `app/themes/dark.css` (and the `auto`-when-OS-prefers-dark branch). Values lifted verbatim from the hex literals the five primitive stylesheets used to inline. ### Token shape Mirrors accent (`--accent` / `--accent-subtle` / `--accent-text`): - `--name`: base — used for active borders, focus rings, icon fills. - `--name-subtle`: low-alpha background tint. - `--name-text`: readable foreground on `-subtle`. Base + subtle stay in lockstep across themes; only `--*-text` shifts to keep contrast on the flipped surface. ### Files migrated - `Badge.module.css` — every `.tone-*` rule reads `var(--*-subtle)` + `var(--*-text)`. Removed all four per-tone `[data-theme="dark"]` + `auto`-media-query overrides. - `Field.module.css` — `.required` and `.error` read `var(--danger-text)`. Matching dark + auto overrides removed. - `Input.module.css`, `Textarea.module.css`, `Select.module.css` — `aria-invalid` border reads `var(--danger)`; focus ring uses `color-mix(in srgb, var(--danger) 35%, transparent)` so its alpha tracks the base colour. ### Visual parity Captured `/dev/components` screenshots in both themes before and after. - **Light-mode page**: byte-identical. - **Dark-mode page**: differs in one band — the badge tones inside the LIGHT-theme panel that's nested inside the dark page. The old code's `[data-theme="dark"] .tone-success { color: #4ade80; }` selector matched any `.tone-success` descendant of the dark page, including badges inside the nested light panel, leaving them rendering dark text on a light surface. Tokens cascade through nested `data-theme` scopes correctly, so the light panel now shows light-theme badge colours. **This is a strict improvement** — no real-app surface relied on the old behaviour (nested mixed-theme scopes only exist in `/dev/components`). ### Browser support note `color-mix(in srgb, ...)` is used in the three input primitives' focus rings. Supported in Chrome 111+, Firefox 113+, Safari 16.2+ (all current). Targets line up with the rest of the codebase. ## Test plan - [x] `npm run lint` / `npx tsc --noEmit` / `npm test` / `npm run build` all clean (440 passed). - [x] `/dev/components` light-mode: byte-identical before/after. - [x] `/dev/components` dark-mode: only the nested light panel changed, in the way that's strictly correct. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
refactor(pwa): add semantic colour tokens + migrate primitives off inlined hex (#149)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 9s
PR / OSV-Scanner (pull_request) Successful in 36s
PR / Static analysis (pull_request) Successful in 58s
PR / npm audit (pull_request) Successful in 1m6s
PR / Typecheck (pull_request) Successful in 1m9s
PR / Package age policy (soft) (pull_request) Successful in 31s
PR / Lint (pull_request) Successful in 1m14s
Secrets / gitleaks (pull_request) Successful in 22s
PR / Test (sqlite) (pull_request) Successful in 1m30s
PR / Test (postgres) (pull_request) Successful in 1m34s
PR / Coverage (soft) (pull_request) Successful in 1m33s
PR / Trivy (image) (pull_request) Failing after 1m52s
PR / Build (pull_request) Successful in 1m56s
62e8b2b2c0
Add four DS-style triplets — `--success` / `--warning` / `--danger`
/ `--info`, each with `-subtle` and `-text` siblings — to
`app/themes/light.css` and `app/themes/dark.css` (and the
`auto`-when-OS-prefers-dark branch). Values lifted verbatim from the
hex literals the five affected primitive stylesheets used to inline;
the visual surface stays the same in light mode and gets sharper in
the dev showcase's mixed-theme view (see below).

Migrated:
- `Badge.module.css` — every `.tone-*` rule now reads
  `var(--*-subtle)` + `var(--*-text)`. Removed the per-tone
  `[data-theme="dark"]` + `auto` media-query overrides.
- `Field.module.css` — `.required` and `.error` read
  `var(--danger-text)`. Removed the matching dark + auto overrides.
- `Input.module.css`, `Textarea.module.css`, `Select.module.css` —
  `aria-invalid` border reads `var(--danger)`; the focus ring uses
  `color-mix(in srgb, var(--danger) 35%, transparent)` so its alpha
  tracks the base colour.

`/dev/components` light-mode page is byte-identical before vs. after.
Dark-mode page differs in one region: the badge tones inside the
LIGHT-theme panel that's nested inside the dark page. The old code's
`[data-theme="dark"] .tone-success` selector matched any
`.tone-success` descendant of the dark page — including badges in the
nested light panel — and left them rendering dark text on the
panel's light surface. Tokens cascade through nested `data-theme`
scopes correctly, so the light panel now shows light-theme badge
colours. This is a strict improvement; no real-app surface relied on
the old behaviour.

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

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 75.3% ≥ 50%
Branches 80.8% ≥ 75%
Functions 88.2% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 75.3% ✅ | ≥ 50% | | Branches | 80.8% ✅ | ≥ 75% | | Functions | 88.2% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.

Trivy (container image)

Threshold: high  ·  Total findings: 121  ·  At/above threshold: 1

critical high medium low
0 1 50 70
severity id package installed / range fix
high CVE-2026-12151 undici 6.25.0 6.27.0, 7.28.0, 8.5.0
<!-- scanner-comment: trivy --> ### Trivy (container image) **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 121 &nbsp;·&nbsp; **At/above threshold:** 1 | critical | high | medium | low | |---:|---:|---:|---:| | 0 | 1 | 50 | 70 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | high | [CVE-2026-12151](https://avd.aquasec.com/nvd/cve-2026-12151) | undici | 6.25.0 | `6.27.0, 7.28.0, 8.5.0` |
james merged commit 20e730261e into main 2026-06-20 14:06:31 +00:00
james deleted branch 149-semantic-color-tokens 2026-06-20 14:06:32 +00:00
Sign in to join this conversation.
No description provided.