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.
115 lines
4.2 KiB
Python
115 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""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
|
|
pointing at a vendored .woff2 would therefore 404, so the faces have to be
|
|
inlined as data: URIs. That is the whole reason this build step exists.
|
|
|
|
Usage: python3 stacks/homepage/theme/build.py
|
|
Then: scripts/deploy-stack.sh esh-docker-vm homepage --conf
|
|
"""
|
|
|
|
import base64
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
HERE = pathlib.Path(__file__).resolve().parent
|
|
SRC = HERE / "australis.css.in"
|
|
OUT = HERE.parent / "conf" / "custom.css"
|
|
|
|
# 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 = [
|
|
("Inter", "Inter-Variable.woff2"),
|
|
("Space Grotesk", "SpaceGrotesk-Variable.woff2"),
|
|
("JetBrains Mono", "JetBrainsMono-Variable.woff2"),
|
|
]
|
|
|
|
# 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 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-family: "{family}";\n'
|
|
f" src: url(data:font/woff2;base64,{b64}) format('woff2');\n"
|
|
f" font-weight: 100 900;\n" # variable axis
|
|
f" font-style: normal;\n"
|
|
f" font-display: swap;\n"
|
|
f"}}"
|
|
)
|
|
return "\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:
|
|
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())
|
|
|
|
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)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|