@bendyline/squisq-editor-react 2.0.1 → 2.2.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 (357) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +40 -0
  3. package/README.md +10 -2
  4. package/dist/index.d.ts +683 -60
  5. package/dist/index.js +9648 -3750
  6. package/dist/monaco.js +0 -1
  7. package/dist/styles/index.css +1160 -241
  8. package/package.json +10 -6
  9. package/dist/index.js.map +0 -1
  10. package/dist/monaco.js.map +0 -1
  11. package/src/BlockCardView.tsx +0 -121
  12. package/src/BlockPreviewPanel.tsx +0 -69
  13. package/src/BlockPropertiesPopover.tsx +0 -222
  14. package/src/DocumentSettingsDialog.tsx +0 -272
  15. package/src/DropZoneOverlay.tsx +0 -137
  16. package/src/EditorContext.tsx +0 -1247
  17. package/src/EditorShell.tsx +0 -1334
  18. package/src/EmojiPicker.tsx +0 -332
  19. package/src/FolderView.tsx +0 -131
  20. package/src/Icon.tsx +0 -26
  21. package/src/ImageEditor.tsx +0 -395
  22. package/src/ImageNodeView.tsx +0 -284
  23. package/src/ImageViewer.tsx +0 -221
  24. package/src/InlineIcon.ts +0 -84
  25. package/src/InlinePreviewGutter.tsx +0 -590
  26. package/src/LinkDialog.tsx +0 -276
  27. package/src/MediaBin.tsx +0 -500
  28. package/src/MentionExtension.tsx +0 -261
  29. package/src/OutlinePanel.tsx +0 -547
  30. package/src/PlainHtmlPreview.tsx +0 -277
  31. package/src/PreviewControls.tsx +0 -1250
  32. package/src/PreviewPanel.tsx +0 -422
  33. package/src/RawEditor.tsx +0 -731
  34. package/src/RecorderEntry.tsx +0 -159
  35. package/src/StatusBar.tsx +0 -51
  36. package/src/TemplateAnnotation.ts +0 -167
  37. package/src/TemplateContentPreview.tsx +0 -56
  38. package/src/TemplatePicker.tsx +0 -1314
  39. package/src/ThemeCustomizerPanel.tsx +0 -304
  40. package/src/ThemePicker.tsx +0 -433
  41. package/src/TimelineBlockPreview.tsx +0 -37
  42. package/src/TimelineTrack.tsx +0 -671
  43. package/src/Toolbar.tsx +0 -2587
  44. package/src/Tooltip.tsx +0 -142
  45. package/src/TransitionPicker.tsx +0 -390
  46. package/src/VersionHistoryPanel.tsx +0 -359
  47. package/src/ViewMenuPanel.tsx +0 -189
  48. package/src/ViewSwitcher.tsx +0 -56
  49. package/src/WysiwygEditor.tsx +0 -792
  50. package/src/__tests__/blockProperties.test.ts +0 -92
  51. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +0 -33
  52. package/src/__tests__/blockRange.test.ts +0 -105
  53. package/src/__tests__/blockTagActivity.test.ts +0 -183
  54. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +0 -75
  55. package/src/__tests__/buildPreviewDocContent.test.ts +0 -48
  56. package/src/__tests__/buildPreviewDocTransition.test.ts +0 -92
  57. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +0 -60
  58. package/src/__tests__/codeContextSectionView.test.tsx +0 -97
  59. package/src/__tests__/codeContextZoneManager.test.ts +0 -127
  60. package/src/__tests__/createShapeLayer.test.ts +0 -46
  61. package/src/__tests__/customLayoutManagerTheme.test.tsx +0 -34
  62. package/src/__tests__/detectMarkdown.test.ts +0 -73
  63. package/src/__tests__/diffContextSections.test.ts +0 -39
  64. package/src/__tests__/documentSettingsDialog.test.tsx +0 -197
  65. package/src/__tests__/drawingShapeRoundTrip.test.ts +0 -49
  66. package/src/__tests__/editorScrollHandoff.test.tsx +0 -102
  67. package/src/__tests__/editorScrollSync.test.ts +0 -65
  68. package/src/__tests__/editorShellCodeContext.test.tsx +0 -86
  69. package/src/__tests__/editorShellProps.test.tsx +0 -485
  70. package/src/__tests__/embeddedMedia.test.ts +0 -48
  71. package/src/__tests__/emojiPicker.test.tsx +0 -93
  72. package/src/__tests__/fileKind.test.ts +0 -112
  73. package/src/__tests__/headingTransition.test.ts +0 -176
  74. package/src/__tests__/imageEditAffordance.test.tsx +0 -272
  75. package/src/__tests__/imageEditModalContract.test.tsx +0 -119
  76. package/src/__tests__/imageEditorShell.test.tsx +0 -94
  77. package/src/__tests__/imageEditorState.test.ts +0 -171
  78. package/src/__tests__/inlinePreviewGutter.test.tsx +0 -62
  79. package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +0 -103
  80. package/src/__tests__/jsonEditor.test.tsx +0 -223
  81. package/src/__tests__/layersPanel.test.tsx +0 -167
  82. package/src/__tests__/layoutChildRoundTrip.test.ts +0 -71
  83. package/src/__tests__/linkDialogDocPicker.test.tsx +0 -137
  84. package/src/__tests__/majorApiTypes.test.ts +0 -26
  85. package/src/__tests__/mediaAttachmentFlow.test.ts +0 -110
  86. package/src/__tests__/mediaBinDrop.test.tsx +0 -90
  87. package/src/__tests__/mediaReferences.test.ts +0 -82
  88. package/src/__tests__/narrationSave.test.ts +0 -148
  89. package/src/__tests__/outlinePanel.test.tsx +0 -169
  90. package/src/__tests__/outlineSource.test.ts +0 -217
  91. package/src/__tests__/plainHtmlPreview.test.tsx +0 -125
  92. package/src/__tests__/presentationMode.test.tsx +0 -290
  93. package/src/__tests__/previewControls.test.tsx +0 -540
  94. package/src/__tests__/previewPanelAudioFailure.test.tsx +0 -46
  95. package/src/__tests__/previewPanelPresentation.test.tsx +0 -126
  96. package/src/__tests__/previewPanelTransforms.test.tsx +0 -117
  97. package/src/__tests__/propertiesPanel.test.tsx +0 -69
  98. package/src/__tests__/rawEditorModelIsolation.test.ts +0 -19
  99. package/src/__tests__/recorderFormats.test.ts +0 -146
  100. package/src/__tests__/recorderMediaInsert.test.ts +0 -86
  101. package/src/__tests__/recorderTheme.test.tsx +0 -42
  102. package/src/__tests__/recorderTimingJson.test.ts +0 -41
  103. package/src/__tests__/resolveBlockVisual.test.ts +0 -43
  104. package/src/__tests__/selectionConversions.test.ts +0 -80
  105. package/src/__tests__/statusBar.test.tsx +0 -27
  106. package/src/__tests__/teleprompter.test.tsx +0 -190
  107. package/src/__tests__/templateAnnotationRoundTrip.test.ts +0 -99
  108. package/src/__tests__/templateContentPreview.test.ts +0 -101
  109. package/src/__tests__/templatePickerMetadata.test.ts +0 -32
  110. package/src/__tests__/templatePickerPortal.test.tsx +0 -62
  111. package/src/__tests__/timelineSource.test.ts +0 -134
  112. package/src/__tests__/tiptapBridge.test.ts +0 -566
  113. package/src/__tests__/tiptapBridgeConformance.test.ts +0 -47
  114. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +0 -109
  115. package/src/__tests__/tiptapImageRoundTrip.test.ts +0 -73
  116. package/src/__tests__/toolbarSelectionConversion.test.tsx +0 -164
  117. package/src/__tests__/tooltip.test.tsx +0 -72
  118. package/src/__tests__/transformStyleId.test.ts +0 -13
  119. package/src/__tests__/transitionCatalog.test.ts +0 -64
  120. package/src/__tests__/useBlockNavigator.test.tsx +0 -67
  121. package/src/__tests__/useImageEditor.test.tsx +0 -226
  122. package/src/__tests__/useJsonEditorTokens.test.ts +0 -59
  123. package/src/__tests__/useMediaRecorder.test.ts +0 -302
  124. package/src/__tests__/useTimelineClock.test.ts +0 -21
  125. package/src/__tests__/versionHistory.test.tsx +0 -197
  126. package/src/__tests__/viewMenuPanel.test.tsx +0 -130
  127. package/src/__tests__/wysiwygImageUpload.test.ts +0 -69
  128. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +0 -56
  129. package/src/asciiDiagram/AsciiDiagramExtension.ts +0 -343
  130. package/src/asciiDiagram/AsciiDiagramWidget.tsx +0 -254
  131. package/src/asciiDiagram/RepairableDiagramExtension.ts +0 -200
  132. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +0 -145
  133. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +0 -159
  134. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +0 -284
  135. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +0 -114
  136. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +0 -43
  137. package/src/asciiDiagram/asciiDiagramCommands.ts +0 -174
  138. package/src/asciiDiagram/asciiDiagramData.ts +0 -149
  139. package/src/asciiDiagram/asciiDiagramOps.ts +0 -238
  140. package/src/asciiDiagram/asciiPaste.ts +0 -19
  141. package/src/blockProperties.ts +0 -88
  142. package/src/blockRange.ts +0 -132
  143. package/src/blockSlice.ts +0 -75
  144. package/src/blockTagActivity.ts +0 -233
  145. package/src/buildDocumentPreviewMarkdown.ts +0 -168
  146. package/src/buildPreviewDoc.ts +0 -405
  147. package/src/codeContext/CodeContextSectionView.tsx +0 -124
  148. package/src/codeContext/CodeContextZoneManager.ts +0 -149
  149. package/src/codeContext/CodeContextZones.tsx +0 -121
  150. package/src/codeContext/diffContextSections.ts +0 -38
  151. package/src/codeContext/types.ts +0 -75
  152. package/src/customTemplates/AddBin.tsx +0 -126
  153. package/src/customTemplates/CustomLayoutManager.tsx +0 -234
  154. package/src/customTemplates/CustomTemplateContext.tsx +0 -188
  155. package/src/customTemplates/LayerToolbar.tsx +0 -580
  156. package/src/customTemplates/ShapeGlyph.tsx +0 -47
  157. package/src/customTemplates/TemplateDesigner.tsx +0 -433
  158. package/src/customTemplates/__tests__/library.test.ts +0 -96
  159. package/src/customTemplates/__tests__/normalizePositions.test.ts +0 -109
  160. package/src/customTemplates/__tests__/shapeDefs.test.ts +0 -49
  161. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +0 -108
  162. package/src/customTemplates/designer.css +0 -732
  163. package/src/customTemplates/index.ts +0 -31
  164. package/src/customTemplates/library.ts +0 -110
  165. package/src/customTemplates/normalizePositions.ts +0 -75
  166. package/src/customTemplates/shapeDefs.ts +0 -131
  167. package/src/customTemplates/thumbnail.tsx +0 -63
  168. package/src/customTemplates/tokenDefs.ts +0 -60
  169. package/src/customTemplates/useDocCustomTemplates.ts +0 -52
  170. package/src/customTemplates/useMemoryLayerAdapter.ts +0 -129
  171. package/src/customThemes/CustomThemeContext.tsx +0 -185
  172. package/src/customThemes/CustomThemeDialog.tsx +0 -316
  173. package/src/customThemes/ImportThemeSection.tsx +0 -178
  174. package/src/customThemes/__tests__/CustomThemeContext.test.tsx +0 -64
  175. package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +0 -47
  176. package/src/customThemes/__tests__/customThemeLibrary.test.ts +0 -60
  177. package/src/customThemes/__tests__/importThemeSection.test.tsx +0 -192
  178. package/src/customThemes/customThemeLibrary.ts +0 -100
  179. package/src/customThemes/index.ts +0 -37
  180. package/src/customThemes/themeControls.tsx +0 -229
  181. package/src/customThemes/themeDraft.ts +0 -288
  182. package/src/customThemes/useDocCustomThemes.ts +0 -49
  183. package/src/detectMarkdown.ts +0 -62
  184. package/src/diagram/DiagramCanvas.tsx +0 -259
  185. package/src/diagram/DiagramMaximizedOverlay.tsx +0 -46
  186. package/src/diagram/diagramCommands.ts +0 -604
  187. package/src/diagram/diagramConstants.ts +0 -17
  188. package/src/diagram/types.ts +0 -35
  189. package/src/editorScrollSync.ts +0 -73
  190. package/src/embeddedMedia.ts +0 -78
  191. package/src/emojiData.ts +0 -1317
  192. package/src/fileKind.ts +0 -158
  193. package/src/frontmatter.ts +0 -29
  194. package/src/frontmatterSettings.ts +0 -23
  195. package/src/headingTransition.ts +0 -291
  196. package/src/hooks/useFileDrop.ts +0 -266
  197. package/src/imageEditor/CanvasSurface.tsx +0 -703
  198. package/src/imageEditor/ImageVersionHistoryDropdown.tsx +0 -396
  199. package/src/imageEditor/LayersPanel.tsx +0 -218
  200. package/src/imageEditor/PropertiesPanel.tsx +0 -474
  201. package/src/imageEditor/Toolbar.tsx +0 -459
  202. package/src/imageEditor/createShapeLayer.ts +0 -280
  203. package/src/imageEditor/icons.tsx +0 -68
  204. package/src/imageEditor/image-editor.css +0 -574
  205. package/src/imageEditor/layers/EditorImageLayer.tsx +0 -45
  206. package/src/imageEditor/layers/EditorShapeLayer.tsx +0 -62
  207. package/src/imageEditor/layers/EditorTextLayer.tsx +0 -45
  208. package/src/imageEditor/layers/SelectionHandles.tsx +0 -86
  209. package/src/imageEditor/state.ts +0 -173
  210. package/src/imageEditor/useImageEditor.ts +0 -385
  211. package/src/imageEditor/useImageEditorTokens.ts +0 -70
  212. package/src/index.ts +0 -470
  213. package/src/jsonEditor/EmbeddedRichTextField.tsx +0 -81
  214. package/src/jsonEditor/JsonEditor.tsx +0 -81
  215. package/src/jsonEditor/JsonEditorContext.tsx +0 -81
  216. package/src/jsonEditor/RenderNode.tsx +0 -87
  217. package/src/jsonEditor/editors.tsx +0 -745
  218. package/src/jsonEditor/index.ts +0 -2
  219. package/src/jsonEditor/json-editor.css +0 -463
  220. package/src/jsonEditor/useJsonEditorTokens.ts +0 -33
  221. package/src/mediaDragMime.ts +0 -32
  222. package/src/mediaEntries.ts +0 -12
  223. package/src/mediaReferences.ts +0 -299
  224. package/src/monaco.ts +0 -35
  225. package/src/monacoWorkers.ts +0 -89
  226. package/src/outlineSource.ts +0 -171
  227. package/src/presentation/PresentationMode.tsx +0 -596
  228. package/src/rawEditorIsolation.ts +0 -18
  229. package/src/recorder/RecorderButton.tsx +0 -80
  230. package/src/recorder/RecorderModal.tsx +0 -706
  231. package/src/recorder/RecorderPanel.tsx +0 -77
  232. package/src/recorder/formats.ts +0 -159
  233. package/src/recorder/hooks/useMediaRecorder.ts +0 -436
  234. package/src/recorder/hooks/useStreamPreview.ts +0 -47
  235. package/src/recorder/insertMediaBlock.ts +0 -30
  236. package/src/recorder/sources/cameraStream.ts +0 -32
  237. package/src/recorder/sources/micStream.ts +0 -25
  238. package/src/recorder/sources/screenStream.ts +0 -162
  239. package/src/recorder/timingJson.ts +0 -66
  240. package/src/resolveBlockVisual.ts +0 -39
  241. package/src/scene/Scene.tsx +0 -616
  242. package/src/scene/SceneBlockExtension.ts +0 -205
  243. package/src/scene/SceneBlockToolbar.tsx +0 -201
  244. package/src/scene/SceneBlockWidget.tsx +0 -441
  245. package/src/scene/ScenePropsBar.tsx +0 -85
  246. package/src/scene/SceneSelection.tsx +0 -111
  247. package/src/scene/SceneSideToolbar.tsx +0 -89
  248. package/src/scene/SceneViewport.tsx +0 -106
  249. package/src/scene/ShapePalette.tsx +0 -196
  250. package/src/scene/__tests__/DiagramAdapter.test.ts +0 -142
  251. package/src/scene/__tests__/SceneBlockExtension.test.ts +0 -33
  252. package/src/scene/__tests__/bezierEdit.test.ts +0 -85
  253. package/src/scene/__tests__/blockLayers.test.ts +0 -57
  254. package/src/scene/__tests__/sceneIsolation.test.tsx +0 -119
  255. package/src/scene/__tests__/shapeLayers.test.ts +0 -106
  256. package/src/scene/__tests__/useSceneHitTest.test.ts +0 -90
  257. package/src/scene/__tests__/useScenePanZoom.test.ts +0 -103
  258. package/src/scene/adapters/DiagramAdapter.ts +0 -79
  259. package/src/scene/adapters/DrawingAdapter.ts +0 -420
  260. package/src/scene/adapters/LayoutAdapter.ts +0 -313
  261. package/src/scene/adapters/blockLayers.ts +0 -159
  262. package/src/scene/commands/SceneCommand.ts +0 -84
  263. package/src/scene/commands/drawingCommands.ts +0 -318
  264. package/src/scene/commands/layoutCommands.ts +0 -301
  265. package/src/scene/hooks/useSceneHitTest.ts +0 -105
  266. package/src/scene/hooks/useScenePanZoom.ts +0 -147
  267. package/src/scene/hooks/useSceneSelection.ts +0 -62
  268. package/src/scene/index.ts +0 -90
  269. package/src/scene/layers/DiagramEdges.tsx +0 -142
  270. package/src/scene/layers/edgeGeometry.ts +0 -104
  271. package/src/scene/layers/nodeCard.tsx +0 -164
  272. package/src/scene/layers/renderLayer.tsx +0 -70
  273. package/src/scene/layers/shapeLayers.ts +0 -201
  274. package/src/scene/paths/bezierEdit.ts +0 -208
  275. package/src/scene/scene.css +0 -792
  276. package/src/scene/text/SceneTextOverlay.tsx +0 -161
  277. package/src/scene/text/sceneTextChannel.ts +0 -46
  278. package/src/scene/text/sceneTextConfig.ts +0 -31
  279. package/src/scene/text/sceneTiptap.ts +0 -36
  280. package/src/scene/text/useSceneTextEditing.ts +0 -39
  281. package/src/scene/tools/ConnectTool.ts +0 -209
  282. package/src/scene/tools/DrawingConnectTool.ts +0 -231
  283. package/src/scene/tools/PathTool.ts +0 -158
  284. package/src/scene/tools/PlaceTool.ts +0 -47
  285. package/src/scene/tools/SceneTool.ts +0 -85
  286. package/src/scene/tools/SelectTool.ts +0 -300
  287. package/src/scene/tools/ShapeTool.ts +0 -143
  288. package/src/scene/tools/TextTool.ts +0 -72
  289. package/src/scene/tools/TokenTool.ts +0 -95
  290. package/src/scene/tools/createDrawShapeTool.ts +0 -82
  291. package/src/selectionConversions.ts +0 -155
  292. package/src/styles/ascii-diagram.css +0 -79
  293. package/src/styles/ascii-timeline.css +0 -596
  294. package/src/styles/code-context.css +0 -155
  295. package/src/styles/diagram.css +0 -183
  296. package/src/styles/editor.css +0 -6811
  297. package/src/styles/folder-view.css +0 -210
  298. package/src/styles/image-edit-affordance.css +0 -201
  299. package/src/styles/index.css +0 -18
  300. package/src/styles/tree-view.css +0 -189
  301. package/src/teleprompter/TeleprompterControls.tsx +0 -218
  302. package/src/teleprompter/TeleprompterSelfView.tsx +0 -22
  303. package/src/teleprompter/TeleprompterSurface.tsx +0 -267
  304. package/src/teleprompter/TeleprompterView.tsx +0 -338
  305. package/src/teleprompter/canvasRenderer.ts +0 -161
  306. package/src/teleprompter/floatingWindow.ts +0 -352
  307. package/src/teleprompter/index.ts +0 -61
  308. package/src/teleprompter/pcmWorklet.ts +0 -62
  309. package/src/teleprompter/recording/insertPreamble.ts +0 -80
  310. package/src/teleprompter/recording/narrationSave.ts +0 -134
  311. package/src/teleprompter/recording/useNarrationRecorder.ts +0 -367
  312. package/src/teleprompter/scrollModel.ts +0 -85
  313. package/src/teleprompter/teleprompterTheme.ts +0 -303
  314. package/src/teleprompter/types.ts +0 -38
  315. package/src/teleprompter/useFloatingWindow.ts +0 -74
  316. package/src/teleprompter/useMicAnalysis.ts +0 -211
  317. package/src/teleprompter/useTeleprompter.ts +0 -421
  318. package/src/templateContentPreviewResolver.ts +0 -360
  319. package/src/timeline/TimelineEditorWidget.tsx +0 -861
  320. package/src/timeline/TimelineViewExtension.ts +0 -252
  321. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +0 -414
  322. package/src/timeline/__tests__/TimelineViewExtension.test.ts +0 -163
  323. package/src/timeline/__tests__/timelineCommands.test.ts +0 -359
  324. package/src/timeline/__tests__/timelineOps.test.ts +0 -277
  325. package/src/timeline/__tests__/timelinePaste.test.ts +0 -34
  326. package/src/timeline/timelineCommands.ts +0 -421
  327. package/src/timeline/timelineData.ts +0 -54
  328. package/src/timeline/timelineOps.ts +0 -381
  329. package/src/timeline/timelinePaste.ts +0 -8
  330. package/src/timelineSource.ts +0 -244
  331. package/src/tiptap/TiptapAudio.tsx +0 -86
  332. package/src/tiptap/TiptapVideo.tsx +0 -119
  333. package/src/tiptap/useResolvedMediaSrc.ts +0 -47
  334. package/src/tiptapBridge.ts +0 -986
  335. package/src/tooltipPlacement.ts +0 -13
  336. package/src/transformStyleId.ts +0 -17
  337. package/src/transitionCatalog.ts +0 -159
  338. package/src/treeview/TreeOutlineWidget.tsx +0 -390
  339. package/src/treeview/TreeViewExtension.ts +0 -250
  340. package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +0 -156
  341. package/src/treeview/__tests__/TreeViewExtension.test.ts +0 -122
  342. package/src/treeview/__tests__/treeOps.test.ts +0 -177
  343. package/src/treeview/__tests__/treePaste.test.ts +0 -40
  344. package/src/treeview/__tests__/treeViewCommands.test.ts +0 -179
  345. package/src/treeview/treeOps.ts +0 -231
  346. package/src/treeview/treePaste.ts +0 -13
  347. package/src/treeview/treeViewCommands.ts +0 -99
  348. package/src/treeview/treeViewData.ts +0 -41
  349. package/src/types/monaco-shims.d.ts +0 -10
  350. package/src/useBlockNavigator.ts +0 -153
  351. package/src/useHeadingLayout.ts +0 -294
  352. package/src/useMonacoLoader.ts +0 -82
  353. package/src/useTimelineClock.ts +0 -76
  354. package/src/utils/collectInlineFontAwesomeCss.ts +0 -69
  355. package/src/utils/dropUtils.ts +0 -136
  356. package/src/utils/normalizeMalformedAssetUrl.ts +0 -22
  357. package/src/wysiwygImageUpload.ts +0 -113
@@ -1,45 +0,0 @@
1
- /**
2
- * SVG renderer for an `ImageEditLayer` of kind `text` inside the editor.
3
- *
4
- * Multi-line text is laid out as a `<text>` element with one `<tspan>`
5
- * per line. The first line sits at `y + fontSize` so the visible top
6
- * of the glyph block aligns with the layer's `y` coordinate (matches
7
- * what users expect from a top-anchored bounding box).
8
- */
9
-
10
- import type { ImageEditCanvas, ImageEditLayer } from '@bendyline/squisq/schemas';
11
-
12
- interface Props {
13
- layer: ImageEditLayer & { type: 'text' };
14
- canvas: ImageEditCanvas;
15
- }
16
-
17
- export function EditorTextLayer({ layer, canvas: _canvas }: Props) {
18
- const p = layer.position;
19
- const x = typeof p.x === 'number' ? p.x : 0;
20
- const y = typeof p.y === 'number' ? p.y : 0;
21
- const { text, style } = layer.content;
22
- const lineHeight = style.lineHeight ?? 1.4;
23
- const lineHeightPx = style.fontSize * lineHeight;
24
- const lines = (text ?? '').split('\n');
25
- const textAnchor =
26
- style.textAlign === 'center' ? 'middle' : style.textAlign === 'right' ? 'end' : 'start';
27
-
28
- return (
29
- <text
30
- x={x}
31
- y={y + style.fontSize}
32
- fontFamily={style.fontFamily ?? 'sans-serif'}
33
- fontSize={style.fontSize}
34
- fontWeight={style.fontWeight ?? 'normal'}
35
- fill={style.color}
36
- textAnchor={textAnchor}
37
- >
38
- {lines.map((line, i) => (
39
- <tspan key={i} x={x} dy={i === 0 ? 0 : lineHeightPx}>
40
- {line || '\u00A0'}
41
- </tspan>
42
- ))}
43
- </text>
44
- );
45
- }
@@ -1,86 +0,0 @@
1
- /**
2
- * SelectionHandles
3
- *
4
- * Renders the dashed selection rectangle plus eight resize handles around
5
- * the currently-selected layer's bounding box. Pointer events on each
6
- * handle bubble back through `onHandlePointerDown` so the surrounding
7
- * `<CanvasSurface>` can run its drag loop with the right resize semantics.
8
- */
9
-
10
- import type { CanvasRect } from '../state.js';
11
-
12
- export type Handle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';
13
-
14
- const HANDLE_SIZE = 10; // canvas-pixel size of each square handle
15
-
16
- interface Props {
17
- box: CanvasRect;
18
- onHandlePointerDown: (e: React.PointerEvent<SVGRectElement>, handle: Handle) => void;
19
- }
20
-
21
- export function SelectionHandles({ box, onHandlePointerDown }: Props) {
22
- const half = HANDLE_SIZE / 2;
23
- const cx = box.x + box.width / 2;
24
- const cy = box.y + box.height / 2;
25
-
26
- const handles: Array<{ id: Handle; x: number; y: number; cursor: string }> = [
27
- { id: 'nw', x: box.x, y: box.y, cursor: 'nwse-resize' },
28
- { id: 'n', x: cx, y: box.y, cursor: 'ns-resize' },
29
- { id: 'ne', x: box.x + box.width, y: box.y, cursor: 'nesw-resize' },
30
- { id: 'e', x: box.x + box.width, y: cy, cursor: 'ew-resize' },
31
- { id: 'se', x: box.x + box.width, y: box.y + box.height, cursor: 'nwse-resize' },
32
- { id: 's', x: cx, y: box.y + box.height, cursor: 'ns-resize' },
33
- { id: 'sw', x: box.x, y: box.y + box.height, cursor: 'nesw-resize' },
34
- { id: 'w', x: box.x, y: cy, cursor: 'ew-resize' },
35
- ];
36
-
37
- return (
38
- <g className="squisq-image-editor-selection-handles" pointerEvents="none">
39
- {/*
40
- * Outline: rendered as two stacked rectangles so the selection
41
- * stays visible over both light and dark imagery. The white
42
- * "halo" goes underneath; the blue dashed line on top. Both use
43
- * `vector-effect: non-scaling-stroke` so the stroke width stays
44
- * crisp at any zoom level (the SVG viewBox is the canvas size,
45
- * which can be much larger than the rendered element).
46
- */}
47
- <rect
48
- x={box.x}
49
- y={box.y}
50
- width={box.width}
51
- height={box.height}
52
- fill="none"
53
- stroke="#ffffff"
54
- strokeOpacity={0.9}
55
- strokeWidth={4}
56
- vectorEffect="non-scaling-stroke"
57
- />
58
- <rect
59
- x={box.x}
60
- y={box.y}
61
- width={box.width}
62
- height={box.height}
63
- fill="none"
64
- stroke="#39f"
65
- strokeWidth={2}
66
- strokeDasharray="6 4"
67
- vectorEffect="non-scaling-stroke"
68
- />
69
- {handles.map((h) => (
70
- <rect
71
- key={h.id}
72
- x={h.x - half}
73
- y={h.y - half}
74
- width={HANDLE_SIZE}
75
- height={HANDLE_SIZE}
76
- fill="#fff"
77
- stroke="#39f"
78
- strokeWidth={2}
79
- vectorEffect="non-scaling-stroke"
80
- style={{ cursor: h.cursor, pointerEvents: 'all' }}
81
- onPointerDown={(e) => onHandlePointerDown(e, h.id)}
82
- />
83
- ))}
84
- </g>
85
- );
86
- }
@@ -1,173 +0,0 @@
1
- /**
2
- * Pure state + reducer for the `<ImageEditor>` component.
3
- *
4
- * Kept React-free so it's easy to unit-test in isolation. The actual
5
- * React hook that wires this to a {@link ContentContainer} (and the
6
- * version manager) lives in `useImageEditor.ts`.
7
- */
8
-
9
- import type { ImageEditDoc, ImageEditLayer } from '@bendyline/squisq/schemas';
10
- import {
11
- addLayer,
12
- removeLayer,
13
- reorderLayer,
14
- setCanvas,
15
- updateLayer,
16
- touch,
17
- } from '@bendyline/squisq/imageEdit';
18
-
19
- // Distributive Omit: applied per union member so the discriminant (`type`) is preserved.
20
- type DOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
21
-
22
- /**
23
- * Layer payload accepted by the `add-layer` action — the `id` field is
24
- * optional and will be assigned by the underlying `addLayer` helper if
25
- * the caller doesn't supply one.
26
- */
27
- export type ImageEditLayerInput = ImageEditLayer | (DOmit<ImageEditLayer, 'id'> & { id?: string });
28
-
29
- /** The currently active interaction tool. */
30
- export type ImageEditorTool = 'select' | 'text' | 'shape' | 'image' | 'crop' | 'zoom-rect';
31
-
32
- /** A pixel-space rectangle in canvas coordinates. */
33
- export interface CanvasRect {
34
- x: number;
35
- y: number;
36
- width: number;
37
- height: number;
38
- }
39
-
40
- export interface ImageEditorState {
41
- /** The persisted document. */
42
- doc: ImageEditDoc;
43
- /** Selected layer id, or `null` when nothing is selected. */
44
- selectedLayerId: string | null;
45
- /** Active tool. */
46
- tool: ImageEditorTool;
47
- /**
48
- * The shape kind the shape tool will drop next (a drawing palette kind,
49
- * e.g. `'rectangle'`, `'diamond'`, `'arrow-right'`). Set when the user
50
- * picks from the shape palette; defaults to `'rectangle'`.
51
- */
52
- shapeKind: string;
53
- /**
54
- * Dirty flag — true when the in-memory doc has unsaved changes
55
- * relative to the last `markClean()` call. The hook uses this to
56
- * debounce writes back to `state.json`.
57
- */
58
- dirty: boolean;
59
- }
60
-
61
- export type ImageEditorAction =
62
- | { type: 'load'; doc: ImageEditDoc }
63
- | { type: 'mark-clean' }
64
- | { type: 'set-tool'; tool: ImageEditorTool }
65
- | { type: 'set-shape-kind'; kind: string }
66
- | { type: 'select'; layerId: string | null }
67
- | { type: 'set-canvas'; canvas: ImageEditDoc['canvas'] }
68
- | { type: 'add-layer'; layer: ImageEditLayerInput; select?: boolean }
69
- | { type: 'remove-layer'; layerId: string }
70
- | { type: 'update-layer'; layerId: string; patch: Partial<ImageEditLayer> }
71
- | { type: 'reorder-layer'; layerId: string; toIndex: number }
72
- | { type: 'crop'; rect: CanvasRect };
73
-
74
- /** Build the initial state from a freshly-loaded doc. */
75
- export function initialImageEditorState(doc: ImageEditDoc): ImageEditorState {
76
- return {
77
- doc,
78
- selectedLayerId: null,
79
- tool: 'select',
80
- shapeKind: 'rectangle',
81
- dirty: false,
82
- };
83
- }
84
-
85
- export function imageEditorReducer(
86
- state: ImageEditorState,
87
- action: ImageEditorAction,
88
- ): ImageEditorState {
89
- switch (action.type) {
90
- case 'load':
91
- return {
92
- doc: action.doc,
93
- selectedLayerId: null,
94
- tool: 'select',
95
- shapeKind: state.shapeKind,
96
- dirty: false,
97
- };
98
-
99
- case 'mark-clean':
100
- return state.dirty ? { ...state, dirty: false } : state;
101
-
102
- case 'set-tool':
103
- return state.tool === action.tool ? state : { ...state, tool: action.tool };
104
-
105
- case 'set-shape-kind':
106
- return state.shapeKind === action.kind ? state : { ...state, shapeKind: action.kind };
107
-
108
- case 'select':
109
- return state.selectedLayerId === action.layerId
110
- ? state
111
- : { ...state, selectedLayerId: action.layerId };
112
-
113
- case 'set-canvas': {
114
- const next = setCanvas(state.doc, action.canvas);
115
- return next === state.doc ? state : { ...state, doc: next, dirty: true };
116
- }
117
-
118
- case 'add-layer': {
119
- // `addLayer` from core assigns an id when missing; cast through
120
- // `ImageEditLayer` for the existing helper signature.
121
- const next = addLayer(state.doc, action.layer as ImageEditLayer);
122
- // Pick up the assigned id (last layer)
123
- const newId = next.layers[next.layers.length - 1]!.id;
124
- return {
125
- ...state,
126
- doc: next,
127
- dirty: true,
128
- selectedLayerId: action.select === false ? state.selectedLayerId : newId,
129
- };
130
- }
131
-
132
- case 'remove-layer': {
133
- const next = removeLayer(state.doc, action.layerId);
134
- if (next === state.doc) return state;
135
- return {
136
- ...state,
137
- doc: next,
138
- dirty: true,
139
- selectedLayerId: state.selectedLayerId === action.layerId ? null : state.selectedLayerId,
140
- };
141
- }
142
-
143
- case 'update-layer': {
144
- const next = updateLayer(state.doc, action.layerId, action.patch);
145
- return next === state.doc ? state : { ...state, doc: next, dirty: true };
146
- }
147
-
148
- case 'reorder-layer': {
149
- const next = reorderLayer(state.doc, action.layerId, action.toIndex);
150
- return next === state.doc ? state : { ...state, doc: next, dirty: true };
151
- }
152
-
153
- case 'crop': {
154
- const { rect } = action;
155
- const newCanvas = {
156
- ...state.doc.canvas,
157
- width: Math.max(1, Math.round(rect.width)),
158
- height: Math.max(1, Math.round(rect.height)),
159
- };
160
- // Translate every layer by (-rect.x, -rect.y). Only handles numeric
161
- // positions; percentage strings are left alone (rare for image-edit
162
- // layers, which the editor produces with numeric coords).
163
- const translated: ImageEditLayer[] = state.doc.layers.map((layer) => {
164
- const { position } = layer;
165
- const x = typeof position.x === 'number' ? position.x - rect.x : position.x;
166
- const y = typeof position.y === 'number' ? position.y - rect.y : position.y;
167
- return { ...layer, position: { ...position, x, y } } as ImageEditLayer;
168
- });
169
- const next = touch({ ...state.doc, canvas: newCanvas, layers: translated });
170
- return { ...state, doc: next, dirty: true };
171
- }
172
- }
173
- }
@@ -1,385 +0,0 @@
1
- /**
2
- * React hook that bundles the image-editor reducer with sidecar
3
- * persistence, versioning, and an object-URL cache for asset bytes.
4
- *
5
- * Hosts pass an already-scoped {@link ContentContainer} (typically built
6
- * with `scopeContainer(parent, basename + '_files')`); the hook never
7
- * looks above that root.
8
- */
9
-
10
- import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
11
- import type { ContentContainer } from '@bendyline/squisq/storage';
12
- import type { ImageEditDoc, ImageEditLayer } from '@bendyline/squisq/schemas';
13
- import {
14
- IMAGE_EDIT_ASSETS_PREFIX,
15
- IMAGE_EDIT_STATE_FILENAME,
16
- ImageEditVersionManager,
17
- createEmptyImageEditDoc,
18
- readImageEditDoc,
19
- writeImageEditDoc,
20
- } from '@bendyline/squisq/imageEdit';
21
- import {
22
- imageEditorReducer,
23
- initialImageEditorState,
24
- type ImageEditorAction,
25
- type ImageEditorState,
26
- } from './state.js';
27
-
28
- export interface UseImageEditorOptions {
29
- /** Sidecar container for the image being edited. */
30
- container: ContentContainer;
31
- /**
32
- * Initial source image URL — used to seed layer 0 when the sidecar has
33
- * no `state.json` yet. Bytes are fetched and copied into
34
- * `assets/source.<ext>` so the doc is portable.
35
- */
36
- initialSrc?: string;
37
- /** Override the state filename. Defaults to `state.json`. */
38
- stateFilename?: string;
39
- /** Enable version history. Default: `false`. */
40
- allowVersioning?: boolean;
41
- /** Auto-save idle delay (ms). `0` disables. Default: `5000`. */
42
- versioningAutoSaveIdleMs?: number;
43
- /** Debounced write delay for state.json (ms). Default: `500`. */
44
- persistDebounceMs?: number;
45
- }
46
-
47
- export interface UseImageEditorReturn {
48
- /** Current reducer state (or `null` while still loading the initial doc). */
49
- state: ImageEditorState | null;
50
- /** Dispatch a reducer action. No-op while loading. */
51
- dispatch: (action: ImageEditorAction) => void;
52
- /** Manually trigger a synchronous write of `state.json`. */
53
- flush: () => Promise<void>;
54
- /** Resolve an asset path inside the sidecar to a blob URL (cached). */
55
- resolveAssetUrl: (path: string) => Promise<string>;
56
- /**
57
- * Write a new asset (raster image) into `assets/` and return the
58
- * sidecar-relative path. The caller is then expected to push a layer
59
- * referencing that path.
60
- */
61
- uploadAsset: (file: Blob, suggestedName?: string) => Promise<string>;
62
- /** Versioning handle. `null` when `allowVersioning` is false or no container. */
63
- versioning: ImageEditVersionManager | null;
64
- /** True after the initial load completes (either an existing doc or seeded). */
65
- ready: boolean;
66
- /** Last load / persistence error, if any. */
67
- error: Error | null;
68
- }
69
-
70
- export function useImageEditor(options: UseImageEditorOptions): UseImageEditorReturn {
71
- const {
72
- container,
73
- initialSrc,
74
- stateFilename = IMAGE_EDIT_STATE_FILENAME,
75
- allowVersioning = false,
76
- versioningAutoSaveIdleMs = 5000,
77
- persistDebounceMs = 500,
78
- } = options;
79
-
80
- const [state, dispatch] = useReducer(
81
- (s: ImageEditorState | null, a: ImageEditorAction): ImageEditorState | null => {
82
- if (s === null) return a.type === 'load' ? initialImageEditorState(a.doc) : null;
83
- return imageEditorReducer(s, a);
84
- },
85
- null,
86
- );
87
- const [ready, setReady] = useState(false);
88
- const [error, setError] = useState<Error | null>(null);
89
- // Set to true inside the initial-load effect when we just seeded the
90
- // sidecar (no prior `state.json`). The versioning effect below reads
91
- // this flag to write an "original" snapshot once the manager exists.
92
- const seededOnLoadRef = useRef(false);
93
-
94
- // ── Initial load (or seed from initialSrc) ────────────────────────────
95
- useEffect(() => {
96
- let cancelled = false;
97
- setReady(false);
98
- setError(null);
99
-
100
- (async () => {
101
- try {
102
- const existing = await readImageEditDoc(container, stateFilename);
103
- if (cancelled) return;
104
- if (existing) {
105
- dispatch({ type: 'load', doc: existing });
106
- setReady(true);
107
- return;
108
- }
109
- // No existing state — seed.
110
- const seeded = await seedFromSource(container, initialSrc);
111
- if (cancelled) return;
112
- await writeImageEditDoc(container, seeded, stateFilename);
113
- if (cancelled) return;
114
- dispatch({ type: 'load', doc: seeded });
115
- setReady(true);
116
- // Capture an initial snapshot of the freshly-seeded state so the
117
- // version history always has an "original" entry the user can
118
- // revert to after their first edit.
119
- seededOnLoadRef.current = true;
120
- } catch (err: unknown) {
121
- if (cancelled) return;
122
- setError(err instanceof Error ? err : new Error(String(err)));
123
- setReady(true);
124
- }
125
- })();
126
-
127
- return () => {
128
- cancelled = true;
129
- };
130
- }, [container, stateFilename, initialSrc]);
131
-
132
- // ── Debounced persistence of state.json ────────────────────────────────
133
- const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
134
- const docRef = useRef<ImageEditDoc | null>(null);
135
- const dirtyRef = useRef(false);
136
- const revisionRef = useRef(0);
137
- const previousDocRef = useRef<ImageEditDoc | null>(null);
138
- const nextDoc = state?.doc ?? null;
139
- if (nextDoc !== previousDocRef.current) {
140
- previousDocRef.current = nextDoc;
141
- revisionRef.current += 1;
142
- }
143
- docRef.current = nextDoc;
144
- dirtyRef.current = state?.dirty ?? false;
145
- const writeQueueRef = useRef<Promise<void>>(Promise.resolve());
146
- const persistTargetRef = useRef({ container, stateFilename });
147
- persistTargetRef.current = { container, stateFilename };
148
-
149
- const enqueueWrite = useCallback(
150
- (doc: ImageEditDoc, revision: number, markClean: boolean): Promise<void> => {
151
- const write = writeQueueRef.current
152
- .catch(() => undefined)
153
- .then(() => writeImageEditDoc(container, doc, stateFilename));
154
- writeQueueRef.current = write.catch(() => undefined);
155
- return write.then(() => {
156
- if (markClean && revision === revisionRef.current && docRef.current === doc) {
157
- dispatch({ type: 'mark-clean' });
158
- }
159
- });
160
- },
161
- [container, stateFilename],
162
- );
163
-
164
- useEffect(() => {
165
- if (!state?.dirty) return;
166
- if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
167
- persistTimerRef.current = setTimeout(() => {
168
- const doc = state.doc;
169
- if (!doc) return;
170
- const revision = revisionRef.current;
171
- enqueueWrite(doc, revision, true).catch((err: unknown) => {
172
- console.warn(
173
- '[squisq-editor] image-edit state persist failed:',
174
- err instanceof Error ? err.message : err,
175
- );
176
- });
177
- }, persistDebounceMs);
178
- return () => {
179
- if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
180
- };
181
- }, [state?.dirty, state?.doc, persistDebounceMs, enqueueWrite]);
182
-
183
- const flush = useCallback(async () => {
184
- const doc = docRef.current;
185
- if (!doc) return;
186
- if (persistTimerRef.current) {
187
- clearTimeout(persistTimerRef.current);
188
- persistTimerRef.current = null;
189
- }
190
- await enqueueWrite(doc, revisionRef.current, true);
191
- }, [enqueueWrite]);
192
-
193
- // A quick modal close must not discard the final edit that is still inside
194
- // the debounce window. Queue it behind any in-flight write on unmount.
195
- useEffect(
196
- () => () => {
197
- if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
198
- const doc = docRef.current;
199
- if (!dirtyRef.current || !doc) return;
200
- const target = persistTargetRef.current;
201
- const write = writeQueueRef.current
202
- .catch(() => undefined)
203
- .then(() => writeImageEditDoc(target.container, doc, target.stateFilename));
204
- writeQueueRef.current = write.catch((err: unknown) => {
205
- console.warn(
206
- '[squisq-editor] image-edit final persist failed:',
207
- err instanceof Error ? err.message : err,
208
- );
209
- });
210
- },
211
- [],
212
- );
213
-
214
- // ── Versioning ─────────────────────────────────────────────────────────
215
- const versioning = useMemo(
216
- () => (allowVersioning ? new ImageEditVersionManager(container, { stateFilename }) : null),
217
- [allowVersioning, container, stateFilename],
218
- );
219
-
220
- // Drop the "original" snapshot once versioning is wired and we just
221
- // seeded a fresh sidecar. Guarded by `seededOnLoadRef` so we never
222
- // duplicate-snapshot on subsequent renders. Uses `force: true` so the
223
- // initial entry always lands even though no diff has occurred yet.
224
- useEffect(() => {
225
- if (!versioning) return;
226
- if (!ready) return;
227
- if (!seededOnLoadRef.current) return;
228
- seededOnLoadRef.current = false;
229
- versioning.saveVersion({ force: true }).catch((err: unknown) => {
230
- console.warn(
231
- '[squisq-editor] image-edit initial snapshot failed:',
232
- err instanceof Error ? err.message : err,
233
- );
234
- });
235
- }, [versioning, ready]);
236
-
237
- useEffect(() => {
238
- if (!versioning) return;
239
- if (versioningAutoSaveIdleMs <= 0) return;
240
- if (!state?.doc) return;
241
- const timer = setTimeout(() => {
242
- versioning.saveVersion({ doc: docRef.current ?? undefined }).catch((err: unknown) => {
243
- console.warn(
244
- '[squisq-editor] image-edit auto-save version failed:',
245
- err instanceof Error ? err.message : err,
246
- );
247
- });
248
- }, versioningAutoSaveIdleMs);
249
- return () => clearTimeout(timer);
250
- }, [versioning, versioningAutoSaveIdleMs, state?.doc]);
251
-
252
- // ── Asset URL cache ────────────────────────────────────────────────────
253
- const urlCacheRef = useRef<Map<string, string>>(new Map());
254
- const urlCacheGenerationRef = useRef(0);
255
-
256
- const resolveAssetUrl = useCallback(
257
- async (path: string): Promise<string> => {
258
- const cache = urlCacheRef.current;
259
- const generation = urlCacheGenerationRef.current;
260
- const cached = cache.get(path);
261
- if (cached) return cached;
262
- const data = await container.readFile(path);
263
- if (generation !== urlCacheGenerationRef.current) {
264
- throw new Error('useImageEditor: asset resolution cancelled');
265
- }
266
- if (!data) throw new Error(`useImageEditor: missing asset "${path}"`);
267
- const list = await container.listFiles(path);
268
- const mime = list.find((e) => e.path === path)?.mimeType ?? 'application/octet-stream';
269
- const url = URL.createObjectURL(new Blob([data], { type: mime }));
270
- if (generation !== urlCacheGenerationRef.current) {
271
- URL.revokeObjectURL(url);
272
- throw new Error('useImageEditor: asset resolution cancelled');
273
- }
274
- cache.set(path, url);
275
- return url;
276
- },
277
- [container],
278
- );
279
-
280
- // Revoke all cached object URLs on unmount / container swap
281
- useEffect(() => {
282
- const cache = urlCacheRef.current;
283
- return () => {
284
- urlCacheGenerationRef.current += 1;
285
- for (const url of cache.values()) URL.revokeObjectURL(url);
286
- cache.clear();
287
- };
288
- }, [container]);
289
-
290
- const uploadAsset = useCallback(
291
- async (file: Blob, suggestedName?: string): Promise<string> => {
292
- const ext = guessExtensionFromMime(file.type) ?? extensionFromName(suggestedName) ?? 'bin';
293
- const id = randomId();
294
- const path = `${IMAGE_EDIT_ASSETS_PREFIX}${id}.${ext}`;
295
- const buf = await file.arrayBuffer();
296
- await container.writeFile(path, buf, file.type || undefined);
297
- return path;
298
- },
299
- [container],
300
- );
301
-
302
- return {
303
- state,
304
- dispatch,
305
- flush,
306
- resolveAssetUrl,
307
- uploadAsset,
308
- versioning,
309
- ready,
310
- error,
311
- };
312
- }
313
-
314
- // ============================================
315
- // Helpers
316
- // ============================================
317
-
318
- async function seedFromSource(
319
- container: ContentContainer,
320
- initialSrc: string | undefined,
321
- ): Promise<ImageEditDoc> {
322
- if (!initialSrc) {
323
- return createEmptyImageEditDoc(800, 600);
324
- }
325
- // Fetch the source bytes (works for blob:, data:, http(s):, and same-origin
326
- // relative URLs).
327
- const resp = await fetch(initialSrc);
328
- if (!resp.ok) throw new Error(`useImageEditor: failed to fetch initialSrc (${resp.status})`);
329
- const blob = await resp.blob();
330
- const ext = guessExtensionFromMime(blob.type) ?? 'png';
331
- const assetPath = `${IMAGE_EDIT_ASSETS_PREFIX}source.${ext}`;
332
- await container.writeFile(assetPath, await blob.arrayBuffer(), blob.type || undefined);
333
-
334
- // Probe natural dimensions by loading into an Image.
335
- const dims = await probeImageDimensions(initialSrc);
336
- const w = dims?.width ?? 800;
337
- const h = dims?.height ?? 600;
338
-
339
- const layer: ImageEditLayer = {
340
- id: 'base',
341
- type: 'image',
342
- name: 'Background',
343
- position: { x: 0, y: 0, width: w, height: h },
344
- content: { src: assetPath, alt: '', fit: 'fill' },
345
- };
346
- return {
347
- version: 1,
348
- canvas: { width: w, height: h, background: 'transparent' },
349
- layers: [layer],
350
- meta: {
351
- sourcePath: assetPath,
352
- createdAt: new Date().toISOString(),
353
- updatedAt: new Date().toISOString(),
354
- },
355
- };
356
- }
357
-
358
- function probeImageDimensions(src: string): Promise<{ width: number; height: number } | null> {
359
- return new Promise((resolve) => {
360
- const img = new Image();
361
- img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
362
- img.onerror = () => resolve(null);
363
- img.src = src;
364
- });
365
- }
366
-
367
- function guessExtensionFromMime(mime: string | undefined): string | null {
368
- if (!mime) return null;
369
- if (mime.includes('png')) return 'png';
370
- if (mime.includes('jpeg') || mime.includes('jpg')) return 'jpg';
371
- if (mime.includes('webp')) return 'webp';
372
- if (mime.includes('gif')) return 'gif';
373
- if (mime.includes('svg')) return 'svg';
374
- return null;
375
- }
376
-
377
- function extensionFromName(name: string | undefined): string | null {
378
- if (!name) return null;
379
- const dot = name.lastIndexOf('.');
380
- return dot >= 0 ? name.slice(dot + 1).toLowerCase() : null;
381
- }
382
-
383
- function randomId(): string {
384
- return Math.random().toString(36).slice(2, 10);
385
- }