mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 19:53:59 +01:00
`tokens.md` freezes the palette as prose and three packages re-express it
as code -- `web_ui/src/tokens.css`, `design_system`'s `tokens.dart`, and
gatelock's `LockConfig`. Nothing compared them, so "one palette
everywhere" held only for as long as four hand-maintained copies happened
to agree. A hex edited in one stack drifted silently in the other three.
`palette_check.py` parses all four and runs two halves:
* agreement -- every mapped token carries the same hex in every
non-exempt stack
* completeness -- every colour parsed from any source must be mapped or
exempted with a stated reason
The second half is the load-bearing one. A pure agreement check passes
happily when someone adds a token to one stack and never touches the map,
which is the drift most likely to actually happen; an unmapped token is
therefore a failure, not a skip. Absences are declared per token per stack
with a reason (Tk has no light theme and no borders; the categorical ramp
is web-only) so "this stack doesn't have it" is on the record rather than
a silent pass.
It found one real gap on its first run: `tokens.css` sets `--surface-2` to
`paper-raised` in the light block -- the light theme collapses both
elevation steps onto one surface -- which `tokens.md` documented only in
prose and no map covered. Now an explicitly compared row, so if either
half of that pair moves, it fails.
Proven to fail, not just to pass: drift injected into each of the four
sources in turn, plus an unmapped token, was caught every time with the
offending stack named; re-proven after the 250-line-cap split into
`palette_map.py`. Runs from its own workflow with deliberately no `paths:`
filter -- drift is a disagreement *between* packages, so a path-scoped
trigger would skip exactly the edit that causes it. That was not
hypothetical: `ramp_check.py` was reachable only from the
`web_ui/**`-filtered workflow, so a ramp edit in `tokens.md` or
`design_system` never ran it. It moves here too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
163 lines
5.2 KiB
Python
163 lines
5.2 KiB
Python
"""The canonical palette table: one row per colour, named in each stack.
|
|
|
|
Split out of `palette_check.py` to hold both files under the 250-line cap.
|
|
This module is pure data -- the parsing and comparison logic lives in the
|
|
checker. Adding a token to any stack means adding a row here; the checker's
|
|
completeness half fails on a token no row accounts for, which is what stops
|
|
this table from quietly falling behind the sources it describes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
STACKS = ("md", "css", "dart", "tk")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Token:
|
|
"""One canonical colour token and its name in each stack.
|
|
|
|
A `None` name means the token is intentionally absent from that stack;
|
|
`why` records the reason so an absence is a decision on the record rather
|
|
than an oversight.
|
|
"""
|
|
|
|
canonical: str
|
|
md: str | None
|
|
css: str | None
|
|
dart: str | None
|
|
tk: str | None
|
|
why: str = ""
|
|
|
|
|
|
# The canonical palette: one row per colour, named per stack.
|
|
#
|
|
# Deliberate absences (the `None`s) and their reasons:
|
|
# * paper/paper-raised/line-light/text-on-light/muted-on-light -- the light
|
|
# theme. CSS carries it in a `prefers-color-scheme` block (parsed
|
|
# separately below); Dart names them as constants; Tk has no light theme
|
|
# at all, because a locker surface is always dark.
|
|
# * cat-1..6 -- the categorical ramp shipped for web only. Dart and Tk have
|
|
# no chart/tag surfaces yet. Adding one means adding it here too.
|
|
# * on-scrim -- introduced by the web layer for translucent overlays; the
|
|
# other two stacks have no scrim surface.
|
|
PALETTE: tuple[Token, ...] = (
|
|
# --- neutrals, dark (all three code stacks) ---------------------------
|
|
Token("ink", "ink", "--bg", "ink", "bg"),
|
|
Token("ink-raised-1", "ink-raised-1", "--surface-1", "inkRaised1", "field_bg"),
|
|
Token(
|
|
"ink-raised-2",
|
|
"ink-raised-2",
|
|
"--surface-2",
|
|
"inkRaised2",
|
|
None,
|
|
why="Tk gates use a single raised step (field_bg); no second elevation.",
|
|
),
|
|
Token(
|
|
"line-dark",
|
|
"line-dark",
|
|
"--border",
|
|
"lineDark",
|
|
None,
|
|
why="Tk gates draw no borders; elevation is a fill step.",
|
|
),
|
|
Token("text-on-dark", "text-on-dark", "--text", "textOnDark", "fg"),
|
|
Token("muted-on-dark", "muted-on-dark", "--text-muted", "mutedOnDark", "muted"),
|
|
# --- neutrals, light --------------------------------------------------
|
|
Token(
|
|
"paper",
|
|
"paper",
|
|
"--bg@light",
|
|
"paper",
|
|
None,
|
|
why="Tk: locker surfaces are always dark; there is no light theme.",
|
|
),
|
|
Token(
|
|
"paper-raised",
|
|
"paper-raised",
|
|
"--surface-1@light",
|
|
"paperRaised",
|
|
None,
|
|
why="Tk: no light theme.",
|
|
),
|
|
# The light theme collapses both dark elevation steps onto one raised
|
|
# surface: `--surface-2` is redefined to `paper-raised` rather than
|
|
# getting a value of its own. Mapped explicitly (not exempted) so it is
|
|
# still compared -- if either half of the pair moves, this row fails.
|
|
Token(
|
|
"paper-raised-2",
|
|
"paper-raised",
|
|
"--surface-2@light",
|
|
"paperRaised",
|
|
None,
|
|
why="Light theme has one raised step; Tk has no light theme.",
|
|
),
|
|
Token(
|
|
"line-light",
|
|
"line-light",
|
|
"--border@light",
|
|
"lineLight",
|
|
None,
|
|
why="Tk: no light theme, and no borders.",
|
|
),
|
|
Token(
|
|
"text-on-light",
|
|
"text-on-light",
|
|
"--text@light",
|
|
"textOnLight",
|
|
None,
|
|
why="Tk: no light theme.",
|
|
),
|
|
Token(
|
|
"muted-on-light",
|
|
"muted-on-light",
|
|
"--text-muted@light",
|
|
"mutedOnLight",
|
|
None,
|
|
why="Tk: no light theme.",
|
|
),
|
|
# --- accent + semantic roles (all three) -------------------------------
|
|
Token("accent", "accent", "--accent", "accent", "accent"),
|
|
Token(
|
|
"info",
|
|
"info",
|
|
"--info",
|
|
"info",
|
|
None,
|
|
why="Tk: no informational role distinct from accent in the gates.",
|
|
),
|
|
Token("success", "success", "--success", "success", "success"),
|
|
Token("warning", "warning", "--warning", "warning", "warning"),
|
|
Token("danger", "danger", "--danger", "danger", "danger"),
|
|
Token("on-fill", "on-fill", "--on-fill", "onFill", "on_fill"),
|
|
Token("focus-ring", "focus-ring", "--focus-ring", None, "focus_ring"),
|
|
Token(
|
|
"on-scrim",
|
|
None,
|
|
"--on-scrim",
|
|
None,
|
|
None,
|
|
why="Web-only: no scrim/overlay surface in the Flutter or Tk layers.",
|
|
),
|
|
# --- categorical ramp (web only, for now) ------------------------------
|
|
*(
|
|
Token(
|
|
f"cat-{i}",
|
|
f"cat-{i}",
|
|
f"--cat-{i}",
|
|
None,
|
|
None,
|
|
why="Ramp shipped for web only; no chart/tag surface in Dart or Tk yet.",
|
|
)
|
|
for i in range(1, 7)
|
|
),
|
|
)
|
|
|
|
# Tokens a source legitimately defines that are not colours in the palette --
|
|
# structural/typographic values checked by the scale check, not the hex check.
|
|
NON_COLOUR_CSS = re.compile(
|
|
r"^--(radius|space|text|tracking|font|measure|shadow)-|^--(font-sans|font-mono|measure)$"
|
|
)
|