From 3b9c9086b606b73dd41effed8cf81a50aea0899f Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Wed, 22 Jul 2026 09:40:51 -0400 Subject: [PATCH 1/2] Non-occluding mobile inspector + always-visible mode bar (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things at once, because they're one layout: on a phone, selecting a bed opened the rail as a bottom sheet that COVERED the whole garden, and opening Journal/Assistant hid the mode bar until you closed it. You couldn't see what you were editing, or switch modes without backing out. Now the rail is an in-flow PEEK. Instead of `fixed bottom-0 max-h-70vh` overlaying everything, EditorRail on mobile is a ≤50vh flex child the editor places BETWEEN the canvas and the mode bar: canvas (flex-1, shrinks) → rail peek (≤50vh) → mode bar (always shown) So the garden stays visible in the top band, the mode bar stays reachable below, and you can tap another mode straight from an open panel. The contextual tool strip yields to the peek (gated on !railTab), and tapping a canvas mode closes the panel (deselecting if it was the inspector). Desktop is untouched — the same EditorRail is the right-hand column there (`md:` styles), the mode bar stays `md:hidden`. Answers Steve's two calls from the end-of-run questions: always-visible mode bar + non-occluding inspector. Verified live at 390px (inspector + journal peeks keep the garden and mode bar visible; mode switching works) and 1280px (desktop unchanged). tsc + vitest + build green; DESIGN updated. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ --- DESIGN.md | 2 +- web/src/editor/EditorRail.tsx | 10 +++-- web/src/pages/GardenEditorPage.tsx | 66 +++++++++++++++++------------- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 0920f13..3d975c7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -129,7 +129,7 @@ React 19 + TypeScript + Vite + Tailwind 4 (`@tailwindcss/vite`), `@tanstack/reac - **Routes:** `/login`, `/register`, `/gardens` (list), `/gardens/:id` (editor, `?focus=objectId`), `/plants` (catalog). Auth guard on the router root via `/auth/me`. - **State:** TanStack Query for all server state (editor keyed on `gardens/:id/full`; optimistic mutations with version-conflict rollback). One small Zustand store for ephemeral editor state only: viewport, selection, focused object, active tool, in-flight drag, and the mobile `mode`. -- **Mobile-first editor: one primary mode (#99).** On a phone the canvas is the whole screen; a bottom mode bar switches which tools dock beneath it — **Fixtures** (the object palette), **Plants** (shown once a bed is focused; focusing a bed puts you in this mode — a "Recent" strip of what you've most recently planted *in this garden* (#100, derived from plantings, not the manual tray) for one-tap re-arming, the seed tray, the picker, and a clump/rows **fill** control that runs the region fill (#77) the UI couldn't reach before), **Journal**, **Assistant** (the last two open the rail sheet; Assistant is hidden with no model). This replaces the old phone layout where a stacked control column shoved the garden into a corner. Desktop keeps its side-column layout (the mode bar is `md:hidden`) and treats `mode` as an inert hint. History stays reachable as a rail sub-tab rather than a fifth primary mode. +- **Mobile-first editor: one primary mode (#99).** On a phone the canvas is the whole screen; a bottom mode bar switches which tools dock beneath it — **Fixtures** (the object palette), **Plants** (shown once a bed is focused; focusing a bed puts you in this mode — a "Recent" strip of what you've most recently planted *in this garden* (#100, derived from plantings, not the manual tray) for one-tap re-arming, the seed tray, the picker, and a clump/rows **fill** control that runs the region fill (#77) the UI couldn't reach before), **Journal**, **Assistant** (Assistant hidden with no model). This replaces the old phone layout where a stacked control column shoved the garden into a corner. The mode bar is **always visible**, and the rail (inspector, journal, history, assistant) is an **in-flow peek** (#101): a ≤50vh panel the editor's flex column places BETWEEN the canvas and the mode bar, so the canvas flexes to keep the garden visible above it and the mode bar reachable below — selecting a bed no longer hides the whole garden, and you can switch modes without closing a panel. Desktop keeps its side-column layout (the mode bar is `md:hidden`, the rail is the right column) and treats `mode` as an inert hint. History stays reachable as a rail sub-tab rather than a fifth primary mode. - **Editor components (`web/src/editor/`):** `GardenCanvas` (svg root + viewport g), `useViewport` (use-gesture pan/zoom/pinch), `ObjectShape`, `PlopMarker` (semantic-zoom branching), `Palette` (drag-to-place object kinds), `EditorRail` (the one side panel), `Inspector`, `HistoryPanel`, `PlantPicker`. - **One rail, tabs inside it.** The inspector, history, journal and assistant all want the same strip of screen; rather than each bolting on its own chrome they are tabs in `EditorRail` — so the canvas is one width instead of a different width per panel, and adding a panel is adding a tab. Selecting an object switches to the Inspector tab automatically, so the rail is never something you operate before you can edit; on a phone the same tabs render in the bottom sheet the inspector already used. Pure geometry helpers (local↔world transforms, unit formatting) in `web/src/lib/geometry.ts`, unit-tested. diff --git a/web/src/editor/EditorRail.tsx b/web/src/editor/EditorRail.tsx index a836274..15e2106 100644 --- a/web/src/editor/EditorRail.tsx +++ b/web/src/editor/EditorRail.tsx @@ -47,9 +47,13 @@ export function EditorRail({ return (
diff --git a/web/src/pages/GardenEditorPage.tsx b/web/src/pages/GardenEditorPage.tsx index dac74ee..f6514c6 100644 --- a/web/src/pages/GardenEditorPage.tsx +++ b/web/src/pages/GardenEditorPage.tsx @@ -244,7 +244,13 @@ export function GardenEditorPage() { } else if (m === 'assistant') { setRailTab('chat') } else { - if (railTab === 'journal' || railTab === 'chat') setRailTab(null) + // A canvas mode closes whatever's in the rail; the inspector is about the + // selection, so closing it deselects too. + if (railTab === 'inspector') { + select(null) + selectPlanting(null) + } + setRailTab(null) if (m === 'fixtures' && focusedObjectId != null) exitFocus() } } @@ -629,11 +635,37 @@ export function GardenEditorPage() { )}
- {/* Mobile bottom: contextual tools for the current mode + the mode switch - bar (#99). md:hidden — desktop uses the left column. A panel-mode rail - (journal/assistant) or the inspector overlays this while open. */} + {/* The rail sits between the canvas and the mode bar. On mobile it's an + in-flow PEEK (≤50vh), so the garden stays visible above it and the mode + bar below (see EditorRail); on desktop it's the right-hand column. */} + {railTab && ( + { + // Only the inspector is *about* the selection, so only closing it + // deselects; dismissing a panel leaves the canvas as you had it. Any + // panel rail (journal/history/chat) drops back to a canvas mode. + if (railTab === 'inspector') { + select(null) + selectPlanting(null) + } else { + // Back to a canvas mode — Plants if you're still inside a bed, else + // Fixtures. (Hardcoding Fixtures here docked the object palette inside + // a focused bed.) + setMode(focusedObjectId != null ? 'plants' : 'fixtures') + } + setRailTab(null) + }} + /> + )} + + {/* Mobile bottom: contextual tools for the current canvas mode + the + always-visible mode switch bar (#99/#101). md:hidden — desktop uses the + left column. The tool strip yields to a rail peek when one is open. */}
- {canEdit && (mode === 'fixtures' || mode === 'plants') && ( + {canEdit && !railTab && (mode === 'fixtures' || mode === 'plants') && (
{mode === 'fixtures' && } {mode === 'plants' && @@ -677,30 +709,6 @@ export function GardenEditorPage() {
- {railTab && ( - { - // Only the inspector is *about* the selection, so only closing it - // deselects; dismissing a panel leaves the canvas as you had it. Any - // panel rail (journal/history/chat) drops back to a canvas mode so the - // mobile mode bar reappears. - if (railTab === 'inspector') { - select(null) - selectPlanting(null) - } else { - // Back to a canvas mode so the mode bar reappears — Plants if you're - // still inside a bed, else Fixtures. (Hardcoding Fixtures here docked - // the object palette inside a focused bed.) - setMode(focusedObjectId != null ? 'plants' : 'fixtures') - } - setRailTab(null) - }} - /> - )} - {picker && ( Date: Wed, 22 Jul 2026 09:49:28 -0400 Subject: [PATCH 2/2] Address inspector-peek review: stale docs + selection cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gadfly on #101: - EditorRail's module doc still described a "fixed 20rem column / bottom sheet"; updated to the in-flow peek (desktop column, phone ≤50vh peek between canvas and mode bar). - (correctness) A canvas-mode tap only deselected when railTab was 'inspector', so a selection made, then routed through Journal/Assistant, survived a return to Fixtures/Plants — the canvas kept highlighting it with no inspector. Canvas modes now clear the selection unconditionally. - Dedup: the "closing the inspector deselects" pair lived in both selectMode and the rail's onClose; extracted a clearSelection() helper (also used by exitFocus) and fixed the now-stale "leave an inspector alone" comment. tsc + vitest + build green. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ --- web/src/editor/EditorRail.tsx | 11 ++++++---- web/src/pages/GardenEditorPage.tsx | 32 ++++++++++++++++-------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/web/src/editor/EditorRail.tsx b/web/src/editor/EditorRail.tsx index 15e2106..591a27c 100644 --- a/web/src/editor/EditorRail.tsx +++ b/web/src/editor/EditorRail.tsx @@ -14,11 +14,14 @@ import { cn } from '@/lib/cn' * - Selecting an object must land you in the inspector with no extra click. * The editor watches the selection and switches to that tab itself, so the * rail never becomes a thing you have to operate before you can edit. - * - The canvas has to stay worth watching while the agent edits it, so the - * rail is a fixed 20rem column and closes completely when nothing needs it. + * - The canvas has to stay worth watching while the agent edits it, so the rail + * closes completely when nothing needs it. * - * On a phone the same tabs render in a bottom sheet, which is where the - * inspector already lived. + * Layout differs by breakpoint. Desktop: a fixed 20rem column beside the canvas. + * Phone: an in-flow PEEK (#101) — a ≤50vh panel the editor's flex column places + * BETWEEN the canvas and the always-visible mode bar, so the canvas shrinks to + * keep the garden visible above it and the mode bar reachable below, rather than + * a bottom sheet that covered the whole garden. */ export interface RailTab { diff --git a/web/src/pages/GardenEditorPage.tsx b/web/src/pages/GardenEditorPage.tsx index f6514c6..6d0be53 100644 --- a/web/src/pages/GardenEditorPage.tsx +++ b/web/src/pages/GardenEditorPage.tsx @@ -225,17 +225,25 @@ export function GardenEditorPage() { } }, [selectedId, selectedPlantingId, setRailTab, setMode]) - const exitFocus = () => { - setFocusedObject(null) - setArmedPlant(null) + // Clearing the selection also closes the inspector via the selection effect + // above; shared by exitFocus, the mode bar, and the rail's close so the + // deselect logic lives in one place. + const clearSelection = () => { select(null) selectPlanting(null) } - // The mobile mode bar. Journal/Assistant are panel modes, so they open the - // rail sheet; Fixtures/Plants are canvas modes, so they close a panel rail (but - // leave an inspector, which is about the selection, alone). Tapping Fixtures - // means going back to arranging objects, so it steps out of a focused bed. + const exitFocus = () => { + setFocusedObject(null) + setArmedPlant(null) + clearSelection() + } + + // The mobile mode bar. Journal/Assistant are panel modes, so they open the rail + // as a peek; Fixtures/Plants are canvas modes — a fresh intent — so they close + // whatever's in the rail AND drop any lingering selection (which the canvas + // would otherwise keep highlighted, e.g. after routing a selection through + // Journal and back). Fixtures also steps out of a focused bed. const selectMode = (m: EditorMode) => { setMode(m) if (m === 'journal') { @@ -244,12 +252,7 @@ export function GardenEditorPage() { } else if (m === 'assistant') { setRailTab('chat') } else { - // A canvas mode closes whatever's in the rail; the inspector is about the - // selection, so closing it deselects too. - if (railTab === 'inspector') { - select(null) - selectPlanting(null) - } + clearSelection() setRailTab(null) if (m === 'fixtures' && focusedObjectId != null) exitFocus() } @@ -648,8 +651,7 @@ export function GardenEditorPage() { // deselects; dismissing a panel leaves the canvas as you had it. Any // panel rail (journal/history/chat) drops back to a canvas mode. if (railTab === 'inspector') { - select(null) - selectPlanting(null) + clearSelection() } else { // Back to a canvas mode — Plants if you're still inside a bed, else // Fixtures. (Hardcoding Fixtures here docked the object palette inside -- 2.54.0