From 365efb7814481cc6af34c0a172a60dbe9dd36779 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:08:48 +0530 Subject: [PATCH 01/14] feat(docs): demo registry + RAF/motion hooks, modal iframe fallback --- docs/app/components/demos/index.ts | 3 ++ docs/app/components/demos/lib/index.ts | 2 + docs/app/components/demos/lib/use-raf-loop.ts | 29 ++++++++++++ .../demos/lib/use-reduced-motion.ts | 23 ++++++++++ docs/app/components/demos/registry.ts | 20 +++++++++ docs/app/components/demos/types.ts | 15 +++++++ docs/app/components/landing/demo-modal.tsx | 44 +++++++++++++------ 7 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 docs/app/components/demos/index.ts create mode 100644 docs/app/components/demos/lib/index.ts create mode 100644 docs/app/components/demos/lib/use-raf-loop.ts create mode 100644 docs/app/components/demos/lib/use-reduced-motion.ts create mode 100644 docs/app/components/demos/registry.ts create mode 100644 docs/app/components/demos/types.ts diff --git a/docs/app/components/demos/index.ts b/docs/app/components/demos/index.ts new file mode 100644 index 00000000..99fbcffb --- /dev/null +++ b/docs/app/components/demos/index.ts @@ -0,0 +1,3 @@ +export { useRafLoop, useReducedMotion } from "./lib"; +export { DEMO_COMPONENTS, demoComponent } from "./registry"; +export type { DemoId, DemoProps } from "./types"; diff --git a/docs/app/components/demos/lib/index.ts b/docs/app/components/demos/lib/index.ts new file mode 100644 index 00000000..6e001513 --- /dev/null +++ b/docs/app/components/demos/lib/index.ts @@ -0,0 +1,2 @@ +export { useRafLoop } from "./use-raf-loop"; +export { useReducedMotion } from "./use-reduced-motion"; diff --git a/docs/app/components/demos/lib/use-raf-loop.ts b/docs/app/components/demos/lib/use-raf-loop.ts new file mode 100644 index 00000000..0b41f648 --- /dev/null +++ b/docs/app/components/demos/lib/use-raf-loop.ts @@ -0,0 +1,29 @@ +import { useEffect, useRef } from "react"; + +/** + * Run `callback` once per animation frame while `active` is true, passing the + * high-resolution timestamp. The loop is fully torn down (no dangling frame) + * when `active` flips false or the component unmounts, so a closed demo modal + * never leaves a RAF running. + * + * `callback` is held in a ref, so a demo can pass a fresh closure each render + * without restarting the loop. + */ +export function useRafLoop( + callback: (now: number) => void, + active: boolean, +): void { + const cbRef = useRef(callback); + cbRef.current = callback; + + useEffect(() => { + if (!active) return; + let frame = 0; + const tick = (now: number) => { + cbRef.current(now); + frame = requestAnimationFrame(tick); + }; + frame = requestAnimationFrame(tick); + return () => cancelAnimationFrame(frame); + }, [active]); +} diff --git a/docs/app/components/demos/lib/use-reduced-motion.ts b/docs/app/components/demos/lib/use-reduced-motion.ts new file mode 100644 index 00000000..4deee75e --- /dev/null +++ b/docs/app/components/demos/lib/use-reduced-motion.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; + +const QUERY = "(prefers-reduced-motion: reduce)"; + +/** + * Reactively tracks the user's reduced-motion preference. Demos use it to render + * a single static frame instead of running their animation loop. SSR-safe: + * returns `false` until mounted on the client. + */ +export function useReducedMotion(): boolean { + const [reduced, setReduced] = useState(false); + + useEffect(() => { + if (typeof window === "undefined" || !window.matchMedia) return; + const mq = window.matchMedia(QUERY); + const update = () => setReduced(mq.matches); + update(); + mq.addEventListener("change", update); + return () => mq.removeEventListener("change", update); + }, []); + + return reduced; +} diff --git a/docs/app/components/demos/registry.ts b/docs/app/components/demos/registry.ts new file mode 100644 index 00000000..29402bfc --- /dev/null +++ b/docs/app/components/demos/registry.ts @@ -0,0 +1,20 @@ +import type { ComponentType, LazyExoticComponent } from "react"; +import type { DemoId, DemoProps } from "./types"; + +type LazyDemo = LazyExoticComponent>; + +/** + * React ports of the interactive demos, keyed by demo id and lazy-loaded so the + * homepage bundle stays small. Ids absent here fall back to the vendored + * `interactive.html` iframe in {@link DemoModal} until ported — see + * `tasks/react-demos-plan.md`. Once every id is present, the iframe path and + * `public/demos/` are removed. + */ +export const DEMO_COMPONENTS: Partial> = { + // Populated one entry per demo as each is ported to React. +}; + +/** The React port for `id`, or `undefined` if it still uses the iframe. */ +export function demoComponent(id: string): LazyDemo | undefined { + return DEMO_COMPONENTS[id as DemoId]; +} diff --git a/docs/app/components/demos/types.ts b/docs/app/components/demos/types.ts new file mode 100644 index 00000000..a7ae38ad --- /dev/null +++ b/docs/app/components/demos/types.ts @@ -0,0 +1,15 @@ +/** The interactive demos the homepage scenario finder can open. */ +export type DemoId = + | "ratelimit" + | "recovery" + | "scaling" + | "workflow" + | "mesh" + | "saga" + | "worksteal"; + +/** Props every demo component receives from {@link DemoModal}. */ +export interface DemoProps { + /** Active host theme, so a demo can pick palette variants if it needs to. */ + theme: "light" | "dark"; +} diff --git a/docs/app/components/landing/demo-modal.tsx b/docs/app/components/landing/demo-modal.tsx index 30afcd54..b5b0ff7f 100644 --- a/docs/app/components/landing/demo-modal.tsx +++ b/docs/app/components/landing/demo-modal.tsx @@ -1,9 +1,10 @@ -import { useEffect, useRef, useState } from "react"; +import { Suspense, useEffect, useRef, useState } from "react"; +import { demoComponent } from "@/components/demos"; import { useThemeMode } from "@/lib/theme"; -/** A live demo the finder can open: the embed id + a human title for the bar. */ +/** A live demo the finder can open: the demo id + a human title for the bar. */ export interface DemoTarget { - /** Demo id understood by `demos/interactive.html?embed=` (e.g. "ratelimit"). */ + /** Demo id — a React port (see demos/registry) or an iframe fallback id. */ id: string; /** Title shown in the modal bar. */ title: string; @@ -135,6 +136,8 @@ export function DemoModal({ if (!current) return null; + // Prefer the React port; ids not yet ported render the vendored iframe. + const Demo = demoComponent(current.id); // `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}`; @@ -175,16 +178,31 @@ export function DemoModal({
-
- - Loading demo… -
-