Units correctness: exact imperial entry, garden grid at dimension scale (#47) (#60)
Build image / build-and-push (push) Successful in 6s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #60.
This commit is contained in:
2026-07-21 04:50:49 +00:00
committed by steve
parent 1716362981
commit 8a12069118
7 changed files with 204 additions and 57 deletions
+45 -34
View File
@@ -9,12 +9,11 @@ import { errorMessage } from '@/lib/api'
import { conflictGarden, useCreateGarden, useUpdateGarden, type Garden } from '@/lib/gardens'
import {
cmFromDisplay,
cmFromSpacing,
dimensionUnitLabel,
displayFromCm,
formatCm,
isValidDimensionCm,
spacingFromCm,
spacingUnitLabel,
MIN_GARDEN_GRID_CM,
type UnitPref,
} from '@/lib/units'
@@ -47,7 +46,7 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
const [width, setWidth] = useState(() => dimString(garden?.widthCm, garden?.unitPref ?? 'metric'))
const [height, setHeight] = useState(() => dimString(garden?.heightCm, garden?.unitPref ?? 'metric'))
const [gridSize, setGridSize] = useState(() =>
String(spacingFromCm(garden?.gridSizeCm ?? DEFAULT_GRID_CM, garden?.unitPref ?? 'metric')),
String(displayFromCm(garden?.gridSizeCm ?? DEFAULT_GRID_CM, garden?.unitPref ?? 'metric')),
)
const [snapToGrid, setSnapToGrid] = useState(garden?.snapToGrid ?? false)
const [notes, setNotes] = useState(garden?.notes ?? '')
@@ -60,18 +59,23 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
const v = parseFloat(s)
return Number.isFinite(v) ? String(displayFromCm(cmFromDisplay(v, unit), next)) : s
}
// The grid is entered at spacing scale (cm/in), so it converts with the
// spacing helpers, not the dimension ones.
const convertGrid = (s: string) => {
const v = parseFloat(s)
return Number.isFinite(v) ? String(spacingFromCm(cmFromSpacing(v, unit), next)) : s
}
setWidth(convert(width))
setHeight(convert(height))
setGridSize(convertGrid(gridSize))
// The garden grid is a layout concern, so it lives at the same scale as the
// garden's own dimensions — same helpers, same unit label. (The *bed* grid in
// the object inspector is a plant-spacing concern and stays at cm/in.)
setGridSize(convert(gridSize))
setUnit(next)
}
// Converted once per render and used by both the submit handler and the
// too-fine hint, so the two can never disagree about what was entered.
const gridSizeCm = cmFromDisplay(parseFloat(gridSize), unit)
// Soft floor: hint, don't refuse. A garden-scale grid this fine is usually
// someone reaching for plant spacing, which lives on the bed instead — but it
// is a legitimate choice for a very small garden, so the save still goes through.
const gridTooFine = Number.isFinite(gridSizeCm) && gridSizeCm > 0 && gridSizeCm < MIN_GARDEN_GRID_CM
async function onSubmit(e: FormEvent) {
e.preventDefault()
setFormError(null)
@@ -90,7 +94,6 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
setFormError('Width and height must be between 1 cm and 100 m.')
return
}
const gridSizeCm = cmFromSpacing(parseFloat(gridSize), unit)
if (!isValidDimensionCm(gridSizeCm)) {
setFormError('Grid size must be between 1 cm and 100 m.')
return
@@ -123,7 +126,7 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
setUnit(current.unitPref)
setWidth(dimString(current.widthCm, current.unitPref))
setHeight(dimString(current.heightCm, current.unitPref))
setGridSize(String(spacingFromCm(current.gridSizeCm, current.unitPref)))
setGridSize(String(displayFromCm(current.gridSizeCm, current.unitPref)))
setSnapToGrid(current.snapToGrid)
setNotes(current.notes)
setConflict('This garden changed elsewhere. The latest values are shown — review and save again.')
@@ -175,28 +178,36 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
/>
</div>
<div className="flex items-end gap-3">
<div className="flex-1">
<TextField
label={`Grid size (${spacingUnitLabel(unit)})`}
name="gridSize"
type="number"
inputMode="decimal"
step="any"
min="0"
value={gridSize}
onChange={(e) => setGridSize(e.target.value)}
/>
<div>
<div className="flex items-end gap-3">
<div className="flex-1">
<TextField
label={`Garden grid (${unitLabel})`}
name="gridSize"
type="number"
inputMode="decimal"
step="any"
min="0"
value={gridSize}
onChange={(e) => setGridSize(e.target.value)}
/>
</div>
<label className="flex h-9 items-center gap-2 whitespace-nowrap text-sm text-fg">
<input
type="checkbox"
checked={snapToGrid}
onChange={(e) => setSnapToGrid(e.target.checked)}
className="h-4 w-4 rounded border-border"
/>
Snap objects
</label>
</div>
<label className="flex h-9 items-center gap-2 whitespace-nowrap text-sm text-fg">
<input
type="checkbox"
checked={snapToGrid}
onChange={(e) => setSnapToGrid(e.target.checked)}
className="h-4 w-4 rounded border-border"
/>
Snap objects
</label>
{gridTooFine && (
<p className="mt-1 text-xs text-muted">
{formatCm(gridSizeCm, unit)} is very fine for a whole-garden grid. Plant spacing lives on each bed
(Bed grid in the inspector), not here.
</p>
)}
</div>
<TextArea label="Notes" name="notes" rows={2} value={notes} onChange={(e) => setNotes(e.target.value)} />