@bendyline/squisq-react 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/DocPlayer.tsx
2
- import { Fragment as Fragment3, useRef as useRef6, useState as useState6, useEffect as useEffect7, useCallback as useCallback5, useMemo as useMemo9 } from "react";
2
+ import { Fragment as Fragment3, useRef as useRef7, useState as useState7, useEffect as useEffect8, useCallback as useCallback6, useMemo as useMemo9 } from "react";
3
3
  import {
4
4
  isTemplateBlock as isTemplateBlock2,
5
5
  getCaptionAtTime as getCaptionAtTime2,
@@ -2245,16 +2245,164 @@ function useViewportOrientation() {
2245
2245
  };
2246
2246
  }
2247
2247
 
2248
+ // src/hooks/useSlideSwipe.ts
2249
+ import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState5 } from "react";
2250
+ var DISTANCE_RATIO = 0.3;
2251
+ var FLICK_VELOCITY = 0.5;
2252
+ var MIN_FLICK_DISTANCE = 12;
2253
+ var RUBBER_BAND = 0.35;
2254
+ var DEFAULT_SETTLE_MS = 260;
2255
+ function decideSwipe({
2256
+ dx,
2257
+ width,
2258
+ elapsedMs,
2259
+ canNext,
2260
+ canPrev
2261
+ }) {
2262
+ const distanceThreshold = width > 0 ? width * DISTANCE_RATIO : Infinity;
2263
+ const velocity = elapsedMs > 0 ? Math.abs(dx) / elapsedMs : 0;
2264
+ const passesDistance = Math.abs(dx) >= distanceThreshold;
2265
+ const passesFlick = velocity >= FLICK_VELOCITY && Math.abs(dx) >= MIN_FLICK_DISTANCE;
2266
+ if (!passesDistance && !passesFlick) return "snap";
2267
+ if (dx < 0) return canNext ? "next" : "snap";
2268
+ if (dx > 0) return canPrev ? "prev" : "snap";
2269
+ return "snap";
2270
+ }
2271
+ function useSlideSwipe(opts) {
2272
+ const settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
2273
+ const [offsetPx, setOffsetPx] = useState5(0);
2274
+ const [phase, setPhase] = useState5("idle");
2275
+ const optsRef = useRef5(opts);
2276
+ optsRef.current = opts;
2277
+ const dragRef = useRef5(null);
2278
+ const phaseRef = useRef5("idle");
2279
+ phaseRef.current = phase;
2280
+ const settleTimer = useRef5(null);
2281
+ const settleRaf = useRef5(null);
2282
+ const clearPending = useCallback4(() => {
2283
+ if (settleTimer.current != null) {
2284
+ clearTimeout(settleTimer.current);
2285
+ settleTimer.current = null;
2286
+ }
2287
+ if (settleRaf.current != null) {
2288
+ cancelAnimationFrame(settleRaf.current);
2289
+ settleRaf.current = null;
2290
+ }
2291
+ }, []);
2292
+ const onPointerDown = useCallback4((e) => {
2293
+ const o = optsRef.current;
2294
+ if (!o.enabled) return;
2295
+ if (phaseRef.current === "settling") return;
2296
+ if (e.pointerType === "mouse" && e.button !== 0) return;
2297
+ const target = e.target;
2298
+ if (target.closest?.("button, a, input, textarea, select, [data-no-swipe]")) return;
2299
+ dragRef.current = {
2300
+ pointerId: e.pointerId,
2301
+ startX: e.clientX,
2302
+ startTime: performance.now(),
2303
+ target
2304
+ };
2305
+ try {
2306
+ target.setPointerCapture?.(e.pointerId);
2307
+ } catch {
2308
+ }
2309
+ setPhase("dragging");
2310
+ setOffsetPx(0);
2311
+ }, []);
2312
+ useEffect7(() => {
2313
+ function currentWidth() {
2314
+ return optsRef.current.containerRef.current?.getBoundingClientRect().width ?? 0;
2315
+ }
2316
+ function endDrag(drag) {
2317
+ dragRef.current = null;
2318
+ try {
2319
+ drag.target.releasePointerCapture?.(drag.pointerId);
2320
+ } catch {
2321
+ }
2322
+ }
2323
+ function settleTo(target, onArrive) {
2324
+ setPhase("settling");
2325
+ settleRaf.current = requestAnimationFrame(() => {
2326
+ settleRaf.current = null;
2327
+ setOffsetPx(target);
2328
+ settleTimer.current = setTimeout(() => {
2329
+ settleTimer.current = null;
2330
+ onArrive?.();
2331
+ setOffsetPx(0);
2332
+ setPhase("idle");
2333
+ }, settleMs);
2334
+ });
2335
+ }
2336
+ function onMove(e) {
2337
+ const drag = dragRef.current;
2338
+ if (!drag || e.pointerId !== drag.pointerId) return;
2339
+ const o = optsRef.current;
2340
+ const raw = e.clientX - drag.startX;
2341
+ const blocked = raw > 0 && !o.canGoPrev || raw < 0 && !o.canGoNext;
2342
+ setOffsetPx(blocked ? raw * RUBBER_BAND : raw);
2343
+ }
2344
+ function onUp(e) {
2345
+ const drag = dragRef.current;
2346
+ if (!drag || e.pointerId !== drag.pointerId) return;
2347
+ endDrag(drag);
2348
+ const o = optsRef.current;
2349
+ const rawDx = e.clientX - drag.startX;
2350
+ const width = currentWidth();
2351
+ const elapsedMs = performance.now() - drag.startTime;
2352
+ const decision = decideSwipe({
2353
+ dx: rawDx,
2354
+ width,
2355
+ elapsedMs,
2356
+ canNext: o.canGoNext,
2357
+ canPrev: o.canGoPrev
2358
+ });
2359
+ if (decision === "snap") {
2360
+ settleTo(0);
2361
+ return;
2362
+ }
2363
+ const distance = Math.max(width, Math.abs(rawDx));
2364
+ const target = decision === "next" ? -distance : distance;
2365
+ settleTo(target, decision === "next" ? o.onNext : o.onPrev);
2366
+ }
2367
+ function onCancel(e) {
2368
+ const drag = dragRef.current;
2369
+ if (!drag || e.pointerId !== drag.pointerId) return;
2370
+ endDrag(drag);
2371
+ settleTo(0);
2372
+ }
2373
+ window.addEventListener("pointermove", onMove);
2374
+ window.addEventListener("pointerup", onUp);
2375
+ window.addEventListener("pointercancel", onCancel);
2376
+ return () => {
2377
+ window.removeEventListener("pointermove", onMove);
2378
+ window.removeEventListener("pointerup", onUp);
2379
+ window.removeEventListener("pointercancel", onCancel);
2380
+ };
2381
+ }, [settleMs]);
2382
+ useEffect7(() => {
2383
+ if (!opts.enabled) {
2384
+ dragRef.current = null;
2385
+ clearPending();
2386
+ setPhase("idle");
2387
+ setOffsetPx(0);
2388
+ }
2389
+ }, [opts.enabled, clearPending]);
2390
+ useEffect7(() => clearPending, [clearPending]);
2391
+ return { offsetPx, phase, onPointerDown };
2392
+ }
2393
+
2248
2394
  // src/DocPlayer.tsx
2249
2395
  import {
2250
2396
  expandCoverBlock,
2251
2397
  createTemplateContext,
2398
+ markdownToDoc as markdownToDoc2,
2252
2399
  DEFAULT_THEME as DEFAULT_THEME3,
2253
2400
  VIEWPORT_PRESETS as VIEWPORT_PRESETS4
2254
2401
  } from "@bendyline/squisq/doc";
2402
+ import { parseMarkdown as parseMarkdown2 } from "@bendyline/squisq/markdown";
2255
2403
 
2256
2404
  // src/DocProgressBar.tsx
2257
- import { useRef as useRef5, useState as useState5, useCallback as useCallback4 } from "react";
2405
+ import { useRef as useRef6, useState as useState6, useCallback as useCallback5 } from "react";
2258
2406
 
2259
2407
  // src/types.ts
2260
2408
  function formatTime(seconds) {
@@ -2272,10 +2420,10 @@ function DocProgressBar({
2272
2420
  expandedBlocks,
2273
2421
  getBlockTitle
2274
2422
  }) {
2275
- const progressBarRef = useRef5(null);
2276
- const [hoverPosition, setHoverPosition] = useState5(null);
2423
+ const progressBarRef = useRef6(null);
2424
+ const [hoverPosition, setHoverPosition] = useState6(null);
2277
2425
  const playProgress = state.totalDuration > 0 ? Math.max(0, Math.min(1, state.currentTime / state.totalDuration)) : 0;
2278
- const handleProgressHover = useCallback4((e) => {
2426
+ const handleProgressHover = useCallback5((e) => {
2279
2427
  const bar = progressBarRef.current;
2280
2428
  if (!bar) return;
2281
2429
  const rect = bar.getBoundingClientRect();
@@ -2283,10 +2431,10 @@ function DocProgressBar({
2283
2431
  const progress = Math.max(0, Math.min(1, x / rect.width));
2284
2432
  setHoverPosition(progress);
2285
2433
  }, []);
2286
- const handleProgressLeave = useCallback4(() => {
2434
+ const handleProgressLeave = useCallback5(() => {
2287
2435
  setHoverPosition(null);
2288
2436
  }, []);
2289
- const getBlockAtTimeLocal = useCallback4(
2437
+ const getBlockAtTimeLocal = useCallback5(
2290
2438
  (time) => {
2291
2439
  for (let i = expandedBlocks.length - 1; i >= 0; i--) {
2292
2440
  const blk = expandedBlocks[i];
@@ -2564,9 +2712,16 @@ function DocControlsOverlay({
2564
2712
  // src/DocControlsSlideshow.tsx
2565
2713
  import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2566
2714
  function DocControlsSlideshow({ state, slideNav }) {
2567
- const { currentBlockIndex, totalBlocks } = state;
2715
+ const {
2716
+ currentBlockIndex,
2717
+ currentSlideLabel,
2718
+ currentSlideNumber,
2719
+ totalBlocks,
2720
+ totalSlideNumber
2721
+ } = state;
2568
2722
  const isFirst = currentBlockIndex <= 0;
2569
2723
  const isLast = currentBlockIndex >= totalBlocks - 1;
2724
+ const counterText = totalBlocks > 0 ? currentSlideLabel ?? `${currentSlideNumber ?? currentBlockIndex + 1} / ${totalSlideNumber ?? totalBlocks}` : "\u2014";
2570
2725
  return /* @__PURE__ */ jsxs10(
2571
2726
  "div",
2572
2727
  {
@@ -2634,7 +2789,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2634
2789
  padding: "0 4px",
2635
2790
  letterSpacing: "0.02em"
2636
2791
  },
2637
- children: totalBlocks > 0 ? `${currentBlockIndex + 1} / ${totalBlocks}` : "\u2014"
2792
+ children: counterText
2638
2793
  }
2639
2794
  ),
2640
2795
  /* @__PURE__ */ jsx15(
@@ -2681,8 +2836,14 @@ import {
2681
2836
  resolveFontFamily as resolveFontFamily2
2682
2837
  } from "@bendyline/squisq/schemas";
2683
2838
  import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
2684
- import { getLayers, hasTemplate, DEFAULT_THEME as DEFAULT_THEME2, deriveTemplateInputs } from "@bendyline/squisq/doc";
2685
- import { extractPlainText } from "@bendyline/squisq/markdown";
2839
+ import {
2840
+ getLayers,
2841
+ hasTemplate,
2842
+ markdownToDoc,
2843
+ DEFAULT_THEME as DEFAULT_THEME2,
2844
+ deriveTemplateInputs
2845
+ } from "@bendyline/squisq/doc";
2846
+ import { extractPlainText, parseMarkdown } from "@bendyline/squisq/markdown";
2686
2847
 
2687
2848
  // src/MarkdownRenderer.tsx
2688
2849
  import { Fragment as Fragment2 } from "react";
@@ -2737,7 +2898,8 @@ function InlineAudioPlayer({
2737
2898
 
2738
2899
  // src/MarkdownRenderer.tsx
2739
2900
  import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
2740
- function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2901
+ var DEFAULT_CTX = { htmlPolicy: "sanitize" };
2902
+ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2741
2903
  return nodes.map((node, i) => {
2742
2904
  const key = `${keyPrefix}i${i}`;
2743
2905
  switch (node.type) {
@@ -2752,17 +2914,17 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2752
2914
  ] }, j)) }, key);
2753
2915
  }
2754
2916
  case "emphasis":
2755
- return /* @__PURE__ */ jsx18("em", { className: "squisq-md-em", children: renderInline(node.children, key, htmlPolicy) }, key);
2917
+ return /* @__PURE__ */ jsx18("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
2756
2918
  case "strong":
2757
- return /* @__PURE__ */ jsx18("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, htmlPolicy) }, key);
2919
+ return /* @__PURE__ */ jsx18("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
2758
2920
  case "delete":
2759
- return /* @__PURE__ */ jsx18("del", { className: "squisq-md-del", children: renderInline(node.children, key, htmlPolicy) }, key);
2921
+ return /* @__PURE__ */ jsx18("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
2760
2922
  case "inlineCode":
2761
2923
  return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-code", children: node.value }, key);
2762
2924
  case "link": {
2763
- const href = sanitizeUrl(node.url, "link");
2925
+ const href = sanitizeUrl(node.url, "link", { extraLinkSchemes: ctx.linkSchemes });
2764
2926
  if (!href) {
2765
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, htmlPolicy) }, key);
2927
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
2766
2928
  }
2767
2929
  return /* @__PURE__ */ jsx18(
2768
2930
  "a",
@@ -2772,7 +2934,7 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2772
2934
  title: node.title ?? void 0,
2773
2935
  target: "_blank",
2774
2936
  rel: "noopener noreferrer",
2775
- children: renderInline(node.children, key, htmlPolicy)
2937
+ children: renderInline(node.children, key, ctx)
2776
2938
  },
2777
2939
  key
2778
2940
  );
@@ -2784,8 +2946,8 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2784
2946
  case "inlineMath":
2785
2947
  return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-math", children: node.value }, key);
2786
2948
  case "htmlInline":
2787
- if (htmlPolicy === "strip") return null;
2788
- if (htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2949
+ if (ctx.htmlPolicy === "strip") return null;
2950
+ if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2789
2951
  return /* @__PURE__ */ jsx18(
2790
2952
  "span",
2791
2953
  {
@@ -2795,7 +2957,7 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2795
2957
  key
2796
2958
  );
2797
2959
  }
2798
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, htmlPolicy), `${key}h`) }, key);
2960
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
2799
2961
  case "footnoteReference":
2800
2962
  return /* @__PURE__ */ jsx18("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs11("a", { href: `#fn-${node.identifier}`, children: [
2801
2963
  "[",
@@ -2803,7 +2965,7 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2803
2965
  "]"
2804
2966
  ] }) }, key);
2805
2967
  case "linkReference":
2806
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, htmlPolicy) }, key);
2968
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
2807
2969
  case "imageReference":
2808
2970
  return /* @__PURE__ */ jsxs11("span", { className: "squisq-md-image-ref", children: [
2809
2971
  "[",
@@ -2811,7 +2973,7 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2811
2973
  "]"
2812
2974
  ] }, key);
2813
2975
  case "textDirective":
2814
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, htmlPolicy) }, key);
2976
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
2815
2977
  case "mention":
2816
2978
  return /* @__PURE__ */ jsxs11(
2817
2979
  "span",
@@ -2833,30 +2995,30 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2833
2995
  }
2834
2996
  });
2835
2997
  }
2836
- function renderBlock(node, key, htmlPolicy = "sanitize") {
2998
+ function renderBlock(node, key, ctx = DEFAULT_CTX) {
2837
2999
  switch (node.type) {
2838
3000
  case "paragraph":
2839
- return /* @__PURE__ */ jsx18("p", { className: "squisq-md-p", children: renderInline(node.children, key, htmlPolicy) }, key);
3001
+ return /* @__PURE__ */ jsx18("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
2840
3002
  case "heading": {
2841
3003
  const Tag = `h${node.depth}`;
2842
- return /* @__PURE__ */ jsx18(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, htmlPolicy) }, key);
3004
+ return /* @__PURE__ */ jsx18(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
2843
3005
  }
2844
3006
  case "blockquote":
2845
- return /* @__PURE__ */ jsx18("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, htmlPolicy) }, key);
3007
+ return /* @__PURE__ */ jsx18("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
2846
3008
  case "list":
2847
3009
  if (node.ordered) {
2848
- return /* @__PURE__ */ jsx18("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, htmlPolicy)) }, key);
3010
+ return /* @__PURE__ */ jsx18("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);
2849
3011
  }
2850
- return /* @__PURE__ */ jsx18("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, htmlPolicy)) }, key);
3012
+ return /* @__PURE__ */ jsx18("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
2851
3013
  case "code":
2852
3014
  return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx18("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
2853
3015
  case "thematicBreak":
2854
3016
  return /* @__PURE__ */ jsx18("hr", { className: "squisq-md-hr" }, key);
2855
3017
  case "table":
2856
- return renderTable(node.children, node.align, key, htmlPolicy);
3018
+ return renderTable(node.children, node.align, key, ctx);
2857
3019
  case "htmlBlock":
2858
- if (htmlPolicy === "strip") return null;
2859
- if (htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
3020
+ if (ctx.htmlPolicy === "strip") return null;
3021
+ if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2860
3022
  return /* @__PURE__ */ jsx18(
2861
3023
  "div",
2862
3024
  {
@@ -2866,7 +3028,7 @@ function renderBlock(node, key, htmlPolicy = "sanitize") {
2866
3028
  key
2867
3029
  );
2868
3030
  }
2869
- return /* @__PURE__ */ jsx18("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, htmlPolicy), `${key}h`) }, key);
3031
+ return /* @__PURE__ */ jsx18("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
2870
3032
  case "math":
2871
3033
  return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx18("code", { children: node.value }) }, key);
2872
3034
  case "definition":
@@ -2874,7 +3036,7 @@ function renderBlock(node, key, htmlPolicy = "sanitize") {
2874
3036
  case "footnoteDefinition":
2875
3037
  return /* @__PURE__ */ jsxs11("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
2876
3038
  /* @__PURE__ */ jsx18("sup", { children: node.label ?? node.identifier }),
2877
- renderBlocks(node.children, key, htmlPolicy)
3039
+ renderBlocks(node.children, key, ctx)
2878
3040
  ] }, key);
2879
3041
  case "containerDirective":
2880
3042
  return /* @__PURE__ */ jsxs11(
@@ -2884,7 +3046,7 @@ function renderBlock(node, key, htmlPolicy = "sanitize") {
2884
3046
  "data-directive": node.name,
2885
3047
  children: [
2886
3048
  node.label && /* @__PURE__ */ jsx18("div", { className: "squisq-md-directive-label", children: node.label }),
2887
- renderBlocks(node.children, key, htmlPolicy)
3049
+ renderBlocks(node.children, key, ctx)
2888
3050
  ]
2889
3051
  },
2890
3052
  key
@@ -2895,29 +3057,29 @@ function renderBlock(node, key, htmlPolicy = "sanitize") {
2895
3057
  {
2896
3058
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
2897
3059
  "data-directive": node.name,
2898
- children: renderInline(node.children, key, htmlPolicy)
3060
+ children: renderInline(node.children, key, ctx)
2899
3061
  },
2900
3062
  key
2901
3063
  );
2902
3064
  case "definitionList":
2903
3065
  return /* @__PURE__ */ jsx18("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
2904
3066
  if (child.type === "definitionTerm") {
2905
- return /* @__PURE__ */ jsx18("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, htmlPolicy) }, `${key}dt${i}`);
3067
+ return /* @__PURE__ */ jsx18("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
2906
3068
  }
2907
- return /* @__PURE__ */ jsx18("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, htmlPolicy) }, `${key}dd${i}`);
3069
+ return /* @__PURE__ */ jsx18("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
2908
3070
  }) }, key);
2909
3071
  default:
2910
3072
  return null;
2911
3073
  }
2912
3074
  }
2913
- function renderListItem(item, key, htmlPolicy = "sanitize") {
3075
+ function renderListItem(item, key, ctx = DEFAULT_CTX) {
2914
3076
  const isTask = item.checked !== null && item.checked !== void 0;
2915
3077
  return /* @__PURE__ */ jsxs11("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
2916
3078
  isTask && /* @__PURE__ */ jsx18("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
2917
- renderBlocks(item.children, key, htmlPolicy)
3079
+ renderBlocks(item.children, key, ctx)
2918
3080
  ] }, key);
2919
3081
  }
2920
- function renderTable(rows, align, key, htmlPolicy = "sanitize") {
3082
+ function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
2921
3083
  const [headerRow, ...bodyRows] = rows;
2922
3084
  return /* @__PURE__ */ jsxs11("table", { className: "squisq-md-table", children: [
2923
3085
  headerRow && /* @__PURE__ */ jsx18("thead", { children: /* @__PURE__ */ jsx18("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx18(
@@ -2925,7 +3087,7 @@ function renderTable(rows, align, key, htmlPolicy = "sanitize") {
2925
3087
  {
2926
3088
  className: "squisq-md-th",
2927
3089
  style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2928
- children: renderInline(cell.children, `${key}th${ci}`, htmlPolicy)
3090
+ children: renderInline(cell.children, `${key}th${ci}`, ctx)
2929
3091
  },
2930
3092
  `${key}th${ci}`
2931
3093
  )) }) }),
@@ -2934,14 +3096,14 @@ function renderTable(rows, align, key, htmlPolicy = "sanitize") {
2934
3096
  {
2935
3097
  className: "squisq-md-td",
2936
3098
  style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2937
- children: renderInline(cell.children, `${key}td${ri}-${ci}`, htmlPolicy)
3099
+ children: renderInline(cell.children, `${key}td${ri}-${ci}`, ctx)
2938
3100
  },
2939
3101
  `${key}td${ri}-${ci}`
2940
3102
  )) }, `${key}tr${ri}`)) })
2941
3103
  ] }, key);
2942
3104
  }
2943
- function renderBlocks(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2944
- return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`, htmlPolicy));
3105
+ function renderBlocks(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
3106
+ return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`, ctx));
2945
3107
  }
2946
3108
  function MdImage({ src, alt, title }) {
2947
3109
  const safeSrc = sanitizeUrl(src, "media");
@@ -3070,18 +3232,29 @@ function renderHtmlNodes(nodes, keyPrefix) {
3070
3232
  function MarkdownRenderer({
3071
3233
  nodes,
3072
3234
  className,
3073
- htmlPolicy = "sanitize"
3235
+ htmlPolicy = "sanitize",
3236
+ linkSchemes
3074
3237
  }) {
3075
3238
  if (!nodes || nodes.length === 0) return null;
3076
- return /* @__PURE__ */ jsx18("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", htmlPolicy) });
3239
+ return /* @__PURE__ */ jsx18("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes }) });
3077
3240
  }
3078
3241
 
3079
3242
  // src/LinearDocView.tsx
3080
3243
  import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3244
+ var warnedUnknownTemplates = /* @__PURE__ */ new Set();
3081
3245
  function isAnnotatedBlock(block) {
3082
3246
  const annotation = block.sourceHeading?.templateAnnotation;
3083
- if (!annotation) return false;
3084
- return !!annotation.template && hasTemplate(annotation.template);
3247
+ if (!annotation?.template) return false;
3248
+ if (!hasTemplate(annotation.template)) {
3249
+ if (!warnedUnknownTemplates.has(annotation.template)) {
3250
+ warnedUnknownTemplates.add(annotation.template);
3251
+ console.warn(
3252
+ `[squisq] Unknown template "${annotation.template}" \u2014 rendering the block as plain markdown.`
3253
+ );
3254
+ }
3255
+ return false;
3256
+ }
3257
+ return true;
3085
3258
  }
3086
3259
  function countAll(blocks) {
3087
3260
  let count = 0;
@@ -3173,6 +3346,7 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
3173
3346
  }
3174
3347
  function LinearDocView({
3175
3348
  doc,
3349
+ markdown,
3176
3350
  basePath = "/",
3177
3351
  viewport,
3178
3352
  className,
@@ -3182,7 +3356,15 @@ function LinearDocView({
3182
3356
  imageDisplayMode = "inline"
3183
3357
  }) {
3184
3358
  const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
3185
- const totalBlocks = useMemo8(() => countAll(doc.blocks), [doc.blocks]);
3359
+ const markdownDoc = useMemo8(
3360
+ () => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
3361
+ [doc, markdown]
3362
+ );
3363
+ const resolvedDoc = doc ?? markdownDoc;
3364
+ const totalBlocks = useMemo8(
3365
+ () => resolvedDoc ? countAll(resolvedDoc.blocks) : 0,
3366
+ [resolvedDoc]
3367
+ );
3186
3368
  const autoSurface = useAutoSurface(surface === "auto");
3187
3369
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
3188
3370
  const renderContext = useMemo8(() => {
@@ -3198,6 +3380,9 @@ function LinearDocView({
3198
3380
  };
3199
3381
  }, [activeViewport, totalBlocks, theme, resolvedSurface]);
3200
3382
  const activeTheme = renderContext.theme;
3383
+ if (!resolvedDoc) {
3384
+ return /* @__PURE__ */ jsx19("div", { className: `squisq-linear squisq-linear--empty ${className || ""}` });
3385
+ }
3201
3386
  const bgColor = activeTheme.colors.background;
3202
3387
  const textColor = activeTheme.colors.text;
3203
3388
  const mutedColor = activeTheme.colors.textMuted;
@@ -3358,7 +3543,7 @@ function LinearDocView({
3358
3543
  background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
3359
3544
  }
3360
3545
  ` }),
3361
- doc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
3546
+ resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
3362
3547
  BlockSection,
3363
3548
  {
3364
3549
  block,
@@ -3395,9 +3580,9 @@ var SMALL_WORDS = /* @__PURE__ */ new Set([
3395
3580
  "by",
3396
3581
  "is"
3397
3582
  ]);
3398
- function buildSegmentTitleMap(script) {
3583
+ function buildSegmentTitleMap(doc) {
3399
3584
  const map = /* @__PURE__ */ new Map();
3400
- for (const block of script.blocks) {
3585
+ for (const block of doc.blocks) {
3401
3586
  if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
3402
3587
  const segIdx = block.audioSegment;
3403
3588
  if (!map.has(segIdx)) {
@@ -3405,9 +3590,9 @@ function buildSegmentTitleMap(script) {
3405
3590
  }
3406
3591
  }
3407
3592
  }
3408
- for (let i = 0; i < script.audio.segments.length; i++) {
3593
+ for (let i = 0; i < doc.audio.segments.length; i++) {
3409
3594
  if (!map.has(i)) {
3410
- const name = script.audio.segments[i].name;
3595
+ const name = doc.audio.segments[i].name;
3411
3596
  if (name === "intro" || name.includes("intro")) {
3412
3597
  map.set(i, "Introduction");
3413
3598
  } else if (name === "flight-context" || name.includes("flight-context")) {
@@ -3423,14 +3608,34 @@ function buildSegmentTitleMap(script) {
3423
3608
  }
3424
3609
  return map;
3425
3610
  }
3426
- function DocPlayer({
3427
- script,
3428
- basePath,
3611
+ function isDevEnvironment() {
3612
+ try {
3613
+ return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
3614
+ } catch {
3615
+ return false;
3616
+ }
3617
+ }
3618
+ var warnedMissingStyles = false;
3619
+ function DocPlayer(props) {
3620
+ const { doc, markdown } = props;
3621
+ const markdownDoc = useMemo9(
3622
+ () => !doc && markdown !== void 0 ? markdownToDoc2(parseMarkdown2(markdown)) : void 0,
3623
+ [doc, markdown]
3624
+ );
3625
+ const resolvedDoc = doc ?? markdownDoc;
3626
+ if (!resolvedDoc) {
3627
+ return /* @__PURE__ */ jsx20("div", { className: "doc-player doc-player--empty" });
3628
+ }
3629
+ return /* @__PURE__ */ jsx20(DocPlayerContent, { ...props, doc: resolvedDoc });
3630
+ }
3631
+ function DocPlayerContent({
3632
+ doc,
3633
+ basePath = ".",
3429
3634
  renderMode = false,
3430
3635
  autoPlay = false,
3431
3636
  onEnded,
3432
3637
  onTimeUpdate,
3433
- audioProvider: externalAudioProvider,
3638
+ audioController: externalAudioController,
3434
3639
  showControls = true,
3435
3640
  showScrubber = false,
3436
3641
  muted = false,
@@ -3443,16 +3648,18 @@ function DocPlayer({
3443
3648
  onBlockMarkers,
3444
3649
  forceViewport,
3445
3650
  displayMode = "video",
3651
+ showCoverSlide = true,
3446
3652
  theme,
3447
3653
  surface,
3448
- captionStyle = "standard"
3654
+ captionStyle = "standard",
3655
+ enableSwipe = true
3449
3656
  }) {
3450
3657
  const isSlideshowMode = displayMode === "slideshow";
3451
3658
  const isLinearMode = displayMode === "linear";
3452
- const audioRef = useRef6(null);
3453
- const containerRef = useRef6(null);
3454
- const [tapFeedback, setTapFeedback] = useState6(null);
3455
- const tapFeedbackTimer = useRef6();
3659
+ const audioRef = useRef7(null);
3660
+ const containerRef = useRef7(null);
3661
+ const [tapFeedback, setTapFeedback] = useState7(null);
3662
+ const tapFeedbackTimer = useRef7();
3456
3663
  const { viewport, orientation } = useViewportOrientation();
3457
3664
  const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
3458
3665
  const isDebugMode = useMemo9(() => {
@@ -3460,8 +3667,20 @@ function DocPlayer({
3460
3667
  const params = new URLSearchParams(window.location.search);
3461
3668
  return params.get("debug") === "true";
3462
3669
  }, []);
3463
- const internalAudio = useAudioSync(audioRef, script.audio, basePath);
3464
- const audio = externalAudioProvider || internalAudio;
3670
+ const internalAudio = useAudioSync(audioRef, doc.audio, basePath);
3671
+ const audio = externalAudioController || internalAudio;
3672
+ useEffect8(() => {
3673
+ if (warnedMissingStyles || !isDevEnvironment()) return;
3674
+ const el = containerRef.current;
3675
+ if (!el || typeof getComputedStyle !== "function") return;
3676
+ const value = getComputedStyle(el).getPropertyValue("--squisq-styles-loaded");
3677
+ if (!value.trim()) {
3678
+ warnedMissingStyles = true;
3679
+ console.warn(
3680
+ '[squisq] @bendyline/squisq-react/styles is not loaded \u2014 import "@bendyline/squisq-react/styles"'
3681
+ );
3682
+ }
3683
+ }, []);
3465
3684
  const {
3466
3685
  currentTime,
3467
3686
  isPlaying,
@@ -3478,13 +3697,13 @@ function DocPlayer({
3478
3697
  skipToSegment: _skipToSegment,
3479
3698
  restart
3480
3699
  } = audio;
3481
- const mediaSchedule = useMemo9(() => resolveMediaSchedule(script), [script]);
3482
- const currentTimeRef = useRef6(currentTime);
3700
+ const mediaSchedule = useMemo9(() => resolveMediaSchedule(doc), [doc]);
3701
+ const currentTimeRef = useRef7(currentTime);
3483
3702
  currentTimeRef.current = currentTime;
3484
- const totalDurationRef = useRef6(totalDuration);
3703
+ const totalDurationRef = useRef7(totalDuration);
3485
3704
  totalDurationRef.current = totalDuration;
3486
- const expandedBlocksLenRef = useRef6(0);
3487
- const handleContainerClick = useCallback5(
3705
+ const expandedBlocksLenRef = useRef7(0);
3706
+ const handleContainerClick = useCallback6(
3488
3707
  (e) => {
3489
3708
  if (renderMode || isSlideshowMode || isLinearMode) return;
3490
3709
  const target = e.target;
@@ -3518,9 +3737,10 @@ function DocPlayer({
3518
3737
  nextBlock: _nextBlock,
3519
3738
  prevBlock: _prevBlock,
3520
3739
  blocks: expandedBlocks
3521
- } = useDocPlayback(script, currentTime, activeViewport, renderMode, effectiveTheme);
3740
+ } = useDocPlayback(doc, currentTime, activeViewport, renderMode, effectiveTheme);
3522
3741
  const coverBlock = useMemo9(() => {
3523
- const startBlockConfig = script.startBlock;
3742
+ const startBlockConfig = doc.startBlock;
3743
+ if (!showCoverSlide) return null;
3524
3744
  if (!startBlockConfig) return null;
3525
3745
  const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
3526
3746
  const layers = expandCoverBlock(startBlockConfig, context);
@@ -3533,40 +3753,59 @@ function DocPlayer({
3533
3753
  audioSegment: -1,
3534
3754
  layers
3535
3755
  };
3536
- }, [script.startBlock, activeViewport, effectiveTheme]);
3537
- const [coverForced, setCoverForced] = useState6(false);
3538
- const [coverGraceActive, setCoverGraceActive] = useState6(false);
3539
- const coverGraceTimer = useRef6();
3540
- const coverWasShowing = useRef6(false);
3541
- const hasPlayedOnce = useRef6(false);
3542
- const atRest = !!(coverBlock && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3756
+ }, [doc.startBlock, activeViewport, effectiveTheme, showCoverSlide]);
3757
+ const hasManagedCover = !!coverBlock;
3758
+ const [slideshowCoverVisible, setSlideshowCoverVisible] = useState7(false);
3759
+ const slideshowCoverInitKeyRef = useRef7("");
3760
+ useEffect8(() => {
3761
+ const initKey = `${isSlideshowMode}:${hasManagedCover}:${renderMode}`;
3762
+ if (slideshowCoverInitKeyRef.current === initKey) return;
3763
+ slideshowCoverInitKeyRef.current = initKey;
3764
+ if (isSlideshowMode && hasManagedCover && !renderMode) {
3765
+ setSlideshowCoverVisible(true);
3766
+ pause();
3767
+ } else {
3768
+ setSlideshowCoverVisible(false);
3769
+ }
3770
+ }, [isSlideshowMode, hasManagedCover, renderMode, pause]);
3771
+ const [coverForced, setCoverForced] = useState7(false);
3772
+ const [coverGraceActive, setCoverGraceActive] = useState7(false);
3773
+ const coverGraceTimer = useRef7();
3774
+ const coverWasShowing = useRef7(false);
3775
+ const hasPlayedOnce = useRef7(false);
3776
+ const atRest = !!(coverBlock && !isSlideshowMode && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3543
3777
  if (atRest) coverWasShowing.current = true;
3544
- useEffect7(() => {
3545
- if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
3778
+ useEffect8(() => {
3779
+ if (isPlaying && coverWasShowing.current && coverBlock && !renderMode && !isSlideshowMode) {
3546
3780
  coverWasShowing.current = false;
3547
3781
  hasPlayedOnce.current = true;
3548
3782
  setCoverGraceActive(true);
3549
3783
  coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
3550
3784
  }
3551
- }, [isPlaying, coverBlock, renderMode]);
3552
- useEffect7(() => () => clearTimeout(coverGraceTimer.current), []);
3553
- const showCoverBlock = !isSlideshowMode && !isLinearMode && coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3554
- const hasAutoPlayed = useRef6(false);
3555
- useEffect7(() => {
3785
+ }, [isPlaying, coverBlock, renderMode, isSlideshowMode]);
3786
+ useEffect8(() => () => clearTimeout(coverGraceTimer.current), []);
3787
+ const showVideoCoverBlock = !isSlideshowMode && !isLinearMode && !!coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3788
+ const showSlideshowCover = !!(isSlideshowMode && !isLinearMode && !renderMode && coverBlock && slideshowCoverVisible);
3789
+ const showCoverBlock = showVideoCoverBlock || showSlideshowCover;
3790
+ const slideshowHasCover = !!(isSlideshowMode && !renderMode && coverBlock);
3791
+ const slideshowSlideIndex = slideshowHasCover ? slideshowCoverVisible ? 0 : currentBlockIndex + 1 : currentBlockIndex;
3792
+ const slideshowTotalSlides = slideshowHasCover ? expandedBlocks.length + 1 : expandedBlocks.length;
3793
+ const hasAutoPlayed = useRef7(false);
3794
+ useEffect8(() => {
3556
3795
  if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
3557
3796
  hasAutoPlayed.current = true;
3558
3797
  play();
3559
3798
  }
3560
3799
  }, [isAudioReady, autoPlay, play]);
3561
- useEffect7(() => {
3800
+ useEffect8(() => {
3562
3801
  onTimeUpdate?.(currentTime);
3563
3802
  }, [currentTime, onTimeUpdate]);
3564
- useEffect7(() => {
3803
+ useEffect8(() => {
3565
3804
  if (isEnded) {
3566
3805
  onEnded?.();
3567
3806
  }
3568
3807
  }, [isEnded, onEnded]);
3569
- useEffect7(() => {
3808
+ useEffect8(() => {
3570
3809
  if ((renderMode || isDebugMode) && typeof window !== "undefined") {
3571
3810
  const w = window;
3572
3811
  w.seekTo = (time) => {
@@ -3645,7 +3884,7 @@ function DocPlayer({
3645
3884
  });
3646
3885
  };
3647
3886
  w.getDuration = () => {
3648
- const mediaDuration = getDocPlaybackDuration(script);
3887
+ const mediaDuration = getDocPlaybackDuration(doc);
3649
3888
  if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
3650
3889
  return mediaDuration;
3651
3890
  };
@@ -3655,20 +3894,20 @@ function DocPlayer({
3655
3894
  startTime: s.startTime,
3656
3895
  duration: s.duration
3657
3896
  }));
3658
- w.getAudioSegments = () => script.audio.segments.map((seg) => ({
3897
+ w.getAudioSegments = () => doc.audio.segments.map((seg) => ({
3659
3898
  src: seg.src,
3660
3899
  name: seg.name,
3661
3900
  duration: seg.duration,
3662
3901
  startTime: seg.startTime
3663
3902
  }));
3664
- w.getCaptions = () => script.captions?.phrases?.map((p) => ({
3903
+ w.getCaptions = () => doc.captions?.phrases?.map((p) => ({
3665
3904
  text: p.text,
3666
3905
  startTime: p.startTime,
3667
3906
  endTime: p.endTime
3668
3907
  })) || [];
3669
3908
  w.getChapters = () => {
3670
- const titleMap = buildSegmentTitleMap(script);
3671
- return script.audio.segments.map((seg, i) => ({
3909
+ const titleMap = buildSegmentTitleMap(doc);
3910
+ return doc.audio.segments.map((seg, i) => ({
3672
3911
  title: titleMap.get(i) || seg.name,
3673
3912
  startTime: seg.startTime,
3674
3913
  duration: seg.duration
@@ -3700,48 +3939,54 @@ function DocPlayer({
3700
3939
  };
3701
3940
  }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
3702
3941
  const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
3703
- const [captionMode, setCaptionMode] = useState6(defaultMode);
3942
+ const [captionMode, setCaptionMode] = useState7(defaultMode);
3943
+ useEffect8(() => {
3944
+ setCaptionMode(defaultMode);
3945
+ }, [defaultMode]);
3704
3946
  const captionsEnabled = captionMode !== "off";
3705
3947
  const activeCaptionStyle = captionMode === "social" ? "social" : "standard";
3706
- const setCaptionsEnabled = useCallback5(
3948
+ const setCaptionsEnabled = useCallback6(
3707
3949
  (enabled) => {
3708
3950
  setCaptionMode(enabled ? captionStyle || "standard" : "off");
3709
3951
  onCaptionsToggle?.(enabled);
3710
3952
  },
3711
3953
  [onCaptionsToggle, captionStyle]
3712
3954
  );
3713
- const cycleCaptionMode = useCallback5(() => {
3955
+ const cycleCaptionMode = useCallback6(() => {
3714
3956
  setCaptionMode((prev) => {
3715
3957
  const next = prev === "off" ? "standard" : prev === "standard" ? "social" : "off";
3716
3958
  onCaptionsToggle?.(next !== "off");
3717
3959
  return next;
3718
3960
  });
3719
3961
  }, [onCaptionsToggle]);
3720
- const hasCaptions = script.captions && script.captions.phrases.length > 0;
3721
- const segmentTitleMap = useMemo9(() => buildSegmentTitleMap(script), [script]);
3962
+ const hasCaptions = doc.captions && doc.captions.phrases.length > 0;
3963
+ const segmentTitleMap = useMemo9(() => buildSegmentTitleMap(doc), [doc]);
3722
3964
  const playbackState = useMemo9(
3723
3965
  () => ({
3724
3966
  isPlaying,
3725
3967
  currentTime,
3726
3968
  totalDuration,
3727
- currentBlockIndex,
3728
- totalBlocks: expandedBlocks.length,
3969
+ currentBlockIndex: slideshowSlideIndex,
3970
+ totalBlocks: slideshowTotalSlides,
3729
3971
  docProgress,
3730
3972
  hasCaptions: !!hasCaptions,
3731
3973
  captionsEnabled,
3732
3974
  captionMode,
3733
3975
  isFullscreen,
3734
3976
  currentSegmentIndex: currentSegment,
3735
- currentSegmentName: segmentTitleMap.get(currentSegment) ?? script.audio.segments[currentSegment]?.name ?? null,
3736
- currentBlock: currentBlock ?? null
3977
+ currentSegmentName: segmentTitleMap.get(currentSegment) ?? doc.audio.segments[currentSegment]?.name ?? null,
3978
+ currentBlock: showSlideshowCover ? coverBlock : currentBlock ?? null,
3979
+ currentSlideLabel: showSlideshowCover ? "Cover" : void 0,
3980
+ currentSlideNumber: slideshowHasCover && !showSlideshowCover ? currentBlockIndex + 1 : void 0,
3981
+ totalSlideNumber: slideshowHasCover ? expandedBlocks.length : void 0
3737
3982
  }),
3738
- // eslint-disable-next-line react-hooks/exhaustive-deps -- script.audio.segments is stable within a given script
3983
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- doc.audio.segments is stable within a given doc
3739
3984
  [
3740
3985
  isPlaying,
3741
3986
  currentTime,
3742
3987
  totalDuration,
3743
- currentBlockIndex,
3744
- expandedBlocks.length,
3988
+ slideshowSlideIndex,
3989
+ slideshowTotalSlides,
3745
3990
  docProgress,
3746
3991
  hasCaptions,
3747
3992
  captionsEnabled,
@@ -3749,7 +3994,12 @@ function DocPlayer({
3749
3994
  isFullscreen,
3750
3995
  currentSegment,
3751
3996
  segmentTitleMap,
3752
- currentBlock
3997
+ currentBlock,
3998
+ currentBlockIndex,
3999
+ showSlideshowCover,
4000
+ coverBlock,
4001
+ slideshowHasCover,
4002
+ expandedBlocks.length
3753
4003
  ]
3754
4004
  );
3755
4005
  const playbackActions = useMemo9(
@@ -3766,24 +4016,56 @@ function DocPlayer({
3766
4016
  const slideNavActions = useMemo9(
3767
4017
  () => ({
3768
4018
  nextSlide: () => {
4019
+ if (slideshowHasCover && slideshowCoverVisible) {
4020
+ const target = expandedBlocks[0];
4021
+ if (target) {
4022
+ setSlideshowCoverVisible(false);
4023
+ seekTo(target.startTime);
4024
+ pause();
4025
+ }
4026
+ return;
4027
+ }
3769
4028
  if (currentBlockIndex < expandedBlocks.length - 1) {
3770
4029
  const target = expandedBlocks[currentBlockIndex + 1];
3771
4030
  if (target) {
4031
+ setSlideshowCoverVisible(false);
3772
4032
  seekTo(target.startTime);
3773
4033
  pause();
3774
4034
  }
3775
4035
  }
3776
4036
  },
3777
4037
  prevSlide: () => {
4038
+ if (slideshowHasCover && !slideshowCoverVisible && currentBlockIndex <= 0) {
4039
+ setSlideshowCoverVisible(true);
4040
+ seekTo(0);
4041
+ pause();
4042
+ return;
4043
+ }
3778
4044
  if (currentBlockIndex > 0) {
3779
4045
  const target = expandedBlocks[currentBlockIndex - 1];
3780
4046
  if (target) {
4047
+ setSlideshowCoverVisible(false);
3781
4048
  seekTo(target.startTime);
3782
4049
  pause();
3783
4050
  }
3784
4051
  }
3785
4052
  },
3786
4053
  goToSlide: (index) => {
4054
+ if (slideshowHasCover) {
4055
+ if (index === 0) {
4056
+ setSlideshowCoverVisible(true);
4057
+ seekTo(0);
4058
+ pause();
4059
+ return;
4060
+ }
4061
+ const target = expandedBlocks[index - 1];
4062
+ if (target) {
4063
+ setSlideshowCoverVisible(false);
4064
+ seekTo(target.startTime);
4065
+ pause();
4066
+ }
4067
+ return;
4068
+ }
3787
4069
  if (index >= 0 && index < expandedBlocks.length) {
3788
4070
  const target = expandedBlocks[index];
3789
4071
  if (target) {
@@ -3793,15 +4075,24 @@ function DocPlayer({
3793
4075
  }
3794
4076
  }
3795
4077
  }),
3796
- [currentBlockIndex, expandedBlocks, seekTo, pause]
4078
+ [currentBlockIndex, expandedBlocks, seekTo, pause, slideshowHasCover, slideshowCoverVisible]
3797
4079
  );
3798
- useEffect7(() => {
4080
+ const swipeEnabled = isSlideshowMode && !isLinearMode && !renderMode && enableSwipe;
4081
+ const swipe = useSlideSwipe({
4082
+ enabled: swipeEnabled,
4083
+ containerRef,
4084
+ canGoNext: slideshowSlideIndex < slideshowTotalSlides - 1,
4085
+ canGoPrev: slideshowSlideIndex > 0,
4086
+ onNext: slideNavActions.nextSlide,
4087
+ onPrev: slideNavActions.prevSlide
4088
+ });
4089
+ useEffect8(() => {
3799
4090
  onPlaybackStateChange?.(playbackState);
3800
4091
  }, [playbackState, onPlaybackStateChange]);
3801
- useEffect7(() => {
4092
+ useEffect8(() => {
3802
4093
  onControlsReady?.({ play, pause, ...playbackActions });
3803
4094
  }, [play, pause, playbackActions, onControlsReady]);
3804
- const getBlockTitle = useCallback5((block) => {
4095
+ const getBlockTitle = useCallback6((block) => {
3805
4096
  const docBlock = block;
3806
4097
  if (isTemplateBlock2(docBlock)) {
3807
4098
  const props = docBlock;
@@ -3840,13 +4131,13 @@ function DocPlayer({
3840
4131
  };
3841
4132
  });
3842
4133
  }, [expandedBlocks, totalDuration, getBlockTitle]);
3843
- useEffect7(() => {
4134
+ useEffect8(() => {
3844
4135
  if (blockMarkers.length > 0) {
3845
4136
  onBlockMarkers?.(blockMarkers);
3846
4137
  }
3847
4138
  }, [blockMarkers, onBlockMarkers]);
3848
- expandedBlocksLenRef.current = expandedBlocks.length;
3849
- const handleKeyDown = useCallback5(
4139
+ expandedBlocksLenRef.current = isSlideshowMode ? slideshowTotalSlides : expandedBlocks.length;
4140
+ const handleKeyDown = useCallback6(
3850
4141
  (e) => {
3851
4142
  const activeEl = document.activeElement;
3852
4143
  if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.tagName === "SELECT")) {
@@ -3892,7 +4183,7 @@ function DocPlayer({
3892
4183
  },
3893
4184
  [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
3894
4185
  );
3895
- useEffect7(() => {
4186
+ useEffect8(() => {
3896
4187
  if (renderMode) return;
3897
4188
  window.addEventListener("keydown", handleKeyDown);
3898
4189
  return () => window.removeEventListener("keydown", handleKeyDown);
@@ -3912,7 +4203,7 @@ function DocPlayer({
3912
4203
  children: /* @__PURE__ */ jsx20(
3913
4204
  LinearDocView,
3914
4205
  {
3915
- doc: script,
4206
+ doc,
3916
4207
  basePath,
3917
4208
  viewport: activeViewport,
3918
4209
  theme,
@@ -3926,15 +4217,19 @@ function DocPlayer({
3926
4217
  "div",
3927
4218
  {
3928
4219
  ref: containerRef,
3929
- className: "doc-player",
4220
+ className: `doc-player${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
3930
4221
  onClick: handleContainerClick,
4222
+ onPointerDown: swipe.onPointerDown,
3931
4223
  style: {
3932
4224
  position: "relative",
3933
4225
  width: "100%",
3934
4226
  aspectRatio: `${activeViewport.width} / ${activeViewport.height}`,
3935
4227
  margin: "0 auto",
3936
4228
  overflow: "hidden",
3937
- cursor: renderMode ? void 0 : "pointer"
4229
+ // Swipe uses the grab/grabbing cursor via CSS classes; let vertical page
4230
+ // scroll through on touch while we own horizontal drags.
4231
+ cursor: renderMode || swipeEnabled ? void 0 : "pointer",
4232
+ touchAction: swipeEnabled ? "pan-y" : void 0
3938
4233
  },
3939
4234
  children: [
3940
4235
  /* @__PURE__ */ jsx20("audio", { ref: audioRef, preload: "auto", muted }),
@@ -3974,21 +4269,29 @@ function DocPlayer({
3974
4269
  viewport: activeViewport
3975
4270
  }
3976
4271
  ) }, previousBlock.id),
3977
- !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--active", children: /* @__PURE__ */ jsx20(
3978
- BlockRenderer,
4272
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20(
4273
+ "div",
3979
4274
  {
3980
- block: currentBlock,
3981
- blockTime,
3982
- basePath,
3983
- isEntering,
3984
- viewport: activeViewport,
3985
- isPlaying
3986
- }
3987
- ) }, currentBlock.id),
4275
+ className: `doc-player__block doc-player__block--active${swipe.phase !== "idle" ? ` doc-player__block--${swipe.phase}` : ""}`,
4276
+ style: swipe.phase !== "idle" ? { transform: `translateX(${swipe.offsetPx}px)` } : void 0,
4277
+ children: /* @__PURE__ */ jsx20(
4278
+ BlockRenderer,
4279
+ {
4280
+ block: currentBlock,
4281
+ blockTime,
4282
+ basePath,
4283
+ isEntering,
4284
+ viewport: activeViewport,
4285
+ isPlaying
4286
+ }
4287
+ )
4288
+ },
4289
+ currentBlock.id
4290
+ ),
3988
4291
  hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx20(
3989
4292
  CaptionOverlay,
3990
4293
  {
3991
- captions: script.captions,
4294
+ captions: doc.captions,
3992
4295
  currentTime,
3993
4296
  enabled: captionsEnabled && (renderMode || isPlaying || currentTime > 0),
3994
4297
  fontSize: 16,
@@ -4049,9 +4352,8 @@ function DocPlayer({
4049
4352
  /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4050
4353
  "(progress: ",
4051
4354
  (docProgress * 100).toFixed(1),
4052
- "%, scriptDur:",
4053
- " ",
4054
- script.duration.toFixed(1),
4355
+ "%, scriptDur: ",
4356
+ doc.duration.toFixed(1),
4055
4357
  ")"
4056
4358
  ] })
4057
4359
  ] }),
@@ -4069,11 +4371,11 @@ function DocPlayer({
4069
4371
  " ",
4070
4372
  currentSegment,
4071
4373
  "/",
4072
- script.audio.segments.length - 1,
4374
+ doc.audio.segments.length - 1,
4073
4375
  " ",
4074
4376
  /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4075
4377
  "(",
4076
- script.audio.segments[currentSegment]?.name || "none",
4378
+ doc.audio.segments[currentSegment]?.name || "none",
4077
4379
  ")"
4078
4380
  ] })
4079
4381
  ] }),
@@ -4095,13 +4397,13 @@ function DocPlayer({
4095
4397
  showCoverBlock && /* @__PURE__ */ jsx20("span", { style: { color: "#60a5fa" }, children: " (cover)" })
4096
4398
  ] }),
4097
4399
  hasCaptions && (() => {
4098
- const debugPhrase = getCaptionAtTime2(script.captions, currentTime);
4400
+ const debugPhrase = getCaptionAtTime2(doc.captions, currentTime);
4099
4401
  const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
4100
4402
  return /* @__PURE__ */ jsxs13(Fragment3, { children: [
4101
4403
  /* @__PURE__ */ jsxs13("div", { children: [
4102
4404
  /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "captions:" }),
4103
4405
  " ",
4104
- script.captions?.phrases.length || 0,
4406
+ doc.captions?.phrases.length || 0,
4105
4407
  " phrases",
4106
4408
  " ",
4107
4409
  /* @__PURE__ */ jsxs13("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
@@ -4331,7 +4633,7 @@ function DocControlsSidebar({ state, actions }) {
4331
4633
  }
4332
4634
 
4333
4635
  // src/DocPlayerWithSidebar.tsx
4334
- import { useRef as useRef7, useState as useState7, useCallback as useCallback6, useEffect as useEffect8 } from "react";
4636
+ import { useRef as useRef8, useState as useState8, useCallback as useCallback7, useEffect as useEffect9 } from "react";
4335
4637
  import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
4336
4638
  var DEFAULT_STATE = {
4337
4639
  isPlaying: false,
@@ -4348,24 +4650,25 @@ var DEFAULT_STATE = {
4348
4650
  currentBlock: null
4349
4651
  };
4350
4652
  function DocPlayerWithSidebar({
4351
- script,
4653
+ doc,
4352
4654
  basePath,
4353
4655
  autoPlay = false,
4354
4656
  onEnded,
4355
4657
  onTimeUpdate,
4356
- audioProvider,
4658
+ audioController,
4357
4659
  muted,
4358
4660
  captionsEnabled,
4359
4661
  isFullscreen,
4360
4662
  onFullscreenToggle,
4361
4663
  forceViewport,
4362
- onPlayingChange
4664
+ onPlayingChange,
4665
+ theme
4363
4666
  }) {
4364
- const stateRef = useRef7(DEFAULT_STATE);
4365
- const actionsRef = useRef7(null);
4366
- const wasPlayingRef = useRef7(false);
4367
- const [, setTick] = useState7(0);
4368
- const handleStateChange = useCallback6(
4667
+ const stateRef = useRef8(DEFAULT_STATE);
4668
+ const actionsRef = useRef8(null);
4669
+ const wasPlayingRef = useRef8(false);
4670
+ const [, setTick] = useState8(0);
4671
+ const handleStateChange = useCallback7(
4369
4672
  (state) => {
4370
4673
  stateRef.current = state;
4371
4674
  if (onPlayingChange && state.isPlaying !== wasPlayingRef.current) {
@@ -4375,7 +4678,7 @@ function DocPlayerWithSidebar({
4375
4678
  },
4376
4679
  [onPlayingChange]
4377
4680
  );
4378
- const handleControlsReady = useCallback6(
4681
+ const handleControlsReady = useCallback7(
4379
4682
  (controls) => {
4380
4683
  const isFirst = !actionsRef.current;
4381
4684
  actionsRef.current = controls;
@@ -4383,7 +4686,7 @@ function DocPlayerWithSidebar({
4383
4686
  },
4384
4687
  []
4385
4688
  );
4386
- useEffect8(() => {
4689
+ useEffect9(() => {
4387
4690
  const interval = setInterval(() => {
4388
4691
  setTick((t) => t + 1);
4389
4692
  }, 250);
@@ -4393,12 +4696,13 @@ function DocPlayerWithSidebar({
4393
4696
  /* @__PURE__ */ jsx23("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx23(
4394
4697
  DocPlayer,
4395
4698
  {
4396
- script,
4699
+ doc,
4700
+ theme,
4397
4701
  basePath,
4398
4702
  autoPlay,
4399
4703
  onEnded,
4400
4704
  onTimeUpdate,
4401
- audioProvider,
4705
+ audioController,
4402
4706
  muted,
4403
4707
  captionsEnabled,
4404
4708
  showControls: isFullscreen,
@@ -4416,36 +4720,15 @@ function DocPlayerWithSidebar({
4416
4720
 
4417
4721
  // src/jsonView/useJsonViewTokens.ts
4418
4722
  import { useMemo as useMemo10 } from "react";
4419
- import {
4420
- applySurface as applySurface3,
4421
- resolveFontFamily as resolveFontFamily3
4422
- } from "@bendyline/squisq/schemas";
4423
- import { DEFAULT_THEME as DEFAULT_THEME4 } from "@bendyline/squisq/doc";
4723
+ import { buildJsonFormTokens, resolveJsonFormTheme } from "@bendyline/squisq/jsonForm";
4424
4724
  function useJsonViewTokens(theme, surface) {
4425
4725
  const auto = useAutoSurface(surface === "auto");
4426
4726
  const effectiveSurface = surface === "auto" ? auto : surface ?? void 0;
4427
4727
  return useMemo10(() => {
4428
- const baseTheme = theme ?? DEFAULT_THEME4;
4429
- const finalTheme = effectiveSurface ? applySurface3(baseTheme, effectiveSurface) : baseTheme;
4430
- const titleFont = resolveFontFamily3(finalTheme.typography.titleFont, "system-ui, sans-serif");
4431
- const bodyFont = resolveFontFamily3(finalTheme.typography.bodyFont, "system-ui, sans-serif");
4432
- const monoFont = resolveFontFamily3(
4433
- finalTheme.typography.monoFont,
4434
- "ui-monospace, Consolas, monospace"
4435
- );
4436
- const style = {
4437
- ["--squisq-json-bg"]: finalTheme.colors.background,
4438
- ["--squisq-json-text"]: finalTheme.colors.text,
4439
- ["--squisq-json-muted"]: finalTheme.colors.textMuted,
4440
- ["--squisq-json-primary"]: finalTheme.colors.primary,
4441
- ["--squisq-json-accent"]: finalTheme.colors.secondary,
4442
- ["--squisq-json-border"]: `color-mix(in srgb, ${finalTheme.colors.textMuted} 35%, transparent)`,
4443
- ["--squisq-json-title-font"]: titleFont,
4444
- ["--squisq-json-body-font"]: bodyFont,
4445
- ["--squisq-json-mono-font"]: monoFont,
4446
- ["--squisq-json-radius"]: `${finalTheme.style.borderRadius ?? 8}px`
4447
- };
4448
- return { style, theme: finalTheme };
4728
+ const style = buildJsonFormTokens(theme, effectiveSurface, {
4729
+ prefix: "--squisq-json"
4730
+ });
4731
+ return { style, theme: resolveJsonFormTheme(theme, effectiveSurface) };
4449
4732
  }, [theme, effectiveSurface]);
4450
4733
  }
4451
4734
 
@@ -4461,7 +4744,7 @@ import { Fragment as Fragment4, useMemo as useMemo11 } from "react";
4461
4744
  import {
4462
4745
  arrayItemKind
4463
4746
  } from "@bendyline/squisq/jsonForm";
4464
- import { parseMarkdown } from "@bendyline/squisq/markdown";
4747
+ import { parseMarkdown as parseMarkdown3 } from "@bendyline/squisq/markdown";
4465
4748
  import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
4466
4749
  function TextViewer({ value }) {
4467
4750
  if (value === void 0 || value === null || value === "") {
@@ -4479,7 +4762,7 @@ function RichTextViewer({ value }) {
4479
4762
  const nodes = useMemo11(() => {
4480
4763
  if (typeof value !== "string" || value === "") return null;
4481
4764
  try {
4482
- const doc = parseMarkdown(value);
4765
+ const doc = parseMarkdown3(value);
4483
4766
  return doc.children;
4484
4767
  } catch {
4485
4768
  return null;