docs: add AI Catalog header button and reorder theme selector - #399
Conversation
There was a problem hiding this comment.
🟢 Approval recommended
Changes are small, consistent with the PR description, and the updated navbar config + CSS appear valid and self-contained.
Pull request overview
This PR updates the Mintlify header configuration and styling to introduce an AI Catalog primary CTA button (with custom “ribbon shimmer” styling) while demoting App to a standard navbar link and reordering the theme selector to appear before those links.
Changes:
- Updated
docs.jsonnavbar config: App moved tonavbar.links, AI Catalog set asnavbar.primary. - Added
custom.cssrules to (a) reposition the theme selector relative to the CTA area and (b) fully restyle/animate the AI Catalog button (including dark mode + reduced-motion behavior).
File summaries
| File | Description |
|---|---|
| docs.json | Reconfigures navbar items so AI Catalog becomes the primary button and App becomes a plain link. |
| custom.css | Adds header layout reordering plus AI Catalog button ribbon/shimmer styling with dark-mode and reduced-motion handling. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
👀 Reviewing |
r-marques
left a comment
There was a problem hiding this comment.
🤖 Automated PR review — 🟡 Mergeable with nits
Adds an AI Catalog primary CTA (navbar.primary → https://nevermined.app/catalog/), demotes App to navbar.links[], and appends ~85 lines to custom.css that (a) move Mintlify's theme selector in front of the nav links with order: on #navbar div:has(> nav #topbar-cta-button) > nav and (b) restyle the CTA with a two-stroke Nevermined-mark "ribbon" drawn on ::before/::after masks. The core change is right: docs.json offers no lever for the toggle's position (only appearance.strict to hide it), so CSS is the only way; the selectors match the live production DOM; the reduced-motion, colour and mask work all check out. Three should-fix items, none blocking: the margin compensation at custom.css:340-347 was written against Tailwind v3 space-x semantics while the served bundle is v4 (:where(.space-x-4>:not(:last-child)){margin-inline-end:1rem}), so > div { margin-inline: 0 } is a no-op and the CTA lands 1rem short of the header's right edge; the comment at custom.css:348 links a personal share artifact that says "Expires in 20d 21h" and custom.css is inlined into every page's HTML; and the :focus-visible rule at custom.css:404 is dead — it loses to the !important global ring at lines 250-256 (and should stay lost: #12a89e on white is 2.95:1).
What I reviewed
- Diff range
origin/main...HEAD— 2 files (custom.css,docs.json), +93/−2, head7780019e. - Panel:
code-reviewer,comment-analyzer(read-only; both delivered). Not security-sensitive. jq . docs.jsonvalid; thedocs.jsondiff is an 8-line text edit (no reflow);git diff --checkclean.mintlify broken-links(CLI 4.2.629): success, no broken links.mintlify validateexits 1 on one warning — the pre-existingreactimport insnippets/hero-banner.jsx, identical onmainand unrelated to this PR.mintlify builddoes not exist in this CLI (prints help, exit 0), so "build validation" ≙validate.Mintlify Deploymentcheck: skipped — "No eligible deployments found for changes" — the same on every recent PR here (#394, #395, #397), so there is no preview URL to check.- Live DOM (
https://nevermined.ai/docs/):nav[aria-label=Main] > ul > li#topbar-cta-button > a > (span.absolute + div > span + svg), siblingdiv > button#theme-preference-menu-trigger— the selectors match.#navbar,#topbar-cta-button,#mobile-navare documented Mintlify CSS hooks; the structuraldiv:has(> nav …) > nav,> div,span.absolutechain is scraped, which Mintlify documents as "subject to change". - Live compiled CSS:
.space-x-4is the Tailwind v4 shape (end-margin on every non-last DOM child) — basis for the margin finding. - Link target:
https://nevermined.app/catalog/→ 200;/catalog(no slash) 301s to it, so the trailing slash is canonical. Same prod host as the existing App link. https://share.onorca.dev/a/y4Pbp441fhCj→ an Orca Artifacts share ofbutton-studio.html, page reads "Expires in 20d 21h" (fetched 2026-09-14).- Mintlify bundle:
#mobile-navis the left navigation drawer; the phone-width App/AI Catalog links render in a portalled "Mobile menu" dialog outside both#navbarand#mobile-nav, so that selector arm matches nothing today. - No visual render was done (no preview deployment; the mechanics were verified against the live DOM + bundle instead).
Verdict: 🟡 Mergeable with nits — 0 blockers, 3 should-fix, 4 nits.
🧭 Inline comments (11)
Each is posted on its line in Files changed.
- 🟡 SHOULD FIX — Margin compensation targets Tailwind v3
space-x; the served bundle is v4, so> div { margin-inline: 0 }is a no-op and the CTA sits 1rem short of the right edge —custom.css:347 - 🟡 SHOULD FIX — This share link expires in ~3 weeks and
custom.cssis inlined into every public page — replace it with a durable reference —custom.css:349 - 🟡 SHOULD FIX — Dead rule: the global focus ring at lines 250-256 is
!importantand wins — delete this rather than promoting it —custom.css:407 - 💡 NIT — Href-keyed selector couples byte-for-byte to
docs.jsonnavbar.primary.href; and the#mobile-navarm matches nothing on this theme —custom.css:351 - 💡 NIT — Comment states the effect, not the why, and the block breaks the file's banner convention —
custom.css:339 - 💡 NIT — "Replace" → these rules hide; say what is hidden and what reappears if the scraped class changes —
custom.css:377 - 💡 NIT — A few generator no-ops against the live markup (harmless) —
custom.css:367 - ✅ NICE — Reduced motion is genuinely honoured —
custom.css:418 - ✅ NICE — Per-pseudo
--catalog-ribbonresolves correctly; box matches the viewBox ratio —custom.css:390 - ✅ NICE — Palette and theme handling check out —
custom.css:372 - ✅ NICE — Minimal, valid edit; trailing slash is the canonical form; host matches precedent —
docs.json:750
| #navbar div:has(> nav #topbar-cta-button) > nav { | ||
| order: 1; | ||
| margin-inline-start: 1rem; | ||
| } | ||
|
|
||
| #navbar div:has(> nav #topbar-cta-button) > div { | ||
| margin-inline: 0; | ||
| } |
There was a problem hiding this comment.
🟡 SHOULD FIX — Margin compensation targets Tailwind v3 space-x; the served bundle is v4, so > div { margin-inline: 0 } is a no-op and the CTA sits 1rem short of the right edge
The parent container is flex … justify-end space-x-4. In the CSS Mintlify actually serves (/docs/_next/static/chunks/c9c6061f3cdd5c21.css) that utility is the Tailwind v4 shape:
:where(.space-x-4 > :not(:last-child)) { margin-inline-end: 1rem; }i.e. an END margin on every child except the DOM-last one — not v3's margin-left on every child except the first. :last-child is DOM order, which order: does not change, so after this rule:
nav(DOM-first) keepsmargin-inline-end: 1remand gainsmargin-inline-start: 1rem. Once it is visually last, that end margin becomes a trailing 16px gap: the AI Catalog button's right edge sits 1rem inside the header's right edge, where the theme toggle used to sit flush.- the theme
divis DOM-last and never had a margin, so> div { margin-inline: 0 }(lines 345-347) changes nothing.
Suggested fix — put the whole compensation on the nav and drop the > div rule:
#navbar div:has(> nav #topbar-cta-button) > nav {
order: 1;
margin-inline: 1rem 0; /* Tailwind v4 space-x puts its 1rem on this (DOM-first) child's END; move it to the start now that the nav is visually last */
}Worth a comment either way — as written a maintainer cannot tell which space-x shape the two margins are compensating, or that the > div rule is dead.
🤖 pr-review-toolkit · code-reviewer · high
|
|
||
| /* AI Catalog ribbon shimmer: https://share.onorca.dev/a/y4Pbp441fhCj. |
There was a problem hiding this comment.
🟡 SHOULD FIX — This share link expires in ~3 weeks and custom.css is inlined into every public page — replace it with a durable reference
https://share.onorca.dev/a/y4Pbp441fhCj is an Orca Artifacts share of a local button-studio.html; the page itself reads "Expires in 20d 21h" (fetched 2026-09-14). Mintlify inlines custom.css verbatim into every page's HTML (<style data-custom-css-path="custom.css"> — the #2789 / #2801 refs above are visible in the live page source today), so from early October every docs page will carry a dead link to a personal tool's domain.
The file's house style is a GitHub reference (nevermined-io/nvm-monorepo#2789, #2801). Suggest citing nevermined-io/docs#399 and putting what the share showed into words, which also fixes the second half of this comment losing its antecedent once the link dies. Concretely, the non-obvious facts a maintainer needs are: the two masks are the first two <path>s of logo/light.svg (the two strokes of the Nevermined mark, coordinates shifted ~0.01 — a logo refresh will not know to touch these data URIs otherwise); ::before carries the left stroke and ::after the right so the second can pulse .12s later; and nvm-catalog-shimmer is a fill-colour pulse (accent ↔ lime), not a moving highlight.
🤖 pr-review-toolkit · comment-analyzer · high
| outline: 2px solid var(--catalog-accent); | ||
| outline-offset: 3px; | ||
| } | ||
|
|
There was a problem hiding this comment.
🟡 SHOULD FIX — Dead rule: the global focus ring at lines 250-256 is !important and wins — delete this rather than promoting it
Lines 250-256 declare outline: 2px solid #0f766e !important; outline-offset: 2px !important on every a:focus-visible (and #5eead4 in dark). An !important declaration beats a normal one regardless of specificity, so neither outline nor outline-offset here ever applies: the computed ring on the focused CTA is the global #0f766e / #5eead4 at 2px, not var(--catalog-accent) at 3px.
Delete the rule rather than adding !important: the ring sits outside the button on the navbar background, and #12a89e on the white navbar is 2.95:1 — below WCAG 1.4.11's 3:1 non-text minimum — whereas the global ring it currently loses to is 5.47:1 light / 13.1:1 dark (the file's own numbers at line 245). The hover/focus animation at lines 409-418 is unaffected — nothing !important touches animation, so keyboard focus still triggers the shimmer.
🤖 pr-review-toolkit · code-reviewer · high
|
|
||
| /* AI Catalog ribbon shimmer: https://share.onorca.dev/a/y4Pbp441fhCj. | ||
| Separate masks preserve the staggered animation without custom scripts. */ | ||
| :is(#navbar, #mobile-nav) a[href="https://nevermined.app/catalog/"] { |
There was a problem hiding this comment.
💡 NIT — Href-keyed selector couples byte-for-byte to docs.json navbar.primary.href; and the #mobile-nav arm matches nothing on this theme
Two coupling notes, neither a bug today.
(a) Mintlify renders navbar.primary.href verbatim (live: href="https://nevermined.app"), so this selector must byte-match docs.json:750 — trailing slash included. Drop the slash there, or add a UTM, and the button silently reverts to Mintlify's default pill while the id-keyed theme-order rules above keep working: a half-applied state nobody is told about, and JSON cannot carry a comment pointing back here. Either key the desktop half on the documented hook the reorder rule already relies on — #topbar-cta-button > a — which has no href coupling, or add a one-line comment naming docs.json → navbar.primary.href as the string that must stay in sync.
(b) #mobile-nav is Mintlify's left navigation drawer (DrawerPopup side="left" id="mobile-nav" aria-label="Navigation" — it renders the sidebar). On the mint theme the phone-width App / AI Catalog links render inside a portalled "Mobile menu" dialog (nav[aria-label="Mobile menu"], no id) that is outside both #navbar and #mobile-nav, so that arm of :is(#navbar, #mobile-nav) matches no CTA today and the small-screen link keeps Mintlify's list style — which is fine, but the selector suggests coverage it does not have. Either drop the arm or say the mobile menu deliberately stays unstyled.
🤖 pr-review-toolkit · code-reviewer · high
| mask-image: radial-gradient(120% 140% at 0 0, #000 0%, transparent 55%); | ||
| } | ||
|
|
||
| /* Place the theme selector before the App / AI Catalog navigation. */ |
There was a problem hiding this comment.
💡 NIT — Comment states the effect, not the why, and the block breaks the file's banner convention
Three things the next maintainer needs that the comment does not say:
- Why
order:at all — Mintlify hard-codes the theme menu afternav[aria-label=Main]anddocs.jsonexposes no way to order them (onlyappearance.strictto hide the toggle), so CSS is the only lever. - What silently breaks —
#navbarand#topbar-cta-buttonare documented Mintlify hooks, but thediv > nav > ul > li#topbar-cta-buttonchain between them is scraped from the DOM, which Mintlify documents as "subject to change". When it stops matching, the toggle moves back to the right of the CTA with no error. - Tab order —
order:changes the visual order (theme → App → AI Catalog) but not the DOM/tab order (App → AI Catalog → theme), so keyboard focus travels right across the links and then jumps back left to the toggle. Inherent to the CSS-only approach, but given the focus-ring block at line 240 targets keyboard users explicitly, a one-line acknowledgement of the WCAG 2.4.3 trade-off is worth having.
House style: every other block in this file opens with a /* ===== Section ===== */ banner carrying the rationale and an issue ref (nvm-monorepo#2789, #2801); appended as three one-liners directly under the "Corner engraving hatch" subsection, this reads as part of the Ledger-background section to anyone scanning banners. A Header — theme toggle order + AI Catalog CTA banner with the originating PR would fix that.
🤖 pr-review-toolkit · comment-analyzer · high
| border: 0; | ||
| border-radius: 12px; | ||
| background: #0d3f48; | ||
| color: #fff; | ||
| font-size: 15px; | ||
| font-weight: 600; | ||
| line-height: 1; | ||
| text-decoration: none; | ||
| white-space: nowrap; | ||
| } |
There was a problem hiding this comment.
💡 NIT — A few generator no-ops against the live markup (harmless)
Against the served page these declarations change nothing: border: 0 (preflight already sets border: 0 solid on *), text-decoration: none (preflight a { text-decoration: inherit }, computed is already none), white-space: nowrap (inherited from li.whitespace-nowrap), and color: #fff (the visible text is a <span class="text-white"> child, so the anchor's colour never paints). Harmless and arguably defensive against Mintlify class changes — position: relative / align-items: center are the ones worth keeping for that reason — but if the block is being touched for the items above, trimming these keeps it honest. line-height: 1 does not interact with the pseudo-element centring: top: 50% is measured on the 41px padding box (13 + 15 + 13), which centres the 17px ribbon exactly.
🤖 pr-review-toolkit · code-reviewer · medium
| @media (prefers-reduced-motion: no-preference) { | ||
| :is(#navbar, #mobile-nav) a[href="https://nevermined.app/catalog/"]:is(:hover, :focus-visible)::before, | ||
| :is(#navbar, #mobile-nav) a[href="https://nevermined.app/catalog/"]:is(:hover, :focus-visible)::after { | ||
| animation: nvm-catalog-shimmer .8s ease-in-out infinite; | ||
| } | ||
|
|
||
| :is(#navbar, #mobile-nav) a[href="https://nevermined.app/catalog/"]:is(:hover, :focus-visible)::after { | ||
| animation-delay: .12s; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
✅ NICE — Reduced motion is genuinely honoured
The animation lives only inside @media (prefers-reduced-motion: no-preference), so under reduce no animation is ever applied — nothing to un-set, and the @keyframes outside the query is inert without a reference. Opt-in is the right shape here (an opt-out reduce block would have to remember every animation it cancels). Hover and :focus-visible both start the shimmer, with the .12s stagger on ::after.
🤖 pr-review-toolkit · code-reviewer · high
| :is(#navbar, #mobile-nav) a[href="https://nevermined.app/catalog/"]::before, | ||
| :is(#navbar, #mobile-nav) a[href="https://nevermined.app/catalog/"]::after { | ||
| content: ""; | ||
| position: absolute; | ||
| left: 22px; | ||
| top: 50%; | ||
| transform: translateY(-50%); | ||
| width: 24px; | ||
| height: 17px; | ||
| background-color: var(--catalog-accent); | ||
| mask: var(--catalog-ribbon) center / contain no-repeat; |
There was a problem hiding this comment.
✅ NICE — Per-pseudo --catalog-ribbon resolves correctly; box matches the viewBox ratio
Declaring --catalog-ribbon on the ::before / ::after rules (396, 400) and consuming it in this shared mask: works as intended — custom properties substitute at computed-value time on the pseudo-element itself, independent of rule order — so each pseudo gets its own half-ribbon. The 24×17 box matches the 31.06×22 viewBox aspect ratio (1.412 vs 1.412), so contain neither letterboxes nor crops. -webkit-mask alongside mask covers Safari.
🤖 pr-review-toolkit · code-reviewer · high
| :is(.dark, [data-theme="dark"]) :is(#navbar, #mobile-nav) a[href="https://nevermined.app/catalog/"] { | ||
| --catalog-accent: #4ac6bf; | ||
| --catalog-lime: #bcdc4a; | ||
| } |
There was a problem hiding this comment.
✅ NICE — Palette and theme handling check out
White on #0d3f48 is 11.5:1. Ribbon accents on the button: #12a89e 3.9:1 / #8fbf1f 5.3:1 (light), #4ac6bf 5.6:1 / #bcdc4a 7.4:1 (dark) — all above the 3:1 non-text floor. Keeping #0d3f48 as the background in dark mode is not a regression: docs.json sets colors.dark: "#0d3f48", so today's bg-primary-dark App button already paints exactly that on the dark navbar, and the new tokens reuse the ledger/hero palette already in this file. :is(.dark, [data-theme="dark"]) is functionally identical to the file's .dark X, [data-theme="dark"] X convention (same specificity, both arms match); only the spelling differs.
🤖 pr-review-toolkit · code-reviewer · high
| "links": [ | ||
| { | ||
| "label": "App", | ||
| "href": "https://nevermined.app" | ||
| } | ||
| ], | ||
| "primary": { | ||
| "type": "button", | ||
| "label": "App", | ||
| "href": "https://nevermined.app" | ||
| "label": "AI Catalog", | ||
| "href": "https://nevermined.app/catalog/" |
There was a problem hiding this comment.
✅ NICE — Minimal, valid edit; trailing slash is the canonical form; host matches precedent
Eight-line text edit, no reflow, jq valid. https://nevermined.app/catalog/ answers 200 directly, while /catalog without the slash 301s to it — so the slash saves a redirect hop on every click. Hardcoding the production host is consistent with the existing App link (https://nevermined.app) — the docs site has one production deployment, so there is no staging variant to parameterise for. The :is(#navbar, #mobile-nav) guard in custom.css keeps the page-body links to nevermined.app untouched. Note for the CSS side: the exact string here is what custom.css:351 matches on (see that comment).
🤖 pr-review-toolkit · code-reviewer · high
r-marques
left a comment
There was a problem hiding this comment.
🟡 Approved with comments — no blockers; 3 should-fix, 4 nit(s) left inline. Auto-merge was disabled so these can be looked at first.
Inline review: #399 (review)
|
Addressed the review in 894b8b8:
Browser verification passed against the local preview and the production page with the proposed CSS and header links applied in a temporary browser session: theme → App → AI Catalog, zero trailing gap, ribbon masks, keyboard-focus shimmer/global outline, and no animation under reduced motion. No production content was changed by that check. |
Adds a direct AI Catalog header button linking to https://nevermined.app/catalog/, with the Nevermined ribbon shimmer from the supplied button studio artifact. App becomes a plain link, with the theme selector positioned to its left: theme selector → App → AI Catalog.
The ribbon animates teal to lime on hover and keyboard focus, uses light/dark theme colors, and respects reduced-motion preferences.
Validation: Mintlify build validation passed (with an existing React import diagnostic in
snippets/hero-banner.jsx). Browser checks confirmed header order, Catalog destination and styling, hover animation, reduced-motion behavior, and a clickable theme selector.git diff --checkpassed.