From 256fa4f29fc66db99f3df8e94bbbc99f6f2aae51 Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Wed, 22 Jul 2026 21:31:35 -0400 Subject: [PATCH] Address #121 review: keep sign-out reachable, dvh peek, exact height MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fold the account menu into the editor's mobile strip. Hiding the global header removed the only sign-out on mobile in the editor; the strip now carries it, so the space win stays but sign-out is one tap away. - EditorRail peek cap vh → dvh, matching the dvh-bounded editor column, so it can't overrun the visible viewport and push the mode bar off-screen. - Mobile editor height 4rem → 3rem: with the header hidden, only
's py-6 (3rem) is outside the editor, so 4rem left ~16px dead. Comment corrected. - Trim two comments that duplicated nearby docs. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ --- web/src/components/layout/AppShell.tsx | 15 ++++++++------- web/src/editor/EditorRail.tsx | 5 ++++- web/src/pages/GardenEditorPage.tsx | 19 +++++++++++-------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/web/src/components/layout/AppShell.tsx b/web/src/components/layout/AppShell.tsx index a8c7ec2..c0cd059 100644 --- a/web/src/components/layout/AppShell.tsx +++ b/web/src/components/layout/AppShell.tsx @@ -43,11 +43,10 @@ export function AppShell() { // max-w-5xl reading measure the other pages use (#107). const canvasRoute = inEditor || inPublicGarden const showBottomNav = !!user && !canvasRoute - // On a phone the editor is a full-screen canvas — the same reason the bottom - // bar hides there. The global top bar (brand + account) is then pure chrome - // above the garden, so hide it too and let the editor's own strip carry a back - // affordance; desktop keeps the header. Only the editor, not the public view, - // which has no strip of its own to fall back on. + // On a phone the editor is a full-screen canvas, so the global top bar is pure + // chrome above the garden — hide it and let the editor's own strip carry the + // back link AND the account menu (so sign-out isn't lost). Editor only, not the + // public view, which has no strip of its own to fall back on. const hideHeaderOnMobile = inEditor const visibleSections = sections.filter((s) => !s.adminOnly || user?.isAdmin) @@ -126,8 +125,10 @@ export function AppShell() { } /** Account control: a compact button that toggles a small sign-out popover. On - * desktop the display name shows inline; on mobile it lives inside the popover. */ -function AccountMenu({ displayName }: { displayName: string }) { + * desktop the display name shows inline; on mobile it lives inside the popover. + * Exported so the editor's mobile strip can carry it — the global header that + * normally hosts it is hidden there (see hideHeaderOnMobile). */ +export function AccountMenu({ displayName }: { displayName: string }) { const logout = useLogout() const navigate = useNavigate() const [open, setOpen] = useState(false) diff --git a/web/src/editor/EditorRail.tsx b/web/src/editor/EditorRail.tsx index 4510e75..0f96fb1 100644 --- a/web/src/editor/EditorRail.tsx +++ b/web/src/editor/EditorRail.tsx @@ -62,7 +62,10 @@ export function EditorRail({ // reachable below. The canvas flexes to fill whatever's left. Desktop: a // fixed-width column beside the canvas (the cap doesn't apply there). 'flex min-h-0 shrink-0 flex-col rounded-t-xl border-t border-border bg-surface shadow-lg', - tall ? 'max-h-[78vh]' : 'max-h-[50vh]', + // dvh, not vh: the enclosing editor column is dvh-bounded, and on mobile + // Safari/Chrome vh is the *largest* viewport, so a vh cap could overrun the + // visible area and shove the mode bar off-screen (same #85 reasoning). + tall ? 'max-h-[78dvh]' : 'max-h-[50dvh]', 'md:static md:max-h-none md:w-80 md:rounded-xl md:border md:shadow-sm', )} > diff --git a/web/src/pages/GardenEditorPage.tsx b/web/src/pages/GardenEditorPage.tsx index c2d8443..2ff92eb 100644 --- a/web/src/pages/GardenEditorPage.tsx +++ b/web/src/pages/GardenEditorPage.tsx @@ -23,6 +23,7 @@ import { isCoarsePointer } from '@/editor/shared' import { cn } from '@/lib/cn' import type { EditorGarden } from '@/editor/types' import { ShareGardenModal } from '@/components/gardens/ShareGardenModal' +import { AccountMenu } from '@/components/layout/AppShell' import { useMe } from '@/lib/auth' import { toEditorObject, @@ -546,11 +547,11 @@ export function GardenEditorPage() { // the canvas bottom + Fit button under the browser chrome (#85). // // The subtracted band differs by breakpoint because the chrome does. On mobile - // the global top bar is hidden here (AppShell), so only
's padding is - // above us — 4rem reclaims the ~48px the header used to cost the canvas. On - // desktop the header is present, so keep the original 8rem. + // the global top bar is hidden here (AppShell), so the only thing outside the + // editor is
's py-6 — 3rem, top + bottom. On desktop the header is + // present, so keep the original 8rem. return ( -
+
{/* Desktop-only control column. On mobile these move to the bottom mode bar + a slim top strip so the canvas — the point of the screen — isn't shoved into a corner by a stack of controls (#99). */} @@ -627,6 +628,10 @@ export function GardenEditorPage() { Share )} + {/* The global header (and its account menu) is hidden on mobile in the + editor, so carry sign-out here — otherwise it's unreachable without + leaving the garden. */} + {me.data && }
{seasonYear !== null && setSeasonYear(null)} />} {/* Focus toolbar is desktop-only; on mobile its plant tools move to the @@ -687,10 +692,8 @@ export function GardenEditorPage() { tabs={railTabs} activeId={railTab} onActivate={setRailTab} - // The inspector is read alongside the canvas, so it stays a half-height - // peek. Journal/History/Assistant are the task themselves — reading and - // typing want room — so they take a taller slice on mobile (the canvas - // above just needs to stay glanceable as the assistant edits it). + // Panel modes want the room; the inspector stays a shorter peek (see the + // `tall` prop doc). tall={railTab !== 'inspector'} onClose={() => { // Only the inspector is *about* the selection, so only closing it