Correctness / stuck-state:
- PlopInspector: the planted date can now be cleared (blur commits null instead
of being blocked); radius/count reset to the current value on invalid input.
- applyPlantingPatch now applies removedAt too (completes the optimistic patch).
- PlopMarker computes the count live from the current radius, so the on-canvas
near-zoom label updates while resizing (not just the inspector).
- GardenEditorPage clears focus when the focused object vanishes — deleted, or a
stale ?focus id on load — so the canvas can't get stuck dimmed with no exit.
- useRemovePlanting adopts the 409 current row, so a stale-version soft-remove
can be retried instead of looping.
- Placement is gated on plantable, so a deep-linked non-plantable focus can't try
to POST a plop the server would 400.
Dedup / perf (introduced by this PR):
- editor/shared.ts: SELECT_COLOR, HANDLE_PX, MIN_RADIUS_CM, DIMMED_OPACITY, and
an objectTransform() helper, now used by ObjectShape/SelectionOverlay/PlopLayer/
PlopOverlay/PlopMarker/GardenCanvas (unified the transform string + the two
different dim opacities).
- Dropped PlopMarker's always-false `dimmed` prop (dimming is at the group level).
- Build plantsById once in the page and pass it to GardenCanvas.
- Moved the livePlanting subscription into PlopInspector so a plop drag
re-renders only the panel, not the whole page (matches the liveObject pattern).
- Memoized PlopLayer's plop grouping so it doesn't rebuild every pan/zoom frame.
Skipped: gesture-scaffolding hook extraction (risky churn in #11 code),
elliptical clamp for circle objects (consistent with the server's rect bounds),
optimistic create (matches useCreateObject).
tsc --noEmit clean; 24/24 vitest; production build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
Pansy's core vision: click into a bed, plop plants in its corners, zoom out and
still see what's planted where. Built on the field editor (#11), PlantPicker
(#13), and plantings API (#14).
Data layer
- lib/plantings.ts: ServerPlanting zod shape, EditorPlanting, effectiveCount, and
a client mirror of the derived-count formula (computeDerivedCount) for live
resize display; unit-tested.
- lib/objects.ts: /full now types plantings + plants; useCreatePlanting /
useUpdatePlanting / useRemovePlanting patch the same FullGarden cache with the
#11 optimistic + 409-rollback contract. A soft-remove drops the plop from the
active list.
Editor
- store: selectedPlantingId / livePlanting / armedPlant (mutually-exclusive
object vs plop selection).
- Focus mode: "Plant here" (object inspector) → focusedObjectId; the canvas
animates fitToRect to the object and dims the rest; a breadcrumb / Escape /
empty-tap exits. focusedObjectId mirrors to ?focus (deep-linkable, survives
reload).
- Placing: "+ Add plant" opens the PlantPicker; the chosen plant stays armed for
repeat placement; tapping inside the focused object drops a plop (default
radius max(1.5·spacing, 15cm)) in the object's local frame.
- Manipulating: PlopOverlay (drag to move, edge handle to resize, one PATCH per
gesture) + PlopInspector (change plant, radius, count override vs live derived,
label, planted date, soft-remove).
- PlopMarker semantic zoom (px/cm bands): far = flat color patch + a dominant-
plant tint on the object; mid = circle + emoji; near = + name + count. Plops
render inside each object's rotated group, so moving/rotating a bed carries
them with no planting writes.
tsc --noEmit clean; 24/24 vitest; production build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi