From 16d336913f46a5f6b0d0f9acd603588a9cb2b87b Mon Sep 17 00:00:00 2001 From: jlightner Date: Thu, 26 Mar 2026 05:07:37 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Built=20app=20shell=20with=20ViewState?= =?UTF-8?q?=20routing,=20drag-and-drop=20FileUpload=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "app/src/App.tsx" - "app/src/App.css" - "app/src/views/ImportConvert.tsx" - "app/src/views/ImportConvert.module.css" - "app/src/components/FileUpload.tsx" - "app/src/components/PresetSelector.tsx" GSD-Task: S01/T02 --- .gsd/event-log.jsonl | 1 + .gsd/milestones/M002/slices/S01/S01-PLAN.md | 2 +- .../M002/slices/S01/tasks/T01-VERIFY.json | 16 ++ .../M002/slices/S01/tasks/T02-SUMMARY.md | 86 +++++++++ .gsd/state-manifest.json | 56 ++++-- app/src/App.css | 172 ++++++++++++++++++ app/src/App.tsx | 26 ++- app/src/components/FileUpload.tsx | 124 +++++++++++++ app/src/components/PresetSelector.tsx | 80 ++++++++ app/src/views/ImportConvert.module.css | 48 +++++ app/src/views/ImportConvert.tsx | 45 +++++ 11 files changed, 639 insertions(+), 17 deletions(-) create mode 100644 .gsd/milestones/M002/slices/S01/tasks/T01-VERIFY.json create mode 100644 .gsd/milestones/M002/slices/S01/tasks/T02-SUMMARY.md create mode 100644 app/src/App.css create mode 100644 app/src/components/FileUpload.tsx create mode 100644 app/src/components/PresetSelector.tsx create mode 100644 app/src/views/ImportConvert.module.css create mode 100644 app/src/views/ImportConvert.tsx diff --git a/.gsd/event-log.jsonl b/.gsd/event-log.jsonl index 557fa52..d97d879 100644 --- a/.gsd/event-log.jsonl +++ b/.gsd/event-log.jsonl @@ -14,3 +14,4 @@ {"cmd":"complete-milestone","params":{"milestoneId":"M001"},"ts":"2026-03-26T04:56:43.004Z","actor":"agent","hash":"c877176040436ab9","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"plan-slice","params":{"milestoneId":"M002","sliceId":"S01"},"ts":"2026-03-26T05:01:43.661Z","actor":"agent","hash":"d83541fb49b2737b","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T01"},"ts":"2026-03-26T05:05:22.658Z","actor":"agent","hash":"59aebe24d8f53b7a","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} +{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T02"},"ts":"2026-03-26T05:07:29.861Z","actor":"agent","hash":"a3980272c7b74afa","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} diff --git a/.gsd/milestones/M002/slices/S01/S01-PLAN.md b/.gsd/milestones/M002/slices/S01/S01-PLAN.md index 8373823..c2c5481 100644 --- a/.gsd/milestones/M002/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M002/slices/S01/S01-PLAN.md @@ -26,7 +26,7 @@ - Estimate: 45m - Files: engine/main.py, app/package.json, app/vite.config.ts, app/tsconfig.json, app/src/types/engine.ts, app/src/api/engine.ts, app/src/api/__tests__/engine.test.ts, app/src/App.tsx - Verify: cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 -- [ ] **T02: Build app shell with view routing, file upload zone, and preset selector** — Build the first user-facing components: the app shell with view-state routing (Import → Canvas → Export), a drag-and-drop file upload zone, and a preset selector that fetches presets from the engine and populates parameter defaults. +- [x] **T02: Built app shell with ViewState routing, drag-and-drop FileUpload with thumbnail preview, PresetSelector fetching from engine API, and two-column ImportConvert layout** — Build the first user-facing components: the app shell with view-state routing (Import → Canvas → Export), a drag-and-drop file upload zone, and a preset selector that fetches presets from the engine and populates parameter defaults. ## Steps diff --git a/.gsd/milestones/M002/slices/S01/tasks/T01-VERIFY.json b/.gsd/milestones/M002/slices/S01/tasks/T01-VERIFY.json new file mode 100644 index 0000000..9ae5c82 --- /dev/null +++ b/.gsd/milestones/M002/slices/S01/tasks/T01-VERIFY.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M002/S01/T01", + "timestamp": 1774501531197, + "passed": true, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd app", + "exitCode": 0, + "durationMs": 5, + "verdict": "pass" + } + ] +} diff --git a/.gsd/milestones/M002/slices/S01/tasks/T02-SUMMARY.md b/.gsd/milestones/M002/slices/S01/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..b92c0da --- /dev/null +++ b/.gsd/milestones/M002/slices/S01/tasks/T02-SUMMARY.md @@ -0,0 +1,86 @@ +--- +id: T02 +parent: S01 +milestone: M002 +provides: [] +requires: [] +affects: [] +key_files: ["app/src/App.tsx", "app/src/App.css", "app/src/views/ImportConvert.tsx", "app/src/views/ImportConvert.module.css", "app/src/components/FileUpload.tsx", "app/src/components/PresetSelector.tsx"] +key_decisions: ["Use CSS modules for view-specific layout, global App.css for shared component styles", "Underscore-prefix unused state variables to keep them wired for T03 without lint warnings"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "Ran `cd app && npx tsc --noEmit` — zero TypeScript errors. Ran `cd app && npx vitest run --reporter=verbose` — all 9 existing tests pass." +completed_at: 2026-03-26T05:07:29.816Z +blocker_discovered: false +--- + +# T02: Built app shell with ViewState routing, drag-and-drop FileUpload with thumbnail preview, PresetSelector fetching from engine API, and two-column ImportConvert layout + +> Built app shell with ViewState routing, drag-and-drop FileUpload with thumbnail preview, PresetSelector fetching from engine API, and two-column ImportConvert layout + +## What Happened +--- +id: T02 +parent: S01 +milestone: M002 +key_files: + - app/src/App.tsx + - app/src/App.css + - app/src/views/ImportConvert.tsx + - app/src/views/ImportConvert.module.css + - app/src/components/FileUpload.tsx + - app/src/components/PresetSelector.tsx +key_decisions: + - Use CSS modules for view-specific layout, global App.css for shared component styles + - Underscore-prefix unused state variables to keep them wired for T03 without lint warnings +duration: "" +verification_result: passed +completed_at: 2026-03-26T05:07:29.827Z +blocker_discovered: false +--- + +# T02: Built app shell with ViewState routing, drag-and-drop FileUpload with thumbnail preview, PresetSelector fetching from engine API, and two-column ImportConvert layout + +**Built app shell with ViewState routing, drag-and-drop FileUpload with thumbnail preview, PresetSelector fetching from engine API, and two-column ImportConvert layout** + +## What Happened + +Created the ImportConvert view (View 1) with a two-column flexbox layout: left panel for controls, right panel for the SVG preview placeholder. Built FileUpload component with drag-and-drop zone using onDragOver/onDrop handlers plus a hidden file input fallback, thumbnail rendering via URL.createObjectURL, file size display, and SVG file detection for mode switching. Built PresetSelector that fetches presets from the engine API on mount with AbortController cleanup, renders preset cards in a 2-column grid with selected state highlighting, and auto-selects 'sign' as the default. Updated App.tsx with a ViewState type union ('import' | 'canvas' | 'export') and conditional rendering. Created App.css with global styles for the upload zone, preset cards, and placeholder views. Wired state in ImportConvert for T03 consumption. + +## Verification + +Ran `cd app && npx tsc --noEmit` — zero TypeScript errors. Ran `cd app && npx vitest run --reporter=verbose` — all 9 existing tests pass. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 832ms | +| 2 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2000ms | + + +## Deviations + +None. All 6 expected output files created as planned. + +## Known Issues + +None. + +## Files Created/Modified + +- `app/src/App.tsx` +- `app/src/App.css` +- `app/src/views/ImportConvert.tsx` +- `app/src/views/ImportConvert.module.css` +- `app/src/components/FileUpload.tsx` +- `app/src/components/PresetSelector.tsx` + + +## Deviations +None. All 6 expected output files created as planned. + +## Known Issues +None. diff --git a/.gsd/state-manifest.json b/.gsd/state-manifest.json index dc3b8ba..bee94c8 100644 --- a/.gsd/state-manifest.json +++ b/.gsd/state-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "exported_at": "2026-03-26T05:05:22.657Z", + "exported_at": "2026-03-26T05:07:29.861Z", "milestones": [ { "id": "M001", @@ -879,19 +879,29 @@ "milestone_id": "M002", "slice_id": "S01", "id": "T02", - "title": "Build app shell with view routing, file upload zone, and preset selector", - "status": "pending", - "one_liner": "", - "narrative": "", - "verification_result": "", + "title": "Built app shell with ViewState routing, drag-and-drop FileUpload with thumbnail preview, PresetSelector fetching from engine API, and two-column ImportConvert layout", + "status": "complete", + "one_liner": "Built app shell with ViewState routing, drag-and-drop FileUpload with thumbnail preview, PresetSelector fetching from engine API, and two-column ImportConvert layout", + "narrative": "Created the ImportConvert view (View 1) with a two-column flexbox layout: left panel for controls, right panel for the SVG preview placeholder. Built FileUpload component with drag-and-drop zone using onDragOver/onDrop handlers plus a hidden file input fallback, thumbnail rendering via URL.createObjectURL, file size display, and SVG file detection for mode switching. Built PresetSelector that fetches presets from the engine API on mount with AbortController cleanup, renders preset cards in a 2-column grid with selected state highlighting, and auto-selects 'sign' as the default. Updated App.tsx with a ViewState type union ('import' | 'canvas' | 'export') and conditional rendering. Created App.css with global styles for the upload zone, preset cards, and placeholder views. Wired state in ImportConvert for T03 consumption.", + "verification_result": "Ran `cd app && npx tsc --noEmit` — zero TypeScript errors. Ran `cd app && npx vitest run --reporter=verbose` — all 9 existing tests pass.", "duration": "", - "completed_at": null, + "completed_at": "2026-03-26T05:07:29.816Z", "blocker_discovered": false, - "deviations": "", - "known_issues": "", - "key_files": [], - "key_decisions": [], - "full_summary_md": "", + "deviations": "None. All 6 expected output files created as planned.", + "known_issues": "None.", + "key_files": [ + "app/src/App.tsx", + "app/src/App.css", + "app/src/views/ImportConvert.tsx", + "app/src/views/ImportConvert.module.css", + "app/src/components/FileUpload.tsx", + "app/src/components/PresetSelector.tsx" + ], + "key_decisions": [ + "Use CSS modules for view-specific layout, global App.css for shared component styles", + "Underscore-prefix unused state variables to keep them wired for T03 without lint warnings" + ], + "full_summary_md": "---\nid: T02\nparent: S01\nmilestone: M002\nkey_files:\n - app/src/App.tsx\n - app/src/App.css\n - app/src/views/ImportConvert.tsx\n - app/src/views/ImportConvert.module.css\n - app/src/components/FileUpload.tsx\n - app/src/components/PresetSelector.tsx\nkey_decisions:\n - Use CSS modules for view-specific layout, global App.css for shared component styles\n - Underscore-prefix unused state variables to keep them wired for T03 without lint warnings\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T05:07:29.827Z\nblocker_discovered: false\n---\n\n# T02: Built app shell with ViewState routing, drag-and-drop FileUpload with thumbnail preview, PresetSelector fetching from engine API, and two-column ImportConvert layout\n\n**Built app shell with ViewState routing, drag-and-drop FileUpload with thumbnail preview, PresetSelector fetching from engine API, and two-column ImportConvert layout**\n\n## What Happened\n\nCreated the ImportConvert view (View 1) with a two-column flexbox layout: left panel for controls, right panel for the SVG preview placeholder. Built FileUpload component with drag-and-drop zone using onDragOver/onDrop handlers plus a hidden file input fallback, thumbnail rendering via URL.createObjectURL, file size display, and SVG file detection for mode switching. Built PresetSelector that fetches presets from the engine API on mount with AbortController cleanup, renders preset cards in a 2-column grid with selected state highlighting, and auto-selects 'sign' as the default. Updated App.tsx with a ViewState type union ('import' | 'canvas' | 'export') and conditional rendering. Created App.css with global styles for the upload zone, preset cards, and placeholder views. Wired state in ImportConvert for T03 consumption.\n\n## Verification\n\nRan `cd app && npx tsc --noEmit` — zero TypeScript errors. Ran `cd app && npx vitest run --reporter=verbose` — all 9 existing tests pass.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 832ms |\n| 2 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2000ms |\n\n\n## Deviations\n\nNone. All 6 expected output files created as planned.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `app/src/App.tsx`\n- `app/src/App.css`\n- `app/src/views/ImportConvert.tsx`\n- `app/src/views/ImportConvert.module.css`\n- `app/src/components/FileUpload.tsx`\n- `app/src/components/PresetSelector.tsx`\n", "description": "Build the first user-facing components: the app shell with view-state routing (Import → Canvas → Export), a drag-and-drop file upload zone, and a preset selector that fetches presets from the engine and populates parameter defaults.\n\n## Steps\n\n1. Create `app/src/views/ImportConvert.tsx` — the main View 1 container. Layout: left panel (upload + controls) and right panel (preview area, placeholder for now). Use CSS modules or a single `app/src/views/ImportConvert.module.css`.\n2. Update `app/src/App.tsx` with view-state routing using React useState:\n - `type ViewState = 'import' | 'canvas' | 'export'`\n - Render `ImportConvert` when state is 'import', placeholder `
View 2: Design Canvas
` for 'canvas', placeholder for 'export'\n - Pass a callback `onUseThis(svgOutput: string, metadata: TraceMetadata)` that transitions to 'canvas'\n3. Create `app/src/components/FileUpload.tsx`:\n - Drag-and-drop zone with `onDragOver`/`onDrop` handlers + hidden file `` fallback\n - Accept: `.png,.jpg,.jpeg,.bmp,.tiff,.webp,.svg` via accept attribute\n - On file selection: store `File` in parent state, show filename + file size, render thumbnail via `URL.createObjectURL()`\n - Detect SVG uploads (check file type or extension) and surface this to parent for mode switching\n4. Create `app/src/components/PresetSelector.tsx`:\n - On mount: call `getPresets()` from `app/src/api/engine.ts`\n - Render preset cards — each shows name and description from the preset config\n - Clicking a card selects it (visual highlight) and calls `onPresetSelect(presetName, presetConfig)` callback\n - Default selection: 'sign' preset\n5. Create `app/src/App.css` (or global styles) with minimal layout: flexbox two-column layout for View 1, card styles for presets, drop zone styling with dashed border + hover state.\n6. Wire components together in `ImportConvert.tsx`: FileUpload at top, PresetSelector below it, both in the left panel. Store selected file and preset in local state.", "estimate": "1h", "files": [ @@ -1247,6 +1257,28 @@ "verdict": "✅ pass", "duration_ms": 2000, "created_at": "2026-03-26T05:05:22.615Z" + }, + { + "id": 17, + "task_id": "T02", + "slice_id": "S01", + "milestone_id": "M002", + "command": "cd app && npx vitest run --reporter=verbose", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 832, + "created_at": "2026-03-26T05:07:29.816Z" + }, + { + "id": 18, + "task_id": "T02", + "slice_id": "S01", + "milestone_id": "M002", + "command": "cd app && npx tsc --noEmit", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 2000, + "created_at": "2026-03-26T05:07:29.816Z" } ] } \ No newline at end of file diff --git a/app/src/App.css b/app/src/App.css new file mode 100644 index 0000000..b6c22ed --- /dev/null +++ b/app/src/App.css @@ -0,0 +1,172 @@ +/* ── Global component styles for Kerf Engine app ── */ + +/* File Upload Zone */ +.file-upload-zone { + border: 2px dashed var(--border); + border-radius: 8px; + padding: 24px 16px; + text-align: center; + cursor: pointer; + transition: border-color 0.15s, background-color 0.15s; + user-select: none; +} + +.file-upload-zone:hover, +.file-upload-zone--drag-over { + border-color: var(--accent); + background: var(--accent-bg); +} + +.file-upload-zone:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.file-upload-prompt { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.file-upload-icon { + font-size: 28px; +} + +.file-upload-prompt p { + margin: 0; + color: var(--text-h); + font-size: 15px; +} + +.file-upload-prompt small { + color: var(--text); + font-size: 13px; +} + +.file-upload-preview { + display: flex; + align-items: center; + gap: 12px; +} + +.file-upload-thumb { + width: 56px; + height: 56px; + object-fit: cover; + border-radius: 4px; + border: 1px solid var(--border); +} + +.file-upload-info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + text-align: left; +} + +.file-upload-name { + font-size: 14px; + font-weight: 500; + color: var(--text-h); + word-break: break-all; +} + +.file-upload-size { + font-size: 13px; + color: var(--text); +} + +.file-upload-badge { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--accent); + background: var(--accent-bg); + padding: 2px 6px; + border-radius: 3px; +} + +/* Preset Selector */ +.preset-selector { + display: flex; + flex-direction: column; + gap: 8px; +} + +.preset-heading { + font-size: 14px; + font-weight: 600; + color: var(--text-h); + margin: 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.preset-loading, +.preset-error { + font-size: 14px; + color: var(--text); + margin: 0; +} + +.preset-error { + color: #e74c3c; +} + +.preset-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.preset-card { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + cursor: pointer; + text-align: left; + transition: border-color 0.12s, background-color 0.12s; + font-family: inherit; +} + +.preset-card:hover { + border-color: var(--accent-border); + background: var(--accent-bg); +} + +.preset-card--selected { + border-color: var(--accent); + background: var(--accent-bg); + box-shadow: 0 0 0 1px var(--accent); +} + +.preset-card-name { + font-size: 14px; + font-weight: 600; + color: var(--text-h); + text-transform: capitalize; +} + +.preset-card-desc { + font-size: 12px; + color: var(--text); + line-height: 1.3; +} + +/* Placeholder views */ +.placeholder-view { + display: flex; + align-items: center; + justify-content: center; + min-height: 60vh; + font-size: 20px; + color: var(--text); +} diff --git a/app/src/App.tsx b/app/src/App.tsx index 8e73d12..f1dd640 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,10 +1,28 @@ +import { useState } from 'react'; +import type { TraceMetadata } from './types/engine'; +import ImportConvert from './views/ImportConvert'; +import './App.css'; + +type ViewState = 'import' | 'canvas' | 'export'; + function App() { + const [view, setView] = useState('import'); + const [_svgResult, setSvgResult] = useState(null); + const [_traceMetadata, setTraceMetadata] = useState(null); + + const handleUseThis = (svgOutput: string, metadata: unknown) => { + setSvgResult(svgOutput); + setTraceMetadata(metadata as TraceMetadata); + setView('canvas'); + }; + return (
-

Kerf Engine

-

Import & Convert view coming in T02.

+ {view === 'import' && } + {view === 'canvas' &&
View 2: Design Canvas
} + {view === 'export' &&
View 3: Export
}
- ) + ); } -export default App +export default App; diff --git a/app/src/components/FileUpload.tsx b/app/src/components/FileUpload.tsx new file mode 100644 index 0000000..d22fc9e --- /dev/null +++ b/app/src/components/FileUpload.tsx @@ -0,0 +1,124 @@ +import { useCallback, useRef, useState } from 'react'; + +const ACCEPTED_TYPES = '.png,.jpg,.jpeg,.bmp,.tiff,.webp,.svg'; + +interface FileUploadProps { + onFileSelect: (file: File, isSvg: boolean) => void; + selectedFile: File | null; +} + +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function isSvgFile(file: File): boolean { + return ( + file.type === 'image/svg+xml' || + file.name.toLowerCase().endsWith('.svg') + ); +} + +export default function FileUpload({ onFileSelect, selectedFile }: FileUploadProps) { + const inputRef = useRef(null); + const [isDragOver, setIsDragOver] = useState(false); + const [thumbnailUrl, setThumbnailUrl] = useState(null); + + const handleFile = useCallback( + (file: File) => { + // Revoke previous thumbnail URL to avoid memory leaks + if (thumbnailUrl) { + URL.revokeObjectURL(thumbnailUrl); + } + const url = URL.createObjectURL(file); + setThumbnailUrl(url); + onFileSelect(file, isSvgFile(file)); + }, + [onFileSelect, thumbnailUrl], + ); + + const onDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(true); + }, []); + + const onDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + }, []); + + const onDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + const file = e.dataTransfer.files[0]; + if (file) handleFile(file); + }, + [handleFile], + ); + + const onInputChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) handleFile(file); + }, + [handleFile], + ); + + return ( +
inputRef.current?.click()} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') inputRef.current?.click(); + }} + aria-label="Upload image file" + > + + {selectedFile ? ( +
+ {thumbnailUrl && ( + Uploaded preview + )} +
+ {selectedFile.name} + + {formatFileSize(selectedFile.size)} + + {isSvgFile(selectedFile) && ( + SVG — simplify mode + )} +
+
+ ) : ( +
+ 📁 +

+ Drop an image here or click to browse +

+ PNG, JPG, BMP, TIFF, WebP, or SVG +
+ )} +
+ ); +} diff --git a/app/src/components/PresetSelector.tsx b/app/src/components/PresetSelector.tsx new file mode 100644 index 0000000..032fbea --- /dev/null +++ b/app/src/components/PresetSelector.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from 'react'; +import { getPresets } from '../api/engine'; +import type { PresetConfig } from '../types/engine'; + +interface PresetSelectorProps { + selectedPreset: string; + onPresetSelect: (presetName: string, config: PresetConfig) => void; +} + +export default function PresetSelector({ + selectedPreset, + onPresetSelect, +}: PresetSelectorProps) { + const [presets, setPresets] = useState>({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const controller = new AbortController(); + + getPresets(controller.signal) + .then((res) => { + setPresets(res.presets); + setLoading(false); + + // Auto-select default preset if none selected yet, or if current selection doesn't exist + if (!res.presets[selectedPreset]) { + const defaultKey = res.presets['sign'] ? 'sign' : Object.keys(res.presets)[0]; + if (defaultKey && res.presets[defaultKey]) { + onPresetSelect(defaultKey, res.presets[defaultKey]); + } + } + }) + .catch((err) => { + if (err instanceof Error && err.name === 'AbortError') return; + setError(err instanceof Error ? err.message : 'Failed to load presets'); + setLoading(false); + }); + + return () => controller.abort(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (loading) { + return

Loading presets…

; + } + + if (error) { + return ( +
+

⚠ {error}

+
+ ); + } + + const entries = Object.entries(presets); + + if (entries.length === 0) { + return

No presets available.

; + } + + return ( +
+

Preset

+
+ {entries.map(([key, config]) => ( + + ))} +
+
+ ); +} diff --git a/app/src/views/ImportConvert.module.css b/app/src/views/ImportConvert.module.css new file mode 100644 index 0000000..7b828ac --- /dev/null +++ b/app/src/views/ImportConvert.module.css @@ -0,0 +1,48 @@ +/* Import & Convert (View 1) layout */ + +.container { + display: flex; + gap: 24px; + padding: 24px; + min-height: calc(100svh - 80px); + box-sizing: border-box; +} + +.leftPanel { + flex: 0 0 340px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.rightPanel { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border: 1px solid var(--border); + border-radius: 8px; + min-height: 400px; + background: var(--code-bg); + padding: 16px; +} + +.previewPlaceholder { + color: var(--text); + font-size: 15px; + text-align: center; + opacity: 0.7; +} + +@media (max-width: 768px) { + .container { + flex-direction: column; + padding: 16px; + } + + .leftPanel { + flex: none; + width: 100%; + } +} diff --git a/app/src/views/ImportConvert.tsx b/app/src/views/ImportConvert.tsx new file mode 100644 index 0000000..2025f0a --- /dev/null +++ b/app/src/views/ImportConvert.tsx @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import type { PresetConfig } from '../types/engine'; +import FileUpload from '../components/FileUpload'; +import PresetSelector from '../components/PresetSelector'; +import styles from './ImportConvert.module.css'; + +interface ImportConvertProps { + onUseThis: (svgOutput: string, metadata: unknown) => void; +} + +export default function ImportConvert({ onUseThis: _onUseThis }: ImportConvertProps) { + const [selectedFile, setSelectedFile] = useState(null); + const [_isSvgMode, setIsSvgMode] = useState(false); + const [selectedPreset, setSelectedPreset] = useState('sign'); + const [_presetConfig, setPresetConfig] = useState(null); + + const handleFileSelect = (file: File, isSvg: boolean) => { + setSelectedFile(file); + setIsSvgMode(isSvg); + }; + + const handlePresetSelect = (name: string, config: PresetConfig) => { + setSelectedPreset(name); + setPresetConfig(config); + }; + + return ( +
+
+ + +
+
+
+ {selectedFile + ? 'Preview will appear here after tracing (T03)' + : 'Upload an image to begin vectorization'} +
+
+
+ ); +}