@bendyline/squisq-react 2.0.1 → 2.2.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +6 -0
  4. package/dist/index.d.ts +101 -96
  5. package/dist/index.js +1483 -782
  6. package/dist/squisq-player.full.global.js +4165 -0
  7. package/dist/squisq-player.global.js +561 -168
  8. package/dist/standalone-source.d.ts +1 -0
  9. package/dist/standalone-source.js +1 -1
  10. package/dist/styles/index.css +64 -0
  11. package/package.json +8 -4
  12. package/dist/index.js.map +0 -1
  13. package/dist/squisq-player.css +0 -2
  14. package/dist/squisq-player.css.map +0 -1
  15. package/dist/squisq-player.global.js.map +0 -1
  16. package/src/BlockRenderer.tsx +0 -194
  17. package/src/CaptionOverlay.tsx +0 -111
  18. package/src/DocControlsBottom.tsx +0 -109
  19. package/src/DocControlsOverlay.tsx +0 -190
  20. package/src/DocControlsSidebar.tsx +0 -113
  21. package/src/DocControlsSlideshow.tsx +0 -360
  22. package/src/DocPlayer.tsx +0 -1528
  23. package/src/DocPlayerWithSidebar.tsx +0 -155
  24. package/src/DocProgressBar.tsx +0 -257
  25. package/src/InlineAudioPlayer.tsx +0 -46
  26. package/src/InlineVideoPlayer.tsx +0 -70
  27. package/src/LinearDocView.tsx +0 -569
  28. package/src/MarkdownRenderer.tsx +0 -624
  29. package/src/MediaClipLayer.tsx +0 -145
  30. package/src/SocialCaptionOverlay.tsx +0 -255
  31. package/src/__tests__/BlockRenderer.test.tsx +0 -235
  32. package/src/__tests__/DocControlsSlideshow.test.tsx +0 -221
  33. package/src/__tests__/DocPlayer.test.tsx +0 -635
  34. package/src/__tests__/DocPlayerStylesSentinel.test.tsx +0 -41
  35. package/src/__tests__/DocProgressBar.test.tsx +0 -102
  36. package/src/__tests__/JsonView.test.tsx +0 -111
  37. package/src/__tests__/LinearDocView.test.tsx +0 -426
  38. package/src/__tests__/MapLayer.test.tsx +0 -63
  39. package/src/__tests__/MarkdownRenderer.test.tsx +0 -357
  40. package/src/__tests__/MediaClipLayer.test.tsx +0 -70
  41. package/src/__tests__/MediaContext.test.tsx +0 -51
  42. package/src/__tests__/PathLayer.test.tsx +0 -84
  43. package/src/__tests__/TableLayer.test.tsx +0 -142
  44. package/src/__tests__/VideoLayer.test.tsx +0 -94
  45. package/src/__tests__/exports.test.ts +0 -55
  46. package/src/__tests__/fillStyle.test.tsx +0 -160
  47. package/src/__tests__/standaloneEntry.test.tsx +0 -103
  48. package/src/__tests__/transitionStyles.test.ts +0 -125
  49. package/src/__tests__/useAudioSync.test.ts +0 -49
  50. package/src/__tests__/useDocPlayback.transition.test.ts +0 -113
  51. package/src/__tests__/useJsonViewTokens.test.ts +0 -41
  52. package/src/__tests__/useSlideSwipe.test.ts +0 -81
  53. package/src/__tests__/useViewportOrientation.test.ts +0 -22
  54. package/src/hooks/AudioController.ts +0 -114
  55. package/src/hooks/MediaContext.tsx +0 -97
  56. package/src/hooks/index.ts +0 -11
  57. package/src/hooks/useAudioSync.ts +0 -456
  58. package/src/hooks/useAutoSurface.ts +0 -33
  59. package/src/hooks/useDocPlayback.ts +0 -265
  60. package/src/hooks/useMediaSchedule.ts +0 -39
  61. package/src/hooks/useSlideSwipe.ts +0 -265
  62. package/src/hooks/useViewportOrientation.ts +0 -115
  63. package/src/index.ts +0 -70
  64. package/src/jsonView/JsonView.tsx +0 -51
  65. package/src/jsonView/RenderNode.tsx +0 -51
  66. package/src/jsonView/index.ts +0 -2
  67. package/src/jsonView/json-view.css +0 -206
  68. package/src/jsonView/useJsonViewTokens.ts +0 -32
  69. package/src/jsonView/viewers.tsx +0 -343
  70. package/src/layers/ImageLayer.tsx +0 -297
  71. package/src/layers/MapLayer.tsx +0 -185
  72. package/src/layers/PathLayer.tsx +0 -155
  73. package/src/layers/ShapeLayer.tsx +0 -158
  74. package/src/layers/TableLayer.tsx +0 -129
  75. package/src/layers/TextLayer.tsx +0 -571
  76. package/src/layers/TreeLayer.tsx +0 -167
  77. package/src/layers/VideoLayer.tsx +0 -171
  78. package/src/layers/index.ts +0 -6
  79. package/src/standalone-entry.tsx +0 -337
  80. package/src/standalone-source.d.ts +0 -10
  81. package/src/styles/doc-animations.css +0 -2431
  82. package/src/styles/index.css +0 -7
  83. package/src/types.ts +0 -183
  84. package/src/utils/animationUtils.ts +0 -13
  85. package/src/utils/fillStyle.tsx +0 -148
  86. package/src/utils/layerUtils.ts +0 -42
  87. package/src/utils/mapTileUtils.ts +0 -375
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/DocPlayer.tsx
2
- import { Fragment as Fragment3, useId as useId7, useRef as useRef9, useState as useState9, useEffect as useEffect10, useCallback as useCallback7, useMemo as useMemo9 } from "react";
2
+ import { Fragment as Fragment4, useId as useId7, useRef as useRef9, useState as useState10, useEffect as useEffect11, useCallback as useCallback7, useMemo as useMemo10 } from "react";
3
3
  import {
4
4
  isTemplateBlock as isTemplateBlock3,
5
5
  getCaptionAtTime as getCaptionAtTime2,
@@ -12,18 +12,29 @@ import { useEffect as useEffect2, useRef } from "react";
12
12
 
13
13
  // src/hooks/MediaContext.tsx
14
14
  import { createContext, useContext, useState, useEffect, useMemo } from "react";
15
+ import {
16
+ DEFAULT_INTERACTIVE_RESOURCE_POLICY,
17
+ isResourceUrlAllowed
18
+ } from "@bendyline/squisq/markdown";
15
19
  var MediaContext = createContext(null);
20
+ var ResourcePolicyContext = createContext(
21
+ DEFAULT_INTERACTIVE_RESOURCE_POLICY
22
+ );
23
+ function useResourcePolicy() {
24
+ return useContext(ResourcePolicyContext);
25
+ }
16
26
  function useMediaProvider() {
17
27
  return useContext(MediaContext);
18
28
  }
19
29
  function useMediaUrl(relativePath, basePath) {
20
30
  const provider = useMediaProvider();
31
+ const resourcePolicy = useResourcePolicy();
21
32
  const safePath = typeof relativePath === "string" ? relativePath : "";
22
- const isAbsolute = !safePath || safePath.startsWith("http") || safePath.startsWith("/") || safePath.startsWith("data:") || safePath.startsWith("blob:");
23
- const fallback = useMemo(
24
- () => isAbsolute ? safePath : `${basePath}/${safePath}`,
25
- [isAbsolute, safePath, basePath]
26
- );
33
+ const isAbsolute = !safePath || /^(?:[a-z][a-z0-9+.-]*:|\/\/|\/)/i.test(safePath);
34
+ const fallback = useMemo(() => {
35
+ const candidate = isAbsolute ? safePath : `${basePath.replace(/\/$/, "")}/${safePath}`;
36
+ return isResourceUrlAllowed(candidate, resourcePolicy) ? candidate : "";
37
+ }, [isAbsolute, safePath, basePath, resourcePolicy]);
27
38
  const needsProvider = !isAbsolute && !!provider;
28
39
  const [url, setUrl] = useState(fallback);
29
40
  useEffect(() => {
@@ -35,7 +46,9 @@ function useMediaUrl(relativePath, basePath) {
35
46
  setUrl(fallback);
36
47
  provider.resolveUrl(safePath).then(
37
48
  (resolved) => {
38
- if (!cancelled) setUrl(resolved);
49
+ if (!cancelled) {
50
+ setUrl(isResourceUrlAllowed(resolved, resourcePolicy) ? resolved : "");
51
+ }
39
52
  },
40
53
  () => {
41
54
  if (!cancelled) setUrl(fallback);
@@ -44,7 +57,7 @@ function useMediaUrl(relativePath, basePath) {
44
57
  return () => {
45
58
  cancelled = true;
46
59
  };
47
- }, [needsProvider, provider, safePath, fallback]);
60
+ }, [needsProvider, provider, safePath, fallback, resourcePolicy]);
48
61
  return needsProvider ? url : fallback;
49
62
  }
50
63
 
@@ -1039,8 +1052,13 @@ function MarkerDef({
1039
1052
 
1040
1053
  // src/layers/MapLayer.tsx
1041
1054
  import { useId as useId4, useState as useState2, useEffect as useEffect3 } from "react";
1055
+ import { ResourcePolicyError as ResourcePolicyError2 } from "@bendyline/squisq/markdown";
1042
1056
 
1043
1057
  // src/utils/mapTileUtils.ts
1058
+ import {
1059
+ isResourceUrlAllowed as isResourceUrlAllowed2,
1060
+ ResourcePolicyError
1061
+ } from "@bendyline/squisq/markdown";
1044
1062
  var TILE_PROVIDERS = {
1045
1063
  terrain: {
1046
1064
  url: "https://tile.opentopomap.org/{z}/{x}/{y}.png",
@@ -1123,6 +1141,13 @@ async function composeMapImage(options) {
1123
1141
  const provider = TILE_PROVIDERS[style];
1124
1142
  const tileSize = provider.tileSize || 256;
1125
1143
  const clampedZoom = Math.min(zoom, provider.maxZoom);
1144
+ const probeUrl = buildTileUrl(provider, 0, 0, clampedZoom);
1145
+ if (!isResourceUrlAllowed2(probeUrl, options.policy)) {
1146
+ throw new ResourcePolicyError(
1147
+ "RESOURCE_BLOCKED",
1148
+ `Map tiles for style "${style}" are blocked by the resource policy (${provider.url}). Composing a map contacts the tile host; supply the map as \`staticSrc\` to render it without any remote request.`
1149
+ );
1150
+ }
1126
1151
  const canvas = document.createElement("canvas");
1127
1152
  canvas.width = width;
1128
1153
  canvas.height = height;
@@ -1228,6 +1253,7 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1228
1253
  const [mapImageUrl, setMapImageUrl] = useState2(null);
1229
1254
  const [isLoading, setIsLoading] = useState2(true);
1230
1255
  const [error, setError] = useState2(null);
1256
+ const [blockedByPolicy, setBlockedByPolicy] = useState2(false);
1231
1257
  const x = resolveValue(position.x, viewport.width);
1232
1258
  const y = resolveValue(position.y, viewport.height);
1233
1259
  const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
@@ -1235,16 +1261,18 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1235
1261
  const offset = getAnchorOffset(position.anchor, width, height);
1236
1262
  const finalX = x + offset.x;
1237
1263
  const finalY = y + offset.y;
1264
+ const staticSrc = useMediaUrl(content.staticSrc ?? "", basePath);
1265
+ const resourcePolicy = useResourcePolicy();
1238
1266
  useEffect3(() => {
1239
1267
  let cancelled = false;
1240
1268
  if (content.staticSrc) {
1241
- const src = content.staticSrc.startsWith("http") ? content.staticSrc : `${basePath}/${content.staticSrc}`;
1242
- setMapImageUrl(src);
1269
+ setMapImageUrl(staticSrc || null);
1243
1270
  setIsLoading(false);
1244
1271
  return;
1245
1272
  }
1246
1273
  setIsLoading(true);
1247
1274
  setError(null);
1275
+ setBlockedByPolicy(false);
1248
1276
  composeMapImage({
1249
1277
  center: content.center,
1250
1278
  zoom: content.zoom,
@@ -1252,7 +1280,8 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1252
1280
  width,
1253
1281
  height,
1254
1282
  markers: content.markers,
1255
- showAttribution: content.showAttribution !== false
1283
+ showAttribution: content.showAttribution !== false,
1284
+ policy: resourcePolicy
1256
1285
  }).then((dataUrl) => {
1257
1286
  if (!cancelled) {
1258
1287
  setMapImageUrl(dataUrl);
@@ -1260,8 +1289,10 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1260
1289
  }
1261
1290
  }).catch((err) => {
1262
1291
  if (!cancelled) {
1263
- console.error("Failed to compose map:", err);
1292
+ const blocked = err instanceof ResourcePolicyError2;
1293
+ if (!blocked) console.error("Failed to compose map:", err);
1264
1294
  setError(err instanceof Error ? err.message : String(err));
1295
+ setBlockedByPolicy(blocked);
1265
1296
  setIsLoading(false);
1266
1297
  }
1267
1298
  });
@@ -1273,11 +1304,13 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1273
1304
  content.zoom,
1274
1305
  content.style,
1275
1306
  content.staticSrc,
1307
+ staticSrc,
1276
1308
  content.markers,
1277
1309
  content.showAttribution,
1278
1310
  width,
1279
1311
  height,
1280
- basePath
1312
+ basePath,
1313
+ resourcePolicy
1281
1314
  ]);
1282
1315
  const animStyle = getAnimationStyle(animation, blockTime);
1283
1316
  if (isLoading) {
@@ -1314,7 +1347,16 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1314
1347
  style: animStyle.style,
1315
1348
  "data-layer-id": layer.id,
1316
1349
  children: [
1317
- /* @__PURE__ */ jsx7("rect", { x: finalX, y: finalY, width, height, fill: "#fef2f2" }),
1350
+ /* @__PURE__ */ jsx7(
1351
+ "rect",
1352
+ {
1353
+ x: finalX,
1354
+ y: finalY,
1355
+ width,
1356
+ height,
1357
+ fill: blockedByPolicy ? "#f3f4f6" : "#fef2f2"
1358
+ }
1359
+ ),
1318
1360
  /* @__PURE__ */ jsx7(
1319
1361
  "text",
1320
1362
  {
@@ -1322,10 +1364,10 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1322
1364
  y: finalY + height / 2,
1323
1365
  textAnchor: "middle",
1324
1366
  dominantBaseline: "middle",
1325
- fill: "#dc2626",
1367
+ fill: blockedByPolicy ? "#6b7280" : "#dc2626",
1326
1368
  fontSize: "18",
1327
1369
  fontFamily: "system-ui, sans-serif",
1328
- children: "Map failed to load"
1370
+ children: blockedByPolicy ? "Map unavailable offline" : "Map failed to load"
1329
1371
  }
1330
1372
  )
1331
1373
  ]
@@ -1462,76 +1504,84 @@ function TableLayer({ layer, viewport, blockTime }) {
1462
1504
  const offset = getAnchorOffset(position.anchor, width, height);
1463
1505
  const finalX = x + offset.x;
1464
1506
  const finalY = y + offset.y;
1465
- const animStyle = animation ? getAnimationStyle(animation, blockTime) : {};
1507
+ const animStyle = getAnimationStyle(animation, blockTime);
1466
1508
  const cellAlign = (ci) => {
1467
1509
  const a = align?.[ci];
1468
1510
  return a ? { textAlign: a } : void 0;
1469
1511
  };
1470
1512
  const borderRadius = style.borderRadius ?? 8;
1471
- return /* @__PURE__ */ jsx9("foreignObject", { x: finalX, y: finalY, width, height, style: animStyle, children: /* @__PURE__ */ jsx9(
1472
- "div",
1513
+ return /* @__PURE__ */ jsx9(
1514
+ "g",
1473
1515
  {
1474
- ...{ xmlns: "http://www.w3.org/1999/xhtml" },
1475
- style: {
1476
- width: `${width}px`,
1477
- height: `${height}px`,
1478
- display: "flex",
1479
- alignItems: "center",
1480
- justifyContent: "center",
1481
- padding: "16px",
1482
- boxSizing: "border-box"
1483
- },
1484
- children: /* @__PURE__ */ jsxs6(
1485
- "table",
1516
+ className: `block-layer block-layer--table ${animStyle.className}`,
1517
+ style: animStyle.style,
1518
+ "data-layer-id": layer.id,
1519
+ children: /* @__PURE__ */ jsx9("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx9(
1520
+ "div",
1486
1521
  {
1522
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
1487
1523
  style: {
1488
- width: "100%",
1489
- borderCollapse: "separate",
1490
- borderSpacing: 0,
1491
- fontSize: `${style.fontSize}px`,
1492
- fontFamily: style.fontFamily ?? "system-ui, sans-serif",
1493
- overflow: "hidden",
1494
- borderRadius: `${borderRadius}px`,
1495
- border: `1px solid ${style.borderColor}`
1524
+ width: `${width}px`,
1525
+ height: `${height}px`,
1526
+ display: "flex",
1527
+ alignItems: "center",
1528
+ justifyContent: "center",
1529
+ padding: "16px",
1530
+ boxSizing: "border-box"
1496
1531
  },
1497
- children: [
1498
- headers.length > 0 && /* @__PURE__ */ jsx9("thead", { children: /* @__PURE__ */ jsx9("tr", { children: headers.map((header, ci) => /* @__PURE__ */ jsx9(
1499
- "th",
1500
- {
1501
- style: {
1502
- background: style.headerBackground,
1503
- color: style.headerColor,
1504
- fontFamily: style.headerFontFamily ?? style.fontFamily ?? "system-ui, sans-serif",
1505
- fontWeight: 600,
1506
- padding: "12px 16px",
1507
- borderBottom: `2px solid ${style.borderColor}`,
1508
- borderRight: ci < headers.length - 1 ? `1px solid ${style.borderColor}` : void 0,
1509
- ...cellAlign(ci)
1510
- },
1511
- children: header
1512
- },
1513
- ci
1514
- )) }) }),
1515
- rows.length > 0 && /* @__PURE__ */ jsx9("tbody", { children: rows.map((row, ri) => /* @__PURE__ */ jsx9("tr", { children: row.map((cell, ci) => /* @__PURE__ */ jsx9(
1516
- "td",
1517
- {
1518
- style: {
1519
- background: style.cellBackground,
1520
- color: style.cellColor,
1521
- padding: "10px 16px",
1522
- borderBottom: ri < rows.length - 1 ? `1px solid ${style.borderColor}` : void 0,
1523
- borderRight: ci < row.length - 1 ? `1px solid ${style.borderColor}` : void 0,
1524
- ...cellAlign(ci)
1525
- },
1526
- children: cell
1532
+ children: /* @__PURE__ */ jsxs6(
1533
+ "table",
1534
+ {
1535
+ style: {
1536
+ width: "100%",
1537
+ borderCollapse: "separate",
1538
+ borderSpacing: 0,
1539
+ fontSize: `${style.fontSize}px`,
1540
+ fontFamily: style.fontFamily ?? "system-ui, sans-serif",
1541
+ overflow: "hidden",
1542
+ borderRadius: `${borderRadius}px`,
1543
+ border: `1px solid ${style.borderColor}`
1527
1544
  },
1528
- ci
1529
- )) }, ri)) })
1530
- ]
1545
+ children: [
1546
+ headers.length > 0 && /* @__PURE__ */ jsx9("thead", { children: /* @__PURE__ */ jsx9("tr", { children: headers.map((header, ci) => /* @__PURE__ */ jsx9(
1547
+ "th",
1548
+ {
1549
+ style: {
1550
+ background: style.headerBackground,
1551
+ color: style.headerColor,
1552
+ fontFamily: style.headerFontFamily ?? style.fontFamily ?? "system-ui, sans-serif",
1553
+ fontWeight: 600,
1554
+ padding: "12px 16px",
1555
+ borderBottom: `2px solid ${style.borderColor}`,
1556
+ borderRight: ci < headers.length - 1 ? `1px solid ${style.borderColor}` : void 0,
1557
+ ...cellAlign(ci)
1558
+ },
1559
+ children: header
1560
+ },
1561
+ ci
1562
+ )) }) }),
1563
+ rows.length > 0 && /* @__PURE__ */ jsx9("tbody", { children: rows.map((row, ri) => /* @__PURE__ */ jsx9("tr", { children: row.map((cell, ci) => /* @__PURE__ */ jsx9(
1564
+ "td",
1565
+ {
1566
+ style: {
1567
+ background: style.cellBackground,
1568
+ color: style.cellColor,
1569
+ padding: "10px 16px",
1570
+ borderBottom: ri < rows.length - 1 ? `1px solid ${style.borderColor}` : void 0,
1571
+ borderRight: ci < row.length - 1 ? `1px solid ${style.borderColor}` : void 0,
1572
+ ...cellAlign(ci)
1573
+ },
1574
+ children: cell
1575
+ },
1576
+ ci
1577
+ )) }, ri)) })
1578
+ ]
1579
+ }
1580
+ )
1531
1581
  }
1532
- )
1582
+ ) })
1533
1583
  }
1534
- ) });
1584
+ );
1535
1585
  }
1536
1586
 
1537
1587
  // src/layers/TreeLayer.tsx
@@ -1554,16 +1604,14 @@ function TreeLayer({ layer, viewport, blockTime }) {
1554
1604
  const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
1555
1605
  const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
1556
1606
  const offset = getAnchorOffset(position.anchor, width, height);
1557
- const animStyle = animation ? getAnimationStyle(animation, blockTime) : {};
1607
+ const animStyle = getAnimationStyle(animation, blockTime);
1558
1608
  return /* @__PURE__ */ jsx10(
1559
- "foreignObject",
1609
+ "g",
1560
1610
  {
1561
- x: x + offset.x,
1562
- y: y + offset.y,
1563
- width,
1564
- height,
1565
- style: animStyle,
1566
- children: /* @__PURE__ */ jsx10(
1611
+ className: `block-layer block-layer--tree ${animStyle.className}`,
1612
+ style: animStyle.style,
1613
+ "data-layer-id": layer.id,
1614
+ children: /* @__PURE__ */ jsx10("foreignObject", { x: x + offset.x, y: y + offset.y, width, height, children: /* @__PURE__ */ jsx10(
1567
1615
  "div",
1568
1616
  {
1569
1617
  ...{ xmlns: "http://www.w3.org/1999/xhtml" },
@@ -1583,7 +1631,7 @@ function TreeLayer({ layer, viewport, blockTime }) {
1583
1631
  },
1584
1632
  children: /* @__PURE__ */ jsx10(TreeList, { items, depth: 0, style })
1585
1633
  }
1586
- )
1634
+ ) })
1587
1635
  }
1588
1636
  );
1589
1637
  }
@@ -1662,8 +1710,146 @@ function TreeRow({
1662
1710
  ] });
1663
1711
  }
1664
1712
 
1665
- // src/BlockRenderer.tsx
1713
+ // src/mermaid/MermaidDiagram.tsx
1714
+ import { useEffect as useEffect5, useState as useState4 } from "react";
1715
+
1716
+ // src/mermaid/mermaidRuntime.ts
1717
+ var mermaidPromise = null;
1718
+ async function loadMermaid() {
1719
+ if (!mermaidPromise) {
1720
+ mermaidPromise = import("mermaid").then(({ default: mermaid }) => {
1721
+ mermaid.initialize({
1722
+ startOnLoad: false,
1723
+ securityLevel: "strict",
1724
+ suppressErrorRendering: true,
1725
+ deterministicIds: false,
1726
+ logLevel: "error"
1727
+ });
1728
+ return mermaid;
1729
+ }).catch((error) => {
1730
+ mermaidPromise = null;
1731
+ throw error;
1732
+ });
1733
+ }
1734
+ return mermaidPromise;
1735
+ }
1736
+ function enqueue(run) {
1737
+ const host = globalThis;
1738
+ const previous = host.__squisqMermaidWorkQueue ?? Promise.resolve();
1739
+ const result = previous.then(run, run);
1740
+ host.__squisqMermaidWorkQueue = result.then(
1741
+ () => void 0,
1742
+ () => void 0
1743
+ );
1744
+ return result;
1745
+ }
1746
+ function renderMermaidSvg(id, source, container) {
1747
+ return enqueue(async () => {
1748
+ const mermaid = await loadMermaid();
1749
+ const { svg, diagramType } = await mermaid.render(id, source, container);
1750
+ return { svg, diagramType };
1751
+ });
1752
+ }
1753
+ function mermaidRenderErrorMessage(error) {
1754
+ const raw = error instanceof Error ? error.message : String(error);
1755
+ return raw.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean).slice(0, 6).join("\n") || "Unknown Mermaid rendering error.";
1756
+ }
1757
+
1758
+ // src/mermaid/MermaidDiagram.tsx
1666
1759
  import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1760
+ var renderSequence = 0;
1761
+ function MermaidDiagram({
1762
+ source,
1763
+ className,
1764
+ ariaLabel = "Mermaid diagram"
1765
+ }) {
1766
+ const [svg, setSvg] = useState4("");
1767
+ const [error, setError] = useState4("");
1768
+ const [rendering, setRendering] = useState4(true);
1769
+ useEffect5(() => {
1770
+ let current = true;
1771
+ const id = `squisq-rich-mermaid-${++renderSequence}`;
1772
+ setRendering(true);
1773
+ setError("");
1774
+ void renderMermaidSvg(id, source).then((result) => {
1775
+ if (!current) return;
1776
+ setSvg(result.svg);
1777
+ }).catch((caught) => {
1778
+ if (!current) return;
1779
+ setSvg("");
1780
+ setError(mermaidRenderErrorMessage(caught));
1781
+ }).finally(() => {
1782
+ if (current) setRendering(false);
1783
+ });
1784
+ return () => {
1785
+ current = false;
1786
+ };
1787
+ }, [source]);
1788
+ const classes = ["squisq-mermaid-rendered", className].filter(Boolean).join(" ");
1789
+ return /* @__PURE__ */ jsxs8("div", { className: classes, role: "img", "aria-label": ariaLabel, children: [
1790
+ rendering && /* @__PURE__ */ jsx11("div", { className: "squisq-mermaid-render-status", children: "Rendering diagram\u2026" }),
1791
+ !rendering && error && /* @__PURE__ */ jsxs8("div", { className: "squisq-mermaid-render-error", role: "alert", children: [
1792
+ /* @__PURE__ */ jsx11("strong", { children: "Mermaid could not render this diagram." }),
1793
+ /* @__PURE__ */ jsx11("pre", { children: error })
1794
+ ] }),
1795
+ !rendering && !error && svg && /* @__PURE__ */ jsx11(
1796
+ "div",
1797
+ {
1798
+ className: "squisq-mermaid-render-svg",
1799
+ dangerouslySetInnerHTML: { __html: svg }
1800
+ }
1801
+ )
1802
+ ] });
1803
+ }
1804
+
1805
+ // src/layers/MermaidLayer.tsx
1806
+ import { jsx as jsx12 } from "react/jsx-runtime";
1807
+ function MermaidLayer({ layer, viewport, blockTime }) {
1808
+ const { position, content, animation } = layer;
1809
+ const width = resolveValue(position.width ?? viewport.width, viewport.width);
1810
+ const height = resolveValue(position.height ?? viewport.height, viewport.height);
1811
+ let x = resolveValue(position.x, viewport.width);
1812
+ let y = resolveValue(position.y, viewport.height);
1813
+ const anchor = position.anchor ?? "top-left";
1814
+ if (anchor === "center") {
1815
+ x -= width / 2;
1816
+ y -= height / 2;
1817
+ } else {
1818
+ if (anchor.endsWith("right")) x -= width;
1819
+ if (anchor.startsWith("bottom")) y -= height;
1820
+ }
1821
+ const animStyle = getAnimationStyle(animation, blockTime);
1822
+ const panelStyle = {
1823
+ boxSizing: "border-box",
1824
+ width: "100%",
1825
+ height: "100%",
1826
+ padding: content.padding ?? 16,
1827
+ borderRadius: 16,
1828
+ background: content.background,
1829
+ color: content.foreground,
1830
+ overflow: "hidden"
1831
+ };
1832
+ return /* @__PURE__ */ jsx12(
1833
+ "g",
1834
+ {
1835
+ className: `block-layer block-layer--mermaid ${animStyle.className}`,
1836
+ "data-layer-id": layer.id,
1837
+ style: animStyle.style,
1838
+ children: /* @__PURE__ */ jsx12("foreignObject", { x, y, width, height, children: /* @__PURE__ */ jsx12(
1839
+ "div",
1840
+ {
1841
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
1842
+ className: "squisq-mermaid-layer-frame",
1843
+ style: panelStyle,
1844
+ children: /* @__PURE__ */ jsx12(MermaidDiagram, { source: content.source, ariaLabel: "Mermaid diagram on slide" })
1845
+ }
1846
+ ) })
1847
+ }
1848
+ );
1849
+ }
1850
+
1851
+ // src/BlockRenderer.tsx
1852
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1667
1853
  var DEFAULT_VIEWPORT = {
1668
1854
  width: 1920,
1669
1855
  height: 1080
@@ -1691,7 +1877,7 @@ function BlockRenderer({
1691
1877
  }
1692
1878
  const instanceId = useId5().replace(/:/g, "");
1693
1879
  const clipId = `vb-clip-${instanceId}-${block.id}`;
1694
- return /* @__PURE__ */ jsxs8(
1880
+ return /* @__PURE__ */ jsxs9(
1695
1881
  "svg",
1696
1882
  {
1697
1883
  className: `block-svg ${transitionClass}`,
@@ -1701,8 +1887,8 @@ function BlockRenderer({
1701
1887
  overflow: "hidden",
1702
1888
  "data-block-id": block.id,
1703
1889
  children: [
1704
- /* @__PURE__ */ jsx11("defs", { children: /* @__PURE__ */ jsx11("clipPath", { id: clipId, children: /* @__PURE__ */ jsx11("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
1705
- /* @__PURE__ */ jsx11("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx11(
1890
+ /* @__PURE__ */ jsx13("defs", { children: /* @__PURE__ */ jsx13("clipPath", { id: clipId, children: /* @__PURE__ */ jsx13("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
1891
+ /* @__PURE__ */ jsx13("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx13(
1706
1892
  LayerRenderer,
1707
1893
  {
1708
1894
  layer,
@@ -1729,7 +1915,7 @@ function LayerRenderer({
1729
1915
  const renderedLayer = animationsEnabled || !layer.animation ? layer : { ...layer, animation: void 0 };
1730
1916
  switch (renderedLayer.type) {
1731
1917
  case "image":
1732
- return /* @__PURE__ */ jsx11(
1918
+ return /* @__PURE__ */ jsx13(
1733
1919
  ImageLayer,
1734
1920
  {
1735
1921
  layer: renderedLayer,
@@ -1740,13 +1926,13 @@ function LayerRenderer({
1740
1926
  }
1741
1927
  );
1742
1928
  case "text":
1743
- return /* @__PURE__ */ jsx11(TextLayer, { layer: renderedLayer, viewport, blockTime });
1929
+ return /* @__PURE__ */ jsx13(TextLayer, { layer: renderedLayer, viewport, blockTime });
1744
1930
  case "shape":
1745
- return /* @__PURE__ */ jsx11(ShapeLayer, { layer: renderedLayer, viewport, blockTime });
1931
+ return /* @__PURE__ */ jsx13(ShapeLayer, { layer: renderedLayer, viewport, blockTime });
1746
1932
  case "path":
1747
- return /* @__PURE__ */ jsx11(PathLayer, { layer: renderedLayer, viewport, blockTime });
1933
+ return /* @__PURE__ */ jsx13(PathLayer, { layer: renderedLayer, viewport, blockTime });
1748
1934
  case "map":
1749
- return /* @__PURE__ */ jsx11(
1935
+ return /* @__PURE__ */ jsx13(
1750
1936
  MapLayer,
1751
1937
  {
1752
1938
  layer: renderedLayer,
@@ -1756,7 +1942,7 @@ function LayerRenderer({
1756
1942
  }
1757
1943
  );
1758
1944
  case "video":
1759
- return /* @__PURE__ */ jsx11(
1945
+ return /* @__PURE__ */ jsx13(
1760
1946
  VideoLayer,
1761
1947
  {
1762
1948
  layer: renderedLayer,
@@ -1767,9 +1953,11 @@ function LayerRenderer({
1767
1953
  }
1768
1954
  );
1769
1955
  case "table":
1770
- return /* @__PURE__ */ jsx11(TableLayer, { layer: renderedLayer, viewport, blockTime });
1956
+ return /* @__PURE__ */ jsx13(TableLayer, { layer: renderedLayer, viewport, blockTime });
1771
1957
  case "tree":
1772
- return /* @__PURE__ */ jsx11(TreeLayer, { layer: renderedLayer, viewport, blockTime });
1958
+ return /* @__PURE__ */ jsx13(TreeLayer, { layer: renderedLayer, viewport, blockTime });
1959
+ case "mermaid":
1960
+ return /* @__PURE__ */ jsx13(MermaidLayer, { layer: renderedLayer, viewport, blockTime });
1773
1961
  default:
1774
1962
  console.warn(`Unknown layer type: ${renderedLayer.type}`);
1775
1963
  return null;
@@ -1782,7 +1970,7 @@ import { getCaptionAtTime } from "@bendyline/squisq/schemas";
1782
1970
  // src/SocialCaptionOverlay.tsx
1783
1971
  import { useMemo as useMemo4 } from "react";
1784
1972
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
1785
- import { jsx as jsx12 } from "react/jsx-runtime";
1973
+ import { jsx as jsx14 } from "react/jsx-runtime";
1786
1974
  var TARGET_CHUNK_SIZE = 4;
1787
1975
  var MIN_CHUNK_SIZE = 2;
1788
1976
  var MAX_CHUNK_SIZE = 6;
@@ -1841,7 +2029,7 @@ function SocialCaptionOverlay({
1841
2029
  [captions]
1842
2030
  );
1843
2031
  if (!enabled || chunks.length === 0) {
1844
- return /* @__PURE__ */ jsx12(
2032
+ return /* @__PURE__ */ jsx14(
1845
2033
  "div",
1846
2034
  {
1847
2035
  className: "social-caption-overlay",
@@ -1903,7 +2091,7 @@ function SocialCaptionOverlay({
1903
2091
  const viewportHeight = viewport?.height ?? 720;
1904
2092
  const baseFontSize = Math.round(viewportHeight * 0.055);
1905
2093
  const fontSize = Math.max(24, Math.min(72, baseFontSize));
1906
- return /* @__PURE__ */ jsx12(
2094
+ return /* @__PURE__ */ jsx14(
1907
2095
  "div",
1908
2096
  {
1909
2097
  className: "social-caption-overlay",
@@ -1920,7 +2108,7 @@ function SocialCaptionOverlay({
1920
2108
  opacity: 1,
1921
2109
  transition: "opacity 0.15s ease-in-out"
1922
2110
  },
1923
- children: /* @__PURE__ */ jsx12(
2111
+ children: /* @__PURE__ */ jsx14(
1924
2112
  "div",
1925
2113
  {
1926
2114
  style: {
@@ -1929,7 +2117,7 @@ function SocialCaptionOverlay({
1929
2117
  },
1930
2118
  children: activeChunk.words.map((word, i) => {
1931
2119
  const isActive = i === activeWordIndex;
1932
- return /* @__PURE__ */ jsx12(
2120
+ return /* @__PURE__ */ jsx14(
1933
2121
  "span",
1934
2122
  {
1935
2123
  style: {
@@ -1954,7 +2142,7 @@ function SocialCaptionOverlay({
1954
2142
  }
1955
2143
 
1956
2144
  // src/CaptionOverlay.tsx
1957
- import { jsx as jsx13 } from "react/jsx-runtime";
2145
+ import { jsx as jsx15 } from "react/jsx-runtime";
1958
2146
  function CaptionOverlay({
1959
2147
  captions,
1960
2148
  currentTime,
@@ -1965,7 +2153,7 @@ function CaptionOverlay({
1965
2153
  viewport
1966
2154
  }) {
1967
2155
  if (captionStyle === "social") {
1968
- return /* @__PURE__ */ jsx13(
2156
+ return /* @__PURE__ */ jsx15(
1969
2157
  SocialCaptionOverlay,
1970
2158
  {
1971
2159
  captions,
@@ -1978,7 +2166,7 @@ function CaptionOverlay({
1978
2166
  }
1979
2167
  const phrase = enabled && captions ? getCaptionAtTime(captions, currentTime) : null;
1980
2168
  const captionText = phrase?.text ?? null;
1981
- return /* @__PURE__ */ jsx13(
2169
+ return /* @__PURE__ */ jsx15(
1982
2170
  "div",
1983
2171
  {
1984
2172
  className: "caption-overlay",
@@ -1997,7 +2185,7 @@ function CaptionOverlay({
1997
2185
  padding: "0 4px",
1998
2186
  boxSizing: "border-box"
1999
2187
  },
2000
- children: captionText && /* @__PURE__ */ jsx13(
2188
+ children: captionText && /* @__PURE__ */ jsx15(
2001
2189
  "div",
2002
2190
  {
2003
2191
  style: {
@@ -2007,7 +2195,7 @@ function CaptionOverlay({
2007
2195
  borderRadius: "4px",
2008
2196
  backdropFilter: "blur(4px)"
2009
2197
  },
2010
- children: /* @__PURE__ */ jsx13(
2198
+ children: /* @__PURE__ */ jsx15(
2011
2199
  "span",
2012
2200
  {
2013
2201
  style: {
@@ -2053,55 +2241,109 @@ function useAutoSurface(enabled) {
2053
2241
  }
2054
2242
 
2055
2243
  // src/hooks/useAudioSync.ts
2056
- import { useState as useState4, useEffect as useEffect5, useRef as useRef3, useCallback as useCallback2 } from "react";
2244
+ import { useState as useState5, useEffect as useEffect6, useRef as useRef3, useCallback as useCallback2 } from "react";
2245
+ import { fetchResourceBytes, isResourceUrlAllowed as isResourceUrlAllowed3 } from "@bendyline/squisq/markdown";
2246
+
2247
+ // src/hooks/AudioController.ts
2248
+ function calculateSegmentTiming(segments) {
2249
+ if (!segments?.length) {
2250
+ return { segmentStarts: [], totalDuration: 0 };
2251
+ }
2252
+ let time = 0;
2253
+ const segmentStarts = segments.map((seg) => {
2254
+ const start = time;
2255
+ time += seg.duration;
2256
+ return start;
2257
+ });
2258
+ return { segmentStarts, totalDuration: time };
2259
+ }
2260
+ function findSegmentAtTime(time, segments, segmentStarts) {
2261
+ if (!segments?.length) {
2262
+ return { segmentIndex: 0, segmentStart: 0 };
2263
+ }
2264
+ let segmentIndex = 0;
2265
+ let segmentStart = 0;
2266
+ for (let i = 0; i < segments.length; i++) {
2267
+ const segEnd = segmentStarts[i] + segments[i].duration;
2268
+ if (time < segEnd) {
2269
+ segmentIndex = i;
2270
+ segmentStart = segmentStarts[i];
2271
+ break;
2272
+ }
2273
+ if (i === segments.length - 1) {
2274
+ segmentIndex = i;
2275
+ segmentStart = segmentStarts[i];
2276
+ }
2277
+ }
2278
+ return { segmentIndex, segmentStart };
2279
+ }
2280
+
2281
+ // src/hooks/useAudioSync.ts
2282
+ var SEEK_COMPLETION_TIMEOUT_MS = 15e3;
2283
+ var UNLOADABLE_MESSAGE = "Audio could not be loaded. Check that the media file is available.";
2057
2284
  function resolveAudioUrl(src, basePath) {
2058
2285
  if (!src || /^(?:[a-z][a-z0-9+.-]*:|\/\/|\/)/i.test(src)) return src;
2059
2286
  if (!basePath) return src;
2060
2287
  return `${basePath.replace(/\/$/, "")}/${src.replace(/^\//, "")}`;
2061
2288
  }
2062
- function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2063
- const [currentTime, setCurrentTime] = useState4(0);
2064
- const [isPlaying, setIsPlaying] = useState4(false);
2065
- const [currentSegment, setCurrentSegment] = useState4(0);
2066
- const [isEnded, setIsEnded] = useState4(false);
2067
- const [isAudioReady, setIsAudioReady] = useState4(false);
2068
- const [totalDuration, setTotalDuration] = useState4(0);
2289
+ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true, mode = "media") {
2290
+ const resourcePolicy = useResourcePolicy();
2291
+ const [currentTime, setCurrentTime] = useState5(0);
2292
+ const [isPlaying, setIsPlaying] = useState5(false);
2293
+ const [currentSegment, setCurrentSegment] = useState5(0);
2294
+ const [isEnded, setIsEnded] = useState5(false);
2295
+ const [isAudioReady, setIsAudioReady] = useState5(false);
2296
+ const [totalDuration, setTotalDuration] = useState5(0);
2297
+ const [isAvailable, setIsAvailable] = useState5(true);
2298
+ const [unavailableMessage, setUnavailableMessage] = useState5();
2069
2299
  const segmentStarts = useRef3([]);
2070
2300
  const pendingSeekTime = useRef3(null);
2301
+ const pendingSeekCompletion = useRef3(null);
2071
2302
  const shouldPlayAfterLoad = useRef3(false);
2072
2303
  const blobUrls = useRef3(/* @__PURE__ */ new Map());
2073
2304
  const loadingPromises = useRef3(/* @__PURE__ */ new Map());
2074
2305
  const abortControllers = useRef3(/* @__PURE__ */ new Set());
2075
2306
  const loadGeneration = useRef3(0);
2307
+ const activeSegmentSrc = useRef3(void 0);
2308
+ activeSegmentSrc.current = audioTrack?.segments[currentSegment]?.src;
2309
+ const boundSource = useRef3(null);
2310
+ const currentSegmentRef = useRef3(0);
2311
+ currentSegmentRef.current = currentSegment;
2076
2312
  const fallbackMode = useRef3(false);
2077
- useEffect5(() => {
2313
+ useEffect6(() => {
2078
2314
  loadGeneration.current += 1;
2079
2315
  pendingSeekTime.current = null;
2316
+ pendingSeekCompletion.current?.();
2317
+ pendingSeekCompletion.current = null;
2080
2318
  shouldPlayAfterLoad.current = false;
2081
2319
  fallbackMode.current = false;
2320
+ boundSource.current = null;
2082
2321
  setCurrentTime(0);
2083
2322
  setCurrentSegment(0);
2084
2323
  setIsPlaying(false);
2085
2324
  setIsEnded(false);
2086
2325
  setIsAudioReady(false);
2326
+ setIsAvailable(true);
2327
+ setUnavailableMessage(void 0);
2087
2328
  if (!enabled || !audioTrack?.segments) {
2088
2329
  segmentStarts.current = [];
2089
2330
  setTotalDuration(0);
2090
2331
  return;
2091
2332
  }
2092
- let time = 0;
2093
- segmentStarts.current = audioTrack.segments.map((seg) => {
2094
- const start = time;
2095
- time += seg.duration;
2096
- return start;
2097
- });
2098
- setTotalDuration(time);
2099
- }, [audioTrack, enabled]);
2333
+ const timing = calculateSegmentTiming(audioTrack.segments);
2334
+ segmentStarts.current = timing.segmentStarts;
2335
+ setTotalDuration(timing.totalDuration);
2336
+ if (mode === "synthetic") setIsAudioReady(true);
2337
+ }, [audioTrack, enabled, mode]);
2100
2338
  const preloadAudio = useCallback2(
2101
2339
  async (src) => {
2102
2340
  const audioUrl = resolveAudioUrl(src, basePath);
2341
+ if (!isResourceUrlAllowed3(audioUrl, resourcePolicy)) return "";
2103
2342
  if (blobUrls.current.has(src)) {
2104
- return blobUrls.current.get(src);
2343
+ const cached = blobUrls.current.get(src);
2344
+ blobUrls.current.delete(src);
2345
+ blobUrls.current.set(src, cached);
2346
+ return cached;
2105
2347
  }
2106
2348
  if (loadingPromises.current.has(src)) {
2107
2349
  return loadingPromises.current.get(src);
@@ -2111,33 +2353,44 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2111
2353
  const generation = loadGeneration.current;
2112
2354
  const loadPromise = (async () => {
2113
2355
  try {
2114
- const response = await fetch(audioUrl, { signal: controller.signal });
2115
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
2116
- const blob = await response.blob();
2356
+ const resource = await fetchResourceBytes(audioUrl, {
2357
+ policy: resourcePolicy,
2358
+ signal: controller.signal,
2359
+ contentTypePrefixes: ["audio/", "video/", "application/octet-stream"]
2360
+ });
2361
+ const blob = new Blob([resource.bytes.slice().buffer], {
2362
+ type: resource.contentType || "application/octet-stream"
2363
+ });
2117
2364
  const blobUrl = URL.createObjectURL(blob);
2118
2365
  if (controller.signal.aborted || generation !== loadGeneration.current) {
2119
2366
  URL.revokeObjectURL(blobUrl);
2120
2367
  return audioUrl;
2121
2368
  }
2122
2369
  blobUrls.current.set(src, blobUrl);
2370
+ while (blobUrls.current.size > 2) {
2371
+ const oldest = [...blobUrls.current.entries()].find(([key]) => key !== activeSegmentSrc.current) ?? blobUrls.current.entries().next().value;
2372
+ if (!oldest) break;
2373
+ blobUrls.current.delete(oldest[0]);
2374
+ URL.revokeObjectURL(oldest[1]);
2375
+ }
2123
2376
  return blobUrl;
2124
2377
  } catch {
2125
- return audioUrl;
2126
- } finally {
2127
- abortControllers.current.delete(controller);
2128
- loadingPromises.current.delete(src);
2378
+ return "";
2129
2379
  }
2130
2380
  })();
2131
2381
  loadingPromises.current.set(src, loadPromise);
2382
+ void loadPromise.then(() => {
2383
+ abortControllers.current.delete(controller);
2384
+ if (loadingPromises.current.get(src) === loadPromise) {
2385
+ loadingPromises.current.delete(src);
2386
+ }
2387
+ });
2132
2388
  return loadPromise;
2133
2389
  },
2134
- [basePath]
2390
+ [basePath, resourcePolicy]
2135
2391
  );
2136
- useEffect5(() => {
2392
+ useEffect6(() => {
2137
2393
  if (!enabled || !audioTrack?.segments) return;
2138
- audioTrack.segments.forEach((segment) => {
2139
- preloadAudio(segment.src);
2140
- });
2141
2394
  const currentBlobUrls = blobUrls.current;
2142
2395
  const currentAbortControllers = abortControllers.current;
2143
2396
  const currentLoadingPromises = loadingPromises.current;
@@ -2152,8 +2405,13 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2152
2405
  currentBlobUrls.clear();
2153
2406
  };
2154
2407
  }, [audioTrack, preloadAudio, enabled]);
2155
- useEffect5(() => {
2156
- if (!enabled) return;
2408
+ useEffect6(() => {
2409
+ if (!enabled || mode === "synthetic") return;
2410
+ const segment = audioTrack?.segments[currentSegment];
2411
+ if (segment) void preloadAudio(segment.src);
2412
+ }, [audioTrack, currentSegment, enabled, mode, preloadAudio]);
2413
+ useEffect6(() => {
2414
+ if (!enabled || mode === "synthetic") return;
2157
2415
  const audio = audioRef.current;
2158
2416
  if (!audio) return;
2159
2417
  const handleTimeUpdate = () => {
@@ -2168,6 +2426,16 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2168
2426
  const handlePause = () => setIsPlaying(false);
2169
2427
  const handleError = () => {
2170
2428
  setIsAudioReady(true);
2429
+ setIsPlaying(false);
2430
+ setIsAvailable(false);
2431
+ setUnavailableMessage(UNLOADABLE_MESSAGE);
2432
+ shouldPlayAfterLoad.current = false;
2433
+ if (pendingSeekTime.current !== null) {
2434
+ setCurrentTime(pendingSeekTime.current);
2435
+ pendingSeekTime.current = null;
2436
+ }
2437
+ pendingSeekCompletion.current?.();
2438
+ pendingSeekCompletion.current = null;
2171
2439
  };
2172
2440
  const handleEnded = () => {
2173
2441
  if (audioTrack && currentSegment < audioTrack.segments.length - 1) {
@@ -2190,9 +2458,9 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2190
2458
  audio.removeEventListener("ended", handleEnded);
2191
2459
  audio.removeEventListener("error", handleError);
2192
2460
  };
2193
- }, [audioRef, currentSegment, audioTrack, enabled]);
2194
- useEffect5(() => {
2195
- if (!enabled) return;
2461
+ }, [audioRef, currentSegment, audioTrack, enabled, mode]);
2462
+ useEffect6(() => {
2463
+ if (!enabled || mode === "synthetic") return;
2196
2464
  const audio = audioRef.current;
2197
2465
  if (!audio || !audioTrack?.segments) return;
2198
2466
  const segment = audioTrack.segments[currentSegment];
@@ -2204,22 +2472,48 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2204
2472
  audio.currentTime = Math.max(0, segmentTime);
2205
2473
  setCurrentTime(pendingSeekTime.current);
2206
2474
  pendingSeekTime.current = null;
2475
+ pendingSeekCompletion.current?.();
2476
+ pendingSeekCompletion.current = null;
2207
2477
  }
2208
2478
  if (shouldPlayAfterLoad.current) {
2209
- audio.play().catch(() => {
2479
+ audio.play().catch((error) => {
2480
+ setIsPlaying(false);
2481
+ if (!(error instanceof Error && error.name === "NotAllowedError")) {
2482
+ setIsAvailable(false);
2483
+ setUnavailableMessage(
2484
+ "Audio playback failed. Check that the media file is supported and available."
2485
+ );
2486
+ }
2210
2487
  });
2211
2488
  shouldPlayAfterLoad.current = false;
2212
2489
  }
2213
2490
  };
2214
2491
  const currentSrc = audio.src;
2215
- const cachedBlobUrl = blobUrls.current.get(segment.src);
2216
- const isSameSource = currentSrc && (currentSrc === cachedBlobUrl || currentSrc.endsWith(segment.src));
2492
+ const bound = boundSource.current;
2493
+ const isSameSource = !!currentSrc && bound?.key === segment.src && bound.url === currentSrc;
2217
2494
  let cancelled = false;
2218
2495
  let handleCanPlay = null;
2219
2496
  if (!isSameSource) {
2220
2497
  const loadAndPlay = async () => {
2221
2498
  const blobUrl = await preloadAudio(segment.src);
2222
2499
  if (cancelled) return;
2500
+ if (!blobUrl) {
2501
+ audio.removeAttribute("src");
2502
+ audio.load();
2503
+ boundSource.current = null;
2504
+ shouldPlayAfterLoad.current = false;
2505
+ setIsAudioReady(true);
2506
+ setIsPlaying(false);
2507
+ setIsAvailable(false);
2508
+ setUnavailableMessage(UNLOADABLE_MESSAGE);
2509
+ if (pendingSeekTime.current !== null) {
2510
+ setCurrentTime(pendingSeekTime.current);
2511
+ pendingSeekTime.current = null;
2512
+ }
2513
+ pendingSeekCompletion.current?.();
2514
+ pendingSeekCompletion.current = null;
2515
+ return;
2516
+ }
2223
2517
  handleCanPlay = () => {
2224
2518
  if (cancelled) return;
2225
2519
  setIsAudioReady(true);
@@ -2228,6 +2522,7 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2228
2522
  };
2229
2523
  audio.addEventListener("canplay", handleCanPlay);
2230
2524
  audio.src = blobUrl;
2525
+ boundSource.current = { key: segment.src, url: audio.src };
2231
2526
  audio.load();
2232
2527
  await Promise.resolve();
2233
2528
  if (audio.readyState >= 3) {
@@ -2244,22 +2539,86 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2244
2539
  cancelled = true;
2245
2540
  if (handleCanPlay) audio.removeEventListener("canplay", handleCanPlay);
2246
2541
  };
2247
- }, [audioRef, currentSegment, audioTrack, preloadAudio, enabled]);
2248
- const play = useCallback2(() => {
2542
+ }, [audioRef, currentSegment, audioTrack, preloadAudio, enabled, mode]);
2543
+ const seekTo = useCallback2(
2544
+ async (time) => {
2545
+ const audio = audioRef.current;
2546
+ if (!audioTrack?.segments) return;
2547
+ const clampedTime = totalDuration > 0 ? Math.max(0, Math.min(time, totalDuration)) : Math.max(0, time);
2548
+ const starts = segmentStarts.current.length === audioTrack.segments.length ? segmentStarts.current : calculateSegmentTiming(audioTrack.segments).segmentStarts;
2549
+ const { segmentIndex, segmentStart } = findSegmentAtTime(
2550
+ clampedTime,
2551
+ audioTrack.segments,
2552
+ starts
2553
+ );
2554
+ const wasPlaying = mode === "synthetic" ? isPlaying : !audio?.paused;
2555
+ setIsEnded(false);
2556
+ if (!audio && mode === "media") {
2557
+ setCurrentSegment(segmentIndex);
2558
+ setCurrentTime(clampedTime);
2559
+ return;
2560
+ }
2561
+ if (segmentIndex !== currentSegmentRef.current) {
2562
+ pendingSeekTime.current = clampedTime;
2563
+ shouldPlayAfterLoad.current = wasPlaying;
2564
+ const completion = new Promise((resolve) => {
2565
+ pendingSeekCompletion.current?.();
2566
+ let settled = false;
2567
+ const settle = () => {
2568
+ if (settled) return;
2569
+ settled = true;
2570
+ clearTimeout(timer);
2571
+ if (pendingSeekCompletion.current === settle) pendingSeekCompletion.current = null;
2572
+ resolve();
2573
+ };
2574
+ const timer = setTimeout(settle, SEEK_COMPLETION_TIMEOUT_MS);
2575
+ pendingSeekCompletion.current = settle;
2576
+ });
2577
+ setCurrentSegment(segmentIndex);
2578
+ if (mode === "synthetic") {
2579
+ setCurrentTime(clampedTime);
2580
+ pendingSeekTime.current = null;
2581
+ pendingSeekCompletion.current?.();
2582
+ pendingSeekCompletion.current = null;
2583
+ }
2584
+ await completion;
2585
+ } else {
2586
+ const segmentTime = clampedTime - segmentStart;
2587
+ if (audio && mode === "media") audio.currentTime = Math.max(0, segmentTime);
2588
+ setCurrentTime(clampedTime);
2589
+ }
2590
+ },
2591
+ [audioRef, audioTrack, isPlaying, mode, totalDuration]
2592
+ );
2593
+ const play = useCallback2(async () => {
2594
+ if (mode === "synthetic") {
2595
+ fallbackMode.current = true;
2596
+ setIsPlaying(true);
2597
+ return;
2598
+ }
2249
2599
  const audio = audioRef.current;
2250
2600
  if (audio) {
2251
2601
  if (isEnded) {
2252
- setCurrentSegment(0);
2253
- setIsEnded(false);
2602
+ await seekTo(0);
2254
2603
  }
2255
- audio.play().then(() => {
2604
+ try {
2605
+ await audio.play();
2256
2606
  fallbackMode.current = false;
2257
- }).catch(() => {
2258
- fallbackMode.current = true;
2259
- setIsPlaying(true);
2260
- });
2607
+ setIsAvailable(true);
2608
+ setUnavailableMessage(void 0);
2609
+ } catch (error) {
2610
+ fallbackMode.current = false;
2611
+ setIsPlaying(false);
2612
+ const name = error instanceof Error ? error.name : "";
2613
+ if (name !== "NotAllowedError") {
2614
+ setIsAvailable(false);
2615
+ setUnavailableMessage(
2616
+ "Audio playback failed. Check that the media file is supported and available."
2617
+ );
2618
+ }
2619
+ }
2261
2620
  }
2262
- }, [audioRef, isEnded]);
2621
+ }, [audioRef, isEnded, mode, seekTo]);
2263
2622
  const pause = useCallback2(() => {
2264
2623
  const audio = audioRef.current;
2265
2624
  if (audio) {
@@ -2269,45 +2628,13 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2269
2628
  }, [audioRef]);
2270
2629
  const toggle = useCallback2(() => {
2271
2630
  const audio = audioRef.current;
2272
- if (!audio) return;
2631
+ if (!audio && mode === "media") return;
2273
2632
  if (!isPlaying) {
2274
2633
  play();
2275
2634
  } else {
2276
2635
  pause();
2277
2636
  }
2278
- }, [audioRef, isPlaying, play, pause]);
2279
- const seekTo = useCallback2(
2280
- (time) => {
2281
- const audio = audioRef.current;
2282
- if (!audio || !audioTrack?.segments) return;
2283
- const clampedTime = totalDuration > 0 ? Math.max(0, Math.min(time, totalDuration)) : Math.max(0, time);
2284
- let segmentIndex = 0;
2285
- let segmentStart = 0;
2286
- for (let i = 0; i < audioTrack.segments.length; i++) {
2287
- const segEnd = segmentStart + audioTrack.segments[i].duration;
2288
- if (clampedTime < segEnd) {
2289
- segmentIndex = i;
2290
- break;
2291
- }
2292
- segmentStart = segEnd;
2293
- if (i === audioTrack.segments.length - 1) {
2294
- segmentIndex = i;
2295
- }
2296
- }
2297
- const wasPlaying = !audio.paused;
2298
- setIsEnded(false);
2299
- if (segmentIndex !== currentSegment) {
2300
- pendingSeekTime.current = clampedTime;
2301
- shouldPlayAfterLoad.current = wasPlaying;
2302
- setCurrentSegment(segmentIndex);
2303
- } else {
2304
- const segmentTime = clampedTime - segmentStart;
2305
- audio.currentTime = Math.max(0, segmentTime);
2306
- setCurrentTime(clampedTime);
2307
- }
2308
- },
2309
- [audioRef, audioTrack, currentSegment, totalDuration]
2310
- );
2637
+ }, [audioRef, isPlaying, mode, play, pause]);
2311
2638
  const skipToSegment = useCallback2(
2312
2639
  (index) => {
2313
2640
  if (!audioTrack?.segments || index < 0 || index >= audioTrack.segments.length) {
@@ -2319,11 +2646,10 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2319
2646
  [audioTrack]
2320
2647
  );
2321
2648
  const restart = useCallback2(async () => {
2322
- seekTo(0);
2323
- await new Promise((resolve) => setTimeout(resolve, 50));
2324
- play();
2649
+ await seekTo(0);
2650
+ await play();
2325
2651
  }, [seekTo, play]);
2326
- useEffect5(() => {
2652
+ useEffect6(() => {
2327
2653
  if (!isPlaying || !fallbackMode.current || !totalDuration) return;
2328
2654
  let lastTime = performance.now();
2329
2655
  let raf;
@@ -2354,13 +2680,13 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2354
2680
  totalDuration,
2355
2681
  isEnded,
2356
2682
  isReady: isAudioReady,
2357
- isAvailable: true,
2358
- // HTML5 audio is always available in browsers
2683
+ isAvailable,
2684
+ unavailableMessage,
2359
2685
  // Actions
2360
- play: async () => play(),
2686
+ play,
2361
2687
  pause: async () => pause(),
2362
2688
  toggle: async () => toggle(),
2363
- seekTo: async (time) => seekTo(time),
2689
+ seekTo,
2364
2690
  skipToSegment: async (index) => skipToSegment(index),
2365
2691
  restart
2366
2692
  };
@@ -2505,7 +2831,7 @@ function useDocPlayback(script, currentTime, options = {}) {
2505
2831
  }
2506
2832
 
2507
2833
  // src/hooks/useViewportOrientation.ts
2508
- import { useState as useState5, useEffect as useEffect6, useMemo as useMemo7 } from "react";
2834
+ import { useState as useState6, useEffect as useEffect7, useMemo as useMemo7 } from "react";
2509
2835
  import {
2510
2836
  VIEWPORT_PRESETS as VIEWPORT_PRESETS2
2511
2837
  } from "@bendyline/squisq/doc";
@@ -2531,11 +2857,11 @@ function getViewportForOrientation(orientation) {
2531
2857
  }
2532
2858
  }
2533
2859
  function useViewportOrientation() {
2534
- const [windowSize, setWindowSize] = useState5(() => ({
2860
+ const [windowSize, setWindowSize] = useState6(() => ({
2535
2861
  width: typeof window !== "undefined" ? window.innerWidth : 1920,
2536
2862
  height: typeof window !== "undefined" ? window.innerHeight : 1080
2537
2863
  }));
2538
- useEffect6(() => {
2864
+ useEffect7(() => {
2539
2865
  if (typeof window === "undefined") return;
2540
2866
  const handleResize = () => {
2541
2867
  setWindowSize({
@@ -2567,7 +2893,7 @@ function useViewportOrientation() {
2567
2893
  }
2568
2894
 
2569
2895
  // src/hooks/useSlideSwipe.ts
2570
- import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState6 } from "react";
2896
+ import { useCallback as useCallback4, useEffect as useEffect8, useRef as useRef5, useState as useState7 } from "react";
2571
2897
  var DISTANCE_RATIO = 0.3;
2572
2898
  var FLICK_VELOCITY = 0.5;
2573
2899
  var MIN_FLICK_DISTANCE = 12;
@@ -2591,8 +2917,8 @@ function decideSwipe({
2591
2917
  }
2592
2918
  function useSlideSwipe(opts) {
2593
2919
  const settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
2594
- const [offsetPx, setOffsetPx] = useState6(0);
2595
- const [phase, setPhase] = useState6("idle");
2920
+ const [offsetPx, setOffsetPx] = useState7(0);
2921
+ const [phase, setPhase] = useState7("idle");
2596
2922
  const optsRef = useRef5(opts);
2597
2923
  optsRef.current = opts;
2598
2924
  const dragRef = useRef5(null);
@@ -2630,7 +2956,7 @@ function useSlideSwipe(opts) {
2630
2956
  setPhase("dragging");
2631
2957
  setOffsetPx(0);
2632
2958
  }, []);
2633
- useEffect7(() => {
2959
+ useEffect8(() => {
2634
2960
  function currentWidth() {
2635
2961
  return optsRef.current.containerRef.current?.getBoundingClientRect().width ?? 0;
2636
2962
  }
@@ -2700,7 +3026,7 @@ function useSlideSwipe(opts) {
2700
3026
  window.removeEventListener("pointercancel", onCancel);
2701
3027
  };
2702
3028
  }, [settleMs]);
2703
- useEffect7(() => {
3029
+ useEffect8(() => {
2704
3030
  if (!opts.enabled) {
2705
3031
  dragRef.current = null;
2706
3032
  clearPending();
@@ -2708,7 +3034,7 @@ function useSlideSwipe(opts) {
2708
3034
  setOffsetPx(0);
2709
3035
  }
2710
3036
  }, [opts.enabled, clearPending]);
2711
- useEffect7(() => clearPending, [clearPending]);
3037
+ useEffect8(() => clearPending, [clearPending]);
2712
3038
  return { offsetPx, phase, onPointerDown };
2713
3039
  }
2714
3040
 
@@ -2717,13 +3043,13 @@ import {
2717
3043
  expandCoverBlock,
2718
3044
  createTemplateContext,
2719
3045
  markdownToDoc as markdownToDoc2,
2720
- DEFAULT_THEME as DEFAULT_THEME3,
2721
- VIEWPORT_PRESETS as VIEWPORT_PRESETS4
3046
+ DEFAULT_THEME as DEFAULT_THEME4,
3047
+ VIEWPORT_PRESETS as VIEWPORT_PRESETS5
2722
3048
  } from "@bendyline/squisq/doc";
2723
3049
  import { parseMarkdown as parseMarkdown2 } from "@bendyline/squisq/markdown";
2724
3050
 
2725
3051
  // src/DocProgressBar.tsx
2726
- import { useRef as useRef6, useState as useState7, useCallback as useCallback5 } from "react";
3052
+ import { useRef as useRef6, useState as useState8, useCallback as useCallback5 } from "react";
2727
3053
 
2728
3054
  // src/types.ts
2729
3055
  function formatTime(seconds) {
@@ -2733,7 +3059,7 @@ function formatTime(seconds) {
2733
3059
  }
2734
3060
 
2735
3061
  // src/DocProgressBar.tsx
2736
- import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
3062
+ import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
2737
3063
  function DocProgressBar({
2738
3064
  state,
2739
3065
  actions,
@@ -2742,7 +3068,7 @@ function DocProgressBar({
2742
3068
  getBlockTitle
2743
3069
  }) {
2744
3070
  const progressBarRef = useRef6(null);
2745
- const [hoverPosition, setHoverPosition] = useState7(null);
3071
+ const [hoverPosition, setHoverPosition] = useState8(null);
2746
3072
  const playProgress = state.totalDuration > 0 ? Math.max(0, Math.min(1, state.currentTime / state.totalDuration)) : 0;
2747
3073
  const handleProgressHover = useCallback5((e) => {
2748
3074
  const bar = progressBarRef.current;
@@ -2792,7 +3118,7 @@ function DocProgressBar({
2792
3118
  },
2793
3119
  [expandedBlocks]
2794
3120
  );
2795
- return /* @__PURE__ */ jsxs9(
3121
+ return /* @__PURE__ */ jsxs10(
2796
3122
  "div",
2797
3123
  {
2798
3124
  ref: progressBarRef,
@@ -2815,7 +3141,7 @@ function DocProgressBar({
2815
3141
  onMouseMove: handleProgressHover,
2816
3142
  onMouseLeave: handleProgressLeave,
2817
3143
  children: [
2818
- /* @__PURE__ */ jsx14(
3144
+ /* @__PURE__ */ jsx16(
2819
3145
  "div",
2820
3146
  {
2821
3147
  role: "slider",
@@ -2836,7 +3162,7 @@ function DocProgressBar({
2836
3162
  }
2837
3163
  }
2838
3164
  ),
2839
- /* @__PURE__ */ jsx14(
3165
+ /* @__PURE__ */ jsx16(
2840
3166
  "div",
2841
3167
  {
2842
3168
  "data-testid": "doc-progress-fill",
@@ -2850,7 +3176,7 @@ function DocProgressBar({
2850
3176
  }
2851
3177
  }
2852
3178
  ),
2853
- blockMarkers.map((marker, i) => /* @__PURE__ */ jsx14(
3179
+ blockMarkers.map((marker, i) => /* @__PURE__ */ jsx16(
2854
3180
  "button",
2855
3181
  {
2856
3182
  type: "button",
@@ -2883,7 +3209,7 @@ function DocProgressBar({
2883
3209
  },
2884
3210
  `${marker.block.id}-${i}`
2885
3211
  )),
2886
- hoverPosition !== null && /* @__PURE__ */ jsxs9(
3212
+ hoverPosition !== null && /* @__PURE__ */ jsxs10(
2887
3213
  "div",
2888
3214
  {
2889
3215
  style: {
@@ -2900,19 +3226,19 @@ function DocProgressBar({
2900
3226
  zIndex: 10
2901
3227
  },
2902
3228
  children: [
2903
- /* @__PURE__ */ jsx14("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
3229
+ /* @__PURE__ */ jsx16("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
2904
3230
  (() => {
2905
3231
  const hoverTime = hoverPosition * state.totalDuration;
2906
3232
  const slideInfo = getBlockAtTimeLocal(hoverTime);
2907
3233
  if (slideInfo && getBlockTitle) {
2908
- return /* @__PURE__ */ jsx14("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
3234
+ return /* @__PURE__ */ jsx16("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
2909
3235
  }
2910
3236
  return null;
2911
3237
  })()
2912
3238
  ]
2913
3239
  }
2914
3240
  ),
2915
- hoverPosition !== null && /* @__PURE__ */ jsx14(
3241
+ hoverPosition !== null && /* @__PURE__ */ jsx16(
2916
3242
  "div",
2917
3243
  {
2918
3244
  style: {
@@ -2934,7 +3260,7 @@ function DocProgressBar({
2934
3260
  }
2935
3261
 
2936
3262
  // src/DocControlsOverlay.tsx
2937
- import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
3263
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
2938
3264
  function DocControlsOverlay({
2939
3265
  state,
2940
3266
  actions,
@@ -2942,7 +3268,7 @@ function DocControlsOverlay({
2942
3268
  expandedBlocks,
2943
3269
  getBlockTitle
2944
3270
  }) {
2945
- return /* @__PURE__ */ jsxs10(
3271
+ return /* @__PURE__ */ jsxs11(
2946
3272
  "div",
2947
3273
  {
2948
3274
  className: "doc-player__controls",
@@ -2959,7 +3285,7 @@ function DocControlsOverlay({
2959
3285
  zIndex: 100
2960
3286
  },
2961
3287
  children: [
2962
- /* @__PURE__ */ jsx15(
3288
+ /* @__PURE__ */ jsx17(
2963
3289
  "button",
2964
3290
  {
2965
3291
  onClick: actions.restart,
@@ -2975,10 +3301,10 @@ function DocControlsOverlay({
2975
3301
  },
2976
3302
  title: "Restart",
2977
3303
  "aria-label": "Restart from beginning",
2978
- children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
3304
+ children: /* @__PURE__ */ jsx17("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx17("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
2979
3305
  }
2980
3306
  ),
2981
- /* @__PURE__ */ jsx15(
3307
+ /* @__PURE__ */ jsx17(
2982
3308
  "button",
2983
3309
  {
2984
3310
  onClick: actions.toggle,
@@ -2997,15 +3323,15 @@ function DocControlsOverlay({
2997
3323
  height: "40px"
2998
3324
  },
2999
3325
  "aria-label": state.isPlaying ? "Pause" : "Play",
3000
- children: state.isPlaying ? /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx15("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx15("path", { d: "M8 5v14l11-7z" }) })
3326
+ children: state.isPlaying ? /* @__PURE__ */ jsx17("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx17("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx17("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx17("path", { d: "M8 5v14l11-7z" }) })
3001
3327
  }
3002
3328
  ),
3003
- /* @__PURE__ */ jsxs10("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
3329
+ /* @__PURE__ */ jsxs11("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
3004
3330
  formatTime(state.currentTime),
3005
3331
  " / ",
3006
3332
  formatTime(state.totalDuration)
3007
3333
  ] }),
3008
- /* @__PURE__ */ jsx15(
3334
+ /* @__PURE__ */ jsx17(
3009
3335
  DocProgressBar,
3010
3336
  {
3011
3337
  state,
@@ -3015,12 +3341,12 @@ function DocControlsOverlay({
3015
3341
  getBlockTitle
3016
3342
  }
3017
3343
  ),
3018
- /* @__PURE__ */ jsxs10("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
3344
+ /* @__PURE__ */ jsxs11("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
3019
3345
  state.currentBlockIndex + 1,
3020
3346
  "/",
3021
3347
  state.totalBlocks
3022
3348
  ] }),
3023
- state.hasCaptions && /* @__PURE__ */ jsxs10(
3349
+ state.hasCaptions && /* @__PURE__ */ jsxs11(
3024
3350
  "button",
3025
3351
  {
3026
3352
  onClick: () => actions.cycleCaptionMode(),
@@ -3039,12 +3365,12 @@ function DocControlsOverlay({
3039
3365
  title: state.captionMode === "off" ? "Captions: Off (click for Standard)" : state.captionMode === "standard" ? "Captions: Standard (click for Social)" : "Captions: Social (click to turn off)",
3040
3366
  "aria-label": "Cycle caption style",
3041
3367
  children: [
3042
- /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) }),
3043
- state.captionMode !== "off" && /* @__PURE__ */ jsx15("span", { style: { fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: state.captionMode === "standard" ? "CC" : "SM" })
3368
+ /* @__PURE__ */ jsx17("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx17("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) }),
3369
+ state.captionMode !== "off" && /* @__PURE__ */ jsx17("span", { style: { fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: state.captionMode === "standard" ? "CC" : "SM" })
3044
3370
  ]
3045
3371
  }
3046
3372
  ),
3047
- actions.toggleFullscreen && /* @__PURE__ */ jsx15(
3373
+ actions.toggleFullscreen && /* @__PURE__ */ jsx17(
3048
3374
  "button",
3049
3375
  {
3050
3376
  onClick: actions.toggleFullscreen,
@@ -3060,7 +3386,7 @@ function DocControlsOverlay({
3060
3386
  },
3061
3387
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
3062
3388
  "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
3063
- children: state.isFullscreen ? /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
3389
+ children: state.isFullscreen ? /* @__PURE__ */ jsx17("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx17("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx17("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx17("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
3064
3390
  }
3065
3391
  )
3066
3392
  ]
@@ -3069,8 +3395,8 @@ function DocControlsOverlay({
3069
3395
  }
3070
3396
 
3071
3397
  // src/DocControlsSlideshow.tsx
3072
- import { useCallback as useCallback6, useEffect as useEffect8, useId as useId6, useLayoutEffect, useRef as useRef7, useState as useState8 } from "react";
3073
- import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
3398
+ import { useCallback as useCallback6, useEffect as useEffect9, useId as useId6, useLayoutEffect, useRef as useRef7, useState as useState9 } from "react";
3399
+ import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
3074
3400
  function DocControlsSlideshow({
3075
3401
  state,
3076
3402
  slideNav,
@@ -3078,7 +3404,7 @@ function DocControlsSlideshow({
3078
3404
  pickerOpen,
3079
3405
  onPickerOpenChange
3080
3406
  }) {
3081
- const [uncontrolledPickerOpen, setUncontrolledPickerOpen] = useState8(false);
3407
+ const [uncontrolledPickerOpen, setUncontrolledPickerOpen] = useState9(false);
3082
3408
  const isPickerOpen = pickerOpen ?? uncontrolledPickerOpen;
3083
3409
  const setPickerOpen = useCallback6(
3084
3410
  (open) => {
@@ -3087,7 +3413,7 @@ function DocControlsSlideshow({
3087
3413
  },
3088
3414
  [pickerOpen, onPickerOpenChange]
3089
3415
  );
3090
- const [pickerMaxHeight, setPickerMaxHeight] = useState8(280);
3416
+ const [pickerMaxHeight, setPickerMaxHeight] = useState9(280);
3091
3417
  const controlsRef = useRef7(null);
3092
3418
  const triggerRef = useRef7(null);
3093
3419
  const menuRef = useRef7(null);
@@ -3102,7 +3428,7 @@ function DocControlsSlideshow({
3102
3428
  const isFirst = currentBlockIndex <= 0;
3103
3429
  const isLast = currentBlockIndex >= totalBlocks - 1;
3104
3430
  const counterText = totalBlocks > 0 ? currentSlideLabel ?? `${currentSlideNumber ?? currentBlockIndex + 1} / ${totalSlideNumber ?? totalBlocks}` : "\u2014";
3105
- useEffect8(() => {
3431
+ useEffect9(() => {
3106
3432
  if (!isPickerOpen) return;
3107
3433
  const handlePointerDown = (event) => {
3108
3434
  if (!controlsRef.current?.contains(event.target)) setPickerOpen(false);
@@ -3141,7 +3467,7 @@ function DocControlsSlideshow({
3141
3467
  window.removeEventListener("resize", updateMaxHeight);
3142
3468
  };
3143
3469
  }, [isPickerOpen]);
3144
- useEffect8(() => {
3470
+ useEffect9(() => {
3145
3471
  if (!isPickerOpen) return;
3146
3472
  menuRef.current?.querySelector('[aria-current="true"]')?.focus();
3147
3473
  }, [isPickerOpen]);
@@ -3160,7 +3486,7 @@ function DocControlsSlideshow({
3160
3486
  else nextIndex = (focusedIndex - 1 + items.length) % items.length;
3161
3487
  items[nextIndex]?.focus();
3162
3488
  };
3163
- return /* @__PURE__ */ jsxs11(
3489
+ return /* @__PURE__ */ jsxs12(
3164
3490
  "div",
3165
3491
  {
3166
3492
  ref: controlsRef,
@@ -3182,7 +3508,7 @@ function DocControlsSlideshow({
3182
3508
  WebkitBackdropFilter: "blur(8px)"
3183
3509
  },
3184
3510
  children: [
3185
- /* @__PURE__ */ jsx16(
3511
+ /* @__PURE__ */ jsx18(
3186
3512
  "button",
3187
3513
  {
3188
3514
  onClick: (e) => {
@@ -3211,10 +3537,10 @@ function DocControlsSlideshow({
3211
3537
  onMouseLeave: (e) => {
3212
3538
  e.currentTarget.style.background = "none";
3213
3539
  },
3214
- children: /* @__PURE__ */ jsx16("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx16("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
3540
+ children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx18("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
3215
3541
  }
3216
3542
  ),
3217
- /* @__PURE__ */ jsx16(
3543
+ /* @__PURE__ */ jsx18(
3218
3544
  "button",
3219
3545
  {
3220
3546
  ref: triggerRef,
@@ -3248,7 +3574,7 @@ function DocControlsSlideshow({
3248
3574
  children: counterText
3249
3575
  }
3250
3576
  ),
3251
- isPickerOpen && /* @__PURE__ */ jsx16(
3577
+ isPickerOpen && /* @__PURE__ */ jsx18(
3252
3578
  "div",
3253
3579
  {
3254
3580
  ref: menuRef,
@@ -3275,7 +3601,7 @@ function DocControlsSlideshow({
3275
3601
  },
3276
3602
  children: slides.map((slide, index) => {
3277
3603
  const isCurrent = index === currentBlockIndex;
3278
- return /* @__PURE__ */ jsxs11(
3604
+ return /* @__PURE__ */ jsxs12(
3279
3605
  "button",
3280
3606
  {
3281
3607
  type: "button",
@@ -3308,7 +3634,7 @@ function DocControlsSlideshow({
3308
3634
  event.currentTarget.style.background = isCurrent ? "rgba(255,255,255,0.14)" : "transparent";
3309
3635
  },
3310
3636
  children: [
3311
- /* @__PURE__ */ jsx16(
3637
+ /* @__PURE__ */ jsx18(
3312
3638
  "span",
3313
3639
  {
3314
3640
  style: {
@@ -3320,7 +3646,7 @@ function DocControlsSlideshow({
3320
3646
  children: slide.label
3321
3647
  }
3322
3648
  ),
3323
- /* @__PURE__ */ jsx16(
3649
+ /* @__PURE__ */ jsx18(
3324
3650
  "span",
3325
3651
  {
3326
3652
  style: {
@@ -3342,7 +3668,7 @@ function DocControlsSlideshow({
3342
3668
  })
3343
3669
  }
3344
3670
  ),
3345
- /* @__PURE__ */ jsx16(
3671
+ /* @__PURE__ */ jsx18(
3346
3672
  "button",
3347
3673
  {
3348
3674
  onClick: (e) => {
@@ -3371,7 +3697,7 @@ function DocControlsSlideshow({
3371
3697
  onMouseLeave: (e) => {
3372
3698
  e.currentTarget.style.background = "none";
3373
3699
  },
3374
- children: /* @__PURE__ */ jsx16("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx16("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
3700
+ children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx18("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
3375
3701
  }
3376
3702
  )
3377
3703
  ]
@@ -3380,20 +3706,85 @@ function DocControlsSlideshow({
3380
3706
  }
3381
3707
 
3382
3708
  // src/LinearDocView.tsx
3383
- import { useEffect as useEffect9, useMemo as useMemo8, useRef as useRef8 } from "react";
3709
+ import { useEffect as useEffect10, useMemo as useMemo9, useRef as useRef8 } from "react";
3384
3710
  import {
3385
3711
  applySurface,
3386
3712
  resolveFontFamily as resolveFontFamily2
3387
3713
  } from "@bendyline/squisq/schemas";
3388
- import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
3714
+ import { VIEWPORT_PRESETS as VIEWPORT_PRESETS4 } from "@bendyline/squisq/schemas";
3389
3715
  import {
3390
- materializeBlockLayers,
3391
3716
  markdownToDoc,
3392
- DEFAULT_THEME as DEFAULT_THEME2,
3393
- deriveTemplateInputs,
3394
- isTemplateBlock as isTemplateBlock2
3717
+ materializePageSections,
3718
+ resolvePageStyle as resolvePageStyle2,
3719
+ buildPageCssVars,
3720
+ pageStyleDataAttributes,
3721
+ PAGE_BASE_CSS,
3722
+ DEFAULT_THEME as DEFAULT_THEME3
3395
3723
  } from "@bendyline/squisq/doc";
3396
- import { extractPlainText, parseMarkdown } from "@bendyline/squisq/markdown";
3724
+ import { parseMarkdown } from "@bendyline/squisq/markdown";
3725
+
3726
+ // src/page/PageViewContext.ts
3727
+ import { createContext as createContext2, useContext as useContext2 } from "react";
3728
+ import { DEFAULT_THEME as DEFAULT_THEME2, resolvePageStyle } from "@bendyline/squisq/doc";
3729
+ import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
3730
+ var defaultValue = {
3731
+ theme: DEFAULT_THEME2,
3732
+ pageStyle: resolvePageStyle(DEFAULT_THEME2),
3733
+ basePath: "/",
3734
+ viewport: VIEWPORT_PRESETS3.landscape,
3735
+ renderContext: { theme: DEFAULT_THEME2 },
3736
+ animationsEnabled: true,
3737
+ imageDisplayMode: "inline"
3738
+ };
3739
+ var PageViewContext = createContext2(defaultValue);
3740
+ function usePageView() {
3741
+ return useContext2(PageViewContext);
3742
+ }
3743
+
3744
+ // src/page/CanvasSection.tsx
3745
+ import { useMemo as useMemo8 } from "react";
3746
+ import { materializeBlockLayers } from "@bendyline/squisq/doc";
3747
+ import { jsx as jsx19 } from "react/jsx-runtime";
3748
+ function CanvasSection({ section }) {
3749
+ const { renderContext, basePath, animationsEnabled } = usePageView();
3750
+ const media = section.slots.media;
3751
+ const canvas = media?.type === "canvas" ? media : void 0;
3752
+ const visualBlock = useMemo8(() => {
3753
+ if (!canvas) return null;
3754
+ const { layers } = materializeBlockLayers(canvas.block, {
3755
+ ...renderContext,
3756
+ blockIndex: section.index
3757
+ });
3758
+ return { ...canvas.block, layers };
3759
+ }, [canvas, renderContext, section.index]);
3760
+ if (!canvas || !visualBlock) return null;
3761
+ const aspect = canvas.aspect;
3762
+ const frame = section.hints?.frame;
3763
+ const frameClass = frame === "terminal" ? " squisq-page-canvas--terminal squisq-page-canvas--framed" : " squisq-page-canvas--framed";
3764
+ return /* @__PURE__ */ jsx19(
3765
+ "div",
3766
+ {
3767
+ className: `squisq-page-canvas${frameClass}`,
3768
+ style: {
3769
+ aspectRatio: `${aspect.width} / ${aspect.height}`,
3770
+ // Clamp tall canvases; derive the width cap from the aspect so a
3771
+ // portrait viewport doesn't produce a page-height skyscraper.
3772
+ maxHeight: "min(70vh, 720px)",
3773
+ maxWidth: `calc(min(70vh, 720px) * ${(aspect.width / Math.max(1, aspect.height)).toFixed(4)})`
3774
+ },
3775
+ children: /* @__PURE__ */ jsx19(
3776
+ BlockRenderer,
3777
+ {
3778
+ block: visualBlock,
3779
+ blockTime: 0,
3780
+ basePath,
3781
+ viewport: { width: aspect.width, height: aspect.height },
3782
+ animationsEnabled
3783
+ }
3784
+ )
3785
+ }
3786
+ );
3787
+ }
3397
3788
 
3398
3789
  // src/MarkdownRenderer.tsx
3399
3790
  import { Fragment as Fragment2 } from "react";
@@ -3403,7 +3794,7 @@ import {
3403
3794
  } from "@bendyline/squisq/markdown";
3404
3795
 
3405
3796
  // src/InlineVideoPlayer.tsx
3406
- import { jsx as jsx17 } from "react/jsx-runtime";
3797
+ import { jsx as jsx20 } from "react/jsx-runtime";
3407
3798
  function InlineVideoPlayer({
3408
3799
  src,
3409
3800
  basePath = "",
@@ -3418,7 +3809,7 @@ function InlineVideoPlayer({
3418
3809
  const resolvedPoster = useMediaUrl(poster ?? "", basePath);
3419
3810
  const posterUrl = poster ? resolvedPoster : void 0;
3420
3811
  if (!resolvedSrc) return null;
3421
- return /* @__PURE__ */ jsx17("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx17(
3812
+ return /* @__PURE__ */ jsx20("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx20(
3422
3813
  "video",
3423
3814
  {
3424
3815
  src: resolvedSrc,
@@ -3433,7 +3824,7 @@ function InlineVideoPlayer({
3433
3824
  }
3434
3825
 
3435
3826
  // src/InlineAudioPlayer.tsx
3436
- import { jsx as jsx18 } from "react/jsx-runtime";
3827
+ import { jsx as jsx21 } from "react/jsx-runtime";
3437
3828
  function InlineAudioPlayer({
3438
3829
  src,
3439
3830
  basePath = "",
@@ -3443,11 +3834,11 @@ function InlineAudioPlayer({
3443
3834
  }) {
3444
3835
  const resolvedSrc = useMediaUrl(src, basePath);
3445
3836
  if (!resolvedSrc) return null;
3446
- return /* @__PURE__ */ jsx18("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx18("audio", { src: resolvedSrc, controls, preload }) });
3837
+ return /* @__PURE__ */ jsx21("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx21("audio", { src: resolvedSrc, controls, preload }) });
3447
3838
  }
3448
3839
 
3449
3840
  // src/MarkdownRenderer.tsx
3450
- import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3841
+ import { jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
3451
3842
  var DEFAULT_CTX = { htmlPolicy: "sanitize" };
3452
3843
  function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
3453
3844
  return nodes.map((node, i) => {
@@ -3455,28 +3846,28 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
3455
3846
  switch (node.type) {
3456
3847
  case "text": {
3457
3848
  if (!node.value.includes("\n")) {
3458
- return /* @__PURE__ */ jsx19(Fragment2, { children: node.value }, key);
3849
+ return /* @__PURE__ */ jsx22(Fragment2, { children: node.value }, key);
3459
3850
  }
3460
3851
  const parts = node.value.split("\n");
3461
- return /* @__PURE__ */ jsx19(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs12(Fragment2, { children: [
3462
- j > 0 && /* @__PURE__ */ jsx19("br", {}),
3852
+ return /* @__PURE__ */ jsx22(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs13(Fragment2, { children: [
3853
+ j > 0 && /* @__PURE__ */ jsx22("br", {}),
3463
3854
  part
3464
3855
  ] }, j)) }, key);
3465
3856
  }
3466
3857
  case "emphasis":
3467
- return /* @__PURE__ */ jsx19("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
3858
+ return /* @__PURE__ */ jsx22("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
3468
3859
  case "strong":
3469
- return /* @__PURE__ */ jsx19("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
3860
+ return /* @__PURE__ */ jsx22("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
3470
3861
  case "delete":
3471
- return /* @__PURE__ */ jsx19("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
3862
+ return /* @__PURE__ */ jsx22("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
3472
3863
  case "inlineCode":
3473
- return /* @__PURE__ */ jsx19("code", { className: "squisq-md-inline-code", children: node.value }, key);
3864
+ return /* @__PURE__ */ jsx22("code", { className: "squisq-md-inline-code", children: node.value }, key);
3474
3865
  case "link": {
3475
3866
  const href = sanitizeUrl(node.url, "link", { extraLinkSchemes: ctx.linkSchemes });
3476
3867
  if (!href) {
3477
- return /* @__PURE__ */ jsx19("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
3868
+ return /* @__PURE__ */ jsx22("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
3478
3869
  }
3479
- return /* @__PURE__ */ jsx19(
3870
+ return /* @__PURE__ */ jsx22(
3480
3871
  "a",
3481
3872
  {
3482
3873
  className: "squisq-md-link",
@@ -3490,32 +3881,32 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
3490
3881
  );
3491
3882
  }
3492
3883
  case "image":
3493
- return /* @__PURE__ */ jsx19(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
3884
+ return /* @__PURE__ */ jsx22(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
3494
3885
  case "break":
3495
- return /* @__PURE__ */ jsx19("br", {}, key);
3886
+ return /* @__PURE__ */ jsx22("br", {}, key);
3496
3887
  case "inlineMath":
3497
- return /* @__PURE__ */ jsx19("code", { className: "squisq-md-inline-math", children: node.value }, key);
3888
+ return /* @__PURE__ */ jsx22("code", { className: "squisq-md-inline-math", children: node.value }, key);
3498
3889
  case "htmlInline":
3499
3890
  if (ctx.htmlPolicy === "strip") return null;
3500
- return /* @__PURE__ */ jsx19("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
3891
+ return /* @__PURE__ */ jsx22("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
3501
3892
  case "footnoteReference":
3502
- return /* @__PURE__ */ jsx19("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs12("a", { href: `#fn-${node.identifier}`, children: [
3893
+ return /* @__PURE__ */ jsx22("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs13("a", { href: `#fn-${node.identifier}`, children: [
3503
3894
  "[",
3504
3895
  node.label ?? node.identifier,
3505
3896
  "]"
3506
3897
  ] }) }, key);
3507
3898
  case "linkReference":
3508
- return /* @__PURE__ */ jsx19("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
3899
+ return /* @__PURE__ */ jsx22("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
3509
3900
  case "imageReference":
3510
- return /* @__PURE__ */ jsxs12("span", { className: "squisq-md-image-ref", children: [
3901
+ return /* @__PURE__ */ jsxs13("span", { className: "squisq-md-image-ref", children: [
3511
3902
  "[",
3512
3903
  node.alt ?? node.identifier,
3513
3904
  "]"
3514
3905
  ] }, key);
3515
3906
  case "textDirective":
3516
- return /* @__PURE__ */ jsx19("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
3907
+ return /* @__PURE__ */ jsx22("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
3517
3908
  case "mention":
3518
- return /* @__PURE__ */ jsxs12(
3909
+ return /* @__PURE__ */ jsxs13(
3519
3910
  "span",
3520
3911
  {
3521
3912
  className: "squisq-md-mention mention",
@@ -3538,51 +3929,62 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
3538
3929
  function renderBlock(node, key, ctx = DEFAULT_CTX) {
3539
3930
  switch (node.type) {
3540
3931
  case "paragraph":
3541
- return /* @__PURE__ */ jsx19("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
3932
+ return /* @__PURE__ */ jsx22("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
3542
3933
  case "heading": {
3543
3934
  const Tag = `h${node.depth}`;
3544
- return /* @__PURE__ */ jsx19(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
3935
+ return /* @__PURE__ */ jsx22(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
3545
3936
  }
3546
3937
  case "blockquote":
3547
- return /* @__PURE__ */ jsx19("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
3938
+ return /* @__PURE__ */ jsx22("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
3548
3939
  case "list":
3549
3940
  if (node.ordered) {
3550
- return /* @__PURE__ */ jsx19("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3941
+ return /* @__PURE__ */ jsx22("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3551
3942
  }
3552
- return /* @__PURE__ */ jsx19("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3943
+ return /* @__PURE__ */ jsx22("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3553
3944
  case "code":
3554
- return /* @__PURE__ */ jsx19("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx19("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
3945
+ if (node.lang?.trim().toLowerCase() === "mermaid") {
3946
+ return /* @__PURE__ */ jsx22(
3947
+ MermaidDiagram,
3948
+ {
3949
+ source: node.value,
3950
+ className: "squisq-md-mermaid",
3951
+ ariaLabel: "Mermaid diagram"
3952
+ },
3953
+ key
3954
+ );
3955
+ }
3956
+ return /* @__PURE__ */ jsx22("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx22("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
3555
3957
  case "thematicBreak":
3556
- return /* @__PURE__ */ jsx19("hr", { className: "squisq-md-hr" }, key);
3958
+ return /* @__PURE__ */ jsx22("hr", { className: "squisq-md-hr" }, key);
3557
3959
  case "table":
3558
3960
  return renderTable(node.children, node.align, key, ctx);
3559
3961
  case "htmlBlock":
3560
3962
  if (ctx.htmlPolicy === "strip") return null;
3561
- return /* @__PURE__ */ jsx19("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
3963
+ return /* @__PURE__ */ jsx22("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
3562
3964
  case "math":
3563
- return /* @__PURE__ */ jsx19("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx19("code", { children: node.value }) }, key);
3965
+ return /* @__PURE__ */ jsx22("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx22("code", { children: node.value }) }, key);
3564
3966
  case "definition":
3565
3967
  return null;
3566
3968
  case "footnoteDefinition":
3567
- return /* @__PURE__ */ jsxs12("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
3568
- /* @__PURE__ */ jsx19("sup", { children: node.label ?? node.identifier }),
3969
+ return /* @__PURE__ */ jsxs13("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
3970
+ /* @__PURE__ */ jsx22("sup", { children: node.label ?? node.identifier }),
3569
3971
  renderBlocks(node.children, key, ctx)
3570
3972
  ] }, key);
3571
3973
  case "containerDirective":
3572
- return /* @__PURE__ */ jsxs12(
3974
+ return /* @__PURE__ */ jsxs13(
3573
3975
  "div",
3574
3976
  {
3575
3977
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
3576
3978
  "data-directive": node.name,
3577
3979
  children: [
3578
- node.label && /* @__PURE__ */ jsx19("div", { className: "squisq-md-directive-label", children: node.label }),
3980
+ node.label && /* @__PURE__ */ jsx22("div", { className: "squisq-md-directive-label", children: node.label }),
3579
3981
  renderBlocks(node.children, key, ctx)
3580
3982
  ]
3581
3983
  },
3582
3984
  key
3583
3985
  );
3584
3986
  case "leafDirective":
3585
- return /* @__PURE__ */ jsx19(
3987
+ return /* @__PURE__ */ jsx22(
3586
3988
  "div",
3587
3989
  {
3588
3990
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
@@ -3592,11 +3994,11 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
3592
3994
  key
3593
3995
  );
3594
3996
  case "definitionList":
3595
- return /* @__PURE__ */ jsx19("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
3997
+ return /* @__PURE__ */ jsx22("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
3596
3998
  if (child.type === "definitionTerm") {
3597
- return /* @__PURE__ */ jsx19("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
3999
+ return /* @__PURE__ */ jsx22("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
3598
4000
  }
3599
- return /* @__PURE__ */ jsx19("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
4001
+ return /* @__PURE__ */ jsx22("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
3600
4002
  }) }, key);
3601
4003
  default:
3602
4004
  return null;
@@ -3604,15 +4006,15 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
3604
4006
  }
3605
4007
  function renderListItem(item, key, ctx = DEFAULT_CTX) {
3606
4008
  const isTask = item.checked !== null && item.checked !== void 0;
3607
- return /* @__PURE__ */ jsxs12("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
3608
- isTask && /* @__PURE__ */ jsx19("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
4009
+ return /* @__PURE__ */ jsxs13("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
4010
+ isTask && /* @__PURE__ */ jsx22("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
3609
4011
  renderBlocks(item.children, key, ctx)
3610
4012
  ] }, key);
3611
4013
  }
3612
4014
  function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
3613
4015
  const [headerRow, ...bodyRows] = rows;
3614
- return /* @__PURE__ */ jsxs12("table", { className: "squisq-md-table", children: [
3615
- headerRow && /* @__PURE__ */ jsx19("thead", { children: /* @__PURE__ */ jsx19("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx19(
4016
+ return /* @__PURE__ */ jsxs13("table", { className: "squisq-md-table", children: [
4017
+ headerRow && /* @__PURE__ */ jsx22("thead", { children: /* @__PURE__ */ jsx22("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx22(
3616
4018
  "th",
3617
4019
  {
3618
4020
  className: "squisq-md-th",
@@ -3621,7 +4023,7 @@ function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
3621
4023
  },
3622
4024
  `${key}th${ci}`
3623
4025
  )) }) }),
3624
- bodyRows.length > 0 && /* @__PURE__ */ jsx19("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx19("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx19(
4026
+ bodyRows.length > 0 && /* @__PURE__ */ jsx22("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx22("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx22(
3625
4027
  "td",
3626
4028
  {
3627
4029
  className: "squisq-md-td",
@@ -3639,7 +4041,7 @@ function MdImage({ src, alt, title }) {
3639
4041
  const safeSrc = sanitizeUrl(src, "media");
3640
4042
  const resolved = useMediaUrl(safeSrc ?? "", ".");
3641
4043
  if (!safeSrc) return null;
3642
- return /* @__PURE__ */ jsx19("img", { className: "squisq-md-image", src: resolved, alt, title });
4044
+ return /* @__PURE__ */ jsx22("img", { className: "squisq-md-image", src: resolved, alt, title });
3643
4045
  }
3644
4046
  function resolveHtmlNodes(nodes, htmlPolicy) {
3645
4047
  if (htmlPolicy === "strip") return [];
@@ -3675,6 +4077,7 @@ var PASSTHROUGH_ATTRS = {
3675
4077
  target: "target",
3676
4078
  rel: "rel"
3677
4079
  };
4080
+ var RE_USED_TARGETS = /* @__PURE__ */ new Set(["", "_self", "_parent", "_top"]);
3678
4081
  function reactPropsFromAttrs(attrs, ctx) {
3679
4082
  const out = {};
3680
4083
  for (const [name, value] of Object.entries(attrs)) {
@@ -3696,6 +4099,12 @@ function reactPropsFromAttrs(attrs, ctx) {
3696
4099
  }
3697
4100
  out[propName] = value;
3698
4101
  }
4102
+ if (typeof out.target === "string" && !RE_USED_TARGETS.has(out.target.toLowerCase())) {
4103
+ const rel = typeof out.rel === "string" ? out.rel : "";
4104
+ const tokens = rel.split(/\s+/).filter((t) => t && t.toLowerCase() !== "opener");
4105
+ if (!tokens.some((t) => t.toLowerCase() === "noopener")) tokens.push("noopener");
4106
+ out.rel = tokens.join(" ");
4107
+ }
3699
4108
  return out;
3700
4109
  }
3701
4110
  function renderHtmlElement(el, key, ctx) {
@@ -3704,7 +4113,7 @@ function renderHtmlElement(el, key, ctx) {
3704
4113
  if (tagName === "video") {
3705
4114
  const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
3706
4115
  const poster = sanitizeUrl(el.attributes.poster ?? "", "media") ?? void 0;
3707
- return /* @__PURE__ */ jsx19(
4116
+ return /* @__PURE__ */ jsx22(
3708
4117
  InlineVideoPlayer,
3709
4118
  {
3710
4119
  src,
@@ -3719,7 +4128,7 @@ function renderHtmlElement(el, key, ctx) {
3719
4128
  }
3720
4129
  if (tagName === "audio") {
3721
4130
  const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
3722
- return /* @__PURE__ */ jsx19(
4131
+ return /* @__PURE__ */ jsx22(
3723
4132
  InlineAudioPlayer,
3724
4133
  {
3725
4134
  src,
@@ -3732,9 +4141,9 @@ function renderHtmlElement(el, key, ctx) {
3732
4141
  const Tag = tagName;
3733
4142
  const props = reactPropsFromAttrs(el.attributes, ctx);
3734
4143
  if (el.selfClosing) {
3735
- return /* @__PURE__ */ jsx19(Tag, { ...props }, key);
4144
+ return /* @__PURE__ */ jsx22(Tag, { ...props }, key);
3736
4145
  }
3737
- return /* @__PURE__ */ jsx19(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`, ctx) }, key);
4146
+ return /* @__PURE__ */ jsx22(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`, ctx) }, key);
3738
4147
  }
3739
4148
  function renderHtmlNodes(nodes, keyPrefix, ctx = DEFAULT_CTX) {
3740
4149
  return nodes.map((node, i) => {
@@ -3743,7 +4152,7 @@ function renderHtmlNodes(nodes, keyPrefix, ctx = DEFAULT_CTX) {
3743
4152
  case "htmlElement":
3744
4153
  return renderHtmlElement(node, key, ctx);
3745
4154
  case "htmlText":
3746
- return /* @__PURE__ */ jsx19(Fragment2, { children: node.value }, key);
4155
+ return /* @__PURE__ */ jsx22(Fragment2, { children: node.value }, key);
3747
4156
  case "htmlComment":
3748
4157
  return null;
3749
4158
  default:
@@ -3758,123 +4167,362 @@ function MarkdownRenderer({
3758
4167
  linkSchemes
3759
4168
  }) {
3760
4169
  if (!nodes || nodes.length === 0) return null;
3761
- return /* @__PURE__ */ jsx19("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes }) });
4170
+ return /* @__PURE__ */ jsx22("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes }) });
3762
4171
  }
3763
4172
 
3764
- // src/LinearDocView.tsx
3765
- import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
3766
- function isAnnotatedBlock(block) {
3767
- return !!block.sourceHeading?.templateAnnotation?.template || !block.sourceHeading && isTemplateBlock2(block);
3768
- }
3769
- function visualTemplateName(block) {
3770
- return block.sourceHeading?.templateAnnotation?.template ?? block.template;
3771
- }
3772
- function countAll(blocks) {
3773
- let count = 0;
3774
- for (const b of blocks) {
3775
- count++;
3776
- if (b.children) count += countAll(b.children);
4173
+ // src/page/sections.tsx
4174
+ import { sanitizeUrl as sanitizeUrl2 } from "@bendyline/squisq/markdown";
4175
+ import { Fragment as Fragment3, jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
4176
+ function PageImg({
4177
+ media,
4178
+ eager,
4179
+ className
4180
+ }) {
4181
+ const { basePath } = usePageView();
4182
+ const safeSrc = sanitizeUrl2(media.src, "media");
4183
+ const resolved = useMediaUrl(safeSrc ?? "", basePath);
4184
+ if (!safeSrc) return null;
4185
+ return /* @__PURE__ */ jsx23(
4186
+ "img",
4187
+ {
4188
+ className,
4189
+ src: resolved,
4190
+ alt: media.alt,
4191
+ loading: eager ? "eager" : "lazy",
4192
+ decoding: "async",
4193
+ width: media.width,
4194
+ height: media.height
4195
+ }
4196
+ );
4197
+ }
4198
+ function BackdropVideo({ media }) {
4199
+ const { basePath } = usePageView();
4200
+ const safeSrc = sanitizeUrl2(media.src, "media");
4201
+ const resolvedSrc = useMediaUrl(safeSrc ?? "", basePath);
4202
+ const safePoster = media.posterSrc ? sanitizeUrl2(media.posterSrc, "media") : void 0;
4203
+ const resolvedPoster = useMediaUrl(safePoster ?? "", basePath);
4204
+ if (!safeSrc) return null;
4205
+ return /* @__PURE__ */ jsx23(
4206
+ "video",
4207
+ {
4208
+ src: resolvedSrc,
4209
+ poster: safePoster ? resolvedPoster : void 0,
4210
+ muted: true,
4211
+ loop: true,
4212
+ autoPlay: true,
4213
+ playsInline: true,
4214
+ preload: "metadata"
4215
+ }
4216
+ );
4217
+ }
4218
+ function Eyebrow({ section }) {
4219
+ if (!section.slots.eyebrow) return null;
4220
+ return /* @__PURE__ */ jsx23("span", { className: "squisq-page-eyebrow", children: section.slots.eyebrow });
4221
+ }
4222
+ function MediaCredit({ media }) {
4223
+ if (!media || media.type === "canvas" || !media.credit) return null;
4224
+ return /* @__PURE__ */ jsxs14("span", { className: "squisq-page-media-credit", children: [
4225
+ media.credit,
4226
+ media.license ? ` \xB7 ${media.license}` : ""
4227
+ ] });
4228
+ }
4229
+ function HeroSection({ section }) {
4230
+ const { pageStyle } = usePageView();
4231
+ const media = section.slots.media;
4232
+ const image = media?.type === "image" ? media : void 0;
4233
+ const heroStyle = pageStyle.tokens.heroStyle;
4234
+ const body = /* @__PURE__ */ jsxs14("div", { className: "squisq-page-hero-body", children: [
4235
+ /* @__PURE__ */ jsx23(Eyebrow, { section }),
4236
+ /* @__PURE__ */ jsx23("h1", { className: "squisq-page-hero-title", children: section.slots.title }),
4237
+ section.slots.subtitle && /* @__PURE__ */ jsx23("p", { className: "squisq-page-hero-subtitle", children: section.slots.subtitle })
4238
+ ] });
4239
+ if (image && heroStyle === "split") {
4240
+ return /* @__PURE__ */ jsxs14("div", { className: "squisq-page-hero-grid", children: [
4241
+ body,
4242
+ /* @__PURE__ */ jsxs14("div", { className: "squisq-page-hero-media squisq-page-media-frame", children: [
4243
+ /* @__PURE__ */ jsx23(PageImg, { media: image, eager: true }),
4244
+ /* @__PURE__ */ jsx23(MediaCredit, { media: image })
4245
+ ] })
4246
+ ] });
3777
4247
  }
3778
- return count;
4248
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
4249
+ image && section.background === "media" && /* @__PURE__ */ jsx23("div", { className: "squisq-page-hero-backdrop", children: /* @__PURE__ */ jsx23(PageImg, { media: image, eager: true }) }),
4250
+ body
4251
+ ] });
3779
4252
  }
3780
- function BlockSection({
3781
- block,
3782
- basePath,
3783
- viewport,
3784
- renderContext,
3785
- blockIndex,
3786
- blockIndices,
3787
- animationsEnabled
4253
+ function BannerSection({ section }) {
4254
+ const media = section.slots.media;
4255
+ const image = media?.type === "image" ? media : void 0;
4256
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
4257
+ image && section.background === "media" && /* @__PURE__ */ jsx23("div", { className: "squisq-page-hero-backdrop", children: /* @__PURE__ */ jsx23(PageImg, { media: image }) }),
4258
+ /* @__PURE__ */ jsxs14("div", { className: "squisq-page-banner-body", children: [
4259
+ /* @__PURE__ */ jsx23(Eyebrow, { section }),
4260
+ /* @__PURE__ */ jsx23("h2", { className: "squisq-page-section-title squisq-page-banner-title", children: section.slots.title })
4261
+ ] })
4262
+ ] });
4263
+ }
4264
+ function StatBandSection({ section }) {
4265
+ const items = section.slots.items ?? [];
4266
+ const comparison = section.variant === "comparison";
4267
+ const numericValues = items.map(
4268
+ (item) => typeof item.value === "number" ? item.value : Number.parseFloat(String(item.value ?? ""))
4269
+ );
4270
+ const maxValue = Math.max(1, ...numericValues.filter((value) => Number.isFinite(value)));
4271
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
4272
+ /* @__PURE__ */ jsx23("div", { className: "squisq-page-stats", children: items.map((item, i) => /* @__PURE__ */ jsxs14("div", { className: `squisq-page-stat${comparison ? " squisq-page-stat--bars" : ""}`, children: [
4273
+ /* @__PURE__ */ jsx23("span", { className: "squisq-page-stat-value", children: item.value }),
4274
+ comparison && Number.isFinite(numericValues[i]) && /* @__PURE__ */ jsx23(
4275
+ "div",
4276
+ {
4277
+ className: "squisq-page-stat-bar",
4278
+ style: { width: `${Math.max(4, numericValues[i] / maxValue * 100)}%` }
4279
+ }
4280
+ ),
4281
+ item.title && /* @__PURE__ */ jsx23("span", { className: "squisq-page-stat-title", children: item.title }),
4282
+ item.body && /* @__PURE__ */ jsx23("span", { className: "squisq-page-stat-body", children: item.body })
4283
+ ] }, i)) }),
4284
+ section.slots.meta && /* @__PURE__ */ jsx23("div", { className: "squisq-page-stat-meta", children: section.slots.meta })
4285
+ ] });
4286
+ }
4287
+ function QuoteBandSection({ section }) {
4288
+ const media = section.slots.media;
4289
+ const backdrop = section.background === "media" && media && media.type !== "canvas";
4290
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
4291
+ backdrop && /* @__PURE__ */ jsx23("div", { className: "squisq-page-quote-backdrop", children: media.type === "image" ? /* @__PURE__ */ jsx23(PageImg, { media }) : /* @__PURE__ */ jsx23(BackdropVideo, { media }) }),
4292
+ /* @__PURE__ */ jsxs14("figure", { className: "squisq-page-quote-figure", style: { margin: 0 }, children: [
4293
+ /* @__PURE__ */ jsx23("blockquote", { className: "squisq-page-quote", children: section.slots.body?.text }),
4294
+ section.slots.attribution && /* @__PURE__ */ jsx23("figcaption", { className: "squisq-page-quote-attribution", children: section.slots.attribution })
4295
+ ] })
4296
+ ] });
4297
+ }
4298
+ function FeatureSplitSection({
4299
+ section,
4300
+ flip
3788
4301
  }) {
3789
- const isAnnotated = isAnnotatedBlock(block);
3790
- const visualBlock = useMemo8(() => {
3791
- if (!isAnnotated) return null;
3792
- const annotation = block.sourceHeading?.templateAnnotation;
3793
- const templateName = visualTemplateName(block) ?? "sectionHeader";
3794
- const templateBlock = annotation ? (() => {
3795
- const headingText = extractPlainText(block.sourceHeading);
3796
- return {
3797
- id: block.id,
3798
- template: templateName,
3799
- startTime: 0,
3800
- duration: 1,
3801
- audioSegment: 0,
3802
- title: headingText,
3803
- contents: block.contents,
3804
- children: block.children,
3805
- ...deriveTemplateInputs(templateName, headingText, block.contents, {
3806
- placeholders: true
3807
- }) ?? {},
3808
- ...annotation.params,
3809
- ...block.templateOverrides
3810
- };
3811
- })() : {
3812
- ...block,
3813
- startTime: block.startTime ?? 0,
3814
- duration: block.duration ?? 1,
3815
- audioSegment: block.audioSegment ?? 0,
3816
- template: templateName
3817
- };
3818
- const ctx = {
3819
- ...renderContext,
3820
- blockIndex
3821
- };
3822
- const { layers } = materializeBlockLayers(templateBlock, ctx);
3823
- return {
3824
- ...block,
3825
- layers,
3826
- template: templateName
3827
- };
3828
- }, [block, isAnnotated, renderContext, blockIndex]);
3829
- return /* @__PURE__ */ jsxs13(
4302
+ const media = section.slots.media;
4303
+ const image = media?.type === "image" ? media : void 0;
4304
+ const mediaRight = flip ? section.variant !== "media-right" : section.variant === "media-right";
4305
+ return /* @__PURE__ */ jsxs14("div", { className: `squisq-page-feature${mediaRight ? " squisq-page-feature--media-right" : ""}`, children: [
4306
+ /* @__PURE__ */ jsxs14("div", { className: "squisq-page-feature-media squisq-page-media-frame", children: [
4307
+ image && /* @__PURE__ */ jsx23(PageImg, { media: image }),
4308
+ /* @__PURE__ */ jsx23(MediaCredit, { media: image })
4309
+ ] }),
4310
+ /* @__PURE__ */ jsxs14("div", { className: "squisq-page-feature-text", children: [
4311
+ /* @__PURE__ */ jsx23(Eyebrow, { section }),
4312
+ section.slots.title && /* @__PURE__ */ jsx23("h3", { className: "squisq-page-section-title squisq-page-feature-title", children: section.slots.title }),
4313
+ section.slots.body?.text && /* @__PURE__ */ jsx23("p", { className: "squisq-page-feature-body", children: section.slots.body.text })
4314
+ ] })
4315
+ ] });
4316
+ }
4317
+ function MediaFigureSection({ section }) {
4318
+ const { basePath } = usePageView();
4319
+ const media = section.slots.media;
4320
+ if (!media || media.type === "canvas") return null;
4321
+ return /* @__PURE__ */ jsxs14("figure", { className: "squisq-page-figure", children: [
4322
+ /* @__PURE__ */ jsx23("div", { className: "squisq-page-media-frame", children: media.type === "image" ? /* @__PURE__ */ jsx23(PageImg, { media }) : /* @__PURE__ */ jsx23(InlineVideoPlayer, { src: media.src, poster: media.posterSrc, basePath }) }),
4323
+ (section.slots.caption || media.credit) && /* @__PURE__ */ jsxs14("figcaption", { children: [
4324
+ section.slots.caption,
4325
+ media.credit && /* @__PURE__ */ jsxs14(Fragment3, { children: [
4326
+ section.slots.caption ? " " : "",
4327
+ /* @__PURE__ */ jsx23(MediaCredit, { media })
4328
+ ] })
4329
+ ] })
4330
+ ] });
4331
+ }
4332
+ function GallerySection({ section }) {
4333
+ const items = section.slots.items ?? [];
4334
+ if (items.length === 0) {
4335
+ return /* @__PURE__ */ jsxs14("div", { className: "squisq-page-gallery-empty", children: [
4336
+ section.slots.title && /* @__PURE__ */ jsx23("h2", { className: "squisq-page-section-title", children: section.slots.title }),
4337
+ section.slots.body?.text && /* @__PURE__ */ jsx23("p", { children: section.slots.body.text }),
4338
+ section.slots.caption && /* @__PURE__ */ jsx23("div", { className: "squisq-page-gallery-caption", children: section.slots.caption })
4339
+ ] });
4340
+ }
4341
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
4342
+ /* @__PURE__ */ jsx23("div", { className: "squisq-page-gallery", children: items.map(
4343
+ (item, i) => item.media?.type === "image" ? /* @__PURE__ */ jsx23("div", { className: "squisq-page-media-frame", children: /* @__PURE__ */ jsx23(PageImg, { media: item.media }) }, i) : null
4344
+ ) }),
4345
+ section.slots.caption && /* @__PURE__ */ jsx23("div", { className: "squisq-page-gallery-caption", children: section.slots.caption })
4346
+ ] });
4347
+ }
4348
+ function CalloutSection({ section }) {
4349
+ const media = section.slots.media;
4350
+ const image = media?.type === "image" ? media : void 0;
4351
+ const framing = section.hints?.framing;
4352
+ return /* @__PURE__ */ jsxs14(
3830
4353
  "div",
3831
4354
  {
3832
- className: "squisq-linear-section",
3833
- "data-block-id": block.id,
3834
- "data-block-index": blockIndex,
3835
- "data-template": isAnnotated ? visualTemplateName(block) : void 0,
4355
+ className: "squisq-page-callout",
4356
+ "data-hint-framing": typeof framing === "string" ? framing : void 0,
3836
4357
  children: [
3837
- block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx20(MarkdownRenderer, { nodes: [block.sourceHeading] }),
3838
- isAnnotated && visualBlock && /* @__PURE__ */ jsx20("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx20(
3839
- "div",
3840
- {
3841
- className: "squisq-linear-card-svg",
3842
- style: {
3843
- width: "100%",
3844
- aspectRatio: `${viewport.width} / ${viewport.height}`,
3845
- overflow: "hidden",
3846
- marginBottom: "1em"
3847
- },
3848
- children: /* @__PURE__ */ jsx20(
3849
- BlockRenderer,
3850
- {
3851
- block: visualBlock,
3852
- blockTime: 0,
3853
- basePath,
3854
- viewport,
3855
- animationsEnabled
3856
- }
3857
- )
3858
- }
3859
- ) }),
3860
- !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx20(MarkdownRenderer, { nodes: block.contents }),
3861
- block.children && block.children.length > 0 && /* @__PURE__ */ jsx20("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx20(
3862
- BlockSection,
3863
- {
3864
- block: child,
3865
- basePath,
3866
- viewport,
3867
- renderContext,
3868
- blockIndex: blockIndices.get(child) ?? blockIndex + i + 1,
3869
- blockIndices,
3870
- animationsEnabled
3871
- },
3872
- child.id
3873
- )) })
4358
+ image && /* @__PURE__ */ jsx23("div", { className: "squisq-page-media-frame", children: /* @__PURE__ */ jsx23(PageImg, { media: image }) }),
4359
+ /* @__PURE__ */ jsxs14("div", { children: [
4360
+ /* @__PURE__ */ jsx23(Eyebrow, { section }),
4361
+ section.slots.title && /* @__PURE__ */ jsx23("h3", { className: "squisq-page-callout-title", children: section.slots.title }),
4362
+ section.slots.body?.text && /* @__PURE__ */ jsx23("p", { className: "squisq-page-callout-body", children: section.slots.body.text }),
4363
+ section.slots.meta && /* @__PURE__ */ jsx23("div", { className: "squisq-page-callout-meta", children: section.slots.meta })
4364
+ ] })
3874
4365
  ]
3875
4366
  }
3876
4367
  );
3877
4368
  }
4369
+ function CardGridSection({ section }) {
4370
+ const { theme } = usePageView();
4371
+ const items = section.slots.items ?? [];
4372
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
4373
+ section.slots.title && /* @__PURE__ */ jsx23("h2", { className: "squisq-page-section-title squisq-page-cards-title", children: section.slots.title }),
4374
+ /* @__PURE__ */ jsx23("div", { className: "squisq-page-cards", children: items.map((item, i) => {
4375
+ const scheme = item.accentScheme ? theme.colorSchemes?.[item.accentScheme] : void 0;
4376
+ return /* @__PURE__ */ jsxs14(
4377
+ "div",
4378
+ {
4379
+ className: "squisq-page-card",
4380
+ style: scheme ? { "--squisq-page-card-accent": scheme.accent } : void 0,
4381
+ children: [
4382
+ item.title && /* @__PURE__ */ jsx23("h3", { className: "squisq-page-card-title", children: item.title }),
4383
+ item.body && /* @__PURE__ */ jsx23("p", { className: "squisq-page-card-body", children: item.body })
4384
+ ]
4385
+ },
4386
+ i
4387
+ );
4388
+ }) })
4389
+ ] });
4390
+ }
4391
+ function ItemListSection({ section }) {
4392
+ const items = section.slots.items ?? [];
4393
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
4394
+ section.slots.title && /* @__PURE__ */ jsx23("h2", { className: "squisq-page-section-title squisq-page-items-title", children: section.slots.title }),
4395
+ /* @__PURE__ */ jsx23("ol", { className: "squisq-page-items", children: items.map((item, i) => /* @__PURE__ */ jsx23("li", { children: item.body }, i)) })
4396
+ ] });
4397
+ }
4398
+ function TimelineRailSection({ section }) {
4399
+ const milestone = section.slots.milestone;
4400
+ if (!milestone) return null;
4401
+ return /* @__PURE__ */ jsxs14("div", { className: "squisq-page-milestone", "data-mood": milestone.mood, children: [
4402
+ /* @__PURE__ */ jsx23("div", { className: "squisq-page-milestone-date", children: milestone.date }),
4403
+ /* @__PURE__ */ jsx23("div", { className: "squisq-page-milestone-rail", "aria-hidden": "true" }),
4404
+ /* @__PURE__ */ jsxs14("div", { className: "squisq-page-milestone-body", children: [
4405
+ milestone.description,
4406
+ milestone.footer && /* @__PURE__ */ jsx23("div", { className: "squisq-page-milestone-footer", children: milestone.footer })
4407
+ ] })
4408
+ ] });
4409
+ }
4410
+ function TableSection({ section }) {
4411
+ const table = section.slots.table;
4412
+ if (!table) return null;
4413
+ const alignFor = (i) => table.align?.[i] ?? void 0;
4414
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
4415
+ section.slots.title && /* @__PURE__ */ jsx23("h2", { className: "squisq-page-section-title squisq-page-table-title", children: section.slots.title }),
4416
+ /* @__PURE__ */ jsx23("div", { className: "squisq-page-table-scroll", children: /* @__PURE__ */ jsxs14("table", { className: "squisq-page-table", children: [
4417
+ /* @__PURE__ */ jsx23("thead", { children: /* @__PURE__ */ jsx23("tr", { children: table.headers.map((header, i) => /* @__PURE__ */ jsx23("th", { style: { textAlign: alignFor(i) }, children: header }, i)) }) }),
4418
+ /* @__PURE__ */ jsx23("tbody", { children: table.rows.map((row, r) => /* @__PURE__ */ jsx23("tr", { children: row.map((cell, c) => /* @__PURE__ */ jsx23("td", { style: { textAlign: alignFor(c) }, children: cell }, c)) }, r)) })
4419
+ ] }) })
4420
+ ] });
4421
+ }
4422
+ function ProseSection({ section, block }) {
4423
+ const heading = section.slots.title !== void 0 && block?.sourceHeading ? /* @__PURE__ */ jsx23(MarkdownRenderer, { nodes: [block.sourceHeading] }) : null;
4424
+ const bodyNodes = section.slots.body?.markdown ?? block?.contents;
4425
+ return /* @__PURE__ */ jsxs14("div", { className: "squisq-page-prose", children: [
4426
+ heading,
4427
+ bodyNodes && bodyNodes.length > 0 && /* @__PURE__ */ jsx23(MarkdownRenderer, { nodes: bodyNodes })
4428
+ ] });
4429
+ }
4430
+ function FooterSection({ section }) {
4431
+ return /* @__PURE__ */ jsxs14("div", { children: [
4432
+ /* @__PURE__ */ jsx23(Eyebrow, { section }),
4433
+ section.slots.title && /* @__PURE__ */ jsx23("h2", { className: "squisq-page-section-title", children: section.slots.title }),
4434
+ section.slots.body?.text && /* @__PURE__ */ jsx23("p", { children: section.slots.body.text })
4435
+ ] });
4436
+ }
4437
+
4438
+ // src/page/PageSectionView.tsx
4439
+ import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
4440
+ function heroHasBackdrop(section, heroStyle) {
4441
+ return section.kind === "hero" && section.background === "media" && section.slots.media?.type === "image" && heroStyle !== "split";
4442
+ }
4443
+ function accentVars(section) {
4444
+ const scheme = section.accent.scheme;
4445
+ if (!scheme) return void 0;
4446
+ return {
4447
+ "--squisq-page-accent": scheme.accent,
4448
+ "--squisq-page-accent-bg": scheme.bg,
4449
+ "--squisq-page-accent-text": scheme.text
4450
+ };
4451
+ }
4452
+ function sectionBody(entry, featureFlip) {
4453
+ const { section, block } = entry;
4454
+ switch (section.kind) {
4455
+ case "hero":
4456
+ return /* @__PURE__ */ jsx24(HeroSection, { section, block });
4457
+ case "banner":
4458
+ return /* @__PURE__ */ jsx24(BannerSection, { section, block });
4459
+ case "stat-band":
4460
+ return /* @__PURE__ */ jsx24(StatBandSection, { section, block });
4461
+ case "quote-band":
4462
+ return /* @__PURE__ */ jsx24(QuoteBandSection, { section, block });
4463
+ case "feature-split":
4464
+ return /* @__PURE__ */ jsx24(FeatureSplitSection, { section, block, flip: featureFlip });
4465
+ case "media-figure":
4466
+ return /* @__PURE__ */ jsx24(MediaFigureSection, { section, block });
4467
+ case "gallery":
4468
+ return /* @__PURE__ */ jsx24(GallerySection, { section, block });
4469
+ case "callout":
4470
+ return /* @__PURE__ */ jsx24(CalloutSection, { section, block });
4471
+ case "card-grid":
4472
+ return /* @__PURE__ */ jsx24(CardGridSection, { section, block });
4473
+ case "item-list":
4474
+ return /* @__PURE__ */ jsx24(ItemListSection, { section, block });
4475
+ case "timeline-rail":
4476
+ return /* @__PURE__ */ jsx24(TimelineRailSection, { section, block });
4477
+ case "table-section":
4478
+ return /* @__PURE__ */ jsx24(TableSection, { section, block });
4479
+ case "canvas-embed":
4480
+ return /* @__PURE__ */ jsx24(CanvasSection, { section });
4481
+ case "footer":
4482
+ return /* @__PURE__ */ jsx24(FooterSection, { section, block });
4483
+ case "prose":
4484
+ default:
4485
+ return /* @__PURE__ */ jsx24(ProseSection, { section, block });
4486
+ }
4487
+ }
4488
+ function PageSectionView({ entry, featureFlip, isLeadProse }) {
4489
+ const { pageStyle } = usePageView();
4490
+ const { section } = entry;
4491
+ const richContent = section.slots.richContent?.markdown;
4492
+ const classes = [
4493
+ "squisq-linear-section",
4494
+ "squisq-page-section",
4495
+ `squisq-page-section--${section.kind}`,
4496
+ `squisq-page-section--bg-${section.background}`,
4497
+ `squisq-page-section--em-${section.emphasis}`
4498
+ ];
4499
+ if (section.variant) classes.push(`squisq-page-section--v-${section.variant}`);
4500
+ if (heroHasBackdrop(section, pageStyle.tokens.heroStyle)) {
4501
+ classes.push("squisq-page-section--hero-media");
4502
+ }
4503
+ const hintAttrs = {};
4504
+ if (section.hints?.vignette) hintAttrs["data-hint-vignette"] = "";
4505
+ if (section.hints?.dropCap && isLeadProse) hintAttrs["data-hint-drop-cap"] = "";
4506
+ return /* @__PURE__ */ jsx24(
4507
+ "section",
4508
+ {
4509
+ className: classes.join(" "),
4510
+ "data-section-kind": section.kind,
4511
+ "data-block-id": section.blockId,
4512
+ "data-block-index": section.index,
4513
+ "data-template": section.template,
4514
+ style: accentVars(section),
4515
+ ...hintAttrs,
4516
+ children: /* @__PURE__ */ jsxs15("div", { className: "squisq-page-section-inner", children: [
4517
+ sectionBody(entry, featureFlip),
4518
+ richContent && richContent.length > 0 && /* @__PURE__ */ jsx24("div", { className: "squisq-page-rich-content", children: /* @__PURE__ */ jsx24(MarkdownRenderer, { nodes: richContent }) })
4519
+ ] })
4520
+ }
4521
+ );
4522
+ }
4523
+
4524
+ // src/LinearDocView.tsx
4525
+ import { jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
3878
4526
  function LinearDocView({
3879
4527
  doc,
3880
4528
  markdown,
@@ -3886,48 +4534,82 @@ function LinearDocView({
3886
4534
  animationsEnabled = true,
3887
4535
  thinMargins = false,
3888
4536
  imageDisplayMode = "inline",
3889
- globalKeyboardShortcuts = false
4537
+ globalKeyboardShortcuts = false,
4538
+ showCover = true,
4539
+ transformPage
3890
4540
  }) {
3891
4541
  const scrollRef = useRef8(null);
3892
- const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
3893
- const markdownDoc = useMemo8(
4542
+ const activeViewport = viewport ?? VIEWPORT_PRESETS4.landscape;
4543
+ const markdownDoc = useMemo9(
3894
4544
  () => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
3895
4545
  [doc, markdown]
3896
4546
  );
3897
4547
  const resolvedDoc = doc ?? markdownDoc;
3898
- const totalBlocks = useMemo8(
3899
- () => resolvedDoc ? countAll(resolvedDoc.blocks) : 0,
3900
- [resolvedDoc]
3901
- );
3902
- const blockIndices = useMemo8(() => {
3903
- const indices = /* @__PURE__ */ new Map();
3904
- let index = 0;
3905
- const visit = (blocks) => {
3906
- for (const block of blocks) {
3907
- indices.set(block, index++);
3908
- if (block.children) visit(block.children);
3909
- }
3910
- };
3911
- if (resolvedDoc) visit(resolvedDoc.blocks);
3912
- return indices;
3913
- }, [resolvedDoc]);
3914
4548
  const autoSurface = useAutoSurface(surface === "auto");
3915
4549
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
3916
- const renderContext = useMemo8(() => {
3917
- const baseTheme = theme ?? DEFAULT_THEME2;
3918
- const effectiveTheme = resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme;
3919
- return {
3920
- theme: effectiveTheme,
4550
+ const activeTheme = useMemo9(() => {
4551
+ const baseTheme = theme ?? DEFAULT_THEME3;
4552
+ return resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme;
4553
+ }, [theme, resolvedSurface]);
4554
+ const pageStyle = useMemo9(
4555
+ () => resolvePageStyle2(activeTheme, transformPage),
4556
+ [activeTheme, transformPage]
4557
+ );
4558
+ const sections = useMemo9(() => {
4559
+ if (!resolvedDoc) return [];
4560
+ return materializePageSections(resolvedDoc, {
4561
+ theme: activeTheme,
3921
4562
  viewport: activeViewport,
3922
- totalBlocks,
3923
- // Theme atmosphere (vignette/grain/gradient persistent layers) shows
3924
- // on the inline template cards so they match the player's look.
3925
- persistentLayers: effectiveTheme.persistentLayers,
4563
+ customTemplates: resolvedDoc.customTemplates,
4564
+ cover: showCover !== false ? resolvedDoc.startBlock : false,
4565
+ transformPage
4566
+ });
4567
+ }, [resolvedDoc, activeTheme, activeViewport, showCover, transformPage]);
4568
+ const renderContext = useMemo9(
4569
+ () => ({
4570
+ theme: activeTheme,
4571
+ viewport: activeViewport,
4572
+ totalBlocks: sections.length,
4573
+ persistentLayers: activeTheme.persistentLayers,
3926
4574
  customTemplates: resolvedDoc?.customTemplates
3927
- };
3928
- }, [activeViewport, resolvedDoc?.customTemplates, totalBlocks, theme, resolvedSurface]);
3929
- const activeTheme = renderContext.theme;
3930
- useEffect9(() => {
4575
+ }),
4576
+ [activeTheme, activeViewport, sections.length, resolvedDoc?.customTemplates]
4577
+ );
4578
+ const pageContext = useMemo9(
4579
+ () => ({
4580
+ theme: activeTheme,
4581
+ pageStyle,
4582
+ basePath,
4583
+ viewport: activeViewport,
4584
+ renderContext,
4585
+ animationsEnabled,
4586
+ imageDisplayMode
4587
+ }),
4588
+ [
4589
+ activeTheme,
4590
+ pageStyle,
4591
+ basePath,
4592
+ activeViewport,
4593
+ renderContext,
4594
+ animationsEnabled,
4595
+ imageDisplayMode
4596
+ ]
4597
+ );
4598
+ const { featureOrdinals, leadProseIndex } = useMemo9(() => {
4599
+ const ordinals = /* @__PURE__ */ new Map();
4600
+ let featureCount = 0;
4601
+ let firstProse = -1;
4602
+ for (const entry of sections) {
4603
+ if (entry.section.kind === "feature-split") {
4604
+ ordinals.set(entry.section.index, featureCount++);
4605
+ }
4606
+ if (firstProse < 0 && entry.section.kind === "prose") {
4607
+ firstProse = entry.section.index;
4608
+ }
4609
+ }
4610
+ return { featureOrdinals: ordinals, leadProseIndex: firstProse };
4611
+ }, [sections]);
4612
+ useEffect10(() => {
3931
4613
  if (!globalKeyboardShortcuts) return;
3932
4614
  const handleKeyDown = (event) => {
3933
4615
  if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.key !== "ArrowDown" && event.key !== "ArrowUp") {
@@ -3952,7 +4634,7 @@ function LinearDocView({
3952
4634
  return () => document.removeEventListener("keydown", handleKeyDown);
3953
4635
  }, [globalKeyboardShortcuts]);
3954
4636
  if (!resolvedDoc) {
3955
- return /* @__PURE__ */ jsx20("div", { ref: scrollRef, className: `squisq-linear squisq-linear--empty ${className || ""}` });
4637
+ return /* @__PURE__ */ jsx25("div", { ref: scrollRef, className: `squisq-linear squisq-linear--empty ${className || ""}` });
3956
4638
  }
3957
4639
  const bgColor = activeTheme.colors.background;
3958
4640
  const textColor = activeTheme.colors.text;
@@ -3960,12 +4642,22 @@ function LinearDocView({
3960
4642
  const primaryColor = activeTheme.colors.primary;
3961
4643
  const bodyFont = resolveFontFamily2(activeTheme.typography.bodyFont, "system-ui, sans-serif");
3962
4644
  const titleFont = resolveFontFamily2(activeTheme.typography.titleFont, "Georgia, serif");
3963
- const lineHt = activeTheme.typography.lineHeight ?? 1.7;
3964
- return /* @__PURE__ */ jsx20(
4645
+ const lineHt = activeTheme.typography.lineHeight ?? 1.65;
4646
+ const contentClasses = [
4647
+ "squisq-linear-content",
4648
+ "squisq-md",
4649
+ "squisq-page",
4650
+ thinMargins ? "squisq-linear-content--thin squisq-page--thin" : "",
4651
+ imageDisplayMode === "thumbnail" ? "squisq-linear-content--thumbnail-images squisq-page--thumbnail-images" : ""
4652
+ ].filter(Boolean).join(" ");
4653
+ return /* @__PURE__ */ jsx25(
3965
4654
  "div",
3966
4655
  {
3967
4656
  ref: scrollRef,
3968
4657
  className: `squisq-linear ${className || ""}`,
4658
+ role: "region",
4659
+ "aria-label": "Document page",
4660
+ tabIndex: thinMargins ? void 0 : 0,
3969
4661
  style: {
3970
4662
  width: "100%",
3971
4663
  // Thin-margins mode is the "embedded in someone else's container"
@@ -3976,25 +4668,26 @@ function LinearDocView({
3976
4668
  height: thinMargins ? "auto" : "100%",
3977
4669
  overflowY: thinMargins ? "visible" : "auto",
3978
4670
  overflowX: "hidden",
3979
- background: bgColor
4671
+ background: bgColor,
4672
+ // Container queries drive the responsive page layout — the page
4673
+ // often lives inside editor panes / chat bubbles where viewport
4674
+ // media queries lie about the available width.
4675
+ containerType: "inline-size",
4676
+ containerName: "squisq-page"
3980
4677
  },
3981
- children: /* @__PURE__ */ jsxs13(
4678
+ children: /* @__PURE__ */ jsxs16(
3982
4679
  "div",
3983
4680
  {
3984
- className: `squisq-linear-content squisq-md${thinMargins ? " squisq-linear-content--thin" : ""}${imageDisplayMode === "thumbnail" ? " squisq-linear-content--thumbnail-images" : ""}`,
4681
+ className: contentClasses,
4682
+ ...pageStyleDataAttributes(pageStyle),
3985
4683
  style: {
3986
- // Thin-margins mode drops the 720px reading column + generous
3987
- // page padding (right for standalone docs) in favor of a tight
3988
- // layout that hugs its container (right for chat bubbles and
3989
- // sidebar previews).
3990
- maxWidth: thinMargins ? "none" : "720px",
3991
- margin: thinMargins ? "0" : "0 auto",
3992
- padding: thinMargins ? "0" : "24px 16px",
3993
4684
  lineHeight: lineHt,
3994
4685
  fontSize: "16px",
3995
4686
  fontFamily: bodyFont,
3996
4687
  color: textColor,
3997
- // CSS custom properties for MarkdownRenderer / nested elements
4688
+ ...buildPageCssVars(activeTheme, pageStyle),
4689
+ // Legacy custom properties kept for one release so host CSS
4690
+ // and MarkdownRenderer content keyed on them keep working.
3998
4691
  "--squisq-linear-title-font": titleFont,
3999
4692
  "--squisq-linear-body-font": bodyFont,
4000
4693
  "--squisq-linear-text": textColor,
@@ -4003,134 +4696,20 @@ function LinearDocView({
4003
4696
  "--squisq-linear-bg": bgColor
4004
4697
  },
4005
4698
  children: [
4006
- /* @__PURE__ */ jsx20("style", { children: `
4007
- .squisq-linear-content h1,
4008
- .squisq-linear-content h2,
4009
- .squisq-linear-content h3,
4010
- .squisq-linear-content h4,
4011
- .squisq-linear-content h5,
4012
- .squisq-linear-content h6 {
4013
- font-family: var(--squisq-linear-title-font);
4014
- color: var(--squisq-linear-text);
4015
- margin-top: 1.2em;
4016
- margin-bottom: 0.4em;
4017
- }
4018
- .squisq-linear-content h1 { font-size: 2em; }
4019
- .squisq-linear-content h2 { font-size: 1.5em; }
4020
- .squisq-linear-content h3 { font-size: 1.25em; }
4021
- .squisq-linear-content p {
4022
- margin-bottom: 0.75em;
4023
- }
4024
- .squisq-linear-content p + p {
4025
- margin-top: 1.25em;
4026
- }
4027
- .squisq-linear-content ul,
4028
- .squisq-linear-content ol {
4029
- padding-left: 2em;
4030
- margin-bottom: 0.75em;
4031
- }
4032
- .squisq-linear-content li {
4033
- margin-bottom: 0.3em;
4034
- }
4035
- .squisq-linear-content a {
4036
- /* Blend the theme's primary toward the body text color so
4037
- links stay theme-flavored but always have enough contrast
4038
- against the background. Some themes (e.g. Gezellig) pick a
4039
- mid-tone primary that's nearly invisible on a dark page
4040
- without this lift. */
4041
- color: color-mix(in srgb, var(--squisq-linear-primary) 65%, var(--squisq-linear-text));
4042
- text-decoration: underline;
4043
- text-decoration-thickness: 1px;
4044
- text-underline-offset: 2px;
4045
- }
4046
- .squisq-linear-content a:hover {
4047
- color: var(--squisq-linear-primary);
4048
- }
4049
- .squisq-linear-content code {
4050
- color: var(--squisq-linear-primary);
4051
- font-size: 0.9em;
4052
- padding: 0.15em 0.3em;
4053
- border-radius: 3px;
4054
- background: rgba(128, 128, 128, 0.15);
4055
- }
4056
- .squisq-linear-content pre {
4057
- padding: 1em;
4058
- border-radius: 6px;
4059
- background: rgba(0, 0, 0, 0.2);
4060
- overflow-x: auto;
4061
- margin-bottom: 0.75em;
4062
- }
4063
- .squisq-linear-content pre code {
4064
- padding: 0;
4065
- background: none;
4066
- }
4067
- .squisq-linear-content blockquote {
4068
- border-left: 3px solid var(--squisq-linear-muted);
4069
- color: var(--squisq-linear-muted);
4070
- padding-left: 1em;
4071
- margin-left: 0;
4072
- margin-bottom: 0.75em;
4073
- }
4074
- .squisq-linear-content hr {
4075
- border: none;
4076
- border-top: 1px solid var(--squisq-linear-muted);
4077
- margin: 1.5em 0;
4078
- }
4079
- .squisq-linear-content img {
4080
- max-width: 100%;
4081
- height: auto;
4082
- border-radius: 6px;
4083
- margin: 0.5em 0;
4084
- }
4085
- .squisq-linear-content--thumbnail-images img {
4086
- max-width: 100px;
4087
- max-height: 100px;
4088
- width: auto;
4089
- height: auto;
4090
- object-fit: contain;
4091
- display: block;
4092
- }
4093
- .squisq-linear-content strong {
4094
- font-weight: 700;
4095
- }
4096
- .squisq-linear-content em {
4097
- font-style: italic;
4098
- }
4099
- .squisq-linear-content table {
4100
- width: 100%;
4101
- border-collapse: collapse;
4102
- margin: 1em 0;
4103
- font-size: 0.95em;
4104
- }
4105
- .squisq-linear-content thead th {
4106
- background: var(--squisq-linear-primary);
4107
- color: var(--squisq-linear-bg);
4108
- font-family: var(--squisq-linear-title-font);
4109
- font-weight: 600;
4110
- padding: 10px 14px;
4111
- text-align: left;
4112
- }
4113
- .squisq-linear-content tbody td {
4114
- padding: 8px 14px;
4115
- border-bottom: 1px solid color-mix(in srgb, var(--squisq-linear-muted) 30%, transparent);
4116
- }
4117
- .squisq-linear-content tbody tr:hover {
4118
- background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
4119
- }
4120
- ` }),
4121
- resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx20(
4122
- BlockSection,
4123
- {
4124
- block,
4125
- basePath,
4126
- viewport: activeViewport,
4127
- renderContext,
4128
- blockIndex: blockIndices.get(block) ?? i,
4129
- blockIndices,
4130
- animationsEnabled
4131
- },
4132
- block.id
4133
- ))
4699
+ /* @__PURE__ */ jsx25("style", { "data-squisq-page": "", children: PAGE_BASE_CSS }),
4700
+ /* @__PURE__ */ jsx25(PageViewContext.Provider, { value: pageContext, children: sections.map((entry) => {
4701
+ const ordinal = featureOrdinals.get(entry.section.index) ?? 0;
4702
+ const flip = !!entry.section.hints?.alternate && ordinal % 2 === 1;
4703
+ return /* @__PURE__ */ jsx25(
4704
+ PageSectionView,
4705
+ {
4706
+ entry,
4707
+ featureFlip: flip,
4708
+ isLeadProse: entry.section.index === leadProseIndex
4709
+ },
4710
+ `${entry.section.blockId}-${entry.section.index}`
4711
+ );
4712
+ }) })
4134
4713
  ]
4135
4714
  }
4136
4715
  )
@@ -4138,8 +4717,8 @@ function LinearDocView({
4138
4717
  );
4139
4718
  }
4140
4719
 
4141
- // src/DocPlayer.tsx
4142
- import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
4720
+ // src/docPlayer/segmentTitles.ts
4721
+ import { isTemplateBlock as isTemplateBlock2 } from "@bendyline/squisq/schemas";
4143
4722
  var SMALL_WORDS = /* @__PURE__ */ new Set([
4144
4723
  "a",
4145
4724
  "an",
@@ -4160,31 +4739,30 @@ var SMALL_WORDS = /* @__PURE__ */ new Set([
4160
4739
  function buildSegmentTitleMap(doc) {
4161
4740
  const map = /* @__PURE__ */ new Map();
4162
4741
  for (const block of doc.blocks) {
4163
- if (isTemplateBlock3(block) && block.template === "sectionHeader" && "title" in block) {
4164
- const segIdx = block.audioSegment;
4165
- if (!map.has(segIdx)) {
4166
- map.set(segIdx, block.title);
4167
- }
4742
+ if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
4743
+ const segmentIndex = block.audioSegment;
4744
+ if (!map.has(segmentIndex)) map.set(segmentIndex, block.title);
4168
4745
  }
4169
4746
  }
4170
- for (let i = 0; i < doc.audio.segments.length; i++) {
4171
- if (!map.has(i)) {
4172
- const name = doc.audio.segments[i].name;
4173
- if (name === "intro" || name.includes("intro")) {
4174
- map.set(i, "Introduction");
4175
- } else if (name === "flight-context" || name.includes("flight-context")) {
4176
- map.set(i, "Flight Context");
4177
- } else {
4178
- const words = name.split("-");
4179
- const titled = words.map(
4180
- (w, idx) => idx === 0 || !SMALL_WORDS.has(w) ? w.charAt(0).toUpperCase() + w.slice(1) : w
4181
- ).join(" ");
4182
- map.set(i, titled);
4183
- }
4747
+ for (let index = 0; index < doc.audio.segments.length; index++) {
4748
+ if (map.has(index)) continue;
4749
+ const name = doc.audio.segments[index].name;
4750
+ if (name.includes("intro")) {
4751
+ map.set(index, "Introduction");
4752
+ } else if (name.includes("flight-context")) {
4753
+ map.set(index, "Flight Context");
4754
+ } else {
4755
+ const title = name.split("-").map(
4756
+ (word, wordIndex) => wordIndex === 0 || !SMALL_WORDS.has(word) ? word.charAt(0).toUpperCase() + word.slice(1) : word
4757
+ ).join(" ");
4758
+ map.set(index, title);
4184
4759
  }
4185
4760
  }
4186
4761
  return map;
4187
4762
  }
4763
+
4764
+ // src/DocPlayer.tsx
4765
+ import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
4188
4766
  function isDevEnvironment() {
4189
4767
  try {
4190
4768
  return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
@@ -4195,15 +4773,15 @@ function isDevEnvironment() {
4195
4773
  var warnedMissingStyles = false;
4196
4774
  function DocPlayer(props) {
4197
4775
  const { doc, markdown } = props;
4198
- const markdownDoc = useMemo9(
4776
+ const markdownDoc = useMemo10(
4199
4777
  () => !doc && markdown !== void 0 ? markdownToDoc2(parseMarkdown2(markdown)) : void 0,
4200
4778
  [doc, markdown]
4201
4779
  );
4202
4780
  const resolvedDoc = doc ?? markdownDoc;
4203
4781
  if (!resolvedDoc) {
4204
- return /* @__PURE__ */ jsx21("div", { className: "doc-player doc-player--empty" });
4782
+ return /* @__PURE__ */ jsx26("div", { className: "doc-player doc-player--empty" });
4205
4783
  }
4206
- return /* @__PURE__ */ jsx21(DocPlayerContent, { ...props, doc: resolvedDoc });
4784
+ return /* @__PURE__ */ jsx26(DocPlayerContent, { ...props, doc: resolvedDoc });
4207
4785
  }
4208
4786
  function DocPlayerContent({
4209
4787
  doc,
@@ -4214,6 +4792,7 @@ function DocPlayerContent({
4214
4792
  onEnded,
4215
4793
  onTimeUpdate,
4216
4794
  audioController: externalAudioController,
4795
+ audioMode = "media",
4217
4796
  showControls = true,
4218
4797
  showScrubber = false,
4219
4798
  muted = false,
@@ -4240,18 +4819,24 @@ function DocPlayerContent({
4240
4819
  const audioRef = useRef9(null);
4241
4820
  const containerRef = useRef9(null);
4242
4821
  const playerId = `squisq-player-${useId7().replace(/:/g, "")}`;
4243
- const [tapFeedback, setTapFeedback] = useState9(null);
4822
+ const [tapFeedback, setTapFeedback] = useState10(null);
4244
4823
  const tapFeedbackTimer = useRef9();
4245
4824
  const { viewport, orientation } = useViewportOrientation();
4246
- const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
4247
- const isDebugMode = useMemo9(() => {
4825
+ const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS5.landscape : viewport);
4826
+ const isDebugMode = useMemo10(() => {
4248
4827
  if (typeof window === "undefined") return false;
4249
4828
  const params = new URLSearchParams(window.location.search);
4250
4829
  return params.get("debug") === "true";
4251
4830
  }, []);
4252
- const internalAudio = useAudioSync(audioRef, doc.audio, basePath, !externalAudioController);
4831
+ const internalAudio = useAudioSync(
4832
+ audioRef,
4833
+ doc.audio,
4834
+ basePath,
4835
+ !externalAudioController,
4836
+ audioMode
4837
+ );
4253
4838
  const audio = externalAudioController || internalAudio;
4254
- useEffect10(() => {
4839
+ useEffect11(() => {
4255
4840
  if (warnedMissingStyles || !isDevEnvironment()) return;
4256
4841
  const el = containerRef.current;
4257
4842
  if (!el || typeof getComputedStyle !== "function") return;
@@ -4279,7 +4864,7 @@ function DocPlayerContent({
4279
4864
  skipToSegment: _skipToSegment,
4280
4865
  restart
4281
4866
  } = audio;
4282
- const mediaSchedule = useMemo9(() => resolveMediaSchedule(doc), [doc]);
4867
+ const mediaSchedule = useMemo10(() => resolveMediaSchedule(doc), [doc]);
4283
4868
  const currentTimeRef = useRef9(currentTime);
4284
4869
  currentTimeRef.current = currentTime;
4285
4870
  const totalDurationRef = useRef9(totalDuration);
@@ -4310,8 +4895,8 @@ function DocPlayerContent({
4310
4895
  );
4311
4896
  const autoSurface = useAutoSurface(surface === "auto");
4312
4897
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
4313
- const effectiveTheme = useMemo9(() => {
4314
- const base = theme ?? DEFAULT_THEME3;
4898
+ const effectiveTheme = useMemo10(() => {
4899
+ const base = theme ?? DEFAULT_THEME4;
4315
4900
  return resolvedSurface ? applySurface2(base, resolvedSurface) : base;
4316
4901
  }, [theme, resolvedSurface]);
4317
4902
  const {
@@ -4332,7 +4917,7 @@ function DocPlayerContent({
4332
4917
  theme: effectiveTheme,
4333
4918
  onSeek: seekTo
4334
4919
  });
4335
- const coverBlock = useMemo9(() => {
4920
+ const coverBlock = useMemo10(() => {
4336
4921
  const startBlockConfig = doc.startBlock;
4337
4922
  if (!showCoverSlide) return null;
4338
4923
  if (!startBlockConfig) return null;
@@ -4349,13 +4934,13 @@ function DocPlayerContent({
4349
4934
  };
4350
4935
  }, [doc.startBlock, activeViewport, effectiveTheme, showCoverSlide]);
4351
4936
  const hasManagedCover = !!coverBlock;
4352
- const [slideshowCoverVisible, setSlideshowCoverVisible] = useState9(false);
4353
- const [isSlideshowPickerOpen, setIsSlideshowPickerOpen] = useState9(false);
4937
+ const [slideshowCoverVisible, setSlideshowCoverVisible] = useState10(false);
4938
+ const [isSlideshowPickerOpen, setIsSlideshowPickerOpen] = useState10(false);
4354
4939
  const slideshowCoverInitKeyRef = useRef9("");
4355
- useEffect10(() => {
4940
+ useEffect11(() => {
4356
4941
  slideshowCoverInitKeyRef.current = "";
4357
4942
  }, [doc]);
4358
- useEffect10(() => {
4943
+ useEffect11(() => {
4359
4944
  const initKey = `${isSlideshowMode}:${hasManagedCover}:${renderMode}`;
4360
4945
  if (slideshowCoverInitKeyRef.current === initKey) return;
4361
4946
  slideshowCoverInitKeyRef.current = initKey;
@@ -4366,12 +4951,12 @@ function DocPlayerContent({
4366
4951
  setSlideshowCoverVisible(false);
4367
4952
  }
4368
4953
  }, [isSlideshowMode, hasManagedCover, renderMode, pause]);
4369
- const [coverForced, setCoverForced] = useState9(false);
4370
- const [coverGraceActive, setCoverGraceActive] = useState9(false);
4954
+ const [coverForced, setCoverForced] = useState10(false);
4955
+ const [coverGraceActive, setCoverGraceActive] = useState10(false);
4371
4956
  const coverGraceTimer = useRef9();
4372
4957
  const coverWasShowing = useRef9(false);
4373
4958
  const hasPlayedOnce = useRef9(false);
4374
- useEffect10(() => {
4959
+ useEffect11(() => {
4375
4960
  hasPlayedOnce.current = false;
4376
4961
  coverWasShowing.current = false;
4377
4962
  clearTimeout(coverGraceTimer.current);
@@ -4380,7 +4965,7 @@ function DocPlayerContent({
4380
4965
  }, [doc]);
4381
4966
  const atRest = !!(coverBlock && !isSlideshowMode && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
4382
4967
  if (atRest) coverWasShowing.current = true;
4383
- useEffect10(() => {
4968
+ useEffect11(() => {
4384
4969
  if (isPlaying && coverWasShowing.current && coverBlock && !renderMode && !isSlideshowMode) {
4385
4970
  coverWasShowing.current = false;
4386
4971
  hasPlayedOnce.current = true;
@@ -4388,7 +4973,8 @@ function DocPlayerContent({
4388
4973
  coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
4389
4974
  }
4390
4975
  }, [isPlaying, coverBlock, renderMode, isSlideshowMode]);
4391
- useEffect10(() => () => clearTimeout(coverGraceTimer.current), []);
4976
+ useEffect11(() => () => clearTimeout(coverGraceTimer.current), []);
4977
+ useEffect11(() => () => clearTimeout(tapFeedbackTimer.current), []);
4392
4978
  const showVideoCoverBlock = !isSlideshowMode && !isLinearMode && !!coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
4393
4979
  const effectiveSlideshowCoverVisible = coverVisible ?? slideshowCoverVisible;
4394
4980
  const showSlideshowCover = !!(isSlideshowMode && !isLinearMode && !renderMode && coverBlock && effectiveSlideshowCoverVisible);
@@ -4397,19 +4983,19 @@ function DocPlayerContent({
4397
4983
  const slideshowSlideIndex = slideshowHasCover ? effectiveSlideshowCoverVisible ? 0 : currentBlockIndex + 1 : currentBlockIndex;
4398
4984
  const slideshowTotalSlides = slideshowHasCover ? expandedBlocks.length + 1 : expandedBlocks.length;
4399
4985
  const hasAutoPlayed = useRef9(false);
4400
- useEffect10(() => {
4986
+ useEffect11(() => {
4401
4987
  hasAutoPlayed.current = false;
4402
4988
  }, [doc]);
4403
- useEffect10(() => {
4989
+ useEffect11(() => {
4404
4990
  if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
4405
4991
  hasAutoPlayed.current = true;
4406
4992
  play();
4407
4993
  }
4408
4994
  }, [isAudioReady, autoPlay, play]);
4409
- useEffect10(() => {
4995
+ useEffect11(() => {
4410
4996
  onTimeUpdate?.(currentTime);
4411
4997
  }, [currentTime, onTimeUpdate]);
4412
- useEffect10(() => {
4998
+ useEffect11(() => {
4413
4999
  if (isEnded) {
4414
5000
  onEnded?.();
4415
5001
  }
@@ -4435,7 +5021,7 @@ function DocPlayerContent({
4435
5021
  };
4436
5022
  }
4437
5023
  const stableRenderAPI = stableRenderAPIRef.current;
4438
- useEffect10(() => {
5024
+ useEffect11(() => {
4439
5025
  if (!renderMode && !isDebugMode) {
4440
5026
  liveRenderAPIRef.current = null;
4441
5027
  return;
@@ -4572,7 +5158,7 @@ function DocPlayerContent({
4572
5158
  if (liveRenderAPIRef.current === api) liveRenderAPIRef.current = null;
4573
5159
  };
4574
5160
  }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock, doc]);
4575
- useEffect10(() => {
5161
+ useEffect11(() => {
4576
5162
  if (!renderMode && !isDebugMode || !containerRef.current) {
4577
5163
  onRenderAPIReady?.(null);
4578
5164
  return;
@@ -4581,8 +5167,8 @@ function DocPlayerContent({
4581
5167
  return () => onRenderAPIReady?.(null);
4582
5168
  }, [renderMode, isDebugMode, onRenderAPIReady, stableRenderAPI]);
4583
5169
  const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
4584
- const [captionMode, setCaptionMode] = useState9(defaultMode);
4585
- useEffect10(() => {
5170
+ const [captionMode, setCaptionMode] = useState10(defaultMode);
5171
+ useEffect11(() => {
4586
5172
  setCaptionMode(defaultMode);
4587
5173
  }, [defaultMode]);
4588
5174
  const captionsEnabled = captionMode !== "off";
@@ -4602,8 +5188,8 @@ function DocPlayerContent({
4602
5188
  });
4603
5189
  }, [onCaptionsToggle]);
4604
5190
  const hasCaptions = doc.captions && doc.captions.phrases.length > 0;
4605
- const segmentTitleMap = useMemo9(() => buildSegmentTitleMap(doc), [doc]);
4606
- const playbackState = useMemo9(
5191
+ const segmentTitleMap = useMemo10(() => buildSegmentTitleMap(doc), [doc]);
5192
+ const playbackState = useMemo10(
4607
5193
  () => ({
4608
5194
  isPlaying,
4609
5195
  currentTime,
@@ -4646,7 +5232,7 @@ function DocPlayerContent({
4646
5232
  expandedBlocks.length
4647
5233
  ]
4648
5234
  );
4649
- const playbackActions = useMemo9(
5235
+ const playbackActions = useMemo10(
4650
5236
  () => ({
4651
5237
  toggle,
4652
5238
  restart,
@@ -4657,7 +5243,7 @@ function DocPlayerContent({
4657
5243
  }),
4658
5244
  [toggle, restart, seekTo, setCaptionsEnabled, cycleCaptionMode, onFullscreenToggle]
4659
5245
  );
4660
- const slideNavActions = useMemo9(
5246
+ const slideNavActions = useMemo10(
4661
5247
  () => ({
4662
5248
  nextSlide: () => {
4663
5249
  if (slideshowHasCover && slideshowCoverVisible) {
@@ -4746,10 +5332,10 @@ function DocPlayerContent({
4746
5332
  onNext: handleSwipeNext,
4747
5333
  onPrev: handleSwipePrev
4748
5334
  });
4749
- useEffect10(() => {
5335
+ useEffect11(() => {
4750
5336
  onPlaybackStateChange?.(playbackState);
4751
5337
  }, [playbackState, onPlaybackStateChange]);
4752
- useEffect10(() => {
5338
+ useEffect11(() => {
4753
5339
  onControlsReady?.({ play, pause, ...playbackActions });
4754
5340
  }, [play, pause, playbackActions, onControlsReady]);
4755
5341
  const getBlockTitle = useCallback7((block) => {
@@ -4776,7 +5362,7 @@ function DocPlayerContent({
4776
5362
  }
4777
5363
  return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
4778
5364
  }, []);
4779
- const slideshowPickerItems = useMemo9(() => {
5365
+ const slideshowPickerItems = useMemo10(() => {
4780
5366
  const blockItems = expandedBlocks.map((block, index) => ({
4781
5367
  id: block.id,
4782
5368
  label: String(index + 1),
@@ -4792,7 +5378,7 @@ function DocPlayerContent({
4792
5378
  ...blockItems
4793
5379
  ];
4794
5380
  }, [coverBlock, expandedBlocks, getBlockTitle, slideshowHasCover]);
4795
- const blockMarkers = useMemo9(() => {
5381
+ const blockMarkers = useMemo10(() => {
4796
5382
  if (!totalDuration || !expandedBlocks.length) return [];
4797
5383
  let prevSegment = -1;
4798
5384
  return expandedBlocks.map((block, index) => {
@@ -4807,7 +5393,7 @@ function DocPlayerContent({
4807
5393
  };
4808
5394
  });
4809
5395
  }, [expandedBlocks, totalDuration, getBlockTitle]);
4810
- useEffect10(() => {
5396
+ useEffect11(() => {
4811
5397
  if (blockMarkers.length > 0) {
4812
5398
  onBlockMarkers?.(blockMarkers);
4813
5399
  }
@@ -4830,6 +5416,12 @@ function DocPlayerContent({
4830
5416
  return;
4831
5417
  }
4832
5418
  if (isLinearMode) return;
5419
+ if (e.key === "f" || e.key === "F") {
5420
+ if (!onFullscreenToggle) return;
5421
+ e.preventDefault();
5422
+ onFullscreenToggle();
5423
+ return;
5424
+ }
4833
5425
  if (isSlideshowMode) {
4834
5426
  switch (e.key) {
4835
5427
  case "ArrowRight":
@@ -4872,13 +5464,13 @@ function DocPlayerContent({
4872
5464
  }
4873
5465
  }
4874
5466
  },
4875
- [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
5467
+ [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions, onFullscreenToggle]
4876
5468
  );
4877
5469
  const handleKeyDown = useCallback7(
4878
5470
  (e) => handleKeyboardShortcut(e, false),
4879
5471
  [handleKeyboardShortcut]
4880
5472
  );
4881
- useEffect10(() => {
5473
+ useEffect11(() => {
4882
5474
  if (!globalKeyboardShortcuts || renderMode || isLinearMode) return;
4883
5475
  const handleDocumentKeyDown = (event) => {
4884
5476
  handleKeyboardShortcut(event, true);
@@ -4887,7 +5479,7 @@ function DocPlayerContent({
4887
5479
  return () => document.removeEventListener("keydown", handleDocumentKeyDown);
4888
5480
  }, [globalKeyboardShortcuts, handleKeyboardShortcut, isLinearMode, renderMode]);
4889
5481
  if (isLinearMode) {
4890
- return /* @__PURE__ */ jsx21(
5482
+ return /* @__PURE__ */ jsx26(
4891
5483
  "div",
4892
5484
  {
4893
5485
  ref: containerRef,
@@ -4899,7 +5491,7 @@ function DocPlayerContent({
4899
5491
  height: "100%",
4900
5492
  overflow: "hidden"
4901
5493
  },
4902
- children: /* @__PURE__ */ jsx21(
5494
+ children: /* @__PURE__ */ jsx26(
4903
5495
  LinearDocView,
4904
5496
  {
4905
5497
  doc,
@@ -4913,7 +5505,7 @@ function DocPlayerContent({
4913
5505
  }
4914
5506
  );
4915
5507
  }
4916
- return /* @__PURE__ */ jsxs14(
5508
+ return /* @__PURE__ */ jsxs17(
4917
5509
  "div",
4918
5510
  {
4919
5511
  ref: containerRef,
@@ -4936,8 +5528,8 @@ function DocPlayerContent({
4936
5528
  touchAction: swipeEnabled ? "pan-y" : void 0
4937
5529
  },
4938
5530
  children: [
4939
- /* @__PURE__ */ jsx21("audio", { ref: audioRef, preload: "auto", muted }),
4940
- /* @__PURE__ */ jsx21(
5531
+ /* @__PURE__ */ jsx26("audio", { ref: audioRef, preload: "auto", muted }),
5532
+ /* @__PURE__ */ jsx26(
4941
5533
  MediaClipLayer,
4942
5534
  {
4943
5535
  schedule: mediaSchedule,
@@ -4948,8 +5540,8 @@ function DocPlayerContent({
4948
5540
  muted
4949
5541
  }
4950
5542
  ),
4951
- /* @__PURE__ */ jsxs14("div", { className: "doc-player__viewport", children: [
4952
- showCoverBlock && coverBlock && /* @__PURE__ */ jsx21("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx21(
5543
+ /* @__PURE__ */ jsxs17("div", { className: "doc-player__viewport", children: [
5544
+ showCoverBlock && coverBlock && /* @__PURE__ */ jsx26("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx26(
4953
5545
  BlockRenderer,
4954
5546
  {
4955
5547
  block: coverBlock,
@@ -4964,7 +5556,7 @@ function DocPlayerContent({
4964
5556
  // reconciles one block's layers onto another's (templates reuse layer
4965
5557
  // ids like `title`/`background`), which would otherwise reuse stale
4966
5558
  // DOM / skip entrance animations mid-transition.
4967
- /* @__PURE__ */ jsx21("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx21(
5559
+ /* @__PURE__ */ jsx26("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx26(
4968
5560
  BlockRenderer,
4969
5561
  {
4970
5562
  block: previousBlock,
@@ -4976,12 +5568,12 @@ function DocPlayerContent({
4976
5568
  animationsEnabled
4977
5569
  }
4978
5570
  ) }, previousBlock.id),
4979
- !showCoverBlock && currentBlock && /* @__PURE__ */ jsx21(
5571
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx26(
4980
5572
  "div",
4981
5573
  {
4982
5574
  className: `doc-player__block doc-player__block--active${swipe.phase !== "idle" ? ` doc-player__block--${swipe.phase}` : ""}`,
4983
5575
  style: swipe.phase !== "idle" ? { transform: `translateX(${swipe.offsetPx}px)` } : void 0,
4984
- children: /* @__PURE__ */ jsx21(
5576
+ children: /* @__PURE__ */ jsx26(
4985
5577
  BlockRenderer,
4986
5578
  {
4987
5579
  block: currentBlock,
@@ -4996,7 +5588,7 @@ function DocPlayerContent({
4996
5588
  },
4997
5589
  currentBlock.id
4998
5590
  ),
4999
- hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx21(
5591
+ hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx26(
5000
5592
  CaptionOverlay,
5001
5593
  {
5002
5594
  captions: doc.captions,
@@ -5008,7 +5600,7 @@ function DocPlayerContent({
5008
5600
  viewport: activeViewport
5009
5601
  }
5010
5602
  ),
5011
- isDebugMode && /* @__PURE__ */ jsxs14(
5603
+ isDebugMode && /* @__PURE__ */ jsxs17(
5012
5604
  "div",
5013
5605
  {
5014
5606
  className: "doc-player__debug",
@@ -5029,27 +5621,27 @@ function DocPlayerContent({
5029
5621
  textAlign: "left"
5030
5622
  },
5031
5623
  children: [
5032
- /* @__PURE__ */ jsx21("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
5033
- /* @__PURE__ */ jsxs14("div", { children: [
5034
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "template:" }),
5624
+ /* @__PURE__ */ jsx26("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
5625
+ /* @__PURE__ */ jsxs17("div", { children: [
5626
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "template:" }),
5035
5627
  " ",
5036
- /* @__PURE__ */ jsx21("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
5628
+ /* @__PURE__ */ jsx26("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
5037
5629
  ] }),
5038
- /* @__PURE__ */ jsxs14("div", { children: [
5039
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "block:" }),
5630
+ /* @__PURE__ */ jsxs17("div", { children: [
5631
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "block:" }),
5040
5632
  " ",
5041
5633
  currentBlockIndex + 1,
5042
5634
  "/",
5043
5635
  expandedBlocks.length,
5044
5636
  " ",
5045
- /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
5637
+ /* @__PURE__ */ jsxs17("span", { style: { color: "#666" }, children: [
5046
5638
  "(",
5047
5639
  currentBlock?.id || "none",
5048
5640
  ")"
5049
5641
  ] })
5050
5642
  ] }),
5051
- /* @__PURE__ */ jsxs14("div", { children: [
5052
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "time:" }),
5643
+ /* @__PURE__ */ jsxs17("div", { children: [
5644
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "time:" }),
5053
5645
  " ",
5054
5646
  currentTime.toFixed(2),
5055
5647
  "s /",
@@ -5057,7 +5649,7 @@ function DocPlayerContent({
5057
5649
  totalDuration.toFixed(1),
5058
5650
  "s",
5059
5651
  " ",
5060
- /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
5652
+ /* @__PURE__ */ jsxs17("span", { style: { color: "#666" }, children: [
5061
5653
  "(progress: ",
5062
5654
  (docProgress * 100).toFixed(1),
5063
5655
  "%, scriptDur: ",
@@ -5065,8 +5657,8 @@ function DocPlayerContent({
5065
5657
  ")"
5066
5658
  ] })
5067
5659
  ] }),
5068
- /* @__PURE__ */ jsxs14("div", { children: [
5069
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "blockTime:" }),
5660
+ /* @__PURE__ */ jsxs17("div", { children: [
5661
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "blockTime:" }),
5070
5662
  " ",
5071
5663
  blockTime.toFixed(2),
5072
5664
  "s /",
@@ -5074,58 +5666,58 @@ function DocPlayerContent({
5074
5666
  (currentBlock?.duration || 0).toFixed(1),
5075
5667
  "s"
5076
5668
  ] }),
5077
- /* @__PURE__ */ jsxs14("div", { children: [
5078
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "segment:" }),
5669
+ /* @__PURE__ */ jsxs17("div", { children: [
5670
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "segment:" }),
5079
5671
  " ",
5080
5672
  currentSegment,
5081
5673
  "/",
5082
5674
  doc.audio.segments.length - 1,
5083
5675
  " ",
5084
- /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
5676
+ /* @__PURE__ */ jsxs17("span", { style: { color: "#666" }, children: [
5085
5677
  "(",
5086
5678
  doc.audio.segments[currentSegment]?.name || "none",
5087
5679
  ")"
5088
5680
  ] })
5089
5681
  ] }),
5090
- /* @__PURE__ */ jsxs14("div", { children: [
5091
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "viewport:" }),
5682
+ /* @__PURE__ */ jsxs17("div", { children: [
5683
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "viewport:" }),
5092
5684
  " ",
5093
5685
  activeViewport.name || `${activeViewport.width}x${activeViewport.height}`,
5094
5686
  " ",
5095
- /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
5687
+ /* @__PURE__ */ jsxs17("span", { style: { color: "#666" }, children: [
5096
5688
  "(",
5097
5689
  orientation,
5098
5690
  ")"
5099
5691
  ] })
5100
5692
  ] }),
5101
- /* @__PURE__ */ jsxs14("div", { children: [
5102
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "playing:" }),
5693
+ /* @__PURE__ */ jsxs17("div", { children: [
5694
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "playing:" }),
5103
5695
  " ",
5104
- /* @__PURE__ */ jsx21("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
5105
- showCoverBlock && /* @__PURE__ */ jsx21("span", { style: { color: "#60a5fa" }, children: " (cover)" })
5696
+ /* @__PURE__ */ jsx26("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
5697
+ showCoverBlock && /* @__PURE__ */ jsx26("span", { style: { color: "#60a5fa" }, children: " (cover)" })
5106
5698
  ] }),
5107
5699
  hasCaptions && (() => {
5108
5700
  const debugPhrase = getCaptionAtTime2(doc.captions, currentTime);
5109
5701
  const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
5110
- return /* @__PURE__ */ jsxs14(Fragment3, { children: [
5111
- /* @__PURE__ */ jsxs14("div", { children: [
5112
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "captions:" }),
5702
+ return /* @__PURE__ */ jsxs17(Fragment4, { children: [
5703
+ /* @__PURE__ */ jsxs17("div", { children: [
5704
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "captions:" }),
5113
5705
  " ",
5114
5706
  doc.captions?.phrases.length || 0,
5115
5707
  " phrases",
5116
5708
  " ",
5117
- /* @__PURE__ */ jsxs14("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
5709
+ /* @__PURE__ */ jsxs17("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
5118
5710
  "(",
5119
5711
  captionsEnabled ? "on" : "off",
5120
5712
  ")"
5121
5713
  ] })
5122
5714
  ] }),
5123
- /* @__PURE__ */ jsxs14("div", { children: [
5124
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.enabled:" }),
5715
+ /* @__PURE__ */ jsxs17("div", { children: [
5716
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "cc.enabled:" }),
5125
5717
  " ",
5126
- /* @__PURE__ */ jsx21("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
5718
+ /* @__PURE__ */ jsx26("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
5127
5719
  " ",
5128
- /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
5720
+ /* @__PURE__ */ jsxs17("span", { style: { color: "#666" }, children: [
5129
5721
  "(playing=",
5130
5722
  String(isPlaying),
5131
5723
  " t>0=",
@@ -5133,15 +5725,15 @@ function DocPlayerContent({
5133
5725
  ")"
5134
5726
  ] })
5135
5727
  ] }),
5136
- /* @__PURE__ */ jsxs14("div", { children: [
5137
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.phrase:" }),
5728
+ /* @__PURE__ */ jsxs17("div", { children: [
5729
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "cc.phrase:" }),
5138
5730
  " ",
5139
- /* @__PURE__ */ jsx21("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
5731
+ /* @__PURE__ */ jsx26("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
5140
5732
  ] }),
5141
- debugPhrase && /* @__PURE__ */ jsxs14("div", { children: [
5142
- /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.range:" }),
5733
+ debugPhrase && /* @__PURE__ */ jsxs17("div", { children: [
5734
+ /* @__PURE__ */ jsx26("span", { style: { color: "#888" }, children: "cc.range:" }),
5143
5735
  " ",
5144
- /* @__PURE__ */ jsxs14("span", { style: { color: "#60a5fa" }, children: [
5736
+ /* @__PURE__ */ jsxs17("span", { style: { color: "#60a5fa" }, children: [
5145
5737
  debugPhrase.startTime.toFixed(2),
5146
5738
  "-",
5147
5739
  debugPhrase.endTime.toFixed(2)
@@ -5153,7 +5745,7 @@ function DocPlayerContent({
5153
5745
  }
5154
5746
  )
5155
5747
  ] }),
5156
- !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs14(
5748
+ !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs17(
5157
5749
  "div",
5158
5750
  {
5159
5751
  className: "doc-player__unavailable",
@@ -5174,12 +5766,12 @@ function DocPlayerContent({
5174
5766
  zIndex: 50
5175
5767
  },
5176
5768
  children: [
5177
- /* @__PURE__ */ jsx21("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
5178
- /* @__PURE__ */ jsx21("span", { children: unavailableMessage })
5769
+ /* @__PURE__ */ jsx26("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
5770
+ /* @__PURE__ */ jsx26("span", { children: unavailableMessage })
5179
5771
  ]
5180
5772
  }
5181
5773
  ),
5182
- !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx21(
5774
+ !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx26(
5183
5775
  DocControlsOverlay,
5184
5776
  {
5185
5777
  state: playbackState,
@@ -5189,7 +5781,7 @@ function DocPlayerContent({
5189
5781
  getBlockTitle
5190
5782
  }
5191
5783
  ),
5192
- !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx21(
5784
+ !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx26(
5193
5785
  "div",
5194
5786
  {
5195
5787
  className: "doc-player__scrubber",
@@ -5204,7 +5796,7 @@ function DocPlayerContent({
5204
5796
  alignItems: "center",
5205
5797
  zIndex: 100
5206
5798
  },
5207
- children: /* @__PURE__ */ jsx21(
5799
+ children: /* @__PURE__ */ jsx26(
5208
5800
  DocProgressBar,
5209
5801
  {
5210
5802
  state: playbackState,
@@ -5216,7 +5808,7 @@ function DocPlayerContent({
5216
5808
  )
5217
5809
  }
5218
5810
  ),
5219
- !renderMode && isSlideshowMode && showControls && /* @__PURE__ */ jsx21(
5811
+ !renderMode && isSlideshowMode && showControls && /* @__PURE__ */ jsx26(
5220
5812
  DocControlsSlideshow,
5221
5813
  {
5222
5814
  state: playbackState,
@@ -5226,14 +5818,14 @@ function DocPlayerContent({
5226
5818
  onPickerOpenChange: setIsSlideshowPickerOpen
5227
5819
  }
5228
5820
  ),
5229
- !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx21("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx21("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx21("path", { d: "M8 5v14l11-7z" }) }) }, tapFeedback)
5821
+ !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx26("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx26("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx26("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx26("path", { d: "M8 5v14l11-7z" }) }) }, tapFeedback)
5230
5822
  ]
5231
5823
  }
5232
5824
  );
5233
5825
  }
5234
5826
 
5235
5827
  // src/DocControlsBottom.tsx
5236
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
5828
+ import { jsx as jsx27, jsxs as jsxs18 } from "react/jsx-runtime";
5237
5829
  function DocControlsBottom({
5238
5830
  state,
5239
5831
  actions,
@@ -5241,32 +5833,32 @@ function DocControlsBottom({
5241
5833
  expandedBlocks,
5242
5834
  getBlockTitle
5243
5835
  }) {
5244
- return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-bottom", children: [
5245
- /* @__PURE__ */ jsx22(
5836
+ return /* @__PURE__ */ jsxs18("div", { className: "doc-controls-bottom", children: [
5837
+ /* @__PURE__ */ jsx27(
5246
5838
  "button",
5247
5839
  {
5248
5840
  className: "bottom-ctrl-btn",
5249
5841
  onClick: actions.restart,
5250
5842
  title: "Restart",
5251
5843
  "aria-label": "Restart from beginning",
5252
- children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx22("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
5844
+ children: /* @__PURE__ */ jsx27("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx27("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
5253
5845
  }
5254
5846
  ),
5255
- /* @__PURE__ */ jsx22(
5847
+ /* @__PURE__ */ jsx27(
5256
5848
  "button",
5257
5849
  {
5258
5850
  className: "bottom-ctrl-btn bottom-play-btn",
5259
5851
  onClick: actions.toggle,
5260
5852
  "aria-label": state.isPlaying ? "Pause" : "Play",
5261
- children: state.isPlaying ? /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M8 5v14l11-7z" }) })
5853
+ children: state.isPlaying ? /* @__PURE__ */ jsx27("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx27("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx27("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx27("path", { d: "M8 5v14l11-7z" }) })
5262
5854
  }
5263
5855
  ),
5264
- /* @__PURE__ */ jsxs15("span", { className: "bottom-time", children: [
5856
+ /* @__PURE__ */ jsxs18("span", { className: "bottom-time", children: [
5265
5857
  formatTime(state.currentTime),
5266
5858
  " / ",
5267
5859
  formatTime(state.totalDuration)
5268
5860
  ] }),
5269
- /* @__PURE__ */ jsx22(
5861
+ /* @__PURE__ */ jsx27(
5270
5862
  DocProgressBar,
5271
5863
  {
5272
5864
  state,
@@ -5276,82 +5868,92 @@ function DocControlsBottom({
5276
5868
  getBlockTitle
5277
5869
  }
5278
5870
  ),
5279
- /* @__PURE__ */ jsxs15("span", { className: "bottom-segment", children: [
5871
+ /* @__PURE__ */ jsxs18("span", { className: "bottom-segment", children: [
5280
5872
  state.currentBlockIndex + 1,
5281
5873
  "/",
5282
5874
  state.totalBlocks
5283
5875
  ] }),
5284
- state.hasCaptions && /* @__PURE__ */ jsx22(
5876
+ state.hasCaptions && /* @__PURE__ */ jsx27(
5285
5877
  "button",
5286
5878
  {
5287
5879
  className: `bottom-ctrl-btn ${state.captionMode !== "off" ? "bottom-ctrl-btn--active" : ""}`,
5288
5880
  onClick: () => actions.cycleCaptionMode(),
5289
5881
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
5290
5882
  "aria-label": "Cycle caption style",
5291
- children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx22("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
5883
+ children: /* @__PURE__ */ jsx27("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx27("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
5884
+ }
5885
+ ),
5886
+ actions.toggleFullscreen && /* @__PURE__ */ jsx27(
5887
+ "button",
5888
+ {
5889
+ className: `bottom-ctrl-btn ${state.isFullscreen ? "bottom-ctrl-btn--active" : ""}`,
5890
+ onClick: actions.toggleFullscreen,
5891
+ title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
5892
+ "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
5893
+ children: state.isFullscreen ? /* @__PURE__ */ jsx27("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx27("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx27("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx27("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
5292
5894
  }
5293
5895
  )
5294
5896
  ] });
5295
5897
  }
5296
5898
 
5297
5899
  // src/DocControlsSidebar.tsx
5298
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
5900
+ import { jsx as jsx28, jsxs as jsxs19 } from "react/jsx-runtime";
5299
5901
  function DocControlsSidebar({ state, actions }) {
5300
- return /* @__PURE__ */ jsxs16("div", { className: "doc-controls-sidebar", children: [
5301
- /* @__PURE__ */ jsx23(
5902
+ return /* @__PURE__ */ jsxs19("div", { className: "doc-controls-sidebar", children: [
5903
+ /* @__PURE__ */ jsx28(
5302
5904
  "button",
5303
5905
  {
5304
5906
  className: "sidebar-ctrl-btn",
5305
5907
  onClick: actions.restart,
5306
5908
  title: "Restart",
5307
5909
  "aria-label": "Restart from beginning",
5308
- children: /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
5910
+ children: /* @__PURE__ */ jsx28("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx28("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
5309
5911
  }
5310
5912
  ),
5311
- /* @__PURE__ */ jsx23(
5913
+ /* @__PURE__ */ jsx28(
5312
5914
  "button",
5313
5915
  {
5314
5916
  className: "sidebar-ctrl-btn sidebar-play-btn",
5315
5917
  onClick: actions.toggle,
5316
5918
  "aria-label": state.isPlaying ? "Pause" : "Play",
5317
- children: state.isPlaying ? /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx23("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx23("path", { d: "M8 5v14l11-7z" }) })
5919
+ children: state.isPlaying ? /* @__PURE__ */ jsx28("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx28("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx28("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx28("path", { d: "M8 5v14l11-7z" }) })
5318
5920
  }
5319
5921
  ),
5320
- /* @__PURE__ */ jsxs16("div", { className: "sidebar-time", children: [
5321
- /* @__PURE__ */ jsx23("div", { children: formatTime(state.currentTime) }),
5322
- /* @__PURE__ */ jsx23("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
5922
+ /* @__PURE__ */ jsxs19("div", { className: "sidebar-time", children: [
5923
+ /* @__PURE__ */ jsx28("div", { children: formatTime(state.currentTime) }),
5924
+ /* @__PURE__ */ jsx28("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
5323
5925
  ] }),
5324
- /* @__PURE__ */ jsxs16("div", { className: "sidebar-segment", children: [
5926
+ /* @__PURE__ */ jsxs19("div", { className: "sidebar-segment", children: [
5325
5927
  state.currentBlockIndex + 1,
5326
5928
  "/",
5327
5929
  state.totalBlocks
5328
5930
  ] }),
5329
- state.hasCaptions && /* @__PURE__ */ jsx23(
5931
+ state.hasCaptions && /* @__PURE__ */ jsx28(
5330
5932
  "button",
5331
5933
  {
5332
5934
  className: `sidebar-ctrl-btn ${state.captionMode !== "off" ? "sidebar-ctrl-btn--active" : ""}`,
5333
5935
  onClick: () => actions.cycleCaptionMode(),
5334
5936
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
5335
5937
  "aria-label": "Cycle caption style",
5336
- children: /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
5938
+ children: /* @__PURE__ */ jsx28("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx28("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
5337
5939
  }
5338
5940
  ),
5339
- actions.toggleFullscreen && /* @__PURE__ */ jsx23(
5941
+ actions.toggleFullscreen && /* @__PURE__ */ jsx28(
5340
5942
  "button",
5341
5943
  {
5342
5944
  className: `sidebar-ctrl-btn ${state.isFullscreen ? "sidebar-ctrl-btn--active" : ""}`,
5343
5945
  onClick: actions.toggleFullscreen,
5344
5946
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
5345
5947
  "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
5346
- children: state.isFullscreen ? /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
5948
+ children: state.isFullscreen ? /* @__PURE__ */ jsx28("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx28("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx28("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx28("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
5347
5949
  }
5348
5950
  )
5349
5951
  ] });
5350
5952
  }
5351
5953
 
5352
5954
  // src/DocPlayerWithSidebar.tsx
5353
- import { useRef as useRef10, useState as useState10, useCallback as useCallback8, useEffect as useEffect11 } from "react";
5354
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
5955
+ import { useRef as useRef10, useState as useState11, useCallback as useCallback8, useEffect as useEffect12 } from "react";
5956
+ import { jsx as jsx29, jsxs as jsxs20 } from "react/jsx-runtime";
5355
5957
  var DEFAULT_STATE = {
5356
5958
  isPlaying: false,
5357
5959
  currentTime: 0,
@@ -5385,7 +5987,7 @@ function DocPlayerWithSidebar({
5385
5987
  const stateRef = useRef10(DEFAULT_STATE);
5386
5988
  const actionsRef = useRef10(null);
5387
5989
  const wasPlayingRef = useRef10(false);
5388
- const [, setTick] = useState10(0);
5990
+ const [, setTick] = useState11(0);
5389
5991
  const handleStateChange = useCallback8(
5390
5992
  (state) => {
5391
5993
  stateRef.current = state;
@@ -5393,6 +5995,7 @@ function DocPlayerWithSidebar({
5393
5995
  wasPlayingRef.current = state.isPlaying;
5394
5996
  onPlayingChange(state.isPlaying);
5395
5997
  }
5998
+ if (!state.isPlaying) setTick((t) => t + 1);
5396
5999
  },
5397
6000
  [onPlayingChange]
5398
6001
  );
@@ -5404,14 +6007,15 @@ function DocPlayerWithSidebar({
5404
6007
  },
5405
6008
  []
5406
6009
  );
5407
- useEffect11(() => {
6010
+ useEffect12(() => {
5408
6011
  const interval = setInterval(() => {
6012
+ if (!stateRef.current.isPlaying) return;
5409
6013
  setTick((t) => t + 1);
5410
6014
  }, 250);
5411
6015
  return () => clearInterval(interval);
5412
6016
  }, []);
5413
- return /* @__PURE__ */ jsxs17("div", { className: "doc-player-sidebar-layout", children: [
5414
- /* @__PURE__ */ jsx24("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx24(
6017
+ return /* @__PURE__ */ jsxs20("div", { className: "doc-player-sidebar-layout", children: [
6018
+ /* @__PURE__ */ jsx29("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx29(
5415
6019
  DocPlayer,
5416
6020
  {
5417
6021
  doc,
@@ -5433,17 +6037,106 @@ function DocPlayerWithSidebar({
5433
6037
  forceViewport
5434
6038
  }
5435
6039
  ) }),
5436
- actionsRef.current && /* @__PURE__ */ jsx24(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
6040
+ actionsRef.current && /* @__PURE__ */ jsx29(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
5437
6041
  ] });
5438
6042
  }
5439
6043
 
6044
+ // src/hooks/useModalDialog.ts
6045
+ import { useEffect as useEffect13, useRef as useRef11 } from "react";
6046
+ var FOCUSABLE_SELECTOR = [
6047
+ "a[href]",
6048
+ "button:not([disabled])",
6049
+ "input:not([disabled])",
6050
+ "select:not([disabled])",
6051
+ "textarea:not([disabled])",
6052
+ '[tabindex]:not([tabindex="-1"])'
6053
+ ].join(",");
6054
+ function isolateModal(root) {
6055
+ const snapshots = [];
6056
+ let branch = root;
6057
+ while (branch?.parentElement) {
6058
+ for (const sibling of branch.parentElement.children) {
6059
+ if (sibling === branch || !(sibling instanceof HTMLElement)) continue;
6060
+ snapshots.push({
6061
+ element: sibling,
6062
+ inert: sibling.inert === true,
6063
+ ariaHidden: sibling.getAttribute("aria-hidden")
6064
+ });
6065
+ sibling.inert = true;
6066
+ sibling.setAttribute("aria-hidden", "true");
6067
+ }
6068
+ branch = branch.parentElement;
6069
+ if (branch === document.body) break;
6070
+ }
6071
+ return snapshots;
6072
+ }
6073
+ function useModalDialog({
6074
+ rootRef,
6075
+ dialogRef,
6076
+ initialFocusRef,
6077
+ returnFocusRef,
6078
+ onClose
6079
+ }) {
6080
+ const onCloseRef = useRef11(onClose);
6081
+ onCloseRef.current = onClose;
6082
+ useEffect13(() => {
6083
+ const root = rootRef.current;
6084
+ const dialog = dialogRef.current;
6085
+ if (!root || !dialog) return;
6086
+ const previousFocus = returnFocusRef?.current ?? (document.activeElement instanceof HTMLElement ? document.activeElement : null);
6087
+ const inertSnapshots = isolateModal(root);
6088
+ const activeWithinDialog = document.activeElement instanceof HTMLElement && dialog.contains(document.activeElement) ? document.activeElement : null;
6089
+ const focusTarget = initialFocusRef?.current ?? activeWithinDialog ?? dialog.querySelector(FOCUSABLE_SELECTOR) ?? dialog;
6090
+ focusTarget.focus();
6091
+ if (focusTarget instanceof HTMLInputElement || focusTarget instanceof HTMLTextAreaElement) {
6092
+ focusTarget.select();
6093
+ }
6094
+ const handleKeyDown = (event) => {
6095
+ if (event.key === "Escape") {
6096
+ event.preventDefault();
6097
+ event.stopPropagation();
6098
+ onCloseRef.current();
6099
+ return;
6100
+ }
6101
+ if (event.key !== "Tab") return;
6102
+ const focusable = [...dialog.querySelectorAll(FOCUSABLE_SELECTOR)].filter(
6103
+ (element) => !element.hidden && element.getAttribute("aria-hidden") !== "true" && element.getAttribute("aria-disabled") !== "true"
6104
+ );
6105
+ if (focusable.length === 0) {
6106
+ event.preventDefault();
6107
+ dialog.focus();
6108
+ return;
6109
+ }
6110
+ const first = focusable[0];
6111
+ const last = focusable[focusable.length - 1];
6112
+ if (event.shiftKey && (document.activeElement === first || !dialog.contains(document.activeElement))) {
6113
+ event.preventDefault();
6114
+ last.focus();
6115
+ } else if (!event.shiftKey && (document.activeElement === last || !dialog.contains(document.activeElement))) {
6116
+ event.preventDefault();
6117
+ first.focus();
6118
+ }
6119
+ };
6120
+ document.addEventListener("keydown", handleKeyDown, true);
6121
+ return () => {
6122
+ document.removeEventListener("keydown", handleKeyDown, true);
6123
+ for (const snapshot of inertSnapshots) {
6124
+ snapshot.element.inert = snapshot.inert;
6125
+ if (snapshot.ariaHidden === null) snapshot.element.removeAttribute("aria-hidden");
6126
+ else snapshot.element.setAttribute("aria-hidden", snapshot.ariaHidden);
6127
+ }
6128
+ if (previousFocus?.isConnected) previousFocus.focus();
6129
+ };
6130
+ }, [dialogRef, initialFocusRef, returnFocusRef, rootRef]);
6131
+ }
6132
+
5440
6133
  // src/jsonView/useJsonViewTokens.ts
5441
- import { useMemo as useMemo10 } from "react";
6134
+ import { useMemo as useMemo11 } from "react";
5442
6135
  import { buildJsonFormTokens, resolveJsonFormTheme } from "@bendyline/squisq/jsonForm";
5443
6136
  function useJsonViewTokens(theme, surface) {
5444
6137
  const auto = useAutoSurface(surface === "auto");
5445
6138
  const effectiveSurface = surface === "auto" ? auto : surface ?? void 0;
5446
- return useMemo10(() => {
6139
+ return useMemo11(() => {
5447
6140
  const style = buildJsonFormTokens(theme, effectiveSurface, {
5448
6141
  prefix: "--squisq-json"
5449
6142
  });
@@ -5459,26 +6152,26 @@ import {
5459
6152
  } from "@bendyline/squisq/jsonForm";
5460
6153
 
5461
6154
  // src/jsonView/viewers.tsx
5462
- import { Fragment as Fragment4, useMemo as useMemo11 } from "react";
6155
+ import { Fragment as Fragment5, useMemo as useMemo12 } from "react";
5463
6156
  import {
5464
6157
  arrayItemKind
5465
6158
  } from "@bendyline/squisq/jsonForm";
5466
6159
  import { parseMarkdown as parseMarkdown3 } from "@bendyline/squisq/markdown";
5467
- import { Fragment as Fragment5, jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
6160
+ import { Fragment as Fragment6, jsx as jsx30, jsxs as jsxs21 } from "react/jsx-runtime";
5468
6161
  function TextViewer({ value }) {
5469
6162
  if (value === void 0 || value === null || value === "") {
5470
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
6163
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-empty", children: "\u2014" });
5471
6164
  }
5472
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: String(value) });
6165
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-value", children: String(value) });
5473
6166
  }
5474
6167
  function MultilineViewer({ value }) {
5475
6168
  if (value === void 0 || value === null || value === "") {
5476
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
6169
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-empty", children: "\u2014" });
5477
6170
  }
5478
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
6171
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
5479
6172
  }
5480
6173
  function RichTextViewer({ value }) {
5481
- const nodes = useMemo11(() => {
6174
+ const nodes = useMemo12(() => {
5482
6175
  if (typeof value !== "string" || value === "") return null;
5483
6176
  try {
5484
6177
  const doc = parseMarkdown3(value);
@@ -5487,39 +6180,39 @@ function RichTextViewer({ value }) {
5487
6180
  return null;
5488
6181
  }
5489
6182
  }, [value]);
5490
- if (!nodes) return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
5491
- return /* @__PURE__ */ jsx25("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx25(MarkdownRenderer, { nodes }) });
6183
+ if (!nodes) return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-empty", children: "\u2014" });
6184
+ return /* @__PURE__ */ jsx30("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx30(MarkdownRenderer, { nodes }) });
5492
6185
  }
5493
6186
  function NumberViewer({ value }) {
5494
6187
  if (value === void 0 || value === null) {
5495
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
6188
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-empty", children: "\u2014" });
5496
6189
  }
5497
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: String(value) });
6190
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-value", children: String(value) });
5498
6191
  }
5499
6192
  function BooleanViewer({ value }) {
5500
6193
  const on = Boolean(value);
5501
- return /* @__PURE__ */ jsx25("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
6194
+ return /* @__PURE__ */ jsx30("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
5502
6195
  }
5503
6196
  function EnumViewer({ value, schema }) {
5504
6197
  if (value === void 0 || value === null || value === "") {
5505
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
6198
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-empty", children: "\u2014" });
5506
6199
  }
5507
6200
  const labels = schema.squisq?.enumLabels;
5508
6201
  const display = labels && typeof value === "string" ? labels[value] ?? value : String(value);
5509
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: display });
6202
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-value", children: display });
5510
6203
  }
5511
6204
  function ColorViewer({ value }) {
5512
6205
  if (typeof value !== "string" || value === "") {
5513
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
6206
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-empty", children: "\u2014" });
5514
6207
  }
5515
- return /* @__PURE__ */ jsxs18("span", { className: "squisq-jv-color", children: [
5516
- /* @__PURE__ */ jsx25("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
5517
- /* @__PURE__ */ jsx25("span", { className: "squisq-jv-color__hex", children: value })
6208
+ return /* @__PURE__ */ jsxs21("span", { className: "squisq-jv-color", children: [
6209
+ /* @__PURE__ */ jsx30("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
6210
+ /* @__PURE__ */ jsx30("span", { className: "squisq-jv-color__hex", children: value })
5518
6211
  ] });
5519
6212
  }
5520
6213
  function DateViewer({ value, schema }) {
5521
6214
  if (typeof value !== "string" || value === "") {
5522
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
6215
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-empty", children: "\u2014" });
5523
6216
  }
5524
6217
  const fmt = schema.format;
5525
6218
  let display = value;
@@ -5536,31 +6229,31 @@ function DateViewer({ value, schema }) {
5536
6229
  }
5537
6230
  } catch {
5538
6231
  }
5539
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: display });
6232
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-value", children: display });
5540
6233
  }
5541
6234
  function ChipBinViewer({ value, schema }) {
5542
6235
  if (!Array.isArray(value) || value.length === 0) {
5543
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
6236
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-empty", children: "\u2014" });
5544
6237
  }
5545
6238
  const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
5546
6239
  const labels = itemSchema?.squisq?.enumLabels;
5547
- return /* @__PURE__ */ jsx25("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
6240
+ return /* @__PURE__ */ jsx30("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
5548
6241
  const label = labels && typeof item === "string" ? labels[item] ?? String(item) : String(item);
5549
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-chip", children: label }, i);
6242
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-chip", children: label }, i);
5550
6243
  }) });
5551
6244
  }
5552
6245
  function CardStackViewer(props) {
5553
6246
  const { value, schema, rootSchema, rootData, pointer, density } = props;
5554
6247
  if (!Array.isArray(value) || value.length === 0) {
5555
- return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "No items" });
6248
+ return /* @__PURE__ */ jsx30("span", { className: "squisq-jv-empty", children: "No items" });
5556
6249
  }
5557
6250
  const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {};
5558
6251
  const itemLabel = itemSchema.squisq?.itemLabel;
5559
- return /* @__PURE__ */ jsx25("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
6252
+ return /* @__PURE__ */ jsx30("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
5560
6253
  const title = resolveItemTitle(itemLabel, item, i);
5561
- return /* @__PURE__ */ jsxs18("div", { className: "squisq-jv-card", children: [
5562
- title ? /* @__PURE__ */ jsx25("h4", { className: "squisq-jv-card__title", children: title }) : null,
5563
- /* @__PURE__ */ jsx25(
6254
+ return /* @__PURE__ */ jsxs21("div", { className: "squisq-jv-card", children: [
6255
+ title ? /* @__PURE__ */ jsx30("h4", { className: "squisq-jv-card__title", children: title }) : null,
6256
+ /* @__PURE__ */ jsx30(
5564
6257
  RenderNode,
5565
6258
  {
5566
6259
  value: item,
@@ -5591,16 +6284,16 @@ function GroupViewer(props) {
5591
6284
  const help = schema.squisq?.help ?? schema.description;
5592
6285
  const obj = (value && typeof value === "object" ? value : {}) ?? {};
5593
6286
  const propEntries = Object.entries(schema.properties ?? {});
5594
- return /* @__PURE__ */ jsxs18("section", { className: "squisq-jv-group", children: [
5595
- title ? /* @__PURE__ */ jsx25("h3", { className: "squisq-jv-group__title", children: title }) : null,
5596
- help ? /* @__PURE__ */ jsx25("p", { className: "squisq-jv-group__help", children: help }) : null,
5597
- propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx25(Fragment4, { children: /* @__PURE__ */ jsx25(
6287
+ return /* @__PURE__ */ jsxs21("section", { className: "squisq-jv-group", children: [
6288
+ title ? /* @__PURE__ */ jsx30("h3", { className: "squisq-jv-group__title", children: title }) : null,
6289
+ help ? /* @__PURE__ */ jsx30("p", { className: "squisq-jv-group__help", children: help }) : null,
6290
+ propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx30(Fragment5, { children: /* @__PURE__ */ jsx30(
5598
6291
  RowOrSection,
5599
6292
  {
5600
6293
  label: propSchema.squisq?.label ?? propSchema.title ?? key,
5601
6294
  help: propSchema.squisq?.help ?? propSchema.description,
5602
6295
  kindHint: propSchema,
5603
- children: /* @__PURE__ */ jsx25(
6296
+ children: /* @__PURE__ */ jsx30(
5604
6297
  RenderNode,
5605
6298
  {
5606
6299
  value: obj[key],
@@ -5624,11 +6317,11 @@ function RowOrSection({
5624
6317
  }) {
5625
6318
  const composite = isCompositeKind(kindHint);
5626
6319
  if (composite) {
5627
- return /* @__PURE__ */ jsx25(Fragment5, { children });
6320
+ return /* @__PURE__ */ jsx30(Fragment6, { children });
5628
6321
  }
5629
- return /* @__PURE__ */ jsxs18("div", { className: "squisq-jv-row", children: [
5630
- /* @__PURE__ */ jsx25("div", { className: "squisq-jv-label", title: help, children: label }),
5631
- /* @__PURE__ */ jsx25("div", { children })
6322
+ return /* @__PURE__ */ jsxs21("div", { className: "squisq-jv-row", children: [
6323
+ /* @__PURE__ */ jsx30("div", { className: "squisq-jv-label", title: help, children: label }),
6324
+ /* @__PURE__ */ jsx30("div", { children })
5632
6325
  ] });
5633
6326
  }
5634
6327
  function isCompositeKind(schema) {
@@ -5649,11 +6342,11 @@ function TabsViewer(props) {
5649
6342
  const matchedIndex = pickMatchingBranch(branches, value);
5650
6343
  const branch = branches[matchedIndex];
5651
6344
  if (!branch) {
5652
- return /* @__PURE__ */ jsx25(TextViewer, { ...props });
6345
+ return /* @__PURE__ */ jsx30(TextViewer, { ...props });
5653
6346
  }
5654
- return /* @__PURE__ */ jsxs18("div", { className: "squisq-jv-tabs", children: [
5655
- /* @__PURE__ */ jsx25("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
5656
- /* @__PURE__ */ jsx25(
6347
+ return /* @__PURE__ */ jsxs21("div", { className: "squisq-jv-tabs", children: [
6348
+ /* @__PURE__ */ jsx30("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
6349
+ /* @__PURE__ */ jsx30(
5657
6350
  RenderNode,
5658
6351
  {
5659
6352
  value,
@@ -5717,7 +6410,7 @@ var VIEWERS = {
5717
6410
  };
5718
6411
 
5719
6412
  // src/jsonView/RenderNode.tsx
5720
- import { jsx as jsx26 } from "react/jsx-runtime";
6413
+ import { jsx as jsx31 } from "react/jsx-runtime";
5721
6414
  function RenderNode(props) {
5722
6415
  const resolved = resolveRef(props.schema, props.rootSchema) ?? props.schema;
5723
6416
  if (resolveFlag(resolved.squisq?.hidden, props.rootData)) return null;
@@ -5733,18 +6426,18 @@ function RenderNode(props) {
5733
6426
  };
5734
6427
  if (kind === "group" || kind === "card") {
5735
6428
  const Group = Viewer;
5736
- return /* @__PURE__ */ jsx26(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
6429
+ return /* @__PURE__ */ jsx31(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
5737
6430
  }
5738
- return /* @__PURE__ */ jsx26(Viewer, { ...viewerProps });
6431
+ return /* @__PURE__ */ jsx31(Viewer, { ...viewerProps });
5739
6432
  }
5740
6433
 
5741
6434
  // src/jsonView/JsonView.tsx
5742
- import { jsx as jsx27 } from "react/jsx-runtime";
6435
+ import { jsx as jsx32 } from "react/jsx-runtime";
5743
6436
  function JsonView(props) {
5744
6437
  const { schema, value, theme, surface, density = "comfortable", className } = props;
5745
6438
  const { style } = useJsonViewTokens(theme, surface);
5746
6439
  const cls = "squisq-json-view" + (density === "compact" ? " squisq-json-view--compact" : "") + (className ? ` ${className}` : "");
5747
- return /* @__PURE__ */ jsx27("div", { className: cls, style, children: /* @__PURE__ */ jsx27(
6440
+ return /* @__PURE__ */ jsx32("div", { className: cls, style, children: /* @__PURE__ */ jsx32(
5748
6441
  RenderNode,
5749
6442
  {
5750
6443
  value,
@@ -5758,6 +6451,7 @@ function JsonView(props) {
5758
6451
  }
5759
6452
  export {
5760
6453
  BlockRenderer,
6454
+ CanvasSection,
5761
6455
  CaptionOverlay,
5762
6456
  DocControlsBottom,
5763
6457
  DocControlsOverlay,
@@ -5775,7 +6469,12 @@ export {
5775
6469
  MarkdownRenderer,
5776
6470
  MediaClipLayer,
5777
6471
  MediaContext,
6472
+ MermaidDiagram,
6473
+ MermaidLayer,
6474
+ PageSectionView,
6475
+ PageViewContext,
5778
6476
  PathLayer,
6477
+ ResourcePolicyContext,
5779
6478
  ShapeLayer,
5780
6479
  SocialCaptionOverlay,
5781
6480
  TableLayer,
@@ -5791,6 +6490,8 @@ export {
5791
6490
  useMediaProvider,
5792
6491
  useMediaSchedule,
5793
6492
  useMediaUrl,
6493
+ useModalDialog,
6494
+ usePageView,
6495
+ useResourcePolicy,
5794
6496
  useViewportOrientation
5795
6497
  };
5796
- //# sourceMappingURL=index.js.map