@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,315 @@
1
+ import { createStore, useStore } from "../core/store.js";
2
+ import { useDeck } from "../react/context.js";
3
+ import { useActions, useLayout, useNav, useUi } from "../react/hooks.js";
4
+ import { Icon } from "../components/bling.js";
5
+ import { useLiveOptional } from "../live/context.js";
6
+ import { EMPTY_ROOM } from "../live/protocol.js";
7
+ import { DRAW_COLORS, DRAW_SIZES, FADE_DWELL, fadeDuration, loadDrawings, newStroke, saveDrawings, segmentsOf, simplify, swatchGradient } from "./strokes.js";
8
+ import { useEffect, useRef, useState } from "react";
9
+ import { createPortal } from "react-dom";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/draw/DrawingLayer.tsx
12
+ const noRoom = createStore(EMPTY_ROOM);
13
+ /**
14
+ * Drawing and annotation on the current slide: pen and highlighter, colours, sizes, undo, clear.
15
+ * Strokes are kept in stage coordinates so they line up on every screen size, persisted per
16
+ * deck in localStorage, and — when the room is live — sent to the room so every window shows
17
+ * them. A stylus draws without the mode being on; fingers and the mouse navigate.
18
+ *
19
+ * A stroke drifts through its colour's family as it travels (see `segmentsOf`). Unless the pin
20
+ * is on, a stroke is a scribble: it lingers a few seconds after the pen lifts, then unwinds
21
+ * along its own path — every screen does the same from the moment it saw the stroke, so nothing
22
+ * more needs to cross the wire. Pinned strokes stay, persist, and can be undone or cleared.
23
+ *
24
+ * `inCanvas`: rendered inside a scaled stage canvas (the presenter's current-slide box) rather
25
+ * than over the viewport — it fills the canvas in stage pixels and its toolbar is portalled out.
26
+ */
27
+ function DrawingLayer({ inCanvas = false } = {}) {
28
+ const deck = useDeck();
29
+ const layout = useLayout();
30
+ const drawing = useUi((s) => s.drawing);
31
+ const { h, v } = useNav((s) => ({
32
+ h: s.h,
33
+ v: s.v
34
+ }));
35
+ const actions = useActions();
36
+ const live = useLiveOptional();
37
+ const slideKey = `${h},${v}`;
38
+ const canDraw = deck.config.drawings.enabled && (!deck.config.drawings.presenterOnly || deck.isPresenter || !live);
39
+ const [local, setLocal] = useState(() => canDraw && deck.config.drawings.persist ? loadDrawings(deck.config.slug || "deck") : {});
40
+ const [tool, setTool] = useState("pen");
41
+ const [color, setColor] = useState(DRAW_COLORS[0]);
42
+ const [size, setSize] = useState(DRAW_SIZES[1]);
43
+ const [keep, setKeep] = useState(false);
44
+ const [current, setCurrent] = useState(null);
45
+ const svg = useRef(null);
46
+ const remote = useStore(live?.client.state ?? noRoom, (s) => s.drawings);
47
+ const seen = useRef(/* @__PURE__ */ new Map());
48
+ const gone = useRef(/* @__PURE__ */ new Set());
49
+ const [progress, setProgress] = useState({});
50
+ const mine = local[slideKey] ?? [];
51
+ const strokes = [...mine, ...(remote[slideKey] ?? []).filter((r) => !mine.some((l) => l.id === r.id))].filter((s) => !gone.current.has(s.id));
52
+ useEffect(() => {
53
+ if (canDraw && deck.config.drawings.persist) saveDrawings(deck.config.slug || "deck", local);
54
+ }, [
55
+ local,
56
+ canDraw,
57
+ deck.config.drawings.persist,
58
+ deck.config.slug
59
+ ]);
60
+ const fadingKey = strokes.filter((s) => s.fade).map((s) => s.id).join(",");
61
+ useEffect(() => {
62
+ if (!fadingKey) return;
63
+ const fading = strokes.filter((s) => s.fade);
64
+ const now = performance.now();
65
+ for (const s of fading) if (!seen.current.has(s.id)) seen.current.set(s.id, now);
66
+ let raf = 0;
67
+ let timer = null;
68
+ const tick = () => {
69
+ const t = performance.now();
70
+ const next = {};
71
+ const finished = [];
72
+ let unwinding = false;
73
+ let soonest = Number.POSITIVE_INFINITY;
74
+ for (const s of fading) {
75
+ const t0 = seen.current.get(s.id) ?? t;
76
+ const p = (t - t0 - FADE_DWELL) / fadeDuration(s);
77
+ if (p >= 1) finished.push(s.id);
78
+ else if (p > 0) {
79
+ next[s.id] = p;
80
+ unwinding = true;
81
+ } else soonest = Math.min(soonest, t0 + FADE_DWELL - t);
82
+ }
83
+ if (finished.length) {
84
+ for (const id of finished) {
85
+ gone.current.add(id);
86
+ seen.current.delete(id);
87
+ }
88
+ setLocal((d) => {
89
+ const list = d[slideKey] ?? [];
90
+ if (!list.some((s) => finished.includes(s.id))) return d;
91
+ return {
92
+ ...d,
93
+ [slideKey]: list.filter((s) => !finished.includes(s.id))
94
+ };
95
+ });
96
+ live?.client.state.update((st) => {
97
+ const list = st.drawings[slideKey] ?? [];
98
+ if (!list.some((s) => finished.includes(s.id))) return st;
99
+ return {
100
+ ...st,
101
+ drawings: {
102
+ ...st.drawings,
103
+ [slideKey]: list.filter((s) => !finished.includes(s.id))
104
+ }
105
+ };
106
+ });
107
+ }
108
+ setProgress((prev) => {
109
+ const a = Object.keys(prev);
110
+ const b = Object.keys(next);
111
+ if (a.length === b.length && a.every((k) => prev[k] === next[k])) return prev;
112
+ return next;
113
+ });
114
+ if (unwinding) raf = requestAnimationFrame(tick);
115
+ else if (Number.isFinite(soonest)) timer = setTimeout(tick, Math.max(16, soonest));
116
+ };
117
+ tick();
118
+ return () => {
119
+ cancelAnimationFrame(raf);
120
+ if (timer) clearTimeout(timer);
121
+ };
122
+ }, [fadingKey, slideKey]);
123
+ const toStage = (e) => {
124
+ const rect = svg.current.getBoundingClientRect();
125
+ return [(e.clientX - rect.left) / rect.width * deck.config.width, (e.clientY - rect.top) / rect.height * deck.config.height];
126
+ };
127
+ const onDown = (e) => {
128
+ if (!canDraw) return;
129
+ if (!drawing && e.pointerType !== "pen") return;
130
+ if (e.button !== 0) return;
131
+ e.target.setPointerCapture?.(e.pointerId);
132
+ const s = newStroke(tool, color, size, !keep);
133
+ s.points.push(toStage(e));
134
+ setCurrent(s);
135
+ };
136
+ const onMove = (e) => {
137
+ if (!current) return;
138
+ setCurrent({
139
+ ...current,
140
+ points: [...current.points, toStage(e)]
141
+ });
142
+ };
143
+ const onUp = () => {
144
+ if (!current) return;
145
+ const done = {
146
+ ...current,
147
+ points: simplify(current.points)
148
+ };
149
+ setCurrent(null);
150
+ setLocal((d) => ({
151
+ ...d,
152
+ [slideKey]: [...d[slideKey] ?? [], done]
153
+ }));
154
+ if (live && (deck.isPresenter || deck.config.drawings.syncAll)) live.client.send({
155
+ type: "draw",
156
+ slide: slideKey,
157
+ stroke: done
158
+ });
159
+ };
160
+ const undo = () => {
161
+ setLocal((d) => ({
162
+ ...d,
163
+ [slideKey]: (d[slideKey] ?? []).slice(0, -1)
164
+ }));
165
+ if (live && (deck.isPresenter || deck.config.drawings.syncAll)) live.client.send({
166
+ type: "draw:undo",
167
+ slide: slideKey
168
+ });
169
+ };
170
+ const clear = () => {
171
+ setLocal((d) => ({
172
+ ...d,
173
+ [slideKey]: []
174
+ }));
175
+ if (live && (deck.isPresenter || deck.config.drawings.syncAll)) live.client.send({
176
+ type: "draw:clear",
177
+ slide: slideKey
178
+ });
179
+ };
180
+ useEffect(() => {
181
+ const onKey = (e) => {
182
+ if (!drawing) return;
183
+ if (e.code === "KeyC" && e.shiftKey) clear();
184
+ if (e.code === "KeyZ" && (e.ctrlKey || e.metaKey)) undo();
185
+ if (e.code === "Escape") actions.toggleDrawing(false);
186
+ };
187
+ window.addEventListener("keydown", onKey);
188
+ return () => window.removeEventListener("keydown", onKey);
189
+ });
190
+ if (!deck.config.drawings.enabled) return null;
191
+ const toolbar = /* @__PURE__ */ jsxs("div", {
192
+ className: "deck-draw-toolbar",
193
+ role: "toolbar",
194
+ "aria-label": "Drawing tools",
195
+ children: [
196
+ /* @__PURE__ */ jsx("button", {
197
+ type: "button",
198
+ className: tool === "pen" ? "is-on" : "",
199
+ onClick: () => setTool("pen"),
200
+ title: "Pen",
201
+ "aria-label": "Pen",
202
+ children: /* @__PURE__ */ jsx(Icon, { name: "pencil" })
203
+ }),
204
+ /* @__PURE__ */ jsx("button", {
205
+ type: "button",
206
+ className: tool === "highlighter" ? "is-on" : "",
207
+ onClick: () => setTool("highlighter"),
208
+ title: "Highlighter",
209
+ "aria-label": "Highlighter",
210
+ children: /* @__PURE__ */ jsx(Icon, { name: "highlighter" })
211
+ }),
212
+ /* @__PURE__ */ jsx("span", { className: "deck-draw-sep" }),
213
+ DRAW_COLORS.map((c) => /* @__PURE__ */ jsx("button", {
214
+ type: "button",
215
+ className: ["deck-draw-color", color === c ? "is-on" : ""].join(" "),
216
+ onClick: () => setColor(c),
217
+ "aria-label": c,
218
+ children: /* @__PURE__ */ jsx("span", {
219
+ className: "deck-draw-swatch",
220
+ style: { background: swatchGradient(c) }
221
+ })
222
+ }, c)),
223
+ /* @__PURE__ */ jsx("span", { className: "deck-draw-sep" }),
224
+ DRAW_SIZES.map((s) => /* @__PURE__ */ jsx("button", {
225
+ type: "button",
226
+ className: size === s ? "is-on" : "",
227
+ onClick: () => setSize(s),
228
+ title: `Size ${s}`,
229
+ children: /* @__PURE__ */ jsx("span", {
230
+ className: "deck-draw-dot",
231
+ style: {
232
+ width: s + 4,
233
+ height: s + 4
234
+ }
235
+ })
236
+ }, s)),
237
+ /* @__PURE__ */ jsx("span", { className: "deck-draw-sep" }),
238
+ /* @__PURE__ */ jsx("button", {
239
+ type: "button",
240
+ className: keep ? "is-on" : "",
241
+ onClick: () => setKeep((k) => !k),
242
+ title: keep ? "Strokes stay on the slide — click to let them fade" : "Strokes fade after a moment — click to keep them",
243
+ "aria-label": keep ? "Strokes stay" : "Strokes fade",
244
+ children: /* @__PURE__ */ jsx(Icon, { name: keep ? "pin" : "pin-off" })
245
+ }),
246
+ /* @__PURE__ */ jsx("button", {
247
+ type: "button",
248
+ onClick: undo,
249
+ title: "Undo (Ctrl Z)",
250
+ "aria-label": "Undo",
251
+ children: /* @__PURE__ */ jsx(Icon, { name: "undo-2" })
252
+ }),
253
+ /* @__PURE__ */ jsx("button", {
254
+ type: "button",
255
+ onClick: clear,
256
+ title: "Clear (Shift C)",
257
+ "aria-label": "Clear",
258
+ children: /* @__PURE__ */ jsx(Icon, { name: "eraser" })
259
+ }),
260
+ /* @__PURE__ */ jsx("button", {
261
+ type: "button",
262
+ onClick: () => actions.toggleDrawing(false),
263
+ title: "Done (Esc)",
264
+ "aria-label": "Done",
265
+ children: /* @__PURE__ */ jsx(Icon, { name: "check" })
266
+ })
267
+ ]
268
+ });
269
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("svg", {
270
+ ref: svg,
271
+ className: "deck-drawing",
272
+ viewBox: `0 0 ${deck.config.width} ${deck.config.height}`,
273
+ style: inCanvas ? {
274
+ left: 0,
275
+ top: 0,
276
+ width: deck.config.width,
277
+ height: deck.config.height,
278
+ pointerEvents: drawing && canDraw ? "auto" : "none"
279
+ } : {
280
+ left: layout.left,
281
+ top: layout.top,
282
+ width: layout.width,
283
+ height: layout.height,
284
+ pointerEvents: drawing && canDraw ? "auto" : "none"
285
+ },
286
+ onPointerDown: onDown,
287
+ onPointerMove: onMove,
288
+ onPointerUp: onUp,
289
+ onPointerCancel: onUp,
290
+ "aria-hidden": true,
291
+ children: [...strokes, ...current ? [current] : []].map((s) => {
292
+ const p = progress[s.id] ?? 0;
293
+ const from = p > 0 ? Math.floor(p * s.points.length) : 0;
294
+ const wide = s.tool === "highlighter";
295
+ return /* @__PURE__ */ jsx("g", {
296
+ opacity: p > 0 ? 1 - p * .35 : 1,
297
+ style: wide ? { mixBlendMode: "multiply" } : void 0,
298
+ children: segmentsOf(s, from).map((seg, i) => /* @__PURE__ */ jsx("path", {
299
+ d: seg.d,
300
+ stroke: seg.color,
301
+ strokeWidth: wide ? s.size * 3 : s.size,
302
+ strokeOpacity: wide ? .35 : 1,
303
+ strokeLinecap: "round",
304
+ strokeLinejoin: "round",
305
+ fill: "none"
306
+ }, i))
307
+ }, s.id);
308
+ })
309
+ }), drawing && canDraw ? inCanvas ? createPortal(/* @__PURE__ */ jsx("div", {
310
+ className: "deck-draw-toolbar-host",
311
+ children: toolbar
312
+ }), document.body) : toolbar : null] });
313
+ }
314
+ //#endregion
315
+ export { DrawingLayer };
@@ -0,0 +1,188 @@
1
+ //#region src/draw/strokes.ts
2
+ /** A smooth SVG path through a stroke's points: quadratic curves through midpoints. */
3
+ function pathOf(points) {
4
+ if (points.length === 0) return "";
5
+ if (points.length === 1) {
6
+ const [x, y] = points[0];
7
+ return `M ${x} ${y} L ${x + .01} ${y}`;
8
+ }
9
+ let d = `M ${points[0][0]} ${points[0][1]}`;
10
+ for (let i = 1; i < points.length - 1; i++) {
11
+ const [x0, y0] = points[i];
12
+ const [x1, y1] = points[i + 1];
13
+ d += ` Q ${x0} ${y0} ${(x0 + x1) / 2} ${(y0 + y1) / 2}`;
14
+ }
15
+ const last = points[points.length - 1];
16
+ d += ` L ${last[0]} ${last[1]}`;
17
+ return d;
18
+ }
19
+ /** Drop points closer than `min` to the previous kept point, so strokes stay light on the wire. */
20
+ function simplify(points, min = 2) {
21
+ const out = [];
22
+ for (const p of points) {
23
+ const last = out[out.length - 1];
24
+ if (!last || Math.hypot(p[0] - last[0], p[1] - last[1]) >= min) out.push(p);
25
+ }
26
+ return out;
27
+ }
28
+ function newStroke(tool, color, size, fade = false) {
29
+ return {
30
+ id: `s_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`,
31
+ tool,
32
+ color,
33
+ size,
34
+ points: [],
35
+ ...fade ? { fade: true } : {}
36
+ };
37
+ }
38
+ const DRAW_COLORS = [
39
+ "#ef4444",
40
+ "#f59e0b",
41
+ "#22c55e",
42
+ "#3b82f6",
43
+ "#a855f7",
44
+ "#111827",
45
+ "#ffffff"
46
+ ];
47
+ const DRAW_SIZES = [
48
+ 4,
49
+ 8,
50
+ 16
51
+ ];
52
+ /**
53
+ * Each pen is a small family rather than one flat colour: the swatch shows the family as a
54
+ * gradient, and a stroke drifts through it as it travels — orange through amber and coral and
55
+ * back — so a scribble reads as one gesture with a little life in it. Every screen derives the
56
+ * same colours from the stroke's base colour and points, so nothing extra goes over the wire.
57
+ */
58
+ const FAMILIES = {
59
+ "#ef4444": [
60
+ "#f43f5e",
61
+ "#ef4444",
62
+ "#fb923c"
63
+ ],
64
+ "#f59e0b": [
65
+ "#f97316",
66
+ "#f59e0b",
67
+ "#fbbf24"
68
+ ],
69
+ "#22c55e": [
70
+ "#10b981",
71
+ "#22c55e",
72
+ "#84cc16"
73
+ ],
74
+ "#3b82f6": [
75
+ "#0ea5e9",
76
+ "#3b82f6",
77
+ "#6366f1"
78
+ ],
79
+ "#a855f7": [
80
+ "#8b5cf6",
81
+ "#a855f7",
82
+ "#ec4899"
83
+ ],
84
+ "#111827": [
85
+ "#0f172a",
86
+ "#111827",
87
+ "#374151"
88
+ ],
89
+ "#ffffff": [
90
+ "#f1f5f9",
91
+ "#ffffff",
92
+ "#e2e8f0"
93
+ ]
94
+ };
95
+ function familyOf(color) {
96
+ return FAMILIES[color.toLowerCase()] ?? [
97
+ color,
98
+ color,
99
+ color
100
+ ];
101
+ }
102
+ function swatchGradient(color) {
103
+ const [a, b, c] = familyOf(color);
104
+ return `linear-gradient(135deg, ${a} 0%, ${b} 50%, ${c} 100%)`;
105
+ }
106
+ function hexToRgb(hex) {
107
+ const h = hex.replace("#", "");
108
+ const n = parseInt(h.length === 3 ? [...h].map((c) => c + c).join("") : h, 16);
109
+ return [
110
+ n >> 16 & 255,
111
+ n >> 8 & 255,
112
+ n & 255
113
+ ];
114
+ }
115
+ function mix(a, b, t) {
116
+ const [r1, g1, b1] = hexToRgb(a);
117
+ const [r2, g2, b2] = hexToRgb(b);
118
+ return `rgb(${Math.round(r1 + (r2 - r1) * t)} ${Math.round(g1 + (g2 - g1) * t)} ${Math.round(b1 + (b2 - b1) * t)})`;
119
+ }
120
+ /** The family colour at phase `u` in 0..1: a → b → c → b → a, so the cycle has no seam. */
121
+ function familyAt(color, u) {
122
+ const [a, b, c] = familyOf(color);
123
+ const q = (u % 1 + 1) % 1 * 4;
124
+ if (q < 1) return mix(a, b, q);
125
+ if (q < 2) return mix(b, c, q - 1);
126
+ if (q < 3) return mix(c, b, q - 2);
127
+ return mix(b, a, q - 3);
128
+ }
129
+ /**
130
+ * A stroke as runs of a few points, each coloured by its distance along the path. Round caps
131
+ * overlap the joins; from `from` on (a point index) the head has already been erased.
132
+ */
133
+ function segmentsOf(stroke, from = 0, run = 5) {
134
+ const pts = stroke.points;
135
+ if (pts.length === 0) return [];
136
+ const [a, b, c] = familyOf(stroke.color);
137
+ if (a === b && b === c) return [{
138
+ d: pathOf(pts.slice(from)),
139
+ color: stroke.color
140
+ }];
141
+ const out = [];
142
+ let dist = 0;
143
+ const start = Math.max(0, Math.min(from, pts.length - 1));
144
+ for (let i = 1; i <= start; i++) dist += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
145
+ for (let i = start; i < pts.length; i += run) {
146
+ const slice = pts.slice(i, Math.min(pts.length, i + run + 1));
147
+ if (slice.length === 0) break;
148
+ let d = dist;
149
+ for (let j = 1; j < slice.length; j++) d += Math.hypot(slice[j][0] - slice[j - 1][0], slice[j][1] - slice[j - 1][1]);
150
+ const mid = (dist + d) / 2;
151
+ out.push({
152
+ d: pathOf(slice),
153
+ color: familyAt(stroke.color, mid / 640)
154
+ });
155
+ dist = d;
156
+ if (slice.length < run + 1) break;
157
+ }
158
+ return out;
159
+ }
160
+ /** A fading stroke lingers this long after the pen lifts, then unwinds along its own path. */
161
+ const FADE_DWELL = 5e3;
162
+ /** How long the unwinding takes: roughly the pace it was drawn at, within reason. */
163
+ function fadeDuration(stroke) {
164
+ return Math.min(2400, Math.max(500, stroke.points.length * 9));
165
+ }
166
+ const key = (slug) => `deck.drawings.${slug}`;
167
+ function loadDrawings(slug) {
168
+ try {
169
+ const raw = localStorage.getItem(key(slug));
170
+ const all = raw ? JSON.parse(raw) : {};
171
+ for (const k of Object.keys(all)) all[k] = (all[k] ?? []).filter((s) => !s.fade);
172
+ return all;
173
+ } catch {
174
+ return {};
175
+ }
176
+ }
177
+ function saveDrawings(slug, drawings) {
178
+ try {
179
+ const kept = {};
180
+ for (const [k, list] of Object.entries(drawings)) {
181
+ const keep = list.filter((s) => !s.fade);
182
+ if (keep.length) kept[k] = keep;
183
+ }
184
+ localStorage.setItem(key(slug), JSON.stringify(kept));
185
+ } catch {}
186
+ }
187
+ //#endregion
188
+ export { DRAW_COLORS, DRAW_SIZES, FADE_DWELL, fadeDuration, familyAt, familyOf, loadDrawings, newStroke, pathOf, saveDrawings, segmentsOf, simplify, swatchGradient };
@@ -0,0 +1,68 @@
1
+ //#region src/export/index.d.ts
2
+ /**
3
+ * ─────────────────────────────────────────────────────────────────────────────
4
+ * EXPORT — PDF, PNG and PPTX from a running deck, with an optional upload to the host
5
+ * ─────────────────────────────────────────────────────────────────────────────
6
+ *
7
+ * Drives a headless Chromium over the deck's print view (`?print-pdf`), which lays every slide
8
+ * (and every click step when `pdfSeparateFragments` is on) out as a page at the authored size.
9
+ * PDF comes from the browser's own printer; PNGs are element screenshots of each page; PPTX
10
+ * wraps those PNGs one per slide with the notes attached, so the deck opens in PowerPoint,
11
+ * Keynote and Google Slides looking exactly like the web.
12
+ *
13
+ * Embedded pages cannot be live on paper, so before the build the exporter captures a poster
14
+ * for every iframe slide and `<Iframe>` that names none: a screenshot of the URL at the pane's
15
+ * size, saved under `public/posters/` where the frame shell looks for it (see core/poster.ts).
16
+ * ─────────────────────────────────────────────────────────────────────────────
17
+ */
18
+ export interface PosterJob {
19
+ /** The page the frame embeds — what names the poster. */
20
+ url: string;
21
+ /** The page to photograph, when it differs (a state the frame's URL does not carry). */
22
+ capture: string;
23
+ width: number;
24
+ height: number;
25
+ /** Relative to the deck's `public/`. */
26
+ file: string;
27
+ /** Milliseconds to let the page settle before the shot. */
28
+ wait: number;
29
+ }
30
+ /** Every embedded page in the deck that has no poster of its own, with the size to capture. */
31
+ export declare function posterJobs(deckDir: string, config: {
32
+ width: number;
33
+ height: number;
34
+ }, entry?: string): PosterJob[];
35
+ /**
36
+ * Capture the posters that are missing (or all of them with `refresh`). WebGL pages render
37
+ * through SwiftShader; each page gets its job's `wait` after load for maps and fonts.
38
+ */
39
+ export declare function capturePosters(deckDir: string, jobs: PosterJob[], { refresh, log }?: {
40
+ refresh?: boolean;
41
+ log?: (line: string) => void;
42
+ }): Promise<number>;
43
+ export interface ExportOptions {
44
+ /** The deck's URL, e.g. http://localhost:4173/showcase/ (with the trailing slash). */
45
+ url: string;
46
+ outDir: string;
47
+ slug: string;
48
+ pdf?: boolean;
49
+ png?: boolean;
50
+ pptx?: boolean;
51
+ /** Device scale for PNG/PPTX captures. */
52
+ scale?: number;
53
+ /** Upload the results to a host (`https://deck.kadal.cc`) using DECK_ADMIN_KEY or a presenter cookie. */
54
+ upload?: string | null;
55
+ adminKey?: string | null;
56
+ log?: (line: string) => void;
57
+ }
58
+ export interface ExportResult {
59
+ files: {
60
+ kind: "pdf" | "png" | "pptx";
61
+ path: string;
62
+ size: number;
63
+ uploaded?: string;
64
+ }[];
65
+ pages: number;
66
+ }
67
+ export declare function exportDeck(options: ExportOptions): Promise<ExportResult>;
68
+ //#endregion