Configurable grid + snap-to-grid in the layout system (#45)
Build image / build-and-push (push) Successful in 7s
Build image / build-and-push (push) Successful in 7s
Closes #44. Two independent grids (garden + per-bed), each with a size and a snap toggle; snapping defaults off. See PR #45 for details. Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #45.
This commit is contained in:
@@ -5,9 +5,12 @@ import { TextArea } from '@/components/ui/TextArea'
|
||||
import { useDeleteObject, useUpdateObject } from '@/lib/objects'
|
||||
import {
|
||||
cmFromDisplay,
|
||||
cmFromSpacing,
|
||||
dimensionUnitLabel,
|
||||
displayFromCm,
|
||||
MIN_DIMENSION_CM,
|
||||
spacingFromCm,
|
||||
spacingUnitLabel,
|
||||
type UnitPref,
|
||||
} from '@/lib/units'
|
||||
import { kindDef } from './kinds'
|
||||
@@ -48,6 +51,7 @@ export function Inspector({
|
||||
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 [gridSize, setGridSize] = useState(String(spacingFromCm(object.gridSizeCm, unit)))
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false)
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -64,7 +68,8 @@ export function Inspector({
|
||||
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])
|
||||
setGridSize(String(spacingFromCm(object.gridSizeCm, unit)))
|
||||
}, [object.version, object.widthCm, object.heightCm, object.xCm, object.yCm, object.rotationDeg, object.name, object.notes, object.color, object.gridSizeCm, unit])
|
||||
|
||||
const patch = (fields: Partial<Omit<EditorObject, 'id' | 'version'>>) => {
|
||||
if (readOnly) return // a blur mustn't fire a mutation if the role changed mid-edit
|
||||
@@ -84,6 +89,17 @@ export function Inspector({
|
||||
if (cm !== current) apply(cm)
|
||||
}
|
||||
|
||||
// Commit the bed grid size (entered at spacing scale, cm/in). Compare at display
|
||||
// precision so a blur without an edit doesn't fire a spurious PATCH; ignore a
|
||||
// sub-1cm value the server would reject.
|
||||
const commitGrid = () => {
|
||||
const v = parseFloat(gridSize)
|
||||
if (!Number.isFinite(v)) return
|
||||
if (v === spacingFromCm(object.gridSizeCm, unit)) return
|
||||
const cm = cmFromSpacing(v, unit)
|
||||
if (cm >= MIN_DIMENSION_CM && cm !== object.gridSizeCm) patch({ gridSizeCm: cm })
|
||||
}
|
||||
|
||||
const u = dimensionUnitLabel(unit)
|
||||
|
||||
return (
|
||||
@@ -218,14 +234,43 @@ export function Inspector({
|
||||
Plantable
|
||||
</label>
|
||||
|
||||
<TextArea
|
||||
label="Notes"
|
||||
name="notes"
|
||||
rows={2}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
onBlur={() => notes !== object.notes && patch({ notes })}
|
||||
/>
|
||||
{/* Bed grid: only plantable beds place plants, so the plant-snapping grid
|
||||
is shown just for them. */}
|
||||
{object.plantable && (
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<TextField
|
||||
label={`Bed grid (${spacingUnitLabel(unit)})`}
|
||||
name="gridSize"
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="any"
|
||||
min="0"
|
||||
value={gridSize}
|
||||
onChange={(e) => setGridSize(e.target.value)}
|
||||
onBlur={commitGrid}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex h-9 items-center gap-2 whitespace-nowrap text-sm text-fg">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={object.snapToGrid}
|
||||
onChange={(e) => patch({ snapToGrid: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
Snap plants
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TextArea
|
||||
label="Notes"
|
||||
name="notes"
|
||||
rows={2}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
onBlur={() => notes !== object.notes && patch({ notes })}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
{!readOnly &&
|
||||
|
||||
Reference in New Issue
Block a user