@bendyline/squisq-react 1.4.0 → 1.4.1

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];
@@ -2681,8 +2829,14 @@ import {
2681
2829
  resolveFontFamily as resolveFontFamily2
2682
2830
  } from "@bendyline/squisq/schemas";
2683
2831
  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";
2832
+ import {
2833
+ getLayers,
2834
+ hasTemplate,
2835
+ markdownToDoc,
2836
+ DEFAULT_THEME as DEFAULT_THEME2,
2837
+ deriveTemplateInputs
2838
+ } from "@bendyline/squisq/doc";
2839
+ import { extractPlainText, parseMarkdown } from "@bendyline/squisq/markdown";
2686
2840
 
2687
2841
  // src/MarkdownRenderer.tsx
2688
2842
  import { Fragment as Fragment2 } from "react";
@@ -2737,7 +2891,8 @@ function InlineAudioPlayer({
2737
2891
 
2738
2892
  // src/MarkdownRenderer.tsx
2739
2893
  import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
2740
- function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2894
+ var DEFAULT_CTX = { htmlPolicy: "sanitize" };
2895
+ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2741
2896
  return nodes.map((node, i) => {
2742
2897
  const key = `${keyPrefix}i${i}`;
2743
2898
  switch (node.type) {
@@ -2752,17 +2907,17 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2752
2907
  ] }, j)) }, key);
2753
2908
  }
2754
2909
  case "emphasis":
2755
- return /* @__PURE__ */ jsx18("em", { className: "squisq-md-em", children: renderInline(node.children, key, htmlPolicy) }, key);
2910
+ return /* @__PURE__ */ jsx18("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
2756
2911
  case "strong":
2757
- return /* @__PURE__ */ jsx18("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, htmlPolicy) }, key);
2912
+ return /* @__PURE__ */ jsx18("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
2758
2913
  case "delete":
2759
- return /* @__PURE__ */ jsx18("del", { className: "squisq-md-del", children: renderInline(node.children, key, htmlPolicy) }, key);
2914
+ return /* @__PURE__ */ jsx18("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
2760
2915
  case "inlineCode":
2761
2916
  return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-code", children: node.value }, key);
2762
2917
  case "link": {
2763
- const href = sanitizeUrl(node.url, "link");
2918
+ const href = sanitizeUrl(node.url, "link", { extraLinkSchemes: ctx.linkSchemes });
2764
2919
  if (!href) {
2765
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, htmlPolicy) }, key);
2920
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
2766
2921
  }
2767
2922
  return /* @__PURE__ */ jsx18(
2768
2923
  "a",
@@ -2772,7 +2927,7 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2772
2927
  title: node.title ?? void 0,
2773
2928
  target: "_blank",
2774
2929
  rel: "noopener noreferrer",
2775
- children: renderInline(node.children, key, htmlPolicy)
2930
+ children: renderInline(node.children, key, ctx)
2776
2931
  },
2777
2932
  key
2778
2933
  );
@@ -2784,8 +2939,8 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2784
2939
  case "inlineMath":
2785
2940
  return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-math", children: node.value }, key);
2786
2941
  case "htmlInline":
2787
- if (htmlPolicy === "strip") return null;
2788
- if (htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2942
+ if (ctx.htmlPolicy === "strip") return null;
2943
+ if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2789
2944
  return /* @__PURE__ */ jsx18(
2790
2945
  "span",
2791
2946
  {
@@ -2795,7 +2950,7 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2795
2950
  key
2796
2951
  );
2797
2952
  }
2798
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, htmlPolicy), `${key}h`) }, key);
2953
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
2799
2954
  case "footnoteReference":
2800
2955
  return /* @__PURE__ */ jsx18("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs11("a", { href: `#fn-${node.identifier}`, children: [
2801
2956
  "[",
@@ -2803,7 +2958,7 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2803
2958
  "]"
2804
2959
  ] }) }, key);
2805
2960
  case "linkReference":
2806
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, htmlPolicy) }, key);
2961
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
2807
2962
  case "imageReference":
2808
2963
  return /* @__PURE__ */ jsxs11("span", { className: "squisq-md-image-ref", children: [
2809
2964
  "[",
@@ -2811,7 +2966,7 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2811
2966
  "]"
2812
2967
  ] }, key);
2813
2968
  case "textDirective":
2814
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, htmlPolicy) }, key);
2969
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
2815
2970
  case "mention":
2816
2971
  return /* @__PURE__ */ jsxs11(
2817
2972
  "span",
@@ -2833,30 +2988,30 @@ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2833
2988
  }
2834
2989
  });
2835
2990
  }
2836
- function renderBlock(node, key, htmlPolicy = "sanitize") {
2991
+ function renderBlock(node, key, ctx = DEFAULT_CTX) {
2837
2992
  switch (node.type) {
2838
2993
  case "paragraph":
2839
- return /* @__PURE__ */ jsx18("p", { className: "squisq-md-p", children: renderInline(node.children, key, htmlPolicy) }, key);
2994
+ return /* @__PURE__ */ jsx18("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
2840
2995
  case "heading": {
2841
2996
  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);
2997
+ return /* @__PURE__ */ jsx18(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
2843
2998
  }
2844
2999
  case "blockquote":
2845
- return /* @__PURE__ */ jsx18("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, htmlPolicy) }, key);
3000
+ return /* @__PURE__ */ jsx18("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
2846
3001
  case "list":
2847
3002
  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);
3003
+ 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
3004
  }
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);
3005
+ 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
3006
  case "code":
2852
3007
  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
3008
  case "thematicBreak":
2854
3009
  return /* @__PURE__ */ jsx18("hr", { className: "squisq-md-hr" }, key);
2855
3010
  case "table":
2856
- return renderTable(node.children, node.align, key, htmlPolicy);
3011
+ return renderTable(node.children, node.align, key, ctx);
2857
3012
  case "htmlBlock":
2858
- if (htmlPolicy === "strip") return null;
2859
- if (htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
3013
+ if (ctx.htmlPolicy === "strip") return null;
3014
+ if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2860
3015
  return /* @__PURE__ */ jsx18(
2861
3016
  "div",
2862
3017
  {
@@ -2866,7 +3021,7 @@ function renderBlock(node, key, htmlPolicy = "sanitize") {
2866
3021
  key
2867
3022
  );
2868
3023
  }
2869
- return /* @__PURE__ */ jsx18("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, htmlPolicy), `${key}h`) }, key);
3024
+ return /* @__PURE__ */ jsx18("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
2870
3025
  case "math":
2871
3026
  return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx18("code", { children: node.value }) }, key);
2872
3027
  case "definition":
@@ -2874,7 +3029,7 @@ function renderBlock(node, key, htmlPolicy = "sanitize") {
2874
3029
  case "footnoteDefinition":
2875
3030
  return /* @__PURE__ */ jsxs11("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
2876
3031
  /* @__PURE__ */ jsx18("sup", { children: node.label ?? node.identifier }),
2877
- renderBlocks(node.children, key, htmlPolicy)
3032
+ renderBlocks(node.children, key, ctx)
2878
3033
  ] }, key);
2879
3034
  case "containerDirective":
2880
3035
  return /* @__PURE__ */ jsxs11(
@@ -2884,7 +3039,7 @@ function renderBlock(node, key, htmlPolicy = "sanitize") {
2884
3039
  "data-directive": node.name,
2885
3040
  children: [
2886
3041
  node.label && /* @__PURE__ */ jsx18("div", { className: "squisq-md-directive-label", children: node.label }),
2887
- renderBlocks(node.children, key, htmlPolicy)
3042
+ renderBlocks(node.children, key, ctx)
2888
3043
  ]
2889
3044
  },
2890
3045
  key
@@ -2895,29 +3050,29 @@ function renderBlock(node, key, htmlPolicy = "sanitize") {
2895
3050
  {
2896
3051
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
2897
3052
  "data-directive": node.name,
2898
- children: renderInline(node.children, key, htmlPolicy)
3053
+ children: renderInline(node.children, key, ctx)
2899
3054
  },
2900
3055
  key
2901
3056
  );
2902
3057
  case "definitionList":
2903
3058
  return /* @__PURE__ */ jsx18("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
2904
3059
  if (child.type === "definitionTerm") {
2905
- return /* @__PURE__ */ jsx18("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, htmlPolicy) }, `${key}dt${i}`);
3060
+ return /* @__PURE__ */ jsx18("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
2906
3061
  }
2907
- return /* @__PURE__ */ jsx18("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, htmlPolicy) }, `${key}dd${i}`);
3062
+ return /* @__PURE__ */ jsx18("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
2908
3063
  }) }, key);
2909
3064
  default:
2910
3065
  return null;
2911
3066
  }
2912
3067
  }
2913
- function renderListItem(item, key, htmlPolicy = "sanitize") {
3068
+ function renderListItem(item, key, ctx = DEFAULT_CTX) {
2914
3069
  const isTask = item.checked !== null && item.checked !== void 0;
2915
3070
  return /* @__PURE__ */ jsxs11("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
2916
3071
  isTask && /* @__PURE__ */ jsx18("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
2917
- renderBlocks(item.children, key, htmlPolicy)
3072
+ renderBlocks(item.children, key, ctx)
2918
3073
  ] }, key);
2919
3074
  }
2920
- function renderTable(rows, align, key, htmlPolicy = "sanitize") {
3075
+ function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
2921
3076
  const [headerRow, ...bodyRows] = rows;
2922
3077
  return /* @__PURE__ */ jsxs11("table", { className: "squisq-md-table", children: [
2923
3078
  headerRow && /* @__PURE__ */ jsx18("thead", { children: /* @__PURE__ */ jsx18("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx18(
@@ -2925,7 +3080,7 @@ function renderTable(rows, align, key, htmlPolicy = "sanitize") {
2925
3080
  {
2926
3081
  className: "squisq-md-th",
2927
3082
  style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2928
- children: renderInline(cell.children, `${key}th${ci}`, htmlPolicy)
3083
+ children: renderInline(cell.children, `${key}th${ci}`, ctx)
2929
3084
  },
2930
3085
  `${key}th${ci}`
2931
3086
  )) }) }),
@@ -2934,14 +3089,14 @@ function renderTable(rows, align, key, htmlPolicy = "sanitize") {
2934
3089
  {
2935
3090
  className: "squisq-md-td",
2936
3091
  style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2937
- children: renderInline(cell.children, `${key}td${ri}-${ci}`, htmlPolicy)
3092
+ children: renderInline(cell.children, `${key}td${ri}-${ci}`, ctx)
2938
3093
  },
2939
3094
  `${key}td${ri}-${ci}`
2940
3095
  )) }, `${key}tr${ri}`)) })
2941
3096
  ] }, key);
2942
3097
  }
2943
- function renderBlocks(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2944
- return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`, htmlPolicy));
3098
+ function renderBlocks(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
3099
+ return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`, ctx));
2945
3100
  }
2946
3101
  function MdImage({ src, alt, title }) {
2947
3102
  const safeSrc = sanitizeUrl(src, "media");
@@ -3070,18 +3225,29 @@ function renderHtmlNodes(nodes, keyPrefix) {
3070
3225
  function MarkdownRenderer({
3071
3226
  nodes,
3072
3227
  className,
3073
- htmlPolicy = "sanitize"
3228
+ htmlPolicy = "sanitize",
3229
+ linkSchemes
3074
3230
  }) {
3075
3231
  if (!nodes || nodes.length === 0) return null;
3076
- return /* @__PURE__ */ jsx18("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", htmlPolicy) });
3232
+ return /* @__PURE__ */ jsx18("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes }) });
3077
3233
  }
3078
3234
 
3079
3235
  // src/LinearDocView.tsx
3080
3236
  import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3237
+ var warnedUnknownTemplates = /* @__PURE__ */ new Set();
3081
3238
  function isAnnotatedBlock(block) {
3082
3239
  const annotation = block.sourceHeading?.templateAnnotation;
3083
- if (!annotation) return false;
3084
- return !!annotation.template && hasTemplate(annotation.template);
3240
+ if (!annotation?.template) return false;
3241
+ if (!hasTemplate(annotation.template)) {
3242
+ if (!warnedUnknownTemplates.has(annotation.template)) {
3243
+ warnedUnknownTemplates.add(annotation.template);
3244
+ console.warn(
3245
+ `[squisq] Unknown template "${annotation.template}" \u2014 rendering the block as plain markdown.`
3246
+ );
3247
+ }
3248
+ return false;
3249
+ }
3250
+ return true;
3085
3251
  }
3086
3252
  function countAll(blocks) {
3087
3253
  let count = 0;
@@ -3173,6 +3339,7 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
3173
3339
  }
3174
3340
  function LinearDocView({
3175
3341
  doc,
3342
+ markdown,
3176
3343
  basePath = "/",
3177
3344
  viewport,
3178
3345
  className,
@@ -3182,7 +3349,15 @@ function LinearDocView({
3182
3349
  imageDisplayMode = "inline"
3183
3350
  }) {
3184
3351
  const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
3185
- const totalBlocks = useMemo8(() => countAll(doc.blocks), [doc.blocks]);
3352
+ const markdownDoc = useMemo8(
3353
+ () => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
3354
+ [doc, markdown]
3355
+ );
3356
+ const resolvedDoc = doc ?? markdownDoc;
3357
+ const totalBlocks = useMemo8(
3358
+ () => resolvedDoc ? countAll(resolvedDoc.blocks) : 0,
3359
+ [resolvedDoc]
3360
+ );
3186
3361
  const autoSurface = useAutoSurface(surface === "auto");
3187
3362
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
3188
3363
  const renderContext = useMemo8(() => {
@@ -3198,6 +3373,9 @@ function LinearDocView({
3198
3373
  };
3199
3374
  }, [activeViewport, totalBlocks, theme, resolvedSurface]);
3200
3375
  const activeTheme = renderContext.theme;
3376
+ if (!resolvedDoc) {
3377
+ return /* @__PURE__ */ jsx19("div", { className: `squisq-linear squisq-linear--empty ${className || ""}` });
3378
+ }
3201
3379
  const bgColor = activeTheme.colors.background;
3202
3380
  const textColor = activeTheme.colors.text;
3203
3381
  const mutedColor = activeTheme.colors.textMuted;
@@ -3358,7 +3536,7 @@ function LinearDocView({
3358
3536
  background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
3359
3537
  }
3360
3538
  ` }),
3361
- doc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
3539
+ resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
3362
3540
  BlockSection,
3363
3541
  {
3364
3542
  block,
@@ -3395,9 +3573,9 @@ var SMALL_WORDS = /* @__PURE__ */ new Set([
3395
3573
  "by",
3396
3574
  "is"
3397
3575
  ]);
3398
- function buildSegmentTitleMap(script) {
3576
+ function buildSegmentTitleMap(doc) {
3399
3577
  const map = /* @__PURE__ */ new Map();
3400
- for (const block of script.blocks) {
3578
+ for (const block of doc.blocks) {
3401
3579
  if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
3402
3580
  const segIdx = block.audioSegment;
3403
3581
  if (!map.has(segIdx)) {
@@ -3405,9 +3583,9 @@ function buildSegmentTitleMap(script) {
3405
3583
  }
3406
3584
  }
3407
3585
  }
3408
- for (let i = 0; i < script.audio.segments.length; i++) {
3586
+ for (let i = 0; i < doc.audio.segments.length; i++) {
3409
3587
  if (!map.has(i)) {
3410
- const name = script.audio.segments[i].name;
3588
+ const name = doc.audio.segments[i].name;
3411
3589
  if (name === "intro" || name.includes("intro")) {
3412
3590
  map.set(i, "Introduction");
3413
3591
  } else if (name === "flight-context" || name.includes("flight-context")) {
@@ -3423,14 +3601,34 @@ function buildSegmentTitleMap(script) {
3423
3601
  }
3424
3602
  return map;
3425
3603
  }
3426
- function DocPlayer({
3427
- script,
3428
- basePath,
3604
+ function isDevEnvironment() {
3605
+ try {
3606
+ return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
3607
+ } catch {
3608
+ return false;
3609
+ }
3610
+ }
3611
+ var warnedMissingStyles = false;
3612
+ function DocPlayer(props) {
3613
+ const { doc, markdown } = props;
3614
+ const markdownDoc = useMemo9(
3615
+ () => !doc && markdown !== void 0 ? markdownToDoc2(parseMarkdown2(markdown)) : void 0,
3616
+ [doc, markdown]
3617
+ );
3618
+ const resolvedDoc = doc ?? markdownDoc;
3619
+ if (!resolvedDoc) {
3620
+ return /* @__PURE__ */ jsx20("div", { className: "doc-player doc-player--empty" });
3621
+ }
3622
+ return /* @__PURE__ */ jsx20(DocPlayerContent, { ...props, doc: resolvedDoc });
3623
+ }
3624
+ function DocPlayerContent({
3625
+ doc,
3626
+ basePath = ".",
3429
3627
  renderMode = false,
3430
3628
  autoPlay = false,
3431
3629
  onEnded,
3432
3630
  onTimeUpdate,
3433
- audioProvider: externalAudioProvider,
3631
+ audioController: externalAudioController,
3434
3632
  showControls = true,
3435
3633
  showScrubber = false,
3436
3634
  muted = false,
@@ -3445,14 +3643,15 @@ function DocPlayer({
3445
3643
  displayMode = "video",
3446
3644
  theme,
3447
3645
  surface,
3448
- captionStyle = "standard"
3646
+ captionStyle = "standard",
3647
+ enableSwipe = true
3449
3648
  }) {
3450
3649
  const isSlideshowMode = displayMode === "slideshow";
3451
3650
  const isLinearMode = displayMode === "linear";
3452
- const audioRef = useRef6(null);
3453
- const containerRef = useRef6(null);
3454
- const [tapFeedback, setTapFeedback] = useState6(null);
3455
- const tapFeedbackTimer = useRef6();
3651
+ const audioRef = useRef7(null);
3652
+ const containerRef = useRef7(null);
3653
+ const [tapFeedback, setTapFeedback] = useState7(null);
3654
+ const tapFeedbackTimer = useRef7();
3456
3655
  const { viewport, orientation } = useViewportOrientation();
3457
3656
  const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
3458
3657
  const isDebugMode = useMemo9(() => {
@@ -3460,8 +3659,20 @@ function DocPlayer({
3460
3659
  const params = new URLSearchParams(window.location.search);
3461
3660
  return params.get("debug") === "true";
3462
3661
  }, []);
3463
- const internalAudio = useAudioSync(audioRef, script.audio, basePath);
3464
- const audio = externalAudioProvider || internalAudio;
3662
+ const internalAudio = useAudioSync(audioRef, doc.audio, basePath);
3663
+ const audio = externalAudioController || internalAudio;
3664
+ useEffect8(() => {
3665
+ if (warnedMissingStyles || !isDevEnvironment()) return;
3666
+ const el = containerRef.current;
3667
+ if (!el || typeof getComputedStyle !== "function") return;
3668
+ const value = getComputedStyle(el).getPropertyValue("--squisq-styles-loaded");
3669
+ if (!value.trim()) {
3670
+ warnedMissingStyles = true;
3671
+ console.warn(
3672
+ '[squisq] @bendyline/squisq-react/styles is not loaded \u2014 import "@bendyline/squisq-react/styles"'
3673
+ );
3674
+ }
3675
+ }, []);
3465
3676
  const {
3466
3677
  currentTime,
3467
3678
  isPlaying,
@@ -3478,13 +3689,13 @@ function DocPlayer({
3478
3689
  skipToSegment: _skipToSegment,
3479
3690
  restart
3480
3691
  } = audio;
3481
- const mediaSchedule = useMemo9(() => resolveMediaSchedule(script), [script]);
3482
- const currentTimeRef = useRef6(currentTime);
3692
+ const mediaSchedule = useMemo9(() => resolveMediaSchedule(doc), [doc]);
3693
+ const currentTimeRef = useRef7(currentTime);
3483
3694
  currentTimeRef.current = currentTime;
3484
- const totalDurationRef = useRef6(totalDuration);
3695
+ const totalDurationRef = useRef7(totalDuration);
3485
3696
  totalDurationRef.current = totalDuration;
3486
- const expandedBlocksLenRef = useRef6(0);
3487
- const handleContainerClick = useCallback5(
3697
+ const expandedBlocksLenRef = useRef7(0);
3698
+ const handleContainerClick = useCallback6(
3488
3699
  (e) => {
3489
3700
  if (renderMode || isSlideshowMode || isLinearMode) return;
3490
3701
  const target = e.target;
@@ -3518,9 +3729,9 @@ function DocPlayer({
3518
3729
  nextBlock: _nextBlock,
3519
3730
  prevBlock: _prevBlock,
3520
3731
  blocks: expandedBlocks
3521
- } = useDocPlayback(script, currentTime, activeViewport, renderMode, effectiveTheme);
3732
+ } = useDocPlayback(doc, currentTime, activeViewport, renderMode, effectiveTheme);
3522
3733
  const coverBlock = useMemo9(() => {
3523
- const startBlockConfig = script.startBlock;
3734
+ const startBlockConfig = doc.startBlock;
3524
3735
  if (!startBlockConfig) return null;
3525
3736
  const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
3526
3737
  const layers = expandCoverBlock(startBlockConfig, context);
@@ -3533,15 +3744,15 @@ function DocPlayer({
3533
3744
  audioSegment: -1,
3534
3745
  layers
3535
3746
  };
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);
3747
+ }, [doc.startBlock, activeViewport, effectiveTheme]);
3748
+ const [coverForced, setCoverForced] = useState7(false);
3749
+ const [coverGraceActive, setCoverGraceActive] = useState7(false);
3750
+ const coverGraceTimer = useRef7();
3751
+ const coverWasShowing = useRef7(false);
3752
+ const hasPlayedOnce = useRef7(false);
3542
3753
  const atRest = !!(coverBlock && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3543
3754
  if (atRest) coverWasShowing.current = true;
3544
- useEffect7(() => {
3755
+ useEffect8(() => {
3545
3756
  if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
3546
3757
  coverWasShowing.current = false;
3547
3758
  hasPlayedOnce.current = true;
@@ -3549,24 +3760,24 @@ function DocPlayer({
3549
3760
  coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
3550
3761
  }
3551
3762
  }, [isPlaying, coverBlock, renderMode]);
3552
- useEffect7(() => () => clearTimeout(coverGraceTimer.current), []);
3763
+ useEffect8(() => () => clearTimeout(coverGraceTimer.current), []);
3553
3764
  const showCoverBlock = !isSlideshowMode && !isLinearMode && coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3554
- const hasAutoPlayed = useRef6(false);
3555
- useEffect7(() => {
3765
+ const hasAutoPlayed = useRef7(false);
3766
+ useEffect8(() => {
3556
3767
  if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
3557
3768
  hasAutoPlayed.current = true;
3558
3769
  play();
3559
3770
  }
3560
3771
  }, [isAudioReady, autoPlay, play]);
3561
- useEffect7(() => {
3772
+ useEffect8(() => {
3562
3773
  onTimeUpdate?.(currentTime);
3563
3774
  }, [currentTime, onTimeUpdate]);
3564
- useEffect7(() => {
3775
+ useEffect8(() => {
3565
3776
  if (isEnded) {
3566
3777
  onEnded?.();
3567
3778
  }
3568
3779
  }, [isEnded, onEnded]);
3569
- useEffect7(() => {
3780
+ useEffect8(() => {
3570
3781
  if ((renderMode || isDebugMode) && typeof window !== "undefined") {
3571
3782
  const w = window;
3572
3783
  w.seekTo = (time) => {
@@ -3645,7 +3856,7 @@ function DocPlayer({
3645
3856
  });
3646
3857
  };
3647
3858
  w.getDuration = () => {
3648
- const mediaDuration = getDocPlaybackDuration(script);
3859
+ const mediaDuration = getDocPlaybackDuration(doc);
3649
3860
  if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
3650
3861
  return mediaDuration;
3651
3862
  };
@@ -3655,20 +3866,20 @@ function DocPlayer({
3655
3866
  startTime: s.startTime,
3656
3867
  duration: s.duration
3657
3868
  }));
3658
- w.getAudioSegments = () => script.audio.segments.map((seg) => ({
3869
+ w.getAudioSegments = () => doc.audio.segments.map((seg) => ({
3659
3870
  src: seg.src,
3660
3871
  name: seg.name,
3661
3872
  duration: seg.duration,
3662
3873
  startTime: seg.startTime
3663
3874
  }));
3664
- w.getCaptions = () => script.captions?.phrases?.map((p) => ({
3875
+ w.getCaptions = () => doc.captions?.phrases?.map((p) => ({
3665
3876
  text: p.text,
3666
3877
  startTime: p.startTime,
3667
3878
  endTime: p.endTime
3668
3879
  })) || [];
3669
3880
  w.getChapters = () => {
3670
- const titleMap = buildSegmentTitleMap(script);
3671
- return script.audio.segments.map((seg, i) => ({
3881
+ const titleMap = buildSegmentTitleMap(doc);
3882
+ return doc.audio.segments.map((seg, i) => ({
3672
3883
  title: titleMap.get(i) || seg.name,
3673
3884
  startTime: seg.startTime,
3674
3885
  duration: seg.duration
@@ -3700,25 +3911,28 @@ function DocPlayer({
3700
3911
  };
3701
3912
  }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
3702
3913
  const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
3703
- const [captionMode, setCaptionMode] = useState6(defaultMode);
3914
+ const [captionMode, setCaptionMode] = useState7(defaultMode);
3915
+ useEffect8(() => {
3916
+ setCaptionMode(defaultMode);
3917
+ }, [defaultMode]);
3704
3918
  const captionsEnabled = captionMode !== "off";
3705
3919
  const activeCaptionStyle = captionMode === "social" ? "social" : "standard";
3706
- const setCaptionsEnabled = useCallback5(
3920
+ const setCaptionsEnabled = useCallback6(
3707
3921
  (enabled) => {
3708
3922
  setCaptionMode(enabled ? captionStyle || "standard" : "off");
3709
3923
  onCaptionsToggle?.(enabled);
3710
3924
  },
3711
3925
  [onCaptionsToggle, captionStyle]
3712
3926
  );
3713
- const cycleCaptionMode = useCallback5(() => {
3927
+ const cycleCaptionMode = useCallback6(() => {
3714
3928
  setCaptionMode((prev) => {
3715
3929
  const next = prev === "off" ? "standard" : prev === "standard" ? "social" : "off";
3716
3930
  onCaptionsToggle?.(next !== "off");
3717
3931
  return next;
3718
3932
  });
3719
3933
  }, [onCaptionsToggle]);
3720
- const hasCaptions = script.captions && script.captions.phrases.length > 0;
3721
- const segmentTitleMap = useMemo9(() => buildSegmentTitleMap(script), [script]);
3934
+ const hasCaptions = doc.captions && doc.captions.phrases.length > 0;
3935
+ const segmentTitleMap = useMemo9(() => buildSegmentTitleMap(doc), [doc]);
3722
3936
  const playbackState = useMemo9(
3723
3937
  () => ({
3724
3938
  isPlaying,
@@ -3732,10 +3946,10 @@ function DocPlayer({
3732
3946
  captionMode,
3733
3947
  isFullscreen,
3734
3948
  currentSegmentIndex: currentSegment,
3735
- currentSegmentName: segmentTitleMap.get(currentSegment) ?? script.audio.segments[currentSegment]?.name ?? null,
3949
+ currentSegmentName: segmentTitleMap.get(currentSegment) ?? doc.audio.segments[currentSegment]?.name ?? null,
3736
3950
  currentBlock: currentBlock ?? null
3737
3951
  }),
3738
- // eslint-disable-next-line react-hooks/exhaustive-deps -- script.audio.segments is stable within a given script
3952
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- doc.audio.segments is stable within a given doc
3739
3953
  [
3740
3954
  isPlaying,
3741
3955
  currentTime,
@@ -3795,13 +4009,22 @@ function DocPlayer({
3795
4009
  }),
3796
4010
  [currentBlockIndex, expandedBlocks, seekTo, pause]
3797
4011
  );
3798
- useEffect7(() => {
4012
+ const swipeEnabled = isSlideshowMode && !isLinearMode && !renderMode && enableSwipe;
4013
+ const swipe = useSlideSwipe({
4014
+ enabled: swipeEnabled,
4015
+ containerRef,
4016
+ canGoNext: currentBlockIndex < expandedBlocks.length - 1,
4017
+ canGoPrev: currentBlockIndex > 0,
4018
+ onNext: slideNavActions.nextSlide,
4019
+ onPrev: slideNavActions.prevSlide
4020
+ });
4021
+ useEffect8(() => {
3799
4022
  onPlaybackStateChange?.(playbackState);
3800
4023
  }, [playbackState, onPlaybackStateChange]);
3801
- useEffect7(() => {
4024
+ useEffect8(() => {
3802
4025
  onControlsReady?.({ play, pause, ...playbackActions });
3803
4026
  }, [play, pause, playbackActions, onControlsReady]);
3804
- const getBlockTitle = useCallback5((block) => {
4027
+ const getBlockTitle = useCallback6((block) => {
3805
4028
  const docBlock = block;
3806
4029
  if (isTemplateBlock2(docBlock)) {
3807
4030
  const props = docBlock;
@@ -3840,13 +4063,13 @@ function DocPlayer({
3840
4063
  };
3841
4064
  });
3842
4065
  }, [expandedBlocks, totalDuration, getBlockTitle]);
3843
- useEffect7(() => {
4066
+ useEffect8(() => {
3844
4067
  if (blockMarkers.length > 0) {
3845
4068
  onBlockMarkers?.(blockMarkers);
3846
4069
  }
3847
4070
  }, [blockMarkers, onBlockMarkers]);
3848
4071
  expandedBlocksLenRef.current = expandedBlocks.length;
3849
- const handleKeyDown = useCallback5(
4072
+ const handleKeyDown = useCallback6(
3850
4073
  (e) => {
3851
4074
  const activeEl = document.activeElement;
3852
4075
  if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.tagName === "SELECT")) {
@@ -3892,7 +4115,7 @@ function DocPlayer({
3892
4115
  },
3893
4116
  [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
3894
4117
  );
3895
- useEffect7(() => {
4118
+ useEffect8(() => {
3896
4119
  if (renderMode) return;
3897
4120
  window.addEventListener("keydown", handleKeyDown);
3898
4121
  return () => window.removeEventListener("keydown", handleKeyDown);
@@ -3912,7 +4135,7 @@ function DocPlayer({
3912
4135
  children: /* @__PURE__ */ jsx20(
3913
4136
  LinearDocView,
3914
4137
  {
3915
- doc: script,
4138
+ doc,
3916
4139
  basePath,
3917
4140
  viewport: activeViewport,
3918
4141
  theme,
@@ -3926,15 +4149,19 @@ function DocPlayer({
3926
4149
  "div",
3927
4150
  {
3928
4151
  ref: containerRef,
3929
- className: "doc-player",
4152
+ className: `doc-player${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
3930
4153
  onClick: handleContainerClick,
4154
+ onPointerDown: swipe.onPointerDown,
3931
4155
  style: {
3932
4156
  position: "relative",
3933
4157
  width: "100%",
3934
4158
  aspectRatio: `${activeViewport.width} / ${activeViewport.height}`,
3935
4159
  margin: "0 auto",
3936
4160
  overflow: "hidden",
3937
- cursor: renderMode ? void 0 : "pointer"
4161
+ // Swipe uses the grab/grabbing cursor via CSS classes; let vertical page
4162
+ // scroll through on touch while we own horizontal drags.
4163
+ cursor: renderMode || swipeEnabled ? void 0 : "pointer",
4164
+ touchAction: swipeEnabled ? "pan-y" : void 0
3938
4165
  },
3939
4166
  children: [
3940
4167
  /* @__PURE__ */ jsx20("audio", { ref: audioRef, preload: "auto", muted }),
@@ -3974,21 +4201,29 @@ function DocPlayer({
3974
4201
  viewport: activeViewport
3975
4202
  }
3976
4203
  ) }, previousBlock.id),
3977
- !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--active", children: /* @__PURE__ */ jsx20(
3978
- BlockRenderer,
4204
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20(
4205
+ "div",
3979
4206
  {
3980
- block: currentBlock,
3981
- blockTime,
3982
- basePath,
3983
- isEntering,
3984
- viewport: activeViewport,
3985
- isPlaying
3986
- }
3987
- ) }, currentBlock.id),
4207
+ className: `doc-player__block doc-player__block--active${swipe.phase !== "idle" ? ` doc-player__block--${swipe.phase}` : ""}`,
4208
+ style: swipe.phase !== "idle" ? { transform: `translateX(${swipe.offsetPx}px)` } : void 0,
4209
+ children: /* @__PURE__ */ jsx20(
4210
+ BlockRenderer,
4211
+ {
4212
+ block: currentBlock,
4213
+ blockTime,
4214
+ basePath,
4215
+ isEntering,
4216
+ viewport: activeViewport,
4217
+ isPlaying
4218
+ }
4219
+ )
4220
+ },
4221
+ currentBlock.id
4222
+ ),
3988
4223
  hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx20(
3989
4224
  CaptionOverlay,
3990
4225
  {
3991
- captions: script.captions,
4226
+ captions: doc.captions,
3992
4227
  currentTime,
3993
4228
  enabled: captionsEnabled && (renderMode || isPlaying || currentTime > 0),
3994
4229
  fontSize: 16,
@@ -4049,9 +4284,8 @@ function DocPlayer({
4049
4284
  /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4050
4285
  "(progress: ",
4051
4286
  (docProgress * 100).toFixed(1),
4052
- "%, scriptDur:",
4053
- " ",
4054
- script.duration.toFixed(1),
4287
+ "%, scriptDur: ",
4288
+ doc.duration.toFixed(1),
4055
4289
  ")"
4056
4290
  ] })
4057
4291
  ] }),
@@ -4069,11 +4303,11 @@ function DocPlayer({
4069
4303
  " ",
4070
4304
  currentSegment,
4071
4305
  "/",
4072
- script.audio.segments.length - 1,
4306
+ doc.audio.segments.length - 1,
4073
4307
  " ",
4074
4308
  /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4075
4309
  "(",
4076
- script.audio.segments[currentSegment]?.name || "none",
4310
+ doc.audio.segments[currentSegment]?.name || "none",
4077
4311
  ")"
4078
4312
  ] })
4079
4313
  ] }),
@@ -4095,13 +4329,13 @@ function DocPlayer({
4095
4329
  showCoverBlock && /* @__PURE__ */ jsx20("span", { style: { color: "#60a5fa" }, children: " (cover)" })
4096
4330
  ] }),
4097
4331
  hasCaptions && (() => {
4098
- const debugPhrase = getCaptionAtTime2(script.captions, currentTime);
4332
+ const debugPhrase = getCaptionAtTime2(doc.captions, currentTime);
4099
4333
  const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
4100
4334
  return /* @__PURE__ */ jsxs13(Fragment3, { children: [
4101
4335
  /* @__PURE__ */ jsxs13("div", { children: [
4102
4336
  /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "captions:" }),
4103
4337
  " ",
4104
- script.captions?.phrases.length || 0,
4338
+ doc.captions?.phrases.length || 0,
4105
4339
  " phrases",
4106
4340
  " ",
4107
4341
  /* @__PURE__ */ jsxs13("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
@@ -4331,7 +4565,7 @@ function DocControlsSidebar({ state, actions }) {
4331
4565
  }
4332
4566
 
4333
4567
  // src/DocPlayerWithSidebar.tsx
4334
- import { useRef as useRef7, useState as useState7, useCallback as useCallback6, useEffect as useEffect8 } from "react";
4568
+ import { useRef as useRef8, useState as useState8, useCallback as useCallback7, useEffect as useEffect9 } from "react";
4335
4569
  import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
4336
4570
  var DEFAULT_STATE = {
4337
4571
  isPlaying: false,
@@ -4348,24 +4582,25 @@ var DEFAULT_STATE = {
4348
4582
  currentBlock: null
4349
4583
  };
4350
4584
  function DocPlayerWithSidebar({
4351
- script,
4585
+ doc,
4352
4586
  basePath,
4353
4587
  autoPlay = false,
4354
4588
  onEnded,
4355
4589
  onTimeUpdate,
4356
- audioProvider,
4590
+ audioController,
4357
4591
  muted,
4358
4592
  captionsEnabled,
4359
4593
  isFullscreen,
4360
4594
  onFullscreenToggle,
4361
4595
  forceViewport,
4362
- onPlayingChange
4596
+ onPlayingChange,
4597
+ theme
4363
4598
  }) {
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(
4599
+ const stateRef = useRef8(DEFAULT_STATE);
4600
+ const actionsRef = useRef8(null);
4601
+ const wasPlayingRef = useRef8(false);
4602
+ const [, setTick] = useState8(0);
4603
+ const handleStateChange = useCallback7(
4369
4604
  (state) => {
4370
4605
  stateRef.current = state;
4371
4606
  if (onPlayingChange && state.isPlaying !== wasPlayingRef.current) {
@@ -4375,7 +4610,7 @@ function DocPlayerWithSidebar({
4375
4610
  },
4376
4611
  [onPlayingChange]
4377
4612
  );
4378
- const handleControlsReady = useCallback6(
4613
+ const handleControlsReady = useCallback7(
4379
4614
  (controls) => {
4380
4615
  const isFirst = !actionsRef.current;
4381
4616
  actionsRef.current = controls;
@@ -4383,7 +4618,7 @@ function DocPlayerWithSidebar({
4383
4618
  },
4384
4619
  []
4385
4620
  );
4386
- useEffect8(() => {
4621
+ useEffect9(() => {
4387
4622
  const interval = setInterval(() => {
4388
4623
  setTick((t) => t + 1);
4389
4624
  }, 250);
@@ -4393,12 +4628,13 @@ function DocPlayerWithSidebar({
4393
4628
  /* @__PURE__ */ jsx23("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx23(
4394
4629
  DocPlayer,
4395
4630
  {
4396
- script,
4631
+ doc,
4632
+ theme,
4397
4633
  basePath,
4398
4634
  autoPlay,
4399
4635
  onEnded,
4400
4636
  onTimeUpdate,
4401
- audioProvider,
4637
+ audioController,
4402
4638
  muted,
4403
4639
  captionsEnabled,
4404
4640
  showControls: isFullscreen,
@@ -4416,36 +4652,15 @@ function DocPlayerWithSidebar({
4416
4652
 
4417
4653
  // src/jsonView/useJsonViewTokens.ts
4418
4654
  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";
4655
+ import { buildJsonFormTokens, resolveJsonFormTheme } from "@bendyline/squisq/jsonForm";
4424
4656
  function useJsonViewTokens(theme, surface) {
4425
4657
  const auto = useAutoSurface(surface === "auto");
4426
4658
  const effectiveSurface = surface === "auto" ? auto : surface ?? void 0;
4427
4659
  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 };
4660
+ const style = buildJsonFormTokens(theme, effectiveSurface, {
4661
+ prefix: "--squisq-json"
4662
+ });
4663
+ return { style, theme: resolveJsonFormTheme(theme, effectiveSurface) };
4449
4664
  }, [theme, effectiveSurface]);
4450
4665
  }
4451
4666
 
@@ -4461,7 +4676,7 @@ import { Fragment as Fragment4, useMemo as useMemo11 } from "react";
4461
4676
  import {
4462
4677
  arrayItemKind
4463
4678
  } from "@bendyline/squisq/jsonForm";
4464
- import { parseMarkdown } from "@bendyline/squisq/markdown";
4679
+ import { parseMarkdown as parseMarkdown3 } from "@bendyline/squisq/markdown";
4465
4680
  import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
4466
4681
  function TextViewer({ value }) {
4467
4682
  if (value === void 0 || value === null || value === "") {
@@ -4479,7 +4694,7 @@ function RichTextViewer({ value }) {
4479
4694
  const nodes = useMemo11(() => {
4480
4695
  if (typeof value !== "string" || value === "") return null;
4481
4696
  try {
4482
- const doc = parseMarkdown(value);
4697
+ const doc = parseMarkdown3(value);
4483
4698
  return doc.children;
4484
4699
  } catch {
4485
4700
  return null;