From 82a883f0f47f0617107b3e34a962cee8c3acf2f6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 28 Aug 2026 08:46:51 -0700 Subject: [PATCH 01/12] feat(design): migrate onto the Moderne design system and rework the header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docusaurus.config.ts | 28 +- package.json | 1 + .../SecondaryNav/SecondaryNav.module.css | 100 +++++ src/components/SecondaryNav/SecondaryNav.tsx | 134 ++++++ src/components/SecondaryNav/index.ts | 2 + src/components/button.module.css | 36 +- src/config/navSections.ts | 34 ++ src/css/custom.css | 417 +++++++++++++----- src/theme/AnnouncementBar/index.tsx | 28 +- src/theme/AnnouncementBar/styles.module.css | 14 +- src/theme/DocCard/styles.module.css | 20 +- src/theme/DocSidebarItems/filterUtils.ts | 62 +++ src/theme/DocSidebarItems/index.tsx | 41 ++ src/theme/Navbar/Content/index.tsx | 127 ++++++ src/theme/Navbar/Content/styles.module.css | 93 ++++ src/theme/Navbar/Layout/index.tsx | 110 +++++ src/theme/Navbar/Layout/styles.module.css | 35 ++ yarn.lock | 5 + 18 files changed, 1120 insertions(+), 167 deletions(-) create mode 100644 src/components/SecondaryNav/SecondaryNav.module.css create mode 100644 src/components/SecondaryNav/SecondaryNav.tsx create mode 100644 src/components/SecondaryNav/index.ts create mode 100644 src/config/navSections.ts create mode 100644 src/theme/DocSidebarItems/filterUtils.ts create mode 100644 src/theme/DocSidebarItems/index.tsx create mode 100644 src/theme/Navbar/Content/index.tsx create mode 100644 src/theme/Navbar/Content/styles.module.css create mode 100644 src/theme/Navbar/Layout/index.tsx create mode 100644 src/theme/Navbar/Layout/styles.module.css diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 0f1d06d77e..845da550fb 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -41,26 +41,16 @@ const config: Config = { }, innerHTML: JSON.stringify(structuredData), }, - // Runs before hydration so neither the bar nor the navbar offset flashes. - // Hides the notice bar for visitors who already dismissed it, and records whether - // a bar is showing on this route so custom.css can offset the sticky navbar. - // Which bar applies depends on the path, but the dismissal flags are global, hence - // the route check. Keys and attributes must match src/theme/AnnouncementBar/index.tsx. + // Runs before hydration so the notice bar does not flash for visitors who + // already dismissed it. The key and attribute must match + // src/theme/AnnouncementBar/index.tsx. { tagName: 'script', attributes: {}, innerHTML: `try { - var noticeDismissed = window.localStorage.getItem('code-genome-project-announcement-dismissed') === 'true'; - if (noticeDismissed) { + if (window.localStorage.getItem('code-genome-project-announcement-dismissed') === 'true') { document.documentElement.setAttribute('data-notice-bar-dismissed', 'true'); } - var onRecipes = window.location.pathname.indexOf('/recipes') === 0; - var dismissed = onRecipes - ? window.localStorage.getItem('docusaurus.announcement.dismiss') === 'true' - : noticeDismissed; - if (!dismissed) { - document.documentElement.setAttribute('data-bar-visible', 'true'); - } } catch (e) {}`, }, // @@ -84,7 +74,7 @@ const config: Config = { tagName: 'link', attributes: { rel: 'stylesheet', - href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', + href: 'https://fonts.googleapis.com/css2?family=Geist:ital,wght@0,300..700;1,300..700&family=Geist+Mono:ital,wght@0,400;0,500;1,400&display=swap', }, }, @@ -136,7 +126,13 @@ const config: Config = { trackingID: "G-Y67JVX3WB7", }, theme: { - customCss: "./src/css/custom.css", + // Order matters: the design-system tokens define every --mod-* the + // site references, and custom.css is the Infima bridge, so it must + // load second for its --ifm-* mappings to win. + customCss: [ + require.resolve("@moderneinc/design-system-tokens/moderne.css"), + "./src/css/custom.css", + ], }, } satisfies Preset.Options, ], diff --git a/package.json b/package.json index a644373024..1d526f1b99 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@docusaurus/theme-mermaid": "^3.9.2", "@mdx-js/react": "^3.1.0", "@mermaid-js/layout-elk": "^0.1.9", + "@moderneinc/design-system-tokens": "7.3.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", diff --git a/src/components/SecondaryNav/SecondaryNav.module.css b/src/components/SecondaryNav/SecondaryNav.module.css new file mode 100644 index 0000000000..6c4888ff99 --- /dev/null +++ b/src/components/SecondaryNav/SecondaryNav.module.css @@ -0,0 +1,100 @@ +/** + * Section navigation, below the primary navbar. Desktop only: on mobile the + * sidebar still lists every section, so this bar would only repeat it. + */ + +.secondaryNav { + display: none; + height: var(--ifm-secondary-nav-height); + padding: 0 var(--mod-spacing_4); + align-items: center; + justify-content: space-between; + /* Hairline that defines the header's lower edge. */ + border-bottom: 1px solid var(--mod-color-border-primary); +} + +@media (min-width: 997px) { + .secondaryNav { + display: flex; + } +} + +.sections { + display: flex; + align-items: center; + gap: var(--mod-spacing_3); + min-width: 0; +} + +.section { + font-size: var(--mod-font-size-default); + font-weight: var(--mod-font-weight-medium); + color: var(--mod-color-text-primary); + text-decoration: none; + white-space: nowrap; + transition: color 0.2s ease; +} + +.section:hover { + color: var(--mod-color-action-default); + text-decoration: none; +} + +/* Both active states take the same colour; custom.css owns the light/dark pair. */ +.sectionActive, +.sectionActive:hover, +.dropdownItemActive, +.dropdownItemActive:hover { + color: var(--docs-color-active); +} + +.more { + position: relative; + flex-shrink: 0; +} + +.moreButton { + display: flex; + align-items: center; + gap: var(--mod-spacing_1_2); + padding: var(--mod-spacing_1) 0; + background: transparent; + border: none; + cursor: pointer; +} + +.chevron { + transition: transform 0.2s ease; +} + +.chevronOpen { + transform: rotate(180deg); +} + +.dropdown { + position: absolute; + top: 100%; + right: 0; + min-width: 200px; + padding: var(--mod-spacing_1) 0; + background-color: var(--mod-color-surface-raised); + border: 1px solid var(--mod-color-border-primary); + border-radius: var(--mod-border-radius-input); + box-shadow: var(--docs-shadow-dropdown); + z-index: var(--ifm-z-index-dropdown); +} + +.dropdownItem { + display: block; + padding: var(--mod-spacing_1) var(--mod-spacing_2); + font-size: var(--mod-font-size-default); + color: var(--mod-color-text-primary); + text-decoration: none; + white-space: nowrap; +} + +.dropdownItem:hover { + background-color: var(--mod-color-surface-row-hover); + color: var(--mod-color-text-primary); + text-decoration: none; +} diff --git a/src/components/SecondaryNav/SecondaryNav.tsx b/src/components/SecondaryNav/SecondaryNav.tsx new file mode 100644 index 0000000000..b2e246b564 --- /dev/null +++ b/src/components/SecondaryNav/SecondaryNav.tsx @@ -0,0 +1,134 @@ +import Link from '@docusaurus/Link'; +import { useLocation } from '@docusaurus/router'; +import clsx from 'clsx'; +import { type FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'; +import type { NavSection } from '@site/src/config/navSections'; +import { findSection } from '@site/src/theme/DocSidebarItems/filterUtils'; +import styles from './SecondaryNav.module.css'; + +export type SecondaryNavProps = { + /** Sections rendered as direct links, left-aligned. */ + sections: NavSection[]; + /** Label for the right-aligned dropdown. */ + moreLabel: string; + /** Items inside that dropdown. */ + moreItems: NavSection[]; +}; + +/** Chevron for the dropdown. Inline rather than an icon package: it is the only + * glyph this component needs, and it inherits currentColor for free. */ +const Chevron: FunctionComponent<{ open: boolean }> = ({ open }) => ( + +); + +/** + * Section navigation, shown below the primary navbar on desktop. + * + * Picks the section; the sidebar then shows that section's contents (see + * src/theme/DocSidebarItems, which scopes on desktop only). Hidden below 997px, + * where the drawer keeps every section and this bar would duplicate it. + */ +export const SecondaryNav: FunctionComponent = ({ + sections, + moreLabel, + moreItems, +}) => { + const [open, setOpen] = useState(false); + const moreRef = useRef(null); + const location = useLocation(); + + // Same matcher the sidebar filter uses, so the highlighted section and the + // scoped sidebar can never disagree. + const activeHref = findSection(location.pathname)?.href; + const isMoreActive = moreItems.some((item) => item.href === activeHref); + + const close = useCallback(() => setOpen(false), []); + + // Close on outside click and on Escape, so the menu never outlives its context. + useEffect(() => { + if (!open) { + return; + } + // mousedown + touchstart + focusin is the set theme-classic's own + // DropdownNavbarItem listens on; mousedown alone leaves the menu open on a + // touch tap and when focus moves away by keyboard. + const onOutside = (event: MouseEvent | TouchEvent | FocusEvent) => { + if (moreRef.current && !moreRef.current.contains(event.target as Node)) { + close(); + } + }; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + close(); + } + }; + const events = ['mousedown', 'touchstart', 'focusin'] as const; + events.forEach((name) => document.addEventListener(name, onOutside)); + document.addEventListener('keydown', onKeyDown); + return () => { + events.forEach((name) => document.removeEventListener(name, onOutside)); + document.removeEventListener('keydown', onKeyDown); + }; + }, [open, close]); + + // A route change means the menu's job is done. + useEffect(close, [location.pathname, close]); + + return ( + + ); +}; + +SecondaryNav.displayName = 'SecondaryNav'; diff --git a/src/components/SecondaryNav/index.ts b/src/components/SecondaryNav/index.ts new file mode 100644 index 0000000000..194f1a9e2a --- /dev/null +++ b/src/components/SecondaryNav/index.ts @@ -0,0 +1,2 @@ +export { SecondaryNav } from './SecondaryNav'; +export type { SecondaryNavProps } from './SecondaryNav'; diff --git a/src/components/button.module.css b/src/components/button.module.css index 921f345ce9..b249fb5e79 100644 --- a/src/components/button.module.css +++ b/src/components/button.module.css @@ -1,10 +1,12 @@ .container { display: grid; grid-template-columns: 1fr 1fr; - border: 1px solid #2546F0; - padding: 30px; + border: 1px solid var(--mod-color-border-primary); + padding: var(--mod-spacing_3_3_4); + /* 15px is off the spacing scale (14 and 16 are the neighbours). Left as a + literal rather than silently shifting the layout by a pixel. */ gap: 15px; - margin: 16px 0; + margin: var(--mod-spacing_2) 0; } .subContainer { @@ -14,27 +16,31 @@ } .heading { - font-weight: 500; - font-size: 18px; + font-weight: var(--mod-font-weight-medium); + font-size: var(--mod-font-size-h5); line-height: 23px; - /* color: #FBFDFF; */ } +/* Primary action: the design system's primary button is a high-contrast + neutral, not a blue fill. It inverts between themes (navy on cream, cream on + near-black), so it stays the strongest element in either mode. */ .link { - font-weight: 400; - font-size: 16px; + font-weight: var(--mod-font-weight-regular); + font-size: var(--mod-font-size-default); line-height: 23px; - background-color: #2546F0; - padding: 8px; + background-color: var(--mod-button-primary-bg); + padding: var(--mod-spacing_1); align-items: center; - border-radius: 6px; - color: #FBFDFF; + border-radius: var(--mod-border-radius-button); + color: var(--mod-color-text-on-primary); display: inline-flex; - gap: 8px; + gap: var(--mod-spacing_1); } +/* Pin the text colour: rendered as an , Infima's global a:hover would + otherwise repaint it with the link colour. */ .link:hover { - background-color: #2546F080; - color: #FBFDFF; + background-color: var(--mod-button-primary-bg-hover); + color: var(--mod-color-text-on-primary); text-decoration: none; } diff --git a/src/config/navSections.ts b/src/config/navSections.ts new file mode 100644 index 0000000000..9df7800768 --- /dev/null +++ b/src/config/navSections.ts @@ -0,0 +1,34 @@ +/** + * Sections shown in the secondary nav. + * + * `href` must match a top-level category's generated-index slug in sidebars.ts — + * that is how a section resolves to its sidebar subtree, and nothing checks it + * at build time. An href matching no category still links, but its pages fall + * back to the full sidebar; Training and Licensing do exactly that, being single + * documents rather than trees. + */ + +export type NavSection = { + name: string; + /** Route the section starts at, and the prefix that marks it active. */ + href: string; +}; + +/** Left-aligned, ordered as a reader meets them. Six is the practical ceiling + * before the bar wraps around 1200px. */ +export const primarySections: NavSection[] = [ + { name: 'Running Recipes', href: '/running-recipes' }, + { name: 'Authoring Recipes', href: '/authoring-recipes' }, + { name: 'Recipe catalog', href: '/recipes' }, + { name: 'Reference', href: '/reference' }, + { name: 'Concepts & explanations', href: '/concepts-explanations' }, + { name: 'Training', href: '/training' }, +]; + +/** Right-aligned dropdown, mirroring Releases on docs.moderne.io. Reference + * material, looked up rather than browsed. */ +export const moreSections: NavSection[] = [ + { name: 'Lists', href: '/lists' }, + { name: 'Changelog', href: '/changelog' }, + { name: 'Licensing', href: '/licensing/openrewrite-licensing' }, +]; diff --git a/src/css/custom.css b/src/css/custom.css index ef58cc272b..1145696499 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -1,58 +1,114 @@ /** - * Any CSS included here will be global. The classic template - * bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. + * Design-system Infima bridge. + * + * Maps Docusaurus/Infima variables (--ifm-*) onto design-system tokens + * (--mod-*, from @moderneinc/design-system-tokens). Most --mod-* tokens are + * mode-aware, so a single :root declaration covers light and dark; the + * html[data-theme='dark'] block below only restates the vars Infima also + * defines in its own dark block, which out-specifies :root. + * + * Guidelines: + * - Reference var(--mod-*) without fallbacks so a missing token fails visibly + * rather than silently rendering transparent. + * - Add brand values here, never as one-off literals in components. */ -@font-face { - font-family: 'Beausite'; - src: url('https://cdn.prod.website-files.com/664e62fe66f5b31edc6c3aeb/664e7a833d9ad9fa2febd048_BeausiteSlickWeb-Light.woff2') format('woff2'), url('https://cdn.prod.website-files.com/664e62fe66f5b31edc6c3aeb/664e7a830c62d5d188a8f45a_BeausiteSlickWeb-Light.woff') format('woff'); - font-weight: 300; - font-style: normal; - font-display: swap; -} - /* You can override the default Infima variables here. */ :root { - --ifm-background-color: #F8F8FF; + --ifm-background-color: var(--mod-color-surface-page); + + /* Mint accent. No design-system token maps to it: the mark is OpenRewrite's + and the system has it only as the raw palette entry digitalGreen.300, which + has no semantic tier. Kept as a brand literal, matching how docs.moderne.io + preserves the same accent. */ --ifm-color-mint: #85FE99; - --ifm-color-primary: #4169e1; - --ifm-color-primary-dark: #2855dd; - --ifm-color-primary-darker: #224ed5; - --ifm-color-primary-darkest: #1c41af; - --ifm-color-primary-light: #5a7de5; - --ifm-color-primary-lighter: #6787e7; - --ifm-color-primary-lightest: #8ca4ed; - --ifm-color-border: #D9D9D9; + + /* Primary accent. The system has no blue ramp, so the derived Infima tiers + collapse onto the two action tokens that exist. */ + --ifm-color-primary: var(--mod-color-action-default); + --ifm-color-primary-dark: var(--mod-color-action-navy); + --ifm-color-primary-darker: var(--mod-color-action-navy); + --ifm-color-primary-darkest: var(--mod-color-action-navy); + --ifm-color-primary-light: var(--mod-color-action-default); + --ifm-color-primary-lighter: var(--mod-color-action-default); + --ifm-color-primary-lightest: var(--mod-color-action-default); + + --ifm-color-border: var(--mod-color-border-primary); --ifm-code-font-size: 95%; - --ifm-color-base: rgba(4, 24, 52); + --ifm-color-base: var(--mod-color-text-primary); --ifm-color-content: var(--ifm-color-base); --ifm-color-announcement-bar-bg: var(--ifm-color-mint); - --ifm-color-announcement-bar-text: #041834; - --ifm-navbar-background-color: var(--ifm-background-color); - --ifm-navbar-padding-vertical: 36px; + + /* Deliberately not mode-aware: the bar ground is always mint, so text that + followed the colour mode would go near-white on green in dark. --mod-ink-800 + is defined only in the package's :root block, never in its dark block, which + is exactly the property needed here. */ + --ifm-color-announcement-bar-text: var(--mod-ink-800); + + /* Transparent: the fixed wrapper in Navbar/Layout paints the header ground, + so the navbar painting its own would double up. */ + --ifm-navbar-background-color: transparent; + --ifm-navbar-padding-vertical: 0; --ifm-navbar-item-padding-horizontal: 1.4rem; - --ifm-navbar-height: 116px; - --ifm-font-family-base: "Inter", sans-serif; - --ifm-heading-font-weight: 400; + + /* The header is two rows: this navbar and the section nav below it. Infima + only knows about the first; --ifm-secondary-nav-height is declared with the + header offsets near the end of this file, where it is used. + + Taller than the 37px docs.moderne.io uses, because this site's chrome is + bigger: a 33px logo and a 38px search field, against their smaller lockup. + At 37px the search overflowed the bar and the logo sat 2.5px under the + announcement bar. 56px leaves ~9px around the search and ~11px around the + logo. */ + --ifm-navbar-height: var(--mod-spacing_7); + + /* System fallbacks in case the webfont fails to load. */ + --ifm-font-family-base: var(--mod-font-family-body), system-ui, sans-serif; + --ifm-font-family-monospace: var(--mod-font-family-code), ui-monospace, monospace; + + --ifm-heading-font-weight: var(--mod-font-weight-regular); --ifm-heading-line-height: 1.25; - --ifm-heading-color: #041834; - --ifm-font-weight-bold: 600; + --ifm-heading-color: var(--mod-color-text-primary); + --ifm-font-weight-bold: var(--mod-font-weight-semi-bold); --ifm-footer-padding-vertical: 2.375rem; --ifm-footer-background-color: transparent; - --ifm-breadcrumb-separator: url("data:image/svg+xml, %3Csvg%20width=%228%22%20height=%2213%22%20viewBox=%220%200%208%2013%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d=%22M1%2012L7%206.5L1%201%22%20stroke=%22%23415067%22%20stroke-width=%221.25%22%2F%3E%0A%3C%2Fsvg%3E%0A"); - --ifm-breadcrumb-item-background-active: #FFF; + + /* Chevron drawn with mask-image rather than a data-URI background, so the + colour comes from a token instead of a hex baked inside the URL. */ + --ifm-breadcrumb-separator: none; + --ifm-breadcrumb-item-background-active: var(--mod-color-surface-raised); --ifm-breadcrumb-spacing: 0.875rem; + --ifm-menu-color-background-hover: transparent; --ifm-menu-color-background-active: transparent; - --ifm-link-color: #283AF7; - --ifm-link-hover-color: var(--ifm-color-content); - --ifm-menu-color: rgba(4, 24, 52); + --ifm-link-color: var(--mod-color-action-default); + --ifm-link-hover-color: var(--mod-color-action-navy); + --ifm-menu-color: var(--mod-color-text-tertiary); --ifm-menu-color-active: var(--ifm-link-color); --ifm-navbar-link-color: var(--ifm-heading-color); --ifm-navbar-shadow: none; --ifm-toc-border-color: transparent; - --ifm-section-divider-color: rgba(4, 24, 52, 0.3); + --ifm-section-divider-color: var(--mod-color-border-primary); + + /* Colour marking the current section, in the section nav and above the scoped + sidebar. Blue in light; mint is the brand accent but measures 1.11:1 on the + light ground, against 14.54:1 on the dark one, so it is used only there. */ + --docs-color-active: var(--mod-color-action-default); + + /* Card/dropdown shadow. Geometry is local because the package's + --mod-shadow-dropdown is a different shape (0 8px 15px); the colour is the + package's mode-aware shadow token, so this needs no light/dark pair. */ + --docs-shadow-dropdown: 0 var(--mod-spacing_1_1_2) var(--mod-spacing_3_1_2) + var(--mod-color-surface-shadow-neutral); + + /* Icon geometry, shared by the breadcrumb separator, the sidebar caret and the + pagination arrows. Used only as masks, so the stroke colour in each URL is + irrelevant — a mask reads alpha, and the colour comes from whatever applies + it. That is the point: a hex inside a data-URI cannot be themed. */ + --docs-chevron: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='8'%20height='13'%20viewBox='0%200%208%2013'%3E%3Cpath%20d='M1%2012L7%206.5L1%201'%20fill='none'%20stroke='%23000'%20stroke-width='1.25'%2F%3E%3C%2Fsvg%3E"); + --docs-chevron-down: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='12'%20height='8'%20viewBox='0%200%2012%208'%3E%3Cpath%20d='M0.5%200.5L6%206.5L11.5%200.5'%20fill='none'%20stroke='%23000'%20stroke-width='1.25'%2F%3E%3C%2Fsvg%3E"); + --docs-arrow-left: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='43'%20height='16'%20viewBox='0%200%2043%2016'%3E%3Cpath%20d='M0.292892%207.2929C-0.0976296%207.68342%20-0.0976295%208.31659%200.292893%208.70711L6.65685%2015.0711C7.04738%2015.4616%207.68054%2015.4616%208.07107%2015.0711C8.46159%2014.6805%208.46159%2014.0474%208.07107%2013.6569L2.41422%208L8.07107%202.34315C8.46159%201.95262%208.46159%201.31946%208.07107%200.928935C7.68054%200.538411%207.04738%200.538411%206.65685%200.928935L0.292892%207.2929ZM43%207L1%207L1%209L43%209L43%207Z'%20fill='%23000'%2F%3E%3C%2Fsvg%3E"); + --docs-arrow-right: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='43'%20height='16'%20viewBox='0%200%2043%2016'%3E%3Cpath%20d='M42.7071%208.70711C43.0976%208.31659%2043.0976%207.68343%2042.7071%207.2929L36.3431%200.928939C35.9526%200.538414%2035.3195%200.538414%2034.9289%200.928938C34.5384%201.31946%2034.5384%201.95263%2034.9289%202.34315L40.5858%208.00001L34.9289%2013.6569C34.5384%2014.0474%2034.5384%2014.6805%2034.9289%2015.0711C35.3195%2015.4616%2035.9526%2015.4616%2036.3431%2015.0711L42.7071%208.70711ZM0%209L42%209.00001L42%207.00001L0%207L0%209Z'%20fill='%23000'%2F%3E%3C%2Fsvg%3E"); } /* Use selector for specificity */ @@ -62,27 +118,29 @@ html.docs-wrapper { /* Use selector for specificity */ html[data-theme=light] { - --docsearch-searchbox-background: #fff; - --docsearch-muted-color: rgba(4, 24, 52, 0.75); + --docsearch-searchbox-background: var(--mod-color-surface-search-input); + --docsearch-muted-color: var(--mod-color-text-tertiary); } /* For readability concerns, you should choose a lighter palette in dark mode. */ +/* Restates only the bridge vars Infima also defines in its own + [data-theme='dark'] block: that selector out-specifies :root, so the + mode-aware tokens have to be repeated here at matching specificity to win. + Vars Infima leaves alone in dark — links, primary, headings — resolve + correctly from :root and are deliberately omitted. */ html[data-theme='dark'] { - --ifm-background-color: #041834; - --ifm-color-primary: #7e9bd2; - --ifm-color-primary-dark: #6588c9; - --ifm-color-primary-darker: #597ec5; - --ifm-color-primary-darkest: #3d64ae; - --ifm-color-primary-light: #97aedb; - --ifm-color-primary-lighter: #a3b8df; - --ifm-color-primary-lightest: #c9d5ec; - --ifm-color-base: #fff; - --ifm-heading-color: #fff; - --ifm-menu-color: #fff; - --ifm-link-color: #85fe99; + --ifm-background-color: var(--mod-color-surface-page); + --ifm-color-base: var(--mod-color-text-primary); + --ifm-color-content: var(--mod-color-text-primary); + + /* The one bridge var that wants a different token per mode: muted tertiary + in light, primary in dark, where tertiary loses too much contrast. */ + --ifm-menu-color: var(--mod-color-text-primary); + --ifm-toc-border-color: transparent; - --ifm-section-divider-color: #d9d9d9; - --ifm-card-background-color: #fff; + --ifm-section-divider-color: var(--mod-color-border-primary); + --ifm-card-background-color: var(--mod-color-surface-raised); + --docs-color-active: var(--ifm-color-mint); --ifm-heading-line-height: 1.25; .theme-code-block { border: 1.5px solid var(--ifm-color-border); @@ -116,10 +174,6 @@ figure figcaption { overflow: clip; } -.navbar { - border-bottom: 1px solid var(--ifm-section-divider-color); -} - .navbar .navbar__link { font-weight: 400; } @@ -137,10 +191,15 @@ figure figcaption { color: var(--ifm-heading-color); } +/* Carets are masked, not painted: the SVG supplies the shape and the token + supplies the colour, so both themes are covered by one rule and neither + caret carries a hex the theme cannot reach. */ .theme-doc-sidebar-menu .menu__caret:before { - background-image: url("data:image/svg+xml, %3Csvg%20width=%228%22%20height=%2213%22%20viewBox=%220%200%208%2013%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d=%22M1%2012L7%206.5L1%201%22%20stroke=%22%23415067%22%20stroke-width=%221.25%22%2F%3E%0A%3C%2Fsvg%3E%0A"); - background-repeat: no-repeat; - background-size: 100%; + background-image: none; + background-color: var(--mod-color-text-tertiary); + mask-image: var(--docs-chevron); + mask-repeat: no-repeat; + mask-size: 100%; width: 8px; height: 12px; transform: rotate(90deg); @@ -151,12 +210,9 @@ figure figcaption { transform: rotate(0); } -html[data-theme=dark] .theme-doc-sidebar-menu .menu__caret:before { - background-image: url("data:image/svg+xml, %3Csvg%20width=%228%22%20height=%2213%22%20viewBox=%220%200%208%2013%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d=%22M1%2012L7%206.5L1%201%22%20stroke=%22%23D9D9D9%22%20stroke-width=%221.25%22%2F%3E%0A%3C%2Fsvg%3E%0A"); -} - .theme-doc-sidebar-menu .menu__link--active + .menu__caret:before { - background-image: url("data:image/svg+xml, %3Csvg%20width=%2212%22%20height=%228%22%20viewBox=%220%200%2012%208%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d=%22M0.500001%200.499999L6%206.5L11.5%200.5%22%20stroke=%22%23283AF7%22%20stroke-width=%221.25%22%2F%3E%0A%3C%2Fsvg%3E%0A"); + background-color: var(--mod-color-action-default); + mask-image: var(--docs-chevron-down); width: 12px; height: 8px; transform: rotate(0); @@ -166,10 +222,6 @@ html[data-theme=dark] .theme-doc-sidebar-menu .menu__caret:before { transform: rotate(-90deg); } -html[data-theme=dark] .theme-doc-sidebar-menu .menu__link--active + .menu__caret:before { - background-image: url("data:image/svg+xml, %3Csvg%20width=%2212%22%20height=%228%22%20viewBox=%220%200%2012%208%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d=%22M0.500001%200.499999L6%206.5L11.5%200.5%22%20stroke=%22%2385FE99%22%20stroke-width=%221.25%22%2F%3E%0A%3C%2Fsvg%3E%0A"); -} - .reactPlayer + p { margin-top: 1.25rem; } @@ -186,25 +238,31 @@ html[data-theme=dark] .theme-doc-sidebar-menu .menu__link--active + .menu__caret color: var(--ifm-heading-color); } -html[data-theme=dark] [class^=docMainContainer_] .breadcrumbs__link:hover { - --ifm-heading-color: #041834; -} - +/* Separator drawn as a mask so it takes a token colour. --ifm-breadcrumb-separator + is set to none in :root because Infima paints it as a background-image, which + would fight the mask. */ [class^=docMainContainer_] .breadcrumbs__item:not(:last-child):after { opacity: 1; margin-left: 0.1rem; + background-color: var(--mod-color-text-tertiary); + mask-image: var(--docs-chevron); + mask-repeat: no-repeat; + /* contain, not 100%: the separator box is square but the chevron is 8x13, and + stretching it to fill would flatten the glyph. */ + mask-size: contain; + mask-position: center; } +/* The active crumb is a raised chip; both ground and text now invert with the + theme, where the old pairing pinned navy text on a #FFF ground. */ [class^=docMainContainer_] .breadcrumbs__item--active .breadcrumbs__link { - --ifm-heading-color: #041834; - border: 1px solid var(--ifm-color-border); } [class^=docMainContainer_] h1 { - --ifm-heading-font-weight: 300; - --ifm-heading-font-family: "Beausite", sans-serif; - --ifm-h1-font-size: 3.75rem; + --ifm-heading-font-weight: var(--mod-font-weight-semi-bold); + --ifm-heading-font-family: var(--mod-font-family-heading), sans-serif; + --ifm-h1-font-size: 2.25rem; --ifm-leading: 1; margin-bottom: 1.25rem; @@ -236,13 +294,9 @@ html[data-theme=dark] [class^=docMainContainer_] .breadcrumbs__link:hover { margin-bottom: 2.5rem !important; } -html[data-theme='dark'] [class^=docMainContainer_] .card { - --ifm-heading-color: #041834; -} - .markdown { - --ifm-heading-font-family: "Beausite", sans-serif; - --ifm-heading-font-weight: 300; + --ifm-heading-font-family: var(--mod-font-family-heading), sans-serif; + --ifm-heading-font-weight: var(--mod-font-weight-medium); --ifm-link-decoration: underline; } @@ -256,15 +310,13 @@ html[data-theme='dark'] [class^=docMainContainer_] .card { } [class^=docMainContainer_] .pagination-nav { - --ifm-heading-color: #041834; justify-content: center; } [class^=docMainContainer_] .pagination-nav__link { - background: var(--ifm-color-white); + background: var(--mod-color-surface-raised); border-radius: 50px; border: 1px solid var(--ifm-color-border); - backdrop-filter: blur(15px); flex-basis: 50%; } @@ -276,12 +328,12 @@ html[data-theme='dark'] [class^=docMainContainer_] .card { } [class^=docMainContainer_] .pagination-nav__link--prev .pagination-nav__sublabel { - background-image: url("data:image/svg+xml, %3Csvg%20width=%2243%22%20height=%2216%22%20viewBox=%220%200%2043%2016%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d=%22M0.292892%207.2929C-0.0976296%207.68342%20-0.0976295%208.31659%200.292893%208.70711L6.65685%2015.0711C7.04738%2015.4616%207.68054%2015.4616%208.07107%2015.0711C8.46159%2014.6805%208.46159%2014.0474%208.07107%2013.6569L2.41422%208L8.07107%202.34315C8.46159%201.95262%208.46159%201.31946%208.07107%200.928935C7.68054%200.538411%207.04738%200.538411%206.65685%200.928935L0.292892%207.2929ZM43%207L1%207L1%209L43%209L43%207Z%22%20fill=%22%23041834%22%2F%3E%0A%3C%2Fsvg%3E%0A"); - background-repeat: no-repeat; + background-color: var(--mod-color-text-primary); + mask-image: var(--docs-arrow-left); + mask-repeat: no-repeat; width: 42px; height: 16px; flex: 0 0 42px; - } [class^=docMainContainer_] .pagination-nav__link--prev:hover .pagination-nav__sublabel { @@ -294,9 +346,10 @@ html[data-theme='dark'] [class^=docMainContainer_] .card { } [class^=docMainContainer_] .pagination-nav__link--next .pagination-nav__sublabel { -background-image: url("data:image/svg+xml, %3Csvg%20width=%2243%22%20height=%2216%22%20viewBox=%220%200%2043%2016%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d=%22M42.7071%208.70711C43.0976%208.31659%2043.0976%207.68343%2042.7071%207.2929L36.3431%200.928939C35.9526%200.538414%2035.3195%200.538414%2034.9289%200.928938C34.5384%201.31946%2034.5384%201.95263%2034.9289%202.34315L40.5858%208.00001L34.9289%2013.6569C34.5384%2014.0474%2034.5384%2014.6805%2034.9289%2015.0711C35.3195%2015.4616%2035.9526%2015.4616%2036.3431%2015.0711L42.7071%208.70711ZM-1.74846e-07%209L42%209.00001L42%207.00001L1.74846e-07%207L-1.74846e-07%209Z%22%20fill=%22%23041834%22%2F%3E%0A%3C%2Fsvg%3E%0A"); -background-repeat: no-repeat; - background-position: 100% 0; + background-color: var(--mod-color-text-primary); + mask-image: var(--docs-arrow-right); + mask-repeat: no-repeat; + mask-position: 100% 0; width: 100%; height: 16px; flex: 0 0 42px; @@ -342,15 +395,15 @@ background-repeat: no-repeat; .footer .footer__bottom::before { width: 47px; height: 48px; - background-image: url("data:image/svg+xml, %3Csvg%20width=%2247%22%20height=%2248%22%20viewBox=%220%200%2047%2048%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d=%22M46.2205%200.092002C45.777%20-0.0920021%2045.2677%200.00862519%2044.9272%200.350758L24.4951%2020.8816C23.1532%209.14558%2013.2189%200%201.18742%200C0.872678%200%200.572248%200.126503%200.349071%200.350758C0.125895%200.575013%200%200.876894%200%201.19315V23.5899V45.9895C0%2046.0097%200.00858372%2046.0269%200.00858372%2046.047C0.00858372%2046.1045%200.0228899%2046.1592%200.0343349%2046.2167C0.0457799%2046.277%200.0572248%2046.3374%200.0772535%2046.3949C0.082976%2046.4122%200.082976%2046.4294%200.0886985%2046.4467C0.103005%2046.4812%200.128756%2046.507%200.145923%2046.5415C0.174536%2046.5962%200.203148%2046.6508%200.240344%2046.6997C0.274679%2046.7457%200.311875%2046.7859%200.351933%2046.8262C0.39199%2046.8664%200.432047%2046.9067%200.480688%2046.9412C0.52933%2046.9786%200.580832%2047.0073%200.635195%2047.0361C0.66953%2047.0533%200.695282%2047.0792%200.729616%2047.0936C0.743923%2047.0993%200.758229%2047.0964%200.772535%2047.1022C0.901291%2047.1511%201.03863%2047.1856%201.18455%2047.1856H45.7627C46.4179%2047.1856%2046.9501%2046.6508%2046.9501%2045.9924V1.19315C46.9501%200.710141%2046.6611%200.276006%2046.2176%200.092002H46.2205ZM44.5781%204.07109V22.3968H26.3406L44.5781%204.07109ZM44.5781%2033.5951H24.661V24.7802H44.5781V33.5951ZM2.37483%2043.1087V24.7831H20.6124L11.4936%2033.9459L2.37483%2043.1087ZM2.37483%202.41793C13.0673%203.01594%2021.6625%2011.6526%2022.2576%2022.3968H2.37483V2.41793ZM13.1703%2035.6335L22.2891%2026.4707V44.7964H4.05152L13.1703%2035.6335ZM24.6639%2044.7964V35.9814H44.581V44.7964H24.6639Z%22%20fill=%22%23041834%22%2F%3E%0A%3C%2Fsvg%3E%0A"); - background-repeat: no-repeat; + /* The mark is masked, not painted, so it takes the theme's text colour. The + artwork is the existing path verbatim — same glyph, retinted, which is why + the dark-mode duplicate below is no longer needed. */ + background-color: var(--mod-color-text-primary); + mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='47'%20height='48'%20viewBox='0%200%2047%2048'%3E%3Cpath%20d='M46.2205%200.092002C45.777%20-0.0920021%2045.2677%200.00862519%2044.9272%200.350758L24.4951%2020.8816C23.1532%209.14558%2013.2189%200%201.18742%200C0.872678%200%200.572248%200.126503%200.349071%200.350758C0.125895%200.575013%200%200.876894%200%201.19315V23.5899V45.9895C0%2046.0097%200.00858372%2046.0269%200.00858372%2046.047C0.00858372%2046.1045%200.0228899%2046.1592%200.0343349%2046.2167C0.0457799%2046.277%200.0572248%2046.3374%200.0772535%2046.3949C0.082976%2046.4122%200.082976%2046.4294%200.0886985%2046.4467C0.103005%2046.4812%200.128756%2046.507%200.145923%2046.5415C0.174536%2046.5962%200.203148%2046.6508%200.240344%2046.6997C0.274679%2046.7457%200.311875%2046.7859%200.351933%2046.8262C0.39199%2046.8664%200.432047%2046.9067%200.480688%2046.9412C0.52933%2046.9786%200.580832%2047.0073%200.635195%2047.0361C0.66953%2047.0533%200.695282%2047.0792%200.729616%2047.0936C0.743923%2047.0993%200.758229%2047.0964%200.772535%2047.1022C0.901291%2047.1511%201.03863%2047.1856%201.18455%2047.1856H45.7627C46.4179%2047.1856%2046.9501%2046.6508%2046.9501%2045.9924V1.19315C46.9501%200.710141%2046.6611%200.276006%2046.2176%200.092002H46.2205ZM44.5781%204.07109V22.3968H26.3406L44.5781%204.07109ZM44.5781%2033.5951H24.661V24.7802H44.5781V33.5951ZM2.37483%2043.1087V24.7831H20.6124L11.4936%2033.9459L2.37483%2043.1087ZM2.37483%202.41793C13.0673%203.01594%2021.6625%2011.6526%2022.2576%2022.3968H2.37483V2.41793ZM13.1703%2035.6335L22.2891%2026.4707V44.7964H4.05152L13.1703%2035.6335ZM24.6639%2044.7964V35.9814H44.581V44.7964H24.6639Z'%20fill='%23000'%2F%3E%3C%2Fsvg%3E"); + mask-repeat: no-repeat; content: ''; } -html[data-theme='dark'] .footer .footer__bottom::before { - background-image: url("data:image/svg+xml, %3Csvg%20width=%2247%22%20height=%2248%22%20viewBox=%220%200%2047%2048%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d=%22M46.2205%200.092002C45.777%20-0.0920021%2045.2677%200.00862519%2044.9272%200.350758L24.4951%2020.8816C23.1532%209.14558%2013.2189%200%201.18742%200C0.872678%200%200.572248%200.126503%200.349071%200.350758C0.125895%200.575013%200%200.876894%200%201.19315V23.5899V45.9895C0%2046.0097%200.00858372%2046.0269%200.00858372%2046.047C0.00858372%2046.1045%200.0228899%2046.1592%200.0343349%2046.2167C0.0457799%2046.277%200.0572248%2046.3374%200.0772535%2046.3949C0.082976%2046.4122%200.082976%2046.4294%200.0886985%2046.4467C0.103005%2046.4812%200.128756%2046.507%200.145923%2046.5415C0.174536%2046.5962%200.203148%2046.6508%200.240344%2046.6997C0.274679%2046.7457%200.311875%2046.7859%200.351933%2046.8262C0.39199%2046.8664%200.432047%2046.9067%200.480688%2046.9412C0.52933%2046.9786%200.580832%2047.0073%200.635195%2047.0361C0.66953%2047.0533%200.695282%2047.0792%200.729616%2047.0936C0.743923%2047.0993%200.758229%2047.0964%200.772535%2047.1022C0.901291%2047.1511%201.03863%2047.1856%201.18455%2047.1856H45.7627C46.4179%2047.1856%2046.9501%2046.6508%2046.9501%2045.9924V1.19315C46.9501%200.710141%2046.6611%200.276006%2046.2176%200.092002H46.2205ZM44.5781%204.07109V22.3968H26.3406L44.5781%204.07109ZM44.5781%2033.5951H24.661V24.7802H44.5781V33.5951ZM2.37483%2043.1087V24.7831H20.6124L11.4936%2033.9459L2.37483%2043.1087ZM2.37483%202.41793C13.0673%203.01594%2021.6625%2011.6526%2022.2576%2022.3968H2.37483V2.41793ZM13.1703%2035.6335L22.2891%2026.4707V44.7964H4.05152L13.1703%2035.6335ZM24.6639%2044.7964V35.9814H44.581V44.7964H24.6639Z%22%20fill=%22white%22%2F%3E%0A%3C%2Fsvg%3E%0A"); - -} .footer .footer__copyright { font-size: 0.875rem; @@ -437,10 +490,168 @@ html[data-theme='dark'] { padding: 0 calc(var(--ifm-pre-padding) - 3px) 0 var(--ifm-pre-padding); } -/* The announcement bar is pinned, so the sticky navbar has to clear it by exactly the - bar's height. data-bar-visible is maintained in src/theme/AnnouncementBar/index.tsx. */ -@media (min-width: 997px) { - html[data-bar-visible='true'] .navbar--fixed-top { - top: var(--docusaurus-announcement-bar-height); - } +/* --------------------------------------------------------------------------- + Fixed header offsets. + + Navbar/Layout puts the announcement bar, the navbar and the section nav in + one fixed block, so nothing below it is pushed down automatically. These + rules reserve exactly that block's height. Previously a single rule nudged + the sticky navbar clear of the bar; a fixed two-row header needs the whole + stack accounted for instead. + --------------------------------------------------------------------------- */ + +/* Height of the section nav row. The row is desktop-only (see + SecondaryNav.module.css); below the breakpoint it is display:none, so this + value simply does not apply. */ +:root { + --ifm-secondary-nav-height: var(--mod-spacing_6); + + /* First-paint default only: 40px bar + 56px navbar + 48px section nav, i.e. + a desktop viewport with a one-line announcement bar. Navbar/Layout measures + the real header on mount and overwrites this, which is what makes mobile — + where the bar wraps to two or three lines — correct. Being wrong for one + frame beats being wrong permanently. + + A literal rather than a calc over --docusaurus-announcement-bar-height: + that property is `auto` below the breakpoint, and `auto` inside calc() + invalidates the whole expression, taking every offset below with it. */ + --docs-header-height: 144px; +} + +/* Anchor targets land below the header rather than behind it. */ +html { + scroll-padding-top: calc(var(--docs-header-height) + var(--mod-spacing_2)); +} + +.main-wrapper { + margin-top: var(--docs-header-height); + min-height: calc(100vh - var(--docs-header-height)); +} + +.theme-doc-sidebar-container { + position: sticky; + top: var(--docs-header-height); + height: calc(100vh - var(--docs-header-height)); + align-self: flex-start; +} + +/* Section name above the scoped sidebar (the html item added in + DocSidebarItems/filterUtils.ts). */ +.theme-doc-sidebar-menu > li > strong { + display: block; + padding: var(--mod-spacing_1) 0 var(--mod-spacing_1_2); + font-size: var(--mod-font-size-default); + font-weight: var(--mod-font-weight-semi-bold); + color: var(--docs-color-active); +} + +/* Antialiased rendering. Without this, WebKit renders light-on-dark and + dark-on-light text at different apparent weights, so Geist headings look + inconsistent between modes. Grayscale AA normalises it. */ +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Admonitions: rounded, hairline border all round, icon inline with the text. + Replaces Infima's thick coloured left stripe, matching docs.moderne.io. + With a title (`:::info[My title]`) the container stacks so the heading row + sits above the content. */ +.theme-admonition { + border: 1px solid; + border-radius: var(--mod-border-radius-card); + /* Hairline only, no elevation: Infima's .alert shadow reads wrong against + the warm canvas. */ + box-shadow: none; + padding: var(--mod-spacing_2); + display: flex; + flex-direction: row; + gap: var(--mod-spacing_1); + align-items: flex-start; + margin-top: var(--mod-spacing_3); + margin-bottom: var(--mod-spacing_3); +} + +.theme-admonition:has([class*='admonitionTitle_']) { + flex-direction: column; + gap: var(--mod-spacing_1_2); +} + +.theme-admonition [class*='admonitionHeading_'] { + margin: 0; + line-height: 1; + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: var(--mod-spacing_1); + text-transform: none; +} + +.theme-admonition [class*='admonitionHeading_'] [class*='admonitionIcon_'] { + margin-right: 0; + display: inline-flex; + align-items: center; +} + +.theme-admonition [class*='admonitionHeading_'] [class*='admonitionIcon_'] svg { + fill: none; + stroke: currentColor; +} + +.theme-admonition [class*='admonitionTitle_'] { + font-weight: var(--mod-font-weight-semi-bold); + font-size: var(--mod-font-size-h5); + line-height: 1.3; +} + +.theme-admonition [class*='admonitionContent_'] { + flex: 1; + /* Let the content column shrink below its intrinsic width so nested blocks + (code blocks especially) scroll inside the box instead of overflowing. */ + min-width: 0; +} + +.theme-admonition [class*='admonitionContent_'] > p:first-child { + margin-top: 0; +} + +.theme-admonition [class*='admonitionContent_'] > p:last-child { + margin-bottom: 0; +} + +/* Per-type admonition tints. The status tokens are mode-aware, so one rule per + type covers both light and dark. The accent drives border, icon (the icon + inherits currentColor) and heading; body copy stays neutral. Caution shares + the warning tint, Summary shares info — matching docs.moderne.io. */ +.theme-admonition-note { + background-color: var(--mod-color-status-neutral-bg); + border-color: var(--mod-color-text-tertiary); + color: var(--mod-color-text-tertiary); +} +.theme-admonition-tip { + background-color: var(--mod-color-feedback-success-bg); + border-color: var(--mod-color-feedback-success-fg); + color: var(--mod-color-feedback-success-fg); +} +.theme-admonition-info, +.theme-admonition-summary { + background-color: var(--mod-color-feedback-info-bg); + border-color: var(--mod-toast-info-fg); + color: var(--mod-toast-info-fg); +} +.theme-admonition-warning, +.theme-admonition-caution { + background-color: var(--mod-color-feedback-warning-bg); + border-color: var(--mod-color-feedback-warning-fg); + color: var(--mod-color-feedback-warning-fg); +} +.theme-admonition-danger { + background-color: var(--mod-color-feedback-error-bg); + border-color: var(--mod-color-feedback-error-fg); + color: var(--mod-color-feedback-error-fg); +} + +/* Body copy stays neutral; only the heading and icon carry the accent. */ +.theme-admonition [class*='admonitionContent_'] { + color: var(--mod-color-text-primary); } diff --git a/src/theme/AnnouncementBar/index.tsx b/src/theme/AnnouncementBar/index.tsx index badb58985e..b53ffc97a7 100644 --- a/src/theme/AnnouncementBar/index.tsx +++ b/src/theme/AnnouncementBar/index.tsx @@ -12,10 +12,22 @@ import styles from './styles.module.css'; // the bar to everyone. const NOTICE_BAR_STORAGE_KEY = 'code-genome-project-announcement-dismissed'; const NOTICE_BAR_DISMISSED_ATTRIBUTE = 'data-notice-bar-dismissed'; -// Read by custom.css to offset the sticky navbar while a bar is showing. -const BAR_VISIBLE_ATTRIBUTE = 'data-bar-visible'; +/** + * Renders nothing in Docusaurus's default slot. + * + * The header — bar, navbar, section nav — is one fixed block built in + * Navbar/Layout, and the bar is rendered there by AnnouncementBarInline. If + * this also rendered, two bars would mount: they would overlap at the top of + * the page while the default one still occupied its height in the document + * flow, leaving a gap the height of the bar below the header. + */ export default function AnnouncementBar(): JSX.Element | null { + return null; +} + +/** The bar itself, mounted inside the fixed header stack by Navbar/Layout. */ +export function AnnouncementBarInline(): JSX.Element | null { const {pathname} = useLocation(); const {announcementBar} = useThemeConfig(); const {isActive, close} = useAnnouncementBar(); @@ -24,7 +36,6 @@ export default function AnnouncementBar(): JSX.Element | null { // The recipe-catalog bar only belongs on /recipes; elsewhere we show the Code Genome // Project notice. Recipe pages already carry that guidance inline (see RunRecipe). const onRecipes = pathname.startsWith('/recipes'); - const barVisible = onRecipes ? isActive : !noticeClosed; // The server can't read localStorage, so a previous dismissal is picked up after // mount. Until then the head script's attribute keeps the first paint correct. @@ -38,17 +49,6 @@ export default function AnnouncementBar(): JSX.Element | null { } }, []); - // Client-side navigation doesn't re-run the head script, and the two bars live on - // different routes, so the navbar offset has to be re-evaluated on every route change. - useEffect(() => { - const html = document.documentElement; - if (barVisible) { - html.setAttribute(BAR_VISIBLE_ATTRIBUTE, 'true'); - } else { - html.removeAttribute(BAR_VISIBLE_ATTRIBUTE); - } - }, [barVisible]); - const closeNotice = useCallback(() => { try { localStorage.setItem(NOTICE_BAR_STORAGE_KEY, 'true'); diff --git a/src/theme/AnnouncementBar/styles.module.css b/src/theme/AnnouncementBar/styles.module.css index 242bee20e4..25f7a32507 100644 --- a/src/theme/AnnouncementBar/styles.module.css +++ b/src/theme/AnnouncementBar/styles.module.css @@ -83,20 +83,12 @@ html[data-notice-bar-dismissed='true'] .noticeBar { @media (min-width: 997px) { :root { - /* Must equal the navbar offset in custom.css: smaller and the navbar overlaps the - bar, larger and a gap opens. Holds one line of copy; two would need ~48px. */ + /* The bar's own height on desktop, where one line of copy always fits. + Below the breakpoint it stays `auto` and wraps. Nothing derives a layout + offset from this any more — Navbar/Layout measures the rendered header. */ --docusaurus-announcement-bar-height: 40px; } - /* Pin the bar instead of letting it scroll away. Desktop only: below the breakpoint - the height is content-driven, so there's no fixed value for the navbar to clear. */ - .announcementBar, - .noticeBar { - position: sticky; - top: 0; - z-index: calc(var(--ifm-z-index-fixed) + 1); - } - .announcementBarPlaceholder, .announcementBarClose { flex-basis: 50px; diff --git a/src/theme/DocCard/styles.module.css b/src/theme/DocCard/styles.module.css index e9d429925e..9cae81ea09 100644 --- a/src/theme/DocCard/styles.module.css +++ b/src/theme/DocCard/styles.module.css @@ -1,17 +1,21 @@ .cardContainer { - --ifm-link-color: var(--ifm-color-emphasis-800); - --ifm-link-hover-color: var(--ifm-color-emphasis-700); + /* The card title is a link but should read as heading text, not as an action, + so it takes the text tokens rather than the link colour. */ + --ifm-link-color: var(--mod-color-text-primary); + --ifm-link-hover-color: var(--mod-color-text-secondary); --ifm-link-hover-decoration: none; box-shadow: none; - border: 1px solid var(--ifm-color-divider); + border: 1px solid var(--mod-color-border-primary); transition: all var(--ifm-transition-fast) ease; transition-property: border, box-shadow; width: 100%; } +/* Mint hover is the OpenRewrite accent and has no design-system token; it comes + through the --ifm-color-mint brand literal declared in custom.css. */ .cardContainer:hover { - box-shadow: 0px 2px 30px 0px rgba(0, 0, 0, 0.15); + box-shadow: var(--docs-shadow-dropdown); background-color: var(--ifm-color-mint); } @@ -20,15 +24,15 @@ } .cardTitle { - --ifm-heading-margin-bottom: 8px; - font-size: 1.25rem; - font-weight: 600; + --ifm-heading-margin-bottom: var(--mod-spacing_1); + font-size: var(--mod-font-size-h4); + font-weight: var(--mod-font-weight-semi-bold); line-height: 1.666; } .cardDescription { color: var(--ifm-heading-color); - font-size: 1rem; + font-size: var(--mod-font-size-default); display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; diff --git a/src/theme/DocSidebarItems/filterUtils.ts b/src/theme/DocSidebarItems/filterUtils.ts new file mode 100644 index 0000000000..f123a5671f --- /dev/null +++ b/src/theme/DocSidebarItems/filterUtils.ts @@ -0,0 +1,62 @@ +import type { PropSidebarItem, PropSidebarItemCategory } from '@docusaurus/plugin-content-docs'; +import { moreSections, primarySections, type NavSection } from '@site/src/config/navSections'; + +/** Every section the secondary nav can select, with hrefs normalised once. */ +const allSections: NavSection[] = [...primarySections, ...moreSections].map((section) => ({ + ...section, + href: section.href.toLowerCase().replace(/\/$/, ''), +})); + +/** The section the current path belongs to, or null at the root. Shared with + * SecondaryNav so the highlight and the scoped sidebar cannot disagree. No + * section href prefixes another, so the first match is the only match. */ +export function findSection(pathname: string): NavSection | null { + const path = pathname.toLowerCase(); + return allSections.find((section) => path.startsWith(section.href)) ?? null; +} + +/** The top-level category whose generated-index permalink is this section. + * Matched on href rather than label, so renaming a section in sidebars.ts + * cannot silently break the match. */ +function findCategoryForSection( + items: readonly PropSidebarItem[], + section: NavSection, +): PropSidebarItemCategory | null { + return ( + items + .filter((item): item is PropSidebarItemCategory => item.type === 'category') + .find((item) => (item.href ?? '').toLowerCase().replace(/\/$/, '') === section.href) ?? null + ); +} + +/** + * Scope the sidebar to the section the reader is in — the secondary nav already + * lists every section, so repeating them here would show each link twice. + * + * Falls back to the full list where scoping would not help: at the root, and for + * single-document sections, where the scoped view would be empty. + */ +export function filterSidebarItemsBySection( + items: readonly PropSidebarItem[], + pathname: string, +): PropSidebarItem[] { + const section = findSection(pathname); + if (!section) { + return items as PropSidebarItem[]; + } + + const category = findCategoryForSection(items, section); + if (!category?.items?.length) { + return items as PropSidebarItem[]; + } + + // Label comes from the category, not from navSections, so the heading always + // matches what sidebars.ts calls the section. + const header: PropSidebarItem = { + type: 'html', + value: `${category.label}`, + defaultStyle: true, + }; + + return [header, ...category.items]; +} diff --git a/src/theme/DocSidebarItems/index.tsx b/src/theme/DocSidebarItems/index.tsx new file mode 100644 index 0000000000..8198ca20ff --- /dev/null +++ b/src/theme/DocSidebarItems/index.tsx @@ -0,0 +1,41 @@ +import { type FunctionComponent, useMemo } from 'react'; +import DocSidebarItems from '@theme-original/DocSidebarItems'; +import type DocSidebarItemsType from '@theme/DocSidebarItems'; +import type { WrapperProps } from '@docusaurus/types'; +import { useLocation } from '@docusaurus/router'; +import { useWindowSize } from '@docusaurus/theme-common'; +import { filterSidebarItemsBySection } from './filterUtils'; + +type Props = WrapperProps; + +/** + * Scopes the sidebar to the section selected in the secondary nav. + * + * Desktop only: the section nav is hidden below 997px, so scoping there would + * strand a reader in one section with no way out. Both sidebars render through + * this component at level 1, so the viewport is the only thing separating them — + * useWindowSize is what Docusaurus itself uses to pick between them. + * + * Only the top level is filtered; nested items arrive at level 2+ already inside + * the chosen section. + */ +const DocSidebarItemsWrapper: FunctionComponent = (props) => { + const location = useLocation(); + const windowSize = useWindowSize(); + + const isTopLevel = (props.level ?? 0) <= 1; + const shouldScope = isTopLevel && windowSize !== 'mobile'; + + // Memoized: DocSidebarItems is memo()'d on items identity, so a fresh array + // each render would defeat it for every top-level category. + const items = useMemo( + () => (shouldScope ? filterSidebarItemsBySection(props.items, location.pathname) : props.items), + [shouldScope, props.items, location.pathname], + ); + + return ; +}; + +DocSidebarItemsWrapper.displayName = 'DocSidebarItemsWrapper'; + +export default DocSidebarItemsWrapper; diff --git a/src/theme/Navbar/Content/index.tsx b/src/theme/Navbar/Content/index.tsx new file mode 100644 index 0000000000..402d61d11b --- /dev/null +++ b/src/theme/Navbar/Content/index.tsx @@ -0,0 +1,127 @@ +/** + * Navbar content. + * + * Three zones: logo left, search and colour-mode toggle centred, external links + * right. Matches the layout of docs.moderne.io. Section navigation lives in + * (see Navbar/Layout), not here. + */ +import Link from '@docusaurus/Link'; +import { type FunctionComponent, type ReactNode } from 'react'; +import clsx from 'clsx'; +import { useThemeConfig, useWindowSize, ThemeClassNames } from '@docusaurus/theme-common'; +import { splitNavbarItems } from '@docusaurus/theme-common/internal'; +import type { Props as NavbarItemConfig } from '@theme/NavbarItem'; +import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; +import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle'; +import IconExternalLink from '@theme/Icon/ExternalLink'; +import SearchBar from '@theme/SearchBar'; +import NavbarLogo from '@theme/Navbar/Logo'; +import NavbarSearch from '@theme/Navbar/Search'; +import styles from './styles.module.css'; + +/** + * External destinations, desktop only. + * + * Read from themeConfig.navbar.items rather than a second list: the mobile + * drawer renders those same items through Docusaurus's own path, and two + * hand-synced copies would let the two navs disagree silently. + */ +type ExternalItem = { href: string; label: string }; + +/** themeConfig items are a union of many shapes; only plain href+label links + * belong in this row. */ +function isExternalItem(item: NavbarItemConfig): item is NavbarItemConfig & ExternalItem { + const candidate = item as Partial; + return typeof candidate.href === 'string' && typeof candidate.label === 'string'; +} + +const ExternalNavLinks: FunctionComponent<{ items: NavbarItemConfig[] }> = ({ items }) => ( +
+ {items.filter(isExternalItem).map((item) => ( + + {item.label} + + + ))} +
+); + +ExternalNavLinks.displayName = 'ExternalNavLinks'; + +interface NavbarContentLayoutProps { + left: ReactNode; + center: ReactNode; + right: ReactNode; +} + +const NavbarContentLayout: FunctionComponent = ({ left, center, right }) => ( +
+
+ {left} +
+
{center}
+
+ {right} +
+
+); + +NavbarContentLayout.displayName = 'NavbarContentLayout'; + +const NavbarContent: FunctionComponent = () => { + // TODO from upstream theme-classic: temporary casting until ThemeConfig improves. + const items = useThemeConfig().navbar.items as NavbarItemConfig[]; + const [, rightItems] = splitNavbarItems(items); + const windowSize = useWindowSize(); + + // One SearchBar and one toggle, placed differently rather than rendered twice: + // each SearchBar mounts its own Algolia preconnect and window keydown listener, + // so a second instance makes Cmd+K fire two handlers. + const controls = ( + <> + + + + + + ); + const isMobile = windowSize === 'mobile'; + + return ( + + +
+ +
+ + } + center={isMobile ? null : controls} + right={ + <> + + {isMobile &&
{controls}
} + + } + /> + ); +}; + +NavbarContent.displayName = 'NavbarContent'; + +export default NavbarContent; diff --git a/src/theme/Navbar/Content/styles.module.css b/src/theme/Navbar/Content/styles.module.css new file mode 100644 index 0000000000..27a85ff7f6 --- /dev/null +++ b/src/theme/Navbar/Content/styles.module.css @@ -0,0 +1,93 @@ +/** + * Navbar content: three zones, logo left, search centred, external links right. + * + * The centre zone is absolutely positioned on desktop so the search field sits + * on the page's midline rather than the midpoint of whatever space the two side + * zones happen to leave. + */ + +.navbarInner { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + position: relative; +} + +.navbarLeft { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-start; + min-width: 0; +} + +.navbarRight { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--mod-spacing_2); +} + +.navbarCenter { + flex: 0 0 auto; + display: flex; + align-items: center; +} + +@media (min-width: 997px) { + .navbarCenter { + position: absolute; + left: 50%; + transform: translateX(-50%); + gap: var(--mod-spacing_1); + } +} + +.logoGroup { + display: flex; + align-items: center; + gap: var(--mod-spacing_2); +} + +/* Search and toggle move here below the breakpoint, where the absolutely + centred zone has no room. Rendered in one place or the other, never both. */ +.mobileControls { + display: flex; + align-items: center; + gap: var(--mod-spacing_1); +} + +.externalLinks { + display: none; +} + +@media (min-width: 997px) { + .externalLinks { + display: flex; + align-items: center; + gap: var(--mod-spacing_3); + } +} + +.externalLink { + display: flex; + align-items: center; + gap: var(--mod-spacing_1_2); + font-size: var(--mod-font-size-default); + font-weight: var(--mod-font-weight-medium); + color: var(--mod-color-text-primary); + text-decoration: none; + white-space: nowrap; +} + +.externalLink:hover { + color: var(--mod-color-action-default); + text-decoration: none; +} + +.colorModeToggle { + display: flex; + align-items: center; +} diff --git a/src/theme/Navbar/Layout/index.tsx b/src/theme/Navbar/Layout/index.tsx new file mode 100644 index 0000000000..02237a6bb7 --- /dev/null +++ b/src/theme/Navbar/Layout/index.tsx @@ -0,0 +1,110 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * EJECTED from @docusaurus/theme-classic. + * Changes from the original: + * - Wrapped the nav in a fixed .navbarWrapper holding the announcement bar, + * the navbar and the section nav as one block + * - Rendered below the primary navbar (desktop only) + */ +import { type ComponentProps, type FunctionComponent, useEffect, useRef } from 'react'; +import clsx from 'clsx'; +import { ThemeClassNames, useThemeConfig } from '@docusaurus/theme-common'; +import { useHideableNavbar, useNavbarMobileSidebar } from '@docusaurus/theme-common/internal'; +import { translate } from '@docusaurus/Translate'; +import NavbarMobileSidebar from '@theme/Navbar/MobileSidebar'; +import type { Props } from '@theme/Navbar/Layout'; +// Imported by path, not via @theme: the alias resolves to Docusaurus's own +// module declaration, which only knows about the default export. +import { AnnouncementBarInline } from '@site/src/theme/AnnouncementBar'; +import { SecondaryNav } from '@site/src/components/SecondaryNav'; +import { primarySections, moreSections } from '@site/src/config/navSections'; +import styles from './styles.module.css'; + +function NavbarBackdrop(props: ComponentProps<'div'>) { + return ( +
+ ); +} + +export interface NavbarLayoutProps extends Props { + readonly className?: string; +} + +/** + * Publishes the header's real height as --docs-header-height. + * + * The header is position:fixed, so everything below it is offset by that + * variable (see the header offsets in custom.css). Its height is not a constant + * we can write down: the announcement bar has no fixed height on mobile, where + * its copy wraps to two or three lines depending on width and font loading. A + * hardcoded reserve is wrong for exactly the viewports hardest to check, so + * measure instead — this is correct at any width, any copy length, and any + * number of rows. + */ +function useHeaderHeight() { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) { + return; + } + const publish = () => + document.documentElement.style.setProperty('--docs-header-height', `${el.offsetHeight}px`); + + publish(); + const observer = new ResizeObserver(publish); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + return ref; +} + +const NavbarLayout: FunctionComponent = ({ children, className }) => { + const { + navbar: { hideOnScroll, style }, + } = useThemeConfig(); + const mobileSidebar = useNavbarMobileSidebar(); + const { navbarRef, isNavbarVisible } = useHideableNavbar(hideOnScroll); + const wrapperRef = useHeaderHeight(); + + return ( +
+ + + +
+ ); +}; + +NavbarLayout.displayName = 'NavbarLayout'; + +export default NavbarLayout; diff --git a/src/theme/Navbar/Layout/styles.module.css b/src/theme/Navbar/Layout/styles.module.css new file mode 100644 index 0000000000..59856933dc --- /dev/null +++ b/src/theme/Navbar/Layout/styles.module.css @@ -0,0 +1,35 @@ +/** + * Navbar layout. + * + * The whole header — announcement bar, primary navbar, section nav — is one + * fixed block. Making the block fixed rather than each row sticky is what lets + * the rows stack without any per-row `top` arithmetic; content below is pushed + * clear by the offsets in custom.css. + */ + +.navbarWrapper { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: var(--ifm-z-index-fixed); + /* Solid, so content scrolling underneath is fully occluded. The section nav's + bottom border draws the lower edge. */ + background-color: var(--ifm-background-color); +} + +/* Hide-on-scroll animation, from the Docusaurus original. */ +.navbarHideable { + transition: transform var(--ifm-transition-fast) ease; +} + +.navbarHidden { + transform: translate3d(0, calc(-100% - 2px), 0); +} + +@media (min-width: 997px) { + .navbar { + padding-left: var(--mod-spacing_4); + padding-right: var(--mod-spacing_4); + } +} diff --git a/yarn.lock b/yarn.lock index 5c106a5aa0..92443a7097 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2261,6 +2261,11 @@ dependencies: langium "3.3.1" +"@moderneinc/design-system-tokens@7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@moderneinc/design-system-tokens/-/design-system-tokens-7.3.0.tgz#bf20c0a0344724d212cad591c6a250b371584510" + integrity sha512-iapUz2bRivt/cno/amWmRDqABsQXlk5AgL0zLTyd7KbowkMpC/nndjpZMvixBw03SDoPg9HsVBFUNtCTUTRSwA== + "@module-federation/error-codes@0.18.0": version "0.18.0" resolved "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.18.0.tgz" From 73df896f941e698629edb30fcc7ac554291787aa Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 28 Aug 2026 08:56:06 -0700 Subject: [PATCH 02/12] fix(design): space the header row and pin card-hover text on mint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/css/custom.css | 6 +++--- src/theme/DocCard/styles.module.css | 11 ++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/css/custom.css b/src/css/custom.css index 1145696499..e34eff24e4 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -58,9 +58,9 @@ Taller than the 37px docs.moderne.io uses, because this site's chrome is bigger: a 33px logo and a 38px search field, against their smaller lockup. At 37px the search overflowed the bar and the logo sat 2.5px under the - announcement bar. 56px leaves ~9px around the search and ~11px around the - logo. */ - --ifm-navbar-height: var(--mod-spacing_7); + announcement bar. The row centres its contents, so this height sets the + clearance on both sides: ~20px around the logo, ~17px around the search. */ + --ifm-navbar-height: var(--mod-spacing_9); /* System fallbacks in case the webfont fails to load. */ --ifm-font-family-base: var(--mod-font-family-body), system-ui, sans-serif; diff --git a/src/theme/DocCard/styles.module.css b/src/theme/DocCard/styles.module.css index 9cae81ea09..e2bb7464b8 100644 --- a/src/theme/DocCard/styles.module.css +++ b/src/theme/DocCard/styles.module.css @@ -13,10 +13,19 @@ } /* Mint hover is the OpenRewrite accent and has no design-system token; it comes - through the --ifm-color-mint brand literal declared in custom.css. */ + through the --ifm-color-mint brand literal declared in custom.css. + + Text is pinned dark for the same reason as the announcement bar: the mint + ground is identical in both themes, so text that followed the colour mode + would go near-white on green — 1.11:1. Pinned it is 11.96:1. */ .cardContainer:hover { box-shadow: var(--docs-shadow-dropdown); background-color: var(--ifm-color-mint); + + --ifm-link-color: var(--mod-ink-800); + --ifm-link-hover-color: var(--mod-ink-800); + --ifm-heading-color: var(--mod-ink-800); + color: var(--mod-ink-800); } .cardContainer *:last-child { From bb94b1248b1d3c49c43d45dc54d86d4c7fb2f16e Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 1 Sep 2026 09:35:18 -0700 Subject: [PATCH 03/12] feat(nav): rework sections, add a Releases menu, and restyle the landing page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/introduction.md | 4 ++ docusaurus.config.ts | 5 ++ sidebars.ts | 2 +- .../SecondaryNav/SecondaryNav.module.css | 4 +- src/components/SecondaryNav/SecondaryNav.tsx | 22 ++++---- src/config/navSections.ts | 51 ++++++++++++++++--- src/css/custom.css | 30 +++++++++++ src/theme/DocSidebarItems/filterUtils.ts | 6 +-- src/theme/Navbar/Layout/index.tsx | 4 +- src/theme/Root.tsx | 20 ++++++++ 10 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 src/theme/Root.tsx diff --git a/docs/introduction.md b/docs/introduction.md index 5b1dae60b5..8f7445a302 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -3,6 +3,10 @@ title: OpenRewrite by Moderne | Large Scale Automated Refactoring keywords: [OpenRewrite, Moderne, refactoring, Java, Spring Boot Migration, auto-remediation, SAST, SCA] description: Large-scale automated source code refactoring slug: / +# The entry point reads as a landing page rather than a doc, so it shows no +# sidebar. Detaching here rather than hiding the column in CSS also releases the +# width Docusaurus reserves for it. +displayed_sidebar: null --- import ReactPlayer from 'react-player' diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 845da550fb..e6334c03a1 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -241,6 +241,11 @@ const config: Config = { ], }, footer: { + // Reference material rather than a reading path, so it sits here instead + // of in the nav — the same place docs.moderne.io keeps Licensing. + links: [ + { label: "Licensing", to: "/licensing/openrewrite-licensing" }, + ], copyright: `© Moderne, ${new Date().getFullYear()}`, }, prism: { diff --git a/sidebars.ts b/sidebars.ts index 4810305669..92f3dd2565 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -87,6 +87,7 @@ const sidebars: SidebarsConfig = { description: 'How to create and test recipes with OpenRewrite and Moderne.', href: 'https://docs.moderne.io/hands-on-learning/fundamentals/workshop-overview', }, + 'training', ], }, { @@ -132,7 +133,6 @@ const sidebars: SidebarsConfig = { 'reference/building-openrewrite-from-source', ], }, - 'training', { type: 'category', label: 'Lists', diff --git a/src/components/SecondaryNav/SecondaryNav.module.css b/src/components/SecondaryNav/SecondaryNav.module.css index 6c4888ff99..a2f98b58d1 100644 --- a/src/components/SecondaryNav/SecondaryNav.module.css +++ b/src/components/SecondaryNav/SecondaryNav.module.css @@ -48,12 +48,12 @@ color: var(--docs-color-active); } -.more { +.menu { position: relative; flex-shrink: 0; } -.moreButton { +.menuButton { display: flex; align-items: center; gap: var(--mod-spacing_1_2); diff --git a/src/components/SecondaryNav/SecondaryNav.tsx b/src/components/SecondaryNav/SecondaryNav.tsx index b2e246b564..77b974e705 100644 --- a/src/components/SecondaryNav/SecondaryNav.tsx +++ b/src/components/SecondaryNav/SecondaryNav.tsx @@ -10,9 +10,9 @@ export type SecondaryNavProps = { /** Sections rendered as direct links, left-aligned. */ sections: NavSection[]; /** Label for the right-aligned dropdown. */ - moreLabel: string; + menuLabel: string; /** Items inside that dropdown. */ - moreItems: NavSection[]; + menuItems: NavSection[]; }; /** Chevron for the dropdown. Inline rather than an icon package: it is the only @@ -39,17 +39,17 @@ const Chevron: FunctionComponent<{ open: boolean }> = ({ open }) => ( */ export const SecondaryNav: FunctionComponent = ({ sections, - moreLabel, - moreItems, + menuLabel, + menuItems, }) => { const [open, setOpen] = useState(false); - const moreRef = useRef(null); + const menuRef = useRef(null); const location = useLocation(); // Same matcher the sidebar filter uses, so the highlighted section and the // scoped sidebar can never disagree. const activeHref = findSection(location.pathname)?.href; - const isMoreActive = moreItems.some((item) => item.href === activeHref); + const isMenuActive = menuItems.some((item) => item.href === activeHref); const close = useCallback(() => setOpen(false), []); @@ -62,7 +62,7 @@ export const SecondaryNav: FunctionComponent = ({ // DropdownNavbarItem listens on; mousedown alone leaves the menu open on a // touch tap and when focus moves away by keyboard. const onOutside = (event: MouseEvent | TouchEvent | FocusEvent) => { - if (moreRef.current && !moreRef.current.contains(event.target as Node)) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { close(); } }; @@ -101,20 +101,20 @@ export const SecondaryNav: FunctionComponent = ({ })}
-
+
{open && (
- {moreItems.map((item) => ( + {menuItems.map((item) => ( + typeof item === 'object' && 'label' in item && item.label === 'Changelog', + ); + + return (changelog?.items ?? []) + .filter((id): id is string => typeof id === 'string') + .slice(0, RECENT_RELEASE_COUNT) + .map((id) => ({ + // 'changelog/8-91-0-Release' -> '8.91.0' + name: `${id.replace(/^changelog\//, '').replace(/-Release$/i, '').replace(/-/g, '.')} release`, + href: `/${id}`, + })); +} + +/** Right-aligned dropdown, mirroring Releases on docs.moderne.io. */ +export const releaseSections: NavSection[] = [ + ...recentReleases(), + { name: 'All releases', href: '/changelog' }, +]; + +/** Sections the secondary nav can mark active. The release entries are + * individual documents rather than sections, so they are excluded — only + * /changelog itself scopes a sidebar. */ +export const selectableSections: NavSection[] = [ + ...primarySections, { name: 'Changelog', href: '/changelog' }, { name: 'Licensing', href: '/licensing/openrewrite-licensing' }, ]; diff --git a/src/css/custom.css b/src/css/custom.css index e34eff24e4..dff6068ddf 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -619,6 +619,36 @@ body { margin-bottom: 0; } +/* --------------------------------------------------------------------------- + Introduction page (/). + + The entry point reads as a landing page. Its sidebar is removed natively by + `displayed_sidebar: null` in the doc's front matter, which also releases the + width Docusaurus reserves for the column — nothing to do here for that. + + data-route comes from src/theme/Root.tsx. Docusaurus's own docs-doc-id-* + classes exist only in a production build, so they cannot style a page + consistently between `yarn start` and `yarn build`. + --------------------------------------------------------------------------- */ + +/* Green accent in place of the site's blue. No single green clears AA against + both grounds — every one in the palette is light-usable or dark-usable — so + this is a mode-aware pair, the same shape as --docs-color-active. mint-700 is + 5.12:1 on the light ground; the brand mint is 14.54:1 on the dark ground and + 1.11:1 on the light, which is why it appears only in the dark block. */ +html[data-route='/'] { + --docs-intro-accent: var(--mod-mint-700); + --docs-intro-accent-hover: var(--mod-mint-800); + --ifm-link-color: var(--docs-intro-accent); + --ifm-link-hover-color: var(--docs-intro-accent-hover); + --ifm-color-primary: var(--docs-intro-accent); +} + +html[data-route='/'][data-theme='dark'] { + --docs-intro-accent: var(--ifm-color-mint); + --docs-intro-accent-hover: var(--mod-mint-400); +} + /* Per-type admonition tints. The status tokens are mode-aware, so one rule per type covers both light and dark. The accent drives border, icon (the icon inherits currentColor) and heading; body copy stays neutral. Caution shares diff --git a/src/theme/DocSidebarItems/filterUtils.ts b/src/theme/DocSidebarItems/filterUtils.ts index f123a5671f..7ab5dfcac0 100644 --- a/src/theme/DocSidebarItems/filterUtils.ts +++ b/src/theme/DocSidebarItems/filterUtils.ts @@ -1,8 +1,8 @@ import type { PropSidebarItem, PropSidebarItemCategory } from '@docusaurus/plugin-content-docs'; -import { moreSections, primarySections, type NavSection } from '@site/src/config/navSections'; +import { selectableSections, type NavSection } from '@site/src/config/navSections'; -/** Every section the secondary nav can select, with hrefs normalised once. */ -const allSections: NavSection[] = [...primarySections, ...moreSections].map((section) => ({ +/** Every section that can scope a sidebar, with hrefs normalised once. */ +const allSections: NavSection[] = selectableSections.map((section) => ({ ...section, href: section.href.toLowerCase().replace(/\/$/, ''), })); diff --git a/src/theme/Navbar/Layout/index.tsx b/src/theme/Navbar/Layout/index.tsx index 02237a6bb7..d8459ec984 100644 --- a/src/theme/Navbar/Layout/index.tsx +++ b/src/theme/Navbar/Layout/index.tsx @@ -21,7 +21,7 @@ import type { Props } from '@theme/Navbar/Layout'; // module declaration, which only knows about the default export. import { AnnouncementBarInline } from '@site/src/theme/AnnouncementBar'; import { SecondaryNav } from '@site/src/components/SecondaryNav'; -import { primarySections, moreSections } from '@site/src/config/navSections'; +import { primarySections, releaseSections } from '@site/src/config/navSections'; import styles from './styles.module.css'; function NavbarBackdrop(props: ComponentProps<'div'>) { @@ -100,7 +100,7 @@ const NavbarLayout: FunctionComponent = ({ children, classNam - +
); }; diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx new file mode 100644 index 0000000000..fe4bbbb10b --- /dev/null +++ b/src/theme/Root.tsx @@ -0,0 +1,20 @@ +import { type ReactNode, useEffect } from 'react'; +import { useLocation } from '@docusaurus/router'; + +/** + * Publishes the current route on as data-route. + * + * Docusaurus's own per-doc classes (docs-doc-id-*) only appear in a production + * build — the dev server ships a bare — so styling a single + * page through them looks right when deployed and wrong while developing. This + * gives one hook that behaves identically in both. + */ +export default function Root({ children }: { children: ReactNode }): ReactNode { + const { pathname } = useLocation(); + + useEffect(() => { + document.documentElement.setAttribute('data-route', pathname); + }, [pathname]); + + return <>{children}; +} From 942958d9fd5e5c9400b75eb9fb782ba455f24c9b Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 1 Sep 2026 09:45:20 -0700 Subject: [PATCH 04/12] feat(footer): one row of links with social, replacing the stacked default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docusaurus.config.ts | 10 +--- src/css/custom.css | 33 ------------- src/theme/Footer/index.tsx | 78 ++++++++++++++++++++++++++++++ src/theme/Footer/styles.module.css | 77 +++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 41 deletions(-) create mode 100644 src/theme/Footer/index.tsx create mode 100644 src/theme/Footer/styles.module.css diff --git a/docusaurus.config.ts b/docusaurus.config.ts index e6334c03a1..5560972a35 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -240,14 +240,8 @@ const config: Config = { }, ], }, - footer: { - // Reference material rather than a reading path, so it sits here instead - // of in the nav — the same place docs.moderne.io keeps Licensing. - links: [ - { label: "Licensing", to: "/licensing/openrewrite-licensing" }, - ], - copyright: `© Moderne, ${new Date().getFullYear()}`, - }, + // The footer is swizzled (src/theme/Footer), which owns its own links and + // copyright, so there is nothing to configure here. prism: { theme: prismThemes.vsDark, darkTheme: prismThemes.vsDark, diff --git a/src/css/custom.css b/src/css/custom.css index dff6068ddf..edbed01e95 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -375,39 +375,6 @@ figure figcaption { content: none; } -.footer { - display: flex; - justify-content: flex-end; -} - -.footer > .container { - width: auto; - margin: 0; -} - -.footer .footer__bottom { - display: flex; - flex-flow: column; - align-items: center; - gap: 1.25rem; -} - -.footer .footer__bottom::before { - width: 47px; - height: 48px; - /* The mark is masked, not painted, so it takes the theme's text colour. The - artwork is the existing path verbatim — same glyph, retinted, which is why - the dark-mode duplicate below is no longer needed. */ - background-color: var(--mod-color-text-primary); - mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='47'%20height='48'%20viewBox='0%200%2047%2048'%3E%3Cpath%20d='M46.2205%200.092002C45.777%20-0.0920021%2045.2677%200.00862519%2044.9272%200.350758L24.4951%2020.8816C23.1532%209.14558%2013.2189%200%201.18742%200C0.872678%200%200.572248%200.126503%200.349071%200.350758C0.125895%200.575013%200%200.876894%200%201.19315V23.5899V45.9895C0%2046.0097%200.00858372%2046.0269%200.00858372%2046.047C0.00858372%2046.1045%200.0228899%2046.1592%200.0343349%2046.2167C0.0457799%2046.277%200.0572248%2046.3374%200.0772535%2046.3949C0.082976%2046.4122%200.082976%2046.4294%200.0886985%2046.4467C0.103005%2046.4812%200.128756%2046.507%200.145923%2046.5415C0.174536%2046.5962%200.203148%2046.6508%200.240344%2046.6997C0.274679%2046.7457%200.311875%2046.7859%200.351933%2046.8262C0.39199%2046.8664%200.432047%2046.9067%200.480688%2046.9412C0.52933%2046.9786%200.580832%2047.0073%200.635195%2047.0361C0.66953%2047.0533%200.695282%2047.0792%200.729616%2047.0936C0.743923%2047.0993%200.758229%2047.0964%200.772535%2047.1022C0.901291%2047.1511%201.03863%2047.1856%201.18455%2047.1856H45.7627C46.4179%2047.1856%2046.9501%2046.6508%2046.9501%2045.9924V1.19315C46.9501%200.710141%2046.6611%200.276006%2046.2176%200.092002H46.2205ZM44.5781%204.07109V22.3968H26.3406L44.5781%204.07109ZM44.5781%2033.5951H24.661V24.7802H44.5781V33.5951ZM2.37483%2043.1087V24.7831H20.6124L11.4936%2033.9459L2.37483%2043.1087ZM2.37483%202.41793C13.0673%203.01594%2021.6625%2011.6526%2022.2576%2022.3968H2.37483V2.41793ZM13.1703%2035.6335L22.2891%2026.4707V44.7964H4.05152L13.1703%2035.6335ZM24.6639%2044.7964V35.9814H44.581V44.7964H24.6639Z'%20fill='%23000'%2F%3E%3C%2Fsvg%3E"); - mask-repeat: no-repeat; - content: ''; -} - - -.footer .footer__copyright { - font-size: 0.875rem; -} @media(min-width: 997px) { .navbar { diff --git a/src/theme/Footer/index.tsx b/src/theme/Footer/index.tsx new file mode 100644 index 0000000000..dc0870bd74 --- /dev/null +++ b/src/theme/Footer/index.tsx @@ -0,0 +1,78 @@ +import { type FunctionComponent } from 'react'; +import Link from '@docusaurus/Link'; +import styles from './styles.module.css'; + +/** + * Footer. + * + * One row: legal and reference links left, social right, over a hairline. + * Replaces the default Infima footer, which stacked its links above a centred + * copyright — matching the treatment on docs.moderne.io. + * + * Icons are inline rather than from an icon package: three glyphs used once + * each do not justify a dependency, and inline SVG inherits currentColor. + */ + +const XIcon: FunctionComponent = () => ( + +); + +const LinkedInIcon: FunctionComponent = () => ( + +); + +const YouTubeIcon: FunctionComponent = () => ( + +); + +const SOCIAL = [ + { label: 'X', href: 'https://x.com/moderneinc', Icon: XIcon }, + { label: 'LinkedIn', href: 'https://www.linkedin.com/company/moderneinc', Icon: LinkedInIcon }, + { label: 'YouTube', href: 'https://www.youtube.com/@moderneinc', Icon: YouTubeIcon }, +]; + +const LINKS = [ + { label: 'Terms', href: 'https://www.moderne.io/terms-of-service' }, + { label: 'Privacy', href: 'https://www.moderne.io/privacy-policy' }, + { label: 'Contact us', href: 'mailto:support@moderne.io' }, + { label: 'Licensing', to: '/licensing/openrewrite-licensing' }, +]; + +const Footer: FunctionComponent = () => ( +
+
+
+ © Moderne, {new Date().getFullYear()} + {LINKS.map(({ label, href, to }) => + to ? ( + + {label} + + ) : ( + + {label} + + ), + )} +
+ +
+ {SOCIAL.map(({ label, href, Icon }) => ( + + + + ))} +
+
+
+); + +Footer.displayName = 'Footer'; + +export default Footer; diff --git a/src/theme/Footer/styles.module.css b/src/theme/Footer/styles.module.css new file mode 100644 index 0000000000..97f555c09d --- /dev/null +++ b/src/theme/Footer/styles.module.css @@ -0,0 +1,77 @@ +.footer { + background-color: var(--ifm-background-color); + /* Hairline between page content and the footer, matching the one under the + section nav at the top of the page. */ + border-top: 1px solid var(--mod-color-border-primary); + padding: var(--ifm-footer-padding-vertical) var(--mod-spacing_4); +} + +.container { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--mod-spacing_4); + width: 100%; + max-width: 1100px; + margin-inline: auto; +} + +.links { + display: flex; + align-items: center; + gap: var(--mod-spacing_2_1_2); + font-size: var(--mod-font-size-caption); + white-space: nowrap; +} + +/* The copyright is the only non-link here, and reads as metadata rather than + something to act on. */ +.links span { + color: var(--mod-color-text-tertiary); +} + +.links a { + color: var(--mod-color-text-primary); + text-decoration: none; +} + +.links a:hover { + color: var(--mod-color-text-primary); + text-decoration: underline; +} + +.social { + display: flex; + align-items: center; + gap: var(--mod-spacing_2); +} + +.social a { + display: flex; + align-items: center; + color: var(--mod-color-text-primary); + transition: opacity 0.2s ease; +} + +.social a:hover { + opacity: 0.7; +} + +.social svg { + width: 18px; + height: 18px; +} + +/* Stack below the breakpoint: the two groups together need more width than a + phone has, and wrapping them mid-row reads as a broken line. */ +@media (max-width: 768px) { + .container { + flex-direction: column; + gap: var(--mod-spacing_3); + } + + .links { + flex-wrap: wrap; + justify-content: center; + } +} From b8edbb5c53a125b23f44787f90fad2e0b3a9d4f5 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 1 Sep 2026 09:47:52 -0700 Subject: [PATCH 05/12] fix(footer): point YouTube at the Moderne and OpenRewrite channel Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia --- src/theme/Footer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme/Footer/index.tsx b/src/theme/Footer/index.tsx index dc0870bd74..8402c2d358 100644 --- a/src/theme/Footer/index.tsx +++ b/src/theme/Footer/index.tsx @@ -34,7 +34,7 @@ const YouTubeIcon: FunctionComponent = () => ( const SOCIAL = [ { label: 'X', href: 'https://x.com/moderneinc', Icon: XIcon }, { label: 'LinkedIn', href: 'https://www.linkedin.com/company/moderneinc', Icon: LinkedInIcon }, - { label: 'YouTube', href: 'https://www.youtube.com/@moderneinc', Icon: YouTubeIcon }, + { label: 'YouTube', href: 'https://www.youtube.com/@moderne-and-openrewrite', Icon: YouTubeIcon }, ]; const LINKS = [ From de220aa84360869f129a707ac76f953c5ce535c1 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 1 Sep 2026 09:56:06 -0700 Subject: [PATCH 06/12] fix(footer): drop Terms and Privacy Claude-Session: https://claude.ai/code/session_019z4aUYg8gVhnMY8vStEdia --- src/theme/Footer/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/theme/Footer/index.tsx b/src/theme/Footer/index.tsx index 8402c2d358..b0b4e2fc3b 100644 --- a/src/theme/Footer/index.tsx +++ b/src/theme/Footer/index.tsx @@ -38,8 +38,6 @@ const SOCIAL = [ ]; const LINKS = [ - { label: 'Terms', href: 'https://www.moderne.io/terms-of-service' }, - { label: 'Privacy', href: 'https://www.moderne.io/privacy-policy' }, { label: 'Contact us', href: 'mailto:support@moderne.io' }, { label: 'Licensing', to: '/licensing/openrewrite-licensing' }, ]; From 289bebff48e018d06f3a95cf69fd893e787f1504 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 1 Sep 2026 10:03:35 -0700 Subject: [PATCH 07/12] fix(sidebar): drop the duplicated navbar offset above the first item 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 --- src/css/custom.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/css/custom.css b/src/css/custom.css index edbed01e95..445b707ac9 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -502,6 +502,22 @@ html { align-self: flex-start; } +/* theme-classic pulls the sidebar container up by the navbar height and pads + the sidebar back down by the same amount, so the menu clears a navbar the + container sits beneath. The rule above positions the container below the + whole fixed header instead, which makes both halves of that trick dead + weight — and leaves the padding showing as a navbar-height gap above the + first item. */ +.theme-doc-sidebar-container { + margin-top: 0; +} + +/* Matches sidebar_ only: docSidebarContainer_ and sidebarViewport_ both + differ in case or the following character. */ +.theme-doc-sidebar-container [class*='sidebar_'] { + padding-top: 0; +} + /* Section name above the scoped sidebar (the html item added in DocSidebarItems/filterUtils.ts). */ .theme-doc-sidebar-menu > li > strong { From a8fe61e7b8f355a834bd6159e0e30559df8a21b7 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 1 Sep 2026 10:13:04 -0700 Subject: [PATCH 08/12] fix(design): drop the category-page backdrop, breadcrumb pill and paginator buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/css/custom.css | 115 +++-------------------- src/theme/DocPaginator/index.tsx | 42 +++++++++ src/theme/DocPaginator/styles.module.css | 43 +++++++++ 3 files changed, 96 insertions(+), 104 deletions(-) create mode 100644 src/theme/DocPaginator/index.tsx create mode 100644 src/theme/DocPaginator/styles.module.css diff --git a/src/css/custom.css b/src/css/custom.css index 445b707ac9..79c64e8887 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -76,7 +76,9 @@ /* Chevron drawn with mask-image rather than a data-URI background, so the colour comes from a token instead of a hex baked inside the URL. */ --ifm-breadcrumb-separator: none; - --ifm-breadcrumb-item-background-active: var(--mod-color-surface-raised); + /* No ground on the active crumb: breadcrumbs are a position indicator, not a + control, so they read as text. */ + --ifm-breadcrumb-item-background-active: transparent; --ifm-breadcrumb-spacing: 0.875rem; --ifm-menu-color-background-hover: transparent; @@ -101,14 +103,12 @@ --docs-shadow-dropdown: 0 var(--mod-spacing_1_1_2) var(--mod-spacing_3_1_2) var(--mod-color-surface-shadow-neutral); - /* Icon geometry, shared by the breadcrumb separator, the sidebar caret and the - pagination arrows. Used only as masks, so the stroke colour in each URL is - irrelevant — a mask reads alpha, and the colour comes from whatever applies - it. That is the point: a hex inside a data-URI cannot be themed. */ + /* Icon geometry, shared by the breadcrumb separator and the sidebar caret. + Used only as masks, so the stroke colour in each URL is irrelevant — a mask + reads alpha, and the colour comes from whatever applies it. That is the + point: a hex inside a data-URI cannot be themed. */ --docs-chevron: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='8'%20height='13'%20viewBox='0%200%208%2013'%3E%3Cpath%20d='M1%2012L7%206.5L1%201'%20fill='none'%20stroke='%23000'%20stroke-width='1.25'%2F%3E%3C%2Fsvg%3E"); --docs-chevron-down: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='12'%20height='8'%20viewBox='0%200%2012%208'%3E%3Cpath%20d='M0.5%200.5L6%206.5L11.5%200.5'%20fill='none'%20stroke='%23000'%20stroke-width='1.25'%2F%3E%3C%2Fsvg%3E"); - --docs-arrow-left: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='43'%20height='16'%20viewBox='0%200%2043%2016'%3E%3Cpath%20d='M0.292892%207.2929C-0.0976296%207.68342%20-0.0976295%208.31659%200.292893%208.70711L6.65685%2015.0711C7.04738%2015.4616%207.68054%2015.4616%208.07107%2015.0711C8.46159%2014.6805%208.46159%2014.0474%208.07107%2013.6569L2.41422%208L8.07107%202.34315C8.46159%201.95262%208.46159%201.31946%208.07107%200.928935C7.68054%200.538411%207.04738%200.538411%206.65685%200.928935L0.292892%207.2929ZM43%207L1%207L1%209L43%209L43%207Z'%20fill='%23000'%2F%3E%3C%2Fsvg%3E"); - --docs-arrow-right: url("data:image/svg+xml,%3Csvg%20xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width='43'%20height='16'%20viewBox='0%200%2043%2016'%3E%3Cpath%20d='M42.7071%208.70711C43.0976%208.31659%2043.0976%207.68343%2042.7071%207.2929L36.3431%200.928939C35.9526%200.538414%2035.3195%200.538414%2034.9289%200.928938C34.5384%201.31946%2034.5384%201.95263%2034.9289%202.34315L40.5858%208.00001L34.9289%2013.6569C34.5384%2014.0474%2034.5384%2014.6805%2034.9289%2015.0711C35.3195%2015.4616%2035.9526%2015.4616%2036.3431%2015.0711L42.7071%208.70711ZM0%209L42%209.00001L42%207.00001L0%207L0%209Z'%20fill='%23000'%2F%3E%3C%2Fsvg%3E"); } /* Use selector for specificity */ @@ -253,10 +253,11 @@ figure figcaption { mask-position: center; } -/* The active crumb is a raised chip; both ground and text now invert with the - theme, where the old pairing pinned navy text on a #FFF ground. */ +/* The active crumb is the current page: no border, and the muted colour that + marks it as context rather than a link. */ [class^=docMainContainer_] .breadcrumbs__item--active .breadcrumbs__link { - border: 1px solid var(--ifm-color-border); + border: none; + color: var(--mod-color-text-tertiary); } [class^=docMainContainer_] h1 { @@ -270,16 +271,6 @@ figure figcaption { [class^=docMainContainer_] [class^=generatedIndexPage_] { --ifm-spacing-horizontal: 1.625rem; - position: relative; -} - -[class^=docMainContainer_] [class^=generatedIndexPage_]::before { - position: absolute; - background: url(/img/gems-bg.webp) no-repeat 0 0; - width: 1322px; - height: 1070px; - z-index: -1; - content: ''; } [class^=docMainContainer_] [class^=generatedIndexPage_] .row[class*=list_] > .col { @@ -309,71 +300,6 @@ figure figcaption { font-size: 0.875rem; } -[class^=docMainContainer_] .pagination-nav { - justify-content: center; -} - -[class^=docMainContainer_] .pagination-nav__link { - background: var(--mod-color-surface-raised); - border-radius: 50px; - border: 1px solid var(--ifm-color-border); - flex-basis: 50%; -} - -[class^=docMainContainer_] .pagination-nav__link--prev { - padding-inline: 27px 38px; -} -[class^=docMainContainer_] .pagination-nav__link .pagination-nav__sublabel { - transition: transform .25s ease-in-out; -} - -[class^=docMainContainer_] .pagination-nav__link--prev .pagination-nav__sublabel { - background-color: var(--mod-color-text-primary); - mask-image: var(--docs-arrow-left); - mask-repeat: no-repeat; - width: 42px; - height: 16px; - flex: 0 0 42px; -} - -[class^=docMainContainer_] .pagination-nav__link--prev:hover .pagination-nav__sublabel { - transform: translateX(-20%); -} - -[class^=docMainContainer_] .pagination-nav__link--next { - padding-inline: 38px 27px; - text-align: right; -} - -[class^=docMainContainer_] .pagination-nav__link--next .pagination-nav__sublabel { - background-color: var(--mod-color-text-primary); - mask-image: var(--docs-arrow-right); - mask-repeat: no-repeat; - mask-position: 100% 0; - width: 100%; - height: 16px; - flex: 0 0 42px; -} - -[class^=docMainContainer_] .pagination-nav__link--next:hover .pagination-nav__sublabel { - transform: translateX(20%); -} - -[class^=docMainContainer_] .pagination-nav__sublabel { - font-size: 0; - color: transparent; -} -[class^=docMainContainer_] .pagination-nav__label { - font-weight: 500; - color: var(--ifm-heading-color); - line-height: 1.375; - flex-grow: 1; -} - -[class^=docMainContainer_] .pagination-nav__label::before, -[class^=docMainContainer_] .pagination-nav__label::after { - content: none; -} @media(min-width: 997px) { @@ -394,25 +320,6 @@ figure figcaption { --ifm-toc-border-color: var(--ifm-section-divider-color); } - [class^=docMainContainer_] .pagination-nav { - gap: calc(2 * var(--ifm-spacing-horizontal)); - margin-right: var(--ifm-spacing-horizontal); - display: flex; - } - - [class^=docMainContainer_] .pagination-nav__link { - width: 22.625rem; - display: flex; - flex-flow: row; - align-items: center; - gap: 1rem; - height: auto; - } - - [class^=docMainContainer_] .pagination-nav__link--next .pagination-nav__sublabel { - order: 99; - width: 42px; - } } /* Custom styles for desktop (screens wider than 768px) */ diff --git a/src/theme/DocPaginator/index.tsx b/src/theme/DocPaginator/index.tsx new file mode 100644 index 0000000000..d56d3da55a --- /dev/null +++ b/src/theme/DocPaginator/index.tsx @@ -0,0 +1,42 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * EJECTED from @docusaurus/theme-classic. + * Changes from the original: plain text links with an inline arrow, replacing + * the default label/sublabel pair that this site had styled as pill buttons. + */ +import Link from '@docusaurus/Link'; +import { translate } from '@docusaurus/Translate'; +import type { Props } from '@theme/DocPaginator'; +import clsx from 'clsx'; +import { type FunctionComponent } from 'react'; +import styles from './styles.module.css'; + +const DocPaginator: FunctionComponent = ({ previous, next, className }) => ( + +); + +DocPaginator.displayName = 'DocPaginator'; + +export default DocPaginator; diff --git a/src/theme/DocPaginator/styles.module.css b/src/theme/DocPaginator/styles.module.css new file mode 100644 index 0000000000..4f7c3fdcbb --- /dev/null +++ b/src/theme/DocPaginator/styles.module.css @@ -0,0 +1,43 @@ +/** + * Previous/next links: plain text with an inline arrow, no ground and no + * border. They are a way out of the page, not a call to action, so they sit + * below the content in the muted text colour rather than as pill buttons. + */ + +.paginationNav { + display: flex; + justify-content: space-between; + gap: var(--mod-spacing_4); + margin-top: var(--mod-spacing_4); + padding: 0; +} + +.paginationLink { + padding: 0; + background: transparent; + border: none; + font-size: var(--mod-font-size-sm); + font-weight: var(--mod-font-weight-medium); + line-height: 1; + color: var(--mod-color-text-tertiary); + text-decoration: none; + transition: color 0.2s ease; +} + +.paginationLink:hover { + color: var(--mod-color-text-primary); + text-decoration: none; +} + +.prev { + text-align: left; +} + +.next { + text-align: right; +} + +/* Inherits the link colour, including on hover. */ +.arrow { + color: inherit; +} From 27a22b5e0708bd21fecb665e1a9ef566663a1334 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 1 Sep 2026 11:43:35 -0700 Subject: [PATCH 09/12] feat(search): bind DocSearch to design-system tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/css/custom.css | 47 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/css/custom.css b/src/css/custom.css index 79c64e8887..aaa1c431da 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -116,10 +116,53 @@ html.docs-wrapper { --doc-sidebar-width: 370px; } -/* Use selector for specificity */ -html[data-theme=light] { +/* --------------------------------------------------------------------------- + Algolia DocSearch. + + Bound to design-system tokens rather than left on the package defaults, + which are a fixed blue-violet set (rgba(0,0,0,.65) ground, #36395a keys) + that reads as a foreign component beside the warm/ink palette. Matches the + treatment on docs.moderne.io. + + One block, no light/dark pair: every token below is mode-aware, so they + invert on their own. + --------------------------------------------------------------------------- */ +:root { + /* Trigger button in the navbar */ --docsearch-searchbox-background: var(--mod-color-surface-search-input); + --docsearch-subtle-color: var(--mod-color-border-primary); --docsearch-muted-color: var(--mod-color-text-tertiary); + --docsearch-key-background: var(--mod-color-surface-sunken); + --docsearch-key-color: var(--mod-color-text-secondary); + + /* Modal. Themed alongside the button because they share these variables — + leaving it on the defaults would open a differently-coloured product. */ + --docsearch-primary-color: var(--mod-color-action-default); + --docsearch-highlight-color: var(--mod-color-action-default); + --docsearch-text-color: var(--mod-color-text-primary); + --docsearch-modal-background: var(--mod-color-surface-raised); + --docsearch-container-background: var(--mod-color-surface-scrim); + --docsearch-footer-background: var(--mod-color-surface-raised); + --docsearch-hit-background: var(--mod-color-surface-page); + --docsearch-hit-color: var(--mod-color-text-primary); + --docsearch-icon-color: var(--mod-color-text-tertiary); +} + +/* The package hard-codes 4px; the reference rounds to 8px and the site's other + inputs use the same step. */ +.DocSearch-Button { + border-radius: var(--mod-border-radius-s); +} + +.DocSearch-Button:hover { + border-color: var(--mod-color-action-border); + box-shadow: none; +} + +/* Keys sit on the button's own ground, so they need their own edge to read as + keys rather than as a darker patch of it. */ +.DocSearch-Button-Key { + border: 1px solid var(--mod-color-border-primary); } /* For readability concerns, you should choose a lighter palette in dark mode. */ From 778b01ccbe631319f663109dcb2b76b48ee51389 Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 2 Sep 2026 09:41:21 -0700 Subject: [PATCH 10/12] fix(design): bump tokens to 7.4.0, share the chevron, pin the table of 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 --- package.json | 2 +- .../SecondaryNav/SecondaryNav.module.css | 10 ++++++++++ src/components/SecondaryNav/SecondaryNav.tsx | 16 ++++------------ src/css/custom.css | 10 ++++++++++ yarn.lock | 8 ++++---- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 1d526f1b99..5f03570c32 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@docusaurus/theme-mermaid": "^3.9.2", "@mdx-js/react": "^3.1.0", "@mermaid-js/layout-elk": "^0.1.9", - "@moderneinc/design-system-tokens": "7.3.0", + "@moderneinc/design-system-tokens": "7.4.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", diff --git a/src/components/SecondaryNav/SecondaryNav.module.css b/src/components/SecondaryNav/SecondaryNav.module.css index a2f98b58d1..78270663ee 100644 --- a/src/components/SecondaryNav/SecondaryNav.module.css +++ b/src/components/SecondaryNav/SecondaryNav.module.css @@ -63,7 +63,17 @@ cursor: pointer; } +/* One definition of this glyph, shared with the sidebar caret in custom.css. + background-color + mask means it follows currentColor like an inline SVG. */ .chevron { + width: 10px; + height: 6px; + flex-shrink: 0; + background-color: currentColor; + mask-image: var(--docs-chevron-down); + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; transition: transform 0.2s ease; } diff --git a/src/components/SecondaryNav/SecondaryNav.tsx b/src/components/SecondaryNav/SecondaryNav.tsx index 77b974e705..8153544477 100644 --- a/src/components/SecondaryNav/SecondaryNav.tsx +++ b/src/components/SecondaryNav/SecondaryNav.tsx @@ -15,19 +15,11 @@ export type SecondaryNavProps = { menuItems: NavSection[]; }; -/** Chevron for the dropdown. Inline rather than an icon package: it is the only - * glyph this component needs, and it inherits currentColor for free. */ +/** Chevron for the dropdown. Masked from --docs-chevron-down rather than + * inlined, so this glyph has one definition shared with the sidebar caret. The + * mask takes its colour from currentColor, as an inline SVG would. */ const Chevron: FunctionComponent<{ open: boolean }> = ({ open }) => ( - +