feat: contextual text formatting toolbar + resize fontSize baking

- Remove persistent font-size slider from main toolbar (tools only)
- Lock in zoom-aware creation defaults as named constants
- Split contextual toolbar by object type: text vs sticky
  - Text: fontSize + fontFamily + text color
  - Sticky: fontSize + fontFamily + text color + note fill color
  - Mixed selection: hide toolbar
- Bake scale into fontSize on text resize (reset sx/sy to 1)
- Sticky resize stays layout-driven (no font change)
This commit is contained in:
Hiren Kangad
2026-03-12 22:47:48 +05:30
parent 83e753bb69
commit 9fac16e6a4
5 changed files with 193 additions and 70 deletions
+10 -6
View File
@@ -26,15 +26,19 @@ export enum ToolType {
export interface ToolOptions { export interface ToolOptions {
color?: string; color?: string;
strokeWidth?: number; strokeWidth?: number;
fontSize?: number;
} }
const defaultOptions: ToolOptions = { const defaultOptions: ToolOptions = {
color: '#ffffff', color: '#ffffff',
strokeWidth: 4, strokeWidth: 4,
fontSize: 24,
}; };
// Creation defaults — zoom-aware via screenToWorld()
const DEFAULT_TEXT_SCREEN_FONT = 24;
const DEFAULT_STICKY_SCREEN_FONT = 14;
const DEFAULT_STICKY_SCREEN_WIDTH = 220;
const DEFAULT_STICKY_SCREEN_HEIGHT = 66;
type CleanupFn = (() => void) | null; type CleanupFn = (() => void) | null;
/** /**
@@ -113,7 +117,7 @@ export function activateTool(
name: '', name: '',
visible: true, visible: true,
text: ' ', // placeholder — will be replaced by user input text: ' ', // placeholder — will be replaced by user input
fontSize: screenToWorld(opts.fontSize!, zoom, 10, 72), fontSize: screenToWorld(DEFAULT_TEXT_SCREEN_FONT, zoom, 10, 72),
fill: opts.color!, fill: opts.color!,
fontFamily: 'sans-serif', fontFamily: 'sans-serif',
}; };
@@ -322,8 +326,8 @@ export function activateTool(
const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top); const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top);
const zoom = viewport.scale.x; const zoom = viewport.scale.x;
const cardW = screenToWorld(220, zoom, 120, 400); const cardW = screenToWorld(DEFAULT_STICKY_SCREEN_WIDTH, zoom, 120, 400);
const cardH = screenToWorld(66, zoom, 40, 200); const cardH = screenToWorld(DEFAULT_STICKY_SCREEN_HEIGHT, zoom, 40, 200);
const stickyData = { const stickyData = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
type: 'sticky' as const, type: 'sticky' as const,
@@ -340,7 +344,7 @@ export function activateTool(
name: '', name: '',
visible: true, visible: true,
text: '', text: '',
fontSize: screenToWorld(opts.fontSize!, zoom, 10, 48), fontSize: screenToWorld(DEFAULT_STICKY_SCREEN_FONT, zoom, 10, 48),
fontFamily: 'Inter, system-ui, sans-serif', fontFamily: 'Inter, system-ui, sans-serif',
fill: '#ffd43b', // default yellow fill: '#ffd43b', // default yellow
textColor: '#1a1a1a', textColor: '#1a1a1a',
+67 -4
View File
@@ -1,15 +1,21 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
interface TextFormatToolbarProps { interface TextFormatToolbarProps {
kind: 'text' | 'sticky';
x: number; x: number;
y: number; y: number;
fontSize: number; fontSize: number;
fontFamily: string; fontFamily: string;
/** Text color (both text and sticky). */
fill: string; fill: string;
/** Note background color (sticky only). */
noteFill?: string;
position?: 'above' | 'below'; position?: 'above' | 'below';
onFontSizeChange: (size: number) => void; onFontSizeChange: (size: number) => void;
onFontFamilyChange: (family: string) => void; onFontFamilyChange: (family: string) => void;
onFillChange: (color: string) => void; onFillChange: (color: string) => void;
/** Called when sticky note background color changes. */
onNoteFillChange?: (color: string) => void;
} }
const FONT_FAMILIES = [ const FONT_FAMILIES = [
@@ -29,14 +35,15 @@ const PRESET_COLORS = [
]; ];
export default function TextFormatToolbar(props: TextFormatToolbarProps) { export default function TextFormatToolbar(props: TextFormatToolbarProps) {
const { x, y, fontSize, fontFamily, fill, position = 'above', onFontSizeChange, onFontFamilyChange, onFillChange } = props; const { kind, x, y, fontSize, fontFamily, fill, noteFill, position = 'above', onFontSizeChange, onFontFamilyChange, onFillChange, onNoteFillChange } = props;
const [showFontMenu, setShowFontMenu] = useState(false); const [showFontMenu, setShowFontMenu] = useState(false);
const [showColorPicker, setShowColorPicker] = useState(false); const [showColorPicker, setShowColorPicker] = useState(false);
const [showNoteFillPicker, setShowNoteFillPicker] = useState(false);
const colorInputRef = useRef<HTMLInputElement>(null); const colorInputRef = useRef<HTMLInputElement>(null);
// Close dropdowns on outside click // Close dropdowns on outside click
useEffect(() => { useEffect(() => {
const onDown = () => { setShowFontMenu(false); setShowColorPicker(false); }; const onDown = () => { setShowFontMenu(false); setShowColorPicker(false); setShowNoteFillPicker(false); };
window.addEventListener('pointerdown', onDown); window.addEventListener('pointerdown', onDown);
return () => window.removeEventListener('pointerdown', onDown); return () => window.removeEventListener('pointerdown', onDown);
}, []); }, []);
@@ -174,11 +181,11 @@ export default function TextFormatToolbar(props: TextFormatToolbarProps) {
{/* Divider */} {/* Divider */}
<div style={{ width: '1px', height: '18px', background: '#333', margin: '0 2px', flexShrink: 0 }} /> <div style={{ width: '1px', height: '18px', background: '#333', margin: '0 2px', flexShrink: 0 }} />
{/* Color */} {/* Text color */}
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<button <button
style={{ ...btnStyle, width: '26px', padding: 0 }} style={{ ...btnStyle, width: '26px', padding: 0 }}
onClick={(e) => { e.stopPropagation(); setShowColorPicker((v) => !v); setShowFontMenu(false); }} onClick={(e) => { e.stopPropagation(); setShowColorPicker((v) => !v); setShowFontMenu(false); setShowNoteFillPicker(false); }}
title="Text color" title="Text color"
{...hoverHandlers} {...hoverHandlers}
> >
@@ -227,6 +234,62 @@ export default function TextFormatToolbar(props: TextFormatToolbarProps) {
</div> </div>
)} )}
</div> </div>
{/* Note fill color (sticky only) */}
{kind === 'sticky' && noteFill != null && onNoteFillChange && (<>
<div style={{ width: '1px', height: '18px', background: '#333', margin: '0 2px', flexShrink: 0 }} />
<div style={{ position: 'relative' }}>
<button
style={{ ...btnStyle, width: '26px', padding: 0 }}
onClick={(e) => { e.stopPropagation(); setShowNoteFillPicker((v) => !v); setShowFontMenu(false); setShowColorPicker(false); }}
title="Note color"
{...hoverHandlers}
>
<div style={{ width: '14px', height: '14px', borderRadius: '6px', background: noteFill, border: '1px solid #555' }} />
</button>
{showNoteFillPicker && (
<div
style={{
position: 'absolute',
top: '100%',
right: 0,
marginTop: '4px',
background: 'rgba(22, 22, 22, 0.98)',
border: '1px solid #333',
borderRadius: '8px',
padding: '8px',
zIndex: 200,
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
}}
onPointerDown={(e) => e.stopPropagation()}
>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '4px', marginBottom: '8px' }}>
{PRESET_COLORS.map((c) => (
<button
key={c}
onClick={() => { onNoteFillChange(c); setShowNoteFillPicker(false); }}
style={{
width: '22px',
height: '22px',
borderRadius: '4px',
background: c,
border: c === noteFill ? '2px solid #4a90d9' : '1px solid #444',
cursor: 'pointer',
padding: 0,
}}
/>
))}
</div>
<input
type="color"
value={noteFill}
onChange={(e) => { onNoteFillChange(e.target.value); }}
style={{ width: '100%', height: '24px', border: 'none', background: 'transparent', cursor: 'pointer' }}
/>
</div>
)}
</div>
</>)}
</div> </div>
); );
} }
-19
View File
@@ -15,8 +15,6 @@ interface ToolbarProps {
onColorChange: (color: string) => void; onColorChange: (color: string) => void;
strokeWidth: number; strokeWidth: number;
onStrokeWidthChange: (width: number) => void; onStrokeWidthChange: (width: number) => void;
fontSize: number;
onFontSizeChange: (size: number) => void;
zoom: number; zoom: number;
onFitAll: () => void; onFitAll: () => void;
onZoomIn?: () => void; onZoomIn?: () => void;
@@ -146,8 +144,6 @@ export default function Toolbar({
onColorChange, onColorChange,
strokeWidth, strokeWidth,
onStrokeWidthChange, onStrokeWidthChange,
fontSize,
onFontSizeChange,
zoom, zoom,
onFitAll, onFitAll,
onZoomIn, onZoomIn,
@@ -169,7 +165,6 @@ export default function Toolbar({
reviewMode, reviewMode,
}: ToolbarProps) { }: ToolbarProps) {
const showStroke = activeTool === ToolType.PEN; const showStroke = activeTool === ToolType.PEN;
const showFontSize = activeTool === ToolType.TEXT || activeTool === ToolType.STICKY;
// Determine active hint // Determine active hint
const activeToolDef = toolButtons.find((t) => t.tool === activeTool); const activeToolDef = toolButtons.find((t) => t.tool === activeTool);
@@ -274,20 +269,6 @@ export default function Toolbar({
</> </>
)} )}
{/* Font size (text) */}
{showFontSize && (
<>
<Divider />
<span style={{ fontSize: '10px', color: '#555', marginLeft: '4px' }}>Size</span>
<input
type="range" min={12} max={72} value={fontSize}
onChange={(e) => onFontSizeChange(Number(e.target.value))}
style={{ width: '60px', height: '3px', accentColor: '#4a9eff', cursor: 'pointer' }}
/>
<span style={{ fontSize: '10px', color: '#666', minWidth: '18px', textAlign: 'center' }}>{fontSize}</span>
</>
)}
<Divider /> <Divider />
{/* Undo/Redo */} {/* Undo/Redo */}
+21
View File
@@ -13,6 +13,7 @@ import { UploadManager } from '../stores/uploadManager';
import { AnnotationStore } from '../stores/annotationStore'; import { AnnotationStore } from '../stores/annotationStore';
import { PinOverlay } from '../canvas/PinOverlay'; import { PinOverlay } from '../canvas/PinOverlay';
import { CropOverlay } from '../canvas/CropOverlay'; import { CropOverlay } from '../canvas/CropOverlay';
import { TextSprite } from '../canvas/sprites/TextSprite';
// PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit // PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit
import { connectSocket, disconnectSocket } from '../socket'; import { connectSocket, disconnectSocket } from '../socket';
import api from '../api'; import api from '../api';
@@ -194,6 +195,26 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
onCanvasChange(itemIds); // broadcasts elements + saves + undo + spatial refresh onCanvasChange(itemIds); // broadcasts elements + saves + undo + spatial refresh
}; };
selection.transformBox.onDragEnd = (itemIds) => { selection.transformBox.onDragEnd = (itemIds) => {
// Bake scale into fontSize for plain text items after resize
for (const id of itemIds) {
const item = scene.getById(id);
if (!item || item.type !== 'text') continue;
const absSx = Math.abs(item.data.sx);
const absSy = Math.abs(item.data.sy);
if (absSx === 1 && absSy === 1) continue;
// Use average scale as font multiplier
const scale = (absSx + absSy) / 2;
const d = item.data as any;
d.fontSize = Math.round(d.fontSize * scale);
d.sx = item.data.sx > 0 ? 1 : -1;
d.sy = item.data.sy > 0 ? 1 : -1;
if (item.displayObject instanceof TextSprite) {
item.displayObject.updateFromData(d);
d.w = item.displayObject.measuredWidth;
d.h = item.displayObject.measuredHeight;
}
item.displayObject.scale.set(d.sx, d.sy);
}
onCanvasChange(itemIds); onCanvasChange(itemIds);
}; };
+95 -41
View File
@@ -35,9 +35,10 @@ import { InboxZone } from '../canvas/InboxZone';
import { getItemWorldBounds } from '../canvas/SceneManager'; import { getItemWorldBounds } from '../canvas/SceneManager';
import { getPointAnchorWorld } from '../canvas/reviewAnchors'; import { getPointAnchorWorld } from '../canvas/reviewAnchors';
import { resolveReviewTargetAtPoint } from '../canvas/reviewTargeting'; import { resolveReviewTargetAtPoint } from '../canvas/reviewTargeting';
import type { TextObject } from '../canvas/scene-format'; import type { TextObject, StickyObject } from '../canvas/scene-format';
import { VideoSprite } from '../canvas/sprites/VideoSprite'; import { VideoSprite } from '../canvas/sprites/VideoSprite';
import { TextSprite } from '../canvas/sprites/TextSprite'; import { TextSprite } from '../canvas/sprites/TextSprite';
import { StickySprite } from '../canvas/sprites/StickySprite';
import * as ops from '../canvas/operations'; import * as ops from '../canvas/operations';
// Hooks // Hooks
@@ -91,7 +92,6 @@ export default function Editor({ isPublicView }: EditorProps) {
const [activeTool, setActiveTool] = useState<ToolType>(ToolType.SELECT); const [activeTool, setActiveTool] = useState<ToolType>(ToolType.SELECT);
const [color, setColor] = useState('#ffffff'); const [color, setColor] = useState('#ffffff');
const [strokeWidth, setStrokeWidth] = useState(4); const [strokeWidth, setStrokeWidth] = useState(4);
const [fontSize, setFontSize] = useState(24);
const [zoom, setZoom] = useState(1); const [zoom, setZoom] = useState(1);
const [objectCount, setObjectCount] = useState(0); const [objectCount, setObjectCount] = useState(0);
const [saveStatus, setSaveStatus] = useState<SaveStatus>('saved'); const [saveStatus, setSaveStatus] = useState<SaveStatus>('saved');
@@ -121,7 +121,11 @@ export default function Editor({ isPublicView }: EditorProps) {
const [layerList, setLayerList] = useState<any[]>([]); const [layerList, setLayerList] = useState<any[]>([]);
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]); const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
const [selToolbar, setSelToolbar] = useState<{ x: number; y: number; count: number } | null>(null); const [selToolbar, setSelToolbar] = useState<{ x: number; y: number; count: number } | null>(null);
const [textToolbar, setTextToolbar] = useState<{ x: number; y: number; fontSize: number; fontFamily: string; fill: string; items: SceneItem[] } | null>(null); const [textToolbar, setTextToolbar] = useState<
| { kind: 'text'; x: number; y: number; fontSize: number; fontFamily: string; fill: string; items: SceneItem[] }
| { kind: 'sticky'; x: number; y: number; fontSize: number; fontFamily: string; textColor: string; fill: string; items: SceneItem[] }
| null
>(null);
const [videoCtrl, setVideoCtrl] = useState<{ videoSprite: VideoSprite; screenRect: { x: number; y: number; w: number; h: number } } | null>(null); const [videoCtrl, setVideoCtrl] = useState<{ videoSprite: VideoSprite; screenRect: { x: number; y: number; w: number; h: number } } | null>(null);
const [showMinimap, setShowMinimap] = useState(true); const [showMinimap, setShowMinimap] = useState(true);
const [minimapData, setMinimapData] = useState<{ items: any[]; viewportBounds: any; contentBounds: any }>({ const [minimapData, setMinimapData] = useState<{ items: any[]; viewportBounds: any; contentBounds: any }>({
@@ -436,8 +440,8 @@ export default function Editor({ isPublicView }: EditorProps) {
}; };
// reviewMode is read at click time via getter so it stays current // reviewMode is read at click time via getter so it stays current
Object.defineProperty(ctx, 'reviewMode', { get: () => reviewModeRef.current }); Object.defineProperty(ctx, 'reviewMode', { get: () => reviewModeRef.current });
toolCleanupRef.current = activateTool(ctx, activeTool, { color, strokeWidth, fontSize }); toolCleanupRef.current = activateTool(ctx, activeTool, { color, strokeWidth });
}, [activeTool, color, strokeWidth, fontSize, onCanvasChange, textEditor]); }, [activeTool, color, strokeWidth, onCanvasChange, textEditor]);
// Layer panel // Layer panel
const { refreshLayers: refreshLayerData, layerHandlers } = useLayerPanel({ canvasRef, selectionRef, onCanvasChange }); const { refreshLayers: refreshLayerData, layerHandlers } = useLayerPanel({ canvasRef, selectionRef, onCanvasChange });
@@ -587,13 +591,16 @@ export default function Editor({ isPublicView }: EditorProps) {
setVideoCtrl(null); setVideoCtrl(null);
} }
// Text format toolbar: show when all selected items are text (1 or more) // Contextual format toolbar: show when all selected items are the same text-like type
const textItems = items.filter((it) => it.type === 'text'); const textItems = items.filter((it) => it.type === 'text');
if (textItems.length > 0 && textItems.length === items.length) { const stickyItems = items.filter((it) => it.type === 'sticky');
// Use first item's properties as representative const formatItems = textItems.length === items.length ? textItems
const td = textItems[0].data as TextObject; : stickyItems.length === items.length ? stickyItems
: null; // mixed or non-text selection → hide
if (formatItems && formatItems.length > 0) {
let minX2 = Infinity, minY2 = Infinity, maxX2 = -Infinity, maxY2 = -Infinity; let minX2 = Infinity, minY2 = Infinity, maxX2 = -Infinity, maxY2 = -Infinity;
for (const item of textItems) { for (const item of formatItems) {
const b = getItemWorldBounds(item); const b = getItemWorldBounds(item);
if (b.x < minX2) minX2 = b.x; if (b.x < minX2) minX2 = b.x;
if (b.y < minY2) minY2 = b.y; if (b.y < minY2) minY2 = b.y;
@@ -604,16 +611,26 @@ export default function Editor({ isPublicView }: EditorProps) {
const screenTR2 = vp.toScreen(maxX2, minY2); const screenTR2 = vp.toScreen(maxX2, minY2);
const screenBL2 = vp.toScreen(minX2, maxY2); const screenBL2 = vp.toScreen(minX2, maxY2);
const screenBR2 = vp.toScreen(maxX2, maxY2); const screenBR2 = vp.toScreen(maxX2, maxY2);
// Single text: show above; multiple: show below (selection toolbar is above) const useBottom = formatItems.length >= 2;
const useBottom = textItems.length >= 2; const posX = useBottom ? (screenBL2.x + screenBR2.x) / 2 : (screenTL2.x + screenTR2.x) / 2;
setTextToolbar({ const posY = useBottom ? (screenBL2.y + screenBR2.y) / 2 + 8 : screenTL2.y;
x: useBottom ? (screenBL2.x + screenBR2.x) / 2 : (screenTL2.x + screenTR2.x) / 2,
y: useBottom ? (screenBL2.y + screenBR2.y) / 2 + 8 : screenTL2.y, if (formatItems === textItems) {
fontSize: td.fontSize, const td = textItems[0].data as TextObject;
fontFamily: td.fontFamily, setTextToolbar({
fill: td.fill, kind: 'text', x: posX, y: posY,
items: textItems, fontSize: td.fontSize, fontFamily: td.fontFamily, fill: td.fill,
}); items: textItems,
});
} else {
const sd = stickyItems[0].data as StickyObject;
setTextToolbar({
kind: 'sticky', x: posX, y: posY,
fontSize: sd.fontSize || 14, fontFamily: sd.fontFamily || 'Inter, system-ui, sans-serif',
textColor: sd.textColor || '#1a1a1a', fill: sd.fill || '#ffd43b',
items: stickyItems,
});
}
} else { } else {
setTextToolbar(null); setTextToolbar(null);
} }
@@ -767,8 +784,6 @@ export default function Editor({ isPublicView }: EditorProps) {
onColorChange={setColor} onColorChange={setColor}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
onStrokeWidthChange={setStrokeWidth} onStrokeWidthChange={setStrokeWidth}
fontSize={fontSize}
onFontSizeChange={setFontSize}
zoom={zoom} zoom={zoom}
onFitAll={() => canvasRef.current?.fitAll()} onFitAll={() => canvasRef.current?.fitAll()}
canUndo={canUndo} canUndo={canUndo}
@@ -891,24 +906,35 @@ export default function Editor({ isPublicView }: EditorProps) {
/> />
)} )}
{/* Text format toolbar (floating, above selected text items) */} {/* Contextual format toolbar (floating, above selected text/sticky items) */}
{textToolbar && activeTool === ToolType.SELECT && !contextMenu && ( {textToolbar && activeTool === ToolType.SELECT && !contextMenu && (
<TextFormatToolbar <TextFormatToolbar
kind={textToolbar.kind}
x={textToolbar.x} x={textToolbar.x}
y={textToolbar.y} y={textToolbar.y}
fontSize={textToolbar.fontSize} fontSize={textToolbar.fontSize}
fontFamily={textToolbar.fontFamily} fontFamily={textToolbar.fontFamily}
fill={textToolbar.fill} fill={textToolbar.kind === 'sticky' ? textToolbar.textColor : textToolbar.fill}
noteFill={textToolbar.kind === 'sticky' ? textToolbar.fill : undefined}
position={textToolbar.items.length >= 2 ? 'below' : 'above'} position={textToolbar.items.length >= 2 ? 'below' : 'above'}
onFontSizeChange={(size) => { onFontSizeChange={(size) => {
const scene = canvasRef.current?.getScene(); const scene = canvasRef.current?.getScene();
for (const item of textToolbar.items) { for (const item of textToolbar.items) {
const d = item.data as TextObject; if (textToolbar.kind === 'text') {
d.fontSize = size; const d = item.data as TextObject;
if (item.displayObject instanceof TextSprite) { d.fontSize = size;
item.displayObject.updateFromData(d); if (item.displayObject instanceof TextSprite) {
d.w = item.displayObject.measuredWidth; item.displayObject.updateFromData(d);
d.h = item.displayObject.measuredHeight; d.w = item.displayObject.measuredWidth;
d.h = item.displayObject.measuredHeight;
}
} else {
const d = item.data as StickyObject;
d.fontSize = size;
if (item.displayObject instanceof StickySprite) {
item.displayObject.updateFromData(d);
d.h = item.displayObject.computedHeight;
}
} }
if (scene) scene.updateSpatialEntry(item); if (scene) scene.updateSpatialEntry(item);
} }
@@ -919,12 +945,21 @@ export default function Editor({ isPublicView }: EditorProps) {
onFontFamilyChange={(family) => { onFontFamilyChange={(family) => {
const scene = canvasRef.current?.getScene(); const scene = canvasRef.current?.getScene();
for (const item of textToolbar.items) { for (const item of textToolbar.items) {
const d = item.data as TextObject; if (textToolbar.kind === 'text') {
d.fontFamily = family; const d = item.data as TextObject;
if (item.displayObject instanceof TextSprite) { d.fontFamily = family;
item.displayObject.updateFromData(d); if (item.displayObject instanceof TextSprite) {
d.w = item.displayObject.measuredWidth; item.displayObject.updateFromData(d);
d.h = item.displayObject.measuredHeight; d.w = item.displayObject.measuredWidth;
d.h = item.displayObject.measuredHeight;
}
} else {
const d = item.data as StickyObject;
d.fontFamily = family;
if (item.displayObject instanceof StickySprite) {
item.displayObject.updateFromData(d);
d.h = item.displayObject.computedHeight;
}
} }
if (scene) scene.updateSpatialEntry(item); if (scene) scene.updateSpatialEntry(item);
} }
@@ -932,17 +967,36 @@ export default function Editor({ isPublicView }: EditorProps) {
onCanvasChange(textToolbar.items.map(i => i.id)); onCanvasChange(textToolbar.items.map(i => i.id));
updateOverlays(); updateOverlays();
}} }}
onFillChange={(color) => { onFillChange={(textColor) => {
for (const item of textToolbar.items) { for (const item of textToolbar.items) {
const d = item.data as TextObject; if (textToolbar.kind === 'text') {
d.fill = color; const d = item.data as TextObject;
if (item.displayObject instanceof TextSprite) { d.fill = textColor;
item.displayObject.updateFromData(d); if (item.displayObject instanceof TextSprite) {
item.displayObject.updateFromData(d);
}
} else {
const d = item.data as StickyObject;
d.textColor = textColor;
if (item.displayObject instanceof StickySprite) {
item.displayObject.updateFromData(d);
}
} }
} }
onCanvasChange(textToolbar.items.map(i => i.id)); onCanvasChange(textToolbar.items.map(i => i.id));
updateOverlays(); updateOverlays();
}} }}
onNoteFillChange={textToolbar.kind === 'sticky' ? (fillColor) => {
for (const item of textToolbar.items) {
const d = item.data as StickyObject;
d.fill = fillColor;
if (item.displayObject instanceof StickySprite) {
item.displayObject.updateFromData(d);
}
}
onCanvasChange(textToolbar.items.map(i => i.id));
updateOverlays();
} : undefined}
/> />
)} )}