@bendyline/squisq-editor-react 1.4.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 +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
package/src/WysiwygEditor.tsx
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* and code blocks.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { useEffect, useMemo, useRef } from 'react';
|
|
13
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
import type { CSSProperties } from 'react';
|
|
14
15
|
import { useEditor, EditorContent } from '@tiptap/react';
|
|
15
16
|
import StarterKit from '@tiptap/starter-kit';
|
|
16
17
|
import Table from '@tiptap/extension-table';
|
|
@@ -19,14 +20,23 @@ import TableCell from '@tiptap/extension-table-cell';
|
|
|
19
20
|
import TableHeader from '@tiptap/extension-table-header';
|
|
20
21
|
import TaskList from '@tiptap/extension-task-list';
|
|
21
22
|
import TaskItem from '@tiptap/extension-task-item';
|
|
23
|
+
import Link from '@tiptap/extension-link';
|
|
22
24
|
import Placeholder from '@tiptap/extension-placeholder';
|
|
25
|
+
import { resolveFontFamily, FONT_FALLBACKS } from '@bendyline/squisq/schemas';
|
|
23
26
|
import { HeadingWithTemplate } from './TemplateAnnotation';
|
|
27
|
+
import { InlineIcon } from './InlineIcon';
|
|
24
28
|
import { ImageWithMediaProvider } from './ImageNodeView';
|
|
29
|
+
import { TiptapVideo } from './tiptap/TiptapVideo';
|
|
30
|
+
import { TiptapAudio } from './tiptap/TiptapAudio';
|
|
31
|
+
import { TemplateBadgePopover, TEMPLATE_NAMES } from './TemplatePicker';
|
|
32
|
+
import { profileBlockContents, recommendTemplatesForBlock } from '@bendyline/squisq/recommend';
|
|
33
|
+
import { findBlockSliceByHeadingIndex } from './blockSlice';
|
|
25
34
|
import { useEditorContext } from './EditorContext';
|
|
26
35
|
import { buildMentionExtension } from './MentionExtension';
|
|
27
36
|
import { markdownToTiptap, tiptapToMarkdown } from './tiptapBridge';
|
|
28
37
|
import { looksLikeMarkdown } from './detectMarkdown';
|
|
29
38
|
import { SQUISQ_MEDIA_MIME, parseSquisqMediaPayload } from './mediaDragMime';
|
|
39
|
+
import { usePreviewSettingsOptional } from './PreviewControls';
|
|
30
40
|
|
|
31
41
|
// ── Frontmatter helpers ────────────────────────────────────────────
|
|
32
42
|
|
|
@@ -90,10 +100,17 @@ export function WysiwygEditor({
|
|
|
90
100
|
submitOnEnter,
|
|
91
101
|
readOnly = false,
|
|
92
102
|
}: WysiwygEditorProps) {
|
|
93
|
-
const {
|
|
94
|
-
|
|
103
|
+
const {
|
|
104
|
+
markdownSource,
|
|
105
|
+
setMarkdownSource,
|
|
106
|
+
setTiptapEditor,
|
|
107
|
+
mediaProvider,
|
|
108
|
+
mentionProvider,
|
|
109
|
+
blockTagsVisible,
|
|
110
|
+
themeInheritance,
|
|
111
|
+
} = useEditorContext();
|
|
95
112
|
// Keep a ref so the mention extension — created once at editor mount —
|
|
96
|
-
// always sees the latest provider. Swapping projects
|
|
113
|
+
// always sees the latest provider. Swapping projects changes
|
|
97
114
|
// the provider without remounting the editor.
|
|
98
115
|
const mentionProviderRef = useRef(mentionProvider);
|
|
99
116
|
useEffect(() => {
|
|
@@ -136,9 +153,17 @@ export function WysiwygEditor({
|
|
|
136
153
|
TableHeader,
|
|
137
154
|
TaskList,
|
|
138
155
|
TaskItem.configure({ nested: true }),
|
|
156
|
+
Link.configure({
|
|
157
|
+
openOnClick: false,
|
|
158
|
+
autolink: true,
|
|
159
|
+
HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' },
|
|
160
|
+
}),
|
|
139
161
|
ImageWithMediaProvider.configure({ inline: false }),
|
|
162
|
+
TiptapVideo,
|
|
163
|
+
TiptapAudio,
|
|
140
164
|
Placeholder.configure({ placeholder: resolvedPlaceholder }),
|
|
141
165
|
buildMentionExtension(() => mentionProviderRef.current),
|
|
166
|
+
InlineIcon,
|
|
142
167
|
],
|
|
143
168
|
content: markdownToTiptap(stripFrontmatter(markdownSource).body),
|
|
144
169
|
onUpdate: ({ editor: ed }) => {
|
|
@@ -159,6 +184,18 @@ export function WysiwygEditor({
|
|
|
159
184
|
// normal behavior (Enter = paragraph break, Shift+Enter = soft break).
|
|
160
185
|
handleKeyDown: (view, event) => {
|
|
161
186
|
if (event.key !== 'Enter' || !submitOnEnterRef.current) return false;
|
|
187
|
+
// Defer Enter to an open mention/suggestion popover so the user
|
|
188
|
+
// can pick the highlighted candidate. ProseMirror plugins fire
|
|
189
|
+
// AFTER editorProps.handleKeyDown, so without this short-circuit
|
|
190
|
+
// plain Enter submits the message and the popover closes
|
|
191
|
+
// without inserting the mention. The MentionExtension marks
|
|
192
|
+
// its container with `display: block` while items are showing
|
|
193
|
+
// and `display: none` when empty — only short-circuit when
|
|
194
|
+
// there's actually a candidate to pick.
|
|
195
|
+
const popover = document.querySelector<HTMLElement>('.squisq-mention-popover');
|
|
196
|
+
if (popover && popover.style.display !== 'none') {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
162
199
|
if (event.metaKey || event.ctrlKey) {
|
|
163
200
|
// User wants a newline. Insert a hard-break and stop propagation so
|
|
164
201
|
// we don't also create a new paragraph.
|
|
@@ -209,10 +246,20 @@ export function WysiwygEditor({
|
|
|
209
246
|
// entries via a custom MIME type and skip the upload step.
|
|
210
247
|
// Falls through to default handling for non-image drops or when no
|
|
211
248
|
// MediaProvider is available.
|
|
212
|
-
handleDrop: (view, event, _slice,
|
|
249
|
+
handleDrop: (view, event, _slice, moved) => {
|
|
213
250
|
const dt = event.dataTransfer;
|
|
214
251
|
if (!dt) return false;
|
|
215
252
|
|
|
253
|
+
// Internal node move (the user dragged an existing node within
|
|
254
|
+
// the document). ProseMirror's `moved` flag is true in this
|
|
255
|
+
// case; let it handle the reposition natively so width/height
|
|
256
|
+
// attributes are preserved and the source node is removed.
|
|
257
|
+
// Without this short-circuit, the browser also exposes the
|
|
258
|
+
// dragged `<img>` as a virtual file in `dataTransfer`, so the
|
|
259
|
+
// upload-and-insert path below would fire — producing a
|
|
260
|
+
// dimension-less duplicate next to the original.
|
|
261
|
+
if (moved) return false;
|
|
262
|
+
|
|
216
263
|
// In-app drag from the MediaBin — insert without uploading
|
|
217
264
|
const squisqRaw = dt.getData(SQUISQ_MEDIA_MIME);
|
|
218
265
|
if (squisqRaw) {
|
|
@@ -226,7 +273,27 @@ export function WysiwygEditor({
|
|
|
226
273
|
}
|
|
227
274
|
|
|
228
275
|
const imageFiles = filesFromDataTransfer(dt);
|
|
229
|
-
if (imageFiles.length === 0
|
|
276
|
+
if (imageFiles.length === 0) {
|
|
277
|
+
// Nothing image-like in the drop. Let the browser / ProseMirror
|
|
278
|
+
// handle it (links, text, etc.). Log enough to debug if the user
|
|
279
|
+
// expected this to be an image drop — Windows/browser combos
|
|
280
|
+
// sometimes deliver image drags without a usable File payload.
|
|
281
|
+
if (dt.files.length > 0 || dt.items.length > 0) {
|
|
282
|
+
console.warn(
|
|
283
|
+
'[squisq-editor] Drop received with no recognizable image File. Types:',
|
|
284
|
+
Array.from(dt.types ?? []),
|
|
285
|
+
'files:',
|
|
286
|
+
Array.from(dt.files).map((f) => `${f.name} (${f.type || 'no-type'})`),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
if (!mediaProviderRef.current) {
|
|
292
|
+
console.warn(
|
|
293
|
+
'[squisq-editor] Image drop received but no MediaProvider is wired up; cannot persist the image. Pass `mediaProvider` to EditorShell / EditorProvider.',
|
|
294
|
+
);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
230
297
|
|
|
231
298
|
event.preventDefault();
|
|
232
299
|
moveSelectionToDropPoint(view, event);
|
|
@@ -251,6 +318,68 @@ export function WysiwygEditor({
|
|
|
251
318
|
if (editor) editor.setEditable(!readOnly);
|
|
252
319
|
}, [editor, readOnly]);
|
|
253
320
|
|
|
321
|
+
// ── Template badge → popover ─────────────────────────────────────
|
|
322
|
+
// The HeadingWithTemplate extension renders an inert `.squisq-template-badge`
|
|
323
|
+
// span inside templated headings. We delegate clicks at the container
|
|
324
|
+
// level so we can locate the heading position and open the gallery
|
|
325
|
+
// anchored at that badge.
|
|
326
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
327
|
+
const [badgeMenu, setBadgeMenu] = useState<{
|
|
328
|
+
rect: DOMRect;
|
|
329
|
+
template: string;
|
|
330
|
+
headingPos: number;
|
|
331
|
+
headingIndex: number;
|
|
332
|
+
} | null>(null);
|
|
333
|
+
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
if (!editor) return;
|
|
336
|
+
const root = containerRef.current;
|
|
337
|
+
if (!root) return;
|
|
338
|
+
const onClick = (e: MouseEvent) => {
|
|
339
|
+
const target = e.target as HTMLElement | null;
|
|
340
|
+
if (!target) return;
|
|
341
|
+
const badge = target.closest('.squisq-template-badge') as HTMLElement | null;
|
|
342
|
+
if (!badge || !root.contains(badge)) return;
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
e.stopPropagation();
|
|
345
|
+
// Find the parent heading element and resolve its document position.
|
|
346
|
+
const headingEl = badge.closest('h1,h2,h3,h4,h5,h6') as HTMLElement | null;
|
|
347
|
+
if (!headingEl) return;
|
|
348
|
+
let pos: number | null = null;
|
|
349
|
+
try {
|
|
350
|
+
pos = editor.view.posAtDOM(headingEl, 0);
|
|
351
|
+
} catch {
|
|
352
|
+
pos = null;
|
|
353
|
+
}
|
|
354
|
+
if (pos == null) return;
|
|
355
|
+
// posAtDOM returns the position *inside* the heading; subtract 1
|
|
356
|
+
// to land on the heading node itself so setNodeMarkup targets it.
|
|
357
|
+
const headingPos = Math.max(0, pos - 1);
|
|
358
|
+
const node = editor.state.doc.nodeAt(headingPos);
|
|
359
|
+
if (!node || node.type.name !== 'heading') return;
|
|
360
|
+
// Count how many headings precede this one so the markdown-source
|
|
361
|
+
// slice helper can locate the matching heading by index.
|
|
362
|
+
let headingIndex = 0;
|
|
363
|
+
let count = 0;
|
|
364
|
+
editor.state.doc.descendants((n, p) => {
|
|
365
|
+
if (n.type.name !== 'heading') return;
|
|
366
|
+
if (p === headingPos) {
|
|
367
|
+
headingIndex = count;
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
count++;
|
|
371
|
+
});
|
|
372
|
+
setBadgeMenu({
|
|
373
|
+
rect: badge.getBoundingClientRect(),
|
|
374
|
+
template: (node.attrs.dataTemplate as string | null) ?? '',
|
|
375
|
+
headingPos,
|
|
376
|
+
headingIndex,
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
root.addEventListener('mousedown', onClick);
|
|
380
|
+
return () => root.removeEventListener('mousedown', onClick);
|
|
381
|
+
}, [editor]);
|
|
382
|
+
|
|
254
383
|
// Sync external changes into Tiptap
|
|
255
384
|
useEffect(() => {
|
|
256
385
|
if (!editor) return;
|
|
@@ -266,26 +395,117 @@ export function WysiwygEditor({
|
|
|
266
395
|
}
|
|
267
396
|
}, [markdownSource, editor]);
|
|
268
397
|
|
|
398
|
+
// Match the WYSIWYG editor's appearance to the active Squisq theme
|
|
399
|
+
// when one is set in frontmatter or picked in the preview dropdown.
|
|
400
|
+
// Driven by the View menu's "Theme inheritance" setting:
|
|
401
|
+
// - 'none' → don't inherit anything
|
|
402
|
+
// - 'fonts' → body + heading fonts only (historical default)
|
|
403
|
+
// - 'fonts-colors' → fonts plus the theme's canvas / text colors
|
|
404
|
+
// Pushed as CSS custom properties on the container so the stylesheet
|
|
405
|
+
// can pick them up (with sensible fallbacks for hosts that don't have
|
|
406
|
+
// a PreviewSettingsProvider in scope).
|
|
407
|
+
const previewSettings = usePreviewSettingsOptional();
|
|
408
|
+
const activeTheme = previewSettings?.activeTheme;
|
|
409
|
+
const themeStyle = useMemo<CSSProperties>(() => {
|
|
410
|
+
if (themeInheritance === 'none' || !activeTheme) return {};
|
|
411
|
+
const out: Record<string, string> = {
|
|
412
|
+
'--squisq-theme-body-font': resolveFontFamily(
|
|
413
|
+
activeTheme.typography.bodyFont,
|
|
414
|
+
FONT_FALLBACKS.sans,
|
|
415
|
+
),
|
|
416
|
+
'--squisq-theme-title-font': resolveFontFamily(
|
|
417
|
+
activeTheme.typography.titleFont,
|
|
418
|
+
FONT_FALLBACKS.sans,
|
|
419
|
+
),
|
|
420
|
+
};
|
|
421
|
+
if (themeInheritance === 'fonts-colors') {
|
|
422
|
+
const colors = activeTheme.colors;
|
|
423
|
+
out['--squisq-theme-bg'] = colors.background;
|
|
424
|
+
// backgroundLight gives a subtle on-canvas surface for inline emphasis
|
|
425
|
+
// (inline `code`, code blocks). Themes always define it.
|
|
426
|
+
out['--squisq-theme-bg-muted'] = colors.backgroundLight;
|
|
427
|
+
out['--squisq-theme-text'] = colors.text;
|
|
428
|
+
out['--squisq-theme-text-muted'] = colors.textMuted;
|
|
429
|
+
out['--squisq-theme-primary'] = colors.primary;
|
|
430
|
+
}
|
|
431
|
+
return out as CSSProperties;
|
|
432
|
+
}, [activeTheme, themeInheritance]);
|
|
433
|
+
|
|
269
434
|
return (
|
|
270
435
|
<div
|
|
271
436
|
className={`squisq-wysiwyg-container${className ? ` ${className}` : ''}`}
|
|
272
|
-
style={{ width: '100%', height: '100%', overflow: 'auto' }}
|
|
437
|
+
style={{ width: '100%', height: '100%', overflow: 'auto', ...themeStyle }}
|
|
273
438
|
data-testid="wysiwyg-container"
|
|
439
|
+
data-block-tags={blockTagsVisible ? 'visible' : 'hidden'}
|
|
440
|
+
data-theme-inheritance={themeInheritance}
|
|
441
|
+
ref={containerRef}
|
|
274
442
|
>
|
|
275
443
|
<EditorContent editor={editor} style={{ height: '100%' }} />
|
|
444
|
+
{badgeMenu && (
|
|
445
|
+
<TemplateBadgePopover
|
|
446
|
+
anchorRect={badgeMenu.rect}
|
|
447
|
+
value={badgeMenu.template}
|
|
448
|
+
recommended={(() => {
|
|
449
|
+
const slice = findBlockSliceByHeadingIndex(markdownSource, badgeMenu.headingIndex);
|
|
450
|
+
if (!slice) return undefined;
|
|
451
|
+
const profile = profileBlockContents(slice);
|
|
452
|
+
return recommendTemplatesForBlock(profile, TEMPLATE_NAMES).recommended;
|
|
453
|
+
})()}
|
|
454
|
+
onChange={(name) => {
|
|
455
|
+
if (!editor) return;
|
|
456
|
+
const tr = editor.state.tr.setNodeMarkup(badgeMenu.headingPos, undefined, {
|
|
457
|
+
...editor.state.doc.nodeAt(badgeMenu.headingPos)?.attrs,
|
|
458
|
+
dataTemplate: name === '' ? null : name,
|
|
459
|
+
});
|
|
460
|
+
editor.view.dispatch(tr);
|
|
461
|
+
}}
|
|
462
|
+
onClose={() => setBadgeMenu(null)}
|
|
463
|
+
/>
|
|
464
|
+
)}
|
|
276
465
|
</div>
|
|
277
466
|
);
|
|
278
467
|
}
|
|
279
468
|
|
|
280
469
|
// ── Image drop / paste helpers ─────────────────────────────────────
|
|
281
470
|
|
|
282
|
-
/**
|
|
471
|
+
/** Extension-based fallback when a dragged file has no `type` set (rare
|
|
472
|
+
* but happens when sources omit the MIME — e.g. some screenshot tools). */
|
|
473
|
+
const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|bmp|svg|avif|ico)$/i;
|
|
474
|
+
|
|
475
|
+
function looksLikeImageFile(file: File): boolean {
|
|
476
|
+
if (file.type.startsWith('image/')) return true;
|
|
477
|
+
return IMAGE_EXT_RE.test(file.name);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Extract image File objects from a DataTransfer (drop event). Reads
|
|
481
|
+
* from both `dt.files` and `dt.items`; some drag sources (cross-tab
|
|
482
|
+
* drags, certain native apps) populate only one of the two. */
|
|
283
483
|
function filesFromDataTransfer(dt: DataTransfer): File[] {
|
|
284
484
|
const files: File[] = [];
|
|
485
|
+
const seen = new Set<string>();
|
|
486
|
+
|
|
285
487
|
for (let i = 0; i < dt.files.length; i++) {
|
|
286
488
|
const file = dt.files[i];
|
|
287
|
-
if (file
|
|
489
|
+
if (looksLikeImageFile(file)) {
|
|
490
|
+
files.push(file);
|
|
491
|
+
seen.add(`${file.name}|${file.size}`);
|
|
492
|
+
}
|
|
288
493
|
}
|
|
494
|
+
|
|
495
|
+
if (dt.items) {
|
|
496
|
+
for (let i = 0; i < dt.items.length; i++) {
|
|
497
|
+
const item = dt.items[i];
|
|
498
|
+
if (item.kind !== 'file') continue;
|
|
499
|
+
const file = item.getAsFile();
|
|
500
|
+
if (!file) continue;
|
|
501
|
+
if (!looksLikeImageFile(file)) continue;
|
|
502
|
+
const key = `${file.name}|${file.size}`;
|
|
503
|
+
if (seen.has(key)) continue;
|
|
504
|
+
files.push(file);
|
|
505
|
+
seen.add(key);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
289
509
|
return files;
|
|
290
510
|
}
|
|
291
511
|
|
|
@@ -70,19 +70,4 @@ describe('looksLikeMarkdown', () => {
|
|
|
70
70
|
it('detects markdown with windows line endings', () => {
|
|
71
71
|
expect(looksLikeMarkdown('# Heading\r\n\r\nBody text')).toBe(true);
|
|
72
72
|
});
|
|
73
|
-
|
|
74
|
-
it('detects a full resume-style document', () => {
|
|
75
|
-
const text = `# Mike Ammerlaan
|
|
76
|
-
|
|
77
|
-
## Projects
|
|
78
|
-
|
|
79
|
-
Qualla (qualla.com) - Designed and built a map-driven storytelling platform.
|
|
80
|
-
|
|
81
|
-
## **Experience**
|
|
82
|
-
|
|
83
|
-
**Principal Product Manager, Minecraft**
|
|
84
|
-
Jan 2021 - Present
|
|
85
|
-
Driving the creator platform.`;
|
|
86
|
-
expect(looksLikeMarkdown(text)).toBe(true);
|
|
87
|
-
});
|
|
88
73
|
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
|
|
3
|
+
import { DocumentSettingsDialog } from '../DocumentSettingsDialog';
|
|
4
|
+
|
|
5
|
+
afterEach(() => cleanup());
|
|
6
|
+
|
|
7
|
+
function open(source: string, onSave: (next: string) => void) {
|
|
8
|
+
return render(
|
|
9
|
+
<DocumentSettingsDialog markdownSource={source} onSave={onSave} onClose={() => {}} />,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function clickSave() {
|
|
14
|
+
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Open the Theme picker popover and click the theme whose row title
|
|
19
|
+
* matches `name`. The picker is portaled to `document.body`, so the
|
|
20
|
+
* listbox lives outside the dialog form — testing-library's `screen`
|
|
21
|
+
* still finds it because it queries the whole document.
|
|
22
|
+
*/
|
|
23
|
+
function pickTheme(name: string) {
|
|
24
|
+
// Trigger button has aria-label "Theme"
|
|
25
|
+
fireEvent.click(screen.getByLabelText('Theme'));
|
|
26
|
+
// Scope the option lookup to the popover — the Transform <select> in
|
|
27
|
+
// the same dialog also exposes role="option" entries (one of which is
|
|
28
|
+
// literally "Documentary"), so a global query would collide.
|
|
29
|
+
const popover = document.getElementById('squisq-theme-picker-popover')!;
|
|
30
|
+
fireEvent.click(within(popover).getByRole('option', { name }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('DocumentSettingsDialog', () => {
|
|
34
|
+
it('shows the inferred title as the placeholder when no title in frontmatter', () => {
|
|
35
|
+
const onSave = vi.fn();
|
|
36
|
+
open('# My Document\n\nBody.', onSave);
|
|
37
|
+
const input = screen.getByLabelText('Title') as HTMLInputElement;
|
|
38
|
+
expect(input.value).toBe('');
|
|
39
|
+
expect(input.placeholder).toBe('My Document');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not write title when input is left empty', () => {
|
|
43
|
+
const onSave = vi.fn();
|
|
44
|
+
open('# My Document\n\nBody.', onSave);
|
|
45
|
+
clickSave();
|
|
46
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
47
|
+
expect(next).not.toMatch(/^---[\s\S]*title:/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not write title when value matches the inferred title', () => {
|
|
51
|
+
const onSave = vi.fn();
|
|
52
|
+
open('# My Document\n\nBody.', onSave);
|
|
53
|
+
fireEvent.change(screen.getByLabelText('Title'), { target: { value: 'My Document' } });
|
|
54
|
+
clickSave();
|
|
55
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
56
|
+
expect(next).not.toMatch(/^---[\s\S]*title:/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('strips an existing title key when the user clears the input', () => {
|
|
60
|
+
const onSave = vi.fn();
|
|
61
|
+
const src = '---\ntitle: Old\n---\n\n# My Document\n';
|
|
62
|
+
open(src, onSave);
|
|
63
|
+
const input = screen.getByLabelText('Title') as HTMLInputElement;
|
|
64
|
+
expect(input.value).toBe('Old');
|
|
65
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
66
|
+
clickSave();
|
|
67
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
68
|
+
expect(next).not.toMatch(/title:/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('writes a title that differs from the inferred value', () => {
|
|
72
|
+
const onSave = vi.fn();
|
|
73
|
+
open('# My Document\n\nBody.', onSave);
|
|
74
|
+
fireEvent.change(screen.getByLabelText('Title'), {
|
|
75
|
+
target: { value: 'Custom Title' },
|
|
76
|
+
});
|
|
77
|
+
clickSave();
|
|
78
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
79
|
+
expect(next).toMatch(/^---\ntitle: Custom Title\n---/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('writes squisq-theme when the user picks a theme', () => {
|
|
83
|
+
const onSave = vi.fn();
|
|
84
|
+
open('# Doc\n', onSave);
|
|
85
|
+
pickTheme('Documentary');
|
|
86
|
+
clickSave();
|
|
87
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
88
|
+
expect(next).toContain('squisq-theme: documentary');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('clears the legacy theme key when switching themes', () => {
|
|
92
|
+
const onSave = vi.fn();
|
|
93
|
+
const src = '---\ntheme: bold\n---\n\n# Doc\n';
|
|
94
|
+
open(src, onSave);
|
|
95
|
+
pickTheme('Documentary');
|
|
96
|
+
clickSave();
|
|
97
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
98
|
+
expect(next).toContain('squisq-theme: documentary');
|
|
99
|
+
expect(next).not.toMatch(/^theme:/m);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('removes squisq-theme when reverting to default', () => {
|
|
103
|
+
const onSave = vi.fn();
|
|
104
|
+
const src = '---\nsquisq-theme: documentary\n---\n\n# Doc\n';
|
|
105
|
+
open(src, onSave);
|
|
106
|
+
pickTheme('Default');
|
|
107
|
+
clickSave();
|
|
108
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
109
|
+
expect(next).not.toContain('squisq-theme');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('writes squisq-transform when a transform is picked', () => {
|
|
113
|
+
const onSave = vi.fn();
|
|
114
|
+
open('# Doc\n', onSave);
|
|
115
|
+
const select = screen.getByLabelText('Transform') as HTMLSelectElement;
|
|
116
|
+
// Pick any non-empty option from the dropdown
|
|
117
|
+
const first = Array.from(select.options).find((o) => o.value !== '');
|
|
118
|
+
if (!first) throw new Error('no transform options available');
|
|
119
|
+
fireEvent.change(select, { target: { value: first.value } });
|
|
120
|
+
clickSave();
|
|
121
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
122
|
+
expect(next).toContain(`squisq-transform: ${first.value}`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('writes squisq-captions when picked', () => {
|
|
126
|
+
const onSave = vi.fn();
|
|
127
|
+
open('# Doc\n', onSave);
|
|
128
|
+
fireEvent.change(screen.getByLabelText('Captions'), { target: { value: 'social' } });
|
|
129
|
+
clickSave();
|
|
130
|
+
const next = onSave.mock.calls[0][0] as string;
|
|
131
|
+
expect(next).toContain('squisq-captions: social');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('closes when the Cancel button is clicked', () => {
|
|
135
|
+
const onClose = vi.fn();
|
|
136
|
+
render(<DocumentSettingsDialog markdownSource="# Doc" onSave={() => {}} onClose={onClose} />);
|
|
137
|
+
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
138
|
+
expect(onClose).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('falls back to the H2 when no H1 exists', () => {
|
|
142
|
+
const onSave = vi.fn();
|
|
143
|
+
open('## Subtitle\n\nBody.', onSave);
|
|
144
|
+
const input = screen.getByLabelText('Title') as HTMLInputElement;
|
|
145
|
+
expect(input.placeholder).toBe('Subtitle');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { EmojiPicker } from '../EmojiPicker';
|
|
4
|
+
import {
|
|
5
|
+
EMOJI_CATEGORIES,
|
|
6
|
+
ALL_EMOJIS,
|
|
7
|
+
searchEmojis,
|
|
8
|
+
PICKER_CATEGORIES,
|
|
9
|
+
ALL_PICKER_ENTRIES,
|
|
10
|
+
searchPickerEntries,
|
|
11
|
+
} from '../emojiData';
|
|
12
|
+
|
|
13
|
+
describe('emojiData — legacy emoji exports', () => {
|
|
14
|
+
it('exposes the standard CLDR category buckets', () => {
|
|
15
|
+
const ids = EMOJI_CATEGORIES.map((c) => c.id);
|
|
16
|
+
expect(ids).toEqual([
|
|
17
|
+
'smileys',
|
|
18
|
+
'people',
|
|
19
|
+
'nature',
|
|
20
|
+
'food',
|
|
21
|
+
'travel',
|
|
22
|
+
'activities',
|
|
23
|
+
'objects',
|
|
24
|
+
'symbols',
|
|
25
|
+
'flags',
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('each entry has a non-empty char and name', () => {
|
|
30
|
+
for (const entry of ALL_EMOJIS) {
|
|
31
|
+
expect(entry.char.length).toBeGreaterThan(0);
|
|
32
|
+
expect(entry.name.length).toBeGreaterThan(0);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('searchEmojis matches by name (case-insensitive)', () => {
|
|
37
|
+
const results = searchEmojis('HEART');
|
|
38
|
+
expect(results.some((e) => e.char === '❤️')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('emojiData — unified picker entries', () => {
|
|
43
|
+
it('appends FA Brands, Solid, and Regular categories after the emoji buckets', () => {
|
|
44
|
+
const ids = PICKER_CATEGORIES.map((c) => c.id);
|
|
45
|
+
expect(ids).toContain('fa-brands');
|
|
46
|
+
expect(ids).toContain('fa-solid');
|
|
47
|
+
expect(ids).toContain('fa-regular');
|
|
48
|
+
// Order matters — emoji first, FA after.
|
|
49
|
+
expect(ids.indexOf('smileys')).toBeLessThan(ids.indexOf('fa-brands'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('FA categories carry icon-kind entries', () => {
|
|
53
|
+
const brands = PICKER_CATEGORIES.find((c) => c.id === 'fa-brands')!;
|
|
54
|
+
expect(brands.entries.length).toBeGreaterThan(0);
|
|
55
|
+
const github = brands.entries.find((e) => e.kind === 'icon' && e.name === 'github');
|
|
56
|
+
expect(github).toBeDefined();
|
|
57
|
+
expect(github!.kind).toBe('icon');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('searchPickerEntries matches across emoji and icons', () => {
|
|
61
|
+
const emojiHit = searchPickerEntries('heart').find((e) => e.kind === 'emoji');
|
|
62
|
+
const iconHit = searchPickerEntries('github').find((e) => e.kind === 'icon');
|
|
63
|
+
expect(emojiHit).toBeDefined();
|
|
64
|
+
expect(iconHit).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('ALL_PICKER_ENTRIES is the concat of all categories', () => {
|
|
68
|
+
const sum = PICKER_CATEGORIES.reduce((acc, c) => acc + c.entries.length, 0);
|
|
69
|
+
expect(ALL_PICKER_ENTRIES.length).toBe(sum);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('EmojiPicker', () => {
|
|
74
|
+
it('does not render when closed', () => {
|
|
75
|
+
render(<EmojiPicker open={false} onSelect={() => {}} onClose={() => {}} />);
|
|
76
|
+
expect(screen.queryByTestId('emoji-picker')).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('renders all emoji + FA tabs when open', () => {
|
|
80
|
+
render(<EmojiPicker open onSelect={() => {}} onClose={() => {}} />);
|
|
81
|
+
for (const cat of EMOJI_CATEGORIES) {
|
|
82
|
+
expect(screen.getByRole('tab', { name: cat.label })).toBeTruthy();
|
|
83
|
+
}
|
|
84
|
+
expect(screen.getByRole('tab', { name: 'Brands' })).toBeTruthy();
|
|
85
|
+
expect(screen.getByRole('tab', { name: 'Solid' })).toBeTruthy();
|
|
86
|
+
expect(screen.getByRole('tab', { name: 'Regular' })).toBeTruthy();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('fires onSelect with an emoji PickerEntry when an emoji tile is clicked', () => {
|
|
90
|
+
const onSelect = vi.fn();
|
|
91
|
+
render(<EmojiPicker open onSelect={onSelect} onClose={() => {}} />);
|
|
92
|
+
fireEvent.click(screen.getByRole('button', { name: 'grinning' }));
|
|
93
|
+
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ kind: 'emoji', char: '😀' }));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('fires onSelect with an icon PickerEntry when an FA tile is clicked', () => {
|
|
97
|
+
const onSelect = vi.fn();
|
|
98
|
+
render(<EmojiPicker open onSelect={onSelect} onClose={() => {}} />);
|
|
99
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Brands' }));
|
|
100
|
+
fireEvent.click(screen.getByRole('button', { name: 'Github' }));
|
|
101
|
+
expect(onSelect).toHaveBeenCalledWith(
|
|
102
|
+
expect.objectContaining({ kind: 'icon', family: 'brands', name: 'github' }),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('switches categories when a tab is clicked', () => {
|
|
107
|
+
render(<EmojiPicker open onSelect={() => {}} onClose={() => {}} />);
|
|
108
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Nature' }));
|
|
109
|
+
expect(screen.getByRole('button', { name: 'dog face' })).toBeTruthy();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('shows search results across emoji + icons and hides the tabs', () => {
|
|
113
|
+
render(<EmojiPicker open onSelect={() => {}} onClose={() => {}} />);
|
|
114
|
+
const search = screen.getByLabelText('Search emoji & icons') as HTMLInputElement;
|
|
115
|
+
fireEvent.change(search, { target: { value: 'github' } });
|
|
116
|
+
expect(screen.getByRole('button', { name: 'Github' })).toBeTruthy();
|
|
117
|
+
expect(screen.queryByRole('tab', { name: 'Smileys' })).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('shows a no-matches state when search returns nothing', () => {
|
|
121
|
+
render(<EmojiPicker open onSelect={() => {}} onClose={() => {}} />);
|
|
122
|
+
const search = screen.getByLabelText('Search emoji & icons') as HTMLInputElement;
|
|
123
|
+
fireEvent.change(search, { target: { value: 'thisdoesnotexist' } });
|
|
124
|
+
expect(screen.getByText(/No matches for/i)).toBeTruthy();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('fires onClose on Escape', () => {
|
|
128
|
+
const onClose = vi.fn();
|
|
129
|
+
render(<EmojiPicker open onSelect={() => {}} onClose={onClose} />);
|
|
130
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
131
|
+
expect(onClose).toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -93,4 +93,20 @@ describe('resolveFileKind', () => {
|
|
|
93
93
|
it('accepts a language with no fileName', () => {
|
|
94
94
|
expect(resolveFileKind(undefined, 'rust')).toEqual({ mode: 'code', language: 'rust' });
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
it('returns image mode for raster image extensions', () => {
|
|
98
|
+
expect(resolveFileKind('foo.png')).toEqual({ mode: 'image', language: 'image' });
|
|
99
|
+
expect(resolveFileKind('photo.JPEG')).toEqual({ mode: 'image', language: 'image' });
|
|
100
|
+
expect(resolveFileKind('anim.gif')).toEqual({ mode: 'image', language: 'image' });
|
|
101
|
+
expect(resolveFileKind('hero.webp')).toEqual({ mode: 'image', language: 'image' });
|
|
102
|
+
expect(resolveFileKind('icon.avif')).toEqual({ mode: 'image', language: 'image' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('keeps SVG as code mode (xml) so the source stays editable', () => {
|
|
106
|
+
expect(resolveFileKind('logo.svg')).toEqual({ mode: 'code', language: 'xml' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('respects an explicit language=image override', () => {
|
|
110
|
+
expect(resolveFileKind(undefined, 'image')).toEqual({ mode: 'image', language: 'image' });
|
|
111
|
+
});
|
|
96
112
|
});
|