--- name: ui-preview-loop description: Runs a screenshot-critique-fix loop against the live app to verify UI changes visually. Use after implementing any UI, when matching a mock or reference image, when the user asks to preview, or to fix layout/styling issues that require seeing the rendered page. license: MIT metadata: author: lammanhhoang version: "1.0.0" --- # UI Preview Loop ## When to use - After implementing or modifying any UI: component, page, layout, theme, CSS, copy that renders. - When matching a mock, Figma export, or reference screenshot. - When the user reports a visual bug ("looks broken", "overlaps", "cut off", "misaligned"). - When the user asks to preview, demo, or "see" the app. When NOT to use: - Pure backend/API/data changes with no rendered surface. - Unit-test-only or refactor-only work where the DOM output is provably identical. - If you are unsure whether the render changed, that doubt is exactly why you run the loop. ## Iron Law **THE CODE IS NOT THE UI.** Never judge visual work from source code — render it, screenshot it, look at it. Agents confidently praise their own unrendered work; that praise is worthless. A visual claim without a screenshot attached is a guess dressed up as a report. ## Workflow 1. **Get a target.** Before major UI work, request a reference image ("paste a screenshot or mock of what this should look like"). A pasted image beats any prose description. If none exists, write down 4-6 concrete visual acceptance criteria first (sizes, colors, breakpoints) and treat those as the target. 2. **Start the server, managed.** Use `scripts/with_server.py` (below). Never `npm run dev &` and hope — you get orphan node processes and races against an unopened port. 3. **Screenshot the matrix.** Key states x 3 viewports x light+dark if themed. Wait for `networkidle` before every screenshot. See matrix below. 4. **Critique by delta-listing.** Never ask "does this look right?" Use the delta prompt verbatim (below). 5. **Fix only the listed deltas.** No opportunistic redesigns mid-loop. 6. **Re-screenshot only affected states.** Compare again. 7. **Stop at 2-3 iterations.** Hard cap. Report remaining gaps *with the screenshots* instead of looping again. More loops = diminishing returns and drift: each fix perturbs adjacent styles and you start chasing your own tail. 8. **Fresh-eyes judge (big jobs only).** See below. 9. **Final audit.** Walk the screenshots through Nielsen's 10 usability heuristics — read `references/HEURISTICS.md` (load only at final-audit time, not during the fix loop). 10. **Kill the server.** `with_server.py` does this for you; if you used anything else, verify the port is free afterward. ## Server lifecycle Default driver: **Playwright** (`pip install playwright && python -m playwright install chromium`). Escape hatch: if your harness has built-in browser tooling — Claude Code's preview pane / `preview_start`, claude-in-chrome, Antigravity's browser — use that instead of Playwright; it manages the server and screenshots natively. The loop, matrix, and critique rules stay identical. Single frontend: ```bash python scripts/with_server.py --server "npm run dev" --port 5173 -- python shoot.py http://localhost:5173 shots ``` Stacked backend + frontend (servers start in order, each waits for its port, all are killed after the command — even on failure): ```bash python scripts/with_server.py \ --server "uvicorn app:app --port 8000" --port 8000 \ --server "npm run dev" --port 5173 \ -- python shoot.py http://localhost:5173 shots ``` Exit code 124 means a server never opened its port within `--timeout` (default 60s) — read the server's own output before retrying; do not just bump the timeout. ## Screenshot matrix | Axis | Values | |-----------|---------------------------------------------------------------------------------| | States | every state you touched: default, empty, loading, error, long-content/overflow | | Viewports | 375x812 (mobile), 768x1024 (tablet), 1280x800 (desktop) | | Themes | light + dark — only if the app is themed | Minimum bar even for a "tiny" change: 1 state x 3 viewports. Skipping 375 is a red flag, not a shortcut. Name files `state-viewport-theme.png` (e.g. `checkout-375-dark.png`) in a scratch directory so the set is diffable across iterations. ```python # shoot.py -- python shoot.py http://localhost:5173 shots import sys from playwright.sync_api import sync_playwright url, outdir = sys.argv[1].rstrip("/"), sys.argv[2].rstrip("/") states = {"home": "/"} # edit: routes/states you touched viewports = {375: 812, 768: 1024, 1280: 800} themes = ["light", "dark"] # drop "dark" if unthemed with sync_playwright() as p: browser = p.chromium.launch() for name, path in states.items(): for w, h in viewports.items(): for theme in themes: page = browser.new_page(viewport={"width": w, "height": h}, color_scheme=theme) page.goto(url + path, wait_until="networkidle") page.screenshot(path=f"{outdir}/{name}-{w}-{theme}.png", full_page=True) page.close() browser.close() ``` `wait_until="networkidle"` is mandatory. A screenshot of a skeleton loader or blank flash tells you nothing and poisons the critique. ## Delta-listing critique "Does this look right?" invites a yes. Asked of your own work it *always* gets a yes. Instead, run this prompt against the screenshot + target, verbatim: > List exactly what differs between screenshot and target in: (1) typography size hierarchy, (2) color palette, (3) spacing rhythm, (4) section structure/alignment. Then fix only the listed deltas. Bad critique output (worthless): > The page looks great and closely matches the design. Clean and professional! Good critique output (actionable): > 1. Typography: h1 renders ~24px, target is ~36px; body/label contrast too flat. > 2. Color: CTA is #3B82F6, target is #16A34A; card borders visible, target has none. > 3. Spacing: sections gap 24px, target rhythm is 64px; hero padding-top too small. > 4. Structure: pricing cards left-aligned in a 2-col wrap; target centers 3 equal columns. Every delta must name a measurable property. "Feels off" is not a delta. ## Fresh-eyes judge (big jobs) For a full page or multi-screen build, spawn a separate agent/pass that sees **only the screenshots and the target — never the code**, and grades 1-10 with one concrete fix per dimension: 1. **Design quality** — visual hierarchy, balance, polish. 2. **Originality** — does it look like a default Bootstrap/Tailwind template? 3. **Craft** — spacing/alignment nits: uneven gutters, off-by-2px baselines, inconsistent radii. 4. **Functionality** — do the screenshots show working states (real data, no overlap, no clipped text)? The builder cannot grade its own work; knowing the code biases the judge toward what was *intended* instead of what *rendered*. ## Rationalization table | Excuse | Counter | |---|---| | "The code obviously renders fine" | Stop. That's rationalization. CSS cascade, flex quirks, font fallbacks, and hydration all break "obvious" code. Render it. | | "I can tell from the JSX" | JSX shows structure, not paint. You cannot see wrapping, overflow, z-index, or dark-mode contrast in JSX. | | "Screenshots are slow" | One matrix run is under a minute. The user re-opening the task because the navbar is broken costs an hour and your credibility. | | "It's just a copy change" | Copy wraps, truncates, and overflows. Screenshot the state you touched. | | "The tests pass" | Tests assert DOM, not pixels. A green suite renders white-on-white text just fine. | | "Starting the server is a hassle" | `with_server.py` is one command, and it cleans up after itself. | | "It looked fine at desktop width" | Then you verified one cell of the matrix. 375px or it didn't happen. | | "One more iteration will nail it" | After 3 you are drifting, not converging. Report the remaining gaps with screenshots and stop. | ## Red flags — stop and course-correct - [ ] You claimed visual success without an attached screenshot. - [ ] You skipped the 375px mobile viewport. - [ ] You are on iteration 4+ of the fix loop. - [ ] You described what the code "should" render instead of what it did render. - [ ] Major UI build started with no reference image and you never asked for one. - [ ] Screenshot captured before `networkidle` (skeletons, blank frames). - [ ] Dev server left running after the loop finished. - [ ] You graded your own large build without a fresh-eyes pass. ## What NOT to do - Do not paste source code as evidence that the UI is correct. - Do not eyeball a single desktop screenshot and call the matrix done. - Do not "fix" things the delta list did not name. - Do not loop past the cap; unresolved deltas go in the report, with images. - Do not run the final Nielsen audit from memory — read `references/HEURISTICS.md` and check each of the 10 against the screenshots. ## Files - references/HEURISTICS.md — Nielsen's 10 usability heuristics adapted for screenshot audits. Load at the final-audit step only, not during the fix loop. - scripts/with_server.py — starts dev server(s), waits on their ports, runs your command, kills the process trees afterwards. Use whenever a dev server is needed. - assets/critique-report-template.md — compact report template (screenshot matrix, delta lists, iteration log, remaining gaps, heuristics audit). Fill it in when reporting results.