@bendyline/squisq-editor-react 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) 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 +173 -4
  10. package/dist/EditorShell.d.ts.map +1 -1
  11. package/dist/EditorShell.js +110 -10
  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/MentionExtension.js +10 -7
  42. package/dist/MentionExtension.js.map +1 -1
  43. package/dist/OutlinePanel.d.ts +17 -0
  44. package/dist/OutlinePanel.d.ts.map +1 -0
  45. package/dist/OutlinePanel.js +167 -0
  46. package/dist/OutlinePanel.js.map +1 -0
  47. package/dist/PlainHtmlPreview.d.ts +50 -0
  48. package/dist/PlainHtmlPreview.d.ts.map +1 -0
  49. package/dist/PlainHtmlPreview.js +155 -0
  50. package/dist/PlainHtmlPreview.js.map +1 -0
  51. package/dist/PreviewControls.d.ts +15 -1
  52. package/dist/PreviewControls.d.ts.map +1 -1
  53. package/dist/PreviewControls.js +75 -18
  54. package/dist/PreviewControls.js.map +1 -1
  55. package/dist/PreviewPanel.d.ts +11 -10
  56. package/dist/PreviewPanel.d.ts.map +1 -1
  57. package/dist/PreviewPanel.js +20 -17
  58. package/dist/PreviewPanel.js.map +1 -1
  59. package/dist/RawEditor.d.ts.map +1 -1
  60. package/dist/RawEditor.js +198 -4
  61. package/dist/RawEditor.js.map +1 -1
  62. package/dist/RecorderEntry.d.ts +24 -0
  63. package/dist/RecorderEntry.d.ts.map +1 -0
  64. package/dist/RecorderEntry.js +139 -0
  65. package/dist/RecorderEntry.js.map +1 -0
  66. package/dist/TemplateAnnotation.d.ts.map +1 -1
  67. package/dist/TemplateAnnotation.js +32 -6
  68. package/dist/TemplateAnnotation.js.map +1 -1
  69. package/dist/TemplatePicker.d.ts +53 -0
  70. package/dist/TemplatePicker.d.ts.map +1 -0
  71. package/dist/TemplatePicker.js +388 -0
  72. package/dist/TemplatePicker.js.map +1 -0
  73. package/dist/ThemeCustomizerPanel.d.ts +32 -0
  74. package/dist/ThemeCustomizerPanel.d.ts.map +1 -0
  75. package/dist/ThemeCustomizerPanel.js +256 -0
  76. package/dist/ThemeCustomizerPanel.js.map +1 -0
  77. package/dist/ThemePicker.d.ts +33 -0
  78. package/dist/ThemePicker.d.ts.map +1 -0
  79. package/dist/ThemePicker.js +148 -0
  80. package/dist/ThemePicker.js.map +1 -0
  81. package/dist/Toolbar.d.ts.map +1 -1
  82. package/dist/Toolbar.js +508 -33
  83. package/dist/Toolbar.js.map +1 -1
  84. package/dist/VersionHistoryPanel.d.ts +14 -0
  85. package/dist/VersionHistoryPanel.d.ts.map +1 -0
  86. package/dist/VersionHistoryPanel.js +147 -0
  87. package/dist/VersionHistoryPanel.js.map +1 -0
  88. package/dist/ViewMenuPanel.d.ts +13 -0
  89. package/dist/ViewMenuPanel.d.ts.map +1 -0
  90. package/dist/ViewMenuPanel.js +58 -0
  91. package/dist/ViewMenuPanel.js.map +1 -0
  92. package/dist/WysiwygEditor.d.ts.map +1 -1
  93. package/dist/WysiwygEditor.js +198 -9
  94. package/dist/WysiwygEditor.js.map +1 -1
  95. package/dist/__tests__/detectMarkdown.test.js +0 -14
  96. package/dist/__tests__/detectMarkdown.test.js.map +1 -1
  97. package/dist/__tests__/documentSettingsDialog.test.d.ts +2 -0
  98. package/dist/__tests__/documentSettingsDialog.test.d.ts.map +1 -0
  99. package/dist/__tests__/documentSettingsDialog.test.js +132 -0
  100. package/dist/__tests__/documentSettingsDialog.test.js.map +1 -0
  101. package/dist/__tests__/emojiPicker.test.d.ts +2 -0
  102. package/dist/__tests__/emojiPicker.test.d.ts.map +1 -0
  103. package/dist/__tests__/emojiPicker.test.js +111 -0
  104. package/dist/__tests__/emojiPicker.test.js.map +1 -0
  105. package/dist/__tests__/fileKind.test.js +13 -0
  106. package/dist/__tests__/fileKind.test.js.map +1 -1
  107. package/dist/__tests__/imageEditAffordance.test.d.ts +2 -0
  108. package/dist/__tests__/imageEditAffordance.test.d.ts.map +1 -0
  109. package/dist/__tests__/imageEditAffordance.test.js +188 -0
  110. package/dist/__tests__/imageEditAffordance.test.js.map +1 -0
  111. package/dist/__tests__/imageEditorShell.test.d.ts +2 -0
  112. package/dist/__tests__/imageEditorShell.test.d.ts.map +1 -0
  113. package/dist/__tests__/imageEditorShell.test.js +52 -0
  114. package/dist/__tests__/imageEditorShell.test.js.map +1 -0
  115. package/dist/__tests__/imageEditorState.test.d.ts +3 -0
  116. package/dist/__tests__/imageEditorState.test.d.ts.map +1 -0
  117. package/dist/__tests__/imageEditorState.test.js +148 -0
  118. package/dist/__tests__/imageEditorState.test.js.map +1 -0
  119. package/dist/__tests__/inlinePreviewGutter.test.d.ts +2 -0
  120. package/dist/__tests__/inlinePreviewGutter.test.d.ts.map +1 -0
  121. package/dist/__tests__/inlinePreviewGutter.test.js +51 -0
  122. package/dist/__tests__/inlinePreviewGutter.test.js.map +1 -0
  123. package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts +2 -0
  124. package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts.map +1 -0
  125. package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js +63 -0
  126. package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js.map +1 -0
  127. package/dist/__tests__/jsonEditor.test.d.ts +2 -0
  128. package/dist/__tests__/jsonEditor.test.d.ts.map +1 -0
  129. package/dist/__tests__/jsonEditor.test.js +134 -0
  130. package/dist/__tests__/jsonEditor.test.js.map +1 -0
  131. package/dist/__tests__/layersPanel.test.d.ts +2 -0
  132. package/dist/__tests__/layersPanel.test.d.ts.map +1 -0
  133. package/dist/__tests__/layersPanel.test.js +84 -0
  134. package/dist/__tests__/layersPanel.test.js.map +1 -0
  135. package/dist/__tests__/linkDialogDocPicker.test.d.ts +7 -0
  136. package/dist/__tests__/linkDialogDocPicker.test.d.ts.map +1 -0
  137. package/dist/__tests__/linkDialogDocPicker.test.js +75 -0
  138. package/dist/__tests__/linkDialogDocPicker.test.js.map +1 -0
  139. package/dist/__tests__/outlinePanel.test.d.ts +2 -0
  140. package/dist/__tests__/outlinePanel.test.d.ts.map +1 -0
  141. package/dist/__tests__/outlinePanel.test.js +68 -0
  142. package/dist/__tests__/outlinePanel.test.js.map +1 -0
  143. package/dist/__tests__/plainHtmlPreview.test.d.ts +2 -0
  144. package/dist/__tests__/plainHtmlPreview.test.d.ts.map +1 -0
  145. package/dist/__tests__/plainHtmlPreview.test.js +87 -0
  146. package/dist/__tests__/plainHtmlPreview.test.js.map +1 -0
  147. package/dist/__tests__/propertiesPanel.test.d.ts +2 -0
  148. package/dist/__tests__/propertiesPanel.test.d.ts.map +1 -0
  149. package/dist/__tests__/propertiesPanel.test.js +64 -0
  150. package/dist/__tests__/propertiesPanel.test.js.map +1 -0
  151. package/dist/__tests__/recorderFormats.test.d.ts +2 -0
  152. package/dist/__tests__/recorderFormats.test.d.ts.map +1 -0
  153. package/dist/__tests__/recorderFormats.test.js +121 -0
  154. package/dist/__tests__/recorderFormats.test.js.map +1 -0
  155. package/dist/__tests__/recorderTimingJson.test.d.ts +2 -0
  156. package/dist/__tests__/recorderTimingJson.test.d.ts.map +1 -0
  157. package/dist/__tests__/recorderTimingJson.test.js +37 -0
  158. package/dist/__tests__/recorderTimingJson.test.js.map +1 -0
  159. package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts +2 -0
  160. package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts.map +1 -0
  161. package/dist/__tests__/templateAnnotationRoundTrip.test.js +31 -0
  162. package/dist/__tests__/templateAnnotationRoundTrip.test.js.map +1 -0
  163. package/dist/__tests__/tiptapBridge.test.js +13 -0
  164. package/dist/__tests__/tiptapBridge.test.js.map +1 -1
  165. package/dist/__tests__/useImageEditor.test.d.ts +2 -0
  166. package/dist/__tests__/useImageEditor.test.d.ts.map +1 -0
  167. package/dist/__tests__/useImageEditor.test.js +131 -0
  168. package/dist/__tests__/useImageEditor.test.js.map +1 -0
  169. package/dist/__tests__/useMediaRecorder.test.d.ts +2 -0
  170. package/dist/__tests__/useMediaRecorder.test.d.ts.map +1 -0
  171. package/dist/__tests__/useMediaRecorder.test.js +153 -0
  172. package/dist/__tests__/useMediaRecorder.test.js.map +1 -0
  173. package/dist/__tests__/versionHistory.test.d.ts +2 -0
  174. package/dist/__tests__/versionHistory.test.d.ts.map +1 -0
  175. package/dist/__tests__/versionHistory.test.js +124 -0
  176. package/dist/__tests__/versionHistory.test.js.map +1 -0
  177. package/dist/blockSlice.d.ts +24 -0
  178. package/dist/blockSlice.d.ts.map +1 -0
  179. package/dist/blockSlice.js +63 -0
  180. package/dist/blockSlice.js.map +1 -0
  181. package/dist/buildPreviewDoc.d.ts.map +1 -1
  182. package/dist/buildPreviewDoc.js +52 -2
  183. package/dist/buildPreviewDoc.js.map +1 -1
  184. package/dist/emojiData.d.ts +81 -0
  185. package/dist/emojiData.d.ts.map +1 -0
  186. package/dist/emojiData.js +1283 -0
  187. package/dist/emojiData.js.map +1 -0
  188. package/dist/fileKind.d.ts +6 -2
  189. package/dist/fileKind.d.ts.map +1 -1
  190. package/dist/fileKind.js +25 -4
  191. package/dist/fileKind.js.map +1 -1
  192. package/dist/hooks/useFileDrop.d.ts.map +1 -1
  193. package/dist/hooks/useFileDrop.js +40 -4
  194. package/dist/hooks/useFileDrop.js.map +1 -1
  195. package/dist/imageEditor/CanvasSurface.d.ts +31 -0
  196. package/dist/imageEditor/CanvasSurface.d.ts.map +1 -0
  197. package/dist/imageEditor/CanvasSurface.js +264 -0
  198. package/dist/imageEditor/CanvasSurface.js.map +1 -0
  199. package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts +39 -0
  200. package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts.map +1 -0
  201. package/dist/imageEditor/ImageVersionHistoryDropdown.js +283 -0
  202. package/dist/imageEditor/ImageVersionHistoryDropdown.js.map +1 -0
  203. package/dist/imageEditor/LayersPanel.d.ts +14 -0
  204. package/dist/imageEditor/LayersPanel.d.ts.map +1 -0
  205. package/dist/imageEditor/LayersPanel.js +43 -0
  206. package/dist/imageEditor/LayersPanel.js.map +1 -0
  207. package/dist/imageEditor/PropertiesPanel.d.ts +14 -0
  208. package/dist/imageEditor/PropertiesPanel.d.ts.map +1 -0
  209. package/dist/imageEditor/PropertiesPanel.js +97 -0
  210. package/dist/imageEditor/PropertiesPanel.js.map +1 -0
  211. package/dist/imageEditor/Toolbar.d.ts +30 -0
  212. package/dist/imageEditor/Toolbar.d.ts.map +1 -0
  213. package/dist/imageEditor/Toolbar.js +108 -0
  214. package/dist/imageEditor/Toolbar.js.map +1 -0
  215. package/dist/imageEditor/icons.d.ts +24 -0
  216. package/dist/imageEditor/icons.d.ts.map +1 -0
  217. package/dist/imageEditor/icons.js +45 -0
  218. package/dist/imageEditor/icons.js.map +1 -0
  219. package/dist/imageEditor/layers/EditorImageLayer.d.ts +16 -0
  220. package/dist/imageEditor/layers/EditorImageLayer.d.ts.map +1 -0
  221. package/dist/imageEditor/layers/EditorImageLayer.js +37 -0
  222. package/dist/imageEditor/layers/EditorImageLayer.js.map +1 -0
  223. package/dist/imageEditor/layers/EditorShapeLayer.d.ts +15 -0
  224. package/dist/imageEditor/layers/EditorShapeLayer.d.ts.map +1 -0
  225. package/dist/imageEditor/layers/EditorShapeLayer.js +20 -0
  226. package/dist/imageEditor/layers/EditorShapeLayer.js.map +1 -0
  227. package/dist/imageEditor/layers/EditorTextLayer.d.ts +18 -0
  228. package/dist/imageEditor/layers/EditorTextLayer.d.ts.map +1 -0
  229. package/dist/imageEditor/layers/EditorTextLayer.js +13 -0
  230. package/dist/imageEditor/layers/EditorTextLayer.js.map +1 -0
  231. package/dist/imageEditor/layers/SelectionHandles.d.ts +17 -0
  232. package/dist/imageEditor/layers/SelectionHandles.d.ts.map +1 -0
  233. package/dist/imageEditor/layers/SelectionHandles.js +19 -0
  234. package/dist/imageEditor/layers/SelectionHandles.js.map +1 -0
  235. package/dist/imageEditor/state.d.ts +76 -0
  236. package/dist/imageEditor/state.d.ts.map +1 -0
  237. package/dist/imageEditor/state.js +87 -0
  238. package/dist/imageEditor/state.js.map +1 -0
  239. package/dist/imageEditor/useImageEditor.d.ts +53 -0
  240. package/dist/imageEditor/useImageEditor.d.ts.map +1 -0
  241. package/dist/imageEditor/useImageEditor.js +244 -0
  242. package/dist/imageEditor/useImageEditor.js.map +1 -0
  243. package/dist/imageEditor/useImageEditorTokens.d.ts +16 -0
  244. package/dist/imageEditor/useImageEditorTokens.d.ts.map +1 -0
  245. package/dist/imageEditor/useImageEditorTokens.js +45 -0
  246. package/dist/imageEditor/useImageEditorTokens.js.map +1 -0
  247. package/dist/index.d.ts +48 -1
  248. package/dist/index.d.ts.map +1 -1
  249. package/dist/index.js +36 -0
  250. package/dist/index.js.map +1 -1
  251. package/dist/jsonEditor/EmbeddedRichTextField.d.ts +15 -0
  252. package/dist/jsonEditor/EmbeddedRichTextField.d.ts.map +1 -0
  253. package/dist/jsonEditor/EmbeddedRichTextField.js +74 -0
  254. package/dist/jsonEditor/EmbeddedRichTextField.js.map +1 -0
  255. package/dist/jsonEditor/JsonEditor.d.ts +36 -0
  256. package/dist/jsonEditor/JsonEditor.d.ts.map +1 -0
  257. package/dist/jsonEditor/JsonEditor.js +15 -0
  258. package/dist/jsonEditor/JsonEditor.js.map +1 -0
  259. package/dist/jsonEditor/JsonEditorContext.d.ts +28 -0
  260. package/dist/jsonEditor/JsonEditorContext.d.ts.map +1 -0
  261. package/dist/jsonEditor/JsonEditorContext.js +41 -0
  262. package/dist/jsonEditor/JsonEditorContext.js.map +1 -0
  263. package/dist/jsonEditor/RenderNode.d.ts +16 -0
  264. package/dist/jsonEditor/RenderNode.d.ts.map +1 -0
  265. package/dist/jsonEditor/RenderNode.js +32 -0
  266. package/dist/jsonEditor/RenderNode.js.map +1 -0
  267. package/dist/jsonEditor/editors.d.ts +36 -0
  268. package/dist/jsonEditor/editors.d.ts.map +1 -0
  269. package/dist/jsonEditor/editors.js +347 -0
  270. package/dist/jsonEditor/editors.js.map +1 -0
  271. package/dist/jsonEditor/index.d.ts +3 -0
  272. package/dist/jsonEditor/index.d.ts.map +1 -0
  273. package/dist/jsonEditor/index.js +2 -0
  274. package/dist/jsonEditor/index.js.map +1 -0
  275. package/dist/jsonEditor/useJsonEditorTokens.d.ts +13 -0
  276. package/dist/jsonEditor/useJsonEditorTokens.d.ts.map +1 -0
  277. package/dist/jsonEditor/useJsonEditorTokens.js +38 -0
  278. package/dist/jsonEditor/useJsonEditorTokens.js.map +1 -0
  279. package/dist/recorder/RecorderButton.d.ts +31 -0
  280. package/dist/recorder/RecorderButton.d.ts.map +1 -0
  281. package/dist/recorder/RecorderButton.js +24 -0
  282. package/dist/recorder/RecorderButton.js.map +1 -0
  283. package/dist/recorder/RecorderModal.d.ts +59 -0
  284. package/dist/recorder/RecorderModal.d.ts.map +1 -0
  285. package/dist/recorder/RecorderModal.js +333 -0
  286. package/dist/recorder/RecorderModal.js.map +1 -0
  287. package/dist/recorder/RecorderPanel.d.ts +25 -0
  288. package/dist/recorder/RecorderPanel.d.ts.map +1 -0
  289. package/dist/recorder/RecorderPanel.js +30 -0
  290. package/dist/recorder/RecorderPanel.js.map +1 -0
  291. package/dist/recorder/formats.d.ts +51 -0
  292. package/dist/recorder/formats.d.ts.map +1 -0
  293. package/dist/recorder/formats.js +144 -0
  294. package/dist/recorder/formats.js.map +1 -0
  295. package/dist/recorder/hooks/useMediaRecorder.d.ts +90 -0
  296. package/dist/recorder/hooks/useMediaRecorder.d.ts.map +1 -0
  297. package/dist/recorder/hooks/useMediaRecorder.js +277 -0
  298. package/dist/recorder/hooks/useMediaRecorder.js.map +1 -0
  299. package/dist/recorder/hooks/useStreamPreview.d.ts +22 -0
  300. package/dist/recorder/hooks/useStreamPreview.d.ts.map +1 -0
  301. package/dist/recorder/hooks/useStreamPreview.js +44 -0
  302. package/dist/recorder/hooks/useStreamPreview.js.map +1 -0
  303. package/dist/recorder/sources/cameraStream.d.ts +22 -0
  304. package/dist/recorder/sources/cameraStream.d.ts.map +1 -0
  305. package/dist/recorder/sources/cameraStream.js +24 -0
  306. package/dist/recorder/sources/cameraStream.js.map +1 -0
  307. package/dist/recorder/sources/micStream.d.ts +15 -0
  308. package/dist/recorder/sources/micStream.d.ts.map +1 -0
  309. package/dist/recorder/sources/micStream.js +24 -0
  310. package/dist/recorder/sources/micStream.js.map +1 -0
  311. package/dist/recorder/sources/screenStream.d.ts +53 -0
  312. package/dist/recorder/sources/screenStream.d.ts.map +1 -0
  313. package/dist/recorder/sources/screenStream.js +114 -0
  314. package/dist/recorder/sources/screenStream.js.map +1 -0
  315. package/dist/recorder/timingJson.d.ts +51 -0
  316. package/dist/recorder/timingJson.d.ts.map +1 -0
  317. package/dist/recorder/timingJson.js +42 -0
  318. package/dist/recorder/timingJson.js.map +1 -0
  319. package/dist/tiptap/TiptapAudio.d.ts +26 -0
  320. package/dist/tiptap/TiptapAudio.d.ts.map +1 -0
  321. package/dist/tiptap/TiptapAudio.js +58 -0
  322. package/dist/tiptap/TiptapAudio.js.map +1 -0
  323. package/dist/tiptap/TiptapVideo.d.ts +30 -0
  324. package/dist/tiptap/TiptapVideo.d.ts.map +1 -0
  325. package/dist/tiptap/TiptapVideo.js +66 -0
  326. package/dist/tiptap/TiptapVideo.js.map +1 -0
  327. package/dist/tiptap/useResolvedMediaSrc.d.ts +2 -0
  328. package/dist/tiptap/useResolvedMediaSrc.d.ts.map +1 -0
  329. package/dist/tiptap/useResolvedMediaSrc.js +42 -0
  330. package/dist/tiptap/useResolvedMediaSrc.js.map +1 -0
  331. package/dist/tiptapBridge.d.ts.map +1 -1
  332. package/dist/tiptapBridge.js +171 -14
  333. package/dist/tiptapBridge.js.map +1 -1
  334. package/dist/useHeadingLayout.d.ts +54 -0
  335. package/dist/useHeadingLayout.d.ts.map +1 -0
  336. package/dist/useHeadingLayout.js +260 -0
  337. package/dist/useHeadingLayout.js.map +1 -0
  338. package/dist/utils/collectInlineFontAwesomeCss.d.ts +21 -0
  339. package/dist/utils/collectInlineFontAwesomeCss.d.ts.map +1 -0
  340. package/dist/utils/collectInlineFontAwesomeCss.js +68 -0
  341. package/dist/utils/collectInlineFontAwesomeCss.js.map +1 -0
  342. package/dist/utils/dropUtils.d.ts +21 -2
  343. package/dist/utils/dropUtils.d.ts.map +1 -1
  344. package/dist/utils/dropUtils.js +43 -4
  345. package/dist/utils/dropUtils.js.map +1 -1
  346. package/dist/utils/normalizeMalformedAssetUrl.d.ts +15 -0
  347. package/dist/utils/normalizeMalformedAssetUrl.d.ts.map +1 -0
  348. package/dist/utils/normalizeMalformedAssetUrl.js +27 -0
  349. package/dist/utils/normalizeMalformedAssetUrl.js.map +1 -0
  350. package/package.json +8 -5
  351. package/src/DocumentSettingsDialog.tsx +266 -0
  352. package/src/EditorContext.tsx +534 -10
  353. package/src/EditorShell.tsx +571 -55
  354. package/src/EmojiPicker.tsx +332 -0
  355. package/src/ImageEditor.tsx +327 -0
  356. package/src/ImageNodeView.tsx +222 -21
  357. package/src/ImageViewer.tsx +221 -0
  358. package/src/InlineIcon.ts +84 -0
  359. package/src/InlinePreviewGutter.tsx +582 -0
  360. package/src/LinkDialog.tsx +276 -0
  361. package/src/MentionExtension.tsx +10 -7
  362. package/src/OutlinePanel.tsx +295 -0
  363. package/src/PlainHtmlPreview.tsx +211 -0
  364. package/src/PreviewControls.tsx +130 -24
  365. package/src/PreviewPanel.tsx +38 -21
  366. package/src/RawEditor.tsx +215 -4
  367. package/src/RecorderEntry.tsx +164 -0
  368. package/src/TemplateAnnotation.ts +32 -6
  369. package/src/TemplatePicker.tsx +818 -0
  370. package/src/ThemeCustomizerPanel.tsx +595 -0
  371. package/src/ThemePicker.tsx +319 -0
  372. package/src/Toolbar.tsx +708 -111
  373. package/src/VersionHistoryPanel.tsx +329 -0
  374. package/src/ViewMenuPanel.tsx +188 -0
  375. package/src/WysiwygEditor.tsx +229 -9
  376. package/src/__tests__/detectMarkdown.test.ts +0 -15
  377. package/src/__tests__/documentSettingsDialog.test.tsx +147 -0
  378. package/src/__tests__/emojiPicker.test.tsx +133 -0
  379. package/src/__tests__/fileKind.test.ts +16 -0
  380. package/src/__tests__/imageEditAffordance.test.tsx +268 -0
  381. package/src/__tests__/imageEditorShell.test.tsx +57 -0
  382. package/src/__tests__/imageEditorState.test.ts +171 -0
  383. package/src/__tests__/inlinePreviewGutter.test.tsx +62 -0
  384. package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +103 -0
  385. package/src/__tests__/jsonEditor.test.tsx +168 -0
  386. package/src/__tests__/layersPanel.test.tsx +97 -0
  387. package/src/__tests__/linkDialogDocPicker.test.tsx +137 -0
  388. package/src/__tests__/outlinePanel.test.tsx +79 -0
  389. package/src/__tests__/plainHtmlPreview.test.tsx +107 -0
  390. package/src/__tests__/propertiesPanel.test.tsx +69 -0
  391. package/src/__tests__/recorderFormats.test.ts +146 -0
  392. package/src/__tests__/recorderTimingJson.test.ts +41 -0
  393. package/src/__tests__/templateAnnotationRoundTrip.test.ts +34 -0
  394. package/src/__tests__/tiptapBridge.test.ts +15 -0
  395. package/src/__tests__/useImageEditor.test.tsx +159 -0
  396. package/src/__tests__/useMediaRecorder.test.ts +186 -0
  397. package/src/__tests__/versionHistory.test.tsx +197 -0
  398. package/src/blockSlice.ts +75 -0
  399. package/src/buildPreviewDoc.ts +61 -6
  400. package/src/emojiData.ts +1337 -0
  401. package/src/fileKind.ts +30 -6
  402. package/src/hooks/useFileDrop.ts +40 -4
  403. package/src/imageEditor/CanvasSurface.tsx +402 -0
  404. package/src/imageEditor/ImageVersionHistoryDropdown.tsx +396 -0
  405. package/src/imageEditor/LayersPanel.tsx +143 -0
  406. package/src/imageEditor/PropertiesPanel.tsx +428 -0
  407. package/src/imageEditor/Toolbar.tsx +242 -0
  408. package/src/imageEditor/icons.tsx +144 -0
  409. package/src/imageEditor/image-editor.css +450 -0
  410. package/src/imageEditor/layers/EditorImageLayer.tsx +45 -0
  411. package/src/imageEditor/layers/EditorShapeLayer.tsx +62 -0
  412. package/src/imageEditor/layers/EditorTextLayer.tsx +45 -0
  413. package/src/imageEditor/layers/SelectionHandles.tsx +86 -0
  414. package/src/imageEditor/state.ts +153 -0
  415. package/src/imageEditor/useImageEditor.ts +328 -0
  416. package/src/imageEditor/useImageEditorTokens.ts +70 -0
  417. package/src/index.ts +82 -0
  418. package/src/jsonEditor/EmbeddedRichTextField.tsx +81 -0
  419. package/src/jsonEditor/JsonEditor.tsx +81 -0
  420. package/src/jsonEditor/JsonEditorContext.tsx +75 -0
  421. package/src/jsonEditor/RenderNode.tsx +66 -0
  422. package/src/jsonEditor/editors.tsx +678 -0
  423. package/src/jsonEditor/index.ts +2 -0
  424. package/src/jsonEditor/json-editor.css +463 -0
  425. package/src/jsonEditor/useJsonEditorTokens.ts +63 -0
  426. package/src/recorder/RecorderButton.tsx +72 -0
  427. package/src/recorder/RecorderModal.tsx +596 -0
  428. package/src/recorder/RecorderPanel.tsx +93 -0
  429. package/src/recorder/formats.ts +159 -0
  430. package/src/recorder/hooks/useMediaRecorder.ts +378 -0
  431. package/src/recorder/hooks/useStreamPreview.ts +47 -0
  432. package/src/recorder/sources/cameraStream.ts +32 -0
  433. package/src/recorder/sources/micStream.ts +25 -0
  434. package/src/recorder/sources/screenStream.ts +162 -0
  435. package/src/recorder/timingJson.ts +66 -0
  436. package/src/styles/editor.css +2490 -51
  437. package/src/styles/image-edit-affordance.css +201 -0
  438. package/src/styles/index.css +10 -0
  439. package/src/tiptap/TiptapAudio.tsx +86 -0
  440. package/src/tiptap/TiptapVideo.tsx +119 -0
  441. package/src/tiptap/useResolvedMediaSrc.ts +47 -0
  442. package/src/tiptapBridge.ts +188 -20
  443. package/src/useHeadingLayout.ts +294 -0
  444. package/src/utils/collectInlineFontAwesomeCss.ts +69 -0
  445. package/src/utils/dropUtils.ts +54 -6
  446. package/src/utils/normalizeMalformedAssetUrl.ts +22 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * LinkDialog — modal for inserting or editing a markdown link.
3
+ *
4
+ * Used by both the WYSIWYG and Raw markdown toolbars so the link-editing
5
+ * UX is identical regardless of view. Shows separate fields for the link
6
+ * text (caption) and the URL target so the user can see and edit both.
7
+ *
8
+ * When a `documentLinkProvider` is supplied, the dialog also exposes a
9
+ * second tab — "Browse documents" — that lists neighbor docs from the
10
+ * host's workspace. Selecting one fills the URL with the candidate
11
+ * path (and the caption, if it was still empty), so authors can link
12
+ * `home.md → resume.md` without typing the relative path by hand.
13
+ */
14
+
15
+ import { useCallback, useEffect, useRef, useState } from 'react';
16
+ import type { DocumentLinkProvider, DocumentLinkCandidate } from './EditorContext';
17
+
18
+ export interface LinkDialogProps {
19
+ /** Whether this is a brand-new link (Insert) or an existing one (Update). */
20
+ mode: 'insert' | 'update';
21
+ /** Initial value for the caption field. */
22
+ initialText: string;
23
+ /** Initial value for the URL field. */
24
+ initialUrl: string;
25
+ /**
26
+ * Confirm — `text` may be empty if the caller wants to fall back to URL
27
+ * as the visible text. Empty `url` means the user cleared it; callers
28
+ * should treat that as "remove link" when in update mode.
29
+ */
30
+ onConfirm: (text: string, url: string) => void;
31
+ /** Dismiss without applying changes. */
32
+ onClose: () => void;
33
+ /**
34
+ * Optional sibling-document picker. When provided, the dialog shows
35
+ * a "Browse documents" tab that queries the provider as the author
36
+ * types and offers click-to-pick suggestions.
37
+ */
38
+ documentLinkProvider?: DocumentLinkProvider | null;
39
+ }
40
+
41
+ /**
42
+ * Centered modal with Text and URL inputs. Submits on Enter, dismisses
43
+ * on Escape or backdrop click. Auto-focuses URL when the text field is
44
+ * already populated; otherwise focuses Text.
45
+ */
46
+ export function LinkDialog({
47
+ mode,
48
+ initialText,
49
+ initialUrl,
50
+ onConfirm,
51
+ onClose,
52
+ documentLinkProvider,
53
+ }: LinkDialogProps) {
54
+ const [text, setText] = useState(initialText);
55
+ const [url, setUrl] = useState(initialUrl);
56
+ const [tab, setTab] = useState<'url' | 'documents'>('url');
57
+ const [docQuery, setDocQuery] = useState('');
58
+ const [docResults, setDocResults] = useState<DocumentLinkCandidate[]>([]);
59
+ const [docLoading, setDocLoading] = useState(false);
60
+ const textRef = useRef<HTMLInputElement>(null);
61
+ const urlRef = useRef<HTMLInputElement>(null);
62
+ const docSearchRef = useRef<HTMLInputElement>(null);
63
+
64
+ useEffect(() => {
65
+ // If we have a caption already, the URL is what the user is most
66
+ // likely here to edit; otherwise focus the caption field first.
67
+ const target = initialText ? urlRef.current : textRef.current;
68
+ target?.focus();
69
+ target?.select();
70
+ }, [initialText]);
71
+
72
+ // Refresh the document candidate list whenever the user types into
73
+ // the picker or first opens it. Empty query = initial list.
74
+ useEffect(() => {
75
+ if (!documentLinkProvider || tab !== 'documents') return;
76
+ let cancelled = false;
77
+ setDocLoading(true);
78
+ documentLinkProvider(docQuery)
79
+ .then((results) => {
80
+ if (!cancelled) {
81
+ setDocResults(results);
82
+ setDocLoading(false);
83
+ }
84
+ })
85
+ .catch(() => {
86
+ if (!cancelled) {
87
+ setDocResults([]);
88
+ setDocLoading(false);
89
+ }
90
+ });
91
+ return () => {
92
+ cancelled = true;
93
+ };
94
+ }, [docQuery, documentLinkProvider, tab]);
95
+
96
+ // Auto-focus the search field when the documents tab opens.
97
+ useEffect(() => {
98
+ if (tab === 'documents') {
99
+ const t = setTimeout(() => docSearchRef.current?.focus(), 0);
100
+ return () => clearTimeout(t);
101
+ }
102
+ return undefined;
103
+ }, [tab]);
104
+
105
+ useEffect(() => {
106
+ const onKey = (e: KeyboardEvent) => {
107
+ if (e.key === 'Escape') {
108
+ e.stopPropagation();
109
+ onClose();
110
+ }
111
+ };
112
+ window.addEventListener('keydown', onKey);
113
+ return () => window.removeEventListener('keydown', onKey);
114
+ }, [onClose]);
115
+
116
+ const handleSubmit = useCallback(
117
+ (e: React.FormEvent) => {
118
+ e.preventDefault();
119
+ onConfirm(text, url);
120
+ },
121
+ [text, url, onConfirm],
122
+ );
123
+
124
+ const handleBackdropClick = useCallback(
125
+ (e: React.MouseEvent) => {
126
+ if (e.target === e.currentTarget) onClose();
127
+ },
128
+ [onClose],
129
+ );
130
+
131
+ const isUpdate = mode === 'update';
132
+ const submitLabel = isUpdate ? 'Update' : 'Insert';
133
+ const heading = isUpdate ? 'Edit link' : 'Insert link';
134
+
135
+ const handlePickCandidate = useCallback((candidate: DocumentLinkCandidate) => {
136
+ // Picking a document fills the URL and the caption (when the
137
+ // caption is still empty — preserve any caption the user already
138
+ // typed). Switch back to the URL tab so the result is immediately
139
+ // visible and editable, and so Enter submits the form.
140
+ setUrl(candidate.path);
141
+ setText((existing) => existing || candidate.label);
142
+ setTab('url');
143
+ }, []);
144
+
145
+ return (
146
+ <div className="squisq-link-dialog-overlay" onMouseDown={handleBackdropClick}>
147
+ <form className="squisq-link-dialog" onSubmit={handleSubmit}>
148
+ <div className="squisq-link-dialog-header">
149
+ <h2 className="squisq-link-dialog-title">{heading}</h2>
150
+ <button
151
+ type="button"
152
+ className="squisq-link-dialog-close"
153
+ onClick={onClose}
154
+ aria-label="Close"
155
+ >
156
+ &times;
157
+ </button>
158
+ </div>
159
+ <div className="squisq-link-dialog-body">
160
+ {documentLinkProvider && (
161
+ <div className="squisq-link-dialog-tabs" role="tablist">
162
+ <button
163
+ type="button"
164
+ role="tab"
165
+ aria-selected={tab === 'url'}
166
+ className={`squisq-link-dialog-tab${tab === 'url' ? ' squisq-link-dialog-tab--active' : ''}`}
167
+ onClick={() => setTab('url')}
168
+ >
169
+ URL
170
+ </button>
171
+ <button
172
+ type="button"
173
+ role="tab"
174
+ aria-selected={tab === 'documents'}
175
+ className={`squisq-link-dialog-tab${tab === 'documents' ? ' squisq-link-dialog-tab--active' : ''}`}
176
+ onClick={() => setTab('documents')}
177
+ >
178
+ Browse documents
179
+ </button>
180
+ </div>
181
+ )}
182
+ <label className="squisq-link-dialog-field">
183
+ <span className="squisq-link-dialog-label">Text</span>
184
+ <input
185
+ ref={textRef}
186
+ type="text"
187
+ className="squisq-link-dialog-input"
188
+ value={text}
189
+ onChange={(e) => setText(e.target.value)}
190
+ placeholder="Link caption"
191
+ />
192
+ </label>
193
+ {tab === 'url' ? (
194
+ <label className="squisq-link-dialog-field">
195
+ <span className="squisq-link-dialog-label">URL</span>
196
+ <input
197
+ ref={urlRef}
198
+ type="text"
199
+ className="squisq-link-dialog-input"
200
+ value={url}
201
+ onChange={(e) => setUrl(e.target.value)}
202
+ placeholder="https://example.com"
203
+ spellCheck={false}
204
+ autoComplete="off"
205
+ />
206
+ </label>
207
+ ) : (
208
+ <div className="squisq-link-dialog-doc-picker">
209
+ <label className="squisq-link-dialog-field">
210
+ <span className="squisq-link-dialog-label">Search</span>
211
+ <input
212
+ ref={docSearchRef}
213
+ type="text"
214
+ className="squisq-link-dialog-input"
215
+ value={docQuery}
216
+ onChange={(e) => setDocQuery(e.target.value)}
217
+ placeholder="Type to filter…"
218
+ spellCheck={false}
219
+ autoComplete="off"
220
+ />
221
+ </label>
222
+ <div
223
+ className="squisq-link-dialog-doc-list"
224
+ role="listbox"
225
+ aria-label="Document candidates"
226
+ aria-busy={docLoading}
227
+ >
228
+ {docLoading && docResults.length === 0 ? (
229
+ <div className="squisq-link-dialog-doc-empty">Loading…</div>
230
+ ) : docResults.length === 0 ? (
231
+ <div className="squisq-link-dialog-doc-empty">
232
+ {docQuery.trim() ? `No matches for "${docQuery.trim()}"` : 'No documents'}
233
+ </div>
234
+ ) : (
235
+ docResults.map((c) => (
236
+ <button
237
+ key={c.path}
238
+ type="button"
239
+ role="option"
240
+ aria-selected={url === c.path}
241
+ className={`squisq-link-dialog-doc-item${url === c.path ? ' squisq-link-dialog-doc-item--selected' : ''}`}
242
+ onClick={() => handlePickCandidate(c)}
243
+ >
244
+ <span className="squisq-link-dialog-doc-item-label">{c.label}</span>
245
+ <span className="squisq-link-dialog-doc-item-path">{c.path}</span>
246
+ {c.description && (
247
+ <span className="squisq-link-dialog-doc-item-desc">{c.description}</span>
248
+ )}
249
+ </button>
250
+ ))
251
+ )}
252
+ </div>
253
+ {url && (
254
+ <div className="squisq-link-dialog-doc-current">
255
+ Selected: <code>{url}</code>
256
+ </div>
257
+ )}
258
+ </div>
259
+ )}
260
+ </div>
261
+ <div className="squisq-link-dialog-footer">
262
+ <button
263
+ type="button"
264
+ className="squisq-link-dialog-btn squisq-link-dialog-btn--secondary"
265
+ onClick={onClose}
266
+ >
267
+ Cancel
268
+ </button>
269
+ <button type="submit" className="squisq-link-dialog-btn squisq-link-dialog-btn--primary">
270
+ {submitLabel}
271
+ </button>
272
+ </div>
273
+ </form>
274
+ </div>
275
+ );
276
+ }
@@ -244,15 +244,18 @@ function positionTo(
244
244
  ): void {
245
245
  const rect = clientRect?.();
246
246
  if (!rect) return;
247
- // Anchor just below the caret; fall back to above when there's no room.
247
+ // Anchor above the caret first chat composers live near the
248
+ // bottom of the viewport, where a "below" popover gets clipped or
249
+ // covers the just-typed text. Fall back to below only when there's
250
+ // no room above (top of a long document, etc.).
248
251
  const viewportH = window.innerHeight;
249
- const below = rect.bottom + 4;
250
- const estH = Math.min(240, el.offsetHeight || 200);
251
- const fitsBelow = below + estH < viewportH;
252
+ const estH = Math.min(el.offsetHeight || 240, viewportH - 16);
253
+ const above = rect.top - estH - 4;
254
+ const fitsAbove = above >= 0;
252
255
  el.style.left = `${rect.left + window.scrollX}px`;
253
- if (fitsBelow) {
254
- el.style.top = `${below + window.scrollY}px`;
256
+ if (fitsAbove) {
257
+ el.style.top = `${above + window.scrollY}px`;
255
258
  } else {
256
- el.style.top = `${rect.top + window.scrollY - estH - 4}px`;
259
+ el.style.top = `${rect.bottom + 4 + window.scrollY}px`;
257
260
  }
258
261
  }
@@ -0,0 +1,295 @@
1
+ /**
2
+ * OutlinePanel
3
+ *
4
+ * Left-side companion to the InlinePreviewGutter. Renders a hierarchical
5
+ * tree of the document's headings (h1 → h2 → h3 …) so the structure is
6
+ * graspable at a glance and the user can jump to any section. Works in
7
+ * BOTH the WYSIWYG and Markdown editor views — view-specific positioning
8
+ * lives in `useHeadingLayout`.
9
+ */
10
+
11
+ import { type CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react';
12
+ import type { Block } from '@bendyline/squisq/schemas';
13
+ import { flattenBlocks, hasTemplate } from '@bendyline/squisq/doc';
14
+ import { extractPlainText } from '@bendyline/squisq/markdown';
15
+ import { useEditorContext } from './EditorContext';
16
+ import { templateLabel } from './TemplatePicker';
17
+ import { useHeadingLayout } from './useHeadingLayout';
18
+ import { usePreviewSettingsOptional } from './PreviewControls';
19
+
20
+ export interface OutlinePanelProps {
21
+ /** Width of the pane in pixels (default: 240). */
22
+ width?: number;
23
+ /** Optional CSS class for the outer container. */
24
+ className?: string;
25
+ }
26
+
27
+ export function OutlinePanel({ width = 240, className }: OutlinePanelProps) {
28
+ const { doc, markdownSource, setMarkdownSource } = useEditorContext();
29
+ const paneRef = useRef<HTMLElement | null>(null);
30
+ const { scrollToBlock } = useHeadingLayout(paneRef);
31
+ const activeBlockId = useActiveOutlineBlockId();
32
+
33
+ // Promote / demote the row's heading by rewriting just the `#` prefix
34
+ // on the heading line. Falls through when the new depth would leave the
35
+ // legal H1–H6 range, so the buttons disable themselves at the edges.
36
+ // Both editor surfaces resync from `markdownSource` automatically.
37
+ const changeHeadingLevel = useCallback(
38
+ (block: Block, delta: number) => {
39
+ const line = block.sourceHeading?.position?.start.line;
40
+ if (typeof line !== 'number') return;
41
+ const next = bumpHeadingLevelInSource(markdownSource, line, delta);
42
+ if (next != null) setMarkdownSource(next);
43
+ },
44
+ [markdownSource, setMarkdownSource],
45
+ );
46
+
47
+ // Inherit the active document theme's primary color so the current-row
48
+ // highlight and template-name chips match the rest of the editor's
49
+ // accent palette (e.g. warm-earth's terracotta) instead of the
50
+ // hard-coded purple fallback. Falls through to the CSS defaults when
51
+ // no PreviewSettingsProvider is mounted.
52
+ const previewSettings = usePreviewSettingsOptional();
53
+ const accentColor = previewSettings?.activeTheme?.colors?.primary;
54
+
55
+ const isEmpty = !doc || doc.blocks.length === 0 || !hasAnyHeading(doc.blocks);
56
+ const paneStyle: CSSProperties = {
57
+ width: `${width}px`,
58
+ flex: `0 0 ${width}px`,
59
+ overflow: 'auto',
60
+ ...(accentColor
61
+ ? ({ ['--squisq-outline-accent' as string]: accentColor } as CSSProperties)
62
+ : {}),
63
+ };
64
+
65
+ return (
66
+ <aside
67
+ ref={paneRef}
68
+ className={`squisq-outline${className ? ` ${className}` : ''}`}
69
+ style={paneStyle}
70
+ data-testid="outline-panel"
71
+ aria-label="Document outline"
72
+ >
73
+ {isEmpty ? (
74
+ <div className="squisq-outline-empty">
75
+ <p>Add a heading to populate the outline.</p>
76
+ </div>
77
+ ) : (
78
+ <ul className="squisq-outline-tree" role="tree">
79
+ {doc!.blocks.map((b) => (
80
+ <OutlineNode
81
+ key={b.id}
82
+ block={b}
83
+ activeBlockId={activeBlockId}
84
+ onSelect={scrollToBlock}
85
+ onChangeLevel={changeHeadingLevel}
86
+ />
87
+ ))}
88
+ </ul>
89
+ )}
90
+ </aside>
91
+ );
92
+ }
93
+
94
+ // ── Subcomponents ──────────────────────────────────────────────────
95
+
96
+ function OutlineNode({
97
+ block,
98
+ activeBlockId,
99
+ onSelect,
100
+ onChangeLevel,
101
+ }: {
102
+ block: Block;
103
+ activeBlockId: string | null;
104
+ onSelect: (b: Block) => void;
105
+ onChangeLevel: (block: Block, delta: number) => void;
106
+ }) {
107
+ const heading = block.sourceHeading;
108
+ const depth = heading?.depth ?? 1;
109
+ const text = heading ? extractPlainText(heading).trim() : '';
110
+ const annotation = heading?.templateAnnotation;
111
+ const tplName = annotation?.template;
112
+ const showChip = tplName && hasTemplate(tplName);
113
+ const isActive = block.id === activeBlockId;
114
+ const canPromote = !!heading && depth > 1;
115
+ const canDemote = !!heading && depth < 6;
116
+
117
+ return (
118
+ <li className="squisq-outline-item" role="treeitem" aria-current={isActive || undefined}>
119
+ <div className="squisq-outline-row-wrap">
120
+ <button
121
+ type="button"
122
+ className={`squisq-outline-row squisq-outline-row--depth-${depth}${
123
+ isActive ? ' squisq-outline-row--current' : ''
124
+ }`}
125
+ onClick={() => onSelect(block)}
126
+ title={text || '(empty heading)'}
127
+ >
128
+ <span className="squisq-outline-row-text">{text || '(untitled)'}</span>
129
+ {showChip && (
130
+ <span className="squisq-outline-template-chip">{templateLabel(tplName!)}</span>
131
+ )}
132
+ </button>
133
+ {heading && (
134
+ <span className="squisq-outline-row-actions">
135
+ <button
136
+ type="button"
137
+ className="squisq-outline-row-arrow"
138
+ aria-label={`Promote heading (currently H${depth})`}
139
+ title="Promote heading"
140
+ disabled={!canPromote}
141
+ onClick={() => onChangeLevel(block, -1)}
142
+ >
143
+ <svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
144
+ <path
145
+ d="M6.5 2.5 L3 5 L6.5 7.5"
146
+ stroke="currentColor"
147
+ strokeWidth="1.5"
148
+ strokeLinecap="round"
149
+ strokeLinejoin="round"
150
+ fill="none"
151
+ />
152
+ </svg>
153
+ </button>
154
+ <button
155
+ type="button"
156
+ className="squisq-outline-row-arrow"
157
+ aria-label={`Demote heading (currently H${depth})`}
158
+ title="Demote heading"
159
+ disabled={!canDemote}
160
+ onClick={() => onChangeLevel(block, +1)}
161
+ >
162
+ <svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
163
+ <path
164
+ d="M3.5 2.5 L7 5 L3.5 7.5"
165
+ stroke="currentColor"
166
+ strokeWidth="1.5"
167
+ strokeLinecap="round"
168
+ strokeLinejoin="round"
169
+ fill="none"
170
+ />
171
+ </svg>
172
+ </button>
173
+ </span>
174
+ )}
175
+ </div>
176
+ {block.children && block.children.length > 0 && (
177
+ <ul className="squisq-outline-tree">
178
+ {block.children.map((child) => (
179
+ <OutlineNode
180
+ key={child.id}
181
+ block={child}
182
+ activeBlockId={activeBlockId}
183
+ onSelect={onSelect}
184
+ onChangeLevel={onChangeLevel}
185
+ />
186
+ ))}
187
+ </ul>
188
+ )}
189
+ </li>
190
+ );
191
+ }
192
+
193
+ // ── Active-block tracking ──────────────────────────────────────────
194
+
195
+ /**
196
+ * Tracks which heading the user's cursor is currently inside (or most
197
+ * recently passed). In WYSIWYG mode this watches Tiptap's selection;
198
+ * in Raw mode it watches Monaco's cursor line. The Preview surface has
199
+ * no cursor concept and reports `null`.
200
+ *
201
+ * The lookup mirrors the heading-pairing logic in `useHeadingLayout`:
202
+ * the Nth heading in document order maps to `flattenBlocks(doc.blocks)[N]`.
203
+ */
204
+ function useActiveOutlineBlockId(): string | null {
205
+ const { doc, activeView, tiptapEditor, monacoEditor } = useEditorContext();
206
+ const flatBlocks = useMemo(() => (doc ? flattenBlocks(doc.blocks) : []), [doc]);
207
+ const [activeId, setActiveId] = useState<string | null>(null);
208
+
209
+ // Reset whenever the active surface changes — a stale highlight from
210
+ // the previous view would mislead the user before the new surface's
211
+ // cursor handler runs.
212
+ useEffect(() => {
213
+ setActiveId(null);
214
+ }, [activeView]);
215
+
216
+ useEffect(() => {
217
+ if (activeView !== 'wysiwyg' || !tiptapEditor) return;
218
+
219
+ const update = () => {
220
+ const { from } = tiptapEditor.state.selection;
221
+ let lastIndex = -1;
222
+ let seen = -1;
223
+ tiptapEditor.state.doc.forEach((node, offset) => {
224
+ if (node.type.name !== 'heading') return;
225
+ seen += 1;
226
+ if (offset <= from) lastIndex = seen;
227
+ });
228
+ const block = lastIndex >= 0 ? flatBlocks[lastIndex] : null;
229
+ setActiveId(block?.id ?? null);
230
+ };
231
+
232
+ update();
233
+ tiptapEditor.on('selectionUpdate', update);
234
+ tiptapEditor.on('update', update);
235
+ return () => {
236
+ tiptapEditor.off('selectionUpdate', update);
237
+ tiptapEditor.off('update', update);
238
+ };
239
+ }, [activeView, tiptapEditor, flatBlocks]);
240
+
241
+ useEffect(() => {
242
+ if (activeView !== 'raw' || !monacoEditor) return;
243
+
244
+ const update = () => {
245
+ const line = monacoEditor.getPosition()?.lineNumber;
246
+ if (typeof line !== 'number') {
247
+ setActiveId(null);
248
+ return;
249
+ }
250
+ let lastIndex = -1;
251
+ flatBlocks.forEach((b, i) => {
252
+ const headingLine = b.sourceHeading?.position?.start.line;
253
+ if (typeof headingLine === 'number' && headingLine <= line) lastIndex = i;
254
+ });
255
+ const block = lastIndex >= 0 ? flatBlocks[lastIndex] : null;
256
+ setActiveId(block?.id ?? null);
257
+ };
258
+
259
+ update();
260
+ const sub = monacoEditor.onDidChangeCursorPosition(update);
261
+ return () => sub.dispose();
262
+ }, [activeView, monacoEditor, flatBlocks]);
263
+
264
+ return activeId;
265
+ }
266
+
267
+ // ── Helpers ────────────────────────────────────────────────────────
268
+
269
+ function hasAnyHeading(blocks: Block[]): boolean {
270
+ for (const b of blocks) {
271
+ if (b.sourceHeading) return true;
272
+ if (b.children && hasAnyHeading(b.children)) return true;
273
+ }
274
+ return false;
275
+ }
276
+
277
+ /**
278
+ * Rewrites just the leading `#` run on the given 1-based line, shifting
279
+ * the heading depth by `delta`. Returns `null` when the line isn't an
280
+ * ATX heading or the resulting depth would fall outside 1–6. Leaves the
281
+ * rest of the line (including any `{[template]}` annotation) untouched.
282
+ */
283
+ function bumpHeadingLevelInSource(source: string, line: number, delta: number): string | null {
284
+ const lines = source.split('\n');
285
+ const idx = line - 1;
286
+ if (idx < 0 || idx >= lines.length) return null;
287
+ const original = lines[idx];
288
+ const match = original.match(/^(#{1,6})(\s|$)/);
289
+ if (!match) return null;
290
+ const currentDepth = match[1].length;
291
+ const newDepth = currentDepth + delta;
292
+ if (newDepth < 1 || newDepth > 6) return null;
293
+ lines[idx] = '#'.repeat(newDepth) + original.slice(currentDepth);
294
+ return lines.join('\n');
295
+ }