>();
+
+function isElementInViewport(element: HTMLElement): boolean {
+ const rect = element.getBoundingClientRect();
+ return rect.bottom > 0 && rect.top < window.innerHeight;
+}
+
+/**
+ * Measures the horizontal scrollbar height of a `` element by
+ * temporarily forcing `overflow-x: scroll`.
+ */
+function measureScrollbarHeight(pre: HTMLElement): number {
+ const prevOverflow = pre.style.overflowX;
+ pre.style.overflowX = 'scroll';
+ const scrollbarHeight = pre.offsetHeight - pre.clientHeight;
+ pre.style.overflowX = prevOverflow;
+ return scrollbarHeight;
+}
+
+function clearGutterState(pre: HTMLElement) {
+ const existingTimer = gutterCleanupTimers.get(pre);
+ if (existingTimer !== undefined) {
+ clearTimeout(existingTimer);
+ gutterCleanupTimers.delete(pre);
+ }
+ pre.removeAttribute(GUTTER_STATE_ATTRIBUTE);
+}
+
+/**
+ * Smoothly transitions the horizontal scrollbar gutter on collapse by
+ * swapping the real scrollbar for equivalent padding-bottom, then
+ * animating that padding down to the CSS base value.
+ *
+ * Skips the animation when content doesn't overflow (no scrollbar exists)
+ * or when the browser uses overlay scrollbars (zero height).
+ */
+function animateScrollbarGutter(pre: HTMLElement) {
+ const scrollbarHeight = measureScrollbarHeight(pre);
+ if (scrollbarHeight === 0) {
+ return; // Overlay scrollbars, nothing to do
+ }
+
+ // Only animate if content actually overflows (scrollbar is visible)
+ if (pre.scrollWidth <= pre.clientWidth) {
+ return;
+ }
+
+ clearGutterState(pre);
+ pre.setAttribute(GUTTER_STATE_ATTRIBUTE, 'collapse-from');
+
+ // Move into the transition state on the next macrotask.
+ setTimeout(() => {
+ pre.setAttribute(GUTTER_STATE_ATTRIBUTE, 'collapse-to');
+ }, 0);
+
+ const timeout = getTransitionTimeout('collapse');
+ const cleanupTimer = setTimeout(() => {
+ clearGutterState(pre);
+ }, timeout + 30);
+ gutterCleanupTimers.set(pre, cleanupTimer);
+}
+
+/**
+ * Smoothly transitions the horizontal scrollbar gutter on expand by
+ * reserving the eventual scrollbar space via padding-bottom first,
+ * then letting CSS swap to real overflow-x at the end of the transition.
+ *
+ * This is primarily needed for the max-size split-frame case where hidden
+ * overflow lines can make the scrollbar appear late during expansion.
+ */
+function animateScrollbarGutterExpand(pre: HTMLElement) {
+ const scrollbarHeight = measureScrollbarHeight(pre);
+ if (scrollbarHeight === 0) {
+ return; // Overlay scrollbars, nothing to do
+ }
+
+ // The element uses `min-width: fit-content`, so its scrollWidth
+ // reflects the widest line including hidden frames. If that doesn't
+ // exceed the container, no scrollbar will appear after expansion.
+ const code = pre.querySelector('code');
+ if (code && code.scrollWidth <= pre.clientWidth) {
+ return;
+ }
+
+ clearGutterState(pre);
+ pre.setAttribute(GUTTER_STATE_ATTRIBUTE, 'expand-from');
+
+ // Move into the transition state on the next macrotask.
+ setTimeout(() => {
+ pre.setAttribute(GUTTER_STATE_ATTRIBUTE, 'expand-to');
+ }, 0);
+
+ const timeout = getTransitionTimeout('expand');
+ const cleanupTimer = setTimeout(() => {
+ clearGutterState(pre);
+ }, timeout + 30);
+ gutterCleanupTimers.set(pre, cleanupTimer);
+}
+
+export function useScrollAnchor() {
+ const containerRef = React.useRef(null);
+ const toggleRef = React.useRef(null);
+
+ // CSS `overflow-anchor: none` on hidden frames (set in CSS) nudges native
+ // scroll anchoring toward the visible highlighted/focus content. In Chromium
+ // and Firefox this usually handles most compensation synchronously, while the
+ // rAF loop below smooths any remaining drift so the transition appears stable
+ // and visually "fixed" to the user. In browsers without native overflow-anchor
+ // support (e.g. Safari), the rAF loop is the primary compensation mechanism.
+
+ const anchorScroll = React.useCallback((direction: 'collapse' | 'expand') => {
+ const container = containerRef.current;
+ if (!container) {
+ return;
+ }
+
+ const primaryAnchor = container.querySelector(ANCHOR_SELECTOR);
+ const toggleAnchor = toggleRef.current;
+
+ let anchor = primaryAnchor ?? toggleAnchor;
+ if (direction === 'collapse' && primaryAnchor && !isElementInViewport(primaryAnchor)) {
+ anchor = toggleAnchor ?? primaryAnchor;
+ }
+
+ if (!anchor) {
+ return;
+ }
+
+ // On collapse, animate the scrollbar gutter (padding swap) to avoid
+ // an instant height shrink when horizontal scrollbar space disappears.
+ // On expand, do the inverse only for truncated/max-size demos where
+ // scrollbar space can appear late and look like a snap.
+ const pre = container.querySelector('pre');
+ if (pre) {
+ if (direction === 'collapse') {
+ animateScrollbarGutter(pre);
+ }
+ if (direction === 'expand' && pre.querySelector('[data-collapsible]')) {
+ animateScrollbarGutterExpand(pre);
+ }
+ }
+
+ const initialTop = anchor.getBoundingClientRect().top;
+ let active = true;
+ let cleanupTimer: ReturnType;
+
+ // Use ResizeObserver to compensate only when the container layout
+ // actually changes, rather than polling every animation frame.
+ // Callbacks fire after layout, so getBoundingClientRect() reads
+ // already-computed values without forcing an extra reflow.
+ const observer = new ResizeObserver(() => {
+ if (!active) {
+ return;
+ }
+ const delta = anchor.getBoundingClientRect().top - initialTop;
+ if (Math.abs(delta) > 0.5) {
+ window.scrollBy(0, delta);
+ }
+ });
+
+ // Stop compensating if the user interacts (scroll, click, keyboard),
+ // since UI changes like tab switches can invalidate anchor measurements.
+ function cleanup() {
+ if (!active) {
+ return;
+ }
+ active = false;
+ clearTimeout(cleanupTimer);
+ observer.disconnect();
+ window.removeEventListener('wheel', stopOnUserInteraction);
+ window.removeEventListener('touchmove', stopOnUserInteraction);
+ window.removeEventListener('pointerdown', stopOnUserInteraction);
+ window.removeEventListener('keydown', stopOnUserInteraction);
+ }
+
+ function stopOnUserInteraction() {
+ cleanup();
+ }
+ window.addEventListener('wheel', stopOnUserInteraction, { passive: true, once: true });
+ window.addEventListener('touchmove', stopOnUserInteraction, { passive: true, once: true });
+ window.addEventListener('pointerdown', stopOnUserInteraction, { passive: true, once: true });
+ window.addEventListener('keydown', stopOnUserInteraction, { passive: true, once: true });
+
+ observer.observe(container);
+
+ // Safety cleanup after the CSS transition completes.
+ const timeout = getTransitionTimeout(direction);
+ cleanupTimer = setTimeout(cleanup, timeout + 500);
+ }, []);
+
+ return { containerRef, toggleRef, anchorScroll };
+}
diff --git a/docs/app/docs-infra/pipeline/enhance-code-emphasis/page.mdx b/docs/app/docs-infra/pipeline/enhance-code-emphasis/page.mdx
index 2ea866ee4..aed60e8d4 100644
--- a/docs/app/docs-infra/pipeline/enhance-code-emphasis/page.mdx
+++ b/docs/app/docs-infra/pipeline/enhance-code-emphasis/page.mdx
@@ -121,7 +121,7 @@ export default function Component() {
}
```
-This wraps the specified text in a `` element, allowing you to highlight a specific word or phrase within a line rather than the entire line.
+This wraps the specified text in a `` element, allowing you to highlight a specific word or phrase within a line rather than the entire line.
### Multiple Text Highlighting
@@ -137,7 +137,78 @@ export default function Component() {
}
```
-Each quoted text is independently wrapped in a `` element.
+Each quoted text is independently wrapped in a `` element.
+
+---
+
+## Configuration
+
+### Adding to Source Enhancers
+
+Configure in your webpack loader or server-side loading:
+
+```ts
+import { enhanceCodeEmphasis } from '@mui/internal-docs-infra/pipeline/enhanceCodeEmphasis';
+import { createLoadServerSource } from '@mui/internal-docs-infra/pipeline/loadServerSource';
+import { loadCodeVariant } from '@mui/internal-docs-infra/pipeline/loadCodeVariant';
+
+const sourceEnhancers = [enhanceCodeEmphasis];
+```
+
+For configurable padding, use the factory:
+
+```ts
+import { createEnhanceCodeEmphasis } from '@mui/internal-docs-infra/pipeline/enhanceCodeEmphasis';
+
+const sourceEnhancers = [
+ createEnhanceCodeEmphasis({
+ paddingFrameMaxSize: 3,
+ focusFramesMaxSize: 10,
+ }),
+];
+```
+
+Then pass to your loading pipeline:
+
+```ts
+// Create a loadSource that extracts emphasis and focus comments
+const loadSource = createLoadServerSource({
+ notableCommentsPrefix: ['@highlight', '@focus'],
+ removeCommentsWithPrefix: ['@highlight', '@focus'],
+});
+
+// Use with loadCodeVariant
+const { code } = await loadCodeVariant(url, variantName, variant, {
+ loadSource,
+ sourceEnhancers,
+ sourceParser,
+});
+
+// Or with CodeHighlighter
+
+
+// Or with useCode
+const { selectedFile } = useCode(props, {
+ sourceEnhancers,
+});
+```
+
+### Comment Prefix
+
+The default comment prefixes are `@highlight` and `@focus`, exported as `EMPHASIS_COMMENT_PREFIX` and `FOCUS_COMMENT_PREFIX`:
+
+```ts
+import {
+ EMPHASIS_COMMENT_PREFIX,
+ FOCUS_COMMENT_PREFIX,
+} from '@mui/internal-docs-infra/pipeline/enhanceCodeEmphasis';
+
+console.log(EMPHASIS_COMMENT_PREFIX); // '@highlight'
+console.log(FOCUS_COMMENT_PREFIX); // '@focus'
+```
---
@@ -147,15 +218,26 @@ Each quoted text is independently wrapped in a `` element.
The enhancer recognizes these comment patterns:
-| Pattern | Effect |
-| ----------------------------- | ---------------------------------------- |
-| `@highlight` | Emphasize current line |
-| `@highlight "description"` | Emphasize with description |
-| `@highlight-start` | Start multi-line block |
-| `@highlight-start "desc"` | Start block with description |
-| `@highlight-end` | End multi-line block |
-| `@highlight-text "text"` | Highlight specific text within the line |
-| `@highlight-text "one" "two"` | Highlight multiple texts within the line |
+| Pattern | Effect |
+| ----------------------------- | ----------------------------------------------------- |
+| `@highlight` | Emphasize current line |
+| `@highlight "description"` | Emphasize with description |
+| `@highlight-start` | Start multi-line block |
+| `@highlight-start "desc"` | Start block with description |
+| `@highlight-end` | End multi-line block |
+| `@highlight-text "text"` | Highlight specific text within the line |
+| `@highlight-text "one" "two"` | Highlight multiple texts within the line |
+| `@focus` | Focus current line (no line highlight) |
+| `@focus-start` | Start focused block (no line highlight) |
+| `@focus-end` | End focused block |
+| `@highlight @padding 2` | Highlight line with per-directive padding override |
+| `@highlight-start @padding 2` | Highlight block with per-directive padding override |
+| `@focus @padding 2` | Focus line with per-directive padding override |
+| `@focus-start @padding 2` | Focus block with per-directive padding override |
+| `@highlight @min 6` | Highlight line with per-directive focus max override |
+| `@highlight-start @min 6` | Highlight block with per-directive focus max override |
+| `@focus @min 6` | Focus line with per-directive focus max override |
+| `@focus-start @min 6` | Focus block with per-directive focus max override |
### Description Format
@@ -287,10 +369,11 @@ When padding is configured (see [Configurable Padding](#configurable-padding)),
### Frame Data Attributes
-| Attribute | Type | Description |
-| ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `data-frame-type` | string | `"highlighted"` (focused region), `"highlighted-unfocused"` (non-focused regions), `"padding-top"`, or `"padding-bottom"`. Normal frames have no type attribute. |
-| `data-frame-indent` | number | Only on `highlighted` and `highlighted-unfocused` frames. The shared indent level of the highlighted lines (min leading spaces ÷ 2). |
+| Attribute | Type | Description |
+| ------------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `data-frame-type` | string | `"highlighted"` (focused region with line highlights), `"highlighted-unfocused"` (non-focused regions with line highlights), `"focus"` (focused region without line highlights), `"focus-unfocused"` (non-focused region without line highlights), `"padding-top"`, or `"padding-bottom"`. Normal frames have no type attribute. |
+| `data-frame-indent` | number | Only on `highlighted`, `highlighted-unfocused`, `focus`, and `focus-unfocused` frames. The shared indent level of the highlighted lines (min leading spaces ÷ 2). |
+| `data-frame-description` | string | Present on highlighted frames when the `@highlight` or `@highlight-start` directive includes a description (e.g., `@highlight "description"`). Not set when highlighting is at the line level (inside a focus region or when strong). |
### Configurable Padding
@@ -307,13 +390,59 @@ const sourceEnhancers = [
];
```
-| Option | Type | Default | Description |
-| --------------------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `paddingFrameMaxSize` | number | `0` | Maximum number of context lines above/below the focused region |
-| `focusFramesMaxSize` | number | — | Maximum total lines in the focus area (padding + highlighted). Remaining budget is split floor/ceil (extra line goes to bottom). |
-| `strictHighlightText` | boolean | `false` | When `true`, throws an error if a `@highlight-text` match has to be fragmented across element boundaries (producing `data-hl-part` spans). Highlights that wrap multiple complete elements in a single `data-hl` span are still allowed. |
+| Option | Type | Default | Description |
+| --------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `paddingFrameMaxSize` | number | `0` | Maximum number of context lines above/below the focused region |
+| `focusFramesMaxSize` | number | — | Maximum total lines in the focus area (padding + region). When the region fits, remaining budget is split floor/ceil between padding-top and padding-bottom. When the region exceeds this limit, a focused window is taken from the start of the region, and the remaining overflow lines are marked as unfocused. |
+| `strictHighlightText` | boolean | `false` | When `true`, throws an error if a `@highlight-text` match has to be fragmented across element boundaries (producing `data-hl-part` spans). Highlights that wrap multiple complete elements in a single `data-hl` span are still allowed. |
+
+When `paddingFrameMaxSize` is `0` (the default), no padding frames are created and only region frames (`highlighted`, `focus`, etc.) and normal frames are produced.
+
+### Per-Directive Padding with `@padding N`
+
+Use `@padding N` directly on emphasis directives to override padding for that region:
+
+```tsx displayComments
+const important = loadData(); // @highlight @padding 1
+
+// @highlight-start @focus @padding 4 "Primary preview area"
+
+
+;
+// @highlight-end
+```
+
+In this example, the focused block uses padding `4` while the single highlighted line uses padding `1`.
+
+Padding precedence is:
+
+1. Per-directive `@padding N` on the focused region
+2. `createEnhanceCodeEmphasis({ paddingFrameMaxSize })`
+
+### Per-Directive Focus Max with `@min N`
-When `paddingFrameMaxSize` is `0` (the default), no padding frames are created and only `highlighted` and normal frames are produced.
+Use `@min N` on emphasis directives to override `focusFramesMaxSize` for the focused region:
+
+```tsx displayComments
+// @highlight-start @focus @min 6
+
+// @highlight-end
+```
+
+In this example, the focused region is constrained to a visible window of `6` lines when collapsed, regardless of the global `focusFramesMaxSize` option.
+
+Focus max size precedence is:
+
+1. Per-directive `@min N` on the focused region
+2. `createEnhanceCodeEmphasis({ focusFramesMaxSize })`
### The `@focus` Directive
@@ -330,13 +459,6 @@ const config = getConfig();
// @highlight-end
```
-```css
-.Viewport::after {
- /* @highlight */
- height: min(40px, var(--scroll-area-overflow-y-end, 40px)); /* @highlight-text ", 40px" */
-}
-```
-
In this example, `@focus` directs the padding frames to surround the `ThemeProvider` block instead of the `theme` line.
`@focus` can be combined with any highlight directive:
@@ -345,20 +467,64 @@ In this example, `@focus` directs the padding frames to surround the `ThemeProvi
- `// @highlight-start @focus`
- `// @highlight-start @focus "We render the provider"`
+### Standalone `@focus` (Focus Without Highlight)
+
+Use `@focus`, `@focus-start`, and `@focus-end` to mark a region as focused without adding line-level highlighting (`data-hl`). Lines in a standalone `@focus` region will be part of a focused frame but won't have visual emphasis on individual lines:
+
+```tsx displayComments
+const theme = getTheme(); // @highlight
+const config = getConfig();
+
+// @focus-start
+
+
+;
+// @focus-end
+```
+
+In this example, the `ThemeProvider` block is focused (padding frames surround it), but only the `theme` line has line-level highlighting.
+
+This is particularly useful when an ESLint rule like [`lintJavascriptDemoFocus`](/docs-infra/pipeline/lint-javascript-demo-focus) auto-inserts focus markers — the auto-generated `@focus-start`/`@focus-end` marks the preview area without adding highlight styling, leaving `@highlight` for manual use.
+
### Styling Frame Types
-Target frame types with CSS to create collapsible code blocks:
+Target frame types with CSS to style highlighted frames and create collapsible code blocks:
```css
-/* Normal frames and unfocused highlighted frames are hidden by default (collapsed) */
-.codeBlock .frame:not([data-frame-type]),
+/* Highlighted frames get rounded corners and background */
+.codeBlock .frame[data-frame-type='highlighted'],
.codeBlock .frame[data-frame-type='highlighted-unfocused'] {
+ background: #ebe4ff;
+ border-radius: 8px;
+ margin: 0 6px;
+ padding: 0 6px;
+}
+
+/* Normal frames and unfocused highlighted/focus frames are hidden by default (collapsed) */
+.codeBlock .frame:not([data-frame-type]),
+.codeBlock .frame[data-frame-type='highlighted-unfocused'],
+.codeBlock .frame[data-frame-type='focus-unfocused'] {
max-height: 0;
overflow: hidden;
opacity: 0;
transition:
- max-height 0.3s ease,
- opacity 0.3s ease;
+ max-height 0.3s cubic-bezier(0.5, 0, 0, 1),
+ opacity 0.2s ease 0.1s;
+}
+
+/* When supported, use interpolate-size for smoother height animation */
+@supports (interpolate-size: allow-keywords) {
+ .codeBlock .frame:not([data-frame-type]),
+ .codeBlock .frame[data-frame-type='highlighted-unfocused'],
+ .codeBlock .frame[data-frame-type='focus-unfocused'] {
+ interpolate-size: allow-keywords;
+ max-height: unset;
+ height: 0;
+ overflow: clip;
+ transition:
+ height 0.3s ease,
+ opacity 0.3s ease;
+ }
}
/* Padding frames appear dimmed when collapsed */
@@ -369,11 +535,28 @@ Target frame types with CSS to create collapsible code blocks:
}
/* When expanded, show all frames */
+/* max-height = line height (18.5px) × max frame size (120 lines) = 2220px */
.expanded .codeBlock .frame:not([data-frame-type]),
-.expanded .codeBlock .frame[data-frame-type='highlighted-unfocused'] {
- max-height: 500px;
- overflow: visible;
+.expanded .codeBlock .frame[data-frame-type='highlighted-unfocused'],
+.expanded .codeBlock .frame[data-frame-type='focus-unfocused'] {
+ max-height: 2220px;
opacity: 1;
+ transition:
+ max-height 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
+ opacity 0.15s ease;
+}
+
+@supports (interpolate-size: allow-keywords) {
+ .expanded .codeBlock .frame:not([data-frame-type]),
+ .expanded .codeBlock .frame[data-frame-type='highlighted-unfocused'],
+ .expanded .codeBlock .frame[data-frame-type='focus-unfocused'] {
+ max-height: unset;
+ height: auto;
+ overflow: clip;
+ transition:
+ height 0.3s ease,
+ opacity 0.3s ease;
+ }
}
.expanded .codeBlock .frame[data-frame-type='padding-top'],
@@ -381,203 +564,107 @@ Target frame types with CSS to create collapsible code blocks:
opacity: 1;
}
-/* Use indent level to shift highlighted frames left when collapsed */
-.codeBlock .frame[data-frame-type='highlighted'] {
- transition: margin-left 0.3s ease;
+/* Use indent level to shift highlighted/focus frames left when collapsed */
+.codeBlock .frame[data-frame-type='highlighted'],
+.codeBlock .frame[data-frame-type='focus'] {
+ transition: transform 0.3s ease;
}
-.codeBlock .frame[data-frame-type='highlighted'][data-frame-indent='1'] {
- margin-left: -2ch;
+.codeBlock .frame[data-frame-type='highlighted'][data-frame-indent='1'],
+.codeBlock .frame[data-frame-type='focus'][data-frame-indent='1'] {
+ transform: translateX(-2ch);
}
-.codeBlock .frame[data-frame-type='highlighted'][data-frame-indent='2'] {
- margin-left: -4ch;
+.codeBlock .frame[data-frame-type='highlighted'][data-frame-indent='2'],
+.codeBlock .frame[data-frame-type='focus'][data-frame-indent='2'] {
+ transform: translateX(-4ch);
}
/* Reset indent shift when expanded */
-.expanded .codeBlock .frame[data-frame-type='highlighted'] {
- margin-left: 0;
+.expanded .codeBlock .frame[data-frame-type='highlighted'],
+.expanded .codeBlock .frame[data-frame-type='focus'] {
+ transform: translateX(0);
}
```
-### Demo: Collapsible Code Block
-
-The following demo shows a code block that starts collapsed, revealing only the highlighted and padding frames. Click **Expand** to show the full source.
-
-import { DemoCollapsibleCode } from './demos/collapsible-code';
-
-
-
-### Demo: @focus with Multiple Regions
-
-When there are multiple highlight regions, `@focus` controls which region receives padding frames. In this demo, the `useEffect` block on the second region is focused — its surrounding lines get padding frames while the first region (`formatDate` call) does not.
-
-import { DemoFocusCode } from './demos/focus-code';
-
-
-
-### Demo: Indent Shifting
-
-With no padding configured, only `highlighted` and normal frames are produced. The `data-frame-indent` attribute on highlighted frames tells CSS how far the code is indented. This demo highlights an import statement (indent 0) and the deeply nested `` usage (indent 3). When collapsed, the JSX frame shifts left by `6ch` so it aligns with the left edge, then resets when expanded.
-
-import { DemoIndentCode } from './demos/indent-code';
-
-
-
----
-
-## Implementation Details
-
-### How It Works
+### Collapsible Code Block
-1. **Comment Extraction**: Comments are extracted during parsing with `notableCommentsPrefix: ['@highlight']`
-2. **Directive Parsing**: The enhancer scans comments for `@highlight`, `@highlight-start`, `@highlight-end`, and `@highlight-text` patterns
-3. **Line Calculation**: Single-line directives mark their own line; multi-line directives mark all lines from the first line after `@highlight-start` to the last line before `@highlight-end`
-4. **Nested Detection**: Lines inside multiple ranges are automatically marked as strong emphasis
-5. **HAST Modification**: The enhancer traverses the HAST tree and adds `dataHl: ''` (or `dataHl: 'strong'` for nested lines or descriptions ending with `!`) to line elements
+When a code block has both visible frames (highlighted, focus, padding) and hidden frames (normal, unfocused), the `` element receives a `data-collapsible` attribute. Use this to conditionally show an expand/collapse toggle — code blocks without collapsible frames won't render the button:
-### HAST Structure
-
-Lines in the HAST tree have this structure after enhancement:
-
-```ts
-{
- type: 'element',
- tagName: 'span',
- properties: {
- className: 'line',
- dataLn: 3, // Line number
- dataHl: '', // Added by enhancer (or 'strong' for nested/!)
- dataHlDescription: 'We track state', // Optional description
- dataHlPosition: 'single', // 'single' | 'start' | 'end' for line position
- },
- children: [/* code tokens */]
+```css
+/* Hide toggle by default */
+.toggle {
+ display: none;
}
-```
-
-For text highlighting with `@highlight-text "text"`, the specified text within the line is wrapped in a span:
-```ts
-{
- type: 'element',
- tagName: 'span',
- properties: { dataHl: '' },
- children: [{ type: 'text', value: 'text' }]
+/* Show only when the code block has collapsible frames */
+.pre code:has([data-collapsible]) ~ .toggle {
+ display: block;
}
```
-### Data Attributes
-
-The enhancer adds these attributes to line elements:
-
-| Attribute | Type | Description |
-| --------------------- | ------ | -------------------------------------------------------------------------------------------------- |
-| `data-hl` | string | `""` for normal, `"strong"` if description ends with `!` or line is nested |
-| `data-hl-description` | string | Description text (if provided in comment) |
-| `data-hl-position` | string | `"single"` for single-line, `"start"` or `"end"` for multiline range bounds (from innermost range) |
-
-For single-line emphasis with `@highlight`:
-
-- `data-hl` is set (or `data-hl="strong"` if description ends with `!`)
-- `data-hl-description` is set if a description was provided
-- `data-hl-position="single"` is set to distinguish from multiline range boundaries
-
-For multi-line emphasis with `@highlight-start` / `@highlight-end`:
-
-- `data-hl` is set on all lines in the range (or `data-hl="strong"` if description ends with `!` or line is nested)
-- `data-hl-description` is set on the **first** line (if provided)
-- `data-hl-position="start"` is set on the **first** line (only for ranges with more than one line)
-- `data-hl-position="end"` is set on the **last** line (only for ranges with more than one line)
+The following demo shows a code block that starts collapsed, revealing only the highlighted and padding frames. Click **Expand** to show the full source.
-For text highlighting with `@highlight-text "text"`:
+import { DemoCollapsibleCode } from './demos/collapsible-code';
-- The specified text within the line is wrapped in a `` element
-- The line element itself does not receive any additional attributes
+
-### Stack-Based Pairing
+### Collapsible Demo
-Multi-line directives are paired using a stack:
+A full demo component with a live preview, file tabs, and collapsible code. The code section starts collapsed, showing only the focused region.
-- `@highlight-start` pushes onto the stack
-- `@highlight-end` pops from the stack and emphasizes the range
-- Inner (nested) ranges are processed before outer ranges
-- Lines that appear in multiple ranges are automatically marked as strong emphasis
-- Position markers from inner ranges are preserved when outer ranges are processed
+import { DemoCollapsibleDemo } from './demos/collapsible-demo';
----
+
-## Configuration
+### @focus with Multiple Regions
-### Adding to Source Enhancers
+This demo uses `@highlight` on the `formatDate` call and standalone `@focus-start`/`@focus-end` around the `useEffect` block. The `useEffect` region is focused (padding frames surround it) but has no line-level highlighting, while the `formatDate` line has line-level highlighting but is unfocused.
-Configure in your webpack loader or server-side loading:
+import { DemoFocusCode } from './demos/focus-code';
-```ts
-import { enhanceCodeEmphasis } from '@mui/internal-docs-infra/pipeline/enhanceCodeEmphasis';
-import { createLoadServerSource } from '@mui/internal-docs-infra/pipeline/loadServerSource';
-import { loadCodeVariant } from '@mui/internal-docs-infra/pipeline/loadCodeVariant';
+
-const sourceEnhancers = [enhanceCodeEmphasis];
-```
+### Indent Shifting
-For configurable padding, use the factory:
+With no padding configured, only highlighted/focus and normal frames are produced. The `data-frame-indent` attribute on region frames tells CSS how far the code is indented. This demo highlights an import statement (indent 0) and uses standalone `@focus-start`/`@focus-end` around the deeply nested `` usage (indent 3). When collapsed, the focused frame shifts left by `6ch` so it aligns with the left edge, then resets when expanded.
-```ts
-import { createEnhanceCodeEmphasis } from '@mui/internal-docs-infra/pipeline/enhanceCodeEmphasis';
+import { DemoIndentCode } from './demos/indent-code';
-const sourceEnhancers = [
- createEnhanceCodeEmphasis({
- paddingFrameMaxSize: 3,
- focusFramesMaxSize: 10,
- }),
-];
-```
+
-Then pass to your loading pipeline:
+### Focus Max Size
-```ts
-// Create a loadSource that extracts emphasis comments
-const loadSource = createLoadServerSource({
- notableCommentsPrefix: ['@highlight'],
- removeCommentsWithPrefix: ['@highlight'],
-});
+When `focusFramesMaxSize` is set and a highlighted region exceeds that limit, a focused window is taken from the start of the region, and the remaining overflow lines are marked as unfocused. This demo uses `focusFramesMaxSize: 6` with the `