Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions next/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ a.tag:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2
.pagination a:hover { border-color: var(--color-primary); text-decoration: none; }

/* ---------- article / prose ---------- */
.article-wrap { display: grid; grid-template-columns: minmax(0, 720px); justify-content: center; padding: 2.5rem 0; }
.article-wrap { display: grid; grid-template-columns: minmax(0, 720px); justify-content: center; padding-block: 2.5rem; }

/* ---------- article side rails (wide screens) ---------- */
/* Left: tags + back. Right: on-this-page TOC. Both sticky, both hidden below
Expand Down Expand Up @@ -321,7 +321,7 @@ a.tag:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2
.admonition-important { border-color: #a25ddc; background: #f4eefb; }

/* ---------- docs layout ---------- */
.docs-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 2.5rem; align-items: start; padding: 2rem 0; }
.docs-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 2.5rem; align-items: start; padding-block: 2rem; }
.docs-sidebar {
position: sticky; top: calc(var(--header-height) + 1rem);
max-height: calc(100vh - var(--header-height) - 2rem);
Expand All @@ -338,11 +338,13 @@ a.tag:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2
.docs-content { min-width: 0; padding-bottom: 3rem; }
.docs-meta { border-top: 1px solid var(--color-border); margin-top: 2.5rem; padding-top: 1rem; font-size: .85rem; color: var(--color-text-soft); display: flex; gap: 1rem; flex-wrap: wrap; }
@media (max-width: 960px) {
/* Content first on phones: the full link tree is ~7 screens tall, so it
moves below the article (grid order) and scrolls within a capped box.
minmax(0,1fr) keeps long code lines from inflating the column. */
/* The nav stays in document order, above the article. It used to be pushed
below it, which put the link tree — and the version picker inside it —
roughly seven screens down, with only ~4% of the tree visible once you got
there. Capping the box at 30vh keeps the article's own heading on the
first screen. minmax(0,1fr) keeps long code lines from inflating the column. */
.docs-layout { grid-template-columns: minmax(0, 1fr); }
.docs-sidebar { order: 2; position: static; max-height: 45vh; max-height: 45dvh; overflow-y: auto; border: 1px solid var(--color-border); border-radius: 8px; padding: .75rem; }
.docs-sidebar { position: static; max-height: 30vh; max-height: 30dvh; overflow-y: auto; border: 1px solid var(--color-border); border-radius: 8px; padding: .75rem; }
}

/* ---------- homepage extras ---------- */
Expand Down
115 changes: 115 additions & 0 deletions next/tests/e2e/docs-mobile-layout.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { expect, test } from '@playwright/test';

/** Computed horizontal padding of the first element matching `selector`. */
async function inlinePadding(page, selector) {
return page.locator(selector).first().evaluate((el) => {
const cs = getComputedStyle(el);
return { left: parseFloat(cs.paddingLeft), right: parseFloat(cs.paddingRight) };
});
}

/**
* Discover a post from the blog index rather than naming one. A hardcoded URL
* turns an unrelated content change into a broken test; the index is the same
* thing a reader would follow. `/blog/20…` matches dated post URLs only —
* `/blog/page/`, `/blog/archive/` and `/blog/tags/` do not start that way.
*/
async function firstBlogPost(page) {
await page.goto('/blog/');
const href = await page.locator('a[href^="/blog/20"]').first().getAttribute('href');
expect(href, 'the blog index must list at least one post').toBeTruthy();
return href;
}

/** Padding restored and nav ahead of the article, for any docs page. */
async function assertDocsLayout(page, url) {
await page.goto(url);

const pad = await inlinePadding(page, '.docs-layout');
expect(pad.left, `${url}: .docs-layout must not zero out .container padding`).toBeGreaterThan(0);
expect(pad.right).toBeGreaterThan(0);

// The header was always correct, so it is the reference the article should
// match — but only once the layout stacks; see the breakpoint note below.
const geom = await page.evaluate(() => ({
viewport: window.innerWidth,
h1Left: document.querySelector('.docs-content h1').getBoundingClientRect().left,
brandLeft: document.querySelector('.site-header .brand').getBoundingClientRect().left,
navTop: document.querySelector('.docs-sidebar').offsetTop,
articleTop: document.querySelector('.docs-content').offsetTop,
}));

expect(geom.h1Left, `${url}: article text must not touch the viewport edge`).toBeGreaterThan(0);

// Both remaining checks are breakpoint-dependent, and 960px is the line
// where .docs-layout collapses to one column (the max-width: 960px media
// query in global.css).
if (geom.viewport <= 960) {
// Stacked: the article shares the container's inline padding with the
// header, so their left edges line up.
expect(Math.abs(geom.h1Left - geom.brandLeft),
`${url}: article should line up with the header brand`).toBeLessThanOrEqual(1);
// Stacked: the nav must precede the article. This is the defect — `order: 2`
// used to push it below.
expect(geom.navTop, `${url}: the docs nav must come before the article`)
.toBeLessThan(geom.articleTop);
} else {
// Side by side: nav and article are grid items on the same row, so their
// offsetTop is EQUAL. Measured on production at 1440px: both 132.
// Asserting `toBeLessThan` here would be unsatisfiable — and asserting
// equality is the guard that catches `order` leaking out of the media
// query and stacking the desktop layout.
expect(geom.navTop, `${url}: nav and article should share a grid row`)
.toBe(geom.articleTop);
}
}

// docs/general/** ships from this repo, so it exists in the PR CI build too —
// no gate, and the fix is verified before anything is deployed.
test('general docs keep padding and put the nav above the article', async ({ page }) => {
await assertDocsLayout(page, '/docs/general/contributor-guide/');
});

// Same assertions over the 200-link apisix tree that motivated the report.
// Gated: apisix docs need .sync/ checkouts only the deploy pipeline has.
test('apisix docs keep padding and put the nav above the article', async ({ page }) => {
test.skip(
process.env.EXPECT_DOCUSARUS_ROUTES !== 'true',
'apisix docs only exist in the final overlaid tree',
);
await assertDocsLayout(page, '/docs/apisix/getting-started/README/');
});

test('blog posts keep their horizontal padding', async ({ page }) => {
await page.goto(await firstBlogPost(page));
const pad = await inlinePadding(page, '.article-wrap');
expect(pad.left, 'blog posts share the .article-wrap defect').toBeGreaterThan(0);
expect(pad.right).toBeGreaterThan(0);
});

test('desktop keeps the three-column article rails', async ({ page }) => {
test.skip(test.info().project.name !== 'desktop-chrome', 'Rails only exist at >=1240px');

await page.goto(await firstBlogPost(page));

// Assert the rails exist before measuring them, so a post that legitimately
// has none fails loudly here instead of silently passing a vacuous check.
const rails = page.locator('.article-wrap.with-rails');
await expect(rails, 'the discovered post should render the rails layout').toHaveCount(1);

const tracks = await rails.evaluate((el) =>
getComputedStyle(el).gridTemplateColumns.split(/\s+/).filter(Boolean).map(parseFloat));

expect(tracks.length, 'the rails grid must stay three columns').toBe(3);

// The count alone is VACUOUS and must not be the only assertion here.
// `.with-rails` uses an explicit template (190px minmax(0,760px) 230px), so
// computed gridTemplateColumns always reports three tracks no matter how
// narrow the container gets. Measured on production: forcing the wrapper to
// 600px still reports 3 tracks — as "190px 44px 230px", with the reading
// column crushed. Restoring the inline padding shrinks the middle track, it
// never removes one, so track WIDTH is the only thing worth guarding.
// Design target is 760px; the rule's own comment allows ~680px at 1240.
expect(tracks[1], 'the reading column must not be squeezed by the padding fix')
.toBeGreaterThan(700);
});
Loading