Skip to content

Migrate onto the Moderne design system and rework the header - #538

Merged
zieka merged 12 commits into
masterfrom
refactor/migrate-moderne-design
Sep 2, 2026
Merged

Migrate onto the Moderne design system and rework the header#538
zieka merged 12 commits into
masterfrom
refactor/migrate-moderne-design

Conversation

@zieka

@zieka zieka commented Aug 28, 2026

Copy link
Copy Markdown
Member

Problem

  • docs.openrewrite.org shares a Docusaurus ancestor with docs.moderne.io but never moved onto the design system, so the two sites have drifted apart visually while still carrying each other's leftovers (this repo's custom.css and moderne-docs' both still declare the same Beausite @font-face and mint #85FE99).
  • 48 colour literals, two hand-written font stacks and a hand-rolled type scale meant every value was a guess. Nothing failed loudly when one drifted.
  • 5 more colours were baked inside data-URI SVGs, where a CSS variable cannot reach them, forcing duplicate light/dark copies of the same glyph.
  • The single 116px navbar had no section navigation, so all 9 doc sections competed in one 370px sidebar with no top-level axis.

Objectives

  1. Resolve every colour, type and dimension value from @moderneinc/design-system-tokens instead of literals.
  2. Make themeable icons themeable, removing the duplicate dark-mode artwork.
  3. Rework the header into the two-row layout docs.moderne.io uses, with the sidebar scoped to the selected section.
  4. Keep the header's reserved height correct at every viewport.
  5. Preserve OpenRewrite's identity where the system has no equivalent.
Assumptions
  • @moderneinc/design-system-tokens@7.3.0 is public on npm and installs without auth, so this repo's public CI can build it. Pinned exact; the installed moderne.css is byte-identical to the published latest.
  • moderne.css is imported whole rather than per-module. Importing only the four files currently used is ~33% smaller, but a token added later would resolve to nothing with no error — worth 7KB gzip to avoid.
  • Mint #85FE99 has no semantic token. It exists in the palette only as a raw entry, and consuming raw palette entries is the misuse the system warns about, so it stays a brand literal — as docs.moderne.io also keeps it.
  • The active-section colour is blue in light and mint in dark. Mint measures 1.11:1 on the light ground and 14.54:1 on the dark one; docs.moderne.io splits it the same way.
  • --ifm-navbar-height is 56px, not the 37px upstream uses: this site's logo is 33px and its search field 38px, against a smaller lockup there.
  • useWindowSize() is the supported way to tell the desktop and mobile sidebars apart — both render through DocSidebarItems at level 1.
  • navSections.ts hrefs must match generated-index slugs in sidebars.ts. Nothing checks this at build time; a mismatch falls back to the full sidebar rather than failing.
Changes
  • (1) package.json, docusaurus.config.ts — add the tokens dependency; customCss loads moderne.css then custom.css; swap the Inter webfont for Geist + Geist Mono.
  • (1) src/css/custom.css — Infima bridge mapping --ifm-* onto --mod-*. 48 colour literals become 5.
  • (1) src/components/button.module.css, src/theme/DocCard/styles.module.css — onto --mod-button-primary-* and text/border/shadow tokens.
  • (1) src/css/custom.css — admonitions take the mode-aware --mod-color-feedback-* tints and the reference's rounded hairline treatment, replacing Infima's coloured left stripe.
  • (2) src/css/custom.css — sidebar carets, breadcrumb separator, pagination arrows and the footer mark redrawn as mask-image; the SVG supplies the shape, a token the colour. Deletes 3 dark-mode overrides and one duplicated 1.4KB path.
  • (3) src/theme/Navbar/Layout/* — fixed wrapper holding announcement bar, navbar and section nav as one block.
  • (3) src/theme/Navbar/Content/* — three zones: logo left, search and colour-mode toggle centred, external links right.
  • (3) src/components/SecondaryNav/*, src/config/navSections.ts — the section bar; six sections left, three behind a More dropdown mirroring upstream's right-aligned Releases.
  • (3) src/theme/DocSidebarItems/* — scopes the sidebar to the active section, headed by its name. Desktop only.
  • (4) src/theme/Navbar/Layout/index.tsx — a ResizeObserver publishes the header's measured height as --docs-header-height; the CSS value is a first-paint default only.
  • (5) src/css/custom.css — mint kept as --ifm-color-mint for the announcement bar, card hover and dark-mode active section.

No tests: this repo has no test framework or test files, and this PR adds no testable logic. tsc and the production build are the available checks.

Notes
  • Announcement-bar text is deliberately not mode-aware. The mint ground never inverts, so a mode-aware token would put near-white on green at 1.11:1. Pinned to --mod-ink-800 (defined only in the package's :root, never its dark block) it is 11.96:1.
  • The header is measured, not declared. A hardcoded reserve was wrong on mobile — the bar's height is auto there and its copy wraps to ~3 lines against a 30px reserve, hiding content under the fixed header. Measuring is correct at any width and makes a third row a zero-CSS change.
  • Sidebar scoping is desktop-only. The section bar is hidden below 997px, so scoping there would strand a reader in one section with no way out.
  • Beausite is gone. Nothing referenced it after the type swap, so the @font-face went too. The doc h1 is 36px Geist, matching docs.moderne.io.
  • Left alone deliberately: the colourblind-accessible amber code-highlight colours (--mod-color-text-warning is a different colour with a different meaning), and gap: 15px in button.module.css (14 and 16 bracket it on the scale). A near-miss substituted silently is worse than a literal left in place.
  • No token equivalent — worth raising upstream: mint has no semantic tier; semanticTypography tops out at 28px so there is no display size for a docs h1; package shadows have no dark variant, though --mod-color-surface-shadow-neutral supplies the mode-aware colour.
  • Review found three defects, all fixed: two announcement bars rendered (the default slot plus the wrapper), leaving a dead 40px band; two SearchBar instances mounted below 997px, so Cmd+K fired twice; and the header reserve above. Also reused @theme/Icon/ExternalLink and @theme/Navbar/MobileSidebar/Toggle rather than the reimplementations first written, restoring the translated "(opens in new tab)" label and the touchstart/focusin dismissal the theme's dropdown uses.
  • Verified: yarn typecheck clean; production build succeeds; all 43 --mod-*/--docs-* references resolve in the built CSS with none dangling; light and dark checked in-browser; 23 contrast pairings pass AA for normal text in both themes, worst 5.39.
  • Pre-existing, not from this PR: the build's <p> minifier warnings come from <ReactPlayer> inline in markdown (no .md file is touched here). Separately, yarn typecheck OOMs locally if you build first — tsconfig.json sets allowJs: true with no exclude, so tsc walks 1.3GB of build/. CI is unaffected (typecheck runs before build); a one-line exclude would fix it.

https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia

zieka added 2 commits August 31, 2026 08:24
…eader

Depend on @moderneinc/design-system-tokens@7.3.0 and bridge Infima's --ifm-*
variables onto its --mod-* tokens, replacing 48 colour literals. Rework the
header into the two-row layout docs.moderne.io uses — navbar above a section
nav — and scope the sidebar to the selected section.

Colours baked inside data-URI SVGs cannot take a CSS variable, so the carets,
breadcrumb separator, pagination arrows and footer mark are redrawn as
mask-image, which let three hand-written dark-mode overrides be deleted.

The header's height is measured rather than declared: the announcement bar
wraps on mobile, so any constant is wrong there.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
Raise the navbar to 72px so the logo clears the announcement bar by 20px
rather than 12px.

Pin card-hover text to --mod-ink-800. The mint hover ground is identical in
both themes, so text following the colour mode went near-white on green in
dark mode — 1.11:1, unreadable. Pinned it is 11.96:1, matching how the
announcement bar already handles the same ground.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
@zieka
zieka force-pushed the refactor/migrate-moderne-design branch from 51a1884 to 73df896 Compare August 31, 2026 15:26
@zieka

zieka commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

per zoom convo
image

  • on main page, update accent colors from blue to green
  • on main page, remove left side nav
  • in nav > more menu, remove licensing and put into a footer
  • in nav > more menu, move training to main nav item
  • in nav, move training to under recipe authorship
  • in nav, change more menu to releases and dropdown 3 most recent releases and a more releases item

…ing page

Nav bar: Training moves under Authoring Recipes in the sidebar tree, Lists
takes its place as a top-level section, and the More menu becomes Releases —
the three most recent releases plus All releases. The release list is read
from the Changelog category in sidebars.ts rather than hand-written, since an
automated job adds an entry on every release.

Licensing moves out of the nav into the footer, matching docs.moderne.io.

The introduction page reads as a landing page: no sidebar, green accents. The
sidebar is detached with `displayed_sidebar: null` rather than hidden in CSS,
which also releases the width Docusaurus reserves for the column.

Accents are a mode-aware pair because no single green clears AA on both
grounds: mint-700 is 5.12:1 on the light ground, the brand mint 14.54:1 on the
dark one and 1.11:1 on the light.

Adds src/theme/Root.tsx to publish the route as data-route. Docusaurus's own
docs-doc-id-* classes exist only in a production build, so styling one page
through them renders differently under `yarn start`.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
…ault

Swizzle Footer to match docs.moderne.io: copyright and links left, social
right, over a hairline. The Infima default stacked its single link above a
centred copyright and the Moderne mark, right-aligned against the page edge.

Adds Terms, Privacy and Contact us alongside Licensing, and X/LinkedIn/YouTube
— the same set docs.moderne.io carries, consistent with the Moderne copyright
this site already showed. Icons are inline SVG rather than an icon package;
three glyphs used once each do not justify a dependency.

Drops the Moderne mark that the old footer rendered as a mask, since the
reference treatment has no mark.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
theme-classic pulls the sidebar container up by --ifm-navbar-height and pads
the sidebar back down by the same amount, so the menu clears a navbar the
container sits beneath. This layout already positions the container below the
whole fixed header, so the padding only showed as a 72px gap above the first
item. 86px from header to heading, now 14px.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
…inator buttons

The generated-index pages painted a 1322x1070 decorative image behind the card
grid, which cut across the cards at most widths and carried no meaning.

Breadcrumbs lose the pill on the active crumb: they mark position rather than
offering an action, so they read as text, with the current page muted.

Previous/next become plain text links with an inline arrow, replacing the pill
buttons with masked arrow artwork — a way out of the page, not a call to
action. Swizzles DocPaginator and removes ~70 lines of CSS along with the two
arrow masks that only it used.

static/img/gems-bg.webp is now unreferenced but left on disk; removing an asset
is a separate call.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
The search bar ran on Algolia's stock palette — an rgba(0,0,0,.65) ground,
#212139 border and #36395a keys — which reads as a foreign component beside the
warm/ink set. Only two variables were bound before, and both were scoped to
html[data-theme=light], so dark mode was entirely unthemed.

Binds the button and the modal to --mod-* tokens in one :root block: every one
is mode-aware, so no light/dark pair is needed. Rounds the button to 8px, which
the package hard-codes at 4px, and gives the keys a border so they read as keys
rather than a darker patch of the button. Matches docs.moderne.io.

The modal shares these variables, so it is themed alongside rather than left to
open in the package's colours.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
@zieka
zieka marked this pull request as ready for review September 1, 2026 21:08

@sjungling sjungling left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits and notes. no blockers

Comment thread package.json Outdated
Comment on lines +16 to +32
const XIcon: FunctionComponent = () => (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);

const LinkedInIcon: FunctionComponent = () => (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.45 20.45h-3.56v-5.57c0-1.33-.03-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.63-1.85 3.36-1.85 3.6 0 4.27 2.37 4.27 5.45v6.29zM5.34 7.43a2.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13zm1.78 13.02H3.55V9h3.57v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.72V1.72C24 .77 23.2 0 22.22 0z" />
</svg>
);

const YouTubeIcon: FunctionComponent = () => (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M23.5 6.19a3.02 3.02 0 0 0-2.12-2.14C19.5 3.55 12 3.55 12 3.55s-7.5 0-9.38.5A3.02 3.02 0 0 0 .5 6.19C0 8.08 0 12 0 12s0 3.92.5 5.81a3.02 3.02 0 0 0 2.12 2.14c1.88.5 9.38.5 9.38.5s7.5 0 9.38-.5a3.02 3.02 0 0 0 2.12-2.14C24 15.92 24 12 24 12s0-3.92-.5-5.81zM9.55 15.57V8.43L15.82 12l-6.27 3.57z" />
</svg>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have an icon package do we? that rebundles lucide and our custom icons?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope; might be worth putting that in the design system

Comment on lines +20 to +31
const Chevron: FunctionComponent<{ open: boolean }> = ({ open }) => (
<svg
className={clsx(styles.chevron, open && styles.chevronOpen)}
width="10"
height="6"
viewBox="0 0 10 6"
fill="none"
aria-hidden="true"
>
<path d="M1 1L5 5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wasn't the chevron also inlined as a data+svg in custom.css?

border-bottom: 1px solid var(--mod-color-border-primary);
}

@media (min-width: 997px) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit i think we have a breakpoint design token?

@github-project-automation github-project-automation Bot moved this from In Progress to Ready to Review in OpenRewrite Sep 2, 2026

@mike-solomon mike-solomon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving - two minor things we talked about in perma zoom to fix:

  1. Scrolling down on a page makes the side nav bar go under the header.
  2. Info/warning/tip/etc boxes have a strange design (lowercase text, weird spacing, etc).

…f contents

Addresses review feedback.

Bumps @moderneinc/design-system-tokens to 7.4.0, the current `latest`. All 43
tokens this repo uses still exist there and every value is byte-identical, so
the bump carries no visual change.

Replaces the chevron inlined in SecondaryNav with the --docs-chevron-down mask
already defined for the sidebar caret. One definition of the glyph instead of
two at different sizes and stroke widths; the mask follows currentColor exactly
as the inline SVG did.

Re-derives the table of contents' sticky offset from --docs-header-height.
theme-classic computes it from --ifm-navbar-height alone, which here is only
the first of the header's three rows, so the TOC pinned 72px behind the fixed
header and its entries slid underneath on scroll. Its max-height overshot the
viewport by the same amount. Now pins at 176px, clearing the 160px header.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
… tree

Admonitions were laid out as a row, so an untitled one ran its body beside the
type label and every line after the first was indented to the width of the word
"info". They are now always stacked: heading on its own line, body starting at
the left edge. The heading takes the size and weight of the heading it is, and
the icons render filled again — they are drawn as fill-rule="evenodd" paths, and
forcing fill:none with a stroke had turned each into a hollow outline.

Separately, section detection matched on URL prefix, which cannot work here.
Concepts & explanations is served at /concepts-explanations while its documents
live at /concepts-and-explanations, so none of them resolved: no scoped sidebar
and no active nav item for the whole section. Lists and Reference both serve
documents from /reference, so a prefix cannot tell them apart either, and every
Lists document resolved to Reference.

Sections are now resolved from the sidebar tree, which is the only thing that
knows which section a document belongs to, falling back to prefix matching for
the index pages themselves. Safe to derive from doc ids: routeBasePath is "/"
and no document in a section overrides its slug.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
":::info" now renders "Info" rather than "info", for all six types and the
three legacy aliases. An author's own title is passed through exactly as
written.

Wraps @theme/Admonition rather than the two more obvious routes, both of which
fail here:

- A translation override in i18n/en/code.json is the documented mechanism, but
  creating an i18n directory switches on Docusaurus's translation machinery for
  the whole site, and the generated recipe-catalog sidebar has duplicate
  category labels (Camel x3, Upgrade x2) that then fail the build.
- text-transform: capitalize cannot be scoped to the generic label. In this
  version a custom title renders as a bare text node in the heading, exactly
  like the default, so CSS cannot tell them apart and would re-case
  ":::info[Which filters require symbol data]" word by word.

The wrapper runs the theme's own processAdmonitionProps, so a title supplied
either as ":::info[Title]" or as a legacy mdxAdmonitionTitle child is detected
and left alone; the label is injected only when there is none.

Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia
@zieka
zieka merged commit 1f95422 into master Sep 2, 2026
2 checks passed
@zieka
zieka deleted the refactor/migrate-moderne-design branch September 2, 2026 17:13
@github-project-automation github-project-automation Bot moved this from Ready to Review to Done in OpenRewrite Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants