diff --git a/.gsd/DECISIONS.md b/.gsd/DECISIONS.md
index d95a2d6..eed3225 100644
--- a/.gsd/DECISIONS.md
+++ b/.gsd/DECISIONS.md
@@ -11,3 +11,4 @@
| D003 | | engine | Post-processing replaces regex metadata extraction | postprocess_svg() pipeline replaces old _extract_svg_metadata() regex approach; all SVG paths are parsed into structured PathInfo objects with coordinates, area, island status | Structured path data is needed for DXF/JSON output generation, RDP simplification, and island detection. XML parsing is now necessary since we need actual coordinate data, not just counts. This supersedes D003's regex approach. | No | agent |
| D004 | | architecture | Engine Docker image build strategy | Multi-stage build: builder stage compiles pypotrace with build-essential/libagg-dev/libpotrace-dev, runtime stage uses python:3.11-slim with only runtime libs (libpotrace0, libagg2, curl). Only engine source is copied — no App code. | Multi-stage keeps the runtime image small by excluding build tools. Copying only engine source enforces the Engine/App separation (D001). curl is included for Docker HEALTHCHECK. | Yes | agent |
| D005 | | architecture | Frontend technology stack for Kerf App | Vite + React + TypeScript with plain CSS modules, Vitest for testing, raw fetch + FormData for engine API calls, no UI framework for V1 | Minimal dependency surface for a tool that needs fast iteration. The engine API uses multipart/form-data which works naturally with native fetch + FormData. CSS modules avoid global style conflicts without adding a framework. Vitest integrates natively with Vite's transform pipeline. No UI library needed — the app has a focused interface with ~8 components. | Yes | agent |
+| D006 | | frontend | React hook params stabilization strategy for useDebouncedTrace | JSON.stringify params in useEffect deps array, single-effect architecture | useCallback-based triggerTrace with object params caused infinite re-render loops because React creates new object references each render. JSON.stringify converts params to a stable string for dependency comparison, eliminating the need for callers to useMemo their params objects. Simpler API surface and no re-render bugs. | Yes | agent |
diff --git a/.gsd/KNOWLEDGE.md b/.gsd/KNOWLEDGE.md
index 81a67cb..5a88b72 100644
--- a/.gsd/KNOWLEDGE.md
+++ b/.gsd/KNOWLEDGE.md
@@ -17,6 +17,9 @@ Agents read this before every unit. Add entries when you discover something wort
| P003 | PostProcessResult is the universal intermediate representation | engine/pipeline/postprocess.py → engine/output/ | All output generators (DXF, JSON, SVG) consume PostProcessResult. Never generate output directly from raw SVG strings. |
| P004 | _format_response() for consistent multi-format API responses | engine/api/routes.py | DXF returns binary Response with X-Kerf-Metadata header; SVG/JSON return JSON envelopes. Use _format_response() for both /engine/trace and /engine/simplify. |
| P005 | Preset-driven pipeline: resolve_params() merges preset → user | engine/presets/loader.py, engine/api/routes.py | Presets define defaults for all pipeline stages. User params override. Mode comes from preset unless user explicitly sets it. |
+| P006 | JSON.stringify for React hook dependency stabilization | app/src/hooks/useDebouncedTrace.ts | Object params in useEffect deps cause infinite loops (new ref each render). Stringify the params object and use the string in the deps array. Avoids requiring callers to useMemo. |
+| P007 | Vite dev proxy for engine API calls | app/vite.config.ts | `/engine/*` requests proxy to `http://localhost:8000`. API client uses relative URLs (`/engine/presets`). No CORS issues in dev because same-origin via proxy. |
+| P008 | CSS modules for views, global App.css for shared component styles | app/src/views/*.module.css, app/src/App.css | View-specific layout in CSS modules avoids conflicts. Shared styles (upload zone, preset cards, buttons) in App.css for cross-component reuse. |
## Lessons Learned
@@ -30,3 +33,5 @@ Agents read this before every unit. Add entries when you discover something wort
| L006 | postprocess_svg() fully parses SVG paths into coordinates | XML parsing + path d-attribute parsing gives structured PathInfo objects | This replaces the old regex-based SVG metadata extraction (D003). All output generators (DXF, JSON, SVG) consume PostProcessResult. | engine pipeline |
| L007 | Docker build context must be project root, not engine/ | Dockerfile.engine uses `COPY engine/...` paths relative to project root | Always build with `docker build -f docker/Dockerfile.engine -t kerf-engine:dev .` from project root | Docker |
| L008 | Engine container has dual health endpoints | /health on root (main.py) and /engine/health on router (routes.py) both exist | Use /engine/health for Docker HEALTHCHECK and external probes for namespace consistency | engine API, Docker |
+| L009 | useCallback-based triggerTrace caused infinite re-render loops | Object params create new references each render, retriggering useEffect even when values are unchanged | Restructure hook to use single useEffect with JSON.stringify(params) in dependency array instead of useCallback-based approach | app hooks, React |
+| L010 | jest-canvas-mock crashes in Vitest with "ReferenceError: jest is not defined" | jest-canvas-mock internally calls jest.fn() in createImageBitmap.js — Jest globals don't exist in Vitest | Use vitest-canvas-mock (a Vitest-compatible fork) instead. Import it in test-setup.ts. | app tests, canvas |
diff --git a/.gsd/event-log.jsonl b/.gsd/event-log.jsonl
index 9d3064b..32ddb1c 100644
--- a/.gsd/event-log.jsonl
+++ b/.gsd/event-log.jsonl
@@ -17,3 +17,6 @@
{"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"}
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T03"},"ts":"2026-03-26T05:15:38.849Z","actor":"agent","hash":"51de22a58ca5b075","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T04"},"ts":"2026-03-26T05:17:44.460Z","actor":"agent","hash":"fd1cf932b3152ba6","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
+{"cmd":"complete-slice","params":{"milestoneId":"M002","sliceId":"S01"},"ts":"2026-03-26T05:20:14.729Z","actor":"agent","hash":"4e07aca5d7cb85a5","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
+{"cmd":"plan-slice","params":{"milestoneId":"M002","sliceId":"S02"},"ts":"2026-03-26T05:26:15.488Z","actor":"agent","hash":"b1dbe18979c01969","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
+{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S02","taskId":"T01"},"ts":"2026-03-26T05:31:55.544Z","actor":"agent","hash":"4c3809e0b1681b4c","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
diff --git a/.gsd/milestones/M002/M002-ROADMAP.md b/.gsd/milestones/M002/M002-ROADMAP.md
index 278a2b6..450f4ed 100644
--- a/.gsd/milestones/M002/M002-ROADMAP.md
+++ b/.gsd/milestones/M002/M002-ROADMAP.md
@@ -6,6 +6,6 @@ Build the React frontend with the Import & Convert view (View 1) and the Design
## Slice Overview
| ID | Slice | Risk | Depends | Done | After this |
|----|-------|------|---------|------|------------|
-| S01 | Import & Convert UI (View 1) | medium — debounced preview updates, engine api integration from browser | — | ⬜ | Upload a PNG, see live preview with preset selection and slider controls, click 'Use This' to advance to canvas |
+| S01 | Import & Convert UI (View 1) | medium — debounced preview updates, engine api integration from browser | — | ✅ | Upload a PNG, see live preview with preset selection and slider controls, click 'Use This' to advance to canvas |
| S02 | Design Canvas Core (View 2) | medium — konva.js setup, selection handles, undo/redo state management | S01 | ⬜ | Create artboards in each shape, add/move/resize shapes, multi-select with alignment, undo/redo through history |
| S03 | Text System + Font Loading | medium — opentype.js integration, font loading from volume, path extraction accuracy | S02 | ⬜ | Add text objects, select fonts from picker, change size/spacing, convert text to paths — paths match original glyphs |
diff --git a/.gsd/milestones/M002/slices/S01/S01-SUMMARY.md b/.gsd/milestones/M002/slices/S01/S01-SUMMARY.md
new file mode 100644
index 0000000..537d735
--- /dev/null
+++ b/.gsd/milestones/M002/slices/S01/S01-SUMMARY.md
@@ -0,0 +1,135 @@
+---
+id: S01
+parent: M002
+milestone: M002
+provides:
+ - Typed API client (getPresets, traceImage, simplifyVector) with AbortSignal support
+ - ViewState routing in App.tsx — S02 adds 'canvas' view rendering
+ - onUseThis callback passes SVG string + TraceMetadata from View 1 to View 2
+ - useDebouncedTrace hook pattern reusable for any debounced API interaction
+ - TypeScript interfaces for all engine API types (PresetConfig, TraceResponse, TraceMetadata)
+requires:
+ []
+affects:
+ - S02
+key_files:
+ - app/src/api/engine.ts
+ - app/src/types/engine.ts
+ - app/src/hooks/useDebouncedTrace.ts
+ - app/src/views/ImportConvert.tsx
+ - app/src/components/FileUpload.tsx
+ - app/src/components/PresetSelector.tsx
+ - app/src/components/ParameterSliders.tsx
+ - app/src/components/SvgPreview.tsx
+ - app/src/components/OutputInfoBar.tsx
+ - app/src/App.tsx
+ - app/vite.config.ts
+ - engine/main.py
+key_decisions:
+ - D005: Vite + React + TS with plain CSS modules, Vitest, raw fetch — minimal dependency surface
+ - D006: JSON.stringify params stabilization in useDebouncedTrace to avoid infinite re-render loops
+ - CORS allows all origins for dev with comment to restrict in production
+ - Use This button always visible but disabled when no SVG — avoids layout shift
+patterns_established:
+ - P006: JSON.stringify for React hook dependency stabilization of object params
+ - P007: Vite dev proxy for engine API — relative URLs, no CORS issues in dev
+ - P008: CSS modules for views, global App.css for shared component styles
+ - ViewState routing via useState in App.tsx — simple state machine (import → canvas → export)
+ - Mode-aware UI: components inspect preset vectorization mode to show/hide relevant controls
+observability_surfaces:
+ - OutputInfoBar color coding: green=normal, yellow=high node count (>5000), red=open paths (>0)
+ - TraceMetadata.warnings array surfaced in OutputInfoBar when present
+ - Processing time displayed in stats bar
+drill_down_paths:
+ - .gsd/milestones/M002/slices/S01/tasks/T01-SUMMARY.md
+ - .gsd/milestones/M002/slices/S01/tasks/T02-SUMMARY.md
+ - .gsd/milestones/M002/slices/S01/tasks/T03-SUMMARY.md
+ - .gsd/milestones/M002/slices/S01/tasks/T04-SUMMARY.md
+duration: ""
+verification_result: passed
+completed_at: 2026-03-26T05:20:14.695Z
+blocker_discovered: false
+---
+
+# S01: Import & Convert UI (View 1)
+
+**Built the complete Import & Convert view (View 1) — file upload, preset selection, debounced live vectorization preview with parameter sliders, output stats bar, and Use This button to advance to canvas.**
+
+## What Happened
+
+This slice delivered the full View 1 of the Kerf App: a React SPA that integrates with the Engine API for interactive vectorization.
+
+**T01 — Foundation:** Added CORSMiddleware to engine/main.py for browser API access. Scaffolded the app/ directory as a Vite + React + TypeScript project with a dev proxy forwarding /engine/* requests to localhost:8000. Built typed API client (getPresets, traceImage, simplifyVector) with AbortSignal support and error handling. Established the test infrastructure with Vitest + jsdom + testing-library. 9 tests pass.
+
+**T02 — UI Shell:** Created the two-column ImportConvert layout (left: controls, right: preview). Built FileUpload component with drag-and-drop + file input fallback, thumbnail preview via URL.createObjectURL, and SVG file detection for mode switching. Built PresetSelector that fetches presets from the engine API on mount with AbortController cleanup, renders preset cards with selection highlighting, and auto-selects 'sign' as default. Set up ViewState routing in App.tsx (import → canvas → export).
+
+**T03 — Core Interaction Loop (highest risk):** Created the useDebouncedTrace custom hook — the central piece — which debounces API calls on params/preset/file change, aborts in-flight requests via AbortController, and routes SVG uploads to simplifyVector() instead of traceImage(). Solved infinite re-render loops by stabilizing params via JSON.stringify in the useEffect dependency array. Built ParameterSliders with mode-aware controls (potrace shows epsilon/turdsize/alphamax; vtracer shows epsilon/filter_speckle/corner_threshold). Built SvgPreview with responsive SVG rendering (strips width/height, preserves viewBox), loading spinner, and error states. Wired the complete live re-trace loop in ImportConvert. 7 hook tests pass.
+
+**T04 — Polish & Completion:** Created OutputInfoBar with color-coded stats (green normal, yellow >5000 nodes, red open paths), warnings display, and processing time. Wired the Use This button (disabled when no SVG output) that passes SVG + metadata to App.tsx for View 2 consumption. 7 component tests pass.
+
+All 23 tests pass. Zero TypeScript errors.
+
+## Verification
+
+All slice-level verification checks pass:
+- `cd app && npx vitest run --reporter=verbose`: 23/23 tests pass across 3 test files (9 API client, 7 OutputInfoBar, 7 useDebouncedTrace)
+- `cd app && npx tsc --noEmit`: zero TypeScript errors
+- All planned files exist: API client, types, 6 components, 1 custom hook, 2 views, CSS modules, 3 test files
+- CORS middleware confirmed in engine/main.py
+- Vite dev proxy confirmed in vite.config.ts
+
+## Requirements Advanced
+
+None.
+
+## Requirements Validated
+
+None.
+
+## New Requirements Surfaced
+
+None.
+
+## Requirements Invalidated or Re-scoped
+
+None.
+
+## Deviations
+
+Minor deviations, none plan-breaking:
+- T01: Added @types/node and test-setup.ts (not in plan) for proper type resolution and jest-dom matchers
+- T03: Use This button was wired in T03 instead of T04 — T04 retained it and added disabled state logic
+- T03: Hook architecture changed from useCallback-based trigger to single-useEffect with JSON.stringify params stabilization to avoid infinite re-render loops
+
+## Known Limitations
+
+- CORS allows all origins (appropriate for dev, needs restriction for production)
+- SVG preview uses dangerouslySetInnerHTML — acceptable for engine-generated SVG but should not be used for untrusted SVG in production
+- No end-to-end integration test with a running engine (unit tests mock fetch)
+
+## Follow-ups
+
+- S02 consumes the SVG string and TraceMetadata passed via onUseThis callback in App.tsx
+- Production CORS configuration should be added when deployment is set up (M003)
+- Consider adding SVG sanitization if untrusted input becomes possible
+
+## Files Created/Modified
+
+- `engine/main.py` — Added CORSMiddleware allowing all origins for dev
+- `app/vite.config.ts` — Vite config with React plugin, dev proxy to engine, Vitest config
+- `app/src/types/engine.ts` — TypeScript interfaces for PresetConfig, TraceResponse, TraceMetadata, PresetsResponse
+- `app/src/api/engine.ts` — Typed API client: getPresets(), traceImage(), simplifyVector() with AbortSignal
+- `app/src/api/__tests__/engine.test.ts` — 9 unit tests for API client covering URL/method/FormData/AbortSignal/errors
+- `app/src/hooks/useDebouncedTrace.ts` — Custom hook: debounced trace with AbortController, SVG mode detection, params stabilization
+- `app/src/hooks/__tests__/useDebouncedTrace.test.ts` — 7 tests for debounce, abort, SVG routing, error handling, cleanup
+- `app/src/views/ImportConvert.tsx` — View 1 container: two-column layout wiring FileUpload, PresetSelector, ParameterSliders, SvgPreview, OutputInfoBar, Use This button
+- `app/src/views/ImportConvert.module.css` — CSS module for ImportConvert layout
+- `app/src/components/FileUpload.tsx` — Drag-and-drop file upload with thumbnail preview and SVG detection
+- `app/src/components/PresetSelector.tsx` — Fetches presets from engine, renders cards with selection state, auto-selects sign
+- `app/src/components/ParameterSliders.tsx` — Mode-aware range sliders for vectorization parameters
+- `app/src/components/SvgPreview.tsx` — Responsive SVG rendering with loading/error states
+- `app/src/components/OutputInfoBar.tsx` — Color-coded stats bar with warnings display
+- `app/src/components/__tests__/OutputInfoBar.test.tsx` — 7 tests for color coding logic and edge cases
+- `app/src/App.tsx` — ViewState routing, SVG/metadata state for View 2 handoff
+- `app/src/App.css` — Global styles for shared components
+- `app/src/test-setup.ts` — Vitest setup with jest-dom matchers
diff --git a/.gsd/milestones/M002/slices/S01/S01-UAT.md b/.gsd/milestones/M002/slices/S01/S01-UAT.md
new file mode 100644
index 0000000..1477f2e
--- /dev/null
+++ b/.gsd/milestones/M002/slices/S01/S01-UAT.md
@@ -0,0 +1,114 @@
+# S01: Import & Convert UI (View 1) — UAT
+
+**Milestone:** M002
+**Written:** 2026-03-26T05:20:14.695Z
+
+## UAT: S01 — Import & Convert UI (View 1)
+
+### Preconditions
+- Engine running at `http://localhost:8000` with presets loaded (`GET /engine/presets` returns preset configs)
+- App dev server running via `cd app && npm run dev` (Vite at localhost:5173)
+- At least one test raster image available (PNG, JPG, or BMP)
+- At least one test SVG file available
+
+---
+
+### Test 1: File Upload — Drag and Drop
+1. Open `http://localhost:5173` in browser
+2. Verify the Import & Convert view loads with a dashed-border upload zone on the left panel and empty preview area on the right
+3. Drag a PNG file onto the upload zone
+4. **Expected:** Upload zone shows the filename, file size, and a thumbnail preview of the image
+5. Verify the thumbnail is visible and roughly matches the uploaded image
+
+### Test 2: File Upload — Click Fallback
+1. Click on the upload zone (not drag-and-drop)
+2. **Expected:** A native file picker dialog opens
+3. Select a JPG file
+4. **Expected:** Same result as Test 1 — filename, size, and thumbnail displayed
+
+### Test 3: File Upload — SVG Detection
+1. Upload an SVG file (via drag or click)
+2. **Expected:** File is accepted and thumbnail/filename shown
+3. Observe that when trace fires, it calls `/engine/simplify` (not `/engine/trace`) — verify in browser devtools Network tab
+
+### Test 4: Preset Selection
+1. Upload a raster image (PNG)
+2. Verify preset cards appear below the upload zone, fetched from the engine API
+3. **Expected:** 'sign' preset is selected by default (highlighted)
+4. Click a different preset card (e.g., 'technical' or 'photo')
+5. **Expected:** The clicked preset becomes highlighted, the previous one unhighlights
+6. **Expected:** The SVG preview updates automatically after a short debounce delay
+
+### Test 5: Live Debounced Re-Trace
+1. Upload a raster PNG with the 'sign' preset selected
+2. Wait for the initial SVG preview to appear in the right panel
+3. **Expected:** Preview shows a vectorized SVG version of the uploaded image
+4. Move the "Detail Level" slider
+5. **Expected:** After ~300ms debounce, the preview updates with the new vectorization result
+6. Rapidly move sliders 3-4 times in quick succession
+7. **Expected:** Only the final parameter set triggers an API call (debounce coalesces rapid changes)
+
+### Test 6: Parameter Sliders — Potrace Mode
+1. Select a preset that uses potrace mode (e.g., 'sign')
+2. **Expected:** Sliders shown: Detail Level (epsilon), Noise Filter (turdsize), Smooth Curves (alphamax)
+3. **Expected:** Corner Threshold slider is NOT shown (vtracer-only)
+4. Each slider displays its current numeric value
+
+### Test 7: Parameter Sliders — VTracer Mode
+1. Select a preset that uses vtracer mode (if available)
+2. **Expected:** Sliders shown: Detail Level (epsilon), Noise Filter (filter_speckle), Corner Threshold (corner_threshold)
+3. **Expected:** Smooth Curves (alphamax) slider is NOT shown (potrace-only)
+
+### Test 8: SVG Preview — Responsive Rendering
+1. Upload an image and wait for SVG preview
+2. Resize the browser window
+3. **Expected:** SVG preview scales responsively (maintains aspect ratio, fills available width, max-height 70vh)
+4. **Expected:** No horizontal scrollbars or overflow
+
+### Test 9: Output Info Bar — Normal Stats
+1. Upload an image, wait for vectorization to complete
+2. **Expected:** Stats bar appears below the preview showing: Path Count, Total Nodes, Open Paths, Processing Time
+3. **Expected:** All indicators are green for typical images with moderate complexity
+
+### Test 10: Output Info Bar — Warning Indicators
+1. Upload a complex/detailed image that produces >5000 total nodes
+2. **Expected:** Total Nodes indicator turns yellow
+3. If the result has open paths > 0: **Expected:** Open Paths indicator turns red
+4. If engine returns warnings: **Expected:** Warnings are displayed in the info bar
+
+### Test 11: Use This Button — Disabled State
+1. Load the page fresh (no file uploaded)
+2. **Expected:** "Use This" button is visible but disabled/grayed out
+3. Upload a file but before the trace completes:
+4. **Expected:** Button remains disabled while loading
+
+### Test 12: Use This Button — Advances to Canvas
+1. Upload a file, wait for SVG preview to appear
+2. **Expected:** "Use This" button becomes enabled
+3. Click the "Use This" button
+4. **Expected:** View transitions from Import & Convert to the Design Canvas placeholder (shows "View 2: Design Canvas")
+
+### Test 13: Error Handling — API Failure
+1. Stop the engine server
+2. Upload a file
+3. **Expected:** Preview area shows an error message (not a crash or blank screen)
+4. Restart the engine
+5. Re-upload or change a slider
+6. **Expected:** Preview recovers and shows the vectorized result
+
+### Test 14: AbortController Cleanup
+1. Upload a file and immediately upload a different file before the first trace completes
+2. **Expected:** First request is aborted (visible in devtools Network tab as cancelled), second request completes normally
+3. Only the second file's result is shown in the preview
+
+### Edge Cases
+
+### Test 15: Large File Upload
+1. Upload a large image (>5MB)
+2. **Expected:** Upload completes, thumbnail shown, vectorization runs (may take longer than usual)
+3. No UI freezing during processing
+
+### Test 16: Rapid Preset Switching
+1. Upload a file, then rapidly click through 3-4 different presets
+2. **Expected:** Intermediate requests are aborted, only the final preset's result is displayed
+3. No stale results from earlier presets appear after the final result loads
diff --git a/.gsd/milestones/M002/slices/S01/tasks/T04-VERIFY.json b/.gsd/milestones/M002/slices/S01/tasks/T04-VERIFY.json
new file mode 100644
index 0000000..e44f29c
--- /dev/null
+++ b/.gsd/milestones/M002/slices/S01/tasks/T04-VERIFY.json
@@ -0,0 +1,16 @@
+{
+ "schemaVersion": 1,
+ "taskId": "T04",
+ "unitId": "M002/S01/T04",
+ "timestamp": 1774502268928,
+ "passed": true,
+ "discoverySource": "task-plan",
+ "checks": [
+ {
+ "command": "cd app",
+ "exitCode": 0,
+ "durationMs": 5,
+ "verdict": "pass"
+ }
+ ]
+}
diff --git a/.gsd/milestones/M002/slices/S02/S02-PLAN.md b/.gsd/milestones/M002/slices/S02/S02-PLAN.md
index 2c1c2c3..00b327a 100644
--- a/.gsd/milestones/M002/slices/S02/S02-PLAN.md
+++ b/.gsd/milestones/M002/slices/S02/S02-PLAN.md
@@ -1,6 +1,90 @@
# S02: Design Canvas Core (View 2)
-**Goal:** Build View 2 Design Canvas with Konva.js — artboard setup, basic shapes, object panel, toolbar, alignment tools, undo/redo
+**Goal:** Build the Konva.js-powered 2D design canvas (View 2) with artboard shapes, basic shapes (rect/circle/ellipse/line), imported SVG rendering, selection handles, multi-select, object/layer panel with reorder, alignment tools, and undo/redo.
**Demo:** After this: Create artboards in each shape, add/move/resize shapes, multi-select with alignment, undo/redo through history
## Tasks
+- [x] **T01: Built canvas type system, useCanvasState hook with undo/redo, artboard shapes, alignment utils, ArtboardSetup component, and 48 new tests passing** — Establish the foundation that every other task depends on: canvas type definitions (discriminated union for canvas objects, artboard config, canvas state), the central useCanvasState hook with full CRUD + selection + undo/redo, artboard shape utilities, the ArtboardSetup component, and canvas mock setup for Vitest. This is the riskiest piece — the state model shape affects every downstream component.
+
+The useCanvasState hook is a useReducer + useRef pattern: current state in a reducer (triggers re-renders), history stack in a useRef (avoids re-renders on history push). Every mutation dispatches through the reducer which also pushes a history snapshot. Undo/redo pops from history.
+
+The ArtboardSetup component renders a modal overlay on first entering the canvas view with shape picker (rect, square, circle, oval, shield, pennant, custom), width/height number inputs, and inches/mm units toggle. On confirm, it produces an ArtboardConfig that initializes the canvas.
+
+Install konva, react-konva, and jest-canvas-mock as dependencies. Set up canvas mock in test-setup.ts so Konva components don't crash in jsdom.
+ - Estimate: 2h
+ - Files: app/src/types/canvas.ts, app/src/hooks/useCanvasState.ts, app/src/hooks/__tests__/useCanvasState.test.ts, app/src/utils/artboardShapes.ts, app/src/utils/__tests__/artboardShapes.test.ts, app/src/utils/alignment.ts, app/src/utils/__tests__/alignment.test.ts, app/src/components/canvas/ArtboardSetup.tsx, app/src/test-setup.ts, app/package.json
+ - Verify: cd app && npx vitest run --reporter=verbose && npx tsc --noEmit
+- [ ] **T02: Konva stage with artboard rendering, imported SVG, selection handles, and shape tools** — Build the core Konva canvas rendering layer and wire it into the app. This task creates the KonvaStage component (Konva Stage + Layer with artboard background, all canvas objects, Transformer for selection handles, rubber-band selection rectangle), the DesignCanvas view container, and shape creation tools.
+
+Key implementation details:
+- DesignCanvas is the View 2 container. Layout: top toolbar area, left canvas with KonvaStage, right panel area (wired in T03). Receives svgData and traceMetadata props from App.tsx.
+- Wire DesignCanvas into App.tsx replacing the placeholder div. Remove underscore prefixes from _svgResult and _traceMetadata variables.
+- KonvaStage renders: (1) artboard background shape using Konva.Rect/Path/Circle based on ArtboardConfig, (2) all canvas objects from useCanvasState mapped to Konva components, (3) Transformer attached to selected nodes, (4) rubber-band selection rectangle for multi-select.
+- Imported SVG from View 1: convert SVG string → Blob URL → Image element → Konva.Image. Auto-add to canvas objects on mount if svgData is provided.
+- Shape tools: rect, circle, ellipse, line creation. Click canvas in tool mode to place shape at click position with default dimensions. Tool state tracked in DesignCanvas.
+- Selection: click shape to select (single), shift-click to add to selection, click empty area to deselect, rubber-band rectangle selects all intersecting shapes.
+- Transformer: on onTransformEnd, reset scaleX/scaleY to 1 and multiply into width/height to keep state model clean.
+- Line objects support dash arrays for style variants (solid, dashed, dotted).
+- Canvas view should fill available width. Override #root width constraint for canvas view or make canvas flex within it.
+
+IMPORTANT tsconfig constraints: noUnusedLocals and noUnusedParameters are strict. All variables must be used or prefixed with underscore.
+ - Estimate: 2h30m
+ - Files: app/src/views/DesignCanvas.tsx, app/src/views/DesignCanvas.module.css, app/src/components/canvas/KonvaStage.tsx, app/src/App.tsx, app/src/App.css
+ - Verify: cd app && npx tsc --noEmit && npx vitest run --reporter=verbose
+- [ ] **T03: Object panel, alignment bar, canvas toolbar, and shape properties panel** — Build the UI panels that surround the canvas: the right-side ObjectPanel for layer management, the AlignmentBar for spatial arrangement, the CanvasToolbar for tool switching and canvas controls, and the ShapeProperties panel for editing selected objects.
+
+ObjectPanel:
+- Lists canvas objects by z-order (top of list = frontmost layer)
+- Each row shows: drag handle, object type icon, name (double-click to rename), visibility toggle (eye icon), lock toggle (lock icon)
+- Click row to select object on canvas, shift-click for multi-select
+- Drag-to-reorder changes z-order in state (which changes render order in KonvaStage)
+- Wire to useCanvasState: reorderObject, toggleVisibility, toggleLock, selectObjects
+
+AlignmentBar:
+- Appears when 1+ objects selected
+- Buttons: align left, align center, align right, align top, align middle, align bottom
+- When 2+ selected: distribute horizontally, distribute vertically
+- Center on artboard button (works with 1+ selected)
+- Consumes alignment utility functions from utils/alignment.ts
+- Dispatches batch updateObject calls through useCanvasState
+
+CanvasToolbar:
+- Tool buttons: pointer/select (default), rectangle, circle, ellipse, line
+- Active tool highlighted with accent color
+- Undo/redo buttons (disabled when stack empty) — connected to useCanvasState undo/redo
+- Grid toggle button
+- Zoom controls (zoom in, zoom out, fit to artboard)
+
+ShapeProperties:
+- Shown when exactly 1 shape selected
+- Fields: stroke color (color input), stroke weight (number input), fill color (color input with toggle), width/height display
+- For line objects: line style dropdown (solid, dashed, dotted)
+- Changes dispatch updateObject through useCanvasState
+
+Wire all panels into DesignCanvas.tsx layout slots.
+ - Estimate: 2h
+ - Files: app/src/components/canvas/ObjectPanel.tsx, app/src/components/canvas/AlignmentBar.tsx, app/src/components/canvas/CanvasToolbar.tsx, app/src/components/canvas/ShapeProperties.tsx, app/src/views/DesignCanvas.tsx, app/src/views/DesignCanvas.module.css, app/src/App.css
+ - Verify: cd app && npx tsc --noEmit && npx vitest run --reporter=verbose
+- [ ] **T04: Keyboard shortcuts, final integration wiring, and verification** — Wire keyboard shortcuts for the canvas, perform final integration checks, and ensure all tests pass.
+
+Keyboard shortcuts:
+- Ctrl+Z: undo (calls useCanvasState.undo)
+- Ctrl+Shift+Z (or Ctrl+Y): redo (calls useCanvasState.redo)
+- Delete/Backspace: remove selected objects (calls useCanvasState.removeObject for each selected ID)
+- Escape: deselect all (calls useCanvasState.deselectAll)
+- Ctrl+A: select all objects
+
+Implement via useEffect with keydown listener on the DesignCanvas container div (or window). Ensure shortcuts only fire when canvas view is active and no text input is focused (check activeElement).
+
+Final integration:
+- Verify the full flow: View 1 → Use This → Artboard Setup → Canvas with imported SVG
+- Verify undo/redo buttons in toolbar reflect stack state (disabled when empty)
+- Verify all panel components render and dispatch correctly
+- Run full test suite: all existing 23 tests + all new tests must pass
+- Run TypeScript compiler: zero errors
+- Ensure no unused imports/variables (strict tsconfig)
+
+This task closes the slice by ensuring everything works together as an integrated whole.
+ - Estimate: 1h
+ - Files: app/src/views/DesignCanvas.tsx, app/src/App.css
+ - Verify: cd app && npx vitest run --reporter=verbose && npx tsc --noEmit
diff --git a/.gsd/milestones/M002/slices/S02/S02-RESEARCH.md b/.gsd/milestones/M002/slices/S02/S02-RESEARCH.md
new file mode 100644
index 0000000..9a4926d
--- /dev/null
+++ b/.gsd/milestones/M002/slices/S02/S02-RESEARCH.md
@@ -0,0 +1,124 @@
+# S02 — Design Canvas Core (View 2) — Research
+
+**Date:** 2026-03-26
+
+## Summary
+
+S02 builds View 2, the Konva.js-powered 2D design canvas. This is a moderately complex slice with well-documented technology (react-konva has excellent examples for every feature needed), a clear handoff from S01 (SVG string + TraceMetadata in App.tsx state), and five requirements to satisfy: R013 (artboard shapes), R015 (basic shapes with selection/resize), R016 (object/layer panel), R017 (alignment tools), R018 (undo/redo).
+
+The core risk is state management complexity — the canvas needs a central store of typed objects with undo/redo across all mutations (add, move, resize, reorder, delete, property changes). react-konva's `Transformer` component handles selection handles and resize/rotate natively; multi-select uses `tr.nodes([...])`. The official Konva docs provide React examples for every interaction pattern needed (Transformer, multi-select with rubber-band, drag-and-drop reordering, undo/redo via history stack, dash patterns for line styles, z-index layering). No custom rendering engine needed.
+
+The imported SVG from View 1 should be rendered via `Konva.Image` from a Blob URL — this is simpler and more reliable than parsing individual SVG paths, preserves visual fidelity, and keeps the vector as a single moveable/scaleable/rotatable object on canvas. Text objects are deferred to S03. Shield and pennant artboard shapes are custom SVG path outlines rendered as Konva.Path clip masks.
+
+## Recommendation
+
+**Build in this order: state model → artboard → Konva stage with imported SVG → shape tools + selection → object panel → alignment → undo/redo.**
+
+Use a single `useCanvasState` hook (or a context + reducer) as the central state store. All canvas objects live in an array with typed discriminated unions (`type: 'rect' | 'circle' | 'ellipse' | 'line' | 'imported-svg'`). Every mutation goes through a dispatch function that records a snapshot for undo/redo. The Konva `Stage` renders objects from this store; `Transformer` handles interactive resize/rotate. The object panel reads and reorders the same store.
+
+Install `konva` and `react-konva` as dependencies. No other new libraries needed — alignment math and undo/redo are simple enough to implement inline. Konva.js handles all 2D rendering, hit detection, drag, and transform.
+
+## Implementation Landscape
+
+### Key Files (existing)
+
+- `app/src/App.tsx` — ViewState routing (`import | canvas | export`). Currently stores `_svgResult` (string) and `_traceMetadata` (TraceMetadata) via `handleUseThis`. The `view === 'canvas'` branch renders a placeholder div that S02 replaces with ``.
+- `app/src/types/engine.ts` — TraceMetadata interface (consumed by canvas for metadata display).
+- `app/src/App.css` — Global component styles. S02 adds canvas toolbar, object panel, and artboard setup styles here.
+- `app/src/index.css` — CSS variables (--accent, --border, --bg, --text-h, etc.) and base typography. Canvas UI must use these.
+- `app/vite.config.ts` — Vite + React + Vitest config. No changes needed.
+- `app/src/test-setup.ts` — Vitest setup with jest-dom matchers. May need `canvas` mock setup for Konva in jsdom.
+
+### Key Files (new)
+
+- `app/src/types/canvas.ts` — Canvas object type definitions: `CanvasObject` discriminated union (`RectObject | CircleObject | EllipseObject | LineObject | ImportedSvgObject`), `ArtboardConfig` (shape + dimensions + units), `CanvasState` (objects array, selectedIds, artboard config, history stack).
+- `app/src/hooks/useCanvasState.ts` — Central state management hook. Manages: object CRUD, selection (single + multi), artboard config, undo/redo history. Returns state + dispatch functions. History stored as `useRef` with a step pointer (same pattern as Konva's official undo/redo example — `useRef` avoids unnecessary re-renders on history push).
+- `app/src/views/DesignCanvas.tsx` — View 2 container. Layout: top toolbar, left canvas area with Konva Stage, right object panel. Receives `svgData` and `metadata` props from App.tsx.
+- `app/src/views/DesignCanvas.module.css` — CSS module for canvas layout (follows P008 pattern).
+- `app/src/components/canvas/ArtboardSetup.tsx` — Modal/overlay shown on first entering canvas. Shape picker (rect, square, circle, oval, shield, pennant, custom), width/height inputs, units toggle (inches/mm). Produces `ArtboardConfig`.
+- `app/src/components/canvas/KonvaStage.tsx` — Wraps `` + ``. Renders artboard background shape, all canvas objects, selection `Transformer`, rubber-band selection rectangle. Handles click-to-select, shift-click multi-select, click-on-empty to deselect.
+- `app/src/components/canvas/CanvasToolbar.tsx` — Top toolbar: select tool, shape tools (rect, circle, ellipse, line), zoom controls, undo/redo buttons, grid toggle.
+- `app/src/components/canvas/ObjectPanel.tsx` — Right sidebar: lists objects by layer order (top = front), drag-to-reorder, visibility toggle (eye icon), lock toggle (lock icon), click-to-select, double-click-to-rename.
+- `app/src/components/canvas/AlignmentBar.tsx` — Appears when objects selected: align left/center/right/top/middle/bottom, distribute H/V, center on artboard.
+- `app/src/components/canvas/ShapeProperties.tsx` — Property panel for selected shape: stroke color, stroke weight, fill toggle, line style (for line objects), dimensions display.
+- `app/src/utils/artboardShapes.ts` — SVG path data for non-primitive artboard shapes (shield, pennant). Also artboard dimension presets. Pure functions, easily testable.
+- `app/src/utils/alignment.ts` — Alignment calculation functions: align, distribute, center-on-artboard. Pure math, easily testable.
+
+### Build Order
+
+**1. Foundation — types + state hook + artboard setup (highest priority, unblocks everything)**
+- Define `CanvasObject` types in `canvas.ts`
+- Build `useCanvasState` hook with add/remove/update/select/deselect/reorder + undo/redo
+- Build `ArtboardSetup` component for shape/dimension selection
+- This is the riskiest piece — state shape affects every downstream component. Get it right first.
+
+**2. Konva Stage + imported SVG rendering**
+- Install `konva` and `react-konva`
+- Build `KonvaStage` with artboard background rendering
+- Wire imported SVG from S01 as a `Konva.Image` (convert SVG string → Blob URL → Image)
+- Add selection via `Transformer`, single + multi-select, deselect on empty click
+- Wire `DesignCanvas` view into App.tsx (replace placeholder)
+
+**3. Shape tools + selection interaction**
+- Add shape creation tools (rect, circle, ellipse, line)
+- Wire toolbar buttons for tool switching
+- Implement rubber-band selection rectangle for multi-select
+- Line style variants (solid, dashed, dotted, double, centerline via dash arrays)
+
+**4. Object panel + alignment**
+- Build `ObjectPanel` with layer list, reorder, visibility, lock
+- Build `AlignmentBar` with align/distribute/center operations
+- Wire both to `useCanvasState`
+
+**5. Undo/redo + keyboard shortcuts**
+- Wire Ctrl+Z / Ctrl+Shift+Z to undo/redo
+- Undo/redo buttons in toolbar
+- Delete key removes selected objects
+
+### Verification Approach
+
+- `cd app && npx vitest run --reporter=verbose` — all tests pass (existing 23 + new)
+- `cd app && npx tsc --noEmit` — zero TypeScript errors
+- Unit tests for pure functions: `alignment.ts` (align calculations), `artboardShapes.ts` (path data generation), `useCanvasState` (add/remove/update/select/undo/redo state transitions)
+- Component tests: `ArtboardSetup` (shape selection produces correct config), `ObjectPanel` (renders object list, reorder dispatches correct action), `AlignmentBar` (button clicks dispatch alignment)
+- Konva rendering tests may require mocking `canvas` in jsdom — `jest-canvas-mock` or `vitest-canvas-mock` package, or test pure state logic separately from rendering
+- Manual visual verification: start dev server (`cd app && npm run dev`), navigate to canvas, create artboard, add shapes, verify selection handles, undo/redo
+
+## Don't Hand-Roll
+
+| Problem | Existing Solution | Why Use It |
+|---------|------------------|------------|
+| 2D canvas rendering + hit detection | `konva` + `react-konva` | Official React bindings for Konva.js. Handles all canvas rendering, event handling, drag, transform. Well-documented with React examples for every pattern we need. |
+| Selection handles (resize/rotate) | Konva `Transformer` component | Built-in to react-konva. Handles resize handles, rotation, multi-node selection, min-size constraints via `boundBoxFunc`. |
+| Multi-select rubber-band | Konva selection rectangle pattern | Official example uses `selectionRectangle.getClientRect()` + `Konva.Util.haveIntersection()` — exact pattern documented in Konva site. |
+
+## Constraints
+
+- **jsdom has no Canvas API** — Konva requires `