refactor(sticky): fixed-width card with S/M/L/XL/XXL font presets
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.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<StickyTextSize, number> = { S: 12, M: 14, L: 18 };
|
||||
const STICKY_SIZES: StickyTextSize[] = ['S', 'M', 'L', 'XL', 'XXL'];
|
||||
const STICKY_SIZE_MAP: Record<StickyTextSize, number> = { 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 (
|
||||
<button
|
||||
@@ -120,13 +137,13 @@ export default function TextFormatToolbar(props: TextFormatToolbarProps) {
|
||||
...btnStyle,
|
||||
width: '26px',
|
||||
padding: 0,
|
||||
fontSize: size === 'S' ? '10px' : size === 'M' ? '12px' : '14px',
|
||||
fontSize: size === 'S' ? '9px' : size === 'M' ? '10px' : size === 'L' ? '11px' : size === 'XL' ? '12px' : '13px',
|
||||
fontWeight: active ? 700 : 400,
|
||||
color: active ? '#fff' : '#666',
|
||||
background: active ? '#333' : 'transparent',
|
||||
}}
|
||||
onClick={() => onStickySizeChange(STICKY_SIZE_MAP[size])}
|
||||
title={`${size === 'S' ? 'Small' : size === 'M' ? 'Medium' : 'Large'} text`}
|
||||
title={`${STICKY_SIZE_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'; } }}
|
||||
>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { AnnotationStore } from '../stores/annotationStore';
|
||||
import { PinOverlay } from '../canvas/PinOverlay';
|
||||
import { CropOverlay } from '../canvas/CropOverlay';
|
||||
import { TextSprite } from '../canvas/sprites/TextSprite';
|
||||
import { StickySprite } from '../canvas/sprites/StickySprite';
|
||||
import { TextSharpnessManager } from '../canvas/textSharpness';
|
||||
// PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit
|
||||
import { connectSocket, disconnectSocket } from '../socket';
|
||||
@@ -229,18 +228,6 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
||||
d.h = item.displayObject.measuredHeight;
|
||||
}
|
||||
item.displayObject.scale.set(d.sx, d.sy);
|
||||
} else if (item.type === 'sticky') {
|
||||
const d = item.data as any;
|
||||
// Bake width only — text re-wraps and height auto-adjusts.
|
||||
// fontSize stays stable so the card acts like a resizable text box.
|
||||
d.w = Math.max(80, Math.round(d.w * absSx));
|
||||
d.sx = item.data.sx > 0 ? 1 : -1;
|
||||
d.sy = item.data.sy > 0 ? 1 : -1;
|
||||
if (item.displayObject instanceof StickySprite) {
|
||||
item.displayObject.updateFromData(d);
|
||||
d.h = item.displayObject.computedHeight;
|
||||
}
|
||||
item.displayObject.scale.set(d.sx, d.sy);
|
||||
}
|
||||
}
|
||||
onCanvasChange(itemIds);
|
||||
|
||||
Reference in New Issue
Block a user