Files
Flaro/Tools/tokens-pruefen.py
T
ScarriffleandClaude Opus 5 6bd9cdb12d Eine Formensprache statt vier Radien und sechs Schriftgrössen
Die Oberfläche wirkte zusammengesetzt, nicht entworfen. Vier Gründe, alle
am Quelltext belegbar.

Die Farben waren von der App abgedriftet: elevated, placeholder, text,
text-dim, text-muted und die Trennlinie wichen in der letzten Stelle ab,
und der Standardakzent war in der App Lilac, im Browser Orchid. Einzeln
unsichtbar, zusammen genau der Grund, warum App und Browser nicht wie
dasselbe Programm aussahen. Alles auf FlaroDesign zurückgeführt.

Vier konkurrierende Radien — 6 auf Plakaten, 8 auf Karten, 999 auf jedem
Knopf, zweimal nacktes 3 — sind jetzt drei, und die ersten beiden stimmen
wieder mit FlaroDesign überein. Pillen bleiben, wo sie hingehören: Suche
und Filter.

Sechs Überschriftengrössen ohne Verhältnis zueinander sind eine Skala.
Der Kacheltitel war mit 13 px/500 leiser als die Metazeile daneben; das
erzeugt keine Hierarchie, sondern Rauschen.

Und der Akzent sass auf neun Dingen gleichzeitig. Wenn alles Akzent ist,
markiert er nichts. Der Primärknopf ist jetzt hell mit dunkler Schrift —
genau das macht Netflix, dessen Rot nur im Schriftzug vorkommt.

Dazu drei Dinge, die vorher ganz fehlten: ein sichtbarer Fokusring auf
allem Bedienbaren, ohne den die Oberfläche mit der Tastatur praktisch
unbedienbar war; Rücksicht auf "Bewegung reduzieren", das im Projekt
nirgends vorkam; und ein Hover, der mehr tut als zwei Bildpunkte zu
heben — mitsamt einem Abspielknopf, der nicht länger das halbe Plakat
verdeckt.

Tools/tokens-pruefen.py vergleicht beide Quellen und läuft im
Veröffentlichungsskript mit. Kein Generator: erzeugter Swift liest sich
schlechter als geschriebener, und die Prüfung fängt dasselbe ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:31:00 +02:00

143 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Prüft, ob App und Weboberfläche dieselben Farben und Formen benutzen.
**Warum es das braucht.** Die Werte stehen an mehreren Stellen von Hand
abgeschrieben — in Swift für die Apps, in CSS für den Browser. Sie sind
auseinandergelaufen, und zwar unsichtbar: `#1D1D21` gegen `#1D1D22`,
`#A0A0A8` gegen `#B4B4BE`, Radius 8 gegen 6. Einzeln merkt das niemand.
Zusammen sorgen sie dafür, dass App und Browser nicht wie dasselbe Programm
aussehen.
**Warum kein Generator.** Einer müsste Swift erzeugen, und erzeugter Swift
lässt sich schlechter lesen und kommentieren als geschriebener. Diese Prüfung
kostet weniger und fängt dasselbe ab: sie sagt, dass etwas auseinanderläuft,
und überlässt die Entscheidung dem Menschen.
Aufruf: python3 Tools/tokens-pruefen.py
Rückgabe: 0, wenn alles übereinstimmt; sonst 1.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
WURZEL = Path(__file__).resolve().parent.parent
SWIFT_FARBEN = WURZEL / "Packages/FlaroKit/Sources/FlaroDesign/Color/FlaroColor.swift"
SWIFT_AKZENT = WURZEL / "Packages/FlaroKit/Sources/FlaroDesign/Color/AccentOption.swift"
SWIFT_FORMEN = WURZEL / "Packages/FlaroKit/Sources/FlaroDesign/FlaroFont.swift"
CSS = WURZEL / "web/app/src/style.css"
# Was womit zusammengehört. Links der Swift-Name, rechts das CSS-Token.
FARBEN = {
"canvas": "--canvas",
"surface": "--surface",
"elevated": "--elevated",
"placeholder": "--placeholder",
"textPrimary": "--text",
"textSecondary": "--text-dim",
"textMuted": "--text-muted",
"destructive": "--destructive",
}
# Radien: Swift-Name → CSS-Token. `sheet` und `panel` haben im Browser kein
# Gegenstück — dort gibt es keine Blätter und keine schwebende Seitenleiste.
RADIEN = {
"card": "--r-karte",
"control": "--r-flaeche",
}
def hex_aus_swift(text: str, name: str) -> str | None:
"""Liest `static let name = RGB(hex: 0xAABBCC)` oder `RGB(0xAABBCC)`."""
m = re.search(rf"\b{name}\b[^\n=]*=\s*[^\n]*?0x([0-9A-Fa-f]{{6}})", text)
return "#" + m.group(1).upper() if m else None
def ohne_tvos(text: str) -> str:
"""Entfernt die tvOS-Zweige.
**Sonst vergleicht man den falschen Wert.** `FlaroRadius.card` ist 8 —
und auf tvOS 12, weil aus drei Metern eine stärkere Rundung nötig ist.
Der Browser läuft nie auf einem Fernseher; massgeblich ist der andere Zweig.
"""
return re.sub(r"#if os\(tvOS\).*?(?:#else(.*?))?#endif", lambda m: m.group(1) or "",
text, flags=re.S)
def zahl_aus_swift(text: str, name: str) -> float | None:
"""Liest den ersten Zahlenwert hinter `static let name`."""
m = re.search(rf"\b{name}\b\s*:\s*CGFloat\s*(?:\{{[^}}]*?return\s*)?([0-9]+(?:\.[0-9]+)?)", text, re.S)
if m:
return float(m.group(1))
m = re.search(rf"\b{name}\b[^\n=]*=\s*([0-9]+(?:\.[0-9]+)?)", text)
return float(m.group(1)) if m else None
def token_aus_css(text: str, name: str) -> str | None:
m = re.search(rf"{re.escape(name)}\s*:\s*([^;]+);", text)
return m.group(1).strip() if m else None
def main() -> int:
fehlend = [p for p in (SWIFT_FARBEN, SWIFT_AKZENT, SWIFT_FORMEN, CSS) if not p.exists()]
if fehlend:
for p in fehlend:
print(f"Nicht gefunden: {p.relative_to(WURZEL)}")
return 1
farben_swift = SWIFT_FARBEN.read_text(encoding="utf-8")
akzent_swift = SWIFT_AKZENT.read_text(encoding="utf-8")
formen_swift = ohne_tvos(SWIFT_FORMEN.read_text(encoding="utf-8"))
css = CSS.read_text(encoding="utf-8")
abweichungen: list[str] = []
geprueft = 0
for swift_name, css_name in FARBEN.items():
a = hex_aus_swift(farben_swift, swift_name)
b = token_aus_css(css, css_name)
if a is None or b is None:
abweichungen.append(f" {swift_name} / {css_name}: nicht gefunden ({a} / {b})")
continue
geprueft += 1
if a.upper() != b.upper():
abweichungen.append(f" {swift_name}: Swift {a}, CSS {css_name} {b}")
# Die Vorgabefarbe der App gegen den Akzent im Browser.
vorgabe = re.search(r'identifier\s*==\s*"([a-z]+)"', akzent_swift)
if not vorgabe:
vorgabe = re.search(r'\.first\s*\{\s*\$0\.id\s*==\s*"([a-z]+)"', akzent_swift)
if vorgabe:
hex_vorgabe = hex_aus_swift(akzent_swift, vorgabe.group(1))
akzent_css = token_aus_css(css, "--accent")
if hex_vorgabe and akzent_css:
geprueft += 1
if hex_vorgabe.upper() != akzent_css.upper():
abweichungen.append(
f" Vorgabe-Akzent: Swift {vorgabe.group(1)} {hex_vorgabe}, CSS {akzent_css}")
for swift_name, css_name in RADIEN.items():
a = zahl_aus_swift(formen_swift, swift_name)
b = token_aus_css(css, css_name)
if a is None or b is None:
abweichungen.append(f" Radius {swift_name} / {css_name}: nicht gefunden ({a} / {b})")
continue
geprueft += 1
b_zahl = float(re.sub(r"[^0-9.]", "", b) or 0)
if abs(a - b_zahl) > 0.01:
abweichungen.append(f" Radius {swift_name}: Swift {a:g}, CSS {css_name} {b}")
if abweichungen:
print(f"App und Weboberfläche laufen auseinander ({len(abweichungen)} Stelle(n)):")
print("\n".join(abweichungen))
print("\nFlaroDesign ist massgeblich — die CSS zieht nach.")
return 1
print(f"App und Weboberfläche stimmen überein ({geprueft} Werte geprüft).")
return 0
if __name__ == "__main__":
sys.exit(main())