Non-occluding mobile inspector + always-visible mode bar (#101) #114

Merged
steve merged 2 commits from feat/mobile-inspector-peek into main 2026-07-22 13:49:52 +00:00
2 changed files with 24 additions and 19 deletions
Showing only changes of commit 78ee892b64 - Show all commits
+7 -4
View File
@@ -14,11 +14,14 @@ import { cn } from '@/lib/cn'
* - Selecting an object must land you in the inspector with no extra click. * - 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 * 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. * 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 * - The canvas has to stay worth watching while the agent edits it, so the rail
* rail is a fixed 20rem column and closes completely when nothing needs it. * closes completely when nothing needs it.
* *
* On a phone the same tabs render in a bottom sheet, which is where the * Layout differs by breakpoint. Desktop: a fixed 20rem column beside the canvas.
* inspector already lived. * 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 { export interface RailTab {
+17 -15
View File
@@ -225,17 +225,25 @@ export function GardenEditorPage() {
} }
}, [selectedId, selectedPlantingId, setRailTab, setMode]) }, [selectedId, selectedPlantingId, setRailTab, setMode])
const exitFocus = () => { // Clearing the selection also closes the inspector via the selection effect
setFocusedObject(null) // above; shared by exitFocus, the mode bar, and the rail's close so the
setArmedPlant(null) // deselect logic lives in one place.
const clearSelection = () => {
select(null) select(null)
selectPlanting(null) selectPlanting(null)
} }
// The mobile mode bar. Journal/Assistant are panel modes, so they open the const exitFocus = () => {
// rail sheet; Fixtures/Plants are canvas modes, so they close a panel rail (but setFocusedObject(null)
// leave an inspector, which is about the selection, alone). Tapping Fixtures setArmedPlant(null)
// means going back to arranging objects, so it steps out of a focused bed. 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) => { const selectMode = (m: EditorMode) => {
setMode(m) setMode(m)
if (m === 'journal') { if (m === 'journal') {
Outdated
Review

🟡 Inspector-closes-on-deselect rule duplicated between selectMode and EditorRail onClose instead of shared

error-handling, maintainability · flagged by 2 models

  • web/src/pages/GardenEditorPage.tsx:249 (selectMode, canvas-mode branch) — the deselect-on-close guard only fires if (railTab === 'inspector'), a direct check against the current rail tab. Selection (selectedId/selectedPlantingId) and railTab can diverge: selecting an object sets railTab to 'inspector' via the effect at GardenEditorPage.tsx:216-226, but tapping Journal afterwards moves railTab to 'journal' without clearing the selection (the journal/assistant bra…

🪰 Gadfly · advisory

🟡 **Inspector-closes-on-deselect rule duplicated between selectMode and EditorRail onClose instead of shared** _error-handling, maintainability · flagged by 2 models_ - `web/src/pages/GardenEditorPage.tsx:249` (`selectMode`, canvas-mode branch) — the deselect-on-close guard only fires `if (railTab === 'inspector')`, a direct check against the *current* rail tab. Selection (`selectedId`/`selectedPlantingId`) and `railTab` can diverge: selecting an object sets `railTab` to `'inspector'` via the effect at `GardenEditorPage.tsx:216-226`, but tapping **Journal** afterwards moves `railTab` to `'journal'` without clearing the selection (the `journal`/`assistant` bra… <sub>🪰 Gadfly · advisory</sub>
@@ -244,12 +252,7 @@ export function GardenEditorPage() {
} else if (m === 'assistant') { } else if (m === 'assistant') {
setRailTab('chat') setRailTab('chat')
} else { } else {
// A canvas mode closes whatever's in the rail; the inspector is about the clearSelection()
// selection, so closing it deselects too.
if (railTab === 'inspector') {
select(null)
selectPlanting(null)
}
setRailTab(null) setRailTab(null)
if (m === 'fixtures' && focusedObjectId != null) exitFocus() 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 // deselects; dismissing a panel leaves the canvas as you had it. Any
// panel rail (journal/history/chat) drops back to a canvas mode. // panel rail (journal/history/chat) drops back to a canvas mode.
if (railTab === 'inspector') { if (railTab === 'inspector') {
select(null) clearSelection()
selectPlanting(null)
} else { } else {
// Back to a canvas mode — Plants if you're still inside a bed, else // Back to a canvas mode — Plants if you're still inside a bed, else
// Fixtures. (Hardcoding Fixtures here docked the object palette inside // Fixtures. (Hardcoding Fixtures here docked the object palette inside