@kadal/deck 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSING.md +40 -0
  3. package/README.md +170 -2
  4. package/dist/_virtual/_rolldown/runtime.js +13 -0
  5. package/dist/audience/index.d.ts +14 -0
  6. package/dist/audience/index.js +680 -0
  7. package/dist/backgrounds/index.d.ts +65 -0
  8. package/dist/backgrounds/index.js +604 -0
  9. package/dist/backgrounds/shared.d.ts +7 -0
  10. package/dist/backgrounds/shared.js +89 -0
  11. package/dist/cli/bin.d.ts +1 -0
  12. package/dist/cli/bin.js +8 -0
  13. package/dist/cli/host.js +111 -0
  14. package/dist/cli/index.d.ts +4 -0
  15. package/dist/cli/index.js +140 -0
  16. package/dist/cli/login.js +56 -0
  17. package/dist/cli/publish.js +159 -0
  18. package/dist/cli/vite.js +39 -0
  19. package/dist/compiler/code-meta.d.ts +33 -0
  20. package/dist/compiler/code-meta.js +81 -0
  21. package/dist/compiler/index.d.ts +8 -0
  22. package/dist/compiler/index.js +8 -0
  23. package/dist/compiler/magic-move.d.ts +26 -0
  24. package/dist/compiler/magic-move.js +33 -0
  25. package/dist/compiler/plain-text.js +7 -0
  26. package/dist/compiler/rehype-code.d.ts +18 -0
  27. package/dist/compiler/rehype-code.js +66 -0
  28. package/dist/compiler/rehype-glossary.d.ts +35 -0
  29. package/dist/compiler/rehype-glossary.js +211 -0
  30. package/dist/compiler/slots.d.ts +3 -0
  31. package/dist/compiler/slots.js +36 -0
  32. package/dist/compiler/split.d.ts +63 -0
  33. package/dist/compiler/split.js +195 -0
  34. package/dist/components/Background.d.ts +15 -0
  35. package/dist/components/Background.js +24 -0
  36. package/dist/components/Click.d.ts +54 -0
  37. package/dist/components/Click.js +129 -0
  38. package/dist/components/Code.d.ts +27 -0
  39. package/dist/components/Code.js +124 -0
  40. package/dist/components/Motion.d.ts +25 -0
  41. package/dist/components/Motion.js +45 -0
  42. package/dist/components/Sketch.d.ts +44 -0
  43. package/dist/components/Sketch.js +141 -0
  44. package/dist/components/Term.d.ts +63 -0
  45. package/dist/components/Term.js +492 -0
  46. package/dist/components/bling.d.ts +75 -0
  47. package/dist/components/bling.js +146 -0
  48. package/dist/components/content.d.ts +232 -0
  49. package/dist/components/content.js +637 -0
  50. package/dist/components/index.d.ts +10 -0
  51. package/dist/components/index.js +88 -0
  52. package/dist/core/clicks.d.ts +45 -0
  53. package/dist/core/clicks.js +75 -0
  54. package/dist/core/hash.d.ts +33 -0
  55. package/dist/core/hash.js +49 -0
  56. package/dist/core/keyboard.d.ts +17 -0
  57. package/dist/core/keyboard.js +198 -0
  58. package/dist/core/layout.d.ts +16 -0
  59. package/dist/core/layout.js +25 -0
  60. package/dist/core/model.d.ts +259 -0
  61. package/dist/core/model.js +377 -0
  62. package/dist/core/navigation.d.ts +74 -0
  63. package/dist/core/navigation.js +244 -0
  64. package/dist/core/poster.js +30 -0
  65. package/dist/core/store.d.ts +20 -0
  66. package/dist/core/store.js +64 -0
  67. package/dist/core/touch.js +27 -0
  68. package/dist/draw/DrawingLayer.js +315 -0
  69. package/dist/draw/strokes.js +188 -0
  70. package/dist/export/index.d.ts +68 -0
  71. package/dist/export/index.js +251 -0
  72. package/dist/export/run.js +81 -0
  73. package/dist/glossary/model.d.ts +81 -0
  74. package/dist/glossary/model.js +156 -0
  75. package/dist/glossary/parse.d.ts +27 -0
  76. package/dist/glossary/parse.js +90 -0
  77. package/dist/index.d.ts +31 -0
  78. package/dist/index.js +30 -0
  79. package/dist/live/LiveProvider.d.ts +20 -0
  80. package/dist/live/LiveProvider.js +450 -0
  81. package/dist/live/analytics.d.ts +15 -0
  82. package/dist/live/analytics.js +103 -0
  83. package/dist/live/client.d.ts +51 -0
  84. package/dist/live/client.js +193 -0
  85. package/dist/live/components.d.ts +64 -0
  86. package/dist/live/components.js +471 -0
  87. package/dist/live/context.d.ts +15 -0
  88. package/dist/live/context.js +28 -0
  89. package/dist/live/index.d.ts +7 -0
  90. package/dist/live/index.js +7 -0
  91. package/dist/live/protocol.d.ts +284 -0
  92. package/dist/live/protocol.js +42 -0
  93. package/dist/live/turnstile.js +49 -0
  94. package/dist/presenter/index.d.ts +20 -0
  95. package/dist/presenter/index.js +1211 -0
  96. package/dist/react/AutoAnimate.js +92 -0
  97. package/dist/react/Backgrounds.d.ts +11 -0
  98. package/dist/react/Backgrounds.js +154 -0
  99. package/dist/react/ConfettiLayer.js +85 -0
  100. package/dist/react/Deck.d.ts +35 -0
  101. package/dist/react/Deck.js +983 -0
  102. package/dist/react/Frame.d.ts +16 -0
  103. package/dist/react/Frame.js +174 -0
  104. package/dist/react/Hints.d.ts +3 -0
  105. package/dist/react/Hints.js +201 -0
  106. package/dist/react/Lightbox.js +54 -0
  107. package/dist/react/Overview.js +93 -0
  108. package/dist/react/PrintView.js +128 -0
  109. package/dist/react/ScrollView.js +128 -0
  110. package/dist/react/SlideFrame.d.ts +51 -0
  111. package/dist/react/SlideFrame.js +323 -0
  112. package/dist/react/Stage.js +151 -0
  113. package/dist/react/ThemeMenu.d.ts +5 -0
  114. package/dist/react/ThemeMenu.js +124 -0
  115. package/dist/react/Zoom.js +23 -0
  116. package/dist/react/chrome.js +339 -0
  117. package/dist/react/context.d.ts +165 -0
  118. package/dist/react/context.js +19 -0
  119. package/dist/react/hooks.d.ts +33 -0
  120. package/dist/react/hooks.js +70 -0
  121. package/dist/react/layouts.d.ts +21 -0
  122. package/dist/react/layouts.js +104 -0
  123. package/dist/react/mdx.d.ts +4 -0
  124. package/dist/react/mdx.js +33 -0
  125. package/dist/react/terms.d.ts +6 -0
  126. package/dist/react/terms.js +66 -0
  127. package/dist/renderers/Island.d.ts +57 -0
  128. package/dist/renderers/Island.js +223 -0
  129. package/dist/renderers/contract.d.ts +173 -0
  130. package/dist/renderers/contract.js +152 -0
  131. package/dist/renderers/index.d.ts +12 -0
  132. package/dist/renderers/index.js +16 -0
  133. package/dist/renderers/react.d.ts +16 -0
  134. package/dist/renderers/react.js +32 -0
  135. package/dist/renderers/web-components.d.ts +4 -0
  136. package/dist/renderers/web-components.js +103 -0
  137. package/dist/sketch/Canvas.js +49 -0
  138. package/dist/sketch/dsl.d.ts +131 -0
  139. package/dist/sketch/dsl.js +281 -0
  140. package/dist/sketch/index.d.ts +4 -0
  141. package/dist/sketch/index.js +3 -0
  142. package/dist/sketch/palette.d.ts +21 -0
  143. package/dist/sketch/palette.js +66 -0
  144. package/dist/sketch/render.d.ts +43 -0
  145. package/dist/sketch/render.js +198 -0
  146. package/dist/themes/base.css +1704 -0
  147. package/dist/themes/bling.css +592 -0
  148. package/dist/themes/glossary.css +317 -0
  149. package/dist/themes/live.css +1557 -0
  150. package/dist/themes/minimal.css +83 -0
  151. package/dist/themes/rla.css +92 -0
  152. package/dist/themes/sketch.css +121 -0
  153. package/dist/themes/stack.css +62 -0
  154. package/dist/vite/index.d.ts +15 -0
  155. package/dist/vite/index.js +320 -0
  156. package/package.json +159 -4
  157. package/tools/verify-island.mjs +150 -0
@@ -0,0 +1,983 @@
1
+ import { columnsOf, fromFlat, readHostOverride, resolveConfig, resolveSlideOptions, withHostOverride } from "../core/model.js";
2
+ import { formatHash, parseFlags, parseHash } from "../core/hash.js";
3
+ import { actionFor, isEditable } from "../core/keyboard.js";
4
+ import { computeLayout, toStage } from "../core/layout.js";
5
+ import { availableRoutes, down, first, goto, initialNav, last, left, next, nextClick, prev, prevClick, right, up } from "../core/navigation.js";
6
+ import { createStore } from "../core/store.js";
7
+ import { createSwipeTracker } from "../core/touch.js";
8
+ import { DeckContext } from "./context.js";
9
+ import { Backgrounds } from "./Backgrounds.js";
10
+ import { useTermEffects } from "./terms.js";
11
+ import { Chrome } from "./chrome.js";
12
+ import { Lightbox } from "./Lightbox.js";
13
+ import { Overview } from "./Overview.js";
14
+ import { PrintView } from "./PrintView.js";
15
+ import { ScrollView } from "./ScrollView.js";
16
+ import { Stage } from "./Stage.js";
17
+ import { ZoomLayer } from "./Zoom.js";
18
+ import { Children, cloneElement, isValidElement, useContext, useEffect, useMemo, useState } from "react";
19
+ import { flushSync } from "react-dom";
20
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
+ //#region src/react/Deck.tsx
22
+ function baseOf() {
23
+ const b = import.meta.env?.BASE_URL ?? "/";
24
+ return b.endsWith("/") ? b : `${b}/`;
25
+ }
26
+ function modeFromLocation(base) {
27
+ if (typeof window === "undefined") return "play";
28
+ const path = window.location.pathname;
29
+ const first = (path.startsWith(base) ? path.slice(base.length) : path.replace(/^\//, "")).split("/")[0]?.replace(/\.html$/, "") ?? "";
30
+ if (first === "presenter") return "presenter";
31
+ if (first === "join" || first === "audience") return "audience";
32
+ if (first === "notes") return "notes";
33
+ if (first === "stats") return "stats";
34
+ if (parseFlags(window.location.search).print) return "print";
35
+ return "play";
36
+ }
37
+ function Deck({ manifest, config: configIn, components = {}, pages = {}, children }) {
38
+ const base = useMemo(baseOf, []);
39
+ const slugFromBase = base.replace(/^\/|\/$/g, "");
40
+ const config = useMemo(() => withHostOverride(resolveConfig({ slug: slugFromBase }, manifest.headmatter, configIn), readHostOverride()), [
41
+ manifest.headmatter,
42
+ configIn,
43
+ slugFromBase
44
+ ]);
45
+ const columns = useMemo(() => columnsOf(manifest.slides, config), [manifest.slides, config]);
46
+ const options = useMemo(() => {
47
+ const map = /* @__PURE__ */ new Map();
48
+ columns.forEach((c) => c.slides.forEach((s, v) => map.set(`${c.h},${v}`, resolveSlideOptions(s.frontmatter, config, s.index === 0))));
49
+ return map;
50
+ }, [columns, config]);
51
+ const mode = useMemo(() => modeFromLocation(base), [base]);
52
+ const flags = useMemo(() => parseFlags(typeof window === "undefined" ? "" : window.location.search), []);
53
+ const windowId = useMemo(() => Math.random().toString(36).slice(2, 10), []);
54
+ /**
55
+ * A PROJECTOR: a window that exists to be looked at — a second screen, or the one window you
56
+ * share on a call. It renders the deck exactly as the deck page does; what it gives up is input.
57
+ * No key, swipe, wheel or click reaches navigation, and the follow can never be broken, because
58
+ * a stray press on a shared window in the middle of a call is not a thing to be recoverable
59
+ * from. See `Flags.projector`.
60
+ */
61
+ const projector = mode === "play" && flags.projector;
62
+ const projectorWindow = useMemo(() => ({ ref: null }), []);
63
+ const sync = useMemo(() => ({ source: null }), []);
64
+ const wantedAtLoad = useMemo(() => typeof window !== "undefined" ? parseHash(window.location.hash, columns, config.hashOneBasedIndex) : null, [columns, config.hashOneBasedIndex]);
65
+ /**
66
+ * Does this window start in step with the presenter?
67
+ *
68
+ * In order of how much each signal actually knows:
69
+ *
70
+ * 1. `?at` — a link the deck itself minted ABOUT a slide (a share link, the phone's "Open
71
+ * full"). That slide is what was asked for, so it lands there, free.
72
+ * 2. What this viewer last chose for this deck, if it is still about this talk. This is what
73
+ * carries a bookmark taken mid-talk, a closed tab or a sleeping phone back in step: the
74
+ * hash in a bookmark cannot say it, because the deck WRITES the hash as they navigate, so
75
+ * a bookmark's address is indistinguishable from a link someone chose to send.
76
+ * 3. Nothing known: a plain link means "show me the talk", and a bare hash is a slide someone
77
+ * typed or pasted.
78
+ */
79
+ const startFollowing = useMemo(() => {
80
+ if (projector) return true;
81
+ if (flags.at) return false;
82
+ return rememberedFollow(config) ?? !wantedAtLoad;
83
+ }, [
84
+ config,
85
+ flags.at,
86
+ projector,
87
+ wantedAtLoad
88
+ ]);
89
+ const nav = useMemo(() => {
90
+ const initial = {
91
+ ...initialNav(),
92
+ follow: startFollowing
93
+ };
94
+ const fromHash = wantedAtLoad;
95
+ return createStore(fromHash ? goto(initial, columns, fromHash) : initial);
96
+ }, [
97
+ columns,
98
+ config.hashOneBasedIndex,
99
+ startFollowing
100
+ ]);
101
+ const ui = useMemo(() => createStore({
102
+ help: false,
103
+ jump: false,
104
+ search: false,
105
+ lightbox: null,
106
+ zoom: null,
107
+ drawing: false,
108
+ pointer: false,
109
+ spotlight: false,
110
+ colorScheme: stored(config, "scheme", ["light", "dark"]) ?? resolveScheme(config.colorScheme),
111
+ cursorHidden: false,
112
+ fullscreen: false,
113
+ hints: false,
114
+ themeMenu: false,
115
+ projectorOpen: false,
116
+ theme: stored(config, "theme", config.themes) ?? config.theme,
117
+ termSync: (stored(config, "termsync", ["on", "off"]) ?? (config.glossary.sync ? "on" : "off")) === "on",
118
+ scroll: mode === "play" && (flags.view === "scroll" || flags.view !== "slide" && config.view === "scroll"),
119
+ toast: null
120
+ }), [
121
+ config.colorScheme,
122
+ config.view,
123
+ flags.view,
124
+ mode
125
+ ]);
126
+ const layout = useMemo(() => createStore(computeLayout(config, viewport())), [config]);
127
+ const actions = useMemo(() => {
128
+ const withTransition = (fn) => {
129
+ const before = nav.get();
130
+ const moved = fn(before);
131
+ if (moved === before) return;
132
+ const after = sync.source === null && moved.follow && !projector ? {
133
+ ...moved,
134
+ follow: false
135
+ } : moved;
136
+ if ((options.get(`${before.h},${before.v}`)?.transition ?? config.transition) === "view-transition" && "startViewTransition" in document && (after.h !== before.h || after.v !== before.v)) document.startViewTransition(() => flushSync(() => nav.set(after)));
137
+ else nav.set(after);
138
+ };
139
+ const uiToggle = (key, force) => ui.update((s) => ({
140
+ ...s,
141
+ [key]: force ?? !s[key]
142
+ }));
143
+ return {
144
+ next: () => withTransition((s) => next(s, columns, config)),
145
+ prev: () => withTransition((s) => prev(s, columns, config)),
146
+ nextSlide: () => withTransition((s) => availableRoutes(s, columns, config).down ? down(s, columns) : right(s, columns, config)),
147
+ prevSlide: () => withTransition((s) => availableRoutes(s, columns, config).up ? up(s, columns) : left(s, columns, config)),
148
+ left: () => withTransition((s) => {
149
+ if (s.click > 0) return prevClick(s);
150
+ if (config.navigationMode === "linear") return prev(s, columns, config);
151
+ return config.rtl ? right(s, columns, config) : left(s, columns, config);
152
+ }),
153
+ right: () => withTransition((s) => {
154
+ const stepped = nextClick(s);
155
+ if (stepped !== s) return stepped;
156
+ if (config.navigationMode === "linear") return next(s, columns, config);
157
+ return config.rtl ? left(s, columns, config) : right(s, columns, config);
158
+ }),
159
+ up: () => withTransition((s) => up(s, columns)),
160
+ down: () => withTransition((s) => down(s, columns)),
161
+ first: () => withTransition((s) => first(s, columns)),
162
+ last: () => withTransition((s) => last(s, columns)),
163
+ goto: (h, v = 0, click = 0) => withTransition((s) => goto(s, columns, {
164
+ h,
165
+ v,
166
+ click
167
+ })),
168
+ gotoFlat: (flat) => {
169
+ const p = fromFlat(columns, flat);
170
+ withTransition((s) => goto(s, columns, {
171
+ ...p,
172
+ click: 0
173
+ }));
174
+ },
175
+ gotoId: (id) => {
176
+ const p = parseHash(`#/${id}`, columns, config.hashOneBasedIndex);
177
+ if (p) withTransition((s) => goto(s, columns, p));
178
+ },
179
+ /**
180
+ * The share window: open one that mirrors this deck, or close the one this window opened.
181
+ *
182
+ * It is a plain `window.open`, so it works in any browser — a popup that follows along — and
183
+ * a desktop shell that wants to give it a frame of its own only has to recognise the URL.
184
+ */
185
+ toggleProjector: (force) => {
186
+ const open = !!projectorWindow.ref && !projectorWindow.ref.closed;
187
+ if (force === void 0 ? open : !force) {
188
+ projectorWindow.ref?.close();
189
+ projectorWindow.ref = null;
190
+ ui.update((s) => s.projectorOpen ? {
191
+ ...s,
192
+ projectorOpen: false
193
+ } : s);
194
+ return;
195
+ }
196
+ if (open) {
197
+ projectorWindow.ref?.focus();
198
+ return;
199
+ }
200
+ const url = `${base}?projector${formatHash(nav.get(), {
201
+ withClick: config.fragmentInURL,
202
+ oneBased: config.hashOneBasedIndex
203
+ })}`;
204
+ projectorWindow.ref = window.open(url, `deck-projector-${config.slug || "deck"}`, "width=1280,height=720");
205
+ ui.update((s) => ({
206
+ ...s,
207
+ projectorOpen: !!projectorWindow.ref
208
+ }));
209
+ },
210
+ setFollow: (on) => nav.update((s) => {
211
+ if (!on) return s.follow ? {
212
+ ...s,
213
+ follow: false
214
+ } : s;
215
+ return s.presenter ? {
216
+ ...goto(s, columns, s.presenter),
217
+ follow: true
218
+ } : {
219
+ ...s,
220
+ follow: true
221
+ };
222
+ }),
223
+ toggleOverview: (force) => {
224
+ if (!config.overview) return;
225
+ nav.update((s) => ({
226
+ ...s,
227
+ overview: force ?? !s.overview
228
+ }));
229
+ },
230
+ togglePause: (force) => config.pause && nav.update((s) => ({
231
+ ...s,
232
+ paused: force ?? !s.paused
233
+ })),
234
+ toggleHelp: (force) => config.help && uiToggle("help", force),
235
+ toggleJump: (force) => config.jumpToSlide && uiToggle("jump", force),
236
+ toggleSearch: (force) => config.search && uiToggle("search", force),
237
+ toggleFullscreen: () => {
238
+ const el = document.documentElement;
239
+ if (document.fullscreenElement) document.exitFullscreen();
240
+ else el.requestFullscreen?.();
241
+ },
242
+ toggleAutoSlide: () => nav.update((s) => ({
243
+ ...s,
244
+ autoSliding: !s.autoSliding
245
+ })),
246
+ toggleDrawing: (force) => config.drawings.enabled && uiToggle("drawing", force),
247
+ togglePointer: (force) => uiToggle("pointer", force),
248
+ toggleColorScheme: () => ui.update((s) => {
249
+ const colorScheme = s.colorScheme === "dark" ? "light" : "dark";
250
+ remember(config, "scheme", colorScheme);
251
+ return {
252
+ ...s,
253
+ colorScheme
254
+ };
255
+ }),
256
+ setColorScheme: (colorScheme) => {
257
+ remember(config, "scheme", colorScheme);
258
+ ui.update((s) => ({
259
+ ...s,
260
+ colorScheme
261
+ }));
262
+ },
263
+ toggleHints: (force) => uiToggle("hints", force),
264
+ toggleThemeMenu: (force) => uiToggle("themeMenu", force),
265
+ toggleTermSync: (force) => ui.update((s) => {
266
+ const termSync = force ?? !s.termSync;
267
+ remember(config, "termsync", termSync ? "on" : "off");
268
+ return {
269
+ ...s,
270
+ termSync
271
+ };
272
+ }),
273
+ setTheme: (theme) => {
274
+ remember(config, "theme", theme);
275
+ ui.update((s) => ({
276
+ ...s,
277
+ theme,
278
+ themeMenu: false
279
+ }));
280
+ },
281
+ openPresenter: () => {
282
+ const p = nav.get();
283
+ window.open(`${base}presenter${formatHash(p, { withClick: true })}`, `deck-presenter-${config.slug || "deck"}`, "width=1280,height=800");
284
+ },
285
+ openLightbox: (kind, src, fit = "scale-down") => ui.update((s) => ({
286
+ ...s,
287
+ lightbox: {
288
+ kind,
289
+ src,
290
+ fit
291
+ }
292
+ })),
293
+ closeLightbox: () => ui.update((s) => ({
294
+ ...s,
295
+ lightbox: null
296
+ })),
297
+ zoomTo: (x, y) => ui.update((s) => ({
298
+ ...s,
299
+ zoom: s.zoom ? null : {
300
+ x,
301
+ y,
302
+ scale: 2
303
+ }
304
+ })),
305
+ zoomOut: () => ui.update((s) => ({
306
+ ...s,
307
+ zoom: null
308
+ })),
309
+ toast: (message) => {
310
+ ui.update((s) => ({
311
+ ...s,
312
+ toast: message
313
+ }));
314
+ setTimeout(() => ui.update((s) => s.toast === message ? {
315
+ ...s,
316
+ toast: null
317
+ } : s), 1800);
318
+ }
319
+ };
320
+ }, [
321
+ nav,
322
+ ui,
323
+ columns,
324
+ config,
325
+ options,
326
+ base,
327
+ sync,
328
+ projector,
329
+ projectorWindow
330
+ ]);
331
+ const terms = useMemo(() => createStore(null), []);
332
+ const glossary = useMemo(() => new Map((manifest.glossary ?? []).map((e) => [e.id, e])), [manifest.glossary]);
333
+ const runtime = useMemo(() => ({
334
+ manifest,
335
+ config,
336
+ columns,
337
+ options,
338
+ nav,
339
+ ui,
340
+ layout,
341
+ mode,
342
+ base,
343
+ components,
344
+ actions,
345
+ windowId,
346
+ sync,
347
+ isPresenter: mode === "presenter",
348
+ projector,
349
+ terms,
350
+ glossary
351
+ }), [
352
+ manifest,
353
+ config,
354
+ columns,
355
+ options,
356
+ nav,
357
+ ui,
358
+ layout,
359
+ mode,
360
+ base,
361
+ components,
362
+ actions,
363
+ windowId,
364
+ sync,
365
+ projector,
366
+ terms,
367
+ glossary
368
+ ]);
369
+ useDeckEffects(runtime, flags, wantedAtLoad);
370
+ useTermEffects(runtime);
371
+ useEffect(() => {
372
+ const id = setInterval(() => {
373
+ if (!projectorWindow.ref?.closed) return;
374
+ projectorWindow.ref = null;
375
+ ui.update((s) => s.projectorOpen ? {
376
+ ...s,
377
+ projectorOpen: false
378
+ } : s);
379
+ }, 1e3);
380
+ return () => clearInterval(id);
381
+ }, [projectorWindow, ui]);
382
+ useEffect(() => {
383
+ if (typeof window === "undefined") return;
384
+ const w = window;
385
+ w.__deck = runtime;
386
+ }, [runtime]);
387
+ const Page = mode !== "play" && mode !== "print" ? pages[mode] : null;
388
+ const view = mode === "print" ? /* @__PURE__ */ jsx(PrintView, {}) : Page ? /* @__PURE__ */ jsx(Page, {}) : mode !== "play" ? /* @__PURE__ */ jsx(MissingPage, { mode }) : /* @__PURE__ */ jsx(PlayView, {});
389
+ const wrapped = Children.toArray(children).reduceRight((inner, child) => isValidElement(child) ? cloneElement(child, {}, inner) : inner, view);
390
+ return /* @__PURE__ */ jsx(DeckContext.Provider, {
391
+ value: runtime,
392
+ children: /* @__PURE__ */ jsx(DeckRoot, { children: wrapped })
393
+ });
394
+ }
395
+ function MissingPage({ mode }) {
396
+ return /* @__PURE__ */ jsxs("div", {
397
+ className: "deck-missing-page",
398
+ children: [
399
+ "This deck was built without the ",
400
+ /* @__PURE__ */ jsx("code", { children: mode }),
401
+ " page. Pass it to",
402
+ " ",
403
+ /* @__PURE__ */ jsx("code", { children: "<Deck pages>" }),
404
+ "."
405
+ ]
406
+ });
407
+ }
408
+ /** The play view: backgrounds, the stage or the scroll view, the overview, and the chrome. */
409
+ function PlayView() {
410
+ const deck = useContextStrict();
411
+ const [scroll, setScroll] = useState(() => deck.ui.get().scroll);
412
+ useEffect(() => deck.ui.subscribe((s) => setScroll(s.scroll)), [deck.ui]);
413
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
414
+ scroll ? /* @__PURE__ */ jsx(ScrollView, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
415
+ /* @__PURE__ */ jsx(Backgrounds, {}),
416
+ /* @__PURE__ */ jsx(ZoomLayer, { children: /* @__PURE__ */ jsx(Stage, {}) }),
417
+ /* @__PURE__ */ jsx(Overview, {})
418
+ ] }),
419
+ /* @__PURE__ */ jsx(Chrome, {}),
420
+ /* @__PURE__ */ jsx(Lightbox, {})
421
+ ] });
422
+ }
423
+ function useContextStrict() {
424
+ const ctx = useContext(DeckContext);
425
+ if (!ctx) throw new Error("no deck");
426
+ return ctx;
427
+ }
428
+ function DeckRoot({ children }) {
429
+ const deck = useContextStrict();
430
+ const [scheme, setScheme] = useState(deck.ui.get().colorScheme);
431
+ const [theme, setTheme] = useState(deck.ui.get().theme);
432
+ const [cursorHidden, setCursorHidden] = useState(false);
433
+ const [paused, setPaused] = useState(false);
434
+ useEffect(() => deck.ui.subscribe((s) => {
435
+ setScheme(s.colorScheme);
436
+ setTheme(s.theme);
437
+ setCursorHidden(s.cursorHidden);
438
+ }), [deck.ui]);
439
+ useEffect(() => deck.nav.subscribe((s) => setPaused(s.paused)), [deck.nav]);
440
+ useEffect(() => {
441
+ document.documentElement.dataset.colorScheme = scheme;
442
+ document.documentElement.classList.toggle("dark", scheme === "dark");
443
+ }, [scheme]);
444
+ return /* @__PURE__ */ jsx("div", {
445
+ className: [
446
+ "deck-root",
447
+ `theme-${theme}`,
448
+ `mode-${deck.mode}`,
449
+ cursorHidden ? "cursor-hidden" : "",
450
+ deck.projector ? "is-projector" : "",
451
+ paused ? "is-paused" : "",
452
+ deck.config.selectable ? "" : "no-select",
453
+ deck.config.rtl ? "rtl" : ""
454
+ ].filter(Boolean).join(" "),
455
+ "data-color-scheme": scheme,
456
+ dir: deck.config.rtl ? "rtl" : void 0,
457
+ children
458
+ });
459
+ }
460
+ /** Per-deck, per-browser memory for the theme menu's choices. */
461
+ function stored(config, what, allowed) {
462
+ try {
463
+ const v = localStorage.getItem(`deck.${what}.${config.slug || "deck"}`);
464
+ return v && allowed.includes(v) ? v : null;
465
+ } catch {
466
+ return null;
467
+ }
468
+ }
469
+ function remember(config, what, value) {
470
+ try {
471
+ localStorage.setItem(`deck.${what}.${config.slug || "deck"}`, value);
472
+ } catch {}
473
+ }
474
+ /**
475
+ * How long a viewer's follow choice speaks for them: the same twelve hours a presenter's own
476
+ * session lasts. Long enough that a bookmark taken mid-talk, a tab closed at the interval or a
477
+ * phone that went to sleep comes back in step; short enough that a choice made at one talk does
478
+ * not quietly govern the next one on the same deck.
479
+ */
480
+ const FOLLOW_MEMORY_MS = 432e5;
481
+ /** What this viewer last chose for this deck, if it is still about this talk. */
482
+ function rememberedFollow(config) {
483
+ try {
484
+ const [value, at] = (localStorage.getItem(`deck.follow.${config.slug || "deck"}`) ?? "").split(":");
485
+ if (value !== "on" && value !== "off" || !Number.isFinite(Number(at))) return null;
486
+ return Date.now() - Number(at) > FOLLOW_MEMORY_MS ? null : value === "on";
487
+ } catch {
488
+ return null;
489
+ }
490
+ }
491
+ function rememberFollow(config, follow) {
492
+ try {
493
+ localStorage.setItem(`deck.follow.${config.slug || "deck"}`, `${follow ? "on" : "off"}:${Date.now()}`);
494
+ } catch {}
495
+ }
496
+ function resolveScheme(scheme) {
497
+ if (scheme === "light" || scheme === "dark") return scheme;
498
+ if (typeof window === "undefined") return "light";
499
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
500
+ }
501
+ function viewport() {
502
+ if (typeof window === "undefined") return {
503
+ width: 1920,
504
+ height: 1080
505
+ };
506
+ return {
507
+ width: window.innerWidth,
508
+ height: window.innerHeight
509
+ };
510
+ }
511
+ /** Every browser side effect, in one place. */
512
+ function useDeckEffects(deck, flags, wantedAtLoad) {
513
+ const { nav, ui, layout, config, columns, actions, base, mode } = deck;
514
+ useEffect(() => {
515
+ document.title = config.title;
516
+ if (!config.fonts.length) return;
517
+ const link = document.createElement("link");
518
+ link.rel = "stylesheet";
519
+ link.href = `https://fonts.googleapis.com/css2?${config.fonts.map((f) => `family=${encodeURIComponent(f).replace(/%3A/g, ":").replace(/%40/g, "@").replace(/%3B/g, ";")}`).join("&")}&display=swap`;
520
+ document.head.appendChild(link);
521
+ return () => link.remove();
522
+ }, [config.title, config.fonts]);
523
+ useEffect(() => {
524
+ const measure = () => layout.set(computeLayout(config, viewport()));
525
+ measure();
526
+ window.addEventListener("resize", measure);
527
+ const narrow = () => {
528
+ if (mode !== "play" || !config.scrollActivationWidth || flags.view === "slide") return;
529
+ const on = window.innerWidth < config.scrollActivationWidth;
530
+ ui.update((s) => s.scroll === on || flags.view === "scroll" ? s : {
531
+ ...s,
532
+ scroll: on
533
+ });
534
+ };
535
+ narrow();
536
+ window.addEventListener("resize", narrow);
537
+ return () => {
538
+ window.removeEventListener("resize", measure);
539
+ window.removeEventListener("resize", narrow);
540
+ };
541
+ }, [
542
+ layout,
543
+ config,
544
+ ui,
545
+ mode,
546
+ flags.view
547
+ ]);
548
+ useEffect(() => {
549
+ if (!config.hash || mode === "print") return;
550
+ const write = (s) => {
551
+ const meta = columns[s.h]?.slides[s.v];
552
+ const id = meta && typeof meta.frontmatter.id === "string" ? meta.frontmatter.id : null;
553
+ const next = formatHash(s, {
554
+ withClick: config.fragmentInURL,
555
+ oneBased: config.hashOneBasedIndex,
556
+ id
557
+ });
558
+ if (window.location.hash === next) return;
559
+ const url = `${window.location.pathname}${window.location.search}${next}`;
560
+ if (config.history) history.pushState(null, "", url);
561
+ else history.replaceState(null, "", url);
562
+ };
563
+ if (flags.at) {
564
+ const q = new URLSearchParams(window.location.search);
565
+ q.delete("at");
566
+ const search = q.toString();
567
+ history.replaceState(null, "", `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`);
568
+ }
569
+ let pending = wantedAtLoad && (wantedAtLoad.click ?? 0) > 0 ? wantedAtLoad : null;
570
+ write(nav.get());
571
+ const unsub = nav.subscribe((s, prev) => {
572
+ if (pending) {
573
+ const target = pending;
574
+ const onSlide = s.h === target.h && s.v === target.v;
575
+ if (onSlide && (s.totals[`${s.h},${s.v}`] ?? 0) >= (target.click ?? 0)) {
576
+ pending = null;
577
+ if (s.click < (target.click ?? 0)) queueMicrotask(() => nav.update((st) => st.h === target.h && st.v === target.v ? goto(st, columns, target) : st));
578
+ } else if (!onSlide && (s.h !== prev.h || s.v !== prev.v)) pending = null;
579
+ }
580
+ if (s.h !== prev.h || s.v !== prev.v || s.click !== prev.click) write(s);
581
+ });
582
+ const onHash = () => {
583
+ const p = parseHash(window.location.hash, columns, config.hashOneBasedIndex);
584
+ if (!p) return;
585
+ const s = nav.get();
586
+ if (p.h === s.h && p.v === s.v && (p.click ?? 0) === s.click) return;
587
+ nav.update((st) => ({
588
+ ...goto(st, columns, p),
589
+ follow: false
590
+ }));
591
+ };
592
+ window.addEventListener("hashchange", onHash);
593
+ return () => {
594
+ unsub();
595
+ window.removeEventListener("hashchange", onHash);
596
+ };
597
+ }, [
598
+ nav,
599
+ columns,
600
+ config,
601
+ mode,
602
+ flags.at
603
+ ]);
604
+ useEffect(() => {
605
+ if (mode !== "play" && mode !== "audience") return;
606
+ let last = null;
607
+ const save = (follow) => {
608
+ if (follow === last) return;
609
+ last = follow;
610
+ rememberFollow(config, follow);
611
+ };
612
+ save(nav.get().follow);
613
+ return nav.subscribe((s) => save(s.follow));
614
+ }, [
615
+ nav,
616
+ config,
617
+ mode
618
+ ]);
619
+ useEffect(() => {
620
+ if (!config.keyboard || deck.projector || mode === "print" || mode === "audience") return;
621
+ let pendingG = 0;
622
+ const onKey = (e) => {
623
+ if (isEditable(e.target)) return;
624
+ const u = ui.get();
625
+ const s = nav.get();
626
+ if (u.lightbox) {
627
+ if (e.code === "Escape") actions.closeLightbox();
628
+ return;
629
+ }
630
+ if (u.zoom && e.code === "Escape") return actions.zoomOut();
631
+ if (u.help && (e.code === "Escape" || e.code === "Shift+Slash")) return actions.toggleHelp(false);
632
+ if (u.jump || u.search || u.hints) return;
633
+ if (u.themeMenu && e.code === "Escape") {
634
+ e.preventDefault();
635
+ actions.toggleThemeMenu(false);
636
+ return;
637
+ }
638
+ if (e.code === "KeyG" && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
639
+ e.preventDefault();
640
+ if (pendingG && Date.now() - pendingG < 450) {
641
+ pendingG = 0;
642
+ actions.first();
643
+ } else {
644
+ pendingG = Date.now();
645
+ setTimeout(() => {
646
+ if (pendingG && Date.now() - pendingG >= 440) {
647
+ pendingG = 0;
648
+ actions.toggleJump(true);
649
+ }
650
+ }, 450);
651
+ }
652
+ return;
653
+ }
654
+ const action = actionFor(e, config.keyboardOverrides ?? {});
655
+ if (!action) return;
656
+ if (s.overview && action !== "overview" && action !== "escape") {
657
+ if ([
658
+ "next",
659
+ "prev",
660
+ "left",
661
+ "right",
662
+ "up",
663
+ "down",
664
+ "first",
665
+ "last"
666
+ ].includes(action)) {
667
+ e.preventDefault();
668
+ actions[action]();
669
+ } else if (action === "fullscreen") actions.toggleFullscreen();
670
+ else if (action === "jump") actions.toggleJump(true);
671
+ else if (action === "search") actions.toggleSearch(true);
672
+ else if (action === "themeMenu") actions.toggleThemeMenu();
673
+ else if (e.code === "Enter" || e.code === "Space") actions.toggleOverview(false);
674
+ return;
675
+ }
676
+ if (s.paused && action !== "pause") {
677
+ actions.togglePause(false);
678
+ return;
679
+ }
680
+ e.preventDefault();
681
+ switch (action) {
682
+ case "next":
683
+ case "prev":
684
+ case "nextSlide":
685
+ case "prevSlide":
686
+ case "left":
687
+ case "right":
688
+ case "up":
689
+ case "down":
690
+ case "first":
691
+ case "last":
692
+ actions[action]();
693
+ break;
694
+ case "overview":
695
+ actions.toggleOverview();
696
+ break;
697
+ case "fullscreen":
698
+ actions.toggleFullscreen();
699
+ break;
700
+ case "pause":
701
+ actions.togglePause();
702
+ break;
703
+ case "presenter":
704
+ if (mode === "play") actions.openPresenter();
705
+ break;
706
+ case "help":
707
+ actions.toggleHelp();
708
+ break;
709
+ case "jump":
710
+ actions.toggleJump(true);
711
+ break;
712
+ case "search":
713
+ actions.toggleSearch(true);
714
+ break;
715
+ case "autoSlide":
716
+ actions.toggleAutoSlide();
717
+ break;
718
+ case "draw":
719
+ actions.toggleDrawing();
720
+ break;
721
+ case "pointer":
722
+ actions.togglePointer();
723
+ break;
724
+ case "colorScheme":
725
+ actions.toggleColorScheme();
726
+ break;
727
+ case "hints":
728
+ actions.toggleHints();
729
+ break;
730
+ case "themeMenu": actions.toggleThemeMenu();
731
+ }
732
+ };
733
+ window.addEventListener("keydown", onKey);
734
+ return () => window.removeEventListener("keydown", onKey);
735
+ }, [
736
+ config,
737
+ nav,
738
+ ui,
739
+ actions,
740
+ mode,
741
+ deck.projector
742
+ ]);
743
+ useEffect(() => {
744
+ if (mode !== "play" || deck.projector) return;
745
+ const swipe = createSwipeTracker();
746
+ const onDown = (e) => {
747
+ if (e.pointerType === "touch" && config.touch) swipe.start(e.clientX, e.clientY, e.timeStamp);
748
+ };
749
+ const onUp = (e) => {
750
+ if (e.pointerType !== "touch" || !config.touch) return;
751
+ const dir = swipe.end(e.clientX, e.clientY, e.timeStamp);
752
+ if (!dir) return;
753
+ if (dir === "left") actions.next();
754
+ else if (dir === "right") actions.prev();
755
+ else if (dir === "up") actions.down();
756
+ else if (dir === "down") actions.up();
757
+ };
758
+ let wheelLock = 0;
759
+ const onWheel = (e) => {
760
+ if (!config.mouseWheel || ui.get().scroll || ui.get().lightbox) return;
761
+ const now = Date.now();
762
+ if (now - wheelLock < 600) return;
763
+ if (Math.abs(e.deltaY) < 10) return;
764
+ wheelLock = now;
765
+ if (e.deltaY > 0) actions.next();
766
+ else actions.prev();
767
+ };
768
+ const onClick = (e) => {
769
+ const target = e.target;
770
+ if (!target) return;
771
+ const preview = target.closest("[data-preview-image], [data-preview-video], [data-preview-link]");
772
+ if (preview) {
773
+ e.preventDefault();
774
+ const fit = preview.dataset.previewFit ?? "scale-down";
775
+ if (preview.dataset.previewImage !== void 0) actions.openLightbox("image", preview.dataset.previewImage || preview.getAttribute("src") || "", fit);
776
+ else if (preview.dataset.previewVideo !== void 0) actions.openLightbox("video", preview.dataset.previewVideo || preview.getAttribute("src") || "", fit);
777
+ else actions.openLightbox("iframe", preview.dataset.previewLink || preview.getAttribute("href") || "", fit);
778
+ return;
779
+ }
780
+ if (config.previewLinks) {
781
+ const a = target.closest("a[href^='http']");
782
+ if (a && !a.dataset.noPreview) {
783
+ e.preventDefault();
784
+ actions.openLightbox("iframe", a.href);
785
+ return;
786
+ }
787
+ }
788
+ if (config.zoom && e.altKey) {
789
+ const l = layout.get();
790
+ const p = toStage(l, e.clientX, e.clientY);
791
+ actions.zoomTo(p.x, p.y);
792
+ }
793
+ };
794
+ let idle = null;
795
+ const wake = () => {
796
+ if (!config.hideInactiveCursor) return;
797
+ ui.update((s) => s.cursorHidden ? {
798
+ ...s,
799
+ cursorHidden: false
800
+ } : s);
801
+ if (idle) clearTimeout(idle);
802
+ idle = setTimeout(() => ui.update((s) => ({
803
+ ...s,
804
+ cursorHidden: true
805
+ })), config.hideCursorTime);
806
+ };
807
+ window.addEventListener("pointerdown", onDown);
808
+ window.addEventListener("pointerup", onUp);
809
+ window.addEventListener("wheel", onWheel, { passive: true });
810
+ window.addEventListener("click", onClick);
811
+ window.addEventListener("pointermove", wake);
812
+ wake();
813
+ return () => {
814
+ window.removeEventListener("pointerdown", onDown);
815
+ window.removeEventListener("pointerup", onUp);
816
+ window.removeEventListener("wheel", onWheel);
817
+ window.removeEventListener("click", onClick);
818
+ window.removeEventListener("pointermove", wake);
819
+ if (idle) clearTimeout(idle);
820
+ };
821
+ }, [
822
+ config,
823
+ actions,
824
+ ui,
825
+ layout,
826
+ mode
827
+ ]);
828
+ useEffect(() => {
829
+ if (!deck.projector || !config.hideInactiveCursor) return;
830
+ let idle = null;
831
+ const sleep = () => ui.update((s) => s.cursorHidden ? s : {
832
+ ...s,
833
+ cursorHidden: true
834
+ });
835
+ const wake = () => {
836
+ ui.update((s) => s.cursorHidden ? {
837
+ ...s,
838
+ cursorHidden: false
839
+ } : s);
840
+ if (idle) clearTimeout(idle);
841
+ idle = setTimeout(sleep, config.hideCursorTime);
842
+ };
843
+ window.addEventListener("pointermove", wake);
844
+ idle = setTimeout(sleep, config.hideCursorTime);
845
+ return () => {
846
+ window.removeEventListener("pointermove", wake);
847
+ if (idle) clearTimeout(idle);
848
+ };
849
+ }, [
850
+ deck.projector,
851
+ config.hideInactiveCursor,
852
+ config.hideCursorTime,
853
+ ui
854
+ ]);
855
+ useEffect(() => {
856
+ const onFs = () => ui.update((s) => ({
857
+ ...s,
858
+ fullscreen: !!document.fullscreenElement
859
+ }));
860
+ document.addEventListener("fullscreenchange", onFs);
861
+ let lock = null;
862
+ const acquire = async () => {
863
+ if (!config.wakeLock || mode === "print") return;
864
+ try {
865
+ lock = await navigator.wakeLock?.request("screen") ?? null;
866
+ } catch {
867
+ lock = null;
868
+ }
869
+ };
870
+ acquire();
871
+ const onVis = () => document.visibilityState === "visible" && void acquire();
872
+ document.addEventListener("visibilitychange", onVis);
873
+ return () => {
874
+ document.removeEventListener("fullscreenchange", onFs);
875
+ document.removeEventListener("visibilitychange", onVis);
876
+ lock?.release();
877
+ };
878
+ }, [
879
+ ui,
880
+ config.wakeLock,
881
+ mode
882
+ ]);
883
+ useEffect(() => {
884
+ if (!config.autoSlide || mode !== "play") return;
885
+ nav.update((s) => ({
886
+ ...s,
887
+ autoSliding: true
888
+ }));
889
+ let timer = null;
890
+ const schedule = () => {
891
+ if (timer) clearTimeout(timer);
892
+ const s = nav.get();
893
+ if (!s.autoSliding || s.paused || s.overview) return;
894
+ const ms = deck.options.get(`${s.h},${s.v}`)?.autoSlide ?? config.autoSlide;
895
+ timer = setTimeout(() => {
896
+ const before = nav.get();
897
+ deck.sync.source = "auto";
898
+ try {
899
+ actions.next();
900
+ if (nav.get() === before && config.loop) actions.first();
901
+ } finally {
902
+ deck.sync.source = null;
903
+ }
904
+ schedule();
905
+ }, ms);
906
+ };
907
+ schedule();
908
+ const unsub = nav.subscribe(() => schedule());
909
+ const stop = () => config.autoSlideStoppable && nav.update((s) => s.autoSliding ? {
910
+ ...s,
911
+ autoSliding: false
912
+ } : s);
913
+ window.addEventListener("pointerdown", stop);
914
+ return () => {
915
+ unsub();
916
+ if (timer) clearTimeout(timer);
917
+ window.removeEventListener("pointerdown", stop);
918
+ };
919
+ }, [
920
+ config,
921
+ nav,
922
+ actions,
923
+ mode,
924
+ deck.options
925
+ ]);
926
+ useEffect(() => {
927
+ if (typeof BroadcastChannel === "undefined" || mode === "print") return;
928
+ const channel = new BroadcastChannel(`deck:${config.slug || base}`);
929
+ const unsub = nav.subscribe((s, prev) => {
930
+ if (deck.sync.source) return;
931
+ if (s.h === prev.h && s.v === prev.v && s.click === prev.click && s.paused === prev.paused) return;
932
+ channel.postMessage({
933
+ type: "nav",
934
+ h: s.h,
935
+ v: s.v,
936
+ click: s.click,
937
+ paused: s.paused,
938
+ from: deck.windowId,
939
+ presenter: deck.isPresenter
940
+ });
941
+ });
942
+ channel.onmessage = (e) => {
943
+ const m = e.data;
944
+ if (m.type !== "nav" || m.from === deck.windowId) return;
945
+ if (deck.isPresenter && !m.presenter) return;
946
+ deck.sync.source = "channel";
947
+ try {
948
+ nav.update((s) => {
949
+ if (m.presenter && !s.follow) return s.paused === m.paused ? s : {
950
+ ...s,
951
+ paused: m.paused
952
+ };
953
+ return {
954
+ ...goto(s, columns, {
955
+ h: m.h,
956
+ v: m.v,
957
+ click: m.click
958
+ }),
959
+ paused: m.paused,
960
+ follow: m.presenter ? s.follow : false
961
+ };
962
+ });
963
+ } finally {
964
+ deck.sync.source = null;
965
+ }
966
+ };
967
+ return () => {
968
+ unsub();
969
+ channel.close();
970
+ };
971
+ }, [
972
+ nav,
973
+ columns,
974
+ config.slug,
975
+ base,
976
+ deck.windowId,
977
+ deck.isPresenter,
978
+ deck.sync,
979
+ mode
980
+ ]);
981
+ }
982
+ //#endregion
983
+ export { Deck };