mirror of
https://github.com/Nexoniarz/Niveus.git
synced 2026-11-03 12:44:26 +00:00
Bevelled controls with light falling across them, fields as wells, solid windows, and glass kept for the chrome that floats over everything else. Warm graphite in dark, soft paper in light, one brand color driving both. Ships its own fonts (Instrument Sans, JetBrains Mono; OFL). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
255 lines
9 KiB
Python
Executable file
255 lines
9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Check that the Niveus sources hold to their own rules.
|
|
|
|
Four things go wrong in a design system long before anything looks broken, and
|
|
all four are mechanical:
|
|
|
|
1. A semantic token is defined for one appearance but not the other, so a
|
|
role silently resolves to nothing in light or in dark.
|
|
2. A component references a token that does not exist — a typo that degrades
|
|
to "no style" instead of to an error.
|
|
3. A visual value is hard-coded in a component — or in an example, which
|
|
claims to be built from the system and nothing else — instead of coming
|
|
from a token, which is how a system stops being a single source of truth.
|
|
4. dist/ drifts from the sources.
|
|
|
|
This script catches the first three; `build.py --check` covers the fourth.
|
|
|
|
python3 tools/check.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SRC = ROOT / "niveus"
|
|
|
|
DEFINITION = re.compile(r"(--[a-zA-Z0-9_-]+)\s*:")
|
|
REFERENCE = re.compile(r"var\(\s*(--[a-zA-Z0-9_-]+)")
|
|
COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
|
|
|
|
# A raw color or a raw shadow geometry in a component means the design language
|
|
# was restated locally instead of referenced.
|
|
HARD_CODED = [
|
|
(re.compile(r"#[0-9a-fA-F]{3,8}\b"), "hard-coded color"),
|
|
(re.compile(r"\b(?:rgb|rgba|hsl|hsla|oklch|lab)\(", re.IGNORECASE), "hard-coded color"),
|
|
]
|
|
|
|
# Properties the *application* supplies per instance, not the system. A menu
|
|
# and its trigger have to name the same anchor, and only the markup knows which
|
|
# pair belongs together.
|
|
CONSUMER_DEFINED = {"--nv-anchor"}
|
|
|
|
TOKEN_FILES = sorted((SRC / "tokens").glob("*.css")) + sorted((SRC / "themes").glob("*.css"))
|
|
CONSUMER_FILES = (
|
|
sorted((SRC / "components").glob("*.css"))
|
|
+ [SRC / "base.css", SRC / "utilities.css"]
|
|
)
|
|
|
|
|
|
def strip_comments(text: str) -> str:
|
|
return COMMENT.sub("", text)
|
|
|
|
|
|
def collect(paths: list[Path]) -> tuple[set[str], dict[str, list[tuple[Path, int, str]]]]:
|
|
"""Return every custom property defined across `paths`, and every reference."""
|
|
defined: set[str] = set()
|
|
references: dict[str, list[tuple[Path, int, str]]] = {}
|
|
|
|
for path in paths:
|
|
source = strip_comments(path.read_text(encoding="utf-8"))
|
|
for line_no, line in enumerate(source.split("\n"), start=1):
|
|
for name in DEFINITION.findall(line):
|
|
defined.add(name)
|
|
for name in REFERENCE.findall(line):
|
|
references.setdefault(name, []).append((path, line_no, line.strip()))
|
|
|
|
return defined, references
|
|
|
|
|
|
def rel(path: Path) -> str:
|
|
return str(path.relative_to(ROOT))
|
|
|
|
|
|
def check_theme_parity(failures: list[str]) -> None:
|
|
light = SRC / "themes" / "light.css"
|
|
dark = SRC / "themes" / "dark.css"
|
|
|
|
light_roles = {
|
|
name[len("--nv-light-") :]
|
|
for name in DEFINITION.findall(strip_comments(light.read_text(encoding="utf-8")))
|
|
if name.startswith("--nv-light-")
|
|
}
|
|
dark_roles = {
|
|
name[len("--nv-dark-") :]
|
|
for name in DEFINITION.findall(strip_comments(dark.read_text(encoding="utf-8")))
|
|
if name.startswith("--nv-dark-")
|
|
}
|
|
|
|
for role in sorted(light_roles - dark_roles):
|
|
failures.append(f"themes/dark.css: missing --nv-dark-{role} (light defines it)")
|
|
for role in sorted(dark_roles - light_roles):
|
|
failures.append(f"themes/light.css: missing --nv-light-{role} (dark defines it)")
|
|
|
|
if not failures:
|
|
print(f" theme parity: {len(light_roles)} roles defined in both appearances")
|
|
|
|
|
|
def check_references(failures: list[str]) -> None:
|
|
all_files = TOKEN_FILES + CONSUMER_FILES
|
|
defined, references = collect(all_files)
|
|
|
|
unresolved = []
|
|
for name, sites in sorted(references.items()):
|
|
if name.startswith("--_"):
|
|
continue # component-private, defined and used in the same rule
|
|
if name in CONSUMER_DEFINED:
|
|
continue
|
|
if name in defined:
|
|
continue
|
|
# var() with a fallback is an intentional escape hatch.
|
|
if all("," in line.split(f"var({name}")[1][:40] for _, _, line in sites):
|
|
continue
|
|
unresolved.append((name, sites[0]))
|
|
|
|
for name, (path, line_no, _) in unresolved:
|
|
failures.append(f"{rel(path)}:{line_no}: {name} is referenced but never defined")
|
|
|
|
if not unresolved:
|
|
print(f" references: {len(references)} tokens referenced, all resolve")
|
|
|
|
|
|
def check_hard_coded(failures: list[str]) -> None:
|
|
found = 0
|
|
for path in CONSUMER_FILES:
|
|
source = strip_comments(path.read_text(encoding="utf-8"))
|
|
for line_no, line in enumerate(source.split("\n"), start=1):
|
|
for pattern, label in HARD_CODED:
|
|
if pattern.search(line):
|
|
failures.append(f"{rel(path)}:{line_no}: {label} — {line.strip()}")
|
|
found += 1
|
|
|
|
if not found:
|
|
print(f" no hard-coded colors in {len(CONSUMER_FILES)} component and base files")
|
|
|
|
|
|
REFERENCE_DOC = ROOT / "REFERENCE.md"
|
|
|
|
EXAMPLES = ROOT / "examples"
|
|
|
|
# The one literal color an example is allowed: the input to the system.
|
|
BRAND_INPUT = re.compile(r"--nv-brand\s*:")
|
|
|
|
STYLE_BLOCK = re.compile(r"<style[^>]*>(.*?)</style>", re.DOTALL | re.IGNORECASE)
|
|
INLINE_STYLE = re.compile(r"""\sstyle=["']([^"']*)["']""")
|
|
|
|
|
|
def check_examples(failures: list[str]) -> None:
|
|
"""The examples claim to be built from the system and nothing else.
|
|
|
|
That claim is only worth making if it is enforced: an example that quietly
|
|
reaches for a hex value is no longer demonstrating the system, it is
|
|
demonstrating how to work around one. The single exception is --nv-brand,
|
|
which is the input the whole color system derives from — a literal there is
|
|
the point.
|
|
"""
|
|
if not EXAMPLES.is_dir():
|
|
return
|
|
|
|
scanned = 0
|
|
for path in sorted(EXAMPLES.rglob("*")):
|
|
if path.suffix not in {".css", ".html"}:
|
|
continue
|
|
scanned += 1
|
|
raw = path.read_text(encoding="utf-8")
|
|
|
|
if path.suffix == ".css":
|
|
regions = [(1, strip_comments(raw))]
|
|
else:
|
|
# Only the parts of an HTML file that can carry style.
|
|
regions = []
|
|
for match in STYLE_BLOCK.finditer(raw):
|
|
line = raw[: match.start()].count("\n") + 1
|
|
regions.append((line, strip_comments(match.group(1))))
|
|
for match in INLINE_STYLE.finditer(raw):
|
|
line = raw[: match.start()].count("\n") + 1
|
|
regions.append((line, match.group(1)))
|
|
|
|
for base_line, region in regions:
|
|
for offset, line in enumerate(region.split("\n")):
|
|
if BRAND_INPUT.search(line):
|
|
continue
|
|
for pattern, label in HARD_CODED:
|
|
if pattern.search(line):
|
|
failures.append(
|
|
f"{rel(path)}:{base_line + offset}: {label} in an example "
|
|
f"— {line.strip()[:70]}"
|
|
)
|
|
|
|
if path.name == "index.html" and "dist/niveus.css" not in raw:
|
|
failures.append(f"{rel(path)}: does not link the system stylesheet")
|
|
|
|
if not failures:
|
|
print(f" examples: {scanned} files, no color stated outside --nv-brand")
|
|
|
|
|
|
def check_reference(failures: list[str]) -> None:
|
|
"""Every class the CSS defines must appear in REFERENCE.md, spelled in full.
|
|
|
|
The reference claims to be the entire API — which is the only reason it is
|
|
safe to hand to someone, or to a model, instead of the source. A class that
|
|
exists but is undocumented breaks that claim silently, and a documented
|
|
class that no longer exists is worse: it reads as real.
|
|
"""
|
|
if not REFERENCE_DOC.is_file():
|
|
return
|
|
|
|
documented = REFERENCE_DOC.read_text(encoding="utf-8")
|
|
|
|
defined: set[str] = set()
|
|
sources = (
|
|
sorted((SRC / "components").glob("*.css"))
|
|
+ [SRC / "utilities.css", SRC / "base.css", SRC / "continuity.css"]
|
|
)
|
|
for path in sources:
|
|
source = strip_comments(path.read_text(encoding="utf-8"))
|
|
defined.update(re.findall(r"\.(nv-[a-z0-9_-]+)", source))
|
|
|
|
missing = sorted(name for name in defined if f".{name}" not in documented)
|
|
for name in missing:
|
|
failures.append(f"REFERENCE.md: .{name} is defined in the CSS but not documented")
|
|
|
|
# And the reverse: nothing documented that no longer exists.
|
|
mentioned = set(re.findall(r"`\.(nv-[a-z0-9_-]+)`", documented))
|
|
for name in sorted(mentioned - defined):
|
|
failures.append(f"REFERENCE.md: .{name} is documented but not defined anywhere")
|
|
|
|
if not missing:
|
|
print(f" reference: {len(defined)} classes, all documented")
|
|
|
|
|
|
def main() -> int:
|
|
failures: list[str] = []
|
|
|
|
print("Niveus checks")
|
|
check_theme_parity(failures)
|
|
check_references(failures)
|
|
check_hard_coded(failures)
|
|
check_examples(failures)
|
|
check_reference(failures)
|
|
|
|
if failures:
|
|
print(f"\n{len(failures)} problem(s):")
|
|
for failure in failures:
|
|
print(f" {failure}")
|
|
return 1
|
|
|
|
print("\nAll checks passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|