From 379f9c69a766d7974f04ac7e4568015b6f56fbdd Mon Sep 17 00:00:00 2001 From: Manish Kumar Date: Tue, 11 Aug 2026 22:51:42 -0500 Subject: [PATCH 01/10] =?UTF-8?q?feat(mobile):=20responsive=20app=20shell?= =?UTF-8?q?=20=E2=80=94=20collapse=20sidebar=20rail=20below=20768px?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additive only: desktop layout (>=768px) is byte-for-byte unchanged, since both new rules are scoped inside max-width:767px media queries. Below the breakpoint the existing inline-styled sidebar wrapper (.studio-sidebar-wrap) is force-collapsed to width:0, and a hidden-by-default hamburger trigger class (.studio-hamburger) is introduced for the next commit's MobileNav drawer to hook into. Part of the v0.8.0 mobile-responsive UI effort. Co-Authored-By: Claude Sonnet 5 --- src/index.css | 34 ++++++++++++++++++++++++++++++++++ src/ui/App.jsx | 6 ++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/index.css b/src/index.css index 7fa0ac36..034c3f75 100644 --- a/src/index.css +++ b/src/index.css @@ -160,3 +160,37 @@ a:focus:not(:focus-visible) { } .toggle.on::after { left: 16px; } .toggle.off::after { left: 2px; } + +/* ── App shell: mobile nav (additive — desktop unaffected above 767px) ── */ +/* Desktop's wrapper already sets width inline (200 or 0 when a + service is open); below the breakpoint MobileNav (drawer) takes over as + the only nav surface, so the inline sidebar rail is force-collapsed. */ +@media (max-width: 767px) { + .studio-sidebar-wrap { + width: 0 !important; + } +} + +/* Hamburger trigger — hidden by default (desktop), shown only under the + breakpoint. Mirrors Tailwind's `md:hidden` without pulling Tailwind into + a file that otherwise uses none of it. */ +.studio-hamburger { + display: none; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + min-width: 44px; + min-height: 44px; + border-radius: var(--radius-sm); + background: var(--bg3); + border: 1px solid var(--border2); + color: var(--text); + cursor: pointer; + flex-shrink: 0; + font-size: 16px; + padding: 0; +} +@media (max-width: 767px) { + .studio-hamburger { display: inline-flex; } +} diff --git a/src/ui/App.jsx b/src/ui/App.jsx index 7bd5a0f3..4fa6f4ed 100644 --- a/src/ui/App.jsx +++ b/src/ui/App.jsx @@ -255,8 +255,10 @@ export default function App() { background:"var(--bg)", color:"var(--text)", fontFamily:"var(--font)", overflow:"hidden", }}> - {/* Sidebar — auto-hides when inside a service/app view */} -
Date: Tue, 11 Aug 2026 22:53:12 -0500 Subject: [PATCH 02/10] feat(mobile): add MobileNav slide-out drawer New MobileNav.jsx reuses App.jsx's existing buildNav()/step/setStep/ currentUser data verbatim (same props Sidebar already takes) -- no nav items or permission logic duplicated. Slide-out drawer + backdrop, closes on outside click, Escape, or route change (step change), close button is always visible and >=44px, nav rows are >=44px touch targets. Wired into App.jsx behind the hamburger trigger added in the previous commit; the drawer is position:fixed so it never participates in the desktop flex layout, and the hamburger that opens it stays display:none above 767px, so desktop is unaffected. Co-Authored-By: Claude Sonnet 5 --- src/ui/App.jsx | 18 ++++ src/ui/components/MobileNav.jsx | 173 ++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 src/ui/components/MobileNav.jsx diff --git a/src/ui/App.jsx b/src/ui/App.jsx index 4fa6f4ed..7552af70 100644 --- a/src/ui/App.jsx +++ b/src/ui/App.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import LicenseGate from "./components/LicenseGate"; import BugReport from "./components/BugReport"; import Sidebar from "./components/Sidebar"; +import MobileNav from "./components/MobileNav"; import UpdateBanner from "./components/UpdateBanner"; import Mode from "./pages/Mode"; import LLM from "./pages/LLM"; @@ -93,6 +94,7 @@ export default function App() { const [authChecked, setAuthChecked] = useState(false); // has the initial session check resolved? (web only) const [oauthNotice, setOauthNotice] = useState(null); // result of an OAuth redirect: link_required | error const [returnTo, setReturnTo] = useState(() => getSafeReturnTo()); // ?return_to= target, captured once + const [mobileNavOpen, setMobileNavOpen] = useState(false); // MobileNav drawer — mobile only, no desktop effect // ─── Load saved config + first-run detection ─────────── useEffect(() => { @@ -284,6 +286,14 @@ export default function App() { display:"flex", alignItems:"center", flexWrap:"wrap", padding:"8px 20px", gap:12, flexShrink:0, }}> + {/* Mobile-only nav trigger — hidden on desktop via .studio-hamburger (index.css) */} + +
)}
+ + {/* MobileNav drawer — position:fixed, so it's out of the flex flow + entirely; renders identically on desktop (just permanently closed + since the hamburger that opens it is display:none there). */} + setMobileNavOpen(false)} + />
)} diff --git a/src/ui/components/MobileNav.jsx b/src/ui/components/MobileNav.jsx new file mode 100644 index 00000000..17d517aa --- /dev/null +++ b/src/ui/components/MobileNav.jsx @@ -0,0 +1,173 @@ +import React, { useEffect, useRef } from "react"; +import { logout } from "../lib/session"; + +// Mobile drawer nav. Deliberately takes the exact same `nav`/`step`/`setStep` +// data App.jsx already builds via buildNav() and passes to the desktop +// — permission filtering (manage_roles) and active-route logic +// stay in perfect sync with desktop because nothing nav-related is +// duplicated here, only re-rendered as a drawer instead of a fixed rail. +export default function MobileNav({ nav, step, setStep, currentUser, open, onClose }) { + const closeBtnRef = useRef(null); + const stepRef = useRef(step); + + // Close on Escape + useEffect(() => { + if (!open) return; + const handler = (e) => { if (e.key === "Escape") onClose(); }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [open, onClose]); + + // Close whenever the active page changes — covers Workbench tiles' own + // "navigate" custom-event dispatch (App.jsx's window "navigate" listener), + // not just taps on a link inside this drawer. + useEffect(() => { + if (open && stepRef.current !== step) onClose(); + stepRef.current = step; + }, [step]); // eslint-disable-line react-hooks/exhaustive-deps + + // Lock body scroll behind the open drawer + useEffect(() => { + if (!open) return; + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { document.body.style.overflow = prev; }; + }, [open]); + + // Focus the close button when opened (basic focus management) + useEffect(() => { + if (open) closeBtnRef.current?.focus(); + }, [open]); + + const go = (idx) => { setStep(idx); onClose(); }; + + return ( + <> + {/* Backdrop — click outside to close */} +