@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,1211 @@
1
+ import { roomIdOf } from "../core/model.js";
2
+ import { initialNav, key, next, progressOf } from "../core/navigation.js";
3
+ import { createStore, useStore } from "../core/store.js";
4
+ import { useDeck } from "../react/context.js";
5
+ import { useActions, useCurrentSlide, useNav, useUi } from "../react/hooks.js";
6
+ import { BackgroundLayer } from "../react/Backgrounds.js";
7
+ import { HintLayer } from "../react/Hints.js";
8
+ import { ConfettiLayer } from "../react/ConfettiLayer.js";
9
+ import { ThemeMenu } from "../react/ThemeMenu.js";
10
+ import { Icon } from "../components/bling.js";
11
+ import { useLiveOptional, useRoomStatus } from "../live/context.js";
12
+ import { Pace, Presence, QrJoin } from "../live/components.js";
13
+ import { SlideFrame, loadSlide } from "../react/SlideFrame.js";
14
+ import { Toast } from "../react/chrome.js";
15
+ import { Stage } from "../react/Stage.js";
16
+ import { DrawingLayer } from "../draw/DrawingLayer.js";
17
+ import { copyText, shareLink } from "../live/analytics.js";
18
+ import { PointerLayer } from "../live/LiveProvider.js";
19
+ import { useEffect, useMemo, useRef, useState } from "react";
20
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
+ //#region src/presenter/index.tsx
22
+ /**
23
+ * ─────────────────────────────────────────────────────────────────────────────
24
+ * THE PRESENTER VIEW — /<slug>/presenter
25
+ * ─────────────────────────────────────────────────────────────────────────────
26
+ *
27
+ * What the speaker sees: the current slide live, the next step, the notes with [click] markers,
28
+ * a timer with pacing, the clock, the drawing and pointer tools, and the room — who is watching,
29
+ * the pace they want, their questions, the polls to open. Opened with `S` from the deck; both
30
+ * windows stay in step through a BroadcastChannel, and the room follows the presenter.
31
+ *
32
+ * Layouts (toggle with L): notes-right (default), notes-left, next-big.
33
+ * ─────────────────────────────────────────────────────────────────────────────
34
+ */
35
+ function PresenterPage() {
36
+ const deck = useDeck();
37
+ const live = useLiveOptional();
38
+ const gate = usePresenterGate();
39
+ const [layout, setLayout] = useState(() => {
40
+ try {
41
+ return localStorage.getItem("deck.presenter.layout") ?? "notes-right";
42
+ } catch {
43
+ return "notes-right";
44
+ }
45
+ });
46
+ const [mirror, setMirror] = useState(false);
47
+ const [roomOpen, setRoomOpen] = useState(() => {
48
+ try {
49
+ return localStorage.getItem("deck.presenter.room") !== "closed";
50
+ } catch {
51
+ return true;
52
+ }
53
+ });
54
+ useEffect(() => {
55
+ try {
56
+ localStorage.setItem("deck.presenter.layout", layout);
57
+ localStorage.setItem("deck.presenter.room", roomOpen ? "open" : "closed");
58
+ } catch {}
59
+ }, [layout, roomOpen]);
60
+ useEffect(() => {
61
+ const onKey = (e) => {
62
+ if (e.code === "KeyL" && !e.metaKey && !e.ctrlKey) setLayout((l) => l === "notes-right" ? "notes-left" : l === "notes-left" ? "next-big" : "notes-right");
63
+ };
64
+ window.addEventListener("keydown", onKey);
65
+ return () => window.removeEventListener("keydown", onKey);
66
+ }, []);
67
+ if (live && gate.required && !gate.ok) return /* @__PURE__ */ jsx(PresenterLogin, { gate });
68
+ return /* @__PURE__ */ jsxs("div", {
69
+ className: `deck-presenter layout-${layout} room-${roomOpen ? "open" : "collapsed"}`,
70
+ children: [
71
+ /* @__PURE__ */ jsxs("header", {
72
+ className: "deck-presenter-bar",
73
+ children: [
74
+ /* @__PURE__ */ jsx("strong", { children: deck.config.title }),
75
+ /* @__PURE__ */ jsx(PresenterTimer, {}),
76
+ /* @__PURE__ */ jsx(Clock, {}),
77
+ /* @__PURE__ */ jsx("span", { className: "deck-presenter-spacer" }),
78
+ /* @__PURE__ */ jsx(PresenterTools, {
79
+ mirror,
80
+ setMirror
81
+ })
82
+ ]
83
+ }),
84
+ /* @__PURE__ */ jsxs("main", {
85
+ className: "deck-presenter-main",
86
+ children: [
87
+ /* @__PURE__ */ jsxs("section", {
88
+ className: "deck-presenter-current",
89
+ children: [/* @__PURE__ */ jsx("div", {
90
+ className: "deck-presenter-label",
91
+ children: "Current"
92
+ }), mirror ? /* @__PURE__ */ jsx(ScreenMirror, {}) : /* @__PURE__ */ jsx(LiveStage, {})]
93
+ }),
94
+ /* @__PURE__ */ jsxs("section", {
95
+ className: "deck-presenter-next",
96
+ children: [/* @__PURE__ */ jsx("div", {
97
+ className: "deck-presenter-label",
98
+ children: "Next"
99
+ }), /* @__PURE__ */ jsx(NextPreview, {})]
100
+ }),
101
+ /* @__PURE__ */ jsxs("section", {
102
+ className: "deck-presenter-notes",
103
+ children: [/* @__PURE__ */ jsx("div", {
104
+ className: "deck-presenter-label",
105
+ children: "Notes"
106
+ }), /* @__PURE__ */ jsx(NotesPanel, {})]
107
+ }),
108
+ /* @__PURE__ */ jsx("aside", {
109
+ className: "deck-presenter-room",
110
+ children: /* @__PURE__ */ jsx(RoomPanel, {
111
+ open: roomOpen,
112
+ setOpen: setRoomOpen
113
+ })
114
+ })
115
+ ]
116
+ }),
117
+ /* @__PURE__ */ jsx(Toast, {}),
118
+ /* @__PURE__ */ jsx(HintLayer, {})
119
+ ]
120
+ });
121
+ }
122
+ function usePresenterGate() {
123
+ const deck = useDeck();
124
+ const api = deck.config.live.api;
125
+ const [state, setState] = useState({
126
+ ok: false,
127
+ required: !!api,
128
+ checking: !!api,
129
+ error: null
130
+ });
131
+ const url = `${api}/decks/${encodeURIComponent(roomIdOf(deck.config))}/presenter`;
132
+ useEffect(() => {
133
+ if (!api) return;
134
+ let alive = true;
135
+ fetch(url, { credentials: "include" }).then(async (r) => {
136
+ const j = await r.json().catch(() => ({}));
137
+ if (alive) setState({
138
+ ok: !!j.ok,
139
+ required: j.required !== false,
140
+ checking: false,
141
+ error: null
142
+ });
143
+ }).catch(() => alive && setState({
144
+ ok: true,
145
+ required: false,
146
+ checking: false,
147
+ error: null
148
+ }));
149
+ return () => {
150
+ alive = false;
151
+ };
152
+ }, [api, url]);
153
+ const login = async (passcode) => {
154
+ setState((s) => ({
155
+ ...s,
156
+ checking: true,
157
+ error: null
158
+ }));
159
+ const r = await fetch(`${url}/login`, {
160
+ method: "POST",
161
+ credentials: "include",
162
+ headers: { "Content-Type": "application/json" },
163
+ body: JSON.stringify({ passcode })
164
+ });
165
+ if (r.ok) {
166
+ setState({
167
+ ok: true,
168
+ required: true,
169
+ checking: false,
170
+ error: null
171
+ });
172
+ window.location.reload();
173
+ } else setState((s) => ({
174
+ ...s,
175
+ checking: false,
176
+ error: r.status === 401 ? "That passcode is wrong." : `The host answered ${r.status}.`
177
+ }));
178
+ };
179
+ return {
180
+ ...state,
181
+ login
182
+ };
183
+ }
184
+ function PresenterLogin({ gate }) {
185
+ const [code, setCode] = useState("");
186
+ return /* @__PURE__ */ jsx("div", {
187
+ className: "deck-gate",
188
+ children: /* @__PURE__ */ jsxs("form", {
189
+ onSubmit: (e) => {
190
+ e.preventDefault();
191
+ gate.login(code);
192
+ },
193
+ children: [
194
+ /* @__PURE__ */ jsx("h1", { children: "Presenter" }),
195
+ /* @__PURE__ */ jsx("p", { children: "This deck's presenter view is protected. Enter the passcode set for the deck on the host." }),
196
+ /* @__PURE__ */ jsx("input", {
197
+ type: "password",
198
+ value: code,
199
+ onChange: (e) => setCode(e.target.value),
200
+ placeholder: "Passcode",
201
+ autoFocus: true,
202
+ disabled: gate.checking
203
+ }),
204
+ /* @__PURE__ */ jsx("button", {
205
+ type: "submit",
206
+ disabled: gate.checking || !code,
207
+ children: gate.checking ? "Checking…" : "Open the presenter view"
208
+ }),
209
+ gate.error ? /* @__PURE__ */ jsx("p", {
210
+ className: "deck-gate-error",
211
+ children: gate.error
212
+ }) : null
213
+ ]
214
+ })
215
+ });
216
+ }
217
+ /** Fit the authored canvas into a box: scale to the shorter side, centre the rest. */
218
+ function useFit(box, width, height) {
219
+ const [fit, setFit] = useState({
220
+ scale: .2,
221
+ left: 0,
222
+ top: 0
223
+ });
224
+ useEffect(() => {
225
+ const el = box.current;
226
+ if (!el) return;
227
+ const measure = () => {
228
+ const scale = Math.min(el.clientWidth / width, el.clientHeight / height);
229
+ setFit({
230
+ scale,
231
+ left: (el.clientWidth - width * scale) / 2,
232
+ top: (el.clientHeight - height * scale) / 2
233
+ });
234
+ };
235
+ measure();
236
+ const ro = new ResizeObserver(measure);
237
+ ro.observe(el);
238
+ return () => ro.disconnect();
239
+ }, [
240
+ box,
241
+ width,
242
+ height
243
+ ]);
244
+ return fit;
245
+ }
246
+ function LiveStage() {
247
+ const deck = useDeck();
248
+ useStore(deck.layout);
249
+ return /* @__PURE__ */ jsx("div", {
250
+ className: "deck-presenter-stage",
251
+ children: /* @__PURE__ */ jsx(ScaledStage, {})
252
+ });
253
+ }
254
+ /** The real stage, scaled into a box: it is the same runtime the audience sees. */
255
+ function ScaledStage() {
256
+ const deck = useDeck();
257
+ const live = useLiveOptional();
258
+ const box = useRef(null);
259
+ const fit = useFit(box, deck.config.width, deck.config.height);
260
+ const { h, v } = useNav((s) => ({
261
+ h: s.h,
262
+ v: s.v
263
+ }));
264
+ const bg = deck.options.get(key(h, v))?.background ?? null;
265
+ return /* @__PURE__ */ jsx("div", {
266
+ ref: box,
267
+ className: "deck-presenter-box",
268
+ children: /* @__PURE__ */ jsxs("div", {
269
+ className: "deck-presenter-canvas",
270
+ style: {
271
+ width: deck.config.width,
272
+ height: deck.config.height,
273
+ transform: `translate(${fit.left}px, ${fit.top}px) scale(${fit.scale})`
274
+ },
275
+ children: [
276
+ /* @__PURE__ */ jsx(BackgroundLayer, {
277
+ bg,
278
+ active: true
279
+ }),
280
+ /* @__PURE__ */ jsx(PresenterInnerStage, {}),
281
+ live ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(DrawingLayer, { inCanvas: true }), /* @__PURE__ */ jsx(PointerLayer, { inCanvas: true })] }) : /* @__PURE__ */ jsx(DrawingLayer, { inCanvas: true }),
282
+ /* @__PURE__ */ jsx(ConfettiLayer, { inCanvas: true })
283
+ ]
284
+ })
285
+ });
286
+ }
287
+ /** The stage without the viewport layout: the presenter box supplies the scale. */
288
+ function PresenterInnerStage() {
289
+ const deck = useDeck();
290
+ useEffect(() => {
291
+ deck.layout.set({
292
+ scale: 1,
293
+ width: deck.config.width,
294
+ height: deck.config.height,
295
+ left: 0,
296
+ top: 0
297
+ });
298
+ }, [
299
+ deck.layout,
300
+ deck.config.width,
301
+ deck.config.height
302
+ ]);
303
+ return /* @__PURE__ */ jsx(Stage, {});
304
+ }
305
+ function NextPreview() {
306
+ const deck = useDeck();
307
+ const nav = useNav((s) => ({
308
+ h: s.h,
309
+ v: s.v,
310
+ click: s.click,
311
+ totals: s.totals
312
+ }));
313
+ const next$1 = useMemo(() => {
314
+ const state = {
315
+ ...initialNav(),
316
+ ...nav
317
+ };
318
+ const moved = next(state, deck.columns, deck.config);
319
+ if (moved === state) return null;
320
+ return {
321
+ h: moved.h,
322
+ v: moved.v,
323
+ click: moved.click,
324
+ sameSlide: moved.h === nav.h && moved.v === nav.v
325
+ };
326
+ }, [
327
+ nav,
328
+ deck.columns,
329
+ deck.config
330
+ ]);
331
+ const box = useRef(null);
332
+ const fit = useFit(box, deck.config.width, deck.config.height);
333
+ if (!next$1) return /* @__PURE__ */ jsx("div", {
334
+ ref: box,
335
+ className: "deck-presenter-box is-end",
336
+ children: /* @__PURE__ */ jsx("span", { children: "End of the deck" })
337
+ });
338
+ const meta = deck.columns[next$1.h].slides[next$1.v];
339
+ const options = deck.options.get(key(next$1.h, next$1.v));
340
+ return /* @__PURE__ */ jsxs("div", {
341
+ ref: box,
342
+ className: "deck-presenter-box",
343
+ children: [/* @__PURE__ */ jsxs("div", {
344
+ className: "deck-presenter-canvas",
345
+ style: {
346
+ width: deck.config.width,
347
+ height: deck.config.height,
348
+ transform: `translate(${fit.left}px, ${fit.top}px) scale(${fit.scale})`
349
+ },
350
+ children: [/* @__PURE__ */ jsx(BackgroundLayer, {
351
+ bg: options.background,
352
+ active: false
353
+ }), /* @__PURE__ */ jsx(SlideFrame, {
354
+ meta,
355
+ options,
356
+ h: next$1.h,
357
+ v: next$1.v,
358
+ active: false,
359
+ click: next$1.click,
360
+ preview: true,
361
+ className: next$1.sameSlide ? "is-mirror" : "is-tile"
362
+ }, `${next$1.h},${next$1.v}`)]
363
+ }), /* @__PURE__ */ jsx("span", {
364
+ className: "deck-presenter-next-label",
365
+ children: next$1.sameSlide ? `Step ${next$1.click}` : `Slide ${next$1.h + 1}${next$1.v ? `.${next$1.v + 1}` : ""}${meta.title ? ` · ${meta.title}` : ""}`
366
+ })]
367
+ });
368
+ }
369
+ /** Notes, with `[click]` markers splitting them into the segments the clicks reveal. */
370
+ function NotesPanel() {
371
+ const { meta } = useCurrentSlide();
372
+ const click = useNav((s) => s.click);
373
+ const [Notes, setNotes] = useState(null);
374
+ const root = useRef(null);
375
+ const [fontSize, setFontSize] = useState(() => {
376
+ try {
377
+ return Number(localStorage.getItem("deck.notes.size") ?? 20);
378
+ } catch {
379
+ return 20;
380
+ }
381
+ });
382
+ useEffect(() => {
383
+ let alive = true;
384
+ setNotes(null);
385
+ if (!meta?.loadNotes) return;
386
+ loadSlide(meta);
387
+ meta.loadNotes().then((m) => alive && setNotes(() => m.default));
388
+ return () => {
389
+ alive = false;
390
+ };
391
+ }, [meta]);
392
+ useEffect(() => {
393
+ const el = root.current;
394
+ if (!el) return;
395
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
396
+ const nodes = [];
397
+ while (walker.nextNode()) nodes.push(walker.currentNode);
398
+ let segment = 0;
399
+ for (const node of nodes) {
400
+ if (!node.data.includes("[click]")) {
401
+ wrap(node, segment);
402
+ continue;
403
+ }
404
+ const parts = node.data.split("[click]");
405
+ const frag = document.createDocumentFragment();
406
+ parts.forEach((p, i) => {
407
+ if (i > 0) {
408
+ segment++;
409
+ const marker = document.createElement("span");
410
+ marker.className = "deck-click-marker";
411
+ marker.dataset.segment = String(segment);
412
+ marker.textContent = String(segment);
413
+ frag.appendChild(marker);
414
+ }
415
+ if (p) {
416
+ const span = document.createElement("span");
417
+ span.className = "deck-notes-segment";
418
+ span.dataset.segment = String(segment);
419
+ span.textContent = p;
420
+ frag.appendChild(span);
421
+ }
422
+ });
423
+ node.replaceWith(frag);
424
+ }
425
+ function wrap(n, seg) {
426
+ if (!n.data.trim() || n.parentElement?.classList.contains("deck-notes-segment")) return;
427
+ const span = document.createElement("span");
428
+ span.className = "deck-notes-segment";
429
+ span.dataset.segment = String(seg);
430
+ n.replaceWith(span);
431
+ span.appendChild(n);
432
+ }
433
+ }, [Notes]);
434
+ useEffect(() => {
435
+ const el = root.current;
436
+ if (!el) return;
437
+ for (const s of el.querySelectorAll("[data-segment]")) {
438
+ const seg = Number(s.dataset.segment);
439
+ s.classList.toggle("is-current", seg === click);
440
+ s.classList.toggle("is-past", seg < click);
441
+ }
442
+ el.querySelector(`.deck-notes-segment.is-current`)?.scrollIntoView({
443
+ block: "nearest",
444
+ behavior: "smooth"
445
+ });
446
+ }, [click, Notes]);
447
+ useEffect(() => {
448
+ try {
449
+ localStorage.setItem("deck.notes.size", String(fontSize));
450
+ } catch {}
451
+ }, [fontSize]);
452
+ return /* @__PURE__ */ jsxs("div", {
453
+ className: "deck-notes-panel",
454
+ children: [/* @__PURE__ */ jsxs("div", {
455
+ className: "deck-notes-tools",
456
+ children: [/* @__PURE__ */ jsx("button", {
457
+ type: "button",
458
+ onClick: () => setFontSize((s) => Math.max(12, s - 2)),
459
+ "aria-label": "Smaller notes",
460
+ children: "A−"
461
+ }), /* @__PURE__ */ jsx("button", {
462
+ type: "button",
463
+ onClick: () => setFontSize((s) => Math.min(48, s + 2)),
464
+ "aria-label": "Larger notes",
465
+ children: "A+"
466
+ })]
467
+ }), /* @__PURE__ */ jsx("div", {
468
+ ref: root,
469
+ className: "deck-notes-body",
470
+ style: { fontSize },
471
+ children: Notes ? /* @__PURE__ */ jsx(Notes, {}) : /* @__PURE__ */ jsx("p", {
472
+ className: "deck-muted",
473
+ children: "No notes for this slide."
474
+ })
475
+ })]
476
+ });
477
+ }
478
+ function pad(n) {
479
+ return String(n).padStart(2, "0");
480
+ }
481
+ /** Elapsed time since the view opened, with pacing against `defaultTiming` or `totalTime`. */
482
+ function PresenterTimer() {
483
+ const deck = useDeck();
484
+ /**
485
+ * Kept for the life of this TAB, not this page load, so reloading — to pick up a new build, or
486
+ * to get past a slide that would not load — does not quietly set the talk back to zero. A new
487
+ * window is a new talk and starts again, which is what sessionStorage already means.
488
+ */
489
+ const startKey = `deck.presenter.start.${deck.config.slug || "deck"}`;
490
+ const [start, setStart] = useState(() => {
491
+ try {
492
+ const saved = Number(sessionStorage.getItem(startKey));
493
+ if (saved > 0) return saved;
494
+ sessionStorage.setItem(startKey, String(Date.now()));
495
+ } catch {}
496
+ return Date.now();
497
+ });
498
+ /** Reset writes through, or a reload would bring the old start back. */
499
+ const restart = (at) => {
500
+ try {
501
+ sessionStorage.setItem(startKey, String(at));
502
+ } catch {}
503
+ setStart(at);
504
+ };
505
+ const [now, setNow] = useState(Date.now());
506
+ const nav = useNav((s) => ({
507
+ h: s.h,
508
+ v: s.v
509
+ }));
510
+ useEffect(() => {
511
+ const t = setInterval(() => setNow(Date.now()), 500);
512
+ return () => clearInterval(t);
513
+ }, []);
514
+ const elapsed = Math.floor((now - start) / 1e3);
515
+ const total = deck.columns.reduce((n, c) => n + c.slides.length, 0);
516
+ const flat = progressOf({
517
+ ...initialNav(),
518
+ ...nav
519
+ }, deck.columns) * (total - 1);
520
+ let expected = null;
521
+ if (deck.config.totalTime) expected = deck.config.totalTime / total * flat;
522
+ else if (deck.config.defaultTiming) {
523
+ let sum = 0;
524
+ let i = 0;
525
+ for (const c of deck.columns) for (let v = 0; v < c.slides.length; v++) {
526
+ if (i >= flat) break;
527
+ sum += deck.options.get(key(c.h, v))?.timing ?? deck.config.defaultTiming;
528
+ i++;
529
+ }
530
+ expected = sum;
531
+ }
532
+ const pace = expected === null ? null : elapsed - expected;
533
+ return /* @__PURE__ */ jsxs("button", {
534
+ type: "button",
535
+ className: ["deck-presenter-timer", pace === null ? "" : pace > 30 ? "is-behind" : pace < -30 ? "is-ahead" : "is-ok"].join(" "),
536
+ onClick: () => restart(Date.now()),
537
+ title: "Click to reset",
538
+ children: [
539
+ pad(Math.floor(elapsed / 3600)),
540
+ ":",
541
+ pad(Math.floor(elapsed % 3600 / 60)),
542
+ ":",
543
+ pad(elapsed % 60),
544
+ pace !== null ? /* @__PURE__ */ jsx("small", { children: pace > 0 ? `+${Math.round(pace / 60)}m` : `${Math.round(pace / 60)}m` }) : null
545
+ ]
546
+ });
547
+ }
548
+ function Clock() {
549
+ const [now, setNow] = useState(/* @__PURE__ */ new Date());
550
+ useEffect(() => {
551
+ const t = setInterval(() => setNow(/* @__PURE__ */ new Date()), 1e3);
552
+ return () => clearInterval(t);
553
+ }, []);
554
+ return /* @__PURE__ */ jsxs("span", {
555
+ className: "deck-presenter-clock",
556
+ children: [
557
+ pad(now.getHours()),
558
+ ":",
559
+ pad(now.getMinutes())
560
+ ]
561
+ });
562
+ }
563
+ function PresenterTools({ mirror, setMirror }) {
564
+ const deck = useDeck();
565
+ const actions = useActions();
566
+ const live = useLiveOptional();
567
+ const ui = useUi((s) => ({
568
+ drawing: s.drawing,
569
+ pointer: s.pointer,
570
+ spotlight: s.spotlight,
571
+ scheme: s.colorScheme,
572
+ termSync: s.termSync,
573
+ projectorOpen: s.projectorOpen
574
+ }));
575
+ const paused = useNav((s) => s.paused);
576
+ const notesPublic = useStore(live?.client.state ?? noStore, (s) => s ? s.notesPublic : false);
577
+ const started = useStore(live?.client.state ?? noStore, (s) => s ? s.startedAt : null);
578
+ const [rec, setRec] = useState(null);
579
+ const [camera, setCamera] = useState(false);
580
+ const toggle = (key) => deck.ui.update((s) => ({
581
+ ...s,
582
+ [key]: !s[key]
583
+ }));
584
+ const record = async () => {
585
+ if (rec) {
586
+ rec.stop();
587
+ setRec(null);
588
+ return;
589
+ }
590
+ try {
591
+ const screen = await navigator.mediaDevices.getDisplayMedia({
592
+ video: true,
593
+ audio: true
594
+ });
595
+ let mic = null;
596
+ try {
597
+ mic = await navigator.mediaDevices.getUserMedia({ audio: true });
598
+ } catch {
599
+ mic = null;
600
+ }
601
+ const tracks = [
602
+ ...screen.getVideoTracks(),
603
+ ...screen.getAudioTracks(),
604
+ ...mic?.getAudioTracks() ?? []
605
+ ];
606
+ const stream = new MediaStream(tracks);
607
+ const r = new MediaRecorder(stream, { mimeType: MediaRecorder.isTypeSupported("video/webm;codecs=vp9") ? "video/webm;codecs=vp9" : "video/webm" });
608
+ const chunks = [];
609
+ r.ondataavailable = (e) => e.data.size && chunks.push(e.data);
610
+ r.onstop = () => {
611
+ for (const t of tracks) t.stop();
612
+ const blob = new Blob(chunks, { type: "video/webm" });
613
+ const a = document.createElement("a");
614
+ a.href = URL.createObjectURL(blob);
615
+ a.download = `${deck.config.slug || "deck"}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-")}.webm`;
616
+ a.click();
617
+ };
618
+ r.start(1e3);
619
+ setRec(r);
620
+ } catch {
621
+ actions.toast("Recording was not allowed");
622
+ }
623
+ };
624
+ return /* @__PURE__ */ jsxs("div", {
625
+ className: "deck-presenter-tools",
626
+ role: "toolbar",
627
+ children: [
628
+ /* @__PURE__ */ jsx(ThemeMenu, { compact: true }),
629
+ /* @__PURE__ */ jsxs("button", {
630
+ type: "button",
631
+ className: ui.drawing ? "is-on" : "",
632
+ onClick: () => toggle("drawing"),
633
+ title: "Draw (C)",
634
+ children: [/* @__PURE__ */ jsx(Icon, { name: "pencil" }), " Draw"]
635
+ }),
636
+ /* @__PURE__ */ jsxs("button", {
637
+ type: "button",
638
+ className: ui.pointer ? "is-on" : "",
639
+ onClick: () => toggle("pointer"),
640
+ title: "Laser pointer (X)",
641
+ children: [/* @__PURE__ */ jsx(Icon, { name: "mouse-pointer-2" }), " Pointer"]
642
+ }),
643
+ /* @__PURE__ */ jsxs("button", {
644
+ type: "button",
645
+ className: ui.spotlight ? "is-on" : "",
646
+ onClick: () => toggle("spotlight"),
647
+ title: "Spotlight follows the pointer",
648
+ children: [/* @__PURE__ */ jsx(Icon, { name: "flashlight" }), " Spotlight"]
649
+ }),
650
+ /* @__PURE__ */ jsxs("button", {
651
+ type: "button",
652
+ className: paused ? "is-on" : "",
653
+ onClick: () => actions.togglePause(),
654
+ title: "Black out the audience screen (B)",
655
+ children: [/* @__PURE__ */ jsx(Icon, { name: "monitor-off" }), " Blackout"]
656
+ }),
657
+ /* @__PURE__ */ jsx("button", {
658
+ type: "button",
659
+ onClick: () => actions.toggleColorScheme(),
660
+ title: "Dark or light (D)",
661
+ "aria-label": "Dark or light",
662
+ children: /* @__PURE__ */ jsx(Icon, { name: ui.scheme === "dark" ? "moon" : "sun" })
663
+ }),
664
+ live ? /* @__PURE__ */ jsxs(Fragment, { children: [
665
+ /* @__PURE__ */ jsxs("button", {
666
+ type: "button",
667
+ className: notesPublic ? "is-on" : "",
668
+ onClick: () => live.client.send({
669
+ type: "notes:public",
670
+ on: !notesPublic
671
+ }),
672
+ title: "Let the audience page show your notes",
673
+ children: [/* @__PURE__ */ jsx(Icon, { name: "notebook-text" }), " Share notes"]
674
+ }),
675
+ /* @__PURE__ */ jsxs("button", {
676
+ type: "button",
677
+ className: ui.termSync ? "is-on" : "",
678
+ onClick: () => actions.toggleTermSync(),
679
+ title: "Definitions you open on a term also open on every screen",
680
+ children: [/* @__PURE__ */ jsx(Icon, { name: "book-open-text" }), " Share terms"]
681
+ }),
682
+ /* @__PURE__ */ jsxs("button", {
683
+ type: "button",
684
+ onClick: () => {
685
+ live.client.send({ type: "confetti" });
686
+ window.dispatchEvent(new CustomEvent("deck:confetti"));
687
+ },
688
+ title: "Confetti on every screen",
689
+ children: [/* @__PURE__ */ jsx(Icon, { name: "party-popper" }), " Confetti"]
690
+ }),
691
+ /* @__PURE__ */ jsxs("button", {
692
+ type: "button",
693
+ className: started ? "is-on" : "",
694
+ onClick: () => live.client.send({
695
+ type: "session",
696
+ action: started ? "end" : "start"
697
+ }),
698
+ title: "Mark the session for the stats",
699
+ children: [
700
+ /* @__PURE__ */ jsx(Icon, { name: started ? "radio" : "play" }),
701
+ " ",
702
+ started ? "Live" : "Start session"
703
+ ]
704
+ })
705
+ ] }) : null,
706
+ /* @__PURE__ */ jsxs("button", {
707
+ type: "button",
708
+ className: ui.projectorOpen ? "is-on" : "",
709
+ onClick: () => actions.toggleProjector(),
710
+ title: "A window showing nothing but this deck, following you — share THAT window on a call",
711
+ children: [/* @__PURE__ */ jsx(Icon, { name: "projector" }), " Share window"]
712
+ }),
713
+ /* @__PURE__ */ jsxs("button", {
714
+ type: "button",
715
+ className: camera ? "is-on" : "",
716
+ onClick: () => setCamera((c) => !c),
717
+ title: "Camera view",
718
+ children: [/* @__PURE__ */ jsx(Icon, { name: "video" }), " Camera"]
719
+ }),
720
+ /* @__PURE__ */ jsxs("button", {
721
+ type: "button",
722
+ className: rec ? "is-on is-rec" : "",
723
+ onClick: record,
724
+ title: "Record the screen",
725
+ children: [
726
+ /* @__PURE__ */ jsx(Icon, { name: rec ? "square" : "circle" }),
727
+ " ",
728
+ rec ? "Stop" : "Record"
729
+ ]
730
+ }),
731
+ /* @__PURE__ */ jsxs("button", {
732
+ type: "button",
733
+ className: mirror ? "is-on" : "",
734
+ onClick: () => setMirror(!mirror),
735
+ title: "Mirror a screen or window here",
736
+ children: [/* @__PURE__ */ jsx(Icon, { name: "screen-share" }), " Mirror"]
737
+ }),
738
+ /* @__PURE__ */ jsxs("button", {
739
+ type: "button",
740
+ onClick: async () => {
741
+ const n = deck.nav.get();
742
+ const url = await shareLink(deck.config.live.api, roomIdOf(deck.config), deck.base, {
743
+ h: n.h,
744
+ v: n.v,
745
+ click: n.click
746
+ });
747
+ actions.toast(await copyText(url) ? `Copied ${url}` : url);
748
+ },
749
+ title: "Copy a short link to this slide",
750
+ children: [/* @__PURE__ */ jsx(Icon, { name: "link" }), " Link"]
751
+ }),
752
+ /* @__PURE__ */ jsxs("a", {
753
+ className: "deck-presenter-link",
754
+ href: `${deck.base}stats`,
755
+ target: "_blank",
756
+ rel: "noreferrer",
757
+ children: [/* @__PURE__ */ jsx(Icon, { name: "chart-column" }), " Stats"]
758
+ }),
759
+ camera ? /* @__PURE__ */ jsx(CameraView, { onClose: () => setCamera(false) }) : null
760
+ ]
761
+ });
762
+ }
763
+ const noStore = createStore(null);
764
+ /** A draggable camera window (Slidev's camera view), for recording with a face. */
765
+ function CameraView({ onClose }) {
766
+ const video = useRef(null);
767
+ const [pos, setPos] = useState({
768
+ x: 24,
769
+ y: 80
770
+ });
771
+ useEffect(() => {
772
+ let stream = null;
773
+ navigator.mediaDevices.getUserMedia({
774
+ video: {
775
+ width: 640,
776
+ height: 480
777
+ },
778
+ audio: false
779
+ }).then((s) => {
780
+ stream = s;
781
+ if (video.current) video.current.srcObject = s;
782
+ }).catch(() => onClose());
783
+ return () => stream?.getTracks().forEach((t) => t.stop());
784
+ }, [onClose]);
785
+ const drag = (e) => {
786
+ const start = {
787
+ x: e.clientX - pos.x,
788
+ y: e.clientY - pos.y
789
+ };
790
+ const move = (ev) => setPos({
791
+ x: ev.clientX - start.x,
792
+ y: ev.clientY - start.y
793
+ });
794
+ const up = () => {
795
+ window.removeEventListener("pointermove", move);
796
+ window.removeEventListener("pointerup", up);
797
+ };
798
+ window.addEventListener("pointermove", move);
799
+ window.addEventListener("pointerup", up);
800
+ };
801
+ return /* @__PURE__ */ jsxs("div", {
802
+ className: "deck-camera",
803
+ style: {
804
+ left: pos.x,
805
+ top: pos.y
806
+ },
807
+ onPointerDown: drag,
808
+ children: [/* @__PURE__ */ jsx("video", {
809
+ ref: video,
810
+ autoPlay: true,
811
+ muted: true,
812
+ playsInline: true
813
+ }), /* @__PURE__ */ jsx("button", {
814
+ type: "button",
815
+ onClick: onClose,
816
+ "aria-label": "Close camera",
817
+ children: "×"
818
+ })]
819
+ });
820
+ }
821
+ /** Show another screen or window inside the presenter view (Slidev's screen mirror). */
822
+ function ScreenMirror() {
823
+ const video = useRef(null);
824
+ const [error, setError] = useState(null);
825
+ useEffect(() => {
826
+ let stream = null;
827
+ navigator.mediaDevices.getDisplayMedia({
828
+ video: true,
829
+ audio: false
830
+ }).then((s) => {
831
+ stream = s;
832
+ if (video.current) video.current.srcObject = s;
833
+ }).catch(() => setError("Mirroring was not allowed"));
834
+ return () => stream?.getTracks().forEach((t) => t.stop());
835
+ }, []);
836
+ return /* @__PURE__ */ jsx("div", {
837
+ className: "deck-presenter-box",
838
+ children: error ? /* @__PURE__ */ jsx("span", { children: error }) : /* @__PURE__ */ jsx("video", {
839
+ ref: video,
840
+ autoPlay: true,
841
+ muted: true,
842
+ playsInline: true,
843
+ style: {
844
+ width: "100%",
845
+ height: "100%",
846
+ objectFit: "contain"
847
+ }
848
+ })
849
+ });
850
+ }
851
+ /** The room: join code, who is watching, pace, questions, the polls and quizzes on the deck. */
852
+ function RoomPanel({ open, setOpen }) {
853
+ const live = useLiveOptional();
854
+ const status = useRoomStatus();
855
+ const state = useStore(live?.client.state ?? noStore, (s) => s);
856
+ const [tab, setTab] = useState("room");
857
+ if (!live) return /* @__PURE__ */ jsx("div", {
858
+ className: "deck-room-panel",
859
+ children: /* @__PURE__ */ jsx("p", {
860
+ className: "deck-muted",
861
+ children: "This deck is not connected to a host, so there is no live room. Presenting still works on this screen."
862
+ })
863
+ });
864
+ const questions = [...state?.questions ?? []].sort((a, b) => Number(a.answered) - Number(b.answered) || b.votes - a.votes);
865
+ const counts = {
866
+ room: state?.presence.hands ?? 0,
867
+ questions: questions.filter((q) => !q.answered).length,
868
+ polls: Object.values(state?.polls ?? {}).filter((p) => p.open).length + Object.values(state?.quizzes ?? {}).filter((q) => q.open).length
869
+ };
870
+ const icons = {
871
+ room: "users",
872
+ questions: "message-square",
873
+ polls: "chart-bar"
874
+ };
875
+ if (!open) return /* @__PURE__ */ jsxs("div", {
876
+ className: "deck-room-panel is-rail",
877
+ children: [
878
+ /* @__PURE__ */ jsx("button", {
879
+ type: "button",
880
+ className: "deck-room-toggle",
881
+ onClick: () => setOpen(true),
882
+ title: "Open the room panel",
883
+ "aria-label": "Open the room panel",
884
+ children: /* @__PURE__ */ jsx(Icon, { name: "panel-right-open" })
885
+ }),
886
+ [
887
+ "room",
888
+ "questions",
889
+ "polls"
890
+ ].map((t) => /* @__PURE__ */ jsxs("button", {
891
+ type: "button",
892
+ className: ["deck-room-rail-item", tab === t ? "is-on" : ""].join(" "),
893
+ onClick: () => {
894
+ setTab(t);
895
+ setOpen(true);
896
+ },
897
+ title: `${t[0].toUpperCase()}${t.slice(1)}${counts[t] ? ` · ${counts[t]}` : ""}`,
898
+ "aria-label": t,
899
+ children: [/* @__PURE__ */ jsx(Icon, { name: icons[t] }), counts[t] ? /* @__PURE__ */ jsx("em", { children: counts[t] }) : null]
900
+ }, t)),
901
+ /* @__PURE__ */ jsx("span", {
902
+ className: ["deck-room-dot", `is-${status}`].join(" "),
903
+ title: status
904
+ })
905
+ ]
906
+ });
907
+ return /* @__PURE__ */ jsxs("div", {
908
+ className: "deck-room-panel",
909
+ children: [
910
+ /* @__PURE__ */ jsxs("div", {
911
+ className: "deck-room-tabs",
912
+ children: [
913
+ [
914
+ "room",
915
+ "questions",
916
+ "polls"
917
+ ].map((t) => /* @__PURE__ */ jsxs("button", {
918
+ type: "button",
919
+ className: tab === t ? "is-on" : "",
920
+ onClick: () => setTab(t),
921
+ children: [t, counts[t] ? /* @__PURE__ */ jsx("em", { children: counts[t] }) : null]
922
+ }, t)),
923
+ /* @__PURE__ */ jsx("span", {
924
+ className: ["deck-room-status", `is-${status}`].join(" "),
925
+ children: status
926
+ }),
927
+ /* @__PURE__ */ jsx("button", {
928
+ type: "button",
929
+ className: "deck-room-toggle",
930
+ onClick: () => setOpen(false),
931
+ title: "Collapse the room panel",
932
+ "aria-label": "Collapse the room panel",
933
+ children: /* @__PURE__ */ jsx(Icon, { name: "panel-right-close" })
934
+ })
935
+ ]
936
+ }),
937
+ tab === "room" ? /* @__PURE__ */ jsxs("div", {
938
+ className: "deck-room-body",
939
+ children: [
940
+ /* @__PURE__ */ jsx(QrJoin, {
941
+ size: 160,
942
+ caption: ""
943
+ }),
944
+ /* @__PURE__ */ jsx(Presence, {}),
945
+ /* @__PURE__ */ jsx(Pace, {}),
946
+ /* @__PURE__ */ jsx(ReactionTotals, { totals: state?.reactions ?? {} })
947
+ ]
948
+ }) : null,
949
+ tab === "questions" ? /* @__PURE__ */ jsx("ul", {
950
+ className: "deck-room-questions",
951
+ children: questions.length ? questions.map((q) => /* @__PURE__ */ jsxs("li", {
952
+ className: q.answered ? "is-answered" : "",
953
+ children: [
954
+ /* @__PURE__ */ jsxs("span", {
955
+ className: "deck-question-votes",
956
+ children: ["▲ ", q.votes]
957
+ }),
958
+ /* @__PURE__ */ jsx("span", {
959
+ className: "deck-question-text",
960
+ children: q.text
961
+ }),
962
+ /* @__PURE__ */ jsxs("span", {
963
+ className: "deck-question-actions",
964
+ children: [/* @__PURE__ */ jsx("button", {
965
+ type: "button",
966
+ onClick: () => live.client.send({
967
+ type: "qa:answered",
968
+ id: q.id,
969
+ answered: !q.answered
970
+ }),
971
+ children: q.answered ? "reopen" : "answered"
972
+ }), /* @__PURE__ */ jsx("button", {
973
+ type: "button",
974
+ onClick: () => live.client.send({
975
+ type: "qa:delete",
976
+ id: q.id
977
+ }),
978
+ children: "delete"
979
+ })]
980
+ })
981
+ ]
982
+ }, q.id)) : /* @__PURE__ */ jsx("li", {
983
+ className: "deck-muted",
984
+ children: "No questions yet."
985
+ })
986
+ }) : null,
987
+ tab === "polls" ? /* @__PURE__ */ jsxs("ul", {
988
+ className: "deck-room-polls",
989
+ children: [[...Object.values(state?.polls ?? {}).map((p) => ({
990
+ kind: "poll",
991
+ id: p.def.id,
992
+ q: p.def.question,
993
+ open: p.open,
994
+ n: p.total
995
+ })), ...Object.values(state?.quizzes ?? {}).map((q) => ({
996
+ kind: "quiz",
997
+ id: q.def.id,
998
+ q: q.def.question,
999
+ open: q.open,
1000
+ n: q.counts.reduce((a, b) => a + b, 0)
1001
+ }))].map((p) => /* @__PURE__ */ jsxs("li", { children: [
1002
+ /* @__PURE__ */ jsx("span", {
1003
+ className: "deck-poll-kind",
1004
+ children: p.kind
1005
+ }),
1006
+ /* @__PURE__ */ jsx("span", {
1007
+ className: "deck-question-text",
1008
+ children: p.q
1009
+ }),
1010
+ /* @__PURE__ */ jsx("span", {
1011
+ className: "deck-muted",
1012
+ children: p.n
1013
+ }),
1014
+ /* @__PURE__ */ jsx("button", {
1015
+ type: "button",
1016
+ onClick: () => live.client.send(p.kind === "poll" ? {
1017
+ type: "poll:open",
1018
+ id: p.id,
1019
+ open: !p.open
1020
+ } : {
1021
+ type: "quiz:open",
1022
+ id: p.id,
1023
+ open: !p.open
1024
+ }),
1025
+ children: p.open ? "close" : "open"
1026
+ })
1027
+ ] }, `${p.kind}:${p.id}`)), !Object.keys(state?.polls ?? {}).length && !Object.keys(state?.quizzes ?? {}).length ? /* @__PURE__ */ jsx("li", {
1028
+ className: "deck-muted",
1029
+ children: "Polls and quizzes register when their slide is shown."
1030
+ }) : null]
1031
+ }) : null
1032
+ ]
1033
+ });
1034
+ }
1035
+ function ReactionTotals({ totals }) {
1036
+ const entries = Object.entries(totals).sort((a, b) => b[1] - a[1]);
1037
+ if (!entries.length) return null;
1038
+ return /* @__PURE__ */ jsx("div", {
1039
+ className: "deck-reaction-totals",
1040
+ children: entries.map(([e, n]) => /* @__PURE__ */ jsxs("span", { children: [
1041
+ e,
1042
+ " ",
1043
+ n
1044
+ ] }, e))
1045
+ });
1046
+ }
1047
+ function NotesPage() {
1048
+ const deck = useDeck();
1049
+ return /* @__PURE__ */ jsxs("div", {
1050
+ className: "deck-notes-page",
1051
+ children: [/* @__PURE__ */ jsxs("h1", { children: [deck.config.title, " — notes"] }), deck.columns.flatMap((c) => c.slides.map((meta, v) => /* @__PURE__ */ jsx(NotesEntry, {
1052
+ meta,
1053
+ label: `${c.h + 1}${v ? `.${v + 1}` : ""}`,
1054
+ href: `${deck.base}#/${c.h}${v ? `/${v}` : ""}`
1055
+ }, `${c.h},${v}`)))]
1056
+ });
1057
+ }
1058
+ function NotesEntry({ meta, label, href }) {
1059
+ const [Notes, setNotes] = useState(null);
1060
+ useEffect(() => {
1061
+ let alive = true;
1062
+ meta.loadNotes?.().then((m) => alive && setNotes(() => m.default));
1063
+ return () => {
1064
+ alive = false;
1065
+ };
1066
+ }, [meta]);
1067
+ return /* @__PURE__ */ jsxs("section", {
1068
+ className: "deck-notes-entry",
1069
+ children: [/* @__PURE__ */ jsx("h2", { children: /* @__PURE__ */ jsxs("a", {
1070
+ href,
1071
+ children: [
1072
+ label,
1073
+ " ",
1074
+ meta.title ?? ""
1075
+ ]
1076
+ }) }), Notes ? /* @__PURE__ */ jsx(Notes, {}) : /* @__PURE__ */ jsx("p", {
1077
+ className: "deck-muted",
1078
+ children: "—"
1079
+ })]
1080
+ });
1081
+ }
1082
+ function StatsPage() {
1083
+ const deck = useDeck();
1084
+ const api = deck.config.live.api;
1085
+ const [stats, setStats] = useState(null);
1086
+ const [error, setError] = useState(null);
1087
+ useEffect(() => {
1088
+ if (!api) return;
1089
+ fetch(`${api}/decks/${encodeURIComponent(roomIdOf(deck.config))}/stats`, { credentials: "include" }).then(async (r) => r.ok ? setStats(await r.json()) : setError(r.status === 401 ? "Sign in on the presenter page first." : `The host answered ${r.status}.`)).catch((e) => setError(String(e)));
1090
+ }, [api, deck.config]);
1091
+ if (!api) return /* @__PURE__ */ jsx("div", {
1092
+ className: "deck-stats-page",
1093
+ children: "This deck has no host, so there are no stats."
1094
+ });
1095
+ if (error) return /* @__PURE__ */ jsx("div", {
1096
+ className: "deck-stats-page",
1097
+ children: error
1098
+ });
1099
+ if (!stats) return /* @__PURE__ */ jsx("div", {
1100
+ className: "deck-stats-page",
1101
+ children: "Loading…"
1102
+ });
1103
+ const maxDwell = Math.max(1, ...stats.slides.map((s) => s.dwellMs));
1104
+ const titleOf = (slide) => {
1105
+ const [h, v] = slide.split(",").map(Number);
1106
+ return deck.columns[h]?.slides[v]?.title ?? "";
1107
+ };
1108
+ return /* @__PURE__ */ jsxs("div", {
1109
+ className: "deck-stats-page",
1110
+ children: [
1111
+ /* @__PURE__ */ jsxs("h1", { children: [deck.config.title, " — stats"] }),
1112
+ /* @__PURE__ */ jsxs("div", {
1113
+ className: "deck-stats-cards",
1114
+ children: [
1115
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: stats.views }), /* @__PURE__ */ jsx("span", { children: "views" })] }),
1116
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: stats.viewers }), /* @__PURE__ */ jsx("span", { children: "distinct viewers" })] }),
1117
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: stats.sessions.length }), /* @__PURE__ */ jsx("span", { children: "live sessions" })] }),
1118
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: stats.questions.length }), /* @__PURE__ */ jsx("span", { children: "questions asked" })] })
1119
+ ]
1120
+ }),
1121
+ /* @__PURE__ */ jsx("h2", { children: "Time per slide" }),
1122
+ /* @__PURE__ */ jsx("table", {
1123
+ className: "deck-stats-table",
1124
+ children: /* @__PURE__ */ jsx("tbody", { children: stats.slides.map((s) => /* @__PURE__ */ jsxs("tr", { children: [
1125
+ /* @__PURE__ */ jsxs("td", {
1126
+ className: "deck-stats-slide",
1127
+ children: [
1128
+ s.slide.replace(",0", "").replace(",", "."),
1129
+ " ",
1130
+ titleOf(s.slide)
1131
+ ]
1132
+ }),
1133
+ /* @__PURE__ */ jsx("td", {
1134
+ className: "deck-stats-bar",
1135
+ children: /* @__PURE__ */ jsx("i", { style: { width: `${s.dwellMs / maxDwell * 100}%` } })
1136
+ }),
1137
+ /* @__PURE__ */ jsxs("td", {
1138
+ className: "deck-stats-num",
1139
+ children: [Math.round(s.dwellMs / 1e3), "s"]
1140
+ }),
1141
+ /* @__PURE__ */ jsxs("td", {
1142
+ className: "deck-stats-num",
1143
+ children: [s.views, " views"]
1144
+ })
1145
+ ] }, s.slide)) })
1146
+ }),
1147
+ /* @__PURE__ */ jsx("h2", { children: "Sessions" }),
1148
+ /* @__PURE__ */ jsx("table", {
1149
+ className: "deck-stats-table",
1150
+ children: /* @__PURE__ */ jsxs("tbody", { children: [stats.sessions.map((s) => /* @__PURE__ */ jsxs("tr", { children: [
1151
+ /* @__PURE__ */ jsx("td", { children: new Date(s.startedAt).toLocaleString() }),
1152
+ /* @__PURE__ */ jsx("td", {
1153
+ className: "deck-stats-num",
1154
+ children: s.endedAt ? `${Math.round((s.endedAt - s.startedAt) / 6e4)} min` : "live"
1155
+ }),
1156
+ /* @__PURE__ */ jsxs("td", {
1157
+ className: "deck-stats-num",
1158
+ children: ["peak ", s.peak]
1159
+ }),
1160
+ /* @__PURE__ */ jsxs("td", {
1161
+ className: "deck-stats-num",
1162
+ children: [s.questions, " questions"]
1163
+ }),
1164
+ /* @__PURE__ */ jsxs("td", {
1165
+ className: "deck-stats-num",
1166
+ children: [s.reactions, " reactions"]
1167
+ })
1168
+ ] }, s.id)), !stats.sessions.length ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
1169
+ className: "deck-muted",
1170
+ children: "No live session yet — press “Start session” in the presenter view."
1171
+ }) }) : null] })
1172
+ }),
1173
+ /* @__PURE__ */ jsx("h2", { children: "Reactions" }),
1174
+ /* @__PURE__ */ jsx(ReactionTotals, { totals: stats.reactions }),
1175
+ /* @__PURE__ */ jsx("h2", { children: "Questions" }),
1176
+ /* @__PURE__ */ jsx("ul", {
1177
+ className: "deck-room-questions",
1178
+ children: stats.questions.map((q, i) => /* @__PURE__ */ jsxs("li", {
1179
+ className: q.answered ? "is-answered" : "",
1180
+ children: [/* @__PURE__ */ jsxs("span", {
1181
+ className: "deck-question-votes",
1182
+ children: ["▲ ", q.votes]
1183
+ }), /* @__PURE__ */ jsx("span", {
1184
+ className: "deck-question-text",
1185
+ children: q.text
1186
+ })]
1187
+ }, i))
1188
+ }),
1189
+ /* @__PURE__ */ jsx("h2", { children: "Exports" }),
1190
+ /* @__PURE__ */ jsxs("ul", {
1191
+ className: "deck-stats-exports",
1192
+ children: [stats.exports.map((e) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", {
1193
+ href: `${api}/decks/${encodeURIComponent(roomIdOf(deck.config))}/exports/${encodeURIComponent(e.key)}`,
1194
+ children: [
1195
+ e.kind,
1196
+ " · ",
1197
+ new Date(e.at).toLocaleString(),
1198
+ " · ",
1199
+ Math.round(e.size / 1024),
1200
+ " KB"
1201
+ ]
1202
+ }) }, e.key)), !stats.exports.length ? /* @__PURE__ */ jsx("li", {
1203
+ className: "deck-muted",
1204
+ children: "Run `bun run export --upload` in the deck to archive a PDF here."
1205
+ }) : null]
1206
+ })
1207
+ ]
1208
+ });
1209
+ }
1210
+ //#endregion
1211
+ export { NotesPage, NotesPanel, PresenterPage, StatsPage };