@bendyline/squisq-react 2.4.7 → 2.6.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.
- package/README.md +19 -0
- package/dist/MarkdownRenderer-CerBKw-c.d.ts +53 -0
- package/dist/{chunk-KF4DTT46.js → chunk-GWD62KLD.js} +113 -49
- package/dist/{chunk-BOZJ655L.js → chunk-MVJQL2W2.js} +19 -12
- package/dist/{chunk-VMPQEUJH.js → chunk-TMCLQNLM.js} +71 -4
- package/dist/{chunk-3HYSWEKG.js → chunk-U6P7HBUY.js} +52 -13
- package/dist/{chunk-MKWVJYUS.js → chunk-UJEHDVQO.js} +63 -12
- package/dist/{chunk-DJRYQQXB.js → chunk-YKBVTYU4.js} +1 -1
- package/dist/hooks/index.d.ts +5 -1
- package/dist/hooks/index.js +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.js +6 -6
- package/dist/json-view/index.js +2 -2
- package/dist/layers/index.js +1 -1
- package/dist/markdown/index.d.ts +3 -31
- package/dist/markdown/index.js +1 -1
- package/dist/page/index.d.ts +11 -2
- package/dist/page/index.js +3 -3
- package/dist/player/index.d.ts +15 -2
- package/dist/player/index.js +5 -5
- package/dist/squisq-player.full.global.js +466 -438
- package/dist/squisq-player.global.js +88 -60
- package/dist/standalone-source.js +1 -1
- package/dist/styles/index.css +36 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,6 +57,25 @@ state rather than crashing.
|
|
|
57
57
|
| `MediaClipLayer` | Hidden `<audio>`/`<video>` elements for timed media clips |
|
|
58
58
|
| `JsonView` | Read-only viewer for JSON values bound to a Squisq-annotated schema |
|
|
59
59
|
|
|
60
|
+
### Fenced-code copy control
|
|
61
|
+
|
|
62
|
+
`MarkdownRenderer` and the linear document surfaces keep code-copy UI off by
|
|
63
|
+
default. Opt in with `showCodeCopyButton`. Web hosts can rely on
|
|
64
|
+
`navigator.clipboard`; Electron or native embeddings can provide their own
|
|
65
|
+
clipboard bridge:
|
|
66
|
+
|
|
67
|
+
````tsx
|
|
68
|
+
<LinearDocView
|
|
69
|
+
markdown={'```\n$ node packages/tooling/dist/cli.mjs components\n```'}
|
|
70
|
+
showCodeCopyButton
|
|
71
|
+
onCopyCode={(code, { language }) => hostClipboard.writeText(code)}
|
|
72
|
+
/>
|
|
73
|
+
````
|
|
74
|
+
|
|
75
|
+
The same two props are available on `DocPlayer` (for linear mode), and on the
|
|
76
|
+
standalone static `mount()` options. The callback receives the exact fence
|
|
77
|
+
contents, without the backtick delimiters.
|
|
78
|
+
|
|
60
79
|
## Layers
|
|
61
80
|
|
|
62
81
|
Blocks are composed of typed layers rendered as SVG:
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { Theme } from '@bendyline/squisq/schemas';
|
|
3
|
+
import { MarkdownBlockNode, HtmlPolicy } from '@bendyline/squisq/markdown';
|
|
4
|
+
|
|
5
|
+
interface CodeBlockCopyContext {
|
|
6
|
+
/** Authored fence language, when one was supplied. */
|
|
7
|
+
language?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Host-owned clipboard adapter for fenced code blocks.
|
|
11
|
+
*
|
|
12
|
+
* The callback runs directly from the button's click handler so Electron
|
|
13
|
+
* bridges and browser Clipboard APIs retain their user-gesture context.
|
|
14
|
+
*/
|
|
15
|
+
type CodeBlockCopyHandler = (code: string, context: CodeBlockCopyContext) => void | Promise<void>;
|
|
16
|
+
interface MarkdownRendererProps {
|
|
17
|
+
/** Block-level AST nodes to render */
|
|
18
|
+
nodes: MarkdownBlockNode[];
|
|
19
|
+
/** Optional CSS class for the wrapper element */
|
|
20
|
+
className?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Raw HTML policy. Defaults to `sanitize`, which removes unsafe tags,
|
|
23
|
+
* event handlers, and executable URL schemes before rendering.
|
|
24
|
+
*/
|
|
25
|
+
htmlPolicy?: HtmlPolicy;
|
|
26
|
+
/**
|
|
27
|
+
* Extra URL schemes to allow on links (e.g. a host app's internal
|
|
28
|
+
* navigation scheme it intercepts on click). Executable schemes are
|
|
29
|
+
* never allowed regardless. See {@link SanitizeUrlOptions}.
|
|
30
|
+
*/
|
|
31
|
+
linkSchemes?: readonly string[];
|
|
32
|
+
/** Resolved Squisq theme inherited by embedded Mermaid diagrams. */
|
|
33
|
+
theme?: Theme;
|
|
34
|
+
/** Show a subtle Copy button on ordinary fenced code blocks (default: false). */
|
|
35
|
+
showCodeCopyButton?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Optional host clipboard adapter. When omitted, enabled copy buttons use
|
|
38
|
+
* `navigator.clipboard.writeText`. Supply this for Electron, native shells,
|
|
39
|
+
* or hosts with their own clipboard permission/error handling.
|
|
40
|
+
*/
|
|
41
|
+
onCopyCode?: CodeBlockCopyHandler;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Renders MarkdownBlockNode[] AST as React HTML elements.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <MarkdownRenderer nodes={block.contents} />
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function MarkdownRenderer({ nodes, className, htmlPolicy, linkSchemes, theme, showCodeCopyButton, onCopyCode, }: MarkdownRendererProps): react_jsx_runtime.JSX.Element | null;
|
|
52
|
+
|
|
53
|
+
export { type CodeBlockCopyHandler as C, MarkdownRenderer as M, type CodeBlockCopyContext as a, type MarkdownRendererProps as b };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BlockRenderer,
|
|
3
3
|
LinearDocView
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-U6P7HBUY.js";
|
|
5
5
|
import {
|
|
6
6
|
useAudioSync,
|
|
7
7
|
useDocPlayback,
|
|
8
8
|
useMediaSchedule,
|
|
9
9
|
useViewportOrientation
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-MVJQL2W2.js";
|
|
11
11
|
import {
|
|
12
12
|
useAutoSurface
|
|
13
13
|
} from "./chunk-TT6ENR6T.js";
|
|
@@ -1243,7 +1243,10 @@ function DocControlsSlideshow({
|
|
|
1243
1243
|
}
|
|
1244
1244
|
|
|
1245
1245
|
// src/docPlayer/playerAppearance.ts
|
|
1246
|
-
import {
|
|
1246
|
+
import {
|
|
1247
|
+
resolveCoverSlideSettings,
|
|
1248
|
+
resolveThemeForDoc
|
|
1249
|
+
} from "@bendyline/squisq/doc";
|
|
1247
1250
|
function readFrontmatterSetting(frontmatter, canonical, legacy) {
|
|
1248
1251
|
if (!frontmatter) return void 0;
|
|
1249
1252
|
return Object.prototype.hasOwnProperty.call(frontmatter, canonical) ? frontmatter[canonical] : frontmatter[legacy];
|
|
@@ -1291,20 +1294,14 @@ function resolvePipPosition(value) {
|
|
|
1291
1294
|
};
|
|
1292
1295
|
return aliases[normalized];
|
|
1293
1296
|
}
|
|
1294
|
-
function resolveBoolean(value) {
|
|
1295
|
-
if (typeof value === "boolean") return value;
|
|
1296
|
-
if (typeof value !== "string") return void 0;
|
|
1297
|
-
const normalized = value.trim().toLowerCase();
|
|
1298
|
-
if (normalized === "true" || normalized === "yes" || normalized === "on" || normalized === "show" || normalized === "visible") {
|
|
1299
|
-
return true;
|
|
1300
|
-
}
|
|
1301
|
-
if (normalized === "false" || normalized === "no" || normalized === "off" || normalized === "hide" || normalized === "hidden") {
|
|
1302
|
-
return false;
|
|
1303
|
-
}
|
|
1304
|
-
return void 0;
|
|
1305
|
-
}
|
|
1306
1297
|
function resolveDocPlayerAppearance(doc, overrides = {}) {
|
|
1307
1298
|
const frontmatter = doc.frontmatter;
|
|
1299
|
+
const cover = resolveCoverSlideSettings(frontmatter, {
|
|
1300
|
+
...overrides.showCoverSlide !== void 0 ? { enabled: overrides.showCoverSlide } : {},
|
|
1301
|
+
...overrides.coverSlideTemplate !== void 0 ? { template: overrides.coverSlideTemplate } : {},
|
|
1302
|
+
...overrides.coverSlideDuration !== void 0 ? { duration: overrides.coverSlideDuration } : {},
|
|
1303
|
+
...overrides.coverSlidePlayback !== void 0 ? { playback: overrides.coverSlidePlayback } : {}
|
|
1304
|
+
});
|
|
1308
1305
|
return {
|
|
1309
1306
|
theme: overrides.theme ?? resolveThemeForDoc(doc),
|
|
1310
1307
|
videoPresentation: overrides.videoPresentation ?? resolveVideoPresentation(
|
|
@@ -1315,7 +1312,10 @@ function resolveDocPlayerAppearance(doc, overrides = {}) {
|
|
|
1315
1312
|
pipPosition: overrides.pipPosition ?? resolvePipPosition(
|
|
1316
1313
|
readFrontmatterSetting(frontmatter, "squisq-pip-position", "pip-position")
|
|
1317
1314
|
) ?? "bottom-right",
|
|
1318
|
-
showCoverSlide:
|
|
1315
|
+
showCoverSlide: cover.enabled,
|
|
1316
|
+
coverSlideTemplate: cover.template,
|
|
1317
|
+
coverSlideDuration: cover.duration,
|
|
1318
|
+
coverSlidePlayback: cover.playback
|
|
1319
1319
|
};
|
|
1320
1320
|
}
|
|
1321
1321
|
|
|
@@ -1567,17 +1567,36 @@ function isVisiblyPresented(video) {
|
|
|
1567
1567
|
}
|
|
1568
1568
|
return true;
|
|
1569
1569
|
}
|
|
1570
|
+
function createCaptureWatchdog(ownerDocument, timeoutMs, expire) {
|
|
1571
|
+
let timeout = null;
|
|
1572
|
+
const clear = () => {
|
|
1573
|
+
if (timeout === null) return;
|
|
1574
|
+
clearTimeout(timeout);
|
|
1575
|
+
timeout = null;
|
|
1576
|
+
};
|
|
1577
|
+
return {
|
|
1578
|
+
arm: () => {
|
|
1579
|
+
clear();
|
|
1580
|
+
if (ownerDocument.visibilityState !== "visible") return;
|
|
1581
|
+
timeout = setTimeout(expire, timeoutMs);
|
|
1582
|
+
},
|
|
1583
|
+
clear
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1570
1586
|
function seekVideoByAssignment(video, targetTime, timeoutMs = DEFAULT_VIDEO_FRAME_TIMEOUT_MS) {
|
|
1571
1587
|
video.pause();
|
|
1572
1588
|
return new Promise((resolve, reject) => {
|
|
1573
1589
|
let settled = false;
|
|
1574
1590
|
let videoFrameRequest = null;
|
|
1591
|
+
const ownerDocument = video.ownerDocument;
|
|
1592
|
+
const isInactive = () => ownerDocument.visibilityState !== "visible";
|
|
1575
1593
|
const cleanup = () => {
|
|
1576
|
-
|
|
1594
|
+
watchdog.clear();
|
|
1577
1595
|
clearInterval(readinessPoll);
|
|
1578
1596
|
video.removeEventListener("seeked", handleMediaReady);
|
|
1579
1597
|
video.removeEventListener("loadeddata", handleMediaReady);
|
|
1580
1598
|
video.removeEventListener("canplay", handleMediaReady);
|
|
1599
|
+
ownerDocument.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
1581
1600
|
if (videoFrameRequest !== null && typeof video.cancelVideoFrameCallback === "function") {
|
|
1582
1601
|
video.cancelVideoFrameCallback(videoFrameRequest);
|
|
1583
1602
|
}
|
|
@@ -1615,34 +1634,48 @@ function seekVideoByAssignment(video, targetTime, timeoutMs = DEFAULT_VIDEO_FRAM
|
|
|
1615
1634
|
function handleMediaReady() {
|
|
1616
1635
|
requestPresentedFrame();
|
|
1617
1636
|
}
|
|
1618
|
-
const
|
|
1637
|
+
const watchdog = createCaptureWatchdog(ownerDocument, timeoutMs, fail);
|
|
1638
|
+
const assignTargetTime = () => {
|
|
1639
|
+
try {
|
|
1640
|
+
video.currentTime = reachableMediaTime(video, targetTime);
|
|
1641
|
+
queueMicrotask(requestPresentedFrame);
|
|
1642
|
+
return true;
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
settled = true;
|
|
1645
|
+
cleanup();
|
|
1646
|
+
reject(error);
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
function handleVisibilityChange() {
|
|
1651
|
+
if (settled) return;
|
|
1652
|
+
if (isInactive()) {
|
|
1653
|
+
watchdog.clear();
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
requestPresentedFrame();
|
|
1657
|
+
if (settled) return;
|
|
1658
|
+
if (!video.seeking && !isAtReachableMediaTime(video, targetTime) && !assignTargetTime()) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
watchdog.arm();
|
|
1662
|
+
}
|
|
1619
1663
|
const readinessPoll = setInterval(requestPresentedFrame, VIDEO_FRAME_READINESS_POLL_MS);
|
|
1620
1664
|
video.addEventListener("seeked", handleMediaReady);
|
|
1621
1665
|
video.addEventListener("loadeddata", handleMediaReady);
|
|
1622
1666
|
video.addEventListener("canplay", handleMediaReady);
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
} catch (error) {
|
|
1627
|
-
settled = true;
|
|
1628
|
-
cleanup();
|
|
1629
|
-
reject(error);
|
|
1630
|
-
}
|
|
1667
|
+
ownerDocument.addEventListener("visibilitychange", handleVisibilityChange);
|
|
1668
|
+
watchdog.arm();
|
|
1669
|
+
assignTargetTime();
|
|
1631
1670
|
});
|
|
1632
1671
|
}
|
|
1633
1672
|
function advanceSequentialCaptureVideo(video, targetTime, timeoutMs) {
|
|
1634
1673
|
return new Promise((resolve, reject) => {
|
|
1635
1674
|
let settled = false;
|
|
1636
|
-
let timeout = null;
|
|
1637
1675
|
const ownerDocument = video.ownerDocument;
|
|
1638
1676
|
const isInactive = () => ownerDocument.visibilityState !== "visible";
|
|
1639
|
-
const clearWatchdog = () => {
|
|
1640
|
-
if (timeout === null) return;
|
|
1641
|
-
clearTimeout(timeout);
|
|
1642
|
-
timeout = null;
|
|
1643
|
-
};
|
|
1644
1677
|
const cleanup = () => {
|
|
1645
|
-
|
|
1678
|
+
watchdog.clear();
|
|
1646
1679
|
clearInterval(readinessPoll);
|
|
1647
1680
|
video.removeEventListener("timeupdate", check);
|
|
1648
1681
|
video.removeEventListener("loadeddata", check);
|
|
@@ -1696,21 +1729,17 @@ function advanceSequentialCaptureVideo(video, targetTime, timeoutMs) {
|
|
|
1696
1729
|
handlePlaybackFailure();
|
|
1697
1730
|
}
|
|
1698
1731
|
};
|
|
1699
|
-
const
|
|
1700
|
-
clearWatchdog();
|
|
1701
|
-
if (settled || isInactive()) return;
|
|
1702
|
-
timeout = setTimeout(fail, timeoutMs);
|
|
1703
|
-
};
|
|
1732
|
+
const watchdog = createCaptureWatchdog(ownerDocument, timeoutMs, fail);
|
|
1704
1733
|
const pauseWhileInactive = () => {
|
|
1705
1734
|
video.pause();
|
|
1706
|
-
|
|
1735
|
+
watchdog.clear();
|
|
1707
1736
|
};
|
|
1708
1737
|
const resumeWhenActive = () => {
|
|
1709
1738
|
if (settled || isInactive()) return;
|
|
1710
1739
|
check();
|
|
1711
1740
|
if (settled) return;
|
|
1712
1741
|
startPlayback();
|
|
1713
|
-
|
|
1742
|
+
watchdog.arm();
|
|
1714
1743
|
};
|
|
1715
1744
|
function handleVisibilityChange() {
|
|
1716
1745
|
if (ownerDocument.visibilityState !== "visible") {
|
|
@@ -1818,6 +1847,9 @@ function DocPlayerContent({
|
|
|
1818
1847
|
forceViewport,
|
|
1819
1848
|
displayMode = "video",
|
|
1820
1849
|
showCoverSlide,
|
|
1850
|
+
coverSlideTemplate,
|
|
1851
|
+
coverSlideDuration,
|
|
1852
|
+
coverSlidePlayback,
|
|
1821
1853
|
coverVisible,
|
|
1822
1854
|
theme,
|
|
1823
1855
|
surface,
|
|
@@ -1827,7 +1859,9 @@ function DocPlayerContent({
|
|
|
1827
1859
|
pipShape,
|
|
1828
1860
|
pipPosition,
|
|
1829
1861
|
enableSwipe = true,
|
|
1830
|
-
globalKeyboardShortcuts = false
|
|
1862
|
+
globalKeyboardShortcuts = false,
|
|
1863
|
+
showCodeCopyButton = false,
|
|
1864
|
+
onCopyCode
|
|
1831
1865
|
}) {
|
|
1832
1866
|
const isSlideshowMode = displayMode === "slideshow";
|
|
1833
1867
|
const isLinearMode = displayMode === "linear";
|
|
@@ -1844,12 +1878,17 @@ function DocPlayerContent({
|
|
|
1844
1878
|
const params = new URLSearchParams(window.location.search);
|
|
1845
1879
|
return params.get("debug") === "true";
|
|
1846
1880
|
}, []);
|
|
1881
|
+
const syntheticDuration = useMemo3(
|
|
1882
|
+
() => audioMode === "synthetic" ? getDocPlaybackDuration(doc) : 0,
|
|
1883
|
+
[audioMode, doc]
|
|
1884
|
+
);
|
|
1847
1885
|
const internalAudio = useAudioSync(
|
|
1848
1886
|
audioRef,
|
|
1849
1887
|
doc.audio,
|
|
1850
1888
|
basePath,
|
|
1851
1889
|
!externalAudioController,
|
|
1852
|
-
audioMode
|
|
1890
|
+
audioMode,
|
|
1891
|
+
syntheticDuration
|
|
1853
1892
|
);
|
|
1854
1893
|
const audio = externalAudioController || internalAudio;
|
|
1855
1894
|
useEffect5(() => {
|
|
@@ -1973,9 +2012,23 @@ function DocPlayerContent({
|
|
|
1973
2012
|
pipSize,
|
|
1974
2013
|
pipShape,
|
|
1975
2014
|
pipPosition,
|
|
1976
|
-
showCoverSlide
|
|
2015
|
+
showCoverSlide,
|
|
2016
|
+
coverSlideTemplate,
|
|
2017
|
+
coverSlideDuration,
|
|
2018
|
+
coverSlidePlayback
|
|
1977
2019
|
}),
|
|
1978
|
-
[
|
|
2020
|
+
[
|
|
2021
|
+
doc,
|
|
2022
|
+
theme,
|
|
2023
|
+
videoPresentation,
|
|
2024
|
+
pipSize,
|
|
2025
|
+
pipShape,
|
|
2026
|
+
pipPosition,
|
|
2027
|
+
showCoverSlide,
|
|
2028
|
+
coverSlideTemplate,
|
|
2029
|
+
coverSlideDuration,
|
|
2030
|
+
coverSlidePlayback
|
|
2031
|
+
]
|
|
1979
2032
|
);
|
|
1980
2033
|
const effectiveTheme = useMemo3(() => {
|
|
1981
2034
|
const base = appearance.theme;
|
|
@@ -2018,7 +2071,7 @@ function DocPlayerContent({
|
|
|
2018
2071
|
if (!appearance.showCoverSlide) return null;
|
|
2019
2072
|
if (!startBlockConfig) return null;
|
|
2020
2073
|
const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
|
|
2021
|
-
const layers = expandCoverBlock(startBlockConfig, context);
|
|
2074
|
+
const layers = expandCoverBlock(startBlockConfig, context, appearance.coverSlideTemplate);
|
|
2022
2075
|
return {
|
|
2023
2076
|
id: "cover-block",
|
|
2024
2077
|
startTime: -1,
|
|
@@ -2028,7 +2081,13 @@ function DocPlayerContent({
|
|
|
2028
2081
|
audioSegment: -1,
|
|
2029
2082
|
layers
|
|
2030
2083
|
};
|
|
2031
|
-
}, [
|
|
2084
|
+
}, [
|
|
2085
|
+
doc.startBlock,
|
|
2086
|
+
activeViewport,
|
|
2087
|
+
effectiveTheme,
|
|
2088
|
+
appearance.showCoverSlide,
|
|
2089
|
+
appearance.coverSlideTemplate
|
|
2090
|
+
]);
|
|
2032
2091
|
const hasManagedCover = !!coverBlock;
|
|
2033
2092
|
const [slideshowCoverVisible, setSlideshowCoverVisible] = useState5(false);
|
|
2034
2093
|
const [isSlideshowPickerOpen, setIsSlideshowPickerOpen] = useState5(false);
|
|
@@ -2066,9 +2125,12 @@ function DocPlayerContent({
|
|
|
2066
2125
|
coverWasShowing.current = false;
|
|
2067
2126
|
hasPlayedOnce.current = true;
|
|
2068
2127
|
setCoverGraceActive(true);
|
|
2069
|
-
coverGraceTimer.current = setTimeout(
|
|
2128
|
+
coverGraceTimer.current = setTimeout(
|
|
2129
|
+
() => setCoverGraceActive(false),
|
|
2130
|
+
appearance.coverSlideDuration * 1e3
|
|
2131
|
+
);
|
|
2070
2132
|
}
|
|
2071
|
-
}, [isPlaying, coverBlock, renderMode, isSlideshowMode]);
|
|
2133
|
+
}, [isPlaying, coverBlock, renderMode, isSlideshowMode, appearance.coverSlideDuration]);
|
|
2072
2134
|
useEffect5(() => () => clearTimeout(coverGraceTimer.current), []);
|
|
2073
2135
|
useEffect5(() => () => clearTimeout(tapFeedbackTimer.current), []);
|
|
2074
2136
|
const showVideoCoverBlock = !isSlideshowMode && !isLinearMode && !!coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
|
|
@@ -2605,7 +2667,9 @@ function DocPlayerContent({
|
|
|
2605
2667
|
viewport: activeViewport,
|
|
2606
2668
|
theme,
|
|
2607
2669
|
surface,
|
|
2608
|
-
animationsEnabled
|
|
2670
|
+
animationsEnabled,
|
|
2671
|
+
showCodeCopyButton,
|
|
2672
|
+
onCopyCode
|
|
2609
2673
|
}
|
|
2610
2674
|
)
|
|
2611
2675
|
}
|
|
@@ -16,7 +16,7 @@ function useMediaSchedule(schedule, currentTime) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// src/hooks/useAudioSync.ts
|
|
19
|
-
import { useState, useEffect, useRef, useCallback } from "react";
|
|
19
|
+
import { useState, useEffect, useMemo as useMemo2, useRef, useCallback } from "react";
|
|
20
20
|
import { fetchResourceBytes, isResourceUrlAllowed } from "@bendyline/squisq/markdown";
|
|
21
21
|
|
|
22
22
|
// src/hooks/AudioController.ts
|
|
@@ -61,7 +61,14 @@ function resolveAudioUrl(src, basePath) {
|
|
|
61
61
|
if (!basePath) return src;
|
|
62
62
|
return `${basePath.replace(/\/$/, "")}/${src.replace(/^\//, "")}`;
|
|
63
63
|
}
|
|
64
|
-
function useAudioSync(audioRef,
|
|
64
|
+
function useAudioSync(audioRef, track, basePath = "", enabled = true, mode = "media", syntheticDuration = 0) {
|
|
65
|
+
const audioTrack = useMemo2(() => {
|
|
66
|
+
if (mode !== "synthetic" || track?.segments?.length || syntheticDuration <= 0) return track;
|
|
67
|
+
return {
|
|
68
|
+
...track,
|
|
69
|
+
segments: [{ src: "", name: "synthetic", duration: syntheticDuration, startTime: 0 }]
|
|
70
|
+
};
|
|
71
|
+
}, [track, mode, syntheticDuration]);
|
|
65
72
|
const resourcePolicy = useResourcePolicy();
|
|
66
73
|
const [currentTime, setCurrentTime] = useState(0);
|
|
67
74
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
@@ -468,7 +475,7 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true, mode
|
|
|
468
475
|
}
|
|
469
476
|
|
|
470
477
|
// src/hooks/useDocPlayback.ts
|
|
471
|
-
import { useMemo as
|
|
478
|
+
import { useMemo as useMemo3, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
472
479
|
import {
|
|
473
480
|
DEFAULT_THEME,
|
|
474
481
|
getBlockAtTime,
|
|
@@ -489,7 +496,7 @@ function useDocPlayback(script, currentTime, options = {}) {
|
|
|
489
496
|
onSeek,
|
|
490
497
|
useAudioSegmentTiming = true
|
|
491
498
|
} = options;
|
|
492
|
-
const blocks =
|
|
499
|
+
const blocks = useMemo3(() => {
|
|
493
500
|
if (!script?.blocks) {
|
|
494
501
|
return [];
|
|
495
502
|
}
|
|
@@ -532,20 +539,20 @@ function useDocPlayback(script, currentTime, options = {}) {
|
|
|
532
539
|
theme,
|
|
533
540
|
useAudioSegmentTiming
|
|
534
541
|
]);
|
|
535
|
-
const currentBlock =
|
|
536
|
-
const currentBlockIndex =
|
|
542
|
+
const currentBlock = useMemo3(() => getBlockAtTime(blocks, currentTime), [blocks, currentTime]);
|
|
543
|
+
const currentBlockIndex = useMemo3(
|
|
537
544
|
() => currentBlock ? blocks.indexOf(currentBlock) : -1,
|
|
538
545
|
[blocks, currentBlock]
|
|
539
546
|
);
|
|
540
|
-
const blockTime =
|
|
547
|
+
const blockTime = useMemo3(() => {
|
|
541
548
|
if (!currentBlock) return 0;
|
|
542
549
|
return Math.max(0, currentTime - currentBlock.startTime);
|
|
543
550
|
}, [currentBlock, currentTime]);
|
|
544
|
-
const blockProgress =
|
|
551
|
+
const blockProgress = useMemo3(() => {
|
|
545
552
|
if (!currentBlock || currentBlock.duration === 0) return 0;
|
|
546
553
|
return Math.min(1, blockTime / currentBlock.duration);
|
|
547
554
|
}, [currentBlock, blockTime]);
|
|
548
|
-
const docProgress =
|
|
555
|
+
const docProgress = useMemo3(() => {
|
|
549
556
|
if (!script || script.duration === 0) return 0;
|
|
550
557
|
return Math.min(1, currentTime / script.duration);
|
|
551
558
|
}, [script, currentTime]);
|
|
@@ -612,7 +619,7 @@ function useDocPlayback(script, currentTime, options = {}) {
|
|
|
612
619
|
}
|
|
613
620
|
|
|
614
621
|
// src/hooks/useViewportOrientation.ts
|
|
615
|
-
import { useState as useState2, useEffect as useEffect2, useMemo as
|
|
622
|
+
import { useState as useState2, useEffect as useEffect2, useMemo as useMemo4 } from "react";
|
|
616
623
|
import {
|
|
617
624
|
VIEWPORT_PRESETS as VIEWPORT_PRESETS2
|
|
618
625
|
} from "@bendyline/squisq/doc";
|
|
@@ -661,11 +668,11 @@ function useViewportOrientation() {
|
|
|
661
668
|
clearTimeout(timeoutId);
|
|
662
669
|
};
|
|
663
670
|
}, []);
|
|
664
|
-
const orientation =
|
|
671
|
+
const orientation = useMemo4(
|
|
665
672
|
() => getOrientationFromWindow(windowSize.width, windowSize.height),
|
|
666
673
|
[windowSize.width, windowSize.height]
|
|
667
674
|
);
|
|
668
|
-
const viewport =
|
|
675
|
+
const viewport = useMemo4(() => getViewportForOrientation(orientation), [orientation]);
|
|
669
676
|
return {
|
|
670
677
|
viewport,
|
|
671
678
|
orientation,
|
|
@@ -50,13 +50,72 @@ function InlineAudioPlayer({
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// src/MarkdownRenderer.tsx
|
|
53
|
-
import { Fragment } from "react";
|
|
53
|
+
import { Fragment, useEffect, useRef, useState } from "react";
|
|
54
54
|
import {
|
|
55
55
|
sanitizeHtmlNodes,
|
|
56
56
|
sanitizeUrl
|
|
57
57
|
} from "@bendyline/squisq/markdown";
|
|
58
58
|
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
59
59
|
var DEFAULT_CTX = { htmlPolicy: "sanitize" };
|
|
60
|
+
var CODE_COPY_LABELS = {
|
|
61
|
+
idle: "Copy",
|
|
62
|
+
copying: "Copying\u2026",
|
|
63
|
+
copied: "Copied",
|
|
64
|
+
failed: "Copy failed"
|
|
65
|
+
};
|
|
66
|
+
async function writeCodeToClipboard(code, language, onCopyCode) {
|
|
67
|
+
if (onCopyCode) {
|
|
68
|
+
await onCopyCode(code, language === void 0 ? {} : { language });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
|
72
|
+
throw new Error("Clipboard access is unavailable in this host");
|
|
73
|
+
}
|
|
74
|
+
await navigator.clipboard.writeText(code);
|
|
75
|
+
}
|
|
76
|
+
function CodeBlock({
|
|
77
|
+
value,
|
|
78
|
+
language,
|
|
79
|
+
ctx
|
|
80
|
+
}) {
|
|
81
|
+
const [status, setStatus] = useState("idle");
|
|
82
|
+
const resetTimer = useRef();
|
|
83
|
+
useEffect(
|
|
84
|
+
() => () => {
|
|
85
|
+
if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
|
|
86
|
+
},
|
|
87
|
+
[]
|
|
88
|
+
);
|
|
89
|
+
const pre = /* @__PURE__ */ jsx3("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx3("code", { className: language ? `language-${language}` : void 0, children: value }) });
|
|
90
|
+
if (!ctx.showCodeCopyButton) return pre;
|
|
91
|
+
const handleCopy = async () => {
|
|
92
|
+
if (status === "copying") return;
|
|
93
|
+
setStatus("copying");
|
|
94
|
+
try {
|
|
95
|
+
await writeCodeToClipboard(value, language ?? void 0, ctx.onCopyCode);
|
|
96
|
+
setStatus("copied");
|
|
97
|
+
} catch {
|
|
98
|
+
setStatus("failed");
|
|
99
|
+
}
|
|
100
|
+
if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
|
|
101
|
+
resetTimer.current = setTimeout(() => setStatus("idle"), 1600);
|
|
102
|
+
};
|
|
103
|
+
return /* @__PURE__ */ jsxs("div", { className: "squisq-md-code-frame", children: [
|
|
104
|
+
pre,
|
|
105
|
+
/* @__PURE__ */ jsx3(
|
|
106
|
+
"button",
|
|
107
|
+
{
|
|
108
|
+
type: "button",
|
|
109
|
+
className: "squisq-md-code-copy",
|
|
110
|
+
"data-copy-state": status,
|
|
111
|
+
disabled: status === "copying",
|
|
112
|
+
onClick: () => void handleCopy(),
|
|
113
|
+
"aria-label": "Copy code to clipboard",
|
|
114
|
+
children: CODE_COPY_LABELS[status]
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
] });
|
|
118
|
+
}
|
|
60
119
|
function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
|
|
61
120
|
return nodes.map((node, i) => {
|
|
62
121
|
const key = `${keyPrefix}i${i}`;
|
|
@@ -181,7 +240,7 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
|
|
|
181
240
|
key
|
|
182
241
|
);
|
|
183
242
|
}
|
|
184
|
-
return /* @__PURE__ */ jsx3(
|
|
243
|
+
return /* @__PURE__ */ jsx3(CodeBlock, { value: node.value, language: node.lang, ctx }, key);
|
|
185
244
|
case "thematicBreak":
|
|
186
245
|
return /* @__PURE__ */ jsx3("hr", { className: "squisq-md-hr" }, key);
|
|
187
246
|
case "table":
|
|
@@ -393,10 +452,18 @@ function MarkdownRenderer({
|
|
|
393
452
|
className,
|
|
394
453
|
htmlPolicy = "sanitize",
|
|
395
454
|
linkSchemes,
|
|
396
|
-
theme
|
|
455
|
+
theme,
|
|
456
|
+
showCodeCopyButton = false,
|
|
457
|
+
onCopyCode
|
|
397
458
|
}) {
|
|
398
459
|
if (!nodes || nodes.length === 0) return null;
|
|
399
|
-
return /* @__PURE__ */ jsx3("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", {
|
|
460
|
+
return /* @__PURE__ */ jsx3("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", {
|
|
461
|
+
htmlPolicy,
|
|
462
|
+
linkSchemes,
|
|
463
|
+
theme,
|
|
464
|
+
showCodeCopyButton,
|
|
465
|
+
onCopyCode
|
|
466
|
+
}) });
|
|
400
467
|
}
|
|
401
468
|
|
|
402
469
|
export {
|