@bendyline/squisq-editor-react 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,428 @@
1
+ /**
2
+ * PropertiesPanel — kind-aware editors for the canvas and the selected
3
+ * layer. Plain controls only (no JsonEditor dependency) so the panel
4
+ * stays focused on the image-editing vocabulary.
5
+ */
6
+
7
+ import type { ImageEditDoc, ImageEditLayer } from '@bendyline/squisq/schemas';
8
+ import type { ImageEditorAction } from './state.js';
9
+ import { NoneIcon } from './icons.js';
10
+
11
+ export interface PropertiesPanelProps {
12
+ doc: ImageEditDoc;
13
+ selectedLayerId: string | null;
14
+ dispatch: (action: ImageEditorAction) => void;
15
+ }
16
+
17
+ export function PropertiesPanel({ doc, selectedLayerId, dispatch }: PropertiesPanelProps) {
18
+ const selected = selectedLayerId
19
+ ? (doc.layers.find((l) => l.id === selectedLayerId) ?? null)
20
+ : null;
21
+
22
+ return (
23
+ <div className="squisq-image-editor-properties" data-testid="image-editor-properties">
24
+ <div className="squisq-image-editor-panel-header">Properties</div>
25
+ <CanvasSection doc={doc} dispatch={dispatch} />
26
+ {selected ? (
27
+ <LayerSection layer={selected} dispatch={dispatch} />
28
+ ) : (
29
+ <div className="squisq-image-editor-properties-empty">No layer selected</div>
30
+ )}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ // ============================================
36
+ // Canvas section
37
+ // ============================================
38
+
39
+ function CanvasSection({
40
+ doc,
41
+ dispatch,
42
+ }: {
43
+ doc: ImageEditDoc;
44
+ dispatch: (a: ImageEditorAction) => void;
45
+ }) {
46
+ const setCanvas = (patch: Partial<ImageEditDoc['canvas']>) => {
47
+ dispatch({ type: 'set-canvas', canvas: { ...doc.canvas, ...patch } });
48
+ };
49
+ return (
50
+ <fieldset className="squisq-image-editor-fieldset">
51
+ <legend>Canvas</legend>
52
+ <NumberField
53
+ label="Width"
54
+ value={doc.canvas.width}
55
+ min={1}
56
+ onChange={(v) => setCanvas({ width: Math.round(v) })}
57
+ />
58
+ <NumberField
59
+ label="Height"
60
+ value={doc.canvas.height}
61
+ min={1}
62
+ onChange={(v) => setCanvas({ height: Math.round(v) })}
63
+ />
64
+ <ColorField
65
+ label="Background"
66
+ value={doc.canvas.background ?? 'transparent'}
67
+ allowTransparent
68
+ onChange={(v) => setCanvas({ background: v })}
69
+ />
70
+ </fieldset>
71
+ );
72
+ }
73
+
74
+ // ============================================
75
+ // Layer section
76
+ // ============================================
77
+
78
+ function LayerSection({
79
+ layer,
80
+ dispatch,
81
+ }: {
82
+ layer: ImageEditLayer;
83
+ dispatch: (a: ImageEditorAction) => void;
84
+ }) {
85
+ const update = (patch: Partial<ImageEditLayer>) =>
86
+ dispatch({ type: 'update-layer', layerId: layer.id, patch });
87
+
88
+ return (
89
+ <>
90
+ <fieldset className="squisq-image-editor-fieldset">
91
+ <legend>Layer</legend>
92
+ <TextField label="Name" value={layer.name ?? ''} onChange={(name) => update({ name })} />
93
+ <NumberField
94
+ label="Opacity"
95
+ value={layer.opacity ?? 1}
96
+ min={0}
97
+ max={1}
98
+ step={0.05}
99
+ onChange={(opacity) => update({ opacity })}
100
+ />
101
+ </fieldset>
102
+
103
+ <PositionFields layer={layer} update={update} />
104
+
105
+ {layer.type === 'image' && <ImageFields layer={layer} update={update} />}
106
+ {layer.type === 'text' && <TextFields layer={layer} update={update} />}
107
+ {layer.type === 'shape' && <ShapeFields layer={layer} update={update} />}
108
+ </>
109
+ );
110
+ }
111
+
112
+ function PositionFields({
113
+ layer,
114
+ update,
115
+ }: {
116
+ layer: ImageEditLayer;
117
+ update: (patch: Partial<ImageEditLayer>) => void;
118
+ }) {
119
+ const p = layer.position;
120
+ const setPos = (patch: Partial<typeof p>) =>
121
+ update({ position: { ...p, ...patch } } as Partial<ImageEditLayer>);
122
+ return (
123
+ <fieldset className="squisq-image-editor-fieldset">
124
+ <legend>Position</legend>
125
+ <NumberField
126
+ label="X"
127
+ value={typeof p.x === 'number' ? p.x : 0}
128
+ onChange={(x) => setPos({ x: Math.round(x) })}
129
+ />
130
+ <NumberField
131
+ label="Y"
132
+ value={typeof p.y === 'number' ? p.y : 0}
133
+ onChange={(y) => setPos({ y: Math.round(y) })}
134
+ />
135
+ <NumberField
136
+ label="Width"
137
+ value={typeof p.width === 'number' ? p.width : 0}
138
+ onChange={(width) => setPos({ width: Math.round(width) })}
139
+ />
140
+ <NumberField
141
+ label="Height"
142
+ value={typeof p.height === 'number' ? p.height : 0}
143
+ onChange={(height) => setPos({ height: Math.round(height) })}
144
+ />
145
+ </fieldset>
146
+ );
147
+ }
148
+
149
+ function ImageFields({
150
+ layer,
151
+ update,
152
+ }: {
153
+ layer: ImageEditLayer & { type: 'image' };
154
+ update: (patch: Partial<ImageEditLayer>) => void;
155
+ }) {
156
+ const c = layer.content;
157
+ const setContent = (patch: Partial<typeof c>) =>
158
+ update({ content: { ...c, ...patch } } as Partial<ImageEditLayer>);
159
+ return (
160
+ <fieldset className="squisq-image-editor-fieldset">
161
+ <legend>Image</legend>
162
+ <TextField label="Alt" value={c.alt ?? ''} onChange={(alt) => setContent({ alt })} />
163
+ <SelectField
164
+ label="Fit"
165
+ value={c.fit ?? 'fill'}
166
+ options={[
167
+ ['fill', 'Fill'],
168
+ ['contain', 'Contain'],
169
+ ['cover', 'Cover'],
170
+ ]}
171
+ onChange={(fit) => setContent({ fit: fit as 'fill' | 'contain' | 'cover' })}
172
+ />
173
+ </fieldset>
174
+ );
175
+ }
176
+
177
+ function TextFields({
178
+ layer,
179
+ update,
180
+ }: {
181
+ layer: ImageEditLayer & { type: 'text' };
182
+ update: (patch: Partial<ImageEditLayer>) => void;
183
+ }) {
184
+ const c = layer.content;
185
+ const setContent = (patch: Partial<typeof c>) =>
186
+ update({ content: { ...c, ...patch } } as Partial<ImageEditLayer>);
187
+ const setStyle = (patch: Partial<typeof c.style>) =>
188
+ setContent({ style: { ...c.style, ...patch } });
189
+ return (
190
+ <fieldset className="squisq-image-editor-fieldset">
191
+ <legend>Text</legend>
192
+ <TextAreaField label="Text" value={c.text} onChange={(text) => setContent({ text })} />
193
+ <NumberField
194
+ label="Font size"
195
+ value={c.style.fontSize}
196
+ min={1}
197
+ onChange={(fontSize) => setStyle({ fontSize: Math.round(fontSize) })}
198
+ />
199
+ <ColorField label="Color" value={c.style.color} onChange={(color) => setStyle({ color })} />
200
+ <SelectField
201
+ label="Weight"
202
+ value={c.style.fontWeight ?? 'normal'}
203
+ options={[
204
+ ['normal', 'Normal'],
205
+ ['bold', 'Bold'],
206
+ ]}
207
+ onChange={(fontWeight) => setStyle({ fontWeight: fontWeight as 'normal' | 'bold' })}
208
+ />
209
+ <SelectField
210
+ label="Align"
211
+ value={c.style.textAlign ?? 'left'}
212
+ options={[
213
+ ['left', 'Left'],
214
+ ['center', 'Center'],
215
+ ['right', 'Right'],
216
+ ]}
217
+ onChange={(textAlign) => setStyle({ textAlign: textAlign as 'left' | 'center' | 'right' })}
218
+ />
219
+ </fieldset>
220
+ );
221
+ }
222
+
223
+ function ShapeFields({
224
+ layer,
225
+ update,
226
+ }: {
227
+ layer: ImageEditLayer & { type: 'shape' };
228
+ update: (patch: Partial<ImageEditLayer>) => void;
229
+ }) {
230
+ const c = layer.content;
231
+ const setContent = (patch: Partial<typeof c>) =>
232
+ update({ content: { ...c, ...patch } } as Partial<ImageEditLayer>);
233
+ return (
234
+ <fieldset className="squisq-image-editor-fieldset">
235
+ <legend>Shape</legend>
236
+ <SelectField
237
+ label="Shape"
238
+ value={c.shape}
239
+ options={[
240
+ ['rect', 'Rectangle'],
241
+ ['circle', 'Circle'],
242
+ ['line', 'Line'],
243
+ ]}
244
+ onChange={(shape) => setContent({ shape: shape as 'rect' | 'circle' | 'line' })}
245
+ />
246
+ <ColorField
247
+ label="Fill"
248
+ value={c.fill ?? '#000000'}
249
+ allowTransparent
250
+ onChange={(fill) => setContent({ fill })}
251
+ />
252
+ <ColorField
253
+ label="Stroke"
254
+ value={c.stroke ?? '#000000'}
255
+ allowTransparent
256
+ onChange={(stroke) => setContent({ stroke })}
257
+ />
258
+ <NumberField
259
+ label="Stroke width"
260
+ value={c.strokeWidth ?? 0}
261
+ min={0}
262
+ onChange={(strokeWidth) => setContent({ strokeWidth })}
263
+ />
264
+ {c.shape === 'rect' && (
265
+ <NumberField
266
+ label="Corner radius"
267
+ value={c.borderRadius ?? 0}
268
+ min={0}
269
+ onChange={(borderRadius) => setContent({ borderRadius: Math.round(borderRadius) })}
270
+ />
271
+ )}
272
+ </fieldset>
273
+ );
274
+ }
275
+
276
+ // ============================================
277
+ // Field primitives
278
+ // ============================================
279
+
280
+ function NumberField({
281
+ label,
282
+ value,
283
+ min,
284
+ max,
285
+ step,
286
+ onChange,
287
+ }: {
288
+ label: string;
289
+ value: number;
290
+ min?: number;
291
+ max?: number;
292
+ step?: number;
293
+ onChange: (v: number) => void;
294
+ }) {
295
+ return (
296
+ <label className="squisq-image-editor-field">
297
+ <span>{label}</span>
298
+ <input
299
+ type="number"
300
+ value={Number.isFinite(value) ? value : 0}
301
+ min={min}
302
+ max={max}
303
+ step={step ?? 1}
304
+ onChange={(e) => {
305
+ const n = Number(e.target.value);
306
+ if (Number.isFinite(n)) onChange(n);
307
+ }}
308
+ />
309
+ </label>
310
+ );
311
+ }
312
+
313
+ function TextField({
314
+ label,
315
+ value,
316
+ onChange,
317
+ }: {
318
+ label: string;
319
+ value: string;
320
+ onChange: (v: string) => void;
321
+ }) {
322
+ return (
323
+ <label className="squisq-image-editor-field">
324
+ <span>{label}</span>
325
+ <input type="text" value={value} onChange={(e) => onChange(e.target.value)} />
326
+ </label>
327
+ );
328
+ }
329
+
330
+ function TextAreaField({
331
+ label,
332
+ value,
333
+ onChange,
334
+ }: {
335
+ label: string;
336
+ value: string;
337
+ onChange: (v: string) => void;
338
+ }) {
339
+ return (
340
+ <label className="squisq-image-editor-field squisq-image-editor-field--multiline">
341
+ <span>{label}</span>
342
+ <textarea rows={3} value={value} onChange={(e) => onChange(e.target.value)} />
343
+ </label>
344
+ );
345
+ }
346
+
347
+ function SelectField({
348
+ label,
349
+ value,
350
+ options,
351
+ onChange,
352
+ }: {
353
+ label: string;
354
+ value: string;
355
+ options: Array<[string, string]>;
356
+ onChange: (v: string) => void;
357
+ }) {
358
+ return (
359
+ <label className="squisq-image-editor-field">
360
+ <span>{label}</span>
361
+ <select value={value} onChange={(e) => onChange(e.target.value)}>
362
+ {options.map(([v, l]) => (
363
+ <option key={v} value={v}>
364
+ {l}
365
+ </option>
366
+ ))}
367
+ </select>
368
+ </label>
369
+ );
370
+ }
371
+
372
+ function ColorField({
373
+ label,
374
+ value,
375
+ allowTransparent,
376
+ onChange,
377
+ }: {
378
+ label: string;
379
+ value: string;
380
+ allowTransparent?: boolean;
381
+ onChange: (v: string) => void;
382
+ }) {
383
+ const isTransparent = value === 'transparent' || value === 'none';
384
+ return (
385
+ <label className="squisq-image-editor-field">
386
+ <span>{label}</span>
387
+ <span className="squisq-image-editor-color-row">
388
+ <input
389
+ type="color"
390
+ value={isTransparent ? '#000000' : normalizeColor(value)}
391
+ onChange={(e) => onChange(e.target.value)}
392
+ />
393
+ <input
394
+ type="text"
395
+ value={value}
396
+ onChange={(e) => onChange(e.target.value)}
397
+ spellCheck={false}
398
+ />
399
+ {allowTransparent && (
400
+ <button
401
+ type="button"
402
+ onClick={() => onChange('transparent')}
403
+ title="Set transparent"
404
+ aria-label="Set transparent"
405
+ className="squisq-image-editor-color-clear"
406
+ >
407
+ <NoneIcon />
408
+ </button>
409
+ )}
410
+ </span>
411
+ </label>
412
+ );
413
+ }
414
+
415
+ function normalizeColor(v: string): string {
416
+ if (/^#[0-9a-f]{6}$/i.test(v)) return v;
417
+ if (/^#[0-9a-f]{3}$/i.test(v)) {
418
+ return (
419
+ '#' +
420
+ v
421
+ .slice(1)
422
+ .split('')
423
+ .map((c) => c + c)
424
+ .join('')
425
+ );
426
+ }
427
+ return '#000000';
428
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Toolbar — tool selector, layer-creation buttons, file-bin uploader,
3
+ * and a Save/Export button. Versioning controls live in
4
+ * `<ImageEditor>` so the toolbar stays purely about authoring tools.
5
+ */
6
+
7
+ import { useRef, useState, useEffect } from 'react';
8
+ import type { ImageEditDoc } from '@bendyline/squisq/schemas';
9
+ import type { ImageEditorAction, ImageEditorTool } from './state.js';
10
+ import { CropIcon, CursorIcon, PlusIcon, ShapeIcon, TextIcon } from './icons.js';
11
+
12
+ export interface ToolbarProps {
13
+ doc: ImageEditDoc;
14
+ tool: ImageEditorTool;
15
+ dispatch: (a: ImageEditorAction) => void;
16
+ /** Upload an image asset and return its sidecar-relative path. */
17
+ uploadAsset: (file: Blob, suggestedName?: string) => Promise<string>;
18
+ /** Trigger an export (PNG/JPEG/WebP) of the flattened canvas. */
19
+ onExport: (format: 'png' | 'jpeg' | 'webp') => void;
20
+ /** Force-flush the state.json (host's "save" button). */
21
+ onSave?: () => void;
22
+ /** Override the Save button label. Default: "Save". */
23
+ saveLabel?: string;
24
+ /** Override the Save button tooltip. Default: "Save state.json". */
25
+ saveTitle?: string;
26
+ /**
27
+ * Optional extra controls rendered just before the Save / Export
28
+ * buttons in the right-aligned tool group. Used by `<ImageEditor>` to
29
+ * mount the version-history dropdown when versioning is enabled.
30
+ */
31
+ extraTools?: React.ReactNode;
32
+ }
33
+
34
+ const TOOLS: Array<{
35
+ id: ImageEditorTool;
36
+ icon: React.ReactNode;
37
+ title: string;
38
+ }> = [
39
+ { id: 'select', icon: <CursorIcon />, title: 'Select / move (V)' },
40
+ { id: 'text', icon: <TextIcon />, title: 'Add text (T)' },
41
+ { id: 'shape', icon: <ShapeIcon />, title: 'Add shape (S)' },
42
+ { id: 'crop', icon: <CropIcon />, title: 'Crop (C)' },
43
+ ];
44
+
45
+ export function Toolbar({
46
+ doc,
47
+ tool,
48
+ dispatch,
49
+ uploadAsset,
50
+ onExport,
51
+ onSave,
52
+ saveLabel = 'Save',
53
+ saveTitle = 'Save state.json',
54
+ extraTools,
55
+ }: ToolbarProps) {
56
+ const fileInputRef = useRef<HTMLInputElement | null>(null);
57
+
58
+ const onFilePicked = async (file: File) => {
59
+ try {
60
+ const path = await uploadAsset(file, file.name);
61
+ // Place the imported image centered at its native size up to canvas.
62
+ const dims = await probeDims(file);
63
+ const w = Math.min(dims.width, doc.canvas.width);
64
+ const h = Math.min(dims.height, doc.canvas.height);
65
+ dispatch({
66
+ type: 'add-layer',
67
+ layer: {
68
+ type: 'image',
69
+ name: file.name,
70
+ position: {
71
+ x: Math.round((doc.canvas.width - w) / 2),
72
+ y: Math.round((doc.canvas.height - h) / 2),
73
+ width: w,
74
+ height: h,
75
+ },
76
+ content: { src: path, alt: file.name, fit: 'fill' },
77
+ },
78
+ });
79
+ } catch (err: unknown) {
80
+ console.warn(
81
+ '[squisq-editor] image upload failed:',
82
+ err instanceof Error ? err.message : err,
83
+ );
84
+ }
85
+ };
86
+
87
+ return (
88
+ <div className="squisq-image-editor-toolbar" data-testid="image-editor-toolbar">
89
+ <div className="squisq-image-editor-tool-group" role="radiogroup" aria-label="Tools">
90
+ {TOOLS.map((t) => (
91
+ <button
92
+ key={t.id}
93
+ type="button"
94
+ role="radio"
95
+ aria-checked={tool === t.id}
96
+ className={['squisq-image-editor-tool-button', tool === t.id ? 'is-active' : '']
97
+ .filter(Boolean)
98
+ .join(' ')}
99
+ onClick={() => dispatch({ type: 'set-tool', tool: t.id })}
100
+ title={t.title}
101
+ aria-label={t.title}
102
+ >
103
+ {t.icon}
104
+ </button>
105
+ ))}
106
+ </div>
107
+
108
+ <div className="squisq-image-editor-tool-group">
109
+ <button
110
+ type="button"
111
+ className="squisq-image-editor-tool-button squisq-image-editor-tool-button--with-label"
112
+ onClick={() => fileInputRef.current?.click()}
113
+ title="Import image as new layer"
114
+ aria-label="Import image as new layer"
115
+ >
116
+ <PlusIcon />
117
+ <span>Image</span>
118
+ </button>
119
+ <input
120
+ ref={fileInputRef}
121
+ type="file"
122
+ accept="image/*"
123
+ hidden
124
+ onChange={(e) => {
125
+ const file = e.target.files?.[0];
126
+ if (file) void onFilePicked(file);
127
+ e.target.value = '';
128
+ }}
129
+ />
130
+ </div>
131
+
132
+ <div className="squisq-image-editor-tool-group squisq-image-editor-tool-group--right">
133
+ {extraTools}
134
+ {onSave && (
135
+ <button
136
+ type="button"
137
+ className="squisq-image-editor-tool-button"
138
+ onClick={onSave}
139
+ title={saveTitle}
140
+ >
141
+ {saveLabel}
142
+ </button>
143
+ )}
144
+ <ExportDropdown onExport={onExport} />
145
+ </div>
146
+ </div>
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Single export dropdown listing all output formats. Replaces the
152
+ * earlier split "Export PNG / Other format…" pair so the toolbar reads
153
+ * as one Export control with a stable label and consistent sizing.
154
+ */
155
+ function ExportDropdown({ onExport }: { onExport: (f: 'png' | 'jpeg' | 'webp') => void }) {
156
+ const [open, setOpen] = useState(false);
157
+ const wrapRef = useRef<HTMLSpanElement | null>(null);
158
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
159
+
160
+ useEffect(() => {
161
+ if (!open) return;
162
+ function onDocClick(e: MouseEvent) {
163
+ const t = e.target as Node | null;
164
+ if (!t) return;
165
+ if (wrapRef.current?.contains(t)) return;
166
+ setOpen(false);
167
+ }
168
+ function onKey(e: KeyboardEvent) {
169
+ if (e.key === 'Escape') setOpen(false);
170
+ }
171
+ document.addEventListener('mousedown', onDocClick);
172
+ document.addEventListener('keydown', onKey);
173
+ return () => {
174
+ document.removeEventListener('mousedown', onDocClick);
175
+ document.removeEventListener('keydown', onKey);
176
+ };
177
+ }, [open]);
178
+
179
+ const pick = (f: 'png' | 'jpeg' | 'webp') => {
180
+ setOpen(false);
181
+ onExport(f);
182
+ };
183
+
184
+ return (
185
+ <span ref={wrapRef} className="squisq-image-editor-version-dropdown">
186
+ <button
187
+ ref={triggerRef}
188
+ type="button"
189
+ className="squisq-image-editor-tool-button squisq-image-editor-tool-button--with-label"
190
+ onClick={() => setOpen((o) => !o)}
191
+ aria-haspopup="menu"
192
+ aria-expanded={open}
193
+ title="Export image"
194
+ >
195
+ <span>Export</span>
196
+ <span aria-hidden="true" style={{ fontSize: '0.8em' }}>
197
+
198
+ </span>
199
+ </button>
200
+ {open && (
201
+ <div className="squisq-image-editor-version-popover" role="menu" style={{ minWidth: 160 }}>
202
+ <ul className="squisq-image-editor-version-popover__list" style={{ maxHeight: 'none' }}>
203
+ {(
204
+ [
205
+ { f: 'png', label: 'PNG' },
206
+ { f: 'jpeg', label: 'JPEG' },
207
+ { f: 'webp', label: 'WebP' },
208
+ ] as const
209
+ ).map(({ f, label }) => (
210
+ <li key={f} className="squisq-image-editor-version-popover__row">
211
+ <button
212
+ type="button"
213
+ role="menuitem"
214
+ className="squisq-image-editor-tool-button squisq-image-editor-tool-button--menu"
215
+ onClick={() => pick(f)}
216
+ >
217
+ Export as {label}
218
+ </button>
219
+ </li>
220
+ ))}
221
+ </ul>
222
+ </div>
223
+ )}
224
+ </span>
225
+ );
226
+ }
227
+
228
+ function probeDims(file: Blob): Promise<{ width: number; height: number }> {
229
+ return new Promise((resolve) => {
230
+ const url = URL.createObjectURL(file);
231
+ const img = new Image();
232
+ img.onload = () => {
233
+ URL.revokeObjectURL(url);
234
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
235
+ };
236
+ img.onerror = () => {
237
+ URL.revokeObjectURL(url);
238
+ resolve({ width: 200, height: 200 });
239
+ };
240
+ img.src = url;
241
+ });
242
+ }