@diagrammo/dgmo 0.4.3 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -335,7 +335,7 @@ function formatUptime(fraction: number): string {
335
335
  // Layout engine
336
336
  // ============================================================
337
337
 
338
- export function layoutInfra(computed: ComputedInfraModel, selectedNodeId?: string | null): InfraLayoutResult {
338
+ export function layoutInfra(computed: ComputedInfraModel, selectedNodeId?: string | null, collapsedNodes?: Set<string> | null): InfraLayoutResult {
339
339
  if (computed.nodes.length === 0) {
340
340
  return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
341
341
  }
@@ -363,9 +363,12 @@ export function layoutInfra(computed: ComputedInfraModel, selectedNodeId?: strin
363
363
  const widthMap = new Map<string, number>();
364
364
  const heightMap = new Map<string, number>();
365
365
  for (const node of computed.nodes) {
366
- const expanded = node.id === selectedNodeId;
366
+ const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
367
+ const expanded = !isNodeCollapsed && node.id === selectedNodeId;
367
368
  const width = computeNodeWidth(node, expanded, computed.options);
368
- const height = computeNodeHeight(node, expanded, computed.options);
369
+ const height = isNodeCollapsed
370
+ ? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM
371
+ : computeNodeHeight(node, expanded, computed.options);
369
372
  widthMap.set(node.id, width);
370
373
  heightMap.set(node.id, height);
371
374
  const inGroup = groupedNodeIds.has(node.id);
@@ -43,8 +43,8 @@ const TAG_VALUE_RE = /^(\w[\w\s]*?)(?:\(([^)]+)\))?(\s+default)?\s*$/;
43
43
  // Component line: ComponentName or ComponentName | t: Backend | env: Prod
44
44
  const COMPONENT_RE = /^([a-zA-Z_][\w]*)(.*)$/;
45
45
 
46
- // Pipe metadata: | key: value
47
- const PIPE_META_RE = /\|\s*(\w+)\s*:\s*([^|]+)/g;
46
+ // Pipe metadata: | key: value or | k1: v1, k2: v2 (comma-separated)
47
+ const PIPE_META_RE = /[|,]\s*(\w+)\s*:\s*([^|,]+)/g;
48
48
 
49
49
  // Property: key: value
50
50
  const PROPERTY_RE = /^([\w-]+)\s*:\s*(.+)$/;
@@ -606,6 +606,23 @@ function renderEdgeLabels(
606
606
  }
607
607
  }
608
608
 
609
+ /** Returns the resolved tag color for a node's active tag group, or null if not applicable. */
610
+ function resolveActiveTagStroke(
611
+ node: InfraLayoutNode,
612
+ activeGroup: string,
613
+ tagGroups: InfraTagGroup[],
614
+ palette: PaletteColors,
615
+ ): string | null {
616
+ const tg = tagGroups.find((t) => t.name.toLowerCase() === activeGroup.toLowerCase());
617
+ if (!tg) return null;
618
+ const tagKey = (tg.alias ?? tg.name).toLowerCase();
619
+ const tagVal = node.tags[tagKey];
620
+ if (!tagVal) return null;
621
+ const tv = tg.values.find((v) => v.name.toLowerCase() === tagVal.toLowerCase());
622
+ if (!tv?.color) return null;
623
+ return resolveColor(tv.color, palette);
624
+ }
625
+
609
626
  function renderNodes(
610
627
  svg: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
611
628
  nodes: InfraLayoutNode[],
@@ -615,11 +632,22 @@ function renderNodes(
615
632
  selectedNodeId?: string | null,
616
633
  activeGroup?: string | null,
617
634
  diagramOptions?: Record<string, string>,
635
+ collapsedNodes?: Set<string> | null,
636
+ tagGroups?: InfraTagGroup[],
618
637
  ) {
619
638
  const mutedColor = palette.textMuted;
620
639
 
621
640
  for (const node of nodes) {
622
- const { fill, stroke, textFill } = nodeColor(node, palette, isDark);
641
+ let { fill, stroke, textFill } = nodeColor(node, palette, isDark);
642
+
643
+ // When a tag legend is active, override border color with tag color
644
+ if (activeGroup && tagGroups && !node.isEdge) {
645
+ const tagStroke = resolveActiveTagStroke(node, activeGroup, tagGroups, palette);
646
+ if (tagStroke) {
647
+ stroke = tagStroke;
648
+ fill = mix(palette.bg, tagStroke, isDark ? 88 : 94);
649
+ }
650
+ }
623
651
  let cls = 'infra-node';
624
652
  if (animate && node.isEdge) {
625
653
  cls += ' infra-node-edge-throb';
@@ -632,12 +660,13 @@ function renderNodes(
632
660
  const g = svg.append('g')
633
661
  .attr('class', cls)
634
662
  .attr('data-line-number', node.lineNumber)
635
- .attr('data-infra-node', node.id);
663
+ .attr('data-infra-node', node.id)
664
+ .attr('data-node-collapse', node.id)
665
+ .style('cursor', 'pointer');
636
666
 
637
- // Collapsed group nodes: toggle attribute + pointer cursor
667
+ // Collapsed group nodes: toggle attribute
638
668
  if (node.id.startsWith('[')) {
639
- g.attr('data-node-toggle', node.id)
640
- .style('cursor', 'pointer');
669
+ g.attr('data-node-toggle', node.id);
641
670
  }
642
671
 
643
672
  // Expose tag values for legend hover dimming
@@ -681,8 +710,22 @@ function renderNodes(
681
710
  .attr('fill', textFill)
682
711
  .text(node.label);
683
712
 
684
- // --- Key-value rows below header ---
685
- {
713
+ // --- Key-value rows below header (skipped for collapsed nodes) ---
714
+ const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
715
+ if (isNodeCollapsed) {
716
+ // Collapsed: show a subtle chevron indicator at the bottom of the header
717
+ const chevronY = y + node.height - 6;
718
+ g.append('text')
719
+ .attr('x', node.x)
720
+ .attr('y', chevronY)
721
+ .attr('text-anchor', 'middle')
722
+ .attr('font-family', FONT_FAMILY)
723
+ .attr('font-size', 8)
724
+ .attr('fill', textFill)
725
+ .attr('opacity', 0.5)
726
+ .text('▼');
727
+ }
728
+ if (!isNodeCollapsed) {
686
729
  const expanded = node.id === selectedNodeId;
687
730
  // Declared properties only shown when node is selected (expanded)
688
731
  const displayProps = (!node.isEdge && expanded) ? getDisplayProps(node, expanded, diagramOptions) : [];
@@ -1380,6 +1423,7 @@ export function renderInfra(
1380
1423
  playback?: InfraPlaybackState | null,
1381
1424
  selectedNodeId?: string | null,
1382
1425
  exportMode?: boolean,
1426
+ collapsedNodes?: Set<string> | null,
1383
1427
  ) {
1384
1428
  // Clear previous render (preserve tooltips if any)
1385
1429
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -1470,7 +1514,7 @@ export function renderInfra(
1470
1514
  // Render layers: groups (back), edge paths, nodes, reject particles, edge labels (front)
1471
1515
  renderGroups(svg, layout.groups, palette, isDark);
1472
1516
  renderEdgePaths(svg, layout.edges, layout.nodes, palette, isDark, shouldAnimate);
1473
- renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, selectedNodeId, activeGroup, layout.options);
1517
+ renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, selectedNodeId, activeGroup, layout.options, collapsedNodes, tagGroups ?? []);
1474
1518
  if (shouldAnimate) {
1475
1519
  renderRejectParticles(svg, layout.nodes);
1476
1520
  }
package/src/sharing.ts CHANGED
@@ -8,6 +8,7 @@ const COMPRESSED_SIZE_LIMIT = 8192; // 8 KB
8
8
 
9
9
  export interface DiagramViewState {
10
10
  activeTagGroup?: string;
11
+ collapsedGroups?: string[];
11
12
  }
12
13
 
13
14
  export interface DecodedDiagramUrl {
@@ -47,6 +48,10 @@ export function encodeDiagramUrl(
47
48
  hash += `&tag=${encodeURIComponent(options.viewState.activeTagGroup)}`;
48
49
  }
49
50
 
51
+ if (options?.viewState?.collapsedGroups?.length) {
52
+ hash += `&cg=${encodeURIComponent(options.viewState.collapsedGroups.join(','))}`;
53
+ }
54
+
50
55
  // Encode in both query param AND hash fragment — some share mechanisms
51
56
  // strip one or the other (iOS share sheet strips #, AirDrop strips ?)
52
57
  return { url: `${baseUrl}?${hash}#${hash}` };
@@ -89,6 +94,9 @@ export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
89
94
  if (key === 'tag' && val) {
90
95
  viewState.activeTagGroup = val;
91
96
  }
97
+ if (key === 'cg' && val) {
98
+ viewState.collapsedGroups = val.split(',').filter(Boolean);
99
+ }
92
100
  }
93
101
 
94
102
  // Strip 'dgmo=' prefix