@honeydeck/honeydeck 0.1.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.
- package/AGENTS.md +25 -0
- package/DEVELOPMENT.md +522 -0
- package/LICENSE +21 -0
- package/Readme.md +49 -0
- package/SPEC.md +88 -0
- package/docs/components.md +63 -0
- package/docs/configuration.md +91 -0
- package/docs/getting-started.md +116 -0
- package/docs/kit-authoring.md +207 -0
- package/docs/kits.md +387 -0
- package/docs/local-development.md +95 -0
- package/docs/mermaid.md +198 -0
- package/docs/mobile.md +108 -0
- package/docs/navigation.md +93 -0
- package/docs/next-steps.md +377 -0
- package/docs/pdf-export.md +91 -0
- package/docs/presenter-mode.md +104 -0
- package/docs/slides.md +130 -0
- package/docs/slidev-migration.md +42 -0
- package/docs/steps-and-reveals.md +171 -0
- package/package.json +134 -0
- package/skills/SPEC.md +21 -0
- package/skills/honeydeck/SKILL.md +65 -0
- package/skills/presentation-writing/SKILL.md +75 -0
- package/skills/slidev-migration/SKILL.md +153 -0
- package/src/SPEC.md +89 -0
- package/src/assets.d.ts +30 -0
- package/src/cli/SPEC.md +230 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/banner.ts +9 -0
- package/src/cli/bin.js +5 -0
- package/src/cli/build.ts +229 -0
- package/src/cli/deck-path.ts +32 -0
- package/src/cli/dev.ts +263 -0
- package/src/cli/index.ts +126 -0
- package/src/cli/init.ts +369 -0
- package/src/cli/pdf.ts +923 -0
- package/src/cli/skill.ts +75 -0
- package/src/cli/templates/SPEC.md +70 -0
- package/src/cli/templates/deck-mdx.ts +15 -0
- package/src/cli/templates/package-json.ts +36 -0
- package/src/cli/templates/sparkle-button.ts +15 -0
- package/src/cli/templates/starter/components/SparkleButton.tsx +84 -0
- package/src/cli/templates/starter/deck.mdx +153 -0
- package/src/cli/templates/starter/styles.css +14 -0
- package/src/cli/templates/styles-css.ts +14 -0
- package/src/defaults.ts +1 -0
- package/src/layouts/ColorModeImage.tsx +55 -0
- package/src/layouts/SPEC.md +393 -0
- package/src/layouts/SlideFrame.tsx +48 -0
- package/src/layouts/bee/Blank.tsx +12 -0
- package/src/layouts/bee/Cover.tsx +70 -0
- package/src/layouts/bee/Default.tsx +42 -0
- package/src/layouts/bee/Image/Image.tsx +151 -0
- package/src/layouts/bee/Image/placeholder-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical.webp +0 -0
- package/src/layouts/bee/Image/placeholder.webp +0 -0
- package/src/layouts/bee/ImageLeft.tsx +27 -0
- package/src/layouts/bee/ImageRight.tsx +27 -0
- package/src/layouts/bee/ImageSide.tsx +107 -0
- package/src/layouts/bee/Section.tsx +40 -0
- package/src/layouts/bee/TwoCol.tsx +108 -0
- package/src/layouts/bee/index.ts +40 -0
- package/src/layouts/clean/Blank.tsx +12 -0
- package/src/layouts/clean/Cover.tsx +58 -0
- package/src/layouts/clean/Default.tsx +33 -0
- package/src/layouts/clean/Image/Image.tsx +103 -0
- package/src/layouts/clean/ImageLeft.tsx +27 -0
- package/src/layouts/clean/ImageRight.tsx +27 -0
- package/src/layouts/clean/ImageSide.tsx +113 -0
- package/src/layouts/clean/Section.tsx +35 -0
- package/src/layouts/clean/TwoCol.tsx +63 -0
- package/src/layouts/clean/index.ts +40 -0
- package/src/layouts/index.ts +60 -0
- package/src/layouts/placeholders.ts +9 -0
- package/src/layouts/utils.ts +13 -0
- package/src/remark/SPEC.md +49 -0
- package/src/remark/h1-extract.ts +124 -0
- package/src/remark/index.ts +4 -0
- package/src/remark/shiki-code-blocks.ts +325 -0
- package/src/remark/step-numbering.ts +412 -0
- package/src/runtime/Deck.tsx +533 -0
- package/src/runtime/SPEC.md +256 -0
- package/src/runtime/SlideCanvas.tsx +95 -0
- package/src/runtime/TimelineContext.tsx +122 -0
- package/src/runtime/app-shell/index.html +31 -0
- package/src/runtime/app-shell/main.tsx +42 -0
- package/src/runtime/aspectRatio.ts +34 -0
- package/src/runtime/colorMode.ts +23 -0
- package/src/runtime/components/BrowserFrame.tsx +233 -0
- package/src/runtime/components/Button.tsx +57 -0
- package/src/runtime/components/CodeBlock.tsx +210 -0
- package/src/runtime/components/ColorModeCycleButton.tsx +59 -0
- package/src/runtime/components/ErrorBoundary.tsx +125 -0
- package/src/runtime/components/Keyboard.tsx +87 -0
- package/src/runtime/components/ListStyle.tsx +203 -0
- package/src/runtime/components/NavBar.tsx +223 -0
- package/src/runtime/components/NavBarButton.tsx +47 -0
- package/src/runtime/components/NavBarDivider.tsx +3 -0
- package/src/runtime/components/Notes.tsx +171 -0
- package/src/runtime/components/Reveal.tsx +82 -0
- package/src/runtime/components/RevealGroup.tsx +193 -0
- package/src/runtime/components/SPEC.md +263 -0
- package/src/runtime/components/SlideNumberBadge.tsx +11 -0
- package/src/runtime/components/TimelineSteps.tsx +115 -0
- package/src/runtime/components/index.ts +55 -0
- package/src/runtime/index.ts +42 -0
- package/src/runtime/inputOwnership.ts +68 -0
- package/src/runtime/keyboardTarget.ts +7 -0
- package/src/runtime/lastSlideRoute.ts +56 -0
- package/src/runtime/navigation.ts +211 -0
- package/src/runtime/router.ts +157 -0
- package/src/runtime/slideData.ts +137 -0
- package/src/runtime/sync.ts +267 -0
- package/src/runtime/types.ts +182 -0
- package/src/runtime/useKeyboardNav.ts +138 -0
- package/src/runtime/useSwipeNav.ts +257 -0
- package/src/runtime/views/DocsView.tsx +74 -0
- package/src/runtime/views/OverviewView.tsx +386 -0
- package/src/runtime/views/PresenterNotesPanel.tsx +76 -0
- package/src/runtime/views/PresenterView.tsx +340 -0
- package/src/runtime/views/SPEC.md +152 -0
- package/src/runtime/views/docs/ComponentsTab.tsx +178 -0
- package/src/runtime/views/docs/DocsHeader.tsx +101 -0
- package/src/runtime/views/docs/Intro.tsx +20 -0
- package/src/runtime/views/docs/LayoutsTab.tsx +324 -0
- package/src/runtime/views/docs/ThemeTab.tsx +110 -0
- package/src/runtime/views/index.ts +7 -0
- package/src/runtime/views/overviewGrid.ts +106 -0
- package/src/runtime/views/presenterPreview.ts +27 -0
- package/src/runtime/virtual-modules.d.ts +98 -0
- package/src/theme/SPEC.md +179 -0
- package/src/theme/base.css +623 -0
- package/src/theme/bee.css +35 -0
- package/src/theme/clean.css +38 -0
- package/src/vite-plugin/SPEC.md +114 -0
- package/src/vite-plugin/component-doc-crawler.ts +350 -0
- package/src/vite-plugin/deck-loader.ts +148 -0
- package/src/vite-plugin/index.ts +373 -0
- package/src/vite-plugin/layout-demo-crawler.ts +802 -0
- package/src/vite-plugin/splitter.ts +353 -0
- package/src/vite-plugin/token-manifest.ts +163 -0
- package/src/vite-plugin/virtual-modules.ts +587 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notes component + context for Honeydeck presenter mode.
|
|
3
|
+
*
|
|
4
|
+
* `<Notes>` always renders **nothing** in the DOM — it is purely a vehicle
|
|
5
|
+
* for surfacing speaker notes to the PresenterView.
|
|
6
|
+
*
|
|
7
|
+
* ### How it works
|
|
8
|
+
*
|
|
9
|
+
* 1. `NotesContext` provides a `setNotes` callback.
|
|
10
|
+
* 2. `<PresenterView>` wraps the current slide preview in
|
|
11
|
+
* `<NotesContext.Provider value={{ setNotes }}>`.
|
|
12
|
+
* 3. When a slide containing `<Notes>` renders inside that tree, the Notes
|
|
13
|
+
* component fires `useEffect` → calls `setNotes(children)`.
|
|
14
|
+
* 4. PresenterView reads the collected notes from its own state.
|
|
15
|
+
*
|
|
16
|
+
* In audience view (no NotesContext), `<Notes>` simply renders null and the
|
|
17
|
+
* effect is a no-op.
|
|
18
|
+
*
|
|
19
|
+
* ### Authoring
|
|
20
|
+
* ```mdx
|
|
21
|
+
* import { Notes } from '@honeydeck/honeydeck'
|
|
22
|
+
*
|
|
23
|
+
* # My Slide
|
|
24
|
+
*
|
|
25
|
+
* <Notes>
|
|
26
|
+
* Remember to demo the sparkle button here!
|
|
27
|
+
* </Notes>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
createContext,
|
|
33
|
+
isValidElement,
|
|
34
|
+
type ReactNode,
|
|
35
|
+
useContext,
|
|
36
|
+
useEffect,
|
|
37
|
+
useRef,
|
|
38
|
+
} from "react";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Context
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export type NotesContextValue = {
|
|
45
|
+
/** Called by `<Notes>` to push its content into the presenter view. */
|
|
46
|
+
setNotes: (content: ReactNode) => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Provided by PresenterView around the current slide preview.
|
|
51
|
+
* `null` when rendering in audience view — Notes is a no-op in that case.
|
|
52
|
+
*/
|
|
53
|
+
export const NotesContext = createContext<NotesContextValue | null>(null);
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Component
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export type NotesProps = {
|
|
60
|
+
children?: ReactNode;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function getNotesSignature(node: ReactNode): string {
|
|
64
|
+
if (node == null || typeof node === "boolean") return "empty";
|
|
65
|
+
if (
|
|
66
|
+
typeof node === "string" ||
|
|
67
|
+
typeof node === "number" ||
|
|
68
|
+
typeof node === "bigint"
|
|
69
|
+
) {
|
|
70
|
+
return `${typeof node}:${String(node)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(node)) {
|
|
74
|
+
return `array:[${node.map((child) => getNotesSignature(child)).join(",")}]`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isValidElement(node)) {
|
|
78
|
+
const props = node.props as Record<string, unknown> & {
|
|
79
|
+
children?: ReactNode;
|
|
80
|
+
};
|
|
81
|
+
const propSignature = Object.entries(props)
|
|
82
|
+
.filter(([key]) => key !== "children")
|
|
83
|
+
.filter(([, value]) => value == null || isPrimitive(value))
|
|
84
|
+
.map(([key, value]) => `${key}:${String(value)}`)
|
|
85
|
+
.sort()
|
|
86
|
+
.join(",");
|
|
87
|
+
|
|
88
|
+
return [
|
|
89
|
+
"element",
|
|
90
|
+
getElementTypeSignature(node.type),
|
|
91
|
+
node.key ?? "",
|
|
92
|
+
propSignature,
|
|
93
|
+
getNotesSignature(props.children),
|
|
94
|
+
].join(":");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isIterableReactNode(node)) {
|
|
98
|
+
return `iterable:[${Array.from(node)
|
|
99
|
+
.map((child) => getNotesSignature(child))
|
|
100
|
+
.join(",")}]`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return typeof node;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Speaker notes — renders nothing in audience view.
|
|
108
|
+
* Content is collected via `NotesContext` for display in PresenterView.
|
|
109
|
+
*
|
|
110
|
+
* Notes render nothing in audience view, overview thumbnails, and normal PDF
|
|
111
|
+
* output. Markdown inside `<Notes>` is rendered as formatted speaker notes in
|
|
112
|
+
* presenter mode, so use notes for delivery cues, demo reminders, and
|
|
113
|
+
* presenter-only context.
|
|
114
|
+
*
|
|
115
|
+
* ```mdx
|
|
116
|
+
* import { Notes } from '@honeydeck/honeydeck'
|
|
117
|
+
*
|
|
118
|
+
* <Notes>
|
|
119
|
+
* # Demo cue
|
|
120
|
+
*
|
|
121
|
+
* - Demo the interactive component.
|
|
122
|
+
* - Mention PDF export.
|
|
123
|
+
* </Notes>
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function Notes({ children }: NotesProps) {
|
|
127
|
+
const ctx = useContext(NotesContext);
|
|
128
|
+
const setNotes = ctx?.setNotes;
|
|
129
|
+
const previousSignatureRef = useRef<string | null>(null);
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!setNotes) return;
|
|
133
|
+
const signature = getNotesSignature(children);
|
|
134
|
+
if (previousSignatureRef.current === signature) return;
|
|
135
|
+
previousSignatureRef.current = signature;
|
|
136
|
+
setNotes(children ?? null);
|
|
137
|
+
}, [children, setNotes]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
return () => {
|
|
141
|
+
previousSignatureRef.current = null;
|
|
142
|
+
setNotes?.(null);
|
|
143
|
+
};
|
|
144
|
+
}, [setNotes]);
|
|
145
|
+
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isPrimitive(value: unknown): boolean {
|
|
150
|
+
return (
|
|
151
|
+
value == null ||
|
|
152
|
+
typeof value === "string" ||
|
|
153
|
+
typeof value === "number" ||
|
|
154
|
+
typeof value === "boolean" ||
|
|
155
|
+
typeof value === "bigint"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getElementTypeSignature(type: unknown): string {
|
|
160
|
+
if (typeof type === "string") return type;
|
|
161
|
+
if (typeof type === "function") {
|
|
162
|
+
const component = type as { displayName?: string; name?: string };
|
|
163
|
+
return component.displayName ?? component.name ?? "fn";
|
|
164
|
+
}
|
|
165
|
+
if (typeof type === "symbol") return type.description ?? String(type);
|
|
166
|
+
return String(type);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isIterableReactNode(node: ReactNode): node is Iterable<ReactNode> {
|
|
170
|
+
return typeof node === "object" && node != null && Symbol.iterator in node;
|
|
171
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { useTimeline } from "../TimelineContext.tsx";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Types
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
export type RevealProps = {
|
|
9
|
+
/**
|
|
10
|
+
* The step index at which this content becomes visible.
|
|
11
|
+
* Injected by the remark step-numbering plugin; defaults to 1.
|
|
12
|
+
*/
|
|
13
|
+
at?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Wrapper element. Injected by the compiler from MDX context:
|
|
16
|
+
* flow/block reveals use `div`, text/inline reveals use `span`.
|
|
17
|
+
*/
|
|
18
|
+
as?: "div" | "span";
|
|
19
|
+
/** Additional CSS class for custom transition overrides. */
|
|
20
|
+
className?: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Component
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Timeline-driven reveal component for progressive slide content.
|
|
30
|
+
*
|
|
31
|
+
* Content appears when the slide's current step reaches `at`. Before that it
|
|
32
|
+
* is invisible while still occupying layout space, so reveals do not cause
|
|
33
|
+
* nearby content to jump around.
|
|
34
|
+
*
|
|
35
|
+
* Reveals are cumulative: once visible, they stay visible as the presenter
|
|
36
|
+
* advances. The default transition is a simple opacity fade.
|
|
37
|
+
*
|
|
38
|
+
* ```mdx
|
|
39
|
+
* import { Reveal } from '@honeydeck/honeydeck'
|
|
40
|
+
*
|
|
41
|
+
* Visible from the start.
|
|
42
|
+
*
|
|
43
|
+
* <Reveal>This appears at step 1.</Reveal>
|
|
44
|
+
*
|
|
45
|
+
* <Reveal>This appears at step 2.</Reveal>
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* Honeydeck normally injects `at` during MDX compilation. It also injects `as`
|
|
49
|
+
* from the MDX context so block reveals render as `div` and inline reveals
|
|
50
|
+
* render as `span`.
|
|
51
|
+
*/
|
|
52
|
+
export function Reveal({
|
|
53
|
+
as: Component = "div",
|
|
54
|
+
at = 1,
|
|
55
|
+
className = "",
|
|
56
|
+
children,
|
|
57
|
+
}: RevealProps) {
|
|
58
|
+
const { stepIndex, showFutureSteps, futureStepOpacity } = useTimeline();
|
|
59
|
+
const visible = stepIndex >= at;
|
|
60
|
+
const previewFuture = !visible && showFutureSteps;
|
|
61
|
+
|
|
62
|
+
const style: CSSProperties = {
|
|
63
|
+
display: Component === "span" ? "inline" : "block",
|
|
64
|
+
visibility: visible || previewFuture ? "visible" : "hidden",
|
|
65
|
+
opacity: visible ? 1 : previewFuture ? futureStepOpacity : 0,
|
|
66
|
+
transition: "opacity 300ms ease",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Component
|
|
71
|
+
className={[
|
|
72
|
+
"honeydeck-reveal mb-[0.75em] text-[length:var(--honeydeck-font-size-body)] leading-[1.6] [&>:last-child]:mb-0",
|
|
73
|
+
className,
|
|
74
|
+
]
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.join(" ")}
|
|
77
|
+
style={style}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</Component>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
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 { Reveal } from "./Reveal.tsx";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export type RevealGroupProps = {
|
|
18
|
+
/**
|
|
19
|
+
* The step index for the first child. Subsequent children increment by 1.
|
|
20
|
+
* Injected by the remark step-numbering plugin; defaults to 1.
|
|
21
|
+
*/
|
|
22
|
+
at?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Internal compiler-provided absolute steps for each direct reveal target.
|
|
25
|
+
* This lets nested timeline entries create gaps before later group targets.
|
|
26
|
+
*/
|
|
27
|
+
targetStepsJson?: string;
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
type ElementWithCommonProps = ReactElement<{
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
className?: string;
|
|
38
|
+
style?: CSSProperties;
|
|
39
|
+
}>;
|
|
40
|
+
|
|
41
|
+
function isMeaningfulReactChild(child: ReactNode): boolean {
|
|
42
|
+
return typeof child === "string" ? child.trim().length > 0 : child !== null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toMeaningfulArray(children: ReactNode): ReactNode[] {
|
|
46
|
+
return Children.toArray(children).filter(isMeaningfulReactChild);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isListElement(child: ReactNode): child is ElementWithCommonProps {
|
|
50
|
+
return (
|
|
51
|
+
isValidElement(child) &&
|
|
52
|
+
typeof child.type === "string" &&
|
|
53
|
+
(child.type === "ul" || child.type === "ol")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isElementWithCommonProps(
|
|
58
|
+
child: ReactNode,
|
|
59
|
+
): child is ElementWithCommonProps {
|
|
60
|
+
return isValidElement(child);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function childKey(child: ReactNode, fallback: string): Key {
|
|
64
|
+
return isValidElement(child) && child.key != null ? child.key : fallback;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function revealStyle(
|
|
68
|
+
stepIndex: number,
|
|
69
|
+
at: number,
|
|
70
|
+
showFutureSteps: boolean,
|
|
71
|
+
futureStepOpacity: number,
|
|
72
|
+
): CSSProperties {
|
|
73
|
+
const visible = stepIndex >= at;
|
|
74
|
+
const previewFuture = !visible && showFutureSteps;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
visibility: visible || previewFuture ? "visible" : "hidden",
|
|
78
|
+
opacity: visible ? 1 : previewFuture ? futureStepOpacity : 0,
|
|
79
|
+
transition: "opacity 300ms ease",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseTargetSteps(targetStepsJson: string | undefined): number[] {
|
|
84
|
+
if (!targetStepsJson) return [];
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(targetStepsJson) as unknown;
|
|
88
|
+
if (!Array.isArray(parsed)) return [];
|
|
89
|
+
return parsed.filter((value): value is number => typeof value === "number");
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Component
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reveals each meaningful direct child as a separate timeline step.
|
|
101
|
+
*
|
|
102
|
+
* Use `RevealGroup` when a short sequence should appear one item at a time.
|
|
103
|
+
* Direct Markdown lists are preserved as lists, while each list item gets its
|
|
104
|
+
* own reveal step.
|
|
105
|
+
*
|
|
106
|
+
* ```mdx
|
|
107
|
+
* import { RevealGroup } from '@honeydeck/honeydeck'
|
|
108
|
+
*
|
|
109
|
+
* <RevealGroup>
|
|
110
|
+
* - First point
|
|
111
|
+
* - Second point
|
|
112
|
+
* - Third point
|
|
113
|
+
* </RevealGroup>
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* 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.
|
|
120
|
+
*/
|
|
121
|
+
export function RevealGroup({
|
|
122
|
+
at = 1,
|
|
123
|
+
targetStepsJson,
|
|
124
|
+
children,
|
|
125
|
+
}: RevealGroupProps) {
|
|
126
|
+
const { stepIndex, showFutureSteps, futureStepOpacity } = useTimeline();
|
|
127
|
+
const revealTargets = toMeaningfulArray(children);
|
|
128
|
+
const targetSteps = parseTargetSteps(targetStepsJson);
|
|
129
|
+
let targetIndex = 0;
|
|
130
|
+
let nextAt = at;
|
|
131
|
+
|
|
132
|
+
function nextTargetAt(): number {
|
|
133
|
+
const explicitAt = targetSteps[targetIndex];
|
|
134
|
+
targetIndex++;
|
|
135
|
+
|
|
136
|
+
if (explicitAt !== undefined) {
|
|
137
|
+
nextAt = Math.max(nextAt, explicitAt + 1);
|
|
138
|
+
return explicitAt;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fallbackAt = nextAt;
|
|
142
|
+
nextAt++;
|
|
143
|
+
return fallbackAt;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
{revealTargets.map((child, _index) => {
|
|
149
|
+
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
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const childAt = nextTargetAt();
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<Reveal key={childKey(child, `reveal-child-${childAt}`)} at={childAt}>
|
|
187
|
+
{child}
|
|
188
|
+
</Reveal>
|
|
189
|
+
);
|
|
190
|
+
})}
|
|
191
|
+
</>
|
|
192
|
+
);
|
|
193
|
+
}
|