feat: Lifted useCanvasState() from DesignCanvas to App.tsx, threaded al…

- "app/src/App.tsx"
- "app/src/views/DesignCanvas.tsx"

GSD-Task: S01/T02
This commit is contained in:
jlightner 2026-03-26 06:19:30 +00:00
parent 4fad89288e
commit 2bcc124542
7 changed files with 228 additions and 38 deletions

View file

@ -32,3 +32,4 @@
{"cmd":"complete-milestone","params":{"milestoneId":"M002"},"ts":"2026-03-26T06:06:46.769Z","actor":"agent","hash":"56704af548d63e18","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"complete-milestone","params":{"milestoneId":"M002"},"ts":"2026-03-26T06:06:46.769Z","actor":"agent","hash":"56704af548d63e18","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
{"cmd":"plan-slice","params":{"milestoneId":"M003","sliceId":"S01"},"ts":"2026-03-26T06:13:19.433Z","actor":"agent","hash":"0c1c14d2a8ee7643","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"plan-slice","params":{"milestoneId":"M003","sliceId":"S01"},"ts":"2026-03-26T06:13:19.433Z","actor":"agent","hash":"0c1c14d2a8ee7643","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
{"cmd":"complete-task","params":{"milestoneId":"M003","sliceId":"S01","taskId":"T01"},"ts":"2026-03-26T06:16:59.236Z","actor":"agent","hash":"f6bd52e1fbbe7e7f","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"complete-task","params":{"milestoneId":"M003","sliceId":"S01","taskId":"T01"},"ts":"2026-03-26T06:16:59.236Z","actor":"agent","hash":"f6bd52e1fbbe7e7f","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
{"cmd":"complete-task","params":{"milestoneId":"M003","sliceId":"S01","taskId":"T02"},"ts":"2026-03-26T06:19:28.695Z","actor":"agent","hash":"20e62f4b5af835c3","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}

View file

@ -8,7 +8,7 @@
- Estimate: 1h - Estimate: 1h
- Files: engine/output/dxf.py, engine/api/routes.py, engine/tests/test_output.py - Files: engine/output/dxf.py, engine/api/routes.py, engine/tests/test_output.py
- Verify: cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning - Verify: cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning
- [ ] **T02: Lift canvas state to App.tsx and add Export navigation** — Currently `useCanvasState()` is instantiated inside `DesignCanvas.tsx`, making canvas state inaccessible to View 3. This task lifts the hook instantiation to `App.tsx`, passes state + actions as props to `DesignCanvas`, and adds an `onExport` callback prop that triggers navigation to the 'export' view. The Export view placeholder in App.tsx is updated to receive canvas state props. DesignCanvas gets an 'Export' button in its toolbar area. All existing DesignCanvas functionality must remain unchanged — this is a mechanical prop-threading refactor. - [x] **T02: Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition** — Currently `useCanvasState()` is instantiated inside `DesignCanvas.tsx`, making canvas state inaccessible to View 3. This task lifts the hook instantiation to `App.tsx`, passes state + actions as props to `DesignCanvas`, and adds an `onExport` callback prop that triggers navigation to the 'export' view. The Export view placeholder in App.tsx is updated to receive canvas state props. DesignCanvas gets an 'Export' button in its toolbar area. All existing DesignCanvas functionality must remain unchanged — this is a mechanical prop-threading refactor.
Key constraints: Key constraints:
- `useCanvasState` hook itself is NOT modified — only where it's called changes - `useCanvasState` hook itself is NOT modified — only where it's called changes

View file

@ -0,0 +1,24 @@
{
"schemaVersion": 1,
"taskId": "T01",
"unitId": "M003/S01/T01",
"timestamp": 1774505826214,
"passed": false,
"discoverySource": "task-plan",
"checks": [
{
"command": "cd engine",
"exitCode": 0,
"durationMs": 11,
"verdict": "pass"
},
{
"command": ".venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning",
"exitCode": 127,
"durationMs": 5,
"verdict": "fail"
}
],
"retryAttempt": 1,
"maxRetries": 2
}

View file

@ -0,0 +1,80 @@
---
id: T02
parent: S01
milestone: M003
provides: []
requires: []
affects: []
key_files: ["app/src/App.tsx", "app/src/views/DesignCanvas.tsx"]
key_decisions: ["UseCanvasStateReturn interface used via extends for DesignCanvasProps — avoids duplicating 14 prop types", "PNG capture happens in handleExport before view transition so Konva stage is still mounted", "pngDataUrl state added to App.tsx — will be passed to ExportView in T04"]
patterns_established: []
drill_down_paths: []
observability_surfaces: []
duration: ""
verification_result: "Ran `cd app && npx tsc -b --noEmit` — 0 new type errors in App.tsx or DesignCanvas.tsx. Ran `cd app && npx vitest run` — 95/95 tests pass across 7 test files. Ran engine tests: 36/36 pass."
completed_at: 2026-03-26T06:19:28.652Z
blocker_discovered: false
---
# T02: Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition
> Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition
## What Happened
---
id: T02
parent: S01
milestone: M003
key_files:
- app/src/App.tsx
- app/src/views/DesignCanvas.tsx
key_decisions:
- UseCanvasStateReturn interface used via extends for DesignCanvasProps — avoids duplicating 14 prop types
- PNG capture happens in handleExport before view transition so Konva stage is still mounted
- pngDataUrl state added to App.tsx — will be passed to ExportView in T04
duration: ""
verification_result: passed
completed_at: 2026-03-26T06:19:28.660Z
blocker_discovered: false
---
# T02: Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition
**Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition**
## What Happened
Moved `useCanvasState(traceMetadata)` call from DesignCanvas.tsx to App.tsx. DesignCanvas now receives all 14 canvas state/action properties via props (using `extends UseCanvasStateReturn` on its props interface), plus `stageRef` and `onExport`. The local `stageRef` in DesignCanvas was removed; it now uses the one passed from App.tsx. App.tsx creates the ref, instantiates the hook, and spreads all return values into DesignCanvas. An Export button was added to DesignCanvas's toolbar area. The `handleExport` callback in App.tsx captures a 2x PNG data URL from the Konva stage before navigating to the export view, storing it in `pngDataUrl` state for later use by ExportView (T04). A placeholder export view shows object count and PNG capture status, with a Back to Design button.
## Verification
Ran `cd app && npx tsc -b --noEmit` — 0 new type errors in App.tsx or DesignCanvas.tsx. Ran `cd app && npx vitest run` — 95/95 tests pass across 7 test files. Ran engine tests: 36/36 pass.
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `cd app && npx tsc -b --noEmit` | 0 | ✅ pass | 5000ms |
| 2 | `cd app && npx vitest run` | 0 | ✅ pass (95/95) | 2260ms |
| 3 | `cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning` | 0 | ✅ pass (36/36) | 390ms |
## Deviations
None.
## Known Issues
None.
## Files Created/Modified
- `app/src/App.tsx`
- `app/src/views/DesignCanvas.tsx`
## Deviations
None.
## Known Issues
None.

View file

@ -1,6 +1,6 @@
{ {
"version": 1, "version": 1,
"exported_at": "2026-03-26T06:16:59.234Z", "exported_at": "2026-03-26T06:19:28.693Z",
"milestones": [ "milestones": [
{ {
"id": "M001", "id": "M001",
@ -1476,19 +1476,26 @@
"milestone_id": "M003", "milestone_id": "M003",
"slice_id": "S01", "slice_id": "S01",
"id": "T02", "id": "T02",
"title": "Lift canvas state to App.tsx and add Export navigation", "title": "Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition",
"status": "pending", "status": "complete",
"one_liner": "", "one_liner": "Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition",
"narrative": "", "narrative": "Moved `useCanvasState(traceMetadata)` call from DesignCanvas.tsx to App.tsx. DesignCanvas now receives all 14 canvas state/action properties via props (using `extends UseCanvasStateReturn` on its props interface), plus `stageRef` and `onExport`. The local `stageRef` in DesignCanvas was removed; it now uses the one passed from App.tsx. App.tsx creates the ref, instantiates the hook, and spreads all return values into DesignCanvas. An Export button was added to DesignCanvas's toolbar area. The `handleExport` callback in App.tsx captures a 2x PNG data URL from the Konva stage before navigating to the export view, storing it in `pngDataUrl` state for later use by ExportView (T04). A placeholder export view shows object count and PNG capture status, with a Back to Design button.",
"verification_result": "", "verification_result": "Ran `cd app && npx tsc -b --noEmit` — 0 new type errors in App.tsx or DesignCanvas.tsx. Ran `cd app && npx vitest run` — 95/95 tests pass across 7 test files. Ran engine tests: 36/36 pass.",
"duration": "", "duration": "",
"completed_at": null, "completed_at": "2026-03-26T06:19:28.652Z",
"blocker_discovered": false, "blocker_discovered": false,
"deviations": "", "deviations": "None.",
"known_issues": "", "known_issues": "None.",
"key_files": [], "key_files": [
"key_decisions": [], "app/src/App.tsx",
"full_summary_md": "", "app/src/views/DesignCanvas.tsx"
],
"key_decisions": [
"UseCanvasStateReturn interface used via extends for DesignCanvasProps — avoids duplicating 14 prop types",
"PNG capture happens in handleExport before view transition so Konva stage is still mounted",
"pngDataUrl state added to App.tsx — will be passed to ExportView in T04"
],
"full_summary_md": "---\nid: T02\nparent: S01\nmilestone: M003\nkey_files:\n - app/src/App.tsx\n - app/src/views/DesignCanvas.tsx\nkey_decisions:\n - UseCanvasStateReturn interface used via extends for DesignCanvasProps — avoids duplicating 14 prop types\n - PNG capture happens in handleExport before view transition so Konva stage is still mounted\n - pngDataUrl state added to App.tsx — will be passed to ExportView in T04\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T06:19:28.660Z\nblocker_discovered: false\n---\n\n# T02: Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition\n\n**Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition**\n\n## What Happened\n\nMoved `useCanvasState(traceMetadata)` call from DesignCanvas.tsx to App.tsx. DesignCanvas now receives all 14 canvas state/action properties via props (using `extends UseCanvasStateReturn` on its props interface), plus `stageRef` and `onExport`. The local `stageRef` in DesignCanvas was removed; it now uses the one passed from App.tsx. App.tsx creates the ref, instantiates the hook, and spreads all return values into DesignCanvas. An Export button was added to DesignCanvas's toolbar area. The `handleExport` callback in App.tsx captures a 2x PNG data URL from the Konva stage before navigating to the export view, storing it in `pngDataUrl` state for later use by ExportView (T04). A placeholder export view shows object count and PNG capture status, with a Back to Design button.\n\n## Verification\n\nRan `cd app && npx tsc -b --noEmit` — 0 new type errors in App.tsx or DesignCanvas.tsx. Ran `cd app && npx vitest run` — 95/95 tests pass across 7 test files. Ran engine tests: 36/36 pass.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `cd app && npx tsc -b --noEmit` | 0 | ✅ pass | 5000ms |\n| 2 | `cd app && npx vitest run` | 0 | ✅ pass (95/95) | 2260ms |\n| 3 | `cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning` | 0 | ✅ pass (36/36) | 390ms |\n\n\n## Deviations\n\nNone.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `app/src/App.tsx`\n- `app/src/views/DesignCanvas.tsx`\n",
"description": "Currently `useCanvasState()` is instantiated inside `DesignCanvas.tsx`, making canvas state inaccessible to View 3. This task lifts the hook instantiation to `App.tsx`, passes state + actions as props to `DesignCanvas`, and adds an `onExport` callback prop that triggers navigation to the 'export' view. The Export view placeholder in App.tsx is updated to receive canvas state props. DesignCanvas gets an 'Export' button in its toolbar area. All existing DesignCanvas functionality must remain unchanged — this is a mechanical prop-threading refactor.\n\nKey constraints:\n- `useCanvasState` hook itself is NOT modified — only where it's called changes\n- DesignCanvas receives `state`, `addObject`, `removeObject`, `updateObject`, `selectObjects`, `deselectAll`, `reorderObject`, `toggleVisibility`, `toggleLock`, `setArtboard`, `undo`, `redo`, `canUndo`, `canRedo` as props instead of calling the hook internally\n- The `traceMetadata` param to `useCanvasState()` comes from App.tsx's existing `traceMetadata` state\n- App.tsx passes a `stageRef` to DesignCanvas and receives it back so PNG export can work later\n- A `Ref<Konva.Stage>` is created in App.tsx and passed to DesignCanvas for stage access from View 3", "description": "Currently `useCanvasState()` is instantiated inside `DesignCanvas.tsx`, making canvas state inaccessible to View 3. This task lifts the hook instantiation to `App.tsx`, passes state + actions as props to `DesignCanvas`, and adds an `onExport` callback prop that triggers navigation to the 'export' view. The Export view placeholder in App.tsx is updated to receive canvas state props. DesignCanvas gets an 'Export' button in its toolbar area. All existing DesignCanvas functionality must remain unchanged — this is a mechanical prop-threading refactor.\n\nKey constraints:\n- `useCanvasState` hook itself is NOT modified — only where it's called changes\n- DesignCanvas receives `state`, `addObject`, `removeObject`, `updateObject`, `selectObjects`, `deselectAll`, `reorderObject`, `toggleVisibility`, `toggleLock`, `setArtboard`, `undo`, `redo`, `canUndo`, `canRedo` as props instead of calling the hook internally\n- The `traceMetadata` param to `useCanvasState()` comes from App.tsx's existing `traceMetadata` state\n- App.tsx passes a `stageRef` to DesignCanvas and receives it back so PNG export can work later\n- A `Ref<Konva.Stage>` is created in App.tsx and passed to DesignCanvas for stage access from View 3",
"estimate": "1.5h", "estimate": "1.5h",
"files": [ "files": [
@ -2134,6 +2141,39 @@
"verdict": "✅ pass", "verdict": "✅ pass",
"duration_ms": 1060, "duration_ms": 1060,
"created_at": "2026-03-26T06:16:59.178Z" "created_at": "2026-03-26T06:16:59.178Z"
},
{
"id": 40,
"task_id": "T02",
"slice_id": "S01",
"milestone_id": "M003",
"command": "cd app && npx tsc -b --noEmit",
"exit_code": 0,
"verdict": "✅ pass",
"duration_ms": 5000,
"created_at": "2026-03-26T06:19:28.653Z"
},
{
"id": 41,
"task_id": "T02",
"slice_id": "S01",
"milestone_id": "M003",
"command": "cd app && npx vitest run",
"exit_code": 0,
"verdict": "✅ pass (95/95)",
"duration_ms": 2260,
"created_at": "2026-03-26T06:19:28.653Z"
},
{
"id": 42,
"task_id": "T02",
"slice_id": "S01",
"milestone_id": "M003",
"command": "cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning",
"exit_code": 0,
"verdict": "✅ pass (36/36)",
"duration_ms": 390,
"created_at": "2026-03-26T06:19:28.653Z"
} }
] ]
} }

View file

@ -1,5 +1,7 @@
import { useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import type Konva from 'konva';
import type { TraceMetadata } from './types/engine'; import type { TraceMetadata } from './types/engine';
import { useCanvasState } from './hooks/useCanvasState';
import ImportConvert from './views/ImportConvert'; import ImportConvert from './views/ImportConvert';
import DesignCanvas from './views/DesignCanvas'; import DesignCanvas from './views/DesignCanvas';
import './App.css'; import './App.css';
@ -10,6 +12,13 @@ function App() {
const [view, setView] = useState<ViewState>('import'); const [view, setView] = useState<ViewState>('import');
const [svgResult, setSvgResult] = useState<string | null>(null); const [svgResult, setSvgResult] = useState<string | null>(null);
const [traceMetadata, setTraceMetadata] = useState<TraceMetadata | null>(null); const [traceMetadata, setTraceMetadata] = useState<TraceMetadata | null>(null);
const [pngDataUrl, setPngDataUrl] = useState<string | null>(null);
// Lifted canvas state — shared between View 2 (DesignCanvas) and View 3 (ExportView)
const canvasState = useCanvasState(traceMetadata);
// Stage ref created here so View 3 can capture PNG from View 2's Konva stage
const stageRef = useRef<Konva.Stage | null>(null);
const handleUseThis = (svgOutput: string, metadata: TraceMetadata) => { const handleUseThis = (svgOutput: string, metadata: TraceMetadata) => {
setSvgResult(svgOutput); setSvgResult(svgOutput);
@ -17,13 +26,40 @@ function App() {
setView('canvas'); setView('canvas');
}; };
const handleExport = useCallback(() => {
// Capture PNG data URL from the Konva stage before navigating away
if (stageRef.current) {
const dataUrl = stageRef.current.toDataURL({ pixelRatio: 2 });
setPngDataUrl(dataUrl);
}
setView('export');
}, []);
const handleBackToCanvas = useCallback(() => {
setView('canvas');
}, []);
return ( return (
<div id="app"> <div id="app">
{view === 'import' && <ImportConvert onUseThis={handleUseThis} />} {view === 'import' && <ImportConvert onUseThis={handleUseThis} />}
{view === 'canvas' && ( {view === 'canvas' && (
<DesignCanvas svgData={svgResult} traceMetadata={traceMetadata} /> <DesignCanvas
svgData={svgResult}
stageRef={stageRef}
onExport={handleExport}
{...canvasState}
/>
)}
{view === 'export' && (
<div className="placeholder-view" data-testid="export-view">
<p>View 3: Export (placeholder)</p>
<p>PNG preview captured: {pngDataUrl ? 'Yes' : 'No'}</p>
<p>Objects: {canvasState.state.objects.length}</p>
<button type="button" onClick={handleBackToCanvas}>
Back to Design
</button>
</div>
)} )}
{view === 'export' && <div className="placeholder-view">View 3: Export</div>}
</div> </div>
); );
} }

View file

@ -8,9 +8,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type Konva from 'konva'; import type Konva from 'konva';
import type { TraceMetadata } from '../types/engine';
import type { ArtboardConfig, CanvasObject, ImageObject } from '../types/canvas'; import type { ArtboardConfig, CanvasObject, ImageObject } from '../types/canvas';
import { useCanvasState } from '../hooks/useCanvasState'; import type { UseCanvasStateReturn } from '../hooks/useCanvasState';
import ArtboardSetup from '../components/canvas/ArtboardSetup'; import ArtboardSetup from '../components/canvas/ArtboardSetup';
import KonvaStage from '../components/canvas/KonvaStage'; import KonvaStage from '../components/canvas/KonvaStage';
import type { CanvasTool } from '../components/canvas/KonvaStage'; import type { CanvasTool } from '../components/canvas/KonvaStage';
@ -21,31 +20,33 @@ import ShapeProperties from '../components/canvas/ShapeProperties';
import { toPx } from '../utils/artboardShapes'; import { toPx } from '../utils/artboardShapes';
import styles from './DesignCanvas.module.css'; import styles from './DesignCanvas.module.css';
interface DesignCanvasProps { export interface DesignCanvasProps extends UseCanvasStateReturn {
svgData: string | null; svgData: string | null;
traceMetadata: TraceMetadata | null; /** Ref to the Konva Stage, created in App.tsx for cross-view access (PNG export). */
stageRef: React.RefObject<Konva.Stage | null>;
/** Called when user clicks Export — App.tsx navigates to View 3. */
onExport: () => void;
} }
export default function DesignCanvas({ export default function DesignCanvas({
svgData, svgData,
traceMetadata, state,
addObject,
removeObject,
updateObject,
selectObjects,
deselectAll,
reorderObject,
toggleVisibility,
toggleLock,
setArtboard,
undo,
redo,
canUndo,
canRedo,
stageRef,
onExport,
}: DesignCanvasProps) { }: DesignCanvasProps) {
const {
state,
addObject,
removeObject,
updateObject,
selectObjects,
deselectAll,
reorderObject,
toggleVisibility,
toggleLock,
setArtboard,
undo,
redo,
canUndo,
canRedo,
} = useCanvasState(traceMetadata);
const [activeTool, setActiveTool] = useState<CanvasTool>('pointer'); const [activeTool, setActiveTool] = useState<CanvasTool>('pointer');
const [showArtboardSetup, setShowArtboardSetup] = useState(true); const [showArtboardSetup, setShowArtboardSetup] = useState(true);
@ -53,7 +54,6 @@ export default function DesignCanvas({
const [showGrid, setShowGrid] = useState(false); const [showGrid, setShowGrid] = useState(false);
const [zoomLevel, setZoomLevel] = useState(1); const [zoomLevel, setZoomLevel] = useState(1);
const stageRef = useRef<Konva.Stage | null>(null);
const canvasContainerRef = useRef<HTMLDivElement | null>(null); const canvasContainerRef = useRef<HTMLDivElement | null>(null);
const [stageSize, setStageSize] = useState({ width: 800, height: 600 }); const [stageSize, setStageSize] = useState({ width: 800, height: 600 });
@ -265,6 +265,15 @@ export default function DesignCanvas({
onZoomOut={handleZoomOut} onZoomOut={handleZoomOut}
onZoomFit={handleZoomFit} onZoomFit={handleZoomFit}
/> />
<button
type="button"
className="canvas-tool-btn"
onClick={onExport}
title="Export design"
data-testid="export-btn"
>
Export
</button>
</div> </div>
{/* Main area: canvas + right panel */} {/* Main area: canvas + right panel */}