@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,680 @@
1
+ import { roomIdOf } from "../core/model.js";
2
+ import { key, next, prev } from "../core/navigation.js";
3
+ import { createStore, useStore } from "../core/store.js";
4
+ import { createSwipeTracker } from "../core/touch.js";
5
+ import { useDeck } from "../react/context.js";
6
+ import { useActions, useNav } from "../react/hooks.js";
7
+ import { BackgroundLayer } from "../react/Backgrounds.js";
8
+ import { ConfettiLayer } from "../react/ConfettiLayer.js";
9
+ import { ThemeMenu } from "../react/ThemeMenu.js";
10
+ import { closeTerm } from "../react/terms.js";
11
+ import { Icon } from "../components/bling.js";
12
+ import { TermReadout } from "../components/Term.js";
13
+ import { useLiveOptional, useRoomStatus } from "../live/context.js";
14
+ import { EMPTY_ROOM, REACTIONS } from "../live/protocol.js";
15
+ import { Poll, Quiz, nickname } from "../live/components.js";
16
+ import { SlideFrame, loadSlide } from "../react/SlideFrame.js";
17
+ import { DrawingLayer } from "../draw/DrawingLayer.js";
18
+ import { copyText, shareLink } from "../live/analytics.js";
19
+ import { PointerLayer, slideLabel } from "../live/LiveProvider.js";
20
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
21
+ import { createPortal } from "react-dom";
22
+ import { jsx, jsxs } from "react/jsx-runtime";
23
+ //#region src/audience/index.tsx
24
+ const noRoom = createStore(EMPTY_ROOM);
25
+ /**
26
+ * ─────────────────────────────────────────────────────────────────────────────
27
+ * THE AUDIENCE PAGE — /<slug>/join
28
+ * ─────────────────────────────────────────────────────────────────────────────
29
+ *
30
+ * What a phone shows after scanning the code: the slide the presenter is on (the real slide,
31
+ * scaled), the reactions, an ask box with upvotes, whatever poll or quiz is open, pace buttons,
32
+ * a raised hand, the presenter's notes when shared, and bookmarks kept on the phone. Nothing
33
+ * requires an account; a nickname is optional and only used for the quiz leaderboard.
34
+ * ─────────────────────────────────────────────────────────────────────────────
35
+ */
36
+ function AudiencePage() {
37
+ const deck = useDeck();
38
+ const live = useLiveOptional();
39
+ const status = useRoomStatus();
40
+ const room = useStore(live?.client.state ?? noRoom);
41
+ const [name, setName] = useState(nickname());
42
+ const [tab, setTab] = useState("follow");
43
+ const [question, setQuestion] = useState("");
44
+ const [pace, setPace] = useState(0);
45
+ const [hand, setHand] = useState(false);
46
+ const [bookmarks, setBookmarks] = useState(() => {
47
+ try {
48
+ return JSON.parse(localStorage.getItem(`deck.bookmarks.${deck.config.slug}`) ?? "[]");
49
+ } catch {
50
+ return [];
51
+ }
52
+ });
53
+ const [mine, setMine] = useState(/* @__PURE__ */ new Set());
54
+ const [voted, setVoted] = useState(/* @__PURE__ */ new Set());
55
+ const [flash, setFlash] = useState([]);
56
+ const [toast, setToast] = useState(null);
57
+ const [expanded, setExpanded] = useState(false);
58
+ const collapse = useCallback(() => setExpanded(false), []);
59
+ useEffect(() => {
60
+ try {
61
+ localStorage.setItem("deck.name", name);
62
+ } catch {}
63
+ }, [name]);
64
+ useEffect(() => {
65
+ try {
66
+ localStorage.setItem(`deck.bookmarks.${deck.config.slug}`, JSON.stringify(bookmarks));
67
+ } catch {}
68
+ }, [bookmarks, deck.config.slug]);
69
+ useEffect(() => {
70
+ if (!live) return;
71
+ return live.client.on((m) => {
72
+ if (m.type === "react") {
73
+ const id = Date.now() + Math.random();
74
+ setFlash((f) => [...f.slice(-12), {
75
+ id,
76
+ emoji: m.emoji
77
+ }]);
78
+ setTimeout(() => setFlash((f) => f.filter((x) => x.id !== id)), 1500);
79
+ }
80
+ });
81
+ }, [live]);
82
+ const actions = useActions();
83
+ const pos = useNav((s) => ({
84
+ h: s.h,
85
+ v: s.v,
86
+ click: s.click,
87
+ follow: s.follow,
88
+ presenter: s.presenter
89
+ }));
90
+ const presenting = room.presence.presenter;
91
+ const browse = useBrowse(() => setExpanded(true));
92
+ const here = deck.nav.get();
93
+ const canPrev = prev(here, deck.columns, deck.config) !== here;
94
+ const canNext = next(here, deck.columns, deck.config) !== here;
95
+ const meta = deck.columns[pos.h]?.slides[pos.v] ?? null;
96
+ const options = meta ? deck.options.get(key(pos.h, pos.v)) : null;
97
+ const slideKey = key(pos.h, pos.v);
98
+ useEffect(() => closeTerm(deck, "local"), [
99
+ deck,
100
+ pos.h,
101
+ pos.v,
102
+ pos.click
103
+ ]);
104
+ const openPolls = Object.values(room.polls).filter((p) => p.open);
105
+ const openQuizzes = Object.values(room.quizzes).filter((q) => q.open || q.revealed);
106
+ const questions = [...room.questions].sort((a, b) => Number(a.answered) - Number(b.answered) || b.votes - a.votes || a.at - b.at);
107
+ const react = (emoji) => {
108
+ live?.client.send({
109
+ type: "react",
110
+ emoji
111
+ });
112
+ if (navigator.vibrate) navigator.vibrate(10);
113
+ };
114
+ const ask = () => {
115
+ const text = question.trim();
116
+ if (!text || !live) return;
117
+ live.client.send({
118
+ type: "qa:ask",
119
+ text,
120
+ slide: slideKey
121
+ });
122
+ setQuestion("");
123
+ setTab("ask");
124
+ };
125
+ const vote = (id) => {
126
+ if (voted.has(id) || !live) return;
127
+ live.client.send({
128
+ type: "qa:vote",
129
+ id
130
+ });
131
+ setVoted((v) => new Set(v).add(id));
132
+ };
133
+ const setPaceValue = (v) => {
134
+ const next = pace === v ? 0 : v;
135
+ setPace(next);
136
+ live?.client.send({
137
+ type: "pace",
138
+ value: next
139
+ });
140
+ };
141
+ const toggleHand = () => {
142
+ setHand((h) => {
143
+ live?.client.send({
144
+ type: "hand",
145
+ up: !h
146
+ });
147
+ return !h;
148
+ });
149
+ };
150
+ const bookmark = () => setBookmarks((b) => b.includes(slideKey) ? b.filter((k) => k !== slideKey) : [...b, slideKey]);
151
+ return /* @__PURE__ */ jsxs("div", {
152
+ className: "deck-audience",
153
+ children: [
154
+ /* @__PURE__ */ jsxs("header", {
155
+ className: "deck-audience-head",
156
+ children: [
157
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: deck.config.title }), /* @__PURE__ */ jsx("span", {
158
+ className: ["deck-audience-status", `is-${status}`].join(" "),
159
+ children: status === "open" ? room.presence.presenter ? "live" : "waiting for the presenter" : status
160
+ })] }),
161
+ /* @__PURE__ */ jsxs("span", {
162
+ className: "deck-audience-count",
163
+ children: [room.presence.viewers, " here"]
164
+ }),
165
+ /* @__PURE__ */ jsx(ThemeMenu, { compact: true })
166
+ ]
167
+ }),
168
+ /* @__PURE__ */ jsxs("section", {
169
+ className: "deck-audience-slide",
170
+ children: [
171
+ meta && options ? /* @__PURE__ */ jsxs("div", {
172
+ className: "deck-audience-tap",
173
+ "data-term-cards": "off",
174
+ ...browse,
175
+ children: [
176
+ /* @__PURE__ */ jsx(AudienceSlide, {
177
+ meta,
178
+ options,
179
+ h: pos.h,
180
+ v: pos.v,
181
+ click: pos.click,
182
+ bg: options.background
183
+ }),
184
+ /* @__PURE__ */ jsx("button", {
185
+ type: "button",
186
+ className: "deck-audience-step is-prev",
187
+ onClick: () => actions.prev(),
188
+ disabled: !canPrev,
189
+ "aria-label": "Back",
190
+ children: /* @__PURE__ */ jsx(Icon, { name: "chevron-left" })
191
+ }),
192
+ /* @__PURE__ */ jsx("button", {
193
+ type: "button",
194
+ className: "deck-audience-step is-next",
195
+ onClick: () => actions.next(),
196
+ disabled: !canNext,
197
+ "aria-label": "Forward",
198
+ children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right" })
199
+ })
200
+ ]
201
+ }) : /* @__PURE__ */ jsx("div", {
202
+ className: "deck-audience-empty",
203
+ children: "The deck will appear here when it starts."
204
+ }),
205
+ presenting && !pos.follow ? /* @__PURE__ */ jsxs("button", {
206
+ type: "button",
207
+ className: "deck-audience-follow",
208
+ onClick: () => actions.setFollow(true),
209
+ children: [
210
+ /* @__PURE__ */ jsx(Icon, { name: "play" }),
211
+ /* @__PURE__ */ jsx("span", { children: "Follow the presenter" }),
212
+ pos.presenter ? /* @__PURE__ */ jsxs("small", { children: ["they are on ", slideLabel(deck, pos.presenter.h, pos.presenter.v)] }) : null
213
+ ]
214
+ }) : null,
215
+ /* @__PURE__ */ jsxs("div", {
216
+ className: "deck-audience-slide-bar",
217
+ children: [
218
+ /* @__PURE__ */ jsxs("span", { children: [
219
+ "Slide ",
220
+ pos.h + 1,
221
+ pos.v ? `.${pos.v + 1}` : "",
222
+ " of",
223
+ " ",
224
+ deck.columns.reduce((n, c) => n + c.slides.length, 0)
225
+ ] }),
226
+ /* @__PURE__ */ jsx("button", {
227
+ type: "button",
228
+ className: bookmarks.includes(slideKey) ? "is-on" : "",
229
+ onClick: bookmark,
230
+ "aria-label": "Bookmark this slide",
231
+ children: /* @__PURE__ */ jsx(Icon, { name: bookmarks.includes(slideKey) ? "bookmark-check" : "bookmark" })
232
+ }),
233
+ /* @__PURE__ */ jsx("button", {
234
+ type: "button",
235
+ className: "deck-audience-share",
236
+ onClick: async () => {
237
+ const url = await shareLink(deck.config.live.api, roomIdOf(deck.config), deck.base, {
238
+ h: pos.h,
239
+ v: pos.v
240
+ });
241
+ setToast(await copyText(url) ? "Link copied" : url);
242
+ setTimeout(() => setToast(null), 2e3);
243
+ },
244
+ "aria-label": "Copy a link to this slide",
245
+ children: /* @__PURE__ */ jsx(Icon, { name: "link" })
246
+ }),
247
+ toast ? /* @__PURE__ */ jsx("span", {
248
+ className: "deck-audience-toast",
249
+ children: toast
250
+ }) : null,
251
+ /* @__PURE__ */ jsxs("button", {
252
+ type: "button",
253
+ className: "deck-audience-expand",
254
+ onClick: () => setExpanded(true),
255
+ disabled: !meta,
256
+ "aria-label": "Expand the slide to full screen",
257
+ children: [/* @__PURE__ */ jsx(Icon, { name: "maximize-2" }), " Expand"]
258
+ }),
259
+ /* @__PURE__ */ jsxs("a", {
260
+ href: `${deck.base}?at#/${pos.h}${pos.v ? `/${pos.v}` : ""}`,
261
+ target: "_blank",
262
+ rel: "noreferrer",
263
+ children: ["Open full ", /* @__PURE__ */ jsx(Icon, { name: "external-link" })]
264
+ })
265
+ ]
266
+ }),
267
+ /* @__PURE__ */ jsx(TermReadout, {}),
268
+ /* @__PURE__ */ jsx("div", {
269
+ className: "deck-audience-flash",
270
+ "aria-hidden": true,
271
+ children: flash.map((f) => /* @__PURE__ */ jsx("span", { children: f.emoji }, f.id))
272
+ })
273
+ ]
274
+ }),
275
+ expanded && meta && options ? /* @__PURE__ */ jsx(FullMirror, {
276
+ onClose: collapse,
277
+ onReact: react,
278
+ flash,
279
+ browse,
280
+ children: /* @__PURE__ */ jsx(AudienceSlide, {
281
+ meta,
282
+ options,
283
+ h: pos.h,
284
+ v: pos.v,
285
+ click: pos.click,
286
+ bg: options.background,
287
+ fit: "contain"
288
+ })
289
+ }) : null,
290
+ /* @__PURE__ */ jsx("section", {
291
+ className: "deck-audience-react",
292
+ children: REACTIONS.map((e) => /* @__PURE__ */ jsx("button", {
293
+ type: "button",
294
+ onClick: () => react(e),
295
+ "aria-label": `React ${e}`,
296
+ children: e
297
+ }, e))
298
+ }),
299
+ /* @__PURE__ */ jsx("nav", {
300
+ className: "deck-audience-tabs",
301
+ children: [
302
+ "follow",
303
+ "ask",
304
+ "polls",
305
+ "notes",
306
+ "mine"
307
+ ].map((t) => /* @__PURE__ */ jsxs("button", {
308
+ type: "button",
309
+ className: tab === t ? "is-on" : "",
310
+ onClick: () => setTab(t),
311
+ children: [
312
+ t === "follow" ? "pace" : t,
313
+ t === "ask" && questions.length ? /* @__PURE__ */ jsx("em", { children: questions.length }) : null,
314
+ t === "polls" && openPolls.length + openQuizzes.length ? /* @__PURE__ */ jsx("em", { children: openPolls.length + openQuizzes.length }) : null
315
+ ]
316
+ }, t))
317
+ }),
318
+ /* @__PURE__ */ jsxs("section", {
319
+ className: "deck-audience-panel",
320
+ children: [
321
+ tab === "follow" ? /* @__PURE__ */ jsxs("div", {
322
+ className: "deck-audience-pace",
323
+ children: [
324
+ /* @__PURE__ */ jsx("p", { children: "How is the pace for you?" }),
325
+ /* @__PURE__ */ jsxs("div", {
326
+ className: "deck-audience-pace-buttons",
327
+ children: [
328
+ /* @__PURE__ */ jsx("button", {
329
+ type: "button",
330
+ className: pace === -1 ? "is-on" : "",
331
+ onClick: () => setPaceValue(-1),
332
+ children: "🐢 Slower"
333
+ }),
334
+ /* @__PURE__ */ jsx("button", {
335
+ type: "button",
336
+ className: pace === 0 ? "is-on" : "",
337
+ onClick: () => setPaceValue(0),
338
+ children: "👍 Just right"
339
+ }),
340
+ /* @__PURE__ */ jsx("button", {
341
+ type: "button",
342
+ className: pace === 1 ? "is-on" : "",
343
+ onClick: () => setPaceValue(1),
344
+ children: "🐇 Faster"
345
+ })
346
+ ]
347
+ }),
348
+ /* @__PURE__ */ jsx("button", {
349
+ type: "button",
350
+ className: ["deck-audience-hand", hand ? "is-on" : ""].join(" "),
351
+ onClick: toggleHand,
352
+ children: hand ? "✋ Hand raised — tap to lower" : "🙋 Raise a hand"
353
+ }),
354
+ /* @__PURE__ */ jsxs("label", {
355
+ className: "deck-audience-name",
356
+ children: ["Your name for the leaderboard", /* @__PURE__ */ jsx("input", {
357
+ value: name,
358
+ onChange: (e) => setName(e.target.value.slice(0, 24)),
359
+ placeholder: "optional"
360
+ })]
361
+ })
362
+ ]
363
+ }) : null,
364
+ tab === "ask" ? /* @__PURE__ */ jsxs("div", {
365
+ className: "deck-audience-ask",
366
+ children: [/* @__PURE__ */ jsxs("form", {
367
+ onSubmit: (e) => {
368
+ e.preventDefault();
369
+ ask();
370
+ },
371
+ children: [/* @__PURE__ */ jsx("textarea", {
372
+ value: question,
373
+ onChange: (e) => setQuestion(e.target.value.slice(0, 280)),
374
+ placeholder: "Ask the presenter…",
375
+ rows: 2
376
+ }), /* @__PURE__ */ jsx("button", {
377
+ type: "submit",
378
+ disabled: !question.trim() || status !== "open",
379
+ children: "Ask"
380
+ })]
381
+ }), /* @__PURE__ */ jsxs("ul", {
382
+ className: "deck-audience-questions",
383
+ children: [questions.map((q) => /* @__PURE__ */ jsxs("li", {
384
+ className: q.answered ? "is-answered" : "",
385
+ children: [
386
+ /* @__PURE__ */ jsxs("button", {
387
+ type: "button",
388
+ className: ["deck-vote", voted.has(q.id) ? "is-on" : ""].join(" "),
389
+ onClick: () => vote(q.id),
390
+ disabled: q.answered,
391
+ children: ["▲ ", q.votes]
392
+ }),
393
+ /* @__PURE__ */ jsx("span", { children: q.text }),
394
+ q.answered ? /* @__PURE__ */ jsx("em", { children: "answered" }) : null
395
+ ]
396
+ }, q.id)), !questions.length ? /* @__PURE__ */ jsx("li", {
397
+ className: "deck-muted",
398
+ children: "Be the first to ask."
399
+ }) : null]
400
+ })]
401
+ }) : null,
402
+ tab === "polls" ? /* @__PURE__ */ jsxs("div", {
403
+ className: "deck-audience-polls",
404
+ children: [
405
+ openPolls.map((p) => /* @__PURE__ */ jsx(Poll, {
406
+ id: p.def.id,
407
+ question: p.def.question,
408
+ options: p.def.options,
409
+ kind: p.def.kind,
410
+ scale: p.def.scale
411
+ }, p.def.id)),
412
+ openQuizzes.map((q) => /* @__PURE__ */ jsx(Quiz, {
413
+ id: q.def.id,
414
+ question: q.def.question,
415
+ options: q.def.options,
416
+ correct: q.def.correct,
417
+ seconds: q.def.seconds
418
+ }, q.def.id)),
419
+ !openPolls.length && !openQuizzes.length ? /* @__PURE__ */ jsx("p", {
420
+ className: "deck-muted",
421
+ children: "Nothing to vote on right now. Polls appear here the moment the presenter opens one."
422
+ }) : null
423
+ ]
424
+ }) : null,
425
+ tab === "notes" ? /* @__PURE__ */ jsx("div", {
426
+ className: "deck-audience-notes",
427
+ children: room.notesPublic || deck.config.audienceNotes ? /* @__PURE__ */ jsx(AudienceNotes, { meta }) : /* @__PURE__ */ jsx("p", {
428
+ className: "deck-muted",
429
+ children: "The presenter has not shared the notes."
430
+ })
431
+ }) : null,
432
+ tab === "mine" ? /* @__PURE__ */ jsxs("div", {
433
+ className: "deck-audience-mine",
434
+ children: [
435
+ /* @__PURE__ */ jsx("h3", { children: "Bookmarks" }),
436
+ /* @__PURE__ */ jsxs("ul", { children: [bookmarks.map((k) => {
437
+ const [h, v] = k.split(",").map(Number);
438
+ const m = deck.columns[h]?.slides[v];
439
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", {
440
+ href: `${deck.base}#/${h}${v ? `/${v}` : ""}`,
441
+ target: "_blank",
442
+ rel: "noreferrer",
443
+ children: [
444
+ h + 1,
445
+ v ? `.${v + 1}` : "",
446
+ " ",
447
+ m?.title ?? ""
448
+ ]
449
+ }) }, k);
450
+ }), !bookmarks.length ? /* @__PURE__ */ jsx("li", {
451
+ className: "deck-muted",
452
+ children: "Tap the bookmark on a slide to keep it."
453
+ }) : null] }),
454
+ /* @__PURE__ */ jsxs("p", {
455
+ className: "deck-muted",
456
+ children: [
457
+ "Explore the whole deck at your own pace:",
458
+ " ",
459
+ /* @__PURE__ */ jsx("a", {
460
+ href: `${deck.base}?view=scroll`,
461
+ target: "_blank",
462
+ rel: "noreferrer",
463
+ children: "open it as a page ↗"
464
+ })
465
+ ]
466
+ })
467
+ ]
468
+ }) : null
469
+ ]
470
+ })
471
+ ]
472
+ });
473
+ }
474
+ /**
475
+ * The followed slide, stage-exact: the deck's `width × height` scaled uniformly, backgrounds
476
+ * clipped to the same box, auto-fit applied as on the projector. `fit="width"` (the strip on
477
+ * the join page) keeps the deck's ratio and follows the column; `fit="contain"` (full screen)
478
+ * centres the stage in whatever box it gets, with bars on the two sides that are left over.
479
+ */
480
+ function AudienceSlide({ meta, options, h, v, click, bg, fit = "width" }) {
481
+ const deck = useDeck();
482
+ const live = useLiveOptional();
483
+ const box = useRef(null);
484
+ const [place, setPlace] = useState({
485
+ scale: .2,
486
+ left: 0,
487
+ top: 0
488
+ });
489
+ /**
490
+ * Whether this screen can carry the slide's embedded pages live.
491
+ *
492
+ * A laptop in the room can; a phone cannot, and should not be asked to. The mirror is a few
493
+ * hundred pixels of someone else's slide, and a live page in it is a second copy of whatever the
494
+ * presenter is already running — a map holding its own WebGL context, on a device that will drop
495
+ * the whole tab rather than go over its budget. The poster says the same thing for the cost of
496
+ * an image, and every slide that embeds a page tells the room how to open it for themselves.
497
+ */
498
+ const roomy = typeof window !== "undefined" && (window.matchMedia?.("(min-width: 900px)").matches ?? true);
499
+ const paused = useNav((s) => s.paused);
500
+ useEffect(() => {
501
+ const el = box.current;
502
+ if (!el) return;
503
+ const measure = () => {
504
+ const { width, height } = deck.config;
505
+ const scale = fit === "contain" ? Math.min(el.clientWidth / width, el.clientHeight / height) : el.clientWidth / width;
506
+ setPlace({
507
+ scale,
508
+ left: (el.clientWidth - width * scale) / 2,
509
+ top: fit === "contain" ? (el.clientHeight - height * scale) / 2 : 0
510
+ });
511
+ };
512
+ measure();
513
+ const ro = new ResizeObserver(measure);
514
+ ro.observe(el);
515
+ return () => ro.disconnect();
516
+ }, [deck.config, fit]);
517
+ return /* @__PURE__ */ jsxs("div", {
518
+ ref: box,
519
+ className: ["deck-audience-canvas-box", `fit-${fit}`].join(" "),
520
+ style: fit === "width" ? { aspectRatio: `${deck.config.width} / ${deck.config.height}` } : void 0,
521
+ children: [/* @__PURE__ */ jsxs("div", {
522
+ className: "deck-audience-canvas",
523
+ style: {
524
+ width: deck.config.width,
525
+ height: deck.config.height,
526
+ transform: `translate(${place.left}px, ${place.top}px) scale(${place.scale})`
527
+ },
528
+ children: [
529
+ /* @__PURE__ */ jsx(BackgroundLayer, {
530
+ bg,
531
+ active: true
532
+ }),
533
+ /* @__PURE__ */ jsx(SlideFrame, {
534
+ meta,
535
+ options,
536
+ h,
537
+ v,
538
+ active: true,
539
+ click,
540
+ preview: true,
541
+ terms: true,
542
+ frames: roomy ? "load" : void 0,
543
+ className: "is-mirror"
544
+ }, `${h},${v}`),
545
+ /* @__PURE__ */ jsx(DrawingLayer, { inCanvas: true }),
546
+ live ? /* @__PURE__ */ jsx(PointerLayer, { inCanvas: true }) : null,
547
+ /* @__PURE__ */ jsx(ConfettiLayer, { inCanvas: true })
548
+ ]
549
+ }), /* @__PURE__ */ jsx("div", {
550
+ className: "deck-pause-curtain is-audience",
551
+ "data-on": paused || void 0,
552
+ "aria-hidden": true,
553
+ children: /* @__PURE__ */ jsx("span", { children: "Paused" })
554
+ })]
555
+ });
556
+ }
557
+ /**
558
+ * The slide filling the screen — a phone turned sideways — with the reactions floating at the
559
+ * bottom. Goes truly full screen where the browser allows it and asks for landscape; a tap on
560
+ * the bars, the close button, Escape, or the browser's own exit comes back.
561
+ *
562
+ * Full screen is requested once, on mount. The page above re-renders on every reaction and
563
+ * every room update, so the callbacks are read through refs — an effect keyed on them would
564
+ * tear full screen down and race to rebuild it on each click.
565
+ */
566
+ function FullMirror({ children, onClose, onReact, flash, browse }) {
567
+ const root = useRef(null);
568
+ const close = useRef(onClose);
569
+ close.current = onClose;
570
+ useEffect(() => {
571
+ const el = root.current;
572
+ if (!el) return;
573
+ let entered = false;
574
+ el.requestFullscreen?.().then(() => {
575
+ entered = true;
576
+ screen.orientation?.lock?.("landscape").catch(() => {});
577
+ }, () => {});
578
+ const onChange = () => {
579
+ if (entered && !document.fullscreenElement) close.current();
580
+ };
581
+ const onKey = (e) => {
582
+ if (e.key === "Escape") close.current();
583
+ };
584
+ document.addEventListener("fullscreenchange", onChange);
585
+ window.addEventListener("keydown", onKey);
586
+ return () => {
587
+ document.removeEventListener("fullscreenchange", onChange);
588
+ window.removeEventListener("keydown", onKey);
589
+ if (document.fullscreenElement === el) document.exitFullscreen?.().catch(() => {});
590
+ };
591
+ }, []);
592
+ return createPortal(/* @__PURE__ */ jsxs("div", {
593
+ ref: root,
594
+ className: "deck-audience-full",
595
+ role: "dialog",
596
+ "aria-label": "The slide, full screen",
597
+ onClick: (e) => {
598
+ if (e.target === e.currentTarget) onClose();
599
+ },
600
+ children: [
601
+ /* @__PURE__ */ jsx("div", {
602
+ className: "deck-audience-full-stage",
603
+ ...browse,
604
+ children
605
+ }),
606
+ /* @__PURE__ */ jsx("button", {
607
+ type: "button",
608
+ className: "deck-audience-full-close",
609
+ onClick: onClose,
610
+ "aria-label": "Back to the join page",
611
+ children: "×"
612
+ }),
613
+ /* @__PURE__ */ jsx("div", {
614
+ className: "deck-audience-full-react",
615
+ children: REACTIONS.map((e) => /* @__PURE__ */ jsx("button", {
616
+ type: "button",
617
+ onClick: () => onReact(e),
618
+ "aria-label": `React ${e}`,
619
+ children: e
620
+ }, e))
621
+ }),
622
+ /* @__PURE__ */ jsx("div", {
623
+ className: "deck-audience-flash",
624
+ "aria-hidden": true,
625
+ children: flash.map((f) => /* @__PURE__ */ jsx("span", { children: f.emoji }, f.id))
626
+ })
627
+ ]
628
+ }), document.body);
629
+ }
630
+ /**
631
+ * Moving through the deck with a thumb: a horizontal swipe turns the page, a vertical one moves
632
+ * within a stack, and a press that went nowhere opens the slide full screen — the gesture this
633
+ * page has always had. The deck's own swipe tracker is bound to the play view alone, so the phone
634
+ * gets its own here. Anything on the slide that takes a press of its own keeps it.
635
+ */
636
+ function useBrowse(onTap) {
637
+ const actions = useActions();
638
+ const swipe = useMemo(() => createSwipeTracker(), []);
639
+ const from = useRef(null);
640
+ const spare = (t) => !!t?.closest?.("button, a, iframe, input, textarea, select");
641
+ return {
642
+ onPointerDown: (e) => {
643
+ if (spare(e.target)) return;
644
+ from.current = {
645
+ x: e.clientX,
646
+ y: e.clientY
647
+ };
648
+ swipe.start(e.clientX, e.clientY, e.timeStamp);
649
+ },
650
+ onPointerUp: (e) => {
651
+ const start = from.current;
652
+ from.current = null;
653
+ if (!start || spare(e.target)) return;
654
+ const dir = swipe.end(e.clientX, e.clientY, e.timeStamp);
655
+ if (dir === "left") actions.next();
656
+ else if (dir === "right") actions.prev();
657
+ else if (!dir && Math.hypot(e.clientX - start.x, e.clientY - start.y) < 10) onTap();
658
+ }
659
+ };
660
+ }
661
+ function AudienceNotes({ meta }) {
662
+ const [Notes, setNotes] = useState(null);
663
+ const memo = useMemo(() => meta, [meta]);
664
+ useEffect(() => {
665
+ let alive = true;
666
+ setNotes(null);
667
+ if (!memo?.loadNotes) return;
668
+ loadSlide(memo);
669
+ memo.loadNotes().then((m) => alive && setNotes(() => m.default));
670
+ return () => {
671
+ alive = false;
672
+ };
673
+ }, [memo]);
674
+ return Notes ? /* @__PURE__ */ jsx(Notes, {}) : /* @__PURE__ */ jsx("p", {
675
+ className: "deck-muted",
676
+ children: "No notes on this slide."
677
+ });
678
+ }
679
+ //#endregion
680
+ export { AudiencePage };