@bendyline/squisq-react 2.5.0 → 2.7.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-CC9dOc6b.d.ts +62 -0
- package/dist/{chunk-ZKNX3EJI.js → chunk-DYAFB3AH.js} +17 -5
- package/dist/{chunk-VMPQEUJH.js → chunk-ESI3P77P.js} +124 -6
- package/dist/{chunk-BOZJ655L.js → chunk-MVJQL2W2.js} +19 -12
- package/dist/{chunk-DJRYQQXB.js → chunk-NJRSGSFW.js} +68 -2
- package/dist/{chunk-THDXCSPC.js → chunk-U3HRSQ45.js} +84 -14
- package/dist/hooks/index.d.ts +5 -1
- package/dist/hooks/index.js +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +13 -7
- package/dist/json-view/index.d.ts +58 -1
- package/dist/json-view/index.js +6 -4
- package/dist/markdown/index.d.ts +10 -31
- package/dist/markdown/index.js +7 -3
- package/dist/page/index.d.ts +30 -2
- package/dist/page/index.js +2 -2
- package/dist/player/index.d.ts +15 -1
- package/dist/player/index.js +4 -4
- package/dist/squisq-player.full.global.js +445 -418
- package/dist/squisq-player.global.js +87 -60
- package/dist/standalone-source.js +1 -1
- package/dist/styles/index.css +66 -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,62 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { Theme } from '@bendyline/squisq/schemas';
|
|
3
|
+
import { FenceRendererMap } from '@bendyline/squisq/fence';
|
|
4
|
+
import { MarkdownBlockNode, HtmlPolicy } from '@bendyline/squisq/markdown';
|
|
5
|
+
|
|
6
|
+
interface CodeBlockCopyContext {
|
|
7
|
+
/** Authored fence language, when one was supplied. */
|
|
8
|
+
language?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Host-owned clipboard adapter for fenced code blocks.
|
|
12
|
+
*
|
|
13
|
+
* The callback runs directly from the button's click handler so Electron
|
|
14
|
+
* bridges and browser Clipboard APIs retain their user-gesture context.
|
|
15
|
+
*/
|
|
16
|
+
type CodeBlockCopyHandler = (code: string, context: CodeBlockCopyContext) => void | Promise<void>;
|
|
17
|
+
interface MarkdownRendererProps {
|
|
18
|
+
/** Block-level AST nodes to render */
|
|
19
|
+
nodes: MarkdownBlockNode[];
|
|
20
|
+
/** Optional CSS class for the wrapper element */
|
|
21
|
+
className?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Raw HTML policy. Defaults to `sanitize`, which removes unsafe tags,
|
|
24
|
+
* event handlers, and executable URL schemes before rendering.
|
|
25
|
+
*/
|
|
26
|
+
htmlPolicy?: HtmlPolicy;
|
|
27
|
+
/**
|
|
28
|
+
* Extra URL schemes to allow on links (e.g. a host app's internal
|
|
29
|
+
* navigation scheme it intercepts on click). Executable schemes are
|
|
30
|
+
* never allowed regardless. See {@link SanitizeUrlOptions}.
|
|
31
|
+
*/
|
|
32
|
+
linkSchemes?: readonly string[];
|
|
33
|
+
/** Resolved Squisq theme inherited by embedded Mermaid diagrams. */
|
|
34
|
+
theme?: Theme;
|
|
35
|
+
/** Show a subtle Copy button on ordinary fenced code blocks (default: false). */
|
|
36
|
+
showCodeCopyButton?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Optional host clipboard adapter. When omitted, enabled copy buttons use
|
|
39
|
+
* `navigator.clipboard.writeText`. Supply this for Electron, native shells,
|
|
40
|
+
* or hosts with their own clipboard permission/error handling.
|
|
41
|
+
*/
|
|
42
|
+
onCopyCode?: CodeBlockCopyHandler;
|
|
43
|
+
/**
|
|
44
|
+
* Host fence-renderer registry (`@bendyline/squisq/fence`): fenced code
|
|
45
|
+
* blocks whose language is claimed render through the host's widget
|
|
46
|
+
* instead of the default code block. Wins over the built-in mermaid
|
|
47
|
+
* handling and over an ambient `FenceRendererContext`; a renderer that
|
|
48
|
+
* throws falls back to the plain code block via an error boundary.
|
|
49
|
+
*/
|
|
50
|
+
fenceRenderers?: FenceRendererMap;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Renders MarkdownBlockNode[] AST as React HTML elements.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <MarkdownRenderer nodes={block.contents} />
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
declare function MarkdownRenderer({ nodes, className, htmlPolicy, linkSchemes, theme, showCodeCopyButton, onCopyCode, fenceRenderers, }: MarkdownRendererProps): react_jsx_runtime.JSX.Element | null;
|
|
61
|
+
|
|
62
|
+
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-U3HRSQ45.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";
|
|
@@ -1859,7 +1859,10 @@ function DocPlayerContent({
|
|
|
1859
1859
|
pipShape,
|
|
1860
1860
|
pipPosition,
|
|
1861
1861
|
enableSwipe = true,
|
|
1862
|
-
globalKeyboardShortcuts = false
|
|
1862
|
+
globalKeyboardShortcuts = false,
|
|
1863
|
+
showCodeCopyButton = false,
|
|
1864
|
+
onCopyCode,
|
|
1865
|
+
fenceRenderers
|
|
1863
1866
|
}) {
|
|
1864
1867
|
const isSlideshowMode = displayMode === "slideshow";
|
|
1865
1868
|
const isLinearMode = displayMode === "linear";
|
|
@@ -1876,12 +1879,17 @@ function DocPlayerContent({
|
|
|
1876
1879
|
const params = new URLSearchParams(window.location.search);
|
|
1877
1880
|
return params.get("debug") === "true";
|
|
1878
1881
|
}, []);
|
|
1882
|
+
const syntheticDuration = useMemo3(
|
|
1883
|
+
() => audioMode === "synthetic" ? getDocPlaybackDuration(doc) : 0,
|
|
1884
|
+
[audioMode, doc]
|
|
1885
|
+
);
|
|
1879
1886
|
const internalAudio = useAudioSync(
|
|
1880
1887
|
audioRef,
|
|
1881
1888
|
doc.audio,
|
|
1882
1889
|
basePath,
|
|
1883
1890
|
!externalAudioController,
|
|
1884
|
-
audioMode
|
|
1891
|
+
audioMode,
|
|
1892
|
+
syntheticDuration
|
|
1885
1893
|
);
|
|
1886
1894
|
const audio = externalAudioController || internalAudio;
|
|
1887
1895
|
useEffect5(() => {
|
|
@@ -2660,7 +2668,10 @@ function DocPlayerContent({
|
|
|
2660
2668
|
viewport: activeViewport,
|
|
2661
2669
|
theme,
|
|
2662
2670
|
surface,
|
|
2663
|
-
animationsEnabled
|
|
2671
|
+
animationsEnabled,
|
|
2672
|
+
showCodeCopyButton,
|
|
2673
|
+
onCopyCode,
|
|
2674
|
+
fenceRenderers
|
|
2664
2675
|
}
|
|
2665
2676
|
)
|
|
2666
2677
|
}
|
|
@@ -2675,6 +2686,7 @@ function DocPlayerContent({
|
|
|
2675
2686
|
"data-playback-state": isPlaying ? "playing" : "paused",
|
|
2676
2687
|
"data-swipe-phase": swipe.phase,
|
|
2677
2688
|
tabIndex: renderMode ? -1 : 0,
|
|
2689
|
+
role: "region",
|
|
2678
2690
|
"aria-label": "Document player",
|
|
2679
2691
|
onKeyDown: renderMode ? void 0 : handleKeyDown,
|
|
2680
2692
|
className: `doc-player${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
|
|
@@ -5,6 +5,13 @@ import {
|
|
|
5
5
|
useMediaUrl
|
|
6
6
|
} from "./chunk-LR3AIGDD.js";
|
|
7
7
|
|
|
8
|
+
// src/hooks/FenceRendererContext.tsx
|
|
9
|
+
import { createContext, useContext } from "react";
|
|
10
|
+
var FenceRendererContext = createContext(null);
|
|
11
|
+
function useFenceRenderers() {
|
|
12
|
+
return useContext(FenceRendererContext);
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
// src/InlineVideoPlayer.tsx
|
|
9
16
|
import { jsx } from "react/jsx-runtime";
|
|
10
17
|
function InlineVideoPlayer({
|
|
@@ -50,13 +57,99 @@ function InlineAudioPlayer({
|
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
// src/MarkdownRenderer.tsx
|
|
53
|
-
import { Fragment } from "react";
|
|
60
|
+
import { Component, Fragment, useEffect, useRef, useState } from "react";
|
|
54
61
|
import {
|
|
55
62
|
sanitizeHtmlNodes,
|
|
56
63
|
sanitizeUrl
|
|
57
64
|
} from "@bendyline/squisq/markdown";
|
|
58
65
|
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
59
66
|
var DEFAULT_CTX = { htmlPolicy: "sanitize" };
|
|
67
|
+
var CODE_COPY_LABELS = {
|
|
68
|
+
idle: "Copy",
|
|
69
|
+
copying: "Copying\u2026",
|
|
70
|
+
copied: "Copied",
|
|
71
|
+
failed: "Copy failed"
|
|
72
|
+
};
|
|
73
|
+
async function writeCodeToClipboard(code, language, onCopyCode) {
|
|
74
|
+
if (onCopyCode) {
|
|
75
|
+
await onCopyCode(code, language === void 0 ? {} : { language });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
|
79
|
+
throw new Error("Clipboard access is unavailable in this host");
|
|
80
|
+
}
|
|
81
|
+
await navigator.clipboard.writeText(code);
|
|
82
|
+
}
|
|
83
|
+
function CodeBlock({
|
|
84
|
+
value,
|
|
85
|
+
language,
|
|
86
|
+
ctx
|
|
87
|
+
}) {
|
|
88
|
+
const [status, setStatus] = useState("idle");
|
|
89
|
+
const resetTimer = useRef();
|
|
90
|
+
useEffect(
|
|
91
|
+
() => () => {
|
|
92
|
+
if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
|
|
93
|
+
},
|
|
94
|
+
[]
|
|
95
|
+
);
|
|
96
|
+
const pre = /* @__PURE__ */ jsx3("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx3("code", { className: language ? `language-${language}` : void 0, children: value }) });
|
|
97
|
+
if (!ctx.showCodeCopyButton) return pre;
|
|
98
|
+
const handleCopy = async () => {
|
|
99
|
+
if (status === "copying") return;
|
|
100
|
+
setStatus("copying");
|
|
101
|
+
try {
|
|
102
|
+
await writeCodeToClipboard(value, language ?? void 0, ctx.onCopyCode);
|
|
103
|
+
setStatus("copied");
|
|
104
|
+
} catch {
|
|
105
|
+
setStatus("failed");
|
|
106
|
+
}
|
|
107
|
+
if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
|
|
108
|
+
resetTimer.current = setTimeout(() => setStatus("idle"), 1600);
|
|
109
|
+
};
|
|
110
|
+
return /* @__PURE__ */ jsxs("div", { className: "squisq-md-code-frame", children: [
|
|
111
|
+
pre,
|
|
112
|
+
/* @__PURE__ */ jsx3(
|
|
113
|
+
"button",
|
|
114
|
+
{
|
|
115
|
+
type: "button",
|
|
116
|
+
className: "squisq-md-code-copy",
|
|
117
|
+
"data-copy-state": status,
|
|
118
|
+
disabled: status === "copying",
|
|
119
|
+
onClick: () => void handleCopy(),
|
|
120
|
+
"aria-label": "Copy code to clipboard",
|
|
121
|
+
children: CODE_COPY_LABELS[status]
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
] });
|
|
125
|
+
}
|
|
126
|
+
var FenceWidgetBoundary = class extends Component {
|
|
127
|
+
constructor() {
|
|
128
|
+
super(...arguments);
|
|
129
|
+
this.state = { failed: false };
|
|
130
|
+
}
|
|
131
|
+
static getDerivedStateFromError() {
|
|
132
|
+
return { failed: true };
|
|
133
|
+
}
|
|
134
|
+
render() {
|
|
135
|
+
return this.state.failed ? this.props.fallback : this.props.children;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
function HostFence({
|
|
139
|
+
node,
|
|
140
|
+
lang,
|
|
141
|
+
ctx
|
|
142
|
+
}) {
|
|
143
|
+
const renderer = ctx.fenceRenderers?.[lang];
|
|
144
|
+
if (!renderer) return null;
|
|
145
|
+
return /* @__PURE__ */ jsx3("div", { className: `squisq-md-fence-widget squisq-md-fence-widget-${lang}`, "data-fence-lang": lang, children: renderer({
|
|
146
|
+
lang,
|
|
147
|
+
...node.meta != null ? { meta: node.meta } : {},
|
|
148
|
+
value: node.value,
|
|
149
|
+
...ctx.theme ? { theme: ctx.theme } : {},
|
|
150
|
+
mode: "read"
|
|
151
|
+
}) });
|
|
152
|
+
}
|
|
60
153
|
function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
|
|
61
154
|
return nodes.map((node, i) => {
|
|
62
155
|
const key = `${keyPrefix}i${i}`;
|
|
@@ -168,8 +261,19 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
|
|
|
168
261
|
return /* @__PURE__ */ jsx3("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);
|
|
169
262
|
}
|
|
170
263
|
return /* @__PURE__ */ jsx3("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
|
|
171
|
-
case "code":
|
|
172
|
-
|
|
264
|
+
case "code": {
|
|
265
|
+
const fenceLang = node.lang?.trim().toLowerCase();
|
|
266
|
+
if (fenceLang && ctx.fenceRenderers?.[fenceLang]) {
|
|
267
|
+
return /* @__PURE__ */ jsx3(
|
|
268
|
+
FenceWidgetBoundary,
|
|
269
|
+
{
|
|
270
|
+
fallback: /* @__PURE__ */ jsx3(CodeBlock, { value: node.value, language: node.lang, ctx }),
|
|
271
|
+
children: /* @__PURE__ */ jsx3(HostFence, { node, lang: fenceLang, ctx })
|
|
272
|
+
},
|
|
273
|
+
key
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (fenceLang === "mermaid") {
|
|
173
277
|
return /* @__PURE__ */ jsx3(
|
|
174
278
|
MermaidDiagram,
|
|
175
279
|
{
|
|
@@ -181,7 +285,8 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
|
|
|
181
285
|
key
|
|
182
286
|
);
|
|
183
287
|
}
|
|
184
|
-
return /* @__PURE__ */ jsx3(
|
|
288
|
+
return /* @__PURE__ */ jsx3(CodeBlock, { value: node.value, language: node.lang, ctx }, key);
|
|
289
|
+
}
|
|
185
290
|
case "thematicBreak":
|
|
186
291
|
return /* @__PURE__ */ jsx3("hr", { className: "squisq-md-hr" }, key);
|
|
187
292
|
case "table":
|
|
@@ -393,13 +498,26 @@ function MarkdownRenderer({
|
|
|
393
498
|
className,
|
|
394
499
|
htmlPolicy = "sanitize",
|
|
395
500
|
linkSchemes,
|
|
396
|
-
theme
|
|
501
|
+
theme,
|
|
502
|
+
showCodeCopyButton = false,
|
|
503
|
+
onCopyCode,
|
|
504
|
+
fenceRenderers
|
|
397
505
|
}) {
|
|
506
|
+
const ambientFenceRenderers = useFenceRenderers();
|
|
398
507
|
if (!nodes || nodes.length === 0) return null;
|
|
399
|
-
return /* @__PURE__ */ jsx3("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", {
|
|
508
|
+
return /* @__PURE__ */ jsx3("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", {
|
|
509
|
+
htmlPolicy,
|
|
510
|
+
linkSchemes,
|
|
511
|
+
theme,
|
|
512
|
+
showCodeCopyButton,
|
|
513
|
+
onCopyCode,
|
|
514
|
+
fenceRenderers: fenceRenderers ?? ambientFenceRenderers ?? void 0
|
|
515
|
+
}) });
|
|
400
516
|
}
|
|
401
517
|
|
|
402
518
|
export {
|
|
519
|
+
FenceRendererContext,
|
|
520
|
+
useFenceRenderers,
|
|
403
521
|
InlineVideoPlayer,
|
|
404
522
|
InlineAudioPlayer,
|
|
405
523
|
MarkdownRenderer
|
|
@@ -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,
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-TT6ENR6T.js";
|
|
4
4
|
import {
|
|
5
5
|
MarkdownRenderer
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ESI3P77P.js";
|
|
7
7
|
|
|
8
8
|
// src/jsonView/useJsonViewTokens.ts
|
|
9
9
|
import { useMemo } from "react";
|
|
@@ -325,6 +325,72 @@ function JsonView(props) {
|
|
|
325
325
|
) });
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
// src/jsonView/jsonFormFenceRenderer.tsx
|
|
329
|
+
import { inferSchema } from "@bendyline/squisq/jsonForm";
|
|
330
|
+
import { parseYamlSubset } from "@bendyline/squisq/doc";
|
|
331
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
332
|
+
function parseFenceBody(value) {
|
|
333
|
+
const trimmed = value.trim();
|
|
334
|
+
if (trimmed.startsWith("{")) {
|
|
335
|
+
try {
|
|
336
|
+
const parsed = JSON.parse(trimmed);
|
|
337
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
338
|
+
return { error: "fence body must be a top-level object" };
|
|
339
|
+
}
|
|
340
|
+
return { data: parsed };
|
|
341
|
+
} catch (err) {
|
|
342
|
+
return { error: `invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
return { data: parseYamlSubset(value) };
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return { error: `invalid YAML: ${err instanceof Error ? err.message : String(err)}` };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function JsonFormFence({
|
|
352
|
+
ctx,
|
|
353
|
+
options
|
|
354
|
+
}) {
|
|
355
|
+
const parsed = ctx.data !== void 0 ? { data: ctx.data } : parseFenceBody(ctx.value);
|
|
356
|
+
const data = parsed.data;
|
|
357
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
358
|
+
return /* @__PURE__ */ jsxs2("div", { className: "squisq-json-fence squisq-json-fence--invalid", children: [
|
|
359
|
+
/* @__PURE__ */ jsx4("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx4("code", { children: ctx.value }) }),
|
|
360
|
+
"error" in parsed && parsed.error && /* @__PURE__ */ jsx4("div", { className: "squisq-json-fence-error", children: parsed.error })
|
|
361
|
+
] });
|
|
362
|
+
}
|
|
363
|
+
const schema = options.schema ?? inferSchema(data);
|
|
364
|
+
const actions = options.actions ?? [];
|
|
365
|
+
return /* @__PURE__ */ jsxs2("div", { className: `squisq-json-fence${options.className ? ` ${options.className}` : ""}`, children: [
|
|
366
|
+
/* @__PURE__ */ jsx4(
|
|
367
|
+
JsonView,
|
|
368
|
+
{
|
|
369
|
+
schema,
|
|
370
|
+
value: data,
|
|
371
|
+
theme: ctx.theme,
|
|
372
|
+
density: options.density ?? "compact"
|
|
373
|
+
}
|
|
374
|
+
),
|
|
375
|
+
actions.length > 0 && options.onAction && /* @__PURE__ */ jsx4("div", { className: "squisq-json-fence-actions", role: "group", children: actions.map((action) => /* @__PURE__ */ jsx4(
|
|
376
|
+
"button",
|
|
377
|
+
{
|
|
378
|
+
type: "button",
|
|
379
|
+
className: `squisq-json-fence-action${action.variant === "primary" ? " squisq-json-fence-action--primary" : ""}`,
|
|
380
|
+
onClick: () => void options.onAction?.(action.id, data, ctx),
|
|
381
|
+
children: action.label
|
|
382
|
+
},
|
|
383
|
+
action.id
|
|
384
|
+
)) })
|
|
385
|
+
] });
|
|
386
|
+
}
|
|
387
|
+
function createJsonFormFenceRenderer(options = {}) {
|
|
388
|
+
return function jsonFormFenceRenderer(ctx) {
|
|
389
|
+
return /* @__PURE__ */ jsx4(JsonFormFence, { ctx, options });
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
328
393
|
export {
|
|
329
|
-
JsonView
|
|
394
|
+
JsonView,
|
|
395
|
+
createJsonFormFenceRenderer
|
|
330
396
|
};
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
InlineVideoPlayer,
|
|
18
18
|
MarkdownRenderer
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-ESI3P77P.js";
|
|
20
20
|
import {
|
|
21
21
|
useMediaUrl
|
|
22
22
|
} from "./chunk-LR3AIGDD.js";
|
|
@@ -165,7 +165,8 @@ var defaultValue = {
|
|
|
165
165
|
viewport: VIEWPORT_PRESETS.landscape,
|
|
166
166
|
renderContext: { theme: DEFAULT_THEME },
|
|
167
167
|
animationsEnabled: true,
|
|
168
|
-
imageDisplayMode: "inline"
|
|
168
|
+
imageDisplayMode: "inline",
|
|
169
|
+
showCodeCopyButton: false
|
|
169
170
|
};
|
|
170
171
|
var PageViewContext = createContext(defaultValue);
|
|
171
172
|
function usePageView() {
|
|
@@ -438,11 +439,22 @@ function CardGridSection({ section }) {
|
|
|
438
439
|
] });
|
|
439
440
|
}
|
|
440
441
|
function ItemListSection({ section }) {
|
|
441
|
-
const { theme } = usePageView();
|
|
442
|
+
const { theme, showCodeCopyButton, onCopyCode, fenceRenderers, htmlPolicy, linkSchemes } = usePageView();
|
|
442
443
|
const items = section.slots.items ?? [];
|
|
443
444
|
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
444
445
|
section.slots.title && /* @__PURE__ */ jsx3("h2", { className: "squisq-page-section-title squisq-page-items-title", children: section.slots.title }),
|
|
445
|
-
/* @__PURE__ */ jsx3("ol", { className: "squisq-page-items", children: items.map((item, i) => /* @__PURE__ */ jsx3("li", { children: item.markdown ? /* @__PURE__ */ jsx3(
|
|
446
|
+
/* @__PURE__ */ jsx3("ol", { className: "squisq-page-items", children: items.map((item, i) => /* @__PURE__ */ jsx3("li", { children: item.markdown ? /* @__PURE__ */ jsx3(
|
|
447
|
+
MarkdownRenderer,
|
|
448
|
+
{
|
|
449
|
+
nodes: item.markdown,
|
|
450
|
+
theme,
|
|
451
|
+
showCodeCopyButton,
|
|
452
|
+
onCopyCode,
|
|
453
|
+
fenceRenderers,
|
|
454
|
+
htmlPolicy,
|
|
455
|
+
linkSchemes
|
|
456
|
+
}
|
|
457
|
+
) : item.body }, i)) })
|
|
446
458
|
] });
|
|
447
459
|
}
|
|
448
460
|
function TimelineRailSection({ section }) {
|
|
@@ -470,12 +482,33 @@ function TableSection({ section }) {
|
|
|
470
482
|
] });
|
|
471
483
|
}
|
|
472
484
|
function ProseSection({ section, block }) {
|
|
473
|
-
const { theme } = usePageView();
|
|
474
|
-
const heading = section.slots.title !== void 0 && block?.sourceHeading ? /* @__PURE__ */ jsx3(
|
|
485
|
+
const { theme, showCodeCopyButton, onCopyCode, fenceRenderers, htmlPolicy, linkSchemes } = usePageView();
|
|
486
|
+
const heading = section.slots.title !== void 0 && block?.sourceHeading ? /* @__PURE__ */ jsx3(
|
|
487
|
+
MarkdownRenderer,
|
|
488
|
+
{
|
|
489
|
+
nodes: [block.sourceHeading],
|
|
490
|
+
theme,
|
|
491
|
+
showCodeCopyButton,
|
|
492
|
+
onCopyCode,
|
|
493
|
+
htmlPolicy,
|
|
494
|
+
linkSchemes
|
|
495
|
+
}
|
|
496
|
+
) : null;
|
|
475
497
|
const bodyNodes = section.slots.body?.markdown ?? block?.contents;
|
|
476
498
|
return /* @__PURE__ */ jsxs2("div", { className: "squisq-page-prose", children: [
|
|
477
499
|
heading,
|
|
478
|
-
bodyNodes && bodyNodes.length > 0 && /* @__PURE__ */ jsx3(
|
|
500
|
+
bodyNodes && bodyNodes.length > 0 && /* @__PURE__ */ jsx3(
|
|
501
|
+
MarkdownRenderer,
|
|
502
|
+
{
|
|
503
|
+
nodes: bodyNodes,
|
|
504
|
+
theme,
|
|
505
|
+
showCodeCopyButton,
|
|
506
|
+
onCopyCode,
|
|
507
|
+
fenceRenderers,
|
|
508
|
+
htmlPolicy,
|
|
509
|
+
linkSchemes
|
|
510
|
+
}
|
|
511
|
+
)
|
|
479
512
|
] });
|
|
480
513
|
}
|
|
481
514
|
function FooterSection({ section }) {
|
|
@@ -537,7 +570,15 @@ function sectionBody(entry, featureFlip) {
|
|
|
537
570
|
}
|
|
538
571
|
}
|
|
539
572
|
function PageSectionView({ entry, featureFlip, isLeadProse }) {
|
|
540
|
-
const {
|
|
573
|
+
const {
|
|
574
|
+
pageStyle,
|
|
575
|
+
theme,
|
|
576
|
+
showCodeCopyButton,
|
|
577
|
+
onCopyCode,
|
|
578
|
+
fenceRenderers,
|
|
579
|
+
htmlPolicy,
|
|
580
|
+
linkSchemes
|
|
581
|
+
} = usePageView();
|
|
541
582
|
const { section } = entry;
|
|
542
583
|
const richContent = section.slots.richContent?.markdown;
|
|
543
584
|
const classes = [
|
|
@@ -566,7 +607,18 @@ function PageSectionView({ entry, featureFlip, isLeadProse }) {
|
|
|
566
607
|
...hintAttrs,
|
|
567
608
|
children: /* @__PURE__ */ jsxs3("div", { className: "squisq-page-section-inner", children: [
|
|
568
609
|
sectionBody(entry, featureFlip),
|
|
569
|
-
richContent && richContent.length > 0 && /* @__PURE__ */ jsx4("div", { className: "squisq-page-rich-content", children: /* @__PURE__ */ jsx4(
|
|
610
|
+
richContent && richContent.length > 0 && /* @__PURE__ */ jsx4("div", { className: "squisq-page-rich-content", children: /* @__PURE__ */ jsx4(
|
|
611
|
+
MarkdownRenderer,
|
|
612
|
+
{
|
|
613
|
+
nodes: richContent,
|
|
614
|
+
theme,
|
|
615
|
+
showCodeCopyButton,
|
|
616
|
+
onCopyCode,
|
|
617
|
+
fenceRenderers,
|
|
618
|
+
htmlPolicy,
|
|
619
|
+
linkSchemes
|
|
620
|
+
}
|
|
621
|
+
) })
|
|
570
622
|
] })
|
|
571
623
|
}
|
|
572
624
|
);
|
|
@@ -589,6 +641,7 @@ import {
|
|
|
589
641
|
DEFAULT_THEME as DEFAULT_THEME2
|
|
590
642
|
} from "@bendyline/squisq/doc";
|
|
591
643
|
import { parseMarkdown } from "@bendyline/squisq/markdown";
|
|
644
|
+
import { fenceRendererLangs } from "@bendyline/squisq/fence";
|
|
592
645
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
593
646
|
function LinearDocView({
|
|
594
647
|
doc,
|
|
@@ -603,7 +656,12 @@ function LinearDocView({
|
|
|
603
656
|
imageDisplayMode = "inline",
|
|
604
657
|
globalKeyboardShortcuts = false,
|
|
605
658
|
showCover = true,
|
|
606
|
-
transformPage
|
|
659
|
+
transformPage,
|
|
660
|
+
showCodeCopyButton = false,
|
|
661
|
+
onCopyCode,
|
|
662
|
+
fenceRenderers,
|
|
663
|
+
htmlPolicy,
|
|
664
|
+
linkSchemes
|
|
607
665
|
}) {
|
|
608
666
|
const scrollRef = useRef(null);
|
|
609
667
|
const activeViewport = viewport ?? VIEWPORT_PRESETS2.landscape;
|
|
@@ -622,6 +680,7 @@ function LinearDocView({
|
|
|
622
680
|
() => resolvePageStyle2(activeTheme, transformPage),
|
|
623
681
|
[activeTheme, transformPage]
|
|
624
682
|
);
|
|
683
|
+
const widgetFenceLangs = useMemo2(() => fenceRendererLangs(fenceRenderers), [fenceRenderers]);
|
|
625
684
|
const sections = useMemo2(() => {
|
|
626
685
|
if (!resolvedDoc) return [];
|
|
627
686
|
return materializePageSections(resolvedDoc, {
|
|
@@ -629,9 +688,10 @@ function LinearDocView({
|
|
|
629
688
|
viewport: activeViewport,
|
|
630
689
|
customTemplates: resolvedDoc.customTemplates,
|
|
631
690
|
cover: showCover !== false ? resolvedDoc.startBlock : false,
|
|
632
|
-
transformPage
|
|
691
|
+
transformPage,
|
|
692
|
+
...widgetFenceLangs.length > 0 ? { widgetFenceLangs } : {}
|
|
633
693
|
});
|
|
634
|
-
}, [resolvedDoc, activeTheme, activeViewport, showCover, transformPage]);
|
|
694
|
+
}, [resolvedDoc, activeTheme, activeViewport, showCover, transformPage, widgetFenceLangs]);
|
|
635
695
|
const renderContext = useMemo2(
|
|
636
696
|
() => ({
|
|
637
697
|
theme: activeTheme,
|
|
@@ -650,7 +710,12 @@ function LinearDocView({
|
|
|
650
710
|
viewport: activeViewport,
|
|
651
711
|
renderContext,
|
|
652
712
|
animationsEnabled,
|
|
653
|
-
imageDisplayMode
|
|
713
|
+
imageDisplayMode,
|
|
714
|
+
showCodeCopyButton,
|
|
715
|
+
onCopyCode,
|
|
716
|
+
fenceRenderers,
|
|
717
|
+
htmlPolicy,
|
|
718
|
+
linkSchemes
|
|
654
719
|
}),
|
|
655
720
|
[
|
|
656
721
|
activeTheme,
|
|
@@ -659,7 +724,12 @@ function LinearDocView({
|
|
|
659
724
|
activeViewport,
|
|
660
725
|
renderContext,
|
|
661
726
|
animationsEnabled,
|
|
662
|
-
imageDisplayMode
|
|
727
|
+
imageDisplayMode,
|
|
728
|
+
showCodeCopyButton,
|
|
729
|
+
onCopyCode,
|
|
730
|
+
fenceRenderers,
|
|
731
|
+
htmlPolicy,
|
|
732
|
+
linkSchemes
|
|
663
733
|
]
|
|
664
734
|
);
|
|
665
735
|
const { featureOrdinals, leadProseIndex } = useMemo2(() => {
|