@bendyline/squisq-react 1.0.2 → 1.1.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/dist/BlockRenderer.d.ts.map +1 -1
  2. package/dist/BlockRenderer.js +3 -0
  3. package/dist/BlockRenderer.js.map +1 -1
  4. package/dist/CaptionOverlay.d.ts +16 -7
  5. package/dist/CaptionOverlay.d.ts.map +1 -1
  6. package/dist/CaptionOverlay.js +7 -3
  7. package/dist/CaptionOverlay.js.map +1 -1
  8. package/dist/DocControlsBottom.d.ts.map +1 -1
  9. package/dist/DocControlsBottom.js +5 -1
  10. package/dist/DocControlsBottom.js.map +1 -1
  11. package/dist/DocControlsOverlay.d.ts.map +1 -1
  12. package/dist/DocControlsOverlay.js +9 -4
  13. package/dist/DocControlsOverlay.js.map +1 -1
  14. package/dist/DocControlsSidebar.d.ts.map +1 -1
  15. package/dist/DocControlsSidebar.js +5 -1
  16. package/dist/DocControlsSidebar.js.map +1 -1
  17. package/dist/DocPlayer.d.ts +5 -2
  18. package/dist/DocPlayer.d.ts.map +1 -1
  19. package/dist/DocPlayer.js +36 -7
  20. package/dist/DocPlayer.js.map +1 -1
  21. package/dist/DocPlayerWithSidebar.d.ts.map +1 -1
  22. package/dist/DocPlayerWithSidebar.js +1 -0
  23. package/dist/DocPlayerWithSidebar.js.map +1 -1
  24. package/dist/DocProgressBar.js +2 -2
  25. package/dist/LinearDocView.d.ts.map +1 -1
  26. package/dist/LinearDocView.js +130 -6
  27. package/dist/LinearDocView.js.map +1 -1
  28. package/dist/SocialCaptionOverlay.d.ts +27 -0
  29. package/dist/SocialCaptionOverlay.d.ts.map +1 -0
  30. package/dist/SocialCaptionOverlay.js +184 -0
  31. package/dist/SocialCaptionOverlay.js.map +1 -0
  32. package/dist/__tests__/DocControlsSlideshow.test.js +1 -0
  33. package/dist/__tests__/DocControlsSlideshow.test.js.map +1 -1
  34. package/dist/__tests__/TableLayer.test.d.ts +2 -0
  35. package/dist/__tests__/TableLayer.test.d.ts.map +1 -0
  36. package/dist/__tests__/TableLayer.test.js +121 -0
  37. package/dist/__tests__/TableLayer.test.js.map +1 -0
  38. package/dist/hooks/useAudioSync.d.ts.map +1 -1
  39. package/dist/hooks/useAudioSync.js +4 -2
  40. package/dist/hooks/useAudioSync.js.map +1 -1
  41. package/dist/hooks/useDocPlayback.d.ts.map +1 -1
  42. package/dist/hooks/useDocPlayback.js +7 -4
  43. package/dist/hooks/useDocPlayback.js.map +1 -1
  44. package/dist/index.d.ts +3 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/layers/ShapeLayer.d.ts.map +1 -1
  49. package/dist/layers/ShapeLayer.js +7 -3
  50. package/dist/layers/ShapeLayer.js.map +1 -1
  51. package/dist/layers/TableLayer.d.ts +23 -0
  52. package/dist/layers/TableLayer.d.ts.map +1 -0
  53. package/dist/layers/TableLayer.js +58 -0
  54. package/dist/layers/TableLayer.js.map +1 -0
  55. package/dist/layers/TextLayer.js +1 -1
  56. package/dist/layers/TextLayer.js.map +1 -1
  57. package/dist/squisq-player.global.js +91 -5
  58. package/dist/squisq-player.global.js.map +1 -1
  59. package/dist/standalone-entry.d.ts +8 -0
  60. package/dist/standalone-entry.d.ts.map +1 -1
  61. package/dist/standalone-entry.js +6 -3
  62. package/dist/standalone-entry.js.map +1 -1
  63. package/dist/standalone-source.js +1 -1
  64. package/dist/types.d.ts +19 -0
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js.map +1 -1
  67. package/package.json +2 -2
  68. package/src/BlockRenderer.tsx +3 -0
  69. package/src/CaptionOverlay.tsx +33 -8
  70. package/src/DocControlsBottom.tsx +11 -5
  71. package/src/DocControlsOverlay.tsx +18 -6
  72. package/src/DocControlsSidebar.tsx +11 -5
  73. package/src/DocPlayer.tsx +56 -9
  74. package/src/DocPlayerWithSidebar.tsx +1 -0
  75. package/src/DocProgressBar.tsx +2 -2
  76. package/src/LinearDocView.tsx +149 -12
  77. package/src/SocialCaptionOverlay.tsx +254 -0
  78. package/src/__tests__/DocControlsSlideshow.test.tsx +1 -0
  79. package/src/__tests__/TableLayer.test.tsx +142 -0
  80. package/src/hooks/useAudioSync.ts +5 -2
  81. package/src/hooks/useDocPlayback.ts +8 -3
  82. package/src/index.ts +4 -0
  83. package/src/layers/ShapeLayer.tsx +8 -3
  84. package/src/layers/TableLayer.tsx +129 -0
  85. package/src/layers/TextLayer.tsx +1 -1
  86. package/src/standalone-entry.tsx +23 -3
  87. package/src/types.ts +21 -0
package/dist/types.d.ts CHANGED
@@ -28,6 +28,21 @@ export type ControlsLayout = 'overlay' | 'sidebar' | 'bottom';
28
28
  * No audio, no timeline, no controls — just a scrollable page.
29
29
  */
30
30
  export type DisplayMode = 'video' | 'slideshow' | 'linear';
31
+ /**
32
+ * Caption display style.
33
+ *
34
+ * - `'standard'` — Traditional broadcast-style captions: small white text
35
+ * on a semi-transparent black badge at the top of the player.
36
+ * - `'social'` — Social media-style (Instagram/TikTok): large centered words
37
+ * showing 3-5 words at a time with the active word highlighted in the
38
+ * theme's primary color. Font and colors pulled from the active theme.
39
+ */
40
+ export type CaptionStyle = 'standard' | 'social';
41
+ /**
42
+ * Caption display mode — combines enable/disable with style selection.
43
+ * The CC button cycles through: off → standard → social → off.
44
+ */
45
+ export type CaptionMode = 'off' | 'standard' | 'social';
31
46
  /** Slide navigation actions for slideshow display mode */
32
47
  export interface SlideNavActions {
33
48
  /** Navigate to the next slide */
@@ -47,6 +62,8 @@ export interface PlaybackState {
47
62
  docProgress: number;
48
63
  hasCaptions: boolean;
49
64
  captionsEnabled: boolean;
65
+ /** Current caption display mode (off, standard, social). */
66
+ captionMode: CaptionMode;
50
67
  isFullscreen?: boolean;
51
68
  /** Current audio segment index (0-based) */
52
69
  currentSegmentIndex: number;
@@ -61,6 +78,8 @@ export interface PlaybackActions {
61
78
  restart: () => void;
62
79
  seekTo: (time: number) => void;
63
80
  setCaptionsEnabled: (enabled: boolean) => void;
81
+ /** Cycle caption mode: off → standard → social → off */
82
+ cycleCaptionMode: () => void;
64
83
  toggleFullscreen?: () => void;
65
84
  }
66
85
  /** Block marker data for the progress bar */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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;AAqIH,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"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA0JH,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.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "React component library for doc playback, block rendering, and media layers",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -52,7 +52,7 @@
52
52
  "react-dom": "^18.0.0 || ^19.0.0"
53
53
  },
54
54
  "dependencies": {
55
- "@bendyline/squisq": "1.1.0"
55
+ "@bendyline/squisq": "1.2.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/react": "^18.0.0",
@@ -12,6 +12,7 @@ import { TextLayer } from './layers/TextLayer';
12
12
  import { ShapeLayer } from './layers/ShapeLayer';
13
13
  import { MapLayer } from './layers/MapLayer';
14
14
  import { VideoLayer } from './layers/VideoLayer';
15
+ import { TableLayer } from './layers/TableLayer';
15
16
  import { getTransitionClass } from './utils/animationUtils';
16
17
 
17
18
  /** Default viewport dimensions (1080p landscape) - for backwards compatibility */
@@ -137,6 +138,8 @@ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }: Laye
137
138
  isPlaying={isPlaying}
138
139
  />
139
140
  );
141
+ case 'table':
142
+ return <TableLayer layer={layer} viewport={viewport} blockTime={blockTime} />;
140
143
  default:
141
144
  console.warn(`Unknown layer type: ${(layer as Layer).type}`);
142
145
  return null;
@@ -2,15 +2,19 @@
2
2
  * CaptionOverlay Component
3
3
  *
4
4
  * Displays closed captions synchronized with audio playback.
5
- * Captions cross-fade smoothly using CSS transitions on a single
6
- * persistent element (no mount/unmount flicker).
5
+ * Supports two styles:
7
6
  *
8
- * The captions are generated by the postprocess-doc command based
9
- * on article text and audio segment timing.
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.
10
11
  */
11
12
 
12
- import type { CaptionTrack } from '@bendyline/squisq/schemas';
13
+ import type { CaptionTrack, ViewportConfig } from '@bendyline/squisq/schemas';
13
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';
14
18
 
15
19
  interface CaptionOverlayProps {
16
20
  /** Caption track with timestamped phrases */
@@ -19,8 +23,14 @@ interface CaptionOverlayProps {
19
23
  currentTime: number;
20
24
  /** Whether captions are enabled */
21
25
  enabled?: boolean;
22
- /** Font size in pixels (default: 16) */
26
+ /** Font size in pixels for standard style (default: 16) */
23
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;
24
34
  }
25
35
 
26
36
  export function CaptionOverlay({
@@ -28,9 +38,24 @@ export function CaptionOverlay({
28
38
  currentTime,
29
39
  enabled = true,
30
40
  fontSize = 16,
41
+ captionStyle = 'standard',
42
+ theme,
43
+ viewport,
31
44
  }: CaptionOverlayProps) {
32
- // Derive caption directly from currentTime -- pure computation, no hooks.
33
- // This avoids race conditions between timeupdate events and async state updates.
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
34
59
  const phrase = enabled && captions ? getCaptionAtTime(captions, currentTime) : null;
35
60
  const captionText = phrase?.text ?? null;
36
61
 
@@ -85,13 +85,19 @@ export function DocControlsBottom({
85
85
  {state.currentBlockIndex + 1}/{state.totalBlocks}
86
86
  </span>
87
87
 
88
- {/* Caption toggle */}
88
+ {/* Caption mode cycle */}
89
89
  {state.hasCaptions && (
90
90
  <button
91
- className={`bottom-ctrl-btn ${state.captionsEnabled ? 'bottom-ctrl-btn--active' : ''}`}
92
- onClick={() => actions.setCaptionsEnabled(!state.captionsEnabled)}
93
- title={state.captionsEnabled ? 'Hide captions' : 'Show captions'}
94
- aria-label={state.captionsEnabled ? 'Hide captions' : 'Show captions'}
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"
95
101
  >
96
102
  <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
97
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" />
@@ -121,27 +121,39 @@ export function DocControlsOverlay({
121
121
  {state.currentBlockIndex + 1}/{state.totalBlocks}
122
122
  </span>
123
123
 
124
- {/* Caption toggle button */}
124
+ {/* Caption mode cycle button: off → standard → social → off */}
125
125
  {state.hasCaptions && (
126
126
  <button
127
- onClick={() => actions.setCaptionsEnabled(!state.captionsEnabled)}
127
+ onClick={() => actions.cycleCaptionMode()}
128
128
  style={{
129
- background: state.captionsEnabled ? 'rgba(255,255,255,0.2)' : 'none',
129
+ background: state.captionMode !== 'off' ? 'rgba(255,255,255,0.2)' : 'none',
130
130
  border: 'none',
131
- color: state.captionsEnabled ? 'white' : 'rgba(255,255,255,0.5)',
131
+ color: state.captionMode !== 'off' ? 'white' : 'rgba(255,255,255,0.5)',
132
132
  cursor: 'pointer',
133
133
  padding: '8px',
134
134
  fontSize: '12px',
135
135
  display: 'flex',
136
136
  alignItems: 'center',
137
+ gap: '4px',
137
138
  borderRadius: '4px',
138
139
  }}
139
- title={state.captionsEnabled ? 'Hide captions' : 'Show captions'}
140
- aria-label={state.captionsEnabled ? 'Hide captions' : 'Show captions'}
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"
141
148
  >
142
149
  <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
143
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" />
144
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
+ )}
145
157
  </button>
146
158
  )}
147
159
 
@@ -69,13 +69,19 @@ export function DocControlsSidebar({ state, actions }: DocControlsSidebarProps)
69
69
  {state.currentBlockIndex + 1}/{state.totalBlocks}
70
70
  </div>
71
71
 
72
- {/* Caption toggle */}
72
+ {/* Caption mode cycle */}
73
73
  {state.hasCaptions && (
74
74
  <button
75
- className={`sidebar-ctrl-btn ${state.captionsEnabled ? 'sidebar-ctrl-btn--active' : ''}`}
76
- onClick={() => actions.setCaptionsEnabled(!state.captionsEnabled)}
77
- title={state.captionsEnabled ? 'Hide captions' : 'Show captions'}
78
- aria-label={state.captionsEnabled ? 'Hide captions' : 'Show captions'}
75
+ className={`sidebar-ctrl-btn ${state.captionMode !== 'off' ? 'sidebar-ctrl-btn--active' : ''}`}
76
+ onClick={() => actions.cycleCaptionMode()}
77
+ title={
78
+ state.captionMode === 'off'
79
+ ? 'Captions: Off'
80
+ : state.captionMode === 'standard'
81
+ ? 'Captions: Standard'
82
+ : 'Captions: Social'
83
+ }
84
+ aria-label="Cycle caption style"
79
85
  >
80
86
  <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
81
87
  <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" />
package/src/DocPlayer.tsx CHANGED
@@ -48,6 +48,8 @@ import type {
48
48
  PlaybackActions,
49
49
  BlockMarker,
50
50
  DisplayMode,
51
+ CaptionStyle,
52
+ CaptionMode,
51
53
  SlideNavActions,
52
54
  SquisqWindow,
53
55
  } from './types';
@@ -169,6 +171,9 @@ interface DocPlayerProps {
169
171
  * template-annotated sections as inline SVG cards. No audio, no timeline.
170
172
  */
171
173
  displayMode?: DisplayMode;
174
+ /** Caption display style (default: 'standard').
175
+ * 'social' shows large centered words with the active word highlighted. */
176
+ captionStyle?: CaptionStyle;
172
177
  }
173
178
 
174
179
  export function DocPlayer({
@@ -192,6 +197,7 @@ export function DocPlayer({
192
197
  forceViewport,
193
198
  displayMode = 'video',
194
199
  theme,
200
+ captionStyle = 'standard',
195
201
  }: DocPlayerProps) {
196
202
  const isSlideshowMode = displayMode === 'slideshow';
197
203
  const isLinearMode = displayMode === 'linear';
@@ -433,7 +439,16 @@ export function DocPlayer({
433
439
  });
434
440
  });
435
441
  };
436
- w.getDuration = () => totalDuration;
442
+ w.getDuration = () => {
443
+ // When audio is present totalDuration comes from audio segments.
444
+ // For audio-less docs, compute from block timings instead.
445
+ if (totalDuration > 0) return totalDuration;
446
+ if (expandedBlocks.length > 0) {
447
+ const last = expandedBlocks[expandedBlocks.length - 1];
448
+ return last.startTime + last.duration;
449
+ }
450
+ return 0;
451
+ };
437
452
  // Expose block metadata for testing -- allows tests to find specific templates
438
453
  w.getBlocks = () =>
439
454
  expandedBlocks.map((s: Block) => ({
@@ -494,14 +509,36 @@ export function DocPlayer({
494
509
  // eslint-disable-next-line react-hooks/exhaustive-deps -- script is a stable prop; re-registering on every script change is unnecessary
495
510
  }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
496
511
 
497
- // Captions state: use prop if provided, otherwise default to true
498
- const captionsEnabled = captionsEnabledProp !== undefined ? captionsEnabledProp : true;
512
+ // Caption mode state: cycles through off standard social → off
513
+ // The captionStyle prop sets the default active style; captionsEnabledProp
514
+ // can override the initial on/off state.
515
+ const defaultMode: CaptionMode =
516
+ captionsEnabledProp === false ? 'off' : captionStyle || 'standard';
517
+ const [captionMode, setCaptionMode] = useState<CaptionMode>(defaultMode);
518
+
519
+ // Derive captionsEnabled and active style from the mode
520
+ const captionsEnabled = captionMode !== 'off';
521
+ const activeCaptionStyle: CaptionStyle = captionMode === 'social' ? 'social' : 'standard';
522
+
499
523
  const setCaptionsEnabled = useCallback(
500
524
  (enabled: boolean) => {
525
+ // When re-enabling, restore the prop-specified style rather than
526
+ // always defaulting to 'standard'
527
+ setCaptionMode(enabled ? captionStyle || 'standard' : 'off');
501
528
  onCaptionsToggle?.(enabled);
502
529
  },
503
- [onCaptionsToggle],
530
+ [onCaptionsToggle, captionStyle],
504
531
  );
532
+
533
+ const cycleCaptionMode = useCallback(() => {
534
+ setCaptionMode((prev) => {
535
+ const next: CaptionMode =
536
+ prev === 'off' ? 'standard' : prev === 'standard' ? 'social' : 'off';
537
+ onCaptionsToggle?.(next !== 'off');
538
+ return next;
539
+ });
540
+ }, [onCaptionsToggle]);
541
+
505
542
  const hasCaptions = script.captions && script.captions.phrases.length > 0;
506
543
 
507
544
  // Map segment indices to human-readable titles (from sectionHeader blocks)
@@ -518,6 +555,7 @@ export function DocPlayer({
518
555
  docProgress,
519
556
  hasCaptions: !!hasCaptions,
520
557
  captionsEnabled,
558
+ captionMode,
521
559
  isFullscreen,
522
560
  currentSegmentIndex: currentSegment,
523
561
  currentSegmentName:
@@ -534,6 +572,7 @@ export function DocPlayer({
534
572
  docProgress,
535
573
  hasCaptions,
536
574
  captionsEnabled,
575
+ captionMode,
537
576
  isFullscreen,
538
577
  currentSegment,
539
578
  segmentTitleMap,
@@ -548,9 +587,10 @@ export function DocPlayer({
548
587
  restart,
549
588
  seekTo,
550
589
  setCaptionsEnabled,
590
+ cycleCaptionMode,
551
591
  toggleFullscreen: onFullscreenToggle,
552
592
  }),
553
- [toggle, restart, seekTo, setCaptionsEnabled, onFullscreenToggle],
593
+ [toggle, restart, seekTo, setCaptionsEnabled, cycleCaptionMode, onFullscreenToggle],
554
594
  );
555
595
 
556
596
  // Slide navigation actions for slideshow mode
@@ -803,13 +843,16 @@ export function DocPlayer({
803
843
  </div>
804
844
  )}
805
845
 
806
- {/* Caption overlay -- hidden in render mode and when stopped on cover block */}
807
- {hasCaptions && !renderMode && (
846
+ {/* Caption overlay -- shown during playback and in render mode when captions are enabled */}
847
+ {hasCaptions && (renderMode ? captionsEnabled : true) && (
808
848
  <CaptionOverlay
809
849
  captions={script.captions}
810
850
  currentTime={currentTime}
811
- enabled={captionsEnabled && (isPlaying || currentTime > 0)}
851
+ enabled={captionsEnabled && (renderMode || isPlaying || currentTime > 0)}
812
852
  fontSize={16}
853
+ captionStyle={activeCaptionStyle}
854
+ theme={theme}
855
+ viewport={activeViewport}
813
856
  />
814
857
  )}
815
858
 
@@ -850,7 +893,11 @@ export function DocPlayer({
850
893
  </div>
851
894
  <div>
852
895
  <span style={{ color: '#888' }}>time:</span> {currentTime.toFixed(2)}s /{' '}
853
- {totalDuration.toFixed(1)}s
896
+ {totalDuration.toFixed(1)}s{' '}
897
+ <span style={{ color: '#666' }}>
898
+ (progress: {(docProgress * 100).toFixed(1)}%, scriptDur:{' '}
899
+ {script.duration.toFixed(1)})
900
+ </span>
854
901
  </div>
855
902
  <div>
856
903
  <span style={{ color: '#888' }}>blockTime:</span> {blockTime.toFixed(2)}s /{' '}
@@ -52,6 +52,7 @@ const DEFAULT_STATE: PlaybackState = {
52
52
  docProgress: 0,
53
53
  hasCaptions: false,
54
54
  captionsEnabled: false,
55
+ captionMode: 'off',
55
56
  currentSegmentIndex: 0,
56
57
  currentSegmentName: null,
57
58
  currentBlock: null,
@@ -106,7 +106,7 @@ export function DocProgressBar({
106
106
  left: 0,
107
107
  width: `${state.docProgress * 100}%`,
108
108
  height: '6px',
109
- background: '#3d5a80',
109
+ background: '#5b9bd5',
110
110
  borderRadius: '3px',
111
111
  transition: 'width 0.1s',
112
112
  }}
@@ -125,7 +125,7 @@ export function DocProgressBar({
125
125
  borderRadius: '50%',
126
126
  background:
127
127
  marker.index === state.currentBlockIndex ? '#ffffff' : 'rgba(255,255,255,0.5)',
128
- border: '2px solid #3d5a80',
128
+ border: '2px solid #5b9bd5',
129
129
  cursor: 'pointer',
130
130
  zIndex: 2,
131
131
  transition: 'transform 0.15s, background 0.15s',
@@ -25,7 +25,7 @@ import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
25
25
  import { getLayers, hasTemplate, DEFAULT_THEME } from '@bendyline/squisq/doc';
26
26
  import type { RenderContext } from '@bendyline/squisq/doc';
27
27
  import { extractPlainText } from '@bendyline/squisq/markdown';
28
- import type { MarkdownBlockNode, MarkdownList } from '@bendyline/squisq/markdown';
28
+ import type { MarkdownBlockNode, MarkdownList, MarkdownTable } from '@bendyline/squisq/markdown';
29
29
  import { BlockRenderer } from './BlockRenderer';
30
30
  import { MarkdownRenderer } from './MarkdownRenderer';
31
31
 
@@ -55,7 +55,7 @@ export interface LinearDocViewProps {
55
55
  function isAnnotatedBlock(block: Block): boolean {
56
56
  const annotation = block.sourceHeading?.templateAnnotation;
57
57
  if (!annotation) return false;
58
- return hasTemplate(annotation.template);
58
+ return !!annotation.template && hasTemplate(annotation.template);
59
59
  }
60
60
 
61
61
  /**
@@ -103,7 +103,12 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }:
103
103
  duration: 1,
104
104
  audioSegment: 0,
105
105
  title: headingText,
106
- ...getTemplateDefaults(annotation.template, headingText, bodyText, block.contents),
106
+ ...getTemplateDefaults(
107
+ annotation.template ?? 'sectionHeader',
108
+ headingText,
109
+ bodyText,
110
+ block.contents,
111
+ ),
107
112
  ...annotation.params,
108
113
  ...block.templateOverrides,
109
114
  };
@@ -211,6 +216,26 @@ function extractListItems(contents?: MarkdownBlockNode[]): string[] {
211
216
  return items;
212
217
  }
213
218
 
219
+ /** Extract table data (headers, rows, alignment) from block contents. */
220
+ function extractTableData(contents?: MarkdownBlockNode[]): {
221
+ headers: string[];
222
+ rows: string[][];
223
+ align?: (('left' | 'right' | 'center') | null)[];
224
+ } | null {
225
+ if (!contents) return null;
226
+ for (const node of contents) {
227
+ if (node.type === 'table') {
228
+ const table = node as MarkdownTable;
229
+ const [headerRow, ...bodyRows] = table.children;
230
+ if (!headerRow) return null;
231
+ const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
232
+ const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
233
+ return { headers, rows, align: table.align };
234
+ }
235
+ }
236
+ return null;
237
+ }
238
+
214
239
  /**
215
240
  * Provide sensible default fields for templates that require more than
216
241
  * just a `title`. Prevents crashes from undefined required fields.
@@ -238,6 +263,10 @@ function getTemplateDefaults(
238
263
  return { term: headingText, definition: bodyText || headingText };
239
264
  case 'dateEvent':
240
265
  return { date: headingText, description: bodyText || headingText };
266
+ case 'dataTable': {
267
+ const tableData = extractTableData(contents);
268
+ return tableData ?? { headers: ['Column'], rows: [['Data']] };
269
+ }
241
270
  default:
242
271
  return {};
243
272
  }
@@ -276,6 +305,15 @@ export function LinearDocView({
276
305
  [activeViewport, totalBlocks, theme],
277
306
  );
278
307
 
308
+ const activeTheme = renderContext.theme!;
309
+ const bgColor = activeTheme.colors.background;
310
+ const textColor = activeTheme.colors.text;
311
+ const mutedColor = activeTheme.colors.textMuted;
312
+ const primaryColor = activeTheme.colors.primary;
313
+ const bodyFont = activeTheme.typography.bodyFontFamily;
314
+ const titleFont = activeTheme.typography.titleFontFamily;
315
+ const lineHt = activeTheme.typography.lineHeight ?? 1.7;
316
+
279
317
  return (
280
318
  <div
281
319
  className={`squisq-linear ${className || ''}`}
@@ -284,19 +322,118 @@ export function LinearDocView({
284
322
  height: '100%',
285
323
  overflowY: 'auto',
286
324
  overflowX: 'hidden',
325
+ background: bgColor,
287
326
  }}
288
327
  >
289
328
  <div
290
- className="squisq-linear-content"
291
- style={{
292
- maxWidth: '720px',
293
- margin: '0 auto',
294
- padding: '24px 16px',
295
- lineHeight: 1.7,
296
- fontSize: '16px',
297
- color: 'var(--squisq-text, #1f2937)',
298
- }}
329
+ className="squisq-linear-content squisq-md"
330
+ style={
331
+ {
332
+ maxWidth: '720px',
333
+ margin: '0 auto',
334
+ padding: '24px 16px',
335
+ lineHeight: lineHt,
336
+ fontSize: '16px',
337
+ fontFamily: bodyFont,
338
+ color: textColor,
339
+ // CSS custom properties for MarkdownRenderer / nested elements
340
+ '--squisq-linear-title-font': titleFont,
341
+ '--squisq-linear-body-font': bodyFont,
342
+ '--squisq-linear-text': textColor,
343
+ '--squisq-linear-muted': mutedColor,
344
+ '--squisq-linear-primary': primaryColor,
345
+ '--squisq-linear-bg': bgColor,
346
+ } as React.CSSProperties
347
+ }
299
348
  >
349
+ {/* Theme-aware typography and layout for document mode */}
350
+ <style>{`
351
+ .squisq-linear-content h1,
352
+ .squisq-linear-content h2,
353
+ .squisq-linear-content h3,
354
+ .squisq-linear-content h4,
355
+ .squisq-linear-content h5,
356
+ .squisq-linear-content h6 {
357
+ font-family: var(--squisq-linear-title-font);
358
+ color: var(--squisq-linear-text);
359
+ margin-top: 1.2em;
360
+ margin-bottom: 0.4em;
361
+ }
362
+ .squisq-linear-content h1 { font-size: 2em; }
363
+ .squisq-linear-content h2 { font-size: 1.5em; }
364
+ .squisq-linear-content h3 { font-size: 1.25em; }
365
+ .squisq-linear-content p {
366
+ margin-bottom: 0.75em;
367
+ }
368
+ .squisq-linear-content ul,
369
+ .squisq-linear-content ol {
370
+ padding-left: 2em;
371
+ margin-bottom: 0.75em;
372
+ }
373
+ .squisq-linear-content li {
374
+ margin-bottom: 0.3em;
375
+ }
376
+ .squisq-linear-content a {
377
+ color: var(--squisq-linear-primary);
378
+ }
379
+ .squisq-linear-content code {
380
+ color: var(--squisq-linear-primary);
381
+ font-size: 0.9em;
382
+ padding: 0.15em 0.3em;
383
+ border-radius: 3px;
384
+ background: rgba(128, 128, 128, 0.15);
385
+ }
386
+ .squisq-linear-content pre {
387
+ padding: 1em;
388
+ border-radius: 6px;
389
+ background: rgba(0, 0, 0, 0.2);
390
+ overflow-x: auto;
391
+ margin-bottom: 0.75em;
392
+ }
393
+ .squisq-linear-content pre code {
394
+ padding: 0;
395
+ background: none;
396
+ }
397
+ .squisq-linear-content blockquote {
398
+ border-left: 3px solid var(--squisq-linear-muted);
399
+ color: var(--squisq-linear-muted);
400
+ padding-left: 1em;
401
+ margin-left: 0;
402
+ margin-bottom: 0.75em;
403
+ }
404
+ .squisq-linear-content hr {
405
+ border: none;
406
+ border-top: 1px solid var(--squisq-linear-muted);
407
+ margin: 1.5em 0;
408
+ }
409
+ .squisq-linear-content strong {
410
+ font-weight: 700;
411
+ }
412
+ .squisq-linear-content em {
413
+ font-style: italic;
414
+ }
415
+ .squisq-linear-content table {
416
+ width: 100%;
417
+ border-collapse: collapse;
418
+ margin: 1em 0;
419
+ font-size: 0.95em;
420
+ }
421
+ .squisq-linear-content thead th {
422
+ background: var(--squisq-linear-primary);
423
+ color: var(--squisq-linear-bg);
424
+ font-family: var(--squisq-linear-title-font);
425
+ font-weight: 600;
426
+ padding: 10px 14px;
427
+ text-align: left;
428
+ }
429
+ .squisq-linear-content tbody td {
430
+ padding: 8px 14px;
431
+ border-bottom: 1px solid color-mix(in srgb, var(--squisq-linear-muted) 30%, transparent);
432
+ }
433
+ .squisq-linear-content tbody tr:hover {
434
+ background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
435
+ }
436
+ `}</style>
300
437
  {doc.blocks.map((block, i) => (
301
438
  <BlockSection
302
439
  key={block.id}