@bendyline/squisq-editor-react 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +184 -4
- package/dist/EditorShell.d.ts.map +1 -1
- package/dist/EditorShell.js +184 -12
- 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/MediaBin.d.ts +12 -1
- package/dist/MediaBin.d.ts.map +1 -1
- package/dist/MediaBin.js +13 -3
- package/dist/MediaBin.js.map +1 -1
- 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__/mediaAttachmentFlow.test.d.ts +2 -0
- package/dist/__tests__/mediaAttachmentFlow.test.d.ts.map +1 -0
- package/dist/__tests__/mediaAttachmentFlow.test.js +99 -0
- package/dist/__tests__/mediaAttachmentFlow.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 +26 -0
- package/dist/__tests__/tiptapBridge.test.js.map +1 -1
- package/dist/__tests__/tiptapImageRoundTrip.test.d.ts +2 -0
- package/dist/__tests__/tiptapImageRoundTrip.test.d.ts.map +1 -0
- package/dist/__tests__/tiptapImageRoundTrip.test.js +68 -0
- package/dist/__tests__/tiptapImageRoundTrip.test.js.map +1 -0
- 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 +210 -16
- 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 +691 -63
- 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/MediaBin.tsx +22 -3
- 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__/mediaAttachmentFlow.test.ts +110 -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 +29 -0
- package/src/__tests__/tiptapImageRoundTrip.test.ts +73 -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 +227 -22
- 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,107 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
4
|
+
import { resolveTheme } from '@bendyline/squisq/schemas';
|
|
5
|
+
import { PlainHtmlPreview } from '../PlainHtmlPreview';
|
|
6
|
+
|
|
7
|
+
function getIframeSrcDoc(): string {
|
|
8
|
+
const iframe = screen.getByTestId('plain-html-preview') as HTMLIFrameElement;
|
|
9
|
+
return iframe.srcdoc;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('PlainHtmlPreview', () => {
|
|
13
|
+
it('renders the markdown into the iframe srcDoc', () => {
|
|
14
|
+
render(<PlainHtmlPreview markdown={'# Hello\n\nWorld'} />);
|
|
15
|
+
const srcdoc = getIframeSrcDoc();
|
|
16
|
+
expect(srcdoc).toContain('<!DOCTYPE html>');
|
|
17
|
+
expect(srcdoc).toContain('<h1>Hello</h1>');
|
|
18
|
+
expect(srcdoc).toContain('<p>World</p>');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('refuses scripts via sandbox attribute (allow-same-origin only)', () => {
|
|
22
|
+
render(<PlainHtmlPreview markdown={'hi'} />);
|
|
23
|
+
const iframe = screen.getByTestId('plain-html-preview') as HTMLIFrameElement;
|
|
24
|
+
const sandbox = iframe.getAttribute('sandbox') ?? '';
|
|
25
|
+
expect(sandbox).toContain('allow-same-origin');
|
|
26
|
+
expect(sandbox).not.toContain('allow-scripts');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('applies pre-resolved images via the images prop', () => {
|
|
30
|
+
render(
|
|
31
|
+
<PlainHtmlPreview
|
|
32
|
+
markdown={''}
|
|
33
|
+
images={new Map([['a.jpg', 'data:image/png;base64,AAA']])}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
expect(getIframeSrcDoc()).toContain('src="data:image/png;base64,AAA"');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('resolves image URLs through the supplied mediaProvider', async () => {
|
|
40
|
+
const provider: MediaProvider = {
|
|
41
|
+
resolveUrl: vi.fn(async (ref: string) => `blob:resolved/${ref}`),
|
|
42
|
+
listMedia: vi.fn(async () => []),
|
|
43
|
+
addMedia: vi.fn(async () => ''),
|
|
44
|
+
removeMedia: vi.fn(async () => {}),
|
|
45
|
+
dispose: vi.fn(() => {}),
|
|
46
|
+
};
|
|
47
|
+
render(<PlainHtmlPreview markdown={''} mediaProvider={provider} />);
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
expect(getIframeSrcDoc()).toContain('src="blob:resolved/cat.jpg"');
|
|
50
|
+
});
|
|
51
|
+
expect(provider.resolveUrl).toHaveBeenCalledWith('cat.jpg');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not resolve external URLs through the provider', async () => {
|
|
55
|
+
const provider: MediaProvider = {
|
|
56
|
+
resolveUrl: vi.fn(async (ref: string) => `blob:should-not-fire/${ref}`),
|
|
57
|
+
listMedia: vi.fn(async () => []),
|
|
58
|
+
addMedia: vi.fn(async () => ''),
|
|
59
|
+
removeMedia: vi.fn(async () => {}),
|
|
60
|
+
dispose: vi.fn(() => {}),
|
|
61
|
+
};
|
|
62
|
+
render(
|
|
63
|
+
<PlainHtmlPreview markdown={''} mediaProvider={provider} />,
|
|
64
|
+
);
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
expect(getIframeSrcDoc()).toContain('src="https://example.com/x.jpg"');
|
|
67
|
+
});
|
|
68
|
+
expect(provider.resolveUrl).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('applies the supplied theme to the iframe document', () => {
|
|
72
|
+
const theme = resolveTheme('warm-earth');
|
|
73
|
+
render(<PlainHtmlPreview markdown={'# Hi'} theme={theme} />);
|
|
74
|
+
const srcdoc = getIframeSrcDoc();
|
|
75
|
+
expect(srcdoc).toContain(`--plain-bg: ${theme.colors.background};`);
|
|
76
|
+
expect(srcdoc).toContain(`--plain-primary: ${theme.colors.primary};`);
|
|
77
|
+
expect(srcdoc).toContain('--plain-body-font:');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('loads Google Fonts for themes that reference google-hosted faces', () => {
|
|
81
|
+
const theme = resolveTheme('documentary');
|
|
82
|
+
render(<PlainHtmlPreview markdown={'# Hi'} theme={theme} />);
|
|
83
|
+
expect(getIframeSrcDoc()).toContain('https://fonts.googleapis.com/css2?');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('resolves raw HTML <img> tags too (resized image case)', async () => {
|
|
87
|
+
// Resized images round-trip through the markdown source as raw
|
|
88
|
+
// HTML, which the preview must scan to keep the iframe's <img>
|
|
89
|
+
// pointing at a fetchable blob URL.
|
|
90
|
+
const provider: MediaProvider = {
|
|
91
|
+
resolveUrl: vi.fn(async (ref: string) => `blob:r/${ref}`),
|
|
92
|
+
listMedia: vi.fn(async () => []),
|
|
93
|
+
addMedia: vi.fn(async () => ''),
|
|
94
|
+
removeMedia: vi.fn(async () => {}),
|
|
95
|
+
dispose: vi.fn(() => {}),
|
|
96
|
+
};
|
|
97
|
+
render(
|
|
98
|
+
<PlainHtmlPreview
|
|
99
|
+
markdown={'<img alt="resized" src="resized.png" width="194">'}
|
|
100
|
+
mediaProvider={provider}
|
|
101
|
+
/>,
|
|
102
|
+
);
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(getIframeSrcDoc()).toContain('src="blob:r/resized.png"');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
6
|
+
import type { ImageEditDoc } from '@bendyline/squisq/schemas';
|
|
7
|
+
import { PropertiesPanel } from '../imageEditor/PropertiesPanel.js';
|
|
8
|
+
import type { ImageEditorAction } from '../imageEditor/state.js';
|
|
9
|
+
|
|
10
|
+
function buildDoc(): ImageEditDoc {
|
|
11
|
+
return {
|
|
12
|
+
version: 1,
|
|
13
|
+
canvas: { width: 100, height: 80, background: '#ffffff' },
|
|
14
|
+
layers: [
|
|
15
|
+
{
|
|
16
|
+
id: 'a',
|
|
17
|
+
type: 'shape',
|
|
18
|
+
name: 'Box',
|
|
19
|
+
position: { x: 5, y: 6, width: 30, height: 20 },
|
|
20
|
+
content: { shape: 'rect', fill: '#3399ff' },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('PropertiesPanel', () => {
|
|
27
|
+
it('shows an empty hint when no layer is selected', () => {
|
|
28
|
+
const dispatch = vi.fn();
|
|
29
|
+
render(<PropertiesPanel doc={buildDoc()} selectedLayerId={null} dispatch={dispatch} />);
|
|
30
|
+
expect(screen.getByText(/no layer selected/i)).toBeTruthy();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('editing the canvas width dispatches set-canvas', () => {
|
|
34
|
+
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
35
|
+
render(<PropertiesPanel doc={buildDoc()} selectedLayerId={null} dispatch={dispatch} />);
|
|
36
|
+
const widthInput = screen.getByDisplayValue('100') as HTMLInputElement;
|
|
37
|
+
fireEvent.change(widthInput, { target: { value: '200' } });
|
|
38
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
39
|
+
type: 'set-canvas',
|
|
40
|
+
canvas: expect.objectContaining({ width: 200, height: 80 }),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renaming a layer dispatches update-layer with patch.name', () => {
|
|
45
|
+
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
46
|
+
render(<PropertiesPanel doc={buildDoc()} selectedLayerId="a" dispatch={dispatch} />);
|
|
47
|
+
const nameInput = screen.getByDisplayValue('Box') as HTMLInputElement;
|
|
48
|
+
fireEvent.change(nameInput, { target: { value: 'Renamed' } });
|
|
49
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
50
|
+
type: 'update-layer',
|
|
51
|
+
layerId: 'a',
|
|
52
|
+
patch: expect.objectContaining({ name: 'Renamed' }),
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('changing position X dispatches update-layer with merged position', () => {
|
|
57
|
+
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
58
|
+
render(<PropertiesPanel doc={buildDoc()} selectedLayerId="a" dispatch={dispatch} />);
|
|
59
|
+
const xInput = screen.getByDisplayValue('5') as HTMLInputElement;
|
|
60
|
+
fireEvent.change(xInput, { target: { value: '50' } });
|
|
61
|
+
const calls = dispatch.mock.calls.map((c) => c[0]);
|
|
62
|
+
const last = calls[calls.length - 1];
|
|
63
|
+
expect(last).toMatchObject({
|
|
64
|
+
type: 'update-layer',
|
|
65
|
+
layerId: 'a',
|
|
66
|
+
patch: { position: { x: 50, y: 6, width: 30, height: 20 } },
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildFilename,
|
|
4
|
+
resolveFormat,
|
|
5
|
+
supportsDisplayMedia,
|
|
6
|
+
supportsMediaRecorder,
|
|
7
|
+
supportsUserMedia,
|
|
8
|
+
} from '../recorder/formats.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Most of these probes touch global APIs that jsdom doesn't implement.
|
|
12
|
+
* Each test installs the minimum stub needed to exercise the probe and
|
|
13
|
+
* cleans up after itself so probes in other suites stay independent.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const originalMediaRecorder = (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
17
|
+
const originalNavigator = globalThis.navigator;
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (originalMediaRecorder === undefined) {
|
|
21
|
+
delete (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
22
|
+
} else {
|
|
23
|
+
(globalThis as { MediaRecorder?: unknown }).MediaRecorder = originalMediaRecorder;
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
26
|
+
value: originalNavigator,
|
|
27
|
+
configurable: true,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function installMediaRecorderStub(supported: readonly string[]) {
|
|
32
|
+
const stub = {
|
|
33
|
+
isTypeSupported: (mime: string) => supported.includes(mime),
|
|
34
|
+
};
|
|
35
|
+
(globalThis as { MediaRecorder?: unknown }).MediaRecorder = stub;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('supportsMediaRecorder', () => {
|
|
39
|
+
it('is false when MediaRecorder is undefined', () => {
|
|
40
|
+
delete (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
41
|
+
expect(supportsMediaRecorder()).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is true when a MediaRecorder global exists', () => {
|
|
45
|
+
installMediaRecorderStub(['audio/webm']);
|
|
46
|
+
expect(supportsMediaRecorder()).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('supportsUserMedia / supportsDisplayMedia', () => {
|
|
51
|
+
it('is false when navigator.mediaDevices is unavailable', () => {
|
|
52
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
53
|
+
value: {},
|
|
54
|
+
configurable: true,
|
|
55
|
+
});
|
|
56
|
+
expect(supportsUserMedia()).toBe(false);
|
|
57
|
+
expect(supportsDisplayMedia()).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('is true only for the methods that exist', () => {
|
|
61
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
62
|
+
value: { mediaDevices: { getUserMedia: vi.fn() } },
|
|
63
|
+
configurable: true,
|
|
64
|
+
});
|
|
65
|
+
expect(supportsUserMedia()).toBe(true);
|
|
66
|
+
expect(supportsDisplayMedia()).toBe(false);
|
|
67
|
+
|
|
68
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
69
|
+
value: { mediaDevices: { getUserMedia: vi.fn(), getDisplayMedia: vi.fn() } },
|
|
70
|
+
configurable: true,
|
|
71
|
+
});
|
|
72
|
+
expect(supportsUserMedia()).toBe(true);
|
|
73
|
+
expect(supportsDisplayMedia()).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('resolveFormat', () => {
|
|
78
|
+
it('picks the first supported audio candidate', () => {
|
|
79
|
+
installMediaRecorderStub(['audio/webm;codecs=opus', 'audio/webm']);
|
|
80
|
+
const fmt = resolveFormat('audio');
|
|
81
|
+
expect(fmt.mimeType).toBe('audio/webm;codecs=opus');
|
|
82
|
+
expect(fmt.extension).toBe('.webm');
|
|
83
|
+
expect(fmt.directory).toBe('audio');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('picks the first supported video candidate', () => {
|
|
87
|
+
installMediaRecorderStub(['video/mp4;codecs=avc1.42E01E,mp4a.40.2']);
|
|
88
|
+
const fmt = resolveFormat('video');
|
|
89
|
+
expect(fmt.mimeType).toBe('video/mp4;codecs=avc1.42E01E,mp4a.40.2');
|
|
90
|
+
expect(fmt.extension).toBe('.mp4');
|
|
91
|
+
expect(fmt.directory).toBe('video');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('honors a caller-supplied preferred MIME when supported', () => {
|
|
95
|
+
installMediaRecorderStub(['audio/webm', 'audio/ogg;codecs=opus']);
|
|
96
|
+
const fmt = resolveFormat('audio', 'audio/ogg;codecs=opus');
|
|
97
|
+
expect(fmt.mimeType).toBe('audio/ogg;codecs=opus');
|
|
98
|
+
expect(fmt.extension).toBe('.ogg');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('falls through the candidate list when the preferred MIME is unsupported', () => {
|
|
102
|
+
installMediaRecorderStub(['audio/webm']);
|
|
103
|
+
const fmt = resolveFormat('audio', 'audio/aac');
|
|
104
|
+
expect(fmt.mimeType).toBe('audio/webm');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns an empty MIME with a .webm extension fallback when nothing matches', () => {
|
|
108
|
+
installMediaRecorderStub([]);
|
|
109
|
+
const fmt = resolveFormat('video');
|
|
110
|
+
expect(fmt.mimeType).toBe('');
|
|
111
|
+
expect(fmt.extension).toBe('.webm');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns the .webm fallback extension when MediaRecorder is absent entirely', () => {
|
|
115
|
+
delete (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
116
|
+
const fmt = resolveFormat('audio');
|
|
117
|
+
expect(fmt.mimeType).toBe('');
|
|
118
|
+
expect(fmt.extension).toBe('.webm');
|
|
119
|
+
expect(fmt.directory).toBe('audio');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('buildFilename', () => {
|
|
124
|
+
it('uses the basename when supplied, prefixed with the chosen extension', () => {
|
|
125
|
+
expect(buildFilename('audio', '.webm', 'intro')).toBe('intro.webm');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('sanitizes filesystem-hostile characters', () => {
|
|
129
|
+
expect(buildFilename('video', '.mp4', 'my recording: take/2?')).toBe(
|
|
130
|
+
'my-recording--take-2-.mp4',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('falls back to a timestamped name when no basename is supplied', () => {
|
|
135
|
+
const name = buildFilename('audio', '.webm');
|
|
136
|
+
expect(name.startsWith('narration-')).toBe(true);
|
|
137
|
+
expect(name.endsWith('.webm')).toBe(true);
|
|
138
|
+
// Format is narration-YYYYMMDD-HHMMSS.webm — 15 chars after the prefix.
|
|
139
|
+
expect(name).toMatch(/^narration-\d{8}-\d{6}\.webm$/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('uses the recording-* prefix for video sources', () => {
|
|
143
|
+
const name = buildFilename('video', '.mp4');
|
|
144
|
+
expect(name).toMatch(/^recording-\d{8}-\d{6}\.mp4$/);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildTimingJson, encodeTimingJson, timingPathFor } from '../recorder/timingJson.js';
|
|
3
|
+
|
|
4
|
+
describe('buildTimingJson', () => {
|
|
5
|
+
it('returns a payload matching what resolveAudioMapping() expects', () => {
|
|
6
|
+
const timing = buildTimingJson('Hello world.', 12.5);
|
|
7
|
+
expect(timing).toEqual({
|
|
8
|
+
sourceText: 'Hello world.',
|
|
9
|
+
duration: 12.5,
|
|
10
|
+
bookmarks: [],
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('coerces missing or negative durations to 0', () => {
|
|
15
|
+
expect(buildTimingJson('script', Number.NaN).duration).toBe(0);
|
|
16
|
+
expect(buildTimingJson('script', -5).duration).toBe(0);
|
|
17
|
+
expect(buildTimingJson('script', Number.POSITIVE_INFINITY).duration).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('treats empty sourceText as an empty string, not undefined', () => {
|
|
21
|
+
expect(buildTimingJson('', 1).sourceText).toBe('');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('encodeTimingJson', () => {
|
|
26
|
+
it('emits valid pretty-printed JSON', () => {
|
|
27
|
+
const bytes = encodeTimingJson(buildTimingJson('Hi.', 3));
|
|
28
|
+
const text = new TextDecoder().decode(bytes);
|
|
29
|
+
const parsed: unknown = JSON.parse(text);
|
|
30
|
+
expect(parsed).toEqual({ sourceText: 'Hi.', duration: 3, bookmarks: [] });
|
|
31
|
+
// Pretty-printed means newlines and indentation are present.
|
|
32
|
+
expect(text).toContain('\n');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('timingPathFor', () => {
|
|
37
|
+
it('appends .timing.json to the audio path verbatim', () => {
|
|
38
|
+
expect(timingPathFor('audio/narration.webm')).toBe('audio/narration.webm.timing.json');
|
|
39
|
+
expect(timingPathFor('intro.mp3')).toBe('intro.mp3.timing.json');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { markdownToTiptap, tiptapToMarkdown } from '../tiptapBridge';
|
|
3
|
+
|
|
4
|
+
describe('Template annotation round-trip', () => {
|
|
5
|
+
it('extracts {[template]} into data-template on markdownToTiptap', () => {
|
|
6
|
+
const html = markdownToTiptap('## Getting Started {[comparisonBar]}');
|
|
7
|
+
expect(html).toContain('data-template="comparisonBar"');
|
|
8
|
+
expect(html).toContain('Getting Started');
|
|
9
|
+
expect(html).not.toContain('{[comparisonBar]}'); // raw text stripped
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('round-trips markdown → HTML → markdown losslessly', () => {
|
|
13
|
+
const original = '## Getting Started {[comparisonBar]}';
|
|
14
|
+
const html = markdownToTiptap(original);
|
|
15
|
+
const back = tiptapToMarkdown(html);
|
|
16
|
+
expect(back.trim()).toBe(original);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('preserves template annotation through Tiptap-rendered HTML (with badge spans)', () => {
|
|
20
|
+
// Simulates HTML that Tiptap actually renders after parse: includes the
|
|
21
|
+
// squisq-heading-content + squisq-template-badge wrapper spans.
|
|
22
|
+
const tiptapRendered =
|
|
23
|
+
'<h2 data-template="comparisonBar">' +
|
|
24
|
+
'<span class="squisq-heading-content">Getting Started</span>' +
|
|
25
|
+
'<span class="squisq-template-badge" contenteditable="false" data-template="comparisonBar" data-template-label="Comparison Bar"></span>' +
|
|
26
|
+
'</h2>';
|
|
27
|
+
const md = tiptapToMarkdown(tiptapRendered);
|
|
28
|
+
expect(md).toContain('## Getting Started');
|
|
29
|
+
expect(md).toContain('{[comparisonBar]}');
|
|
30
|
+
// No literal badge spans should leak into markdown
|
|
31
|
+
expect(md).not.toContain('squisq-template-badge');
|
|
32
|
+
expect(md).not.toContain('Comparison Bar'); // CSS-rendered label shouldn't bleed
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -74,6 +74,21 @@ describe('markdownToTiptap', () => {
|
|
|
74
74
|
expect(back.trim()).toBe(md);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it('converts FontAwesome inline icons to <i data-icon> tags', () => {
|
|
78
|
+
const html = markdownToTiptap('{[angellist]}');
|
|
79
|
+
expect(html).toContain('data-icon="angellist"');
|
|
80
|
+
expect(html).toContain('data-family="brands"');
|
|
81
|
+
expect(html).toContain('data-name="angellist"');
|
|
82
|
+
expect(html).toContain('class="fa-brands fa-angellist"');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('round-trips FA inline icons through markdown↔HTML', () => {
|
|
86
|
+
const md = 'Repo {[github]} and {[angellist]}.';
|
|
87
|
+
const html = markdownToTiptap(md);
|
|
88
|
+
const back = tiptapToMarkdown(html);
|
|
89
|
+
expect(back.trim()).toBe(md);
|
|
90
|
+
});
|
|
91
|
+
|
|
77
92
|
it('converts images', () => {
|
|
78
93
|
const html = markdownToTiptap('');
|
|
79
94
|
expect(html).toContain('alt="Logo"');
|
|
@@ -190,6 +205,20 @@ describe('tiptapToMarkdown', () => {
|
|
|
190
205
|
expect(md).toContain('`foo`');
|
|
191
206
|
});
|
|
192
207
|
|
|
208
|
+
it('converts images regardless of attribute order', () => {
|
|
209
|
+
// TipTap's default Image extension renders `src` before `alt`. The
|
|
210
|
+
// tiptapToMarkdown parser previously required `alt` first and
|
|
211
|
+
// silently dropped every src-first image, so a pasted/toolbar
|
|
212
|
+
// image attached in the WYSIWYG editor never made it back into
|
|
213
|
+
// the outgoing markdown.
|
|
214
|
+
const altFirst = tiptapToMarkdown('<p><img alt="Logo" src="logo.png"></p>');
|
|
215
|
+
expect(altFirst).toContain('');
|
|
216
|
+
const srcFirst = tiptapToMarkdown('<p><img src="attachments/xyz.png" alt="xyz"></p>');
|
|
217
|
+
expect(srcFirst).toContain('');
|
|
218
|
+
const srcOnly = tiptapToMarkdown('<p><img src="attachments/nm.png"></p>');
|
|
219
|
+
expect(srcOnly).toContain('');
|
|
220
|
+
});
|
|
221
|
+
|
|
193
222
|
it('converts links', () => {
|
|
194
223
|
const md = tiptapToMarkdown('<p><a href="https://example.com">Example</a></p>');
|
|
195
224
|
expect(md).toContain('[Example](https://example.com)');
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/core';
|
|
2
|
+
import Image from '@tiptap/extension-image';
|
|
3
|
+
import Document from '@tiptap/extension-document';
|
|
4
|
+
import Paragraph from '@tiptap/extension-paragraph';
|
|
5
|
+
import Text from '@tiptap/extension-text';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { tiptapToMarkdown } from '../tiptapBridge';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The actual end-to-end question the chat composer hinges on:
|
|
11
|
+
*
|
|
12
|
+
* when a pasted / uploaded image is inserted into the tiptap
|
|
13
|
+
* editor via `setImage({src, alt})`, does the markdown we serialize
|
|
14
|
+
* out (via `getHTML()` + `tiptapToMarkdown`) contain
|
|
15
|
+
* `` — the shape the gezel service's image-attachment
|
|
16
|
+
* extractor expects?
|
|
17
|
+
*
|
|
18
|
+
* Unit tests on the regex alone have been green the whole time, and
|
|
19
|
+
* yet the production app kept sending image-less messages. This test
|
|
20
|
+
* exercises the real tiptap editor in jsdom so the HTML tiptap
|
|
21
|
+
* actually emits is what we check, not a hand-synthesized string.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function makeEditor() {
|
|
25
|
+
return new Editor({
|
|
26
|
+
// Bare-minimum schema: doc → paragraph (text) + block image. No
|
|
27
|
+
// React node-view — we only care about the serialized HTML.
|
|
28
|
+
extensions: [Document, Paragraph, Text, Image.configure({ inline: false })],
|
|
29
|
+
content: '<p></p>',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('tiptap Image node → markdown round-trip', () => {
|
|
34
|
+
it('produces `` for an image inserted via setImage', () => {
|
|
35
|
+
const editor = makeEditor();
|
|
36
|
+
editor.chain().focus().setImage({ src: 'attachments/xyz.png', alt: 'my screenshot' }).run();
|
|
37
|
+
const html = editor.getHTML();
|
|
38
|
+
const md = tiptapToMarkdown(html);
|
|
39
|
+
editor.destroy();
|
|
40
|
+
expect(html).toMatch(/<img\b/);
|
|
41
|
+
expect(html).toContain('src="attachments/xyz.png"');
|
|
42
|
+
expect(md).toContain('');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('produces `` when alt is empty (most common pasted-image shape)', () => {
|
|
46
|
+
const editor = makeEditor();
|
|
47
|
+
editor.chain().focus().setImage({ src: 'attachments/pasted.png', alt: '' }).run();
|
|
48
|
+
const md = tiptapToMarkdown(editor.getHTML());
|
|
49
|
+
editor.destroy();
|
|
50
|
+
expect(md).toContain('');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('produces `` when alt is omitted entirely', () => {
|
|
54
|
+
const editor = makeEditor();
|
|
55
|
+
editor.chain().focus().setImage({ src: 'attachments/no-alt.png' }).run();
|
|
56
|
+
const md = tiptapToMarkdown(editor.getHTML());
|
|
57
|
+
editor.destroy();
|
|
58
|
+
expect(md).toContain('');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('coexists with paragraph text', () => {
|
|
62
|
+
// Seed the editor with a paragraph, then insert the image after
|
|
63
|
+
// it — mirrors the common case where the user types "here:" and
|
|
64
|
+
// then pastes an image.
|
|
65
|
+
const editor = makeEditor();
|
|
66
|
+
editor.chain().focus().insertContent('here you go').run();
|
|
67
|
+
editor.chain().focus().setImage({ src: 'attachments/foo.png', alt: 'foo' }).run();
|
|
68
|
+
const md = tiptapToMarkdown(editor.getHTML());
|
|
69
|
+
editor.destroy();
|
|
70
|
+
expect(md).toContain('');
|
|
71
|
+
expect(md).toContain('here you go');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
5
|
+
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
6
|
+
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
7
|
+
import {
|
|
8
|
+
IMAGE_EDIT_STATE_FILENAME,
|
|
9
|
+
readImageEditDoc,
|
|
10
|
+
writeImageEditDoc,
|
|
11
|
+
createEmptyImageEditDoc,
|
|
12
|
+
} from '@bendyline/squisq/imageEdit';
|
|
13
|
+
import { useImageEditor } from '../imageEditor/useImageEditor.js';
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// jsdom doesn't implement createObjectURL or HTMLImageElement loading.
|
|
17
|
+
// Stub both so the hook can complete its asset-resolution path.
|
|
18
|
+
if (typeof URL.createObjectURL !== 'function') {
|
|
19
|
+
Object.defineProperty(URL, 'createObjectURL', {
|
|
20
|
+
configurable: true,
|
|
21
|
+
value: vi.fn(() => 'blob:stub'),
|
|
22
|
+
});
|
|
23
|
+
} else {
|
|
24
|
+
vi.spyOn(URL, 'createObjectURL').mockImplementation(() => 'blob:stub');
|
|
25
|
+
}
|
|
26
|
+
if (typeof URL.revokeObjectURL !== 'function') {
|
|
27
|
+
Object.defineProperty(URL, 'revokeObjectURL', {
|
|
28
|
+
configurable: true,
|
|
29
|
+
value: vi.fn(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('useImageEditor', () => {
|
|
35
|
+
it('loads an existing state.json from the container', async () => {
|
|
36
|
+
const container = new MemoryContentContainer();
|
|
37
|
+
const doc = createEmptyImageEditDoc(120, 80);
|
|
38
|
+
await writeImageEditDoc(container, doc);
|
|
39
|
+
|
|
40
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
41
|
+
|
|
42
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
43
|
+
expect(result.current.state?.doc.canvas.width).toBe(120);
|
|
44
|
+
expect(result.current.error).toBe(null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('seeds an empty doc when no state.json and no initialSrc', async () => {
|
|
48
|
+
const container = new MemoryContentContainer();
|
|
49
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
50
|
+
|
|
51
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
52
|
+
expect(result.current.state?.doc.layers).toHaveLength(0);
|
|
53
|
+
// The seed should have been written back to the container.
|
|
54
|
+
const persisted = await readImageEditDoc(container);
|
|
55
|
+
expect(persisted).not.toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('persists state.json after a debounced action', async () => {
|
|
59
|
+
const container = new MemoryContentContainer();
|
|
60
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(50, 50));
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
63
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
64
|
+
|
|
65
|
+
act(() => {
|
|
66
|
+
result.current.dispatch({
|
|
67
|
+
type: 'add-layer',
|
|
68
|
+
layer: {
|
|
69
|
+
type: 'shape',
|
|
70
|
+
position: { x: 0, y: 0, width: 10, height: 10 },
|
|
71
|
+
content: { shape: 'rect', fill: '#000' },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await waitFor(() => expect(result.current.state?.dirty).toBe(false), {
|
|
77
|
+
timeout: 500,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const persisted = await readImageEditDoc(container);
|
|
81
|
+
expect(persisted?.layers).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('uploadAsset writes bytes under assets/ and returns the path', async () => {
|
|
85
|
+
const container = new MemoryContentContainer();
|
|
86
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
87
|
+
|
|
88
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
89
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
90
|
+
|
|
91
|
+
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
92
|
+
// jsdom's Blob predates the arrayBuffer() method on some versions, so
|
|
93
|
+
// shim a minimal Blob-like that the hook only needs `.arrayBuffer()`
|
|
94
|
+
// and `.type` from.
|
|
95
|
+
const file = {
|
|
96
|
+
type: 'image/png',
|
|
97
|
+
arrayBuffer: async () => bytes.buffer,
|
|
98
|
+
} as unknown as Blob;
|
|
99
|
+
let path = '';
|
|
100
|
+
await act(async () => {
|
|
101
|
+
path = await result.current.uploadAsset(file, 'pic.png');
|
|
102
|
+
});
|
|
103
|
+
expect(path).toMatch(/^assets\/.+\.png$/);
|
|
104
|
+
const written = await container.readFile(path);
|
|
105
|
+
expect(written).not.toBeNull();
|
|
106
|
+
expect(written!.byteLength).toBe(4);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('flush writes synchronously and clears dirty', async () => {
|
|
110
|
+
const container = new MemoryContentContainer();
|
|
111
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
112
|
+
|
|
113
|
+
const { result } = renderHook(
|
|
114
|
+
() => useImageEditor({ container, persistDebounceMs: 100000 }), // effectively disabled
|
|
115
|
+
);
|
|
116
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
117
|
+
|
|
118
|
+
act(() => {
|
|
119
|
+
result.current.dispatch({
|
|
120
|
+
type: 'set-canvas',
|
|
121
|
+
canvas: { width: 25, height: 25 },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
expect(result.current.state?.dirty).toBe(true);
|
|
125
|
+
|
|
126
|
+
await act(async () => {
|
|
127
|
+
await result.current.flush();
|
|
128
|
+
});
|
|
129
|
+
expect(result.current.state?.dirty).toBe(false);
|
|
130
|
+
const persisted = await readImageEditDoc(container);
|
|
131
|
+
expect(persisted?.canvas.width).toBe(25);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('versioning is null when allowVersioning is false', async () => {
|
|
135
|
+
const container = new MemoryContentContainer();
|
|
136
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
137
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
138
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
139
|
+
expect(result.current.versioning).toBe(null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('versioning manager is exposed when allowVersioning is true', async () => {
|
|
143
|
+
const container = new MemoryContentContainer();
|
|
144
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
145
|
+
const { result } = renderHook(() =>
|
|
146
|
+
useImageEditor({
|
|
147
|
+
container,
|
|
148
|
+
persistDebounceMs: 5,
|
|
149
|
+
allowVersioning: true,
|
|
150
|
+
versioningAutoSaveIdleMs: 0, // disable autosave
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
154
|
+
expect(result.current.versioning).not.toBe(null);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Ensure constants are referenced so unused-import lint stays quiet.
|
|
159
|
+
void IMAGE_EDIT_STATE_FILENAME;
|