`** - Generated user properties including name, slug, and custom props
+
+See [useCode documentation](../use-code/page.mdx) for detailed information about inherited functionality.
+
+## URL Management and Deep-Linking
+
+Since `useDemo` extends `useCode`, it automatically inherits all URL hash management capabilities. This enables powerful deep-linking features for demos with multiple files:
+
+### Automatic URL Hash Generation
+
+```tsx
+// Demo with URL-based name/slug generation
+const ButtonDemo = createDemo({
+ url: 'file:///components/demos/interactive-button/index.ts',
+ code: buttonCode,
+ components: { Default: ButtonComponent },
+ Content: DemoContent,
+});
+
+// Automatically generates:
+// - name: "Interactive Button"
+// - slug: "interactive-button"
+// - File URLs: #interactive-button:button.tsx, #interactive-button:styles.css
+```
+
+### Deep-Linking to Specific Files
+
+Users can bookmark and share links to specific files within your demos:
+
+```
+# Links to main file in default variant
+https://yoursite.com/demos/button#interactive-button:button.tsx
+
+# Links to specific file in TypeScript variant
+https://yoursite.com/demos/button#interactive-button:typescript:button.tsx
+
+# Links to styling file
+https://yoursite.com/demos/button#interactive-button:styles.css
+```
+
+### URL Management Behavior
+
+**Initial Load:**
+
+- Hash is read to determine initial file and variant selection
+- Priority: URL hash > localStorage > `initialVariant` > first variant
+- Demos automatically expand when a relevant hash is present
+
+**User Interactions:**
+
+- **File Tab Clicks**: Hash behavior controlled by `fileHashMode` option (default: `'remove-hash'`)
+ - `'remove-hash'`: Removes entire hash on click
+ - `'remove-filename'`: Keeps variant in hash, removes filename
+- **Variant Changes**: Hash behavior controlled by `fileHashMode` option
+ - `'remove-hash'`: Removes entire hash on variant switch
+ - `'remove-filename'`: Updates hash to reflect new variant
+
+**localStorage Persistence:**
+
+- Controlled by `saveHashVariantToLocalStorage` option (default: `'on-interaction'`)
+- `'on-load'`: Hash variant saved immediately when page loads
+- `'on-interaction'`: Hash variant saved only when user clicks a file tab
+- `'never'`: Hash variant never saved to localStorage
+
+**Component Integration:**
+
+- Works seamlessly with demo component rendering
+- File selection synchronized with URL hash
+- Variant switching coordinated with hash updates
+
+For detailed information about URL hash patterns and configuration, see the [useCode URL Management](../use-code/page.mdx#controlling-url-hash-behavior) section.
+
+## Integration Patterns
+
+### Standard Usage Pattern
+
+The most common pattern is to use `useDemo` in a content component that receives props from the demo factory:
+
+```tsx
+// In your demo's Content component
+export function DemoContent(props: ContentProps<{}>) {
+ const demo = useDemo(props, { preClassName: styles.codeBlock }); // Always pass props directly
+
+ return (
+
+
{demo.component}
+
+
{demo.selectedFile}
+
+ );
+}
+```
+
+### Demo Factory Integration
+
+This content component is used with the demo factory pattern, not as a direct child of `CodeHighlighter`:
+
+```tsx
+// [✓] Correct - Demo factory usage
+const ButtonDemo = createDemo({
+ name: 'Button Demo',
+ code: buttonCode,
+ components: { Default: ButtonComponent },
+ Content: DemoContent,
+});
+
+// [x] Incorrect - Never use DemoContent as a direct child
+
+ {/* This won't work */}
+ ;
+```
+
+## Best Practices
+
+### 1. Always Pass Props Directly
+
+```tsx
+export function DemoContent(props: ContentProps<{}>) {
+ const demo = useDemo(props, { preClassName: styles.codeBlock }); // [✓] Pass props directly
+
+ // [x] Never access props.name, props.code, etc.
+ // [✓] Use demo.name, demo.selectedFile, etc.
+
+ return (
+
+
{demo.component}
+
{demo.selectedFile}
+
+ );
+}
+```
+
+### 2. Conditional UI Elements
+
+```tsx
+export function DemoContent(props) {
+ const demo = useDemo(props, { preClassName: styles.codeBlock });
+
+ return (
+
+ {demo.component}
+
+ {/* Only show file tabs if multiple files */}
+ {demo.files.length > 1 ? (
+ ({ id: f.name, name: f.name }))}
+ selectedTabId={demo.selectedFileName}
+ onTabSelect={demo.selectFileName}
+ />
+ ) : (
+ {demo.selectedFileName}
+ )}
+
+ {demo.selectedFile}
+
+ );
+}
+```
+
+### 3. Simple Transform Toggle
+
+```tsx
+export function DemoContent(props) {
+ const demo = useDemo(props, { preClassName: styles.codeBlock });
+
+ const hasTransforms = demo.availableTransforms.length > 0;
+ const isJsSelected = demo.selectedTransform === 'js';
+
+ return (
+
+ {demo.component}
+
+ {hasTransforms && (
+ demo.selectTransform(isJsSelected ? null : 'js')}>
+ {isJsSelected ? 'Show TS' : 'Show JS'}
+
+ )}
+
+ {demo.selectedFile}
+
+ );
+}
+```
+
+### 4. Leverage URL-Based Demo Properties
+
+```tsx
+// Recommended: Let useDemo generate name/slug from URL
+const ButtonDemo = createDemo({
+ url: 'file:///components/demos/advanced-button/index.ts',
+ code: buttonCode,
+ components: { Default: ButtonComponent },
+ Content: DemoContent,
+});
+
+function DemoContent(props) {
+ const demo = useDemo(props);
+
+ return (
+
+
{demo.name} {/* "Advanced Button" */}
+
+ {' '}
+ {/* "advanced-button" */}
+ {demo.component}
+
+ {/* File navigation with automatic URL hash management */}
+ {demo.files.map((file) => (
+
demo.selectFileName(file.name)}
+ data-file-slug={file.slug} // For analytics/debugging
+ >
+ {file.name}
+
+ ))}
+ {demo.selectedFile}
+
+ );
+}
+```
+
+## Common Patterns
+
+### Simple Demo Display
+
+The most basic pattern for showing a component with its code:
+
+```tsx
+export function DemoContent(props) {
+ const demo = useDemo(props, { preClassName: styles.codeBlock });
+
+ return (
+
+
{demo.component}
+
{demo.selectedFile}
+
+ );
+}
+```
+
+### Demo with File Navigation
+
+When you have multiple files to show (includes automatic URL hash management):
+
+```tsx
+export function MultiFileDemoContent(props) {
+ const demo = useDemo(props, { preClassName: styles.codeBlock });
+
+ return (
+
+
{demo.component}
+
+
+ {demo.files.length > 1 && (
+
+ {/* File selection automatically updates URL hash for deep-linking */}
+ {demo.files.map((file) => (
+ demo.selectFileName(file.name)} // Updates URL hash
+ className={demo.selectedFileName === file.name ? styles.active : ''}
+ title={`View ${file.name} (URL: #${file.slug})`}
+ >
+ {file.name}
+
+ ))}
+
+ )}
+
+ {demo.selectedFile}
+
+
+ );
+}
+```
+
+### Demo with Language Toggle
+
+For demos that support TypeScript/JavaScript switching:
+
+```tsx
+export function DemoWithLanguageToggle(props) {
+ const demo = useDemo(props, { preClassName: styles.codeBlock });
+
+ const canToggleJs = demo.availableTransforms.includes('js');
+ const showingJs = demo.selectedTransform === 'js';
+
+ return (
+
+
{demo.component}
+
+
+ {canToggleJs && (
+
+ demo.selectTransform(showingJs ? null : 'js')}>
+ {showingJs ? 'TypeScript' : 'JavaScript'}
+
+
+ )}
+
+ {demo.selectedFile}
+
+
+ );
+}
+```
+
+## Performance Considerations
+
+- **Component memoization**: Components are automatically memoized by the demo factory
+- **Code lazy loading**: Inherited from `useCode` - syntax highlighting can be deferred
+- **Transform caching**: Transform results are cached for quick switching
+- **File switching**: File navigation is optimized for instant switching
+
+## Error Handling
+
+`useDemo` inherits error handling from `useCode` and adds demo-specific safeguards:
+
+- **Missing components**: Gracefully handles when components aren't available for a variant
+- **Invalid names/slugs**: Provides fallback values for missing identification
+- **Component render errors**: Use React Error Boundaries to catch component-specific issues
+
+## Troubleshooting
+
+### Component Not Rendering
+
+- Verify the component is passed in the `components` prop
+- Check that the variant name matches between `code` and `components`
+- Ensure the component doesn't have render-blocking errors
+
+### Code Not Showing
+
+- See [useCode troubleshooting](../use-code/page.mdx#troubleshooting) for code-related issues
+- Verify `contentProps` contains the expected code structure
+
+### Name/Slug Not Generated
+
+- Ensure a valid `url` property is provided in demo creation or available in context
+- Provide explicit `name` or `slug` properties as fallbacks if URL parsing fails
+- Check that the URL follows a recognizable pattern for automatic generation
+
+### URL Hash Issues
+
+- **Deep-linking not working**: See [useCode URL troubleshooting](../use-code/page.mdx#url-hash-not-working) for detailed debugging steps
+- **File navigation not updating URL**: Ensure component is running in browser environment (not SSR)
+- **Hash conflicts**: Check that demo slugs are unique across your application
+
+## Related
+
+- **[useCode](../use-code/page.mdx)**: The underlying hook for code management (see [useCode documentation](../use-code/page.mdx))
+- **[CodeHighlighter](../../components/code-highlighter/page.mdx)**: The main component that provides context for demos
+- **[abstractCreateDemo](../../functions/abstract-create-demo/page.mdx)**: Factory function for creating precomputed demos
diff --git a/docs/app/docs-infra/hooks/use-errors/page.mdx b/docs/app/docs-infra/hooks/use-errors/page.mdx
new file mode 100644
index 000000000..3266d9480
--- /dev/null
+++ b/docs/app/docs-infra/hooks/use-errors/page.mdx
@@ -0,0 +1,426 @@
+# useErrors
+
+The `useErrors` hook provides access to error state in an isomorphic error handling system. It implements the [Props Context Layering pattern](../../patterns/props-context-layering/page.mdx) to work seamlessly across server and client boundaries, making it ideal for code highlighting and interactive demos that need robust error handling.
+
+## Features
+
+- **Props Context Layering** - Implements the isomorphic pattern for React Server Components
+- **Context integration** - Accesses errors from `CodeErrorsContext`
+- **Graceful fallbacks** - Handles cases where context is unavailable
+- **Runtime error updates** - Errors can be updated dynamically via context
+- **Simple API** - Automatically merges props and context
+
+## API
+
+```typescript
+const { errors } = useErrors(props?);
+```
+
+### Parameters
+
+| Parameter | Type | Description |
+| --------- | ---------------------- | -------------------------------------------------------------- |
+| `props` | `{ errors?: Error[] }` | Optional props containing fallback errors (typically from SSR) |
+
+### Returns
+
+| Property | Type | Description |
+| -------- | ---------------------- | ------------------------------------------------------------------- |
+| `errors` | `Error[] \| undefined` | Array of current errors (context errors take precedence over props) |
+
+## Usage Examples
+
+### Basic Error Display
+
+```tsx
+import { useErrors } from '@mui/internal-docs-infra/useErrors';
+
+function DemoErrorHandler({ errors: propErrors }: { errors?: Error[] }) {
+ const { errors } = useErrors({ errors: propErrors });
+
+ if (!errors || errors.length === 0) {
+ return null; // No errors to display
+ }
+
+ return (
+
+
Errors occurred:
+ {errors.map((error, index) => (
+
+ {error.message}
+
+ ))}
+
+ );
+}
+```
+
+### Isomorphic Error Handler
+
+```tsx
+import { useErrors } from '@mui/internal-docs-infra/useErrors';
+
+interface ErrorHandlerProps {
+ errors?: Error[]; // Props-based errors for SSR
+}
+
+function CodeErrorHandler({ errors }: ErrorHandlerProps) {
+ const { errors: resolvedErrors } = useErrors({ errors });
+
+ if (!resolvedErrors || resolvedErrors.length === 0) {
+ return An error occurred, but details were not provided.
;
+ }
+
+ return (
+
+
Error occurred when highlighting code:
+ {resolvedErrors.map((error, index) => (
+
+ {error.message}
+
+ ))}
+
+ );
+}
+```
+
+### Conditional Rendering Based on Errors
+
+```tsx
+import { useErrors } from '@mui/internal-docs-infra/useErrors';
+import { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter';
+
+function SafeCodeDemo({
+ code,
+ language,
+ errors: propErrors,
+}: {
+ code: string;
+ language: string;
+ errors?: Error[];
+}) {
+ const { errors } = useErrors({ errors: propErrors });
+
+ return (
+
+ {errors && errors.length > 0 ? (
+
+
Failed to render code demo:
+
+ {errors.map((error, index) => (
+ {error.message}
+ ))}
+
+
+ Raw Code
+ {code}
+
+
+ ) : (
+
+ )}
+
+ );
+}
+```
+
+### Custom Error Formatting
+
+```tsx
+import { useErrors } from '@mui/internal-docs-infra/useErrors';
+
+function FormattedErrorDisplay({ errors: propErrors }: { errors?: Error[] }) {
+ const { errors } = useErrors({ errors: propErrors });
+
+ if (!errors || errors.length === 0) {
+ return null;
+ }
+
+ const formatError = (error: Error) => {
+ // Extract useful information from error
+ const { message, name, stack } = error;
+
+ return {
+ title: name || 'Error',
+ description: message || 'An unknown error occurred',
+ details: stack?.split('\n').slice(0, 3).join('\n'), // First 3 lines of stack
+ };
+ };
+
+ return (
+
+ {errors.map((error, index) => {
+ const formatted = formatError(error);
+ return (
+
+
{formatted.title}
+
{formatted.description}
+ {formatted.details && (
+
+ Stack trace
+ {formatted.details}
+
+ )}
+
+ );
+ })}
+
+ );
+}
+```
+
+## How It Works
+
+### Props Context Layering Pattern
+
+This hook implements the [Props Context Layering pattern](../../patterns/props-context-layering/page.mdx) for React Server Components:
+
+1. **Components render before crossing client boundary**: Error handler components must render on the server before client-side props are available
+2. **Props used first, context layers on top**: Initial server-side errors come via props, client-side updates come via context
+3. **Custom hook merges props and context**: `useErrors({ errors })` automatically handles the precedence logic
+4. **Seamless server/client transition**: Users get consistent error handling regardless of where errors occur
+
+### Isomorphic Design
+
+The hook works across server-side and client-side environments:
+
+1. **Server-side rendering**: Errors are passed as props to components during SSR
+2. **Client-side updates**: Errors are provided via `CodeErrorsContext` when processing occurs
+3. **Automatic precedence**: Context errors override props errors, ensuring latest state is shown
+
+### The Problem This Solves
+
+In isomorphic applications with code highlighting and live demos, errors can occur at different layers:
+
+1. **Server-side errors**: Occur during initial server rendering (e.g., syntax parsing failures)
+2. **Client-side errors**: Occur during client-side post-processing (e.g., live demo execution, dynamic transformations)
+
+**Without `useErrors()`:** If error handlers only accept props, client-side errors appear as generic errors with no useful information.
+
+**With `useErrors()`:** Both server and client errors are handled uniformly, providing detailed error information regardless of where the error occurred.
+
+### Architectural Pattern
+
+This follows the Props Context Layering pattern for isomorphic components:
+
+```tsx
+// ❌ Props-only approach - breaks on client-side updates
+function BadErrorHandler({ errors }: { errors?: Error[] }) {
+ // Only sees server-side errors passed as props
+ // Client-side errors are lost or appear generic
+ return errors ? : null;
+}
+
+// ✅ Props Context Layering - works server and client
+function GoodErrorHandler({ errors }: { errors?: Error[] }) {
+ const { errors: resolvedErrors } = useErrors({ errors });
+
+ // Hook implements: contextErrors || props.errors
+ // Server: uses props.errors (context undefined)
+ // Client: uses context.errors (takes precedence)
+ return resolvedErrors ? : null;
+}
+
+// 🎯 Server Component - no client code needed
+function ServerOnlyErrorHandler({ errors }: { errors?: Error[] }) {
+ // When no client-side processing is needed, can stay server component
+ return errors ? : null;
+}
+```
+
+### Implementation Pattern
+
+```tsx
+'use client';
+
+import { useContext } from 'react';
+import { CodeErrorsContext } from './ErrorsContext';
+
+const useErrors = (props?: { errors?: Error[] }) => {
+ const contextErrors = useContext(CodeErrorsContext);
+ // Context takes precedence over props
+ return { errors: contextErrors?.errors || props?.errors };
+};
+
+const ErrorHandler = (props: { errors?: Error[] }) => {
+ const { errors } = useErrors(props);
+ return ;
+};
+```
+
+### Multi-Layer Error Flow
+
+1. **Component renders on server**: May encounter parsing/highlighting errors → passed as props
+2. **Component hydrates on client**: Server errors available via props
+3. **Client-side processing occurs**: New errors → updated in context via `CodeErrorsContext.Provider`
+4. **`useErrors({ errors })` provides latest state**: Context errors override props automatically, ensuring users see the most relevant error
+
+This pattern ensures that users get detailed error information regardless of whether the error occurred during:
+
+- Initial server-side code parsing
+- Client-side syntax highlighting
+- Live demo compilation
+- Dynamic code transformation
+- Runtime execution
+
+The hook handles the precedence logic internally, so error handler components simply pass their props and get back the most relevant errors.
+
+### Why This Pattern Is Essential
+
+The Props Context Layering pattern solves critical React Server Component challenges:
+
+1. **Server Component Constraints**: Components must render before crossing client boundaries
+2. **Early Rendering**: Error handlers render before client-side processing provides updated errors
+3. **Async Component Isolation**: Heavy error processing can be client-side only when needed
+4. **Seamless Updates**: Context layers client updates over server-rendered props
+
+Without this pattern, you'd need separate components for:
+
+- Server-side rendering errors
+- Client-side hydration errors
+- Runtime processing errors
+- Live demo execution errors
+
+Instead, `useErrors({ errors })` provides a unified interface that automatically handles:
+
+- **Server rendering**: Uses props.errors when context is unavailable
+- **Client updates**: Context errors override props when processing occurs
+- **Error boundaries**: Consistent error display regardless of error source
+- **Bundle optimization**: Heavy error processing stays client-side only
+
+This ensures consistent error reporting and user experience while respecting React Server Component boundaries.
+
+### Context Integration
+
+The hook connects to the `CodeErrorsContext` which is provided by components like `CodeHighlighterClient`:
+
+```tsx
+import { CodeErrorsContext } from '@mui/internal-docs-infra/useErrors/ErrorsContext';
+
+function ErrorProvider({ children, errors }: { children: React.ReactNode; errors?: Error[] }) {
+ const errorsContextValue = React.useMemo(() => ({ errors }), [errors]);
+
+ return (
+ {children}
+ );
+}
+```
+
+### Error Flow
+
+1. **Initial render**: Errors may be undefined or passed as props
+2. **Error occurrence**: Context is updated with new errors
+3. **Hook response**: `useErrors()` returns the latest error state
+4. **Component update**: UI updates to reflect current error state
+
+## Integration with CodeHighlighter
+
+The hook is commonly used with code highlighting components:
+
+```tsx
+import { useErrors } from '@mui/internal-docs-infra/useErrors';
+import { CodeHighlighterClient } from '@mui/internal-docs-infra/CodeHighlighter';
+
+function CodeDemo() {
+ return (
+
+
+
+ );
+}
+
+function CodeDemoContent() {
+ const { errors } = useErrors();
+
+ // Component has access to errors from CodeHighlighterClient context
+ return errors ? : ;
+}
+```
+
+## Error Context Type
+
+```typescript
+export interface CodeErrorsContext {
+ errors?: Error[];
+}
+
+export function useErrors(props?: { errors?: Error[] }): { errors?: Error[] } {
+ const context = useErrorsContext();
+
+ // Context errors take precedence over prop errors
+ const errors = context?.errors || props?.errors;
+
+ return { errors };
+}
+```
+
+## Best Practices
+
+### 1. Always Check for Errors
+
+```tsx
+const { errors } = useErrors({ errors: propErrors });
+
+// Always check if errors exist and have length
+if (!errors || errors.length === 0) {
+ return ;
+}
+
+return ;
+```
+
+### 2. Provide Meaningful Fallbacks
+
+```tsx
+const { errors } = useErrors({ errors: propErrors });
+
+if (errors && errors.length > 0) {
+ return (
+
+
Something went wrong. Here's the raw content:
+
{rawContent}
+
+ );
+}
+```
+
+### 3. Use in Error Boundaries
+
+```tsx
+class CodeErrorBoundary extends React.Component {
+ state = { hasError: false, error: null };
+
+ static getDerivedStateFromError(error: Error) {
+ return { hasError: true, error };
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+```
+
+## When to Use
+
+- **Code highlighting components** - Display errors when syntax highlighting fails
+- **Interactive demos** - Show errors from code execution
+- **Dynamic content rendering** - Handle errors in real-time content updates
+- **Isomorphic applications** - Need error handling that works server and client-side
+- **Error boundaries** - Provide context for error display components
+
+## Related
+
+- [**Props Context Layering**](../../patterns/props-context-layering/page.mdx) - The architectural pattern this hook implements
+- [`CodeHighlighter`](../../components/code-highlighter/page.mdx) - Uses this hook for error display
+- [`CodeErrorHandler`](../../components/code-error-handler/page.mdx) - Built-in error handler component
+- [`CodeErrorsContext`](../../contexts/code-errors-context/page.mdx) - The underlying context
diff --git a/docs/app/docs-infra/hooks/use-local-storage-state/page.mdx b/docs/app/docs-infra/hooks/use-local-storage-state/page.mdx
new file mode 100644
index 000000000..5ebfd76f5
--- /dev/null
+++ b/docs/app/docs-infra/hooks/use-local-storage-state/page.mdx
@@ -0,0 +1,297 @@
+# useLocalStorageState
+
+The `useLocalStorageState` hook provides persistent state management using localStorage with cross-tab synchronization, server-side rendering support, and a useState-like API. It's designed for user preferences, demo state, and any data that should persist across browser sessions.
+
+## Features
+
+- **localStorage persistence** - Automatically saves and loads state from localStorage
+- **Cross-tab synchronization** - State updates sync across browser tabs and windows
+- **SSR-safe** - Works with server-side rendering and hydration
+- **useState-compatible API** - Drop-in replacement for useState with persistence
+- **Function updates** - Supports functional state updates like `setState(prev => prev + 1)`
+- **Null key support** - Disables persistence when key is null (useful for conditional persistence)
+- **Initializer functions** - Lazy initialization support
+- **Error handling** - Gracefully handles localStorage unavailability
+
+## API
+
+```typescript
+const [value, setValue] = useLocalStorageState(key, initializer);
+```
+
+### Parameters
+
+| Parameter | Type | Description |
+| ------------- | ------------------------------------------ | -------------------------------------------------- |
+| `key` | `string \| null` | localStorage key. If null, persistence is disabled |
+| `initializer` | `string \| null \| (() => string \| null)` | Initial value or function returning initial value |
+
+### Returns
+
+| Property | Type | Description |
+| ---------- | ------------------------------------------------------ | ------------------------------------------------ |
+| `value` | `string \| null` | Current value from localStorage or initial value |
+| `setValue` | `React.Dispatch>` | Function to update the value |
+
+## Usage Examples
+
+### Basic Persistence
+
+```tsx
+import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
+
+function ThemeToggle() {
+ const [theme, setTheme] = useLocalStorageState('theme', () => 'light');
+
+ const toggleTheme = () => {
+ setTheme(theme === 'light' ? 'dark' : 'light');
+ };
+
+ return Current theme: {theme} (Click to toggle) ;
+}
+```
+
+### Function Updates
+
+```tsx
+import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
+
+function Counter() {
+ const [count, setCount] = useLocalStorageState('counter', () => '0');
+
+ const increment = () => {
+ setCount((prev) => String(Number(prev || '0') + 1));
+ };
+
+ const decrement = () => {
+ setCount((prev) => String(Number(prev || '0') - 1));
+ };
+
+ return (
+
+
Count: {count}
+
+
+
-
+
+ );
+}
+```
+
+### Conditional Persistence
+
+```tsx
+import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
+
+function UserSettings({ enablePersistence }: { enablePersistence: boolean }) {
+ // When enablePersistence is false, key is null and state isn't persisted
+ const [settings, setSettings] = useLocalStorageState(
+ enablePersistence ? 'user-settings' : null,
+ () => 'default-settings',
+ );
+
+ return (
+
+
Settings: {settings}
+
Persistence: {enablePersistence ? 'enabled' : 'disabled'}
+
setSettings('custom-settings')}>Update Settings
+
+ );
+}
+```
+
+### Demo Code Editor
+
+```tsx
+import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
+
+function CodeEditor({ demoId }: { demoId: string }) {
+ const [code, setCode] = useLocalStorageState(
+ `demo-code-${demoId}`,
+ () => `// Default code for ${demoId}\nconsole.log('Hello World');`,
+ );
+
+ return (
+
+ );
+}
+```
+
+### User Preferences Panel
+
+```tsx
+import useLocalStorageState from '@mui/internal-docs-infra/useLocalStorageState';
+
+function PreferencesPanel() {
+ const [language, setLanguage] = useLocalStorageState('ui-language', () => 'en');
+ const [fontSize, setFontSize] = useLocalStorageState('font-size', () => 'medium');
+ const [autoSave, setAutoSave] = useLocalStorageState('auto-save', () => 'true');
+
+ return (
+
+
User Preferences
+
+
+ Language:
+ setLanguage(e.target.value)}>
+ English
+ Spanish
+ French
+
+
+
+
+ Font Size:
+ setFontSize(e.target.value)}>
+ Small
+ Medium
+ Large
+
+
+
+
+ setAutoSave(e.target.checked ? 'true' : 'false')}
+ />
+ Auto-save
+
+
+ );
+}
+```
+
+## How It Works
+
+### Server-Side Rendering
+
+The hook handles SSR by:
+
+1. **Server**: Returns `[null, () => {}]` - no localStorage access
+2. **Client hydration**: Uses `useSyncExternalStore` with server snapshot returning `null`
+3. **Post-hydration**: Switches to actual localStorage values
+
+This prevents hydration mismatches while providing immediate localStorage access after hydration.
+
+### Cross-Tab Synchronization
+
+```typescript
+// Internal event system for same-tab updates
+const currentTabChangeListeners = new Map void>>();
+
+// Listens to both:
+// 1. `storage` events (for other tabs)
+// 2. Custom events (for current tab)
+function subscribe(area: Storage, key: string | null, callback: () => void) {
+ const storageHandler = (event: StorageEvent) => {
+ if (event.storageArea === area && event.key === key) {
+ callback(); // Other tabs changed this key
+ }
+ };
+ window.addEventListener('storage', storageHandler);
+ onCurrentTabStorageChange(key, callback); // Same tab changes
+ // ...
+}
+```
+
+### Value Management
+
+**Setting Values:**
+
+```typescript
+// Supports both direct values and function updates
+setValue('new-value');
+setValue((prev) => `${prev}-updated`);
+
+// null removes the item and falls back to initial value
+setValue(null);
+```
+
+**Storage Operations:**
+
+- `setValue(value)` → `localStorage.setItem(key, value)`
+- `setValue(null)` → `localStorage.removeItem(key)` + fallback to initial
+- Error handling for storage quota/permissions issues
+
+### Null Key Behavior
+
+When `key` is `null`:
+
+- No localStorage operations occur
+- Hook behaves like regular `useState`
+- Useful for conditional persistence
+
+```tsx
+const [value, setValue] = useLocalStorageState(shouldPersist ? 'my-key' : null, () => 'default');
+```
+
+## Error Handling
+
+The hook gracefully handles:
+
+- **localStorage unavailable** (private browsing, disabled)
+- **Storage quota exceeded**
+- **Permission errors**
+- **Invalid JSON** (though this hook only handles strings)
+
+All errors are caught and ignored, falling back to non-persistent behavior.
+
+## TypeScript Support
+
+```typescript
+// Hook signature
+function useLocalStorageState(
+ key: string | null,
+ initializer?: string | null | (() => string | null),
+): [string | null, React.Dispatch>];
+
+// Example usage
+const [value, setValue] = useLocalStorageState('key', () => 'initial');
+// ^string | null ^React.Dispatch>
+```
+
+## Performance Considerations
+
+- **useSyncExternalStore**: Uses React 18's external store API for optimal performance
+- **Event-driven updates**: Only re-renders when localStorage actually changes
+- **Lazy initialization**: Initializer functions called only once
+- **Memory efficient**: Automatic cleanup of event listeners
+
+## Browser Support
+
+- **Modern browsers**: Full functionality with localStorage and storage events
+- **Legacy browsers**: Falls back to non-persistent useState behavior
+- **SSR environments**: Safe server-side rendering with hydration support
+
+## When to Use
+
+- **User preferences** - Theme, language, UI settings
+- **Demo state** - Code editor content, configuration
+- **Form data** - Draft content, auto-save functionality
+- **UI state** - Sidebar collapsed, tabs selected
+- **Cache** - Non-critical data that can be lost
+- **Cross-tab sync** - When state should sync across browser tabs
+
+## Limitations
+
+- **String values only** - Use JSON.stringify/parse for objects
+- **Storage quota** - localStorage has size limits (~5-10MB)
+- **Same-origin only** - Can't share data across different domains
+- **Client-side only** - No server-side persistence
+
+## Related
+
+- [`usePreference`](../use-preference/page.mdx) - Higher-level preference management
+- [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) - Underlying browser API
diff --git a/docs/app/docs-infra/hooks/use-preference/page.mdx b/docs/app/docs-infra/hooks/use-preference/page.mdx
new file mode 100644
index 000000000..e698c1a89
--- /dev/null
+++ b/docs/app/docs-infra/hooks/use-preference/page.mdx
@@ -0,0 +1,370 @@
+# usePreference
+
+The `usePreference` hook provides specialized preference management for code demo variants and transformations. It builds on `useLocalStorageState` with intelligent storage key generation, prefix support, and automatic optimization for single-option scenarios.
+
+## Features
+
+- **Specialized for demos** - Designed for variant and transform preferences
+- **Intelligent storage** - Only persists when there are multiple options to choose from
+- **Automatic key generation** - Creates storage keys from variant/transform names
+- **Prefix support** - Configurable prefixes via PreferencesProvider
+- **localStorage persistence** - Built on useLocalStorageState for cross-tab sync
+- **Smart optimization** - Skips storage for single-option scenarios
+
+## API
+
+```typescript
+const [preference, setPreference] = usePreference(type, name, initializer);
+```
+
+### Parameters
+
+| Parameter | Type | Description |
+| ------------- | ------------------------------------------ | ------------------------------------------------------- |
+| `type` | `'variant' \| 'transform'` | Type of preference (affects storage prefix) |
+| `name` | `string \| string[]` | Variant/transform name(s). Array gets sorted and joined |
+| `initializer` | `string \| null \| (() => string \| null)` | Initial value or function |
+
+### Returns
+
+| Property | Type | Description |
+| --------------- | ------------------------------------------------------ | ----------------------------- |
+| `preference` | `string \| null` | Current preference value |
+| `setPreference` | `React.Dispatch>` | Function to update preference |
+
+## Usage Examples
+
+### Variant Preferences
+
+```tsx
+import { usePreference } from '@mui/internal-docs-infra/usePreference';
+
+function ButtonVariantSelector() {
+ // For multiple variants - will persist to localStorage
+ const [variant, setVariant] = usePreference(
+ 'variant',
+ ['contained', 'outlined', 'text'], // Multiple options
+ () => 'contained',
+ );
+
+ return (
+
+
Current variant: {variant}
+ {['contained', 'outlined', 'text'].map((option) => (
+
setVariant(option)}
+ style={{
+ fontWeight: variant === option ? 'bold' : 'normal',
+ }}
+ >
+ {option}
+
+ ))}
+
+ );
+}
+```
+
+### Transform Preferences
+
+```tsx
+import { usePreference } from '@mui/internal-docs-infra/usePreference';
+
+function CodeLanguageSelector() {
+ const [language, setLanguage] = usePreference(
+ 'transform',
+ ['typescript', 'javascript'], // Multiple languages available
+ () => 'typescript',
+ );
+
+ return (
+
+
+ setLanguage('typescript')}
+ />
+ TypeScript
+
+
+ setLanguage('javascript')}
+ />
+ JavaScript
+
+
+ );
+}
+```
+
+### Single Option (No Persistence)
+
+```tsx
+import { usePreference } from '@mui/internal-docs-infra/usePreference';
+
+function SingleVariantDemo() {
+ // Only one variant - no localStorage persistence (key will be null)
+ const [variant, setVariant] = usePreference(
+ 'variant',
+ ['contained'], // Single option
+ () => 'contained',
+ );
+
+ // This behaves like useState since there's no choice to remember
+ return Only variant available: {variant}
;
+}
+```
+
+### With Custom Prefix
+
+```tsx
+import { usePreference, PreferencesProvider } from '@mui/internal-docs-infra/usePreference';
+
+function CustomPrefixDemo() {
+ return (
+
+
+
+ );
+}
+
+function ComponentWithPreferences() {
+ // Storage key will be: "my-app_variant:contained:outlined:text"
+ const [variant, setVariant] = usePreference(
+ 'variant',
+ ['contained', 'outlined', 'text'],
+ () => 'contained',
+ );
+
+ return (
+ setVariant(e.target.value)}>
+ Contained
+ Outlined
+ Text
+
+ );
+}
+```
+
+### Demo Configuration Panel
+
+```tsx
+import { usePreference } from '@mui/internal-docs-infra/usePreference';
+
+function DemoConfigPanel() {
+ const [size, setSize] = usePreference('variant', ['small', 'medium', 'large'], () => 'medium');
+
+ const [color, setColor] = usePreference(
+ 'variant',
+ ['primary', 'secondary', 'error'],
+ () => 'primary',
+ );
+
+ const [format, setFormat] = usePreference(
+ 'transform',
+ ['typescript', 'javascript'],
+ () => 'typescript',
+ );
+
+ return (
+
+
Demo Configuration
+
+
+ Size:
+ setSize(e.target.value)}>
+ Small
+ Medium
+ Large
+
+
+
+
+ Color:
+ setColor(e.target.value)}>
+ Primary
+ Secondary
+ Error
+
+
+
+
+ Code Format:
+ setFormat(e.target.value)}>
+ TypeScript
+ JavaScript
+
+
+
+ );
+}
+```
+
+## How It Works
+
+### Storage Key Generation
+
+The hook generates localStorage keys based on the type and name parameters:
+
+```typescript
+// For variant preferences
+// Single string name
+usePreference('variant', 'contained');
+// → Storage key: "_docs_variant_pref:contained"
+
+// Array of names (sorted and joined)
+usePreference('variant', ['outlined', 'contained', 'text']);
+// → Storage key: "_docs_variant_pref:contained:outlined:text"
+
+// For transform preferences
+usePreference('transform', ['typescript', 'javascript']);
+// → Storage key: "_docs_transform_pref:javascript:typescript"
+```
+
+### Intelligent Optimization
+
+```typescript
+const key = React.useMemo(() => {
+ if (!Array.isArray(name)) {
+ return name; // Single string always persists
+ }
+
+ if (name.length <= 1) {
+ return null; // Single option - no need to persist choice
+ }
+
+ return [...name].sort().join(':'); // Multiple options - create stable key
+}, [name]);
+```
+
+### Prefix System
+
+```typescript
+// Default prefixes
+const defaultPrefixes = {
+ variant: '_docs_variant_pref',
+ transform: '_docs_transform_pref',
+};
+
+// With custom prefix from context
+// usePreference('variant', ['a', 'b']) with prefix "my-app"
+// → Storage key: "my-app_variant:a:b"
+```
+
+### Context Integration
+
+```tsx
+export interface PreferencesContext {
+ prefix?: string;
+}
+
+// Usage
+
+
+ ;
+```
+
+## Storage Key Examples
+
+| Type | Name | Generated Key |
+| ------------------- | ----------------------- | ---------------------------------- |
+| `variant` | `'contained'` | `_docs_variant_pref:contained` |
+| `variant` | `['text', 'outlined']` | `_docs_variant_pref:outlined:text` |
+| `variant` | `['single']` | `null` (no persistence) |
+| `transform` | `['ts', 'js']` | `_docs_transform_pref:js:ts` |
+| With prefix `'app'` | `variant`, `['a', 'b']` | `app_variant:a:b` |
+
+## Integration with CodeHighlighter
+
+```tsx
+import { usePreference } from '@mui/internal-docs-infra/usePreference';
+import { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter';
+
+function CodeDemo({ availableVariants, availableTransforms }) {
+ const [selectedVariant, setSelectedVariant] = usePreference(
+ 'variant',
+ availableVariants,
+ () => availableVariants[0],
+ );
+
+ const [selectedTransform, setSelectedTransform] = usePreference(
+ 'transform',
+ availableTransforms,
+ () => availableTransforms[0],
+ );
+
+ return (
+
+ {/* Variant selector only shows if multiple variants */}
+ {availableVariants.length > 1 && (
+
+ )}
+
+ {/* Transform selector only shows if multiple transforms */}
+ {availableTransforms.length > 1 && (
+
+ )}
+
+
+
+ );
+}
+```
+
+## Performance Optimizations
+
+1. **Memoized key generation** - Storage key only recalculated when name changes
+2. **Conditional persistence** - Single options skip localStorage entirely
+3. **Stable array keys** - Sorted arrays ensure consistent storage keys
+4. **Built on useLocalStorageState** - Inherits cross-tab sync and SSR safety
+
+## When to Use
+
+- **Demo variant selection** - When users can choose between component variants
+- **Code transform preferences** - TypeScript/JavaScript, different syntax styles
+- **Multi-option scenarios** - Any case where users choose from multiple options
+- **Persistent UI state** - Remember user choices across sessions
+
+## When NOT to Use
+
+- **Single options** - Hook automatically optimizes this case
+- **Non-demo preferences** - Use `useLocalStorageState` directly for general preferences
+- **Complex objects** - This hook is designed for string preferences
+
+## TypeScript Support
+
+```typescript
+// Hook signature
+function usePreference(
+ type: 'variant' | 'transform',
+ name: string | string[],
+ initializer?: string | null | (() => string | null),
+): [string | null, React.Dispatch>];
+
+// Context types
+interface PreferencesContext {
+ prefix?: string;
+}
+```
+
+## Related
+
+- [`useLocalStorageState`](../use-local-storage-state/page.mdx) - Underlying persistence mechanism
+- [`PreferencesProvider`](../../components/preferences-provider/page.mdx) - Context provider for custom prefixes
+- [`CodeHighlighter`](../../components/code-highlighter/page.mdx) - Common use case for variant preferences
diff --git a/docs/app/docs-infra/hooks/use-url-hash-state/page.mdx b/docs/app/docs-infra/hooks/use-url-hash-state/page.mdx
new file mode 100644
index 000000000..2f7c3b472
--- /dev/null
+++ b/docs/app/docs-infra/hooks/use-url-hash-state/page.mdx
@@ -0,0 +1,290 @@
+# useUrlHashState
+
+The `useUrlHashState` hook provides a simple way to synchronize component state with the URL hash fragment, enabling deep linking, state persistence, and browser navigation support in documentation and demo pages.
+
+## Features
+
+- **URL Synchronization**: Automatically syncs state with the URL hash fragment
+- **Deep Linking**: Enables direct links to specific application states
+- **Browser Navigation**: Supports back/forward navigation with history API
+- **SSR Safe**: Handles server-side rendering gracefully
+- **Simple API**: Returns `[hash, setHash]` tuple similar to `useState`
+
+## API
+
+```tsx
+const [hash, setHash] = useUrlHashState();
+```
+
+### Returns
+
+A tuple containing:
+
+| Index | Type | Description |
+| ----- | ---------------------------------------------------- | ------------------------------------------- |
+| `0` | `string \| null` | Current hash value (without the '#' prefix) |
+| `1` | `(value: string \| null, replace?: boolean) => void` | Function to update hash and URL |
+
+### setHash Parameters
+
+| Parameter | Type | Default | Description |
+| --------- | ---------------- | ------- | ----------------------------------------------------------- |
+| `value` | `string \| null` | - | New hash value to set, or `null` to clear the hash |
+| `replace` | `boolean` | `true` | Whether to use `replaceState` (true) or `pushState` (false) |
+
+## Usage
+
+### Basic Usage
+
+```tsx
+import { useUrlHashState } from '@mui/internal-docs-infra/useUrlHashState';
+
+function TabNavigation() {
+ const [hash, setHash] = useUrlHashState();
+
+ return (
+
+ setHash('overview')} className={hash === 'overview' ? 'active' : ''}>
+ Overview
+
+ setHash('details')} className={hash === 'details' ? 'active' : ''}>
+ Details
+
+ setHash(null)}>Clear Hash
+
+ );
+}
+```
+
+### History Management
+
+```tsx
+function NavigationExample() {
+ const [hash, setHash] = useUrlHashState();
+
+ const goToSection = (section: string, addToHistory = false) => {
+ // Use replace=false to add entry to browser history
+ // Use replace=true (default) to replace current entry
+ setHash(section, !addToHistory);
+ };
+
+ return (
+
+ goToSection('intro', true)}>Go to Intro (new history entry)
+ goToSection('content')}>Go to Content (replace current)
+
+ );
+}
+```
+
+### Responding to Hash Changes
+
+```tsx
+function SectionNavigator() {
+ const [hash, setHash] = useUrlHashState();
+
+ // React to hash changes (including browser back/forward)
+ React.useEffect(() => {
+ if (hash) {
+ const element = document.getElementById(hash);
+ if (element) {
+ element.scrollIntoView({ behavior: 'smooth' });
+ }
+ }
+ }, [hash]);
+
+ return (
+
+
Current section: {hash || 'none'}
+
setHash('section1')}>Go to Section 1
+
setHash('section2')}>Go to Section 2
+
+ );
+}
+```
+
+## Implementation Details
+
+The hook uses the following strategy:
+
+1. **Initial Reading**: On mount, reads the current hash from `window.location.hash`
+2. **State Management**: Uses `useSyncExternalStore` to synchronize with URL changes
+3. **URL Updates**: Uses `history.replaceState()` or `history.pushState()` to update URL
+4. **Change Detection**: Listens to `hashchange` events for browser navigation
+5. **SSR Handling**: Returns `null` and skips URL operations on the server
+6. **Hash Parsing**: Automatically removes the '#' prefix from hash values
+
+## Common Patterns
+
+### Documentation Sections
+
+```tsx
+function DocumentationPage() {
+ const [hash, setHash] = useUrlHashState();
+
+ const sections = ['introduction', 'api', 'examples', 'troubleshooting'];
+ const activeSection = hash || 'introduction';
+
+ return (
+
+
+ {sections.map((section) => (
+ setHash(section)}
+ className={activeSection === section ? 'active' : ''}
+ >
+ {section}
+
+ ))}
+
+
+ {activeSection === 'introduction' && }
+ {activeSection === 'api' && }
+ {activeSection === 'examples' && }
+ {activeSection === 'troubleshooting' && }
+
+
+ );
+}
+```
+
+### Demo State Persistence
+
+```tsx
+function DemoPage() {
+ const [hash, setHash] = useUrlHashState();
+
+ // Parse hash as JSON for complex state
+ const demoState = React.useMemo(() => {
+ if (!hash) return { variant: 'default', size: 'medium' };
+ try {
+ return JSON.parse(decodeURIComponent(hash));
+ } catch {
+ return { variant: 'default', size: 'medium' };
+ }
+ }, [hash]);
+
+ const updateDemoState = (updates: Partial) => {
+ const newState = { ...demoState, ...updates };
+ setHash(encodeURIComponent(JSON.stringify(newState)));
+ };
+
+ return (
+
+
+
+
+ );
+}
+```
+
+### Modal Deep Linking
+
+```tsx
+function ModalExample() {
+ const [hash, setHash] = useUrlHashState();
+ const isModalOpen = hash === 'modal';
+
+ const openModal = () => setHash('modal');
+ const closeModal = () => setHash(null);
+
+ return (
+
+
Open Modal
+ {isModalOpen && (
+
+ Modal content - shareable URL!
+
+ )}
+
+ );
+}
+```
+
+## When to Use
+
+- **Deep Linking**: Enable direct links to specific states or sections
+- **State Persistence**: Preserve application state across page reloads
+- **Browser Navigation**: Support back/forward navigation for state changes
+- **Shareable URLs**: Create URLs that capture current application state
+- **Documentation Navigation**: Navigate between sections with persistent URLs
+- **Demo Configuration**: Persist demo settings in URL for sharing
+
+## Fragment Structure and Delimiters
+
+This hook works with any hash format, but for consistency across MUI documentation, consider using the [fragment delimiter convention](../../conventions/fragment-delimeters/page.mdx). This convention uses `:` to express hierarchy in URL fragments:
+
+```tsx
+function HierarchicalNavigation() {
+ const [hash, setHash] = useUrlHashState();
+
+ // Parse hierarchical fragments like "api:props" or "demos:button:outlined"
+ const segments = hash?.split(':') || [];
+ const [section, subsection, variant] = segments;
+
+ return (
+
+ {/* Primary navigation */}
+
+ setHash('api')} className={section === 'api' ? 'active' : ''}>
+ API
+
+ setHash('demos')} className={section === 'demos' ? 'active' : ''}>
+ Demos
+
+
+
+ {/* Secondary navigation within API section */}
+ {section === 'api' && (
+
+ setHash('api:props')}
+ className={subsection === 'props' ? 'active' : ''}
+ >
+ Props
+
+ setHash('api:methods')}
+ className={subsection === 'methods' ? 'active' : ''}
+ >
+ Methods
+
+
+ )}
+
+ {/* Demo variants */}
+ {section === 'demos' && subsection === 'button' && (
+
+ setHash('demos:button:outlined')}
+ className={variant === 'outlined' ? 'active' : ''}
+ >
+ Outlined
+
+ setHash('demos:button:contained')}
+ className={variant === 'contained' ? 'active' : ''}
+ >
+ Contained
+
+
+ )}
+
+ );
+}
+```
+
+Benefits of using structured fragments:
+
+- **Consistent navigation**: Follows established patterns across MUI docs
+- **Deep linking**: Users can link directly to specific sections and variants
+- **History behavior**: Back button navigates between major sections, not every variant change
+- **Accessibility**: Screen readers can better understand content structure
+
+## When Not to Use
+
+- **Sensitive Data**: Don't store sensitive information in URL hashes
+- **Large State Objects**: Avoid storing complex state that makes URLs unwieldy
+- **Frequently Changing State**: Don't sync rapidly changing state that would spam browser history
+- **Private State**: Use for state that's appropriate to be visible in URLs
diff --git a/docs/app/docs-infra/layout.tsx b/docs/app/docs-infra/layout.tsx
new file mode 100644
index 000000000..332d5b025
--- /dev/null
+++ b/docs/app/docs-infra/layout.tsx
@@ -0,0 +1,24 @@
+import * as React from 'react';
+import type { Metadata } from 'next';
+import Link from 'next/link';
+import styles from '../layout.module.css';
+
+export const metadata: Metadata = {
+ title: 'MUI Docs Infra Documentation',
+ description: 'How to use the MUI Docs-Infra package',
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ MUI Docs Infra
+
+
{children}
+
+ );
+}
diff --git a/docs/app/docs-infra/page.mdx b/docs/app/docs-infra/page.mdx
new file mode 100644
index 000000000..f016e113c
--- /dev/null
+++ b/docs/app/docs-infra/page.mdx
@@ -0,0 +1,72 @@
+# Docs Infra
+
+This is the documentation for the MUI Internal Docs Infra package. It provides components and utilities for building documentation sites.
+
+You can install this package using:
+
+```bash variant=pnpm
+pnpm install @mui/internal-docs-infra
+```
+
+```bash variant=yarn
+yarn add @mui/internal-docs-infra
+```
+
+```bash variant=npm
+npm install @mui/internal-docs-infra
+```
+
+# Components
+
+These usually do not add additional HTML elements to the page, but rather provide functionality to wrap developer provided components.
+
+Some fundamental components are:
+
+- [`CodeHighlighter`](./components/code-highlighter/page.mdx): A component for displaying and rendering code snippets with syntax highlighting.
+
+See the full list of components in the [Components section](./components/page.mdx).
+
+# Hooks
+
+These are React hooks that provide client-side functionality for building documentation sites.
+
+Some fundamental hooks are:
+
+- [`useCode`](./hooks/use-code/page.mdx): A hook for rendering code blocks
+- [`useDemo`](./hooks/use-demo/page.mdx): A hook for rendering demos
+
+See the full list of hooks in the [Hooks section](./hooks/page.mdx).
+
+# Functions
+
+These are utility functions that provide functionality outside of React components.
+
+Some fundamental functions are:
+
+- [`abstractCreateDemo`](./functions/abstract-create-demo/page.mdx) - Factory utilities for creating structured demos that work with CodeHighlighter
+- [`loadPrecomputedCodeHighlighter`](./functions/load-precomputed-code-highlighter/page.mdx) - Webpack loader for build-time demo optimization
+- [`transformMarkdownCode`](./functions/transform-markdown-code/page.mdx) - Remark plugin for transforming markdown code blocks with variants into HTML structures
+- [`transformMarkdownRelativePaths`](./transform-markdown-relative-paths/page.mdx) - Remark plugin for transforming relative markdown links to absolute paths
+
+See the full list of functions in the [Functions section](./functions/page.mdx).
+
+# Patterns
+
+These are architectural patterns and best practices for building documentation infrastructure.
+
+Some key patterns are:
+
+- [`Built Factories`](./patterns/built-factories/page.mdx): A pattern for creating factory functions that use `import.meta.url` as a starting point for operations.
+- [`Props Context Layering`](./patterns/props-context-layering/page.mdx): A pattern for creating isomorphic components with React Server Components using context layering.
+
+See the full list of patterns in the [Patterns section](./patterns/page.mdx).
+
+# Contributing
+
+If you want to contribute to the MUI Docs Infra project, please read the [Contributing guide](./contributing/page.mdx).
+
+# License & Use
+
+This project is licensed under the [MIT License](https://github.com/mui/mui-public/blob/master/LICENSE).
+
+This is an internal project, and is not intended for public use. No support or stability guarantees are provided. Use at your own risk.
diff --git a/docs/app/docs-infra/patterns/built-factories/page.mdx b/docs/app/docs-infra/patterns/built-factories/page.mdx
new file mode 100644
index 000000000..9a4ba1f12
--- /dev/null
+++ b/docs/app/docs-infra/patterns/built-factories/page.mdx
@@ -0,0 +1,250 @@
+# Built Factories Pattern
+
+At its core, a factory only needs a URL. In TypeScript we can rely on `import.meta.url` as the starting point for any operation.
+
+```ts
+// src/objects/object.ts
+import { createObject } from '../createObject';
+
+export const object = createObject(import.meta.url);
+```
+
+We treat `file:///src/objects/object.ts` as the factory's input.
+By using the module URL, we can lean on filesystem-based routing.
+
+Because we defined `createObject`, we control how it works.
+We know that object names follow the pattern `file:///src/objects/(?[^/]+)\.ts`.
+From any layer (build time, server runtime, or client render), we can derive the object name `object`.
+
+We can perform any async task to produce the `object` instance: read the filesystem, query a database, or call an external API.
+
+But the index file for this object stays the same. This lets you change `createObject` without affecting potentially hundreds of index files.
+
+The index file can be copy-pasted many times; only customized factories differ.
+
+## Build
+
+During build time, a loader can access `import.meta.url` and know which object is being created. It can then inject the object into an options parameter:
+
+```ts
+// src/objects/object.ts
+import { createObject } from '../createObject';
+
+export const object = createObject(import.meta.url, { precompute: {} });
+```
+
+The loader returns this after creating the object instance. `createObject` can then skip async work and return the precomputed object.
+
+This is powerful for filesystem‑derived objects, because loaders can declare dependent files. When those change, the cache is busted.
+
+So if you had a store at `data/objects/object.json`, you could read it during build. If it changes, the object is re-created.
+
+This especially helps when objects are expensive to create.
+
+Not all objects need build-time creation. Some can't be precomputed. The index file shouldn't need to change if you later decide to create objects during a client render.
+
+## Options
+
+Factories can take options to augment behavior. For example, you might override the derived name:
+
+```ts
+// src/objects/object.ts
+import { createObject } from '../createObject';
+
+export const object = createObject(import.meta.url, { name: 'CustomObjectName' });
+```
+
+They can also accept flags:
+
+```ts
+export const object = createObject(import.meta.url, { isVisible: true });
+```
+
+Or functions:
+
+```ts
+export const object = createObject(import.meta.url, { getName: () => 'CustomObjectName' });
+```
+
+Or additional data:
+
+```ts
+export const object = createObject(import.meta.url, { data: { key: 'value' } });
+```
+
+Anything useful for creating the object can go in options.
+
+## Imported Sources
+
+Sometimes the object is created using an importable source.
+
+A simple example: a code snippet from an external module:
+
+```ts
+// src/externalModule.ts
+// This is an external function
+export const externalFunction = () => {
+ // Some implementation
+};
+```
+
+```ts
+// src/objects/object.ts
+import { createSnippet } from '../createSnippet';
+import { externalFunction } from '../externalModule';
+
+export const snippet = createSnippet(import.meta.url, externalFunction);
+```
+
+At build time the snippet could be injected:
+
+```ts
+// src/objects/object.ts
+import { createSnippet } from '../createSnippet'
+import { externalFunction } from '../externalModule'
+
+export const snippet = createSnippet(import.meta.url, externalFunction, precompute: `// This is an external function
+export const externalFunction = () => {
+ // Some implementation
+}
+`)
+```
+
+Then `createSnippet` has both the executable function and its source text.
+
+Sometimes objects have variations users can choose from, each imported as a source.
+
+```ts
+// src/objects/object.ts
+import { createSnippet } from '../createSnippet';
+import { externalFunctionA } from '../externalModuleA';
+import { externalFunctionB } from '../externalModuleB';
+
+export const snippet = createSnippet(import.meta.url, {
+ A: externalFunctionA,
+ B: externalFunctionB,
+});
+```
+
+which could be precomputed as:
+
+```ts
+// src/objects/object.ts
+import { createSnippet } from '../createSnippet';
+import { externalFunctionA } from '../externalModuleA';
+import { externalFunctionB } from '../externalModuleB';
+
+export const snippet = createSnippet(
+ import.meta.url,
+ { A: externalFunctionA, B: externalFunctionB },
+ {
+ precompute: {
+ A: `// This is an external function A
+export const externalFunctionA = () => {
+ // Some implementation
+}
+`,
+ B: `// This is an external function B
+export const externalFunctionB = () => {
+ // Some implementation
+}
+`,
+ },
+ },
+);
+```
+
+You can also add options when using imported sources:
+
+```ts
+export const snippet = createSnippet(
+ import.meta.url,
+ { A: externalFunctionA, B: externalFunctionB },
+ { stripComments: true },
+);
+```
+
+If the snippet isn't generated at build time, we still have enough info to load the code on the server or client.
+
+## Strong Typing
+
+Next.js has challenges defining TypeScript types for exports in `page.tsx` files.
+
+This pattern avoids them because it mandates a factory function that supplies types.
+
+## Centralized Configuration
+
+The actual `createObject` factory centralizes shared behavior across all objects created with it.
+
+For example:
+
+```ts
+// src/createObject.ts
+const DEBUG = true;
+
+export const createObject = (url: string, options: any) => {
+ const { object, headers } = fetch(url.replace('file:///', 'http://example.com/'));
+ if (DEBUG) {
+ return { object, headers };
+ }
+
+ return { object };
+};
+```
+
+Changing the config inside `createObject` affects all objects created with it.
+
+This is instrumental in a library that provides abstract factories:
+
+```ts
+// src/createObject.ts
+import abstractCreateObject from 'lib/abstractCreateObject';
+
+export const createObject = abstractCreateObject({
+ debug: true,
+});
+```
+
+You can migrate between an abstract factory maintained elsewhere and a custom implementation without moving the config.
+
+You can also pass one factory's result into another; each can be cached by its own dependency graph.
+
+## Aligned with Next.js App Router
+
+```
+/app/component/page.tsx <-- 'component' page
+/app/component/layout.tsx <-- 'component' layout
+/app/component/object.ts <-- 'component' object (using createObject factory)
+/app/component/snippets/simple/index.ts <-- 'component' snippet 'simple' (using createSnippet factory)
+/app/component/snippets/simple/page.tsx <-- 'component' snippet 'simple' page using ./index
+```
+
+Names come from the filesystem instead of being hardcoded twice in factory params.
+
+This pattern can extend Next.js filesystem-based routing.
+
+## Essential Implementation Notes
+
+Keep the mental model simple:
+
+**Call shape:** `createX(import.meta.url, variants?, options?)`. The URL is the identity. Variants are optional. Options are optional.
+
+**Variants:** A single component becomes the `Default` variant. An object literal lists named variants (`{ TypeScript, JavaScript }`). That's all most cases need.
+
+**One per file:** Use exactly one factory call per file. Loaders enforce this for deterministic transforms.
+
+**Precompute:** Build tooling can replace `precompute: true` (or an existing object) with generated data (`precompute: { ... }`). Server precompute injects variant code metadata; client precompute injects only an externals map. Runtime code doesn't change—only the factory implementation or loader evolves.
+
+**Options:** Pass metadata (`name`, `slug`, flags). Unknown keys are fine; they flow through. Use `skipPrecompute: true` to leave the call untouched.
+
+**Server vs Client:** A server factory (no `'use client'`) declares variants and can precompute heavy code metadata. A separate client factory file (with `'use client'`) has no variants—tooling looks at the sibling server file to know them—and only injects the externals it truly needs.
+
+**Composition:** You can expose a lightweight client wrapper as a variant inside the server factory to strictly control what reaches the client bundle.
+
+**Benefit:** Precomputation removes runtime syntax highlighting + dependency resolution cost, shrinking client work.
+
+These basics are enough to adopt the pattern. For implementation details, see the related function docs above.
+
+### Server / Client Boundary Constraint
+
+A single factory call runs entirely on either the server or the client—never both. If you need specific imports or data to ship to the client bundle you must define a _separate_ client factory file (with `'use client'`). The server factory can reference that client factory (e.g. as a variant or option), but it cannot implicitly “bridge” code across the boundary. This explicit duplication (one factory per boundary) guarantees predictable bundle contents.
diff --git a/docs/app/docs-infra/patterns/page.mdx b/docs/app/docs-infra/patterns/page.mdx
new file mode 100644
index 000000000..0c142846e
--- /dev/null
+++ b/docs/app/docs-infra/patterns/page.mdx
@@ -0,0 +1,4 @@
+# Patterns
+
+- [`Built Factories`](./built-factories/page.mdx): A pattern for creating factory functions that use `import.meta.url` as a starting point for operations.
+- [`Props Context Layering`](./props-context-layering/page.mdx): A pattern for creating isomorphic components with React Server Components using context layering.
diff --git a/docs/app/docs-infra/patterns/props-context-layering/page.mdx b/docs/app/docs-infra/patterns/props-context-layering/page.mdx
new file mode 100644
index 000000000..a7db196da
--- /dev/null
+++ b/docs/app/docs-infra/patterns/props-context-layering/page.mdx
@@ -0,0 +1,301 @@
+# Props Context Layering
+
+**Purpose**: Enable isomorphic components to work seamlessly with React Server Components while maintaining a single, ergonomic API.
+
+**Core Strategy**: Render early with initial props, then progressively enhance via context when additional data/functions become available on the client.
+
+This pattern solves a fundamental challenge: components need to render before the server-client boundary is crossed, but the complete data they need might only be available after client-side processing.
+
+## Server-Client Boundary Constraints
+
+A key driver for this pattern is React Server Components' serialization limitations:
+
+**Cannot pass across server-client boundary:**
+
+- Functions (event handlers, utilities, transformers)
+- Class instances (complex objects, parsed data structures)
+- Non-serializable values (Date objects, Map/Set, symbols)
+
+**Can pass across server-client boundary:**
+
+- Primitive values (strings, numbers, booleans)
+- Plain objects and arrays
+- **React Nodes** (pre-rendered JSX elements)
+
+**Example of the constraint:**
+
+```tsx
+// This won't work - functions can't serialize
+ console.log(data)}
+ parser={parseComplexData}
+/>
+
+// This works - React Nodes can serialize
+
+
+
+
+```
+
+**Why Props Context Layering helps:**
+
+- **Props**: Carry serializable data and pre-rendered React Nodes from server
+- **Context**: Provide functions and complex objects on the client side
+- **Result**: Same API works in both environments without serialization issues
+
+---
+
+## 1. Early rendering with fallback values
+
+Components must render on the server before crossing the client boundary. When users pass components as props to server components, those child components are immediately rendered—often before all ideal props are available.
+
+**The challenge**: You need to display something useful immediately while waiting for enhanced data.
+
+**The solution**: Accept whatever props are available initially, then seamlessly upgrade via context without changing the component's API.
+
+**Implementation pattern**: Create a custom hook that merges props (immediate) with context (enhanced) values.
+
+## 2. Conditional async operations
+
+**The problem**: The same component may run in either server or client environments. Async server components will throw errors when executed on the client.
+
+**The solution**: Guard async operations behind conditions: only execute them when the data is actually needed and the environment supports it.
+
+**When to defer async work**:
+
+- Props already contain the required data → skip async fetching
+- Heavy functions are missing → assume they'll be provided later via context
+- `forceClient` flag is set → defer to `useEffect` instead of server async
+
+**Example scenarios**:
+
+```tsx
+// IsomorphicData decides whether to fetch on the server or defer to the client.
+// 1. If data already provided -> render immediately.
+// 2. If data missing & we can run async on the server (not forceClient) -> render an async loader.
+// 3. Otherwise -> return a client component that will load data after hydration.
+
+// Synchronous decision component (never async itself)
+function IsomorphicData({ data, url, forceClient = false }) {
+ if (data) return ; // Already have data
+ if (!forceClient && url) return ; // Let server do async
+ return ; // Defer to client
+}
+
+// Async server-capable loader (can be an RSC async component)
+async function ServerDataLoader({ url }) {
+ const res = await fetch(url);
+ const fetched = await res.json();
+ return ;
+}
+
+// Client-only loader (separate module/file marked with 'use client')
+// File: ClientDataLoader.tsx
+// 'use client';
+import React, { useEffect, useState } from 'react';
+
+function ClientDataLoader({ initialData, url }) {
+ const [loaded, setLoaded] = useState(initialData);
+
+ useEffect(() => {
+ if (!loaded && url) {
+ fetch(url)
+ .then((r) => r.json())
+ .then(setLoaded)
+ .catch(() => {}); // swallow / handle errors as desired
+ }
+ }, [loaded, url]);
+
+ return ;
+}
+
+// Usage - the same props API; behavior chosen automatically
+ ;
+```
+
+Heavy functions can be provided either as props (server-side) or context (client-side). When using the [Built Factories pattern](../built-factories/page.mdx), expensive operations can even run at build time with caching.
+
+## 3. Props-first, context-enhanced hooks
+
+**The pattern**: Create hooks that accept props and automatically layer context updates on top. This hides the complexity from consumers while enabling progressive enhancement.
+
+**Real-world example** from this codebase:
+
+```ts
+// useErrors hook - implements Props → Context Layering
+function useErrors(props?: { errors?: Error[] }) {
+ const context = useErrorsContext();
+
+ // Context errors override props errors (latest wins)
+ const errors = context?.errors || props?.errors;
+
+ return { errors };
+}
+
+// Usage in components
+function ErrorHandler({ errors }: { errors?: Error[] }) {
+ const { errors: effectiveErrors } = useErrors({ errors });
+
+ if (!effectiveErrors?.length) return null;
+ return ;
+}
+```
+
+**Another example** - the `useCode` hook:
+
+```ts
+function useCode(contentProps, opts) {
+ const context = useCodeHighlighterContextOptional();
+
+ // Context code overrides contentProps code when available
+ const effectiveCode = context?.code || contentProps.code || {};
+
+ // Context URL overrides contentProps URL
+ const effectiveUrl = context?.url || contentProps.url;
+
+ // ... rest of implementation
+}
+```
+
+**Benefits**:
+
+- **Server components** can pass data via props normally
+- **Client components** get enhanced data via context automatically
+- **Same API** works in both environments
+- **No mental overhead** for consumers—they just pass props
+
+**Component flexibility**: The same component can perform their async task as either a server or client component:
+
+```ts
+// Server component version (no client code needed)
+function ObjectHandler({ object }: { object: any }) {
+ return ;
+}
+
+// Client component version (with context enhancement)
+'use client';
+function ObjectHandler({ object }: { object: any }) {
+ const { object: effectiveObject } = useObject({ object });
+ return ;
+}
+```
+
+Both preserve the same API shape and timing semantics.
+
+## 4. Lazy-load heavy functions
+
+**The goal**: Avoid shipping expensive functions to the client unless actually needed.
+
+**The strategy**:
+
+- Heavy functions are **imported conditionally**—if not imported, they're not bundled
+- Provide them via props (server-side) or context (client-side) only when needed
+- Keep the core component logic lightweight
+
+**Example**:
+
+```tsx
+// Three deployment modes for heavy functions (parsers, transformers, etc.)
+
+// 1. SERVER RENDERING: Import and pass directly as props (never reaches client bundle).
+// File: ServerPage.tsx (React Server Component)
+import 'server-only';
+
+import { expensiveParser, complexTransformer } from './heavyUtils';
+
+export function ServerRenderedFeature({ source }) {
+ return ;
+}
+
+// 2. CLIENT RENDERING: Provide lazily through a Provider that only loads after hydration.
+// File: HeavyFunctionsProvider.tsx
+('use client');
+import React, { useCallback, useState, useEffect } from 'react';
+
+const HeavyFunctionsContext = React.createContext(null);
+export function useHeavyFunctions() {
+ return React.useContext(HeavyFunctionsContext);
+}
+
+export function HeavyFunctionsProvider({ children, preload = false }) {
+ const [fns, setFns] = useState(null);
+
+ const ensureLoaded = useCallback(async () => {
+ if (!fns) {
+ const mod = await import('./heavyUtils'); // code-split boundary
+ setFns({ parser: mod.expensiveParser, transform: mod.complexTransformer });
+ }
+ }, [fns]);
+
+ // Optional eager load (e.g., user interacted or heuristic)
+ useEffect(() => {
+ if (preload) ensureLoaded();
+ }, [preload, ensureLoaded]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Usage inside a client component
+function ClientFeature({ source }) {
+ const heavy = useHeavyFunctions();
+ // Trigger load only if user expands advanced panel, etc.
+ const onDemand = () => heavy?.ensureLoaded();
+ const parsed = heavy?.parser ? heavy.parser(source) : null; // fallback UI until ready
+ return ;
+}
+
+// 3. BUILD-TIME RENDERING (no runtime import):
+// In a build script or static generation step you run heavy logic once and serialize results.
+// File: build/generate-data.ts (executed at build time, not bundled for runtime)
+import { expensiveParser, complexTransformer } from '../src/heavyUtils';
+import fs from 'node:fs';
+const raw = fs.readFileSync('content.txt', 'utf8');
+const parsed = complexTransformer(expensiveParser(raw));
+fs.writeFileSync('dist/precomputed.json', JSON.stringify(parsed));
+
+// File: PrecomputedFeature.tsx (RSC or client) - ONLY loads JSON, not heavy functions.
+import precomputed from '../../dist/precomputed.json';
+export function PrecomputedFeature() {
+ return ; // heavy functions never shipped
+}
+```
+
+**Tip - Works with Built Factories**: This build-time path composes directly with the [Built Factories pattern](../built-factories/page.mdx). Instead of hand‑writing a separate script you can let a factory call (`createX(import.meta.url, variants?, options?)`) produce and cache the heavy result via a `precompute` injection. Tooling (loader / build step) replaces the original call with one that includes `precompute: { ... }`, so runtime code:
+
+- Keeps the one-line factory contract (identity = `import.meta.url`)
+- Ships only the precomputed data object (no parser / transformer code)
+- Lets a server factory precompute rich metadata while a sibling client factory only receives the minimal externals map
+- Still supports progressive enhancement: props carry precomputed output early, context can re-load heavy functions later (client provider with dynamic `import()`) for live editing or re-parsing
+- Avoids dynamic `import()` entirely when live mutation isn't needed; add it back only in the client enhancement path
+
+If requirements change (need variants, extra metadata, live editing), you update the shared factory implementation—call sites and component APIs stay stable, and Props Context layering continues to deliver the upgraded client experience without breaking server renders.
+
+**Outcome**: Minimal initial bundle, rich functionality loads on-demand.
+
+---
+
+## Implementation Checklist
+
+When implementing Props → Context Layering:
+
+- **Create a merging hook** that accepts props and checks context
+- **Context values override props** (latest data wins)
+- **Handle undefined context gracefully** (server/client compatibility)
+- **Guard async operations** behind conditions
+- **Heavy functions via dynamic imports** + context providers
+- **Same component API** works in server and client environments
+- **Progressive enhancement** without breaking changes
+
+## Real-World Usage
+
+This pattern is used throughout the docs-infra system:
+
+- **[`useErrors`](../../hooks/use-errors/page.mdx)**: Server-side syntax errors → client-side runtime errors
+- **[`useCode`](../../hooks/use-code/page.mdx)**: Static code props → dynamic context code
+- **[`useDemo`](../../hooks/use-demo/page.mdx)**: Build-time demos → interactive client demos
+- **[`CodeHighlighter`](../../components/code-highlighter/page.mdx)**: Server highlighting → client enhancement
diff --git a/docs/app/layout.module.css b/docs/app/layout.module.css
new file mode 100644
index 000000000..0b3593047
--- /dev/null
+++ b/docs/app/layout.module.css
@@ -0,0 +1,105 @@
+.body {
+ font-family: var(--font-geist-sans);
+ margin: 0;
+ margin-bottom: 20px;
+ background: color(display-p3 0.995 0.988 0.996);
+ background: #fefcfe;
+}
+
+.container {
+ max-width: 792px;
+ margin: 0 auto;
+ padding: 0 20px 12px 20px;
+}
+
+.header {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 60px;
+ background-color: color(display-p3 0.983 0.971 0.993);
+ background-color: #fbf7fe;
+ border-bottom: 1px solid color(display-p3 0.86 0.774 0.942);
+ border-bottom: 1px solid #e0c4f4;
+}
+
+.header a {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: color(display-p3 0.234 0.132 0.363);
+ color: #402060;
+ text-decoration: none;
+}
+
+.body code {
+ font-family: var(--font-geist-mono);
+ font-size: 14px;
+ background: color(display-p3 0.995 0.971 0.974);
+ background: #fff7f8;
+}
+
+.body code {
+ color: color(display-p3 0.728 0.211 0.311);
+ color: #ca244d;
+ text-decoration-color: color(display-p3 0.728 0.211 0.311);
+ text-decoration-color: #ca244d;
+}
+
+.body a:has(code) {
+ text-decoration-color: color(display-p3 0.728 0.211 0.311);
+ text-decoration-color: #ca244d;
+}
+
+.body a:visited code {
+ background: color(display-p3 0.966 0.983 0.964);
+ background: #f5fbf5;
+ color: color(display-p3 0.263 0.488 0.261);
+ color: #2a7e3b;
+}
+
+.body a:visited:has(code) {
+ text-decoration-color: color(display-p3 0.263 0.488 0.261);
+ text-decoration-color: #2a7e3b;
+}
+
+.body a {
+ color: color(display-p3 0.15 0.44 0.84);
+ color: #0d74ce;
+}
+
+.body a:visited {
+ color: color(display-p3 0.473 0.281 0.687);
+ color: #8145b5;
+}
+
+.body {
+ color: color(display-p3 0.128 0.122 0.147);
+ color: #211f26;
+}
+
+.body h1,
+.body h2,
+.body h3,
+.body h4,
+.body h5,
+.body h6 {
+ color: color(display-p3 0.473 0.281 0.687);
+ color: #8145b5;
+}
+
+.body h1 > a,
+.body h2 > a,
+.body h3 > a,
+.body h4 > a,
+.body h5 > a,
+.body h6 > a {
+ color: color(display-p3 0.473 0.281 0.687);
+ color: #8145b5;
+ text-decoration-color: color(display-p3 0.473 0.281 0.687);
+ text-decoration-color: #8145b5;
+}
+
+.body pre > code {
+ color: initial;
+ background: initial;
+}
diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx
new file mode 100644
index 000000000..fec2e5c06
--- /dev/null
+++ b/docs/app/layout.tsx
@@ -0,0 +1,33 @@
+import * as React from 'react';
+import type { Metadata } from 'next';
+import { Geist, Geist_Mono } from 'next/font/google';
+import styles from './layout.module.css';
+
+const geistSans = Geist({
+ variable: '--font-geist-sans',
+ subsets: ['latin'],
+});
+
+const geistMono = Geist_Mono({
+ variable: '--font-geist-mono',
+ subsets: ['latin'],
+});
+
+export const metadata: Metadata = {
+ title: 'MUI Infra Documentation',
+ description: 'How to use the MUI Infra packages',
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/docs/components/Blockquote/Blockquote.module.css b/docs/components/Blockquote/Blockquote.module.css
new file mode 100644
index 000000000..b8f5537d7
--- /dev/null
+++ b/docs/components/Blockquote/Blockquote.module.css
@@ -0,0 +1,67 @@
+.root {
+ color: #211f26;
+ border-left: 4px solid #d0cdd7;
+ padding: 8px 16px;
+ margin: 0 0 16px 0;
+}
+
+.root p {
+ margin: 0;
+}
+
+.root .title {
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+.title > svg {
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+ vertical-align: -2px;
+}
+
+.root[data-callout-type='note'] {
+ border-left-color: color(display-p3 0.247 0.556 0.969);
+ border-left-color: #0090ff;
+}
+.root[data-callout-type='note'] .title {
+ color: color(display-p3 0.15 0.44 0.84);
+ color: #0d74ce;
+}
+
+.root[data-callout-type='tip'] {
+ border-left-color: color(display-p3 0.38 0.647 0.378);
+ border-left-color: #46a758;
+}
+.root[data-callout-type='tip'] .title {
+ color: color(display-p3 0.263 0.488 0.261);
+ color: #2a7e3b;
+}
+
+.root[data-callout-type='important'] {
+ border-left-color: color(display-p3 0.523 0.318 0.751);
+ border-left-color: #8e4ec6;
+}
+.root[data-callout-type='important'] .title {
+ color: color(display-p3 0.473 0.281 0.687);
+ color: #8145b5;
+}
+
+.root[data-callout-type='warning'] {
+ border-left-color: color(display-p3 1 0.92 0.22);
+ border-left-color: #ffe629;
+}
+.root[data-callout-type='warning'] .title {
+ color: color(display-p3 0.6 0.44 0);
+ color: #9e6c00;
+}
+
+.root[data-callout-type='caution'] {
+ border-left-color: color(display-p3 0.831 0.345 0.231);
+ border-left-color: #e54d2e;
+}
+.root[data-callout-type='caution'] .title {
+ color: color(display-p3 0.755 0.259 0.152);
+ color: #d13415;
+}
diff --git a/docs/components/Blockquote/Blockquote.tsx b/docs/components/Blockquote/Blockquote.tsx
new file mode 100644
index 000000000..830eb0215
--- /dev/null
+++ b/docs/components/Blockquote/Blockquote.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react';
+import styles from './Blockquote.module.css';
+
+type BlockquoteProps = {
+ children: React.ReactNode;
+ [key: string]: unknown; // Allow additional props
+};
+
+const svg: Record = {
+ note: (
+
+
+
+ ),
+ tip: (
+
+
+
+ ),
+ important: (
+
+
+
+ ),
+ warning: (
+
+
+
+ ),
+ caution: (
+
+
+
+ ),
+};
+
+export default function Blockquote(props: BlockquoteProps) {
+ const { children, ...otherProps } = props;
+
+ let calloutType =
+ typeof props['data-callout-type'] === 'string' ? props['data-callout-type'] : undefined;
+ const icon = calloutType && svg[calloutType];
+
+ if (calloutType) {
+ calloutType = `${calloutType.charAt(0).toUpperCase()}${calloutType.slice(1)}`;
+ }
+
+ return (
+
+ {calloutType && (
+
+ {icon}
+ {calloutType}
+
+ )}
+ {children}
+
+ );
+}
diff --git a/docs/components/Blockquote/index.ts b/docs/components/Blockquote/index.ts
new file mode 100644
index 000000000..f5825f9d8
--- /dev/null
+++ b/docs/components/Blockquote/index.ts
@@ -0,0 +1 @@
+export { default as Blockquote } from './Blockquote';
diff --git a/docs/components/Checkbox/index.module.css b/docs/components/Checkbox/index.module.css
new file mode 100644
index 000000000..784751c84
--- /dev/null
+++ b/docs/components/Checkbox/index.module.css
@@ -0,0 +1,61 @@
+.checkbox {
+ position: relative;
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+}
+
+.checkbox input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.checkbox .checkmark {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 20px;
+ height: 20px;
+ background-color: #eee;
+ border-radius: 4px;
+ border: 2px solid #ddd;
+ transition: all 0.2s ease;
+}
+
+.checkbox input:checked + .checkmark {
+ background-color: #9a5cd0;
+ border-color: #9a5cd0;
+}
+
+.checkbox .checkmark:after {
+ content: '';
+ position: absolute;
+ display: none;
+}
+
+.checkbox input:checked + .checkmark:after {
+ display: block;
+}
+
+.checkbox .checkmark:after {
+ left: 7px;
+ top: 3px;
+ width: 4px;
+ height: 9px;
+ border: solid white;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
diff --git a/docs/components/Checkbox/index.tsx b/docs/components/Checkbox/index.tsx
new file mode 100644
index 000000000..722154fb4
--- /dev/null
+++ b/docs/components/Checkbox/index.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import * as React from 'react';
+import styles from './index.module.css';
+
+type CheckboxProps = {
+ defaultChecked: boolean;
+ name?: string;
+ className?: string;
+ style?: React.CSSProperties;
+};
+
+// This component mainly serves as a mock for a Checkbox component used in demos.
+
+export function Checkbox({ defaultChecked, name = 'checkbox', className, style }: CheckboxProps) {
+ const [checked, setChecked] = React.useState(defaultChecked);
+ const onChange = React.useCallback(() => {
+ setChecked((prev) => !prev);
+ }, []);
+
+ return (
+
+
+
+ Checkbox
+
+ );
+}
diff --git a/docs/components/Code/index.ts b/docs/components/Code/index.ts
new file mode 100644
index 000000000..41abdcbb0
--- /dev/null
+++ b/docs/components/Code/index.ts
@@ -0,0 +1,2 @@
+// dog-food the demo content
+export * from '../../app/docs-infra/components/code-highlighter/demos/Code';
diff --git a/docs/components/CodeContent/index.ts b/docs/components/CodeContent/index.ts
new file mode 100644
index 000000000..bb68605f9
--- /dev/null
+++ b/docs/components/CodeContent/index.ts
@@ -0,0 +1,2 @@
+// dog-food the demo content
+export * from '../../app/docs-infra/components/code-highlighter/demos/CodeContent';
diff --git a/docs/components/CopyButton/CopyButton.module.css b/docs/components/CopyButton/CopyButton.module.css
new file mode 100644
index 000000000..14c50de17
--- /dev/null
+++ b/docs/components/CopyButton/CopyButton.module.css
@@ -0,0 +1,25 @@
+.copyButton {
+ background: none;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px;
+ border-radius: 12px;
+ color: #0400119c;
+ transition: background-color 0.2s ease;
+}
+
+.copyButton:hover:not(:disabled) {
+ background-color: #30004010;
+}
+
+.copyButton:active:not(:disabled) {
+ background-color: #20003820;
+}
+
+.copyButton:disabled {
+ cursor: default;
+ color: #4caf50;
+}
diff --git a/docs/components/CopyButton/CopyButton.tsx b/docs/components/CopyButton/CopyButton.tsx
new file mode 100644
index 000000000..5514a2738
--- /dev/null
+++ b/docs/components/CopyButton/CopyButton.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+import styles from './CopyButton.module.css';
+
+type CopyButtonProps = {
+ copy: (event: React.MouseEvent) => Promise;
+ copyDisabled?: boolean;
+};
+
+export function CopyButton({ copy, copyDisabled }: CopyButtonProps) {
+ return (
+
+ {copyDisabled ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+}
diff --git a/docs/components/CopyButton/index.ts b/docs/components/CopyButton/index.ts
new file mode 100644
index 000000000..367e65cb2
--- /dev/null
+++ b/docs/components/CopyButton/index.ts
@@ -0,0 +1 @@
+export * from './CopyButton';
diff --git a/docs/components/DemoContent/index.ts b/docs/components/DemoContent/index.ts
new file mode 100644
index 000000000..d775afea9
--- /dev/null
+++ b/docs/components/DemoContent/index.ts
@@ -0,0 +1,2 @@
+// dog-food the demo content
+export * from '../../app/docs-infra/components/code-highlighter/demos/DemoContent';
diff --git a/docs/components/FileConventions/FileConventions.tsx b/docs/components/FileConventions/FileConventions.tsx
new file mode 100644
index 000000000..f78ac039a
--- /dev/null
+++ b/docs/components/FileConventions/FileConventions.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import { getFileConventions } from '@mui/internal-docs-infra/pipeline/getFileConventions';
+import Link from 'next/link';
+
+export async function FileConventions() {
+ const conventions = await getFileConventions();
+
+ return (
+
+ {conventions.map((convention, i) => (
+
+ {convention.rule} -{' '}
+ {convention.loader}
+
+ ))}
+
+ );
+}
diff --git a/docs/components/FileConventions/index.ts b/docs/components/FileConventions/index.ts
new file mode 100644
index 000000000..00b819f8c
--- /dev/null
+++ b/docs/components/FileConventions/index.ts
@@ -0,0 +1 @@
+export * from './FileConventions';
diff --git a/docs/components/LabeledSwitch/LabeledSwitch.module.css b/docs/components/LabeledSwitch/LabeledSwitch.module.css
new file mode 100644
index 000000000..f5c2bb047
--- /dev/null
+++ b/docs/components/LabeledSwitch/LabeledSwitch.module.css
@@ -0,0 +1,67 @@
+.root {
+ display: flex;
+ width: 88px;
+ height: 28px;
+}
+
+.indicator {
+ position: absolute;
+ height: 28px;
+ width: 44px;
+ background-color: color(display-p3 0.57 0.373 0.791);
+ background-color: #9a5cd0;
+ z-index: -100;
+ border-radius: 12px 0 0 12px;
+ transition: all 0.2s ease;
+}
+
+.indicator.checked {
+ transform: translateX(45px);
+ border-radius: 0 12px 12px 0;
+}
+
+.segment {
+ flex: 1;
+ padding: 0;
+ font-size: 1rem;
+ line-height: 1;
+ border: 1px solid #10003332;
+ border: 1px solid color(display-p3 0.067 0.008 0.184 / 0.197);
+ background-color: transparent;
+ color: #0400119c;
+ color: color(display-p3 0.016 0 0.059 / 0.612);
+ cursor: pointer;
+ transition:
+ background 0.2s,
+ color 0.2s,
+ border-color 0.2s;
+}
+
+.segment + .segment {
+ margin-left: -1px;
+ border-left: none;
+}
+
+.segment:nth-child(2) {
+ border-radius: 12px 0 0 12px;
+}
+.segment:nth-child(2).active {
+ border-radius: 12px 0 0 12px;
+}
+.segment:last-child {
+ border-radius: 0 12px 12px 0;
+}
+
+.segment.active {
+ background-color: transparent;
+ border-color: color(display-p3 0.57 0.373 0.791);
+ border-color: #9a5cd0;
+ color: #fff;
+}
+
+.segment:hover {
+ background-color: #30004010;
+ background-color: color(display-p3 0.129 0.008 0.255 / 0.063);
+ border-color: #30004010;
+ border-color: color(display-p3 0.129 0.008 0.255 / 0.063);
+}
diff --git a/docs/components/LabeledSwitch/LabeledSwitch.tsx b/docs/components/LabeledSwitch/LabeledSwitch.tsx
new file mode 100644
index 000000000..b5e6a47ec
--- /dev/null
+++ b/docs/components/LabeledSwitch/LabeledSwitch.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react';
+import { Toggle } from '@base-ui-components/react/toggle';
+import { ToggleGroup } from '@base-ui-components/react/toggle-group';
+import styles from './LabeledSwitch.module.css';
+
+/**
+ * A two-option switch with labels.
+ * @param checked the currently selected value
+ * @param onCheckedChange called when the value changes
+ * @param labels to show for each option, e.g. { false: 'TS', true: 'JS' }
+ * @param defaultChecked the initial value when the component mounts
+ */
+export function LabeledSwitch({
+ checked,
+ onCheckedChange,
+ labels,
+ defaultChecked,
+}: {
+ checked: boolean | undefined;
+ onCheckedChange: (checked: boolean) => void;
+ labels: { false: string; true: string };
+ defaultChecked?: boolean;
+}) {
+ const handleChange = React.useCallback(
+ (value: string[]) => {
+ if (value.length === 0) {
+ return;
+ }
+
+ if (value.length === 1) {
+ const newChecked = value[0] === 'true';
+ onCheckedChange(newChecked);
+ } else {
+ const newChecked = !checked;
+ onCheckedChange(newChecked);
+ }
+ },
+ [checked, onCheckedChange],
+ );
+
+ return (
+
+
+
+ {labels.false}
+
+
+ {labels.true}
+
+
+ );
+}
diff --git a/docs/components/LabeledSwitch/index.ts b/docs/components/LabeledSwitch/index.ts
new file mode 100644
index 000000000..b4442fc70
--- /dev/null
+++ b/docs/components/LabeledSwitch/index.ts
@@ -0,0 +1 @@
+export * from './LabeledSwitch';
diff --git a/docs/components/Pre/Pre.module.css b/docs/components/Pre/Pre.module.css
new file mode 100644
index 000000000..2c4c6dfc9
--- /dev/null
+++ b/docs/components/Pre/Pre.module.css
@@ -0,0 +1,13 @@
+.root {
+ font-family: var(--font-geist-mono);
+ font-size: 13px;
+ border: 1px solid #d0cdd7;
+ padding: 8px;
+ border-radius: 8px;
+}
+
+.root pre {
+ overflow-x: auto;
+ padding: 8px;
+ margin: 0;
+}
diff --git a/docs/components/Pre/Pre.tsx b/docs/components/Pre/Pre.tsx
new file mode 100644
index 000000000..b8fecf583
--- /dev/null
+++ b/docs/components/Pre/Pre.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter';
+import type { CodeHighlighterProps } from '@mui/internal-docs-infra/CodeHighlighter/types';
+import { CodeContent } from '../CodeContent';
+
+type PreProps = {
+ 'data-precompute'?: string;
+};
+
+export function Pre(props: PreProps) {
+ if (!props['data-precompute']) {
+ return (
+
+ Expected precompute data to be provided. Ensure that transformHtmlCode rehype plugin is
+ used.
+
+ );
+ }
+
+ const precompute = JSON.parse(
+ props['data-precompute'],
+ ) as CodeHighlighterProps['precompute'];
+
+ return ;
+}
diff --git a/docs/components/Pre/index.ts b/docs/components/Pre/index.ts
new file mode 100644
index 000000000..876a84b3e
--- /dev/null
+++ b/docs/components/Pre/index.ts
@@ -0,0 +1 @@
+export * from './Pre';
diff --git a/docs/components/Select/Select.module.css b/docs/components/Select/Select.module.css
new file mode 100644
index 000000000..a16920154
--- /dev/null
+++ b/docs/components/Select/Select.module.css
@@ -0,0 +1,217 @@
+.Select {
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ height: 1.75rem;
+ padding-left: 12px;
+ padding-right: 10px;
+ margin: 0;
+ outline: 0;
+ border: 1px solid #d0cdd7;
+ border-radius: 12px;
+ font-family: inherit;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ color: #65636d;
+ cursor: default;
+ user-select: none;
+ min-width: 140px;
+
+ @media (hover: hover) {
+ &:hover {
+ background-color: #f2eff3;
+ }
+ }
+
+ &:active {
+ background-color: #f2eff3;
+ }
+
+ &[data-popup-open] {
+ background-color: #f2eff3;
+ }
+
+ &:focus-visible {
+ outline: 2px solid color(display-p3 0.523 0.318 0.751);
+ outline: 2px solid #8e4ec6;
+ outline-offset: -1px;
+ }
+}
+
+.Select[aria-disabled='true'] {
+ opacity: 0.7;
+ cursor: progress;
+}
+
+.SelectIcon {
+ display: flex;
+}
+
+.Popup {
+ box-sizing: border-box;
+ padding-block: 0.25rem;
+ border-radius: 0.375rem;
+ background-color: canvas;
+ color: #65636d;
+ transform-origin: var(--transform-origin);
+ transition:
+ transform 150ms,
+ opacity 150ms;
+ overflow-y: auto;
+ max-height: var(--available-height);
+
+ &[data-starting-style],
+ &[data-ending-style] {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+
+ &[data-side='none'] {
+ transition: none;
+ transform: none;
+ opacity: 1;
+ }
+
+ @media (prefers-color-scheme: light) {
+ outline: 1px solid #d0cdd7;
+ box-shadow:
+ 0 10px 15px -3px #d0cdd7,
+ 0 4px 6px -4px #d0cdd7;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ outline: 1px solid var(--color-gray-300);
+ outline-offset: -1px;
+ }
+}
+
+.Arrow {
+ display: flex;
+
+ &[data-side='top'] {
+ bottom: -8px;
+ rotate: 180deg;
+ }
+
+ &[data-side='bottom'] {
+ top: -8px;
+ rotate: 0deg;
+ }
+
+ &[data-side='left'] {
+ right: -13px;
+ rotate: 90deg;
+ }
+
+ &[data-side='right'] {
+ left: -13px;
+ rotate: -90deg;
+ }
+}
+
+.ArrowFill {
+ fill: canvas;
+}
+
+.ArrowOuterStroke {
+ @media (prefers-color-scheme: light) {
+ fill: #d0cdd7;
+ }
+}
+
+.ArrowInnerStroke {
+ @media (prefers-color-scheme: dark) {
+ fill: #d0cdd7;
+ }
+}
+
+.Item {
+ box-sizing: border-box;
+ outline: 0;
+ font-size: 0.875rem;
+ line-height: 1rem;
+ padding-block: 0.5rem;
+ padding-left: 0.625rem;
+ padding-right: 1rem;
+ min-width: var(--anchor-width);
+ display: grid;
+ gap: 0.5rem;
+ align-items: center;
+ grid-template-columns: 0.75rem 1fr;
+ cursor: default;
+ user-select: none;
+ scroll-margin-block: 1rem;
+
+ [data-side='none'] & {
+ font-size: 1rem;
+ padding-right: 3rem;
+ min-width: calc(var(--anchor-width) + 1rem);
+ }
+
+ &[data-highlighted] {
+ z-index: 0;
+ position: relative;
+ }
+
+ &[data-highlighted]::before {
+ content: '';
+ z-index: -1;
+ position: absolute;
+ inset-block: 0;
+ inset-inline: 0.25rem;
+ border-radius: 12px;
+ background-color: #f2eff3;
+ }
+}
+
+.ItemIndicator {
+ grid-column-start: 1;
+}
+
+.ItemIndicatorIcon {
+ display: block;
+ width: 0.75rem;
+ height: 0.75rem;
+}
+
+.ItemText {
+ grid-column-start: 2;
+}
+
+.ScrollArrow {
+ width: 100%;
+ background: canvas;
+ z-index: 1;
+ text-align: center;
+ cursor: default;
+ border-radius: 0.375rem;
+ height: 1rem;
+ font-size: 0.75rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &::before {
+ content: '';
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ }
+
+ &[data-direction='up'] {
+ &::before {
+ top: -100%;
+ }
+ }
+
+ &[data-direction='down'] {
+ bottom: 0;
+
+ &::before {
+ bottom: -100%;
+ }
+ }
+}
diff --git a/docs/components/Select/Select.tsx b/docs/components/Select/Select.tsx
new file mode 100644
index 000000000..9927b4b84
--- /dev/null
+++ b/docs/components/Select/Select.tsx
@@ -0,0 +1,64 @@
+import * as React from 'react';
+import { Select as SelectParts } from '@base-ui-components/react/select';
+import styles from './Select.module.css';
+
+export interface Props {
+ items: { label: string; value: string }[];
+ value?: string;
+ onValueChange?: (value: string) => void;
+ disabled?: boolean;
+}
+
+export function Select({ items, value, onValueChange, disabled }: Props) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {items.map(({ label, value: itemValue }) => (
+
+
+
+
+ {label}
+
+ ))}
+
+
+
+
+
+ );
+}
+
+function ChevronUpDownIcon(props: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+
+ );
+}
+
+function CheckIcon(props: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ );
+}
diff --git a/docs/components/Select/index.ts b/docs/components/Select/index.ts
new file mode 100644
index 000000000..7868ecbae
--- /dev/null
+++ b/docs/components/Select/index.ts
@@ -0,0 +1 @@
+export * from './Select';
diff --git a/docs/components/Tabs/Tabs.module.css b/docs/components/Tabs/Tabs.module.css
new file mode 100644
index 000000000..f0241d28d
--- /dev/null
+++ b/docs/components/Tabs/Tabs.module.css
@@ -0,0 +1,113 @@
+.tabsRoot {
+ display: flex;
+}
+
+.tabsList {
+ display: flex;
+ gap: 0px;
+ overflow-x: auto;
+ max-width: 100%;
+ padding-top: 14px;
+}
+
+.name {
+ display: flex;
+ align-items: center;
+ height: 48px;
+}
+
+.name:hover {
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.name span {
+ color: #65636d;
+ padding: 8px 12px;
+ font-size: 12px;
+ font-family: var(--font-geist-mono);
+}
+
+.tab {
+ padding: 10px 8px 8px 8px;
+ border: 1px solid #d0cdd7;
+ border-bottom: none;
+ background-color: transparent;
+ color: #65636d;
+ cursor: pointer;
+ font-size: 12px;
+ font-family: var(--font-geist-mono);
+ transition: all 0.2s ease;
+ position: relative;
+ z-index: 1;
+ margin-left: 0;
+ margin-right: 0;
+ white-space: nowrap;
+}
+
+.tab:hover:not(.tabSelected) {
+ background-color: #30004010;
+}
+
+.tab:active:not(.tabSelected) {
+ background-color: #20003820;
+}
+
+.tab[aria-disabled='true'] {
+ opacity: 0.7;
+ cursor: progress;
+}
+
+.tabSelected {
+ border-radius: 12px 12px 0 0;
+ background-color: color(display-p3 0.523 0.318 0.751);
+ background-color: #8e4ec6;
+ border-color: color(display-p3 0.523 0.318 0.751);
+ border-color: #8e4ec6;
+ color: #ffffff;
+ z-index: 2;
+}
+
+.tabSelected:active {
+ border-radius: 16px 16px 0 0;
+}
+
+.tabFirst {
+ border-radius: 12px 0 0 0;
+}
+
+.tabLast {
+ border-radius: 0 12px 0 0;
+}
+
+.tabMiddle {
+ border-radius: 0;
+}
+
+.tabNextSelected {
+ padding: 10px 20px 8px 8px;
+ margin-right: -12px;
+}
+
+.tabPrevSelected {
+ padding: 10px 8px 8px 20px;
+ margin-left: -12px;
+}
+
+.tabNoBorderRight {
+ border-right: 1px solid transparent;
+}
+
+.tabNoBorderRight:not(.tabNextSelected) {
+ margin-right: -1px;
+}
+
+.tabWithBorderRight {
+ border-right: 1px solid #d0cdd7;
+}
+
+.tabSelected.tabWithBorderRight {
+ border-right: 1px solid #8e4ec6;
+ border-color: color(display-p3 0.523 0.318 0.751);
+ border-color: #8e4ec6;
+}
diff --git a/docs/components/Tabs/Tabs.tsx b/docs/components/Tabs/Tabs.tsx
new file mode 100644
index 000000000..a6f5dadf9
--- /dev/null
+++ b/docs/components/Tabs/Tabs.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+import { Tabs as TabsParts } from '@base-ui-components/react/tabs';
+import styles from './Tabs.module.css';
+
+export interface Tab {
+ name: string;
+ id: string;
+}
+
+export interface TabsProps {
+ tabs: Tab[];
+ selectedTabId?: string;
+ onTabSelect: (tabId: string) => void;
+ disabled?: boolean;
+}
+
+export function Tabs({ tabs, selectedTabId, onTabSelect, disabled }: TabsProps) {
+ const clickName = React.useCallback(() => {
+ onTabSelect(tabs[0].id);
+ }, [onTabSelect, tabs]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ onTabSelect(tabs[0].id);
+ }
+ },
+ [onTabSelect, tabs],
+ );
+
+ if (tabs.length <= 1) {
+ return tabs.length === 1 ? (
+
+ {tabs[0].name}
+
+ ) : null;
+ }
+
+ return (
+
+
+ {tabs.map((tab, index) => {
+ const isSelected = selectedTabId ? tab.id === selectedTabId : index === 0;
+ const isFirst = index === 0;
+ const isLast = index === tabs.length - 1;
+ const nextTabSelected = index < tabs.length - 1 && tabs[index + 1].id === selectedTabId;
+ const prevTabSelected = index > 0 && tabs[index - 1].id === selectedTabId;
+
+ const tabClasses = [
+ styles.tab,
+ isSelected && styles.tabSelected,
+ !isSelected && isFirst && styles.tabFirst,
+ !isSelected && isLast && styles.tabLast,
+ !isSelected && !isFirst && !isLast && styles.tabMiddle,
+ nextTabSelected && styles.tabNextSelected,
+ prevTabSelected && styles.tabPrevSelected,
+ isLast || isSelected ? styles.tabWithBorderRight : styles.tabNoBorderRight,
+ ]
+ .filter(Boolean)
+ .join(' ');
+
+ return (
+
+ {tab.name}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/docs/components/Tabs/index.ts b/docs/components/Tabs/index.ts
new file mode 100644
index 000000000..d87151ff6
--- /dev/null
+++ b/docs/components/Tabs/index.ts
@@ -0,0 +1 @@
+export { Tabs, type Tab, type TabsProps } from './Tabs';
diff --git a/docs/components/index.ts b/docs/components/index.ts
new file mode 100644
index 000000000..d87151ff6
--- /dev/null
+++ b/docs/components/index.ts
@@ -0,0 +1 @@
+export { Tabs, type Tab, type TabsProps } from './Tabs';
diff --git a/docs/functions/createDemo/index.ts b/docs/functions/createDemo/index.ts
new file mode 100644
index 000000000..988a50dea
--- /dev/null
+++ b/docs/functions/createDemo/index.ts
@@ -0,0 +1,2 @@
+// dog-food the demo content
+export * from '../../app/docs-infra/components/code-highlighter/demos/createDemo';
diff --git a/docs/mdx-components.tsx b/docs/mdx-components.tsx
new file mode 100644
index 000000000..677af8091
--- /dev/null
+++ b/docs/mdx-components.tsx
@@ -0,0 +1,11 @@
+import type { MDXComponents } from 'mdx/types';
+import Blockquote from './components/Blockquote/Blockquote';
+import { Pre } from './components/Pre';
+
+export function useMDXComponents(components: MDXComponents): MDXComponents {
+ return {
+ ...components,
+ blockquote: Blockquote,
+ pre: Pre,
+ };
+}
diff --git a/docs/next.config.mjs b/docs/next.config.mjs
new file mode 100644
index 000000000..4cc6bdd82
--- /dev/null
+++ b/docs/next.config.mjs
@@ -0,0 +1,33 @@
+import createMDX from '@next/mdx';
+import { withDocsInfra, getDocsInfraMdxOptions } from '@mui/internal-docs-infra/withDocsInfra';
+import bundleAnalyzer from '@next/bundle-analyzer';
+
+const withBundleAnalyzer = bundleAnalyzer({
+ enabled: process.env.ANALYZE === 'true',
+});
+
+// Create MDX with docs-infra configuration
+const withMDX = createMDX({
+ options: getDocsInfraMdxOptions({
+ additionalRemarkPlugins: [],
+ additionalRehypePlugins: [],
+ }),
+});
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ // Your custom configuration here
+ // The withDocsInfra plugin will add the necessary docs infrastructure setup
+};
+
+export default withBundleAnalyzer(
+ withDocsInfra({
+ // Add demo-* patterns specific to this docs site
+ additionalDemoPatterns: {
+ // Note: The demo-* pattern below is specific to our internal docs structure
+ // where we create "demos of demos". This is not a typical use case.
+ index: ['./app/**/demos/*/demo-*/index.ts'],
+ client: ['./app/**/demos/*/demo-*/client.ts'],
+ },
+ })(withMDX(nextConfig)),
+);
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 000000000..776bf047f
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@mui/internal-infra-docs",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --turbopack",
+ "build": "next build",
+ "build-exp": "next build --turbopack",
+ "start": "serve ./out"
+ },
+ "dependencies": {
+ "@base-ui-components/react": "1.0.0-beta.1",
+ "@mdx-js/loader": "^3.1.0",
+ "@mdx-js/react": "^3.1.0",
+ "@mui/internal-docs-infra": "workspace:^",
+ "@next/mdx": "^15.3.4",
+ "@types/mdx": "^2.0.13",
+ "next": "v15.5.2",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-runner": "^1.0.5",
+ "remark-gfm": "^4.0.1",
+ "server-only": "^0.0.1",
+ "use-editable": "^2.3.3",
+ "vscode-oniguruma": "^2.0.1"
+ },
+ "devDependencies": {
+ "@next/bundle-analyzer": "^15.5.2",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "@wooorm/starry-night": "^3.8.0",
+ "serve": "^14.2.5",
+ "typescript": "^5"
+ }
+}
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
new file mode 100644
index 000000000..6b1070751
--- /dev/null
+++ b/docs/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index fa63843b8..b947239c9 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -2,6 +2,7 @@ import { defineConfig } from 'eslint/config';
import {
createBaseConfig,
createTestConfig,
+ createDocsConfig,
EXTENSION_TEST_FILE,
EXTENSION_TS,
} from '@mui/internal-code-infra/eslint';
@@ -42,6 +43,20 @@ export default /** @type {import('eslint').Linter.Config[]} */ (
],
extends: createTestConfig(),
},
+ {
+ files: ['docs/**/*'],
+ extends: createDocsConfig(),
+ settings: {
+ 'import/resolver': {
+ typescript: {
+ project: ['docs/tsconfig.json'],
+ },
+ },
+ },
+ rules: {
+ '@next/next/no-img-element': 'off',
+ },
+ },
{
files: [`apps/**/*${EXTENSION_TS}`],
rules: {
diff --git a/package.json b/package.json
index 890f1a334..7716d4b0f 100644
--- a/package.json
+++ b/package.json
@@ -17,8 +17,9 @@
"size:snapshot": "pnpm -F ./test/bundle-size check",
"size:why": "pnpm size:snapshot --analyze",
"clean": "pnpm -r exec rm -rf build",
- "docs:build": "pnpm -F \"./packages/docs-infra\" run build",
- "docs:test": "pnpm -F \"./packages/docs-infra\" run test"
+ "docs:dev": "pnpm -F \"./docs\" run dev",
+ "docs:build": "pnpm -F \"./docs\" run build",
+ "docs:start": "pnpm -F \"./docs\" run start"
},
"pnpm": {
"packageExtensions": {
diff --git a/packages/code-infra/README.md b/packages/code-infra/README.md
index c5d4bdd80..252bfb954 100644
--- a/packages/code-infra/README.md
+++ b/packages/code-infra/README.md
@@ -2,6 +2,14 @@
Scripts and configs to be used across MUI repos.
+## Documentation
+
+This is stored in the `docs` top-level directory.
+
+[Read in Markdown](../../docs/app/code-infra/page.mdx)
+
+[Read in Browser](https://infra.mui.com/code-infra)
+
## Publishing packages
1. Go to the publish action -
diff --git a/packages/code-infra/src/eslint/docsConfig.mjs b/packages/code-infra/src/eslint/docsConfig.mjs
index 6144cc16d..bc933b3fb 100644
--- a/packages/code-infra/src/eslint/docsConfig.mjs
+++ b/packages/code-infra/src/eslint/docsConfig.mjs
@@ -11,6 +11,7 @@ export function createDocsConfig() {
rootDir: 'docs',
},
},
+ files: ['**/*.js', '**/*.mjs', '**/*.jsx', '**/*.ts', '**/*.tsx'],
rules: {
'compat/compat': 'off',
'jsx-a11y/anchor-is-valid': 'off',
diff --git a/packages/docs-infra/README.md b/packages/docs-infra/README.md
index 66c3188f1..455f7700a 100644
--- a/packages/docs-infra/README.md
+++ b/packages/docs-infra/README.md
@@ -4,6 +4,7 @@ This package hosts the tools that help create the documentation.
## Documentation
-This is stored in the `docs` directory.
+This is stored in the `docs` top-level directory.
-[Read More](../../docs/app/docs-infra/page.mdx)
+[Read in Markdown](../../docs/app/docs-infra/page.mdx)
+[Read in Browser](https://infra.mui.com/docs-infra)
diff --git a/packages/docs-infra/package.json b/packages/docs-infra/package.json
index f08c50b3a..ec7464609 100644
--- a/packages/docs-infra/package.json
+++ b/packages/docs-infra/package.json
@@ -22,6 +22,10 @@
"./usePreference": "./src/usePreference/index.ts",
"./useUrlHashState": "./src/useUrlHashState/index.ts",
"./withDocsInfra": "./src/withDocsInfra/index.ts",
+ "./pipeline/getFileConventions": "./src/pipeline/getFileConventions/index.ts",
+ "./pipeline/transformMarkdownBlockquoteCallouts": "./src/pipeline/transformMarkdownBlockquoteCallouts/index.ts",
+ "./pipeline/transformMarkdownDemoLinks": "./src/pipeline/transformMarkdownDemoLinks/index.ts",
+ "./pipeline/transformMarkdownRelativePaths": "./src/pipeline/transformMarkdownRelativePaths/index.ts",
"./pipeline/hastUtils": "./src/pipeline/hastUtils/index.ts",
"./pipeline/loadCodeVariant": "./src/pipeline/loadCodeVariant/index.ts",
"./pipeline/loaderUtils": "./src/pipeline/loaderUtils/index.ts",
@@ -56,7 +60,7 @@
"release": "pnpm build && pnpm publish --no-git-checks",
"test": "pnpm -w test --project @mui/internal-docs-infra",
"test:watch": "pnpm -w test:watch --project @mui/internal-docs-infra",
- "test:coverage": "pnpm -w test --project @mui/internal-docs-infra --coverage --coverage.include=packages/docs-infra --coverage.exclude=packages/docs-infra/build --coverage.exclude=packages/docs-infra/scripts",
+ "test:coverage": "pnpm -w test --project @mui/internal-docs-infra --coverage --coverage.include=packages/docs-infra --coverage.exclude=packages/docs-infra/docs --coverage.exclude=packages/docs-infra/build --coverage.exclude=packages/docs-infra/scripts",
"typescript": "tsc -p tsconfig.json"
},
"dependencies": {
diff --git a/packages/docs-infra/src/pipeline/getFileConventions/fileConventions.ts b/packages/docs-infra/src/pipeline/getFileConventions/fileConventions.ts
new file mode 100644
index 000000000..9bdcb6c68
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/getFileConventions/fileConventions.ts
@@ -0,0 +1,3 @@
+export const fileConventions = [
+ { rule: './app/**/demos/*/index.ts', loader: 'loadPrecomputedCodeHighlighter' },
+];
diff --git a/packages/docs-infra/src/pipeline/getFileConventions/getFileConventions.ts b/packages/docs-infra/src/pipeline/getFileConventions/getFileConventions.ts
new file mode 100644
index 000000000..3c6a98831
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/getFileConventions/getFileConventions.ts
@@ -0,0 +1,5 @@
+import { fileConventions } from './fileConventions';
+
+export async function getFileConventions() {
+ return fileConventions; // TODO: Parse the next.config.js file to get convention overrides.
+}
diff --git a/packages/docs-infra/src/pipeline/getFileConventions/index.ts b/packages/docs-infra/src/pipeline/getFileConventions/index.ts
new file mode 100644
index 000000000..3bef9febc
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/getFileConventions/index.ts
@@ -0,0 +1 @@
+export * from './getFileConventions';
diff --git a/packages/docs-infra/src/pipeline/loadCodeVariant/calculateMainFilePath.ts b/packages/docs-infra/src/pipeline/loadCodeVariant/calculateMainFilePath.ts
index 477fdd220..4e121b2e3 100644
--- a/packages/docs-infra/src/pipeline/loadCodeVariant/calculateMainFilePath.ts
+++ b/packages/docs-infra/src/pipeline/loadCodeVariant/calculateMainFilePath.ts
@@ -1,4 +1,4 @@
-import { createSyntheticDirectories, buildPath } from './pathUtils.js';
+import { createSyntheticDirectories, buildPath } from './pathUtils';
export function calculateMainFilePath(
url: string,
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/index.ts b/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/index.ts
new file mode 100644
index 000000000..4c42b26c6
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/index.ts
@@ -0,0 +1,5 @@
+// This is the export format expected by a remark plugin.
+
+import { transformMarkdownBlockquoteCallouts } from './transformMarkdownBlockquoteCallouts';
+
+export default transformMarkdownBlockquoteCallouts;
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/integration.test.ts b/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/integration.test.ts
new file mode 100644
index 000000000..41d80fa0a
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/integration.test.ts
@@ -0,0 +1,56 @@
+import { describe, it, expect } from 'vitest';
+import { unified } from 'unified';
+import remarkParse from 'remark-parse';
+import remarkRehype from 'remark-rehype';
+import rehypeStringify from 'rehype-stringify';
+import { transformMarkdownBlockquoteCallouts } from './transformMarkdownBlockquoteCallouts';
+
+describe('remarkBlockquoteCallouts integration', () => {
+ it('should work in a complete pipeline like Next.js MDX', async () => {
+ const processor = unified()
+ .use(remarkParse)
+ .use(transformMarkdownBlockquoteCallouts)
+ .use(remarkRehype)
+ .use(rehypeStringify);
+
+ const markdown = `
+# Test Document
+
+> [!NOTE]
+> This is a note callout with **bold text**.
+
+> [!TIP]
+> This is a tip with a [link](https://example.com).
+
+> Regular blockquote without callouts.
+
+> [!WARNING]
+> Multi-line warning
+>
+> With multiple paragraphs.
+`;
+
+ const result = await processor.process(markdown);
+ const html = result.toString();
+
+ // Check that callouts are processed correctly
+ expect(html).toContain('');
+ expect(html).toContain('');
+ expect(html).toContain('');
+
+ // Check that regular blockquote doesn't have data attribute
+ expect(html).toContain('\nRegular blockquote');
+
+ // Check that callout markers are removed
+ expect(html).not.toContain('[!NOTE]');
+ expect(html).not.toContain('[!TIP]');
+ expect(html).not.toContain('[!WARNING]');
+
+ // Check that markdown content is still processed
+ expect(html).toContain('bold text ');
+ expect(html).toContain('link ');
+
+ // Check that the HTML structure is correct
+ expect(html).toContain('
Test Document ');
+ });
+});
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/transformMarkdownBlockquoteCallouts.test.ts b/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/transformMarkdownBlockquoteCallouts.test.ts
new file mode 100644
index 000000000..a562c9a1f
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/transformMarkdownBlockquoteCallouts.test.ts
@@ -0,0 +1,161 @@
+import { describe, it, expect } from 'vitest';
+import { unified } from 'unified';
+import remarkParse from 'remark-parse';
+import remarkRehype from 'remark-rehype';
+import rehypeStringify from 'rehype-stringify';
+import type { Blockquote } from 'mdast';
+import { transformMarkdownBlockquoteCallouts } from './transformMarkdownBlockquoteCallouts';
+
+describe('transformMarkdownBlockquoteCallouts', () => {
+ const createProcessor = () => {
+ return unified()
+ .use(remarkParse)
+ .use(transformMarkdownBlockquoteCallouts)
+ .use(remarkRehype)
+ .use(rehypeStringify);
+ };
+
+ const processMarkdown = async (markdown: string) => {
+ const processor = createProcessor();
+ const result = await processor.process(markdown);
+ return result.toString();
+ };
+
+ const getAstFromMarkdown = async (markdown: string) => {
+ const processor = unified().use(remarkParse).use(transformMarkdownBlockquoteCallouts);
+ const tree = await processor.run(processor.parse(markdown));
+ return tree as any;
+ };
+
+ it('should add data-callout-type attribute for NOTE callout', async () => {
+ const markdown = '> [!NOTE]\n> This is a note.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="note"');
+ expect(html).toContain('This is a note.
');
+ expect(html).not.toContain('[!NOTE]');
+ });
+
+ it('should add data-callout-type attribute for TIP callout', async () => {
+ const markdown = '> [!TIP]\n> This is a tip.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="tip"');
+ expect(html).toContain('This is a tip.
');
+ expect(html).not.toContain('[!TIP]');
+ });
+
+ it('should add data-callout-type attribute for IMPORTANT callout', async () => {
+ const markdown = '> [!IMPORTANT]\n> This is important.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="important"');
+ expect(html).toContain('This is important.
');
+ expect(html).not.toContain('[!IMPORTANT]');
+ });
+
+ it('should add data-callout-type attribute for WARNING callout', async () => {
+ const markdown = '> [!WARNING]\n> This is a warning.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="warning"');
+ expect(html).toContain('This is a warning.
');
+ expect(html).not.toContain('[!WARNING]');
+ });
+
+ it('should add data-callout-type attribute for CAUTION callout', async () => {
+ const markdown = '> [!CAUTION]\n> This is a caution.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="caution"');
+ expect(html).toContain('This is a caution.
');
+ expect(html).not.toContain('[!CAUTION]');
+ });
+
+ it('should handle callouts with extra whitespace', async () => {
+ const markdown = '> [!NOTE] This is a note with extra spaces.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="note"');
+ expect(html).toContain('This is a note with extra spaces.
');
+ expect(html).not.toContain('[!NOTE]');
+ });
+
+ it('should handle callouts on same line as text', async () => {
+ const markdown = '> [!NOTE] This is a note on the same line.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="note"');
+ expect(html).toContain('This is a note on the same line.
');
+ expect(html).not.toContain('[!NOTE]');
+ });
+
+ it('should not modify blockquotes without callout markers', async () => {
+ const markdown = '> This is a regular blockquote.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).not.toContain('data-callout-type');
+ expect(html).toContain('This is a regular blockquote.
');
+ });
+
+ it('should not modify blockquotes with invalid callout types', async () => {
+ const markdown = '> [!INVALID] This is an invalid callout.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).not.toContain('data-callout-type');
+ expect(html).toContain('[!INVALID] This is an invalid callout.
');
+ });
+
+ it('should handle blockquotes with multiple paragraphs', async () => {
+ const markdown = '> [!NOTE] This is a note.\n>\n> This is another paragraph.';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="note"');
+ expect(html).toContain('This is a note.
');
+ expect(html).toContain('This is another paragraph.
');
+ expect(html).not.toContain('[!NOTE]');
+ });
+
+ it('should handle empty blockquotes', async () => {
+ const markdown = '>';
+ const html = await processMarkdown(markdown);
+
+ expect(html).not.toContain('data-callout-type');
+ });
+
+ it('should handle callouts that make the text node empty', async () => {
+ const markdown = '> [!NOTE]';
+ const tree = await getAstFromMarkdown(markdown);
+
+ const blockquote = tree.children[0] as Blockquote;
+ expect((blockquote.data as any)?.hProperties?.['data-callout-type']).toBe('note');
+
+ // The paragraph should still exist but be empty
+ expect(blockquote.children).toHaveLength(1);
+ expect(blockquote.children[0].type).toBe('paragraph');
+ expect((blockquote.children[0] as any).children).toHaveLength(0);
+ });
+
+ it('should preserve other blockquote content when processing callouts', async () => {
+ const markdown =
+ '> [!NOTE] Important note\n>\n> Additional content\n>\n> - List item\n> - Another item';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="note"');
+ expect(html).toContain('Important note
');
+ expect(html).toContain('Additional content
');
+ expect(html).toContain('List item ');
+ expect(html).toContain('Another item ');
+ expect(html).not.toContain('[!NOTE]');
+ });
+
+ it('should work with nested blockquotes', async () => {
+ const markdown = '> [!NOTE] Outer note\n>\n> > [!TIP] Inner tip';
+ const html = await processMarkdown(markdown);
+
+ expect(html).toContain('data-callout-type="note"');
+ expect(html).toContain('data-callout-type="tip"');
+ expect(html).toContain('Outer note
');
+ expect(html).toContain('Inner tip
');
+ });
+});
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/transformMarkdownBlockquoteCallouts.ts b/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/transformMarkdownBlockquoteCallouts.ts
new file mode 100644
index 000000000..ab4980c49
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/transformMarkdownBlockquoteCallouts/transformMarkdownBlockquoteCallouts.ts
@@ -0,0 +1,64 @@
+import { visit } from 'unist-util-visit';
+import type { Plugin } from 'unified';
+import type { Blockquote, Text } from 'mdast';
+
+/**
+ * Remark plugin that extracts GitHub-style callouts from blockquotes and injects them into data attributes.
+ *
+ * Transforms blockquotes like:
+ * > [!NOTE]
+ * > This is a note.
+ *
+ * Into blockquotes with a custom data attribute that will be preserved when converted to HTML:
+ *
+ * This is a note.
+ *
+ *
+ * Supported callout types: NOTE, TIP, IMPORTANT, WARNING, CAUTION
+ */
+export const transformMarkdownBlockquoteCallouts: Plugin = () => {
+ return (tree) => {
+ visit(tree, 'blockquote', (node: Blockquote) => {
+ // Find the first paragraph in the blockquote
+ const firstChild = node.children[0];
+ if (!firstChild || firstChild.type !== 'paragraph') {
+ return;
+ }
+
+ // Find the first text node in the paragraph
+ const firstTextNode = firstChild.children[0];
+ if (!firstTextNode || firstTextNode.type !== 'text') {
+ return;
+ }
+
+ const textNode = firstTextNode as Text;
+ const calloutPattern = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/;
+ const match = textNode.value.match(calloutPattern);
+
+ if (match) {
+ const calloutType = match[1].toLowerCase();
+
+ // Remove the callout marker from the text
+ const newText = textNode.value.replace(calloutPattern, '');
+
+ if (newText.trim() === '') {
+ // Remove the text node if it becomes empty
+ firstChild.children.shift();
+ } else {
+ // Update the text content
+ textNode.value = newText;
+ }
+
+ // Add the data attribute to the blockquote
+ // This creates a custom property that will be preserved when converting to HTML
+ if (!node.data) {
+ node.data = {};
+ }
+ if (!(node.data as any).hProperties) {
+ (node.data as any).hProperties = {};
+ }
+ (node.data as any).hProperties['data-callout-type'] = calloutType;
+ }
+ });
+ };
+};
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownCode/transformMarkdownCode.test.ts b/packages/docs-infra/src/pipeline/transformMarkdownCode/transformMarkdownCode.test.ts
index b7cc5aca0..373ac7b12 100644
--- a/packages/docs-infra/src/pipeline/transformMarkdownCode/transformMarkdownCode.test.ts
+++ b/packages/docs-infra/src/pipeline/transformMarkdownCode/transformMarkdownCode.test.ts
@@ -3,7 +3,7 @@ import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import { describe, it, expect } from 'vitest';
-import transformMarkdownCodeVariants from './index.js';
+import transformMarkdownCodeVariants from '.';
// Processor for testing AST structure
const astProcessor = unified().use(remarkParse).use(transformMarkdownCodeVariants);
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownDemoLinks/index.ts b/packages/docs-infra/src/pipeline/transformMarkdownDemoLinks/index.ts
new file mode 100644
index 000000000..ffa3e53b3
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/transformMarkdownDemoLinks/index.ts
@@ -0,0 +1,5 @@
+// This is the export format expected by a remark plugin.
+
+import { transformMarkdownDemoLinks } from './transformMarkdownDemoLinks';
+
+export default transformMarkdownDemoLinks;
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownDemoLinks/transformMarkdownDemoLinks.test.ts b/packages/docs-infra/src/pipeline/transformMarkdownDemoLinks/transformMarkdownDemoLinks.test.ts
new file mode 100644
index 000000000..eebab5fc8
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/transformMarkdownDemoLinks/transformMarkdownDemoLinks.test.ts
@@ -0,0 +1,653 @@
+import { unified } from 'unified';
+import remarkParse from 'remark-parse';
+import remarkRehype from 'remark-rehype';
+import rehypeStringify from 'rehype-stringify';
+import { describe, it, expect } from 'vitest';
+import transformMarkdownDemoLinks from '.';
+
+// Processor for testing AST structure
+const astProcessor = unified().use(remarkParse).use(transformMarkdownDemoLinks);
+
+// End-to-end processor for testing final HTML output
+const e2eProcessor = unified()
+ .use(remarkParse)
+ .use(transformMarkdownDemoLinks)
+ .use(remarkRehype, { allowDangerousHtml: true })
+ .use(rehypeStringify, { allowDangerousHtml: true });
+
+describe('transformMarkdownDemoLinks', () => {
+ describe('AST Structure Tests', () => {
+ it('should remove "[See Demo]" link and horizontal rule after Demo component', () => {
+ const markdown = `
+
+
+[See Demo](./demos/code/)
+
+---
+
+## Next Section
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 2 children: the demo paragraph and the heading
+ expect(ast.children).toHaveLength(2);
+
+ // First child should be the demo html node
+ const demoHtml = ast.children[0];
+ expect(demoHtml.type).toBe('html');
+ expect(demoHtml.value).toBe(' ');
+
+ // Second child should be the heading (the link and separator should be removed)
+ const heading = ast.children[1];
+ expect(heading.type).toBe('heading');
+ expect(heading.children[0].value).toBe('Next Section');
+ });
+
+ it('should handle multiple Demo patterns in the same document', () => {
+ const markdown = `
+
+
+[See Demo](./demos/first/)
+
+---
+
+Some content in between.
+
+
+
+[See Demo](./demos/second/)
+
+---
+
+Final content.
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 3 children: demo1, content paragraph, demo2, final content
+ expect(ast.children).toHaveLength(4);
+
+ // Check first demo
+ const firstDemo = ast.children[0];
+ expect(firstDemo.type).toBe('html');
+ expect(firstDemo.value).toBe(' ');
+
+ // Check content paragraph
+ const contentPara = ast.children[1];
+ expect(contentPara.type).toBe('paragraph');
+ expect(contentPara.children[0].value).toBe('Some content in between.');
+
+ // Check second demo
+ const secondDemo = ast.children[2];
+ expect(secondDemo.type).toBe('html');
+ expect(secondDemo.value).toBe(' ');
+
+ // Check final content
+ const finalContent = ast.children[3];
+ expect(finalContent.type).toBe('paragraph');
+ expect(finalContent.children[0].value).toBe('Final content.');
+ });
+
+ it('should NOT remove pattern when Demo is just the .Title', () => {
+ const markdown = `
+
+
+[See Demo](./demos/code/)
+
+---
+
+## Next Section
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 4 children: demo, link, separator, heading (nothing removed)
+ expect(ast.children).toHaveLength(4);
+
+ // Check that all original elements are preserved
+ expect(ast.children[0].type).toBe('paragraph'); // Demo
+ expect(ast.children[1].type).toBe('paragraph'); // Link
+ expect(ast.children[2].type).toBe('thematicBreak'); // Separator
+ expect(ast.children[3].type).toBe('heading'); // Heading
+ });
+
+ it('should remove "[See Demo]" link even when there is no horizontal rule', () => {
+ const markdown = `
+
+
+[See Demo](./demos/code/)
+
+## Next Section
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 2 children: demo and heading (link removed even without separator)
+ expect(ast.children).toHaveLength(2);
+
+ expect(ast.children[0].type).toBe('html'); // Demo
+ expect(ast.children[1].type).toBe('heading'); // Heading
+ });
+
+ it('should remove both "[See Demo]" link and horizontal rule when both are present', () => {
+ const markdown = `
+
+
+[See Demo](./demos/code/)
+
+---
+
+## Next Section
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 2 children: demo and heading (both link and separator removed)
+ expect(ast.children).toHaveLength(2);
+
+ expect(ast.children[0].type).toBe('html'); // Demo
+ expect(ast.children[1].type).toBe('heading'); // Heading
+ });
+
+ it('should NOT remove pattern when there is no "[See Demo]" link', () => {
+ const markdown = `
+
+
+[Different Link](./demos/code/)
+
+---
+
+## Next Section
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 3 children: demo, link, heading (HR removed even without "See Demo" link)
+ expect(ast.children).toHaveLength(3);
+
+ expect(ast.children[0].type).toBe('html'); // Demo
+ expect(ast.children[1].type).toBe('paragraph'); // Link (preserved)
+ expect(ast.children[2].type).toBe('heading'); // Heading
+ });
+
+ it('should handle Demo components mixed with other content', () => {
+ const markdown = `
+Some text before
+
+[See Demo](./demos/code/)
+
+---
+
+## Next Section
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should process Demo even when mixed with other content - remove See Demo link and HR
+ expect(ast.children).toHaveLength(2);
+
+ expect(ast.children[0].type).toBe('paragraph'); // Text with Demo (preserved)
+ expect(ast.children[1].type).toBe('heading'); // Heading
+ });
+
+ it('should handle Demo components with props', () => {
+ const markdown = `
+
+
+[See Demo](./demos/with-props/)
+
+---
+
+## Next Section
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 2 children: demo and heading (link and separator removed)
+ expect(ast.children).toHaveLength(2);
+
+ const demoHtml = ast.children[0];
+ expect(demoHtml.type).toBe('html');
+ expect(demoHtml.value).toBe(' ');
+ });
+
+ it('should handle self-closing and non-self-closing Demo components', () => {
+ const markdown = `
+
+
+[See Demo](./demos/self-closing/)
+
+---
+
+Content
+
+[See Demo](./demos/with-children/)
+
+---
+
+Final content.
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 3 children: demo1, demo2, final content
+ expect(ast.children).toHaveLength(3);
+
+ // Check first demo (self-closing)
+ const firstDemo = ast.children[0];
+ expect(firstDemo.type).toBe('html');
+ expect(firstDemo.value).toBe(' ');
+
+ // Check second demo (with children) - this stays as a paragraph structure
+ const secondDemo = ast.children[1];
+ expect(secondDemo.type).toBe('paragraph');
+ expect(secondDemo.children).toHaveLength(3); // opening tag, content, closing tag
+ expect(secondDemo.children[0].type).toBe('html');
+ expect(secondDemo.children[0].value).toBe('');
+ expect(secondDemo.children[1].type).toBe('text');
+ expect(secondDemo.children[1].value).toBe('Content');
+ expect(secondDemo.children[2].type).toBe('html');
+ expect(secondDemo.children[2].value).toBe(' ');
+ });
+
+ it('should handle empty Demo components', () => {
+ const markdown = `
+
+
+[See Demo](./demos/empty/)
+
+---
+
+## Next Section
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 2 children: demo and heading
+ expect(ast.children).toHaveLength(2);
+
+ const demoHtml = ast.children[0];
+ expect(demoHtml.type).toBe('html');
+ expect(demoHtml.value).toBe(' ');
+ });
+
+ it('should NOT process non-Demo HTML elements', () => {
+ const markdown = `
+Some content
+
+[See Demo](./demos/div/)
+
+---
+
+## Next Section
+`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have 4 children (nothing removed because it's not a Demo component)
+ expect(ast.children).toHaveLength(4);
+
+ expect(ast.children[0].type).toBe('html'); // div
+ expect(ast.children[1].type).toBe('paragraph'); // Link
+ expect(ast.children[2].type).toBe('thematicBreak'); // Separator
+ expect(ast.children[3].type).toBe('heading'); // Heading
+ });
+ });
+
+ describe('End-to-End HTML Output Tests', () => {
+ it('should produce clean HTML output with Demo component only', () => {
+ const markdown = `
+
+[See Demo](./demos/code/)
+
+---
+
+## Next Section`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // Demo component is preserved as raw HTML, but links and HR are removed
+ expect(result).toEqual(' \nNext Section ');
+ });
+
+ it('should handle multiple Demo patterns correctly in HTML output', () => {
+ const markdown = `
+
+[See Demo](./demos/first/)
+
+---
+
+Some content between demos.
+
+
+
+[See Demo](./demos/second/)
+
+---
+
+Final content.`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // Demo components preserved as raw HTML, links and HR removed
+ expect(result).toEqual(
+ ' \nSome content between demos.
\n \nFinal content.
',
+ );
+ });
+
+ it('should preserve Demo components with .Title in HTML output', () => {
+ const markdown = `
+
+[See Demo](./demos/code/)
+
+---
+
+## Next Section`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // All elements should be preserved since Demo.Title should NOT be processed
+ expect(result).toEqual(
+ '<DemoCodeHighlighter.Title />
\nSee Demo
\n \nNext Section ',
+ );
+ });
+
+ it('should handle Demo components with complex props in HTML output', () => {
+ const markdown = ` console.log('test')}
+/>
+
+[See Demo](./demos/advanced/)
+
+---
+
+## Next Section`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // Complex JSX syntax in multiline Demo components sometimes gets escaped by remark-rehype
+ // When this happens, the plugin doesn't process it (since it's text, not HTML), so everything is preserved
+ expect(result).toEqual(
+ '<DemoAdvanced\nvariant="complex"\ndata={{"key": "value"}}\nonEvent={() => console.log(\'test\')}\n/>
\nSee Demo
\n \nNext Section ',
+ );
+ });
+
+ it('should preserve other links that are not "See Demo"', () => {
+ const markdown = `
+
+[Different Link](./demos/code/)
+
+---
+
+## Next Section`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // Link should be preserved since it's not "See Demo", but HR is now removed after Demo components
+ // With allowDangerousHtml, Demo component appears as raw HTML even when not processed
+ expect(result).toEqual(
+ ' \nDifferent Link
\nNext Section ',
+ );
+ });
+
+ it('should remove "[See Demo]" link even without horizontal rule in HTML output', () => {
+ const markdown = `
+
+[See Demo](./demos/code/)
+
+## Next Section`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // Demo component preserved, See Demo link removed (no HR to remove)
+ expect(result).toEqual(' \nNext Section ');
+ });
+
+ it('should handle mixed content with demos and regular content', () => {
+ const markdown = `# Documentation
+
+Some introduction text.
+
+
+
+[See Demo](./demos/basic/)
+
+---
+
+## Features
+
+More documentation content.
+
+
+
+[See Demo](./demos/advanced/)
+
+---
+
+## Conclusion
+
+Final thoughts.`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // Demo components preserved as raw HTML, See Demo links and HR removed
+ expect(result).toEqual(
+ 'Documentation \nSome introduction text.
\n \nFeatures \nMore documentation content.
\n \nConclusion \nFinal thoughts.
',
+ );
+ });
+
+ it('should handle edge case with empty See Demo link', () => {
+ const markdown = `
+
+[See Demo]()
+
+---
+
+## Next Section`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // Should still process even if the link is empty
+ expect(result).toEqual(' \nNext Section ');
+ });
+
+ it('should handle Demo components nested in other HTML', () => {
+ const markdown = `
+
+[See Demo](./demos/nested/)
+
+---
+
+## Next Section`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // The nested Demo SHOULD be processed since it's in a valid HTML node that contains \nNext Section ');
+ });
+
+ it('should handle Demo components with line breaks', () => {
+ const markdown = `
+
+[See Demo](./demos/multiline/)
+
+---
+
+## Next Section`;
+
+ const result = e2eProcessor.processSync(markdown).toString();
+
+ // Multiline Demo component remains in paragraph tags, but See Demo link and HR are removed
+ expect(result).toMatch(/ <\/p>\n
Next Section<\/h2>/);
+ });
+ });
+
+ describe('Edge Cases', () => {
+ it('should handle document with only Demo pattern', () => {
+ const markdown = `
+
+[See Demo](./demos/only/)
+
+---`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should have only 1 child: the demo html node
+ expect(ast.children).toHaveLength(1);
+ expect(ast.children[0].type).toBe('html');
+ expect(ast.children[0].value).toBe(' ');
+ });
+
+ it('should handle malformed Demo tags gracefully', () => {
+ const markdown = ` {
+ const markdown = `
+
+[See Demo](./demos/case/)
+
+---
+
+## Next Section`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should NOT process because it's lowercase 'demo', not 'Demo'
+ expect(ast.children).toHaveLength(4);
+ });
+
+ it('should handle Demo.Title vs DemoTitle correctly', () => {
+ const markdown1 = `
+
+[See Demo](./demos/dot-title/)
+
+---`;
+
+ const markdown2 = `
+
+[See Demo](./demos/title/)
+
+---`;
+
+ const ast1 = astProcessor.runSync(astProcessor.parse(markdown1)) as any;
+ const ast2 = astProcessor.runSync(astProcessor.parse(markdown2)) as any;
+
+ // First should NOT be processed (contains .Title)
+ expect(ast1.children).toHaveLength(3);
+
+ // Second SHOULD be processed (DemoTitle, not Demo.Title)
+ expect(ast2.children).toHaveLength(1);
+ expect(ast2.children[0].type).toBe('html');
+ expect(ast2.children[0].value).toBe(' ');
+ });
+
+ it('should handle multiple horizontal rules correctly', () => {
+ const markdown = `
+
+[See Demo](./demos/test/)
+
+---
+
+---
+
+## Next Section`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Should process the first pattern (Demo + See Demo link + first HR) and preserve the second HR
+ expect(ast.children).toHaveLength(3);
+ expect(ast.children[0].type).toBe('html'); // Demo
+ expect(ast.children[1].type).toBe('thematicBreak'); // Second HR (preserved)
+ expect(ast.children[2].type).toBe('heading'); // Heading
+ });
+
+ it('should successfully process imported Demo components', () => {
+ // This simulates how imported MDX components appear in markdown
+ const markdown = `import { DemoCodeHighlighterCode } from './demos/code';
+
+
+
+[See Demo](./demos/code/)
+
+---
+
+## Next Section`;
+
+ const ast = astProcessor.runSync(astProcessor.parse(markdown)) as any;
+
+ // Plugin should work! It removes the "[See Demo]" link and HR
+ expect(ast.children).toHaveLength(3);
+
+ expect(ast.children[0].type).toBe('paragraph'); // import statement
+ expect(ast.children[1].type).toBe('html'); // becomes html node
+ expect(ast.children[1].value).toBe(' ');
+ expect(ast.children[2].type).toBe('heading'); // heading remains
+ });
+
+ it('should handle MDX JSX flow elements (simulated)', () => {
+ // This simulates an MDX JSX flow element directly in the AST
+ const mockAst = {
+ type: 'root',
+ children: [
+ {
+ type: 'mdxJsxFlowElement',
+ name: 'DemoCodeHighlighter',
+ attributes: [],
+ children: [],
+ },
+ {
+ type: 'paragraph',
+ children: [
+ {
+ type: 'link',
+ url: './demos/code/',
+ children: [{ type: 'text', value: 'See Demo' }],
+ },
+ ],
+ },
+ {
+ type: 'thematicBreak',
+ },
+ {
+ type: 'heading',
+ depth: 2,
+ children: [{ type: 'text', value: 'Next Section' }],
+ },
+ ],
+ };
+
+ // Create processor with our plugin and apply it
+ const processor = unified().use(transformMarkdownDemoLinks);
+ const result = processor.runSync(mockAst as any) as any;
+
+ // Should remove the link and HR, leaving just the Demo and heading
+ expect(result.children).toHaveLength(2);
+ expect(result.children[0].type).toBe('mdxJsxFlowElement');
+ expect(result.children[1].type).toBe('heading');
+ });
+ });
+});
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownDemoLinks/transformMarkdownDemoLinks.ts b/packages/docs-infra/src/pipeline/transformMarkdownDemoLinks/transformMarkdownDemoLinks.ts
new file mode 100644
index 000000000..2f016ea59
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/transformMarkdownDemoLinks/transformMarkdownDemoLinks.ts
@@ -0,0 +1,136 @@
+import type { Plugin } from 'unified';
+import type { Parent, PhrasingContent, Html, Paragraph } from 'mdast';
+
+// MDX JSX types
+interface MdxJsxFlowElement {
+ type: 'mdxJsxFlowElement';
+ name: string | null;
+ attributes: Array;
+ children: Array;
+}
+
+/**
+ * Remark plugin that cleans up demo patterns in markdown.
+ *
+ * Looks for patterns where a Demo component is followed by a "[See Demo]" link
+ * and optionally a horizontal rule (---). When found, removes the link and
+ * any following horizontal rule.
+ *
+ * This is useful for markdown that will be converted to HTML where the link
+ * and separator are distracting on the page.
+ *
+ * Pattern it matches:
+ * ```
+ *
+ *
+ * [See Demo](./demos/base/)
+ *
+ * --- (optional)
+ * ```
+ *
+ * Gets transformed to:
+ * ```
+ *
+ * ```
+ */
+export const transformMarkdownDemoLinks: Plugin = () => {
+ return (tree) => {
+ const parent = tree as Parent;
+ const children = parent.children;
+
+ for (let i = 0; i < children.length - 1; i += 1) {
+ const current = children[i];
+ const next = children[i + 1];
+ const separator = children[i + 2]; // May not exist
+
+ let hasDemo = false;
+
+ // Check if current node is an HTML element containing a Demo component without .Title
+ if (current?.type === 'html') {
+ const htmlNode = current as Html;
+ hasDemo = htmlNode.value.includes('= 2 &&
+ paragraphNode.children[0].type === 'html' &&
+ paragraphNode.children[paragraphNode.children.length - 1].type === 'html'
+ ) {
+ // Check if this looks like a Demo component with opening and closing tags
+ const openingTag = paragraphNode.children[0] as Html;
+ const closingTag = paragraphNode.children[paragraphNode.children.length - 1] as Html;
+
+ if (
+ openingTag.value.includes(' {
+ return (
+ child.type === 'html' &&
+ child.value.includes(' {
+ return (
+ child.type === 'link' &&
+ child.children.some(
+ (linkChild) => linkChild.type === 'text' && linkChild.value === 'See Demo',
+ )
+ );
+ });
+
+ // Check if there's also a thematic break (---) after the paragraph
+ const hasThematicBreak = separator?.type === 'thematicBreak';
+
+ if (hasSeeDemo) {
+ // Remove the "See Demo" paragraph and any following thematic break
+ if (hasThematicBreak) {
+ // Remove both the "See Demo" paragraph and the thematic break
+ children.splice(i + 1, 2);
+ removedSomething = true;
+ } else {
+ // Remove only the "See Demo" paragraph
+ children.splice(i + 1, 1);
+ removedSomething = true;
+ }
+ } else if (hasThematicBreak) {
+ // No "See Demo" link, but there's a thematic break after the paragraph - remove just the HR
+ children.splice(i + 2, 1);
+ removedSomething = true;
+ }
+ }
+
+ // If we removed something, adjust the loop index to prevent skipping
+ if (removedSomething) {
+ i -= 1;
+ }
+ }
+ };
+};
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownRelativePaths/index.ts b/packages/docs-infra/src/pipeline/transformMarkdownRelativePaths/index.ts
new file mode 100644
index 000000000..45a9967c2
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/transformMarkdownRelativePaths/index.ts
@@ -0,0 +1,5 @@
+// This is the export format expected by a remark plugin.
+
+import { transformMarkdownRelativePaths } from './transformMarkdownRelativePaths';
+
+export default transformMarkdownRelativePaths;
diff --git a/packages/docs-infra/src/pipeline/transformMarkdownRelativePaths/transformMarkdownRelativePaths.ts b/packages/docs-infra/src/pipeline/transformMarkdownRelativePaths/transformMarkdownRelativePaths.ts
new file mode 100644
index 000000000..0ff97452a
--- /dev/null
+++ b/packages/docs-infra/src/pipeline/transformMarkdownRelativePaths/transformMarkdownRelativePaths.ts
@@ -0,0 +1,41 @@
+// webpack does not like node: imports
+// eslint-disable-next-line n/prefer-node-protocol
+import path from 'path';
+
+import { visit } from 'unist-util-visit';
+import type { Plugin } from 'unified';
+import type { Link } from 'mdast';
+
+/**
+ * Remark plugin that strips page file extensions from URLs.
+ * Removes /page.tsx, /page.jsx, /page.js, /page.mdx, /page.md from both absolute and relative URLs.
+ * For relative URLs (both ./ and ../), converts them to absolute paths based on the current file's location.
+ *
+ * Examples:
+ * - /components/page.tsx -> /components
+ * - ./code-highlighter/page.mdx -> /components/code-highlighter (when processed from /components/page.mdx)
+ * - ../code-highlighter/page.tsx -> /code-highlighter (when processed from /components/button/page.mdx)
+ * This allows URLs to resolve when reading in VSCode and Github
+ */
+export const transformMarkdownRelativePaths: Plugin = () => {
+ return (tree, file) => {
+ visit(tree, 'link', (node: Link) => {
+ if (node.url) {
+ node.url = node.url.replace(/\/page\.(tsx|jsx|js|mdx|md)$/g, '');
+ node.url = node.url.replace(/\/page\.(tsx|jsx|js|mdx|md)(\?[^#]*)?(#.*)?$/g, '$2$3');
+
+ if ((node.url.startsWith('./') || node.url.startsWith('../')) && file.path) {
+ const currentDir = path.dirname(file.path);
+ const appIndex = currentDir.indexOf('/app/');
+ const baseDir = appIndex !== -1 ? currentDir.substring(appIndex + 4) : '/';
+
+ // Resolve the relative path from the current directory
+ const resolvedPath = path.resolve('/', baseDir, node.url);
+ node.url = resolvedPath;
+ }
+
+ node.url = node.url.replace(/\/$/, '');
+ }
+ });
+ };
+};
diff --git a/packages/docs-infra/src/useCode/Pre.tsx b/packages/docs-infra/src/useCode/Pre.tsx
index 9e3ca00d6..78d0e26ae 100644
--- a/packages/docs-infra/src/useCode/Pre.tsx
+++ b/packages/docs-infra/src/useCode/Pre.tsx
@@ -144,6 +144,10 @@ export function Pre({
const frames = React.useMemo(() => {
return hast?.children.map((child, index) => {
if (child.type !== 'element') {
+ if (child.type === 'text') {
+ return {child.value} ;
+ }
+
return null;
}
@@ -171,7 +175,7 @@ export function Pre({
return (
- {typeof children === 'string' ? children : frames}
+ {typeof children === 'string' ? children : frames}
);
}
diff --git a/packages/docs-infra/src/useErrors/useErrors.ts b/packages/docs-infra/src/useErrors/useErrors.ts
index c79015fb7..2b21bead7 100644
--- a/packages/docs-infra/src/useErrors/useErrors.ts
+++ b/packages/docs-infra/src/useErrors/useErrors.ts
@@ -4,8 +4,12 @@ type Errors = {
errors?: Error[];
};
-export function useErrors(): Errors {
+export function useErrors(props?: Errors): Errors {
const context = useErrorsContext();
- return { errors: context?.errors };
+ // Context errors take precedence over prop errors
+ // This ensures client-side errors override server-side errors
+ const errors = context?.errors || props?.errors;
+
+ return { errors };
}
diff --git a/packages/docs-infra/src/withDocsInfra/withDocsInfra.test.ts b/packages/docs-infra/src/withDocsInfra/withDocsInfra.test.ts
index 16e1a5d68..0b5dcfd7d 100644
--- a/packages/docs-infra/src/withDocsInfra/withDocsInfra.test.ts
+++ b/packages/docs-infra/src/withDocsInfra/withDocsInfra.test.ts
@@ -673,7 +673,10 @@ describe('getDocsInfraMdxOptions', () => {
expect(result.remarkPlugins).toEqual([
['remark-gfm'],
+ ['@mui/internal-docs-infra/pipeline/transformMarkdownRelativePaths'],
+ ['@mui/internal-docs-infra/pipeline/transformMarkdownBlockquoteCallouts'],
['@mui/internal-docs-infra/pipeline/transformMarkdownCode'],
+ ['@mui/internal-docs-infra/pipeline/transformMarkdownDemoLinks'],
]);
expect(result.rehypePlugins).toEqual([
@@ -689,7 +692,10 @@ describe('getDocsInfraMdxOptions', () => {
expect(result.remarkPlugins).toEqual([
['remark-gfm'],
+ ['@mui/internal-docs-infra/pipeline/transformMarkdownRelativePaths'],
+ ['@mui/internal-docs-infra/pipeline/transformMarkdownBlockquoteCallouts'],
['@mui/internal-docs-infra/pipeline/transformMarkdownCode'],
+ ['@mui/internal-docs-infra/pipeline/transformMarkdownDemoLinks'],
['remark-emoji'],
]);
diff --git a/packages/docs-infra/src/withDocsInfra/withDocsInfra.ts b/packages/docs-infra/src/withDocsInfra/withDocsInfra.ts
index 8eb097993..0eea86bb0 100644
--- a/packages/docs-infra/src/withDocsInfra/withDocsInfra.ts
+++ b/packages/docs-infra/src/withDocsInfra/withDocsInfra.ts
@@ -87,7 +87,10 @@ export function getDocsInfraMdxOptions(
): DocsInfraMdxOptions {
const defaultRemarkPlugins: Array = [
['remark-gfm'],
+ ['@mui/internal-docs-infra/pipeline/transformMarkdownRelativePaths'],
+ ['@mui/internal-docs-infra/pipeline/transformMarkdownBlockquoteCallouts'],
['@mui/internal-docs-infra/pipeline/transformMarkdownCode'],
+ ['@mui/internal-docs-infra/pipeline/transformMarkdownDemoLinks'],
];
const defaultRehypePlugins: Array = [
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 39a7220cd..7b724af9a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -221,6 +221,73 @@ importers:
specifier: ^1.15.5
version: 1.15.5
+ docs:
+ dependencies:
+ '@base-ui-components/react':
+ specifier: 1.0.0-beta.1
+ version: 1.0.0-beta.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@mdx-js/loader':
+ specifier: ^3.1.0
+ version: 3.1.1(webpack@5.102.1(jiti@2.6.1))
+ '@mdx-js/react':
+ specifier: ^3.1.0
+ version: 3.1.1(@types/react@19.2.2)(react@19.2.0)
+ '@mui/internal-docs-infra':
+ specifier: workspace:^
+ version: link:../packages/docs-infra/build
+ '@next/mdx':
+ specifier: ^15.3.4
+ version: 15.5.6(@mdx-js/loader@3.1.1(webpack@5.102.1(jiti@2.6.1)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0))
+ '@types/mdx':
+ specifier: ^2.0.13
+ version: 2.0.13
+ next:
+ specifier: v15.5.2
+ version: 15.5.2(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react:
+ specifier: ^19.0.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.2.0(react@19.2.0)
+ react-runner:
+ specifier: ^1.0.5
+ version: 1.0.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ remark-gfm:
+ specifier: ^4.0.1
+ version: 4.0.1
+ server-only:
+ specifier: ^0.0.1
+ version: 0.0.1
+ use-editable:
+ specifier: ^2.3.3
+ version: 2.3.3(react@19.2.0)
+ vscode-oniguruma:
+ specifier: ^2.0.1
+ version: 2.0.1
+ devDependencies:
+ '@next/bundle-analyzer':
+ specifier: ^15.5.2
+ version: 15.5.6
+ '@types/node':
+ specifier: ^20
+ version: 20.19.24
+ '@types/react':
+ specifier: ^19
+ version: 19.2.2
+ '@types/react-dom':
+ specifier: ^19
+ version: 19.2.2(@types/react@19.2.2)
+ '@wooorm/starry-night':
+ specifier: ^3.8.0
+ version: 3.8.0
+ serve:
+ specifier: ^14.2.5
+ version: 14.2.5
+ typescript:
+ specifier: ^5
+ version: 5.9.3
+
packages/babel-plugin-display-name:
dependencies:
'@babel/helper-module-imports':
@@ -1511,6 +1578,17 @@ packages:
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
+ '@base-ui-components/react@1.0.0-beta.1':
+ resolution: {integrity: sha512-7zmGiz4/+HKnv99lWftItoSMqnj2PdSvt2krh0/GP+Rj0xK0NMnFI/gIVvP7CB2G+k0JPUrRWXjXa3y08oiakg==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@types/react': ^17 || ^18 || ^19
+ react: ^17 || ^18 || ^19
+ react-dom: ^17 || ^18 || ^19
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@base-ui-components/react@1.0.0-beta.4':
resolution: {integrity: sha512-sPYKj26gbFHD2ZsrMYqQshXnMuomBodzPn+d0dDxWieTj232XCQ9QGt9fU9l5SDGC9hi8s24lDlg9FXPSI7T8A==}
engines: {node: '>=14.0.0'}
@@ -1623,6 +1701,10 @@ packages:
resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==}
engines: {node: '>=18'}
+ '@discoveryjs/json-ext@0.5.7':
+ resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
+ engines: {node: '>=10.0.0'}
+
'@dmsnell/diff-match-patch@1.1.0':
resolution: {integrity: sha512-yejLPmM5pjsGvxS9gXablUSbInW7H976c/FJ4iQxWIm7/38xBySRemTPDe34lhg1gVLbJntX0+sH0jYfU+PN9A==}
@@ -2819,6 +2901,23 @@ packages:
'@mdn/browser-compat-data@5.7.6':
resolution: {integrity: sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==}
+ '@mdx-js/loader@3.1.1':
+ resolution: {integrity: sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ==}
+ peerDependencies:
+ webpack: '>=5'
+ peerDependenciesMeta:
+ webpack:
+ optional: true
+
+ '@mdx-js/mdx@3.1.1':
+ resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
+
+ '@mdx-js/react@3.1.1':
+ resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
+ peerDependencies:
+ '@types/react': '>=16'
+ react: '>=16'
+
'@mui/base@5.0.0-beta.69':
resolution: {integrity: sha512-r2YyGUXpZxj8rLAlbjp1x2BnMERTZ/dMqd9cClKj2OJ7ALAuiv/9X5E9eHfRc9o/dGRuLSMq/WTjREktJVjxVA==}
engines: {node: '>=14.0.0'}
@@ -3669,54 +3768,119 @@ packages:
engines: {node: '>=18.14.0'}
hasBin: true
+ '@next/bundle-analyzer@15.5.6':
+ resolution: {integrity: sha512-IHeyk2s9/fVDAGDLNbBkCSG8XBabhuMajiaJggjsg4GyFIswh78DzLo5Nl5th8QTs3U/teYeczvfeV9w1Tx3qA==}
+
+ '@next/env@15.5.2':
+ resolution: {integrity: sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==}
+
'@next/env@15.5.6':
resolution: {integrity: sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==}
'@next/eslint-plugin-next@15.5.6':
resolution: {integrity: sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==}
+ '@next/mdx@15.5.6':
+ resolution: {integrity: sha512-lyzXcnZWPjYxbkz/5tv1bRlCOjKYX1lFg3LIuoIf9ERTOUBDzkCvUnWjtRsmFRxKv1/6uwpLVQvrJDd54gVDBw==}
+ peerDependencies:
+ '@mdx-js/loader': '>=0.15.0'
+ '@mdx-js/react': '>=0.15.0'
+ peerDependenciesMeta:
+ '@mdx-js/loader':
+ optional: true
+ '@mdx-js/react':
+ optional: true
+
+ '@next/swc-darwin-arm64@15.5.2':
+ resolution: {integrity: sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
'@next/swc-darwin-arm64@15.5.6':
resolution: {integrity: sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
+ '@next/swc-darwin-x64@15.5.2':
+ resolution: {integrity: sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
'@next/swc-darwin-x64@15.5.6':
resolution: {integrity: sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
+ '@next/swc-linux-arm64-gnu@15.5.2':
+ resolution: {integrity: sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
'@next/swc-linux-arm64-gnu@15.5.6':
resolution: {integrity: sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ '@next/swc-linux-arm64-musl@15.5.2':
+ resolution: {integrity: sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
'@next/swc-linux-arm64-musl@15.5.6':
resolution: {integrity: sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ '@next/swc-linux-x64-gnu@15.5.2':
+ resolution: {integrity: sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
'@next/swc-linux-x64-gnu@15.5.6':
resolution: {integrity: sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ '@next/swc-linux-x64-musl@15.5.2':
+ resolution: {integrity: sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
'@next/swc-linux-x64-musl@15.5.6':
resolution: {integrity: sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ '@next/swc-win32-arm64-msvc@15.5.2':
+ resolution: {integrity: sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
'@next/swc-win32-arm64-msvc@15.5.6':
resolution: {integrity: sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
+ '@next/swc-win32-x64-msvc@15.5.2':
+ resolution: {integrity: sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
'@next/swc-win32-x64-msvc@15.5.6':
resolution: {integrity: sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==}
engines: {node: '>= 10'}
@@ -4238,6 +4402,9 @@ packages:
resolution: {integrity: sha512-bWLDlHsBlgKY/05wDN/V3ETcn5G2SV/SiA2ZmNvKGGlmVX4G5li7GRDhHcgYvHJHyJ8TUStqg2xtHmCs0UbAbg==}
engines: {node: '>=18'}
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
@@ -5695,6 +5862,9 @@ packages:
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+ '@types/mdx@2.0.13':
+ resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
+
'@types/micromatch@4.0.10':
resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==}
@@ -6324,6 +6494,9 @@ packages:
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
engines: {node: '>=14'}
+ any-promise@1.3.0:
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -6465,6 +6638,10 @@ packages:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'}
+ astring@1.9.0:
+ resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
+ hasBin: true
+
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
@@ -6957,6 +7134,9 @@ packages:
resolution: {integrity: sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==}
engines: {node: ^18.17.0 || >=20.5.0}
+ collapse-white-space@2.1.0:
+ resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -7017,10 +7197,18 @@ packages:
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+ commander@4.1.1:
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
+ engines: {node: '>= 6'}
+
commander@6.2.1:
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
engines: {node: '>= 6'}
+ commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
@@ -7378,6 +7566,9 @@ packages:
resolution: {integrity: sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==}
engines: {node: '>=12'}
+ debounce@1.2.1:
+ resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
+
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@@ -7804,6 +7995,12 @@ packages:
resolution: {integrity: sha512-aiQ/QyJBVJbabtsSediM1S4qI+P3p8F5J5YR5o/bH003BCnnclzxK9pi5Qd2Hg01ktAtZCaQBdejHrkOBGwf5Q==}
engines: {node: '>=0.4.0'}
+ esast-util-from-estree@2.0.0:
+ resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
+
+ esast-util-from-js@2.0.1:
+ resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
+
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
@@ -8024,9 +8221,24 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
+ estree-util-attach-comments@3.0.0:
+ resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
+
+ estree-util-build-jsx@3.0.1:
+ resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==}
+
estree-util-is-identifier-name@3.0.0:
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
+ estree-util-scope@1.0.0:
+ resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==}
+
+ estree-util-to-js@2.0.0:
+ resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==}
+
+ estree-util-visit@2.0.0:
+ resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
+
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -8738,6 +8950,9 @@ packages:
hast-util-parse-selector@4.0.0:
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+ hast-util-to-estree@3.1.3:
+ resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==}
+
hast-util-to-html@9.0.5:
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
@@ -9845,6 +10060,13 @@ packages:
resolution: {integrity: sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ markdown-extensions@2.0.0:
+ resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
+ engines: {node: '>=16'}
+
+ markdown-table@3.0.4:
+ resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
+
markdown-to-jsx@7.7.4:
resolution: {integrity: sha512-1bSfXyBKi+EYS3YY+e0Csuxf8oZ3decdfhOav/Z7Wrk89tjudyL5FOmwZQUoy0/qVXGUl+6Q3s2SWtpDEWITfQ==}
engines: {node: '>= 10'}
@@ -9864,15 +10086,39 @@ packages:
maxstache@1.0.7:
resolution: {integrity: sha512-53ZBxHrZM+W//5AcRVewiLpDunHnucfdzZUGz54Fnvo4tE+J3p8EL66kBrs2UhBXvYKTWckWYYWBqJqoTcenqg==}
+ mdast-util-find-and-replace@3.0.2:
+ resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
+
mdast-util-from-markdown@2.0.2:
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
+ mdast-util-gfm-autolink-literal@2.0.1:
+ resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
+
+ mdast-util-gfm-footnote@2.1.0:
+ resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
+
+ mdast-util-gfm-strikethrough@2.0.0:
+ resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==}
+
+ mdast-util-gfm-table@2.0.0:
+ resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
+
+ mdast-util-gfm-task-list-item@2.0.0:
+ resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
+
+ mdast-util-gfm@3.1.0:
+ resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
+
mdast-util-mdx-expression@2.0.1:
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
mdast-util-mdx-jsx@3.2.0:
resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
+ mdast-util-mdx@3.0.0:
+ resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==}
+
mdast-util-mdxjs-esm@2.0.1:
resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
@@ -9937,12 +10183,51 @@ packages:
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
+ micromark-extension-gfm-autolink-literal@2.1.0:
+ resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
+
+ micromark-extension-gfm-footnote@2.1.0:
+ resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
+
+ micromark-extension-gfm-strikethrough@2.1.0:
+ resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==}
+
+ micromark-extension-gfm-table@2.1.1:
+ resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==}
+
+ micromark-extension-gfm-tagfilter@2.0.0:
+ resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==}
+
+ micromark-extension-gfm-task-list-item@2.1.0:
+ resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==}
+
+ micromark-extension-gfm@3.0.0:
+ resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
+
+ micromark-extension-mdx-expression@3.0.1:
+ resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==}
+
+ micromark-extension-mdx-jsx@3.0.2:
+ resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==}
+
+ micromark-extension-mdx-md@2.0.0:
+ resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==}
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==}
+
+ micromark-extension-mdxjs@3.0.0:
+ resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==}
+
micromark-factory-destination@2.0.1:
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
micromark-factory-label@2.0.1:
resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+ micromark-factory-mdx-expression@2.0.3:
+ resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==}
+
micromark-factory-space@2.0.1:
resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
@@ -9973,6 +10258,9 @@ packages:
micromark-util-encode@2.0.1:
resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+ micromark-util-events-to-acorn@2.0.3:
+ resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==}
+
micromark-util-html-tag-name@2.0.1:
resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
@@ -10182,6 +10470,10 @@ packages:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@@ -10218,6 +10510,9 @@ packages:
resolution: {integrity: sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==}
engines: {node: '>= 0.6'}
+ mz@2.7.0:
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+
named-placeholders@1.1.3:
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
engines: {node: '>=12.0.0'}
@@ -10272,6 +10567,27 @@ packages:
netlify-redirector@0.5.0:
resolution: {integrity: sha512-4zdzIP+6muqPCuE8avnrgDJ6KW/2+UpHTRcTbMXCIRxiRmyrX+IZ4WSJGZdHPWF3WmQpXpy603XxecZ9iygN7w==}
+ next@15.5.2:
+ resolution: {integrity: sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==}
+ engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ '@playwright/test': ^1.51.1
+ babel-plugin-react-compiler: '*'
+ react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ '@playwright/test':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+ sass:
+ optional: true
+
next@15.5.6:
resolution: {integrity: sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
@@ -10555,6 +10871,10 @@ packages:
openapi-typescript-helpers@0.0.15:
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
+ opener@1.5.2:
+ resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
+ hasBin: true
+
oppa@0.4.0:
resolution: {integrity: sha512-DFvM3+F+rB/igo3FRnkDWitjZgBH9qZAn68IacYHsqbZBKwuTA+LdD4zSJiQtgQpWq7M08we5FlGAVHz0yW7PQ==}
engines: {node: '>=10'}
@@ -10932,6 +11252,10 @@ packages:
resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==}
hasBin: true
+ pirates@4.0.7:
+ resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
+ engines: {node: '>= 6'}
+
piscina@4.8.0:
resolution: {integrity: sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==}
@@ -11329,6 +11653,12 @@ packages:
react-dom:
optional: true
+ react-runner@1.0.5:
+ resolution: {integrity: sha512-eCIybRpssp6ffjqXqId024esO9UP2lV838Lvm3fC7VgMQ/dQHhR0jJwOY2IPrYD3AaM/bcvMikmASIRZqNUHsw==}
+ peerDependencies:
+ react: ^16.0.0 || ^17 || ^18
+ react-dom: ^16.0.0 || ^17 || ^18
+
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
@@ -11425,6 +11755,20 @@ packages:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
+ recma-build-jsx@1.0.0:
+ resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
+
+ recma-jsx@1.0.1:
+ resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ recma-parse@1.0.0:
+ resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==}
+
+ recma-stringify@1.0.0:
+ resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==}
+
recursive-readdir@2.2.3:
resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==}
engines: {node: '>=6.0.0'}
@@ -11481,6 +11825,9 @@ packages:
rehype-parse@9.0.1:
resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
+ rehype-recma@1.0.0:
+ resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
+
rehype-stringify@10.0.1:
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
@@ -11488,12 +11835,21 @@ packages:
resolution: {integrity: sha512-dLN0+SBPutC6bVFCH2+1o2VrHrvAj/PX6MzTemeaEKlCL10JKPMRlqszkitLQnHVgm90QQ94wxoBJRgfIEkstg==}
engines: {node: ^20.18.0 || ^22.12.0 || >=23.3.0}
+ remark-gfm@4.0.1:
+ resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
+
+ remark-mdx@3.1.1:
+ resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==}
+
remark-parse@11.0.0:
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
remark-rehype@11.1.2:
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
+ remark-stringify@11.0.0:
+ resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
+
remove-trailing-separator@1.1.0:
resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==}
@@ -11741,6 +12097,9 @@ packages:
engines: {node: '>= 14'}
hasBin: true
+ server-only@0.0.1:
+ resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
+
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -11819,6 +12178,10 @@ packages:
resolution: {integrity: sha512-Gw/FgHtrLM9WP8P5lLcSGh9OQcrTruWCELAiS48ik1QbL0cH+dfjomiRTUE9zzz+D1N6rOLkwXUvVmXZAsNE0Q==}
engines: {node: ^20.17.0 || >=22.9.0}
+ sirv@2.0.4:
+ resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
+ engines: {node: '>= 10'}
+
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@@ -12145,6 +12508,11 @@ packages:
stylis@4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
+ sucrase@3.35.0:
+ resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+
superjson@2.0.0:
resolution: {integrity: sha512-W3n+NJ7TFjaLle8ihIIvsr/bbuKpnxeatsyjmhy7iSkom+/cshaHziCQAWXrHGWJVQSQFDOuES6C3nSEvcbrQg==}
engines: {node: '>=16'}
@@ -12256,6 +12624,13 @@ packages:
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
+ thenify-all@1.6.0:
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+ engines: {node: '>=0.8'}
+
+ thenify@3.3.1:
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
@@ -12326,6 +12701,10 @@ packages:
tomlify-j0.4@3.0.0:
resolution: {integrity: sha512-2Ulkc8T7mXJ2l0W476YC/A209PR38Nw8PuaCNtk9uI3t1zzFdGQeWYGQvmj2PZkVvRC/Yoi4xQKMRnWc/N29tQ==}
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
tough-cookie@6.0.0:
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
engines: {node: '>=16'}
@@ -12381,6 +12760,9 @@ packages:
peerDependencies:
typescript: '>=4.0.0'
+ ts-interface-checker@0.1.13:
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+
ts-node@10.9.2:
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true
@@ -12593,6 +12975,9 @@ packages:
unist-util-is@6.0.1:
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+ unist-util-position-from-estree@2.0.0:
+ resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==}
+
unist-util-position@5.0.0:
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
@@ -12749,6 +13134,11 @@ packages:
'@types/react':
optional: true
+ use-editable@2.3.3:
+ resolution: {integrity: sha512-7wVD2JbfAFJ3DK0vITvXBdpd9JAz5BcKAAolsnLBuBn6UDDwBGuCIAGvR3yA2BNKm578vAMVHFCWaOcA+BhhiA==}
+ peerDependencies:
+ react: '>= 16.8.0'
+
use-sidecar@1.1.3:
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
engines: {node: '>=10'}
@@ -12970,6 +13360,11 @@ packages:
resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==}
engines: {node: '>=20'}
+ webpack-bundle-analyzer@4.10.1:
+ resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==}
+ engines: {node: '>= 10.13.0'}
+ hasBin: true
+
webpack-sources@3.3.3:
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
engines: {node: '>=10.13.0'}
@@ -13109,6 +13504,18 @@ packages:
resolution: {integrity: sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA==}
engines: {node: '>=8'}
+ ws@7.5.10:
+ resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
+ engines: {node: '>=8.3.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: ^5.0.2
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
ws@8.18.1:
resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==}
engines: {node: '>=10.0.0'}
@@ -14694,6 +15101,19 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
+ '@base-ui-components/react@1.0.0-beta.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@babel/runtime': 7.28.4
+ '@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@floating-ui/utils': 0.2.10
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ reselect: 5.1.1
+ tabbable: 6.3.0
+ use-sync-external-store: 1.6.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.2
+
'@base-ui-components/react@1.0.0-beta.4(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@babel/runtime': 7.28.4
@@ -14814,6 +15234,8 @@ snapshots:
gonzales-pe: 4.3.0
node-source-walk: 7.0.1
+ '@discoveryjs/json-ext@0.5.7': {}
+
'@dmsnell/diff-match-patch@1.1.0': {}
'@dual-bundle/import-meta-resolve@4.2.1': {}
@@ -15819,6 +16241,51 @@ snapshots:
'@mdn/browser-compat-data@5.7.6': {}
+ '@mdx-js/loader@3.1.1(webpack@5.102.1(jiti@2.6.1))':
+ dependencies:
+ '@mdx-js/mdx': 3.1.1
+ source-map: 0.7.6
+ optionalDependencies:
+ webpack: 5.102.1(jiti@2.6.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@mdx-js/mdx@3.1.1':
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdx': 2.0.13
+ acorn: 8.15.0
+ collapse-white-space: 2.1.0
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-util-scope: 1.0.0
+ estree-walker: 3.0.3
+ hast-util-to-jsx-runtime: 2.3.6
+ markdown-extensions: 2.0.0
+ recma-build-jsx: 1.0.0
+ recma-jsx: 1.0.1(acorn@8.15.0)
+ recma-stringify: 1.0.0
+ rehype-recma: 1.0.0
+ remark-mdx: 3.1.1
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ source-map: 0.7.6
+ unified: 11.0.5
+ unist-util-position-from-estree: 2.0.0
+ unist-util-stringify-position: 4.0.0
+ unist-util-visit: 5.0.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0)':
+ dependencies:
+ '@types/mdx': 2.0.13
+ '@types/react': 19.2.2
+ react: 19.2.0
+
'@mui/base@5.0.0-beta.69(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@babel/runtime': 7.28.4
@@ -16974,33 +17441,73 @@ snapshots:
- rollup
- supports-color
+ '@next/bundle-analyzer@15.5.6':
+ dependencies:
+ webpack-bundle-analyzer: 4.10.1
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ '@next/env@15.5.2': {}
+
'@next/env@15.5.6': {}
'@next/eslint-plugin-next@15.5.6':
dependencies:
fast-glob: 3.3.1
+ '@next/mdx@15.5.6(@mdx-js/loader@3.1.1(webpack@5.102.1(jiti@2.6.1)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0))':
+ dependencies:
+ source-map: 0.7.6
+ optionalDependencies:
+ '@mdx-js/loader': 3.1.1(webpack@5.102.1(jiti@2.6.1))
+ '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.2.0)
+
+ '@next/swc-darwin-arm64@15.5.2':
+ optional: true
+
'@next/swc-darwin-arm64@15.5.6':
optional: true
+ '@next/swc-darwin-x64@15.5.2':
+ optional: true
+
'@next/swc-darwin-x64@15.5.6':
optional: true
+ '@next/swc-linux-arm64-gnu@15.5.2':
+ optional: true
+
'@next/swc-linux-arm64-gnu@15.5.6':
optional: true
- '@next/swc-linux-arm64-musl@15.5.6':
+ '@next/swc-linux-arm64-musl@15.5.2':
optional: true
- '@next/swc-linux-x64-gnu@15.5.6':
+ '@next/swc-linux-arm64-musl@15.5.6':
optional: true
- '@next/swc-linux-x64-musl@15.5.6':
+ '@next/swc-linux-x64-gnu@15.5.2':
+ optional: true
+
+ '@next/swc-linux-x64-gnu@15.5.6':
+ optional: true
+
+ '@next/swc-linux-x64-musl@15.5.2':
+ optional: true
+
+ '@next/swc-linux-x64-musl@15.5.6':
+ optional: true
+
+ '@next/swc-win32-arm64-msvc@15.5.2':
optional: true
'@next/swc-win32-arm64-msvc@15.5.6':
optional: true
+ '@next/swc-win32-x64-msvc@15.5.2':
+ optional: true
+
'@next/swc-win32-x64-msvc@15.5.6':
optional: true
@@ -17587,6 +18094,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@polka/url@1.0.0-next.29': {}
+
'@popperjs/core@2.11.8': {}
'@protobufjs/aspromise@1.1.2': {}
@@ -19404,6 +19913,8 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ '@types/mdx@2.0.13': {}
+
'@types/micromatch@4.0.10':
dependencies:
'@types/braces': 3.0.5
@@ -20230,6 +20741,8 @@ snapshots:
ansis@4.2.0: {}
+ any-promise@1.3.0: {}
+
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@@ -20434,6 +20947,8 @@ snapshots:
astral-regex@2.0.0: {}
+ astring@1.9.0: {}
+
async-function@1.0.0: {}
async-sema@3.1.1: {}
@@ -20950,6 +21465,8 @@ snapshots:
cmd-shim@7.0.0: {}
+ collapse-white-space@2.1.0: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -20996,8 +21513,12 @@ snapshots:
commander@2.20.3: {}
+ commander@4.1.1: {}
+
commander@6.2.1: {}
+ commander@7.2.0: {}
+
commander@9.5.0: {}
comment-json@4.3.0:
@@ -21404,6 +21925,8 @@ snapshots:
dependencies:
mimic-fn: 4.0.0
+ debounce@1.2.1: {}
+
debug@2.6.9:
dependencies:
ms: 2.0.0
@@ -21877,6 +22400,20 @@ snapshots:
string.prototype.trimleft: 2.1.3
string.prototype.trimright: 2.1.3
+ esast-util-from-estree@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ unist-util-position-from-estree: 2.0.0
+
+ esast-util-from-js@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ acorn: 8.15.0
+ esast-util-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
esbuild@0.21.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
@@ -22276,8 +22813,35 @@ snapshots:
estraverse@5.3.0: {}
+ estree-util-attach-comments@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ estree-util-build-jsx@3.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-walker: 3.0.3
+
estree-util-is-identifier-name@3.0.0: {}
+ estree-util-scope@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+
+ estree-util-to-js@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ astring: 1.9.0
+ source-map: 0.7.6
+
+ estree-util-visit@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/unist': 3.0.3
+
estree-walker@2.0.2: {}
estree-walker@3.0.3:
@@ -23188,6 +23752,27 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
+ hast-util-to-estree@3.1.3:
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-attach-comments: 3.0.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.19
+ unist-util-position: 5.0.0
+ zwitch: 2.0.4
+ transitivePeerDependencies:
+ - supports-color
+
hast-util-to-html@9.0.5:
dependencies:
'@types/hast': 3.0.4
@@ -24473,6 +25058,10 @@ snapshots:
map-obj@5.0.2: {}
+ markdown-extensions@2.0.0: {}
+
+ markdown-table@3.0.4: {}
+
markdown-to-jsx@7.7.4(react@19.2.0):
dependencies:
react: 19.2.0
@@ -24490,6 +25079,13 @@ snapshots:
maxstache@1.0.7: {}
+ mdast-util-find-and-replace@3.0.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ escape-string-regexp: 5.0.0
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
mdast-util-from-markdown@2.0.2:
dependencies:
'@types/mdast': 4.0.4
@@ -24507,6 +25103,63 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ mdast-util-gfm-autolink-literal@2.0.1:
+ dependencies:
+ '@types/mdast': 4.0.4
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-find-and-replace: 3.0.2
+ micromark-util-character: 2.1.1
+
+ mdast-util-gfm-footnote@2.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-to-markdown: 2.1.2
+ micromark-util-normalize-identifier: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-strikethrough@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-table@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ markdown-table: 3.0.4
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-task-list-item@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm@3.1.0:
+ dependencies:
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-gfm-autolink-literal: 2.0.1
+ mdast-util-gfm-footnote: 2.1.0
+ mdast-util-gfm-strikethrough: 2.0.0
+ mdast-util-gfm-table: 2.0.0
+ mdast-util-gfm-task-list-item: 2.0.0
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
mdast-util-mdx-expression@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -24535,6 +25188,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ mdast-util-mdx@3.0.0:
+ dependencies:
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
mdast-util-mdxjs-esm@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -24640,6 +25303,115 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
+ micromark-extension-gfm-autolink-literal@2.1.0:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-footnote@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-strikethrough@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-table@2.1.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-tagfilter@2.0.0:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-task-list-item@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm@3.0.0:
+ dependencies:
+ micromark-extension-gfm-autolink-literal: 2.1.0
+ micromark-extension-gfm-footnote: 2.1.0
+ micromark-extension-gfm-strikethrough: 2.1.0
+ micromark-extension-gfm-table: 2.1.1
+ micromark-extension-gfm-tagfilter: 2.0.0
+ micromark-extension-gfm-task-list-item: 2.1.0
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdx-expression@3.0.1:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdx-jsx@3.0.2:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.3
+
+ micromark-extension-mdx-md@2.0.0:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
+ micromark-extension-mdxjs@3.0.0:
+ dependencies:
+ acorn: 8.15.0
+ acorn-jsx: 5.3.2(acorn@8.15.0)
+ micromark-extension-mdx-expression: 3.0.1
+ micromark-extension-mdx-jsx: 3.0.2
+ micromark-extension-mdx-md: 2.0.0
+ micromark-extension-mdxjs-esm: 3.0.0
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-types: 2.0.2
+
micromark-factory-destination@2.0.1:
dependencies:
micromark-util-character: 2.1.1
@@ -24653,6 +25425,18 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
+ micromark-factory-mdx-expression@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
micromark-factory-space@2.0.1:
dependencies:
micromark-util-character: 2.1.1
@@ -24705,6 +25489,16 @@ snapshots:
micromark-util-encode@2.0.1: {}
+ micromark-util-events-to-acorn@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.3
+
micromark-util-html-tag-name@2.0.1: {}
micromark-util-normalize-identifier@2.0.1:
@@ -24911,6 +25705,8 @@ snapshots:
mri@1.2.0: {}
+ mrmime@2.0.1: {}
+
ms@2.0.0: {}
ms@2.1.3: {}
@@ -24969,6 +25765,12 @@ snapshots:
safe-buffer: 5.1.2
sqlstring: 2.3.1
+ mz@2.7.0:
+ dependencies:
+ any-promise: 1.3.0
+ object-assign: 4.1.1
+ thenify-all: 1.6.0
+
named-placeholders@1.1.3:
dependencies:
lru-cache: 7.18.3
@@ -25131,6 +25933,30 @@ snapshots:
netlify-redirector@0.5.0: {}
+ next@15.5.2(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@next/env': 15.5.2
+ '@swc/helpers': 0.5.15
+ caniuse-lite: 1.0.30001753
+ postcss: 8.4.31
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ styled-jsx: 5.1.6(@babel/core@7.28.5)(babel-plugin-macros@3.1.0)(react@19.2.0)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 15.5.2
+ '@next/swc-darwin-x64': 15.5.2
+ '@next/swc-linux-arm64-gnu': 15.5.2
+ '@next/swc-linux-arm64-musl': 15.5.2
+ '@next/swc-linux-x64-gnu': 15.5.2
+ '@next/swc-linux-x64-musl': 15.5.2
+ '@next/swc-win32-arm64-msvc': 15.5.2
+ '@next/swc-win32-x64-msvc': 15.5.2
+ '@opentelemetry/api': 1.9.0
+ sharp: 0.34.4
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+
next@15.5.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
'@next/env': 15.5.6
@@ -25508,6 +26334,8 @@ snapshots:
openapi-typescript-helpers@0.0.15: {}
+ opener@1.5.2: {}
+
oppa@0.4.0:
dependencies:
chalk: 4.1.2
@@ -25894,6 +26722,8 @@ snapshots:
sonic-boom: 4.2.0
thread-stream: 3.1.0
+ pirates@4.0.7: {}
+
piscina@4.8.0:
optionalDependencies:
'@napi-rs/nice': 1.1.1
@@ -26376,6 +27206,12 @@ snapshots:
optionalDependencies:
react-dom: 19.2.0(react@19.2.0)
+ react-runner@1.0.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ sucrase: 3.35.0
+
react-style-singleton@2.2.3(@types/react@19.2.2)(react@19.2.0):
dependencies:
get-nonce: 1.0.1
@@ -26503,6 +27339,35 @@ snapshots:
real-require@0.2.0: {}
+ recma-build-jsx@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ estree-util-build-jsx: 3.0.1
+ vfile: 6.0.3
+
+ recma-jsx@1.0.1(acorn@8.15.0):
+ dependencies:
+ acorn: 8.15.0
+ acorn-jsx: 5.3.2(acorn@8.15.0)
+ estree-util-to-js: 2.0.0
+ recma-parse: 1.0.0
+ recma-stringify: 1.0.0
+ unified: 11.0.5
+
+ recma-parse@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ esast-util-from-js: 2.0.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ recma-stringify@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ estree-util-to-js: 2.0.0
+ unified: 11.0.5
+ vfile: 6.0.3
+
recursive-readdir@2.2.3:
dependencies:
minimatch: 3.1.2
@@ -26585,6 +27450,14 @@ snapshots:
hast-util-from-html: 2.0.3
unified: 11.0.5
+ rehype-recma@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/hast': 3.0.4
+ hast-util-to-estree: 3.1.3
+ transitivePeerDependencies:
+ - supports-color
+
rehype-stringify@10.0.1:
dependencies:
'@types/hast': 3.0.4
@@ -26598,6 +27471,24 @@ snapshots:
core-js: 3.46.0
exit-hook: 4.0.0
+ remark-gfm@4.0.1:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-gfm: 3.1.0
+ micromark-extension-gfm: 3.0.0
+ remark-parse: 11.0.0
+ remark-stringify: 11.0.0
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-mdx@3.1.1:
+ dependencies:
+ mdast-util-mdx: 3.0.0
+ micromark-extension-mdxjs: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+
remark-parse@11.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -26615,6 +27506,12 @@ snapshots:
unified: 11.0.5
vfile: 6.0.3
+ remark-stringify@11.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-to-markdown: 2.1.2
+ unified: 11.0.5
+
remove-trailing-separator@1.1.0: {}
require-directory@2.1.1: {}
@@ -26886,6 +27783,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ server-only@0.0.1: {}
+
set-blocking@2.0.0: {}
set-cookie-parser@2.7.2: {}
@@ -27013,6 +27912,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ sirv@2.0.4:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
sisteransi@1.0.5: {}
slash@2.0.0: {}
@@ -27397,6 +28302,16 @@ snapshots:
stylis@4.2.0: {}
+ sucrase@3.35.0:
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ commander: 4.1.1
+ glob: 10.4.5
+ lines-and-columns: 1.2.4
+ mz: 2.7.0
+ pirates: 4.0.7
+ ts-interface-checker: 0.1.13
+
superjson@2.0.0:
dependencies:
copy-anything: 3.0.5
@@ -27517,6 +28432,14 @@ snapshots:
text-table@0.2.0: {}
+ thenify-all@1.6.0:
+ dependencies:
+ thenify: 3.3.1
+
+ thenify@3.3.1:
+ dependencies:
+ any-promise: 1.3.0
+
thread-stream@3.1.0:
dependencies:
real-require: 0.2.0
@@ -27584,6 +28507,8 @@ snapshots:
tomlify-j0.4@3.0.0: {}
+ totalist@3.0.1: {}
+
tough-cookie@6.0.0:
dependencies:
tldts: 7.0.17
@@ -27625,6 +28550,8 @@ snapshots:
picomatch: 4.0.3
typescript: 5.9.3
+ ts-interface-checker@0.1.13: {}
+
ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
@@ -27843,6 +28770,10 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ unist-util-position-from-estree@2.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
unist-util-position@5.0.0:
dependencies:
'@types/unist': 3.0.3
@@ -27989,6 +28920,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.2
+ use-editable@2.3.3(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+
use-sidecar@1.1.3(@types/react@19.2.2)(react@19.2.0):
dependencies:
detect-node-es: 1.1.0
@@ -28155,6 +29090,25 @@ snapshots:
webidl-conversions@8.0.0: {}
+ webpack-bundle-analyzer@4.10.1:
+ dependencies:
+ '@discoveryjs/json-ext': 0.5.7
+ acorn: 8.15.0
+ acorn-walk: 8.3.4
+ commander: 7.2.0
+ debounce: 1.2.1
+ escape-string-regexp: 4.0.0
+ gzip-size: 6.0.0
+ html-escaper: 2.0.2
+ is-plain-object: 5.0.0
+ opener: 1.5.2
+ picocolors: 1.1.1
+ sirv: 2.0.4
+ ws: 7.5.10
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
webpack-sources@3.3.3: {}
webpack@5.102.1(jiti@2.6.1):
@@ -28369,6 +29323,8 @@ snapshots:
type-fest: 0.4.1
write-json-file: 3.2.0
+ ws@7.5.10: {}
+
ws@8.18.1: {}
ws@8.18.3: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index d7baa4c3b..773bfc338 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -2,6 +2,7 @@ packages:
- 'packages/*'
- 'apps/*'
- 'test/*'
+ - 'docs'
overrides:
# This is a workaround for the issue when the same type refers to @types/eslint for one instance
# and to eslint for another instance, causing a conflict.
diff --git a/tsconfig.json b/tsconfig.json
index a247660fe..a78fbb3e1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,7 +18,8 @@
{ "path": "packages/docs-infra" },
{ "path": "packages/netlify-cache" },
{ "path": "tsconfig.node.json" },
- { "path": "scripts" }
+ { "path": "scripts" },
+ { "path": "docs" }
],
"files": []
}