@honeydeck/honeydeck 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/AGENTS.md +4 -4
  2. package/DEVELOPMENT.md +6 -4
  3. package/Readme.md +15 -15
  4. package/SPEC.md +5 -4
  5. package/docs/browser-frame.md +38 -0
  6. package/docs/components.md +16 -57
  7. package/docs/configuration.md +13 -0
  8. package/docs/customization.md +2 -0
  9. package/docs/deeper-dive.md +32 -7
  10. package/docs/getting-started.md +4 -2
  11. package/docs/index.json +258 -0
  12. package/docs/keyboard.md +35 -0
  13. package/docs/list-style.md +53 -0
  14. package/docs/local-development.md +3 -1
  15. package/docs/mermaid.md +2 -0
  16. package/docs/mobile.md +2 -0
  17. package/docs/navigation.md +3 -1
  18. package/docs/notes.md +40 -0
  19. package/docs/pdf-export.md +6 -2
  20. package/docs/presenter-mode.md +8 -3
  21. package/docs/reveal-group.md +60 -0
  22. package/docs/reveal-with.md +39 -0
  23. package/docs/reveal.md +35 -0
  24. package/docs/skills.md +5 -3
  25. package/docs/slides.md +2 -0
  26. package/docs/slidev-migration.md +5 -0
  27. package/docs/steps-and-reveals.md +145 -8
  28. package/docs/timeline-steps.md +50 -0
  29. package/package.json +6 -2
  30. package/skills/SPEC.md +6 -6
  31. package/skills/honeydeck/SKILL.md +9 -9
  32. package/skills/slidev-migration/SKILL.md +7 -6
  33. package/src/SPEC.md +8 -3
  34. package/src/cli/SPEC.md +3 -2
  35. package/src/cli/pdf.ts +11 -4
  36. package/src/remark/SPEC.md +102 -2
  37. package/src/remark/code-utils.ts +151 -0
  38. package/src/remark/shiki-code-blocks.ts +329 -136
  39. package/src/remark/step-numbering.ts +408 -103
  40. package/src/runtime/Deck.tsx +133 -116
  41. package/src/runtime/EffectiveColorModeContext.tsx +37 -0
  42. package/src/runtime/SPEC.md +21 -8
  43. package/src/runtime/SlideCanvas.tsx +19 -16
  44. package/src/runtime/SlideScaleContext.tsx +23 -0
  45. package/src/runtime/components/CodeBlock.tsx +19 -202
  46. package/src/runtime/components/CodeBlockCopyButton.tsx +64 -0
  47. package/src/runtime/components/CodeBlockShared.ts +17 -0
  48. package/src/runtime/components/Fade.tsx +51 -0
  49. package/src/runtime/components/FadeGroup.tsx +175 -0
  50. package/src/runtime/components/FadeWith.tsx +54 -0
  51. package/src/runtime/components/MagicCodeBlock.tsx +223 -0
  52. package/src/runtime/components/NavBar.tsx +1 -1
  53. package/src/runtime/components/NormalCodeBlock.tsx +128 -0
  54. package/src/runtime/components/Reveal.tsx +27 -27
  55. package/src/runtime/components/RevealGroup.tsx +143 -41
  56. package/src/runtime/components/RevealWith.tsx +63 -0
  57. package/src/runtime/components/SPEC.md +115 -10
  58. package/src/runtime/components/TimelineReveal.tsx +81 -0
  59. package/src/runtime/components/index.ts +13 -5
  60. package/src/runtime/components/timelineVisibility.ts +45 -0
  61. package/src/runtime/index.ts +9 -1
  62. package/src/runtime/navigation.ts +6 -4
  63. package/src/runtime/presentationApi.ts +449 -0
  64. package/src/runtime/views/PresenterCastButton.tsx +39 -0
  65. package/src/runtime/views/PresenterView.tsx +21 -4
  66. package/src/runtime/views/SPEC.md +7 -5
  67. package/src/theme/base.css +67 -2
  68. package/src/vite-plugin/SPEC.md +20 -2
  69. package/src/vite-plugin/index.ts +16 -2
  70. package/src/vite-plugin/splitter.ts +1 -0
  71. package/src/vite-plugin/virtual-modules.ts +16 -6
@@ -2,6 +2,7 @@ import {
2
2
  Children,
3
3
  type CSSProperties,
4
4
  cloneElement,
5
+ Fragment,
5
6
  isValidElement,
6
7
  type Key,
7
8
  type ReactElement,
@@ -14,17 +15,26 @@ import { Reveal } from "./Reveal.tsx";
14
15
  // Types
15
16
  // ---------------------------------------------------------------------------
16
17
 
18
+ export type RevealGroupListRevealMode = "direct" | "nested";
19
+
17
20
  export type RevealGroupProps = {
18
21
  /**
19
22
  * The step index for the first child. Subsequent children increment by 1.
20
23
  * Injected by the remark step-numbering plugin; defaults to 1.
21
24
  */
22
25
  at?: number;
26
+ /**
27
+ * List reveal strategy. `"direct"` reveals only top-level list items;
28
+ * `"nested"` reveals nested list items depth-first.
29
+ */
30
+ listRevealMode?: RevealGroupListRevealMode;
23
31
  /**
24
32
  * Internal compiler-provided absolute steps for each direct reveal target.
25
33
  * This lets nested timeline entries create gaps before later group targets.
26
34
  */
27
35
  targetStepsJson?: string;
36
+ /** Remove hidden children from the DOM/layout instead of reserving space. */
37
+ ephemeral?: boolean;
28
38
  children?: ReactNode;
29
39
  };
30
40
 
@@ -54,6 +64,20 @@ function isListElement(child: ReactNode): child is ElementWithCommonProps {
54
64
  );
55
65
  }
56
66
 
67
+ function isListItemElement(child: ReactNode): child is ElementWithCommonProps {
68
+ return isValidElement(child) && child.type === "li";
69
+ }
70
+
71
+ function isIntrinsicElementWithCommonProps(
72
+ child: ReactNode,
73
+ ): child is ElementWithCommonProps {
74
+ return isValidElement(child) && typeof child.type === "string";
75
+ }
76
+
77
+ function isFragmentElement(child: ReactNode): child is ElementWithCommonProps {
78
+ return isValidElement(child) && child.type === Fragment;
79
+ }
80
+
57
81
  function isElementWithCommonProps(
58
82
  child: ReactNode,
59
83
  ): child is ElementWithCommonProps {
@@ -64,19 +88,23 @@ function childKey(child: ReactNode, fallback: string): Key {
64
88
  return isValidElement(child) && child.key != null ? child.key : fallback;
65
89
  }
66
90
 
67
- function revealStyle(
91
+ function revealVisibility(
68
92
  stepIndex: number,
69
93
  at: number,
70
94
  showFutureSteps: boolean,
71
95
  futureStepOpacity: number,
72
- ): CSSProperties {
96
+ ephemeral: boolean,
97
+ ): { shouldRender: boolean; style: CSSProperties } {
73
98
  const visible = stepIndex >= at;
74
99
  const previewFuture = !visible && showFutureSteps;
75
100
 
76
101
  return {
77
- visibility: visible || previewFuture ? "visible" : "hidden",
78
- opacity: visible ? 1 : previewFuture ? futureStepOpacity : 0,
79
- transition: "opacity 300ms ease",
102
+ shouldRender: visible || previewFuture || !ephemeral,
103
+ style: {
104
+ visibility: visible || previewFuture ? "visible" : "hidden",
105
+ opacity: visible ? 1 : previewFuture ? futureStepOpacity : 0,
106
+ transition: "opacity 300ms ease",
107
+ },
80
108
  };
81
109
  }
82
110
 
@@ -114,18 +142,22 @@ function parseTargetSteps(targetStepsJson: string | undefined): number[] {
114
142
  * ```
115
143
  *
116
144
  * Honeydeck assigns the starting `at` value during MDX compilation and advances
117
- * the slide step counter by the number of reveal targets. Nested timeline
118
- * entries can provide `targetStepsJson` so later group items keep the correct
119
- * absolute step positions.
145
+ * the slide step counter by the number of reveal targets. Set
146
+ * `listRevealMode="nested"` to reveal nested list items depth-first. Nested
147
+ * timeline entries can provide `targetStepsJson` so later group items keep the
148
+ * correct absolute step positions.
120
149
  */
121
150
  export function RevealGroup({
122
151
  at = 1,
152
+ listRevealMode = "direct",
123
153
  targetStepsJson,
154
+ ephemeral = false,
124
155
  children,
125
156
  }: RevealGroupProps) {
126
157
  const { stepIndex, showFutureSteps, futureStepOpacity } = useTimeline();
127
158
  const revealTargets = toMeaningfulArray(children);
128
159
  const targetSteps = parseTargetSteps(targetStepsJson);
160
+ const revealNestedListItems = listRevealMode === "nested";
129
161
  let targetIndex = 0;
130
162
  let nextAt = at;
131
163
 
@@ -143,47 +175,117 @@ export function RevealGroup({
143
175
  return fallbackAt;
144
176
  }
145
177
 
178
+ function cloneRevealedElement(
179
+ element: ElementWithCommonProps,
180
+ elementAt: number,
181
+ key: Key,
182
+ childrenOverride?: ReactNode,
183
+ ): ReactNode {
184
+ const { shouldRender, style } = revealVisibility(
185
+ stepIndex,
186
+ elementAt,
187
+ showFutureSteps,
188
+ futureStepOpacity,
189
+ ephemeral,
190
+ );
191
+
192
+ if (!shouldRender) return null;
193
+
194
+ return cloneElement(element, {
195
+ key,
196
+ style: {
197
+ ...element.props.style,
198
+ ...style,
199
+ },
200
+ ...(childrenOverride === undefined ? {} : { children: childrenOverride }),
201
+ });
202
+ }
203
+
204
+ function renderDirectListItem(listItem: ReactNode): ReactNode {
205
+ const itemAt = nextTargetAt();
206
+ const itemKey = childKey(listItem, `reveal-item-${itemAt}`);
207
+
208
+ if (!isElementWithCommonProps(listItem)) {
209
+ return (
210
+ <Reveal key={itemKey} at={itemAt} ephemeral={ephemeral}>
211
+ {listItem}
212
+ </Reveal>
213
+ );
214
+ }
215
+
216
+ return cloneRevealedElement(listItem, itemAt, itemKey);
217
+ }
218
+
219
+ function renderNestedListItem(listItem: ReactNode): ReactNode {
220
+ if (!isListItemElement(listItem)) {
221
+ return renderDirectListItem(listItem);
222
+ }
223
+
224
+ const itemAt = nextTargetAt();
225
+ const itemKey = childKey(listItem, `reveal-item-${itemAt}`);
226
+
227
+ return cloneRevealedElement(
228
+ listItem,
229
+ itemAt,
230
+ itemKey,
231
+ renderNestedListChildren(listItem.props.children),
232
+ );
233
+ }
234
+
235
+ function renderNestedListChildren(listChildren: ReactNode): ReactNode {
236
+ return Children.map(listChildren, (child) => {
237
+ if (isListElement(child)) {
238
+ return renderListElement(child);
239
+ }
240
+
241
+ if (
242
+ (isIntrinsicElementWithCommonProps(child) ||
243
+ isFragmentElement(child)) &&
244
+ child.props.children !== undefined
245
+ ) {
246
+ return cloneElement(child, {
247
+ children: renderNestedListChildren(child.props.children),
248
+ });
249
+ }
250
+
251
+ return child;
252
+ });
253
+ }
254
+
255
+ function renderListElement(listElement: ElementWithCommonProps): ReactNode {
256
+ const listItems = toMeaningfulArray(listElement.props.children);
257
+ const listKey = childKey(listElement, `reveal-list-${at}-${targetIndex}`);
258
+ const renderedListItems = listItems.map((listItem) =>
259
+ revealNestedListItems
260
+ ? renderNestedListItem(listItem)
261
+ : renderDirectListItem(listItem),
262
+ );
263
+
264
+ if (ephemeral && renderedListItems.every((item) => item === null)) {
265
+ return null;
266
+ }
267
+
268
+ return cloneElement(listElement, {
269
+ key: listKey,
270
+ children: renderedListItems,
271
+ });
272
+ }
273
+
146
274
  return (
147
275
  <>
148
- {revealTargets.map((child, _index) => {
276
+ {revealTargets.map((child) => {
149
277
  if (isListElement(child)) {
150
- const listItems = toMeaningfulArray(child.props.children);
151
- const listKey = childKey(child, `reveal-list-${at}-${targetIndex}`);
152
-
153
- return cloneElement(child, {
154
- key: listKey,
155
- children: listItems.map((listItem) => {
156
- const itemAt = nextTargetAt();
157
- const itemKey = childKey(listItem, `reveal-item-${itemAt}`);
158
-
159
- if (!isElementWithCommonProps(listItem)) {
160
- return (
161
- <Reveal key={itemKey} at={itemAt}>
162
- {listItem}
163
- </Reveal>
164
- );
165
- }
166
-
167
- return cloneElement(listItem, {
168
- key: itemKey,
169
- style: {
170
- ...listItem.props.style,
171
- ...revealStyle(
172
- stepIndex,
173
- itemAt,
174
- showFutureSteps,
175
- futureStepOpacity,
176
- ),
177
- },
178
- });
179
- }),
180
- });
278
+ return renderListElement(child);
181
279
  }
182
280
 
183
281
  const childAt = nextTargetAt();
184
282
 
185
283
  return (
186
- <Reveal key={childKey(child, `reveal-child-${childAt}`)} at={childAt}>
284
+ <Reveal
285
+ key={childKey(child, `reveal-child-${childAt}`)}
286
+ at={childAt}
287
+ ephemeral={ephemeral}
288
+ >
187
289
  {child}
188
290
  </Reveal>
189
291
  );
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ TimelineReveal,
4
+ type TimelineRevealElement,
5
+ } from "./TimelineReveal.tsx";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export type RevealWithProps = {
12
+ /**
13
+ * Slide-local reveal target name or existing slide-local timeline step.
14
+ * String targets are resolved by the compiler to an internal `at` value.
15
+ */
16
+ target?: string | number;
17
+ /** Existing slide-local timeline step to reveal with. */
18
+ at?: number;
19
+ /**
20
+ * Wrapper element. Injected by the compiler from MDX context:
21
+ * flow/block usages use `div`, text/inline usages use `span`.
22
+ */
23
+ as?: TimelineRevealElement;
24
+ /** Additional CSS class for custom transition overrides. */
25
+ className?: string;
26
+ /** Remove hidden content from the DOM/layout instead of reserving space. */
27
+ ephemeral?: boolean;
28
+ children?: ReactNode;
29
+ };
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Component
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Reveals content at the same timeline step as an existing reveal or explicit
37
+ * slide-local step, without creating a new step.
38
+ */
39
+ export function RevealWith({
40
+ as = "div",
41
+ at,
42
+ target,
43
+ className = "",
44
+ ephemeral = false,
45
+ children,
46
+ }: RevealWithProps) {
47
+ const revealAt = typeof target === "number" ? target : (at ?? 1);
48
+
49
+ return (
50
+ <TimelineReveal
51
+ as={as}
52
+ at={revealAt}
53
+ className={className}
54
+ ephemeral={ephemeral}
55
+ dataAttributes={{
56
+ "data-honeydeck-reveal-with":
57
+ typeof target === "string" ? target : undefined,
58
+ }}
59
+ >
60
+ {children}
61
+ </TimelineReveal>
62
+ );
63
+ }
@@ -7,14 +7,14 @@
7
7
  All core components are explicit imports from the `'@honeydeck/honeydeck'` package. They are also exported from `@honeydeck/honeydeck/components`:
8
8
 
9
9
  ```mdx
10
- import { Reveal, RevealGroup, TimelineSteps, ListStyle, Keyboard, BrowserFrame, Notes } from '@honeydeck/honeydeck'
10
+ import { Reveal, RevealWith, RevealGroup, Fade, FadeWith, FadeGroup, TimelineSteps, ListStyle, Keyboard, BrowserFrame, Notes } from '@honeydeck/honeydeck'
11
11
  ```
12
12
 
13
- Injected fenced code blocks render through `HoneydeckCodeBlock` from the direct `@honeydeck/honeydeck/components/code-block` subpath. The component is not part of the public barrel, but transformed slide code blocks must show syntax-highlighted code and reveal an icon-only copy button on hover or keyboard focus. Copying writes the original fenced source text.
13
+ Injected fenced code blocks render through `HoneydeckCodeBlock` from the direct `@honeydeck/honeydeck/components/code-block/normal` subpath. Injected Magic Code blocks render through `HoneydeckMagicCodeBlock` from the direct `@honeydeck/honeydeck/components/code-block/magic` subpath. Both implementations share the `CodeBlock` parent frame from `@honeydeck/honeydeck/components/code-block` for wrapper styling and copy-button placement, but generated slides import the concrete implementations directly instead of using a code-block barrel. These components are not part of the public component barrel, but transformed slide code blocks must show syntax-highlighted code and reveal an icon-only copy button on hover or keyboard focus. Copying writes the original fenced source text, or for Magic Code, the currently visible source state. Normal stepped code blocks and Magic Code blocks fade specifically highlighted lines from dimmed opacity to full opacity when those lines become active. Magic Code line highlighting must update correctly in both directions, including transitions from a dimmed state to an `all` highlight state. Magic Code token movement, enter/leave transitions, and container size transitions start together and use Shiki Magic Move's duration so resizing stays synchronized with content movement. Entering tokens fade slowly from transparent to their active line-dimming opacity, and leaving tokens fade slowly to transparent instead of popping in or out. Magic Code uses Honeydeck's centralized effective color-mode context so code blocks do not observe document attributes individually. Magic Code lazily parses only the active theme's precompiled token JSON; duplicated timeline states may reference shared unique token states to keep generated output smaller. Magic Code lets Shiki perform its initial render for the starting state, then waits two animation frames before forwarding timeline state changes so the first user-driven transition diffs from a stable baseline. During PDF export, Magic Code skips that baseline delay and disables Magic Move animation so screenshots capture the requested timeline state instead of an in-progress transition. Magic Code passes Honeydeck's current slide transform scale to Shiki Magic Move so measured positions and inline animation sizes stay in the same coordinate system and the container does not pop when the animation releases its inline size.
14
14
 
15
15
  ### Color Mode Controls
16
16
 
17
- `@honeydeck/honeydeck/components` exports the configured color mode type and color mode cycle button for public imports used by Honeydeck chrome surfaces and the marketing site.
17
+ `@honeydeck/honeydeck/components` exports the configured color mode type and color mode cycle button for public imports used by Honeydeck chrome surfaces and the docs site.
18
18
 
19
19
  ```tsx
20
20
  import { ColorModeCycleButton, type ColorMode } from '@honeydeck/honeydeck/components'
@@ -30,7 +30,7 @@ Behavior:
30
30
 
31
31
  ### Button Controls
32
32
 
33
- `@honeydeck/honeydeck/components` exports generic Honeydeck-token-based button primitives and class recipes for runtime chrome, runtime reference pages, and marketing controls.
33
+ `@honeydeck/honeydeck/components` exports generic Honeydeck-token-based button primitives and class recipes for runtime chrome, runtime reference pages, and docs controls.
34
34
 
35
35
  ```tsx
36
36
  import { Button, buttonPrimaryClass, iconButtonClass } from '@honeydeck/honeydeck/components'
@@ -41,7 +41,7 @@ Behavior:
41
41
  - `<Button>` renders a native `button` with `type="button"` by default.
42
42
  - `variant` selects one of `primary`, `secondary`, `icon`, `small`, or `quiet`.
43
43
  - `buttonClass(variant, className)` returns the matching token-based Tailwind class string and appends `className` when provided.
44
- - Exported class recipes use only shipped Honeydeck base-theme tokens for foreground, surface, border, primary, and transition behavior (`--honeydeck-primary`, `--honeydeck-primary-foreground`, `--honeydeck-surface`, `--honeydeck-surface-foreground`, `--honeydeck-border`, `--honeydeck-foreground`, `--honeydeck-background`) so public imports do not depend on marketing-only aliases at publish time.
44
+ - Exported class recipes use only shipped Honeydeck base-theme tokens for foreground, surface, border, primary, and transition behavior (`--honeydeck-primary`, `--honeydeck-primary-foreground`, `--honeydeck-surface`, `--honeydeck-surface-foreground`, `--honeydeck-border`, `--honeydeck-foreground`, `--honeydeck-background`) so public imports do not depend on docs-site-only aliases at publish time.
45
45
 
46
46
  ### Runtime chrome buttons
47
47
 
@@ -54,23 +54,96 @@ Reveals content at the next timeline step.
54
54
  ```mdx
55
55
  <Reveal>This appears at step 1</Reveal>
56
56
  <Reveal>This appears at step 2</Reveal>
57
+ <Reveal name="summary">This named reveal appears at step 3</Reveal>
57
58
  ```
58
59
 
59
60
  Behavior:
60
61
 
61
- - Hidden content **reserves layout space** (`visibility: hidden` + `opacity: 0`, not `display: none`)
62
+ - Hidden content **reserves layout space** by default (`visibility: hidden` + `opacity: 0`, not `display: none`)
63
+ - With `ephemeral`, hidden content renders `null` and does not reserve layout space; future-step previews still render a muted ghost
62
64
  - Runtime wrapper matches MDX context: flow/block reveals render a block-level `div`, text/inline reveals render an inline `span`
63
65
  - Nested reveals are supported; inline nested reveals inside paragraphs must not create invalid `div`-inside-`p` HTML
64
66
  - Default effect: fade in
65
67
  - Reveals are **cumulative** (once visible, stays visible)
66
68
  - Supports `className` for custom transitions
67
- - Supports `at?: number`; Honeydeck injects this during compilation and manual use works as an escape hatch
68
- - Supports `as?: "div" | "span"`; Honeydeck injects this during compilation and manual use works as an escape hatch
69
+ - Supports `ephemeral?: boolean`
70
+ - Each authored `<Reveal>` adds one step to the slide timeline
71
+ - Supports `name?: string` as a slide-local reveal target for `<RevealWith target="...">` or `<FadeWith target="...">`
72
+ - `name` must be a literal non-empty string when present; dynamic expressions are not supported because target resolution happens at build time
73
+ - Duplicate `name` values on `<Reveal>` components in the same slide are build errors; the same name may be reused on different slides
74
+ - A named reveal renders `data-honeydeck-reveal-id="name"` on its wrapper for debugging/inspection; it does not render a DOM `id`
75
+ - `at?: number` is an internal compiler-injected prop, not a user-facing API; author-authored `at` values are build errors. Use `<RevealWith at={n}>` or `<FadeWith at={n}>` to sync content with an existing step
76
+ - Supports `as?: "div" | "span"`; Honeydeck injects this during compilation
69
77
  - No `effect` prop
70
78
 
79
+ ### `<RevealWith>`
80
+
81
+ Reveals content at the same timeline step as an existing reveal or explicit slide-local step. It never creates or consumes timeline steps.
82
+
83
+ ````mdx
84
+ <Reveal name="left">Left column appears first</Reveal>
85
+ <RevealWith target="left">Right column appears with the left column</RevealWith>
86
+
87
+ ```ts {1|2|3}
88
+ const answer = 42
89
+ console.log(answer)
90
+ ```
91
+
92
+ <RevealWith at={2}>This appears with the second slide step, such as a code highlight</RevealWith>
93
+ <RevealWith target={3}>This appears with slide step 3</RevealWith>
94
+ ````
95
+
96
+ Behavior:
97
+
98
+ - Requires exactly one of `target` or `at`
99
+ - String `target` must be a literal non-empty string and resolves to a `<Reveal name="...">` on the same slide
100
+ - Numeric `target={n}` and `at={n}` target an existing 1-based slide-local timeline step
101
+ - String `target` supports forward references; the named `<Reveal>` may appear before or after `<RevealWith>` in the same slide
102
+ - Missing targets and out-of-range step targets are build errors
103
+ - `RevealWith` visibility is cumulative, matching `<Reveal>`: once visible, it stays visible
104
+ - Hidden content reserves layout space by default; with `ephemeral`, hidden content renders `null` and does not reserve layout space
105
+ - Runtime wrapper matches MDX context: flow/block usages render a block-level `div`, text/inline usages render an inline `span`
106
+ - When string `target` is used, the wrapper renders `data-honeydeck-reveal-with="target"` for debugging/inspection
107
+ - `RevealWith` must not contain nested timeline producers
108
+
109
+ ### `<Fade>`
110
+
111
+ Starts visible and fades out at the next timeline step.
112
+
113
+ ```mdx
114
+ <Fade>This disappears at step 1</Fade>
115
+ <Fade>This disappears at step 2</Fade>
116
+ ```
117
+
118
+ Behavior:
119
+
120
+ - Content is visible while `stepIndex < at`
121
+ - Content is hidden while `stepIndex >= at`
122
+ - Hidden content reserves layout space by default
123
+ - With `ephemeral`, hidden content renders `null` and does not reserve layout space; future-step previews still render a muted ghost
124
+ - Runtime wrapper matches MDX context like `<Reveal>`
125
+ - Supports `className`, `ephemeral?: boolean`, and compiler-injected `as?: "div" | "span"`
126
+ - Authored `at` is a build error because `at` is internal compiler plumbing for step-producing components
127
+ - Must not contain nested timeline producers because a faded parent would hide later nested steps; put fade components inside a reveal target instead
128
+ - Default effect: fade out
129
+
130
+ ### `<FadeWith>`
131
+
132
+ Fades content out at the same timeline step as an existing reveal or explicit slide-local step. It never creates or consumes timeline steps.
133
+
134
+ ```mdx
135
+ <FadeWith target="left">This disappears with named reveal left</FadeWith>
136
+ <FadeWith target={3}>This disappears when step 3 is active</FadeWith>
137
+ <FadeWith at={2}>This disappears with slide step 2</FadeWith>
138
+ ```
139
+
140
+ Behavior matches `<Fade>` for wrapper selection, `className`, `ephemeral`, previews, and default transition. `<FadeWith>` uses the same `target`/`at` validation rules as `<RevealWith>` and must not contain nested timeline producers.
141
+
71
142
  ### `<RevealGroup>`
72
143
 
73
- Convenience: reveals each meaningful direct child one by one. Whitespace-only text children are ignored. As a special case, when a direct child is a Markdown/HTML/JSX list, each item in that list is revealed one after another while preserving the list container. Empty groups currently consume one timeline step.
144
+ Convenience: reveals each meaningful direct child one by one. Whitespace-only text children are ignored. As a special case, when a direct child is a Markdown/HTML/JSX list, each top-level item in that list is revealed one after another while preserving the list container. Empty groups currently consume one timeline step.
145
+
146
+ `listRevealMode?: "direct" | "nested"` controls list flattening. The default `"direct"` preserves existing behavior and reveals only top-level items of a direct child list. `"nested"` makes every nested list item in direct child lists a reveal target in depth-first document order while preserving the nested list structure. Because this prop changes compile-time step counting, authored MDX must use a static literal value.
74
147
 
75
148
  ```mdx
76
149
  <RevealGroup>
@@ -80,7 +153,7 @@ Convenience: reveals each meaningful direct child one by one. Whitespace-only te
80
153
  </RevealGroup>
81
154
  ```
82
155
 
83
- Each list item becomes its own timeline step.
156
+ Each top-level list item becomes its own timeline step. Supports `ephemeral?: boolean`, which is forwarded to generated child reveals.
84
157
 
85
158
  Nested timeline entries inside a group target are flattened after that target
86
159
  and before the following group target:
@@ -101,6 +174,38 @@ Timeline:
101
174
  2. Nested detail appears
102
175
  3. Sibling item appears
103
176
 
177
+ Nested list items can be revealed separately with `listRevealMode="nested"`:
178
+
179
+ ```mdx
180
+ <RevealGroup listRevealMode="nested">
181
+ - Parent
182
+ - Child A
183
+ - Child B
184
+ - Sibling
185
+ </RevealGroup>
186
+ ```
187
+
188
+ Timeline:
189
+
190
+ 1. Parent appears
191
+ 2. Child A appears
192
+ 3. Child B appears
193
+ 4. Sibling appears
194
+
195
+ ### `<FadeGroup>`
196
+
197
+ Convenience: fades each meaningful direct child out one by one. Whitespace-only text children are ignored. Direct Markdown/HTML/JSX lists are preserved and each list item fades out one after another. Empty groups currently consume one timeline step.
198
+
199
+ ```mdx
200
+ <FadeGroup>
201
+ - First point
202
+ - Second point
203
+ - Third point
204
+ </FadeGroup>
205
+ ```
206
+
207
+ Each list item becomes its own timeline step. Supports `ephemeral?: boolean`, which is forwarded to generated child fades. Fade group targets must not contain nested timeline producers because a faded parent would hide later nested steps; put fade components inside a reveal/reveal-group target instead.
208
+
104
209
  ### `<ListStyle>`
105
210
 
106
211
  Styles Markdown/HTML/JSX lists inside a wrapper. By default, it removes bullets from every list inside it. With the `bullets` prop, it renders custom bullet markers and supports one marker per nesting level. Deeper levels reuse the last configured marker.
@@ -0,0 +1,81 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { useTimeline } from "../TimelineContext.tsx";
3
+
4
+ export type TimelineRevealElement = "div" | "span";
5
+
6
+ export type TimelineRevealProps = {
7
+ /** The step index at which this content becomes visible. */
8
+ at?: number;
9
+ /** Wrapper element chosen from MDX block/inline context. */
10
+ as?: TimelineRevealElement;
11
+ /** Additional CSS class for custom transition overrides. */
12
+ className?: string;
13
+ /** Remove hidden content from the DOM/layout instead of reserving space. */
14
+ ephemeral?: boolean;
15
+ /** Extra data attributes for debugging/inspection. */
16
+ dataAttributes?: Record<string, string | undefined>;
17
+ children?: ReactNode;
18
+ };
19
+
20
+ export type TimelineRevealStyleOptions = {
21
+ stepIndex: number;
22
+ at: number;
23
+ showFutureSteps: boolean;
24
+ futureStepOpacity: number;
25
+ display?: CSSProperties["display"];
26
+ };
27
+
28
+ export function getTimelineRevealStyle({
29
+ stepIndex,
30
+ at,
31
+ showFutureSteps,
32
+ futureStepOpacity,
33
+ display,
34
+ }: TimelineRevealStyleOptions): CSSProperties {
35
+ const visible = stepIndex >= at;
36
+ const previewFuture = !visible && showFutureSteps;
37
+
38
+ return {
39
+ ...(display ? { display } : {}),
40
+ visibility: visible || previewFuture ? "visible" : "hidden",
41
+ opacity: visible ? 1 : previewFuture ? futureStepOpacity : 0,
42
+ transition: "opacity 300ms ease",
43
+ };
44
+ }
45
+
46
+ export function TimelineReveal({
47
+ as: Component = "div",
48
+ at = 1,
49
+ className = "",
50
+ ephemeral = false,
51
+ dataAttributes,
52
+ children,
53
+ }: TimelineRevealProps) {
54
+ const { stepIndex, showFutureSteps, futureStepOpacity } = useTimeline();
55
+ const visible = stepIndex >= at;
56
+ const previewFuture = !visible && showFutureSteps;
57
+ const style = getTimelineRevealStyle({
58
+ stepIndex,
59
+ at,
60
+ showFutureSteps,
61
+ futureStepOpacity,
62
+ display: Component === "span" ? "inline" : "block",
63
+ });
64
+
65
+ if (ephemeral && !visible && !previewFuture) return null;
66
+
67
+ return (
68
+ <Component
69
+ className={[
70
+ "honeydeck-reveal mb-[0.75em] text-[length:var(--honeydeck-font-size-body)] leading-[1.6] [&>:last-child]:mb-0",
71
+ className,
72
+ ]
73
+ .filter(Boolean)
74
+ .join(" ")}
75
+ style={style}
76
+ {...dataAttributes}
77
+ >
78
+ {children}
79
+ </Component>
80
+ );
81
+ }
@@ -4,7 +4,7 @@
4
4
  * These are the components that end users import in their MDX slides:
5
5
  *
6
6
  * ```mdx
7
- * import { Reveal, RevealGroup, Notes } from '@honeydeck/honeydeck'
7
+ * import { Reveal, RevealWith, RevealGroup, Notes } from '@honeydeck/honeydeck'
8
8
  * ```
9
9
  */
10
10
 
@@ -32,6 +32,12 @@ export {
32
32
  ColorModeCycleButton,
33
33
  getNextColorMode,
34
34
  } from "./ColorModeCycleButton.tsx";
35
+ export type { FadeProps } from "./Fade.tsx";
36
+ export { Fade } from "./Fade.tsx";
37
+ export type { FadeGroupProps } from "./FadeGroup.tsx";
38
+ export { FadeGroup } from "./FadeGroup.tsx";
39
+ export type { FadeWithProps } from "./FadeWith.tsx";
40
+ export { FadeWith } from "./FadeWith.tsx";
35
41
  export type { KeyboardKey, KeyboardProps } from "./Keyboard.tsx";
36
42
  export { Keyboard } from "./Keyboard.tsx";
37
43
  export type { ListBullet, ListBullets, ListStyleProps } from "./ListStyle.tsx";
@@ -42,6 +48,8 @@ export type { RevealProps } from "./Reveal.tsx";
42
48
  export { Reveal } from "./Reveal.tsx";
43
49
  export type { RevealGroupProps } from "./RevealGroup.tsx";
44
50
  export { RevealGroup } from "./RevealGroup.tsx";
51
+ export type { RevealWithProps } from "./RevealWith.tsx";
52
+ export { RevealWith } from "./RevealWith.tsx";
45
53
  export type {
46
54
  TimelineStepsPhase,
47
55
  TimelineStepsProps,
@@ -49,7 +57,7 @@ export type {
49
57
  } from "./TimelineSteps.tsx";
50
58
  export { TimelineSteps, useTimelineSteps } from "./TimelineSteps.tsx";
51
59
 
52
- // HoneydeckCodeBlock is intentionally NOT exported from the public barrel.
53
- // It is an internal component injected by remarkShikiCodeBlocks via a
54
- // direct subpath import: '@honeydeck/honeydeck/components/code-block'.
55
- // Do not import it from '@honeydeck/honeydeck' or '@honeydeck/honeydeck/components' — use the subpath.
60
+ // HoneydeckCodeBlock and HoneydeckMagicCodeBlock are intentionally NOT exported
61
+ // from the public component barrel. They are internal components injected by
62
+ // remarkShikiCodeBlocks via direct code-block implementation subpaths.
63
+ // Do not import them from '@honeydeck/honeydeck' or '@honeydeck/honeydeck/components'.
@@ -0,0 +1,45 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { useTimeline } from "../TimelineContext.tsx";
3
+
4
+ export type TimelineVisibilityMode = "reveal" | "fade";
5
+
6
+ export type TimelineVisibilityOptions = {
7
+ mode: TimelineVisibilityMode;
8
+ at: number;
9
+ target?: number;
10
+ ephemeral?: boolean;
11
+ };
12
+
13
+ export type TimelineVisibilityState = {
14
+ visible: boolean;
15
+ previewFuture: boolean;
16
+ shouldRender: boolean;
17
+ style: CSSProperties;
18
+ };
19
+
20
+ export function useTimelineVisibility({
21
+ mode,
22
+ at,
23
+ target = at,
24
+ ephemeral = false,
25
+ }: TimelineVisibilityOptions): TimelineVisibilityState {
26
+ const { stepIndex, showFutureSteps, futureStepOpacity } = useTimeline();
27
+ const visible = mode === "reveal" ? stepIndex >= target : stepIndex < target;
28
+ const previewFuture = !visible && showFutureSteps;
29
+ const shouldRender = visible || previewFuture || !ephemeral;
30
+
31
+ return {
32
+ visible,
33
+ previewFuture,
34
+ shouldRender,
35
+ style: {
36
+ visibility: visible || previewFuture ? "visible" : "hidden",
37
+ opacity: visible ? 1 : previewFuture ? futureStepOpacity : 0,
38
+ transition: "opacity 300ms ease",
39
+ },
40
+ };
41
+ }
42
+
43
+ export type TimelineVisibilityWrapperProps = {
44
+ children?: ReactNode;
45
+ };