@bendyline/squisq-react 1.3.1 → 1.4.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/index.d.ts +766 -35
- package/dist/index.js +4793 -33
- package/dist/index.js.map +1 -1
- package/dist/squisq-player.css +1 -1
- package/dist/squisq-player.css.map +1 -1
- package/dist/squisq-player.global.js +17 -13
- package/dist/squisq-player.global.js.map +1 -1
- package/dist/standalone-source.js +1 -1
- package/package.json +3 -2
- package/src/BlockRenderer.tsx +15 -7
- package/src/DocPlayer.tsx +65 -12
- package/src/DocProgressBar.tsx +21 -3
- package/src/LinearDocView.tsx +11 -197
- package/src/MarkdownRenderer.tsx +165 -41
- package/src/MediaClipLayer.tsx +135 -0
- package/src/__tests__/DocPlayer.test.tsx +51 -0
- package/src/__tests__/DocProgressBar.test.tsx +76 -0
- package/src/__tests__/MarkdownRenderer.test.tsx +95 -1
- package/src/__tests__/PathLayer.test.tsx +73 -0
- package/src/__tests__/fillStyle.test.tsx +112 -0
- package/src/__tests__/transitionStyles.test.ts +125 -0
- package/src/__tests__/useDocPlayback.transition.test.ts +70 -0
- package/src/hooks/useAudioSync.ts +14 -1
- package/src/hooks/useDocPlayback.ts +81 -100
- package/src/hooks/useMediaSchedule.ts +39 -0
- package/src/index.ts +7 -0
- package/src/layers/ImageLayer.tsx +11 -1
- package/src/layers/PathLayer.tsx +146 -0
- package/src/layers/ShapeLayer.tsx +27 -5
- package/src/layers/TextLayer.tsx +395 -22
- package/src/layers/VideoLayer.tsx +16 -9
- package/src/layers/index.ts +1 -0
- package/src/styles/doc-animations.css +1857 -2
- package/src/utils/fillStyle.tsx +148 -0
- package/dist/BlockRenderer.d.ts +0 -37
- package/dist/BlockRenderer.d.ts.map +0 -1
- package/dist/BlockRenderer.js +0 -54
- package/dist/BlockRenderer.js.map +0 -1
- package/dist/CaptionOverlay.d.ts +0 -33
- package/dist/CaptionOverlay.d.ts.map +0 -1
- package/dist/CaptionOverlay.js +0 -44
- package/dist/CaptionOverlay.js.map +0 -1
- package/dist/DocControlsBottom.d.ts +0 -28
- package/dist/DocControlsBottom.d.ts.map +0 -1
- package/dist/DocControlsBottom.js +0 -27
- package/dist/DocControlsBottom.js.map +0 -1
- package/dist/DocControlsOverlay.d.ts +0 -28
- package/dist/DocControlsOverlay.d.ts.map +0 -1
- package/dist/DocControlsOverlay.js +0 -80
- package/dist/DocControlsOverlay.js.map +0 -1
- package/dist/DocControlsSidebar.d.ts +0 -27
- package/dist/DocControlsSidebar.d.ts.map +0 -1
- package/dist/DocControlsSidebar.js +0 -10
- package/dist/DocControlsSidebar.js.map +0 -1
- package/dist/DocControlsSlideshow.d.ts +0 -20
- package/dist/DocControlsSlideshow.d.ts.map +0 -1
- package/dist/DocControlsSlideshow.js +0 -69
- package/dist/DocControlsSlideshow.js.map +0 -1
- package/dist/DocPlayer.d.ts +0 -97
- package/dist/DocPlayer.d.ts.map +0 -1
- package/dist/DocPlayer.js +0 -657
- package/dist/DocPlayer.js.map +0 -1
- package/dist/DocPlayerWithSidebar.d.ts +0 -41
- package/dist/DocPlayerWithSidebar.d.ts.map +0 -1
- package/dist/DocPlayerWithSidebar.js +0 -69
- package/dist/DocPlayerWithSidebar.js.map +0 -1
- package/dist/DocProgressBar.d.ts +0 -31
- package/dist/DocProgressBar.d.ts.map +0 -1
- package/dist/DocProgressBar.js +0 -122
- package/dist/DocProgressBar.js.map +0 -1
- package/dist/InlineAudioPlayer.d.ts +0 -15
- package/dist/InlineAudioPlayer.d.ts.map +0 -1
- package/dist/InlineAudioPlayer.js +0 -22
- package/dist/InlineAudioPlayer.js.map +0 -1
- package/dist/InlineVideoPlayer.d.ts +0 -21
- package/dist/InlineVideoPlayer.d.ts.map +0 -1
- package/dist/InlineVideoPlayer.js +0 -29
- package/dist/InlineVideoPlayer.js.map +0 -1
- package/dist/LinearDocView.d.ts +0 -73
- package/dist/LinearDocView.d.ts.map +0 -1
- package/dist/LinearDocView.js +0 -451
- package/dist/LinearDocView.js.map +0 -1
- package/dist/MarkdownRenderer.d.ts +0 -32
- package/dist/MarkdownRenderer.d.ts.map +0 -1
- package/dist/MarkdownRenderer.js +0 -259
- package/dist/MarkdownRenderer.js.map +0 -1
- package/dist/SocialCaptionOverlay.d.ts +0 -27
- package/dist/SocialCaptionOverlay.d.ts.map +0 -1
- package/dist/SocialCaptionOverlay.js +0 -185
- package/dist/SocialCaptionOverlay.js.map +0 -1
- package/dist/__tests__/BlockRenderer.test.d.ts +0 -2
- package/dist/__tests__/BlockRenderer.test.d.ts.map +0 -1
- package/dist/__tests__/BlockRenderer.test.js +0 -82
- package/dist/__tests__/BlockRenderer.test.js.map +0 -1
- package/dist/__tests__/DocControlsSlideshow.test.d.ts +0 -2
- package/dist/__tests__/DocControlsSlideshow.test.d.ts.map +0 -1
- package/dist/__tests__/DocControlsSlideshow.test.js +0 -89
- package/dist/__tests__/DocControlsSlideshow.test.js.map +0 -1
- package/dist/__tests__/JsonView.test.d.ts +0 -2
- package/dist/__tests__/JsonView.test.d.ts.map +0 -1
- package/dist/__tests__/JsonView.test.js +0 -93
- package/dist/__tests__/JsonView.test.js.map +0 -1
- package/dist/__tests__/LinearDocView.test.d.ts +0 -2
- package/dist/__tests__/LinearDocView.test.d.ts.map +0 -1
- package/dist/__tests__/LinearDocView.test.js +0 -250
- package/dist/__tests__/LinearDocView.test.js.map +0 -1
- package/dist/__tests__/MarkdownRenderer.test.d.ts +0 -2
- package/dist/__tests__/MarkdownRenderer.test.d.ts.map +0 -1
- package/dist/__tests__/MarkdownRenderer.test.js +0 -208
- package/dist/__tests__/MarkdownRenderer.test.js.map +0 -1
- package/dist/__tests__/TableLayer.test.d.ts +0 -2
- package/dist/__tests__/TableLayer.test.d.ts.map +0 -1
- package/dist/__tests__/TableLayer.test.js +0 -121
- package/dist/__tests__/TableLayer.test.js.map +0 -1
- package/dist/__tests__/exports.test.d.ts +0 -2
- package/dist/__tests__/exports.test.d.ts.map +0 -1
- package/dist/__tests__/exports.test.js +0 -48
- package/dist/__tests__/exports.test.js.map +0 -1
- package/dist/hooks/AudioProvider.d.ts +0 -71
- package/dist/hooks/AudioProvider.d.ts.map +0 -1
- package/dist/hooks/AudioProvider.js +0 -52
- package/dist/hooks/AudioProvider.js.map +0 -1
- package/dist/hooks/MediaContext.d.ts +0 -35
- package/dist/hooks/MediaContext.d.ts.map +0 -1
- package/dist/hooks/MediaContext.js +0 -72
- package/dist/hooks/MediaContext.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -6
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -5
- package/dist/hooks/index.js.map +0 -1
- package/dist/hooks/useAudioSync.d.ts +0 -18
- package/dist/hooks/useAudioSync.d.ts.map +0 -1
- package/dist/hooks/useAudioSync.js +0 -340
- package/dist/hooks/useAudioSync.js.map +0 -1
- package/dist/hooks/useAutoSurface.d.ts +0 -11
- package/dist/hooks/useAutoSurface.d.ts.map +0 -1
- package/dist/hooks/useAutoSurface.js +0 -24
- package/dist/hooks/useAutoSurface.js.map +0 -1
- package/dist/hooks/useDocPlayback.d.ts +0 -47
- package/dist/hooks/useDocPlayback.d.ts.map +0 -1
- package/dist/hooks/useDocPlayback.js +0 -179
- package/dist/hooks/useDocPlayback.js.map +0 -1
- package/dist/hooks/useViewportOrientation.d.ts +0 -31
- package/dist/hooks/useViewportOrientation.d.ts.map +0 -1
- package/dist/hooks/useViewportOrientation.js +0 -92
- package/dist/hooks/useViewportOrientation.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/jsonView/JsonView.d.ts +0 -26
- package/dist/jsonView/JsonView.d.ts.map +0 -1
- package/dist/jsonView/JsonView.js +0 -12
- package/dist/jsonView/JsonView.js.map +0 -1
- package/dist/jsonView/RenderNode.d.ts +0 -22
- package/dist/jsonView/RenderNode.d.ts.map +0 -1
- package/dist/jsonView/RenderNode.js +0 -30
- package/dist/jsonView/RenderNode.js.map +0 -1
- package/dist/jsonView/index.d.ts +0 -3
- package/dist/jsonView/index.d.ts.map +0 -1
- package/dist/jsonView/index.js +0 -2
- package/dist/jsonView/index.js.map +0 -1
- package/dist/jsonView/useJsonViewTokens.d.ts +0 -14
- package/dist/jsonView/useJsonViewTokens.d.ts.map +0 -1
- package/dist/jsonView/useJsonViewTokens.js +0 -34
- package/dist/jsonView/useJsonViewTokens.js.map +0 -1
- package/dist/jsonView/viewers.d.ts +0 -30
- package/dist/jsonView/viewers.d.ts.map +0 -1
- package/dist/jsonView/viewers.js +0 -226
- package/dist/jsonView/viewers.js.map +0 -1
- package/dist/layers/ImageLayer.d.ts +0 -23
- package/dist/layers/ImageLayer.d.ts.map +0 -1
- package/dist/layers/ImageLayer.js +0 -104
- package/dist/layers/ImageLayer.js.map +0 -1
- package/dist/layers/MapLayer.d.ts +0 -29
- package/dist/layers/MapLayer.d.ts.map +0 -1
- package/dist/layers/MapLayer.js +0 -97
- package/dist/layers/MapLayer.js.map +0 -1
- package/dist/layers/ShapeLayer.d.ts +0 -20
- package/dist/layers/ShapeLayer.d.ts.map +0 -1
- package/dist/layers/ShapeLayer.js +0 -39
- package/dist/layers/ShapeLayer.js.map +0 -1
- package/dist/layers/TableLayer.d.ts +0 -23
- package/dist/layers/TableLayer.d.ts.map +0 -1
- package/dist/layers/TableLayer.js +0 -58
- package/dist/layers/TableLayer.js.map +0 -1
- package/dist/layers/TextLayer.d.ts +0 -23
- package/dist/layers/TextLayer.d.ts.map +0 -1
- package/dist/layers/TextLayer.js +0 -126
- package/dist/layers/TextLayer.js.map +0 -1
- package/dist/layers/VideoLayer.d.ts +0 -40
- package/dist/layers/VideoLayer.d.ts.map +0 -1
- package/dist/layers/VideoLayer.js +0 -105
- package/dist/layers/VideoLayer.js.map +0 -1
- package/dist/layers/index.d.ts +0 -6
- package/dist/layers/index.d.ts.map +0 -1
- package/dist/layers/index.js +0 -6
- package/dist/layers/index.js.map +0 -1
- package/dist/standalone-entry.d.ts +0 -69
- package/dist/standalone-entry.d.ts.map +0 -1
- package/dist/standalone-entry.js +0 -185
- package/dist/standalone-entry.js.map +0 -1
- package/dist/types.d.ts +0 -154
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -21
- package/dist/types.js.map +0 -1
- package/dist/utils/animationUtils.d.ts +0 -8
- package/dist/utils/animationUtils.d.ts.map +0 -1
- package/dist/utils/animationUtils.js +0 -8
- package/dist/utils/animationUtils.js.map +0 -1
- package/dist/utils/layerUtils.d.ts +0 -17
- package/dist/utils/layerUtils.d.ts.map +0 -1
- package/dist/utils/layerUtils.js +0 -37
- package/dist/utils/layerUtils.js.map +0 -1
- package/dist/utils/mapTileUtils.d.ts +0 -105
- package/dist/utils/mapTileUtils.d.ts.map +0 -1
- package/dist/utils/mapTileUtils.js +0 -268
- package/dist/utils/mapTileUtils.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "React component library for doc playback, block rendering, and media layers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -53,13 +53,14 @@
|
|
|
53
53
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@bendyline/squisq": "1.
|
|
56
|
+
"@bendyline/squisq": "1.5.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/react": "18.3.28",
|
|
60
60
|
"preact": "10.29.0",
|
|
61
61
|
"react": "18.3.1",
|
|
62
62
|
"react-dom": "18.3.1",
|
|
63
|
+
"@testing-library/dom": "10.4.1",
|
|
63
64
|
"@testing-library/react": "16.3.2",
|
|
64
65
|
"@testing-library/jest-dom": "6.9.1",
|
|
65
66
|
"jsdom": "25.0.1",
|
package/src/BlockRenderer.tsx
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* Handles positioning, animations, and transitions.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { Block, Layer } from '@bendyline/squisq/schemas';
|
|
9
|
+
import type { Block, Layer, Transition } from '@bendyline/squisq/schemas';
|
|
10
|
+
import { resolveTransitionDuration } from '@bendyline/squisq/schemas';
|
|
10
11
|
import { ImageLayer } from './layers/ImageLayer';
|
|
11
12
|
import { TextLayer } from './layers/TextLayer';
|
|
12
13
|
import { ShapeLayer } from './layers/ShapeLayer';
|
|
14
|
+
import { PathLayer } from './layers/PathLayer';
|
|
13
15
|
import { MapLayer } from './layers/MapLayer';
|
|
14
16
|
import { VideoLayer } from './layers/VideoLayer';
|
|
15
17
|
import { TableLayer } from './layers/TableLayer';
|
|
@@ -39,6 +41,8 @@ interface BlockRendererProps {
|
|
|
39
41
|
isEntering?: boolean;
|
|
40
42
|
/** Whether this block is exiting (for transition) */
|
|
41
43
|
isExiting?: boolean;
|
|
44
|
+
/** Transition to apply. Defaults to block.transition. */
|
|
45
|
+
transition?: Transition;
|
|
42
46
|
/** Viewport dimensions (defaults to 1920x1080 landscape) */
|
|
43
47
|
viewport?: ViewportDimensions;
|
|
44
48
|
/** Whether the doc is currently playing (controls video playback) */
|
|
@@ -51,18 +55,20 @@ export function BlockRenderer({
|
|
|
51
55
|
basePath,
|
|
52
56
|
isEntering = false,
|
|
53
57
|
isExiting = false,
|
|
58
|
+
transition,
|
|
54
59
|
viewport = VIEWPORT,
|
|
55
60
|
isPlaying,
|
|
56
61
|
}: BlockRendererProps) {
|
|
57
62
|
// Build transition class and inline style for dynamic duration
|
|
58
63
|
let transitionClass = '';
|
|
59
64
|
const transitionStyle: Record<string, string> = {};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const activeTransition = transition ?? block.transition;
|
|
66
|
+
if (activeTransition && isEntering) {
|
|
67
|
+
transitionClass = getTransitionClass(activeTransition.type, true, activeTransition.direction);
|
|
68
|
+
transitionStyle['--transition-duration'] = `${resolveTransitionDuration(activeTransition)}s`;
|
|
69
|
+
} else if (activeTransition && isExiting) {
|
|
70
|
+
transitionClass = getTransitionClass(activeTransition.type, false, activeTransition.direction);
|
|
71
|
+
transitionStyle['--transition-duration'] = `${resolveTransitionDuration(activeTransition)}s`;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
// Unique clip path ID per block to avoid conflicts when multiple blocks render simultaneously
|
|
@@ -124,6 +130,8 @@ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }: Laye
|
|
|
124
130
|
return <TextLayer layer={layer} viewport={viewport} blockTime={blockTime} />;
|
|
125
131
|
case 'shape':
|
|
126
132
|
return <ShapeLayer layer={layer} viewport={viewport} blockTime={blockTime} />;
|
|
133
|
+
case 'path':
|
|
134
|
+
return <PathLayer layer={layer} viewport={viewport} blockTime={blockTime} />;
|
|
127
135
|
case 'map':
|
|
128
136
|
return (
|
|
129
137
|
<MapLayer layer={layer} basePath={basePath} viewport={viewport} blockTime={blockTime} />
|
package/src/DocPlayer.tsx
CHANGED
|
@@ -24,7 +24,13 @@
|
|
|
24
24
|
|
|
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
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
isTemplateBlock,
|
|
29
|
+
getCaptionAtTime,
|
|
30
|
+
resolveMediaSchedule,
|
|
31
|
+
getDocPlaybackDuration,
|
|
32
|
+
} from '@bendyline/squisq/schemas';
|
|
33
|
+
import { MediaClipLayer } from './MediaClipLayer';
|
|
28
34
|
import type { SurfaceScheme, Theme } from '@bendyline/squisq/schemas';
|
|
29
35
|
import { applySurface } from '@bendyline/squisq/schemas';
|
|
30
36
|
import { BlockRenderer } from './BlockRenderer';
|
|
@@ -255,6 +261,11 @@ export function DocPlayer({
|
|
|
255
261
|
restart,
|
|
256
262
|
} = audio;
|
|
257
263
|
|
|
264
|
+
// Timed media clips (block.media + doc.documentMedia) resolved to absolute
|
|
265
|
+
// doc-timeline coordinates. Empty for documents without the media model, so
|
|
266
|
+
// <MediaClipLayer> renders nothing and the legacy audio path is unaffected.
|
|
267
|
+
const mediaSchedule = useMemo(() => resolveMediaSchedule(script), [script]);
|
|
268
|
+
|
|
258
269
|
// Refs for frequently-changing values used in the keyboard handler,
|
|
259
270
|
// so the handler callback doesn't need to be recreated every frame.
|
|
260
271
|
const currentTimeRef = useRef(currentTime);
|
|
@@ -450,7 +461,13 @@ export function DocPlayer({
|
|
|
450
461
|
const video = el as HTMLVideoElement;
|
|
451
462
|
const clipStart = parseFloat(video.dataset.clipStart || '0');
|
|
452
463
|
const clipEnd = parseFloat(video.dataset.clipEnd || '0');
|
|
453
|
-
|
|
464
|
+
// Honor the per-clip startAt offset: before it, hold at the
|
|
465
|
+
// in-point; after, advance by (blockElapsed - startAt).
|
|
466
|
+
const startAt = parseFloat(video.dataset.startAt || '0');
|
|
467
|
+
const targetTime = Math.min(
|
|
468
|
+
clipStart + Math.max(0, blockElapsed - startAt),
|
|
469
|
+
clipEnd,
|
|
470
|
+
);
|
|
454
471
|
|
|
455
472
|
video.pause();
|
|
456
473
|
video.currentTime = targetTime;
|
|
@@ -468,6 +485,30 @@ export function DocPlayer({
|
|
|
468
485
|
});
|
|
469
486
|
}
|
|
470
487
|
|
|
488
|
+
// Seek player-level scheduled videos (document-spanning clips
|
|
489
|
+
// rendered by MediaClipLayer, outside any single block). Each
|
|
490
|
+
// carries data-abs-start/data-abs-end/data-source-in.
|
|
491
|
+
document.querySelectorAll('video[data-clip-id]').forEach((el) => {
|
|
492
|
+
const video = el as HTMLVideoElement;
|
|
493
|
+
const absStart = parseFloat(video.dataset.absStart || '0');
|
|
494
|
+
const absEnd = parseFloat(video.dataset.absEnd || '0');
|
|
495
|
+
const sourceIn = parseFloat(video.dataset.sourceIn || '0');
|
|
496
|
+
video.pause();
|
|
497
|
+
if (time < absStart || time >= absEnd) return;
|
|
498
|
+
const targetTime = sourceIn + (time - absStart);
|
|
499
|
+
video.currentTime = targetTime;
|
|
500
|
+
videoSeekPromises.push(
|
|
501
|
+
new Promise<void>((r) => {
|
|
502
|
+
if (Math.abs(video.currentTime - targetTime) < 0.1) {
|
|
503
|
+
r();
|
|
504
|
+
} else {
|
|
505
|
+
video.addEventListener('seeked', () => r(), { once: true });
|
|
506
|
+
setTimeout(r, 200);
|
|
507
|
+
}
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
|
|
471
512
|
// Wait for video seeks + one more frame for the browser to render
|
|
472
513
|
Promise.all(videoSeekPromises).then(() => {
|
|
473
514
|
requestAnimationFrame(() => resolve());
|
|
@@ -476,14 +517,12 @@ export function DocPlayer({
|
|
|
476
517
|
});
|
|
477
518
|
};
|
|
478
519
|
w.getDuration = () => {
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
return 0;
|
|
520
|
+
// The larger of the audio/block timeline and any media that spills
|
|
521
|
+
// past the last block (block-clip spillover or document-spanning
|
|
522
|
+
// media), so frame capture covers the full tail.
|
|
523
|
+
const mediaDuration = getDocPlaybackDuration(script);
|
|
524
|
+
if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
|
|
525
|
+
return mediaDuration;
|
|
487
526
|
};
|
|
488
527
|
// Expose block metadata for testing -- allows tests to find specific templates
|
|
489
528
|
w.getBlocks = () =>
|
|
@@ -843,6 +882,15 @@ export function DocPlayer({
|
|
|
843
882
|
{/* Hidden audio element */}
|
|
844
883
|
<audio ref={audioRef} preload="auto" muted={muted} />
|
|
845
884
|
|
|
885
|
+
{/* Timed media clips (per-block + document-spanning audio/video). */}
|
|
886
|
+
<MediaClipLayer
|
|
887
|
+
schedule={mediaSchedule}
|
|
888
|
+
currentTime={currentTime}
|
|
889
|
+
isPlaying={isPlaying}
|
|
890
|
+
basePath={basePath}
|
|
891
|
+
renderMode={renderMode}
|
|
892
|
+
/>
|
|
893
|
+
|
|
846
894
|
{/* Block viewport */}
|
|
847
895
|
<div className="doc-player__viewport">
|
|
848
896
|
{/* Cover block (shown at rest before playback) */}
|
|
@@ -860,12 +908,17 @@ export function DocPlayer({
|
|
|
860
908
|
|
|
861
909
|
{/* Previous block (during transition) */}
|
|
862
910
|
{!showCoverBlock && previousBlock && isExiting && (
|
|
863
|
-
|
|
911
|
+
// Keyed by block id so each block is its own DOM subtree: React never
|
|
912
|
+
// reconciles one block's layers onto another's (templates reuse layer
|
|
913
|
+
// ids like `title`/`background`), which would otherwise reuse stale
|
|
914
|
+
// DOM / skip entrance animations mid-transition.
|
|
915
|
+
<div key={previousBlock.id} className="doc-player__block doc-player__block--previous">
|
|
864
916
|
<BlockRenderer
|
|
865
917
|
block={previousBlock}
|
|
866
918
|
blockTime={blockTime}
|
|
867
919
|
basePath={basePath}
|
|
868
920
|
isExiting={true}
|
|
921
|
+
transition={currentBlock?.transition}
|
|
869
922
|
viewport={activeViewport}
|
|
870
923
|
/>
|
|
871
924
|
</div>
|
|
@@ -873,7 +926,7 @@ export function DocPlayer({
|
|
|
873
926
|
|
|
874
927
|
{/* Current block */}
|
|
875
928
|
{!showCoverBlock && currentBlock && (
|
|
876
|
-
<div className="doc-player__block doc-player__block--active">
|
|
929
|
+
<div key={currentBlock.id} className="doc-player__block doc-player__block--active">
|
|
877
930
|
<BlockRenderer
|
|
878
931
|
block={currentBlock}
|
|
879
932
|
blockTime={blockTime}
|
package/src/DocProgressBar.tsx
CHANGED
|
@@ -41,6 +41,15 @@ export function DocProgressBar({
|
|
|
41
41
|
const progressBarRef = useRef<HTMLDivElement>(null);
|
|
42
42
|
const [hoverPosition, setHoverPosition] = useState<number | null>(null);
|
|
43
43
|
|
|
44
|
+
// Fill position tracks the same timeline as the clock readout and the block
|
|
45
|
+
// marker dots — elapsed playback time over the audio `totalDuration`. (We
|
|
46
|
+
// deliberately don't use `state.docProgress`, which is keyed to the doc's
|
|
47
|
+
// estimated `script.duration`; when that diverges from the actual playback
|
|
48
|
+
// length — e.g. the editor preview's reading-time estimate — the fill would
|
|
49
|
+
// desync from the clock and the dots.)
|
|
50
|
+
const playProgress =
|
|
51
|
+
state.totalDuration > 0 ? Math.max(0, Math.min(1, state.currentTime / state.totalDuration)) : 0;
|
|
52
|
+
|
|
44
53
|
const handleProgressHover = useCallback((e: React.MouseEvent) => {
|
|
45
54
|
const bar = progressBarRef.current;
|
|
46
55
|
if (!bar) return;
|
|
@@ -99,16 +108,25 @@ export function DocProgressBar({
|
|
|
99
108
|
}}
|
|
100
109
|
/>
|
|
101
110
|
|
|
102
|
-
{/* Progress fill
|
|
111
|
+
{/* Progress fill.
|
|
112
|
+
No CSS `width` transition: the fill is driven by frequent JS updates
|
|
113
|
+
(the synthetic fallback timer runs ~60fps; real audio fires
|
|
114
|
+
`timeupdate` several times/sec), so it's already smooth. A CSS
|
|
115
|
+
transition here actively breaks playback — after a discontinuous
|
|
116
|
+
backward seek, the per-frame inline-width updates on resume keep
|
|
117
|
+
interrupting the in-flight transition before it advances, so the
|
|
118
|
+
*rendered* width latches at the seek position even though the inline
|
|
119
|
+
style (and the clock) keep climbing. The result is a frozen-looking
|
|
120
|
+
bar while time advances. Update width instantly instead. */}
|
|
103
121
|
<div
|
|
122
|
+
data-testid="doc-progress-fill"
|
|
104
123
|
style={{
|
|
105
124
|
position: 'absolute',
|
|
106
125
|
left: 0,
|
|
107
|
-
width: `${
|
|
126
|
+
width: `${playProgress * 100}%`,
|
|
108
127
|
height: '6px',
|
|
109
128
|
background: '#5b9bd5',
|
|
110
129
|
borderRadius: '3px',
|
|
111
|
-
transition: 'width 0.1s',
|
|
112
130
|
}}
|
|
113
131
|
/>
|
|
114
132
|
|
package/src/LinearDocView.tsx
CHANGED
|
@@ -28,10 +28,9 @@ import {
|
|
|
28
28
|
type Theme,
|
|
29
29
|
} from '@bendyline/squisq/schemas';
|
|
30
30
|
import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
31
|
-
import { getLayers, hasTemplate, DEFAULT_THEME } from '@bendyline/squisq/doc';
|
|
31
|
+
import { getLayers, hasTemplate, DEFAULT_THEME, deriveTemplateInputs } from '@bendyline/squisq/doc';
|
|
32
32
|
import type { RenderContext } from '@bendyline/squisq/doc';
|
|
33
33
|
import { extractPlainText } from '@bendyline/squisq/markdown';
|
|
34
|
-
import type { MarkdownBlockNode, MarkdownList, MarkdownTable } from '@bendyline/squisq/markdown';
|
|
35
34
|
import { BlockRenderer } from './BlockRenderer';
|
|
36
35
|
import { MarkdownRenderer } from './MarkdownRenderer';
|
|
37
36
|
|
|
@@ -126,7 +125,6 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }:
|
|
|
126
125
|
|
|
127
126
|
const annotation = block.sourceHeading!.templateAnnotation!;
|
|
128
127
|
const headingText = extractPlainText(block.sourceHeading!);
|
|
129
|
-
const bodyText = extractBodyPlainText(block.contents);
|
|
130
128
|
|
|
131
129
|
// Build a TemplateBlock-compatible object
|
|
132
130
|
const templateBlock: Record<string, unknown> = {
|
|
@@ -136,12 +134,14 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }:
|
|
|
136
134
|
duration: 1,
|
|
137
135
|
audioSegment: 0,
|
|
138
136
|
title: headingText,
|
|
139
|
-
...
|
|
137
|
+
...(deriveTemplateInputs(
|
|
140
138
|
annotation.template ?? 'sectionHeader',
|
|
141
139
|
headingText,
|
|
142
|
-
bodyText,
|
|
143
140
|
block.contents,
|
|
144
|
-
|
|
141
|
+
{
|
|
142
|
+
placeholders: true,
|
|
143
|
+
},
|
|
144
|
+
) ?? {}),
|
|
145
145
|
...annotation.params,
|
|
146
146
|
...block.templateOverrides,
|
|
147
147
|
};
|
|
@@ -223,196 +223,6 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }:
|
|
|
223
223
|
|
|
224
224
|
// ── Template Defaults (mirrored from PreviewPanel) ─────────────────
|
|
225
225
|
|
|
226
|
-
/** Extract plain text from block contents. */
|
|
227
|
-
function extractBodyPlainText(contents?: MarkdownBlockNode[]): string {
|
|
228
|
-
if (!contents || contents.length === 0) return '';
|
|
229
|
-
return contents
|
|
230
|
-
.map((n) => extractPlainText(n))
|
|
231
|
-
.join('\n')
|
|
232
|
-
.trim();
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** Extract list items as plain text. */
|
|
236
|
-
function extractListItems(contents?: MarkdownBlockNode[]): string[] {
|
|
237
|
-
if (!contents) return [];
|
|
238
|
-
const items: string[] = [];
|
|
239
|
-
for (const node of contents) {
|
|
240
|
-
if (node.type === 'list') {
|
|
241
|
-
for (const item of (node as MarkdownList).children) {
|
|
242
|
-
const text = extractPlainText(item).trim();
|
|
243
|
-
if (text) items.push(text);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return items;
|
|
248
|
-
}
|
|
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
|
-
|
|
327
|
-
/** Extract table data (headers, rows, alignment) from block contents. */
|
|
328
|
-
function extractTableData(contents?: MarkdownBlockNode[]): {
|
|
329
|
-
headers: string[];
|
|
330
|
-
rows: string[][];
|
|
331
|
-
align?: (('left' | 'right' | 'center') | null)[];
|
|
332
|
-
} | null {
|
|
333
|
-
if (!contents) return null;
|
|
334
|
-
for (const node of contents) {
|
|
335
|
-
if (node.type === 'table') {
|
|
336
|
-
const table = node as MarkdownTable;
|
|
337
|
-
const [headerRow, ...bodyRows] = table.children;
|
|
338
|
-
if (!headerRow) return null;
|
|
339
|
-
const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
|
|
340
|
-
const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
|
|
341
|
-
return { headers, rows, align: table.align };
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Provide sensible default fields for templates that require more than
|
|
349
|
-
* just a `title`. Prevents crashes from undefined required fields.
|
|
350
|
-
*/
|
|
351
|
-
function getTemplateDefaults(
|
|
352
|
-
templateName: string,
|
|
353
|
-
headingText: string,
|
|
354
|
-
bodyText: string,
|
|
355
|
-
contents?: MarkdownBlockNode[],
|
|
356
|
-
): Record<string, unknown> {
|
|
357
|
-
switch (templateName) {
|
|
358
|
-
case 'statHighlight':
|
|
359
|
-
return { stat: headingText, description: bodyText || headingText };
|
|
360
|
-
case 'quote':
|
|
361
|
-
case 'fullBleedQuote':
|
|
362
|
-
case 'pullQuote':
|
|
363
|
-
return { quote: bodyText || headingText };
|
|
364
|
-
case 'factCard':
|
|
365
|
-
return { fact: headingText, explanation: bodyText || headingText };
|
|
366
|
-
case 'comparisonBar':
|
|
367
|
-
return { leftLabel: 'A', leftValue: 60, rightLabel: 'B', rightValue: 40 };
|
|
368
|
-
case 'list': {
|
|
369
|
-
const items = extractListItems(contents);
|
|
370
|
-
return { items: items.length > 0 ? items : ['Item 1', 'Item 2', 'Item 3'] };
|
|
371
|
-
}
|
|
372
|
-
case 'definitionCard':
|
|
373
|
-
return { term: headingText, definition: bodyText || headingText };
|
|
374
|
-
case 'dateEvent':
|
|
375
|
-
return { date: headingText, description: bodyText || headingText };
|
|
376
|
-
case 'dataTable': {
|
|
377
|
-
const tableData = extractTableData(contents);
|
|
378
|
-
return tableData ?? { headers: ['Column'], rows: [['Data']] };
|
|
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
|
-
}
|
|
411
|
-
default:
|
|
412
|
-
return {};
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
226
|
// ── Main Component ─────────────────────────────────────────────────
|
|
417
227
|
|
|
418
228
|
/**
|
|
@@ -444,10 +254,14 @@ export function LinearDocView({
|
|
|
444
254
|
|
|
445
255
|
const renderContext: RenderContext = useMemo(() => {
|
|
446
256
|
const baseTheme = theme ?? DEFAULT_THEME;
|
|
257
|
+
const effectiveTheme = resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme;
|
|
447
258
|
return {
|
|
448
|
-
theme:
|
|
259
|
+
theme: effectiveTheme,
|
|
449
260
|
viewport: activeViewport,
|
|
450
261
|
totalBlocks,
|
|
262
|
+
// Theme atmosphere (vignette/grain/gradient persistent layers) shows
|
|
263
|
+
// on the inline template cards so they match the player's look.
|
|
264
|
+
persistentLayers: effectiveTheme.persistentLayers,
|
|
451
265
|
};
|
|
452
266
|
}, [activeViewport, totalBlocks, theme, resolvedSurface]);
|
|
453
267
|
|