@bendyline/squisq-editor-react 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (461) hide show
  1. package/dist/DocumentSettingsDialog.d.ts +26 -0
  2. package/dist/DocumentSettingsDialog.d.ts.map +1 -0
  3. package/dist/DocumentSettingsDialog.js +115 -0
  4. package/dist/DocumentSettingsDialog.js.map +1 -0
  5. package/dist/EditorContext.d.ts +248 -4
  6. package/dist/EditorContext.d.ts.map +1 -1
  7. package/dist/EditorContext.js +248 -10
  8. package/dist/EditorContext.js.map +1 -1
  9. package/dist/EditorShell.d.ts +184 -4
  10. package/dist/EditorShell.d.ts.map +1 -1
  11. package/dist/EditorShell.js +184 -12
  12. package/dist/EditorShell.js.map +1 -1
  13. package/dist/EmojiPicker.d.ts +50 -0
  14. package/dist/EmojiPicker.d.ts.map +1 -0
  15. package/dist/EmojiPicker.js +182 -0
  16. package/dist/EmojiPicker.js.map +1 -0
  17. package/dist/ImageEditor.d.ts +68 -0
  18. package/dist/ImageEditor.d.ts.map +1 -0
  19. package/dist/ImageEditor.js +166 -0
  20. package/dist/ImageEditor.js.map +1 -0
  21. package/dist/ImageNodeView.d.ts +13 -1
  22. package/dist/ImageNodeView.d.ts.map +1 -1
  23. package/dist/ImageNodeView.js +172 -19
  24. package/dist/ImageNodeView.js.map +1 -1
  25. package/dist/ImageViewer.d.ts +26 -0
  26. package/dist/ImageViewer.d.ts.map +1 -0
  27. package/dist/ImageViewer.js +119 -0
  28. package/dist/ImageViewer.js.map +1 -0
  29. package/dist/InlineIcon.d.ts +17 -0
  30. package/dist/InlineIcon.d.ts.map +1 -0
  31. package/dist/InlineIcon.js +72 -0
  32. package/dist/InlineIcon.js.map +1 -0
  33. package/dist/InlinePreviewGutter.d.ts +52 -0
  34. package/dist/InlinePreviewGutter.d.ts.map +1 -0
  35. package/dist/InlinePreviewGutter.js +397 -0
  36. package/dist/InlinePreviewGutter.js.map +1 -0
  37. package/dist/LinkDialog.d.ts +43 -0
  38. package/dist/LinkDialog.d.ts.map +1 -0
  39. package/dist/LinkDialog.js +102 -0
  40. package/dist/LinkDialog.js.map +1 -0
  41. package/dist/MediaBin.d.ts +12 -1
  42. package/dist/MediaBin.d.ts.map +1 -1
  43. package/dist/MediaBin.js +13 -3
  44. package/dist/MediaBin.js.map +1 -1
  45. package/dist/MentionExtension.js +10 -7
  46. package/dist/MentionExtension.js.map +1 -1
  47. package/dist/OutlinePanel.d.ts +17 -0
  48. package/dist/OutlinePanel.d.ts.map +1 -0
  49. package/dist/OutlinePanel.js +167 -0
  50. package/dist/OutlinePanel.js.map +1 -0
  51. package/dist/PlainHtmlPreview.d.ts +50 -0
  52. package/dist/PlainHtmlPreview.d.ts.map +1 -0
  53. package/dist/PlainHtmlPreview.js +155 -0
  54. package/dist/PlainHtmlPreview.js.map +1 -0
  55. package/dist/PreviewControls.d.ts +15 -1
  56. package/dist/PreviewControls.d.ts.map +1 -1
  57. package/dist/PreviewControls.js +75 -18
  58. package/dist/PreviewControls.js.map +1 -1
  59. package/dist/PreviewPanel.d.ts +11 -10
  60. package/dist/PreviewPanel.d.ts.map +1 -1
  61. package/dist/PreviewPanel.js +20 -17
  62. package/dist/PreviewPanel.js.map +1 -1
  63. package/dist/RawEditor.d.ts.map +1 -1
  64. package/dist/RawEditor.js +198 -4
  65. package/dist/RawEditor.js.map +1 -1
  66. package/dist/RecorderEntry.d.ts +24 -0
  67. package/dist/RecorderEntry.d.ts.map +1 -0
  68. package/dist/RecorderEntry.js +139 -0
  69. package/dist/RecorderEntry.js.map +1 -0
  70. package/dist/TemplateAnnotation.d.ts.map +1 -1
  71. package/dist/TemplateAnnotation.js +32 -6
  72. package/dist/TemplateAnnotation.js.map +1 -1
  73. package/dist/TemplatePicker.d.ts +53 -0
  74. package/dist/TemplatePicker.d.ts.map +1 -0
  75. package/dist/TemplatePicker.js +388 -0
  76. package/dist/TemplatePicker.js.map +1 -0
  77. package/dist/ThemeCustomizerPanel.d.ts +32 -0
  78. package/dist/ThemeCustomizerPanel.d.ts.map +1 -0
  79. package/dist/ThemeCustomizerPanel.js +256 -0
  80. package/dist/ThemeCustomizerPanel.js.map +1 -0
  81. package/dist/ThemePicker.d.ts +33 -0
  82. package/dist/ThemePicker.d.ts.map +1 -0
  83. package/dist/ThemePicker.js +148 -0
  84. package/dist/ThemePicker.js.map +1 -0
  85. package/dist/Toolbar.d.ts.map +1 -1
  86. package/dist/Toolbar.js +508 -33
  87. package/dist/Toolbar.js.map +1 -1
  88. package/dist/VersionHistoryPanel.d.ts +14 -0
  89. package/dist/VersionHistoryPanel.d.ts.map +1 -0
  90. package/dist/VersionHistoryPanel.js +147 -0
  91. package/dist/VersionHistoryPanel.js.map +1 -0
  92. package/dist/ViewMenuPanel.d.ts +13 -0
  93. package/dist/ViewMenuPanel.d.ts.map +1 -0
  94. package/dist/ViewMenuPanel.js +58 -0
  95. package/dist/ViewMenuPanel.js.map +1 -0
  96. package/dist/WysiwygEditor.d.ts.map +1 -1
  97. package/dist/WysiwygEditor.js +198 -9
  98. package/dist/WysiwygEditor.js.map +1 -1
  99. package/dist/__tests__/detectMarkdown.test.js +0 -14
  100. package/dist/__tests__/detectMarkdown.test.js.map +1 -1
  101. package/dist/__tests__/documentSettingsDialog.test.d.ts +2 -0
  102. package/dist/__tests__/documentSettingsDialog.test.d.ts.map +1 -0
  103. package/dist/__tests__/documentSettingsDialog.test.js +132 -0
  104. package/dist/__tests__/documentSettingsDialog.test.js.map +1 -0
  105. package/dist/__tests__/emojiPicker.test.d.ts +2 -0
  106. package/dist/__tests__/emojiPicker.test.d.ts.map +1 -0
  107. package/dist/__tests__/emojiPicker.test.js +111 -0
  108. package/dist/__tests__/emojiPicker.test.js.map +1 -0
  109. package/dist/__tests__/fileKind.test.js +13 -0
  110. package/dist/__tests__/fileKind.test.js.map +1 -1
  111. package/dist/__tests__/imageEditAffordance.test.d.ts +2 -0
  112. package/dist/__tests__/imageEditAffordance.test.d.ts.map +1 -0
  113. package/dist/__tests__/imageEditAffordance.test.js +188 -0
  114. package/dist/__tests__/imageEditAffordance.test.js.map +1 -0
  115. package/dist/__tests__/imageEditorShell.test.d.ts +2 -0
  116. package/dist/__tests__/imageEditorShell.test.d.ts.map +1 -0
  117. package/dist/__tests__/imageEditorShell.test.js +52 -0
  118. package/dist/__tests__/imageEditorShell.test.js.map +1 -0
  119. package/dist/__tests__/imageEditorState.test.d.ts +3 -0
  120. package/dist/__tests__/imageEditorState.test.d.ts.map +1 -0
  121. package/dist/__tests__/imageEditorState.test.js +148 -0
  122. package/dist/__tests__/imageEditorState.test.js.map +1 -0
  123. package/dist/__tests__/inlinePreviewGutter.test.d.ts +2 -0
  124. package/dist/__tests__/inlinePreviewGutter.test.d.ts.map +1 -0
  125. package/dist/__tests__/inlinePreviewGutter.test.js +51 -0
  126. package/dist/__tests__/inlinePreviewGutter.test.js.map +1 -0
  127. package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts +2 -0
  128. package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts.map +1 -0
  129. package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js +63 -0
  130. package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js.map +1 -0
  131. package/dist/__tests__/jsonEditor.test.d.ts +2 -0
  132. package/dist/__tests__/jsonEditor.test.d.ts.map +1 -0
  133. package/dist/__tests__/jsonEditor.test.js +134 -0
  134. package/dist/__tests__/jsonEditor.test.js.map +1 -0
  135. package/dist/__tests__/layersPanel.test.d.ts +2 -0
  136. package/dist/__tests__/layersPanel.test.d.ts.map +1 -0
  137. package/dist/__tests__/layersPanel.test.js +84 -0
  138. package/dist/__tests__/layersPanel.test.js.map +1 -0
  139. package/dist/__tests__/linkDialogDocPicker.test.d.ts +7 -0
  140. package/dist/__tests__/linkDialogDocPicker.test.d.ts.map +1 -0
  141. package/dist/__tests__/linkDialogDocPicker.test.js +75 -0
  142. package/dist/__tests__/linkDialogDocPicker.test.js.map +1 -0
  143. package/dist/__tests__/mediaAttachmentFlow.test.d.ts +2 -0
  144. package/dist/__tests__/mediaAttachmentFlow.test.d.ts.map +1 -0
  145. package/dist/__tests__/mediaAttachmentFlow.test.js +99 -0
  146. package/dist/__tests__/mediaAttachmentFlow.test.js.map +1 -0
  147. package/dist/__tests__/outlinePanel.test.d.ts +2 -0
  148. package/dist/__tests__/outlinePanel.test.d.ts.map +1 -0
  149. package/dist/__tests__/outlinePanel.test.js +68 -0
  150. package/dist/__tests__/outlinePanel.test.js.map +1 -0
  151. package/dist/__tests__/plainHtmlPreview.test.d.ts +2 -0
  152. package/dist/__tests__/plainHtmlPreview.test.d.ts.map +1 -0
  153. package/dist/__tests__/plainHtmlPreview.test.js +87 -0
  154. package/dist/__tests__/plainHtmlPreview.test.js.map +1 -0
  155. package/dist/__tests__/propertiesPanel.test.d.ts +2 -0
  156. package/dist/__tests__/propertiesPanel.test.d.ts.map +1 -0
  157. package/dist/__tests__/propertiesPanel.test.js +64 -0
  158. package/dist/__tests__/propertiesPanel.test.js.map +1 -0
  159. package/dist/__tests__/recorderFormats.test.d.ts +2 -0
  160. package/dist/__tests__/recorderFormats.test.d.ts.map +1 -0
  161. package/dist/__tests__/recorderFormats.test.js +121 -0
  162. package/dist/__tests__/recorderFormats.test.js.map +1 -0
  163. package/dist/__tests__/recorderTimingJson.test.d.ts +2 -0
  164. package/dist/__tests__/recorderTimingJson.test.d.ts.map +1 -0
  165. package/dist/__tests__/recorderTimingJson.test.js +37 -0
  166. package/dist/__tests__/recorderTimingJson.test.js.map +1 -0
  167. package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts +2 -0
  168. package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts.map +1 -0
  169. package/dist/__tests__/templateAnnotationRoundTrip.test.js +31 -0
  170. package/dist/__tests__/templateAnnotationRoundTrip.test.js.map +1 -0
  171. package/dist/__tests__/tiptapBridge.test.js +26 -0
  172. package/dist/__tests__/tiptapBridge.test.js.map +1 -1
  173. package/dist/__tests__/tiptapImageRoundTrip.test.d.ts +2 -0
  174. package/dist/__tests__/tiptapImageRoundTrip.test.d.ts.map +1 -0
  175. package/dist/__tests__/tiptapImageRoundTrip.test.js +68 -0
  176. package/dist/__tests__/tiptapImageRoundTrip.test.js.map +1 -0
  177. package/dist/__tests__/useImageEditor.test.d.ts +2 -0
  178. package/dist/__tests__/useImageEditor.test.d.ts.map +1 -0
  179. package/dist/__tests__/useImageEditor.test.js +131 -0
  180. package/dist/__tests__/useImageEditor.test.js.map +1 -0
  181. package/dist/__tests__/useMediaRecorder.test.d.ts +2 -0
  182. package/dist/__tests__/useMediaRecorder.test.d.ts.map +1 -0
  183. package/dist/__tests__/useMediaRecorder.test.js +153 -0
  184. package/dist/__tests__/useMediaRecorder.test.js.map +1 -0
  185. package/dist/__tests__/versionHistory.test.d.ts +2 -0
  186. package/dist/__tests__/versionHistory.test.d.ts.map +1 -0
  187. package/dist/__tests__/versionHistory.test.js +124 -0
  188. package/dist/__tests__/versionHistory.test.js.map +1 -0
  189. package/dist/blockSlice.d.ts +24 -0
  190. package/dist/blockSlice.d.ts.map +1 -0
  191. package/dist/blockSlice.js +63 -0
  192. package/dist/blockSlice.js.map +1 -0
  193. package/dist/buildPreviewDoc.d.ts.map +1 -1
  194. package/dist/buildPreviewDoc.js +52 -2
  195. package/dist/buildPreviewDoc.js.map +1 -1
  196. package/dist/emojiData.d.ts +81 -0
  197. package/dist/emojiData.d.ts.map +1 -0
  198. package/dist/emojiData.js +1283 -0
  199. package/dist/emojiData.js.map +1 -0
  200. package/dist/fileKind.d.ts +6 -2
  201. package/dist/fileKind.d.ts.map +1 -1
  202. package/dist/fileKind.js +25 -4
  203. package/dist/fileKind.js.map +1 -1
  204. package/dist/hooks/useFileDrop.d.ts.map +1 -1
  205. package/dist/hooks/useFileDrop.js +40 -4
  206. package/dist/hooks/useFileDrop.js.map +1 -1
  207. package/dist/imageEditor/CanvasSurface.d.ts +31 -0
  208. package/dist/imageEditor/CanvasSurface.d.ts.map +1 -0
  209. package/dist/imageEditor/CanvasSurface.js +264 -0
  210. package/dist/imageEditor/CanvasSurface.js.map +1 -0
  211. package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts +39 -0
  212. package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts.map +1 -0
  213. package/dist/imageEditor/ImageVersionHistoryDropdown.js +283 -0
  214. package/dist/imageEditor/ImageVersionHistoryDropdown.js.map +1 -0
  215. package/dist/imageEditor/LayersPanel.d.ts +14 -0
  216. package/dist/imageEditor/LayersPanel.d.ts.map +1 -0
  217. package/dist/imageEditor/LayersPanel.js +43 -0
  218. package/dist/imageEditor/LayersPanel.js.map +1 -0
  219. package/dist/imageEditor/PropertiesPanel.d.ts +14 -0
  220. package/dist/imageEditor/PropertiesPanel.d.ts.map +1 -0
  221. package/dist/imageEditor/PropertiesPanel.js +97 -0
  222. package/dist/imageEditor/PropertiesPanel.js.map +1 -0
  223. package/dist/imageEditor/Toolbar.d.ts +30 -0
  224. package/dist/imageEditor/Toolbar.d.ts.map +1 -0
  225. package/dist/imageEditor/Toolbar.js +108 -0
  226. package/dist/imageEditor/Toolbar.js.map +1 -0
  227. package/dist/imageEditor/icons.d.ts +24 -0
  228. package/dist/imageEditor/icons.d.ts.map +1 -0
  229. package/dist/imageEditor/icons.js +45 -0
  230. package/dist/imageEditor/icons.js.map +1 -0
  231. package/dist/imageEditor/layers/EditorImageLayer.d.ts +16 -0
  232. package/dist/imageEditor/layers/EditorImageLayer.d.ts.map +1 -0
  233. package/dist/imageEditor/layers/EditorImageLayer.js +37 -0
  234. package/dist/imageEditor/layers/EditorImageLayer.js.map +1 -0
  235. package/dist/imageEditor/layers/EditorShapeLayer.d.ts +15 -0
  236. package/dist/imageEditor/layers/EditorShapeLayer.d.ts.map +1 -0
  237. package/dist/imageEditor/layers/EditorShapeLayer.js +20 -0
  238. package/dist/imageEditor/layers/EditorShapeLayer.js.map +1 -0
  239. package/dist/imageEditor/layers/EditorTextLayer.d.ts +18 -0
  240. package/dist/imageEditor/layers/EditorTextLayer.d.ts.map +1 -0
  241. package/dist/imageEditor/layers/EditorTextLayer.js +13 -0
  242. package/dist/imageEditor/layers/EditorTextLayer.js.map +1 -0
  243. package/dist/imageEditor/layers/SelectionHandles.d.ts +17 -0
  244. package/dist/imageEditor/layers/SelectionHandles.d.ts.map +1 -0
  245. package/dist/imageEditor/layers/SelectionHandles.js +19 -0
  246. package/dist/imageEditor/layers/SelectionHandles.js.map +1 -0
  247. package/dist/imageEditor/state.d.ts +76 -0
  248. package/dist/imageEditor/state.d.ts.map +1 -0
  249. package/dist/imageEditor/state.js +87 -0
  250. package/dist/imageEditor/state.js.map +1 -0
  251. package/dist/imageEditor/useImageEditor.d.ts +53 -0
  252. package/dist/imageEditor/useImageEditor.d.ts.map +1 -0
  253. package/dist/imageEditor/useImageEditor.js +244 -0
  254. package/dist/imageEditor/useImageEditor.js.map +1 -0
  255. package/dist/imageEditor/useImageEditorTokens.d.ts +16 -0
  256. package/dist/imageEditor/useImageEditorTokens.d.ts.map +1 -0
  257. package/dist/imageEditor/useImageEditorTokens.js +45 -0
  258. package/dist/imageEditor/useImageEditorTokens.js.map +1 -0
  259. package/dist/index.d.ts +48 -1
  260. package/dist/index.d.ts.map +1 -1
  261. package/dist/index.js +36 -0
  262. package/dist/index.js.map +1 -1
  263. package/dist/jsonEditor/EmbeddedRichTextField.d.ts +15 -0
  264. package/dist/jsonEditor/EmbeddedRichTextField.d.ts.map +1 -0
  265. package/dist/jsonEditor/EmbeddedRichTextField.js +74 -0
  266. package/dist/jsonEditor/EmbeddedRichTextField.js.map +1 -0
  267. package/dist/jsonEditor/JsonEditor.d.ts +36 -0
  268. package/dist/jsonEditor/JsonEditor.d.ts.map +1 -0
  269. package/dist/jsonEditor/JsonEditor.js +15 -0
  270. package/dist/jsonEditor/JsonEditor.js.map +1 -0
  271. package/dist/jsonEditor/JsonEditorContext.d.ts +28 -0
  272. package/dist/jsonEditor/JsonEditorContext.d.ts.map +1 -0
  273. package/dist/jsonEditor/JsonEditorContext.js +41 -0
  274. package/dist/jsonEditor/JsonEditorContext.js.map +1 -0
  275. package/dist/jsonEditor/RenderNode.d.ts +16 -0
  276. package/dist/jsonEditor/RenderNode.d.ts.map +1 -0
  277. package/dist/jsonEditor/RenderNode.js +32 -0
  278. package/dist/jsonEditor/RenderNode.js.map +1 -0
  279. package/dist/jsonEditor/editors.d.ts +36 -0
  280. package/dist/jsonEditor/editors.d.ts.map +1 -0
  281. package/dist/jsonEditor/editors.js +347 -0
  282. package/dist/jsonEditor/editors.js.map +1 -0
  283. package/dist/jsonEditor/index.d.ts +3 -0
  284. package/dist/jsonEditor/index.d.ts.map +1 -0
  285. package/dist/jsonEditor/index.js +2 -0
  286. package/dist/jsonEditor/index.js.map +1 -0
  287. package/dist/jsonEditor/useJsonEditorTokens.d.ts +13 -0
  288. package/dist/jsonEditor/useJsonEditorTokens.d.ts.map +1 -0
  289. package/dist/jsonEditor/useJsonEditorTokens.js +38 -0
  290. package/dist/jsonEditor/useJsonEditorTokens.js.map +1 -0
  291. package/dist/recorder/RecorderButton.d.ts +31 -0
  292. package/dist/recorder/RecorderButton.d.ts.map +1 -0
  293. package/dist/recorder/RecorderButton.js +24 -0
  294. package/dist/recorder/RecorderButton.js.map +1 -0
  295. package/dist/recorder/RecorderModal.d.ts +59 -0
  296. package/dist/recorder/RecorderModal.d.ts.map +1 -0
  297. package/dist/recorder/RecorderModal.js +333 -0
  298. package/dist/recorder/RecorderModal.js.map +1 -0
  299. package/dist/recorder/RecorderPanel.d.ts +25 -0
  300. package/dist/recorder/RecorderPanel.d.ts.map +1 -0
  301. package/dist/recorder/RecorderPanel.js +30 -0
  302. package/dist/recorder/RecorderPanel.js.map +1 -0
  303. package/dist/recorder/formats.d.ts +51 -0
  304. package/dist/recorder/formats.d.ts.map +1 -0
  305. package/dist/recorder/formats.js +144 -0
  306. package/dist/recorder/formats.js.map +1 -0
  307. package/dist/recorder/hooks/useMediaRecorder.d.ts +90 -0
  308. package/dist/recorder/hooks/useMediaRecorder.d.ts.map +1 -0
  309. package/dist/recorder/hooks/useMediaRecorder.js +277 -0
  310. package/dist/recorder/hooks/useMediaRecorder.js.map +1 -0
  311. package/dist/recorder/hooks/useStreamPreview.d.ts +22 -0
  312. package/dist/recorder/hooks/useStreamPreview.d.ts.map +1 -0
  313. package/dist/recorder/hooks/useStreamPreview.js +44 -0
  314. package/dist/recorder/hooks/useStreamPreview.js.map +1 -0
  315. package/dist/recorder/sources/cameraStream.d.ts +22 -0
  316. package/dist/recorder/sources/cameraStream.d.ts.map +1 -0
  317. package/dist/recorder/sources/cameraStream.js +24 -0
  318. package/dist/recorder/sources/cameraStream.js.map +1 -0
  319. package/dist/recorder/sources/micStream.d.ts +15 -0
  320. package/dist/recorder/sources/micStream.d.ts.map +1 -0
  321. package/dist/recorder/sources/micStream.js +24 -0
  322. package/dist/recorder/sources/micStream.js.map +1 -0
  323. package/dist/recorder/sources/screenStream.d.ts +53 -0
  324. package/dist/recorder/sources/screenStream.d.ts.map +1 -0
  325. package/dist/recorder/sources/screenStream.js +114 -0
  326. package/dist/recorder/sources/screenStream.js.map +1 -0
  327. package/dist/recorder/timingJson.d.ts +51 -0
  328. package/dist/recorder/timingJson.d.ts.map +1 -0
  329. package/dist/recorder/timingJson.js +42 -0
  330. package/dist/recorder/timingJson.js.map +1 -0
  331. package/dist/tiptap/TiptapAudio.d.ts +26 -0
  332. package/dist/tiptap/TiptapAudio.d.ts.map +1 -0
  333. package/dist/tiptap/TiptapAudio.js +58 -0
  334. package/dist/tiptap/TiptapAudio.js.map +1 -0
  335. package/dist/tiptap/TiptapVideo.d.ts +30 -0
  336. package/dist/tiptap/TiptapVideo.d.ts.map +1 -0
  337. package/dist/tiptap/TiptapVideo.js +66 -0
  338. package/dist/tiptap/TiptapVideo.js.map +1 -0
  339. package/dist/tiptap/useResolvedMediaSrc.d.ts +2 -0
  340. package/dist/tiptap/useResolvedMediaSrc.d.ts.map +1 -0
  341. package/dist/tiptap/useResolvedMediaSrc.js +42 -0
  342. package/dist/tiptap/useResolvedMediaSrc.js.map +1 -0
  343. package/dist/tiptapBridge.d.ts.map +1 -1
  344. package/dist/tiptapBridge.js +210 -16
  345. package/dist/tiptapBridge.js.map +1 -1
  346. package/dist/useHeadingLayout.d.ts +54 -0
  347. package/dist/useHeadingLayout.d.ts.map +1 -0
  348. package/dist/useHeadingLayout.js +260 -0
  349. package/dist/useHeadingLayout.js.map +1 -0
  350. package/dist/utils/collectInlineFontAwesomeCss.d.ts +21 -0
  351. package/dist/utils/collectInlineFontAwesomeCss.d.ts.map +1 -0
  352. package/dist/utils/collectInlineFontAwesomeCss.js +68 -0
  353. package/dist/utils/collectInlineFontAwesomeCss.js.map +1 -0
  354. package/dist/utils/dropUtils.d.ts +21 -2
  355. package/dist/utils/dropUtils.d.ts.map +1 -1
  356. package/dist/utils/dropUtils.js +43 -4
  357. package/dist/utils/dropUtils.js.map +1 -1
  358. package/dist/utils/normalizeMalformedAssetUrl.d.ts +15 -0
  359. package/dist/utils/normalizeMalformedAssetUrl.d.ts.map +1 -0
  360. package/dist/utils/normalizeMalformedAssetUrl.js +27 -0
  361. package/dist/utils/normalizeMalformedAssetUrl.js.map +1 -0
  362. package/package.json +8 -5
  363. package/src/DocumentSettingsDialog.tsx +266 -0
  364. package/src/EditorContext.tsx +534 -10
  365. package/src/EditorShell.tsx +691 -63
  366. package/src/EmojiPicker.tsx +332 -0
  367. package/src/ImageEditor.tsx +327 -0
  368. package/src/ImageNodeView.tsx +222 -21
  369. package/src/ImageViewer.tsx +221 -0
  370. package/src/InlineIcon.ts +84 -0
  371. package/src/InlinePreviewGutter.tsx +582 -0
  372. package/src/LinkDialog.tsx +276 -0
  373. package/src/MediaBin.tsx +22 -3
  374. package/src/MentionExtension.tsx +10 -7
  375. package/src/OutlinePanel.tsx +295 -0
  376. package/src/PlainHtmlPreview.tsx +211 -0
  377. package/src/PreviewControls.tsx +130 -24
  378. package/src/PreviewPanel.tsx +38 -21
  379. package/src/RawEditor.tsx +215 -4
  380. package/src/RecorderEntry.tsx +164 -0
  381. package/src/TemplateAnnotation.ts +32 -6
  382. package/src/TemplatePicker.tsx +818 -0
  383. package/src/ThemeCustomizerPanel.tsx +595 -0
  384. package/src/ThemePicker.tsx +319 -0
  385. package/src/Toolbar.tsx +708 -111
  386. package/src/VersionHistoryPanel.tsx +329 -0
  387. package/src/ViewMenuPanel.tsx +188 -0
  388. package/src/WysiwygEditor.tsx +229 -9
  389. package/src/__tests__/detectMarkdown.test.ts +0 -15
  390. package/src/__tests__/documentSettingsDialog.test.tsx +147 -0
  391. package/src/__tests__/emojiPicker.test.tsx +133 -0
  392. package/src/__tests__/fileKind.test.ts +16 -0
  393. package/src/__tests__/imageEditAffordance.test.tsx +268 -0
  394. package/src/__tests__/imageEditorShell.test.tsx +57 -0
  395. package/src/__tests__/imageEditorState.test.ts +171 -0
  396. package/src/__tests__/inlinePreviewGutter.test.tsx +62 -0
  397. package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +103 -0
  398. package/src/__tests__/jsonEditor.test.tsx +168 -0
  399. package/src/__tests__/layersPanel.test.tsx +97 -0
  400. package/src/__tests__/linkDialogDocPicker.test.tsx +137 -0
  401. package/src/__tests__/mediaAttachmentFlow.test.ts +110 -0
  402. package/src/__tests__/outlinePanel.test.tsx +79 -0
  403. package/src/__tests__/plainHtmlPreview.test.tsx +107 -0
  404. package/src/__tests__/propertiesPanel.test.tsx +69 -0
  405. package/src/__tests__/recorderFormats.test.ts +146 -0
  406. package/src/__tests__/recorderTimingJson.test.ts +41 -0
  407. package/src/__tests__/templateAnnotationRoundTrip.test.ts +34 -0
  408. package/src/__tests__/tiptapBridge.test.ts +29 -0
  409. package/src/__tests__/tiptapImageRoundTrip.test.ts +73 -0
  410. package/src/__tests__/useImageEditor.test.tsx +159 -0
  411. package/src/__tests__/useMediaRecorder.test.ts +186 -0
  412. package/src/__tests__/versionHistory.test.tsx +197 -0
  413. package/src/blockSlice.ts +75 -0
  414. package/src/buildPreviewDoc.ts +61 -6
  415. package/src/emojiData.ts +1337 -0
  416. package/src/fileKind.ts +30 -6
  417. package/src/hooks/useFileDrop.ts +40 -4
  418. package/src/imageEditor/CanvasSurface.tsx +402 -0
  419. package/src/imageEditor/ImageVersionHistoryDropdown.tsx +396 -0
  420. package/src/imageEditor/LayersPanel.tsx +143 -0
  421. package/src/imageEditor/PropertiesPanel.tsx +428 -0
  422. package/src/imageEditor/Toolbar.tsx +242 -0
  423. package/src/imageEditor/icons.tsx +144 -0
  424. package/src/imageEditor/image-editor.css +450 -0
  425. package/src/imageEditor/layers/EditorImageLayer.tsx +45 -0
  426. package/src/imageEditor/layers/EditorShapeLayer.tsx +62 -0
  427. package/src/imageEditor/layers/EditorTextLayer.tsx +45 -0
  428. package/src/imageEditor/layers/SelectionHandles.tsx +86 -0
  429. package/src/imageEditor/state.ts +153 -0
  430. package/src/imageEditor/useImageEditor.ts +328 -0
  431. package/src/imageEditor/useImageEditorTokens.ts +70 -0
  432. package/src/index.ts +82 -0
  433. package/src/jsonEditor/EmbeddedRichTextField.tsx +81 -0
  434. package/src/jsonEditor/JsonEditor.tsx +81 -0
  435. package/src/jsonEditor/JsonEditorContext.tsx +75 -0
  436. package/src/jsonEditor/RenderNode.tsx +66 -0
  437. package/src/jsonEditor/editors.tsx +678 -0
  438. package/src/jsonEditor/index.ts +2 -0
  439. package/src/jsonEditor/json-editor.css +463 -0
  440. package/src/jsonEditor/useJsonEditorTokens.ts +63 -0
  441. package/src/recorder/RecorderButton.tsx +72 -0
  442. package/src/recorder/RecorderModal.tsx +596 -0
  443. package/src/recorder/RecorderPanel.tsx +93 -0
  444. package/src/recorder/formats.ts +159 -0
  445. package/src/recorder/hooks/useMediaRecorder.ts +378 -0
  446. package/src/recorder/hooks/useStreamPreview.ts +47 -0
  447. package/src/recorder/sources/cameraStream.ts +32 -0
  448. package/src/recorder/sources/micStream.ts +25 -0
  449. package/src/recorder/sources/screenStream.ts +162 -0
  450. package/src/recorder/timingJson.ts +66 -0
  451. package/src/styles/editor.css +2490 -51
  452. package/src/styles/image-edit-affordance.css +201 -0
  453. package/src/styles/index.css +10 -0
  454. package/src/tiptap/TiptapAudio.tsx +86 -0
  455. package/src/tiptap/TiptapVideo.tsx +119 -0
  456. package/src/tiptap/useResolvedMediaSrc.ts +47 -0
  457. package/src/tiptapBridge.ts +227 -22
  458. package/src/useHeadingLayout.ts +294 -0
  459. package/src/utils/collectInlineFontAwesomeCss.ts +69 -0
  460. package/src/utils/dropUtils.ts +54 -6
  461. 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 { getAvailableTemplates } from '@bendyline/squisq/doc';
15
-
16
- /** Template names are static — computed once at module load. */
17
- const TEMPLATE_NAMES = getAvailableTemplates();
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 url = window.prompt('URL:');
314
- if (url) {
315
- (chain as unknown as Record<string, (opts: { href: string }) => typeof chain>)
316
- .setLink?.({ href: url })
317
- .run();
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
- if (hasSelection) {
411
- replacement = '[' + selectedText + '](url)';
412
- } else {
413
- replacement = '[link text](url)';
414
- newCursorOffset = 1; // inside the []
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
- break;
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
- [activeView, tiptapEditor, handleTiptap, handleRaw],
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 currentTemplate = isWysiwyg
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 = isWysiwyg ? isTiptapActive(tiptapEditor, btn.id) : false;
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 === 'table' ? (
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 (WYSIWYG) */}
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
- <label
668
- className="squisq-template-picker-label"
669
- data-tooltip="Block template for this heading"
670
- >
671
- Template:
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).map((btn) => {
864
- const active = isWysiwyg ? isTiptapActive(tiptapEditor, btn.id) : false;
865
- const disabled = btn.id === 'image' && !mediaProvider;
866
- return (
867
- <button
868
- key={btn.id}
869
- className={`squisq-toolbar-overflow-item${active ? ' squisq-toolbar-overflow-item--active' : ''}`}
870
- onClick={() => {
871
- handleAction(btn.id);
872
- setShowOverflow(false);
873
- }}
874
- disabled={disabled}
875
- >
876
- {btn.id === 'table' ? (
877
- <svg
878
- width="14"
879
- height="14"
880
- viewBox="0 0 14 14"
881
- fill="none"
882
- stroke="currentColor"
883
- strokeWidth="1.4"
884
- strokeLinecap="round"
885
- >
886
- <rect x="1" y="1" width="12" height="12" rx="1" />
887
- <line x1="1" y1="5" x2="13" y2="5" />
888
- <line x1="1" y1="9" x2="13" y2="9" />
889
- <line x1="5" y1="1" x2="5" y2="13" />
890
- <line x1="9" y1="1" x2="9" y2="13" />
891
- </svg>
892
- ) : (
893
- <span className="squisq-toolbar-overflow-icon" style={btn.iconStyle}>
894
- {btn.icon}
895
- </span>
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
- <select
907
- className="squisq-template-picker-select"
1431
+ <TemplatePicker
908
1432
  value={currentTemplate}
909
- onChange={(e) => {
910
- handleTemplatePick(e.target.value);
1433
+ onChange={(v) => {
1434
+ handleTemplatePick(v);
911
1435
  setShowOverflow(false);
912
1436
  }}
913
- >
914
- <option value="">— none —</option>
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
- {'\u{1F4CE}'}
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
  }