Address review: disarm on tray-remove, cap load, tidy
Build image / build-and-push (push) Successful in 9s

- Removing the armed plant from the tray now disarms it, so placement
  doesn't continue for a plant that's no longer shown.
- loadIds honors TRAY_MAX even if localStorage was hand-edited.
- Consolidate the repeated `canEdit && plantable` toolbar guards into one
  nested conditional (3-model finding).
- Rename the hook's SeedTray interface to SeedTrayState (avoids colliding
  with the SeedTray component) and the component prop plants → trayPlants.
- Hoist ensurePlant to one call in onPickPlant; pass onPickPlant directly as
  onSelect; use ASCII "+" for the tray chip; document the FIFO eviction.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
2026-07-19 02:08:53 -04:00
co-authored by Claude Opus 4.8
parent 75cdac2925
commit 30e8d3e56c
3 changed files with 52 additions and 38 deletions
+40 -28
View File
@@ -244,18 +244,26 @@ export function GardenEditorPage() {
setArmedPlant(plant)
}
// Removing the armed plant from the tray also disarms it, so placement doesn't
// silently continue for a plant the user just cleared out.
function removeFromTrayAndDisarm(id: number) {
removeFromTray(id)
if (armedPlant?.id === id) setArmedPlant(null)
}
// The picker hands back the full Plant (from the whole catalog), so use it
// directly rather than re-resolving against the garden's referenced-plant map —
// a not-yet-placed plant isn't in that map, which used to silently abort the
// pick (nothing armed, picker left open).
// pick (nothing armed, picker left open). Picking (place OR change) makes sure
// the plant renders and drops it into this garden's tray for quick reuse.
function onPickPlant(plant: Plant) {
if (!canEdit) return // defense in depth: viewers can't reach the picker anyway
ensurePlant(plant)
addToTray(plant)
if (picker === 'change' && selectedPlop) {
ensurePlant(plant)
updatePlanting.mutate({ id: selectedPlop.id, version: selectedPlop.version, plantId: plant.id })
} else {
armPlant(plant) // 'place' mode: arm for repeat placement
setArmedPlant(plant) // 'place' mode: arm for repeat placement
}
setPicker(null)
}
@@ -284,30 +292,34 @@ export function GardenEditorPage() {
{garden.name}
</button>
<span className="max-w-[8rem] truncate text-muted">{objectDisplayName(focusedObject)}</span>
{canEdit && !focusedObject.plantable && <span className="text-xs text-muted">Not plantable</span>}
{canEdit && focusedObject.plantable && (
<SeedTray
plants={trayPlants}
armedPlantId={armedPlant?.id ?? null}
onArm={armPlant}
onRemove={removeFromTray}
onOpenPicker={() => setPicker('place')}
/>
)}
{canEdit && focusedObject.plantable && armedPlant && (
<Button variant="ghost" className="px-2 py-1 text-xs" onClick={() => setArmedPlant(null)}>
Done
</Button>
)}
{canEdit && focusedObject.plantable && focusedPlops.length > 0 && (
<Button
variant="ghost"
className="px-2 py-1 text-xs text-red-600 dark:text-red-400"
onClick={() => setClearing(true)}
>
Clear ({focusedPlops.length})
</Button>
)}
{canEdit &&
(focusedObject.plantable ? (
<>
<SeedTray
trayPlants={trayPlants}
armedPlantId={armedPlant?.id ?? null}
onArm={armPlant}
onRemove={removeFromTrayAndDisarm}
onOpenPicker={() => setPicker('place')}
/>
{armedPlant && (
<Button variant="ghost" className="px-2 py-1 text-xs" onClick={() => setArmedPlant(null)}>
Done
</Button>
)}
{focusedPlops.length > 0 && (
<Button
variant="ghost"
className="px-2 py-1 text-xs text-red-600 dark:text-red-400"
onClick={() => setClearing(true)}
>
Clear ({focusedPlops.length})
</Button>
)}
</>
) : (
<span className="text-xs text-muted">Not plantable</span>
))}
</div>
)}
<GardenCanvas garden={garden} objects={objects} plantings={plantings} plantsById={plantsById} canEdit={canEdit} />
@@ -355,7 +367,7 @@ export function GardenEditorPage() {
<PlantPicker
unit={garden.unitPref}
onClose={() => setPicker(null)}
onSelect={(p) => onPickPlant(p)}
onSelect={onPickPlant}
/>
)}