aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-11 23:36:51 -0500
committerCraig Jennings <c@cjennings.net>2026-07-11 23:36:51 -0500
commit440877965e8879f3c9567cf923b7c65c0eee227c (patch)
tree86d10cc52d87aef2e3f5234a35ba327bf81479d3 /docs
parent18d4b33e9338fac2a316f51a479574db07d7cedc (diff)
downloadarchsetup-440877965e8879f3c9567cf923b7c65c0eee227c.tar.gz
archsetup-440877965e8879f3c9567cf923b7c65c0eee227c.zip
refactor(gallery): extract design tokens to one source for 3 targets
The colors, glows, gradient ramp, and pulse rate were hardcoded literals scattered across ~40 sites in the gallery. Retuning the amber meant a find-and-replace through the whole file. I pulled them into tokens.json as the single source. gen_tokens.py reads that source and emits three targets: web CSS custom properties (into the gallery :root, between markers), waybar GTK @define-color declarations, and an elisp alist for the future svg.el renderer. The three genuinely differ (CSS --vars with rgb-triple glows, GTK @define-color with underscore names, elisp hex alist), which is why one generator beats three hand-maintained copies. The amber hue is defined once, and its glow triples are derived from the hex, so retuning it means editing one line and rerunning the generator. The gallery render is unchanged. I verified it pixel-identical against the prior commit with reduced-motion forced and the live clock masked (diff of 0). Tests cover hex conversion, the three emitters, marker replacement, and idempotency: 27 tests, 100% line coverage, wired into make test-unit.
Diffstat (limited to 'docs')
-rw-r--r--docs/prototypes/gen_tokens.py180
-rw-r--r--docs/prototypes/panel-widget-gallery.html109
-rw-r--r--docs/prototypes/tokens-waybar.css34
-rw-r--r--docs/prototypes/tokens.el41
-rw-r--r--docs/prototypes/tokens.json46
5 files changed, 370 insertions, 40 deletions
diff --git a/docs/prototypes/gen_tokens.py b/docs/prototypes/gen_tokens.py
new file mode 100644
index 0000000..f966451
--- /dev/null
+++ b/docs/prototypes/gen_tokens.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+"""Single-source design-token generator for the panel widget gallery.
+
+tokens.json is the neutral source of truth for the gallery's design tokens
+(palette, the amber family, glows, font, timing). This script reads it and
+emits three target representations so the same look and feel travels across
+every place the widgets get built:
+
+ web CSS :root { --gold:#e2a038; --glow-hi:255,190,84; ... }
+ waybar GTK @define-color gold #e2a038; (GTK has no custom properties)
+ Emacs (defconst gallery-tokens '((gold . "#e2a038") ...)) for svg.el
+
+The three differ on purpose, which is the whole reason a generator earns its
+keep instead of hand-maintaining three copies:
+
+ - CSS custom props are hyphenated (--glow-hi) and store glow colors as bare
+ "r,g,b" triples so rgba(var(--glow-hi),.5) resolves with a variable alpha.
+ - GTK CSS has no custom properties; it uses @define-color with underscore
+ names and resolves glows to their source hex (GTK does alpha(@color,a)).
+ - Elisp uses a hyphenated-symbol alist of hex strings, which svg.el /
+ librsvg consume directly (a bare triple is not a color there).
+
+The amber hue is defined once, in tokens.json. Both its solid form (--gold)
+and its glow form (--glow-hi, the rgb triple) are derived here, so retuning
+the amber is a one-line edit to the source plus a regenerate.
+
+Usage:
+ python3 gen_tokens.py # regenerate all three targets in place
+
+The web CSS is written into panel-widget-gallery.html between the
+`/* @tokens:start */` and `/* @tokens:end */` markers; the waybar and elisp
+targets are written to tokens-waybar.css and tokens.el beside it.
+"""
+
+import json
+import os
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+TOKENS_START = "/* @tokens:start */"
+TOKENS_END = "/* @tokens:end */"
+
+# sections whose values are plain colors (searched by resolve_color, and the
+# ones that become @define-color / alist color entries)
+COLOR_SECTIONS = ("palette", "amber")
+
+
+def hex_to_triple(h):
+ """'#ffbe54' -> '255,190,84'. Accepts 3- or 6-digit hex, '#' optional."""
+ h = h.lstrip("#")
+ if len(h) == 3:
+ h = "".join(c * 2 for c in h)
+ if len(h) != 6:
+ raise ValueError(f"expected 3- or 6-digit hex, got {h!r}")
+ try:
+ r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4))
+ except ValueError:
+ raise ValueError(f"non-hex digits in {h!r}")
+ return f"{r},{g},{b}"
+
+
+def resolve_color(tokens, key):
+ """Return the hex for a color named in palette or amber; KeyError if absent."""
+ for section in COLOR_SECTIONS:
+ if key in tokens.get(section, {}):
+ return tokens[section][key]
+ raise KeyError(key)
+
+
+def _el_str(v):
+ """Quote a value as an elisp string literal, escaping backslashes/quotes."""
+ return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"'
+
+
+def emit_web_css(tokens):
+ """The inner :root block: hyphenated --vars, glows as rgb triples."""
+ lines = []
+ for section in COLOR_SECTIONS:
+ for k, v in tokens.get(section, {}).items():
+ lines.append(f"--{k}:{v};")
+ for name, source in tokens.get("glow", {}).items():
+ lines.append(f"--{name}:{hex_to_triple(resolve_color(tokens, source))};")
+ for k, v in tokens.get("font", {}).items():
+ lines.append(f"--{k}:{v};")
+ for k, v in tokens.get("timing", {}).items():
+ lines.append(f"--{k}:{v};")
+ return "\n".join(" " + line for line in lines)
+
+
+def emit_waybar_gtk(tokens):
+ """GTK @define-color declarations; underscore names, glows resolved to hex."""
+ def gtk(name):
+ return name.replace("-", "_")
+
+ lines = [
+ "/* generated from tokens.json by gen_tokens.py — do not edit by hand.",
+ " GTK CSS has no custom properties; reference these as @name, and use",
+ " alpha(@name, 0.5) where the web build uses rgba(var(--name),.5). */",
+ ]
+ for section in COLOR_SECTIONS:
+ for k, v in tokens.get(section, {}).items():
+ lines.append(f"@define-color {gtk(k)} {v};")
+ for name, source in tokens.get("glow", {}).items():
+ lines.append(f"@define-color {gtk(name)} {resolve_color(tokens, source)};")
+ return "\n".join(lines)
+
+
+def emit_elisp(tokens):
+ """An alist of (name . hex) plus timing, for the future svg.el renderer."""
+ pairs = []
+ for section in COLOR_SECTIONS:
+ for k, v in tokens.get(section, {}).items():
+ pairs.append(f"({k} . {_el_str(v)})")
+ for name, source in tokens.get("glow", {}).items():
+ pairs.append(f"({name} . {_el_str(resolve_color(tokens, source))})")
+ for k, v in tokens.get("font", {}).items():
+ pairs.append(f"({k} . {_el_str(v)})")
+ for k, v in tokens.get("timing", {}).items():
+ pairs.append(f"({k} . {_el_str(v)})")
+ body = "\n ".join(pairs)
+ return (
+ ";;; gallery-tokens.el --- generated from tokens.json -*- lexical-binding: t; -*-\n"
+ ";;; Commentary:\n"
+ ";; Generated by gen_tokens.py — do not edit by hand; edit tokens.json.\n"
+ ";;; Code:\n"
+ "(defconst gallery-tokens\n '(" + body + ")\n"
+ ' "Design tokens for the panel widget gallery.")\n'
+ "(provide 'gallery-tokens)\n"
+ ";;; gallery-tokens.el ends here\n"
+ )
+
+
+def replace_between_markers(text, start, end, block):
+ """Replace the text between the start and end marker lines with block.
+
+ The marker lines themselves survive; only their interior is swapped.
+ Idempotent: re-running with the same block is a no-op. Raises ValueError
+ if either marker is missing or out of order.
+ """
+ si = text.find(start)
+ ei = text.find(end)
+ if si == -1 or ei == -1 or ei < si:
+ raise ValueError("token markers not found (or out of order)")
+ start_line_end = text.find("\n", si)
+ if start_line_end == -1:
+ start_line_end = si + len(start)
+ end_line_start = text.rfind("\n", 0, ei) + 1
+ return text[:start_line_end + 1] + block + "\n" + text[end_line_start:]
+
+
+def main(tokens_path=None, html_path=None, waybar_path=None, elisp_path=None):
+ """Regenerate all three targets from tokens.json."""
+ tokens_path = tokens_path or os.path.join(HERE, "tokens.json")
+ html_path = html_path or os.path.join(HERE, "panel-widget-gallery.html")
+ waybar_path = waybar_path or os.path.join(HERE, "tokens-waybar.css")
+ elisp_path = elisp_path or os.path.join(HERE, "tokens.el")
+
+ with open(tokens_path) as f:
+ tokens = json.load(f)
+
+ note = (" /* generated from tokens.json by gen_tokens.py — "
+ "edit tokens.json, then run: python3 gen_tokens.py */")
+ block = note + "\n" + emit_web_css(tokens)
+
+ with open(html_path) as f:
+ html = f.read()
+ html = replace_between_markers(html, TOKENS_START, TOKENS_END, block)
+ with open(html_path, "w") as f:
+ f.write(html)
+
+ with open(waybar_path, "w") as f:
+ f.write(emit_waybar_gtk(tokens) + "\n")
+ with open(elisp_path, "w") as f:
+ f.write(emit_elisp(tokens))
+
+ return {"html": html_path, "waybar": waybar_path, "elisp": elisp_path}
+
+
+if __name__ == "__main__": # pragma: no cover
+ out = main()
+ print("regenerated:", ", ".join(f"{k}={v}" for k, v in out.items()))
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index 5f1dcf3..1d95488 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -6,13 +6,42 @@
<title>Panel widget gallery — dupre instrument console (interactive)</title>
<style>
:root{
- --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
- --gold:#e2a038; --gold-hi:#ffbe54; --silver:#bfc4d0; --cream:#f3e7c5;
- --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d;
- --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d;
+/* @tokens:start */
+ /* generated from tokens.json by gen_tokens.py — edit tokens.json, then run: python3 gen_tokens.py */
+ --ground:#151311;
+ --panel:#100f0f;
+ --well:#0a0c0d;
+ --raise:#1a1917;
+ --silver:#bfc4d0;
+ --cream:#f3e7c5;
+ --steel:#969385;
+ --dim:#7c838a;
+ --slate:#424f5e;
+ --slate-hi:#54677d;
+ --wash:#2c2f32;
+ --pass:#74932f;
+ --fail:#cb6b4d;
+ --phos:#7fe0a0;
+ --phos-dim:#1c3626;
+ --vfd:#63e6c8;
+ --sevgrn:#57d357;
+ --sevred:#e2543f;
+ --sevoff:#1a1613;
+ --jewel-r:#ff5b45;
+ --jewel-a:#ffb43a;
+ --jewel-g:#6fce33;
+ --gold:#e2a038;
+ --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a;
+ --amber-grad-mid:#d29638;
+ --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16;
+ --amber-warn:#f0b552;
+ --glow-hi:255,190,84;
+ --glow-lo:226,160,56;
--mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
- --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8; --sevgrn:#57d357; --sevred:#e2543f;
- --sevoff:#1a1613; --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --pulse-rate:1s;
+/* @tokens:end */
}
*{box-sizing:border-box;margin:0;padding:0}
html{background:var(--ground)}
@@ -43,10 +72,10 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
/* ---- shared primitives ---- */
.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);box-shadow:0 0 6px 1px rgba(116,147,47,.55)}
-.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(226,160,56,.6)}
+.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(var(--glow-lo),.6)}
.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)}
.lamp.off{background:var(--wash);box-shadow:none}
-.lamp.busy{background:var(--gold);animation:pulse 1s ease-in-out infinite}
+.lamp.busy{background:var(--gold);animation:pulse var(--pulse-rate) ease-in-out infinite}
/* standard pulse: 1s ease-in-out — the norm for every pulsing / flashing element */
@keyframes pulse{50%{opacity:.25}}
@@ -64,7 +93,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
border-radius:8px;padding:8px 12px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)}
.key:hover{color:var(--gold);border-color:var(--gold)}
.key:active{transform:translateY(1px)}
-.key.on{color:var(--panel);background:linear-gradient(180deg,#f2c76a,var(--gold));border-color:var(--gold-hi);font-weight:700}
+.key.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));border-color:var(--gold-hi);font-weight:700}
.key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)}
.key.off{opacity:.4}
@@ -78,22 +107,22 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
/* fader */
.fader{width:150px;height:16px;position:relative;cursor:pointer;touch-action:none}
.fader .slot{position:absolute;top:6px;left:0;right:0;height:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden}
-.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,#8f671f,var(--gold))}
+.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold))}
.fader .cap{position:absolute;top:1px;width:7px;height:14px;border-radius:2px;margin-left:-3.5px;
- background:linear-gradient(180deg,#f2c76a,#d29638);border:1px solid #7d5c16;box-shadow:0 1px 2px rgba(0,0,0,.5)}
+ background:linear-gradient(180deg,var(--amber-grad-top),var(--amber-grad-mid));border:1px solid var(--amber-edge);box-shadow:0 1px 2px rgba(0,0,0,.5)}
/* vertical fader */
.vfader{width:16px;height:64px;position:relative;cursor:pointer;touch-action:none}
.vfader .slot{position:absolute;left:6px;top:0;bottom:0;width:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden}
-.vfader .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,#8f671f,var(--gold))}
+.vfader .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,var(--amber-grad-bot),var(--gold))}
.vfader .cap{position:absolute;left:1px;height:7px;width:14px;border-radius:2px;margin-top:-3.5px;
- background:linear-gradient(90deg,#d29638,#f2c76a);border:1px solid #7d5c16}
+ background:linear-gradient(90deg,var(--amber-grad-mid),var(--amber-grad-top));border:1px solid var(--amber-edge)}
/* rotary knob */
.knob{width:52px;height:52px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none;
background:radial-gradient(circle at 40% 35%,#2a2622,#141210);border:1px solid #3a352c;
box-shadow:inset 0 2px 3px rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5)}
.knob .ind{position:absolute;left:50%;top:5px;width:2px;height:16px;background:var(--gold-hi);
- margin-left:-1px;transform-origin:50% 21px;border-radius:1px;box-shadow:0 0 5px rgba(255,190,84,.6)}
+ margin-left:-1px;transform-origin:50% 21px;border-radius:1px;box-shadow:0 0 5px rgba(var(--glow-hi),.6)}
/* needle gauge */
.gauge{width:96px;cursor:ns-resize;touch-action:none}
@@ -101,7 +130,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.gauge .arc{position:absolute;inset:0 0 -48px 0;border:2px solid var(--wash);border-radius:50%}
.gauge .tk{position:absolute;left:50%;bottom:0;width:1.5px;height:8px;background:var(--steel);transform-origin:50% 48px}
.gauge .ndl{position:absolute;left:50%;bottom:0;width:2px;height:40px;background:var(--gold-hi);
- transform-origin:50% 100%;transform:rotate(0deg);border-radius:2px;box-shadow:0 0 6px rgba(255,190,84,.5);
+ transform-origin:50% 100%;transform:rotate(0deg);border-radius:2px;box-shadow:0 0 6px rgba(var(--glow-hi),.5);
transition:transform .12s cubic-bezier(.3,1.3,.5,1)}
.gauge .hub{position:absolute;left:50%;bottom:-4px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--gold)}
.gauge .gv{color:var(--cream);text-align:center;font-size:12px;font-weight:700;margin-top:5px;font-variant-numeric:tabular-nums}
@@ -129,7 +158,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
/* linear progress / fuel bar */
.bar{width:160px;height:12px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative;cursor:pointer;touch-action:none}
-.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,#8f671f,var(--gold));border-radius:6px}
+.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold));border-radius:6px}
.bar.warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))}
/* radial ring */
@@ -163,7 +192,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden}
.seg button{font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 11px;cursor:pointer}
.seg button:last-child{border-right:0}
-.seg button.on{background:linear-gradient(180deg,#f2c76a,var(--gold));color:var(--panel);font-weight:700}
+.seg button.on{background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));color:var(--panel);font-weight:700}
/* engraved section label */
.engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer;
@@ -189,15 +218,15 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.rotsel{position:relative;width:118px;height:74px}
.rotsel>.knob{position:absolute;left:50%;top:20px;margin-left:-26px;cursor:pointer}
.rotsel .pos{position:absolute;font-size:9px;color:var(--dim);transform:translate(-50%,-50%);letter-spacing:.02em}
-.rotsel .pos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(255,190,84,.55)}
+.rotsel .pos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.55)}
/* slide-rule tuner dial */
.tuner{width:180px;height:46px;position:relative;border-radius:6px;overflow:hidden;cursor:pointer;
background:linear-gradient(180deg,#191510,#0b0908);border:1px solid #2a251c;
- box-shadow:inset 0 0 20px rgba(226,160,56,.12),inset 0 1px 0 rgba(255,255,255,.03)}
+ box-shadow:inset 0 0 20px rgba(var(--glow-lo),.12),inset 0 1px 0 rgba(255,255,255,.03)}
.tuner .tick{position:absolute;top:6px;width:1px;height:11px;background:var(--steel);transform:translateX(-50%)}
.tuner .mk{position:absolute;bottom:8px;transform:translateX(-50%);color:var(--steel);font-size:10px}
-.tuner .mk.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(255,190,84,.6)}
+.tuner .mk.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.6)}
.tuner:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.tuner .ndl{position:absolute;top:3px;bottom:3px;width:2px;margin-left:-1px;border-radius:1px;
background:var(--fail);box-shadow:0 0 7px rgba(203,107,77,.85);transition:left .25s}
@@ -222,7 +251,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
font-size:12px;letter-spacing:.06em;color:var(--dim)}
.rocker .top{top:0;border-bottom:1px solid #0c0b0a;background:linear-gradient(180deg,#211d19,#181513)}
.rocker .bot{bottom:0;background:linear-gradient(180deg,#141210,#100e0c);box-shadow:inset 0 3px 5px rgba(0,0,0,.55)}
-.rocker.on .top{background:linear-gradient(180deg,#f2c76a,var(--gold));color:var(--panel);font-weight:700;box-shadow:0 0 10px rgba(255,190,84,.35)}
+.rocker.on .top{background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));color:var(--panel);font-weight:700;box-shadow:0 0 10px rgba(var(--glow-hi),.35)}
.rocker.on .bot{color:var(--steel)}
.rocker:not(.on) .top{color:var(--steel)}
.rocker:not(.on) .bot{background:linear-gradient(180deg,#2a1512,#1c0f0d);color:var(--fail);box-shadow:inset 0 3px 5px rgba(0,0,0,.55),0 0 8px rgba(203,107,77,.25)}
@@ -233,13 +262,13 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
background:radial-gradient(circle at 45% 40%,#211d18,#0d0b09)}
.reel::before{content:"";position:absolute;inset:9px;border-radius:50%;border:1px solid #4a443a;background:#161310}
.reel i{position:absolute;left:50%;top:50%;width:2px;height:11px;background:var(--steel);margin:-5.5px 0 0 -1px;transform-origin:50% 5.5px}
-.reel i:nth-child(1){background:var(--gold-hi);box-shadow:0 0 4px rgba(255,190,84,.55)}
+.reel i:nth-child(1){background:var(--gold-hi);box-shadow:0 0 4px rgba(var(--glow-hi),.55)}
.reel i:nth-child(2){transform:rotate(120deg)}.reel i:nth-child(3){transform:rotate(240deg)}
.transport{display:flex;gap:5px}
.tbtn{font:inherit;font-size:12px;color:var(--silver);cursor:pointer;width:30px;height:26px;display:grid;place-items:center;
background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a;border-radius:6px}
.tbtn:hover{color:var(--gold);border-color:var(--gold)}
-.tbtn.on{color:var(--panel);background:linear-gradient(180deg,#f2c76a,var(--gold));border-color:var(--gold-hi)}
+.tbtn.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));border-color:var(--gold-hi)}
.tbtn.rec.on{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)}
/* radio bank */
@@ -249,7 +278,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
background:linear-gradient(180deg,#211d19,#161310);box-shadow:inset 0 1px 0 rgba(255,255,255,.04)}
.preset+.preset{margin-left:3px}
.preset:hover{color:var(--silver)}
-.preset.on{color:var(--panel);background:linear-gradient(180deg,#f2c76a,var(--gold));font-weight:700;
+.preset.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));font-weight:700;
box-shadow:inset 0 2px 4px rgba(0,0,0,.4)}
/* concentric dual knob */
@@ -261,13 +290,13 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.dualknob .inner{width:34px;height:34px;border-radius:50%;position:absolute;left:15px;top:15px;cursor:ns-resize;touch-action:none;
background:radial-gradient(circle at 40% 35%,#37322a,#1a1713);border:1px solid #4a443a;box-shadow:0 2px 4px rgba(0,0,0,.5)}
.dualknob .inner .ind{position:absolute;left:50%;top:3px;width:2px;height:11px;background:var(--gold-hi);margin-left:-1px;
- transform-origin:50% 14px;border-radius:1px;box-shadow:0 0 5px rgba(255,190,84,.6)}
+ transform-origin:50% 14px;border-radius:1px;box-shadow:0 0 5px rgba(var(--glow-hi),.6)}
/* rotary encoder + LED ring */
.encoder{position:relative;width:66px;height:66px;display:grid;place-items:center;cursor:ns-resize;touch-action:none}
.encoder .led{position:absolute;width:5px;height:5px;border-radius:50%;background:var(--wash);
left:50%;top:50%;margin:-2.5px}
-.encoder .led.on{background:var(--gold);box-shadow:0 0 5px 1px rgba(226,160,56,.7)}
+.encoder .led.on{background:var(--gold);box-shadow:0 0 5px 1px rgba(var(--glow-lo),.7)}
.encoder .knob{width:40px;height:40px;cursor:ns-resize}
.encoder .knob .ind{transform-origin:50% 15px;top:4px;height:12px}
@@ -279,9 +308,9 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.keylock .barrel{position:absolute;left:50%;top:50%;width:5px;height:5px;margin:-2.5px;border-radius:1px;background:#0a0908;
box-shadow:0 0 2px rgba(0,0,0,.8)}
.keylock .bit{position:absolute;left:50%;top:50%;width:3px;height:15px;margin:-15px 0 0 -1.5px;background:var(--gold-hi);
- border-radius:1px;transform-origin:50% 100%;box-shadow:0 0 5px rgba(255,190,84,.55);transition:transform .2s}
+ border-radius:1px;transform-origin:50% 100%;box-shadow:0 0 5px rgba(var(--glow-hi),.55);transition:transform .2s}
.keylock .kpos{position:absolute;font-size:9px;color:var(--dim);letter-spacing:.05em;transform:translate(-50%,-50%)}
-.keylock .kpos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(255,190,84,.55)}
+.keylock .kpos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.55)}
/* crossfader */
.xfader{width:160px;height:22px;position:relative;cursor:pointer;touch-action:none}
@@ -289,7 +318,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.xfader .detent{position:absolute;top:2px;left:50%;width:1px;height:18px;background:var(--steel);margin-left:-.5px;opacity:.7}
.xfader .end{position:absolute;top:11px;font-size:9px;color:var(--steel);transform:translateY(-50%)}
.xfader .cap{position:absolute;top:3px;width:9px;height:16px;border-radius:2px;margin-left:-4.5px;transition:left .05s;
- background:linear-gradient(180deg,#f2c76a,#d29638);border:1px solid #7d5c16;box-shadow:0 1px 2px rgba(0,0,0,.5)}
+ background:linear-gradient(180deg,var(--amber-grad-top),var(--amber-grad-mid));border:1px solid var(--amber-edge);box-shadow:0 1px 2px rgba(0,0,0,.5)}
/* thumbwheel */
.thumbw{display:flex;align-items:center;gap:10px}
@@ -306,7 +335,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.dipsw{width:12px;height:26px;background:#0a1220;border-radius:2px;position:relative;cursor:pointer;border:1px solid #1c2c42}
.dipsw i{position:absolute;left:1px;right:1px;height:11px;border-radius:1px;background:linear-gradient(180deg,#eae4d2,#b4ad98);
bottom:1px;transition:bottom .12s,top .12s}
-.dipsw.on i{bottom:auto;top:1px;background:linear-gradient(180deg,#f2c76a,var(--gold))}
+.dipsw.on i{bottom:auto;top:1px;background:linear-gradient(180deg,var(--amber-grad-top),var(--gold))}
/* jog / shuttle */
.jog{position:relative;width:74px;height:74px;cursor:ns-resize;touch-action:none}
@@ -342,7 +371,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.crossm .face{position:relative;height:56px;overflow:hidden;cursor:ns-resize;touch-action:none}
.crossm .arc{position:absolute;inset:2px 4px -56px;border:2px solid var(--wash);border-radius:50%}
.crossm .nA{position:absolute;left:14px;bottom:2px;width:2px;height:52px;background:var(--gold-hi);transform-origin:50% 100%;
- border-radius:2px;box-shadow:0 0 5px rgba(255,190,84,.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)}
+ border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)}
.crossm .nB{position:absolute;right:14px;bottom:2px;width:2px;height:52px;background:var(--fail);transform-origin:50% 100%;
border-radius:2px;box-shadow:0 0 5px rgba(203,107,77,.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)}
.crossm .lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px}
@@ -362,7 +391,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
box-shadow:inset 0 2px 5px rgba(0,0,0,.6)}
.bourdon svg{position:absolute;inset:0}
.bourdon .ndl{position:absolute;left:50%;top:50%;width:2px;height:32px;margin:-32px 0 0 -1px;background:var(--gold-hi);
- transform-origin:50% 100%;border-radius:2px;box-shadow:0 0 5px rgba(255,190,84,.55);transition:transform .12s cubic-bezier(.3,1.2,.5,1)}
+ transform-origin:50% 100%;border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.55);transition:transform .12s cubic-bezier(.3,1.2,.5,1)}
.bourdon .hub{position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;background:var(--gold)}
.bourdon .cap{position:absolute;left:0;right:0;bottom:12px;text-align:center;font-size:8px;letter-spacing:.16em;color:var(--steel)}
@@ -374,7 +403,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.strip svg{position:absolute;inset:0;width:100%;height:100%}
.strip polyline{fill:none;stroke:var(--gold);stroke-width:1.4}
.strip .pen{position:absolute;right:2px;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold-hi);
- box-shadow:0 0 6px rgba(255,190,84,.8);transition:top .08s linear}
+ box-shadow:0 0 6px rgba(var(--glow-hi),.8);transition:top .08s linear}
/* correlation */
.corr{width:150px;cursor:pointer;touch-action:none}
@@ -382,7 +411,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.corr .arc{position:absolute;inset:0 0 -44px;border:2px solid var(--wash);border-radius:50%}
.corr .zero{position:absolute;left:50%;top:2px;width:1px;height:10px;background:var(--gold);margin-left:-.5px}
.corr .ndl{position:absolute;left:50%;bottom:0;width:2px;height:38px;background:var(--gold-hi);transform-origin:50% 100%;
- border-radius:2px;box-shadow:0 0 5px rgba(255,190,84,.5);transition:transform .1s ease-out}
+ border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.5);transition:transform .1s ease-out}
.corr .lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px}
/* battery */
@@ -423,8 +452,8 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.annun{display:grid;grid-template-columns:repeat(3,1fr);gap:3px}
.acell{font-size:8.5px;letter-spacing:.08em;text-align:center;color:var(--dim);padding:6px 4px;border-radius:3px;cursor:pointer;
background:#141210;border:1px solid #262320;line-height:1.2}
-.acell.warn{color:var(--panel);background:linear-gradient(180deg,#f0b552,var(--gold));font-weight:700;box-shadow:0 0 8px rgba(226,160,56,.4)}
-.acell.fault{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));font-weight:700;box-shadow:0 0 8px rgba(203,107,77,.4);animation:pulse 1s ease-in-out infinite}
+.acell.warn{color:var(--panel);background:linear-gradient(180deg,var(--amber-warn),var(--gold));font-weight:700;box-shadow:0 0 8px rgba(var(--glow-lo),.4)}
+.acell.fault{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));font-weight:700;box-shadow:0 0 8px rgba(203,107,77,.4);animation:pulse var(--pulse-rate) ease-in-out infinite}
/* jewel */
.jewel{width:24px;height:24px;border-radius:50%;position:relative;cursor:pointer;
@@ -457,13 +486,13 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.clock .mh{position:absolute;left:50%;top:50%;width:2px;height:28px;margin:-28px 0 0 -1px;background:var(--silver);
transform-origin:50% 100%;border-radius:2px}
.clock .sh{position:absolute;left:50%;top:50%;width:1px;height:30px;margin:-30px 0 0 -.5px;background:var(--gold-hi);
- transform-origin:50% 100%;box-shadow:0 0 4px rgba(255,190,84,.5)}
+ transform-origin:50% 100%;box-shadow:0 0 4px rgba(var(--glow-hi),.5)}
.clock .pin{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold)}
/* frequency-dial scale */
.freqscale{width:184px;height:44px;position:relative;border-radius:5px;overflow:hidden;cursor:pointer;touch-action:none;
background:linear-gradient(180deg,#191510,#0b0908);border:1px solid #2a251c;
- box-shadow:inset 0 0 16px rgba(226,160,56,.1)}
+ box-shadow:inset 0 0 16px rgba(var(--glow-lo),.1)}
.freqscale .tick{position:absolute;top:5px;width:1px;background:var(--steel);transform:translateX(-50%)}
.freqscale .mk{position:absolute;bottom:6px;transform:translateX(-50%);color:var(--steel);font-size:9px}
.freqscale .band{position:absolute;bottom:2px;left:6px;color:var(--gold);font-size:8px;letter-spacing:.18em}
@@ -476,8 +505,8 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.patch .row+.row{margin-top:9px}
.patch .jack{width:13px;height:13px;border-radius:50%;cursor:pointer;background:radial-gradient(circle at 40% 35%,#3a352c,#0a0908 70%);
border:1px solid #4a443a;box-shadow:inset 0 1px 2px rgba(0,0,0,.8)}
-.patch .jack.hot{background:radial-gradient(circle at 40% 35%,#7d5c16,#1a1408 70%)}
-.patch .jack.sel{border-color:var(--gold-hi);box-shadow:0 0 6px 1px rgba(255,190,84,.7)}
+.patch .jack.hot{background:radial-gradient(circle at 40% 35%,var(--amber-edge),#1a1408 70%)}
+.patch .jack.sel{border-color:var(--gold-hi);box-shadow:0 0 6px 1px rgba(var(--glow-hi),.7)}
.patch svg{position:absolute;inset:0;pointer-events:none;width:100%;height:100%}
.patch svg path{fill:none;stroke:var(--gold);stroke-width:2.4;opacity:.85;stroke-linecap:round}
diff --git a/docs/prototypes/tokens-waybar.css b/docs/prototypes/tokens-waybar.css
new file mode 100644
index 0000000..d18ba23
--- /dev/null
+++ b/docs/prototypes/tokens-waybar.css
@@ -0,0 +1,34 @@
+/* generated from tokens.json by gen_tokens.py — do not edit by hand.
+ GTK CSS has no custom properties; reference these as @name, and use
+ alpha(@name, 0.5) where the web build uses rgba(var(--name),.5). */
+@define-color ground #151311;
+@define-color panel #100f0f;
+@define-color well #0a0c0d;
+@define-color raise #1a1917;
+@define-color silver #bfc4d0;
+@define-color cream #f3e7c5;
+@define-color steel #969385;
+@define-color dim #7c838a;
+@define-color slate #424f5e;
+@define-color slate_hi #54677d;
+@define-color wash #2c2f32;
+@define-color pass #74932f;
+@define-color fail #cb6b4d;
+@define-color phos #7fe0a0;
+@define-color phos_dim #1c3626;
+@define-color vfd #63e6c8;
+@define-color sevgrn #57d357;
+@define-color sevred #e2543f;
+@define-color sevoff #1a1613;
+@define-color jewel_r #ff5b45;
+@define-color jewel_a #ffb43a;
+@define-color jewel_g #6fce33;
+@define-color gold #e2a038;
+@define-color gold_hi #ffbe54;
+@define-color amber_grad_top #f2c76a;
+@define-color amber_grad_mid #d29638;
+@define-color amber_grad_bot #8f671f;
+@define-color amber_edge #7d5c16;
+@define-color amber_warn #f0b552;
+@define-color glow_hi #ffbe54;
+@define-color glow_lo #e2a038;
diff --git a/docs/prototypes/tokens.el b/docs/prototypes/tokens.el
new file mode 100644
index 0000000..1fb6e52
--- /dev/null
+++ b/docs/prototypes/tokens.el
@@ -0,0 +1,41 @@
+;;; gallery-tokens.el --- generated from tokens.json -*- lexical-binding: t; -*-
+;;; Commentary:
+;; Generated by gen_tokens.py — do not edit by hand; edit tokens.json.
+;;; Code:
+(defconst gallery-tokens
+ '((ground . "#151311")
+ (panel . "#100f0f")
+ (well . "#0a0c0d")
+ (raise . "#1a1917")
+ (silver . "#bfc4d0")
+ (cream . "#f3e7c5")
+ (steel . "#969385")
+ (dim . "#7c838a")
+ (slate . "#424f5e")
+ (slate-hi . "#54677d")
+ (wash . "#2c2f32")
+ (pass . "#74932f")
+ (fail . "#cb6b4d")
+ (phos . "#7fe0a0")
+ (phos-dim . "#1c3626")
+ (vfd . "#63e6c8")
+ (sevgrn . "#57d357")
+ (sevred . "#e2543f")
+ (sevoff . "#1a1613")
+ (jewel-r . "#ff5b45")
+ (jewel-a . "#ffb43a")
+ (jewel-g . "#6fce33")
+ (gold . "#e2a038")
+ (gold-hi . "#ffbe54")
+ (amber-grad-top . "#f2c76a")
+ (amber-grad-mid . "#d29638")
+ (amber-grad-bot . "#8f671f")
+ (amber-edge . "#7d5c16")
+ (amber-warn . "#f0b552")
+ (glow-hi . "#ffbe54")
+ (glow-lo . "#e2a038")
+ (mono . "\"BerkeleyMono Nerd Font\",\"Berkeley Mono\",monospace")
+ (pulse-rate . "1s"))
+ "Design tokens for the panel widget gallery.")
+(provide 'gallery-tokens)
+;;; gallery-tokens.el ends here
diff --git a/docs/prototypes/tokens.json b/docs/prototypes/tokens.json
new file mode 100644
index 0000000..6974805
--- /dev/null
+++ b/docs/prototypes/tokens.json
@@ -0,0 +1,46 @@
+{
+ "_note": "Single source of truth for the panel widget gallery's design tokens. Edit here, then run: python3 gen_tokens.py (regenerates the :root block in panel-widget-gallery.html plus tokens-waybar.css and tokens.el). The amber hue is defined once; its glow rgb triples are derived by the generator, so retuning amber is a one-line change here.",
+ "palette": {
+ "ground": "#151311",
+ "panel": "#100f0f",
+ "well": "#0a0c0d",
+ "raise": "#1a1917",
+ "silver": "#bfc4d0",
+ "cream": "#f3e7c5",
+ "steel": "#969385",
+ "dim": "#7c838a",
+ "slate": "#424f5e",
+ "slate-hi": "#54677d",
+ "wash": "#2c2f32",
+ "pass": "#74932f",
+ "fail": "#cb6b4d",
+ "phos": "#7fe0a0",
+ "phos-dim": "#1c3626",
+ "vfd": "#63e6c8",
+ "sevgrn": "#57d357",
+ "sevred": "#e2543f",
+ "sevoff": "#1a1613",
+ "jewel-r": "#ff5b45",
+ "jewel-a": "#ffb43a",
+ "jewel-g": "#6fce33"
+ },
+ "amber": {
+ "gold": "#e2a038",
+ "gold-hi": "#ffbe54",
+ "amber-grad-top": "#f2c76a",
+ "amber-grad-mid": "#d29638",
+ "amber-grad-bot": "#8f671f",
+ "amber-edge": "#7d5c16",
+ "amber-warn": "#f0b552"
+ },
+ "glow": {
+ "glow-hi": "gold-hi",
+ "glow-lo": "gold"
+ },
+ "font": {
+ "mono": "\"BerkeleyMono Nerd Font\",\"Berkeley Mono\",monospace"
+ },
+ "timing": {
+ "pulse-rate": "1s"
+ }
+}