@bendyline/squisq-editor-react 1.6.1 → 2.0.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/README.md +12 -3
- package/dist/index.d.ts +1304 -208
- package/dist/index.js +22838 -14494
- 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 +2384 -408
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +48 -17
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +403 -270
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +276 -40
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +466 -93
- package/src/PreviewPanel.tsx +313 -77
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +34 -10
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +355 -134
- package/src/ThemeCustomizerPanel.tsx +30 -15
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +719 -238
- package/src/TransitionPicker.tsx +44 -5
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +167 -92
- 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 +22 -3
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +390 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +53 -15
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +38 -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__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/mediaReferences.test.ts +82 -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 +312 -6
- 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__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +70 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -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 +224 -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 +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -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/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/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- 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 +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/headingTransition.ts +85 -25
- 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 +193 -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/mediaReferences.ts +299 -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/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +101 -25
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +11 -4
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- 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 +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +24 -9
- package/src/scene/layers/edgeGeometry.ts +31 -4
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +145 -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 +126 -28
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +10 -0
- package/src/scene/tools/SelectTool.ts +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +1566 -219
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -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 +360 -0
- package/src/timeline/TimelineEditorWidget.tsx +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +84 -49
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -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 -270
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -28,6 +28,8 @@ interface SceneSelectionProps {
|
|
|
28
28
|
} | null;
|
|
29
29
|
/** Fired when the user presses a corner/edge handle. */
|
|
30
30
|
onHandlePointerDown?: (e: React.PointerEvent<SVGElement>, corner: ResizeCorner) => void;
|
|
31
|
+
/** Whether to render resize handles in addition to the selection outline. */
|
|
32
|
+
showHandles?: boolean;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const HANDLE_SIZE = 8;
|
|
@@ -38,6 +40,7 @@ export function SceneSelection({
|
|
|
38
40
|
liveOffset,
|
|
39
41
|
liveResize,
|
|
40
42
|
onHandlePointerDown,
|
|
43
|
+
showHandles = true,
|
|
41
44
|
}: SceneSelectionProps) {
|
|
42
45
|
if (selection.size === 0) return null;
|
|
43
46
|
const selected = hitItems.filter((it) => selection.has(it.id));
|
|
@@ -84,21 +87,22 @@ export function SceneSelection({
|
|
|
84
87
|
className="squisq-scene-selection-outline"
|
|
85
88
|
pointerEvents="none"
|
|
86
89
|
/>
|
|
87
|
-
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
{showHandles &&
|
|
91
|
+
corners.map(({ corner, cx, cy, cursor }) => (
|
|
92
|
+
<rect
|
|
93
|
+
key={corner}
|
|
94
|
+
x={cx - HANDLE_SIZE / 2}
|
|
95
|
+
y={cy - HANDLE_SIZE / 2}
|
|
96
|
+
width={HANDLE_SIZE}
|
|
97
|
+
height={HANDLE_SIZE}
|
|
98
|
+
className="squisq-scene-selection-handle"
|
|
99
|
+
style={{ cursor }}
|
|
100
|
+
onPointerDown={
|
|
101
|
+
onHandlePointerDown ? (e) => onHandlePointerDown(e, corner) : undefined
|
|
102
|
+
}
|
|
103
|
+
data-corner={corner}
|
|
104
|
+
/>
|
|
105
|
+
))}
|
|
102
106
|
</g>
|
|
103
107
|
);
|
|
104
108
|
})}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SceneSideToolbar — placement wrapper for a `SceneBlockToolbar` beside an
|
|
3
|
+
* *inline* diagram / drawing / layout canvas.
|
|
4
|
+
*
|
|
5
|
+
* The toolbar normally floats as an always-open vertical column in the page's
|
|
6
|
+
* right gutter (`.squisq-scene-side-toolbar`, positioned at `left: 100%`). That
|
|
7
|
+
* only works when the gutter is wide enough to hold the 180px column; on a
|
|
8
|
+
* narrow editor — or in a host whose page fills most of the width — the column
|
|
9
|
+
* overhangs the editor's clipping edge and gets cut off.
|
|
10
|
+
*
|
|
11
|
+
* So we measure the real gutter (the gap between the canvas's right edge and
|
|
12
|
+
* the nearest horizontally-clipping ancestor) rather than guessing from a
|
|
13
|
+
* viewport breakpoint, since page width / centering / host chrome all move it:
|
|
14
|
+
* • Fits — render the column as-is (the common, wider case).
|
|
15
|
+
* • Clips — render the toolbar as a compact horizontal bar in normal flow
|
|
16
|
+
* above the canvas (`.squisq-scene-inline-toolbar`). It stays visible and
|
|
17
|
+
* is never clipped; it just costs a small strip of vertical space.
|
|
18
|
+
*
|
|
19
|
+
* A normal-flow bar is used rather than an on-demand overlay popover because
|
|
20
|
+
* absolutely-positioned content overlapping the React Flow canvas fails to
|
|
21
|
+
* paint in some hosts (the diagram's own maximize overlay hits the same wall),
|
|
22
|
+
* whereas in-flow content always renders.
|
|
23
|
+
*
|
|
24
|
+
* Only the inline placement adapts. Maximized mode has a full screen to reserve
|
|
25
|
+
* a static right column, so the widgets render the bare
|
|
26
|
+
* `.squisq-scene-side-toolbar` there and never mount this wrapper.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useLayoutEffect, useRef, useState, type ReactNode } from 'react';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Horizontal room the gutter must have to host the open column, kept in sync
|
|
33
|
+
* with `.squisq-scene-side-toolbar` in scene.css (12px `margin-left` + 180px
|
|
34
|
+
* `width`) plus a few px of breathing room so we fall back to the bar just
|
|
35
|
+
* before the column would touch the clip edge.
|
|
36
|
+
*/
|
|
37
|
+
const SIDE_TOOLBAR_FIT_PX = 12 + 180 + 4;
|
|
38
|
+
|
|
39
|
+
interface SceneSideToolbarProps {
|
|
40
|
+
/** The `SceneBlockToolbar` element built by the host widget. */
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function SceneSideToolbar({ children }: SceneSideToolbarProps) {
|
|
45
|
+
const wrapRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
47
|
+
|
|
48
|
+
// Measure whether the right gutter can host the column. The column is
|
|
49
|
+
// absolutely positioned at the shell's right edge, so its clipping depends on
|
|
50
|
+
// `shellRight` vs. the nearest ancestor that clips horizontally — recomputed
|
|
51
|
+
// whenever the editor body resizes (window resize, outline toggle, etc.).
|
|
52
|
+
// Switching to the in-flow bar changes shell *height*, not `shellRight`, so
|
|
53
|
+
// there's no measure↔collapse loop.
|
|
54
|
+
useLayoutEffect(() => {
|
|
55
|
+
const wrap = wrapRef.current;
|
|
56
|
+
if (!wrap) return;
|
|
57
|
+
const shell = wrap.closest('.squisq-scene-shell') as HTMLElement | null;
|
|
58
|
+
const clip = wrap.closest('.squisq-editor-content') as HTMLElement | null;
|
|
59
|
+
const measure = () => {
|
|
60
|
+
if (!shell) return;
|
|
61
|
+
const shellRight = shell.getBoundingClientRect().right;
|
|
62
|
+
const clipRight = clip
|
|
63
|
+
? clip.getBoundingClientRect().right
|
|
64
|
+
: document.documentElement.clientWidth;
|
|
65
|
+
setCollapsed(clipRight - shellRight < SIDE_TOOLBAR_FIT_PX);
|
|
66
|
+
};
|
|
67
|
+
measure();
|
|
68
|
+
const ro = new ResizeObserver(measure);
|
|
69
|
+
const target = clip ?? shell;
|
|
70
|
+
if (target) ro.observe(target);
|
|
71
|
+
if (shell && shell !== target) ro.observe(shell);
|
|
72
|
+
window.addEventListener('resize', measure);
|
|
73
|
+
return () => {
|
|
74
|
+
ro.disconnect();
|
|
75
|
+
window.removeEventListener('resize', measure);
|
|
76
|
+
};
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
ref={wrapRef}
|
|
82
|
+
className={collapsed ? 'squisq-scene-inline-toolbar' : 'squisq-scene-side-toolbar'}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default SceneSideToolbar;
|
|
@@ -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,93 @@
|
|
|
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, 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('creates independent text-toolbar channels', () => {
|
|
65
|
+
const first = createSceneTextChannel();
|
|
66
|
+
const second = createSceneTextChannel();
|
|
67
|
+
const listener = vi.fn();
|
|
68
|
+
second.subscribe(listener);
|
|
69
|
+
first.set({ editor: {} as never, level: 'inline' });
|
|
70
|
+
expect(listener).not.toHaveBeenCalled();
|
|
71
|
+
expect(second.get()).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('namespaces grid and edge marker definitions per canvas', () => {
|
|
75
|
+
const edge = { id: 'e', source: 'a', target: 'b', endMarker: 'arrow' as const };
|
|
76
|
+
const nodes = [
|
|
77
|
+
{ id: 'a', x: 0, y: 0, width: 10, height: 10, cx: 5, cy: 5 },
|
|
78
|
+
{ id: 'b', x: 30, y: 0, width: 10, height: 10, cx: 35, cy: 5 },
|
|
79
|
+
];
|
|
80
|
+
const { container } = render(
|
|
81
|
+
<>
|
|
82
|
+
<SceneViewport width={100} height={100} transform={{ tx: 0, ty: 0, scale: 1 }}>
|
|
83
|
+
<DiagramEdges nodes={nodes} edges={[edge]} />
|
|
84
|
+
</SceneViewport>
|
|
85
|
+
<SceneViewport width={100} height={100} transform={{ tx: 0, ty: 0, scale: 1 }}>
|
|
86
|
+
<DiagramEdges nodes={nodes} edges={[edge]} />
|
|
87
|
+
</SceneViewport>
|
|
88
|
+
</>,
|
|
89
|
+
);
|
|
90
|
+
const ids = Array.from(container.querySelectorAll('[id]')).map((node) => node.id);
|
|
91
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -10,26 +10,15 @@
|
|
|
10
10
|
* underlying heading id when dispatching writes.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { Editor } from '@tiptap/react';
|
|
14
13
|
import type { Layer } from '@bendyline/squisq/schemas';
|
|
15
|
-
import type {
|
|
14
|
+
import type { SceneEdge } from '../commands/SceneCommand';
|
|
16
15
|
import {
|
|
17
16
|
nodesToCardLayers,
|
|
18
|
-
nodeIdFromCardLayerId,
|
|
19
17
|
NODE_WIDTH,
|
|
20
18
|
NODE_HEIGHT,
|
|
21
19
|
type DiagramNodeDescriptor,
|
|
22
20
|
} from '../layers/nodeCard';
|
|
23
|
-
import {
|
|
24
|
-
moveNode,
|
|
25
|
-
addConnection,
|
|
26
|
-
removeConnection,
|
|
27
|
-
renameNode,
|
|
28
|
-
addNode,
|
|
29
|
-
removeNode,
|
|
30
|
-
listDiagramChildren,
|
|
31
|
-
} from '../../diagram/diagramCommands';
|
|
32
|
-
import type { DiagramRFNode, DiagramRFEdge } from '../../diagram/useDiagramData';
|
|
21
|
+
import type { DiagramNode, DiagramEdge } from '../../diagram/types';
|
|
33
22
|
|
|
34
23
|
export interface DiagramSceneData {
|
|
35
24
|
layers: Layer[];
|
|
@@ -44,8 +33,8 @@ export interface DiagramSceneData {
|
|
|
44
33
|
* shape so the existing data hook can stay unchanged during the swap.
|
|
45
34
|
*/
|
|
46
35
|
export function buildDiagramScene(
|
|
47
|
-
nodes: readonly
|
|
48
|
-
edges: readonly
|
|
36
|
+
nodes: readonly DiagramNode[],
|
|
37
|
+
edges: readonly DiagramEdge[],
|
|
49
38
|
): DiagramSceneData {
|
|
50
39
|
const descriptors: DiagramNodeDescriptor[] = nodes.map((n) => ({
|
|
51
40
|
id: n.id,
|
|
@@ -54,6 +43,7 @@ export function buildDiagramScene(
|
|
|
54
43
|
y: n.position.y,
|
|
55
44
|
...(n.width != null ? { width: n.width } : {}),
|
|
56
45
|
...(n.height != null ? { height: n.height } : {}),
|
|
46
|
+
...(n.kind === 'container' ? { kind: 'container' as const } : {}),
|
|
57
47
|
}));
|
|
58
48
|
const layers = nodesToCardLayers(descriptors);
|
|
59
49
|
const sceneEdges: SceneEdge[] = edges.map((e) => ({
|
|
@@ -61,6 +51,10 @@ export function buildDiagramScene(
|
|
|
61
51
|
source: e.source,
|
|
62
52
|
target: e.target,
|
|
63
53
|
...(e.label ? { label: e.label } : {}),
|
|
54
|
+
...(e.directed === false ? { endMarker: 'none' as const } : {}),
|
|
55
|
+
...(e.sourceAnchor ? { sourceAnchor: e.sourceAnchor } : {}),
|
|
56
|
+
...(e.targetAnchor ? { targetAnchor: e.targetAnchor } : {}),
|
|
57
|
+
...(e.routing ? { routing: e.routing } : {}),
|
|
64
58
|
}));
|
|
65
59
|
return { layers, edges: sceneEdges, nodes: descriptors };
|
|
66
60
|
}
|
|
@@ -80,89 +74,6 @@ export function nodeCenter(node: DiagramNodeDescriptor): { x: number; y: number
|
|
|
80
74
|
return { x: node.x + NODE_WIDTH / 2, y: node.y + NODE_HEIGHT / 2 };
|
|
81
75
|
}
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* given (editor, parentPos) tuple.
|
|
87
|
-
*/
|
|
88
|
-
export function makeDiagramDispatch(
|
|
89
|
-
editor: Editor,
|
|
90
|
-
parentPos: number,
|
|
91
|
-
): (cmd: SceneCommand) => void {
|
|
92
|
-
return (cmd: SceneCommand) => {
|
|
93
|
-
switch (cmd.kind) {
|
|
94
|
-
case 'moveLayer': {
|
|
95
|
-
const nodeId = nodeIdFromCardLayerId(cmd.id);
|
|
96
|
-
if (!nodeId) return;
|
|
97
|
-
// Only the card layer carries the node's position; ignore label
|
|
98
|
-
// moves (they'd duplicate the same write).
|
|
99
|
-
if (!cmd.id.startsWith('node-card-')) return;
|
|
100
|
-
moveNode(editor, parentPos, nodeId, cmd.x, cmd.y);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
case 'addEdge':
|
|
104
|
-
addConnection(editor, parentPos, cmd.source, cmd.target, cmd.type);
|
|
105
|
-
return;
|
|
106
|
-
case 'removeEdge':
|
|
107
|
-
removeConnection(editor, parentPos, cmd.source, cmd.target, cmd.type);
|
|
108
|
-
return;
|
|
109
|
-
case 'removeLayer': {
|
|
110
|
-
const nodeId = nodeIdFromCardLayerId(cmd.id);
|
|
111
|
-
if (nodeId) removeNode(editor, parentPos, nodeId);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
case 'renameLayer': {
|
|
115
|
-
const nodeId = nodeIdFromCardLayerId(cmd.id);
|
|
116
|
-
if (nodeId) renameNode(editor, parentPos, nodeId, cmd.label);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
case 'addLayer': {
|
|
120
|
-
// Diagram mode interprets addLayer as "add a new diagram node at
|
|
121
|
-
// the layer's position". Pull (x, y) from the layer's position
|
|
122
|
-
// and synthesize a fresh id.
|
|
123
|
-
const pos = cmd.layer.position;
|
|
124
|
-
const x = typeof pos.x === 'number' ? pos.x : 0;
|
|
125
|
-
const y = typeof pos.y === 'number' ? pos.y : 0;
|
|
126
|
-
const id = nextNodeId(editor, parentPos);
|
|
127
|
-
const label = inferAddedLabel(cmd.layer) ?? `Node`;
|
|
128
|
-
addNode(editor, parentPos, id, label, x, y);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
case 'setLayerAttr': {
|
|
132
|
-
const nodeId = nodeIdFromCardLayerId(cmd.id);
|
|
133
|
-
if (!nodeId) return;
|
|
134
|
-
if (cmd.path === 'content.text' && typeof cmd.value === 'string') {
|
|
135
|
-
renameNode(editor, parentPos, nodeId, cmd.value);
|
|
136
|
-
}
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
case 'resizeLayer':
|
|
140
|
-
// v1: diagram nodes are fixed-size. Ignore resize.
|
|
141
|
-
return;
|
|
142
|
-
case 'setLayerText': {
|
|
143
|
-
const nodeId = nodeIdFromCardLayerId(cmd.id);
|
|
144
|
-
if (nodeId) renameNode(editor, parentPos, nodeId, cmd.text);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// Exhaustiveness check — TS will flag unhandled command kinds.
|
|
149
|
-
const _exhaustive: never = cmd;
|
|
150
|
-
void _exhaustive;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
function inferAddedLabel(layer: Layer): string | null {
|
|
154
|
-
if (layer.type === 'text') return layer.content.text;
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function nextNodeId(editor: Editor, parentPos: number): string {
|
|
160
|
-
const used = new Set(listDiagramChildren(editor, parentPos).map((c) => c.id));
|
|
161
|
-
let i = used.size + 1;
|
|
162
|
-
let id = `node-${i}`;
|
|
163
|
-
while (used.has(id)) {
|
|
164
|
-
i++;
|
|
165
|
-
id = `node-${i}`;
|
|
166
|
-
}
|
|
167
|
-
return id;
|
|
168
|
-
}
|
|
77
|
+
// (The heading-based `makeDiagramDispatch` was removed with the ASCII-fence
|
|
78
|
+
// cutover — canvas commands for diagrams now flow through
|
|
79
|
+
// `asciiDiagram/asciiDiagramCommands.ts` instead of heading mutations.)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* component drive three different editing surfaces.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { Layer } from '@bendyline/squisq/schemas';
|
|
14
|
+
import type { DiagramEdgeAnchor, Layer } from '@bendyline/squisq/schemas';
|
|
15
15
|
|
|
16
16
|
export type SceneCommand =
|
|
17
17
|
/** Commit a layer's new top-left position (in viewport units). */
|
|
@@ -67,4 +67,7 @@ export interface SceneEdge {
|
|
|
67
67
|
dasharray?: string;
|
|
68
68
|
/** Per-edge routing override (else the renderer's variant default). */
|
|
69
69
|
routing?: import('@bendyline/squisq/doc').ConnectorRouting;
|
|
70
|
+
/** Optional authored node-side attachments (used by ASCII-derived diagrams). */
|
|
71
|
+
sourceAnchor?: DiagramEdgeAnchor;
|
|
72
|
+
targetAnchor?: DiagramEdgeAnchor;
|
|
70
73
|
}
|
package/src/scene/index.ts
CHANGED
|
@@ -59,12 +59,7 @@ export {
|
|
|
59
59
|
} from './layers/nodeCard';
|
|
60
60
|
export { DiagramEdges } from './layers/DiagramEdges';
|
|
61
61
|
|
|
62
|
-
export {
|
|
63
|
-
buildDiagramScene,
|
|
64
|
-
makeDiagramDispatch,
|
|
65
|
-
nodeCenter,
|
|
66
|
-
type DiagramSceneData,
|
|
67
|
-
} from './adapters/DiagramAdapter';
|
|
62
|
+
export { buildDiagramScene, nodeCenter, type DiagramSceneData } from './adapters/DiagramAdapter';
|
|
68
63
|
|
|
69
64
|
export {
|
|
70
65
|
useLayoutAdapter,
|