Existing frontend: detect, report, then keep or restyle (ticket 03)

- templates/detect_frontend.py: finds plain HTML, React/Vue/Angular/Svelte apps
  and PyQt web views; prints location, stack, main colours, font and every
  token block (CSS custom properties, <style> tags, Tailwind config).
- templates/frontend.md: shared report / keep / restyle procedure, written
  so rpa-dashboard-ai can point to it.
- SKILL.md steps 1, 2 and 6 point to it.
- tests: detection self-check; fixture c-web-existing-frontend; eval entries
  c-keep and c-restyle (new pages only, per-question grading, checks on what
  changed in the existing files), both passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-16 14:27:50 +05:30
1 parent 3eb3fdf00e
commit f11bddd958
15 files changed
+1210 -12

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
"""Frontend detection: where an existing frontend is, its stack and its colour tokens."""
import importlib.util
import json
import subprocess
import sys
from pathlib import Path
DETECT = Path(__file__).resolve().parents[1] / "rpa-dashboard-no-ai" / "templates" / "detect_frontend.py"
spec = importlib.util.spec_from_file_location("detect_frontend", DETECT)
detect_frontend = importlib.util.module_from_spec(spec)
spec.loader.exec_module(detect_frontend)
def project(tmp_path, files):
for name, text in files.items():
(tmp_path / name).parent.mkdir(parents=True, exist_ok=True)
(tmp_path / name).write_text(text, encoding="utf-8")
return tmp_path
def cli(root):
return subprocess.run([sys.executable, str(DETECT), str(root)], capture_output=True, text=True, check=True).stdout
def test_no_frontend_when_only_tooling_and_installed_skills_have_html(tmp_path):
root = project(tmp_path, {
"main.py": "print('[step 1] Login')\n",
"node_modules/pkg/index.html": "<style>:root{--x:#123456}</style>",
".venv/lib/site.html": "<p>",
".claude/skills/rpa-dashboard-no-ai/templates/dashboard.html": "<style>body{--accent:#3d9bd0}</style>",
"dist/index.html": "<p>",
})
assert detect_frontend.detect(root) == []
assert cli(root).strip() == "No existing frontend found."
FIXTURE_C = Path(__file__).resolve().parent / "fixtures" / "c-web-existing-frontend"
def test_plain_html_site_reports_location_stack_light_and_dark_tokens():
[f] = detect_frontend.detect(FIXTURE_C)
assert f["path"] == "web/"
assert f["stack"] == "plain HTML (web)"
light, dark = f["blocks"]
assert (light["file"], light["selector"], light["dark"]) == ("web/css/theme.css", ":root", False)
assert light["tokens"]["--color-brand"] == "#0f766e"
assert light["tokens"]["--font-sans"] == '"Trebuchet MS", "Segoe UI", sans-serif'
assert (dark["selector"], dark["dark"]) == ('[data-theme="dark"]', True)
assert dark["tokens"]["--color-brand"] == "#0d9488"
out = cli(FIXTURE_C)
first = out.splitlines()[0]
assert first.startswith("Frontend found at `web/`, stack `plain HTML (web)`, colours `--color-brand #0f766e,")
assert 'fonts `"Trebuchet MS", "Segoe UI", sans-serif`' in first
assert "web/css/theme.css" in out and '[data-theme="dark"]' in out
def test_react_app_is_one_frontend_with_its_media_query_dark_tokens(tmp_path):
root = project(tmp_path, {
"server.py": "from fastapi import FastAPI\n",
"frontend/package.json": json.dumps({"dependencies": {"react": "^19.0.0", "react-dom": "^19.0.0"},
"devDependencies": {"vite": "^6.0.0"}}),
"frontend/index.html": '<div id="root"></div><script type="module" src="/src/main.jsx"></script>',
"frontend/src/index.css": ":root { --primary: 222.2 47.4% 11.2%; --radius: .5rem }\n"
"@media (prefers-color-scheme: dark) { :root { --primary: 210 40% 98% } }",
"frontend/node_modules/react/index.html": "<p>",
})
[f] = detect_frontend.detect(root)
assert (f["path"], f["stack"]) == ("frontend/", "React (Vite, web)")
light, dark = f["blocks"]
assert (light["file"], light["tokens"]["--primary"], light["dark"]) == ("frontend/src/index.css", "222.2 47.4% 11.2%", False)
assert (dark["selector"], dark["tokens"], dark["dark"]) == (
"@media (prefers-color-scheme: dark) :root", {"--primary": "210 40% 98%"}, True)
assert "colours `--primary 222.2 47.4% 11.2%`" in cli(root)
def test_vue_app_reads_component_styles_and_tailwind_theme(tmp_path):
root = project(tmp_path, {
"package.json": json.dumps({"dependencies": {"vue": "^3.5.0"}, "devDependencies": {"tailwindcss": "^3.4.0"}}),
"tailwind.config.js": "module.exports = { theme: { extend: { colors: { brand: '#e11d48', 'brand-dark': \"#9f1239\" } } } }",
"src/App.vue": "<template><p>{{ msg }}</p></template>\n<style>:root { --surface: #ffffff; }</style>",
})
[f] = detect_frontend.detect(root)
assert (f["path"], f["stack"]) == ("./", "Vue (Tailwind, web)")
blocks = {b["file"]: b for b in f["blocks"]}
assert blocks["src/App.vue"]["tokens"] == {"--surface": "#ffffff"}
assert blocks["tailwind.config.js"]["tokens"] == {"brand": "#e11d48", "brand-dark": "#9f1239"}
assert "brand #e11d48" in cli(root).splitlines()[0]
def test_pyqt_web_view_is_a_desktop_frontend(tmp_path):
host = "from PyQt6.QtWebEngineWidgets import QWebEngineView\nview = QWebEngineView()\n"
root = project(tmp_path / "file", {
"app.py": host + "view.load(QUrl.fromLocalFile('ui/page.html'))\n",
"bot.py": "print('[step 1] Login')\n",
"ui/page.html": "<style>:root { --accent: #2563eb; }</style>",
})
[f] = detect_frontend.detect(root)
assert (f["path"], f["stack"]) == ("ui/", "PyQt web view (desktop, host `app.py`)")
assert f["blocks"][0]["tokens"] == {"--accent": "#2563eb"}
inline = project(tmp_path / "inline", {"main.py": host + "view.setHtml('<style>body{--accent:#16a34a}</style><p>Hi</p>')\n"})
[f] = detect_frontend.detect(inline)
assert (f["path"], f["stack"]) == ("main.py", "PyQt web view (desktop, host `main.py`)")
assert f["blocks"][0]["tokens"] == {"--accent": "#16a34a"}