@bendyline/squisq-editor-react 1.6.2 → 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 +1229 -179
- package/dist/index.js +18017 -11231
- 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 +1471 -245
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +348 -260
- 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 +309 -69
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- 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 +202 -41
- 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__/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 +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__/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 +218 -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__/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 +26 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +23 -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 +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 +191 -19
- 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/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 +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 +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 +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 +706 -120
- 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 +12 -5
- 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 +24 -16
- 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 -272
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -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,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* drag).
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { useId } from 'react';
|
|
16
17
|
import type { MarkerStyle } from '@bendyline/squisq/schemas';
|
|
17
18
|
import { connectorPath, markerPath } from '@bendyline/squisq/doc';
|
|
18
19
|
import type { SceneEdge } from '../commands/SceneCommand';
|
|
@@ -33,8 +34,8 @@ interface DiagramEdgesProps {
|
|
|
33
34
|
/** Marker styles that carry a glyph (everything except 'none'). */
|
|
34
35
|
const MARKER_STYLES: MarkerStyle[] = ['arrow', 'open', 'diamond', 'circle', 'square'];
|
|
35
36
|
|
|
36
|
-
function markerId(style: MarkerStyle, dir: 'start' | 'end'): string {
|
|
37
|
-
return
|
|
37
|
+
function markerId(prefix: string, style: MarkerStyle, dir: 'start' | 'end'): string {
|
|
38
|
+
return `${prefix}-squisq-edge-${style}-${dir}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function DiagramEdges({
|
|
@@ -45,6 +46,7 @@ export function DiagramEdges({
|
|
|
45
46
|
onEdgeClick,
|
|
46
47
|
}: DiagramEdgesProps) {
|
|
47
48
|
const boxById = boxesOf(nodes);
|
|
49
|
+
const markerPrefix = useId().replace(/:/g, '');
|
|
48
50
|
const defaultRouting = variant === 'straight' ? 'straight' : 'curved';
|
|
49
51
|
|
|
50
52
|
return (
|
|
@@ -56,8 +58,8 @@ export function DiagramEdges({
|
|
|
56
58
|
if (!mp) return null;
|
|
57
59
|
return (
|
|
58
60
|
<marker
|
|
59
|
-
key={markerId(style, dir)}
|
|
60
|
-
id={markerId(style, dir)}
|
|
61
|
+
key={markerId(markerPrefix, style, dir)}
|
|
62
|
+
id={markerId(markerPrefix, style, dir)}
|
|
61
63
|
viewBox="0 0 10 10"
|
|
62
64
|
refX={dir === 'end' ? 9 : 1}
|
|
63
65
|
refY={5}
|
|
@@ -81,7 +83,13 @@ export function DiagramEdges({
|
|
|
81
83
|
const a = boxById.get(edge.source);
|
|
82
84
|
const b = boxById.get(edge.target);
|
|
83
85
|
if (!a || !b) return null;
|
|
84
|
-
const snapped = edgeEndpoints(
|
|
86
|
+
const snapped = edgeEndpoints(
|
|
87
|
+
nodes,
|
|
88
|
+
edge.source,
|
|
89
|
+
edge.target,
|
|
90
|
+
edge.sourceAnchor,
|
|
91
|
+
edge.targetAnchor,
|
|
92
|
+
);
|
|
85
93
|
if (!snapped) return null;
|
|
86
94
|
const { start, end } = snapped;
|
|
87
95
|
const d = connectorPath(edge.routing ?? defaultRouting, start, end);
|
|
@@ -99,9 +107,15 @@ export function DiagramEdges({
|
|
|
99
107
|
className="squisq-scene-edge-path"
|
|
100
108
|
strokeDasharray={edge.dasharray}
|
|
101
109
|
markerStart={
|
|
102
|
-
startMarker !== 'none'
|
|
110
|
+
startMarker !== 'none'
|
|
111
|
+
? `url(#${markerId(markerPrefix, startMarker, 'start')})`
|
|
112
|
+
: undefined
|
|
113
|
+
}
|
|
114
|
+
markerEnd={
|
|
115
|
+
endMarker !== 'none'
|
|
116
|
+
? `url(#${markerId(markerPrefix, endMarker, 'end')})`
|
|
117
|
+
: undefined
|
|
103
118
|
}
|
|
104
|
-
markerEnd={endMarker !== 'none' ? `url(#${markerId(endMarker, 'end')})` : undefined}
|
|
105
119
|
/>
|
|
106
120
|
{edge.label ? (
|
|
107
121
|
<text
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { NODE_WIDTH, NODE_HEIGHT } from './nodeCard';
|
|
11
11
|
import {
|
|
12
|
+
anchorPoint,
|
|
12
13
|
nearestSnapPoint as nearestCoreSnapPoint,
|
|
13
14
|
snapEndpoints as coreSnapEndpoints,
|
|
15
|
+
type ConnectorAnchor,
|
|
14
16
|
type ConnectorSnapPoint,
|
|
15
17
|
} from '@bendyline/squisq/doc';
|
|
16
18
|
|
|
@@ -77,12 +79,18 @@ export function edgeEndpoints(
|
|
|
77
79
|
nodes: readonly EdgeNodeBox[],
|
|
78
80
|
source: string,
|
|
79
81
|
target: string,
|
|
82
|
+
sourceAnchor?: ConnectorAnchor,
|
|
83
|
+
targetAnchor?: ConnectorAnchor,
|
|
80
84
|
): { start: ConnectorSnapPoint; end: ConnectorSnapPoint } | null {
|
|
81
85
|
const boxes = boxesOf(nodes);
|
|
82
86
|
const a = boxes.get(source);
|
|
83
87
|
const b = boxes.get(target);
|
|
84
88
|
if (!a || !b) return null;
|
|
85
|
-
|
|
89
|
+
const snapped = coreSnapEndpoints(a, b);
|
|
90
|
+
return {
|
|
91
|
+
start: sourceAnchor ? anchorPoint(a, sourceAnchor) : snapped.start,
|
|
92
|
+
end: targetAnchor ? anchorPoint(b, targetAnchor) : snapped.end,
|
|
93
|
+
};
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
export function straightPath(a: { x: number; y: number }, b: { x: number; y: number }): string {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { ShapeLayer, TextLayer, Layer } from '@bendyline/squisq/schemas';
|
|
14
|
+
import { DIAGRAM_LABEL_LINE_HEIGHT, fitDiagramLabel } from '@bendyline/squisq/doc';
|
|
14
15
|
|
|
15
16
|
export const NODE_WIDTH = 180;
|
|
16
17
|
export const NODE_HEIGHT = 64;
|
|
@@ -34,6 +35,12 @@ export interface DiagramNodeDescriptor {
|
|
|
34
35
|
stroke?: string;
|
|
35
36
|
/** Optional shape: 'rect' (default) or 'pill' (fully rounded). */
|
|
36
37
|
shape?: 'rect' | 'pill';
|
|
38
|
+
/**
|
|
39
|
+
* 'container' renders as a background grouping card: translucent fill,
|
|
40
|
+
* muted stroke, label anchored near the top edge instead of centered
|
|
41
|
+
* (children paint over the interior).
|
|
42
|
+
*/
|
|
43
|
+
kind?: 'container';
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
/**
|
|
@@ -45,43 +52,55 @@ export interface DiagramNodeDescriptor {
|
|
|
45
52
|
export function nodeCardLayers(node: DiagramNodeDescriptor): [ShapeLayer, TextLayer] {
|
|
46
53
|
const width = node.width ?? NODE_WIDTH;
|
|
47
54
|
const height = node.height ?? NODE_HEIGHT;
|
|
55
|
+
const isContainer = node.kind === 'container';
|
|
48
56
|
const card: ShapeLayer = {
|
|
49
57
|
id: `node-card-${node.id}`,
|
|
50
58
|
type: 'shape',
|
|
51
59
|
position: { x: node.x, y: node.y, width, height },
|
|
52
60
|
content: {
|
|
53
61
|
shape: 'rect',
|
|
54
|
-
fill: node.fill ?? '#ffffff',
|
|
55
|
-
|
|
62
|
+
fill: node.fill ?? (isContainer ? '#94a3b8' : '#ffffff'),
|
|
63
|
+
...(isContainer ? { fillOpacity: 0.12 } : {}),
|
|
64
|
+
stroke: node.stroke ?? (isContainer ? '#94a3b8' : '#1e293b'),
|
|
56
65
|
strokeWidth: 2,
|
|
57
66
|
borderRadius: node.shape === 'pill' ? height / 2 : 10,
|
|
58
67
|
},
|
|
59
68
|
};
|
|
60
69
|
|
|
61
70
|
// Scale the label font with the node height so resized cards stay
|
|
62
|
-
// legible without the text overflowing.
|
|
63
|
-
|
|
71
|
+
// legible without the text overflowing. Containers keep a fixed, small
|
|
72
|
+
// title size — their height tracks their children, not their label.
|
|
73
|
+
const labelText = isContainer ? (node.label.split('\n')[0] ?? '') : node.label;
|
|
74
|
+
const preferredFontSize = Math.max(12, Math.min(48, Math.round(height * 0.34)));
|
|
75
|
+
const fit = isContainer
|
|
76
|
+
? { fontSize: 16, firstLineOffset: 0, textWidth: width }
|
|
77
|
+
: fitDiagramLabel(labelText, width, height, preferredFontSize);
|
|
78
|
+
const { fontSize } = fit;
|
|
64
79
|
|
|
65
80
|
// Label has no explicit height — that keeps its bounding box
|
|
66
81
|
// unresolvable in `layerBounds`, which in turn keeps it out of the
|
|
67
82
|
// Scene's hit-test. The label rides along visually via `layerFollows`
|
|
68
83
|
// but never wins a pointer-down (the card underneath does), so a
|
|
69
84
|
// drag-on-text moves the card as expected.
|
|
85
|
+
//
|
|
86
|
+
// Container labels anchor near the top edge — the interior belongs to
|
|
87
|
+
// the children painting over it.
|
|
70
88
|
const label: TextLayer = {
|
|
71
89
|
id: `node-label-${node.id}`,
|
|
72
90
|
type: 'text',
|
|
73
91
|
position: {
|
|
74
92
|
x: node.x + width / 2,
|
|
75
|
-
y: node.y + height / 2,
|
|
76
|
-
width,
|
|
93
|
+
y: isContainer ? node.y + fontSize + 8 : node.y + height / 2 + fit.firstLineOffset,
|
|
94
|
+
width: fit.textWidth,
|
|
77
95
|
anchor: 'center',
|
|
78
96
|
},
|
|
79
97
|
content: {
|
|
80
|
-
text:
|
|
98
|
+
text: labelText,
|
|
81
99
|
style: {
|
|
82
100
|
fontSize,
|
|
101
|
+
...(!isContainer ? { lineHeight: DIAGRAM_LABEL_LINE_HEIGHT } : {}),
|
|
83
102
|
fontWeight: 'bold',
|
|
84
|
-
color: node.stroke ?? '#1e293b',
|
|
103
|
+
color: node.stroke ?? (isContainer ? '#64748b' : '#1e293b'),
|
|
85
104
|
textAlign: 'center',
|
|
86
105
|
},
|
|
87
106
|
},
|
package/src/scene/scene.css
CHANGED
|
@@ -37,11 +37,14 @@
|
|
|
37
37
|
|
|
38
38
|
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--shape,
|
|
39
39
|
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--path,
|
|
40
|
-
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--text
|
|
41
|
-
.squisq-editor-shell[data-theme='dark'] .squisq-scene-extras {
|
|
40
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--text {
|
|
42
41
|
filter: invert(1) hue-rotate(180deg) saturate(0.9);
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
/* Scene extras (diagram/drawing connectors) already use the dark-aware
|
|
45
|
+
--squisq-text tokens below. Inverting this group a second time turns the
|
|
46
|
+
intended light strokes and arrowheads nearly black against the canvas. */
|
|
47
|
+
|
|
45
48
|
.squisq-scene-viewport {
|
|
46
49
|
display: block;
|
|
47
50
|
width: 100%;
|
|
@@ -19,7 +19,6 @@ import { layerBounds } from '../hooks/useSceneHitTest';
|
|
|
19
19
|
import type { SceneTransform } from '../hooks/useScenePanZoom';
|
|
20
20
|
import type { SceneTextEditConfig } from './sceneTextConfig';
|
|
21
21
|
import { buildSceneTextExtensions } from './sceneTiptap';
|
|
22
|
-
import { sceneTextChannel } from './sceneTextChannel';
|
|
23
22
|
|
|
24
23
|
interface SceneTextOverlayProps {
|
|
25
24
|
config: SceneTextEditConfig;
|
|
@@ -90,12 +89,13 @@ export function SceneTextOverlay({
|
|
|
90
89
|
|
|
91
90
|
// Publish to the toolbar channel while mounted; clear on unmount.
|
|
92
91
|
useEffect(() => {
|
|
93
|
-
if (!editor) return;
|
|
94
|
-
|
|
92
|
+
if (!editor || !config.channel) return;
|
|
93
|
+
const channel = config.channel;
|
|
94
|
+
channel.set({ editor, level: config.level });
|
|
95
95
|
return () => {
|
|
96
|
-
if (
|
|
96
|
+
if (channel.get()?.editor === editor) channel.set(null);
|
|
97
97
|
};
|
|
98
|
-
}, [editor, config.level]);
|
|
98
|
+
}, [editor, config.level, config.channel]);
|
|
99
99
|
|
|
100
100
|
// Commit when focus leaves the editor — unless it went to the toolbar
|
|
101
101
|
// (the user clicking Bold) or stayed within the overlay.
|