No-AI skill tracer: SKILL.md, theme presets, sample data, Seam 1 page check
- rpa-dashboard-no-ai/SKILL.md: trigger description and the workflow in spec order (scan, report/keep-or-restyle, colours, stack, sections, build, wire, verify). - templates/themes.md: Tebra Sky and Superbill Ember, light and dark blocks. - templates/palette.py: derives a custom palette from one brand hex and checks text contrast in light and dark. - dashboard.html: built-in sample data so a page with no bot works on first open; cleared batches in the donut; component tokens so Ember is a token-only swap; accessible names for KPI cards, nav, steps, log, search and the dark toggle. - bridge.py: placeholders inside string literals, so it passes the lint hook unfilled. - tests/page_check: Seam 1 harness (fake Qt bridge, light/dark/narrow). - tests/evals: Seam 2 runner and fixture a-empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
49f8f49b65
commit
7f2ddecffb
13 files changed
+865
-57
No files matched your search
@@ -0,0 +1,100 @@
|
||||
"""Derive a custom dashboard palette from one brand hex, then check text contrast.
|
||||
|
||||
Usage: python palette.py "#7a3cff"
|
||||
|
||||
Prints the accent tokens for the body{} (light) and body.dark{} blocks of
|
||||
dashboard.html, which keeps the Tebra Sky base for everything else, then a
|
||||
contrast table for light and dark. A brand colour that is too light or too dark
|
||||
is nudged until text on it is readable. Exits 1 if any check still fails.
|
||||
"""
|
||||
import sys
|
||||
|
||||
WHITE, BLACK = "#ffffff", "#000000"
|
||||
# Tebra Sky base colours the accents sit on. Keep in step with dashboard.html.
|
||||
BASE = {"light": {"bg": "#eaf6fb", "ink": "#1c2b33"}, "dark": {"bg": "#182a38", "ink": "#e6eef4"}}
|
||||
LOG_TEXT = "#f68c82" # dimmest log line colour; the log box background is --accent-dark
|
||||
|
||||
|
||||
def rgb(h):
|
||||
return [int(h[i:i + 2], 16) for i in (1, 3, 5)]
|
||||
|
||||
|
||||
def mix(a, b, t):
|
||||
"""Blend hex colour a toward b by t (0..1)."""
|
||||
return "#" + "".join(f"{round(x + (y - x) * t):02x}" for x, y in zip(rgb(a), rgb(b)))
|
||||
|
||||
|
||||
def contrast(a, b):
|
||||
"""WCAG 2 contrast ratio between two hex colours."""
|
||||
def lum(h):
|
||||
c = [v / 255 for v in rgb(h)]
|
||||
c = [x / 12.92 if x <= 0.03928 else ((x + 0.055) / 1.055) ** 2.4 for x in c]
|
||||
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
|
||||
hi, lo = sorted((lum(a), lum(b)), reverse=True)
|
||||
return (hi + 0.05) / (lo + 0.05)
|
||||
|
||||
|
||||
def checks(t):
|
||||
"""(mode, what, ratio, needed) for every text pair the accents affect."""
|
||||
rows = []
|
||||
for mode, m in t.items():
|
||||
bg, ink = BASE[mode]["bg"], BASE[mode]["ink"]
|
||||
rows += [(mode, "white button text on --accent", contrast(WHITE, m["--accent"]), 3),
|
||||
(mode, "white button text on --accent-2", contrast(WHITE, m["--accent-2"]), 3),
|
||||
(mode, "--accent text on the page", contrast(m["--accent"], bg), 3),
|
||||
(mode, "ink on --accent-soft buttons", contrast(ink, m["--accent-soft"]), 4.5),
|
||||
(mode, "log text on --accent-dark", contrast(LOG_TEXT, m["--accent-dark"]), 4.5)]
|
||||
return rows
|
||||
|
||||
|
||||
def nudge(colour, fix):
|
||||
"""Step colour toward whatever fix() returns (WHITE/BLACK) until it returns None."""
|
||||
for _ in range(60):
|
||||
toward = fix(colour)
|
||||
if toward is None:
|
||||
break
|
||||
colour = mix(colour, toward, 0.06)
|
||||
return colour
|
||||
|
||||
|
||||
def derive(brand):
|
||||
brand = brand.strip().lower()
|
||||
if len(brand) == 4: # #abc -> #aabbcc
|
||||
brand = "#" + "".join(ch * 2 for ch in brand[1:])
|
||||
out = {}
|
||||
for mode, lift in [("light", 0.15), ("dark", 0.10)]:
|
||||
bg, ink = BASE[mode]["bg"], BASE[mode]["ink"]
|
||||
dark_page = mode == "dark"
|
||||
|
||||
def fix_accent(c):
|
||||
if contrast(c, bg) < 3: # accent text must stand out from the page
|
||||
return WHITE if dark_page else BLACK
|
||||
if contrast(WHITE, mix(c, WHITE, lift)) < 3: # white text on the button
|
||||
return BLACK
|
||||
return None
|
||||
|
||||
a = nudge(brand, fix_accent)
|
||||
soft = mix(a, BLACK, 0.35) if dark_page else mix(a, WHITE, 0.82)
|
||||
out[mode] = {
|
||||
"--accent": a,
|
||||
"--accent-2": mix(a, WHITE, lift),
|
||||
"--accent-dark": nudge(mix(a, BLACK, 0.45 if dark_page else 0.75),
|
||||
lambda c: BLACK if contrast(LOG_TEXT, c) < 4.5 else None),
|
||||
"--accent-soft": nudge(soft, lambda c: None if contrast(ink, c) >= 4.5 else BLACK if dark_page else WHITE),
|
||||
"--accent-soft-2": mix(a, BLACK, 0.6) if dark_page else mix(a, WHITE, 0.62),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2 or not sys.argv[1].startswith("#"):
|
||||
sys.exit(__doc__)
|
||||
t = derive(sys.argv[1])
|
||||
for mode, block in [("light", "body{"), ("dark", "body.dark{")]:
|
||||
print(f"/* {mode}: replace these tokens in the {block} block */")
|
||||
print(block + " " + "; ".join(f"{k}:{v}" for k, v in t[mode].items()) + "; }")
|
||||
print()
|
||||
rows = checks(t)
|
||||
for mode, what, ratio, need in rows:
|
||||
print(f"{mode:<6} {what:<34} {ratio:5.2f}:1 needs {need}:1 {'PASS' if ratio >= need else 'FAIL'}")
|
||||
sys.exit(0 if all(r >= n for *_, r, n in rows) else 1)
|
||||
Reference in new issue
Block a user