From 93c665fdbf17cd0859c6fe6fa3adcea69a60be16 Mon Sep 17 00:00:00 2001 From: Iqbal Date: Fri, 30 Oct 2026 13:54:18 +0700 Subject: [PATCH] Fit the layout between phone and desktop widths From about 920px down to the phone layout (700px), the sidebar kept its full width and the topbar's credit balance, Learning Lab link and language switch ran past the right edge. Between 701 and 1024px the sidebar now starts as the icon rail (the toggle opens it for the visit), and up to 1100px the topbar and the page head's buttons wrap onto another line, without the engine label. Co-Authored-By: Claude Opus 5.5 --- src/components/AppLayout.tsx | 28 ++++++++++++++++++++++------ src/styles/theme.css | 10 ++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index f3b4f56..63e02a6 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useRef, useState } from 'react' +import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react' import { useClerk, useOrganization, useOrganizationList, useUser } from '@clerk/react' import { useQuery } from '@tanstack/react-query' import { NavLink, Outlet, useLocation } from 'react-router-dom' @@ -130,6 +130,16 @@ function OrgSwitcher() { } // Remembered per browser; storage can throw in private windows, so it falls back to expanded. +// Between the phone layout and a full desktop, the sidebar would leave too little room for the +// page and the topbar, so it starts as the icon rail there; the toggle still opens it for this visit. +const NARROW = '(min-width: 701px) and (max-width: 1024px)' +function useNarrow() { + return useSyncExternalStore( + notify => { const m = matchMedia(NARROW); m.addEventListener('change', notify); return () => m.removeEventListener('change', notify) }, + () => matchMedia(NARROW).matches, + ) +} + const COLLAPSED_KEY = 'sidebar-collapsed' function readCollapsed() { try { return localStorage.getItem(COLLAPSED_KEY) === '1' } catch { return false } @@ -149,11 +159,17 @@ export function AppLayout() { // In braces: Chrome's scrollTo now returns a Promise, which React would keep as the effect's // cleanup and call on the next route change ("is not a function"), unmounting the whole app. useLayoutEffect(() => { window.scrollTo(0, 0) }, [pathname]) - const [collapsed, setCollapsed] = useState(readCollapsed) - const toggleCollapsed = () => setCollapsed(c => { - try { localStorage.setItem(COLLAPSED_KEY, c ? '0' : '1') } catch { /* keep it for this visit only */ } - return !c - }) + const [saved, setSaved] = useState(readCollapsed) + const narrow = useNarrow() + const [openNarrow, setOpenNarrow] = useState(false) + const collapsed = narrow ? !openNarrow : saved + const toggleCollapsed = () => { + if (narrow) return setOpenNarrow(o => !o) + setSaved(c => { + try { localStorage.setItem(COLLAPSED_KEY, c ? '0' : '1') } catch { /* keep it for this visit only */ } + return !c + }) + } const name = user?.fullName || user?.primaryEmailAddress?.emailAddress || '-' return ( diff --git a/src/styles/theme.css b/src/styles/theme.css index 5c46507..f40f6d8 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -428,3 +428,13 @@ dialog::backdrop{background:rgba(16,24,40,.45);backdrop-filter:blur(4px)} } @media(max-width:700px){.sidebar-toggle,.sidebar-help-icon{display:none}} @media(prefers-reduced-motion:reduce){.sidebar,.workspace-main{transition:none}} + +/* Tablet widths: the topbar wraps instead of pushing its pills past the edge, and the page head's + buttons go under the title when they no longer fit beside it */ +@media(min-width:701px) and (max-width:1100px){ + .topbar{height:auto;min-height:64px;padding-top:10px;padding-bottom:10px;flex-wrap:wrap;row-gap:8px} + .topbar-right{flex-wrap:wrap;justify-content:flex-end;gap:8px 10px} + .demo-indicator{display:none} + .page-head{flex-wrap:wrap} + .page-head .head-actions{flex-wrap:wrap} +}