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>
143 lines
5.4 KiB
Python
Executable File
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())
|