feat: Added text-specific property controls (content, font family, size…
- "app/src/components/canvas/ShapeProperties.tsx" - "app/src/views/DesignCanvas.tsx" GSD-Task: S03/T03
This commit is contained in:
parent
40690e9c80
commit
46169ecf02
7 changed files with 347 additions and 16 deletions
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
30
.gsd/milestones/M002/slices/S03/tasks/T02-VERIFY.json
Normal file
30
.gsd/milestones/M002/slices/S03/tasks/T02-VERIFY.json
Normal file
|
|
@ -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
|
||||
}
|
||||
78
.gsd/milestones/M002/slices/S03/tasks/T03-SUMMARY.md
Normal file
78
.gsd/milestones/M002/slices/S03/tasks/T03-SUMMARY.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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: `<svg xmlns=... viewBox=...><path d=\"{pathData}\" fill=\"{obj.fill}\" stroke=\"{obj.stroke}\" stroke-width=\"{obj.strokeWidth}\"/></svg>`\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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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<LineStyle, number[]> = {
|
|||
export interface ShapePropertiesProps {
|
||||
object: CanvasObject;
|
||||
onUpdate: (id: string, changes: Partial<CanvasObject>) => 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<CanvasObject>) => {
|
||||
|
|
@ -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 = [
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${result.width} ${result.height}" width="${result.width}" height="${result.height}">`,
|
||||
`<path d="${result.pathData}" fill="${textObj.fill}" stroke="${textObj.stroke}" stroke-width="${textObj.strokeWidth}"/>`,
|
||||
`</svg>`,
|
||||
].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 (
|
||||
<div className="shape-properties" data-testid="shape-properties">
|
||||
<div className="shape-properties-header">Properties</div>
|
||||
|
|
@ -98,6 +161,110 @@ export default function ShapeProperties({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- Text-specific controls ---- */}
|
||||
{isText && object.type === 'text' && (
|
||||
<>
|
||||
{/* Text content */}
|
||||
<div className="shape-prop-section">
|
||||
<label className="shape-prop-label" htmlFor="text-content">
|
||||
Text
|
||||
</label>
|
||||
<textarea
|
||||
id="text-content"
|
||||
className="shape-prop-textarea"
|
||||
rows={3}
|
||||
value={object.text}
|
||||
onChange={(e) =>
|
||||
handleChange({ text: e.target.value } as Partial<CanvasObject>)
|
||||
}
|
||||
data-testid="text-content-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Font family */}
|
||||
<div className="shape-prop-section">
|
||||
<label className="shape-prop-label" htmlFor="font-family">
|
||||
Font Family
|
||||
</label>
|
||||
<select
|
||||
id="font-family"
|
||||
className="shape-prop-select"
|
||||
value={object.fontFamily}
|
||||
onChange={(e) =>
|
||||
handleChange({ fontFamily: e.target.value } as Partial<CanvasObject>)
|
||||
}
|
||||
data-testid="font-family-select"
|
||||
>
|
||||
{getAvailableFonts().map((f) => (
|
||||
<option key={f.family} value={f.family}>
|
||||
{f.family}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Font size */}
|
||||
<div className="shape-prop-section">
|
||||
<label className="shape-prop-label" htmlFor="font-size">
|
||||
Font Size
|
||||
</label>
|
||||
<input
|
||||
id="font-size"
|
||||
type="number"
|
||||
className="shape-prop-number-input"
|
||||
min={8}
|
||||
max={200}
|
||||
step={1}
|
||||
value={object.fontSize}
|
||||
onChange={(e) =>
|
||||
handleChange({ fontSize: Number(e.target.value) } as Partial<CanvasObject>)
|
||||
}
|
||||
data-testid="font-size-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Letter spacing */}
|
||||
<div className="shape-prop-section">
|
||||
<label className="shape-prop-label" htmlFor="letter-spacing">
|
||||
Letter Spacing
|
||||
</label>
|
||||
<input
|
||||
id="letter-spacing"
|
||||
type="number"
|
||||
className="shape-prop-number-input"
|
||||
min={-5}
|
||||
max={20}
|
||||
step={0.5}
|
||||
value={object.letterSpacing}
|
||||
onChange={(e) =>
|
||||
handleChange({ letterSpacing: Number(e.target.value) } as Partial<CanvasObject>)
|
||||
}
|
||||
data-testid="letter-spacing-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Line height */}
|
||||
<div className="shape-prop-section">
|
||||
<label className="shape-prop-label" htmlFor="line-height">
|
||||
Line Height
|
||||
</label>
|
||||
<input
|
||||
id="line-height"
|
||||
type="number"
|
||||
className="shape-prop-number-input"
|
||||
min={0.5}
|
||||
max={3}
|
||||
step={0.1}
|
||||
value={object.lineHeight}
|
||||
onChange={(e) =>
|
||||
handleChange({ lineHeight: Number(e.target.value) } as Partial<CanvasObject>)
|
||||
}
|
||||
data-testid="line-height-input"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Stroke color */}
|
||||
{hasStroke && (
|
||||
<div className="shape-prop-section">
|
||||
|
|
@ -231,6 +398,21 @@ export default function ShapeProperties({
|
|||
<span>{Math.round(object.rotation)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Convert to Paths (text objects only) */}
|
||||
{isText && onConvertToPath && (
|
||||
<div className="shape-prop-section">
|
||||
<button
|
||||
type="button"
|
||||
className="shape-prop-convert-btn"
|
||||
onClick={handleConvertToPath}
|
||||
disabled={converting}
|
||||
data-testid="convert-to-paths-btn"
|
||||
>
|
||||
{converting ? 'Converting…' : 'Convert to Paths'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type Konva from 'konva';
|
||||
import type { TraceMetadata } from '../types/engine';
|
||||
import type { ArtboardConfig, CanvasObject } from '../types/canvas';
|
||||
import type { ArtboardConfig, CanvasObject, ImageObject } from '../types/canvas';
|
||||
import { useCanvasState } from '../hooks/useCanvasState';
|
||||
import ArtboardSetup from '../components/canvas/ArtboardSetup';
|
||||
import KonvaStage from '../components/canvas/KonvaStage';
|
||||
|
|
@ -223,6 +223,17 @@ export default function DesignCanvas({
|
|||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [undo, redo, removeObject, selectObjects, deselectAll, state.objects, state.selectedIds]);
|
||||
|
||||
// -- Convert text to paths ------------------------------------------------
|
||||
|
||||
const handleConvertToPath = useCallback(
|
||||
(textObjectId: string, imageObject: ImageObject) => {
|
||||
removeObject(textObjectId);
|
||||
addObject(imageObject);
|
||||
selectObjects([imageObject.id]);
|
||||
},
|
||||
[removeObject, addObject, selectObjects],
|
||||
);
|
||||
|
||||
// -- Selected object for properties panel ---------------------------------
|
||||
|
||||
const selectedObject = useMemo(() => {
|
||||
|
|
@ -305,6 +316,7 @@ export default function DesignCanvas({
|
|||
<ShapeProperties
|
||||
object={selectedObject}
|
||||
onUpdate={updateObject}
|
||||
onConvertToPath={handleConvertToPath}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue