@bendyline/squisq-editor-react 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import {
5
5
  NarrationStage,
6
6
  buildFilename,
7
7
  buildTimingJson,
8
+ encodeNarrationTimingJson,
8
9
  encodeTimingJson,
9
10
  requestCameraStream,
10
11
  requestMicStream,
@@ -16,7 +17,7 @@ import {
16
17
  timingPathFor,
17
18
  useNarrationStage,
18
19
  useStreamPreview
19
- } from "./chunk-OWOKKZAU.js";
20
+ } from "./chunk-IRPOMTYK.js";
20
21
 
21
22
  // src/recorder/sources/screenStream.ts
22
23
  function mixAudioTracks(streams) {
@@ -634,6 +635,11 @@ function useMediaRecorder(options = {}) {
634
635
  }
635
636
  }, 100);
636
637
  }, [clearTicker, deactivateCapture, transition]);
638
+ const getElapsedMs = useCallback(() => {
639
+ if (stateRef.current !== "recording") return null;
640
+ const startedAt = startTimestampRef.current;
641
+ return startedAt === null ? null : Math.max(0, Date.now() - startedAt);
642
+ }, []);
637
643
  const stop = useCallback(() => {
638
644
  if (stopPromiseRef.current) return stopPromiseRef.current;
639
645
  const rec = recorderRef.current;
@@ -707,6 +713,7 @@ function useMediaRecorder(options = {}) {
707
713
  extension: format?.extension ?? null,
708
714
  directory: format?.directory ?? null,
709
715
  durationMs,
716
+ getElapsedMs,
710
717
  error,
711
718
  camera,
712
719
  cameraOffsetSec,
@@ -1652,16 +1659,371 @@ function recordedMediaKind(source, stream, mimeType) {
1652
1659
  return source === "mic" ? "audio" : "video";
1653
1660
  }
1654
1661
 
1662
+ // src/recorder/slidesModePolicy.ts
1663
+ function isExpandedPanel(mode) {
1664
+ return mode !== "none";
1665
+ }
1666
+ function panelModeAfterToggle(current, target, checked) {
1667
+ if (checked) return target;
1668
+ return current === target ? "none" : current;
1669
+ }
1670
+ function clampSlideIndex(index, count) {
1671
+ if (count <= 0) return 0;
1672
+ if (!Number.isFinite(index)) return 0;
1673
+ return Math.min(count - 1, Math.max(0, Math.floor(index)));
1674
+ }
1675
+ function slideStepForKey(key) {
1676
+ switch (key) {
1677
+ case "ArrowRight":
1678
+ case "ArrowDown":
1679
+ case "PageDown":
1680
+ case " ":
1681
+ case "Spacebar":
1682
+ return 1;
1683
+ case "ArrowLeft":
1684
+ case "ArrowUp":
1685
+ case "PageUp":
1686
+ return -1;
1687
+ default:
1688
+ return 0;
1689
+ }
1690
+ }
1691
+ function showSlideTimingCheckbox(options) {
1692
+ return options.slidesOn && options.captureTimings && options.recorderState === "stopped" && options.hasBlob;
1693
+ }
1694
+ function unshownSlidesWarning(total, unshown) {
1695
+ if (unshown <= 0 || total <= 0) return null;
1696
+ const noun = unshown === 1 ? "slide was" : "slides were";
1697
+ return `${unshown} of ${total} ${noun} never shown \u2014 they'll be skipped during playback.`;
1698
+ }
1699
+ var SLIDE_TIMING_CHECKBOX_LABEL = "Update block timings when I save this narration";
1700
+
1701
+ // src/buildPreviewDoc.ts
1702
+ import {
1703
+ buildPreviewDoc,
1704
+ documentTitleFromFileName
1705
+ } from "@bendyline/squisq/doc";
1706
+
1707
+ // src/recorder/slides/slideDeck.ts
1708
+ import {
1709
+ buildPreviewDoc as buildPreviewDoc2,
1710
+ flattenRenderableBlocks,
1711
+ getBlockBodyText,
1712
+ materializeBlockLayers as materializeBlockLayers2
1713
+ } from "@bendyline/squisq/doc";
1714
+
1715
+ // src/resolveBlockVisual.ts
1716
+ import { materializeBlockLayers } from "@bendyline/squisq/doc";
1717
+ function resolveBlockVisual(doc, block, theme, viewport) {
1718
+ try {
1719
+ const slide = buildPreviewDoc({ ...doc, blocks: [block] }).blocks[0];
1720
+ if (!slide) return null;
1721
+ const ctx = {
1722
+ blockIndex: 0,
1723
+ totalBlocks: 1,
1724
+ theme,
1725
+ viewport,
1726
+ customTemplates: doc.customTemplates
1727
+ };
1728
+ const { layers } = materializeBlockLayers(slide, ctx);
1729
+ return { ...slide, layers };
1730
+ } catch {
1731
+ return null;
1732
+ }
1733
+ }
1734
+
1735
+ // src/recorder/slides/slideDeck.ts
1736
+ function buildRecorderSlideDeck(doc, theme, viewport) {
1737
+ const flat = flattenRenderableBlocks(doc.blocks);
1738
+ if (flat.length === 0) return [];
1739
+ let slidesById;
1740
+ try {
1741
+ const preview = buildPreviewDoc2(doc, { interleaveImages: false });
1742
+ slidesById = new Map(preview.blocks.map((slide) => [slide.id, slide]));
1743
+ } catch {
1744
+ slidesById = /* @__PURE__ */ new Map();
1745
+ }
1746
+ return flat.map((block, index) => {
1747
+ const heading = block.title?.trim() || `Slide ${index + 1}`;
1748
+ let bodyText = "";
1749
+ try {
1750
+ bodyText = getBlockBodyText(block);
1751
+ } catch {
1752
+ bodyText = "";
1753
+ }
1754
+ return {
1755
+ blockId: block.id,
1756
+ heading,
1757
+ bodyText,
1758
+ visual: resolveSlideVisual(
1759
+ doc,
1760
+ block,
1761
+ slidesById.get(block.id),
1762
+ theme,
1763
+ viewport,
1764
+ index,
1765
+ flat.length
1766
+ )
1767
+ };
1768
+ });
1769
+ }
1770
+ function resolveSlideVisual(doc, block, slide, theme, viewport, index, totalBlocks) {
1771
+ if (slide) {
1772
+ try {
1773
+ const { layers } = materializeBlockLayers2(slide, {
1774
+ blockIndex: index,
1775
+ totalBlocks,
1776
+ theme,
1777
+ viewport,
1778
+ customTemplates: doc.customTemplates
1779
+ });
1780
+ return { ...slide, layers };
1781
+ } catch {
1782
+ }
1783
+ }
1784
+ return resolveBlockVisual(doc, block, theme, viewport);
1785
+ }
1786
+
1787
+ // src/recorder/slides/RecorderSlidesPanel.tsx
1788
+ import { useCallback as useCallback3 } from "react";
1789
+
1790
+ // src/TimelineBlockPreview.tsx
1791
+ import { memo } from "react";
1792
+ import { VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
1793
+ import { BlockRenderer, MediaContext } from "@bendyline/squisq-react";
1794
+ import { jsx as jsx3 } from "react/jsx-runtime";
1795
+ var BlockThumbnail = memo(function BlockThumbnail2({
1796
+ visual,
1797
+ viewport = VIEWPORT_PRESETS.landscape,
1798
+ basePath = "/",
1799
+ mediaProvider = null
1800
+ }) {
1801
+ return /* @__PURE__ */ jsx3(MediaContext.Provider, { value: mediaProvider, children: /* @__PURE__ */ jsx3(BlockRenderer, { block: visual, blockTime: 0, basePath, viewport }) });
1802
+ });
1803
+
1804
+ // src/recorder/slides/RecorderSlidesPanel.tsx
1805
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1806
+ var rootStyle = {
1807
+ flex: "1 1 auto",
1808
+ minHeight: 0,
1809
+ minWidth: 0,
1810
+ display: "flex",
1811
+ flexDirection: "column",
1812
+ outline: "none",
1813
+ color: "var(--squisq-recorder-text)"
1814
+ };
1815
+ var headerStyle = {
1816
+ flex: "0 0 auto",
1817
+ display: "flex",
1818
+ alignItems: "center",
1819
+ gap: 12,
1820
+ padding: "8px 10px",
1821
+ borderBottom: "1px solid var(--squisq-recorder-border)"
1822
+ };
1823
+ var counterStyle = {
1824
+ fontSize: 13,
1825
+ fontWeight: 600,
1826
+ whiteSpace: "nowrap"
1827
+ };
1828
+ var dotsStyle = {
1829
+ flex: "1 1 auto",
1830
+ minWidth: 0,
1831
+ display: "flex",
1832
+ flexWrap: "wrap",
1833
+ alignItems: "center",
1834
+ gap: 3
1835
+ };
1836
+ var navButtonStyle = {
1837
+ padding: "4px 10px",
1838
+ fontSize: 13,
1839
+ fontFamily: "inherit",
1840
+ cursor: "pointer",
1841
+ background: "var(--squisq-recorder-input)",
1842
+ color: "var(--squisq-recorder-text)",
1843
+ border: "1px solid var(--squisq-recorder-border)",
1844
+ borderRadius: 0
1845
+ };
1846
+ var stageStyle = {
1847
+ flex: "1 1 auto",
1848
+ minHeight: 0,
1849
+ overflow: "hidden",
1850
+ display: "flex",
1851
+ alignItems: "center",
1852
+ justifyContent: "center",
1853
+ padding: 8,
1854
+ background: "var(--squisq-recorder-surface-muted)"
1855
+ };
1856
+ var notesStyle = {
1857
+ flex: "0 0 auto",
1858
+ maxHeight: "32%",
1859
+ minHeight: 0,
1860
+ overflowY: "auto",
1861
+ padding: "10px 12px",
1862
+ borderTop: "1px solid var(--squisq-recorder-border)",
1863
+ fontSize: 14,
1864
+ lineHeight: 1.5,
1865
+ whiteSpace: "pre-wrap"
1866
+ };
1867
+ var hintStyle = {
1868
+ margin: 0,
1869
+ fontSize: 12,
1870
+ color: "var(--squisq-recorder-muted)"
1871
+ };
1872
+ var headingStyle = {
1873
+ margin: "0 0 6px 0",
1874
+ fontSize: 13,
1875
+ fontWeight: 600
1876
+ };
1877
+ var SELF_HANDLING = /^(BUTTON|INPUT|TEXTAREA|SELECT|A)$/;
1878
+ function RecorderSlidesPanel({
1879
+ slides,
1880
+ index,
1881
+ onIndexChange,
1882
+ viewport,
1883
+ basePath = "/",
1884
+ mediaProvider = null,
1885
+ recording = false,
1886
+ shownBlockIds
1887
+ }) {
1888
+ const count = slides.length;
1889
+ const active = slides[clampSlideIndex(index, count)];
1890
+ const step = useCallback3(
1891
+ (delta) => onIndexChange(clampSlideIndex(index + delta, count)),
1892
+ [index, count, onIndexChange]
1893
+ );
1894
+ const handleKeyDown = useCallback3(
1895
+ (event) => {
1896
+ const delta = slideStepForKey(event.key);
1897
+ if (delta === 0) return;
1898
+ const target = event.target;
1899
+ if (target !== event.currentTarget && SELF_HANDLING.test(target.tagName)) return;
1900
+ event.preventDefault();
1901
+ step(delta);
1902
+ },
1903
+ [step]
1904
+ );
1905
+ if (count === 0 || !active) {
1906
+ return /* @__PURE__ */ jsx4("div", { style: { ...rootStyle, alignItems: "center", justifyContent: "center", padding: 24 }, children: /* @__PURE__ */ jsx4("p", { style: hintStyle, children: "This document has no slides yet." }) });
1907
+ }
1908
+ return /* @__PURE__ */ jsxs3(
1909
+ "div",
1910
+ {
1911
+ style: rootStyle,
1912
+ tabIndex: 0,
1913
+ onKeyDown: handleKeyDown,
1914
+ "data-testid": "recorder-slides-panel",
1915
+ "aria-label": "Slides",
1916
+ children: [
1917
+ /* @__PURE__ */ jsxs3("div", { style: headerStyle, children: [
1918
+ /* @__PURE__ */ jsxs3("span", { style: counterStyle, role: "status", children: [
1919
+ "Slide ",
1920
+ index + 1,
1921
+ " of ",
1922
+ count
1923
+ ] }),
1924
+ /* @__PURE__ */ jsx4("span", { style: dotsStyle, "aria-hidden": "true", children: slides.map((slide, i) => /* @__PURE__ */ jsx4(
1925
+ "span",
1926
+ {
1927
+ style: {
1928
+ width: 6,
1929
+ height: 6,
1930
+ borderRadius: "50%",
1931
+ background: i === index ? "var(--squisq-recorder-accent)" : shownBlockIds?.has(slide.blockId) ? "var(--squisq-recorder-text)" : "var(--squisq-recorder-border)"
1932
+ }
1933
+ },
1934
+ slide.blockId
1935
+ )) }),
1936
+ /* @__PURE__ */ jsx4(
1937
+ "button",
1938
+ {
1939
+ type: "button",
1940
+ style: navButtonStyle,
1941
+ onClick: () => step(-1),
1942
+ disabled: index <= 0,
1943
+ "aria-label": "Previous slide",
1944
+ children: /* @__PURE__ */ jsx4(Icon, { icon: "fa-solid fa-chevron-left" })
1945
+ }
1946
+ ),
1947
+ /* @__PURE__ */ jsx4(
1948
+ "button",
1949
+ {
1950
+ type: "button",
1951
+ style: navButtonStyle,
1952
+ onClick: () => step(1),
1953
+ disabled: index >= count - 1,
1954
+ "aria-label": "Next slide",
1955
+ children: /* @__PURE__ */ jsx4(Icon, { icon: "fa-solid fa-chevron-right" })
1956
+ }
1957
+ )
1958
+ ] }),
1959
+ /* @__PURE__ */ jsx4("div", { style: stageStyle, children: /* @__PURE__ */ jsx4(
1960
+ "div",
1961
+ {
1962
+ style: {
1963
+ width: "100%",
1964
+ maxHeight: "100%",
1965
+ aspectRatio: `${viewport.width} / ${viewport.height}`,
1966
+ overflow: "hidden",
1967
+ background: "#000"
1968
+ },
1969
+ children: active.visual ? /* @__PURE__ */ jsx4(
1970
+ BlockThumbnail,
1971
+ {
1972
+ visual: active.visual,
1973
+ viewport,
1974
+ basePath,
1975
+ mediaProvider
1976
+ }
1977
+ ) : /* @__PURE__ */ jsxs3(
1978
+ "div",
1979
+ {
1980
+ style: {
1981
+ width: "100%",
1982
+ height: "100%",
1983
+ display: "flex",
1984
+ alignItems: "center",
1985
+ justifyContent: "center",
1986
+ padding: 16,
1987
+ textAlign: "center",
1988
+ background: "var(--squisq-recorder-input)",
1989
+ color: "var(--squisq-recorder-muted)",
1990
+ fontSize: 13
1991
+ },
1992
+ children: [
1993
+ active.heading,
1994
+ " \u2014 no slide rendition"
1995
+ ]
1996
+ }
1997
+ )
1998
+ }
1999
+ ) }),
2000
+ /* @__PURE__ */ jsxs3("div", { style: notesStyle, "data-testid": "recorder-slide-notes", children: [
2001
+ /* @__PURE__ */ jsx4("p", { style: headingStyle, children: active.heading }),
2002
+ active.bodyText ? /* @__PURE__ */ jsx4("div", { children: active.bodyText }) : /* @__PURE__ */ jsx4("p", { style: hintStyle, children: "No additional narrative for this slide." }),
2003
+ /* @__PURE__ */ jsx4("p", { style: { ...hintStyle, marginTop: 8 }, children: recording ? "Recording \u2014 each slide is timed from when you first show it." : "Use the arrow keys or the buttons to move through the deck." })
2004
+ ] })
2005
+ ]
2006
+ }
2007
+ );
2008
+ }
2009
+
1655
2010
  // src/recorder/RecorderModal.tsx
1656
2011
  import {
1657
2012
  Fragment as Fragment2,
1658
- useCallback as useCallback3,
2013
+ useCallback as useCallback4,
1659
2014
  useEffect as useEffect4,
1660
2015
  useId as useId2,
1661
2016
  useMemo,
1662
2017
  useRef as useRef2,
1663
2018
  useState as useState3
1664
2019
  } from "react";
2020
+ import { VIEWPORT_PRESETS as VIEWPORT_PRESETS2 } from "@bendyline/squisq/schemas";
2021
+ import {
2022
+ EMPTY_ADVANCE_LOG,
2023
+ advanceCoverage,
2024
+ buildAdvanceTimingJson,
2025
+ recordSlideShown
2026
+ } from "@bendyline/squisq/narration";
1665
2027
 
1666
2028
  // src/modal/useModalDialog.ts
1667
2029
  import { useModalDialog } from "@bendyline/squisq-react";
@@ -1689,7 +2051,7 @@ function narrationCaptureSummary(withCamera) {
1689
2051
  }
1690
2052
 
1691
2053
  // src/recorder/RecorderModal.tsx
1692
- import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2054
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1693
2055
  var overlayStyle = {
1694
2056
  position: "fixed",
1695
2057
  inset: 0,
@@ -2034,7 +2396,9 @@ function RecorderModal({
2034
2396
  colorScheme = "light",
2035
2397
  onClose,
2036
2398
  onSave,
2037
- narration = null
2399
+ narration = null,
2400
+ slides = null,
2401
+ title = "Record media"
2038
2402
  }) {
2039
2403
  const initialToggles = toggleStateFromMode(initialMode);
2040
2404
  const [micOn, setMicOn] = useState3(initialToggles.micOn);
@@ -2048,7 +2412,10 @@ function RecorderModal({
2048
2412
  const [playbackUrl, setPlaybackUrl] = useState3(null);
2049
2413
  const [cameraPlaybackUrl, setCameraPlaybackUrl] = useState3(null);
2050
2414
  const [playbackPositionMs, setPlaybackPositionMs] = useState3(0);
2051
- const [narrationOn, setNarrationOn] = useState3(false);
2415
+ const [panelMode, setPanelMode] = useState3("none");
2416
+ const narrationOn = panelMode === "narration";
2417
+ const slidesOn = panelMode === "slides";
2418
+ const expanded = isExpandedPanel(panelMode);
2052
2419
  const [narrationRequesting, setNarrationRequesting] = useState3(false);
2053
2420
  const [narrationPreview, setNarrationPreview] = useState3(null);
2054
2421
  const [deviceSettings, setDeviceSettings] = useState3(() => ({
@@ -2131,7 +2498,43 @@ function RecorderModal({
2131
2498
  });
2132
2499
  const stageRef = useRef2(stage);
2133
2500
  stageRef.current = stage;
2134
- const handleDeviceSettingsChange = useCallback3(
2501
+ const slidesViewport = slides?.viewport ?? VIEWPORT_PRESETS2.landscape;
2502
+ const slideDeck = useMemo(
2503
+ () => slides?.doc ? buildRecorderSlideDeck(slides.doc, slides.theme, slidesViewport) : [],
2504
+ [slides?.doc, slides?.theme, slidesViewport]
2505
+ );
2506
+ const slidesAvailable = slideDeck.length > 0;
2507
+ const slidesCaptureTimings = Boolean(slides?.captureTimings && slides?.doc);
2508
+ const [slideIndex, setSlideIndex] = useState3(0);
2509
+ const [advanceLog, setAdvanceLog] = useState3(EMPTY_ADVANCE_LOG);
2510
+ const [applySlideTimings, setApplySlideTimings] = useState3(true);
2511
+ const slideIndexRef = useRef2(slideIndex);
2512
+ slideIndexRef.current = slideIndex;
2513
+ const advanceLogRef = useRef2(advanceLog);
2514
+ advanceLogRef.current = advanceLog;
2515
+ const slideDeckRef = useRef2(slideDeck);
2516
+ slideDeckRef.current = slideDeck;
2517
+ const shownBlockIds = useMemo(
2518
+ () => new Set(advanceLog.map((advance) => advance.blockId)),
2519
+ [advanceLog]
2520
+ );
2521
+ const handleSlideIndexChange = useCallback4(
2522
+ (next) => {
2523
+ const clamped = clampSlideIndex(next, slideDeckRef.current.length);
2524
+ setSlideIndex(clamped);
2525
+ const elapsedMs = recorder.getElapsedMs();
2526
+ const blockId = slideDeckRef.current[clamped]?.blockId;
2527
+ if (elapsedMs === null || !blockId) return;
2528
+ setAdvanceLog((log) => recordSlideShown(log, blockId, elapsedMs));
2529
+ },
2530
+ [recorder]
2531
+ );
2532
+ const slideDeckKey = slideDeck.map((slide) => slide.blockId).join("\0");
2533
+ useEffect4(() => {
2534
+ setAdvanceLog(EMPTY_ADVANCE_LOG);
2535
+ setSlideIndex(0);
2536
+ }, [panelMode, slideDeckKey]);
2537
+ const handleDeviceSettingsChange = useCallback4(
2135
2538
  (next) => {
2136
2539
  setDeviceSettings(next);
2137
2540
  if (narrationOn) {
@@ -2216,7 +2619,7 @@ function RecorderModal({
2216
2619
  }
2217
2620
  };
2218
2621
  }, [cameraConstraints, narrationPreviewWanted]);
2219
- const handleNarrationPreview = useCallback3(async () => {
2622
+ const handleNarrationPreview = useCallback4(async () => {
2220
2623
  setNarrationRequesting(true);
2221
2624
  try {
2222
2625
  const controller = stageRef.current.controller;
@@ -2225,7 +2628,7 @@ function RecorderModal({
2225
2628
  setNarrationRequesting(false);
2226
2629
  }
2227
2630
  }, []);
2228
- const handleNarrationRecord = useCallback3(() => {
2631
+ const handleNarrationRecord = useCallback4(() => {
2229
2632
  void stageRef.current.recorder.start();
2230
2633
  }, []);
2231
2634
  const narrationCameraStream = narrationOn ? stage.recorder.cameraStream ?? narrationPreview : null;
@@ -2274,10 +2677,11 @@ function RecorderModal({
2274
2677
  if (previousKeyRef.current !== captureKey) {
2275
2678
  previousKeyRef.current = captureKey;
2276
2679
  dualSaveProgressRef.current = {};
2680
+ setAdvanceLog(EMPTY_ADVANCE_LOG);
2277
2681
  recorder.cancel();
2278
2682
  }
2279
2683
  }, [captureKey, recorder]);
2280
- const handleClose = useCallback3(() => {
2684
+ const handleClose = useCallback4(() => {
2281
2685
  const s = stageRef.current;
2282
2686
  if (closeNeedsConfirm(narrationOn, s.recorder.state, s.recorder.take !== null)) {
2283
2687
  if (!window.confirm("Discard the current narration take?")) return;
@@ -2302,23 +2706,55 @@ function RecorderModal({
2302
2706
  stage.controller.transport
2303
2707
  )
2304
2708
  });
2305
- const handleRequest = useCallback3(async () => {
2709
+ const handleRequest = useCallback4(async () => {
2306
2710
  setSaveError(null);
2307
2711
  try {
2308
2712
  await recorder.request();
2309
2713
  } catch {
2310
2714
  }
2311
2715
  }, [recorder]);
2312
- const handleStart = useCallback3(() => {
2716
+ const handleStart = useCallback4(() => {
2313
2717
  setSaveError(null);
2314
2718
  dualSaveProgressRef.current = {};
2315
2719
  recorder.start();
2720
+ const blockId = slideDeckRef.current[slideIndexRef.current]?.blockId;
2721
+ setAdvanceLog(blockId ? recordSlideShown(EMPTY_ADVANCE_LOG, blockId, 0) : EMPTY_ADVANCE_LOG);
2316
2722
  }, [recorder]);
2317
- const handleStop = useCallback3(async () => {
2723
+ const handleStop = useCallback4(async () => {
2318
2724
  setSaveError(null);
2319
2725
  await recorder.stop();
2320
2726
  }, [recorder]);
2321
- const handleSave = useCallback3(async () => {
2727
+ const writeSlideTimingSidecar = useCallback4(
2728
+ async (mediaRelativePath, durationSec) => {
2729
+ const doc = slides?.doc;
2730
+ if (!slidesOn || !slidesCaptureTimings || !applySlideTimings || !doc) return null;
2731
+ const log = advanceLogRef.current;
2732
+ if (log.length === 0) return null;
2733
+ const timing = buildAdvanceTimingJson(doc, log, durationSec);
2734
+ const encoded = encodeNarrationTimingJson(timing);
2735
+ const sidecarPath = timingPathFor(mediaRelativePath);
2736
+ if (container) {
2737
+ await container.writeFile(sidecarPath, encoded, "application/json");
2738
+ } else {
2739
+ const written = await mediaProvider.addMedia(sidecarPath, encoded, "application/json");
2740
+ if (written !== sidecarPath) {
2741
+ console.warn(
2742
+ `[squisq-recorder] block timings were saved as "${written}" instead of "${sidecarPath}" \u2014 they will not be applied.`
2743
+ );
2744
+ return null;
2745
+ }
2746
+ }
2747
+ const coverage = advanceCoverage(doc, log);
2748
+ return {
2749
+ sidecarPath,
2750
+ blockCount: coverage.total,
2751
+ unshownCount: coverage.unshown,
2752
+ durationSec: timing.duration
2753
+ };
2754
+ },
2755
+ [slidesOn, slidesCaptureTimings, applySlideTimings, slides?.doc, container, mediaProvider]
2756
+ );
2757
+ const handleSave = useCallback4(async () => {
2322
2758
  if (!recorder.blob || !recorder.mimeType || !recorder.extension || !recorder.directory) {
2323
2759
  setSaveError("Nothing to save yet \u2014 record something first.");
2324
2760
  return;
@@ -2365,6 +2801,8 @@ function RecorderModal({
2365
2801
  );
2366
2802
  progress.cameraPath = cameraPath;
2367
2803
  const duration = recorder.durationMs / 1e3;
2804
+ const slideTiming = progress.slideTiming ?? await writeSlideTimingSidecar(screenPath, duration);
2805
+ progress.slideTiming = slideTiming;
2368
2806
  const offsetSec = recorder.cameraOffsetSec ?? 0;
2369
2807
  const result = {
2370
2808
  relativePath: screenPath,
@@ -2373,7 +2811,8 @@ function RecorderModal({
2373
2811
  mediaKind: "video",
2374
2812
  mimeType: recorder.mimeType,
2375
2813
  duration,
2376
- hasTimingSidecar: false,
2814
+ hasTimingSidecar: slideTiming !== null,
2815
+ ...slideTiming ? { slideTiming } : {},
2377
2816
  camera: {
2378
2817
  relativePath: cameraPath,
2379
2818
  filename: cameraFilename,
@@ -2404,9 +2843,11 @@ function RecorderModal({
2404
2843
  recorder.blob,
2405
2844
  recorder.mimeType
2406
2845
  );
2407
- let hasTimingSidecar = false;
2408
- if (source === "mic") {
2409
- const timing = buildTimingJson(sourceText, recorder.durationMs / 1e3);
2846
+ const duration = recorder.durationMs / 1e3;
2847
+ const slideTiming = await writeSlideTimingSidecar(relativePath, duration);
2848
+ let hasTimingSidecar = slideTiming !== null;
2849
+ if (!slideTiming && source === "mic") {
2850
+ const timing = buildTimingJson(sourceText, duration);
2410
2851
  const encoded = encodeTimingJson(timing);
2411
2852
  const sidecarPath = timingPathFor(relativePath);
2412
2853
  if (container) {
@@ -2428,11 +2869,12 @@ function RecorderModal({
2428
2869
  source,
2429
2870
  mediaKind,
2430
2871
  mimeType: recorder.mimeType,
2431
- duration: recorder.durationMs / 1e3,
2872
+ duration,
2432
2873
  hasTimingSidecar
2433
2874
  };
2875
+ if (slideTiming) result.slideTiming = slideTiming;
2434
2876
  if (source === "mic") {
2435
- result.sourceText = sourceText;
2877
+ result.sourceText = slideTiming ? void 0 : sourceText;
2436
2878
  }
2437
2879
  onSave?.(result);
2438
2880
  handleClose();
@@ -2454,13 +2896,15 @@ function RecorderModal({
2454
2896
  mediaProvider,
2455
2897
  container,
2456
2898
  onSave,
2457
- handleClose
2899
+ handleClose,
2900
+ writeSlideTimingSidecar
2458
2901
  ]);
2459
- const handleDiscard = useCallback3(() => {
2902
+ const handleDiscard = useCallback4(() => {
2460
2903
  dualSaveProgressRef.current = {};
2904
+ setAdvanceLog(EMPTY_ADVANCE_LOG);
2461
2905
  recorder.reset();
2462
2906
  }, [recorder]);
2463
- const handlePlaybackTimeUpdate = useCallback3(
2907
+ const handlePlaybackTimeUpdate = useCallback4(
2464
2908
  (media) => {
2465
2909
  const currentMs = Number.isFinite(media.currentTime) ? media.currentTime * 1e3 : 0;
2466
2910
  setPlaybackPositionMs(Math.min(recorder.durationMs, Math.max(0, currentMs)));
@@ -2473,6 +2917,18 @@ function RecorderModal({
2473
2917
  const canStop = recorder.state === "recording";
2474
2918
  const canSave = recorder.state === "stopped" && recorder.blob !== null;
2475
2919
  const isBusy = recorder.state === "requesting" || recorder.state === "stopping" || isSaving;
2920
+ const slidesTimingCapture = slidesOn && slidesCaptureTimings;
2921
+ const showTimingCheckbox = showSlideTimingCheckbox({
2922
+ slidesOn,
2923
+ captureTimings: slidesCaptureTimings,
2924
+ recorderState: recorder.state,
2925
+ hasBlob: recorder.blob !== null
2926
+ });
2927
+ const slideCoverage = useMemo(
2928
+ () => slides?.doc && showTimingCheckbox ? advanceCoverage(slides.doc, advanceLog) : null,
2929
+ [slides?.doc, showTimingCheckbox, advanceLog]
2930
+ );
2931
+ const slideCoverageWarning = slideCoverage ? unshownSlidesWarning(slideCoverage.total, slideCoverage.unshown) : null;
2476
2932
  const togglesLocked = recorder.state === "recording" || recorder.state === "requesting" || canSave;
2477
2933
  const toggleLockReason = canSave ? "Save or discard this recording before changing sources" : void 0;
2478
2934
  const systemAudioHasCompanion = micOn || cameraOn || screenOn;
@@ -2542,22 +2998,24 @@ function RecorderModal({
2542
2998
  };
2543
2999
  }
2544
3000
  };
2545
- return /* @__PURE__ */ jsx3(
3001
+ return /* @__PURE__ */ jsx5(
2546
3002
  "div",
2547
3003
  {
2548
3004
  ref: overlayRef,
2549
3005
  className: "squisq-editor-shell squisq-recorder-overlay",
2550
3006
  "data-theme": colorScheme,
2551
3007
  style: { ...overlayStyle, ...recorderThemeStyle(colorScheme) },
2552
- children: /* @__PURE__ */ jsxs3(
3008
+ children: /* @__PURE__ */ jsxs4(
2553
3009
  "div",
2554
3010
  {
2555
3011
  ref: dialogRef,
2556
3012
  className: "squisq-editor-shell",
2557
3013
  "data-theme": colorScheme,
2558
3014
  "data-narration": narrationOn ? "true" : void 0,
3015
+ "data-slides": slidesOn ? "true" : void 0,
3016
+ "data-panel-mode": panelMode,
2559
3017
  style: {
2560
- ...narrationOn ? modalExpandedStyle : modalStyle,
3018
+ ...expanded ? modalExpandedStyle : modalStyle,
2561
3019
  ...recorderThemeStyle(colorScheme)
2562
3020
  },
2563
3021
  onClick: (e) => e.stopPropagation(),
@@ -2566,15 +3024,15 @@ function RecorderModal({
2566
3024
  "aria-labelledby": headingId,
2567
3025
  tabIndex: -1,
2568
3026
  children: [
2569
- /* @__PURE__ */ jsx3("h2", { id: headingId, style: titleStyle, children: "Record media" }),
2570
- /* @__PURE__ */ jsxs3("div", { style: narrationOn ? bodyRowStyle : void 0, children: [
2571
- /* @__PURE__ */ jsxs3("div", { style: narrationOn ? leftColStyle : void 0, children: [
2572
- /* @__PURE__ */ jsx3("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLE_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxs3(Fragment2, { children: [
2573
- groupIndex > 0 && /* @__PURE__ */ jsx3("div", { "aria-hidden": "true", style: groupDividerStyle }),
2574
- /* @__PURE__ */ jsx3("div", { style: toggleGroupStyle, children: group.toggles.map((t) => {
3027
+ /* @__PURE__ */ jsx5("h2", { id: headingId, style: titleStyle, children: title }),
3028
+ /* @__PURE__ */ jsxs4("div", { style: expanded ? bodyRowStyle : void 0, children: [
3029
+ /* @__PURE__ */ jsxs4("div", { style: expanded ? leftColStyle : void 0, children: [
3030
+ /* @__PURE__ */ jsx5("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLE_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxs4(Fragment2, { children: [
3031
+ groupIndex > 0 && /* @__PURE__ */ jsx5("div", { "aria-hidden": "true", style: groupDividerStyle }),
3032
+ /* @__PURE__ */ jsx5("div", { style: toggleGroupStyle, children: group.toggles.map((t) => {
2575
3033
  if (t.key === "systemAudio" && !canIncludeSystemAudio) return null;
2576
3034
  const props = narrationOn ? narrationToggleFor(t.key) : simpleToggleProps(t.key);
2577
- return /* @__PURE__ */ jsx3(
3035
+ return /* @__PURE__ */ jsx5(
2578
3036
  "button",
2579
3037
  {
2580
3038
  type: "button",
@@ -2589,22 +3047,36 @@ function RecorderModal({
2589
3047
  );
2590
3048
  }) })
2591
3049
  ] }, group.label)) }),
2592
- /* @__PURE__ */ jsx3("p", { style: summaryStyle, children: narrationOn ? narrationCaptureSummary(stage.recorder.withCamera) : captureSummary(micOn, cameraOn, screenOn, includeSystemAudio) }),
2593
- narrationAvailable && /* @__PURE__ */ jsxs3("label", { style: checkboxRowStyle, children: [
2594
- /* @__PURE__ */ jsx3(
3050
+ /* @__PURE__ */ jsx5("p", { style: summaryStyle, children: narrationOn ? narrationCaptureSummary(stage.recorder.withCamera) : captureSummary(micOn, cameraOn, screenOn, includeSystemAudio) }),
3051
+ narrationAvailable && /* @__PURE__ */ jsxs4("label", { style: checkboxRowStyle, children: [
3052
+ /* @__PURE__ */ jsx5(
2595
3053
  "input",
2596
3054
  {
2597
3055
  type: "checkbox",
2598
3056
  style: { accentColor: "var(--squisq-recorder-accent)" },
2599
3057
  checked: narrationOn,
2600
- onChange: (e) => setNarrationOn(e.target.checked),
3058
+ onChange: (e) => setPanelMode(panelModeAfterToggle(panelMode, "narration", e.target.checked)),
2601
3059
  disabled: narrationToggleDisabled,
2602
3060
  title: narrationToggleDisabled ? narrationOn ? "Finish or discard the narration take first" : "Save or discard this recording before switching modes" : void 0
2603
3061
  }
2604
3062
  ),
2605
3063
  "Show narration mode"
2606
3064
  ] }),
2607
- /* @__PURE__ */ jsx3(
3065
+ slidesAvailable && /* @__PURE__ */ jsxs4("label", { style: checkboxRowStyle, children: [
3066
+ /* @__PURE__ */ jsx5(
3067
+ "input",
3068
+ {
3069
+ type: "checkbox",
3070
+ style: { accentColor: "var(--squisq-recorder-accent)" },
3071
+ checked: slidesOn,
3072
+ onChange: (e) => setPanelMode(panelModeAfterToggle(panelMode, "slides", e.target.checked)),
3073
+ disabled: narrationToggleDisabled,
3074
+ title: narrationToggleDisabled ? "Save or discard this recording before switching modes" : void 0
3075
+ }
3076
+ ),
3077
+ "Show slides mode"
3078
+ ] }),
3079
+ /* @__PURE__ */ jsx5(
2608
3080
  RecorderDeviceQuickPicks,
2609
3081
  {
2610
3082
  devices: mediaDevices,
@@ -2623,7 +3095,7 @@ function RecorderModal({
2623
3095
  disabled: deviceSettingsLocked
2624
3096
  }
2625
3097
  ),
2626
- /* @__PURE__ */ jsx3(
3098
+ /* @__PURE__ */ jsx5(
2627
3099
  RecorderDeviceSettingsPanel,
2628
3100
  {
2629
3101
  value: deviceSettings,
@@ -2638,10 +3110,10 @@ function RecorderModal({
2638
3110
  cameraStream: activeCameraStream
2639
3111
  }
2640
3112
  ),
2641
- !narrationOn && recorder.error && /* @__PURE__ */ jsx3("div", { style: errorStyle, children: recorder.error.message }),
2642
- !narrationOn && saveError && /* @__PURE__ */ jsx3("div", { style: errorStyle, children: saveError }),
2643
- narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx3("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
2644
- narrationOn && narrationCameraStream && /* @__PURE__ */ jsx3("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx3(
3113
+ !narrationOn && recorder.error && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: recorder.error.message }),
3114
+ !narrationOn && saveError && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: saveError }),
3115
+ narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
3116
+ narrationOn && narrationCameraStream && /* @__PURE__ */ jsx5("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx5(
2645
3117
  "video",
2646
3118
  {
2647
3119
  ref: previewRef,
@@ -2651,10 +3123,10 @@ function RecorderModal({
2651
3123
  style: { width: "100%", height: "100%", objectFit: "contain" }
2652
3124
  }
2653
3125
  ) }),
2654
- narrationOn && !narrationCameraStream && narrationTakeDone && /* @__PURE__ */ jsx3("div", { style: audioMeterStyle, children: stage.recorder.take ? `\u2713 Recorded ${formatDurationMs(stage.recorder.take.durationSec * 1e3)}` : "\u25CF Processing take\u2026" }),
2655
- narrationOn && !narrationCameraStream && !narrationTakeDone && stage.recorder.withCamera && /* @__PURE__ */ jsx3("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx3("span", { children: narrationPreviewWanted ? "Camera starting\u2026" : "Click Start preview to turn on your camera." }) }),
2656
- narrationOn && !narrationCameraStream && !narrationTakeDone && !stage.recorder.withCamera && /* @__PURE__ */ jsxs3("div", { style: { ...audioMeterStyle, position: "relative", overflow: "hidden" }, children: [
2657
- /* @__PURE__ */ jsx3(
3126
+ narrationOn && !narrationCameraStream && narrationTakeDone && /* @__PURE__ */ jsx5("div", { style: audioMeterStyle, children: stage.recorder.take ? `\u2713 Recorded ${formatDurationMs(stage.recorder.take.durationSec * 1e3)}` : "\u25CF Processing take\u2026" }),
3127
+ narrationOn && !narrationCameraStream && !narrationTakeDone && stage.recorder.withCamera && /* @__PURE__ */ jsx5("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx5("span", { children: narrationPreviewWanted ? "Camera starting\u2026" : "Click Start preview to turn on your camera." }) }),
3128
+ narrationOn && !narrationCameraStream && !narrationTakeDone && !stage.recorder.withCamera && /* @__PURE__ */ jsxs4("div", { style: { ...audioMeterStyle, position: "relative", overflow: "hidden" }, children: [
3129
+ /* @__PURE__ */ jsx5(
2658
3130
  "div",
2659
3131
  {
2660
3132
  "aria-hidden": "true",
@@ -2664,11 +3136,11 @@ function RecorderModal({
2664
3136
  }
2665
3137
  }
2666
3138
  ),
2667
- /* @__PURE__ */ jsx3("span", { style: { position: "relative" }, children: stage.recorder.state === "recording" ? "\u25CF Recording narration" : narrationMicLive ? stage.controller.voiceActive ? "Voice detected" : "Microphone ready" : "Click Start preview to check your mic." })
3139
+ /* @__PURE__ */ jsx5("span", { style: { position: "relative" }, children: stage.recorder.state === "recording" ? "\u25CF Recording narration" : narrationMicLive ? stage.controller.voiceActive ? "Voice detected" : "Microphone ready" : "Click Start preview to check your mic." })
2668
3140
  ] }),
2669
- !narrationOn && !showPreview && /* @__PURE__ */ jsx3("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx3("span", { children: "Click Start Preview to start a recording." }) }),
2670
- !narrationOn && showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsxs3("div", { style: isDual ? { ...previewBoxStyle, position: "relative" } : previewBoxStyle, children: [
2671
- /* @__PURE__ */ jsx3(
3141
+ !narrationOn && !showPreview && /* @__PURE__ */ jsx5("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx5("span", { children: "Click Start Preview to start a recording." }) }),
3142
+ !narrationOn && showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: isDual ? { ...previewBoxStyle, position: "relative" } : previewBoxStyle, children: [
3143
+ /* @__PURE__ */ jsx5(
2672
3144
  "video",
2673
3145
  {
2674
3146
  ref: previewRef,
@@ -2678,7 +3150,7 @@ function RecorderModal({
2678
3150
  style: { width: "100%", height: "100%", objectFit: "contain" }
2679
3151
  }
2680
3152
  ),
2681
- isDual && /* @__PURE__ */ jsx3(
3153
+ isDual && /* @__PURE__ */ jsx5(
2682
3154
  "video",
2683
3155
  {
2684
3156
  ref: cameraPreviewRef,
@@ -2699,12 +3171,12 @@ function RecorderModal({
2699
3171
  }
2700
3172
  )
2701
3173
  ] }),
2702
- !narrationOn && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx3("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
3174
+ !narrationOn && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx5("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
2703
3175
  "\u25CF Recording ",
2704
3176
  formatDurationMs(recorder.durationMs)
2705
- ] }) : /* @__PURE__ */ jsx3(Fragment3, { children: "Microphone ready" }) }),
2706
- !narrationOn && recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsxs3("div", { style: { ...previewBoxStyle, position: "relative" }, children: [
2707
- /* @__PURE__ */ jsx3(
3177
+ ] }) : /* @__PURE__ */ jsx5(Fragment3, { children: "Microphone ready" }) }),
3178
+ !narrationOn && recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: { ...previewBoxStyle, position: "relative" }, children: [
3179
+ /* @__PURE__ */ jsx5(
2708
3180
  "video",
2709
3181
  {
2710
3182
  src: playbackUrl,
@@ -2716,7 +3188,7 @@ function RecorderModal({
2716
3188
  style: { width: "100%", height: "100%", objectFit: "contain" }
2717
3189
  }
2718
3190
  ),
2719
- /* @__PURE__ */ jsxs3(
3191
+ /* @__PURE__ */ jsxs4(
2720
3192
  "div",
2721
3193
  {
2722
3194
  role: "timer",
@@ -2730,9 +3202,9 @@ function RecorderModal({
2730
3202
  }
2731
3203
  )
2732
3204
  ] }),
2733
- !narrationOn && recorder.state === "stopped" && isDual && cameraPlaybackUrl && /* @__PURE__ */ jsxs3("div", { style: { marginBottom: 12 }, children: [
2734
- /* @__PURE__ */ jsx3("div", { style: summaryStyle, children: "Camera (picture-in-picture)" }),
2735
- /* @__PURE__ */ jsx3(
3205
+ !narrationOn && recorder.state === "stopped" && isDual && cameraPlaybackUrl && /* @__PURE__ */ jsxs4("div", { style: { marginBottom: 12 }, children: [
3206
+ /* @__PURE__ */ jsx5("div", { style: summaryStyle, children: "Camera (picture-in-picture)" }),
3207
+ /* @__PURE__ */ jsx5(
2736
3208
  "video",
2737
3209
  {
2738
3210
  src: cameraPlaybackUrl,
@@ -2748,16 +3220,16 @@ function RecorderModal({
2748
3220
  }
2749
3221
  )
2750
3222
  ] }),
2751
- !narrationOn && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs3("div", { style: { marginBottom: 12 }, children: [
2752
- /* @__PURE__ */ jsxs3("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
3223
+ !narrationOn && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: { marginBottom: 12 }, children: [
3224
+ /* @__PURE__ */ jsxs4("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
2753
3225
  "\u2713 Recorded ",
2754
3226
  formatDurationMs(recorder.durationMs)
2755
3227
  ] }),
2756
- /* @__PURE__ */ jsx3("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
3228
+ /* @__PURE__ */ jsx5("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
2757
3229
  ] }),
2758
- !narrationOn && source === "mic" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2759
- /* @__PURE__ */ jsx3("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
2760
- /* @__PURE__ */ jsx3(
3230
+ !narrationOn && !slidesTimingCapture && source === "mic" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3231
+ /* @__PURE__ */ jsx5("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
3232
+ /* @__PURE__ */ jsx5(
2761
3233
  "textarea",
2762
3234
  {
2763
3235
  id: "recorder-source-text",
@@ -2769,8 +3241,8 @@ function RecorderModal({
2769
3241
  }
2770
3242
  )
2771
3243
  ] }),
2772
- /* @__PURE__ */ jsx3("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
2773
- /* @__PURE__ */ jsx3(
3244
+ /* @__PURE__ */ jsx5("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
3245
+ /* @__PURE__ */ jsx5(
2774
3246
  "input",
2775
3247
  {
2776
3248
  id: "recorder-basename",
@@ -2782,12 +3254,28 @@ function RecorderModal({
2782
3254
  disabled: narrationOn ? !narrationRecorderIdle : recorder.state === "recording"
2783
3255
  }
2784
3256
  ),
2785
- !narrationOn && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs3("div", { style: recordingStatusStyle, children: [
3257
+ !narrationOn && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: recordingStatusStyle, children: [
2786
3258
  "\u25CF Recording ",
2787
3259
  formatDurationMs(recorder.durationMs)
2788
3260
  ] }),
2789
- /* @__PURE__ */ jsxs3("div", { style: buttonRowStyle, children: [
2790
- /* @__PURE__ */ jsx3(
3261
+ showTimingCheckbox && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3262
+ /* @__PURE__ */ jsxs4("label", { style: checkboxRowStyle, children: [
3263
+ /* @__PURE__ */ jsx5(
3264
+ "input",
3265
+ {
3266
+ type: "checkbox",
3267
+ style: { accentColor: "var(--squisq-recorder-accent)" },
3268
+ checked: applySlideTimings,
3269
+ onChange: (e) => setApplySlideTimings(e.target.checked),
3270
+ disabled: isSaving
3271
+ }
3272
+ ),
3273
+ SLIDE_TIMING_CHECKBOX_LABEL
3274
+ ] }),
3275
+ applySlideTimings && slideCoverageWarning && /* @__PURE__ */ jsx5("p", { style: summaryStyle, children: slideCoverageWarning })
3276
+ ] }),
3277
+ /* @__PURE__ */ jsxs4("div", { style: buttonRowStyle, children: [
3278
+ /* @__PURE__ */ jsx5(
2791
3279
  "button",
2792
3280
  {
2793
3281
  type: "button",
@@ -2797,8 +3285,8 @@ function RecorderModal({
2797
3285
  children: "Close"
2798
3286
  }
2799
3287
  ),
2800
- narrationOn && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2801
- narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx3(
3288
+ narrationOn && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3289
+ narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx5(
2802
3290
  "button",
2803
3291
  {
2804
3292
  type: "button",
@@ -2808,14 +3296,14 @@ function RecorderModal({
2808
3296
  children: narrationRequesting ? "Requesting\u2026" : "Start preview"
2809
3297
  }
2810
3298
  ),
2811
- narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs3("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
2812
- /* @__PURE__ */ jsx3(
3299
+ narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs4("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
3300
+ /* @__PURE__ */ jsx5(
2813
3301
  "span",
2814
3302
  {
2815
3303
  className: "squisq-recorder-record-dot",
2816
3304
  style: recordDotFrameStyle,
2817
3305
  "aria-hidden": "true",
2818
- children: /* @__PURE__ */ jsx3(
3306
+ children: /* @__PURE__ */ jsx5(
2819
3307
  "span",
2820
3308
  {
2821
3309
  className: "squisq-recorder-record-dot-center",
@@ -2826,7 +3314,7 @@ function RecorderModal({
2826
3314
  ),
2827
3315
  "Record"
2828
3316
  ] }),
2829
- (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx3(
3317
+ (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx5(
2830
3318
  "button",
2831
3319
  {
2832
3320
  type: "button",
@@ -2835,11 +3323,11 @@ function RecorderModal({
2835
3323
  children: "Stop"
2836
3324
  }
2837
3325
  ),
2838
- stage.recorder.state === "processing" && /* @__PURE__ */ jsx3("span", { style: recordingStatusStyle, children: "Aligning take\u2026" }),
2839
- stage.recorder.state === "saving" && /* @__PURE__ */ jsx3("span", { style: recordingStatusStyle, children: "Saving\u2026" }),
2840
- stage.recorder.state === "review" && stage.recorder.take && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2841
- /* @__PURE__ */ jsx3("button", { type: "button", style: btnSecondary, onClick: stage.handleRetake, children: "Discard & re-record" }),
2842
- /* @__PURE__ */ jsx3(
3326
+ stage.recorder.state === "processing" && /* @__PURE__ */ jsx5("span", { style: recordingStatusStyle, children: "Aligning take\u2026" }),
3327
+ stage.recorder.state === "saving" && /* @__PURE__ */ jsx5("span", { style: recordingStatusStyle, children: "Saving\u2026" }),
3328
+ stage.recorder.state === "review" && stage.recorder.take && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3329
+ /* @__PURE__ */ jsx5("button", { type: "button", style: btnSecondary, onClick: stage.handleRetake, children: "Discard & re-record" }),
3330
+ /* @__PURE__ */ jsx5(
2843
3331
  "button",
2844
3332
  {
2845
3333
  type: "button",
@@ -2850,8 +3338,8 @@ function RecorderModal({
2850
3338
  )
2851
3339
  ] })
2852
3340
  ] }),
2853
- !narrationOn && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2854
- (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx3(
3341
+ !narrationOn && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3342
+ (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx5(
2855
3343
  "button",
2856
3344
  {
2857
3345
  type: "button",
@@ -2861,14 +3349,14 @@ function RecorderModal({
2861
3349
  children: recorder.state === "requesting" ? "Requesting\u2026" : "Start preview"
2862
3350
  }
2863
3351
  ),
2864
- canRecord && /* @__PURE__ */ jsxs3("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
2865
- /* @__PURE__ */ jsx3(
3352
+ canRecord && /* @__PURE__ */ jsxs4("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
3353
+ /* @__PURE__ */ jsx5(
2866
3354
  "span",
2867
3355
  {
2868
3356
  className: "squisq-recorder-record-dot",
2869
3357
  style: recordDotFrameStyle,
2870
3358
  "aria-hidden": "true",
2871
- children: /* @__PURE__ */ jsx3(
3359
+ children: /* @__PURE__ */ jsx5(
2872
3360
  "span",
2873
3361
  {
2874
3362
  className: "squisq-recorder-record-dot-center",
@@ -2879,9 +3367,9 @@ function RecorderModal({
2879
3367
  ),
2880
3368
  "Record"
2881
3369
  ] }),
2882
- canStop && /* @__PURE__ */ jsx3("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
2883
- canSave && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2884
- /* @__PURE__ */ jsx3(
3370
+ canStop && /* @__PURE__ */ jsx5("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
3371
+ canSave && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3372
+ /* @__PURE__ */ jsx5(
2885
3373
  "button",
2886
3374
  {
2887
3375
  type: "button",
@@ -2891,7 +3379,7 @@ function RecorderModal({
2891
3379
  children: "Discard & re-record"
2892
3380
  }
2893
3381
  ),
2894
- /* @__PURE__ */ jsx3(
3382
+ /* @__PURE__ */ jsx5(
2895
3383
  "button",
2896
3384
  {
2897
3385
  type: "button",
@@ -2905,7 +3393,7 @@ function RecorderModal({
2905
3393
  ] })
2906
3394
  ] })
2907
3395
  ] }),
2908
- narrationOn && narration && /* @__PURE__ */ jsx3("div", { style: rightColStyle, children: /* @__PURE__ */ jsx3(
3396
+ narrationOn && narration && /* @__PURE__ */ jsx5("div", { style: rightColStyle, children: /* @__PURE__ */ jsx5(
2909
3397
  NarrationStage,
2910
3398
  {
2911
3399
  stage,
@@ -2916,6 +3404,19 @@ function RecorderModal({
2916
3404
  showTransportPlay: false,
2917
3405
  showReviewActions: false
2918
3406
  }
3407
+ ) }),
3408
+ slidesOn && /* @__PURE__ */ jsx5("div", { style: rightColStyle, children: /* @__PURE__ */ jsx5(
3409
+ RecorderSlidesPanel,
3410
+ {
3411
+ slides: slideDeck,
3412
+ index: slideIndex,
3413
+ onIndexChange: handleSlideIndexChange,
3414
+ viewport: slidesViewport,
3415
+ basePath: slides?.basePath,
3416
+ mediaProvider: slides?.mediaProvider ?? mediaProvider,
3417
+ recording: recorder.state === "recording",
3418
+ shownBlockIds
3419
+ }
2919
3420
  ) })
2920
3421
  ] })
2921
3422
  ]
@@ -2926,9 +3427,9 @@ function RecorderModal({
2926
3427
  }
2927
3428
 
2928
3429
  // src/recorder/RecorderPanel.tsx
2929
- import { useCallback as useCallback4, useState as useState4 } from "react";
3430
+ import { useCallback as useCallback5, useState as useState4 } from "react";
2930
3431
  import { createPortal } from "react-dom";
2931
- import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3432
+ import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2932
3433
  function RecorderPanel({
2933
3434
  mediaProvider,
2934
3435
  container = null,
@@ -2936,7 +3437,9 @@ function RecorderPanel({
2936
3437
  colorScheme = "light",
2937
3438
  onSave,
2938
3439
  narration = null,
3440
+ slides = null,
2939
3441
  tooltip = "Record media",
3442
+ title,
2940
3443
  className,
2941
3444
  open: controlledOpen,
2942
3445
  onOpenChange,
@@ -2944,16 +3447,16 @@ function RecorderPanel({
2944
3447
  }) {
2945
3448
  const [uncontrolledOpen, setUncontrolledOpen] = useState4(false);
2946
3449
  const open = controlledOpen ?? uncontrolledOpen;
2947
- const setOpen = useCallback4(
3450
+ const setOpen = useCallback5(
2948
3451
  (nextOpen) => {
2949
3452
  if (controlledOpen === void 0) setUncontrolledOpen(nextOpen);
2950
3453
  onOpenChange?.(nextOpen);
2951
3454
  },
2952
3455
  [controlledOpen, onOpenChange]
2953
3456
  );
2954
- const handleClose = useCallback4(() => setOpen(false), [setOpen]);
2955
- return /* @__PURE__ */ jsxs4(Fragment4, { children: [
2956
- showTrigger && /* @__PURE__ */ jsx4(
3457
+ const handleClose = useCallback5(() => setOpen(false), [setOpen]);
3458
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
3459
+ showTrigger && /* @__PURE__ */ jsx6(
2957
3460
  "button",
2958
3461
  {
2959
3462
  type: "button",
@@ -2962,11 +3465,11 @@ function RecorderPanel({
2962
3465
  "aria-label": tooltip,
2963
3466
  "aria-expanded": open,
2964
3467
  onClick: () => setOpen(!open),
2965
- children: /* @__PURE__ */ jsx4(Icon, { icon: "fa-solid fa-microphone" })
3468
+ children: /* @__PURE__ */ jsx6(Icon, { icon: "fa-solid fa-microphone" })
2966
3469
  }
2967
3470
  ),
2968
3471
  open && typeof document !== "undefined" && createPortal(
2969
- /* @__PURE__ */ jsx4(
3472
+ /* @__PURE__ */ jsx6(
2970
3473
  RecorderModal,
2971
3474
  {
2972
3475
  mediaProvider,
@@ -2974,6 +3477,8 @@ function RecorderPanel({
2974
3477
  initialMode,
2975
3478
  colorScheme,
2976
3479
  narration,
3480
+ slides,
3481
+ ...title !== void 0 ? { title } : {},
2977
3482
  onClose: handleClose,
2978
3483
  onSave: (result) => {
2979
3484
  onSave?.(result);
@@ -3002,6 +3507,19 @@ export {
3002
3507
  buildRecorderScreenAudioConstraints,
3003
3508
  recorderBitsPerSecond,
3004
3509
  recordedMediaKind,
3510
+ isExpandedPanel,
3511
+ panelModeAfterToggle,
3512
+ clampSlideIndex,
3513
+ slideStepForKey,
3514
+ showSlideTimingCheckbox,
3515
+ unshownSlidesWarning,
3516
+ SLIDE_TIMING_CHECKBOX_LABEL,
3517
+ buildPreviewDoc,
3518
+ documentTitleFromFileName,
3519
+ resolveBlockVisual,
3520
+ buildRecorderSlideDeck,
3521
+ BlockThumbnail,
3522
+ RecorderSlidesPanel,
3005
3523
  useModalDialog,
3006
3524
  RecorderModal,
3007
3525
  RecorderPanel