site: rebuilt on Niveus, and the palette stops being a copy
Niveus is the interface language the project uses from here on -- this site
first, applications after it. Rebuilding the site on it is what proves the
system is one, rather than a folder of CSS with a README.
**`style.css` is gone.** 21 KB that transcribed include/theme.h by hand:
palette, type scale, shadows, spacing, every component. The intent was right --
"a site describing an operating system should look like the operating system"
-- and the mechanism was a copy, which means two files that have to be kept in
step by remembering to. What replaced it is `app.css`, which is layout and
nothing else, and one declaration in each page:
:root { --nv-brand: #7C5CFF; }
That is SCH_ACCENT. Niveus derives the accent and its states, the link colour,
the focus ring, the tint in the greys and how far the status colours lean, for
light and dark both. One number to keep true instead of forty.
**The site is now adaptive.** Dark by default, because the OS is dark, with a
Light/Dark/Auto group in the bar that is real. The choice is kept in
localStorage and re-applied by an inline script before first paint, so the page
never renders in one appearance and corrects itself while you watch.
**PSOGM keeps its one purple, and now says so structurally.** #4B1D9A is
spelled the way Niveus spells a subtree with its own colour -- `data-nv-brand`
with its own `--nv-brand` on that section -- so every role inside re-derives
from it rather than being painted on. Still exactly one place, which is what
branding/README.md asks for.
Content is unchanged; what changed is what it is made of. Cards are `.nv-card`,
tags are `.nv-badge`, the support table is `.nv-table`, the crash section is a
`.nv-bed`, and the docs reader's two rails are `.nv-nav--vertical` with the
current document marked by `aria-current` rather than a class of our own.
`docs.js` emits `.nv-table` and `.nv-code`, so a change to the system reaches
the documentation too.
Three things were found by looking at it in a browser rather than by reading
it, and all three are the same mistake: **a `.nv-card` carries no padding of
its own -- its parts do.** The stat tiles, the "One ISO, one stick" band and
the card headers were all built from bare children, so numbers ran to the card
edge, the USB mark hung off it, and a title and its badge sat at opposite ends
of a row meant for a title and a trailing slot. Where the component had a right
way to say it, the markup now says it that way; where it did not -- a tile has
no parts -- `app.css` supplies the padding and says why.
Checked: every nv- class used across the three pages and docs.js exists in the
CSS, every token referenced by app.css is defined, and app.css contains no hex
value at all. Niveus's own tools/check.py passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9ab8a7a426
commit
6490e5c759
89 changed files with 20804 additions and 1363 deletions
255
docs/Website/niveus/tools/check.py
Normal file
255
docs/Website/niveus/tools/check.py
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
#!/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())
|
||||
Loading…
Reference in a new issue