@bendyline/squisq-react 2.4.6 → 2.5.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.
@@ -9,7 +9,7 @@ import {
9
9
  TreeLayer,
10
10
  VideoLayer,
11
11
  getTransitionClass
12
- } from "./chunk-TI5X7SBC.js";
12
+ } from "./chunk-UJEHDVQO.js";
13
13
  import {
14
14
  useAutoSurface
15
15
  } from "./chunk-TT6ENR6T.js";
@@ -239,7 +239,7 @@ function remapToKenBurns(anim) {
239
239
  }
240
240
 
241
241
  // src/layers/TextLayer.tsx
242
- import { useId, useMemo } from "react";
242
+ import { useId, useLayoutEffect, useMemo, useRef, useState } from "react";
243
243
  import { DEFAULT_DOC_FONT } from "@bendyline/squisq/schemas";
244
244
  import {
245
245
  parseHtmlToNodes,
@@ -454,6 +454,55 @@ function PlainTextLayer({ layer, viewport, blockTime }) {
454
454
  }
455
455
  const lineHeight = style.lineHeight || 1.4;
456
456
  const lineHeightPx = style.fontSize * lineHeight;
457
+ const shrinkToFit = style.shrinkToFit === true;
458
+ const centerBlock = shrinkToFit && dominantBaseline === "middle";
459
+ const firstLineDy = centerBlock ? -((lines.length - 1) / 2) * lineHeightPx : 0;
460
+ const textRef = useRef(null);
461
+ const [fit, setFit] = useState(null);
462
+ const linesKey = lines.join("\n");
463
+ useLayoutEffect(() => {
464
+ if (!shrinkToFit) {
465
+ setFit(null);
466
+ return;
467
+ }
468
+ const element = textRef.current;
469
+ if (!element || typeof element.getBBox !== "function") return;
470
+ const measure = () => {
471
+ try {
472
+ const bbox = element.getBBox();
473
+ if (bbox.width <= 0 || bbox.height <= 0) return;
474
+ const maxW = boxWidth ?? viewport.width;
475
+ const maxH = boxHeight ?? viewport.height;
476
+ const scale = Math.min(1, maxW / bbox.width, maxH / bbox.height);
477
+ setFit(
478
+ scale < 0.995 ? { scale, cx: bbox.x + bbox.width / 2, cy: bbox.y + bbox.height / 2 } : null
479
+ );
480
+ } catch {
481
+ }
482
+ };
483
+ measure();
484
+ const fonts = element.ownerDocument?.fonts;
485
+ if (fonts && fonts.status !== "loaded") {
486
+ let cancelled = false;
487
+ void fonts.ready.then(() => {
488
+ if (!cancelled) measure();
489
+ });
490
+ return () => {
491
+ cancelled = true;
492
+ };
493
+ }
494
+ }, [
495
+ shrinkToFit,
496
+ linesKey,
497
+ boxWidth,
498
+ boxHeight,
499
+ viewport.width,
500
+ viewport.height,
501
+ style.fontSize,
502
+ style.fontFamily,
503
+ style.fontWeight
504
+ ]);
505
+ const fitTransform = fit ? `translate(${fit.cx} ${fit.cy}) scale(${fit.scale}) translate(${-fit.cx} ${-fit.cy})` : void 0;
457
506
  const textStyles = {
458
507
  fontSize: `${style.fontSize}px`,
459
508
  fontFamily: style.fontFamily || DEFAULT_DOC_FONT,
@@ -486,13 +535,15 @@ function PlainTextLayer({ layer, viewport, blockTime }) {
486
535
  /* @__PURE__ */ jsx3(
487
536
  "text",
488
537
  {
538
+ ref: textRef,
489
539
  x,
490
540
  y,
491
541
  textAnchor,
492
542
  dominantBaseline,
493
543
  style: textStyles,
494
544
  filter: filterId ? `url(#${filterId})` : void 0,
495
- children: lines.map((line, i) => /* @__PURE__ */ jsxs2("tspan", { x, dy: i === 0 ? 0 : lineHeightPx, children: [
545
+ transform: fitTransform,
546
+ children: lines.map((line, i) => /* @__PURE__ */ jsxs2("tspan", { x, dy: i === 0 ? firstLineDy : lineHeightPx, children: [
496
547
  line || "\xA0",
497
548
  " "
498
549
  ] }, i))
@@ -893,7 +944,7 @@ function MarkerDef({
893
944
  }
894
945
 
895
946
  // src/layers/MapLayer.tsx
896
- import { useId as useId4, useState, useEffect } from "react";
947
+ import { useId as useId4, useState as useState2, useEffect } from "react";
897
948
  import { ResourcePolicyError as ResourcePolicyError2 } from "@bendyline/squisq/markdown";
898
949
 
899
950
  // src/utils/mapTileUtils.ts
@@ -1092,10 +1143,10 @@ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1092
1143
  function MapLayer({ layer, basePath, viewport, blockTime }) {
1093
1144
  const { content, position, animation } = layer;
1094
1145
  const clipId = `map-clip-${useId4().replace(/:/g, "")}-${layer.id}`;
1095
- const [mapImageUrl, setMapImageUrl] = useState(null);
1096
- const [isLoading, setIsLoading] = useState(true);
1097
- const [error, setError] = useState(null);
1098
- const [blockedByPolicy, setBlockedByPolicy] = useState(false);
1146
+ const [mapImageUrl, setMapImageUrl] = useState2(null);
1147
+ const [isLoading, setIsLoading] = useState2(true);
1148
+ const [error, setError] = useState2(null);
1149
+ const [blockedByPolicy, setBlockedByPolicy] = useState2(false);
1099
1150
  const x = resolveValue(position.x, viewport.width);
1100
1151
  const y = resolveValue(position.y, viewport.height);
1101
1152
  const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
@@ -1242,7 +1293,7 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1242
1293
  }
1243
1294
 
1244
1295
  // src/layers/VideoLayer.tsx
1245
- import { useRef, useEffect as useEffect2 } from "react";
1296
+ import { useRef as useRef2, useEffect as useEffect2 } from "react";
1246
1297
  import { jsx as jsx7 } from "react/jsx-runtime";
1247
1298
  var VIDEO_SYNC_DRIFT_SECONDS = 0.2;
1248
1299
  function VideoLayer({
@@ -1254,8 +1305,8 @@ function VideoLayer({
1254
1305
  muted = false
1255
1306
  }) {
1256
1307
  const { content, position } = layer;
1257
- const videoRef = useRef(null);
1258
- const hasStartedRef = useRef(false);
1308
+ const videoRef = useRef2(null);
1309
+ const hasStartedRef = useRef2(false);
1259
1310
  const startAt = content.startAt ?? 0;
1260
1311
  const gated = blockTime < startAt;
1261
1312
  const x = resolveValue(position.x, viewport.width);
@@ -1296,7 +1347,7 @@ function VideoLayer({
1296
1347
  const video = videoRef.current;
1297
1348
  if (!video || !hasStartedRef.current) return;
1298
1349
  const targetTime = gated ? content.clipStart : Math.min(content.clipEnd, content.clipStart + Math.max(0, blockTime - startAt));
1299
- if (Math.abs(video.currentTime - targetTime) > VIDEO_SYNC_DRIFT_SECONDS) {
1350
+ if (video.dataset.captureSequential !== "true" && Math.abs(video.currentTime - targetTime) > VIDEO_SYNC_DRIFT_SECONDS) {
1300
1351
  video.currentTime = targetTime;
1301
1352
  }
1302
1353
  if (gated) {
@@ -1313,7 +1364,7 @@ function VideoLayer({
1313
1364
  playPromise.catch(() => {
1314
1365
  });
1315
1366
  }
1316
- } else {
1367
+ } else if (video.dataset.captureSequential !== "true") {
1317
1368
  video.pause();
1318
1369
  }
1319
1370
  }, [isPlaying, gated, blockTime, startAt, src, content.clipStart, content.clipEnd]);
@@ -1434,7 +1485,7 @@ function TableLayer({ layer, viewport, blockTime }) {
1434
1485
  }
1435
1486
 
1436
1487
  // src/layers/TreeLayer.tsx
1437
- import { useState as useState2 } from "react";
1488
+ import { useState as useState3 } from "react";
1438
1489
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1439
1490
  function faClass(token, fallback) {
1440
1491
  const name = token && token.trim() ? token.trim() : fallback;
@@ -1508,7 +1559,7 @@ function TreeRow({
1508
1559
  style
1509
1560
  }) {
1510
1561
  const hasChildren = item.children.length > 0;
1511
- const [collapsed, setCollapsed] = useState2(false);
1562
+ const [collapsed, setCollapsed] = useState3(false);
1512
1563
  const isDir = item.isDir || hasChildren;
1513
1564
  const iconCls = isDir ? faClass(style.folderIcon, collapsed ? "folder" : "folder-open") : faClass(style.fileIcon, "file");
1514
1565
  return /* @__PURE__ */ jsxs7("li", { style: { position: "relative" }, children: [
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BlockRenderer,
3
3
  LinearDocView
4
- } from "./chunk-ICM7AWQT.js";
4
+ } from "./chunk-THDXCSPC.js";
5
5
  import {
6
6
  useAudioSync,
7
7
  useDocPlayback,
@@ -165,7 +165,7 @@ function MediaClipElement({
165
165
  return;
166
166
  }
167
167
  const target = Math.max(0, clip.sourceIn + (currentTime - clip.absoluteStart));
168
- if (renderMode || !isPlaying || Math.abs(el.currentTime - target) > DRIFT) {
168
+ if (el.dataset.captureSequential !== "true" && (renderMode || !isPlaying || Math.abs(el.currentTime - target) > DRIFT)) {
169
169
  try {
170
170
  el.currentTime = target;
171
171
  } catch {
@@ -175,7 +175,7 @@ function MediaClipElement({
175
175
  const p = el.play();
176
176
  if (p) p.catch(() => {
177
177
  });
178
- } else {
178
+ } else if (el.dataset.captureSequential !== "true") {
179
179
  el.pause();
180
180
  }
181
181
  }, [active, currentTime, isPlaying, renderMode, clip.sourceIn, clip.absoluteStart, src]);
@@ -1243,7 +1243,10 @@ function DocControlsSlideshow({
1243
1243
  }
1244
1244
 
1245
1245
  // src/docPlayer/playerAppearance.ts
1246
- import { resolveThemeForDoc } from "@bendyline/squisq/doc";
1246
+ import {
1247
+ resolveCoverSlideSettings,
1248
+ resolveThemeForDoc
1249
+ } from "@bendyline/squisq/doc";
1247
1250
  function readFrontmatterSetting(frontmatter, canonical, legacy) {
1248
1251
  if (!frontmatter) return void 0;
1249
1252
  return Object.prototype.hasOwnProperty.call(frontmatter, canonical) ? frontmatter[canonical] : frontmatter[legacy];
@@ -1291,20 +1294,14 @@ function resolvePipPosition(value) {
1291
1294
  };
1292
1295
  return aliases[normalized];
1293
1296
  }
1294
- function resolveBoolean(value) {
1295
- if (typeof value === "boolean") return value;
1296
- if (typeof value !== "string") return void 0;
1297
- const normalized = value.trim().toLowerCase();
1298
- if (normalized === "true" || normalized === "yes" || normalized === "on" || normalized === "show" || normalized === "visible") {
1299
- return true;
1300
- }
1301
- if (normalized === "false" || normalized === "no" || normalized === "off" || normalized === "hide" || normalized === "hidden") {
1302
- return false;
1303
- }
1304
- return void 0;
1305
- }
1306
1297
  function resolveDocPlayerAppearance(doc, overrides = {}) {
1307
1298
  const frontmatter = doc.frontmatter;
1299
+ const cover = resolveCoverSlideSettings(frontmatter, {
1300
+ ...overrides.showCoverSlide !== void 0 ? { enabled: overrides.showCoverSlide } : {},
1301
+ ...overrides.coverSlideTemplate !== void 0 ? { template: overrides.coverSlideTemplate } : {},
1302
+ ...overrides.coverSlideDuration !== void 0 ? { duration: overrides.coverSlideDuration } : {},
1303
+ ...overrides.coverSlidePlayback !== void 0 ? { playback: overrides.coverSlidePlayback } : {}
1304
+ });
1308
1305
  return {
1309
1306
  theme: overrides.theme ?? resolveThemeForDoc(doc),
1310
1307
  videoPresentation: overrides.videoPresentation ?? resolveVideoPresentation(
@@ -1315,7 +1312,10 @@ function resolveDocPlayerAppearance(doc, overrides = {}) {
1315
1312
  pipPosition: overrides.pipPosition ?? resolvePipPosition(
1316
1313
  readFrontmatterSetting(frontmatter, "squisq-pip-position", "pip-position")
1317
1314
  ) ?? "bottom-right",
1318
- showCoverSlide: overrides.showCoverSlide ?? resolveBoolean(readFrontmatterSetting(frontmatter, "squisq-cover-slide", "cover-slide")) ?? true
1315
+ showCoverSlide: cover.enabled,
1316
+ coverSlideTemplate: cover.template,
1317
+ coverSlideDuration: cover.duration,
1318
+ coverSlidePlayback: cover.playback
1319
1319
  };
1320
1320
  }
1321
1321
 
@@ -1542,6 +1542,8 @@ function buildSegmentTitleMap(doc) {
1542
1542
  var DEFAULT_VIDEO_FRAME_TIMEOUT_MS = 2e3;
1543
1543
  var VIDEO_FRAME_READINESS_POLL_MS = 16;
1544
1544
  var VIDEO_TIME_TOLERANCE_SECONDS = 0.01;
1545
+ var MAX_SEQUENTIAL_CAPTURE_STEP_SECONDS = 0.5;
1546
+ var SEQUENTIAL_CAPTURE_PLAYBACK_RATE = 1;
1545
1547
  function formatMediaTime(time) {
1546
1548
  return Number.isFinite(time) ? `${time.toFixed(3)}s` : String(time);
1547
1549
  }
@@ -1565,19 +1567,36 @@ function isVisiblyPresented(video) {
1565
1567
  }
1566
1568
  return true;
1567
1569
  }
1568
- function seekVideoToFrame(video, targetTime, timeoutMs = DEFAULT_VIDEO_FRAME_TIMEOUT_MS) {
1570
+ function createCaptureWatchdog(ownerDocument, timeoutMs, expire) {
1571
+ let timeout = null;
1572
+ const clear = () => {
1573
+ if (timeout === null) return;
1574
+ clearTimeout(timeout);
1575
+ timeout = null;
1576
+ };
1577
+ return {
1578
+ arm: () => {
1579
+ clear();
1580
+ if (ownerDocument.visibilityState !== "visible") return;
1581
+ timeout = setTimeout(expire, timeoutMs);
1582
+ },
1583
+ clear
1584
+ };
1585
+ }
1586
+ function seekVideoByAssignment(video, targetTime, timeoutMs = DEFAULT_VIDEO_FRAME_TIMEOUT_MS) {
1569
1587
  video.pause();
1570
- const alreadyReady = !video.seeking && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && isAtReachableMediaTime(video, targetTime);
1571
- if (alreadyReady) return Promise.resolve();
1572
1588
  return new Promise((resolve, reject) => {
1573
1589
  let settled = false;
1574
1590
  let videoFrameRequest = null;
1591
+ const ownerDocument = video.ownerDocument;
1592
+ const isInactive = () => ownerDocument.visibilityState !== "visible";
1575
1593
  const cleanup = () => {
1576
- clearTimeout(timeout);
1594
+ watchdog.clear();
1577
1595
  clearInterval(readinessPoll);
1578
1596
  video.removeEventListener("seeked", handleMediaReady);
1579
1597
  video.removeEventListener("loadeddata", handleMediaReady);
1580
1598
  video.removeEventListener("canplay", handleMediaReady);
1599
+ ownerDocument.removeEventListener("visibilitychange", handleVisibilityChange);
1581
1600
  if (videoFrameRequest !== null && typeof video.cancelVideoFrameCallback === "function") {
1582
1601
  video.cancelVideoFrameCallback(videoFrameRequest);
1583
1602
  }
@@ -1615,21 +1634,150 @@ function seekVideoToFrame(video, targetTime, timeoutMs = DEFAULT_VIDEO_FRAME_TIM
1615
1634
  function handleMediaReady() {
1616
1635
  requestPresentedFrame();
1617
1636
  }
1618
- const timeout = setTimeout(fail, timeoutMs);
1637
+ const watchdog = createCaptureWatchdog(ownerDocument, timeoutMs, fail);
1638
+ const assignTargetTime = () => {
1639
+ try {
1640
+ video.currentTime = reachableMediaTime(video, targetTime);
1641
+ queueMicrotask(requestPresentedFrame);
1642
+ return true;
1643
+ } catch (error) {
1644
+ settled = true;
1645
+ cleanup();
1646
+ reject(error);
1647
+ return false;
1648
+ }
1649
+ };
1650
+ function handleVisibilityChange() {
1651
+ if (settled) return;
1652
+ if (isInactive()) {
1653
+ watchdog.clear();
1654
+ return;
1655
+ }
1656
+ requestPresentedFrame();
1657
+ if (settled) return;
1658
+ if (!video.seeking && !isAtReachableMediaTime(video, targetTime) && !assignTargetTime()) {
1659
+ return;
1660
+ }
1661
+ watchdog.arm();
1662
+ }
1619
1663
  const readinessPoll = setInterval(requestPresentedFrame, VIDEO_FRAME_READINESS_POLL_MS);
1620
1664
  video.addEventListener("seeked", handleMediaReady);
1621
1665
  video.addEventListener("loadeddata", handleMediaReady);
1622
1666
  video.addEventListener("canplay", handleMediaReady);
1623
- try {
1624
- video.currentTime = reachableMediaTime(video, targetTime);
1625
- queueMicrotask(requestPresentedFrame);
1626
- } catch (error) {
1667
+ ownerDocument.addEventListener("visibilitychange", handleVisibilityChange);
1668
+ watchdog.arm();
1669
+ assignTargetTime();
1670
+ });
1671
+ }
1672
+ function advanceSequentialCaptureVideo(video, targetTime, timeoutMs) {
1673
+ return new Promise((resolve, reject) => {
1674
+ let settled = false;
1675
+ const ownerDocument = video.ownerDocument;
1676
+ const isInactive = () => ownerDocument.visibilityState !== "visible";
1677
+ const cleanup = () => {
1678
+ watchdog.clear();
1679
+ clearInterval(readinessPoll);
1680
+ video.removeEventListener("timeupdate", check);
1681
+ video.removeEventListener("loadeddata", check);
1682
+ video.removeEventListener("canplay", check);
1683
+ video.removeEventListener("error", fail);
1684
+ ownerDocument.removeEventListener("visibilitychange", handleVisibilityChange);
1685
+ };
1686
+ const finish = () => {
1687
+ if (!settled) {
1688
+ settled = true;
1689
+ cleanup();
1690
+ resolve();
1691
+ }
1692
+ };
1693
+ const fallbackToSeek = () => {
1694
+ if (settled) return;
1695
+ settled = true;
1696
+ video.pause();
1697
+ cleanup();
1698
+ void seekVideoByAssignment(video, targetTime, timeoutMs).then(resolve, reject);
1699
+ };
1700
+ const fail = () => {
1701
+ if (settled) return;
1627
1702
  settled = true;
1703
+ video.pause();
1628
1704
  cleanup();
1629
- reject(error);
1705
+ reject(
1706
+ new Error(
1707
+ `Video frame did not advance to ${formatMediaTime(targetTime)} within ${timeoutMs}ms (currentTime=${formatMediaTime(video.currentTime)}, readyState=${video.readyState}).`
1708
+ )
1709
+ );
1710
+ };
1711
+ function check() {
1712
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.currentTime >= reachableMediaTime(video, targetTime)) {
1713
+ video.pause();
1714
+ finish();
1715
+ }
1716
+ }
1717
+ const handlePlaybackFailure = () => {
1718
+ if (settled || isInactive()) return;
1719
+ fallbackToSeek();
1720
+ };
1721
+ const startPlayback = () => {
1722
+ if (settled || isInactive()) return;
1723
+ try {
1724
+ video.playbackRate = SEQUENTIAL_CAPTURE_PLAYBACK_RATE;
1725
+ if (video.paused) {
1726
+ void video.play().catch(handlePlaybackFailure);
1727
+ }
1728
+ } catch {
1729
+ handlePlaybackFailure();
1730
+ }
1731
+ };
1732
+ const watchdog = createCaptureWatchdog(ownerDocument, timeoutMs, fail);
1733
+ const pauseWhileInactive = () => {
1734
+ video.pause();
1735
+ watchdog.clear();
1736
+ };
1737
+ const resumeWhenActive = () => {
1738
+ if (settled || isInactive()) return;
1739
+ check();
1740
+ if (settled) return;
1741
+ startPlayback();
1742
+ watchdog.arm();
1743
+ };
1744
+ function handleVisibilityChange() {
1745
+ if (ownerDocument.visibilityState !== "visible") {
1746
+ pauseWhileInactive();
1747
+ return;
1748
+ }
1749
+ resumeWhenActive();
1630
1750
  }
1751
+ const readinessPoll = setInterval(check, VIDEO_FRAME_READINESS_POLL_MS);
1752
+ video.addEventListener("timeupdate", check);
1753
+ video.addEventListener("loadeddata", check);
1754
+ video.addEventListener("canplay", check);
1755
+ video.addEventListener("error", fail, { once: true });
1756
+ ownerDocument.addEventListener("visibilitychange", handleVisibilityChange);
1757
+ resumeWhenActive();
1631
1758
  });
1632
1759
  }
1760
+ function seekVideoToFrame(video, targetTime, timeoutMs = DEFAULT_VIDEO_FRAME_TIMEOUT_MS) {
1761
+ if (video.readyState >= HTMLMediaElement.HAVE_METADATA && video.videoWidth <= 0 && video.videoHeight <= 0) {
1762
+ video.pause();
1763
+ return Promise.resolve();
1764
+ }
1765
+ const reachableTargetTime = reachableMediaTime(video, targetTime);
1766
+ const step = reachableTargetTime - video.currentTime;
1767
+ if (video.dataset.captureSequential === "true") {
1768
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && step <= VIDEO_TIME_TOLERANCE_SECONDS) {
1769
+ if (step < -VIDEO_TIME_TOLERANCE_SECONDS) video.pause();
1770
+ return Promise.resolve();
1771
+ }
1772
+ if (step > 0 && step <= MAX_SEQUENTIAL_CAPTURE_STEP_SECONDS && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1773
+ return advanceSequentialCaptureVideo(video, targetTime, timeoutMs);
1774
+ }
1775
+ }
1776
+ video.pause();
1777
+ const alreadyReady = !video.seeking && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && isAtReachableMediaTime(video, targetTime);
1778
+ if (alreadyReady) return Promise.resolve();
1779
+ return seekVideoByAssignment(video, targetTime, timeoutMs);
1780
+ }
1633
1781
 
1634
1782
  // src/DocPlayer.tsx
1635
1783
  import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -1699,6 +1847,9 @@ function DocPlayerContent({
1699
1847
  forceViewport,
1700
1848
  displayMode = "video",
1701
1849
  showCoverSlide,
1850
+ coverSlideTemplate,
1851
+ coverSlideDuration,
1852
+ coverSlidePlayback,
1702
1853
  coverVisible,
1703
1854
  theme,
1704
1855
  surface,
@@ -1854,9 +2005,23 @@ function DocPlayerContent({
1854
2005
  pipSize,
1855
2006
  pipShape,
1856
2007
  pipPosition,
1857
- showCoverSlide
2008
+ showCoverSlide,
2009
+ coverSlideTemplate,
2010
+ coverSlideDuration,
2011
+ coverSlidePlayback
1858
2012
  }),
1859
- [doc, theme, videoPresentation, pipSize, pipShape, pipPosition, showCoverSlide]
2013
+ [
2014
+ doc,
2015
+ theme,
2016
+ videoPresentation,
2017
+ pipSize,
2018
+ pipShape,
2019
+ pipPosition,
2020
+ showCoverSlide,
2021
+ coverSlideTemplate,
2022
+ coverSlideDuration,
2023
+ coverSlidePlayback
2024
+ ]
1860
2025
  );
1861
2026
  const effectiveTheme = useMemo3(() => {
1862
2027
  const base = appearance.theme;
@@ -1899,7 +2064,7 @@ function DocPlayerContent({
1899
2064
  if (!appearance.showCoverSlide) return null;
1900
2065
  if (!startBlockConfig) return null;
1901
2066
  const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
1902
- const layers = expandCoverBlock(startBlockConfig, context);
2067
+ const layers = expandCoverBlock(startBlockConfig, context, appearance.coverSlideTemplate);
1903
2068
  return {
1904
2069
  id: "cover-block",
1905
2070
  startTime: -1,
@@ -1909,7 +2074,13 @@ function DocPlayerContent({
1909
2074
  audioSegment: -1,
1910
2075
  layers
1911
2076
  };
1912
- }, [doc.startBlock, activeViewport, effectiveTheme, appearance.showCoverSlide]);
2077
+ }, [
2078
+ doc.startBlock,
2079
+ activeViewport,
2080
+ effectiveTheme,
2081
+ appearance.showCoverSlide,
2082
+ appearance.coverSlideTemplate
2083
+ ]);
1913
2084
  const hasManagedCover = !!coverBlock;
1914
2085
  const [slideshowCoverVisible, setSlideshowCoverVisible] = useState5(false);
1915
2086
  const [isSlideshowPickerOpen, setIsSlideshowPickerOpen] = useState5(false);
@@ -1947,9 +2118,12 @@ function DocPlayerContent({
1947
2118
  coverWasShowing.current = false;
1948
2119
  hasPlayedOnce.current = true;
1949
2120
  setCoverGraceActive(true);
1950
- coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
2121
+ coverGraceTimer.current = setTimeout(
2122
+ () => setCoverGraceActive(false),
2123
+ appearance.coverSlideDuration * 1e3
2124
+ );
1951
2125
  }
1952
- }, [isPlaying, coverBlock, renderMode, isSlideshowMode]);
2126
+ }, [isPlaying, coverBlock, renderMode, isSlideshowMode, appearance.coverSlideDuration]);
1953
2127
  useEffect5(() => () => clearTimeout(coverGraceTimer.current), []);
1954
2128
  useEffect5(() => () => clearTimeout(tapFeedbackTimer.current), []);
1955
2129
  const showVideoCoverBlock = !isSlideshowMode && !isLinearMode && !!coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
package/dist/index.d.ts CHANGED
@@ -4,9 +4,10 @@ import { Theme, VideoPresentation, VideoPipSize, VideoPipShape, VideoPipPosition
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';
6
6
  export { ImageLayer, MapLayer, MermaidLayer, PathLayer, ShapeLayer, TableLayer, TextLayer, TreeLayer, VideoLayer } from './layers/index.js';
7
+ import { CoverSlideTemplate, CoverSlidePlayback } from '@bendyline/squisq/doc';
8
+ export { getAnimationStyle, getTransitionClass } from '@bendyline/squisq/doc';
7
9
  export { MediaContext, MediaScheduleController, ModalDialogOptions, ResourcePolicyContext, UseDocPlaybackOptions, useAudioSync, useAutoSurface, useDocPlayback, useMediaProvider, useMediaSchedule, useMediaUrl, useModalDialog, useResourcePolicy, useViewportOrientation } from './hooks/index.js';
8
10
  export { A as AudioActions, a as AudioController, b as AudioState } from './AudioController-DwMsPe38.js';
9
- export { getAnimationStyle, getTransitionClass } from '@bendyline/squisq/doc';
10
11
  export { JsonView, JsonViewProps } from './json-view/index.js';
11
12
  import 'react/jsx-runtime';
12
13
  import 'react';
@@ -103,6 +104,9 @@ interface DocPlayerAppearanceOverrides {
103
104
  pipShape?: PipShape;
104
105
  pipPosition?: PipPosition;
105
106
  showCoverSlide?: boolean;
107
+ coverSlideTemplate?: CoverSlideTemplate;
108
+ coverSlideDuration?: number;
109
+ coverSlidePlayback?: CoverSlidePlayback;
106
110
  }
107
111
  interface ResolvedDocPlayerAppearance {
108
112
  theme: Theme;
@@ -111,6 +115,9 @@ interface ResolvedDocPlayerAppearance {
111
115
  pipShape: PipShape;
112
116
  pipPosition: PipPosition;
113
117
  showCoverSlide: boolean;
118
+ coverSlideTemplate: CoverSlideTemplate;
119
+ coverSlideDuration: number;
120
+ coverSlidePlayback: CoverSlidePlayback;
114
121
  }
115
122
  /** Resolve the visual settings a standalone/export player must inherit from its Doc. */
116
123
  declare function resolveDocPlayerAppearance(doc: Doc, overrides?: DocPlayerAppearanceOverrides): ResolvedDocPlayerAppearance;
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  formatTime,
13
13
  resolveDocPlayerAppearance,
14
14
  useMediaClipDurations
15
- } from "./chunk-7CDO336T.js";
15
+ } from "./chunk-ZKNX3EJI.js";
16
16
  import {
17
17
  BlockRenderer,
18
18
  CanvasSection,
@@ -20,7 +20,7 @@ import {
20
20
  PageSectionView,
21
21
  PageViewContext,
22
22
  usePageView
23
- } from "./chunk-ICM7AWQT.js";
23
+ } from "./chunk-THDXCSPC.js";
24
24
  import {
25
25
  ImageLayer,
26
26
  MapLayer,
@@ -33,7 +33,7 @@ import {
33
33
  VideoLayer,
34
34
  getAnimationStyle,
35
35
  getTransitionClass
36
- } from "./chunk-TI5X7SBC.js";
36
+ } from "./chunk-UJEHDVQO.js";
37
37
  import {
38
38
  useModalDialog
39
39
  } from "./chunk-65POMKHR.js";
@@ -8,7 +8,7 @@ import {
8
8
  TextLayer,
9
9
  TreeLayer,
10
10
  VideoLayer
11
- } from "../chunk-TI5X7SBC.js";
11
+ } from "../chunk-UJEHDVQO.js";
12
12
  import "../chunk-WLUZTUNZ.js";
13
13
  import "../chunk-LR3AIGDD.js";
14
14
  export {
@@ -4,8 +4,8 @@ import {
4
4
  PageSectionView,
5
5
  PageViewContext,
6
6
  usePageView
7
- } from "../chunk-ICM7AWQT.js";
8
- import "../chunk-TI5X7SBC.js";
7
+ } from "../chunk-THDXCSPC.js";
8
+ import "../chunk-UJEHDVQO.js";
9
9
  import "../chunk-TT6ENR6T.js";
10
10
  import "../chunk-VMPQEUJH.js";
11
11
  import "../chunk-WLUZTUNZ.js";
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { Block, VideoPresentation as VideoPresentation$1, VideoPipSize, VideoPipShape, VideoPipPosition, Doc, Theme, SurfaceScheme, Transition, CaptionTrack, ViewportConfig as ViewportConfig$1, ScheduledClip } from '@bendyline/squisq/schemas';
3
- import { ViewportConfig } from '@bendyline/squisq/doc';
3
+ import { ViewportConfig, CoverSlideTemplate, CoverSlidePlayback } from '@bendyline/squisq/doc';
4
4
  import { a as AudioController } from '../AudioController-DwMsPe38.js';
5
5
  import { CSSProperties } from 'react';
6
6
 
@@ -222,6 +222,12 @@ interface DocPlayerProps {
222
222
  /** Video, manual slideshow, or long-scrolling linear rendition. */
223
223
  displayMode?: DisplayMode;
224
224
  showCoverSlide?: boolean;
225
+ /** Visual template used to materialize the managed cover. */
226
+ coverSlideTemplate?: CoverSlideTemplate;
227
+ /** Seconds the cover remains visible after Video playback starts. */
228
+ coverSlideDuration?: number;
229
+ /** Whether video export advances or delays the story while the cover is visible. */
230
+ coverSlidePlayback?: CoverSlidePlayback;
225
231
  coverVisible?: boolean;
226
232
  captionStyle?: CaptionStyle;
227
233
  /**
@@ -10,11 +10,11 @@ import {
10
10
  MediaClipLayer,
11
11
  SocialCaptionOverlay,
12
12
  formatTime
13
- } from "../chunk-7CDO336T.js";
13
+ } from "../chunk-ZKNX3EJI.js";
14
14
  import {
15
15
  BlockRenderer
16
- } from "../chunk-ICM7AWQT.js";
17
- import "../chunk-TI5X7SBC.js";
16
+ } from "../chunk-THDXCSPC.js";
17
+ import "../chunk-UJEHDVQO.js";
18
18
  import "../chunk-BOZJ655L.js";
19
19
  import "../chunk-TT6ENR6T.js";
20
20
  import {