@honeydeck/honeydeck 0.5.0 → 0.7.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/AGENTS.md +4 -4
- package/DEVELOPMENT.md +2 -3
- package/Readme.md +15 -15
- package/SPEC.md +2 -1
- package/docs/{components-browser-frame.md → browser-frame.md} +4 -0
- package/docs/components.md +12 -12
- package/docs/configuration.md +2 -0
- package/docs/customization.md +2 -0
- package/docs/deeper-dive.md +2 -0
- package/docs/getting-started.md +2 -0
- package/docs/index.json +258 -0
- package/docs/{components-keyboard.md → keyboard.md} +4 -0
- package/docs/{components-list-style.md → list-style.md} +4 -0
- package/docs/local-development.md +3 -1
- package/docs/mermaid.md +2 -0
- package/docs/mobile.md +3 -1
- package/docs/navigation.md +2 -0
- package/docs/{components-notes.md → notes.md} +4 -0
- package/docs/pdf-export.md +2 -0
- package/docs/presenter-mode.md +14 -6
- package/docs/{components-reveal-group.md → reveal-group.md} +2 -0
- package/docs/{components-reveal-with.md → reveal-with.md} +2 -0
- package/docs/{components-reveal.md → reveal.md} +5 -3
- package/docs/skills.md +3 -1
- package/docs/slides.md +2 -0
- package/docs/slidev-migration.md +2 -0
- package/docs/steps-and-reveals.md +2 -0
- package/docs/{components-timeline-steps.md → timeline-steps.md} +2 -0
- package/package.json +3 -2
- package/skills/SPEC.md +4 -4
- package/skills/honeydeck/SKILL.md +7 -7
- package/skills/slidev-migration/SKILL.md +6 -6
- package/src/runtime/Deck.tsx +18 -1
- package/src/runtime/components/SPEC.md +3 -3
- package/src/runtime/presentationApi.ts +112 -6
- package/src/runtime/sync.ts +130 -12
- package/src/runtime/views/PresenterCastButton.tsx +17 -9
- package/src/runtime/views/PresenterView.tsx +247 -30
- package/src/runtime/views/SPEC.md +28 -5
- package/src/runtime/views/presenterTime.ts +15 -0
package/docs/slides.md
CHANGED
package/docs/slidev-migration.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- Generated from packages/docs/content/docs/(advanced)/slidev-migration.mdx. Do not edit by hand. -->
|
|
2
|
+
|
|
1
3
|
# Slidev migration
|
|
2
4
|
|
|
3
5
|
Coming from [Slidev](https://sli.dev)? Honeydeck ships an optional `slidev-migration` agent skill that can help an AI coding agent initialize a Honeydeck project and migrate a Slidev deck.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- Generated from packages/docs/content/docs/(core)/steps-and-reveals.mdx. Do not edit by hand. -->
|
|
2
|
+
|
|
1
3
|
# Steps & Reveals
|
|
2
4
|
|
|
3
5
|
Honeydeck has a first-class step concept. Each slide has a timeline built from `Reveal`/`RevealGroup`/`Fade`/`FadeGroup` components, `RevealWith`/`FadeWith` synchronized content, custom component step blocks, code highlight ranges, and Magic Code states, counted in document order.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@honeydeck/honeydeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MDX and React-based presentation framework for AI-friendly slide decks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"!src/**/*.test.tsx",
|
|
36
36
|
"!src/**/fixtures",
|
|
37
37
|
"!src/**/fixtures/**",
|
|
38
|
-
"docs",
|
|
39
38
|
"skills",
|
|
39
|
+
"docs",
|
|
40
40
|
"Readme.md",
|
|
41
41
|
"SPEC.md",
|
|
42
42
|
"DEVELOPMENT.md",
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"dev:init": "node --import tsx ./src/cli/index.ts dev --deck src/cli/templates/starter/deck.mdx",
|
|
96
96
|
"format": "biome check --write",
|
|
97
97
|
"lint": "biome check",
|
|
98
|
+
"prepack": "npm -w @honeydeck/docs run export:package-docs",
|
|
98
99
|
"pack:dry-run": "npm pack --dry-run",
|
|
99
100
|
"pack:smoke": "node ./scripts/packed-smoke.mjs",
|
|
100
101
|
"release:plan": "node ./scripts/release-plan.mjs",
|
package/skills/SPEC.md
CHANGED
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
Honeydeck ships installable agent skills:
|
|
8
8
|
|
|
9
|
-
- `skills/honeydeck/SKILL.md` (`honeydeck`) helps AI agents use Honeydeck correctly: MDX entrypoint, deck frontmatter, exact `---` slide separators, slide frontmatter, built-in layouts, explicit theme/CSS imports, presenter notes, steps/reveals, code blocks, PDF export, custom React components, customization,
|
|
9
|
+
- `skills/honeydeck/SKILL.md` (`honeydeck`) helps AI agents use Honeydeck correctly: MDX entrypoint, deck frontmatter, exact `---` slide separators, slide frontmatter, built-in layouts, explicit theme/CSS imports, presenter notes, steps/reveals, code blocks, PDF export, custom React components, customization, public docs, specs, and runtime reference pages.
|
|
10
10
|
- `skills/presentation-writing/SKILL.md` (`presentation-writing`) gives opinionated guidance for writing great slides and delivering good presentations: audience/goal/duration discovery, storyline, one idea per slide, strong headlines, sparse visuals, speaker notes, timing, narrative flow, progressive disclosure, and review heuristics.
|
|
11
11
|
- `skills/slidev-migration/SKILL.md` (`slidev-migration`) helps AI agents migrate presentations from Slidev/sli.dev to Honeydeck: initialize a Honeydeck project when needed, convert `slides.md` to `deck.mdx`, map Slidev frontmatter/layouts/assets/notes/reveals/scripts/Magic Move code blocks to Honeydeck equivalents, and flag Vue/theme features that need manual React follow-up.
|
|
12
12
|
|
|
13
13
|
Expected behavior:
|
|
14
14
|
|
|
15
15
|
- All skills are discoverable by the `skills` CLI when users run `npx skills add <honeydeck-repo-url> --copy` or explicit `npx skills add <honeydeck-repo-url> --copy --skill honeydeck` / `--skill presentation-writing` / `--skill slidev-migration`.
|
|
16
|
-
- The `honeydeck` skill instructs agents to use Honeydeck documentation (`
|
|
17
|
-
- The `honeydeck` skill documents a local docs discovery order: current project's `node_modules/@honeydeck/honeydeck
|
|
16
|
+
- The `honeydeck` skill instructs agents to use installed Honeydeck package documentation (`node_modules/@honeydeck/honeydeck/docs/*.md`, `node_modules/@honeydeck/honeydeck/docs/index.json`, `Readme.md`, package `SPEC.md`, linked colocated `SPEC.md` files, and the public docs site) as the source of truth when available.
|
|
17
|
+
- The `honeydeck` skill documents a consumer-focused local docs discovery order: current project's `node_modules/@honeydeck/honeydeck/docs` package docs first, then installed package specs, then package-root checkout docs/specs, then the public docs URL. It must not require monorepo-only `packages/docs` paths for normal installed-package use.
|
|
18
18
|
- The `presentation-writing` skill is framework-agnostic enough to help with presentation quality, while still working well alongside the Honeydeck skill.
|
|
19
19
|
- The `slidev-migration` skill instructs agents to inspect existing Slidev projects, preserve source files until the user approves cleanup, initialize or merge Honeydeck starter files, migrate common Slidev syntax to Honeydeck MDX/React, rewrite `md magic-move` blocks to Honeydeck's canonical `md magic-code` syntax, and document unsupported or approximated features.
|
|
20
20
|
- `honeydeck init` should offer to open the interactive skills installer for the generated project and make clear that accepting runs `npx skills add`.
|
|
21
21
|
- `honeydeck init` and `honeydeck skill` should delegate bundled skill selection, scope selection, and agent selection to the same `npx skills add <honeydeck-package-source> --copy` flow.
|
|
22
|
-
- `docs/skills.
|
|
22
|
+
- `packages/docs/content/docs/(advanced)/skills.mdx` should document why and how to install the bundled skills, list the `honeydeck`, `presentation-writing`, and `slidev-migration` skills, and link to the Slidev migration guide for migration-specific details.
|
|
@@ -9,28 +9,28 @@ You help the user work with Honeydeck presentations. Honeydeck decks are MDX fil
|
|
|
9
9
|
|
|
10
10
|
## Source of truth
|
|
11
11
|
|
|
12
|
-
Before giving Honeydeck-specific syntax or changing a deck, prefer
|
|
12
|
+
Before giving Honeydeck-specific syntax or changing a deck, prefer the installed package docs and package specs when available. This skill is for consumers of the `@honeydeck/honeydeck` package, so do not depend on monorepo-only paths.
|
|
13
13
|
|
|
14
14
|
Docs discovery order:
|
|
15
15
|
|
|
16
|
-
1. Current project dependency docs: `node_modules/@honeydeck/honeydeck/
|
|
17
|
-
2.
|
|
18
|
-
3.
|
|
19
|
-
4. Public docs URL when local docs are unavailable: `/docs` on the Honeydeck marketing site.
|
|
16
|
+
1. Current project dependency docs: `node_modules/@honeydeck/honeydeck/docs/index.json`, `node_modules/@honeydeck/honeydeck/docs/*.md`, `node_modules/@honeydeck/honeydeck/Readme.md`, `node_modules/@honeydeck/honeydeck/SPEC.md`, and linked colocated `SPEC.md` files.
|
|
17
|
+
2. Package-root checkout docs: `docs/index.json`, `docs/*.md`, `Readme.md`, `SPEC.md`, and linked colocated `SPEC.md` files.
|
|
18
|
+
3. Public docs URL when local docs are unavailable: `https://honeydeck.dev/docs`.
|
|
20
19
|
|
|
21
20
|
Important docs:
|
|
22
21
|
|
|
23
22
|
- `Readme.md` for the compact package overview and documentation index
|
|
23
|
+
- `docs/index.json` for the installed docs page list and order
|
|
24
24
|
- `docs/getting-started.md` for quick start and first-run orientation
|
|
25
25
|
- `docs/deeper-dive.md` for CLI details, feature overview, architecture, exports, and agent skills
|
|
26
|
-
-
|
|
26
|
+
- package `SPEC.md` and linked colocated `SPEC.md` files for expected behavior
|
|
27
27
|
- `docs/slides.md` for deck structure, slide separators, and multi-file decks
|
|
28
28
|
- `docs/steps-and-reveals.md` for step-by-step reveals, code steps, and Magic Code
|
|
29
29
|
- `docs/customization.md` for themes, layouts, custom React components, layout maps, demos, and design tokens
|
|
30
30
|
- `docs/configuration.md` for frontmatter options
|
|
31
31
|
- `docs/navigation.md`, `docs/mobile.md`, `docs/presenter-mode.md`, and `docs/pdf-export.md` for presenting/exporting
|
|
32
32
|
|
|
33
|
-
If
|
|
33
|
+
If local package docs are unavailable, tell the user they can use the public docs site for prose docs and run `npx honeydeck dev` to open runtime reference pages for active theme tokens, layouts, and built-in components.
|
|
34
34
|
|
|
35
35
|
## Honeydeck basics to remember
|
|
36
36
|
|
|
@@ -11,15 +11,15 @@ Start by reading all Honeydeck related skills available. Alternatively you can c
|
|
|
11
11
|
|
|
12
12
|
## Source of truth
|
|
13
13
|
|
|
14
|
-
Before changing files, read the available project
|
|
14
|
+
Before changing files, read the available project docs and package specs:
|
|
15
15
|
|
|
16
|
-
- `Readme.md`, `docs/
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
16
|
+
- `Readme.md`, public docs content under `packages/docs/content/docs` when in the monorepo, root `SPEC.md`, linked colocated `SPEC.md` files, and the Slides guide for Honeydeck deck structure
|
|
17
|
+
- The Configuration guide for Honeydeck frontmatter
|
|
18
|
+
- The Steps and reveals guide for Reveal/RevealGroup/code steps
|
|
19
|
+
- The Customization guide for layouts, themes, components, layout maps, and design tokens
|
|
20
20
|
- Existing Slidev files: `slides.md`, `pages/**/*.md`, `components/**/*.vue`, `layouts/**/*.vue`, `styles/**`, `public/**`, `package.json`, `vite.config.*`, and local theme/addon packages
|
|
21
21
|
|
|
22
|
-
If Honeydeck docs are not nearby, package
|
|
22
|
+
If Honeydeck docs are not nearby, package specs may be in `node_modules/@honeydeck/honeydeck`, or the user can use the public docs site. A migrated deck's runtime reference pages cover active theme tokens, layouts, and built-in components.
|
|
23
23
|
|
|
24
24
|
## Migration goal
|
|
25
25
|
|
package/src/runtime/Deck.tsx
CHANGED
|
@@ -156,12 +156,22 @@ export function Deck() {
|
|
|
156
156
|
}, [route]);
|
|
157
157
|
|
|
158
158
|
// ── Audience sync: BroadcastChannel + Presentation API receiver ──────
|
|
159
|
+
const [blankScreen, setBlankScreen] = useState<"black" | null>(null);
|
|
160
|
+
const handleBlankScreen = useCallback(
|
|
161
|
+
(mode: "black" | "off") =>
|
|
162
|
+
setBlankScreen(mode === "black" ? "black" : null),
|
|
163
|
+
[],
|
|
164
|
+
);
|
|
159
165
|
useSync({
|
|
160
166
|
enabled: route.view === "slide" || route.view === "overview",
|
|
161
167
|
isPresenter: false,
|
|
168
|
+
onSetColorMode: setColorMode,
|
|
169
|
+
onBlankScreen: handleBlankScreen,
|
|
162
170
|
});
|
|
163
171
|
usePresentationReceiverSync({
|
|
164
172
|
enabled: route.view === "slide" || route.view === "overview",
|
|
173
|
+
onSetColorMode: setColorMode,
|
|
174
|
+
onBlankScreen: handleBlankScreen,
|
|
165
175
|
});
|
|
166
176
|
|
|
167
177
|
const resetZoom = useCallback(() => {
|
|
@@ -250,7 +260,9 @@ export function Deck() {
|
|
|
250
260
|
|
|
251
261
|
// ── Presenter mode: delegate to PresenterView ──────────────────────────
|
|
252
262
|
if (route.view === "presenter") {
|
|
253
|
-
return
|
|
263
|
+
return (
|
|
264
|
+
<PresenterView colorMode={colorMode} onSetColorMode={setColorMode} />
|
|
265
|
+
);
|
|
254
266
|
}
|
|
255
267
|
|
|
256
268
|
// Whether slide transitions are enabled (can be disabled via deck frontmatter)
|
|
@@ -394,6 +406,11 @@ export function Deck() {
|
|
|
394
406
|
/>
|
|
395
407
|
)}
|
|
396
408
|
|
|
409
|
+
{/* ── Black screen overlay (controlled by presenter) ────────── */}
|
|
410
|
+
{blankScreen === "black" && (
|
|
411
|
+
<div className="fixed inset-0 bg-black z-[100]" aria-hidden="true" />
|
|
412
|
+
)}
|
|
413
|
+
|
|
397
414
|
{/* ── Navigation bar ────────────────────────────────────────────── */}
|
|
398
415
|
{/* GAP-06: showSlideNumbers wired from config */}
|
|
399
416
|
{!isOverview && (
|
|
@@ -14,7 +14,7 @@ Injected fenced code blocks render through `HoneydeckCodeBlock` from the direct
|
|
|
14
14
|
|
|
15
15
|
### Color Mode Controls
|
|
16
16
|
|
|
17
|
-
`@honeydeck/honeydeck/components` exports the configured color mode type and color mode cycle button for public imports used by Honeydeck chrome surfaces and the
|
|
17
|
+
`@honeydeck/honeydeck/components` exports the configured color mode type and color mode cycle button for public imports used by Honeydeck chrome surfaces and the docs site.
|
|
18
18
|
|
|
19
19
|
```tsx
|
|
20
20
|
import { ColorModeCycleButton, type ColorMode } from '@honeydeck/honeydeck/components'
|
|
@@ -30,7 +30,7 @@ Behavior:
|
|
|
30
30
|
|
|
31
31
|
### Button Controls
|
|
32
32
|
|
|
33
|
-
`@honeydeck/honeydeck/components` exports generic Honeydeck-token-based button primitives and class recipes for runtime chrome, runtime reference pages, and
|
|
33
|
+
`@honeydeck/honeydeck/components` exports generic Honeydeck-token-based button primitives and class recipes for runtime chrome, runtime reference pages, and docs controls.
|
|
34
34
|
|
|
35
35
|
```tsx
|
|
36
36
|
import { Button, buttonPrimaryClass, iconButtonClass } from '@honeydeck/honeydeck/components'
|
|
@@ -41,7 +41,7 @@ Behavior:
|
|
|
41
41
|
- `<Button>` renders a native `button` with `type="button"` by default.
|
|
42
42
|
- `variant` selects one of `primary`, `secondary`, `icon`, `small`, or `quiet`.
|
|
43
43
|
- `buttonClass(variant, className)` returns the matching token-based Tailwind class string and appends `className` when provided.
|
|
44
|
-
- Exported class recipes use only shipped Honeydeck base-theme tokens for foreground, surface, border, primary, and transition behavior (`--honeydeck-primary`, `--honeydeck-primary-foreground`, `--honeydeck-surface`, `--honeydeck-surface-foreground`, `--honeydeck-border`, `--honeydeck-foreground`, `--honeydeck-background`) so public imports do not depend on
|
|
44
|
+
- Exported class recipes use only shipped Honeydeck base-theme tokens for foreground, surface, border, primary, and transition behavior (`--honeydeck-primary`, `--honeydeck-primary-foreground`, `--honeydeck-surface`, `--honeydeck-surface-foreground`, `--honeydeck-border`, `--honeydeck-foreground`, `--honeydeck-background`) so public imports do not depend on docs-site-only aliases at publish time.
|
|
45
45
|
|
|
46
46
|
### Runtime chrome buttons
|
|
47
47
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { ColorMode } from "./components/ColorModeCycleButton.tsx";
|
|
2
3
|
import { getRouteUrl } from "./navigation.ts";
|
|
3
4
|
import { navigate, parseHash, type Route } from "./router.ts";
|
|
4
5
|
import {
|
|
6
|
+
createSyncColorModeMessage,
|
|
5
7
|
createSyncResponseMessage,
|
|
6
8
|
resolveAudienceRouteFromSyncMessage,
|
|
9
|
+
type SyncBlankScreenMessage,
|
|
10
|
+
type SyncColorModeMessage,
|
|
7
11
|
type SyncMessage,
|
|
8
12
|
type SyncNavigateMessage,
|
|
9
13
|
type SyncRequestMessage,
|
|
@@ -60,6 +64,10 @@ type PresenterRoute = {
|
|
|
60
64
|
step: number;
|
|
61
65
|
};
|
|
62
66
|
|
|
67
|
+
type PresentationColorModeRef = {
|
|
68
|
+
current: ColorMode;
|
|
69
|
+
};
|
|
70
|
+
|
|
63
71
|
type PresentationWindowLike = Window & {
|
|
64
72
|
PresentationRequest?: PresentationRequestLike;
|
|
65
73
|
navigator: Navigator & {
|
|
@@ -142,13 +150,21 @@ function sendPresentationSyncResponse(
|
|
|
142
150
|
connection: PresentationConnectionLike | null,
|
|
143
151
|
slide: number,
|
|
144
152
|
step: number,
|
|
153
|
+
colorMode?: ColorMode,
|
|
145
154
|
): void {
|
|
146
155
|
sendPresentationMessage(
|
|
147
156
|
connection,
|
|
148
|
-
createSyncResponseMessage({ slide, step }),
|
|
157
|
+
createSyncResponseMessage({ slide, step }, colorMode),
|
|
149
158
|
);
|
|
150
159
|
}
|
|
151
160
|
|
|
161
|
+
function sendPresentationColorModeToConnection(
|
|
162
|
+
connection: PresentationConnectionLike | null,
|
|
163
|
+
colorMode: ColorMode,
|
|
164
|
+
): void {
|
|
165
|
+
sendPresentationMessage(connection, createSyncColorModeMessage(colorMode));
|
|
166
|
+
}
|
|
167
|
+
|
|
152
168
|
type PresentationRouteRef = {
|
|
153
169
|
current: PresenterRoute;
|
|
154
170
|
};
|
|
@@ -163,7 +179,9 @@ export async function startPresentationCast({
|
|
|
163
179
|
audienceUrl,
|
|
164
180
|
currentSlide,
|
|
165
181
|
currentStep,
|
|
182
|
+
currentColorMode,
|
|
166
183
|
routeRef,
|
|
184
|
+
colorModeRef,
|
|
167
185
|
requestConstructor,
|
|
168
186
|
connectionRef,
|
|
169
187
|
startInFlightRef,
|
|
@@ -175,7 +193,9 @@ export async function startPresentationCast({
|
|
|
175
193
|
audienceUrl: string | null;
|
|
176
194
|
currentSlide: number;
|
|
177
195
|
currentStep: number;
|
|
196
|
+
currentColorMode: ColorMode;
|
|
178
197
|
routeRef: PresentationRouteRef;
|
|
198
|
+
colorModeRef: PresentationColorModeRef;
|
|
179
199
|
requestConstructor: PresentationRequestLike | null;
|
|
180
200
|
connectionRef: { current: PresentationConnectionLike | null };
|
|
181
201
|
startInFlightRef: { current: boolean };
|
|
@@ -212,6 +232,7 @@ export async function startPresentationCast({
|
|
|
212
232
|
connection,
|
|
213
233
|
routeRef.current.slide,
|
|
214
234
|
routeRef.current.step,
|
|
235
|
+
colorModeRef.current,
|
|
215
236
|
);
|
|
216
237
|
};
|
|
217
238
|
|
|
@@ -235,6 +256,7 @@ export async function startPresentationCast({
|
|
|
235
256
|
connection.addEventListener?.("statechange", onStateChange);
|
|
236
257
|
|
|
237
258
|
sendPresentationRouteToConnection(connection, currentSlide, currentStep);
|
|
259
|
+
sendPresentationColorModeToConnection(connection, currentColorMode);
|
|
238
260
|
} catch {
|
|
239
261
|
if (castGeneration !== castGenerationRef.current) {
|
|
240
262
|
return;
|
|
@@ -265,8 +287,12 @@ function getConnectionFromEvent(event: {
|
|
|
265
287
|
|
|
266
288
|
export function usePresentationReceiverSync({
|
|
267
289
|
enabled = true,
|
|
290
|
+
onSetColorMode,
|
|
291
|
+
onBlankScreen,
|
|
268
292
|
}: {
|
|
269
293
|
enabled?: boolean;
|
|
294
|
+
onSetColorMode?: (mode: ColorMode) => void;
|
|
295
|
+
onBlankScreen?: (mode: "black" | "off") => void;
|
|
270
296
|
}): void {
|
|
271
297
|
useEffect(() => {
|
|
272
298
|
if (!enabled) return;
|
|
@@ -283,6 +309,20 @@ export function usePresentationReceiverSync({
|
|
|
283
309
|
function handleMessage(event: MessageEvent<unknown>) {
|
|
284
310
|
const message = parsePresentationMessage(event.data);
|
|
285
311
|
if (cancelled || !isSyncMessage(message)) return;
|
|
312
|
+
|
|
313
|
+
if (isColorModeMessage(message)) {
|
|
314
|
+
onSetColorMode?.(message.colorMode);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (isBlankScreenMessage(message)) {
|
|
319
|
+
onBlankScreen?.(message.mode);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (message.type === "sync-response" && message.colorMode) {
|
|
324
|
+
onSetColorMode?.(message.colorMode);
|
|
325
|
+
}
|
|
286
326
|
const currentRoute = parseHash(location.hash);
|
|
287
327
|
const nextRoute = resolveAudienceRouteFromSyncMessage(
|
|
288
328
|
currentRoute,
|
|
@@ -342,7 +382,7 @@ export function usePresentationReceiverSync({
|
|
|
342
382
|
});
|
|
343
383
|
connections.clear();
|
|
344
384
|
};
|
|
345
|
-
}, [enabled]);
|
|
385
|
+
}, [enabled, onSetColorMode, onBlankScreen]);
|
|
346
386
|
}
|
|
347
387
|
|
|
348
388
|
export function usePresentationCast({
|
|
@@ -350,16 +390,19 @@ export function usePresentationCast({
|
|
|
350
390
|
audienceUrl,
|
|
351
391
|
currentSlide,
|
|
352
392
|
currentStep,
|
|
393
|
+
currentColorMode,
|
|
353
394
|
}: {
|
|
354
395
|
enabled?: boolean;
|
|
355
396
|
audienceUrl: string | null;
|
|
356
397
|
currentSlide: number;
|
|
357
398
|
currentStep: number;
|
|
399
|
+
currentColorMode: ColorMode;
|
|
358
400
|
}): {
|
|
359
401
|
supported: boolean;
|
|
360
402
|
isCasting: boolean;
|
|
361
403
|
startCasting: () => Promise<void>;
|
|
362
404
|
stopCasting: () => void;
|
|
405
|
+
sendMessage: (message: SyncMessage) => void;
|
|
363
406
|
} {
|
|
364
407
|
const [isCasting, setIsCasting] = useState(false);
|
|
365
408
|
const isMountedRef = useRef(true);
|
|
@@ -370,6 +413,7 @@ export function usePresentationCast({
|
|
|
370
413
|
slide: currentSlide,
|
|
371
414
|
step: currentStep,
|
|
372
415
|
});
|
|
416
|
+
const colorModeRef = useRef<ColorMode>(currentColorMode);
|
|
373
417
|
const supported = isPresentationApiSupported();
|
|
374
418
|
|
|
375
419
|
useEffect(() => {
|
|
@@ -379,6 +423,10 @@ export function usePresentationCast({
|
|
|
379
423
|
};
|
|
380
424
|
}, [currentSlide, currentStep]);
|
|
381
425
|
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
colorModeRef.current = currentColorMode;
|
|
428
|
+
}, [currentColorMode]);
|
|
429
|
+
|
|
382
430
|
const setCastingState = useCallback((next: boolean) => {
|
|
383
431
|
if (isMountedRef.current) setIsCasting(next);
|
|
384
432
|
}, []);
|
|
@@ -394,7 +442,9 @@ export function usePresentationCast({
|
|
|
394
442
|
audienceUrl,
|
|
395
443
|
currentSlide,
|
|
396
444
|
currentStep,
|
|
445
|
+
currentColorMode,
|
|
397
446
|
routeRef,
|
|
447
|
+
colorModeRef,
|
|
398
448
|
requestConstructor: getPresentationRequestConstructor(),
|
|
399
449
|
connectionRef,
|
|
400
450
|
startInFlightRef,
|
|
@@ -405,6 +455,7 @@ export function usePresentationCast({
|
|
|
405
455
|
audienceUrl,
|
|
406
456
|
currentSlide,
|
|
407
457
|
currentStep,
|
|
458
|
+
currentColorMode,
|
|
408
459
|
enabled,
|
|
409
460
|
supported,
|
|
410
461
|
setCastingState,
|
|
@@ -419,6 +470,14 @@ export function usePresentationCast({
|
|
|
419
470
|
);
|
|
420
471
|
}, [currentSlide, currentStep, isCasting]);
|
|
421
472
|
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
if (!isCasting) return;
|
|
475
|
+
sendPresentationColorModeToConnection(
|
|
476
|
+
connectionRef.current,
|
|
477
|
+
currentColorMode,
|
|
478
|
+
);
|
|
479
|
+
}, [currentColorMode, isCasting]);
|
|
480
|
+
|
|
422
481
|
useEffect(() => {
|
|
423
482
|
isMountedRef.current = true;
|
|
424
483
|
return () => {
|
|
@@ -427,9 +486,13 @@ export function usePresentationCast({
|
|
|
427
486
|
};
|
|
428
487
|
}, [stopCasting]);
|
|
429
488
|
|
|
489
|
+
const sendMessage = useCallback((message: SyncMessage) => {
|
|
490
|
+
sendPresentationMessage(connectionRef.current, message);
|
|
491
|
+
}, []);
|
|
492
|
+
|
|
430
493
|
return useMemo(
|
|
431
|
-
() => ({ supported, isCasting, startCasting, stopCasting }),
|
|
432
|
-
[supported, isCasting, startCasting, stopCasting],
|
|
494
|
+
() => ({ supported, isCasting, startCasting, stopCasting, sendMessage }),
|
|
495
|
+
[supported, isCasting, startCasting, stopCasting, sendMessage],
|
|
433
496
|
);
|
|
434
497
|
}
|
|
435
498
|
|
|
@@ -441,9 +504,52 @@ function isSyncRequestMessage(value: unknown): value is SyncRequestMessage {
|
|
|
441
504
|
|
|
442
505
|
function isSyncMessage(
|
|
443
506
|
value: unknown,
|
|
444
|
-
): value is
|
|
507
|
+
): value is
|
|
508
|
+
| SyncNavigateMessage
|
|
509
|
+
| SyncResponseMessage
|
|
510
|
+
| SyncColorModeMessage
|
|
511
|
+
| SyncBlankScreenMessage {
|
|
445
512
|
if (typeof value !== "object" || value === null) return false;
|
|
446
513
|
if (!("type" in value)) return false;
|
|
447
514
|
const type = (value as SyncMessage).type;
|
|
448
|
-
|
|
515
|
+
if (type === "color-mode") return isColorModeMessage(value);
|
|
516
|
+
if (type === "blank-screen") return isBlankScreenMessage(value);
|
|
517
|
+
if (type !== "navigate" && type !== "sync-response") return false;
|
|
518
|
+
|
|
519
|
+
const slide = (value as { slide?: unknown }).slide;
|
|
520
|
+
const step = (value as { step?: unknown }).step;
|
|
521
|
+
if (
|
|
522
|
+
typeof slide !== "number" ||
|
|
523
|
+
!Number.isFinite(slide) ||
|
|
524
|
+
typeof step !== "number" ||
|
|
525
|
+
!Number.isFinite(step)
|
|
526
|
+
) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const colorMode = (value as { colorMode?: unknown }).colorMode;
|
|
531
|
+
return colorMode === undefined || isColorMode(colorMode);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function isColorModeMessage(value: unknown): value is SyncColorModeMessage {
|
|
535
|
+
if (typeof value !== "object" || value === null) return false;
|
|
536
|
+
if (!("type" in value)) return false;
|
|
537
|
+
return (
|
|
538
|
+
(value as SyncMessage).type === "color-mode" &&
|
|
539
|
+
isColorMode((value as { colorMode?: unknown }).colorMode)
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function isBlankScreenMessage(value: unknown): value is SyncBlankScreenMessage {
|
|
544
|
+
if (typeof value !== "object" || value === null) return false;
|
|
545
|
+
if (!("type" in value)) return false;
|
|
546
|
+
const mode = (value as { mode?: unknown }).mode;
|
|
547
|
+
return (
|
|
548
|
+
(value as SyncMessage).type === "blank-screen" &&
|
|
549
|
+
(mode === "black" || mode === "off")
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isColorMode(value: unknown): value is ColorMode {
|
|
554
|
+
return value === "system" || value === "light" || value === "dark";
|
|
449
555
|
}
|