@bendyline/squisq-react 1.1.2 → 1.3.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/dist/DocPlayer.d.ts +9 -2
- package/dist/DocPlayer.d.ts.map +1 -1
- package/dist/DocPlayer.js +27 -8
- package/dist/DocPlayer.js.map +1 -1
- package/dist/InlineAudioPlayer.d.ts +15 -0
- package/dist/InlineAudioPlayer.d.ts.map +1 -0
- package/dist/InlineAudioPlayer.js +22 -0
- package/dist/InlineAudioPlayer.js.map +1 -0
- package/dist/InlineVideoPlayer.d.ts +21 -0
- package/dist/InlineVideoPlayer.d.ts.map +1 -0
- package/dist/InlineVideoPlayer.js +29 -0
- package/dist/InlineVideoPlayer.js.map +1 -0
- package/dist/LinearDocView.d.ts +28 -2
- package/dist/LinearDocView.d.ts.map +1 -1
- package/dist/LinearDocView.js +159 -24
- package/dist/LinearDocView.js.map +1 -1
- package/dist/MarkdownRenderer.d.ts.map +1 -1
- package/dist/MarkdownRenderer.js +123 -4
- package/dist/MarkdownRenderer.js.map +1 -1
- package/dist/SocialCaptionOverlay.d.ts.map +1 -1
- package/dist/SocialCaptionOverlay.js +3 -2
- package/dist/SocialCaptionOverlay.js.map +1 -1
- package/dist/__tests__/JsonView.test.d.ts +2 -0
- package/dist/__tests__/JsonView.test.d.ts.map +1 -0
- package/dist/__tests__/JsonView.test.js +93 -0
- package/dist/__tests__/JsonView.test.js.map +1 -0
- package/dist/__tests__/LinearDocView.test.js +90 -0
- package/dist/__tests__/LinearDocView.test.js.map +1 -1
- package/dist/hooks/MediaContext.d.ts.map +1 -1
- package/dist/hooks/MediaContext.js +13 -7
- package/dist/hooks/MediaContext.js.map +1 -1
- package/dist/hooks/useAutoSurface.d.ts +11 -0
- package/dist/hooks/useAutoSurface.d.ts.map +1 -0
- package/dist/hooks/useAutoSurface.js +24 -0
- package/dist/hooks/useAutoSurface.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonView/JsonView.d.ts +26 -0
- package/dist/jsonView/JsonView.d.ts.map +1 -0
- package/dist/jsonView/JsonView.js +12 -0
- package/dist/jsonView/JsonView.js.map +1 -0
- package/dist/jsonView/RenderNode.d.ts +22 -0
- package/dist/jsonView/RenderNode.d.ts.map +1 -0
- package/dist/jsonView/RenderNode.js +30 -0
- package/dist/jsonView/RenderNode.js.map +1 -0
- package/dist/jsonView/index.d.ts +3 -0
- package/dist/jsonView/index.d.ts.map +1 -0
- package/dist/jsonView/index.js +2 -0
- package/dist/jsonView/index.js.map +1 -0
- package/dist/jsonView/useJsonViewTokens.d.ts +14 -0
- package/dist/jsonView/useJsonViewTokens.d.ts.map +1 -0
- package/dist/jsonView/useJsonViewTokens.js +34 -0
- package/dist/jsonView/useJsonViewTokens.js.map +1 -0
- package/dist/jsonView/viewers.d.ts +30 -0
- package/dist/jsonView/viewers.d.ts.map +1 -0
- package/dist/jsonView/viewers.js +226 -0
- package/dist/jsonView/viewers.js.map +1 -0
- package/dist/squisq-player.css +1 -1
- package/dist/squisq-player.css.map +1 -1
- package/dist/squisq-player.global.js +32 -8
- package/dist/squisq-player.global.js.map +1 -1
- package/dist/standalone-source.js +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +8 -7
- package/src/DocPlayer.tsx +44 -8
- package/src/InlineAudioPlayer.tsx +46 -0
- package/src/InlineVideoPlayer.tsx +70 -0
- package/src/LinearDocView.tsx +198 -26
- package/src/MarkdownRenderer.tsx +189 -12
- package/src/SocialCaptionOverlay.tsx +3 -2
- package/src/__tests__/JsonView.test.tsx +111 -0
- package/src/__tests__/LinearDocView.test.tsx +101 -0
- package/src/hooks/MediaContext.tsx +15 -8
- package/src/hooks/useAutoSurface.ts +33 -0
- package/src/index.ts +10 -0
- package/src/jsonView/JsonView.tsx +51 -0
- package/src/jsonView/RenderNode.tsx +51 -0
- package/src/jsonView/index.ts +2 -0
- package/src/jsonView/json-view.css +206 -0
- package/src/jsonView/useJsonViewTokens.ts +57 -0
- package/src/jsonView/viewers.tsx +343 -0
- package/src/styles/doc-animations.css +36 -0
- package/src/styles/index.css +7 -0
- package/src/types.ts +5 -1
package/dist/types.d.ts
CHANGED
|
@@ -26,8 +26,12 @@ export type ControlsLayout = 'overlay' | 'sidebar' | 'bottom';
|
|
|
26
26
|
* - `'linear'` — Long-scrolling document view. Renders the full markdown as
|
|
27
27
|
* readable HTML with template-annotated sections shown as inline SVG cards.
|
|
28
28
|
* No audio, no timeline, no controls — just a scrollable page.
|
|
29
|
+
* - `'page'` — Plain semantic HTML preview matching what the
|
|
30
|
+
* `markdownDocToPlainHtml` export produces. No SquisqPlayer, no SVG
|
|
31
|
+
* cards — just `<h1>`/`<p>`/`<ul>` etc. inside a sandboxed iframe.
|
|
32
|
+
* Use when you want a WYSIWYG view of the simple HTML export.
|
|
29
33
|
*/
|
|
30
|
-
export type DisplayMode = 'video' | 'slideshow' | 'linear';
|
|
34
|
+
export type DisplayMode = 'video' | 'slideshow' | 'linear' | 'page';
|
|
31
35
|
/**
|
|
32
36
|
* Caption display style.
|
|
33
37
|
*
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAEvD,0CAA0C;AAC1C,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE9D
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA8JH,6CAA6C;AAC7C,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACtC,OAAO,GAAG,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACvD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "React component library for doc playback, block rendering, and media layers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -31,20 +31,21 @@
|
|
|
31
31
|
],
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
36
|
},
|
|
37
|
-
"./styles": "./src/styles/
|
|
37
|
+
"./styles": "./src/styles/index.css",
|
|
38
38
|
"./standalone": "./dist/squisq-player.global.js",
|
|
39
39
|
"./standalone-source": {
|
|
40
|
-
"
|
|
41
|
-
"
|
|
40
|
+
"types": "./dist/standalone-source.d.ts",
|
|
41
|
+
"import": "./dist/standalone-source.js"
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsup && tsup --config tsup.standalone.config.ts && node scripts/generate-standalone-source.mjs",
|
|
46
46
|
"build:esm": "tsup",
|
|
47
47
|
"build:standalone": "tsup --config tsup.standalone.config.ts && node scripts/generate-standalone-source.mjs",
|
|
48
|
+
"dev": "npm run build:standalone && concurrently -n js,dts -c blue,gray -r \"tsup --watch --no-dts --no-clean\" \"tsup --watch --dts-only --no-clean\"",
|
|
48
49
|
"typecheck": "tsc --noEmit"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@bendyline/squisq": "1.
|
|
56
|
+
"@bendyline/squisq": "1.4.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/react": "18.3.28",
|
package/src/DocPlayer.tsx
CHANGED
|
@@ -25,9 +25,11 @@
|
|
|
25
25
|
import { Fragment, useRef, useState, useEffect, useCallback, useMemo } from 'react';
|
|
26
26
|
import type { Doc, Block, TextLayer, StartBlockConfig, DocBlock } from '@bendyline/squisq/schemas';
|
|
27
27
|
import { isTemplateBlock, getCaptionAtTime } from '@bendyline/squisq/schemas';
|
|
28
|
-
import type { Theme } from '@bendyline/squisq/schemas';
|
|
28
|
+
import type { SurfaceScheme, Theme } from '@bendyline/squisq/schemas';
|
|
29
|
+
import { applySurface } from '@bendyline/squisq/schemas';
|
|
29
30
|
import { BlockRenderer } from './BlockRenderer';
|
|
30
31
|
import { CaptionOverlay } from './CaptionOverlay';
|
|
32
|
+
import { useAutoSurface } from './hooks/useAutoSurface';
|
|
31
33
|
import { useAudioSync } from './hooks/useAudioSync';
|
|
32
34
|
import { useDocPlayback } from './hooks/useDocPlayback';
|
|
33
35
|
import { useViewportOrientation } from './hooks/useViewportOrientation';
|
|
@@ -162,6 +164,13 @@ interface DocPlayerProps {
|
|
|
162
164
|
forceViewport?: ViewportConfig;
|
|
163
165
|
/** Theme to use for rendering (default: DEFAULT_THEME from the theme library) */
|
|
164
166
|
theme?: Theme;
|
|
167
|
+
/**
|
|
168
|
+
* Optional surface scheme (light / dark paper) overlaid on top of the
|
|
169
|
+
* theme's colors. Passed through to the underlying LinearDocView when
|
|
170
|
+
* `displayMode === 'linear'`; otherwise overlaid onto the theme that
|
|
171
|
+
* renders the player's SVG blocks.
|
|
172
|
+
*/
|
|
173
|
+
surface?: SurfaceScheme | 'auto';
|
|
165
174
|
/**
|
|
166
175
|
* Display mode for the player.
|
|
167
176
|
* - `'video'` (default) — Traditional video playback with play/pause, scrub bar, auto-advance.
|
|
@@ -197,6 +206,7 @@ export function DocPlayer({
|
|
|
197
206
|
forceViewport,
|
|
198
207
|
displayMode = 'video',
|
|
199
208
|
theme,
|
|
209
|
+
surface,
|
|
200
210
|
captionStyle = 'standard',
|
|
201
211
|
}: DocPlayerProps) {
|
|
202
212
|
const isSlideshowMode = displayMode === 'slideshow';
|
|
@@ -275,6 +285,15 @@ export function DocPlayer({
|
|
|
275
285
|
[renderMode, toggle, isPlaying, isSlideshowMode, isLinearMode],
|
|
276
286
|
);
|
|
277
287
|
|
|
288
|
+
// Resolve surface (light/dark paper) and apply it to the theme before
|
|
289
|
+
// handing off to downstream renderers. Orthogonal to the editorial theme.
|
|
290
|
+
const autoSurface = useAutoSurface(surface === 'auto');
|
|
291
|
+
const resolvedSurface = surface === 'auto' ? autoSurface : surface;
|
|
292
|
+
const effectiveTheme = useMemo(() => {
|
|
293
|
+
const base = theme ?? DEFAULT_THEME;
|
|
294
|
+
return resolvedSurface ? applySurface(base, resolvedSurface) : base;
|
|
295
|
+
}, [theme, resolvedSurface]);
|
|
296
|
+
|
|
278
297
|
// Doc playback hook - pass viewport for responsive template expansion
|
|
279
298
|
const {
|
|
280
299
|
currentBlock,
|
|
@@ -288,14 +307,14 @@ export function DocPlayer({
|
|
|
288
307
|
nextBlock: _nextBlock,
|
|
289
308
|
prevBlock: _prevBlock,
|
|
290
309
|
blocks: expandedBlocks,
|
|
291
|
-
} = useDocPlayback(script, currentTime, activeViewport, renderMode,
|
|
310
|
+
} = useDocPlayback(script, currentTime, activeViewport, renderMode, effectiveTheme);
|
|
292
311
|
|
|
293
312
|
// Expand cover block (startBlock) if present - uses active viewport
|
|
294
313
|
const coverBlock = useMemo((): Block | null => {
|
|
295
314
|
const startBlockConfig = script.startBlock as StartBlockConfig | undefined;
|
|
296
315
|
if (!startBlockConfig) return null;
|
|
297
316
|
|
|
298
|
-
const context = createTemplateContext(
|
|
317
|
+
const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
|
|
299
318
|
const layers = expandCoverBlock(startBlockConfig, context);
|
|
300
319
|
|
|
301
320
|
return {
|
|
@@ -305,7 +324,7 @@ export function DocPlayer({
|
|
|
305
324
|
audioSegment: -1,
|
|
306
325
|
layers,
|
|
307
326
|
};
|
|
308
|
-
}, [script.startBlock, activeViewport,
|
|
327
|
+
}, [script.startBlock, activeViewport, effectiveTheme]);
|
|
309
328
|
|
|
310
329
|
// Render-mode cover block control: allows Playwright to force-show the cover block
|
|
311
330
|
const [coverForced, setCoverForced] = useState(false);
|
|
@@ -314,14 +333,25 @@ export function DocPlayer({
|
|
|
314
333
|
const [coverGraceActive, setCoverGraceActive] = useState(false);
|
|
315
334
|
const coverGraceTimer = useRef<ReturnType<typeof setTimeout>>();
|
|
316
335
|
const coverWasShowing = useRef(false);
|
|
336
|
+
// Track whether playback has ever been initiated — prevents the cover block
|
|
337
|
+
// from re-appearing when paused at currentTime === 0 (e.g., no audio source).
|
|
338
|
+
const hasPlayedOnce = useRef(false);
|
|
317
339
|
|
|
318
340
|
// Track when cover is showing at rest (before play)
|
|
319
|
-
const atRest = !!(
|
|
341
|
+
const atRest = !!(
|
|
342
|
+
coverBlock &&
|
|
343
|
+
!isPlaying &&
|
|
344
|
+
currentTime === 0 &&
|
|
345
|
+
!hasPlayedOnce.current &&
|
|
346
|
+
!renderMode &&
|
|
347
|
+
!autoPlay
|
|
348
|
+
);
|
|
320
349
|
if (atRest) coverWasShowing.current = true;
|
|
321
350
|
|
|
322
351
|
useEffect(() => {
|
|
323
352
|
if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
|
|
324
353
|
coverWasShowing.current = false;
|
|
354
|
+
hasPlayedOnce.current = true;
|
|
325
355
|
setCoverGraceActive(true);
|
|
326
356
|
// Intentionally no cleanup: if coverBlock's memoized reference changes
|
|
327
357
|
// mid-grace (e.g., due to a preview re-render), clearing the timer would
|
|
@@ -344,7 +374,7 @@ export function DocPlayer({
|
|
|
344
374
|
coverBlock &&
|
|
345
375
|
(coverForced ||
|
|
346
376
|
coverGraceActive ||
|
|
347
|
-
(!isPlaying && currentTime === 0 && !renderMode && !autoPlay));
|
|
377
|
+
(!isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay));
|
|
348
378
|
|
|
349
379
|
// Auto-play if enabled (wait for audio to be ready)
|
|
350
380
|
// Use a ref to track if we've already auto-played to avoid repeating on every render
|
|
@@ -785,7 +815,13 @@ export function DocPlayer({
|
|
|
785
815
|
overflow: 'hidden',
|
|
786
816
|
}}
|
|
787
817
|
>
|
|
788
|
-
<LinearDocView
|
|
818
|
+
<LinearDocView
|
|
819
|
+
doc={script}
|
|
820
|
+
basePath={basePath}
|
|
821
|
+
viewport={activeViewport}
|
|
822
|
+
theme={theme}
|
|
823
|
+
surface={surface}
|
|
824
|
+
/>
|
|
789
825
|
</div>
|
|
790
826
|
);
|
|
791
827
|
}
|
|
@@ -857,7 +893,7 @@ export function DocPlayer({
|
|
|
857
893
|
enabled={captionsEnabled && (renderMode || isPlaying || currentTime > 0)}
|
|
858
894
|
fontSize={16}
|
|
859
895
|
captionStyle={activeCaptionStyle}
|
|
860
|
-
theme={
|
|
896
|
+
theme={effectiveTheme}
|
|
861
897
|
viewport={activeViewport}
|
|
862
898
|
/>
|
|
863
899
|
)}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InlineAudioPlayer
|
|
3
|
+
*
|
|
4
|
+
* Small inline HTML5 audio player for `<audio>` tags embedded directly in
|
|
5
|
+
* a markdown document — e.g. a narration recording the user wants to
|
|
6
|
+
* audition in the editor. Resolves the `src` through `MediaContext` so a
|
|
7
|
+
* container-backed `audio/foo.webm` path returns a playable blob URL.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately thin: native browser controls, no autoplay. The
|
|
10
|
+
* MarkdownRenderer swaps this in for raw `<audio>` htmlElements; the
|
|
11
|
+
* editor-react Tiptap audio node reuses it for its NodeView.
|
|
12
|
+
*/
|
|
13
|
+
import { useMediaUrl } from './hooks/MediaContext.js';
|
|
14
|
+
|
|
15
|
+
export interface InlineAudioPlayerProps {
|
|
16
|
+
/** Source path — resolved through MediaContext when relative. */
|
|
17
|
+
src: string;
|
|
18
|
+
/** Base path used when no MediaProvider is in context. */
|
|
19
|
+
basePath?: string;
|
|
20
|
+
/** Whether to show native controls. Defaults to true. */
|
|
21
|
+
controls?: boolean;
|
|
22
|
+
/** `preload` attribute passthrough. Defaults to `'metadata'`. */
|
|
23
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
24
|
+
/** Extra className on the wrapper. */
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function InlineAudioPlayer({
|
|
29
|
+
src,
|
|
30
|
+
basePath = '',
|
|
31
|
+
controls = true,
|
|
32
|
+
preload = 'metadata',
|
|
33
|
+
className,
|
|
34
|
+
}: InlineAudioPlayerProps) {
|
|
35
|
+
const resolvedSrc = useMediaUrl(src, basePath);
|
|
36
|
+
|
|
37
|
+
if (!resolvedSrc) return null;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<span className={`squisq-inline-audio-player ${className ?? ''}`.trim()}>
|
|
41
|
+
<audio src={resolvedSrc} controls={controls} preload={preload} />
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default InlineAudioPlayer;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InlineVideoPlayer
|
|
3
|
+
*
|
|
4
|
+
* Small inline HTML5 video player for `<video>` tags embedded directly in
|
|
5
|
+
* a markdown document — e.g. a recording dropped into the doc by the
|
|
6
|
+
* recorder. Resolves the `src` through `MediaContext` (same path
|
|
7
|
+
* `VideoLayer` uses) so a container-backed `audio/foo.webm` or
|
|
8
|
+
* `video/foo.mp4` resolves to a blob URL instead of 404-ing on the
|
|
9
|
+
* filesystem path.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately thin: native browser controls, intrinsic aspect ratio,
|
|
12
|
+
* no autoplay. The MarkdownRenderer swaps this in for a raw `<video>`
|
|
13
|
+
* htmlElement; the editor-react Tiptap video node uses the same
|
|
14
|
+
* component for its NodeView.
|
|
15
|
+
*/
|
|
16
|
+
import { useMediaUrl } from './hooks/MediaContext.js';
|
|
17
|
+
|
|
18
|
+
export interface InlineVideoPlayerProps {
|
|
19
|
+
/** Source path — resolved through MediaContext when relative. */
|
|
20
|
+
src: string;
|
|
21
|
+
/** Base path used when no MediaProvider is in context. */
|
|
22
|
+
basePath?: string;
|
|
23
|
+
/** Optional explicit width (pixels or CSS length). */
|
|
24
|
+
width?: number | string;
|
|
25
|
+
/** Optional explicit height (pixels or CSS length). */
|
|
26
|
+
height?: number | string;
|
|
27
|
+
/** Optional poster image src — also resolved through MediaContext. */
|
|
28
|
+
poster?: string;
|
|
29
|
+
/** Whether to show native controls. Defaults to true. */
|
|
30
|
+
controls?: boolean;
|
|
31
|
+
/** `preload` attribute passthrough. Defaults to `'metadata'`. */
|
|
32
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
33
|
+
/** Extra className on the wrapper. */
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function InlineVideoPlayer({
|
|
38
|
+
src,
|
|
39
|
+
basePath = '',
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
poster,
|
|
43
|
+
controls = true,
|
|
44
|
+
preload = 'metadata',
|
|
45
|
+
className,
|
|
46
|
+
}: InlineVideoPlayerProps) {
|
|
47
|
+
const resolvedSrc = useMediaUrl(src, basePath);
|
|
48
|
+
// Always call the hook (Rules of Hooks); only use the result if a poster
|
|
49
|
+
// was actually requested. Empty input → fallback path which we discard.
|
|
50
|
+
const resolvedPoster = useMediaUrl(poster ?? '', basePath);
|
|
51
|
+
const posterUrl = poster ? resolvedPoster : undefined;
|
|
52
|
+
|
|
53
|
+
if (!resolvedSrc) return null;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<span className={`squisq-inline-video-player ${className ?? ''}`.trim()}>
|
|
57
|
+
<video
|
|
58
|
+
src={resolvedSrc}
|
|
59
|
+
poster={posterUrl}
|
|
60
|
+
controls={controls}
|
|
61
|
+
preload={preload}
|
|
62
|
+
width={width}
|
|
63
|
+
height={height}
|
|
64
|
+
playsInline
|
|
65
|
+
/>
|
|
66
|
+
</span>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default InlineVideoPlayer;
|
package/src/LinearDocView.tsx
CHANGED
|
@@ -18,9 +18,15 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { useMemo } from 'react';
|
|
21
|
+
import { useAutoSurface } from './hooks/useAutoSurface';
|
|
21
22
|
import type { Doc, Block, DocBlock } from '@bendyline/squisq/schemas';
|
|
22
23
|
import type { ViewportConfig } from '@bendyline/squisq/schemas';
|
|
23
|
-
import
|
|
24
|
+
import {
|
|
25
|
+
applySurface,
|
|
26
|
+
resolveFontFamily,
|
|
27
|
+
type SurfaceScheme,
|
|
28
|
+
type Theme,
|
|
29
|
+
} from '@bendyline/squisq/schemas';
|
|
24
30
|
import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
25
31
|
import { getLayers, hasTemplate, DEFAULT_THEME } from '@bendyline/squisq/doc';
|
|
26
32
|
import type { RenderContext } from '@bendyline/squisq/doc';
|
|
@@ -42,8 +48,35 @@ export interface LinearDocViewProps {
|
|
|
42
48
|
className?: string;
|
|
43
49
|
/** Theme to use for rendering (default: DEFAULT_THEME from the theme library) */
|
|
44
50
|
theme?: Theme;
|
|
51
|
+
/**
|
|
52
|
+
* Optional surface scheme (light / dark paper) overlaid on top of the
|
|
53
|
+
* theme's colors. Orthogonal to `theme` — a theme picks editorial
|
|
54
|
+
* identity, a surface picks the paper. Pass `'auto'` to follow the
|
|
55
|
+
* user's OS `prefers-color-scheme`, a `SurfaceScheme` object to force a
|
|
56
|
+
* specific surface, or omit to use the theme's built-in colors.
|
|
57
|
+
*/
|
|
58
|
+
surface?: SurfaceScheme | 'auto';
|
|
59
|
+
/**
|
|
60
|
+
* Use tight padding + a wider content column. The default layout is
|
|
61
|
+
* designed for a reading surface with breathing room (720px column,
|
|
62
|
+
* 24×16px padding). Short conversational snippets like chat replies
|
|
63
|
+
* benefit from a much tighter layout. Set to `true` to render with
|
|
64
|
+
* minimal padding and no max-width cap so the content hugs its
|
|
65
|
+
* container.
|
|
66
|
+
*/
|
|
67
|
+
thinMargins?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* How images inside the doc should be sized. `'inline'` (default)
|
|
70
|
+
* flows them at natural size up to the column width; `'thumbnail'`
|
|
71
|
+
* constrains each image to a 100×100 box with aspect-preserving
|
|
72
|
+
* containment — use for chat history and other dense surfaces where
|
|
73
|
+
* full-size images would dominate the layout.
|
|
74
|
+
*/
|
|
75
|
+
imageDisplayMode?: ImageDisplayMode;
|
|
45
76
|
}
|
|
46
77
|
|
|
78
|
+
export type ImageDisplayMode = 'inline' | 'thumbnail';
|
|
79
|
+
|
|
47
80
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
48
81
|
|
|
49
82
|
/**
|
|
@@ -136,23 +169,21 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }:
|
|
|
136
169
|
{/* Render the heading (if present — preamble has no sourceHeading) */}
|
|
137
170
|
{block.sourceHeading && !isAnnotated && <MarkdownRenderer nodes={[block.sourceHeading]} />}
|
|
138
171
|
|
|
139
|
-
{/* Annotated block: render SVG card
|
|
172
|
+
{/* Annotated block: render SVG card.
|
|
173
|
+
The heading is intentionally *not* duplicated above the card —
|
|
174
|
+
every template card renders its own title layer internally, so
|
|
175
|
+
a separate `squisq-linear-card-label` only made the heading
|
|
176
|
+
appear twice in the linear view. The card also drops its
|
|
177
|
+
rounded border + drop shadow so it reads as a continuation of
|
|
178
|
+
the surrounding page rather than a chrome'd preview. */}
|
|
140
179
|
{isAnnotated && visualBlock && (
|
|
141
180
|
<div className="squisq-linear-card">
|
|
142
|
-
{/* Optional heading label above the card */}
|
|
143
|
-
{block.sourceHeading && (
|
|
144
|
-
<div className="squisq-linear-card-label squisq-md">
|
|
145
|
-
<MarkdownRenderer nodes={[block.sourceHeading]} />
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
148
181
|
<div
|
|
149
182
|
className="squisq-linear-card-svg"
|
|
150
183
|
style={{
|
|
151
184
|
width: '100%',
|
|
152
185
|
aspectRatio: `${viewport.width} / ${viewport.height}`,
|
|
153
186
|
overflow: 'hidden',
|
|
154
|
-
borderRadius: '8px',
|
|
155
|
-
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.12)',
|
|
156
187
|
marginBottom: '1em',
|
|
157
188
|
}}
|
|
158
189
|
>
|
|
@@ -216,6 +247,83 @@ function extractListItems(contents?: MarkdownBlockNode[]): string[] {
|
|
|
216
247
|
return items;
|
|
217
248
|
}
|
|
218
249
|
|
|
250
|
+
/** First image discovered in a block's body, with any explicit
|
|
251
|
+
* dimensions captured (set by `<img width>` / `<img height>` in raw
|
|
252
|
+
* HTML — markdown shorthand has no syntax for dimensions). */
|
|
253
|
+
interface FirstImage {
|
|
254
|
+
src: string;
|
|
255
|
+
alt: string;
|
|
256
|
+
width?: number;
|
|
257
|
+
height?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Find the first image referenced anywhere in block contents — both
|
|
262
|
+
* markdown shorthand `` (type `image`) and raw HTML `<img>`
|
|
263
|
+
* tags (type `htmlBlock`/`htmlInline`). The WYSIWYG editor emits the
|
|
264
|
+
* HTML form whenever a user resizes an image (markdown shorthand has no
|
|
265
|
+
* width syntax), so missing that path silently breaks every resized
|
|
266
|
+
* image in the linear view.
|
|
267
|
+
*/
|
|
268
|
+
function extractFirstImage(contents: MarkdownBlockNode[] | undefined): FirstImage | null {
|
|
269
|
+
if (!contents || contents.length === 0) return null;
|
|
270
|
+
|
|
271
|
+
function fromHtml(nodes: unknown[]): FirstImage | null {
|
|
272
|
+
for (const node of nodes) {
|
|
273
|
+
if (!node || typeof node !== 'object') continue;
|
|
274
|
+
const n = node as Record<string, unknown>;
|
|
275
|
+
if (n.type === 'htmlElement' && n.tagName === 'img') {
|
|
276
|
+
const attrs = n.attributes as Record<string, string> | undefined;
|
|
277
|
+
if (attrs && typeof attrs.src === 'string' && attrs.src) {
|
|
278
|
+
return {
|
|
279
|
+
src: attrs.src,
|
|
280
|
+
alt: typeof attrs.alt === 'string' ? attrs.alt : '',
|
|
281
|
+
width: parseDim(attrs.width),
|
|
282
|
+
height: parseDim(attrs.height),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (Array.isArray(n.children)) {
|
|
287
|
+
const found = fromHtml(n.children);
|
|
288
|
+
if (found) return found;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function walk(node: unknown): FirstImage | null {
|
|
295
|
+
if (!node || typeof node !== 'object') return null;
|
|
296
|
+
const n = node as Record<string, unknown>;
|
|
297
|
+
if (n.type === 'image' && typeof n.url === 'string' && n.url) {
|
|
298
|
+
return { src: n.url, alt: typeof n.alt === 'string' ? n.alt : '' };
|
|
299
|
+
}
|
|
300
|
+
if ((n.type === 'htmlBlock' || n.type === 'htmlInline') && Array.isArray(n.htmlChildren)) {
|
|
301
|
+
const found = fromHtml(n.htmlChildren);
|
|
302
|
+
if (found) return found;
|
|
303
|
+
}
|
|
304
|
+
if (Array.isArray(n.children)) {
|
|
305
|
+
for (const child of n.children) {
|
|
306
|
+
const found = walk(child);
|
|
307
|
+
if (found) return found;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const node of contents) {
|
|
314
|
+
const found = walk(node);
|
|
315
|
+
if (found) return found;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Parse a `width`/`height` HTML attribute to a positive number. */
|
|
321
|
+
function parseDim(raw: string | undefined): number | undefined {
|
|
322
|
+
if (raw === undefined) return undefined;
|
|
323
|
+
const n = parseFloat(raw);
|
|
324
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
219
327
|
/** Extract table data (headers, rows, alignment) from block contents. */
|
|
220
328
|
function extractTableData(contents?: MarkdownBlockNode[]): {
|
|
221
329
|
headers: string[];
|
|
@@ -249,7 +357,7 @@ function getTemplateDefaults(
|
|
|
249
357
|
switch (templateName) {
|
|
250
358
|
case 'statHighlight':
|
|
251
359
|
return { stat: headingText, description: bodyText || headingText };
|
|
252
|
-
case '
|
|
360
|
+
case 'quote':
|
|
253
361
|
case 'fullBleedQuote':
|
|
254
362
|
case 'pullQuote':
|
|
255
363
|
return { quote: bodyText || headingText };
|
|
@@ -257,7 +365,7 @@ function getTemplateDefaults(
|
|
|
257
365
|
return { fact: headingText, explanation: bodyText || headingText };
|
|
258
366
|
case 'comparisonBar':
|
|
259
367
|
return { leftLabel: 'A', leftValue: 60, rightLabel: 'B', rightValue: 40 };
|
|
260
|
-
case '
|
|
368
|
+
case 'list': {
|
|
261
369
|
const items = extractListItems(contents);
|
|
262
370
|
return { items: items.length > 0 ? items : ['Item 1', 'Item 2', 'Item 3'] };
|
|
263
371
|
}
|
|
@@ -269,6 +377,37 @@ function getTemplateDefaults(
|
|
|
269
377
|
const tableData = extractTableData(contents);
|
|
270
378
|
return tableData ?? { headers: ['Column'], rows: [['Data']] };
|
|
271
379
|
}
|
|
380
|
+
case 'imageWithCaption': {
|
|
381
|
+
// The template requires `imageSrc` — without it the image layer
|
|
382
|
+
// renders a broken `<img src=undefined>`. Pull the first image
|
|
383
|
+
// out of the block's body (markdown shorthand or HTML <img>) and
|
|
384
|
+
// use the heading as a caption fallback so a bare `# Title
|
|
385
|
+
// [Image with Caption]` block renders something sensible.
|
|
386
|
+
const img = extractFirstImage(contents);
|
|
387
|
+
if (!img) return { caption: headingText };
|
|
388
|
+
return {
|
|
389
|
+
imageSrc: img.src,
|
|
390
|
+
imageAlt: img.alt || headingText,
|
|
391
|
+
caption: headingText,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
case 'leftFeature':
|
|
395
|
+
case 'rightFeature': {
|
|
396
|
+
// Feature blocks pair an image with the heading + the first
|
|
397
|
+
// paragraph of body text. Both inputs come from the section's
|
|
398
|
+
// contents — the image via the same scan used by imageWithCaption,
|
|
399
|
+
// the body text via the plain-text extractor that's already in
|
|
400
|
+
// scope here.
|
|
401
|
+
const img = extractFirstImage(contents);
|
|
402
|
+
return {
|
|
403
|
+
imageSrc: img?.src ?? '',
|
|
404
|
+
imageAlt: img?.alt || headingText,
|
|
405
|
+
imageWidth: img?.width,
|
|
406
|
+
imageHeight: img?.height,
|
|
407
|
+
title: headingText,
|
|
408
|
+
body: bodyText,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
272
411
|
default:
|
|
273
412
|
return {};
|
|
274
413
|
}
|
|
@@ -294,26 +433,31 @@ export function LinearDocView({
|
|
|
294
433
|
viewport,
|
|
295
434
|
className,
|
|
296
435
|
theme,
|
|
436
|
+
surface,
|
|
437
|
+
thinMargins = false,
|
|
438
|
+
imageDisplayMode = 'inline',
|
|
297
439
|
}: LinearDocViewProps) {
|
|
298
440
|
const activeViewport = viewport ?? VIEWPORT_PRESETS.landscape;
|
|
299
441
|
const totalBlocks = useMemo(() => countAll(doc.blocks), [doc.blocks]);
|
|
442
|
+
const autoSurface = useAutoSurface(surface === 'auto');
|
|
443
|
+
const resolvedSurface: SurfaceScheme | undefined = surface === 'auto' ? autoSurface : surface;
|
|
300
444
|
|
|
301
|
-
const renderContext: RenderContext = useMemo(
|
|
302
|
-
|
|
303
|
-
|
|
445
|
+
const renderContext: RenderContext = useMemo(() => {
|
|
446
|
+
const baseTheme = theme ?? DEFAULT_THEME;
|
|
447
|
+
return {
|
|
448
|
+
theme: resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme,
|
|
304
449
|
viewport: activeViewport,
|
|
305
450
|
totalBlocks,
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
);
|
|
451
|
+
};
|
|
452
|
+
}, [activeViewport, totalBlocks, theme, resolvedSurface]);
|
|
309
453
|
|
|
310
454
|
const activeTheme = renderContext.theme!;
|
|
311
455
|
const bgColor = activeTheme.colors.background;
|
|
312
456
|
const textColor = activeTheme.colors.text;
|
|
313
457
|
const mutedColor = activeTheme.colors.textMuted;
|
|
314
458
|
const primaryColor = activeTheme.colors.primary;
|
|
315
|
-
const bodyFont = activeTheme.typography.
|
|
316
|
-
const titleFont = activeTheme.typography.
|
|
459
|
+
const bodyFont = resolveFontFamily(activeTheme.typography.bodyFont, 'system-ui, sans-serif');
|
|
460
|
+
const titleFont = resolveFontFamily(activeTheme.typography.titleFont, 'Georgia, serif');
|
|
317
461
|
const lineHt = activeTheme.typography.lineHeight ?? 1.7;
|
|
318
462
|
|
|
319
463
|
return (
|
|
@@ -321,19 +465,28 @@ export function LinearDocView({
|
|
|
321
465
|
className={`squisq-linear ${className || ''}`}
|
|
322
466
|
style={{
|
|
323
467
|
width: '100%',
|
|
324
|
-
|
|
325
|
-
|
|
468
|
+
// Thin-margins mode is the "embedded in someone else's container"
|
|
469
|
+
// signal (chat bubble, sidebar preview). Fit to content there so
|
|
470
|
+
// the host's bubble doesn't render a tall empty box when the doc
|
|
471
|
+
// is short. Standalone mode keeps height:100% for full-viewport
|
|
472
|
+
// scrolling.
|
|
473
|
+
height: thinMargins ? 'auto' : '100%',
|
|
474
|
+
overflowY: thinMargins ? 'visible' : 'auto',
|
|
326
475
|
overflowX: 'hidden',
|
|
327
476
|
background: bgColor,
|
|
328
477
|
}}
|
|
329
478
|
>
|
|
330
479
|
<div
|
|
331
|
-
className=
|
|
480
|
+
className={`squisq-linear-content squisq-md${thinMargins ? ' squisq-linear-content--thin' : ''}${imageDisplayMode === 'thumbnail' ? ' squisq-linear-content--thumbnail-images' : ''}`}
|
|
332
481
|
style={
|
|
333
482
|
{
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
483
|
+
// Thin-margins mode drops the 720px reading column + generous
|
|
484
|
+
// page padding (right for standalone docs) in favor of a tight
|
|
485
|
+
// layout that hugs its container (right for chat bubbles and
|
|
486
|
+
// sidebar previews).
|
|
487
|
+
maxWidth: thinMargins ? 'none' : '720px',
|
|
488
|
+
margin: thinMargins ? '0' : '0 auto',
|
|
489
|
+
padding: thinMargins ? '0' : '24px 16px',
|
|
337
490
|
lineHeight: lineHt,
|
|
338
491
|
fontSize: '16px',
|
|
339
492
|
fontFamily: bodyFont,
|
|
@@ -376,6 +529,17 @@ export function LinearDocView({
|
|
|
376
529
|
margin-bottom: 0.3em;
|
|
377
530
|
}
|
|
378
531
|
.squisq-linear-content a {
|
|
532
|
+
/* Blend the theme's primary toward the body text color so
|
|
533
|
+
links stay theme-flavored but always have enough contrast
|
|
534
|
+
against the background. Some themes (e.g. Gezellig) pick a
|
|
535
|
+
mid-tone primary that's nearly invisible on a dark page
|
|
536
|
+
without this lift. */
|
|
537
|
+
color: color-mix(in srgb, var(--squisq-linear-primary) 65%, var(--squisq-linear-text));
|
|
538
|
+
text-decoration: underline;
|
|
539
|
+
text-decoration-thickness: 1px;
|
|
540
|
+
text-underline-offset: 2px;
|
|
541
|
+
}
|
|
542
|
+
.squisq-linear-content a:hover {
|
|
379
543
|
color: var(--squisq-linear-primary);
|
|
380
544
|
}
|
|
381
545
|
.squisq-linear-content code {
|
|
@@ -414,6 +578,14 @@ export function LinearDocView({
|
|
|
414
578
|
border-radius: 6px;
|
|
415
579
|
margin: 0.5em 0;
|
|
416
580
|
}
|
|
581
|
+
.squisq-linear-content--thumbnail-images img {
|
|
582
|
+
max-width: 100px;
|
|
583
|
+
max-height: 100px;
|
|
584
|
+
width: auto;
|
|
585
|
+
height: auto;
|
|
586
|
+
object-fit: contain;
|
|
587
|
+
display: block;
|
|
588
|
+
}
|
|
417
589
|
.squisq-linear-content strong {
|
|
418
590
|
font-weight: 700;
|
|
419
591
|
}
|