Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
60ddaa9
chore(docs): vendor interactive demos for finder modal
pratyush618 Jun 21, 2026
bfce5ff
feat(docs): add scenario finder to homepage
pratyush618 Jun 21, 2026
6afcdd5
fix(docs): use font-display optional to kill landing CLS
pratyush618 Jun 21, 2026
c14faa1
fix(docs): reserve height for lazy page chunk to kill CLS
pratyush618 Jun 21, 2026
2ea820f
docs(node): add examples section + wire into nav
pratyush618 Jun 21, 2026
aff0d57
fix(docs): contain landing hero/finder overflow on mobile
pratyush618 Jun 21, 2026
20608d9
feat(docs): mobile sidebar drawer with collapsible subsections
pratyush618 Jun 21, 2026
3a2e796
fix(docs): drop no-op self-replace in mesh demo stroke
pratyush618 Jun 21, 2026
ab4fb80
fix(docs): verify origin in demo postMessage handler
pratyush618 Jun 21, 2026
a76f00c
fix(docs): trap Tab focus inside the demo modal
pratyush618 Jun 21, 2026
7a8c20f
fix(docs): drop closed mobile drawer from tab order
pratyush618 Jun 21, 2026
0076616
fix(docs): scope demo postMessage to parent origin, fix sim count
pratyush618 Jun 21, 2026
03de3ed
fix(docs): sync work-steal pause label on motion resume
pratyush618 Jun 21, 2026
35a8662
docs: add language to demos README fenced block
pratyush618 Jun 21, 2026
05ce318
docs(node): switch scraper example to fan-out/fan-in
pratyush618 Jun 21, 2026
97aa9a7
docs(node): fix benchmark wording + digest timezone
pratyush618 Jun 21, 2026
5e88059
fix(docs): font-display optional + wire standalone theme toggle
pratyush618 Jun 21, 2026
a373bd0
fix(docs): snap burst tokens into static rate-limit frame
pratyush618 Jun 21, 2026
c778d8a
fix(docs): clear stale RAF so motion toggle doesn't auto-finish
pratyush618 Jun 21, 2026
288c84d
docs(node): make bulk-emails send command runnable
pratyush618 Jun 21, 2026
5394051
fix(docs): expose demo toggle state via aria-pressed
pratyush618 Jun 21, 2026
7141ce4
refactor(docs): make demo host embed-only, drop standalone page
pratyush618 Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 50 additions & 23 deletions docs/app/components/docs/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ function NavLink({ node, current }: { node: NavNode; current: string }) {
);
}

/** A subsection with children — collapsible, auto-opens around the active page. */
/** A subsection with children — collapsible at any depth, auto-opens around the
* active page. The label itself toggles (same behaviour as a top-level group),
* so a subsection-of-a-subsection expands/collapses on click just like its parent. */
function NavSection({ node, current }: { node: NavNode; current: string }) {
const active = node.children?.some((c) => containsHref(c, current)) ?? false;
const [open, setOpen] = useState(active);
Expand All @@ -112,15 +114,21 @@ function NavSection({ node, current }: { node: NavNode; current: string }) {
setOpen(true);
}
}, [active]);
const toggle = () => setOpen((o) => !o);
return (
<div className="nav-subsection">
<div className="nav-sub-head">
<NavLink node={node} current={current} />
<Caret
open={open}
onToggle={() => setOpen((o) => !o)}
title={node.title}
/>
<button
type="button"
className={`nav-item nav-sub-toggle ${
node.href === current ? "active" : ""
}`.trim()}
aria-expanded={open}
onClick={toggle}
>
{node.title}
</button>
<Caret open={open} onToggle={toggle} title={node.title} />
</div>
{open ? <NavTree nodes={node.children ?? []} current={current} /> : null}
</div>
Expand Down Expand Up @@ -189,25 +197,44 @@ function NavGroup({ group, current }: { group: NavNode; current: string }) {
);
}

export function Sidebar({ onSearch }: { onSearch?: () => void }) {
export function Sidebar({
onSearch,
open = false,
onClose,
}: {
onSearch?: () => void;
/** Drawer open state — only affects the mobile (≤860px) overlay layout. */
open?: boolean;
onClose?: () => void;
}) {
const { pathname } = useLocation();
const current = pathname.replace(/\/$/, "") || "/";
const sdk = useActiveSdk();
return (
<aside className="sidebar">
<button type="button" className="side-search" onClick={onSearch}>
Search docs
<span className="sk">
<kbd>⌘</kbd>
<kbd>K</kbd>
</span>
</button>
<SdkSwitch sdk={sdk} current={current} />
<nav id="sidenav">
{navForSdk(sdk).map((group) => (
<NavGroup key={group.title} group={group} current={current} />
))}
</nav>
</aside>
<>
{/* Backdrop sits under the drawer on mobile; tapping it closes the menu. */}
<button
type="button"
className={`sidebar-backdrop ${open ? "open" : ""}`.trim()}
aria-label="Close navigation menu"
tabIndex={open ? 0 : -1}
onClick={onClose}
/>
<aside className={`sidebar ${open ? "open" : ""}`.trim()}>
<button type="button" className="side-search" onClick={onSearch}>
Search docs
<span className="sk">
<kbd>⌘</kbd>
<kbd>K</kbd>
</span>
</button>
<SdkSwitch sdk={sdk} current={current} />
<nav id="sidenav">
{navForSdk(sdk).map((group) => (
<NavGroup key={group.title} group={group} current={current} />
))}
</nav>
</aside>
</>
);
}
192 changes: 192 additions & 0 deletions docs/app/components/landing/demo-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { useEffect, useRef, useState } from "react";
import { useThemeMode } from "@/lib/theme";

/** A live demo the finder can open: the embed id + a human title for the bar. */
export interface DemoTarget {
/** Demo id understood by `demos/interactive.html?embed=` (e.g. "ratelimit"). */
id: string;
/** Title shown in the modal bar. */
title: string;
}

function prefersReducedMotion(): boolean {
if (typeof window === "undefined") return false;
return Boolean(
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches,
);
}

const PLAY_ICON = (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m8 5 11 7-11 7V5z" />
</svg>
);

const CLOSE_ICON = (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
);

/**
* Centered overlay that opens a finder scenario's live demo in an `<iframe>`,
* pointed at the vendored `demos/interactive.html` embed mode (theme inherited
* from the host). React port of `INTEGRATION-scenario-finder.md` §6: focus moves
* to the close button on open and restores on close, Escape + backdrop close,
* background scroll is locked, and the enter transition is motion-aware.
*
* `demo === null` keeps the modal closed; a brief exit transition plays before
* the iframe unmounts so the demo stops running.
*/
export function DemoModal({
demo,
onClose,
}: {
demo: DemoTarget | null;
onClose: () => void;
}) {
const theme = useThemeMode();
// Mounted target (survives the exit transition after `demo` goes null).
const [current, setCurrent] = useState<DemoTarget | null>(demo);
// Drives the `.open` class — toggled a frame after mount so the enter plays.
const [shown, setShown] = useState(false);
const [loading, setLoading] = useState(true);
const closeRef = useRef<HTMLButtonElement>(null);
const dialogRef = useRef<HTMLDivElement>(null);
const restoreRef = useRef<HTMLElement | null>(null);

useEffect(() => {
if (demo) {
restoreRef.current = (document.activeElement as HTMLElement) ?? null;
setCurrent(demo);
setLoading(true);
const raf = requestAnimationFrame(() => setShown(true));
return () => cancelAnimationFrame(raf);
}
setShown(false);
const timer = setTimeout(
() => setCurrent(null),
prefersReducedMotion() ? 0 : 260,
);
return () => clearTimeout(timer);
}, [demo]);

// While a demo is mounted: lock scroll, close on Escape, and trap Tab focus
// inside the dialog so keyboard users can't reach background content.
useEffect(() => {
if (!current) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
return;
}
if (e.key !== "Tab") return;
const dialog = dialogRef.current;
if (!dialog) return;
const focusable = dialog.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), iframe, [tabindex]:not([tabindex="-1"])',
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (!first || !last) return;
const active = document.activeElement;
if (e.shiftKey && active === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && active === last) {
e.preventDefault();
first.focus();
}
};
document.addEventListener("keydown", onKey);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = "";
};
}, [current, onClose]);

// Move focus into the dialog on open; restore it to the trigger on close.
useEffect(() => {
if (shown) closeRef.current?.focus();
}, [shown]);
useEffect(() => {
if (!current && restoreRef.current) {
restoreRef.current.focus?.();
restoreRef.current = null;
}
}, [current]);

if (!current) return null;

// `import.meta.env.BASE_URL` ends with "/" and respects DOCS_BASE_PATH.
const src = `${import.meta.env.BASE_URL}demos/interactive.html?embed=${current.id}&theme=${theme}&accent=brand#${current.id}`;

return (
<div className={`dm-overlay${shown ? " open" : ""}`} aria-hidden={!shown}>
<button
type="button"
className="dm-backdrop"
tabIndex={-1}
aria-hidden="true"
onClick={onClose}
/>
<div
className="dm-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="dm-title"
ref={dialogRef}
>
<div className="dm-bar">
<span className="dm-mark">{PLAY_ICON}</span>
<div className="dm-meta">
<span className="dm-eyebrow">Live demo</span>
<span className="dm-title" id="dm-title">
{current.title}
</span>
</div>
<div className="dm-actions">
<button
type="button"
className="dm-x"
ref={closeRef}
onClick={onClose}
aria-label="Close demo"
>
{CLOSE_ICON}
</button>
</div>
</div>
<div className="dm-stage">
<div className={`dm-loading${loading ? "" : " hide"}`}>
<span className="dm-spin" />
Loading demo…
</div>
<iframe
className="dm-frame"
title={`${current.title} — interactive demo`}
src={src}
onLoad={() => setLoading(false)}
/>
</div>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions docs/app/components/landing/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { Footer } from "./footer";
export { Hero } from "./hero";
export { useReveal } from "./reveal";
export { ScenarioFinder } from "./scenario-finder";
export {
Comparison,
CTA,
Expand Down
Loading