Skip to content

feat: add a mobile layout to the paper theme - #178

Merged
rsbh merged 1 commit into
feat/sidebar-footer-linksfrom
feat/paper-mobile-layout
Aug 31, 2026
Merged

feat: add a mobile layout to the paper theme#178
rsbh merged 1 commit into
feat/sidebar-footer-linksfrom
feat/paper-mobile-layout

Conversation

@rsbh

@rsbh rsbh commented Aug 31, 2026

Copy link
Copy Markdown
Member

Stacked on #176 — review that one first; this PR's diff shows only the mobile layout.

Problem

The paper theme had no responsive layout whatsoever. grep -c "@media" returned 0 across all four of its CSS modules — the small-screen branch was simply never written.

At 390px the theme rendered its full desktop layout:

  • .sidebar is an unconditional width: var(--paper-sidebar-width) (262px), so it consumed two thirds of the viewport.
  • .content carries margin-left: calc(-1 * var(--paper-sidebar-width)) to slide under that rail. With the rail still present the article was dragged underneath it and clipped on the left — headings rendered as …e (3.0), …st, …ed.
  • There was no mobile header, no hamburger, and no way to reach the navigation.

This was not horizontal overflow: document.documentElement.scrollWidth === window.innerWidth === 390. The layout just consumed the viewport.

Approach

Followed the default theme as the reference implementation, at the same max-width: 768px breakpoint:

  1. Mobile header — sticky, 48px, carrying SidebarHeader, ClientThemeSwitcher (the existing shared component) and a hamburger.
  2. Full-screen menu — chapter nav → divider → <SidebarLinks variant='list' /> → version switcher.
  3. Desktop sidebar hidden, .content's negative margin-left neutralised so the article is full-width and unclipped.
  4. Closes on navigation via a useEffect on pathname.

Desktop is untouched: every new rule lives either inside the media query or on a new class that is display: none until the query turns it on.

Where SidebarHeader went, and why

In the header, not the menu.

SidebarHeader is paper's brand slot: it renders the site title as plain text, and only becomes a content-dir Select when a site declares more than one content directory. On desktop it occupies the sidebar's header position — the top-left of the page.

Moving it into the menu would have left the mobile header with no identity at all, which is a worse trade than a slightly busier bar; it would also have nested a Select popover inside a full-screen overlay, which is awkward to operate. Keeping it in the header preserves the desktop mental model — same control, same top-left position — and it stays compact enough to sit alongside two icon buttons at 390px.

This does differ from the default theme, which puts its content-dir entries in the menu as nav items. That theme has a separate SidebarLogo to carry identity in the header; paper has no such component, so the two roles are conflated in one control and it has to live where identity belongs.

Composing with reader mode

Paper wraps its layout in ReaderModeProvider, and readerMode already hides the sidebar. The mobile header and menu render under the same showSidebar condition, so reader mode unmounts them along with the sidebar rather than leaving a header floating over a distraction-free page. Verified: in reader mode at 390px the header and menu are both absent and the content is the full 390px.

The reading-progress rail

ReadingProgress is position: fixed, width: 200px, pinned to the right edge — over half the viewport at 390px, sitting on top of the article. It is hidden below the breakpoint rather than shrunk; the page navbar already carries prev/next.

Two defects that surfaced once content went full-width

Both are fixed inside the media query, so desktop is unaffected:

  • A long breadcrumb stretched the page past the viewport. A flex item defaults to min-width: auto, so Developer Guide > Developer Guide (1.0) refused to shrink and forced the layout column to 397px inside a 390px viewport. Fixed with min-width: 0 on the layout column and the navbar's left cluster, plus clipping on the breadcrumb.
  • A long site title wrapped out of the fixed-height header. Now ellipsizes on one line.

Verification

Measured with headless Chrome over the DevTools Protocol (Emulation.setDeviceMetricsOverride), which gives a genuine 390px viewport. The harness was validated first against a 50/50 split probe.

Check Result
bun run build:cli exit 0
bun test 314 pass, 0 fail
biome lint src/themes/paper/ 1 warning — unchanged from baseline
tsc on themes/paper 2 errors, both pre-existing (<Text size={2} weight={500}>)

At 390x844, after:

asideDisplay: "none",  asideWidth: 0
contentMarginLeft: "0px",  contentWidth: 390,  contentLeft: 0
h1Left: 12,  h1Right: 378          // unclipped, inside the viewport
burgerVisible: true,  burgerInViewport: true
innerWidth: 390,  scrollWidth: 390 // no overflow

The rail and the closed menu both compute to display: none, and nothing paints over the article's right edge.

Breakpoint boundary: at 768px the sidebar is none and the hamburger shows; at 769px the sidebar is flex with margin-left: -262px and the hamburger is hidden.

Menu contents, read out of the DOM in order — chapter nav, then a footer with a 1px top border, then the links, then the switcher:

A "Docs home (3.0)"  inFooter: false
A "Guide"            inFooter: false
A "Go to app"        inFooter: true
A "Support"          inFooter: true
BUTTON "3.0"         inFooter: true

Menu closes on navigation: opening sets data-open="true" / display: block / aria-expanded="true"; clicking Guide moves /docs/docs/guide and returns the menu to data-open="false" / display: none.

Links still attach the ref param — with window.open stubbed, clicking Go to app captured:

["https://example.com/?ref=http%3A%2F%2Flocalhost%3A3100%2Fdocs", "_blank", "noopener"]

Desktop unchanged: screenshots at 1440x900 before and after are byte-identical (sha256 430b2d40…), re-confirmed after every subsequent change.

A note on specificity

The brief flagged that the default theme had .sidebar { display: none } losing to a .layout aside.sidebar base rule. Paper has no such compound selector — .sidebar is a plain single-class rule — so the media query wins on source order. Confirmed at runtime rather than assumed: the aside computes to display: none with a measured width of 0.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chronicle Ready Ready Preview Aug 31, 2026 6:46am

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a1e1201-d9ed-4076-9e39-3436e951ffd3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The paper theme had no small-screen branch at all — none of its four CSS
modules contained a single media query. At 390px it rendered the full desktop
layout: the 262px sidebar ate two thirds of the viewport, and `.content`'s
`margin-left: calc(-1 * var(--paper-sidebar-width))` dragged the article
underneath it, clipping headings to "e (3.0)", "st" and "ed". There was no
header, no hamburger and no way to reach the navigation.

Add a mobile layout mirroring the default theme's, at the same 768px
breakpoint:

- a sticky mobile header carrying the existing SidebarHeader, the theme
  switcher and a hamburger toggle
- a full-screen menu holding the chapter nav, a divider, the configured links
  as inline items (SidebarLinks variant='list') and the version switcher
- the desktop sidebar hidden and `.content`'s negative offset neutralised, so
  the article is full-width and unclipped
- the menu closes on navigation

The reading-progress rail is a fixed 200px column pinned to the right edge, so
it is dropped below the breakpoint rather than left overlapping the article,
and the page navbar parks below the header instead of colliding with it.

Two narrow-viewport defects surfaced once the content became full-width: a long
breadcrumb could not shrink past its intrinsic width and stretched the page 7px
wider than the viewport, and a long site title wrapped out of the fixed-height
header. Both are corrected inside the media query.

The mobile chrome only renders when the sidebar does, so it composes with
reader mode rather than fighting it. Desktop rendering is unchanged —
screenshots at 1440x900 before and after are byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rsbh
rsbh force-pushed the feat/paper-mobile-layout branch from f6a3d04 to afe7ea9 Compare August 31, 2026 06:46
@rsbh
rsbh merged commit 4f8497a into feat/sidebar-footer-links Aug 31, 2026
9 checks passed
@rsbh
rsbh deleted the feat/paper-mobile-layout branch August 31, 2026 06:58
rsbh added a commit that referenced this pull request Aug 31, 2026
* feat: add configurable links to the sidebar footer

Adds a top-level `links:` key to chronicle.yaml, surfaced from the sidebar
footer in both the default and paper themes.

On desktop the links sit behind a `?` icon button that opens a menu upward,
matching the design. In the default theme's mobile hamburger menu they render
inline as nav items instead, above the version switcher — no dropdown.

Each destination is tagged with a `ref` query param carrying the full URL of
the page the link was clicked from. External links open with `noopener` (not
`noreferrer`) so the destination also receives a Referer header; under the
default referrer policy that header carries only the origin, so `ref` is what
identifies the specific page. Existing query strings on the href are
preserved, and non-web schemes such as mailto: are left untouched.

The sidebar footer previously rendered only when versions were configured; it
now renders when either versions or links are present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: show latest.label as a static footer label when unversioned

`latest` is valid without `versions` — the schema only requires it in the
other direction — but both VersionSwitchers returned null whenever no
versions were configured, so a configured `latest.label` was silently
dropped.

It now renders as static text in the sidebar footer. Deliberately not a
dropdown: with nothing to switch to, a menu holding a single option would
imply a navigation that does not exist.

The footer visibility check now also accounts for a latest-only config.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: add a mobile layout to the paper theme (#178)

The paper theme had no small-screen branch at all — none of its four CSS
modules contained a single media query. At 390px it rendered the full desktop
layout: the 262px sidebar ate two thirds of the viewport, and `.content`'s
`margin-left: calc(-1 * var(--paper-sidebar-width))` dragged the article
underneath it, clipping headings to "e (3.0)", "st" and "ed". There was no
header, no hamburger and no way to reach the navigation.

Add a mobile layout mirroring the default theme's, at the same 768px
breakpoint:

- a sticky mobile header carrying the existing SidebarHeader, the theme
  switcher and a hamburger toggle
- a full-screen menu holding the chapter nav, a divider, the configured links
  as inline items (SidebarLinks variant='list') and the version switcher
- the desktop sidebar hidden and `.content`'s negative offset neutralised, so
  the article is full-width and unclipped
- the menu closes on navigation

The reading-progress rail is a fixed 200px column pinned to the right edge, so
it is dropped below the breakpoint rather than left overlapping the article,
and the page navbar parks below the header instead of colliding with it.

Two narrow-viewport defects surfaced once the content became full-width: a long
breadcrumb could not shrink past its intrinsic width and stretched the page 7px
wider than the viewport, and a long site title wrapped out of the fixed-height
header. Both are corrected inside the media query.

The mobile chrome only renders when the sidebar does, so it composes with
reader mode rather than fighting it. Desktop rendering is unchanged —
screenshots at 1440x900 before and after are byte-identical.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant