@dforge-core/diagram 0.0.4 → 0.0.5

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/CHANGELOG.md ADDED
@@ -0,0 +1,112 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@dforge-core/diagram` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.0.4] — 2026-09-14
9
+
10
+ ### Fixed
11
+
12
+ - **Nodes no longer scale wrongly in Safari/WebKit.** Entity nodes rendered inside an
13
+ SVG `<foreignObject>`, and WebKit does not apply a scaled SVG ancestor transform to
14
+ the HTML content of a `foreignObject` — so zooming moved the boxes without resizing
15
+ their contents. Nodes now render in an HTML overlay layer that carries the same
16
+ pan/zoom transform as the SVG `<g>` beneath it, which every browser applies the same
17
+ way.
18
+
19
+ The layer is `pointer-events: none` with each node opting back in, so empty space
20
+ still falls through to the SVG for panning, area-select and edge hit areas. Pointer
21
+ and double-click handlers moved from the `<svg>` up to the container, since events
22
+ now originate on both sides of that boundary; the delegated walk-up stops at the
23
+ container rather than the SVG element. Overlaid zoom controls are excluded from the
24
+ delegated handler so their clicks aren't read as a pan.
25
+
26
+ ### Added
27
+
28
+ - **`buildGraph` takes `localized` and `moduleColorIndex` options.**
29
+
30
+ `localized` draws `resLabelLocalized` in place of `resLabel` wherever a translation
31
+ exists, so a tenant that has translated its metadata can read the diagram in its own
32
+ language. The base label stays the default: the model browser is a developer view,
33
+ and `metadata.getModel` deliberately loads install-time labels.
34
+
35
+ `moduleColorIndex` overrides the default palette assignment of "position in
36
+ `modules`". Needed when `modules` is an arbitrary subset — a module plus the ones it
37
+ connects to — which must keep the colors those modules carry on their own diagrams.
38
+ `intraModuleEdgesOnly` and `colorOffset` are unchanged.
39
+
40
+ - **`findRelatedModules` and `expandBridgeSelection`**, for drawing a module alongside
41
+ its neighbours. A bridge module (`crm-fin`, `wms-fin`) owns no tables of its own — it
42
+ only adds extension columns to the entities of the modules it bridges — so its
43
+ diagram was empty, hiding the one thing the module does.
44
+
45
+ `findRelatedModules(response, moduleCd)` returns the modules on either end of a
46
+ dependency plus those reachable through a cross-module reference.
47
+ `expandBridgeSelection(response, moduleCds)` pulls in a selected bridge's non-system,
48
+ non-bridge dependencies, so picking the bridge alone still draws something.
49
+
50
+ - **`entityLabel(ent, localized?)` and `columnLabel(col, localized?)`** — the label
51
+ fallback used internally by `buildGraph`, exported so a consumer rendering its own
52
+ chrome resolves labels the same way.
53
+
54
+ - **`SimpleDiagram` accepts `fitKey`** (`string | number`, optional). Changing it
55
+ re-fits the content to the viewport. Added for the print view, where the diagram is
56
+ laid out into a fixed sheet box rather than the screen viewport, and the fit has to
57
+ be recomputed when that box appears.
58
+
59
+ - **Three optional fields on the model contract**, mirroring what `metadata.getModel`
60
+ now returns: `ModelModule.dependencies` (manifest `dependencies` keys),
61
+ `ModelEntity.resLabelLocalized` and `ModelColumn.resLabelLocalized` (label for the
62
+ viewer's culture, absent when no translation exists).
63
+
64
+ All of the above are additive. Component props are unchanged apart from the optional
65
+ `fitKey`, so 0.0.3 code compiles and behaves the same against this release.
66
+
67
+ ### Changed
68
+
69
+ - `@dforge-core/metadata` moves from `0.0.2` to `0.0.22`. It is a type-only import
70
+ (`model-types.ts` pulls `BaseDatatypeCd`, `ColumnTypeCd` and `FieldTypeCd` from it),
71
+ so the jump carries no runtime code.
72
+
73
+ ## [0.0.3] — 2026-06-13
74
+
75
+ ### Changed
76
+
77
+ - **The model contract's code unions come from `@dforge-core/metadata`.**
78
+ `model-types.ts` had restated `BaseDatatypeCd`, `ColumnTypeCd` and `FieldTypeCd`
79
+ inline; it now type-imports them from the package that owns those registries, which
80
+ is the same source the SDK reads. Adds `@dforge-core/metadata` as this package's only
81
+ dependency.
82
+
83
+ ## [0.0.2] — 2026-06-12
84
+
85
+ ### Removed
86
+
87
+ - **The `bootstrap-icons` dependency.** `IconButton` rendered icons as Bootstrap Icons
88
+ font classes, which silently drew nothing unless the host page happened to load that
89
+ stylesheet. The handful of icons the diagram controls need are now inline SVG paths
90
+ in `icons.ts`, so the package renders correctly on any page.
91
+
92
+ ## [0.0.1] — 2026-06-09
93
+
94
+ ### Added
95
+
96
+ - Initial release, published so the diagram can be used outside this repo (the VS Code
97
+ extension is the first such consumer). `SimpleDiagram`, `ModelDiagram`, `EntityNode`,
98
+ `WorkspaceMap` and `ModuleCard`, with the layout, edge-routing, graph-building,
99
+ geometry, pan/zoom and i18n helpers behind them.
100
+ - `model-types.ts` — a local mirror of the dForge API `metadata.getModel` shape, so the
101
+ package types the payload it draws without depending on the SDK.
102
+ - MIT `LICENSE` and `README.md`.
103
+
104
+ ### Changed
105
+
106
+ - Renamed from `@dforge/diagram` to `@dforge-core/diagram`.
107
+
108
+ ### Removed
109
+
110
+ - **The `@dforge/sdk` and `@dforge/ui` dependencies**, both workspace-only packages
111
+ that made this one unpublishable. The single component used from `@dforge/ui`,
112
+ `IconButton`, is now local. `svelte` is the only peer dependency.
@@ -41,6 +41,66 @@
41
41
  let draggingSeg = $state<{ edgeId: string; segIndex: number; orientation: 'H' | 'V' } | null>(null);
42
42
  let patternId = `grid-${Math.random().toString(36).slice(2, 8)}`;
43
43
 
44
+ // --- Undo / redo of layout edits (node moves, edge reshapes, resets) ---
45
+
46
+ interface LayoutSnapshot {
47
+ dragOffsets: Record<string, { dx: number; dy: number }>;
48
+ edgeOverrides: Record<string, Point[]>;
49
+ }
50
+
51
+ const HISTORY_LIMIT = 100;
52
+ let undoStack = $state.raw<LayoutSnapshot[]>([]);
53
+ let redoStack = $state.raw<LayoutSnapshot[]>([]);
54
+ let gestureStart: LayoutSnapshot | null = null;
55
+ let gestureMoved = false;
56
+
57
+ const takeSnapshot = (): LayoutSnapshot =>
58
+ $state.snapshot({ dragOffsets, edgeOverrides }) as LayoutSnapshot;
59
+
60
+ function pushHistory(snap: LayoutSnapshot) {
61
+ undoStack = [...undoStack.slice(-(HISTORY_LIMIT - 1)), snap];
62
+ redoStack = [];
63
+ }
64
+
65
+ /** Record the gesture's starting state, if the gesture changed anything. */
66
+ function commitGesture() {
67
+ gestureMoved = false;
68
+ if (!gestureStart) return;
69
+ if (JSON.stringify(gestureStart) !== JSON.stringify(takeSnapshot())) pushHistory(gestureStart);
70
+ gestureStart = null;
71
+ }
72
+
73
+ function applySnapshot(snap: LayoutSnapshot) {
74
+ dragOffsets = snap.dragOffsets;
75
+ edgeOverrides = snap.edgeOverrides;
76
+ autoEdges = routeWithOffsets(snap.dragOffsets);
77
+ }
78
+
79
+ function undo() {
80
+ const snap = undoStack.at(-1);
81
+ if (!snap) return;
82
+ redoStack = [...redoStack, takeSnapshot()];
83
+ undoStack = undoStack.slice(0, -1);
84
+ applySnapshot(snap);
85
+ }
86
+
87
+ function redo() {
88
+ const snap = redoStack.at(-1);
89
+ if (!snap) return;
90
+ undoStack = [...undoStack, takeSnapshot()];
91
+ redoStack = redoStack.slice(0, -1);
92
+ applySnapshot(snap);
93
+ }
94
+
95
+ function handleKeyDown(e: KeyboardEvent) {
96
+ if (!(e.metaKey || e.ctrlKey) || e.altKey || dragging || draggingSeg) return;
97
+ const key = e.key.toLowerCase();
98
+ if (key === 'z' && !e.shiftKey) undo();
99
+ else if ((key === 'z' && e.shiftKey) || (key === 'y' && e.ctrlKey)) redo();
100
+ else return;
101
+ e.preventDefault();
102
+ }
103
+
44
104
  const pz = createPanZoom({
45
105
  get minZoom() { return minZoom; },
46
106
  get maxZoom() { return maxZoom; },
@@ -87,18 +147,8 @@
87
147
  edgeOverrides = saved.edgeOverrides;
88
148
  pz.setTransform(saved.transform);
89
149
  restoredTransform = true;
90
- // Re-route edges for moved nodes (use local `offsets` to avoid reading $state)
91
- const movedIds = new Set(Object.keys(offsets));
92
- const posMap: Record<string, DiagramNode> = {};
93
- for (const n of nodes) {
94
- const off = offsets[n.id];
95
- posMap[n.id] = off ? { ...n, x: n.x + off.dx, y: n.y + off.dy } : n;
96
- }
97
- const posLookup = (id: string) => posMap[id];
98
- autoEdges = edges.map((e) => {
99
- if (!movedIds.has(e.source) && !movedIds.has(e.target)) return e;
100
- return routeEdge(e, posLookup);
101
- });
150
+ // Use local `offsets` to avoid reading $state
151
+ autoEdges = routeWithOffsets(offsets);
102
152
  } else if (saved) {
103
153
  dragOffsets = {};
104
154
  edgeOverrides = saved.edgeOverrides;
@@ -113,8 +163,25 @@
113
163
  }
114
164
  selectedNodeIds = new Set();
115
165
  selectedEdgeId = null;
166
+ undoStack = [];
167
+ redoStack = [];
116
168
  });
117
169
 
170
+ /** Auto-route edges for the given node offsets; edges between unmoved nodes keep their layout route. */
171
+ function routeWithOffsets(offsets: Record<string, { dx: number; dy: number }>): ResolvedEdge[] {
172
+ const movedIds = new Set(Object.keys(offsets));
173
+ const posMap: Record<string, DiagramNode> = {};
174
+ for (const n of nodes) {
175
+ const off = offsets[n.id];
176
+ posMap[n.id] = off ? { ...n, x: n.x + off.dx, y: n.y + off.dy } : n;
177
+ }
178
+ const posLookup = (id: string) => posMap[id];
179
+ return edges.map((e) => {
180
+ if (!movedIds.has(e.source) && !movedIds.has(e.target)) return e;
181
+ return routeEdge(e, posLookup);
182
+ });
183
+ }
184
+
118
185
  // Derive node positions with drag offsets applied
119
186
  const nodePositions = $derived.by(() => {
120
187
  const pos: Record<string, DiagramNode> = {};
@@ -209,6 +276,9 @@
209
276
  // clicks would otherwise reach this delegated handler.
210
277
  if (target.closest('.diagram-controls')) return;
211
278
 
279
+ // Take focus so Ctrl/Cmd+Z reaches handleKeyDown
280
+ containerEl.focus({ preventScroll: true });
281
+
212
282
  // Segment drag handle
213
283
  const handle = closest(target, 'data-handle');
214
284
  if (handle) {
@@ -286,6 +356,7 @@
286
356
  }
287
357
  if (pz.onPointerMove(e)) return;
288
358
  if (draggingSeg) {
359
+ gestureMoved = true;
289
360
  const pos = pz.clientToSvg(e.clientX, e.clientY);
290
361
  const pts = getEdgePoints(draggingSeg.edgeId);
291
362
  if (pts.length === 0) return;
@@ -294,6 +365,7 @@
294
365
  return;
295
366
  }
296
367
  if (dragging) {
368
+ gestureMoved = true;
297
369
  const pos = pz.clientToSvg(e.clientX, e.clientY);
298
370
  const newOffsets = { ...dragOffsets };
299
371
  for (const [nid, off] of Object.entries(dragging.offsets)) {
@@ -334,12 +406,16 @@
334
406
  if (draggingSeg) {
335
407
  const edgeId = draggingSeg.edgeId;
336
408
  const pts = edgeOverrides[edgeId];
337
- if (pts) {
409
+ // A handle click without a move must not turn the auto route into an override
410
+ if (!gestureMoved && gestureStart) {
411
+ edgeOverrides = gestureStart.edgeOverrides;
412
+ } else if (pts) {
338
413
  edgeOverrides = { ...edgeOverrides, [edgeId]: consolidatePoints(pts) };
339
414
  }
340
415
  }
341
416
  dragging = null;
342
417
  draggingSeg = null;
418
+ commitGesture();
343
419
  }
344
420
 
345
421
  // --- Node interaction ---
@@ -367,12 +443,15 @@
367
443
  offsets[nid] = { offsetX: pos.x - np.x, offsetY: pos.y - np.y };
368
444
  }
369
445
  }
446
+ gestureStart = takeSnapshot();
370
447
  dragging = { nodeId, offsets };
371
448
  }
372
449
 
373
450
  // --- Edge interaction ---
374
451
 
375
452
  function resetEdgeRoute(edgeId: string) {
453
+ if (!edgeOverrides[edgeId]) return;
454
+ pushHistory(takeSnapshot());
376
455
  const { [edgeId]: _, ...rest } = edgeOverrides;
377
456
  edgeOverrides = rest;
378
457
  }
@@ -380,6 +459,7 @@
380
459
  function startSegDrag(_e: PointerEvent, edgeId: string, segIndex: number, orientation: 'H' | 'V') {
381
460
  const pts = getEdgePoints(edgeId);
382
461
  if (pts.length === 0) return;
462
+ gestureStart = takeSnapshot();
383
463
 
384
464
  if (!edgeOverrides[edgeId]) {
385
465
  edgeOverrides = { ...edgeOverrides, [edgeId]: pts.map((p) => ({ ...p })) };
@@ -393,6 +473,7 @@
393
473
  function startSplitDrag(_e: PointerEvent, edgeId: string, segIndex: number, orientation: 'H' | 'V') {
394
474
  const pts = getEdgePoints(edgeId);
395
475
  if (pts.length === 0) return;
476
+ gestureStart = takeSnapshot();
396
477
 
397
478
  const jog = insertJog(pts, segIndex, orientation);
398
479
 
@@ -421,6 +502,7 @@
421
502
  }
422
503
 
423
504
  function resetLayout() {
505
+ if (Object.keys(dragOffsets).length > 0 || Object.keys(edgeOverrides).length > 0) pushHistory(takeSnapshot());
424
506
  dragOffsets = {};
425
507
  edgeOverrides = {};
426
508
  selectedNodeIds = new Set();
@@ -464,6 +546,8 @@
464
546
  <div
465
547
  class="model-diagram"
466
548
  bind:this={containerEl}
549
+ tabindex="-1"
550
+ onkeydown={handleKeyDown}
467
551
  onpointerdown={handlePointerDown}
468
552
  onpointermove={handlePointerMove}
469
553
  onpointerup={handlePointerUp}
@@ -638,6 +722,8 @@
638
722
  <IconButton icon="bi-plus-lg" title={t.zoom_in} size="sm" onclick={() => pz.zoomBy(1.3)} />
639
723
  <IconButton icon="bi-dash-lg" title={t.zoom_out} size="sm" onclick={() => pz.zoomBy(1 / 1.3)} />
640
724
  <IconButton icon="bi-fullscreen" title={t.fit_view} size="sm" onclick={fitView} />
725
+ <IconButton icon="bi-arrow-90deg-left" title={t.undo} size="sm" disabled={undoStack.length === 0} onclick={undo} />
726
+ <IconButton icon="bi-arrow-90deg-right" title={t.redo} size="sm" disabled={redoStack.length === 0} onclick={redo} />
641
727
  <IconButton icon="bi-arrow-counterclockwise" title={t.reset_layout} size="sm" onclick={resetLayout} />
642
728
  </div>
643
729
  </div>
@@ -648,6 +734,7 @@
648
734
  height: 100%;
649
735
  position: relative;
650
736
  overflow: hidden;
737
+ outline: none;
651
738
  }
652
739
 
653
740
  .model-diagram svg {
@@ -1 +1 @@
1
- {"version":3,"file":"SimpleDiagram.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/SimpleDiagram.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAS,MAAM,kBAAkB,CAAC;AAUxE,UAAU,KAAK;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C;AA+iBF,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"SimpleDiagram.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/SimpleDiagram.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAS,MAAM,kBAAkB,CAAC;AAUxE,UAAU,KAAK;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C;AAooBF,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"simple-layout.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/simple-layout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAS,MAAM,kBAAkB,CAAC;AActF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,WAAW,GAAG,SAAS,CAAC;AA0HjE;;GAEG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,WAAW,EAAE,EACpB,KAAK,EAAE,WAAW,EAAE,GAClB;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,EAAE,YAAY,EAAE,CAAA;CAAE,CAejD;AAqFD;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,WAAW,EAAE,EACpB,KAAK,EAAE,WAAW,EAAE,GAClB;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,EAAE,YAAY,EAAE,CAAA;CAAE,CAmCjD;AA0GD;;GAEG;AACH,wBAAgB,SAAS,CACxB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,UAAU,GAChB,YAAY,CAcd;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,WAAW,EAAE,EACpB,KAAK,EAAE,WAAW,EAAE,GAClB,YAAY,EAAE,CAIhB"}
1
+ {"version":3,"file":"simple-layout.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/simple-layout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAS,MAAM,kBAAkB,CAAC;AAetF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,WAAW,GAAG,SAAS,CAAC;AA0HjE;;GAEG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,WAAW,EAAE,EACpB,KAAK,EAAE,WAAW,EAAE,GAClB;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,EAAE,YAAY,EAAE,CAAA;CAAE,CAejD;AAqFD;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,WAAW,EAAE,EACpB,KAAK,EAAE,WAAW,EAAE,GAClB;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,EAAE,YAAY,EAAE,CAAA;CAAE,CAmCjD;AA+GD;;GAEG;AACH,wBAAgB,SAAS,CACxB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,UAAU,GAChB,YAAY,CAcd;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,WAAW,EAAE,EACpB,KAAK,EAAE,WAAW,EAAE,GAClB,YAAY,EAAE,CAIhB"}
@@ -7,6 +7,7 @@ const HEADER_HEIGHT = 30;
7
7
  const ROW_HEIGHT = 22;
8
8
  const MODULE_BLOCK_GAP = 80;
9
9
  const OVERLAP_MARGIN = 30;
10
+ const MIN_ROUTE_GAP = 12;
10
11
  /** Group items by a key function into a Map of arrays. */
11
12
  function groupBy(items, key) {
12
13
  const map = new Map();
@@ -271,24 +272,31 @@ function routeSelfEdge(edge, node, srcY, tgtY) {
271
272
  }
272
273
  /** Route stubs that avoid crossing through the connected nodes. */
273
274
  function stubRoute(src, tgt, dir, srcNode, tgtNode) {
274
- const stubSrc = src.x + dir * STUB_LENGTH;
275
- const stubTgt = tgt.x - dir * STUB_LENGTH;
275
+ let stubSrc = src.x + dir * STUB_LENGTH;
276
+ let stubTgt = tgt.x - dir * STUB_LENGTH;
276
277
  // Find midY that doesn't cross through either node
277
278
  const srcBottom = srcNode.y + srcNode.height;
278
279
  const tgtTop = tgtNode.y;
279
280
  const srcTop = srcNode.y;
280
281
  const tgtBottom = tgtNode.y + tgtNode.height;
281
282
  let midY;
282
- if (srcBottom + OVERLAP_MARGIN < tgtTop) {
283
+ if (srcBottom + MIN_ROUTE_GAP < tgtTop) {
283
284
  // Gap between nodes — route through it
284
285
  midY = (srcBottom + tgtTop) / 2;
285
286
  }
286
- else if (tgtBottom + OVERLAP_MARGIN < srcTop) {
287
+ else if (tgtBottom + MIN_ROUTE_GAP < srcTop) {
287
288
  midY = (tgtBottom + srcTop) / 2;
288
289
  }
289
290
  else {
290
- // Nodes overlap vertically — route above both
291
+ // Nodes overlap vertically — route above both; a stub that would land inside the other node moves outside both
291
292
  midY = Math.min(srcTop, tgtTop) - OVERLAP_MARGIN;
293
+ const left = Math.min(srcNode.x, tgtNode.x) - STUB_LENGTH;
294
+ const right = Math.max(srcNode.x + srcNode.width, tgtNode.x + tgtNode.width) + STUB_LENGTH;
295
+ const inside = (x, n) => x > n.x && x < n.x + n.width;
296
+ if (inside(stubSrc, tgtNode))
297
+ stubSrc = dir > 0 ? right : left;
298
+ if (inside(stubTgt, srcNode))
299
+ stubTgt = dir > 0 ? left : right;
292
300
  }
293
301
  return [
294
302
  src,
package/dist/i18n.d.ts CHANGED
@@ -3,6 +3,8 @@ export interface DiagramTranslations {
3
3
  zoom_out: string;
4
4
  fit_view: string;
5
5
  reset_layout: string;
6
+ undo: string;
7
+ redo: string;
6
8
  col_type_formula: string;
7
9
  col_type_ref: string;
8
10
  col_type_set: string;
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/lib/i18n.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,uBAAuB,EAAE,mBAQrC,CAAC;AAIF,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAE9E;AAED,wBAAgB,sBAAsB,IAAI,mBAAmB,CAK5D"}
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/lib/i18n.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,uBAAuB,EAAE,mBAUrC,CAAC;AAIF,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAE9E;AAED,wBAAgB,sBAAsB,IAAI,mBAAmB,CAK5D"}
package/dist/i18n.js CHANGED
@@ -4,6 +4,8 @@ export const DIAGRAM_TRANSLATIONS_EN = {
4
4
  zoom_out: 'Zoom out',
5
5
  fit_view: 'Fit view',
6
6
  reset_layout: 'Reset layout',
7
+ undo: 'Undo',
8
+ redo: 'Redo',
7
9
  col_type_formula: 'formula',
8
10
  col_type_ref: 'ref',
9
11
  col_type_set: 'set',
@@ -1 +1 @@
1
- {"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../src/lib/icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CASxC,CAAC"}
1
+ {"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../src/lib/icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAaxC,CAAC"}
package/dist/icons.js CHANGED
@@ -13,5 +13,7 @@ export const ICONS = {
13
13
  'bi-plus-lg': '<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2"/>',
14
14
  'bi-dash-lg': '<path fill-rule="evenodd" d="M2 8a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11A.5.5 0 0 1 2 8"/>',
15
15
  'bi-fullscreen': '<path d="M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5M.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5m15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5"/>',
16
+ 'bi-arrow-90deg-left': '<path fill-rule="evenodd" d="M1.146 4.854a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 4H12.5A2.5 2.5 0 0 1 15 6.5v8a.5.5 0 0 1-1 0v-8A1.5 1.5 0 0 0 12.5 5H2.707l3.147 3.146a.5.5 0 1 1-.708.708z"/>',
17
+ 'bi-arrow-90deg-right': '<path fill-rule="evenodd" d="M14.854 4.854a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 4H3.5A2.5 2.5 0 0 0 1 6.5v8a.5.5 0 0 0 1 0v-8A1.5 1.5 0 0 1 3.5 5h9.793l-3.147 3.146a.5.5 0 0 0 .708.708z"/>',
16
18
  'bi-arrow-counterclockwise': '<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2z"/><path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466"/>',
17
19
  };
package/package.json CHANGED
@@ -1,58 +1,60 @@
1
1
  {
2
- "name": "@dforge-core/diagram",
3
- "version": "0.0.4",
4
- "description": "Entity-relationship and workspace diagram components for dForge models. Standalone Svelte 5 library with no dForge runtime dependencies.",
5
- "license": "MIT",
6
- "author": "dForge",
7
- "homepage": "https://github.com/iash44/dForge-core/tree/main/clients/packages/diagram#readme",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/iash44/dForge-core.git",
11
- "directory": "clients/packages/diagram"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/iash44/dForge-core/issues"
15
- },
16
- "keywords": [
17
- "svelte",
18
- "diagram",
19
- "erd",
20
- "entity-relationship",
21
- "dforge",
22
- "visualization"
23
- ],
24
- "publishConfig": {
25
- "access": "public"
26
- },
27
- "type": "module",
28
- "svelte": "./dist/index.js",
29
- "types": "./dist/index.d.ts",
30
- "exports": {
31
- ".": {
32
- "source": "./src/lib/index.ts",
33
- "types": "./dist/index.d.ts",
34
- "svelte": "./dist/index.js",
35
- "default": "./dist/index.js"
36
- }
37
- },
38
- "files": [
39
- "dist"
40
- ],
41
- "dependencies": {
42
- "@dforge-core/metadata": "0.0.22"
43
- },
44
- "peerDependencies": {
45
- "svelte": "^5.0.0"
46
- },
47
- "devDependencies": {
48
- "@sveltejs/package": "^2.5.8",
49
- "@sveltejs/vite-plugin-svelte": "^7.3.0",
50
- "svelte": "^5.56.8",
51
- "typescript": "^6.0.3"
52
- },
53
- "scripts": {
54
- "build": "svelte-package",
55
- "dev": "svelte-package --watch",
56
- "package": "pnpm pack"
57
- }
58
- }
2
+ "name": "@dforge-core/diagram",
3
+ "version": "0.0.5",
4
+ "description": "Entity-relationship and workspace diagram components for dForge models. Standalone Svelte 5 library with no dForge runtime dependencies.",
5
+ "license": "MIT",
6
+ "author": "dForge",
7
+ "homepage": "https://github.com/iash44/dForge-core/tree/main/clients/packages/diagram#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/iash44/dForge-core.git",
11
+ "directory": "clients/packages/diagram"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/iash44/dForge-core/issues"
15
+ },
16
+ "keywords": [
17
+ "svelte",
18
+ "diagram",
19
+ "erd",
20
+ "entity-relationship",
21
+ "dforge",
22
+ "visualization"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "svelte": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "source": "./src/lib/index.ts",
33
+ "types": "./dist/index.d.ts",
34
+ "svelte": "./dist/index.js",
35
+ "default": "./dist/index.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "CHANGELOG.md"
41
+ ],
42
+ "scripts": {
43
+ "build": "svelte-package",
44
+ "dev": "svelte-package --watch",
45
+ "prepack": "pnpm build",
46
+ "package": "pnpm pack"
47
+ },
48
+ "dependencies": {
49
+ "@dforge-core/metadata": "workspace:*"
50
+ },
51
+ "peerDependencies": {
52
+ "svelte": "^5.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@sveltejs/package": "^2.5.8",
56
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
57
+ "svelte": "^5.56.8",
58
+ "typescript": "^6.0.3"
59
+ }
60
+ }