Move all three skills into skills/; add root README; fix paths
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
312742cfef
commit
d95e5d452d
84 files changed
+97
-31
No files matched your search
@@ -0,0 +1,99 @@
|
||||
"""Theme the Run tab: a preset or one brand hex -> the THEME block of run_tab.html.
|
||||
|
||||
python palette.py sky|teal|"#7a3cff" print the block and a contrast table
|
||||
python palette.py "#7a3cff" --write run_tab/run_tab.html also write it into the page
|
||||
|
||||
Only the sky gradient and the accent change. ok/warn/bad and the log colours stay fixed (R9).
|
||||
A brand hex that is too light for white button text is darkened until it passes.
|
||||
Exits 1 if a contrast check still fails.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
WHITE, BLACK = "#ffffff", "#000000"
|
||||
INK = {"light": "#1c2b33", "dark": "#e6eef4"}
|
||||
PRESETS = {
|
||||
"sky": {"light": {"--sky-top": "#8fc9e8", "--sky-mid": "#bfe3f2", "--sky-bottom": "#eaf6fb",
|
||||
"--accent": "#3d9bd0", "--accent-lite": "#5cb2e2", "--accent-dark": "#16202a"},
|
||||
"dark": {"--sky-top": "#0f1b26", "--sky-mid": "#13212e", "--sky-bottom": "#182a38",
|
||||
"--accent": "#3d9bd0", "--accent-lite": "#5cb2e2", "--accent-dark": "#2e5d80"}},
|
||||
"teal": {"light": {"--sky-top": "#8fe8ad", "--sky-mid": "#bff2d0", "--sky-bottom": "#eafbf0",
|
||||
"--accent": "#0d9488", "--accent-lite": "#2dd4bf", "--accent-dark": "#16202a"},
|
||||
"dark": {"--sky-top": "#0f2617", "--sky-mid": "#132e1c", "--sky-bottom": "#183823",
|
||||
"--accent": "#0d9488", "--accent-lite": "#2dd4bf", "--accent-dark": "#2e5d80"}},
|
||||
}
|
||||
|
||||
|
||||
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 derive(brand):
|
||||
brand = brand.strip().lower()
|
||||
if len(brand) == 4: # #abc -> #aabbcc
|
||||
brand = "#" + "".join(ch * 2 for ch in brand[1:])
|
||||
accent = brand
|
||||
for _ in range(60): # white text on the Trigger button must stay readable
|
||||
if contrast(WHITE, accent) >= 3:
|
||||
break
|
||||
accent = mix(accent, BLACK, 0.06)
|
||||
base = {"--accent": accent, "--accent-lite": mix(accent, WHITE, 0.2)}
|
||||
return {
|
||||
"light": {"--sky-top": mix(accent, WHITE, 0.55), "--sky-mid": mix(accent, WHITE, 0.72),
|
||||
"--sky-bottom": mix(accent, WHITE, 0.9), **base, "--accent-dark": mix(accent, "#0b1218", 0.85)},
|
||||
"dark": {"--sky-top": mix(accent, BLACK, 0.85), "--sky-mid": mix(accent, BLACK, 0.82),
|
||||
"--sky-bottom": mix(accent, BLACK, 0.78), **base, "--accent-dark": mix(accent, BLACK, 0.4)},
|
||||
}
|
||||
|
||||
|
||||
def checks(t):
|
||||
rows = []
|
||||
for mode, m in t.items():
|
||||
rows += [(mode, "white text on the Trigger button", contrast(WHITE, m["--accent"]), 3),
|
||||
(mode, "ink on the top of the sky", contrast(INK[mode], m["--sky-top"]), 4.5),
|
||||
(mode, "ink on the bottom of the sky", contrast(INK[mode], m["--sky-bottom"]), 4.5)]
|
||||
rows.append(("light", "progress % on the sky", contrast(t["light"]["--accent-dark"], t["light"]["--sky-bottom"]), 4.5))
|
||||
return rows
|
||||
|
||||
|
||||
def block(t):
|
||||
def line(m):
|
||||
r = ",".join(str(v) for v in rgb(m["--accent"]))
|
||||
return "; ".join(f"{k}:{v}" for k, v in m.items()) + f"; --accent-rgb:{r};"
|
||||
return ("/* THEME:START (palette.py --write replaces everything down to THEME:END) */\n"
|
||||
f":root{{\n {line(t['light'])}\n}}\nbody.dark{{\n {line(t['dark'])}\n}}\n/* THEME:END */")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2 or not (sys.argv[1] in PRESETS or sys.argv[1].startswith("#")):
|
||||
sys.exit(__doc__)
|
||||
t = PRESETS.get(sys.argv[1]) or derive(sys.argv[1])
|
||||
css = block(t)
|
||||
print(css + "\n")
|
||||
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'}")
|
||||
if "--write" in sys.argv:
|
||||
page = sys.argv[sys.argv.index("--write") + 1]
|
||||
html = open(page, encoding="utf-8").read()
|
||||
new, n = re.subn(r"/\* THEME:START.*?THEME:END \*/", lambda _: css, html, flags=re.S)
|
||||
if n != 1:
|
||||
sys.exit(f"THEME:START/END markers not found in {page}")
|
||||
open(page, "w", encoding="utf-8").write(new)
|
||||
print(f"\nwrote the theme into {page}")
|
||||
sys.exit(0 if all(r >= n for *_, r, n in rows) else 1)
|
||||
Reference in new issue
Block a user