@bendyline/squisq-editor-react 1.6.0 → 1.6.2
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 +57 -10
- package/dist/index.d.ts +476 -105
- package/dist/index.js +8703 -6741
- package/dist/index.js.map +1 -1
- package/dist/styles/fa-brands-400-AHOAZHCU.woff2 +0 -0
- package/dist/styles/fa-regular-400-VRZYIBIZ.woff2 +0 -0
- package/dist/styles/fa-solid-900-MDEYK55F.woff2 +0 -0
- package/dist/styles/fa-v4compatibility-ETEVP6IB.woff2 +0 -0
- package/dist/styles/index.css +14617 -0
- package/package.json +15 -7
- package/src/BlockPropertiesPopover.tsx +23 -7
- package/src/EditorContext.tsx +22 -16
- package/src/EditorShell.tsx +121 -35
- package/src/MediaBin.tsx +171 -28
- package/src/OutlinePanel.tsx +26 -4
- package/src/PreviewControls.tsx +475 -145
- package/src/PreviewPanel.tsx +17 -9
- package/src/RawEditor.tsx +10 -4
- package/src/TemplateAnnotation.ts +22 -7
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +295 -128
- package/src/ThemeCustomizerPanel.tsx +22 -15
- package/src/Toolbar.tsx +547 -217
- package/src/TransitionPicker.tsx +8 -1
- package/src/VersionHistoryPanel.tsx +2 -2
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +45 -3
- package/src/__tests__/buildPreviewDocTransition.test.ts +1 -2
- package/src/__tests__/codeContextSectionView.test.tsx +95 -0
- package/src/__tests__/codeContextZoneManager.test.ts +127 -0
- package/src/__tests__/diffContextSections.test.ts +39 -0
- package/src/__tests__/editorShellCodeContext.test.tsx +86 -0
- package/src/__tests__/editorShellProps.test.tsx +363 -0
- package/src/__tests__/headingTransition.test.ts +59 -9
- package/src/__tests__/imageEditorShell.test.tsx +23 -0
- package/src/__tests__/mediaReferences.test.ts +82 -0
- package/src/__tests__/previewControls.test.tsx +163 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +23 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/tiptapBridge.test.ts +47 -0
- package/src/__tests__/useJsonEditorTokens.test.ts +59 -0
- package/src/__tests__/useMediaRecorder.test.ts +17 -0
- package/src/codeContext/CodeContextSectionView.tsx +124 -0
- package/src/codeContext/CodeContextZoneManager.ts +149 -0
- package/src/codeContext/CodeContextZones.tsx +121 -0
- package/src/codeContext/diffContextSections.ts +38 -0
- package/src/codeContext/types.ts +75 -0
- package/src/diagram/DiagramWidget.tsx +6 -4
- package/src/headingTransition.ts +96 -21
- package/src/index.ts +34 -2
- package/src/jsonEditor/useJsonEditorTokens.ts +13 -43
- package/src/mediaReferences.ts +299 -0
- package/src/recorder/hooks/useMediaRecorder.ts +9 -10
- package/src/scene/Scene.tsx +53 -7
- package/src/scene/SceneBlockWidget.tsx +6 -3
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- package/src/scene/layers/DiagramEdges.tsx +4 -3
- package/src/scene/layers/edgeGeometry.ts +23 -4
- package/src/scene/scene.css +142 -2
- package/src/scene/tools/ConnectTool.ts +113 -24
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +2 -0
- package/src/styles/code-context.css +155 -0
- package/src/styles/editor.css +991 -84
- package/src/styles/index.css +1 -0
- package/src/templateContentPreviewResolver.ts +353 -0
- package/src/tiptapBridge.ts +60 -33
|
@@ -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;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import type { MarkerStyle } from '@bendyline/squisq/schemas';
|
|
17
17
|
import { connectorPath, markerPath } from '@bendyline/squisq/doc';
|
|
18
18
|
import type { SceneEdge } from '../commands/SceneCommand';
|
|
19
|
-
import { boxesOf,
|
|
19
|
+
import { boxesOf, edgeEndpoints } from './edgeGeometry';
|
|
20
20
|
import type { EdgeNodeBox } from './edgeGeometry';
|
|
21
21
|
|
|
22
22
|
interface DiagramEdgesProps {
|
|
@@ -81,8 +81,9 @@ export function DiagramEdges({
|
|
|
81
81
|
const a = boxById.get(edge.source);
|
|
82
82
|
const b = boxById.get(edge.target);
|
|
83
83
|
if (!a || !b) return null;
|
|
84
|
-
const
|
|
85
|
-
|
|
84
|
+
const snapped = edgeEndpoints(nodes, edge.source, edge.target);
|
|
85
|
+
if (!snapped) return null;
|
|
86
|
+
const { start, end } = snapped;
|
|
86
87
|
const d = connectorPath(edge.routing ?? defaultRouting, start, end);
|
|
87
88
|
const startMarker = edge.startMarker ?? 'none';
|
|
88
89
|
const endMarker = edge.endMarker ?? 'arrow';
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { NODE_WIDTH, NODE_HEIGHT } from './nodeCard';
|
|
11
|
+
import {
|
|
12
|
+
nearestSnapPoint as nearestCoreSnapPoint,
|
|
13
|
+
snapEndpoints as coreSnapEndpoints,
|
|
14
|
+
type ConnectorSnapPoint,
|
|
15
|
+
} from '@bendyline/squisq/doc';
|
|
11
16
|
|
|
12
17
|
/** Minimal box an edge needs from a node/shape (id + bounds). */
|
|
13
18
|
export interface EdgeNodeBox {
|
|
@@ -49,21 +54,35 @@ export function edgePoint(from: NodeBox, to: NodeBox): { x: number; y: number }
|
|
|
49
54
|
return { x: from.cx + dx * s, y: from.cy + dy * s };
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
function boxFor(nodes: readonly EdgeNodeBox[], id: string): NodeBox | null {
|
|
58
|
+
return boxesOf(nodes).get(id) ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Nearest stable connector port on the named node/shape to `point`. */
|
|
62
|
+
export function snapPointToward(
|
|
63
|
+
nodes: readonly EdgeNodeBox[],
|
|
64
|
+
id: string,
|
|
65
|
+
point: { x: number; y: number },
|
|
66
|
+
): ConnectorSnapPoint | null {
|
|
67
|
+
const box = boxFor(nodes, id);
|
|
68
|
+
return box ? nearestCoreSnapPoint(box, point) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
/**
|
|
53
|
-
*
|
|
54
|
-
* endpoint shape is missing. Used by the renderer and by
|
|
72
|
+
* Snapped start/end points of the edge `source`→`target`, or null when an
|
|
73
|
+
* endpoint shape is missing. Used by the renderer and by tool hit-testing
|
|
55
74
|
* endpoint handles against the same geometry the renderer draws.
|
|
56
75
|
*/
|
|
57
76
|
export function edgeEndpoints(
|
|
58
77
|
nodes: readonly EdgeNodeBox[],
|
|
59
78
|
source: string,
|
|
60
79
|
target: string,
|
|
61
|
-
): { start:
|
|
80
|
+
): { start: ConnectorSnapPoint; end: ConnectorSnapPoint } | null {
|
|
62
81
|
const boxes = boxesOf(nodes);
|
|
63
82
|
const a = boxes.get(source);
|
|
64
83
|
const b = boxes.get(target);
|
|
65
84
|
if (!a || !b) return null;
|
|
66
|
-
return
|
|
85
|
+
return coreSnapEndpoints(a, b);
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
export function straightPath(a: { x: number; y: number }, b: { x: number; y: number }): string {
|
package/src/scene/scene.css
CHANGED
|
@@ -20,6 +20,28 @@
|
|
|
20
20
|
overflow: hidden;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-shell {
|
|
24
|
+
--squisq-surface: #0f172a;
|
|
25
|
+
--squisq-surface-hover: #1e293b;
|
|
26
|
+
--squisq-border: #475569;
|
|
27
|
+
--squisq-text: #f8fafc;
|
|
28
|
+
--squisq-text-muted: #94a3b8;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-root,
|
|
32
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-inline,
|
|
33
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-diagram-inline {
|
|
34
|
+
background: #0f172a;
|
|
35
|
+
border-color: #475569;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--shape,
|
|
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 {
|
|
42
|
+
filter: invert(1) hue-rotate(180deg) saturate(0.9);
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
.squisq-scene-viewport {
|
|
24
46
|
display: block;
|
|
25
47
|
width: 100%;
|
|
@@ -111,6 +133,18 @@
|
|
|
111
133
|
pointer-events: none;
|
|
112
134
|
}
|
|
113
135
|
|
|
136
|
+
.squisq-scene-connection-point {
|
|
137
|
+
fill: #ffffff;
|
|
138
|
+
stroke: #6366f1;
|
|
139
|
+
stroke-width: 2;
|
|
140
|
+
vector-effect: non-scaling-stroke;
|
|
141
|
+
pointer-events: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.squisq-scene-connection-point--active {
|
|
145
|
+
fill: #6366f1;
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
/* ── Toolbar ──────────────────────────────────────────────── */
|
|
115
149
|
|
|
116
150
|
.squisq-scene-toolbar {
|
|
@@ -261,6 +295,46 @@
|
|
|
261
295
|
color: #b91c1c;
|
|
262
296
|
}
|
|
263
297
|
|
|
298
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-toolbar,
|
|
299
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-block-tools,
|
|
300
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-block-actions,
|
|
301
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-block-props {
|
|
302
|
+
background: #111827;
|
|
303
|
+
border-color: #475569;
|
|
304
|
+
color: #e5e7eb;
|
|
305
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.32);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool,
|
|
309
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action {
|
|
310
|
+
background: #111827;
|
|
311
|
+
color: #e5e7eb;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool:hover,
|
|
315
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action:hover:not(:disabled) {
|
|
316
|
+
background: #1f2937;
|
|
317
|
+
border-color: #64748b;
|
|
318
|
+
color: #f8fafc;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool--active,
|
|
322
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool--active:hover {
|
|
323
|
+
background: #312e81;
|
|
324
|
+
border-color: #818cf8;
|
|
325
|
+
color: #f8fafc;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action--danger {
|
|
329
|
+
color: #fca5a5;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action--danger:hover:not(:disabled) {
|
|
333
|
+
background: #450a0a;
|
|
334
|
+
border-color: #f87171;
|
|
335
|
+
color: #fecaca;
|
|
336
|
+
}
|
|
337
|
+
|
|
264
338
|
/* Maximized overlay: a flex-filling canvas with the toolbar as a right-hand
|
|
265
339
|
column (full screen has room to reserve it). */
|
|
266
340
|
.squisq-scene-block-max {
|
|
@@ -303,8 +377,8 @@
|
|
|
303
377
|
overflow-y: auto;
|
|
304
378
|
}
|
|
305
379
|
|
|
306
|
-
/* Re-flow the (normally horizontal, right-aligned) toolbar into a
|
|
307
|
-
vertical column whose button groups wrap within the 180px
|
|
380
|
+
/* Re-flow the (normally horizontal, right-aligned) gutter toolbar into a
|
|
381
|
+
top-aligned vertical column whose button groups wrap within the 180px. */
|
|
308
382
|
.squisq-scene-side-toolbar .squisq-scene-block-toolbar {
|
|
309
383
|
flex-direction: column;
|
|
310
384
|
align-items: stretch;
|
|
@@ -326,6 +400,23 @@
|
|
|
326
400
|
right: calc(100% + 6px);
|
|
327
401
|
}
|
|
328
402
|
|
|
403
|
+
/* ── Narrow-width fallback bar ─────────────────────────────────
|
|
404
|
+
When SceneSideToolbar measures that the right gutter can't hold the 180px
|
|
405
|
+
column, it renders the same toolbar as an in-flow horizontal bar above the
|
|
406
|
+
canvas instead. Normal flow (not an overlay) so it always paints and is
|
|
407
|
+
never clipped; it just keeps the toolbar's natural horizontal layout and
|
|
408
|
+
wraps on very narrow widths. */
|
|
409
|
+
.squisq-scene-inline-toolbar {
|
|
410
|
+
display: flex;
|
|
411
|
+
margin-bottom: 8px;
|
|
412
|
+
}
|
|
413
|
+
.squisq-scene-inline-toolbar .squisq-scene-block-toolbar {
|
|
414
|
+
flex-wrap: wrap;
|
|
415
|
+
justify-content: flex-start;
|
|
416
|
+
width: 100%;
|
|
417
|
+
margin: 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
329
420
|
/* ── Maximize button ──────────────────────────────────────── */
|
|
330
421
|
|
|
331
422
|
.squisq-scene-maximize-btn {
|
|
@@ -354,6 +445,19 @@
|
|
|
354
445
|
color: #0f172a;
|
|
355
446
|
}
|
|
356
447
|
|
|
448
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-maximize-btn {
|
|
449
|
+
background: #111827;
|
|
450
|
+
border-color: #475569;
|
|
451
|
+
color: #e5e7eb;
|
|
452
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.32);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-maximize-btn:hover {
|
|
456
|
+
background: #1f2937;
|
|
457
|
+
border-color: #64748b;
|
|
458
|
+
color: #f8fafc;
|
|
459
|
+
}
|
|
460
|
+
|
|
357
461
|
/* ── Diagram node card (used by nodeCard.tsx) ─────────────── */
|
|
358
462
|
|
|
359
463
|
.squisq-scene-node-card {
|
|
@@ -502,6 +606,35 @@
|
|
|
502
606
|
font-weight: 700;
|
|
503
607
|
}
|
|
504
608
|
|
|
609
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-palette {
|
|
610
|
+
background: #111827;
|
|
611
|
+
color: #e5e7eb;
|
|
612
|
+
border-color: #475569;
|
|
613
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.42);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-palette-search,
|
|
617
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-select {
|
|
618
|
+
background: #0f172a;
|
|
619
|
+
color: #e5e7eb;
|
|
620
|
+
border-color: #475569;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-palette-item:hover,
|
|
624
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-clear:hover {
|
|
625
|
+
background: #1f2937;
|
|
626
|
+
border-color: #64748b;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-thumb,
|
|
630
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-thumb-line {
|
|
631
|
+
stroke: #e5e7eb;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-thumb-text {
|
|
635
|
+
fill: #e5e7eb;
|
|
636
|
+
}
|
|
637
|
+
|
|
505
638
|
/* Per-selection style controls, inline in the contextual toolbar (Fill /
|
|
506
639
|
Stroke / Color swatches + connector dropdowns) — styled as a pill that
|
|
507
640
|
matches the tools/actions groups. */
|
|
@@ -566,6 +699,13 @@
|
|
|
566
699
|
background: #f1f5f9;
|
|
567
700
|
}
|
|
568
701
|
|
|
702
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-color,
|
|
703
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-clear {
|
|
704
|
+
background: #0f172a;
|
|
705
|
+
color: #e5e7eb;
|
|
706
|
+
border-color: #475569;
|
|
707
|
+
}
|
|
708
|
+
|
|
569
709
|
/* ── Inline widget host (mirrors squisq-diagram-inline) ──── */
|
|
570
710
|
|
|
571
711
|
.squisq-scene-widget-host {
|
|
@@ -13,14 +13,21 @@
|
|
|
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
|
}
|
|
@@ -32,13 +39,73 @@ function pointerFromEvent(e: React.PointerEvent): { sx: number; sy: number } {
|
|
|
32
39
|
return { sx: e.clientX - rect.left, sy: e.clientY - rect.top };
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
function
|
|
36
|
-
ctx: SceneToolContext,
|
|
37
|
-
nodeId: string,
|
|
38
|
-
): { x: number; y: number } | null {
|
|
42
|
+
function nodeBoxFromCard(ctx: SceneToolContext, nodeId: string): EdgeNodeBox | null {
|
|
39
43
|
const card = ctx.hitItems.find((it) => it.id === `node-card-${nodeId}`);
|
|
40
44
|
if (!card) return null;
|
|
41
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
id: nodeId,
|
|
47
|
+
x: card.bounds.x,
|
|
48
|
+
y: card.bounds.y,
|
|
49
|
+
width: card.bounds.width,
|
|
50
|
+
height: card.bounds.height,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nodeBoxes(ctx: SceneToolContext): EdgeNodeBox[] {
|
|
55
|
+
const boxes: EdgeNodeBox[] = [];
|
|
56
|
+
for (const it of ctx.hitItems) {
|
|
57
|
+
if (!it.id.startsWith('node-card-')) continue;
|
|
58
|
+
const nodeId = nodeIdFromCardLayerId(it.id);
|
|
59
|
+
if (!nodeId) continue;
|
|
60
|
+
boxes.push({
|
|
61
|
+
id: nodeId,
|
|
62
|
+
x: it.bounds.x,
|
|
63
|
+
y: it.bounds.y,
|
|
64
|
+
width: it.bounds.width,
|
|
65
|
+
height: it.bounds.height,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return boxes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function connectionPoints(box: EdgeNodeBox): Array<{ x: number; y: number }> {
|
|
72
|
+
const width = box.width ?? 0;
|
|
73
|
+
const height = box.height ?? 0;
|
|
74
|
+
const left = box.x;
|
|
75
|
+
const right = box.x + width;
|
|
76
|
+
const top = box.y;
|
|
77
|
+
const bottom = box.y + height;
|
|
78
|
+
const centerX = box.x + width / 2;
|
|
79
|
+
const centerY = box.y + height / 2;
|
|
80
|
+
return [
|
|
81
|
+
{ x: centerX, y: top },
|
|
82
|
+
{ x: right, y: centerY },
|
|
83
|
+
{ x: centerX, y: bottom },
|
|
84
|
+
{ x: left, y: centerY },
|
|
85
|
+
{ x: left, y: top },
|
|
86
|
+
{ x: right, y: top },
|
|
87
|
+
{ x: right, y: bottom },
|
|
88
|
+
{ x: left, y: bottom },
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function updatePreview(
|
|
93
|
+
sourceBox: EdgeNodeBox,
|
|
94
|
+
current: { x: number; y: number },
|
|
95
|
+
targetBox: EdgeNodeBox | null,
|
|
96
|
+
): { start: { x: number; y: number }; end: { x: number; y: number } } {
|
|
97
|
+
if (targetBox) {
|
|
98
|
+
return (
|
|
99
|
+
edgeEndpoints([sourceBox, targetBox], sourceBox.id, targetBox.id) ?? {
|
|
100
|
+
start: current,
|
|
101
|
+
end: current,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
start: snapPointToward([sourceBox], sourceBox.id, current) ?? current,
|
|
107
|
+
end: current,
|
|
108
|
+
};
|
|
42
109
|
}
|
|
43
110
|
|
|
44
111
|
export const ConnectTool: SceneTool = {
|
|
@@ -46,6 +113,7 @@ export const ConnectTool: SceneTool = {
|
|
|
46
113
|
label: 'Connect',
|
|
47
114
|
cursor: 'crosshair',
|
|
48
115
|
shortcut: 'c',
|
|
116
|
+
hideSelectionHandles: true,
|
|
49
117
|
|
|
50
118
|
onPointerDown(e, ctx) {
|
|
51
119
|
if (e.button !== 0) return;
|
|
@@ -55,12 +123,14 @@ export const ConnectTool: SceneTool = {
|
|
|
55
123
|
if (!hitId) return;
|
|
56
124
|
const nodeId = nodeIdFromCardLayerId(hitId);
|
|
57
125
|
if (!nodeId) return;
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
126
|
+
const sourceBox = nodeBoxFromCard(ctx, nodeId);
|
|
127
|
+
if (!sourceBox) return;
|
|
128
|
+
const preview = updatePreview(sourceBox, v, null);
|
|
60
129
|
state = {
|
|
61
130
|
sourceNodeId: nodeId,
|
|
62
|
-
|
|
63
|
-
|
|
131
|
+
sourceBox,
|
|
132
|
+
start: preview.start,
|
|
133
|
+
end: preview.end,
|
|
64
134
|
hoveredTargetId: null,
|
|
65
135
|
};
|
|
66
136
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
@@ -71,10 +141,14 @@ export const ConnectTool: SceneTool = {
|
|
|
71
141
|
if (!state) return;
|
|
72
142
|
const { sx, sy } = pointerFromEvent(e);
|
|
73
143
|
const v = ctx.screenToViewport(sx, sy);
|
|
74
|
-
state.current = v;
|
|
75
144
|
const hitId = ctx.hit(v);
|
|
76
145
|
const nodeId = hitId ? nodeIdFromCardLayerId(hitId) : null;
|
|
77
|
-
|
|
146
|
+
const targetId = nodeId && nodeId !== state.sourceNodeId ? nodeId : null;
|
|
147
|
+
const targetBox = targetId ? nodeBoxFromCard(ctx, targetId) : null;
|
|
148
|
+
const preview = updatePreview(state.sourceBox, v, targetBox);
|
|
149
|
+
state.start = preview.start;
|
|
150
|
+
state.end = preview.end;
|
|
151
|
+
state.hoveredTargetId = targetBox?.id ?? null;
|
|
78
152
|
},
|
|
79
153
|
|
|
80
154
|
onPointerUp(e, ctx) {
|
|
@@ -87,17 +161,32 @@ export const ConnectTool: SceneTool = {
|
|
|
87
161
|
state = null;
|
|
88
162
|
},
|
|
89
163
|
|
|
90
|
-
renderOverlay(): JSX.Element | null {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
164
|
+
renderOverlay(ctx): JSX.Element | null {
|
|
165
|
+
const points = nodeBoxes(ctx).flatMap((box) =>
|
|
166
|
+
connectionPoints(box).map((point, index) =>
|
|
167
|
+
createElement('circle', {
|
|
168
|
+
key: `${box.id}-${index}`,
|
|
169
|
+
cx: point.x,
|
|
170
|
+
cy: point.y,
|
|
171
|
+
r: 4,
|
|
172
|
+
className:
|
|
173
|
+
state?.hoveredTargetId === box.id
|
|
174
|
+
? 'squisq-scene-connection-point squisq-scene-connection-point--active'
|
|
175
|
+
: 'squisq-scene-connection-point',
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (!state) {
|
|
181
|
+
return points.length ? createElement('g', { key: 'connection-points' }, ...points) : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { start: a, end: b } = state;
|
|
185
|
+
const d = curvedPath(a, b);
|
|
98
186
|
return createElement(
|
|
99
187
|
'g',
|
|
100
188
|
{ key: 'connect-preview' },
|
|
189
|
+
...points,
|
|
101
190
|
createElement('path', { d, className: 'squisq-scene-connect-preview' }),
|
|
102
191
|
// Optional: a dot at the endpoint so the user sees the snap target.
|
|
103
192
|
createElement('circle', {
|