feat(whiteboard): resizable nodes — grips, snap, aspect lock, keyboard resize (9.17.23) - #358
Merged
Merged
Conversation
…ard resize (9.17.23) Nothing on the whiteboard was resizable (user-reported, owner-confirmed). Nodes carried width/height but there was zero resize machinery. - logic/resize.ts (pure): ResizeHandle enum, per-kind minimum sizes, anchored resizeBounds (opposite corner/edge never moves, never inverts, never undercuts the min), aspect lock (Shift; default-on for images), computeResizeSnap magnetising only the MOVING edges via the drag matcher (snapLineMatch exported from snap.ts) - engine: 8 grips on the single selected node (4 visible corner squares, 4 invisible edge strips below the midpoint connector dots), live style-only frames through paintDragFrame, ONE persist + history entry on pointerup (single-step undo), guides via the existing layer - lock gating both layers: no grips on a locked node / read-only board, and the commit gate voids a resize if a lock lands mid-gesture - keyboard: Alt+Arrows resize the selection (shortcut registry ids), live-region announcement; grip aria-labels via t() in all six locales (hint bar updated) - dev hook resizeNodeBy/endResize (same primitives as the pointer loop) - tests: logic/resize.test.ts (anchoring per handle, min clamp, aspect, generative no-inversion loop, snap cases) + engine-resize.test.tsx (grip visibility, pointer pipeline, one-commit undo, lock refusal, snap guides, keyboard) — whiteboard 528 green - visual: whiteboard-resize.spec.ts real-shell spec (grips on selection, edge snap + guide, clear on end) — run green locally; repaired the stale whiteboard-snap.spec.ts creation flow (`.whiteboard__add-trigger` died with the React-chrome migration) via dev seedGrid — run green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A real user asked for resizable sticky notes; the owner confirmed nothing on the whiteboard was resizable — nodes carried
width/heightin the model but there was zero resize machinery. This ships node resize as a core capability (plan rung 9.17.23, friction F-469).Interaction design
nwse/neswcursors) + 4 invisible edge hit-strips spanning the borders (ew/nscursors). The strips sit below the midpoint connector dots, so connecting from a selected node still works — corners resize both axes anchored on the opposite corner, edges one axis anchored on the opposite edge.snapLineMatch), guides paint through the existing layer; anchored edges are never nudged; skipped under aspect lock.paintDragFrame, no node-DOM rebuild); ONE persist + ONE history entry on pointerup → a single undo restores the pre-resize box.pre-wrap+ hidden overflow) — verified in the interaction tests and the real shell.Screenshot path
Real-shell captures from the new visual spec (
.screenshots/whiteboard-resize/):01-grips.png(selection chrome with corner grips) and02-snap-guide.png(right edge snapped flush to the neighbour with the accent guide painted).Keyboard path
Alt+ArrowRight/Left/Down/Upresizes the selected node in 8px steps (bottom-right anchored), through the app shortcut registry (ResizeWiden/Narrow/Taller/Shorter), clamped at the per-kind minimum, inert when locked/read-only.Screen-reader path
Every grip carries a translated
aria-label(whiteboard.resize.*, all six catalogs en/es/de/fr/it/pt with full parity); each keyboard/pointer resize announces "Resized to {width} by {height}" through the app live region.Discoverability path
The hint bar now reads "… drag a corner to resize …" (all six locales); grips appear the moment a node is single-selected; edge cursors signal the single-axis affordance.
Tests
logic/resize.test.ts— anchoring per handle, min clamp, aspect lock (corner + edge + min re-floor), a 500-iteration generative no-inversion/anchor-fixed loop, resize-snap cases (moving-edge only, min guard, guide spans).engine-resize.test.tsx— grip visibility (single/multi/locked/read-only), pointer pipeline (SE + NW anchoring, min clamp, element identity), one-commit undo, Shift aspect lock, locked refusal at grip and at commit, dev-hook snap + guide clear, Alt+Arrow keyboard resize.bun run typecheck(packages + apps) andbun run lint(biome + all app ratchets incl. css-token + hardcoded-radius) clean; per-apptsc -p apps/whiteboardclean.tests/visual/specs/whiteboard-resize.spec.ts(production build, dev hookresizeNodeBy— synthetic pointers can't drivesetPointerCapture): grips materialise on selection, edge snap engages + guide paints + clears, zero console errors — run green locally. Also repairedwhiteboard-snap.spec.ts, which was dead on main (its.whiteboard__add-triggerselector died with the React-chrome migration) — now seeds via the dev hook and runs green again.Security
No new capability surface, no new IPC, no new dependency — the feature is renderer-local geometry over the existing persistence path, write-gated by the existing per-node lock + board read-only checks (fail-closed at both the gesture and the commit).
Notes / residue
packages/editortypeahead exception and other standing conventions untouched.🤖 Generated with Claude Code