@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.
- package/dist/index.d.ts +757 -20
- package/dist/index.js +16875 -6883
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +143 -0
- package/src/EditorShell.tsx +200 -120
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +38 -3
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +180 -8
- package/src/RawEditor.tsx +169 -13
- package/src/RecorderEntry.tsx +9 -16
- package/src/TemplateAnnotation.ts +44 -0
- package/src/TemplatePicker.tsx +329 -54
- package/src/ThemeCustomizerPanel.tsx +30 -336
- package/src/ThemePicker.tsx +112 -3
- package/src/TimelineBlockPreview.tsx +37 -0
- package/src/TimelineTrack.tsx +671 -0
- package/src/Toolbar.tsx +528 -174
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +2 -14
- package/src/ViewMenuPanel.tsx +17 -14
- package/src/WysiwygEditor.tsx +161 -65
- package/src/__tests__/blockProperties.test.ts +92 -0
- package/src/__tests__/blockRange.test.ts +105 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/embeddedMedia.test.ts +48 -0
- package/src/__tests__/headingTransition.test.ts +138 -0
- package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
- package/src/__tests__/recorderMediaInsert.test.ts +86 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
- package/src/__tests__/templatePickerMetadata.test.ts +32 -0
- package/src/__tests__/timelineSource.test.ts +134 -0
- package/src/__tests__/tiptapBridge.test.ts +92 -0
- package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
- package/src/__tests__/tooltip.test.tsx +72 -0
- package/src/__tests__/transitionCatalog.test.ts +64 -0
- package/src/__tests__/useBlockNavigator.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +24 -0
- package/src/__tests__/useTimelineClock.test.ts +21 -0
- package/src/blockProperties.ts +88 -0
- package/src/blockRange.ts +132 -0
- package/src/buildPreviewDoc.ts +98 -9
- package/src/customTemplates/AddBin.tsx +126 -0
- package/src/customTemplates/CustomLayoutManager.tsx +233 -0
- package/src/customTemplates/CustomTemplateContext.tsx +182 -0
- package/src/customTemplates/LayerToolbar.tsx +580 -0
- package/src/customTemplates/ShapeGlyph.tsx +47 -0
- package/src/customTemplates/TemplateDesigner.tsx +430 -0
- package/src/customTemplates/__tests__/library.test.ts +88 -0
- package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
- package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
- package/src/customTemplates/designer.css +673 -0
- package/src/customTemplates/index.ts +31 -0
- package/src/customTemplates/library.ts +97 -0
- package/src/customTemplates/normalizePositions.ts +75 -0
- package/src/customTemplates/shapeDefs.ts +131 -0
- package/src/customTemplates/thumbnail.tsx +63 -0
- package/src/customTemplates/tokenDefs.ts +60 -0
- package/src/customTemplates/useDocCustomTemplates.ts +52 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
- package/src/customThemes/CustomThemeContext.tsx +179 -0
- package/src/customThemes/CustomThemeDialog.tsx +286 -0
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
- package/src/customThemes/customThemeLibrary.ts +97 -0
- package/src/customThemes/index.ts +31 -0
- package/src/customThemes/themeControls.tsx +229 -0
- package/src/customThemes/themeDraft.ts +272 -0
- package/src/customThemes/useDocCustomThemes.ts +49 -0
- package/src/diagram/DiagramCanvas.tsx +240 -0
- package/src/diagram/DiagramExtension.ts +209 -0
- package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
- package/src/diagram/DiagramWidget.tsx +270 -0
- package/src/diagram/diagramCommands.ts +604 -0
- package/src/diagram/diagramConstants.ts +17 -0
- package/src/diagram/useDiagramData.ts +126 -0
- package/src/embeddedMedia.ts +78 -0
- package/src/frontmatter.ts +29 -0
- package/src/headingTransition.ts +231 -0
- package/src/imageEditor/CanvasSurface.tsx +383 -88
- package/src/imageEditor/PropertiesPanel.tsx +47 -1
- package/src/imageEditor/Toolbar.tsx +229 -16
- package/src/imageEditor/createShapeLayer.ts +280 -0
- package/src/imageEditor/icons.tsx +34 -114
- package/src/imageEditor/image-editor.css +54 -5
- package/src/imageEditor/state.ts +23 -3
- package/src/index.ts +77 -0
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +8 -1
- package/src/recorder/insertMediaBlock.ts +30 -0
- package/src/resolveBlockVisual.ts +33 -0
- package/src/scene/Scene.tsx +540 -0
- package/src/scene/SceneBlockExtension.ts +198 -0
- package/src/scene/SceneBlockToolbar.tsx +201 -0
- package/src/scene/SceneBlockWidget.tsx +434 -0
- package/src/scene/ScenePropsBar.tsx +85 -0
- package/src/scene/SceneSelection.tsx +107 -0
- package/src/scene/SceneViewport.tsx +102 -0
- package/src/scene/ShapePalette.tsx +181 -0
- package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
- package/src/scene/__tests__/bezierEdit.test.ts +85 -0
- package/src/scene/__tests__/blockLayers.test.ts +57 -0
- package/src/scene/__tests__/shapeLayers.test.ts +106 -0
- package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
- package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
- package/src/scene/adapters/DiagramAdapter.ts +168 -0
- package/src/scene/adapters/DrawingAdapter.ts +415 -0
- package/src/scene/adapters/LayoutAdapter.ts +310 -0
- package/src/scene/adapters/blockLayers.ts +159 -0
- package/src/scene/commands/SceneCommand.ts +70 -0
- package/src/scene/commands/drawingCommands.ts +318 -0
- package/src/scene/commands/layoutCommands.ts +301 -0
- package/src/scene/hooks/useSceneHitTest.ts +105 -0
- package/src/scene/hooks/useScenePanZoom.ts +147 -0
- package/src/scene/hooks/useSceneSelection.ts +62 -0
- package/src/scene/index.ts +95 -0
- package/src/scene/layers/DiagramEdges.tsx +127 -0
- package/src/scene/layers/edgeGeometry.ts +77 -0
- package/src/scene/layers/nodeCard.tsx +145 -0
- package/src/scene/layers/renderLayer.tsx +70 -0
- package/src/scene/layers/shapeLayers.ts +201 -0
- package/src/scene/paths/bezierEdit.ts +208 -0
- package/src/scene/scene.css +649 -0
- package/src/scene/text/SceneTextOverlay.tsx +161 -0
- package/src/scene/text/sceneTextChannel.ts +40 -0
- package/src/scene/text/sceneTextConfig.ts +27 -0
- package/src/scene/text/sceneTiptap.ts +36 -0
- package/src/scene/text/useSceneTextEditing.ts +39 -0
- package/src/scene/tools/ConnectTool.ts +111 -0
- package/src/scene/tools/DrawingConnectTool.ts +161 -0
- package/src/scene/tools/PathTool.ts +158 -0
- package/src/scene/tools/PlaceTool.ts +47 -0
- package/src/scene/tools/SceneTool.ts +75 -0
- package/src/scene/tools/SelectTool.ts +284 -0
- package/src/scene/tools/ShapeTool.ts +144 -0
- package/src/scene/tools/TextTool.ts +72 -0
- package/src/scene/tools/TokenTool.ts +95 -0
- package/src/scene/tools/createDrawShapeTool.ts +82 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1615 -203
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +4 -0
- package/src/timelineSource.ts +244 -0
- package/src/tiptapBridge.ts +115 -34
- package/src/tooltipPlacement.ts +13 -0
- package/src/transitionCatalog.ts +159 -0
- package/src/types/monaco-shims.d.ts +10 -0
- package/src/useBlockNavigator.ts +153 -0
- package/src/useMonacoLoader.ts +23 -1
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
|
@@ -154,34 +154,29 @@ const btnDanger: CSSProperties = {
|
|
|
154
154
|
borderColor: '#902929',
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
-
const
|
|
157
|
+
const toggleRowStyle: CSSProperties = {
|
|
158
158
|
display: 'flex',
|
|
159
|
-
gap:
|
|
159
|
+
gap: 8,
|
|
160
160
|
marginBottom: 16,
|
|
161
|
-
borderBottom: '1px solid #c9b98a',
|
|
162
161
|
};
|
|
163
162
|
|
|
164
|
-
const
|
|
165
|
-
padding: '6px
|
|
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: '
|
|
172
|
-
|
|
173
|
-
marginBottom: -1,
|
|
170
|
+
border: '1px solid #c9b98a',
|
|
171
|
+
borderRadius: 999,
|
|
174
172
|
};
|
|
175
173
|
|
|
176
|
-
const
|
|
177
|
-
...
|
|
178
|
-
color: '#
|
|
174
|
+
const toggleActive: CSSProperties = {
|
|
175
|
+
...toggleBase,
|
|
176
|
+
color: '#fffdf5',
|
|
179
177
|
fontWeight: 600,
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
294
|
-
// recorder we had so the new mode acquires a fresh one.
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
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 (
|
|
300
|
-
|
|
351
|
+
if (previousKeyRef.current !== captureKey) {
|
|
352
|
+
previousKeyRef.current = captureKey;
|
|
301
353
|
recorder.cancel();
|
|
302
354
|
}
|
|
303
|
-
}, [
|
|
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
|
-
|
|
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={
|
|
414
|
-
{
|
|
415
|
-
const active =
|
|
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={
|
|
419
|
-
role="tab"
|
|
420
|
-
aria-selected={active}
|
|
483
|
+
key={t.key}
|
|
421
484
|
type="button"
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
485
|
+
aria-pressed={active}
|
|
486
|
+
style={active ? toggleActive : toggleBase}
|
|
487
|
+
onClick={() => onToggle(t.key)}
|
|
488
|
+
disabled={togglesLocked}
|
|
425
489
|
>
|
|
426
|
-
{
|
|
490
|
+
{t.label}
|
|
427
491
|
</button>
|
|
428
492
|
);
|
|
429
493
|
})}
|
|
430
494
|
</div>
|
|
431
495
|
|
|
432
|
-
{
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
{
|
|
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
|
|
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
|
-
<
|
|
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
|
+
}
|