import { useEffect, useRef, useState, type ChangeEvent } from 'react' import { Button } from '@/components/ui/Button' import { TextField } from '@/components/ui/TextField' import { TextArea } from '@/components/ui/TextArea' import { useDeleteObject, useUpdateObject } from '@/lib/objects' import { cmFromDisplay, dimensionUnitLabel, displayFromCm, MIN_DIMENSION_CM, type UnitPref, } from '@/lib/units' import { kindDef } from './kinds' import { useEditorStore } from './store' import type { EditorObject } from './types' const DEFAULT_COLOR = '#8a8a8a' /** * Property panel for the selected object. Each field commits a PATCH on * blur/change (carrying the object's version); dimensions and position are shown * in the garden's unit. Keyed by object id in the parent so it re-inits cleanly * on selection change. */ export function Inspector({ object, gardenId, unit, onFocus, readOnly = false, }: { object: EditorObject gardenId: number unit: UnitPref onFocus?: () => void readOnly?: boolean }) { const update = useUpdateObject(gardenId) const del = useDeleteObject(gardenId) const select = useEditorStore((s) => s.select) // Local field state (initialized once; committed on blur/change). const [name, setName] = useState(object.name) const [notes, setNotes] = useState(object.notes) const [width, setWidth] = useState(String(displayFromCm(object.widthCm, unit))) const [height, setHeight] = useState(String(displayFromCm(object.heightCm, unit))) const [x, setX] = useState(String(displayFromCm(object.xCm, unit))) const [y, setY] = useState(String(displayFromCm(object.yCm, unit))) const [rotation, setRotation] = useState(String(Math.round(object.rotationDeg))) const [color, setColor] = useState(object.color ?? DEFAULT_COLOR) const [confirmingDelete, setConfirmingDelete] = useState(false) const rootRef = useRef(null) // When the object changes underneath us (e.g. a canvas move/resize/rotate, or // an optimistic PATCH result), re-sync the fields — unless the user is // actively editing one here, so we don't clobber their typing. useEffect(() => { if (rootRef.current?.contains(document.activeElement)) return setName(object.name) setNotes(object.notes) setWidth(String(displayFromCm(object.widthCm, unit))) setHeight(String(displayFromCm(object.heightCm, unit))) setX(String(displayFromCm(object.xCm, unit))) setY(String(displayFromCm(object.yCm, unit))) setRotation(String(Math.round(object.rotationDeg))) setColor(object.color ?? DEFAULT_COLOR) }, [object.version, object.widthCm, object.heightCm, object.xCm, object.yCm, object.rotationDeg, object.name, object.notes, object.color, unit]) const patch = (fields: Partial>) => { if (readOnly) return // a blur mustn't fire a mutation if the role changed mid-edit update.mutate({ id: object.id, version: object.version, ...fields }) } // Commit a dimension/position field. `positive` gates width/height (must be // ≥ the server minimum) but not x/y, which may be zero or negative. Compare at // display precision first so a blur without an edit doesn't fire a spurious // PATCH from cm↔ft rounding drift (e.g. 240cm shows 7.9ft → 241cm). const commitDim = (raw: string, current: number, apply: (cm: number) => void, positive = false) => { const v = parseFloat(raw) if (!Number.isFinite(v)) return if (v === displayFromCm(current, unit)) return const cm = cmFromDisplay(v, unit) if (positive && cm < MIN_DIMENSION_CM) return if (cm !== current) apply(cm) } const u = dimensionUnitLabel(unit) return (

{kindDef(object.kind)?.label ?? object.kind}

{readOnly && (

View only — you can't edit this garden.

)} {!readOnly && object.plantable && onFocus && ( )} {/* A disabled fieldset makes every control below read-only for viewers in one shot (no per-input disabled). */}
setName(e.target.value)} onBlur={() => name !== object.name && patch({ name })} />
setWidth(e.target.value)} onBlur={() => commitDim(width, object.widthCm, (cm) => patch({ widthCm: cm }), true)} /> setHeight(e.target.value)} onBlur={() => commitDim(height, object.heightCm, (cm) => patch({ heightCm: cm }), true)} /> setX(e.target.value)} onBlur={() => commitDim(x, object.xCm, (cm) => patch({ xCm: cm }))} /> setY(e.target.value)} onBlur={() => commitDim(y, object.yCm, (cm) => patch({ yCm: cm }))} />
setRotation(e.target.value)} onBlur={() => { const v = parseFloat(rotation) if (Number.isFinite(v) && v !== object.rotationDeg) patch({ rotationDeg: v }) }} />
) => setColor(e.target.value)} onBlur={() => color !== (object.color ?? DEFAULT_COLOR) && patch({ color })} className="h-9 w-14 cursor-pointer rounded-md border border-border bg-surface" />
{object.color && ( )}