Redesign docs theme and apply sentence case throughout - #378
Conversation
Rebuild the theme around a flat, hairline-ruled visual language driven by Defined Networking's brand palette, and normalize casing across the docs. Design system - Anchor the colour ramps on the exact brand values, so purple 50 is literally #5d22dd and gray 55 is brand slate. Add semantic tokens for a four-tier surface system (background/surface/surface-2/border), text, accent and shape, defined for both themes. - Plus Jakarta Sans for headings, labels and UI; Inter for body copy. - Move the logo, search and colour-mode toggle into the sidebar and drop the top bar on desktop. Below 997px the navbar stays, since it carries the hamburger drawer that is the only navigation on small screens. - Replace DocCard with a flatter card that titles itself from the doc rather than the sidebar label, so shortened nav labels do not leak into the index pages. Casing - Sentence case for headings, frontmatter titles, category labels and navbar/footer items. Case-only heading edits preserve anchor slugs, and the build throws on broken anchors, so the passing build validates every cross-reference. - Normalize prose where the repo already had a dominant convention: lighthouse (was 42 capitalized vs 237 lower) and certificate authority (4 vs 18), skipping fenced code blocks. - Config pills carry literal YAML values, so drop the uppercase transform and correct "Default: False" to "false" in 14 places. Accessibility - Fix 14 WCAG AA contrast failures found by auditing every text node against its effective background. The systemic one was muted text at 3.19-3.49:1; gray-42 is the lightest stop clearing 4.5:1 on all light surfaces. Also code comments and four admonition headings. - Raise mobile touch targets: the drawer close button was 21x21, under the 24x24 minimum in WCAG 2.2 SC 2.5.8. Desktop keeps its tighter rhythm. - Unclamp sidebar labels, which theme-classic truncates to two lines. Other - Re-encode the Open Graph image as JPEG: 990KB to 176KB. It is a photographic starfield, close to worst case for PNG. - Add @types/react and @docusaurus/plugin-content-docs as direct dependencies. Under pnpm's strict layout neither was resolvable for types, which broke typecheck on every swizzled theme component. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying docs-nebula with
|
| Latest commit: |
ba5edec
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://de58b4a5.docs-nebula.pages.dev |
| Branch Preview URL: | https://claude-docs-design-casing-82.docs-nebula.pages.dev |
The TOC column arrived unlabelled, fenced by a continuous left rule that looked identical wherever you were on the page, and its links had no padding, so there was no hover target. - Add an "On this page" heading in a <nav> landmark. Upstream renders a bare list in a plain <div>, so assistive tech had nothing to jump to and sighted readers had nothing naming the column. aria-labelledby points at the visible heading rather than repeating the string in an aria-label. - Drop the continuous rule. The current section is marked by the type going accent-coloured and semibold, leaving the column to just its words. - Move padding onto the links so they have a real hover target, and add a hover state. - Make the wrapper sticky rather than the list, so the heading stays put while a long list scrolls beneath it. Contrast verified in both themes: label 14.6-15.7:1, inactive links 5.4-5.9:1, active 6.8-7.3:1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jasikpark
left a comment
There was a problem hiding this comment.
Reviewed on the Cloudflare preview (b478aee3) at 1280×800 and 375×812, comparing against prod where behaviour changed.
The casing pass is clean: every heading edit is case-only and onBrokenAnchors: 'throw' validated the anchors, the intro.md link-text fix is right, the @docusaurus/plugin-content-docs promotion is justified by the DocCard import, and the OG image rename is complete. The TOC <nav aria-labelledby> landmark and the Prism comment-contrast bump are real accessibility wins.
Three regressions I could reproduce on the preview, each with a line comment:
- Wide tables clip on mobile instead of scrolling (
components.css). - ⌘K opens two DocSearch modals on desktop (a second
SearchBaris mounted in the sidebar). - Non-doc pages (the 404, any future
src/pagesroute) lose all chrome on desktop, since the navbar is hidden globally but the sidebar only exists on doc pages.
Smaller suggestions and nits inline, all labelled.
— Caleb + Cache
| .markdown table { | ||
| display: table; | ||
| width: 100%; | ||
| border-collapse: collapse; | ||
| border: 1px solid var(--border-color); | ||
| border-radius: var(--radius-md); | ||
| overflow: hidden; |
There was a problem hiding this comment.
issue (blocking): display: table + overflow: hidden replaces Infima's table { display: block; overflow: auto }, which is what lets a wide table scroll inside the content column instead of pushing the whole page sideways. On the preview at 375px, /docs/guides/unsafe_routes/ has a document scrollWidth of 509 vs 360 on prod, and the right edge of the first table is clipped.
Suggest keeping display: block; overflow: auto — border-radius still clips a scrolling block's corners, so the rounded border survives.
| @media (min-width: 997px) { | ||
| html:root { | ||
| --ifm-navbar-height: 0px; | ||
| } | ||
|
|
||
| .navbar { | ||
| display: none; | ||
| } |
There was a problem hiding this comment.
issue (blocking): this hides the navbar at ≥997px on every page, but the sidebar that replaces it only exists on doc pages. On the preview, /docs/this-page-does-not-exist/ renders with no logo, no search and no colour-mode toggle — only the footer links. Any future src/pages route hits the same.
Suggest scoping the hide to pages that actually have a sidebar, e.g. #__docusaurus:has(.theme-doc-sidebar-container) .navbar.
| <div className={styles.header}> | ||
| <Logo className={styles.brand} imageClassName={styles.brandImage} titleClassName={styles.brandTitle} /> | ||
| <div className={styles.search}> | ||
| <SearchBar /> |
There was a problem hiding this comment.
issue (blocking): this is a second mounted SearchBar. The navbar's implicit one is still in the DOM (only display: none), and each instance registers its own ⌘K listener. On the preview at 1280px, Meta+K opens two .DocSearch-Modal nodes at once.
Keep exactly one instance mounted — either drop the navbar's (swizzle Navbar/Content to skip the implicit search item) or don't render one here.
| const { label, href, to, ...rest } = item as { label?: string; href?: string; to?: string }; | ||
| if (!label || (!href && !to)) { | ||
| return []; | ||
| } | ||
| return [ | ||
| <Link key={label} className={styles.footerLink} {...(href ? { href } : { to })} {...rest}> |
There was a problem hiding this comment.
suggestion: ...rest forwards every navbar-item config key onto the anchor, so position="right" lands in the DOM as an unknown attribute (visible on the preview's footer link). Pick the ones you want (rel, target, className) explicitly.
| ## stats.message_metrics | ||
|
|
||
| <Pill className="mb-24">Default: False</Pill> | ||
| <Pill className="mb-24">Default: false</Pill> |
There was a problem hiding this comment.
suggestion: two pills further down this file (L67 DEFAULT: nebula, L74 DEFAULT: tcp) still use the all-caps label. With text-transform: uppercase gone from Pill, they now render as literal caps next to sentence-case Default: false everywhere else. The no-transform class on them is dead now too.
| --font-sans: 'Inter', var(--font-fallback); | ||
| /* Plus Jakarta Sans carries headings and labels */ | ||
| --font-display: 'Plus Jakarta Sans', var(--font-sans); | ||
| --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; |
There was a problem hiding this comment.
suggestion: 'JetBrains Mono' heads the stack but is never loaded, so code blocks fall through to ui-monospace. Either add it to the font loading or drop it from the stack so the declared font matches what renders.
| max-height: calc(100vh - 2rem); | ||
| } | ||
|
|
||
| .container > div[class*='tableOfContents'] { |
There was a problem hiding this comment.
suggestion (non-blocking): [class*='tableOfContents'] matches an upstream CSS-module hash, so it will stop matching silently on the next theme-classic bump. Same pattern in components.css ([class*='linkLabel'], [class*='docCardListItem']). Where a stable theme-* class exists, prefer it; where not, a class set from the swizzled component is more robust.
| --sidebar-bg: var(--surface-1); | ||
| --field-bg: var(--surface-2); | ||
|
|
||
| --accent: var(--dn-color-purple-75, hsl(var(--dn-color-purple-hs), 75%)); |
There was a problem hiding this comment.
nitpick (non-blocking): --dn-color-purple-75 doesn't exist (the ramp is 10–90 in tens plus 95), so this always resolves to the fallback. Same for --dn-color-purple-45 in theme.css L10. Either add the stops or write the hsl() directly.
| border-radius: var(--radius-pill); | ||
| padding: 5px 10px; | ||
|
|
||
| &:global(.no-transform), |
There was a problem hiding this comment.
nitpick (non-blocking): with text-transform: uppercase gone, this .no-transform block has nothing to undo (and the selector is duplicated). Safe to delete along with the no-transform usages in stats.mdx.
| * | ||
| * Ejected rather than wrapped because the upstream card renders its icon as a | ||
| * text node inside the heading (`{icon} {title}`), which no wrapper or CSS can | ||
| * remove. The same generic page emoji on every entry carried no information. |
There was a problem hiding this comment.
nitpick (non-blocking): "carried no information" describes the diff rather than the code; it reads fine in the commit message but will look odd to a reader who never saw the emoji. The sentence before it already carries the constraint (icon is a text node in the heading), so this one can go.
All three reported by @jasikpark on #378 and reproduced locally. Wide tables pushed the page sideways on mobile. `display: table` replaced Infima's `display: block; overflow: auto`, which is what keeps a wide table scrolling inside the content column. At a 343px column the table now clamps to the column, scrolls internally, and the document no longer overflows; border-radius still clips the scrolling block, so the rounded border survives. Non-doc pages lost all chrome on desktop. The navbar was hidden at ≥997px everywhere, but the sidebar that replaces it only exists on doc pages, so the 404 rendered with no logo, search or colour-mode toggle. The hide is now scoped to pages that actually have a sidebar. Where :has() is unsupported the rule simply does not apply, leaving the navbar visible — redundant chrome rather than a broken page. Cmd+K opened two DocSearch modals. Hiding the navbar with CSS left its SearchBar mounted, and each instance binds its own listener. The navbar's copy is now suppressed while the rail renders one. Deciding that needed a signal neither component had on its own: the navbar renders outside DocsSidebarProvider, so useDocsSidebar throws there, and the route is no help either — the docs plugin owns /docs/**, so a 404 under that prefix reports the same route context as a real doc, and suppressing on the route left the 404 with no search at all. A small context published from Root carries the fact from the rail, which knows it, to the navbar, which needs it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to #378 review by @jasikpark. Non-blocking items 1-7; the Google Fonts question and the CSS-module hash selectors are left for a separate decision. - Two pills in stats.mdx still read DEFAULT: nebula / DEFAULT: tcp in caps, next to sentence-case Default: false everywhere else. Their no-transform class had nothing left to undo, so drop it and the now-dead rule in Pill.module.css with it. - "Certificate Authority" in the cert v2 guide was missed by the sentence-case pass: the regex required a lowercase character before the match, and this one follows "v2 ". - --dn-color-purple-75 and --dn-color-purple-45 are not stops on the ramp (it runs in tens plus 95), so both always resolved to their fallback. Write the hsl() directly rather than implying a stop exists. - 'JetBrains Mono' headed the mono stack but was never loaded, so code already rendered in ui-monospace. Declare what actually renders. - The sidebar footer links spread the whole navbar item config onto the anchor, putting `position` into the DOM as an invalid attribute. Take only href/to/rel/target. - Drop a DocCard comment that described the diff rather than the code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing renders at 800 — the heaviest weight in the built CSS is 700, and --label-weight is 600 — so the face was downloaded and never used. Staying on Google Fonts for now; self-hosting via @fontsource is a separate call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks Caleb — all three blockers reproduced exactly as described, including your scrollWidth numbers. Fixed in BlockersTables — restored Worth flagging that I'd claimed "no horizontal overflow on mobile" in my own audit. That was wrong — I tested Non-doc pages — took your suggested scoping, Duplicate search — one This one didn't land where I expected, and it's why the diff now carries a small context provider. Both routes I tried first were dead ends:
So the rail publishes its own presence through a context provided at SuggestionsDone: the two all-caps pills and the dead DeferredGoogle Fonts — staying for now, Brian's call. Dropped the unused Hash selectors — agreed on the risk, and that silent failure is the bad part. I'd rather pair it with the pending 3.9.2 → 3.10.2 bump on its own branch, since that's exactly where a break would surface. Happy to do it here instead if you'd prefer it not linger. Not coveredViewport emulation stopped working in my browser partway through, so the table fix is verified by constraining the container rather than at a true 375px. The mechanism is the one that matters and the sub-997px path was confirmed at 980px, but a glance at the preview on 🤖 Generated with Claude Code |
jasikpark
left a comment
There was a problem hiding this comment.
Re-reviewed at ba5edec. The three blocking regressions are resolved: wide tables scroll correctly on mobile, desktop DocSearch mounts only once, and non-doc pages retain their navbar chrome. The follow-up cleanup also addresses the earlier inline suggestions. No further findings—approving.
Rebuilds the docs theme around a flat, hairline-ruled visual language driven by the Defined Networking brand palette, and normalizes casing across all 31 docs.
Design system
#5d22ddand gray 55 is brand slate, so the palette is verifiably the brand's rather than an approximation. Semantic tokens sit on top: a four-tier surface system (background / surface / surface-2 / border), text, accent, and shape, defined for both themes.Casing
lighthouse(was 42 capitalized vs 237 lowercase) andcertificate authority(4 vs 18). Fenced code blocks were skipped.Default: Falseis corrected tofalsein 14 places —FALSEmisrepresented a case-sensitive value.Accessibility
Audited every text node against its effective background and fixed 14 WCAG AA contrast failures:
Also raised mobile touch targets — the drawer close button was 21×21, under the 24×24 minimum in WCAG 2.2 SC 2.5.8. Desktop keeps its tighter rhythm. Sidebar labels are no longer clamped to two lines by theme-classic, which was silently truncating the longest guide titles.
Other
@types/reactand@docusaurus/plugin-content-docs. Under pnpm's strict layout neither was resolvable for types, which brokepnpm typecheckon every swizzled theme component. This pre-dated the branch — typecheck now passes for the first time.Reviewer notes
package.json/ lockfile change is the least expected part of a design PR — see above for why it's needed.src/theme/are swizzled Docusaurus components.DocSidebar/DesktopandDocCardare full ejects; both were unavoidable (the card renders its emoji as a text node inside the heading, which no wrapper or CSS can remove). Each carries a comment explaining why.build,typecheck,format:check, andtestall pass. Both light and dark themes checked across six pages, plus a 375px mobile viewport.🤖 Generated with Claude Code