@honeydeck/honeydeck 0.4.0 → 0.6.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 (71) hide show
  1. package/AGENTS.md +4 -4
  2. package/DEVELOPMENT.md +6 -4
  3. package/Readme.md +15 -15
  4. package/SPEC.md +5 -4
  5. package/docs/browser-frame.md +38 -0
  6. package/docs/components.md +16 -57
  7. package/docs/configuration.md +13 -0
  8. package/docs/customization.md +2 -0
  9. package/docs/deeper-dive.md +32 -7
  10. package/docs/getting-started.md +4 -2
  11. package/docs/index.json +258 -0
  12. package/docs/keyboard.md +35 -0
  13. package/docs/list-style.md +53 -0
  14. package/docs/local-development.md +3 -1
  15. package/docs/mermaid.md +2 -0
  16. package/docs/mobile.md +2 -0
  17. package/docs/navigation.md +3 -1
  18. package/docs/notes.md +40 -0
  19. package/docs/pdf-export.md +6 -2
  20. package/docs/presenter-mode.md +8 -3
  21. package/docs/reveal-group.md +60 -0
  22. package/docs/reveal-with.md +39 -0
  23. package/docs/reveal.md +35 -0
  24. package/docs/skills.md +5 -3
  25. package/docs/slides.md +2 -0
  26. package/docs/slidev-migration.md +5 -0
  27. package/docs/steps-and-reveals.md +145 -8
  28. package/docs/timeline-steps.md +50 -0
  29. package/package.json +6 -2
  30. package/skills/SPEC.md +6 -6
  31. package/skills/honeydeck/SKILL.md +9 -9
  32. package/skills/slidev-migration/SKILL.md +7 -6
  33. package/src/SPEC.md +8 -3
  34. package/src/cli/SPEC.md +3 -2
  35. package/src/cli/pdf.ts +11 -4
  36. package/src/remark/SPEC.md +102 -2
  37. package/src/remark/code-utils.ts +151 -0
  38. package/src/remark/shiki-code-blocks.ts +329 -136
  39. package/src/remark/step-numbering.ts +408 -103
  40. package/src/runtime/Deck.tsx +133 -116
  41. package/src/runtime/EffectiveColorModeContext.tsx +37 -0
  42. package/src/runtime/SPEC.md +21 -8
  43. package/src/runtime/SlideCanvas.tsx +19 -16
  44. package/src/runtime/SlideScaleContext.tsx +23 -0
  45. package/src/runtime/components/CodeBlock.tsx +19 -202
  46. package/src/runtime/components/CodeBlockCopyButton.tsx +64 -0
  47. package/src/runtime/components/CodeBlockShared.ts +17 -0
  48. package/src/runtime/components/Fade.tsx +51 -0
  49. package/src/runtime/components/FadeGroup.tsx +175 -0
  50. package/src/runtime/components/FadeWith.tsx +54 -0
  51. package/src/runtime/components/MagicCodeBlock.tsx +223 -0
  52. package/src/runtime/components/NavBar.tsx +1 -1
  53. package/src/runtime/components/NormalCodeBlock.tsx +128 -0
  54. package/src/runtime/components/Reveal.tsx +27 -27
  55. package/src/runtime/components/RevealGroup.tsx +143 -41
  56. package/src/runtime/components/RevealWith.tsx +63 -0
  57. package/src/runtime/components/SPEC.md +115 -10
  58. package/src/runtime/components/TimelineReveal.tsx +81 -0
  59. package/src/runtime/components/index.ts +13 -5
  60. package/src/runtime/components/timelineVisibility.ts +45 -0
  61. package/src/runtime/index.ts +9 -1
  62. package/src/runtime/navigation.ts +6 -4
  63. package/src/runtime/presentationApi.ts +449 -0
  64. package/src/runtime/views/PresenterCastButton.tsx +39 -0
  65. package/src/runtime/views/PresenterView.tsx +21 -4
  66. package/src/runtime/views/SPEC.md +7 -5
  67. package/src/theme/base.css +67 -2
  68. package/src/vite-plugin/SPEC.md +20 -2
  69. package/src/vite-plugin/index.ts +16 -2
  70. package/src/vite-plugin/splitter.ts +1 -0
  71. package/src/vite-plugin/virtual-modules.ts +16 -6
@@ -13,7 +13,7 @@
13
13
  * - Overlay with `<OverviewView>` when overview mode is toggled
14
14
  * - `<NavBar>` always present (auto-hides on desktop, visible on touch)
15
15
  * - `useSwipeNav` for touch devices
16
- * - `useSync` for BroadcastChannel audience-side sync
16
+ * - `useSync` for BroadcastChannel fallback and `usePresentationReceiverSync` for Presentation API receiver sync
17
17
  * - Manual color mode override (system / light / dark) via NavBar
18
18
  *
19
19
  * ### Viewport scaling
@@ -41,12 +41,18 @@ import type { ColorMode } from "./components/ColorModeCycleButton.tsx";
41
41
  import { ErrorBoundary } from "./components/ErrorBoundary.tsx";
42
42
  import { NavBar } from "./components/NavBar.tsx";
43
43
  import { SlideNumberBadge } from "./components/SlideNumberBadge.tsx";
44
+ import {
45
+ type EffectiveColorMode,
46
+ EffectiveColorModeProvider,
47
+ } from "./EffectiveColorModeContext.tsx";
44
48
  import { rememberSlideRoute } from "./lastSlideRoute.ts";
45
49
  import {
46
50
  closeOverview,
47
51
  toggleOverview as toggleOverviewRoute,
48
52
  } from "./navigation.ts";
53
+ import { usePresentationReceiverSync } from "./presentationApi.ts";
49
54
  import { useRoute } from "./router.ts";
55
+ import { SlideScaleProvider } from "./SlideScaleContext.tsx";
50
56
  import {
51
57
  BASE_HEIGHT,
52
58
  BASE_WIDTH,
@@ -94,6 +100,8 @@ export function Deck() {
94
100
  if (c === "light" || c === "dark") return c;
95
101
  return "system";
96
102
  });
103
+ const [effectiveColorMode, setEffectiveColorMode] =
104
+ useState<EffectiveColorMode>("light");
97
105
 
98
106
  const route = useRoute();
99
107
  const pointerLayout = usePointerLayout();
@@ -105,9 +113,9 @@ export function Deck() {
105
113
  // ── Color mode: apply data-honeydeck-color-mode to <html> ──────────────────
106
114
  useLayoutEffect(() => {
107
115
  function applyMode(darkFromSystem: boolean) {
108
- applyHoneydeckColorMode(
109
- resolveEffectiveColorMode(colorMode, darkFromSystem),
110
- );
116
+ const mode = resolveEffectiveColorMode(colorMode, darkFromSystem);
117
+ applyHoneydeckColorMode(mode);
118
+ setEffectiveColorMode(mode);
111
119
  }
112
120
 
113
121
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
@@ -147,11 +155,14 @@ export function Deck() {
147
155
  });
148
156
  }, [route]);
149
157
 
150
- // ── BroadcastChannel: audience side (listen for presenter navigation) ──
158
+ // ── Audience sync: BroadcastChannel + Presentation API receiver ──────
151
159
  useSync({
152
160
  enabled: route.view === "slide" || route.view === "overview",
153
161
  isPresenter: false,
154
162
  });
163
+ usePresentationReceiverSync({
164
+ enabled: route.view === "slide" || route.view === "overview",
165
+ });
155
166
 
156
167
  const resetZoom = useCallback(() => {
157
168
  setSlideZoom(1);
@@ -263,7 +274,8 @@ export function Deck() {
263
274
  route.view === "slide" || route.view === "overview"
264
275
  ? { ...route, slide: currentSlide, step: currentStep }
265
276
  : route;
266
- const slideTransform = `translate(${slidePan.x}px, ${slidePan.y}px) scale(${scale * slideZoom})`;
277
+ const activeSlideScale = scale * slideZoom;
278
+ const slideTransform = `translate(${slidePan.x}px, ${slidePan.y}px) scale(${activeSlideScale})`;
267
279
  const showSlideNumbers = config.showSlideNumbers === true;
268
280
  const disableSlideTextSelection =
269
281
  route.view === "slide" &&
@@ -275,129 +287,134 @@ export function Deck() {
275
287
  // ---------------------------------------------------------------------------
276
288
 
277
289
  return (
278
- <div className="fixed inset-0 overflow-hidden bg-black">
279
- {/* ── Sizing container: fills viewport for scale calc ──────── */}
280
- <div
281
- ref={stageRef}
282
- className={`absolute inset-0 ${disableSlideTextSelection ? "select-none" : ""}`}
283
- >
284
- {/* ── Stage backdrop: themed bg at slide size, prevents flicker ──── */}
290
+ <EffectiveColorModeProvider mode={effectiveColorMode}>
291
+ <div className="fixed inset-0 overflow-hidden bg-black">
292
+ {/* ── Sizing container: fills viewport for scale calc ──────── */}
285
293
  <div
286
- aria-hidden="true"
287
- className="absolute inset-0 flex items-center justify-center"
294
+ ref={stageRef}
295
+ className={`absolute inset-0 ${disableSlideTextSelection ? "select-none" : ""}`}
288
296
  >
297
+ {/* ── Stage backdrop: themed bg at slide size, prevents flicker ──── */}
289
298
  <div
290
- className="shrink-0 bg-background"
291
- style={{
292
- width: BASE_WIDTH,
293
- height: BASE_HEIGHT,
294
- transform: `scale(${scale})`,
295
- transformOrigin: "center center",
296
- }}
297
- />
298
- </div>
299
-
300
- {/* ── All slides (only current is visible) ──────────────────────── */}
301
- {slideData.map((data, i) => {
302
- const slideNumber = i + 1;
303
- const isCurrent = slideNumber === currentSlide;
304
- const { Component, stepCount, title, frontmatter, layoutName } = data;
305
- const LayoutComponent = resolveLayout(layoutName);
306
-
307
- return (
299
+ aria-hidden="true"
300
+ className="absolute inset-0 flex items-center justify-center"
301
+ >
308
302
  <div
309
- key={data.id}
310
- aria-hidden={!isCurrent}
311
- className={`absolute inset-0 flex items-center justify-center ${
312
- isCurrent
313
- ? "opacity-100 visible pointer-events-auto z-1"
314
- : "opacity-0 invisible pointer-events-none z-0"
315
- }`}
303
+ className="shrink-0 bg-background"
316
304
  style={{
317
- transition: enableTransition
318
- ? `opacity 200ms ease, visibility 0s ${isCurrent ? "0s" : "200ms"}`
319
- : "none",
305
+ width: BASE_WIDTH,
306
+ height: BASE_HEIGHT,
307
+ transform: `scale(${scale})`,
308
+ transformOrigin: "center center",
320
309
  }}
321
- >
322
- <TimelineProvider
323
- stepIndex={isCurrent ? currentStep : 0}
324
- stepCount={stepCount}
310
+ />
311
+ </div>
312
+
313
+ {/* ── All slides (only current is visible) ──────────────────────── */}
314
+ {slideData.map((data, i) => {
315
+ const slideNumber = i + 1;
316
+ const isCurrent = slideNumber === currentSlide;
317
+ const { Component, stepCount, title, frontmatter, layoutName } =
318
+ data;
319
+ const LayoutComponent = resolveLayout(layoutName);
320
+
321
+ return (
322
+ <div
323
+ key={data.id}
324
+ aria-hidden={!isCurrent}
325
+ className={`absolute inset-0 flex items-center justify-center ${
326
+ isCurrent
327
+ ? "opacity-100 visible pointer-events-auto z-1"
328
+ : "opacity-0 invisible pointer-events-none z-0"
329
+ }`}
330
+ style={{
331
+ transition: enableTransition
332
+ ? `opacity 200ms ease, visibility 0s ${isCurrent ? "0s" : "200ms"}`
333
+ : "none",
334
+ }}
325
335
  >
326
- <div
327
- className="honeydeck-slide-canvas shrink-0 relative overflow-hidden box-border"
328
- style={{
329
- width: BASE_WIDTH,
330
- height: BASE_HEIGHT,
331
- transform: slideTransform,
332
- transformOrigin: "center center",
333
- }}
336
+ <TimelineProvider
337
+ stepIndex={isCurrent ? currentStep : 0}
338
+ stepCount={stepCount}
334
339
  >
335
- <ErrorBoundary slideNumber={slideNumber}>
336
- <LayoutComponent
337
- title={title || null}
338
- frontmatter={frontmatter}
339
- rawChildren={<Component />}
340
+ <SlideScaleProvider scale={activeSlideScale}>
341
+ <div
342
+ className="honeydeck-slide-canvas shrink-0 relative overflow-hidden box-border"
343
+ style={{
344
+ width: BASE_WIDTH,
345
+ height: BASE_HEIGHT,
346
+ transform: slideTransform,
347
+ transformOrigin: "center center",
348
+ }}
340
349
  >
341
- <Component />
342
- </LayoutComponent>
343
- </ErrorBoundary>
344
- </div>
345
- </TimelineProvider>
350
+ <ErrorBoundary slideNumber={slideNumber}>
351
+ <LayoutComponent
352
+ title={title || null}
353
+ frontmatter={frontmatter}
354
+ rawChildren={<Component />}
355
+ >
356
+ <Component />
357
+ </LayoutComponent>
358
+ </ErrorBoundary>
359
+ </div>
360
+ </SlideScaleProvider>
361
+ </TimelineProvider>
362
+ </div>
363
+ );
364
+ })}
365
+
366
+ {showSlideNumbers && (
367
+ <div className="absolute inset-0 z-10 flex items-center justify-center pointer-events-none">
368
+ <div
369
+ className="honeydeck-slide-number-layer shrink-0 relative"
370
+ style={{
371
+ width: BASE_WIDTH,
372
+ height: BASE_HEIGHT,
373
+ transform: slideTransform,
374
+ transformOrigin: "center center",
375
+ }}
376
+ >
377
+ <SlideNumberBadge slide={currentSlide} />
378
+ </div>
346
379
  </div>
347
- );
348
- })}
380
+ )}
381
+ </div>
349
382
 
350
- {showSlideNumbers && (
351
- <div className="absolute inset-0 z-10 flex items-center justify-center pointer-events-none">
352
- <div
353
- className="honeydeck-slide-number-layer shrink-0 relative"
354
- style={{
355
- width: BASE_WIDTH,
356
- height: BASE_HEIGHT,
357
- transform: slideTransform,
358
- transformOrigin: "center center",
359
- }}
360
- >
361
- <SlideNumberBadge slide={currentSlide} />
362
- </div>
363
- </div>
383
+ {/* ── Overview overlay ──────────────────────────────────────────── */}
384
+ {isOverview && (
385
+ <OverviewView
386
+ currentSlide={currentSlide}
387
+ currentStep={currentStep}
388
+ onClose={() =>
389
+ closeOverview(controlRoute, {
390
+ slideCount: slideData.length,
391
+ getStepCount,
392
+ })
393
+ }
394
+ />
364
395
  )}
365
- </div>
366
396
 
367
- {/* ── Overview overlay ──────────────────────────────────────────── */}
368
- {isOverview && (
369
- <OverviewView
370
- currentSlide={currentSlide}
371
- currentStep={currentStep}
372
- onClose={() =>
373
- closeOverview(controlRoute, {
374
- slideCount: slideData.length,
375
- getStepCount,
376
- })
377
- }
378
- />
379
- )}
380
-
381
- {/* ── Navigation bar ────────────────────────────────────────────── */}
382
- {/* GAP-06: showSlideNumbers wired from config */}
383
- {!isOverview && (
384
- <NavBarWithHover
385
- route={controlRoute}
386
- isOverview={isOverview}
387
- colorMode={colorMode}
388
- onToggleOverview={toggleOverview}
389
- onSetColorMode={setColorMode}
390
- isZoomed={slideZoom > 1}
391
- onResetZoom={resetZoom}
392
- toggleSignal={navBarToggleSignal}
393
- showTextSelectionToggle={pointerLayout.isTouchDevice}
394
- isTextSelectionEnabled={slideTextSelectionEnabled}
395
- onToggleTextSelection={() =>
396
- setSlideTextSelectionEnabled((value) => !value)
397
- }
398
- />
399
- )}
400
- </div>
397
+ {/* ── Navigation bar ────────────────────────────────────────────── */}
398
+ {/* GAP-06: showSlideNumbers wired from config */}
399
+ {!isOverview && (
400
+ <NavBarWithHover
401
+ route={controlRoute}
402
+ isOverview={isOverview}
403
+ colorMode={colorMode}
404
+ onToggleOverview={toggleOverview}
405
+ onSetColorMode={setColorMode}
406
+ isZoomed={slideZoom > 1}
407
+ onResetZoom={resetZoom}
408
+ toggleSignal={navBarToggleSignal}
409
+ showTextSelectionToggle={pointerLayout.isTouchDevice}
410
+ isTextSelectionEnabled={slideTextSelectionEnabled}
411
+ onToggleTextSelection={() =>
412
+ setSlideTextSelectionEnabled((value) => !value)
413
+ }
414
+ />
415
+ )}
416
+ </div>
417
+ </EffectiveColorModeProvider>
401
418
  );
402
419
  }
403
420
 
@@ -0,0 +1,37 @@
1
+ import { createContext, type ReactNode, useContext } from "react";
2
+
3
+ export type EffectiveColorMode = "light" | "dark";
4
+
5
+ const EffectiveColorModeContext = createContext<EffectiveColorMode | null>(
6
+ null,
7
+ );
8
+
9
+ type EffectiveColorModeProviderProps = {
10
+ children: ReactNode;
11
+ mode: EffectiveColorMode;
12
+ };
13
+
14
+ export function EffectiveColorModeProvider({
15
+ children,
16
+ mode,
17
+ }: EffectiveColorModeProviderProps) {
18
+ return (
19
+ <EffectiveColorModeContext.Provider value={mode}>
20
+ {children}
21
+ </EffectiveColorModeContext.Provider>
22
+ );
23
+ }
24
+
25
+ export function readDocumentEffectiveColorMode(): EffectiveColorMode {
26
+ if (typeof document === "undefined") return "light";
27
+ return document.documentElement.getAttribute("data-honeydeck-color-mode") ===
28
+ "dark"
29
+ ? "dark"
30
+ : "light";
31
+ }
32
+
33
+ export function useEffectiveColorMode(): EffectiveColorMode {
34
+ return (
35
+ useContext(EffectiveColorModeContext) ?? readDocumentEffectiveColorMode()
36
+ );
37
+ }
@@ -6,23 +6,36 @@
6
6
 
7
7
  ### Concept
8
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.
9
+ The **timeline** is a first-class Honeydeck concept. Each slide has a local timeline of steps. Code walkthroughs, Magic Code blocks, reveal/fade components, and statically registered custom component steps all hook into the same timeline.
10
10
 
11
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
12
 
13
+ Magic Code blocks join the same timeline. Each inner code fence contributes its normal code highlight states; Honeydeck advances through those highlight states before morphing to the next inner code fence. Magic Code step counting is `sum(inner fence highlight groups) - 1`.
14
+
13
15
  ### Timeline State
14
16
 
15
17
  - Initial state: `stepIndex = 0` (no reveal or custom step content active)
16
18
  - 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
+ - Magic Code blocks show their first inner code fence and its first metadata group immediately whenever the block is visible
20
+ - First reveal/fade/custom timeline entry activates at `stepIndex = 1`
21
+ - For code walkthroughs and Magic Code inner code states, the second and later metadata groups activate at their assigned timeline steps
19
22
  - 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
23
+ - Each authored `<Reveal>` or `<Fade>` adds one step to the slide timeline. Honeydeck injects an internal `at={n}` prop during compilation to connect each component to its assigned timeline step; `at` is not a user-facing API for step-producing components, and author-authored `at` values are build errors.
24
+ - `<Reveal>` content is visible when `stepIndex >= at`; `<Fade>` content is visible when `stepIndex < at`.
25
+ - `<Reveal name="...">` may name that reveal's assigned step for same-slide `<RevealWith target="...">` or `<FadeWith target="...">` synchronization. Reveal names are slide-local, literal, non-empty strings.
26
+ - `<RevealWith>` and `<FadeWith>` never add timeline steps. They sync with an existing step resolved either from `target="name"` on a same-slide `<Reveal>`, from numeric `target={n}`, or from literal numeric `at={n}` targeting an existing 1-based slide-local step.
27
+ - Reveal/fade components reserve hidden layout space by default; with `ephemeral`, hidden content renders `null` and reserves no space while presenter future previews still render a muted ghost.
21
28
  - Timeline entries are flat within each slide, even when authored with nested
22
29
  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.
30
+ first, then any nested reveal/fade group, reveal/fade, or code walkthrough
31
+ steps inside it are appended to the same slide timeline before the next
32
+ sibling timeline target. `<RevealGroup listRevealMode="nested">` also treats
33
+ nested list items in direct child lists as timeline targets in depth-first
34
+ document order.
35
+ - `<RevealWith>` and `<FadeWith>` must not contain nested timeline producers because they do not add steps themselves. Target them at sibling timeline steps instead.
36
+ - `<Fade>` and `<FadeGroup>` targets must not contain nested timeline producers
37
+ because a faded parent would hide later nested steps. Put fade components
38
+ inside reveal targets instead.
26
39
  - Custom React components can participate by wrapping their usage in
27
40
  `<TimelineSteps steps={N}>`. The wrapper must be visible in slide MDX so the
28
41
  compiler can reserve those steps at build time.
@@ -113,7 +126,7 @@ Reference page routes intentionally do not encode slide or step. During one brow
113
126
  | `↓` / `s` | Next slide; in overview, timeline navigation is disabled: arrow keys move overview selection and WASD are no-ops |
114
127
  | `↑` / `w` | Previous slide; in overview, timeline navigation is disabled: arrow keys move overview selection and WASD are no-ops |
115
128
  | `o` | Toggle overview mode |
116
- | `p` | Open presenter mode (new window) |
129
+ | `p` | Open presenter mode (same tab) |
117
130
  | `f` | Toggle fullscreen |
118
131
  | `Escape` | Exit overview; in reference pages, return to slides; browser-native Escape handles fullscreen exit |
119
132
 
@@ -10,6 +10,7 @@
10
10
  * so that surrounding layout can measure and position it correctly.
11
11
  */
12
12
 
13
+ import { SlideScaleProvider } from "./SlideScaleContext.tsx";
13
14
  import {
14
15
  BASE_HEIGHT,
15
16
  BASE_WIDTH,
@@ -72,23 +73,25 @@ export function SlideCanvas({
72
73
  stepCount={stepCount}
73
74
  showFutureSteps={showFutureSteps}
74
75
  >
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 />}
76
+ <SlideScaleProvider scale={scale}>
77
+ <div
78
+ className="honeydeck-slide-canvas absolute top-0 left-0 overflow-hidden box-border"
79
+ style={{
80
+ width: BASE_WIDTH,
81
+ height: BASE_HEIGHT,
82
+ transform: `scale(${scale})`,
83
+ transformOrigin: "top left",
84
+ }}
88
85
  >
89
- <Component />
90
- </LayoutComponent>
91
- </div>
86
+ <LayoutComponent
87
+ title={title || null}
88
+ frontmatter={frontmatter}
89
+ rawChildren={<Component />}
90
+ >
91
+ <Component />
92
+ </LayoutComponent>
93
+ </div>
94
+ </SlideScaleProvider>
92
95
  </TimelineProvider>
93
96
  </div>
94
97
  );
@@ -0,0 +1,23 @@
1
+ import { createContext, type ReactNode, useContext } from "react";
2
+
3
+ const SlideScaleContext = createContext(1);
4
+
5
+ type SlideScaleProviderProps = {
6
+ children: ReactNode;
7
+ scale: number;
8
+ };
9
+
10
+ export function SlideScaleProvider({
11
+ children,
12
+ scale,
13
+ }: SlideScaleProviderProps) {
14
+ return (
15
+ <SlideScaleContext.Provider value={scale}>
16
+ {children}
17
+ </SlideScaleContext.Provider>
18
+ );
19
+ }
20
+
21
+ export function useSlideScale(): number {
22
+ return useContext(SlideScaleContext);
23
+ }