@kadal/deck 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/LICENSING.md +40 -0
- package/README.md +170 -2
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/audience/index.d.ts +14 -0
- package/dist/audience/index.js +680 -0
- package/dist/backgrounds/index.d.ts +65 -0
- package/dist/backgrounds/index.js +604 -0
- package/dist/backgrounds/shared.d.ts +7 -0
- package/dist/backgrounds/shared.js +89 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +8 -0
- package/dist/cli/host.js +111 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +140 -0
- package/dist/cli/login.js +56 -0
- package/dist/cli/publish.js +159 -0
- package/dist/cli/vite.js +39 -0
- package/dist/compiler/code-meta.d.ts +33 -0
- package/dist/compiler/code-meta.js +81 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +8 -0
- package/dist/compiler/magic-move.d.ts +26 -0
- package/dist/compiler/magic-move.js +33 -0
- package/dist/compiler/plain-text.js +7 -0
- package/dist/compiler/rehype-code.d.ts +18 -0
- package/dist/compiler/rehype-code.js +66 -0
- package/dist/compiler/rehype-glossary.d.ts +35 -0
- package/dist/compiler/rehype-glossary.js +211 -0
- package/dist/compiler/slots.d.ts +3 -0
- package/dist/compiler/slots.js +36 -0
- package/dist/compiler/split.d.ts +63 -0
- package/dist/compiler/split.js +195 -0
- package/dist/components/Background.d.ts +15 -0
- package/dist/components/Background.js +24 -0
- package/dist/components/Click.d.ts +54 -0
- package/dist/components/Click.js +129 -0
- package/dist/components/Code.d.ts +27 -0
- package/dist/components/Code.js +124 -0
- package/dist/components/Motion.d.ts +25 -0
- package/dist/components/Motion.js +45 -0
- package/dist/components/Sketch.d.ts +44 -0
- package/dist/components/Sketch.js +141 -0
- package/dist/components/Term.d.ts +63 -0
- package/dist/components/Term.js +492 -0
- package/dist/components/bling.d.ts +75 -0
- package/dist/components/bling.js +146 -0
- package/dist/components/content.d.ts +232 -0
- package/dist/components/content.js +637 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +88 -0
- package/dist/core/clicks.d.ts +45 -0
- package/dist/core/clicks.js +75 -0
- package/dist/core/hash.d.ts +33 -0
- package/dist/core/hash.js +49 -0
- package/dist/core/keyboard.d.ts +17 -0
- package/dist/core/keyboard.js +198 -0
- package/dist/core/layout.d.ts +16 -0
- package/dist/core/layout.js +25 -0
- package/dist/core/model.d.ts +259 -0
- package/dist/core/model.js +377 -0
- package/dist/core/navigation.d.ts +74 -0
- package/dist/core/navigation.js +244 -0
- package/dist/core/poster.js +30 -0
- package/dist/core/store.d.ts +20 -0
- package/dist/core/store.js +64 -0
- package/dist/core/touch.js +27 -0
- package/dist/draw/DrawingLayer.js +315 -0
- package/dist/draw/strokes.js +188 -0
- package/dist/export/index.d.ts +68 -0
- package/dist/export/index.js +251 -0
- package/dist/export/run.js +81 -0
- package/dist/glossary/model.d.ts +81 -0
- package/dist/glossary/model.js +156 -0
- package/dist/glossary/parse.d.ts +27 -0
- package/dist/glossary/parse.js +90 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +30 -0
- package/dist/live/LiveProvider.d.ts +20 -0
- package/dist/live/LiveProvider.js +450 -0
- package/dist/live/analytics.d.ts +15 -0
- package/dist/live/analytics.js +103 -0
- package/dist/live/client.d.ts +51 -0
- package/dist/live/client.js +193 -0
- package/dist/live/components.d.ts +64 -0
- package/dist/live/components.js +471 -0
- package/dist/live/context.d.ts +15 -0
- package/dist/live/context.js +28 -0
- package/dist/live/index.d.ts +7 -0
- package/dist/live/index.js +7 -0
- package/dist/live/protocol.d.ts +284 -0
- package/dist/live/protocol.js +42 -0
- package/dist/live/turnstile.js +49 -0
- package/dist/presenter/index.d.ts +20 -0
- package/dist/presenter/index.js +1211 -0
- package/dist/react/AutoAnimate.js +92 -0
- package/dist/react/Backgrounds.d.ts +11 -0
- package/dist/react/Backgrounds.js +154 -0
- package/dist/react/ConfettiLayer.js +85 -0
- package/dist/react/Deck.d.ts +35 -0
- package/dist/react/Deck.js +983 -0
- package/dist/react/Frame.d.ts +16 -0
- package/dist/react/Frame.js +174 -0
- package/dist/react/Hints.d.ts +3 -0
- package/dist/react/Hints.js +201 -0
- package/dist/react/Lightbox.js +54 -0
- package/dist/react/Overview.js +93 -0
- package/dist/react/PrintView.js +128 -0
- package/dist/react/ScrollView.js +128 -0
- package/dist/react/SlideFrame.d.ts +51 -0
- package/dist/react/SlideFrame.js +323 -0
- package/dist/react/Stage.js +151 -0
- package/dist/react/ThemeMenu.d.ts +5 -0
- package/dist/react/ThemeMenu.js +124 -0
- package/dist/react/Zoom.js +23 -0
- package/dist/react/chrome.js +339 -0
- package/dist/react/context.d.ts +165 -0
- package/dist/react/context.js +19 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.js +70 -0
- package/dist/react/layouts.d.ts +21 -0
- package/dist/react/layouts.js +104 -0
- package/dist/react/mdx.d.ts +4 -0
- package/dist/react/mdx.js +33 -0
- package/dist/react/terms.d.ts +6 -0
- package/dist/react/terms.js +66 -0
- package/dist/renderers/Island.d.ts +57 -0
- package/dist/renderers/Island.js +223 -0
- package/dist/renderers/contract.d.ts +173 -0
- package/dist/renderers/contract.js +152 -0
- package/dist/renderers/index.d.ts +12 -0
- package/dist/renderers/index.js +16 -0
- package/dist/renderers/react.d.ts +16 -0
- package/dist/renderers/react.js +32 -0
- package/dist/renderers/web-components.d.ts +4 -0
- package/dist/renderers/web-components.js +103 -0
- package/dist/sketch/Canvas.js +49 -0
- package/dist/sketch/dsl.d.ts +131 -0
- package/dist/sketch/dsl.js +281 -0
- package/dist/sketch/index.d.ts +4 -0
- package/dist/sketch/index.js +3 -0
- package/dist/sketch/palette.d.ts +21 -0
- package/dist/sketch/palette.js +66 -0
- package/dist/sketch/render.d.ts +43 -0
- package/dist/sketch/render.js +198 -0
- package/dist/themes/base.css +1704 -0
- package/dist/themes/bling.css +592 -0
- package/dist/themes/glossary.css +317 -0
- package/dist/themes/live.css +1557 -0
- package/dist/themes/minimal.css +83 -0
- package/dist/themes/rla.css +92 -0
- package/dist/themes/sketch.css +121 -0
- package/dist/themes/stack.css +62 -0
- package/dist/vite/index.d.ts +15 -0
- package/dist/vite/index.js +320 -0
- package/package.json +159 -4
- package/tools/verify-island.mjs +150 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/react/Frame.d.ts
|
|
2
|
+
export declare function FrameShell({ url, className, preload, title, poster, posterSize, scale }: {
|
|
3
|
+
url: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
preload?: boolean;
|
|
6
|
+
title?: string;
|
|
7
|
+
poster?: string;
|
|
8
|
+
/** The pane's size in stage pixels, which names the captured poster. */
|
|
9
|
+
posterSize?: readonly [number, number];
|
|
10
|
+
/**
|
|
11
|
+
* Render the page at 1/scale of the pane and shrink it to fit: 0.5 shows a small pane the
|
|
12
|
+
* desktop layout it would get at twice the size.
|
|
13
|
+
*/
|
|
14
|
+
scale?: number;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useDeck, useSlideOptional } from "./context.js";
|
|
2
|
+
import { POSTER_DEFAULT_SIZE, posterName, resolvePoster } from "../core/poster.js";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/react/Frame.tsx
|
|
6
|
+
/**
|
|
7
|
+
* An embedded page inside a slide, with manners: it loads when its slide is shown (data-src),
|
|
8
|
+
* unloads when the slide is left, and sits behind a shield until you mean to interact — one
|
|
9
|
+
* click, or the F-hint on it — so wheel and arrow keys keep driving the deck until then. While
|
|
10
|
+
* it has the keyboard a "Done" button hands control back; leaving the slide does the same.
|
|
11
|
+
*
|
|
12
|
+
* Until the page has loaded — and wherever it never will (print, overview, the next-slide box,
|
|
13
|
+
* the scroll view) — a poster stands in: the one the deck names, else the screenshot `deck
|
|
14
|
+
* export` captured for this URL at this pane's size, else a card with the address.
|
|
15
|
+
*
|
|
16
|
+
* And it can always be loaded again, which it could not before. The loader in `SlideFrame` gives
|
|
17
|
+
* an iframe its `src` once — often from the prefetch, a slide ahead of arriving — and then skips
|
|
18
|
+
* any frame whose `src` already matches, so a page that failed up there stayed failed for the rest
|
|
19
|
+
* of the talk. Worse, a browser's own error page fires `load` like any other document, so nothing
|
|
20
|
+
* in here could tell a dead frame from a live one. Hence a Reload control that is always one click
|
|
21
|
+
* away, and a timeout for the page that never finishes at all.
|
|
22
|
+
*/
|
|
23
|
+
const preconnected = /* @__PURE__ */ new Set();
|
|
24
|
+
function FrameShell({ url, className, preload = false, title, poster, posterSize, scale }) {
|
|
25
|
+
const deck = useDeck();
|
|
26
|
+
const [live, setLive] = useState(false);
|
|
27
|
+
const [loaded, setLoaded] = useState(false);
|
|
28
|
+
const [posterState, setPosterState] = useState("pending");
|
|
29
|
+
const [attempt, setAttempt] = useState(0);
|
|
30
|
+
/** Bumped to throw the iframe element away and start again with a clean one. */
|
|
31
|
+
const [nonce, setNonce] = useState(0);
|
|
32
|
+
const ref = useRef(null);
|
|
33
|
+
const slide = useSlideOptional();
|
|
34
|
+
const active = slide?.active ?? true;
|
|
35
|
+
const preview = slide?.preview ?? false;
|
|
36
|
+
const [w, h] = posterSize ?? POSTER_DEFAULT_SIZE;
|
|
37
|
+
const posterSrc = resolvePoster(poster ?? posterName(url, w, h), deck.base);
|
|
38
|
+
const zoom = scale && scale > 0 && scale !== 1 ? scale : 1;
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
try {
|
|
41
|
+
const origin = new URL(url, window.location.href).origin;
|
|
42
|
+
if (origin === window.location.origin || preconnected.has(origin)) return;
|
|
43
|
+
preconnected.add(origin);
|
|
44
|
+
const link = document.createElement("link");
|
|
45
|
+
link.rel = "preconnect";
|
|
46
|
+
link.href = origin;
|
|
47
|
+
link.crossOrigin = "anonymous";
|
|
48
|
+
document.head.appendChild(link);
|
|
49
|
+
} catch {}
|
|
50
|
+
}, [url]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const el = ref.current;
|
|
53
|
+
if (!el) return;
|
|
54
|
+
const on = () => setLive(true);
|
|
55
|
+
el.addEventListener("deck:interact", on);
|
|
56
|
+
return () => el.removeEventListener("deck:interact", on);
|
|
57
|
+
}, []);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!active) {
|
|
60
|
+
setLive(false);
|
|
61
|
+
setLoaded(false);
|
|
62
|
+
setAttempt(0);
|
|
63
|
+
}
|
|
64
|
+
}, [active]);
|
|
65
|
+
/**
|
|
66
|
+
* Load the page again, properly.
|
|
67
|
+
*
|
|
68
|
+
* Dropping `src` and putting the same address back does NOT reload a frame: removing the
|
|
69
|
+
* attribute leaves the document where it is, and assigning an address a frame already has is
|
|
70
|
+
* not a navigation — measured, it makes no request at all, which is why pressing Reload looked
|
|
71
|
+
* like pressing nothing. Two things are needed, and both are cheap:
|
|
72
|
+
*
|
|
73
|
+
* · a new element, so there is no document to keep and no address to match, and
|
|
74
|
+
* · a different address, so nothing cached — least of all a cached failure — can be handed
|
|
75
|
+
* back instead of a fresh load.
|
|
76
|
+
*/
|
|
77
|
+
const reload = useCallback(() => {
|
|
78
|
+
setLoaded(false);
|
|
79
|
+
setAttempt((n) => n + 1);
|
|
80
|
+
setNonce((n) => n + 1);
|
|
81
|
+
}, []);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!nonce) return;
|
|
84
|
+
const el = ref.current;
|
|
85
|
+
if (!el) return;
|
|
86
|
+
el.setAttribute("src", `${url}${url.includes("?") ? "&" : "?"}deck-reload=${Date.now()}`);
|
|
87
|
+
}, [nonce, url]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (preview || !active || loaded || attempt >= 2) return;
|
|
90
|
+
const id = setTimeout(reload, 12e3);
|
|
91
|
+
return () => clearTimeout(id);
|
|
92
|
+
}, [
|
|
93
|
+
preview,
|
|
94
|
+
active,
|
|
95
|
+
loaded,
|
|
96
|
+
attempt,
|
|
97
|
+
reload
|
|
98
|
+
]);
|
|
99
|
+
const touch = typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches;
|
|
100
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
101
|
+
className: [
|
|
102
|
+
"deck-frame",
|
|
103
|
+
live ? "is-live" : "",
|
|
104
|
+
loaded ? "is-loaded" : "",
|
|
105
|
+
preview ? "is-preview" : "",
|
|
106
|
+
className
|
|
107
|
+
].filter(Boolean).join(" "),
|
|
108
|
+
children: [
|
|
109
|
+
/* @__PURE__ */ jsx("iframe", {
|
|
110
|
+
ref,
|
|
111
|
+
"data-src": url,
|
|
112
|
+
"data-preload": preload ? "" : void 0,
|
|
113
|
+
title: title ?? url,
|
|
114
|
+
allow: "fullscreen; geolocation; clipboard-write; autoplay",
|
|
115
|
+
style: zoom !== 1 ? {
|
|
116
|
+
width: `${100 / zoom}%`,
|
|
117
|
+
height: `${100 / zoom}%`,
|
|
118
|
+
maxWidth: "none",
|
|
119
|
+
maxHeight: "none",
|
|
120
|
+
transform: `scale(${zoom})`,
|
|
121
|
+
transformOrigin: "0 0"
|
|
122
|
+
} : void 0,
|
|
123
|
+
onLoad: (e) => {
|
|
124
|
+
if (e.currentTarget.getAttribute("src")) setLoaded(true);
|
|
125
|
+
}
|
|
126
|
+
}, nonce),
|
|
127
|
+
!loaded && posterState !== "missing" ? /* @__PURE__ */ jsx("img", {
|
|
128
|
+
className: "deck-frame-poster",
|
|
129
|
+
src: posterSrc,
|
|
130
|
+
alt: "",
|
|
131
|
+
"aria-hidden": true,
|
|
132
|
+
onLoad: () => setPosterState("ok"),
|
|
133
|
+
onError: () => setPosterState("missing")
|
|
134
|
+
}) : null,
|
|
135
|
+
!loaded && posterState === "missing" ? /* @__PURE__ */ jsx("div", {
|
|
136
|
+
className: "deck-frame-placeholder",
|
|
137
|
+
"aria-hidden": true,
|
|
138
|
+
children: /* @__PURE__ */ jsx("span", { children: url })
|
|
139
|
+
}) : null,
|
|
140
|
+
preview ? null : /* @__PURE__ */ jsx("button", {
|
|
141
|
+
type: "button",
|
|
142
|
+
className: "deck-frame-reload",
|
|
143
|
+
"data-hint": "reload",
|
|
144
|
+
title: "Load this page again",
|
|
145
|
+
onClick: (event) => {
|
|
146
|
+
event.stopPropagation();
|
|
147
|
+
reload();
|
|
148
|
+
},
|
|
149
|
+
children: "Reload"
|
|
150
|
+
}),
|
|
151
|
+
preview ? null : live ? /* @__PURE__ */ jsx("button", {
|
|
152
|
+
type: "button",
|
|
153
|
+
className: "deck-frame-done",
|
|
154
|
+
onClick: () => {
|
|
155
|
+
setLive(false);
|
|
156
|
+
document.activeElement?.blur?.();
|
|
157
|
+
window.focus();
|
|
158
|
+
},
|
|
159
|
+
children: "Done · back to the slides"
|
|
160
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
161
|
+
type: "button",
|
|
162
|
+
className: "deck-frame-shield",
|
|
163
|
+
"data-hint": "interact",
|
|
164
|
+
onClick: () => {
|
|
165
|
+
setLive(true);
|
|
166
|
+
setTimeout(() => ref.current?.focus(), 0);
|
|
167
|
+
},
|
|
168
|
+
children: /* @__PURE__ */ jsx("span", { children: touch ? "Tap to interact" : "Click to interact · F to focus with the keyboard" })
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
export { FrameShell };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { useDeck } from "./context.js";
|
|
2
|
+
import { useActions, useUi } from "./hooks.js";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/react/Hints.tsx
|
|
7
|
+
/**
|
|
8
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
* HINTS — vim's `f`: label every clickable thing, type the label to use it
|
|
10
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* Press F and every link, button, embedded page, image preview and input on the current slide
|
|
13
|
+
* (plus the deck's own controls) gets a short home-row label. Type it and the target is used:
|
|
14
|
+
* links open (external ones in a new tab), buttons click, an embedded page takes the keyboard,
|
|
15
|
+
* an image opens in the lightbox. Escape or F again leaves. Labels are one letter when there
|
|
16
|
+
* are few targets and two when there are many, as in Vimium.
|
|
17
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
*/
|
|
19
|
+
const SELECTOR = [
|
|
20
|
+
"a[href]",
|
|
21
|
+
"button:not([disabled])",
|
|
22
|
+
"[role='button']",
|
|
23
|
+
"iframe",
|
|
24
|
+
"input:not([type='hidden']):not([disabled])",
|
|
25
|
+
"select",
|
|
26
|
+
"textarea",
|
|
27
|
+
"summary",
|
|
28
|
+
"video[controls]",
|
|
29
|
+
"audio[controls]",
|
|
30
|
+
"[data-hint]",
|
|
31
|
+
".deck-image[data-preview]"
|
|
32
|
+
].join(",");
|
|
33
|
+
const LETTERS = "asdfghjkl";
|
|
34
|
+
function labelsFor(n) {
|
|
35
|
+
if (n <= 9) return [...LETTERS].slice(0, n);
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const a of LETTERS) for (const b of LETTERS) out.push(a + b);
|
|
38
|
+
return out.slice(0, n);
|
|
39
|
+
}
|
|
40
|
+
function kindOf(el) {
|
|
41
|
+
const tag = el.tagName;
|
|
42
|
+
if (tag === "A") return "link";
|
|
43
|
+
if (tag === "IFRAME") return "iframe";
|
|
44
|
+
if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") return "input";
|
|
45
|
+
if (tag === "BUTTON" || el.getAttribute("role") === "button") return "button";
|
|
46
|
+
return "other";
|
|
47
|
+
}
|
|
48
|
+
function visible(el) {
|
|
49
|
+
const r = el.getBoundingClientRect();
|
|
50
|
+
if (r.width < 4 || r.height < 4) return false;
|
|
51
|
+
if (r.bottom < 0 || r.right < 0 || r.top > window.innerHeight || r.left > window.innerWidth) return false;
|
|
52
|
+
const cs = getComputedStyle(el);
|
|
53
|
+
if (cs.visibility === "hidden" || cs.display === "none") return false;
|
|
54
|
+
return cs.opacity !== "0" || el.hasAttribute("data-hint");
|
|
55
|
+
}
|
|
56
|
+
function HintLayer() {
|
|
57
|
+
const deck = useDeck();
|
|
58
|
+
const on = useUi((s) => s.hints);
|
|
59
|
+
const actions = useActions();
|
|
60
|
+
const [targets, setTargets] = useState([]);
|
|
61
|
+
const [typed, setTyped] = useState("");
|
|
62
|
+
const collect = useCallback(() => {
|
|
63
|
+
const roots = [];
|
|
64
|
+
const slide = document.querySelector(".deck-slide.is-present");
|
|
65
|
+
if (slide) roots.push(slide);
|
|
66
|
+
for (const extra of document.querySelectorAll(".deck-controls, .deck-theme-menu, .deck-audience, .deck-presenter-tools, .deck-room-panel")) roots.push(extra);
|
|
67
|
+
const seen = /* @__PURE__ */ new Set();
|
|
68
|
+
const els = [];
|
|
69
|
+
for (const root of roots) for (const el of root.querySelectorAll(SELECTOR)) {
|
|
70
|
+
if (seen.has(el) || !visible(el) || el.closest(".deck-hints")) continue;
|
|
71
|
+
if (el.tagName !== "A" && el.closest("a[href]")) continue;
|
|
72
|
+
if (el.tagName === "IFRAME" && el.closest(".deck-frame:not(.is-live)")) continue;
|
|
73
|
+
seen.add(el);
|
|
74
|
+
els.push(el);
|
|
75
|
+
}
|
|
76
|
+
els.sort((a, b) => {
|
|
77
|
+
const ra = a.getBoundingClientRect();
|
|
78
|
+
const rb = b.getBoundingClientRect();
|
|
79
|
+
return ra.top - rb.top || ra.left - rb.left;
|
|
80
|
+
});
|
|
81
|
+
const labels = labelsFor(els.length);
|
|
82
|
+
setTargets(els.map((el, i) => ({
|
|
83
|
+
el,
|
|
84
|
+
label: labels[i],
|
|
85
|
+
rect: el.getBoundingClientRect(),
|
|
86
|
+
kind: kindOf(el)
|
|
87
|
+
})));
|
|
88
|
+
}, []);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!on) {
|
|
91
|
+
setTargets([]);
|
|
92
|
+
setTyped("");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
collect();
|
|
96
|
+
const again = () => collect();
|
|
97
|
+
window.addEventListener("resize", again);
|
|
98
|
+
window.addEventListener("scroll", again, true);
|
|
99
|
+
const unsub = deck.nav.subscribe(again);
|
|
100
|
+
return () => {
|
|
101
|
+
window.removeEventListener("resize", again);
|
|
102
|
+
window.removeEventListener("scroll", again, true);
|
|
103
|
+
unsub();
|
|
104
|
+
};
|
|
105
|
+
}, [
|
|
106
|
+
on,
|
|
107
|
+
collect,
|
|
108
|
+
deck.nav
|
|
109
|
+
]);
|
|
110
|
+
const activate = useCallback((t) => {
|
|
111
|
+
actions.toggleHints(false);
|
|
112
|
+
const el = t.el;
|
|
113
|
+
if (t.kind === "iframe") {
|
|
114
|
+
el.dispatchEvent(new CustomEvent("deck:interact", { bubbles: true }));
|
|
115
|
+
el.focus();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (t.kind === "link") {
|
|
119
|
+
const a = el;
|
|
120
|
+
if (a.target === "_blank" || a.origin && a.origin !== window.location.origin) window.open(a.href, "_blank", "noopener");
|
|
121
|
+
else a.click();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (t.kind === "input") {
|
|
125
|
+
el.focus();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
el.click();
|
|
129
|
+
}, [actions]);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!on) return;
|
|
132
|
+
const onKey = (e) => {
|
|
133
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
134
|
+
if (e.key === "Escape") {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
actions.toggleHints(false);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (e.key === "Backspace") {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
setTyped((t) => t.slice(0, -1));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (e.key.length !== 1) return;
|
|
147
|
+
const key = e.key.toLowerCase();
|
|
148
|
+
if (!LETTERS.includes(key)) {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
e.stopPropagation();
|
|
155
|
+
setTyped((prev) => {
|
|
156
|
+
const next = prev + key;
|
|
157
|
+
const hit = targets.find((t) => t.label === next);
|
|
158
|
+
if (hit) {
|
|
159
|
+
queueMicrotask(() => activate(hit));
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
return targets.some((t) => t.label.startsWith(next)) ? next : "";
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
window.addEventListener("keydown", onKey, true);
|
|
166
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
167
|
+
}, [
|
|
168
|
+
on,
|
|
169
|
+
targets,
|
|
170
|
+
activate,
|
|
171
|
+
actions
|
|
172
|
+
]);
|
|
173
|
+
if (!on || typeof document === "undefined") return null;
|
|
174
|
+
return createPortal(/* @__PURE__ */ jsxs("div", {
|
|
175
|
+
className: "deck-hints",
|
|
176
|
+
"aria-live": "polite",
|
|
177
|
+
children: [targets.map((t) => {
|
|
178
|
+
const match = typed && t.label.startsWith(typed);
|
|
179
|
+
const dim = typed && !match;
|
|
180
|
+
return /* @__PURE__ */ jsx("span", {
|
|
181
|
+
className: [
|
|
182
|
+
"deck-hint",
|
|
183
|
+
`kind-${t.kind}`,
|
|
184
|
+
match ? "is-match" : "",
|
|
185
|
+
dim ? "is-dim" : ""
|
|
186
|
+
].filter(Boolean).join(" "),
|
|
187
|
+
style: {
|
|
188
|
+
left: Math.max(2, t.rect.left),
|
|
189
|
+
top: Math.max(2, t.rect.top - 10)
|
|
190
|
+
},
|
|
191
|
+
onClick: () => activate(t),
|
|
192
|
+
children: match ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("b", { children: typed }), t.label.slice(typed.length)] }) : t.label
|
|
193
|
+
}, t.label);
|
|
194
|
+
}), targets.length === 0 ? /* @__PURE__ */ jsx("span", {
|
|
195
|
+
className: "deck-hint-empty",
|
|
196
|
+
children: "Nothing to focus on this slide"
|
|
197
|
+
}) : null]
|
|
198
|
+
}), document.body);
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
export { HintLayer };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useActions, useUi } from "./hooks.js";
|
|
2
|
+
import { Suspense, lazy } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/react/Lightbox.tsx
|
|
5
|
+
const SketchCanvas = lazy(() => import("../sketch/Canvas.js"));
|
|
6
|
+
/** A full-screen overlay for an image, a video or a web page (reveal.js 5.2's lightbox). */
|
|
7
|
+
function Lightbox() {
|
|
8
|
+
const box = useUi((s) => s.lightbox);
|
|
9
|
+
const actions = useActions();
|
|
10
|
+
if (!box) return null;
|
|
11
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
12
|
+
className: "deck-lightbox",
|
|
13
|
+
role: "dialog",
|
|
14
|
+
"aria-label": "Preview",
|
|
15
|
+
onClick: actions.closeLightbox,
|
|
16
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
17
|
+
type: "button",
|
|
18
|
+
className: "deck-lightbox-close",
|
|
19
|
+
"aria-label": "Close",
|
|
20
|
+
onClick: actions.closeLightbox,
|
|
21
|
+
children: "×"
|
|
22
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
23
|
+
className: "deck-lightbox-media",
|
|
24
|
+
"data-fit": box.fit,
|
|
25
|
+
onClick: (e) => e.stopPropagation(),
|
|
26
|
+
children: [
|
|
27
|
+
box.kind === "image" ? /* @__PURE__ */ jsx("img", {
|
|
28
|
+
src: box.src,
|
|
29
|
+
alt: ""
|
|
30
|
+
}) : null,
|
|
31
|
+
box.kind === "video" ? /* @__PURE__ */ jsx("video", {
|
|
32
|
+
src: box.src,
|
|
33
|
+
controls: true,
|
|
34
|
+
autoPlay: true,
|
|
35
|
+
playsInline: true
|
|
36
|
+
}) : null,
|
|
37
|
+
box.kind === "iframe" ? /* @__PURE__ */ jsx("iframe", {
|
|
38
|
+
src: box.src,
|
|
39
|
+
title: "Preview",
|
|
40
|
+
allow: "fullscreen; autoplay"
|
|
41
|
+
}) : null,
|
|
42
|
+
box.kind === "sketch" ? /* @__PURE__ */ jsx(Suspense, {
|
|
43
|
+
fallback: /* @__PURE__ */ jsx("div", {
|
|
44
|
+
className: "deck-sketch-loading",
|
|
45
|
+
children: "Opening the canvas…"
|
|
46
|
+
}),
|
|
47
|
+
children: /* @__PURE__ */ jsx(SketchCanvas, { id: box.src })
|
|
48
|
+
}) : null
|
|
49
|
+
]
|
|
50
|
+
})]
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Lightbox };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { key } from "../core/navigation.js";
|
|
2
|
+
import { useDeck } from "./context.js";
|
|
3
|
+
import { useActions, useNav } from "./hooks.js";
|
|
4
|
+
import { SlideFrame } from "./SlideFrame.js";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/react/Overview.tsx
|
|
8
|
+
/**
|
|
9
|
+
* The overview: every column as a row of its stack, each slide live and scaled down, the current
|
|
10
|
+
* one ringed. Click a tile to go there; Esc or O leaves. Slides render in preview mode so nothing
|
|
11
|
+
* plays until it is chosen.
|
|
12
|
+
*/
|
|
13
|
+
function Overview() {
|
|
14
|
+
const deck = useDeck();
|
|
15
|
+
const { overview, h, v } = useNav((s) => ({
|
|
16
|
+
overview: s.overview,
|
|
17
|
+
h: s.h,
|
|
18
|
+
v: s.v
|
|
19
|
+
}));
|
|
20
|
+
const actions = useActions();
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!overview) return;
|
|
23
|
+
document.querySelector(".deck-overview .is-current")?.scrollIntoView({
|
|
24
|
+
block: "center",
|
|
25
|
+
inline: "center"
|
|
26
|
+
});
|
|
27
|
+
}, [
|
|
28
|
+
overview,
|
|
29
|
+
h,
|
|
30
|
+
v
|
|
31
|
+
]);
|
|
32
|
+
if (!overview) return null;
|
|
33
|
+
const tileW = 320;
|
|
34
|
+
const scale = tileW / deck.config.width;
|
|
35
|
+
const tileH = deck.config.height * scale;
|
|
36
|
+
return /* @__PURE__ */ jsx("div", {
|
|
37
|
+
className: "deck-overview",
|
|
38
|
+
role: "dialog",
|
|
39
|
+
"aria-label": "Slide overview",
|
|
40
|
+
onClick: () => actions.toggleOverview(false),
|
|
41
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
42
|
+
className: "deck-overview-grid",
|
|
43
|
+
onClick: (e) => e.stopPropagation(),
|
|
44
|
+
children: deck.columns.map((col) => /* @__PURE__ */ jsx("div", {
|
|
45
|
+
className: "deck-overview-column",
|
|
46
|
+
children: col.slides.map((meta, cv) => {
|
|
47
|
+
const options = deck.options.get(key(col.h, cv));
|
|
48
|
+
const isCurrent = col.h === h && cv === v;
|
|
49
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
50
|
+
type: "button",
|
|
51
|
+
className: ["deck-overview-tile", isCurrent ? "is-current" : ""].join(" "),
|
|
52
|
+
style: {
|
|
53
|
+
width: tileW,
|
|
54
|
+
height: tileH
|
|
55
|
+
},
|
|
56
|
+
onClick: () => {
|
|
57
|
+
actions.goto(col.h, cv);
|
|
58
|
+
actions.toggleOverview(false);
|
|
59
|
+
},
|
|
60
|
+
"aria-label": meta.title ?? `Slide ${col.h + 1}.${cv + 1}`,
|
|
61
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
62
|
+
className: "deck-overview-canvas",
|
|
63
|
+
style: {
|
|
64
|
+
width: deck.config.width,
|
|
65
|
+
height: deck.config.height,
|
|
66
|
+
transform: `scale(${scale})`
|
|
67
|
+
},
|
|
68
|
+
children: /* @__PURE__ */ jsx(SlideFrame, {
|
|
69
|
+
meta,
|
|
70
|
+
options,
|
|
71
|
+
h: col.h,
|
|
72
|
+
v: cv,
|
|
73
|
+
active: false,
|
|
74
|
+
click: 0,
|
|
75
|
+
preview: true,
|
|
76
|
+
className: "is-tile"
|
|
77
|
+
})
|
|
78
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
79
|
+
className: "deck-overview-label",
|
|
80
|
+
children: [
|
|
81
|
+
col.h + 1,
|
|
82
|
+
cv ? `.${cv + 1}` : "",
|
|
83
|
+
meta.title ? ` · ${meta.title}` : ""
|
|
84
|
+
]
|
|
85
|
+
})]
|
|
86
|
+
}, `${col.h},${cv}`);
|
|
87
|
+
})
|
|
88
|
+
}, col.h))
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { Overview };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { key } from "../core/navigation.js";
|
|
2
|
+
import { useDeck } from "./context.js";
|
|
3
|
+
import { BackgroundLayer } from "./Backgrounds.js";
|
|
4
|
+
import { SlideFrame, loadSlide } from "./SlideFrame.js";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/react/PrintView.tsx
|
|
8
|
+
/**
|
|
9
|
+
* `?print-pdf`: every slide laid out as a page at the authored size, one page per click step when
|
|
10
|
+
* `pdfSeparateFragments` is on, notes printed in a box or on a separate page when `showNotes` is
|
|
11
|
+
* set. Print to PDF from the browser, or let `deck export` drive a headless Chromium over it.
|
|
12
|
+
*/
|
|
13
|
+
function PrintView() {
|
|
14
|
+
const deck = useDeck();
|
|
15
|
+
const [ready, setReady] = useState(false);
|
|
16
|
+
const [totals, setTotals] = useState({});
|
|
17
|
+
const [zoom, setZoom] = useState(1);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const fit = () => setZoom(Math.min(1, (window.innerWidth - 32) / deck.config.width));
|
|
20
|
+
fit();
|
|
21
|
+
window.addEventListener("resize", fit);
|
|
22
|
+
return () => window.removeEventListener("resize", fit);
|
|
23
|
+
}, [deck.config.width]);
|
|
24
|
+
const flat = deck.columns.flatMap((c) => c.slides.map((meta, v) => ({
|
|
25
|
+
meta,
|
|
26
|
+
h: c.h,
|
|
27
|
+
v
|
|
28
|
+
})));
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
let alive = true;
|
|
31
|
+
const grace = setTimeout(() => alive && setReady(true), 15e3);
|
|
32
|
+
Promise.allSettled(flat.map((s) => loadSlide(s.meta))).then(() => {
|
|
33
|
+
if (!alive) return;
|
|
34
|
+
clearTimeout(grace);
|
|
35
|
+
setTimeout(() => alive && setReady(true), 300);
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
alive = false;
|
|
39
|
+
clearTimeout(grace);
|
|
40
|
+
};
|
|
41
|
+
}, [flat.length]);
|
|
42
|
+
useEffect(() => deck.nav.subscribe((s) => setTotals(s.totals)), [deck.nav]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
document.documentElement.classList.add("deck-print");
|
|
45
|
+
const style = document.createElement("style");
|
|
46
|
+
style.textContent = `@page { size: ${deck.config.width}px ${deck.config.height}px; margin: 0; }`;
|
|
47
|
+
document.head.appendChild(style);
|
|
48
|
+
return () => {
|
|
49
|
+
document.documentElement.classList.remove("deck-print");
|
|
50
|
+
style.remove();
|
|
51
|
+
};
|
|
52
|
+
}, [deck.config.width, deck.config.height]);
|
|
53
|
+
const pages = [];
|
|
54
|
+
for (const s of flat) {
|
|
55
|
+
const k = key(s.h, s.v);
|
|
56
|
+
const total = Math.min(totals[k] ?? 0, deck.config.pdfSeparateFragments ? Number.POSITIVE_INFINITY : 0);
|
|
57
|
+
const steps = deck.config.pdfSeparateFragments ? total + 1 : 1;
|
|
58
|
+
for (let c = 0; c < steps; c++) pages.push({
|
|
59
|
+
key: `${k}:${c}`,
|
|
60
|
+
h: s.h,
|
|
61
|
+
v: s.v,
|
|
62
|
+
click: deck.config.pdfSeparateFragments ? c : totals[k] ?? 0
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
66
|
+
className: "deck-print-pages",
|
|
67
|
+
"data-ready": ready || void 0,
|
|
68
|
+
style: {
|
|
69
|
+
width: deck.config.width,
|
|
70
|
+
zoom
|
|
71
|
+
},
|
|
72
|
+
children: [pages.map((p) => {
|
|
73
|
+
const meta = deck.columns[p.h].slides[p.v];
|
|
74
|
+
const options = deck.options.get(key(p.h, p.v));
|
|
75
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
76
|
+
className: "deck-print-page",
|
|
77
|
+
style: {
|
|
78
|
+
width: deck.config.width,
|
|
79
|
+
height: deck.config.height
|
|
80
|
+
},
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx(BackgroundLayer, {
|
|
83
|
+
bg: options.background,
|
|
84
|
+
active: false
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ jsx(SlideFrame, {
|
|
87
|
+
meta,
|
|
88
|
+
options,
|
|
89
|
+
h: p.h,
|
|
90
|
+
v: p.v,
|
|
91
|
+
active: false,
|
|
92
|
+
click: p.click,
|
|
93
|
+
preview: true,
|
|
94
|
+
className: "is-print"
|
|
95
|
+
}),
|
|
96
|
+
deck.config.showNotes === true && meta.hasNotes ? /* @__PURE__ */ jsx(PrintNotes, { meta }) : null
|
|
97
|
+
]
|
|
98
|
+
}, p.key);
|
|
99
|
+
}), deck.config.showNotes === "separate-page" ? pages.filter((p) => p.click === 0 && deck.columns[p.h].slides[p.v].hasNotes).map((p) => /* @__PURE__ */ jsx("div", {
|
|
100
|
+
className: "deck-print-page deck-print-notes-page",
|
|
101
|
+
style: {
|
|
102
|
+
width: deck.config.width,
|
|
103
|
+
height: deck.config.height
|
|
104
|
+
},
|
|
105
|
+
children: /* @__PURE__ */ jsx(PrintNotes, {
|
|
106
|
+
meta: deck.columns[p.h].slides[p.v],
|
|
107
|
+
full: true
|
|
108
|
+
})
|
|
109
|
+
}, `notes:${p.key}`)) : null]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function PrintNotes({ meta, full = false }) {
|
|
113
|
+
const [Notes, setNotes] = useState(null);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
let alive = true;
|
|
116
|
+
meta.loadNotes?.().then((m) => alive && setNotes(() => m.default));
|
|
117
|
+
return () => {
|
|
118
|
+
alive = false;
|
|
119
|
+
};
|
|
120
|
+
}, [meta]);
|
|
121
|
+
if (!Notes) return null;
|
|
122
|
+
return /* @__PURE__ */ jsxs("aside", {
|
|
123
|
+
className: ["deck-print-notes", full ? "is-full" : ""].join(" "),
|
|
124
|
+
children: [full && meta.title ? /* @__PURE__ */ jsx("h3", { children: meta.title }) : null, /* @__PURE__ */ jsx(Notes, {})]
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { PrintView };
|