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}
.example-preview-grip]:border-fd-muted-foreground/80',
'data-[separator=active]:[&>.example-preview-grip]:border-fd-muted-foreground',
'data-[separator=focus]:[&>.example-preview-grip]:ring-2 data-[separator=focus]:[&>.example-preview-grip]:ring-fd-ring',
)}
disabled={!isResizable}
onDoubleClick={() => {
groupHandle.current?.setLayout({ [previewId]: 100, [outsideId]: 0 });
}}
>
);
}
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 (
}
>
{isExpanded ? 'Hide code' : 'Show code'}
);
}
// 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 (
}>
{children}
);
}
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;
}