import { Box } from '@luke-ui/react/box'; import { Button } from '@luke-ui/react/button'; import type { IconName } from '@luke-ui/react/icon'; import { createIcon, Icon } from '@luke-ui/react/icon'; import { LoadingSkeleton } from '@luke-ui/react/loading-skeleton'; import { LoadingSpinner } from '@luke-ui/react/loading-spinner'; import { ScrollFade } from '@luke-ui/react/scroll-fade'; import { Text } from '@luke-ui/react/text'; import { deriveNestedRadius, vars } from '@luke-ui/react/theme'; import { cx } from '@luke-ui/react/utils'; import type { ComponentType, JSX, ReactNode } from 'react'; import { Suspense, use, useEffect, useId, useRef, useState } from 'react'; import type { GroupImperativeHandle } from 'react-resizable-panels'; import { Group, Panel, Separator } from 'react-resizable-panels'; import type { HighlightedSource } from '../lib/highlighted-source.js'; import { StoryWrapper } from '../lib/story-wrapper.js'; import { CodeBlock } from './code-block/code-block.js'; import { DocsLink } from './docs-link.js'; import { useIsDesktop } from './playground/use-is-desktop.js'; // The frame, header, preview, and code block nest one border's gap inside // `OUTER_RADIUS`, so their corners stay concentric with the frame's own. const OUTER_RADIUS = vars.radius.control; const INNER_RADIUS = deriveNestedRadius(OUTER_RADIUS, '1px'); type ExampleBlockProps = { src: string; title: string; layout?: 'flow' | 'centered' | 'full-bleed'; }; export function ExampleBlock(props: ExampleBlockProps): JSX.Element { return ( }> ); } function ExampleContent({ layout, src, title }: ExampleBlockProps): JSX.Element { const slashIndex = src.indexOf('/'); const component = src.slice(0, slashIndex); const name = src.slice(slashIndex + 1); const result = use(loadExample(component, name)); const [showCode, setShowCode] = useState(false); const codeId = useId(); if (!result.ok) { return ( Failed to load example {component}/{name}: {result.error.message} ); } const [PreviewComponent, highlightedSource] = result.data; return ( {highlightedSource.playgroundHash != null ? ( ) : null} setShowCode((prev) => !prev)} /> } title={title} > {showCode ? ( {/* Shiki escapes the source before the Vite plugin generates this HTML. */} ) : null} ); } export function ExampleLoadingState({ layout, title, }: Pick) { const loadingLabel = `Loading ${title} example`; const isFullBleed = layout === 'full-bleed'; return ( } ariaLabel={loadingLabel} title={title}> ); } // The Tailwind classes in ExamplePreview repeat these values, so change both together: // `md:` is DESKTOP_MEDIA_QUERY, `@[640px]/example-preview-card` is MIN_RESIZABLE_CARD_WIDTH, // `pe-6` is RESIZE_GUTTER_WIDTH, and `min-inline-3!` is OUTSIDE_STRIP_WIDTH. const MIN_RESIZABLE_CARD_WIDTH = 640; const MIN_PREVIEW_WIDTH = 320; const RESIZE_GUTTER_WIDTH = 24; /** Half the grip's inline size, so the grip stays inside the card at full width. */ const OUTSIDE_STRIP_WIDTH = 12; // Larger than the playground's `RESIZE_TARGET_MINIMUM_SIZE` so the grip is easier to hit. const EXAMPLE_RESIZE_TARGET_MINIMUM_SIZE = { coarse: 32, fine: 32 }; export function ExamplePreview({ children, layout, title, }: { children: ReactNode; layout?: ExampleBlockProps['layout']; title: string; }) { const isDesktop = useIsDesktop(); const groupElement = useRef(null); const groupHandle = useRef(null); const [cardWidth, setCardWidth] = useState(0); const previewId = useId(); const outsideId = useId(); const isResizable = isDesktop && cardWidth >= MIN_RESIZABLE_CARD_WIDTH; useEffect(() => { const element = groupElement.current; if (!element) return; const observer = new ResizeObserver(() => setCardWidth(element.getBoundingClientRect().width)); observer.observe(element); return () => observer.disconnect(); }, []); useEffect(() => { if (!isResizable) groupHandle.current?.setLayout({ [previewId]: 100, [outsideId]: 0 }); }, [isResizable, outsideId, previewId]); return ( {/* This is the nearest inline-size container for a responsive example, so it narrows against the preview width rather than the viewport. The gutter and the separator use container queries, not `isResizable`, so the first paint already has its final width. */}
{children}
); } const GripIcon = createIcon({ path: ( <> ), }); function ExamplePreviewResizeGrip() { return ( ); } function OpenInPlayground({ hash }: { hash: string }) { return ( } target="_blank" to="/playground" > Open in playground ); } function ShowCode({ codeId, isExpanded, onPress, }: { codeId: string; isExpanded: boolean; onPress: () => void; }) { return ( ); } // Mirrors `OpenInPlayground` and `ShowCode`'s visuals without mounting a // router link or wiring up real interaction — this is an `aria-hidden`, // `inert` placeholder, so a plain disabled `Button` is enough for both. function ActionPlaceholder({ children, iconName }: { children: ReactNode; iconName: IconName }) { return ( ); } function ExampleLoadingActions() { return ( Open in playground Show code ); } type ExampleFrameProps = { actions?: ReactNode; ariaLabel?: string; children: ReactNode; title: string; }; function ExampleFrame({ actions, ariaLabel, children, title }: ExampleFrameProps) { const titleId = useId(); return ( {title} {actions} {children} ); } const _modules = import.meta.glob('../examples/*/*.tsx', { eager: false, import: 'default', }); const _highlightedSources = import.meta.glob('../examples/*/*.tsx', { eager: false, import: 'default', query: '?highlight', }); type ExampleLoader = [() => Promise, () => Promise]; type ExampleTuple = [ComponentType, HighlightedSource]; type ExampleResult = | { ok: true; data: ExampleTuple; } | { ok: false; error: Error; }; const exampleCache = new Map>(); function findExample(component: string, name: string): ExampleLoader | null { const key = `../examples/${component}/${name}.tsx`; const loadModule = _modules[key]; const loadHighlightedSource = _highlightedSources[key]; if (!loadModule || !loadHighlightedSource) return null; return [loadModule, loadHighlightedSource]; } function loadExample(component: string, name: string): Promise { const key = `${component}/${name}`; const cached = exampleCache.get(key); if (cached) return cached; const match = findExample(component, name); if (!match) { const promise = Promise.resolve({ error: new Error(`Example not found: ${key}`), ok: false, } satisfies ExampleResult); exampleCache.set(key, promise); return promise; } const [loadModule, loadHighlightedSource] = match; const promise = Promise.all([loadModule(), loadHighlightedSource()]) .then(([loadedComponent, loadedSource]): ExampleResult => ({ data: [loadedComponent, loadedSource], ok: true, })) .catch((err): ExampleResult => ({ error: err instanceof Error ? err : new Error(String(err)), ok: false, })); exampleCache.set(key, promise); return promise; }