diff --git a/CLAUDE.md b/CLAUDE.md index 2223ad3..ff148e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,7 +134,10 @@ Conventions that follow from it: a view, `cm` changes only when the person types. Never re-parse the display string on save — "29′ 6.3″" is the nearest tenth of an inch, and parsing it back is how a no-change Save turned 900 cm into 899.922 (and bumped the - version, and wrote a bogus history entry). The inspector guards the same way. + version, and wrote a bogus history entry). The inspector still keeps display + strings but gets the same result by refusing to commit text that still equals + the formatted original (`commitDim`); either way, a no-op save sends exactly + what was loaded — or nothing. - **"Today" is the browser's local day**, from `today()` in `web/src/lib/dates.ts`, and the UI always sends it: journal `observedAt`, plop/fill `plantedAt`, `removedAt`. The server's UTC default is only for diff --git a/web/src/components/gardens/CopyDialog.tsx b/web/src/components/gardens/CopyDialog.tsx index a741a0b..fdc107d 100644 --- a/web/src/components/gardens/CopyDialog.tsx +++ b/web/src/components/gardens/CopyDialog.tsx @@ -1,4 +1,4 @@ -import { useState, type FormEvent } from 'react' +import { useEffect, useState, type FormEvent } from 'react' import { useNavigate } from '@tanstack/react-router' import { Alert } from '@/components/ui/Alert' import { Button } from '@/components/ui/Button' @@ -27,7 +27,13 @@ export function CopyDialog({ garden, onClose }: { garden: Garden; onClose: () => const from = (parsePlanName(garden.name)?.year ?? new Date().getFullYear()) + 1 const year = nextPlanYear(base, names, from) const [name, setName] = useState(() => planNameFor(base, year)) + const [touched, setTouched] = useState(false) const [error, setError] = useState(null) + // The gardens list can still be loading when this opens; until the person + // edits the name, keep the proposal in step with what the list says is free. + useEffect(() => { + if (!touched) setName(planNameFor(base, year)) + }, [base, year, touched]) // The API allows duplicate names; say so rather than let two gardens read as // the same season's plan. const taken = names.some((n) => n.trim() === name.trim()) @@ -52,7 +58,17 @@ export function CopyDialog({ garden, onClose }: { garden: Garden; onClose: () => A copy of {garden.name} to scheme in — rearrange freely, the original stays put. Beds and what's planted come along; shares and the public link don't.

- setName(e.target.value)} /> + { + setTouched(true) + setName(e.target.value) + }} + />

Keep the “— {year}” and it shows up as that season's plan in the editor.

{taken && You already have a garden called “{name.trim()}” — pick another name so the two don't read as the same plan.} {error && {error}} diff --git a/web/src/components/gardens/GardenCard.tsx b/web/src/components/gardens/GardenCard.tsx index 61b9c06..63f23f3 100644 --- a/web/src/components/gardens/GardenCard.tsx +++ b/web/src/components/gardens/GardenCard.tsx @@ -15,10 +15,11 @@ import { GardenThumb } from './GardenThumb' const COUNTED_KINDS = ['bed', 'grow_bag', 'container', 'in_ground', 'tree', 'path', 'structure'] /** - * One garden as a card: the plot thumbnail (a link into the editor), name (a - * plan copy shows its base name and a ` plan` tag), size, a counts line, who it's shared with, and a footer - * of Open + share / copy / edit / delete. A garden shared WITH you shows its - * role and a leave action instead of the owner's tools. + * One garden as a card: the plot thumbnail (a link into the editor), name, size, + * a counts line, who it's shared with, and a footer of Open + share / copy / + * edit / delete. A plan copy shows its base name with a ` plan` tag. A + * garden shared WITH you shows its role and a leave action instead of the + * owner's tools. */ export function GardenCard({ garden, @@ -40,10 +41,11 @@ export function GardenCard({ const owner = currentUserId != null && garden.ownerId === currentUserId const full = useGardenFull(garden.id) const shares = useQuery({ ...sharesQueryOptions(garden.id), enabled: owner }) + const plan = parsePlanName(garden.name) const planYear = planYearOf(garden.name) // A plan's year is the point of its name, and the first thing truncation // would eat ("Back Yard — 20…"); show the base name and put the year on the tag. - const title = planYear != null ? (parsePlanName(garden.name)?.base ?? garden.name) : garden.name + const title = planYear != null && plan ? plan.base : garden.name const meta = useMemo(() => { const data = full.data diff --git a/web/src/components/gardens/GardenThumb.tsx b/web/src/components/gardens/GardenThumb.tsx index 3c0462d..a6d8c9a 100644 --- a/web/src/components/gardens/GardenThumb.tsx +++ b/web/src/components/gardens/GardenThumb.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { localToWorld } from '@/lib/geometry' import type { FullGarden } from '@/lib/objects' +import { FALLBACK_PLANT_COLOR } from '@/lib/plants' import { objectStyle, rectRadius } from '@/editor/kinds' /** @@ -72,7 +73,7 @@ export function GardenThumb({ const o = byId.get(p.objectId) if (!o) return null const w = localToWorld({ x: p.xCm, y: p.yCm }, { x: o.xCm, y: o.yCm }, o.rotationDeg) - return + return })} ) diff --git a/web/src/editor/Canvas.tsx b/web/src/editor/Canvas.tsx index 3021de2..3ff9058 100644 --- a/web/src/editor/Canvas.tsx +++ b/web/src/editor/Canvas.tsx @@ -12,7 +12,7 @@ import { import { clampScale, type Point } from '@/lib/geometry' import { monogramInk } from '@/lib/monogram' import { useCreateObject, useCreatePlanting, useUpdateObject, useUpdatePlanting } from '@/lib/objects' -import type { Plant } from '@/lib/plants' +import { FALLBACK_PLANT_COLOR, type Plant } from '@/lib/plants' import type { EditorPlanting } from '@/lib/plantings' import { formatSize } from '@/lib/units' import { kindDef, objectStyle, rectRadius } from './kinds' @@ -35,10 +35,6 @@ import { import { useEditorStore, type Viewport } from './store' import type { EditorGarden, EditorObject } from './types' -// A plop whose plant is missing from the catalog (a shared garden's private -// plant) still needs a color to be drawn in. -const FALLBACK_PLANT_COLOR = '#97a97c' - const WHEEL_SENSITIVITY = 0.0016 const ANIM_MS = 520 const REFIT_THRESHOLD_PX = 60 diff --git a/web/src/editor/Inspector.tsx b/web/src/editor/Inspector.tsx index 1e25c38..b34f21c 100644 --- a/web/src/editor/Inspector.tsx +++ b/web/src/editor/Inspector.tsx @@ -8,7 +8,7 @@ import { Tag } from '@/components/ui/Tag' import { Toggle } from '@/components/ui/Toggle' import { cn } from '@/lib/cn' import { useDeleteObject, useRemovePlanting, useUpdateObject, useUpdatePlanting } from '@/lib/objects' -import type { Plant } from '@/lib/plants' +import { FALLBACK_PLANT_COLOR, type Plant } from '@/lib/plants' import type { EditorPlanting } from '@/lib/plantings' import { cmFromSpacing, @@ -356,7 +356,7 @@ export function PlopInspector({ return (
- + {plant?.name ?? 'Unknown plant'} {noteCount > 0 && (