feat(homepage): rebuild the theme on canonical Australis tokens
The predecessor theme was ugly for two structural reasons, not one. It did not use the design system's colours. It built a parallel OKLCH palette "derived from the Australis philosophy" and swapped the canonical typeface for Supreme -- a fork, not a theme. Every hex here is now copied verbatim from ~/.claude/skills/australis-design/colors_and_type.css, and build.py re-checks all 19 against that file at build time and warns on drift so it cannot quietly fork again. Type is the canonical stack: Space Grotesk / Inter / JetBrains Mono, vendored as latin-subset VARIABLE woff2 (one file per family, 102 KB total against 56 KB for three static Supreme cuts, and no Google Fonts request at page load). It also carried a generated full-bleed aurora image behind the entire dashboard. Canon forbids exactly that -- "solid fills only on chrome, no full-bleed photography, no decorative gradients", and the aurora motif "never as a background fill behind text". The predecessor knew, said so in its own header, and dialled the opacity down rather than dropping it. The image is gone; the aurora survives as a 1px accent edge under the tab bar, which is where canon sanctions it. The asset stays in images/ in case it is ever revisited. Direction is instrument panel. Group headings become the Australis mono eyebrow with a hairline to the right edge -- canon calls the eyebrow a system signature, and it turns the groups into register bands instead of headings floating over a grid. Status stops shouting: the filled emerald chips read louder than the service names they annotated, so they are now a semantic dot plus a mono micro-label at tertiary contrast. Cards are bordered and opaque, per canon's border-over-shadow rule for chrome. Alignment, per operator feedback that pills and cards did not line up: - The status cluster is centred on the service name's line rather than parked in the card's top-right corner, where Homepage's `absolute top-0` left it floating ~7px above the title's optical centre. The offsets reconstruct the title line box and are documented as moving together. - Descriptions get a two-line minimum, so the common one-line/two-line mix bottom-aligns across a row. This is what made the grid look ragged. useEqualHeights stays false: it inflated short cards to match a widget card twice their height, which was the worse failure. - The status dot is flex-centred rather than nudged with vertical-align, so it stays centred if the type scale changes. Retires the Skyfall sources and the Supreme faces; theme/ now has one source of truth.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate conf/custom.css from the Skyfall source in this directory.
|
||||
"""Generate conf/custom.css from the Australis source in this directory.
|
||||
|
||||
Homepage serves exactly two files out of its config dir — custom.css and
|
||||
custom.js — with no static route for anything alongside them. A @font-face
|
||||
@@ -16,33 +16,40 @@ import re
|
||||
import sys
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
SRC = HERE / "australis.css.in"
|
||||
OUT = HERE.parent / "conf" / "custom.css"
|
||||
|
||||
# Only the body/UI face is embedded. See the header comment in skyfall.css.in
|
||||
# for why Bespoke Sans and Victor Mono are deliberately left out.
|
||||
# The canonical token file. Present on a box with the australis-design skill
|
||||
# installed; absent elsewhere, in which case the drift check is skipped rather
|
||||
# than failing the build.
|
||||
CANON = pathlib.Path.home() / ".claude/skills/australis-design/colors_and_type.css"
|
||||
|
||||
# One VARIABLE face per family — the whole 100-900 axis in a single file, so
|
||||
# there is no per-weight fan-out. Latin subset only.
|
||||
FONTS = [
|
||||
("Supreme", "Supreme-400.woff2", "400"),
|
||||
("Supreme", "Supreme-500.woff2", "500"),
|
||||
("Supreme", "Supreme-700.woff2", "600 700"), # Supreme ships no 600 cut
|
||||
("Inter", "Inter-Variable.woff2"),
|
||||
("Space Grotesk", "SpaceGrotesk-Variable.woff2"),
|
||||
("JetBrains Mono", "JetBrainsMono-Variable.woff2"),
|
||||
]
|
||||
|
||||
# Vendored verbatim from the handoff bundle; order matters (colors defines the
|
||||
# families that layout and typography reference).
|
||||
TOKEN_FILES = ["colors.css", "layout.css", "typography.css"]
|
||||
# Tokens the generated sheet cannot work without. A vendoring mistake that
|
||||
# drops the :root block produces a stylesheet that parses fine and renders
|
||||
# nothing, so assert on the values rather than trusting the copy.
|
||||
REQUIRED_TOKENS = ["--aus-black", "--aus-cyan", "--bg-1", "--font-mono"]
|
||||
|
||||
|
||||
def font_faces() -> str:
|
||||
out = []
|
||||
for family, filename, weight in FONTS:
|
||||
for family, filename in FONTS:
|
||||
path = HERE / "fonts" / filename
|
||||
if not path.exists():
|
||||
sys.exit(f"missing font: {path}")
|
||||
b64 = base64.b64encode(path.read_bytes()).decode("ascii")
|
||||
out.append(
|
||||
f'@font-face {{\n'
|
||||
f"@font-face {{\n"
|
||||
f' font-family: "{family}";\n'
|
||||
f" src: url(data:font/woff2;base64,{b64}) format('woff2');\n"
|
||||
f" font-weight: {weight};\n"
|
||||
f" font-weight: 100 900;\n" # variable axis
|
||||
f" font-style: normal;\n"
|
||||
f" font-display: swap;\n"
|
||||
f"}}"
|
||||
@@ -50,29 +57,55 @@ def font_faces() -> str:
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def tokens() -> str:
|
||||
out = []
|
||||
for name in TOKEN_FILES:
|
||||
path = HERE / name
|
||||
if not path.exists():
|
||||
sys.exit(f"missing token file: {path}")
|
||||
text = path.read_text()
|
||||
# The bundle's fonts.css is not vendored, so typography.css's family
|
||||
# stacks would reference faces that never load. Nothing to strip here
|
||||
# today, but keep the read verbatim so drift is a plain diff.
|
||||
out.append(f"/* ---- {name} (vendored verbatim) ---- */\n{text.strip()}")
|
||||
return "\n\n".join(out)
|
||||
def check_canon_drift(css: str) -> None:
|
||||
"""Warn if a token here no longer matches the Australis skill's value.
|
||||
|
||||
Non-fatal on purpose: the skill is a per-workstation install, so a missing
|
||||
file is normal and must not break a build on a box that lacks it. A *value*
|
||||
mismatch is worth shouting about — that is the palette forking, which is
|
||||
exactly how the theme this one replaced went wrong.
|
||||
"""
|
||||
if not CANON.exists():
|
||||
print(f"note: {CANON} not present — canonical drift check skipped")
|
||||
return
|
||||
|
||||
canon = dict(re.findall(r"(--aus-[a-z0-9-]+):\s*(#[0-9a-fA-F]{6});", CANON.read_text()))
|
||||
ours = dict(re.findall(r"(--aus-[a-z0-9-]+):\s*(#[0-9a-fA-F]{6});", css))
|
||||
if not canon:
|
||||
print(f"note: parsed no --aus-* tokens from {CANON} — drift check skipped")
|
||||
return
|
||||
|
||||
drift = [(k, v, canon[k]) for k, v in ours.items() if k in canon and v.lower() != canon[k].lower()]
|
||||
missing = sorted(set(ours) - set(canon))
|
||||
for key, mine, theirs in drift:
|
||||
print(f"DRIFT: {key} is {mine} here, {theirs} in the skill", file=sys.stderr)
|
||||
for key in missing:
|
||||
print(f"note: {key} is not a canonical token", file=sys.stderr)
|
||||
if drift:
|
||||
print(
|
||||
f"WARNING: {len(drift)} token(s) diverge from the canonical palette. "
|
||||
"Reconcile before deploying — a derived palette is how the previous "
|
||||
"theme forked from the design system.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
print(f"canonical palette check: {len(ours)} tokens match {CANON.name}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
src = (HERE / "skyfall.css.in").read_text()
|
||||
if "@@FONTS@@" not in src or "@@TOKENS@@" not in src:
|
||||
sys.exit("skyfall.css.in lost one of its placeholders")
|
||||
if not SRC.exists():
|
||||
sys.exit(f"missing source: {SRC}")
|
||||
src = SRC.read_text()
|
||||
if "/* @@FONTS@@ */" not in src:
|
||||
sys.exit(f"{SRC.name} lost its @@FONTS@@ placeholder")
|
||||
|
||||
css = src.replace("/* @@FONTS@@ */", font_faces())
|
||||
css = css.replace("/* @@TOKENS@@ */", tokens())
|
||||
# Guard the one rule that silently breaks the theme if it regresses.
|
||||
if not re.search(r"--surface-sunken", css):
|
||||
sys.exit("generated css has no surface tokens — token vendoring failed")
|
||||
|
||||
for token in REQUIRED_TOKENS:
|
||||
if token not in css:
|
||||
sys.exit(f"generated css has no {token} — the token block did not survive")
|
||||
|
||||
check_canon_drift(css)
|
||||
OUT.write_text(css)
|
||||
print(f"wrote {OUT} ({len(css) / 1024:.0f} KB)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user