feat: Built app shell with ViewState routing, drag-and-drop FileUpload…

- "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
This commit is contained in:
jlightner 2026-03-26 05:07:37 +00:00
parent 9be90a4494
commit 16d336913f
11 changed files with 639 additions and 17 deletions

View file

@ -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"}

View file

@ -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

View file

@ -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"
}
]
}

View file

@ -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.

View file

@ -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 `<div>View 2: Design Canvas</div>` 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 `<input>` 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"
}
]
}

172
app/src/App.css Normal file
View file

@ -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);
}

View file

@ -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<ViewState>('import');
const [_svgResult, setSvgResult] = useState<string | null>(null);
const [_traceMetadata, setTraceMetadata] = useState<TraceMetadata | null>(null);
const handleUseThis = (svgOutput: string, metadata: unknown) => {
setSvgResult(svgOutput);
setTraceMetadata(metadata as TraceMetadata);
setView('canvas');
};
return (
<div id="app">
<h1>Kerf Engine</h1>
<p>Import &amp; Convert view coming in T02.</p>
{view === 'import' && <ImportConvert onUseThis={handleUseThis} />}
{view === 'canvas' && <div className="placeholder-view">View 2: Design Canvas</div>}
{view === 'export' && <div className="placeholder-view">View 3: Export</div>}
</div>
)
);
}
export default App
export default App;

View file

@ -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<HTMLInputElement>(null);
const [isDragOver, setIsDragOver] = useState(false);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(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<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) handleFile(file);
},
[handleFile],
);
return (
<div
className={`file-upload-zone ${isDragOver ? 'file-upload-zone--drag-over' : ''}`}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
onClick={() => inputRef.current?.click()}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') inputRef.current?.click();
}}
aria-label="Upload image file"
>
<input
ref={inputRef}
type="file"
accept={ACCEPTED_TYPES}
onChange={onInputChange}
style={{ display: 'none' }}
data-testid="file-input"
/>
{selectedFile ? (
<div className="file-upload-preview">
{thumbnailUrl && (
<img
src={thumbnailUrl}
alt="Uploaded preview"
className="file-upload-thumb"
/>
)}
<div className="file-upload-info">
<span className="file-upload-name">{selectedFile.name}</span>
<span className="file-upload-size">
{formatFileSize(selectedFile.size)}
</span>
{isSvgFile(selectedFile) && (
<span className="file-upload-badge">SVG simplify mode</span>
)}
</div>
</div>
) : (
<div className="file-upload-prompt">
<span className="file-upload-icon">📁</span>
<p>
<strong>Drop an image here</strong> or click to browse
</p>
<small>PNG, JPG, BMP, TIFF, WebP, or SVG</small>
</div>
)}
</div>
);
}

View file

@ -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<Record<string, PresetConfig>>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 <div className="preset-selector"><p className="preset-loading">Loading presets</p></div>;
}
if (error) {
return (
<div className="preset-selector">
<p className="preset-error"> {error}</p>
</div>
);
}
const entries = Object.entries(presets);
if (entries.length === 0) {
return <div className="preset-selector"><p>No presets available.</p></div>;
}
return (
<div className="preset-selector">
<h3 className="preset-heading">Preset</h3>
<div className="preset-grid">
{entries.map(([key, config]) => (
<button
key={key}
className={`preset-card ${selectedPreset === key ? 'preset-card--selected' : ''}`}
onClick={() => onPresetSelect(key, config)}
type="button"
>
<span className="preset-card-name">{config.name}</span>
<span className="preset-card-desc">{config.description}</span>
</button>
))}
</div>
</div>
);
}

View file

@ -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%;
}
}

View file

@ -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<File | null>(null);
const [_isSvgMode, setIsSvgMode] = useState(false);
const [selectedPreset, setSelectedPreset] = useState('sign');
const [_presetConfig, setPresetConfig] = useState<PresetConfig | null>(null);
const handleFileSelect = (file: File, isSvg: boolean) => {
setSelectedFile(file);
setIsSvgMode(isSvg);
};
const handlePresetSelect = (name: string, config: PresetConfig) => {
setSelectedPreset(name);
setPresetConfig(config);
};
return (
<div className={styles.container}>
<div className={styles.leftPanel}>
<FileUpload onFileSelect={handleFileSelect} selectedFile={selectedFile} />
<PresetSelector
selectedPreset={selectedPreset}
onPresetSelect={handlePresetSelect}
/>
</div>
<div className={styles.rightPanel}>
<div className={styles.previewPlaceholder}>
{selectedFile
? 'Preview will appear here after tracing (T03)'
: 'Upload an image to begin vectorization'}
</div>
</div>
</div>
);
}