@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.
Files changed (144) hide show
  1. package/AGENTS.md +25 -0
  2. package/DEVELOPMENT.md +522 -0
  3. package/LICENSE +21 -0
  4. package/Readme.md +49 -0
  5. package/SPEC.md +88 -0
  6. package/docs/components.md +63 -0
  7. package/docs/configuration.md +91 -0
  8. package/docs/getting-started.md +116 -0
  9. package/docs/kit-authoring.md +207 -0
  10. package/docs/kits.md +387 -0
  11. package/docs/local-development.md +95 -0
  12. package/docs/mermaid.md +198 -0
  13. package/docs/mobile.md +108 -0
  14. package/docs/navigation.md +93 -0
  15. package/docs/next-steps.md +377 -0
  16. package/docs/pdf-export.md +91 -0
  17. package/docs/presenter-mode.md +104 -0
  18. package/docs/slides.md +130 -0
  19. package/docs/slidev-migration.md +42 -0
  20. package/docs/steps-and-reveals.md +171 -0
  21. package/package.json +134 -0
  22. package/skills/SPEC.md +21 -0
  23. package/skills/honeydeck/SKILL.md +65 -0
  24. package/skills/presentation-writing/SKILL.md +75 -0
  25. package/skills/slidev-migration/SKILL.md +153 -0
  26. package/src/SPEC.md +89 -0
  27. package/src/assets.d.ts +30 -0
  28. package/src/cli/SPEC.md +230 -0
  29. package/src/cli/args.ts +3 -0
  30. package/src/cli/banner.ts +9 -0
  31. package/src/cli/bin.js +5 -0
  32. package/src/cli/build.ts +229 -0
  33. package/src/cli/deck-path.ts +32 -0
  34. package/src/cli/dev.ts +263 -0
  35. package/src/cli/index.ts +126 -0
  36. package/src/cli/init.ts +369 -0
  37. package/src/cli/pdf.ts +923 -0
  38. package/src/cli/skill.ts +75 -0
  39. package/src/cli/templates/SPEC.md +70 -0
  40. package/src/cli/templates/deck-mdx.ts +15 -0
  41. package/src/cli/templates/package-json.ts +36 -0
  42. package/src/cli/templates/sparkle-button.ts +15 -0
  43. package/src/cli/templates/starter/components/SparkleButton.tsx +84 -0
  44. package/src/cli/templates/starter/deck.mdx +153 -0
  45. package/src/cli/templates/starter/styles.css +14 -0
  46. package/src/cli/templates/styles-css.ts +14 -0
  47. package/src/defaults.ts +1 -0
  48. package/src/layouts/ColorModeImage.tsx +55 -0
  49. package/src/layouts/SPEC.md +393 -0
  50. package/src/layouts/SlideFrame.tsx +48 -0
  51. package/src/layouts/bee/Blank.tsx +12 -0
  52. package/src/layouts/bee/Cover.tsx +70 -0
  53. package/src/layouts/bee/Default.tsx +42 -0
  54. package/src/layouts/bee/Image/Image.tsx +151 -0
  55. package/src/layouts/bee/Image/placeholder-dark.webp +0 -0
  56. package/src/layouts/bee/Image/placeholder-vertical-dark.webp +0 -0
  57. package/src/layouts/bee/Image/placeholder-vertical.webp +0 -0
  58. package/src/layouts/bee/Image/placeholder.webp +0 -0
  59. package/src/layouts/bee/ImageLeft.tsx +27 -0
  60. package/src/layouts/bee/ImageRight.tsx +27 -0
  61. package/src/layouts/bee/ImageSide.tsx +107 -0
  62. package/src/layouts/bee/Section.tsx +40 -0
  63. package/src/layouts/bee/TwoCol.tsx +108 -0
  64. package/src/layouts/bee/index.ts +40 -0
  65. package/src/layouts/clean/Blank.tsx +12 -0
  66. package/src/layouts/clean/Cover.tsx +58 -0
  67. package/src/layouts/clean/Default.tsx +33 -0
  68. package/src/layouts/clean/Image/Image.tsx +103 -0
  69. package/src/layouts/clean/ImageLeft.tsx +27 -0
  70. package/src/layouts/clean/ImageRight.tsx +27 -0
  71. package/src/layouts/clean/ImageSide.tsx +113 -0
  72. package/src/layouts/clean/Section.tsx +35 -0
  73. package/src/layouts/clean/TwoCol.tsx +63 -0
  74. package/src/layouts/clean/index.ts +40 -0
  75. package/src/layouts/index.ts +60 -0
  76. package/src/layouts/placeholders.ts +9 -0
  77. package/src/layouts/utils.ts +13 -0
  78. package/src/remark/SPEC.md +49 -0
  79. package/src/remark/h1-extract.ts +124 -0
  80. package/src/remark/index.ts +4 -0
  81. package/src/remark/shiki-code-blocks.ts +325 -0
  82. package/src/remark/step-numbering.ts +412 -0
  83. package/src/runtime/Deck.tsx +533 -0
  84. package/src/runtime/SPEC.md +256 -0
  85. package/src/runtime/SlideCanvas.tsx +95 -0
  86. package/src/runtime/TimelineContext.tsx +122 -0
  87. package/src/runtime/app-shell/index.html +31 -0
  88. package/src/runtime/app-shell/main.tsx +42 -0
  89. package/src/runtime/aspectRatio.ts +34 -0
  90. package/src/runtime/colorMode.ts +23 -0
  91. package/src/runtime/components/BrowserFrame.tsx +233 -0
  92. package/src/runtime/components/Button.tsx +57 -0
  93. package/src/runtime/components/CodeBlock.tsx +210 -0
  94. package/src/runtime/components/ColorModeCycleButton.tsx +59 -0
  95. package/src/runtime/components/ErrorBoundary.tsx +125 -0
  96. package/src/runtime/components/Keyboard.tsx +87 -0
  97. package/src/runtime/components/ListStyle.tsx +203 -0
  98. package/src/runtime/components/NavBar.tsx +223 -0
  99. package/src/runtime/components/NavBarButton.tsx +47 -0
  100. package/src/runtime/components/NavBarDivider.tsx +3 -0
  101. package/src/runtime/components/Notes.tsx +171 -0
  102. package/src/runtime/components/Reveal.tsx +82 -0
  103. package/src/runtime/components/RevealGroup.tsx +193 -0
  104. package/src/runtime/components/SPEC.md +263 -0
  105. package/src/runtime/components/SlideNumberBadge.tsx +11 -0
  106. package/src/runtime/components/TimelineSteps.tsx +115 -0
  107. package/src/runtime/components/index.ts +55 -0
  108. package/src/runtime/index.ts +42 -0
  109. package/src/runtime/inputOwnership.ts +68 -0
  110. package/src/runtime/keyboardTarget.ts +7 -0
  111. package/src/runtime/lastSlideRoute.ts +56 -0
  112. package/src/runtime/navigation.ts +211 -0
  113. package/src/runtime/router.ts +157 -0
  114. package/src/runtime/slideData.ts +137 -0
  115. package/src/runtime/sync.ts +267 -0
  116. package/src/runtime/types.ts +182 -0
  117. package/src/runtime/useKeyboardNav.ts +138 -0
  118. package/src/runtime/useSwipeNav.ts +257 -0
  119. package/src/runtime/views/DocsView.tsx +74 -0
  120. package/src/runtime/views/OverviewView.tsx +386 -0
  121. package/src/runtime/views/PresenterNotesPanel.tsx +76 -0
  122. package/src/runtime/views/PresenterView.tsx +340 -0
  123. package/src/runtime/views/SPEC.md +152 -0
  124. package/src/runtime/views/docs/ComponentsTab.tsx +178 -0
  125. package/src/runtime/views/docs/DocsHeader.tsx +101 -0
  126. package/src/runtime/views/docs/Intro.tsx +20 -0
  127. package/src/runtime/views/docs/LayoutsTab.tsx +324 -0
  128. package/src/runtime/views/docs/ThemeTab.tsx +110 -0
  129. package/src/runtime/views/index.ts +7 -0
  130. package/src/runtime/views/overviewGrid.ts +106 -0
  131. package/src/runtime/views/presenterPreview.ts +27 -0
  132. package/src/runtime/virtual-modules.d.ts +98 -0
  133. package/src/theme/SPEC.md +179 -0
  134. package/src/theme/base.css +623 -0
  135. package/src/theme/bee.css +35 -0
  136. package/src/theme/clean.css +38 -0
  137. package/src/vite-plugin/SPEC.md +114 -0
  138. package/src/vite-plugin/component-doc-crawler.ts +350 -0
  139. package/src/vite-plugin/deck-loader.ts +148 -0
  140. package/src/vite-plugin/index.ts +373 -0
  141. package/src/vite-plugin/layout-demo-crawler.ts +802 -0
  142. package/src/vite-plugin/splitter.ts +353 -0
  143. package/src/vite-plugin/token-manifest.ts +163 -0
  144. 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
+ }