@dforge-core/diagram 0.0.3 → 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.
@@ -264,14 +264,18 @@
264
264
  });
265
265
  </script>
266
266
 
267
- <div class="model-diagram" bind:this={containerEl}>
267
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
268
+ <div
269
+ class="model-diagram"
270
+ bind:this={containerEl}
271
+ onpointermove={handlePointerMove}
272
+ onpointerup={handlePointerUp}
273
+ >
268
274
  <!-- svelte-ignore a11y_no_static_element_interactions -->
269
275
  <svg
270
276
  bind:this={svgEl}
271
277
  role="application"
272
278
  onpointerdown={handleBackgroundPointerDown}
273
- onpointermove={handlePointerMove}
274
- onpointerup={handlePointerUp}
275
279
  >
276
280
  <defs>
277
281
  <pattern id={patternId} width="20" height="20" patternUnits="userSpaceOnUse"
@@ -395,26 +399,33 @@
395
399
  {/if}
396
400
  {/if}
397
401
 
398
- <!-- Nodes -->
399
- {#each nodes as node (node.id)}
400
- {@const pos = nodePositions[node.id] ?? { x: node.x, y: node.y }}
401
- <!-- svelte-ignore a11y_no_static_element_interactions -->
402
- <foreignObject
403
- x={pos.x}
404
- y={pos.y}
405
- width={node.width}
406
- height={node.height}
407
- onpointerdown={(e: PointerEvent) => handleNodePointerDown(e, node.id)}
408
- class="diagram-node-fo"
409
- >
410
- <div xmlns="http://www.w3.org/1999/xhtml">
411
- <EntityNode data={node.data} />
412
- </div>
413
- </foreignObject>
414
- {/each}
415
402
  </g>
416
403
  </svg>
417
404
 
405
+ <!--
406
+ Nodes render in an HTML overlay layer (not SVG <foreignObject>) so they
407
+ scale correctly in Safari/WebKit, which does not apply a scaled SVG
408
+ ancestor transform to foreignObject HTML content. The layer shares the
409
+ same pan/zoom transform as the SVG <g> above via a CSS transform.
410
+ -->
411
+ <div
412
+ class="node-layer"
413
+ style:transform="translate({pz.transform.x}px, {pz.transform.y}px) scale({pz.transform.k})"
414
+ style:transform-origin="0 0"
415
+ >
416
+ {#each nodes as node (node.id)}
417
+ {@const pos = nodePositions[node.id] ?? { x: node.x, y: node.y }}
418
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
419
+ <div
420
+ class="diagram-node"
421
+ style="left: {pos.x}px; top: {pos.y}px; width: {node.width}px; height: {node.height}px;"
422
+ onpointerdown={(e: PointerEvent) => handleNodePointerDown(e, node.id)}
423
+ >
424
+ <EntityNode data={node.data} />
425
+ </div>
426
+ {/each}
427
+ </div>
428
+
418
429
  <!-- Controls -->
419
430
  <div class="diagram-controls">
420
431
  <IconButton icon="bi-plus-lg" title={t.zoom_in} size="sm" onclick={() => pz.zoomBy(1.3)} />
@@ -442,12 +453,23 @@
442
453
  cursor: grabbing;
443
454
  }
444
455
 
445
- .diagram-node-fo {
456
+ .node-layer {
457
+ position: absolute;
458
+ inset: 0;
459
+ /* Empty areas let pointer events fall through to the SVG (pan / edge hit
460
+ areas); individual nodes re-enable events below. transform /
461
+ transform-origin are applied via style: directives. */
462
+ pointer-events: none;
463
+ }
464
+
465
+ .diagram-node {
466
+ position: absolute;
467
+ box-sizing: border-box;
468
+ pointer-events: auto;
446
469
  cursor: grab;
447
- overflow: visible;
448
470
  }
449
471
 
450
- .diagram-node-fo:active {
472
+ .diagram-node:active {
451
473
  cursor: grabbing;
452
474
  }
453
475
 
@@ -1 +1 @@
1
- {"version":3,"file":"ModelDiagram.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/ModelDiagram.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,EAAe,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAiI,KAAK,QAAQ,EAAsB,MAAM,iBAAiB,CAAC;AAMlM,UAAU,KAAK;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9D,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,EAAE,KAAK,IAAI,CAAC;CAC7E;AAiWF,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"ModelDiagram.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/ModelDiagram.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,EAAe,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAiI,KAAK,QAAQ,EAAsB,MAAM,iBAAiB,CAAC;AAMlM,UAAU,KAAK;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9D,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,EAAE,KAAK,IAAI,CAAC;CAC7E;AAmWF,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -16,12 +16,19 @@
16
16
  nodes: DiagramNode[];
17
17
  edges: ResolvedEdge[];
18
18
  storageKey?: string;
19
+ /**
20
+ * Refit the diagram to its container whenever this value changes. For a
21
+ * host that resizes the viewport out from under the diagram (entering a
22
+ * full-screen or print layout), where the saved transform would otherwise
23
+ * leave the content parked off to one side of a much larger canvas.
24
+ */
25
+ fitKey?: string | number;
19
26
  minZoom?: number;
20
27
  maxZoom?: number;
21
28
  onNodeDblClick?: (nodeId: string) => void;
22
29
  }
23
30
 
24
- let { nodes, edges, storageKey, minZoom = 0.1, maxZoom = 2, onNodeDblClick }: Props = $props();
31
+ let { nodes, edges, storageKey, fitKey, minZoom = 0.1, maxZoom = 2, onNodeDblClick }: Props = $props();
25
32
 
26
33
  let svgEl: SVGSVGElement;
27
34
  let containerEl: HTMLDivElement;
@@ -34,6 +41,66 @@
34
41
  let draggingSeg = $state<{ edgeId: string; segIndex: number; orientation: 'H' | 'V' } | null>(null);
35
42
  let patternId = `grid-${Math.random().toString(36).slice(2, 8)}`;
36
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
+
37
104
  const pz = createPanZoom({
38
105
  get minZoom() { return minZoom; },
39
106
  get maxZoom() { return maxZoom; },
@@ -80,18 +147,8 @@
80
147
  edgeOverrides = saved.edgeOverrides;
81
148
  pz.setTransform(saved.transform);
82
149
  restoredTransform = true;
83
- // Re-route edges for moved nodes (use local `offsets` to avoid reading $state)
84
- const movedIds = new Set(Object.keys(offsets));
85
- const posMap: Record<string, DiagramNode> = {};
86
- for (const n of nodes) {
87
- const off = offsets[n.id];
88
- posMap[n.id] = off ? { ...n, x: n.x + off.dx, y: n.y + off.dy } : n;
89
- }
90
- const posLookup = (id: string) => posMap[id];
91
- autoEdges = edges.map((e) => {
92
- if (!movedIds.has(e.source) && !movedIds.has(e.target)) return e;
93
- return routeEdge(e, posLookup);
94
- });
150
+ // Use local `offsets` to avoid reading $state
151
+ autoEdges = routeWithOffsets(offsets);
95
152
  } else if (saved) {
96
153
  dragOffsets = {};
97
154
  edgeOverrides = saved.edgeOverrides;
@@ -106,8 +163,25 @@
106
163
  }
107
164
  selectedNodeIds = new Set();
108
165
  selectedEdgeId = null;
166
+ undoStack = [];
167
+ redoStack = [];
109
168
  });
110
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
+
111
185
  // Derive node positions with drag offsets applied
112
186
  const nodePositions = $derived.by(() => {
113
187
  const pos: Record<string, DiagramNode> = {};
@@ -173,10 +247,14 @@
173
247
 
174
248
  // --- Delegated event helpers ---
175
249
 
176
- /** Walk up from event target to find a data attribute (works across HTML/SVG boundary in foreignObject). */
250
+ /**
251
+ * Walk up from event target to find a data attribute. Delegation runs on the
252
+ * container, so nodes (HTML overlay) and edges/handles (SVG) both resolve here;
253
+ * we stop at the container element.
254
+ */
177
255
  function closest(target: Element, attr: string): Element | null {
178
256
  let el: Element | null = target;
179
- while (el && el !== svgEl) {
257
+ while (el && el !== containerEl) {
180
258
  if (el.hasAttribute(attr)) return el;
181
259
  // parentElement is null at HTML↔SVG boundary in some browsers; fall back to parentNode
182
260
  el = el.parentElement ?? (el.parentNode as Element | null);
@@ -193,6 +271,14 @@
193
271
  function handlePointerDown(e: PointerEvent) {
194
272
  const target = e.target as Element;
195
273
 
274
+ // Ignore interactions that originate on the overlaid controls (buttons
275
+ // handle their own clicks). Handlers now live on the container, so control
276
+ // clicks would otherwise reach this delegated handler.
277
+ if (target.closest('.diagram-controls')) return;
278
+
279
+ // Take focus so Ctrl/Cmd+Z reaches handleKeyDown
280
+ containerEl.focus({ preventScroll: true });
281
+
196
282
  // Segment drag handle
197
283
  const handle = closest(target, 'data-handle');
198
284
  if (handle) {
@@ -270,6 +356,7 @@
270
356
  }
271
357
  if (pz.onPointerMove(e)) return;
272
358
  if (draggingSeg) {
359
+ gestureMoved = true;
273
360
  const pos = pz.clientToSvg(e.clientX, e.clientY);
274
361
  const pts = getEdgePoints(draggingSeg.edgeId);
275
362
  if (pts.length === 0) return;
@@ -278,6 +365,7 @@
278
365
  return;
279
366
  }
280
367
  if (dragging) {
368
+ gestureMoved = true;
281
369
  const pos = pz.clientToSvg(e.clientX, e.clientY);
282
370
  const newOffsets = { ...dragOffsets };
283
371
  for (const [nid, off] of Object.entries(dragging.offsets)) {
@@ -318,12 +406,16 @@
318
406
  if (draggingSeg) {
319
407
  const edgeId = draggingSeg.edgeId;
320
408
  const pts = edgeOverrides[edgeId];
321
- 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) {
322
413
  edgeOverrides = { ...edgeOverrides, [edgeId]: consolidatePoints(pts) };
323
414
  }
324
415
  }
325
416
  dragging = null;
326
417
  draggingSeg = null;
418
+ commitGesture();
327
419
  }
328
420
 
329
421
  // --- Node interaction ---
@@ -351,12 +443,15 @@
351
443
  offsets[nid] = { offsetX: pos.x - np.x, offsetY: pos.y - np.y };
352
444
  }
353
445
  }
446
+ gestureStart = takeSnapshot();
354
447
  dragging = { nodeId, offsets };
355
448
  }
356
449
 
357
450
  // --- Edge interaction ---
358
451
 
359
452
  function resetEdgeRoute(edgeId: string) {
453
+ if (!edgeOverrides[edgeId]) return;
454
+ pushHistory(takeSnapshot());
360
455
  const { [edgeId]: _, ...rest } = edgeOverrides;
361
456
  edgeOverrides = rest;
362
457
  }
@@ -364,6 +459,7 @@
364
459
  function startSegDrag(_e: PointerEvent, edgeId: string, segIndex: number, orientation: 'H' | 'V') {
365
460
  const pts = getEdgePoints(edgeId);
366
461
  if (pts.length === 0) return;
462
+ gestureStart = takeSnapshot();
367
463
 
368
464
  if (!edgeOverrides[edgeId]) {
369
465
  edgeOverrides = { ...edgeOverrides, [edgeId]: pts.map((p) => ({ ...p })) };
@@ -377,6 +473,7 @@
377
473
  function startSplitDrag(_e: PointerEvent, edgeId: string, segIndex: number, orientation: 'H' | 'V') {
378
474
  const pts = getEdgePoints(edgeId);
379
475
  if (pts.length === 0) return;
476
+ gestureStart = takeSnapshot();
380
477
 
381
478
  const jog = insertJog(pts, segIndex, orientation);
382
479
 
@@ -405,6 +502,7 @@
405
502
  }
406
503
 
407
504
  function resetLayout() {
505
+ if (Object.keys(dragOffsets).length > 0 || Object.keys(edgeOverrides).length > 0) pushHistory(takeSnapshot());
408
506
  dragOffsets = {};
409
507
  edgeOverrides = {};
410
508
  selectedNodeIds = new Set();
@@ -413,6 +511,25 @@
413
511
  requestAnimationFrame(() => fitView());
414
512
  }
415
513
 
514
+ // Refit on fitKey change. Skips the very first run so it doesn't fight the
515
+ // restored transform on mount.
516
+ //
517
+ // The first fit is synchronous, not deferred to a frame: a host entering a
518
+ // print layout flips fitKey inside `beforeprint` and flushes, and the print
519
+ // snapshot is taken as soon as that handler returns — anything left to rAF
520
+ // misses it entirely. Measuring here forces the pending layout, so the new
521
+ // container size is already available. The rAF pass after it is a cheap
522
+ // idempotent safety net for hosts whose box settles a frame late (CSS
523
+ // transitions on the shell); re-fitting to the same bounds is a no-op.
524
+ let fitKeySeen = false;
525
+ $effect(() => {
526
+ fitKey; // track
527
+ if (fitKey === undefined) return;
528
+ if (!fitKeySeen) { fitKeySeen = true; return; }
529
+ fitView();
530
+ requestAnimationFrame(() => fitView());
531
+ });
532
+
416
533
  onMount(() => {
417
534
  svgEl.addEventListener('wheel', pz.handleWheel, { passive: false });
418
535
  if (!restoredTransform) {
@@ -425,15 +542,20 @@
425
542
  });
426
543
  </script>
427
544
 
428
- <div class="model-diagram" bind:this={containerEl}>
429
- <!-- svelte-ignore a11y_no_static_element_interactions -->
545
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
546
+ <div
547
+ class="model-diagram"
548
+ bind:this={containerEl}
549
+ tabindex="-1"
550
+ onkeydown={handleKeyDown}
551
+ onpointerdown={handlePointerDown}
552
+ onpointermove={handlePointerMove}
553
+ onpointerup={handlePointerUp}
554
+ ondblclick={handleDblClick}
555
+ >
430
556
  <svg
431
557
  bind:this={svgEl}
432
558
  role="application"
433
- onpointerdown={handlePointerDown}
434
- onpointermove={handlePointerMove}
435
- onpointerup={handlePointerUp}
436
- ondblclick={handleDblClick}
437
559
  >
438
560
  <defs>
439
561
  <pattern id={patternId} width="20" height="20" patternUnits="userSpaceOnUse"
@@ -503,24 +625,6 @@
503
625
  {/if}
504
626
  {/each}
505
627
 
506
- <!-- Nodes -->
507
- {#each nodes as node (node.id)}
508
- {@const pos = nodePositions[node.id] ?? node}
509
- {@const isSelected = selectedNodeIds.has(node.id)}
510
- <foreignObject
511
- x={pos.x}
512
- y={pos.y}
513
- width={node.width}
514
- height={node.height}
515
- class="diagram-node-fo"
516
- class:selected={isSelected}
517
- >
518
- <div xmlns="http://www.w3.org/1999/xhtml" data-node-id={node.id}>
519
- <EntityNode data={node.data} />
520
- </div>
521
- </foreignObject>
522
- {/each}
523
-
524
628
  <!-- Selected edge on top of nodes -->
525
629
  {#if selectedEdgeId}
526
630
  {#each resolvedEdges as edge (edge.id)}
@@ -587,11 +691,39 @@
587
691
  </g>
588
692
  </svg>
589
693
 
694
+ <!--
695
+ Nodes render in an HTML overlay layer (not SVG <foreignObject>) so they
696
+ scale correctly in Safari/WebKit, which does not apply a scaled SVG
697
+ ancestor transform to foreignObject HTML content. The layer shares the
698
+ same pan/zoom transform as the SVG <g> above. Pointer events are handled
699
+ by delegation on the container (see handlePointerDown), so nodes only need
700
+ their data-node-id and to opt back into pointer events.
701
+ -->
702
+ <div
703
+ class="node-layer"
704
+ style:transform="translate({pz.transform.x}px, {pz.transform.y}px) scale({pz.transform.k})"
705
+ style:transform-origin="0 0"
706
+ >
707
+ {#each nodes as node (node.id)}
708
+ {@const pos = nodePositions[node.id] ?? node}
709
+ <div
710
+ class="diagram-node"
711
+ class:selected={selectedNodeIds.has(node.id)}
712
+ style="left: {pos.x}px; top: {pos.y}px; width: {node.width}px; height: {node.height}px;"
713
+ data-node-id={node.id}
714
+ >
715
+ <EntityNode data={node.data} />
716
+ </div>
717
+ {/each}
718
+ </div>
719
+
590
720
  <!-- Controls -->
591
721
  <div class="diagram-controls">
592
722
  <IconButton icon="bi-plus-lg" title={t.zoom_in} size="sm" onclick={() => pz.zoomBy(1.3)} />
593
723
  <IconButton icon="bi-dash-lg" title={t.zoom_out} size="sm" onclick={() => pz.zoomBy(1 / 1.3)} />
594
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} />
595
727
  <IconButton icon="bi-arrow-counterclockwise" title={t.reset_layout} size="sm" onclick={resetLayout} />
596
728
  </div>
597
729
  </div>
@@ -602,6 +734,7 @@
602
734
  height: 100%;
603
735
  position: relative;
604
736
  overflow: hidden;
737
+ outline: none;
605
738
  }
606
739
 
607
740
  .model-diagram svg {
@@ -615,16 +748,28 @@
615
748
  cursor: grabbing;
616
749
  }
617
750
 
618
- .diagram-node-fo {
751
+ .node-layer {
752
+ position: absolute;
753
+ inset: 0;
754
+ /* Empty areas let pointer events fall through to the SVG (pan / area
755
+ select / edge hit areas); individual nodes re-enable events below.
756
+ transform / transform-origin are applied via style: directives so the
757
+ layer tracks the SVG <g> transform exactly. */
758
+ pointer-events: none;
759
+ }
760
+
761
+ .diagram-node {
762
+ position: absolute;
763
+ box-sizing: border-box;
764
+ pointer-events: auto;
619
765
  cursor: grab;
620
- overflow: visible;
621
766
  }
622
767
 
623
- .diagram-node-fo.selected {
768
+ .diagram-node.selected {
624
769
  filter: drop-shadow(0 0 4px var(--bs-primary, #0d6efd));
625
770
  }
626
771
 
627
- .diagram-node-fo:active {
772
+ .diagram-node:active {
628
773
  cursor: grabbing;
629
774
  }
630
775
 
@@ -3,6 +3,13 @@ interface Props {
3
3
  nodes: DiagramNode[];
4
4
  edges: ResolvedEdge[];
5
5
  storageKey?: string;
6
+ /**
7
+ * Refit the diagram to its container whenever this value changes. For a
8
+ * host that resizes the viewport out from under the diagram (entering a
9
+ * full-screen or print layout), where the saved transform would otherwise
10
+ * leave the content parked off to one side of a much larger canvas.
11
+ */
12
+ fitKey?: string | number;
6
13
  minZoom?: number;
7
14
  maxZoom?: number;
8
15
  onNodeDblClick?: (nodeId: string) => void;
@@ -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,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C;AAohBF,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,
@@ -207,14 +207,18 @@
207
207
  });
208
208
  </script>
209
209
 
210
- <div class="ws-map" bind:this={containerEl}>
210
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
211
+ <div
212
+ class="ws-map"
213
+ bind:this={containerEl}
214
+ onpointermove={handlePointerMove}
215
+ onpointerup={handlePointerUp}
216
+ >
211
217
  <!-- svelte-ignore a11y_no_static_element_interactions -->
212
218
  <svg
213
219
  bind:this={svgEl}
214
220
  role="application"
215
221
  onpointerdown={handleBackgroundPointerDown}
216
- onpointermove={handlePointerMove}
217
- onpointerup={handlePointerUp}
218
222
  >
219
223
  <defs>
220
224
  <pattern id={patternId} width="20" height="20" patternUnits="userSpaceOnUse"
@@ -242,26 +246,33 @@
242
246
  />
243
247
  {/each}
244
248
 
245
- <!-- Module cards -->
246
- {#each modules as mod (mod.id)}
247
- {@const pos = modulePositions[mod.id]}
248
- <!-- svelte-ignore a11y_no_static_element_interactions -->
249
- <foreignObject
250
- x={pos.x}
251
- y={pos.y}
252
- width={mod.width}
253
- height={mod.height}
254
- onpointerdown={(e: PointerEvent) => handleModulePointerDown(e, mod.id)}
255
- class="ws-module-fo"
256
- >
257
- <div xmlns="http://www.w3.org/1999/xhtml">
258
- <ModuleCard module={mod} />
259
- </div>
260
- </foreignObject>
261
- {/each}
262
249
  </g>
263
250
  </svg>
264
251
 
252
+ <!--
253
+ Module cards render in an HTML overlay layer (not SVG <foreignObject>) so
254
+ they scale correctly in Safari/WebKit, which does not apply a scaled SVG
255
+ ancestor transform to foreignObject HTML content. The layer shares the same
256
+ pan/zoom transform as the SVG <g> above.
257
+ -->
258
+ <div
259
+ class="ws-node-layer"
260
+ style:transform="translate({pz.transform.x}px, {pz.transform.y}px) scale({pz.transform.k})"
261
+ style:transform-origin="0 0"
262
+ >
263
+ {#each modules as mod (mod.id)}
264
+ {@const pos = modulePositions[mod.id]}
265
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
266
+ <div
267
+ class="ws-module"
268
+ style="left: {pos.x}px; top: {pos.y}px; width: {mod.width}px; height: {mod.height}px;"
269
+ onpointerdown={(e: PointerEvent) => handleModulePointerDown(e, mod.id)}
270
+ >
271
+ <ModuleCard module={mod} />
272
+ </div>
273
+ {/each}
274
+ </div>
275
+
265
276
  <!-- Controls -->
266
277
  <div class="ws-controls">
267
278
  <IconButton icon="bi-plus-lg" title={t.zoom_in} size="sm" onclick={() => pz.zoomBy(1.3)} />
@@ -290,12 +301,23 @@
290
301
  cursor: grabbing;
291
302
  }
292
303
 
293
- .ws-module-fo {
304
+ .ws-node-layer {
305
+ position: absolute;
306
+ inset: 0;
307
+ /* Empty areas let pointer events fall through to the SVG for panning;
308
+ individual cards re-enable events below. transform / transform-origin
309
+ are applied via style: directives. */
310
+ pointer-events: none;
311
+ }
312
+
313
+ .ws-module {
314
+ position: absolute;
315
+ box-sizing: border-box;
316
+ pointer-events: auto;
294
317
  cursor: grab;
295
- overflow: visible;
296
318
  }
297
319
 
298
- .ws-module-fo:active {
320
+ .ws-module:active {
299
321
  cursor: grabbing;
300
322
  }
301
323
 
@@ -1 +1 @@
1
- {"version":3,"file":"WorkspaceMap.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/WorkspaceMap/WorkspaceMap.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAQvE,UAAU,KAAK;IACd,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAyPF,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"WorkspaceMap.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/WorkspaceMap/WorkspaceMap.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAQvE,UAAU,KAAK;IACd,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AA2PF,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
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/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { default as EntityNode } from './EntityNode.svelte';
8
8
  export { default as DiagramDebug } from './SimpleDiagram/DiagramDebug.svelte';
9
9
  export { simpleLayout, moduleLayout, routeEdge, routeAllEdges } from './SimpleDiagram/simple-layout';
10
10
  export type { NodeLookup } from './SimpleDiagram/simple-layout';
11
- export { buildGraph, buildEntityMap, getModuleColor, MODULE_COLORS } from './model-graph';
11
+ export { buildGraph, buildEntityMap, columnLabel, entityLabel, expandBridgeSelection, findRelatedModules, getModuleColor, MODULE_COLORS } from './model-graph';
12
12
  export { buildSvgPath, edgeLabelPos, prepareDragPoints, consolidatePoints, insertJog, getSegmentHandles, applySegmentDrag } from './SimpleDiagram/diagram-geometry';
13
13
  export type { SegHandle } from './SimpleDiagram/diagram-geometry';
14
14
  export { createPanZoom } from './pan-zoom.svelte';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAC;AACjG,YAAY,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAGlD,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGjI,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG9G,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAChF,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAG9E,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACrG,YAAY,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpK,YAAY,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAGlE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG9D,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACtG,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAGzE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC/J,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAC;AACjG,YAAY,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAGlD,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGjI,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG9G,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAChF,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAG9E,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACrG,YAAY,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG/J,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpK,YAAY,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAGlE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG9D,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACtG,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAGzE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC/J,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ export { default as DiagramDebug } from './SimpleDiagram/DiagramDebug.svelte';
8
8
  // Layout
9
9
  export { simpleLayout, moduleLayout, routeEdge, routeAllEdges } from './SimpleDiagram/simple-layout';
10
10
  // Graph building
11
- export { buildGraph, buildEntityMap, getModuleColor, MODULE_COLORS } from './model-graph';
11
+ export { buildGraph, buildEntityMap, columnLabel, entityLabel, expandBridgeSelection, findRelatedModules, getModuleColor, MODULE_COLORS } from './model-graph';
12
12
  // Geometry
13
13
  export { buildSvgPath, edgeLabelPos, prepareDragPoints, consolidatePoints, insertJog, getSegmentHandles, applySegmentDrag } from './SimpleDiagram/diagram-geometry';
14
14
  // Pan/Zoom & persistence utilities
@@ -1,7 +1,19 @@
1
1
  import type { DiagramNode, DiagramEdge } from './diagram-types';
2
- import type { ModelGetResponse, ModelModule } from './model-types';
2
+ import type { ModelColumn, ModelEntity, ModelGetResponse, ModelModule } from './model-types';
3
+ /**
4
+ * Display label for an entity / column.
5
+ *
6
+ * `resLabel` is the install-time label the module author wrote — the same string
7
+ * for every viewer, and the model browser's default because it matches what is in
8
+ * the module package. `resLabelLocalized` is the translation for the viewer's
9
+ * culture, present only where the tenant has one; `localized` opts into it and
10
+ * still falls back per-item, so a partly translated module reads as a mix rather
11
+ * than losing the untranslated half.
12
+ */
13
+ export declare const entityLabel: (ent: ModelEntity, localized?: boolean) => string;
14
+ export declare const columnLabel: (col: ModelColumn, localized?: boolean) => string;
3
15
  export declare const MODULE_COLORS: string[];
4
- export declare function getModuleColor(index: number): string;
16
+ export declare const getModuleColor: (index: number) => string;
5
17
  /**
6
18
  * Build a diagram graph from the given modules.
7
19
  * Nodes start at position {0,0} — call simpleLayout() to compute positions.
@@ -9,6 +21,14 @@ export declare function getModuleColor(index: number): string;
9
21
  export declare function buildGraph(modules: ModelModule[], entityMap: Map<number, string>, options?: {
10
22
  intraModuleEdgesOnly?: boolean;
11
23
  colorOffset?: number;
24
+ /** Show translated entity/column labels where the tenant has them. */
25
+ localized?: boolean;
26
+ /**
27
+ * Palette index per module, overriding the default "position in `modules`".
28
+ * Needed when `modules` is an arbitrary subset (a module plus the ones it
29
+ * connects to) that must keep the colors those modules have elsewhere.
30
+ */
31
+ moduleColorIndex?: (moduleCd: string) => number | undefined;
12
32
  }): {
13
33
  nodes: DiagramNode[];
14
34
  edges: DiagramEdge[];
@@ -16,5 +36,40 @@ export declare function buildGraph(modules: ModelModule[], entityMap: Map<number
16
36
  /**
17
37
  * Build the full entityId → label map from a ModelGetResponse.
18
38
  */
19
- export declare function buildEntityMap(response: ModelGetResponse): Map<number, string>;
39
+ export declare function buildEntityMap(response: ModelGetResponse, localized?: boolean): Map<number, string>;
40
+ /**
41
+ * Module codes related to `moduleCd` — the candidate set for a "connected
42
+ * modules" view of that module's diagram.
43
+ *
44
+ * A bridge module (`crm-fin`, `wms-fin`) usually owns no entities at all: it
45
+ * only adds extension columns to the entities of the modules it bridges. Its
46
+ * own diagram is therefore empty, and nothing in the entity graph points back
47
+ * at it either — the columns it contributes are stored on the *target* entity.
48
+ * So the relation is collected from three independent places:
49
+ *
50
+ * 1. declared dependencies — what this module was built on top of. This is
51
+ * the only signal that works for a bridge with no entities.
52
+ * 2. reverse dependencies — modules built on top of this one. A core module
53
+ * has no way of knowing which bridges were installed against it.
54
+ * 3. cross-module references — FK edges leaving or entering this module's
55
+ * entities, which still resolves links that skip a manifest declaration.
56
+ *
57
+ * The module itself and system modules are never included. Results come back in
58
+ * `response.modules` order so chip order and palette indices stay stable.
59
+ */
60
+ export declare function findRelatedModules(response: ModelGetResponse, moduleCd: string): string[];
61
+ /**
62
+ * Expand a chosen set of modules to one that actually draws something.
63
+ *
64
+ * Picking a bridge means "show me what this connects", but the bridge itself
65
+ * has no entities — its whole contribution is extension columns living on the
66
+ * entities of the modules it bridges. Selecting one alone would therefore change
67
+ * the URL and redraw nothing, so each bridge in the set brings its own drawable
68
+ * dependencies along.
69
+ *
70
+ * Expansion is one level deep: a bridge that depends on another bridge is not a
71
+ * shape any module in the wild has, and recursing would quietly pull in half the
72
+ * workspace. Returns the input order with the additions appended, deduped.
73
+ */
74
+ export declare function expandBridgeSelection(response: ModelGetResponse, moduleCds: string[]): string[];
20
75
  //# sourceMappingURL=model-graph.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-graph.d.ts","sourceRoot":"","sources":["../src/lib/model-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEnE,eAAO,MAAM,aAAa,UASzB,CAAC;AAEF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAMD;;;GAGG;AACH,wBAAgB,UAAU,CACzB,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE;IAAE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChE;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,EAAE,WAAW,EAAE,CAAA;CAAE,CA6EhD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ9E"}
1
+ {"version":3,"file":"model-graph.d.ts","sourceRoot":"","sources":["../src/lib/model-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE7F;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,WAAW,EAAE,mBAAiB,KAAG,MACc,CAAC;AAEjF,eAAO,MAAM,WAAW,GAAI,KAAK,WAAW,EAAE,mBAAiB,KAAG,MACc,CAAC;AAEjF,eAAO,MAAM,aAAa,UASzB,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,MACH,CAAC;AAM7C;;;GAGG;AACH,wBAAgB,UAAU,CACzB,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE;IACT,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC5D,GACC;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,EAAE,WAAW,EAAE,CAAA;CAAE,CAoFhD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,EAAE,SAAS,UAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQjG;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CA4CzF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAc/F"}
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Display label for an entity / column.
3
+ *
4
+ * `resLabel` is the install-time label the module author wrote — the same string
5
+ * for every viewer, and the model browser's default because it matches what is in
6
+ * the module package. `resLabelLocalized` is the translation for the viewer's
7
+ * culture, present only where the tenant has one; `localized` opts into it and
8
+ * still falls back per-item, so a partly translated module reads as a mix rather
9
+ * than losing the untranslated half.
10
+ */
11
+ export const entityLabel = (ent, localized = false) => (localized ? ent.resLabelLocalized : undefined) || ent.resLabel || ent.entityCd;
12
+ export const columnLabel = (col, localized = false) => (localized ? col.resLabelLocalized : undefined) || col.resLabel || col.columnCd;
1
13
  export const MODULE_COLORS = [
2
14
  '#0d6efd', // blue
3
15
  '#198754', // green
@@ -8,9 +20,7 @@ export const MODULE_COLORS = [
8
20
  '#d63384', // pink
9
21
  '#0dcaf0', // cyan
10
22
  ];
11
- export function getModuleColor(index) {
12
- return MODULE_COLORS[index % MODULE_COLORS.length];
13
- }
23
+ export const getModuleColor = (index) => MODULE_COLORS[index % MODULE_COLORS.length];
14
24
  const NODE_WIDTH = 260;
15
25
  const NODE_HEIGHT_BASE = 60;
16
26
  const ROW_HEIGHT = 22;
@@ -30,9 +40,11 @@ export function buildGraph(modules, entityMap, options) {
30
40
  }
31
41
  }
32
42
  const offset = options?.colorOffset ?? 0;
43
+ const localized = options?.localized ?? false;
44
+ const colorOf = (moduleCd, mi) => getModuleColor(options?.moduleColorIndex?.(moduleCd) ?? mi + offset);
33
45
  for (let mi = 0; mi < modules.length; mi++) {
34
46
  const mod = modules[mi];
35
- const color = getModuleColor(mi + offset);
47
+ const color = colorOf(mod.moduleCd, mi);
36
48
  for (const ent of mod.entities) {
37
49
  const nodeId = `entity-${ent.entityId}`;
38
50
  entityNodeMap.set(ent.entityId, nodeId);
@@ -45,11 +57,15 @@ export function buildGraph(modules, entityMap, options) {
45
57
  height: nodeHeight,
46
58
  data: {
47
59
  entityCd: ent.entityCd,
48
- resLabel: ent.resLabel,
60
+ resLabel: entityLabel(ent, localized),
49
61
  resIcon: ent.resIcon,
50
62
  moduleCd: mod.moduleCd,
51
63
  moduleColor: color,
52
- columns: ent.columns,
64
+ // Only rewrite the label the node renders; everything else on the
65
+ // column is passed through untouched.
66
+ columns: localized
67
+ ? ent.columns.map(col => ({ ...col, resLabel: columnLabel(col, true) }))
68
+ : ent.columns,
53
69
  entityMap,
54
70
  },
55
71
  });
@@ -58,7 +74,7 @@ export function buildGraph(modules, entityMap, options) {
58
74
  // Build edges for FK columns (type 'R')
59
75
  for (let mi = 0; mi < modules.length; mi++) {
60
76
  const mod = modules[mi];
61
- const color = getModuleColor(mi + offset);
77
+ const color = colorOf(mod.moduleCd, mi);
62
78
  for (const ent of mod.entities) {
63
79
  const sourceNodeId = entityNodeMap.get(ent.entityId);
64
80
  if (!sourceNodeId)
@@ -77,7 +93,7 @@ export function buildGraph(modules, entityMap, options) {
77
93
  sourceHandle: col.columnCd,
78
94
  target: targetNodeId,
79
95
  color,
80
- label: col.resLabel || col.columnCd,
96
+ label: columnLabel(col, localized),
81
97
  isNullable: col.isNullable ?? false,
82
98
  });
83
99
  }
@@ -89,12 +105,110 @@ export function buildGraph(modules, entityMap, options) {
89
105
  /**
90
106
  * Build the full entityId → label map from a ModelGetResponse.
91
107
  */
92
- export function buildEntityMap(response) {
108
+ export function buildEntityMap(response, localized = false) {
93
109
  const entityMap = new Map();
94
110
  for (const mod of response.modules) {
95
111
  for (const ent of mod.entities) {
96
- entityMap.set(ent.entityId, ent.resLabel || ent.entityCd);
112
+ entityMap.set(ent.entityId, entityLabel(ent, localized));
97
113
  }
98
114
  }
99
115
  return entityMap;
100
116
  }
117
+ /** A module with no entities of its own contributes no nodes to a diagram. */
118
+ const isBridge = (mod) => mod.entities.length === 0;
119
+ /**
120
+ * Module codes related to `moduleCd` — the candidate set for a "connected
121
+ * modules" view of that module's diagram.
122
+ *
123
+ * A bridge module (`crm-fin`, `wms-fin`) usually owns no entities at all: it
124
+ * only adds extension columns to the entities of the modules it bridges. Its
125
+ * own diagram is therefore empty, and nothing in the entity graph points back
126
+ * at it either — the columns it contributes are stored on the *target* entity.
127
+ * So the relation is collected from three independent places:
128
+ *
129
+ * 1. declared dependencies — what this module was built on top of. This is
130
+ * the only signal that works for a bridge with no entities.
131
+ * 2. reverse dependencies — modules built on top of this one. A core module
132
+ * has no way of knowing which bridges were installed against it.
133
+ * 3. cross-module references — FK edges leaving or entering this module's
134
+ * entities, which still resolves links that skip a manifest declaration.
135
+ *
136
+ * The module itself and system modules are never included. Results come back in
137
+ * `response.modules` order so chip order and palette indices stay stable.
138
+ */
139
+ export function findRelatedModules(response, moduleCd) {
140
+ const self = response.modules.find(mod => mod.moduleCd === moduleCd);
141
+ if (!self)
142
+ return [];
143
+ const related = new Set(self.dependencies ?? []);
144
+ for (const mod of response.modules) {
145
+ if (mod.moduleCd !== moduleCd && (mod.dependencies ?? []).includes(moduleCd)) {
146
+ related.add(mod.moduleCd);
147
+ }
148
+ }
149
+ const moduleOfEntity = new Map();
150
+ for (const mod of response.modules) {
151
+ for (const ent of mod.entities)
152
+ moduleOfEntity.set(ent.entityId, mod.moduleCd);
153
+ }
154
+ for (const mod of response.modules) {
155
+ for (const ent of mod.entities) {
156
+ for (const col of ent.columns) {
157
+ if (col.columnType !== 'R' || !col.refEntityId)
158
+ continue;
159
+ const targetModuleCd = moduleOfEntity.get(col.refEntityId);
160
+ if (!targetModuleCd || targetModuleCd === mod.moduleCd)
161
+ continue;
162
+ // Outgoing from this module, or incoming into it.
163
+ if (mod.moduleCd === moduleCd)
164
+ related.add(targetModuleCd);
165
+ else if (targetModuleCd === moduleCd)
166
+ related.add(mod.moduleCd);
167
+ }
168
+ }
169
+ }
170
+ // A bridge owns no entities, so on its own it draws nothing — a chip for one
171
+ // is only meaningful next to what it bridges. Pull each related bridge's own
172
+ // dependencies into the candidate set, which is how `fin` becomes reachable
173
+ // from crm's page at all: crm neither depends on fin nor references it, and
174
+ // the only thing tying them together is crm-fin.
175
+ for (const mod of response.modules) {
176
+ if (mod.moduleCd === moduleCd || !related.has(mod.moduleCd))
177
+ continue;
178
+ if (!isBridge(mod))
179
+ continue;
180
+ for (const dep of mod.dependencies ?? [])
181
+ related.add(dep);
182
+ }
183
+ return response.modules
184
+ .filter(mod => mod.moduleCd !== moduleCd && !mod.isSystem && related.has(mod.moduleCd))
185
+ .map(mod => mod.moduleCd);
186
+ }
187
+ /**
188
+ * Expand a chosen set of modules to one that actually draws something.
189
+ *
190
+ * Picking a bridge means "show me what this connects", but the bridge itself
191
+ * has no entities — its whole contribution is extension columns living on the
192
+ * entities of the modules it bridges. Selecting one alone would therefore change
193
+ * the URL and redraw nothing, so each bridge in the set brings its own drawable
194
+ * dependencies along.
195
+ *
196
+ * Expansion is one level deep: a bridge that depends on another bridge is not a
197
+ * shape any module in the wild has, and recursing would quietly pull in half the
198
+ * workspace. Returns the input order with the additions appended, deduped.
199
+ */
200
+ export function expandBridgeSelection(response, moduleCds) {
201
+ const byCode = new Map(response.modules.map(mod => [mod.moduleCd, mod]));
202
+ const expanded = new Set(moduleCds);
203
+ for (const cd of moduleCds) {
204
+ const mod = byCode.get(cd);
205
+ if (!mod || !isBridge(mod))
206
+ continue;
207
+ for (const dep of mod.dependencies ?? []) {
208
+ const target = byCode.get(dep);
209
+ if (target && !target.isSystem && !isBridge(target))
210
+ expanded.add(dep);
211
+ }
212
+ }
213
+ return [...expanded];
214
+ }
@@ -15,6 +15,8 @@ export interface ModelModule {
15
15
  license?: string;
16
16
  authorName?: string;
17
17
  tags?: string;
18
+ /** Module codes this module declares a dependency on (manifest `dependencies` keys). */
19
+ dependencies?: string[];
18
20
  entities: ModelEntity[];
19
21
  }
20
22
  export interface ModelEntity {
@@ -25,6 +27,8 @@ export interface ModelEntity {
25
27
  dbObject?: string;
26
28
  toString?: string;
27
29
  resLabel?: string;
30
+ /** Label for the viewer's culture, when a translation exists. Absent = fall back to `resLabel`. */
31
+ resLabelLocalized?: string;
28
32
  resIcon?: string;
29
33
  columns: ModelColumn[];
30
34
  constraints: ModelConstraint[];
@@ -52,5 +56,7 @@ export interface ModelColumn {
52
56
  thisKey?: string;
53
57
  otherKey?: string;
54
58
  resLabel?: string;
59
+ /** Label for the viewer's culture, when a translation exists. Absent = fall back to `resLabel`. */
60
+ resLabelLocalized?: string;
55
61
  }
56
62
  //# sourceMappingURL=model-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-types.d.ts","sourceRoot":"","sources":["../src/lib/model-types.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEvF,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,WAAW,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,WAAW,EAAE,eAAe,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,cAAc,EAAE,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB"}
1
+ {"version":3,"file":"model-types.d.ts","sourceRoot":"","sources":["../src/lib/model-types.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEvF,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mGAAmG;IACnG,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,WAAW,EAAE,eAAe,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,cAAc,EAAE,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mGAAmG;IACnG,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B"}
package/package.json CHANGED
@@ -1,58 +1,60 @@
1
1
  {
2
- "name": "@dforge-core/diagram",
3
- "version": "0.0.3",
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.2"
43
- },
44
- "peerDependencies": {
45
- "svelte": "^5.0.0"
46
- },
47
- "devDependencies": {
48
- "@sveltejs/package": "^2.5.7",
49
- "@sveltejs/vite-plugin-svelte": "^7.1.2",
50
- "svelte": "^5.55.10",
51
- "typescript": "^6.0.2"
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
+ }