From 46169ecf023467ef059b696a7c8350ed1ae96fda Mon Sep 17 00:00:00 2001 From: jlightner Date: Thu, 26 Mar 2026 05:58:10 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20text-specific=20property=20cont?= =?UTF-8?q?rols=20(content,=20font=20family,=20size=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "app/src/components/canvas/ShapeProperties.tsx" - "app/src/views/DesignCanvas.tsx" GSD-Task: S03/T03 --- .gsd/event-log.jsonl | 1 + .gsd/milestones/M002/slices/S03/S03-PLAN.md | 2 +- .../M002/slices/S03/tasks/T02-VERIFY.json | 30 +++ .../M002/slices/S03/tasks/T03-SUMMARY.md | 78 ++++++++ .gsd/state-manifest.json | 52 +++-- app/src/components/canvas/ShapeProperties.tsx | 186 +++++++++++++++++- app/src/views/DesignCanvas.tsx | 14 +- 7 files changed, 347 insertions(+), 16 deletions(-) create mode 100644 .gsd/milestones/M002/slices/S03/tasks/T02-VERIFY.json create mode 100644 .gsd/milestones/M002/slices/S03/tasks/T03-SUMMARY.md diff --git a/.gsd/event-log.jsonl b/.gsd/event-log.jsonl index 860d619..f1baa35 100644 --- a/.gsd/event-log.jsonl +++ b/.gsd/event-log.jsonl @@ -27,3 +27,4 @@ {"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"} +{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S03","taskId":"T03"},"ts":"2026-03-26T05:58:01.944Z","actor":"agent","hash":"61f3bf5bb0b4c33f","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 e5ec860..139fd12 100644 --- a/.gsd/milestones/M002/slices/S03/S03-PLAN.md +++ b/.gsd/milestones/M002/slices/S03/S03-PLAN.md @@ -36,7 +36,7 @@ This is the riskiest piece of the slice — if opentype.js path extraction doesn - Estimate: 1h - 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 - Verify: cd app && npx tsc --noEmit && npx vitest run --reporter=verbose -- [ ] **T03: Build text properties panel and convert-to-paths action in ShapeProperties** — Add text-specific property controls to ShapeProperties and implement the 'Convert to Paths' action that replaces a TextObject with path-based objects using fontService.textToPathData(). +- [x] **T03: Added text-specific property controls (content, font family, size, letter spacing, line height) and Convert to Paths button to ShapeProperties, wired through DesignCanvas** — Add text-specific property controls to ShapeProperties and implement the 'Convert to Paths' action that replaces a TextObject with path-based objects using fontService.textToPathData(). ## Text Property Controls (in ShapeProperties) When the selected object has type 'text', show: diff --git a/.gsd/milestones/M002/slices/S03/tasks/T02-VERIFY.json b/.gsd/milestones/M002/slices/S03/tasks/T02-VERIFY.json new file mode 100644 index 0000000..cbf5226 --- /dev/null +++ b/.gsd/milestones/M002/slices/S03/tasks/T02-VERIFY.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M002/S03/T02", + "timestamp": 1774504547344, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd app", + "exitCode": 0, + "durationMs": 4, + "verdict": "pass" + }, + { + "command": "npx tsc --noEmit", + "exitCode": 1, + "durationMs": 784, + "verdict": "fail" + }, + { + "command": "npx vitest run --reporter=verbose", + "exitCode": 1, + "durationMs": 1590, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M002/slices/S03/tasks/T03-SUMMARY.md b/.gsd/milestones/M002/slices/S03/tasks/T03-SUMMARY.md new file mode 100644 index 0000000..c7b48d2 --- /dev/null +++ b/.gsd/milestones/M002/slices/S03/tasks/T03-SUMMARY.md @@ -0,0 +1,78 @@ +--- +id: T03 +parent: S03 +milestone: M002 +provides: [] +requires: [] +affects: [] +key_files: ["app/src/components/canvas/ShapeProperties.tsx", "app/src/views/DesignCanvas.tsx"] +key_decisions: ["Convert-to-paths uses a single onConvertToPath(textObjectId, imageObject) callback rather than separate onAddObject + onRemoveObject — simpler prop interface, atomic replacement", "Convert-to-paths generates an SVG Blob URL with the text path data and creates an ImageObject at the same position — reuses existing image rendering pipeline"] +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 95 tests pass across 7 test files in 2.2s." +completed_at: 2026-03-26T05:58:01.901Z +blocker_discovered: false +--- + +# T03: Added text-specific property controls (content, font family, size, letter spacing, line height) and Convert to Paths button to ShapeProperties, wired through DesignCanvas + +> Added text-specific property controls (content, font family, size, letter spacing, line height) and Convert to Paths button to ShapeProperties, wired through DesignCanvas + +## What Happened +--- +id: T03 +parent: S03 +milestone: M002 +key_files: + - app/src/components/canvas/ShapeProperties.tsx + - app/src/views/DesignCanvas.tsx +key_decisions: + - Convert-to-paths uses a single onConvertToPath(textObjectId, imageObject) callback rather than separate onAddObject + onRemoveObject — simpler prop interface, atomic replacement + - Convert-to-paths generates an SVG Blob URL with the text path data and creates an ImageObject at the same position — reuses existing image rendering pipeline +duration: "" +verification_result: passed +completed_at: 2026-03-26T05:58:01.911Z +blocker_discovered: false +--- + +# T03: Added text-specific property controls (content, font family, size, letter spacing, line height) and Convert to Paths button to ShapeProperties, wired through DesignCanvas + +**Added text-specific property controls (content, font family, size, letter spacing, line height) and Convert to Paths button to ShapeProperties, wired through DesignCanvas** + +## What Happened + +Extended ShapeProperties with text-specific controls that render when the selected object has type 'text': a textarea for text content, a font family dropdown populated from fontService.getAvailableFonts(), numeric inputs for font size (8–200), letter spacing (-5–20), and line height (0.5–3). Added a "Convert to Paths" button that calls fontService.textToPathData() to convert the text to SVG path data, wraps it in an SVG string, creates a Blob URL, and replaces the TextObject with an ImageObject at the same position via a new onConvertToPath callback prop. The button shows a window.confirm dialog before proceeding (destructive action) and displays a "Converting…" disabled state during the async operation. Updated DesignCanvas to pass the onConvertToPath handler that removes the text object, adds the new image object, and selects it. + +## Verification + +Ran `cd app && npx tsc --noEmit` — zero TypeScript errors. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.2s. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2400ms | +| 2 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 2200ms | + + +## Deviations + +Used a single onConvertToPath(textObjectId, imageObject) callback instead of separate onAddObject + onRemoveObject callbacks as the task plan suggested — simpler and keeps the replacement atomic. + +## Known Issues + +None. + +## Files Created/Modified + +- `app/src/components/canvas/ShapeProperties.tsx` +- `app/src/views/DesignCanvas.tsx` + + +## Deviations +Used a single onConvertToPath(textObjectId, imageObject) callback instead of separate onAddObject + onRemoveObject callbacks as the task plan suggested — simpler and keeps the replacement atomic. + +## Known Issues +None. diff --git a/.gsd/state-manifest.json b/.gsd/state-manifest.json index abc0a05..a851c08 100644 --- a/.gsd/state-manifest.json +++ b/.gsd/state-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "exported_at": "2026-03-26T05:55:43.881Z", + "exported_at": "2026-03-26T05:58:01.942Z", "milestones": [ { "id": "M001", @@ -1384,19 +1384,25 @@ "milestone_id": "M002", "slice_id": "S03", "id": "T03", - "title": "Build text properties panel and convert-to-paths action in ShapeProperties", - "status": "pending", - "one_liner": "", - "narrative": "", - "verification_result": "", + "title": "Added text-specific property controls (content, font family, size, letter spacing, line height) and Convert to Paths button to ShapeProperties, wired through DesignCanvas", + "status": "complete", + "one_liner": "Added text-specific property controls (content, font family, size, letter spacing, line height) and Convert to Paths button to ShapeProperties, wired through DesignCanvas", + "narrative": "Extended ShapeProperties with text-specific controls that render when the selected object has type 'text': a textarea for text content, a font family dropdown populated from fontService.getAvailableFonts(), numeric inputs for font size (8–200), letter spacing (-5–20), and line height (0.5–3). Added a \"Convert to Paths\" button that calls fontService.textToPathData() to convert the text to SVG path data, wraps it in an SVG string, creates a Blob URL, and replaces the TextObject with an ImageObject at the same position via a new onConvertToPath callback prop. The button shows a window.confirm dialog before proceeding (destructive action) and displays a \"Converting…\" disabled state during the async operation. Updated DesignCanvas to pass the onConvertToPath handler that removes the text object, adds the new image object, and selects it.", + "verification_result": "Ran `cd app && npx tsc --noEmit` — zero TypeScript errors. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.2s.", "duration": "", - "completed_at": null, + "completed_at": "2026-03-26T05:58:01.901Z", "blocker_discovered": false, - "deviations": "", - "known_issues": "", - "key_files": [], - "key_decisions": [], - "full_summary_md": "", + "deviations": "Used a single onConvertToPath(textObjectId, imageObject) callback instead of separate onAddObject + onRemoveObject callbacks as the task plan suggested — simpler and keeps the replacement atomic.", + "known_issues": "None.", + "key_files": [ + "app/src/components/canvas/ShapeProperties.tsx", + "app/src/views/DesignCanvas.tsx" + ], + "key_decisions": [ + "Convert-to-paths uses a single onConvertToPath(textObjectId, imageObject) callback rather than separate onAddObject + onRemoveObject — simpler prop interface, atomic replacement", + "Convert-to-paths generates an SVG Blob URL with the text path data and creates an ImageObject at the same position — reuses existing image rendering pipeline" + ], + "full_summary_md": "---\nid: T03\nparent: S03\nmilestone: M002\nkey_files:\n - app/src/components/canvas/ShapeProperties.tsx\n - app/src/views/DesignCanvas.tsx\nkey_decisions:\n - Convert-to-paths uses a single onConvertToPath(textObjectId, imageObject) callback rather than separate onAddObject + onRemoveObject — simpler prop interface, atomic replacement\n - Convert-to-paths generates an SVG Blob URL with the text path data and creates an ImageObject at the same position — reuses existing image rendering pipeline\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T05:58:01.911Z\nblocker_discovered: false\n---\n\n# T03: Added text-specific property controls (content, font family, size, letter spacing, line height) and Convert to Paths button to ShapeProperties, wired through DesignCanvas\n\n**Added text-specific property controls (content, font family, size, letter spacing, line height) and Convert to Paths button to ShapeProperties, wired through DesignCanvas**\n\n## What Happened\n\nExtended ShapeProperties with text-specific controls that render when the selected object has type 'text': a textarea for text content, a font family dropdown populated from fontService.getAvailableFonts(), numeric inputs for font size (8–200), letter spacing (-5–20), and line height (0.5–3). Added a \"Convert to Paths\" button that calls fontService.textToPathData() to convert the text to SVG path data, wraps it in an SVG string, creates a Blob URL, and replaces the TextObject with an ImageObject at the same position via a new onConvertToPath callback prop. The button shows a window.confirm dialog before proceeding (destructive action) and displays a \"Converting…\" disabled state during the async operation. Updated DesignCanvas to pass the onConvertToPath handler that removes the text object, adds the new image object, and selects it.\n\n## Verification\n\nRan `cd app && npx tsc --noEmit` — zero TypeScript errors. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.2s.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2400ms |\n| 2 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 2200ms |\n\n\n## Deviations\n\nUsed a single onConvertToPath(textObjectId, imageObject) callback instead of separate onAddObject + onRemoveObject callbacks as the task plan suggested — simpler and keeps the replacement atomic.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `app/src/components/canvas/ShapeProperties.tsx`\n- `app/src/views/DesignCanvas.tsx`\n", "description": "Add text-specific property controls to ShapeProperties and implement the 'Convert to Paths' action that replaces a TextObject with path-based objects using fontService.textToPathData().\n\n## Text Property Controls (in ShapeProperties)\nWhen the selected object has type 'text', show:\n1. **Text content** — textarea input bound to obj.text\n2. **Font family** — dropdown/select populated from fontService.getAvailableFonts(), bound to obj.fontFamily\n3. **Font size** — number input (min 8, max 200, step 1), bound to obj.fontSize\n4. **Letter spacing** — number input (min -5, max 20, step 0.5), bound to obj.letterSpacing\n5. **Line height** — number input (min 0.5, max 3, step 0.1), bound to obj.lineHeight\n6. **Fill color** — color input (reuse existing pattern from hasFill), bound to obj.fill\n7. **Stroke color + weight** — reuse existing stroke controls\n8. **Convert to Paths button** — calls fontService.textToPathData(), then replaces the text object with a new path object (type 'image' with SVG data URL, or a new 'path' type if simpler)\n\n## Convert to Paths Implementation\n- Show confirmation dialog (window.confirm) since this is destructive\n- Call `textToPathData(obj.text, obj.fontFamily, obj.fontSize, obj.letterSpacing)` from fontService\n- Create SVG string from returned path data: ``\n- Convert SVG to Blob URL, create ImageObject replacing the TextObject at same x,y position\n- Use onUpdate to remove old text object and add new image object (or dispatch through a callback prop)\n\n## Integration Notes\n- fontService.ts from T01 provides loadFont(), getAvailableFonts(), textToPathData()\n- The text property controls should only show when object.type === 'text'\n- Existing stroke/fill/opacity/rotation controls still show for text objects\n- ShapeProperties receives onUpdate callback from DesignCanvas — convert-to-paths needs onUpdate + onAddObject + onRemoveObject, so ShapeProperties props may need extending, or the convert action can be handled via a new callback prop\n- DesignCanvas.tsx needs to pass the additional callback(s) to ShapeProperties", "estimate": "1h", "files": [ @@ -1890,6 +1896,28 @@ "verdict": "✅ pass", "duration_ms": 2450, "created_at": "2026-03-26T05:55:43.833Z" + }, + { + "id": 36, + "task_id": "T03", + "slice_id": "S03", + "milestone_id": "M002", + "command": "cd app && npx tsc --noEmit", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 2400, + "created_at": "2026-03-26T05:58:01.901Z" + }, + { + "id": 37, + "task_id": "T03", + "slice_id": "S03", + "milestone_id": "M002", + "command": "cd app && npx vitest run --reporter=verbose", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 2200, + "created_at": "2026-03-26T05:58:01.901Z" } ] } \ No newline at end of file diff --git a/app/src/components/canvas/ShapeProperties.tsx b/app/src/components/canvas/ShapeProperties.tsx index f6bafc1..234d29b 100644 --- a/app/src/components/canvas/ShapeProperties.tsx +++ b/app/src/components/canvas/ShapeProperties.tsx @@ -3,10 +3,13 @@ * * Shows stroke color, stroke weight, fill color (with toggle), dimensions. * For line objects: line style dropdown (solid, dashed, dotted). + * For text objects: text content, font family, font size, letter spacing, + * line height, and a "Convert to Paths" action button. */ -import { useCallback } from 'react'; -import type { CanvasObject, LineStyle } from '../../types/canvas'; +import { useCallback, useState } from 'react'; +import type { CanvasObject, ImageObject, LineStyle, TextObject } from '../../types/canvas'; +import { getAvailableFonts, textToPathData } from '../../utils/fontService'; // -- Helpers ------------------------------------------------------------------ @@ -57,6 +60,8 @@ const DASH_PRESETS: Record = { export interface ShapePropertiesProps { object: CanvasObject; onUpdate: (id: string, changes: Partial) => void; + /** Called to replace a text object with an image object (convert-to-paths). */ + onConvertToPath?: (textObjectId: string, imageObject: ImageObject) => void; } // -- Component ---------------------------------------------------------------- @@ -64,10 +69,14 @@ export interface ShapePropertiesProps { export default function ShapeProperties({ object, onUpdate, + onConvertToPath, }: ShapePropertiesProps) { const hasStroke = object.type !== 'image'; const hasFill = object.type === 'rect' || object.type === 'circle' || object.type === 'ellipse' || object.type === 'text'; const isLine = object.type === 'line'; + const isText = object.type === 'text'; + + const [converting, setConverting] = useState(false); const handleChange = useCallback( (changes: Partial) => { @@ -76,6 +85,60 @@ export default function ShapeProperties({ [object.id, onUpdate], ); + /** Convert the current text object to an SVG image via fontService. */ + const handleConvertToPath = useCallback(async () => { + if (object.type !== 'text' || !onConvertToPath) return; + + const confirmed = window.confirm( + 'Convert text to paths? This replaces the editable text with a vector image and cannot be undone.', + ); + if (!confirmed) return; + + const textObj = object as TextObject; + setConverting(true); + + try { + const result = await textToPathData( + textObj.text, + textObj.fontFamily, + textObj.fontSize, + textObj.letterSpacing, + ); + + // Build an SVG string from the path data + const svgString = [ + ``, + ``, + ``, + ].join(''); + + // Create a Blob URL for the SVG + const blob = new Blob([svgString], { type: 'image/svg+xml' }); + const src = URL.createObjectURL(blob); + + const imageObj: ImageObject = { + type: 'image', + id: `path-${Date.now()}`, + name: `${textObj.name} (paths)`, + x: textObj.x, + y: textObj.y, + width: result.width, + height: result.height, + rotation: textObj.rotation, + visible: textObj.visible, + locked: textObj.locked, + opacity: textObj.opacity, + src, + }; + + onConvertToPath(textObj.id, imageObj); + } catch (err) { + console.error('Convert to paths failed:', err); + } finally { + setConverting(false); + } + }, [object, onConvertToPath]); + return (
Properties
@@ -98,6 +161,110 @@ export default function ShapeProperties({
+ {/* ---- Text-specific controls ---- */} + {isText && object.type === 'text' && ( + <> + {/* Text content */} +
+ +