Skip to content

feat(whiteboard): resizable nodes — grips, snap, aspect lock, keyboard resize (9.17.23) - #358

Merged
th3-br41n merged 1 commit into
mainfrom
feat/whiteboard-node-resize
Jul 29, 2026
Merged

feat(whiteboard): resizable nodes — grips, snap, aspect lock, keyboard resize (9.17.23)#358
th3-br41n merged 1 commit into
mainfrom
feat/whiteboard-node-resize

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

What

A real user asked for resizable sticky notes; the owner confirmed nothing on the whiteboard was resizable — nodes carried width/height in 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

  • 8 handles on the single selected node (v1: multi-select shows none): 4 visible corner squares (accent-bordered, nwse/nesw cursors) + 4 invisible edge hit-strips spanning the borders (ew/ns cursors). 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.
  • Per-kind minimums (derived from spawn defaults): sticky 80×80 · text 80×40 · image 40×40 · frame/embed 160×120 · shape/ink 24×24. Groups are not resizable (their box derives from members).
  • Aspect lock: Shift while dragging; default-on for images (Shift frees them).
  • Snap: the moving edges magnetise to neighbour edges/centres via the same matcher the drag magnet uses (snapLineMatch), guides paint through the existing layer; anchored edges are never nudged; skipped under aspect lock.
  • Commit/undo: live preview is style-only frames (paintDragFrame, no node-DOM rebuild); ONE persist + ONE history entry on pointerup → a single undo restores the pre-resize box.
  • Lock gating, both layers: a locked node / read-only board shows no grips and refuses the gesture; the commit gate additionally voids a resize if the lock lands mid-gesture.
  • Text reflow: sticky/text bodies already reflow (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) and 02-snap-guide.png (right edge snapped flush to the neighbour with the accent guide painted).

Keyboard path

Alt+ArrowRight/Left/Down/Up resizes 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.
  • Whiteboard suite 49 files / 528 green; bun run typecheck (packages + apps) and bun run lint (biome + all app ratchets incl. css-token + hardcoded-radius) clean; per-app tsc -p apps/whiteboard clean.
  • Real-shell visual spec tests/visual/specs/whiteboard-resize.spec.ts (production build, dev hook resizeNodeBy — synthetic pointers can't drive setPointerCapture): grips materialise on selection, edge snap engages + guide paints + clears, zero console errors — run green locally. Also repaired whiteboard-snap.spec.ts, which was dead on main (its .whiteboard__add-trigger selector 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

  • v1 scope: single-selection resize only (documented in code + tests); multi-select resize is a follow-up.
  • The packages/editor typeahead exception and other standing conventions untouched.

🤖 Generated with Claude Code

…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>
@th3-br41n
th3-br41n merged commit cad8f2c into main Jul 29, 2026
2 of 3 checks passed
@th3-br41n
th3-br41n deleted the feat/whiteboard-node-resize branch July 29, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant