@diagrammo/dgmo 0.8.18 → 0.8.19

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/dist/index.cjs CHANGED
@@ -2102,7 +2102,7 @@ function measureLegendText(text, fontSize) {
2102
2102
  }
2103
2103
  return w;
2104
2104
  }
2105
- var LEGEND_HEIGHT, LEGEND_PILL_PAD, LEGEND_PILL_FONT_SIZE, LEGEND_CAPSULE_PAD, LEGEND_DOT_R, LEGEND_ENTRY_FONT_SIZE, LEGEND_ENTRY_DOT_GAP, LEGEND_ENTRY_TRAIL, LEGEND_GROUP_GAP, LEGEND_EYE_SIZE, LEGEND_EYE_GAP, LEGEND_ICON_W, LEGEND_MAX_ENTRY_ROWS, CHAR_W, DEFAULT_W, EYE_OPEN_PATH, EYE_CLOSED_PATH;
2105
+ var LEGEND_HEIGHT, LEGEND_PILL_PAD, LEGEND_PILL_FONT_SIZE, LEGEND_CAPSULE_PAD, LEGEND_DOT_R, LEGEND_ENTRY_FONT_SIZE, LEGEND_ENTRY_DOT_GAP, LEGEND_ENTRY_TRAIL, LEGEND_GROUP_GAP, LEGEND_EYE_SIZE, LEGEND_EYE_GAP, LEGEND_ICON_W, LEGEND_MAX_ENTRY_ROWS, CHAR_W, DEFAULT_W, EYE_OPEN_PATH, EYE_CLOSED_PATH, CONTROLS_ICON_PATH, LEGEND_TOGGLE_DOT_R, LEGEND_TOGGLE_OFF_OPACITY, LEGEND_GEAR_PILL_W;
2106
2106
  var init_legend_constants = __esm({
2107
2107
  "src/utils/legend-constants.ts"() {
2108
2108
  "use strict";
@@ -2209,6 +2209,10 @@ var init_legend_constants = __esm({
2209
2209
  DEFAULT_W = 0.56;
2210
2210
  EYE_OPEN_PATH = "M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z";
2211
2211
  EYE_CLOSED_PATH = "M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1";
2212
+ CONTROLS_ICON_PATH = "M5.6 1.7L8.4 1.7L7.9 3.6L9.5 4.5L10.9 3.1L12.3 5.6L10.4 6.1L10.4 7.9L12.3 8.4L10.9 10.9L9.5 9.5L7.9 10.4L8.4 12.3L5.6 12.3L6.1 10.4L4.5 9.5L3.1 10.9L1.7 8.4L3.6 7.9L3.6 6.1L1.7 5.6L3.1 3.1L4.5 4.5L6.1 3.6ZM5 7a2 2 0 1 0 4 0a2 2 0 1 0-4 0Z";
2213
+ LEGEND_TOGGLE_DOT_R = LEGEND_DOT_R;
2214
+ LEGEND_TOGGLE_OFF_OPACITY = 0.4;
2215
+ LEGEND_GEAR_PILL_W = 14 + LEGEND_PILL_PAD;
2212
2216
  }
2213
2217
  });
2214
2218
 
@@ -2282,6 +2286,63 @@ function capsuleWidth(name, entries, containerWidth, addonWidth = 0) {
2282
2286
  visibleEntries: entries.length
2283
2287
  };
2284
2288
  }
2289
+ function controlsGroupCapsuleWidth(toggles) {
2290
+ let w = LEGEND_CAPSULE_PAD * 2 + LEGEND_GEAR_PILL_W + 4;
2291
+ for (const t of toggles) {
2292
+ w += LEGEND_TOGGLE_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(t.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
2293
+ }
2294
+ return w;
2295
+ }
2296
+ function buildControlsGroupLayout(config, state) {
2297
+ const cg = config.controlsGroup;
2298
+ if (!cg || cg.toggles.length === 0) return void 0;
2299
+ const expanded = !!state.controlsExpanded;
2300
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
2301
+ if (!expanded) {
2302
+ return {
2303
+ x: 0,
2304
+ y: 0,
2305
+ width: LEGEND_GEAR_PILL_W,
2306
+ height: LEGEND_HEIGHT,
2307
+ expanded: false,
2308
+ pill: { x: 0, y: 0, width: LEGEND_GEAR_PILL_W, height: LEGEND_HEIGHT },
2309
+ toggles: []
2310
+ };
2311
+ }
2312
+ const capsuleW = controlsGroupCapsuleWidth(cg.toggles);
2313
+ const toggleLayouts = [];
2314
+ let tx = LEGEND_CAPSULE_PAD + LEGEND_GEAR_PILL_W + 4;
2315
+ for (const toggle of cg.toggles) {
2316
+ const dotCx = tx + LEGEND_TOGGLE_DOT_R;
2317
+ const dotCy = LEGEND_HEIGHT / 2;
2318
+ const textX = tx + LEGEND_TOGGLE_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
2319
+ const textY = LEGEND_HEIGHT / 2;
2320
+ toggleLayouts.push({
2321
+ id: toggle.id,
2322
+ label: toggle.label,
2323
+ active: toggle.active,
2324
+ dotCx,
2325
+ dotCy,
2326
+ textX,
2327
+ textY
2328
+ });
2329
+ tx += LEGEND_TOGGLE_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(toggle.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
2330
+ }
2331
+ return {
2332
+ x: 0,
2333
+ y: 0,
2334
+ width: capsuleW,
2335
+ height: LEGEND_HEIGHT,
2336
+ expanded: true,
2337
+ pill: {
2338
+ x: LEGEND_CAPSULE_PAD,
2339
+ y: LEGEND_CAPSULE_PAD,
2340
+ width: LEGEND_GEAR_PILL_W - LEGEND_CAPSULE_PAD * 2,
2341
+ height: pillH
2342
+ },
2343
+ toggles: toggleLayouts
2344
+ };
2345
+ }
2285
2346
  function computeLegendLayout(config, state, containerWidth) {
2286
2347
  const { groups, controls: configControls, mode } = config;
2287
2348
  const isExport = mode === "inline";
@@ -2296,8 +2357,9 @@ function computeLegendLayout(config, state, containerWidth) {
2296
2357
  activeCapsule: void 0
2297
2358
  };
2298
2359
  }
2360
+ const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
2299
2361
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
2300
- if (visibleGroups.length === 0 && (!configControls || configControls.length === 0)) {
2362
+ if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
2301
2363
  return {
2302
2364
  height: 0,
2303
2365
  width: 0,
@@ -2361,7 +2423,8 @@ function computeLegendLayout(config, state, containerWidth) {
2361
2423
  if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2362
2424
  }
2363
2425
  const controlsSpace = totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0;
2364
- const groupAvailW = containerWidth - controlsSpace;
2426
+ const gearSpace = controlsGroupLayout ? controlsGroupLayout.width + LEGEND_GROUP_GAP : 0;
2427
+ const groupAvailW = containerWidth - controlsSpace - gearSpace;
2365
2428
  const pills = [];
2366
2429
  let activeCapsule;
2367
2430
  for (const g of visibleGroups) {
@@ -2370,7 +2433,7 @@ function computeLegendLayout(config, state, containerWidth) {
2370
2433
  if (isActive) {
2371
2434
  activeCapsule = buildCapsuleLayout(
2372
2435
  g,
2373
- containerWidth,
2436
+ groupAvailW,
2374
2437
  config.capsulePillAddonWidth ?? 0
2375
2438
  );
2376
2439
  } else {
@@ -2393,7 +2456,8 @@ function computeLegendLayout(config, state, containerWidth) {
2393
2456
  groupAvailW,
2394
2457
  containerWidth,
2395
2458
  totalControlsW,
2396
- alignLeft
2459
+ alignLeft,
2460
+ controlsGroupLayout
2397
2461
  );
2398
2462
  const height = rows.length * LEGEND_HEIGHT;
2399
2463
  const width = containerWidth;
@@ -2403,7 +2467,8 @@ function computeLegendLayout(config, state, containerWidth) {
2403
2467
  rows,
2404
2468
  activeCapsule,
2405
2469
  controls: controlLayouts,
2406
- pills
2470
+ pills,
2471
+ controlsGroup: controlsGroupLayout
2407
2472
  };
2408
2473
  }
2409
2474
  function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
@@ -2467,19 +2532,27 @@ function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
2467
2532
  addonX: addonWidth > 0 ? LEGEND_CAPSULE_PAD + pw + 4 : void 0
2468
2533
  };
2469
2534
  }
2470
- function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW, alignLeft = false) {
2535
+ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW, alignLeft = false, controlsGroup) {
2471
2536
  const rows = [];
2472
2537
  const groupItems = [];
2473
2538
  if (activeCapsule) groupItems.push(activeCapsule);
2474
2539
  groupItems.push(...pills);
2540
+ const gearW = controlsGroup ? controlsGroup.width + LEGEND_GROUP_GAP : 0;
2475
2541
  let currentRowItems = [];
2476
2542
  let currentRowW = 0;
2477
2543
  let rowY = 0;
2478
2544
  for (const item of groupItems) {
2479
2545
  const itemW = item.width + LEGEND_GROUP_GAP;
2480
2546
  if (currentRowW + item.width > groupAvailW && currentRowItems.length > 0) {
2481
- if (!alignLeft)
2482
- centerRowItems(currentRowItems, containerWidth, totalControlsW);
2547
+ if (!alignLeft) {
2548
+ const rowGearW = rows.length === 0 ? gearW : 0;
2549
+ centerRowItems(
2550
+ currentRowItems,
2551
+ containerWidth,
2552
+ totalControlsW,
2553
+ rowGearW
2554
+ );
2555
+ }
2483
2556
  rows.push({ y: rowY, items: currentRowItems });
2484
2557
  rowY += LEGEND_HEIGHT;
2485
2558
  currentRowItems = [];
@@ -2507,19 +2580,32 @@ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth,
2507
2580
  }
2508
2581
  }
2509
2582
  if (currentRowItems.length > 0) {
2510
- centerRowItems(currentRowItems, containerWidth, totalControlsW);
2583
+ centerRowItems(currentRowItems, containerWidth, totalControlsW, gearW);
2511
2584
  rows.push({ y: rowY, items: currentRowItems });
2512
2585
  }
2586
+ if (controlsGroup) {
2587
+ const row0Items = rows[0]?.items ?? [];
2588
+ const groupItemsInRow0 = row0Items.filter(
2589
+ (it) => "groupName" in it
2590
+ );
2591
+ if (groupItemsInRow0.length > 0) {
2592
+ const last = groupItemsInRow0[groupItemsInRow0.length - 1];
2593
+ controlsGroup.x = last.x + last.width + LEGEND_GROUP_GAP;
2594
+ } else {
2595
+ controlsGroup.x = 0;
2596
+ }
2597
+ controlsGroup.y = 0;
2598
+ }
2513
2599
  if (rows.length === 0) {
2514
2600
  rows.push({ y: 0, items: [] });
2515
2601
  }
2516
2602
  return rows;
2517
2603
  }
2518
- function centerRowItems(items, containerWidth, totalControlsW) {
2604
+ function centerRowItems(items, containerWidth, totalControlsW, controlsGroupW = 0) {
2519
2605
  const groupItems = items.filter((it) => "groupName" in it);
2520
2606
  if (groupItems.length === 0) return;
2521
2607
  const totalGroupW = groupItems.reduce((s, it) => s + it.width, 0) + (groupItems.length - 1) * LEGEND_GROUP_GAP;
2522
- const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0);
2608
+ const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0) - controlsGroupW;
2523
2609
  const offset = Math.max(0, (availW - totalGroupW) / 2);
2524
2610
  let x = offset;
2525
2611
  for (const item of groupItems) {
@@ -2577,6 +2663,17 @@ function renderLegendD3(container, config, state, palette, isDark, callbacks, co
2577
2663
  for (const pill of currentLayout.pills) {
2578
2664
  renderPill(legendG, pill, palette, groupBg, callbacks);
2579
2665
  }
2666
+ if (currentLayout.controlsGroup) {
2667
+ renderControlsGroup(
2668
+ legendG,
2669
+ currentLayout.controlsGroup,
2670
+ palette,
2671
+ groupBg,
2672
+ pillBorder,
2673
+ callbacks,
2674
+ config
2675
+ );
2676
+ }
2580
2677
  for (const ctrl of currentLayout.controls) {
2581
2678
  renderControl(
2582
2679
  legendG,
@@ -2691,6 +2788,57 @@ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, con
2691
2788
  g.on("click", () => onClick());
2692
2789
  }
2693
2790
  }
2791
+ function renderControlsGroup(parent, layout, palette, groupBg, pillBorder, callbacks, config) {
2792
+ const g = parent.append("g").attr("transform", `translate(${layout.x},${layout.y})`).attr("data-legend-controls", layout.expanded ? "expanded" : "collapsed").attr("data-export-ignore", "true").style("cursor", "pointer");
2793
+ if (!layout.expanded) {
2794
+ g.append("rect").attr("width", layout.width).attr("height", layout.height).attr("rx", layout.height / 2).attr("fill", groupBg);
2795
+ const iconSize = 14;
2796
+ const iconX = (layout.width - iconSize) / 2;
2797
+ const iconY = (layout.height - iconSize) / 2;
2798
+ g.append("path").attr("d", CONTROLS_ICON_PATH).attr("transform", `translate(${iconX},${iconY})`).attr("fill", palette.textMuted).attr("fill-rule", "evenodd").attr("pointer-events", "none");
2799
+ if (callbacks?.onControlsExpand) {
2800
+ const cb = callbacks.onControlsExpand;
2801
+ g.on("click", () => cb());
2802
+ }
2803
+ } else {
2804
+ const pill = layout.pill;
2805
+ g.append("rect").attr("width", layout.width).attr("height", layout.height).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
2806
+ const pillG = g.append("g").attr("class", "controls-gear-pill").style("cursor", "pointer");
2807
+ pillG.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", palette.bg);
2808
+ pillG.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", "none").attr("stroke", pillBorder).attr("stroke-width", 0.75);
2809
+ const iconSize = 14;
2810
+ const iconX = pill.x + (pill.width - iconSize) / 2;
2811
+ const iconY = pill.y + (pill.height - iconSize) / 2;
2812
+ pillG.append("path").attr("d", CONTROLS_ICON_PATH).attr("transform", `translate(${iconX},${iconY})`).attr("fill", palette.text).attr("fill-rule", "evenodd").attr("pointer-events", "none");
2813
+ if (callbacks?.onControlsExpand) {
2814
+ const cb = callbacks.onControlsExpand;
2815
+ pillG.on("click", (event) => {
2816
+ event.stopPropagation();
2817
+ cb();
2818
+ });
2819
+ }
2820
+ const toggles = config?.controlsGroup?.toggles ?? [];
2821
+ for (const tl of layout.toggles) {
2822
+ const toggle = toggles.find((t) => t.id === tl.id);
2823
+ const entryG = g.append("g").attr("data-controls-toggle", tl.id).style("cursor", "pointer");
2824
+ if (tl.active) {
2825
+ entryG.append("circle").attr("cx", tl.dotCx).attr("cy", tl.dotCy).attr("r", LEGEND_TOGGLE_DOT_R).attr("fill", palette.primary ?? palette.text);
2826
+ } else {
2827
+ entryG.append("circle").attr("cx", tl.dotCx).attr("cy", tl.dotCy).attr("r", LEGEND_TOGGLE_DOT_R).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1);
2828
+ }
2829
+ entryG.append("text").attr("x", tl.textX).attr("y", tl.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("opacity", tl.active ? 1 : LEGEND_TOGGLE_OFF_OPACITY).attr("font-family", FONT_FAMILY).text(tl.label);
2830
+ if (callbacks?.onControlsToggle && toggle) {
2831
+ const cb = callbacks.onControlsToggle;
2832
+ const id = tl.id;
2833
+ const newActive = !tl.active;
2834
+ entryG.on("click", (event) => {
2835
+ event.stopPropagation();
2836
+ cb(id, newActive);
2837
+ });
2838
+ }
2839
+ }
2840
+ }
2841
+ }
2694
2842
  var init_legend_d3 = __esm({
2695
2843
  "src/utils/legend-d3.ts"() {
2696
2844
  "use strict";
@@ -3244,7 +3392,13 @@ function parseSequenceDgmo(content) {
3244
3392
  const groupName = groupMatch[1].trim();
3245
3393
  const groupColor = groupMatch[2]?.trim();
3246
3394
  let groupMeta;
3247
- const afterBracket = groupMatch[3]?.trim() || "";
3395
+ let afterBracket = groupMatch[3]?.trim() || "";
3396
+ let isCollapsed = false;
3397
+ const collapseMatch = afterBracket.match(/^collapse\b/i);
3398
+ if (collapseMatch) {
3399
+ isCollapsed = true;
3400
+ afterBracket = afterBracket.slice(collapseMatch[0].length).trim();
3401
+ }
3248
3402
  if (afterBracket.startsWith("|")) {
3249
3403
  const segments = afterBracket.split("|");
3250
3404
  const meta = parsePipeMetadata(
@@ -3265,7 +3419,8 @@ function parseSequenceDgmo(content) {
3265
3419
  name: groupName,
3266
3420
  participantIds: [],
3267
3421
  lineNumber,
3268
- ...groupMeta ? { metadata: groupMeta } : {}
3422
+ ...groupMeta ? { metadata: groupMeta } : {},
3423
+ ...isCollapsed ? { collapsed: true } : {}
3269
3424
  };
3270
3425
  result.groups.push(activeGroup);
3271
3426
  continue;
@@ -23449,6 +23604,8 @@ function renderGantt(container, resolved, palette, isDark, options, exportDims)
23449
23604
  options?.currentActiveGroup
23450
23605
  );
23451
23606
  let criticalPathActive = false;
23607
+ let dependenciesActive = !!resolved.options.dependencies;
23608
+ let controlsExpanded = false;
23452
23609
  const tagRows = currentSwimlaneGroup ? buildTagLaneRowList(resolved, currentSwimlaneGroup, collapsedLanes) : null;
23453
23610
  const rows = tagRows ?? buildRowList(resolved, collapsedGroups);
23454
23611
  const isTagMode = tagRows !== null;
@@ -23465,9 +23622,11 @@ function renderGantt(container, resolved, palette, isDark, options, exportDims)
23465
23622
  const maxLabelLen = Math.max(...allLabels.map((l) => l.length), 10);
23466
23623
  const leftMargin = Math.max(MIN_LEFT_MARGIN, maxLabelLen * 7 + 30);
23467
23624
  const totalRows = rows.length;
23625
+ const hasCriticalPath = resolved.options.criticalPath && resolved.tasks.some((t) => t.isCriticalPath);
23626
+ const hasDependencies = resolved.options.dependencies && resolved.tasks.some((t) => t.task.dependencies.length > 0);
23468
23627
  const title = resolved.options.title;
23469
23628
  const titleHeight = title ? 50 : 20;
23470
- const tagLegendReserve = resolved.tagGroups.length > 0 ? LEGEND_HEIGHT + 8 : 0;
23629
+ const tagLegendReserve = resolved.tagGroups.length > 0 || hasCriticalPath || hasDependencies ? LEGEND_HEIGHT + 8 : 0;
23471
23630
  const topDateLabelReserve = 22;
23472
23631
  const hasOverheadLabels = resolved.markers.length > 0 || resolved.eras.length > 0;
23473
23632
  const markerLabelReserve = hasOverheadLabels ? 28 : 0;
@@ -23485,10 +23644,9 @@ function renderGantt(container, resolved, palette, isDark, options, exportDims)
23485
23644
  if (title) {
23486
23645
  svg.append("text").attr("x", containerWidth / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).text(title);
23487
23646
  }
23488
- const hasCriticalPath = resolved.options.criticalPath && resolved.tasks.some((t) => t.isCriticalPath);
23489
23647
  function drawLegend() {
23490
23648
  svg.selectAll(".gantt-tag-legend-container").remove();
23491
- if (resolved.tagGroups.length > 0 || hasCriticalPath) {
23649
+ if (resolved.tagGroups.length > 0 || hasCriticalPath || hasDependencies) {
23492
23650
  const legendY = titleHeight;
23493
23651
  renderTagLegend(
23494
23652
  svg,
@@ -23510,16 +23668,38 @@ function renderGantt(container, resolved, palette, isDark, options, exportDims)
23510
23668
  recolorBars();
23511
23669
  },
23512
23670
  () => {
23513
- criticalPathActive = !criticalPathActive;
23671
+ controlsExpanded = !controlsExpanded;
23514
23672
  drawLegend();
23515
23673
  },
23516
23674
  currentSwimlaneGroup,
23517
23675
  onSwimlaneChange,
23518
23676
  viewMode,
23519
- resolved.tasks
23677
+ resolved.tasks,
23678
+ controlsExpanded,
23679
+ hasDependencies,
23680
+ dependenciesActive,
23681
+ (toggleId, active) => {
23682
+ if (toggleId === "critical-path") {
23683
+ criticalPathActive = active;
23684
+ } else if (toggleId === "dependencies") {
23685
+ dependenciesActive = active;
23686
+ g.selectAll(
23687
+ ".gantt-dep-arrow, .gantt-dep-arrowhead, .gantt-dep-label"
23688
+ ).attr("display", active ? null : "none");
23689
+ }
23690
+ drawLegend();
23691
+ }
23520
23692
  );
23521
23693
  }
23522
23694
  }
23695
+ function restoreHighlight() {
23696
+ if (criticalPathActive) {
23697
+ applyCriticalPathHighlight(svg, g);
23698
+ } else {
23699
+ svg.attr("data-critical-path-active", null);
23700
+ resetHighlight(g, svg);
23701
+ }
23702
+ }
23523
23703
  function recolorBars() {
23524
23704
  g.selectAll(".gantt-task").each(function() {
23525
23705
  const el = d3Selection10.select(this);
@@ -23645,7 +23825,7 @@ function renderGantt(container, resolved, palette, isDark, options, exportDims)
23645
23825
  );
23646
23826
  }
23647
23827
  }).on("mouseleave", () => {
23648
- resetHighlight(g, svg);
23828
+ restoreHighlight();
23649
23829
  hideGanttDateIndicators(g);
23650
23830
  });
23651
23831
  labelG.append("text").attr("x", labelX).attr("y", marginTop + yOffset + BAR_H / 2).attr("dy", "0.35em").attr("text-anchor", "start").attr("font-size", "11px").attr("font-weight", "bold").attr("fill", laneColor).text(
@@ -23850,7 +24030,7 @@ function renderGantt(container, resolved, palette, isDark, options, exportDims)
23850
24030
  );
23851
24031
  g.append("text").attr("class", "gantt-milestone-hover-label").attr("x", mx - MILESTONE_SIZE - 4).attr("y", my).attr("dy", "0.35em").attr("text-anchor", "end").attr("font-size", "10px").attr("fill", barColor).attr("font-weight", "600").text(task.label);
23852
24032
  }).on("mouseleave", () => {
23853
- resetHighlight(g, svg);
24033
+ restoreHighlight();
23854
24034
  hideGanttDateIndicators(g);
23855
24035
  g.selectAll(".gantt-milestone-hover-label").remove();
23856
24036
  });
@@ -23879,11 +24059,7 @@ function renderGantt(container, resolved, palette, isDark, options, exportDims)
23879
24059
  );
23880
24060
  }).on("mouseleave", () => {
23881
24061
  if (resolved.options.dependencies) {
23882
- if (criticalPathActive) {
23883
- applyCriticalPathHighlight(svg, g);
23884
- } else {
23885
- resetHighlight(g, svg);
23886
- }
24062
+ restoreHighlight();
23887
24063
  }
23888
24064
  resetTaskLabels(svg);
23889
24065
  hideGanttDateIndicators(g);
@@ -24198,6 +24374,7 @@ function arrowheadPoints(x, y, size, angle) {
24198
24374
  return `${x},${y} ${x + size * Math.cos(a1)},${y + size * Math.sin(a1)} ${x + size * Math.cos(a2)},${y + size * Math.sin(a2)}`;
24199
24375
  }
24200
24376
  function applyCriticalPathHighlight(svg, chartG) {
24377
+ svg.attr("data-critical-path-active", "true");
24201
24378
  chartG.selectAll(".gantt-task").each(function() {
24202
24379
  const el = d3Selection10.select(this);
24203
24380
  el.attr(
@@ -24250,8 +24427,31 @@ function drawSwimlaneIcon2(parent, x, y, isActive, palette) {
24250
24427
  }
24251
24428
  return iconG;
24252
24429
  }
24253
- function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargin, chartInnerWidth, legendY, palette, isDark, hasCriticalPath, criticalPathActive, optionLineNumbers, onToggle, onToggleCriticalPath, currentSwimlaneGroup, onSwimlaneChange, legendViewMode, resolvedTasks) {
24254
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
24430
+ function buildControlsToggles(hasCriticalPath, criticalPathActive, hasDependencies, dependenciesActive) {
24431
+ const toggles = [];
24432
+ if (hasCriticalPath) {
24433
+ toggles.push({
24434
+ id: "critical-path",
24435
+ type: "toggle",
24436
+ label: "Critical Path",
24437
+ active: criticalPathActive,
24438
+ onToggle: () => {
24439
+ }
24440
+ });
24441
+ }
24442
+ if (hasDependencies) {
24443
+ toggles.push({
24444
+ id: "dependencies",
24445
+ type: "toggle",
24446
+ label: "Dependencies",
24447
+ active: dependenciesActive,
24448
+ onToggle: () => {
24449
+ }
24450
+ });
24451
+ }
24452
+ return toggles;
24453
+ }
24454
+ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargin, chartInnerWidth, legendY, palette, isDark, hasCriticalPath, criticalPathActive, optionLineNumbers, onToggle, onToggleControlsExpand, currentSwimlaneGroup, onSwimlaneChange, legendViewMode, resolvedTasks, controlsExpanded = false, hasDependencies = false, dependenciesActive = false, onControlsToggle) {
24255
24455
  let visibleGroups;
24256
24456
  if (activeGroupName) {
24257
24457
  const activeGroup = tagGroups.filter(
@@ -24312,16 +24512,17 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
24312
24512
  totalW += groupW;
24313
24513
  }
24314
24514
  totalW += Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
24315
- const cpLabel = "Critical Path";
24316
- const cpPillW = measureLegendText(cpLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
24317
- if (hasCriticalPath) {
24515
+ const hasControls = hasCriticalPath || hasDependencies;
24516
+ const controlsToggleLabels = [];
24517
+ if (hasCriticalPath) controlsToggleLabels.push({ label: "Critical Path" });
24518
+ if (hasDependencies) controlsToggleLabels.push({ label: "Dependencies" });
24519
+ if (hasControls) {
24318
24520
  if (visibleGroups.length > 0) totalW += LEGEND_GROUP_GAP;
24319
- totalW += cpPillW;
24521
+ totalW += controlsExpanded ? controlsGroupCapsuleWidth(controlsToggleLabels) : LEGEND_GEAR_PILL_W;
24320
24522
  }
24321
24523
  const containerWidth = chartLeftMargin + chartInnerWidth + RIGHT_MARGIN;
24322
24524
  const legendX = (containerWidth - totalW) / 2;
24323
24525
  const legendRow = svg.append("g").attr("class", "gantt-tag-legend-container").attr("transform", `translate(${legendX}, ${legendY})`);
24324
- let cursorX = 0;
24325
24526
  if (visibleGroups.length > 0) {
24326
24527
  const showIcon = !legendViewMode && tagGroups.length > 0;
24327
24528
  const iconReserve = showIcon ? LEGEND_ICON_W : 0;
@@ -24333,6 +24534,12 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
24333
24534
  entries: entries.map((e) => ({ value: e.value, color: e.color }))
24334
24535
  };
24335
24536
  });
24537
+ const controlsToggles = buildControlsToggles(
24538
+ hasCriticalPath,
24539
+ criticalPathActive,
24540
+ hasDependencies,
24541
+ dependenciesActive
24542
+ );
24336
24543
  const legendConfig = {
24337
24544
  groups: legendGroups,
24338
24545
  position: {
@@ -24340,13 +24547,23 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
24340
24547
  titleRelation: "below-title"
24341
24548
  },
24342
24549
  mode: "fixed",
24343
- capsulePillAddonWidth: iconReserve
24550
+ capsulePillAddonWidth: iconReserve,
24551
+ controlsGroup: controlsToggles.length > 0 ? { toggles: controlsToggles } : void 0
24344
24552
  };
24345
- const legendState = { activeGroup: activeGroupName };
24346
- const tagGroupsW = visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) + Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
24553
+ const legendState = {
24554
+ activeGroup: activeGroupName,
24555
+ controlsExpanded
24556
+ };
24557
+ let tagGroupsW = visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) + Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
24558
+ if (hasControls) {
24559
+ if (visibleGroups.length > 0) tagGroupsW += LEGEND_GROUP_GAP;
24560
+ tagGroupsW += controlsExpanded ? controlsGroupCapsuleWidth(controlsToggleLabels) : LEGEND_GEAR_PILL_W;
24561
+ }
24347
24562
  const tagGroupG = legendRow.append("g");
24348
24563
  const legendCallbacks = {
24349
24564
  onGroupToggle: onToggle,
24565
+ onControlsExpand: onToggleControlsExpand,
24566
+ onControlsToggle,
24350
24567
  onEntryHover: (groupName, entryValue) => {
24351
24568
  const tagKey = groupName.toLowerCase();
24352
24569
  if (entryValue) {
@@ -24423,31 +24640,38 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
24423
24640
  legendCallbacks,
24424
24641
  tagGroupsW
24425
24642
  );
24426
- for (let i = 0; i < visibleGroups.length; i++) {
24427
- cursorX += groupWidths[i] + LEGEND_GROUP_GAP;
24428
- }
24643
+ } else if (hasControls) {
24644
+ const controlsToggles = buildControlsToggles(
24645
+ hasCriticalPath,
24646
+ criticalPathActive,
24647
+ hasDependencies,
24648
+ dependenciesActive
24649
+ );
24650
+ const legendConfig = {
24651
+ groups: [],
24652
+ position: {
24653
+ placement: "top-center",
24654
+ titleRelation: "below-title"
24655
+ },
24656
+ mode: "fixed",
24657
+ controlsGroup: { toggles: controlsToggles }
24658
+ };
24659
+ const tagGroupG = legendRow.append("g");
24660
+ renderLegendD3(
24661
+ tagGroupG,
24662
+ legendConfig,
24663
+ { activeGroup: null, controlsExpanded },
24664
+ palette,
24665
+ isDark,
24666
+ {
24667
+ onControlsExpand: onToggleControlsExpand,
24668
+ onControlsToggle
24669
+ },
24670
+ totalW
24671
+ );
24429
24672
  }
24430
- if (hasCriticalPath) {
24431
- const cpLineNum = optionLineNumbers["critical-path"];
24432
- const cpG = legendRow.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "gantt-legend-critical-path").style("cursor", "pointer").on("click", () => {
24433
- if (onToggleCriticalPath) onToggleCriticalPath();
24434
- });
24435
- if (cpLineNum) cpG.attr("data-line-number", String(cpLineNum));
24436
- cpG.append("rect").attr("width", cpPillW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", criticalPathActive ? palette.bg : groupBg);
24437
- if (criticalPathActive) {
24438
- cpG.append("rect").attr("width", cpPillW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
24439
- }
24440
- cpG.append("text").attr("x", cpPillW / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("text-anchor", "middle").attr("font-size", `${LEGEND_PILL_FONT_SIZE}px`).attr("font-weight", "500").attr("fill", criticalPathActive ? palette.text : palette.textMuted).text(cpLabel);
24441
- if (criticalPathActive) {
24442
- applyCriticalPathHighlight(svg, chartG);
24443
- }
24444
- cpG.on("mouseenter", () => {
24445
- applyCriticalPathHighlight(svg, chartG);
24446
- }).on("mouseleave", () => {
24447
- if (!criticalPathActive) {
24448
- resetHighlightAll(svg, chartG);
24449
- }
24450
- });
24673
+ if (criticalPathActive) {
24674
+ applyCriticalPathHighlight(svg, chartG);
24451
24675
  }
24452
24676
  }
24453
24677
  function renderErasAndMarkers(g, svg, resolved, xScale, innerHeight, palette) {
@@ -24915,6 +25139,10 @@ function resetTaskLabels(svg) {
24915
25139
  svg.selectAll(".gantt-task-label").attr("opacity", 1);
24916
25140
  }
24917
25141
  function resetHighlight(g, svg) {
25142
+ if (svg.attr("data-critical-path-active") === "true") {
25143
+ applyCriticalPathHighlight(svg, g);
25144
+ return;
25145
+ }
24918
25146
  g.selectAll(".gantt-task, .gantt-milestone").attr(
24919
25147
  "opacity",
24920
25148
  1
@@ -25188,6 +25416,7 @@ var init_renderer9 = __esm({
25188
25416
  init_d3();
25189
25417
  init_legend_constants();
25190
25418
  init_legend_d3();
25419
+ init_legend_layout();
25191
25420
  init_title_constants();
25192
25421
  BAR_H = 22;
25193
25422
  ROW_GAP = 6;
@@ -25481,6 +25710,109 @@ var init_state_renderer = __esm({
25481
25710
  }
25482
25711
  });
25483
25712
 
25713
+ // src/sequence/collapse.ts
25714
+ function applyCollapseProjection(parsed, collapsedGroups) {
25715
+ if (collapsedGroups.size === 0) {
25716
+ return {
25717
+ participants: parsed.participants,
25718
+ messages: parsed.messages,
25719
+ elements: parsed.elements,
25720
+ groups: parsed.groups,
25721
+ collapsedGroupIds: /* @__PURE__ */ new Map()
25722
+ };
25723
+ }
25724
+ const memberToGroup = /* @__PURE__ */ new Map();
25725
+ const collapsedGroupNames = /* @__PURE__ */ new Set();
25726
+ for (const group of parsed.groups) {
25727
+ if (collapsedGroups.has(group.lineNumber)) {
25728
+ collapsedGroupNames.add(group.name);
25729
+ for (const memberId of group.participantIds) {
25730
+ memberToGroup.set(memberId, group.name);
25731
+ }
25732
+ }
25733
+ }
25734
+ const participants = [];
25735
+ const insertedGroups = /* @__PURE__ */ new Set();
25736
+ for (const p of parsed.participants) {
25737
+ const groupName = memberToGroup.get(p.id);
25738
+ if (groupName) {
25739
+ if (!insertedGroups.has(groupName)) {
25740
+ insertedGroups.add(groupName);
25741
+ const group = parsed.groups.find(
25742
+ (g) => g.name === groupName && collapsedGroups.has(g.lineNumber)
25743
+ );
25744
+ participants.push({
25745
+ id: groupName,
25746
+ label: groupName,
25747
+ type: "default",
25748
+ lineNumber: group.lineNumber
25749
+ });
25750
+ }
25751
+ } else if (collapsedGroupNames.has(p.id)) {
25752
+ } else {
25753
+ participants.push(p);
25754
+ }
25755
+ }
25756
+ const remap = (id) => memberToGroup.get(id) ?? id;
25757
+ const messages = parsed.messages.map((msg) => ({
25758
+ ...msg,
25759
+ from: remap(msg.from),
25760
+ to: remap(msg.to)
25761
+ }));
25762
+ const elements = remapElements(parsed.elements, memberToGroup);
25763
+ const groups = parsed.groups.filter(
25764
+ (g) => !collapsedGroups.has(g.lineNumber)
25765
+ );
25766
+ return {
25767
+ participants,
25768
+ messages,
25769
+ elements,
25770
+ groups,
25771
+ collapsedGroupIds: memberToGroup
25772
+ };
25773
+ }
25774
+ function remapElements(elements, memberToGroup) {
25775
+ const remap = (id) => memberToGroup.get(id) ?? id;
25776
+ const result = [];
25777
+ for (const el of elements) {
25778
+ if (isSequenceSection(el)) {
25779
+ result.push(el);
25780
+ } else if (isSequenceNote(el)) {
25781
+ result.push({
25782
+ ...el,
25783
+ participantId: remap(el.participantId)
25784
+ });
25785
+ } else if (isSequenceBlock(el)) {
25786
+ result.push({
25787
+ ...el,
25788
+ children: remapElements(el.children, memberToGroup),
25789
+ elseChildren: remapElements(el.elseChildren, memberToGroup),
25790
+ ...el.elseIfBranches ? {
25791
+ elseIfBranches: el.elseIfBranches.map((branch) => ({
25792
+ ...branch,
25793
+ children: remapElements(branch.children, memberToGroup)
25794
+ }))
25795
+ } : {}
25796
+ });
25797
+ } else {
25798
+ const msg = el;
25799
+ const from = remap(msg.from);
25800
+ const to = remap(msg.to);
25801
+ if (from === to && from !== msg.from && !msg.label) {
25802
+ continue;
25803
+ }
25804
+ result.push({ ...msg, from, to });
25805
+ }
25806
+ }
25807
+ return result;
25808
+ }
25809
+ var init_collapse3 = __esm({
25810
+ "src/sequence/collapse.ts"() {
25811
+ "use strict";
25812
+ init_parser();
25813
+ }
25814
+ });
25815
+
25484
25816
  // src/sequence/tag-resolution.ts
25485
25817
  function propagateGroupTags(participantMeta, groups) {
25486
25818
  for (const group of groups) {
@@ -25935,13 +26267,32 @@ function applyGroupOrdering(participants, groups, messages = []) {
25935
26267
  }
25936
26268
  function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateToLine, options) {
25937
26269
  d3Selection12.select(container).selectAll("*").remove();
25938
- const { title, messages, elements, groups, options: parsedOptions } = parsed;
26270
+ const { title, options: parsedOptions } = parsed;
26271
+ const effectiveCollapsedGroups = /* @__PURE__ */ new Set();
26272
+ for (const group of parsed.groups) {
26273
+ if (group.collapsed) effectiveCollapsedGroups.add(group.lineNumber);
26274
+ }
26275
+ if (options?.collapsedGroups) {
26276
+ for (const ln of options.collapsedGroups) {
26277
+ if (effectiveCollapsedGroups.has(ln)) {
26278
+ effectiveCollapsedGroups.delete(ln);
26279
+ } else {
26280
+ effectiveCollapsedGroups.add(ln);
26281
+ }
26282
+ }
26283
+ }
26284
+ const collapsed = effectiveCollapsedGroups.size > 0 ? applyCollapseProjection(parsed, effectiveCollapsedGroups) : null;
26285
+ const messages = collapsed ? collapsed.messages : parsed.messages;
26286
+ const elements = collapsed ? collapsed.elements : parsed.elements;
26287
+ const groups = collapsed ? collapsed.groups : parsed.groups;
26288
+ const collapsedGroupIds = collapsed?.collapsedGroupIds ?? /* @__PURE__ */ new Map();
25939
26289
  const collapsedSections = options?.collapsedSections;
25940
26290
  const expandedNoteLines = options?.expandedNoteLines;
25941
26291
  const collapseNotesDisabled = parsedOptions["collapse-notes"]?.toLowerCase() === "no";
25942
26292
  const isNoteExpanded = (note) => expandedNoteLines === void 0 || collapseNotesDisabled || expandedNoteLines.has(note.lineNumber);
26293
+ const sourceParticipants = collapsed ? collapsed.participants : parsed.participants;
25943
26294
  const participants = applyPositionOverrides(
25944
- applyGroupOrdering(parsed.participants, groups, messages)
26295
+ applyGroupOrdering(sourceParticipants, groups, messages)
25945
26296
  );
25946
26297
  if (participants.length === 0) return;
25947
26298
  const activationsOff = parsedOptions.activations?.toLowerCase() === "off";
@@ -26112,13 +26463,16 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26112
26463
  const preSectionMsgIndices = [];
26113
26464
  const sectionRegions = [];
26114
26465
  {
26466
+ const msgLineToIndex = /* @__PURE__ */ new Map();
26467
+ messages.forEach((m, i) => msgLineToIndex.set(m.lineNumber, i));
26468
+ const findMsgIndex = (child) => msgLineToIndex.get(child.lineNumber) ?? -1;
26115
26469
  const collectMsgIndicesFromBlock = (block) => {
26116
26470
  const indices = [];
26117
26471
  for (const child of block.children) {
26118
26472
  if (isSequenceBlock(child)) {
26119
26473
  indices.push(...collectMsgIndicesFromBlock(child));
26120
26474
  } else if (!isSequenceSection(child) && !isSequenceNote(child)) {
26121
- const idx = messages.indexOf(child);
26475
+ const idx = findMsgIndex(child);
26122
26476
  if (idx >= 0) indices.push(idx);
26123
26477
  }
26124
26478
  }
@@ -26128,7 +26482,7 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26128
26482
  if (isSequenceBlock(child)) {
26129
26483
  indices.push(...collectMsgIndicesFromBlock(child));
26130
26484
  } else if (!isSequenceSection(child) && !isSequenceNote(child)) {
26131
- const idx = messages.indexOf(child);
26485
+ const idx = findMsgIndex(child);
26132
26486
  if (idx >= 0) indices.push(idx);
26133
26487
  }
26134
26488
  }
@@ -26138,7 +26492,7 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26138
26492
  if (isSequenceBlock(child)) {
26139
26493
  indices.push(...collectMsgIndicesFromBlock(child));
26140
26494
  } else if (!isSequenceSection(child) && !isSequenceNote(child)) {
26141
- const idx = messages.indexOf(child);
26495
+ const idx = findMsgIndex(child);
26142
26496
  if (idx >= 0) indices.push(idx);
26143
26497
  }
26144
26498
  }
@@ -26153,7 +26507,7 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26153
26507
  } else if (isSequenceBlock(el)) {
26154
26508
  currentTarget.push(...collectMsgIndicesFromBlock(el));
26155
26509
  } else {
26156
- const idx = messages.indexOf(el);
26510
+ const idx = findMsgIndex(el);
26157
26511
  if (idx >= 0) currentTarget.push(idx);
26158
26512
  }
26159
26513
  }
@@ -26215,7 +26569,7 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26215
26569
  const titleOffset = title ? TITLE_HEIGHT5 : 0;
26216
26570
  const LEGEND_FIXED_GAP4 = 8;
26217
26571
  const legendTopSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + LEGEND_FIXED_GAP4 : 0;
26218
- const groupOffset = groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
26572
+ const groupOffset = parsed.groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
26219
26573
  const participantStartY = TOP_MARGIN + titleOffset + legendTopSpace + PARTICIPANT_Y_OFFSET + groupOffset;
26220
26574
  const lifelineStartY0 = participantStartY + PARTICIPANT_BOX_HEIGHT;
26221
26575
  const hasActors = participants.some((p) => p.type === "actor");
@@ -26385,6 +26739,17 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26385
26739
  svgWidth
26386
26740
  );
26387
26741
  }
26742
+ const collapsedGroupNames = /* @__PURE__ */ new Set();
26743
+ const collapsedGroupMeta = /* @__PURE__ */ new Map();
26744
+ for (const group of parsed.groups) {
26745
+ if (effectiveCollapsedGroups.has(group.lineNumber)) {
26746
+ collapsedGroupNames.add(group.name);
26747
+ collapsedGroupMeta.set(group.name, {
26748
+ lineNumber: group.lineNumber,
26749
+ metadata: group.metadata
26750
+ });
26751
+ }
26752
+ }
26388
26753
  for (const group of groups) {
26389
26754
  if (group.participantIds.length === 0) continue;
26390
26755
  const memberXs = group.participantIds.map((id) => participantX.get(id)).filter((x) => x !== void 0);
@@ -26401,8 +26766,10 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26401
26766
  isDark ? 15 : 20
26402
26767
  ) : isDark ? palette.surface : palette.bg;
26403
26768
  const strokeColor = groupTagColor || palette.textMuted;
26404
- svg.append("rect").attr("x", minX).attr("y", boxY).attr("width", maxX - minX).attr("height", boxH).attr("rx", 6).attr("fill", fillColor).attr("stroke", strokeColor).attr("stroke-width", 1).attr("stroke-opacity", 0.5).attr("class", "group-box").attr("data-group-line", String(group.lineNumber));
26405
- svg.append("text").attr("x", minX + 8).attr("y", boxY + GROUP_LABEL_SIZE + 4).attr("fill", strokeColor).attr("font-size", GROUP_LABEL_SIZE).attr("font-weight", "bold").attr("opacity", 0.7).attr("class", "group-label").attr("data-group-line", String(group.lineNumber)).text(group.name);
26769
+ const groupG = svg.append("g").attr("class", "group-box-wrapper").attr("data-group-toggle", "").attr("data-group-line", String(group.lineNumber)).attr("cursor", "pointer");
26770
+ groupG.append("title").text("Click to collapse");
26771
+ groupG.append("rect").attr("x", minX).attr("y", boxY).attr("width", maxX - minX).attr("height", boxH).attr("rx", 6).attr("fill", fillColor).attr("stroke", strokeColor).attr("stroke-width", 1).attr("stroke-opacity", 0.5).attr("class", "group-box");
26772
+ groupG.append("text").attr("x", minX + 8).attr("y", boxY + GROUP_LABEL_SIZE + 4).attr("fill", strokeColor).attr("font-size", GROUP_LABEL_SIZE).attr("font-weight", "bold").attr("opacity", 0.7).attr("class", "group-label").text(group.name);
26406
26773
  }
26407
26774
  const lifelineStartY = lifelineStartY0;
26408
26775
  participants.forEach((participant, index) => {
@@ -26411,6 +26778,14 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26411
26778
  const pTagValue = tagMap?.participants.get(participant.id);
26412
26779
  const pTagColor = getTagColor(pTagValue);
26413
26780
  const pTagAttr = tagKey && pTagValue ? { key: tagKey, value: pTagValue.toLowerCase() } : void 0;
26781
+ const isCollapsedGroup = collapsedGroupNames.has(participant.id);
26782
+ let effectiveTagColor = pTagColor;
26783
+ if (isCollapsedGroup && !effectiveTagColor) {
26784
+ const meta = collapsedGroupMeta.get(participant.id);
26785
+ if (meta?.metadata && tagKey) {
26786
+ effectiveTagColor = getTagColor(meta.metadata[tagKey]);
26787
+ }
26788
+ }
26414
26789
  renderParticipant(
26415
26790
  svg,
26416
26791
  participant,
@@ -26418,10 +26793,35 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26418
26793
  cy,
26419
26794
  palette,
26420
26795
  isDark,
26421
- pTagColor,
26796
+ effectiveTagColor,
26422
26797
  pTagAttr
26423
26798
  );
26424
- const lifelineEl = svg.append("line").attr("x1", cx).attr("y1", lifelineStartY).attr("x2", cx).attr("y2", lifelineStartY + lifelineLength).attr("stroke", pTagColor || palette.textMuted).attr("stroke-width", 1).attr("stroke-dasharray", "6 4").attr("class", "lifeline").attr("data-participant-id", participant.id);
26799
+ if (isCollapsedGroup) {
26800
+ const meta = collapsedGroupMeta.get(participant.id);
26801
+ const drillColor = effectiveTagColor || palette.textMuted;
26802
+ const drillBarH = 6;
26803
+ const boxW = PARTICIPANT_BOX_WIDTH;
26804
+ const fullH = PARTICIPANT_BOX_HEIGHT + GROUP_PADDING_TOP + GROUP_PADDING_BOTTOM;
26805
+ const clipId = `clip-drill-group-${participant.id.replace(/[^a-zA-Z0-9-]/g, "-")}`;
26806
+ const participantG = svg.select(
26807
+ `.participant[data-participant-id="${participant.id}"]`
26808
+ );
26809
+ participantG.attr("data-group-toggle", "").attr("data-group-line", String(meta.lineNumber)).attr("cursor", "pointer");
26810
+ participantG.append("title").text("Click to expand");
26811
+ const pFill = effectiveTagColor ? mix(
26812
+ effectiveTagColor,
26813
+ isDark ? palette.surface : palette.bg,
26814
+ isDark ? 30 : 40
26815
+ ) : isDark ? mix(palette.overlay, palette.surface, 50) : mix(palette.bg, palette.surface, 50);
26816
+ const pStroke = effectiveTagColor || palette.border;
26817
+ participantG.append("rect").attr("x", -boxW / 2).attr("y", -GROUP_PADDING_TOP).attr("width", boxW).attr("height", fullH).attr("rx", 6).attr("fill", pFill).attr("stroke", pStroke).attr("stroke-width", 1.5);
26818
+ participantG.append("text").attr("x", 0).attr("y", -GROUP_PADDING_TOP + fullH / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", 13).attr("font-weight", 500).text(participant.label);
26819
+ participantG.append("clipPath").attr("id", clipId).append("rect").attr("x", -boxW / 2).attr("y", -GROUP_PADDING_TOP).attr("width", boxW).attr("height", fullH).attr("rx", 6);
26820
+ participantG.append("rect").attr("class", "sequence-drill-bar").attr("x", -boxW / 2).attr("y", -GROUP_PADDING_TOP + fullH - drillBarH).attr("width", boxW).attr("height", drillBarH).attr("fill", drillColor).attr("clip-path", `url(#${clipId})`);
26821
+ }
26822
+ const llY = isCollapsedGroup ? lifelineStartY + GROUP_PADDING_BOTTOM : lifelineStartY;
26823
+ const llColor = isCollapsedGroup ? effectiveTagColor || palette.textMuted : pTagColor || palette.textMuted;
26824
+ const lifelineEl = svg.append("line").attr("x1", cx).attr("y1", llY).attr("x2", cx).attr("y2", lifelineStartY + lifelineLength).attr("stroke", llColor).attr("stroke-width", 1).attr("stroke-dasharray", "6 4").attr("class", "lifeline").attr("data-participant-id", participant.id);
26425
26825
  if (tagKey && pTagValue) {
26426
26826
  lifelineEl.attr(`data-tag-${tagKey}`, pTagValue.toLowerCase());
26427
26827
  }
@@ -26649,23 +27049,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26649
27049
  sectionG.append("rect").attr("x", bandX).attr("y", secY - BAND_HEIGHT / 2).attr("width", bandWidth).attr("height", BAND_HEIGHT).attr("fill", lineColor).attr("opacity", bandOpacity).attr("rx", 2).attr("class", "section-divider");
26650
27050
  const msgCount = sectionMsgCounts.get(sec.lineNumber) ?? 0;
26651
27051
  const labelText = isCollapsed ? `${sec.label} (${msgCount} ${msgCount === 1 ? "message" : "messages"})` : sec.label;
26652
- const labelColor = isCollapsed ? "#ffffff" : lineColor;
26653
- const chevronSpace = 14;
26654
27052
  const labelX = (sectionLineX1 + sectionLineX2) / 2;
26655
- const chevronX = labelX - (labelText.length * 3.5 + 8 + chevronSpace / 2);
26656
- const chevronY = secY;
26657
- if (isCollapsed) {
26658
- sectionG.append("path").attr(
26659
- "d",
26660
- `M ${chevronX} ${chevronY - 4} L ${chevronX + 6} ${chevronY} L ${chevronX} ${chevronY + 4} Z`
26661
- ).attr("fill", labelColor).attr("class", "section-chevron");
26662
- } else {
26663
- sectionG.append("path").attr(
26664
- "d",
26665
- `M ${chevronX - 1} ${chevronY - 3} L ${chevronX + 7} ${chevronY - 3} L ${chevronX + 3} ${chevronY + 3} Z`
26666
- ).attr("fill", labelColor).attr("class", "section-chevron");
26667
- }
26668
- sectionG.append("text").attr("x", labelX + chevronSpace / 2).attr("y", secY + 4).attr("text-anchor", "middle").attr("fill", labelColor).attr("font-size", 11).attr("font-weight", "bold").attr("class", "section-label").text(labelText);
27053
+ sectionG.append("text").attr("x", labelX).attr("y", secY + 4).attr("text-anchor", "middle").attr("fill", lineColor).attr("font-size", 11).attr("font-weight", "bold").attr("class", "section-label").text(labelText);
26669
27054
  }
26670
27055
  const SELF_CALL_WIDTH = 30;
26671
27056
  const SELF_CALL_HEIGHT = 25;
@@ -26947,6 +27332,7 @@ var init_renderer10 = __esm({
26947
27332
  init_inline_markdown();
26948
27333
  init_fonts();
26949
27334
  init_parser();
27335
+ init_collapse3();
26950
27336
  init_tag_resolution();
26951
27337
  init_tag_groups();
26952
27338
  init_legend_constants();
@@ -31741,6 +32127,7 @@ __export(index_exports, {
31741
32127
  RECOGNIZED_COLOR_NAMES: () => RECOGNIZED_COLOR_NAMES,
31742
32128
  RULE_COUNT: () => RULE_COUNT,
31743
32129
  addDurationToDate: () => addDurationToDate,
32130
+ applyCollapseProjection: () => applyCollapseProjection,
31744
32131
  applyGroupOrdering: () => applyGroupOrdering,
31745
32132
  applyPositionOverrides: () => applyPositionOverrides,
31746
32133
  boldPalette: () => boldPalette,
@@ -31770,8 +32157,10 @@ __export(index_exports, {
31770
32157
  computeTimeTicks: () => computeTimeTicks,
31771
32158
  contrastText: () => contrastText,
31772
32159
  decodeDiagramUrl: () => decodeDiagramUrl,
32160
+ decodeViewState: () => decodeViewState,
31773
32161
  draculaPalette: () => draculaPalette,
31774
32162
  encodeDiagramUrl: () => encodeDiagramUrl,
32163
+ encodeViewState: () => encodeViewState,
31775
32164
  extractDiagramSymbols: () => extractDiagramSymbols,
31776
32165
  extractTagDeclarations: () => extractTagDeclarations,
31777
32166
  formatDateLabel: () => formatDateLabel,
@@ -32681,6 +33070,7 @@ init_legend_d3();
32681
33070
  init_legend_layout();
32682
33071
  init_d3();
32683
33072
  init_renderer10();
33073
+ init_collapse3();
32684
33074
  init_colors();
32685
33075
  init_palettes();
32686
33076
 
@@ -32688,6 +33078,24 @@ init_palettes();
32688
33078
  var import_lz_string = __toESM(require_lz_string(), 1);
32689
33079
  var DEFAULT_BASE_URL = "https://online.diagrammo.app";
32690
33080
  var COMPRESSED_SIZE_LIMIT = 8192;
33081
+ function encodeViewState(state) {
33082
+ const keys = Object.keys(state);
33083
+ if (keys.length === 0) return "";
33084
+ return (0, import_lz_string.compressToEncodedURIComponent)(JSON.stringify(state));
33085
+ }
33086
+ function decodeViewState(encoded) {
33087
+ if (!encoded) return {};
33088
+ try {
33089
+ const json = (0, import_lz_string.decompressFromEncodedURIComponent)(encoded);
33090
+ if (!json) return {};
33091
+ const parsed = JSON.parse(json);
33092
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
33093
+ return {};
33094
+ return parsed;
33095
+ } catch {
33096
+ return {};
33097
+ }
33098
+ }
32691
33099
  function encodeDiagramUrl(dsl, options) {
32692
33100
  const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL;
32693
33101
  const compressed = (0, import_lz_string.compressToEncodedURIComponent)(dsl);
@@ -32700,23 +33108,17 @@ function encodeDiagramUrl(dsl, options) {
32700
33108
  };
32701
33109
  }
32702
33110
  let hash = `dgmo=${compressed}`;
32703
- if (options?.viewState?.activeTagGroup) {
32704
- hash += `&tag=${encodeURIComponent(options.viewState.activeTagGroup)}`;
32705
- }
32706
- if (options?.viewState?.collapsedGroups?.length) {
32707
- hash += `&cg=${encodeURIComponent(options.viewState.collapsedGroups.join(","))}`;
32708
- }
32709
- if (options?.viewState?.swimlaneTagGroup) {
32710
- hash += `&swim=${encodeURIComponent(options.viewState.swimlaneTagGroup)}`;
32711
- }
32712
- if (options?.viewState?.collapsedLanes?.length) {
32713
- hash += `&cl=${encodeURIComponent(options.viewState.collapsedLanes.join(","))}`;
33111
+ if (options?.viewState) {
33112
+ const vsEncoded = encodeViewState(options.viewState);
33113
+ if (vsEncoded) {
33114
+ hash += `&vs=${vsEncoded}`;
33115
+ }
32714
33116
  }
32715
- if (options?.viewState?.palette && options.viewState.palette !== "nord") {
32716
- hash += `&pal=${encodeURIComponent(options.viewState.palette)}`;
33117
+ if (options?.palette && options.palette !== "nord") {
33118
+ hash += `&pal=${encodeURIComponent(options.palette)}`;
32717
33119
  }
32718
- if (options?.viewState?.theme && options.viewState.theme !== "dark") {
32719
- hash += `&th=${encodeURIComponent(options.viewState.theme)}`;
33120
+ if (options?.theme && options.theme !== "dark") {
33121
+ hash += `&th=${encodeURIComponent(options.theme)}`;
32720
33122
  }
32721
33123
  if (options?.filename) {
32722
33124
  hash += `&fn=${encodeURIComponent(options.filename)}`;
@@ -32726,6 +33128,8 @@ function encodeDiagramUrl(dsl, options) {
32726
33128
  function decodeDiagramUrl(hash) {
32727
33129
  const empty = { dsl: "", viewState: {} };
32728
33130
  let filename;
33131
+ let palette;
33132
+ let theme;
32729
33133
  if (!hash) return empty;
32730
33134
  let raw = hash;
32731
33135
  if (raw.startsWith("#") || raw.startsWith("?")) {
@@ -32733,38 +33137,31 @@ function decodeDiagramUrl(hash) {
32733
33137
  }
32734
33138
  const parts = raw.split("&");
32735
33139
  let payload = parts[0];
32736
- const viewState = {};
33140
+ let viewState = {};
32737
33141
  for (let i = 1; i < parts.length; i++) {
32738
33142
  const eq = parts[i].indexOf("=");
32739
33143
  if (eq === -1) continue;
32740
33144
  const key = parts[i].slice(0, eq);
32741
- const val = decodeURIComponent(parts[i].slice(eq + 1));
32742
- if (key === "tag" && val) {
32743
- viewState.activeTagGroup = val;
32744
- }
32745
- if (key === "cg" && val) {
32746
- viewState.collapsedGroups = val.split(",").filter(Boolean);
32747
- }
32748
- if (key === "swim" && val) {
32749
- viewState.swimlaneTagGroup = val;
33145
+ const val = parts[i].slice(eq + 1);
33146
+ if (key === "vs" && val) {
33147
+ viewState = decodeViewState(val);
32750
33148
  }
32751
- if (key === "cl" && val) {
32752
- viewState.collapsedLanes = val.split(",").filter(Boolean);
33149
+ if (key === "pal" && val) palette = decodeURIComponent(val);
33150
+ if (key === "th") {
33151
+ const decoded = decodeURIComponent(val);
33152
+ if (decoded === "light" || decoded === "dark") theme = decoded;
32753
33153
  }
32754
- if (key === "pal" && val) viewState.palette = val;
32755
- if (key === "th" && (val === "light" || val === "dark"))
32756
- viewState.theme = val;
32757
- if (key === "fn" && val) filename = val;
33154
+ if (key === "fn" && val) filename = decodeURIComponent(val);
32758
33155
  }
32759
33156
  if (payload.startsWith("dgmo=")) {
32760
33157
  payload = payload.slice(5);
32761
33158
  }
32762
- if (!payload) return { dsl: "", viewState, filename };
33159
+ if (!payload) return { dsl: "", viewState, palette, theme, filename };
32763
33160
  try {
32764
33161
  const result = (0, import_lz_string.decompressFromEncodedURIComponent)(payload);
32765
- return { dsl: result ?? "", viewState, filename };
33162
+ return { dsl: result ?? "", viewState, palette, theme, filename };
32766
33163
  } catch {
32767
- return { dsl: "", viewState, filename };
33164
+ return { dsl: "", viewState, palette, theme, filename };
32768
33165
  }
32769
33166
  }
32770
33167
 
@@ -33525,6 +33922,7 @@ init_branding();
33525
33922
  RECOGNIZED_COLOR_NAMES,
33526
33923
  RULE_COUNT,
33527
33924
  addDurationToDate,
33925
+ applyCollapseProjection,
33528
33926
  applyGroupOrdering,
33529
33927
  applyPositionOverrides,
33530
33928
  boldPalette,
@@ -33554,8 +33952,10 @@ init_branding();
33554
33952
  computeTimeTicks,
33555
33953
  contrastText,
33556
33954
  decodeDiagramUrl,
33955
+ decodeViewState,
33557
33956
  draculaPalette,
33558
33957
  encodeDiagramUrl,
33958
+ encodeViewState,
33559
33959
  extractDiagramSymbols,
33560
33960
  extractTagDeclarations,
33561
33961
  formatDateLabel,