Build the Wolfskin TV shell and navigation
This commit is contained in:
parent
de452448ba
commit
f60a22e435
97 changed files with 4363 additions and 91 deletions
121
wolfskin-design/generate_tokens.py
Executable file
121
wolfskin-design/generate_tokens.py
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate Compose and CSS tokens and a build-verifiable drift manifest."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
KOTLIN = Path("src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt")
|
||||
|
||||
|
||||
def number(value: float) -> str:
|
||||
"""Render stable Kotlin decimal literals."""
|
||||
return str(float(value))
|
||||
|
||||
|
||||
def property_name(name: str) -> str:
|
||||
"""Keep source names valid as Kotlin identifiers."""
|
||||
return name.replace("-", "_")
|
||||
|
||||
|
||||
def render_tokens(tokens: dict) -> tuple[str, str]:
|
||||
"""Translate typed source values without choosing new design values."""
|
||||
lines = [
|
||||
"// Generated by generate_tokens.py; edit the JSON sources instead.",
|
||||
"package org.justinvisser.wolfskin.design",
|
||||
"",
|
||||
"import androidx.compose.ui.graphics.Color as ComposeColor",
|
||||
"import androidx.compose.ui.text.TextStyle",
|
||||
"import androidx.compose.ui.text.font.FontWeight",
|
||||
"import androidx.compose.ui.unit.dp",
|
||||
"import androidx.compose.ui.unit.em",
|
||||
"import androidx.compose.ui.unit.sp",
|
||||
"",
|
||||
"/** The locked TV design, converted at the source density. */",
|
||||
"object WolfskinTokens {",
|
||||
]
|
||||
css = ["/* Generated from the locked TV token sources. */", ":root {"]
|
||||
for group_name, group in tokens.items():
|
||||
if group_name.startswith("$") or group_name in ("meta", "font"):
|
||||
continue
|
||||
lines.append(f" object {group_name[0].upper() + group_name[1:]} {{")
|
||||
for name, token in group.items():
|
||||
if not isinstance(token, dict):
|
||||
continue
|
||||
name = property_name(name)
|
||||
kind = token.get("$type")
|
||||
value = token.get("$value")
|
||||
if kind == "color":
|
||||
rgba = value.lstrip("#")
|
||||
argb = (rgba[6:8] if len(rgba) == 8 else "ff") + rgba[:6]
|
||||
lines.append(f" val {name} = ComposeColor(0x{argb.upper()})")
|
||||
css.append(f" --{group_name}-{name}: {value};")
|
||||
elif kind == "typography" and "sp" in token:
|
||||
size = token["sp"]
|
||||
weight = token.get("weight", token["$value"]["fontWeight"])
|
||||
height = size * token.get("lineHeight", 1.3)
|
||||
tracking = token.get("letterSpacingEm", 0)
|
||||
lines.append(f" val {name} = TextStyle(fontSize = {number(size)}.sp, fontWeight = FontWeight({weight}), lineHeight = {number(height)}.sp, letterSpacing = {number(tracking)}.em)")
|
||||
css.append(f" --{group_name}-{name}: {number(token['px'])}px;")
|
||||
elif kind == "typography" and "sizes" in token:
|
||||
for context, pixels in token["sizes"].items():
|
||||
lines.append(f" val {name}{context[0].upper() + context[1:]} = TextStyle(fontSize = {number(pixels / 2)}.sp, fontWeight = FontWeight({token['weight']}))")
|
||||
elif kind == "duration":
|
||||
lines.append(f" const val {name} = {int(value.removesuffix('ms'))}")
|
||||
css.append(f" --{group_name}-{name}: {value};")
|
||||
elif kind == "transition" and value.get("type") == "spring":
|
||||
lines.append(f" const val {name}DampingRatio = {number(value['dampingRatio'])}f")
|
||||
lines.append(f" const val {name}Stiffness = {number(value['stiffness'])}f")
|
||||
elif kind == "shadow":
|
||||
for suffix in ("gapPx", "ringPx"):
|
||||
lines.append(f" val {name}{suffix[0].upper() + suffix[1:-2]} = {number(token[suffix] / 2)}.dp")
|
||||
elif isinstance(token.get("dp"), (float, int)):
|
||||
lines.append(f" val {name} = {number(token['dp'])}.dp")
|
||||
css.append(f" --{group_name}-{name}: {number(token['px'])}px;")
|
||||
elif isinstance(token.get("dp"), list):
|
||||
for suffix, dimension in zip(("Width", "Height"), token["dp"]):
|
||||
lines.append(f" val {name}{suffix} = {number(dimension)}.dp")
|
||||
elif isinstance(token.get("dp"), dict):
|
||||
for suffix, dimension in token["dp"].items():
|
||||
lines.append(f" val {name}{suffix[0].upper() + suffix[1:]} = {number(dimension)}.dp")
|
||||
elif isinstance(value, (float, int)) and not isinstance(value, bool):
|
||||
lines.append(f" const val {name} = {number(value)}f")
|
||||
elif kind in ("gradient", "filter", "rule") and isinstance(value, str):
|
||||
lines.append(f" const val {name} = {json.dumps(value)}")
|
||||
if kind == "gradient":
|
||||
css.append(f" --{group_name}-{name}: {value};")
|
||||
lines.append(" }")
|
||||
lines.append("}")
|
||||
css.append("}")
|
||||
return "\n".join(lines) + "\n", "\n".join(css) + "\n"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Write generated artifacts, or fail when committed artifacts drift."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true")
|
||||
arguments = parser.parse_args()
|
||||
tokens = json.loads((ROOT / "tokens.json").read_text())
|
||||
tokens.update(json.loads((ROOT / "shell-tokens.json").read_text()))
|
||||
kotlin, css = render_tokens(tokens)
|
||||
outputs = {str(KOTLIN): kotlin, "tokens.css": css}
|
||||
inputs = ["tokens.json", "shell-tokens.json", "generate_tokens.py"]
|
||||
checksums = {name: hashlib.sha256((ROOT / name).read_bytes()).hexdigest() for name in inputs}
|
||||
checksums.update({name: hashlib.sha256(content.encode()).hexdigest() for name, content in outputs.items()})
|
||||
outputs["tokens.sha256"] = "".join(f"{checksum} {name}\n" for name, checksum in sorted(checksums.items()))
|
||||
for name, content in outputs.items():
|
||||
path = ROOT / name
|
||||
if arguments.check:
|
||||
if not path.exists() or path.read_text() != content:
|
||||
raise SystemExit(f"Token drift: run python3 wolfskin-design/generate_tokens.py ({name})")
|
||||
else:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
print("Token drift check passed" if arguments.check else "Generated Kotlin, CSS and drift manifest")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue