mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 16:41:08 +02:00
refactor(theming): re-apply theme validator suite after parallel rebase
The plan-doc commits129971ffc+9db044178dropped the audit-theme-tokens → validate-theme-variables rename, the validate-theme-tokens → validate-theme-utilities rename, the new validate-theme-parity script, brand-literals.md, and the corresponding package.json + lint-staged.config.js + themes.css wiring. The files still existed on disk (git mv changes survived) but were untracked. Restore the validator suite so `pnpm run validate:all` works again: - validate:theme-variables (CSS var names: --muted → --color-muted) - validate:theme-utilities (Tailwind: no white/N, no neutral palette) - validate:theme-parity (every --color-* in :root ⇔ .dark + each [data-theme="..."]) All three wired into validate:all and lint-staged. `pnpm run validate:all` is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
766ad2ea8f
commit
430aa30cbf
7 changed files with 411 additions and 17 deletions
|
|
@ -4,11 +4,18 @@ export default {
|
||||||
'prettier --config .prettierrc.json --write',
|
'prettier --config .prettierrc.json --write',
|
||||||
],
|
],
|
||||||
'*.{json,md,svelte,astro}': ['prettier --config .prettierrc.json --write'],
|
'*.{json,md,svelte,astro}': ['prettier --config .prettierrc.json --write'],
|
||||||
// Theme-token audit whenever styles-bearing files change. Runs on the
|
// Theme-variable + utility audit whenever styles-bearing files change.
|
||||||
// whole tree (ignores staged filenames), scans ~3k files in <1s, and
|
// Runs on the whole tree (ignores staged filenames), scans ~3k files
|
||||||
// fails if any file re-introduces bare --muted / --theme-* references
|
// in <1s, and fails if any file re-introduces:
|
||||||
// instead of Mana's canonical --color-* tokens.
|
// - bare `--muted` / `--theme-*` CSS variables instead of Mana's
|
||||||
'*.{svelte,css}': () => 'node scripts/audit-theme-tokens.mjs',
|
// canonical `--color-*` tokens (validate-theme-variables.mjs), or
|
||||||
|
// - raw white-alpha / neutral-palette Tailwind utilities instead of
|
||||||
|
// theme-token utilities (validate-theme-utilities.mjs).
|
||||||
|
'*.{svelte,css}': () => [
|
||||||
|
'node scripts/validate-theme-variables.mjs',
|
||||||
|
'node scripts/validate-theme-utilities.mjs',
|
||||||
|
'node scripts/validate-theme-parity.mjs',
|
||||||
|
],
|
||||||
// Validate the tunnel config locally so a malformed ingress map can
|
// Validate the tunnel config locally so a malformed ingress map can
|
||||||
// never reach main. The validator runs entirely in node (no
|
// never reach main. The validator runs entirely in node (no
|
||||||
// cloudflared CLI dependency on the dev box) and catches the same
|
// cloudflared CLI dependency on the dev box) and catches the same
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@
|
||||||
"validate:dockerfiles": "node scripts/validate-dockerfiles.mjs",
|
"validate:dockerfiles": "node scripts/validate-dockerfiles.mjs",
|
||||||
"validate:turbo": "node scripts/validate-no-recursive-turbo.mjs",
|
"validate:turbo": "node scripts/validate-no-recursive-turbo.mjs",
|
||||||
"validate:pg-schema": "node scripts/validate-pg-schema-isolation.mjs",
|
"validate:pg-schema": "node scripts/validate-pg-schema-isolation.mjs",
|
||||||
"validate:theme-tokens": "node scripts/validate-theme-tokens.mjs",
|
"validate:theme-variables": "node scripts/validate-theme-variables.mjs",
|
||||||
"validate:all": "pnpm run validate:turbo && pnpm run validate:pg-schema && pnpm run validate:theme-tokens && pnpm run check:crypto",
|
"validate:theme-utilities": "node scripts/validate-theme-utilities.mjs",
|
||||||
|
"validate:theme-parity": "node scripts/validate-theme-parity.mjs",
|
||||||
|
"validate:all": "pnpm run validate:turbo && pnpm run validate:pg-schema && pnpm run validate:theme-variables && pnpm run validate:theme-utilities && pnpm run validate:theme-parity && pnpm run check:crypto",
|
||||||
"check:crypto": "node scripts/audit-crypto-registry.mjs",
|
"check:crypto": "node scripts/audit-crypto-registry.mjs",
|
||||||
"check:crypto:seed": "node scripts/audit-crypto-registry.mjs --seed",
|
"check:crypto:seed": "node scripts/audit-crypto-registry.mjs --seed",
|
||||||
"audit:deps": "node scripts/audit-workspace-deps.mjs",
|
"audit:deps": "node scripts/audit-workspace-deps.mjs",
|
||||||
|
|
@ -30,7 +32,6 @@
|
||||||
"audit:coupling": "node scripts/audit-module-coupling.mjs",
|
"audit:coupling": "node scripts/audit-module-coupling.mjs",
|
||||||
"audit:complexity": "node scripts/audit-complexity.mjs",
|
"audit:complexity": "node scripts/audit-complexity.mjs",
|
||||||
"audit:map": "node scripts/build-complexity-map.mjs",
|
"audit:map": "node scripts/build-complexity-map.mjs",
|
||||||
"audit:theme-tokens": "node scripts/audit-theme-tokens.mjs",
|
|
||||||
"generate:dockerfiles": "node scripts/generate-dockerfiles.mjs",
|
"generate:dockerfiles": "node scripts/generate-dockerfiles.mjs",
|
||||||
"setup:env": "node scripts/generate-env.mjs",
|
"setup:env": "node scripts/generate-env.mjs",
|
||||||
"setup:secrets": "node scripts/setup-secrets.mjs",
|
"setup:secrets": "node scripts/setup-secrets.mjs",
|
||||||
|
|
|
||||||
202
packages/shared-tailwind/brand-literals.md
Normal file
202
packages/shared-tailwind/brand-literals.md
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
# Brand-Literal Color Register
|
||||||
|
|
||||||
|
This document lists every module that intentionally uses literal color
|
||||||
|
values (`#RRGGBB`, `rgb()`, `rgba()`, `hsl()` without `var(--color-*)`)
|
||||||
|
**instead of** theme tokens — and the reason why.
|
||||||
|
|
||||||
|
It exists because the theme-token validators (`validate-theme-utilities`,
|
||||||
|
`validate-theme-variables`) would otherwise flag these as drift. They are
|
||||||
|
not drift: the color _is the point_.
|
||||||
|
|
||||||
|
## The rule
|
||||||
|
|
||||||
|
Colors in Mana fall into three buckets:
|
||||||
|
|
||||||
|
1. **Theme tokens** (`bg-muted`, `text-foreground`, `hsl(var(--color-primary))`)
|
||||||
|
— surfaces, text, borders. Must track the active theme variant.
|
||||||
|
|
||||||
|
2. **Semantic tokens** (`--color-success`, `--color-warning`, `--color-error`)
|
||||||
|
— feedback states. Also theme-aware.
|
||||||
|
|
||||||
|
3. **Brand literals** (documented here) — domain-semantic colors that
|
||||||
|
intentionally _do not_ track the theme. A period tracker's pink is
|
||||||
|
pink in every theme.
|
||||||
|
|
||||||
|
If you're adding a literal color that isn't on this list, think: "does
|
||||||
|
the domain require this exact hue, or am I reaching for a nearby theme
|
||||||
|
token?" Default to the token. Add to this register only when the domain
|
||||||
|
genuinely demands the literal.
|
||||||
|
|
||||||
|
## Per-module inventory
|
||||||
|
|
||||||
|
### `period` — menstrual cycle tracking
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| ------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
||||||
|
| Flow severity ramp | `#fda4af`, `#fb7185`, `#e11d48`, `#9f1239` | Spotting → light → medium → heavy. Literal red ramp is the domain. |
|
||||||
|
| Cycle phases | `#e11d48`, `#f59e0b`, `#22c55e`, `#8b5cf6` | Menstruation/follicular/ovulation/luteal — standardised colours in reproductive-health UX. |
|
||||||
|
| Module accent | `#ec4899` + `rgba(236,72,153, …)` | Pink is the module's identity; users recognise it across contexts. |
|
||||||
|
|
||||||
|
Files: `lib/modules/period/types.ts`, `lib/modules/period/ListView.svelte`
|
||||||
|
|
||||||
|
### `citycorners` — city discovery
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| ---------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Location category pins | 11 colours from `CATEGORY_COLORS` | Sight/restaurant/shop/museum/cafe/bar/park/beach/hotel/venue/viewpoint — map-convention colours that readers recognise at a glance. |
|
||||||
|
|
||||||
|
Files: `lib/modules/citycorners/types.ts`
|
||||||
|
|
||||||
|
### `who` — historical-persona guessing game
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| ------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||||
|
| Deck accents | `#a855f7` historical, `#ec4899` women, `#f59e0b` antiquity, `#0ea5e9` inventors | Per-deck identity; colour primes the player about era/theme. |
|
||||||
|
|
||||||
|
Files: `lib/modules/who/ListView.svelte`, `lib/modules/who/views/PlayView.svelte`
|
||||||
|
|
||||||
|
### `firsts` — life-experience log
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| --------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||||
|
| Experience categories | 11 colours (`#f97316` culinary, `#ef4444` adventure, …) | Domain-semantic tags the user picks — must look identical every visit. |
|
||||||
|
|
||||||
|
Files: `lib/modules/firsts/types.ts`
|
||||||
|
|
||||||
|
### `habits` — habit tracker
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| -------------------------- | ------------------------------------ | ------------------------------------------------------------------ |
|
||||||
|
| User-pickable habit colour | 14-colour palette, full hue spectrum | User-owned choice; changing under theme would feel like data loss. |
|
||||||
|
|
||||||
|
Files: `lib/modules/habits/types.ts`
|
||||||
|
|
||||||
|
### `finance` — expense tracking
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| -------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||||
|
| Default expense categories | 8 colours (`#f97316` food, `#3b82f6` transport, …) | Category-colour conventions (health=green, travel=blue) stable across themes. |
|
||||||
|
| Custom-category palette | 13 curated colours | User-pickable palette for personal categories. |
|
||||||
|
|
||||||
|
Files: `lib/modules/finance/types.ts`
|
||||||
|
|
||||||
|
### `times` — time tracking
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| --------------- | ------------------------------------ | -------------------------------------------------------------- |
|
||||||
|
| Project colours | 16-colour palette (`PROJECT_COLORS`) | Full hue spectrum for visual distinction across many projects. |
|
||||||
|
|
||||||
|
Files: `lib/modules/times/types.ts`
|
||||||
|
|
||||||
|
### `journal` / `mood` / `dreams` — emotion logging
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| -------------------------- | --------------------------------------------------- | ------------------------------------------------------------ |
|
||||||
|
| Emotion valence | Green (positive) → amber → red (negative) spectrums | Domain convention: emotion-colour psychology is the feature. |
|
||||||
|
| User-pickable mood colours | 16 fixed hues | User-selected identity per mood. |
|
||||||
|
|
||||||
|
Files: `lib/modules/journal/types.ts`, `lib/modules/mood/types.ts`,
|
||||||
|
`lib/modules/dreams/types.ts`
|
||||||
|
|
||||||
|
### `recipes` — difficulty indicator
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| ---------- | ------------------------------------------------ | ------------------------------------------------------- |
|
||||||
|
| Difficulty | `#22c55e` easy, `#f59e0b` medium, `#ef4444` hard | Traffic-light pattern — universal, not theme-dependent. |
|
||||||
|
|
||||||
|
Files: `lib/modules/recipes/types.ts`
|
||||||
|
|
||||||
|
### `notes` — highlight colour palette
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| -------------- | -------------------------------- | ------------------------------- |
|
||||||
|
| Note highlight | 10-colour curated palette + null | User-pickable; stable per note. |
|
||||||
|
|
||||||
|
Files: `lib/modules/notes/types.ts`
|
||||||
|
|
||||||
|
### `drink` — beverage categories
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| ------------------ | ----------------------------------------------------------------- | ------------------------------------------------------ |
|
||||||
|
| Drink-type palette | 13 colours (`#92400e` coffee, `#881337` wine, `#3b82f6` water, …) | Semantic mapping to beverage (brown coffee, red wine). |
|
||||||
|
|
||||||
|
Files: `lib/modules/drink/types.ts`
|
||||||
|
|
||||||
|
### `sleep` — quality ratings
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| ------------------- | --------------------------------- | -------------------------------- |
|
||||||
|
| Module accent | `#6366f1` indigo | Module identity. |
|
||||||
|
| Sleep-quality scale | `#22c55e` / `#f59e0b` / `#ef4444` | Traffic-light quality indicator. |
|
||||||
|
|
||||||
|
Files: `lib/modules/sleep/ListView.svelte`, `lib/modules/sleep/components/*.svelte`
|
||||||
|
|
||||||
|
### `spiral` / `quotes` — spiral canvas visualisation
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| --------------------- | --------------------------------- | ---------------------------------------------------------- |
|
||||||
|
| Canvas background | `#1a1a1a` (near-black) | Canvas needs a fixed contrast surface regardless of theme. |
|
||||||
|
| Golden accent lines | `#fbbf24` | Intentional warm-gold against dark canvas. |
|
||||||
|
| Violet overlay + glow | `#8b5cf6`, `rgba(99,102,241,0.1)` | Module identity: indigo → violet ramp. |
|
||||||
|
|
||||||
|
Files: `lib/modules/spiral/ListView.svelte`,
|
||||||
|
`lib/modules/spiral/components/SpiralCanvas.svelte`,
|
||||||
|
`lib/modules/quotes/components/SpiralCanvas.svelte`
|
||||||
|
|
||||||
|
### `photos` — photo viewer
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| ---------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
|
| Near-black backdrop gradient | `rgba(0,0,0,0.8) → transparent` | Photo viewers always use black backdrop — theme variants would leak colour onto the photo. |
|
||||||
|
| Upload status overlays | `rgba(0,0,0,0.4)`, `rgba(34,197,94,0.5)`, `rgba(239,68,68,0.5)` | State feedback against arbitrary image content. |
|
||||||
|
|
||||||
|
Files: `lib/modules/photos/ListView.svelte`,
|
||||||
|
`lib/modules/photos/components/albums/AlbumCard.svelte`
|
||||||
|
|
||||||
|
### `moodlit` — ambient mood gradients
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| ---------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| User-authored gradient stops | Arbitrary user-picked hex | The whole module's value is rendering the user's colours verbatim. |
|
||||||
|
| White-alpha overlays | `bg-white/20`, `text-white`, etc. | UI chrome sits on a vivid gradient background — only white reads. Validator exempts `MoodFullscreen.svelte`, `MoodCard.svelte`, `CreateMoodDialog.svelte`. |
|
||||||
|
|
||||||
|
Files: `lib/modules/moodlit/components/mood/*.svelte`
|
||||||
|
|
||||||
|
### `calc` — retro calculator skins
|
||||||
|
|
||||||
|
| Skin | Colors | Why literal |
|
||||||
|
| ----------- | ------------------------------------------ | ----------------------------------------------------- |
|
||||||
|
| TI-84 Plus | `#2a4a3a`, `#aaffaa`, `#88cc88`, `#3366aa` | 1990s TI LCD green — the authenticity is the feature. |
|
||||||
|
| HP-35 | `#ff3333`, `#ff2200`, `#c63030` | 1971 HP LED red display. |
|
||||||
|
| Casio fx-82 | `#b8c8a0`, `#3a4a2a`, `#1a2a0a` | 1980s Casio LCD beige-green. |
|
||||||
|
|
||||||
|
Files: `lib/modules/calc/components/{TI84Skin,HP35Skin,CasioSkin}.svelte`
|
||||||
|
|
||||||
|
### `presi` — rehearsal blocks
|
||||||
|
|
||||||
|
| Purpose | Colors | Why literal |
|
||||||
|
| --------------------------- | -------------- | ---------------------------------------------------------- |
|
||||||
|
| Rehearsal time-block accent | `#84cc16` lime | Distinguishes practice sessions from real calendar events. |
|
||||||
|
|
||||||
|
Files: `lib/modules/presi/stores/decks.svelte.ts`
|
||||||
|
|
||||||
|
### `--color-branch-*` — skilltree accents (actually theme-aware)
|
||||||
|
|
||||||
|
`skilltree` uses per-branch accent colours defined as _theme-agnostic CSS
|
||||||
|
variables_ (`--color-branch-intellect`, `--color-branch-body`, …) in
|
||||||
|
`themes.css`. These are declared once at `:root` and intentionally do not
|
||||||
|
have dark/variant overrides — they look identical regardless of theme.
|
||||||
|
Consumers must still wrap with `hsl(var(--color-branch-X))`.
|
||||||
|
|
||||||
|
The parity validator exempts them via the `THEME_AGNOSTIC` allowlist.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **16 modules** hold brand-literal colours.
|
||||||
|
- **~70 unique hex values** across them.
|
||||||
|
- The two validators (`validate-theme-utilities`, `validate-theme-variables`)
|
||||||
|
already ignore these files / these patterns where necessary. If a new
|
||||||
|
literal trips the validator, either:
|
||||||
|
1. Add it to this register and (if it's a white-alpha overlay case)
|
||||||
|
to `BRAND_OVERLAY_FILES` in `validate-theme-utilities.mjs`, or
|
||||||
|
2. Migrate the literal to a theme token — usually the right answer.
|
||||||
|
|
@ -35,12 +35,15 @@
|
||||||
* ✅ color: hsl(var(--color-foreground));
|
* ✅ color: hsl(var(--color-foreground));
|
||||||
* ✅ background: hsl(var(--color-primary) / 0.12);
|
* ✅ background: hsl(var(--color-primary) / 0.12);
|
||||||
*
|
*
|
||||||
* 4. Brand-literal colors (period pink, observatory cosmic scenes, the
|
* 4. Brand-literal colors (period pink, photo-viewer near-black backdrop,
|
||||||
* automations/spiral indigo→violet ramp, sport/category palettes, the
|
* skilltree branch accents, spiral indigo→violet ramp, calc retro
|
||||||
* photo viewer's near-black backdrop, etc.) deliberately stay as
|
* skins, etc.) deliberately stay as literal hex/rgba/hsl. They are
|
||||||
* literal hex/rgba/hsl. They are NOT theme intent — they encode brand
|
* NOT theme intent — they encode brand or domain semantics that must
|
||||||
* or domain semantics that must look the same under every theme
|
* look the same under every theme variant. Don't migrate them to
|
||||||
* variant. Don't migrate them to tokens.
|
* tokens. The complete inventory with per-module rationale lives in
|
||||||
|
* packages/shared-tailwind/brand-literals.md — add to that register
|
||||||
|
* when introducing a new literal, or prefer a theme token if the
|
||||||
|
* colour is not domain-semantic.
|
||||||
*
|
*
|
||||||
* 5. Token names are stable. There is intentionally NO `--color-info`,
|
* 5. Token names are stable. There is intentionally NO `--color-info`,
|
||||||
* `--color-text`, `--color-destructive`, `--color-surface`, or
|
* `--color-text`, `--color-destructive`, `--color-surface`, or
|
||||||
|
|
|
||||||
178
scripts/validate-theme-parity.mjs
Normal file
178
scripts/validate-theme-parity.mjs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Validate that every --color-* token defined for the theme is present in
|
||||||
|
* every variant block. A token defined only in :root (but missing from
|
||||||
|
* .dark, or from [data-theme="ocean"]) silently falls back to `inherit`
|
||||||
|
* or `undefined` under that variant, producing invisible text or
|
||||||
|
* miscoloured surfaces in ways that are easy to miss during dev.
|
||||||
|
*
|
||||||
|
* This is the parity invariant: if one variant has `--color-X`, every
|
||||||
|
* variant MUST have `--color-X`.
|
||||||
|
*
|
||||||
|
* Scope: the "canonical" token set is derived from the default :root
|
||||||
|
* block (after the base :root block for domain accents). Each
|
||||||
|
* theme-variant block is compared to it.
|
||||||
|
*
|
||||||
|
* Exclusions: `--color-branch-*` and `--color-mana` are declared once
|
||||||
|
* (intentionally theme-agnostic brand accents, see themes.css §Domain
|
||||||
|
* Accent Colors) and do not need per-variant definitions.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/validate-theme-parity.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const REPO_ROOT = join(__dirname, '..');
|
||||||
|
const THEMES_CSS = join(REPO_ROOT, 'packages/shared-tailwind/src/themes.css');
|
||||||
|
|
||||||
|
/** Tokens defined once at :root that do NOT participate in parity. */
|
||||||
|
const THEME_AGNOSTIC = /^--color-(?:branch-|mana$)/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse themes.css into selector → Set<tokenName>. A block starts at a
|
||||||
|
* line matching a selector followed by `{` and ends at the matching `}`.
|
||||||
|
* The base `@theme` / `@theme inline` blocks are skipped — they define
|
||||||
|
* Tailwind's utility generator, not the per-variant surfaces.
|
||||||
|
*/
|
||||||
|
function parseBlocks(src) {
|
||||||
|
const blocks = new Map();
|
||||||
|
const lines = src.split('\n');
|
||||||
|
let currentSelector = null;
|
||||||
|
let depth = 0;
|
||||||
|
let inBase = false; // true while inside @theme {} / @theme inline {}
|
||||||
|
|
||||||
|
for (const raw of lines) {
|
||||||
|
const line = raw.trim();
|
||||||
|
|
||||||
|
// Track @theme blocks so we skip their contents.
|
||||||
|
if (/^@theme\b/.test(line)) {
|
||||||
|
inBase = true;
|
||||||
|
if (line.includes('{')) depth++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inBase) {
|
||||||
|
if (line.includes('{')) depth++;
|
||||||
|
if (line.includes('}')) {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) inBase = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for block open: `selector {` or `selector,` continued.
|
||||||
|
if (!currentSelector) {
|
||||||
|
const selectorMatch = raw.match(
|
||||||
|
/^(:root(?:\.dark)?|\.dark|\[data-theme=['"](?:[a-z-]+)['"]\](?:\.dark)?|\.dark\[data-theme=['"](?:[a-z-]+)['"]\])\s*(?:,|\{)/
|
||||||
|
);
|
||||||
|
if (selectorMatch) {
|
||||||
|
// Normalize variant key so `.dark, :root.dark` both fold into "dark".
|
||||||
|
const sel = selectorMatch[1];
|
||||||
|
currentSelector = normalizeSelector(sel);
|
||||||
|
if (line.includes('{')) depth = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Comma-chained selector continuation: if a previous line had
|
||||||
|
// `selector,`, the next line is another selector that shares the
|
||||||
|
// same block.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inside a tracked block.
|
||||||
|
if (line.includes('{')) depth++;
|
||||||
|
if (line.includes('}')) {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) currentSelector = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token definition: `--color-X: value;`
|
||||||
|
const tokenMatch = line.match(/^(--color-[a-z0-9-]+)\s*:/);
|
||||||
|
if (tokenMatch && currentSelector) {
|
||||||
|
if (!blocks.has(currentSelector)) blocks.set(currentSelector, new Set());
|
||||||
|
blocks.get(currentSelector).add(tokenMatch[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSelector(sel) {
|
||||||
|
// `:root.dark` and `.dark` collapse to the same variant key.
|
||||||
|
if (sel === '.dark' || sel === ':root.dark') return 'dark';
|
||||||
|
const variantMatch = sel.match(/\[data-theme=['"]([a-z-]+)['"]\]/);
|
||||||
|
if (variantMatch) {
|
||||||
|
return sel.includes('.dark') ? `${variantMatch[1]}.dark` : variantMatch[1];
|
||||||
|
}
|
||||||
|
return sel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapse blocks that share a canonical identity (e.g. `.dark` and
|
||||||
|
* `:root.dark` both represent "dark"; each per-variant block and its
|
||||||
|
* `.dark` counterpart stay separate).
|
||||||
|
*/
|
||||||
|
function mergeBlocks(blocks) {
|
||||||
|
const merged = new Map();
|
||||||
|
for (const [sel, tokens] of blocks) {
|
||||||
|
const existing = merged.get(sel) ?? new Set();
|
||||||
|
for (const t of tokens) existing.add(t);
|
||||||
|
merged.set(sel, existing);
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
const src = readFileSync(THEMES_CSS, 'utf8');
|
||||||
|
const blocks = mergeBlocks(parseBlocks(src));
|
||||||
|
|
||||||
|
if (!blocks.has(':root')) {
|
||||||
|
console.error('✗ Could not find :root block in themes.css — parser broken?');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonical token set = :root tokens, minus theme-agnostic ones.
|
||||||
|
const canonical = new Set([...blocks.get(':root')].filter((t) => !THEME_AGNOSTIC.test(t)));
|
||||||
|
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
for (const [selector, tokens] of blocks) {
|
||||||
|
if (selector === ':root') continue;
|
||||||
|
|
||||||
|
const filtered = new Set([...tokens].filter((t) => !THEME_AGNOSTIC.test(t)));
|
||||||
|
|
||||||
|
const missing = [...canonical].filter((t) => !filtered.has(t));
|
||||||
|
const extra = [...filtered].filter((t) => !canonical.has(t));
|
||||||
|
|
||||||
|
if (missing.length > 0) violations.push({ selector, kind: 'missing', tokens: missing });
|
||||||
|
if (extra.length > 0) violations.push({ selector, kind: 'extra', tokens: extra });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.error(`\n✗ Theme parity check FAILED (${violations.length} block-level issue(s)):\n`);
|
||||||
|
for (const v of violations) {
|
||||||
|
const marker = v.kind === 'missing' ? 'missing from' : 'extra in';
|
||||||
|
console.error(` ${v.tokens.length} token(s) ${marker} [${v.selector}]:`);
|
||||||
|
for (const t of v.tokens.slice(0, 10)) console.error(` - ${t}`);
|
||||||
|
if (v.tokens.length > 10) console.error(` … +${v.tokens.length - 10} more`);
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
`\nEvery --color-* token must be defined in :root AND .dark AND every\n` +
|
||||||
|
`[data-theme="..."] block. A missing token silently falls back to\n` +
|
||||||
|
`inherit (or undefined), which produces invisible text in the affected\n` +
|
||||||
|
`variant. Theme-agnostic accents (--color-branch-*, --color-mana) are\n` +
|
||||||
|
`exempt. See packages/shared-tailwind/src/themes.css.\n`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenCount = canonical.size;
|
||||||
|
const variantCount = blocks.size - 1; // minus :root
|
||||||
|
console.log(`✓ Theme parity: ${tokenCount} tokens × ${variantCount} variants — all aligned.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate();
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audit Theme Tokens
|
* Validate theme CSS variable names.
|
||||||
*
|
*
|
||||||
* The Mana theme system standardizes on `--color-*` CSS custom properties
|
* The Mana theme system standardizes on `--color-*` CSS custom properties
|
||||||
* (defined in `packages/shared-tailwind/src/themes.css`). Earlier, components
|
* (defined in `packages/shared-tailwind/src/themes.css`). Earlier, components
|
||||||
|
|
@ -10,12 +10,15 @@
|
||||||
* references silently fell back to nothing (or to literal fallbacks) and
|
* references silently fell back to nothing (or to literal fallbacks) and
|
||||||
* stopped tracking the active theme variant.
|
* stopped tracking the active theme variant.
|
||||||
*
|
*
|
||||||
* This audit greps Svelte/CSS/TS source files for those legacy patterns
|
* This check greps Svelte/CSS/TS source files for those legacy patterns
|
||||||
* and fails if any remain. Run in CI and lint-staged so the drift can't
|
* and fails if any remain. Run in CI and lint-staged so the drift can't
|
||||||
* sneak back in.
|
* sneak back in.
|
||||||
*
|
*
|
||||||
|
* Companion check: `validate-theme-utilities.mjs` enforces theme-token
|
||||||
|
* Tailwind classes (e.g. `text-foreground` instead of `text-white/80`).
|
||||||
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node scripts/audit-theme-tokens.mjs
|
* node scripts/validate-theme-variables.mjs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readdirSync, statSync, readFileSync } from 'fs';
|
import { readdirSync, statSync, readFileSync } from 'fs';
|
||||||
Loading…
Add table
Add a link
Reference in a new issue