@matchina/viz-svg 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,522 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
+ import { runElkLayout } from "./elk-layout.js";
3
+ import { buildCurvedPath, pathAtT } from "./svg-path.js";
4
+ const V = {
5
+ accent: "var(--matchina-viz-accent, #2dd4bf)",
6
+ bg: "var(--matchina-viz-bg, #0a0f17)",
7
+ node: "var(--matchina-viz-node, rgba(28,38,54,0.95))",
8
+ nodeActive: "var(--matchina-viz-node-active, rgba(20,90,82,0.85))",
9
+ nodeCompound: "var(--matchina-viz-node-compound, rgba(20,28,40,0.7))",
10
+ border: "var(--matchina-viz-border, rgba(148,163,184,0.25))",
11
+ text: "var(--matchina-viz-text, rgba(226,232,240,0.92))",
12
+ textActive: "var(--matchina-viz-text-active, #e6fffb)",
13
+ edge: "var(--matchina-viz-edge, rgba(100,116,139,0.55))",
14
+ labelBg: "var(--matchina-viz-label-bg, rgba(15,23,33,0.95))",
15
+ labelBgActive: "var(--matchina-viz-label-bg-active, rgba(8,47,51,0.95))",
16
+ labelText: "var(--matchina-viz-label-text, rgba(203,213,225,0.82))",
17
+ ctrlBg: "var(--matchina-viz-ctrl-bg, rgba(20,28,40,0.85))",
18
+ ctrlBorder: "var(--matchina-viz-ctrl-border, rgba(148,163,184,0.24))",
19
+ ctrlText: "var(--matchina-viz-ctrl-text, rgba(226,232,240,0.65))"
20
+ };
21
+ function NodeShape({ node, isActive, isAncestor }) {
22
+ const stroke = isActive || isAncestor ? V.accent : V.border;
23
+ const strokeWidth = isActive ? 2 : isAncestor ? 1.5 : 1;
24
+ const fill = node.isCompound ? V.nodeCompound : isActive ? V.nodeActive : V.node;
25
+ const textFill = isActive ? V.textActive : isActive || isAncestor ? V.accent : V.text;
26
+ return /* @__PURE__ */ React.createElement("g", null, /* @__PURE__ */ React.createElement(
27
+ "rect",
28
+ {
29
+ x: node.x,
30
+ y: node.y,
31
+ width: node.width,
32
+ height: node.height,
33
+ rx: 10,
34
+ ry: 10,
35
+ style: { fill, stroke, strokeWidth, transition: "stroke 280ms ease, fill 280ms ease" }
36
+ }
37
+ ), node.isCompound ? /* @__PURE__ */ React.createElement(
38
+ "text",
39
+ {
40
+ x: node.x + 14,
41
+ y: node.y + 22,
42
+ style: {
43
+ fill: isActive || isAncestor ? V.accent : V.text,
44
+ fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
45
+ fontSize: 12,
46
+ fontWeight: 600,
47
+ letterSpacing: "0.06em",
48
+ transition: "fill 280ms ease"
49
+ }
50
+ },
51
+ node.label
52
+ ) : /* @__PURE__ */ React.createElement(
53
+ "text",
54
+ {
55
+ x: node.x + node.width / 2,
56
+ y: node.y + node.height / 2 + 5,
57
+ textAnchor: "middle",
58
+ style: {
59
+ fill: textFill,
60
+ fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
61
+ fontSize: 14,
62
+ fontWeight: isActive ? 600 : 500,
63
+ transition: "fill 280ms ease"
64
+ }
65
+ },
66
+ node.label
67
+ ), isActive && !node.isCompound && /* @__PURE__ */ React.createElement("circle", { cx: node.x + node.width - 10, cy: node.y + 10, r: 4, style: { fill: V.accent } }, /* @__PURE__ */ React.createElement("animate", { attributeName: "opacity", values: "1;0.35;1", dur: "1.6s", repeatCount: "indefinite" })));
68
+ }
69
+ function SelfLoopShape({ edge, node, isOutgoing, onFire, loopIndex }) {
70
+ const [hovered, setHovered] = useState(false);
71
+ const stroke = isOutgoing ? V.accent : V.edge;
72
+ const strokeWidth = isOutgoing ? hovered ? 2.5 : 2 : 1.25;
73
+ const opacity = isOutgoing ? 1 : 0.65;
74
+ const markerId = isOutgoing ? "matchina-svg-arrow-active" : "matchina-svg-arrow";
75
+ const { label } = edge;
76
+ const hw = node.width / 2;
77
+ const hh = node.height / 2;
78
+ const sx = node.x + hw;
79
+ const sy = node.y + hh;
80
+ const loopRadius = 28 + loopIndex * 16;
81
+ const startX = sx + hw - 8 - loopIndex * 2;
82
+ const startY = sy - hh;
83
+ const endX = sx + hw;
84
+ const endY = sy - hh + 8 + loopIndex * 2;
85
+ const d = `M ${startX} ${startY} C ${startX} ${startY - loopRadius}, ${endX + loopRadius} ${endY}, ${endX} ${endY}`;
86
+ const labelX = sx + hw + loopRadius + 4;
87
+ const labelY = sy - hh - 10 + loopIndex * (label ? label.height + 8 : 24);
88
+ return /* @__PURE__ */ React.createElement(
89
+ "g",
90
+ {
91
+ style: { cursor: isOutgoing ? "pointer" : "default" },
92
+ onClick: isOutgoing ? () => onFire(edge.event) : void 0,
93
+ onMouseEnter: isOutgoing ? () => setHovered(true) : void 0,
94
+ onMouseLeave: isOutgoing ? () => setHovered(false) : void 0
95
+ },
96
+ isOutgoing && /* @__PURE__ */ React.createElement("path", { d, fill: "none", stroke: "transparent", strokeWidth: 14 }),
97
+ /* @__PURE__ */ React.createElement(
98
+ "path",
99
+ {
100
+ d,
101
+ fill: "none",
102
+ style: { stroke, strokeWidth, opacity, transition: "stroke 220ms ease, opacity 220ms ease" },
103
+ markerEnd: `url(#${markerId})`
104
+ }
105
+ ),
106
+ label && /* @__PURE__ */ React.createElement(
107
+ "g",
108
+ {
109
+ transform: `translate(${labelX}, ${labelY - label.height / 2})`,
110
+ style: { opacity, transition: "opacity 220ms ease", cursor: isOutgoing ? "pointer" : "default" },
111
+ onClick: isOutgoing ? () => onFire(edge.event) : void 0
112
+ },
113
+ /* @__PURE__ */ React.createElement(
114
+ "rect",
115
+ {
116
+ x: -6,
117
+ y: -2,
118
+ width: label.width + 12,
119
+ height: label.height + 4,
120
+ rx: 6,
121
+ ry: 6,
122
+ style: {
123
+ fill: isOutgoing ? hovered ? V.accent : V.labelBgActive : V.labelBg,
124
+ stroke: isOutgoing ? V.accent : "rgba(100,116,139,0.45)",
125
+ strokeWidth: isOutgoing ? 1 : 0.75,
126
+ transition: "fill 150ms ease, stroke 150ms ease"
127
+ }
128
+ }
129
+ ),
130
+ /* @__PURE__ */ React.createElement(
131
+ "text",
132
+ {
133
+ x: label.width / 2,
134
+ y: (label.height + 4) / 2 + 4,
135
+ textAnchor: "middle",
136
+ style: {
137
+ fill: isOutgoing ? hovered ? V.labelBg : V.accent : V.labelText,
138
+ fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
139
+ fontSize: 11,
140
+ fontWeight: isOutgoing ? 600 : 500,
141
+ letterSpacing: "0.04em",
142
+ userSelect: "none",
143
+ transition: "fill 150ms ease"
144
+ }
145
+ },
146
+ label.text
147
+ )
148
+ )
149
+ );
150
+ }
151
+ function EdgeShape({ edge, isOutgoing, onFire, labelT = 0.5 }) {
152
+ const [hovered, setHovered] = useState(false);
153
+ const section = edge.sections?.[0];
154
+ if (!section?.startPoint || !section?.endPoint) return null;
155
+ const d = buildCurvedPath(section);
156
+ const stroke = isOutgoing ? V.accent : V.edge;
157
+ const strokeWidth = isOutgoing ? hovered ? 2.5 : 2 : 1.25;
158
+ const opacity = isOutgoing ? 1 : 0.65;
159
+ const { label } = edge;
160
+ const markerId = isOutgoing ? "matchina-svg-arrow-active" : "matchina-svg-arrow";
161
+ const mid = label ? pathAtT(section, labelT) : null;
162
+ return /* @__PURE__ */ React.createElement(
163
+ "g",
164
+ {
165
+ style: { cursor: isOutgoing ? "pointer" : "default" },
166
+ onClick: isOutgoing ? () => onFire(edge.event) : void 0,
167
+ onMouseEnter: isOutgoing ? () => setHovered(true) : void 0,
168
+ onMouseLeave: isOutgoing ? () => setHovered(false) : void 0
169
+ },
170
+ isOutgoing && /* @__PURE__ */ React.createElement("path", { d, fill: "none", stroke: "transparent", strokeWidth: 18 }),
171
+ /* @__PURE__ */ React.createElement(
172
+ "path",
173
+ {
174
+ d,
175
+ fill: "none",
176
+ style: {
177
+ stroke,
178
+ strokeWidth,
179
+ opacity,
180
+ transition: "stroke 220ms ease, opacity 220ms ease"
181
+ },
182
+ markerEnd: `url(#${markerId})`
183
+ }
184
+ ),
185
+ label && mid && /* @__PURE__ */ React.createElement(
186
+ "g",
187
+ {
188
+ transform: `translate(${mid.x - label.width / 2}, ${mid.y - label.height / 2})`,
189
+ style: { opacity, transition: "opacity 220ms ease", cursor: isOutgoing ? "pointer" : "default" },
190
+ onClick: isOutgoing ? () => onFire(edge.event) : void 0
191
+ },
192
+ /* @__PURE__ */ React.createElement(
193
+ "rect",
194
+ {
195
+ x: -6,
196
+ y: -2,
197
+ width: label.width + 12,
198
+ height: label.height + 4,
199
+ rx: 6,
200
+ ry: 6,
201
+ style: {
202
+ fill: isOutgoing ? hovered ? V.accent : V.labelBgActive : V.labelBg,
203
+ stroke: isOutgoing ? V.accent : "rgba(100,116,139,0.45)",
204
+ strokeWidth: isOutgoing ? 1 : 0.75,
205
+ transition: "fill 150ms ease, stroke 150ms ease"
206
+ }
207
+ }
208
+ ),
209
+ /* @__PURE__ */ React.createElement(
210
+ "text",
211
+ {
212
+ x: label.width / 2,
213
+ y: (label.height + 4) / 2 + 4,
214
+ textAnchor: "middle",
215
+ style: {
216
+ fill: isOutgoing ? hovered ? V.labelBg : V.accent : V.labelText,
217
+ fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
218
+ fontSize: 11,
219
+ fontWeight: isOutgoing ? 600 : 500,
220
+ letterSpacing: "0.04em",
221
+ userSelect: "none",
222
+ transition: "fill 150ms ease"
223
+ }
224
+ },
225
+ label.text
226
+ )
227
+ )
228
+ );
229
+ }
230
+ const ctrlBtn = {
231
+ background: "transparent",
232
+ border: "none",
233
+ color: V.ctrlText,
234
+ // 44×44 meets a11y touch-target minimum on coarse pointers; on fine pointers
235
+ // (mouse) the visual hit area is still adequate. Keeps the corner control
236
+ // stack compact while remaining tappable on mobile.
237
+ width: 44,
238
+ height: 44,
239
+ display: "flex",
240
+ alignItems: "center",
241
+ justifyContent: "center",
242
+ fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
243
+ fontSize: 14,
244
+ lineHeight: 1,
245
+ cursor: "pointer",
246
+ padding: 0
247
+ };
248
+ const MAX_FIT_ZOOM = 1;
249
+ function computeFit(contentW, contentH, containerW, containerH) {
250
+ const scaleX = containerW / contentW;
251
+ const scaleY = containerH / contentH;
252
+ const zoom = Math.min(scaleX, scaleY, MAX_FIT_ZOOM);
253
+ const pan = {
254
+ x: (containerW - contentW * zoom) / 2,
255
+ y: (containerH - contentH * zoom) / 2
256
+ };
257
+ return { zoom, pan };
258
+ }
259
+ export const SvgInspector = React.memo(function SvgInspector2({
260
+ shape,
261
+ value,
262
+ onFire,
263
+ options,
264
+ interactive = true,
265
+ precomputedLayout
266
+ }) {
267
+ const [layout, setLayout] = useState(precomputedLayout ?? null);
268
+ const [pan, setPan] = useState({ x: 20, y: 20 });
269
+ const [zoom, setZoom] = useState(1);
270
+ const [interacted, setInteracted] = useState(false);
271
+ const dragRef = useRef({ active: false, sx: 0, sy: 0, px: 0, py: 0 });
272
+ const containerRef = useRef(null);
273
+ const optionsKey = JSON.stringify(options ?? {});
274
+ const initialOptionsKey = useRef(optionsKey);
275
+ const initialShapeRef = useRef(shape);
276
+ useEffect(() => {
277
+ if (precomputedLayout && shape === initialShapeRef.current && optionsKey === initialOptionsKey.current) {
278
+ return;
279
+ }
280
+ runElkLayout(shape, options ?? {}).then((l) => {
281
+ setLayout(l);
282
+ if (interacted) fitToContainer(l);
283
+ }).catch(console.error);
284
+ }, [shape, optionsKey]);
285
+ function fitToContainer(l) {
286
+ const el = containerRef.current;
287
+ if (!el) return;
288
+ const { zoom: z, pan: p } = computeFit(l.width, l.height, el.clientWidth, el.clientHeight);
289
+ setZoom(z);
290
+ setPan(p);
291
+ }
292
+ function leaveViewBoxMode(l) {
293
+ const el = containerRef.current;
294
+ if (!el) {
295
+ setInteracted(true);
296
+ return;
297
+ }
298
+ const scaleX = el.clientWidth / l.width;
299
+ const scaleY = el.clientHeight / l.height;
300
+ const z = Math.min(scaleX, scaleY, MAX_FIT_ZOOM);
301
+ const p = {
302
+ x: (el.clientWidth - l.width * z) / 2,
303
+ y: (el.clientHeight - l.height * z) / 2
304
+ };
305
+ setZoom(z);
306
+ setPan(p);
307
+ setInteracted(true);
308
+ }
309
+ const activePath = useMemo(() => value ? value.split(".") : [], [value]);
310
+ const activeLeafId = value;
311
+ const activeAncestorIds = useMemo(() => {
312
+ const set = /* @__PURE__ */ new Set();
313
+ for (let i = 0; i < activePath.length - 1; i++) {
314
+ set.add(activePath.slice(0, i + 1).join("."));
315
+ }
316
+ return set;
317
+ }, [activePath]);
318
+ const activeSourceIds = useMemo(() => {
319
+ const set = /* @__PURE__ */ new Set();
320
+ for (let i = 1; i <= activePath.length; i++) {
321
+ set.add(activePath.slice(0, i).join("."));
322
+ }
323
+ return set;
324
+ }, [activePath]);
325
+ function handleFire(event) {
326
+ if (interactive) onFire?.(event);
327
+ }
328
+ function onWheel(e) {
329
+ e.preventDefault();
330
+ if (!interacted && layout) leaveViewBoxMode(layout);
331
+ setZoom((z) => Math.min(2.5, Math.max(0.3, z * (e.deltaY > 0 ? 0.92 : 1.08))));
332
+ }
333
+ function onMouseDown(e) {
334
+ if (e.button !== 0) return;
335
+ if (!interacted && layout) leaveViewBoxMode(layout);
336
+ dragRef.current = { active: true, sx: e.clientX, sy: e.clientY, px: pan.x, py: pan.y };
337
+ }
338
+ function onMouseMove(e) {
339
+ if (!dragRef.current.active) return;
340
+ setPan({
341
+ x: dragRef.current.px + e.clientX - dragRef.current.sx,
342
+ y: dragRef.current.py + e.clientY - dragRef.current.sy
343
+ });
344
+ }
345
+ function onMouseUp() {
346
+ dragRef.current.active = false;
347
+ }
348
+ const edgeLabelT = useMemo(() => {
349
+ const allEdges = layout?.edges ?? [];
350
+ const pairNextIdx = /* @__PURE__ */ new Map();
351
+ const pairTotal = /* @__PURE__ */ new Map();
352
+ for (const edge of allEdges) {
353
+ const key = `${edge.sourcePath.join(".")}\u2192${edge.targetPath.join(".")}`;
354
+ pairTotal.set(key, (pairTotal.get(key) ?? 0) + 1);
355
+ }
356
+ const result = /* @__PURE__ */ new Map();
357
+ for (const edge of allEdges) {
358
+ const key = `${edge.sourcePath.join(".")}\u2192${edge.targetPath.join(".")}`;
359
+ const count = pairTotal.get(key) ?? 1;
360
+ const idx = pairNextIdx.get(key) ?? 0;
361
+ pairNextIdx.set(key, idx + 1);
362
+ const t = count === 1 ? 0.5 : 0.3 + idx / (count - 1) * 0.4;
363
+ result.set(edge.id, t);
364
+ }
365
+ return result;
366
+ }, [layout]);
367
+ if (!layout) {
368
+ return /* @__PURE__ */ React.createElement("div", { style: { width: "100%", height: "100%", background: V.bg, display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement("span", { style: { color: V.edge, fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)", fontSize: 12 } }, "computing layout\u2026"));
369
+ }
370
+ const { nodes, edges, width, height } = layout;
371
+ const compounds = nodes.filter((n) => n.isCompound);
372
+ const leaves = nodes.filter((n) => !n.isCompound);
373
+ return /* @__PURE__ */ React.createElement(
374
+ "div",
375
+ {
376
+ ref: containerRef,
377
+ style: {
378
+ position: "relative",
379
+ width: "100%",
380
+ height: "100%",
381
+ overflow: "hidden",
382
+ cursor: "grab",
383
+ background: V.bg,
384
+ backgroundImage: `radial-gradient(ellipse 80% 60% at 70% 0%, color-mix(in srgb, ${V.accent} 5%, transparent), transparent 60%)`,
385
+ // Pre-interaction we let the SVG act as a flex item so its maxWidth/maxHeight
386
+ // (set to content dimensions) caps it at 1x and centers it. Post-interaction
387
+ // the inner <g transform> controls placement, so we revert to block layout.
388
+ ...!interacted && {
389
+ display: "flex",
390
+ alignItems: "center",
391
+ justifyContent: "center"
392
+ }
393
+ },
394
+ onWheel,
395
+ onMouseDown,
396
+ onMouseMove,
397
+ onMouseUp,
398
+ onMouseLeave: onMouseUp
399
+ },
400
+ /* @__PURE__ */ React.createElement(
401
+ "svg",
402
+ {
403
+ ...interacted && { width: "100%", height: "100%" },
404
+ style: {
405
+ display: "block",
406
+ // Pre-interaction: cap intrinsic size at the content's natural dimensions so
407
+ // small diagrams sit at 1x (centered by the parent flex container) instead of
408
+ // ballooning to fill via viewBox. Larger diagrams still shrink to fit via the
409
+ // viewBox + `meet` because we still allow width/height to expand to 100%.
410
+ ...!interacted && {
411
+ width: "100%",
412
+ height: "100%",
413
+ maxWidth: width,
414
+ maxHeight: height
415
+ }
416
+ },
417
+ ...!interacted && {
418
+ viewBox: `0 0 ${width} ${height}`,
419
+ preserveAspectRatio: "xMidYMid meet"
420
+ }
421
+ },
422
+ /* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("marker", { id: "matchina-svg-arrow", viewBox: "0 0 10 10", refX: "9", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse" }, /* @__PURE__ */ React.createElement("path", { d: "M 0 0 L 10 5 L 0 10 z", style: { fill: "rgba(100,116,139,0.7)" } })), /* @__PURE__ */ React.createElement("marker", { id: "matchina-svg-arrow-active", viewBox: "0 0 10 10", refX: "9", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse" }, /* @__PURE__ */ React.createElement("path", { d: "M 0 0 L 10 5 L 0 10 z", style: { fill: V.accent } }))),
423
+ /* @__PURE__ */ React.createElement("g", { transform: interacted ? `translate(${pan.x}, ${pan.y}) scale(${zoom})` : void 0 }, compounds.map((node) => /* @__PURE__ */ React.createElement(
424
+ NodeShape,
425
+ {
426
+ key: node.id,
427
+ node,
428
+ isActive: node.id === activeLeafId,
429
+ isAncestor: activeAncestorIds.has(node.id)
430
+ }
431
+ )), (() => {
432
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
433
+ const selfLoopIndexByNode = /* @__PURE__ */ new Map();
434
+ return edges.map((edge) => {
435
+ const isSelf = edge.sourcePath.join(".") === edge.targetPath.join(".");
436
+ const isOutgoing = activeSourceIds.has(edge.sourcePath.join("."));
437
+ if (isSelf) {
438
+ const nodeId = edge.sourcePath.join(".");
439
+ const node = nodeById.get(nodeId);
440
+ if (!node) return null;
441
+ const loopIndex = selfLoopIndexByNode.get(nodeId) ?? 0;
442
+ selfLoopIndexByNode.set(nodeId, loopIndex + 1);
443
+ return /* @__PURE__ */ React.createElement(
444
+ SelfLoopShape,
445
+ {
446
+ key: edge.id,
447
+ edge,
448
+ node,
449
+ isOutgoing,
450
+ onFire: handleFire,
451
+ loopIndex
452
+ }
453
+ );
454
+ }
455
+ return /* @__PURE__ */ React.createElement(
456
+ EdgeShape,
457
+ {
458
+ key: edge.id,
459
+ edge,
460
+ isOutgoing,
461
+ onFire: handleFire,
462
+ labelT: edgeLabelT.get(edge.id) ?? 0.5
463
+ }
464
+ );
465
+ });
466
+ })(), leaves.map((node) => /* @__PURE__ */ React.createElement(
467
+ NodeShape,
468
+ {
469
+ key: node.id,
470
+ node,
471
+ isActive: node.id === activeLeafId,
472
+ isAncestor: activeAncestorIds.has(node.id)
473
+ }
474
+ )))
475
+ ),
476
+ /* @__PURE__ */ React.createElement("div", { style: {
477
+ position: "absolute",
478
+ bottom: 14,
479
+ right: 14,
480
+ display: "flex",
481
+ flexDirection: "column",
482
+ background: V.ctrlBg,
483
+ border: `1px solid ${V.ctrlBorder}`,
484
+ borderRadius: "var(--matchina-viz-radius, 2px)",
485
+ overflow: "hidden"
486
+ } }, /* @__PURE__ */ React.createElement(
487
+ "button",
488
+ {
489
+ "aria-label": "Zoom in",
490
+ title: "Zoom in",
491
+ onClick: () => {
492
+ if (!interacted && layout) leaveViewBoxMode(layout);
493
+ setZoom((z) => Math.min(2.5, z * 1.15));
494
+ },
495
+ style: ctrlBtn
496
+ },
497
+ "+"
498
+ ), /* @__PURE__ */ React.createElement(
499
+ "button",
500
+ {
501
+ "aria-label": "Zoom out",
502
+ title: "Zoom out",
503
+ onClick: () => {
504
+ if (!interacted && layout) leaveViewBoxMode(layout);
505
+ setZoom((z) => Math.max(0.3, z * 0.87));
506
+ },
507
+ style: { ...ctrlBtn, borderTop: `1px solid ${V.ctrlBorder}` }
508
+ },
509
+ "\u2212"
510
+ ), /* @__PURE__ */ React.createElement(
511
+ "button",
512
+ {
513
+ "aria-label": "Fit view",
514
+ title: "Fit view",
515
+ onClick: () => layout && leaveViewBoxMode(layout),
516
+ style: { ...ctrlBtn, borderTop: `1px solid ${V.ctrlBorder}` }
517
+ },
518
+ "\u26F6"
519
+ ))
520
+ );
521
+ });
522
+ export default SvgInspector;
@@ -0,0 +1,51 @@
1
+ import type { MachineShape } from 'matchina';
2
+ export interface SvgNode {
3
+ id: string;
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ label: string;
9
+ isCompound: boolean;
10
+ path: string[];
11
+ }
12
+ export interface SvgEdge {
13
+ id: string;
14
+ event: string;
15
+ sourcePath: string[];
16
+ targetPath: string[];
17
+ sections: {
18
+ startPoint: {
19
+ x: number;
20
+ y: number;
21
+ };
22
+ endPoint: {
23
+ x: number;
24
+ y: number;
25
+ };
26
+ bendPoints?: {
27
+ x: number;
28
+ y: number;
29
+ }[];
30
+ }[];
31
+ label: {
32
+ text: string;
33
+ x: number;
34
+ y: number;
35
+ width: number;
36
+ height: number;
37
+ } | null;
38
+ }
39
+ export interface SvgLayout {
40
+ nodes: SvgNode[];
41
+ edges: SvgEdge[];
42
+ width: number;
43
+ height: number;
44
+ }
45
+ export interface ElkLayoutOptions {
46
+ direction?: 'RIGHT' | 'DOWN';
47
+ edgeRouting?: 'ORTHOGONAL' | 'POLYLINE';
48
+ nodeSpacing?: number;
49
+ layerSpacing?: number;
50
+ }
51
+ export declare function runElkLayout(shape: MachineShape, opts?: ElkLayoutOptions): Promise<SvgLayout>;