@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.js CHANGED
@@ -2100,7 +2100,7 @@ function measureLegendText(text, fontSize) {
2100
2100
  }
2101
2101
  return w;
2102
2102
  }
2103
- 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;
2103
+ 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;
2104
2104
  var init_legend_constants = __esm({
2105
2105
  "src/utils/legend-constants.ts"() {
2106
2106
  "use strict";
@@ -2207,6 +2207,10 @@ var init_legend_constants = __esm({
2207
2207
  DEFAULT_W = 0.56;
2208
2208
  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";
2209
2209
  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";
2210
+ 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";
2211
+ LEGEND_TOGGLE_DOT_R = LEGEND_DOT_R;
2212
+ LEGEND_TOGGLE_OFF_OPACITY = 0.4;
2213
+ LEGEND_GEAR_PILL_W = 14 + LEGEND_PILL_PAD;
2210
2214
  }
2211
2215
  });
2212
2216
 
@@ -2280,6 +2284,63 @@ function capsuleWidth(name, entries, containerWidth, addonWidth = 0) {
2280
2284
  visibleEntries: entries.length
2281
2285
  };
2282
2286
  }
2287
+ function controlsGroupCapsuleWidth(toggles) {
2288
+ let w = LEGEND_CAPSULE_PAD * 2 + LEGEND_GEAR_PILL_W + 4;
2289
+ for (const t of toggles) {
2290
+ w += LEGEND_TOGGLE_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(t.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
2291
+ }
2292
+ return w;
2293
+ }
2294
+ function buildControlsGroupLayout(config, state) {
2295
+ const cg = config.controlsGroup;
2296
+ if (!cg || cg.toggles.length === 0) return void 0;
2297
+ const expanded = !!state.controlsExpanded;
2298
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
2299
+ if (!expanded) {
2300
+ return {
2301
+ x: 0,
2302
+ y: 0,
2303
+ width: LEGEND_GEAR_PILL_W,
2304
+ height: LEGEND_HEIGHT,
2305
+ expanded: false,
2306
+ pill: { x: 0, y: 0, width: LEGEND_GEAR_PILL_W, height: LEGEND_HEIGHT },
2307
+ toggles: []
2308
+ };
2309
+ }
2310
+ const capsuleW = controlsGroupCapsuleWidth(cg.toggles);
2311
+ const toggleLayouts = [];
2312
+ let tx = LEGEND_CAPSULE_PAD + LEGEND_GEAR_PILL_W + 4;
2313
+ for (const toggle of cg.toggles) {
2314
+ const dotCx = tx + LEGEND_TOGGLE_DOT_R;
2315
+ const dotCy = LEGEND_HEIGHT / 2;
2316
+ const textX = tx + LEGEND_TOGGLE_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
2317
+ const textY = LEGEND_HEIGHT / 2;
2318
+ toggleLayouts.push({
2319
+ id: toggle.id,
2320
+ label: toggle.label,
2321
+ active: toggle.active,
2322
+ dotCx,
2323
+ dotCy,
2324
+ textX,
2325
+ textY
2326
+ });
2327
+ tx += LEGEND_TOGGLE_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(toggle.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
2328
+ }
2329
+ return {
2330
+ x: 0,
2331
+ y: 0,
2332
+ width: capsuleW,
2333
+ height: LEGEND_HEIGHT,
2334
+ expanded: true,
2335
+ pill: {
2336
+ x: LEGEND_CAPSULE_PAD,
2337
+ y: LEGEND_CAPSULE_PAD,
2338
+ width: LEGEND_GEAR_PILL_W - LEGEND_CAPSULE_PAD * 2,
2339
+ height: pillH
2340
+ },
2341
+ toggles: toggleLayouts
2342
+ };
2343
+ }
2283
2344
  function computeLegendLayout(config, state, containerWidth) {
2284
2345
  const { groups, controls: configControls, mode } = config;
2285
2346
  const isExport = mode === "inline";
@@ -2294,8 +2355,9 @@ function computeLegendLayout(config, state, containerWidth) {
2294
2355
  activeCapsule: void 0
2295
2356
  };
2296
2357
  }
2358
+ const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
2297
2359
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
2298
- if (visibleGroups.length === 0 && (!configControls || configControls.length === 0)) {
2360
+ if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
2299
2361
  return {
2300
2362
  height: 0,
2301
2363
  width: 0,
@@ -2359,7 +2421,8 @@ function computeLegendLayout(config, state, containerWidth) {
2359
2421
  if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2360
2422
  }
2361
2423
  const controlsSpace = totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0;
2362
- const groupAvailW = containerWidth - controlsSpace;
2424
+ const gearSpace = controlsGroupLayout ? controlsGroupLayout.width + LEGEND_GROUP_GAP : 0;
2425
+ const groupAvailW = containerWidth - controlsSpace - gearSpace;
2363
2426
  const pills = [];
2364
2427
  let activeCapsule;
2365
2428
  for (const g of visibleGroups) {
@@ -2368,7 +2431,7 @@ function computeLegendLayout(config, state, containerWidth) {
2368
2431
  if (isActive) {
2369
2432
  activeCapsule = buildCapsuleLayout(
2370
2433
  g,
2371
- containerWidth,
2434
+ groupAvailW,
2372
2435
  config.capsulePillAddonWidth ?? 0
2373
2436
  );
2374
2437
  } else {
@@ -2391,7 +2454,8 @@ function computeLegendLayout(config, state, containerWidth) {
2391
2454
  groupAvailW,
2392
2455
  containerWidth,
2393
2456
  totalControlsW,
2394
- alignLeft
2457
+ alignLeft,
2458
+ controlsGroupLayout
2395
2459
  );
2396
2460
  const height = rows.length * LEGEND_HEIGHT;
2397
2461
  const width = containerWidth;
@@ -2401,7 +2465,8 @@ function computeLegendLayout(config, state, containerWidth) {
2401
2465
  rows,
2402
2466
  activeCapsule,
2403
2467
  controls: controlLayouts,
2404
- pills
2468
+ pills,
2469
+ controlsGroup: controlsGroupLayout
2405
2470
  };
2406
2471
  }
2407
2472
  function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
@@ -2465,19 +2530,27 @@ function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
2465
2530
  addonX: addonWidth > 0 ? LEGEND_CAPSULE_PAD + pw + 4 : void 0
2466
2531
  };
2467
2532
  }
2468
- function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW, alignLeft = false) {
2533
+ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW, alignLeft = false, controlsGroup) {
2469
2534
  const rows = [];
2470
2535
  const groupItems = [];
2471
2536
  if (activeCapsule) groupItems.push(activeCapsule);
2472
2537
  groupItems.push(...pills);
2538
+ const gearW = controlsGroup ? controlsGroup.width + LEGEND_GROUP_GAP : 0;
2473
2539
  let currentRowItems = [];
2474
2540
  let currentRowW = 0;
2475
2541
  let rowY = 0;
2476
2542
  for (const item of groupItems) {
2477
2543
  const itemW = item.width + LEGEND_GROUP_GAP;
2478
2544
  if (currentRowW + item.width > groupAvailW && currentRowItems.length > 0) {
2479
- if (!alignLeft)
2480
- centerRowItems(currentRowItems, containerWidth, totalControlsW);
2545
+ if (!alignLeft) {
2546
+ const rowGearW = rows.length === 0 ? gearW : 0;
2547
+ centerRowItems(
2548
+ currentRowItems,
2549
+ containerWidth,
2550
+ totalControlsW,
2551
+ rowGearW
2552
+ );
2553
+ }
2481
2554
  rows.push({ y: rowY, items: currentRowItems });
2482
2555
  rowY += LEGEND_HEIGHT;
2483
2556
  currentRowItems = [];
@@ -2505,19 +2578,32 @@ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth,
2505
2578
  }
2506
2579
  }
2507
2580
  if (currentRowItems.length > 0) {
2508
- centerRowItems(currentRowItems, containerWidth, totalControlsW);
2581
+ centerRowItems(currentRowItems, containerWidth, totalControlsW, gearW);
2509
2582
  rows.push({ y: rowY, items: currentRowItems });
2510
2583
  }
2584
+ if (controlsGroup) {
2585
+ const row0Items = rows[0]?.items ?? [];
2586
+ const groupItemsInRow0 = row0Items.filter(
2587
+ (it) => "groupName" in it
2588
+ );
2589
+ if (groupItemsInRow0.length > 0) {
2590
+ const last = groupItemsInRow0[groupItemsInRow0.length - 1];
2591
+ controlsGroup.x = last.x + last.width + LEGEND_GROUP_GAP;
2592
+ } else {
2593
+ controlsGroup.x = 0;
2594
+ }
2595
+ controlsGroup.y = 0;
2596
+ }
2511
2597
  if (rows.length === 0) {
2512
2598
  rows.push({ y: 0, items: [] });
2513
2599
  }
2514
2600
  return rows;
2515
2601
  }
2516
- function centerRowItems(items, containerWidth, totalControlsW) {
2602
+ function centerRowItems(items, containerWidth, totalControlsW, controlsGroupW = 0) {
2517
2603
  const groupItems = items.filter((it) => "groupName" in it);
2518
2604
  if (groupItems.length === 0) return;
2519
2605
  const totalGroupW = groupItems.reduce((s, it) => s + it.width, 0) + (groupItems.length - 1) * LEGEND_GROUP_GAP;
2520
- const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0);
2606
+ const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0) - controlsGroupW;
2521
2607
  const offset = Math.max(0, (availW - totalGroupW) / 2);
2522
2608
  let x = offset;
2523
2609
  for (const item of groupItems) {
@@ -2575,6 +2661,17 @@ function renderLegendD3(container, config, state, palette, isDark, callbacks, co
2575
2661
  for (const pill of currentLayout.pills) {
2576
2662
  renderPill(legendG, pill, palette, groupBg, callbacks);
2577
2663
  }
2664
+ if (currentLayout.controlsGroup) {
2665
+ renderControlsGroup(
2666
+ legendG,
2667
+ currentLayout.controlsGroup,
2668
+ palette,
2669
+ groupBg,
2670
+ pillBorder,
2671
+ callbacks,
2672
+ config
2673
+ );
2674
+ }
2578
2675
  for (const ctrl of currentLayout.controls) {
2579
2676
  renderControl(
2580
2677
  legendG,
@@ -2689,6 +2786,57 @@ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, con
2689
2786
  g.on("click", () => onClick());
2690
2787
  }
2691
2788
  }
2789
+ function renderControlsGroup(parent, layout, palette, groupBg, pillBorder, callbacks, config) {
2790
+ 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");
2791
+ if (!layout.expanded) {
2792
+ g.append("rect").attr("width", layout.width).attr("height", layout.height).attr("rx", layout.height / 2).attr("fill", groupBg);
2793
+ const iconSize = 14;
2794
+ const iconX = (layout.width - iconSize) / 2;
2795
+ const iconY = (layout.height - iconSize) / 2;
2796
+ 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");
2797
+ if (callbacks?.onControlsExpand) {
2798
+ const cb = callbacks.onControlsExpand;
2799
+ g.on("click", () => cb());
2800
+ }
2801
+ } else {
2802
+ const pill = layout.pill;
2803
+ g.append("rect").attr("width", layout.width).attr("height", layout.height).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
2804
+ const pillG = g.append("g").attr("class", "controls-gear-pill").style("cursor", "pointer");
2805
+ 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);
2806
+ 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);
2807
+ const iconSize = 14;
2808
+ const iconX = pill.x + (pill.width - iconSize) / 2;
2809
+ const iconY = pill.y + (pill.height - iconSize) / 2;
2810
+ 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");
2811
+ if (callbacks?.onControlsExpand) {
2812
+ const cb = callbacks.onControlsExpand;
2813
+ pillG.on("click", (event) => {
2814
+ event.stopPropagation();
2815
+ cb();
2816
+ });
2817
+ }
2818
+ const toggles = config?.controlsGroup?.toggles ?? [];
2819
+ for (const tl of layout.toggles) {
2820
+ const toggle = toggles.find((t) => t.id === tl.id);
2821
+ const entryG = g.append("g").attr("data-controls-toggle", tl.id).style("cursor", "pointer");
2822
+ if (tl.active) {
2823
+ entryG.append("circle").attr("cx", tl.dotCx).attr("cy", tl.dotCy).attr("r", LEGEND_TOGGLE_DOT_R).attr("fill", palette.primary ?? palette.text);
2824
+ } else {
2825
+ 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);
2826
+ }
2827
+ 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);
2828
+ if (callbacks?.onControlsToggle && toggle) {
2829
+ const cb = callbacks.onControlsToggle;
2830
+ const id = tl.id;
2831
+ const newActive = !tl.active;
2832
+ entryG.on("click", (event) => {
2833
+ event.stopPropagation();
2834
+ cb(id, newActive);
2835
+ });
2836
+ }
2837
+ }
2838
+ }
2839
+ }
2692
2840
  var init_legend_d3 = __esm({
2693
2841
  "src/utils/legend-d3.ts"() {
2694
2842
  "use strict";
@@ -3242,7 +3390,13 @@ function parseSequenceDgmo(content) {
3242
3390
  const groupName = groupMatch[1].trim();
3243
3391
  const groupColor = groupMatch[2]?.trim();
3244
3392
  let groupMeta;
3245
- const afterBracket = groupMatch[3]?.trim() || "";
3393
+ let afterBracket = groupMatch[3]?.trim() || "";
3394
+ let isCollapsed = false;
3395
+ const collapseMatch = afterBracket.match(/^collapse\b/i);
3396
+ if (collapseMatch) {
3397
+ isCollapsed = true;
3398
+ afterBracket = afterBracket.slice(collapseMatch[0].length).trim();
3399
+ }
3246
3400
  if (afterBracket.startsWith("|")) {
3247
3401
  const segments = afterBracket.split("|");
3248
3402
  const meta = parsePipeMetadata(
@@ -3263,7 +3417,8 @@ function parseSequenceDgmo(content) {
3263
3417
  name: groupName,
3264
3418
  participantIds: [],
3265
3419
  lineNumber,
3266
- ...groupMeta ? { metadata: groupMeta } : {}
3420
+ ...groupMeta ? { metadata: groupMeta } : {},
3421
+ ...isCollapsed ? { collapsed: true } : {}
3267
3422
  };
3268
3423
  result.groups.push(activeGroup);
3269
3424
  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
@@ -25186,6 +25414,7 @@ var init_renderer9 = __esm({
25186
25414
  init_d3();
25187
25415
  init_legend_constants();
25188
25416
  init_legend_d3();
25417
+ init_legend_layout();
25189
25418
  init_title_constants();
25190
25419
  BAR_H = 22;
25191
25420
  ROW_GAP = 6;
@@ -25479,6 +25708,109 @@ var init_state_renderer = __esm({
25479
25708
  }
25480
25709
  });
25481
25710
 
25711
+ // src/sequence/collapse.ts
25712
+ function applyCollapseProjection(parsed, collapsedGroups) {
25713
+ if (collapsedGroups.size === 0) {
25714
+ return {
25715
+ participants: parsed.participants,
25716
+ messages: parsed.messages,
25717
+ elements: parsed.elements,
25718
+ groups: parsed.groups,
25719
+ collapsedGroupIds: /* @__PURE__ */ new Map()
25720
+ };
25721
+ }
25722
+ const memberToGroup = /* @__PURE__ */ new Map();
25723
+ const collapsedGroupNames = /* @__PURE__ */ new Set();
25724
+ for (const group of parsed.groups) {
25725
+ if (collapsedGroups.has(group.lineNumber)) {
25726
+ collapsedGroupNames.add(group.name);
25727
+ for (const memberId of group.participantIds) {
25728
+ memberToGroup.set(memberId, group.name);
25729
+ }
25730
+ }
25731
+ }
25732
+ const participants = [];
25733
+ const insertedGroups = /* @__PURE__ */ new Set();
25734
+ for (const p of parsed.participants) {
25735
+ const groupName = memberToGroup.get(p.id);
25736
+ if (groupName) {
25737
+ if (!insertedGroups.has(groupName)) {
25738
+ insertedGroups.add(groupName);
25739
+ const group = parsed.groups.find(
25740
+ (g) => g.name === groupName && collapsedGroups.has(g.lineNumber)
25741
+ );
25742
+ participants.push({
25743
+ id: groupName,
25744
+ label: groupName,
25745
+ type: "default",
25746
+ lineNumber: group.lineNumber
25747
+ });
25748
+ }
25749
+ } else if (collapsedGroupNames.has(p.id)) {
25750
+ } else {
25751
+ participants.push(p);
25752
+ }
25753
+ }
25754
+ const remap = (id) => memberToGroup.get(id) ?? id;
25755
+ const messages = parsed.messages.map((msg) => ({
25756
+ ...msg,
25757
+ from: remap(msg.from),
25758
+ to: remap(msg.to)
25759
+ }));
25760
+ const elements = remapElements(parsed.elements, memberToGroup);
25761
+ const groups = parsed.groups.filter(
25762
+ (g) => !collapsedGroups.has(g.lineNumber)
25763
+ );
25764
+ return {
25765
+ participants,
25766
+ messages,
25767
+ elements,
25768
+ groups,
25769
+ collapsedGroupIds: memberToGroup
25770
+ };
25771
+ }
25772
+ function remapElements(elements, memberToGroup) {
25773
+ const remap = (id) => memberToGroup.get(id) ?? id;
25774
+ const result = [];
25775
+ for (const el of elements) {
25776
+ if (isSequenceSection(el)) {
25777
+ result.push(el);
25778
+ } else if (isSequenceNote(el)) {
25779
+ result.push({
25780
+ ...el,
25781
+ participantId: remap(el.participantId)
25782
+ });
25783
+ } else if (isSequenceBlock(el)) {
25784
+ result.push({
25785
+ ...el,
25786
+ children: remapElements(el.children, memberToGroup),
25787
+ elseChildren: remapElements(el.elseChildren, memberToGroup),
25788
+ ...el.elseIfBranches ? {
25789
+ elseIfBranches: el.elseIfBranches.map((branch) => ({
25790
+ ...branch,
25791
+ children: remapElements(branch.children, memberToGroup)
25792
+ }))
25793
+ } : {}
25794
+ });
25795
+ } else {
25796
+ const msg = el;
25797
+ const from = remap(msg.from);
25798
+ const to = remap(msg.to);
25799
+ if (from === to && from !== msg.from && !msg.label) {
25800
+ continue;
25801
+ }
25802
+ result.push({ ...msg, from, to });
25803
+ }
25804
+ }
25805
+ return result;
25806
+ }
25807
+ var init_collapse3 = __esm({
25808
+ "src/sequence/collapse.ts"() {
25809
+ "use strict";
25810
+ init_parser();
25811
+ }
25812
+ });
25813
+
25482
25814
  // src/sequence/tag-resolution.ts
25483
25815
  function propagateGroupTags(participantMeta, groups) {
25484
25816
  for (const group of groups) {
@@ -25934,13 +26266,32 @@ function applyGroupOrdering(participants, groups, messages = []) {
25934
26266
  }
25935
26267
  function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateToLine, options) {
25936
26268
  d3Selection12.select(container).selectAll("*").remove();
25937
- const { title, messages, elements, groups, options: parsedOptions } = parsed;
26269
+ const { title, options: parsedOptions } = parsed;
26270
+ const effectiveCollapsedGroups = /* @__PURE__ */ new Set();
26271
+ for (const group of parsed.groups) {
26272
+ if (group.collapsed) effectiveCollapsedGroups.add(group.lineNumber);
26273
+ }
26274
+ if (options?.collapsedGroups) {
26275
+ for (const ln of options.collapsedGroups) {
26276
+ if (effectiveCollapsedGroups.has(ln)) {
26277
+ effectiveCollapsedGroups.delete(ln);
26278
+ } else {
26279
+ effectiveCollapsedGroups.add(ln);
26280
+ }
26281
+ }
26282
+ }
26283
+ const collapsed = effectiveCollapsedGroups.size > 0 ? applyCollapseProjection(parsed, effectiveCollapsedGroups) : null;
26284
+ const messages = collapsed ? collapsed.messages : parsed.messages;
26285
+ const elements = collapsed ? collapsed.elements : parsed.elements;
26286
+ const groups = collapsed ? collapsed.groups : parsed.groups;
26287
+ const collapsedGroupIds = collapsed?.collapsedGroupIds ?? /* @__PURE__ */ new Map();
25938
26288
  const collapsedSections = options?.collapsedSections;
25939
26289
  const expandedNoteLines = options?.expandedNoteLines;
25940
26290
  const collapseNotesDisabled = parsedOptions["collapse-notes"]?.toLowerCase() === "no";
25941
26291
  const isNoteExpanded = (note) => expandedNoteLines === void 0 || collapseNotesDisabled || expandedNoteLines.has(note.lineNumber);
26292
+ const sourceParticipants = collapsed ? collapsed.participants : parsed.participants;
25942
26293
  const participants = applyPositionOverrides(
25943
- applyGroupOrdering(parsed.participants, groups, messages)
26294
+ applyGroupOrdering(sourceParticipants, groups, messages)
25944
26295
  );
25945
26296
  if (participants.length === 0) return;
25946
26297
  const activationsOff = parsedOptions.activations?.toLowerCase() === "off";
@@ -26111,13 +26462,16 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26111
26462
  const preSectionMsgIndices = [];
26112
26463
  const sectionRegions = [];
26113
26464
  {
26465
+ const msgLineToIndex = /* @__PURE__ */ new Map();
26466
+ messages.forEach((m, i) => msgLineToIndex.set(m.lineNumber, i));
26467
+ const findMsgIndex = (child) => msgLineToIndex.get(child.lineNumber) ?? -1;
26114
26468
  const collectMsgIndicesFromBlock = (block) => {
26115
26469
  const indices = [];
26116
26470
  for (const child of block.children) {
26117
26471
  if (isSequenceBlock(child)) {
26118
26472
  indices.push(...collectMsgIndicesFromBlock(child));
26119
26473
  } else if (!isSequenceSection(child) && !isSequenceNote(child)) {
26120
- const idx = messages.indexOf(child);
26474
+ const idx = findMsgIndex(child);
26121
26475
  if (idx >= 0) indices.push(idx);
26122
26476
  }
26123
26477
  }
@@ -26127,7 +26481,7 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26127
26481
  if (isSequenceBlock(child)) {
26128
26482
  indices.push(...collectMsgIndicesFromBlock(child));
26129
26483
  } else if (!isSequenceSection(child) && !isSequenceNote(child)) {
26130
- const idx = messages.indexOf(child);
26484
+ const idx = findMsgIndex(child);
26131
26485
  if (idx >= 0) indices.push(idx);
26132
26486
  }
26133
26487
  }
@@ -26137,7 +26491,7 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26137
26491
  if (isSequenceBlock(child)) {
26138
26492
  indices.push(...collectMsgIndicesFromBlock(child));
26139
26493
  } else if (!isSequenceSection(child) && !isSequenceNote(child)) {
26140
- const idx = messages.indexOf(child);
26494
+ const idx = findMsgIndex(child);
26141
26495
  if (idx >= 0) indices.push(idx);
26142
26496
  }
26143
26497
  }
@@ -26152,7 +26506,7 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26152
26506
  } else if (isSequenceBlock(el)) {
26153
26507
  currentTarget.push(...collectMsgIndicesFromBlock(el));
26154
26508
  } else {
26155
- const idx = messages.indexOf(el);
26509
+ const idx = findMsgIndex(el);
26156
26510
  if (idx >= 0) currentTarget.push(idx);
26157
26511
  }
26158
26512
  }
@@ -26214,7 +26568,7 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26214
26568
  const titleOffset = title ? TITLE_HEIGHT5 : 0;
26215
26569
  const LEGEND_FIXED_GAP4 = 8;
26216
26570
  const legendTopSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + LEGEND_FIXED_GAP4 : 0;
26217
- const groupOffset = groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
26571
+ const groupOffset = parsed.groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
26218
26572
  const participantStartY = TOP_MARGIN + titleOffset + legendTopSpace + PARTICIPANT_Y_OFFSET + groupOffset;
26219
26573
  const lifelineStartY0 = participantStartY + PARTICIPANT_BOX_HEIGHT;
26220
26574
  const hasActors = participants.some((p) => p.type === "actor");
@@ -26384,6 +26738,17 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26384
26738
  svgWidth
26385
26739
  );
26386
26740
  }
26741
+ const collapsedGroupNames = /* @__PURE__ */ new Set();
26742
+ const collapsedGroupMeta = /* @__PURE__ */ new Map();
26743
+ for (const group of parsed.groups) {
26744
+ if (effectiveCollapsedGroups.has(group.lineNumber)) {
26745
+ collapsedGroupNames.add(group.name);
26746
+ collapsedGroupMeta.set(group.name, {
26747
+ lineNumber: group.lineNumber,
26748
+ metadata: group.metadata
26749
+ });
26750
+ }
26751
+ }
26387
26752
  for (const group of groups) {
26388
26753
  if (group.participantIds.length === 0) continue;
26389
26754
  const memberXs = group.participantIds.map((id) => participantX.get(id)).filter((x) => x !== void 0);
@@ -26400,8 +26765,10 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26400
26765
  isDark ? 15 : 20
26401
26766
  ) : isDark ? palette.surface : palette.bg;
26402
26767
  const strokeColor = groupTagColor || palette.textMuted;
26403
- 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));
26404
- 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);
26768
+ const groupG = svg.append("g").attr("class", "group-box-wrapper").attr("data-group-toggle", "").attr("data-group-line", String(group.lineNumber)).attr("cursor", "pointer");
26769
+ groupG.append("title").text("Click to collapse");
26770
+ 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");
26771
+ 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);
26405
26772
  }
26406
26773
  const lifelineStartY = lifelineStartY0;
26407
26774
  participants.forEach((participant, index) => {
@@ -26410,6 +26777,14 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26410
26777
  const pTagValue = tagMap?.participants.get(participant.id);
26411
26778
  const pTagColor = getTagColor(pTagValue);
26412
26779
  const pTagAttr = tagKey && pTagValue ? { key: tagKey, value: pTagValue.toLowerCase() } : void 0;
26780
+ const isCollapsedGroup = collapsedGroupNames.has(participant.id);
26781
+ let effectiveTagColor = pTagColor;
26782
+ if (isCollapsedGroup && !effectiveTagColor) {
26783
+ const meta = collapsedGroupMeta.get(participant.id);
26784
+ if (meta?.metadata && tagKey) {
26785
+ effectiveTagColor = getTagColor(meta.metadata[tagKey]);
26786
+ }
26787
+ }
26413
26788
  renderParticipant(
26414
26789
  svg,
26415
26790
  participant,
@@ -26417,10 +26792,35 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26417
26792
  cy,
26418
26793
  palette,
26419
26794
  isDark,
26420
- pTagColor,
26795
+ effectiveTagColor,
26421
26796
  pTagAttr
26422
26797
  );
26423
- 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);
26798
+ if (isCollapsedGroup) {
26799
+ const meta = collapsedGroupMeta.get(participant.id);
26800
+ const drillColor = effectiveTagColor || palette.textMuted;
26801
+ const drillBarH = 6;
26802
+ const boxW = PARTICIPANT_BOX_WIDTH;
26803
+ const fullH = PARTICIPANT_BOX_HEIGHT + GROUP_PADDING_TOP + GROUP_PADDING_BOTTOM;
26804
+ const clipId = `clip-drill-group-${participant.id.replace(/[^a-zA-Z0-9-]/g, "-")}`;
26805
+ const participantG = svg.select(
26806
+ `.participant[data-participant-id="${participant.id}"]`
26807
+ );
26808
+ participantG.attr("data-group-toggle", "").attr("data-group-line", String(meta.lineNumber)).attr("cursor", "pointer");
26809
+ participantG.append("title").text("Click to expand");
26810
+ const pFill = effectiveTagColor ? mix(
26811
+ effectiveTagColor,
26812
+ isDark ? palette.surface : palette.bg,
26813
+ isDark ? 30 : 40
26814
+ ) : isDark ? mix(palette.overlay, palette.surface, 50) : mix(palette.bg, palette.surface, 50);
26815
+ const pStroke = effectiveTagColor || palette.border;
26816
+ 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);
26817
+ 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);
26818
+ 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);
26819
+ 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})`);
26820
+ }
26821
+ const llY = isCollapsedGroup ? lifelineStartY + GROUP_PADDING_BOTTOM : lifelineStartY;
26822
+ const llColor = isCollapsedGroup ? effectiveTagColor || palette.textMuted : pTagColor || palette.textMuted;
26823
+ 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);
26424
26824
  if (tagKey && pTagValue) {
26425
26825
  lifelineEl.attr(`data-tag-${tagKey}`, pTagValue.toLowerCase());
26426
26826
  }
@@ -26648,23 +27048,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
26648
27048
  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");
26649
27049
  const msgCount = sectionMsgCounts.get(sec.lineNumber) ?? 0;
26650
27050
  const labelText = isCollapsed ? `${sec.label} (${msgCount} ${msgCount === 1 ? "message" : "messages"})` : sec.label;
26651
- const labelColor = isCollapsed ? "#ffffff" : lineColor;
26652
- const chevronSpace = 14;
26653
27051
  const labelX = (sectionLineX1 + sectionLineX2) / 2;
26654
- const chevronX = labelX - (labelText.length * 3.5 + 8 + chevronSpace / 2);
26655
- const chevronY = secY;
26656
- if (isCollapsed) {
26657
- sectionG.append("path").attr(
26658
- "d",
26659
- `M ${chevronX} ${chevronY - 4} L ${chevronX + 6} ${chevronY} L ${chevronX} ${chevronY + 4} Z`
26660
- ).attr("fill", labelColor).attr("class", "section-chevron");
26661
- } else {
26662
- sectionG.append("path").attr(
26663
- "d",
26664
- `M ${chevronX - 1} ${chevronY - 3} L ${chevronX + 7} ${chevronY - 3} L ${chevronX + 3} ${chevronY + 3} Z`
26665
- ).attr("fill", labelColor).attr("class", "section-chevron");
26666
- }
26667
- 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);
27052
+ 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);
26668
27053
  }
26669
27054
  const SELF_CALL_WIDTH = 30;
26670
27055
  const SELF_CALL_HEIGHT = 25;
@@ -26945,6 +27330,7 @@ var init_renderer10 = __esm({
26945
27330
  init_inline_markdown();
26946
27331
  init_fonts();
26947
27332
  init_parser();
27333
+ init_collapse3();
26948
27334
  init_tag_resolution();
26949
27335
  init_tag_groups();
26950
27336
  init_legend_constants();
@@ -32507,6 +32893,7 @@ init_legend_d3();
32507
32893
  init_legend_layout();
32508
32894
  init_d3();
32509
32895
  init_renderer10();
32896
+ init_collapse3();
32510
32897
  init_colors();
32511
32898
  init_palettes();
32512
32899
 
@@ -32514,6 +32901,24 @@ init_palettes();
32514
32901
  var import_lz_string = __toESM(require_lz_string(), 1);
32515
32902
  var DEFAULT_BASE_URL = "https://online.diagrammo.app";
32516
32903
  var COMPRESSED_SIZE_LIMIT = 8192;
32904
+ function encodeViewState(state) {
32905
+ const keys = Object.keys(state);
32906
+ if (keys.length === 0) return "";
32907
+ return (0, import_lz_string.compressToEncodedURIComponent)(JSON.stringify(state));
32908
+ }
32909
+ function decodeViewState(encoded) {
32910
+ if (!encoded) return {};
32911
+ try {
32912
+ const json = (0, import_lz_string.decompressFromEncodedURIComponent)(encoded);
32913
+ if (!json) return {};
32914
+ const parsed = JSON.parse(json);
32915
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
32916
+ return {};
32917
+ return parsed;
32918
+ } catch {
32919
+ return {};
32920
+ }
32921
+ }
32517
32922
  function encodeDiagramUrl(dsl, options) {
32518
32923
  const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL;
32519
32924
  const compressed = (0, import_lz_string.compressToEncodedURIComponent)(dsl);
@@ -32526,23 +32931,17 @@ function encodeDiagramUrl(dsl, options) {
32526
32931
  };
32527
32932
  }
32528
32933
  let hash = `dgmo=${compressed}`;
32529
- if (options?.viewState?.activeTagGroup) {
32530
- hash += `&tag=${encodeURIComponent(options.viewState.activeTagGroup)}`;
32531
- }
32532
- if (options?.viewState?.collapsedGroups?.length) {
32533
- hash += `&cg=${encodeURIComponent(options.viewState.collapsedGroups.join(","))}`;
32534
- }
32535
- if (options?.viewState?.swimlaneTagGroup) {
32536
- hash += `&swim=${encodeURIComponent(options.viewState.swimlaneTagGroup)}`;
32537
- }
32538
- if (options?.viewState?.collapsedLanes?.length) {
32539
- hash += `&cl=${encodeURIComponent(options.viewState.collapsedLanes.join(","))}`;
32934
+ if (options?.viewState) {
32935
+ const vsEncoded = encodeViewState(options.viewState);
32936
+ if (vsEncoded) {
32937
+ hash += `&vs=${vsEncoded}`;
32938
+ }
32540
32939
  }
32541
- if (options?.viewState?.palette && options.viewState.palette !== "nord") {
32542
- hash += `&pal=${encodeURIComponent(options.viewState.palette)}`;
32940
+ if (options?.palette && options.palette !== "nord") {
32941
+ hash += `&pal=${encodeURIComponent(options.palette)}`;
32543
32942
  }
32544
- if (options?.viewState?.theme && options.viewState.theme !== "dark") {
32545
- hash += `&th=${encodeURIComponent(options.viewState.theme)}`;
32943
+ if (options?.theme && options.theme !== "dark") {
32944
+ hash += `&th=${encodeURIComponent(options.theme)}`;
32546
32945
  }
32547
32946
  if (options?.filename) {
32548
32947
  hash += `&fn=${encodeURIComponent(options.filename)}`;
@@ -32552,6 +32951,8 @@ function encodeDiagramUrl(dsl, options) {
32552
32951
  function decodeDiagramUrl(hash) {
32553
32952
  const empty = { dsl: "", viewState: {} };
32554
32953
  let filename;
32954
+ let palette;
32955
+ let theme;
32555
32956
  if (!hash) return empty;
32556
32957
  let raw = hash;
32557
32958
  if (raw.startsWith("#") || raw.startsWith("?")) {
@@ -32559,38 +32960,31 @@ function decodeDiagramUrl(hash) {
32559
32960
  }
32560
32961
  const parts = raw.split("&");
32561
32962
  let payload = parts[0];
32562
- const viewState = {};
32963
+ let viewState = {};
32563
32964
  for (let i = 1; i < parts.length; i++) {
32564
32965
  const eq = parts[i].indexOf("=");
32565
32966
  if (eq === -1) continue;
32566
32967
  const key = parts[i].slice(0, eq);
32567
- const val = decodeURIComponent(parts[i].slice(eq + 1));
32568
- if (key === "tag" && val) {
32569
- viewState.activeTagGroup = val;
32570
- }
32571
- if (key === "cg" && val) {
32572
- viewState.collapsedGroups = val.split(",").filter(Boolean);
32573
- }
32574
- if (key === "swim" && val) {
32575
- viewState.swimlaneTagGroup = val;
32968
+ const val = parts[i].slice(eq + 1);
32969
+ if (key === "vs" && val) {
32970
+ viewState = decodeViewState(val);
32576
32971
  }
32577
- if (key === "cl" && val) {
32578
- viewState.collapsedLanes = val.split(",").filter(Boolean);
32972
+ if (key === "pal" && val) palette = decodeURIComponent(val);
32973
+ if (key === "th") {
32974
+ const decoded = decodeURIComponent(val);
32975
+ if (decoded === "light" || decoded === "dark") theme = decoded;
32579
32976
  }
32580
- if (key === "pal" && val) viewState.palette = val;
32581
- if (key === "th" && (val === "light" || val === "dark"))
32582
- viewState.theme = val;
32583
- if (key === "fn" && val) filename = val;
32977
+ if (key === "fn" && val) filename = decodeURIComponent(val);
32584
32978
  }
32585
32979
  if (payload.startsWith("dgmo=")) {
32586
32980
  payload = payload.slice(5);
32587
32981
  }
32588
- if (!payload) return { dsl: "", viewState, filename };
32982
+ if (!payload) return { dsl: "", viewState, palette, theme, filename };
32589
32983
  try {
32590
32984
  const result = (0, import_lz_string.decompressFromEncodedURIComponent)(payload);
32591
- return { dsl: result ?? "", viewState, filename };
32985
+ return { dsl: result ?? "", viewState, palette, theme, filename };
32592
32986
  } catch {
32593
- return { dsl: "", viewState, filename };
32987
+ return { dsl: "", viewState, palette, theme, filename };
32594
32988
  }
32595
32989
  }
32596
32990
 
@@ -33350,6 +33744,7 @@ export {
33350
33744
  RECOGNIZED_COLOR_NAMES,
33351
33745
  RULE_COUNT,
33352
33746
  addDurationToDate,
33747
+ applyCollapseProjection,
33353
33748
  applyGroupOrdering,
33354
33749
  applyPositionOverrides,
33355
33750
  boldPalette,
@@ -33379,8 +33774,10 @@ export {
33379
33774
  computeTimeTicks,
33380
33775
  contrastText,
33381
33776
  decodeDiagramUrl,
33777
+ decodeViewState,
33382
33778
  draculaPalette,
33383
33779
  encodeDiagramUrl,
33780
+ encodeViewState,
33384
33781
  extractDiagramSymbols,
33385
33782
  extractTagDeclarations,
33386
33783
  formatDateLabel,