@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,124 @@
1
+ import { useDeck } from "./context.js";
2
+ import { useActions, useUi } from "./hooks.js";
3
+ import { useEffect, useRef } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/react/ThemeMenu.tsx
6
+ /**
7
+ * The theme selector: every screen picks its own look — the themes a deck ships (its main.tsx
8
+ * imports their CSS and `config.themes` lists them) plus light or dark — and keeps the choice
9
+ * in the browser. `T` opens it; the palette button in the corner does the same.
10
+ */
11
+ function themeLabel(name) {
12
+ return name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
13
+ }
14
+ function ThemeMenu({ compact = false }) {
15
+ const deck = useDeck();
16
+ const open = useUi((s) => s.themeMenu);
17
+ const theme = useUi((s) => s.theme);
18
+ const scheme = useUi((s) => s.colorScheme);
19
+ const actions = useActions();
20
+ const root = useRef(null);
21
+ const themes = deck.config.themes.length ? deck.config.themes : [deck.config.theme];
22
+ useEffect(() => {
23
+ if (!open) return;
24
+ const onDown = (e) => {
25
+ if (!root.current?.contains(e.target)) actions.toggleThemeMenu(false);
26
+ };
27
+ window.addEventListener("pointerdown", onDown);
28
+ return () => {
29
+ window.removeEventListener("pointerdown", onDown);
30
+ };
31
+ }, [open, actions]);
32
+ return /* @__PURE__ */ jsxs("div", {
33
+ ref: root,
34
+ className: [
35
+ "deck-theme-menu",
36
+ compact ? "is-compact" : "",
37
+ open ? "is-open" : ""
38
+ ].filter(Boolean).join(" "),
39
+ children: [/* @__PURE__ */ jsxs("button", {
40
+ type: "button",
41
+ className: "deck-theme-button",
42
+ onClick: () => actions.toggleThemeMenu(),
43
+ "aria-haspopup": "menu",
44
+ "aria-expanded": open,
45
+ title: "Theme (T)",
46
+ children: [/* @__PURE__ */ jsxs("svg", {
47
+ viewBox: "0 0 24 24",
48
+ width: "18",
49
+ height: "18",
50
+ fill: "none",
51
+ stroke: "currentColor",
52
+ strokeWidth: "1.8",
53
+ strokeLinecap: "round",
54
+ strokeLinejoin: "round",
55
+ "aria-hidden": true,
56
+ children: [
57
+ /* @__PURE__ */ jsx("path", { d: "M12 3a9 9 0 0 0 0 18c1.1 0 2-.9 2-2v-1.3c0-.5.4-.9.9-.9H16a5 5 0 0 0 5-5c0-5-4-8.8-9-8.8Z" }),
58
+ /* @__PURE__ */ jsx("circle", {
59
+ cx: "7.5",
60
+ cy: "11.5",
61
+ r: "1.2",
62
+ fill: "currentColor",
63
+ stroke: "none"
64
+ }),
65
+ /* @__PURE__ */ jsx("circle", {
66
+ cx: "10.5",
67
+ cy: "7.5",
68
+ r: "1.2",
69
+ fill: "currentColor",
70
+ stroke: "none"
71
+ }),
72
+ /* @__PURE__ */ jsx("circle", {
73
+ cx: "15",
74
+ cy: "7.5",
75
+ r: "1.2",
76
+ fill: "currentColor",
77
+ stroke: "none"
78
+ })
79
+ ]
80
+ }), compact ? null : /* @__PURE__ */ jsx("span", { children: themeLabel(theme) })]
81
+ }), open ? /* @__PURE__ */ jsxs("div", {
82
+ className: "deck-theme-popover",
83
+ role: "menu",
84
+ children: [
85
+ /* @__PURE__ */ jsx("div", {
86
+ className: "deck-theme-section",
87
+ children: "Theme"
88
+ }),
89
+ themes.map((name) => /* @__PURE__ */ jsxs("button", {
90
+ type: "button",
91
+ role: "menuitemradio",
92
+ "aria-checked": name === theme,
93
+ className: name === theme ? "is-on" : "",
94
+ onClick: () => actions.setTheme(name),
95
+ children: [/* @__PURE__ */ jsx("i", { className: `deck-theme-swatch theme-${name}` }), themeLabel(name)]
96
+ }, name)),
97
+ /* @__PURE__ */ jsx("div", {
98
+ className: "deck-theme-section",
99
+ children: "Scheme"
100
+ }),
101
+ /* @__PURE__ */ jsxs("div", {
102
+ className: "deck-theme-row",
103
+ children: [/* @__PURE__ */ jsx("button", {
104
+ type: "button",
105
+ className: scheme === "light" ? "is-on" : "",
106
+ onClick: () => actions.setColorScheme("light"),
107
+ children: "☀ Light"
108
+ }), /* @__PURE__ */ jsx("button", {
109
+ type: "button",
110
+ className: scheme === "dark" ? "is-on" : "",
111
+ onClick: () => actions.setColorScheme("dark"),
112
+ children: "☾ Dark"
113
+ })]
114
+ }),
115
+ /* @__PURE__ */ jsx("div", {
116
+ className: "deck-theme-hint",
117
+ children: "Yours only — every screen keeps its own choice."
118
+ })
119
+ ]
120
+ }) : null]
121
+ });
122
+ }
123
+ //#endregion
124
+ export { ThemeMenu, themeLabel };
@@ -0,0 +1,23 @@
1
+ import { useDeck } from "./context.js";
2
+ import { useLayout, useUi } from "./hooks.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/react/Zoom.tsx
5
+ /** Alt+click zoom: the stage scales up around the clicked point; click again or Esc to return. */
6
+ function ZoomLayer({ children }) {
7
+ const zoom = useUi((s) => s.zoom);
8
+ const layout = useLayout();
9
+ useDeck();
10
+ const style = zoom ? {
11
+ transformOrigin: `${layout.left + zoom.x * layout.scale}px ${layout.top + zoom.y * layout.scale}px`,
12
+ transform: `scale(${zoom.scale})`,
13
+ transition: "transform 0.6s ease"
14
+ } : { transition: "transform 0.6s ease" };
15
+ return /* @__PURE__ */ jsx("div", {
16
+ className: "deck-zoom",
17
+ style,
18
+ "data-zoomed": zoom ? "true" : void 0,
19
+ children
20
+ });
21
+ }
22
+ //#endregion
23
+ export { ZoomLayer };
@@ -0,0 +1,339 @@
1
+ import { KEY_BINDINGS } from "../core/keyboard.js";
2
+ import { availableRoutes, formatSlideNumber, progressOf } from "../core/navigation.js";
3
+ import { useDeck } from "./context.js";
4
+ import { useActions, useCurrentSlide, useNav, useUi } from "./hooks.js";
5
+ import { HintLayer } from "./Hints.js";
6
+ import { ConfettiLayer } from "./ConfettiLayer.js";
7
+ import { ThemeMenu } from "./ThemeMenu.js";
8
+ import { loadSlide } from "./SlideFrame.js";
9
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/react/chrome.tsx
12
+ /**
13
+ * The chrome around the slides: arrows, progress, the slide number, the pause curtain, the
14
+ * help sheet, jump-to, search, the speaker-notes footer, and a toast. Each piece reads only the
15
+ * slice of state it needs.
16
+ */
17
+ function Chrome() {
18
+ const deck = useDeck();
19
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
20
+ deck.config.controls === true ? /* @__PURE__ */ jsx(Controls, {}) : null,
21
+ deck.config.progress ? /* @__PURE__ */ jsx(Progress, {}) : null,
22
+ deck.config.slideNumber && (deck.config.showSlideNumber === "all" || deck.config.showSlideNumber === "print" && deck.mode === "print") ? /* @__PURE__ */ jsx(SlideNumber, {}) : null,
23
+ deck.config.showNotes ? /* @__PURE__ */ jsx(NotesFooter, {}) : null,
24
+ /* @__PURE__ */ jsx(ThemeMenu, {}),
25
+ /* @__PURE__ */ jsx(PauseCurtain, {}),
26
+ /* @__PURE__ */ jsx(HelpSheet, {}),
27
+ /* @__PURE__ */ jsx(JumpTo, {}),
28
+ /* @__PURE__ */ jsx(SearchSheet, {}),
29
+ /* @__PURE__ */ jsx(HintLayer, {}),
30
+ /* @__PURE__ */ jsx(Toast, {}),
31
+ /* @__PURE__ */ jsx(ConfettiLayer, {}),
32
+ deck.config.badge && deck.mode === "play" ? /* @__PURE__ */ jsx(MadeWith, {}) : null
33
+ ] });
34
+ }
35
+ /**
36
+ * "Made with Kadal Deck", in the corner of a deck the host serves on the free plan. Only in the
37
+ * play window: never in print or export (mode "print"), the presenter's own window, or the
38
+ * audience page.
39
+ */
40
+ function MadeWith() {
41
+ return /* @__PURE__ */ jsxs("a", {
42
+ className: "deck-made-with",
43
+ href: "https://deck.kadal.cc/?ref=badge",
44
+ target: "_blank",
45
+ rel: "noopener",
46
+ "data-hint": true,
47
+ children: ["Made with ", /* @__PURE__ */ jsx("strong", { children: "Kadal Deck" })]
48
+ });
49
+ }
50
+ function Controls() {
51
+ const deck = useDeck();
52
+ const nav = useNav((s) => ({
53
+ h: s.h,
54
+ v: s.v,
55
+ click: s.click,
56
+ totals: s.totals,
57
+ overview: s.overview
58
+ }));
59
+ const actions = useActions();
60
+ const routes = availableRoutes({
61
+ ...nav,
62
+ direction: "none",
63
+ paused: false,
64
+ autoSliding: false,
65
+ presenter: null,
66
+ follow: true,
67
+ seq: 0
68
+ }, deck.columns, deck.config);
69
+ const [tutorial, setTutorial] = useState(deck.config.controlsTutorial);
70
+ useEffect(() => {
71
+ if (!tutorial) return;
72
+ return deck.nav.subscribe((s, p) => {
73
+ if (s.h !== p.h || s.v !== p.v || s.click !== p.click) setTutorial(false);
74
+ });
75
+ }, [deck.nav, tutorial]);
76
+ const back = deck.config.controlsBackArrows;
77
+ const arrow = (dir, enabled, onClick, isBack) => /* @__PURE__ */ jsx("button", {
78
+ type: "button",
79
+ className: [
80
+ "deck-control",
81
+ `deck-control-${dir}`,
82
+ enabled ? "is-enabled" : "",
83
+ isBack && back === "faded" ? "is-faded" : "",
84
+ tutorial && enabled && !isBack ? "is-highlight" : ""
85
+ ].filter(Boolean).join(" "),
86
+ "aria-label": `Navigate ${dir}`,
87
+ disabled: !enabled,
88
+ hidden: isBack && back === "hidden" && enabled,
89
+ onClick,
90
+ children: /* @__PURE__ */ jsx("span", {})
91
+ });
92
+ return /* @__PURE__ */ jsxs("div", {
93
+ className: `deck-controls layout-${deck.config.controlsLayout}`,
94
+ "aria-hidden": nav.overview || void 0,
95
+ children: [
96
+ arrow("left", routes.left || routes.prevClick, actions.prev, true),
97
+ arrow("right", routes.right || routes.nextClick, actions.next, false),
98
+ arrow("up", routes.up, actions.up, true),
99
+ arrow("down", routes.down, actions.down, false)
100
+ ]
101
+ });
102
+ }
103
+ function Progress() {
104
+ const deck = useDeck();
105
+ const nav = useNav((s) => ({
106
+ h: s.h,
107
+ v: s.v
108
+ }));
109
+ const actions = useActions();
110
+ const p = progressOf({
111
+ ...nav,
112
+ click: 0,
113
+ direction: "none",
114
+ overview: false,
115
+ paused: false,
116
+ autoSliding: false,
117
+ totals: {},
118
+ presenter: null,
119
+ follow: true,
120
+ seq: 0
121
+ }, deck.columns);
122
+ const onClick = (e) => {
123
+ const rect = e.currentTarget.getBoundingClientRect();
124
+ const frac = (e.clientX - rect.left) / rect.width;
125
+ const total = deck.columns.reduce((n, c) => n + c.slides.length, 0);
126
+ actions.gotoFlat(Math.round(frac * (total - 1)));
127
+ };
128
+ return /* @__PURE__ */ jsx("div", {
129
+ className: "deck-progress",
130
+ onClick,
131
+ role: "progressbar",
132
+ "aria-valuenow": Math.round(p * 100),
133
+ "aria-valuemin": 0,
134
+ "aria-valuemax": 100,
135
+ children: /* @__PURE__ */ jsx("span", { style: { transform: `scaleX(${p})` } })
136
+ });
137
+ }
138
+ function SlideNumber() {
139
+ const deck = useDeck();
140
+ const nav = useNav((s) => ({
141
+ h: s.h,
142
+ v: s.v,
143
+ click: s.click
144
+ }));
145
+ const format = deck.config.slideNumber === true ? "h.v" : deck.config.slideNumber;
146
+ if (!format) return null;
147
+ const text = formatSlideNumber(format, nav, deck.columns, true);
148
+ return /* @__PURE__ */ jsx("a", {
149
+ className: "deck-slide-number",
150
+ href: typeof window === "undefined" ? void 0 : window.location.hash,
151
+ children: text
152
+ });
153
+ }
154
+ function PauseCurtain() {
155
+ const paused = useNav((s) => s.paused);
156
+ const actions = useActions();
157
+ return /* @__PURE__ */ jsx("div", {
158
+ className: "deck-pause-curtain",
159
+ "data-on": paused || void 0,
160
+ onClick: () => actions.togglePause(false),
161
+ role: "presentation",
162
+ children: /* @__PURE__ */ jsx("span", { children: "Paused — press any key" })
163
+ });
164
+ }
165
+ function HelpSheet() {
166
+ const open = useUi((s) => s.help);
167
+ const actions = useActions();
168
+ if (!open) return null;
169
+ return /* @__PURE__ */ jsx("div", {
170
+ className: "deck-sheet deck-help",
171
+ role: "dialog",
172
+ "aria-label": "Keyboard shortcuts",
173
+ onClick: () => actions.toggleHelp(false),
174
+ children: /* @__PURE__ */ jsxs("div", {
175
+ className: "deck-sheet-panel",
176
+ onClick: (e) => e.stopPropagation(),
177
+ children: [
178
+ /* @__PURE__ */ jsx("h2", { children: "Keyboard" }),
179
+ /* @__PURE__ */ jsx("table", { children: /* @__PURE__ */ jsxs("tbody", { children: [KEY_BINDINGS.map((b) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("kbd", { children: b.label }) }), /* @__PURE__ */ jsx("td", { children: b.description })] }, b.action)), /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("kbd", { children: "Alt click" }) }), /* @__PURE__ */ jsx("td", { children: "Zoom in on a point" })] })] }) }),
180
+ /* @__PURE__ */ jsx("p", {
181
+ className: "deck-sheet-hint",
182
+ children: "Press ? or Esc to close."
183
+ })
184
+ ]
185
+ })
186
+ });
187
+ }
188
+ function JumpTo() {
189
+ const open = useUi((s) => s.jump);
190
+ const actions = useActions();
191
+ const deck = useDeck();
192
+ const [value, setValue] = useState("");
193
+ const input = useRef(null);
194
+ useLayoutEffect(() => {
195
+ if (open) {
196
+ setValue("");
197
+ input.current?.focus();
198
+ }
199
+ }, [open]);
200
+ if (!open) return null;
201
+ const total = deck.columns.reduce((n, c) => n + c.slides.length, 0);
202
+ const submit = () => {
203
+ const raw = value.trim();
204
+ if (/^\d+$/.test(raw)) actions.gotoFlat(Number(raw) - 1);
205
+ else if (/^\d+\.\d+$/.test(raw)) {
206
+ const [h, v] = raw.split(".").map((n) => Number(n) - 1);
207
+ actions.goto(h, v);
208
+ } else if (raw) actions.gotoId(raw);
209
+ actions.toggleJump(false);
210
+ };
211
+ return /* @__PURE__ */ jsx("div", {
212
+ className: "deck-jump",
213
+ role: "dialog",
214
+ "aria-label": "Jump to slide",
215
+ children: /* @__PURE__ */ jsx("input", {
216
+ ref: input,
217
+ autoFocus: true,
218
+ value,
219
+ placeholder: `Slide 1–${total}, or an id`,
220
+ onChange: (e) => setValue(e.target.value),
221
+ onKeyDown: (e) => {
222
+ if (e.key === "Enter") submit();
223
+ if (e.key === "Escape") actions.toggleJump(false);
224
+ e.stopPropagation();
225
+ },
226
+ onBlur: () => actions.toggleJump(false)
227
+ })
228
+ });
229
+ }
230
+ function SearchSheet() {
231
+ const open = useUi((s) => s.search);
232
+ const actions = useActions();
233
+ const deck = useDeck();
234
+ const [query, setQuery] = useState("");
235
+ const [texts, setTexts] = useState(/* @__PURE__ */ new Map());
236
+ const input = useRef(null);
237
+ useEffect(() => {
238
+ if (!open) return;
239
+ setTimeout(() => input.current?.focus(), 0);
240
+ const map = /* @__PURE__ */ new Map();
241
+ deck.columns.forEach((c) => c.slides.forEach((s, v) => {
242
+ const t = s.text;
243
+ if (t) map.set(`${c.h},${v}`, t);
244
+ }));
245
+ for (const el of document.querySelectorAll(".deck-slide[data-h]")) {
246
+ const k = `${el.dataset.h},${el.dataset.v}`;
247
+ if (!map.has(k)) map.set(k, el.innerText);
248
+ }
249
+ setTexts(map);
250
+ }, [open, deck.columns]);
251
+ const hits = useMemo(() => {
252
+ const q = query.trim().toLowerCase();
253
+ if (!q) return [];
254
+ const out = [];
255
+ deck.columns.forEach((c) => c.slides.forEach((s, v) => {
256
+ const text = texts.get(`${c.h},${v}`) ?? "";
257
+ const i = text.toLowerCase().indexOf(q);
258
+ if (i === -1 && !(s.title ?? "").toLowerCase().includes(q)) return;
259
+ const start = Math.max(0, i - 40);
260
+ out.push({
261
+ h: c.h,
262
+ v,
263
+ title: s.title,
264
+ snippet: i === -1 ? s.title ?? "" : `…${text.slice(start, i + q.length + 40)}…`
265
+ });
266
+ }));
267
+ return out.slice(0, 20);
268
+ }, [
269
+ query,
270
+ texts,
271
+ deck.columns
272
+ ]);
273
+ if (!open) return null;
274
+ return /* @__PURE__ */ jsx("div", {
275
+ className: "deck-sheet deck-search",
276
+ role: "dialog",
277
+ "aria-label": "Search",
278
+ onClick: () => actions.toggleSearch(false),
279
+ children: /* @__PURE__ */ jsxs("div", {
280
+ className: "deck-sheet-panel",
281
+ onClick: (e) => e.stopPropagation(),
282
+ children: [/* @__PURE__ */ jsx("input", {
283
+ ref: input,
284
+ value: query,
285
+ placeholder: "Search the deck",
286
+ onChange: (e) => setQuery(e.target.value),
287
+ onKeyDown: (e) => {
288
+ if (e.key === "Escape") actions.toggleSearch(false);
289
+ if (e.key === "Enter" && hits[0]) {
290
+ actions.goto(hits[0].h, hits[0].v);
291
+ actions.toggleSearch(false);
292
+ }
293
+ e.stopPropagation();
294
+ }
295
+ }), /* @__PURE__ */ jsxs("ul", { children: [hits.map((hit) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
296
+ type: "button",
297
+ onClick: () => {
298
+ actions.goto(hit.h, hit.v);
299
+ actions.toggleSearch(false);
300
+ },
301
+ children: [/* @__PURE__ */ jsx("strong", { children: hit.title ?? `Slide ${hit.h + 1}${hit.v ? `.${hit.v + 1}` : ""}` }), /* @__PURE__ */ jsx("span", { children: hit.snippet })]
302
+ }) }, `${hit.h},${hit.v}`)), query && !hits.length ? /* @__PURE__ */ jsx("li", {
303
+ className: "deck-search-empty",
304
+ children: "Nothing matches."
305
+ }) : null] })]
306
+ })
307
+ });
308
+ }
309
+ /** The current slide's notes, rendered under the stage when `showNotes` is on. */
310
+ function NotesFooter() {
311
+ const { meta } = useCurrentSlide();
312
+ const [Notes, setNotes] = useState(null);
313
+ useEffect(() => {
314
+ let alive = true;
315
+ setNotes(null);
316
+ if (!meta?.loadNotes) return;
317
+ loadSlide(meta);
318
+ meta.loadNotes().then((m) => alive && setNotes(() => m.default));
319
+ return () => {
320
+ alive = false;
321
+ };
322
+ }, [meta]);
323
+ if (!Notes) return null;
324
+ return /* @__PURE__ */ jsx("aside", {
325
+ className: "deck-notes-footer",
326
+ children: /* @__PURE__ */ jsx(Notes, {})
327
+ });
328
+ }
329
+ function Toast() {
330
+ const toast = useUi((s) => s.toast);
331
+ return /* @__PURE__ */ jsx("div", {
332
+ className: "deck-toast",
333
+ "data-on": toast ? "true" : void 0,
334
+ role: "status",
335
+ children: toast
336
+ });
337
+ }
338
+ //#endregion
339
+ export { Chrome, Controls, HelpSheet, JumpTo, MadeWith, NotesFooter, PauseCurtain, Progress, SearchSheet, SlideNumber, Toast };
@@ -0,0 +1,165 @@
1
+ import { GlossaryEntry } from "../glossary/model.js";
2
+ import { Column, DeckConfig, DeckManifest, MagicMoveData, SlideMeta, SlideOptions } from "../core/model.js";
3
+ import { ClickRegistry } from "../core/clicks.js";
4
+ import { Layout } from "../core/layout.js";
5
+ import { NavState } from "../core/navigation.js";
6
+ import { Store } from "../core/store.js";
7
+ import { ComponentType } from "react";
8
+ //#region src/react/context.d.ts
9
+ /** Which page of the deck app is showing. */
10
+ export type ViewMode = "play" | "presenter" | "audience" | "print" | "stats" | "notes";
11
+ /** What the lightbox can show; "sketch" is an Excalidraw canvas keyed by its sketch. */
12
+ export type LightboxKind = "image" | "video" | "iframe" | "sketch";
13
+ export interface UiState {
14
+ help: boolean;
15
+ jump: boolean;
16
+ search: boolean;
17
+ lightbox: {
18
+ kind: LightboxKind;
19
+ src: string;
20
+ fit: string;
21
+ } | null;
22
+ /** The zoomed-in point in stage coordinates, or null. */
23
+ zoom: {
24
+ x: number;
25
+ y: number;
26
+ scale: number;
27
+ } | null;
28
+ drawing: boolean;
29
+ pointer: boolean;
30
+ spotlight: boolean;
31
+ colorScheme: "light" | "dark";
32
+ cursorHidden: boolean;
33
+ fullscreen: boolean;
34
+ /** The scroll view is on (by config, the `?view=scroll` flag, or a narrow viewport). */
35
+ scroll: boolean;
36
+ /** Shown for a moment when the audience tries a bound key the presenter has disabled, etc. */
37
+ toast: string | null;
38
+ /** Vim's `f`: labels on every link and control, type one to use it. */
39
+ hints: boolean;
40
+ themeMenu: boolean;
41
+ /** This screen's theme — its own choice, kept in the browser; starts at the deck's. */
42
+ theme: string;
43
+ /** Whether this window has a projector window of its own open (the presenter's share button). */
44
+ projectorOpen: boolean;
45
+ /** The presenter's switch: term cards opened here also open on every screen in the room. */
46
+ termSync: boolean;
47
+ }
48
+ /** The term card on show in this window: which mention of which term, and whose doing. */
49
+ export interface TermReveal {
50
+ /** The slide it is on, "h,v". */
51
+ slide: string;
52
+ /** The glossary entry's id, or a one-off `<Term def>`'s. */
53
+ id: string;
54
+ /** Which of the slide's mentions of that term, in document order — every screen runs the same build. */
55
+ n: number;
56
+ /** The related entry the card was swapped to through a "see also" chip, or null. */
57
+ view: string | null;
58
+ /** Opened by a click, a tap or a hint label: stays until dismissed. A hover's card leaves with the pointer. */
59
+ pinned: boolean;
60
+ /** Opened in this window, or relayed from the presenter through the room. */
61
+ from: "local" | "remote";
62
+ }
63
+ export interface DeckActions {
64
+ next(): void;
65
+ prev(): void;
66
+ nextSlide(): void;
67
+ prevSlide(): void;
68
+ left(): void;
69
+ right(): void;
70
+ up(): void;
71
+ down(): void;
72
+ first(): void;
73
+ last(): void;
74
+ goto(h: number, v?: number, click?: number): void;
75
+ gotoFlat(flat: number): void;
76
+ gotoId(id: string): void;
77
+ /** Follow the presenter's navigation, jumping to where they are — or stop following. */
78
+ setFollow(on: boolean): void;
79
+ /**
80
+ * Open a window that mirrors this deck and nothing else — a second screen, or the one window to
81
+ * share on a call — or close the one this window opened. See `Flags.projector`.
82
+ */
83
+ toggleProjector(force?: boolean): void;
84
+ toggleOverview(force?: boolean): void;
85
+ togglePause(force?: boolean): void;
86
+ toggleHelp(force?: boolean): void;
87
+ toggleJump(force?: boolean): void;
88
+ toggleSearch(force?: boolean): void;
89
+ toggleFullscreen(): void;
90
+ toggleAutoSlide(): void;
91
+ toggleDrawing(force?: boolean): void;
92
+ togglePointer(force?: boolean): void;
93
+ toggleColorScheme(): void;
94
+ setColorScheme(scheme: "light" | "dark"): void;
95
+ toggleHints(force?: boolean): void;
96
+ toggleThemeMenu(force?: boolean): void;
97
+ toggleTermSync(force?: boolean): void;
98
+ setTheme(name: string): void;
99
+ openPresenter(): void;
100
+ openLightbox(kind: LightboxKind, src: string, fit?: string): void;
101
+ closeLightbox(): void;
102
+ zoomTo(x: number, y: number): void;
103
+ zoomOut(): void;
104
+ toast(message: string): void;
105
+ }
106
+ /**
107
+ * Where a navigation change came from, while it is being applied: the cross-window channel, the
108
+ * live room, the deck's own auto-slide — or nowhere, which means a person, here. A window
109
+ * forwards only what originated locally, or two windows would relay stale positions to each other
110
+ * forever; and only a move with no source at all ends this window's follow (see `NavState.follow`
111
+ * and `withTransition` in Deck.tsx).
112
+ */
113
+ export type SyncSource = "channel" | "room" | "auto" | null;
114
+ export interface DeckRuntime {
115
+ manifest: DeckManifest;
116
+ config: DeckConfig;
117
+ columns: Column[];
118
+ /** Per-slide resolved options, keyed "h,v". */
119
+ options: Map<string, SlideOptions>;
120
+ nav: Store<NavState>;
121
+ ui: Store<UiState>;
122
+ layout: Store<Layout>;
123
+ mode: ViewMode;
124
+ /** The URL base of the deck app, e.g. "/showcase/". */
125
+ base: string;
126
+ components: Record<string, ComponentType<Record<string, unknown>>>;
127
+ actions: DeckActions;
128
+ /** A window-scoped id, so a window ignores its own broadcast. */
129
+ windowId: string;
130
+ /** Set while a remote position is being applied (store listeners run synchronously). */
131
+ sync: {
132
+ source: SyncSource;
133
+ };
134
+ /** Whether this window is the presenter (drives the room and the audience). */
135
+ isPresenter: boolean;
136
+ /** Whether this window is a projector: it follows, always, and takes no input. */
137
+ projector: boolean;
138
+ /** The term card this window is showing, or null. One at a time. */
139
+ terms: Store<TermReveal | null>;
140
+ /** Definitions by id: the deck's glossary, plus every mounted one-off `<Term def>`. */
141
+ glossary: Map<string, GlossaryEntry>;
142
+ }
143
+ export declare const DeckContext: import("react").Context<DeckRuntime | null>;
144
+ export declare function useDeck(): DeckRuntime;
145
+ /** What one mounted slide knows about itself. */
146
+ export interface SlideContextValue {
147
+ meta: SlideMeta;
148
+ options: SlideOptions;
149
+ h: number;
150
+ v: number;
151
+ /** The slide currently shown (as opposed to a preloaded neighbour, an overview tile, a print page). */
152
+ active: boolean;
153
+ /** The click to render at. Past slides render fully clicked, future ones at 0. */
154
+ click: number;
155
+ registry: ClickRegistry;
156
+ magic: MagicMoveData[] | null;
157
+ /** True inside the presenter's next-slide preview, the overview and print: no media, no autoplay. */
158
+ preview: boolean;
159
+ /** Whether glossary terms respond here (see `SlideFrameProps.terms`). */
160
+ terms: boolean;
161
+ }
162
+ export declare const SlideContext: import("react").Context<SlideContextValue | null>;
163
+ export declare function useSlide(): SlideContextValue;
164
+ export declare function useSlideOptional(): SlideContextValue | null;
165
+ //#endregion
@@ -0,0 +1,19 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region src/react/context.ts
3
+ const DeckContext = createContext(null);
4
+ function useDeck() {
5
+ const ctx = useContext(DeckContext);
6
+ if (!ctx) throw new Error("useDeck must be used inside <Deck>");
7
+ return ctx;
8
+ }
9
+ const SlideContext = createContext(null);
10
+ function useSlide() {
11
+ const ctx = useContext(SlideContext);
12
+ if (!ctx) throw new Error("This component must be used inside a slide");
13
+ return ctx;
14
+ }
15
+ function useSlideOptional() {
16
+ return useContext(SlideContext);
17
+ }
18
+ //#endregion
19
+ export { DeckContext, SlideContext, useDeck, useSlide, useSlideOptional };