Feet-and-inches entry: type 2' 7\" instead of 2.6 (#59) (#62)
Build image / build-and-push (push) Successful in 6s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #62.
This commit is contained in:
2026-07-21 05:08:57 +00:00
committed by steve
parent f208da94d6
commit 3ec77a1099
4 changed files with 301 additions and 73 deletions
+33 -29
View File
@@ -4,11 +4,12 @@ import { TextField } from '@/components/ui/TextField'
import { TextArea } from '@/components/ui/TextArea'
import { useDeleteObject, useUpdateObject } from '@/lib/objects'
import {
cmFromDisplay,
cmFromSpacing,
dimensionUnitLabel,
displayFromCm,
dimensionInputMode,
formatDimensionInput,
MIN_DIMENSION_CM,
parseDimension,
spacingFromCm,
spacingUnitLabel,
type UnitPref,
@@ -45,10 +46,10 @@ export function Inspector({
// 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 [width, setWidth] = useState(formatDimensionInput(object.widthCm, unit))
const [height, setHeight] = useState(formatDimensionInput(object.heightCm, unit))
const [x, setX] = useState(formatDimensionInput(object.xCm, unit))
const [y, setY] = useState(formatDimensionInput(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)))
@@ -62,10 +63,10 @@ export function Inspector({
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)))
setWidth(formatDimensionInput(object.widthCm, unit))
setHeight(formatDimensionInput(object.heightCm, unit))
setX(formatDimensionInput(object.xCm, unit))
setY(formatDimensionInput(object.yCm, unit))
setRotation(String(Math.round(object.rotationDeg)))
setColor(object.color ?? DEFAULT_COLOR)
setGridSize(String(spacingFromCm(object.gridSizeCm, unit)))
@@ -77,14 +78,20 @@ export function Inspector({
}
// 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).
// ≥ the server minimum) but not x/y, which may be zero or negative.
//
// The no-op guard compares the field's TEXT against the string this field
// renders for the current value. With compound entry there is no single
// display number left to compare, and a numeric tolerance would be worse:
// a bed dragged to some arbitrary cm renders as, say, 2 7.9″, and merely
// tabbing through the field would snap it to a whole inch. Text equality means
// "you didn't edit this", which is what the guard is actually for. Typing the
// same value a different way (2' 7" vs 2 7″) falls through to the cm compare
// below and is still a no-op.
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 (raw.trim() === formatDimensionInput(current, unit)) return
const cm = parseDimension(raw, unit)
if (cm === null) return // unparseable: leave the row alone rather than commit a zero
if (positive && cm < MIN_DIMENSION_CM) return
if (cm !== current) apply(cm)
}
@@ -101,6 +108,7 @@ export function Inspector({
}
const u = dimensionUnitLabel(unit)
const inputMode = dimensionInputMode(unit)
return (
<div ref={rootRef} className="flex flex-col gap-3">
@@ -141,9 +149,8 @@ export function Inspector({
<TextField
label={`Width (${u})`}
name="width"
type="number"
inputMode="decimal"
step="any"
type="text"
inputMode={inputMode}
value={width}
onChange={(e) => setWidth(e.target.value)}
onBlur={() => commitDim(width, object.widthCm, (cm) => patch({ widthCm: cm }), true)}
@@ -151,9 +158,8 @@ export function Inspector({
<TextField
label={`Height (${u})`}
name="height"
type="number"
inputMode="decimal"
step="any"
type="text"
inputMode={inputMode}
value={height}
onChange={(e) => setHeight(e.target.value)}
onBlur={() => commitDim(height, object.heightCm, (cm) => patch({ heightCm: cm }), true)}
@@ -161,9 +167,8 @@ export function Inspector({
<TextField
label={`X (${u})`}
name="x"
type="number"
inputMode="decimal"
step="any"
type="text"
inputMode={inputMode}
value={x}
onChange={(e) => setX(e.target.value)}
onBlur={() => commitDim(x, object.xCm, (cm) => patch({ xCm: cm }))}
@@ -171,9 +176,8 @@ export function Inspector({
<TextField
label={`Y (${u})`}
name="y"
type="number"
inputMode="decimal"
step="any"
type="text"
inputMode={inputMode}
value={y}
onChange={(e) => setY(e.target.value)}
onBlur={() => commitDim(y, object.yCm, (cm) => patch({ yCm: cm }))}