Build image / build-and-push (push) Successful in 8s
Fix the real bugs surfaced by the PR #30 adversarial review (duplicates across models collapsed): - Optimistic PATCH now bumps the cached row version so a second edit started before the first PATCH resolves sends the next version instead of re-sending a stale one and self-conflicting (spurious 409 + "updated elsewhere" toast + lost edit). Drop the per-PATCH onSettled refetch: onSuccess already writes the authoritative row and onError always rolls back, so the invalidate was a request storm during drags and a refetch race, with no drift benefit. - Color picker committed a PATCH on every onChange (continuous during a native picker drag). Track the value locally and commit one PATCH on blur. - Rotate read shiftKey from the pointerdown event, so toggling Shift mid-drag didn't switch snap/free. Pass the live pointermove event into the gesture callback and read shiftKey from it. - Window pointer listeners had no unmount cleanup: deleting/deselecting an object mid-drag leaked listeners and could leave objectDragging stuck true (freezing pan). Track the active gesture's cleanup and run it on unmount. - commitDim accepted 0/negative width/height (invalid geometry flashed into the optimistic cache before rollback) and fired a spurious 1cm PATCH on blur-without-edit from cm<->ft rounding drift. Guard width/height against the server minimum and compare at display precision first. Perf: - Split the z-order sort from the live-merge so a pointermove is an O(n) map, not an O(n log n) re-sort. - Memoize the toEditorObject map so object identities are stable across renders and ObjectShape's memo holds. - Snapshot the svg rect once per gesture instead of calling getBoundingClientRect (a layout reflow) on every pointermove. Maintainability: - Move per-kind default z-index into kinds.ts (single source of truth). - Rename the ToastItem `toast` param so it stops shadowing the module's `toast` export. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi