From cbb636e183e06cb035b3dd5eccc2fa67772ccefa Mon Sep 17 00:00:00 2001 From: PAMulligan Date: Sun, 21 Jun 2026 00:31:33 -0400 Subject: [PATCH] feat(gui): native pickers + Figma validation for pipeline selection (#103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the remaining pipeline-selection gaps. - Native folder picker (Canva export) and file picker (.idml/PDF for InDesign) via new sandboxed selectDirectory / selectFile bridges (dialog.showOpenDialog), with a Browse… button beside each field. - Figma URL validation (figma.com format, inline error) gates launch; the note now states the Figma Dev Mode / Professional-plan requirement explicitly. - "Learn more" opens the per-pipeline doc (figma-to-wordpress / canva-to-wordpress / pipelines/indesign) via openPath. Pipeline selection + launch (Figma→claude, Canva→init, InDesign→flavian CLI) and the no-duplication wiring shipped in #129; this adds the pickers, validation, Professional-plan surfacing, and doc links the issue asks for. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/GUI.md | 2 +- .../gui/src/main/ipc/register-handlers.ts | 31 +++++-- packages/gui/src/preload/index.ts | 3 + .../src/renderer/components/PipelinePanel.tsx | 89 ++++++++++++++----- packages/gui/src/renderer/styles/app.css | 15 ++++ packages/gui/src/shared/ipc-channels.ts | 2 + packages/gui/src/shared/types/ipc.ts | 6 ++ 7 files changed, 119 insertions(+), 29 deletions(-) diff --git a/docs/GUI.md b/docs/GUI.md index 3a6fd6d..49467a1 100644 --- a/docs/GUI.md +++ b/docs/GUI.md @@ -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 diff --git a/packages/gui/src/main/ipc/register-handlers.ts b/packages/gui/src/main/ipc/register-handlers.ts index eb72f78..f9b6e87 100644 --- a/packages/gui/src/main/ipc/register-handlers.ts +++ b/packages/gui/src/main/ipc/register-handlers.ts @@ -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'; @@ -51,18 +51,20 @@ export function registerHandlers(deps: HandlerDeps): void { const initResults = new Map>(); const pipelineResults = new Map>(); + // 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 => { - 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]); @@ -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 => { + 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 => { + 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, diff --git a/packages/gui/src/preload/index.ts b/packages/gui/src/preload/index.ts index ff435fb..c3a20b5 100644 --- a/packages/gui/src/preload/index.ts +++ b/packages/gui/src/preload/index.ts @@ -12,6 +12,9 @@ import type { TaskEvent, TaskSnapshot } from '../shared/types/task'; const bridge: FlavianBridge = { getProject: () => ipcRenderer.invoke(IPC.projectGet) as Promise, selectProject: () => ipcRenderer.invoke(IPC.projectSelect) as Promise, + selectDirectory: () => ipcRenderer.invoke(IPC.dialogDirectory) as Promise, + selectFile: (extensions: string[]) => + ipcRenderer.invoke(IPC.dialogFile, extensions) as Promise, runPrereqCheck: () => ipcRenderer.invoke(IPC.prereqRun) as Promise<{ taskId: string }>, getPrereqResult: (taskId) => ipcRenderer.invoke(IPC.prereqResult, taskId) as Promise, runInit: (input) => ipcRenderer.invoke(IPC.initRun, input) as Promise<{ taskId: string }>, diff --git a/packages/gui/src/renderer/components/PipelinePanel.tsx b/packages/gui/src/renderer/components/PipelinePanel.tsx index 3db33f8..607310e 100644 --- a/packages/gui/src/renderer/components/PipelinePanel.tsx +++ b/packages/gui/src/renderer/components/PipelinePanel.tsx @@ -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'; @@ -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 = { + 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('figma'); const [slug, setSlug] = useState(''); @@ -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); @@ -115,47 +144,67 @@ export function PipelinePanel() { {kind === 'figma' && ( )} {kind === 'canva' && ( -