feat: enforce shared UI foundations and pin reference journey
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Enforce Core ownership of WebUI visual foundations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
REPOS_ROOT = META_ROOT.parent
|
||||
TOKENS_PATH = REPOS_ROOT / "govoplan-core/webui/src/styles/tokens.css"
|
||||
|
||||
RAW_HEX_COLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b")
|
||||
RAW_COLOR_FUNCTION = re.compile(r"\b(?:rgb|rgba|hsl|hsla)\((?!\s*var\()", re.IGNORECASE)
|
||||
RADIUS_DECLARATION = re.compile(r"border-radius\s*:\s*([^;}]+)")
|
||||
MEDIA_MAX_WIDTH = re.compile(r"@media[^\n{]*\(max-width\s*:\s*(\d+)px\)")
|
||||
|
||||
RESPONSIVE_BANDS = {560, 600, 680, 760, 900, 1100, 1280}
|
||||
REQUIRED_TOKENS = {
|
||||
"--radius-hairline",
|
||||
"--radius-tight",
|
||||
"--radius-xs",
|
||||
"--radius-sm",
|
||||
"--radius-compact",
|
||||
"--radius-md",
|
||||
"--radius-lg",
|
||||
"--radius-xl",
|
||||
"--radius-round",
|
||||
"--radius-pill",
|
||||
"--shadow-drawer-side",
|
||||
"--shadow-drawer-bottom",
|
||||
"--action-primary-bg",
|
||||
"--action-primary-border",
|
||||
"--action-primary-text",
|
||||
"--action-danger-bg",
|
||||
"--action-danger-text",
|
||||
"--badge-accent-text",
|
||||
"--data-category-blue",
|
||||
"--data-category-green",
|
||||
"--data-category-amber",
|
||||
"--data-category-purple",
|
||||
"--data-category-rose",
|
||||
*(f"--data-series-{index}" for index in range(1, 9)),
|
||||
}
|
||||
|
||||
|
||||
def line_number(source: str, offset: int) -> int:
|
||||
return source.count("\n", 0, offset) + 1
|
||||
|
||||
|
||||
def css_files() -> list[pathlib.Path]:
|
||||
files: list[pathlib.Path] = []
|
||||
for repository in sorted(REPOS_ROOT.glob("govoplan-*")):
|
||||
styles = repository / "webui/src"
|
||||
if styles.is_dir():
|
||||
files.extend(sorted(styles.rglob("*.css")))
|
||||
return files
|
||||
|
||||
|
||||
def display_path(path: pathlib.Path) -> str:
|
||||
return str(path.relative_to(REPOS_ROOT))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
tokens = TOKENS_PATH.read_text(encoding="utf-8")
|
||||
for token in sorted(REQUIRED_TOKENS):
|
||||
if f"{token}:" not in tokens:
|
||||
errors.append(f"{display_path(TOKENS_PATH)}: missing required foundation token {token}")
|
||||
|
||||
files = css_files()
|
||||
for path in files:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
owns_literals = path == TOKENS_PATH
|
||||
|
||||
if not owns_literals:
|
||||
for pattern, label in (
|
||||
(RAW_HEX_COLOR, "raw color"),
|
||||
(RAW_COLOR_FUNCTION, "raw color function"),
|
||||
):
|
||||
for match in pattern.finditer(source):
|
||||
errors.append(
|
||||
f"{display_path(path)}:{line_number(source, match.start())}: "
|
||||
f"{label} must use a Core theme token"
|
||||
)
|
||||
|
||||
for match in RADIUS_DECLARATION.finditer(source):
|
||||
value = match.group(1).strip()
|
||||
if "var(" not in value and value not in {"0", "inherit", "initial", "unset"}:
|
||||
errors.append(
|
||||
f"{display_path(path)}:{line_number(source, match.start())}: "
|
||||
f"border radius {value!r} must use a Core radius token"
|
||||
)
|
||||
|
||||
for match in MEDIA_MAX_WIDTH.finditer(source):
|
||||
width = int(match.group(1))
|
||||
if width not in RESPONSIVE_BANDS:
|
||||
errors.append(
|
||||
f"{display_path(path)}:{line_number(source, match.start())}: "
|
||||
f"{width}px is not a shared responsive band; use one of "
|
||||
f"{', '.join(f'{value}px' for value in sorted(RESPONSIVE_BANDS))}"
|
||||
)
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors))
|
||||
return 1
|
||||
|
||||
print(
|
||||
"Shared WebUI foundation contract passed for "
|
||||
f"{len(files)} stylesheets and {len(RESPONSIVE_BANDS)} responsive bands."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user