@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,33 @@
|
|
|
1
|
+
import { DeckConfig, SlideMeta, SlideOptions } from "../core/model.js";
|
|
2
|
+
import { ClickRange, ClickSpec } from "../core/clicks.js";
|
|
3
|
+
import { Layout } from "../core/layout.js";
|
|
4
|
+
import { NavState } from "../core/navigation.js";
|
|
5
|
+
import { DeckActions, UiState } from "./context.js";
|
|
6
|
+
import "../index.js";
|
|
7
|
+
//#region src/react/hooks.d.ts
|
|
8
|
+
export declare function useNav<S = NavState>(selector?: (s: NavState) => S): S;
|
|
9
|
+
export declare function useUi<S = UiState>(selector?: (s: UiState) => S): S;
|
|
10
|
+
export declare function useLayout(): Layout;
|
|
11
|
+
export declare function useConfig(): DeckConfig;
|
|
12
|
+
export declare function useActions(): DeckActions;
|
|
13
|
+
/** The current slide's meta and options, or null when nothing is active yet. */
|
|
14
|
+
export declare function useCurrentSlide(): {
|
|
15
|
+
h: number;
|
|
16
|
+
v: number;
|
|
17
|
+
meta: SlideMeta;
|
|
18
|
+
options: SlideOptions | null;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Register a click step for the calling element and learn whether it is shown right now.
|
|
22
|
+
* Registration happens during render so document order is preserved; the range is stable across
|
|
23
|
+
* re-renders and Strict Mode's double render because it is keyed by `useId`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useClick(spec?: ClickSpec): {
|
|
26
|
+
range: ClickRange;
|
|
27
|
+
shown: boolean;
|
|
28
|
+
current: boolean;
|
|
29
|
+
click: number;
|
|
30
|
+
};
|
|
31
|
+
/** Read the current click without registering anything (a code block after its `<pre>` did). */
|
|
32
|
+
export declare function useClickIndex(): number;
|
|
33
|
+
//#endregion
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useStore } from "../core/store.js";
|
|
2
|
+
import { useDeck, useSlide, useSlideOptional } from "./context.js";
|
|
3
|
+
import { isCurrent, isShown } from "../core/clicks.js";
|
|
4
|
+
import { useId, useLayoutEffect, useMemo } from "react";
|
|
5
|
+
//#region src/react/hooks.ts
|
|
6
|
+
function useNav(selector) {
|
|
7
|
+
const { nav } = useDeck();
|
|
8
|
+
return useStore(nav, selector);
|
|
9
|
+
}
|
|
10
|
+
function useUi(selector) {
|
|
11
|
+
const { ui } = useDeck();
|
|
12
|
+
return useStore(ui, selector);
|
|
13
|
+
}
|
|
14
|
+
function useLayout() {
|
|
15
|
+
const { layout } = useDeck();
|
|
16
|
+
return useStore(layout);
|
|
17
|
+
}
|
|
18
|
+
function useConfig() {
|
|
19
|
+
return useDeck().config;
|
|
20
|
+
}
|
|
21
|
+
function useActions() {
|
|
22
|
+
return useDeck().actions;
|
|
23
|
+
}
|
|
24
|
+
/** The current slide's meta and options, or null when nothing is active yet. */
|
|
25
|
+
function useCurrentSlide() {
|
|
26
|
+
const { columns, options } = useDeck();
|
|
27
|
+
const { h, v } = useNav((s) => ({
|
|
28
|
+
h: s.h,
|
|
29
|
+
v: s.v
|
|
30
|
+
}));
|
|
31
|
+
const meta = columns[h]?.slides[v] ?? null;
|
|
32
|
+
return {
|
|
33
|
+
h,
|
|
34
|
+
v,
|
|
35
|
+
meta,
|
|
36
|
+
options: meta ? options.get(`${h},${v}`) ?? null : null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Register a click step for the calling element and learn whether it is shown right now.
|
|
41
|
+
* Registration happens during render so document order is preserved; the range is stable across
|
|
42
|
+
* re-renders and Strict Mode's double render because it is keyed by `useId`.
|
|
43
|
+
*/
|
|
44
|
+
function useClick(spec = {}) {
|
|
45
|
+
const slide = useSlide();
|
|
46
|
+
const id = useId();
|
|
47
|
+
const range = useMemo(() => slide.registry.allocate(id, spec), [
|
|
48
|
+
slide.registry,
|
|
49
|
+
id,
|
|
50
|
+
spec.at,
|
|
51
|
+
spec.hide,
|
|
52
|
+
spec.span
|
|
53
|
+
]);
|
|
54
|
+
useLayoutEffect(() => {
|
|
55
|
+
slide.registry.allocate(id, spec);
|
|
56
|
+
return () => slide.registry.release(id);
|
|
57
|
+
}, [slide.registry, id]);
|
|
58
|
+
return {
|
|
59
|
+
range,
|
|
60
|
+
shown: isShown(range, slide.click),
|
|
61
|
+
current: isCurrent(range, slide.click),
|
|
62
|
+
click: slide.click
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Read the current click without registering anything (a code block after its `<pre>` did). */
|
|
66
|
+
function useClickIndex() {
|
|
67
|
+
return useSlideOptional()?.click ?? 0;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { useActions, useClick, useClickIndex, useConfig, useCurrentSlide, useLayout, useNav, useUi };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Frontmatter } from "../compiler/split.js";
|
|
2
|
+
import { SlideOptions } from "../core/model.js";
|
|
3
|
+
import { ComponentType, ReactNode } from "react";
|
|
4
|
+
//#region src/react/layouts.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Layouts, the Slidev set, as React components a slide's frontmatter picks by name:
|
|
7
|
+
*
|
|
8
|
+
* default · center · cover · intro · section · end · fact · quote · statement · full · none ·
|
|
9
|
+
* two-cols · image · image-left · image-right · iframe · iframe-left · iframe-right
|
|
10
|
+
*
|
|
11
|
+
* Every layout wraps the slide's MDX in `.deck-layout` with a modifier class the theme styles;
|
|
12
|
+
* the image/iframe ones read `image` / `url` / `backgroundSize` from the frontmatter. A deck adds
|
|
13
|
+
* its own by passing `components={{ "layout:name": Component }}` to `<Deck>`.
|
|
14
|
+
*/
|
|
15
|
+
export interface LayoutProps {
|
|
16
|
+
frontmatter: Frontmatter;
|
|
17
|
+
options: SlideOptions;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export declare const LAYOUTS: Record<string, ComponentType<LayoutProps>>;
|
|
21
|
+
//#endregion
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useDeck } from "./context.js";
|
|
2
|
+
import { FrameShell } from "./Frame.js";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/react/layouts.tsx
|
|
5
|
+
function str(v) {
|
|
6
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
7
|
+
}
|
|
8
|
+
const simple = (name) => function Layout({ children }) {
|
|
9
|
+
return /* @__PURE__ */ jsx("div", {
|
|
10
|
+
className: `deck-layout deck-layout-${name}`,
|
|
11
|
+
children
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
function TwoCols({ children }) {
|
|
15
|
+
return /* @__PURE__ */ jsx("div", {
|
|
16
|
+
className: "deck-layout deck-layout-two-cols",
|
|
17
|
+
children
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function makeImage(side) {
|
|
21
|
+
return function ImageLayout({ frontmatter, children }) {
|
|
22
|
+
const image = str(frontmatter.image) ?? "";
|
|
23
|
+
const size = str(frontmatter.backgroundSize) ?? "cover";
|
|
24
|
+
const figure = /* @__PURE__ */ jsx("div", {
|
|
25
|
+
className: "deck-layout-image-pane",
|
|
26
|
+
style: {
|
|
27
|
+
backgroundImage: `url(${image})`,
|
|
28
|
+
backgroundSize: size
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
if (side === "full") return /* @__PURE__ */ jsxs("div", {
|
|
32
|
+
className: "deck-layout deck-layout-image",
|
|
33
|
+
children: [figure, /* @__PURE__ */ jsx("div", {
|
|
34
|
+
className: "deck-layout-image-content",
|
|
35
|
+
children
|
|
36
|
+
})]
|
|
37
|
+
});
|
|
38
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
39
|
+
className: `deck-layout deck-layout-image-${side}`,
|
|
40
|
+
children: [
|
|
41
|
+
side === "left" ? figure : null,
|
|
42
|
+
/* @__PURE__ */ jsx("div", {
|
|
43
|
+
className: "deck-layout-image-content",
|
|
44
|
+
children
|
|
45
|
+
}),
|
|
46
|
+
side === "right" ? figure : null
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function makeIframe(side) {
|
|
52
|
+
return function IframeLayout({ frontmatter, children }) {
|
|
53
|
+
const deck = useDeck();
|
|
54
|
+
const poster = str(frontmatter.poster) ?? void 0;
|
|
55
|
+
const posterSize = side === "full" ? [deck.config.width, deck.config.height] : [Math.round(deck.config.width / 2), deck.config.height];
|
|
56
|
+
const url = str(frontmatter.url) ?? "about:blank";
|
|
57
|
+
const frame = /* @__PURE__ */ jsx(FrameShell, {
|
|
58
|
+
url,
|
|
59
|
+
className: "deck-layout-iframe-pane",
|
|
60
|
+
poster,
|
|
61
|
+
posterSize
|
|
62
|
+
});
|
|
63
|
+
if (side === "full") return /* @__PURE__ */ jsx("div", {
|
|
64
|
+
className: "deck-layout deck-layout-iframe",
|
|
65
|
+
children: frame
|
|
66
|
+
});
|
|
67
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
68
|
+
className: `deck-layout deck-layout-iframe-${side}`,
|
|
69
|
+
children: [
|
|
70
|
+
side === "left" ? frame : null,
|
|
71
|
+
/* @__PURE__ */ jsx("div", {
|
|
72
|
+
className: "deck-layout-iframe-content",
|
|
73
|
+
children
|
|
74
|
+
}),
|
|
75
|
+
side === "right" ? frame : null
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const LAYOUTS = {
|
|
81
|
+
default: simple("default"),
|
|
82
|
+
center: simple("center"),
|
|
83
|
+
cover: simple("cover"),
|
|
84
|
+
intro: simple("intro"),
|
|
85
|
+
section: simple("section"),
|
|
86
|
+
end: simple("end"),
|
|
87
|
+
fact: simple("fact"),
|
|
88
|
+
quote: simple("quote"),
|
|
89
|
+
statement: simple("statement"),
|
|
90
|
+
full: simple("full"),
|
|
91
|
+
none: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
|
|
92
|
+
"two-cols": TwoCols,
|
|
93
|
+
image: makeImage("full"),
|
|
94
|
+
"image-left": makeImage("left"),
|
|
95
|
+
"image-right": makeImage("right"),
|
|
96
|
+
iframe: makeIframe("full"),
|
|
97
|
+
"iframe-left": makeIframe("left"),
|
|
98
|
+
"iframe-right": makeIframe("right")
|
|
99
|
+
};
|
|
100
|
+
function layoutFor(name, components) {
|
|
101
|
+
return components[`layout:`] ?? LAYOUTS[name] ?? LAYOUTS.default;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { LAYOUTS, layoutFor };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useDeck } from "./context.js";
|
|
2
|
+
import { Code } from "../components/Code.js";
|
|
3
|
+
import { Anchor, Image } from "../components/content.js";
|
|
4
|
+
import { components_exports } from "../components/index.js";
|
|
5
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/react/mdx.tsx
|
|
7
|
+
/**
|
|
8
|
+
* The components every slide sees without importing anything: the engine's own (`Click`, `Code`,
|
|
9
|
+
* `Cols`, `Poll`…), the deck's custom ones (passed to `<Deck components>`), and the Markdown
|
|
10
|
+
* element mappings — `pre` becomes the stepping code block, `img` the lightbox-aware image, `a`
|
|
11
|
+
* the slide-aware link. MDX's `providerImportSource` resolves to `useMDXComponents` below.
|
|
12
|
+
*/
|
|
13
|
+
let current = {};
|
|
14
|
+
function useMDXComponents() {
|
|
15
|
+
return current;
|
|
16
|
+
}
|
|
17
|
+
function MDXProvider({ children }) {
|
|
18
|
+
const custom = useDeck().components;
|
|
19
|
+
if (cachedFor !== custom) {
|
|
20
|
+
cachedFor = custom;
|
|
21
|
+
current = {
|
|
22
|
+
...components_exports,
|
|
23
|
+
pre: Code,
|
|
24
|
+
img: Image,
|
|
25
|
+
a: Anchor,
|
|
26
|
+
...Object.fromEntries(Object.entries(custom).filter(([k]) => !k.startsWith("layout:")))
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
30
|
+
}
|
|
31
|
+
let cachedFor = null;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { MDXProvider, useMDXComponents };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DeckRuntime, TermReveal } from "./context.js";
|
|
2
|
+
//#region src/react/terms.d.ts
|
|
3
|
+
export declare function openTerm(deck: DeckRuntime, reveal: TermReveal): void;
|
|
4
|
+
/** Close the card; with `only`, just when it is that kind (a relayed "off" spares a local card). */
|
|
5
|
+
export declare function closeTerm(deck: DeckRuntime, only?: TermReveal["from"]): void;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { termId } from "../glossary/model.js";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
//#region src/react/terms.ts
|
|
4
|
+
/**
|
|
5
|
+
* The window's one term card, as plain functions over `deck.terms`. A reveal names a mention —
|
|
6
|
+
* slide, term id, which occurrence — rather than carrying a definition or a position: every
|
|
7
|
+
* screen runs the same build, so each resolves the words from its own glossary and anchors the
|
|
8
|
+
* card to its own copy of the element. That is what lets the presenter's reveal travel through
|
|
9
|
+
* the room as three small values and land correctly on a projector and a phone alike.
|
|
10
|
+
*/
|
|
11
|
+
/** "h,v" of the slide an element sits in, or null outside one. */
|
|
12
|
+
function slideKeyOf(el) {
|
|
13
|
+
const slide = el.closest(".deck-slide");
|
|
14
|
+
if (!slide || slide.dataset.h === void 0) return null;
|
|
15
|
+
return `${slide.dataset.h},${slide.dataset.v ?? "0"}`;
|
|
16
|
+
}
|
|
17
|
+
/** Which of its slide's mentions of `id` this element is, in document order. */
|
|
18
|
+
function mentionIndex(el, id) {
|
|
19
|
+
const slide = el.closest(".deck-slide");
|
|
20
|
+
if (!slide) return 0;
|
|
21
|
+
const all = slide.querySelectorAll(`[data-term="${CSS.escape(id)}"]`);
|
|
22
|
+
return Math.max(0, Array.prototype.indexOf.call(all, el));
|
|
23
|
+
}
|
|
24
|
+
function entryFor(deck, id) {
|
|
25
|
+
return deck.glossary.get(id) ?? deck.glossary.get(termId(id));
|
|
26
|
+
}
|
|
27
|
+
function openTerm(deck, reveal) {
|
|
28
|
+
deck.terms.set(reveal);
|
|
29
|
+
}
|
|
30
|
+
/** Close the card; with `only`, just when it is that kind (a relayed "off" spares a local card). */
|
|
31
|
+
function closeTerm(deck, only) {
|
|
32
|
+
const now = deck.terms.get();
|
|
33
|
+
if (now && (!only || now.from === only)) deck.terms.set(null);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What dismisses a card besides its own term: moving through the deck, Escape, the overview, and
|
|
37
|
+
* a press anywhere that is not the card or a term. Escape is taken in the capture phase and kept
|
|
38
|
+
* from the deck's bindings, where it would open the overview.
|
|
39
|
+
*/
|
|
40
|
+
function useTermEffects(deck) {
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const offNav = deck.nav.subscribe((s, prev) => {
|
|
43
|
+
if (s.h !== prev.h || s.v !== prev.v || s.click !== prev.click || s.overview !== prev.overview) deck.terms.set(null);
|
|
44
|
+
});
|
|
45
|
+
const onKey = (e) => {
|
|
46
|
+
if (e.key !== "Escape" || !deck.terms.get()) return;
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
e.stopImmediatePropagation();
|
|
49
|
+
deck.terms.set(null);
|
|
50
|
+
};
|
|
51
|
+
const onPress = (e) => {
|
|
52
|
+
if (!deck.terms.get()) return;
|
|
53
|
+
if (e.target?.closest?.(".deck-term, .deck-term-card, .deck-term-readout, .deck-hints")) return;
|
|
54
|
+
deck.terms.set(null);
|
|
55
|
+
};
|
|
56
|
+
window.addEventListener("keydown", onKey, true);
|
|
57
|
+
document.addEventListener("pointerdown", onPress, true);
|
|
58
|
+
return () => {
|
|
59
|
+
offNav();
|
|
60
|
+
window.removeEventListener("keydown", onKey, true);
|
|
61
|
+
document.removeEventListener("pointerdown", onPress, true);
|
|
62
|
+
};
|
|
63
|
+
}, [deck]);
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { closeTerm, entryFor, mentionIndex, openTerm, slideKeyOf, useTermEffects };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { IslandComponent, IslandProps } from "./contract.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/renderers/Island.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* When an island comes alive.
|
|
6
|
+
*
|
|
7
|
+
* slide when its slide is the one being shown, and torn down when it is left (the default)
|
|
8
|
+
* load as soon as it is rendered
|
|
9
|
+
* visible when it scrolls into view — for the scroll view and long slides
|
|
10
|
+
* idle when the browser next has nothing better to do
|
|
11
|
+
* none never: the static fallback is the whole story
|
|
12
|
+
*
|
|
13
|
+
* `slide` is the default and is the one a deck actually wants. Astro's strategies all assume a page
|
|
14
|
+
* you scroll; a deck is twenty pages deep where nineteen are off-screen but mounted — the engine
|
|
15
|
+
* keeps neighbours ready so transitions do not stall. Hydrating all of them is how a phone runs out
|
|
16
|
+
* of memory on slide three. This way a live map exists for exactly as long as it is being looked at.
|
|
17
|
+
*/
|
|
18
|
+
export type ClientStrategy = "slide" | "load" | "visible" | "idle" | "none";
|
|
19
|
+
/** A dynamic import of a component, in whatever shape the module exports it. */
|
|
20
|
+
export type IslandLoader = () => Promise<IslandComponent | {
|
|
21
|
+
default: IslandComponent;
|
|
22
|
+
}>;
|
|
23
|
+
export interface IslandOwnProps {
|
|
24
|
+
/** The component, in whatever framework wrote it. Omit when passing `load`. */
|
|
25
|
+
component?: IslandComponent;
|
|
26
|
+
/**
|
|
27
|
+
* Fetch the component when the island arms, instead of importing it with the slide.
|
|
28
|
+
*
|
|
29
|
+
* <Island framework="vue" load={() => import("./chart.ts").then((m) => m.Chart)} />
|
|
30
|
+
*
|
|
31
|
+
* This is the half of islands that is about bytes rather than lifetime: with `load`, neither the
|
|
32
|
+
* component nor — paired with `registerLazyRenderer` — its framework's runtime is in the deck's
|
|
33
|
+
* first chunk. Read once, when the island first arms; the component it names is the island's
|
|
34
|
+
* identity for as long as the island lives.
|
|
35
|
+
*/
|
|
36
|
+
load?: IslandLoader;
|
|
37
|
+
/** Name the renderer explicitly. Otherwise the build's stamp decides, then `owns()`. */
|
|
38
|
+
framework?: string;
|
|
39
|
+
client?: ClientStrategy;
|
|
40
|
+
/** Passed through to the component. Compared shallowly: new identities alone do not remount. */
|
|
41
|
+
props?: IslandProps;
|
|
42
|
+
/** Shown before the island is alive, and instead of it where it never will be. */
|
|
43
|
+
fallback?: ReactNode;
|
|
44
|
+
className?: string;
|
|
45
|
+
/** Rendered to HTML and handed over as `context.slot`. */
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One piece of another framework, living inside a slide.
|
|
50
|
+
*
|
|
51
|
+
* The React case is deliberately NOT an island: when the resolved renderer is the one the deck
|
|
52
|
+
* runtime draws with, the component is rendered straight into the tree. An island would work, but
|
|
53
|
+
* it would open a second React root, and everything the deck puts in context — the click state, the
|
|
54
|
+
* theme, the live room, the glossary — stops at a root boundary. Native stays native.
|
|
55
|
+
*/
|
|
56
|
+
export declare function Island({ component, load, framework, client, props, fallback, className, children }: IslandOwnProps): import("react").JSX.Element;
|
|
57
|
+
//#endregion
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { useDeck, useSlideOptional } from "../react/context.js";
|
|
2
|
+
import { resolveRenderer, resolveRendererAsync } from "./contract.js";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/renderers/Island.tsx
|
|
6
|
+
function shallowEqual(a, b) {
|
|
7
|
+
const ak = Object.keys(a);
|
|
8
|
+
if (ak.length !== Object.keys(b).length) return false;
|
|
9
|
+
return ak.every((k) => Object.is(a[k], b[k]));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A thunk may resolve to a module or to a component. For every framework whose component is a
|
|
13
|
+
* value, the module's `default` IS the component, so that is what is taken.
|
|
14
|
+
*
|
|
15
|
+
* The exception worth naming: a module whose `default` is an *initialiser* rather than a component
|
|
16
|
+
* — `wasm-bindgen --target web` emits exactly that. Unwrapping one yields the init function and the
|
|
17
|
+
* failure is silent, so such a module is adapted by its own renderer before it gets here. See
|
|
18
|
+
* `wasmBindgenModule` in `@kadal/deck-wasm`.
|
|
19
|
+
*/
|
|
20
|
+
function unwrap(module) {
|
|
21
|
+
return module && typeof module === "object" && "default" in module ? module.default : module;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One piece of another framework, living inside a slide.
|
|
25
|
+
*
|
|
26
|
+
* The React case is deliberately NOT an island: when the resolved renderer is the one the deck
|
|
27
|
+
* runtime draws with, the component is rendered straight into the tree. An island would work, but
|
|
28
|
+
* it would open a second React root, and everything the deck puts in context — the click state, the
|
|
29
|
+
* theme, the live room, the glossary — stops at a root boundary. Native stays native.
|
|
30
|
+
*/
|
|
31
|
+
function Island({ component, load, framework, client = "slide", props = {}, fallback = null, className, children }) {
|
|
32
|
+
const deck = useDeck();
|
|
33
|
+
const slide = useSlideOptional();
|
|
34
|
+
const host = useRef(null);
|
|
35
|
+
const slotHost = useRef(null);
|
|
36
|
+
const mounted = useRef(null);
|
|
37
|
+
const lastProps = useRef(props);
|
|
38
|
+
const loader = useRef(load);
|
|
39
|
+
loader.current = load;
|
|
40
|
+
const printing = deck.mode === "print";
|
|
41
|
+
const slideWants = printing || (slide?.active ?? true);
|
|
42
|
+
const [armed, setArmed] = useState(() => client === "load" || client === "slide" && slideWants);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (client === "none") return;
|
|
45
|
+
if (client === "load") return setArmed(true);
|
|
46
|
+
if (client === "slide") return setArmed(slideWants);
|
|
47
|
+
if (client === "idle") {
|
|
48
|
+
const hasIdle = typeof window.requestIdleCallback === "function";
|
|
49
|
+
const id = hasIdle ? window.requestIdleCallback(() => setArmed(true)) : window.setTimeout(() => setArmed(true), 200);
|
|
50
|
+
return () => {
|
|
51
|
+
if (hasIdle) window.cancelIdleCallback?.(id);
|
|
52
|
+
else clearTimeout(id);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const el = host.current;
|
|
56
|
+
if (!el) return;
|
|
57
|
+
if (typeof IntersectionObserver === "undefined") return setArmed(true);
|
|
58
|
+
const io = new IntersectionObserver((entries) => entries.some((e) => e.isIntersecting) && setArmed(true), { rootMargin: "200px" });
|
|
59
|
+
io.observe(el);
|
|
60
|
+
return () => io.disconnect();
|
|
61
|
+
}, [client, slideWants]);
|
|
62
|
+
const [fetched, setFetched] = useState(null);
|
|
63
|
+
const target = component ?? fetched?.component;
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (component !== void 0 || !armed) return;
|
|
66
|
+
const fetch = loader.current;
|
|
67
|
+
if (!fetch) return;
|
|
68
|
+
let live = true;
|
|
69
|
+
fetch().then((module) => live && setFetched({ component: unwrap(module) })).catch((error) => {
|
|
70
|
+
console.error("[deck] an island's component failed to load:", error);
|
|
71
|
+
});
|
|
72
|
+
return () => {
|
|
73
|
+
live = false;
|
|
74
|
+
};
|
|
75
|
+
}, [component, armed]);
|
|
76
|
+
const immediate = useMemo(() => target === void 0 ? null : resolveRenderer(target, framework), [target, framework]);
|
|
77
|
+
const [arrived, setArrived] = useState(null);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!immediate || immediate.how !== "deferred" || !armed) return;
|
|
80
|
+
let live = true;
|
|
81
|
+
resolveRendererAsync(target, framework).then((r) => live && setArrived(r));
|
|
82
|
+
return () => {
|
|
83
|
+
live = false;
|
|
84
|
+
};
|
|
85
|
+
}, [
|
|
86
|
+
immediate,
|
|
87
|
+
armed,
|
|
88
|
+
target,
|
|
89
|
+
framework
|
|
90
|
+
]);
|
|
91
|
+
const resolution = immediate?.how === "deferred" && arrived?.renderer ? arrived : immediate ?? null;
|
|
92
|
+
const renderer = resolution?.renderer ?? null;
|
|
93
|
+
const native = renderer?.native ?? false;
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (native || !renderer || !armed || target === void 0) return;
|
|
96
|
+
const el = host.current;
|
|
97
|
+
if (!el) return;
|
|
98
|
+
const context = {
|
|
99
|
+
slot: slotHost.current?.innerHTML || void 0,
|
|
100
|
+
scheme: document.documentElement.dataset.colorScheme === "dark" ? "dark" : "light",
|
|
101
|
+
animate: !printing && !(slide?.preview ?? false)
|
|
102
|
+
};
|
|
103
|
+
let live = null;
|
|
104
|
+
try {
|
|
105
|
+
live = renderer.mount(el, target, lastProps.current, context);
|
|
106
|
+
mounted.current = live;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(`[deck] island “${renderer.name}” failed to mount:`, error);
|
|
109
|
+
el.dataset.islandError = error instanceof Error ? error.message : String(error);
|
|
110
|
+
}
|
|
111
|
+
return () => {
|
|
112
|
+
mounted.current = null;
|
|
113
|
+
try {
|
|
114
|
+
live?.destroy();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`[deck] island “${renderer.name}” failed to tear down:`, error);
|
|
117
|
+
}
|
|
118
|
+
el.replaceChildren();
|
|
119
|
+
delete el.dataset.islandError;
|
|
120
|
+
};
|
|
121
|
+
}, [
|
|
122
|
+
native,
|
|
123
|
+
renderer,
|
|
124
|
+
armed,
|
|
125
|
+
target,
|
|
126
|
+
printing,
|
|
127
|
+
slide?.preview
|
|
128
|
+
]);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (shallowEqual(lastProps.current, props)) return;
|
|
131
|
+
lastProps.current = props;
|
|
132
|
+
const live = mounted.current;
|
|
133
|
+
if (!live) return;
|
|
134
|
+
if (live.update) {
|
|
135
|
+
try {
|
|
136
|
+
live.update(props);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error("[deck] island refused new props:", error);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const el = host.current;
|
|
143
|
+
if (!el || !renderer || target === void 0) return;
|
|
144
|
+
try {
|
|
145
|
+
live.destroy();
|
|
146
|
+
el.replaceChildren();
|
|
147
|
+
mounted.current = renderer.mount(el, target, props, {
|
|
148
|
+
slot: slotHost.current?.innerHTML || void 0,
|
|
149
|
+
animate: !printing
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error("[deck] island failed to remount with new props:", error);
|
|
153
|
+
}
|
|
154
|
+
}, [
|
|
155
|
+
props,
|
|
156
|
+
renderer,
|
|
157
|
+
target,
|
|
158
|
+
printing
|
|
159
|
+
]);
|
|
160
|
+
if (component === void 0 && !load) return /* @__PURE__ */ jsx("div", {
|
|
161
|
+
className: [
|
|
162
|
+
"deck-island",
|
|
163
|
+
"is-unresolved",
|
|
164
|
+
className
|
|
165
|
+
].filter(Boolean).join(" "),
|
|
166
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
167
|
+
className: "deck-island-error",
|
|
168
|
+
children: "Island: pass a component, or load to fetch one"
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
if (resolution?.how === "unknown") return /* @__PURE__ */ jsx("div", {
|
|
172
|
+
className: [
|
|
173
|
+
"deck-island",
|
|
174
|
+
"is-unresolved",
|
|
175
|
+
className
|
|
176
|
+
].filter(Boolean).join(" "),
|
|
177
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
178
|
+
className: "deck-island-error",
|
|
179
|
+
children: ["Island: ", resolution.reason]
|
|
180
|
+
})
|
|
181
|
+
});
|
|
182
|
+
if (native) {
|
|
183
|
+
const Component = target;
|
|
184
|
+
return /* @__PURE__ */ jsx("div", {
|
|
185
|
+
className: [
|
|
186
|
+
"deck-island",
|
|
187
|
+
"is-native",
|
|
188
|
+
className
|
|
189
|
+
].filter(Boolean).join(" "),
|
|
190
|
+
children: /* @__PURE__ */ jsx(Component, {
|
|
191
|
+
...props,
|
|
192
|
+
children
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const pending = renderer === null;
|
|
197
|
+
const name = renderer?.name ?? (resolution?.how === "deferred" ? resolution.name : framework);
|
|
198
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
199
|
+
className: [
|
|
200
|
+
"deck-island",
|
|
201
|
+
name ? `is-${name}` : null,
|
|
202
|
+
pending ? "is-pending" : armed ? "is-live" : "is-waiting",
|
|
203
|
+
className
|
|
204
|
+
].filter(Boolean).join(" "),
|
|
205
|
+
"data-framework": name,
|
|
206
|
+
"data-client": client,
|
|
207
|
+
children: [
|
|
208
|
+
/* @__PURE__ */ jsx("div", {
|
|
209
|
+
className: "deck-island-slot",
|
|
210
|
+
hidden: true,
|
|
211
|
+
ref: slotHost,
|
|
212
|
+
children
|
|
213
|
+
}),
|
|
214
|
+
/* @__PURE__ */ jsx("div", {
|
|
215
|
+
className: "deck-island-host",
|
|
216
|
+
ref: host
|
|
217
|
+
}),
|
|
218
|
+
armed && !pending ? null : fallback
|
|
219
|
+
]
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { Island };
|