@bendyline/squisq-react 2.0.1 → 2.2.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +6 -0
  4. package/dist/index.d.ts +101 -96
  5. package/dist/index.js +1483 -782
  6. package/dist/squisq-player.full.global.js +4165 -0
  7. package/dist/squisq-player.global.js +561 -168
  8. package/dist/standalone-source.d.ts +1 -0
  9. package/dist/standalone-source.js +1 -1
  10. package/dist/styles/index.css +64 -0
  11. package/package.json +8 -4
  12. package/dist/index.js.map +0 -1
  13. package/dist/squisq-player.css +0 -2
  14. package/dist/squisq-player.css.map +0 -1
  15. package/dist/squisq-player.global.js.map +0 -1
  16. package/src/BlockRenderer.tsx +0 -194
  17. package/src/CaptionOverlay.tsx +0 -111
  18. package/src/DocControlsBottom.tsx +0 -109
  19. package/src/DocControlsOverlay.tsx +0 -190
  20. package/src/DocControlsSidebar.tsx +0 -113
  21. package/src/DocControlsSlideshow.tsx +0 -360
  22. package/src/DocPlayer.tsx +0 -1528
  23. package/src/DocPlayerWithSidebar.tsx +0 -155
  24. package/src/DocProgressBar.tsx +0 -257
  25. package/src/InlineAudioPlayer.tsx +0 -46
  26. package/src/InlineVideoPlayer.tsx +0 -70
  27. package/src/LinearDocView.tsx +0 -569
  28. package/src/MarkdownRenderer.tsx +0 -624
  29. package/src/MediaClipLayer.tsx +0 -145
  30. package/src/SocialCaptionOverlay.tsx +0 -255
  31. package/src/__tests__/BlockRenderer.test.tsx +0 -235
  32. package/src/__tests__/DocControlsSlideshow.test.tsx +0 -221
  33. package/src/__tests__/DocPlayer.test.tsx +0 -635
  34. package/src/__tests__/DocPlayerStylesSentinel.test.tsx +0 -41
  35. package/src/__tests__/DocProgressBar.test.tsx +0 -102
  36. package/src/__tests__/JsonView.test.tsx +0 -111
  37. package/src/__tests__/LinearDocView.test.tsx +0 -426
  38. package/src/__tests__/MapLayer.test.tsx +0 -63
  39. package/src/__tests__/MarkdownRenderer.test.tsx +0 -357
  40. package/src/__tests__/MediaClipLayer.test.tsx +0 -70
  41. package/src/__tests__/MediaContext.test.tsx +0 -51
  42. package/src/__tests__/PathLayer.test.tsx +0 -84
  43. package/src/__tests__/TableLayer.test.tsx +0 -142
  44. package/src/__tests__/VideoLayer.test.tsx +0 -94
  45. package/src/__tests__/exports.test.ts +0 -55
  46. package/src/__tests__/fillStyle.test.tsx +0 -160
  47. package/src/__tests__/standaloneEntry.test.tsx +0 -103
  48. package/src/__tests__/transitionStyles.test.ts +0 -125
  49. package/src/__tests__/useAudioSync.test.ts +0 -49
  50. package/src/__tests__/useDocPlayback.transition.test.ts +0 -113
  51. package/src/__tests__/useJsonViewTokens.test.ts +0 -41
  52. package/src/__tests__/useSlideSwipe.test.ts +0 -81
  53. package/src/__tests__/useViewportOrientation.test.ts +0 -22
  54. package/src/hooks/AudioController.ts +0 -114
  55. package/src/hooks/MediaContext.tsx +0 -97
  56. package/src/hooks/index.ts +0 -11
  57. package/src/hooks/useAudioSync.ts +0 -456
  58. package/src/hooks/useAutoSurface.ts +0 -33
  59. package/src/hooks/useDocPlayback.ts +0 -265
  60. package/src/hooks/useMediaSchedule.ts +0 -39
  61. package/src/hooks/useSlideSwipe.ts +0 -265
  62. package/src/hooks/useViewportOrientation.ts +0 -115
  63. package/src/index.ts +0 -70
  64. package/src/jsonView/JsonView.tsx +0 -51
  65. package/src/jsonView/RenderNode.tsx +0 -51
  66. package/src/jsonView/index.ts +0 -2
  67. package/src/jsonView/json-view.css +0 -206
  68. package/src/jsonView/useJsonViewTokens.ts +0 -32
  69. package/src/jsonView/viewers.tsx +0 -343
  70. package/src/layers/ImageLayer.tsx +0 -297
  71. package/src/layers/MapLayer.tsx +0 -185
  72. package/src/layers/PathLayer.tsx +0 -155
  73. package/src/layers/ShapeLayer.tsx +0 -158
  74. package/src/layers/TableLayer.tsx +0 -129
  75. package/src/layers/TextLayer.tsx +0 -571
  76. package/src/layers/TreeLayer.tsx +0 -167
  77. package/src/layers/VideoLayer.tsx +0 -171
  78. package/src/layers/index.ts +0 -6
  79. package/src/standalone-entry.tsx +0 -337
  80. package/src/standalone-source.d.ts +0 -10
  81. package/src/styles/doc-animations.css +0 -2431
  82. package/src/styles/index.css +0 -7
  83. package/src/types.ts +0 -183
  84. package/src/utils/animationUtils.ts +0 -13
  85. package/src/utils/fillStyle.tsx +0 -148
  86. package/src/utils/layerUtils.ts +0 -42
  87. package/src/utils/mapTileUtils.ts +0 -375
@@ -1,194 +0,0 @@
1
- /**
2
- * BlockRenderer Component
3
- *
4
- * Renders a single block as an SVG element with all its layers.
5
- * Each layer is rendered back-to-front (first layer is background).
6
- * Handles positioning, animations, and transitions.
7
- */
8
-
9
- import { useId } from 'react';
10
- import type { Block, Layer, Transition } from '@bendyline/squisq/schemas';
11
- import { resolveTransitionDuration } from '@bendyline/squisq/schemas';
12
- import { ImageLayer } from './layers/ImageLayer';
13
- import { TextLayer } from './layers/TextLayer';
14
- import { ShapeLayer } from './layers/ShapeLayer';
15
- import { PathLayer } from './layers/PathLayer';
16
- import { MapLayer } from './layers/MapLayer';
17
- import { VideoLayer } from './layers/VideoLayer';
18
- import { TableLayer } from './layers/TableLayer';
19
- import { TreeLayer } from './layers/TreeLayer';
20
- import { getTransitionClass } from './utils/animationUtils';
21
-
22
- /** Default viewport dimensions (1080p landscape). */
23
- const DEFAULT_VIEWPORT = {
24
- width: 1920,
25
- height: 1080,
26
- };
27
-
28
- /** Viewport configuration type */
29
- export interface ViewportDimensions {
30
- width: number;
31
- height: number;
32
- }
33
-
34
- interface BlockRendererProps {
35
- /** The block to render */
36
- block: Block;
37
- /** Current time relative to block start (seconds) */
38
- blockTime: number;
39
- /** Base path for resolving media URLs */
40
- basePath: string;
41
- /** Whether this block is entering (for transition) */
42
- isEntering?: boolean;
43
- /** Whether this block is exiting (for transition) */
44
- isExiting?: boolean;
45
- /** Transition to apply. Defaults to block.transition. */
46
- transition?: Transition;
47
- /** Viewport dimensions (defaults to 1920x1080 landscape) */
48
- viewport?: ViewportDimensions;
49
- /** Whether the doc is currently playing (controls video playback) */
50
- isPlaying?: boolean;
51
- /**
52
- * Whether to render block transitions and layer animations (default: true).
53
- * Disabling this only removes authored/render-style motion; timed video
54
- * layers continue to advance normally.
55
- */
56
- animationsEnabled?: boolean;
57
- }
58
-
59
- export function BlockRenderer({
60
- block,
61
- blockTime,
62
- basePath,
63
- isEntering = false,
64
- isExiting = false,
65
- transition,
66
- viewport = DEFAULT_VIEWPORT,
67
- isPlaying,
68
- animationsEnabled = true,
69
- }: BlockRendererProps) {
70
- // Build transition class and inline style for dynamic duration
71
- let transitionClass = '';
72
- const transitionStyle: Record<string, string> = {};
73
- const activeTransition = animationsEnabled ? (transition ?? block.transition) : undefined;
74
- if (activeTransition && isEntering) {
75
- transitionClass = getTransitionClass(activeTransition.type, true, activeTransition.direction);
76
- transitionStyle['--transition-duration'] = `${resolveTransitionDuration(activeTransition)}s`;
77
- } else if (activeTransition && isExiting) {
78
- transitionClass = getTransitionClass(activeTransition.type, false, activeTransition.direction);
79
- transitionStyle['--transition-duration'] = `${resolveTransitionDuration(activeTransition)}s`;
80
- }
81
-
82
- // React's instance id keeps SVG fragment references local even when the
83
- // same block is rendered in a thumbnail, preview, and player at once.
84
- const instanceId = useId().replace(/:/g, '');
85
- const clipId = `vb-clip-${instanceId}-${block.id}`;
86
-
87
- return (
88
- <svg
89
- className={`block-svg ${transitionClass}`}
90
- style={transitionStyle}
91
- viewBox={`0 0 ${viewport.width} ${viewport.height}`}
92
- preserveAspectRatio="xMidYMid meet"
93
- overflow="hidden"
94
- data-block-id={block.id}
95
- >
96
- {/* Clip path matching the viewBox -- prevents Ken Burns animations from
97
- bleeding outside the block area (foreignObject + transform: scale
98
- can escape SVG overflow="hidden" in some browsers) */}
99
- <defs>
100
- <clipPath id={clipId}>
101
- <rect x="0" y="0" width={viewport.width} height={viewport.height} />
102
- </clipPath>
103
- </defs>
104
-
105
- {/* All layers clipped to viewBox bounds */}
106
- <g clipPath={`url(#${clipId})`}>
107
- {(block.layers ?? []).map((layer) => (
108
- <LayerRenderer
109
- key={layer.id}
110
- layer={layer}
111
- basePath={basePath}
112
- viewport={viewport}
113
- blockTime={blockTime}
114
- isPlaying={isPlaying}
115
- animationsEnabled={animationsEnabled}
116
- />
117
- ))}
118
- </g>
119
- </svg>
120
- );
121
- }
122
-
123
- interface LayerRendererProps {
124
- layer: Layer;
125
- basePath: string;
126
- viewport: { width: number; height: number };
127
- blockTime: number;
128
- isPlaying?: boolean;
129
- animationsEnabled: boolean;
130
- }
131
-
132
- /**
133
- * Dispatch to the appropriate layer component based on type.
134
- */
135
- function LayerRenderer({
136
- layer,
137
- basePath,
138
- viewport,
139
- blockTime,
140
- isPlaying,
141
- animationsEnabled,
142
- }: LayerRendererProps) {
143
- // Render policy must not mutate caller-owned Docs. A shallow copy is enough:
144
- // every layer renderer reads animation only from the base layer field.
145
- const renderedLayer: Layer =
146
- animationsEnabled || !layer.animation ? layer : { ...layer, animation: undefined };
147
-
148
- switch (renderedLayer.type) {
149
- case 'image':
150
- return (
151
- <ImageLayer
152
- layer={renderedLayer}
153
- basePath={basePath}
154
- viewport={viewport}
155
- blockTime={blockTime}
156
- animationsEnabled={animationsEnabled}
157
- />
158
- );
159
- case 'text':
160
- return <TextLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
161
- case 'shape':
162
- return <ShapeLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
163
- case 'path':
164
- return <PathLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
165
- case 'map':
166
- return (
167
- <MapLayer
168
- layer={renderedLayer}
169
- basePath={basePath}
170
- viewport={viewport}
171
- blockTime={blockTime}
172
- />
173
- );
174
- case 'video':
175
- return (
176
- <VideoLayer
177
- layer={renderedLayer}
178
- basePath={basePath}
179
- viewport={viewport}
180
- blockTime={blockTime}
181
- isPlaying={isPlaying}
182
- />
183
- );
184
- case 'table':
185
- return <TableLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
186
- case 'tree':
187
- return <TreeLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
188
- default:
189
- console.warn(`Unknown layer type: ${(renderedLayer as Layer).type}`);
190
- return null;
191
- }
192
- }
193
-
194
- export default BlockRenderer;
@@ -1,111 +0,0 @@
1
- /**
2
- * CaptionOverlay Component
3
- *
4
- * Displays closed captions synchronized with audio playback.
5
- * Supports two styles:
6
- *
7
- * - `'standard'` (default): Traditional broadcast captions — small white text
8
- * on a semi-transparent black badge at the top of the player.
9
- * - `'social'`: Social media-style (Instagram/TikTok) — large centered words
10
- * with the active word highlighted in the theme's primary color.
11
- */
12
-
13
- import type { CaptionTrack, ViewportConfig } from '@bendyline/squisq/schemas';
14
- import { getCaptionAtTime } from '@bendyline/squisq/schemas';
15
- import type { Theme } from '@bendyline/squisq/schemas';
16
- import type { CaptionStyle } from './types';
17
- import { SocialCaptionOverlay } from './SocialCaptionOverlay';
18
-
19
- interface CaptionOverlayProps {
20
- /** Caption track with timestamped phrases */
21
- captions: CaptionTrack | undefined;
22
- /** Current playback time in seconds */
23
- currentTime: number;
24
- /** Whether captions are enabled */
25
- enabled?: boolean;
26
- /** Font size in pixels for standard style (default: 16) */
27
- fontSize?: number;
28
- /** Caption display style (default: 'standard') */
29
- captionStyle?: CaptionStyle;
30
- /** Theme for social-style caption colors and fonts */
31
- theme?: Theme;
32
- /** Viewport config for social-style font scaling */
33
- viewport?: ViewportConfig;
34
- }
35
-
36
- export function CaptionOverlay({
37
- captions,
38
- currentTime,
39
- enabled = true,
40
- fontSize = 16,
41
- captionStyle = 'standard',
42
- theme,
43
- viewport,
44
- }: CaptionOverlayProps) {
45
- // Delegate to social overlay when that style is active
46
- if (captionStyle === 'social') {
47
- return (
48
- <SocialCaptionOverlay
49
- captions={captions}
50
- currentTime={currentTime}
51
- enabled={enabled}
52
- theme={theme}
53
- viewport={viewport}
54
- />
55
- );
56
- }
57
-
58
- // Standard caption rendering
59
- const phrase = enabled && captions ? getCaptionAtTime(captions, currentTime) : null;
60
- const captionText = phrase?.text ?? null;
61
-
62
- return (
63
- <div
64
- className="caption-overlay"
65
- style={{
66
- position: 'absolute',
67
- top: '6px',
68
- left: '50%',
69
- transform: 'translateX(-50%)',
70
- zIndex: 50,
71
- pointerEvents: 'none',
72
- maxWidth: '100%',
73
- width: '100%',
74
- textAlign: 'center',
75
- opacity: captionText ? 1 : 0,
76
- transition: 'opacity 0.15s ease-in-out',
77
- padding: '0 4px',
78
- boxSizing: 'border-box',
79
- }}
80
- >
81
- {captionText && (
82
- <div
83
- style={{
84
- display: 'inline-block',
85
- padding: '3px 10px',
86
- background: 'rgba(0, 0, 0, 0.65)',
87
- borderRadius: '4px',
88
- backdropFilter: 'blur(4px)',
89
- }}
90
- >
91
- <span
92
- style={{
93
- color: '#ffffff',
94
- fontSize: `${fontSize}px`,
95
- fontFamily: "'Hanken Grotesk', system-ui, sans-serif",
96
- fontWeight: 500,
97
- lineHeight: 1.25,
98
- textShadow: '0 1px 3px rgba(0,0,0,0.5)',
99
- whiteSpace: 'pre-wrap',
100
- wordWrap: 'break-word',
101
- }}
102
- >
103
- {captionText}
104
- </span>
105
- </div>
106
- )}
107
- </div>
108
- );
109
- }
110
-
111
- export default CaptionOverlay;
@@ -1,109 +0,0 @@
1
- /**
2
- * DocControlsBottom Component
3
- *
4
- * Horizontal control strip that renders below the doc player video.
5
- * An alternative to the overlay for landscape mode -- controls sit in a
6
- * separate bar below the video instead of overlapping the content.
7
- *
8
- * Contains: restart, play/pause, time display, progress bar, segment
9
- * indicator, CC toggle. Same controls as the overlay but in an external bar.
10
- *
11
- * Related Files:
12
- * - DocControlsOverlay.tsx -- Overlay variant (default landscape)
13
- * - DocControlsSidebar.tsx -- Sidebar variant (portrait)
14
- * - DocProgressBar.tsx -- Shared progress bar component
15
- * - types.ts -- PlaybackState/PlaybackActions interfaces
16
- */
17
-
18
- import { DocProgressBar } from './DocProgressBar';
19
- import type { PlaybackState, PlaybackActions, BlockMarker } from './types';
20
- import { formatTime } from './types';
21
- import type { Block } from '@bendyline/squisq/schemas';
22
-
23
- interface DocControlsBottomProps {
24
- state: PlaybackState;
25
- actions: PlaybackActions;
26
- blockMarkers: BlockMarker[];
27
- expandedBlocks: Block[];
28
- getBlockTitle?: (block: Block) => string;
29
- }
30
-
31
- export function DocControlsBottom({
32
- state,
33
- actions,
34
- blockMarkers,
35
- expandedBlocks,
36
- getBlockTitle,
37
- }: DocControlsBottomProps) {
38
- return (
39
- <div className="doc-controls-bottom">
40
- {/* Restart button */}
41
- <button
42
- className="bottom-ctrl-btn"
43
- onClick={actions.restart}
44
- title="Restart"
45
- aria-label="Restart from beginning"
46
- >
47
- <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
48
- <path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" />
49
- </svg>
50
- </button>
51
-
52
- {/* Play/Pause button */}
53
- <button
54
- className="bottom-ctrl-btn bottom-play-btn"
55
- onClick={actions.toggle}
56
- aria-label={state.isPlaying ? 'Pause' : 'Play'}
57
- >
58
- {state.isPlaying ? (
59
- <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
60
- <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
61
- </svg>
62
- ) : (
63
- <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
64
- <path d="M8 5v14l11-7z" />
65
- </svg>
66
- )}
67
- </button>
68
-
69
- {/* Time display */}
70
- <span className="bottom-time">
71
- {formatTime(state.currentTime)} / {formatTime(state.totalDuration)}
72
- </span>
73
-
74
- {/* Progress bar */}
75
- <DocProgressBar
76
- state={state}
77
- actions={actions}
78
- blockMarkers={blockMarkers}
79
- expandedBlocks={expandedBlocks}
80
- getBlockTitle={getBlockTitle}
81
- />
82
-
83
- {/* Segment indicator */}
84
- <span className="bottom-segment">
85
- {state.currentBlockIndex + 1}/{state.totalBlocks}
86
- </span>
87
-
88
- {/* Caption mode cycle */}
89
- {state.hasCaptions && (
90
- <button
91
- className={`bottom-ctrl-btn ${state.captionMode !== 'off' ? 'bottom-ctrl-btn--active' : ''}`}
92
- onClick={() => actions.cycleCaptionMode()}
93
- title={
94
- state.captionMode === 'off'
95
- ? 'Captions: Off'
96
- : state.captionMode === 'standard'
97
- ? 'Captions: Standard'
98
- : 'Captions: Social'
99
- }
100
- aria-label="Cycle caption style"
101
- >
102
- <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
103
- <path d="M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" />
104
- </svg>
105
- </button>
106
- )}
107
- </div>
108
- );
109
- }
@@ -1,190 +0,0 @@
1
- /**
2
- * DocControlsOverlay Component
3
- *
4
- * The default overlay controls that render at the bottom of the doc player
5
- * video with a gradient background. Contains all playback controls: restart,
6
- * play/pause, time display, progress bar with block markers, segment indicator,
7
- * and caption toggle.
8
- *
9
- * This is the original control layout extracted from DocPlayer. It's used
10
- * for the 'overlay' controls layout mode (Flow mode, landscape default).
11
- *
12
- * Related Files:
13
- * - DocPlayer.tsx -- Parent component
14
- * - DocProgressBar.tsx -- Progress bar sub-component
15
- * - types.ts -- Shared type definitions
16
- */
17
-
18
- import { DocProgressBar } from './DocProgressBar';
19
- import type { PlaybackState, PlaybackActions, BlockMarker } from './types';
20
- import { formatTime } from './types';
21
- import type { Block } from '@bendyline/squisq/schemas';
22
-
23
- interface DocControlsOverlayProps {
24
- state: PlaybackState;
25
- actions: PlaybackActions;
26
- blockMarkers: BlockMarker[];
27
- expandedBlocks: Block[];
28
- getBlockTitle?: (block: Block) => string;
29
- }
30
-
31
- export function DocControlsOverlay({
32
- state,
33
- actions,
34
- blockMarkers,
35
- expandedBlocks,
36
- getBlockTitle,
37
- }: DocControlsOverlayProps) {
38
- return (
39
- <div
40
- className="doc-player__controls"
41
- style={{
42
- position: 'absolute',
43
- bottom: 0,
44
- left: 0,
45
- right: 0,
46
- padding: '12px 16px',
47
- background: 'linear-gradient(transparent, rgba(0,0,0,0.8))',
48
- display: 'flex',
49
- alignItems: 'center',
50
- gap: '8px',
51
- zIndex: 100,
52
- }}
53
- >
54
- {/* Restart button */}
55
- <button
56
- onClick={actions.restart}
57
- style={{
58
- background: 'none',
59
- border: 'none',
60
- color: 'rgba(255,255,255,0.7)',
61
- cursor: 'pointer',
62
- padding: '8px',
63
- fontSize: '14px',
64
- display: 'flex',
65
- alignItems: 'center',
66
- }}
67
- title="Restart"
68
- aria-label="Restart from beginning"
69
- >
70
- <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
71
- <path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" />
72
- </svg>
73
- </button>
74
-
75
- {/* Play/Pause button */}
76
- <button
77
- onClick={actions.toggle}
78
- style={{
79
- background: 'rgba(255,255,255,0.2)',
80
- border: 'none',
81
- borderRadius: '50%',
82
- color: 'white',
83
- cursor: 'pointer',
84
- padding: '10px',
85
- fontSize: '16px',
86
- display: 'flex',
87
- alignItems: 'center',
88
- justifyContent: 'center',
89
- width: '40px',
90
- height: '40px',
91
- }}
92
- aria-label={state.isPlaying ? 'Pause' : 'Play'}
93
- >
94
- {state.isPlaying ? (
95
- <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
96
- <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
97
- </svg>
98
- ) : (
99
- <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
100
- <path d="M8 5v14l11-7z" />
101
- </svg>
102
- )}
103
- </button>
104
-
105
- {/* Time display */}
106
- <span style={{ color: 'white', fontSize: '12px', minWidth: '80px', fontFamily: 'monospace' }}>
107
- {formatTime(state.currentTime)} / {formatTime(state.totalDuration)}
108
- </span>
109
-
110
- {/* Progress bar */}
111
- <DocProgressBar
112
- state={state}
113
- actions={actions}
114
- blockMarkers={blockMarkers}
115
- expandedBlocks={expandedBlocks}
116
- getBlockTitle={getBlockTitle}
117
- />
118
-
119
- {/* Segment indicator */}
120
- <span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '11px', whiteSpace: 'nowrap' }}>
121
- {state.currentBlockIndex + 1}/{state.totalBlocks}
122
- </span>
123
-
124
- {/* Caption mode cycle button: off → standard → social → off */}
125
- {state.hasCaptions && (
126
- <button
127
- onClick={() => actions.cycleCaptionMode()}
128
- style={{
129
- background: state.captionMode !== 'off' ? 'rgba(255,255,255,0.2)' : 'none',
130
- border: 'none',
131
- color: state.captionMode !== 'off' ? 'white' : 'rgba(255,255,255,0.5)',
132
- cursor: 'pointer',
133
- padding: '8px',
134
- fontSize: '12px',
135
- display: 'flex',
136
- alignItems: 'center',
137
- gap: '4px',
138
- borderRadius: '4px',
139
- }}
140
- title={
141
- state.captionMode === 'off'
142
- ? 'Captions: Off (click for Standard)'
143
- : state.captionMode === 'standard'
144
- ? 'Captions: Standard (click for Social)'
145
- : 'Captions: Social (click to turn off)'
146
- }
147
- aria-label="Cycle caption style"
148
- >
149
- <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
150
- <path d="M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" />
151
- </svg>
152
- {state.captionMode !== 'off' && (
153
- <span style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
154
- {state.captionMode === 'standard' ? 'CC' : 'SM'}
155
- </span>
156
- )}
157
- </button>
158
- )}
159
-
160
- {/* Fullscreen toggle button */}
161
- {actions.toggleFullscreen && (
162
- <button
163
- onClick={actions.toggleFullscreen}
164
- style={{
165
- background: state.isFullscreen ? 'rgba(255,255,255,0.2)' : 'none',
166
- border: 'none',
167
- color: state.isFullscreen ? 'white' : 'rgba(255,255,255,0.5)',
168
- cursor: 'pointer',
169
- padding: '8px',
170
- display: 'flex',
171
- alignItems: 'center',
172
- borderRadius: '4px',
173
- }}
174
- title={state.isFullscreen ? 'Exit fullscreen (F)' : 'Fullscreen (F)'}
175
- aria-label={state.isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
176
- >
177
- {state.isFullscreen ? (
178
- <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
179
- <path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" />
180
- </svg>
181
- ) : (
182
- <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
183
- <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
184
- </svg>
185
- )}
186
- </button>
187
- )}
188
- </div>
189
- );
190
- }