@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,471 @@
1
+ import { createStore, useStore } from "../core/store.js";
2
+ import { useDeck, useSlideOptional } from "../react/context.js";
3
+ import { Qr } from "../components/content.js";
4
+ import { useLiveOptional, useRole, useRoomStatus } from "./context.js";
5
+ import { REACTIONS } from "./protocol.js";
6
+ import { useEffect, useMemo, useState } from "react";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/live/components.tsx
9
+ /**
10
+ * The interactive pieces a slide can carry. Each works in three states: live (the room is up),
11
+ * offline (a local, single-screen fallback so rehearsals and exports still make sense), and
12
+ * presenter (extra controls to open, close, reveal).
13
+ */
14
+ function useRoomSlice(selector, fallback) {
15
+ const live = useLiveOptional();
16
+ const value = useStore(live?.client.state ?? fallbackStore, (s) => live ? selector(s) : fallback);
17
+ return live ? value : fallback;
18
+ }
19
+ const fallbackStore = createStore(null);
20
+ /** The QR code and short URL for the audience page. */
21
+ function QrJoin({ size = 260, caption = "Scan to follow along, react and ask", className }) {
22
+ const live = useLiveOptional();
23
+ const deck = useDeck();
24
+ const url = live?.joinUrl ?? `${typeof window === "undefined" ? "" : window.location.origin}${deck.base}join`;
25
+ return /* @__PURE__ */ jsxs("div", {
26
+ className: ["deck-qr-join", className].filter(Boolean).join(" "),
27
+ children: [/* @__PURE__ */ jsx(Qr, {
28
+ value: url,
29
+ size
30
+ }), /* @__PURE__ */ jsxs("div", {
31
+ className: "deck-qr-join-text",
32
+ children: [/* @__PURE__ */ jsx("strong", { children: url.replace(/^https?:\/\//, "") }), /* @__PURE__ */ jsx("span", { children: caption })]
33
+ })]
34
+ });
35
+ }
36
+ /** How many are watching. */
37
+ function Presence({ className }) {
38
+ const presence = useRoomSlice((s) => s.presence, null);
39
+ const status = useRoomStatus();
40
+ if (!presence) return null;
41
+ return /* @__PURE__ */ jsxs("span", {
42
+ className: [
43
+ "deck-presence",
44
+ status === "open" ? "is-live" : "",
45
+ className
46
+ ].filter(Boolean).join(" "),
47
+ children: [
48
+ /* @__PURE__ */ jsx("i", {}),
49
+ presence.viewers,
50
+ " watching",
51
+ presence.hands ? ` · ${presence.hands} ✋` : ""
52
+ ]
53
+ });
54
+ }
55
+ /** The reaction bar (for slides that invite it; the audience page has its own). */
56
+ function Reactions({ className }) {
57
+ const live = useLiveOptional();
58
+ const totals = useRoomSlice((s) => s.reactions, {});
59
+ return /* @__PURE__ */ jsx("div", {
60
+ className: ["deck-reaction-bar", className].filter(Boolean).join(" "),
61
+ children: REACTIONS.map((e) => /* @__PURE__ */ jsxs("button", {
62
+ type: "button",
63
+ onClick: () => live?.client.send({
64
+ type: "react",
65
+ emoji: e
66
+ }),
67
+ children: [/* @__PURE__ */ jsx("span", { children: e }), totals[e] ? /* @__PURE__ */ jsx("small", { children: totals[e] }) : null]
68
+ }, e))
69
+ });
70
+ }
71
+ /** A poll: defined by the slide, opened by the presenter, answered from the audience page or here. */
72
+ function Poll({ id, question, options = [], kind = "choice", scale = 5, liveResults = true, className }) {
73
+ const live = useLiveOptional();
74
+ const role = useRole();
75
+ const slide = useSlideOptional();
76
+ const state = useRoomSlice((s) => s.polls[id] ?? null, null);
77
+ const [localVote, setLocalVote] = useState(null);
78
+ const [word, setWord] = useState("");
79
+ const def = useMemo(() => ({
80
+ id,
81
+ kind,
82
+ question,
83
+ options: kind === "rating" ? Array.from({ length: scale }, (_, i) => String(i + 1)) : options,
84
+ scale
85
+ }), [
86
+ id,
87
+ kind,
88
+ question,
89
+ options,
90
+ scale
91
+ ]);
92
+ useEffect(() => {
93
+ if (!live || role !== "presenter" || !slide?.active) return;
94
+ live.client.send({
95
+ type: "poll:define",
96
+ poll: def
97
+ });
98
+ }, [
99
+ live,
100
+ role,
101
+ slide?.active,
102
+ def
103
+ ]);
104
+ const counts = state?.counts ?? def.options.map(() => 0);
105
+ const total = state?.total ?? 0;
106
+ const open = state?.open ?? false;
107
+ const mine = state?.mine ?? localVote;
108
+ const showResults = liveResults || !open || role === "presenter";
109
+ const vote = (i) => {
110
+ if (kind === "multi") {
111
+ const cur = Array.isArray(mine) ? mine : [];
112
+ const next = cur.includes(i) ? cur.filter((x) => x !== i) : [...cur, i];
113
+ setLocalVote(next);
114
+ live?.client.send({
115
+ type: "poll:vote",
116
+ id,
117
+ options: next
118
+ });
119
+ return;
120
+ }
121
+ setLocalVote(i);
122
+ live?.client.send(kind === "rating" ? {
123
+ type: "poll:vote",
124
+ id,
125
+ rating: i + 1
126
+ } : {
127
+ type: "poll:vote",
128
+ id,
129
+ option: i
130
+ });
131
+ };
132
+ return /* @__PURE__ */ jsxs("div", {
133
+ className: [
134
+ "deck-poll",
135
+ `kind-${kind}`,
136
+ open ? "is-open" : "is-closed",
137
+ className
138
+ ].filter(Boolean).join(" "),
139
+ "data-poll": id,
140
+ children: [/* @__PURE__ */ jsxs("div", {
141
+ className: "deck-poll-head",
142
+ children: [
143
+ /* @__PURE__ */ jsx("strong", { children: question }),
144
+ /* @__PURE__ */ jsxs("span", {
145
+ className: "deck-poll-meta",
146
+ children: [
147
+ open ? "open" : "closed",
148
+ " · ",
149
+ total,
150
+ " ",
151
+ total === 1 ? "answer" : "answers"
152
+ ]
153
+ }),
154
+ role === "presenter" ? /* @__PURE__ */ jsxs("span", {
155
+ className: "deck-poll-controls",
156
+ children: [/* @__PURE__ */ jsx("button", {
157
+ type: "button",
158
+ onClick: () => live?.client.send({
159
+ type: "poll:open",
160
+ id,
161
+ open: !open
162
+ }),
163
+ children: open ? "Close" : "Open"
164
+ }), /* @__PURE__ */ jsx("button", {
165
+ type: "button",
166
+ onClick: () => live?.client.send({
167
+ type: "poll:reset",
168
+ id
169
+ }),
170
+ children: "Reset"
171
+ })]
172
+ }) : null
173
+ ]
174
+ }), kind === "words" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(WordCloudView, { words: state?.words ?? {} }), /* @__PURE__ */ jsxs("form", {
175
+ className: "deck-poll-word",
176
+ onSubmit: (e) => {
177
+ e.preventDefault();
178
+ if (!word.trim()) return;
179
+ live?.client.send({
180
+ type: "poll:vote",
181
+ id,
182
+ word: word.trim()
183
+ });
184
+ setLocalVote(word.trim());
185
+ setWord("");
186
+ },
187
+ children: [/* @__PURE__ */ jsx("input", {
188
+ value: word,
189
+ onChange: (e) => setWord(e.target.value),
190
+ placeholder: "One word",
191
+ maxLength: 24,
192
+ disabled: !open && !!live
193
+ }), /* @__PURE__ */ jsx("button", {
194
+ type: "submit",
195
+ disabled: !open && !!live,
196
+ children: "Add"
197
+ })]
198
+ })] }) : /* @__PURE__ */ jsx("ul", {
199
+ className: "deck-poll-options",
200
+ children: def.options.map((opt, i) => {
201
+ const n = counts[i] ?? 0;
202
+ const pct = total ? Math.round(n / total * 100) : 0;
203
+ const chosen = Array.isArray(mine) ? mine.includes(i) : mine === i;
204
+ return /* @__PURE__ */ jsx("li", {
205
+ className: chosen ? "is-mine" : "",
206
+ children: /* @__PURE__ */ jsxs("button", {
207
+ type: "button",
208
+ onClick: () => vote(i),
209
+ disabled: !open && !!live,
210
+ children: [
211
+ /* @__PURE__ */ jsx("span", {
212
+ className: "deck-poll-bar",
213
+ style: { width: showResults ? `${pct}%` : 0 }
214
+ }),
215
+ /* @__PURE__ */ jsx("span", {
216
+ className: "deck-poll-label",
217
+ children: opt
218
+ }),
219
+ showResults ? /* @__PURE__ */ jsxs("span", {
220
+ className: "deck-poll-count",
221
+ children: [
222
+ pct,
223
+ "% · ",
224
+ n
225
+ ]
226
+ }) : null
227
+ ]
228
+ })
229
+ }, i);
230
+ })
231
+ })]
232
+ });
233
+ }
234
+ function WordCloudView({ words }) {
235
+ const entries = Object.entries(words).sort((a, b) => b[1] - a[1]).slice(0, 40);
236
+ const max = entries[0]?.[1] ?? 1;
237
+ return /* @__PURE__ */ jsx("div", {
238
+ className: "deck-wordcloud",
239
+ children: entries.length ? entries.map(([w, n]) => /* @__PURE__ */ jsx("span", {
240
+ style: {
241
+ fontSize: `${.6 + n / max * 1.6}em`,
242
+ opacity: .55 + n / max * .45
243
+ },
244
+ children: w
245
+ }, w)) : /* @__PURE__ */ jsx("span", {
246
+ className: "deck-muted",
247
+ children: "Waiting for words…"
248
+ })
249
+ });
250
+ }
251
+ function WordCloud({ id, question, className }) {
252
+ return /* @__PURE__ */ jsx(Poll, {
253
+ id,
254
+ question,
255
+ kind: "words",
256
+ className
257
+ });
258
+ }
259
+ /** A timed quiz question with points for speed and a leaderboard. */
260
+ function Quiz({ id, question, options, correct, seconds = 20, className }) {
261
+ const live = useLiveOptional();
262
+ const role = useRole();
263
+ const slide = useSlideOptional();
264
+ const state = useRoomSlice((s) => s.quizzes[id] ?? null, null);
265
+ const [now, setNow] = useState(Date.now());
266
+ const [local, setLocal] = useState(null);
267
+ const def = useMemo(() => ({
268
+ id,
269
+ question,
270
+ options,
271
+ correct,
272
+ seconds
273
+ }), [
274
+ id,
275
+ question,
276
+ options,
277
+ correct,
278
+ seconds
279
+ ]);
280
+ useEffect(() => {
281
+ if (!live || role !== "presenter" || !slide?.active) return;
282
+ live.client.send({
283
+ type: "quiz:define",
284
+ quiz: def
285
+ });
286
+ }, [
287
+ live,
288
+ role,
289
+ slide?.active,
290
+ def
291
+ ]);
292
+ useEffect(() => {
293
+ const t = setInterval(() => setNow(Date.now()), 250);
294
+ return () => clearInterval(t);
295
+ }, []);
296
+ const open = state?.open ?? false;
297
+ const left = state?.deadline ? Math.max(0, Math.ceil((state.deadline - now) / 1e3)) : seconds;
298
+ const revealed = state?.revealed ?? false;
299
+ const mine = state?.mine ?? local;
300
+ const counts = state?.counts ?? options.map(() => 0);
301
+ const total = counts.reduce((a, b) => a + b, 0);
302
+ return /* @__PURE__ */ jsxs("div", {
303
+ className: [
304
+ "deck-quiz",
305
+ open ? "is-open" : "",
306
+ revealed ? "is-revealed" : "",
307
+ className
308
+ ].filter(Boolean).join(" "),
309
+ children: [/* @__PURE__ */ jsxs("div", {
310
+ className: "deck-quiz-head",
311
+ children: [
312
+ /* @__PURE__ */ jsx("strong", { children: question }),
313
+ /* @__PURE__ */ jsx("span", {
314
+ className: ["deck-quiz-timer", open && left <= 5 ? "is-urgent" : ""].join(" "),
315
+ children: open ? `${left}s` : revealed ? "revealed" : "ready"
316
+ }),
317
+ role === "presenter" ? /* @__PURE__ */ jsxs("span", {
318
+ className: "deck-poll-controls",
319
+ children: [/* @__PURE__ */ jsx("button", {
320
+ type: "button",
321
+ onClick: () => live?.client.send({
322
+ type: "quiz:open",
323
+ id,
324
+ open: !open
325
+ }),
326
+ children: open ? "Close" : "Start"
327
+ }), /* @__PURE__ */ jsx("button", {
328
+ type: "button",
329
+ onClick: () => live?.client.send({
330
+ type: "quiz:reveal",
331
+ id
332
+ }),
333
+ disabled: open,
334
+ children: "Reveal"
335
+ })]
336
+ }) : null
337
+ ]
338
+ }), /* @__PURE__ */ jsx("ul", {
339
+ className: "deck-poll-options",
340
+ children: options.map((opt, i) => {
341
+ const n = counts[i] ?? 0;
342
+ const pct = total ? Math.round(n / total * 100) : 0;
343
+ return /* @__PURE__ */ jsx("li", {
344
+ className: [
345
+ mine === i ? "is-mine" : "",
346
+ revealed && i === correct ? "is-correct" : "",
347
+ revealed && mine === i && i !== correct ? "is-wrong" : ""
348
+ ].join(" "),
349
+ children: /* @__PURE__ */ jsxs("button", {
350
+ type: "button",
351
+ disabled: !open && !!live || mine !== null,
352
+ onClick: () => {
353
+ setLocal(i);
354
+ live?.client.send({
355
+ type: "quiz:answer",
356
+ id,
357
+ option: i,
358
+ name: nickname()
359
+ });
360
+ },
361
+ children: [
362
+ /* @__PURE__ */ jsx("span", {
363
+ className: "deck-poll-bar",
364
+ style: { width: revealed ? `${pct}%` : 0 }
365
+ }),
366
+ /* @__PURE__ */ jsx("span", {
367
+ className: "deck-poll-label",
368
+ children: opt
369
+ }),
370
+ revealed ? /* @__PURE__ */ jsx("span", {
371
+ className: "deck-poll-count",
372
+ children: n
373
+ }) : null
374
+ ]
375
+ })
376
+ }, i);
377
+ })
378
+ })]
379
+ });
380
+ }
381
+ function nickname() {
382
+ try {
383
+ return localStorage.getItem("deck.name") ?? "";
384
+ } catch {
385
+ return "";
386
+ }
387
+ }
388
+ /** The leaderboard across every quiz in the room. */
389
+ function Leaderboard({ top = 8, className }) {
390
+ const quizzes = useRoomSlice((s) => s.quizzes, {});
391
+ const scores = /* @__PURE__ */ new Map();
392
+ for (const q of Object.values(quizzes)) for (const e of q.leaderboard) scores.set(e.name, (scores.get(e.name) ?? 0) + e.score);
393
+ const rows = [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, top);
394
+ return /* @__PURE__ */ jsx("ol", {
395
+ className: ["deck-leaderboard", className].filter(Boolean).join(" "),
396
+ children: rows.length ? rows.map(([name, score], i) => /* @__PURE__ */ jsxs("li", { children: [
397
+ /* @__PURE__ */ jsx("span", {
398
+ className: "deck-leaderboard-rank",
399
+ children: i + 1
400
+ }),
401
+ /* @__PURE__ */ jsx("span", {
402
+ className: "deck-leaderboard-name",
403
+ children: name || "anonymous"
404
+ }),
405
+ /* @__PURE__ */ jsx("span", {
406
+ className: "deck-leaderboard-score",
407
+ children: score
408
+ })
409
+ ] }, name)) : /* @__PURE__ */ jsx("li", {
410
+ className: "deck-muted",
411
+ children: "No scores yet."
412
+ })
413
+ });
414
+ }
415
+ /** Questions from the audience, on a slide: most voted first, answered ones dimmed. */
416
+ function Questions({ limit = 6, className, children }) {
417
+ const live = useLiveOptional();
418
+ const role = useRole();
419
+ const sorted = [...useRoomSlice((s) => s.questions, [])].sort((a, b) => Number(a.answered) - Number(b.answered) || b.votes - a.votes || a.at - b.at).slice(0, limit);
420
+ return /* @__PURE__ */ jsxs("div", {
421
+ className: ["deck-questions", className].filter(Boolean).join(" "),
422
+ children: [children, sorted.length ? /* @__PURE__ */ jsx("ul", { children: sorted.map((q) => /* @__PURE__ */ jsxs("li", {
423
+ className: q.answered ? "is-answered" : "",
424
+ children: [
425
+ /* @__PURE__ */ jsxs("span", {
426
+ className: "deck-question-votes",
427
+ children: ["▲ ", q.votes]
428
+ }),
429
+ /* @__PURE__ */ jsx("span", {
430
+ className: "deck-question-text",
431
+ children: q.text
432
+ }),
433
+ role === "presenter" ? /* @__PURE__ */ jsx("button", {
434
+ type: "button",
435
+ onClick: () => live?.client.send({
436
+ type: "qa:answered",
437
+ id: q.id,
438
+ answered: !q.answered
439
+ }),
440
+ children: q.answered ? "Reopen" : "Answered"
441
+ }) : null
442
+ ]
443
+ }, q.id)) }) : /* @__PURE__ */ jsx("p", {
444
+ className: "deck-muted",
445
+ children: "No questions yet — the audience page has an ask box."
446
+ })]
447
+ });
448
+ }
449
+ /** A gauge of the audience's pace feedback: are they asking for slower or faster? */
450
+ function Pace({ className }) {
451
+ const pace = useRoomSlice((s) => s.presence.pace, {
452
+ slower: 0,
453
+ faster: 0,
454
+ total: 0
455
+ });
456
+ const bias = pace.total ? (pace.faster - pace.slower) / pace.total : 0;
457
+ return /* @__PURE__ */ jsxs("div", {
458
+ className: ["deck-pace", className].filter(Boolean).join(" "),
459
+ title: `${pace.slower} slower · ${pace.faster} faster`,
460
+ children: [
461
+ /* @__PURE__ */ jsx("span", { children: "slower" }),
462
+ /* @__PURE__ */ jsx("div", {
463
+ className: "deck-pace-track",
464
+ children: /* @__PURE__ */ jsx("i", { style: { left: `${50 + bias * 50}%` } })
465
+ }),
466
+ /* @__PURE__ */ jsx("span", { children: "faster" })
467
+ ]
468
+ });
469
+ }
470
+ //#endregion
471
+ export { Leaderboard, Pace, Poll, Presence, QrJoin, Questions, Quiz, Reactions, WordCloud, WordCloudView, nickname };
@@ -0,0 +1,15 @@
1
+ import { Role, RoomState } from "./protocol.js";
2
+ import { RoomClient, Status } from "./client.js";
3
+ //#region src/live/context.d.ts
4
+ export interface LiveValue {
5
+ client: RoomClient;
6
+ /** The audience page URL for the QR code. */
7
+ joinUrl: string;
8
+ }
9
+ export declare const LiveContext: import("react").Context<LiveValue | null>;
10
+ export declare function useLiveOptional(): LiveValue | null;
11
+ export declare function useLive(): LiveValue;
12
+ export declare function useRoom<S = RoomState>(selector?: (s: RoomState) => S): S;
13
+ export declare function useRoomStatus(): Status;
14
+ export declare function useRole(): Role;
15
+ //#endregion
@@ -0,0 +1,28 @@
1
+ import { createStore, useStore } from "../core/store.js";
2
+ import { createContext, useContext } from "react";
3
+ //#region src/live/context.ts
4
+ const LiveContext = createContext(null);
5
+ function useLiveOptional() {
6
+ return useContext(LiveContext);
7
+ }
8
+ function useLive() {
9
+ const v = useContext(LiveContext);
10
+ if (!v) throw new Error("This needs <LiveProvider> inside <Deck>");
11
+ return v;
12
+ }
13
+ function useRoom(selector) {
14
+ const live = useLive();
15
+ return useStore(live.client.state, selector);
16
+ }
17
+ function useRoomStatus() {
18
+ const live = useLiveOptional();
19
+ return useStore(live?.client.status ?? emptyStatus);
20
+ }
21
+ function useRole() {
22
+ const live = useLiveOptional();
23
+ return useStore(live?.client.role ?? viewerRole);
24
+ }
25
+ const emptyStatus = createStore("idle");
26
+ const viewerRole = createStore("viewer");
27
+ //#endregion
28
+ export { LiveContext, useLive, useLiveOptional, useRole, useRoom, useRoomStatus };
@@ -0,0 +1,7 @@
1
+ import { ClientMessage, EMPTY_ROOM, PollDef, PollKind, PollState, Question, QuizDef, QuizState, REACTIONS, Role, RoomPosition, RoomState, RoomTerm, ServerMessage, Stroke } from "./protocol.js";
2
+ import { Leaderboard, Pace, Poll, Presence, QrJoin, Questions, Quiz, Reactions, WordCloud, WordCloudView, nickname } from "./components.js";
3
+ import { LiveProvider, PointerLayer } from "./LiveProvider.js";
4
+ import { RoomClient, Status, viewerId } from "./client.js";
5
+ import { LiveContext, useLive, useLiveOptional, useRole, useRoom, useRoomStatus } from "./context.js";
6
+ import { Analytics, copyText, shareLink } from "./analytics.js";
7
+ export { Analytics, ClientMessage, EMPTY_ROOM, Leaderboard, LiveContext, LiveProvider, Pace, PointerLayer, Poll, PollDef, PollKind, PollState, Presence, QrJoin, Question, Questions, Quiz, QuizDef, QuizState, REACTIONS, Reactions, Role, RoomClient, RoomPosition, RoomState, RoomTerm, ServerMessage, type Status, Stroke, WordCloud, WordCloudView, copyText, nickname, shareLink, useLive, useLiveOptional, useRole, useRoom, useRoomStatus, viewerId };
@@ -0,0 +1,7 @@
1
+ import { LiveContext, useLive, useLiveOptional, useRole, useRoom, useRoomStatus } from "./context.js";
2
+ import { EMPTY_ROOM, REACTIONS } from "./protocol.js";
3
+ import { Leaderboard, Pace, Poll, Presence, QrJoin, Questions, Quiz, Reactions, WordCloud, WordCloudView, nickname } from "./components.js";
4
+ import { RoomClient, viewerId } from "./client.js";
5
+ import { Analytics, copyText, shareLink } from "./analytics.js";
6
+ import { LiveProvider, PointerLayer } from "./LiveProvider.js";
7
+ export { Analytics, EMPTY_ROOM, Leaderboard, LiveContext, LiveProvider, Pace, PointerLayer, Poll, Presence, QrJoin, Questions, Quiz, REACTIONS, Reactions, RoomClient, WordCloud, WordCloudView, copyText, nickname, shareLink, useLive, useLiveOptional, useRole, useRoom, useRoomStatus, viewerId };