@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,211 @@
1
+ import { rememberSlideRoute } from "./lastSlideRoute.ts";
2
+ import type { Route } from "./router.ts";
3
+ import { navigate, serializeRoute } from "./router.ts";
4
+
5
+ export type NavigationRoute = Route & {
6
+ view: "slide" | "presenter" | "overview";
7
+ };
8
+
9
+ export type StepCountGetter = (slideIndex: number) => number;
10
+
11
+ export type NavigationOptions = {
12
+ slideCount?: number;
13
+ getStepCount?: StepCountGetter;
14
+ };
15
+
16
+ function getSlideCount(options?: NavigationOptions): number {
17
+ return options?.slideCount ?? Number.POSITIVE_INFINITY;
18
+ }
19
+
20
+ function getStepCountForSlide(
21
+ slideIndex: number,
22
+ options?: NavigationOptions,
23
+ ): number {
24
+ return options?.getStepCount?.(slideIndex) ?? 0;
25
+ }
26
+
27
+ function normalizeNavigableRoute(
28
+ route: Route,
29
+ options?: NavigationOptions,
30
+ ): NavigationRoute | null {
31
+ if (
32
+ route.view !== "slide" &&
33
+ route.view !== "presenter" &&
34
+ route.view !== "overview"
35
+ ) {
36
+ return null;
37
+ }
38
+ const totalSlides = getSlideCount(options);
39
+ const slide = Math.max(1, Math.min(route.slide, totalSlides));
40
+ return { ...route, slide } as NavigationRoute;
41
+ }
42
+
43
+ function withRoutePosition(
44
+ route: NavigationRoute,
45
+ slide: number,
46
+ step: number,
47
+ ): Route {
48
+ return { ...route, slide, step };
49
+ }
50
+
51
+ export function getRouteUrl(route: Route, baseUrl?: string): string {
52
+ const url = new URL(baseUrl ?? location.href);
53
+ url.hash = serializeRoute(route);
54
+ return url.toString();
55
+ }
56
+
57
+ export function openUrlInNewTab(url: string): void {
58
+ const opened = window.open(url, "_blank", "noopener,noreferrer");
59
+ if (opened) {
60
+ opened.opener = null;
61
+ }
62
+ }
63
+
64
+ export function getPreviousStepRoute(
65
+ route: Route,
66
+ options?: NavigationOptions,
67
+ ): Route | null {
68
+ const navigable = normalizeNavigableRoute(route, options);
69
+ if (!navigable) return null;
70
+
71
+ if (navigable.step > 0) {
72
+ return withRoutePosition(navigable, navigable.slide, navigable.step - 1);
73
+ }
74
+
75
+ if (navigable.slide > 1) {
76
+ const prevSlide = navigable.slide - 1;
77
+ const prevSteps = getStepCountForSlide(prevSlide - 1, options);
78
+ return withRoutePosition(navigable, prevSlide, prevSteps);
79
+ }
80
+
81
+ return null;
82
+ }
83
+
84
+ export function getNextStepRoute(
85
+ route: Route,
86
+ options?: NavigationOptions,
87
+ ): Route | null {
88
+ const navigable = normalizeNavigableRoute(route, options);
89
+ if (!navigable) return null;
90
+
91
+ const totalSlides = getSlideCount(options);
92
+ const stepCount = getStepCountForSlide(navigable.slide - 1, options);
93
+
94
+ if (navigable.step < stepCount) {
95
+ return withRoutePosition(navigable, navigable.slide, navigable.step + 1);
96
+ }
97
+
98
+ if (navigable.slide < totalSlides) {
99
+ return withRoutePosition(navigable, navigable.slide + 1, 0);
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ export function getPreviousSlideRoute(
106
+ route: Route,
107
+ options?: NavigationOptions,
108
+ ): Route | null {
109
+ const navigable = normalizeNavigableRoute(route, options);
110
+ if (!navigable || navigable.slide <= 1) return null;
111
+ return withRoutePosition(navigable, navigable.slide - 1, 0);
112
+ }
113
+
114
+ export function getNextSlideRoute(
115
+ route: Route,
116
+ options?: NavigationOptions,
117
+ ): Route | null {
118
+ const navigable = normalizeNavigableRoute(route, options);
119
+ if (!navigable) return null;
120
+ const totalSlides = getSlideCount(options);
121
+ if (navigable.slide >= totalSlides) return null;
122
+ return withRoutePosition(navigable, navigable.slide + 1, 0);
123
+ }
124
+
125
+ export function getOverviewRoute(
126
+ route: Route,
127
+ options?: NavigationOptions,
128
+ ): Route | null {
129
+ const navigable = normalizeNavigableRoute(route, options);
130
+ if (!navigable) return null;
131
+ if (navigable.view === "overview") return navigable;
132
+ return { view: "overview", slide: navigable.slide, step: navigable.step };
133
+ }
134
+
135
+ export function getSlideRouteFromRoute(
136
+ route: Route,
137
+ options?: NavigationOptions,
138
+ ): Route | null {
139
+ const navigable = normalizeNavigableRoute(route, options);
140
+ if (!navigable) return null;
141
+ return { view: "slide", slide: navigable.slide, step: navigable.step };
142
+ }
143
+
144
+ export function getToggleOverviewRoute(
145
+ route: Route,
146
+ options?: NavigationOptions,
147
+ ): Route | null {
148
+ if (route.view === "overview") return getSlideRouteFromRoute(route, options);
149
+ return getOverviewRoute(route, options);
150
+ }
151
+
152
+ export function getReferenceRoute(): Route {
153
+ return { view: "kit", slide: 1, step: 0, kitTab: "layouts" };
154
+ }
155
+
156
+ export function getDocsWebsiteUrl(): string {
157
+ return "https://honeydeck.dev";
158
+ }
159
+
160
+ export function navigateTo(route: Route | null): void {
161
+ if (route) navigate(route);
162
+ }
163
+
164
+ export function previousStep(route: Route, options?: NavigationOptions): void {
165
+ navigateTo(getPreviousStepRoute(route, options));
166
+ }
167
+
168
+ export function nextStep(route: Route, options?: NavigationOptions): void {
169
+ navigateTo(getNextStepRoute(route, options));
170
+ }
171
+
172
+ export function previousSlide(route: Route, options?: NavigationOptions): void {
173
+ navigateTo(getPreviousSlideRoute(route, options));
174
+ }
175
+
176
+ export function nextSlide(route: Route, options?: NavigationOptions): void {
177
+ navigateTo(getNextSlideRoute(route, options));
178
+ }
179
+
180
+ export function openOverview(route: Route, options?: NavigationOptions): void {
181
+ navigateTo(getOverviewRoute(route, options));
182
+ }
183
+
184
+ export function closeOverview(route: Route, options?: NavigationOptions): void {
185
+ navigateTo(getSlideRouteFromRoute(route, options));
186
+ }
187
+
188
+ export function toggleOverview(
189
+ route: Route,
190
+ options?: NavigationOptions,
191
+ ): void {
192
+ navigateTo(getToggleOverviewRoute(route, options));
193
+ }
194
+
195
+ export function openReference(route: Route): void {
196
+ if (route.view === "slide" || route.view === "overview") {
197
+ rememberSlideRoute({ view: "slide", slide: route.slide, step: route.step });
198
+ }
199
+ navigate(getReferenceRoute());
200
+ }
201
+
202
+ export function openDocsWebsite(): void {
203
+ openUrlInNewTab(getDocsWebsiteUrl());
204
+ }
205
+
206
+ export function openPresenter(route: Route): void {
207
+ if (route.view === "kit") return;
208
+ openUrlInNewTab(
209
+ getRouteUrl({ view: "presenter", slide: route.slide, step: route.step }),
210
+ );
211
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Hash-based router for Honeydeck.
3
+ *
4
+ * URL format:
5
+ * Audience: /#/slideNumber/stepIndex
6
+ * Overview: /#/overview/slideNumber/stepIndex
7
+ * Presenter: /#/presenter/slideNumber/stepIndex
8
+ * Reference: /#/theme, /#/layouts, /#/components
9
+ *
10
+ * - slideNumber is 1-based
11
+ * - stepIndex is 0-based
12
+ *
13
+ * Examples:
14
+ * /#/1/0 → slide 1, initial state
15
+ * /#/1/2 → slide 1, step 2 active
16
+ * /#/3/0 → slide 3, initial state
17
+ * /#/presenter/2/1 → presenter view, slide 2, step 1
18
+ * /#/theme → runtime reference (theme tab)
19
+ * /#/layouts → runtime reference (layouts tab)
20
+ * /#/components → runtime reference (components tab)
21
+ */
22
+
23
+ import { useEffect, useState } from "react";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Types
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export type KitTab = "theme" | "layouts" | "components";
30
+
31
+ export type Route = {
32
+ /** Which view is active. */
33
+ view: "slide" | "overview" | "presenter" | "kit";
34
+ /** 1-based slide number (unused / defaults to 1 for reference routes) */
35
+ slide: number;
36
+ /** 0-based step index (unused / defaults to 0 for reference routes) */
37
+ step: number;
38
+ /** Active tab when view === 'kit' */
39
+ kitTab?: KitTab;
40
+ };
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Pure helpers — no side effects, no browser globals
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Parse a `location.hash` string into a `Route`.
48
+ *
49
+ * Handles:
50
+ * - `""` or `"#"` → default slide route
51
+ * - `"#/1/0"` → { view: 'slide', slide: 1, step: 0 }
52
+ * - `"#/overview/2/1"` → { view: 'overview', slide: 2, step: 1 }
53
+ * - `"#/presenter/2/1"` → { view: 'presenter', slide: 2, step: 1 }
54
+ * - `"#/theme"` → { view: 'kit', kitTab: 'theme', slide: 1, step: 0 }
55
+ * - `"#/layouts"` → { view: 'kit', kitTab: 'layouts', slide: 1, step: 0 }
56
+ * - `"#/components"` → { view: 'kit', kitTab: 'components', slide: 1, step: 0 }
57
+ * - `"#/2"` → { view: 'slide', slide: 2, step: 0 }
58
+ * - NaN / negative values → clamp to valid range
59
+ */
60
+ export function parseHash(hash: string): Route {
61
+ // Strip leading '#' then split on '/'
62
+ const raw = hash.startsWith("#") ? hash.slice(1) : hash;
63
+ const parts = raw.split("/").filter(Boolean);
64
+
65
+ if (
66
+ parts[0] === "theme" ||
67
+ parts[0] === "layouts" ||
68
+ parts[0] === "components"
69
+ ) {
70
+ return { view: "kit", slide: 1, step: 0, kitTab: parts[0] };
71
+ }
72
+
73
+ // Overview route: /overview/slide/step
74
+ if (parts[0] === "overview") {
75
+ const rawSlide = parseInt(parts[1] ?? "1", 10);
76
+ const rawStep = parseInt(parts[2] ?? "0", 10);
77
+ return {
78
+ view: "overview",
79
+ slide: Number.isNaN(rawSlide) || rawSlide < 1 ? 1 : rawSlide,
80
+ step: Number.isNaN(rawStep) || rawStep < 0 ? 0 : rawStep,
81
+ };
82
+ }
83
+
84
+ // Presenter route: /presenter/slide/step
85
+ if (parts[0] === "presenter") {
86
+ const rawSlide = parseInt(parts[1] ?? "1", 10);
87
+ const rawStep = parseInt(parts[2] ?? "0", 10);
88
+ return {
89
+ view: "presenter",
90
+ slide: Number.isNaN(rawSlide) || rawSlide < 1 ? 1 : rawSlide,
91
+ step: Number.isNaN(rawStep) || rawStep < 0 ? 0 : rawStep,
92
+ };
93
+ }
94
+
95
+ // Regular slide route: /slide/step
96
+ const rawSlide = parseInt(parts[0] ?? "1", 10);
97
+ const rawStep = parseInt(parts[1] ?? "0", 10);
98
+ return {
99
+ view: "slide",
100
+ slide: Number.isNaN(rawSlide) || rawSlide < 1 ? 1 : rawSlide,
101
+ step: Number.isNaN(rawStep) || rawStep < 0 ? 0 : rawStep,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Convert a `Route` into a hash string suitable for `location.hash`.
107
+ *
108
+ * @example serializeRoute({ view: 'slide', slide: 2, step: 3 }) → '#/2/3'
109
+ * @example serializeRoute({ view: 'presenter', slide: 2, step: 3 }) → '#/presenter/2/3'
110
+ */
111
+ export function serializeRoute(route: Route): string {
112
+ if (route.view === "kit") {
113
+ if (route.kitTab === "theme") return "#/theme";
114
+ if (route.kitTab === "layouts") return "#/layouts";
115
+ if (route.kitTab === "components") return "#/components";
116
+ return "#/theme";
117
+ }
118
+ if (route.view === "overview") {
119
+ return `#/overview/${route.slide}/${route.step}`;
120
+ }
121
+ if (route.view === "presenter") {
122
+ return `#/presenter/${route.slide}/${route.step}`;
123
+ }
124
+ return `#/${route.slide}/${route.step}`;
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Browser-side API
129
+ // ---------------------------------------------------------------------------
130
+
131
+ /**
132
+ * Navigate to a route by updating `location.hash`.
133
+ * Triggers a `hashchange` event picked up by `useRoute()`.
134
+ */
135
+ export function navigate(route: Route): void {
136
+ location.hash = serializeRoute(route);
137
+ }
138
+
139
+ /**
140
+ * React hook that returns the current route and re-renders on navigation.
141
+ *
142
+ * Uses the `hashchange` window event as the reactive signal.
143
+ * Initial value is parsed from `location.hash` synchronously, so
144
+ * server-side rendering or test environments without `location` will need
145
+ * the pure helpers instead.
146
+ */
147
+ export function useRoute(): Route {
148
+ const [route, setRoute] = useState<Route>(() => parseHash(location.hash));
149
+
150
+ useEffect(() => {
151
+ const handler = () => setRoute(parseHash(location.hash));
152
+ window.addEventListener("hashchange", handler);
153
+ return () => window.removeEventListener("hashchange", handler);
154
+ }, []);
155
+
156
+ return route;
157
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Shared slide data for Honeydeck runtime.
3
+ *
4
+ * Extracts and exposes all slide metadata from `virtual:honeydeck/slides` in a
5
+ * single place so that Deck, PresenterView, and OverviewView can all import
6
+ * from here instead of duplicating the assembly logic.
7
+ *
8
+ * Also exports `resolveLayout` — the layout-lookup helper with dev-mode
9
+ * fallback warning.
10
+ */
11
+
12
+ import { config } from "virtual:honeydeck/config";
13
+ import { layoutMap } from "virtual:honeydeck/layouts";
14
+ import * as slideModules from "virtual:honeydeck/slides";
15
+ import type { ComponentType } from "react";
16
+ import { parseAspectRatio } from "./aspectRatio.ts";
17
+ import type { LayoutProps } from "./types.ts";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Constants — derived from config.aspectRatio (default 16:9 → 1920×1080)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const _dimensions = parseAspectRatio(config.aspectRatio);
24
+ export const BASE_WIDTH: number = _dimensions.width;
25
+ export const BASE_HEIGHT: number = _dimensions.height;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export type SlideData = {
32
+ /** Stable slide id derived from the virtual module export name. */
33
+ id: string;
34
+ /** Compiled slide React component. */
35
+ Component: ComponentType;
36
+ /** Number of timeline steps on this slide. */
37
+ stepCount: number;
38
+ /** Plain-text content of the extracted h1. */
39
+ title: string;
40
+ /** Parsed YAML frontmatter. */
41
+ frontmatter: Record<string, unknown>;
42
+ /** Layout name from frontmatter.layout, or 'Default' when absent. */
43
+ layoutName: string;
44
+ };
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Assembly
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const { slideCount } = slideModules;
51
+ const _allExports = slideModules as unknown as Record<string, unknown>;
52
+
53
+ function buildSlideData(): SlideData[] {
54
+ return Array.from({ length: slideCount }, (_, i) => {
55
+ const Component = _allExports[`Slide${i}`] as ComponentType | undefined;
56
+ if (!Component) {
57
+ throw new Error(
58
+ `[honeydeck] Slide${i} not found in virtual:honeydeck/slides — ` +
59
+ `deck reports slideCount=${slideCount}. ` +
60
+ `This is a bug in the virtual module plugin.`,
61
+ );
62
+ }
63
+ return {
64
+ id: `Slide${i}`,
65
+ Component,
66
+ stepCount: (_allExports[`stepCount${i}`] as number | undefined) ?? 0,
67
+ title: (_allExports[`slideTitle${i}`] as string | undefined) ?? "",
68
+ frontmatter:
69
+ (_allExports[`slideFrontmatter${i}`] as
70
+ | Record<string, unknown>
71
+ | undefined) ?? {},
72
+ layoutName:
73
+ (_allExports[`slideLayout${i}`] as string | undefined) ||
74
+ (config.defaultLayout as string | undefined) ||
75
+ "Default",
76
+ };
77
+ });
78
+ }
79
+
80
+ /** All slide data in deck order. Singleton — assembled once at import time. */
81
+ export const slideData: SlideData[] = buildSlideData();
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Layout resolution
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * Look up a layout component by name.
89
+ * Falls back to Default with a console warning in dev when the name isn't
90
+ * registered. Build/PDF hard-error behavior is enforced when Vite runs in
91
+ * production mode.
92
+ */
93
+ export function resolveLayout(
94
+ layoutName: string,
95
+ ): ComponentType<LayoutProps<Record<string, unknown>>> {
96
+ const availableLayouts = Object.keys(layoutMap);
97
+ const layout = layoutMap[layoutName];
98
+ if (layout)
99
+ return layout as ComponentType<LayoutProps<Record<string, unknown>>>;
100
+
101
+ const fallbackName =
102
+ (config.defaultLayout as string | undefined) || "Default";
103
+ const fallbackEntry = layoutMap[fallbackName]
104
+ ? { name: fallbackName, Component: layoutMap[fallbackName] }
105
+ : layoutMap.Default
106
+ ? { name: "Default", Component: layoutMap.Default }
107
+ : availableLayouts
108
+ .map((name) => ({ name, Component: layoutMap[name] }))
109
+ .find((entry) => entry.Component);
110
+ const fallback = fallbackEntry?.Component;
111
+
112
+ if (layoutName) {
113
+ const message =
114
+ `Layout "${layoutName}" not found in layout map.\n` +
115
+ `Available layouts: ${availableLayouts.join(", ") || "(none)"}`;
116
+
117
+ const isProduction =
118
+ (import.meta as unknown as { env?: { PROD?: boolean } }).env?.PROD ??
119
+ false;
120
+ if (isProduction) {
121
+ throw new Error(message);
122
+ }
123
+
124
+ console.warn(
125
+ `[honeydeck] ⚠️ Layout "${layoutName}" not found. Falling back to ${fallbackEntry?.name ?? "nothing"}. ` +
126
+ `Available: ${availableLayouts.join(", ") || "(none)"}`,
127
+ );
128
+ }
129
+
130
+ if (!fallback) {
131
+ throw new Error(
132
+ "[honeydeck] No layouts are available in the active layout map.",
133
+ );
134
+ }
135
+
136
+ return fallback as ComponentType<LayoutProps<Record<string, unknown>>>;
137
+ }