@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,616 +0,0 @@
1
- /**
2
- * Scene — the editing surface used by diagrams, layouts, and drawings.
3
- *
4
- * Owns the viewport (pan/zoom), the selection state, and the active
5
- * tool. Tools are plain `SceneTool` objects that respond to pointer/
6
- * keyboard events via the `SceneToolContext` the Scene constructs each
7
- * render. Adapters (DiagramAdapter, LayoutAdapter, DrawingAdapter) sit
8
- * outside the Scene and translate `SceneCommand`s into domain edits
9
- * (markdown attrs, Tiptap mutations, etc).
10
- *
11
- * The Scene is deliberately small — most of the logic lives in the hooks
12
- * (pan/zoom, hit-test, selection) and the tools (interaction rules).
13
- */
14
-
15
- import {
16
- useCallback,
17
- useEffect,
18
- useLayoutEffect,
19
- useMemo,
20
- useRef,
21
- useState,
22
- type ReactNode,
23
- } from 'react';
24
- import type { Layer } from '@bendyline/squisq/schemas';
25
- import type { SceneCommand, SceneEdge } from './commands/SceneCommand';
26
- import type { SceneInteractionState, SceneTool, SceneToolContext } from './tools/SceneTool';
27
- import { useScenePanZoom } from './hooks/useScenePanZoom';
28
- import { useSceneSelection } from './hooks/useSceneSelection';
29
- import { useSceneHitTest, layerBounds, type HitTestable } from './hooks/useSceneHitTest';
30
- import { SceneViewport } from './SceneViewport';
31
- import { SceneSelection, type ResizeCorner } from './SceneSelection';
32
- import { getActiveMoveOffset, getActiveResize, beginHandleDrag } from './tools/SelectTool';
33
- import { RenderLayer } from './layers/renderLayer';
34
- import { useSceneTextEditing } from './text/useSceneTextEditing';
35
- import { SceneTextOverlay } from './text/SceneTextOverlay';
36
- import type { SceneTextEditConfig } from './text/sceneTextConfig';
37
-
38
- export interface SceneProps {
39
- /** Viewport size in viewport units. Layers render in this coordinate space. */
40
- viewport: { width: number; height: number };
41
- /** Layers, back-to-front. Identity changes trigger re-render. */
42
- layers: readonly Layer[];
43
- /** Optional edges (diagram mode). Rendered as PathLayer-style curves. */
44
- edges?: readonly SceneEdge[];
45
- /** Ordered list of available tools. The first is active by default. */
46
- tools: readonly SceneTool[];
47
- /** Override the active tool id. Falls back to `tools[0].id`. */
48
- activeToolId?: string;
49
- /** Called when the user picks a different tool from the built-in toolbar. */
50
- onActiveToolIdChange?: (id: string) => void;
51
- /** Called when a tool issues a command. */
52
- onCommand: (cmd: SceneCommand) => void;
53
- /** Called when the layer selection changes (lets a host drive a properties panel). */
54
- onSelectionChange?: (selectedIds: ReadonlySet<string>) => void;
55
- /**
56
- * Optional renderer for non-Layer content (e.g. diagram edges, in-flight
57
- * connection preview). Receives the same SceneToolContext the tools do.
58
- * Rendered behind the layer pass so edges sit below cards.
59
- */
60
- renderExtras?: (ctx: SceneToolContext) => ReactNode;
61
- /**
62
- * Optional override for how individual layers render. Defaults to the
63
- * SSR-identical `RenderLayer` from `packages/react/src/layers/`. Hosts
64
- * that want bespoke per-layer decoration (custom diagram card shells,
65
- * inline editing affordances) can replace this.
66
- */
67
- renderLayer?: (layer: Layer, viewport: { width: number; height: number }) => ReactNode;
68
- /**
69
- * Optional follower relationship — given a layer id, return the id of
70
- * the layer it "follows" (or null when independent). A follower
71
- * receives the same live drag/resize transform as its owner, so a
72
- * decorative pair (e.g. a diagram node's card + label) moves together
73
- * even though only one is selectable. Pure visual sugar — selection
74
- * state itself is unaffected.
75
- */
76
- layerFollows?: (layerId: string) => string | null;
77
- /** When true, render the corner maximize toggle. */
78
- showMaximize?: boolean;
79
- maximized?: boolean;
80
- onToggleMaximize?: () => void;
81
- /** Render the built-in toolbar (Select / Connect / etc.). Default true. */
82
- showToolbar?: boolean;
83
- /**
84
- * Enables the shared inline text editor. When provided, double-clicking a
85
- * (resolvable) layer opens a Tiptap overlay over it. The host supplies
86
- * read/commit per its persistence model. Omit to disable text editing.
87
- */
88
- textEditing?: SceneTextEditConfig;
89
- /**
90
- * Called when content is dropped onto the canvas via HTML5 drag-and-drop
91
- * (e.g. a placeholder dragged from the template designer's palette).
92
- * `point` is in viewport coordinates — already through the pan/zoom
93
- * transform — so callers can build a layer at the drop location. When
94
- * omitted, the canvas accepts no drops.
95
- */
96
- onDrop?: (e: React.DragEvent<SVGSVGElement>, point: { x: number; y: number }) => void;
97
- }
98
-
99
- export function Scene(props: SceneProps) {
100
- const {
101
- viewport,
102
- layers,
103
- edges = [],
104
- tools,
105
- activeToolId: controlledActiveId,
106
- onActiveToolIdChange,
107
- onCommand,
108
- onSelectionChange,
109
- renderExtras,
110
- renderLayer,
111
- layerFollows,
112
- showMaximize,
113
- maximized,
114
- onToggleMaximize,
115
- showToolbar = true,
116
- textEditing,
117
- onDrop,
118
- } = props;
119
-
120
- const layerRenderer = renderLayer ?? defaultRenderLayer;
121
-
122
- // Inline text editing (double-click a layer → Tiptap overlay).
123
- const textEdit = useSceneTextEditing(textEditing);
124
-
125
- // ── Tool state ──────────────────────────────────────────────
126
- const [internalActiveId, setInternalActiveId] = useState<string>(tools[0]?.id ?? 'select');
127
- const activeId = controlledActiveId ?? internalActiveId;
128
- const setActiveTool = useCallback(
129
- (id: string) => {
130
- if (onActiveToolIdChange) onActiveToolIdChange(id);
131
- else setInternalActiveId(id);
132
- },
133
- [onActiveToolIdChange],
134
- );
135
- const activeTool = useMemo(
136
- () => tools.find((t) => t.id === activeId) ?? tools[0],
137
- [tools, activeId],
138
- );
139
-
140
- // ── Pan/zoom + selection ────────────────────────────────────
141
- const panZoom = useScenePanZoom();
142
- const selection = useSceneSelection();
143
- const setSceneSelection = selection.setSelection;
144
- const { hit } = useSceneHitTest();
145
- const interaction = useRef<SceneInteractionState>({}).current;
146
-
147
- // Surface selection in the originating interaction rather than a passive
148
- // effect. Detached editor roots can then update their toolbar without
149
- // forcing a flush from inside another component's lifecycle.
150
- const setSelection = useCallback(
151
- (ids: Iterable<string>) => {
152
- const next = new Set(ids);
153
- setSceneSelection(next);
154
- onSelectionChange?.(next);
155
- },
156
- [setSceneSelection, onSelectionChange],
157
- );
158
-
159
- // ── Hit-test cache ──────────────────────────────────────────
160
- const hitItems: HitTestable[] = useMemo(() => {
161
- const out: HitTestable[] = [];
162
- for (const layer of layers) {
163
- const bounds = layerBounds(layer, viewport);
164
- if (!bounds) continue;
165
- out.push({ id: layer.id, layer, bounds });
166
- }
167
- return out;
168
- }, [layers, viewport]);
169
-
170
- // ── Tool context (rebuilt every render — cheap, no allocations in hot path) ──
171
- const ctx: SceneToolContext = useMemo(
172
- () => ({
173
- interaction,
174
- viewport,
175
- transform: panZoom.transform,
176
- layers,
177
- edges,
178
- selection: selection.selection,
179
- hitItems,
180
- screenToViewport: panZoom.screenToViewport,
181
- viewportToScreen: panZoom.viewportToScreen,
182
- hit: (p) => hit(p, hitItems),
183
- setSelection,
184
- dispatch: onCommand,
185
- setActiveTool,
186
- beginTextEdit: textEdit.begin,
187
- }),
188
- [
189
- viewport,
190
- panZoom.transform,
191
- panZoom.screenToViewport,
192
- panZoom.viewportToScreen,
193
- layers,
194
- edges,
195
- selection.selection,
196
- setSelection,
197
- hitItems,
198
- hit,
199
- onCommand,
200
- setActiveTool,
201
- textEdit.begin,
202
- interaction,
203
- ],
204
- );
205
- // Mirror ctx in a ref so root keyboard handlers read the latest selection
206
- // without re-binding on every selection change.
207
- const ctxRef = useRef(ctx);
208
- ctxRef.current = ctx;
209
-
210
- // ── Pointer dispatch ────────────────────────────────────────
211
- const containerRef = useRef<SVGSVGElement>(null);
212
- const isPanning = useRef(false);
213
- const panLast = useRef<{ x: number; y: number } | null>(null);
214
-
215
- const handleWheel = useCallback(
216
- (e: React.WheelEvent<SVGSVGElement>) => {
217
- // A plain mouse wheel must scroll the PAGE — the canvas is embedded in a
218
- // scrolling document, so hijacking the wheel to zoom (which makes shapes
219
- // appear to grow/shrink under the cursor) is surprising. Only zoom on an
220
- // explicit zoom gesture: Ctrl/Cmd + wheel, which is also what a trackpad
221
- // pinch emits. Otherwise let the event bubble so the page scrolls.
222
- if (!e.ctrlKey && !e.metaKey) return;
223
- e.preventDefault();
224
- const rect = (e.currentTarget as SVGSVGElement).getBoundingClientRect();
225
- const sx = e.clientX - rect.left;
226
- const sy = e.clientY - rect.top;
227
- // Negative deltaY = wheel up = zoom in.
228
- const factor = Math.exp(-e.deltaY * 0.0015);
229
- panZoom.zoomAt(factor, sx, sy);
230
- },
231
- [panZoom],
232
- );
233
-
234
- // Gesture state belongs to this Scene instance and lives outside React's
235
- // render cycle. Bump `dragTick` while a pointer gesture is active so its
236
- // live drag/resize preview is visible.
237
- const [, setDragTick] = useState(0);
238
- const pointerActive = useRef(false);
239
- const bumpTick = useCallback(() => setDragTick((t) => t + 1), []);
240
-
241
- const handlePointerDown = useCallback(
242
- (e: React.PointerEvent<SVGSVGElement>) => {
243
- // Focus the SVG so it can receive keyboard events (Delete, Escape,
244
- // tool shortcuts). preventDefault on focus loss is what makes the
245
- // ProseMirror parent NOT steal focus back mid-gesture.
246
- (e.currentTarget as SVGSVGElement).focus({ preventScroll: true });
247
- // Middle-button or space-modified drag → pan, regardless of tool.
248
- if (e.button === 1 || (e.button === 0 && e.altKey)) {
249
- isPanning.current = true;
250
- panLast.current = { x: e.clientX, y: e.clientY };
251
- (e.currentTarget as Element).setPointerCapture?.(e.pointerId);
252
- e.preventDefault();
253
- return;
254
- }
255
- pointerActive.current = true;
256
- activeTool?.onPointerDown?.(e, ctxRef.current);
257
- bumpTick();
258
- },
259
- [activeTool, bumpTick],
260
- );
261
-
262
- const handlePointerMove = useCallback(
263
- (e: React.PointerEvent<SVGSVGElement>) => {
264
- if (isPanning.current && panLast.current) {
265
- const dx = e.clientX - panLast.current.x;
266
- const dy = e.clientY - panLast.current.y;
267
- panLast.current = { x: e.clientX, y: e.clientY };
268
- panZoom.panBy(dx, dy);
269
- return;
270
- }
271
- activeTool?.onPointerMove?.(e, ctxRef.current);
272
- if (pointerActive.current) bumpTick();
273
- },
274
- [activeTool, panZoom, bumpTick],
275
- );
276
-
277
- const handlePointerUp = useCallback(
278
- (e: React.PointerEvent<SVGSVGElement>) => {
279
- pointerActive.current = false;
280
- if (isPanning.current) {
281
- isPanning.current = false;
282
- panLast.current = null;
283
- (e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
284
- return;
285
- }
286
- activeTool?.onPointerUp?.(e, ctxRef.current);
287
- },
288
- [activeTool],
289
- );
290
-
291
- const handlePointerCancel = useCallback(
292
- (e: React.PointerEvent<SVGSVGElement>) => {
293
- pointerActive.current = false;
294
- isPanning.current = false;
295
- panLast.current = null;
296
- interaction.selectDrag = undefined;
297
- interaction.connect = undefined;
298
- (e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
299
- bumpTick();
300
- },
301
- [interaction, bumpTick],
302
- );
303
-
304
- useEffect(
305
- () => () => {
306
- interaction.selectDrag = undefined;
307
- interaction.connect = undefined;
308
- },
309
- [interaction],
310
- );
311
-
312
- const handleDoubleClick = useCallback(
313
- (e: React.MouseEvent<SVGSVGElement>) => {
314
- activeTool?.onDoubleClick?.(e, ctxRef.current);
315
- },
316
- [activeTool],
317
- );
318
-
319
- // ── Drag-and-drop (palette → canvas) ────────────────────────
320
- // HTML5 DnD is independent of the pointer-event tool pipeline; the
321
- // browser suppresses pointer events during a drag, so there's no
322
- // conflict with the active tool. We only enable it when a host wires
323
- // `onDrop`, and we translate the drop point into viewport coordinates
324
- // (through the live pan/zoom transform) before handing it back.
325
- const handleDragOver = useCallback(
326
- (e: React.DragEvent<SVGSVGElement>) => {
327
- if (!onDrop) return;
328
- e.preventDefault();
329
- e.dataTransfer.dropEffect = 'copy';
330
- },
331
- [onDrop],
332
- );
333
-
334
- const handleDrop = useCallback(
335
- (e: React.DragEvent<SVGSVGElement>) => {
336
- if (!onDrop) return;
337
- e.preventDefault();
338
- const rect = (e.currentTarget as SVGSVGElement).getBoundingClientRect();
339
- const sx = e.clientX - rect.left;
340
- const sy = e.clientY - rect.top;
341
- onDrop(e, panZoom.screenToViewport(sx, sy));
342
- },
343
- [onDrop, panZoom],
344
- );
345
-
346
- // ── Keyboard handling ──────────────────────────────────────
347
- // We make the SVG focusable (tabIndex=-1, set in SceneViewport) and
348
- // focus it on pointer-down. Keydown listeners on the SVG only fire
349
- // when it has focus — that keeps us from hijacking Delete in
350
- // surrounding form fields or the parent ProseMirror editor.
351
- useEffect(() => {
352
- const root = containerRef.current;
353
- if (!root) return;
354
- const onKey = (e: KeyboardEvent) => {
355
- // While a text overlay is open it owns the keyboard — don't let tool
356
- // shortcuts / Delete fire against the canvas.
357
- if (textEdit.activeRef.current) return;
358
- // Tool shortcuts (single-letter, no modifier).
359
- if (!e.ctrlKey && !e.metaKey && !e.altKey && e.key.length === 1) {
360
- const shortcut = e.key.toLowerCase();
361
- const match = tools.find((t) => t.shortcut === shortcut);
362
- if (match) {
363
- setActiveTool(match.id);
364
- e.preventDefault();
365
- return;
366
- }
367
- }
368
- // Tool keydown (Delete, Escape, etc.).
369
- activeTool?.onKeyDown?.(e, ctxRef.current);
370
- };
371
- root.addEventListener('keydown', onKey);
372
- return () => root.removeEventListener('keydown', onKey);
373
- }, [tools, activeTool, setActiveTool, textEdit.activeRef]);
374
-
375
- // ── Initial fit ─────────────────────────────────────────────
376
- const didFitRef = useRef(false);
377
- const [containerSize, setContainerSize] = useState<{ width: number; height: number } | null>(
378
- null,
379
- );
380
-
381
- useLayoutEffect(() => {
382
- if (!containerRef.current) return;
383
- const el = containerRef.current;
384
- const measure = () => {
385
- const rect = el.getBoundingClientRect();
386
- setContainerSize({ width: rect.width, height: rect.height });
387
- };
388
- measure();
389
- const ro = new ResizeObserver(measure);
390
- ro.observe(el);
391
- return () => ro.disconnect();
392
- }, []);
393
-
394
- useEffect(() => {
395
- if (didFitRef.current) return;
396
- if (!containerSize) return;
397
- if (hitItems.length === 0) return;
398
- didFitRef.current = true;
399
- // Fit to the bounding box of all hit items (the visible content).
400
- let minX = Infinity;
401
- let minY = Infinity;
402
- let maxX = -Infinity;
403
- let maxY = -Infinity;
404
- for (const it of hitItems) {
405
- if (it.bounds.x < minX) minX = it.bounds.x;
406
- if (it.bounds.y < minY) minY = it.bounds.y;
407
- if (it.bounds.x + it.bounds.width > maxX) maxX = it.bounds.x + it.bounds.width;
408
- if (it.bounds.y + it.bounds.height > maxY) maxY = it.bounds.y + it.bounds.height;
409
- }
410
- if (!Number.isFinite(minX)) return;
411
- panZoom.fitBox(
412
- { x: minX, y: minY, width: maxX - minX, height: maxY - minY },
413
- containerSize,
414
- 40,
415
- );
416
- }, [containerSize, hitItems, panZoom]);
417
-
418
- // ── Render ──────────────────────────────────────────────────
419
- const liveOffset = activeId === 'select' ? getActiveMoveOffset(interaction) : null;
420
- const liveResize = activeId === 'select' ? getActiveResize(interaction) : null;
421
- const showSelectionHandles = activeTool?.hideSelectionHandles !== true;
422
-
423
- // Per-layer transform applied during an in-flight drag or resize so
424
- // the user sees the layer move/stretch in real time, not just on commit.
425
- // A layer is "active" for transform purposes if it's directly selected,
426
- // OR if it follows a directly-selected layer via `layerFollows`.
427
- const wrapperFor = (layer: Layer): { transform?: string } => {
428
- const followsId = layerFollows?.(layer.id) ?? null;
429
- const ownerId = selection.selection.has(layer.id)
430
- ? layer.id
431
- : followsId && selection.selection.has(followsId)
432
- ? followsId
433
- : null;
434
- if (!ownerId) return {};
435
- if (liveResize && liveResize.layerId === ownerId) {
436
- const bounds = hitItems.find((it) => it.id === ownerId)?.bounds;
437
- if (!bounds) return {};
438
- const sx = bounds.width > 0 ? liveResize.bounds.width / bounds.width : 1;
439
- const sy = bounds.height > 0 ? liveResize.bounds.height / bounds.height : 1;
440
- const tx = liveResize.bounds.x - bounds.x * sx;
441
- const ty = liveResize.bounds.y - bounds.y * sy;
442
- return { transform: `matrix(${sx} 0 0 ${sy} ${tx} ${ty})` };
443
- }
444
- if (liveOffset) return { transform: `translate(${liveOffset.dx} ${liveOffset.dy})` };
445
- return {};
446
- };
447
-
448
- // A text box's font size is independent of its box size — a wide box exists
449
- // precisely so a small caption can left/center/right-align within it. So
450
- // when a text box is directly resized we reshape the box and re-align its
451
- // text at the *unchanged* font, rather than scaling the glyphs with the box
452
- // (the generic matrix path in `wrapperFor`, which would stretch the text and
453
- // then snap it back to its real size on commit). Returns a clone positioned
454
- // at the live-resize box, or null when `layer` isn't a text box being
455
- // directly resized. Follower labels (drawing/diagram, whose font is derived
456
- // from box height) are resized via their shape, not directly, so they never
457
- // match here and keep the scale-with-box preview.
458
- const liveResizedTextBox = (layer: Layer): Layer | null => {
459
- if (!liveResize || liveResize.layerId !== layer.id || layer.type !== 'text') return null;
460
- const b = liveResize.bounds;
461
- return {
462
- ...layer,
463
- position: {
464
- ...layer.position,
465
- x: b.x,
466
- y: b.y,
467
- width: b.width,
468
- height: b.height,
469
- anchor: 'top-left',
470
- },
471
- };
472
- };
473
-
474
- const handleHandlePointerDown = (e: React.PointerEvent<SVGElement>, corner: ResizeCorner) => {
475
- const id = selection.selection.values().next().value as string | undefined;
476
- if (!id) return;
477
- const bounds = hitItems.find((it) => it.id === id)?.bounds;
478
- if (!bounds) return;
479
- const root = containerRef.current;
480
- if (!root) return;
481
- const rect = root.getBoundingClientRect();
482
- const sx = e.clientX - rect.left;
483
- const sy = e.clientY - rect.top;
484
- const startV = panZoom.screenToViewport(sx, sy);
485
- beginHandleDrag({ layerId: id, corner, startV, startBounds: bounds }, interaction);
486
- (e.currentTarget as Element).setPointerCapture?.(e.pointerId);
487
- // Mark a gesture as active so the Scene's pointer-move handler
488
- // triggers re-renders for the live resize preview.
489
- pointerActive.current = true;
490
- bumpTick();
491
- e.stopPropagation();
492
- };
493
-
494
- return (
495
- <div className="squisq-scene-root">
496
- {showToolbar && <SceneToolbar tools={tools} activeId={activeId} onSelect={setActiveTool} />}
497
- <SceneViewport
498
- ref={containerRef}
499
- width={containerSize?.width ?? viewport.width}
500
- height={containerSize?.height ?? viewport.height}
501
- transform={panZoom.transform}
502
- cursor={activeTool?.cursor}
503
- onWheel={handleWheel}
504
- onPointerDown={handlePointerDown}
505
- onPointerMove={handlePointerMove}
506
- onPointerUp={handlePointerUp}
507
- onPointerCancel={handlePointerCancel}
508
- onDoubleClick={handleDoubleClick}
509
- onDragOver={onDrop ? handleDragOver : undefined}
510
- onDrop={onDrop ? handleDrop : undefined}
511
- >
512
- {/* Extras (e.g. diagram edges) render behind the layers so the
513
- cards visually clip edge endpoints. */}
514
- {renderExtras && <g className="squisq-scene-extras">{renderExtras(ctx)}</g>}
515
- {/* Layers — the Scene wraps each in a transformable <g> so a
516
- selected layer can show a live drag/resize preview without
517
- requiring the host to know about drag state. */}
518
- {layers.map((layer) => {
519
- const layerClassName = `squisq-scene-layer squisq-scene-layer--${layer.type}`;
520
- // Text boxes reshape (font-fixed) rather than scale during resize;
521
- // everything else stretches via the group transform.
522
- const resizedText = liveResizedTextBox(layer);
523
- if (resizedText) {
524
- return (
525
- <g key={layer.id} className={layerClassName} data-layer-id={layer.id}>
526
- {layerRenderer(resizedText, viewport)}
527
- </g>
528
- );
529
- }
530
- return (
531
- <g
532
- key={layer.id}
533
- className={layerClassName}
534
- data-layer-id={layer.id}
535
- {...wrapperFor(layer)}
536
- >
537
- {layerRenderer(layer, viewport)}
538
- </g>
539
- );
540
- })}
541
- <SceneSelection
542
- selection={selection.selection}
543
- hitItems={hitItems}
544
- liveOffset={liveOffset}
545
- liveResize={liveResize}
546
- onHandlePointerDown={showSelectionHandles ? handleHandlePointerDown : undefined}
547
- showHandles={showSelectionHandles}
548
- />
549
- {activeTool?.renderOverlay?.(ctx)}
550
- </SceneViewport>
551
- {showMaximize && onToggleMaximize && (
552
- <button
553
- type="button"
554
- className="squisq-scene-maximize-btn"
555
- onClick={onToggleMaximize}
556
- title={maximized ? 'Exit fullscreen (Esc)' : 'Maximize'}
557
- >
558
- {maximized ? '✕' : '⛶'}
559
- </button>
560
- )}
561
- {/* Inline text editor overlay (HTML over the SVG). Renders only once
562
- the edited layer exists in `layers` (tolerates new-text timing). */}
563
- {textEditing &&
564
- textEdit.editingId &&
565
- (() => {
566
- const layer = layers.find((l) => l.id === textEdit.editingId);
567
- if (!layer) return null;
568
- return (
569
- <SceneTextOverlay
570
- config={textEditing}
571
- layer={layer}
572
- editableId={textEdit.editingId}
573
- viewport={viewport}
574
- transform={panZoom.transform}
575
- viewportToScreen={panZoom.viewportToScreen}
576
- onClose={textEdit.close}
577
- />
578
- );
579
- })()}
580
- </div>
581
- );
582
- }
583
-
584
- function defaultRenderLayer(layer: Layer, viewport: { width: number; height: number }): ReactNode {
585
- return <RenderLayer layer={layer} viewport={viewport} />;
586
- }
587
-
588
- // ── Toolbar ────────────────────────────────────────────────────
589
-
590
- function SceneToolbar({
591
- tools,
592
- activeId,
593
- onSelect,
594
- }: {
595
- tools: readonly SceneTool[];
596
- activeId: string;
597
- onSelect: (id: string) => void;
598
- }) {
599
- if (tools.length <= 1) return null;
600
- return (
601
- <div className="squisq-scene-toolbar" role="toolbar" aria-label="Scene tools">
602
- {tools.map((t) => (
603
- <button
604
- key={t.id}
605
- type="button"
606
- className={`squisq-scene-tool${activeId === t.id ? ' squisq-scene-tool--active' : ''}`}
607
- onClick={() => onSelect(t.id)}
608
- title={t.shortcut ? `${t.label} (${t.shortcut.toUpperCase()})` : t.label}
609
- aria-pressed={activeId === t.id}
610
- >
611
- {t.icon ?? <span className="squisq-scene-tool-label">{t.label}</span>}
612
- </button>
613
- ))}
614
- </div>
615
- );
616
- }