@bendyline/squisq-editor-react 1.4.0 → 1.5.1
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/DocumentSettingsDialog.d.ts +26 -0
- package/dist/DocumentSettingsDialog.d.ts.map +1 -0
- package/dist/DocumentSettingsDialog.js +115 -0
- package/dist/DocumentSettingsDialog.js.map +1 -0
- package/dist/EditorContext.d.ts +248 -4
- package/dist/EditorContext.d.ts.map +1 -1
- package/dist/EditorContext.js +248 -10
- package/dist/EditorContext.js.map +1 -1
- package/dist/EditorShell.d.ts +173 -4
- package/dist/EditorShell.d.ts.map +1 -1
- package/dist/EditorShell.js +110 -10
- package/dist/EditorShell.js.map +1 -1
- package/dist/EmojiPicker.d.ts +50 -0
- package/dist/EmojiPicker.d.ts.map +1 -0
- package/dist/EmojiPicker.js +182 -0
- package/dist/EmojiPicker.js.map +1 -0
- package/dist/ImageEditor.d.ts +68 -0
- package/dist/ImageEditor.d.ts.map +1 -0
- package/dist/ImageEditor.js +166 -0
- package/dist/ImageEditor.js.map +1 -0
- package/dist/ImageNodeView.d.ts +13 -1
- package/dist/ImageNodeView.d.ts.map +1 -1
- package/dist/ImageNodeView.js +172 -19
- package/dist/ImageNodeView.js.map +1 -1
- package/dist/ImageViewer.d.ts +26 -0
- package/dist/ImageViewer.d.ts.map +1 -0
- package/dist/ImageViewer.js +119 -0
- package/dist/ImageViewer.js.map +1 -0
- package/dist/InlineIcon.d.ts +17 -0
- package/dist/InlineIcon.d.ts.map +1 -0
- package/dist/InlineIcon.js +72 -0
- package/dist/InlineIcon.js.map +1 -0
- package/dist/InlinePreviewGutter.d.ts +52 -0
- package/dist/InlinePreviewGutter.d.ts.map +1 -0
- package/dist/InlinePreviewGutter.js +397 -0
- package/dist/InlinePreviewGutter.js.map +1 -0
- package/dist/LinkDialog.d.ts +43 -0
- package/dist/LinkDialog.d.ts.map +1 -0
- package/dist/LinkDialog.js +102 -0
- package/dist/LinkDialog.js.map +1 -0
- package/dist/MentionExtension.js +10 -7
- package/dist/MentionExtension.js.map +1 -1
- package/dist/OutlinePanel.d.ts +17 -0
- package/dist/OutlinePanel.d.ts.map +1 -0
- package/dist/OutlinePanel.js +167 -0
- package/dist/OutlinePanel.js.map +1 -0
- package/dist/PlainHtmlPreview.d.ts +50 -0
- package/dist/PlainHtmlPreview.d.ts.map +1 -0
- package/dist/PlainHtmlPreview.js +155 -0
- package/dist/PlainHtmlPreview.js.map +1 -0
- package/dist/PreviewControls.d.ts +15 -1
- package/dist/PreviewControls.d.ts.map +1 -1
- package/dist/PreviewControls.js +75 -18
- package/dist/PreviewControls.js.map +1 -1
- package/dist/PreviewPanel.d.ts +11 -10
- package/dist/PreviewPanel.d.ts.map +1 -1
- package/dist/PreviewPanel.js +20 -17
- package/dist/PreviewPanel.js.map +1 -1
- package/dist/RawEditor.d.ts.map +1 -1
- package/dist/RawEditor.js +198 -4
- package/dist/RawEditor.js.map +1 -1
- package/dist/RecorderEntry.d.ts +24 -0
- package/dist/RecorderEntry.d.ts.map +1 -0
- package/dist/RecorderEntry.js +139 -0
- package/dist/RecorderEntry.js.map +1 -0
- package/dist/TemplateAnnotation.d.ts.map +1 -1
- package/dist/TemplateAnnotation.js +32 -6
- package/dist/TemplateAnnotation.js.map +1 -1
- package/dist/TemplatePicker.d.ts +53 -0
- package/dist/TemplatePicker.d.ts.map +1 -0
- package/dist/TemplatePicker.js +388 -0
- package/dist/TemplatePicker.js.map +1 -0
- package/dist/ThemeCustomizerPanel.d.ts +32 -0
- package/dist/ThemeCustomizerPanel.d.ts.map +1 -0
- package/dist/ThemeCustomizerPanel.js +256 -0
- package/dist/ThemeCustomizerPanel.js.map +1 -0
- package/dist/ThemePicker.d.ts +33 -0
- package/dist/ThemePicker.d.ts.map +1 -0
- package/dist/ThemePicker.js +148 -0
- package/dist/ThemePicker.js.map +1 -0
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar.js +508 -33
- package/dist/Toolbar.js.map +1 -1
- package/dist/VersionHistoryPanel.d.ts +14 -0
- package/dist/VersionHistoryPanel.d.ts.map +1 -0
- package/dist/VersionHistoryPanel.js +147 -0
- package/dist/VersionHistoryPanel.js.map +1 -0
- package/dist/ViewMenuPanel.d.ts +13 -0
- package/dist/ViewMenuPanel.d.ts.map +1 -0
- package/dist/ViewMenuPanel.js +58 -0
- package/dist/ViewMenuPanel.js.map +1 -0
- package/dist/WysiwygEditor.d.ts.map +1 -1
- package/dist/WysiwygEditor.js +198 -9
- package/dist/WysiwygEditor.js.map +1 -1
- package/dist/__tests__/detectMarkdown.test.js +0 -14
- package/dist/__tests__/detectMarkdown.test.js.map +1 -1
- package/dist/__tests__/documentSettingsDialog.test.d.ts +2 -0
- package/dist/__tests__/documentSettingsDialog.test.d.ts.map +1 -0
- package/dist/__tests__/documentSettingsDialog.test.js +132 -0
- package/dist/__tests__/documentSettingsDialog.test.js.map +1 -0
- package/dist/__tests__/emojiPicker.test.d.ts +2 -0
- package/dist/__tests__/emojiPicker.test.d.ts.map +1 -0
- package/dist/__tests__/emojiPicker.test.js +111 -0
- package/dist/__tests__/emojiPicker.test.js.map +1 -0
- package/dist/__tests__/fileKind.test.js +13 -0
- package/dist/__tests__/fileKind.test.js.map +1 -1
- package/dist/__tests__/imageEditAffordance.test.d.ts +2 -0
- package/dist/__tests__/imageEditAffordance.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditAffordance.test.js +188 -0
- package/dist/__tests__/imageEditAffordance.test.js.map +1 -0
- package/dist/__tests__/imageEditorShell.test.d.ts +2 -0
- package/dist/__tests__/imageEditorShell.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditorShell.test.js +52 -0
- package/dist/__tests__/imageEditorShell.test.js.map +1 -0
- package/dist/__tests__/imageEditorState.test.d.ts +3 -0
- package/dist/__tests__/imageEditorState.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditorState.test.js +148 -0
- package/dist/__tests__/imageEditorState.test.js.map +1 -0
- package/dist/__tests__/inlinePreviewGutter.test.d.ts +2 -0
- package/dist/__tests__/inlinePreviewGutter.test.d.ts.map +1 -0
- package/dist/__tests__/inlinePreviewGutter.test.js +51 -0
- package/dist/__tests__/inlinePreviewGutter.test.js.map +1 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts +2 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts.map +1 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js +63 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js.map +1 -0
- package/dist/__tests__/jsonEditor.test.d.ts +2 -0
- package/dist/__tests__/jsonEditor.test.d.ts.map +1 -0
- package/dist/__tests__/jsonEditor.test.js +134 -0
- package/dist/__tests__/jsonEditor.test.js.map +1 -0
- package/dist/__tests__/layersPanel.test.d.ts +2 -0
- package/dist/__tests__/layersPanel.test.d.ts.map +1 -0
- package/dist/__tests__/layersPanel.test.js +84 -0
- package/dist/__tests__/layersPanel.test.js.map +1 -0
- package/dist/__tests__/linkDialogDocPicker.test.d.ts +7 -0
- package/dist/__tests__/linkDialogDocPicker.test.d.ts.map +1 -0
- package/dist/__tests__/linkDialogDocPicker.test.js +75 -0
- package/dist/__tests__/linkDialogDocPicker.test.js.map +1 -0
- package/dist/__tests__/outlinePanel.test.d.ts +2 -0
- package/dist/__tests__/outlinePanel.test.d.ts.map +1 -0
- package/dist/__tests__/outlinePanel.test.js +68 -0
- package/dist/__tests__/outlinePanel.test.js.map +1 -0
- package/dist/__tests__/plainHtmlPreview.test.d.ts +2 -0
- package/dist/__tests__/plainHtmlPreview.test.d.ts.map +1 -0
- package/dist/__tests__/plainHtmlPreview.test.js +87 -0
- package/dist/__tests__/plainHtmlPreview.test.js.map +1 -0
- package/dist/__tests__/propertiesPanel.test.d.ts +2 -0
- package/dist/__tests__/propertiesPanel.test.d.ts.map +1 -0
- package/dist/__tests__/propertiesPanel.test.js +64 -0
- package/dist/__tests__/propertiesPanel.test.js.map +1 -0
- package/dist/__tests__/recorderFormats.test.d.ts +2 -0
- package/dist/__tests__/recorderFormats.test.d.ts.map +1 -0
- package/dist/__tests__/recorderFormats.test.js +121 -0
- package/dist/__tests__/recorderFormats.test.js.map +1 -0
- package/dist/__tests__/recorderTimingJson.test.d.ts +2 -0
- package/dist/__tests__/recorderTimingJson.test.d.ts.map +1 -0
- package/dist/__tests__/recorderTimingJson.test.js +37 -0
- package/dist/__tests__/recorderTimingJson.test.js.map +1 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts +2 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts.map +1 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.js +31 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.js.map +1 -0
- package/dist/__tests__/tiptapBridge.test.js +13 -0
- package/dist/__tests__/tiptapBridge.test.js.map +1 -1
- package/dist/__tests__/useImageEditor.test.d.ts +2 -0
- package/dist/__tests__/useImageEditor.test.d.ts.map +1 -0
- package/dist/__tests__/useImageEditor.test.js +131 -0
- package/dist/__tests__/useImageEditor.test.js.map +1 -0
- package/dist/__tests__/useMediaRecorder.test.d.ts +2 -0
- package/dist/__tests__/useMediaRecorder.test.d.ts.map +1 -0
- package/dist/__tests__/useMediaRecorder.test.js +153 -0
- package/dist/__tests__/useMediaRecorder.test.js.map +1 -0
- package/dist/__tests__/versionHistory.test.d.ts +2 -0
- package/dist/__tests__/versionHistory.test.d.ts.map +1 -0
- package/dist/__tests__/versionHistory.test.js +124 -0
- package/dist/__tests__/versionHistory.test.js.map +1 -0
- package/dist/blockSlice.d.ts +24 -0
- package/dist/blockSlice.d.ts.map +1 -0
- package/dist/blockSlice.js +63 -0
- package/dist/blockSlice.js.map +1 -0
- package/dist/buildPreviewDoc.d.ts.map +1 -1
- package/dist/buildPreviewDoc.js +52 -2
- package/dist/buildPreviewDoc.js.map +1 -1
- package/dist/emojiData.d.ts +81 -0
- package/dist/emojiData.d.ts.map +1 -0
- package/dist/emojiData.js +1283 -0
- package/dist/emojiData.js.map +1 -0
- package/dist/fileKind.d.ts +6 -2
- package/dist/fileKind.d.ts.map +1 -1
- package/dist/fileKind.js +25 -4
- package/dist/fileKind.js.map +1 -1
- package/dist/hooks/useFileDrop.d.ts.map +1 -1
- package/dist/hooks/useFileDrop.js +40 -4
- package/dist/hooks/useFileDrop.js.map +1 -1
- package/dist/imageEditor/CanvasSurface.d.ts +31 -0
- package/dist/imageEditor/CanvasSurface.d.ts.map +1 -0
- package/dist/imageEditor/CanvasSurface.js +264 -0
- package/dist/imageEditor/CanvasSurface.js.map +1 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts +39 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts.map +1 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.js +283 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.js.map +1 -0
- package/dist/imageEditor/LayersPanel.d.ts +14 -0
- package/dist/imageEditor/LayersPanel.d.ts.map +1 -0
- package/dist/imageEditor/LayersPanel.js +43 -0
- package/dist/imageEditor/LayersPanel.js.map +1 -0
- package/dist/imageEditor/PropertiesPanel.d.ts +14 -0
- package/dist/imageEditor/PropertiesPanel.d.ts.map +1 -0
- package/dist/imageEditor/PropertiesPanel.js +97 -0
- package/dist/imageEditor/PropertiesPanel.js.map +1 -0
- package/dist/imageEditor/Toolbar.d.ts +30 -0
- package/dist/imageEditor/Toolbar.d.ts.map +1 -0
- package/dist/imageEditor/Toolbar.js +108 -0
- package/dist/imageEditor/Toolbar.js.map +1 -0
- package/dist/imageEditor/icons.d.ts +24 -0
- package/dist/imageEditor/icons.d.ts.map +1 -0
- package/dist/imageEditor/icons.js +45 -0
- package/dist/imageEditor/icons.js.map +1 -0
- package/dist/imageEditor/layers/EditorImageLayer.d.ts +16 -0
- package/dist/imageEditor/layers/EditorImageLayer.d.ts.map +1 -0
- package/dist/imageEditor/layers/EditorImageLayer.js +37 -0
- package/dist/imageEditor/layers/EditorImageLayer.js.map +1 -0
- package/dist/imageEditor/layers/EditorShapeLayer.d.ts +15 -0
- package/dist/imageEditor/layers/EditorShapeLayer.d.ts.map +1 -0
- package/dist/imageEditor/layers/EditorShapeLayer.js +20 -0
- package/dist/imageEditor/layers/EditorShapeLayer.js.map +1 -0
- package/dist/imageEditor/layers/EditorTextLayer.d.ts +18 -0
- package/dist/imageEditor/layers/EditorTextLayer.d.ts.map +1 -0
- package/dist/imageEditor/layers/EditorTextLayer.js +13 -0
- package/dist/imageEditor/layers/EditorTextLayer.js.map +1 -0
- package/dist/imageEditor/layers/SelectionHandles.d.ts +17 -0
- package/dist/imageEditor/layers/SelectionHandles.d.ts.map +1 -0
- package/dist/imageEditor/layers/SelectionHandles.js +19 -0
- package/dist/imageEditor/layers/SelectionHandles.js.map +1 -0
- package/dist/imageEditor/state.d.ts +76 -0
- package/dist/imageEditor/state.d.ts.map +1 -0
- package/dist/imageEditor/state.js +87 -0
- package/dist/imageEditor/state.js.map +1 -0
- package/dist/imageEditor/useImageEditor.d.ts +53 -0
- package/dist/imageEditor/useImageEditor.d.ts.map +1 -0
- package/dist/imageEditor/useImageEditor.js +244 -0
- package/dist/imageEditor/useImageEditor.js.map +1 -0
- package/dist/imageEditor/useImageEditorTokens.d.ts +16 -0
- package/dist/imageEditor/useImageEditorTokens.d.ts.map +1 -0
- package/dist/imageEditor/useImageEditorTokens.js +45 -0
- package/dist/imageEditor/useImageEditorTokens.js.map +1 -0
- package/dist/index.d.ts +48 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonEditor/EmbeddedRichTextField.d.ts +15 -0
- package/dist/jsonEditor/EmbeddedRichTextField.d.ts.map +1 -0
- package/dist/jsonEditor/EmbeddedRichTextField.js +74 -0
- package/dist/jsonEditor/EmbeddedRichTextField.js.map +1 -0
- package/dist/jsonEditor/JsonEditor.d.ts +36 -0
- package/dist/jsonEditor/JsonEditor.d.ts.map +1 -0
- package/dist/jsonEditor/JsonEditor.js +15 -0
- package/dist/jsonEditor/JsonEditor.js.map +1 -0
- package/dist/jsonEditor/JsonEditorContext.d.ts +28 -0
- package/dist/jsonEditor/JsonEditorContext.d.ts.map +1 -0
- package/dist/jsonEditor/JsonEditorContext.js +41 -0
- package/dist/jsonEditor/JsonEditorContext.js.map +1 -0
- package/dist/jsonEditor/RenderNode.d.ts +16 -0
- package/dist/jsonEditor/RenderNode.d.ts.map +1 -0
- package/dist/jsonEditor/RenderNode.js +32 -0
- package/dist/jsonEditor/RenderNode.js.map +1 -0
- package/dist/jsonEditor/editors.d.ts +36 -0
- package/dist/jsonEditor/editors.d.ts.map +1 -0
- package/dist/jsonEditor/editors.js +347 -0
- package/dist/jsonEditor/editors.js.map +1 -0
- package/dist/jsonEditor/index.d.ts +3 -0
- package/dist/jsonEditor/index.d.ts.map +1 -0
- package/dist/jsonEditor/index.js +2 -0
- package/dist/jsonEditor/index.js.map +1 -0
- package/dist/jsonEditor/useJsonEditorTokens.d.ts +13 -0
- package/dist/jsonEditor/useJsonEditorTokens.d.ts.map +1 -0
- package/dist/jsonEditor/useJsonEditorTokens.js +38 -0
- package/dist/jsonEditor/useJsonEditorTokens.js.map +1 -0
- package/dist/recorder/RecorderButton.d.ts +31 -0
- package/dist/recorder/RecorderButton.d.ts.map +1 -0
- package/dist/recorder/RecorderButton.js +24 -0
- package/dist/recorder/RecorderButton.js.map +1 -0
- package/dist/recorder/RecorderModal.d.ts +59 -0
- package/dist/recorder/RecorderModal.d.ts.map +1 -0
- package/dist/recorder/RecorderModal.js +333 -0
- package/dist/recorder/RecorderModal.js.map +1 -0
- package/dist/recorder/RecorderPanel.d.ts +25 -0
- package/dist/recorder/RecorderPanel.d.ts.map +1 -0
- package/dist/recorder/RecorderPanel.js +30 -0
- package/dist/recorder/RecorderPanel.js.map +1 -0
- package/dist/recorder/formats.d.ts +51 -0
- package/dist/recorder/formats.d.ts.map +1 -0
- package/dist/recorder/formats.js +144 -0
- package/dist/recorder/formats.js.map +1 -0
- package/dist/recorder/hooks/useMediaRecorder.d.ts +90 -0
- package/dist/recorder/hooks/useMediaRecorder.d.ts.map +1 -0
- package/dist/recorder/hooks/useMediaRecorder.js +277 -0
- package/dist/recorder/hooks/useMediaRecorder.js.map +1 -0
- package/dist/recorder/hooks/useStreamPreview.d.ts +22 -0
- package/dist/recorder/hooks/useStreamPreview.d.ts.map +1 -0
- package/dist/recorder/hooks/useStreamPreview.js +44 -0
- package/dist/recorder/hooks/useStreamPreview.js.map +1 -0
- package/dist/recorder/sources/cameraStream.d.ts +22 -0
- package/dist/recorder/sources/cameraStream.d.ts.map +1 -0
- package/dist/recorder/sources/cameraStream.js +24 -0
- package/dist/recorder/sources/cameraStream.js.map +1 -0
- package/dist/recorder/sources/micStream.d.ts +15 -0
- package/dist/recorder/sources/micStream.d.ts.map +1 -0
- package/dist/recorder/sources/micStream.js +24 -0
- package/dist/recorder/sources/micStream.js.map +1 -0
- package/dist/recorder/sources/screenStream.d.ts +53 -0
- package/dist/recorder/sources/screenStream.d.ts.map +1 -0
- package/dist/recorder/sources/screenStream.js +114 -0
- package/dist/recorder/sources/screenStream.js.map +1 -0
- package/dist/recorder/timingJson.d.ts +51 -0
- package/dist/recorder/timingJson.d.ts.map +1 -0
- package/dist/recorder/timingJson.js +42 -0
- package/dist/recorder/timingJson.js.map +1 -0
- package/dist/tiptap/TiptapAudio.d.ts +26 -0
- package/dist/tiptap/TiptapAudio.d.ts.map +1 -0
- package/dist/tiptap/TiptapAudio.js +58 -0
- package/dist/tiptap/TiptapAudio.js.map +1 -0
- package/dist/tiptap/TiptapVideo.d.ts +30 -0
- package/dist/tiptap/TiptapVideo.d.ts.map +1 -0
- package/dist/tiptap/TiptapVideo.js +66 -0
- package/dist/tiptap/TiptapVideo.js.map +1 -0
- package/dist/tiptap/useResolvedMediaSrc.d.ts +2 -0
- package/dist/tiptap/useResolvedMediaSrc.d.ts.map +1 -0
- package/dist/tiptap/useResolvedMediaSrc.js +42 -0
- package/dist/tiptap/useResolvedMediaSrc.js.map +1 -0
- package/dist/tiptapBridge.d.ts.map +1 -1
- package/dist/tiptapBridge.js +171 -14
- package/dist/tiptapBridge.js.map +1 -1
- package/dist/useHeadingLayout.d.ts +54 -0
- package/dist/useHeadingLayout.d.ts.map +1 -0
- package/dist/useHeadingLayout.js +260 -0
- package/dist/useHeadingLayout.js.map +1 -0
- package/dist/utils/collectInlineFontAwesomeCss.d.ts +21 -0
- package/dist/utils/collectInlineFontAwesomeCss.d.ts.map +1 -0
- package/dist/utils/collectInlineFontAwesomeCss.js +68 -0
- package/dist/utils/collectInlineFontAwesomeCss.js.map +1 -0
- package/dist/utils/dropUtils.d.ts +21 -2
- package/dist/utils/dropUtils.d.ts.map +1 -1
- package/dist/utils/dropUtils.js +43 -4
- package/dist/utils/dropUtils.js.map +1 -1
- package/dist/utils/normalizeMalformedAssetUrl.d.ts +15 -0
- package/dist/utils/normalizeMalformedAssetUrl.d.ts.map +1 -0
- package/dist/utils/normalizeMalformedAssetUrl.js +27 -0
- package/dist/utils/normalizeMalformedAssetUrl.js.map +1 -0
- package/package.json +8 -5
- package/src/DocumentSettingsDialog.tsx +266 -0
- package/src/EditorContext.tsx +534 -10
- package/src/EditorShell.tsx +571 -55
- package/src/EmojiPicker.tsx +332 -0
- package/src/ImageEditor.tsx +327 -0
- package/src/ImageNodeView.tsx +222 -21
- package/src/ImageViewer.tsx +221 -0
- package/src/InlineIcon.ts +84 -0
- package/src/InlinePreviewGutter.tsx +582 -0
- package/src/LinkDialog.tsx +276 -0
- package/src/MentionExtension.tsx +10 -7
- package/src/OutlinePanel.tsx +295 -0
- package/src/PlainHtmlPreview.tsx +211 -0
- package/src/PreviewControls.tsx +130 -24
- package/src/PreviewPanel.tsx +38 -21
- package/src/RawEditor.tsx +215 -4
- package/src/RecorderEntry.tsx +164 -0
- package/src/TemplateAnnotation.ts +32 -6
- package/src/TemplatePicker.tsx +818 -0
- package/src/ThemeCustomizerPanel.tsx +595 -0
- package/src/ThemePicker.tsx +319 -0
- package/src/Toolbar.tsx +708 -111
- package/src/VersionHistoryPanel.tsx +329 -0
- package/src/ViewMenuPanel.tsx +188 -0
- package/src/WysiwygEditor.tsx +229 -9
- package/src/__tests__/detectMarkdown.test.ts +0 -15
- package/src/__tests__/documentSettingsDialog.test.tsx +147 -0
- package/src/__tests__/emojiPicker.test.tsx +133 -0
- package/src/__tests__/fileKind.test.ts +16 -0
- package/src/__tests__/imageEditAffordance.test.tsx +268 -0
- package/src/__tests__/imageEditorShell.test.tsx +57 -0
- package/src/__tests__/imageEditorState.test.ts +171 -0
- package/src/__tests__/inlinePreviewGutter.test.tsx +62 -0
- package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +103 -0
- package/src/__tests__/jsonEditor.test.tsx +168 -0
- package/src/__tests__/layersPanel.test.tsx +97 -0
- package/src/__tests__/linkDialogDocPicker.test.tsx +137 -0
- package/src/__tests__/outlinePanel.test.tsx +79 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +107 -0
- package/src/__tests__/propertiesPanel.test.tsx +69 -0
- package/src/__tests__/recorderFormats.test.ts +146 -0
- package/src/__tests__/recorderTimingJson.test.ts +41 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +34 -0
- package/src/__tests__/tiptapBridge.test.ts +15 -0
- package/src/__tests__/useImageEditor.test.tsx +159 -0
- package/src/__tests__/useMediaRecorder.test.ts +186 -0
- package/src/__tests__/versionHistory.test.tsx +197 -0
- package/src/blockSlice.ts +75 -0
- package/src/buildPreviewDoc.ts +61 -6
- package/src/emojiData.ts +1337 -0
- package/src/fileKind.ts +30 -6
- package/src/hooks/useFileDrop.ts +40 -4
- package/src/imageEditor/CanvasSurface.tsx +402 -0
- package/src/imageEditor/ImageVersionHistoryDropdown.tsx +396 -0
- package/src/imageEditor/LayersPanel.tsx +143 -0
- package/src/imageEditor/PropertiesPanel.tsx +428 -0
- package/src/imageEditor/Toolbar.tsx +242 -0
- package/src/imageEditor/icons.tsx +144 -0
- package/src/imageEditor/image-editor.css +450 -0
- package/src/imageEditor/layers/EditorImageLayer.tsx +45 -0
- package/src/imageEditor/layers/EditorShapeLayer.tsx +62 -0
- package/src/imageEditor/layers/EditorTextLayer.tsx +45 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +86 -0
- package/src/imageEditor/state.ts +153 -0
- package/src/imageEditor/useImageEditor.ts +328 -0
- package/src/imageEditor/useImageEditorTokens.ts +70 -0
- package/src/index.ts +82 -0
- package/src/jsonEditor/EmbeddedRichTextField.tsx +81 -0
- package/src/jsonEditor/JsonEditor.tsx +81 -0
- package/src/jsonEditor/JsonEditorContext.tsx +75 -0
- package/src/jsonEditor/RenderNode.tsx +66 -0
- package/src/jsonEditor/editors.tsx +678 -0
- package/src/jsonEditor/index.ts +2 -0
- package/src/jsonEditor/json-editor.css +463 -0
- package/src/jsonEditor/useJsonEditorTokens.ts +63 -0
- package/src/recorder/RecorderButton.tsx +72 -0
- package/src/recorder/RecorderModal.tsx +596 -0
- package/src/recorder/RecorderPanel.tsx +93 -0
- package/src/recorder/formats.ts +159 -0
- package/src/recorder/hooks/useMediaRecorder.ts +378 -0
- package/src/recorder/hooks/useStreamPreview.ts +47 -0
- package/src/recorder/sources/cameraStream.ts +32 -0
- package/src/recorder/sources/micStream.ts +25 -0
- package/src/recorder/sources/screenStream.ts +162 -0
- package/src/recorder/timingJson.ts +66 -0
- package/src/styles/editor.css +2490 -51
- package/src/styles/image-edit-affordance.css +201 -0
- package/src/styles/index.css +10 -0
- package/src/tiptap/TiptapAudio.tsx +86 -0
- package/src/tiptap/TiptapVideo.tsx +119 -0
- package/src/tiptap/useResolvedMediaSrc.ts +47 -0
- package/src/tiptapBridge.ts +188 -20
- package/src/useHeadingLayout.ts +294 -0
- package/src/utils/collectInlineFontAwesomeCss.ts +69 -0
- package/src/utils/dropUtils.ts +54 -6
- package/src/utils/normalizeMalformedAssetUrl.ts +22 -0
|
@@ -0,0 +1,596 @@
|
|
|
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` (cream / gold palette, inline styles, no external CSS).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';
|
|
19
|
+
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
20
|
+
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
21
|
+
import { useMediaRecorder, type RecorderSource } from './hooks/useMediaRecorder.js';
|
|
22
|
+
import { useStreamPreview } from './hooks/useStreamPreview.js';
|
|
23
|
+
import { buildFilename } from './formats.js';
|
|
24
|
+
import { buildTimingJson, encodeTimingJson, timingPathFor } from './timingJson.js';
|
|
25
|
+
|
|
26
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface RecorderModalProps {
|
|
29
|
+
/** Required — recordings are written here. */
|
|
30
|
+
mediaProvider: MediaProvider;
|
|
31
|
+
/**
|
|
32
|
+
* Optional — when provided, narration-mode recordings drop a
|
|
33
|
+
* `.timing.json` sidecar at the matching container path so
|
|
34
|
+
* `resolveAudioMapping()` can auto-link them. Without it, only the
|
|
35
|
+
* raw recording is saved.
|
|
36
|
+
*/
|
|
37
|
+
container?: ContentContainer | null;
|
|
38
|
+
/** Initial capture source. Defaults to `'mic'` (narration). */
|
|
39
|
+
initialMode?: RecorderSource;
|
|
40
|
+
/** Called after the modal is dismissed (save or cancel). */
|
|
41
|
+
onClose: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Fired after a successful save. Hosts typically use this to insert a
|
|
44
|
+
* markdown reference at the cursor — see {@link RecorderSaveResult}
|
|
45
|
+
* for the fields a host needs to build that reference.
|
|
46
|
+
*/
|
|
47
|
+
onSave?: (result: RecorderSaveResult) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Payload handed to {@link RecorderModalProps.onSave} on a successful save. */
|
|
51
|
+
export interface RecorderSaveResult {
|
|
52
|
+
/** Path returned by `mediaProvider.addMedia()` — what the doc should reference. */
|
|
53
|
+
relativePath: string;
|
|
54
|
+
/** Filename the modal chose (e.g. `narration-20260516-091200.webm`). */
|
|
55
|
+
filename: string;
|
|
56
|
+
/** Capture source the user picked. */
|
|
57
|
+
source: RecorderSource;
|
|
58
|
+
/** MIME type of the saved blob. */
|
|
59
|
+
mimeType: string;
|
|
60
|
+
/** Recording length in seconds. */
|
|
61
|
+
duration: number;
|
|
62
|
+
/** Whether a narration sidecar was written. Always `false` for video sources. */
|
|
63
|
+
hasTimingSidecar: boolean;
|
|
64
|
+
/** Script text the user typed (narration only). */
|
|
65
|
+
sourceText?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Styles ─────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
const overlayStyle: CSSProperties = {
|
|
71
|
+
position: 'fixed',
|
|
72
|
+
inset: 0,
|
|
73
|
+
background: 'rgba(0, 0, 0, 0.5)',
|
|
74
|
+
display: 'flex',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
justifyContent: 'center',
|
|
77
|
+
zIndex: 10000,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const modalStyle: CSSProperties = {
|
|
81
|
+
background: '#FFFDF7',
|
|
82
|
+
border: '1px solid #c9b98a',
|
|
83
|
+
borderRadius: 0,
|
|
84
|
+
padding: '24px 28px',
|
|
85
|
+
width: 'min(560px, calc(100vw - 48px))',
|
|
86
|
+
maxHeight: 'calc(100vh - 48px)',
|
|
87
|
+
overflowY: 'auto',
|
|
88
|
+
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
|
|
89
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
90
|
+
color: '#4a3c1f',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const titleStyle: CSSProperties = {
|
|
94
|
+
margin: '0 0 16px 0',
|
|
95
|
+
fontSize: 18,
|
|
96
|
+
fontWeight: 600,
|
|
97
|
+
color: '#2d2310',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const labelStyle: CSSProperties = {
|
|
101
|
+
display: 'block',
|
|
102
|
+
fontSize: 13,
|
|
103
|
+
fontWeight: 500,
|
|
104
|
+
marginBottom: 4,
|
|
105
|
+
color: '#5a4a2a',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const inputStyle: CSSProperties = {
|
|
109
|
+
width: '100%',
|
|
110
|
+
padding: '6px 8px',
|
|
111
|
+
fontSize: 13,
|
|
112
|
+
fontFamily: 'inherit',
|
|
113
|
+
border: '1px solid #c9b98a',
|
|
114
|
+
borderRadius: 0,
|
|
115
|
+
background: '#fff',
|
|
116
|
+
color: '#4a3c1f',
|
|
117
|
+
marginBottom: 12,
|
|
118
|
+
boxSizing: 'border-box',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const textareaStyle: CSSProperties = {
|
|
122
|
+
...inputStyle,
|
|
123
|
+
resize: 'vertical',
|
|
124
|
+
minHeight: 72,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const btnPrimary: CSSProperties = {
|
|
128
|
+
padding: '8px 20px',
|
|
129
|
+
fontSize: 14,
|
|
130
|
+
fontFamily: 'inherit',
|
|
131
|
+
fontWeight: 500,
|
|
132
|
+
cursor: 'pointer',
|
|
133
|
+
background: '#8B6914',
|
|
134
|
+
color: '#fff',
|
|
135
|
+
border: '1px solid #7a5c10',
|
|
136
|
+
borderRadius: 0,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const btnSecondary: CSSProperties = {
|
|
140
|
+
padding: '8px 20px',
|
|
141
|
+
fontSize: 14,
|
|
142
|
+
fontFamily: 'inherit',
|
|
143
|
+
fontWeight: 500,
|
|
144
|
+
cursor: 'pointer',
|
|
145
|
+
background: '#E8DFC6',
|
|
146
|
+
color: '#4a3c1f',
|
|
147
|
+
border: '1px solid #c9b98a',
|
|
148
|
+
borderRadius: 0,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const btnDanger: CSSProperties = {
|
|
152
|
+
...btnPrimary,
|
|
153
|
+
background: '#B33A3A',
|
|
154
|
+
borderColor: '#902929',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const tabRowStyle: CSSProperties = {
|
|
158
|
+
display: 'flex',
|
|
159
|
+
gap: 4,
|
|
160
|
+
marginBottom: 16,
|
|
161
|
+
borderBottom: '1px solid #c9b98a',
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const tabBase: CSSProperties = {
|
|
165
|
+
padding: '6px 12px',
|
|
166
|
+
fontSize: 13,
|
|
167
|
+
fontFamily: 'inherit',
|
|
168
|
+
cursor: 'pointer',
|
|
169
|
+
background: 'transparent',
|
|
170
|
+
color: '#5a4a2a',
|
|
171
|
+
border: 'none',
|
|
172
|
+
borderBottom: '2px solid transparent',
|
|
173
|
+
marginBottom: -1,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const tabActive: CSSProperties = {
|
|
177
|
+
...tabBase,
|
|
178
|
+
color: '#2d2310',
|
|
179
|
+
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',
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const previewBoxStyle: CSSProperties = {
|
|
188
|
+
width: '100%',
|
|
189
|
+
background: '#000',
|
|
190
|
+
borderRadius: 0,
|
|
191
|
+
marginBottom: 12,
|
|
192
|
+
overflow: 'hidden',
|
|
193
|
+
aspectRatio: '16 / 9',
|
|
194
|
+
display: 'flex',
|
|
195
|
+
alignItems: 'center',
|
|
196
|
+
justifyContent: 'center',
|
|
197
|
+
color: '#888',
|
|
198
|
+
fontSize: 13,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const audioMeterStyle: CSSProperties = {
|
|
202
|
+
width: '100%',
|
|
203
|
+
height: 56,
|
|
204
|
+
background: '#F2EBD9',
|
|
205
|
+
border: '1px solid #c9b98a',
|
|
206
|
+
marginBottom: 12,
|
|
207
|
+
display: 'flex',
|
|
208
|
+
alignItems: 'center',
|
|
209
|
+
justifyContent: 'center',
|
|
210
|
+
color: '#5a4a2a',
|
|
211
|
+
fontSize: 13,
|
|
212
|
+
fontVariantNumeric: 'tabular-nums',
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const errorStyle: CSSProperties = {
|
|
216
|
+
background: '#FCEEEE',
|
|
217
|
+
border: '1px solid #D88A8A',
|
|
218
|
+
color: '#8C2A2A',
|
|
219
|
+
padding: '8px 10px',
|
|
220
|
+
fontSize: 13,
|
|
221
|
+
marginBottom: 12,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const buttonRowStyle: CSSProperties = {
|
|
225
|
+
display: 'flex',
|
|
226
|
+
gap: 8,
|
|
227
|
+
justifyContent: 'flex-end',
|
|
228
|
+
marginTop: 8,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
function formatDurationMs(ms: number): string {
|
|
234
|
+
const totalSec = Math.floor(ms / 1000);
|
|
235
|
+
const m = Math.floor(totalSec / 60);
|
|
236
|
+
const s = totalSec % 60;
|
|
237
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
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.' },
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
// ── Component ──────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
export function RecorderModal({
|
|
254
|
+
mediaProvider,
|
|
255
|
+
container = null,
|
|
256
|
+
initialMode = 'mic',
|
|
257
|
+
onClose,
|
|
258
|
+
onSave,
|
|
259
|
+
}: RecorderModalProps) {
|
|
260
|
+
const [source, setSource] = useState<RecorderSource>(initialMode);
|
|
261
|
+
const [sourceText, setSourceText] = useState('');
|
|
262
|
+
const [basename, setBasename] = useState('');
|
|
263
|
+
const [includeSystemAudio, setIncludeSystemAudio] = useState(false);
|
|
264
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
265
|
+
const [saveError, setSaveError] = useState<string | null>(null);
|
|
266
|
+
const [playbackUrl, setPlaybackUrl] = useState<string | null>(null);
|
|
267
|
+
|
|
268
|
+
const previewRef = useRef<HTMLVideoElement | null>(null);
|
|
269
|
+
|
|
270
|
+
const recorder = useMediaRecorder({
|
|
271
|
+
source,
|
|
272
|
+
systemAudio: source === 'screen' || source === 'screen+mic' ? includeSystemAudio : false,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
useStreamPreview(previewRef, recorder.state === 'stopped' ? null : recorder.stream);
|
|
276
|
+
|
|
277
|
+
// Generate (and later revoke) a blob URL for the recorded clip so the
|
|
278
|
+
// playback element has something to point at. The dependency on the
|
|
279
|
+
// blob identity means a new URL is created every time a fresh
|
|
280
|
+
// recording lands, and the cleanup callback revokes the previous one.
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (!recorder.blob) {
|
|
283
|
+
setPlaybackUrl(null);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const url = URL.createObjectURL(recorder.blob);
|
|
287
|
+
setPlaybackUrl(url);
|
|
288
|
+
return () => {
|
|
289
|
+
URL.revokeObjectURL(url);
|
|
290
|
+
};
|
|
291
|
+
}, [recorder.blob]);
|
|
292
|
+
|
|
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);
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (previousSourceRef.current !== source) {
|
|
300
|
+
previousSourceRef.current = source;
|
|
301
|
+
recorder.cancel();
|
|
302
|
+
}
|
|
303
|
+
}, [source, recorder]);
|
|
304
|
+
|
|
305
|
+
// Make sure tearing down the modal always releases the camera /
|
|
306
|
+
// screen-capture indicator. The hook's own unmount effect handles
|
|
307
|
+
// this, but we also kill the stream eagerly on close so a slow
|
|
308
|
+
// unmount doesn't leave the indicator lit between renders.
|
|
309
|
+
const handleClose = useCallback(() => {
|
|
310
|
+
recorder.cancel();
|
|
311
|
+
onClose();
|
|
312
|
+
}, [recorder, onClose]);
|
|
313
|
+
|
|
314
|
+
const handleRequest = useCallback(async () => {
|
|
315
|
+
setSaveError(null);
|
|
316
|
+
try {
|
|
317
|
+
await recorder.request();
|
|
318
|
+
} catch {
|
|
319
|
+
// Already surfaced via recorder.error.
|
|
320
|
+
}
|
|
321
|
+
}, [recorder]);
|
|
322
|
+
|
|
323
|
+
const handleStart = useCallback(() => {
|
|
324
|
+
setSaveError(null);
|
|
325
|
+
recorder.start();
|
|
326
|
+
}, [recorder]);
|
|
327
|
+
|
|
328
|
+
const handleStop = useCallback(async () => {
|
|
329
|
+
setSaveError(null);
|
|
330
|
+
await recorder.stop();
|
|
331
|
+
}, [recorder]);
|
|
332
|
+
|
|
333
|
+
const handleSave = useCallback(async () => {
|
|
334
|
+
if (!recorder.blob || !recorder.mimeType || !recorder.extension || !recorder.directory) {
|
|
335
|
+
setSaveError('Nothing to save yet — record something first.');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
setIsSaving(true);
|
|
339
|
+
setSaveError(null);
|
|
340
|
+
try {
|
|
341
|
+
const filename = buildFilename(
|
|
342
|
+
source === 'mic' ? 'audio' : 'video',
|
|
343
|
+
recorder.extension,
|
|
344
|
+
basename,
|
|
345
|
+
);
|
|
346
|
+
const relativeName = `${recorder.directory}/${filename}`;
|
|
347
|
+
const relativePath = await mediaProvider.addMedia(
|
|
348
|
+
relativeName,
|
|
349
|
+
recorder.blob,
|
|
350
|
+
recorder.mimeType,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
let hasTimingSidecar = false;
|
|
354
|
+
if (source === 'mic') {
|
|
355
|
+
const timing = buildTimingJson(sourceText, recorder.durationMs / 1000);
|
|
356
|
+
const encoded = encodeTimingJson(timing);
|
|
357
|
+
const sidecarPath = timingPathFor(relativePath);
|
|
358
|
+
// Prefer direct container write so the sidecar lands at the
|
|
359
|
+
// exact path the audio-mapping pipeline expects. Fall back to
|
|
360
|
+
// addMedia(), which may rename — log if so.
|
|
361
|
+
if (container) {
|
|
362
|
+
await container.writeFile(sidecarPath, encoded, 'application/json');
|
|
363
|
+
hasTimingSidecar = true;
|
|
364
|
+
} else {
|
|
365
|
+
const written = await mediaProvider.addMedia(sidecarPath, encoded, 'application/json');
|
|
366
|
+
hasTimingSidecar = written === sidecarPath;
|
|
367
|
+
if (!hasTimingSidecar) {
|
|
368
|
+
console.warn(
|
|
369
|
+
`[squisq-recorder] timing.json was saved as "${written}" instead of "${sidecarPath}" — auto-mapping may not pick it up.`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const result: RecorderSaveResult = {
|
|
376
|
+
relativePath,
|
|
377
|
+
filename,
|
|
378
|
+
source,
|
|
379
|
+
mimeType: recorder.mimeType,
|
|
380
|
+
duration: recorder.durationMs / 1000,
|
|
381
|
+
hasTimingSidecar,
|
|
382
|
+
};
|
|
383
|
+
if (source === 'mic') {
|
|
384
|
+
result.sourceText = sourceText;
|
|
385
|
+
}
|
|
386
|
+
onSave?.(result);
|
|
387
|
+
handleClose();
|
|
388
|
+
} catch (err: unknown) {
|
|
389
|
+
setSaveError(err instanceof Error ? err.message : 'Failed to save recording');
|
|
390
|
+
} finally {
|
|
391
|
+
setIsSaving(false);
|
|
392
|
+
}
|
|
393
|
+
}, [recorder, source, basename, sourceText, mediaProvider, container, onSave, handleClose]);
|
|
394
|
+
|
|
395
|
+
const handleDiscard = useCallback(() => {
|
|
396
|
+
recorder.reset();
|
|
397
|
+
}, [recorder]);
|
|
398
|
+
|
|
399
|
+
const isAudioOnly = source === 'mic';
|
|
400
|
+
const showPreview = recorder.state !== 'idle' && recorder.state !== 'error';
|
|
401
|
+
const canRecord = recorder.state === 'ready';
|
|
402
|
+
const canStop = recorder.state === 'recording';
|
|
403
|
+
const canSave = recorder.state === 'stopped' && recorder.blob !== null;
|
|
404
|
+
const isBusy = recorder.state === 'requesting' || recorder.state === 'stopping' || isSaving;
|
|
405
|
+
|
|
406
|
+
const activeTabDescription = TABS.find((t) => t.id === source)?.description;
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<div style={overlayStyle} role="dialog" aria-modal="true" aria-label="Record media">
|
|
410
|
+
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
|
|
411
|
+
<h2 style={titleStyle}>Record media</h2>
|
|
412
|
+
|
|
413
|
+
<div style={tabRowStyle} role="tablist">
|
|
414
|
+
{TABS.map((tab) => {
|
|
415
|
+
const active = tab.id === source;
|
|
416
|
+
return (
|
|
417
|
+
<button
|
|
418
|
+
key={tab.id}
|
|
419
|
+
role="tab"
|
|
420
|
+
aria-selected={active}
|
|
421
|
+
type="button"
|
|
422
|
+
style={active ? tabActive : tabBase}
|
|
423
|
+
onClick={() => setSource(tab.id)}
|
|
424
|
+
disabled={recorder.state === 'recording' || recorder.state === 'requesting'}
|
|
425
|
+
>
|
|
426
|
+
{tab.label}
|
|
427
|
+
</button>
|
|
428
|
+
);
|
|
429
|
+
})}
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
{activeTabDescription && (
|
|
433
|
+
<p style={{ margin: '0 0 12px 0', fontSize: 12, color: '#5a4a2a' }}>
|
|
434
|
+
{activeTabDescription}
|
|
435
|
+
</p>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{recorder.error && <div style={errorStyle}>{recorder.error.message}</div>}
|
|
439
|
+
{saveError && <div style={errorStyle}>{saveError}</div>}
|
|
440
|
+
|
|
441
|
+
{/* Preview surface. Three modes:
|
|
442
|
+
- Pre-acquisition (idle / error): a static prompt.
|
|
443
|
+
- Live (ready / recording / requesting / stopping): the stream
|
|
444
|
+
piped into a muted <video>, or a recording meter for mic.
|
|
445
|
+
- Playback (stopped): the captured blob bound to a <video>/<audio>
|
|
446
|
+
with native controls so the user can audition before saving.
|
|
447
|
+
*/}
|
|
448
|
+
{!showPreview && (
|
|
449
|
+
<div style={previewBoxStyle}>
|
|
450
|
+
<span>Click Start Preview to start a recording.</span>
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
{showPreview && recorder.state !== 'stopped' && !isAudioOnly && (
|
|
454
|
+
<div style={previewBoxStyle}>
|
|
455
|
+
<video
|
|
456
|
+
ref={previewRef}
|
|
457
|
+
autoPlay
|
|
458
|
+
muted
|
|
459
|
+
playsInline
|
|
460
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
461
|
+
/>
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
{showPreview && recorder.state !== 'stopped' && isAudioOnly && (
|
|
465
|
+
<div style={audioMeterStyle}>
|
|
466
|
+
{recorder.state === 'recording' ? (
|
|
467
|
+
<>● Recording {formatDurationMs(recorder.durationMs)}</>
|
|
468
|
+
) : (
|
|
469
|
+
<>Microphone ready</>
|
|
470
|
+
)}
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
{recorder.state === 'stopped' && playbackUrl && !isAudioOnly && (
|
|
474
|
+
<div style={previewBoxStyle}>
|
|
475
|
+
<video
|
|
476
|
+
src={playbackUrl}
|
|
477
|
+
controls
|
|
478
|
+
playsInline
|
|
479
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
480
|
+
/>
|
|
481
|
+
</div>
|
|
482
|
+
)}
|
|
483
|
+
{recorder.state === 'stopped' && playbackUrl && isAudioOnly && (
|
|
484
|
+
<div style={{ marginBottom: 12 }}>
|
|
485
|
+
<div style={{ ...audioMeterStyle, marginBottom: 8 }}>
|
|
486
|
+
✓ Recorded {formatDurationMs(recorder.durationMs)}
|
|
487
|
+
</div>
|
|
488
|
+
<audio src={playbackUrl} controls style={{ width: '100%' }} />
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
|
|
492
|
+
{/* Mode-specific fields */}
|
|
493
|
+
{source === 'mic' && (
|
|
494
|
+
<>
|
|
495
|
+
<label style={labelStyle} htmlFor="recorder-source-text">
|
|
496
|
+
Script (used to auto-match this narration to a block)
|
|
497
|
+
</label>
|
|
498
|
+
<textarea
|
|
499
|
+
id="recorder-source-text"
|
|
500
|
+
style={textareaStyle}
|
|
501
|
+
placeholder="Type the text you're going to read aloud."
|
|
502
|
+
value={sourceText}
|
|
503
|
+
onChange={(e) => setSourceText(e.target.value)}
|
|
504
|
+
disabled={recorder.state === 'recording'}
|
|
505
|
+
/>
|
|
506
|
+
</>
|
|
507
|
+
)}
|
|
508
|
+
{(source === 'screen' || source === 'screen+mic') && (
|
|
509
|
+
<label
|
|
510
|
+
style={{
|
|
511
|
+
display: 'flex',
|
|
512
|
+
alignItems: 'center',
|
|
513
|
+
gap: 6,
|
|
514
|
+
marginBottom: 12,
|
|
515
|
+
fontSize: 13,
|
|
516
|
+
}}
|
|
517
|
+
>
|
|
518
|
+
<input
|
|
519
|
+
type="checkbox"
|
|
520
|
+
checked={includeSystemAudio}
|
|
521
|
+
onChange={(e) => setIncludeSystemAudio(e.target.checked)}
|
|
522
|
+
disabled={recorder.state === 'recording' || recorder.state === 'requesting'}
|
|
523
|
+
/>
|
|
524
|
+
Include system audio (Chrome only)
|
|
525
|
+
</label>
|
|
526
|
+
)}
|
|
527
|
+
|
|
528
|
+
<label style={labelStyle} htmlFor="recorder-basename">
|
|
529
|
+
Filename (optional)
|
|
530
|
+
</label>
|
|
531
|
+
<input
|
|
532
|
+
id="recorder-basename"
|
|
533
|
+
type="text"
|
|
534
|
+
style={inputStyle}
|
|
535
|
+
placeholder={source === 'mic' ? 'narration' : 'recording'}
|
|
536
|
+
value={basename}
|
|
537
|
+
onChange={(e) => setBasename(e.target.value)}
|
|
538
|
+
disabled={recorder.state === 'recording'}
|
|
539
|
+
/>
|
|
540
|
+
|
|
541
|
+
{/* Live duration during recording */}
|
|
542
|
+
{recorder.state === 'recording' && !isAudioOnly && (
|
|
543
|
+
<div
|
|
544
|
+
style={{
|
|
545
|
+
fontSize: 13,
|
|
546
|
+
fontVariantNumeric: 'tabular-nums',
|
|
547
|
+
marginBottom: 12,
|
|
548
|
+
color: '#8B6914',
|
|
549
|
+
fontWeight: 600,
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
● Recording {formatDurationMs(recorder.durationMs)}
|
|
553
|
+
</div>
|
|
554
|
+
)}
|
|
555
|
+
|
|
556
|
+
{/* Action buttons. Layout depends on state. */}
|
|
557
|
+
<div style={buttonRowStyle}>
|
|
558
|
+
<button type="button" style={btnSecondary} onClick={handleClose} disabled={isBusy}>
|
|
559
|
+
Close
|
|
560
|
+
</button>
|
|
561
|
+
|
|
562
|
+
{(recorder.state === 'idle' ||
|
|
563
|
+
recorder.state === 'error' ||
|
|
564
|
+
recorder.state === 'requesting') && (
|
|
565
|
+
<button type="button" style={btnPrimary} onClick={handleRequest} disabled={isBusy}>
|
|
566
|
+
{recorder.state === 'requesting' ? 'Requesting…' : 'Start preview'}
|
|
567
|
+
</button>
|
|
568
|
+
)}
|
|
569
|
+
|
|
570
|
+
{canRecord && (
|
|
571
|
+
<button type="button" style={btnPrimary} onClick={handleStart} disabled={isBusy}>
|
|
572
|
+
Record
|
|
573
|
+
</button>
|
|
574
|
+
)}
|
|
575
|
+
|
|
576
|
+
{canStop && (
|
|
577
|
+
<button type="button" style={btnDanger} onClick={handleStop} disabled={isBusy}>
|
|
578
|
+
Stop
|
|
579
|
+
</button>
|
|
580
|
+
)}
|
|
581
|
+
|
|
582
|
+
{canSave && (
|
|
583
|
+
<>
|
|
584
|
+
<button type="button" style={btnSecondary} onClick={handleDiscard} disabled={isBusy}>
|
|
585
|
+
Discard & re-record
|
|
586
|
+
</button>
|
|
587
|
+
<button type="button" style={btnPrimary} onClick={handleSave} disabled={isBusy}>
|
|
588
|
+
{isSaving ? 'Saving…' : 'Save to document'}
|
|
589
|
+
</button>
|
|
590
|
+
</>
|
|
591
|
+
)}
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecorderPanel — toolbar-anchored trigger that opens the
|
|
3
|
+
* {@link RecorderModal} in a portal. Shaped to slot into an editor
|
|
4
|
+
* toolbar alongside other panels (e.g. `VersionHistoryPanel`); ships a
|
|
5
|
+
* compact mic/record icon and no label by default.
|
|
6
|
+
*
|
|
7
|
+
* For a button that owns its own visual label, use {@link RecorderButton}
|
|
8
|
+
* instead.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useState } from 'react';
|
|
12
|
+
import { createPortal } from 'react-dom';
|
|
13
|
+
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
14
|
+
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
15
|
+
import { RecorderModal, type RecorderSaveResult } from './RecorderModal.js';
|
|
16
|
+
import type { RecorderSource } from './hooks/useMediaRecorder.js';
|
|
17
|
+
|
|
18
|
+
export interface RecorderPanelProps {
|
|
19
|
+
mediaProvider: MediaProvider;
|
|
20
|
+
container?: ContentContainer | null;
|
|
21
|
+
initialMode?: RecorderSource;
|
|
22
|
+
onSave?: (result: RecorderSaveResult) => void;
|
|
23
|
+
/** ARIA / tooltip label. Defaults to `'Record media'`. */
|
|
24
|
+
tooltip?: string;
|
|
25
|
+
/** Optional className for the trigger button. */
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
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
|
+
export function RecorderPanel({
|
|
55
|
+
mediaProvider,
|
|
56
|
+
container = null,
|
|
57
|
+
initialMode = 'mic',
|
|
58
|
+
onSave,
|
|
59
|
+
tooltip = 'Record media',
|
|
60
|
+
className,
|
|
61
|
+
}: RecorderPanelProps) {
|
|
62
|
+
const [open, setOpen] = useState(false);
|
|
63
|
+
const handleClose = useCallback(() => setOpen(false), []);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
className={className}
|
|
70
|
+
data-tooltip={tooltip}
|
|
71
|
+
aria-label={tooltip}
|
|
72
|
+
aria-expanded={open}
|
|
73
|
+
onClick={() => setOpen((v) => !v)}
|
|
74
|
+
>
|
|
75
|
+
<MicIcon />
|
|
76
|
+
</button>
|
|
77
|
+
{open &&
|
|
78
|
+
typeof document !== 'undefined' &&
|
|
79
|
+
createPortal(
|
|
80
|
+
<RecorderModal
|
|
81
|
+
mediaProvider={mediaProvider}
|
|
82
|
+
container={container}
|
|
83
|
+
initialMode={initialMode}
|
|
84
|
+
onClose={handleClose}
|
|
85
|
+
onSave={(result) => {
|
|
86
|
+
onSave?.(result);
|
|
87
|
+
}}
|
|
88
|
+
/>,
|
|
89
|
+
document.body,
|
|
90
|
+
)}
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
}
|