@honeydeck/honeydeck 0.7.0 → 0.9.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 +1 -1
- package/docs/configuration.md +16 -4
- package/docs/deeper-dive.md +6 -3
- package/docs/index.json +11 -0
- package/docs/transitions.md +126 -0
- package/package.json +1 -1
- package/src/cli/templates/SPEC.md +2 -1
- package/src/cli/templates/starter/deck.mdx +22 -0
- package/src/runtime/Deck.tsx +237 -35
- package/src/runtime/SPEC.md +10 -2
- package/src/theme/base.css +85 -0
- package/src/vite-plugin/SPEC.md +7 -2
- package/src/vite-plugin/splitter.ts +2 -0
package/AGENTS.md
CHANGED
|
@@ -5,7 +5,7 @@ This package publishes the scoped public `@honeydeck/honeydeck` npm package. It
|
|
|
5
5
|
## Rules
|
|
6
6
|
|
|
7
7
|
- Keep public import paths as `@honeydeck/honeydeck/...`.
|
|
8
|
-
- `Readme.md` is the compact package README and links to the public docs site. Reader-facing docs live in `packages/docs/content/docs
|
|
8
|
+
- `Readme.md` is the compact package README and links to the public docs site. Reader-facing docs live in `packages/docs/content/docs`; update those canonical docs for user-facing behavior changes.
|
|
9
9
|
- Built-in runtime reference pages cover project-specific theme tokens, active layouts, and built-in component docs. They do not render public docs in-deck.
|
|
10
10
|
- Specs, `DEVELOPMENT.md`, and skills must remain included in npm package contents.
|
|
11
11
|
- Use suffixed `lucide-react` icon exports.
|
package/docs/configuration.md
CHANGED
|
@@ -16,7 +16,9 @@ Defined in the first frontmatter block of the deck entry file (before any slide
|
|
|
16
16
|
| `colorMode` | `"system" \| "light" \| "dark"` | `"system"` | Browser color mode |
|
|
17
17
|
| `pdfColorMode` | `"light" \| "dark"` | unset | Optional PDF color mode; when unset, falls back to pinned `colorMode`, then `light` |
|
|
18
18
|
| `pdfSteps` | `"final" \| "all"` | `"final"` | PDF includes all steps or final state |
|
|
19
|
-
| `transition` | `boolean` | `
|
|
19
|
+
| `transition` | `string \| boolean` | `fade` | Default named slide transition (`fade`, `none`, `slide-left`, or custom CSS name) |
|
|
20
|
+
| `transitionDuration` | `number` | `200` | Default slide transition duration in milliseconds |
|
|
21
|
+
| `transitionEasing` | `string` | `ease` | Default slide transition timing function |
|
|
20
22
|
| `magicCodeDuration` | `number` | `800` | Default Magic Code animation duration in milliseconds |
|
|
21
23
|
| `layouts` | `string` | `""` (built-in) | Layout map module path |
|
|
22
24
|
| `defaultLayout` | `string` | `"Default"` | Layout used when slide has no `layout:` |
|
|
@@ -46,13 +48,17 @@ When pinned, the viewer cannot switch mode from navigation controls.
|
|
|
46
48
|
|
|
47
49
|
### Transitions
|
|
48
50
|
|
|
49
|
-
A subtle
|
|
51
|
+
A subtle named `fade` transition is applied between slides by default:
|
|
50
52
|
|
|
51
53
|
```yaml
|
|
52
|
-
transition:
|
|
53
|
-
transition:
|
|
54
|
+
transition: fade # default
|
|
55
|
+
transition: none # disable
|
|
54
56
|
```
|
|
55
57
|
|
|
58
|
+
Legacy booleans still work: `transition: true` maps to `fade`, and `transition: false` maps to `none`.
|
|
59
|
+
|
|
60
|
+
For built-ins, custom CSS transitions, duration, and easing, see [Transitions](transitions.md).
|
|
61
|
+
|
|
56
62
|
### Magic Code Duration
|
|
57
63
|
|
|
58
64
|
Magic Code animations default to 800ms. Set a deck-wide default with `magicCodeDuration`:
|
|
@@ -70,6 +76,9 @@ Per-slide frontmatter (after `---`):
|
|
|
70
76
|
| Property | Type | Default | Description |
|
|
71
77
|
|----------|------|---------|-------------|
|
|
72
78
|
| `layout` | `string` | (uses `defaultLayout`) | Layout to use (PascalCase) |
|
|
79
|
+
| `transition` | `string \| boolean` | deck default | Named transition into this slide |
|
|
80
|
+
| `transitionDuration` | `number` | deck default | Transition duration into this slide in milliseconds |
|
|
81
|
+
| `transitionEasing` | `string` | deck default | Transition easing into this slide |
|
|
73
82
|
| ...layout props | varies | — | Additional props the layout accepts |
|
|
74
83
|
|
|
75
84
|
Example:
|
|
@@ -96,6 +105,9 @@ in the first frontmatter block alongside deck-level settings.
|
|
|
96
105
|
---
|
|
97
106
|
title: "My First Deck"
|
|
98
107
|
colorMode: system
|
|
108
|
+
transition: fade
|
|
109
|
+
transitionDuration: 200
|
|
110
|
+
transitionEasing: ease
|
|
99
111
|
layouts: "@honeydeck/honeydeck/layouts"
|
|
100
112
|
layout: Cover
|
|
101
113
|
---
|
package/docs/deeper-dive.md
CHANGED
|
@@ -111,19 +111,22 @@ Deck-level settings live in the first frontmatter block of the deck entry file.
|
|
|
111
111
|
| `colorMode` | `"system" \| "light" \| "dark"` | `"system"` | Color mode |
|
|
112
112
|
| `pdfColorMode` | `"light" \| "dark"` | unset | Optional PDF color mode; falls back to pinned `colorMode`, then `light` |
|
|
113
113
|
| `pdfSteps` | `"final" \| "all"` | `"final"` | PDF step handling |
|
|
114
|
-
| `transition` | `boolean` | `
|
|
114
|
+
| `transition` | `string \| boolean` | `fade` | Named slide transition |
|
|
115
115
|
| `layouts` | `string` | `""` (built-in) | Custom layout map path |
|
|
116
116
|
| `defaultLayout` | `string` | `"Default"` | Fallback layout |
|
|
117
117
|
| `showSlideNumbers` | `boolean` | `false` | Show slide numbers |
|
|
118
118
|
|
|
119
|
-
Slide-level frontmatter chooses the slide layout
|
|
119
|
+
Slide-level frontmatter chooses the slide layout, passes layout-specific props, and can override the transition into that slide.
|
|
120
120
|
|
|
121
121
|
| Property | Type | Description |
|
|
122
122
|
|----------|------|-------------|
|
|
123
123
|
| `layout` | `string` | Layout name in PascalCase |
|
|
124
|
+
| `transition` | `string \| boolean` | Named transition into this slide |
|
|
125
|
+
| `transitionDuration` | `number` | Transition duration in milliseconds |
|
|
126
|
+
| `transitionEasing` | `string` | Transition timing function |
|
|
124
127
|
| layout props | varies | Layout-specific fields |
|
|
125
128
|
|
|
126
|
-
For the full reference, see [Configuration](configuration.md).
|
|
129
|
+
For the full reference, see [Configuration](configuration.md) and [Transitions](transitions.md).
|
|
127
130
|
|
|
128
131
|
## Core components
|
|
129
132
|
|
package/docs/index.json
CHANGED
|
@@ -45,6 +45,17 @@
|
|
|
45
45
|
"file": "configuration.md",
|
|
46
46
|
"sourcePath": "packages/docs/content/docs/(core)/configuration.mdx"
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
"slug": "transitions",
|
|
50
|
+
"title": "Transitions",
|
|
51
|
+
"description": "Configure built-in and custom slide transitions in Honeydeck.",
|
|
52
|
+
"breadcrumbs": [
|
|
53
|
+
"Core",
|
|
54
|
+
"Transitions"
|
|
55
|
+
],
|
|
56
|
+
"file": "transitions.md",
|
|
57
|
+
"sourcePath": "packages/docs/content/docs/(core)/transitions.mdx"
|
|
58
|
+
},
|
|
48
59
|
{
|
|
49
60
|
"slug": "steps-and-reveals",
|
|
50
61
|
"title": "Steps and reveals",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!-- Generated from packages/docs/content/docs/(core)/transitions.mdx. Do not edit by hand. -->
|
|
2
|
+
|
|
3
|
+
# Transitions
|
|
4
|
+
|
|
5
|
+
Honeydeck uses named slide transitions. Set a deck-wide default in the first frontmatter block, then override individual slides when needed.
|
|
6
|
+
|
|
7
|
+
## Deck defaults
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
---
|
|
11
|
+
transition: fade
|
|
12
|
+
transitionDuration: 200
|
|
13
|
+
transitionEasing: ease
|
|
14
|
+
---
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Property | Type | Default | Description |
|
|
18
|
+
|----------|------|---------|-------------|
|
|
19
|
+
| `transition` | `string \| boolean` | `fade` | Named transition for slide changes |
|
|
20
|
+
| `transitionDuration` | `number` | `200` | Duration in milliseconds |
|
|
21
|
+
| `transitionEasing` | `string` | `ease` | CSS timing function |
|
|
22
|
+
|
|
23
|
+
Built-in transition names:
|
|
24
|
+
|
|
25
|
+
- `fade` — default crossfade
|
|
26
|
+
- `none` — no slide transition
|
|
27
|
+
- `slide-left` — horizontal slide, reverse-aware when navigating backward
|
|
28
|
+
|
|
29
|
+
## Slide overrides
|
|
30
|
+
|
|
31
|
+
Slide frontmatter controls the transition **into that slide**:
|
|
32
|
+
|
|
33
|
+
```mdx
|
|
34
|
+
---
|
|
35
|
+
transition: slide-left
|
|
36
|
+
transitionDuration: 500
|
|
37
|
+
transitionEasing: cubic-bezier(0.22, 1, 0.36, 1)
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# A slide with a custom entrance
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Slides without transition frontmatter use the deck defaults.
|
|
44
|
+
|
|
45
|
+
## Custom transitions
|
|
46
|
+
|
|
47
|
+
Any transition name that is not built in becomes a CSS hook. For example:
|
|
48
|
+
|
|
49
|
+
```mdx
|
|
50
|
+
---
|
|
51
|
+
transition: honey-spin
|
|
52
|
+
transitionDuration: 700
|
|
53
|
+
transitionEasing: cubic-bezier(0.22, 1, 0.36, 1)
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
# Custom CSS Transition
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Honeydeck adds classes to only the entering and exiting slide layers:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<div class="honeydeck-slide-layer honeydeck-transition-honey-spin honeydeck-transition-enter">
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<div class="honeydeck-slide-layer honeydeck-transition-honey-spin honeydeck-transition-exit">
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Scope your CSS to both the transition name and enter/exit class:
|
|
70
|
+
|
|
71
|
+
```css
|
|
72
|
+
.honeydeck-slide-layer.honeydeck-transition-honey-spin.honeydeck-transition-enter {
|
|
73
|
+
animation-name: honey-spin-enter;
|
|
74
|
+
animation-duration: var(--honeydeck-transition-duration);
|
|
75
|
+
animation-timing-function: var(--honeydeck-transition-easing);
|
|
76
|
+
animation-fill-mode: both;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.honeydeck-slide-layer.honeydeck-transition-honey-spin.honeydeck-transition-exit {
|
|
80
|
+
animation-name: honey-spin-exit;
|
|
81
|
+
animation-duration: var(--honeydeck-transition-duration);
|
|
82
|
+
animation-timing-function: var(--honeydeck-transition-easing);
|
|
83
|
+
animation-fill-mode: both;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Reverse-aware CSS
|
|
88
|
+
|
|
89
|
+
Honeydeck provides a direction variable:
|
|
90
|
+
|
|
91
|
+
```css
|
|
92
|
+
--honeydeck-transition-direction: 1; /* forward */
|
|
93
|
+
--honeydeck-transition-direction: -1; /* backward */
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Use it in transform math when your custom transition should reverse with navigation direction:
|
|
97
|
+
|
|
98
|
+
```css
|
|
99
|
+
@keyframes custom-enter {
|
|
100
|
+
from {
|
|
101
|
+
opacity: 0;
|
|
102
|
+
transform: translateX(calc(40% * var(--honeydeck-transition-direction)));
|
|
103
|
+
}
|
|
104
|
+
to {
|
|
105
|
+
opacity: 1;
|
|
106
|
+
transform: translateX(0);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@keyframes custom-exit {
|
|
111
|
+
from {
|
|
112
|
+
opacity: 1;
|
|
113
|
+
transform: translateX(0);
|
|
114
|
+
}
|
|
115
|
+
to {
|
|
116
|
+
opacity: 0;
|
|
117
|
+
transform: translateX(calc(-20% * var(--honeydeck-transition-direction)));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Honeydeck cannot safely invert arbitrary custom keyframes automatically, so reverse behavior is opt-in through CSS variables.
|
|
123
|
+
|
|
124
|
+
## Reduced motion
|
|
125
|
+
|
|
126
|
+
If the viewer has `prefers-reduced-motion: reduce`, Honeydeck disables slide transition animations.
|
package/package.json
CHANGED
|
@@ -43,8 +43,9 @@ Generated starter tree includes `package.json`, `deck.mdx`, `styles.css`, `.giti
|
|
|
43
43
|
|
|
44
44
|
Tutorial-style `deck.mdx` imports `./styles.css` so styling stays explicit and user-controlled. It demonstrates:
|
|
45
45
|
|
|
46
|
-
- Deck frontmatter, including `colorMode: system`
|
|
46
|
+
- Deck frontmatter, including `colorMode: system`, named transition defaults (`transition`, `transitionDuration`, `transitionEasing`), and layout map hints
|
|
47
47
|
- Slide separators
|
|
48
|
+
- Built-in slide transitions via deck-level defaults and at least one slide-level `transition:` override
|
|
48
49
|
- Built-in layouts via per-slide `layout:` (`Default`, `Section`, `TwoCol`, `Cover`, `Blank`)
|
|
49
50
|
- Code highlighting with step-through
|
|
50
51
|
- Custom interactive component (`SparkleButton`)
|
|
@@ -3,6 +3,10 @@ title: demo-deck
|
|
|
3
3
|
description: A clean Honeydeck starter deck
|
|
4
4
|
# Define the color mode for your slides: light, dark, system.
|
|
5
5
|
colorMode: system
|
|
6
|
+
# Configure slide transitions. Use "none" to disable.
|
|
7
|
+
transition: fade
|
|
8
|
+
transitionDuration: 200
|
|
9
|
+
transitionEasing: ease
|
|
6
10
|
# Swap layouts for a custom layout map or the included bee theme (also update css).
|
|
7
11
|
# - Custom layouts: layouts: "./layouts"
|
|
8
12
|
# - Bee layouts: layouts: "@honeydeck/honeydeck/layouts/bee"
|
|
@@ -44,6 +48,24 @@ All you need is `---` for new slides.
|
|
|
44
48
|
You can press <Keyboard>↓</Keyboard> to skip revealed content and jump to the next slide.
|
|
45
49
|
</Reveal>
|
|
46
50
|
|
|
51
|
+
---
|
|
52
|
+
transition: slide-left
|
|
53
|
+
transitionDuration: 500
|
|
54
|
+
transitionEasing: cubic-bezier(0.22, 1, 0.36, 1)
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# Choose slide transitions 🎬
|
|
58
|
+
|
|
59
|
+
Deck-level frontmatter sets the default transition. Slide frontmatter overrides the transition into that slide.
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
transition: fade
|
|
63
|
+
transitionDuration: 200
|
|
64
|
+
transitionEasing: ease
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use `transition: none` when you want no slide animation.
|
|
68
|
+
|
|
47
69
|
---
|
|
48
70
|
|
|
49
71
|
# Grow into React when needed ⚛️
|
package/src/runtime/Deck.tsx
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { config } from "virtual:honeydeck/config";
|
|
29
29
|
import {
|
|
30
|
+
type CSSProperties,
|
|
30
31
|
useCallback,
|
|
31
32
|
useEffect,
|
|
32
33
|
useLayoutEffect,
|
|
@@ -82,6 +83,80 @@ function calcScaleFromElement(el: HTMLElement | null): number | null {
|
|
|
82
83
|
return Math.min(el.clientWidth / BASE_WIDTH, el.clientHeight / BASE_HEIGHT);
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
type SlideTransitionState = {
|
|
87
|
+
from: number;
|
|
88
|
+
to: number;
|
|
89
|
+
name: string;
|
|
90
|
+
className: string;
|
|
91
|
+
duration: number;
|
|
92
|
+
easing: string;
|
|
93
|
+
direction: 1 | -1;
|
|
94
|
+
enterFromOpacity: number;
|
|
95
|
+
exitFromOpacity: number;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function normalizeTransitionName(value: unknown): string {
|
|
99
|
+
if (value === false) return "none";
|
|
100
|
+
if (value === true || value == null) return "fade";
|
|
101
|
+
if (typeof value !== "string") return "fade";
|
|
102
|
+
|
|
103
|
+
const name = value.trim();
|
|
104
|
+
if (!name) return "fade";
|
|
105
|
+
if (name === "true") return "fade";
|
|
106
|
+
if (name === "false") return "none";
|
|
107
|
+
return name;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeTransitionDuration(value: unknown): number | null {
|
|
111
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
112
|
+
return Math.max(0, Math.round(value));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeTransitionEasing(value: unknown): string | null {
|
|
116
|
+
if (typeof value !== "string") return null;
|
|
117
|
+
const easing = value.trim();
|
|
118
|
+
return easing ? easing : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function transitionClassName(name: string): string {
|
|
122
|
+
return name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readLayerOpacity(
|
|
126
|
+
element: HTMLElement | null | undefined,
|
|
127
|
+
): number | null {
|
|
128
|
+
if (!element) return null;
|
|
129
|
+
const opacity = Number.parseFloat(window.getComputedStyle(element).opacity);
|
|
130
|
+
return Number.isFinite(opacity) ? opacity : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getTransitionOptions(
|
|
134
|
+
slideIndex: number,
|
|
135
|
+
): Omit<
|
|
136
|
+
SlideTransitionState,
|
|
137
|
+
"from" | "to" | "direction" | "enterFromOpacity" | "exitFromOpacity"
|
|
138
|
+
> {
|
|
139
|
+
const frontmatter = slideData[slideIndex]?.frontmatter ?? {};
|
|
140
|
+
const name = normalizeTransitionName(
|
|
141
|
+
frontmatter.transition ?? config.transition,
|
|
142
|
+
);
|
|
143
|
+
const duration =
|
|
144
|
+
normalizeTransitionDuration(frontmatter.transitionDuration) ??
|
|
145
|
+
normalizeTransitionDuration(config.transitionDuration) ??
|
|
146
|
+
200;
|
|
147
|
+
const easing =
|
|
148
|
+
normalizeTransitionEasing(frontmatter.transitionEasing) ??
|
|
149
|
+
normalizeTransitionEasing(config.transitionEasing) ??
|
|
150
|
+
"ease";
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
name,
|
|
154
|
+
className: transitionClassName(name),
|
|
155
|
+
duration,
|
|
156
|
+
easing,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
85
160
|
// ---------------------------------------------------------------------------
|
|
86
161
|
// Component
|
|
87
162
|
// ---------------------------------------------------------------------------
|
|
@@ -102,6 +177,7 @@ export function Deck() {
|
|
|
102
177
|
});
|
|
103
178
|
const [effectiveColorMode, setEffectiveColorMode] =
|
|
104
179
|
useState<EffectiveColorMode>("light");
|
|
180
|
+
const [reducedMotion, setReducedMotion] = useState(false);
|
|
105
181
|
|
|
106
182
|
const route = useRoute();
|
|
107
183
|
const pointerLayout = usePointerLayout();
|
|
@@ -126,6 +202,17 @@ export function Deck() {
|
|
|
126
202
|
return () => mq.removeEventListener("change", onChange);
|
|
127
203
|
}, [colorMode]);
|
|
128
204
|
|
|
205
|
+
// ── Reduced motion: disable slide transition animations ────────────────
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
208
|
+
setReducedMotion(mq.matches);
|
|
209
|
+
|
|
210
|
+
const onChange = (event: MediaQueryListEvent) =>
|
|
211
|
+
setReducedMotion(event.matches);
|
|
212
|
+
mq.addEventListener("change", onChange);
|
|
213
|
+
return () => mq.removeEventListener("change", onChange);
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
129
216
|
// ── Observe stage size → recalculate scale ─────────────────────────────
|
|
130
217
|
useEffect(() => {
|
|
131
218
|
if (route.view !== "slide" && route.view !== "overview") return;
|
|
@@ -247,6 +334,73 @@ export function Deck() {
|
|
|
247
334
|
});
|
|
248
335
|
}, [scale, slideZoom]);
|
|
249
336
|
|
|
337
|
+
const currentSlide = Math.max(
|
|
338
|
+
1,
|
|
339
|
+
Math.min(route.slide, slideData.length || 1),
|
|
340
|
+
);
|
|
341
|
+
const previousSlideRef = useRef<number | null>(null);
|
|
342
|
+
const slideLayerRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
|
343
|
+
const [slideTransition, setSlideTransition] =
|
|
344
|
+
useState<SlideTransitionState | null>(null);
|
|
345
|
+
const slideTransitionRef = useRef<SlideTransitionState | null>(null);
|
|
346
|
+
slideTransitionRef.current = slideTransition;
|
|
347
|
+
|
|
348
|
+
useLayoutEffect(() => {
|
|
349
|
+
if (route.view !== "slide") {
|
|
350
|
+
previousSlideRef.current = currentSlide;
|
|
351
|
+
setSlideTransition(null);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (reducedMotion) {
|
|
356
|
+
previousSlideRef.current = currentSlide;
|
|
357
|
+
setSlideTransition(null);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const previousSlide = previousSlideRef.current;
|
|
362
|
+
if (previousSlide === null) {
|
|
363
|
+
previousSlideRef.current = currentSlide;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (previousSlide === currentSlide) return;
|
|
367
|
+
|
|
368
|
+
const direction: 1 | -1 = currentSlide > previousSlide ? 1 : -1;
|
|
369
|
+
const options = getTransitionOptions(currentSlide - 1);
|
|
370
|
+
previousSlideRef.current = currentSlide;
|
|
371
|
+
|
|
372
|
+
if (options.name === "none" || options.duration === 0) {
|
|
373
|
+
setSlideTransition(null);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const activeTransition = slideTransitionRef.current;
|
|
378
|
+
const isInterruptingFade = activeTransition?.name === "fade";
|
|
379
|
+
const nextTransition = {
|
|
380
|
+
...options,
|
|
381
|
+
from: previousSlide,
|
|
382
|
+
to: currentSlide,
|
|
383
|
+
direction,
|
|
384
|
+
enterFromOpacity: isInterruptingFade
|
|
385
|
+
? (readLayerOpacity(slideLayerRefs.current[currentSlide]) ?? 0)
|
|
386
|
+
: 0,
|
|
387
|
+
exitFromOpacity: isInterruptingFade
|
|
388
|
+
? (readLayerOpacity(slideLayerRefs.current[previousSlide]) ?? 1)
|
|
389
|
+
: 1,
|
|
390
|
+
};
|
|
391
|
+
setSlideTransition(nextTransition);
|
|
392
|
+
|
|
393
|
+
const timeout = window.setTimeout(() => {
|
|
394
|
+
setSlideTransition((active) =>
|
|
395
|
+
active?.from === nextTransition.from && active.to === nextTransition.to
|
|
396
|
+
? null
|
|
397
|
+
: active,
|
|
398
|
+
);
|
|
399
|
+
}, options.duration);
|
|
400
|
+
|
|
401
|
+
return () => window.clearTimeout(timeout);
|
|
402
|
+
}, [currentSlide, reducedMotion, route.view]);
|
|
403
|
+
|
|
250
404
|
// ── Reference mode: delegate to DocsView ─────────────────────────────
|
|
251
405
|
if (route.view === "kit") {
|
|
252
406
|
return (
|
|
@@ -265,9 +419,6 @@ export function Deck() {
|
|
|
265
419
|
);
|
|
266
420
|
}
|
|
267
421
|
|
|
268
|
-
// Whether slide transitions are enabled (can be disabled via deck frontmatter)
|
|
269
|
-
const enableTransition = config.transition !== false;
|
|
270
|
-
|
|
271
422
|
// ─────────────────────────────────────────────────────────────────────────
|
|
272
423
|
// Guard: no slides
|
|
273
424
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -280,14 +431,15 @@ export function Deck() {
|
|
|
280
431
|
);
|
|
281
432
|
}
|
|
282
433
|
|
|
283
|
-
const currentSlide = Math.max(1, Math.min(route.slide, slideData.length));
|
|
284
434
|
const currentStep = Math.max(0, route.step);
|
|
285
435
|
const controlRoute =
|
|
286
436
|
route.view === "slide" || route.view === "overview"
|
|
287
437
|
? { ...route, slide: currentSlide, step: currentStep }
|
|
288
438
|
: route;
|
|
289
439
|
const activeSlideScale = scale * slideZoom;
|
|
440
|
+
const viewportScale = scale || 1;
|
|
290
441
|
const slideTransform = `translate(${slidePan.x}px, ${slidePan.y}px) scale(${activeSlideScale})`;
|
|
442
|
+
const zoomedSlideTransform = `translate(${slidePan.x / viewportScale}px, ${slidePan.y / viewportScale}px) scale(${slideZoom})`;
|
|
291
443
|
const showSlideNumbers = config.showSlideNumbers === true;
|
|
292
444
|
const disableSlideTextSelection =
|
|
293
445
|
route.view === "slide" &&
|
|
@@ -326,6 +478,31 @@ export function Deck() {
|
|
|
326
478
|
{slideData.map((data, i) => {
|
|
327
479
|
const slideNumber = i + 1;
|
|
328
480
|
const isCurrent = slideNumber === currentSlide;
|
|
481
|
+
const activeTransition = slideTransition;
|
|
482
|
+
const transitionRole =
|
|
483
|
+
activeTransition?.to === slideNumber
|
|
484
|
+
? "enter"
|
|
485
|
+
: activeTransition?.from === slideNumber
|
|
486
|
+
? "exit"
|
|
487
|
+
: null;
|
|
488
|
+
const isVisible = isCurrent || transitionRole !== null;
|
|
489
|
+
const transitionLayerClass =
|
|
490
|
+
transitionRole && activeTransition
|
|
491
|
+
? `honeydeck-transition-${activeTransition.className} honeydeck-transition-${transitionRole}`
|
|
492
|
+
: "";
|
|
493
|
+
const layerStyle =
|
|
494
|
+
transitionRole && activeTransition
|
|
495
|
+
? ({
|
|
496
|
+
"--honeydeck-transition-duration": `${activeTransition.duration}ms`,
|
|
497
|
+
"--honeydeck-transition-easing": activeTransition.easing,
|
|
498
|
+
"--honeydeck-transition-direction":
|
|
499
|
+
activeTransition.direction,
|
|
500
|
+
"--honeydeck-transition-enter-from-opacity":
|
|
501
|
+
activeTransition.enterFromOpacity,
|
|
502
|
+
"--honeydeck-transition-exit-from-opacity":
|
|
503
|
+
activeTransition.exitFromOpacity,
|
|
504
|
+
} as CSSProperties)
|
|
505
|
+
: undefined;
|
|
329
506
|
const { Component, stepCount, title, frontmatter, layoutName } =
|
|
330
507
|
data;
|
|
331
508
|
const LayoutComponent = resolveLayout(layoutName);
|
|
@@ -335,42 +512,67 @@ export function Deck() {
|
|
|
335
512
|
key={data.id}
|
|
336
513
|
aria-hidden={!isCurrent}
|
|
337
514
|
className={`absolute inset-0 flex items-center justify-center ${
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
515
|
+
isVisible ? "visible" : "invisible"
|
|
516
|
+
} ${isCurrent ? "pointer-events-auto" : "pointer-events-none"} ${
|
|
517
|
+
transitionRole === "enter"
|
|
518
|
+
? "z-2"
|
|
519
|
+
: transitionRole === "exit"
|
|
520
|
+
? "z-1"
|
|
521
|
+
: isCurrent
|
|
522
|
+
? "z-1"
|
|
523
|
+
: "z-0"
|
|
341
524
|
}`}
|
|
342
|
-
style={{
|
|
343
|
-
transition: enableTransition
|
|
344
|
-
? `opacity 200ms ease, visibility 0s ${isCurrent ? "0s" : "200ms"}`
|
|
345
|
-
: "none",
|
|
346
|
-
}}
|
|
347
525
|
>
|
|
348
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
526
|
+
<div
|
|
527
|
+
className="shrink-0 overflow-hidden"
|
|
528
|
+
style={{
|
|
529
|
+
width: BASE_WIDTH,
|
|
530
|
+
height: BASE_HEIGHT,
|
|
531
|
+
transform: `scale(${scale})`,
|
|
532
|
+
transformOrigin: "center center",
|
|
533
|
+
}}
|
|
351
534
|
>
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
535
|
+
<div
|
|
536
|
+
ref={(element) => {
|
|
537
|
+
slideLayerRefs.current[slideNumber] = element;
|
|
538
|
+
}}
|
|
539
|
+
className={`honeydeck-slide-layer relative ${
|
|
540
|
+
isCurrent ? "opacity-100" : "opacity-0"
|
|
541
|
+
} ${transitionLayerClass}`}
|
|
542
|
+
style={{
|
|
543
|
+
width: BASE_WIDTH,
|
|
544
|
+
height: BASE_HEIGHT,
|
|
545
|
+
...layerStyle,
|
|
546
|
+
}}
|
|
547
|
+
>
|
|
548
|
+
<TimelineProvider
|
|
549
|
+
stepIndex={isCurrent ? currentStep : 0}
|
|
550
|
+
stepCount={stepCount}
|
|
361
551
|
>
|
|
362
|
-
<
|
|
363
|
-
<
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
552
|
+
<SlideScaleProvider scale={activeSlideScale}>
|
|
553
|
+
<div
|
|
554
|
+
className="honeydeck-slide-canvas shrink-0 relative overflow-hidden box-border"
|
|
555
|
+
style={{
|
|
556
|
+
width: BASE_WIDTH,
|
|
557
|
+
height: BASE_HEIGHT,
|
|
558
|
+
transform: zoomedSlideTransform,
|
|
559
|
+
transformOrigin: "center center",
|
|
560
|
+
}}
|
|
367
561
|
>
|
|
368
|
-
<
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
562
|
+
<ErrorBoundary slideNumber={slideNumber}>
|
|
563
|
+
<LayoutComponent
|
|
564
|
+
title={title || null}
|
|
565
|
+
frontmatter={frontmatter}
|
|
566
|
+
rawChildren={<Component />}
|
|
567
|
+
>
|
|
568
|
+
<Component />
|
|
569
|
+
</LayoutComponent>
|
|
570
|
+
</ErrorBoundary>
|
|
571
|
+
</div>
|
|
572
|
+
</SlideScaleProvider>
|
|
573
|
+
</TimelineProvider>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
374
576
|
</div>
|
|
375
577
|
);
|
|
376
578
|
})}
|
package/src/runtime/SPEC.md
CHANGED
|
@@ -223,12 +223,20 @@ Build output is a single-page application. The app preserves client-side slide t
|
|
|
223
223
|
|
|
224
224
|
### Slide Transitions
|
|
225
225
|
|
|
226
|
-
Honeydeck
|
|
226
|
+
Honeydeck uses a named slide transition system. Deck-level frontmatter sets defaults, and slide-level frontmatter overrides the transition **into that slide**:
|
|
227
227
|
|
|
228
228
|
```yaml
|
|
229
|
-
transition:
|
|
229
|
+
transition: fade
|
|
230
|
+
transitionDuration: 200
|
|
231
|
+
transitionEasing: ease
|
|
230
232
|
```
|
|
231
233
|
|
|
234
|
+
Built-in transition names are `fade`, `none`, and `slide-left`. Any other string is exposed as a custom CSS hook on the participating slide layers. Legacy `transition: true` maps to `fade`, and `transition: false` maps to `none`.
|
|
235
|
+
|
|
236
|
+
During slide navigation, Honeydeck keeps the outgoing and incoming slide layers mounted inside a scaled slide-sized clipping viewport, applies `honeydeck-slide-layer`, `honeydeck-transition-{name}`, and either `honeydeck-transition-enter` or `honeydeck-transition-exit` only to those two layers, then clears transition state after the configured duration. Transition visuals are clipped to the slide canvas area and must not animate into letterbox or pillarbox bars around the slide. If the next transition is `none` or navigation is interrupted, stale transition state is cleared/replaced so old slides do not remain visible. Outgoing layers are visible during the transition but have pointer events disabled. The built-in `fade` transition uses keyframes and, when a fade is interrupted, starts the next fade from the participating layers' current computed opacity so quick back-and-forth navigation stays close to the old opacity-transition behavior.
|
|
237
|
+
|
|
238
|
+
Participating slide layers receive CSS variables: `--honeydeck-transition-duration`, `--honeydeck-transition-easing`, and `--honeydeck-transition-direction` (`1` forward, `-1` backward). Built-in `slide-left` uses the direction variable so backward navigation reverses direction. Custom transition CSS can use the same variable for opt-in reverse awareness. Reduced-motion preferences disable slide transition animations.
|
|
239
|
+
|
|
232
240
|
### Aspect Ratio
|
|
233
241
|
|
|
234
242
|
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`.
|
package/src/theme/base.css
CHANGED
|
@@ -531,6 +531,91 @@
|
|
|
531
531
|
font-size: var(--honeydeck-font-size-code);
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
+
/* ── Slide transitions ───────────────────────────────────────────────────── */
|
|
535
|
+
|
|
536
|
+
.honeydeck-slide-layer.honeydeck-transition-fade.honeydeck-transition-enter {
|
|
537
|
+
animation-name: honeydeck-transition-fade-enter;
|
|
538
|
+
animation-duration: var(--honeydeck-transition-duration, 200ms);
|
|
539
|
+
animation-timing-function: var(--honeydeck-transition-easing, ease);
|
|
540
|
+
animation-fill-mode: both;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.honeydeck-slide-layer.honeydeck-transition-fade.honeydeck-transition-exit {
|
|
544
|
+
animation-name: honeydeck-transition-fade-exit;
|
|
545
|
+
animation-duration: var(--honeydeck-transition-duration, 200ms);
|
|
546
|
+
animation-timing-function: var(--honeydeck-transition-easing, ease);
|
|
547
|
+
animation-fill-mode: both;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
@keyframes honeydeck-transition-fade-enter {
|
|
551
|
+
from {
|
|
552
|
+
opacity: var(--honeydeck-transition-enter-from-opacity, 0);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
to {
|
|
556
|
+
opacity: 1;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
@keyframes honeydeck-transition-fade-exit {
|
|
561
|
+
from {
|
|
562
|
+
opacity: var(--honeydeck-transition-exit-from-opacity, 1);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
to {
|
|
566
|
+
opacity: 0;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.honeydeck-slide-layer.honeydeck-transition-slide-left.honeydeck-transition-enter {
|
|
571
|
+
animation-name: honeydeck-transition-slide-left-enter;
|
|
572
|
+
animation-duration: var(--honeydeck-transition-duration, 200ms);
|
|
573
|
+
animation-timing-function: var(--honeydeck-transition-easing, ease);
|
|
574
|
+
animation-fill-mode: both;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.honeydeck-slide-layer.honeydeck-transition-slide-left.honeydeck-transition-exit {
|
|
578
|
+
animation-name: honeydeck-transition-slide-left-exit;
|
|
579
|
+
animation-duration: var(--honeydeck-transition-duration, 200ms);
|
|
580
|
+
animation-timing-function: var(--honeydeck-transition-easing, ease);
|
|
581
|
+
animation-fill-mode: both;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
@keyframes honeydeck-transition-slide-left-enter {
|
|
585
|
+
from {
|
|
586
|
+
opacity: 1;
|
|
587
|
+
transform: translateX(
|
|
588
|
+
calc(100% * var(--honeydeck-transition-direction, 1))
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
to {
|
|
593
|
+
opacity: 1;
|
|
594
|
+
transform: translateX(0);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
@keyframes honeydeck-transition-slide-left-exit {
|
|
599
|
+
from {
|
|
600
|
+
opacity: 1;
|
|
601
|
+
transform: translateX(0);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
to {
|
|
605
|
+
opacity: 1;
|
|
606
|
+
transform: translateX(
|
|
607
|
+
calc(-100% * var(--honeydeck-transition-direction, 1))
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
@media (prefers-reduced-motion: reduce) {
|
|
613
|
+
.honeydeck-slide-layer.honeydeck-transition-enter,
|
|
614
|
+
.honeydeck-slide-layer.honeydeck-transition-exit {
|
|
615
|
+
animation: none;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
534
619
|
/* ── Documentation Markdown typography ──────────────────────────────────────
|
|
535
620
|
Rendered README/docs pages use plain MDX elements. Keep their typography
|
|
536
621
|
here instead of in DocsView so the React view stays structural. */
|
package/src/vite-plugin/SPEC.md
CHANGED
|
@@ -108,7 +108,9 @@ All settings use **camelCase**. No separate config file exists. Frontmatter pars
|
|
|
108
108
|
| `colorMode` | `"system" \| "light" \| "dark"` | `"system"` | Browser color mode |
|
|
109
109
|
| `pdfColorMode` | `"light" \| "dark"` | unset | Optional explicit PDF color mode; when unset, PDF falls back to pinned deck `colorMode`, then `light` |
|
|
110
110
|
| `pdfSteps` | `"final" \| "all"` | `"final"` | Whether PDF includes all steps or final state |
|
|
111
|
-
| `transition` | `boolean` | `
|
|
111
|
+
| `transition` | `string \| boolean` | `fade` | Default named slide transition (`fade`, `none`, `slide-left`, or a custom CSS name); legacy `true` maps to `fade` and `false` maps to `none` |
|
|
112
|
+
| `transitionDuration` | `number` | `200` | Default slide transition duration in milliseconds |
|
|
113
|
+
| `transitionEasing` | `string` | `ease` | Default slide transition timing function |
|
|
112
114
|
| `magicCodeDuration` | `number` | `800` | Default Magic Code animation duration in milliseconds |
|
|
113
115
|
| `layouts` | `string` | built-in `@honeydeck/honeydeck/layouts` | Layout map module path |
|
|
114
116
|
| `defaultLayout` | `string` | `"Default"` | Layout used when slide has no `layout:` |
|
|
@@ -119,6 +121,9 @@ All settings use **camelCase**. No separate config file exists. Frontmatter pars
|
|
|
119
121
|
| Property | Type | Default | Description |
|
|
120
122
|
|----------|------|---------|-------------|
|
|
121
123
|
| `layout` | `string` | (uses `defaultLayout`) | Layout map key to use (PascalCase by convention, not validated) |
|
|
124
|
+
| `transition` | `string \| boolean` | deck default | Named transition into this slide; legacy booleans map to `fade`/`none` |
|
|
125
|
+
| `transitionDuration` | `number` | deck default | Transition duration into this slide in milliseconds |
|
|
126
|
+
| `transitionEasing` | `string` | deck default | Transition easing into this slide |
|
|
122
127
|
| ...layout-specific props | varies | — | Any additional props the layout accepts |
|
|
123
128
|
|
|
124
129
|
### Root frontmatter semantics
|
|
@@ -127,6 +132,6 @@ The first frontmatter block in the deck entry file is parsed as deck config. Dec
|
|
|
127
132
|
|
|
128
133
|
Slide-level frontmatter is a frontmatter-only block after a slide separator and applies to the following slide. Imported MDX files are normal MDX modules and cannot set deck-level properties. `magicCodeDuration` is deck-level only; the same key in slide-level frontmatter is treated as a normal layout prop and does not configure Magic Code.
|
|
129
134
|
|
|
130
|
-
Invalid `aspectRatio`, `colorMode`, and `pdfSteps` values fall back to defaults. Invalid `pdfColorMode` is ignored as unset, allowing the pinned `colorMode` fallback. `showSlideNumbers` is enabled only by literal `true`;
|
|
135
|
+
Invalid `aspectRatio`, `colorMode`, and `pdfSteps` values fall back to defaults. Invalid `pdfColorMode` is ignored as unset, allowing the pinned `colorMode` fallback. `showSlideNumbers` is enabled only by literal `true`; slide transition values normalize at runtime, with non-empty strings treated as named built-ins or custom CSS hooks. Invalid explicit Magic Code block `duration` values are compile errors; invalid deck-level `magicCodeDuration` falls back to the default Magic Code duration.
|
|
131
136
|
|
|
132
137
|
During development, changes to deck-level frontmatter invalidate the virtual config and every compiled virtual slide module, because slide compilation can depend on deck settings such as `magicCodeDuration`. Layout-related virtual modules are invalidated as before so layout map and demo previews stay current.
|