diff --git a/.gsd/event-log.jsonl b/.gsd/event-log.jsonl index a593307..860d619 100644 --- a/.gsd/event-log.jsonl +++ b/.gsd/event-log.jsonl @@ -26,3 +26,4 @@ {"cmd":"complete-slice","params":{"milestoneId":"M002","sliceId":"S02"},"ts":"2026-03-26T05:44:01.083Z","actor":"agent","hash":"7c28ef1e308c7de7","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"plan-slice","params":{"milestoneId":"M002","sliceId":"S03"},"ts":"2026-03-26T05:48:40.020Z","actor":"agent","hash":"22ad4efa07f9be81","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S03","taskId":"T01"},"ts":"2026-03-26T05:52:47.302Z","actor":"agent","hash":"c631fa7e62a3f1cc","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} +{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S03","taskId":"T02"},"ts":"2026-03-26T05:55:43.882Z","actor":"agent","hash":"e4c20836b9c0ee25","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} diff --git a/.gsd/milestones/M002/slices/S03/S03-PLAN.md b/.gsd/milestones/M002/slices/S03/S03-PLAN.md index c8a56ca..e5ec860 100644 --- a/.gsd/milestones/M002/slices/S03/S03-PLAN.md +++ b/.gsd/milestones/M002/slices/S03/S03-PLAN.md @@ -19,7 +19,7 @@ This is the riskiest piece of the slice — if opentype.js path extraction doesn - Estimate: 1h - Files: app/src/utils/fontService.ts, app/src/utils/__tests__/fontService.test.ts, app/public/fonts/, app/src/App.css, app/package.json - Verify: cd app && npx vitest run src/utils/__tests__/fontService.test.ts --reporter=verbose && npx tsc --noEmit && ls public/fonts/*.ttf | wc -l -- [ ] **T02: Add TextObject type and wire text tool into canvas rendering** — Extend the CanvasObject discriminated union with a TextObject type. Wire text objects into all canvas components: KonvaStage (render via Konva Text + case in getObjWidth/getObjHeight + text tool creation in handleStageMouseDown), CanvasToolbar (add 'text' to CanvasTool union and TOOLS array), ObjectPanel (add 'text' icon to TYPE_ICONS), and DesignCanvas (pass through). This follows the exact pattern established in S02 for adding object types. +- [x] **T02: Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage** — Extend the CanvasObject discriminated union with a TextObject type. Wire text objects into all canvas components: KonvaStage (render via Konva Text + case in getObjWidth/getObjHeight + text tool creation in handleStageMouseDown), CanvasToolbar (add 'text' to CanvasTool union and TOOLS array), ObjectPanel (add 'text' icon to TYPE_ICONS), and DesignCanvas (pass through). This follows the exact pattern established in S02 for adding object types. ## Key Implementation Details - TextObject interface: extends BaseCanvasObject with type: 'text', text: string, fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number, fill: string, stroke: string, strokeWidth: number, width: number (for wrapping) diff --git a/.gsd/milestones/M002/slices/S03/tasks/T01-VERIFY.json b/.gsd/milestones/M002/slices/S03/tasks/T01-VERIFY.json new file mode 100644 index 0000000..b0026fd --- /dev/null +++ b/.gsd/milestones/M002/slices/S03/tasks/T01-VERIFY.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M002/S03/T01", + "timestamp": 1774504384634, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd app", + "exitCode": 0, + "durationMs": 4, + "verdict": "pass" + }, + { + "command": "npx vitest run src/utils/__tests__/fontService.test.ts --reporter=verbose", + "exitCode": 0, + "durationMs": 1408, + "verdict": "pass" + }, + { + "command": "npx tsc --noEmit", + "exitCode": 1, + "durationMs": 734, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M002/slices/S03/tasks/T02-SUMMARY.md b/.gsd/milestones/M002/slices/S03/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..e0b3bf5 --- /dev/null +++ b/.gsd/milestones/M002/slices/S03/tasks/T02-SUMMARY.md @@ -0,0 +1,86 @@ +--- +id: T02 +parent: S03 +milestone: M002 +provides: [] +requires: [] +affects: [] +key_files: ["app/src/types/canvas.ts", "app/src/components/canvas/KonvaStage.tsx", "app/src/components/canvas/CanvasToolbar.tsx", "app/src/components/canvas/ObjectPanel.tsx", "app/src/components/canvas/ShapeProperties.tsx", "app/src/components/canvas/AlignmentBar.tsx"] +key_decisions: ["Text objects default to fill '#000000' and stroke 'transparent' (opposite of shape defaults) for natural text appearance", "Text transform scales width only (for wrapping), keeping fontSize unchanged — consistent with design tool UX"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "Ran `cd app && npx tsc --noEmit` — zero errors under strict mode. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.45s." +completed_at: 2026-03-26T05:55:43.833Z +blocker_discovered: false +--- + +# T02: Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage + +> Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage + +## What Happened +--- +id: T02 +parent: S03 +milestone: M002 +key_files: + - app/src/types/canvas.ts + - app/src/components/canvas/KonvaStage.tsx + - app/src/components/canvas/CanvasToolbar.tsx + - app/src/components/canvas/ObjectPanel.tsx + - app/src/components/canvas/ShapeProperties.tsx + - app/src/components/canvas/AlignmentBar.tsx +key_decisions: + - Text objects default to fill '#000000' and stroke 'transparent' (opposite of shape defaults) for natural text appearance + - Text transform scales width only (for wrapping), keeping fontSize unchanged — consistent with design tool UX +duration: "" +verification_result: passed +completed_at: 2026-03-26T05:55:43.846Z +blocker_discovered: false +--- + +# T02: Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage + +**Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage** + +## What Happened + +Added a TextObject interface to canvas.ts extending BaseCanvasObject with text-specific fields (text, fontFamily, fontSize, letterSpacing, lineHeight, fill, stroke, strokeWidth, width for wrapping). Added it to the CanvasObject discriminated union. Then wired the text type through all six canvas component files: KonvaStage (rendering, tool creation, sizing, transform), CanvasToolbar (TOOLS array), ObjectPanel (TYPE_ICONS), ShapeProperties (getWidth/getHeight + hasFill), and AlignmentBar (toBoundingRect). All switch statements are exhaustive for the updated union. TypeScript compiles cleanly and all 95 tests pass. + +## Verification + +Ran `cd app && npx tsc --noEmit` — zero errors under strict mode. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.45s. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2000ms | +| 2 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 2450ms | + + +## Deviations + +Added case 'text' to AlignmentBar.tsx toBoundingRect — not listed in task plan but required for exhaustive switch coverage. + +## Known Issues + +None. + +## Files Created/Modified + +- `app/src/types/canvas.ts` +- `app/src/components/canvas/KonvaStage.tsx` +- `app/src/components/canvas/CanvasToolbar.tsx` +- `app/src/components/canvas/ObjectPanel.tsx` +- `app/src/components/canvas/ShapeProperties.tsx` +- `app/src/components/canvas/AlignmentBar.tsx` + + +## Deviations +Added case 'text' to AlignmentBar.tsx toBoundingRect — not listed in task plan but required for exhaustive switch coverage. + +## Known Issues +None. diff --git a/.gsd/state-manifest.json b/.gsd/state-manifest.json index 914a062..abc0a05 100644 --- a/.gsd/state-manifest.json +++ b/.gsd/state-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "exported_at": "2026-03-26T05:52:47.299Z", + "exported_at": "2026-03-26T05:55:43.881Z", "milestones": [ { "id": "M001", @@ -1329,19 +1329,29 @@ "milestone_id": "M002", "slice_id": "S03", "id": "T02", - "title": "Add TextObject type and wire text tool into canvas rendering", - "status": "pending", - "one_liner": "", - "narrative": "", - "verification_result": "", + "title": "Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage", + "status": "complete", + "one_liner": "Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage", + "narrative": "Added a TextObject interface to canvas.ts extending BaseCanvasObject with text-specific fields (text, fontFamily, fontSize, letterSpacing, lineHeight, fill, stroke, strokeWidth, width for wrapping). Added it to the CanvasObject discriminated union. Then wired the text type through all six canvas component files: KonvaStage (rendering, tool creation, sizing, transform), CanvasToolbar (TOOLS array), ObjectPanel (TYPE_ICONS), ShapeProperties (getWidth/getHeight + hasFill), and AlignmentBar (toBoundingRect). All switch statements are exhaustive for the updated union. TypeScript compiles cleanly and all 95 tests pass.", + "verification_result": "Ran `cd app && npx tsc --noEmit` — zero errors under strict mode. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.45s.", "duration": "", - "completed_at": null, + "completed_at": "2026-03-26T05:55:43.833Z", "blocker_discovered": false, - "deviations": "", - "known_issues": "", - "key_files": [], - "key_decisions": [], - "full_summary_md": "", + "deviations": "Added case 'text' to AlignmentBar.tsx toBoundingRect — not listed in task plan but required for exhaustive switch coverage.", + "known_issues": "None.", + "key_files": [ + "app/src/types/canvas.ts", + "app/src/components/canvas/KonvaStage.tsx", + "app/src/components/canvas/CanvasToolbar.tsx", + "app/src/components/canvas/ObjectPanel.tsx", + "app/src/components/canvas/ShapeProperties.tsx", + "app/src/components/canvas/AlignmentBar.tsx" + ], + "key_decisions": [ + "Text objects default to fill '#000000' and stroke 'transparent' (opposite of shape defaults) for natural text appearance", + "Text transform scales width only (for wrapping), keeping fontSize unchanged — consistent with design tool UX" + ], + "full_summary_md": "---\nid: T02\nparent: S03\nmilestone: M002\nkey_files:\n - app/src/types/canvas.ts\n - app/src/components/canvas/KonvaStage.tsx\n - app/src/components/canvas/CanvasToolbar.tsx\n - app/src/components/canvas/ObjectPanel.tsx\n - app/src/components/canvas/ShapeProperties.tsx\n - app/src/components/canvas/AlignmentBar.tsx\nkey_decisions:\n - Text objects default to fill '#000000' and stroke 'transparent' (opposite of shape defaults) for natural text appearance\n - Text transform scales width only (for wrapping), keeping fontSize unchanged — consistent with design tool UX\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T05:55:43.846Z\nblocker_discovered: false\n---\n\n# T02: Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage\n\n**Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage**\n\n## What Happened\n\nAdded a TextObject interface to canvas.ts extending BaseCanvasObject with text-specific fields (text, fontFamily, fontSize, letterSpacing, lineHeight, fill, stroke, strokeWidth, width for wrapping). Added it to the CanvasObject discriminated union. Then wired the text type through all six canvas component files: KonvaStage (rendering, tool creation, sizing, transform), CanvasToolbar (TOOLS array), ObjectPanel (TYPE_ICONS), ShapeProperties (getWidth/getHeight + hasFill), and AlignmentBar (toBoundingRect). All switch statements are exhaustive for the updated union. TypeScript compiles cleanly and all 95 tests pass.\n\n## Verification\n\nRan `cd app && npx tsc --noEmit` — zero errors under strict mode. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.45s.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2000ms |\n| 2 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 2450ms |\n\n\n## Deviations\n\nAdded case 'text' to AlignmentBar.tsx toBoundingRect — not listed in task plan but required for exhaustive switch coverage.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `app/src/types/canvas.ts`\n- `app/src/components/canvas/KonvaStage.tsx`\n- `app/src/components/canvas/CanvasToolbar.tsx`\n- `app/src/components/canvas/ObjectPanel.tsx`\n- `app/src/components/canvas/ShapeProperties.tsx`\n- `app/src/components/canvas/AlignmentBar.tsx`\n", "description": "Extend the CanvasObject discriminated union with a TextObject type. Wire text objects into all canvas components: KonvaStage (render via Konva Text + case in getObjWidth/getObjHeight + text tool creation in handleStageMouseDown), CanvasToolbar (add 'text' to CanvasTool union and TOOLS array), ObjectPanel (add 'text' icon to TYPE_ICONS), and DesignCanvas (pass through). This follows the exact pattern established in S02 for adding object types.\n\n## Key Implementation Details\n- TextObject interface: extends BaseCanvasObject with type: 'text', text: string, fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number, fill: string, stroke: string, strokeWidth: number, width: number (for wrapping)\n- CanvasTool union in KonvaStage.tsx must add 'text' — this is imported by CanvasToolbar\n- KonvaStage renderObject switch: case 'text' renders from react-konva with fontFamily, fontSize, fill, stroke, strokeWidth, width, letterSpacing, lineHeight props\n- KonvaStage handleStageMouseDown switch: case 'text' creates a TextObject with defaults (text: 'Text', fontFamily: 'Roboto', fontSize: 24, letterSpacing: 0, lineHeight: 1.2)\n- KonvaStage getObjWidth for text: use obj.width (wrapping width) or estimate from fontSize * text.length * 0.6\n- KonvaStage getObjHeight for text: obj.fontSize * obj.lineHeight\n- KonvaStage onTransformEnd for text: scale width, keep fontSize unchanged (text wraps to new width)\n- ObjectPanel TYPE_ICONS: text → 'T'\n- ShapeProperties getWidth/getHeight: add case 'text' returning obj.width and obj.fontSize * obj.lineHeight\n- All switch statements must be exhaustive — TypeScript noFallthroughCasesInSwitch enforces this\n- TypeScript strict mode — no unused locals/params, erasableSyntaxOnly, use import type", "estimate": "1h", "files": [ @@ -1858,6 +1868,28 @@ "verdict": "✅ pass", "duration_ms": 100, "created_at": "2026-03-26T05:52:47.243Z" + }, + { + "id": 34, + "task_id": "T02", + "slice_id": "S03", + "milestone_id": "M002", + "command": "cd app && npx tsc --noEmit", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 2000, + "created_at": "2026-03-26T05:55:43.833Z" + }, + { + "id": 35, + "task_id": "T02", + "slice_id": "S03", + "milestone_id": "M002", + "command": "cd app && npx vitest run --reporter=verbose", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 2450, + "created_at": "2026-03-26T05:55:43.833Z" } ] } \ No newline at end of file diff --git a/app/src/components/canvas/AlignmentBar.tsx b/app/src/components/canvas/AlignmentBar.tsx index ef2fd63..af037ba 100644 --- a/app/src/components/canvas/AlignmentBar.tsx +++ b/app/src/components/canvas/AlignmentBar.tsx @@ -48,6 +48,10 @@ function toBoundingRect(obj: CanvasObject): BoundingRect { h = Math.max(...ys) - Math.min(...ys); break; } + case 'text': + w = obj.width; + h = obj.fontSize * obj.lineHeight; + break; } return { id: obj.id, x: obj.x, y: obj.y, width: w, height: h }; } diff --git a/app/src/components/canvas/CanvasToolbar.tsx b/app/src/components/canvas/CanvasToolbar.tsx index d39961e..984d869 100644 --- a/app/src/components/canvas/CanvasToolbar.tsx +++ b/app/src/components/canvas/CanvasToolbar.tsx @@ -14,6 +14,7 @@ const TOOLS: { tool: CanvasTool; label: string; icon: string }[] = [ { tool: 'circle', label: 'Circle', icon: '○' }, { tool: 'ellipse', label: 'Ellipse', icon: '⬯' }, { tool: 'line', label: 'Line', icon: '╱' }, + { tool: 'text', label: 'Text', icon: 'T' }, ]; // -- Props -------------------------------------------------------------------- diff --git a/app/src/components/canvas/KonvaStage.tsx b/app/src/components/canvas/KonvaStage.tsx index 394ed1e..3055c25 100644 --- a/app/src/components/canvas/KonvaStage.tsx +++ b/app/src/components/canvas/KonvaStage.tsx @@ -23,6 +23,7 @@ import { Ellipse, Line, Image as KonvaImage, + Text as KonvaText, Transformer, Path, } from 'react-konva'; @@ -36,7 +37,7 @@ import { toPx, artboardClipPath } from '../../utils/artboardShapes'; // -- Types -------------------------------------------------------------------- -export type CanvasTool = 'pointer' | 'rect' | 'circle' | 'ellipse' | 'line'; +export type CanvasTool = 'pointer' | 'rect' | 'circle' | 'ellipse' | 'line' | 'text'; export interface KonvaStageProps { width: number; @@ -247,6 +248,9 @@ export default function KonvaStage({ } else if (obj.type === 'ellipse') { (changes as Record).radiusX = Math.max(5, (node.width() * scaleX) / 2); (changes as Record).radiusY = Math.max(5, (node.height() * scaleY) / 2); + } else if (obj.type === 'text') { + // Scale width for wrapping, keep fontSize unchanged + (changes as Record).width = Math.max(20, node.width() * scaleX); } onUpdateObject(obj.id, changes); @@ -315,6 +319,23 @@ export default function KonvaStage({ /> ); + case 'text': + return ( + + ); + default: return null; } @@ -426,6 +447,29 @@ export default function KonvaStage({ dash: [], }; break; + + case 'text': + newObj = { + id: nextId('text'), + type: 'text', + name: 'Text', + x, + y, + rotation: 0, + visible: true, + locked: false, + opacity: 1, + text: 'Text', + fontFamily: 'Roboto', + fontSize: 24, + letterSpacing: 0, + lineHeight: 1.2, + fill: '#000000', + stroke: 'transparent', + strokeWidth: 0, + width: 200, + }; + break; } if (newObj) { @@ -592,6 +636,8 @@ function getObjWidth(obj: CanvasObject): number { case 'line': return Math.max(...obj.points.filter((_, i) => i % 2 === 0)) - Math.min(...obj.points.filter((_, i) => i % 2 === 0)); + case 'text': + return obj.width || obj.fontSize * obj.text.length * 0.6; } } @@ -607,6 +653,8 @@ function getObjHeight(obj: CanvasObject): number { case 'line': return Math.max(...obj.points.filter((_, i) => i % 2 === 1)) - Math.min(...obj.points.filter((_, i) => i % 2 === 1)); + case 'text': + return obj.fontSize * obj.lineHeight; } } diff --git a/app/src/components/canvas/ObjectPanel.tsx b/app/src/components/canvas/ObjectPanel.tsx index 7037a70..aba960f 100644 --- a/app/src/components/canvas/ObjectPanel.tsx +++ b/app/src/components/canvas/ObjectPanel.tsx @@ -17,6 +17,7 @@ const TYPE_ICONS: Record = { ellipse: '⬯', line: '╱', image: '🖼', + text: 'T', }; // -- Props -------------------------------------------------------------------- diff --git a/app/src/components/canvas/ShapeProperties.tsx b/app/src/components/canvas/ShapeProperties.tsx index d761fe4..f6bafc1 100644 --- a/app/src/components/canvas/ShapeProperties.tsx +++ b/app/src/components/canvas/ShapeProperties.tsx @@ -23,6 +23,8 @@ function getWidth(obj: CanvasObject): number { const xs = obj.points.filter((_, i) => i % 2 === 0); return Math.round((Math.max(...xs) - Math.min(...xs)) * 100) / 100; } + case 'text': + return Math.round(obj.width * 100) / 100; } } @@ -39,6 +41,8 @@ function getHeight(obj: CanvasObject): number { const ys = obj.points.filter((_, i) => i % 2 === 1); return Math.round((Math.max(...ys) - Math.min(...ys)) * 100) / 100; } + case 'text': + return Math.round(obj.fontSize * obj.lineHeight * 100) / 100; } } @@ -62,7 +66,7 @@ export default function ShapeProperties({ onUpdate, }: ShapePropertiesProps) { const hasStroke = object.type !== 'image'; - const hasFill = object.type === 'rect' || object.type === 'circle' || object.type === 'ellipse'; + const hasFill = object.type === 'rect' || object.type === 'circle' || object.type === 'ellipse' || object.type === 'text'; const isLine = object.type === 'line'; const handleChange = useCallback( diff --git a/app/src/types/canvas.ts b/app/src/types/canvas.ts index 503c7ea..bf27fdc 100644 --- a/app/src/types/canvas.ts +++ b/app/src/types/canvas.ts @@ -82,12 +82,27 @@ export interface ImageObject extends BaseCanvasObject { src: string; } +export interface TextObject extends BaseCanvasObject { + type: 'text'; + text: string; + fontFamily: string; + fontSize: number; + letterSpacing: number; + lineHeight: number; + fill: string; + stroke: string; + strokeWidth: number; + /** Wrapping width for text layout. */ + width: number; +} + export type CanvasObject = | RectObject | CircleObject | EllipseObject | LineObject - | ImageObject; + | ImageObject + | TextObject; export type CanvasObjectType = CanvasObject['type'];