@dforge-core/diagram 0.0.3 → 0.0.4

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.
@@ -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;
@@ -173,10 +180,14 @@
173
180
 
174
181
  // --- Delegated event helpers ---
175
182
 
176
- /** Walk up from event target to find a data attribute (works across HTML/SVG boundary in foreignObject). */
183
+ /**
184
+ * Walk up from event target to find a data attribute. Delegation runs on the
185
+ * container, so nodes (HTML overlay) and edges/handles (SVG) both resolve here;
186
+ * we stop at the container element.
187
+ */
177
188
  function closest(target: Element, attr: string): Element | null {
178
189
  let el: Element | null = target;
179
- while (el && el !== svgEl) {
190
+ while (el && el !== containerEl) {
180
191
  if (el.hasAttribute(attr)) return el;
181
192
  // parentElement is null at HTML↔SVG boundary in some browsers; fall back to parentNode
182
193
  el = el.parentElement ?? (el.parentNode as Element | null);
@@ -193,6 +204,11 @@
193
204
  function handlePointerDown(e: PointerEvent) {
194
205
  const target = e.target as Element;
195
206
 
207
+ // Ignore interactions that originate on the overlaid controls (buttons
208
+ // handle their own clicks). Handlers now live on the container, so control
209
+ // clicks would otherwise reach this delegated handler.
210
+ if (target.closest('.diagram-controls')) return;
211
+
196
212
  // Segment drag handle
197
213
  const handle = closest(target, 'data-handle');
198
214
  if (handle) {
@@ -413,6 +429,25 @@
413
429
  requestAnimationFrame(() => fitView());
414
430
  }
415
431
 
432
+ // Refit on fitKey change. Skips the very first run so it doesn't fight the
433
+ // restored transform on mount.
434
+ //
435
+ // The first fit is synchronous, not deferred to a frame: a host entering a
436
+ // print layout flips fitKey inside `beforeprint` and flushes, and the print
437
+ // snapshot is taken as soon as that handler returns — anything left to rAF
438
+ // misses it entirely. Measuring here forces the pending layout, so the new
439
+ // container size is already available. The rAF pass after it is a cheap
440
+ // idempotent safety net for hosts whose box settles a frame late (CSS
441
+ // transitions on the shell); re-fitting to the same bounds is a no-op.
442
+ let fitKeySeen = false;
443
+ $effect(() => {
444
+ fitKey; // track
445
+ if (fitKey === undefined) return;
446
+ if (!fitKeySeen) { fitKeySeen = true; return; }
447
+ fitView();
448
+ requestAnimationFrame(() => fitView());
449
+ });
450
+
416
451
  onMount(() => {
417
452
  svgEl.addEventListener('wheel', pz.handleWheel, { passive: false });
418
453
  if (!restoredTransform) {
@@ -425,15 +460,18 @@
425
460
  });
426
461
  </script>
427
462
 
428
- <div class="model-diagram" bind:this={containerEl}>
429
- <!-- svelte-ignore a11y_no_static_element_interactions -->
463
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
464
+ <div
465
+ class="model-diagram"
466
+ bind:this={containerEl}
467
+ onpointerdown={handlePointerDown}
468
+ onpointermove={handlePointerMove}
469
+ onpointerup={handlePointerUp}
470
+ ondblclick={handleDblClick}
471
+ >
430
472
  <svg
431
473
  bind:this={svgEl}
432
474
  role="application"
433
- onpointerdown={handlePointerDown}
434
- onpointermove={handlePointerMove}
435
- onpointerup={handlePointerUp}
436
- ondblclick={handleDblClick}
437
475
  >
438
476
  <defs>
439
477
  <pattern id={patternId} width="20" height="20" patternUnits="userSpaceOnUse"
@@ -503,24 +541,6 @@
503
541
  {/if}
504
542
  {/each}
505
543
 
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
544
  <!-- Selected edge on top of nodes -->
525
545
  {#if selectedEdgeId}
526
546
  {#each resolvedEdges as edge (edge.id)}
@@ -587,6 +607,32 @@
587
607
  </g>
588
608
  </svg>
589
609
 
610
+ <!--
611
+ Nodes render in an HTML overlay layer (not SVG <foreignObject>) so they
612
+ scale correctly in Safari/WebKit, which does not apply a scaled SVG
613
+ ancestor transform to foreignObject HTML content. The layer shares the
614
+ same pan/zoom transform as the SVG <g> above. Pointer events are handled
615
+ by delegation on the container (see handlePointerDown), so nodes only need
616
+ their data-node-id and to opt back into pointer events.
617
+ -->
618
+ <div
619
+ class="node-layer"
620
+ style:transform="translate({pz.transform.x}px, {pz.transform.y}px) scale({pz.transform.k})"
621
+ style:transform-origin="0 0"
622
+ >
623
+ {#each nodes as node (node.id)}
624
+ {@const pos = nodePositions[node.id] ?? node}
625
+ <div
626
+ class="diagram-node"
627
+ class:selected={selectedNodeIds.has(node.id)}
628
+ style="left: {pos.x}px; top: {pos.y}px; width: {node.width}px; height: {node.height}px;"
629
+ data-node-id={node.id}
630
+ >
631
+ <EntityNode data={node.data} />
632
+ </div>
633
+ {/each}
634
+ </div>
635
+
590
636
  <!-- Controls -->
591
637
  <div class="diagram-controls">
592
638
  <IconButton icon="bi-plus-lg" title={t.zoom_in} size="sm" onclick={() => pz.zoomBy(1.3)} />
@@ -615,16 +661,28 @@
615
661
  cursor: grabbing;
616
662
  }
617
663
 
618
- .diagram-node-fo {
664
+ .node-layer {
665
+ position: absolute;
666
+ inset: 0;
667
+ /* Empty areas let pointer events fall through to the SVG (pan / area
668
+ select / edge hit areas); individual nodes re-enable events below.
669
+ transform / transform-origin are applied via style: directives so the
670
+ layer tracks the SVG <g> transform exactly. */
671
+ pointer-events: none;
672
+ }
673
+
674
+ .diagram-node {
675
+ position: absolute;
676
+ box-sizing: border-box;
677
+ pointer-events: auto;
619
678
  cursor: grab;
620
- overflow: visible;
621
679
  }
622
680
 
623
- .diagram-node-fo.selected {
681
+ .diagram-node.selected {
624
682
  filter: drop-shadow(0 0 4px var(--bs-primary, #0d6efd));
625
683
  }
626
684
 
627
- .diagram-node-fo:active {
685
+ .diagram-node:active {
628
686
  cursor: grabbing;
629
687
  }
630
688
 
@@ -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;AA+iBF,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/diagram",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Entity-relationship and workspace diagram components for dForge models. Standalone Svelte 5 library with no dForge runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "dForge",
@@ -39,16 +39,16 @@
39
39
  "dist"
40
40
  ],
41
41
  "dependencies": {
42
- "@dforge-core/metadata": "0.0.2"
42
+ "@dforge-core/metadata": "0.0.22"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "svelte": "^5.0.0"
46
46
  },
47
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"
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
52
  },
53
53
  "scripts": {
54
54
  "build": "svelte-package",