Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/GUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ 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** | Launches a Figma, Canva, or InDesign conversion and streams progress. | `claude -p` / init canva path / `flavian pipeline indesign` |
| **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` |

The **Setup wizard** mirrors `pnpm run init` — the same inputs and flags documented in
Expand Down
31 changes: 23 additions & 8 deletions packages/gui/src/main/ipc/register-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BrowserWindow, dialog, ipcMain, shell } from 'electron';
import { BrowserWindow, dialog, ipcMain, shell, type OpenDialogOptions } from 'electron';
import { isAbsolute, relative, resolve } from 'node:path';
import { IPC } from '../../shared/ipc-channels';
import type { DockerCommand, DockerService } from '../../shared/types/docker';
Expand Down Expand Up @@ -51,18 +51,20 @@ export function registerHandlers(deps: HandlerDeps): void {
const initResults = new Map<string, Promise<InitResult>>();
const pipelineResults = new Map<string, Promise<PipelineResult>>();

// Open a native dialog parented to the active window (or window-less if none).
const showOpen = (options: OpenDialogOptions) => {
const win = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
return win ? dialog.showOpenDialog(win, options) : dialog.showOpenDialog(options);
};

ipcMain.handle(IPC.projectGet, (): ProjectRef => deps.project.current);

ipcMain.handle(IPC.projectSelect, async (): Promise<ProjectRef> => {
const win = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
const options = {
const result = await showOpen({
title: 'Select your Flavian project folder',
message: 'Choose the directory that contains wordpress-local.sh and scripts/.',
properties: ['openDirectory' as const],
};
const result = win
? await dialog.showOpenDialog(win, options)
: await dialog.showOpenDialog(options);
properties: ['openDirectory'],
});
if (result.canceled || result.filePaths.length === 0) return deps.project.current;

const ref = await validateRepoRoot(result.filePaths[0]);
Expand All @@ -73,6 +75,19 @@ export function registerHandlers(deps: HandlerDeps): void {
return ref; // invalid refs carry a reason for the renderer to surface
});

ipcMain.handle(IPC.dialogDirectory, async (): Promise<string | null> => {
const result = await showOpen({ properties: ['openDirectory'] });
return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
});

ipcMain.handle(IPC.dialogFile, async (_event, extensions: string[]): Promise<string | null> => {
const result = await showOpen({
properties: ['openFile'],
filters: [{ name: 'Supported files', extensions }],
});
return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
});

ipcMain.handle(IPC.prereqRun, async (): Promise<{ taskId: string }> => {
const prereq = await createPrereqRun({
runner,
Expand Down
3 changes: 3 additions & 0 deletions packages/gui/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import type { TaskEvent, TaskSnapshot } from '../shared/types/task';
const bridge: FlavianBridge = {
getProject: () => ipcRenderer.invoke(IPC.projectGet) as Promise<ProjectRef>,
selectProject: () => ipcRenderer.invoke(IPC.projectSelect) as Promise<ProjectRef>,
selectDirectory: () => ipcRenderer.invoke(IPC.dialogDirectory) as Promise<string | null>,
selectFile: (extensions: string[]) =>
ipcRenderer.invoke(IPC.dialogFile, extensions) as Promise<string | null>,
runPrereqCheck: () => ipcRenderer.invoke(IPC.prereqRun) as Promise<{ taskId: string }>,
getPrereqResult: (taskId) => ipcRenderer.invoke(IPC.prereqResult, taskId) as Promise<PrereqReport>,
runInit: (input) => ipcRenderer.invoke(IPC.initRun, input) as Promise<{ taskId: string }>,
Expand Down
89 changes: 69 additions & 20 deletions packages/gui/src/renderer/components/PipelinePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import type { PipelineInput, PipelineKind } from '../../shared/types/pipeline';
import { bridge } from '../api/bridge';
import { usePipeline } from '../hooks/usePipeline';
import { LogStream } from './LogStream';

Expand All @@ -9,6 +10,15 @@ const KINDS: { value: PipelineKind; label: string }[] = [
{ value: 'indesign', label: 'InDesign' },
];

const FIGMA_URL = /^https?:\/\/(www\.)?figma\.com\//i;

/** Per-pipeline reference docs (opened from the panel). */
const DOCS: Record<PipelineKind, string> = {
figma: 'docs/figma-to-wordpress/README.md',
canva: 'docs/canva-to-wordpress/README.md',
indesign: 'docs/pipelines/indesign.md',
};

export function PipelinePanel() {
const [kind, setKind] = useState<PipelineKind>('figma');
const [slug, setSlug] = useState('');
Expand All @@ -18,14 +28,33 @@ export function PipelinePanel() {

const { result, error, running, lines, run, cancel, reset } = usePipeline();

const figmaUrlValid = FIGMA_URL.test(figmaUrl.trim());
const inputReady =
kind === 'figma'
? figmaUrl.trim() !== ''
? figmaUrlValid
: kind === 'canva'
? canvaExport.trim() !== ''
: indesignFile.trim() !== '';
const canLaunch = slug.trim() !== '' && inputReady && !running;

const browseDir = (): void => {
void bridge()
.selectDirectory()
.then((p) => {
if (p) setCanvaExport(p);
});
};
const browseFile = (): void => {
void bridge()
.selectFile(['idml', 'pdf'])
.then((p) => {
if (p) setIndesignFile(p);
});
};
const openDoc = (): void => {
void bridge().openPath(DOCS[kind]);
};

const launch = () => {
const input: PipelineInput = { kind, slug: slug.trim(), figmaUrl, canvaExport, indesignFile };
run(input);
Expand Down Expand Up @@ -115,47 +144,67 @@ export function PipelinePanel() {

{kind === 'figma' && (
<label className="field">
<span>Figma file URL</span>
<span>Figma file URL (Dev Mode)</span>
<input
value={figmaUrl}
onChange={(e) => setFigmaUrl(e.target.value)}
placeholder="https://www.figma.com/design/…"
spellCheck={false}
/>
{figmaUrl.trim() !== '' && !figmaUrlValid && (
<span className="field-error">Enter a valid figma.com URL.</span>
)}
</label>
)}

{kind === 'canva' && (
<label className="field">
<div className="field">
<span>Canva export directory</span>
<input
value={canvaExport}
onChange={(e) => setCanvaExport(e.target.value)}
placeholder="./canva-export"
spellCheck={false}
/>
</label>
<div className="input-row">
<input
value={canvaExport}
onChange={(e) => setCanvaExport(e.target.value)}
placeholder="./canva-export"
spellCheck={false}
/>
<button type="button" onClick={browseDir}>
Browse…
</button>
</div>
</div>
)}

{kind === 'indesign' && (
<label className="field">
<div className="field">
<span>InDesign file (.idml or .pdf)</span>
<input
value={indesignFile}
onChange={(e) => setIndesignFile(e.target.value)}
placeholder="./brochure.idml"
spellCheck={false}
/>
</label>
<div className="input-row">
<input
value={indesignFile}
onChange={(e) => setIndesignFile(e.target.value)}
placeholder="./brochure.idml"
spellCheck={false}
/>
<button type="button" onClick={browseFile}>
Browse…
</button>
</div>
</div>
)}

{kind === 'figma' && (
<p className="hint-note">
Figma conversion opens a headless Claude Code session and requires Claude Code with Figma
access configured.
Figma conversion needs <strong>Figma Dev Mode</strong> (a Figma Professional plan or
higher) and opens a headless Claude Code session — Claude Code with Figma access must be
configured.
</p>
)}

<p className="panel-intro">
<button type="button" className="link-btn" onClick={openDoc}>
Learn more about the {KINDS.find((k) => k.value === kind)?.label} pipeline →
</button>
</p>

<div className="form-actions">
<button type="submit" disabled={!canLaunch}>
Launch conversion
Expand Down
15 changes: 15 additions & 0 deletions packages/gui/src/renderer/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,21 @@ button:disabled {
border-color: var(--accent);
}

.input-row {
display: flex;
gap: 8px;
align-items: center;
}

.input-row input {
flex: 1;
}

.field-error {
color: var(--fail);
font-size: 12px;
}

.field-check {
display: flex;
align-items: center;
Expand Down
2 changes: 2 additions & 0 deletions packages/gui/src/shared/ipc-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const IPC = {
// request / response
projectGet: 'flavian:project:get',
projectSelect: 'flavian:project:select',
dialogDirectory: 'flavian:dialog:directory',
dialogFile: 'flavian:dialog:file',
prereqRun: 'flavian:prereq:run',
prereqResult: 'flavian:prereq:result',
initRun: 'flavian:init:run',
Expand Down
6 changes: 6 additions & 0 deletions packages/gui/src/shared/types/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface FlavianBridge {
/** Open a native folder picker to choose the Flavian project; persists the choice. */
selectProject(): Promise<ProjectRef>;

/** Native folder picker; returns the chosen absolute path, or null if cancelled. */
selectDirectory(): Promise<string | null>;

/** Native file picker filtered to the given extensions; returns path, or null. */
selectFile(extensions: string[]): Promise<string | null>;

/** Start a prerequisite check; returns the task id to subscribe to. */
runPrereqCheck(): Promise<{ taskId: string }>;

Expand Down
Loading