feat(@projects/@magic-civilization): 🎨 token aliasing + tier the tech tokens (B cluster-1)

Make the design-token system genuinely layered instead of flat single-tier.

- build-ui-theme.py: add W3C-style alias resolution. A token $value may now be
  a reference `{color.x.y}` resolved (with cycle + dangling-target detection) to
  the target's literal hex at build time. Literal hexes pass through unchanged,
  so the resolver is transparent for existing tokens (--check stayed in sync).
- design-tokens.json: introduce a primitive `palette.*` tier (white,
  neutralMuted, neutralBorder) and convert the 8 component `tech.*` tokens from
  bespoke hex into ALIASES: researched→semantic.positive, available→accent.gold,
  available border→accent.goldBright, current→accent.science, locked→palette
  neutrals, selected→palette.white. tech.* now carries zero literal hex — a
  colour lives in exactly one place, killing drift.

Rationale: the prior `tech.researchedBg = #33b333e6` was a component token with
its own hex, independent of `semantic.positive` — the duplication the token
system exists to prevent. Now component → semantic → primitive.

Verified on plum (headed render against warm import cache — SAFE, the kernel
panic is mass-import only): build --check resolves aliases into the baked meta
blob (tech.researchedBg→66e666 etc.); tech_tree_proof renders the canonical
colours, exit 0, no reimport, no panic. Screenshot reviewed in conversation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-18 19:47:21 -05:00
parent fd24254a7a
commit 5d5fda4127
3 changed files with 100 additions and 45 deletions

View file

@ -11,46 +11,63 @@
]
},
"color": {
"tech": {
"researchedBg": {
"$value": "#33b333e6",
"$type": "color",
"$description": "Knowledge-tree node card — already-researched fill"
},
"researchedBorder": {
"$value": "#4de64d",
"$type": "color",
"$description": "Knowledge-tree node card — already-researched border"
},
"availableBg": {
"$value": "#d9bf1ae6",
"$type": "color",
"$description": "Knowledge-tree node card — available-to-research fill"
},
"availableBorder": {
"$value": "#ffe64d",
"$type": "color",
"$description": "Knowledge-tree node card — available-to-research border"
},
"lockedBg": {
"$value": "#666666b3",
"$type": "color",
"$description": "Knowledge-tree node card — locked/unreachable fill"
},
"lockedBorder": {
"$value": "#808080",
"$type": "color",
"$description": "Knowledge-tree node card — locked/unreachable border"
},
"currentBg": {
"$value": "#4d80e6e6",
"$type": "color",
"$description": "Knowledge-tree node card — currently-researching fill"
},
"selectedBorder": {
"palette": {
"white": {
"$value": "#ffffffff",
"$type": "color",
"$description": "Knowledge-tree node card — selected/current highlight border"
"$description": "Primitive — pure white (selection highlights, contrast strokes)"
},
"neutralMuted": {
"$value": "#666666b3",
"$type": "color",
"$description": "Primitive — muted neutral fill (disabled/locked surfaces)"
},
"neutralBorder": {
"$value": "#808080ff",
"$type": "color",
"$description": "Primitive — neutral border (disabled/locked outlines)"
}
},
"tech": {
"researchedBg": {
"$value": "{color.semantic.positive}",
"$type": "color",
"$description": "Knowledge-tree node card — researched fill (alias → success green)"
},
"researchedBorder": {
"$value": "{color.semantic.positive}",
"$type": "color",
"$description": "Knowledge-tree node card — researched border (alias → success green)"
},
"availableBg": {
"$value": "{color.accent.gold}",
"$type": "color",
"$description": "Knowledge-tree node card — available fill (alias → actionable gold)"
},
"availableBorder": {
"$value": "{color.accent.goldBright}",
"$type": "color",
"$description": "Knowledge-tree node card — available border (alias → bright gold)"
},
"lockedBg": {
"$value": "{color.palette.neutralMuted}",
"$type": "color",
"$description": "Knowledge-tree node card — locked fill (alias → muted neutral)"
},
"lockedBorder": {
"$value": "{color.palette.neutralBorder}",
"$type": "color",
"$description": "Knowledge-tree node card — locked border (alias → neutral border)"
},
"currentBg": {
"$value": "{color.accent.science}",
"$type": "color",
"$description": "Knowledge-tree node card — researching fill (alias → science blue)"
},
"selectedBorder": {
"$value": "{color.palette.white}",
"$type": "color",
"$description": "Knowledge-tree node card — selection highlight (alias → white)"
}
},
"unlockAccent": {

View file

@ -102,7 +102,7 @@ corner_radius_bottom_left = 2
corner_radius_bottom_right = 2
[resource]
metadata/tokens = "{\"accent.gold\":\"d9a020\",\"accent.goldBright\":\"d9b33f\",\"accent.goldPress\":\"ffd14d\",\"accent.goldResource\":\"f2d133\",\"accent.ping\":\"ffd973\",\"accent.sage\":\"66b866\",\"accent.science\":\"66bfff\",\"background.base\":\"1a1410\",\"background.deepest\":\"171219\",\"background.happiness\":\"0f0d07\",\"background.hud\":\"00000099\",\"background.list\":\"120e1e\",\"background.listSelected\":\"3f2d0d\",\"background.menu\":\"0e0a17\",\"background.overlay\":\"0000009e\",\"background.panel\":\"17121e\",\"background.raised\":\"2a2018\",\"background.surface\":\"221a14\",\"border.divider\":\"99731f80\",\"border.focus\":\"d9b340ff\",\"border.happiness\":\"b39940d9\",\"border.list\":\"4d4014b2\",\"border.listSelected\":\"d9b340cc\",\"border.panel\":\"73591fcc\",\"button.bgHover\":\"331a0d\",\"button.bgNormal\":\"1f1733\",\"button.bgPressed\":\"472f0f\",\"climate.cold\":\"1a4dff\",\"climate.hot\":\"ff260d\",\"climate.textCold\":\"66b3ff\",\"climate.textNeutral\":\"d9e0d9\",\"climate.textWarming\":\"ff731a\",\"climate.warm\":\"26cc40\",\"fog.explored\":\"000000b2\",\"fog.unexplored\":\"1a160fff\",\"guide.bgPrimary\":\"1a1410\",\"guide.bgSecondary\":\"221a14\",\"guide.bgTertiary\":\"2a2018\",\"guide.dwarfAccent\":\"8b6a1a\",\"guide.dwarfPrimary\":\"c07040\",\"guide.dwarfPrimaryDark\":\"8a4a28\",\"guide.dwarfPrimaryLight\":\"e09868\",\"guide.textMuted\":\"7a6048\",\"guide.textPrimary\":\"f0e4d0\",\"guide.textSecondary\":\"b8a078\",\"player.blue\":\"3366ff\",\"player.brown\":\"806659\",\"player.cyan\":\"1accd9\",\"player.gray\":\"999999\",\"player.green\":\"33cc4d\",\"player.magenta\":\"cc4d80\",\"player.navy\":\"4d4d99\",\"player.orange\":\"e6801a\",\"player.purple\":\"b24de6\",\"player.red\":\"e63333\",\"player.sage\":\"66b366\",\"player.yellow\":\"e6cc1a\",\"semantic.diplomacy\":\"e68c73\",\"semantic.goldenAge\":\"ffeb66\",\"semantic.negative\":\"d95940\",\"semantic.positive\":\"66e666\",\"semantic.trade\":\"ccbf73\",\"semantic.warning\":\"e69933\",\"tech.availableBg\":\"d9bf1ae6\",\"tech.availableBorder\":\"ffe64d\",\"tech.currentBg\":\"4d80e6e6\",\"tech.lockedBg\":\"666666b3\",\"tech.lockedBorder\":\"808080\",\"tech.researchedBg\":\"33b333e6\",\"tech.researchedBorder\":\"4de64d\",\"tech.selectedBorder\":\"ffffffff\",\"text.button\":\"e0d199\",\"text.buttonHover\":\"ffeb80\",\"text.buttonPressed\":\"ffffb3\",\"text.disabled\":\"80806680\",\"text.muted\":\"b2b2b2\",\"text.primary\":\"e0d8c8\",\"text.secondary\":\"bfb7a6\",\"text.title\":\"f2d973\",\"throne.court\":\"665947\",\"throne.default\":\"40382e\",\"throne.forge\":\"804714\",\"throne.garden\":\"1f522e\",\"throne.mapTable\":\"264d59\",\"throne.pedestal\":\"8c7a33\",\"throne.provisions\":\"4d6626\",\"throne.seat\":\"8c6b1a\",\"throne.shrine\":\"334766\",\"throne.special\":\"73528c\",\"throne.structure\":\"4d3824\",\"throne.trophy\":\"802e1a\",\"unlockAccent.building\":\"8b6914\",\"unlockAccent.dim\":\"ffffff8c\",\"unlockAccent.improvement\":\"4a7c3f\",\"unlockAccent.lens\":\"2d5a8b\",\"unlockAccent.mechanic\":\"6b3fa0\",\"unlockAccent.resource\":\"a0522d\",\"unlockAccent.unit\":\"c9a84c\",\"unlockAccent.wonder\":\"a06a3f\"}"
metadata/tokens = "{\"accent.gold\":\"d9a020\",\"accent.goldBright\":\"d9b33f\",\"accent.goldPress\":\"ffd14d\",\"accent.goldResource\":\"f2d133\",\"accent.ping\":\"ffd973\",\"accent.sage\":\"66b866\",\"accent.science\":\"66bfff\",\"background.base\":\"1a1410\",\"background.deepest\":\"171219\",\"background.happiness\":\"0f0d07\",\"background.hud\":\"00000099\",\"background.list\":\"120e1e\",\"background.listSelected\":\"3f2d0d\",\"background.menu\":\"0e0a17\",\"background.overlay\":\"0000009e\",\"background.panel\":\"17121e\",\"background.raised\":\"2a2018\",\"background.surface\":\"221a14\",\"border.divider\":\"99731f80\",\"border.focus\":\"d9b340ff\",\"border.happiness\":\"b39940d9\",\"border.list\":\"4d4014b2\",\"border.listSelected\":\"d9b340cc\",\"border.panel\":\"73591fcc\",\"button.bgHover\":\"331a0d\",\"button.bgNormal\":\"1f1733\",\"button.bgPressed\":\"472f0f\",\"climate.cold\":\"1a4dff\",\"climate.hot\":\"ff260d\",\"climate.textCold\":\"66b3ff\",\"climate.textNeutral\":\"d9e0d9\",\"climate.textWarming\":\"ff731a\",\"climate.warm\":\"26cc40\",\"fog.explored\":\"000000b2\",\"fog.unexplored\":\"1a160fff\",\"guide.bgPrimary\":\"1a1410\",\"guide.bgSecondary\":\"221a14\",\"guide.bgTertiary\":\"2a2018\",\"guide.dwarfAccent\":\"8b6a1a\",\"guide.dwarfPrimary\":\"c07040\",\"guide.dwarfPrimaryDark\":\"8a4a28\",\"guide.dwarfPrimaryLight\":\"e09868\",\"guide.textMuted\":\"7a6048\",\"guide.textPrimary\":\"f0e4d0\",\"guide.textSecondary\":\"b8a078\",\"palette.neutralBorder\":\"808080ff\",\"palette.neutralMuted\":\"666666b3\",\"palette.white\":\"ffffffff\",\"player.blue\":\"3366ff\",\"player.brown\":\"806659\",\"player.cyan\":\"1accd9\",\"player.gray\":\"999999\",\"player.green\":\"33cc4d\",\"player.magenta\":\"cc4d80\",\"player.navy\":\"4d4d99\",\"player.orange\":\"e6801a\",\"player.purple\":\"b24de6\",\"player.red\":\"e63333\",\"player.sage\":\"66b366\",\"player.yellow\":\"e6cc1a\",\"semantic.diplomacy\":\"e68c73\",\"semantic.goldenAge\":\"ffeb66\",\"semantic.negative\":\"d95940\",\"semantic.positive\":\"66e666\",\"semantic.trade\":\"ccbf73\",\"semantic.warning\":\"e69933\",\"tech.availableBg\":\"d9a020\",\"tech.availableBorder\":\"d9b33f\",\"tech.currentBg\":\"66bfff\",\"tech.lockedBg\":\"666666b3\",\"tech.lockedBorder\":\"808080ff\",\"tech.researchedBg\":\"66e666\",\"tech.researchedBorder\":\"66e666\",\"tech.selectedBorder\":\"ffffffff\",\"text.button\":\"e0d199\",\"text.buttonHover\":\"ffeb80\",\"text.buttonPressed\":\"ffffb3\",\"text.disabled\":\"80806680\",\"text.muted\":\"b2b2b2\",\"text.primary\":\"e0d8c8\",\"text.secondary\":\"bfb7a6\",\"text.title\":\"f2d973\",\"throne.court\":\"665947\",\"throne.default\":\"40382e\",\"throne.forge\":\"804714\",\"throne.garden\":\"1f522e\",\"throne.mapTable\":\"264d59\",\"throne.pedestal\":\"8c7a33\",\"throne.provisions\":\"4d6626\",\"throne.seat\":\"8c6b1a\",\"throne.shrine\":\"334766\",\"throne.special\":\"73528c\",\"throne.structure\":\"4d3824\",\"throne.trophy\":\"802e1a\",\"unlockAccent.building\":\"8b6914\",\"unlockAccent.dim\":\"ffffff8c\",\"unlockAccent.improvement\":\"4a7c3f\",\"unlockAccent.lens\":\"2d5a8b\",\"unlockAccent.mechanic\":\"6b3fa0\",\"unlockAccent.resource\":\"a0522d\",\"unlockAccent.unit\":\"c9a84c\",\"unlockAccent.wonder\":\"a06a3f\"}"
Button/colors/font_color = Color(0.878431, 0.819608, 0.6, 1)
Button/colors/font_hover_color = Color(1, 0.921569, 0.501961, 1)
Button/colors/font_pressed_color = Color(1, 1, 0.701961, 1)

View file

@ -30,6 +30,7 @@ from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
@ -51,9 +52,11 @@ def load_tokens() -> dict:
def _walk_colors(node: dict, prefix: str, out: dict[str, str]) -> None:
"""Recursively flatten the `color.*` subtree into dotted -> hex strings.
"""Recursively flatten the `color.*` subtree into dotted -> raw-value strings.
A token leaf is a dict carrying a `$value`. Intermediate groups recurse.
The raw `$value` is preserved verbatim (literal hex OR a `{color.x.y}`
alias reference) alias resolution happens in `_resolve_aliases`.
"""
for key, value in node.items():
if key.startswith("$"):
@ -62,16 +65,51 @@ def _walk_colors(node: dict, prefix: str, out: dict[str, str]) -> None:
continue
path = f"{prefix}.{key}" if prefix else key
if "$value" in value:
out[path] = str(value["$value"]).lstrip("#").lower()
out[path] = str(value["$value"])
else:
_walk_colors(value, path, out)
_ALIAS_RE = re.compile(r"^\{(.+)\}$")
def _resolve_aliases(raw: dict[str, str]) -> dict[str, str]:
"""Resolve W3C-style `{color.x.y}` alias references to literal `rrggbb[aa]`.
Tiered tokens (component -> semantic -> primitive) reference each other so a
colour lives in exactly one place. Literal hexes pass through unchanged.
Detects cycles and dangling targets so a typo fails the build loudly.
"""
resolved: dict[str, str] = {}
def resolve(name: str, seen: frozenset[str]) -> str:
if name in resolved:
return resolved[name]
if name not in raw:
raise ValueError(f"alias target not found: '{name}'")
if name in seen:
raise ValueError(f"alias cycle through '{name}'")
match = _ALIAS_RE.match(raw[name].strip())
if match:
target = match.group(1).strip()
if target.startswith("color."):
target = target[len("color."):]
value = resolve(target, seen | {name})
else:
value = raw[name].lstrip("#").lower()
resolved[name] = value
return value
for token_name in raw:
resolve(token_name, frozenset())
return resolved
def flatten_color_tokens(tokens: dict) -> dict[str, str]:
"""All `color.*` tokens as a flat {dotted_name: 'rrggbb[aa]'} dict."""
out: dict[str, str] = {}
_walk_colors(tokens.get("color", {}), "", out)
return dict(sorted(out.items()))
"""All `color.*` tokens as flat {dotted_name: 'rrggbb[aa]'}, aliases resolved."""
raw: dict[str, str] = {}
_walk_colors(tokens.get("color", {}), "", raw)
return dict(sorted(_resolve_aliases(raw).items()))
def _px(value) -> float: