Season view: filter the editor to a year (#54) (#66)
Build image / build-and-push (push) Successful in 17s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #66.
This commit is contained in:
2026-07-21 05:47:34 +00:00
committed by steve
parent b96ca1ec0a
commit 8c5ddb21ec
17 changed files with 556 additions and 36 deletions
+69
View File
@@ -0,0 +1,69 @@
import { cn } from '@/lib/cn'
/**
* Which season the canvas is showing.
*
* "Now" is the live, editable garden — what is in the ground today. A year is a
* read-only view of everything whose time in the ground overlapped that calendar
* year, plops since removed included. They are genuinely different questions:
* "what's growing" versus "what did I grow", and only the first one is editable.
*
* Only years the garden holds data for are offered. A free numeric field invites
* a typo, and a typo'd year produces a confidently empty garden that reads as
* data loss rather than a mistake.
*/
export function SeasonPicker({
years,
value,
onChange,
}: {
years: number[]
value: number | null
onChange: (year: number | null) => void
}) {
if (years.length === 0) return null
return (
<label className="flex items-center gap-1.5 text-xs text-muted">
<span>Season</span>
<select
value={value ?? ''}
onChange={(e) => onChange(e.target.value === '' ? null : Number(e.target.value))}
className={cn(
'rounded-md border border-border bg-surface px-1.5 py-1 text-xs text-fg outline-none',
'focus-visible:ring-2 focus-visible:ring-accent/40',
)}
>
<option value="">Now</option>
{years.map((y) => (
<option key={y} value={y}>
{y}
</option>
))}
</select>
</label>
)
}
/**
* The banner that stops you thinking you're looking at now. Editing the past by
* accident is the failure mode this whole feature introduces, so the state is
* stated rather than implied by a dropdown you set a while ago — with the way
* back to the live garden right next to it.
*/
export function SeasonBanner({ year, onExit }: { year: number; onExit: () => void }) {
return (
<div className="flex flex-wrap items-center gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-sm">
<span className="font-medium text-amber-800 dark:text-amber-300">Viewing {year}</span>
<span className="text-xs text-amber-800/80 dark:text-amber-300/80">
read-only, including plantings since removed
</span>
<button
type="button"
onClick={onExit}
className="ml-auto rounded px-1.5 py-0.5 text-xs font-medium text-amber-900 underline outline-none hover:no-underline focus-visible:ring-2 focus-visible:ring-accent/40 dark:text-amber-200"
>
Back to now
</button>
</div>
)
}
+10
View File
@@ -32,6 +32,12 @@ interface EditorState {
railTab: string | null
setRailTab: (tab: string | null) => void
// Which season the canvas is showing: null is "now" (live and editable), a
// year is a read-only view of what was in the ground that year. Ephemeral —
// which year you were last looking at is not a property of the garden.
seasonYear: number | null
setSeasonYear: (year: number | null) => void
// The plant armed for placing plops (set after the PlantPicker choice); stays
// armed for repeat-placement until cleared (Escape / done). null = not placing.
armedPlant: Plant | null
@@ -79,6 +85,9 @@ export const useEditorStore = create<EditorState>((set) => ({
railTab: null,
setRailTab: (tab) => set({ railTab: tab }),
seasonYear: null,
setSeasonYear: (year) => set({ seasonYear: year }),
armedPlant: null,
setArmedPlant: (p) => set({ armedPlant: p }),
@@ -104,5 +113,6 @@ export const useEditorStore = create<EditorState>((set) => ({
liveObject: null,
livePlanting: null,
railTab: null,
seasonYear: null,
}),
}))