diff --git a/docs/GUI.md b/docs/GUI.md index 49467a1..df90ca1 100644 --- a/docs/GUI.md +++ b/docs/GUI.md @@ -57,8 +57,8 @@ Figma `claude` session and `docker logs -f`). | **Prerequisites** | Runs the prereq check and shows a pass/fail checklist with actionable guidance. | `scripts/check-prerequisites.sh` | | **Setup wizard** | Scaffolds a project (slug, theme starter, Canva/Woo/multisite/port…). | `scripts/init/` `resolveDefaults()` + `apply()` (in-process) | | **WordPress (Docker)** | Build/start/stop/restart/install, live container status, theme activation, log streaming; quick links to the site / wp-admin / phpMyAdmin, first-run ordering guidance, and actionable error hints (Docker not running, port conflicts) linking to `docs/docker-troubleshooting.md`. | `wordpress-local.sh` + `docker compose` | -| **Convert design** | Pick Figma / Canva / InDesign, provide the input (Figma URL with validation + Dev Mode/Professional-plan note; native folder picker for Canva; native `.idml`/PDF file picker for InDesign), launch the existing workflow, and stream progress. Links to each pipeline's docs. | `claude -p` / init canva path / `flavian pipeline indesign` | -| **Visual QA** | Runs visual-diff + Lighthouse and renders the artifacts (diff triptychs, scores, report). | `pnpm visual:diff` / `pnpm lighthouse:run` | +| **Convert design** | Pick Figma / Canva / InDesign, provide the input (Figma URL with validation + Dev Mode/Professional-plan note; native folder picker for Canva; native `.idml`/PDF file picker for InDesign), launch the existing workflow, and stream progress with running/success/failure states. On success, surfaces the theme location with a direct **Activate** action; on failure, links to the troubleshooting docs. Links to each pipeline's docs. | `claude -p` / init canva path / `flavian pipeline indesign` | +| **Visual QA** | Runs visual-diff + Lighthouse and renders the artifacts: regression diff triptychs, Lighthouse scores, the QA agent's report, and (where available) **design-vs-result** side-by-side from the visual-qa-agent screenshots. | `pnpm visual:diff` / `pnpm lighthouse:run` | The **Setup wizard** mirrors `pnpm run init` — the same inputs and flags documented in **[CLI-WIZARD.md](CLI-WIZARD.md)** — and runs the wizard's `apply()` / `resolveDefaults()` diff --git a/packages/gui/src/core/qa/discover.ts b/packages/gui/src/core/qa/discover.ts index 0e247cf..697c2fd 100644 --- a/packages/gui/src/core/qa/discover.ts +++ b/packages/gui/src/core/qa/discover.ts @@ -1,6 +1,7 @@ import { promises as fs } from 'node:fs'; import { join } from 'node:path'; import type { + DesignComparison, LighthouseSummary, QaArtifacts, VisualReport, @@ -80,6 +81,31 @@ export function mapLighthouse(raw: RawAssertion[]): LighthouseSummary { }; } +/** Pair design screenshots (figma/) with rendered results (wordpress/chromium/) by page stem. */ +export function pairDesignResults(designs: string[], results: string[]): DesignComparison[] { + return designs + .filter((d) => /\.png$/i.test(d)) + .map((d) => { + const stem = d.replace(/\.png$/i, ''); + const match = + results.find((r) => r.startsWith(`${stem}-`) && /desktop/i.test(r)) ?? + results.find((r) => r.startsWith(`${stem}-`) || r.startsWith(`${stem}.`) || r === d); + return { + name: stem, + designRel: `.claude/visual-qa/screenshots/figma/${d}`, + resultRel: match ? `.claude/visual-qa/screenshots/wordpress/chromium/${match}` : undefined, + }; + }); +} + +async function listPngs(dir: string): Promise { + try { + return (await fs.readdir(dir)).filter((f) => f.toLowerCase().endsWith('.png')).sort(); + } catch { + return []; + } +} + /** Discover whatever QA artifacts exist on disk for the project. */ export async function discoverQaArtifacts(repoRoot: string): Promise { const rawVisual = await readJson( @@ -93,9 +119,15 @@ export async function discoverQaArtifacts(repoRoot: string): Promise(null); + const activate = useTaskStream(activateTaskId); + const activateTheme = (themeSlug: string): void => { + void bridge() + .runDocker('activate-theme', themeSlug) + .then(({ taskId }) => setActivateTaskId(taskId)) + .catch(() => {}); + }; + const openHelp = (relPath: string): void => { + void bridge().openPath(relPath); + }; + const launch = () => { const input: PipelineInput = { kind, slug: slug.trim(), figmaUrl, canvaExport, indesignFile }; run(input); @@ -87,17 +100,54 @@ export function PipelinePanel() { {result.ok ? ( -
- Conversion finished ({result.kind}) - - Theme: themes/{result.slug} — activate it from the WordPress tab. - -
+ <> +
+ Conversion finished ({result.kind}) + + Theme: themes/{result.slug} + +
+
+ + {activate.state === 'succeeded' && ( + Activated — open it from the WordPress tab. + )} + {activate.state === 'failed' && ( + Activation failed — start WordPress first, then retry. + )} +
+ ) : ( -
- Conversion failed - {result.error} -
+ <> +
+ Conversion failed + {result.error} +
+

+ Troubleshooting:{' '} + + {' · '} + +

+ )}
Conversion log diff --git a/packages/gui/src/renderer/components/QaPanel.tsx b/packages/gui/src/renderer/components/QaPanel.tsx index a08817a..cda5403 100644 --- a/packages/gui/src/renderer/components/QaPanel.tsx +++ b/packages/gui/src/renderer/components/QaPanel.tsx @@ -137,6 +137,33 @@ export function QaPanel() { )} + {artifacts && artifacts.design.length > 0 && ( +
+

Design vs result

+ {artifacts.design.map((d) => ( +
+
+ {d.name} +
+
+
+
Design
+ +
+
+
Result
+ {d.resultRel ? ( + + ) : ( +
no render yet
+ )} +
+
+
+ ))} +
+ )} +

Lighthouse

{!lighthouse ? ( diff --git a/packages/gui/src/renderer/styles/app.css b/packages/gui/src/renderer/styles/app.css index 7b572b9..c4d6b11 100644 --- a/packages/gui/src/renderer/styles/app.css +++ b/packages/gui/src/renderer/styles/app.css @@ -378,6 +378,11 @@ button:disabled { font-size: 12px; } +.activate-note { + color: var(--ok); + font-size: 13px; +} + .field-check { display: flex; align-items: center; diff --git a/packages/gui/src/shared/types/qa.ts b/packages/gui/src/shared/types/qa.ts index 24e3d87..7fbd2a4 100644 --- a/packages/gui/src/shared/types/qa.ts +++ b/packages/gui/src/shared/types/qa.ts @@ -36,11 +36,20 @@ export interface LighthouseSummary { failures: LighthouseFailure[]; } +/** A design screenshot paired with its rendered result (visual-qa-agent output). */ +export interface DesignComparison { + name: string; + designRel: string; + resultRel?: string; +} + export interface QaArtifacts { /** Parsed tests/visual/report.json (pixel-diff results), or null if absent. */ visual: VisualReport | null; /** Parsed .lighthouseci/assertion-results.json summary, or null if absent. */ lighthouse: LighthouseSummary | null; + /** Design-vs-result pairs from .claude/visual-qa/screenshots (empty if none). */ + design: DesignComparison[]; /** Repo-relative path to .claude/visual-qa/report.md if present. */ qaReportPath: string | null; } diff --git a/packages/gui/tests/core/qa/qa.test.mts b/packages/gui/tests/core/qa/qa.test.mts index e94f1ba..26f3ad0 100644 --- a/packages/gui/tests/core/qa/qa.test.mts +++ b/packages/gui/tests/core/qa/qa.test.mts @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { mapLighthouse, mapVisualReport } from '../../../src/core/qa/discover'; +import { mapLighthouse, mapVisualReport, pairDesignResults } from '../../../src/core/qa/discover'; import { isQaScript, qaCommandSpec } from '../../../src/core/qa/qa-commands'; import { CommandBuilder } from '../../../src/core/shell/command-builder'; import type { ShellResolver } from '../../../src/core/shell/shell-resolver'; @@ -44,6 +44,22 @@ test('qaCommandSpec runs a pnpm script via `bash -c`', async () => { assert.deepEqual(spec.args, ['-c', 'pnpm run visual:diff']); }); +test('pairDesignResults pairs design shots with desktop results by page stem', () => { + const pairs = pairDesignResults( + ['home.png', 'about.png'], + ['home-desktop-1440px.png', 'home-mobile-375px.png'], + ); + assert.equal(pairs.length, 2); + const home = pairs.find((p) => p.name === 'home'); + assert.equal(home?.designRel, '.claude/visual-qa/screenshots/figma/home.png'); + assert.equal( + home?.resultRel, + '.claude/visual-qa/screenshots/wordpress/chromium/home-desktop-1440px.png', + ); + const about = pairs.find((p) => p.name === 'about'); + assert.equal(about?.resultRel, undefined); // no matching result render +}); + test('isQaScript validates the script set', () => { assert.ok(isQaScript('visual:diff')); assert.ok(isQaScript('lighthouse:run'));