From 868b444595f8a1feb2c955953a8f9adcdcbed449 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 --- app/src/components/canvas/ShapeProperties.tsx | 186 +++++++++++++++++- app/src/views/DesignCanvas.tsx | 14 +- 2 files changed, 197 insertions(+), 3 deletions(-) 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 */} +
+ +