fix: sticky was disabled site-wide, and 100vh is wrong on iOS
`html, body { overflow-x: hidden }` makes both a scroll container, and a
`position: sticky` descendant then sticks to that rather than to the
viewport — which is to say it does not stick at all. Every sticky thing
in the app was affected: the session rail, the settings nav, the article
column, the study-plan rail. `overflow-x: clip` does the same job without
becoming a scroll container. Hidden stays as the fallback, so a browser
without `clip` still cannot be scrolled sideways and only loses
stickiness, which is the lesser fault.
Putting the rail away destroyed the layout. The collapsed grid was
`0 minmax(0, 1fr)` and the rail is `display: none`, so the content
became the *first* grid item and landed in the zero-width column —
wrapping one word per line beside an empty page. One column when there
is one thing in it.
100vh is the largest viewport on iOS — the one with the URL bar hidden —
so anything sized to it is taller than the screen really is and its
bottom sits behind the bar. Eleven files now use 100dvh, which tracks
the viewport as it changes.
The AI Mode composer was the one bottom-sticky bar with no safe-area
inset; its send button sat under the home indicator.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
parent
816ab0a66c
commit
b2ddee74dc
13 changed files with 49 additions and 15 deletions
|
|
@ -16,7 +16,10 @@
|
|||
margin-inline: calc(50% - 50vw);
|
||||
padding-inline: max(16px, calc(50vw - 660px));
|
||||
}
|
||||
.ax-layout.is-collapsed { grid-template-columns: 0 minmax(0, 1fr); gap: 0; }
|
||||
/* One column when the rail is away. It was `0 minmax(0, 1fr)`, and since the
|
||||
rail is `display: none` the content became the *first* grid item — landing
|
||||
in the zero-width column and wrapping one word per line. */
|
||||
.ax-layout.is-collapsed { grid-template-columns: minmax(0, 1fr); gap: 0; }
|
||||
|
||||
.ax-main { min-width: 0; padding: 8px 0 48px; }
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@
|
|||
cross-reference must not move the article you were reading. */
|
||||
.article-split.is-open > .article-split-main,
|
||||
.article-split.is-open > .article-split-pane {
|
||||
max-height: calc(100vh - 120px);
|
||||
max-height: calc(100dvh - 120px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* The rail is sticky inside its own pane now, not inside the window. */
|
||||
.article-page.is-split .article-sections { top: 6px; max-height: calc(100vh - 200px); }
|
||||
.article-page.is-split .article-sections { top: 6px; max-height: calc(100dvh - 200px); }
|
||||
.article-page.is-split .article-layout { grid-template-columns: 190px 1fr; gap: 14px; }
|
||||
.article-page.is-split .article-content { padding: 16px 18px; }
|
||||
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ export default function TeachChat({ question, attemptId, elevated = false }) {
|
|||
left: auto !important;
|
||||
top: 0 !important;
|
||||
width: 340px !important;
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important;
|
||||
border-radius: 0 !important;
|
||||
border-top: none !important;
|
||||
border-left: 2px solid var(--primary) !important;
|
||||
|
|
|
|||
|
|
@ -114,7 +114,18 @@ body {
|
|||
too wide then scrolls the whole page sideways — which is why the navbar
|
||||
could be found clipped mid-word at its left edge. Anything genuinely wider
|
||||
than the window scrolls inside its own box instead. */
|
||||
html, body { overflow-x: hidden; max-width: 100%; }
|
||||
html, body { max-width: 100%; }
|
||||
/* `overflow-x: hidden` on an ancestor silently disables `position: sticky` for
|
||||
every descendant — it makes the element a scroll container, and a sticky
|
||||
child sticks to that instead of to the viewport. That is why the session
|
||||
rail, the settings nav and the article column would not stay put. `clip`
|
||||
does the same job without becoming a scroll container. Hidden is kept as
|
||||
the fallback, so a browser without `clip` still cannot be scrolled
|
||||
sideways; it just loses stickiness, which is the lesser fault. */
|
||||
html, body { overflow-x: hidden; }
|
||||
@supports (overflow-x: clip) {
|
||||
html, body { overflow-x: clip; }
|
||||
}
|
||||
|
||||
/* ── Layout ─────────────────────────────────────────────────── */
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 0 28px; }
|
||||
|
|
@ -732,7 +743,7 @@ html, body { overflow-x: hidden; max-width: 100%; }
|
|||
border-radius: var(--card-radius);
|
||||
padding: 16px;
|
||||
box-shadow: var(--card-shadow);
|
||||
max-height: calc(100vh - 100px);
|
||||
max-height: calc(100dvh - 100px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
.ai-page { display: grid; grid-template-columns: 230px 1fr; gap: 18px; align-items: start; max-width: 1060px; margin: 0 auto; }
|
||||
|
||||
.ai-rail {
|
||||
position: sticky; top: 76px; max-height: calc(100vh - 100px); overflow-y: auto;
|
||||
position: sticky; top: 76px; max-height: calc(100dvh - 100px); overflow-y: auto;
|
||||
background: var(--card-bg); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 12px;
|
||||
}
|
||||
|
|
@ -72,7 +72,12 @@
|
|||
|
||||
.ai-error { color: var(--wrong-fg); font-size: 0.85rem; margin: 0; }
|
||||
|
||||
.ai-composer { display: flex; gap: 8px; align-items: flex-end; position: sticky; bottom: 0; padding-bottom: 12px; background: var(--bg); }
|
||||
.ai-composer {
|
||||
display: flex; gap: 8px; align-items: flex-end;
|
||||
position: sticky; bottom: 0; background: var(--bg);
|
||||
/* Clear of the home indicator, or the send button sits under it. */
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
}
|
||||
.ai-composer textarea {
|
||||
flex: 1; min-width: 0; resize: vertical; padding: 11px 14px;
|
||||
border: 1px solid var(--border); border-radius: 10px;
|
||||
|
|
|
|||
|
|
@ -138,6 +138,21 @@ describe('a session whose only attempt is still in progress', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('the session rail', () => {
|
||||
it('gives the content the whole width when the rail is put away', async () => {
|
||||
mountAttempt()
|
||||
await screen.findByRole('complementary', { name: 'Sessions' })
|
||||
const layout = document.querySelector('.ax-layout')
|
||||
expect(layout).not.toHaveClass('is-collapsed')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /Hide|Collapse/i }))
|
||||
// Hiding the rail used to leave the content in a zero-width grid column,
|
||||
// wrapping one word per line beside an empty page.
|
||||
expect(document.querySelector('.ax-layout')).toHaveClass('is-collapsed')
|
||||
expect(document.querySelector('.ax-main')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('a study session with every question answered', () => {
|
||||
it('becomes the review without waiting to be handed in', async () => {
|
||||
api.get.mockImplementation(url => {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
its whole length, scrolling on their own when there are more sections than
|
||||
screen. */
|
||||
.article-sections {
|
||||
position: sticky; top: 76px; max-height: calc(100vh - 96px); overflow-y: auto;
|
||||
position: sticky; top: 76px; max-height: calc(100dvh - 96px); overflow-y: auto;
|
||||
background: var(--card-bg); border: 1px solid var(--border);
|
||||
border-radius: var(--card-radius); padding: 14px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function ForgotPasswordPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f1f5f9' }}>
|
||||
<div style={{ minHeight: '100dvh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f1f5f9' }}>
|
||||
<div style={{ width: '100%', maxWidth: 400, padding: '0 16px' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 800, color: '#2563eb' }}>🏥 PedsHub</div>
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ export default function LandingPage() {
|
|||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: 'var(--bg)' }}>
|
||||
<div style={{ minHeight: '100dvh', background: 'var(--bg)' }}>
|
||||
|
||||
{/* Auth modal overlay */}
|
||||
{authModal && (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
/* Session rail beside the question, like a Qbank session's left column.
|
||||
Below 1150px there is not room for both, so the topbar dropdown takes over. */
|
||||
.quiz-player .quiz-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 28px; align-items: start; }
|
||||
.quiz-player .quiz-sidebar { display: block; order: -1; position: sticky; top: 16px; max-height: calc(100vh - 32px); overflow-y: auto; }
|
||||
.quiz-player .quiz-sidebar { display: block; order: -1; position: sticky; top: 16px; max-height: calc(100dvh - 32px); overflow-y: auto; }
|
||||
.quiz-rail-head { font-size: .68rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: #8b929c; padding: 0 0 8px; }
|
||||
.quiz-rail-list { display: flex; flex-direction: column; }
|
||||
.quiz-rail-item {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function ResetPasswordPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f1f5f9' }}>
|
||||
<div style={{ minHeight: '100dvh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f1f5f9' }}>
|
||||
<div style={{ width: '100%', maxWidth: 400, padding: '0 16px' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 800, color: '#2563eb' }}>🏥 PedsHub</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
}
|
||||
|
||||
/* ── Blocks rail ─────────────────────────────────────────────────── */
|
||||
.spb-rail { position: sticky; top: 76px; align-self: start; max-height: calc(100vh - 100px); overflow-y: auto; }
|
||||
.spb-rail { position: sticky; top: 76px; align-self: start; max-height: calc(100dvh - 100px); overflow-y: auto; }
|
||||
.spb-rail-plan {
|
||||
display: block; padding: 12px 14px; font-weight: 700; font-size: .95rem;
|
||||
color: var(--text); text-decoration: none; border-bottom: 1px solid var(--border);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function VerifyEmailPage() {
|
|||
}, [token])
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f1f5f9' }}>
|
||||
<div style={{ minHeight: '100dvh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f1f5f9' }}>
|
||||
<div className="card" style={{ maxWidth: 420, width: '100%', textAlign: 'center' }}>
|
||||
{status === 'verifying' && (
|
||||
<><div className="spinner" style={{ margin: '0 auto 16px' }}></div><p>Verifying your email...</p></>
|
||||
|
|
|
|||
Loading…
Reference in a new issue