Editor: the toolkit is a rail tab on desktop, and the rail is wider #133
@@ -90,6 +90,11 @@ Conventions that follow from it:
|
|||||||
- **The editor's breakpoint is container width** (`PHONE_BREAKPOINT` = 760 in
|
- **The editor's breakpoint is container width** (`PHONE_BREAKPOINT` = 760 in
|
||||||
`web/src/editor/shared.ts`, measured with a ResizeObserver), not a media
|
`web/src/editor/shared.ts`, measured with a ResizeObserver), not a media
|
||||||
query. One component tree, two chromes; don't build a second page.
|
query. One component tree, two chromes; don't build a second page.
|
||||||
|
- **The desktop toolkit is a rail tab, not the handoff's left card** (Steve's
|
||||||
|
call, 2026-08-23: the card's width was better spent on the plan and the
|
||||||
|
rail). `Toolkit` renders `embedded` inside the rail as its first tab;
|
||||||
|
focusing a bed (double-click) switches the rail to it, a single click
|
||||||
|
selects into Plot. Don't bring the third column back.
|
||||||
- **Plant markers are monograms** derived from the name (`web/src/lib/monogram.ts`);
|
- **Plant markers are monograms** derived from the name (`web/src/lib/monogram.ts`);
|
||||||
the collision set is the whole catalog so the letters match everywhere.
|
the collision set is the whole catalog so the letters match everywhere.
|
||||||
`plant.icon` still exists in the API but nothing renders it.
|
`plant.icon` still exists in the API but nothing renders it.
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ React 19 + TypeScript + Vite + Tailwind 4 (`@tailwindcss/vite`), `@tanstack/reac
|
|||||||
- **Routes:** `/login`, `/register`, `/gardens` (list), `/gardens/:id` (editor, `?focus=objectId`), `/plants` (catalog), `/settings` (admin), `/g/:token` (public read-only). Auth guard on the router root via `/auth/me`. Every page renders its own nav; the root shell is only a Suspense boundary plus the toast stack.
|
- **Routes:** `/login`, `/register`, `/gardens` (list), `/gardens/:id` (editor, `?focus=objectId`), `/plants` (catalog), `/settings` (admin), `/g/:token` (public read-only). Auth guard on the router root via `/auth/me`. Every page renders its own nav; the root shell is only a Suspense boundary plus the toast stack.
|
||||||
- **State:** TanStack Query for all server state (editor keyed on `gardens/:id/full`; optimistic mutations with version-conflict rollback). One Zustand store (`web/src/editor/store.ts`) for ephemeral editor state only: camera `{tx, ty, s}`, selection, focused bed, the armed kind or plant (+ seed lot), rail tab, phone mode, journal scope, in-flight drag geometry.
|
- **State:** TanStack Query for all server state (editor keyed on `gardens/:id/full`; optimistic mutations with version-conflict rollback). One Zustand store (`web/src/editor/store.ts`) for ephemeral editor state only: camera `{tx, ty, s}`, selection, focused bed, the armed kind or plant (+ seed lot), rail tab, phone mode, journal scope, in-flight drag geometry.
|
||||||
- **The canvas (`web/src/editor/Canvas.tsx`)** is one SVG with one `translate/scale` group and its own pointer-event model (above). Object drags snap their center to a 3″ grid (the garden's own grid when it snaps) and commit ONE PATCH on release; plops drag in their bed's local frame and may overhang its edge by half their radius (the spacing rule). Fit and focus animate through a CSS transition on the group; drags and zooms don't. Corner handles on the selected object resize it — the one addition to the prototype, since the kinds' fixed default sizes can't make a 2′×8′ bed. Tap-to-place makes a one-plant plop (radius = spacing/2); "Fill the bed" (rows or clumps, `POST /objects/:id/fill`) is how you plant in bulk.
|
- **The canvas (`web/src/editor/Canvas.tsx`)** is one SVG with one `translate/scale` group and its own pointer-event model (above). Object drags snap their center to a 3″ grid (the garden's own grid when it snaps) and commit ONE PATCH on release; plops drag in their bed's local frame and may overhang its edge by half their radius (the spacing rule). Fit and focus animate through a CSS transition on the group; drags and zooms don't. Corner handles on the selected object resize it — the one addition to the prototype, since the kinds' fixed default sizes can't make a 2′×8′ bed. Tap-to-place makes a one-plant plop (radius = spacing/2); "Fill the bed" (rows or clumps, `POST /objects/:id/fill`) is how you plant in bulk.
|
||||||
- **Two chromes, one tree.** The editor measures its own container: ≥ 760px is the desktop workspace (toolkit 216px | plan | rail 336px, the rail's Plot/Journal/History/Assistant tabs); below it the phone layout — header, full-screen canvas, an in-flow **peek** (≤ 45% tall, docked between the canvas and the mode bar) for the inspector, journal or assistant, a tool strip for Build or Plants mode, and the always-visible mode bar. Focusing a bed on the phone switches to Plants mode; a plant can also be tapped straight into any bed without focusing.
|
- **Two chromes, one tree.** The editor measures its own container: ≥ 760px is the desktop workspace (plan | rail 400px, the rail's Toolkit/Plot/Journal/History/Assistant tabs — the toolkit was the handoff's 216px left card until 2026-08-23, and folding it into the rail gave the plan and the rail the width; focusing a bed switches the rail to Toolkit, selecting one switches it to Plot); below it the phone layout — header, full-screen canvas, an in-flow **peek** (≤ 45% tall, docked between the canvas and the mode bar) for the inspector, journal or assistant, a tool strip for Build or Plants mode, and the always-visible mode bar. Focusing a bed on the phone switches to Plants mode; a plant can also be tapped straight into any bed without focusing.
|
||||||
- **Plant markers** are the plant's color plus a monogram derived from its name (`web/src/lib/monogram.ts`, collisions resolved across the whole catalog so a letter means the same thing on every screen).
|
- **Plant markers** are the plant's color plus a monogram derived from its name (`web/src/lib/monogram.ts`, collisions resolved across the whole catalog so a letter means the same thing on every screen).
|
||||||
- **Undo** in the editor's header reverts the newest change set still in effect (not already reverted, not itself a revert) — after re-reading the history, because the cached list trails the canvas right after a placement, and undoing the step *before* the one you meant is the worst thing an undo button can do. The History tab offers every step, including undoing an undo.
|
- **Undo** in the editor's header reverts the newest change set still in effect (not already reverted, not itself a revert) — after re-reading the history, because the cached list trails the canvas right after a placement, and undoing the step *before* the one you meant is the worst thing an undo button can do. The History tab offers every step, including undoing an undo.
|
||||||
- **Seasons** are the years with planting data (`GET /gardens/:id/years`); the current year is live, any other is read-only. A *plan* is a whole-garden copy named `<garden> — <year>` (`web/src/lib/plan.ts`); the season control lists a garden's plan copies and, from inside one, the way back. The name is the only link the API keeps, which is deliberate — rename the copy and it is simply a garden.
|
- **Seasons** are the years with planting data (`GET /gardens/:id/years`); the current year is live, any other is read-only. A *plan* is a whole-garden copy named `<garden> — <year>` (`web/src/lib/plan.ts`); the season control lists a garden's plan copies and, from inside one, the way back. The name is the only link the API keeps, which is deliberate — rename the copy and it is simply a garden.
|
||||||
|
|||||||
@@ -177,7 +177,8 @@ export const Canvas = forwardRef<
|
|||||||
st.setSel(m ? null : { type: 'object', id: o.id })
|
st.setSel(m ? null : { type: 'object', id: o.id })
|
||||||
st.setArmedKind(null)
|
st.setArmedKind(null)
|
||||||
st.setGhost(null)
|
st.setGhost(null)
|
||||||
st.setTab('plot')
|
// Focusing is planting intent: the rail shows the toolkit's plant palette.
|
||||||
|
|
|||||||
|
st.setTab('toolkit')
|
||||||
if (!el) return
|
if (!el) return
|
||||||
const bw = o.rotationDeg % 180 ? o.heightCm : o.widthCm
|
const bw = o.rotationDeg % 180 ? o.heightCm : o.widthCm
|
||||||
const bh = o.rotationDeg % 180 ? o.widthCm : o.heightCm
|
const bh = o.rotationDeg % 180 ? o.widthCm : o.heightCm
|
||||||
|
|||||||
@@ -14,10 +14,16 @@ import { useEditorStore } from './store'
|
|||||||
import type { EditorObject } from './types'
|
import type { EditorObject } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The desktop editor's left card. Out of focus: the seven object kinds as pill
|
* The desktop editor's toolkit, the rail's first tab. Out of focus: the seven
|
||||||
* rows — click to arm (then click the plan), or drag one straight onto it. In a
|
* object kinds as pill rows — click to arm (then click the plan), or drag one
|
||||||
* focused bed it swaps to the plant list with a search, same arm/drag behavior,
|
* straight onto it. In a focused bed it swaps to the plant list with a search,
|
||||||
* plus the bed's bulk tools (fill, clear, scan a packet).
|
* same arm/drag behavior, plus the bed's bulk tools (fill, clear, scan a
|
||||||
|
* packet).
|
||||||
|
*
|
||||||
|
* It started life as the workspace's left card (the handoff's layout) and
|
||||||
|
* moved into the rail so the plan and the rail get the width the card took;
|
||||||
|
* the rail's tab body provides the card chrome and the scrolling, and the tab
|
||||||
|
* is the heading.
|
||||||
*/
|
*/
|
||||||
export function Toolkit({
|
export function Toolkit({
|
||||||
|
gitea-actions
commented
🟠 embedded=false default keeps dead non-embedded rendering path with no callers maintainability · flagged by 1 model
🪰 Gadfly · advisory 🟠 **embedded=false default keeps dead non-embedded rendering path with no callers**
_maintainability · flagged by 1 model_
- **`embedded = false` default retains a dead rendering path** (`web/src/editor/Toolkit.tsx:28,65,68`). There is exactly one `<Toolkit` call site (GardenEditorPage.tsx:587) and it always passes `embedded`. The `embedded = false` default makes the non-embedded branch — the `panel overflow-y-auto p-3.5` wrapper and the `<h6>Toolkit</h6>` heading — unreachable dead code. CLAUDE.md is explicit: "Don't add features or introduce abstractions beyond what the task requires." Either make `embedded` a req…
<sub>🪰 Gadfly · advisory</sub>
|
|||||||
unit,
|
unit,
|
||||||
@@ -55,10 +61,9 @@ export function Toolkit({
|
|||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel flex min-h-0 flex-col gap-2 overflow-y-auto overflow-x-hidden p-3.5">
|
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-x-hidden">
|
||||||
{!focused ? (
|
{!focused ? (
|
||||||
|
gitea-actions
commented
🟡 error-handling, maintainability · flagged by 2 models
🪰 Gadfly · advisory 🟡 **`embedded` prop is always passed true; non-embedded left-card branch is now dead code**
_error-handling, maintainability · flagged by 2 models_
- **`web/src/editor/Toolkit.tsx:65,68` — the `embedded` prop is always passed `true`, so the non-embedded (left-card) branch is dead code.** The only render site is `web/src/pages/GardenEditorPage.tsx:587`, which passes `embedded`; no test or phone-layout path renders `Toolkit` standalone. That leaves the `panel overflow-y-auto p-3.5` styling and the `{!embedded && <h6>Toolkit</h6>}` heading unreachable. Since CLAUDE.md/DESIGN.md now state the left card is gone deliberately ("Don't bring the thi…
<sub>🪰 Gadfly · advisory</sub>
|
|||||||
<>
|
<>
|
||||||
<h6 className="mx-1 mb-1.5 mt-1">Toolkit</h6>
|
|
||||||
{OBJECT_KINDS.map((k) => {
|
{OBJECT_KINDS.map((k) => {
|
||||||
const armed = armedKind === k.kind
|
const armed = armedKind === k.kind
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface Viewport {
|
|||||||
export type Selection = { type: 'object'; id: number } | { type: 'plop'; id: number }
|
export type Selection = { type: 'object'; id: number } | { type: 'plop'; id: number }
|
||||||
|
|
||||||
/** The right-hand rail's tabs (desktop). */
|
/** The right-hand rail's tabs (desktop). */
|
||||||
export type RailTab = 'plot' | 'journal' | 'history' | 'chat'
|
export type RailTab = 'toolkit' | 'plot' | 'journal' | 'history' | 'chat'
|
||||||
|
|
||||||
/** The phone's primary mode — which tools dock under the canvas. */
|
/** The phone's primary mode — which tools dock under the canvas. */
|
||||||
export type PhoneMode = 'build' | 'plants' | 'journal' | 'chat'
|
export type PhoneMode = 'build' | 'plants' | 'journal' | 'chat'
|
||||||
@@ -85,7 +85,7 @@ const TRANSIENT = {
|
|||||||
armedLotId: null,
|
armedLotId: null,
|
||||||
ghost: null,
|
ghost: null,
|
||||||
seasonYear: null,
|
seasonYear: null,
|
||||||
tab: 'plot' as RailTab,
|
tab: 'plot' as RailTab, // the garden summary, not the first tab: a fresh editor shows what is there before how to add to it
|
||||||
mode: 'build' as PhoneMode,
|
mode: 'build' as PhoneMode,
|
||||||
journalScope: null,
|
journalScope: null,
|
||||||
liveObject: null,
|
liveObject: null,
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ const MODES: { id: PhoneMode; label: string; icon: IconName }[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const TABS: { id: RailTab; label: string }[] = [
|
const TABS: { id: RailTab; label: string }[] = [
|
||||||
|
{ id: 'toolkit', label: 'Toolkit' },
|
||||||
{ id: 'plot', label: 'Plot' },
|
{ id: 'plot', label: 'Plot' },
|
||||||
{ id: 'journal', label: 'Journal' },
|
{ id: 'journal', label: 'Journal' },
|
||||||
{ id: 'history', label: 'History' },
|
{ id: 'history', label: 'History' },
|
||||||
@@ -124,9 +125,11 @@ const TABS: { id: RailTab; label: string }[] = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The editor: one canvas, two chromes. Above 760px of container width it is the
|
* The editor: one canvas, two chromes. Above 760px of container width it is the
|
||||||
* three-card workspace — toolkit, plan, rail; below, a phone layout with the
|
* two-card workspace — the plan, and a rail whose first tab is the toolkit
|
||||||
* canvas as the whole screen, a peek panel that docks between canvas and mode
|
* (the handoff drew it as a third card on the left; folding it into the rail
|
||||||
* bar, and a tool strip for the current mode. Same state, same components.
|
* gave the plan and the rail its width); below, a phone layout with the canvas
|
||||||
|
* as the whole screen, a peek panel that docks between canvas and mode bar,
|
||||||
|
* and a tool strip for the current mode. Same state, same components.
|
||||||
*/
|
*/
|
||||||
function Editor({
|
function Editor({
|
||||||
gid,
|
gid,
|
||||||
@@ -580,11 +583,7 @@ function Editor({
|
|||||||
// ── desktop ─────────────────────────────────────────────────────────────
|
// ── desktop ─────────────────────────────────────────────────────────────
|
||||||
const tabs = TABS.filter((t) => t.id !== 'chat' || hasAssistant)
|
const tabs = TABS.filter((t) => t.id !== 'chat' || hasAssistant)
|
||||||
const plot = inspector ?? <GardenSummary objects={objects} plantings={plantings} plantsById={plantsById} canEdit={canEdit} />
|
const plot = inspector ?? <GardenSummary objects={objects} plantings={plantings} plantsById={plantsById} canEdit={canEdit} />
|
||||||
|
const toolkit = (
|
||||||
return (
|
|
||||||
<div ref={rootRef} className="flex h-dvh flex-col bg-bg">
|
|
||||||
<Nav active="gardens" />
|
|
||||||
<div className="grid min-h-0 flex-1 gap-3.5 p-3.5 pt-0 [grid-template-columns:216px_minmax(0,1fr)_336px]">
|
|
||||||
<Toolkit
|
<Toolkit
|
||||||
unit={unit}
|
unit={unit}
|
||||||
plants={plants}
|
plants={plants}
|
||||||
@@ -600,7 +599,12 @@ function Editor({
|
|||||||
onClear={() => setClearing(true)}
|
onClear={() => setClearing(true)}
|
||||||
onScan={() => setScanning(true)}
|
onScan={() => setScanning(true)}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={rootRef} className="flex h-dvh flex-col bg-bg">
|
||||||
|
<Nav active="gardens" />
|
||||||
|
<div className="grid min-h-0 flex-1 gap-3.5 p-3.5 pt-0 [grid-template-columns:minmax(0,1fr)_400px]">
|
||||||
<div className="panel flex min-h-0 flex-col overflow-hidden">
|
<div className="panel flex min-h-0 flex-col overflow-hidden">
|
||||||
<div className="flex flex-wrap items-center gap-3 border-b border-divider px-[18px] py-3">
|
<div className="flex flex-wrap items-center gap-3 border-b border-divider px-[18px] py-3">
|
||||||
<h4 className="text-[19px]">{g.name}</h4>
|
<h4 className="text-[19px]">{g.name}</h4>
|
||||||
@@ -645,6 +649,7 @@ function Editor({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4">
|
<div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4">
|
||||||
|
{tab === 'toolkit' && toolkit}
|
||||||
{tab === 'plot' && plot}
|
{tab === 'plot' && plot}
|
||||||
{tab === 'journal' && journal}
|
{tab === 'journal' && journal}
|
||||||
{tab === 'history' && <HistoryTab canEdit={canEdit} undoLast={undoLast} />}
|
{tab === 'history' && <HistoryTab canEdit={canEdit} undoLast={undoLast} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user
⚪ 3-line comment exceeds one-line CLAUDE.md convention; third line describes a different code path and can rot
maintainability · flagged by 1 model
web/src/editor/Canvas.tsx:180–182). CLAUDE.md permits one short line maximum, for non-obvious WHY only. The added block is three lines; the third line ("A single click still selects into the Plot tab") describes a different code path's behavior and will silently lie if the single-click handler changes. The first line's WHY is worth keeping; the rest is noise.🪰 Gadfly · advisory