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 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-30 13:54:18 +07:00
parent 7ce5a0fa12
commit 93c665fdbf
2 changed files with 32 additions and 6 deletions

View File

@@ -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 => {
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 (

View File

@@ -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}
}