@bendyline/squisq-editor-react 1.5.3 → 1.6.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 (165) hide show
  1. package/dist/index.d.ts +757 -20
  2. package/dist/index.js +16875 -6883
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -4
  5. package/src/BlockCardView.tsx +121 -0
  6. package/src/BlockPreviewPanel.tsx +69 -0
  7. package/src/BlockPropertiesPopover.tsx +191 -0
  8. package/src/EditorContext.tsx +143 -0
  9. package/src/EditorShell.tsx +200 -120
  10. package/src/FolderView.tsx +131 -0
  11. package/src/Icon.tsx +26 -0
  12. package/src/ImageEditor.tsx +69 -22
  13. package/src/OutlinePanel.tsx +38 -3
  14. package/src/PlainHtmlPreview.tsx +30 -3
  15. package/src/PreviewControls.tsx +180 -8
  16. package/src/RawEditor.tsx +169 -13
  17. package/src/RecorderEntry.tsx +9 -16
  18. package/src/TemplateAnnotation.ts +44 -0
  19. package/src/TemplatePicker.tsx +329 -54
  20. package/src/ThemeCustomizerPanel.tsx +30 -336
  21. package/src/ThemePicker.tsx +112 -3
  22. package/src/TimelineBlockPreview.tsx +37 -0
  23. package/src/TimelineTrack.tsx +671 -0
  24. package/src/Toolbar.tsx +528 -174
  25. package/src/Tooltip.tsx +22 -4
  26. package/src/TransitionPicker.tsx +351 -0
  27. package/src/VersionHistoryPanel.tsx +2 -14
  28. package/src/ViewMenuPanel.tsx +17 -14
  29. package/src/WysiwygEditor.tsx +161 -65
  30. package/src/__tests__/blockProperties.test.ts +92 -0
  31. package/src/__tests__/blockRange.test.ts +105 -0
  32. package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
  33. package/src/__tests__/createShapeLayer.test.ts +46 -0
  34. package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
  35. package/src/__tests__/embeddedMedia.test.ts +48 -0
  36. package/src/__tests__/headingTransition.test.ts +138 -0
  37. package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
  38. package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
  39. package/src/__tests__/recorderMediaInsert.test.ts +86 -0
  40. package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
  41. package/src/__tests__/templatePickerMetadata.test.ts +32 -0
  42. package/src/__tests__/timelineSource.test.ts +134 -0
  43. package/src/__tests__/tiptapBridge.test.ts +92 -0
  44. package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
  45. package/src/__tests__/tooltip.test.tsx +72 -0
  46. package/src/__tests__/transitionCatalog.test.ts +64 -0
  47. package/src/__tests__/useBlockNavigator.test.tsx +67 -0
  48. package/src/__tests__/useMediaRecorder.test.ts +24 -0
  49. package/src/__tests__/useTimelineClock.test.ts +21 -0
  50. package/src/blockProperties.ts +88 -0
  51. package/src/blockRange.ts +132 -0
  52. package/src/buildPreviewDoc.ts +98 -9
  53. package/src/customTemplates/AddBin.tsx +126 -0
  54. package/src/customTemplates/CustomLayoutManager.tsx +233 -0
  55. package/src/customTemplates/CustomTemplateContext.tsx +182 -0
  56. package/src/customTemplates/LayerToolbar.tsx +580 -0
  57. package/src/customTemplates/ShapeGlyph.tsx +47 -0
  58. package/src/customTemplates/TemplateDesigner.tsx +430 -0
  59. package/src/customTemplates/__tests__/library.test.ts +88 -0
  60. package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
  61. package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
  62. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
  63. package/src/customTemplates/designer.css +673 -0
  64. package/src/customTemplates/index.ts +31 -0
  65. package/src/customTemplates/library.ts +97 -0
  66. package/src/customTemplates/normalizePositions.ts +75 -0
  67. package/src/customTemplates/shapeDefs.ts +131 -0
  68. package/src/customTemplates/thumbnail.tsx +63 -0
  69. package/src/customTemplates/tokenDefs.ts +60 -0
  70. package/src/customTemplates/useDocCustomTemplates.ts +52 -0
  71. package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
  72. package/src/customThemes/CustomThemeContext.tsx +179 -0
  73. package/src/customThemes/CustomThemeDialog.tsx +286 -0
  74. package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
  75. package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
  76. package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
  77. package/src/customThemes/customThemeLibrary.ts +97 -0
  78. package/src/customThemes/index.ts +31 -0
  79. package/src/customThemes/themeControls.tsx +229 -0
  80. package/src/customThemes/themeDraft.ts +272 -0
  81. package/src/customThemes/useDocCustomThemes.ts +49 -0
  82. package/src/diagram/DiagramCanvas.tsx +240 -0
  83. package/src/diagram/DiagramExtension.ts +209 -0
  84. package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
  85. package/src/diagram/DiagramWidget.tsx +270 -0
  86. package/src/diagram/diagramCommands.ts +604 -0
  87. package/src/diagram/diagramConstants.ts +17 -0
  88. package/src/diagram/useDiagramData.ts +126 -0
  89. package/src/embeddedMedia.ts +78 -0
  90. package/src/frontmatter.ts +29 -0
  91. package/src/headingTransition.ts +231 -0
  92. package/src/imageEditor/CanvasSurface.tsx +383 -88
  93. package/src/imageEditor/PropertiesPanel.tsx +47 -1
  94. package/src/imageEditor/Toolbar.tsx +229 -16
  95. package/src/imageEditor/createShapeLayer.ts +280 -0
  96. package/src/imageEditor/icons.tsx +34 -114
  97. package/src/imageEditor/image-editor.css +54 -5
  98. package/src/imageEditor/state.ts +23 -3
  99. package/src/index.ts +77 -0
  100. package/src/recorder/RecorderModal.tsx +120 -53
  101. package/src/recorder/RecorderPanel.tsx +2 -26
  102. package/src/recorder/hooks/useMediaRecorder.ts +8 -1
  103. package/src/recorder/insertMediaBlock.ts +30 -0
  104. package/src/resolveBlockVisual.ts +33 -0
  105. package/src/scene/Scene.tsx +540 -0
  106. package/src/scene/SceneBlockExtension.ts +198 -0
  107. package/src/scene/SceneBlockToolbar.tsx +201 -0
  108. package/src/scene/SceneBlockWidget.tsx +434 -0
  109. package/src/scene/ScenePropsBar.tsx +85 -0
  110. package/src/scene/SceneSelection.tsx +107 -0
  111. package/src/scene/SceneViewport.tsx +102 -0
  112. package/src/scene/ShapePalette.tsx +181 -0
  113. package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
  114. package/src/scene/__tests__/bezierEdit.test.ts +85 -0
  115. package/src/scene/__tests__/blockLayers.test.ts +57 -0
  116. package/src/scene/__tests__/shapeLayers.test.ts +106 -0
  117. package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
  118. package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
  119. package/src/scene/adapters/DiagramAdapter.ts +168 -0
  120. package/src/scene/adapters/DrawingAdapter.ts +415 -0
  121. package/src/scene/adapters/LayoutAdapter.ts +310 -0
  122. package/src/scene/adapters/blockLayers.ts +159 -0
  123. package/src/scene/commands/SceneCommand.ts +70 -0
  124. package/src/scene/commands/drawingCommands.ts +318 -0
  125. package/src/scene/commands/layoutCommands.ts +301 -0
  126. package/src/scene/hooks/useSceneHitTest.ts +105 -0
  127. package/src/scene/hooks/useScenePanZoom.ts +147 -0
  128. package/src/scene/hooks/useSceneSelection.ts +62 -0
  129. package/src/scene/index.ts +95 -0
  130. package/src/scene/layers/DiagramEdges.tsx +127 -0
  131. package/src/scene/layers/edgeGeometry.ts +77 -0
  132. package/src/scene/layers/nodeCard.tsx +145 -0
  133. package/src/scene/layers/renderLayer.tsx +70 -0
  134. package/src/scene/layers/shapeLayers.ts +201 -0
  135. package/src/scene/paths/bezierEdit.ts +208 -0
  136. package/src/scene/scene.css +649 -0
  137. package/src/scene/text/SceneTextOverlay.tsx +161 -0
  138. package/src/scene/text/sceneTextChannel.ts +40 -0
  139. package/src/scene/text/sceneTextConfig.ts +27 -0
  140. package/src/scene/text/sceneTiptap.ts +36 -0
  141. package/src/scene/text/useSceneTextEditing.ts +39 -0
  142. package/src/scene/tools/ConnectTool.ts +111 -0
  143. package/src/scene/tools/DrawingConnectTool.ts +161 -0
  144. package/src/scene/tools/PathTool.ts +158 -0
  145. package/src/scene/tools/PlaceTool.ts +47 -0
  146. package/src/scene/tools/SceneTool.ts +75 -0
  147. package/src/scene/tools/SelectTool.ts +284 -0
  148. package/src/scene/tools/ShapeTool.ts +144 -0
  149. package/src/scene/tools/TextTool.ts +72 -0
  150. package/src/scene/tools/TokenTool.ts +95 -0
  151. package/src/scene/tools/createDrawShapeTool.ts +82 -0
  152. package/src/styles/diagram.css +183 -0
  153. package/src/styles/editor.css +1615 -203
  154. package/src/styles/folder-view.css +210 -0
  155. package/src/styles/image-edit-affordance.css +2 -2
  156. package/src/styles/index.css +4 -0
  157. package/src/timelineSource.ts +244 -0
  158. package/src/tiptapBridge.ts +115 -34
  159. package/src/tooltipPlacement.ts +13 -0
  160. package/src/transitionCatalog.ts +159 -0
  161. package/src/types/monaco-shims.d.ts +10 -0
  162. package/src/useBlockNavigator.ts +153 -0
  163. package/src/useMonacoLoader.ts +23 -1
  164. package/src/useTimelineClock.ts +76 -0
  165. package/src/utils/dropUtils.ts +1 -1
@@ -154,34 +154,29 @@ const btnDanger: CSSProperties = {
154
154
  borderColor: '#902929',
155
155
  };
156
156
 
157
- const tabRowStyle: CSSProperties = {
157
+ const toggleRowStyle: CSSProperties = {
158
158
  display: 'flex',
159
- gap: 4,
159
+ gap: 8,
160
160
  marginBottom: 16,
161
- borderBottom: '1px solid #c9b98a',
162
161
  };
163
162
 
164
- const tabBase: CSSProperties = {
165
- padding: '6px 12px',
163
+ const toggleBase: CSSProperties = {
164
+ padding: '6px 14px',
166
165
  fontSize: 13,
167
166
  fontFamily: 'inherit',
168
167
  cursor: 'pointer',
169
168
  background: 'transparent',
170
169
  color: '#5a4a2a',
171
- border: 'none',
172
- borderBottom: '2px solid transparent',
173
- marginBottom: -1,
170
+ border: '1px solid #c9b98a',
171
+ borderRadius: 999,
174
172
  };
175
173
 
176
- const tabActive: CSSProperties = {
177
- ...tabBase,
178
- color: '#2d2310',
174
+ const toggleActive: CSSProperties = {
175
+ ...toggleBase,
176
+ color: '#fffdf5',
179
177
  fontWeight: 600,
180
- // Use the `borderBottom` shorthand (not `borderBottomColor` longhand)
181
- // so React's style diff cleanly resets the underline when this tab
182
- // goes inactive — mixing shorthand + longhand can leave the old
183
- // color stuck on a previously-active tab between renders.
184
- borderBottom: '2px solid #8B6914',
178
+ background: '#8B6914',
179
+ borderColor: '#8B6914',
185
180
  };
186
181
 
187
182
  const previewBoxStyle: CSSProperties = {
@@ -237,17 +232,61 @@ function formatDurationMs(ms: number): string {
237
232
  return `${m}:${s.toString().padStart(2, '0')}`;
238
233
  }
239
234
 
240
- const TABS: Array<{ id: RecorderSource; label: string; description: string }> = [
241
- {
242
- id: 'mic',
243
- label: 'Narration',
244
- description: 'Voice-only audio. Pairs with a written script for auto-mapping to blocks.',
245
- },
246
- { id: 'camera', label: 'Camera', description: 'Camera + microphone. Saved as a video clip.' },
247
- { id: 'screen', label: 'Screen', description: 'Screen capture. System audio when available.' },
248
- { id: 'screen+mic', label: 'Screen + Mic', description: 'Screen with your microphone mixed in.' },
235
+ /** The two mutually-exclusive video sources, plus "none" (audio-only). */
236
+ type VideoSource = 'none' | 'camera' | 'screen';
237
+
238
+ /**
239
+ * Resolve the toggle state (microphone on/off + which video source) to the
240
+ * recorder's capture mode. Returns null when nothing is selected — there's
241
+ * nothing to capture, so the controls stay disabled.
242
+ *
243
+ * Camera + Screen can't be captured together (no compositing), so the two
244
+ * are mutually exclusive; the microphone composes with either, or stands
245
+ * alone as narration.
246
+ */
247
+ function deriveSource(micOn: boolean, video: VideoSource): RecorderSource | null {
248
+ if (video === 'camera') return 'camera'; // mic handled via includeMicrophone
249
+ if (video === 'screen') return micOn ? 'screen+mic' : 'screen';
250
+ return micOn ? 'mic' : null;
251
+ }
252
+
253
+ /** Initial toggle state for a given starting capture mode. */
254
+ function toggleStateFromMode(mode: RecorderSource): { micOn: boolean; video: VideoSource } {
255
+ switch (mode) {
256
+ case 'mic':
257
+ return { micOn: true, video: 'none' };
258
+ case 'camera':
259
+ return { micOn: true, video: 'camera' };
260
+ case 'screen':
261
+ return { micOn: false, video: 'screen' };
262
+ case 'screen+mic':
263
+ return { micOn: true, video: 'screen' };
264
+ }
265
+ }
266
+
267
+ const TOGGLES: Array<{ key: 'mic' | 'camera' | 'screen'; label: string }> = [
268
+ { key: 'mic', label: 'Microphone' },
269
+ { key: 'camera', label: 'Camera' },
270
+ { key: 'screen', label: 'Screen' },
249
271
  ];
250
272
 
273
+ /** One-line summary of what the current toggle combination will capture. */
274
+ function captureSummary(micOn: boolean, video: VideoSource): string {
275
+ if (video === 'camera') {
276
+ return micOn
277
+ ? 'Camera video with your microphone. Saved as a video clip.'
278
+ : 'Camera video only (no microphone). Saved as a video clip.';
279
+ }
280
+ if (video === 'screen') {
281
+ return micOn
282
+ ? 'Screen capture with your microphone mixed in. System audio when available.'
283
+ : 'Screen capture (no microphone). System audio when available.';
284
+ }
285
+ return micOn
286
+ ? 'Voice-only audio. Pairs with a written script for auto-mapping to blocks.'
287
+ : 'Pick at least one source to record.';
288
+ }
289
+
251
290
  // ── Component ──────────────────────────────────────────────────────
252
291
 
253
292
  export function RecorderModal({
@@ -257,7 +296,9 @@ export function RecorderModal({
257
296
  onClose,
258
297
  onSave,
259
298
  }: RecorderModalProps) {
260
- const [source, setSource] = useState<RecorderSource>(initialMode);
299
+ const initialToggles = toggleStateFromMode(initialMode);
300
+ const [micOn, setMicOn] = useState(initialToggles.micOn);
301
+ const [video, setVideo] = useState<VideoSource>(initialToggles.video);
261
302
  const [sourceText, setSourceText] = useState('');
262
303
  const [basename, setBasename] = useState('');
263
304
  const [includeSystemAudio, setIncludeSystemAudio] = useState(false);
@@ -267,9 +308,17 @@ export function RecorderModal({
267
308
 
268
309
  const previewRef = useRef<HTMLVideoElement | null>(null);
269
310
 
311
+ // Toggle state → the recorder's capture mode. `source` is null when nothing
312
+ // is selected; we feed the hook a harmless default and gate the controls on
313
+ // `canCapture` instead.
314
+ const derivedSource = deriveSource(micOn, video);
315
+ const canCapture = derivedSource !== null;
316
+ const source: RecorderSource = derivedSource ?? 'mic';
317
+
270
318
  const recorder = useMediaRecorder({
271
319
  source,
272
- systemAudio: source === 'screen' || source === 'screen+mic' ? includeSystemAudio : false,
320
+ includeMicrophone: video === 'camera' ? micOn : undefined,
321
+ systemAudio: video === 'screen' ? includeSystemAudio : false,
273
322
  });
274
323
 
275
324
  useStreamPreview(previewRef, recorder.state === 'stopped' ? null : recorder.stream);
@@ -290,17 +339,20 @@ export function RecorderModal({
290
339
  };
291
340
  }, [recorder.blob]);
292
341
 
293
- // Switching capture source mid-session: tear down whatever stream/
294
- // recorder we had so the new mode acquires a fresh one. The hook
295
- // already handles its own internal teardown on unmount; cancel()
296
- // here covers in-place source changes.
297
- const previousSourceRef = useRef(source);
342
+ // Switching capture config mid-session: tear down whatever stream/
343
+ // recorder we had so the new mode acquires a fresh one. We key on a
344
+ // signature of everything that changes the acquired stream — including
345
+ // the camera microphone and screen system-audio flags, which don't
346
+ // change `source` on their own. The hook handles its own teardown on
347
+ // unmount; cancel() here covers in-place changes.
348
+ const captureKey = `${source}:${video === 'camera' ? micOn : ''}:${includeSystemAudio}`;
349
+ const previousKeyRef = useRef(captureKey);
298
350
  useEffect(() => {
299
- if (previousSourceRef.current !== source) {
300
- previousSourceRef.current = source;
351
+ if (previousKeyRef.current !== captureKey) {
352
+ previousKeyRef.current = captureKey;
301
353
  recorder.cancel();
302
354
  }
303
- }, [source, recorder]);
355
+ }, [captureKey, recorder]);
304
356
 
305
357
  // Make sure tearing down the modal always releases the camera /
306
358
  // screen-capture indicator. The hook's own unmount effect handles
@@ -403,37 +455,47 @@ export function RecorderModal({
403
455
  const canSave = recorder.state === 'stopped' && recorder.blob !== null;
404
456
  const isBusy = recorder.state === 'requesting' || recorder.state === 'stopping' || isSaving;
405
457
 
406
- const activeTabDescription = TABS.find((t) => t.id === source)?.description;
458
+ // Toggles lock while a stream is live (acquiring or recording) changing
459
+ // the capture config then would tear down the in-progress take.
460
+ const togglesLocked = recorder.state === 'recording' || recorder.state === 'requesting';
461
+ const toggleActiveFor = (key: 'mic' | 'camera' | 'screen') =>
462
+ key === 'mic' ? micOn : video === key;
463
+ const onToggle = (key: 'mic' | 'camera' | 'screen') => {
464
+ if (key === 'mic') {
465
+ setMicOn((on) => !on);
466
+ } else {
467
+ // Camera and Screen are mutually exclusive — turning one on clears the
468
+ // other (no camera+screen compositing).
469
+ setVideo((v) => (v === key ? 'none' : key));
470
+ }
471
+ };
407
472
 
408
473
  return (
409
474
  <div style={overlayStyle} role="dialog" aria-modal="true" aria-label="Record media">
410
475
  <div style={modalStyle} onClick={(e) => e.stopPropagation()}>
411
476
  <h2 style={titleStyle}>Record media</h2>
412
477
 
413
- <div style={tabRowStyle} role="tablist">
414
- {TABS.map((tab) => {
415
- const active = tab.id === source;
478
+ <div style={toggleRowStyle} role="group" aria-label="Capture sources">
479
+ {TOGGLES.map((t) => {
480
+ const active = toggleActiveFor(t.key);
416
481
  return (
417
482
  <button
418
- key={tab.id}
419
- role="tab"
420
- aria-selected={active}
483
+ key={t.key}
421
484
  type="button"
422
- style={active ? tabActive : tabBase}
423
- onClick={() => setSource(tab.id)}
424
- disabled={recorder.state === 'recording' || recorder.state === 'requesting'}
485
+ aria-pressed={active}
486
+ style={active ? toggleActive : toggleBase}
487
+ onClick={() => onToggle(t.key)}
488
+ disabled={togglesLocked}
425
489
  >
426
- {tab.label}
490
+ {t.label}
427
491
  </button>
428
492
  );
429
493
  })}
430
494
  </div>
431
495
 
432
- {activeTabDescription && (
433
- <p style={{ margin: '0 0 12px 0', fontSize: 12, color: '#5a4a2a' }}>
434
- {activeTabDescription}
435
- </p>
436
- )}
496
+ <p style={{ margin: '0 0 12px 0', fontSize: 12, color: '#5a4a2a' }}>
497
+ {captureSummary(micOn, video)}
498
+ </p>
437
499
 
438
500
  {recorder.error && <div style={errorStyle}>{recorder.error.message}</div>}
439
501
  {saveError && <div style={errorStyle}>{saveError}</div>}
@@ -505,7 +567,7 @@ export function RecorderModal({
505
567
  />
506
568
  </>
507
569
  )}
508
- {(source === 'screen' || source === 'screen+mic') && (
570
+ {video === 'screen' && (
509
571
  <label
510
572
  style={{
511
573
  display: 'flex',
@@ -562,7 +624,12 @@ export function RecorderModal({
562
624
  {(recorder.state === 'idle' ||
563
625
  recorder.state === 'error' ||
564
626
  recorder.state === 'requesting') && (
565
- <button type="button" style={btnPrimary} onClick={handleRequest} disabled={isBusy}>
627
+ <button
628
+ type="button"
629
+ style={btnPrimary}
630
+ onClick={handleRequest}
631
+ disabled={isBusy || !canCapture}
632
+ >
566
633
  {recorder.state === 'requesting' ? 'Requesting…' : 'Start preview'}
567
634
  </button>
568
635
  )}
@@ -13,6 +13,7 @@ import { createPortal } from 'react-dom';
13
13
  import type { MediaProvider } from '@bendyline/squisq/schemas';
14
14
  import type { ContentContainer } from '@bendyline/squisq/storage';
15
15
  import { RecorderModal, type RecorderSaveResult } from './RecorderModal.js';
16
+ import { Icon } from '../Icon';
16
17
  import type { RecorderSource } from './hooks/useMediaRecorder.js';
17
18
 
18
19
  export interface RecorderPanelProps {
@@ -26,31 +27,6 @@ export interface RecorderPanelProps {
26
27
  className?: string;
27
28
  }
28
29
 
29
- /**
30
- * Inline 16×16 SVG mic icon — currentColor-driven so it inherits the
31
- * toolbar's icon color regardless of theme.
32
- */
33
- function MicIcon() {
34
- return (
35
- <svg
36
- width={16}
37
- height={16}
38
- viewBox="0 0 16 16"
39
- fill="none"
40
- stroke="currentColor"
41
- strokeWidth={1.5}
42
- strokeLinecap="round"
43
- strokeLinejoin="round"
44
- aria-hidden="true"
45
- >
46
- <rect x={5.5} y={2} width={5} height={8} rx={2.5} />
47
- <path d="M3.5 7.5v1a4.5 4.5 0 0 0 9 0v-1" />
48
- <line x1={8} y1={13} x2={8} y2={15} />
49
- <line x1={5.5} y1={15} x2={10.5} y2={15} />
50
- </svg>
51
- );
52
- }
53
-
54
30
  export function RecorderPanel({
55
31
  mediaProvider,
56
32
  container = null,
@@ -72,7 +48,7 @@ export function RecorderPanel({
72
48
  aria-expanded={open}
73
49
  onClick={() => setOpen((v) => !v)}
74
50
  >
75
- <MicIcon />
51
+ <Icon icon="fa-solid fa-microphone" />
76
52
  </button>
77
53
  {open &&
78
54
  typeof document !== 'undefined' &&
@@ -59,6 +59,13 @@ export interface UseMediaRecorderOptions {
59
59
  * only); when unsupported the resulting stream simply omits it.
60
60
  */
61
61
  systemAudio?: boolean;
62
+ /**
63
+ * For `source === 'camera'`, whether to include the microphone track.
64
+ * Defaults to `true` (camera + mic). Set `false` to capture silent
65
+ * video. Ignored for other sources, whose mic handling is encoded in
66
+ * the source itself (`'mic'`, `'screen+mic'`).
67
+ */
68
+ includeMicrophone?: boolean;
62
69
  }
63
70
 
64
71
  export interface UseMediaRecorderResult {
@@ -119,7 +126,7 @@ async function acquireStream(
119
126
  case 'camera': {
120
127
  const stream = await requestCameraStream({
121
128
  video: opts.videoConstraints ?? true,
122
- audio: opts.audioConstraints ?? true,
129
+ audio: opts.includeMicrophone === false ? false : (opts.audioConstraints ?? true),
123
130
  });
124
131
  return { stream, dispose: () => {} };
125
132
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * insertMediaBlock — drop a recorded clip into the WYSIWYG editor as a
3
+ * *top-level* block, not at the raw caret position.
4
+ *
5
+ * The recorder's media nodes (`TiptapVideo` / `TiptapAudio`) are block
6
+ * atoms. A plain `insertContent` at a caret that happens to sit inside a
7
+ * list item (or blockquote) nests the atom inside that container — and
8
+ * `tiptapToMarkdown`'s list serializer emits only the item's inline text,
9
+ * so the nested `<video>` / `<audio>` is silently dropped on the next
10
+ * sync and never reaches the markdown source (so it also never shows up
11
+ * as a timeline clip). Lifting the insertion to the position right after
12
+ * the caret's top-level ancestor keeps the tag at document level, where
13
+ * the serializer's block-media handler round-trips it.
14
+ */
15
+ import type { Editor } from '@tiptap/core';
16
+
17
+ export interface MediaNodeSpec {
18
+ type: 'video' | 'audio';
19
+ attrs: Record<string, unknown>;
20
+ }
21
+
22
+ export function insertMediaBlock(editor: Editor, node: MediaNodeSpec): void {
23
+ const { $from } = editor.state.selection;
24
+ // `after(1)` is the position immediately after the depth-1 (top-level)
25
+ // ancestor of the selection — after the whole list / blockquote /
26
+ // paragraph the caret is in. Fall back to the document end for the rare
27
+ // depth-0 (node) selection.
28
+ const pos = $from.depth >= 1 ? $from.after(1) : editor.state.doc.content.size;
29
+ editor.chain().focus().insertContentAt(pos, node).run();
30
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * resolveBlockVisual — resolves a doc block into a renderable card (with
3
+ * `layers`) via the shared preview pipeline (`buildPreviewDoc` → `getLayers`).
4
+ *
5
+ * Kept in its own module (rather than alongside `BlockThumbnail` in
6
+ * `TimelineBlockPreview.tsx`) so that component file exports only components,
7
+ * which keeps React Fast Refresh working.
8
+ */
9
+
10
+ import type { Block, DocBlock, Doc, ViewportConfig, Theme } from '@bendyline/squisq/schemas';
11
+ import { getLayers, type RenderContext } from '@bendyline/squisq/doc';
12
+ import { buildPreviewDoc } from './buildPreviewDoc';
13
+
14
+ /**
15
+ * Resolve a doc block into a renderable card (with `layers`) via the shared
16
+ * preview pipeline, or null when it can't be rendered.
17
+ */
18
+ export function resolveBlockVisual(
19
+ doc: Doc,
20
+ block: Block,
21
+ theme: Theme,
22
+ viewport: ViewportConfig,
23
+ ): Block | null {
24
+ try {
25
+ const slide = buildPreviewDoc({ ...doc, blocks: [block] }).blocks[0] as DocBlock | undefined;
26
+ if (!slide) return null;
27
+ const ctx: RenderContext = { blockIndex: 0, totalBlocks: 1, theme, viewport };
28
+ const layers = getLayers(slide, ctx);
29
+ return { ...(slide as unknown as Block), layers };
30
+ } catch {
31
+ return null;
32
+ }
33
+ }