Skip to content

Commit 137a045

Browse files
authored
Merge pull request #37 from audiophore/feat/color-tokens-export
feat(tokens): export palette as brand.css + brand.json
2 parents f1786d0 + 6440c2a commit 137a045

6 files changed

Lines changed: 129 additions & 8 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ jobs:
3636

3737
- name: Verify checked-in assets match build output
3838
run: |
39-
git diff --exit-code -- logos/ || {
40-
echo "::error::Generated assets in logos/ differ from checked-in versions."
39+
git diff --exit-code -- logos/ tokens/ || {
40+
echo "::error::Generated assets in logos/ or tokens/ differ from checked-in versions."
4141
echo "::error::Run 'make' locally and commit the result."
4242
exit 1
4343
}

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# make svgs # only SVGs
66
# make pngs # only PNGs (requires SVGs to exist)
77
# make favicons # only favicons
8+
# make tokens # only color tokens (brand.css + brand.json)
89
# make docs # only BRANDING.md
910
# make clean # remove all generated assets
1011
# make check # CI check: exit 1 if BRANDING.md is out of sync
@@ -16,7 +17,7 @@ PYTHON := python3
1617
SCRIPTS := scripts
1718
ASSETS := logos
1819

19-
.PHONY: all build svgs pngs favicons docs clean check install help
20+
.PHONY: all build svgs pngs favicons tokens docs clean check install help
2021

2122
all: build docs
2223

@@ -27,6 +28,7 @@ help:
2728
@echo " svgs - generate only SVGs"
2829
@echo " pngs - generate only PNGs (requires SVGs)"
2930
@echo " favicons - generate only favicons"
31+
@echo " tokens - generate color tokens (brand.css + brand.json)"
3032
@echo " docs - regenerate BRANDING.md from brand.toml"
3133
@echo " clean - remove all generated assets"
3234
@echo " check - verify BRANDING.md is in sync with brand.toml"
@@ -44,6 +46,9 @@ pngs:
4446
favicons:
4547
$(PYTHON) $(SCRIPTS)/build.py favicon
4648

49+
tokens:
50+
$(PYTHON) $(SCRIPTS)/build.py tokens
51+
4752
docs:
4853
$(PYTHON) $(SCRIPTS)/render_branding_md.py
4954

brand.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ wash_dark = "#0a0e1a" # neutral dark surface
4141
# Edit BRANDING.md descriptions only by editing this section.
4242
primary_accent = "mid_highs" # the single "brand color" when only one is allowed
4343

44+
# ──────────────────────────────────────────────────────────────────────
45+
# Token export
46+
#
47+
# Downstream consumers (the website, hardware render/CAD scripts) should
48+
# reference the palette, not hand-copy hexes. `make tokens` emits
49+
# tokens/brand.css (:root custom properties) + tokens/brand.json straight
50+
# from [colors.*] above — change a hex there and both regenerate.
51+
# ──────────────────────────────────────────────────────────────────────
52+
53+
[tokens]
54+
css_prefix = "ap" # CSS custom-property namespace → --ap-highs, --ap-ink, …
55+
4456
# ──────────────────────────────────────────────────────────────────────
4557
# Symbol layout
4658
#

scripts/build.py

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
- logos/png/*.png (all rasters at all configured sizes)
88
- logos/favicon/*.png (favicon sizes)
99
- logos/favicon/favicon.ico (multi-resolution legacy favicon)
10+
- tokens/brand.css (palette as :root CSS custom properties)
11+
- tokens/brand.json (palette as structured JSON)
1012
1113
Usage:
1214
python scripts/build.py # Build everything
@@ -20,6 +22,7 @@
2022
from __future__ import annotations
2123

2224
import argparse
25+
import json
2326
import os
2427
import shutil
2528
import subprocess
@@ -41,6 +44,9 @@
4144
SVG_DIR = ASSETS_DIR / "svg"
4245
PNG_DIR = ASSETS_DIR / "png"
4346
FAVICON_DIR = ASSETS_DIR / "favicon"
47+
# Non-logo machine-readable exports (palette tokens) live one level up from
48+
# logos/ so consumers can grab just the tokens without the whole asset tree.
49+
TOKENS_DIR = ROOT / "tokens"
4450

4551

4652
# ─── Config helpers ─────────────────────────────────────────────────
@@ -374,28 +380,81 @@ def build_favicons(cfg: dict) -> list[Path]:
374380
return written
375381

376382

383+
# ─── Token export (CSS + JSON) ──────────────────────────────────────
384+
385+
def _css_var(prefix: str, key: str) -> str:
386+
"""Map a palette key to a CSS custom-property name: mid_highs → --ap-mid-highs."""
387+
return f"--{prefix}-{key.replace('_', '-')}"
388+
389+
390+
def write_tokens(cfg: dict) -> list[Path]:
391+
"""Export the palette as tokens/brand.css (:root vars) + tokens/brand.json.
392+
393+
Sourced straight from [colors.*] so consumers reference the brand
394+
instead of hand-copying hexes. tomllib preserves insertion order, so
395+
output is deterministic.
396+
"""
397+
prefix = cfg["tokens"]["css_prefix"]
398+
spectrum = cfg["colors"]["spectrum"]
399+
neutrals = cfg["colors"]["neutrals"]
400+
semantics = cfg["colors"]["semantics"]
401+
402+
# CSS
403+
lines = [
404+
"/* Audiophore brand color tokens — generated from brand.toml by scripts/build.py.",
405+
" * Do not edit by hand; run `make tokens`. */",
406+
":root {",
407+
" /* Spectrum — each maps to an FFT band (high → low). */",
408+
]
409+
lines += [f" {_css_var(prefix, k)}: {v};" for k, v in spectrum.items()]
410+
lines.append(" /* Neutrals. */")
411+
lines += [f" {_css_var(prefix, k)}: {v};" for k, v in neutrals.items()]
412+
lines.append(" /* Semantic aliases. */")
413+
lines += [
414+
f" {_css_var(prefix, k)}: var({_css_var(prefix, target)});"
415+
for k, target in semantics.items()
416+
]
417+
lines.append("}")
418+
css = "\n".join(lines) + "\n"
419+
420+
# JSON
421+
data = {
422+
"meta": {"name": cfg["meta"]["name"], "slug": cfg["meta"]["slug"]},
423+
"spectrum": dict(spectrum),
424+
"neutrals": dict(neutrals),
425+
"semantics": {k: neutrals.get(t) or spectrum.get(t) for k, t in semantics.items()},
426+
}
427+
js = json.dumps(data, indent=2) + "\n"
428+
429+
css_path = TOKENS_DIR / "brand.css"
430+
json_path = TOKENS_DIR / "brand.json"
431+
css_path.write_text(css)
432+
json_path.write_text(js)
433+
return [css_path, json_path]
434+
435+
377436
# ─── Cleanup and CLI ────────────────────────────────────────────────
378437

379438
def clean():
380439
"""Remove all generated assets."""
381-
for d in (SVG_DIR, PNG_DIR, FAVICON_DIR):
440+
for d in (SVG_DIR, PNG_DIR, FAVICON_DIR, TOKENS_DIR):
382441
if d.exists():
383442
shutil.rmtree(d)
384443
print(f" removed {d.relative_to(ROOT)}")
385-
for d in (SVG_DIR, PNG_DIR, FAVICON_DIR):
444+
for d in (SVG_DIR, PNG_DIR, FAVICON_DIR, TOKENS_DIR):
386445
d.mkdir(parents=True, exist_ok=True)
387446

388447

389448
def ensure_dirs():
390-
for d in (SVG_DIR, PNG_DIR, FAVICON_DIR):
449+
for d in (SVG_DIR, PNG_DIR, FAVICON_DIR, TOKENS_DIR):
391450
d.mkdir(parents=True, exist_ok=True)
392451

393452

394453
def main():
395454
parser = argparse.ArgumentParser(description=__doc__.strip().split("\n")[0])
396455
parser.add_argument(
397456
"targets", nargs="*",
398-
choices=["all", "symbol", "wordmark", "lockup", "png", "favicon", []],
457+
choices=["all", "symbol", "wordmark", "lockup", "png", "favicon", "tokens", []],
399458
default=[],
400459
help="Which assets to build (default: all)",
401460
)
@@ -411,7 +470,7 @@ def main():
411470

412471
targets = set(args.targets) if args.targets else {"all"}
413472
if "all" in targets:
414-
targets = {"symbol", "wordmark", "lockup", "png", "favicon"}
473+
targets = {"symbol", "wordmark", "lockup", "png", "favicon", "tokens"}
415474

416475
if "symbol" in targets:
417476
print("Building symbol SVGs…")
@@ -433,6 +492,10 @@ def main():
433492
print("Building favicons…")
434493
for p in build_favicons(cfg):
435494
print(f" {p.relative_to(ROOT)}")
495+
if "tokens" in targets:
496+
print("Exporting color tokens…")
497+
for p in write_tokens(cfg):
498+
print(f" {p.relative_to(ROOT)}")
436499

437500
print("\nDone.")
438501

tokens/brand.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* Audiophore brand color tokens — generated from brand.toml by scripts/build.py.
2+
* Do not edit by hand; run `make tokens`. */
3+
:root {
4+
/* Spectrum — each maps to an FFT band (high → low). */
5+
--ap-highs: #00E5FF;
6+
--ap-mid-highs: #00D4C8;
7+
--ap-mids: #3DD68C;
8+
--ap-mid-lows: #FFB020;
9+
--ap-bass: #FF2D9C;
10+
/* Neutrals. */
11+
--ap-ink: #1a1f2e;
12+
--ap-paper: #FFFFFF;
13+
--ap-mute: #8b95a8;
14+
--ap-wash-light: #f8f9fa;
15+
--ap-wash-dark: #0a0e1a;
16+
/* Semantic aliases. */
17+
--ap-primary-accent: var(--ap-mid-highs);
18+
}

tokens/brand.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"meta": {
3+
"name": "Audiophore",
4+
"slug": "audiophore"
5+
},
6+
"spectrum": {
7+
"highs": "#00E5FF",
8+
"mid_highs": "#00D4C8",
9+
"mids": "#3DD68C",
10+
"mid_lows": "#FFB020",
11+
"bass": "#FF2D9C"
12+
},
13+
"neutrals": {
14+
"ink": "#1a1f2e",
15+
"paper": "#FFFFFF",
16+
"mute": "#8b95a8",
17+
"wash_light": "#f8f9fa",
18+
"wash_dark": "#0a0e1a"
19+
},
20+
"semantics": {
21+
"primary_accent": "#00D4C8"
22+
}
23+
}

0 commit comments

Comments
 (0)