@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,76 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type PresenterNotesPanelProps = {
4
+ notes: ReactNode;
5
+ className?: string;
6
+ };
7
+
8
+ const notesContentClassName = [
9
+ "honeydeck-presenter-notes-content",
10
+ "space-y-2",
11
+ "[&_a]:text-white",
12
+ "[&_a]:underline",
13
+ "[&_blockquote]:border-l-2",
14
+ "[&_blockquote]:border-white/20",
15
+ "[&_blockquote]:pl-3",
16
+ "[&_blockquote]:text-white/55",
17
+ "[&_code]:rounded-xs",
18
+ "[&_code]:bg-white/10",
19
+ "[&_code]:px-1",
20
+ "[&_code]:py-0.5",
21
+ "[&_code]:font-mono",
22
+ "[&_code]:text-sm",
23
+ "[&_h1]:mb-2",
24
+ "[&_h1]:mt-1",
25
+ "[&_h1]:text-xl",
26
+ "[&_h1]:font-semibold",
27
+ "[&_h1]:leading-snug",
28
+ "[&_h1]:text-white/90",
29
+ "[&_h2]:mb-2",
30
+ "[&_h2]:mt-3",
31
+ "[&_h2]:text-lg",
32
+ "[&_h2]:font-semibold",
33
+ "[&_h2]:leading-snug",
34
+ "[&_h2]:text-white/85",
35
+ "[&_h3]:mb-1.5",
36
+ "[&_h3]:mt-3",
37
+ "[&_h3]:text-base",
38
+ "[&_h3]:font-semibold",
39
+ "[&_h3]:leading-snug",
40
+ "[&_h3]:text-white/80",
41
+ "[&_li]:my-1",
42
+ "[&_ol]:list-decimal",
43
+ "[&_ol]:pl-5",
44
+ "[&_p]:my-2",
45
+ "[&_pre]:overflow-x-auto",
46
+ "[&_pre]:rounded",
47
+ "[&_pre]:bg-white/10",
48
+ "[&_pre]:p-2",
49
+ "[&_pre_code]:bg-transparent",
50
+ "[&_pre_code]:p-0",
51
+ "[&_strong]:font-semibold",
52
+ "[&_strong]:text-white/85",
53
+ "[&_ul]:list-disc",
54
+ "[&_ul]:pl-5",
55
+ ].join(" ");
56
+
57
+ export function PresenterNotesPanel({
58
+ notes,
59
+ className = "",
60
+ }: PresenterNotesPanelProps) {
61
+ return (
62
+ <div
63
+ className={`px-4 py-3 bg-white/4 rounded-md border border-white/8 min-h-0 overflow-y-auto overscroll-contain text-lg leading-relaxed text-white/75 ${className}`}
64
+ data-honeydeck-scrollable="true"
65
+ >
66
+ <div className="text-xs font-semibold text-white/35 tracking-wider uppercase mb-2">
67
+ Notes
68
+ </div>
69
+ {notes == null ? (
70
+ <span className="text-white/25 italic">No notes for this slide.</span>
71
+ ) : (
72
+ <div className={notesContentClassName}>{notes}</div>
73
+ )}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,340 @@
1
+ /**
2
+ * PresenterView — Honeydeck presenter mode.
3
+ *
4
+ * Route: /#/presenter/slideNumber/stepIndex
5
+ *
6
+ * Layout:
7
+ * ┌──────────────────────────────────────────┐
8
+ * │ ┌──────────────────────┐ ┌──────────┐ │
9
+ * │ │ │ │ Next │ │
10
+ * │ │ Current slide │ │ (small) │ │
11
+ * │ │ (large) │ ├──────────┤ │
12
+ * │ │ │ │ Notes │ │
13
+ * │ └──────────────────────┘ └──────────┘ │
14
+ * │ Slide 3/12 · Step 2/4 12:34 [Open] │
15
+ * └──────────────────────────────────────────┘
16
+ *
17
+ * ### Notes collection
18
+ * The current slide is rendered inside a `<NotesContext.Provider>`. Any
19
+ * `<Notes>` component in that slide pushes its children into a state slot
20
+ * via `setNotes`, which is then displayed in the notes area.
21
+ *
22
+ * ### BroadcastChannel sync
23
+ * PresenterView is the controller: it broadcasts `navigate` messages whenever
24
+ * its route changes. Audience windows (`Deck`) listen and follow.
25
+ *
26
+ * ### Keyboard navigation
27
+ * `useKeyboardNav` is wired so that arrow keys advance the presenter route
28
+ * (view: 'presenter'), which in turn is broadcast to the audience.
29
+ */
30
+
31
+ import {
32
+ ChevronDownIcon,
33
+ ChevronLeftIcon,
34
+ ChevronRightIcon,
35
+ ChevronUpIcon,
36
+ ExternalLinkIcon,
37
+ } from "lucide-react";
38
+ import {
39
+ type ReactNode,
40
+ useCallback,
41
+ useEffect,
42
+ useMemo,
43
+ useRef,
44
+ useState,
45
+ } from "react";
46
+ import { NotesContext } from "../components/Notes.tsx";
47
+ import {
48
+ getRouteUrl,
49
+ nextSlide,
50
+ nextStep,
51
+ openUrlInNewTab,
52
+ previousSlide,
53
+ previousStep,
54
+ } from "../navigation.ts";
55
+ import { useRoute } from "../router.ts";
56
+ import { SlideCanvas } from "../SlideCanvas.tsx";
57
+ import { BASE_HEIGHT, BASE_WIDTH, slideData } from "../slideData.ts";
58
+ import { useSync } from "../sync.ts";
59
+ import { useKeyboardNav } from "../useKeyboardNav.ts";
60
+ import { useSwipeNav } from "../useSwipeNav.ts";
61
+ import { PresenterNotesPanel } from "./PresenterNotesPanel.tsx";
62
+ import { getPresenterNextPreview } from "./presenterPreview.ts";
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Helpers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * A slide preview scaled to fit a measured container.
70
+ * Uses ResizeObserver so it adjusts when the presenter window is resized.
71
+ */
72
+ function SlidePreview({
73
+ slideIndex,
74
+ stepIndex,
75
+ label,
76
+ showFutureSteps,
77
+ }: {
78
+ slideIndex: number;
79
+ stepIndex: number;
80
+ label?: string;
81
+ showFutureSteps?: boolean;
82
+ }) {
83
+ const containerRef = useRef<HTMLDivElement>(null);
84
+ const [scale, setScale] = useState(0.4);
85
+
86
+ useEffect(() => {
87
+ const el = containerRef.current;
88
+ if (!el) return;
89
+ const ro = new ResizeObserver((entries) => {
90
+ for (const entry of entries) {
91
+ const { width, height } = entry.contentRect;
92
+ setScale(Math.min(width / BASE_WIDTH, height / BASE_HEIGHT));
93
+ }
94
+ });
95
+ ro.observe(el);
96
+ return () => ro.disconnect();
97
+ }, []);
98
+
99
+ if (slideIndex < 0 || slideIndex >= slideData.length) {
100
+ return (
101
+ <div className="flex flex-col gap-1.5 overflow-hidden">
102
+ {label && (
103
+ <div className="text-xs font-semibold text-white/50 tracking-wider uppercase">
104
+ {label}
105
+ </div>
106
+ )}
107
+ <div className="flex-1 bg-white/5 rounded-md flex items-center justify-center text-white/30 text-md italic outline-1 outline-solid outline-white/10">
108
+ No next step
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ const visualW = BASE_WIDTH * scale;
115
+ const visualH = BASE_HEIGHT * scale;
116
+
117
+ return (
118
+ <div className="flex flex-col gap-1.5 overflow-hidden">
119
+ {label && (
120
+ <div className="text-xs font-semibold text-white/50 tracking-wider uppercase">
121
+ {label}
122
+ </div>
123
+ )}
124
+ <div
125
+ ref={containerRef}
126
+ className="flex-1 overflow-hidden rounded-md flex items-center justify-center outline-1 outline-solid outline-white/10"
127
+ >
128
+ <SlideCanvas
129
+ slideIndex={slideIndex}
130
+ stepIndex={stepIndex}
131
+ scale={scale}
132
+ style={{ width: visualW, height: visualH }}
133
+ showFutureSteps={showFutureSteps}
134
+ />
135
+ </div>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ function usePresenterMobile(): boolean {
141
+ const [isMobile, setIsMobile] = useState(() => {
142
+ if (typeof window === "undefined") return false;
143
+ return window.matchMedia("(max-width: 767px)").matches;
144
+ });
145
+
146
+ useEffect(() => {
147
+ const query = window.matchMedia("(max-width: 767px)");
148
+ const update = () => setIsMobile(query.matches);
149
+ update();
150
+ query.addEventListener("change", update);
151
+ return () => query.removeEventListener("change", update);
152
+ }, []);
153
+
154
+ return isMobile;
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Component
159
+ // ---------------------------------------------------------------------------
160
+
161
+ export function PresenterView() {
162
+ const route = useRoute();
163
+ const totalSlides = slideData.length;
164
+ const slide = Math.max(1, Math.min(route.slide, totalSlides || 1));
165
+ const step = Math.max(0, route.step);
166
+
167
+ const [clock, setClock] = useState(() => new Date().toLocaleTimeString());
168
+ const [notes, setNotes] = useState<ReactNode>(null);
169
+ const notesContextValue = useMemo(() => ({ setNotes }), []);
170
+ const isMobile = usePresenterMobile();
171
+
172
+ const currentIndex = slide - 1; // 0-based
173
+ const currentStepCount = slideData[currentIndex]?.stepCount ?? 0;
174
+ const nextPreview = getPresenterNextPreview({
175
+ currentIndex,
176
+ step,
177
+ stepCount: currentStepCount,
178
+ totalSlides,
179
+ });
180
+
181
+ // ── Wall clock ─────────────────────────────────────────────────────────
182
+ useEffect(() => {
183
+ const timer = setInterval(() => {
184
+ setClock(new Date().toLocaleTimeString());
185
+ }, 1000);
186
+ return () => clearInterval(timer);
187
+ }, []);
188
+
189
+ // ── BroadcastChannel: this window IS the presenter (controller) ─────────
190
+ useSync({ isPresenter: true, currentSlide: slide, currentStep: step });
191
+
192
+ // ── Keyboard navigation ─────────────────────────────────────────────────
193
+ const getStepCount = useCallback(
194
+ (i: number) => slideData[i]?.stepCount ?? 0,
195
+ [],
196
+ );
197
+ useKeyboardNav({
198
+ slideCount: totalSlides,
199
+ getStepCount,
200
+ onToggleOverview: () => {},
201
+ });
202
+ useSwipeNav({ enabled: isMobile });
203
+
204
+ // ── Open audience window ────────────────────────────────────────────────
205
+ function openAudienceView() {
206
+ openUrlInNewTab(getRouteUrl({ view: "slide", slide, step }));
207
+ }
208
+
209
+ // ── Navigate (presenter stays on presenter route) ───────────────────────
210
+ function goNext() {
211
+ nextStep(
212
+ { view: "presenter", slide, step },
213
+ { slideCount: totalSlides, getStepCount },
214
+ );
215
+ }
216
+
217
+ function goPrev() {
218
+ previousStep(
219
+ { view: "presenter", slide, step },
220
+ { slideCount: totalSlides, getStepCount },
221
+ );
222
+ }
223
+
224
+ function goNextSlide() {
225
+ nextSlide({ view: "presenter", slide, step }, { slideCount: totalSlides });
226
+ }
227
+
228
+ function goPrevSlide() {
229
+ previousSlide({ view: "presenter", slide, step });
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Render
234
+ // ---------------------------------------------------------------------------
235
+
236
+ return (
237
+ <div className="fixed inset-0 bg-black text-white font-sans grid grid-rows-[1fr_auto_auto] grid-cols-1 overflow-hidden select-none">
238
+ {/* ── Top section: current slide plus desktop next/notes column ─── */}
239
+ <div
240
+ className={
241
+ isMobile
242
+ ? "grid grid-cols-1 gap-3 px-3 pt-3 pb-2 min-h-0 overflow-hidden"
243
+ : "grid grid-cols-[minmax(0,3fr)_minmax(280px,2fr)] gap-4 px-4 pt-4 pb-2 min-h-0 overflow-hidden"
244
+ }
245
+ >
246
+ {/* Current slide owns NotesContext. Next preview must not overwrite notes. */}
247
+ <NotesContext.Provider value={notesContextValue}>
248
+ <SlidePreview
249
+ slideIndex={currentIndex}
250
+ stepIndex={step}
251
+ label="Current"
252
+ />
253
+ </NotesContext.Provider>
254
+
255
+ {!isMobile && (
256
+ <div className="grid grid-rows-[minmax(0,1fr)_minmax(8rem,0.8fr)] gap-4 min-h-0 overflow-hidden">
257
+ <SlidePreview
258
+ slideIndex={nextPreview?.slideIndex ?? -1}
259
+ stepIndex={nextPreview?.stepIndex ?? 0}
260
+ label="Next"
261
+ showFutureSteps
262
+ />
263
+ <PresenterNotesPanel notes={notes} />
264
+ </div>
265
+ )}
266
+ </div>
267
+
268
+ {/* ── Mobile notes live below the current slide ─────────────────── */}
269
+ {isMobile && (
270
+ <PresenterNotesPanel
271
+ notes={notes}
272
+ className="mx-3 mb-2 min-h-20 max-h-40"
273
+ />
274
+ )}
275
+
276
+ {/* ── Bottom bar: counter · fixed nav buttons · clock/actions ───── */}
277
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] sm:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center px-3 sm:px-5 py-2.5 border-t border-white/8 bg-black/30 gap-x-3 sm:gap-x-4 gap-y-2">
278
+ {/* Counter */}
279
+ <div className="min-w-0 text-md text-white/60 tabular-nums truncate">
280
+ Slide {slide}/{totalSlides}
281
+ {currentStepCount > 0 && ` · Step ${step}/${currentStepCount}`}
282
+ </div>
283
+
284
+ {/* Nav buttons */}
285
+ <div className="flex gap-2 items-center justify-self-end sm:justify-self-center">
286
+ <NavButton onClick={goPrevSlide} title="Previous slide (↑)">
287
+ <ChevronUpIcon aria-hidden="true" size={18} />
288
+ </NavButton>
289
+ <NavButton onClick={goPrev} title="Previous step (←)">
290
+ <ChevronLeftIcon aria-hidden="true" size={18} />
291
+ </NavButton>
292
+ <NavButton onClick={goNext} title="Next step (→)">
293
+ <ChevronRightIcon aria-hidden="true" size={18} />
294
+ </NavButton>
295
+ <NavButton onClick={goNextSlide} title="Next slide (↓)">
296
+ <ChevronDownIcon aria-hidden="true" size={18} />
297
+ </NavButton>
298
+ </div>
299
+
300
+ {/* Clock + open button */}
301
+ <div className="col-span-2 sm:col-span-1 sm:col-start-3 flex flex-wrap gap-3 items-center justify-self-start sm:justify-self-end">
302
+ <span className="text-md tabular-nums text-white/60">{clock}</span>
303
+ <button
304
+ type="button"
305
+ onClick={openAudienceView}
306
+ className="px-3 py-1 rounded border border-white/20 bg-white/6 text-white/80 text-sm font-[inherit] inline-flex items-center gap-1.5"
307
+ >
308
+ Open audience view
309
+ <ExternalLinkIcon aria-hidden="true" size={14} />
310
+ </button>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ );
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Small inline button component
319
+ // ---------------------------------------------------------------------------
320
+
321
+ function NavButton({
322
+ onClick,
323
+ title,
324
+ children,
325
+ }: {
326
+ onClick: () => void;
327
+ title?: string;
328
+ children: ReactNode;
329
+ }) {
330
+ return (
331
+ <button
332
+ type="button"
333
+ onClick={onClick}
334
+ title={title}
335
+ className="w-9 h-9 rounded border border-white/15 bg-white/6 text-white/75 text-md flex items-center justify-center font-[inherit]"
336
+ >
337
+ {children}
338
+ </button>
339
+ );
340
+ }
@@ -0,0 +1,152 @@
1
+ # Honeydeck Runtime Views Specification
2
+
3
+ > Observable behavior for presenter mode, overview mode, and built-in reference pages.
4
+
5
+ ## Presenter Mode
6
+
7
+ ### Activation
8
+
9
+ - Keyboard shortcut `p` (opens in new window/tab using the current deck base path)
10
+ - Navigation controls button
11
+ - Direct URL: `/#/presenter/1/0`
12
+
13
+ ### UI Layout
14
+
15
+ ```txt
16
+ ┌──────────────────────────────────────────┐
17
+ │ ┌──────────────────┐ ┌──────────┐ │
18
+ │ │ │ │ │ │
19
+ │ │ Current │ │ Next │ │
20
+ │ │ (larger) │ │ (smaller)│ │
21
+ │ │ │ │ │ │
22
+ │ └──────────────────┘ └──────────┘ │
23
+ │ │
24
+ │ Notes: │
25
+ │ - Remember to demo the sparkle button │
26
+ │ - Mention PDF export │
27
+ │ │
28
+ │ Slide 3/12 · Step 2/4 12:34 [Open] │
29
+ └──────────────────────────────────────────┘
30
+ ```
31
+
32
+ Includes:
33
+ - Current slide preview (larger)
34
+ - Next timeline-state preview (smaller): the next step on the current slide when one exists, otherwise the next slide at step 0
35
+ - Speaker notes for current slide, with Markdown formatting from `<Notes>` rendered as compact presenter prose
36
+ - Slide/step counter
37
+ - Clock (wall clock)
38
+ - Button to open audience view in new tab, preserving the current slide/step and deck base path
39
+ - Presenter navigation buttons provide previous/next timeline-step navigation and previous/next slide navigation. Timeline keyboard shortcuts (`→`/`←`/`↓`/`↑`, `d`/`a`/`s`/`w`) update the presenter route and keep the window in presenter mode.
40
+ - Presenter navigation uses the shared Honeydeck navigation command abstraction so button, keyboard, and touch inputs share the same semantics as audience view.
41
+ - Presenter notes are scroll-owned regions: wheel, trackpad, touch scroll, and swipe gestures that start in notes scroll notes and never navigate slides, even at scroll boundaries.
42
+ - On mobile presenter layouts, the Current preview may use tap zones and swipe navigation; speaker notes remain scroll-only. Pinch-to-zoom and pinch-to-overview are not required in presenter mode.
43
+ - Code step-through previews use the same timeline state as audience view, so the Next preview shows the upcoming highlighted code step.
44
+ - In the Next preview, reveal content from later timeline steps is visible at reduced opacity so the speaker can see what is still coming on that slide. Audience view and the Current preview keep future steps hidden.
45
+ - When no next timeline state exists (final step of the final slide), the Next preview shows a placeholder (`No next step`) instead of trying to render a missing slide.
46
+
47
+ ### Presenter Responsiveness
48
+
49
+ Presenter mode uses a two-column preview area (`Current` larger, `Next` smaller), a notes panel, and a bottom status/action bar on desktop. On narrow/mobile screens it switches to a single-column layout and hides the Next preview.
50
+
51
+ ### Audience Sync
52
+
53
+ Presenter mode and audience view synchronize via `BroadcastChannel`:
54
+
55
+ - Same browser/profile only (no server needed)
56
+ - Presenter mode is the controller
57
+ - Audience view listens for navigation updates
58
+ - Late-opening audience tabs request the current presenter position via a `sync-request` / `sync-response` handshake so they sync immediately instead of waiting for the next presenter move
59
+ - Presence messages (`presenter-connected` / `presenter-disconnected`) are broadcast
60
+ - Audience sync ignores navigation while the audience window is in the docs/reference view
61
+ - If sync unavailable, both still work independently
62
+
63
+ ---
64
+
65
+ ---
66
+
67
+ ## Overview Mode
68
+
69
+ Toggled via `o` or the overview button. Overview is also directly addressable as `/#/overview/<slideNumber>/<stepIndex>`.
70
+
71
+ - Responsive grid of rendered slide thumbnails
72
+ - Overview appears over the current slide with a translucent themed background (`bg-background`) and backdrop blur so the active slide remains softly visible behind the grid
73
+ - Desktop thumbnails render the first step of each slide at a fixed visual width (360px) using `SlideCanvas`
74
+ - Mobile overview is a responsive page with a fixed two-column grid; two columns are the minimum supported mobile overview density
75
+ - Future reveal steps are visible at reduced opacity in thumbnails so authors can see what each slide will contain by the end
76
+ - The current slide gets a `Current` badge; desktop keyboard selection gets the stronger accent-color outline. Mobile overview does not show a separate focused-slide indicator because compact padding makes that indicator visually noisy.
77
+ - Click or tap a thumbnail to jump to that slide, reset to step 0, and exit overview by creating a new slide history entry
78
+ - Browser Back from overview closes overview by returning to the previous route
79
+ - Overview scroll is scroll-owned: wheel, trackpad, touch scroll, and swipe gestures that start in the overview grid scroll the grid and never navigate slides, even at scroll boundaries
80
+ - Overview does not handle pinch gestures; pinch zoom is only supported on slides
81
+ - The overview header is sticky and uses a blurred translucent themed background while the slide grid scrolls underneath
82
+ - On entering overview, the route/current slide is scrolled into view
83
+ - **Timeline navigation is disabled** — arrow keys are repurposed for overview grid selection; WASD are no-ops
84
+ - Keyboard in overview:
85
+ - Arrow keys move selection within the rendered grid; vertical movement uses the actual measured column count so it stays correct after initial layout and window resizes while overview is open
86
+ - Pressing Up while the selected thumbnail is already in the topmost rendered row keeps the current selection instead of jumping to the first slide
87
+ - Pressing Down while the selected thumbnail is already in the bottommost rendered row keeps the current selection instead of jumping to the last slide
88
+ - Overview gives a short visual boundary nudge when Up or Down cannot move because the selection is already at the top or bottom row
89
+ - Keyboard navigation keeps the selected thumbnail smoothly scrolled into view, with margin so the selection is not flush against the viewport edge; if repeated key presses change the selection while a scroll animation is running, the new scroll target takes over instead of queueing behind the previous animation
90
+ - Enter jumps to selected slide, resets to step 0, exits overview, and creates a new slide history entry
91
+ - `o` toggles (also exits) overview
92
+ - Escape exits overview
93
+ - Overview button in nav bar exits overview
94
+
95
+ ---
96
+
97
+ ---
98
+
99
+ ## Reference Pages
100
+
101
+ Built-in reference pages start at `/#/theme`. Included in both dev and production builds.
102
+
103
+ User-facing product copy should call this area "reference pages". Reserve "kit" for the reusable theme/layout/component package concept described in [Kits — Theme, Layouts, Components](../../layouts/SPEC.md#kits--theme-layouts-components).
104
+
105
+ ### Routes
106
+
107
+ ```txt
108
+ /#/theme → deep link to theme tokens tab
109
+ /#/layouts → deep link to layouts tab
110
+ /#/components → deep link to built-in components tab
111
+ ```
112
+
113
+ ### Returning to Slides
114
+
115
+ Reference pages include a "Back to slides" button. It returns to the last visited audience slide and step from the current browser session. Pressing `Escape` on any reference page performs the same return-to-slides action, unless focus is in an editable field. Reference tab changes and theme/layout/component deep links keep the same return target. Directly opening or reloading a reference page without a known previous slide falls back to slide 1, step 0.
116
+
117
+ The reference header shows only Theme tokens, Layouts, and Components tabs. It also provides an always-underlined external Docs link to `https://honeydeck.dev` with an icon indicating that it opens a new tab.
118
+
119
+ ### Theme Tab
120
+
121
+ Displays all `--honeydeck-*` CSS tokens with:
122
+
123
+ - Current computed values
124
+ - Default values from `base.css`
125
+ - Descriptions when available
126
+
127
+ ### Layouts Tab
128
+
129
+ Shows one card for each layout currently available to the deck author, i.e. every key in the active layout map (`layouts:` or built-in fallback), regardless of whether a slide currently uses it:
130
+
131
+ - Visual preview rendered from the layout's `demo` export when statically discoverable
132
+ - "No demo MDX provided" hint when no static demo MDX is discovered
133
+ - Usage reference with visibly tab-like controls:
134
+ - `Usage` shows a copyable MDX snippet from the layout demo's explicit `mdx` field
135
+ - `Props` shows slide frontmatter fields accepted by that layout, including property name, required marker, type, and description
136
+ - Active/inactive states must read as tabs, not plain text links
137
+ - The copy action in the usage reference must look and behave like a button, with clear affordance and copied feedback
138
+
139
+ Layout prop docs are statically extracted from the layout component's `LayoutProps<Frontmatter>` type. Property descriptions come from JSDoc comments on the frontmatter type fields. If no layout-specific frontmatter is discovered, the `Props` tab still documents the required `layout` selector.
140
+
141
+ ### Components Tab
142
+
143
+ Shows generated documentation for each public built-in component exported from `@honeydeck/honeydeck/components`, discovered from the component barrel at build time. Unlike the layouts tab, components are primarily documented with prose and usage examples rather than visual previews:
144
+
145
+ - A side navigation lists all discovered built-in components and scrolls to the matching section
146
+ - Each component appears as a full-width documentation section, one below another
147
+ - The info section comes from the exported component declaration's JSDoc comment, interpreted as Markdown/MDX
148
+ - Usage examples live in that component JSDoc comment, usually as fenced `mdx` code blocks
149
+ - Params are generated from the component's exported props type or interface, including prop names, TypeScript type text, required/optional state, prop JSDoc descriptions, and default values inferred from destructured parameter defaults when possible
150
+ - The component params table gives most horizontal space to the Description column; Param, Type, and Default columns stay narrower and wrap when needed
151
+ - Components without a docs comment or exported props type still appear with a helpful fallback
152
+ - Non-component exports such as hooks are skipped from the generated section list