From 811978b0d9e6c39522e143bdd74bbf34c49bc02b Mon Sep 17 00:00:00 2001 From: Pat Capulong Date: Mon, 6 Jul 2026 20:25:59 -0700 Subject: [PATCH 1/9] Playground: default the app/API panels to a 50/50 split and deepen the laptop API peek The API column's default (and drag-snap target) is now half the space right of the configure column instead of a fixed 475px, and it tracks the split on window resize until the user drags a custom width. On the stacked laptop layout the API panel peeks 64px of body under its header so it reads as content waiting below, not just a bar. Mobile is untouched. Co-authored-by: Cursor --- .../grid-wallet-demo/src/app/page.module.scss | 4 +- .../src/hooks/useColumnResize.ts | 37 ++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/components/grid-wallet-demo/src/app/page.module.scss b/components/grid-wallet-demo/src/app/page.module.scss index 88db2b62..fb557909 100644 --- a/components/grid-wallet-demo/src/app/page.module.scss +++ b/components/grid-wallet-demo/src/app/page.module.scss @@ -60,7 +60,9 @@ .appCol { flex: none; width: 100% !important; - min-height: calc(100% - var(--panel-header-height, 52px)); + /* Peek the API panel's header PLUS a strip of its body under the phone — + enough to read as content waiting below, not just a bar. */ + min-height: calc(100% - var(--panel-header-height, 52px) - 64px); overflow: visible; } diff --git a/components/grid-wallet-demo/src/hooks/useColumnResize.ts b/components/grid-wallet-demo/src/hooks/useColumnResize.ts index 32dcdc39..502894e4 100644 --- a/components/grid-wallet-demo/src/hooks/useColumnResize.ts +++ b/components/grid-wallet-demo/src/hooks/useColumnResize.ts @@ -4,40 +4,47 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react'; const CONFIGURE_WIDTH = 475; const MIN_APP = 320; -// Never resize the API column below its default/snap width (= the configure -// column width); dragging only widens it from there. +// Never resize the API column below the configure column width; dragging only +// widens it from there. const MIN_API = CONFIGURE_WIDTH; const SNAP_THRESHOLD = 28; -/** Default + snap target — matches configure column width. */ +/** SSR/first-paint fallback — replaced by the 50/50 split once the layout + * measures (pre-paint, in the layout effect below). */ export const API_DEFAULT_WIDTH = CONFIGURE_WIDTH; function clampApiWidth(width: number, totalMiddle: number): number { return Math.max(MIN_API, Math.min(totalMiddle - MIN_APP, width)); } -function snapApiWidth(width: number, totalMiddle: number): number { - const target = clampApiWidth(API_DEFAULT_WIDTH, totalMiddle); - if (Math.abs(width - target) <= SNAP_THRESHOLD) return target; - return width; +/** Default + snap target — the app and API panels split the space right of + * the configure column 50/50. */ +function defaultApiWidth(totalMiddle: number): number { + return clampApiWidth(Math.round(totalMiddle / 2), totalMiddle); } export function useColumnResize() { const layoutRef = useRef(null); const apiColRef = useRef(null); const [apiWidth, setApiWidth] = useState(API_DEFAULT_WIDTH); + // While the column sits at the default split, window resizes keep it AT the + // split (both panels breathe together). A custom drag pins it to a px width + // until the user snaps it back to the split. + const userResized = useRef(false); useLayoutEffect(() => { - const clampToLayout = () => { + const fitToLayout = () => { const layout = layoutRef.current; if (!layout) return; const totalMiddle = layout.getBoundingClientRect().width - CONFIGURE_WIDTH; - setApiWidth((w) => clampApiWidth(w, totalMiddle)); + setApiWidth((w) => + userResized.current ? clampApiWidth(w, totalMiddle) : defaultApiWidth(totalMiddle), + ); }; - clampToLayout(); - window.addEventListener('resize', clampToLayout); - return () => window.removeEventListener('resize', clampToLayout); + fitToLayout(); + window.addEventListener('resize', fitToLayout); + return () => window.removeEventListener('resize', fitToLayout); }, []); const onResizeStart = useCallback((e: React.MouseEvent) => { @@ -54,7 +61,11 @@ export function useColumnResize() { const totalMiddle = layout.getBoundingClientRect().width - CONFIGURE_WIDTH; const delta = clientX - startX; const raw = clampApiWidth(startApiWidth - delta, totalMiddle); - setApiWidth(snapApiWidth(raw, totalMiddle)); + const target = defaultApiWidth(totalMiddle); + const snapped = Math.abs(raw - target) <= SNAP_THRESHOLD ? target : raw; + // Snapping back to the split resumes 50/50 tracking on window resize. + userResized.current = snapped !== target; + setApiWidth(snapped); }; const onMove = (ev: MouseEvent) => applyWidth(ev.clientX); From d227b9cb42f88f25cfe0b84935c6045036da4114 Mon Sep 17 00:00:00 2001 From: Pat Capulong Date: Mon, 6 Jul 2026 20:35:25 -0700 Subject: [PATCH 2/9] Playground: drop the App header everywhere and the API calls header on the wide layout The phone stage runs full-bleed on the dot grid, and side-by-side the calls column opens straight onto its content (the resize handle already draws the full-height divider). The API header stays on stacked layouts, where it's the peek target and carries the new-call count pill. Co-authored-by: Cursor --- .../src/components/ApiPanel/ApiPanel.module.scss | 8 +++----- .../grid-wallet-demo/src/components/AppPanel/AppPanel.tsx | 3 --- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanel.module.scss b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanel.module.scss index 6e16cdf8..175d3a2b 100644 --- a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanel.module.scss +++ b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanel.module.scss @@ -81,12 +81,10 @@ overflow: visible; } + /* Side-by-side: the calls ARE the column — no header bar needed (it only + earns its keep stacked, where it's the peek target + new-call signifier). + The resize handle draws the full-height column divider. */ .headerStacked { - border-left: var(--stroke-xs) solid var(--border-primary); - } - - /* Side-by-side: calls are always in view, so no "new" signifier. */ - .newPill { display: none; } } diff --git a/components/grid-wallet-demo/src/components/AppPanel/AppPanel.tsx b/components/grid-wallet-demo/src/components/AppPanel/AppPanel.tsx index 67e60202..f1b5b3b0 100644 --- a/components/grid-wallet-demo/src/components/AppPanel/AppPanel.tsx +++ b/components/grid-wallet-demo/src/components/AppPanel/AppPanel.tsx @@ -1,7 +1,5 @@ 'use client'; -import { IconPhoneDynamicIsland } from '@central-icons-react/round-outlined-radius-3-stroke-1.5/IconPhoneDynamicIsland'; -import { PanelHeader } from '@/components/PanelHeader/PanelHeader'; import { DotGridCanvas } from '@/components/DotGridCanvas/DotGridCanvas'; import type { PhoneProps } from '@/components/Phone'; import { DemoPhone } from '@/components/DemoPhone/DemoPhone'; @@ -114,7 +112,6 @@ export function AppPanel(phone: DemoLogicPhoneSlice) { return (
- } title="App" />
From 5b5e595247bdc504a31e7ddda59806f7e8dce7ef Mon Sep 17 00:00:00 2001 From: Pat Capulong Date: Mon, 6 Jul 2026 20:40:45 -0700 Subject: [PATCH 3/9] Playground: make the laptop layout's compact configure panel the default everywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The configure column is 400px with 40px body padding at every breakpoint (was 475/48 on wide) — the laptop-only overrides are gone, and the resize hook's CONFIGURE_WIDTH follows, which also lowers the API column's minimum width to match. The freed space flows into the app/API 50/50 split. Co-authored-by: Cursor --- .../ConfigurePanel/ConfigurePanel.module.scss | 15 +++------------ .../grid-wallet-demo/src/hooks/useColumnResize.ts | 4 +++- .../grid-wallet-demo/src/styles/breakpoints.scss | 9 ++++----- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/components/grid-wallet-demo/src/components/ConfigurePanel/ConfigurePanel.module.scss b/components/grid-wallet-demo/src/components/ConfigurePanel/ConfigurePanel.module.scss index b9bf65f9..f4c23735 100644 --- a/components/grid-wallet-demo/src/components/ConfigurePanel/ConfigurePanel.module.scss +++ b/components/grid-wallet-demo/src/components/ConfigurePanel/ConfigurePanel.module.scss @@ -1,7 +1,8 @@ @use '../../styles/breakpoints' as *; .panel { - width: 475px; + /* The laptop layout's compact sizing is the default at every breakpoint. */ + width: $layout-laptop-config-width; flex-shrink: 0; display: flex; flex-direction: column; @@ -14,12 +15,6 @@ } } -@include layout-laptop { - .panel { - width: $layout-laptop-config-width; - } -} - @media (max-width: $breakpoint-layout-mobile) { .panel { width: 100%; @@ -32,15 +27,11 @@ flex: 1; display: flex; flex-direction: column; - padding: var(--spacing-4xl); + padding: $layout-laptop-content-padding; overflow-y: auto; overscroll-behavior: contain; min-height: 0; - @include layout-laptop { - padding: $layout-laptop-content-padding; - } - // Mobile: tighter 24px padding (32px on top for breathing room under the navbar) // + room for the fixed "Explore playground" button. Declared after the base // padding so it wins at equal specificity. diff --git a/components/grid-wallet-demo/src/hooks/useColumnResize.ts b/components/grid-wallet-demo/src/hooks/useColumnResize.ts index 502894e4..d6e11576 100644 --- a/components/grid-wallet-demo/src/hooks/useColumnResize.ts +++ b/components/grid-wallet-demo/src/hooks/useColumnResize.ts @@ -2,7 +2,9 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react'; -const CONFIGURE_WIDTH = 475; +// Matches $layout-laptop-config-width — the compact configure column is the +// default at every breakpoint. +const CONFIGURE_WIDTH = 400; const MIN_APP = 320; // Never resize the API column below the configure column width; dragging only // widens it from there. diff --git a/components/grid-wallet-demo/src/styles/breakpoints.scss b/components/grid-wallet-demo/src/styles/breakpoints.scss index 603a6df4..13b4757e 100644 --- a/components/grid-wallet-demo/src/styles/breakpoints.scss +++ b/components/grid-wallet-demo/src/styles/breakpoints.scss @@ -1,9 +1,8 @@ /** App + API side-by-side inside the right column. Above this the layout is 3 - columns: Configure (475) | App (phone) | API (475 default), so the App column - is roughly (viewport − 950). The phone needs ~466px (434 shell + 16px inset - each side) to render full size, i.e. ~1416px of viewport; below that the App - column squeezes the phone tiny. Stack at 1440px so the phone never shrinks - before App + API stack vertically (each full-width). Tune to taste. */ + columns: Configure (400) | App | API (App and API split the rest 50/50 by + default). The phone needs ~466px (434 shell + 16px inset each side) to + render full size; stack at 1440px so the phone never shrinks before App + + API stack vertically (each full-width). Tune to taste. */ $breakpoint-layout-wide: 1440px; /** Configure stacks above app + API (phone). */ From 7ae1074365355810ae43899636845b3d9c42ff09 Mon Sep 17 00:00:00 2001 From: Pat Capulong Date: Mon, 6 Jul 2026 20:49:37 -0700 Subject: [PATCH 4/9] Playground: lift the light dot-grid stage off the Mintlify chrome gray and align API panel side padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stage is #F5F5F3 — two steps under the panel surface (#F8F8F7), clearly lighter than surface-secondary (#F0F0EE) so it stops matching the docs chrome. The API feed's side padding drops 48 → 40px to match the configure panel. Co-authored-by: Cursor --- components/grid-wallet-demo/src/app/globals.scss | 10 ++++++---- .../src/components/ApiPanel/ApiCallList.module.scss | 3 ++- .../components/ApiPanel/ApiPanelSkeleton.module.scss | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/components/grid-wallet-demo/src/app/globals.scss b/components/grid-wallet-demo/src/app/globals.scss index a8104980..0f9dd66d 100644 --- a/components/grid-wallet-demo/src/app/globals.scss +++ b/components/grid-wallet-demo/src/app/globals.scss @@ -147,10 +147,12 @@ body { [data-theme] flips, so the whole backdrop changes in lockstep (no stagger). `--dot-grid-dot` is the resting dot; `--dot-grid-dot-peak` is the ripple crest. */ :root { - /* Light: surface-secondary is the sunken stage gray (#F0F0EE), like - grid-visualizer's dot panels. Dark overrides to gray-925 below — the WebGL - dots' original backdrop; surface-secondary's #111111 reads too dark. */ - --dot-grid-bg: var(--surface-secondary); + /* Light: halfway between surface-secondary's sunken stage gray (#F0F0EE — + too close to the Mintlify chrome) and the panel surface (#F8F8F7), so the + stage reads as its own layer without matching either. Dark overrides to + gray-925 below — the WebGL dots' original backdrop; surface-secondary's + #111111 reads too dark. */ + --dot-grid-bg: #f5f5f3; --dot-grid-dot: #deded9; --dot-grid-dot-peak: #b8b8b3; } diff --git a/components/grid-wallet-demo/src/components/ApiPanel/ApiCallList.module.scss b/components/grid-wallet-demo/src/components/ApiPanel/ApiCallList.module.scss index c2edc355..e26c598f 100644 --- a/components/grid-wallet-demo/src/components/ApiPanel/ApiCallList.module.scss +++ b/components/grid-wallet-demo/src/components/ApiPanel/ApiCallList.module.scss @@ -13,7 +13,8 @@ display: flex; flex-direction: column; width: 100%; - padding: 48px; + /* Sides match the Configure panel's content padding. */ + padding: 48px $layout-laptop-content-padding; padding-top: calc(var(--spacing-md) + 12px); // Mobile: match the Configure panel's 24px side padding. diff --git a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelSkeleton.module.scss b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelSkeleton.module.scss index 967c941a..a09a674e 100644 --- a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelSkeleton.module.scss +++ b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelSkeleton.module.scss @@ -16,7 +16,8 @@ $skeleton-bone-dark: var(--color-gray-850); display: flex; flex-direction: column; width: 100%; - padding: 48px; + /* Sides match the Configure panel's content padding. */ + padding: 48px $layout-laptop-content-padding; padding-top: calc(var(--spacing-md) + 12px); gap: var(--spacing-md); opacity: 0.5; From bdaa53d3fe85f955c571e5e694f422de1fbd4c05 Mon Sep 17 00:00:00 2001 From: Pat Capulong Date: Mon, 6 Jul 2026 22:39:16 -0700 Subject: [PATCH 5/9] Playground: chrome-gray panels experiment + nav-aware code panel default Side-by-side, both side panels wear the docs-chrome gray (surface-secondary, both modes) with cards/tiles stepped one notch softer, the skeleton melt and bone contrast tuned to match, and the light stage at #F4F4F3 (wide only; the GL canvas re-reads the palette on resize). The code panel's default + snap width is the docs sidebar + configure column (680), following the live sidebar width via a new nav-sync embed message (48px rail when collapsed); dragging still grows it freely like before. Configure content top-aligns. Co-authored-by: Cursor --- .../grid-wallet-demo/src/app/globals.scss | 28 +++-- .../ApiPanel/ApiCallList.module.scss | 32 +++-- .../components/ApiPanel/ApiPanel.module.scss | 7 ++ .../ApiPanel/ApiPanelEmpty.module.scss | 19 ++- .../ApiPanel/ApiPanelSkeleton.module.scss | 13 ++- .../AuthMethodPicker.module.scss | 25 ++-- .../ConfigurePanel/ConfigurePanel.module.scss | 14 ++- .../FlowPicker/FlowPicker.module.scss | 30 +++-- .../UseCasePicker/UseCasePicker.module.scss | 25 ++-- .../src/components/glass-gl/StageGL.tsx | 3 + .../src/hooks/useColumnResize.ts | 109 +++++++++++------- .../global-accounts/wallet-demo-embed.mdx | 21 ++++ 12 files changed, 221 insertions(+), 105 deletions(-) diff --git a/components/grid-wallet-demo/src/app/globals.scss b/components/grid-wallet-demo/src/app/globals.scss index 0f9dd66d..04dd23c5 100644 --- a/components/grid-wallet-demo/src/app/globals.scss +++ b/components/grid-wallet-demo/src/app/globals.scss @@ -147,16 +147,24 @@ body { [data-theme] flips, so the whole backdrop changes in lockstep (no stagger). `--dot-grid-dot` is the resting dot; `--dot-grid-dot-peak` is the ripple crest. */ :root { - /* Light: halfway between surface-secondary's sunken stage gray (#F0F0EE — - too close to the Mintlify chrome) and the panel surface (#F8F8F7), so the - stage reads as its own layer without matching either. Dark overrides to - gray-925 below — the WebGL dots' original backdrop; surface-secondary's - #111111 reads too dark. */ - --dot-grid-bg: #f5f5f3; + /* Light: surface-primary (#F8F8F7) — the stage sits a step ABOVE the + chrome-gray side panels (surface-secondary), matching the raised cards. + Dark overrides below mirror it: gray-950 stage over #111111 chrome. */ + --dot-grid-bg: var(--surface-primary); --dot-grid-dot: #deded9; --dot-grid-dot-peak: #b8b8b3; } +/* 3-col layout (>= $breakpoint-layout-wide): the light stage steps down a hair + so it separates from the raised cards flanking it. Declared BEFORE the dark + block below so dark mode (same specificity, later in source) still wins. + The JS canvas re-reads the token on resize (StageGL's ResizeObserver). */ +@media (min-width: 1440px) { + :root { + --dot-grid-bg: #f4f4f3; + } +} + :root { --p-accent: #11a967; --p-accent-press: #0e8f57; @@ -178,9 +186,11 @@ body { } [data-theme='dark'] { - --dot-grid-bg: var(--color-gray-925); - --dot-grid-dot: #333330; - --dot-grid-dot-peak: #4d4d47; + /* gray-950 (#1A1A1A) — a step darker than the chrome-gray side panels' + raised cards, mirroring the light stage sitting under the panel surface. */ + --dot-grid-bg: var(--color-gray-950); + --dot-grid-dot: #2d2d2a; + --dot-grid-dot-peak: #474741; --p-bg: #000000; --p-screen: #000000; --p-surface: #141414; diff --git a/components/grid-wallet-demo/src/components/ApiPanel/ApiCallList.module.scss b/components/grid-wallet-demo/src/components/ApiPanel/ApiCallList.module.scss index e26c598f..b9b9d808 100644 --- a/components/grid-wallet-demo/src/components/ApiPanel/ApiCallList.module.scss +++ b/components/grid-wallet-demo/src/components/ApiPanel/ApiCallList.module.scss @@ -102,16 +102,19 @@ corner-shape: var(--corner-shape); --badge-height: 20px; --row-padding: calc((48px - var(--badge-height)) / 2); + /* Single source for the card surface — the endpoint fade + tab indicator + must match it exactly, and the wide layout re-tints all three at once. */ + --call-card-bg: var(--surface-base); display: flex; flex-direction: column; width: 100%; - background: var(--surface-base); + background: var(--call-card-bg); border: 0.5px solid var(--border-tertiary); border-radius: var(--corner-radius-md); overflow: clip; :global([data-theme='dark']) & { - background: var(--color-gray-925); + --call-card-bg: var(--color-gray-925); } &:hover, @@ -190,11 +193,7 @@ pointer-events: none; opacity: 0; transition: opacity 150ms ease; - background: linear-gradient(to right, transparent, var(--surface-base) 52%); - - :global([data-theme='dark']) & { - background: linear-gradient(to right, transparent, var(--color-gray-925) 52%); - } + background: linear-gradient(to right, transparent, var(--call-card-bg) 52%); } .endpointCopyBtn { @@ -254,12 +253,8 @@ box-sizing: border-box; border-left: var(--stroke-xs) solid var(--border-tertiary); border-right: var(--stroke-xs) solid var(--border-tertiary); - background: var(--surface-base); + background: var(--call-card-bg); pointer-events: none; - - :global([data-theme='dark']) & { - background: var(--color-gray-925); - } } .tabGroupLeadingActive .tabIndicator { @@ -372,6 +367,19 @@ } } +/* Side-by-side the panel wears the docs-chrome gray, so the cards step down + with it: pure white (light) / gray-925 (dark) read too hot on the chrome — + one notch softer keeps the same card-to-panel delta as the stacked layout. */ +@media (min-width: $breakpoint-layout-wide) { + .callCard { + --call-card-bg: var(--surface-primary); + + :global([data-theme='dark']) & { + --call-card-bg: var(--color-gray-950); + } + } +} + @include layout-laptop { .list { align-items: center; diff --git a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanel.module.scss b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanel.module.scss index 175d3a2b..228d4a58 100644 --- a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanel.module.scss +++ b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanel.module.scss @@ -75,6 +75,13 @@ .panel { overflow-y: auto; overscroll-behavior: contain; + /* Experiment: side-by-side, the calls column wears the docs-chrome gray + (both modes — surface-secondary flips with the theme). */ + background: var(--surface-secondary); + + :global([data-theme='dark']) & { + background: var(--surface-secondary); + } } .body { diff --git a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelEmpty.module.scss b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelEmpty.module.scss index a84b608e..dbaffbba 100644 --- a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelEmpty.module.scss +++ b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelEmpty.module.scss @@ -22,6 +22,8 @@ .skeletonCover { --cover-fade-height: 56px; --cover-top: 0px; + /* Must match the panel surface exactly or the melt reads as a tint band. */ + --cover-bg: var(--surface-primary); position: absolute; inset: var(--cover-top) 0 0; pointer-events: none; @@ -30,16 +32,21 @@ transition: opacity var(--cover-duration, 550ms) cubic-bezier(0.19, 1, 0.22, 1); background: linear-gradient( to top, - var(--surface-primary) calc(100% - var(--cover-fade-height)), + var(--cover-bg) calc(100% - var(--cover-fade-height)), transparent 100% ); :global([data-theme='dark']) & { - background: linear-gradient( - to top, - var(--color-gray-950) calc(100% - var(--cover-fade-height)), - transparent 100% - ); + --cover-bg: var(--color-gray-950); + } + + /* Side-by-side the panel wears the docs-chrome gray (both modes). */ + @media (min-width: $breakpoint-layout-wide) { + --cover-bg: var(--surface-secondary); + + :global([data-theme='dark']) & { + --cover-bg: var(--surface-secondary); + } } } diff --git a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelSkeleton.module.scss b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelSkeleton.module.scss index a09a674e..df391aae 100644 --- a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelSkeleton.module.scss +++ b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelSkeleton.module.scss @@ -1,6 +1,8 @@ @use '../../styles/breakpoints' as *; -$skeleton-bone-light: var(--color-gray-075); +/* gray-100 (was 075): at the feed's 0.5 opacity the pills washed out against + the chrome-gray panel — one notch darker keeps them faint but legible. */ +$skeleton-bone-light: var(--color-gray-100); $skeleton-bone-dark: var(--color-gray-850); .list { @@ -57,6 +59,15 @@ $skeleton-bone-dark: var(--color-gray-850); :global([data-theme='dark']) & { background: var(--color-gray-925); } + + /* Match the live cards' softer surface on the chrome-gray panel (wide). */ + @media (min-width: $breakpoint-layout-wide) { + background: var(--surface-primary); + + :global([data-theme='dark']) & { + background: var(--color-gray-950); + } + } } .entryHeader { diff --git a/components/grid-wallet-demo/src/components/AuthMethodPicker/AuthMethodPicker.module.scss b/components/grid-wallet-demo/src/components/AuthMethodPicker/AuthMethodPicker.module.scss index f81ef324..c0b1bb9f 100644 --- a/components/grid-wallet-demo/src/components/AuthMethodPicker/AuthMethodPicker.module.scss +++ b/components/grid-wallet-demo/src/components/AuthMethodPicker/AuthMethodPicker.module.scss @@ -3,16 +3,27 @@ .group { corner-shape: var(--corner-shape); + /* Single source for the tile surface — press states mix from it, and the + wide layout steps it down to sit on the chrome-gray panel. */ + --tile-bg: var(--surface-base); display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; - background: var(--surface-base); + background: var(--tile-bg); border: var(--stroke-xs) solid var(--border-primary); border-radius: var(--corner-radius-md); overflow: clip; :global([data-theme='dark']) & { - background: var(--color-gray-925); + --tile-bg: var(--color-gray-925); + } + + @media (min-width: $breakpoint-layout-wide) { + --tile-bg: var(--surface-primary); + + :global([data-theme='dark']) & { + --tile-bg: var(--color-gray-950); + } } } @@ -24,16 +35,12 @@ gap: var(--spacing-sm); min-width: 0; padding: var(--spacing-md); - background: var(--surface-base); + background: var(--tile-bg); border-bottom: var(--stroke-xs) solid var(--border-primary); overflow: clip; cursor: pointer; transition: background 100ms ease; - :global([data-theme='dark']) & { - background: var(--color-gray-925); - } - &:nth-child(odd) { border-right: var(--stroke-xs) solid var(--border-primary); } @@ -54,7 +61,7 @@ // Hover only with a real pointer — avoids the fill sticking after a tap on touch. @media (hover: hover) { &:not(:disabled):hover { - background: color-mix(in srgb, var(--surface-base) 98%, black); + background: color-mix(in srgb, var(--tile-bg) 98%, black); :global([data-theme='dark']) & { background: var(--color-alpha-white-04); @@ -64,7 +71,7 @@ // Tap/press feedback — works on touch and resets on release (unlike :hover). &:not(:disabled):active { - background: color-mix(in srgb, var(--surface-base) 98%, black); + background: color-mix(in srgb, var(--tile-bg) 98%, black); :global([data-theme='dark']) & { background: var(--color-alpha-white-04); diff --git a/components/grid-wallet-demo/src/components/ConfigurePanel/ConfigurePanel.module.scss b/components/grid-wallet-demo/src/components/ConfigurePanel/ConfigurePanel.module.scss index f4c23735..528773c6 100644 --- a/components/grid-wallet-demo/src/components/ConfigurePanel/ConfigurePanel.module.scss +++ b/components/grid-wallet-demo/src/components/ConfigurePanel/ConfigurePanel.module.scss @@ -13,6 +13,16 @@ :global([data-theme='dark']) & { background: var(--color-gray-950); } + + /* Side-by-side, the configure column wears the docs-chrome gray, matching + the API panel (both modes — surface-secondary flips with the theme). */ + @media (min-width: $breakpoint-layout-wide) { + background: var(--surface-secondary); + + :global([data-theme='dark']) & { + background: var(--surface-secondary); + } + } } @media (max-width: $breakpoint-layout-mobile) { @@ -47,10 +57,6 @@ flex-direction: column; gap: var(--spacing-md); width: 100%; - /* Center in the panel when there's spare room; auto margins collapse to 0 when - the content overflows, so it top-aligns and scrolls without clipping (unlike - justify-content: center, which would clip the top). */ - margin-block: auto; } .section { diff --git a/components/grid-wallet-demo/src/components/FlowPicker/FlowPicker.module.scss b/components/grid-wallet-demo/src/components/FlowPicker/FlowPicker.module.scss index 9aef3545..e6ea452e 100644 --- a/components/grid-wallet-demo/src/components/FlowPicker/FlowPicker.module.scss +++ b/components/grid-wallet-demo/src/components/FlowPicker/FlowPicker.module.scss @@ -1,17 +1,29 @@ @use '@lightsparkdev/origin/src/tokens/text-styles' as *; +@use '../../styles/breakpoints' as *; .group { corner-shape: var(--corner-shape); + /* Single source for the tile surface — press states mix from it, and the + wide layout steps it down to sit on the chrome-gray panel. */ + --tile-bg: var(--surface-base); display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; - background: var(--surface-base); + background: var(--tile-bg); border: var(--stroke-xs) solid var(--border-primary); border-radius: var(--corner-radius-md); overflow: clip; :global([data-theme='dark']) & { - background: var(--color-gray-925); + --tile-bg: var(--color-gray-925); + } + + @media (min-width: $breakpoint-layout-wide) { + --tile-bg: var(--surface-primary); + + :global([data-theme='dark']) & { + --tile-bg: var(--color-gray-950); + } } } @@ -23,15 +35,11 @@ gap: var(--spacing-sm); min-width: 0; padding: var(--spacing-md); - background: var(--surface-base); + background: var(--tile-bg); border-bottom: var(--stroke-xs) solid var(--border-primary); cursor: default; transition: background 100ms ease; - :global([data-theme='dark']) & { - background: var(--color-gray-925); - } - /* Left column of each pair. */ &:nth-child(odd) { border-right: var(--stroke-xs) solid var(--border-primary); @@ -49,9 +57,9 @@ // Hover only with a real pointer — avoids the fill sticking after a tap on touch. @media (hover: hover) { &:hover { - // Opaque equivalent of alpha-black-02 on surface-base — transparent tokens - // composite differently depending on parent bg (see use case/auth vs flow). - background: color-mix(in srgb, var(--surface-base) 98%, black); + // Opaque equivalent of alpha-black-02 on the tile surface — transparent + // tokens composite differently depending on parent bg. + background: color-mix(in srgb, var(--tile-bg) 98%, black); :global([data-theme='dark']) & { background: var(--color-alpha-white-04); @@ -61,7 +69,7 @@ // Tap/press feedback — works on touch and resets on release (unlike :hover). &:active { - background: color-mix(in srgb, var(--surface-base) 98%, black); + background: color-mix(in srgb, var(--tile-bg) 98%, black); :global([data-theme='dark']) & { background: var(--color-alpha-white-04); diff --git a/components/grid-wallet-demo/src/components/UseCasePicker/UseCasePicker.module.scss b/components/grid-wallet-demo/src/components/UseCasePicker/UseCasePicker.module.scss index de8c01c5..5465f730 100644 --- a/components/grid-wallet-demo/src/components/UseCasePicker/UseCasePicker.module.scss +++ b/components/grid-wallet-demo/src/components/UseCasePicker/UseCasePicker.module.scss @@ -13,18 +13,29 @@ $ease-out-swift: cubic-bezier(0.175, 0.885, 0.32, 1.1); .group { corner-shape: var(--corner-shape); + /* Single source for the tile surface — press states mix from it, and the + wide layout steps it down to sit on the chrome-gray panel. */ + --tile-bg: var(--surface-base); position: relative; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; - background: var(--surface-base); + background: var(--tile-bg); border: var(--stroke-xs) solid var(--border-primary); border-radius: var(--corner-radius-md); // Ring extends past cell bounds (and morphs across cells during layout animation). overflow: visible; :global([data-theme='dark']) & { - background: var(--color-gray-925); + --tile-bg: var(--color-gray-925); + } + + @media (min-width: $breakpoint-layout-wide) { + --tile-bg: var(--surface-primary); + + :global([data-theme='dark']) & { + --tile-bg: var(--color-gray-950); + } } } @@ -37,16 +48,12 @@ $ease-out-swift: cubic-bezier(0.175, 0.885, 0.32, 1.1); align-items: center; min-width: 0; padding: var(--spacing-md); - background: var(--surface-base); + background: var(--tile-bg); border-bottom: var(--stroke-xs) solid var(--border-primary); overflow: visible; cursor: pointer; transition: background 100ms ease; - :global([data-theme='dark']) & { - background: var(--color-gray-925); - } - &:nth-child(odd) { border-right: var(--stroke-xs) solid var(--border-primary); } @@ -84,7 +91,7 @@ $ease-out-swift: cubic-bezier(0.175, 0.885, 0.32, 1.1); // tap on touch (mobile emulates :hover until you tap elsewhere). @media (hover: hover) { &:not(:disabled):not(.cardSelected):hover { - background: color-mix(in srgb, var(--surface-base) 98%, black); + background: color-mix(in srgb, var(--tile-bg) 98%, black); :global([data-theme='dark']) & { background: var(--color-alpha-white-04); @@ -95,7 +102,7 @@ $ease-out-swift: cubic-bezier(0.175, 0.885, 0.32, 1.1); // Tap/press feedback — applies on touch too and resets on release (unlike // :hover, which sticks after a tap). Same fill as the hover state. &:not(:disabled):not(.cardSelected):active { - background: color-mix(in srgb, var(--surface-base) 98%, black); + background: color-mix(in srgb, var(--tile-bg) 98%, black); :global([data-theme='dark']) & { background: var(--color-alpha-white-04); diff --git a/components/grid-wallet-demo/src/components/glass-gl/StageGL.tsx b/components/grid-wallet-demo/src/components/glass-gl/StageGL.tsx index c3d6712a..32b77bce 100644 --- a/components/grid-wallet-demo/src/components/glass-gl/StageGL.tsx +++ b/components/grid-wallet-demo/src/components/glass-gl/StageGL.tsx @@ -769,6 +769,9 @@ export const StageGL = forwardRef(function StageGL( // static dot field can show immediately. paintBootFrame(); ro = new ResizeObserver(() => { + // The palette is media-query-dependent (the light stage color changes + // at the wide breakpoint), so a resize can change it too. + palette = readDotGridPalette(offCtx); markResize(); lastRadius = NaN; invalidate(true); diff --git a/components/grid-wallet-demo/src/hooks/useColumnResize.ts b/components/grid-wallet-demo/src/hooks/useColumnResize.ts index d6e11576..6c4bd9a9 100644 --- a/components/grid-wallet-demo/src/hooks/useColumnResize.ts +++ b/components/grid-wallet-demo/src/hooks/useColumnResize.ts @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; // Matches $layout-laptop-config-width — the compact configure column is the // default at every breakpoint. @@ -9,80 +9,101 @@ const MIN_APP = 320; // Never resize the API column below the configure column width; dragging only // widens it from there. const MIN_API = CONFIGURE_WIDTH; +// The DEFAULT width + snap target: the docs sidebar plus the configure column — +// the combined chrome flanking the embedded playground. Standalone (and until +// the docs parent reports in) this assumes the expanded sidebar: 280px +// (--ls-sidebar-width) + 400 = 680 = $layout-laptop-api-content-max-width. +// Embedded, the docs page posts `nav-sync` with the live sidebar width (48px +// rail when collapsed, drag-resized values too) and the default follows. +// Dragging can still grow the panel freely up to (middle − MIN_APP). +const DOCS_SIDEBAR_DEFAULT = 280; const SNAP_THRESHOLD = 28; -/** SSR/first-paint fallback — replaced by the 50/50 split once the layout - * measures (pre-paint, in the layout effect below). */ -export const API_DEFAULT_WIDTH = CONFIGURE_WIDTH; +/** SSR/first-paint fallback — replaced by the chrome-width default once the + * layout measures (pre-paint, in the layout effect below). */ +export const API_DEFAULT_WIDTH = DOCS_SIDEBAR_DEFAULT + CONFIGURE_WIDTH; function clampApiWidth(width: number, totalMiddle: number): number { return Math.max(MIN_API, Math.min(totalMiddle - MIN_APP, width)); } -/** Default + snap target — the app and API panels split the space right of - * the configure column 50/50. */ -function defaultApiWidth(totalMiddle: number): number { - return clampApiWidth(Math.round(totalMiddle / 2), totalMiddle); -} - export function useColumnResize() { const layoutRef = useRef(null); const apiColRef = useRef(null); + const [defaultApi, setDefaultApi] = useState(API_DEFAULT_WIDTH); const [apiWidth, setApiWidth] = useState(API_DEFAULT_WIDTH); - // While the column sits at the default split, window resizes keep it AT the - // split (both panels breathe together). A custom drag pins it to a px width - // until the user snaps it back to the split. + // While the column sits at the default, window resizes (and nav-sync + // updates) keep it there. A custom drag pins it to a px width until the + // user snaps it back to the default. const userResized = useRef(false); + // Embed contract (same shape as theme-sync): the docs page reports its + // sidebar width on request and whenever it changes (collapse / drag-resize). + useEffect(() => { + const onMessage = (e: MessageEvent) => { + if (e.data && e.data.type === 'nav-sync' && typeof e.data.sidebarWidth === 'number') { + setDefaultApi(Math.round(e.data.sidebarWidth) + CONFIGURE_WIDTH); + } + }; + window.addEventListener('message', onMessage); + if (window.parent && window.parent !== window) { + window.parent.postMessage({ type: 'nav-request' }, '*'); + } + return () => window.removeEventListener('message', onMessage); + }, []); + useLayoutEffect(() => { const fitToLayout = () => { const layout = layoutRef.current; if (!layout) return; const totalMiddle = layout.getBoundingClientRect().width - CONFIGURE_WIDTH; setApiWidth((w) => - userResized.current ? clampApiWidth(w, totalMiddle) : defaultApiWidth(totalMiddle), + clampApiWidth(userResized.current ? w : defaultApi, totalMiddle), ); }; fitToLayout(); window.addEventListener('resize', fitToLayout); return () => window.removeEventListener('resize', fitToLayout); - }, []); + }, [defaultApi]); - const onResizeStart = useCallback((e: React.MouseEvent) => { - e.preventDefault(); - const startX = e.clientX; - const startApiWidth = apiColRef.current?.getBoundingClientRect().width ?? API_DEFAULT_WIDTH; + const onResizeStart = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + const startX = e.clientX; + const startApiWidth = apiColRef.current?.getBoundingClientRect().width ?? defaultApi; - document.body.style.cursor = 'col-resize'; - document.body.style.userSelect = 'none'; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; - const applyWidth = (clientX: number) => { - const layout = layoutRef.current; - if (!layout) return; - const totalMiddle = layout.getBoundingClientRect().width - CONFIGURE_WIDTH; - const delta = clientX - startX; - const raw = clampApiWidth(startApiWidth - delta, totalMiddle); - const target = defaultApiWidth(totalMiddle); - const snapped = Math.abs(raw - target) <= SNAP_THRESHOLD ? target : raw; - // Snapping back to the split resumes 50/50 tracking on window resize. - userResized.current = snapped !== target; - setApiWidth(snapped); - }; + const applyWidth = (clientX: number) => { + const layout = layoutRef.current; + if (!layout) return; + const totalMiddle = layout.getBoundingClientRect().width - CONFIGURE_WIDTH; + const delta = clientX - startX; + const raw = clampApiWidth(startApiWidth - delta, totalMiddle); + const target = clampApiWidth(defaultApi, totalMiddle); + const snapped = Math.abs(raw - target) <= SNAP_THRESHOLD ? target : raw; + // Snapping back to the default resumes default tracking on resize. + userResized.current = snapped !== target; + setApiWidth(snapped); + }; - const onMove = (ev: MouseEvent) => applyWidth(ev.clientX); + const onMove = (ev: MouseEvent) => applyWidth(ev.clientX); - const onUp = (ev: MouseEvent) => { - applyWidth(ev.clientX); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - }; + const onUp = (ev: MouseEvent) => { + applyWidth(ev.clientX); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + }; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - }, []); + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }, + [defaultApi], + ); return { layoutRef, apiColRef, apiWidth, onResizeStart }; } diff --git a/mintlify/snippets/global-accounts/wallet-demo-embed.mdx b/mintlify/snippets/global-accounts/wallet-demo-embed.mdx index ebb52722..84316ced 100644 --- a/mintlify/snippets/global-accounts/wallet-demo-embed.mdx +++ b/mintlify/snippets/global-accounts/wallet-demo-embed.mdx @@ -21,6 +21,20 @@ export const WalletDemoEmbed = () => { if (window.__wdHideTimer) { clearTimeout(window.__wdHideTimer); window.__wdHideTimer = null; } host.style.display = 'block'; + const navWidth = () => { + const root = document.documentElement; + const collapsed = root.classList.contains('ls-nav-collapsed'); + const cs = getComputedStyle(root); + const v = parseFloat(cs.getPropertyValue(collapsed ? '--ls-nav-rail-w' : '--ls-sidebar-width')); + return Number.isFinite(v) ? v : (collapsed ? 48 : 280); + }; + const sendNav = () => { + const iframe = document.getElementById('wallet-demo-iframe'); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage({ type: 'nav-sync', sidebarWidth: navWidth() }, '*'); + } + }; + let raf = 0; let last = { left: -1, top: -1, width: -1, height: -1 }; const sync = () => { @@ -33,6 +47,7 @@ export const WalletDemoEmbed = () => { host.style.width = r.width + 'px'; host.style.height = r.height + 'px'; last = { left: r.left, top: r.top, width: r.width, height: r.height }; + sendNav(); } } raf = requestAnimationFrame(sync); @@ -53,6 +68,10 @@ export const WalletDemoEmbed = () => { sendTheme(); return; } + if (e.data && e.data.type === 'nav-request') { + sendNav(); + return; + } if (e.data && e.data.type === 'theme-sync') { const wantsDark = e.data.theme === 'dark'; if (isDark() !== wantsDark) { @@ -64,12 +83,14 @@ export const WalletDemoEmbed = () => { window.addEventListener('message', handleMessage); const obs = new MutationObserver(() => { + sendNav(); if (ignoreNextMutation) { ignoreNextMutation = false; return; } sendTheme(); }); obs.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); sendTheme(); + sendNav(); requestAnimationFrame(sendTheme); return () => { From b6b8aa8d706ddc49e91484462ec5683b27f92c74 Mon Sep 17 00:00:00 2001 From: Pat Capulong Date: Mon, 6 Jul 2026 23:03:07 -0700 Subject: [PATCH 6/9] Playground: graceful code-panel behavior around docs sidebar toggles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embed reports nav state directionally — immediately when collapsing (invisible while stacked, lets the width glide run in parallel with the wipe), on settle when expanding (kills the wide-panel flash before the layout flip). The API column eases programmatic width changes (240ms, wide layout only, never during a drag), and snaps at stacked ⇄ 3-col breakpoint crossings via a matchMedia listener so entering 3-col doesn't read as a giant shrink sweep. Co-authored-by: Cursor --- .../grid-wallet-demo/src/app/page.module.scss | 15 ++++++++++ components/grid-wallet-demo/src/app/page.tsx | 3 +- .../src/hooks/useColumnResize.ts | 30 ++++++++++++++++++- .../global-accounts/wallet-demo-embed.mdx | 4 +++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/components/grid-wallet-demo/src/app/page.module.scss b/components/grid-wallet-demo/src/app/page.module.scss index fb557909..2d1f2964 100644 --- a/components/grid-wallet-demo/src/app/page.module.scss +++ b/components/grid-wallet-demo/src/app/page.module.scss @@ -47,6 +47,21 @@ overflow: hidden; } +/* Side-by-side: ease programmatic width changes (the default following a docs + sidebar collapse/expand) — but never a live drag, which must track 1:1. + Scoped to the wide layout so the stacked breakpoints (width: 100%) flip + instantly with the media query. */ +@media (min-width: $breakpoint-layout-wide) { + .apiCol { + transition: width 240ms cubic-bezier(0.25, 0.1, 0.25, 1); + } + + .apiCol[data-resizing], + .apiCol[data-snap] { + transition: none; + } +} + /* Laptop: configure left, app over API on the right. Floored at the mobile breakpoint so these rules never leak into the mobile single-column layout. */ @media (min-width: ($breakpoint-layout-mobile + 1px)) and (max-width: ($breakpoint-layout-wide - 1px)) { diff --git a/components/grid-wallet-demo/src/app/page.tsx b/components/grid-wallet-demo/src/app/page.tsx index c9b4865b..37453a85 100644 --- a/components/grid-wallet-demo/src/app/page.tsx +++ b/components/grid-wallet-demo/src/app/page.tsx @@ -33,7 +33,7 @@ function withViewTransition(update: () => void) { export default function Page() { const logic = useWalletDemoLogic(); - const { layoutRef, apiColRef, apiWidth, onResizeStart } = useColumnResize(); + const { layoutRef, apiColRef, apiWidth, resizing, onResizeStart } = useColumnResize(); // Mobile only (<=767): the layout collapses to one view at a time. On desktop // this stays 'configure' forever (the switch is gated to the mobile viewport), @@ -153,6 +153,7 @@ export default function Page() {
diff --git a/components/grid-wallet-demo/src/hooks/useColumnResize.ts b/components/grid-wallet-demo/src/hooks/useColumnResize.ts index 6c4bd9a9..91a77d92 100644 --- a/components/grid-wallet-demo/src/hooks/useColumnResize.ts +++ b/components/grid-wallet-demo/src/hooks/useColumnResize.ts @@ -32,11 +32,37 @@ export function useColumnResize() { const apiColRef = useRef(null); const [defaultApi, setDefaultApi] = useState(API_DEFAULT_WIDTH); const [apiWidth, setApiWidth] = useState(API_DEFAULT_WIDTH); + // True while the user drags the divider — the column's width transition is + // disabled so the drag tracks the pointer 1:1 (it only eases programmatic + // changes, e.g. the default following a docs-sidebar collapse). + const [resizing, setResizing] = useState(false); // While the column sits at the default, window resizes (and nav-sync // updates) keep it there. A custom drag pins it to a px width until the // user snaps it back to the default. const userResized = useRef(false); + // Crossing the stacked ⇄ side-by-side breakpoint must SNAP, not glide: the + // column arrives from a full-width stacked layout, and easing that change + // reads as a giant shrink sweep. Kill the transition for a beat around the + // crossing (direct DOM write — React state could land a frame late, after + // the transition has already started). Width changes WITHIN the wide layout + // (nav-sync defaults) keep the ease. + useEffect(() => { + // Matches $breakpoint-layout-wide. + const mql = window.matchMedia('(min-width: 1440px)'); + let timer = 0; + const onChange = () => { + apiColRef.current?.setAttribute('data-snap', ''); + window.clearTimeout(timer); + timer = window.setTimeout(() => apiColRef.current?.removeAttribute('data-snap'), 120); + }; + mql.addEventListener('change', onChange); + return () => { + mql.removeEventListener('change', onChange); + window.clearTimeout(timer); + }; + }, []); + // Embed contract (same shape as theme-sync): the docs page reports its // sidebar width on request and whenever it changes (collapse / drag-resize). useEffect(() => { @@ -75,6 +101,7 @@ export function useColumnResize() { document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; + setResizing(true); const applyWidth = (clientX: number) => { const layout = layoutRef.current; @@ -95,6 +122,7 @@ export function useColumnResize() { applyWidth(ev.clientX); document.body.style.cursor = ''; document.body.style.userSelect = ''; + setResizing(false); document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; @@ -105,5 +133,5 @@ export function useColumnResize() { [defaultApi], ); - return { layoutRef, apiColRef, apiWidth, onResizeStart }; + return { layoutRef, apiColRef, apiWidth, resizing, onResizeStart }; } diff --git a/mintlify/snippets/global-accounts/wallet-demo-embed.mdx b/mintlify/snippets/global-accounts/wallet-demo-embed.mdx index 84316ced..ef139842 100644 --- a/mintlify/snippets/global-accounts/wallet-demo-embed.mdx +++ b/mintlify/snippets/global-accounts/wallet-demo-embed.mdx @@ -29,6 +29,10 @@ export const WalletDemoEmbed = () => { return Number.isFinite(v) ? v : (collapsed ? 48 : 280); }; const sendNav = () => { + const html = document.documentElement; + const animating = html.classList.contains('ls-nav-animating'); + const collapsed = html.classList.contains('ls-nav-collapsed'); + if (animating && !collapsed) return; const iframe = document.getElementById('wallet-demo-iframe'); if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage({ type: 'nav-sync', sidebarWidth: navWidth() }, '*'); From ba6151be63bc467221add6248709fba7a21ff63e Mon Sep 17 00:00:00 2001 From: Pat Capulong Date: Tue, 7 Jul 2026 00:18:23 -0700 Subject: [PATCH 7/9] Playground: drop the nav veil, fix the stacked-layout boot flash and the breakpoint-crossing column crush The stacked/3-col attribute now lives on , set pre-paint by the same inline script as data-theme, so narrow viewports no longer flash the SSR 3-col default while JS loads. The veil choreography is removed on both sides (demo + docs toggle). The code panel's width ease now arms only when the painted width matches the tracked inline width, so nav-sync messages racing the stacked->wide flip can't start the transition from the full-row stacked width and crush the app column. Co-authored-by: Cursor --- .../grid-wallet-demo/src/app/globals.scss | 16 ++--- .../grid-wallet-demo/src/app/layout.tsx | 8 ++- .../grid-wallet-demo/src/app/page.module.scss | 67 ++++++++++--------- components/grid-wallet-demo/src/app/page.tsx | 23 ++++++- .../components/ApiPanel/ApiPanel.module.scss | 63 ++++++++--------- .../components/AppPanel/AppPanel.module.scss | 3 + .../AuthMethodPicker.module.scss | 2 + .../ConfigurePanel/ConfigurePanel.module.scss | 15 +++-- .../FlowPicker/FlowPicker.module.scss | 2 + .../UseCasePicker/UseCasePicker.module.scss | 2 + .../src/hooks/useColumnResize.ts | 56 +++++++++------- .../src/styles/breakpoints.scss | 12 ++-- 12 files changed, 160 insertions(+), 109 deletions(-) diff --git a/components/grid-wallet-demo/src/app/globals.scss b/components/grid-wallet-demo/src/app/globals.scss index 04dd23c5..c24d8d4b 100644 --- a/components/grid-wallet-demo/src/app/globals.scss +++ b/components/grid-wallet-demo/src/app/globals.scss @@ -155,14 +155,14 @@ body { --dot-grid-dot-peak: #b8b8b3; } -/* 3-col layout (>= $breakpoint-layout-wide): the light stage steps down a hair - so it separates from the raised cards flanking it. Declared BEFORE the dark - block below so dark mode (same specificity, later in source) still wins. - The JS canvas re-reads the token on resize (StageGL's ResizeObserver). */ -@media (min-width: 1440px) { - :root { - --dot-grid-bg: #f4f4f3; - } +/* 3-col layout: the light stage steps down a hair so it separates from the + raised cards flanking it. Keyed to html's data-layout attribute (set + pre-paint by layout.tsx, kept live by page.tsx) so the color rides the + same clock as the arrangement flip. Light-only — the dark stage is + gray-950 in both arrangements. The JS canvas re-reads the token on resize + (StageGL's ResizeObserver), which every arrangement flip triggers. */ +html:not([data-theme='dark']):not([data-layout='stacked']) { + --dot-grid-bg: #f4f4f3; } :root { diff --git a/components/grid-wallet-demo/src/app/layout.tsx b/components/grid-wallet-demo/src/app/layout.tsx index c27f8e1b..ba9e727d 100644 --- a/components/grid-wallet-demo/src/app/layout.tsx +++ b/components/grid-wallet-demo/src/app/layout.tsx @@ -44,7 +44,12 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - {/* Set data-embed and an initial data-theme before paint to avoid a flash. */} + {/* Set data-embed, an initial data-theme, and the stacked ⇄ 3-col + data-layout before paint to avoid a flash. The layout attribute + must land pre-paint (not in a React effect): the SSR HTML paints + long before hydration, and the wide default would flash 3-col on + stacked viewports while the JS loads. 1600 matches + $breakpoint-layout-wide. */}