@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
|
@@ -13,32 +13,105 @@
|
|
|
13
13
|
|
|
14
14
|
import { createElement, type JSX } from 'react';
|
|
15
15
|
import type { SceneTool, SceneToolContext } from './SceneTool';
|
|
16
|
-
import { nodeIdFromCardLayerId
|
|
16
|
+
import { nodeIdFromCardLayerId } from '../layers/nodeCard';
|
|
17
|
+
import {
|
|
18
|
+
curvedPath,
|
|
19
|
+
edgeEndpoints,
|
|
20
|
+
snapPointToward,
|
|
21
|
+
type EdgeNodeBox,
|
|
22
|
+
} from '../layers/edgeGeometry';
|
|
17
23
|
|
|
18
24
|
interface ConnectState {
|
|
19
25
|
sourceNodeId: string;
|
|
20
|
-
/** Source node
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
26
|
+
/** Source node/card box in viewport units (cached at drag start). */
|
|
27
|
+
sourceBox: EdgeNodeBox;
|
|
28
|
+
/** Preview start/end in viewport units. */
|
|
29
|
+
start: { x: number; y: number };
|
|
30
|
+
end: { x: number; y: number };
|
|
24
31
|
/** Currently-hovered target node id, if any (for highlight + drop). */
|
|
25
32
|
hoveredTargetId: string | null;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
function getState(ctx: SceneToolContext): ConnectState | null {
|
|
36
|
+
return (ctx.interaction.connect as ConnectState | undefined) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setState(ctx: SceneToolContext, state: ConnectState | null): void {
|
|
40
|
+
ctx.interaction.connect = state ?? undefined;
|
|
41
|
+
}
|
|
29
42
|
|
|
30
43
|
function pointerFromEvent(e: React.PointerEvent): { sx: number; sy: number } {
|
|
31
44
|
const rect = (e.currentTarget as Element).getBoundingClientRect();
|
|
32
45
|
return { sx: e.clientX - rect.left, sy: e.clientY - rect.top };
|
|
33
46
|
}
|
|
34
47
|
|
|
35
|
-
function
|
|
36
|
-
ctx: SceneToolContext,
|
|
37
|
-
nodeId: string,
|
|
38
|
-
): { x: number; y: number } | null {
|
|
48
|
+
function nodeBoxFromCard(ctx: SceneToolContext, nodeId: string): EdgeNodeBox | null {
|
|
39
49
|
const card = ctx.hitItems.find((it) => it.id === `node-card-${nodeId}`);
|
|
40
50
|
if (!card) return null;
|
|
41
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
id: nodeId,
|
|
53
|
+
x: card.bounds.x,
|
|
54
|
+
y: card.bounds.y,
|
|
55
|
+
width: card.bounds.width,
|
|
56
|
+
height: card.bounds.height,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nodeBoxes(ctx: SceneToolContext): EdgeNodeBox[] {
|
|
61
|
+
const boxes: EdgeNodeBox[] = [];
|
|
62
|
+
for (const it of ctx.hitItems) {
|
|
63
|
+
if (!it.id.startsWith('node-card-')) continue;
|
|
64
|
+
const nodeId = nodeIdFromCardLayerId(it.id);
|
|
65
|
+
if (!nodeId) continue;
|
|
66
|
+
boxes.push({
|
|
67
|
+
id: nodeId,
|
|
68
|
+
x: it.bounds.x,
|
|
69
|
+
y: it.bounds.y,
|
|
70
|
+
width: it.bounds.width,
|
|
71
|
+
height: it.bounds.height,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return boxes;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function connectionPoints(box: EdgeNodeBox): Array<{ x: number; y: number }> {
|
|
78
|
+
const width = box.width ?? 0;
|
|
79
|
+
const height = box.height ?? 0;
|
|
80
|
+
const left = box.x;
|
|
81
|
+
const right = box.x + width;
|
|
82
|
+
const top = box.y;
|
|
83
|
+
const bottom = box.y + height;
|
|
84
|
+
const centerX = box.x + width / 2;
|
|
85
|
+
const centerY = box.y + height / 2;
|
|
86
|
+
return [
|
|
87
|
+
{ x: centerX, y: top },
|
|
88
|
+
{ x: right, y: centerY },
|
|
89
|
+
{ x: centerX, y: bottom },
|
|
90
|
+
{ x: left, y: centerY },
|
|
91
|
+
{ x: left, y: top },
|
|
92
|
+
{ x: right, y: top },
|
|
93
|
+
{ x: right, y: bottom },
|
|
94
|
+
{ x: left, y: bottom },
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function updatePreview(
|
|
99
|
+
sourceBox: EdgeNodeBox,
|
|
100
|
+
current: { x: number; y: number },
|
|
101
|
+
targetBox: EdgeNodeBox | null,
|
|
102
|
+
): { start: { x: number; y: number }; end: { x: number; y: number } } {
|
|
103
|
+
if (targetBox) {
|
|
104
|
+
return (
|
|
105
|
+
edgeEndpoints([sourceBox, targetBox], sourceBox.id, targetBox.id) ?? {
|
|
106
|
+
start: current,
|
|
107
|
+
end: current,
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
start: snapPointToward([sourceBox], sourceBox.id, current) ?? current,
|
|
113
|
+
end: current,
|
|
114
|
+
};
|
|
42
115
|
}
|
|
43
116
|
|
|
44
117
|
export const ConnectTool: SceneTool = {
|
|
@@ -46,6 +119,7 @@ export const ConnectTool: SceneTool = {
|
|
|
46
119
|
label: 'Connect',
|
|
47
120
|
cursor: 'crosshair',
|
|
48
121
|
shortcut: 'c',
|
|
122
|
+
hideSelectionHandles: true,
|
|
49
123
|
|
|
50
124
|
onPointerDown(e, ctx) {
|
|
51
125
|
if (e.button !== 0) return;
|
|
@@ -55,49 +129,73 @@ export const ConnectTool: SceneTool = {
|
|
|
55
129
|
if (!hitId) return;
|
|
56
130
|
const nodeId = nodeIdFromCardLayerId(hitId);
|
|
57
131
|
if (!nodeId) return;
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
60
|
-
|
|
132
|
+
const sourceBox = nodeBoxFromCard(ctx, nodeId);
|
|
133
|
+
if (!sourceBox) return;
|
|
134
|
+
const preview = updatePreview(sourceBox, v, null);
|
|
135
|
+
setState(ctx, {
|
|
61
136
|
sourceNodeId: nodeId,
|
|
62
|
-
|
|
63
|
-
|
|
137
|
+
sourceBox,
|
|
138
|
+
start: preview.start,
|
|
139
|
+
end: preview.end,
|
|
64
140
|
hoveredTargetId: null,
|
|
65
|
-
};
|
|
141
|
+
});
|
|
66
142
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
67
143
|
e.stopPropagation();
|
|
68
144
|
},
|
|
69
145
|
|
|
70
146
|
onPointerMove(e, ctx) {
|
|
147
|
+
const state = getState(ctx);
|
|
71
148
|
if (!state) return;
|
|
72
149
|
const { sx, sy } = pointerFromEvent(e);
|
|
73
150
|
const v = ctx.screenToViewport(sx, sy);
|
|
74
|
-
state.current = v;
|
|
75
151
|
const hitId = ctx.hit(v);
|
|
76
152
|
const nodeId = hitId ? nodeIdFromCardLayerId(hitId) : null;
|
|
77
|
-
|
|
153
|
+
const targetId = nodeId && nodeId !== state.sourceNodeId ? nodeId : null;
|
|
154
|
+
const targetBox = targetId ? nodeBoxFromCard(ctx, targetId) : null;
|
|
155
|
+
const preview = updatePreview(state.sourceBox, v, targetBox);
|
|
156
|
+
state.start = preview.start;
|
|
157
|
+
state.end = preview.end;
|
|
158
|
+
state.hoveredTargetId = targetBox?.id ?? null;
|
|
78
159
|
},
|
|
79
160
|
|
|
80
161
|
onPointerUp(e, ctx) {
|
|
162
|
+
const state = getState(ctx);
|
|
81
163
|
if (!state) return;
|
|
82
164
|
const target = state.hoveredTargetId;
|
|
83
165
|
if (target) {
|
|
84
166
|
ctx.dispatch({ kind: 'addEdge', source: state.sourceNodeId, target });
|
|
85
167
|
}
|
|
86
168
|
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
|
87
|
-
|
|
169
|
+
setState(ctx, null);
|
|
88
170
|
},
|
|
89
171
|
|
|
90
|
-
renderOverlay(): JSX.Element | null {
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
172
|
+
renderOverlay(ctx): JSX.Element | null {
|
|
173
|
+
const state = getState(ctx);
|
|
174
|
+
const points = nodeBoxes(ctx).flatMap((box) =>
|
|
175
|
+
connectionPoints(box).map((point, index) =>
|
|
176
|
+
createElement('circle', {
|
|
177
|
+
key: `${box.id}-${index}`,
|
|
178
|
+
cx: point.x,
|
|
179
|
+
cy: point.y,
|
|
180
|
+
r: 4,
|
|
181
|
+
className:
|
|
182
|
+
state?.hoveredTargetId === box.id
|
|
183
|
+
? 'squisq-scene-connection-point squisq-scene-connection-point--active'
|
|
184
|
+
: 'squisq-scene-connection-point',
|
|
185
|
+
}),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (!state) {
|
|
190
|
+
return points.length ? createElement('g', { key: 'connection-points' }, ...points) : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const { start: a, end: b } = state;
|
|
194
|
+
const d = curvedPath(a, b);
|
|
98
195
|
return createElement(
|
|
99
196
|
'g',
|
|
100
197
|
{ key: 'connect-preview' },
|
|
198
|
+
...points,
|
|
101
199
|
createElement('path', { d, className: 'squisq-scene-connect-preview' }),
|
|
102
200
|
// Optional: a dot at the endpoint so the user sees the snap target.
|
|
103
201
|
createElement('circle', {
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
import { createElement, type JSX } from 'react';
|
|
17
17
|
import type { SceneTool, SceneToolContext } from './SceneTool';
|
|
18
18
|
import { shapeIdFromLayerId, isPrimaryShapeLayer } from '../layers/shapeLayers';
|
|
19
|
+
import {
|
|
20
|
+
edgeEndpoints,
|
|
21
|
+
snapPointToward,
|
|
22
|
+
straightPath,
|
|
23
|
+
type EdgeNodeBox,
|
|
24
|
+
} from '../layers/edgeGeometry';
|
|
19
25
|
|
|
20
26
|
/** A grab on the selected connector's endpoint handle. */
|
|
21
27
|
export interface EndpointHit {
|
|
@@ -37,8 +43,9 @@ type DragState =
|
|
|
37
43
|
| {
|
|
38
44
|
mode: 'new';
|
|
39
45
|
sourceId: string;
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
sourceBox: EdgeNodeBox;
|
|
47
|
+
start: { x: number; y: number };
|
|
48
|
+
end: { x: number; y: number };
|
|
42
49
|
hovered: string | null;
|
|
43
50
|
}
|
|
44
51
|
| {
|
|
@@ -46,8 +53,9 @@ type DragState =
|
|
|
46
53
|
connectorId: string;
|
|
47
54
|
end: 'from' | 'to';
|
|
48
55
|
fixedShapeId: string;
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
fixedBox: EdgeNodeBox;
|
|
57
|
+
start: { x: number; y: number };
|
|
58
|
+
endPoint: { x: number; y: number };
|
|
51
59
|
hovered: string | null;
|
|
52
60
|
};
|
|
53
61
|
|
|
@@ -59,10 +67,16 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
59
67
|
return { sx: e.clientX - rect.left, sy: e.clientY - rect.top };
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
function
|
|
70
|
+
function shapeBox(ctx: SceneToolContext, shapeId: string): EdgeNodeBox | null {
|
|
63
71
|
const it = ctx.hitItems.find((h) => h.id === `dshape-${shapeId}`);
|
|
64
72
|
if (!it) return null;
|
|
65
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
id: shapeId,
|
|
75
|
+
x: it.bounds.x,
|
|
76
|
+
y: it.bounds.y,
|
|
77
|
+
width: it.bounds.width,
|
|
78
|
+
height: it.bounds.height,
|
|
79
|
+
};
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
function shapeAt(ctx: SceneToolContext, v: { x: number; y: number }): string | null {
|
|
@@ -71,6 +85,45 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
71
85
|
return shapeIdFromLayerId(hitId);
|
|
72
86
|
}
|
|
73
87
|
|
|
88
|
+
function previewNew(
|
|
89
|
+
sourceBox: EdgeNodeBox,
|
|
90
|
+
current: { x: number; y: number },
|
|
91
|
+
targetBox: EdgeNodeBox | null,
|
|
92
|
+
): { start: { x: number; y: number }; end: { x: number; y: number } } {
|
|
93
|
+
if (targetBox) {
|
|
94
|
+
return (
|
|
95
|
+
edgeEndpoints([sourceBox, targetBox], sourceBox.id, targetBox.id) ?? {
|
|
96
|
+
start: current,
|
|
97
|
+
end: current,
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
start: snapPointToward([sourceBox], sourceBox.id, current) ?? current,
|
|
103
|
+
end: current,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function previewRetarget(
|
|
108
|
+
fixedBox: EdgeNodeBox,
|
|
109
|
+
movedEnd: 'from' | 'to',
|
|
110
|
+
current: { x: number; y: number },
|
|
111
|
+
targetBox: EdgeNodeBox | null,
|
|
112
|
+
): { start: { x: number; y: number }; end: { x: number; y: number } } {
|
|
113
|
+
if (targetBox) {
|
|
114
|
+
const source = movedEnd === 'from' ? targetBox : fixedBox;
|
|
115
|
+
const target = movedEnd === 'from' ? fixedBox : targetBox;
|
|
116
|
+
return (
|
|
117
|
+
edgeEndpoints([source, target], source.id, target.id) ?? {
|
|
118
|
+
start: current,
|
|
119
|
+
end: current,
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const fixed = snapPointToward([fixedBox], fixedBox.id, current) ?? current;
|
|
124
|
+
return movedEnd === 'from' ? { start: current, end: fixed } : { start: fixed, end: current };
|
|
125
|
+
}
|
|
126
|
+
|
|
74
127
|
return {
|
|
75
128
|
id: 'connect',
|
|
76
129
|
label: 'Connect',
|
|
@@ -85,14 +138,17 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
85
138
|
// Endpoint handle of the selected connector → re-target gesture.
|
|
86
139
|
const ep = options.endpointAt?.(v);
|
|
87
140
|
if (ep) {
|
|
88
|
-
const
|
|
141
|
+
const fixedBox = shapeBox(ctx, ep.fixedShapeId);
|
|
142
|
+
if (!fixedBox) return;
|
|
143
|
+
const preview = previewRetarget(fixedBox, ep.end, v, null);
|
|
89
144
|
drag = {
|
|
90
145
|
mode: 'retarget',
|
|
91
146
|
connectorId: ep.connectorId,
|
|
92
147
|
end: ep.end,
|
|
93
148
|
fixedShapeId: ep.fixedShapeId,
|
|
94
|
-
|
|
95
|
-
|
|
149
|
+
fixedBox,
|
|
150
|
+
start: preview.start,
|
|
151
|
+
endPoint: preview.end,
|
|
96
152
|
hovered: null,
|
|
97
153
|
};
|
|
98
154
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
@@ -103,11 +159,15 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
103
159
|
// Otherwise start a new connection from the shape under the pointer.
|
|
104
160
|
const src = shapeAt(ctx, v);
|
|
105
161
|
if (!src) return;
|
|
162
|
+
const sourceBox = shapeBox(ctx, src);
|
|
163
|
+
if (!sourceBox) return;
|
|
164
|
+
const preview = previewNew(sourceBox, v, null);
|
|
106
165
|
drag = {
|
|
107
166
|
mode: 'new',
|
|
108
167
|
sourceId: src,
|
|
109
|
-
|
|
110
|
-
|
|
168
|
+
sourceBox,
|
|
169
|
+
start: preview.start,
|
|
170
|
+
end: preview.end,
|
|
111
171
|
hovered: null,
|
|
112
172
|
};
|
|
113
173
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
@@ -118,10 +178,20 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
118
178
|
if (!drag) return;
|
|
119
179
|
const { sx, sy } = pointer(e);
|
|
120
180
|
const v = ctx.screenToViewport(sx, sy);
|
|
121
|
-
drag.current = v;
|
|
122
181
|
const tgt = shapeAt(ctx, v);
|
|
123
182
|
const exclude = drag.mode === 'new' ? drag.sourceId : drag.fixedShapeId;
|
|
124
|
-
|
|
183
|
+
const targetId = tgt && tgt !== exclude ? tgt : null;
|
|
184
|
+
const targetBox = targetId ? shapeBox(ctx, targetId) : null;
|
|
185
|
+
if (drag.mode === 'new') {
|
|
186
|
+
const preview = previewNew(drag.sourceBox, v, targetBox);
|
|
187
|
+
drag.start = preview.start;
|
|
188
|
+
drag.end = preview.end;
|
|
189
|
+
} else {
|
|
190
|
+
const preview = previewRetarget(drag.fixedBox, drag.end, v, targetBox);
|
|
191
|
+
drag.start = preview.start;
|
|
192
|
+
drag.endPoint = preview.end;
|
|
193
|
+
}
|
|
194
|
+
drag.hovered = targetBox?.id ?? null;
|
|
125
195
|
},
|
|
126
196
|
|
|
127
197
|
onPointerUp(e, ctx) {
|
|
@@ -139,13 +209,13 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
139
209
|
|
|
140
210
|
renderOverlay(): JSX.Element | null {
|
|
141
211
|
if (!drag) return null;
|
|
142
|
-
const a = drag.
|
|
143
|
-
const b = drag.
|
|
212
|
+
const a = drag.start;
|
|
213
|
+
const b = drag.mode === 'new' ? drag.end : drag.endPoint;
|
|
144
214
|
return createElement(
|
|
145
215
|
'g',
|
|
146
216
|
{ key: 'drawing-connect-preview' },
|
|
147
217
|
createElement('path', {
|
|
148
|
-
d:
|
|
218
|
+
d: straightPath(a, b),
|
|
149
219
|
className: 'squisq-scene-connect-preview',
|
|
150
220
|
fill: 'none',
|
|
151
221
|
}),
|
|
@@ -14,7 +14,15 @@ import type { SceneCommand, SceneEdge } from '../commands/SceneCommand';
|
|
|
14
14
|
import type { HitTestable } from '../hooks/useSceneHitTest';
|
|
15
15
|
import type { SceneTransform } from '../hooks/useScenePanZoom';
|
|
16
16
|
|
|
17
|
+
/** Mutable gesture storage owned by one Scene instance. */
|
|
18
|
+
export interface SceneInteractionState {
|
|
19
|
+
selectDrag?: unknown;
|
|
20
|
+
connect?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export interface SceneToolContext {
|
|
24
|
+
/** Gesture state private to the owning Scene instance. */
|
|
25
|
+
interaction: SceneInteractionState;
|
|
18
26
|
/** Current viewport size in viewport units. */
|
|
19
27
|
viewport: { width: number; height: number };
|
|
20
28
|
/** Current pan/zoom transform. */
|
|
@@ -55,6 +63,8 @@ export interface SceneTool {
|
|
|
55
63
|
cursor: string;
|
|
56
64
|
/** Optional keyboard accelerator (single-character, lowercase). */
|
|
57
65
|
shortcut?: string;
|
|
66
|
+
/** Hide resize handles on selected layers while this tool is active. */
|
|
67
|
+
hideSelectionHandles?: boolean;
|
|
58
68
|
/**
|
|
59
69
|
* Optional icon — JSX rendered inside a fixed-size toolbar button. Keep
|
|
60
70
|
* it light (a single SVG path is ideal); the toolbar wrapper handles
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { createElement, type JSX } from 'react';
|
|
21
|
-
import type { SceneTool, SceneToolContext } from './SceneTool';
|
|
21
|
+
import type { SceneInteractionState, SceneTool, SceneToolContext } from './SceneTool';
|
|
22
22
|
import type { SceneCommand } from '../commands/SceneCommand';
|
|
23
23
|
import type { ResizeCorner } from '../SceneSelection';
|
|
24
24
|
|
|
@@ -53,7 +53,13 @@ type DragState = MoveDragState | MarqueeDragState | ResizeDragState;
|
|
|
53
53
|
const DRAG_THRESHOLD_PX = 3;
|
|
54
54
|
const MIN_LAYER_SIZE = 8;
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
function getDragState(interaction: SceneInteractionState): DragState | null {
|
|
57
|
+
return (interaction.selectDrag as DragState | undefined) ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setDragState(interaction: SceneInteractionState, state: DragState | null): void {
|
|
61
|
+
interaction.selectDrag = state ?? undefined;
|
|
62
|
+
}
|
|
57
63
|
|
|
58
64
|
function pointerCoords(e: React.PointerEvent): { sx: number; sy: number } {
|
|
59
65
|
const rect = (e.currentTarget as Element).getBoundingClientRect();
|
|
@@ -90,20 +96,23 @@ function applyResize(
|
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
/** Begin a handle-initiated resize drag. Called by the Scene when a corner is pressed. */
|
|
93
|
-
export function beginHandleDrag(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
export function beginHandleDrag(
|
|
100
|
+
args: {
|
|
101
|
+
layerId: string;
|
|
102
|
+
corner: ResizeCorner;
|
|
103
|
+
startV: { x: number; y: number };
|
|
104
|
+
startBounds: { x: number; y: number; width: number; height: number };
|
|
105
|
+
},
|
|
106
|
+
interaction: SceneInteractionState,
|
|
107
|
+
): void {
|
|
108
|
+
setDragState(interaction, {
|
|
100
109
|
kind: 'resize',
|
|
101
110
|
layerId: args.layerId,
|
|
102
111
|
corner: args.corner,
|
|
103
112
|
startV: args.startV,
|
|
104
113
|
startBounds: args.startBounds,
|
|
105
114
|
currentBounds: args.startBounds,
|
|
106
|
-
};
|
|
115
|
+
});
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
export const SelectTool: SceneTool = {
|
|
@@ -135,29 +144,30 @@ export const SelectTool: SceneTool = {
|
|
|
135
144
|
const origin = getLayerOrigin(ctx, id);
|
|
136
145
|
if (origin) layerStarts.set(id, origin);
|
|
137
146
|
}
|
|
138
|
-
|
|
147
|
+
setDragState(ctx.interaction, {
|
|
139
148
|
kind: 'move',
|
|
140
149
|
startV: v,
|
|
141
150
|
startS: { x: sx, y: sy },
|
|
142
151
|
layerStarts,
|
|
143
152
|
currentV: v,
|
|
144
153
|
movedPastThreshold: false,
|
|
145
|
-
};
|
|
154
|
+
});
|
|
146
155
|
} else {
|
|
147
156
|
if (!e.shiftKey) ctx.setSelection([]);
|
|
148
|
-
|
|
157
|
+
setDragState(ctx.interaction, {
|
|
149
158
|
kind: 'marquee',
|
|
150
159
|
startV: v,
|
|
151
160
|
startS: { x: sx, y: sy },
|
|
152
161
|
currentV: v,
|
|
153
162
|
movedPastThreshold: false,
|
|
154
|
-
};
|
|
163
|
+
});
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
158
167
|
},
|
|
159
168
|
|
|
160
169
|
onPointerMove(e, ctx) {
|
|
170
|
+
const dragState = getDragState(ctx.interaction);
|
|
161
171
|
if (!dragState) return;
|
|
162
172
|
const { sx, sy } = pointerCoords(e);
|
|
163
173
|
const v = ctx.screenToViewport(sx, sy);
|
|
@@ -182,6 +192,7 @@ export const SelectTool: SceneTool = {
|
|
|
182
192
|
},
|
|
183
193
|
|
|
184
194
|
onPointerUp(e, ctx) {
|
|
195
|
+
const dragState = getDragState(ctx.interaction);
|
|
185
196
|
if (!dragState) return;
|
|
186
197
|
if (
|
|
187
198
|
dragState.kind === 'move' &&
|
|
@@ -208,7 +219,7 @@ export const SelectTool: SceneTool = {
|
|
|
208
219
|
ctx.dispatch({ kind: 'resizeLayer', id: dragState.layerId, width, height });
|
|
209
220
|
}
|
|
210
221
|
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
|
211
|
-
|
|
222
|
+
setDragState(ctx.interaction, null);
|
|
212
223
|
},
|
|
213
224
|
|
|
214
225
|
onDoubleClick(e, ctx) {
|
|
@@ -232,7 +243,8 @@ export const SelectTool: SceneTool = {
|
|
|
232
243
|
}
|
|
233
244
|
},
|
|
234
245
|
|
|
235
|
-
renderOverlay(): JSX.Element | null {
|
|
246
|
+
renderOverlay(ctx): JSX.Element | null {
|
|
247
|
+
const dragState = getDragState(ctx.interaction);
|
|
236
248
|
if (!dragState || dragState.kind !== 'marquee' || !dragState.movedPastThreshold) return null;
|
|
237
249
|
const box = boxFromCorners(dragState.startV, dragState.currentV);
|
|
238
250
|
return createElement('rect', {
|
|
@@ -247,7 +259,10 @@ export const SelectTool: SceneTool = {
|
|
|
247
259
|
};
|
|
248
260
|
|
|
249
261
|
/** Return the active move-drag offset (viewport units). Null if no drag, or if the drag is a marquee/resize. */
|
|
250
|
-
export function getActiveMoveOffset(
|
|
262
|
+
export function getActiveMoveOffset(
|
|
263
|
+
interaction: SceneInteractionState,
|
|
264
|
+
): { dx: number; dy: number } | null {
|
|
265
|
+
const dragState = getDragState(interaction);
|
|
251
266
|
if (!dragState || dragState.kind !== 'move' || !dragState.movedPastThreshold) return null;
|
|
252
267
|
return {
|
|
253
268
|
dx: dragState.currentV.x - dragState.startV.x,
|
|
@@ -256,10 +271,11 @@ export function getActiveMoveOffset(): { dx: number; dy: number } | null {
|
|
|
256
271
|
}
|
|
257
272
|
|
|
258
273
|
/** Return the live bounds for the layer currently being resized. */
|
|
259
|
-
export function getActiveResize(): {
|
|
274
|
+
export function getActiveResize(interaction: SceneInteractionState): {
|
|
260
275
|
layerId: string;
|
|
261
276
|
bounds: { x: number; y: number; width: number; height: number };
|
|
262
277
|
} | null {
|
|
278
|
+
const dragState = getDragState(interaction);
|
|
263
279
|
if (!dragState || dragState.kind !== 'resize') return null;
|
|
264
280
|
return { layerId: dragState.layerId, bounds: dragState.currentBounds };
|
|
265
281
|
}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* ShapeTool — drag to draw a rectangle or circle as a new ShapeLayer.
|
|
3
3
|
*
|
|
4
4
|
* Exposed as a factory so a host can pre-bind the shape kind and
|
|
5
|
-
* stroke/fill defaults. Returns a fresh SceneTool instance
|
|
6
|
-
*
|
|
7
|
-
* sharing module-level drag state.
|
|
5
|
+
* stroke/fill defaults. Returns a fresh SceneTool instance so hosts can bind
|
|
6
|
+
* independent tool identities and defaults; gesture state belongs to Scene.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { createElement, type JSX } from 'react';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* ASCII diagram fences in the WYSIWYG editor.
|
|
2
|
+
*
|
|
3
|
+
* A qualifying code fence is hidden behind its interactive canvas
|
|
4
|
+
* (AsciiDiagramWidget); toggling "Source" on the widget swaps the hide
|
|
5
|
+
* class for a monospace, non-wrapping view of the raw art. */
|
|
6
|
+
|
|
7
|
+
.squisq-wysiwyg-editor pre.squisq-ascii-fence-hidden {
|
|
8
|
+
display: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.squisq-wysiwyg-editor pre.squisq-ascii-fence-source {
|
|
12
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
13
|
+
/* prosemirror.css sets `.ProseMirror pre { white-space: pre-wrap }`,
|
|
14
|
+
which soft-wraps and visually scrambles column-aligned art. */
|
|
15
|
+
white-space: pre;
|
|
16
|
+
overflow-x: auto;
|
|
17
|
+
line-height: 1.2;
|
|
18
|
+
tab-size: 4;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.squisq-ascii-diagram-widget-host {
|
|
22
|
+
margin: 0.75rem 0 1.25rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.squisq-ascii-diagram-warnings {
|
|
26
|
+
margin-top: 4px;
|
|
27
|
+
font-size: 12px;
|
|
28
|
+
color: var(--squisq-theme-text-muted, #6b7280);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.squisq-ascii-diagram-warnings summary {
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
user-select: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.squisq-ascii-diagram-warnings ul {
|
|
37
|
+
margin: 4px 0 0;
|
|
38
|
+
padding-left: 18px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* "Repair as diagram" affordance on broken box-art fences. The fence stays
|
|
42
|
+
* visible above this banner; clicking rewrites it to clean diagram art. */
|
|
43
|
+
.squisq-repair-banner {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 10px;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
margin: 4px 0 1rem;
|
|
49
|
+
padding: 8px 12px;
|
|
50
|
+
border: 1px dashed var(--squisq-border, #d1d5db);
|
|
51
|
+
border-radius: 8px;
|
|
52
|
+
background: var(--squisq-input-bg, #f9fafb);
|
|
53
|
+
font-size: 13px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.squisq-repair-banner-text {
|
|
57
|
+
color: var(--squisq-text-muted, #6b7280);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.squisq-repair-banner-btn {
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
border: 1px solid var(--squisq-accent, #6366f1);
|
|
63
|
+
border-radius: 6px;
|
|
64
|
+
background: var(--squisq-accent, #6366f1);
|
|
65
|
+
color: #fff;
|
|
66
|
+
padding: 4px 12px;
|
|
67
|
+
font-size: 13px;
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
line-height: 1.4;
|
|
70
|
+
white-space: nowrap;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.squisq-repair-banner-btn:hover {
|
|
74
|
+
filter: brightness(1.05);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.squisq-repair-banner-btn:active {
|
|
78
|
+
filter: brightness(0.95);
|
|
79
|
+
}
|