@bendyline/squisq-react 2.3.3 → 2.4.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.
@@ -483,7 +483,12 @@ import {
483
483
  VIEWPORT_PRESETS
484
484
  } from "@bendyline/squisq/doc";
485
485
  function useDocPlayback(script, currentTime, options = {}) {
486
- const { viewport = VIEWPORT_PRESETS.landscape, theme, onSeek } = options;
486
+ const {
487
+ viewport = VIEWPORT_PRESETS.landscape,
488
+ theme,
489
+ onSeek,
490
+ useAudioSegmentTiming = true
491
+ } = options;
487
492
  const blocks = useMemo2(() => {
488
493
  if (!script?.blocks) {
489
494
  return [];
@@ -497,10 +502,10 @@ function useDocPlayback(script, currentTime, options = {}) {
497
502
  resolvedTheme
498
503
  );
499
504
  if (hasTemplates) {
500
- const audioSegments = script.audio?.segments?.map((seg) => ({
505
+ const audioSegments = useAudioSegmentTiming ? script.audio?.segments?.map((seg) => ({
501
506
  startTime: seg.startTime,
502
507
  duration: seg.duration
503
- }));
508
+ })) : void 0;
504
509
  const expanded = expandDocBlocks(flatBlocks, {
505
510
  audioSegments,
506
511
  viewport,
@@ -524,7 +529,8 @@ function useDocPlayback(script, currentTime, options = {}) {
524
529
  script?.persistentLayers,
525
530
  script?.customTemplates,
526
531
  viewport,
527
- theme
532
+ theme,
533
+ useAudioSegmentTiming
528
534
  ]);
529
535
  const currentBlock = useMemo2(() => getBlockAtTime(blocks, currentTime), [blocks, currentTime]);
530
536
  const currentBlockIndex = useMemo2(
@@ -7,7 +7,7 @@ import {
7
7
  useDocPlayback,
8
8
  useMediaSchedule,
9
9
  useViewportOrientation
10
- } from "./chunk-NWZQGZIJ.js";
10
+ } from "./chunk-BOZJ655L.js";
11
11
  import {
12
12
  useAutoSurface
13
13
  } from "./chunk-TT6ENR6T.js";
@@ -17,7 +17,7 @@ import {
17
17
 
18
18
  // src/MediaClipLayer.tsx
19
19
  import { useEffect, useRef } from "react";
20
- import { jsx } from "react/jsx-runtime";
20
+ import { Fragment, jsx } from "react/jsx-runtime";
21
21
  var DRIFT = 0.25;
22
22
  function MediaClipLayer({
23
23
  schedule,
@@ -25,22 +25,44 @@ function MediaClipLayer({
25
25
  isPlaying,
26
26
  basePath,
27
27
  renderMode = false,
28
- muted = false
28
+ muted = false,
29
+ presentation = "background",
30
+ pipShape = "rounded",
31
+ pipPosition = "bottom-right",
32
+ honorClipPresentation = true
29
33
  }) {
30
34
  const { renderClips, activeIds } = useMediaSchedule(schedule, currentTime);
31
35
  if (renderClips.length === 0) return null;
32
- return /* @__PURE__ */ jsx("div", { className: "doc-player__media-clips", "aria-hidden": true, children: renderClips.map((clip) => /* @__PURE__ */ jsx(
33
- MediaClipElement,
36
+ const groups = /* @__PURE__ */ new Map();
37
+ for (const clip of renderClips) {
38
+ const clipPresentation = honorClipPresentation && clip.kind === "video" ? clip.placement === "picture-in-picture" ? "picture-in-picture" : clip.placement === "overlay" ? "full-frame" : presentation : presentation;
39
+ const group = groups.get(clipPresentation) ?? [];
40
+ group.push(clip);
41
+ groups.set(clipPresentation, group);
42
+ }
43
+ return /* @__PURE__ */ jsx(Fragment, { children: [...groups].map(([groupPresentation, clips]) => /* @__PURE__ */ jsx(
44
+ "div",
34
45
  {
35
- clip,
36
- active: activeIds.has(clip.id),
37
- currentTime,
38
- isPlaying,
39
- basePath,
40
- renderMode,
41
- muted
46
+ className: `doc-player__media-clips doc-player__media-clips--${groupPresentation}`,
47
+ "data-presentation": groupPresentation,
48
+ "data-pip-shape": pipShape,
49
+ "data-pip-position": pipPosition,
50
+ "aria-hidden": true,
51
+ children: clips.map((clip) => /* @__PURE__ */ jsx(
52
+ MediaClipElement,
53
+ {
54
+ clip,
55
+ active: activeIds.has(clip.id),
56
+ currentTime,
57
+ isPlaying,
58
+ basePath,
59
+ renderMode,
60
+ muted
61
+ },
62
+ clip.id
63
+ ))
42
64
  },
43
- clip.id
65
+ groupPresentation
44
66
  )) });
45
67
  }
46
68
  function MediaClipElement({
@@ -62,7 +84,7 @@ function MediaClipElement({
62
84
  return;
63
85
  }
64
86
  const target = Math.max(0, clip.sourceIn + (currentTime - clip.absoluteStart));
65
- if (renderMode || Math.abs(el.currentTime - target) > DRIFT) {
87
+ if (renderMode || !isPlaying || Math.abs(el.currentTime - target) > DRIFT) {
66
88
  try {
67
89
  el.currentTime = target;
68
90
  } catch {
@@ -91,17 +113,12 @@ function MediaClipElement({
91
113
  "video",
92
114
  {
93
115
  ...common,
94
- muted: true,
116
+ className: `doc-player__media-video${active ? " doc-player__media-video--active" : ""}`,
117
+ "data-active": active ? "true" : "false",
118
+ "data-video-placement": clip.placement ?? "default",
119
+ muted: renderMode || muted,
95
120
  playsInline: true,
96
- style: {
97
- position: "absolute",
98
- inset: 0,
99
- width: "100%",
100
- height: "100%",
101
- objectFit: "cover",
102
- zIndex: 0,
103
- pointerEvents: "none"
104
- }
121
+ style: { pointerEvents: "none" }
105
122
  }
106
123
  );
107
124
  }
@@ -1022,7 +1039,7 @@ function DocControlsSlideshow({
1022
1039
  }
1023
1040
 
1024
1041
  // src/DocPlayer.tsx
1025
- import { Fragment, useId as useId2, useRef as useRef5, useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo2 } from "react";
1042
+ import { Fragment as Fragment2, useId as useId2, useRef as useRef5, useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo2 } from "react";
1026
1043
  import {
1027
1044
  isTemplateBlock as isTemplateBlock2,
1028
1045
  getCaptionAtTime as getCaptionAtTime2,
@@ -1279,6 +1296,7 @@ function DocPlayerContent({
1279
1296
  renderMode = false,
1280
1297
  animationsEnabled = true,
1281
1298
  autoPlay = false,
1299
+ loop = false,
1282
1300
  onEnded,
1283
1301
  onTimeUpdate,
1284
1302
  audioController: externalAudioController,
@@ -1301,6 +1319,9 @@ function DocPlayerContent({
1301
1319
  theme,
1302
1320
  surface,
1303
1321
  captionStyle = "standard",
1322
+ videoPresentation = "background",
1323
+ pipShape = "rounded",
1324
+ pipPosition = "bottom-right",
1304
1325
  enableSwipe = true,
1305
1326
  globalKeyboardShortcuts = false
1306
1327
  }) {
@@ -1405,7 +1426,11 @@ function DocPlayerContent({
1405
1426
  } = useDocPlayback(doc, currentTime, {
1406
1427
  viewport: activeViewport,
1407
1428
  theme: effectiveTheme,
1408
- onSeek: seekTo
1429
+ onSeek: seekTo,
1430
+ // A synthetic track is only the timer used by an unnarrated preview.
1431
+ // Narration pacing may compact short visual beats, but it must never
1432
+ // remove authored slides from the default loss-averse projection.
1433
+ useAudioSegmentTiming: audioMode !== "synthetic"
1409
1434
  });
1410
1435
  const coverBlock = useMemo2(() => {
1411
1436
  const startBlockConfig = doc.startBlock;
@@ -1488,8 +1513,11 @@ function DocPlayerContent({
1488
1513
  useEffect4(() => {
1489
1514
  if (isEnded) {
1490
1515
  onEnded?.();
1516
+ if (loop && !isSlideshowMode && !isLinearMode) {
1517
+ void restart();
1518
+ }
1491
1519
  }
1492
- }, [isEnded, onEnded]);
1520
+ }, [isEnded, isLinearMode, isSlideshowMode, loop, onEnded, restart]);
1493
1521
  const liveRenderAPIRef = useRef5(null);
1494
1522
  const stableRenderAPIRef = useRef5(null);
1495
1523
  if (!stableRenderAPIRef.current) {
@@ -2000,6 +2028,7 @@ function DocPlayerContent({
2000
2028
  {
2001
2029
  ref: containerRef,
2002
2030
  "data-player-id": playerId,
2031
+ "data-orientation": orientation,
2003
2032
  tabIndex: renderMode ? -1 : 0,
2004
2033
  "aria-label": "Document player",
2005
2034
  onKeyDown: renderMode ? void 0 : handleKeyDown,
@@ -2027,7 +2056,10 @@ function DocPlayerContent({
2027
2056
  isPlaying,
2028
2057
  basePath,
2029
2058
  renderMode,
2030
- muted
2059
+ muted,
2060
+ presentation: videoPresentation,
2061
+ pipShape,
2062
+ pipPosition
2031
2063
  }
2032
2064
  ),
2033
2065
  /* @__PURE__ */ jsxs4("div", { className: "doc-player__viewport", children: [
@@ -2192,7 +2224,7 @@ function DocPlayerContent({
2192
2224
  hasCaptions && (() => {
2193
2225
  const debugPhrase = getCaptionAtTime2(doc.captions, currentTime);
2194
2226
  const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
2195
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
2227
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
2196
2228
  /* @__PURE__ */ jsxs4("div", { children: [
2197
2229
  /* @__PURE__ */ jsx7("span", { style: { color: "#888" }, children: "captions:" }),
2198
2230
  " ",
@@ -2465,6 +2497,7 @@ function DocPlayerWithSidebar({
2465
2497
  doc,
2466
2498
  basePath,
2467
2499
  autoPlay = false,
2500
+ loop = false,
2468
2501
  onEnded,
2469
2502
  onTimeUpdate,
2470
2503
  audioController,
@@ -2515,6 +2548,7 @@ function DocPlayerWithSidebar({
2515
2548
  theme,
2516
2549
  basePath,
2517
2550
  autoPlay,
2551
+ loop,
2518
2552
  onEnded,
2519
2553
  onTimeUpdate,
2520
2554
  audioController,
@@ -116,6 +116,13 @@ interface UseDocPlaybackOptions {
116
116
  theme?: Theme;
117
117
  /** Host seek callback used by block navigation actions. */
118
118
  onSeek?: (time: number) => void;
119
+ /**
120
+ * Whether real audio-segment timing should reshape the authored block
121
+ * sequence. Synthetic preview tracks are only a playback clock; treating
122
+ * them as narration can merge short authored slides to satisfy narration
123
+ * pacing rules.
124
+ */
125
+ useAudioSegmentTiming?: boolean;
119
126
  }
120
127
  declare function useDocPlayback(script: Doc | null, currentTime: number, options?: UseDocPlaybackOptions): PlaybackState & PlaybackActions;
121
128
 
@@ -6,7 +6,7 @@ import {
6
6
  useDocPlayback,
7
7
  useMediaSchedule,
8
8
  useViewportOrientation
9
- } from "../chunk-NWZQGZIJ.js";
9
+ } from "../chunk-BOZJ655L.js";
10
10
  import {
11
11
  useAutoSurface
12
12
  } from "../chunk-TT6ENR6T.js";
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SquisqRenderAPI } from './player/index.js';
2
- export { BlockMarker, BlockRenderer, CaptionMode, CaptionOverlay, CaptionStyle, ControlsLayout, DisplayMode, DocControlsBottom, DocControlsOverlay, DocControlsSidebar, DocControlsSlideshow, DocPlayer, DocPlayerProps, DocPlayerWithSidebar, DocProgressBar, InlineAudioPlayer, InlineAudioPlayerProps, InlineVideoPlayer, InlineVideoPlayerProps, MediaClipLayer, MediaClipLayerProps, PlaybackActions, PlaybackState, RenderAudioSegmentInfo, RenderBlockInfo, RenderCaptionInfo, RenderChapterInfo, SlideNavActions, SocialCaptionOverlay, formatTime } from './player/index.js';
2
+ export { BlockMarker, BlockRenderer, CaptionMode, CaptionOverlay, CaptionStyle, ControlsLayout, DisplayMode, DocControlsBottom, DocControlsOverlay, DocControlsSidebar, DocControlsSlideshow, DocPlayer, DocPlayerProps, DocPlayerWithSidebar, DocProgressBar, InlineAudioPlayer, InlineAudioPlayerProps, InlineVideoPlayer, InlineVideoPlayerProps, MediaClipLayer, MediaClipLayerProps, PipPosition, PipShape, PlaybackActions, PlaybackState, RenderAudioSegmentInfo, RenderBlockInfo, RenderCaptionInfo, RenderChapterInfo, SlideNavActions, SocialCaptionOverlay, VideoPresentation, formatTime } from './player/index.js';
3
3
  import { Theme } from '@bendyline/squisq/schemas';
4
4
  export { MarkdownRenderer, MermaidDiagram, MermaidDiagramProps } from './markdown/index.js';
5
5
  export { CanvasSection, CanvasSectionProps, ImageDisplayMode, LinearDocView, LinearDocViewProps, PageSectionView, PageSectionViewProps, PageViewContext, PageViewContextValue, usePageView } from './page/index.js';
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  MediaClipLayer,
11
11
  SocialCaptionOverlay,
12
12
  formatTime
13
- } from "./chunk-IUFWLNXS.js";
13
+ } from "./chunk-HML6PFH2.js";
14
14
  import {
15
15
  BlockRenderer,
16
16
  CanvasSection,
@@ -40,7 +40,7 @@ import {
40
40
  useDocPlayback,
41
41
  useMediaSchedule,
42
42
  useViewportOrientation
43
- } from "./chunk-NWZQGZIJ.js";
43
+ } from "./chunk-BOZJ655L.js";
44
44
  import {
45
45
  JsonView
46
46
  } from "./chunk-NANF7REM.js";
@@ -20,6 +20,17 @@ import { a as AudioController } from '../AudioController-DwMsPe38.js';
20
20
 
21
21
  /** Layout mode for doc player controls */
22
22
  type ControlsLayout = 'overlay' | 'sidebar' | 'bottom';
23
+ /**
24
+ * Placement of scheduled video relative to the rendered slide.
25
+ *
26
+ * `'background'` preserves the legacy full-bleed treatment behind the
27
+ * content. The other variants render above the content for presenter video.
28
+ */
29
+ type VideoPresentation = 'background' | 'full-frame' | 'picture-in-picture';
30
+ /** Shape of a picture-in-picture presenter video. */
31
+ type PipShape = 'square' | 'rounded' | 'circle';
32
+ /** Corner occupied by a picture-in-picture presenter video. */
33
+ type PipPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
23
34
  /**
24
35
  * Display mode for DocPlayer.
25
36
  *
@@ -178,6 +189,8 @@ interface DocPlayerProps {
178
189
  /** Receives the instance-scoped render API, and `null` on cleanup. */
179
190
  onRenderAPIReady?: (api: SquisqRenderAPI | null) => void;
180
191
  autoPlay?: boolean;
192
+ /** Restart Video-mode playback automatically when the timeline ends. */
193
+ loop?: boolean;
181
194
  onEnded?: () => void;
182
195
  onTimeUpdate?: (time: number) => void;
183
196
  /** Optional host-owned audio controller. */
@@ -205,6 +218,15 @@ interface DocPlayerProps {
205
218
  showCoverSlide?: boolean;
206
219
  coverVisible?: boolean;
207
220
  captionStyle?: CaptionStyle;
221
+ /**
222
+ * Placement of scheduled video relative to the slide content.
223
+ * Defaults to `'background'` for backward compatibility.
224
+ */
225
+ videoPresentation?: VideoPresentation;
226
+ /** Shape of presenter video in picture-in-picture mode. */
227
+ pipShape?: PipShape;
228
+ /** Corner used by presenter video in picture-in-picture mode. */
229
+ pipPosition?: PipPosition;
208
230
  enableSwipe?: boolean;
209
231
  globalKeyboardShortcuts?: boolean;
210
232
  }
@@ -327,6 +349,8 @@ interface DocPlayerWithSidebarProps {
327
349
  /** Base path for resolving media URLs (default: `'.'`) */
328
350
  basePath?: string;
329
351
  autoPlay?: boolean;
352
+ /** Restart playback automatically when the video timeline ends. */
353
+ loop?: boolean;
330
354
  onEnded?: () => void;
331
355
  onTimeUpdate?: (time: number) => void;
332
356
  /** Optional audio controller (if not provided, uses default HTML5 audio) */
@@ -349,7 +373,7 @@ interface DocPlayerWithSidebarProps {
349
373
  */
350
374
  theme?: Theme;
351
375
  }
352
- declare function DocPlayerWithSidebar({ doc, basePath, autoPlay, onEnded, onTimeUpdate, audioController, animationsEnabled, muted, captionsEnabled, isFullscreen, onFullscreenToggle, forceViewport, onPlayingChange, theme, }: DocPlayerWithSidebarProps): react_jsx_runtime.JSX.Element;
376
+ declare function DocPlayerWithSidebar({ doc, basePath, autoPlay, loop, onEnded, onTimeUpdate, audioController, animationsEnabled, muted, captionsEnabled, isFullscreen, onFullscreenToggle, forceViewport, onPlayingChange, theme, }: DocPlayerWithSidebarProps): react_jsx_runtime.JSX.Element;
353
377
 
354
378
  interface DocProgressBarProps {
355
379
  state: PlaybackState;
@@ -404,7 +428,15 @@ interface MediaClipLayerProps {
404
428
  renderMode?: boolean;
405
429
  /** Silence every scheduled clip during live playback. */
406
430
  muted?: boolean;
431
+ /** Placement of video clips relative to the rendered document. */
432
+ presentation?: VideoPresentation;
433
+ /** Shape of a picture-in-guide? */
434
+ pipShape?: PipShape;
435
+ /** Corner occupied by picture-in-picture video. */
436
+ pipPosition?: PipPosition;
437
+ /** Honor each scheduled video's authored placement override. Default true. */
438
+ honorClipPresentation?: boolean;
407
439
  }
408
- declare function MediaClipLayer({ schedule, currentTime, isPlaying, basePath, renderMode, muted, }: MediaClipLayerProps): react_jsx_runtime.JSX.Element | null;
440
+ declare function MediaClipLayer({ schedule, currentTime, isPlaying, basePath, renderMode, muted, presentation, pipShape, pipPosition, honorClipPresentation, }: MediaClipLayerProps): react_jsx_runtime.JSX.Element | null;
409
441
 
410
- export { type BlockMarker, BlockRenderer, type CaptionMode, CaptionOverlay, type CaptionStyle, type ControlsLayout, type DisplayMode, DocControlsBottom, DocControlsOverlay, DocControlsSidebar, DocControlsSlideshow, DocPlayer, type DocPlayerProps, DocPlayerWithSidebar, DocProgressBar, InlineAudioPlayer, type InlineAudioPlayerProps, InlineVideoPlayer, type InlineVideoPlayerProps, MediaClipLayer, type MediaClipLayerProps, type PlaybackActions, type PlaybackState, type RenderAudioSegmentInfo, type RenderBlockInfo, type RenderCaptionInfo, type RenderChapterInfo, type SlideNavActions, SocialCaptionOverlay, type SquisqRenderAPI, formatTime };
442
+ export { type BlockMarker, BlockRenderer, type CaptionMode, CaptionOverlay, type CaptionStyle, type ControlsLayout, type DisplayMode, DocControlsBottom, DocControlsOverlay, DocControlsSidebar, DocControlsSlideshow, DocPlayer, type DocPlayerProps, DocPlayerWithSidebar, DocProgressBar, InlineAudioPlayer, type InlineAudioPlayerProps, InlineVideoPlayer, type InlineVideoPlayerProps, MediaClipLayer, type MediaClipLayerProps, type PipPosition, type PipShape, type PlaybackActions, type PlaybackState, type RenderAudioSegmentInfo, type RenderBlockInfo, type RenderCaptionInfo, type RenderChapterInfo, type SlideNavActions, SocialCaptionOverlay, type SquisqRenderAPI, type VideoPresentation, formatTime };
@@ -10,12 +10,12 @@ import {
10
10
  MediaClipLayer,
11
11
  SocialCaptionOverlay,
12
12
  formatTime
13
- } from "../chunk-IUFWLNXS.js";
13
+ } from "../chunk-HML6PFH2.js";
14
14
  import {
15
15
  BlockRenderer
16
16
  } from "../chunk-M2HEFJZP.js";
17
17
  import "../chunk-XYS7HMP4.js";
18
- import "../chunk-NWZQGZIJ.js";
18
+ import "../chunk-BOZJ655L.js";
19
19
  import "../chunk-TT6ENR6T.js";
20
20
  import {
21
21
  InlineAudioPlayer,