fix: one menu, at the left, holding whatever the page has

The previous attempt split it in two — a session button on the left, the site
menu on the right — which is not what was asked for and is worse: a second menu
an inch from the first is a choice nobody wants to make.

One button. It sits at the left-hand end of the bar on a phone, where it always
fits, and what is behind it changes with where you are: the site's sections
always, plus the questions in this session, or the contents of the article you
are reading. A control that changes what it holds must at least not change
where it is.

Reading registers its contents with the same mechanism a session uses for its
questions, so the drawer grows the tab and nothing else has to know.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
Daniel 2026-09-12 20:58:38 +02:00
parent 9658ddb207
commit 658482c53a
3 changed files with 49 additions and 46 deletions

View file

@ -1,4 +1,6 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import useMediaQuery from '../hooks/useMediaQuery'
import { useSessionDrawer } from '../context/SessionDrawer'
import RichText from './RichText'
/**
* Article prose goes through the same renderer as everything else.
@ -111,6 +113,16 @@ export default function ArticleReader({
// you consult, and a wall of prose hides the one heading you came for.
const [openIds, setOpenIds] = useState({})
const [drawerOpen, setDrawerOpen] = useState(false)
// On a phone the contents live behind the same button as the site menu, the
// way a session's questions do: one control at the top left whose contents
// change with where you are, rather than a second menu to hunt for. The rail
// is on screen above 1150px, so there is nothing to open there.
const narrow = useMediaQuery('(max-width: 1150px)')
const { register: registerDrawer } = useSessionDrawer()
useEffect(() => {
if (!narrow || bare) return undefined
return registerDrawer(() => setDrawerOpen(true))
}, [narrow, bare, registerDrawer])
// Collapsing the contents rail hands its width to the prose.
const [railOpen, setRailOpen] = useState(() => stored(RAIL_KEY, 'open') !== 'closed')
const [size, setSize] = useState(() => (SIZES.includes(stored(SIZE_KEY, 'm')) ? stored(SIZE_KEY, 'm') : 'm'))

View file

@ -281,17 +281,37 @@ export default function Navbar({ onSignIn, onRegister, onSearch }) {
for stays put; where you are going gets out of the way while you read. */}
<div className="navbar-primary">
<div className="container navbar-inner">
{/* While a session is open on a phone, the questions get their own
control on the left and the site menu keeps the right. One burger
that changed meaning depending on where you were meant the two
menus took turns, and the one you wanted was the other one. */}
{sessionDrawer.opener && (
<button type="button" className="nav-session-burger"
aria-label="Questions in this session" onClick={sessionDrawer.opener}>
<span aria-hidden="true"></span>
</button>
)}
<Link to="/" className="logo" onClick={() => setMenuOpen(false)}
{/* One menu, at the left-hand end on a phone. What is inside it
changes with where you are the site's sections always, plus the
questions in this session, or the contents of what you are
reading but it is always the same button in the same place. Two
buttons was the previous attempt and it was wrong: a second menu
an inch from the first is a choice nobody wants to make.
It opens the page's own drawer where there is one, because the
site menu is a tab inside that; otherwise the site menu itself. */}
<button
className="nav-burger"
onClick={() => (sessionDrawer.opener ? sessionDrawer.opener() : setMenuOpen(v => !v))}
aria-label={sessionDrawer.opener ? 'Menu and contents' : 'Menu'}
aria-expanded={sessionDrawer.opener ? undefined : menuOpen}
>
<span style={{
display: 'block', width: 22, height: 2, background: 'currentColor',
borderRadius: 2, transition: 'transform 0.2s, opacity 0.2s',
transform: menuOpen ? 'translateY(7px) rotate(45deg)' : 'none',
}} />
<span style={{
display: 'block', width: 22, height: 2, background: 'currentColor',
borderRadius: 2, opacity: menuOpen ? 0 : 1, transition: 'opacity 0.2s',
}} />
<span style={{
display: 'block', width: 22, height: 2, background: 'currentColor',
borderRadius: 2, transition: 'transform 0.2s, opacity 0.2s',
transform: menuOpen ? 'translateY(-7px) rotate(-45deg)' : 'none',
}} />
</button>
<Link to="/" className="logo" onClick={() => setMenuOpen(false)}
onMouseEnter={() => setPeek(true)} onFocus={() => setPeek(true)}>🏥 PedsHub</Link>
{user && <GlobalSearch onOpenOverlay={onSearch} />}
@ -300,31 +320,7 @@ export default function Navbar({ onSignIn, onRegister, onSearch }) {
<FeedbackBadge />
<JobsBadge jobs={jobs} />
<AccountMenu user={user} onLogout={logout} />
{/* Always the site menu. It used to become the session's list of
questions while a session was open, which put both menus on
the same button and neither where the thumb expected it; the
session now has its own control at the other end of the bar. */}
<button
className="nav-burger"
onClick={() => setMenuOpen(v => !v)}
aria-label="Menu"
aria-expanded={menuOpen}
>
<span style={{
display: 'block', width: 22, height: 2, background: 'currentColor',
borderRadius: 2, transition: 'transform 0.2s, opacity 0.2s',
transform: menuOpen ? 'translateY(7px) rotate(45deg)' : 'none',
}} />
<span style={{
display: 'block', width: 22, height: 2, background: 'currentColor',
borderRadius: 2, opacity: menuOpen ? 0 : 1, transition: 'opacity 0.2s',
}} />
<span style={{
display: 'block', width: 22, height: 2, background: 'currentColor',
borderRadius: 2, transition: 'transform 0.2s, opacity 0.2s',
transform: menuOpen ? 'translateY(-7px) rotate(-45deg)' : 'none',
}} />
</button>
</div>
) : (
/* Logged-out: Sign In + Register */

View file

@ -218,15 +218,6 @@ html, body { overflow-x: hidden; }
@media (prefers-reduced-motion: reduce) { .navbar-sections { transition: none; } }
[data-theme="markdown"] .navbar .logo { color: #d4a96a; }
/* The session's own control, at the left-hand end of the bar. Off the desktop
layout entirely: there the list of questions is a rail beside the question,
and a button for it would open a drawer over something already on screen. */
.nav-session-burger {
display: none; margin-right: 2px; padding: 6px 8px; cursor: pointer;
border: 1px solid rgba(255, 255, 255, .22); border-radius: 8px;
background: none; color: var(--navbar-fg); font-size: 1rem; line-height: 1;
}
.nav-session-burger:hover { border-color: rgba(255, 255, 255, .45); }
.nav-burger { display: none; background: none !important; border: 0 !important; cursor: pointer; color: var(--navbar-fg); padding: 6px; flex-direction: column; gap: 5px; opacity: 0.85; }
/* Scoped to the dark bar. These were `.navbar a` and `.navbar button`, which
also caught everything in the light section bar beneath it near-white text
@ -810,8 +801,12 @@ html, body { overflow-x: hidden; }
/* The burger carries the sections on a phone; the strip would be a second
copy of the same links competing for the same thumb. */
.nav-burger { display: flex; }
.nav-session-burger { display: inline-flex; align-items: center; }
.nav-logout { display: none; }
/* At the left-hand end, before the logo it is the first child of the bar
now, so nothing here has to reorder it. A control that changes what it
holds must at least not change where it is: reaching for the top right and
finding the account chip is how a menu gets a reputation. */
.navbar-inner > .nav-burger { margin-right: 2px; }
/* Hide the whole strip, wrapper and arrows, not just the link row. */
.navbar-sections .ss-wrap { display: none; }
.navbar-sections, .navbar-sections-inner, .navbar-sections:focus-within { height: 42px; }