Skip to content

fix: symmetric article gutters, Mintlify DOM retargeting, and light/dark theme audit - #80

Merged
alexander-sei merged 6 commits into
mainfrom
fix/layout-gutters-and-theme-audit
Sep 16, 2026
Merged

alexander-sei merged 6 commits into
mainfrom
fix/layout-gutters-and-theme-audit

Conversation

@alexander-sei

Copy link
Copy Markdown
Collaborator

Stacked on #79 (docs/sei-mainnet-testnet-naming) because both branches touch evm/index.mdx and several snippets; GitHub retargets this PR to main automatically once #79 merges. Review the two commits separately: style.css first, then the snippet/page theme fixes.

Why

Mintlify renamed the Tailwind classes our layout overrides in style.css were keyed on (lg:pl-[23.7rem]lg:pl-[5.7rem], max-w-[984px]max-w-246, …). Most overrides silently stopped matching and the leftovers produced the layout in the screenshot that started this: a 91 px gutter left of the article but 4 px on the right ("Copy page" touching the TOC), a footer stuck at 984 px left-aligned, and tabs indented 48 px while the logo sat at 16 px.

What changed

Layout (style.css)

  • Overrides now hook only onto Mintlify's stable ids (#sidebar, #content-area, #content-side-layout, #table-of-contents-layout, #navbar, #footer) plus :has(), driven by tokens:
    --sei-page-px: 24px (shared margin for logo, tabs, sidebar text, TOC edge, footer), --sei-sidebar-w: 220px, --sei-gutter: 40px (identical on both sides of the article), --sei-toc-w: 180px, --sei-layout-max: 1536px (wider screens centre the layout instead of stretching prose lines).
  • Measured at 1440: article 260→1196 px, sidebar ends 220, TOC starts 1236. Checked 1024 / 1280 / 1440 / 1920 and mobile in both themes.

Dead rules retargeted or removed (style.css)

  • Callouts key on Mintlify's data-callout-type (the only hook that survives their class renames): info neutral, note blue, tip/check green, warning gold, danger maroon; neutral body copy, colour only on bar + icon.
  • Sidebar group labels (h3.sidebar-title) are the intended mono/uppercase/gold eyebrows; step indicators are maroon.
  • Two sidebar bugs fixed: the whole expanded group was tinted ([data-active="true"] also matches the group <li>), and nested pages had lost their indent (padding-left: 8px !important overrode Mintlify's depth padding).
  • Removed verified-dead selectors (callout-*, mintlify-card, step-number, ml-4 border-l, nav-anchor, sidebar search, --color-primary); title divider is an explicit rule instead of an accidental header match.

Light/dark audit (snippets + pages)

  • Systemic: Mintlify only compiles Tailwind classes written literally inside className (plain strings or template literals, prefixed mint-*). Classes kept in variables or a bare className={cond ? 'a' : 'b'} are left raw and only work by coincidence, so their dark: variants never existed. Inlined in network-tabs, run-snippet, sandbox-embed, add-sei-button (this is why the network tabs showed white text on a light chip in dark mode).
  • Selected <CodeGroup> tabs were maroon-100 on the always-dark code header (1.4:1 in light mode, 14+ pages).
  • Diagram gold/status colours unreadable on white (light-dark() pairs), changelog chips, RPC explorer first-paint theme + sei namespace + faint text, SSTORE inactive tab, sandbox iframes follow the theme, isDark first-paint flashes, caption colour neutral-500neutral-600 in light mode, faucet disabled label, dark search-focus outline.
  • Pages: brand-kit specimens/wells, RPC public badges, Symphony embed (theme= param), OpenZeppelin wizard iframe.

Verification

  • Automated text-contrast scan of all 136 pages × 2 themes: 0 failures below 3:1 remain on touched pages (remaining 3.5–4.4:1 warnings are brand accents in diagrams, gold eyebrows, RPC namespace chips and a disabled button).
  • DOM check for uncompiled Tailwind classes: 0 on every snippet page.
  • Health check (compile/runtime errors, snippet roots rendering, theme applied) passes on all touched pages in both themes.

Not in this PR

Content PNGs with white backgrounds (assets/pointer-contracts-*, interoperability.png, address-derivation.png, brand clearspace/co-branding, ecosystem screenshots) need dark variants exported from the source files.

alexander-sei and others added 2 commits September 15, 2026 16:29
… to Mintlify's current DOM

Mintlify renamed the Tailwind classes our layout overrides keyed on
(lg:pl-[23.7rem] -> lg:pl-[5.7rem], max-w-[984px] -> max-w-246, ...), so
most of them silently stopped matching. The leftovers gave the article a
91px gutter on the left but 4px on the right, pinned the footer to a
984px left-aligned block and indented the tabs 48px while the logo sat at
16px.

Rewrite the layout block around Mintlify's stable ids (#sidebar,
#content-area, #content-side-layout, #table-of-contents-layout, #navbar,
#footer) plus :has(), driven by tokens (--sei-page-px, --sei-sidebar-w,
--sei-gutter, --sei-toc-w, --sei-layout-max). Logo, tabs, sidebar text,
TOC edge and footer now share one 24px page margin, the article gets the
same 40px gutter on both sides, and the whole layout centres above 1536px
instead of stretching prose lines indefinitely.

While retargeting, restore the Sei treatment for components whose
selectors were also dead: callouts (via data-callout-type, with neutral
body copy and gold/maroon/blue/green bars), sidebar group labels
(h3.sidebar-title mono eyebrows) and step indicators. Fix two sidebar
bugs: the whole expanded group was tinted because [data-active="true"]
also matches the group <li>, and nested pages lost their indent because
padding-left: 8px !important overrode Mintlify's depth padding.

Theme fixes: selected <CodeGroup> tabs were maroon-100 on the always-dark
code header (1.4:1 in light mode); the dark search focus outline was
near-invisible. Drop rules that no longer match anything (callout-* and
blockquote variants, mintlify-card/card_, step-number, ml-4 border-l,
nav-anchor, sidebar search, --color-primary overrides) and make the title
divider an explicit rule instead of an accidental `header` match.

Co-authored-by: Cursor <cursoragent@cursor.com>
…k mode

Audit of every snippet and MDX page in both themes (static review plus an
automated text-contrast scan of all 136 pages x 2 themes).

Systemic: Mintlify only compiles Tailwind classes that appear literally
inside a className attribute (plain strings or template literals) and
prefixes them mint-*. Class strings kept in variables or a bare
className={cond ? 'a' : 'b'} are left raw and only work when Mintlify's
own bundle happens to ship the class, so their dark: variants never
existed. Inline those in network-tabs, run-snippet, sandbox-embed and
add-sei-button (this is why the network tabs showed white text on a
light chip in dark mode).

Snippets: gold and status colours in the Giga and nonce-lane diagrams were
unreadable on the light page (light-dark() pairs); changelog chips and
Release badges had no dark background; the RPC explorer assumed dark on
first paint, its sei namespace was 1.5:1 on black and its faint text
2.85:1 on white; SSTORE inactive tab was maroon-100 on black; sandbox
iframes now follow the docs theme; isDark first-paint flashes fixed in
five snippets; caption colour bumped from neutral-500 to neutral-600 in
light mode (was 4.0-4.4:1); faucet disabled label legible on both
surfaces.

Pages: brand kit specimens and wells were hardcoded to light-only colours;
RPC "public" badges now use classes with a dark variant and a
4.5:1-compliant green; Symphony swap embed uses its documented theme=
param per theme; OpenZeppelin wizard iframe drops its forced white
background; #999999 swatch label uses dark text like its neighbours.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Large style.css and snippet changes affect global docs layout and hydration; mis-synced theme seeds would flash wrong colors until CI catches them.

Overview
This PR fixes broken Mintlify layout overrides and tightens light/dark behavior across snippets and MDX.

Layout (style.css) retargets page structure onto stable ids (#sidebar, #content-area, #navbar, #footer, TOC columns) and CSS variables (--sei-page-px, --sei-gutter, --sei-toc-w, --sei-layout-max) so article gutters match on both sides, the footer/nav align with the content column, and wide viewports cap width instead of stretching prose. Callouts, cards, sidebar labels, and TOC styling are updated to use Mintlify-stable hooks (data-callout-type, .sidebar-title, etc.) instead of class names that changed between releases.

Theme and Mintlify Tailwind rules: Docs in AGENTS.md / CONTRIBUTING.md now require literal className strings in snippets (no dynamic class assembly). CI runs scripts/check-snippet-theme-default.mjs so theme-aware snippets seed useState from docs.json appearance.default and sync via useLayoutEffect. Snippets and pages get contrast fixes, diagram CSS tokens, inlined utilities (network-tabs, run-snippet, …), semantic hooks in style.css (sei-network-tab, sei-faucet-verify), and theme-aware embeds (ThemeAwareIframe, sandbox-embed query theme=).

Reviewed by Cursor Bugbot for commit d2a58df. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread learn/general-brand-kit.mdx Outdated
Comment thread snippets/sandbox-embed.jsx

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Large, well-documented CSS/theming refactor that correctly moves Mintlify layout overrides from renamed Tailwind classes onto stable ids/data-attributes and fixes a broad set of real light-mode contrast failures. No blocking correctness or security issues found; the notes below are behavioural regressions in the iframe-theming changes, a few spots where the PR's own "don't key on Mintlify classes" principle isn't followed, and the absence of any durable guard for the systemic className rule it discovers.

Findings: 0 blocking | 17 non-blocking | 9 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion passes: cursor-review.md is empty (that pass produced no output), and REVIEW_GUIDELINES.md is empty, so no repo-specific review standards were applied. Codex's two findings are incorporated below.
  • Hydration-mismatch risk: six snippets (changelog.jsx, ecosystem-app-grid.jsx, ecosystem-contracts.jsx, sip-index.jsx, sandbox-embed.jsx, sstore-gas-live.jsx, rpc-methods-viewer.jsx) now seed useState from document.documentElement.classList. The typeof document guard makes this safe on the server, but if Mintlify SSRs these components the server value (light) will differ from the client value (dark is the docs.json default appearance), which can produce React hydration warnings or markup that stays on the SSR branch until the effect fires. Worth confirming these render client-only before assuming the first-paint flash is gone.
  • The PR's central discovery — Mintlify only compiles Tailwind classes written literally inside a className attribute, so classes held in variables never get their dark: variants — is now load-bearing across network-tabs, run-snippet, sandbox-embed and add-sei-button, but it lives only in two inline code comments. Nothing in CONTRIBUTING.md / AGENTS.md mentions it and no check in .github/workflows/validate-docs.yml enforces it, so the next contributor will reintroduce it. Suggest documenting the rule and adding the DOM scan described in the PR body as a CI step.
  • Verification is described in the PR body (136-page × 2-theme contrast scan, uncompiled-class DOM check, health check) but none of it landed in scripts/ or CI, so none of it is repeatable or regression-proof for a change set that is almost entirely visual.
  • light-dark() (now used ~20 times across giga-diagrams.jsx and nonce-lane-diagrams.jsx) has no fallback. It works because .dark { color-scheme: dark } is set and the default normal resolves to the light value — but where it isn't supported the SVG fill/stroke presentation attribute becomes invalid and falls back to the initial black, which is invisible on the dark default appearance. A preceding plain-colour attribute or a CSS custom property with a fallback would degrade more gracefully.
  • Removing the .prose h1..h4 a[href^="#"] opacity: 0 / hover-reveal rules leaves heading anchors entirely to Mintlify's own behaviour. If Mintlify doesn't hide them, every heading now shows a permanent #. Worth a screenshot check on a heading-dense page.
  • Removing [data-theme="dark"] from the color-scheme rule is safe — data-theme appears nowhere else in the repo.
  • The PR is stacked on #79; confirm GitHub has retargeted the base to main (and that evm/index.mdx merged cleanly) before merging.
  • 9 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread snippets/sandbox-embed.jsx Outdated
) : loaded ? (
<iframe
src={src}
src={themedSrc}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] Driving the iframe src from isDark means toggling the docs theme changes the URL and forces a full iframe navigation, resetting the running CodeSandbox/StackBlitz session and discarding any unsaved edits the reader made. (Codex flagged this too.)

Consider freezing the URL once loaded is true — e.g. capture themedSrc into a ref on the transition to loaded and render that — so the theme only applies to the initial load. The Open link on line 152 can keep using the live themedSrc.

Comment thread evm/in-app-swaps.mdx Outdated

<div style={{ display: 'flex', justifyContent: 'center' }}>
<iframe src="https://symph.ag/embed" style={{ width: '100%', maxWidth: '420px', minWidth: '377px', minHeight: '680px', border: 'none', borderRadius: '12px' }} title="Symphony Swap Widget Demo" />
<iframe className="block dark:hidden" src="https://symph.ag/embed?theme=light" style={{ width: '100%', maxWidth: '420px', minWidth: '377px', minHeight: '680px', border: 'none', borderRadius: '12px' }} title="Symphony Swap Widget Demo (light)" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] hidden/dark:hidden only hides the element — both iframes are in the DOM and both load. That means two full third-party Symphony widget sessions on every page view (doubled network cost and JS), and, as Codex notes, toggling the theme swaps to the other session, so a reader mid-swap loses their selected tokens, amount, and progress.

The original single iframe also had no loading="lazy", so this compounds it. Prefer one iframe whose theme= follows the toggle (the pattern snippets/sandbox-embed.jsx now uses), or at minimum add loading="lazy" to both.

Comment thread snippets/sandbox-embed.jsx Outdated
if (!src) return src;
const theme = isDark ? 'dark' : 'light';
const replaced = src.replace(/([?&#])theme=(dark|light)/gi, `$1theme=${theme}`);
if (/[?&#]theme=/i.test(replaced)) return replaced;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] Two edge cases in the rewrite:

  1. The replace only matches theme=dark|light, but the early-return guard matches any theme=. A src carrying theme=auto / theme=system / anything else falls through to return replaced unchanged, so it silently never follows the docs toggle — the opposite of the intent.
  2. ([?&#])theme= also matches inside the hash fragment, which the comment on line 76 says is left intact. A Remix/StackBlitz #code=… payload containing &theme= would be rewritten.

Parsing with new URL(src) and operating on searchParams (leaving url.hash alone) would handle both and is shorter than the manual ?/&/# joining on lines 82-86.

Comment thread snippets/ecosystem-app-grid.jsx Outdated
<WorldIcon
size={24}
style={{ color: hover ? 'var(--sei-maroon-100)' : '#737373', transition: 'color 0.3s' }}
style={{ color: hover ? 'var(--sei-maroon-100)' : 'var(--sei-grey-200)', transition: 'color 0.3s' }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] --sei-grey-200 is a fixed #666666 (style.css:117) with no .dark override, so this swap is theme-independent and actually lowers dark-mode contrast for the placeholder icon: #737373 on the black page background is ~4.6:1, #666666 is ~3.7:1. In light mode it's an improvement, so an isDark-conditional value (this component already tracks isDark) would get both.

Comment thread style.css Outdated
#navbar [class*="lg:px-12"][class*="mx-4"] {
/* Top row (logo · search · links) and the tabs row both ship with px-12
(48px). Pull them in to the shared page margin. */
#navbar [class*="px-12"] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] This is the exact failure mode the section header above warns about — the selector is still keyed on a Mintlify Tailwind class, so a px-12px-6 rename silently drops the navbar back to a 48px margin while everything else uses --sei-page-px.

It's also broader than the selector it replaces ([class*="lg:px-12"][class*="mx-4"]): unscoped by element and matching any descendant whose class list contains the substring px-12 (including sm:px-12, px-12.5). If no stable id/data hook exists for these rows, worth saying so explicitly in the comment so the next Mintlify bump has a place to look. Same applies to [class*="max-w-8xl"] on line 903.

Comment thread style.css Outdated
/* Mintlify reserves 32px (pr-8) on the right of the sidebar scroll area for a
fade/scrollbar that is hidden here. Reclaim it so labels wrap later and the
dead space between sidebar text and the article shrinks. */
#sidebar-content [class*="pr-8"] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] Same fragility note: [class*="pr-8"] will stop matching on the next Mintlify class rename, and unlike the layout rules above there's no id-based fallback, so the sidebar silently reverts to 32px of dead right padding. Consider a comment noting the intended effect so the regression is diagnosable.

Comment thread snippets/add-sei-button.jsx Outdated
// not in a bare `className={cond ? 'a' : 'b'}` expression.
<span
className={status.type === 'error' ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}
className={`${status.type === 'error' ? 'text-red-600 dark:text-red-400' : 'text-green-700 dark:text-green-400'}`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] The template-literal wrapper is a no-op at runtime, so this fix rests entirely on the claim in the comment about how Mintlify's class extractor treats `${cond ? 'a' : 'b'}` differently from cond ? 'a' : 'b' — both contain the same literal strings inside the attribute. If that distinction turns out not to be the actual rule, this change does nothing and the bug stays.

snippets/network-tabs.jsx took the more robust route of writing both branches out in full. Either match that here, or link the DOM-check evidence for the template-literal behaviour in the comment.

Comment thread evm/evm-wizard.mdx Outdated
<iframe
src="https://wizard.openzeppelin.com/embed"
style={{ width: "100%", minHeight: "700px", border: "0", background: "white" }}
style={{ width: "100%", minHeight: "700px", border: "0" }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] Dropping background: white only helps if the OpenZeppelin embed paints its own background; if its document background is transparent, the wizard's dark text will now render directly on the docs page background and be unreadable in dark mode. The embed accepts a theme query param, so …/embed?theme=dark (or a toggle-driven value) would be a more deterministic fix than relying on the third party's default. Worth a screenshot in both themes.

Comment thread learn/general-brand-kit.mdx Outdated
---

<div style={{ fontFamily: 'var(--sei-font-mono, "ABC Repro Mono")', fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--sei-gold-100, #966f22)', marginTop: '2.5rem', marginBottom: '0.5rem' }}>
<div className="sei-eyebrow" style={{ marginTop: '2.5rem', marginBottom: '0.5rem' }}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] .sei-eyebrow (style.css:236) is font-size: 10px; letter-spacing: 0.04em, but the inline styles being replaced here were 11px / 0.08em. The consolidation is the right call, but it does change how these four section eyebrows render — flagging in case the tighter tracking wasn't intended.

Co-authored-by: Cursor <cursoragent@cursor.com>

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A large, well-documented CSS/theme repair: layout overrides are re-keyed from Mintlify's renamed Tailwind classes onto stable ids plus tokens, dynamic className expressions are inlined so their dark: variants actually compile, and a CI check plus AGENTS.md/CONTRIBUTING.md guidance are added to keep the snippet convention enforced. I found no correctness or security blockers — the remaining notes are a description/diff mismatch on the OpenZeppelin wizard page, gaps in the new checker's heuristics and file coverage, and a few unverifiable-without-Mintlify claims.

Findings: 0 blocking | 12 non-blocking | 6 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • PR description overstates coverage: it lists the "OpenZeppelin wizard iframe" as fixed, but evm/evm-wizard.mdx is not in the diff and still renders a raw <iframe src="https://wizard.openzeppelin.com/embed" style={{ ... background: "white" }}>. That is the second-best candidate for the new ThemeAwareIframe and still forces a white panel on a black page. Either convert it or drop the claim from the description.
  • The whole layout rewrite (and the callout/sidebar/TOC/step retargeting) now depends on Mintlify internals — #sidebar, #content-area, #content-side-layout, #table-of-contents-layout, #navbar, #navbar-transition, #search-bar-entry, .nav-tabs, .card, .callout[data-callout-type], [data-component-part="callout-icon"], .sidebar-title, .toc-item, .code-group/.codeblock-dark, [class*="step-indicator"] — yet nothing in CI asserts those hooks still exist. Ids are more stable than arbitrary-value Tailwind classes, so this is a real improvement, but the next Mintlify rename will fail exactly as silently as the one this PR is fixing. The new check-snippet-classnames.mjs guards a different failure mode. Consider a smoke check that fetches a built page and asserts the expected hooks are present.
  • Seeding isDark/theme to the dark default (changelog, ecosystem-app-grid, ecosystem-contracts, sip-index, sstore-gas-live, sandbox-embed) removes the hydration mismatch and matches docs.json appearance.default: "dark", but it moves the first-paint flash onto light-preference readers across every data widget at once. Worth confirming on a light-preference profile that the flip lands within the same frame as Mintlify's own theme application; otherwise a light-dark()/CSS-driven variant would avoid the flash entirely.
  • snippets/sandbox-embed.jsx now sets ?theme= on Remix URLs (https://remix.ethereum.org/?#activate=...&code=<base64>). URL.toString() correctly leaves the hash payload intact, but Remix historically reads its theme from the fragment and has used capitalised values (Dark/Light). If the query form isn't honoured this is a silent no-op rather than a break — but it's the one theme parameter in this PR that isn't a documented embed option, alongside https://symph.ag/embed?theme=. Please confirm both against the live embeds.
  • Cursor's second-opinion pass (cursor-review.md) is empty — that review produced no output, so its perspective is missing from this synthesis. Codex reported no material issues, noting it could not verify Mintlify runtime/visual behaviour because the CLI is unavailable; the same limitation applies here, so the layout/contrast numbers in the description are taken on trust.
  • REVIEW_GUIDELINES.md is empty on the base branch, so no repo-specific standards were applied.
  • 6 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread scripts/check-snippet-classnames.mjs Outdated
const expression = match[1].trim();
const line = code.slice(0, match.index).split('\n').length;
const forwardedProp = /^[A-Za-z_$][\w$]*$/.test(expression)
&& !new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*=`).test(code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] Two gaps in the forwarded-prop heuristic:

  1. It only rejects identifiers declared with const|let|var <name> =. A class string reached any other way passes silently — const { cardClass } = STYLES; ... className={cardClass}, a function parameter holding a utility list, or an import. That is precisely the pattern this PR spent most of its diff removing (cardClass, labelClass, tabButtonClass), so the guard would not catch a regression written slightly differently.
  2. expression is interpolated into new RegExp(...) unescaped. Since the identifier pattern allows $, a variable named e.g. cls$ produces \b(?:const|let|var)\s+cls$\s*=, where $ anchors to end-of-string and the test can never match — so the declared variable is classified as a forwarded prop and waved through.

Escaping the interpolation, and additionally scanning for className identifiers that appear anywhere on the left of a = or inside a destructuring pattern that isn't the component's own props, would close both.

Comment thread scripts/check-snippet-classnames.mjs Outdated
import { fileURLToPath } from 'node:url';

const snippetsDir = fileURLToPath(new URL('../snippets/', import.meta.url));
const files = (await readdir(snippetsDir)).filter((file) => file.endsWith('.jsx')).sort();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] readdir is non-recursive and the filter is .jsx-only, so the check covers exactly the current flat snippets/*.jsx layout and nothing else.

More relevant to this PR: it doesn't look at .mdx at all, and this PR newly introduces Tailwind utility lists into MDX — evm/index.mdx:89 (bg-green-500/15 text-green-800 dark:text-green-400) and learn/general-brand-kit.mdx (bg-[var(--sei-card-bg-light)], text-[#111] dark:text-[var(--sei-grey-25)], …). Those pages are now subject to the same extractor constraint the script exists to enforce, but CI won't flag a dynamic className there. Consider { recursive: true } plus an .mdx pass.

Comment thread style.css
border-bottom-color: var(--sei-cream);
}

/* Heading anchor links — Mintlify handles the hover reveal; just make sure the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] This drops the opacity: 0 / :hover { opacity: 0.5 } pair and keeps only border-bottom: none, on the assumption that Mintlify reveals heading anchors itself. If it doesn't, a # is now permanently visible next to every h1h4 on all 136 pages — a broad, purely cosmetic regression that the text-contrast scan described in the PR body would not detect.

Worth an explicit visual confirmation on a page with several headings in both themes; if Mintlify does handle it, saying so with the release/build you checked against would help the next person.

Comment thread snippets/changelog.jsx
<div className="flex items-center gap-3 mb-4">
<h2 className="text-xl font-bold text-gray-900 dark:text-white m-0">{version.version}</h2>
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">Release</span>
<span className="text-xs text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-neutral-800 px-2 py-1 rounded-full">Release</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] This pass moved sibling captions from text-neutral-500text-neutral-600, but the "Release" chip keeps text-gray-500 (#6b7280) on bg-gray-100 (#f3f4f6) — roughly 4.3:1 at text-xs/12px, just under the 4.5:1 AA threshold for normal-weight small text. Above the PR's 3:1 bar, so it's consistent with the stated scope, but text-gray-600 here (and at line 980) would close it for the same one-token change applied everywhere else.

// <SandboxEmbed
// kind="codesandbox"
// src="https://codesandbox.io/embed/<id>?view=split&hidenavigation=1&theme=dark"
// src="https://codesandbox.io/embed/<id>?view=split&hidenavigation=1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] The doc comment drops &theme=dark from the CodeSandbox example, but the two call sites still hardcode it: evm/evm-parity/examples/viem-quickstart.mdx:100 and evm/evm-parity/examples/ethers-quickstart.mdx:81. searchParams.set overwrites it, so behaviour is correct either way — just worth stripping the now-misleading parameter from those pages so the comment and the real usage agree.

Comment thread evm/index.mdx
| `https://sei-evm-rpc.publicnode.com` | PublicNode | <span style={{ backgroundColor: 'rgba(34,197,94,0.15)', color: '#16a34a', padding: '0.125rem 0.5rem', borderRadius: '9999px', fontSize: '0.75rem', fontWeight: 600 }}>public</span> | 5 req/s | Community maintained |
| `https://seievm-rpc.polkachu.com` | Polkachu | <span style={{ backgroundColor: 'rgba(34,197,94,0.15)', color: '#16a34a', padding: '0.125rem 0.5rem', borderRadius: '9999px', fontSize: '0.75rem', fontWeight: 600 }}>public</span> | — | Community maintained |
| `https://jsonrpc.lavenderfive.com:443/sei` | LavenderFive | <span style={{ backgroundColor: 'rgba(34,197,94,0.15)', color: '#16a34a', padding: '0.125rem 0.5rem', borderRadius: '9999px', fontSize: '0.75rem', fontWeight: 600 }}>public</span> | — | Community maintained |
| `https://evm-rpc.sei-apis.com` | Sei Foundation | <span className="rounded-full px-2 py-0.5 text-xs font-semibold bg-green-500/15 text-green-800 dark:text-green-400">public</span> | 10 req/s | Recommended for development |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] These badges move from inline styles (which render unconditionally) to Tailwind utilities, which is the opposite direction from this PR's central thesis that Mintlify's extractor is the fragile part. In MDX the classes are literal so extraction should work, and the dark-mode fix is a genuine improvement — just noting the trade: if the extractor ever misses MDX table cells the badges lose all styling (pill, padding, size), not just their colour, whereas the previous form could only be wrong about colour. Since the fix needed is only color, keeping the inline style and adding a dark: hook via a small semantic class in style.css would be strictly safer.

Base automatically changed from docs/sei-mainnet-testnet-naming to main September 16, 2026 09:54
Both sides of snippets/network-tabs.jsx changed the same label rows: main
(#79) split "Chain ID" into distinct EVM and Cosmos chain ID entries using
the labelClass/valueClass variables, while this branch removed those
variables because Mintlify only compiles Tailwind classes written literally
inside className. Kept main's labels and the new Cosmos chain ID rows with
the utility lists written out as literals.

snippets/sandbox-embed.jsx: kept main's "Sei Testnet" capitalisation in the
usage comment without re-adding the &theme=dark parameter this branch drops,
since SandboxEmbed now sets theme from the docs toggle.

snippets/nonce-lane-diagrams.jsx: the throughput charts #78 added after this
branch forked hardcoded the ceiling line to #f59e0b, the dark value of
--sei-diagram-warning (2.1:1 on the light page). Routed them through the
token like the other diagrams so dark mode is unchanged and light mode uses

Co-authored-by: Cursor <cursoragent@cursor.com>
#b45309.
@mintlify

mintlify Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
sei-docs 🟢 Ready View Preview Sep 16, 2026, 12:15 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A large but well-reasoned CSS/theming refactor: the layout overrides are re-keyed onto Mintlify's stable ids and tokenised, and the snippet audit correctly removes dynamically-assembled className strings in favour of literals, data-attribute hooks, and CSS custom properties. I found no correctness blockers — the notable points are a theme= query param that probably doesn't reach the Remix embeds, a gap in the new CI guard, and the dark-default assumption now duplicated across six snippets.

Findings: 0 blocking | 11 non-blocking | 5 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • cursor-review.md is empty — the Cursor pass produced no output, so only Codex ("no material issues found") and this review contributed second opinions.
  • No regression coverage for the thing that broke in the first place: the PR's whole premise is that Mintlify silently renames classes, but the new CI gate only checks the shape of snippet className attributes. A small DOM smoke check against a preview build (assert #content-area, #table-of-contents-layout, [data-callout-type], [data-component-part="scroll-area-viewport"], #search-bar-entry and .nav-tabs all exist and match ≥1 node) would surface the next rename at PR time rather than in production.
  • style.css now depends on several Mintlify-internal hooks (data-component-part, #search-bar-entry, .nav-tabs, #navbar-transition). The layout comment block documents the DOM shape but not which Mintlify release it was read from — worth recording so the next reader knows what to re-verify against.
  • snippets/network-tabs.jsx:83 still uses a lazy useState initializer that reads window.location.hash. That's the same SSR/hydration-determinism pattern commit 3 deliberately removed from every isDark state; it'd be good to make the two consistent (either both are safe here, or neither is).
  • The PR description's claim that "isDark first-paint flashes" are fixed in five snippets no longer matches the shipped code: commit 3 replaced the lazy initializers with a hard-coded true, which inverts the flash (light-preference readers now flash dark) rather than removing it. Worth editing the body before merge so the changelog stays accurate.
  • Verified as fine, for the record: the new .sei-network-tab / .sei-faucet-verify semantic hooks follow the already-working .sei-eco-grid precedent, so non-utility class names are not mint-*-prefixed; useState/useEffect without imports matches every other snippet; all CSS variables referenced by the new rules (--sei-grey-600, --sei-card-divider-dark, --sei-diagram-*) are defined; changelog.jsx's renderLink signature change is applied consistently at all five call sites; and docs.json does set appearance.default: "dark". I also found no prompt-injection attempts in the diff, commit messages, or PR body.
  • 5 suggestion(s)/nit(s) flagged inline on specific lines.

if (!src) return src;
try {
const url = new URL(src);
url.searchParams.set('theme', isDark ? 'dark' : 'light');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] searchParams.set puts theme in the query string, but 8 of the 10 SandboxEmbed call sites are Remix IDE URLs of the form https://remix.ethereum.org/?#activate=solidity,fileManager&code=…, and Remix reads its URL parameters from the hash fragment. So this produces https://remix.ethereum.org/?theme=dark#activate=… and those embeds most likely keep their own default theme regardless of the docs toggle — only the two CodeSandbox embeds actually follow it.

The hash payload does survive the URL round-trip intact (the base64 code= contains no characters the URL parser re-encodes), so nothing is broken — the feature just may not take effect where it's used most. Worth confirming against Remix; if it needs to be in the fragment, this has to merge into the existing hash params instead of the query.

Comment thread scripts/check-snippet-classnames.mjs Outdated
for (const match of code.matchAll(/className\s*=\s*\{([\s\S]*?)\}/g)) {
const expression = match[1].trim();
const line = code.slice(0, match.index).split('\n').length;
const forwardedProp = /^[A-Za-z_$][\w$]*$/.test(expression)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The forwarded-prop escape hatch is keyed on "not declared with const/let/var anywhere in the file", which lets through the exact pattern the rule exists to ban whenever the identifier comes from a function parameter or a destructuring pattern. For example:

const chip = (x, y, cls) => <div className={cls} />;   // passes the gate
const { wrapper } = STYLES;                            // passes the gate

Both are dynamic class lists Mintlify can't extract, but neither matches const wrapper = / const cls =. Conversely a genuinely safe className={'a b'} or className={`a b`} is rejected with a message that implies it's unsafe.

It happens to pass today (all 16 remaining className={…} sites are literal className={className} forwards and no snippet declares const className), so this isn't blocking. But since this now gates CI, consider tightening it: allow only the exact identifier className when it appears in the component's parameter destructuring, and allow static string/template literals with no ${} explicitly.

Also worth scoping: only snippets/*.jsx is checked, while MDX pages can contain the same dynamic className patterns.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in c9be3de. The validator now permits only static string/template literals or the exact className prop destructured by the nearest enclosing component. It recursively checks snippet JSX and rendered MDX (excluding fenced examples), with regression tests for the safe and unsafe cases.

const [err, setErr] = useState(null);
const [loading, setLoading] = useState(true);
// Keep SSR and hydration deterministic with the configured dark default.
const [isDark, setIsDark] = useState(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] Reverting the lazy initializer to a constant is the right call for hydration determinism, but useState(true) is now hard-coded in six snippets (changelog, ecosystem-app-grid, ecosystem-contracts, sandbox-embed, sip-index, sstore-gas-live, plus useState('dark') in rpc-methods-viewer), each with a comment asserting "docs.json defaults to dark". That's an invariant duplicated seven times with nothing linking it back to docs.json — flip appearance.default to light or system and all seven silently render the wrong first paint with no failing check.

Two things would help:

  • Put the default in one place the snippets share (or at minimum reference docs.json → appearance.default explicitly in one comment and point the others at it).
  • The sync runs in useEffect, i.e. after paint, so light-mode readers still get a visible dark flash. useLayoutEffect would correct it before the browser paints and would close the gap the PR description claims is already closed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in c9be3de. All seven theme observers now synchronize in useLayoutEffect. A new CI check reads docs.json appearance.default, validates every registered hydration seed, and fails if the config and snippet defaults diverge.

// that the browser cannot parse.
setFrameSrc(src);
}
}, [src]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The effect only depends on src, so the theme is resolved once at mount and never again — toggling the docs theme afterwards leaves the Symphony widget in the old theme for the rest of the session. The header comment justifies this as protecting reader state, which is the right instinct for SandboxEmbed (a Remix editor with unsaved edits), but weaker here: commit 2's block dark:hidden / hidden dark:block pair switched instantly, and a swap widget the reader hasn't touched has nothing worth preserving.

Consider observing the dark class the way SandboxEmbed does and re-resolving the URL — either unconditionally, or only until the iframe has been interacted with.

Separate minor note: because the iframe is only created inside the effect, it's absent from the server-rendered HTML where the two-iframe version was present. The placeholder <div> inherits the caller's minHeight: 680px so there's no layout shift, just worth being aware of.

Comment thread style.css
/* Mintlify exposes no ids on these row containers. Anchor the top row to the
stable search id and the tab row to its semantic .nav-tabs marker instead
of their current `px-12` utility. */
#navbar div:has(> div > div > #search-bar-entry),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] This selector hardcodes an exact three-level nesting (div > div > div > #search-bar-entry), which is about as release-fragile as the [class*="px-12"] match it replaces — if Mintlify adds or removes one wrapper, the navbar rows silently go back to their 48px padding, which is precisely the class of failure this PR is fixing. The depth is clearly deliberate (a bare :has(#search-bar-entry) would match every ancestor and compound the padding), so there may be no better option, but a :has(> * > * > #search-bar-entry) variant or a note that this is the known-fragile part of the block would help the next person debugging a gutter regression.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c9be3de. Configure here.

Comment thread snippets/changelog.jsx
const [isDark, setIsDark] = useState(true);

useEffect(() => {
useLayoutEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

useLayoutEffect missing from snippet scope

High Severity

Theme sync now calls useLayoutEffect, which Mintlify does not inject into snippet scope. Snippets cannot import from react, so these components throw useLayoutEffect is not defined and the changelog, ecosystem, RPC explorer, sandbox, SIP index, and SSTORE widgets fail to render. The new CI check also requires this hook, so the broken pattern is locked in.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by learned rule: Mintlify snippet conventions — do not flag inline styles or suggest npm imports

Reviewed by Cursor Bugbot for commit c9be3de. Configure here.

seidroid[bot]
seidroid Bot previously requested changes Sep 16, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A large, well-documented layout/theming repair that correctly re-keys Mintlify overrides onto stable ids and inlines dynamic className strings so their dark: variants actually compile. The one must-fix is the new CI guard itself: scripts/check-snippet-classnames.mjs treats apostrophes in MDX prose and JSX text as JavaScript string delimiters, so it silently skips large regions of nearly every file it is supposed to police.

Findings: 2 blocking | 13 non-blocking | 8 posted inline

Blockers

  • scripts/check-snippet-classnames.mjs is unsound on this repo's actual content (see inline at line 107), so the new required CI step will report "0 failures" while the exact regression it exists to prevent ships. The accompanying test file only exercises synthetic sources with no prose apostrophes, no bare // in JSX text, and no MDX narrative, so the gap is not covered by tests either — fix the lexer (distinguish JS context from JSX/Markdown text) and add regression cases drawn from real files such as snippets/nonce-lane-diagrams.jsx and learn/general-brand-kit.mdx.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • REVIEW_GUIDELINES.md (base branch) is empty, so no repo-specific review standards were applied. cursor-review.md is also empty — the Cursor pass produced no output for this synthesis (Cursor Bugbot did post separately on the PR, but its findings were not available in the expected file). Codex contributed one finding, which is confirmed and promoted to the blocker above.
  • useLayoutEffect is newly introduced in seven snippets (changelog, ecosystem-app-grid, ecosystem-contracts, rpc-methods-viewer, sandbox-embed, sip-index, sstore-gas-live). These files import nothing — every hook comes from Mintlify's injected MDX scope, and useState/useEffect are the only hooks the repo had previously proven to be available. If useLayoutEffect is not in that scope, all seven snippets throw at render. Worth confirming in a Mintlify preview before merge, since check-snippet-theme-default.mjs now requires the pattern.
  • PR description overstates coverage: it lists "OpenZeppelin wizard iframe" under the light/dark audit, but commit 4 reverted the evm/evm-wizard.mdx change, so the final diff touches that page not at all. The wizard is now the only remaining plain third-party <iframe> while the Symphony embed got the new ThemeAwareIframe — either apply the wrapper there too or drop the claim from the description.
  • The workflow adds node --test and two node script steps without an actions/setup-node step, so the checks silently depend on whatever Node the ubuntu-latest image happens to ship. Pin it for reproducibility.
  • The PR asserts several results that cannot be reproduced from the diff ("automated text-contrast scan of all 136 pages × 2 themes", "DOM check for uncompiled Tailwind classes", "health check"). None of those tools are checked in, so the contrast and first-paint claims are unverifiable in review and will not be re-run on future PRs — consider landing the contrast scanner alongside the two scripts that were committed.
  • :has() is load-bearing for the whole desktop layout now (div:has(> #sidebar), #content-container > div:has(> #content-area), the two #navbar selectors). That is fine for current evergreen browsers but means Firefox ≤120 falls back to Mintlify's default layout rather than degrading partially — acceptable, just worth knowing.
  • 7 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread scripts/check-snippet-classnames.mjs Outdated
const next = source[index + 1];
let end = index + 1;

if (character === "'" || character === '"') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[blocker] structuralMask starts a string at any '/", with no notion of whether it is in JavaScript context or in JSX text / Markdown prose. Everything up to the next matching quote (or EOF) is blanked, and findUnsafeClassNames then scans only the mask — so any className={…} inside a masked region is invisible to the check.

This repo's content triggers it constantly. snippets/nonce-lane-diagrams.jsx:185 contains three apostrophes in one JSX text node (EOA's, LaneAccount's, EOA's); the odd count leaves the lexer "inside a string" until the next ' several lines later. Add <span className={someVar} /> anywhere in that window and CI stays green. Same for learn/general-brand-kit.mdx (the logo's clarity, Don't rescale…) and essentially every .mdx page — grep -rn "[a-z]'[a-z]" --include=*.mdx . returns hundreds of hits.

It can also fail the other way: an odd apostrophe count that swallows a { or } unbalances findMatchingForward, producing a spurious className has an unclosed JSX expression on unrelated prose edits.

The fix needs real context tracking (only treat quotes as delimiters inside JSX expression containers / JS regions), plus the same treatment for the // branch below — a bare https://… in JSX text currently masks the rest of that line as a line comment. Please add regression tests using actual file content, not quote-free synthetic sources.

(Independently reported by the Codex pass.)


const escapedSetter = seed.setter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const layoutSync = new RegExp(
`useLayoutEffect\\s*\\([\\s\\S]*?\\b${escapedSetter}\\s*\\(`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] useLayoutEffect\s*\([\s\S]*?\b${setter}\s*\( is applied to source.slice(seed.index) with an unbounded lazy gap, so it only proves the setter is called somewhere after some useLayoutEffect( in the file — not that the call is inside that effect's callback. A snippet with an unrelated useLayoutEffect near the top and setIsDark(...) in a plain useEffect 200 lines down passes. Bound the match to the effect's callback body (balance the parens the way findMatchingForward does in the sibling script) so the "applied before paint" invariant is actually enforced.

const repoDir = fileURLToPath(new URL('../', import.meta.url));
const snippetsDir = fileURLToPath(new URL('../snippets/', import.meta.url));
const docsConfigPath = fileURLToPath(new URL('../docs.json', import.meta.url));
const themeSeedFiles = new Set([

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] themeSeedFiles is a hardcoded allowlist, so a newly added theme-aware snippet is unguarded until someone remembers to register it here — and the check's value is precisely that nobody has to remember. snippets/theme-aware-iframe.jsx, added in this same PR, is a live example: it reads document.documentElement.classList.contains('dark') but is neither listed nor structured the way the script demands (plain useEffect, no isDark/theme seed), so it is exempt from the invariant. Consider detecting theme-aware snippets by content (any snippet that reads the dark class on <html>) and treating the set as derived rather than declared.

} = {}) => {
const [frameSrc, setFrameSrc] = useState(null);

useEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] Two things worth weighing here:

  1. The theme is read in useEffect, so frameSrc is null for the first paint and the component renders an empty <div> instead of the iframe. The sibling snippets in this PR deliberately moved to useLayoutEffect for exactly this reason; here it means the embed is absent from server-rendered HTML where the previous plain <iframe src="…"> was present.
  2. The wrapper accepts loading, allow and allowFullScreen but drops sandbox, referrerPolicy and width/height. Since this is now the sanctioned way to embed third-party content, a future caller needing sandbox will lose it silently — either forward the rest or spread remaining props.

Comment thread style.css
/* Mintlify exposes no ids on these row containers. Anchor the top row to the
stable search id and the tab row to its semantic .nav-tabs marker instead
of their current `px-12` utility. */
#navbar div:has(> div > div > #search-bar-entry),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] This hard-codes an exact three-level ancestor chain (div > div > div > #search-bar-entry), which is the same class of coupling the rest of this rewrite is removing — inserting or removing one wrapper div breaks it just as silently as lg:pl-[23.7rem] did. #navbar div:has(#search-bar-entry) with a :not() narrowing, or anchoring to whatever data-component-part Mintlify puts on that row, survives re-nesting.

<WorldIcon
size={24}
style={{ color: hover ? 'var(--sei-maroon-100)' : '#737373', transition: 'color 0.3s' }}
style={{ color: hover ? 'var(--sei-maroon-100)' : isDark ? '#737373' : 'var(--sei-grey-200)', transition: 'color 0.3s' }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The hover colour stays --sei-maroon-100 (#600014) in both themes while every other snippet touched by this audit switches to --sei-maroon-25 on dark surfaces. On the dark card background that is roughly 1.3:1, so the icon effectively disappears on hover in dark mode — the same failure mode the audit fixed elsewhere. Suggest hover ? (isDark ? 'var(--sei-maroon-25)' : 'var(--sei-maroon-100)') : ….

Comment thread snippets/network-tabs.jsx
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] clip: rect(0, 0, 0, 0) is deprecated; clipPath: 'inset(50%)' is the current visually-hidden idiom. Also, this object is rebuilt on every render for every inactive tab — hoisting it to a module-level constant keeps it out of the render path and makes the intent ("this is the sr-only recipe") clearer.

Separately, while this block was being rewritten: the three tab buttons are plain <button>s inside a <div> with no role="tablist" / role="tab" / aria-selected, so they are not exposed as tabs to assistive tech. Pre-existing, but cheap to add alongside the existing data-active.

if (!src) return src;
try {
const url = new URL(src);
url.searchParams.set('theme', isDark ? 'dark' : 'light');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] The comment above correctly notes this leaves hash payloads intact, but that also means the theme= query parameter is unlikely to reach Remix (which reads its theme from the hash config) — StackBlitz and CodeSandbox do honour ?theme=. Worth spot-checking each provider and noting which ones actually respond, otherwise the added param is inert for some embeds while still changing the "Open" link URL.

Co-authored-by: Cursor <cursoragent@cursor.com>

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A large but coherent styling refactor: the layout overrides move from Mintlify's renamed Tailwind classes onto stable ids plus :has(), dead selectors are pruned, and the Tailwind-extraction problem in snippets is fixed by inlining literal class lists with a new CI guard for theme seeds. I found no correctness blockers — verified that every referenced CSS variable exists, docs.json appearance.default is dark and matches all seven snippet seeds, and no references to the deleted cardClass/labelClass/contentClass variables remain — but the new CI guard is narrower than the convention it enforces, and two components now duplicate the same themed-iframe logic.

Findings: 0 blocking | 13 non-blocking | 7 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • ./cursor-review.md is empty — the Cursor pass produced no output for this PR. ./codex-review.md reported "No material issues found". ./REVIEW_GUIDELINES.md is also empty, so no repo-specific standards were applied beyond AGENTS.md/CONTRIBUTING.md.
  • useLayoutEffect is new to this repo — all seven occurrences are added by this PR, and no snippet imports React hooks (they rely on Mintlify's injected scope, which previously only needed useState/useEffect). Worth confirming on the preview deploy that the hook is in scope; React also logs a "useLayoutEffect does nothing on the server" warning during prerender.
  • The theme-seed invariant now has a CI guard, but the layout rewrite — the part that actually regressed and motivated this PR — has none. A DOM assertion in CI (e.g. sidebar width == 220, article padding-left == padding-right) would catch the next Mintlify rename the same way check-snippet-theme-default.mjs catches seed drift; today the failure mode is still silent.
  • The .dark, [data-theme="dark"] selector is narrowed to .dark only. That's consistent with every snippet detecting the theme via classList.contains('dark'), but it removes the fallback if Mintlify ever switches to a data-theme attribute.
  • The new workflow step runs bare node with no actions/setup-node, so it silently uses whatever Node the runner image ships. Fine today (the script only uses stable node:fs/promises), but pinning would make it reproducible.
  • No prompt-injection or instruction-like content was found in the diff, commit messages, or PR description.
  • 7 suggestion(s)/nit(s) flagged inline on specific lines.

const repoDir = fileURLToPath(new URL('../', import.meta.url));
const snippetsDir = fileURLToPath(new URL('../snippets/', import.meta.url));
const docsConfigPath = fileURLToPath(new URL('../docs.json', import.meta.url));
const themeSeedFiles = new Set([

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The guard is doubly opt-in: a file must be listed in themeSeedFiles and its state must be spelled exactly const [isDark, …] = useState(true|false) or const [theme, …] = useState('dark'|'light'). Anything else is invisible to it.

This PR already ships a counterexample — snippets/theme-aware-iframe.jsx is theme-aware, is not in this set, and its useState(null) matches neither regex, so it gets no coverage at all. Any future snippet that names the state dark, or seeds it as useState(APPEARANCE === 'dark'), is equally unchecked while CI still reports success.

Consider deriving the candidate list instead of hardcoding it — e.g. every snippets/*.jsx containing classList.contains('dark') must have a registered seed. That way adding a theme-aware snippet without a correct seed fails, rather than passing silently.

allow,
allowFullScreen
} = {}) => {
const [frameSrc, setFrameSrc] = useState(null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] This component doesn't follow the convention the same PR adds to AGENTS.md ("Theme-aware snippets must seed hydration from docs.json appearance.default and synchronize the DOM theme in useLayoutEffect"). It seeds null and resolves in useEffect, so the <iframe> is absent from the prerendered HTML entirely and only appears after hydration — the placeholder <div> does reserve the 680px via the passed style, so there's no layout shift, but the embed can't start loading until JS runs.

Seeding from the same dark default the other snippets use (and syncing in useLayoutEffect) would let the iframe ship in the server HTML and would make the file eligible for the new CI check.


// Update only the query string. URL.searchParams replaces theme=auto/system
// as well as light/dark and leaves Remix/StackBlitz hash payloads intact.
const themedSrc = (() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] themedSrc here and ThemeAwareIframe (added in this same PR) implement the identical behaviour: new URL(src)searchParams.set('theme', …)try/catch fallback to the raw src, with the iframe's src frozen at load time so a theme toggle can't reload the embed.

Since SandboxEmbed already gates the iframe behind a click, it could render <ThemeAwareIframe> for the frame body and keep only the click-to-load state locally. One implementation of the "themed third-party embed" rule is easier to keep correct than two — right now a fix to the URL handling has to be applied in both places.

Comment thread snippets/network-tabs.jsx
// as direct literals; use data attributes plus style.css or inline styles
// for conditional state. Tab surfaces use rgba hairlines so they read the
// same on both page backgrounds.
const [hoverTab, setHoverTab] = useState(null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] Hover moves from CSS (hover:bg-neutral-200 dark:hover:bg-neutral-700/70) into React state. Every mouseenter/mouseleave over a tab now re-renders the whole NetworkTabs subtree — which is the visible panel plus the two hidden copies kept for search indexing (lines 348-350).

The inlining is necessary for the colour tokens, but the hover background doesn't need JS: style.css already gained a .sei-network-tab hook in this PR, and it handles :hover colour there. Moving the three background values into .sei-network-tab, .sei-network-tab:hover, .sei-network-tab[data-active="true"] would drop hoverTab entirely and keep one owner for tab appearance instead of splitting it between style.css (colour) and tabButtonStyle (background/border).

Comment thread style.css
margin-right: 0 !important;
/* 1. Wrapper holding sidebar + content: flush left, capped and centred on
very wide screens so article lines never grow unreadably long. */
div:has(> #sidebar) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] Worth naming the residual risk: the overrides trade coupling to Mintlify's class names for coupling to its DOM structure. div:has(> #sidebar) requires the sidebar to stay a direct child of the layout wrapper, and #navbar div:has(> div > div > #search-bar-entry) (line 958) pins an exact three-level nesting.

Ids are certainly more stable than lg:pl-[23.7rem], but a Mintlify release that inserts one wrapper div breaks these the same way and just as silently — no build error, just the asymmetric gutter returning. The --sei-* tokens and the comment block documenting the expected tree are a real improvement for diagnosing that; a CI DOM assertion (see the general notes) would be what actually catches it.


const escapedSetter = seed.setter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const layoutSync = new RegExp(
`useLayoutEffect\\s*\\([\\s\\S]*?\\b${escapedSetter}\\s*\\(`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] useLayoutEffect\s*\([\s\S]*?\b<setter>\s*\( is unanchored over the entire remainder of the file, so it's satisfied by any later useLayoutEffect( followed by any later call to the setter — even from unrelated effects. It can only produce false passes, never false failures, so it's safe as a guard; scoping the match to the effect's own body (or asserting the setter appears between useLayoutEffect( and the matching }, [) would make it mean what the failure message claims.

Also minor: snippetsDir already ends in / (line 6), so line 39 builds …/snippets//changelog.jsx. Harmless — relative() normalizes it for the messages — but join() would read cleaner.

<div className="not-prose" style={{ border: '1px solid #ececee', marginTop: '1.25rem', background: '#ffffff' }}>
<div style={{ display: 'flex', aspectRatio: '16 / 6', width: '100%', alignItems: 'center', justifyContent: 'center', background: '#ffffff', padding: '32px', borderBottom: '1px solid #ececee' }}>
<div className="not-prose mt-5 border border-[var(--sei-card-border-light)] dark:border-[var(--sei-card-border-dark)] bg-[var(--sei-card-bg-light)] dark:bg-[var(--sei-card-bg-dark)]">
<div className="bg-white" style={{ display: 'flex', aspectRatio: '16 / 6', width: '100%', alignItems: 'center', justifyContent: 'center', padding: '32px', borderBottom: '1px solid var(--sei-card-border-light)' }}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] The clearspace panel stays bg-white in both themes (correct — the PNG has a white background, and the PR notes dark exports are out of scope), but borderBottom hardcodes var(--sei-card-border-light) (#ececee). The parent card is now themed via dark:bg-[var(--sei-card-bg-dark)], so in dark mode this hairline sits between white above and a near-black card below and reads as a bright line. The sibling rules in this file use border-[var(--sei-card-border-light)] dark:border-[var(--sei-card-border-dark)]; the same pair here would keep it consistent.

@seidroid
seidroid Bot dismissed their stale review September 16, 2026 12:22

Superseded: latest AI review found no blocking issues.

@alexander-sei
alexander-sei merged commit 8b96b9a into main Sep 16, 2026
19 of 23 checks passed
@alexander-sei
alexander-sei deleted the fix/layout-gutters-and-theme-audit branch September 16, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant