@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,31 @@
1
+ import { DEFAULT_GLOSSARY, GlossaryEntry, GlossaryOptions, TermMatcher, buildMatcher, inlineHtml, termId } from "./glossary/model.js";
2
+ import { Column, DEFAULT_CONFIG, DeckConfig, DeckConfigInput, DeckManifest, HostOverride, SlideBackground, SlideMeta, SlideOptions, Transition, columnsOf, readHostOverride, resolveBackground, resolveConfig, resolveSlideOptions, roomIdOf, withHostOverride } from "./core/model.js";
3
+ import { Direction, NavState, Position, navigation_d_exports } from "./core/navigation.js";
4
+ import { Store, createStore, useStore } from "./core/store.js";
5
+ import { DeckActions, DeckContext, DeckRuntime, SlideContext, SlideContextValue, TermReveal, UiState, ViewMode, useDeck, useSlide, useSlideOptional } from "./react/context.js";
6
+ import { Deck, DeckProps } from "./react/Deck.js";
7
+ import { DECK_RENDERER, DeckRenderer, IslandMount, IslandProps, RenderContext, RendererLoader, loadRenderer, registerLazyRenderer, registerRenderer, resolveRenderer, resolveRendererAsync } from "./renderers/contract.js";
8
+ import { ClientStrategy, Island, IslandLoader } from "./renderers/Island.js";
9
+ import { registerBuiltinRenderers } from "./renderers/index.js";
10
+ import { useMDXComponents } from "./react/mdx.js";
11
+ import { useActions, useClick, useClickIndex, useConfig, useCurrentSlide, useLayout, useNav, useUi } from "./react/hooks.js";
12
+ import { SlideFrame, loadSlide } from "./react/SlideFrame.js";
13
+ import { BackgroundLayer, backgroundComponent, registerBackground } from "./react/Backgrounds.js";
14
+ import { LAYOUTS, LayoutProps } from "./react/layouts.js";
15
+ import { After, Click, Clicks, Fragment, Li, Ol, Ul } from "./components/Click.js";
16
+ import { Code, MagicMove } from "./components/Code.js";
17
+ import { Absolute, Anchor, Arrow, Audio, Badge, Callout, Center, Col, Cols, Confetti, Counter, Encrypted, FitText, Frame, Grid, Iframe, Image, Kbd, Link, Mark, Mermaid, Notes, Qr, Spacer, Stack, Stretch, Tex, Timer, Toc, Transform, Tweet, Typewriter, Video, Youtube } from "./components/content.js";
18
+ import { Motion } from "./components/Motion.js";
19
+ import { Term, TermProps, TermReadout, Terms } from "./components/Term.js";
20
+ import { Sketch, SketchProps, SketchStep } from "./components/Sketch.js";
21
+ import { Background } from "./components/Background.js";
22
+ import { Leaderboard, Pace, Poll, Presence, QrJoin, Questions, Quiz, Reactions, WordCloud } from "./live/components.js";
23
+ import { Blob, Chip, Divider, Glass, Glow, GradientText, Halo, Icon, Ribbon, Shimmer, Tilt } from "./components/bling.js";
24
+ import "./components/index.js";
25
+ import { closeTerm, openTerm } from "./react/terms.js";
26
+ import { KEY_BINDINGS, KeyAction } from "./core/keyboard.js";
27
+ import { formatHash, parseFlags, parseHash } from "./core/hash.js";
28
+ import { HintLayer } from "./react/Hints.js";
29
+ import { ThemeMenu } from "./react/ThemeMenu.js";
30
+ import { FrameShell } from "./react/Frame.js";
31
+ export { Absolute, After, Anchor, Arrow, Audio, Background, BackgroundLayer, Badge, Blob, Callout, Center, Chip, Click, Clicks, type ClientStrategy, Code, Col, Cols, type Column, Confetti, Counter, DECK_RENDERER, DEFAULT_CONFIG, DEFAULT_GLOSSARY, Deck, type DeckActions, type DeckConfig, type DeckConfigInput, DeckContext, type DeckManifest, type DeckProps, type DeckRenderer, type DeckRuntime, type Direction, Divider, Encrypted, FitText, Fragment, Frame, FrameShell, Glass, type GlossaryEntry, type GlossaryOptions, Glow, GradientText, Grid, Halo, HintLayer, type HostOverride, Icon, Iframe, Image, Island, type IslandLoader, type IslandMount, type IslandProps, KEY_BINDINGS, Kbd, type KeyAction, LAYOUTS, type LayoutProps, Leaderboard, Li, Link, MagicMove, Mark, Tex as Math, Mermaid, Motion, type NavState, Notes, Ol, Pace, Poll, type Position, Presence, Qr, QrJoin, Questions, Quiz, Reactions, type RenderContext, type RendererLoader, Ribbon, Shimmer, Sketch, type SketchProps, type SketchStep, type SlideBackground, SlideContext, type SlideContextValue, SlideFrame, type SlideMeta, type SlideOptions, Spacer, Stack, type Store, Stretch, Term, type TermMatcher, type TermProps, TermReadout, type TermReveal, Terms, Tex, ThemeMenu, Tilt, Timer, Toc, Transform, type Transition, Tweet, Typewriter, type UiState, Ul, Video, type ViewMode, WordCloud, Youtube, backgroundComponent, buildMatcher, closeTerm, columnsOf, createStore, formatHash, inlineHtml, Li as li, loadRenderer, loadSlide, navigation_d_exports as navigation, Ol as ol, openTerm, parseFlags, parseHash, readHostOverride, registerBackground, registerBuiltinRenderers, registerLazyRenderer, registerRenderer, resolveBackground, resolveConfig, resolveRenderer, resolveRendererAsync, resolveSlideOptions, roomIdOf, termId, Ul as ul, useActions, useClick, useClickIndex, useConfig, useCurrentSlide, useDeck, useLayout, useMDXComponents, useNav, useSlide, useSlideOptional, useStore, useUi, withHostOverride };
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ import { DEFAULT_GLOSSARY, buildMatcher, inlineHtml, termId } from "./glossary/model.js";
2
+ import { DEFAULT_CONFIG, columnsOf, readHostOverride, resolveBackground, resolveConfig, resolveSlideOptions, roomIdOf, withHostOverride } from "./core/model.js";
3
+ import { formatHash, parseFlags, parseHash } from "./core/hash.js";
4
+ import { KEY_BINDINGS } from "./core/keyboard.js";
5
+ import { navigation_exports } from "./core/navigation.js";
6
+ import { createStore, useStore } from "./core/store.js";
7
+ import { DeckContext, SlideContext, useDeck, useSlide, useSlideOptional } from "./react/context.js";
8
+ import { useActions, useClick, useClickIndex, useConfig, useCurrentSlide, useLayout, useNav, useUi } from "./react/hooks.js";
9
+ import { BackgroundLayer, backgroundComponent, registerBackground } from "./react/Backgrounds.js";
10
+ import { HintLayer } from "./react/Hints.js";
11
+ import { ThemeMenu } from "./react/ThemeMenu.js";
12
+ import { FrameShell } from "./react/Frame.js";
13
+ import { LAYOUTS } from "./react/layouts.js";
14
+ import { After, Click, Clicks, Fragment, Li, Ol, Ul } from "./components/Click.js";
15
+ import { Code, MagicMove } from "./components/Code.js";
16
+ import { Absolute, Anchor, Arrow, Audio, Badge, Callout, Center, Col, Cols, Confetti, Counter, Encrypted, FitText, Frame, Grid, Iframe, Image, Kbd, Link, Mark, Mermaid, Notes, Qr, Spacer, Stack, Stretch, Tex, Timer, Toc, Transform, Tweet, Typewriter, Video, Youtube } from "./components/content.js";
17
+ import { Motion } from "./components/Motion.js";
18
+ import { closeTerm, openTerm } from "./react/terms.js";
19
+ import { Blob, Chip, Divider, Glass, Glow, GradientText, Halo, Icon, Ribbon, Shimmer, Tilt } from "./components/bling.js";
20
+ import { Term, TermReadout, Terms } from "./components/Term.js";
21
+ import { Sketch } from "./components/Sketch.js";
22
+ import { Background } from "./components/Background.js";
23
+ import { Leaderboard, Pace, Poll, Presence, QrJoin, Questions, Quiz, Reactions, WordCloud } from "./live/components.js";
24
+ import { useMDXComponents } from "./react/mdx.js";
25
+ import { SlideFrame, loadSlide } from "./react/SlideFrame.js";
26
+ import { Deck } from "./react/Deck.js";
27
+ import { DECK_RENDERER, loadRenderer, registerLazyRenderer, registerRenderer, resolveRenderer, resolveRendererAsync } from "./renderers/contract.js";
28
+ import { Island } from "./renderers/Island.js";
29
+ import { registerBuiltinRenderers } from "./renderers/index.js";
30
+ export { Absolute, After, Anchor, Arrow, Audio, Background, BackgroundLayer, Badge, Blob, Callout, Center, Chip, Click, Clicks, Code, Col, Cols, Confetti, Counter, DECK_RENDERER, DEFAULT_CONFIG, DEFAULT_GLOSSARY, Deck, DeckContext, Divider, Encrypted, FitText, Fragment, Frame, FrameShell, Glass, Glow, GradientText, Grid, Halo, HintLayer, Icon, Iframe, Image, Island, KEY_BINDINGS, Kbd, LAYOUTS, Leaderboard, Li, Link, MagicMove, Mark, Tex as Math, Mermaid, Motion, Notes, Ol, Pace, Poll, Presence, Qr, QrJoin, Questions, Quiz, Reactions, Ribbon, Shimmer, Sketch, SlideContext, SlideFrame, Spacer, Stack, Stretch, Term, TermReadout, Terms, Tex, ThemeMenu, Tilt, Timer, Toc, Transform, Tweet, Typewriter, Ul, Video, WordCloud, Youtube, backgroundComponent, buildMatcher, closeTerm, columnsOf, createStore, formatHash, inlineHtml, Li as li, loadRenderer, loadSlide, navigation_exports as navigation, Ol as ol, openTerm, parseFlags, parseHash, readHostOverride, registerBackground, registerBuiltinRenderers, registerLazyRenderer, registerRenderer, resolveBackground, resolveConfig, resolveRenderer, resolveRendererAsync, resolveSlideOptions, roomIdOf, termId, Ul as ul, useActions, useClick, useClickIndex, useConfig, useCurrentSlide, useDeck, useLayout, useMDXComponents, useNav, useSlide, useSlideOptional, useStore, useUi, withHostOverride };
@@ -0,0 +1,20 @@
1
+ import "../react/context.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/live/LiveProvider.d.ts
4
+ /**
5
+ * Put `<LiveProvider />` inside `<Deck>` and the deck joins its room: the presenter window pushes
6
+ * its position, pointer and drawings; the play window and the audience follow (or explore, then
7
+ * catch up); reactions float up on every screen; questions and polls flow both ways. Without a
8
+ * backend the provider stays quiet and the deck works exactly as before.
9
+ */
10
+ export declare function LiveProvider({ children }: {
11
+ children?: ReactNode;
12
+ }): import("react").JSX.Element;
13
+ /**
14
+ * The laser pointer and spotlight: published by the presenter, drawn by everyone. `inCanvas`
15
+ * renders it inside a scaled stage canvas (the presenter's current-slide box) in stage pixels.
16
+ */
17
+ export declare function PointerLayer({ inCanvas }?: {
18
+ inCanvas?: boolean;
19
+ }): import("react").JSX.Element;
20
+ //#endregion
@@ -0,0 +1,450 @@
1
+ import { roomIdOf } from "../core/model.js";
2
+ import { formatSlideNumber, goto, key } from "../core/navigation.js";
3
+ import { useStore } from "../core/store.js";
4
+ import { useDeck } from "../react/context.js";
5
+ import { useActions, useLayout, useNav, useUi } from "../react/hooks.js";
6
+ import { closeTerm, openTerm } from "../react/terms.js";
7
+ import { Icon } from "../components/bling.js";
8
+ import { LiveContext, useLiveOptional } from "./context.js";
9
+ import { DrawingLayer } from "../draw/DrawingLayer.js";
10
+ import { RoomClient, viewerId } from "./client.js";
11
+ import { Analytics } from "./analytics.js";
12
+ import { turnstileToken } from "./turnstile.js";
13
+ import { useEffect, useMemo, useRef, useState } from "react";
14
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
+ //#region src/live/LiveProvider.tsx
16
+ /**
17
+ * Put `<LiveProvider />` inside `<Deck>` and the deck joins its room: the presenter window pushes
18
+ * its position, pointer and drawings; the play window and the audience follow (or explore, then
19
+ * catch up); reactions float up on every screen; questions and polls flow both ways. Without a
20
+ * backend the provider stays quiet and the deck works exactly as before.
21
+ */
22
+ function LiveProvider({ children }) {
23
+ const deck = useDeck();
24
+ const api = deck.config.live.api;
25
+ const room = roomIdOf(deck.config);
26
+ const value = useMemo(() => {
27
+ if (!api || deck.mode === "print") return null;
28
+ const siteKey = deck.config.live.turnstile;
29
+ return {
30
+ client: new RoomClient({
31
+ api,
32
+ room,
33
+ role: deck.mode === "presenter" ? "presenter" : "viewer",
34
+ viewer: viewerId(),
35
+ token: siteKey && deck.mode !== "presenter" ? () => turnstileToken(siteKey) : void 0
36
+ }),
37
+ joinUrl: typeof window === "undefined" ? "" : `${window.location.origin}${deck.base}join`
38
+ };
39
+ }, [
40
+ api,
41
+ room,
42
+ deck.mode,
43
+ deck.base,
44
+ deck.config.live.turnstile
45
+ ]);
46
+ useEffect(() => {
47
+ if (!value) return;
48
+ value.client.connect();
49
+ return () => value.client.close();
50
+ }, [value]);
51
+ if (!value) return /* @__PURE__ */ jsx(Fragment, { children });
52
+ return /* @__PURE__ */ jsxs(LiveContext.Provider, {
53
+ value,
54
+ children: [
55
+ children,
56
+ /* @__PURE__ */ jsx(Analytics, {}),
57
+ /* @__PURE__ */ jsx(Wiring, {}),
58
+ deck.mode === "play" ? /* @__PURE__ */ jsxs(Fragment, { children: [
59
+ /* @__PURE__ */ jsx(DrawingLayer, {}),
60
+ /* @__PURE__ */ jsx(PointerLayer, {}),
61
+ /* @__PURE__ */ jsx(ReactionsOverlay, {}),
62
+ /* @__PURE__ */ jsx(FollowControl, {})
63
+ ] }) : null,
64
+ deck.mode === "presenter" ? /* @__PURE__ */ jsx(ReactionsOverlay, {}) : null
65
+ ]
66
+ });
67
+ }
68
+ /** Nav in, nav out, presence, blackout. */
69
+ function Wiring() {
70
+ const deck = useDeck();
71
+ const live = useLiveOptional();
72
+ const role = useStore(live.client.role);
73
+ useNav((s) => s.follow);
74
+ useEffect(() => {
75
+ let timer = null;
76
+ let sentAt = 0;
77
+ let pending = null;
78
+ const flush = () => {
79
+ timer = null;
80
+ if (!pending) return;
81
+ sentAt = Date.now();
82
+ live.client.send({
83
+ type: "nav",
84
+ position: pending
85
+ });
86
+ pending = null;
87
+ };
88
+ const unsub = deck.nav.subscribe((s, prev) => {
89
+ live.client.position = {
90
+ h: s.h,
91
+ v: s.v,
92
+ click: s.click
93
+ };
94
+ if (s.h === prev.h && s.v === prev.v && s.click === prev.click) return;
95
+ if (deck.sync.source === "room") return;
96
+ if (role === "presenter") {
97
+ pending = {
98
+ h: s.h,
99
+ v: s.v,
100
+ click: s.click
101
+ };
102
+ const wait = 80 - (Date.now() - sentAt);
103
+ if (wait <= 0) flush();
104
+ else if (!timer) timer = setTimeout(flush, wait);
105
+ } else live.client.send({
106
+ type: "presence",
107
+ slide: key(s.h, s.v)
108
+ });
109
+ });
110
+ return () => {
111
+ unsub();
112
+ if (timer) clearTimeout(timer);
113
+ flush();
114
+ };
115
+ }, [
116
+ deck.nav,
117
+ deck.sync,
118
+ live.client,
119
+ role
120
+ ]);
121
+ useEffect(() => {
122
+ if (role === "presenter") return;
123
+ const apply = (m) => {
124
+ if (m.type === "welcome" || m.type === "nav") {
125
+ const p = m.type === "welcome" ? m.state.position : m.position;
126
+ const presenting = m.type === "nav" || m.state.presence.presenter;
127
+ deck.sync.source = "room";
128
+ try {
129
+ deck.nav.update((s) => {
130
+ const next = {
131
+ ...s,
132
+ presenter: p
133
+ };
134
+ if (!s.follow || !presenting) return next;
135
+ return goto(next, deck.columns, p);
136
+ });
137
+ } finally {
138
+ deck.sync.source = null;
139
+ }
140
+ }
141
+ };
142
+ return live.client.on(apply);
143
+ }, [
144
+ deck.nav,
145
+ deck.columns,
146
+ deck.sync,
147
+ live.client,
148
+ role
149
+ ]);
150
+ useEffect(() => {
151
+ if (role !== "presenter") return;
152
+ return deck.nav.subscribe((s, prev) => {
153
+ if (s.paused !== prev.paused) live.client.send({
154
+ type: "blackout",
155
+ on: s.paused
156
+ });
157
+ });
158
+ }, [
159
+ deck.nav,
160
+ live.client,
161
+ role
162
+ ]);
163
+ useEffect(() => {
164
+ if (role === "presenter") return;
165
+ const apply = (blackout) => {
166
+ deck.sync.source = "room";
167
+ try {
168
+ deck.nav.update((n) => n.paused === blackout ? n : {
169
+ ...n,
170
+ paused: blackout
171
+ });
172
+ } finally {
173
+ deck.sync.source = null;
174
+ }
175
+ };
176
+ if (live.client.state.get().blackout) apply(true);
177
+ return live.client.state.subscribe((s, prev) => {
178
+ if (s.blackout !== prev.blackout) apply(s.blackout);
179
+ });
180
+ }, [
181
+ deck.nav,
182
+ deck.sync,
183
+ live.client,
184
+ role
185
+ ]);
186
+ /**
187
+ * The spotlight, the same way the blackout travels.
188
+ *
189
+ * The laser's POSITION has always been published; the darkness around it never was — it was a
190
+ * local flag on whichever window pressed the button, so the presenter dimmed their own screen
191
+ * and nobody else's. The room has carried a `spotlight` field and the message to set it since
192
+ * the protocol was written; this is the half that was missing at both ends.
193
+ */
194
+ useEffect(() => {
195
+ if (role !== "presenter") return;
196
+ return deck.ui.subscribe((s, prev) => {
197
+ if (s.spotlight !== prev.spotlight) live.client.send({
198
+ type: "spotlight",
199
+ on: s.spotlight
200
+ });
201
+ });
202
+ }, [
203
+ deck.ui,
204
+ live.client,
205
+ role
206
+ ]);
207
+ useEffect(() => {
208
+ if (role === "presenter") return;
209
+ const apply = (on) => deck.ui.update((u) => u.spotlight === on ? u : {
210
+ ...u,
211
+ spotlight: on
212
+ });
213
+ apply(live.client.state.get().spotlight);
214
+ return live.client.state.subscribe((s, prev) => {
215
+ if (s.spotlight !== prev.spotlight) apply(s.spotlight);
216
+ });
217
+ }, [
218
+ deck.ui,
219
+ live.client,
220
+ role
221
+ ]);
222
+ useEffect(() => {
223
+ if (role !== "presenter") return;
224
+ let shared = false;
225
+ const push = () => {
226
+ const r = deck.terms.get();
227
+ if (r && r.from === "local" && deck.ui.get().termSync) {
228
+ live.client.send({
229
+ type: "term",
230
+ slide: r.slide,
231
+ id: r.id,
232
+ n: r.n,
233
+ view: r.view
234
+ });
235
+ shared = true;
236
+ } else if (shared) {
237
+ live.client.send({ type: "term:off" });
238
+ shared = false;
239
+ }
240
+ };
241
+ const offTerms = deck.terms.subscribe(push);
242
+ const offUi = deck.ui.subscribe((s, prev) => {
243
+ if (s.termSync !== prev.termSync) push();
244
+ });
245
+ return () => {
246
+ offTerms();
247
+ offUi();
248
+ if (shared) live.client.send({ type: "term:off" });
249
+ };
250
+ }, [
251
+ deck.terms,
252
+ deck.ui,
253
+ live.client,
254
+ role
255
+ ]);
256
+ useEffect(() => {
257
+ if (role === "presenter") return;
258
+ return live.client.on((m) => {
259
+ const t = m.type === "term" ? m : m.type === "welcome" ? m.state.term : null;
260
+ if (t) openTerm(deck, {
261
+ slide: t.slide,
262
+ id: t.id,
263
+ n: t.n,
264
+ view: t.view ?? null,
265
+ pinned: true,
266
+ from: "remote"
267
+ });
268
+ else if (m.type === "term:off") closeTerm(deck, "remote");
269
+ });
270
+ }, [
271
+ deck,
272
+ live.client,
273
+ role
274
+ ]);
275
+ useEffect(() => {
276
+ return live.client.on((m) => {
277
+ if (m.type === "confetti") window.dispatchEvent(new CustomEvent("deck:confetti"));
278
+ });
279
+ }, [live.client]);
280
+ return null;
281
+ }
282
+ /**
283
+ * The laser pointer and spotlight: published by the presenter, drawn by everyone. `inCanvas`
284
+ * renders it inside a scaled stage canvas (the presenter's current-slide box) in stage pixels.
285
+ */
286
+ function PointerLayer({ inCanvas = false } = {}) {
287
+ const deck = useDeck();
288
+ const live = useLiveOptional();
289
+ const layout = useLayout();
290
+ const host = useRef(null);
291
+ const on = useUi((s) => s.pointer);
292
+ const spotlight = useUi((s) => s.spotlight);
293
+ const role = useStore(live.client.role);
294
+ const remote = useStore(live.client.state, (s) => s.pointer);
295
+ const follow = useNav((s) => s.follow);
296
+ const [local, setLocal] = useState(null);
297
+ useEffect(() => {
298
+ if (!on || role !== "presenter") {
299
+ if (role === "presenter") live.client.send({ type: "pointer:off" });
300
+ setLocal(null);
301
+ return;
302
+ }
303
+ let last = 0;
304
+ const onMove = (e) => {
305
+ const rect = host.current?.getBoundingClientRect();
306
+ const l = deck.layout.get();
307
+ const p = rect && rect.width > 0 ? {
308
+ x: (e.clientX - rect.left) / rect.width,
309
+ y: (e.clientY - rect.top) / rect.height
310
+ } : {
311
+ x: (e.clientX - l.left) / l.scale / deck.config.width,
312
+ y: (e.clientY - l.top) / l.scale / deck.config.height
313
+ };
314
+ setLocal(p);
315
+ const now = performance.now();
316
+ if (now - last > 33) {
317
+ last = now;
318
+ live.client.send({
319
+ type: "pointer",
320
+ x: p.x,
321
+ y: p.y
322
+ });
323
+ }
324
+ };
325
+ window.addEventListener("pointermove", onMove);
326
+ return () => window.removeEventListener("pointermove", onMove);
327
+ }, [
328
+ on,
329
+ role,
330
+ live.client,
331
+ deck.layout,
332
+ deck.config.width,
333
+ deck.config.height
334
+ ]);
335
+ const p = role === "presenter" ? local : follow ? remote : null;
336
+ const frame = inCanvas ? {
337
+ left: 0,
338
+ top: 0,
339
+ width: deck.config.width,
340
+ height: deck.config.height
341
+ } : {
342
+ left: layout.left,
343
+ top: layout.top,
344
+ width: layout.width,
345
+ height: layout.height
346
+ };
347
+ const x = p ? p.x * frame.width : 0;
348
+ const y = p ? p.y * frame.height : 0;
349
+ const radius = inCanvas ? 140 / Math.max(.05, layout.scale || 1) : 140;
350
+ return /* @__PURE__ */ jsxs("div", {
351
+ ref: host,
352
+ className: "deck-pointer-layer",
353
+ style: {
354
+ position: "absolute",
355
+ pointerEvents: "none",
356
+ ...frame
357
+ },
358
+ children: [p && spotlight ? /* @__PURE__ */ jsx("div", {
359
+ className: "deck-spotlight",
360
+ style: { background: `radial-gradient(circle ${radius}px at ${x}px ${y}px, transparent 0, rgba(0,0,0,0.75) ${radius + 20}px)` }
361
+ }) : null, p ? /* @__PURE__ */ jsx("div", {
362
+ className: "deck-pointer",
363
+ style: { transform: `translate(${x}px, ${y}px)${inCanvas ? " scale(2.5)" : ""}` }
364
+ }) : null]
365
+ });
366
+ }
367
+ /** Reactions float up from the bottom on every window as they arrive. */
368
+ function ReactionsOverlay() {
369
+ const live = useLiveOptional();
370
+ const [items, setItems] = useState([]);
371
+ useEffect(() => {
372
+ let n = 0;
373
+ return live.client.on((m) => {
374
+ if (m.type !== "react") return;
375
+ const count = Math.min(6, m.n);
376
+ const batch = Array.from({ length: count }, () => ({
377
+ id: n++,
378
+ emoji: m.emoji,
379
+ x: 10 + Math.random() * 80
380
+ }));
381
+ setItems((prev) => [...prev.slice(-40), ...batch]);
382
+ setTimeout(() => setItems((prev) => prev.filter((i) => !batch.some((b) => b.id === i.id))), 2600);
383
+ });
384
+ }, [live.client]);
385
+ return /* @__PURE__ */ jsx("div", {
386
+ className: "deck-reactions-overlay",
387
+ "aria-hidden": true,
388
+ children: items.map((i) => /* @__PURE__ */ jsx("span", {
389
+ className: "deck-reaction-float",
390
+ style: {
391
+ left: `${i.x}%`,
392
+ animationDelay: `${i.id % 5 * 60}ms`
393
+ },
394
+ children: i.emoji
395
+ }, i.id))
396
+ });
397
+ }
398
+ /**
399
+ * THE WAY BACK, for a viewer reading on their own.
400
+ *
401
+ * It shows only while this window is NOT following: in step there is nothing to say, and a viewer
402
+ * who wants out of step simply presses an arrow — any move of their own ends the follow (see
403
+ * `withTransition` in Deck.tsx). `presenter` stays null until the room has said where they are, so
404
+ * a late joiner gets the button without a slide number rather than no button at all.
405
+ */
406
+ function FollowControl() {
407
+ const deck = useDeck();
408
+ const live = useLiveOptional();
409
+ const actions = useActions();
410
+ const role = useStore(live.client.role);
411
+ const presenting = useStore(live.client.state, (s) => s.presence.presenter);
412
+ const { follow, presenter, h, v } = useNav((s) => ({
413
+ follow: s.follow,
414
+ presenter: s.presenter,
415
+ h: s.h,
416
+ v: s.v
417
+ }));
418
+ if (role === "presenter" || deck.mode !== "play" || !presenting || follow) return null;
419
+ const away = !presenter || presenter.h !== h || presenter.v !== v;
420
+ return /* @__PURE__ */ jsxs("div", {
421
+ className: "deck-follow",
422
+ role: "status",
423
+ children: [/* @__PURE__ */ jsx("span", { children: presenter && away ? /* @__PURE__ */ jsxs(Fragment, { children: [
424
+ "You are on ",
425
+ slideLabel(deck, h, v),
426
+ " · they are on",
427
+ " ",
428
+ /* @__PURE__ */ jsx("b", { children: slideLabel(deck, presenter.h, presenter.v) })
429
+ ] }) : "Reading on your own" }), /* @__PURE__ */ jsxs("button", {
430
+ type: "button",
431
+ onClick: () => actions.setFollow(true),
432
+ children: [/* @__PURE__ */ jsx(Icon, { name: "play" }), " Follow the presenter"]
433
+ })]
434
+ });
435
+ }
436
+ /**
437
+ * A slide, spelled the way THIS deck's own slide number spells it — so "they are on 10" and the
438
+ * counter in the corner are talking about the same thing. A `c/t` deck loses its total here: the
439
+ * sentence around the number does not need one.
440
+ */
441
+ function slideLabel(deck, h, v) {
442
+ const format = deck.config.slideNumber === true ? "h.v" : deck.config.slideNumber || "c";
443
+ return formatSlideNumber(format === "c/t" ? "c" : format, {
444
+ h,
445
+ v,
446
+ click: 0
447
+ }, deck.columns);
448
+ }
449
+ //#endregion
450
+ export { LiveProvider, PointerLayer, slideLabel };
@@ -0,0 +1,15 @@
1
+ //#region src/live/analytics.d.ts
2
+ /**
3
+ * Time on each slide, sent to the host every 20 seconds and when the page is hidden, with the
4
+ * anonymous viewer id (a random string kept in localStorage — no cookies, no fingerprinting).
5
+ * The host counts a view per viewer per day and sums dwell per slide for the stats page.
6
+ */
7
+ export declare function Analytics(): null;
8
+ /** Ask the host for a short link to a slide; falls back to the deck's own hash URL offline. */
9
+ export declare function shareLink(api: string | null, slug: string, base: string, pos: {
10
+ h: number;
11
+ v: number;
12
+ click?: number;
13
+ }, label?: string): Promise<string>;
14
+ export declare function copyText(text: string): Promise<boolean>;
15
+ //#endregion
@@ -0,0 +1,103 @@
1
+ import { roomIdOf } from "../core/model.js";
2
+ import { key } from "../core/navigation.js";
3
+ import { useDeck } from "../react/context.js";
4
+ import { viewerId } from "./client.js";
5
+ import { useEffect } from "react";
6
+ //#region src/live/analytics.tsx
7
+ /**
8
+ * Time on each slide, sent to the host every 20 seconds and when the page is hidden, with the
9
+ * anonymous viewer id (a random string kept in localStorage — no cookies, no fingerprinting).
10
+ * The host counts a view per viewer per day and sums dwell per slide for the stats page.
11
+ */
12
+ function Analytics() {
13
+ const deck = useDeck();
14
+ const api = deck.config.live.api;
15
+ const slug = roomIdOf(deck.config);
16
+ useEffect(() => {
17
+ if (!api || !slug || deck.mode === "print" || deck.mode === "presenter") return;
18
+ const dwell = {};
19
+ const enters = {};
20
+ let current = key(deck.nav.get().h, deck.nav.get().v);
21
+ let since = Date.now();
22
+ enters[current] = 1;
23
+ const settle = () => {
24
+ const t = Date.now();
25
+ if (!document.hidden) dwell[current] = (dwell[current] ?? 0) + (t - since);
26
+ since = t;
27
+ };
28
+ const unsub = deck.nav.subscribe((s) => {
29
+ const k = key(s.h, s.v);
30
+ if (k === current) return;
31
+ settle();
32
+ current = k;
33
+ enters[k] = (enters[k] ?? 0) + 1;
34
+ });
35
+ const flush = () => {
36
+ settle();
37
+ if (!Object.keys(dwell).length && !Object.keys(enters).length) return;
38
+ const body = JSON.stringify({
39
+ viewer: viewerId(),
40
+ dwell,
41
+ enters
42
+ });
43
+ for (const k of Object.keys(dwell)) delete dwell[k];
44
+ for (const k of Object.keys(enters)) delete enters[k];
45
+ const url = `${api}/decks/${encodeURIComponent(slug)}/beacon`;
46
+ if (!navigator.sendBeacon?.(url, new Blob([body], { type: "application/json" }))) fetch(url, {
47
+ method: "POST",
48
+ body,
49
+ headers: { "content-type": "application/json" },
50
+ keepalive: true
51
+ }).catch(() => {});
52
+ };
53
+ const timer = setInterval(flush, 2e4);
54
+ const onVisibility = () => {
55
+ if (document.hidden) flush();
56
+ else since = Date.now();
57
+ };
58
+ document.addEventListener("visibilitychange", onVisibility);
59
+ window.addEventListener("pagehide", flush);
60
+ return () => {
61
+ clearInterval(timer);
62
+ document.removeEventListener("visibilitychange", onVisibility);
63
+ window.removeEventListener("pagehide", flush);
64
+ unsub();
65
+ flush();
66
+ };
67
+ }, [
68
+ api,
69
+ slug,
70
+ deck.mode,
71
+ deck.nav
72
+ ]);
73
+ return null;
74
+ }
75
+ /** Ask the host for a short link to a slide; falls back to the deck's own hash URL offline. */
76
+ async function shareLink(api, slug, base, pos, label) {
77
+ const fallback = `${window.location.origin}${base}#/${pos.h}${pos.v || pos.click ? `/${pos.v}` : ""}${pos.click ? `/${pos.click}` : ""}`;
78
+ if (!api) return fallback;
79
+ try {
80
+ const r = await fetch(`${api}/decks/${encodeURIComponent(slug)}/share`, {
81
+ method: "POST",
82
+ headers: { "content-type": "application/json" },
83
+ body: JSON.stringify({
84
+ ...pos,
85
+ label
86
+ })
87
+ });
88
+ if (!r.ok) return fallback;
89
+ return (await r.json()).url ?? fallback;
90
+ } catch {
91
+ return fallback;
92
+ }
93
+ }
94
+ async function copyText(text) {
95
+ try {
96
+ await navigator.clipboard.writeText(text);
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+ //#endregion
103
+ export { Analytics, copyText, shareLink };