Address touch review: one coarse-pointer signal + stable nudge callbacks
Build image / build-and-push (push) Successful in 10s

Gadfly on #104:
- The handles keyed off `pointer: coarse` but the NudgePad off `md:hidden`
  (viewport), so a large touchscreen or a narrow mouse window got them
  disagreeing. Extracted one `isCoarsePointer` in shared.ts that both use —
  the pad now shows on a coarse pointer, same as the bigger handles.
- Wrapped commitLater/nudgeSelected in useCallback([]) — stable identity, so
  NudgePad doesn't re-render each parent render, and the mount-once keydown
  effect capturing nudgeSelected is now explicitly safe (a comment spells out
  the refs-only invariant that makes the empty-deps capture correct).
- isCoarsePointer's optional-chained matchMedia keeps it false (mouse
  defaults) under test/SSR, addressing the constants-file testability note.

tsc + build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
2026-07-22 10:19:53 -04:00
co-authored by Claude Opus 4.8
parent 1323a03acd
commit b0e11bce17
2 changed files with 30 additions and 19 deletions
+11 -6
View File
@@ -3,12 +3,17 @@
// one place instead of drifting between files.
export const SELECT_COLOR = '#2f7a3e' // selection stroke/handles
// On-screen size of a drag/resize handle. Bigger on a coarse pointer (touch) so
// a fingertip can actually grab a resize corner or the rotate knob — 12px is fine
// for a mouse but frustrating for a thumb (#104). Read once at load; a device
// doesn't switch its primary pointer mid-session.
export const HANDLE_PX =
typeof window !== 'undefined' && window.matchMedia?.('(pointer: coarse)').matches ? 22 : 12
// Whether the primary pointer is a fingertip rather than a mouse — the one signal
// the touch affordances key off (bigger handles here, the on-screen nudge pad in
// the editor), so they can't disagree about what "touch" means. Read once at
// load; a device doesn't switch its primary pointer mid-session, and the optional
// chain keeps it false (mouse defaults) under test / SSR where matchMedia is absent.
export const isCoarsePointer =
typeof window !== 'undefined' && !!window.matchMedia?.('(pointer: coarse)').matches
// On-screen size of a drag/resize handle. Bigger on touch so a fingertip can
// actually grab a resize corner or the rotate knob — 12px is fine for a mouse but
// frustrating for a thumb (#104).
export const HANDLE_PX = isCoarsePointer ? 22 : 12
export const MIN_RADIUS_CM = 1 // smallest plop radius
export const DIMMED_OPACITY = 0.4 // non-focused objects/plops in focus mode