Flow builder: match the docs' new three-option theme switcher#673
Conversation
…/light/dark) Mintlify's hosted renderer replaced the docs' two-icon toggle with a system/light/dark switcher, so the flow builder's duplicated footer toggle no longer matched. The footer is now the same three-option control (same order, same aria-labels, active by preference with system resolving live against the OS), and the theme bridge carries the preference both ways: the docs side routes iframe changes through Mintlify's own (hidden) switcher so its stored preference stays true, and mirrors the docs preference into the footer. Both directions fall back to the old resolved-theme messages, so either side can deploy first. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Greptile SummaryThis PR upgrades the flow builder's footer theme toggle from a two-option light/dark toggle to a three-option system/light/dark switcher, matching Mintlify's new sidebar control. The docs-side bridge in
Confidence Score: 4/5The change is well-structured and the deploy-order fallback is thoughtfully handled; the only practical concerns are an unvalidated object-key access on the docs side and a missing origin check on the postMessage listener. The core theme-sync logic is sound — prefRef prevents stale closures, the pre-paint script removes flash, and the MutationObserver guard (ignoreNextMutation) prevents echo loops. The two items that warrant attention before merging: e.data.pref is used as an object key on btns without being whitelisted against the three valid values, which could invoke .click() on the group container div via a crafted postMessage from any same-page script; and the message listener has no origin check, accepting theme-sync from any sender. mintlify/flow-builder.mdx — the message handler and btns key access deserve a second look before shipping.
|
| Filename | Overview |
|---|---|
| mintlify/flow-builder.mdx | Adds prefButtons()/currentPref() helpers and routes iframe-initiated changes through Mintlify's own switcher buttons. btns[want] uses e.data.pref as an unvalidated object key with no origin check on the message listener. |
| components/grid-visualizer/src/app/page.tsx | handleThemeChange posts postMessage without an origin check and always fires even when the preference hasn't changed. The theme variable is now unused after destructuring. |
| components/grid-visualizer/src/hooks/useTheme.ts | Extended from 2-state (light/dark) to 3-state (system/light/dark); adds prefRef for stable closure access, matchMedia OS listener for live system resolution, and correct cleanup. Logic is sound. |
| components/grid-visualizer/src/components/Footer/Footer.tsx | Replaced single toggle button with a role="group" of three buttons (system/light/dark); aria-labels mirror Mintlify's switcher; icons correctly use the radius-3 stroke-1.5 variant. Clean implementation. |
| components/grid-visualizer/src/components/Footer/Footer.module.scss | Updated embed-mode sizing from 52px to 76px three-option pill; active state now keyed to data-theme-pref instead of data-theme; all: unset moved to .toggleItem. No issues. |
| components/grid-visualizer/src/app/layout.tsx | Inline pre-paint script now initializes data-theme-pref alongside data-theme; correctly defaults unknown/null values to 'system'. No issues. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant OS as OS (matchMedia)
participant IFrame as Flow Builder iframe
participant Docs as flow-builder.mdx (Docs page)
participant Mintlify as Mintlify Sidebar Switcher
Note over IFrame: Mount: sends theme-request
IFrame->>Docs: "postMessage { type: 'theme-request' }"
Docs->>Mintlify: currentPref() reads aria-pressed
Docs->>IFrame: "postMessage { type: 'theme-sync', theme, pref }"
Note over IFrame: User clicks footer toggle
IFrame->>IFrame: setPref(p) + apply(p)
IFrame->>Docs: "postMessage { type: 'theme-sync', theme: resolved, pref: p }"
Docs->>Mintlify: btns[pref].click() — routes through real switcher
Mintlify->>Docs: DOM mutation (.dark class change)
Docs->>Docs: MutationObserver fires (ignoreNextMutation guards echo)
Note over OS: OS preference changes
OS->>IFrame: matchMedia 'change' event
IFrame->>IFrame: "prefRef==='system' → apply('system')"
IFrame->>Docs: "postMessage { type: 'theme-sync', theme: resolved, pref: 'system' }"
Note over Docs: Docs page theme changes
Docs->>Docs: MutationObserver fires
Docs->>Mintlify: currentPref() reads new aria-pressed
Docs->>IFrame: "postMessage { type: 'theme-sync', theme, pref }"
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant OS as OS (matchMedia)
participant IFrame as Flow Builder iframe
participant Docs as flow-builder.mdx (Docs page)
participant Mintlify as Mintlify Sidebar Switcher
Note over IFrame: Mount: sends theme-request
IFrame->>Docs: "postMessage { type: 'theme-request' }"
Docs->>Mintlify: currentPref() reads aria-pressed
Docs->>IFrame: "postMessage { type: 'theme-sync', theme, pref }"
Note over IFrame: User clicks footer toggle
IFrame->>IFrame: setPref(p) + apply(p)
IFrame->>Docs: "postMessage { type: 'theme-sync', theme: resolved, pref: p }"
Docs->>Mintlify: btns[pref].click() — routes through real switcher
Mintlify->>Docs: DOM mutation (.dark class change)
Docs->>Docs: MutationObserver fires (ignoreNextMutation guards echo)
Note over OS: OS preference changes
OS->>IFrame: matchMedia 'change' event
IFrame->>IFrame: "prefRef==='system' → apply('system')"
IFrame->>Docs: "postMessage { type: 'theme-sync', theme: resolved, pref: 'system' }"
Note over Docs: Docs page theme changes
Docs->>Docs: MutationObserver fires
Docs->>Mintlify: currentPref() reads new aria-pressed
Docs->>IFrame: "postMessage { type: 'theme-sync', theme, pref }"
Comments Outside Diff (1)
-
mintlify/flow-builder.mdx, line 47-48 (link)The
handleMessagelistener has noe.origincheck. Sinceflow-builder.mdxis a public docs page, any window (e.g., a third-party script, or another iframe on the same page) could send{ type: 'theme-sync' }messages and trigger Mintlify's switcher buttons. The iframe's origin should be verified before processing.Prompt To Fix With AI
This is a comment left during a code review. Path: mintlify/flow-builder.mdx Line: 47-48 Comment: The `handleMessage` listener has no `e.origin` check. Since `flow-builder.mdx` is a public docs page, any window (e.g., a third-party script, or another iframe on the same page) could send `{ type: 'theme-sync' }` messages and trigger Mintlify's switcher buttons. The iframe's origin should be verified before processing. How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 4
mintlify/flow-builder.mdx:56-57
**Unvalidated `pref` used as object key on `btns`**
`want` comes directly from `e.data.pref` and is only checked for truthiness before being used as a property key. The `btns` object has a `group` key (the container `<div>`) in addition to `system`, `light`, and `dark`. A `theme-sync` message with `pref: 'group'` would reach `btns['group'].click()`, invoking `.click()` on the wrapper div — bypassing the intended switcher logic. Since there's no `e.origin` check on this handler, any script on the same page (including other iframes) can send such a message. Guard `want` against the known-valid values before accessing `btns`.
### Issue 2 of 4
mintlify/flow-builder.mdx:55-57
The `want` value from `e.data.pref` flows directly into an object property access on `btns`. Guard it against the three valid prefs so a malformed or unexpected message can never reach `.click()` on the `group` container element.
```suggestion
const want = e.data.pref;
const validPref = want === 'system' || want === 'light' || want === 'dark' ? want : null;
const btns = validPref ? prefButtons() : null;
if (btns && btns[validPref]) {
```
### Issue 3 of 4
mintlify/flow-builder.mdx:47-48
The `handleMessage` listener has no `e.origin` check. Since `flow-builder.mdx` is a public docs page, any window (e.g., a third-party script, or another iframe on the same page) could send `{ type: 'theme-sync' }` messages and trigger Mintlify's switcher buttons. The iframe's origin should be verified before processing.
```suggestion
const iframeOrigin = 'https://grid-flow-builder.vercel.app';
const handleMessage = (e) => {
if (e.origin !== iframeOrigin) return;
if (e.data && e.data.type === 'theme-request') {
```
### Issue 4 of 4
components/grid-visualizer/src/app/page.tsx:88
`theme` is destructured from `useTheme()` but is no longer passed to `Footer` (which now takes `pref` instead), and `handleThemeChange` recomputes the resolved value independently via `window.matchMedia`. This leaves `theme` unused, which TypeScript's `no-unused-vars` rule will flag.
```suggestion
const { pref, setPref } = useTheme();
```
Reviews (1): Last reviewed commit: "Flow builder: match the docs' new three-..." | Re-trigger Greptile
…ill now squircles Active/inactive colors were carried over inverted from the old embed CSS; they now match values measured from Mintlify's live component in both modes (active 505050 on gray-200/50 light, DFDFDF on 171717 dark; inactive 9F9F9F / 707070). The active pill also gets corner-shape: squircle, since the docs apply it globally and their pill reads as a soft square. Co-authored-by: Cursor <cursoragent@cursor.com>
| const btns = want ? prefButtons() : null; | ||
| if (btns && btns[want]) { |
There was a problem hiding this comment.
Unvalidated
pref used as object key on btns
want comes directly from e.data.pref and is only checked for truthiness before being used as a property key. The btns object has a group key (the container <div>) in addition to system, light, and dark. A theme-sync message with pref: 'group' would reach btns['group'].click(), invoking .click() on the wrapper div — bypassing the intended switcher logic. Since there's no e.origin check on this handler, any script on the same page (including other iframes) can send such a message. Guard want against the known-valid values before accessing btns.
Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/flow-builder.mdx
Line: 56-57
Comment:
**Unvalidated `pref` used as object key on `btns`**
`want` comes directly from `e.data.pref` and is only checked for truthiness before being used as a property key. The `btns` object has a `group` key (the container `<div>`) in addition to `system`, `light`, and `dark`. A `theme-sync` message with `pref: 'group'` would reach `btns['group'].click()`, invoking `.click()` on the wrapper div — bypassing the intended switcher logic. Since there's no `e.origin` check on this handler, any script on the same page (including other iframes) can send such a message. Guard `want` against the known-valid values before accessing `btns`.
How can I resolve this? If you propose a fix, please make it concise.| const want = e.data.pref; | ||
| const btns = want ? prefButtons() : null; | ||
| if (btns && btns[want]) { |
There was a problem hiding this comment.
The
want value from e.data.pref flows directly into an object property access on btns. Guard it against the three valid prefs so a malformed or unexpected message can never reach .click() on the group container element.
| const want = e.data.pref; | |
| const btns = want ? prefButtons() : null; | |
| if (btns && btns[want]) { | |
| const want = e.data.pref; | |
| const validPref = want === 'system' || want === 'light' || want === 'dark' ? want : null; | |
| const btns = validPref ? prefButtons() : null; | |
| if (btns && btns[validPref]) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/flow-builder.mdx
Line: 55-57
Comment:
The `want` value from `e.data.pref` flows directly into an object property access on `btns`. Guard it against the three valid prefs so a malformed or unexpected message can never reach `.click()` on the `group` container element.
```suggestion
const want = e.data.pref;
const validPref = want === 'system' || want === 'light' || want === 'dark' ? want : null;
const btns = validPref ? prefButtons() : null;
if (btns && btns[validPref]) {
```
How can I resolve this? If you propose a fix, please make it concise.| }, [mobileView]); | ||
|
|
||
| const { theme, setTheme } = useTheme(); | ||
| const { theme, pref, setPref } = useTheme(); |
There was a problem hiding this comment.
theme is destructured from useTheme() but is no longer passed to Footer (which now takes pref instead), and handleThemeChange recomputes the resolved value independently via window.matchMedia. This leaves theme unused, which TypeScript's no-unused-vars rule will flag.
| const { theme, pref, setPref } = useTheme(); | |
| const { pref, setPref } = useTheme(); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-visualizer/src/app/page.tsx
Line: 88
Comment:
`theme` is destructured from `useTheme()` but is no longer passed to `Footer` (which now takes `pref` instead), and `handleThemeChange` recomputes the resolved value independently via `window.matchMedia`. This leaves `theme` unused, which TypeScript's `no-unused-vars` rule will flag.
```suggestion
const { pref, setPref } = useTheme();
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
Stroke weights didn't line up: the central-icons set renders 24-grid icons with non-scaling 1.5px strokes, while the docs' glyphs are 16-grid at 12px (1.125px effective). Embed mode now renders the docs' own artwork verbatim (Mintlify's system + sun, the classic crescent) with scaling strokes, so the footer toggle is pixel-identical to the sidebar switcher. Standalone keeps the central-icons set. Co-authored-by: Cursor <cursoragent@cursor.com>
…e docs' true weight) The docs pin all inline svg strokes at their authored 1.5px via a global non-scaling-stroke rule, so matching them means matching that absolute weight, not the scaled 1.125px. Co-authored-by: Cursor <cursoragent@cursor.com>
…in weight, matching their scoped override Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Mintlify's hosted renderer replaced the docs' two-icon theme toggle with a three-option system/light/dark switcher, leaving the flow builder's duplicated footer toggle mismatched. This PR:
data-theme-pref, set pre-paint), andsystemresolves live against the OS via a matchMedia listener. Embed mode matches the docs switcher's geometry (76×28 pill, 20px buttons, 12px glyphs).flow-builder.mdx+ visualizer): messages now carry the preference alongside the resolved theme. The docs side routes iframe-initiated changes through Mintlify's own (hidden) switcher buttons so its stored preference andaria-pressedstay true — no more class-vs-preference desync — and reports its preference into the iframe so the footer mirrors the docs exactly.Test plan
Made with Cursor