@bendyline/squisq-editor-react 1.6.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/dist/index.d.ts +1275 -183
- package/dist/index.js +20345 -12777
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +1607 -241
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +39 -33
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +349 -261
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +107 -14
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +362 -89
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/RecorderEntry.tsx +2 -0
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +12 -3
- package/src/TemplatePicker.tsx +75 -21
- package/src/ThemeCustomizerPanel.tsx +8 -0
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +357 -51
- package/src/TransitionPicker.tsx +36 -4
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/WysiwygEditor.tsx +124 -91
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/codeContextSectionView.test.tsx +8 -6
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +51 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +123 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +0 -12
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +15 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +382 -5
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/recorderTheme.test.tsx +42 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/selectionConversions.test.ts +80 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +71 -7
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
- package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +254 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +284 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
- package/src/asciiDiagram/asciiDiagramData.ts +149 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +238 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/codeContext/types.ts +1 -1
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +25 -6
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/frontmatterSettings.ts +23 -0
- package/src/headingTransition.ts +0 -15
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +198 -20
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/RecorderButton.tsx +9 -1
- package/src/recorder/RecorderModal.tsx +84 -41
- package/src/recorder/RecorderPanel.tsx +9 -1
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +48 -18
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +5 -1
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +119 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/adapters/DrawingAdapter.ts +6 -1
- package/src/scene/adapters/LayoutAdapter.ts +3 -0
- package/src/scene/commands/SceneCommand.ts +17 -3
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +21 -7
- package/src/scene/layers/edgeGeometry.ts +9 -1
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +5 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +13 -4
- package/src/scene/tools/SceneTool.ts +8 -0
- package/src/scene/tools/SelectTool.ts +39 -23
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/selectionConversions.ts +155 -0
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +596 -0
- package/src/styles/editor.css +736 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +189 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +12 -5
- package/src/timeline/TimelineEditorWidget.tsx +861 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
- package/src/timeline/__tests__/timelineOps.test.ts +277 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +421 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +381 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +47 -21
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +390 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +177 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
- package/src/treeview/treeOps.ts +231 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +99 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -272
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DiagramExtension — Tiptap/ProseMirror plugin that:
|
|
3
|
-
*
|
|
4
|
-
* 1. Mounts a React-Flow canvas (`DiagramWidget`) immediately after every
|
|
5
|
-
* heading whose `dataTemplate === 'diagram'`.
|
|
6
|
-
* 2. Hides the direct sub-headings of each diagram parent (until the next
|
|
7
|
-
* equal-or-shallower heading) by tagging them with a `data-squisq-diagram-child`
|
|
8
|
-
* attribute — CSS in `styles/diagram.css` does the actual hiding.
|
|
9
|
-
*
|
|
10
|
-
* Widgets are rendered as plain DOM nodes attached to a ProseMirror
|
|
11
|
-
* `Decoration.widget`. React is mounted into the widget DOM with
|
|
12
|
-
* `react-dom/client`'s `createRoot`, and unmounted on the widget's
|
|
13
|
-
* `destroy` hook.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { Extension } from '@tiptap/core';
|
|
17
|
-
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
18
|
-
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
19
|
-
import type { EditorState } from '@tiptap/pm/state';
|
|
20
|
-
import type { Editor } from '@tiptap/react';
|
|
21
|
-
import { createRoot, type Root } from 'react-dom/client';
|
|
22
|
-
import { createElement } from 'react';
|
|
23
|
-
import type { Node as PMNode } from '@tiptap/pm/model';
|
|
24
|
-
import { parsePandocAttrTokens } from '@bendyline/squisq/markdown';
|
|
25
|
-
import { DiagramWidget } from './DiagramWidget';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Stable identifier for a diagram heading — prefers the persisted
|
|
29
|
-
* Pandoc `#id`, falls back to a slugified version of the heading's
|
|
30
|
-
* text. Used as part of the widget decoration's `key` so the widget
|
|
31
|
-
* survives attribute-only doc changes (drag/resize commits).
|
|
32
|
-
*/
|
|
33
|
-
export function getHeadingKey(node: PMNode): string {
|
|
34
|
-
const raw = (node.attrs as Record<string, unknown>).dataBlockAttrs;
|
|
35
|
-
if (typeof raw === 'string' && raw.length > 0) {
|
|
36
|
-
const attrs = parsePandocAttrTokens(raw);
|
|
37
|
-
if (attrs.id) return attrs.id;
|
|
38
|
-
}
|
|
39
|
-
let text = '';
|
|
40
|
-
node.content.forEach((child) => {
|
|
41
|
-
if (child.isText) text += child.text ?? '';
|
|
42
|
-
});
|
|
43
|
-
return (
|
|
44
|
-
text
|
|
45
|
-
.toLowerCase()
|
|
46
|
-
.replace(/[^\w\s-]/g, '')
|
|
47
|
-
.replace(/[\s_]+/g, '-')
|
|
48
|
-
.replace(/-+/g, '-')
|
|
49
|
-
.replace(/^-|-$/g, '') || 'block'
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Find the current document position of a diagram heading by its
|
|
55
|
-
* stable key. Returns `null` if the heading no longer exists (e.g. the
|
|
56
|
-
* user removed it). Used by the widget to re-resolve its parent
|
|
57
|
-
* position on every read, since the kept-alive widget's captured
|
|
58
|
-
* `parentPos` would otherwise go stale when content above shifts.
|
|
59
|
-
*/
|
|
60
|
-
export function findDiagramHeadingPos(editor: Editor, headingKey: string): number | null {
|
|
61
|
-
let found: number | null = null;
|
|
62
|
-
editor.state.doc.descendants((node, pos) => {
|
|
63
|
-
if (found != null) return false;
|
|
64
|
-
if (node.type.name !== 'heading') return;
|
|
65
|
-
const attrs = node.attrs as { dataTemplate?: string };
|
|
66
|
-
if (attrs.dataTemplate !== 'diagram') return;
|
|
67
|
-
if (getHeadingKey(node) === headingKey) {
|
|
68
|
-
found = pos;
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
return found;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const KEY = new PluginKey('squisq-diagram');
|
|
76
|
-
|
|
77
|
-
interface DiagramRoot {
|
|
78
|
-
el: HTMLElement;
|
|
79
|
-
root: Root;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function buildDecorations(state: EditorState, editor: Editor): DecorationSet {
|
|
83
|
-
const decos: Decoration[] = [];
|
|
84
|
-
|
|
85
|
-
state.doc.descendants((node, pos) => {
|
|
86
|
-
if (node.type.name !== 'heading') return;
|
|
87
|
-
const attrs = node.attrs as { level: number; dataTemplate?: string };
|
|
88
|
-
if (attrs.dataTemplate === 'diagram') {
|
|
89
|
-
const parentDepth = attrs.level;
|
|
90
|
-
const widgetPos = pos + node.nodeSize;
|
|
91
|
-
const parentPos = pos;
|
|
92
|
-
// Stable widget key derived from the heading's persisted id (or its
|
|
93
|
-
// text-derived slug). ProseMirror treats decorations with matching
|
|
94
|
-
// keys as equal, so it keeps the existing widget DOM (and the
|
|
95
|
-
// mounted React root) across attribute-only doc changes — which
|
|
96
|
-
// happen on every drag commit when we write `x=`/`y=` back to the
|
|
97
|
-
// heading. Without this, every drop would destroy and recreate the
|
|
98
|
-
// canvas, blowing away pan/zoom and making the cards appear to snap
|
|
99
|
-
// back to their pre-fit position.
|
|
100
|
-
const headingKey = getHeadingKey(node);
|
|
101
|
-
decos.push(
|
|
102
|
-
Decoration.widget(
|
|
103
|
-
widgetPos,
|
|
104
|
-
(view) => {
|
|
105
|
-
const container = document.createElement('div');
|
|
106
|
-
container.className = 'squisq-diagram-widget-host';
|
|
107
|
-
container.contentEditable = 'false';
|
|
108
|
-
// Stop ProseMirror from treating clicks/keys inside the
|
|
109
|
-
// widget as document interactions.
|
|
110
|
-
container.addEventListener('mousedown', (e) => e.stopPropagation());
|
|
111
|
-
container.addEventListener('keydown', (e) => e.stopPropagation());
|
|
112
|
-
|
|
113
|
-
const root = createRoot(container);
|
|
114
|
-
root.render(
|
|
115
|
-
createElement(DiagramWidget, {
|
|
116
|
-
editor,
|
|
117
|
-
headingKey,
|
|
118
|
-
fallbackParentPos: parentPos,
|
|
119
|
-
host: view.dom.parentElement ?? view.dom,
|
|
120
|
-
}),
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
// Stash for destroy.
|
|
124
|
-
(container as HTMLElement & { __squisqDiagramRoot?: DiagramRoot }).__squisqDiagramRoot =
|
|
125
|
-
{
|
|
126
|
-
el: container,
|
|
127
|
-
root,
|
|
128
|
-
};
|
|
129
|
-
return container;
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
destroy: (dom) => {
|
|
133
|
-
const ref = (dom as HTMLElement & { __squisqDiagramRoot?: DiagramRoot })
|
|
134
|
-
.__squisqDiagramRoot;
|
|
135
|
-
if (ref) {
|
|
136
|
-
// Defer to next tick so React isn't unmounted mid-render.
|
|
137
|
-
setTimeout(() => ref.root.unmount(), 0);
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
// Cluster the widget tightly after the heading; don't side-bias.
|
|
141
|
-
side: 1,
|
|
142
|
-
ignoreSelection: true,
|
|
143
|
-
key: `squisq-diagram-${headingKey}`,
|
|
144
|
-
},
|
|
145
|
-
),
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
// Mark child headings (depth = parentDepth + 1 .. 6, until next ≤parentDepth).
|
|
149
|
-
let cursor = pos + node.nodeSize;
|
|
150
|
-
while (cursor < state.doc.content.size) {
|
|
151
|
-
const child = state.doc.nodeAt(cursor);
|
|
152
|
-
if (!child) break;
|
|
153
|
-
if (child.type.name === 'heading') {
|
|
154
|
-
const childLevel = (child.attrs as { level: number }).level;
|
|
155
|
-
if (childLevel <= parentDepth) break;
|
|
156
|
-
decos.push(
|
|
157
|
-
Decoration.node(cursor, cursor + child.nodeSize, {
|
|
158
|
-
'data-squisq-diagram-child': 'true',
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
cursor += child.nodeSize;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
return DecorationSet.create(state.doc, decos);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export interface DiagramExtensionOptions {
|
|
171
|
-
/** When false, the extension is inert (no widgets, no decorations). */
|
|
172
|
-
enabled?: boolean;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export const DiagramExtension = Extension.create<DiagramExtensionOptions>({
|
|
176
|
-
name: 'squisqDiagram',
|
|
177
|
-
|
|
178
|
-
addOptions() {
|
|
179
|
-
return {
|
|
180
|
-
enabled: true,
|
|
181
|
-
};
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
addProseMirrorPlugins() {
|
|
185
|
-
const editor = this.editor as Editor;
|
|
186
|
-
const enabled = this.options.enabled !== false;
|
|
187
|
-
if (!enabled) return [];
|
|
188
|
-
|
|
189
|
-
return [
|
|
190
|
-
new Plugin({
|
|
191
|
-
key: KEY,
|
|
192
|
-
state: {
|
|
193
|
-
init: (_config, state) => buildDecorations(state, editor),
|
|
194
|
-
apply: (tr, oldDecos, _oldState, newState) => {
|
|
195
|
-
if (!tr.docChanged) return oldDecos;
|
|
196
|
-
return buildDecorations(newState, editor);
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
props: {
|
|
200
|
-
decorations(state) {
|
|
201
|
-
return this.getState(state);
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
}),
|
|
205
|
-
];
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
export default DiagramExtension;
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DiagramWidget — the Squisq-Scene-backed canvas that sits below a
|
|
3
|
-
* `### Title {[diagram]}` heading in the WYSIWYG view.
|
|
4
|
-
*
|
|
5
|
-
* Owns the maximize toggle. Resolves the parent heading's position
|
|
6
|
-
* dynamically each render via `headingKey`, so the widget can survive
|
|
7
|
-
* attribute-only doc changes (drag commits) without going stale when
|
|
8
|
-
* content above the diagram shifts.
|
|
9
|
-
*
|
|
10
|
-
* Pushes user actions back through the `diagramCommands` helpers.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
-
import type { Editor } from '@tiptap/react';
|
|
15
|
-
import { DiagramCanvas, type DiagramCommand } from './DiagramCanvas';
|
|
16
|
-
import { DIAGRAM_TOOLS, DIAGRAM_VIEWPORT } from './diagramConstants';
|
|
17
|
-
import { DiagramMaximizedOverlay } from './DiagramMaximizedOverlay';
|
|
18
|
-
import { useDiagramData } from './useDiagramData';
|
|
19
|
-
import { findDiagramHeadingPos } from './DiagramExtension';
|
|
20
|
-
import { SceneBlockToolbar, type SceneBlockAction } from '../scene/SceneBlockToolbar';
|
|
21
|
-
import { SceneSideToolbar } from '../scene/SceneSideToolbar';
|
|
22
|
-
import { nodeIdFromCardLayerId, NODE_WIDTH, NODE_HEIGHT } from '../scene';
|
|
23
|
-
import { Icon } from '../Icon';
|
|
24
|
-
import {
|
|
25
|
-
moveNode,
|
|
26
|
-
resizeNode,
|
|
27
|
-
addConnection,
|
|
28
|
-
removeConnection,
|
|
29
|
-
renameNode,
|
|
30
|
-
addNode,
|
|
31
|
-
removeNode,
|
|
32
|
-
getDiagramHeight,
|
|
33
|
-
setDiagramHeight,
|
|
34
|
-
} from './diagramCommands';
|
|
35
|
-
|
|
36
|
-
/** Smallest height the canvas can be dragged to (px). */
|
|
37
|
-
const MIN_DIAGRAM_HEIGHT = 160;
|
|
38
|
-
/** Default canvas height when none is pinned — must match `.squisq-diagram-inline` in diagram.css. */
|
|
39
|
-
const DEFAULT_DIAGRAM_HEIGHT = 420;
|
|
40
|
-
|
|
41
|
-
interface DiagramWidgetProps {
|
|
42
|
-
editor: Editor;
|
|
43
|
-
/** Stable id derived from the parent heading (slug / `#id`). */
|
|
44
|
-
headingKey: string;
|
|
45
|
-
/** Position of the parent heading at widget-creation time. Used as a
|
|
46
|
-
* fallback when the dynamic lookup fails (e.g. before the first
|
|
47
|
-
* transaction). */
|
|
48
|
-
fallbackParentPos: number;
|
|
49
|
-
/** Host element used for portal targeting by the maximize overlay. */
|
|
50
|
-
host?: HTMLElement | null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function DiagramWidget({ editor, headingKey, fallbackParentPos, host }: DiagramWidgetProps) {
|
|
54
|
-
// Re-resolve parentPos on every editor transaction so writes target
|
|
55
|
-
// the current heading position even after content above shifts. The same
|
|
56
|
-
// pass refreshes the pinned canvas height from the heading annotation so
|
|
57
|
-
// an external edit (or markdown reload) is reflected.
|
|
58
|
-
const [parentPos, setParentPos] = useState<number>(
|
|
59
|
-
() => findDiagramHeadingPos(editor, headingKey) ?? fallbackParentPos,
|
|
60
|
-
);
|
|
61
|
-
const [pinnedHeight, setPinnedHeight] = useState<number | null>(() =>
|
|
62
|
-
getDiagramHeight(editor, findDiagramHeadingPos(editor, headingKey) ?? fallbackParentPos),
|
|
63
|
-
);
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const onUpdate = () => {
|
|
66
|
-
const next = findDiagramHeadingPos(editor, headingKey);
|
|
67
|
-
if (next != null && next !== parentPos) setParentPos(next);
|
|
68
|
-
setPinnedHeight(getDiagramHeight(editor, next ?? parentPos));
|
|
69
|
-
};
|
|
70
|
-
editor.on('transaction', onUpdate);
|
|
71
|
-
return () => {
|
|
72
|
-
editor.off('transaction', onUpdate);
|
|
73
|
-
};
|
|
74
|
-
}, [editor, headingKey, parentPos]);
|
|
75
|
-
|
|
76
|
-
const { nodes, edges } = useDiagramData(editor, parentPos);
|
|
77
|
-
const [maximized, setMaximized] = useState(false);
|
|
78
|
-
|
|
79
|
-
// Live height while the bottom handle is being dragged (null = not
|
|
80
|
-
// dragging). Takes precedence over the persisted height so the canvas
|
|
81
|
-
// tracks the pointer before the markdown write lands on release.
|
|
82
|
-
const [dragHeight, setDragHeight] = useState<number | null>(null);
|
|
83
|
-
const inlineRef = useRef<HTMLDivElement>(null);
|
|
84
|
-
const effectiveHeight = dragHeight ?? pinnedHeight;
|
|
85
|
-
|
|
86
|
-
const onResizeStart = useCallback(
|
|
87
|
-
(e: React.PointerEvent) => {
|
|
88
|
-
// The handle owns this gesture — keep ProseMirror and the Scene from
|
|
89
|
-
// treating it as a selection / canvas drag.
|
|
90
|
-
e.preventDefault();
|
|
91
|
-
e.stopPropagation();
|
|
92
|
-
const startY = e.clientY;
|
|
93
|
-
// Measure the live rendered height so a drag starting from the CSS
|
|
94
|
-
// default (no pinned height yet) grows from the right baseline.
|
|
95
|
-
const startHeight =
|
|
96
|
-
inlineRef.current?.getBoundingClientRect().height ??
|
|
97
|
-
effectiveHeight ??
|
|
98
|
-
DEFAULT_DIAGRAM_HEIGHT;
|
|
99
|
-
const heightAt = (clientY: number) =>
|
|
100
|
-
Math.max(MIN_DIAGRAM_HEIGHT, Math.round(startHeight + (clientY - startY)));
|
|
101
|
-
const prevCursor = document.body.style.cursor;
|
|
102
|
-
const prevSelect = document.body.style.userSelect;
|
|
103
|
-
document.body.style.cursor = 'ns-resize';
|
|
104
|
-
document.body.style.userSelect = 'none';
|
|
105
|
-
const onMove = (ev: PointerEvent) => setDragHeight(heightAt(ev.clientY));
|
|
106
|
-
const onUp = (ev: PointerEvent) => {
|
|
107
|
-
window.removeEventListener('pointermove', onMove);
|
|
108
|
-
window.removeEventListener('pointerup', onUp);
|
|
109
|
-
document.body.style.cursor = prevCursor;
|
|
110
|
-
document.body.style.userSelect = prevSelect;
|
|
111
|
-
const finalH = heightAt(ev.clientY);
|
|
112
|
-
setDragHeight(null);
|
|
113
|
-
const livePos = findDiagramHeadingPos(editor, headingKey) ?? parentPos;
|
|
114
|
-
setDiagramHeight(editor, livePos, finalH);
|
|
115
|
-
};
|
|
116
|
-
window.addEventListener('pointermove', onMove);
|
|
117
|
-
window.addEventListener('pointerup', onUp);
|
|
118
|
-
},
|
|
119
|
-
[editor, headingKey, parentPos, effectiveHeight],
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
// Double-click the handle to clear the pinned height (revert to default).
|
|
123
|
-
const onResizeReset = useCallback(() => {
|
|
124
|
-
const livePos = findDiagramHeadingPos(editor, headingKey) ?? parentPos;
|
|
125
|
-
setDragHeight(null);
|
|
126
|
-
setDiagramHeight(editor, livePos, null);
|
|
127
|
-
}, [editor, headingKey, parentPos]);
|
|
128
|
-
|
|
129
|
-
const dispatch = useCallback(
|
|
130
|
-
(cmd: DiagramCommand) => {
|
|
131
|
-
// Re-resolve at dispatch time too — the React state may not have
|
|
132
|
-
// caught up yet when the user issues rapid-fire commands.
|
|
133
|
-
const livePos = findDiagramHeadingPos(editor, headingKey) ?? parentPos;
|
|
134
|
-
switch (cmd.kind) {
|
|
135
|
-
case 'moveNode':
|
|
136
|
-
moveNode(editor, livePos, cmd.nodeId, cmd.x, cmd.y);
|
|
137
|
-
break;
|
|
138
|
-
case 'resizeNode':
|
|
139
|
-
resizeNode(editor, livePos, cmd.nodeId, cmd.width, cmd.height);
|
|
140
|
-
break;
|
|
141
|
-
case 'addConnection':
|
|
142
|
-
addConnection(editor, livePos, cmd.source, cmd.target, cmd.type);
|
|
143
|
-
break;
|
|
144
|
-
case 'removeConnection':
|
|
145
|
-
removeConnection(editor, livePos, cmd.source, cmd.target, cmd.type);
|
|
146
|
-
break;
|
|
147
|
-
case 'renameNode':
|
|
148
|
-
renameNode(editor, livePos, cmd.nodeId, cmd.newLabel);
|
|
149
|
-
break;
|
|
150
|
-
case 'addNode': {
|
|
151
|
-
const used = new Set(nodes.map((n) => n.id));
|
|
152
|
-
let i = nodes.length + 1;
|
|
153
|
-
let id = `node-${i}`;
|
|
154
|
-
while (used.has(id)) {
|
|
155
|
-
i++;
|
|
156
|
-
id = `node-${i}`;
|
|
157
|
-
}
|
|
158
|
-
addNode(editor, livePos, id, `Node ${i}`, cmd.x, cmd.y);
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
case 'removeNode':
|
|
162
|
-
removeNode(editor, livePos, cmd.nodeId);
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
[editor, headingKey, parentPos, nodes],
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
// ── Shared toolbar (above the canvas) ────────────────────
|
|
170
|
-
// Tool selection + node CRUD live here rather than inside the canvas,
|
|
171
|
-
// so diagram/drawing/layout share one consistent bar above each canvas.
|
|
172
|
-
const [activeToolId, setActiveToolId] = useState<string>('select');
|
|
173
|
-
const [selectedIds, setSelectedIds] = useState<ReadonlySet<string>>(() => new Set());
|
|
174
|
-
// Map the Scene's selected layer ids (node cards) back to node ids.
|
|
175
|
-
const selectedNodeIds = useMemo(
|
|
176
|
-
() =>
|
|
177
|
-
[...selectedIds]
|
|
178
|
-
.map((id) => nodeIdFromCardLayerId(id))
|
|
179
|
-
.filter((id): id is string => id != null),
|
|
180
|
-
[selectedIds],
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const actions: SceneBlockAction[] = [
|
|
184
|
-
{
|
|
185
|
-
id: 'add-node',
|
|
186
|
-
label: 'Node',
|
|
187
|
-
icon: <Icon icon="fa-solid fa-plus" />,
|
|
188
|
-
title: 'Add node',
|
|
189
|
-
onClick: () =>
|
|
190
|
-
dispatch({
|
|
191
|
-
kind: 'addNode',
|
|
192
|
-
x: DIAGRAM_VIEWPORT.width / 2 - NODE_WIDTH / 2,
|
|
193
|
-
y: DIAGRAM_VIEWPORT.height / 2 - NODE_HEIGHT / 2,
|
|
194
|
-
}),
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
id: 'delete',
|
|
198
|
-
label: 'Delete',
|
|
199
|
-
icon: <Icon icon="fa-solid fa-trash" />,
|
|
200
|
-
title: 'Delete selected node',
|
|
201
|
-
danger: true,
|
|
202
|
-
disabled: selectedNodeIds.length === 0,
|
|
203
|
-
onClick: () => selectedNodeIds.forEach((id) => dispatch({ kind: 'removeNode', nodeId: id })),
|
|
204
|
-
},
|
|
205
|
-
];
|
|
206
|
-
|
|
207
|
-
const toolbar = (
|
|
208
|
-
<SceneBlockToolbar
|
|
209
|
-
tools={DIAGRAM_TOOLS}
|
|
210
|
-
activeToolId={activeToolId}
|
|
211
|
-
onSelectTool={setActiveToolId}
|
|
212
|
-
actions={actions}
|
|
213
|
-
/>
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const canvas = (
|
|
217
|
-
<DiagramCanvas
|
|
218
|
-
nodes={nodes}
|
|
219
|
-
edges={edges}
|
|
220
|
-
onCommand={dispatch}
|
|
221
|
-
showMaximize
|
|
222
|
-
maximized={maximized}
|
|
223
|
-
onToggleMaximize={() => setMaximized((m) => !m)}
|
|
224
|
-
activeToolId={activeToolId}
|
|
225
|
-
onActiveToolIdChange={setActiveToolId}
|
|
226
|
-
onSelectionChange={setSelectedIds}
|
|
227
|
-
/>
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
if (maximized) {
|
|
231
|
-
return (
|
|
232
|
-
<div
|
|
233
|
-
className="squisq-diagram-inline-placeholder"
|
|
234
|
-
style={effectiveHeight != null ? { height: effectiveHeight } : undefined}
|
|
235
|
-
>
|
|
236
|
-
<DiagramMaximizedOverlay host={host ?? null} onClose={() => setMaximized(false)}>
|
|
237
|
-
{/* Maximized has a full screen for a static right column — no collapse. */}
|
|
238
|
-
<div className="squisq-scene-block-max">
|
|
239
|
-
{canvas}
|
|
240
|
-
<div className="squisq-scene-side-toolbar">{toolbar}</div>
|
|
241
|
-
</div>
|
|
242
|
-
</DiagramMaximizedOverlay>
|
|
243
|
-
</div>
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return (
|
|
248
|
-
<div className="squisq-scene-shell">
|
|
249
|
-
{/* Before the canvas so the narrow-width fallback bar sits above it; the
|
|
250
|
-
wide-width gutter column is absolute and unaffected by DOM order. */}
|
|
251
|
-
<SceneSideToolbar>{toolbar}</SceneSideToolbar>
|
|
252
|
-
<div
|
|
253
|
-
className="squisq-diagram-inline"
|
|
254
|
-
ref={inlineRef}
|
|
255
|
-
style={effectiveHeight != null ? { height: effectiveHeight } : undefined}
|
|
256
|
-
>
|
|
257
|
-
{canvas}
|
|
258
|
-
<div
|
|
259
|
-
className="squisq-diagram-resize-handle"
|
|
260
|
-
onPointerDown={onResizeStart}
|
|
261
|
-
onDoubleClick={onResizeReset}
|
|
262
|
-
role="separator"
|
|
263
|
-
aria-orientation="horizontal"
|
|
264
|
-
aria-label="Resize diagram height"
|
|
265
|
-
title="Drag to resize · double-click to reset"
|
|
266
|
-
/>
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export default DiagramWidget;
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read diagram nodes + edges from the live Tiptap state.
|
|
3
|
-
*
|
|
4
|
-
* For a given parent heading position, walks the diagram section's child
|
|
5
|
-
* headings, builds synthetic `Block` objects from their text + Pandoc
|
|
6
|
-
* attributes, runs `computeDiagramLayout` from core to fill in missing
|
|
7
|
-
* positions, and returns the result in the shape React Flow consumes.
|
|
8
|
-
*
|
|
9
|
-
* The hook re-derives on every editor transaction — no caching layer
|
|
10
|
-
* means there's nothing to invalidate when the user types or the markdown
|
|
11
|
-
* is reloaded from disk.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
15
|
-
import type { Editor } from '@tiptap/react';
|
|
16
|
-
import type { Block } from '@bendyline/squisq/schemas';
|
|
17
|
-
import { computeDiagramLayout } from '@bendyline/squisq/doc';
|
|
18
|
-
import { listDiagramChildren } from './diagramCommands';
|
|
19
|
-
|
|
20
|
-
export interface DiagramRFNode {
|
|
21
|
-
id: string;
|
|
22
|
-
position: { x: number; y: number };
|
|
23
|
-
data: { label: string };
|
|
24
|
-
type?: string;
|
|
25
|
-
/** Per-node width override (from the heading's `w=` Pandoc param). */
|
|
26
|
-
width?: number;
|
|
27
|
-
/** Per-node height override (from the heading's `h=` Pandoc param). */
|
|
28
|
-
height?: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface DiagramRFEdge {
|
|
32
|
-
id: string;
|
|
33
|
-
source: string;
|
|
34
|
-
target: string;
|
|
35
|
-
label?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface DiagramData {
|
|
39
|
-
nodes: DiagramRFNode[];
|
|
40
|
-
edges: DiagramRFEdge[];
|
|
41
|
-
warnings: string[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function useDiagramData(editor: Editor, parentPos: number): DiagramData {
|
|
45
|
-
// Track a version counter that bumps on every editor transaction so the
|
|
46
|
-
// hook re-renders. We avoid storing the full doc to keep references
|
|
47
|
-
// stable for React Flow's internal memoisation.
|
|
48
|
-
const [version, setVersion] = useState(0);
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const onUpdate = () => setVersion((v) => v + 1);
|
|
51
|
-
editor.on('transaction', onUpdate);
|
|
52
|
-
return () => {
|
|
53
|
-
editor.off('transaction', onUpdate);
|
|
54
|
-
};
|
|
55
|
-
}, [editor]);
|
|
56
|
-
|
|
57
|
-
return useMemo(() => {
|
|
58
|
-
const children = listDiagramChildren(editor, parentPos);
|
|
59
|
-
// Per-node size overrides keyed by id, so the post-layout pass can
|
|
60
|
-
// re-attach them without polluting the Block schema used by SSR.
|
|
61
|
-
const sizeOverrides = new Map<string, { width?: number; height?: number }>();
|
|
62
|
-
// Build synthetic Blocks for the layout helper.
|
|
63
|
-
const syntheticBlocks: Block[] = children.map((c) => {
|
|
64
|
-
const params = c.attrs.params ?? {};
|
|
65
|
-
const xRaw = params.x;
|
|
66
|
-
const yRaw = params.y;
|
|
67
|
-
const x = xRaw != null ? Number(xRaw) : undefined;
|
|
68
|
-
const y = yRaw != null ? Number(yRaw) : undefined;
|
|
69
|
-
const wRaw = params.w;
|
|
70
|
-
const hRaw = params.h;
|
|
71
|
-
const w = wRaw != null ? Number(wRaw) : undefined;
|
|
72
|
-
const h = hRaw != null ? Number(hRaw) : undefined;
|
|
73
|
-
if (Number.isFinite(w) || Number.isFinite(h)) {
|
|
74
|
-
sizeOverrides.set(c.id, {
|
|
75
|
-
...(Number.isFinite(w) ? { width: w as number } : {}),
|
|
76
|
-
...(Number.isFinite(h) ? { height: h as number } : {}),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
const blockMeta = c.attrs.blockMeta;
|
|
80
|
-
const text = getHeadingText(c.node);
|
|
81
|
-
return {
|
|
82
|
-
id: c.id,
|
|
83
|
-
startTime: 0,
|
|
84
|
-
duration: 0,
|
|
85
|
-
audioSegment: 0,
|
|
86
|
-
title: text || c.id,
|
|
87
|
-
...(Number.isFinite(x) ? { x: x as number } : {}),
|
|
88
|
-
...(Number.isFinite(y) ? { y: y as number } : {}),
|
|
89
|
-
...(blockMeta?.connectsTo ? { connectsTo: blockMeta.connectsTo } : {}),
|
|
90
|
-
...(c.attrs.classes ? { classes: c.attrs.classes } : {}),
|
|
91
|
-
};
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const layout = computeDiagramLayout(syntheticBlocks);
|
|
95
|
-
|
|
96
|
-
const nodes: DiagramRFNode[] = layout.nodes.map((n) => {
|
|
97
|
-
const size = sizeOverrides.get(n.id);
|
|
98
|
-
return {
|
|
99
|
-
id: n.id,
|
|
100
|
-
position: { x: n.x, y: n.y },
|
|
101
|
-
data: { label: n.label },
|
|
102
|
-
...(size?.width != null ? { width: size.width } : {}),
|
|
103
|
-
...(size?.height != null ? { height: size.height } : {}),
|
|
104
|
-
};
|
|
105
|
-
});
|
|
106
|
-
const edges: DiagramRFEdge[] = layout.edges.map((e) => ({
|
|
107
|
-
id: e.id,
|
|
108
|
-
source: e.source,
|
|
109
|
-
target: e.target,
|
|
110
|
-
...(e.type ? { label: e.type } : {}),
|
|
111
|
-
}));
|
|
112
|
-
|
|
113
|
-
return { nodes, edges, warnings: layout.warnings };
|
|
114
|
-
// `version` is the dependency we track for transactions; editor and
|
|
115
|
-
// parentPos shouldn't change on re-render.
|
|
116
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
117
|
-
}, [editor, parentPos, version]);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getHeadingText(node: import('@tiptap/pm/model').Node): string {
|
|
121
|
-
let text = '';
|
|
122
|
-
node.content.forEach((child) => {
|
|
123
|
-
if (child.isText) text += child.text ?? '';
|
|
124
|
-
});
|
|
125
|
-
return text;
|
|
126
|
-
}
|