@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.
- package/LICENSE +21 -0
- package/NOTICE.md +40 -0
- package/README.md +10 -2
- package/dist/index.d.ts +683 -60
- package/dist/index.js +9648 -3750
- package/dist/monaco.js +0 -1
- package/dist/styles/index.css +1160 -241
- package/package.json +10 -6
- package/dist/index.js.map +0 -1
- package/dist/monaco.js.map +0 -1
- package/src/BlockCardView.tsx +0 -121
- package/src/BlockPreviewPanel.tsx +0 -69
- package/src/BlockPropertiesPopover.tsx +0 -222
- package/src/DocumentSettingsDialog.tsx +0 -272
- package/src/DropZoneOverlay.tsx +0 -137
- package/src/EditorContext.tsx +0 -1247
- package/src/EditorShell.tsx +0 -1334
- package/src/EmojiPicker.tsx +0 -332
- package/src/FolderView.tsx +0 -131
- package/src/Icon.tsx +0 -26
- package/src/ImageEditor.tsx +0 -395
- package/src/ImageNodeView.tsx +0 -284
- package/src/ImageViewer.tsx +0 -221
- package/src/InlineIcon.ts +0 -84
- package/src/InlinePreviewGutter.tsx +0 -590
- package/src/LinkDialog.tsx +0 -276
- package/src/MediaBin.tsx +0 -500
- package/src/MentionExtension.tsx +0 -261
- package/src/OutlinePanel.tsx +0 -547
- package/src/PlainHtmlPreview.tsx +0 -277
- package/src/PreviewControls.tsx +0 -1250
- package/src/PreviewPanel.tsx +0 -422
- package/src/RawEditor.tsx +0 -731
- package/src/RecorderEntry.tsx +0 -159
- package/src/StatusBar.tsx +0 -51
- package/src/TemplateAnnotation.ts +0 -167
- package/src/TemplateContentPreview.tsx +0 -56
- package/src/TemplatePicker.tsx +0 -1314
- package/src/ThemeCustomizerPanel.tsx +0 -304
- package/src/ThemePicker.tsx +0 -433
- package/src/TimelineBlockPreview.tsx +0 -37
- package/src/TimelineTrack.tsx +0 -671
- package/src/Toolbar.tsx +0 -2587
- package/src/Tooltip.tsx +0 -142
- package/src/TransitionPicker.tsx +0 -390
- package/src/VersionHistoryPanel.tsx +0 -359
- package/src/ViewMenuPanel.tsx +0 -189
- package/src/ViewSwitcher.tsx +0 -56
- package/src/WysiwygEditor.tsx +0 -792
- package/src/__tests__/blockProperties.test.ts +0 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +0 -33
- package/src/__tests__/blockRange.test.ts +0 -105
- package/src/__tests__/blockTagActivity.test.ts +0 -183
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +0 -75
- package/src/__tests__/buildPreviewDocContent.test.ts +0 -48
- package/src/__tests__/buildPreviewDocTransition.test.ts +0 -92
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +0 -60
- package/src/__tests__/codeContextSectionView.test.tsx +0 -97
- package/src/__tests__/codeContextZoneManager.test.ts +0 -127
- package/src/__tests__/createShapeLayer.test.ts +0 -46
- package/src/__tests__/customLayoutManagerTheme.test.tsx +0 -34
- package/src/__tests__/detectMarkdown.test.ts +0 -73
- package/src/__tests__/diffContextSections.test.ts +0 -39
- package/src/__tests__/documentSettingsDialog.test.tsx +0 -197
- package/src/__tests__/drawingShapeRoundTrip.test.ts +0 -49
- package/src/__tests__/editorScrollHandoff.test.tsx +0 -102
- package/src/__tests__/editorScrollSync.test.ts +0 -65
- package/src/__tests__/editorShellCodeContext.test.tsx +0 -86
- package/src/__tests__/editorShellProps.test.tsx +0 -485
- package/src/__tests__/embeddedMedia.test.ts +0 -48
- package/src/__tests__/emojiPicker.test.tsx +0 -93
- package/src/__tests__/fileKind.test.ts +0 -112
- package/src/__tests__/headingTransition.test.ts +0 -176
- package/src/__tests__/imageEditAffordance.test.tsx +0 -272
- package/src/__tests__/imageEditModalContract.test.tsx +0 -119
- package/src/__tests__/imageEditorShell.test.tsx +0 -94
- package/src/__tests__/imageEditorState.test.ts +0 -171
- package/src/__tests__/inlinePreviewGutter.test.tsx +0 -62
- package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +0 -103
- package/src/__tests__/jsonEditor.test.tsx +0 -223
- package/src/__tests__/layersPanel.test.tsx +0 -167
- package/src/__tests__/layoutChildRoundTrip.test.ts +0 -71
- package/src/__tests__/linkDialogDocPicker.test.tsx +0 -137
- package/src/__tests__/majorApiTypes.test.ts +0 -26
- package/src/__tests__/mediaAttachmentFlow.test.ts +0 -110
- package/src/__tests__/mediaBinDrop.test.tsx +0 -90
- package/src/__tests__/mediaReferences.test.ts +0 -82
- package/src/__tests__/narrationSave.test.ts +0 -148
- package/src/__tests__/outlinePanel.test.tsx +0 -169
- package/src/__tests__/outlineSource.test.ts +0 -217
- package/src/__tests__/plainHtmlPreview.test.tsx +0 -125
- package/src/__tests__/presentationMode.test.tsx +0 -290
- package/src/__tests__/previewControls.test.tsx +0 -540
- package/src/__tests__/previewPanelAudioFailure.test.tsx +0 -46
- package/src/__tests__/previewPanelPresentation.test.tsx +0 -126
- package/src/__tests__/previewPanelTransforms.test.tsx +0 -117
- package/src/__tests__/propertiesPanel.test.tsx +0 -69
- package/src/__tests__/rawEditorModelIsolation.test.ts +0 -19
- package/src/__tests__/recorderFormats.test.ts +0 -146
- package/src/__tests__/recorderMediaInsert.test.ts +0 -86
- package/src/__tests__/recorderTheme.test.tsx +0 -42
- package/src/__tests__/recorderTimingJson.test.ts +0 -41
- package/src/__tests__/resolveBlockVisual.test.ts +0 -43
- package/src/__tests__/selectionConversions.test.ts +0 -80
- package/src/__tests__/statusBar.test.tsx +0 -27
- package/src/__tests__/teleprompter.test.tsx +0 -190
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +0 -99
- package/src/__tests__/templateContentPreview.test.ts +0 -101
- package/src/__tests__/templatePickerMetadata.test.ts +0 -32
- package/src/__tests__/templatePickerPortal.test.tsx +0 -62
- package/src/__tests__/timelineSource.test.ts +0 -134
- package/src/__tests__/tiptapBridge.test.ts +0 -566
- package/src/__tests__/tiptapBridgeConformance.test.ts +0 -47
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +0 -109
- package/src/__tests__/tiptapImageRoundTrip.test.ts +0 -73
- package/src/__tests__/toolbarSelectionConversion.test.tsx +0 -164
- package/src/__tests__/tooltip.test.tsx +0 -72
- package/src/__tests__/transformStyleId.test.ts +0 -13
- package/src/__tests__/transitionCatalog.test.ts +0 -64
- package/src/__tests__/useBlockNavigator.test.tsx +0 -67
- package/src/__tests__/useImageEditor.test.tsx +0 -226
- package/src/__tests__/useJsonEditorTokens.test.ts +0 -59
- package/src/__tests__/useMediaRecorder.test.ts +0 -302
- package/src/__tests__/useTimelineClock.test.ts +0 -21
- package/src/__tests__/versionHistory.test.tsx +0 -197
- package/src/__tests__/viewMenuPanel.test.tsx +0 -130
- package/src/__tests__/wysiwygImageUpload.test.ts +0 -69
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +0 -56
- package/src/asciiDiagram/AsciiDiagramExtension.ts +0 -343
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +0 -254
- package/src/asciiDiagram/RepairableDiagramExtension.ts +0 -200
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +0 -145
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +0 -159
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +0 -284
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +0 -114
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +0 -43
- package/src/asciiDiagram/asciiDiagramCommands.ts +0 -174
- package/src/asciiDiagram/asciiDiagramData.ts +0 -149
- package/src/asciiDiagram/asciiDiagramOps.ts +0 -238
- package/src/asciiDiagram/asciiPaste.ts +0 -19
- package/src/blockProperties.ts +0 -88
- package/src/blockRange.ts +0 -132
- package/src/blockSlice.ts +0 -75
- package/src/blockTagActivity.ts +0 -233
- package/src/buildDocumentPreviewMarkdown.ts +0 -168
- package/src/buildPreviewDoc.ts +0 -405
- package/src/codeContext/CodeContextSectionView.tsx +0 -124
- package/src/codeContext/CodeContextZoneManager.ts +0 -149
- package/src/codeContext/CodeContextZones.tsx +0 -121
- package/src/codeContext/diffContextSections.ts +0 -38
- package/src/codeContext/types.ts +0 -75
- package/src/customTemplates/AddBin.tsx +0 -126
- package/src/customTemplates/CustomLayoutManager.tsx +0 -234
- package/src/customTemplates/CustomTemplateContext.tsx +0 -188
- package/src/customTemplates/LayerToolbar.tsx +0 -580
- package/src/customTemplates/ShapeGlyph.tsx +0 -47
- package/src/customTemplates/TemplateDesigner.tsx +0 -433
- package/src/customTemplates/__tests__/library.test.ts +0 -96
- package/src/customTemplates/__tests__/normalizePositions.test.ts +0 -109
- package/src/customTemplates/__tests__/shapeDefs.test.ts +0 -49
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +0 -108
- package/src/customTemplates/designer.css +0 -732
- package/src/customTemplates/index.ts +0 -31
- package/src/customTemplates/library.ts +0 -110
- package/src/customTemplates/normalizePositions.ts +0 -75
- package/src/customTemplates/shapeDefs.ts +0 -131
- package/src/customTemplates/thumbnail.tsx +0 -63
- package/src/customTemplates/tokenDefs.ts +0 -60
- package/src/customTemplates/useDocCustomTemplates.ts +0 -52
- package/src/customTemplates/useMemoryLayerAdapter.ts +0 -129
- package/src/customThemes/CustomThemeContext.tsx +0 -185
- package/src/customThemes/CustomThemeDialog.tsx +0 -316
- package/src/customThemes/ImportThemeSection.tsx +0 -178
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +0 -64
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +0 -47
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +0 -60
- package/src/customThemes/__tests__/importThemeSection.test.tsx +0 -192
- package/src/customThemes/customThemeLibrary.ts +0 -100
- package/src/customThemes/index.ts +0 -37
- package/src/customThemes/themeControls.tsx +0 -229
- package/src/customThemes/themeDraft.ts +0 -288
- package/src/customThemes/useDocCustomThemes.ts +0 -49
- package/src/detectMarkdown.ts +0 -62
- package/src/diagram/DiagramCanvas.tsx +0 -259
- package/src/diagram/DiagramMaximizedOverlay.tsx +0 -46
- package/src/diagram/diagramCommands.ts +0 -604
- package/src/diagram/diagramConstants.ts +0 -17
- package/src/diagram/types.ts +0 -35
- package/src/editorScrollSync.ts +0 -73
- package/src/embeddedMedia.ts +0 -78
- package/src/emojiData.ts +0 -1317
- package/src/fileKind.ts +0 -158
- package/src/frontmatter.ts +0 -29
- package/src/frontmatterSettings.ts +0 -23
- package/src/headingTransition.ts +0 -291
- package/src/hooks/useFileDrop.ts +0 -266
- package/src/imageEditor/CanvasSurface.tsx +0 -703
- package/src/imageEditor/ImageVersionHistoryDropdown.tsx +0 -396
- package/src/imageEditor/LayersPanel.tsx +0 -218
- package/src/imageEditor/PropertiesPanel.tsx +0 -474
- package/src/imageEditor/Toolbar.tsx +0 -459
- package/src/imageEditor/createShapeLayer.ts +0 -280
- package/src/imageEditor/icons.tsx +0 -68
- package/src/imageEditor/image-editor.css +0 -574
- package/src/imageEditor/layers/EditorImageLayer.tsx +0 -45
- package/src/imageEditor/layers/EditorShapeLayer.tsx +0 -62
- package/src/imageEditor/layers/EditorTextLayer.tsx +0 -45
- package/src/imageEditor/layers/SelectionHandles.tsx +0 -86
- package/src/imageEditor/state.ts +0 -173
- package/src/imageEditor/useImageEditor.ts +0 -385
- package/src/imageEditor/useImageEditorTokens.ts +0 -70
- package/src/index.ts +0 -470
- package/src/jsonEditor/EmbeddedRichTextField.tsx +0 -81
- package/src/jsonEditor/JsonEditor.tsx +0 -81
- package/src/jsonEditor/JsonEditorContext.tsx +0 -81
- package/src/jsonEditor/RenderNode.tsx +0 -87
- package/src/jsonEditor/editors.tsx +0 -745
- package/src/jsonEditor/index.ts +0 -2
- package/src/jsonEditor/json-editor.css +0 -463
- package/src/jsonEditor/useJsonEditorTokens.ts +0 -33
- package/src/mediaDragMime.ts +0 -32
- package/src/mediaEntries.ts +0 -12
- package/src/mediaReferences.ts +0 -299
- package/src/monaco.ts +0 -35
- package/src/monacoWorkers.ts +0 -89
- package/src/outlineSource.ts +0 -171
- package/src/presentation/PresentationMode.tsx +0 -596
- package/src/rawEditorIsolation.ts +0 -18
- package/src/recorder/RecorderButton.tsx +0 -80
- package/src/recorder/RecorderModal.tsx +0 -706
- package/src/recorder/RecorderPanel.tsx +0 -77
- package/src/recorder/formats.ts +0 -159
- package/src/recorder/hooks/useMediaRecorder.ts +0 -436
- package/src/recorder/hooks/useStreamPreview.ts +0 -47
- package/src/recorder/insertMediaBlock.ts +0 -30
- package/src/recorder/sources/cameraStream.ts +0 -32
- package/src/recorder/sources/micStream.ts +0 -25
- package/src/recorder/sources/screenStream.ts +0 -162
- package/src/recorder/timingJson.ts +0 -66
- package/src/resolveBlockVisual.ts +0 -39
- package/src/scene/Scene.tsx +0 -616
- package/src/scene/SceneBlockExtension.ts +0 -205
- package/src/scene/SceneBlockToolbar.tsx +0 -201
- package/src/scene/SceneBlockWidget.tsx +0 -441
- package/src/scene/ScenePropsBar.tsx +0 -85
- package/src/scene/SceneSelection.tsx +0 -111
- package/src/scene/SceneSideToolbar.tsx +0 -89
- package/src/scene/SceneViewport.tsx +0 -106
- package/src/scene/ShapePalette.tsx +0 -196
- package/src/scene/__tests__/DiagramAdapter.test.ts +0 -142
- package/src/scene/__tests__/SceneBlockExtension.test.ts +0 -33
- package/src/scene/__tests__/bezierEdit.test.ts +0 -85
- package/src/scene/__tests__/blockLayers.test.ts +0 -57
- package/src/scene/__tests__/sceneIsolation.test.tsx +0 -119
- package/src/scene/__tests__/shapeLayers.test.ts +0 -106
- package/src/scene/__tests__/useSceneHitTest.test.ts +0 -90
- package/src/scene/__tests__/useScenePanZoom.test.ts +0 -103
- package/src/scene/adapters/DiagramAdapter.ts +0 -79
- package/src/scene/adapters/DrawingAdapter.ts +0 -420
- package/src/scene/adapters/LayoutAdapter.ts +0 -313
- package/src/scene/adapters/blockLayers.ts +0 -159
- package/src/scene/commands/SceneCommand.ts +0 -84
- package/src/scene/commands/drawingCommands.ts +0 -318
- package/src/scene/commands/layoutCommands.ts +0 -301
- package/src/scene/hooks/useSceneHitTest.ts +0 -105
- package/src/scene/hooks/useScenePanZoom.ts +0 -147
- package/src/scene/hooks/useSceneSelection.ts +0 -62
- package/src/scene/index.ts +0 -90
- package/src/scene/layers/DiagramEdges.tsx +0 -142
- package/src/scene/layers/edgeGeometry.ts +0 -104
- package/src/scene/layers/nodeCard.tsx +0 -164
- package/src/scene/layers/renderLayer.tsx +0 -70
- package/src/scene/layers/shapeLayers.ts +0 -201
- package/src/scene/paths/bezierEdit.ts +0 -208
- package/src/scene/scene.css +0 -792
- package/src/scene/text/SceneTextOverlay.tsx +0 -161
- package/src/scene/text/sceneTextChannel.ts +0 -46
- package/src/scene/text/sceneTextConfig.ts +0 -31
- package/src/scene/text/sceneTiptap.ts +0 -36
- package/src/scene/text/useSceneTextEditing.ts +0 -39
- package/src/scene/tools/ConnectTool.ts +0 -209
- package/src/scene/tools/DrawingConnectTool.ts +0 -231
- package/src/scene/tools/PathTool.ts +0 -158
- package/src/scene/tools/PlaceTool.ts +0 -47
- package/src/scene/tools/SceneTool.ts +0 -85
- package/src/scene/tools/SelectTool.ts +0 -300
- package/src/scene/tools/ShapeTool.ts +0 -143
- package/src/scene/tools/TextTool.ts +0 -72
- package/src/scene/tools/TokenTool.ts +0 -95
- package/src/scene/tools/createDrawShapeTool.ts +0 -82
- package/src/selectionConversions.ts +0 -155
- package/src/styles/ascii-diagram.css +0 -79
- package/src/styles/ascii-timeline.css +0 -596
- package/src/styles/code-context.css +0 -155
- package/src/styles/diagram.css +0 -183
- package/src/styles/editor.css +0 -6811
- package/src/styles/folder-view.css +0 -210
- package/src/styles/image-edit-affordance.css +0 -201
- package/src/styles/index.css +0 -18
- package/src/styles/tree-view.css +0 -189
- package/src/teleprompter/TeleprompterControls.tsx +0 -218
- package/src/teleprompter/TeleprompterSelfView.tsx +0 -22
- package/src/teleprompter/TeleprompterSurface.tsx +0 -267
- package/src/teleprompter/TeleprompterView.tsx +0 -338
- package/src/teleprompter/canvasRenderer.ts +0 -161
- package/src/teleprompter/floatingWindow.ts +0 -352
- package/src/teleprompter/index.ts +0 -61
- package/src/teleprompter/pcmWorklet.ts +0 -62
- package/src/teleprompter/recording/insertPreamble.ts +0 -80
- package/src/teleprompter/recording/narrationSave.ts +0 -134
- package/src/teleprompter/recording/useNarrationRecorder.ts +0 -367
- package/src/teleprompter/scrollModel.ts +0 -85
- package/src/teleprompter/teleprompterTheme.ts +0 -303
- package/src/teleprompter/types.ts +0 -38
- package/src/teleprompter/useFloatingWindow.ts +0 -74
- package/src/teleprompter/useMicAnalysis.ts +0 -211
- package/src/teleprompter/useTeleprompter.ts +0 -421
- package/src/templateContentPreviewResolver.ts +0 -360
- package/src/timeline/TimelineEditorWidget.tsx +0 -861
- package/src/timeline/TimelineViewExtension.ts +0 -252
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +0 -414
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +0 -163
- package/src/timeline/__tests__/timelineCommands.test.ts +0 -359
- package/src/timeline/__tests__/timelineOps.test.ts +0 -277
- package/src/timeline/__tests__/timelinePaste.test.ts +0 -34
- package/src/timeline/timelineCommands.ts +0 -421
- package/src/timeline/timelineData.ts +0 -54
- package/src/timeline/timelineOps.ts +0 -381
- package/src/timeline/timelinePaste.ts +0 -8
- package/src/timelineSource.ts +0 -244
- package/src/tiptap/TiptapAudio.tsx +0 -86
- package/src/tiptap/TiptapVideo.tsx +0 -119
- package/src/tiptap/useResolvedMediaSrc.ts +0 -47
- package/src/tiptapBridge.ts +0 -986
- package/src/tooltipPlacement.ts +0 -13
- package/src/transformStyleId.ts +0 -17
- package/src/transitionCatalog.ts +0 -159
- package/src/treeview/TreeOutlineWidget.tsx +0 -390
- package/src/treeview/TreeViewExtension.ts +0 -250
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +0 -156
- package/src/treeview/__tests__/TreeViewExtension.test.ts +0 -122
- package/src/treeview/__tests__/treeOps.test.ts +0 -177
- package/src/treeview/__tests__/treePaste.test.ts +0 -40
- package/src/treeview/__tests__/treeViewCommands.test.ts +0 -179
- package/src/treeview/treeOps.ts +0 -231
- package/src/treeview/treePaste.ts +0 -13
- package/src/treeview/treeViewCommands.ts +0 -99
- package/src/treeview/treeViewData.ts +0 -41
- package/src/types/monaco-shims.d.ts +0 -10
- package/src/useBlockNavigator.ts +0 -153
- package/src/useHeadingLayout.ts +0 -294
- package/src/useMonacoLoader.ts +0 -82
- package/src/useTimelineClock.ts +0 -76
- package/src/utils/collectInlineFontAwesomeCss.ts +0 -69
- package/src/utils/dropUtils.ts +0 -136
- package/src/utils/normalizeMalformedAssetUrl.ts +0 -22
- package/src/wysiwygImageUpload.ts +0 -113
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AsciiDiagramExtension — Tiptap/ProseMirror plugin that turns code blocks
|
|
3
|
-
* containing ASCII box-and-line diagrams into interactive diagram canvases.
|
|
4
|
-
*
|
|
5
|
-
* The FENCE TEXT REMAINS THE SOURCE OF TRUTH: the plugin never converts
|
|
6
|
-
* the document. For each qualifying codeBlock it:
|
|
7
|
-
* 1. hides the `<pre>` via a node decoration (or tags it visible-monospace
|
|
8
|
-
* when the user toggles "source" mode), and
|
|
9
|
-
* 2. mounts an `AsciiDiagramWidget` after it via a widget decoration.
|
|
10
|
-
*
|
|
11
|
-
* Identity: codeBlocks have no persisted id and their content changes on
|
|
12
|
-
* every canvas edit (self-inflicted fence rewrite), so neither a content
|
|
13
|
-
* hash nor a raw position can key the widget. Instead the plugin keeps a
|
|
14
|
-
* POSITION REGISTRY in its state: each qualifying block gets a synthetic
|
|
15
|
-
* session id (`ascii-N`); on every doc change old positions are remapped
|
|
16
|
-
* through `tr.mapping` and matched against the new doc walk. Same id →
|
|
17
|
-
* same widget decoration key → ProseMirror keeps the DOM + React root
|
|
18
|
-
* alive (pan/zoom survives), exactly like the heading-key trick the
|
|
19
|
-
* legacy DiagramExtension used.
|
|
20
|
-
*
|
|
21
|
-
* Hysteresis: a NEW block must pass full detection (`detectAsciiDiagram`,
|
|
22
|
-
* ≥2 boxes + thresholds); an already-registered block stays interactive
|
|
23
|
-
* while its fence still parses to ≥1 box, so deleting down to one node
|
|
24
|
-
* doesn't kick the user back to a raw code block mid-edit.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { Extension } from '@tiptap/core';
|
|
28
|
-
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
29
|
-
import type { Transaction } from '@tiptap/pm/state';
|
|
30
|
-
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
31
|
-
import type { Node as PMNode } from '@tiptap/pm/model';
|
|
32
|
-
import type { Editor } from '@tiptap/react';
|
|
33
|
-
import { createRoot, type Root } from 'react-dom/client';
|
|
34
|
-
import { createElement } from 'react';
|
|
35
|
-
import {
|
|
36
|
-
detectAsciiDiagram,
|
|
37
|
-
isEligibleAsciiFenceLang,
|
|
38
|
-
isExplicitDiagramLang,
|
|
39
|
-
parseAsciiDiagram,
|
|
40
|
-
type AsciiDiagram,
|
|
41
|
-
} from '@bendyline/squisq/doc';
|
|
42
|
-
import { AsciiDiagramWidget } from './AsciiDiagramWidget';
|
|
43
|
-
import type { SceneTextChannel } from '../scene/text/sceneTextChannel';
|
|
44
|
-
|
|
45
|
-
export interface AsciiDiagramBlockEntry {
|
|
46
|
-
/** Synthetic session id (`ascii-N`), stable across edits for one block. */
|
|
47
|
-
id: string;
|
|
48
|
-
/** Document position of the codeBlock node at the current state. */
|
|
49
|
-
pos: number;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface AsciiDiagramPluginState {
|
|
53
|
-
entries: AsciiDiagramBlockEntry[];
|
|
54
|
-
decorations: DecorationSet;
|
|
55
|
-
sourceVisible: ReadonlySet<string>;
|
|
56
|
-
/** Monotonic id allocator. */
|
|
57
|
-
seq: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const ASCII_DIAGRAM_KEY = new PluginKey<AsciiDiagramPluginState>('squisq-ascii-diagram');
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Gate caches — keyed on the PMNode instance. ProseMirror's structural
|
|
64
|
-
// sharing reuses node objects for unchanged blocks, so only an actually
|
|
65
|
-
// edited fence re-runs detection/parsing.
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
const detectCache = new WeakMap<PMNode, AsciiDiagram | null>();
|
|
69
|
-
const parseCache = new WeakMap<PMNode, AsciiDiagram | null>();
|
|
70
|
-
|
|
71
|
-
function fenceLangOf(node: PMNode): string | null {
|
|
72
|
-
const lang = (node.attrs as { language?: string | null }).language;
|
|
73
|
-
return typeof lang === 'string' ? lang : null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Full-detection gate for NEW blocks: lang allowlist + content thresholds. */
|
|
77
|
-
export function getAsciiDiagramForNode(node: PMNode): AsciiDiagram | null {
|
|
78
|
-
if (node.type.name !== 'codeBlock') return null;
|
|
79
|
-
const cached = detectCache.get(node);
|
|
80
|
-
if (cached !== undefined) return cached;
|
|
81
|
-
let result: AsciiDiagram | null = null;
|
|
82
|
-
const lang = fenceLangOf(node);
|
|
83
|
-
if (isEligibleAsciiFenceLang(lang)) {
|
|
84
|
-
const detection = detectAsciiDiagram(node.textContent, {
|
|
85
|
-
explicit: isExplicitDiagramLang(lang),
|
|
86
|
-
});
|
|
87
|
-
if (detection.isDiagram && detection.diagram) {
|
|
88
|
-
result = detection.diagram;
|
|
89
|
-
parseCache.set(node, result);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
detectCache.set(node, result);
|
|
93
|
-
return result;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Box-drawing corners/junctions/verticals leaking into a label = a garbled parse. */
|
|
97
|
-
const GARBLED_LABEL = /[│┃║┌┐└┘├┤┬┴┼╭╮╰╯┏┓┗┛╔╗╚╝┣┫┳┻╋╠╣╦╩╬]/;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Relaxed gate for ALREADY-REGISTERED blocks: any parse with ≥1 box, so
|
|
101
|
-
* editing a diagram down doesn't kick the user out mid-edit. But a GARBLED
|
|
102
|
-
* parse (box-drawing chars leaked into labels — the desync signature of
|
|
103
|
-
* broken art) is never a valid diagram; rejecting it here keeps a broken
|
|
104
|
-
* fence from being held as a diagram by hysteresis (e.g. after undoing a
|
|
105
|
-
* repair, where the fence reverts to broken art the RepairableExtension owns).
|
|
106
|
-
*/
|
|
107
|
-
export function parseAsciiDiagramForNode(node: PMNode): AsciiDiagram | null {
|
|
108
|
-
if (node.type.name !== 'codeBlock') return null;
|
|
109
|
-
const cached = parseCache.get(node);
|
|
110
|
-
if (cached !== undefined) return cached;
|
|
111
|
-
let result: AsciiDiagram | null = null;
|
|
112
|
-
if (isEligibleAsciiFenceLang(fenceLangOf(node))) {
|
|
113
|
-
const parsed = parseAsciiDiagram(node.textContent);
|
|
114
|
-
if (parsed.nodes.length > 0 && !parsed.nodes.some((n) => GARBLED_LABEL.test(n.label))) {
|
|
115
|
-
result = parsed;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
parseCache.set(node, result);
|
|
119
|
-
return result;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Resolve a registered block's current doc position (null when gone). */
|
|
123
|
-
export function findAsciiDiagramBlockPos(editor: Editor, blockId: string): number | null {
|
|
124
|
-
const state = ASCII_DIAGRAM_KEY.getState(editor.state);
|
|
125
|
-
return state?.entries.find((e) => e.id === blockId)?.pos ?? null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function isAsciiSourceVisible(editor: Editor, blockId: string): boolean {
|
|
129
|
-
const state = ASCII_DIAGRAM_KEY.getState(editor.state);
|
|
130
|
-
return state?.sourceVisible.has(blockId) ?? false;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Toggle the raw-fence view for one diagram block (meta-only transaction). */
|
|
134
|
-
export function toggleAsciiSource(editor: Editor, blockId: string): void {
|
|
135
|
-
const tr = editor.state.tr.setMeta(ASCII_DIAGRAM_KEY, { toggleSource: blockId });
|
|
136
|
-
editor.view.dispatch(tr);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
// Plugin state
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
|
|
143
|
-
interface WidgetRootRef {
|
|
144
|
-
root: Root;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function buildDecorations(
|
|
148
|
-
doc: PMNode,
|
|
149
|
-
entries: AsciiDiagramBlockEntry[],
|
|
150
|
-
sourceVisible: ReadonlySet<string>,
|
|
151
|
-
editor: Editor,
|
|
152
|
-
textChannel?: SceneTextChannel,
|
|
153
|
-
): DecorationSet {
|
|
154
|
-
const decos: Decoration[] = [];
|
|
155
|
-
for (const entry of entries) {
|
|
156
|
-
const node = doc.nodeAt(entry.pos);
|
|
157
|
-
if (!node || node.type.name !== 'codeBlock') continue;
|
|
158
|
-
const cls = sourceVisible.has(entry.id)
|
|
159
|
-
? 'squisq-ascii-fence-source'
|
|
160
|
-
: 'squisq-ascii-fence-hidden';
|
|
161
|
-
decos.push(Decoration.node(entry.pos, entry.pos + node.nodeSize, { class: cls }));
|
|
162
|
-
const blockId = entry.id;
|
|
163
|
-
const fallbackPos = entry.pos;
|
|
164
|
-
decos.push(
|
|
165
|
-
Decoration.widget(
|
|
166
|
-
entry.pos + node.nodeSize,
|
|
167
|
-
(view) => {
|
|
168
|
-
const container = document.createElement('div');
|
|
169
|
-
container.className = 'squisq-ascii-diagram-widget-host';
|
|
170
|
-
container.contentEditable = 'false';
|
|
171
|
-
// Keep ProseMirror from treating clicks/keys inside the widget
|
|
172
|
-
// as document interactions.
|
|
173
|
-
container.addEventListener('mousedown', (e) => e.stopPropagation());
|
|
174
|
-
container.addEventListener('keydown', (e) => e.stopPropagation());
|
|
175
|
-
const root = createRoot(container);
|
|
176
|
-
root.render(
|
|
177
|
-
createElement(AsciiDiagramWidget, {
|
|
178
|
-
editor,
|
|
179
|
-
blockId,
|
|
180
|
-
fallbackPos,
|
|
181
|
-
host: view.dom.parentElement ?? view.dom,
|
|
182
|
-
textChannel,
|
|
183
|
-
}),
|
|
184
|
-
);
|
|
185
|
-
(container as HTMLElement & { __squisqAsciiRoot?: WidgetRootRef }).__squisqAsciiRoot = {
|
|
186
|
-
root,
|
|
187
|
-
};
|
|
188
|
-
return container;
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
destroy: (dom) => {
|
|
192
|
-
const ref = (dom as HTMLElement & { __squisqAsciiRoot?: WidgetRootRef })
|
|
193
|
-
.__squisqAsciiRoot;
|
|
194
|
-
// Defer so React isn't unmounted mid-render.
|
|
195
|
-
if (ref) setTimeout(() => ref.root.unmount(), 0);
|
|
196
|
-
},
|
|
197
|
-
side: 1,
|
|
198
|
-
ignoreSelection: true,
|
|
199
|
-
key: `squisq-ascii-${entry.id}`,
|
|
200
|
-
},
|
|
201
|
-
),
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
return DecorationSet.create(doc, decos);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function applyState(
|
|
208
|
-
tr: Transaction,
|
|
209
|
-
prev: AsciiDiagramPluginState,
|
|
210
|
-
editor: Editor,
|
|
211
|
-
doc: PMNode,
|
|
212
|
-
textChannel?: SceneTextChannel,
|
|
213
|
-
): AsciiDiagramPluginState {
|
|
214
|
-
const meta = tr.getMeta(ASCII_DIAGRAM_KEY) as { toggleSource?: string } | undefined;
|
|
215
|
-
let sourceVisible = prev.sourceVisible;
|
|
216
|
-
if (meta?.toggleSource) {
|
|
217
|
-
const next = new Set(sourceVisible);
|
|
218
|
-
if (next.has(meta.toggleSource)) next.delete(meta.toggleSource);
|
|
219
|
-
else next.add(meta.toggleSource);
|
|
220
|
-
sourceVisible = next;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!tr.docChanged) {
|
|
224
|
-
if (!meta?.toggleSource) return prev;
|
|
225
|
-
return {
|
|
226
|
-
...prev,
|
|
227
|
-
sourceVisible,
|
|
228
|
-
decorations: buildDecorations(doc, prev.entries, sourceVisible, editor, textChannel),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Doc changed: remap known positions (bias 1 so an insertion exactly at
|
|
233
|
-
// the block's start shifts the tracked position forward WITH the node),
|
|
234
|
-
// then walk the new doc and reconcile.
|
|
235
|
-
const mapped = new Map<number, string>();
|
|
236
|
-
const claimed = new Set<string>();
|
|
237
|
-
// Pass 1: survivors whose start position wasn't touched.
|
|
238
|
-
for (const entry of prev.entries) {
|
|
239
|
-
const result = tr.mapping.mapResult(entry.pos, 1);
|
|
240
|
-
if (!result.deleted) {
|
|
241
|
-
mapped.set(result.pos, entry.id);
|
|
242
|
-
claimed.add(entry.id);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
// Pass 2: boundary-churn survivors. An attrs-only `setNodeMarkup` (the
|
|
246
|
-
// language-promotion step) rewrites the codeBlock's opening token, so
|
|
247
|
-
// `mapResult` reports the start as `deleted` even though the block is still
|
|
248
|
-
// there. Adopt the old id when a codeBlock still sits at the mapped position
|
|
249
|
-
// and that slot isn't already claimed — this keeps the widget's React root
|
|
250
|
-
// (pan/zoom) alive across the one-time promotion, instead of remounting.
|
|
251
|
-
for (const entry of prev.entries) {
|
|
252
|
-
if (claimed.has(entry.id)) continue;
|
|
253
|
-
const result = tr.mapping.mapResult(entry.pos, 1);
|
|
254
|
-
if (mapped.has(result.pos)) continue;
|
|
255
|
-
if (doc.nodeAt(result.pos)?.type.name === 'codeBlock') {
|
|
256
|
-
mapped.set(result.pos, entry.id);
|
|
257
|
-
claimed.add(entry.id);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
let seq = prev.seq;
|
|
262
|
-
const entries: AsciiDiagramBlockEntry[] = [];
|
|
263
|
-
doc.descendants((node, pos) => {
|
|
264
|
-
if (node.type.name !== 'codeBlock') return;
|
|
265
|
-
const knownId = mapped.get(pos);
|
|
266
|
-
// Hysteresis: known blocks stay while they parse at all; new blocks
|
|
267
|
-
// must pass full detection.
|
|
268
|
-
const diagram = knownId ? parseAsciiDiagramForNode(node) : getAsciiDiagramForNode(node);
|
|
269
|
-
if (!diagram) return false;
|
|
270
|
-
entries.push({ id: knownId ?? `ascii-${++seq}`, pos });
|
|
271
|
-
return false;
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const liveIds = new Set(entries.map((e) => e.id));
|
|
275
|
-
const prunedSourceVisible = new Set([...sourceVisible].filter((id) => liveIds.has(id)));
|
|
276
|
-
|
|
277
|
-
return {
|
|
278
|
-
entries,
|
|
279
|
-
seq,
|
|
280
|
-
sourceVisible: prunedSourceVisible,
|
|
281
|
-
decorations: buildDecorations(doc, entries, prunedSourceVisible, editor, textChannel),
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export interface AsciiDiagramExtensionOptions {
|
|
286
|
-
/** When false, the extension is inert (no widgets, no decorations). */
|
|
287
|
-
enabled?: boolean;
|
|
288
|
-
textChannel?: SceneTextChannel;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export const AsciiDiagramExtension = Extension.create<AsciiDiagramExtensionOptions>({
|
|
292
|
-
name: 'squisqAsciiDiagram',
|
|
293
|
-
|
|
294
|
-
addOptions() {
|
|
295
|
-
return {
|
|
296
|
-
enabled: true,
|
|
297
|
-
};
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
addProseMirrorPlugins() {
|
|
301
|
-
const editor = this.editor as Editor;
|
|
302
|
-
if (this.options.enabled === false) return [];
|
|
303
|
-
|
|
304
|
-
return [
|
|
305
|
-
new Plugin<AsciiDiagramPluginState>({
|
|
306
|
-
key: ASCII_DIAGRAM_KEY,
|
|
307
|
-
state: {
|
|
308
|
-
init: (_config, state) => {
|
|
309
|
-
let seq = 0;
|
|
310
|
-
const entries: AsciiDiagramBlockEntry[] = [];
|
|
311
|
-
state.doc.descendants((node, pos) => {
|
|
312
|
-
if (node.type.name !== 'codeBlock') return;
|
|
313
|
-
if (!getAsciiDiagramForNode(node)) return false;
|
|
314
|
-
entries.push({ id: `ascii-${++seq}`, pos });
|
|
315
|
-
return false;
|
|
316
|
-
});
|
|
317
|
-
return {
|
|
318
|
-
entries,
|
|
319
|
-
seq,
|
|
320
|
-
sourceVisible: new Set<string>(),
|
|
321
|
-
decorations: buildDecorations(
|
|
322
|
-
state.doc,
|
|
323
|
-
entries,
|
|
324
|
-
new Set(),
|
|
325
|
-
editor,
|
|
326
|
-
this.options.textChannel,
|
|
327
|
-
),
|
|
328
|
-
};
|
|
329
|
-
},
|
|
330
|
-
apply: (tr, prev, _oldState, newState) =>
|
|
331
|
-
applyState(tr, prev, editor, newState.doc, this.options.textChannel),
|
|
332
|
-
},
|
|
333
|
-
props: {
|
|
334
|
-
decorations(state) {
|
|
335
|
-
return this.getState(state)?.decorations ?? DecorationSet.empty;
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
}),
|
|
339
|
-
];
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
export default AsciiDiagramExtension;
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AsciiDiagramWidget — the interactive canvas mounted below a hidden
|
|
3
|
-
* ASCII-diagram code fence in the WYSIWYG view.
|
|
4
|
-
*
|
|
5
|
-
* Mirrors the legacy heading-based DiagramWidget's chrome (shared toolbar,
|
|
6
|
-
* maximize overlay, height handle) but reads/writes the FENCE: data comes
|
|
7
|
-
* from `useAsciiDiagramData` (live re-parse), edits go through
|
|
8
|
-
* `applyAsciiDiagramCommand` (op → render → verify → single-transaction
|
|
9
|
-
* text replace).
|
|
10
|
-
*
|
|
11
|
-
* The height handle is session-local by design: a fence has no annotation
|
|
12
|
-
* surface to persist a `height=` on, and writing chrome state into the art
|
|
13
|
-
* would violate "the fence is the diagram".
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
17
|
-
import type { Editor } from '@tiptap/react';
|
|
18
|
-
import { DiagramCanvas, type DiagramCommand } from '../diagram/DiagramCanvas';
|
|
19
|
-
import { DIAGRAM_TOOLS, DIAGRAM_VIEWPORT } from '../diagram/diagramConstants';
|
|
20
|
-
import { DiagramMaximizedOverlay } from '../diagram/DiagramMaximizedOverlay';
|
|
21
|
-
import { SceneBlockToolbar, type SceneBlockAction } from '../scene/SceneBlockToolbar';
|
|
22
|
-
import { SceneSideToolbar } from '../scene/SceneSideToolbar';
|
|
23
|
-
import { nodeIdFromCardLayerId, NODE_WIDTH, NODE_HEIGHT } from '../scene';
|
|
24
|
-
import { Icon } from '../Icon';
|
|
25
|
-
import {
|
|
26
|
-
asciiDiagramToCanvas,
|
|
27
|
-
initialAsciiDiagramCanvasOffset,
|
|
28
|
-
offsetAsciiDiagram,
|
|
29
|
-
useAsciiDiagramData,
|
|
30
|
-
type AsciiDiagramCanvasOffset,
|
|
31
|
-
} from './asciiDiagramData';
|
|
32
|
-
import { applyAsciiDiagramCommand } from './asciiDiagramCommands';
|
|
33
|
-
import { isAsciiSourceVisible, toggleAsciiSource } from './AsciiDiagramExtension';
|
|
34
|
-
import type { SceneTextChannel } from '../scene/text/sceneTextChannel';
|
|
35
|
-
|
|
36
|
-
/** Smallest height the canvas can be dragged to (px). */
|
|
37
|
-
const MIN_DIAGRAM_HEIGHT = 160;
|
|
38
|
-
const DEFAULT_DIAGRAM_HEIGHT = 420;
|
|
39
|
-
|
|
40
|
-
interface AsciiDiagramWidgetProps {
|
|
41
|
-
editor: Editor;
|
|
42
|
-
/** Session id from the extension's position registry. */
|
|
43
|
-
blockId: string;
|
|
44
|
-
/** Position at widget-creation time — display-only fallback. */
|
|
45
|
-
fallbackPos: number;
|
|
46
|
-
/** Host element used for portal targeting by the maximize overlay. */
|
|
47
|
-
host?: HTMLElement | null;
|
|
48
|
-
textChannel?: SceneTextChannel;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function AsciiDiagramWidget({
|
|
52
|
-
editor,
|
|
53
|
-
blockId,
|
|
54
|
-
host,
|
|
55
|
-
textChannel,
|
|
56
|
-
}: AsciiDiagramWidgetProps) {
|
|
57
|
-
const view = useAsciiDiagramData(editor, blockId);
|
|
58
|
-
const [maximized, setMaximized] = useState(false);
|
|
59
|
-
const [height, setHeight] = useState<number | null>(null);
|
|
60
|
-
const [dragHeight, setDragHeight] = useState<number | null>(null);
|
|
61
|
-
const canvasOffsetRef = useRef<AsciiDiagramCanvasOffset | null>(null);
|
|
62
|
-
const [canvasOffsetVersion, setCanvasOffsetVersion] = useState(0);
|
|
63
|
-
const inlineRef = useRef<HTMLDivElement>(null);
|
|
64
|
-
const effectiveHeight = dragHeight ?? height;
|
|
65
|
-
|
|
66
|
-
if (view && canvasOffsetRef.current === null) {
|
|
67
|
-
canvasOffsetRef.current = initialAsciiDiagramCanvasOffset(view.diagram);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const canvasView = useMemo(() => {
|
|
71
|
-
if (!view) return null;
|
|
72
|
-
const offset = canvasOffsetRef.current ?? { col: 0, row: 0 };
|
|
73
|
-
return asciiDiagramToCanvas(offsetAsciiDiagram(view.diagram, offset));
|
|
74
|
-
// The ref is intentionally versioned only when its value is consumed.
|
|
75
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
-
}, [view, canvasOffsetVersion]);
|
|
77
|
-
|
|
78
|
-
const dispatch = useCallback(
|
|
79
|
-
(cmd: DiagramCommand) => {
|
|
80
|
-
const offset = canvasOffsetRef.current ?? { col: 0, row: 0 };
|
|
81
|
-
const applied = applyAsciiDiagramCommand(editor, blockId, cmd, {
|
|
82
|
-
diagramOffset: offset,
|
|
83
|
-
});
|
|
84
|
-
if (applied && (offset.col !== 0 || offset.row !== 0)) {
|
|
85
|
-
// The same rewrite that applied the edit has now persisted the virtual
|
|
86
|
-
// gutter. Drop the projection offset so the on-screen coordinates do
|
|
87
|
-
// not change while the fence-backed view refreshes.
|
|
88
|
-
canvasOffsetRef.current = { col: 0, row: 0 };
|
|
89
|
-
setCanvasOffsetVersion((version) => version + 1);
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
[editor, blockId],
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const onResizeStart = useCallback(
|
|
96
|
-
(e: React.PointerEvent) => {
|
|
97
|
-
e.preventDefault();
|
|
98
|
-
e.stopPropagation();
|
|
99
|
-
const startY = e.clientY;
|
|
100
|
-
const startHeight =
|
|
101
|
-
inlineRef.current?.getBoundingClientRect().height ??
|
|
102
|
-
effectiveHeight ??
|
|
103
|
-
DEFAULT_DIAGRAM_HEIGHT;
|
|
104
|
-
const heightAt = (clientY: number) =>
|
|
105
|
-
Math.max(MIN_DIAGRAM_HEIGHT, Math.round(startHeight + (clientY - startY)));
|
|
106
|
-
const prevCursor = document.body.style.cursor;
|
|
107
|
-
const prevSelect = document.body.style.userSelect;
|
|
108
|
-
document.body.style.cursor = 'ns-resize';
|
|
109
|
-
document.body.style.userSelect = 'none';
|
|
110
|
-
const onMove = (ev: PointerEvent) => setDragHeight(heightAt(ev.clientY));
|
|
111
|
-
const onUp = (ev: PointerEvent) => {
|
|
112
|
-
window.removeEventListener('pointermove', onMove);
|
|
113
|
-
window.removeEventListener('pointerup', onUp);
|
|
114
|
-
document.body.style.cursor = prevCursor;
|
|
115
|
-
document.body.style.userSelect = prevSelect;
|
|
116
|
-
setDragHeight(null);
|
|
117
|
-
setHeight(heightAt(ev.clientY));
|
|
118
|
-
};
|
|
119
|
-
window.addEventListener('pointermove', onMove);
|
|
120
|
-
window.addEventListener('pointerup', onUp);
|
|
121
|
-
},
|
|
122
|
-
[effectiveHeight],
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const onResizeReset = useCallback(() => {
|
|
126
|
-
setDragHeight(null);
|
|
127
|
-
setHeight(null);
|
|
128
|
-
}, []);
|
|
129
|
-
|
|
130
|
-
// ── Shared toolbar (above the canvas) ────────────────────
|
|
131
|
-
const [activeToolId, setActiveToolId] = useState<string>('select');
|
|
132
|
-
const [selectedIds, setSelectedIds] = useState<ReadonlySet<string>>(() => new Set());
|
|
133
|
-
const selectedNodeIds = useMemo(
|
|
134
|
-
() =>
|
|
135
|
-
[...selectedIds]
|
|
136
|
-
.map((id) => nodeIdFromCardLayerId(id))
|
|
137
|
-
.filter((id): id is string => id != null),
|
|
138
|
-
[selectedIds],
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const sourceVisible = isAsciiSourceVisible(editor, blockId);
|
|
142
|
-
|
|
143
|
-
if (!view) return null;
|
|
144
|
-
|
|
145
|
-
const actions: SceneBlockAction[] = [
|
|
146
|
-
{
|
|
147
|
-
id: 'add-node',
|
|
148
|
-
label: 'Node',
|
|
149
|
-
icon: <Icon icon="fa-solid fa-plus" />,
|
|
150
|
-
title: 'Add node',
|
|
151
|
-
onClick: () =>
|
|
152
|
-
dispatch({
|
|
153
|
-
kind: 'addNode',
|
|
154
|
-
x: DIAGRAM_VIEWPORT.width / 2 - NODE_WIDTH / 2,
|
|
155
|
-
y: DIAGRAM_VIEWPORT.height / 2 - NODE_HEIGHT / 2,
|
|
156
|
-
}),
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
id: 'delete',
|
|
160
|
-
label: 'Delete',
|
|
161
|
-
icon: <Icon icon="fa-solid fa-trash" />,
|
|
162
|
-
title: 'Delete selected node',
|
|
163
|
-
danger: true,
|
|
164
|
-
disabled: selectedNodeIds.length === 0,
|
|
165
|
-
onClick: () => selectedNodeIds.forEach((id) => dispatch({ kind: 'removeNode', nodeId: id })),
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
id: 'ascii-source',
|
|
169
|
-
label: 'Source',
|
|
170
|
-
icon: <Icon icon="fa-solid fa-code" />,
|
|
171
|
-
title: sourceVisible ? 'Hide ASCII source' : 'Show ASCII source',
|
|
172
|
-
onClick: () => toggleAsciiSource(editor, blockId),
|
|
173
|
-
},
|
|
174
|
-
];
|
|
175
|
-
|
|
176
|
-
const toolbar = (
|
|
177
|
-
<SceneBlockToolbar
|
|
178
|
-
tools={DIAGRAM_TOOLS}
|
|
179
|
-
activeToolId={activeToolId}
|
|
180
|
-
onSelectTool={setActiveToolId}
|
|
181
|
-
actions={actions}
|
|
182
|
-
/>
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
const warnings =
|
|
186
|
-
view.warnings.length > 0 ? (
|
|
187
|
-
<details className="squisq-ascii-diagram-warnings">
|
|
188
|
-
<summary>
|
|
189
|
-
{view.warnings.length} parser note{view.warnings.length === 1 ? '' : 's'}
|
|
190
|
-
</summary>
|
|
191
|
-
<ul>
|
|
192
|
-
{view.warnings.map((w, i) => (
|
|
193
|
-
<li key={i}>{w}</li>
|
|
194
|
-
))}
|
|
195
|
-
</ul>
|
|
196
|
-
</details>
|
|
197
|
-
) : null;
|
|
198
|
-
|
|
199
|
-
const canvas = (
|
|
200
|
-
<DiagramCanvas
|
|
201
|
-
textChannel={textChannel}
|
|
202
|
-
nodes={canvasView?.nodes ?? view.nodes}
|
|
203
|
-
edges={canvasView?.edges ?? view.edges}
|
|
204
|
-
onCommand={dispatch}
|
|
205
|
-
showMaximize
|
|
206
|
-
maximized={maximized}
|
|
207
|
-
onToggleMaximize={() => setMaximized((m) => !m)}
|
|
208
|
-
activeToolId={activeToolId}
|
|
209
|
-
onActiveToolIdChange={setActiveToolId}
|
|
210
|
-
onSelectionChange={setSelectedIds}
|
|
211
|
-
/>
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
if (maximized) {
|
|
215
|
-
return (
|
|
216
|
-
<div
|
|
217
|
-
className="squisq-diagram-inline-placeholder"
|
|
218
|
-
style={effectiveHeight != null ? { height: effectiveHeight } : undefined}
|
|
219
|
-
>
|
|
220
|
-
<DiagramMaximizedOverlay host={host ?? null} onClose={() => setMaximized(false)}>
|
|
221
|
-
<div className="squisq-scene-block-max">
|
|
222
|
-
{canvas}
|
|
223
|
-
<div className="squisq-scene-side-toolbar">{toolbar}</div>
|
|
224
|
-
</div>
|
|
225
|
-
</DiagramMaximizedOverlay>
|
|
226
|
-
</div>
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<div className="squisq-scene-shell">
|
|
232
|
-
<SceneSideToolbar>{toolbar}</SceneSideToolbar>
|
|
233
|
-
<div
|
|
234
|
-
className="squisq-diagram-inline"
|
|
235
|
-
ref={inlineRef}
|
|
236
|
-
style={effectiveHeight != null ? { height: effectiveHeight } : undefined}
|
|
237
|
-
>
|
|
238
|
-
{canvas}
|
|
239
|
-
<div
|
|
240
|
-
className="squisq-diagram-resize-handle"
|
|
241
|
-
onPointerDown={onResizeStart}
|
|
242
|
-
onDoubleClick={onResizeReset}
|
|
243
|
-
role="separator"
|
|
244
|
-
aria-orientation="horizontal"
|
|
245
|
-
aria-label="Resize diagram height"
|
|
246
|
-
title="Drag to resize · double-click to reset"
|
|
247
|
-
/>
|
|
248
|
-
</div>
|
|
249
|
-
{warnings}
|
|
250
|
-
</div>
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export default AsciiDiagramWidget;
|