Add rpa-run-tab skill

A Tebra-style Run tab for desktop PyQt6 bots, built in about 5 minutes:
Trigger button, Excel input card, progress with timer, live log, checkpoint
boxes with time limits, and green/amber/red endings. It is checked with a
fake bot (run_tab.py --selfcheck). The design decisions are in rpa-run-tab/docs/adr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-16 16:30:59 +05:30
commit a57b32e8cb
12 files changed
+1435

No files matched your search

+99
View File
@@ -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)