Simplify Ctrl+V shortcut to only handle recent internal copies, letting
native paste events flow to setupPaste for external clipboard content.
Extend setupPaste with text/HTML detection and popup callbacks. Wire
PasteChoicePopup in Editor.tsx with markdown (turndown HTML-to-MD),
plain text, and image paste choices.
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.
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.
Sticky UX overhaul — clear separation between card resize and text size:
- S/M/L toggle (12/14/18px) in contextual toolbar for sticky text size
- TransformBox: horizontal-only resize for stickies with live text
reflow during drag (no more stretch-then-snap)
- Hide vertical handles (tc/bc) for sticky-only selections
- Minimum sticky width of 80px enforced in both drag and bake
- TextEditor: sticky background auto-resizes as user types
- textLimits.ts comment corrected to match actual usage
Plain text unchanged: resize gesture scales fontSize via bake.
Fully backward compatible — existing stickies map to nearest preset.
Add TextSharpnessManager for crisp text rendering at all zoom levels:
- Discrete zoom buckets (0.5–3x) avoid texture churn on small zoom changes
- Initial bucket applied to all items on setup and newly created items
via SceneManager.onItemCreated callback (fixes blurry-on-load)
- Visibility check uses getItemWorldBounds for correct grouped item coords
- TextSprite/StickySprite gain setZoomBucket() for resolution control
Remove fontSize +/- controls from contextual toolbar — font size is now
controlled purely through direct manipulation (resize → bake).
- 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)
New reviewTargeting.ts provides resolveReviewTargetAtPoint() which:
- descends into groups to find the deepest commentable child under the click
- walks children in z-order (front-to-back) for correct visual stacking
- handles nested groups recursively
- returns null if no valid child exists under the point
Editor.tsx review click handler now delegates to the resolver instead of
raw queryRegion + topmost-z pick. Defensive guard rejects group objectIds
before draft creation.
Existing group-anchored threads continue to render and open normally.
1. TEXT and ERASER tool clicks now suppressed during review mode via
reviewMode flag on ToolContext (read from ref for live value)
2. Object-comment input hidden when draftPin is active — only one
creation input visible at a time
3. PinOverlay entrance animation removed — focus styling (scale 1.15x,
alpha changes) no longer fights with fade-in animation loop
- Pass expandRequest, focusedThreadId, draftPin, onCreatePointThread to FeedbackPanel
- Lift thread detail state via onThreadDetailChange → setOpenThreadDetailId
- Jump-to-object centers on pin location for point-pinned threads
- Pan-only by default; zoom in if object appears <50px on screen
- Show toast when jumping to deleted object
- Escape priority: draft pin → thread detail collapse → review mode exit
- Draft/detail clearing works even when focus is in input field
- Review mode exit respects input/textarea focus context
- handleCreatePointThread: REST POST with auto-focus on success
- Clear draft/focus/expandRequest when review mode exits
- Add DraftPin interface (exported for FeedbackPanel)
- Add expandRequest state with seq counter for re-trigger
- Add draftPin, openThreadDetailId, expandSeqRef state
- Remove old pointerdown pin-click handler with requestAnimationFrame clear
The cropOverlay was created asynchronously inside a polling interval but
returned synchronously as .current (null). By returning the ref itself,
Editor.tsx reads .current at call time when the overlay actually exists.
- CropOverlay: register move/up handlers dynamically on drag start (single
handler instead of 8x per-handle), remove on drag end. Override destroy()
to call _cleanup() preventing keyboard listener leaks.
- Text format toolbar: re-measure text bounds (w/h) after fontSize/fontFamily
changes, update spatial index and transform box.
- TextEditor: add clearText() method; tools.ts uses it instead of fragile
document.querySelector('textarea').
- SceneManager._updateItem: apply crop mask on remote sync for image items.
- useCanvasSetup: stop TextEditor on unmount to prevent orphaned textarea.
- Double-click zoom: use item.data dimensions instead of getBounds() (which
includes shadow offset).
- Text tool: click-to-place immediately opens inline editor, auto-switches
back to select tool. Empty text cleanup on save.
- Crop: select image + press C (or right-click > Crop) to enter crop mode.
8 drag handles with rule-of-thirds grid, dimmed outside area.
Enter confirms, Escape cancels. Non-destructive (stored as normalized rect).
- Text format toolbar: appears when text items selected, with font size +/-,
font family dropdown, and color picker with presets.
- Double-click image: zoom-to-fit (PureRef-style focus)
- Entrance animation: replaced bounce with simple fade-in (no delay before
items become interactive)
- TextEditor: allow empty text (caller handles cleanup)
- Fix 403 on save for public collection viewers (return role in GET board response)
- Add read-only status indicator (StatusBar + StatusIndicator)
- Fix beforeunload save to use fetch+keepalive with auth header
- Socket reconnect now rejoins board room automatically
- Canvas setup uses polling instead of brittle 200ms timer
- Fix double user:left on disconnect (use disconnecting event, snapshot rooms)
- Thread + comment creation wrapped in db.transaction
- Prevent owner downgrade via addCollectionMember (check existing member)
- Bound redirect depth in downloadImage to 5
- Arrangement operations anchor to bounding box top-left (no drift)
- Distribute H/V also anchor to top-left
- Fix annotations fetch to use axios api instance (401 interceptor)
- Replace require() with static import in shortcut-definitions
- PinOverlay: zero-allocation rendering with in-place Graphics/Text updates,
fix WebGL texture crash (addressModeU null) from orphaned Text objects
- Pins always visible on media, track during drag via updatePositions() fast path
- Replace eraser with Review mode in toolbar tool group (orange gradient)
- Add tool instruction hints bar below toolbar (context-sensitive per tool)
- Feedback panel: glassmorphism, improved ThreadDetail/CommentItem/ThreadListItem
- Timestamps: relative with "ago" suffix, full date on hover tooltip
- De-emphasize pin numbers, show status text in thread list
- Animated arrangement transitions (easeOutCubic, 320ms) for pack/grid/row/column
- Disable snap guides (user requested)
- Paste now selects newly created items
- Remove eraser keyboard shortcuts, remap tool shortcuts
- Orphaned thread cleanup: safe destroy check for deleted media pins
1. toggleVote wrapped in transaction (race condition fix)
2. FeedbackPanel fetch calls now surface errors via onError/toast
3. Extracted resolveBoard/hasCollectionRole to shared board-access.js
4. AnnotationStore uses monotonic version counter for snapshots
5. PinOverlay uses object pool instead of destroy/recreate on refresh
6. canvasObjects prop memoized with useMemo
7. PinOverlay store subscription cleaned up on unmount
8. Comment content capped at 5000 chars (backend validation)
10. anchor_type validated to 'object' or 'point'
New ExportDialog component with scope (selection/all), filename,
format (PNG/JPEG/WebP), quality slider, scale multiplier, and
background color picker. Wired into Toolbar and Editor.
1. URL imports now participate in upload manager pipeline:
addUrlJob() creates a job row, uploadComplete/setFailed called
on success/failure — consistent with local file uploads
2. activeCount only counts uploading/processing jobs, not failed.
Failed jobs no longer inflate the "Uploads (n)" header count.
3. uploadManager.clear() called on boardId change so stale jobs
from a previous board don't linger in the next board's panel.
- UploadManager store: tracks jobs through uploading → processing → done/failed
- UploadPanel component: floating bottom-left widget showing upload progress,
file names, sizes, status with auto-dismiss for completed items
- axios onUploadProgress wired for real-time upload percentage
- Video jobs transition to "processing" after upload, then "done" when
media:job:update socket event arrives (reuses existing pipeline)
- Failed uploads show error message from server response
- Clear button to dismiss finished/failed items
- Background media worker: polls media_jobs table, runs ffprobe+ffmpeg
with concurrency limit, generates video posters, emits socket events
- Non-blocking video upload: stores file + enqueues job, returns immediately
- Poster hydration on board load: GET /boards/:id injects poster/dimensions
from DB into canvas_state video objects
- SpatialGrid: fixed-cell (512px) spatial hash for O(nearby) culling instead
of O(all) item scanning, eliminates setTimeout violations
- Canvas-based video rendering: draws video frames to offscreen canvas then
uploads to GPU, completely eliminates GL_INVALID_OPERATION errors from
PixiJS VideoSource auto-update mechanism
- Server poster upgrade path: culling ticker and applyProcessedMedia() both
upgrade client-captured posters to server posters when available
- Pause restores server poster (paused video behaves like an image)
- Selection drag-end persistence: onObjectDragEnd broadcasts + saves + undo
- Live media:job:update socket handler patches scene data + VideoSprite
dimensions without broadcast fanout
- Add onDragOver/onDrop preventDefault on container div (React level)
to always block browser default file-open behavior
- Add DOM fallback in useCanvasSetup if ref is somehow null
- setupDragDrop's native listeners handle the actual upload logic
The drag/drop setup was searching for a <canvas> element to find its
parent, which was fragile and could fail (returning null). When it
failed, document-level dragover/drop prevention was never registered,
so the browser's default behavior (open file in new tab) kicked in.
Fix: pass a direct ref to the outer canvas container div from Editor
to useCanvasSetup. No more DOM querying for the drop target.
Replace broadcastSceneNow() with incremental element:update for all
property-modifying operations (align, arrange, flip, resize, rotate,
normalize, frame color). Full scene sync now only fires via debounced
fallback (500ms) for structural changes (add/remove/paste/group).
Changes:
- onCanvasChange() accepts optional changedIds for incremental sync
- SelectionToolbar, context menu, shortcuts all pass item IDs
- TransformBox.onDragEnd passes item IDs instead of full scene
- ShortcutContext, MenuContext, hook interfaces updated
This dramatically reduces sync traffic during normal editing — only
the changed elements are sent instead of the entire board state.
- Make PixiJS canvas background transparent so SVG dot grid shows through
- Move dot grid SVG before canvas in DOM with zIndex 0 (behind images)
- Set wrapper div background to #1e1e1e
- Remove 600px cap on pasted images — preserve native dimensions
onCanvasChange now calls broadcastSceneNow() so rearrangement,
alignment, and other bulk operations sync to other tabs in real-time
instead of waiting for the next periodic sync or page reload.
Floating HTML overlay with play/pause, seekbar, time display, and
mute toggle that appears when a single video element is selected.
Positioned at the bottom of the video using viewport.toScreen()
coordinate conversion, polled at 100ms for position and 250ms for
playback progress.