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:
parent
fd24254a7a
commit
5d5fda4127
3 changed files with 100 additions and 45 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue