feat(sticky): proportional card width per text size preset
Each S/M/L/XL/XXL preset now sets both fontSize and card width: S(20px/200w), M(28px/260w), L(36px/320w), XL(48px/400w), XXL(60px/480w). Creation picks preset based on zoom, toolbar toggle updates both. Wider size toggle buttons for XL/XXL labels, font name maxWidth 110px. Backward compatible — existing stickies keep original width.
This commit is contained in:
@@ -37,7 +37,7 @@ const defaultOptions: ToolOptions = {
|
|||||||
|
|
||||||
// Creation defaults — zoom-aware via screenToWorld()
|
// Creation defaults — zoom-aware via screenToWorld()
|
||||||
const DEFAULT_TEXT_SCREEN_FONT = 24;
|
const DEFAULT_TEXT_SCREEN_FONT = 24;
|
||||||
const DEFAULT_STICKY_SCREEN_FONT = 14;
|
const DEFAULT_STICKY_SCREEN_FONT = 28;
|
||||||
const DEFAULT_STICKY_SCREEN_WIDTH = 220;
|
const DEFAULT_STICKY_SCREEN_WIDTH = 220;
|
||||||
const DEFAULT_STICKY_SCREEN_HEIGHT = 66;
|
const DEFAULT_STICKY_SCREEN_HEIGHT = 66;
|
||||||
|
|
||||||
@@ -328,7 +328,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(DEFAULT_STICKY_SCREEN_WIDTH, zoom, 120, 400);
|
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 cardH = screenToWorld(DEFAULT_STICKY_SCREEN_HEIGHT, zoom, 40, 200);
|
||||||
const stickyData = {
|
const stickyData = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
@@ -346,7 +347,7 @@ export function activateTool(
|
|||||||
name: '',
|
name: '',
|
||||||
visible: true,
|
visible: true,
|
||||||
text: '',
|
text: '',
|
||||||
fontSize: snapToStickyPreset(screenToWorld(DEFAULT_STICKY_SCREEN_FONT, zoom, 8, 40)),
|
fontSize: preset.fontSize,
|
||||||
fontFamily: 'Inter, system-ui, sans-serif',
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
fill: '#ffd43b', // default yellow
|
fill: '#ffd43b', // default yellow
|
||||||
textColor: '#1a1a1a',
|
textColor: '#1a1a1a',
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import React, { useState, useRef, useEffect } from 'react';
|
|||||||
type StickyTextSize = 'S' | 'M' | 'L' | 'XL' | 'XXL';
|
type StickyTextSize = 'S' | 'M' | 'L' | 'XL' | 'XXL';
|
||||||
|
|
||||||
const STICKY_SIZES: StickyTextSize[] = ['S', 'M', 'L', 'XL', 'XXL'];
|
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_MAP: Record<StickyTextSize, number> = { S: 20, M: 28, L: 36, XL: 48, XXL: 60 };
|
||||||
|
const STICKY_WIDTH_MAP: Record<StickyTextSize, number> = { S: 200, M: 260, L: 320, XL: 400, XXL: 480 };
|
||||||
const STICKY_SIZE_VALUES = Object.values(STICKY_SIZE_MAP);
|
const STICKY_SIZE_VALUES = Object.values(STICKY_SIZE_MAP);
|
||||||
|
|
||||||
function nearestStickySize(fontSize: number): StickyTextSize {
|
function nearestStickySize(fontSize: number): StickyTextSize {
|
||||||
@@ -16,15 +17,21 @@ function nearestStickySize(fontSize: number): StickyTextSize {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Snap a raw fontSize to the nearest preset value. */
|
/** Snap a raw fontSize to the nearest preset. Returns { fontSize, width }. */
|
||||||
export function snapToStickyPreset(fontSize: number): number {
|
export function snapToStickyPreset(fontSize: number): { fontSize: number; width: number } {
|
||||||
let best = STICKY_SIZE_VALUES[0];
|
let best: StickyTextSize = 'M';
|
||||||
let bestDist = Infinity;
|
let bestDist = Infinity;
|
||||||
for (const v of STICKY_SIZE_VALUES) {
|
for (const size of STICKY_SIZES) {
|
||||||
const dist = Math.abs(fontSize - v);
|
const dist = Math.abs(fontSize - STICKY_SIZE_MAP[size]);
|
||||||
if (dist < bestDist) { bestDist = dist; best = v; }
|
if (dist < bestDist) { bestDist = dist; best = size; }
|
||||||
}
|
}
|
||||||
return best;
|
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];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextFormatToolbarProps {
|
interface TextFormatToolbarProps {
|
||||||
@@ -135,9 +142,9 @@ export default function TextFormatToolbar(props: TextFormatToolbarProps) {
|
|||||||
key={size}
|
key={size}
|
||||||
style={{
|
style={{
|
||||||
...btnStyle,
|
...btnStyle,
|
||||||
width: '26px',
|
minWidth: size.length > 2 ? '32px' : '24px',
|
||||||
padding: 0,
|
padding: '0 3px',
|
||||||
fontSize: size === 'S' ? '9px' : size === 'M' ? '10px' : size === 'L' ? '11px' : size === 'XL' ? '12px' : '13px',
|
fontSize: '10px',
|
||||||
fontWeight: active ? 700 : 400,
|
fontWeight: active ? 700 : 400,
|
||||||
color: active ? '#fff' : '#666',
|
color: active ? '#fff' : '#666',
|
||||||
background: active ? '#333' : 'transparent',
|
background: active ? '#333' : 'transparent',
|
||||||
@@ -157,7 +164,7 @@ export default function TextFormatToolbar(props: TextFormatToolbarProps) {
|
|||||||
{/* Font family dropdown */}
|
{/* Font family dropdown */}
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<button
|
<button
|
||||||
style={{ ...btnStyle, maxWidth: '90px', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
style={{ ...btnStyle, maxWidth: '110px', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||||
onClick={(e) => { e.stopPropagation(); setShowFontMenu((v) => !v); setShowColorPicker(false); }}
|
onClick={(e) => { e.stopPropagation(); setShowFontMenu((v) => !v); setShowColorPicker(false); }}
|
||||||
title="Font family"
|
title="Font family"
|
||||||
{...hoverHandlers}
|
{...hoverHandlers}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import UserCursors from '../components/UserCursors';
|
|||||||
import ContextMenu from '../components/ContextMenu';
|
import ContextMenu from '../components/ContextMenu';
|
||||||
import LayerPanel from '../components/LayerPanel';
|
import LayerPanel from '../components/LayerPanel';
|
||||||
import SelectionToolbar from '../components/SelectionToolbar';
|
import SelectionToolbar from '../components/SelectionToolbar';
|
||||||
import TextFormatToolbar from '../components/TextFormatToolbar';
|
import TextFormatToolbar, { getStickyWidthForSize } from '../components/TextFormatToolbar';
|
||||||
import VideoControls from '../components/VideoControls';
|
import VideoControls from '../components/VideoControls';
|
||||||
import ShortcutsHelp from '../components/ShortcutsHelp';
|
import ShortcutsHelp from '../components/ShortcutsHelp';
|
||||||
import MattermostImport from '../components/MattermostImport';
|
import MattermostImport from '../components/MattermostImport';
|
||||||
@@ -975,9 +975,11 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
} : undefined}
|
} : undefined}
|
||||||
onStickySizeChange={textToolbar.kind === 'sticky' ? (fontSize) => {
|
onStickySizeChange={textToolbar.kind === 'sticky' ? (fontSize) => {
|
||||||
const scene = canvasRef.current?.getScene();
|
const scene = canvasRef.current?.getScene();
|
||||||
|
const newWidth = getStickyWidthForSize(fontSize);
|
||||||
for (const item of textToolbar.items) {
|
for (const item of textToolbar.items) {
|
||||||
const d = item.data as StickyObject;
|
const d = item.data as StickyObject;
|
||||||
d.fontSize = fontSize;
|
d.fontSize = fontSize;
|
||||||
|
d.w = newWidth;
|
||||||
if (item.displayObject instanceof StickySprite) {
|
if (item.displayObject instanceof StickySprite) {
|
||||||
item.displayObject.updateFromData(d);
|
item.displayObject.updateFromData(d);
|
||||||
d.h = item.displayObject.computedHeight;
|
d.h = item.displayObject.computedHeight;
|
||||||
|
|||||||
Reference in New Issue
Block a user