From 2d05226ddd7ce3fa5f7fd0a8b47c41babba57a6b Mon Sep 17 00:00:00 2001 From: Hiren Kangad Date: Fri, 13 Mar 2026 00:31:44 +0530 Subject: [PATCH] fix(sticky): fixed-size creation, shared presets module, live resize sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes three review findings: 1. Sticky creation no longer zoom-adapts — always creates at M preset (28px/260w) in world space. Consistent at any zoom level. 2. Preset definitions extracted to stickyPresets.ts (shared domain module). TextFormatToolbar and tools.ts both import from there — no more canvas→component dependency inversion. 3. TextEditor.onLiveResize callback syncs spatial index and transform box as sticky background grows during typing. --- frontend/src/canvas/TextEditor.ts | 5 +++ frontend/src/canvas/stickyPresets.ts | 38 +++++++++++++++++ frontend/src/canvas/tools.ts | 15 +++---- frontend/src/components/TextFormatToolbar.tsx | 42 +++---------------- frontend/src/hooks/useCanvasSetup.ts | 3 ++ frontend/src/pages/Editor.tsx | 3 +- 6 files changed, 59 insertions(+), 47 deletions(-) create mode 100644 frontend/src/canvas/stickyPresets.ts diff --git a/frontend/src/canvas/TextEditor.ts b/frontend/src/canvas/TextEditor.ts index 6c5a56f..9206c30 100644 --- a/frontend/src/canvas/TextEditor.ts +++ b/frontend/src/canvas/TextEditor.ts @@ -19,6 +19,7 @@ export class TextEditor { private _viewport: Viewport | null = null; private _container: HTMLElement | null = null; private _onChange: (() => void) | null = null; + private _onLiveResize: ((item: SceneItem) => void) | null = null; private _originalText: string = ''; /** True when a textarea is open. */ @@ -42,6 +43,7 @@ export class TextEditor { viewport: Viewport, container: HTMLElement, onChange: () => void, + onLiveResize?: (item: SceneItem) => void, ): void { // Accept text and sticky items if (item.type !== 'text' && item.type !== 'sticky') return; @@ -53,6 +55,7 @@ export class TextEditor { this._viewport = viewport; this._container = container; this._onChange = onChange; + this._onLiveResize = onLiveResize || null; const zoom = viewport.scale.x; @@ -186,6 +189,7 @@ export class TextEditor { data.text = ta.value; item.displayObject.updateFromData(data); data.h = item.displayObject.computedHeight; + this._onLiveResize?.(item); } }; @@ -221,6 +225,7 @@ export class TextEditor { this._viewport = null; this._container = null; this._onChange = null; + this._onLiveResize = null; this._originalText = ''; if (save) { diff --git a/frontend/src/canvas/stickyPresets.ts b/frontend/src/canvas/stickyPresets.ts new file mode 100644 index 0000000..5ca47cd --- /dev/null +++ b/frontend/src/canvas/stickyPresets.ts @@ -0,0 +1,38 @@ +/** + * stickyPresets — sticky note size presets (shared domain module). + * + * Defines the S/M/L/XL/XXL presets for sticky text size and card width. + * Used by both canvas logic (tools, SceneManager) and UI (TextFormatToolbar). + * No React dependencies. + */ + +export type StickyTextSize = 'S' | 'M' | 'L' | 'XL' | 'XXL'; + +export const STICKY_SIZES: StickyTextSize[] = ['S', 'M', 'L', 'XL', 'XXL']; + +export const STICKY_FONT_MAP: Record = { + S: 20, M: 28, L: 36, XL: 48, XXL: 60, +}; + +export const STICKY_WIDTH_MAP: Record = { + S: 200, M: 260, L: 320, XL: 400, XXL: 480, +}; + +/** Default preset for new sticky creation. */ +export const DEFAULT_STICKY_PRESET: StickyTextSize = 'M'; + +/** Map a fontSize to the nearest preset label. */ +export function nearestStickySize(fontSize: number): StickyTextSize { + let best: StickyTextSize = 'M'; + let bestDist = Infinity; + for (const size of STICKY_SIZES) { + const dist = Math.abs(fontSize - STICKY_FONT_MAP[size]); + if (dist < bestDist) { bestDist = dist; best = size; } + } + return best; +} + +/** Get the card width for a given fontSize (snaps to nearest preset). */ +export function getStickyWidthForSize(fontSize: number): number { + return STICKY_WIDTH_MAP[nearestStickySize(fontSize)]; +} diff --git a/frontend/src/canvas/tools.ts b/frontend/src/canvas/tools.ts index 79ed71f..a4d4dbb 100644 --- a/frontend/src/canvas/tools.ts +++ b/frontend/src/canvas/tools.ts @@ -14,7 +14,7 @@ import { DrawingSprite } from './sprites/DrawingSprite'; import type { DrawingObject } from './scene-format'; import { TextEditor } from './TextEditor'; import { clampTextFontSize } from './textLimits'; -import { snapToStickyPreset } from '../components/TextFormatToolbar'; +import { DEFAULT_STICKY_PRESET, STICKY_FONT_MAP, STICKY_WIDTH_MAP } from './stickyPresets'; export enum ToolType { SELECT = 'SELECT', @@ -37,9 +37,6 @@ const defaultOptions: ToolOptions = { // Creation defaults — zoom-aware via screenToWorld() const DEFAULT_TEXT_SCREEN_FONT = 24; -const DEFAULT_STICKY_SCREEN_FONT = 28; -const DEFAULT_STICKY_SCREEN_WIDTH = 220; -const DEFAULT_STICKY_SCREEN_HEIGHT = 66; type CleanupFn = (() => void) | null; @@ -327,17 +324,15 @@ export function activateTool( const rect = container.getBoundingClientRect(); const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top); - const zoom = viewport.scale.x; - const preset = snapToStickyPreset(screenToWorld(DEFAULT_STICKY_SCREEN_FONT, zoom, 16, 72)); - const cardW = preset.width; - const cardH = screenToWorld(DEFAULT_STICKY_SCREEN_HEIGHT, zoom, 40, 200); + const cardW = STICKY_WIDTH_MAP[DEFAULT_STICKY_PRESET]; + const cardH = 60; // auto-computed by StickySprite const stickyData = { id: crypto.randomUUID(), type: 'sticky' as const, x: world.x - cardW / 2, y: world.y - cardH / 2, w: cardW, - h: cardH, // will be auto-computed by StickySprite + h: cardH, sx: 1, sy: 1, angle: 0, @@ -347,7 +342,7 @@ export function activateTool( name: '', visible: true, text: '', - fontSize: preset.fontSize, + fontSize: STICKY_FONT_MAP[DEFAULT_STICKY_PRESET], fontFamily: 'Inter, system-ui, sans-serif', fill: '#ffd43b', // default yellow textColor: '#1a1a1a', diff --git a/frontend/src/components/TextFormatToolbar.tsx b/frontend/src/components/TextFormatToolbar.tsx index f6e5b03..ff97031 100644 --- a/frontend/src/components/TextFormatToolbar.tsx +++ b/frontend/src/components/TextFormatToolbar.tsx @@ -1,38 +1,8 @@ import React, { useState, useRef, useEffect } from 'react'; - -type StickyTextSize = 'S' | 'M' | 'L' | 'XL' | 'XXL'; - -const STICKY_SIZES: StickyTextSize[] = ['S', 'M', 'L', 'XL', 'XXL']; -const STICKY_SIZE_MAP: Record = { S: 20, M: 28, L: 36, XL: 48, XXL: 60 }; -const STICKY_WIDTH_MAP: Record = { S: 200, M: 260, L: 320, XL: 400, XXL: 480 }; -const STICKY_SIZE_VALUES = Object.values(STICKY_SIZE_MAP); - -function nearestStickySize(fontSize: number): StickyTextSize { - let best: StickyTextSize = 'M'; - let bestDist = Infinity; - for (const size of STICKY_SIZES) { - const dist = Math.abs(fontSize - STICKY_SIZE_MAP[size]); - if (dist < bestDist) { bestDist = dist; best = size; } - } - return best; -} - -/** Snap a raw fontSize to the nearest preset. Returns { fontSize, width }. */ -export function snapToStickyPreset(fontSize: number): { fontSize: number; width: number } { - let best: StickyTextSize = 'M'; - let bestDist = Infinity; - for (const size of STICKY_SIZES) { - const dist = Math.abs(fontSize - STICKY_SIZE_MAP[size]); - if (dist < bestDist) { bestDist = dist; best = size; } - } - return { fontSize: STICKY_SIZE_MAP[best], width: STICKY_WIDTH_MAP[best] }; -} - -/** Get the width for a given sticky text size preset. */ -export function getStickyWidthForSize(fontSize: number): number { - const size = nearestStickySize(fontSize); - return STICKY_WIDTH_MAP[size]; -} +import { + STICKY_SIZES, STICKY_FONT_MAP, nearestStickySize, + type StickyTextSize, +} from '../canvas/stickyPresets'; interface TextFormatToolbarProps { kind: 'text' | 'sticky'; @@ -149,8 +119,8 @@ export default function TextFormatToolbar(props: TextFormatToolbarProps) { color: active ? '#fff' : '#666', background: active ? '#333' : 'transparent', }} - onClick={() => onStickySizeChange(STICKY_SIZE_MAP[size])} - title={`${STICKY_SIZE_MAP[size]}px text`} + onClick={() => onStickySizeChange(STICKY_FONT_MAP[size])} + title={`${STICKY_FONT_MAP[size]}px text`} onMouseEnter={(e) => { if (!active) { e.currentTarget.style.background = '#333'; e.currentTarget.style.color = '#fff'; } }} onMouseLeave={(e) => { if (!active) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#666'; } }} > diff --git a/frontend/src/hooks/useCanvasSetup.ts b/frontend/src/hooks/useCanvasSetup.ts index c3f918d..305120c 100644 --- a/frontend/src/hooks/useCanvasSetup.ts +++ b/frontend/src/hooks/useCanvasSetup.ts @@ -130,6 +130,9 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { } syncRef.current?.broadcastElements([item.id]); onCanvasChange(); + }, (resizedItem) => { + scene.updateSpatialEntry(resizedItem); + selection.transformBox.update([resizedItem]); }); }; diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index bb1ac36..b5896e7 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -20,7 +20,8 @@ import UserCursors from '../components/UserCursors'; import ContextMenu from '../components/ContextMenu'; import LayerPanel from '../components/LayerPanel'; import SelectionToolbar from '../components/SelectionToolbar'; -import TextFormatToolbar, { getStickyWidthForSize } from '../components/TextFormatToolbar'; +import TextFormatToolbar from '../components/TextFormatToolbar'; +import { getStickyWidthForSize } from '../canvas/stickyPresets'; import VideoControls from '../components/VideoControls'; import ShortcutsHelp from '../components/ShortcutsHelp'; import MattermostImport from '../components/MattermostImport';