From b081ed71690935d5bc32c1574411f1b287ffc246 Mon Sep 17 00:00:00 2001 From: Hiren Kangad Date: Fri, 13 Mar 2026 00:18:41 +0530 Subject: [PATCH] refactor(sticky): fixed-width card with S/M/L/XL/XXL font presets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stickies are now fixed-width cards — no resize handles shown. Text size controlled via 5 presets (10/14/18/24/32px). On creation, zoom-aware screenToWorld snaps to nearest preset. Height auto-adjusts from content. Plain text resize unchanged. Removes: sticky width bake, live reflow drag, min width constraint. Backward compatible — existing stickies map to nearest preset. --- frontend/src/canvas/TransformBox.ts | 32 +++--------------- frontend/src/canvas/textLimits.ts | 14 +++----- frontend/src/canvas/tools.ts | 5 +-- frontend/src/components/TextFormatToolbar.tsx | 33 ++++++++++++++----- frontend/src/hooks/useCanvasSetup.ts | 13 -------- 5 files changed, 37 insertions(+), 60 deletions(-) diff --git a/frontend/src/canvas/TransformBox.ts b/frontend/src/canvas/TransformBox.ts index 3ca7c36..fae7cd8 100644 --- a/frontend/src/canvas/TransformBox.ts +++ b/frontend/src/canvas/TransformBox.ts @@ -9,7 +9,6 @@ import { Container, Graphics, FederatedPointerEvent, Text, TextStyle } from 'pixi.js'; import type { Viewport } from 'pixi-viewport'; import { type SceneItem, getItemWorldBounds } from './SceneManager'; -import { StickySprite } from './sprites/StickySprite'; import type { SnapGuides } from './SnapGuides'; // --------------------------------------------------------------------------- @@ -21,7 +20,6 @@ const BORDER_WIDTH = 1.5; const HANDLE_SIZE = 8; const HANDLE_FILL = 0xffffff; const HANDLE_STROKE = 0x4a90d9; -const MIN_STICKY_WIDTH = 80; type HandleId = 'tl' | 'tc' | 'tr' | 'ml' | 'mr' | 'bl' | 'bc' | 'br'; @@ -199,11 +197,9 @@ export class TransformBox extends Container { const s = HANDLE_SIZE / zoom; const half = s / 2; - // Vertical-only handles hidden for sticky-only selections (width-only resize) - const hideVertical = this._stickyOnly; - for (const [id, handle] of this._handles) { - if (hideVertical && (id === 'tc' || id === 'bc')) { + // No resize handles for sticky-only selections (fixed width, auto height) + if (this._stickyOnly) { handle.visible = false; handle.eventMode = 'none'; continue; @@ -329,11 +325,6 @@ export class TransformBox extends Container { } } - // Sticky-only: horizontal resize only — fy stays 1 - if (this._stickyOnly) { - fy = 1; - } - // Compute the anchor point (fixed edge of bounding box) let anchorX = ob.x; // default: top-left is fixed (br, mr, bc handles) let anchorY = ob.y; @@ -354,22 +345,9 @@ export class TransformBox extends Container { item.data.x = anchorX + (orig.x - anchorX) * fx; item.data.y = anchorY + (orig.y - anchorY) * fy; - if (this._stickyOnly && item.type === 'sticky') { - // Live reflow: bake width directly so text re-wraps every frame - item.data.w = Math.max(MIN_STICKY_WIDTH, Math.round(orig.w * fx)); - item.data.sx = orig.sx > 0 ? 1 : -1; - item.data.sy = orig.sy > 0 ? 1 : -1; - if (item.displayObject instanceof StickySprite) { - item.displayObject.updateFromData(item.data as any); - item.data.h = item.displayObject.computedHeight; - } - item.displayObject.scale.set(item.data.sx, item.data.sy); - } else { - // Normal proportional scale - item.data.sx = orig.sx * fx; - item.data.sy = orig.sy * fy; - item.displayObject.scale.set(item.data.sx, item.data.sy); - } + item.data.sx = orig.sx * fx; + item.data.sy = orig.sy * fy; + item.displayObject.scale.set(item.data.sx, item.data.sy); item.displayObject.position.set(item.data.x, item.data.y); this._onItemTransform?.(item); diff --git a/frontend/src/canvas/textLimits.ts b/frontend/src/canvas/textLimits.ts index 3036e02..b72c06c 100644 --- a/frontend/src/canvas/textLimits.ts +++ b/frontend/src/canvas/textLimits.ts @@ -1,23 +1,17 @@ /** - * textLimits — single source of truth for font-size constraints. + * textLimits — font-size constraints for plain text items. * * Used by creation defaults to keep initial sizes reasonable. * Direct manipulation (resize-bake) intentionally bypasses these * clamps so users can scale text to any size. + * + * Sticky notes use discrete presets (S/M/L/XL/XXL) defined in + * TextFormatToolbar.tsx — not continuous clamps. */ -// Plain text: generous range for direct manipulation const TEXT_FONT_MIN = 6; const TEXT_FONT_MAX = 160; -// Sticky note text: narrower range (card layout breaks at extremes) -const STICKY_FONT_MIN = 8; -const STICKY_FONT_MAX = 96; - export function clampTextFontSize(size: number): number { return Math.round(Math.min(TEXT_FONT_MAX, Math.max(TEXT_FONT_MIN, size))); } - -export function clampStickyFontSize(size: number): number { - return Math.round(Math.min(STICKY_FONT_MAX, Math.max(STICKY_FONT_MIN, size))); -} diff --git a/frontend/src/canvas/tools.ts b/frontend/src/canvas/tools.ts index a4ddb47..dc6894b 100644 --- a/frontend/src/canvas/tools.ts +++ b/frontend/src/canvas/tools.ts @@ -13,7 +13,8 @@ import type { SelectionManager } from './SelectionManager'; import { DrawingSprite } from './sprites/DrawingSprite'; import type { DrawingObject } from './scene-format'; import { TextEditor } from './TextEditor'; -import { clampTextFontSize, clampStickyFontSize } from './textLimits'; +import { clampTextFontSize } from './textLimits'; +import { snapToStickyPreset } from '../components/TextFormatToolbar'; export enum ToolType { SELECT = 'SELECT', @@ -345,7 +346,7 @@ export function activateTool( name: '', visible: true, text: '', - fontSize: clampStickyFontSize(screenToWorld(DEFAULT_STICKY_SCREEN_FONT, zoom, 8, 96)), + fontSize: snapToStickyPreset(screenToWorld(DEFAULT_STICKY_SCREEN_FONT, zoom, 8, 40)), 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 9c00050..dc61e48 100644 --- a/frontend/src/components/TextFormatToolbar.tsx +++ b/frontend/src/components/TextFormatToolbar.tsx @@ -1,13 +1,30 @@ import React, { useState, useRef, useEffect } from 'react'; -type StickyTextSize = 'S' | 'M' | 'L'; +type StickyTextSize = 'S' | 'M' | 'L' | 'XL' | 'XXL'; -const STICKY_SIZE_MAP: Record = { S: 12, M: 14, L: 18 }; +const STICKY_SIZES: StickyTextSize[] = ['S', 'M', 'L', 'XL', 'XXL']; +const STICKY_SIZE_MAP: Record = { S: 10, M: 14, L: 18, XL: 24, XXL: 32 }; +const STICKY_SIZE_VALUES = Object.values(STICKY_SIZE_MAP); function nearestStickySize(fontSize: number): StickyTextSize { - if (fontSize <= 12) return 'S'; - if (fontSize <= 15) return 'M'; - return 'L'; + 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 value. */ +export function snapToStickyPreset(fontSize: number): number { + let best = STICKY_SIZE_VALUES[0]; + let bestDist = Infinity; + for (const v of STICKY_SIZE_VALUES) { + const dist = Math.abs(fontSize - v); + if (dist < bestDist) { bestDist = dist; best = v; } + } + return best; } interface TextFormatToolbarProps { @@ -111,7 +128,7 @@ export default function TextFormatToolbar(props: TextFormatToolbarProps) { > {/* S/M/L text size toggle (sticky only) */} {kind === 'sticky' && onStickySizeChange && (<> - {(['S', 'M', 'L'] as StickyTextSize[]).map((size) => { + {STICKY_SIZES.map((size) => { const active = nearestStickySize(stickyFontSize || 14) === size; return (