@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.
Files changed (40) hide show
  1. package/AGENTS.md +4 -4
  2. package/DEVELOPMENT.md +2 -3
  3. package/Readme.md +15 -15
  4. package/SPEC.md +2 -1
  5. package/docs/{components-browser-frame.md → browser-frame.md} +4 -0
  6. package/docs/components.md +12 -12
  7. package/docs/configuration.md +2 -0
  8. package/docs/customization.md +2 -0
  9. package/docs/deeper-dive.md +2 -0
  10. package/docs/getting-started.md +2 -0
  11. package/docs/index.json +258 -0
  12. package/docs/{components-keyboard.md → keyboard.md} +4 -0
  13. package/docs/{components-list-style.md → list-style.md} +4 -0
  14. package/docs/local-development.md +3 -1
  15. package/docs/mermaid.md +2 -0
  16. package/docs/mobile.md +3 -1
  17. package/docs/navigation.md +2 -0
  18. package/docs/{components-notes.md → notes.md} +4 -0
  19. package/docs/pdf-export.md +2 -0
  20. package/docs/presenter-mode.md +14 -6
  21. package/docs/{components-reveal-group.md → reveal-group.md} +2 -0
  22. package/docs/{components-reveal-with.md → reveal-with.md} +2 -0
  23. package/docs/{components-reveal.md → reveal.md} +5 -3
  24. package/docs/skills.md +3 -1
  25. package/docs/slides.md +2 -0
  26. package/docs/slidev-migration.md +2 -0
  27. package/docs/steps-and-reveals.md +2 -0
  28. package/docs/{components-timeline-steps.md → timeline-steps.md} +2 -0
  29. package/package.json +3 -2
  30. package/skills/SPEC.md +4 -4
  31. package/skills/honeydeck/SKILL.md +7 -7
  32. package/skills/slidev-migration/SKILL.md +6 -6
  33. package/src/runtime/Deck.tsx +18 -1
  34. package/src/runtime/components/SPEC.md +3 -3
  35. package/src/runtime/presentationApi.ts +112 -6
  36. package/src/runtime/sync.ts +130 -12
  37. package/src/runtime/views/PresenterCastButton.tsx +17 -9
  38. package/src/runtime/views/PresenterView.tsx +247 -30
  39. package/src/runtime/views/SPEC.md +28 -5
  40. package/src/runtime/views/presenterTime.ts +15 -0
package/docs/slides.md CHANGED
@@ -1,3 +1,5 @@
1
+ <!-- Generated from packages/docs/content/docs/(core)/slides.mdx. Do not edit by hand. -->
2
+
1
3
  # Slides
2
4
 
3
5
  ## Basics
@@ -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.
@@ -1,3 +1,5 @@
1
+ <!-- Generated from packages/docs/content/docs/(components)/timeline-steps.mdx. Do not edit by hand. -->
2
+
1
3
  # TimelineSteps
2
4
 
3
5
  Use `<TimelineSteps>` when an imported React component should control part of a slide timeline.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honeydeck/honeydeck",
3
- "version": "0.5.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, canonical package docs, and runtime reference pages.
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 (`Readme.md` for package overview, `docs/getting-started.md` for first-run guidance, package/root `SPEC.md`, linked colocated `SPEC.md` files, `docs/deeper-dive.md`, `docs/slides.md`, `docs/steps-and-reveals.md`, `docs/customization.md`, `docs/configuration.md`, and related docs) as the source of truth when available.
17
- - The `honeydeck` skill documents a local docs discovery order: current project's `node_modules/@honeydeck/honeydeck`, monorepo checkout `packages/honeydeck`, package-root checkout docs, then the public docs URL.
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.md` 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.
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 repository/package docs when available.
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/Readme.md`, `node_modules/@honeydeck/honeydeck/SPEC.md`, and `node_modules/@honeydeck/honeydeck/docs/*.md`.
17
- 2. Monorepo checkout docs: `packages/honeydeck/Readme.md`, `packages/honeydeck/SPEC.md`, and `packages/honeydeck/docs/*.md`.
18
- 3. Fresh app or package-root docs: `Readme.md`, `SPEC.md`, and `docs/*.md`.
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
- - root/package `SPEC.md` and linked colocated `SPEC.md` files for expected behavior
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 the user is inside a generated Honeydeck project and package docs are not nearby, tell them 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.
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/package docs:
14
+ Before changing files, read the available project docs and package specs:
15
15
 
16
- - `Readme.md`, `docs/getting-started.md`, root `SPEC.md`, linked colocated `SPEC.md` files, and `docs/slides.md` for Honeydeck deck structure
17
- - `docs/configuration.md` for Honeydeck frontmatter
18
- - `docs/steps-and-reveals.md` for Reveal/RevealGroup/code steps
19
- - `docs/customization.md` for layouts, themes, components, layout maps, and design tokens
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 docs 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.
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
 
@@ -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 <PresenterView />;
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 marketing site.
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 marketing controls.
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 marketing-only aliases at publish time.
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 SyncNavigateMessage | SyncResponseMessage {
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
- return type === "navigate" || type === "sync-response";
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
  }