Turns the canvas foundation into the real editor, loading a garden from the
server and editing it with optimistic sync. Completes Phase 3.
- lib/objects.ts: react-query over GET /gardens/:id/full + optimistic
create/update/delete. Update applies to the cache up front and PATCHes
with the row version; a 409 adopts the server's `current` row and toasts
"updated elsewhere". One PATCH per gesture.
- editor/store.ts: adds liveObject (in-gesture geometry, rendered instantly),
armedKind (palette tap-to-place), and objectDragging (so the pan gesture
stands down while an object drag owns the pointer).
- editor/Palette.tsx + kinds.ts: the seven kinds with icons + default sizes;
tap a kind, tap the canvas to place (works desktop + touch).
- editor/SelectionOverlay.tsx: move (drag body), resize (corner handles,
opposite corner fixed via the object-local frame so rotation is honored),
rotate (knob, snaps to 15°, free with Shift). Each updates liveObject and
fires one PATCH on release.
- editor/Inspector.tsx: name, dimensions/position (unit-aware), rotation,
color override (+ clear), plantable, notes, two-step delete. Commits per
field; re-syncs from the object unless a field is focused.
- GardenCanvas: placement, live-drag merge, z-ordered render, selection
overlay, and ?focus=<id> frames an object on load. GardenEditorPage loads
/full and lays out palette | canvas | inspector (bottom sheet on mobile).
- components/ui/toast.tsx + Toaster in AppShell.
Verified in a real browser against the embedded binary: place a bed → move,
resize, rotate (rotate snapped to 60°) → set its name in the inspector →
reload, all persisted; each gesture was exactly one PATCH (version 1→2→3→4→5);
an out-of-band edit made the next drag 409 → rollback to the server's value +
toast. tsc + 17 vitest tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
The editor's technically-riskiest slice, built against mock data so #11
only adds interactions on top.
- lib/geometry.ts (+ vitest): world<->screen and object-local<->world
transforms, clampScale, zoomViewportAt (the wheel/pinch "keep the point
under the cursor stationary" math), zoomToFitRect. 11 geometry tests
(round-trips, rotation, cursor-anchored zoom, fit) + 6 units tests.
- editor/store.ts: Zustand store for ephemeral editor state — viewport,
selection, focusedObjectId (selection interactions grow in #11).
- editor/useViewport.ts: @use-gesture wiring — wheel + pinch zoom toward
the pointer, drag-pan, scale clamped to [0.05, 20] px/cm, and an animated
fitToRect (eased rAF tween). touch-action:none so the browser doesn't
fight gestures.
- editor/ObjectShape.tsx: rect/circle centered + rotated, kind default
colors, name label; non-scaling strokes so outlines stay 1px at any zoom.
- editor/GardenCanvas.tsx: full-size svg, single world <g>, fading 1m grid,
garden boundary, z-ordered objects; ResizeObserver-driven auto-fit, a
zoom readout, and a Fit control. Deps: zustand, @use-gesture/react,
vitest (+ test scripts).
- GardenEditorPage renders the canvas with a mock scene (#11 swaps in
/full).
Verified in a real browser: wheel zoom keeps the world point under the
cursor stationary (sub-cm drift on a 12m garden), a real drag pans, and
Fit animates to frame the garden. tsc + vitest green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi