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}}