@bendyline/squisq-editor-react 2.1.0 → 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.
- package/LICENSE +21 -0
- package/NOTICE.md +40 -0
- package/dist/index.d.ts +188 -55
- package/dist/index.js +5094 -2948
- package/dist/monaco.js +0 -1
- package/dist/styles/index.css +234 -145
- package/package.json +9 -6
- package/dist/index.js.map +0 -1
- package/dist/monaco.js.map +0 -1
- package/src/BlockCardView.tsx +0 -121
- package/src/BlockPreviewPanel.tsx +0 -69
- package/src/BlockPropertiesPopover.tsx +0 -222
- package/src/DocumentSettingsDialog.tsx +0 -272
- package/src/DropZoneOverlay.tsx +0 -137
- package/src/EditorContext.tsx +0 -1274
- package/src/EditorShell.tsx +0 -1358
- package/src/EmojiPicker.tsx +0 -332
- package/src/FolderView.tsx +0 -131
- package/src/Icon.tsx +0 -26
- package/src/ImageEditor.tsx +0 -395
- package/src/ImageNodeView.tsx +0 -284
- package/src/ImageViewer.tsx +0 -221
- package/src/InlineIcon.ts +0 -84
- package/src/InlinePreviewGutter.tsx +0 -590
- package/src/LinkDialog.tsx +0 -276
- package/src/MediaBin.tsx +0 -500
- package/src/MentionExtension.tsx +0 -261
- package/src/OutlinePanel.tsx +0 -547
- package/src/PlainHtmlPreview.tsx +0 -277
- package/src/PreviewControls.tsx +0 -1265
- package/src/PreviewPanel.tsx +0 -426
- package/src/RawEditor.tsx +0 -743
- package/src/RecorderEntry.tsx +0 -162
- package/src/StatusBar.tsx +0 -51
- package/src/TemplateAnnotation.ts +0 -167
- package/src/TemplateContentPreview.tsx +0 -56
- package/src/TemplatePicker.tsx +0 -1314
- package/src/ThemeCustomizerPanel.tsx +0 -304
- package/src/ThemePicker.tsx +0 -433
- package/src/TimelineBlockPreview.tsx +0 -37
- package/src/TimelineTrack.tsx +0 -671
- package/src/Toolbar.tsx +0 -2885
- package/src/Tooltip.tsx +0 -142
- package/src/TransitionPicker.tsx +0 -390
- package/src/VersionHistoryPanel.tsx +0 -359
- package/src/ViewMenuPanel.tsx +0 -189
- package/src/ViewSwitcher.tsx +0 -56
- package/src/WysiwygEditor.tsx +0 -806
- package/src/__tests__/blockProperties.test.ts +0 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +0 -33
- package/src/__tests__/blockRange.test.ts +0 -105
- package/src/__tests__/blockTagActivity.test.ts +0 -183
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +0 -75
- package/src/__tests__/buildPreviewDocContent.test.ts +0 -65
- package/src/__tests__/buildPreviewDocTransition.test.ts +0 -92
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +0 -60
- package/src/__tests__/codeContextSectionView.test.tsx +0 -97
- package/src/__tests__/codeContextZoneManager.test.ts +0 -127
- package/src/__tests__/createShapeLayer.test.ts +0 -46
- package/src/__tests__/customLayoutManagerTheme.test.tsx +0 -34
- package/src/__tests__/detectMarkdown.test.ts +0 -73
- package/src/__tests__/diffContextSections.test.ts +0 -39
- package/src/__tests__/documentSettingsDialog.test.tsx +0 -197
- package/src/__tests__/drawingShapeRoundTrip.test.ts +0 -49
- package/src/__tests__/editorScrollHandoff.test.tsx +0 -102
- package/src/__tests__/editorScrollSync.test.ts +0 -65
- package/src/__tests__/editorShellCodeContext.test.tsx +0 -86
- package/src/__tests__/editorShellProps.test.tsx +0 -550
- package/src/__tests__/embeddedMedia.test.ts +0 -48
- package/src/__tests__/emojiPicker.test.tsx +0 -93
- package/src/__tests__/fileKind.test.ts +0 -112
- package/src/__tests__/findMode.test.tsx +0 -104
- package/src/__tests__/findModel.test.ts +0 -55
- package/src/__tests__/headingTransition.test.ts +0 -176
- package/src/__tests__/imageEditAffordance.test.tsx +0 -272
- package/src/__tests__/imageEditModalContract.test.tsx +0 -119
- package/src/__tests__/imageEditorShell.test.tsx +0 -94
- package/src/__tests__/imageEditorState.test.ts +0 -171
- package/src/__tests__/inlinePreviewGutter.test.tsx +0 -62
- package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +0 -103
- package/src/__tests__/jsonEditor.test.tsx +0 -223
- package/src/__tests__/layersPanel.test.tsx +0 -167
- package/src/__tests__/layoutChildRoundTrip.test.ts +0 -71
- package/src/__tests__/linkDialogDocPicker.test.tsx +0 -137
- package/src/__tests__/majorApiTypes.test.ts +0 -26
- package/src/__tests__/markdownCodeFence.test.ts +0 -45
- package/src/__tests__/mediaAttachmentFlow.test.ts +0 -110
- package/src/__tests__/mediaBinDrop.test.tsx +0 -90
- package/src/__tests__/mediaReferences.test.ts +0 -97
- package/src/__tests__/narrationSave.test.ts +0 -148
- package/src/__tests__/outlinePanel.test.tsx +0 -169
- package/src/__tests__/outlineSource.test.ts +0 -217
- package/src/__tests__/plainHtmlPreview.test.tsx +0 -125
- package/src/__tests__/presentationMode.test.tsx +0 -290
- package/src/__tests__/previewControls.test.tsx +0 -571
- package/src/__tests__/previewPanelAudioFailure.test.tsx +0 -46
- package/src/__tests__/previewPanelPresentation.test.tsx +0 -126
- package/src/__tests__/previewPanelTransforms.test.tsx +0 -117
- package/src/__tests__/propertiesPanel.test.tsx +0 -69
- package/src/__tests__/rawEditorModelIsolation.test.ts +0 -19
- package/src/__tests__/recorderFormats.test.ts +0 -146
- package/src/__tests__/recorderMediaInsert.test.ts +0 -86
- package/src/__tests__/recorderTheme.test.tsx +0 -42
- package/src/__tests__/recorderTimingJson.test.ts +0 -41
- package/src/__tests__/resolveBlockVisual.test.ts +0 -43
- package/src/__tests__/selectionConversions.test.ts +0 -80
- package/src/__tests__/statusBar.test.tsx +0 -27
- package/src/__tests__/teleprompter.test.tsx +0 -190
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +0 -99
- package/src/__tests__/templateContentPreview.test.ts +0 -101
- package/src/__tests__/templatePickerMetadata.test.ts +0 -32
- package/src/__tests__/templatePickerPortal.test.tsx +0 -62
- package/src/__tests__/timelineSource.test.ts +0 -134
- package/src/__tests__/tiptapBridge.test.ts +0 -575
- package/src/__tests__/tiptapBridgeConformance.test.ts +0 -47
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +0 -109
- package/src/__tests__/tiptapImageRoundTrip.test.ts +0 -73
- package/src/__tests__/toolbarSelectionConversion.test.tsx +0 -190
- package/src/__tests__/tooltip.test.tsx +0 -72
- package/src/__tests__/transformStyleId.test.ts +0 -13
- package/src/__tests__/transitionCatalog.test.ts +0 -64
- package/src/__tests__/useBlockNavigator.test.tsx +0 -67
- package/src/__tests__/useImageEditor.test.tsx +0 -226
- package/src/__tests__/useJsonEditorTokens.test.ts +0 -59
- package/src/__tests__/useMediaRecorder.test.ts +0 -302
- package/src/__tests__/useTimelineClock.test.ts +0 -21
- package/src/__tests__/versionHistory.test.tsx +0 -197
- package/src/__tests__/viewMenuPanel.test.tsx +0 -130
- package/src/__tests__/writeCanvasSettings.test.ts +0 -15
- package/src/__tests__/wysiwygImageUpload.test.ts +0 -69
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +0 -56
- package/src/asciiDiagram/AsciiDiagramExtension.ts +0 -343
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +0 -254
- package/src/asciiDiagram/RepairableDiagramExtension.ts +0 -200
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +0 -164
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +0 -159
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +0 -284
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +0 -114
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +0 -43
- package/src/asciiDiagram/asciiDiagramCommands.ts +0 -174
- package/src/asciiDiagram/asciiDiagramData.ts +0 -149
- package/src/asciiDiagram/asciiDiagramOps.ts +0 -238
- package/src/asciiDiagram/asciiPaste.ts +0 -19
- package/src/blockProperties.ts +0 -88
- package/src/blockRange.ts +0 -132
- package/src/blockSlice.ts +0 -75
- package/src/blockTagActivity.ts +0 -233
- package/src/buildDocumentPreviewMarkdown.ts +0 -168
- package/src/buildPreviewDoc.ts +0 -407
- package/src/codeContext/CodeContextSectionView.tsx +0 -124
- package/src/codeContext/CodeContextZoneManager.ts +0 -149
- package/src/codeContext/CodeContextZones.tsx +0 -121
- package/src/codeContext/diffContextSections.ts +0 -38
- package/src/codeContext/types.ts +0 -75
- package/src/codeSnippet/CodeSnippetExtension.ts +0 -205
- package/src/codeSnippet/CodeSnippetWidget.tsx +0 -109
- package/src/codeSnippet/__tests__/CodeSnippetExtension.test.ts +0 -95
- package/src/codeSnippet/__tests__/codeSnippetLanguages.test.ts +0 -50
- package/src/codeSnippet/codeSnippetCommands.ts +0 -21
- package/src/codeSnippet/codeSnippetData.ts +0 -43
- package/src/codeSnippet/codeSnippetLanguages.ts +0 -216
- package/src/customTemplates/AddBin.tsx +0 -126
- package/src/customTemplates/CustomLayoutManager.tsx +0 -234
- package/src/customTemplates/CustomTemplateContext.tsx +0 -188
- package/src/customTemplates/LayerToolbar.tsx +0 -580
- package/src/customTemplates/ShapeGlyph.tsx +0 -47
- package/src/customTemplates/TemplateDesigner.tsx +0 -433
- package/src/customTemplates/__tests__/library.test.ts +0 -96
- package/src/customTemplates/__tests__/normalizePositions.test.ts +0 -109
- package/src/customTemplates/__tests__/shapeDefs.test.ts +0 -49
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +0 -108
- package/src/customTemplates/designer.css +0 -732
- package/src/customTemplates/index.ts +0 -31
- package/src/customTemplates/library.ts +0 -110
- package/src/customTemplates/normalizePositions.ts +0 -75
- package/src/customTemplates/shapeDefs.ts +0 -131
- package/src/customTemplates/thumbnail.tsx +0 -63
- package/src/customTemplates/tokenDefs.ts +0 -60
- package/src/customTemplates/useDocCustomTemplates.ts +0 -52
- package/src/customTemplates/useMemoryLayerAdapter.ts +0 -129
- package/src/customThemes/CustomThemeContext.tsx +0 -185
- package/src/customThemes/CustomThemeDialog.tsx +0 -316
- package/src/customThemes/ImportThemeSection.tsx +0 -178
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +0 -64
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +0 -47
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +0 -60
- package/src/customThemes/__tests__/importThemeSection.test.tsx +0 -192
- package/src/customThemes/customThemeLibrary.ts +0 -100
- package/src/customThemes/index.ts +0 -37
- package/src/customThemes/themeControls.tsx +0 -229
- package/src/customThemes/themeDraft.ts +0 -288
- package/src/customThemes/useDocCustomThemes.ts +0 -49
- package/src/detectMarkdown.ts +0 -62
- package/src/diagram/DiagramCanvas.tsx +0 -260
- package/src/diagram/DiagramMaximizedOverlay.tsx +0 -46
- package/src/diagram/diagramCommands.ts +0 -604
- package/src/diagram/diagramConstants.ts +0 -17
- package/src/diagram/types.ts +0 -35
- package/src/editorScrollSync.ts +0 -73
- package/src/embeddedMedia.ts +0 -78
- package/src/emojiData.ts +0 -1317
- package/src/fileKind.ts +0 -158
- package/src/find/FindHighlightExtension.ts +0 -81
- package/src/find/FindToolbar.tsx +0 -314
- package/src/find/findModel.ts +0 -77
- package/src/frontmatter.ts +0 -29
- package/src/frontmatterSettings.ts +0 -23
- package/src/headingTransition.ts +0 -291
- package/src/hooks/useFileDrop.ts +0 -266
- package/src/imageEditor/CanvasSurface.tsx +0 -703
- package/src/imageEditor/ImageVersionHistoryDropdown.tsx +0 -396
- package/src/imageEditor/LayersPanel.tsx +0 -218
- package/src/imageEditor/PropertiesPanel.tsx +0 -474
- package/src/imageEditor/Toolbar.tsx +0 -459
- package/src/imageEditor/createShapeLayer.ts +0 -280
- package/src/imageEditor/icons.tsx +0 -68
- package/src/imageEditor/image-editor.css +0 -574
- package/src/imageEditor/layers/EditorImageLayer.tsx +0 -45
- package/src/imageEditor/layers/EditorShapeLayer.tsx +0 -62
- package/src/imageEditor/layers/EditorTextLayer.tsx +0 -45
- package/src/imageEditor/layers/SelectionHandles.tsx +0 -86
- package/src/imageEditor/state.ts +0 -173
- package/src/imageEditor/useImageEditor.ts +0 -385
- package/src/imageEditor/useImageEditorTokens.ts +0 -70
- package/src/index.ts +0 -559
- package/src/jsonEditor/EmbeddedRichTextField.tsx +0 -81
- package/src/jsonEditor/JsonEditor.tsx +0 -81
- package/src/jsonEditor/JsonEditorContext.tsx +0 -81
- package/src/jsonEditor/RenderNode.tsx +0 -87
- package/src/jsonEditor/editors.tsx +0 -745
- package/src/jsonEditor/index.ts +0 -2
- package/src/jsonEditor/json-editor.css +0 -463
- package/src/jsonEditor/useJsonEditorTokens.ts +0 -33
- package/src/markdownCodeFence.ts +0 -72
- package/src/mediaDragMime.ts +0 -32
- package/src/mediaEntries.ts +0 -12
- package/src/mediaReferences.ts +0 -301
- package/src/mermaid/MermaidDiagramCanvas.tsx +0 -693
- package/src/mermaid/MermaidDiagramExtension.ts +0 -243
- package/src/mermaid/MermaidDiagramTypeThumbnail.tsx +0 -184
- package/src/mermaid/MermaidDiagramWidget.tsx +0 -653
- package/src/mermaid/MermaidShapePalette.tsx +0 -245
- package/src/mermaid/__tests__/MermaidDiagramExtension.test.ts +0 -361
- package/src/mermaid/__tests__/mermaidDiagramTypes.test.ts +0 -35
- package/src/mermaid/__tests__/mermaidRenderer.test.ts +0 -49
- package/src/mermaid/__tests__/mermaidSourceOps.test.ts +0 -213
- package/src/mermaid/__tests__/mermaidSyntax.test.ts +0 -71
- package/src/mermaid/mermaidCommands.ts +0 -34
- package/src/mermaid/mermaidData.ts +0 -31
- package/src/mermaid/mermaidDiagramTypes.ts +0 -325
- package/src/mermaid/mermaidModel.ts +0 -31
- package/src/mermaid/mermaidRenderer.ts +0 -181
- package/src/mermaid/mermaidShapes.ts +0 -113
- package/src/mermaid/mermaidSourceOps.ts +0 -454
- package/src/monaco.ts +0 -35
- package/src/monacoWorkers.ts +0 -89
- package/src/outlineSource.ts +0 -171
- package/src/presentation/PresentationMode.tsx +0 -596
- package/src/rawEditorIsolation.ts +0 -18
- package/src/recorder/RecorderButton.tsx +0 -80
- package/src/recorder/RecorderModal.tsx +0 -706
- package/src/recorder/RecorderPanel.tsx +0 -77
- package/src/recorder/formats.ts +0 -159
- package/src/recorder/hooks/useMediaRecorder.ts +0 -436
- package/src/recorder/hooks/useStreamPreview.ts +0 -47
- package/src/recorder/insertMediaBlock.ts +0 -30
- package/src/recorder/sources/cameraStream.ts +0 -32
- package/src/recorder/sources/micStream.ts +0 -25
- package/src/recorder/sources/screenStream.ts +0 -162
- package/src/recorder/timingJson.ts +0 -66
- package/src/resolveBlockVisual.ts +0 -39
- package/src/scene/Scene.tsx +0 -647
- package/src/scene/SceneBlockExtension.ts +0 -205
- package/src/scene/SceneBlockToolbar.tsx +0 -216
- package/src/scene/SceneBlockWidget.tsx +0 -441
- package/src/scene/ScenePropsBar.tsx +0 -85
- package/src/scene/SceneSelection.tsx +0 -111
- package/src/scene/SceneSideToolbar.tsx +0 -89
- package/src/scene/SceneViewControls.tsx +0 -43
- package/src/scene/SceneViewport.tsx +0 -106
- package/src/scene/ShapePalette.tsx +0 -196
- package/src/scene/__tests__/DiagramAdapter.test.ts +0 -142
- package/src/scene/__tests__/SceneBlockExtension.test.ts +0 -33
- package/src/scene/__tests__/bezierEdit.test.ts +0 -85
- package/src/scene/__tests__/blockLayers.test.ts +0 -57
- package/src/scene/__tests__/fitScale.test.ts +0 -19
- package/src/scene/__tests__/sceneIsolation.test.tsx +0 -119
- package/src/scene/__tests__/shapeLayers.test.ts +0 -106
- package/src/scene/__tests__/useSceneHitTest.test.ts +0 -90
- package/src/scene/__tests__/useScenePanZoom.test.ts +0 -130
- package/src/scene/adapters/DiagramAdapter.ts +0 -79
- package/src/scene/adapters/DrawingAdapter.ts +0 -420
- package/src/scene/adapters/LayoutAdapter.ts +0 -313
- package/src/scene/adapters/blockLayers.ts +0 -159
- package/src/scene/commands/SceneCommand.ts +0 -84
- package/src/scene/commands/drawingCommands.ts +0 -318
- package/src/scene/commands/layoutCommands.ts +0 -301
- package/src/scene/fitScale.ts +0 -17
- package/src/scene/hooks/useSceneHitTest.ts +0 -105
- package/src/scene/hooks/useScenePanZoom.ts +0 -154
- package/src/scene/hooks/useSceneSelection.ts +0 -62
- package/src/scene/index.ts +0 -90
- package/src/scene/layers/DiagramEdges.tsx +0 -142
- package/src/scene/layers/edgeGeometry.ts +0 -104
- package/src/scene/layers/nodeCard.tsx +0 -164
- package/src/scene/layers/renderLayer.tsx +0 -70
- package/src/scene/layers/shapeLayers.ts +0 -201
- package/src/scene/paths/bezierEdit.ts +0 -208
- package/src/scene/scene.css +0 -874
- package/src/scene/text/SceneTextOverlay.tsx +0 -161
- package/src/scene/text/sceneTextChannel.ts +0 -46
- package/src/scene/text/sceneTextConfig.ts +0 -31
- package/src/scene/text/sceneTiptap.ts +0 -36
- package/src/scene/text/useSceneTextEditing.ts +0 -39
- package/src/scene/tools/ConnectTool.ts +0 -209
- package/src/scene/tools/DrawingConnectTool.ts +0 -231
- package/src/scene/tools/PathTool.ts +0 -158
- package/src/scene/tools/PlaceTool.ts +0 -47
- package/src/scene/tools/SceneTool.ts +0 -85
- package/src/scene/tools/SelectTool.ts +0 -300
- package/src/scene/tools/ShapeTool.ts +0 -143
- package/src/scene/tools/TextTool.ts +0 -72
- package/src/scene/tools/TokenTool.ts +0 -95
- package/src/scene/tools/createDrawShapeTool.ts +0 -82
- package/src/selectionConversions.ts +0 -155
- package/src/styles/ascii-diagram.css +0 -79
- package/src/styles/ascii-timeline.css +0 -596
- package/src/styles/code-context.css +0 -155
- package/src/styles/code-snippet.css +0 -76
- package/src/styles/diagram.css +0 -183
- package/src/styles/editor.css +0 -7131
- package/src/styles/folder-view.css +0 -210
- package/src/styles/image-edit-affordance.css +0 -201
- package/src/styles/index.css +0 -20
- package/src/styles/mermaid-diagram.css +0 -487
- package/src/styles/tree-view.css +0 -189
- package/src/teleprompter/TeleprompterControls.tsx +0 -218
- package/src/teleprompter/TeleprompterSelfView.tsx +0 -22
- package/src/teleprompter/TeleprompterSurface.tsx +0 -267
- package/src/teleprompter/TeleprompterView.tsx +0 -338
- package/src/teleprompter/canvasRenderer.ts +0 -161
- package/src/teleprompter/floatingWindow.ts +0 -352
- package/src/teleprompter/index.ts +0 -61
- package/src/teleprompter/pcmWorklet.ts +0 -62
- package/src/teleprompter/recording/insertPreamble.ts +0 -80
- package/src/teleprompter/recording/narrationSave.ts +0 -134
- package/src/teleprompter/recording/useNarrationRecorder.ts +0 -367
- package/src/teleprompter/scrollModel.ts +0 -85
- package/src/teleprompter/teleprompterTheme.ts +0 -303
- package/src/teleprompter/types.ts +0 -38
- package/src/teleprompter/useFloatingWindow.ts +0 -74
- package/src/teleprompter/useMicAnalysis.ts +0 -211
- package/src/teleprompter/useTeleprompter.ts +0 -421
- package/src/templateContentPreviewResolver.ts +0 -360
- package/src/timeline/TimelineEditorWidget.tsx +0 -861
- package/src/timeline/TimelineViewExtension.ts +0 -252
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +0 -414
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +0 -163
- package/src/timeline/__tests__/timelineCommands.test.ts +0 -359
- package/src/timeline/__tests__/timelineOps.test.ts +0 -277
- package/src/timeline/__tests__/timelinePaste.test.ts +0 -34
- package/src/timeline/timelineCommands.ts +0 -421
- package/src/timeline/timelineData.ts +0 -54
- package/src/timeline/timelineOps.ts +0 -381
- package/src/timeline/timelinePaste.ts +0 -8
- package/src/timelineSource.ts +0 -244
- package/src/tiptap/TiptapAudio.tsx +0 -86
- package/src/tiptap/TiptapVideo.tsx +0 -119
- package/src/tiptap/useResolvedMediaSrc.ts +0 -47
- package/src/tiptapBridge.ts +0 -986
- package/src/tooltipPlacement.ts +0 -13
- package/src/transformStyleId.ts +0 -17
- package/src/transitionCatalog.ts +0 -159
- package/src/treeview/TreeOutlineWidget.tsx +0 -390
- package/src/treeview/TreeViewExtension.ts +0 -250
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +0 -156
- package/src/treeview/__tests__/TreeViewExtension.test.ts +0 -122
- package/src/treeview/__tests__/treeOps.test.ts +0 -177
- package/src/treeview/__tests__/treePaste.test.ts +0 -40
- package/src/treeview/__tests__/treeViewCommands.test.ts +0 -179
- package/src/treeview/treeOps.ts +0 -231
- package/src/treeview/treePaste.ts +0 -13
- package/src/treeview/treeViewCommands.ts +0 -99
- package/src/treeview/treeViewData.ts +0 -41
- package/src/types/monaco-shims.d.ts +0 -10
- package/src/useBlockNavigator.ts +0 -153
- package/src/useHeadingLayout.ts +0 -294
- package/src/useMonacoLoader.ts +0 -82
- package/src/useTimelineClock.ts +0 -76
- package/src/utils/collectInlineFontAwesomeCss.ts +0 -69
- package/src/utils/dropUtils.ts +0 -136
- package/src/utils/normalizeMalformedAssetUrl.ts +0 -22
- package/src/writeCanvasSettings.ts +0 -30
- package/src/wysiwygImageUpload.ts +0 -113
|
@@ -1,706 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RecorderModal — configure-and-capture dialog for browser-based audio,
|
|
3
|
-
* camera, and screen recording.
|
|
4
|
-
*
|
|
5
|
-
* States: configure (pick mode + optional script) → previewing (acquired
|
|
6
|
-
* stream, not yet recording) → recording → review (blob in hand) → saved
|
|
7
|
-
* | error. The user can cancel from any state.
|
|
8
|
-
*
|
|
9
|
-
* Persists the captured `Blob` into the supplied `MediaProvider` and,
|
|
10
|
-
* for narration mode, writes a `.timing.json` sidecar so
|
|
11
|
-
* `resolveAudioMapping()` in `@bendyline/squisq` picks it up at the next
|
|
12
|
-
* doc parse.
|
|
13
|
-
*
|
|
14
|
-
* Visual conventions match `VideoExportModal` from `@bendyline/squisq-
|
|
15
|
-
* video-react`, with inline theme tokens so the body-level portal follows
|
|
16
|
-
* the editor's light/dark chrome scheme.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';
|
|
20
|
-
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
21
|
-
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
22
|
-
import { useMediaRecorder, type RecorderSource } from './hooks/useMediaRecorder.js';
|
|
23
|
-
import { useStreamPreview } from './hooks/useStreamPreview.js';
|
|
24
|
-
import { buildFilename } from './formats.js';
|
|
25
|
-
import { buildTimingJson, encodeTimingJson, timingPathFor } from './timingJson.js';
|
|
26
|
-
|
|
27
|
-
// ── Types ──────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export type RecorderColorScheme = 'light' | 'dark';
|
|
30
|
-
|
|
31
|
-
export interface RecorderModalProps {
|
|
32
|
-
/** Required — recordings are written here. */
|
|
33
|
-
mediaProvider: MediaProvider;
|
|
34
|
-
/**
|
|
35
|
-
* Optional — when provided, narration-mode recordings drop a
|
|
36
|
-
* `.timing.json` sidecar at the matching container path so
|
|
37
|
-
* `resolveAudioMapping()` can auto-link them. Without it, only the
|
|
38
|
-
* raw recording is saved.
|
|
39
|
-
*/
|
|
40
|
-
container?: ContentContainer | null;
|
|
41
|
-
/** Initial capture source. Defaults to `'mic'` (narration). */
|
|
42
|
-
initialMode?: RecorderSource;
|
|
43
|
-
/** Light/dark chrome scheme. Defaults to `'light'`. */
|
|
44
|
-
colorScheme?: RecorderColorScheme;
|
|
45
|
-
/** Called after the modal is dismissed (save or cancel). */
|
|
46
|
-
onClose: () => void;
|
|
47
|
-
/**
|
|
48
|
-
* Fired after a successful save. Hosts typically use this to insert a
|
|
49
|
-
* markdown reference at the cursor — see {@link RecorderSaveResult}
|
|
50
|
-
* for the fields a host needs to build that reference.
|
|
51
|
-
*/
|
|
52
|
-
onSave?: (result: RecorderSaveResult) => void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Payload handed to {@link RecorderModalProps.onSave} on a successful save. */
|
|
56
|
-
export interface RecorderSaveResult {
|
|
57
|
-
/** Path returned by `mediaProvider.addMedia()` — what the doc should reference. */
|
|
58
|
-
relativePath: string;
|
|
59
|
-
/** Filename the modal chose (e.g. `narration-20260516-091200.webm`). */
|
|
60
|
-
filename: string;
|
|
61
|
-
/** Capture source the user picked. */
|
|
62
|
-
source: RecorderSource;
|
|
63
|
-
/** MIME type of the saved blob. */
|
|
64
|
-
mimeType: string;
|
|
65
|
-
/** Recording length in seconds. */
|
|
66
|
-
duration: number;
|
|
67
|
-
/** Whether a narration sidecar was written. Always `false` for video sources. */
|
|
68
|
-
hasTimingSidecar: boolean;
|
|
69
|
-
/** Script text the user typed (narration only). */
|
|
70
|
-
sourceText?: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── Styles ─────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
const overlayStyle: CSSProperties = {
|
|
76
|
-
position: 'fixed',
|
|
77
|
-
inset: 0,
|
|
78
|
-
background: 'rgba(0, 0, 0, 0.5)',
|
|
79
|
-
display: 'flex',
|
|
80
|
-
alignItems: 'center',
|
|
81
|
-
justifyContent: 'center',
|
|
82
|
-
zIndex: 10000,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* The recorder is normally portaled to `document.body`, outside the editor
|
|
87
|
-
* shell's CSS-variable scope. Copy the scheme onto a fresh scope and provide
|
|
88
|
-
* inline fallbacks so the standalone RecorderModal/RecorderButton exports are
|
|
89
|
-
* themed even when a host has not loaded the editor stylesheet.
|
|
90
|
-
*/
|
|
91
|
-
function recorderThemeStyle(colorScheme: RecorderColorScheme): CSSProperties {
|
|
92
|
-
const dark = colorScheme === 'dark';
|
|
93
|
-
return {
|
|
94
|
-
colorScheme,
|
|
95
|
-
'--squisq-recorder-surface': `var(--squisq-bg, ${dark ? '#1f2937' : '#fffdf7'})`,
|
|
96
|
-
'--squisq-recorder-input': `var(--squisq-input-bg, ${dark ? '#374151' : '#fff'})`,
|
|
97
|
-
'--squisq-recorder-border': `var(--squisq-border, ${dark ? '#4b5563' : '#c9b98a'})`,
|
|
98
|
-
'--squisq-recorder-text': `var(--squisq-text, ${dark ? '#e5e7eb' : '#4a3c1f'})`,
|
|
99
|
-
'--squisq-recorder-muted': `var(--squisq-text-muted, ${dark ? '#9ca3af' : '#5a4a2a'})`,
|
|
100
|
-
'--squisq-recorder-accent': 'var(--squisq-accent, #8b6914)',
|
|
101
|
-
'--squisq-recorder-accent-text': '#fff',
|
|
102
|
-
'--squisq-recorder-danger': dark ? '#dc4c4c' : '#b33a3a',
|
|
103
|
-
'--squisq-recorder-danger-border': dark ? '#ef6a6a' : '#902929',
|
|
104
|
-
'--squisq-recorder-error-bg': dark ? '#3f151b' : '#fceeee',
|
|
105
|
-
'--squisq-recorder-error-border': dark ? '#7f1d1d' : '#d88a8a',
|
|
106
|
-
'--squisq-recorder-error-text': dark ? '#fecdd3' : '#8c2a2a',
|
|
107
|
-
} as CSSProperties;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const modalStyle: CSSProperties = {
|
|
111
|
-
background: 'var(--squisq-recorder-surface)',
|
|
112
|
-
border: '1px solid var(--squisq-recorder-border)',
|
|
113
|
-
borderRadius: 0,
|
|
114
|
-
padding: '24px 28px',
|
|
115
|
-
width: 'min(560px, calc(100vw - 48px))',
|
|
116
|
-
maxHeight: 'calc(100vh - 48px)',
|
|
117
|
-
overflowY: 'auto',
|
|
118
|
-
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
|
|
119
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
120
|
-
color: 'var(--squisq-recorder-text)',
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const titleStyle: CSSProperties = {
|
|
124
|
-
margin: '0 0 16px 0',
|
|
125
|
-
fontSize: 18,
|
|
126
|
-
fontWeight: 600,
|
|
127
|
-
color: 'var(--squisq-recorder-text)',
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const labelStyle: CSSProperties = {
|
|
131
|
-
display: 'block',
|
|
132
|
-
fontSize: 13,
|
|
133
|
-
fontWeight: 500,
|
|
134
|
-
marginBottom: 4,
|
|
135
|
-
color: 'var(--squisq-recorder-text)',
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const inputStyle: CSSProperties = {
|
|
139
|
-
width: '100%',
|
|
140
|
-
padding: '6px 8px',
|
|
141
|
-
fontSize: 13,
|
|
142
|
-
fontFamily: 'inherit',
|
|
143
|
-
border: '1px solid var(--squisq-recorder-border)',
|
|
144
|
-
borderRadius: 0,
|
|
145
|
-
background: 'var(--squisq-recorder-input)',
|
|
146
|
-
color: 'var(--squisq-recorder-text)',
|
|
147
|
-
marginBottom: 12,
|
|
148
|
-
boxSizing: 'border-box',
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const textareaStyle: CSSProperties = {
|
|
152
|
-
...inputStyle,
|
|
153
|
-
resize: 'vertical',
|
|
154
|
-
minHeight: 72,
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const btnPrimary: CSSProperties = {
|
|
158
|
-
padding: '8px 20px',
|
|
159
|
-
fontSize: 14,
|
|
160
|
-
fontFamily: 'inherit',
|
|
161
|
-
fontWeight: 500,
|
|
162
|
-
cursor: 'pointer',
|
|
163
|
-
background: 'var(--squisq-recorder-accent)',
|
|
164
|
-
color: 'var(--squisq-recorder-accent-text)',
|
|
165
|
-
border: '1px solid var(--squisq-recorder-accent)',
|
|
166
|
-
borderRadius: 0,
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const btnSecondary: CSSProperties = {
|
|
170
|
-
padding: '8px 20px',
|
|
171
|
-
fontSize: 14,
|
|
172
|
-
fontFamily: 'inherit',
|
|
173
|
-
fontWeight: 500,
|
|
174
|
-
cursor: 'pointer',
|
|
175
|
-
background: 'var(--squisq-recorder-input)',
|
|
176
|
-
color: 'var(--squisq-recorder-text)',
|
|
177
|
-
border: '1px solid var(--squisq-recorder-border)',
|
|
178
|
-
borderRadius: 0,
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const btnDanger: CSSProperties = {
|
|
182
|
-
...btnPrimary,
|
|
183
|
-
background: 'var(--squisq-recorder-danger)',
|
|
184
|
-
borderColor: 'var(--squisq-recorder-danger-border)',
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const toggleRowStyle: CSSProperties = {
|
|
188
|
-
display: 'flex',
|
|
189
|
-
gap: 8,
|
|
190
|
-
marginBottom: 16,
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const toggleBase: CSSProperties = {
|
|
194
|
-
padding: '6px 14px',
|
|
195
|
-
fontSize: 13,
|
|
196
|
-
fontFamily: 'inherit',
|
|
197
|
-
cursor: 'pointer',
|
|
198
|
-
background: 'transparent',
|
|
199
|
-
color: 'var(--squisq-recorder-text)',
|
|
200
|
-
border: '1px solid var(--squisq-recorder-border)',
|
|
201
|
-
borderRadius: 999,
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const toggleActive: CSSProperties = {
|
|
205
|
-
...toggleBase,
|
|
206
|
-
color: 'var(--squisq-recorder-accent-text)',
|
|
207
|
-
fontWeight: 600,
|
|
208
|
-
background: 'var(--squisq-recorder-accent)',
|
|
209
|
-
borderColor: 'var(--squisq-recorder-accent)',
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const previewBoxStyle: CSSProperties = {
|
|
213
|
-
width: '100%',
|
|
214
|
-
background: '#000',
|
|
215
|
-
borderRadius: 0,
|
|
216
|
-
marginBottom: 12,
|
|
217
|
-
overflow: 'hidden',
|
|
218
|
-
aspectRatio: '16 / 9',
|
|
219
|
-
display: 'flex',
|
|
220
|
-
alignItems: 'center',
|
|
221
|
-
justifyContent: 'center',
|
|
222
|
-
color: '#888',
|
|
223
|
-
fontSize: 13,
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const audioMeterStyle: CSSProperties = {
|
|
227
|
-
width: '100%',
|
|
228
|
-
height: 56,
|
|
229
|
-
background: 'var(--squisq-recorder-input)',
|
|
230
|
-
border: '1px solid var(--squisq-recorder-border)',
|
|
231
|
-
marginBottom: 12,
|
|
232
|
-
display: 'flex',
|
|
233
|
-
alignItems: 'center',
|
|
234
|
-
justifyContent: 'center',
|
|
235
|
-
color: 'var(--squisq-recorder-muted)',
|
|
236
|
-
fontSize: 13,
|
|
237
|
-
fontVariantNumeric: 'tabular-nums',
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const errorStyle: CSSProperties = {
|
|
241
|
-
background: 'var(--squisq-recorder-error-bg)',
|
|
242
|
-
border: '1px solid var(--squisq-recorder-error-border)',
|
|
243
|
-
color: 'var(--squisq-recorder-error-text)',
|
|
244
|
-
padding: '8px 10px',
|
|
245
|
-
fontSize: 13,
|
|
246
|
-
marginBottom: 12,
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const buttonRowStyle: CSSProperties = {
|
|
250
|
-
display: 'flex',
|
|
251
|
-
gap: 8,
|
|
252
|
-
justifyContent: 'flex-end',
|
|
253
|
-
marginTop: 8,
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const summaryStyle: CSSProperties = {
|
|
257
|
-
margin: '0 0 12px 0',
|
|
258
|
-
fontSize: 12,
|
|
259
|
-
color: 'var(--squisq-recorder-muted)',
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const recordingStatusStyle: CSSProperties = {
|
|
263
|
-
fontSize: 13,
|
|
264
|
-
fontVariantNumeric: 'tabular-nums',
|
|
265
|
-
marginBottom: 12,
|
|
266
|
-
color: 'var(--squisq-recorder-accent)',
|
|
267
|
-
fontWeight: 600,
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
271
|
-
|
|
272
|
-
function formatDurationMs(ms: number): string {
|
|
273
|
-
const totalSec = Math.floor(ms / 1000);
|
|
274
|
-
const m = Math.floor(totalSec / 60);
|
|
275
|
-
const s = totalSec % 60;
|
|
276
|
-
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** The two mutually-exclusive video sources, plus "none" (audio-only). */
|
|
280
|
-
type VideoSource = 'none' | 'camera' | 'screen';
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Resolve the toggle state (microphone on/off + which video source) to the
|
|
284
|
-
* recorder's capture mode. Returns null when nothing is selected — there's
|
|
285
|
-
* nothing to capture, so the controls stay disabled.
|
|
286
|
-
*
|
|
287
|
-
* Camera + Screen can't be captured together (no compositing), so the two
|
|
288
|
-
* are mutually exclusive; the microphone composes with either, or stands
|
|
289
|
-
* alone as narration.
|
|
290
|
-
*/
|
|
291
|
-
function deriveSource(micOn: boolean, video: VideoSource): RecorderSource | null {
|
|
292
|
-
if (video === 'camera') return 'camera'; // mic handled via includeMicrophone
|
|
293
|
-
if (video === 'screen') return micOn ? 'screen+mic' : 'screen';
|
|
294
|
-
return micOn ? 'mic' : null;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Initial toggle state for a given starting capture mode. */
|
|
298
|
-
function toggleStateFromMode(mode: RecorderSource): { micOn: boolean; video: VideoSource } {
|
|
299
|
-
switch (mode) {
|
|
300
|
-
case 'mic':
|
|
301
|
-
return { micOn: true, video: 'none' };
|
|
302
|
-
case 'camera':
|
|
303
|
-
return { micOn: true, video: 'camera' };
|
|
304
|
-
case 'screen':
|
|
305
|
-
return { micOn: false, video: 'screen' };
|
|
306
|
-
case 'screen+mic':
|
|
307
|
-
return { micOn: true, video: 'screen' };
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const TOGGLES: Array<{ key: 'mic' | 'camera' | 'screen'; label: string }> = [
|
|
312
|
-
{ key: 'mic', label: 'Microphone' },
|
|
313
|
-
{ key: 'camera', label: 'Camera' },
|
|
314
|
-
{ key: 'screen', label: 'Screen' },
|
|
315
|
-
];
|
|
316
|
-
|
|
317
|
-
/** One-line summary of what the current toggle combination will capture. */
|
|
318
|
-
function captureSummary(micOn: boolean, video: VideoSource): string {
|
|
319
|
-
if (video === 'camera') {
|
|
320
|
-
return micOn
|
|
321
|
-
? 'Camera video with your microphone. Saved as a video clip.'
|
|
322
|
-
: 'Camera video only (no microphone). Saved as a video clip.';
|
|
323
|
-
}
|
|
324
|
-
if (video === 'screen') {
|
|
325
|
-
return micOn
|
|
326
|
-
? 'Screen capture with your microphone mixed in. System audio when available.'
|
|
327
|
-
: 'Screen capture (no microphone). System audio when available.';
|
|
328
|
-
}
|
|
329
|
-
return micOn
|
|
330
|
-
? 'Voice-only audio. Pairs with a written script for auto-mapping to blocks.'
|
|
331
|
-
: 'Pick at least one source to record.';
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ── Component ──────────────────────────────────────────────────────
|
|
335
|
-
|
|
336
|
-
export function RecorderModal({
|
|
337
|
-
mediaProvider,
|
|
338
|
-
container = null,
|
|
339
|
-
initialMode = 'mic',
|
|
340
|
-
colorScheme = 'light',
|
|
341
|
-
onClose,
|
|
342
|
-
onSave,
|
|
343
|
-
}: RecorderModalProps) {
|
|
344
|
-
const initialToggles = toggleStateFromMode(initialMode);
|
|
345
|
-
const [micOn, setMicOn] = useState(initialToggles.micOn);
|
|
346
|
-
const [video, setVideo] = useState<VideoSource>(initialToggles.video);
|
|
347
|
-
const [sourceText, setSourceText] = useState('');
|
|
348
|
-
const [basename, setBasename] = useState('');
|
|
349
|
-
const [includeSystemAudio, setIncludeSystemAudio] = useState(false);
|
|
350
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
351
|
-
const [saveError, setSaveError] = useState<string | null>(null);
|
|
352
|
-
const [playbackUrl, setPlaybackUrl] = useState<string | null>(null);
|
|
353
|
-
|
|
354
|
-
const previewRef = useRef<HTMLVideoElement | null>(null);
|
|
355
|
-
|
|
356
|
-
// Toggle state → the recorder's capture mode. `source` is null when nothing
|
|
357
|
-
// is selected; we feed the hook a harmless default and gate the controls on
|
|
358
|
-
// `canCapture` instead.
|
|
359
|
-
const derivedSource = deriveSource(micOn, video);
|
|
360
|
-
const canCapture = derivedSource !== null;
|
|
361
|
-
const source: RecorderSource = derivedSource ?? 'mic';
|
|
362
|
-
|
|
363
|
-
const recorder = useMediaRecorder({
|
|
364
|
-
source,
|
|
365
|
-
includeMicrophone: video === 'camera' ? micOn : undefined,
|
|
366
|
-
systemAudio: video === 'screen' ? includeSystemAudio : false,
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
useStreamPreview(previewRef, recorder.state === 'stopped' ? null : recorder.stream);
|
|
370
|
-
|
|
371
|
-
// Generate (and later revoke) a blob URL for the recorded clip so the
|
|
372
|
-
// playback element has something to point at. The dependency on the
|
|
373
|
-
// blob identity means a new URL is created every time a fresh
|
|
374
|
-
// recording lands, and the cleanup callback revokes the previous one.
|
|
375
|
-
useEffect(() => {
|
|
376
|
-
if (!recorder.blob) {
|
|
377
|
-
setPlaybackUrl(null);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
const url = URL.createObjectURL(recorder.blob);
|
|
381
|
-
setPlaybackUrl(url);
|
|
382
|
-
return () => {
|
|
383
|
-
URL.revokeObjectURL(url);
|
|
384
|
-
};
|
|
385
|
-
}, [recorder.blob]);
|
|
386
|
-
|
|
387
|
-
// Switching capture config mid-session: tear down whatever stream/
|
|
388
|
-
// recorder we had so the new mode acquires a fresh one. We key on a
|
|
389
|
-
// signature of everything that changes the acquired stream — including
|
|
390
|
-
// the camera microphone and screen system-audio flags, which don't
|
|
391
|
-
// change `source` on their own. The hook handles its own teardown on
|
|
392
|
-
// unmount; cancel() here covers in-place changes.
|
|
393
|
-
const captureKey = `${source}:${video === 'camera' ? micOn : ''}:${includeSystemAudio}`;
|
|
394
|
-
const previousKeyRef = useRef(captureKey);
|
|
395
|
-
useEffect(() => {
|
|
396
|
-
if (previousKeyRef.current !== captureKey) {
|
|
397
|
-
previousKeyRef.current = captureKey;
|
|
398
|
-
recorder.cancel();
|
|
399
|
-
}
|
|
400
|
-
}, [captureKey, recorder]);
|
|
401
|
-
|
|
402
|
-
// Make sure tearing down the modal always releases the camera /
|
|
403
|
-
// screen-capture indicator. The hook's own unmount effect handles
|
|
404
|
-
// this, but we also kill the stream eagerly on close so a slow
|
|
405
|
-
// unmount doesn't leave the indicator lit between renders.
|
|
406
|
-
const handleClose = useCallback(() => {
|
|
407
|
-
recorder.cancel();
|
|
408
|
-
onClose();
|
|
409
|
-
}, [recorder, onClose]);
|
|
410
|
-
|
|
411
|
-
const handleRequest = useCallback(async () => {
|
|
412
|
-
setSaveError(null);
|
|
413
|
-
try {
|
|
414
|
-
await recorder.request();
|
|
415
|
-
} catch {
|
|
416
|
-
// Already surfaced via recorder.error.
|
|
417
|
-
}
|
|
418
|
-
}, [recorder]);
|
|
419
|
-
|
|
420
|
-
const handleStart = useCallback(() => {
|
|
421
|
-
setSaveError(null);
|
|
422
|
-
recorder.start();
|
|
423
|
-
}, [recorder]);
|
|
424
|
-
|
|
425
|
-
const handleStop = useCallback(async () => {
|
|
426
|
-
setSaveError(null);
|
|
427
|
-
await recorder.stop();
|
|
428
|
-
}, [recorder]);
|
|
429
|
-
|
|
430
|
-
const handleSave = useCallback(async () => {
|
|
431
|
-
if (!recorder.blob || !recorder.mimeType || !recorder.extension || !recorder.directory) {
|
|
432
|
-
setSaveError('Nothing to save yet — record something first.');
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
setIsSaving(true);
|
|
436
|
-
setSaveError(null);
|
|
437
|
-
try {
|
|
438
|
-
const filename = buildFilename(
|
|
439
|
-
source === 'mic' ? 'audio' : 'video',
|
|
440
|
-
recorder.extension,
|
|
441
|
-
basename,
|
|
442
|
-
);
|
|
443
|
-
const relativeName = `${recorder.directory}/${filename}`;
|
|
444
|
-
const relativePath = await mediaProvider.addMedia(
|
|
445
|
-
relativeName,
|
|
446
|
-
recorder.blob,
|
|
447
|
-
recorder.mimeType,
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
let hasTimingSidecar = false;
|
|
451
|
-
if (source === 'mic') {
|
|
452
|
-
const timing = buildTimingJson(sourceText, recorder.durationMs / 1000);
|
|
453
|
-
const encoded = encodeTimingJson(timing);
|
|
454
|
-
const sidecarPath = timingPathFor(relativePath);
|
|
455
|
-
// Prefer direct container write so the sidecar lands at the
|
|
456
|
-
// exact path the audio-mapping pipeline expects. Fall back to
|
|
457
|
-
// addMedia(), which may rename — log if so.
|
|
458
|
-
if (container) {
|
|
459
|
-
await container.writeFile(sidecarPath, encoded, 'application/json');
|
|
460
|
-
hasTimingSidecar = true;
|
|
461
|
-
} else {
|
|
462
|
-
const written = await mediaProvider.addMedia(sidecarPath, encoded, 'application/json');
|
|
463
|
-
hasTimingSidecar = written === sidecarPath;
|
|
464
|
-
if (!hasTimingSidecar) {
|
|
465
|
-
console.warn(
|
|
466
|
-
`[squisq-recorder] timing.json was saved as "${written}" instead of "${sidecarPath}" — auto-mapping may not pick it up.`,
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const result: RecorderSaveResult = {
|
|
473
|
-
relativePath,
|
|
474
|
-
filename,
|
|
475
|
-
source,
|
|
476
|
-
mimeType: recorder.mimeType,
|
|
477
|
-
duration: recorder.durationMs / 1000,
|
|
478
|
-
hasTimingSidecar,
|
|
479
|
-
};
|
|
480
|
-
if (source === 'mic') {
|
|
481
|
-
result.sourceText = sourceText;
|
|
482
|
-
}
|
|
483
|
-
onSave?.(result);
|
|
484
|
-
handleClose();
|
|
485
|
-
} catch (err: unknown) {
|
|
486
|
-
setSaveError(err instanceof Error ? err.message : 'Failed to save recording');
|
|
487
|
-
} finally {
|
|
488
|
-
setIsSaving(false);
|
|
489
|
-
}
|
|
490
|
-
}, [recorder, source, basename, sourceText, mediaProvider, container, onSave, handleClose]);
|
|
491
|
-
|
|
492
|
-
const handleDiscard = useCallback(() => {
|
|
493
|
-
recorder.reset();
|
|
494
|
-
}, [recorder]);
|
|
495
|
-
|
|
496
|
-
const isAudioOnly = source === 'mic';
|
|
497
|
-
const showPreview = recorder.state !== 'idle' && recorder.state !== 'error';
|
|
498
|
-
const canRecord = recorder.state === 'ready';
|
|
499
|
-
const canStop = recorder.state === 'recording';
|
|
500
|
-
const canSave = recorder.state === 'stopped' && recorder.blob !== null;
|
|
501
|
-
const isBusy = recorder.state === 'requesting' || recorder.state === 'stopping' || isSaving;
|
|
502
|
-
|
|
503
|
-
// Toggles lock while a stream is live (acquiring or recording) — changing
|
|
504
|
-
// the capture config then would tear down the in-progress take.
|
|
505
|
-
const togglesLocked = recorder.state === 'recording' || recorder.state === 'requesting';
|
|
506
|
-
const toggleActiveFor = (key: 'mic' | 'camera' | 'screen') =>
|
|
507
|
-
key === 'mic' ? micOn : video === key;
|
|
508
|
-
const onToggle = (key: 'mic' | 'camera' | 'screen') => {
|
|
509
|
-
if (key === 'mic') {
|
|
510
|
-
setMicOn((on) => !on);
|
|
511
|
-
} else {
|
|
512
|
-
// Camera and Screen are mutually exclusive — turning one on clears the
|
|
513
|
-
// other (no camera+screen compositing).
|
|
514
|
-
setVideo((v) => (v === key ? 'none' : key));
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
return (
|
|
519
|
-
<div
|
|
520
|
-
className="squisq-editor-shell squisq-recorder-overlay"
|
|
521
|
-
data-theme={colorScheme}
|
|
522
|
-
style={{ ...overlayStyle, ...recorderThemeStyle(colorScheme) }}
|
|
523
|
-
role="dialog"
|
|
524
|
-
aria-modal="true"
|
|
525
|
-
aria-label="Record media"
|
|
526
|
-
>
|
|
527
|
-
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
|
|
528
|
-
<h2 style={titleStyle}>Record media</h2>
|
|
529
|
-
|
|
530
|
-
<div style={toggleRowStyle} role="group" aria-label="Capture sources">
|
|
531
|
-
{TOGGLES.map((t) => {
|
|
532
|
-
const active = toggleActiveFor(t.key);
|
|
533
|
-
return (
|
|
534
|
-
<button
|
|
535
|
-
key={t.key}
|
|
536
|
-
type="button"
|
|
537
|
-
aria-pressed={active}
|
|
538
|
-
style={active ? toggleActive : toggleBase}
|
|
539
|
-
onClick={() => onToggle(t.key)}
|
|
540
|
-
disabled={togglesLocked}
|
|
541
|
-
>
|
|
542
|
-
{t.label}
|
|
543
|
-
</button>
|
|
544
|
-
);
|
|
545
|
-
})}
|
|
546
|
-
</div>
|
|
547
|
-
|
|
548
|
-
<p style={summaryStyle}>{captureSummary(micOn, video)}</p>
|
|
549
|
-
|
|
550
|
-
{recorder.error && <div style={errorStyle}>{recorder.error.message}</div>}
|
|
551
|
-
{saveError && <div style={errorStyle}>{saveError}</div>}
|
|
552
|
-
|
|
553
|
-
{/* Preview surface. Three modes:
|
|
554
|
-
- Pre-acquisition (idle / error): a static prompt.
|
|
555
|
-
- Live (ready / recording / requesting / stopping): the stream
|
|
556
|
-
piped into a muted <video>, or a recording meter for mic.
|
|
557
|
-
- Playback (stopped): the captured blob bound to a <video>/<audio>
|
|
558
|
-
with native controls so the user can audition before saving.
|
|
559
|
-
*/}
|
|
560
|
-
{!showPreview && (
|
|
561
|
-
<div style={previewBoxStyle}>
|
|
562
|
-
<span>Click Start Preview to start a recording.</span>
|
|
563
|
-
</div>
|
|
564
|
-
)}
|
|
565
|
-
{showPreview && recorder.state !== 'stopped' && !isAudioOnly && (
|
|
566
|
-
<div style={previewBoxStyle}>
|
|
567
|
-
<video
|
|
568
|
-
ref={previewRef}
|
|
569
|
-
autoPlay
|
|
570
|
-
muted
|
|
571
|
-
playsInline
|
|
572
|
-
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
573
|
-
/>
|
|
574
|
-
</div>
|
|
575
|
-
)}
|
|
576
|
-
{showPreview && recorder.state !== 'stopped' && isAudioOnly && (
|
|
577
|
-
<div style={audioMeterStyle}>
|
|
578
|
-
{recorder.state === 'recording' ? (
|
|
579
|
-
<>● Recording {formatDurationMs(recorder.durationMs)}</>
|
|
580
|
-
) : (
|
|
581
|
-
<>Microphone ready</>
|
|
582
|
-
)}
|
|
583
|
-
</div>
|
|
584
|
-
)}
|
|
585
|
-
{recorder.state === 'stopped' && playbackUrl && !isAudioOnly && (
|
|
586
|
-
<div style={previewBoxStyle}>
|
|
587
|
-
<video
|
|
588
|
-
src={playbackUrl}
|
|
589
|
-
controls
|
|
590
|
-
playsInline
|
|
591
|
-
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
592
|
-
/>
|
|
593
|
-
</div>
|
|
594
|
-
)}
|
|
595
|
-
{recorder.state === 'stopped' && playbackUrl && isAudioOnly && (
|
|
596
|
-
<div style={{ marginBottom: 12 }}>
|
|
597
|
-
<div style={{ ...audioMeterStyle, marginBottom: 8 }}>
|
|
598
|
-
✓ Recorded {formatDurationMs(recorder.durationMs)}
|
|
599
|
-
</div>
|
|
600
|
-
<audio src={playbackUrl} controls style={{ width: '100%' }} />
|
|
601
|
-
</div>
|
|
602
|
-
)}
|
|
603
|
-
|
|
604
|
-
{/* Mode-specific fields */}
|
|
605
|
-
{source === 'mic' && (
|
|
606
|
-
<>
|
|
607
|
-
<label style={labelStyle} htmlFor="recorder-source-text">
|
|
608
|
-
Script (used to auto-match this narration to a block)
|
|
609
|
-
</label>
|
|
610
|
-
<textarea
|
|
611
|
-
id="recorder-source-text"
|
|
612
|
-
style={textareaStyle}
|
|
613
|
-
placeholder="Type the text you're going to read aloud."
|
|
614
|
-
value={sourceText}
|
|
615
|
-
onChange={(e) => setSourceText(e.target.value)}
|
|
616
|
-
disabled={recorder.state === 'recording'}
|
|
617
|
-
/>
|
|
618
|
-
</>
|
|
619
|
-
)}
|
|
620
|
-
{video === 'screen' && (
|
|
621
|
-
<label
|
|
622
|
-
style={{
|
|
623
|
-
display: 'flex',
|
|
624
|
-
alignItems: 'center',
|
|
625
|
-
gap: 6,
|
|
626
|
-
marginBottom: 12,
|
|
627
|
-
fontSize: 13,
|
|
628
|
-
}}
|
|
629
|
-
>
|
|
630
|
-
<input
|
|
631
|
-
type="checkbox"
|
|
632
|
-
style={{ accentColor: 'var(--squisq-recorder-accent)' }}
|
|
633
|
-
checked={includeSystemAudio}
|
|
634
|
-
onChange={(e) => setIncludeSystemAudio(e.target.checked)}
|
|
635
|
-
disabled={recorder.state === 'recording' || recorder.state === 'requesting'}
|
|
636
|
-
/>
|
|
637
|
-
Include system audio (Chrome only)
|
|
638
|
-
</label>
|
|
639
|
-
)}
|
|
640
|
-
|
|
641
|
-
<label style={labelStyle} htmlFor="recorder-basename">
|
|
642
|
-
Filename (optional)
|
|
643
|
-
</label>
|
|
644
|
-
<input
|
|
645
|
-
id="recorder-basename"
|
|
646
|
-
type="text"
|
|
647
|
-
style={inputStyle}
|
|
648
|
-
placeholder={source === 'mic' ? 'narration' : 'recording'}
|
|
649
|
-
value={basename}
|
|
650
|
-
onChange={(e) => setBasename(e.target.value)}
|
|
651
|
-
disabled={recorder.state === 'recording'}
|
|
652
|
-
/>
|
|
653
|
-
|
|
654
|
-
{/* Live duration during recording */}
|
|
655
|
-
{recorder.state === 'recording' && !isAudioOnly && (
|
|
656
|
-
<div style={recordingStatusStyle}>
|
|
657
|
-
● Recording {formatDurationMs(recorder.durationMs)}
|
|
658
|
-
</div>
|
|
659
|
-
)}
|
|
660
|
-
|
|
661
|
-
{/* Action buttons. Layout depends on state. */}
|
|
662
|
-
<div style={buttonRowStyle}>
|
|
663
|
-
<button type="button" style={btnSecondary} onClick={handleClose} disabled={isBusy}>
|
|
664
|
-
Close
|
|
665
|
-
</button>
|
|
666
|
-
|
|
667
|
-
{(recorder.state === 'idle' ||
|
|
668
|
-
recorder.state === 'error' ||
|
|
669
|
-
recorder.state === 'requesting') && (
|
|
670
|
-
<button
|
|
671
|
-
type="button"
|
|
672
|
-
style={btnPrimary}
|
|
673
|
-
onClick={handleRequest}
|
|
674
|
-
disabled={isBusy || !canCapture}
|
|
675
|
-
>
|
|
676
|
-
{recorder.state === 'requesting' ? 'Requesting…' : 'Start preview'}
|
|
677
|
-
</button>
|
|
678
|
-
)}
|
|
679
|
-
|
|
680
|
-
{canRecord && (
|
|
681
|
-
<button type="button" style={btnPrimary} onClick={handleStart} disabled={isBusy}>
|
|
682
|
-
Record
|
|
683
|
-
</button>
|
|
684
|
-
)}
|
|
685
|
-
|
|
686
|
-
{canStop && (
|
|
687
|
-
<button type="button" style={btnDanger} onClick={handleStop} disabled={isBusy}>
|
|
688
|
-
Stop
|
|
689
|
-
</button>
|
|
690
|
-
)}
|
|
691
|
-
|
|
692
|
-
{canSave && (
|
|
693
|
-
<>
|
|
694
|
-
<button type="button" style={btnSecondary} onClick={handleDiscard} disabled={isBusy}>
|
|
695
|
-
Discard & re-record
|
|
696
|
-
</button>
|
|
697
|
-
<button type="button" style={btnPrimary} onClick={handleSave} disabled={isBusy}>
|
|
698
|
-
{isSaving ? 'Saving…' : 'Save to document'}
|
|
699
|
-
</button>
|
|
700
|
-
</>
|
|
701
|
-
)}
|
|
702
|
-
</div>
|
|
703
|
-
</div>
|
|
704
|
-
</div>
|
|
705
|
-
);
|
|
706
|
-
}
|