@bendyline/squisq-editor-react 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DocumentSettingsDialog.d.ts +26 -0
- package/dist/DocumentSettingsDialog.d.ts.map +1 -0
- package/dist/DocumentSettingsDialog.js +115 -0
- package/dist/DocumentSettingsDialog.js.map +1 -0
- package/dist/EditorContext.d.ts +248 -4
- package/dist/EditorContext.d.ts.map +1 -1
- package/dist/EditorContext.js +248 -10
- package/dist/EditorContext.js.map +1 -1
- package/dist/EditorShell.d.ts +173 -4
- package/dist/EditorShell.d.ts.map +1 -1
- package/dist/EditorShell.js +110 -10
- package/dist/EditorShell.js.map +1 -1
- package/dist/EmojiPicker.d.ts +50 -0
- package/dist/EmojiPicker.d.ts.map +1 -0
- package/dist/EmojiPicker.js +182 -0
- package/dist/EmojiPicker.js.map +1 -0
- package/dist/ImageEditor.d.ts +68 -0
- package/dist/ImageEditor.d.ts.map +1 -0
- package/dist/ImageEditor.js +166 -0
- package/dist/ImageEditor.js.map +1 -0
- package/dist/ImageNodeView.d.ts +13 -1
- package/dist/ImageNodeView.d.ts.map +1 -1
- package/dist/ImageNodeView.js +172 -19
- package/dist/ImageNodeView.js.map +1 -1
- package/dist/ImageViewer.d.ts +26 -0
- package/dist/ImageViewer.d.ts.map +1 -0
- package/dist/ImageViewer.js +119 -0
- package/dist/ImageViewer.js.map +1 -0
- package/dist/InlineIcon.d.ts +17 -0
- package/dist/InlineIcon.d.ts.map +1 -0
- package/dist/InlineIcon.js +72 -0
- package/dist/InlineIcon.js.map +1 -0
- package/dist/InlinePreviewGutter.d.ts +52 -0
- package/dist/InlinePreviewGutter.d.ts.map +1 -0
- package/dist/InlinePreviewGutter.js +397 -0
- package/dist/InlinePreviewGutter.js.map +1 -0
- package/dist/LinkDialog.d.ts +43 -0
- package/dist/LinkDialog.d.ts.map +1 -0
- package/dist/LinkDialog.js +102 -0
- package/dist/LinkDialog.js.map +1 -0
- package/dist/MentionExtension.js +10 -7
- package/dist/MentionExtension.js.map +1 -1
- package/dist/OutlinePanel.d.ts +17 -0
- package/dist/OutlinePanel.d.ts.map +1 -0
- package/dist/OutlinePanel.js +167 -0
- package/dist/OutlinePanel.js.map +1 -0
- package/dist/PlainHtmlPreview.d.ts +50 -0
- package/dist/PlainHtmlPreview.d.ts.map +1 -0
- package/dist/PlainHtmlPreview.js +155 -0
- package/dist/PlainHtmlPreview.js.map +1 -0
- package/dist/PreviewControls.d.ts +15 -1
- package/dist/PreviewControls.d.ts.map +1 -1
- package/dist/PreviewControls.js +75 -18
- package/dist/PreviewControls.js.map +1 -1
- package/dist/PreviewPanel.d.ts +11 -10
- package/dist/PreviewPanel.d.ts.map +1 -1
- package/dist/PreviewPanel.js +20 -17
- package/dist/PreviewPanel.js.map +1 -1
- package/dist/RawEditor.d.ts.map +1 -1
- package/dist/RawEditor.js +198 -4
- package/dist/RawEditor.js.map +1 -1
- package/dist/RecorderEntry.d.ts +24 -0
- package/dist/RecorderEntry.d.ts.map +1 -0
- package/dist/RecorderEntry.js +139 -0
- package/dist/RecorderEntry.js.map +1 -0
- package/dist/TemplateAnnotation.d.ts.map +1 -1
- package/dist/TemplateAnnotation.js +32 -6
- package/dist/TemplateAnnotation.js.map +1 -1
- package/dist/TemplatePicker.d.ts +53 -0
- package/dist/TemplatePicker.d.ts.map +1 -0
- package/dist/TemplatePicker.js +388 -0
- package/dist/TemplatePicker.js.map +1 -0
- package/dist/ThemeCustomizerPanel.d.ts +32 -0
- package/dist/ThemeCustomizerPanel.d.ts.map +1 -0
- package/dist/ThemeCustomizerPanel.js +256 -0
- package/dist/ThemeCustomizerPanel.js.map +1 -0
- package/dist/ThemePicker.d.ts +33 -0
- package/dist/ThemePicker.d.ts.map +1 -0
- package/dist/ThemePicker.js +148 -0
- package/dist/ThemePicker.js.map +1 -0
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar.js +508 -33
- package/dist/Toolbar.js.map +1 -1
- package/dist/VersionHistoryPanel.d.ts +14 -0
- package/dist/VersionHistoryPanel.d.ts.map +1 -0
- package/dist/VersionHistoryPanel.js +147 -0
- package/dist/VersionHistoryPanel.js.map +1 -0
- package/dist/ViewMenuPanel.d.ts +13 -0
- package/dist/ViewMenuPanel.d.ts.map +1 -0
- package/dist/ViewMenuPanel.js +58 -0
- package/dist/ViewMenuPanel.js.map +1 -0
- package/dist/WysiwygEditor.d.ts.map +1 -1
- package/dist/WysiwygEditor.js +198 -9
- package/dist/WysiwygEditor.js.map +1 -1
- package/dist/__tests__/detectMarkdown.test.js +0 -14
- package/dist/__tests__/detectMarkdown.test.js.map +1 -1
- package/dist/__tests__/documentSettingsDialog.test.d.ts +2 -0
- package/dist/__tests__/documentSettingsDialog.test.d.ts.map +1 -0
- package/dist/__tests__/documentSettingsDialog.test.js +132 -0
- package/dist/__tests__/documentSettingsDialog.test.js.map +1 -0
- package/dist/__tests__/emojiPicker.test.d.ts +2 -0
- package/dist/__tests__/emojiPicker.test.d.ts.map +1 -0
- package/dist/__tests__/emojiPicker.test.js +111 -0
- package/dist/__tests__/emojiPicker.test.js.map +1 -0
- package/dist/__tests__/fileKind.test.js +13 -0
- package/dist/__tests__/fileKind.test.js.map +1 -1
- package/dist/__tests__/imageEditAffordance.test.d.ts +2 -0
- package/dist/__tests__/imageEditAffordance.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditAffordance.test.js +188 -0
- package/dist/__tests__/imageEditAffordance.test.js.map +1 -0
- package/dist/__tests__/imageEditorShell.test.d.ts +2 -0
- package/dist/__tests__/imageEditorShell.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditorShell.test.js +52 -0
- package/dist/__tests__/imageEditorShell.test.js.map +1 -0
- package/dist/__tests__/imageEditorState.test.d.ts +3 -0
- package/dist/__tests__/imageEditorState.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditorState.test.js +148 -0
- package/dist/__tests__/imageEditorState.test.js.map +1 -0
- package/dist/__tests__/inlinePreviewGutter.test.d.ts +2 -0
- package/dist/__tests__/inlinePreviewGutter.test.d.ts.map +1 -0
- package/dist/__tests__/inlinePreviewGutter.test.js +51 -0
- package/dist/__tests__/inlinePreviewGutter.test.js.map +1 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts +2 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts.map +1 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js +63 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js.map +1 -0
- package/dist/__tests__/jsonEditor.test.d.ts +2 -0
- package/dist/__tests__/jsonEditor.test.d.ts.map +1 -0
- package/dist/__tests__/jsonEditor.test.js +134 -0
- package/dist/__tests__/jsonEditor.test.js.map +1 -0
- package/dist/__tests__/layersPanel.test.d.ts +2 -0
- package/dist/__tests__/layersPanel.test.d.ts.map +1 -0
- package/dist/__tests__/layersPanel.test.js +84 -0
- package/dist/__tests__/layersPanel.test.js.map +1 -0
- package/dist/__tests__/linkDialogDocPicker.test.d.ts +7 -0
- package/dist/__tests__/linkDialogDocPicker.test.d.ts.map +1 -0
- package/dist/__tests__/linkDialogDocPicker.test.js +75 -0
- package/dist/__tests__/linkDialogDocPicker.test.js.map +1 -0
- package/dist/__tests__/outlinePanel.test.d.ts +2 -0
- package/dist/__tests__/outlinePanel.test.d.ts.map +1 -0
- package/dist/__tests__/outlinePanel.test.js +68 -0
- package/dist/__tests__/outlinePanel.test.js.map +1 -0
- package/dist/__tests__/plainHtmlPreview.test.d.ts +2 -0
- package/dist/__tests__/plainHtmlPreview.test.d.ts.map +1 -0
- package/dist/__tests__/plainHtmlPreview.test.js +87 -0
- package/dist/__tests__/plainHtmlPreview.test.js.map +1 -0
- package/dist/__tests__/propertiesPanel.test.d.ts +2 -0
- package/dist/__tests__/propertiesPanel.test.d.ts.map +1 -0
- package/dist/__tests__/propertiesPanel.test.js +64 -0
- package/dist/__tests__/propertiesPanel.test.js.map +1 -0
- package/dist/__tests__/recorderFormats.test.d.ts +2 -0
- package/dist/__tests__/recorderFormats.test.d.ts.map +1 -0
- package/dist/__tests__/recorderFormats.test.js +121 -0
- package/dist/__tests__/recorderFormats.test.js.map +1 -0
- package/dist/__tests__/recorderTimingJson.test.d.ts +2 -0
- package/dist/__tests__/recorderTimingJson.test.d.ts.map +1 -0
- package/dist/__tests__/recorderTimingJson.test.js +37 -0
- package/dist/__tests__/recorderTimingJson.test.js.map +1 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts +2 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts.map +1 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.js +31 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.js.map +1 -0
- package/dist/__tests__/tiptapBridge.test.js +13 -0
- package/dist/__tests__/tiptapBridge.test.js.map +1 -1
- package/dist/__tests__/useImageEditor.test.d.ts +2 -0
- package/dist/__tests__/useImageEditor.test.d.ts.map +1 -0
- package/dist/__tests__/useImageEditor.test.js +131 -0
- package/dist/__tests__/useImageEditor.test.js.map +1 -0
- package/dist/__tests__/useMediaRecorder.test.d.ts +2 -0
- package/dist/__tests__/useMediaRecorder.test.d.ts.map +1 -0
- package/dist/__tests__/useMediaRecorder.test.js +153 -0
- package/dist/__tests__/useMediaRecorder.test.js.map +1 -0
- package/dist/__tests__/versionHistory.test.d.ts +2 -0
- package/dist/__tests__/versionHistory.test.d.ts.map +1 -0
- package/dist/__tests__/versionHistory.test.js +124 -0
- package/dist/__tests__/versionHistory.test.js.map +1 -0
- package/dist/blockSlice.d.ts +24 -0
- package/dist/blockSlice.d.ts.map +1 -0
- package/dist/blockSlice.js +63 -0
- package/dist/blockSlice.js.map +1 -0
- package/dist/buildPreviewDoc.d.ts.map +1 -1
- package/dist/buildPreviewDoc.js +52 -2
- package/dist/buildPreviewDoc.js.map +1 -1
- package/dist/emojiData.d.ts +81 -0
- package/dist/emojiData.d.ts.map +1 -0
- package/dist/emojiData.js +1283 -0
- package/dist/emojiData.js.map +1 -0
- package/dist/fileKind.d.ts +6 -2
- package/dist/fileKind.d.ts.map +1 -1
- package/dist/fileKind.js +25 -4
- package/dist/fileKind.js.map +1 -1
- package/dist/hooks/useFileDrop.d.ts.map +1 -1
- package/dist/hooks/useFileDrop.js +40 -4
- package/dist/hooks/useFileDrop.js.map +1 -1
- package/dist/imageEditor/CanvasSurface.d.ts +31 -0
- package/dist/imageEditor/CanvasSurface.d.ts.map +1 -0
- package/dist/imageEditor/CanvasSurface.js +264 -0
- package/dist/imageEditor/CanvasSurface.js.map +1 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts +39 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts.map +1 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.js +283 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.js.map +1 -0
- package/dist/imageEditor/LayersPanel.d.ts +14 -0
- package/dist/imageEditor/LayersPanel.d.ts.map +1 -0
- package/dist/imageEditor/LayersPanel.js +43 -0
- package/dist/imageEditor/LayersPanel.js.map +1 -0
- package/dist/imageEditor/PropertiesPanel.d.ts +14 -0
- package/dist/imageEditor/PropertiesPanel.d.ts.map +1 -0
- package/dist/imageEditor/PropertiesPanel.js +97 -0
- package/dist/imageEditor/PropertiesPanel.js.map +1 -0
- package/dist/imageEditor/Toolbar.d.ts +30 -0
- package/dist/imageEditor/Toolbar.d.ts.map +1 -0
- package/dist/imageEditor/Toolbar.js +108 -0
- package/dist/imageEditor/Toolbar.js.map +1 -0
- package/dist/imageEditor/icons.d.ts +24 -0
- package/dist/imageEditor/icons.d.ts.map +1 -0
- package/dist/imageEditor/icons.js +45 -0
- package/dist/imageEditor/icons.js.map +1 -0
- package/dist/imageEditor/layers/EditorImageLayer.d.ts +16 -0
- package/dist/imageEditor/layers/EditorImageLayer.d.ts.map +1 -0
- package/dist/imageEditor/layers/EditorImageLayer.js +37 -0
- package/dist/imageEditor/layers/EditorImageLayer.js.map +1 -0
- package/dist/imageEditor/layers/EditorShapeLayer.d.ts +15 -0
- package/dist/imageEditor/layers/EditorShapeLayer.d.ts.map +1 -0
- package/dist/imageEditor/layers/EditorShapeLayer.js +20 -0
- package/dist/imageEditor/layers/EditorShapeLayer.js.map +1 -0
- package/dist/imageEditor/layers/EditorTextLayer.d.ts +18 -0
- package/dist/imageEditor/layers/EditorTextLayer.d.ts.map +1 -0
- package/dist/imageEditor/layers/EditorTextLayer.js +13 -0
- package/dist/imageEditor/layers/EditorTextLayer.js.map +1 -0
- package/dist/imageEditor/layers/SelectionHandles.d.ts +17 -0
- package/dist/imageEditor/layers/SelectionHandles.d.ts.map +1 -0
- package/dist/imageEditor/layers/SelectionHandles.js +19 -0
- package/dist/imageEditor/layers/SelectionHandles.js.map +1 -0
- package/dist/imageEditor/state.d.ts +76 -0
- package/dist/imageEditor/state.d.ts.map +1 -0
- package/dist/imageEditor/state.js +87 -0
- package/dist/imageEditor/state.js.map +1 -0
- package/dist/imageEditor/useImageEditor.d.ts +53 -0
- package/dist/imageEditor/useImageEditor.d.ts.map +1 -0
- package/dist/imageEditor/useImageEditor.js +244 -0
- package/dist/imageEditor/useImageEditor.js.map +1 -0
- package/dist/imageEditor/useImageEditorTokens.d.ts +16 -0
- package/dist/imageEditor/useImageEditorTokens.d.ts.map +1 -0
- package/dist/imageEditor/useImageEditorTokens.js +45 -0
- package/dist/imageEditor/useImageEditorTokens.js.map +1 -0
- package/dist/index.d.ts +48 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonEditor/EmbeddedRichTextField.d.ts +15 -0
- package/dist/jsonEditor/EmbeddedRichTextField.d.ts.map +1 -0
- package/dist/jsonEditor/EmbeddedRichTextField.js +74 -0
- package/dist/jsonEditor/EmbeddedRichTextField.js.map +1 -0
- package/dist/jsonEditor/JsonEditor.d.ts +36 -0
- package/dist/jsonEditor/JsonEditor.d.ts.map +1 -0
- package/dist/jsonEditor/JsonEditor.js +15 -0
- package/dist/jsonEditor/JsonEditor.js.map +1 -0
- package/dist/jsonEditor/JsonEditorContext.d.ts +28 -0
- package/dist/jsonEditor/JsonEditorContext.d.ts.map +1 -0
- package/dist/jsonEditor/JsonEditorContext.js +41 -0
- package/dist/jsonEditor/JsonEditorContext.js.map +1 -0
- package/dist/jsonEditor/RenderNode.d.ts +16 -0
- package/dist/jsonEditor/RenderNode.d.ts.map +1 -0
- package/dist/jsonEditor/RenderNode.js +32 -0
- package/dist/jsonEditor/RenderNode.js.map +1 -0
- package/dist/jsonEditor/editors.d.ts +36 -0
- package/dist/jsonEditor/editors.d.ts.map +1 -0
- package/dist/jsonEditor/editors.js +347 -0
- package/dist/jsonEditor/editors.js.map +1 -0
- package/dist/jsonEditor/index.d.ts +3 -0
- package/dist/jsonEditor/index.d.ts.map +1 -0
- package/dist/jsonEditor/index.js +2 -0
- package/dist/jsonEditor/index.js.map +1 -0
- package/dist/jsonEditor/useJsonEditorTokens.d.ts +13 -0
- package/dist/jsonEditor/useJsonEditorTokens.d.ts.map +1 -0
- package/dist/jsonEditor/useJsonEditorTokens.js +38 -0
- package/dist/jsonEditor/useJsonEditorTokens.js.map +1 -0
- package/dist/recorder/RecorderButton.d.ts +31 -0
- package/dist/recorder/RecorderButton.d.ts.map +1 -0
- package/dist/recorder/RecorderButton.js +24 -0
- package/dist/recorder/RecorderButton.js.map +1 -0
- package/dist/recorder/RecorderModal.d.ts +59 -0
- package/dist/recorder/RecorderModal.d.ts.map +1 -0
- package/dist/recorder/RecorderModal.js +333 -0
- package/dist/recorder/RecorderModal.js.map +1 -0
- package/dist/recorder/RecorderPanel.d.ts +25 -0
- package/dist/recorder/RecorderPanel.d.ts.map +1 -0
- package/dist/recorder/RecorderPanel.js +30 -0
- package/dist/recorder/RecorderPanel.js.map +1 -0
- package/dist/recorder/formats.d.ts +51 -0
- package/dist/recorder/formats.d.ts.map +1 -0
- package/dist/recorder/formats.js +144 -0
- package/dist/recorder/formats.js.map +1 -0
- package/dist/recorder/hooks/useMediaRecorder.d.ts +90 -0
- package/dist/recorder/hooks/useMediaRecorder.d.ts.map +1 -0
- package/dist/recorder/hooks/useMediaRecorder.js +277 -0
- package/dist/recorder/hooks/useMediaRecorder.js.map +1 -0
- package/dist/recorder/hooks/useStreamPreview.d.ts +22 -0
- package/dist/recorder/hooks/useStreamPreview.d.ts.map +1 -0
- package/dist/recorder/hooks/useStreamPreview.js +44 -0
- package/dist/recorder/hooks/useStreamPreview.js.map +1 -0
- package/dist/recorder/sources/cameraStream.d.ts +22 -0
- package/dist/recorder/sources/cameraStream.d.ts.map +1 -0
- package/dist/recorder/sources/cameraStream.js +24 -0
- package/dist/recorder/sources/cameraStream.js.map +1 -0
- package/dist/recorder/sources/micStream.d.ts +15 -0
- package/dist/recorder/sources/micStream.d.ts.map +1 -0
- package/dist/recorder/sources/micStream.js +24 -0
- package/dist/recorder/sources/micStream.js.map +1 -0
- package/dist/recorder/sources/screenStream.d.ts +53 -0
- package/dist/recorder/sources/screenStream.d.ts.map +1 -0
- package/dist/recorder/sources/screenStream.js +114 -0
- package/dist/recorder/sources/screenStream.js.map +1 -0
- package/dist/recorder/timingJson.d.ts +51 -0
- package/dist/recorder/timingJson.d.ts.map +1 -0
- package/dist/recorder/timingJson.js +42 -0
- package/dist/recorder/timingJson.js.map +1 -0
- package/dist/tiptap/TiptapAudio.d.ts +26 -0
- package/dist/tiptap/TiptapAudio.d.ts.map +1 -0
- package/dist/tiptap/TiptapAudio.js +58 -0
- package/dist/tiptap/TiptapAudio.js.map +1 -0
- package/dist/tiptap/TiptapVideo.d.ts +30 -0
- package/dist/tiptap/TiptapVideo.d.ts.map +1 -0
- package/dist/tiptap/TiptapVideo.js +66 -0
- package/dist/tiptap/TiptapVideo.js.map +1 -0
- package/dist/tiptap/useResolvedMediaSrc.d.ts +2 -0
- package/dist/tiptap/useResolvedMediaSrc.d.ts.map +1 -0
- package/dist/tiptap/useResolvedMediaSrc.js +42 -0
- package/dist/tiptap/useResolvedMediaSrc.js.map +1 -0
- package/dist/tiptapBridge.d.ts.map +1 -1
- package/dist/tiptapBridge.js +171 -14
- package/dist/tiptapBridge.js.map +1 -1
- package/dist/useHeadingLayout.d.ts +54 -0
- package/dist/useHeadingLayout.d.ts.map +1 -0
- package/dist/useHeadingLayout.js +260 -0
- package/dist/useHeadingLayout.js.map +1 -0
- package/dist/utils/collectInlineFontAwesomeCss.d.ts +21 -0
- package/dist/utils/collectInlineFontAwesomeCss.d.ts.map +1 -0
- package/dist/utils/collectInlineFontAwesomeCss.js +68 -0
- package/dist/utils/collectInlineFontAwesomeCss.js.map +1 -0
- package/dist/utils/dropUtils.d.ts +21 -2
- package/dist/utils/dropUtils.d.ts.map +1 -1
- package/dist/utils/dropUtils.js +43 -4
- package/dist/utils/dropUtils.js.map +1 -1
- package/dist/utils/normalizeMalformedAssetUrl.d.ts +15 -0
- package/dist/utils/normalizeMalformedAssetUrl.d.ts.map +1 -0
- package/dist/utils/normalizeMalformedAssetUrl.js +27 -0
- package/dist/utils/normalizeMalformedAssetUrl.js.map +1 -0
- package/package.json +8 -5
- package/src/DocumentSettingsDialog.tsx +266 -0
- package/src/EditorContext.tsx +534 -10
- package/src/EditorShell.tsx +571 -55
- package/src/EmojiPicker.tsx +332 -0
- package/src/ImageEditor.tsx +327 -0
- package/src/ImageNodeView.tsx +222 -21
- package/src/ImageViewer.tsx +221 -0
- package/src/InlineIcon.ts +84 -0
- package/src/InlinePreviewGutter.tsx +582 -0
- package/src/LinkDialog.tsx +276 -0
- package/src/MentionExtension.tsx +10 -7
- package/src/OutlinePanel.tsx +295 -0
- package/src/PlainHtmlPreview.tsx +211 -0
- package/src/PreviewControls.tsx +130 -24
- package/src/PreviewPanel.tsx +38 -21
- package/src/RawEditor.tsx +215 -4
- package/src/RecorderEntry.tsx +164 -0
- package/src/TemplateAnnotation.ts +32 -6
- package/src/TemplatePicker.tsx +818 -0
- package/src/ThemeCustomizerPanel.tsx +595 -0
- package/src/ThemePicker.tsx +319 -0
- package/src/Toolbar.tsx +708 -111
- package/src/VersionHistoryPanel.tsx +329 -0
- package/src/ViewMenuPanel.tsx +188 -0
- package/src/WysiwygEditor.tsx +229 -9
- package/src/__tests__/detectMarkdown.test.ts +0 -15
- package/src/__tests__/documentSettingsDialog.test.tsx +147 -0
- package/src/__tests__/emojiPicker.test.tsx +133 -0
- package/src/__tests__/fileKind.test.ts +16 -0
- package/src/__tests__/imageEditAffordance.test.tsx +268 -0
- package/src/__tests__/imageEditorShell.test.tsx +57 -0
- package/src/__tests__/imageEditorState.test.ts +171 -0
- package/src/__tests__/inlinePreviewGutter.test.tsx +62 -0
- package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +103 -0
- package/src/__tests__/jsonEditor.test.tsx +168 -0
- package/src/__tests__/layersPanel.test.tsx +97 -0
- package/src/__tests__/linkDialogDocPicker.test.tsx +137 -0
- package/src/__tests__/outlinePanel.test.tsx +79 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +107 -0
- package/src/__tests__/propertiesPanel.test.tsx +69 -0
- package/src/__tests__/recorderFormats.test.ts +146 -0
- package/src/__tests__/recorderTimingJson.test.ts +41 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +34 -0
- package/src/__tests__/tiptapBridge.test.ts +15 -0
- package/src/__tests__/useImageEditor.test.tsx +159 -0
- package/src/__tests__/useMediaRecorder.test.ts +186 -0
- package/src/__tests__/versionHistory.test.tsx +197 -0
- package/src/blockSlice.ts +75 -0
- package/src/buildPreviewDoc.ts +61 -6
- package/src/emojiData.ts +1337 -0
- package/src/fileKind.ts +30 -6
- package/src/hooks/useFileDrop.ts +40 -4
- package/src/imageEditor/CanvasSurface.tsx +402 -0
- package/src/imageEditor/ImageVersionHistoryDropdown.tsx +396 -0
- package/src/imageEditor/LayersPanel.tsx +143 -0
- package/src/imageEditor/PropertiesPanel.tsx +428 -0
- package/src/imageEditor/Toolbar.tsx +242 -0
- package/src/imageEditor/icons.tsx +144 -0
- package/src/imageEditor/image-editor.css +450 -0
- package/src/imageEditor/layers/EditorImageLayer.tsx +45 -0
- package/src/imageEditor/layers/EditorShapeLayer.tsx +62 -0
- package/src/imageEditor/layers/EditorTextLayer.tsx +45 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +86 -0
- package/src/imageEditor/state.ts +153 -0
- package/src/imageEditor/useImageEditor.ts +328 -0
- package/src/imageEditor/useImageEditorTokens.ts +70 -0
- package/src/index.ts +82 -0
- package/src/jsonEditor/EmbeddedRichTextField.tsx +81 -0
- package/src/jsonEditor/JsonEditor.tsx +81 -0
- package/src/jsonEditor/JsonEditorContext.tsx +75 -0
- package/src/jsonEditor/RenderNode.tsx +66 -0
- package/src/jsonEditor/editors.tsx +678 -0
- package/src/jsonEditor/index.ts +2 -0
- package/src/jsonEditor/json-editor.css +463 -0
- package/src/jsonEditor/useJsonEditorTokens.ts +63 -0
- package/src/recorder/RecorderButton.tsx +72 -0
- package/src/recorder/RecorderModal.tsx +596 -0
- package/src/recorder/RecorderPanel.tsx +93 -0
- package/src/recorder/formats.ts +159 -0
- package/src/recorder/hooks/useMediaRecorder.ts +378 -0
- package/src/recorder/hooks/useStreamPreview.ts +47 -0
- package/src/recorder/sources/cameraStream.ts +32 -0
- package/src/recorder/sources/micStream.ts +25 -0
- package/src/recorder/sources/screenStream.ts +162 -0
- package/src/recorder/timingJson.ts +66 -0
- package/src/styles/editor.css +2490 -51
- package/src/styles/image-edit-affordance.css +201 -0
- package/src/styles/index.css +10 -0
- package/src/tiptap/TiptapAudio.tsx +86 -0
- package/src/tiptap/TiptapVideo.tsx +119 -0
- package/src/tiptap/useResolvedMediaSrc.ts +47 -0
- package/src/tiptapBridge.ts +188 -20
- package/src/useHeadingLayout.ts +294 -0
- package/src/utils/collectInlineFontAwesomeCss.ts +69 -0
- package/src/utils/dropUtils.ts +54 -6
- package/src/utils/normalizeMalformedAssetUrl.ts +22 -0
package/src/Toolbar.tsx
CHANGED
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ReactNode } from 'react';
|
|
11
|
-
import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
|
|
11
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
12
12
|
import type { Editor as TiptapEditor } from '@tiptap/core';
|
|
13
|
+
import type { IRange } from 'monaco-editor';
|
|
13
14
|
import { useEditorContext, type EditorView } from './EditorContext';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
import { VersionHistoryPanel } from './VersionHistoryPanel';
|
|
16
|
+
import { RecorderEntry } from './RecorderEntry';
|
|
17
|
+
import { ViewMenuPanel } from './ViewMenuPanel';
|
|
18
|
+
import { TemplatePicker, TEMPLATE_NAMES } from './TemplatePicker';
|
|
19
|
+
import { profileBlockContents, recommendTemplatesForBlock } from '@bendyline/squisq/recommend';
|
|
20
|
+
import { findBlockSliceAtLine, findBlockSliceByHeadingIndex } from './blockSlice';
|
|
21
|
+
import { LinkDialog } from './LinkDialog';
|
|
22
|
+
import { DocumentSettingsDialog } from './DocumentSettingsDialog';
|
|
23
|
+
import { EmojiPicker, EMOJI_PICKER_WIDTH, EMOJI_PICKER_MAX_HEIGHT } from './EmojiPicker';
|
|
24
|
+
import type { PickerEntry } from './emojiData';
|
|
25
|
+
import { createPortal } from 'react-dom';
|
|
18
26
|
|
|
19
27
|
const VIEWS: { id: EditorView; label: string; shortLabel?: string; shortcut: string }[] = [
|
|
20
28
|
{ id: 'wysiwyg', label: 'Editor', shortcut: '⌘1' },
|
|
@@ -89,6 +97,9 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
89
97
|
{ id: 'h1', label: 'H1', icon: 'H1', title: 'Heading 1', group: 'structure' },
|
|
90
98
|
{ id: 'h2', label: 'H2', icon: 'H2', title: 'Heading 2', group: 'structure' },
|
|
91
99
|
{ id: 'h3', label: 'H3', icon: 'H3', title: 'Heading 3', group: 'structure' },
|
|
100
|
+
{ id: 'h4', label: 'H4', icon: 'H4', title: 'Heading 4', group: 'structure' },
|
|
101
|
+
{ id: 'h5', label: 'H5', icon: 'H5', title: 'Heading 5', group: 'structure' },
|
|
102
|
+
{ id: 'h6', label: 'H6', icon: 'H6', title: 'Heading 6', group: 'structure' },
|
|
92
103
|
|
|
93
104
|
// Insert group — block-level inserts (quote, code blocks, rules)
|
|
94
105
|
{ id: 'quote', label: '❝', icon: '❝', title: 'Blockquote', group: 'insert' },
|
|
@@ -96,12 +107,116 @@ const BUTTONS: ToolbarButton[] = [
|
|
|
96
107
|
{ id: 'code', label: '</>', icon: '</>', title: 'Inline code', group: 'insert' },
|
|
97
108
|
{ id: 'hr', label: '—', icon: '—', title: 'Horizontal rule', group: 'insert' },
|
|
98
109
|
|
|
99
|
-
// Media group — links, tables, images
|
|
110
|
+
// Media group — links, tables, images, emoji
|
|
100
111
|
{ id: 'link', label: '🔗', icon: '🔗', title: 'Insert link', group: 'media' },
|
|
101
112
|
{ id: 'table', label: 'table', icon: '', title: 'Insert table', group: 'media' },
|
|
102
113
|
{ id: 'image', label: '🖼', icon: '🖼', title: 'Insert image', group: 'media' },
|
|
114
|
+
{ id: 'emoji', label: '😊', icon: '😊', title: 'Insert emoji', group: 'media' },
|
|
103
115
|
];
|
|
104
116
|
|
|
117
|
+
// ─── Inline SVG icons (line-art, currentColor) ──────────
|
|
118
|
+
|
|
119
|
+
const TABLE_ICON = (
|
|
120
|
+
<svg
|
|
121
|
+
width="14"
|
|
122
|
+
height="14"
|
|
123
|
+
viewBox="0 0 14 14"
|
|
124
|
+
fill="none"
|
|
125
|
+
stroke="currentColor"
|
|
126
|
+
strokeWidth="1.4"
|
|
127
|
+
strokeLinecap="round"
|
|
128
|
+
>
|
|
129
|
+
<rect x="1" y="1" width="12" height="12" rx="1" />
|
|
130
|
+
<line x1="1" y1="5" x2="13" y2="5" />
|
|
131
|
+
<line x1="1" y1="9" x2="13" y2="9" />
|
|
132
|
+
<line x1="5" y1="1" x2="5" y2="13" />
|
|
133
|
+
<line x1="9" y1="1" x2="9" y2="13" />
|
|
134
|
+
</svg>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const LINK_ICON = (
|
|
138
|
+
<svg
|
|
139
|
+
width="14"
|
|
140
|
+
height="14"
|
|
141
|
+
viewBox="0 0 14 14"
|
|
142
|
+
fill="none"
|
|
143
|
+
stroke="currentColor"
|
|
144
|
+
strokeWidth="1.4"
|
|
145
|
+
strokeLinecap="round"
|
|
146
|
+
strokeLinejoin="round"
|
|
147
|
+
>
|
|
148
|
+
<path d="M5.75 8.25 L8.25 5.75" />
|
|
149
|
+
<path d="M6.5 3.75 L8 2.25 a2.5 2.5 0 0 1 3.54 3.54 L10 7.25" />
|
|
150
|
+
<path d="M7.5 10.25 L6 11.75 a2.5 2.5 0 0 1 -3.54 -3.54 L4 6.75" />
|
|
151
|
+
</svg>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const IMAGE_ICON = (
|
|
155
|
+
<svg
|
|
156
|
+
width="14"
|
|
157
|
+
height="14"
|
|
158
|
+
viewBox="0 0 14 14"
|
|
159
|
+
fill="none"
|
|
160
|
+
stroke="currentColor"
|
|
161
|
+
strokeWidth="1.4"
|
|
162
|
+
strokeLinecap="round"
|
|
163
|
+
strokeLinejoin="round"
|
|
164
|
+
>
|
|
165
|
+
<rect x="1.5" y="2.5" width="11" height="9" rx="1" />
|
|
166
|
+
<circle cx="5" cy="5.5" r="0.9" />
|
|
167
|
+
<path d="M2 10 L5.5 7 L8 9 L10 7.5 L12.5 10" />
|
|
168
|
+
</svg>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const PAPERCLIP_ICON = (
|
|
172
|
+
<svg
|
|
173
|
+
width="14"
|
|
174
|
+
height="14"
|
|
175
|
+
viewBox="0 0 14 14"
|
|
176
|
+
fill="none"
|
|
177
|
+
stroke="currentColor"
|
|
178
|
+
strokeWidth="1.4"
|
|
179
|
+
strokeLinecap="round"
|
|
180
|
+
strokeLinejoin="round"
|
|
181
|
+
>
|
|
182
|
+
<path d="M11 4 L5.5 9.5 a1.75 1.75 0 0 0 2.5 2.5 L12.5 7.5 a3 3 0 0 0 -4.25 -4.25 L3 8.5 a4.25 4.25 0 0 0 6 6 L13 10.5" />
|
|
183
|
+
</svg>
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const EMOJI_ICON = (
|
|
187
|
+
<svg
|
|
188
|
+
width="14"
|
|
189
|
+
height="14"
|
|
190
|
+
viewBox="0 0 14 14"
|
|
191
|
+
fill="none"
|
|
192
|
+
stroke="currentColor"
|
|
193
|
+
strokeWidth="1.4"
|
|
194
|
+
strokeLinecap="round"
|
|
195
|
+
strokeLinejoin="round"
|
|
196
|
+
>
|
|
197
|
+
<circle cx="7" cy="7" r="5.25" />
|
|
198
|
+
<circle cx="5.25" cy="5.75" r="0.6" fill="currentColor" stroke="none" />
|
|
199
|
+
<circle cx="8.75" cy="5.75" r="0.6" fill="currentColor" stroke="none" />
|
|
200
|
+
<path d="M4.75 8.5 a2.5 2.5 0 0 0 4.5 0" />
|
|
201
|
+
</svg>
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
/** Returns an SVG element when the button id maps to one, otherwise null. */
|
|
205
|
+
function buttonIconSvg(id: string): React.ReactNode | null {
|
|
206
|
+
switch (id) {
|
|
207
|
+
case 'table':
|
|
208
|
+
return TABLE_ICON;
|
|
209
|
+
case 'link':
|
|
210
|
+
return LINK_ICON;
|
|
211
|
+
case 'image':
|
|
212
|
+
return IMAGE_ICON;
|
|
213
|
+
case 'emoji':
|
|
214
|
+
return EMOJI_ICON;
|
|
215
|
+
default:
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
105
220
|
// ─── Tiptap active-state map ────────────────────────────
|
|
106
221
|
|
|
107
222
|
/** Returns true if the given button id is currently active in Tiptap */
|
|
@@ -122,6 +237,12 @@ function isTiptapActive(editor: TiptapEditor, id: string): boolean {
|
|
|
122
237
|
return editor.isActive('heading', { level: 2 });
|
|
123
238
|
case 'h3':
|
|
124
239
|
return editor.isActive('heading', { level: 3 });
|
|
240
|
+
case 'h4':
|
|
241
|
+
return editor.isActive('heading', { level: 4 });
|
|
242
|
+
case 'h5':
|
|
243
|
+
return editor.isActive('heading', { level: 5 });
|
|
244
|
+
case 'h6':
|
|
245
|
+
return editor.isActive('heading', { level: 6 });
|
|
125
246
|
case 'quote':
|
|
126
247
|
return editor.isActive('blockquote');
|
|
127
248
|
case 'ul':
|
|
@@ -158,6 +279,10 @@ export function Toolbar({
|
|
|
158
279
|
monacoEditor,
|
|
159
280
|
mediaProvider,
|
|
160
281
|
editorMode,
|
|
282
|
+
versioning,
|
|
283
|
+
allowRecording,
|
|
284
|
+
documentLinkProvider,
|
|
285
|
+
theme,
|
|
161
286
|
} = useEditorContext();
|
|
162
287
|
const isCodeMode = editorMode === 'code';
|
|
163
288
|
// In code mode only the raw view is meaningful; the WYSIWYG and Preview
|
|
@@ -172,6 +297,56 @@ export function Toolbar({
|
|
|
172
297
|
// Hidden file input for image picker
|
|
173
298
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
|
174
299
|
|
|
300
|
+
// Link dialog — shared by WYSIWYG and Raw views.
|
|
301
|
+
const [linkDialog, setLinkDialog] = useState<{
|
|
302
|
+
mode: 'insert' | 'update';
|
|
303
|
+
target: 'wysiwyg' | 'raw';
|
|
304
|
+
initialText: string;
|
|
305
|
+
initialUrl: string;
|
|
306
|
+
/** For target='raw': the range to replace when editing an existing
|
|
307
|
+
* [text](url) under the cursor. Null means use the current Monaco
|
|
308
|
+
* selection (insert at cursor / wrap selection). */
|
|
309
|
+
rawRange: IRange | null;
|
|
310
|
+
} | null>(null);
|
|
311
|
+
|
|
312
|
+
// Emoji picker — toolbar-anchored popover. We track the trigger
|
|
313
|
+
// button's screen rect so the picker can position itself just below
|
|
314
|
+
// it via createPortal (the toolbar's overflow:hidden actions row
|
|
315
|
+
// would otherwise clip the popover).
|
|
316
|
+
const emojiButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
317
|
+
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<{
|
|
318
|
+
top: number;
|
|
319
|
+
left: number;
|
|
320
|
+
} | null>(null);
|
|
321
|
+
|
|
322
|
+
const openEmojiPicker = useCallback(() => {
|
|
323
|
+
const btn = emojiButtonRef.current;
|
|
324
|
+
if (!btn) return;
|
|
325
|
+
const rect = btn.getBoundingClientRect();
|
|
326
|
+
// Position just below the trigger by default, then clamp into the
|
|
327
|
+
// visible viewport so the picker is never clipped on the right or
|
|
328
|
+
// bottom — flips above the trigger when there isn't room below.
|
|
329
|
+
const gap = 6;
|
|
330
|
+
const margin = 8;
|
|
331
|
+
const vw = window.innerWidth;
|
|
332
|
+
const vh = window.innerHeight;
|
|
333
|
+
let left = rect.left;
|
|
334
|
+
if (left + EMOJI_PICKER_WIDTH + margin > vw) {
|
|
335
|
+
left = Math.max(margin, vw - EMOJI_PICKER_WIDTH - margin);
|
|
336
|
+
}
|
|
337
|
+
let top = rect.bottom + gap;
|
|
338
|
+
if (top + EMOJI_PICKER_MAX_HEIGHT + margin > vh) {
|
|
339
|
+
const flipped = rect.top - EMOJI_PICKER_MAX_HEIGHT - gap;
|
|
340
|
+
// Prefer flipping above when there's more room there; otherwise
|
|
341
|
+
// pin to the top edge with margin and let the picker's own
|
|
342
|
+
// maxHeight clip it.
|
|
343
|
+
top = flipped >= margin ? flipped : margin;
|
|
344
|
+
}
|
|
345
|
+
setEmojiPickerAnchor({ top, left });
|
|
346
|
+
}, []);
|
|
347
|
+
|
|
348
|
+
const closeEmojiPicker = useCallback(() => setEmojiPickerAnchor(null), []);
|
|
349
|
+
|
|
175
350
|
// ── Narrow-screen detection ──────────────────────────
|
|
176
351
|
const [isNarrow, setIsNarrow] = useState(
|
|
177
352
|
() => typeof window !== 'undefined' && window.matchMedia('(max-width: 768px)').matches,
|
|
@@ -189,6 +364,9 @@ export function Toolbar({
|
|
|
189
364
|
const [showOverflow, setShowOverflow] = useState(false);
|
|
190
365
|
const overflowRef = useRef<HTMLDivElement>(null);
|
|
191
366
|
|
|
367
|
+
// Document settings (frontmatter) dialog
|
|
368
|
+
const [showDocSettings, setShowDocSettings] = useState(false);
|
|
369
|
+
|
|
192
370
|
// On narrow screens, force all buttons into the overflow menu
|
|
193
371
|
const overflowIndex = isNarrow ? 0 : measuredOverflowIndex;
|
|
194
372
|
|
|
@@ -294,6 +472,15 @@ export function Toolbar({
|
|
|
294
472
|
case 'h3':
|
|
295
473
|
chain.toggleHeading({ level: 3 }).run();
|
|
296
474
|
break;
|
|
475
|
+
case 'h4':
|
|
476
|
+
chain.toggleHeading({ level: 4 }).run();
|
|
477
|
+
break;
|
|
478
|
+
case 'h5':
|
|
479
|
+
chain.toggleHeading({ level: 5 }).run();
|
|
480
|
+
break;
|
|
481
|
+
case 'h6':
|
|
482
|
+
chain.toggleHeading({ level: 6 }).run();
|
|
483
|
+
break;
|
|
297
484
|
case 'quote':
|
|
298
485
|
chain.toggleBlockquote().run();
|
|
299
486
|
break;
|
|
@@ -310,12 +497,29 @@ export function Toolbar({
|
|
|
310
497
|
chain.setHorizontalRule().run();
|
|
311
498
|
break;
|
|
312
499
|
case 'link': {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
500
|
+
const isActive = tiptapEditor.isActive('link');
|
|
501
|
+
let initialText = '';
|
|
502
|
+
let initialUrl = '';
|
|
503
|
+
if (isActive) {
|
|
504
|
+
// Snap selection to the full link mark so editing replaces
|
|
505
|
+
// the entire `[text](url)` rather than just the cursor word.
|
|
506
|
+
tiptapEditor.chain().focus().extendMarkRange('link').run();
|
|
507
|
+
const sel = tiptapEditor.state.selection;
|
|
508
|
+
initialText = tiptapEditor.state.doc.textBetween(sel.from, sel.to, ' ');
|
|
509
|
+
initialUrl = (tiptapEditor.getAttributes('link') as { href?: string }).href ?? '';
|
|
510
|
+
} else {
|
|
511
|
+
const { from, to, empty } = tiptapEditor.state.selection;
|
|
512
|
+
if (!empty) {
|
|
513
|
+
initialText = tiptapEditor.state.doc.textBetween(from, to, ' ');
|
|
514
|
+
}
|
|
318
515
|
}
|
|
516
|
+
setLinkDialog({
|
|
517
|
+
mode: isActive ? 'update' : 'insert',
|
|
518
|
+
target: 'wysiwyg',
|
|
519
|
+
initialText,
|
|
520
|
+
initialUrl,
|
|
521
|
+
rawRange: null,
|
|
522
|
+
});
|
|
319
523
|
break;
|
|
320
524
|
}
|
|
321
525
|
case 'table':
|
|
@@ -387,6 +591,15 @@ export function Toolbar({
|
|
|
387
591
|
case 'h3':
|
|
388
592
|
prefixLines('### ', 'Heading 3');
|
|
389
593
|
break;
|
|
594
|
+
case 'h4':
|
|
595
|
+
prefixLines('#### ', 'Heading 4');
|
|
596
|
+
break;
|
|
597
|
+
case 'h5':
|
|
598
|
+
prefixLines('##### ', 'Heading 5');
|
|
599
|
+
break;
|
|
600
|
+
case 'h6':
|
|
601
|
+
prefixLines('###### ', 'Heading 6');
|
|
602
|
+
break;
|
|
390
603
|
case 'quote':
|
|
391
604
|
prefixLines('> ', 'Quote');
|
|
392
605
|
break;
|
|
@@ -407,13 +620,42 @@ export function Toolbar({
|
|
|
407
620
|
break;
|
|
408
621
|
}
|
|
409
622
|
case 'link': {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
623
|
+
// Open the LinkDialog instead of inserting literal text. If the
|
|
624
|
+
// cursor sits inside an existing `[text](url)` on this line,
|
|
625
|
+
// prefill from it and replace the whole match on confirm.
|
|
626
|
+
const lineNumber = selection.startLineNumber;
|
|
627
|
+
const lineText = model.getLineContent(lineNumber);
|
|
628
|
+
const cursorCol = selection.startColumn;
|
|
629
|
+
const linkRe = /\[([^\]]*)\]\(([^)]*)\)/g;
|
|
630
|
+
let match: RegExpExecArray | null;
|
|
631
|
+
let existing: { text: string; url: string; range: IRange } | null = null;
|
|
632
|
+
while ((match = linkRe.exec(lineText)) !== null) {
|
|
633
|
+
const startCol = match.index + 1; // 1-based
|
|
634
|
+
const endCol = startCol + match[0].length;
|
|
635
|
+
if (cursorCol >= startCol && cursorCol <= endCol) {
|
|
636
|
+
existing = {
|
|
637
|
+
text: match[1],
|
|
638
|
+
url: match[2],
|
|
639
|
+
range: {
|
|
640
|
+
startLineNumber: lineNumber,
|
|
641
|
+
startColumn: startCol,
|
|
642
|
+
endLineNumber: lineNumber,
|
|
643
|
+
endColumn: endCol,
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
415
648
|
}
|
|
416
|
-
|
|
649
|
+
setLinkDialog({
|
|
650
|
+
mode: existing ? 'update' : 'insert',
|
|
651
|
+
target: 'raw',
|
|
652
|
+
initialText: existing ? existing.text : hasSelection ? selectedText : '',
|
|
653
|
+
initialUrl: existing ? existing.url : '',
|
|
654
|
+
rawRange: existing ? existing.range : null,
|
|
655
|
+
});
|
|
656
|
+
// Skip the executeEdits/setPosition tail below — the dialog will
|
|
657
|
+
// apply its own edit on confirm.
|
|
658
|
+
return;
|
|
417
659
|
}
|
|
418
660
|
case 'table': {
|
|
419
661
|
const tpl =
|
|
@@ -525,33 +767,344 @@ export function Toolbar({
|
|
|
525
767
|
imageInputRef.current?.click();
|
|
526
768
|
return;
|
|
527
769
|
}
|
|
770
|
+
if (id === 'emoji') {
|
|
771
|
+
// Toggle the popover: clicking the button again closes it.
|
|
772
|
+
if (emojiPickerAnchor) closeEmojiPicker();
|
|
773
|
+
else openEmojiPicker();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
528
776
|
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
529
777
|
handleTiptap(id);
|
|
530
778
|
} else {
|
|
531
779
|
handleRaw(id);
|
|
532
780
|
}
|
|
533
781
|
},
|
|
534
|
-
[
|
|
782
|
+
[
|
|
783
|
+
activeView,
|
|
784
|
+
tiptapEditor,
|
|
785
|
+
handleTiptap,
|
|
786
|
+
handleRaw,
|
|
787
|
+
emojiPickerAnchor,
|
|
788
|
+
openEmojiPicker,
|
|
789
|
+
closeEmojiPicker,
|
|
790
|
+
],
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
// ── Picker insert (emoji or FontAwesome icon) ──────
|
|
794
|
+
// Inserts a chosen picker entry at the cursor. We bypass
|
|
795
|
+
// `insertAtCursor` (which routes through markdown→Tiptap conversion
|
|
796
|
+
// and wraps the input in a paragraph) so entries land inline at the
|
|
797
|
+
// caret rather than starting a new block. Emoji insert as a plain
|
|
798
|
+
// character; FontAwesome icons insert as the `InlineIcon` Tiptap
|
|
799
|
+
// node so the editor renders them inline immediately.
|
|
800
|
+
const handleEmojiSelect = useCallback(
|
|
801
|
+
(entry: PickerEntry) => {
|
|
802
|
+
if (activeView === 'wysiwyg' && tiptapEditor) {
|
|
803
|
+
if (entry.kind === 'emoji') {
|
|
804
|
+
tiptapEditor.chain().focus().insertContent(entry.char).run();
|
|
805
|
+
} else {
|
|
806
|
+
tiptapEditor
|
|
807
|
+
.chain()
|
|
808
|
+
.focus()
|
|
809
|
+
.insertContent({
|
|
810
|
+
type: 'inlineIcon',
|
|
811
|
+
attrs: { token: entry.token, family: entry.family, name: entry.name },
|
|
812
|
+
})
|
|
813
|
+
.run();
|
|
814
|
+
}
|
|
815
|
+
} else if (activeView === 'raw' && monacoEditor) {
|
|
816
|
+
const insertion = entry.kind === 'emoji' ? entry.char : `{[${entry.token}]}`;
|
|
817
|
+
const position = monacoEditor.getPosition();
|
|
818
|
+
if (position) {
|
|
819
|
+
const range = {
|
|
820
|
+
startLineNumber: position.lineNumber,
|
|
821
|
+
startColumn: position.column,
|
|
822
|
+
endLineNumber: position.lineNumber,
|
|
823
|
+
endColumn: position.column,
|
|
824
|
+
};
|
|
825
|
+
monacoEditor.executeEdits('picker-insert', [{ range, text: insertion }]);
|
|
826
|
+
monacoEditor.focus();
|
|
827
|
+
} else {
|
|
828
|
+
setMarkdownSource(markdownSource + insertion);
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
const insertion = entry.kind === 'emoji' ? entry.char : `{[${entry.token}]}`;
|
|
832
|
+
setMarkdownSource(markdownSource + insertion);
|
|
833
|
+
}
|
|
834
|
+
closeEmojiPicker();
|
|
835
|
+
},
|
|
836
|
+
[activeView, tiptapEditor, monacoEditor, markdownSource, setMarkdownSource, closeEmojiPicker],
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
// ── Ctrl+K / Cmd+K → open the link dialog ────────────
|
|
840
|
+
// Mirrors the behaviour of common editors (Word, Google Docs, VS Code's
|
|
841
|
+
// Markdown preview): if the cursor is in a Squisq editor surface, the
|
|
842
|
+
// shortcut routes through the same handler the toolbar Link button uses,
|
|
843
|
+
// which prefills the dialog from the current selection (or the link
|
|
844
|
+
// under the cursor) before opening.
|
|
845
|
+
useEffect(() => {
|
|
846
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
847
|
+
if (!(e.ctrlKey || e.metaKey) || e.altKey || e.shiftKey) return;
|
|
848
|
+
if (e.key.toLowerCase() !== 'k') return;
|
|
849
|
+
const target = e.target as HTMLElement | null;
|
|
850
|
+
if (!target) return;
|
|
851
|
+
// Only intercept when focus is inside one of our editor surfaces.
|
|
852
|
+
const inEditor = !!target.closest(
|
|
853
|
+
'.squisq-wysiwyg-editor, .ProseMirror, .squisq-raw-editor-container, .monaco-editor',
|
|
854
|
+
);
|
|
855
|
+
if (!inEditor) return;
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
e.stopPropagation();
|
|
858
|
+
handleAction('link');
|
|
859
|
+
};
|
|
860
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
861
|
+
return () => window.removeEventListener('keydown', onKeyDown, true);
|
|
862
|
+
}, [handleAction]);
|
|
863
|
+
|
|
864
|
+
// ── Link dialog confirm ──────────────────────────────
|
|
865
|
+
const handleLinkConfirm = useCallback(
|
|
866
|
+
(text: string, url: string) => {
|
|
867
|
+
if (!linkDialog) return;
|
|
868
|
+
const trimmedUrl = url.trim();
|
|
869
|
+
const trimmedText = text.trim();
|
|
870
|
+
|
|
871
|
+
if (linkDialog.target === 'wysiwyg' && tiptapEditor) {
|
|
872
|
+
if (!trimmedUrl) {
|
|
873
|
+
// Empty URL on update = unlink. On insert with no URL, do nothing.
|
|
874
|
+
if (linkDialog.mode === 'update') {
|
|
875
|
+
tiptapEditor.chain().focus().unsetLink().run();
|
|
876
|
+
}
|
|
877
|
+
setLinkDialog(null);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const visibleText = trimmedText || trimmedUrl;
|
|
881
|
+
const chain = tiptapEditor.chain().focus();
|
|
882
|
+
// Insert (or replace selection) with text carrying a link mark. When
|
|
883
|
+
// updating an existing link, the selection was extended to the full
|
|
884
|
+
// mark range earlier, so this replaces the entire `[text](url)`.
|
|
885
|
+
chain
|
|
886
|
+
.insertContent({
|
|
887
|
+
type: 'text',
|
|
888
|
+
text: visibleText,
|
|
889
|
+
marks: [{ type: 'link', attrs: { href: trimmedUrl } }],
|
|
890
|
+
})
|
|
891
|
+
.run();
|
|
892
|
+
setLinkDialog(null);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (linkDialog.target === 'raw' && monacoEditor) {
|
|
897
|
+
const model = monacoEditor.getModel();
|
|
898
|
+
if (!model) {
|
|
899
|
+
setLinkDialog(null);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (!trimmedUrl && linkDialog.mode === 'update' && linkDialog.rawRange) {
|
|
903
|
+
// Empty URL on update = strip the markdown link, keep the text.
|
|
904
|
+
monacoEditor.executeEdits('toolbar-link-edit', [
|
|
905
|
+
{ range: linkDialog.rawRange, text: trimmedText || linkDialog.initialText },
|
|
906
|
+
]);
|
|
907
|
+
monacoEditor.focus();
|
|
908
|
+
setLinkDialog(null);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (!trimmedUrl) {
|
|
912
|
+
setLinkDialog(null);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const visibleText = trimmedText || trimmedUrl;
|
|
916
|
+
const replacement = `[${visibleText}](${trimmedUrl})`;
|
|
917
|
+
const range = linkDialog.rawRange ?? monacoEditor.getSelection();
|
|
918
|
+
if (!range) {
|
|
919
|
+
setLinkDialog(null);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
monacoEditor.executeEdits('toolbar-link-edit', [{ range, text: replacement }]);
|
|
923
|
+
monacoEditor.focus();
|
|
924
|
+
setLinkDialog(null);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
setLinkDialog(null);
|
|
929
|
+
},
|
|
930
|
+
[linkDialog, tiptapEditor, monacoEditor],
|
|
535
931
|
);
|
|
536
932
|
|
|
537
933
|
const groups = ['format', 'lists', 'structure', 'insert', 'media'] as const;
|
|
538
934
|
const isWysiwyg = activeView === 'wysiwyg' && tiptapEditor;
|
|
539
935
|
const isPreview = activeView === 'preview';
|
|
540
936
|
|
|
937
|
+
// ── Progressive heading disclosure ───────────────────
|
|
938
|
+
// H1\u2013H3 are always visible. H4 appears once the document already
|
|
939
|
+
// contains an H3, H5 once it contains an H4, and H6 once it contains
|
|
940
|
+
// an H5. This keeps the toolbar compact for typical short documents
|
|
941
|
+
// while letting deeply nested documents reach every level.
|
|
942
|
+
const maxHeadingLevelInDoc = useMemo(() => {
|
|
943
|
+
if (!markdownSource) return 0;
|
|
944
|
+
let max = 0;
|
|
945
|
+
let inFence = false;
|
|
946
|
+
for (const rawLine of markdownSource.split('\n')) {
|
|
947
|
+
const line = rawLine.trimEnd();
|
|
948
|
+
if (/^\s*```/.test(line)) {
|
|
949
|
+
inFence = !inFence;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (inFence) continue;
|
|
953
|
+
const m = /^(#{1,6})\s+\S/.exec(line);
|
|
954
|
+
if (m && m[1].length > max) max = m[1].length;
|
|
955
|
+
}
|
|
956
|
+
return max;
|
|
957
|
+
}, [markdownSource]);
|
|
958
|
+
// Show H(n+1) when the document already contains H(n), starting from H3.
|
|
959
|
+
const visibleHeadingMax = Math.min(6, Math.max(3, maxHeadingLevelInDoc + 1));
|
|
960
|
+
const isButtonVisible = (id: string): boolean => {
|
|
961
|
+
const m = /^h([1-6])$/.exec(id);
|
|
962
|
+
if (!m) return true;
|
|
963
|
+
return Number(m[1]) <= visibleHeadingMax;
|
|
964
|
+
};
|
|
965
|
+
|
|
541
966
|
// Detect whether cursor is inside a table (WYSIWYG mode only)
|
|
542
967
|
const isInTable = isWysiwyg ? tiptapEditor.isActive('table') : false;
|
|
543
968
|
|
|
544
969
|
// Detect current heading template (WYSIWYG mode only)
|
|
545
|
-
const
|
|
970
|
+
const wysiwygTemplate = isWysiwyg
|
|
546
971
|
? tiptapEditor.isActive('heading')
|
|
547
972
|
? (tiptapEditor.getAttributes('heading')?.dataTemplate ?? '')
|
|
548
973
|
: null
|
|
549
974
|
: null;
|
|
550
975
|
|
|
976
|
+
// ── Monaco heading detection (Markdown view) ─────────────────────
|
|
977
|
+
// Watch the Monaco cursor and surface the template picker whenever the
|
|
978
|
+
// cursor is on a heading line. `null` hides the picker; '' shows it
|
|
979
|
+
// with no template selected; any other string is the current template.
|
|
980
|
+
const isRawView = activeView === 'raw';
|
|
981
|
+
const [rawTemplate, setRawTemplate] = useState<string | null>(null);
|
|
982
|
+
const [rawHeadingLine, setRawHeadingLine] = useState<number | null>(null);
|
|
983
|
+
useEffect(() => {
|
|
984
|
+
if (!isRawView || !monacoEditor) {
|
|
985
|
+
setRawTemplate(null);
|
|
986
|
+
setRawHeadingLine(null);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const recompute = () => {
|
|
990
|
+
const model = monacoEditor.getModel();
|
|
991
|
+
const pos = monacoEditor.getPosition();
|
|
992
|
+
if (!model || !pos) {
|
|
993
|
+
setRawTemplate(null);
|
|
994
|
+
setRawHeadingLine(null);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const line = model.getLineContent(pos.lineNumber);
|
|
998
|
+
const headingMatch = line.match(/^#{1,6}\s+(.+)$/);
|
|
999
|
+
if (!headingMatch) {
|
|
1000
|
+
setRawTemplate(null);
|
|
1001
|
+
setRawHeadingLine(null);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
setRawHeadingLine(pos.lineNumber);
|
|
1005
|
+
const annotMatch = headingMatch[1].match(/\s*\{\[([^\]]+)\]\}[\s\]}]*$/);
|
|
1006
|
+
if (annotMatch) {
|
|
1007
|
+
// First whitespace-delimited token is the template name; the rest are params.
|
|
1008
|
+
const name = annotMatch[1].trim().split(/\s+/)[0];
|
|
1009
|
+
setRawTemplate(name);
|
|
1010
|
+
} else {
|
|
1011
|
+
setRawTemplate('');
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
recompute();
|
|
1015
|
+
const cursorSub = monacoEditor.onDidChangeCursorPosition(recompute);
|
|
1016
|
+
const contentSub = monacoEditor.onDidChangeModelContent(recompute);
|
|
1017
|
+
return () => {
|
|
1018
|
+
cursorSub.dispose();
|
|
1019
|
+
contentSub.dispose();
|
|
1020
|
+
};
|
|
1021
|
+
}, [isRawView, monacoEditor]);
|
|
1022
|
+
|
|
1023
|
+
// Track the index of the heading the WYSIWYG cursor is in among all
|
|
1024
|
+
// top-level headings. Used to locate the same heading in the markdown
|
|
1025
|
+
// source for content-based template recommendations.
|
|
1026
|
+
const [wysiwygHeadingIndex, setWysiwygHeadingIndex] = useState<number | null>(null);
|
|
1027
|
+
useEffect(() => {
|
|
1028
|
+
if (!isWysiwyg || !tiptapEditor) {
|
|
1029
|
+
setWysiwygHeadingIndex(null);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const recompute = () => {
|
|
1033
|
+
if (!tiptapEditor.isActive('heading')) {
|
|
1034
|
+
setWysiwygHeadingIndex(null);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const cursor = tiptapEditor.state.selection.from;
|
|
1038
|
+
let index = -1;
|
|
1039
|
+
let count = 0;
|
|
1040
|
+
tiptapEditor.state.doc.descendants((node, pos) => {
|
|
1041
|
+
if (node.type.name !== 'heading') return;
|
|
1042
|
+
if (pos <= cursor && pos + node.nodeSize > cursor) {
|
|
1043
|
+
index = count;
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
count++;
|
|
1047
|
+
});
|
|
1048
|
+
setWysiwygHeadingIndex(index >= 0 ? index : null);
|
|
1049
|
+
};
|
|
1050
|
+
recompute();
|
|
1051
|
+
tiptapEditor.on('selectionUpdate', recompute);
|
|
1052
|
+
tiptapEditor.on('update', recompute);
|
|
1053
|
+
return () => {
|
|
1054
|
+
tiptapEditor.off('selectionUpdate', recompute);
|
|
1055
|
+
tiptapEditor.off('update', recompute);
|
|
1056
|
+
};
|
|
1057
|
+
}, [isWysiwyg, tiptapEditor]);
|
|
1058
|
+
|
|
1059
|
+
const currentTemplate = isWysiwyg ? wysiwygTemplate : isRawView ? rawTemplate : null;
|
|
1060
|
+
|
|
1061
|
+
// Compute recommended templates for the active block. Heading slice
|
|
1062
|
+
// comes from markdownSource — raw view supplies the cursor line,
|
|
1063
|
+
// WYSIWYG supplies the heading index.
|
|
1064
|
+
const recommendedTemplates = useMemo(() => {
|
|
1065
|
+
if (currentTemplate === null) return undefined;
|
|
1066
|
+
let slice = null;
|
|
1067
|
+
if (isRawView && rawHeadingLine !== null) {
|
|
1068
|
+
slice = findBlockSliceAtLine(markdownSource, rawHeadingLine);
|
|
1069
|
+
} else if (isWysiwyg && wysiwygHeadingIndex !== null) {
|
|
1070
|
+
slice = findBlockSliceByHeadingIndex(markdownSource, wysiwygHeadingIndex);
|
|
1071
|
+
}
|
|
1072
|
+
if (slice === null) return undefined;
|
|
1073
|
+
const profile = profileBlockContents(slice);
|
|
1074
|
+
return recommendTemplatesForBlock(profile, TEMPLATE_NAMES).recommended;
|
|
1075
|
+
}, [currentTemplate, isRawView, isWysiwyg, rawHeadingLine, wysiwygHeadingIndex, markdownSource]);
|
|
1076
|
+
|
|
551
1077
|
const handleTemplatePick = (value: string) => {
|
|
1078
|
+
// Raw (Monaco) — rewrite the heading line's annotation suffix in place.
|
|
1079
|
+
if (isRawView && monacoEditor) {
|
|
1080
|
+
const model = monacoEditor.getModel();
|
|
1081
|
+
const pos = monacoEditor.getPosition();
|
|
1082
|
+
if (!model || !pos) return;
|
|
1083
|
+
const lineNumber = pos.lineNumber;
|
|
1084
|
+
const lineText = model.getLineContent(lineNumber);
|
|
1085
|
+
const headingMatch = lineText.match(/^(#{1,6}\s+)(.+)$/);
|
|
1086
|
+
if (!headingMatch) return;
|
|
1087
|
+
const prefix = headingMatch[1];
|
|
1088
|
+
// Strip any existing trailing annotation
|
|
1089
|
+
const bareText = headingMatch[2].replace(/\s*\{\[[^\]]+\]\}[\s\]}]*$/, '').trimEnd();
|
|
1090
|
+
const newLine = value === '' ? `${prefix}${bareText}` : `${prefix}${bareText} {[${value}]}`;
|
|
1091
|
+
monacoEditor.executeEdits('toolbar-template-pick', [
|
|
1092
|
+
{
|
|
1093
|
+
range: {
|
|
1094
|
+
startLineNumber: lineNumber,
|
|
1095
|
+
startColumn: 1,
|
|
1096
|
+
endLineNumber: lineNumber,
|
|
1097
|
+
endColumn: lineText.length + 1,
|
|
1098
|
+
},
|
|
1099
|
+
text: newLine,
|
|
1100
|
+
},
|
|
1101
|
+
]);
|
|
1102
|
+
monacoEditor.focus();
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
// WYSIWYG — update the heading node attributes.
|
|
552
1106
|
if (!tiptapEditor) return;
|
|
553
1107
|
if (value === '') {
|
|
554
|
-
// Clear template
|
|
555
1108
|
tiptapEditor
|
|
556
1109
|
.chain()
|
|
557
1110
|
.focus()
|
|
@@ -620,12 +1173,18 @@ export function Toolbar({
|
|
|
620
1173
|
{groups.map((group, gi) => (
|
|
621
1174
|
<div key={group} className="squisq-toolbar-group">
|
|
622
1175
|
{gi > 0 && <div className="squisq-toolbar-separator" />}
|
|
623
|
-
{BUTTONS.filter((b) => b.group === group).map((btn) => {
|
|
624
|
-
const active =
|
|
1176
|
+
{BUTTONS.filter((b) => b.group === group && isButtonVisible(b.id)).map((btn) => {
|
|
1177
|
+
const active =
|
|
1178
|
+
btn.id === 'emoji'
|
|
1179
|
+
? emojiPickerAnchor !== null
|
|
1180
|
+
: isWysiwyg
|
|
1181
|
+
? isTiptapActive(tiptapEditor, btn.id)
|
|
1182
|
+
: false;
|
|
625
1183
|
const disabled = btn.id === 'image' && !mediaProvider;
|
|
626
1184
|
return (
|
|
627
1185
|
<button
|
|
628
1186
|
key={btn.id}
|
|
1187
|
+
ref={btn.id === 'emoji' ? emojiButtonRef : undefined}
|
|
629
1188
|
className={`squisq-toolbar-button${active ? ' squisq-toolbar-button--active' : ''}`}
|
|
630
1189
|
data-tooltip={disabled ? 'Insert image (requires media provider)' : btn.title}
|
|
631
1190
|
onClick={() => handleAction(btn.id)}
|
|
@@ -634,54 +1193,25 @@ export function Toolbar({
|
|
|
634
1193
|
disabled={disabled}
|
|
635
1194
|
style={btn.iconStyle}
|
|
636
1195
|
>
|
|
637
|
-
{btn.id
|
|
638
|
-
<svg
|
|
639
|
-
width="14"
|
|
640
|
-
height="14"
|
|
641
|
-
viewBox="0 0 14 14"
|
|
642
|
-
fill="none"
|
|
643
|
-
stroke="currentColor"
|
|
644
|
-
strokeWidth="1.4"
|
|
645
|
-
strokeLinecap="round"
|
|
646
|
-
>
|
|
647
|
-
<rect x="1" y="1" width="12" height="12" rx="1" />
|
|
648
|
-
<line x1="1" y1="5" x2="13" y2="5" />
|
|
649
|
-
<line x1="1" y1="9" x2="13" y2="9" />
|
|
650
|
-
<line x1="5" y1="1" x2="5" y2="13" />
|
|
651
|
-
<line x1="9" y1="1" x2="9" y2="13" />
|
|
652
|
-
</svg>
|
|
653
|
-
) : (
|
|
654
|
-
btn.icon
|
|
655
|
-
)}
|
|
1196
|
+
{buttonIconSvg(btn.id) ?? btn.icon}
|
|
656
1197
|
</button>
|
|
657
1198
|
);
|
|
658
1199
|
})}
|
|
659
1200
|
</div>
|
|
660
1201
|
))}
|
|
661
1202
|
|
|
662
|
-
{/* Template picker — visible when cursor is in a heading
|
|
1203
|
+
{/* Template picker — visible when the cursor is in a heading.
|
|
1204
|
+
In WYSIWYG, reads from the heading node's `dataTemplate`; in
|
|
1205
|
+
Markdown view, parses the `{[...]}` suffix on the cursor's line. */}
|
|
663
1206
|
{currentTemplate !== null && (
|
|
664
1207
|
<>
|
|
665
1208
|
<div className="squisq-toolbar-separator" />
|
|
666
1209
|
<div className="squisq-toolbar-group squisq-template-picker">
|
|
667
|
-
<
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
<select
|
|
673
|
-
className="squisq-template-picker-select"
|
|
674
|
-
value={currentTemplate}
|
|
675
|
-
onChange={(e) => handleTemplatePick(e.target.value)}
|
|
676
|
-
>
|
|
677
|
-
<option value="">— none —</option>
|
|
678
|
-
{TEMPLATE_NAMES.map((name) => (
|
|
679
|
-
<option key={name} value={name}>
|
|
680
|
-
{name}
|
|
681
|
-
</option>
|
|
682
|
-
))}
|
|
683
|
-
</select>
|
|
684
|
-
</label>
|
|
1210
|
+
<TemplatePicker
|
|
1211
|
+
value={currentTemplate}
|
|
1212
|
+
onChange={handleTemplatePick}
|
|
1213
|
+
recommended={recommendedTemplates}
|
|
1214
|
+
/>
|
|
685
1215
|
</div>
|
|
686
1216
|
</>
|
|
687
1217
|
)}
|
|
@@ -860,64 +1390,52 @@ export function Toolbar({
|
|
|
860
1390
|
<div
|
|
861
1391
|
className={`squisq-toolbar-overflow-menu squisq-toolbar-overflow-menu--${overflowPlacement}`}
|
|
862
1392
|
>
|
|
863
|
-
{BUTTONS.slice(overflowIndex)
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
<
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
)}
|
|
897
|
-
<span>{btn.title}</span>
|
|
898
|
-
</button>
|
|
899
|
-
);
|
|
900
|
-
})}
|
|
1393
|
+
{BUTTONS.slice(overflowIndex)
|
|
1394
|
+
.filter((b) => isButtonVisible(b.id))
|
|
1395
|
+
.map((btn) => {
|
|
1396
|
+
const active =
|
|
1397
|
+
btn.id === 'emoji'
|
|
1398
|
+
? emojiPickerAnchor !== null
|
|
1399
|
+
: isWysiwyg
|
|
1400
|
+
? isTiptapActive(tiptapEditor, btn.id)
|
|
1401
|
+
: false;
|
|
1402
|
+
const disabled = btn.id === 'image' && !mediaProvider;
|
|
1403
|
+
return (
|
|
1404
|
+
<button
|
|
1405
|
+
key={btn.id}
|
|
1406
|
+
ref={btn.id === 'emoji' ? emojiButtonRef : undefined}
|
|
1407
|
+
className={`squisq-toolbar-overflow-item${active ? ' squisq-toolbar-overflow-item--active' : ''}`}
|
|
1408
|
+
onClick={() => {
|
|
1409
|
+
handleAction(btn.id);
|
|
1410
|
+
// Keep the overflow open when opening the emoji
|
|
1411
|
+
// picker — otherwise its anchor (the overflow
|
|
1412
|
+
// item) unmounts and the popover loses its ref.
|
|
1413
|
+
if (btn.id !== 'emoji') setShowOverflow(false);
|
|
1414
|
+
}}
|
|
1415
|
+
disabled={disabled}
|
|
1416
|
+
>
|
|
1417
|
+
{buttonIconSvg(btn.id) ?? (
|
|
1418
|
+
<span className="squisq-toolbar-overflow-icon" style={btn.iconStyle}>
|
|
1419
|
+
{btn.icon}
|
|
1420
|
+
</span>
|
|
1421
|
+
)}
|
|
1422
|
+
<span>{btn.title}</span>
|
|
1423
|
+
</button>
|
|
1424
|
+
);
|
|
1425
|
+
})}
|
|
901
1426
|
|
|
902
1427
|
{/* Contextual: template picker in overflow */}
|
|
903
1428
|
{currentTemplate !== null && (
|
|
904
1429
|
<div className="squisq-toolbar-overflow-item squisq-toolbar-overflow-template">
|
|
905
1430
|
<span>Template:</span>
|
|
906
|
-
<
|
|
907
|
-
className="squisq-template-picker-select"
|
|
1431
|
+
<TemplatePicker
|
|
908
1432
|
value={currentTemplate}
|
|
909
|
-
onChange={(
|
|
910
|
-
handleTemplatePick(
|
|
1433
|
+
onChange={(v) => {
|
|
1434
|
+
handleTemplatePick(v);
|
|
911
1435
|
setShowOverflow(false);
|
|
912
1436
|
}}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
{TEMPLATE_NAMES.map((name) => (
|
|
916
|
-
<option key={name} value={name}>
|
|
917
|
-
{name}
|
|
918
|
-
</option>
|
|
919
|
-
))}
|
|
920
|
-
</select>
|
|
1437
|
+
recommended={recommendedTemplates}
|
|
1438
|
+
/>
|
|
921
1439
|
</div>
|
|
922
1440
|
)}
|
|
923
1441
|
|
|
@@ -981,6 +1499,41 @@ export function Toolbar({
|
|
|
981
1499
|
{/* Spacer — only needed when the actions container (which has flex:1
|
|
982
1500
|
and already pushes right-side items to the end) isn't rendered. */}
|
|
983
1501
|
{(isPreview || isNarrow || isCodeMode) && <div style={{ flex: 1 }} />}
|
|
1502
|
+
{/* Version history — renders only when the host enabled versioning
|
|
1503
|
+
and a container is wired up. The component owns its own button
|
|
1504
|
+
and popover; we just give it a slot in the toolbar. */}
|
|
1505
|
+
{versioning && !isCodeMode && <VersionHistoryPanel />}
|
|
1506
|
+
{/* Media recorder — surfaces when the host has a mediaProvider
|
|
1507
|
+
and hasn't opted out. RecorderEntry returns null when no
|
|
1508
|
+
provider is wired, so this stays a no-op for hosts that
|
|
1509
|
+
haven't enabled media at all. */}
|
|
1510
|
+
{allowRecording && !isCodeMode && mediaProvider && <RecorderEntry />}
|
|
1511
|
+
{!isCodeMode && (
|
|
1512
|
+
<button
|
|
1513
|
+
type="button"
|
|
1514
|
+
className="squisq-toolbar-button"
|
|
1515
|
+
onClick={() => setShowDocSettings(true)}
|
|
1516
|
+
data-tooltip="Document settings"
|
|
1517
|
+
aria-label="Document settings"
|
|
1518
|
+
>
|
|
1519
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
1520
|
+
<path
|
|
1521
|
+
d="M3 2.5h7l3 3v8a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-10a1 1 0 0 1 1-1Z"
|
|
1522
|
+
stroke="currentColor"
|
|
1523
|
+
strokeWidth="1.3"
|
|
1524
|
+
strokeLinejoin="round"
|
|
1525
|
+
/>
|
|
1526
|
+
<path d="M10 2.5v3h3" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" />
|
|
1527
|
+
<path
|
|
1528
|
+
d="M5 8.5h6M5 11h4"
|
|
1529
|
+
stroke="currentColor"
|
|
1530
|
+
strokeWidth="1.3"
|
|
1531
|
+
strokeLinecap="round"
|
|
1532
|
+
/>
|
|
1533
|
+
</svg>
|
|
1534
|
+
</button>
|
|
1535
|
+
)}
|
|
1536
|
+
{!isCodeMode && <ViewMenuPanel />}
|
|
984
1537
|
{/* Files toggle — visible when callback is provided */}
|
|
985
1538
|
{onToggleFiles && (
|
|
986
1539
|
<button
|
|
@@ -990,11 +1543,55 @@ export function Toolbar({
|
|
|
990
1543
|
aria-pressed={showFiles}
|
|
991
1544
|
aria-label="Toggle Files panel"
|
|
992
1545
|
>
|
|
993
|
-
{
|
|
1546
|
+
{PAPERCLIP_ICON}
|
|
994
1547
|
</button>
|
|
995
1548
|
)}
|
|
996
1549
|
{/* Right slot — rightmost end of toolbar */}
|
|
997
1550
|
{slotRight}
|
|
1551
|
+
|
|
1552
|
+
{/* Document settings (frontmatter) dialog */}
|
|
1553
|
+
{showDocSettings && (
|
|
1554
|
+
<DocumentSettingsDialog
|
|
1555
|
+
markdownSource={markdownSource}
|
|
1556
|
+
onSave={(next) => {
|
|
1557
|
+
setMarkdownSource(next);
|
|
1558
|
+
setShowDocSettings(false);
|
|
1559
|
+
}}
|
|
1560
|
+
onClose={() => setShowDocSettings(false)}
|
|
1561
|
+
/>
|
|
1562
|
+
)}
|
|
1563
|
+
|
|
1564
|
+
{/* Link insert/edit dialog — shared by WYSIWYG and Raw views. */}
|
|
1565
|
+
{linkDialog && (
|
|
1566
|
+
<LinkDialog
|
|
1567
|
+
mode={linkDialog.mode}
|
|
1568
|
+
initialText={linkDialog.initialText}
|
|
1569
|
+
initialUrl={linkDialog.initialUrl}
|
|
1570
|
+
onConfirm={handleLinkConfirm}
|
|
1571
|
+
onClose={() => setLinkDialog(null)}
|
|
1572
|
+
documentLinkProvider={documentLinkProvider}
|
|
1573
|
+
/>
|
|
1574
|
+
)}
|
|
1575
|
+
|
|
1576
|
+
{/* Emoji picker — portaled to the document body so the toolbar's
|
|
1577
|
+
overflow:hidden actions row doesn't clip the popover. Position
|
|
1578
|
+
is computed from the trigger button's screen rect at open. */}
|
|
1579
|
+
{emojiPickerAnchor &&
|
|
1580
|
+
createPortal(
|
|
1581
|
+
<EmojiPicker
|
|
1582
|
+
open
|
|
1583
|
+
onSelect={handleEmojiSelect}
|
|
1584
|
+
onClose={closeEmojiPicker}
|
|
1585
|
+
anchorRef={emojiButtonRef as React.RefObject<HTMLElement>}
|
|
1586
|
+
theme={theme === 'dark' ? 'dark' : 'light'}
|
|
1587
|
+
style={{
|
|
1588
|
+
position: 'fixed',
|
|
1589
|
+
top: emojiPickerAnchor.top,
|
|
1590
|
+
left: emojiPickerAnchor.left,
|
|
1591
|
+
}}
|
|
1592
|
+
/>,
|
|
1593
|
+
document.body,
|
|
1594
|
+
)}
|
|
998
1595
|
</div>
|
|
999
1596
|
);
|
|
1000
1597
|
}
|