@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,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,4 @@
1
+ import { ComponentType, ReactNode } from "react";
2
+ //#region src/react/mdx.d.ts
3
+ export declare function useMDXComponents(): Record<string, ComponentType<Record<string, unknown>>>;
4
+ //#endregion
@@ -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 };