Address #125 review: memoized ink, one fallback color, reactive copy name
Build image / build-and-push (push) Successful in 10s
Build image / build-and-push (push) Successful in 10s
- monogramInk is memoized by color string; the canvas asks for every visible plop on every frame of a pan (Gadfly, 2/4 models). - FALLBACK_PLANT_COLOR lives in lib/plants and is used by the canvas, the inspector and the garden thumbnail instead of three raw '#97a97c's. - CopyDialog keeps its proposed "<base> — <year>" in step with the gardens list until the person edits the name, so a list that loads after the dialog opens can't leave a taken year in the field. - GardenCard: reflowed the summary comment; no dead fallback on a plan name that's already known to parse. - today() has one import path (lib/dates); the journal re-export is gone. - CLAUDE.md says what the inspector actually does (a text-compare guard) rather than claiming it uses LengthField. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -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<string | null>(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 <span className="font-semibold text-text">{garden.name}</span> to scheme in — rearrange freely, the
|
||||
original stays put. Beds and what's planted come along; shares and the public link don't.
|
||||
</p>
|
||||
<TextField label="Name" name="name" required autoFocus value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<TextField
|
||||
label="Name"
|
||||
name="name"
|
||||
required
|
||||
autoFocus
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setTouched(true)
|
||||
setName(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-ink-mute">Keep the “— {year}” and it shows up as that season's plan in the editor.</p>
|
||||
{taken && <Alert tone="info">You already have a garden called “{name.trim()}” — pick another name so the two don't read as the same plan.</Alert>}
|
||||
{error && <Alert>{error}</Alert>}
|
||||
|
||||
@@ -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 `<year> 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 `<year> 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
|
||||
|
||||
@@ -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 <circle key={p.id} cx={w.x} cy={w.y} r={p.radiusCm} fill={plantColor.get(p.plantId) ?? '#97a97c'} />
|
||||
return <circle key={p.id} cx={w.x} cy={w.y} r={p.radiusCm} fill={plantColor.get(p.plantId) ?? FALLBACK_PLANT_COLOR} />
|
||||
})}
|
||||
</svg>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user