@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.
- package/LICENSE +661 -0
- package/LICENSING.md +40 -0
- package/README.md +170 -2
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/audience/index.d.ts +14 -0
- package/dist/audience/index.js +680 -0
- package/dist/backgrounds/index.d.ts +65 -0
- package/dist/backgrounds/index.js +604 -0
- package/dist/backgrounds/shared.d.ts +7 -0
- package/dist/backgrounds/shared.js +89 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +8 -0
- package/dist/cli/host.js +111 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +140 -0
- package/dist/cli/login.js +56 -0
- package/dist/cli/publish.js +159 -0
- package/dist/cli/vite.js +39 -0
- package/dist/compiler/code-meta.d.ts +33 -0
- package/dist/compiler/code-meta.js +81 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +8 -0
- package/dist/compiler/magic-move.d.ts +26 -0
- package/dist/compiler/magic-move.js +33 -0
- package/dist/compiler/plain-text.js +7 -0
- package/dist/compiler/rehype-code.d.ts +18 -0
- package/dist/compiler/rehype-code.js +66 -0
- package/dist/compiler/rehype-glossary.d.ts +35 -0
- package/dist/compiler/rehype-glossary.js +211 -0
- package/dist/compiler/slots.d.ts +3 -0
- package/dist/compiler/slots.js +36 -0
- package/dist/compiler/split.d.ts +63 -0
- package/dist/compiler/split.js +195 -0
- package/dist/components/Background.d.ts +15 -0
- package/dist/components/Background.js +24 -0
- package/dist/components/Click.d.ts +54 -0
- package/dist/components/Click.js +129 -0
- package/dist/components/Code.d.ts +27 -0
- package/dist/components/Code.js +124 -0
- package/dist/components/Motion.d.ts +25 -0
- package/dist/components/Motion.js +45 -0
- package/dist/components/Sketch.d.ts +44 -0
- package/dist/components/Sketch.js +141 -0
- package/dist/components/Term.d.ts +63 -0
- package/dist/components/Term.js +492 -0
- package/dist/components/bling.d.ts +75 -0
- package/dist/components/bling.js +146 -0
- package/dist/components/content.d.ts +232 -0
- package/dist/components/content.js +637 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +88 -0
- package/dist/core/clicks.d.ts +45 -0
- package/dist/core/clicks.js +75 -0
- package/dist/core/hash.d.ts +33 -0
- package/dist/core/hash.js +49 -0
- package/dist/core/keyboard.d.ts +17 -0
- package/dist/core/keyboard.js +198 -0
- package/dist/core/layout.d.ts +16 -0
- package/dist/core/layout.js +25 -0
- package/dist/core/model.d.ts +259 -0
- package/dist/core/model.js +377 -0
- package/dist/core/navigation.d.ts +74 -0
- package/dist/core/navigation.js +244 -0
- package/dist/core/poster.js +30 -0
- package/dist/core/store.d.ts +20 -0
- package/dist/core/store.js +64 -0
- package/dist/core/touch.js +27 -0
- package/dist/draw/DrawingLayer.js +315 -0
- package/dist/draw/strokes.js +188 -0
- package/dist/export/index.d.ts +68 -0
- package/dist/export/index.js +251 -0
- package/dist/export/run.js +81 -0
- package/dist/glossary/model.d.ts +81 -0
- package/dist/glossary/model.js +156 -0
- package/dist/glossary/parse.d.ts +27 -0
- package/dist/glossary/parse.js +90 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +30 -0
- package/dist/live/LiveProvider.d.ts +20 -0
- package/dist/live/LiveProvider.js +450 -0
- package/dist/live/analytics.d.ts +15 -0
- package/dist/live/analytics.js +103 -0
- package/dist/live/client.d.ts +51 -0
- package/dist/live/client.js +193 -0
- package/dist/live/components.d.ts +64 -0
- package/dist/live/components.js +471 -0
- package/dist/live/context.d.ts +15 -0
- package/dist/live/context.js +28 -0
- package/dist/live/index.d.ts +7 -0
- package/dist/live/index.js +7 -0
- package/dist/live/protocol.d.ts +284 -0
- package/dist/live/protocol.js +42 -0
- package/dist/live/turnstile.js +49 -0
- package/dist/presenter/index.d.ts +20 -0
- package/dist/presenter/index.js +1211 -0
- package/dist/react/AutoAnimate.js +92 -0
- package/dist/react/Backgrounds.d.ts +11 -0
- package/dist/react/Backgrounds.js +154 -0
- package/dist/react/ConfettiLayer.js +85 -0
- package/dist/react/Deck.d.ts +35 -0
- package/dist/react/Deck.js +983 -0
- package/dist/react/Frame.d.ts +16 -0
- package/dist/react/Frame.js +174 -0
- package/dist/react/Hints.d.ts +3 -0
- package/dist/react/Hints.js +201 -0
- package/dist/react/Lightbox.js +54 -0
- package/dist/react/Overview.js +93 -0
- package/dist/react/PrintView.js +128 -0
- package/dist/react/ScrollView.js +128 -0
- package/dist/react/SlideFrame.d.ts +51 -0
- package/dist/react/SlideFrame.js +323 -0
- package/dist/react/Stage.js +151 -0
- package/dist/react/ThemeMenu.d.ts +5 -0
- package/dist/react/ThemeMenu.js +124 -0
- package/dist/react/Zoom.js +23 -0
- package/dist/react/chrome.js +339 -0
- package/dist/react/context.d.ts +165 -0
- package/dist/react/context.js +19 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.js +70 -0
- package/dist/react/layouts.d.ts +21 -0
- package/dist/react/layouts.js +104 -0
- package/dist/react/mdx.d.ts +4 -0
- package/dist/react/mdx.js +33 -0
- package/dist/react/terms.d.ts +6 -0
- package/dist/react/terms.js +66 -0
- package/dist/renderers/Island.d.ts +57 -0
- package/dist/renderers/Island.js +223 -0
- package/dist/renderers/contract.d.ts +173 -0
- package/dist/renderers/contract.js +152 -0
- package/dist/renderers/index.d.ts +12 -0
- package/dist/renderers/index.js +16 -0
- package/dist/renderers/react.d.ts +16 -0
- package/dist/renderers/react.js +32 -0
- package/dist/renderers/web-components.d.ts +4 -0
- package/dist/renderers/web-components.js +103 -0
- package/dist/sketch/Canvas.js +49 -0
- package/dist/sketch/dsl.d.ts +131 -0
- package/dist/sketch/dsl.js +281 -0
- package/dist/sketch/index.d.ts +4 -0
- package/dist/sketch/index.js +3 -0
- package/dist/sketch/palette.d.ts +21 -0
- package/dist/sketch/palette.js +66 -0
- package/dist/sketch/render.d.ts +43 -0
- package/dist/sketch/render.js +198 -0
- package/dist/themes/base.css +1704 -0
- package/dist/themes/bling.css +592 -0
- package/dist/themes/glossary.css +317 -0
- package/dist/themes/live.css +1557 -0
- package/dist/themes/minimal.css +83 -0
- package/dist/themes/rla.css +92 -0
- package/dist/themes/sketch.css +121 -0
- package/dist/themes/stack.css +62 -0
- package/dist/vite/index.d.ts +15 -0
- package/dist/vite/index.js +320 -0
- package/package.json +159 -4
- 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 };
|