@honeydeck/honeydeck 0.4.0 → 0.5.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 (64) hide show
  1. package/DEVELOPMENT.md +4 -1
  2. package/Readme.md +2 -2
  3. package/SPEC.md +3 -3
  4. package/docs/components-browser-frame.md +34 -0
  5. package/docs/components-keyboard.md +31 -0
  6. package/docs/components-list-style.md +49 -0
  7. package/docs/components-notes.md +36 -0
  8. package/docs/components-reveal-group.md +58 -0
  9. package/docs/components-reveal-with.md +37 -0
  10. package/docs/components-reveal.md +33 -0
  11. package/docs/components-timeline-steps.md +48 -0
  12. package/docs/components.md +13 -54
  13. package/docs/configuration.md +11 -0
  14. package/docs/deeper-dive.md +30 -7
  15. package/docs/getting-started.md +2 -2
  16. package/docs/navigation.md +1 -1
  17. package/docs/pdf-export.md +4 -2
  18. package/docs/presenter-mode.md +6 -3
  19. package/docs/skills.md +3 -3
  20. package/docs/slidev-migration.md +3 -0
  21. package/docs/steps-and-reveals.md +143 -8
  22. package/package.json +4 -1
  23. package/skills/SPEC.md +2 -2
  24. package/skills/honeydeck/SKILL.md +2 -2
  25. package/skills/slidev-migration/SKILL.md +1 -0
  26. package/src/SPEC.md +8 -3
  27. package/src/cli/SPEC.md +3 -2
  28. package/src/cli/pdf.ts +11 -4
  29. package/src/remark/SPEC.md +102 -2
  30. package/src/remark/code-utils.ts +151 -0
  31. package/src/remark/shiki-code-blocks.ts +329 -136
  32. package/src/remark/step-numbering.ts +408 -103
  33. package/src/runtime/Deck.tsx +133 -116
  34. package/src/runtime/EffectiveColorModeContext.tsx +37 -0
  35. package/src/runtime/SPEC.md +21 -8
  36. package/src/runtime/SlideCanvas.tsx +19 -16
  37. package/src/runtime/SlideScaleContext.tsx +23 -0
  38. package/src/runtime/components/CodeBlock.tsx +19 -202
  39. package/src/runtime/components/CodeBlockCopyButton.tsx +64 -0
  40. package/src/runtime/components/CodeBlockShared.ts +17 -0
  41. package/src/runtime/components/Fade.tsx +51 -0
  42. package/src/runtime/components/FadeGroup.tsx +175 -0
  43. package/src/runtime/components/FadeWith.tsx +54 -0
  44. package/src/runtime/components/MagicCodeBlock.tsx +223 -0
  45. package/src/runtime/components/NavBar.tsx +1 -1
  46. package/src/runtime/components/NormalCodeBlock.tsx +128 -0
  47. package/src/runtime/components/Reveal.tsx +27 -27
  48. package/src/runtime/components/RevealGroup.tsx +143 -41
  49. package/src/runtime/components/RevealWith.tsx +63 -0
  50. package/src/runtime/components/SPEC.md +112 -7
  51. package/src/runtime/components/TimelineReveal.tsx +81 -0
  52. package/src/runtime/components/index.ts +13 -5
  53. package/src/runtime/components/timelineVisibility.ts +45 -0
  54. package/src/runtime/index.ts +9 -1
  55. package/src/runtime/navigation.ts +6 -4
  56. package/src/runtime/presentationApi.ts +449 -0
  57. package/src/runtime/views/PresenterCastButton.tsx +39 -0
  58. package/src/runtime/views/PresenterView.tsx +21 -4
  59. package/src/runtime/views/SPEC.md +7 -5
  60. package/src/theme/base.css +67 -2
  61. package/src/vite-plugin/SPEC.md +20 -2
  62. package/src/vite-plugin/index.ts +16 -2
  63. package/src/vite-plugin/splitter.ts +1 -0
  64. package/src/vite-plugin/virtual-modules.ts +16 -6
@@ -1,210 +1,27 @@
1
- /**
2
- * HoneydeckCodeBlock runtime code block component with syntax highlighting
3
- * and timeline-driven step-through.
4
- *
5
- * ### Usage
6
- * This component is **not imported directly by slide authors**. The
7
- * `remarkShikiCodeBlocks` remark plugin automatically replaces fenced code
8
- * blocks in MDX with `<HoneydeckCodeBlock>` elements at compile time.
9
- *
10
- * ### Props
11
- * - `html` — Pre-highlighted HTML string from shiki (full `<pre>…</pre>`).
12
- * - `stepsJson` — JSON-encoded `StepGroup[]` (e.g. `[[2],[4,5],"all"]`).
13
- * Empty array = no step-through.
14
- * - `startAt` — 1-based timeline step at which group 1 activates.
15
- * Group 0 is the baseline active highlight; 0 for non-stepped blocks.
16
- * - `source` — Original fenced code text for clipboard copying.
17
- *
18
- * ### Step-through mechanics
19
- * The component reads `stepIndex` from `useTimeline()` and computes the active
20
- * group index: group 0 before `startAt`, then later groups from `startAt`.
21
- * It then adds `data-dim="1"` and an inline opacity style to non-highlighted
22
- * `.line` spans in the rendered HTML.
23
- * Base CSS also targets the attribute as a fallback.
24
- *
25
- * The highlighted HTML is injected via `dangerouslySetInnerHTML`, so the
26
- * step-specific `data-dim` attributes are computed as part of the HTML string
27
- * before React writes it to the DOM.
28
- */
1
+ import type { ReactNode } from "react";
2
+ import { CodeBlockCopyButton } from "./CodeBlockCopyButton.tsx";
29
3
 
30
- import { CheckIcon, CopyIcon } from "lucide-react";
31
- import { useMemo, useState } from "react";
32
- import { useTimeline } from "../TimelineContext.tsx";
33
-
34
- // ---------------------------------------------------------------------------
35
- // Types
36
- // ---------------------------------------------------------------------------
37
-
38
- /** A single step-through group: line numbers (1-based) or 'all'. */
39
- export type StepGroup = number[] | "all";
40
-
41
- type HoneydeckCodeBlockProps = {
42
- /** Full shiki HTML output (includes the `<pre>` element). */
43
- html: string;
44
- /** JSON-encoded StepGroup[] — empty array = no step-through. */
45
- stepsJson: string;
46
- /** 1-based timeline step where group 1 activates (0 = no step-through). */
47
- startAt: number;
48
- /** Original fenced code text copied by the hover/focus copy control. */
4
+ type CodeBlockProps = {
5
+ /** Original source copied by the hover/focus copy control. */
49
6
  source?: string;
7
+ /** Extra classes for a specific code block implementation. */
8
+ className?: string;
9
+ children: ReactNode;
50
10
  };
51
11
 
52
- const DATA_DIM_ATTR = /\sdata-dim=(["'])1\1/g;
53
- const LINE_SPAN = /<span\b([^>]*)>/g;
54
- const LINE_CLASS = /\bclass=(["'])[^"']*\bline\b[^"']*\1/;
55
- const DATA_LINE = /\bdata-line=(["'])(\d+)\1/;
56
- const STYLE_ATTR = /\sstyle=(["'])(.*?)\1/g;
57
- const DIM_STYLE_DECL = "opacity: var(--honeydeck-code-line-dim-opacity);";
58
-
59
- async function writeClipboardText(text: string): Promise<boolean> {
60
- try {
61
- if (navigator.clipboard) {
62
- await navigator.clipboard.writeText(text);
63
- return true;
64
- }
65
- } catch {
66
- // Fall back to the textarea path below.
67
- }
68
-
69
- try {
70
- const textarea = document.createElement("textarea");
71
- textarea.value = text;
72
- textarea.setAttribute("readonly", "");
73
- textarea.style.position = "fixed";
74
- textarea.style.inset = "0";
75
- textarea.style.opacity = "0";
76
- document.body.appendChild(textarea);
77
- textarea.select();
78
- const didCopy = document.execCommand("copy");
79
- document.body.removeChild(textarea);
80
- return didCopy;
81
- } catch {
82
- return false;
83
- }
84
- }
85
-
86
- function removeDimStyle(html: string): string {
87
- return html.replace(STYLE_ATTR, (match, quote: string, style: string) => {
88
- if (!style.includes(DIM_STYLE_DECL)) return match;
89
-
90
- const cleanedStyle = style
91
- .replace(DIM_STYLE_DECL, "")
92
- .replace(/\s{2,}/g, " ")
93
- .trim();
94
-
95
- return cleanedStyle ? ` style=${quote}${cleanedStyle}${quote}` : "";
96
- });
97
- }
98
-
99
- function addDimAttributes(attrs: string): string {
100
- if (attrs.includes(DIM_STYLE_DECL)) return `${attrs} data-dim="1"`;
101
-
102
- const withStyle = attrs.replace(
103
- STYLE_ATTR,
104
- (_match, quote: string, style: string) =>
105
- ` style=${quote}${style.trim()}${style.trim().endsWith(";") ? "" : ";"} ${DIM_STYLE_DECL}${quote}`,
106
- );
107
-
108
- if (withStyle !== attrs) return `${withStyle} data-dim="1"`;
109
-
110
- return `${attrs} data-dim="1" style="${DIM_STYLE_DECL}"`;
111
- }
112
-
113
- export function applyCodeStepDimming(
114
- html: string,
115
- steps: StepGroup[],
116
- stepIndex: number,
117
- startAt: number,
118
- ): string {
119
- const cleanHtml = removeDimStyle(html.replace(DATA_DIM_ATTR, ""));
120
-
121
- if (steps.length === 0) return cleanHtml;
122
-
123
- let activeGroupIndex = 0;
124
- if (startAt > 0 && stepIndex >= startAt) {
125
- activeGroupIndex = Math.min(stepIndex - startAt + 1, steps.length - 1);
126
- }
127
-
128
- const activeGroup = steps[activeGroupIndex];
129
- if (!activeGroup || activeGroup === "all") return cleanHtml;
130
-
131
- return cleanHtml.replace(LINE_SPAN, (match, attrs: string) => {
132
- if (!LINE_CLASS.test(attrs)) return match;
133
-
134
- const dataLine = attrs.match(DATA_LINE);
135
- if (!dataLine) return match;
136
-
137
- const lineNumber = parseInt(dataLine[2] ?? "", 10);
138
- if (activeGroup.includes(lineNumber)) return match;
139
-
140
- return `<span${addDimAttributes(attrs)}>`;
141
- });
142
- }
143
-
144
- // ---------------------------------------------------------------------------
145
- // Component
146
- // ---------------------------------------------------------------------------
147
-
148
- /**
149
- * Renders a pre-highlighted code block and applies timeline-driven line
150
- * dimming for step-through walkthroughs.
151
- *
152
- * Export name matches the identifier injected by `remarkShikiCodeBlocks`:
153
- * `import { HoneydeckCodeBlock } from '@honeydeck/honeydeck/components/code-block'`
154
- */
155
- export function HoneydeckCodeBlock({
156
- html,
157
- stepsJson,
158
- startAt,
159
- source,
160
- }: HoneydeckCodeBlockProps) {
161
- const { stepIndex } = useTimeline();
162
- const [copied, setCopied] = useState(false);
163
-
164
- // Parse step groups once (stepsJson is static — set at compile time)
165
- const steps = useMemo((): StepGroup[] => {
166
- try {
167
- return JSON.parse(stepsJson) as StepGroup[];
168
- } catch {
169
- return [];
170
- }
171
- }, [stepsJson]);
172
-
173
- const dimmedHtml = useMemo(
174
- () => applyCodeStepDimming(html, steps, stepIndex, startAt),
175
- [html, startAt, stepIndex, steps],
176
- );
177
-
178
- async function copySource() {
179
- if (!source) return;
180
-
181
- if (await writeClipboardText(source)) {
182
- setCopied(true);
183
- window.setTimeout(() => setCopied(false), 1600);
184
- }
185
- }
186
-
12
+ /** Shared frame for Honeydeck code block implementations. */
13
+ export function CodeBlock({ source, className, children }: CodeBlockProps) {
187
14
  return (
188
- <div className="honeydeck-code-block group relative mb-[0.75em] overflow-hidden rounded-honeydeck font-mono text-[length:var(--honeydeck-font-size-code)] [&_.line]:transition-opacity [&_.line]:duration-150 [&_.line]:ease-in">
189
- {source ? (
190
- <button
191
- className="honeydeck-code-copy-button"
192
- type="button"
193
- aria-label={copied ? "Code copied" : "Copy code"}
194
- title={copied ? "Copied" : "Copy code"}
195
- onClick={copySource}
196
- >
197
- {copied ? (
198
- <CheckIcon aria-hidden="true" />
199
- ) : (
200
- <CopyIcon aria-hidden="true" />
201
- )}
202
- </button>
203
- ) : null}
204
- <div
205
- // biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki generates this highlighted HTML during MDX compilation.
206
- dangerouslySetInnerHTML={{ __html: dimmedHtml }}
207
- />
15
+ <div
16
+ className={[
17
+ "honeydeck-code-block group relative mb-[0.75em] overflow-hidden rounded-honeydeck font-mono text-[length:var(--honeydeck-font-size-code)]",
18
+ className,
19
+ ]
20
+ .filter(Boolean)
21
+ .join(" ")}
22
+ >
23
+ <CodeBlockCopyButton source={source} />
24
+ {children}
208
25
  </div>
209
26
  );
210
27
  }
@@ -0,0 +1,64 @@
1
+ import { CheckIcon, CopyIcon } from "lucide-react";
2
+ import { useState } from "react";
3
+
4
+ async function writeClipboardText(text: string): Promise<boolean> {
5
+ try {
6
+ if (navigator.clipboard) {
7
+ await navigator.clipboard.writeText(text);
8
+ return true;
9
+ }
10
+ } catch {
11
+ // Fall back to the textarea path below.
12
+ }
13
+
14
+ try {
15
+ const textarea = document.createElement("textarea");
16
+ textarea.value = text;
17
+ textarea.setAttribute("readonly", "");
18
+ textarea.style.position = "fixed";
19
+ textarea.style.inset = "0";
20
+ textarea.style.opacity = "0";
21
+ document.body.appendChild(textarea);
22
+ textarea.select();
23
+ const didCopy = document.execCommand("copy");
24
+ document.body.removeChild(textarea);
25
+ return didCopy;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ type CodeBlockCopyButtonProps = {
32
+ source?: string;
33
+ };
34
+
35
+ export function CodeBlockCopyButton({ source }: CodeBlockCopyButtonProps) {
36
+ const [copied, setCopied] = useState(false);
37
+
38
+ async function copySource() {
39
+ if (!source) return;
40
+
41
+ if (await writeClipboardText(source)) {
42
+ setCopied(true);
43
+ window.setTimeout(() => setCopied(false), 1600);
44
+ }
45
+ }
46
+
47
+ if (!source) return null;
48
+
49
+ return (
50
+ <button
51
+ className="honeydeck-code-copy-button"
52
+ type="button"
53
+ aria-label={copied ? "Code copied" : "Copy code"}
54
+ title={copied ? "Copied" : "Copy code"}
55
+ onClick={copySource}
56
+ >
57
+ {copied ? (
58
+ <CheckIcon aria-hidden="true" />
59
+ ) : (
60
+ <CopyIcon aria-hidden="true" />
61
+ )}
62
+ </button>
63
+ );
64
+ }
@@ -0,0 +1,17 @@
1
+ /** A single step-through group: line numbers (1-based) or 'all'. */
2
+ export type StepGroup = number[] | "all";
3
+
4
+ export function parseJsonProp<T>(
5
+ value: string,
6
+ fallback: T,
7
+ propName?: string,
8
+ ): T {
9
+ try {
10
+ return JSON.parse(value) as T;
11
+ } catch (error) {
12
+ if (propName) {
13
+ console.warn(`Honeydeck Magic Code could not parse ${propName}.`, error);
14
+ }
15
+ return fallback;
16
+ }
17
+ }
@@ -0,0 +1,51 @@
1
+ import type { ReactNode } from "react";
2
+ import { useTimelineVisibility } from "./timelineVisibility.ts";
3
+
4
+ export type FadeProps = {
5
+ /** The step index at which this content fades out. */
6
+ at?: number;
7
+ /** Wrapper element. Injected by the compiler from MDX context. */
8
+ as?: "div" | "span";
9
+ /** Additional CSS class for custom transition overrides. */
10
+ className?: string;
11
+ /** Remove hidden content from the DOM/layout instead of reserving space. */
12
+ ephemeral?: boolean;
13
+ children?: ReactNode;
14
+ };
15
+
16
+ /**
17
+ * Timeline-driven fade component for content that starts visible and disappears
18
+ * once the slide reaches its assigned step.
19
+ */
20
+ export function Fade({
21
+ as: Component = "div",
22
+ at = 1,
23
+ className = "",
24
+ ephemeral = false,
25
+ children,
26
+ }: FadeProps) {
27
+ const { shouldRender, style } = useTimelineVisibility({
28
+ mode: "fade",
29
+ at,
30
+ ephemeral,
31
+ });
32
+
33
+ if (!shouldRender) return null;
34
+
35
+ return (
36
+ <Component
37
+ className={[
38
+ "honeydeck-fade mb-[0.75em] text-[length:var(--honeydeck-font-size-body)] leading-[1.6] [&>:last-child]:mb-0",
39
+ className,
40
+ ]
41
+ .filter(Boolean)
42
+ .join(" ")}
43
+ style={{
44
+ display: Component === "span" ? "inline" : "block",
45
+ ...style,
46
+ }}
47
+ >
48
+ {children}
49
+ </Component>
50
+ );
51
+ }
@@ -0,0 +1,175 @@
1
+ import {
2
+ Children,
3
+ type CSSProperties,
4
+ cloneElement,
5
+ isValidElement,
6
+ type Key,
7
+ type ReactElement,
8
+ type ReactNode,
9
+ } from "react";
10
+ import { useTimeline } from "../TimelineContext.tsx";
11
+ import { Fade } from "./Fade.tsx";
12
+
13
+ type ElementWithCommonProps = ReactElement<{
14
+ children?: ReactNode;
15
+ className?: string;
16
+ style?: CSSProperties;
17
+ }>;
18
+
19
+ export type FadeGroupProps = {
20
+ /** The step index for the first child. Subsequent children increment by 1. */
21
+ at?: number;
22
+ /** Internal compiler-provided absolute steps for each direct fade target. */
23
+ targetStepsJson?: string;
24
+ /** Remove hidden children from the DOM/layout instead of reserving space. */
25
+ ephemeral?: boolean;
26
+ children?: ReactNode;
27
+ };
28
+
29
+ function isMeaningfulReactChild(child: ReactNode): boolean {
30
+ return typeof child === "string" ? child.trim().length > 0 : child !== null;
31
+ }
32
+
33
+ function toMeaningfulArray(children: ReactNode): ReactNode[] {
34
+ return Children.toArray(children).filter(isMeaningfulReactChild);
35
+ }
36
+
37
+ function isListElement(child: ReactNode): child is ElementWithCommonProps {
38
+ return (
39
+ isValidElement(child) &&
40
+ typeof child.type === "string" &&
41
+ (child.type === "ul" || child.type === "ol")
42
+ );
43
+ }
44
+
45
+ function isElementWithCommonProps(
46
+ child: ReactNode,
47
+ ): child is ElementWithCommonProps {
48
+ return isValidElement(child);
49
+ }
50
+
51
+ function childKey(child: ReactNode, fallback: string): Key {
52
+ return isValidElement(child) && child.key != null ? child.key : fallback;
53
+ }
54
+
55
+ function fadeVisibility(
56
+ stepIndex: number,
57
+ at: number,
58
+ showFutureSteps: boolean,
59
+ futureStepOpacity: number,
60
+ ephemeral: boolean,
61
+ ): { shouldRender: boolean; style: CSSProperties } {
62
+ const visible = stepIndex < at;
63
+ const previewFuture = !visible && showFutureSteps;
64
+
65
+ return {
66
+ shouldRender: visible || previewFuture || !ephemeral,
67
+ style: {
68
+ visibility: visible || previewFuture ? "visible" : "hidden",
69
+ opacity: visible ? 1 : previewFuture ? futureStepOpacity : 0,
70
+ transition: "opacity 300ms ease",
71
+ },
72
+ };
73
+ }
74
+
75
+ function parseTargetSteps(targetStepsJson: string | undefined): number[] {
76
+ if (!targetStepsJson) return [];
77
+
78
+ try {
79
+ const parsed = JSON.parse(targetStepsJson) as unknown;
80
+ if (!Array.isArray(parsed)) return [];
81
+ return parsed.filter((value): value is number => typeof value === "number");
82
+ } catch {
83
+ return [];
84
+ }
85
+ }
86
+
87
+ /** Fades each meaningful direct child out as a separate timeline step. */
88
+ export function FadeGroup({
89
+ at = 1,
90
+ targetStepsJson,
91
+ ephemeral = false,
92
+ children,
93
+ }: FadeGroupProps) {
94
+ const { stepIndex, showFutureSteps, futureStepOpacity } = useTimeline();
95
+ const fadeTargets = toMeaningfulArray(children);
96
+ const targetSteps = parseTargetSteps(targetStepsJson);
97
+ let targetIndex = 0;
98
+ let nextAt = at;
99
+
100
+ function nextTargetAt(): number {
101
+ const explicitAt = targetSteps[targetIndex];
102
+ targetIndex++;
103
+
104
+ if (explicitAt !== undefined) {
105
+ nextAt = Math.max(nextAt, explicitAt + 1);
106
+ return explicitAt;
107
+ }
108
+
109
+ const fallbackAt = nextAt;
110
+ nextAt++;
111
+ return fallbackAt;
112
+ }
113
+
114
+ return (
115
+ <>
116
+ {fadeTargets.map((child, _index) => {
117
+ if (isListElement(child)) {
118
+ const listItems = toMeaningfulArray(child.props.children);
119
+ const listKey = childKey(child, `fade-list-${at}-${targetIndex}`);
120
+ const renderedListItems = listItems.map((listItem) => {
121
+ const itemAt = nextTargetAt();
122
+ const itemKey = childKey(listItem, `fade-item-${itemAt}`);
123
+
124
+ if (!isElementWithCommonProps(listItem)) {
125
+ return (
126
+ <Fade key={itemKey} at={itemAt} ephemeral={ephemeral}>
127
+ {listItem}
128
+ </Fade>
129
+ );
130
+ }
131
+
132
+ const { shouldRender, style } = fadeVisibility(
133
+ stepIndex,
134
+ itemAt,
135
+ showFutureSteps,
136
+ futureStepOpacity,
137
+ ephemeral,
138
+ );
139
+
140
+ if (!shouldRender) return null;
141
+
142
+ return cloneElement(listItem, {
143
+ key: itemKey,
144
+ style: {
145
+ ...listItem.props.style,
146
+ ...style,
147
+ },
148
+ });
149
+ });
150
+
151
+ if (ephemeral && renderedListItems.every((item) => item === null)) {
152
+ return null;
153
+ }
154
+
155
+ return cloneElement(child, {
156
+ key: listKey,
157
+ children: renderedListItems,
158
+ });
159
+ }
160
+
161
+ const childAt = nextTargetAt();
162
+
163
+ return (
164
+ <Fade
165
+ key={childKey(child, `fade-child-${childAt}`)}
166
+ at={childAt}
167
+ ephemeral={ephemeral}
168
+ >
169
+ {child}
170
+ </Fade>
171
+ );
172
+ })}
173
+ </>
174
+ );
175
+ }
@@ -0,0 +1,54 @@
1
+ import type { ReactNode } from "react";
2
+ import { useTimelineVisibility } from "./timelineVisibility.ts";
3
+
4
+ export type FadeWithProps = {
5
+ /** Slide-local reveal target name or existing slide-local timeline step. */
6
+ target?: string | number;
7
+ /** Existing slide-local timeline step to fade with. */
8
+ at?: number;
9
+ /** Wrapper element. Injected by the compiler from MDX context. */
10
+ as?: "div" | "span";
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
+ children?: ReactNode;
16
+ };
17
+
18
+ /** Fade content out at an explicit target step without creating a timeline step. */
19
+ export function FadeWith({
20
+ as: Component = "div",
21
+ at,
22
+ target,
23
+ className = "",
24
+ ephemeral = false,
25
+ children,
26
+ }: FadeWithProps) {
27
+ const fadeAt = typeof target === "number" ? target : (at ?? 1);
28
+ const { shouldRender, style } = useTimelineVisibility({
29
+ mode: "fade",
30
+ at: fadeAt,
31
+ target: fadeAt,
32
+ ephemeral,
33
+ });
34
+
35
+ if (!shouldRender) return null;
36
+
37
+ return (
38
+ <Component
39
+ className={[
40
+ "honeydeck-fade honeydeck-fade-with mb-[0.75em] text-[length:var(--honeydeck-font-size-body)] leading-[1.6] [&>:last-child]:mb-0",
41
+ className,
42
+ ]
43
+ .filter(Boolean)
44
+ .join(" ")}
45
+ style={{
46
+ display: Component === "span" ? "inline" : "block",
47
+ ...style,
48
+ }}
49
+ data-honeydeck-fade-with={typeof target === "string" ? target : undefined}
50
+ >
51
+ {children}
52
+ </Component>
53
+ );
54
+ }