119 lines
3.7 KiB
Python
119 lines
3.7 KiB
Python
#!/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())
|