@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,1247 +0,0 @@
1
- /**
2
- * EditorContext
3
- *
4
- * Shared React context that synchronizes state across all three editor views
5
- * (Raw/Monaco, WYSIWYG/Tiptap, Preview/DocPlayer). When any view modifies the
6
- * markdown source, the context re-parses and regenerates the MarkdownDocument
7
- * and Doc so all views stay in sync.
8
- */
9
-
10
- import {
11
- createContext,
12
- useContext,
13
- useState,
14
- useCallback,
15
- useMemo,
16
- useRef,
17
- useEffect,
18
- type ReactNode,
19
- } from 'react';
20
- import type { Doc, MediaProvider } from '@bendyline/squisq/schemas';
21
- import type { MarkdownDocument } from '@bendyline/squisq/markdown';
22
- import { parseMarkdown, stringifyMarkdown } from '@bendyline/squisq/markdown';
23
- import { markdownToDoc } from '@bendyline/squisq/doc';
24
- import type { ContentContainer } from '@bendyline/squisq/storage';
25
- import {
26
- DocumentVersionManager,
27
- type PrunePolicy,
28
- type SaveVersionOptions,
29
- type SaveVersionResult,
30
- } from '@bendyline/squisq/versions';
31
- import type { Editor as TiptapEditor } from '@tiptap/core';
32
- import type { editor as MonacoEditorNs } from 'monaco-editor';
33
- import { markdownToTiptap } from './tiptapBridge';
34
- import { resolveFileKind } from './fileKind';
35
- import { useBlockNavigator } from './useBlockNavigator';
36
- import {
37
- createSceneTextChannel,
38
- type SceneTextChannel,
39
- type SceneTextHandle,
40
- } from './scene/text/sceneTextChannel';
41
- import {
42
- readMonacoScrollRatio,
43
- readWysiwygScrollRatio,
44
- restoreMonacoScrollRatio,
45
- restoreWysiwygScrollRatio,
46
- } from './editorScrollSync';
47
-
48
- /** Monaco standalone code editor instance type */
49
- type MonacoEditor = MonacoEditorNs.IStandaloneCodeEditor;
50
-
51
- /**
52
- * One candidate returned by a {@link MentionProvider}. Shown in the editor's
53
- * `@` popover. `id` is the stable identifier (serialized into the mention
54
- * wire format); `label` is what the reader sees; `scheme` is the namespace
55
- * (e.g. `'user'`, `'issue'`) written into the markdown as `@[label](scheme:id)`;
56
- * `description` and `group` are optional hints for richer suggestion UIs.
57
- *
58
- * Different candidates in the same result set may carry different schemes —
59
- * a provider that returns both users and issues, for example, tags each
60
- * candidate with its own namespace and the editor emits mentions accordingly.
61
- */
62
- export interface MentionCandidate {
63
- id: string;
64
- label: string;
65
- scheme: string;
66
- description?: string;
67
- group?: string;
68
- }
69
-
70
- /**
71
- * Looks up mention candidates matching a query. Called as the user types
72
- * after `@`. The provider is free to do server-side or client-side filtering;
73
- * the editor only cares that candidates come back in relevance order.
74
- */
75
- export type MentionProvider = (query: string) => Promise<MentionCandidate[]>;
76
-
77
- /**
78
- * A document that the link dialog's "Browse documents" picker can offer.
79
- * `path` is what lands in the markdown URL (typically relative to the
80
- * current document so `home.md → resume.md` round-trips through file-
81
- * system serializers). `label` is the human name shown in the list.
82
- * `description` is an optional secondary line (e.g. workspace folder,
83
- * last-modified date).
84
- */
85
- export interface DocumentLinkCandidate {
86
- path: string;
87
- label: string;
88
- description?: string;
89
- }
90
-
91
- /**
92
- * Resolves sibling / workspace document candidates for the link dialog.
93
- * The editor itself has no notion of "neighbors" — hosts that organize
94
- * docs in a workspace (e.g. docblocks) implement this to power the
95
- * dialog's document picker. Pass `''` as the query for an initial list
96
- * (the dialog calls it once on open); subsequent calls narrow by user
97
- * input.
98
- */
99
- export type DocumentLinkProvider = (query: string) => Promise<DocumentLinkCandidate[]>;
100
-
101
- // ─── Types ───────────────────────────────────────────────
102
-
103
- export type EditorView = 'raw' | 'wysiwyg' | 'preview';
104
- /**
105
- * Light/dark chrome mode for the editor shell (toolbar, tabs, status bar,
106
- * side panes). This is the editor's *UI color scheme* — distinct from a
107
- * Squisq `Theme` object, which styles the rendered document. Renamed from
108
- * the former `EditorTheme` to remove that ambiguity.
109
- */
110
- export type EditorColorScheme = 'light' | 'dark';
111
- /**
112
- * Document layout mode. `'document'` shows the whole markdown document in
113
- * the active view (the historical behavior). `'block'` is the
114
- * block-at-a-time view — one heading-defined block on a card at a time,
115
- * with the editor scoped to just that block. `'timeline'` is block mode plus
116
- * a horizontal timeline track for editing block durations and media slices.
117
- * See {@link useBlockNavigator}.
118
- */
119
- export type LayoutMode = 'document' | 'block' | 'timeline';
120
- /**
121
- * How much of the active Squisq theme the WYSIWYG editing surface
122
- * mirrors. `'fonts'` is the historical default — body and heading
123
- * fonts only. `'fonts-colors'` also borrows the theme canvas / text
124
- * colors. `'none'` opts out completely.
125
- */
126
- export type ThemeInheritance = 'none' | 'fonts' | 'fonts-colors';
127
- /**
128
- * When inline block-template tags are shown in the WYSIWYG surface.
129
- * `'active'` shows tags for the cursor's block and the block under the pointer.
130
- */
131
- export type BlockTagVisibility = 'none' | 'active' | 'always';
132
- /**
133
- * Editor operating mode. `markdown` is the full experience (WYSIWYG +
134
- * Preview tabs, formatting toolbar). `code` is a Monaco-only view used
135
- * when the content represents a non-markdown file like `foo.ts`.
136
- */
137
- export type EditorMode = 'markdown' | 'code' | 'image';
138
-
139
- export interface EditorState {
140
- /** Raw markdown source string */
141
- markdownSource: string;
142
- /** Parsed markdown document (JSON DOM) */
143
- markdownDoc: MarkdownDocument | null;
144
- /** Generated Doc (block hierarchy) */
145
- doc: Doc | null;
146
- /** Currently active editor view */
147
- activeView: EditorView;
148
- /** Parse error, if any */
149
- parseError: string | null;
150
- /** Whether a parse is pending */
151
- isParsing: boolean;
152
- /** Current light/dark chrome color scheme for the editor shell. */
153
- colorScheme: EditorColorScheme;
154
- /** Operating mode — 'markdown' for the full shell, 'code' for Monaco-only. */
155
- editorMode: EditorMode;
156
- /** Monaco language ID for the Raw editor. */
157
- language: string;
158
- /**
159
- * Whether the inline preview gutter (per-block card previews next to the
160
- * WYSIWYG surface) is currently visible. Initialized from the EditorShell
161
- * `inlinePreview` prop; the View menu in the toolbar can toggle it at
162
- * runtime.
163
- */
164
- inlinePreviewVisible: boolean;
165
- /**
166
- * Whether the bottom status bar is currently visible. Initialized from
167
- * the EditorShell `showStatusBar` prop (default true); the View menu in
168
- * the toolbar can toggle it at runtime.
169
- */
170
- statusBarVisible: boolean;
171
- /**
172
- * Whether the left-side outline pane is currently visible. Initialized
173
- * from the EditorShell `outline` prop (default false); the View menu in
174
- * the toolbar can toggle it at runtime.
175
- */
176
- outlineVisible: boolean;
177
- /** When inline block-template tags are shown in the WYSIWYG view. */
178
- blockTagVisibility: BlockTagVisibility;
179
- /**
180
- * Whether inline block-template tags can currently be visible.
181
- * Kept for compatibility; prefer {@link blockTagVisibility}.
182
- */
183
- blockTagsVisible: boolean;
184
- /**
185
- * How much of the active Squisq theme the WYSIWYG editing surface should
186
- * inherit. `'none'` shows the default editor styling, `'fonts'` (the
187
- * default) matches body and heading fonts only, and `'fonts-colors'`
188
- * also borrows the theme's canvas / text colors so authors get a
189
- * closer preview while editing.
190
- */
191
- themeInheritance: ThemeInheritance;
192
- /**
193
- * Relative path of an image the user requested to edit, or `null` when
194
- * no editor is open. Surfaced by `<ImageNodeView>`'s hover affordance
195
- * and consumed by `<EditorShell>` to render the modal `<ImageEditor>`.
196
- */
197
- imageEditTarget: string | null;
198
- /**
199
- * Monotonic counter bumped whenever a managed media asset is rewritten
200
- * (e.g. after the image-editor modal saves back). Image render paths
201
- * that cache resolved blob URLs should include this in their effect
202
- * deps so the new bytes get picked up.
203
- */
204
- mediaRevision: number;
205
- /**
206
- * Whether the in-editor media recorder should be available. Defaults
207
- * to true when a `mediaProvider` is wired; hosts that explicitly
208
- * don't want the affordance (e.g. read-only embeds, surfaces where
209
- * camera/screen prompts would be jarring) can pass `false` on the
210
- * shell.
211
- */
212
- allowRecording: boolean;
213
- /**
214
- * Whether the Narrate (teleprompter) display mode is offered under the
215
- * Use tab. Orthogonal to `allowRecording`: the teleprompter is useful
216
- * without any capture (reading for external recording software), and a
217
- * host may allow the recorder modal but not want a prompter surface.
218
- */
219
- allowNarrate: boolean;
220
- /**
221
- * Document layout mode. `'document'` (default) edits the whole document;
222
- * `'block'` activates the block-at-a-time card view. Initialized from the
223
- * EditorShell `layoutMode` prop; the View menu can toggle it at runtime.
224
- */
225
- layoutMode: LayoutMode;
226
- /**
227
- * The markdown the active text editor should bind to: the full source in
228
- * `'document'` mode, or just the active block's slice in `'block'` mode.
229
- * Editors read this instead of `markdownSource` so the same surfaces work
230
- * in both layouts.
231
- */
232
- editorSource: string;
233
- /** Number of navigable blocks (cards) in the current document. */
234
- blockCount: number;
235
- /** Index of the block currently shown on the card (block mode). */
236
- activeBlockKey: number;
237
- /** 1-based source line where the active block begins, or null. */
238
- activeBlockStartLine: number | null;
239
- }
240
-
241
- export interface EditorActions {
242
- /** Set markdown source and trigger re-parse */
243
- setMarkdownSource: (source: string) => void;
244
- /**
245
- * Write through the active editor channel. In `'document'` mode this is
246
- * `setMarkdownSource`; in `'block'` mode it splices the edited block back
247
- * into the full document. Editors call this instead of `setMarkdownSource`.
248
- */
249
- setEditorSource: (source: string) => void;
250
- /** Switch between Document and Block-at-a-time layouts. */
251
- setLayoutMode: (mode: LayoutMode) => void;
252
- /** Show a block by index in block mode (clamped to range). */
253
- goToBlock: (key: number) => void;
254
- /** Show the block that owns a given 1-based source line (used by the outline). */
255
- goToBlockByLine: (line: number) => void;
256
- /** Move the card to the previous block. */
257
- prevBlock: () => void;
258
- /** Move the card to the next block. */
259
- nextBlock: () => void;
260
- /** Insert a new heading block after the active one and move to it. */
261
- addBlock: () => void;
262
- /** Set markdown from a MarkdownDocument (e.g. from WYSIWYG) */
263
- setMarkdownDoc: (doc: MarkdownDocument) => void;
264
- /** Switch the active view */
265
- setActiveView: (view: EditorView) => void;
266
- /** Register / unregister the Tiptap editor instance (called by WysiwygEditor) */
267
- setTiptapEditor: (editor: TiptapEditor | null) => void;
268
- /** Register / unregister the Monaco editor instance (called by RawEditor) */
269
- setMonacoEditor: (editor: MonacoEditor | null) => void;
270
- /** Set the light/dark chrome color scheme for the editor shell. */
271
- setColorScheme: (colorScheme: EditorColorScheme) => void;
272
- /** Show or hide the inline preview gutter at runtime (driven by the View menu). */
273
- setInlinePreviewVisible: (visible: boolean) => void;
274
- /** Show or hide the bottom status bar at runtime (driven by the View menu). */
275
- setStatusBarVisible: (visible: boolean) => void;
276
- /** Show or hide the left-side outline pane at runtime (driven by the View menu). */
277
- setOutlineVisible: (visible: boolean) => void;
278
- /** Show or hide inline block-template tags at runtime (driven by the View menu). */
279
- setBlockTagsVisible: (visible: boolean) => void;
280
- /** Choose when inline block-template tags are shown. */
281
- setBlockTagVisibility: (visibility: BlockTagVisibility) => void;
282
- /** Change how much of the active Squisq theme the WYSIWYG surface mirrors. */
283
- setThemeInheritance: (mode: ThemeInheritance) => void;
284
- /** Insert text at the current cursor position in the active editor */
285
- insertAtCursor: (text: string) => void;
286
- /** Replace all editor content with the given text */
287
- replaceAll: (text: string) => void;
288
- /**
289
- * Request the modal image editor open on the given relative media path.
290
- * The path must resolve through the active `mediaProvider`. No-op when
291
- * no provider is wired — callers should hide the affordance in that
292
- * case.
293
- */
294
- openImageEdit: (relativePath: string) => void;
295
- /** Close the image editor modal without saving. */
296
- closeImageEdit: () => void;
297
- /**
298
- * Bump `mediaRevision`. Called after the image editor writes back to
299
- * the original media path so dependent `<img>` nodes re-resolve their
300
- * blob URL.
301
- */
302
- bumpMediaRevision: () => void;
303
- }
304
-
305
- export interface EditorContextValue extends EditorState, EditorActions {
306
- /** The live Tiptap editor instance (null when WYSIWYG is not mounted) */
307
- tiptapEditor: TiptapEditor | null;
308
- /** The live Monaco editor instance (null when Raw is not mounted) */
309
- monacoEditor: MonacoEditor | null;
310
- /**
311
- * The focused canvas textbox editor, if any — a small Tiptap instance for
312
- * a diagram/drawing/layout textbox being edited inline. Published via
313
- * `sceneTextChannel` (the canvas renders in a detached React root). The
314
- * top formatting toolbar retargets to this when set. `level` gates which
315
- * buttons apply (`inline` = marks only; `rich` = headings/lists too).
316
- */
317
- activeSceneText: SceneTextHandle | null;
318
- /** Instance-owned bridge used by detached scene widget roots. */
319
- sceneTextChannel: SceneTextChannel;
320
- /**
321
- * Workspace-scoped `ContentContainer` for this document — the folder
322
- * holding the doc, its `_files/` sidecar, sibling documents, and any
323
- * version snapshots. Drives audio mapping, version history, and
324
- * sibling-doc reads for the recursive HTML export.
325
- */
326
- workspaceContainer: ContentContainer | null;
327
- /**
328
- * Version manager — non-null only when the host opted into versioning
329
- * (`allowVersioning` + a `workspaceContainer`). Components can call
330
- * `saveVersion` directly, or render the version-history panel which
331
- * reads it from here.
332
- */
333
- versioning: DocumentVersionManager | null;
334
- /**
335
- * Stamp a new snapshot of the current document. No-op (returns
336
- * `unchanged`) when content matches the latest version. Always safe
337
- * to call — when versioning is disabled, returns `no-document`
338
- * without writing.
339
- */
340
- saveVersion: (options?: SaveVersionOptions) => Promise<SaveVersionResult>;
341
- /** MediaProvider for resolving image URLs in the WYSIWYG editor */
342
- mediaProvider: MediaProvider | null;
343
- /**
344
- * How pasted/inserted images should be displayed in the WYSIWYG view.
345
- * `'inline'` (default) lets them flow at natural size up to the editor
346
- * width; `'thumbnail'` constrains them to a 100×100 box so chat
347
- * composers and other dense surfaces don't get dominated by a single
348
- * pasted screenshot. The stored image bytes are unchanged — this is a
349
- * pure render-time decision.
350
- */
351
- imageDisplayMode: ImageDisplayMode;
352
- /**
353
- * Optional provider for `@`-mention suggestions. When set, both the
354
- * WYSIWYG (Tiptap) and Raw (Monaco) editors show a mention popover as
355
- * the user types `@<query>`. When unset, `@` is just a literal character.
356
- */
357
- mentionProvider: MentionProvider | null;
358
- /**
359
- * Optional provider for sibling-document suggestions in the link
360
- * dialog. When set, the dialog shows a "Browse documents" picker that
361
- * lets authors search neighbor docs by name and insert a relative-
362
- * path link. When unset, the dialog falls back to URL-only.
363
- */
364
- documentLinkProvider: DocumentLinkProvider | null;
365
- }
366
-
367
- export type ImageDisplayMode = 'inline' | 'thumbnail';
368
-
369
- // ─── Context ─────────────────────────────────────────────
370
-
371
- const EditorContext = createContext<EditorContextValue | null>(null);
372
-
373
- /**
374
- * Hook to access the editor context. Must be used within an EditorProvider.
375
- */
376
- // eslint-disable-next-line react-refresh/only-export-components
377
- export function useEditorContext(): EditorContextValue {
378
- const ctx = useContext(EditorContext);
379
- if (!ctx) {
380
- throw new Error('useEditorContext must be used within an <EditorProvider>');
381
- }
382
- return ctx;
383
- }
384
-
385
- // ─── Provider ────────────────────────────────────────────
386
-
387
- export interface EditorProviderProps {
388
- /** Initial markdown content */
389
- initialMarkdown?: string;
390
- /** Initial active view */
391
- initialView?: EditorView;
392
- /** Article ID used when generating the Doc */
393
- articleId?: string;
394
- /** Light/dark chrome color scheme for the editor shell. */
395
- colorScheme?: EditorColorScheme;
396
- /**
397
- * Workspace-scoped `ContentContainer` for this document — the folder
398
- * holding the doc, its `_files/` sidecar, sibling documents, and any
399
- * version snapshots. Required for `allowVersioning` to take effect.
400
- */
401
- workspaceContainer?: ContentContainer | null;
402
- /**
403
- * Enable version history. Snapshots are stored at
404
- * `.versions/<basename>.<timestamp>.md` inside `workspaceContainer`.
405
- * Auto-save fires after `versioningAutoSaveIdleMs` of idle; hosts can
406
- * also call `saveVersion()` from the context. Without a
407
- * `workspaceContainer`, this prop is ignored (and a `console.warn` is
408
- * emitted).
409
- */
410
- allowVersioning?: boolean;
411
- /** Override the basename used in version filenames. Defaults to the
412
- * basename of the container's primary document path. */
413
- versionBasename?: string;
414
- /**
415
- * Prune policy applied after each successful auto-save. Defaults to
416
- * keeping the last 50 snapshots so the count doesn't grow unbounded.
417
- */
418
- versioningPrunePolicy?: PrunePolicy;
419
- /**
420
- * Idle delay (ms) before auto-saving a version. `0` disables auto-save
421
- * entirely (versions are saved only via host-driven `saveVersion`
422
- * calls). Default: 5000.
423
- */
424
- versioningAutoSaveIdleMs?: number;
425
- /**
426
- * Notified after each `saveVersion` attempt — both successful saves
427
- * (`reason: 'saved'`) and skips (`'unchanged'`, `'no-document'`,
428
- * `'empty'`). Useful for hosts that want a "Last saved" indicator.
429
- */
430
- onSaveVersion?: (result: SaveVersionResult) => void;
431
- /** MediaProvider for resolving image URLs */
432
- mediaProvider?: MediaProvider | null;
433
- /** Display mode for images in the WYSIWYG view. Defaults to `'inline'`. */
434
- imageDisplayMode?: ImageDisplayMode;
435
- /**
436
- * Async provider for `@`-mention suggestions. Omit to disable mentions
437
- * entirely — typing `@` becomes just a literal character again.
438
- */
439
- mentionProvider?: MentionProvider | null;
440
- /**
441
- * Async provider for sibling-document suggestions in the link dialog.
442
- * Omit to fall back to URL-only link insertion.
443
- */
444
- documentLinkProvider?: DocumentLinkProvider | null;
445
- /**
446
- * Whether the in-editor media recorder is available in the toolbar.
447
- * Defaults to true. Set to false to suppress the recorder affordance
448
- * even when a `mediaProvider` is wired (e.g. read-only embeds,
449
- * surfaces where camera/screen prompts would be jarring).
450
- */
451
- allowRecording?: boolean;
452
- /**
453
- * Whether the Narrate (teleprompter) display mode is offered under the
454
- * Use tab. Defaults to true. When false the mode button is hidden and a
455
- * frontmatter-forced `display-mode: narrate` clamps back to video.
456
- */
457
- allowNarrate?: boolean;
458
- /**
459
- * File name (e.g. `foo.ts`) or bare extension — used to pick a Monaco
460
- * language and decide between markdown vs. code mode.
461
- */
462
- fileName?: string;
463
- /** Explicit Monaco language ID — wins over the fileName-derived one. */
464
- language?: string;
465
- /**
466
- * Initial visibility of the inline preview gutter. Defaults to false.
467
- * The toolbar's View menu can toggle it at runtime.
468
- */
469
- inlinePreview?: boolean;
470
- /**
471
- * Initial visibility of the bottom status bar. Defaults to true.
472
- * The toolbar's View menu can toggle it at runtime.
473
- */
474
- showStatusBar?: boolean;
475
- /**
476
- * Initial visibility of the left-side outline pane. Defaults to false.
477
- * The toolbar's View menu can toggle it at runtime.
478
- */
479
- outline?: boolean;
480
- /**
481
- * Legacy initial visibility of inline block-template tags on headings.
482
- * `true` maps to always visible and `false` maps to hidden. When omitted,
483
- * {@link blockTagVisibility} defaults to `'active'`.
484
- */
485
- blockTags?: boolean;
486
- /**
487
- * Initial block-tag visibility mode. When set, this takes precedence over
488
- * the legacy boolean {@link blockTags} prop. Defaults to `'active'`.
489
- */
490
- blockTagVisibility?: BlockTagVisibility;
491
- /**
492
- * Initial value for how much of the active Squisq theme the WYSIWYG
493
- * editing surface should mirror. Defaults to `'fonts'` — the
494
- * historical behavior of inheriting body / heading fonts only. The
495
- * toolbar's View menu can change it at runtime.
496
- */
497
- themeInheritance?: ThemeInheritance;
498
- /**
499
- * Initial layout mode. Defaults to `'document'` (whole-document editing).
500
- * `'block'` boots into the block-at-a-time card view. The toolbar's View
501
- * menu can toggle it at runtime.
502
- */
503
- layoutMode?: LayoutMode;
504
- /**
505
- * Bundled view preferences — a serializable JSON blob covering all
506
- * runtime-toggleable view options. When provided, individual values
507
- * here override the matching individual props (`inlinePreview`,
508
- * `showStatusBar`, `outline`, `blockTagVisibility`, `blockTags`). Hosts
509
- * wiring this up typically load the blob from their own preferences storage
510
- * and pair it with {@link onViewPreferencesChange}.
511
- */
512
- viewPreferences?: ViewPreferences;
513
- /**
514
- * Notified after each user-driven toggle in the View menu (or any
515
- * programmatic call to the corresponding context setters). The
516
- * argument is a full snapshot — hosts can persist it as-is.
517
- * Not called when {@link viewPreferences} is changed externally.
518
- */
519
- onViewPreferencesChange?: (prefs: ViewPreferences) => void;
520
- children: ReactNode;
521
- }
522
-
523
- /**
524
- * Serializable bundle of all runtime-toggleable view preferences for
525
- * the editor shell. Hosts can persist this verbatim (e.g. to
526
- * localStorage) and pass it back via {@link EditorProviderProps.viewPreferences}
527
- * to restore the user's last view configuration.
528
- */
529
- export interface ViewPreferences {
530
- /** Whether the left-side outline pane is visible. */
531
- outline?: boolean;
532
- /** Whether the inline preview gutter (per-block cards) is visible. */
533
- inlinePreview?: boolean;
534
- /** Whether the bottom status bar is visible. */
535
- showStatusBar?: boolean;
536
- /** Whether inline block-template tags on headings are visible. */
537
- blockTags?: boolean;
538
- /** When inline block-template tags are shown. Takes precedence over `blockTags`. */
539
- blockTagVisibility?: BlockTagVisibility;
540
- /** How much of the active Squisq theme the WYSIWYG surface mirrors. */
541
- themeInheritance?: ThemeInheritance;
542
- /** Document vs. block-at-a-time layout. */
543
- layoutMode?: LayoutMode;
544
- }
545
-
546
- /**
547
- * Provides shared editor state to all child components.
548
- * Automatically parses markdown and generates a Doc whenever the source changes.
549
- */
550
- const DEFAULT_PRUNE_POLICY: PrunePolicy = { type: 'keep-last-n', n: 50 };
551
- const DEFAULT_AUTOSAVE_IDLE_MS = 5_000;
552
-
553
- function normalizeBlockTagVisibility(
554
- value: BlockTagVisibility | boolean | undefined,
555
- ): BlockTagVisibility {
556
- if (value === false || value === 'none') return 'none';
557
- if (value === undefined || value === 'active') return 'active';
558
- return 'always';
559
- }
560
-
561
- export function EditorProvider({
562
- initialMarkdown = '',
563
- initialView = 'raw',
564
- articleId = 'untitled',
565
- colorScheme: initialColorScheme = 'light',
566
- workspaceContainer = null,
567
- allowVersioning = false,
568
- versionBasename,
569
- versioningPrunePolicy = DEFAULT_PRUNE_POLICY,
570
- versioningAutoSaveIdleMs = DEFAULT_AUTOSAVE_IDLE_MS,
571
- onSaveVersion,
572
- mediaProvider = null,
573
- imageDisplayMode = 'inline',
574
- mentionProvider = null,
575
- documentLinkProvider = null,
576
- allowRecording = true,
577
- allowNarrate = true,
578
- fileName,
579
- language,
580
- inlinePreview = false,
581
- showStatusBar = true,
582
- outline = false,
583
- blockTags,
584
- blockTagVisibility: initialBlockTagVisibility,
585
- themeInheritance = 'fonts',
586
- layoutMode = 'document',
587
- viewPreferences,
588
- onViewPreferencesChange,
589
- children,
590
- }: EditorProviderProps) {
591
- // Resolve effective initial values: bundled `viewPreferences` wins over
592
- // individual props when both are passed. Individual props remain valid
593
- // for hosts that haven't migrated to the bundled API.
594
- const effectiveInlinePreview = viewPreferences?.inlinePreview ?? inlinePreview;
595
- const effectiveShowStatusBar = viewPreferences?.showStatusBar ?? showStatusBar;
596
- const effectiveOutline = viewPreferences?.outline ?? outline;
597
- const effectiveBlockTagVisibility = normalizeBlockTagVisibility(
598
- viewPreferences?.blockTagVisibility ??
599
- viewPreferences?.blockTags ??
600
- initialBlockTagVisibility ??
601
- blockTags,
602
- );
603
- const effectiveThemeInheritance = viewPreferences?.themeInheritance ?? themeInheritance;
604
- const effectiveLayoutMode = viewPreferences?.layoutMode ?? layoutMode;
605
- // Resolve once per provider mount. Changing fileName/language after mount
606
- // would require recreating the Monaco model anyway, so treat it as static.
607
- const { mode: editorMode, language: resolvedLanguage } = useMemo(
608
- () => resolveFileKind(fileName, language),
609
- [fileName, language],
610
- );
611
- // In code mode, WYSIWYG and Preview aren't rendered — force the starting
612
- // view to 'raw' so we don't boot into an unmounted surface. Image mode
613
- // has no text-editing surface at all; keep the same fallback so that any
614
- // host that switches into image mode doesn't end up in a stale view id.
615
- const [markdownSource, setMarkdownSourceRaw] = useState(initialMarkdown);
616
- const [markdownDoc, setMarkdownDocState] = useState<MarkdownDocument | null>(null);
617
- const [doc, setDoc] = useState<Doc | null>(null);
618
- const [activeView, setActiveViewRaw] = useState<EditorView>(
619
- editorMode === 'markdown' ? initialView : 'raw',
620
- );
621
- const activeViewRef = useRef(activeView);
622
- activeViewRef.current = activeView;
623
- const tiptapEditorRef = useRef<TiptapEditor | null>(null);
624
- const monacoEditorRef = useRef<MonacoEditor | null>(null);
625
- const pendingEditorScrollRef = useRef<{ view: 'raw' | 'wysiwyg'; ratio: number } | null>(null);
626
- const setActiveView = useCallback(
627
- (view: EditorView) => {
628
- // In code mode only the raw view is valid. In image mode no text view
629
- // is valid at all — ignore any switch attempt.
630
- if (editorMode === 'code' && view !== 'raw') return;
631
- if (editorMode === 'image') return;
632
-
633
- const currentView = activeViewRef.current;
634
- if (view === currentView) return;
635
-
636
- // Write and Source are separate editor instances and are unmounted as
637
- // their tabs are hidden. Capture a proportional document position
638
- // before the current instance disappears, then restore it after the
639
- // destination has mounted and completed layout.
640
- let ratio: number | null = null;
641
- let targetView: 'raw' | 'wysiwyg' | null = null;
642
- if (currentView === 'raw' && view === 'wysiwyg' && monacoEditorRef.current) {
643
- ratio = readMonacoScrollRatio(monacoEditorRef.current);
644
- targetView = 'wysiwyg';
645
- } else if (currentView === 'wysiwyg' && view === 'raw' && tiptapEditorRef.current) {
646
- ratio = readWysiwygScrollRatio(tiptapEditorRef.current);
647
- targetView = 'raw';
648
- }
649
- pendingEditorScrollRef.current =
650
- ratio === null || targetView === null ? null : { view: targetView, ratio };
651
- setActiveViewRaw(view);
652
- },
653
- [editorMode],
654
- );
655
- const [parseError, setParseError] = useState<string | null>(null);
656
- const [isParsing, setIsParsing] = useState(false);
657
- const [colorScheme, setColorScheme] = useState<EditorColorScheme>(initialColorScheme);
658
- const [inlinePreviewVisible, setInlinePreviewVisibleRaw] =
659
- useState<boolean>(effectiveInlinePreview);
660
- // Sync visibility when the host changes the prop (e.g., toggle from outside).
661
- useEffect(() => {
662
- if (viewPreferences?.inlinePreview === undefined) setInlinePreviewVisibleRaw(inlinePreview);
663
- }, [inlinePreview, viewPreferences?.inlinePreview]);
664
- const [statusBarVisible, setStatusBarVisibleRaw] = useState<boolean>(effectiveShowStatusBar);
665
- useEffect(() => {
666
- if (viewPreferences?.showStatusBar === undefined) setStatusBarVisibleRaw(showStatusBar);
667
- }, [showStatusBar, viewPreferences?.showStatusBar]);
668
- const [outlineVisible, setOutlineVisibleRaw] = useState<boolean>(effectiveOutline);
669
- useEffect(() => {
670
- if (viewPreferences?.outline === undefined) setOutlineVisibleRaw(outline);
671
- }, [outline, viewPreferences?.outline]);
672
- const [blockTagVisibilityState, setBlockTagVisibilityRaw] = useState<BlockTagVisibility>(
673
- effectiveBlockTagVisibility,
674
- );
675
- const blockTagsVisible = blockTagVisibilityState !== 'none';
676
- useEffect(() => {
677
- if (
678
- viewPreferences?.blockTagVisibility === undefined &&
679
- viewPreferences?.blockTags === undefined
680
- ) {
681
- setBlockTagVisibilityRaw(normalizeBlockTagVisibility(initialBlockTagVisibility ?? blockTags));
682
- }
683
- }, [blockTags, initialBlockTagVisibility, viewPreferences]);
684
- const [themeInheritanceState, setThemeInheritanceRaw] =
685
- useState<ThemeInheritance>(effectiveThemeInheritance);
686
- useEffect(() => {
687
- if (viewPreferences?.themeInheritance === undefined) {
688
- setThemeInheritanceRaw(themeInheritance);
689
- }
690
- }, [themeInheritance, viewPreferences?.themeInheritance]);
691
- const [layoutModeState, setLayoutModeRaw] = useState<LayoutMode>(effectiveLayoutMode);
692
- useEffect(() => {
693
- if (viewPreferences?.layoutMode === undefined) setLayoutModeRaw(layoutMode);
694
- }, [layoutMode, viewPreferences?.layoutMode]);
695
- const [imageEditTarget, setImageEditTarget] = useState<string | null>(null);
696
- const [mediaRevision, setMediaRevision] = useState(0);
697
- const openImageEdit = useCallback((relativePath: string) => {
698
- setImageEditTarget(relativePath);
699
- }, []);
700
- const closeImageEdit = useCallback(() => {
701
- setImageEditTarget(null);
702
- }, []);
703
- const bumpMediaRevision = useCallback(() => {
704
- setMediaRevision((n) => n + 1);
705
- }, []);
706
-
707
- // Sync from the bundled `viewPreferences` prop. Runs in addition to the
708
- // individual prop syncs above. When both APIs are present, the bundled
709
- // values are applied here last, keeping `viewPreferences` authoritative.
710
- useEffect(() => {
711
- if (!viewPreferences) return;
712
- if (viewPreferences.inlinePreview !== undefined) {
713
- setInlinePreviewVisibleRaw(viewPreferences.inlinePreview);
714
- }
715
- if (viewPreferences.showStatusBar !== undefined) {
716
- setStatusBarVisibleRaw(viewPreferences.showStatusBar);
717
- }
718
- if (viewPreferences.outline !== undefined) {
719
- setOutlineVisibleRaw(viewPreferences.outline);
720
- }
721
- if (viewPreferences.blockTagVisibility !== undefined) {
722
- setBlockTagVisibilityRaw(normalizeBlockTagVisibility(viewPreferences.blockTagVisibility));
723
- } else if (viewPreferences.blockTags !== undefined) {
724
- setBlockTagVisibilityRaw(normalizeBlockTagVisibility(viewPreferences.blockTags));
725
- }
726
- if (viewPreferences.themeInheritance !== undefined) {
727
- setThemeInheritanceRaw(viewPreferences.themeInheritance);
728
- }
729
- if (viewPreferences.layoutMode !== undefined) {
730
- setLayoutModeRaw(viewPreferences.layoutMode);
731
- }
732
- }, [viewPreferences]);
733
-
734
- // Wrap the view setters so user-driven changes emit a snapshot via
735
- // `onViewPreferencesChange`. Refs hold the latest values + callback so
736
- // each wrapper can build a current snapshot without re-creating itself
737
- // on every state change (the setters are kept referentially stable for
738
- // the context value's memoization).
739
- const onViewPreferencesChangeRef = useRef(onViewPreferencesChange);
740
- onViewPreferencesChangeRef.current = onViewPreferencesChange;
741
- const inlinePreviewRef = useRef(inlinePreviewVisible);
742
- inlinePreviewRef.current = inlinePreviewVisible;
743
- const statusBarRef = useRef(statusBarVisible);
744
- statusBarRef.current = statusBarVisible;
745
- const outlineRef = useRef(outlineVisible);
746
- outlineRef.current = outlineVisible;
747
- const blockTagVisibilityRef = useRef(blockTagVisibilityState);
748
- blockTagVisibilityRef.current = blockTagVisibilityState;
749
- const themeInheritanceRef = useRef(themeInheritanceState);
750
- themeInheritanceRef.current = themeInheritanceState;
751
- const layoutModeRef = useRef(layoutModeState);
752
- layoutModeRef.current = layoutModeState;
753
- const setInlinePreviewVisible = useCallback((visible: boolean) => {
754
- setInlinePreviewVisibleRaw(visible);
755
- onViewPreferencesChangeRef.current?.({
756
- inlinePreview: visible,
757
- showStatusBar: statusBarRef.current,
758
- outline: outlineRef.current,
759
- blockTags: blockTagVisibilityRef.current !== 'none',
760
- blockTagVisibility: blockTagVisibilityRef.current,
761
- themeInheritance: themeInheritanceRef.current,
762
- layoutMode: layoutModeRef.current,
763
- });
764
- }, []);
765
- const setStatusBarVisible = useCallback((visible: boolean) => {
766
- setStatusBarVisibleRaw(visible);
767
- onViewPreferencesChangeRef.current?.({
768
- inlinePreview: inlinePreviewRef.current,
769
- showStatusBar: visible,
770
- outline: outlineRef.current,
771
- blockTags: blockTagVisibilityRef.current !== 'none',
772
- blockTagVisibility: blockTagVisibilityRef.current,
773
- themeInheritance: themeInheritanceRef.current,
774
- layoutMode: layoutModeRef.current,
775
- });
776
- }, []);
777
- const setOutlineVisible = useCallback((visible: boolean) => {
778
- setOutlineVisibleRaw(visible);
779
- onViewPreferencesChangeRef.current?.({
780
- inlinePreview: inlinePreviewRef.current,
781
- showStatusBar: statusBarRef.current,
782
- outline: visible,
783
- blockTags: blockTagVisibilityRef.current !== 'none',
784
- blockTagVisibility: blockTagVisibilityRef.current,
785
- themeInheritance: themeInheritanceRef.current,
786
- layoutMode: layoutModeRef.current,
787
- });
788
- }, []);
789
- const setBlockTagVisibility = useCallback((visibility: BlockTagVisibility) => {
790
- setBlockTagVisibilityRaw(visibility);
791
- onViewPreferencesChangeRef.current?.({
792
- inlinePreview: inlinePreviewRef.current,
793
- showStatusBar: statusBarRef.current,
794
- outline: outlineRef.current,
795
- blockTags: visibility !== 'none',
796
- blockTagVisibility: visibility,
797
- themeInheritance: themeInheritanceRef.current,
798
- layoutMode: layoutModeRef.current,
799
- });
800
- }, []);
801
- const setBlockTagsVisible = useCallback(
802
- (visible: boolean) => setBlockTagVisibility(visible ? 'always' : 'none'),
803
- [setBlockTagVisibility],
804
- );
805
- const setThemeInheritance = useCallback((mode: ThemeInheritance) => {
806
- setThemeInheritanceRaw(mode);
807
- onViewPreferencesChangeRef.current?.({
808
- inlinePreview: inlinePreviewRef.current,
809
- showStatusBar: statusBarRef.current,
810
- outline: outlineRef.current,
811
- blockTags: blockTagVisibilityRef.current !== 'none',
812
- blockTagVisibility: blockTagVisibilityRef.current,
813
- themeInheritance: mode,
814
- layoutMode: layoutModeRef.current,
815
- });
816
- }, []);
817
- const setLayoutMode = useCallback((mode: LayoutMode) => {
818
- setLayoutModeRaw(mode);
819
- onViewPreferencesChangeRef.current?.({
820
- inlinePreview: inlinePreviewRef.current,
821
- showStatusBar: statusBarRef.current,
822
- outline: outlineRef.current,
823
- blockTags: blockTagVisibilityRef.current !== 'none',
824
- blockTagVisibility: blockTagVisibilityRef.current,
825
- themeInheritance: themeInheritanceRef.current,
826
- layoutMode: mode,
827
- });
828
- }, []);
829
- const [tiptapEditor, setTiptapEditorRaw] = useState<TiptapEditor | null>(null);
830
- const [monacoEditor, setMonacoEditorRaw] = useState<MonacoEditor | null>(null);
831
- const setTiptapEditor = useCallback((editor: TiptapEditor | null) => {
832
- tiptapEditorRef.current = editor;
833
- setTiptapEditorRaw(editor);
834
- }, []);
835
- const setMonacoEditor = useCallback((editor: MonacoEditor | null) => {
836
- monacoEditorRef.current = editor;
837
- setMonacoEditorRaw(editor);
838
- }, []);
839
-
840
- useEffect(() => {
841
- const pending = pendingEditorScrollRef.current;
842
- if (!pending || pending.view !== activeView) return;
843
-
844
- const restore =
845
- activeView === 'raw' && monacoEditor
846
- ? () => restoreMonacoScrollRatio(monacoEditor, pending.ratio)
847
- : activeView === 'wysiwyg' && tiptapEditor
848
- ? () => restoreWysiwygScrollRatio(tiptapEditor, pending.ratio)
849
- : null;
850
- if (!restore) return;
851
-
852
- let cancelled = false;
853
- const apply = () => {
854
- if (cancelled) return;
855
- restore();
856
- if (pendingEditorScrollRef.current === pending) pendingEditorScrollRef.current = null;
857
- };
858
-
859
- if (typeof requestAnimationFrame === 'function') {
860
- const frame = requestAnimationFrame(apply);
861
- return () => {
862
- cancelled = true;
863
- cancelAnimationFrame(frame);
864
- };
865
- }
866
-
867
- const timeout = setTimeout(apply, 0);
868
- return () => {
869
- cancelled = true;
870
- clearTimeout(timeout);
871
- };
872
- }, [activeView, monacoEditor, tiptapEditor]);
873
- // Mirror the focused canvas textbox editor (published by the inline
874
- // SceneTextOverlay through a module channel, since the canvas renders in
875
- // a detached React root outside this provider) so the toolbar can target it.
876
- const [activeSceneText, setActiveSceneText] = useState<SceneTextHandle | null>(null);
877
- const sceneTextChannel = useMemo(createSceneTextChannel, []);
878
- useEffect(() => sceneTextChannel.subscribe(setActiveSceneText), [sceneTextChannel]);
879
-
880
- const articleIdRef = useRef(articleId);
881
- articleIdRef.current = articleId;
882
-
883
- // Sync color scheme when prop changes
884
- useEffect(() => {
885
- setColorScheme(initialColorScheme);
886
- }, [initialColorScheme]);
887
-
888
- // Debounced parse on markdown source change
889
- const parseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
890
- const skipInitialDebouncedParseRef = useRef(Boolean(initialMarkdown));
891
-
892
- const doParse = useCallback((source: string) => {
893
- setIsParsing(true);
894
- try {
895
- const parsed = parseMarkdown(source);
896
- setMarkdownDocState(parsed);
897
- setParseError(null);
898
-
899
- // Generate Doc from parsed markdown
900
- try {
901
- const generatedDoc = markdownToDoc(parsed, {
902
- articleId: articleIdRef.current,
903
- });
904
- setDoc(generatedDoc);
905
- } catch (docErr: unknown) {
906
- // Doc generation can fail but markdown parse succeeded
907
- setDoc(null);
908
- console.warn('Doc generation failed:', docErr instanceof Error ? docErr.message : docErr);
909
- }
910
- } catch (err: unknown) {
911
- setParseError(err instanceof Error ? err.message : 'Parse error');
912
- setMarkdownDocState(null);
913
- setDoc(null);
914
- } finally {
915
- setIsParsing(false);
916
- }
917
- }, []);
918
-
919
- // Parse on source changes with debounce. Skipped in code/image mode —
920
- // the WYSIWYG/Preview surfaces that consume markdownDoc/doc aren't
921
- // mounted, so there's nothing to feed and no reason to run the markdown
922
- // parser on TypeScript / JSON / a binary image asset.
923
- useEffect(() => {
924
- if (editorMode !== 'markdown') return;
925
- if (skipInitialDebouncedParseRef.current) {
926
- skipInitialDebouncedParseRef.current = false;
927
- return;
928
- }
929
- if (parseTimeoutRef.current) {
930
- clearTimeout(parseTimeoutRef.current);
931
- }
932
- setIsParsing(true);
933
- parseTimeoutRef.current = setTimeout(() => {
934
- doParse(markdownSource);
935
- }, 150);
936
- return () => {
937
- if (parseTimeoutRef.current) {
938
- clearTimeout(parseTimeoutRef.current);
939
- }
940
- };
941
- }, [markdownSource, doParse, editorMode]);
942
-
943
- // Initial parse
944
- useEffect(() => {
945
- if (editorMode !== 'markdown') return;
946
- if (initialMarkdown) {
947
- doParse(initialMarkdown);
948
- }
949
- // eslint-disable-next-line react-hooks/exhaustive-deps
950
- }, []);
951
-
952
- const setMarkdownSource = useCallback((source: string) => {
953
- setMarkdownSourceRaw(source);
954
- }, []);
955
-
956
- // Block-at-a-time navigation. In 'document' mode the channel passes
957
- // through to the full source; in 'block' mode `editorSource` is the active
958
- // block's slice and `setEditorSource` splices edits back in. Gated to
959
- // markdown mode — code/image surfaces always edit the whole file.
960
- const blockNav = useBlockNavigator(markdownSource, setMarkdownSource, {
961
- enabled:
962
- (layoutModeState === 'block' || layoutModeState === 'timeline') && editorMode === 'markdown',
963
- });
964
- const {
965
- editorSource,
966
- setEditorSource,
967
- blockCount,
968
- activeBlockKey,
969
- activeBlockStartLine,
970
- goToBlock,
971
- goToBlockByLine,
972
- prevBlock,
973
- nextBlock,
974
- addBlock,
975
- } = blockNav;
976
-
977
- const insertAtCursor = useCallback(
978
- (text: string) => {
979
- if (activeView === 'wysiwyg' && tiptapEditor) {
980
- // Insert as HTML so formatting is preserved
981
- const html = markdownToTiptap(text);
982
- tiptapEditor.chain().focus().insertContent(html).run();
983
- } else if (activeView === 'raw' && monacoEditor) {
984
- const position = monacoEditor.getPosition();
985
- if (position) {
986
- const model = monacoEditor.getModel();
987
- if (model) {
988
- const range = {
989
- startLineNumber: position.lineNumber,
990
- startColumn: position.column,
991
- endLineNumber: position.lineNumber,
992
- endColumn: position.column,
993
- };
994
- monacoEditor.executeEdits('drop', [{ range, text }]);
995
- }
996
- } else {
997
- // No cursor — append
998
- setMarkdownSourceRaw((prev) => prev + '\n\n' + text);
999
- }
1000
- } else {
1001
- // Preview or no editor — append to end
1002
- setMarkdownSourceRaw((prev) => prev + '\n\n' + text);
1003
- }
1004
- },
1005
- [activeView, tiptapEditor, monacoEditor],
1006
- );
1007
-
1008
- const replaceAll = useCallback(
1009
- (text: string) => {
1010
- setMarkdownSourceRaw(text);
1011
-
1012
- // Push to editors if mounted
1013
- if (tiptapEditor) {
1014
- const html = markdownToTiptap(text);
1015
- tiptapEditor.commands.setContent(html);
1016
- }
1017
- if (monacoEditor) {
1018
- monacoEditor.setValue(text);
1019
- }
1020
- },
1021
- [tiptapEditor, monacoEditor],
1022
- );
1023
-
1024
- // ── Versioning ─────────────────────────────────────────
1025
- // Build a manager only when versioning is opted in *and* a workspace
1026
- // container exists. A versioning request without one is a misconfiguration
1027
- // — warn once so it surfaces in dev without breaking the editor.
1028
- const versioningWarnedRef = useRef(false);
1029
- useEffect(() => {
1030
- if (allowVersioning && !workspaceContainer && !versioningWarnedRef.current) {
1031
- console.warn(
1032
- '[squisq-editor] allowVersioning requires a `workspaceContainer` prop; versioning is disabled.',
1033
- );
1034
- versioningWarnedRef.current = true;
1035
- }
1036
- }, [allowVersioning, workspaceContainer]);
1037
-
1038
- const versioning = useMemo<DocumentVersionManager | null>(() => {
1039
- if (!allowVersioning || !workspaceContainer) return null;
1040
- return new DocumentVersionManager(workspaceContainer, { basename: versionBasename });
1041
- }, [allowVersioning, workspaceContainer, versionBasename]);
1042
-
1043
- const onSaveVersionRef = useRef(onSaveVersion);
1044
- onSaveVersionRef.current = onSaveVersion;
1045
- const prunePolicyRef = useRef(versioningPrunePolicy);
1046
- prunePolicyRef.current = versioningPrunePolicy;
1047
-
1048
- const saveVersion = useCallback(
1049
- async (options?: SaveVersionOptions): Promise<SaveVersionResult> => {
1050
- if (!versioning) {
1051
- const skipped: SaveVersionResult = { saved: false, version: null, reason: 'no-document' };
1052
- onSaveVersionRef.current?.(skipped);
1053
- return skipped;
1054
- }
1055
- const result = await versioning.saveVersion(options);
1056
- onSaveVersionRef.current?.(result);
1057
- if (result.saved) {
1058
- // Fire-and-forget prune. Failures here shouldn't block the save.
1059
- versioning.pruneVersions(prunePolicyRef.current).catch((err: unknown) => {
1060
- console.warn(
1061
- '[squisq-editor] pruneVersions failed:',
1062
- err instanceof Error ? err.message : err,
1063
- );
1064
- });
1065
- }
1066
- return result;
1067
- },
1068
- [versioning],
1069
- );
1070
-
1071
- // Auto-save: stamp a new snapshot after `versioningAutoSaveIdleMs` of
1072
- // idle. The "only save if different" check inside `saveVersion` makes
1073
- // most ticks no-ops, so this is cheap. Disabled when the idle delay is
1074
- // 0 or versioning isn't active.
1075
- //
1076
- // We pass the live `markdownSource` explicitly so saveVersion never has
1077
- // to fall back to `container.readDocument()`. That fallback would fail
1078
- // in setups where the markdown file lives outside the versioning
1079
- // container's scope (e.g. DocBlocks, where the container points at
1080
- // `<basename>_files/` while the doc itself lives in the parent
1081
- // directory). Using the editor's live state also ensures the snapshot
1082
- // captures the most recent edit even if the host's autosave to the
1083
- // container hasn't flushed yet.
1084
- useEffect(() => {
1085
- if (!versioning) return;
1086
- if (versioningAutoSaveIdleMs <= 0) return;
1087
- const timer = setTimeout(() => {
1088
- saveVersion({ content: markdownSource }).catch((err: unknown) => {
1089
- console.warn(
1090
- '[squisq-editor] auto-save version failed:',
1091
- err instanceof Error ? err.message : err,
1092
- );
1093
- });
1094
- }, versioningAutoSaveIdleMs);
1095
- return () => clearTimeout(timer);
1096
- }, [markdownSource, versioning, versioningAutoSaveIdleMs, saveVersion]);
1097
-
1098
- const setMarkdownDoc = useCallback((newDoc: MarkdownDocument) => {
1099
- setMarkdownDocState(newDoc);
1100
- // Stringify to update the raw source
1101
- try {
1102
- const newSource = stringifyMarkdown(newDoc);
1103
- setMarkdownSourceRaw(newSource);
1104
- setParseError(null);
1105
-
1106
- // Generate Doc
1107
- try {
1108
- const generatedDoc = markdownToDoc(newDoc, {
1109
- articleId: articleIdRef.current,
1110
- });
1111
- setDoc(generatedDoc);
1112
- } catch (docErr: unknown) {
1113
- setDoc(null);
1114
- console.warn('Doc generation failed:', docErr instanceof Error ? docErr.message : docErr);
1115
- }
1116
- } catch (err: unknown) {
1117
- setParseError(err instanceof Error ? err.message : 'Stringify error');
1118
- }
1119
- }, []);
1120
-
1121
- const value = useMemo<EditorContextValue>(
1122
- () => ({
1123
- markdownSource,
1124
- markdownDoc,
1125
- doc,
1126
- activeView,
1127
- parseError,
1128
- isParsing,
1129
- colorScheme,
1130
- editorMode,
1131
- language: resolvedLanguage,
1132
- inlinePreviewVisible,
1133
- statusBarVisible,
1134
- outlineVisible,
1135
- blockTagVisibility: blockTagVisibilityState,
1136
- blockTagsVisible,
1137
- themeInheritance: themeInheritanceState,
1138
- layoutMode: layoutModeState,
1139
- editorSource,
1140
- blockCount,
1141
- activeBlockKey,
1142
- activeBlockStartLine,
1143
- imageEditTarget,
1144
- mediaRevision,
1145
- allowRecording,
1146
- allowNarrate,
1147
- tiptapEditor,
1148
- monacoEditor,
1149
- activeSceneText,
1150
- sceneTextChannel,
1151
- workspaceContainer,
1152
- versioning,
1153
- saveVersion,
1154
- mediaProvider,
1155
- imageDisplayMode,
1156
- mentionProvider,
1157
- documentLinkProvider,
1158
- setMarkdownSource,
1159
- setEditorSource,
1160
- setLayoutMode,
1161
- goToBlock,
1162
- goToBlockByLine,
1163
- prevBlock,
1164
- nextBlock,
1165
- addBlock,
1166
- setMarkdownDoc,
1167
- setActiveView,
1168
- setTiptapEditor,
1169
- setMonacoEditor,
1170
- setColorScheme,
1171
- setInlinePreviewVisible,
1172
- setStatusBarVisible,
1173
- setOutlineVisible,
1174
- setBlockTagVisibility,
1175
- setBlockTagsVisible,
1176
- setThemeInheritance,
1177
- insertAtCursor,
1178
- replaceAll,
1179
- openImageEdit,
1180
- closeImageEdit,
1181
- bumpMediaRevision,
1182
- }),
1183
- [
1184
- markdownSource,
1185
- markdownDoc,
1186
- doc,
1187
- activeView,
1188
- parseError,
1189
- isParsing,
1190
- colorScheme,
1191
- editorMode,
1192
- resolvedLanguage,
1193
- inlinePreviewVisible,
1194
- statusBarVisible,
1195
- outlineVisible,
1196
- blockTagVisibilityState,
1197
- blockTagsVisible,
1198
- themeInheritanceState,
1199
- layoutModeState,
1200
- editorSource,
1201
- blockCount,
1202
- activeBlockKey,
1203
- activeBlockStartLine,
1204
- tiptapEditor,
1205
- monacoEditor,
1206
- activeSceneText,
1207
- sceneTextChannel,
1208
- workspaceContainer,
1209
- versioning,
1210
- saveVersion,
1211
- mediaProvider,
1212
- imageDisplayMode,
1213
- mentionProvider,
1214
- documentLinkProvider,
1215
- setMarkdownSource,
1216
- setEditorSource,
1217
- setLayoutMode,
1218
- goToBlock,
1219
- goToBlockByLine,
1220
- prevBlock,
1221
- nextBlock,
1222
- addBlock,
1223
- setMarkdownDoc,
1224
- setActiveView,
1225
- setTiptapEditor,
1226
- setMonacoEditor,
1227
- setColorScheme,
1228
- setInlinePreviewVisible,
1229
- setStatusBarVisible,
1230
- setOutlineVisible,
1231
- setBlockTagVisibility,
1232
- setBlockTagsVisible,
1233
- setThemeInheritance,
1234
- insertAtCursor,
1235
- replaceAll,
1236
- imageEditTarget,
1237
- mediaRevision,
1238
- allowRecording,
1239
- allowNarrate,
1240
- openImageEdit,
1241
- closeImageEdit,
1242
- bumpMediaRevision,
1243
- ],
1244
- );
1245
-
1246
- return <EditorContext.Provider value={value}>{children}</EditorContext.Provider>;
1247
- }