From 0169e32584d7ba15bd052b779efdc0062ade7d94 Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Tue, 21 Jul 2026 00:49:23 -0400 Subject: [PATCH] Address Gadfly review on the units fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five findings, all fair: The soft-minimum hint contradicted itself. "did you mean ft?" made sense when the grid field was labelled inches sitting under a feet field — which is exactly what this PR removed. The remaining failure mode is different: someone reaching for plant spacing, which belongs to the bed, not the garden. The hint now says that, and points at where the bed grid actually lives. The stale "1 typed meaning one foot" comments went with it. gridNote was a three-level nested ternary; it is now gridNoteFor, with the same logic in explicit branches and the reasoning in one doc comment rather than threaded between the arms. cmFromDisplay(parseFloat(gridSize), unit) was computed once for the hint and again in the submit handler. Hoisted to a single per-render value so the two can't disagree about what was entered. roundCm's comment called the 1e-6 cm grain "1 nanometer"; it is 10 nm. Corrected — the constant was always fine, only the comment was wrong. Extracted the duplicated overlay-badge class string. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ --- .claude/settings.json | 5 +++ .../components/gardens/GardenFormModal.tsx | 16 +++++--- web/src/editor/GardenCanvas.tsx | 41 +++++++++++-------- web/src/lib/units.ts | 12 +++--- 4 files changed, 45 insertions(+), 29 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9030888 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "frontend-design@claude-plugins-official": true + } +} diff --git a/web/src/components/gardens/GardenFormModal.tsx b/web/src/components/gardens/GardenFormModal.tsx index 46a6c82..70375be 100644 --- a/web/src/components/gardens/GardenFormModal.tsx +++ b/web/src/components/gardens/GardenFormModal.tsx @@ -68,6 +68,14 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose: 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) @@ -86,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 = cmFromDisplay(parseFloat(gridSize), unit) if (!isValidDimensionCm(gridSizeCm)) { setFormError('Grid size must be between 1 cm and 100 m.') return @@ -130,10 +137,6 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose: } const unitLabel = dimensionUnitLabel(unit) - // Soft floor: warn, don't refuse. A sub-4″ garden grid is almost always "1" - // typed meaning one foot, but it's a legitimate choice for a tiny garden. - const gridEntered = cmFromDisplay(parseFloat(gridSize), unit) - const gridTooFine = Number.isFinite(gridEntered) && gridEntered > 0 && gridEntered < MIN_GARDEN_GRID_CM return ( @@ -201,7 +204,8 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose: {gridTooFine && (

- That's a {formatCm(gridEntered, unit)} grid for the whole garden — did you mean {unitLabel}? + {formatCm(gridSizeCm, unit)} is very fine for a whole-garden grid. Plant spacing lives on each bed + (Bed grid in the inspector), not here.

)} diff --git a/web/src/editor/GardenCanvas.tsx b/web/src/editor/GardenCanvas.tsx index f035ec7..c77bfc3 100644 --- a/web/src/editor/GardenCanvas.tsx +++ b/web/src/editor/GardenCanvas.tsx @@ -8,7 +8,7 @@ import { type Rect, type Size, } from '@/lib/geometry' -import { formatCm } from '@/lib/units' +import { formatCm, type UnitPref } from '@/lib/units' import { useCreateObject, useCreatePlanting } from '@/lib/objects' import type { Plant } from '@/lib/plants' import type { EditorPlanting } from '@/lib/plantings' @@ -24,8 +24,28 @@ import type { EditorGarden, EditorObject } from './types' const GRID_MIN_CELL_PX = 6 const GRID_OPACITY = 0.18 +const OVERLAY_BADGE_CLASS = 'rounded-md bg-surface/80 px-2 py-1 text-xs text-muted backdrop-blur' const BED_GRID_MAX_LINES = 200 // safety cap on lines drawn inside one bed +/** + * What to tell the user about the grid they're looking at, or null when the + * drawn lines are simply the garden's own grid and need no explanation. Two + * states are worth naming rather than leaving them to infer: lines drawn every + * Nth grid cell (the grid was too fine to draw at this zoom), and — degenerate — + * a grid so fine no multiple of it is drawable, which matters most when snapping + * is on and would otherwise be invisibly active (#47). + */ +function gridNoteFor( + drawnCm: number | null, + gridCm: number, + snapToGrid: boolean, + unit: UnitPref, +): string | null { + if (drawnCm == null) return snapToGrid ? 'grid too fine to draw — zoom in' : null + if (drawnCm === gridCm) return null + return `grid every ${formatCm(drawnCm, unit)}` +} + /** The world-space bounds rect of an object (center + size → top-left rect). */ function objectRect(o: EditorObject): Rect { return { x: o.xCm - o.widthCm / 2, y: o.yCm - o.heightCm / 2, w: o.widthCm, h: o.heightCm } @@ -113,16 +133,7 @@ export function GardenCanvas({ // the drawn lines are a subset of the snap positions, never a different grid. const gridCm = garden.gridSizeCm const drawnGridCm = visibleGridStepCm(gridCm, viewport.scale, GRID_MIN_CELL_PX) - // Two states worth explaining rather than leaving the user to infer: lines that - // are every Nth grid cell, and (degenerate) a grid too fine to draw at all. - const gridNote = - drawnGridCm == null - ? garden.snapToGrid - ? 'grid too fine to draw — zoom in' - : null - : drawnGridCm !== gridCm - ? `grid every ${formatCm(drawnGridCm, garden.unitPref)}` - : null + const gridNote = gridNoteFor(drawnGridCm, gridCm, garden.snapToGrid, garden.unitPref) const sorted = useMemo(() => [...objects].sort((a, b) => a.zIndex - b.zIndex), [objects]) const rendered = useMemo( @@ -300,12 +311,8 @@ export function GardenCanvas({
- - {viewport.scale.toFixed(2)} px/cm - - {gridNote && ( - {gridNote} - )} + {viewport.scale.toFixed(2)} px/cm + {gridNote && {gridNote}}