@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
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DEFAULT_GLOSSARY, GlossaryEntry, GlossaryOptions, TermMatcher, buildMatcher, inlineHtml, termId } from "./glossary/model.js";
|
|
2
|
+
import { Column, DEFAULT_CONFIG, DeckConfig, DeckConfigInput, DeckManifest, HostOverride, SlideBackground, SlideMeta, SlideOptions, Transition, columnsOf, readHostOverride, resolveBackground, resolveConfig, resolveSlideOptions, roomIdOf, withHostOverride } from "./core/model.js";
|
|
3
|
+
import { Direction, NavState, Position, navigation_d_exports } from "./core/navigation.js";
|
|
4
|
+
import { Store, createStore, useStore } from "./core/store.js";
|
|
5
|
+
import { DeckActions, DeckContext, DeckRuntime, SlideContext, SlideContextValue, TermReveal, UiState, ViewMode, useDeck, useSlide, useSlideOptional } from "./react/context.js";
|
|
6
|
+
import { Deck, DeckProps } from "./react/Deck.js";
|
|
7
|
+
import { DECK_RENDERER, DeckRenderer, IslandMount, IslandProps, RenderContext, RendererLoader, loadRenderer, registerLazyRenderer, registerRenderer, resolveRenderer, resolveRendererAsync } from "./renderers/contract.js";
|
|
8
|
+
import { ClientStrategy, Island, IslandLoader } from "./renderers/Island.js";
|
|
9
|
+
import { registerBuiltinRenderers } from "./renderers/index.js";
|
|
10
|
+
import { useMDXComponents } from "./react/mdx.js";
|
|
11
|
+
import { useActions, useClick, useClickIndex, useConfig, useCurrentSlide, useLayout, useNav, useUi } from "./react/hooks.js";
|
|
12
|
+
import { SlideFrame, loadSlide } from "./react/SlideFrame.js";
|
|
13
|
+
import { BackgroundLayer, backgroundComponent, registerBackground } from "./react/Backgrounds.js";
|
|
14
|
+
import { LAYOUTS, LayoutProps } from "./react/layouts.js";
|
|
15
|
+
import { After, Click, Clicks, Fragment, Li, Ol, Ul } from "./components/Click.js";
|
|
16
|
+
import { Code, MagicMove } from "./components/Code.js";
|
|
17
|
+
import { Absolute, Anchor, Arrow, Audio, Badge, Callout, Center, Col, Cols, Confetti, Counter, Encrypted, FitText, Frame, Grid, Iframe, Image, Kbd, Link, Mark, Mermaid, Notes, Qr, Spacer, Stack, Stretch, Tex, Timer, Toc, Transform, Tweet, Typewriter, Video, Youtube } from "./components/content.js";
|
|
18
|
+
import { Motion } from "./components/Motion.js";
|
|
19
|
+
import { Term, TermProps, TermReadout, Terms } from "./components/Term.js";
|
|
20
|
+
import { Sketch, SketchProps, SketchStep } from "./components/Sketch.js";
|
|
21
|
+
import { Background } from "./components/Background.js";
|
|
22
|
+
import { Leaderboard, Pace, Poll, Presence, QrJoin, Questions, Quiz, Reactions, WordCloud } from "./live/components.js";
|
|
23
|
+
import { Blob, Chip, Divider, Glass, Glow, GradientText, Halo, Icon, Ribbon, Shimmer, Tilt } from "./components/bling.js";
|
|
24
|
+
import "./components/index.js";
|
|
25
|
+
import { closeTerm, openTerm } from "./react/terms.js";
|
|
26
|
+
import { KEY_BINDINGS, KeyAction } from "./core/keyboard.js";
|
|
27
|
+
import { formatHash, parseFlags, parseHash } from "./core/hash.js";
|
|
28
|
+
import { HintLayer } from "./react/Hints.js";
|
|
29
|
+
import { ThemeMenu } from "./react/ThemeMenu.js";
|
|
30
|
+
import { FrameShell } from "./react/Frame.js";
|
|
31
|
+
export { Absolute, After, Anchor, Arrow, Audio, Background, BackgroundLayer, Badge, Blob, Callout, Center, Chip, Click, Clicks, type ClientStrategy, Code, Col, Cols, type Column, Confetti, Counter, DECK_RENDERER, DEFAULT_CONFIG, DEFAULT_GLOSSARY, Deck, type DeckActions, type DeckConfig, type DeckConfigInput, DeckContext, type DeckManifest, type DeckProps, type DeckRenderer, type DeckRuntime, type Direction, Divider, Encrypted, FitText, Fragment, Frame, FrameShell, Glass, type GlossaryEntry, type GlossaryOptions, Glow, GradientText, Grid, Halo, HintLayer, type HostOverride, Icon, Iframe, Image, Island, type IslandLoader, type IslandMount, type IslandProps, KEY_BINDINGS, Kbd, type KeyAction, LAYOUTS, type LayoutProps, Leaderboard, Li, Link, MagicMove, Mark, Tex as Math, Mermaid, Motion, type NavState, Notes, Ol, Pace, Poll, type Position, Presence, Qr, QrJoin, Questions, Quiz, Reactions, type RenderContext, type RendererLoader, Ribbon, Shimmer, Sketch, type SketchProps, type SketchStep, type SlideBackground, SlideContext, type SlideContextValue, SlideFrame, type SlideMeta, type SlideOptions, Spacer, Stack, type Store, Stretch, Term, type TermMatcher, type TermProps, TermReadout, type TermReveal, Terms, Tex, ThemeMenu, Tilt, Timer, Toc, Transform, type Transition, Tweet, Typewriter, type UiState, Ul, Video, type ViewMode, WordCloud, Youtube, backgroundComponent, buildMatcher, closeTerm, columnsOf, createStore, formatHash, inlineHtml, Li as li, loadRenderer, loadSlide, navigation_d_exports as navigation, Ol as ol, openTerm, parseFlags, parseHash, readHostOverride, registerBackground, registerBuiltinRenderers, registerLazyRenderer, registerRenderer, resolveBackground, resolveConfig, resolveRenderer, resolveRendererAsync, resolveSlideOptions, roomIdOf, termId, Ul as ul, useActions, useClick, useClickIndex, useConfig, useCurrentSlide, useDeck, useLayout, useMDXComponents, useNav, useSlide, useSlideOptional, useStore, useUi, withHostOverride };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DEFAULT_GLOSSARY, buildMatcher, inlineHtml, termId } from "./glossary/model.js";
|
|
2
|
+
import { DEFAULT_CONFIG, columnsOf, readHostOverride, resolveBackground, resolveConfig, resolveSlideOptions, roomIdOf, withHostOverride } from "./core/model.js";
|
|
3
|
+
import { formatHash, parseFlags, parseHash } from "./core/hash.js";
|
|
4
|
+
import { KEY_BINDINGS } from "./core/keyboard.js";
|
|
5
|
+
import { navigation_exports } from "./core/navigation.js";
|
|
6
|
+
import { createStore, useStore } from "./core/store.js";
|
|
7
|
+
import { DeckContext, SlideContext, useDeck, useSlide, useSlideOptional } from "./react/context.js";
|
|
8
|
+
import { useActions, useClick, useClickIndex, useConfig, useCurrentSlide, useLayout, useNav, useUi } from "./react/hooks.js";
|
|
9
|
+
import { BackgroundLayer, backgroundComponent, registerBackground } from "./react/Backgrounds.js";
|
|
10
|
+
import { HintLayer } from "./react/Hints.js";
|
|
11
|
+
import { ThemeMenu } from "./react/ThemeMenu.js";
|
|
12
|
+
import { FrameShell } from "./react/Frame.js";
|
|
13
|
+
import { LAYOUTS } from "./react/layouts.js";
|
|
14
|
+
import { After, Click, Clicks, Fragment, Li, Ol, Ul } from "./components/Click.js";
|
|
15
|
+
import { Code, MagicMove } from "./components/Code.js";
|
|
16
|
+
import { Absolute, Anchor, Arrow, Audio, Badge, Callout, Center, Col, Cols, Confetti, Counter, Encrypted, FitText, Frame, Grid, Iframe, Image, Kbd, Link, Mark, Mermaid, Notes, Qr, Spacer, Stack, Stretch, Tex, Timer, Toc, Transform, Tweet, Typewriter, Video, Youtube } from "./components/content.js";
|
|
17
|
+
import { Motion } from "./components/Motion.js";
|
|
18
|
+
import { closeTerm, openTerm } from "./react/terms.js";
|
|
19
|
+
import { Blob, Chip, Divider, Glass, Glow, GradientText, Halo, Icon, Ribbon, Shimmer, Tilt } from "./components/bling.js";
|
|
20
|
+
import { Term, TermReadout, Terms } from "./components/Term.js";
|
|
21
|
+
import { Sketch } from "./components/Sketch.js";
|
|
22
|
+
import { Background } from "./components/Background.js";
|
|
23
|
+
import { Leaderboard, Pace, Poll, Presence, QrJoin, Questions, Quiz, Reactions, WordCloud } from "./live/components.js";
|
|
24
|
+
import { useMDXComponents } from "./react/mdx.js";
|
|
25
|
+
import { SlideFrame, loadSlide } from "./react/SlideFrame.js";
|
|
26
|
+
import { Deck } from "./react/Deck.js";
|
|
27
|
+
import { DECK_RENDERER, loadRenderer, registerLazyRenderer, registerRenderer, resolveRenderer, resolveRendererAsync } from "./renderers/contract.js";
|
|
28
|
+
import { Island } from "./renderers/Island.js";
|
|
29
|
+
import { registerBuiltinRenderers } from "./renderers/index.js";
|
|
30
|
+
export { Absolute, After, Anchor, Arrow, Audio, Background, BackgroundLayer, Badge, Blob, Callout, Center, Chip, Click, Clicks, Code, Col, Cols, Confetti, Counter, DECK_RENDERER, DEFAULT_CONFIG, DEFAULT_GLOSSARY, Deck, DeckContext, Divider, Encrypted, FitText, Fragment, Frame, FrameShell, Glass, Glow, GradientText, Grid, Halo, HintLayer, Icon, Iframe, Image, Island, KEY_BINDINGS, Kbd, LAYOUTS, Leaderboard, Li, Link, MagicMove, Mark, Tex as Math, Mermaid, Motion, Notes, Ol, Pace, Poll, Presence, Qr, QrJoin, Questions, Quiz, Reactions, Ribbon, Shimmer, Sketch, SlideContext, SlideFrame, Spacer, Stack, Stretch, Term, TermReadout, Terms, Tex, ThemeMenu, Tilt, Timer, Toc, Transform, Tweet, Typewriter, Ul, Video, WordCloud, Youtube, backgroundComponent, buildMatcher, closeTerm, columnsOf, createStore, formatHash, inlineHtml, Li as li, loadRenderer, loadSlide, navigation_exports as navigation, Ol as ol, openTerm, parseFlags, parseHash, readHostOverride, registerBackground, registerBuiltinRenderers, registerLazyRenderer, registerRenderer, resolveBackground, resolveConfig, resolveRenderer, resolveRendererAsync, resolveSlideOptions, roomIdOf, termId, Ul as ul, useActions, useClick, useClickIndex, useConfig, useCurrentSlide, useDeck, useLayout, useMDXComponents, useNav, useSlide, useSlideOptional, useStore, useUi, withHostOverride };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "../react/context.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/live/LiveProvider.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Put `<LiveProvider />` inside `<Deck>` and the deck joins its room: the presenter window pushes
|
|
6
|
+
* its position, pointer and drawings; the play window and the audience follow (or explore, then
|
|
7
|
+
* catch up); reactions float up on every screen; questions and polls flow both ways. Without a
|
|
8
|
+
* backend the provider stays quiet and the deck works exactly as before.
|
|
9
|
+
*/
|
|
10
|
+
export declare function LiveProvider({ children }: {
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/**
|
|
14
|
+
* The laser pointer and spotlight: published by the presenter, drawn by everyone. `inCanvas`
|
|
15
|
+
* renders it inside a scaled stage canvas (the presenter's current-slide box) in stage pixels.
|
|
16
|
+
*/
|
|
17
|
+
export declare function PointerLayer({ inCanvas }?: {
|
|
18
|
+
inCanvas?: boolean;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { roomIdOf } from "../core/model.js";
|
|
2
|
+
import { formatSlideNumber, goto, key } from "../core/navigation.js";
|
|
3
|
+
import { useStore } from "../core/store.js";
|
|
4
|
+
import { useDeck } from "../react/context.js";
|
|
5
|
+
import { useActions, useLayout, useNav, useUi } from "../react/hooks.js";
|
|
6
|
+
import { closeTerm, openTerm } from "../react/terms.js";
|
|
7
|
+
import { Icon } from "../components/bling.js";
|
|
8
|
+
import { LiveContext, useLiveOptional } from "./context.js";
|
|
9
|
+
import { DrawingLayer } from "../draw/DrawingLayer.js";
|
|
10
|
+
import { RoomClient, viewerId } from "./client.js";
|
|
11
|
+
import { Analytics } from "./analytics.js";
|
|
12
|
+
import { turnstileToken } from "./turnstile.js";
|
|
13
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
14
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
//#region src/live/LiveProvider.tsx
|
|
16
|
+
/**
|
|
17
|
+
* Put `<LiveProvider />` inside `<Deck>` and the deck joins its room: the presenter window pushes
|
|
18
|
+
* its position, pointer and drawings; the play window and the audience follow (or explore, then
|
|
19
|
+
* catch up); reactions float up on every screen; questions and polls flow both ways. Without a
|
|
20
|
+
* backend the provider stays quiet and the deck works exactly as before.
|
|
21
|
+
*/
|
|
22
|
+
function LiveProvider({ children }) {
|
|
23
|
+
const deck = useDeck();
|
|
24
|
+
const api = deck.config.live.api;
|
|
25
|
+
const room = roomIdOf(deck.config);
|
|
26
|
+
const value = useMemo(() => {
|
|
27
|
+
if (!api || deck.mode === "print") return null;
|
|
28
|
+
const siteKey = deck.config.live.turnstile;
|
|
29
|
+
return {
|
|
30
|
+
client: new RoomClient({
|
|
31
|
+
api,
|
|
32
|
+
room,
|
|
33
|
+
role: deck.mode === "presenter" ? "presenter" : "viewer",
|
|
34
|
+
viewer: viewerId(),
|
|
35
|
+
token: siteKey && deck.mode !== "presenter" ? () => turnstileToken(siteKey) : void 0
|
|
36
|
+
}),
|
|
37
|
+
joinUrl: typeof window === "undefined" ? "" : `${window.location.origin}${deck.base}join`
|
|
38
|
+
};
|
|
39
|
+
}, [
|
|
40
|
+
api,
|
|
41
|
+
room,
|
|
42
|
+
deck.mode,
|
|
43
|
+
deck.base,
|
|
44
|
+
deck.config.live.turnstile
|
|
45
|
+
]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!value) return;
|
|
48
|
+
value.client.connect();
|
|
49
|
+
return () => value.client.close();
|
|
50
|
+
}, [value]);
|
|
51
|
+
if (!value) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
52
|
+
return /* @__PURE__ */ jsxs(LiveContext.Provider, {
|
|
53
|
+
value,
|
|
54
|
+
children: [
|
|
55
|
+
children,
|
|
56
|
+
/* @__PURE__ */ jsx(Analytics, {}),
|
|
57
|
+
/* @__PURE__ */ jsx(Wiring, {}),
|
|
58
|
+
deck.mode === "play" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
59
|
+
/* @__PURE__ */ jsx(DrawingLayer, {}),
|
|
60
|
+
/* @__PURE__ */ jsx(PointerLayer, {}),
|
|
61
|
+
/* @__PURE__ */ jsx(ReactionsOverlay, {}),
|
|
62
|
+
/* @__PURE__ */ jsx(FollowControl, {})
|
|
63
|
+
] }) : null,
|
|
64
|
+
deck.mode === "presenter" ? /* @__PURE__ */ jsx(ReactionsOverlay, {}) : null
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Nav in, nav out, presence, blackout. */
|
|
69
|
+
function Wiring() {
|
|
70
|
+
const deck = useDeck();
|
|
71
|
+
const live = useLiveOptional();
|
|
72
|
+
const role = useStore(live.client.role);
|
|
73
|
+
useNav((s) => s.follow);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
let timer = null;
|
|
76
|
+
let sentAt = 0;
|
|
77
|
+
let pending = null;
|
|
78
|
+
const flush = () => {
|
|
79
|
+
timer = null;
|
|
80
|
+
if (!pending) return;
|
|
81
|
+
sentAt = Date.now();
|
|
82
|
+
live.client.send({
|
|
83
|
+
type: "nav",
|
|
84
|
+
position: pending
|
|
85
|
+
});
|
|
86
|
+
pending = null;
|
|
87
|
+
};
|
|
88
|
+
const unsub = deck.nav.subscribe((s, prev) => {
|
|
89
|
+
live.client.position = {
|
|
90
|
+
h: s.h,
|
|
91
|
+
v: s.v,
|
|
92
|
+
click: s.click
|
|
93
|
+
};
|
|
94
|
+
if (s.h === prev.h && s.v === prev.v && s.click === prev.click) return;
|
|
95
|
+
if (deck.sync.source === "room") return;
|
|
96
|
+
if (role === "presenter") {
|
|
97
|
+
pending = {
|
|
98
|
+
h: s.h,
|
|
99
|
+
v: s.v,
|
|
100
|
+
click: s.click
|
|
101
|
+
};
|
|
102
|
+
const wait = 80 - (Date.now() - sentAt);
|
|
103
|
+
if (wait <= 0) flush();
|
|
104
|
+
else if (!timer) timer = setTimeout(flush, wait);
|
|
105
|
+
} else live.client.send({
|
|
106
|
+
type: "presence",
|
|
107
|
+
slide: key(s.h, s.v)
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
return () => {
|
|
111
|
+
unsub();
|
|
112
|
+
if (timer) clearTimeout(timer);
|
|
113
|
+
flush();
|
|
114
|
+
};
|
|
115
|
+
}, [
|
|
116
|
+
deck.nav,
|
|
117
|
+
deck.sync,
|
|
118
|
+
live.client,
|
|
119
|
+
role
|
|
120
|
+
]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (role === "presenter") return;
|
|
123
|
+
const apply = (m) => {
|
|
124
|
+
if (m.type === "welcome" || m.type === "nav") {
|
|
125
|
+
const p = m.type === "welcome" ? m.state.position : m.position;
|
|
126
|
+
const presenting = m.type === "nav" || m.state.presence.presenter;
|
|
127
|
+
deck.sync.source = "room";
|
|
128
|
+
try {
|
|
129
|
+
deck.nav.update((s) => {
|
|
130
|
+
const next = {
|
|
131
|
+
...s,
|
|
132
|
+
presenter: p
|
|
133
|
+
};
|
|
134
|
+
if (!s.follow || !presenting) return next;
|
|
135
|
+
return goto(next, deck.columns, p);
|
|
136
|
+
});
|
|
137
|
+
} finally {
|
|
138
|
+
deck.sync.source = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
return live.client.on(apply);
|
|
143
|
+
}, [
|
|
144
|
+
deck.nav,
|
|
145
|
+
deck.columns,
|
|
146
|
+
deck.sync,
|
|
147
|
+
live.client,
|
|
148
|
+
role
|
|
149
|
+
]);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (role !== "presenter") return;
|
|
152
|
+
return deck.nav.subscribe((s, prev) => {
|
|
153
|
+
if (s.paused !== prev.paused) live.client.send({
|
|
154
|
+
type: "blackout",
|
|
155
|
+
on: s.paused
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}, [
|
|
159
|
+
deck.nav,
|
|
160
|
+
live.client,
|
|
161
|
+
role
|
|
162
|
+
]);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (role === "presenter") return;
|
|
165
|
+
const apply = (blackout) => {
|
|
166
|
+
deck.sync.source = "room";
|
|
167
|
+
try {
|
|
168
|
+
deck.nav.update((n) => n.paused === blackout ? n : {
|
|
169
|
+
...n,
|
|
170
|
+
paused: blackout
|
|
171
|
+
});
|
|
172
|
+
} finally {
|
|
173
|
+
deck.sync.source = null;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
if (live.client.state.get().blackout) apply(true);
|
|
177
|
+
return live.client.state.subscribe((s, prev) => {
|
|
178
|
+
if (s.blackout !== prev.blackout) apply(s.blackout);
|
|
179
|
+
});
|
|
180
|
+
}, [
|
|
181
|
+
deck.nav,
|
|
182
|
+
deck.sync,
|
|
183
|
+
live.client,
|
|
184
|
+
role
|
|
185
|
+
]);
|
|
186
|
+
/**
|
|
187
|
+
* The spotlight, the same way the blackout travels.
|
|
188
|
+
*
|
|
189
|
+
* The laser's POSITION has always been published; the darkness around it never was — it was a
|
|
190
|
+
* local flag on whichever window pressed the button, so the presenter dimmed their own screen
|
|
191
|
+
* and nobody else's. The room has carried a `spotlight` field and the message to set it since
|
|
192
|
+
* the protocol was written; this is the half that was missing at both ends.
|
|
193
|
+
*/
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (role !== "presenter") return;
|
|
196
|
+
return deck.ui.subscribe((s, prev) => {
|
|
197
|
+
if (s.spotlight !== prev.spotlight) live.client.send({
|
|
198
|
+
type: "spotlight",
|
|
199
|
+
on: s.spotlight
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}, [
|
|
203
|
+
deck.ui,
|
|
204
|
+
live.client,
|
|
205
|
+
role
|
|
206
|
+
]);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (role === "presenter") return;
|
|
209
|
+
const apply = (on) => deck.ui.update((u) => u.spotlight === on ? u : {
|
|
210
|
+
...u,
|
|
211
|
+
spotlight: on
|
|
212
|
+
});
|
|
213
|
+
apply(live.client.state.get().spotlight);
|
|
214
|
+
return live.client.state.subscribe((s, prev) => {
|
|
215
|
+
if (s.spotlight !== prev.spotlight) apply(s.spotlight);
|
|
216
|
+
});
|
|
217
|
+
}, [
|
|
218
|
+
deck.ui,
|
|
219
|
+
live.client,
|
|
220
|
+
role
|
|
221
|
+
]);
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (role !== "presenter") return;
|
|
224
|
+
let shared = false;
|
|
225
|
+
const push = () => {
|
|
226
|
+
const r = deck.terms.get();
|
|
227
|
+
if (r && r.from === "local" && deck.ui.get().termSync) {
|
|
228
|
+
live.client.send({
|
|
229
|
+
type: "term",
|
|
230
|
+
slide: r.slide,
|
|
231
|
+
id: r.id,
|
|
232
|
+
n: r.n,
|
|
233
|
+
view: r.view
|
|
234
|
+
});
|
|
235
|
+
shared = true;
|
|
236
|
+
} else if (shared) {
|
|
237
|
+
live.client.send({ type: "term:off" });
|
|
238
|
+
shared = false;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const offTerms = deck.terms.subscribe(push);
|
|
242
|
+
const offUi = deck.ui.subscribe((s, prev) => {
|
|
243
|
+
if (s.termSync !== prev.termSync) push();
|
|
244
|
+
});
|
|
245
|
+
return () => {
|
|
246
|
+
offTerms();
|
|
247
|
+
offUi();
|
|
248
|
+
if (shared) live.client.send({ type: "term:off" });
|
|
249
|
+
};
|
|
250
|
+
}, [
|
|
251
|
+
deck.terms,
|
|
252
|
+
deck.ui,
|
|
253
|
+
live.client,
|
|
254
|
+
role
|
|
255
|
+
]);
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (role === "presenter") return;
|
|
258
|
+
return live.client.on((m) => {
|
|
259
|
+
const t = m.type === "term" ? m : m.type === "welcome" ? m.state.term : null;
|
|
260
|
+
if (t) openTerm(deck, {
|
|
261
|
+
slide: t.slide,
|
|
262
|
+
id: t.id,
|
|
263
|
+
n: t.n,
|
|
264
|
+
view: t.view ?? null,
|
|
265
|
+
pinned: true,
|
|
266
|
+
from: "remote"
|
|
267
|
+
});
|
|
268
|
+
else if (m.type === "term:off") closeTerm(deck, "remote");
|
|
269
|
+
});
|
|
270
|
+
}, [
|
|
271
|
+
deck,
|
|
272
|
+
live.client,
|
|
273
|
+
role
|
|
274
|
+
]);
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
return live.client.on((m) => {
|
|
277
|
+
if (m.type === "confetti") window.dispatchEvent(new CustomEvent("deck:confetti"));
|
|
278
|
+
});
|
|
279
|
+
}, [live.client]);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The laser pointer and spotlight: published by the presenter, drawn by everyone. `inCanvas`
|
|
284
|
+
* renders it inside a scaled stage canvas (the presenter's current-slide box) in stage pixels.
|
|
285
|
+
*/
|
|
286
|
+
function PointerLayer({ inCanvas = false } = {}) {
|
|
287
|
+
const deck = useDeck();
|
|
288
|
+
const live = useLiveOptional();
|
|
289
|
+
const layout = useLayout();
|
|
290
|
+
const host = useRef(null);
|
|
291
|
+
const on = useUi((s) => s.pointer);
|
|
292
|
+
const spotlight = useUi((s) => s.spotlight);
|
|
293
|
+
const role = useStore(live.client.role);
|
|
294
|
+
const remote = useStore(live.client.state, (s) => s.pointer);
|
|
295
|
+
const follow = useNav((s) => s.follow);
|
|
296
|
+
const [local, setLocal] = useState(null);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (!on || role !== "presenter") {
|
|
299
|
+
if (role === "presenter") live.client.send({ type: "pointer:off" });
|
|
300
|
+
setLocal(null);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
let last = 0;
|
|
304
|
+
const onMove = (e) => {
|
|
305
|
+
const rect = host.current?.getBoundingClientRect();
|
|
306
|
+
const l = deck.layout.get();
|
|
307
|
+
const p = rect && rect.width > 0 ? {
|
|
308
|
+
x: (e.clientX - rect.left) / rect.width,
|
|
309
|
+
y: (e.clientY - rect.top) / rect.height
|
|
310
|
+
} : {
|
|
311
|
+
x: (e.clientX - l.left) / l.scale / deck.config.width,
|
|
312
|
+
y: (e.clientY - l.top) / l.scale / deck.config.height
|
|
313
|
+
};
|
|
314
|
+
setLocal(p);
|
|
315
|
+
const now = performance.now();
|
|
316
|
+
if (now - last > 33) {
|
|
317
|
+
last = now;
|
|
318
|
+
live.client.send({
|
|
319
|
+
type: "pointer",
|
|
320
|
+
x: p.x,
|
|
321
|
+
y: p.y
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
window.addEventListener("pointermove", onMove);
|
|
326
|
+
return () => window.removeEventListener("pointermove", onMove);
|
|
327
|
+
}, [
|
|
328
|
+
on,
|
|
329
|
+
role,
|
|
330
|
+
live.client,
|
|
331
|
+
deck.layout,
|
|
332
|
+
deck.config.width,
|
|
333
|
+
deck.config.height
|
|
334
|
+
]);
|
|
335
|
+
const p = role === "presenter" ? local : follow ? remote : null;
|
|
336
|
+
const frame = inCanvas ? {
|
|
337
|
+
left: 0,
|
|
338
|
+
top: 0,
|
|
339
|
+
width: deck.config.width,
|
|
340
|
+
height: deck.config.height
|
|
341
|
+
} : {
|
|
342
|
+
left: layout.left,
|
|
343
|
+
top: layout.top,
|
|
344
|
+
width: layout.width,
|
|
345
|
+
height: layout.height
|
|
346
|
+
};
|
|
347
|
+
const x = p ? p.x * frame.width : 0;
|
|
348
|
+
const y = p ? p.y * frame.height : 0;
|
|
349
|
+
const radius = inCanvas ? 140 / Math.max(.05, layout.scale || 1) : 140;
|
|
350
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
351
|
+
ref: host,
|
|
352
|
+
className: "deck-pointer-layer",
|
|
353
|
+
style: {
|
|
354
|
+
position: "absolute",
|
|
355
|
+
pointerEvents: "none",
|
|
356
|
+
...frame
|
|
357
|
+
},
|
|
358
|
+
children: [p && spotlight ? /* @__PURE__ */ jsx("div", {
|
|
359
|
+
className: "deck-spotlight",
|
|
360
|
+
style: { background: `radial-gradient(circle ${radius}px at ${x}px ${y}px, transparent 0, rgba(0,0,0,0.75) ${radius + 20}px)` }
|
|
361
|
+
}) : null, p ? /* @__PURE__ */ jsx("div", {
|
|
362
|
+
className: "deck-pointer",
|
|
363
|
+
style: { transform: `translate(${x}px, ${y}px)${inCanvas ? " scale(2.5)" : ""}` }
|
|
364
|
+
}) : null]
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/** Reactions float up from the bottom on every window as they arrive. */
|
|
368
|
+
function ReactionsOverlay() {
|
|
369
|
+
const live = useLiveOptional();
|
|
370
|
+
const [items, setItems] = useState([]);
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
let n = 0;
|
|
373
|
+
return live.client.on((m) => {
|
|
374
|
+
if (m.type !== "react") return;
|
|
375
|
+
const count = Math.min(6, m.n);
|
|
376
|
+
const batch = Array.from({ length: count }, () => ({
|
|
377
|
+
id: n++,
|
|
378
|
+
emoji: m.emoji,
|
|
379
|
+
x: 10 + Math.random() * 80
|
|
380
|
+
}));
|
|
381
|
+
setItems((prev) => [...prev.slice(-40), ...batch]);
|
|
382
|
+
setTimeout(() => setItems((prev) => prev.filter((i) => !batch.some((b) => b.id === i.id))), 2600);
|
|
383
|
+
});
|
|
384
|
+
}, [live.client]);
|
|
385
|
+
return /* @__PURE__ */ jsx("div", {
|
|
386
|
+
className: "deck-reactions-overlay",
|
|
387
|
+
"aria-hidden": true,
|
|
388
|
+
children: items.map((i) => /* @__PURE__ */ jsx("span", {
|
|
389
|
+
className: "deck-reaction-float",
|
|
390
|
+
style: {
|
|
391
|
+
left: `${i.x}%`,
|
|
392
|
+
animationDelay: `${i.id % 5 * 60}ms`
|
|
393
|
+
},
|
|
394
|
+
children: i.emoji
|
|
395
|
+
}, i.id))
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* THE WAY BACK, for a viewer reading on their own.
|
|
400
|
+
*
|
|
401
|
+
* It shows only while this window is NOT following: in step there is nothing to say, and a viewer
|
|
402
|
+
* who wants out of step simply presses an arrow — any move of their own ends the follow (see
|
|
403
|
+
* `withTransition` in Deck.tsx). `presenter` stays null until the room has said where they are, so
|
|
404
|
+
* a late joiner gets the button without a slide number rather than no button at all.
|
|
405
|
+
*/
|
|
406
|
+
function FollowControl() {
|
|
407
|
+
const deck = useDeck();
|
|
408
|
+
const live = useLiveOptional();
|
|
409
|
+
const actions = useActions();
|
|
410
|
+
const role = useStore(live.client.role);
|
|
411
|
+
const presenting = useStore(live.client.state, (s) => s.presence.presenter);
|
|
412
|
+
const { follow, presenter, h, v } = useNav((s) => ({
|
|
413
|
+
follow: s.follow,
|
|
414
|
+
presenter: s.presenter,
|
|
415
|
+
h: s.h,
|
|
416
|
+
v: s.v
|
|
417
|
+
}));
|
|
418
|
+
if (role === "presenter" || deck.mode !== "play" || !presenting || follow) return null;
|
|
419
|
+
const away = !presenter || presenter.h !== h || presenter.v !== v;
|
|
420
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
421
|
+
className: "deck-follow",
|
|
422
|
+
role: "status",
|
|
423
|
+
children: [/* @__PURE__ */ jsx("span", { children: presenter && away ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
424
|
+
"You are on ",
|
|
425
|
+
slideLabel(deck, h, v),
|
|
426
|
+
" · they are on",
|
|
427
|
+
" ",
|
|
428
|
+
/* @__PURE__ */ jsx("b", { children: slideLabel(deck, presenter.h, presenter.v) })
|
|
429
|
+
] }) : "Reading on your own" }), /* @__PURE__ */ jsxs("button", {
|
|
430
|
+
type: "button",
|
|
431
|
+
onClick: () => actions.setFollow(true),
|
|
432
|
+
children: [/* @__PURE__ */ jsx(Icon, { name: "play" }), " Follow the presenter"]
|
|
433
|
+
})]
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* A slide, spelled the way THIS deck's own slide number spells it — so "they are on 10" and the
|
|
438
|
+
* counter in the corner are talking about the same thing. A `c/t` deck loses its total here: the
|
|
439
|
+
* sentence around the number does not need one.
|
|
440
|
+
*/
|
|
441
|
+
function slideLabel(deck, h, v) {
|
|
442
|
+
const format = deck.config.slideNumber === true ? "h.v" : deck.config.slideNumber || "c";
|
|
443
|
+
return formatSlideNumber(format === "c/t" ? "c" : format, {
|
|
444
|
+
h,
|
|
445
|
+
v,
|
|
446
|
+
click: 0
|
|
447
|
+
}, deck.columns);
|
|
448
|
+
}
|
|
449
|
+
//#endregion
|
|
450
|
+
export { LiveProvider, PointerLayer, slideLabel };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/live/analytics.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Time on each slide, sent to the host every 20 seconds and when the page is hidden, with the
|
|
4
|
+
* anonymous viewer id (a random string kept in localStorage — no cookies, no fingerprinting).
|
|
5
|
+
* The host counts a view per viewer per day and sums dwell per slide for the stats page.
|
|
6
|
+
*/
|
|
7
|
+
export declare function Analytics(): null;
|
|
8
|
+
/** Ask the host for a short link to a slide; falls back to the deck's own hash URL offline. */
|
|
9
|
+
export declare function shareLink(api: string | null, slug: string, base: string, pos: {
|
|
10
|
+
h: number;
|
|
11
|
+
v: number;
|
|
12
|
+
click?: number;
|
|
13
|
+
}, label?: string): Promise<string>;
|
|
14
|
+
export declare function copyText(text: string): Promise<boolean>;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { roomIdOf } from "../core/model.js";
|
|
2
|
+
import { key } from "../core/navigation.js";
|
|
3
|
+
import { useDeck } from "../react/context.js";
|
|
4
|
+
import { viewerId } from "./client.js";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
//#region src/live/analytics.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Time on each slide, sent to the host every 20 seconds and when the page is hidden, with the
|
|
9
|
+
* anonymous viewer id (a random string kept in localStorage — no cookies, no fingerprinting).
|
|
10
|
+
* The host counts a view per viewer per day and sums dwell per slide for the stats page.
|
|
11
|
+
*/
|
|
12
|
+
function Analytics() {
|
|
13
|
+
const deck = useDeck();
|
|
14
|
+
const api = deck.config.live.api;
|
|
15
|
+
const slug = roomIdOf(deck.config);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!api || !slug || deck.mode === "print" || deck.mode === "presenter") return;
|
|
18
|
+
const dwell = {};
|
|
19
|
+
const enters = {};
|
|
20
|
+
let current = key(deck.nav.get().h, deck.nav.get().v);
|
|
21
|
+
let since = Date.now();
|
|
22
|
+
enters[current] = 1;
|
|
23
|
+
const settle = () => {
|
|
24
|
+
const t = Date.now();
|
|
25
|
+
if (!document.hidden) dwell[current] = (dwell[current] ?? 0) + (t - since);
|
|
26
|
+
since = t;
|
|
27
|
+
};
|
|
28
|
+
const unsub = deck.nav.subscribe((s) => {
|
|
29
|
+
const k = key(s.h, s.v);
|
|
30
|
+
if (k === current) return;
|
|
31
|
+
settle();
|
|
32
|
+
current = k;
|
|
33
|
+
enters[k] = (enters[k] ?? 0) + 1;
|
|
34
|
+
});
|
|
35
|
+
const flush = () => {
|
|
36
|
+
settle();
|
|
37
|
+
if (!Object.keys(dwell).length && !Object.keys(enters).length) return;
|
|
38
|
+
const body = JSON.stringify({
|
|
39
|
+
viewer: viewerId(),
|
|
40
|
+
dwell,
|
|
41
|
+
enters
|
|
42
|
+
});
|
|
43
|
+
for (const k of Object.keys(dwell)) delete dwell[k];
|
|
44
|
+
for (const k of Object.keys(enters)) delete enters[k];
|
|
45
|
+
const url = `${api}/decks/${encodeURIComponent(slug)}/beacon`;
|
|
46
|
+
if (!navigator.sendBeacon?.(url, new Blob([body], { type: "application/json" }))) fetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body,
|
|
49
|
+
headers: { "content-type": "application/json" },
|
|
50
|
+
keepalive: true
|
|
51
|
+
}).catch(() => {});
|
|
52
|
+
};
|
|
53
|
+
const timer = setInterval(flush, 2e4);
|
|
54
|
+
const onVisibility = () => {
|
|
55
|
+
if (document.hidden) flush();
|
|
56
|
+
else since = Date.now();
|
|
57
|
+
};
|
|
58
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
59
|
+
window.addEventListener("pagehide", flush);
|
|
60
|
+
return () => {
|
|
61
|
+
clearInterval(timer);
|
|
62
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
63
|
+
window.removeEventListener("pagehide", flush);
|
|
64
|
+
unsub();
|
|
65
|
+
flush();
|
|
66
|
+
};
|
|
67
|
+
}, [
|
|
68
|
+
api,
|
|
69
|
+
slug,
|
|
70
|
+
deck.mode,
|
|
71
|
+
deck.nav
|
|
72
|
+
]);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/** Ask the host for a short link to a slide; falls back to the deck's own hash URL offline. */
|
|
76
|
+
async function shareLink(api, slug, base, pos, label) {
|
|
77
|
+
const fallback = `${window.location.origin}${base}#/${pos.h}${pos.v || pos.click ? `/${pos.v}` : ""}${pos.click ? `/${pos.click}` : ""}`;
|
|
78
|
+
if (!api) return fallback;
|
|
79
|
+
try {
|
|
80
|
+
const r = await fetch(`${api}/decks/${encodeURIComponent(slug)}/share`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/json" },
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
...pos,
|
|
85
|
+
label
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
if (!r.ok) return fallback;
|
|
89
|
+
return (await r.json()).url ?? fallback;
|
|
90
|
+
} catch {
|
|
91
|
+
return fallback;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function copyText(text) {
|
|
95
|
+
try {
|
|
96
|
+
await navigator.clipboard.writeText(text);
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { Analytics, copyText, shareLink };
|