@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
package/src/scene/Scene.tsx
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from 'react';
|
|
24
24
|
import type { Layer } from '@bendyline/squisq/schemas';
|
|
25
25
|
import type { SceneCommand, SceneEdge } from './commands/SceneCommand';
|
|
26
|
-
import type { SceneTool, SceneToolContext } from './tools/SceneTool';
|
|
26
|
+
import type { SceneInteractionState, SceneTool, SceneToolContext } from './tools/SceneTool';
|
|
27
27
|
import { useScenePanZoom } from './hooks/useScenePanZoom';
|
|
28
28
|
import { useSceneSelection } from './hooks/useSceneSelection';
|
|
29
29
|
import { useSceneHitTest, layerBounds, type HitTestable } from './hooks/useSceneHitTest';
|
|
@@ -140,12 +140,21 @@ export function Scene(props: SceneProps) {
|
|
|
140
140
|
// ── Pan/zoom + selection ────────────────────────────────────
|
|
141
141
|
const panZoom = useScenePanZoom();
|
|
142
142
|
const selection = useSceneSelection();
|
|
143
|
+
const setSceneSelection = selection.setSelection;
|
|
143
144
|
const { hit } = useSceneHitTest();
|
|
145
|
+
const interaction = useRef<SceneInteractionState>({}).current;
|
|
144
146
|
|
|
145
|
-
// Surface selection
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
// Surface selection in the originating interaction rather than a passive
|
|
148
|
+
// effect. Detached editor roots can then update their toolbar without
|
|
149
|
+
// forcing a flush from inside another component's lifecycle.
|
|
150
|
+
const setSelection = useCallback(
|
|
151
|
+
(ids: Iterable<string>) => {
|
|
152
|
+
const next = new Set(ids);
|
|
153
|
+
setSceneSelection(next);
|
|
154
|
+
onSelectionChange?.(next);
|
|
155
|
+
},
|
|
156
|
+
[setSceneSelection, onSelectionChange],
|
|
157
|
+
);
|
|
149
158
|
|
|
150
159
|
// ── Hit-test cache ──────────────────────────────────────────
|
|
151
160
|
const hitItems: HitTestable[] = useMemo(() => {
|
|
@@ -161,6 +170,7 @@ export function Scene(props: SceneProps) {
|
|
|
161
170
|
// ── Tool context (rebuilt every render — cheap, no allocations in hot path) ──
|
|
162
171
|
const ctx: SceneToolContext = useMemo(
|
|
163
172
|
() => ({
|
|
173
|
+
interaction,
|
|
164
174
|
viewport,
|
|
165
175
|
transform: panZoom.transform,
|
|
166
176
|
layers,
|
|
@@ -170,7 +180,7 @@ export function Scene(props: SceneProps) {
|
|
|
170
180
|
screenToViewport: panZoom.screenToViewport,
|
|
171
181
|
viewportToScreen: panZoom.viewportToScreen,
|
|
172
182
|
hit: (p) => hit(p, hitItems),
|
|
173
|
-
setSelection
|
|
183
|
+
setSelection,
|
|
174
184
|
dispatch: onCommand,
|
|
175
185
|
setActiveTool,
|
|
176
186
|
beginTextEdit: textEdit.begin,
|
|
@@ -183,16 +193,17 @@ export function Scene(props: SceneProps) {
|
|
|
183
193
|
layers,
|
|
184
194
|
edges,
|
|
185
195
|
selection.selection,
|
|
186
|
-
|
|
196
|
+
setSelection,
|
|
187
197
|
hitItems,
|
|
188
198
|
hit,
|
|
189
199
|
onCommand,
|
|
190
200
|
setActiveTool,
|
|
191
201
|
textEdit.begin,
|
|
202
|
+
interaction,
|
|
192
203
|
],
|
|
193
204
|
);
|
|
194
|
-
// Mirror ctx in a ref so keyboard handlers
|
|
195
|
-
//
|
|
205
|
+
// Mirror ctx in a ref so root keyboard handlers read the latest selection
|
|
206
|
+
// without re-binding on every selection change.
|
|
196
207
|
const ctxRef = useRef(ctx);
|
|
197
208
|
ctxRef.current = ctx;
|
|
198
209
|
|
|
@@ -220,12 +231,9 @@ export function Scene(props: SceneProps) {
|
|
|
220
231
|
[panZoom],
|
|
221
232
|
);
|
|
222
233
|
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
// a gesture is in progress. We bump `dragTick` on pointer-down and on
|
|
227
|
-
// every move thereafter — cheap (one setState per move) and only fires
|
|
228
|
-
// while the user is actively interacting.
|
|
234
|
+
// Gesture state belongs to this Scene instance and lives outside React's
|
|
235
|
+
// render cycle. Bump `dragTick` while a pointer gesture is active so its
|
|
236
|
+
// live drag/resize preview is visible.
|
|
229
237
|
const [, setDragTick] = useState(0);
|
|
230
238
|
const pointerActive = useRef(false);
|
|
231
239
|
const bumpTick = useCallback(() => setDragTick((t) => t + 1), []);
|
|
@@ -280,6 +288,27 @@ export function Scene(props: SceneProps) {
|
|
|
280
288
|
[activeTool],
|
|
281
289
|
);
|
|
282
290
|
|
|
291
|
+
const handlePointerCancel = useCallback(
|
|
292
|
+
(e: React.PointerEvent<SVGSVGElement>) => {
|
|
293
|
+
pointerActive.current = false;
|
|
294
|
+
isPanning.current = false;
|
|
295
|
+
panLast.current = null;
|
|
296
|
+
interaction.selectDrag = undefined;
|
|
297
|
+
interaction.connect = undefined;
|
|
298
|
+
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
|
299
|
+
bumpTick();
|
|
300
|
+
},
|
|
301
|
+
[interaction, bumpTick],
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
useEffect(
|
|
305
|
+
() => () => {
|
|
306
|
+
interaction.selectDrag = undefined;
|
|
307
|
+
interaction.connect = undefined;
|
|
308
|
+
},
|
|
309
|
+
[interaction],
|
|
310
|
+
);
|
|
311
|
+
|
|
283
312
|
const handleDoubleClick = useCallback(
|
|
284
313
|
(e: React.MouseEvent<SVGSVGElement>) => {
|
|
285
314
|
activeTool?.onDoubleClick?.(e, ctxRef.current);
|
|
@@ -387,8 +416,8 @@ export function Scene(props: SceneProps) {
|
|
|
387
416
|
}, [containerSize, hitItems, panZoom]);
|
|
388
417
|
|
|
389
418
|
// ── Render ──────────────────────────────────────────────────
|
|
390
|
-
const liveOffset = activeId === 'select' ? getActiveMoveOffset() : null;
|
|
391
|
-
const liveResize = activeId === 'select' ? getActiveResize() : null;
|
|
419
|
+
const liveOffset = activeId === 'select' ? getActiveMoveOffset(interaction) : null;
|
|
420
|
+
const liveResize = activeId === 'select' ? getActiveResize(interaction) : null;
|
|
392
421
|
const showSelectionHandles = activeTool?.hideSelectionHandles !== true;
|
|
393
422
|
|
|
394
423
|
// Per-layer transform applied during an in-flight drag or resize so
|
|
@@ -453,7 +482,7 @@ export function Scene(props: SceneProps) {
|
|
|
453
482
|
const sx = e.clientX - rect.left;
|
|
454
483
|
const sy = e.clientY - rect.top;
|
|
455
484
|
const startV = panZoom.screenToViewport(sx, sy);
|
|
456
|
-
beginHandleDrag({ layerId: id, corner, startV, startBounds: bounds });
|
|
485
|
+
beginHandleDrag({ layerId: id, corner, startV, startBounds: bounds }, interaction);
|
|
457
486
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
458
487
|
// Mark a gesture as active so the Scene's pointer-move handler
|
|
459
488
|
// triggers re-renders for the live resize preview.
|
|
@@ -475,6 +504,7 @@ export function Scene(props: SceneProps) {
|
|
|
475
504
|
onPointerDown={handlePointerDown}
|
|
476
505
|
onPointerMove={handlePointerMove}
|
|
477
506
|
onPointerUp={handlePointerUp}
|
|
507
|
+
onPointerCancel={handlePointerCancel}
|
|
478
508
|
onDoubleClick={handleDoubleClick}
|
|
479
509
|
onDragOver={onDrop ? handleDragOver : undefined}
|
|
480
510
|
onDrop={onDrop ? handleDrop : undefined}
|
|
@@ -20,6 +20,7 @@ import { createElement } from 'react';
|
|
|
20
20
|
import type { Node as PMNode } from '@tiptap/pm/model';
|
|
21
21
|
import { parsePandocAttrTokens } from '@bendyline/squisq/markdown';
|
|
22
22
|
import { SceneBlockWidget, type SceneBlockMode } from './SceneBlockWidget';
|
|
23
|
+
import type { SceneTextChannel } from './text/sceneTextChannel';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Stable identifier for a layout/drawing heading — used as part of the
|
|
@@ -27,24 +28,24 @@ import { SceneBlockWidget, type SceneBlockMode } from './SceneBlockWidget';
|
|
|
27
28
|
* changes (drag/resize commits write into the heading's
|
|
28
29
|
* `data-block-attrs`).
|
|
29
30
|
*/
|
|
30
|
-
export function getHeadingKey(node: PMNode): string {
|
|
31
|
+
export function getHeadingKey(node: PMNode, position?: number): string {
|
|
31
32
|
const raw = (node.attrs as Record<string, unknown>).dataBlockAttrs;
|
|
32
33
|
if (typeof raw === 'string' && raw.length > 0) {
|
|
33
34
|
const attrs = parsePandocAttrTokens(raw);
|
|
34
|
-
if (attrs.id) return attrs.id
|
|
35
|
+
if (attrs.id) return position == null ? attrs.id : `${attrs.id}@${position}`;
|
|
35
36
|
}
|
|
36
37
|
let text = '';
|
|
37
38
|
node.content.forEach((child) => {
|
|
38
39
|
if (child.isText) text += child.text ?? '';
|
|
39
40
|
});
|
|
40
|
-
|
|
41
|
+
const slug =
|
|
41
42
|
text
|
|
42
43
|
.toLowerCase()
|
|
43
44
|
.replace(/[^\w\s-]/g, '')
|
|
44
45
|
.replace(/[\s_]+/g, '-')
|
|
45
46
|
.replace(/-+/g, '-')
|
|
46
|
-
.replace(/^-|-$/g, '') || 'block'
|
|
47
|
-
|
|
47
|
+
.replace(/^-|-$/g, '') || 'block';
|
|
48
|
+
return position == null ? slug : `${slug}@${position}`;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/**
|
|
@@ -63,7 +64,7 @@ export function findSceneHeadingPos(
|
|
|
63
64
|
if (node.type.name !== 'heading') return;
|
|
64
65
|
const attrs = node.attrs as { dataTemplate?: string };
|
|
65
66
|
if (attrs.dataTemplate !== expectedTemplate) return;
|
|
66
|
-
if (getHeadingKey(node) === headingKey) {
|
|
67
|
+
if (getHeadingKey(node, pos) === headingKey) {
|
|
67
68
|
found = pos;
|
|
68
69
|
return false;
|
|
69
70
|
}
|
|
@@ -82,7 +83,11 @@ interface SceneRoot {
|
|
|
82
83
|
root: Root;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
function buildDecorations(
|
|
86
|
+
function buildDecorations(
|
|
87
|
+
state: EditorState,
|
|
88
|
+
editor: Editor,
|
|
89
|
+
textChannel?: SceneTextChannel,
|
|
90
|
+
): DecorationSet {
|
|
86
91
|
const decos: Decoration[] = [];
|
|
87
92
|
|
|
88
93
|
state.doc.descendants((node, pos) => {
|
|
@@ -93,7 +98,7 @@ function buildDecorations(state: EditorState, editor: Editor): DecorationSet {
|
|
|
93
98
|
const parentDepth = attrs.level;
|
|
94
99
|
const widgetPos = pos + node.nodeSize;
|
|
95
100
|
const parentPos = pos;
|
|
96
|
-
const headingKey = getHeadingKey(node);
|
|
101
|
+
const headingKey = getHeadingKey(node, pos);
|
|
97
102
|
decos.push(
|
|
98
103
|
Decoration.widget(
|
|
99
104
|
widgetPos,
|
|
@@ -112,6 +117,7 @@ function buildDecorations(state: EditorState, editor: Editor): DecorationSet {
|
|
|
112
117
|
fallbackParentPos: parentPos,
|
|
113
118
|
mode,
|
|
114
119
|
host: view.dom.parentElement ?? view.dom,
|
|
120
|
+
textChannel,
|
|
115
121
|
}),
|
|
116
122
|
);
|
|
117
123
|
|
|
@@ -161,6 +167,7 @@ function buildDecorations(state: EditorState, editor: Editor): DecorationSet {
|
|
|
161
167
|
export interface SceneBlockExtensionOptions {
|
|
162
168
|
/** When false, the extension is inert. */
|
|
163
169
|
enabled?: boolean;
|
|
170
|
+
textChannel?: SceneTextChannel;
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
export const SceneBlockExtension = Extension.create<SceneBlockExtensionOptions>({
|
|
@@ -179,10 +186,10 @@ export const SceneBlockExtension = Extension.create<SceneBlockExtensionOptions>(
|
|
|
179
186
|
new Plugin({
|
|
180
187
|
key: KEY,
|
|
181
188
|
state: {
|
|
182
|
-
init: (_config, state) => buildDecorations(state, editor),
|
|
189
|
+
init: (_config, state) => buildDecorations(state, editor, this.options.textChannel),
|
|
183
190
|
apply: (tr, oldDecos, _oldState, newState) => {
|
|
184
191
|
if (!tr.docChanged) return oldDecos;
|
|
185
|
-
return buildDecorations(newState, editor);
|
|
192
|
+
return buildDecorations(newState, editor, this.options.textChannel);
|
|
186
193
|
},
|
|
187
194
|
},
|
|
188
195
|
props: {
|
|
@@ -30,6 +30,7 @@ import type { SceneTextEditConfig } from './text/sceneTextConfig';
|
|
|
30
30
|
import { markdownToTiptap } from '../tiptapBridge';
|
|
31
31
|
import { Icon } from '../Icon';
|
|
32
32
|
import { DiagramMaximizedOverlay } from '../diagram/DiagramMaximizedOverlay';
|
|
33
|
+
import type { SceneTextChannel } from './text/sceneTextChannel';
|
|
33
34
|
|
|
34
35
|
export type SceneBlockMode = 'layout' | 'drawing';
|
|
35
36
|
|
|
@@ -40,6 +41,7 @@ interface SceneBlockWidgetProps {
|
|
|
40
41
|
mode: SceneBlockMode;
|
|
41
42
|
/** Host element for portal targeting (used by maximize overlay). */
|
|
42
43
|
host?: HTMLElement | null;
|
|
44
|
+
textChannel?: SceneTextChannel;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
// Default viewport for layout/drawing surfaces. Matches Squisq's 16:9
|
|
@@ -59,6 +61,7 @@ export function SceneBlockWidget({
|
|
|
59
61
|
fallbackParentPos,
|
|
60
62
|
mode,
|
|
61
63
|
host,
|
|
64
|
+
textChannel,
|
|
62
65
|
}: SceneBlockWidgetProps) {
|
|
63
66
|
const [parentPos, setParentPos] = useState<number>(
|
|
64
67
|
() => findSceneHeadingPos(editor, headingKey, mode) ?? fallbackParentPos,
|
|
@@ -89,6 +92,7 @@ export function SceneBlockWidget({
|
|
|
89
92
|
dispatchRef.current = adapter.dispatch;
|
|
90
93
|
const textConfig = useMemo<SceneTextEditConfig>(
|
|
91
94
|
() => ({
|
|
95
|
+
channel: textChannel,
|
|
92
96
|
resolveEditableId: (id) => {
|
|
93
97
|
const l = layersRef.current.find((x) => x.id === id);
|
|
94
98
|
return l && l.type === 'text' ? id : null;
|
|
@@ -114,7 +118,7 @@ export function SceneBlockWidget({
|
|
|
114
118
|
// content is stored as the child sub-block's markdown body.
|
|
115
119
|
level: isDrawing ? 'inline' : 'block',
|
|
116
120
|
}),
|
|
117
|
-
[isDrawing],
|
|
121
|
+
[isDrawing, textChannel],
|
|
118
122
|
);
|
|
119
123
|
|
|
120
124
|
const [maximized, setMaximized] = useState(false);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* canvas grid would.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { forwardRef, type ReactNode, type WheelEvent } from 'react';
|
|
14
|
+
import { forwardRef, useId, type ReactNode, type WheelEvent } from 'react';
|
|
15
15
|
import type { SceneTransform } from './hooks/useScenePanZoom';
|
|
16
16
|
|
|
17
17
|
interface SceneViewportProps {
|
|
@@ -24,6 +24,7 @@ interface SceneViewportProps {
|
|
|
24
24
|
onPointerDown?: (e: React.PointerEvent<SVGSVGElement>) => void;
|
|
25
25
|
onPointerMove?: (e: React.PointerEvent<SVGSVGElement>) => void;
|
|
26
26
|
onPointerUp?: (e: React.PointerEvent<SVGSVGElement>) => void;
|
|
27
|
+
onPointerCancel?: (e: React.PointerEvent<SVGSVGElement>) => void;
|
|
27
28
|
onDoubleClick?: (e: React.MouseEvent<SVGSVGElement>) => void;
|
|
28
29
|
/** HTML5 drag-and-drop handlers — used for palette drag-to-place. */
|
|
29
30
|
onDragOver?: (e: React.DragEvent<SVGSVGElement>) => void;
|
|
@@ -43,6 +44,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
43
44
|
onPointerDown,
|
|
44
45
|
onPointerMove,
|
|
45
46
|
onPointerUp,
|
|
47
|
+
onPointerCancel,
|
|
46
48
|
onDoubleClick,
|
|
47
49
|
onDragOver,
|
|
48
50
|
onDrop,
|
|
@@ -52,6 +54,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
52
54
|
ref,
|
|
53
55
|
) {
|
|
54
56
|
const t = transform;
|
|
57
|
+
const gridId = `squisq-scene-dot-grid-${useId().replace(/:/g, '')}`;
|
|
55
58
|
const groupTransform = `translate(${t.tx} ${t.ty}) scale(${t.scale})`;
|
|
56
59
|
|
|
57
60
|
return (
|
|
@@ -70,6 +73,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
70
73
|
onPointerDown={onPointerDown}
|
|
71
74
|
onPointerMove={onPointerMove}
|
|
72
75
|
onPointerUp={onPointerUp}
|
|
76
|
+
onPointerCancel={onPointerCancel}
|
|
73
77
|
onDoubleClick={onDoubleClick}
|
|
74
78
|
onDragOver={onDragOver}
|
|
75
79
|
onDrop={onDrop}
|
|
@@ -85,7 +89,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
85
89
|
>
|
|
86
90
|
<defs>
|
|
87
91
|
<pattern
|
|
88
|
-
id=
|
|
92
|
+
id={gridId}
|
|
89
93
|
x={t.tx}
|
|
90
94
|
y={t.ty}
|
|
91
95
|
width={24 * t.scale}
|
|
@@ -95,7 +99,7 @@ export const SceneViewport = forwardRef<SVGSVGElement, SceneViewportProps>(funct
|
|
|
95
99
|
<circle cx={1} cy={1} r={1} className="squisq-scene-dot" />
|
|
96
100
|
</pattern>
|
|
97
101
|
</defs>
|
|
98
|
-
<rect width="100%" height="100%" fill=
|
|
102
|
+
<rect width="100%" height="100%" fill={`url(#${gridId})`} />
|
|
99
103
|
<g transform={groupTransform}>{children}</g>
|
|
100
104
|
</svg>
|
|
101
105
|
);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* gets drawn.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
11
|
+
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
12
12
|
import { shapePath } from '@bendyline/squisq/doc';
|
|
13
13
|
|
|
14
14
|
interface ShapePaletteProps {
|
|
@@ -152,8 +152,23 @@ export function ShapePalette({
|
|
|
152
152
|
/** A 40×30 SVG thumbnail of a shape, inside a 48×38 button viewport. */
|
|
153
153
|
function ShapeThumb({ kind }: { kind: string }) {
|
|
154
154
|
const d = shapePath(kind, 4, 4, 40, 30);
|
|
155
|
+
const arrowId = `squisq-shape-thumb-arrow-${useId().replace(/:/g, '')}`;
|
|
155
156
|
return (
|
|
156
157
|
<svg viewBox="0 0 48 38" width={36} height={28} aria-hidden="true">
|
|
158
|
+
{kind === 'arrow' ? (
|
|
159
|
+
<defs>
|
|
160
|
+
<marker
|
|
161
|
+
id={arrowId}
|
|
162
|
+
viewBox="0 0 10 10"
|
|
163
|
+
refX={9}
|
|
164
|
+
refY={5}
|
|
165
|
+
markerWidth={4}
|
|
166
|
+
markerHeight={4}
|
|
167
|
+
>
|
|
168
|
+
<path d="M 0 0 L 10 5 L 0 10 z" className="squisq-shape-thumb-line" />
|
|
169
|
+
</marker>
|
|
170
|
+
</defs>
|
|
171
|
+
) : null}
|
|
157
172
|
{d ? (
|
|
158
173
|
<path d={d} className="squisq-shape-thumb" />
|
|
159
174
|
) : kind === 'circle' ? (
|
|
@@ -167,7 +182,7 @@ function ShapeThumb({ kind }: { kind: string }) {
|
|
|
167
182
|
x2={44}
|
|
168
183
|
y2={4}
|
|
169
184
|
className="squisq-shape-thumb-line"
|
|
170
|
-
markerEnd=
|
|
185
|
+
markerEnd={`url(#${arrowId})`}
|
|
171
186
|
/>
|
|
172
187
|
) : kind === 'text' ? (
|
|
173
188
|
<text x={24} y={26} textAnchor="middle" className="squisq-shape-thumb-text">
|
|
@@ -53,4 +53,90 @@ describe('buildDiagramScene', () => {
|
|
|
53
53
|
);
|
|
54
54
|
expect(out.edges[0]).toEqual({ id: 'a->b', source: 'a', target: 'b' });
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it('passes container kind through and styles the label top-anchored', () => {
|
|
58
|
+
const out = buildDiagramScene(
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
id: 'group',
|
|
62
|
+
position: { x: 0, y: 0 },
|
|
63
|
+
data: { label: 'Cluster\nextra' },
|
|
64
|
+
width: 600,
|
|
65
|
+
height: 400,
|
|
66
|
+
kind: 'container',
|
|
67
|
+
},
|
|
68
|
+
{ id: 'a', position: { x: 40, y: 60 }, data: { label: 'A' } },
|
|
69
|
+
],
|
|
70
|
+
[],
|
|
71
|
+
);
|
|
72
|
+
expect(out.nodes[0].kind).toBe('container');
|
|
73
|
+
const label = out.layers.find((l) => l.id === 'node-label-group');
|
|
74
|
+
expect(label?.position.y).toBeLessThan(0 + 400 / 2); // top-anchored, not centered
|
|
75
|
+
if (label?.type === 'text') expect(label.content.text).toBe('Cluster'); // first line only
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('fits and vertically centers dense multi-line labels inside ASCII-sized cards', () => {
|
|
79
|
+
const out = buildDiagramScene(
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
id: 'kernel',
|
|
83
|
+
position: { x: 100, y: 200 },
|
|
84
|
+
data: {
|
|
85
|
+
label: '@bendyline/molen-kernel\nheadless sim — no DOM, no thread\nWorker + Node',
|
|
86
|
+
},
|
|
87
|
+
width: 252,
|
|
88
|
+
height: 112,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
[],
|
|
92
|
+
);
|
|
93
|
+
const label = out.layers.find((layer) => layer.id === 'node-label-kernel');
|
|
94
|
+
expect(label?.type).toBe('text');
|
|
95
|
+
if (label?.type !== 'text') return;
|
|
96
|
+
|
|
97
|
+
expect(label.content.style.fontSize).toBeLessThan(38);
|
|
98
|
+
expect(label.content.style.lineHeight).toBe(1.25);
|
|
99
|
+
expect(label.position.width).toBe(228);
|
|
100
|
+
expect(label.position.y).toBeLessThan(200 + 112 / 2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('maps directed:false to endMarker none, leaves default edges alone', () => {
|
|
104
|
+
const out = buildDiagramScene(
|
|
105
|
+
[
|
|
106
|
+
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'A' } },
|
|
107
|
+
{ id: 'b', position: { x: 300, y: 0 }, data: { label: 'B' } },
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
{ id: 'a->b', source: 'a', target: 'b', directed: false },
|
|
111
|
+
{ id: 'b->a', source: 'b', target: 'a' },
|
|
112
|
+
],
|
|
113
|
+
);
|
|
114
|
+
expect(out.edges[0]).toEqual({ id: 'a->b', source: 'a', target: 'b', endMarker: 'none' });
|
|
115
|
+
expect(out.edges[1]).toEqual({ id: 'b->a', source: 'b', target: 'a' });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('preserves authored anchors and routing for ASCII-derived edges', () => {
|
|
119
|
+
const out = buildDiagramScene(
|
|
120
|
+
[
|
|
121
|
+
{ id: 'source', position: { x: 0, y: 0 }, data: { label: 'Source' } },
|
|
122
|
+
{ id: 'target', position: { x: 0, y: 200 }, data: { label: 'Target' } },
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
{
|
|
126
|
+
id: 'source->target',
|
|
127
|
+
source: 'source',
|
|
128
|
+
target: 'target',
|
|
129
|
+
sourceAnchor: { side: 'bottom', offset: 0.25 },
|
|
130
|
+
targetAnchor: { side: 'top', offset: 0.5 },
|
|
131
|
+
routing: 'orthogonal',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(out.edges[0]).toMatchObject({
|
|
137
|
+
sourceAnchor: { side: 'bottom', offset: 0.25 },
|
|
138
|
+
targetAnchor: { side: 'top', offset: 0.5 },
|
|
139
|
+
routing: 'orthogonal',
|
|
140
|
+
});
|
|
141
|
+
});
|
|
56
142
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Schema } from '@tiptap/pm/model';
|
|
3
|
+
import type { Editor } from '@tiptap/react';
|
|
4
|
+
import { findSceneHeadingPos, getHeadingKey } from '../SceneBlockExtension';
|
|
5
|
+
|
|
6
|
+
const schema = new Schema({
|
|
7
|
+
nodes: {
|
|
8
|
+
doc: { content: 'heading+' },
|
|
9
|
+
text: {},
|
|
10
|
+
heading: {
|
|
11
|
+
content: 'text*',
|
|
12
|
+
attrs: {
|
|
13
|
+
level: { default: 1 },
|
|
14
|
+
dataTemplate: { default: 'layout' },
|
|
15
|
+
dataBlockAttrs: { default: null },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('SceneBlockExtension heading identity', () => {
|
|
22
|
+
it('targets the correct canvas when headings have duplicate text or ids', () => {
|
|
23
|
+
const first = schema.node('heading', { dataBlockAttrs: '#same' }, schema.text('Layout'));
|
|
24
|
+
const second = schema.node('heading', { dataBlockAttrs: '#same' }, schema.text('Layout'));
|
|
25
|
+
const secondPos = first.nodeSize;
|
|
26
|
+
const doc = schema.node('doc', null, [first, second]);
|
|
27
|
+
const editor = { state: { doc } } as unknown as Editor;
|
|
28
|
+
const firstKey = getHeadingKey(first, 0);
|
|
29
|
+
const secondKey = getHeadingKey(second, secondPos);
|
|
30
|
+
expect(firstKey).not.toBe(secondKey);
|
|
31
|
+
expect(findSceneHeadingPos(editor, secondKey, 'layout')).toBe(secondPos);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import type { SceneToolContext } from '../tools/SceneTool';
|
|
5
|
+
import { SelectTool, beginHandleDrag, getActiveMoveOffset } from '../tools/SelectTool';
|
|
6
|
+
import { ConnectTool } from '../tools/ConnectTool';
|
|
7
|
+
import { createSceneTextChannel } from '../text/sceneTextChannel';
|
|
8
|
+
import { SceneViewport } from '../SceneViewport';
|
|
9
|
+
import { DiagramEdges } from '../layers/DiagramEdges';
|
|
10
|
+
|
|
11
|
+
function context(id = 'layer'): SceneToolContext {
|
|
12
|
+
const layer = {
|
|
13
|
+
id,
|
|
14
|
+
type: 'shape' as const,
|
|
15
|
+
position: { x: 0, y: 0, width: 20, height: 20 },
|
|
16
|
+
content: { shape: 'rect' as const },
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
interaction: {},
|
|
20
|
+
viewport: { width: 100, height: 100 },
|
|
21
|
+
transform: { tx: 0, ty: 0, scale: 1 },
|
|
22
|
+
layers: [layer],
|
|
23
|
+
edges: [],
|
|
24
|
+
selection: new Set(),
|
|
25
|
+
hitItems: [{ id, layer, bounds: { x: 0, y: 0, width: 20, height: 20 } }],
|
|
26
|
+
screenToViewport: (x, y) => ({ x, y }),
|
|
27
|
+
viewportToScreen: (x, y) => ({ x, y }),
|
|
28
|
+
hit: () => id,
|
|
29
|
+
setSelection: vi.fn(),
|
|
30
|
+
dispatch: vi.fn(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pointer(clientX: number, clientY: number) {
|
|
35
|
+
return {
|
|
36
|
+
button: 0,
|
|
37
|
+
clientX,
|
|
38
|
+
clientY,
|
|
39
|
+
pointerId: 1,
|
|
40
|
+
shiftKey: false,
|
|
41
|
+
currentTarget: {
|
|
42
|
+
getBoundingClientRect: () => ({ left: 0, top: 0 }),
|
|
43
|
+
setPointerCapture: vi.fn(),
|
|
44
|
+
releasePointerCapture: vi.fn(),
|
|
45
|
+
},
|
|
46
|
+
stopPropagation: vi.fn(),
|
|
47
|
+
} as unknown as React.PointerEvent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('Scene instance isolation', () => {
|
|
51
|
+
it('keeps select and connect gestures in the owning Scene context', () => {
|
|
52
|
+
const first = context('node-card-a');
|
|
53
|
+
const second = context('node-card-a');
|
|
54
|
+
SelectTool.onPointerDown!(pointer(0, 0), first);
|
|
55
|
+
SelectTool.onPointerMove!(pointer(10, 0), first);
|
|
56
|
+
expect(getActiveMoveOffset(first.interaction)).toEqual({ dx: 10, dy: 0 });
|
|
57
|
+
expect(getActiveMoveOffset(second.interaction)).toBeNull();
|
|
58
|
+
|
|
59
|
+
ConnectTool.onPointerDown!(pointer(5, 5), first);
|
|
60
|
+
expect(first.interaction.connect).toBeTruthy();
|
|
61
|
+
expect(second.interaction.connect).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('commits a resize as one command containing origin and size', () => {
|
|
65
|
+
const ctx = context('node-card-a');
|
|
66
|
+
beginHandleDrag(
|
|
67
|
+
{
|
|
68
|
+
layerId: 'node-card-a',
|
|
69
|
+
corner: 'nw',
|
|
70
|
+
startV: { x: 10, y: 20 },
|
|
71
|
+
startBounds: { x: 10, y: 20, width: 100, height: 50 },
|
|
72
|
+
},
|
|
73
|
+
ctx.interaction,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
SelectTool.onPointerMove!(pointer(0, 0), ctx);
|
|
77
|
+
SelectTool.onPointerUp!(pointer(0, 0), ctx);
|
|
78
|
+
|
|
79
|
+
expect(ctx.dispatch).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(ctx.dispatch).toHaveBeenCalledWith({
|
|
81
|
+
kind: 'resizeLayer',
|
|
82
|
+
id: 'node-card-a',
|
|
83
|
+
x: 0,
|
|
84
|
+
y: 0,
|
|
85
|
+
width: 110,
|
|
86
|
+
height: 70,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('creates independent text-toolbar channels', () => {
|
|
91
|
+
const first = createSceneTextChannel();
|
|
92
|
+
const second = createSceneTextChannel();
|
|
93
|
+
const listener = vi.fn();
|
|
94
|
+
second.subscribe(listener);
|
|
95
|
+
first.set({ editor: {} as never, level: 'inline' });
|
|
96
|
+
expect(listener).not.toHaveBeenCalled();
|
|
97
|
+
expect(second.get()).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('namespaces grid and edge marker definitions per canvas', () => {
|
|
101
|
+
const edge = { id: 'e', source: 'a', target: 'b', endMarker: 'arrow' as const };
|
|
102
|
+
const nodes = [
|
|
103
|
+
{ id: 'a', x: 0, y: 0, width: 10, height: 10, cx: 5, cy: 5 },
|
|
104
|
+
{ id: 'b', x: 30, y: 0, width: 10, height: 10, cx: 35, cy: 5 },
|
|
105
|
+
];
|
|
106
|
+
const { container } = render(
|
|
107
|
+
<>
|
|
108
|
+
<SceneViewport width={100} height={100} transform={{ tx: 0, ty: 0, scale: 1 }}>
|
|
109
|
+
<DiagramEdges nodes={nodes} edges={[edge]} />
|
|
110
|
+
</SceneViewport>
|
|
111
|
+
<SceneViewport width={100} height={100} transform={{ tx: 0, ty: 0, scale: 1 }}>
|
|
112
|
+
<DiagramEdges nodes={nodes} edges={[edge]} />
|
|
113
|
+
</SceneViewport>
|
|
114
|
+
</>,
|
|
115
|
+
);
|
|
116
|
+
const ids = Array.from(container.querySelectorAll('[id]')).map((node) => node.id);
|
|
117
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
118
|
+
});
|
|
119
|
+
});
|