@honeydeck/honeydeck 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +25 -0
- package/DEVELOPMENT.md +522 -0
- package/LICENSE +21 -0
- package/Readme.md +49 -0
- package/SPEC.md +88 -0
- package/docs/components.md +63 -0
- package/docs/configuration.md +91 -0
- package/docs/getting-started.md +116 -0
- package/docs/kit-authoring.md +207 -0
- package/docs/kits.md +387 -0
- package/docs/local-development.md +95 -0
- package/docs/mermaid.md +198 -0
- package/docs/mobile.md +108 -0
- package/docs/navigation.md +93 -0
- package/docs/next-steps.md +377 -0
- package/docs/pdf-export.md +91 -0
- package/docs/presenter-mode.md +104 -0
- package/docs/slides.md +130 -0
- package/docs/slidev-migration.md +42 -0
- package/docs/steps-and-reveals.md +171 -0
- package/package.json +134 -0
- package/skills/SPEC.md +21 -0
- package/skills/honeydeck/SKILL.md +65 -0
- package/skills/presentation-writing/SKILL.md +75 -0
- package/skills/slidev-migration/SKILL.md +153 -0
- package/src/SPEC.md +89 -0
- package/src/assets.d.ts +30 -0
- package/src/cli/SPEC.md +230 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/banner.ts +9 -0
- package/src/cli/bin.js +5 -0
- package/src/cli/build.ts +229 -0
- package/src/cli/deck-path.ts +32 -0
- package/src/cli/dev.ts +263 -0
- package/src/cli/index.ts +126 -0
- package/src/cli/init.ts +369 -0
- package/src/cli/pdf.ts +923 -0
- package/src/cli/skill.ts +75 -0
- package/src/cli/templates/SPEC.md +70 -0
- package/src/cli/templates/deck-mdx.ts +15 -0
- package/src/cli/templates/package-json.ts +36 -0
- package/src/cli/templates/sparkle-button.ts +15 -0
- package/src/cli/templates/starter/components/SparkleButton.tsx +84 -0
- package/src/cli/templates/starter/deck.mdx +153 -0
- package/src/cli/templates/starter/styles.css +14 -0
- package/src/cli/templates/styles-css.ts +14 -0
- package/src/defaults.ts +1 -0
- package/src/layouts/ColorModeImage.tsx +55 -0
- package/src/layouts/SPEC.md +393 -0
- package/src/layouts/SlideFrame.tsx +48 -0
- package/src/layouts/bee/Blank.tsx +12 -0
- package/src/layouts/bee/Cover.tsx +70 -0
- package/src/layouts/bee/Default.tsx +42 -0
- package/src/layouts/bee/Image/Image.tsx +151 -0
- package/src/layouts/bee/Image/placeholder-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical.webp +0 -0
- package/src/layouts/bee/Image/placeholder.webp +0 -0
- package/src/layouts/bee/ImageLeft.tsx +27 -0
- package/src/layouts/bee/ImageRight.tsx +27 -0
- package/src/layouts/bee/ImageSide.tsx +107 -0
- package/src/layouts/bee/Section.tsx +40 -0
- package/src/layouts/bee/TwoCol.tsx +108 -0
- package/src/layouts/bee/index.ts +40 -0
- package/src/layouts/clean/Blank.tsx +12 -0
- package/src/layouts/clean/Cover.tsx +58 -0
- package/src/layouts/clean/Default.tsx +33 -0
- package/src/layouts/clean/Image/Image.tsx +103 -0
- package/src/layouts/clean/ImageLeft.tsx +27 -0
- package/src/layouts/clean/ImageRight.tsx +27 -0
- package/src/layouts/clean/ImageSide.tsx +113 -0
- package/src/layouts/clean/Section.tsx +35 -0
- package/src/layouts/clean/TwoCol.tsx +63 -0
- package/src/layouts/clean/index.ts +40 -0
- package/src/layouts/index.ts +60 -0
- package/src/layouts/placeholders.ts +9 -0
- package/src/layouts/utils.ts +13 -0
- package/src/remark/SPEC.md +49 -0
- package/src/remark/h1-extract.ts +124 -0
- package/src/remark/index.ts +4 -0
- package/src/remark/shiki-code-blocks.ts +325 -0
- package/src/remark/step-numbering.ts +412 -0
- package/src/runtime/Deck.tsx +533 -0
- package/src/runtime/SPEC.md +256 -0
- package/src/runtime/SlideCanvas.tsx +95 -0
- package/src/runtime/TimelineContext.tsx +122 -0
- package/src/runtime/app-shell/index.html +31 -0
- package/src/runtime/app-shell/main.tsx +42 -0
- package/src/runtime/aspectRatio.ts +34 -0
- package/src/runtime/colorMode.ts +23 -0
- package/src/runtime/components/BrowserFrame.tsx +233 -0
- package/src/runtime/components/Button.tsx +57 -0
- package/src/runtime/components/CodeBlock.tsx +210 -0
- package/src/runtime/components/ColorModeCycleButton.tsx +59 -0
- package/src/runtime/components/ErrorBoundary.tsx +125 -0
- package/src/runtime/components/Keyboard.tsx +87 -0
- package/src/runtime/components/ListStyle.tsx +203 -0
- package/src/runtime/components/NavBar.tsx +223 -0
- package/src/runtime/components/NavBarButton.tsx +47 -0
- package/src/runtime/components/NavBarDivider.tsx +3 -0
- package/src/runtime/components/Notes.tsx +171 -0
- package/src/runtime/components/Reveal.tsx +82 -0
- package/src/runtime/components/RevealGroup.tsx +193 -0
- package/src/runtime/components/SPEC.md +263 -0
- package/src/runtime/components/SlideNumberBadge.tsx +11 -0
- package/src/runtime/components/TimelineSteps.tsx +115 -0
- package/src/runtime/components/index.ts +55 -0
- package/src/runtime/index.ts +42 -0
- package/src/runtime/inputOwnership.ts +68 -0
- package/src/runtime/keyboardTarget.ts +7 -0
- package/src/runtime/lastSlideRoute.ts +56 -0
- package/src/runtime/navigation.ts +211 -0
- package/src/runtime/router.ts +157 -0
- package/src/runtime/slideData.ts +137 -0
- package/src/runtime/sync.ts +267 -0
- package/src/runtime/types.ts +182 -0
- package/src/runtime/useKeyboardNav.ts +138 -0
- package/src/runtime/useSwipeNav.ts +257 -0
- package/src/runtime/views/DocsView.tsx +74 -0
- package/src/runtime/views/OverviewView.tsx +386 -0
- package/src/runtime/views/PresenterNotesPanel.tsx +76 -0
- package/src/runtime/views/PresenterView.tsx +340 -0
- package/src/runtime/views/SPEC.md +152 -0
- package/src/runtime/views/docs/ComponentsTab.tsx +178 -0
- package/src/runtime/views/docs/DocsHeader.tsx +101 -0
- package/src/runtime/views/docs/Intro.tsx +20 -0
- package/src/runtime/views/docs/LayoutsTab.tsx +324 -0
- package/src/runtime/views/docs/ThemeTab.tsx +110 -0
- package/src/runtime/views/index.ts +7 -0
- package/src/runtime/views/overviewGrid.ts +106 -0
- package/src/runtime/views/presenterPreview.ts +27 -0
- package/src/runtime/virtual-modules.d.ts +98 -0
- package/src/theme/SPEC.md +179 -0
- package/src/theme/base.css +623 -0
- package/src/theme/bee.css +35 -0
- package/src/theme/clean.css +38 -0
- package/src/vite-plugin/SPEC.md +114 -0
- package/src/vite-plugin/component-doc-crawler.ts +350 -0
- package/src/vite-plugin/deck-loader.ts +148 -0
- package/src/vite-plugin/index.ts +373 -0
- package/src/vite-plugin/layout-demo-crawler.ts +802 -0
- package/src/vite-plugin/splitter.ts +353 -0
- package/src/vite-plugin/token-manifest.ts +163 -0
- package/src/vite-plugin/virtual-modules.ts +587 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Honeydeck Runtime Specification
|
|
2
|
+
|
|
3
|
+
> Observable behavior for timeline state, slide navigation, SPA behavior, and runtime errors.
|
|
4
|
+
|
|
5
|
+
## Timeline & Steps
|
|
6
|
+
|
|
7
|
+
### Concept
|
|
8
|
+
|
|
9
|
+
The **timeline** is a first-class Honeydeck concept. Each slide has a local timeline of steps. Code walkthroughs, reveal components, and statically registered custom component steps all hook into the same timeline.
|
|
10
|
+
|
|
11
|
+
Fenced code blocks join the timeline when their metadata uses `{group|group}` step syntax. The first code group is the block's baseline active highlight and consumes no timeline step. Each later group consumes one timeline step.
|
|
12
|
+
|
|
13
|
+
### Timeline State
|
|
14
|
+
|
|
15
|
+
- Initial state: `stepIndex = 0` (no reveal or custom step content active)
|
|
16
|
+
- Stepped code blocks show their first metadata group immediately as their baseline state whenever the block is visible
|
|
17
|
+
- First reveal/custom timeline entry activates at `stepIndex = 1`
|
|
18
|
+
- For code walkthroughs, the second and later metadata groups activate at their assigned timeline steps
|
|
19
|
+
- Timeline entries are determined by document order (top-to-bottom)
|
|
20
|
+
- Compiler-injected or manually-authored `<Reveal at={n}>` is preserved and excluded from automatic step counting
|
|
21
|
+
- Timeline entries are flat within each slide, even when authored with nested
|
|
22
|
+
components. A parent `<Reveal>` or `<RevealGroup>` target consumes its step
|
|
23
|
+
first, then any nested `<Reveal>`, `<RevealGroup>`, or code walkthrough steps
|
|
24
|
+
inside it are appended to the same slide timeline before the next sibling
|
|
25
|
+
timeline target.
|
|
26
|
+
- Custom React components can participate by wrapping their usage in
|
|
27
|
+
`<TimelineSteps steps={N}>`. The wrapper must be visible in slide MDX so the
|
|
28
|
+
compiler can reserve those steps at build time.
|
|
29
|
+
- Timeline context exposes `isPdfFinalRender` for components that need a special
|
|
30
|
+
all-open/all-visible rendering when PDF export captures one final-state page
|
|
31
|
+
per slide. This flag is false during normal presentation mode and during
|
|
32
|
+
`pdfSteps: all` step-by-step PDF export.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
|
|
36
|
+
````mdx
|
|
37
|
+
<Reveal>
|
|
38
|
+
Parent
|
|
39
|
+
<Reveal>Nested detail</Reveal>
|
|
40
|
+
```ts {1|2}
|
|
41
|
+
const a = 1
|
|
42
|
+
const b = 2
|
|
43
|
+
```
|
|
44
|
+
</Reveal>
|
|
45
|
+
````
|
|
46
|
+
|
|
47
|
+
Timeline:
|
|
48
|
+
|
|
49
|
+
1. Parent reveal appears; nested code is visible with line 1 highlighted
|
|
50
|
+
2. Nested detail appears
|
|
51
|
+
3. Code highlights line 2
|
|
52
|
+
|
|
53
|
+
### Navigation
|
|
54
|
+
|
|
55
|
+
Steps and slides have separate navigation axes:
|
|
56
|
+
|
|
57
|
+
| Input | Action |
|
|
58
|
+
|-------|--------|
|
|
59
|
+
| `→` / `d` | Next step; if no next step, next slide at step 0 |
|
|
60
|
+
| `←` / `a` | Previous step; if at step 0, previous slide at final step |
|
|
61
|
+
| `↓` / `s` | Next slide directly (skip remaining steps) |
|
|
62
|
+
| `↑` / `w` | Previous slide directly |
|
|
63
|
+
|
|
64
|
+
Horizontal = detailed progression through timeline. Vertical = jump between slides.
|
|
65
|
+
|
|
66
|
+
### URL State
|
|
67
|
+
|
|
68
|
+
Hash-based routing preserves position and navigable application views:
|
|
69
|
+
|
|
70
|
+
```txt
|
|
71
|
+
/#/slideNumber/stepIndex
|
|
72
|
+
/#/slideNumber
|
|
73
|
+
/#/overview/slideNumber/stepIndex
|
|
74
|
+
/#/presenter/slideNumber/stepIndex
|
|
75
|
+
/#/theme
|
|
76
|
+
/#/layouts
|
|
77
|
+
/#/components
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- Slide number is 1-based
|
|
81
|
+
- Step index is 0-based
|
|
82
|
+
- Missing step defaults to `0`
|
|
83
|
+
- Invalid or negative slide/step values clamp to slide 1 / step 0; out-of-range positive steps are accepted as “past final step”
|
|
84
|
+
- Overview routes encode the slide to scroll into view and the remembered step
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
|
|
88
|
+
```txt
|
|
89
|
+
/#/1/0 → slide 1, initial state
|
|
90
|
+
/#/1/2 → slide 1, step 2
|
|
91
|
+
/#/3/0 → slide 3, initial state
|
|
92
|
+
/#/overview/3/1 → overview at slide 3, remembering step 1
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Reloading or sharing the URL restores both slide and step. Slide numbers beyond the deck length clamp to the final slide. Reloading or sharing an overview URL restores overview with the encoded slide scrolled into view.
|
|
96
|
+
|
|
97
|
+
Honeydeck navigation is browser navigation. Every slide/step navigation, overview entry, overview slide selection, and reference route navigation creates a browser history entry. Browser Back therefore moves through the user's actual Honeydeck navigation path. Entering overview from `/#/3/1` pushes `/#/overview/3/1`; browser Back from overview returns to `/#/3/1` and closes overview.
|
|
98
|
+
|
|
99
|
+
Reference page routes intentionally do not encode slide or step. During one browser session, Honeydeck remembers the last visited audience slide route (`slide` view with slide number and step index). Entering or navigating within reference pages (`/#/theme`, `/#/layouts`, `/#/components`) must not reset that remembered route. A reference page "Back to slides" action returns to the remembered slide and step. If no previous audience slide route is known, such as on a direct load of `/#/theme`, it falls back to `/#/1/0`.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Navigation
|
|
106
|
+
|
|
107
|
+
### Keyboard Shortcuts
|
|
108
|
+
|
|
109
|
+
| Shortcut | Action |
|
|
110
|
+
|----------|--------|
|
|
111
|
+
| `→` / `d` | Next step (crosses slide boundary); in overview, timeline navigation is disabled: arrow keys move overview selection and WASD are no-ops |
|
|
112
|
+
| `←` / `a` | Previous step (crosses slide boundary); in overview, timeline navigation is disabled: arrow keys move overview selection and WASD are no-ops |
|
|
113
|
+
| `↓` / `s` | Next slide; in overview, timeline navigation is disabled: arrow keys move overview selection and WASD are no-ops |
|
|
114
|
+
| `↑` / `w` | Previous slide; in overview, timeline navigation is disabled: arrow keys move overview selection and WASD are no-ops |
|
|
115
|
+
| `o` | Toggle overview mode |
|
|
116
|
+
| `p` | Open presenter mode (new window) |
|
|
117
|
+
| `f` | Toggle fullscreen |
|
|
118
|
+
| `Escape` | Exit overview; in reference pages, return to slides; browser-native Escape handles fullscreen exit |
|
|
119
|
+
|
|
120
|
+
### Navigation UI Bar
|
|
121
|
+
|
|
122
|
+
Shown in normal slide view only (not presenter/reference views).
|
|
123
|
+
|
|
124
|
+
- Positioned bottom-left
|
|
125
|
+
- **Hidden by default** on desktop — appears on cursor hover near bottom edge
|
|
126
|
+
- **Always visible** on mobile/tablet portrait
|
|
127
|
+
- **Hidden by default** on mobile/tablet landscape — appears when the center tap zone is tapped, fades after roughly 3 seconds of idle time, and stays visible while being interacted with
|
|
128
|
+
- Contains:
|
|
129
|
+
- Current slide number
|
|
130
|
+
- Navigation arrows (step left/right)
|
|
131
|
+
- Overview mode button
|
|
132
|
+
- Layouts reference button (opens `/#/layouts` while preserving the current slide/step as the return target)
|
|
133
|
+
- Docs website button (opens `https://honeydeck.dev` in a new tab)
|
|
134
|
+
- Presenter mode button
|
|
135
|
+
- Fullscreen button
|
|
136
|
+
- Mobile text selection toggle (off by default, enables selecting slide content when needed)
|
|
137
|
+
- Color mode switch (system → light → dark → system)
|
|
138
|
+
- All icon-only controls expose explicit accessible names via `aria-label` and matching hover titles
|
|
139
|
+
- Zoom reset button when slide zoom is greater than `1`
|
|
140
|
+
|
|
141
|
+
When `showSlideNumbers: true`, normal slide view also renders the current slide number as a single global viewer overlay aligned to the bottom-right corner of the slide canvas. The slide number overlay is not part of individual slide content and does not participate in slide transition animations. It renders only the current slide number, not a total count, as larger unobtrusive text without a background container and uses the deck theme's foreground color.
|
|
142
|
+
|
|
143
|
+
### Input Ownership
|
|
144
|
+
|
|
145
|
+
Navigation input is routed through a shared command abstraction used by audience, presenter, overview, reference pages, keyboard, touch, and button controls. Input handlers call semantic commands such as `nextStep`, `previousStep`, `nextSlide`, `previousSlide`, `openOverview`, `closeOverview`, `openReference`, `openPresenter`, `toggleNavBar`, and `resetZoom` instead of directly mutating route state.
|
|
146
|
+
|
|
147
|
+
Wheel and trackpad scroll never navigate slides. Scrollable content owns scroll input. If a touch/pointer gesture starts inside an interactive element (`button`, `a`, `input`, `textarea`, `select`, etc.), an element marked `data-honeydeck-no-swipe`, or an auto-detected scrollable ancestor before the deck/slide root, Honeydeck does not claim swipe navigation for that gesture. A scrollable ancestor is an element whose scroll dimensions exceed client dimensions and whose computed overflow allows `auto` or `scroll` on either axis. Scroll-owned gestures never hand off to slide navigation at scroll boundaries.
|
|
148
|
+
|
|
149
|
+
### Mobile/Touch
|
|
150
|
+
|
|
151
|
+
Normal slide view uses a five-zone tap model:
|
|
152
|
+
|
|
153
|
+
```txt
|
|
154
|
+
┌───────────────┐
|
|
155
|
+
│ Previous slide│
|
|
156
|
+
├─────┬───┬─────┤
|
|
157
|
+
│Prev │Nav│Next │
|
|
158
|
+
│step │bar│step │
|
|
159
|
+
├─────┴───┴─────┤
|
|
160
|
+
│ Next slide │
|
|
161
|
+
└───────────────┘
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- Left center zone → previous step
|
|
165
|
+
- Right center zone → next step
|
|
166
|
+
- Top zone → previous slide
|
|
167
|
+
- Bottom zone → next slide
|
|
168
|
+
- Center zone → toggle the navigation bar and never navigate slides
|
|
169
|
+
- Swipe left → next step (crosses slide boundary)
|
|
170
|
+
- Swipe right → previous step (crosses slide boundary)
|
|
171
|
+
- Swipe up → next slide
|
|
172
|
+
- Swipe down → previous slide
|
|
173
|
+
- Swipes use dominant axis and require roughly 50px movement
|
|
174
|
+
- Pinch gestures and slide zoom take precedence over tap and swipe navigation
|
|
175
|
+
- Slide content is not text-selectable by default on touch/mobile devices so taps and drags remain presentation controls; the navigation bar provides a mobile toggle to temporarily enable slide text selection. While selection mode is active, mobile tap/swipe/pinch presentation gestures pause and the navigation bar remains visible so the user can turn selection mode off.
|
|
176
|
+
|
|
177
|
+
Slide zoom is Honeydeck-controlled rather than browser page zoom:
|
|
178
|
+
|
|
179
|
+
- Pinch outward in normal slide view zooms the current slide content for readability
|
|
180
|
+
- Zoom applies a scale plus pan transform to the slide canvas while keeping browser layout stable
|
|
181
|
+
- Minimum zoom is `1`, maximum zoom is `5`, and double-tap may toggle between `1` and a readable preset around `2`
|
|
182
|
+
- When zoom is greater than `1`, dragging pans the slide, tap-zone slide/step navigation and swipe navigation are disabled, center tap still toggles the navigation bar, and nav bar buttons still work
|
|
183
|
+
- Navigating to another slide or step resets zoom to `1`
|
|
184
|
+
- Pinching back below roughly `1.05` resets zoom to `1`
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Build & Runtime Behavior
|
|
191
|
+
|
|
192
|
+
### Build System
|
|
193
|
+
|
|
194
|
+
Honeydeck owns the build/dev configuration. Users do not provide `index.html` or `vite.config.ts`.
|
|
195
|
+
|
|
196
|
+
Observable build/dev behavior:
|
|
197
|
+
|
|
198
|
+
- `honeydeck dev` starts a hot-reloading development server.
|
|
199
|
+
- `honeydeck build` produces a static single-page application.
|
|
200
|
+
- Project `public/` assets are served/copied at the web root.
|
|
201
|
+
- Project-local CSS, React components, layout maps, and static image imports work from the selected deck root.
|
|
202
|
+
- Built decks use hash-based routes and can be deployed to static hosts without server-side routing.
|
|
203
|
+
- The app shell applies the initial effective `data-honeydeck-color-mode` to `<html>` before mounting React so first-render browser defaults and generated assets match the deck mode.
|
|
204
|
+
|
|
205
|
+
### SPA Architecture
|
|
206
|
+
|
|
207
|
+
Build output is a single-page application. The app preserves client-side slide transitions, presenter sync, and step timeline state.
|
|
208
|
+
|
|
209
|
+
### Slide Transitions
|
|
210
|
+
|
|
211
|
+
Honeydeck includes a single subtle crossfade (~200ms) between slides. Disabled with:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
transition: false
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Aspect Ratio
|
|
218
|
+
|
|
219
|
+
Slides render at a fixed 1920px logical width. Height is derived from deck-level `aspectRatio` when it is a string ratio matching `N:N` (default `16:9` → `1080`, `4:3` → `1440`, etc.). Invalid or missing ratios fall back to `16:9`.
|
|
220
|
+
|
|
221
|
+
The slide canvas scales uniformly to fit the available stage. There is no viewport inset token: slides fill the viewport as much as their aspect ratio allows. Any remaining letterbox/pillarbox area is black.
|
|
222
|
+
|
|
223
|
+
PDF pages use the same 1920px-wide dimensions derived from deck-level `aspectRatio`, so exported pages match the deck ratio without stretching or letterbox/pillarbox space. During crossfades, Honeydeck paints a themed `bg-background` backdrop at the scaled slide size behind the slides to avoid flicker.
|
|
224
|
+
|
|
225
|
+
No per-slide ratio.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Error Handling
|
|
232
|
+
|
|
233
|
+
### Runtime Slide Errors
|
|
234
|
+
|
|
235
|
+
Each slide is wrapped in a per-slide React `ErrorBoundary` in the main deck. A render error in one slide does not crash the whole presentation; other slides remain navigable.
|
|
236
|
+
|
|
237
|
+
- Dev mode: shows a full-slide error view with slide number, error message, stack trace, component stack, and `BombIcon`.
|
|
238
|
+
- Production mode: shows a minimal “Something went wrong” fallback with the slide number and `AlertTriangleIcon`.
|
|
239
|
+
- Errors are also logged to `console.error`.
|
|
240
|
+
|
|
241
|
+
`SlideCanvas` instances used by presenter/overview previews are not wrapped in this per-slide boundary.
|
|
242
|
+
|
|
243
|
+
### Invalid Layout Name
|
|
244
|
+
|
|
245
|
+
**Dev mode:** Logs a browser `console.warn` and falls back to the configured `defaultLayout`, then `Default`, then the first available layout.
|
|
246
|
+
|
|
247
|
+
```txt
|
|
248
|
+
Layout "Covr" not found in layout map. Falling back to "Default". Available layouts: Blank, Default, Cover, Section, TwoCol, Image, ImageLeft, ImageRight
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Build/PDF:** Production rendering throws a hard error with available layout names. The error does not include a slide number.
|
|
252
|
+
|
|
253
|
+
```txt
|
|
254
|
+
Error: Layout "Covr" not found in layout map.
|
|
255
|
+
Available layouts: Blank, Default, Cover, Section, TwoCol, Image, ImageLeft, ImageRight
|
|
256
|
+
```
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SlideCanvas — renders a single slide at an arbitrary scale.
|
|
3
|
+
*
|
|
4
|
+
* Used by Deck (full-size, scale from viewport), PresenterView (medium
|
|
5
|
+
* previews), and OverviewView (thumbnail grid).
|
|
6
|
+
*
|
|
7
|
+
* The canvas is always 1920 × 1080 px internally, then scaled down via
|
|
8
|
+
* `transform: scale()` with `transformOrigin: 'top left'`. The wrapper
|
|
9
|
+
* div shrinks to the visual size (`BASE_WIDTH * scale × BASE_HEIGHT * scale`)
|
|
10
|
+
* so that surrounding layout can measure and position it correctly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
BASE_HEIGHT,
|
|
15
|
+
BASE_WIDTH,
|
|
16
|
+
resolveLayout,
|
|
17
|
+
slideData,
|
|
18
|
+
} from "./slideData.ts";
|
|
19
|
+
import { TimelineProvider } from "./TimelineContext.tsx";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export type SlideCanvasProps = {
|
|
26
|
+
/** 0-based slide index. */
|
|
27
|
+
slideIndex: number;
|
|
28
|
+
/** Current step index for the timeline. */
|
|
29
|
+
stepIndex: number;
|
|
30
|
+
/**
|
|
31
|
+
* Uniform scale factor. `1` = full 1920 × 1080.
|
|
32
|
+
* The wrapper div will have dimensions `BASE_WIDTH * scale × BASE_HEIGHT * scale`.
|
|
33
|
+
*/
|
|
34
|
+
scale: number;
|
|
35
|
+
/** Optional extra styles applied to the outer wrapper. */
|
|
36
|
+
style?: React.CSSProperties;
|
|
37
|
+
/** Optional className applied to the outer wrapper. */
|
|
38
|
+
className?: string;
|
|
39
|
+
/** Show future reveal steps as muted previews instead of hiding them. */
|
|
40
|
+
showFutureSteps?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Component
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export function SlideCanvas({
|
|
48
|
+
slideIndex,
|
|
49
|
+
stepIndex,
|
|
50
|
+
scale,
|
|
51
|
+
style,
|
|
52
|
+
className,
|
|
53
|
+
showFutureSteps = false,
|
|
54
|
+
}: SlideCanvasProps) {
|
|
55
|
+
const data = slideData[slideIndex];
|
|
56
|
+
if (!data) return null;
|
|
57
|
+
|
|
58
|
+
const { Component, stepCount, title, frontmatter, layoutName } = data;
|
|
59
|
+
const LayoutComponent = resolveLayout(layoutName);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={`relative overflow-hidden shrink-0 ${className ?? ""}`}
|
|
64
|
+
style={{
|
|
65
|
+
width: BASE_WIDTH * scale,
|
|
66
|
+
height: BASE_HEIGHT * scale,
|
|
67
|
+
...style,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<TimelineProvider
|
|
71
|
+
stepIndex={stepIndex}
|
|
72
|
+
stepCount={stepCount}
|
|
73
|
+
showFutureSteps={showFutureSteps}
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
className="honeydeck-slide-canvas absolute top-0 left-0 overflow-hidden box-border"
|
|
77
|
+
style={{
|
|
78
|
+
width: BASE_WIDTH,
|
|
79
|
+
height: BASE_HEIGHT,
|
|
80
|
+
transform: `scale(${scale})`,
|
|
81
|
+
transformOrigin: "top left",
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<LayoutComponent
|
|
85
|
+
title={title || null}
|
|
86
|
+
frontmatter={frontmatter}
|
|
87
|
+
rawChildren={<Component />}
|
|
88
|
+
>
|
|
89
|
+
<Component />
|
|
90
|
+
</LayoutComponent>
|
|
91
|
+
</div>
|
|
92
|
+
</TimelineProvider>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline context for Honeydeck.
|
|
3
|
+
*
|
|
4
|
+
* Each slide is wrapped in a `<TimelineProvider>` that supplies the current
|
|
5
|
+
* step index (read from the route) and the total step count (derived from
|
|
6
|
+
* build-time metadata). Components like `<Reveal>` and `<CodeBlock>` read
|
|
7
|
+
* from this context to decide their visibility state.
|
|
8
|
+
*
|
|
9
|
+
* ### Step semantics
|
|
10
|
+
* - stepIndex = 0 → initial state, no step content active
|
|
11
|
+
* - stepIndex = n → step n is the latest active step (cumulative reveals)
|
|
12
|
+
* - stepCount → total number of steps for the slide
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createContext, type ReactNode, useContext } from "react";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Context value shape
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export type TimelineContextValue = {
|
|
22
|
+
/** Current step index (0 = initial, no reveals active). */
|
|
23
|
+
stepIndex: number;
|
|
24
|
+
/**
|
|
25
|
+
* Total number of timeline steps for this slide.
|
|
26
|
+
* Used by keyboard navigation to detect end-of-slide.
|
|
27
|
+
*/
|
|
28
|
+
stepCount: number;
|
|
29
|
+
/** Show future reveal steps as muted previews instead of hiding them. */
|
|
30
|
+
showFutureSteps: boolean;
|
|
31
|
+
/** Opacity used when future reveal steps are previewed. */
|
|
32
|
+
futureStepOpacity: number;
|
|
33
|
+
/**
|
|
34
|
+
* True while `honeydeck pdf` is capturing one final-state page per slide.
|
|
35
|
+
* False for normal presentation mode and step-by-step PDF export.
|
|
36
|
+
*/
|
|
37
|
+
isPdfFinalRender: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Context
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const TimelineContext = createContext<TimelineContextValue>({
|
|
45
|
+
stepIndex: 0,
|
|
46
|
+
stepCount: 0,
|
|
47
|
+
showFutureSteps: false,
|
|
48
|
+
futureStepOpacity: 0.28,
|
|
49
|
+
isPdfFinalRender: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Provider
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
export type TimelineProviderProps = {
|
|
57
|
+
/** Step index driven by the current route (0-based). */
|
|
58
|
+
stepIndex: number;
|
|
59
|
+
/** Total step count for this slide, from build-time metadata. */
|
|
60
|
+
stepCount: number;
|
|
61
|
+
/** Show future reveal steps as muted previews instead of hiding them. */
|
|
62
|
+
showFutureSteps?: boolean;
|
|
63
|
+
/** Opacity used when future reveal steps are previewed. */
|
|
64
|
+
futureStepOpacity?: number;
|
|
65
|
+
/** Whether this render is a PDF final-state capture. */
|
|
66
|
+
isPdfFinalRender?: boolean;
|
|
67
|
+
children?: ReactNode;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function readPdfFinalRenderFromLocation(): boolean {
|
|
71
|
+
if (typeof window === "undefined") return false;
|
|
72
|
+
|
|
73
|
+
const params = new URLSearchParams(window.location.search);
|
|
74
|
+
return params.get("honeydeckPdfRender") === "final";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wrap a slide component in `<TimelineProvider>` to make the timeline state
|
|
79
|
+
* available to all nested `<Reveal>`, `<RevealGroup>`, and code block
|
|
80
|
+
* components.
|
|
81
|
+
*/
|
|
82
|
+
export function TimelineProvider({
|
|
83
|
+
stepIndex,
|
|
84
|
+
stepCount,
|
|
85
|
+
showFutureSteps = false,
|
|
86
|
+
futureStepOpacity = 0.28,
|
|
87
|
+
isPdfFinalRender = readPdfFinalRenderFromLocation(),
|
|
88
|
+
children,
|
|
89
|
+
}: TimelineProviderProps) {
|
|
90
|
+
return (
|
|
91
|
+
<TimelineContext.Provider
|
|
92
|
+
value={{
|
|
93
|
+
stepIndex,
|
|
94
|
+
stepCount,
|
|
95
|
+
showFutureSteps,
|
|
96
|
+
futureStepOpacity,
|
|
97
|
+
isPdfFinalRender,
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</TimelineContext.Provider>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Hook
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Read the current timeline state from within a slide.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* function Reveal({ at }) {
|
|
115
|
+
* const { stepIndex } = useTimeline();
|
|
116
|
+
* return <div style={{ opacity: stepIndex >= at ? 1 : 0 }}>{children}</div>;
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function useTimeline(): TimelineContextValue {
|
|
121
|
+
return useContext(TimelineContext);
|
|
122
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>honeydeck</title>
|
|
7
|
+
<!--
|
|
8
|
+
Minimal reset: prevent scrollbars and body margin from fighting the
|
|
9
|
+
viewport-filling Deck container.
|
|
10
|
+
-->
|
|
11
|
+
<style>
|
|
12
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
13
|
+
html, body {
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
background: #111;
|
|
20
|
+
}
|
|
21
|
+
#root {
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<div id="root"></div>
|
|
29
|
+
<script type="module" src="__HONEYDECK_MAIN_ENTRY__"></script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App shell entry point.
|
|
3
|
+
*
|
|
4
|
+
* Bootstraps React and mounts the <Deck> component into the #root element
|
|
5
|
+
* injected by index.html.
|
|
6
|
+
*
|
|
7
|
+
* This file is intentionally minimal — all presentation logic lives in Deck.
|
|
8
|
+
* The path to this file is embedded at dev-time by the `honeydeck:app-shell` Vite
|
|
9
|
+
* plugin using Vite's `/@fs/` file-system access mechanism, so it can live
|
|
10
|
+
* outside the user's project root.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { config } from "virtual:honeydeck/config";
|
|
14
|
+
import { StrictMode } from "react";
|
|
15
|
+
import { createRoot } from "react-dom/client";
|
|
16
|
+
import {
|
|
17
|
+
applyHoneydeckColorMode,
|
|
18
|
+
readSystemPrefersDark,
|
|
19
|
+
resolveEffectiveColorMode,
|
|
20
|
+
} from "../colorMode.ts";
|
|
21
|
+
import { Deck } from "../Deck.tsx";
|
|
22
|
+
|
|
23
|
+
applyHoneydeckColorMode(
|
|
24
|
+
resolveEffectiveColorMode(
|
|
25
|
+
config.colorMode as string | null | undefined,
|
|
26
|
+
readSystemPrefersDark(),
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const rootElement = document.getElementById("root");
|
|
31
|
+
|
|
32
|
+
if (!rootElement) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"[honeydeck] #root element not found — the index.html template may be corrupted.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
createRoot(rootElement).render(
|
|
39
|
+
<StrictMode>
|
|
40
|
+
<Deck />
|
|
41
|
+
</StrictMode>,
|
|
42
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const BASE_WIDTH = 1920;
|
|
2
|
+
const DEFAULT_HEIGHT = 1080;
|
|
3
|
+
|
|
4
|
+
export type DeckDimensions = {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function parseAspectRatio(ratio: unknown): DeckDimensions {
|
|
10
|
+
if (typeof ratio !== "string") {
|
|
11
|
+
return { width: BASE_WIDTH, height: DEFAULT_HEIGHT };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const match = ratio.match(/^(\d+):(\d+)$/);
|
|
15
|
+
if (!match) {
|
|
16
|
+
return { width: BASE_WIDTH, height: DEFAULT_HEIGHT };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const [, widthValue, heightValue] = match;
|
|
20
|
+
if (!widthValue || !heightValue) {
|
|
21
|
+
return { width: BASE_WIDTH, height: DEFAULT_HEIGHT };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const width = parseInt(widthValue, 10);
|
|
25
|
+
const height = parseInt(heightValue, 10);
|
|
26
|
+
if (!width || !height) {
|
|
27
|
+
return { width: BASE_WIDTH, height: DEFAULT_HEIGHT };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
width: BASE_WIDTH,
|
|
32
|
+
height: Math.round((BASE_WIDTH * height) / width),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type EffectiveColorMode = "light" | "dark";
|
|
2
|
+
export type ConfiguredColorMode =
|
|
3
|
+
| EffectiveColorMode
|
|
4
|
+
| "system"
|
|
5
|
+
| string
|
|
6
|
+
| null
|
|
7
|
+
| undefined;
|
|
8
|
+
|
|
9
|
+
export function resolveEffectiveColorMode(
|
|
10
|
+
configured: ConfiguredColorMode,
|
|
11
|
+
prefersDark: boolean,
|
|
12
|
+
): EffectiveColorMode {
|
|
13
|
+
if (configured === "light" || configured === "dark") return configured;
|
|
14
|
+
return prefersDark ? "dark" : "light";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readSystemPrefersDark(): boolean {
|
|
18
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function applyHoneydeckColorMode(mode: EffectiveColorMode): void {
|
|
22
|
+
document.documentElement.setAttribute("data-honeydeck-color-mode", mode);
|
|
23
|
+
}
|