Skip to content

Flow builder: match the docs' new three-option theme switcher#673

Merged
patcapulong merged 5 commits into
mainfrom
pat/flow-builder-theme-preference
Jul 9, 2026
Merged

Flow builder: match the docs' new three-option theme switcher#673
patcapulong merged 5 commits into
mainfrom
pat/flow-builder-theme-preference

Conversation

@patcapulong

Copy link
Copy Markdown
Contributor

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:

  • Footer toggle (grid-visualizer): now the same three-option control as the docs — monitor/sun/moon in the same order with the same aria-labels, active state tracks the preference (data-theme-pref, set pre-paint), and system resolves live against the OS via a matchMedia listener. Embed mode matches the docs switcher's geometry (76×28 pill, 20px buttons, 12px glyphs).
  • Theme bridge (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 and aria-pressed stay true — no more class-vs-preference desync — and reports its preference into the iframe so the footer mirrors the docs exactly.
  • Deploy-order safe: both sides fall back to the old resolved-theme messages, so docs and the visualizer can ship in either order.

Test plan

  • Flow builder standalone: three-option toggle works, system follows OS
  • Docs flow-builder page: flipping the footer toggle updates the docs theme AND Mintlify's switcher state (check another page's sidebar switcher afterwards)
  • Docs → iframe: changing theme on another docs page carries into the flow builder on navigation
  • Old-format compatibility: prod visualizer + new docs (and vice versa) still sync resolved themes

Made with Cursor

…/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>
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
grid-flow-builder Ready Ready Preview, Comment Jul 9, 2026 12:05am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
grid-wallet-demo Ignored Ignored Preview Jul 9, 2026 12:05am

Request Review

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 flow-builder.mdx is updated to route iframe-initiated changes through Mintlify's own hidden switcher buttons so that Mintlify's internal preference state and aria-pressed attributes stay accurate, and both sides fall back to the old resolved-theme message format for deploy-order safety.

  • useTheme.ts + layout.tsx: Introduces ThemePref ('system' | 'light' | 'dark'), a prefRef for stable OS-listener closures, and a pre-paint inline script that initializes data-theme-pref alongside data-theme before first paint — preventing a flash of the wrong active toggle option.
  • Footer.tsx + Footer.module.scss: The outer toggle <button> becomes a <div role="group"> containing three individual <button> elements; embed-mode sizing grows from a 52×28 two-option pill to a 76×28 three-option pill, with CSS-only active-state rules keyed to data-theme-pref.
  • flow-builder.mdx: Adds prefButtons() and currentPref() helpers that locate and read Mintlify's sidebar switcher; theme messages from the iframe now carry pref alongside the resolved theme, letting the docs route changes through the real Mintlify buttons rather than directly toggling the .dark class.

Confidence Score: 4/5

The 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.

Important Files Changed

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 }"
Loading
%%{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 }"
Loading

Comments Outside Diff (1)

  1. mintlify/flow-builder.mdx, line 47-48 (link)

    P2 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.

    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>
Comment thread mintlify/flow-builder.mdx
Comment on lines +56 to +57
const btns = want ? prefButtons() : null;
if (btns && btns[want]) {

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.

P2 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.

Comment thread mintlify/flow-builder.mdx
Comment on lines +55 to +57
const want = e.data.pref;
const btns = want ? prefButtons() : null;
if (btns && btns[want]) {

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.

P2 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.

Suggested change
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();

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.

P2 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.

Suggested change
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!

@mintlify

mintlify Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview Jul 8, 2026, 11:52 PM

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>
@patcapulong patcapulong merged commit de87a28 into main Jul 9, 2026
9 checks passed
@patcapulong patcapulong deleted the pat/flow-builder-theme-preference branch July 9, 2026 00: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.

2 participants