@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,263 @@
1
+ # Honeydeck Core Components Specification
2
+
3
+ > Observable behavior for public built-in React components.
4
+
5
+ ## Core Components
6
+
7
+ All core components are explicit imports from the `'@honeydeck/honeydeck'` package. They are also exported from `@honeydeck/honeydeck/components`:
8
+
9
+ ```mdx
10
+ import { Reveal, RevealGroup, TimelineSteps, ListStyle, Keyboard, BrowserFrame, Notes } from '@honeydeck/honeydeck'
11
+ ```
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.
14
+
15
+ ### Color Mode Controls
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.
18
+
19
+ ```tsx
20
+ import { ColorModeCycleButton, type ColorMode } from '@honeydeck/honeydeck/components'
21
+ ```
22
+
23
+ Behavior:
24
+
25
+ - `ColorMode` is `"system" | "light" | "dark"`.
26
+ - `getNextColorMode(mode)` cycles `system` → `light` → `dark` → `system`.
27
+ - `<ColorModeCycleButton>` renders the matching Lucide icon for the configured mode: monitor for `system`, sun for `light`, moon for `dark`.
28
+ - Clicking the button calls `onSetColorMode` with the next configured mode.
29
+ - The button accepts `className`, `iconSize`, `title`, and `ariaLabel` so slide chrome, reference pages, and other Honeydeck surfaces can share behavior while styling the control locally.
30
+
31
+ ### Button Controls
32
+
33
+ `@honeydeck/honeydeck/components` exports generic Honeydeck-token-based button primitives and class recipes for runtime chrome, runtime reference pages, and marketing controls.
34
+
35
+ ```tsx
36
+ import { Button, buttonPrimaryClass, iconButtonClass } from '@honeydeck/honeydeck/components'
37
+ ```
38
+
39
+ Behavior:
40
+
41
+ - `<Button>` renders a native `button` with `type="button"` by default.
42
+ - `variant` selects one of `primary`, `secondary`, `icon`, `small`, or `quiet`.
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.
45
+
46
+ ### Runtime chrome buttons
47
+
48
+ Icon-only runtime chrome buttons, including the floating navigation bar actions, expose an explicit accessible name with `aria-label` and keep matching hover titles for the same action text.
49
+
50
+ ### `<Reveal>`
51
+
52
+ Reveals content at the next timeline step.
53
+
54
+ ```mdx
55
+ <Reveal>This appears at step 1</Reveal>
56
+ <Reveal>This appears at step 2</Reveal>
57
+ ```
58
+
59
+ Behavior:
60
+
61
+ - Hidden content **reserves layout space** (`visibility: hidden` + `opacity: 0`, not `display: none`)
62
+ - Runtime wrapper matches MDX context: flow/block reveals render a block-level `div`, text/inline reveals render an inline `span`
63
+ - Nested reveals are supported; inline nested reveals inside paragraphs must not create invalid `div`-inside-`p` HTML
64
+ - Default effect: fade in
65
+ - Reveals are **cumulative** (once visible, stays visible)
66
+ - 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
+ - No `effect` prop
70
+
71
+ ### `<RevealGroup>`
72
+
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.
74
+
75
+ ```mdx
76
+ <RevealGroup>
77
+ - First point
78
+ - Second point
79
+ - Third point
80
+ </RevealGroup>
81
+ ```
82
+
83
+ Each list item becomes its own timeline step.
84
+
85
+ Nested timeline entries inside a group target are flattened after that target
86
+ and before the following group target:
87
+
88
+ ```mdx
89
+ <RevealGroup>
90
+ <div>
91
+ Parent item
92
+ <Reveal>Nested detail</Reveal>
93
+ </div>
94
+ <div>Sibling item</div>
95
+ </RevealGroup>
96
+ ```
97
+
98
+ Timeline:
99
+
100
+ 1. Parent item appears
101
+ 2. Nested detail appears
102
+ 3. Sibling item appears
103
+
104
+ ### `<ListStyle>`
105
+
106
+ 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.
107
+
108
+ ```mdx
109
+ import { ListStyle } from '@honeydeck/honeydeck'
110
+ import { CheckIcon, CircleIcon } from 'lucide-react'
111
+
112
+ <ListStyle>
113
+ - No marker
114
+ - Still aligned
115
+ </ListStyle>
116
+
117
+ <ListStyle bullets={[<CheckIcon />, <CircleIcon />]}>
118
+ - Level one uses a check icon
119
+ - Level two uses a circle icon
120
+ </ListStyle>
121
+
122
+ <ListStyle bullets={["→", "–", "·"]}>
123
+ - Level one
124
+ - Level two
125
+ - Level three
126
+ </ListStyle>
127
+ ```
128
+
129
+ Behavior:
130
+
131
+ - Native list markers are removed for all nested lists in the wrapper.
132
+ - `bullets` accepts a single React node/string marker or an array of markers by nesting level.
133
+ - `bullets={false}`, `bullets="none"`, `bullets={null}`, or omitting `bullets` renders markerless lists.
134
+ - Custom marker injection applies to authored list elements passed as children; native markers remain hidden for any deeper rendered lists because styling is scoped to the wrapper.
135
+
136
+ ### `<Keyboard>`
137
+
138
+ Displays one keyboard key or a keyboard shortcut using semantic `<kbd>` markup.
139
+
140
+ ```mdx
141
+ import { Keyboard } from '@honeydeck/honeydeck'
142
+
143
+ Press <Keyboard>Esc</Keyboard> to close overview.
144
+
145
+ Open command palette with <Keyboard keys={["Ctrl", "Shift", "P"]} />.
146
+
147
+ Advance with <Keyboard keys="Space" />.
148
+ ```
149
+
150
+ Props:
151
+
152
+ - `keys?: ReactNode | ReactNode[]` — key label or ordered shortcut key labels.
153
+ - `children?: ReactNode` — single key label when `keys` is omitted.
154
+ - `separator?: ReactNode` — separator rendered between array entries; defaults to `+`.
155
+ - `className?: string` — applied to the outer wrapper.
156
+
157
+ Behavior:
158
+
159
+ - A single `children` value or single `keys` value renders one `<kbd>`.
160
+ - An array `keys` value renders one `<kbd>` per item, in order, separated by `separator`.
161
+ - The component is inline by default so it works inside prose.
162
+ - It uses Honeydeck default styling and can be customized with `className`.
163
+ - It does not participate in the Honeydeck timeline.
164
+
165
+ ### `<BrowserFrame>`
166
+
167
+ Displays an iframe inside a macOS-style browser window frame.
168
+
169
+ ```mdx
170
+ import { BrowserFrame } from '@honeydeck/honeydeck'
171
+
172
+ <BrowserFrame
173
+ src="https://example.com"
174
+ addressBar="example.com"
175
+ fallbackImage="/example-fallback-light.png"
176
+ fallbackDarkImage="/example-fallback-dark.png"
177
+ />
178
+ ```
179
+
180
+ Props:
181
+
182
+ - `src: string` — iframe URL.
183
+ - `addressBar?: ReactNode` — optional content shown in the address-bar field. When omitted, no address-bar field is rendered.
184
+ - `fallbackImage?: string` — light/default screenshot shown when the iframe cannot be loaded or fallback mode is toggled on.
185
+ - `fallbackDarkImage?: string` — dark-mode screenshot shown in dark color mode; falls back to `fallbackImage` when omitted.
186
+ - `fallbackAlt?: string` — accessible alt text for fallback images; defaults to `Fallback preview`.
187
+ - `defaultFallback?: boolean` — initially render the fallback image instead of the iframe, useful for demos and final-state screenshots.
188
+ - `aspectRatio?: CSSProperties["aspectRatio"]` — aspect ratio for the full browser window; defaults to `16 / 9`.
189
+ - `className?: string` — applied to the outer wrapper.
190
+ - `iframeClassName?: string` — applied to the iframe.
191
+ - Standard iframe attributes such as `allow`, `sandbox`, `loading`, and `referrerPolicy` are forwarded.
192
+
193
+ Behavior:
194
+
195
+ - Renders a single `<iframe>` with a surrounding browser chrome.
196
+ - The frame stretches to the largest size that fits its available parent space while preserving the configured aspect ratio, using CSS sizing (Tailwind utilities and container query units) rather than JavaScript measurement.
197
+ - The chrome uses macOS traffic-light controls and an optional address-bar field.
198
+ - When a fallback image is configured, iframe load errors switch the frame to fallback mode.
199
+ - The fallback uses `fallbackDarkImage` in dark mode and `fallbackImage` otherwise.
200
+ - While fallback mode is active, the top browser chrome shows a badge aligned with the address-bar field so presenters can see that the iframe is not live.
201
+ - When a fallback image is configured, a fourth round control sits next to the macOS traffic-light controls. It is visible only when the control itself is hovered or keyboard-focused, and it toggles fallback mode on and off.
202
+ - Styling uses Honeydeck theme tokens for surface, foreground, border, radius, font, and shadow colors.
203
+ - The component does not participate in the Honeydeck timeline.
204
+
205
+ ### `<TimelineSteps>`
206
+
207
+ Reserves a static block of timeline steps for an imported custom component.
208
+ The custom component reads its local state with `useTimelineSteps()`.
209
+
210
+ ```mdx
211
+ import { Reveal, TimelineSteps } from '@honeydeck/honeydeck'
212
+ import { AccordionDemo } from './AccordionDemo'
213
+
214
+ <Reveal>Intro appears first</Reveal>
215
+
216
+ <TimelineSteps steps={3}>
217
+ <AccordionDemo />
218
+ </TimelineSteps>
219
+
220
+ <Reveal>Outro appears after the accordion</Reveal>
221
+ ```
222
+
223
+ Inside `AccordionDemo`:
224
+
225
+ ```tsx
226
+ import { useTimelineSteps } from '@honeydeck/honeydeck'
227
+
228
+ export function AccordionDemo() {
229
+ const { phase, stepIndex, stepCount, isPdfFinalRender } = useTimelineSteps()
230
+ // phase: "before" | "active" | "after"
231
+ // stepIndex: 0 before start, 1..stepCount while active, stepCount after end
232
+ // isPdfFinalRender: true only for one-page final-state PDF export
233
+ }
234
+ ```
235
+
236
+ Behavior:
237
+
238
+ - `steps` must be a literal positive integer in slide MDX, for example
239
+ `steps={3}`. Dynamic values are not supported because the timeline is counted
240
+ at build time.
241
+ - `<TimelineSteps>` must appear at the usage site in slide MDX. Imported TSX
242
+ components cannot register steps by rendering `<TimelineSteps>` internally,
243
+ because their internals are not visible to the MDX compiler.
244
+ - Nested Honeydeck timeline producers inside `<TimelineSteps>` are not supported.
245
+ Use the wrapper to reserve the block, then use `useTimelineSteps()` inside
246
+ the custom component.
247
+ - Hook state includes `{ phase, stepIndex, stepCount, startAt, endAt, isPdfFinalRender }`.
248
+ - In `isPdfFinalRender`, custom step components may render a PDF-specific final
249
+ composition, such as opening all accordion sections. Step-by-step PDF export
250
+ (`pdfSteps: all`) uses the normal timeline states and does not set this flag.
251
+
252
+ ### `<Notes>`
253
+
254
+ Presenter notes. Hidden from audience view and normal PDF. Notes are collected in presenter mode through runtime context and are not emitted into the audience DOM. Markdown authored inside `<Notes>` renders as formatted speaker notes in presenter mode, including headings, paragraphs, lists, links, inline code, code blocks, and block quotes.
255
+
256
+ ```mdx
257
+ <Notes>
258
+ # Demo cue
259
+
260
+ - Remember to demo the sparkle button here.
261
+ - Mention PDF export.
262
+ </Notes>
263
+ ```
@@ -0,0 +1,11 @@
1
+ export function SlideNumberBadge({ slide }: { slide: number }) {
2
+ return (
3
+ <div
4
+ role="status"
5
+ aria-label={`Slide ${slide}`}
6
+ className="absolute right-4 bottom-4 z-50 pointer-events-none text-3xl md:text-4xl tabular-nums text-foreground/75"
7
+ >
8
+ {slide}
9
+ </div>
10
+ );
11
+ }
@@ -0,0 +1,115 @@
1
+ import { createContext, type ReactNode, useContext } from "react";
2
+ import { useTimeline } from "../TimelineContext.tsx";
3
+
4
+ export type TimelineStepsPhase = "before" | "active" | "after";
5
+
6
+ export type TimelineStepsState = {
7
+ /** Local step index within this block. 0 before start, 1..stepCount while active. */
8
+ stepIndex: number;
9
+ /** Number of steps reserved by this block. */
10
+ stepCount: number;
11
+ /** Where the slide timeline is relative to this block. */
12
+ phase: TimelineStepsPhase;
13
+ /** First absolute slide step reserved by this block. */
14
+ startAt: number;
15
+ /** Last absolute slide step reserved by this block. */
16
+ endAt: number;
17
+ /**
18
+ * True while `honeydeck pdf` is capturing one final-state page per slide.
19
+ * Use this for components that should render an all-open/all-visible PDF state.
20
+ */
21
+ isPdfFinalRender: boolean;
22
+ };
23
+
24
+ export type TimelineStepsProps = {
25
+ /**
26
+ * Number of slide timeline steps reserved for the children.
27
+ * Must be a literal positive integer in MDX.
28
+ */
29
+ steps: number;
30
+ /**
31
+ * First absolute slide step. Injected by the Honeydeck compiler.
32
+ * Defaults to 1 only for direct runtime/test usage.
33
+ */
34
+ at?: number;
35
+ children?: ReactNode;
36
+ };
37
+
38
+ const TimelineStepsContext = createContext<TimelineStepsState | null>(null);
39
+
40
+ function assertPositiveInteger(value: number, propName: string): void {
41
+ if (!Number.isInteger(value) || value < 1) {
42
+ throw new Error(
43
+ `Honeydeck <TimelineSteps> requires ${propName} to be a positive integer.`,
44
+ );
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Reserves a static block of slide timeline steps for a custom component.
50
+ *
51
+ * Wrap custom interactive content in `TimelineSteps`, then call
52
+ * `useTimelineSteps()` inside that content to read the local step index,
53
+ * total step count, phase, and PDF-final-render state.
54
+ *
55
+ * ```mdx
56
+ * import { TimelineSteps, useTimelineSteps } from '@honeydeck/honeydeck'
57
+ *
58
+ * function AccordionDemo() {
59
+ * const { phase, stepIndex, stepCount, isPdfFinalRender } = useTimelineSteps()
60
+ * return <div>{phase}: {stepIndex} / {stepCount}</div>
61
+ * }
62
+ *
63
+ * <TimelineSteps steps={3}>
64
+ * <AccordionDemo />
65
+ * </TimelineSteps>
66
+ * ```
67
+ *
68
+ * Keep `steps` as a literal positive integer in slide MDX so Honeydeck can
69
+ * calculate the slide step count at build time. The compiler injects `at`;
70
+ * set it manually only in runtime tests or highly controlled custom usage.
71
+ */
72
+ export function TimelineSteps({ steps, at = 1, children }: TimelineStepsProps) {
73
+ assertPositiveInteger(steps, "steps");
74
+ assertPositiveInteger(at, "at");
75
+
76
+ const { stepIndex: slideStepIndex, isPdfFinalRender } = useTimeline();
77
+ const startAt = at;
78
+ const endAt = at + steps - 1;
79
+
80
+ let phase: TimelineStepsPhase = "active";
81
+ let localStepIndex = slideStepIndex - startAt + 1;
82
+
83
+ if (slideStepIndex < startAt) {
84
+ phase = "before";
85
+ localStepIndex = 0;
86
+ } else if (slideStepIndex > endAt) {
87
+ phase = "after";
88
+ localStepIndex = steps;
89
+ }
90
+
91
+ const value: TimelineStepsState = {
92
+ stepIndex: localStepIndex,
93
+ stepCount: steps,
94
+ phase,
95
+ startAt,
96
+ endAt,
97
+ isPdfFinalRender,
98
+ };
99
+
100
+ return (
101
+ <TimelineStepsContext.Provider value={value}>
102
+ {children}
103
+ </TimelineStepsContext.Provider>
104
+ );
105
+ }
106
+
107
+ export function useTimelineSteps(): TimelineStepsState {
108
+ const value = useContext(TimelineStepsContext);
109
+ if (!value) {
110
+ throw new Error(
111
+ "Honeydeck useTimelineSteps() must be used inside <TimelineSteps>.",
112
+ );
113
+ }
114
+ return value;
115
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Public runtime component exports for Honeydeck.
3
+ *
4
+ * These are the components that end users import in their MDX slides:
5
+ *
6
+ * ```mdx
7
+ * import { Reveal, RevealGroup, Notes } from '@honeydeck/honeydeck'
8
+ * ```
9
+ */
10
+
11
+ export type { BrowserFrameProps } from "./BrowserFrame.tsx";
12
+ export { BrowserFrame } from "./BrowserFrame.tsx";
13
+ export type { ButtonProps, ButtonVariant } from "./Button.tsx";
14
+ export {
15
+ Button,
16
+ buttonClass,
17
+ buttonPrimaryClass,
18
+ buttonSecondaryClass,
19
+ hoverBorderClass,
20
+ iconButtonClass,
21
+ quietLinkClass,
22
+ smallButtonClass,
23
+ surfaceControlClass,
24
+ transitionClass,
25
+ } from "./Button.tsx";
26
+ export type {
27
+ ColorMode,
28
+ ColorModeCycleButtonProps,
29
+ } from "./ColorModeCycleButton.tsx";
30
+ export {
31
+ COLOR_MODES,
32
+ ColorModeCycleButton,
33
+ getNextColorMode,
34
+ } from "./ColorModeCycleButton.tsx";
35
+ export type { KeyboardKey, KeyboardProps } from "./Keyboard.tsx";
36
+ export { Keyboard } from "./Keyboard.tsx";
37
+ export type { ListBullet, ListBullets, ListStyleProps } from "./ListStyle.tsx";
38
+ export { ListStyle } from "./ListStyle.tsx";
39
+ export type { NotesProps } from "./Notes.tsx";
40
+ export { Notes } from "./Notes.tsx";
41
+ export type { RevealProps } from "./Reveal.tsx";
42
+ export { Reveal } from "./Reveal.tsx";
43
+ export type { RevealGroupProps } from "./RevealGroup.tsx";
44
+ export { RevealGroup } from "./RevealGroup.tsx";
45
+ export type {
46
+ TimelineStepsPhase,
47
+ TimelineStepsProps,
48
+ TimelineStepsState,
49
+ } from "./TimelineSteps.tsx";
50
+ export { TimelineSteps, useTimelineSteps } from "./TimelineSteps.tsx";
51
+
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.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Main Honeydeck runtime entry point.
3
+ *
4
+ * This is what end-users import in their MDX slides:
5
+ * ```mdx
6
+ * import { Reveal, RevealGroup, Notes } from '@honeydeck/honeydeck'
7
+ * ```
8
+ */
9
+
10
+ export type { ColorModeImageProps } from "../layouts/ColorModeImage.tsx";
11
+ export { ColorModeImage } from "../layouts/ColorModeImage.tsx";
12
+ export type { BrowserFrameProps } from "./components/BrowserFrame.tsx";
13
+ export { BrowserFrame } from "./components/BrowserFrame.tsx";
14
+ export type { KeyboardKey, KeyboardProps } from "./components/Keyboard.tsx";
15
+ export { Keyboard } from "./components/Keyboard.tsx";
16
+ export type {
17
+ ListBullet,
18
+ ListBullets,
19
+ ListStyleProps,
20
+ } from "./components/ListStyle.tsx";
21
+ export { ListStyle } from "./components/ListStyle.tsx";
22
+ export type { NotesProps } from "./components/Notes.tsx";
23
+ export { Notes } from "./components/Notes.tsx";
24
+ export type { RevealProps } from "./components/Reveal.tsx";
25
+ export { Reveal } from "./components/Reveal.tsx";
26
+ export type { RevealGroupProps } from "./components/RevealGroup.tsx";
27
+ export { RevealGroup } from "./components/RevealGroup.tsx";
28
+ export type {
29
+ TimelineStepsPhase,
30
+ TimelineStepsProps,
31
+ TimelineStepsState,
32
+ } from "./components/TimelineSteps.tsx";
33
+ export {
34
+ TimelineSteps,
35
+ useTimelineSteps,
36
+ } from "./components/TimelineSteps.tsx";
37
+ export type {
38
+ TimelineContextValue,
39
+ TimelineProviderProps,
40
+ } from "./TimelineContext.tsx";
41
+ // TimelineProvider and useTimeline are exported for kit/layout authors.
42
+ export { TimelineProvider, useTimeline } from "./TimelineContext.tsx";
@@ -0,0 +1,68 @@
1
+ const INTERACTIVE_SELECTOR = [
2
+ "button",
3
+ "a[href]",
4
+ "input",
5
+ "textarea",
6
+ "select",
7
+ "summary",
8
+ "[contenteditable='true']",
9
+ "[role='button']",
10
+ "[role='link']",
11
+ ].join(",");
12
+
13
+ const SCROLL_OVERFLOW_VALUES = new Set(["auto", "scroll"]);
14
+
15
+ function isElement(value: EventTarget | null): value is Element {
16
+ return typeof Element !== "undefined" && value instanceof Element;
17
+ }
18
+
19
+ function isHTMLElement(value: Element): value is HTMLElement {
20
+ return typeof HTMLElement !== "undefined" && value instanceof HTMLElement;
21
+ }
22
+
23
+ function allowsScroll(value: string): boolean {
24
+ return SCROLL_OVERFLOW_VALUES.has(value);
25
+ }
26
+
27
+ export function isInteractiveTarget(target: EventTarget | null): boolean {
28
+ if (!isElement(target)) return false;
29
+ return target.closest(INTERACTIVE_SELECTOR) !== null;
30
+ }
31
+
32
+ export function findScrollableAncestor(
33
+ target: EventTarget | null,
34
+ boundary?: Element | null,
35
+ ): HTMLElement | null {
36
+ if (!isElement(target)) return null;
37
+
38
+ let current: Element | null = target;
39
+ while (current && current !== boundary) {
40
+ if (isHTMLElement(current)) {
41
+ if (current.hasAttribute("data-honeydeck-scrollable")) return current;
42
+
43
+ const style = window.getComputedStyle(current);
44
+ const canScrollY =
45
+ allowsScroll(style.overflowY) &&
46
+ current.scrollHeight > current.clientHeight;
47
+ const canScrollX =
48
+ allowsScroll(style.overflowX) &&
49
+ current.scrollWidth > current.clientWidth;
50
+
51
+ if (canScrollY || canScrollX) return current;
52
+ }
53
+
54
+ current = current.parentElement;
55
+ }
56
+
57
+ return null;
58
+ }
59
+
60
+ export function shouldDeckOwnTouchGesture(
61
+ target: EventTarget | null,
62
+ boundary?: Element | null,
63
+ ): boolean {
64
+ if (!isElement(target)) return false;
65
+ if (target.closest("[data-honeydeck-no-swipe]")) return false;
66
+ if (isInteractiveTarget(target)) return false;
67
+ return findScrollableAncestor(target, boundary) === null;
68
+ }
@@ -0,0 +1,7 @@
1
+ export function isEditableKeyboardTarget(target: EventTarget | null): boolean {
2
+ if (typeof HTMLElement === "undefined") return false;
3
+ if (!(target instanceof HTMLElement)) return false;
4
+
5
+ const tag = target.tagName;
6
+ return tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable;
7
+ }
@@ -0,0 +1,56 @@
1
+ import type { Route } from "./router.ts";
2
+
3
+ const STORAGE_KEY = "honeydeck:last-slide-route";
4
+ const FALLBACK_SLIDE_ROUTE: Route = { view: "slide", slide: 1, step: 0 };
5
+
6
+ function isStorageAvailable(): boolean {
7
+ return typeof sessionStorage !== "undefined";
8
+ }
9
+
10
+ function toStoredSlideRoute(route: Route): Route | null {
11
+ if (route.view !== "slide") return null;
12
+
13
+ const slide =
14
+ Number.isFinite(route.slide) && route.slide >= 1 ? route.slide : 1;
15
+ const step = Number.isFinite(route.step) && route.step >= 0 ? route.step : 0;
16
+
17
+ return { view: "slide", slide, step };
18
+ }
19
+
20
+ export function rememberSlideRoute(route: Route): void {
21
+ const slideRoute = toStoredSlideRoute(route);
22
+ if (!slideRoute || !isStorageAvailable()) return;
23
+
24
+ try {
25
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(slideRoute));
26
+ } catch {
27
+ // Session storage can be unavailable in restrictive browser contexts.
28
+ }
29
+ }
30
+
31
+ export function getRememberedSlideRoute(): Route {
32
+ if (!isStorageAvailable()) return FALLBACK_SLIDE_ROUTE;
33
+
34
+ try {
35
+ const value = sessionStorage.getItem(STORAGE_KEY);
36
+ if (!value) return FALLBACK_SLIDE_ROUTE;
37
+
38
+ const parsed = JSON.parse(value) as Partial<Route>;
39
+ const slide =
40
+ typeof parsed.slide === "number" &&
41
+ Number.isFinite(parsed.slide) &&
42
+ parsed.slide >= 1
43
+ ? parsed.slide
44
+ : 1;
45
+ const step =
46
+ typeof parsed.step === "number" &&
47
+ Number.isFinite(parsed.step) &&
48
+ parsed.step >= 0
49
+ ? parsed.step
50
+ : 0;
51
+
52
+ return { view: "slide", slide, step };
53
+ } catch {
54
+ return FALLBACK_SLIDE_ROUTE;
55
+ }
56
+ }