@cardenelabs/cdl 0.123.0 → 0.125.0

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/react.cjs CHANGED
@@ -1006,7 +1006,7 @@ function drawsTitleOutsideBox(kind) {
1006
1006
  }
1007
1007
  var TITLE_FONT_SIZE_DEFAULT = 18;
1008
1008
  var TITLE_FONT_SIZE = new Map([
1009
- ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon", "station"].map(
1009
+ ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"].map(
1010
1010
  (k) => [k, 13]
1011
1011
  ),
1012
1012
  ...[
@@ -1051,8 +1051,10 @@ var TITLE_FONT_SIZE = new Map([
1051
1051
  "webhook"
1052
1052
  ].map((k) => [k, 20]),
1053
1053
  ...["card", "event", "function"].map((k) => [k, 24]),
1054
+ ["station", 25],
1054
1055
  ["actor", 32],
1055
- ["storage", 34]
1056
+ ["storage", 34],
1057
+ ["timeline-number", 18]
1056
1058
  ]);
1057
1059
  function titleFontSize(kind) {
1058
1060
  return TITLE_FONT_SIZE.get(kind ?? "") ?? TITLE_FONT_SIZE_DEFAULT;
@@ -1062,9 +1064,17 @@ function skipsNodeTitleWidthCheck(kind) {
1062
1064
  return KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE.has(kind ?? "");
1063
1065
  }
1064
1066
  var TITLE_BOX_PADDING = 52;
1067
+ var TIMELINE_NUMBER_STROKE = 4;
1068
+ function titleUsableWidth(kind, w, h) {
1069
+ if (kind === "timeline-number") {
1070
+ return Math.min(w, h) - 2 * TIMELINE_NUMBER_STROKE;
1071
+ }
1072
+ return w - (drawsTitleOutsideBox(kind) ? 0 : TITLE_BOX_PADDING);
1073
+ }
1065
1074
  var KINDS_SCALING_TITLE_WHEN_LOW = /* @__PURE__ */ new Set(["actor", "event", "function", "storage"]);
1066
1075
  var KINDS_SCALING_TITLE_ALWAYS = /* @__PURE__ */ new Set([
1067
1076
  "card",
1077
+ "timeline-number",
1068
1078
  "shape-file",
1069
1079
  "shape-folder",
1070
1080
  "shape-cloud",
@@ -1148,14 +1158,16 @@ var KINDS_DRAWING_ONLY_SHAPE = [
1148
1158
  ];
1149
1159
  var KINDS_WITHOUT_EYEBROW_TEXT = /* @__PURE__ */ new Set([
1150
1160
  ...KINDS_DRAWING_ONLY_SHAPE,
1151
- ...KINDS_WITHOUT_TITLE_TEXT
1161
+ ...KINDS_WITHOUT_TITLE_TEXT,
1162
+ "timeline-number"
1152
1163
  ]);
1153
1164
  function rendersEyebrow(kind) {
1154
1165
  return !KINDS_WITHOUT_EYEBROW_TEXT.has(kind ?? "");
1155
1166
  }
1156
1167
  var KINDS_WITHOUT_SUBTITLE_TEXT = /* @__PURE__ */ new Set([
1157
1168
  ...KINDS_DRAWING_OWN_PAYLOAD,
1158
- ...KINDS_WITHOUT_TITLE_TEXT
1169
+ ...KINDS_WITHOUT_TITLE_TEXT,
1170
+ "timeline-number"
1159
1171
  ]);
1160
1172
  function rendersSubtitle(kind) {
1161
1173
  return !KINDS_WITHOUT_SUBTITLE_TEXT.has(kind ?? "");
@@ -3089,6 +3101,14 @@ function curvePath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3089
3101
 
3090
3102
  // src/layout/metro-path.ts
3091
3103
  var METRO_CORNER_RADIUS = 24;
3104
+ var METRO_BRANCH_CORNER_RADIUS = 22;
3105
+ var METRO_BRANCH_STUB = 30;
3106
+ var METRO_DECISION_HEIGHT = 88;
3107
+ var METRO_DECISION_WIDTH = 116;
3108
+ var METRO_RETURN_END_OFFSET = 43;
3109
+ var METRO_RETURN_START_OFFSET = 24;
3110
+ var METRO_LINE_WIDTH = 10;
3111
+ var METRO_LINE_WIDTH_CSS = `var(--cdl-metro-line-width, ${METRO_LINE_WIDTH}px)`;
3092
3112
  var T = Math.SQRT2 - 1;
3093
3113
  function round2(v) {
3094
3114
  return Math.round(v * 100) / 100;
@@ -3106,7 +3126,63 @@ function polyline(points) {
3106
3126
  });
3107
3127
  return distinct.map((p, i) => `${i === 0 ? "M" : "L"} ${point(p)}`).join(" ");
3108
3128
  }
3109
- function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proportional") {
3129
+ function quadraticCornerPath(start, corner, end, radius) {
3130
+ const horizontalDir = Math.sign(corner.x - start.x);
3131
+ const verticalDir = Math.sign(end.y - corner.y);
3132
+ const r = Math.min(
3133
+ radius,
3134
+ Math.abs(corner.x - start.x),
3135
+ Math.abs(end.y - corner.y)
3136
+ );
3137
+ const before = { x: corner.x - horizontalDir * r, y: corner.y };
3138
+ const after = { x: corner.x, y: corner.y + verticalDir * r };
3139
+ return [
3140
+ `M ${point(start)}`,
3141
+ `L ${point(before)}`,
3142
+ `Q ${point(corner)} ${point(after)}`,
3143
+ `L ${point(end)}`
3144
+ ].join(" ");
3145
+ }
3146
+ function decisionBranchPath(start, end) {
3147
+ const horizontalDir = Math.sign(end.x - start.x);
3148
+ const verticalDir = Math.sign(end.y - start.y);
3149
+ const turnX = start.x + horizontalDir * (METRO_BRANCH_STUB + METRO_BRANCH_CORNER_RADIUS);
3150
+ const radius = Math.min(
3151
+ METRO_BRANCH_CORNER_RADIUS,
3152
+ Math.abs(end.y - start.y) / 2,
3153
+ Math.abs(end.x - turnX)
3154
+ );
3155
+ const firstCorner = { x: turnX, y: start.y };
3156
+ const secondCorner = { x: turnX, y: end.y };
3157
+ const beforeFirst = {
3158
+ x: turnX - horizontalDir * radius,
3159
+ y: start.y
3160
+ };
3161
+ const afterFirst = {
3162
+ x: turnX,
3163
+ y: start.y + verticalDir * radius
3164
+ };
3165
+ const beforeSecond = {
3166
+ x: turnX,
3167
+ y: end.y - verticalDir * radius
3168
+ };
3169
+ const afterSecond = {
3170
+ x: turnX + horizontalDir * radius,
3171
+ y: end.y
3172
+ };
3173
+ return {
3174
+ d: [
3175
+ `M ${point(start)}`,
3176
+ `L ${point(beforeFirst)}`,
3177
+ `Q ${point(firstCorner)} ${point(afterFirst)}`,
3178
+ `L ${point(beforeSecond)}`,
3179
+ `Q ${point(secondCorner)} ${point(afterSecond)}`,
3180
+ `L ${point(end)}`
3181
+ ].join(" "),
3182
+ turnX
3183
+ };
3184
+ }
3185
+ function metroPath(from, to, edge, _labelMarginExtra = 0, _textMetrics = "proportional") {
3110
3186
  const fromLeft = from.cx - from.w / 2;
3111
3187
  const fromRight = from.cx + from.w / 2;
3112
3188
  const toLeft = to.cx - to.w / 2;
@@ -3141,11 +3217,45 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3141
3217
  const sgn = Math.sign(dy);
3142
3218
  let d;
3143
3219
  let mid;
3144
- let tangent;
3145
- if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
3220
+ let metroReturn = false;
3221
+ if (from.kind === "decision" && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
3222
+ const verticalDir = Math.sign(dy);
3223
+ if (Math.abs(to.cx - from.cx) < SAME_HEIGHT_THRESHOLD) {
3224
+ const start = { x: from.cx, y: from.cy + verticalDir * from.h / 2 };
3225
+ const end = { x: to.cx, y: to.cy };
3226
+ d = polyline([start, end]);
3227
+ fromSide = verticalDir < 0 ? "top" : "bottom";
3228
+ toSide = verticalDir < 0 ? "bottom" : "top";
3229
+ mid = { x: start.x, y: (start.y + end.y) / 2 };
3230
+ } else {
3231
+ const horizontalDir = Math.sign(to.cx - from.cx);
3232
+ const start = {
3233
+ x: from.cx + horizontalDir * from.w / 2,
3234
+ y: from.cy
3235
+ };
3236
+ const end = {
3237
+ x: to.cx - horizontalDir * to.w / 2,
3238
+ y: to.cy
3239
+ };
3240
+ const branch = decisionBranchPath(start, end);
3241
+ d = branch.d;
3242
+ fromSide = horizontalDir > 0 ? "right" : "left";
3243
+ toSide = horizontalDir > 0 ? "left" : "right";
3244
+ mid = { x: branch.turnX, y: (start.y + end.y) / 2 };
3245
+ }
3246
+ } else if (edge?.style === "dashed" && from.kind === "station" && to.kind === "station" && to.cx < from.cx && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
3247
+ const verticalDir = Math.sign(to.cy - from.cy);
3248
+ const start = { x: from.cx - METRO_RETURN_START_OFFSET, y: from.cy };
3249
+ const end = { x: to.cx, y: to.cy - verticalDir * METRO_RETURN_END_OFFSET };
3250
+ const corner = { x: end.x, y: start.y };
3251
+ fromSide = "left";
3252
+ toSide = verticalDir < 0 ? "bottom" : "top";
3253
+ d = quadraticCornerPath(start, corner, end, METRO_BRANCH_CORNER_RADIUS);
3254
+ mid = { x: (start.x + corner.x) / 2, y: start.y };
3255
+ metroReturn = true;
3256
+ } else if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
3146
3257
  d = `M ${point({ x: x1, y: y1 })} L ${point({ x: x2, y: y2 })}`;
3147
3258
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3148
- tangent = { x: x2 - x1, y: y2 - y1 };
3149
3259
  } else if (fromSide !== toSide && Math.abs(x2 - x1) >= Math.abs(dy)) {
3150
3260
  const horizontal = Math.abs(x2 - x1);
3151
3261
  const s = (horizontal - Math.abs(dy)) / 2;
@@ -3175,7 +3285,6 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3175
3285
  d = polyline([p0, p1, p2, p3]);
3176
3286
  }
3177
3287
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3178
- tangent = w;
3179
3288
  } else {
3180
3289
  fromSide = "right";
3181
3290
  toSide = "right";
@@ -3205,28 +3314,15 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3205
3314
  `L ${point(p4)}`
3206
3315
  ].join(" ");
3207
3316
  mid = { x: p2.x - T * r, y: p2.y };
3208
- tangent = { x: 0, y: sgn };
3209
- }
3210
- if (edge?.overlay) {
3211
- return { d, fromSide, toSide, labelX: mid.x, labelY: mid.y, labelAnchor: "middle" };
3212
3317
  }
3213
- const tangentLength = Math.hypot(tangent.x, tangent.y);
3214
- let normalX = tangent.y / tangentLength;
3215
- let normalY = -tangent.x / tangentLength;
3216
- if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
3217
- normalX *= -1;
3218
- normalY *= -1;
3219
- }
3220
- const labelW = computeLabelBoxW(edge?.label, edge?.sub, textMetrics);
3221
- const labelH = labelPillH(edge?.sub);
3222
- const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
3223
3318
  return {
3224
3319
  d,
3225
3320
  fromSide,
3226
3321
  toSide,
3227
- labelX: mid.x + normalX * distance,
3228
- labelY: mid.y + normalY * distance,
3229
- labelAnchor: "middle"
3322
+ labelX: mid.x,
3323
+ labelY: mid.y,
3324
+ labelAnchor: "middle",
3325
+ ...metroReturn ? { metroReturn: true } : {}
3230
3326
  };
3231
3327
  }
3232
3328
 
@@ -3353,12 +3449,14 @@ var NODE_KINDS = [
3353
3449
  "shape-lawyer",
3354
3450
  "shape-trader",
3355
3451
  "shape-customer-service",
3356
- // 流れの始まりと終わりの印 2 種 (#560) ... 塗った丸と輪で囲んだ丸。
3357
- // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
3452
+ // 流れの始まりと終わりの印 2 種 (#560) ... 始点と終点は線の端から推測せず、著者が
3453
+ // `kind: "mark-start"` / `kind: "mark-end"` の node を明示する。印は文字を描かない
3358
3454
  "mark-start",
3359
3455
  "mark-end",
3360
3456
  // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
3361
3457
  "station",
3458
+ // 時間軸の番号 (#1017) ... 実線の軸上に番号を入れた丸を置く。路線図の駅とは別の意味を持つ。
3459
+ "timeline-number",
3362
3460
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
3363
3461
  "sequence-board"
3364
3462
  ];
@@ -6550,7 +6648,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6550
6648
  const LIFELINE_W_THRESHOLD = 8;
6551
6649
  for (const n of laid.nodes) {
6552
6650
  if (n.w < LIFELINE_W_THRESHOLD) continue;
6553
- if (n.kind === "station") continue;
6651
+ if (n.kind === "station" || n.kind === "timeline-number") continue;
6554
6652
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
6555
6653
  push(
6556
6654
  "node-visibility",
@@ -6576,8 +6674,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6576
6674
  if (!isRenderedNode(n)) continue;
6577
6675
  const titleTrimmed = n.title?.trim() ?? "";
6578
6676
  if (!titleTrimmed) continue;
6579
- const \u4F59\u767D = drawsTitleOutsideBox(n.kind) ? 0 : TITLE_BOX_PADDING;
6580
- const \u4F7F\u3048\u308B\u5E452 = n.w - \u4F59\u767D;
6677
+ const \u4F7F\u3048\u308B\u5E452 = titleUsableWidth(n.kind, n.w, n.h);
6581
6678
  const \u5B9F\u5E45 = measureTextWidth(titleTrimmed, {
6582
6679
  fontSize: titleFontSize(n.kind),
6583
6680
  bound: "drawn",
@@ -6596,7 +6693,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6596
6693
  push(
6597
6694
  "text-readability",
6598
6695
  // 字ごとの実幅は小数になるので、読む人に出す時だけ丸める (判定は丸めない)
6599
- `node "${n.id}" title "${n.title}" \u304C node \u5E45 ${n.w}px \u3092\u8D85\u904E\u3059\u308B\u53EF\u80FD\u6027 (\u671F\u5F85 ${Math.round(\u5B9F\u5E45 + \u4F59\u767D)}px)`,
6696
+ `node "${n.id}" title "${n.title}" \u304C node \u5E45 ${n.w}px \u3092\u8D85\u904E\u3059\u308B\u53EF\u80FD\u6027 (\u671F\u5F85 ${Math.round(\u5B9F\u5E45 + (n.w - \u4F7F\u3048\u308B\u5E452))}px)`,
6600
6697
  "warn"
6601
6698
  );
6602
6699
  }
@@ -8156,6 +8253,8 @@ var TOKENS = {
8156
8253
  lane_label_h: 40,
8157
8254
  lane_top_pad: 100,
8158
8255
  contain_pad: 28,
8256
+ stage_column_gap: 48,
8257
+ stage_card_text_inset: 18,
8159
8258
  // node sizes (kind ごと、 unified row h は engine が max() で算出)
8160
8259
  node: {
8161
8260
  actor: { w: 340, h: 165 },
@@ -8282,7 +8381,10 @@ var TOKENS = {
8282
8381
  "mark-start": { w: 96, h: 96 },
8283
8382
  "mark-end": { w: 96, h: 96 },
8284
8383
  // 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
8285
- station: { w: 24, h: 24 },
8384
+ // 半径 16 と縁 6 の外径を、線路との接点を含めて収める。
8385
+ station: { w: 38, h: 38 },
8386
+ // 時間軸の番号 (#1017) ... 半径 24 の丸と太さ 4 の縁を収める。
8387
+ "timeline-number": { w: 52, h: 52 },
8286
8388
  // 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
8287
8389
  // ここは書かなかった時の受け皿
8288
8390
  "sequence-board": { w: 960, h: 640 }
@@ -8297,6 +8399,8 @@ var LANE_TOP_PAD = TOKENS.lane_top_pad;
8297
8399
  var STACK_GAP = TOKENS.row_gap;
8298
8400
  var NODE_SIZE = TOKENS.node;
8299
8401
  var CONTAIN_PAD = TOKENS.contain_pad;
8402
+ var STAGE_COLUMN_GAP = TOKENS.stage_column_gap;
8403
+ var STAGE_CARD_TEXT_INSET = TOKENS.stage_card_text_inset;
8300
8404
  var VIEW_PAD = TOKENS.view_pad;
8301
8405
  var HEADER_RESERVE = TOKENS.header_reserve;
8302
8406
  var DEFAULT_LANE_GAP = 80;
@@ -8397,12 +8501,24 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
8397
8501
  if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
8398
8502
  if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
8399
8503
  const c = curvePath(from, to, e, labelMarginExtra, textMetrics);
8504
+ const fromLane = lmap.get(from.lane);
8505
+ const toLane = lmap.get(to.lane);
8506
+ let labelX = c.labelX;
8507
+ if (fromLane?.stage !== void 0 && toLane?.stage !== void 0 && fromLane.id !== toLane.id && (e.label || e.sub)) {
8508
+ const [leftLane, rightLane] = fromLane.x <= toLane.x ? [fromLane, toLane] : [toLane, fromLane];
8509
+ const gapLeft = leftLane.x + leftLane.width - STAGE_COLUMN_GAP / 2;
8510
+ const gapRight = rightLane.x + STAGE_COLUMN_GAP / 2;
8511
+ const labelHalfWidth = computeLabelBoxW(e.label, e.sub, textMetrics) / 2;
8512
+ const minX = gapLeft + labelHalfWidth;
8513
+ const maxX = gapRight - labelHalfWidth;
8514
+ if (minX <= maxX) labelX = Math.min(maxX, Math.max(minX, labelX));
8515
+ }
8400
8516
  positionedEdges.push({
8401
8517
  ...e,
8402
8518
  d: c.d,
8403
8519
  fromSide: c.fromSide,
8404
8520
  toSide: c.toSide,
8405
- labelX: c.labelX + (e.labelOffsetX ?? 0),
8521
+ labelX: labelX + (e.labelOffsetX ?? 0),
8406
8522
  labelY: c.labelY + (e.labelOffsetY ?? 0),
8407
8523
  labelAnchor: c.labelAnchor
8408
8524
  });
@@ -8419,7 +8535,8 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
8419
8535
  toSide: m.toSide,
8420
8536
  labelX: m.labelX + (e.labelOffsetX ?? 0),
8421
8537
  labelY: m.labelY + (e.labelOffsetY ?? 0),
8422
- labelAnchor: m.labelAnchor
8538
+ labelAnchor: m.labelAnchor,
8539
+ ...m.metroReturn ? { metroReturn: true } : {}
8423
8540
  });
8424
8541
  } else {
8425
8542
  autoEdges.push(e);
@@ -9107,6 +9224,57 @@ function bowSharedStraightPaths(edges, nodes, skipIds, textMetrics = "proportion
9107
9224
  function nodeRect(n) {
9108
9225
  return { x: n.cx - n.w / 2, y: n.cy - n.h / 2, w: n.w, h: n.h };
9109
9226
  }
9227
+ function countPathObstacleCrossings(d, obstacles) {
9228
+ const segments = flattenPathSegments(extractPathSegments(d));
9229
+ return obstacles.filter((obstacle) => segmentsCrossRect(segments, nodeRect(obstacle))).length;
9230
+ }
9231
+ function makeBackDetourCandidate(A, B, from, to, obstacles, detourY, side, columnXOverride) {
9232
+ const lowIsA = A.y >= B.y;
9233
+ const low = lowIsA ? A : B;
9234
+ const lowBendY = low.y - VERTICAL_DETOUR_EXIT;
9235
+ const bandTop = Math.min(detourY, lowBendY);
9236
+ const bandBottom = Math.max(detourY, lowBendY);
9237
+ const bandObstacles = obstacles.map(nodeRect).filter((rect) => rect.y + rect.h >= bandTop && rect.y <= bandBottom);
9238
+ let columnX = columnXOverride ?? detourColumnFor([from, to], side);
9239
+ if (columnXOverride === void 0) {
9240
+ while (true) {
9241
+ const blockers = bandObstacles.filter(
9242
+ (rect) => rect.x - VERTICAL_DETOUR_GAP < columnX && columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP
9243
+ );
9244
+ if (blockers.length === 0) break;
9245
+ columnX = side === "left" ? Math.min(...blockers.map((rect) => rect.x - VERTICAL_DETOUR_GAP)) : Math.max(...blockers.map((rect) => rect.x + rect.w + VERTICAL_DETOUR_GAP));
9246
+ }
9247
+ }
9248
+ const bendAY = lowIsA ? lowBendY : detourY;
9249
+ const bendBY = lowIsA ? detourY : lowBendY;
9250
+ const vertices = [
9251
+ A,
9252
+ { x: A.x, y: bendAY },
9253
+ { x: columnX, y: bendAY },
9254
+ { x: columnX, y: bendBY },
9255
+ { x: B.x, y: bendBY },
9256
+ B
9257
+ ];
9258
+ const d = verticesToPath(vertices);
9259
+ const high = lowIsA ? B : A;
9260
+ return {
9261
+ d,
9262
+ labelX: (high.x + columnX) / 2,
9263
+ crossings: countPathObstacleCrossings(d, obstacles),
9264
+ length: vertices.slice(1).reduce((sum, vertex, index) => {
9265
+ const previous = vertices[index];
9266
+ return sum + Math.hypot(vertex.x - previous.x, vertex.y - previous.y);
9267
+ }, 0)
9268
+ };
9269
+ }
9270
+ function chooseBackDetourCandidate(A, B, from, to, obstacles, detourY) {
9271
+ const left = makeBackDetourCandidate(A, B, from, to, obstacles, detourY, "left");
9272
+ const right = makeBackDetourCandidate(A, B, from, to, obstacles, detourY, "right");
9273
+ if (left.crossings !== right.crossings) return left.crossings < right.crossings ? left : right;
9274
+ if (Math.abs(left.length - right.length) >= 0.5) return left.length < right.length ? left : right;
9275
+ const preferred = Math.abs(A.x - B.x) < 0.5 ? "right" : A.x < B.x ? "left" : "right";
9276
+ return preferred === "left" ? left : right;
9277
+ }
9110
9278
  function rectGap(a, b) {
9111
9279
  const dx = Math.max(0, a.x - (b.x + b.w), b.x - (a.x + a.w));
9112
9280
  const dy = Math.max(0, a.y - (b.y + b.h), b.y - (a.y + a.h));
@@ -9184,7 +9352,7 @@ function sidePoint(n, side) {
9184
9352
  return { x: n.cx + n.w / 2, y: n.cy };
9185
9353
  }
9186
9354
  }
9187
- function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _fromLane, _toLane, edge, labelObstacles = obstacles, fromOffset = { x: 0, y: 0 }, toOffset = { x: 0, y: 0 }, avoidRects = [], textMetrics = "proportional") {
9355
+ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, fromLane, toLane, edge, labelObstacles = obstacles, fromOffset = { x: 0, y: 0 }, toOffset = { x: 0, y: 0 }, avoidRects = [], textMetrics = "proportional") {
9188
9356
  const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub, textMetrics) : 0;
9189
9357
  const labelHalfW = labelBoxW / 2;
9190
9358
  const labelInit = labelInitClearance(edge);
@@ -9554,23 +9722,52 @@ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _from
9554
9722
  };
9555
9723
  }
9556
9724
  if (topTopPair) {
9557
- const detourY = Math.min(A.y, B.y) - 100;
9725
+ const stagePoint = (point2, node, lane) => lane?.stage === void 0 ? point2 : {
9726
+ x: Math.min(node.cx + node.w / 2 - 24, lane.x + lane.width - 24),
9727
+ y: point2.y
9728
+ };
9729
+ const backA = stagePoint(A, from, fromLane);
9730
+ const backB = stagePoint(B, to, toLane);
9731
+ const stageTops = [fromLane, toLane].filter((lane) => lane?.stage !== void 0).map((lane) => lane.y);
9732
+ const detourY = Math.min(
9733
+ Math.min(backA.y, backB.y) - 100,
9734
+ ...stageTops.map((top) => top - VERTICAL_DETOUR_GAP)
9735
+ );
9558
9736
  const corner = 14;
9559
- const xDir = B.x > A.x ? 1 : -1;
9560
- return {
9561
- d: `M ${A.x} ${A.y}
9562
- L ${A.x} ${detourY + corner}
9563
- Q ${A.x} ${detourY}, ${A.x + xDir * corner} ${detourY}
9564
- L ${B.x - xDir * corner} ${detourY}
9565
- Q ${B.x} ${detourY}, ${B.x} ${detourY + corner}
9566
- L ${B.x} ${B.y}`,
9567
- labelX: (A.x + B.x) / 2,
9737
+ const xDir = backB.x > backA.x ? 1 : -1;
9738
+ const existing = {
9739
+ d: `M ${backA.x} ${backA.y}
9740
+ L ${backA.x} ${detourY + corner}
9741
+ Q ${backA.x} ${detourY}, ${backA.x + xDir * corner} ${detourY}
9742
+ L ${backB.x - xDir * corner} ${detourY}
9743
+ Q ${backB.x} ${detourY}, ${backB.x} ${detourY + corner}
9744
+ L ${backB.x} ${backB.y}`,
9745
+ labelX: (backA.x + backB.x) / 2,
9568
9746
  // CAR-468 SSOT ... 旧 detourY - 30 magic number を LABEL_INIT_CLEARANCE (26、 CAR-570 で 34 → 26 縮小) に置換。
9569
9747
  // CAR-518 SSOT ... dotted-flow は 62 world (marker glow radius 分拡張) を labelInit で参照。
9570
9748
  // top-top detour の水平 segment 上方 labelInit world = spec 準拠距離。
9571
9749
  labelY: detourY - labelInit,
9572
9750
  labelAnchor: "middle"
9573
9751
  };
9752
+ const existingCrossings = countPathObstacleCrossings(existing.d, obstacles);
9753
+ if (existingCrossings === 0) return existing;
9754
+ const candidate = from.lane === to.lane && fromLane?.stage !== void 0 ? makeBackDetourCandidate(
9755
+ backA,
9756
+ backB,
9757
+ from,
9758
+ to,
9759
+ obstacles,
9760
+ detourY,
9761
+ "right",
9762
+ fromLane.x + fromLane.width
9763
+ ) : chooseBackDetourCandidate(backA, backB, from, to, obstacles, detourY);
9764
+ if (candidate.crossings >= existingCrossings) return existing;
9765
+ return {
9766
+ d: candidate.d,
9767
+ labelX: candidate.labelX,
9768
+ labelY: detourY - labelInit,
9769
+ labelAnchor: "middle"
9770
+ };
9574
9771
  }
9575
9772
  const midX = fromSide === "left" || fromSide === "right" ? B.x : A.x;
9576
9773
  const midY = fromSide === "top" || fromSide === "bottom" ? B.y : A.y;
@@ -10497,12 +10694,56 @@ function \u76F4\u524D\u306E\u6298\u308C\u3054\u3068\u305A\u3089\u3059(segs, \u6A
10497
10694
  return parts.join(" ");
10498
10695
  }
10499
10696
 
10697
+ // src/layout/legend.ts
10698
+ var LEGEND_FONT_SIZE = 17;
10699
+ var LEGEND_MARK_SCALE = LEGEND_FONT_SIZE / 21;
10700
+ var LEGEND_MARK_REFERENCE_WIDTH = 46;
10701
+ var LEGEND_MARK_WIDTH = LEGEND_MARK_REFERENCE_WIDTH * LEGEND_MARK_SCALE;
10702
+ var LEGEND_MARK_TEXT_GAP = 12 * LEGEND_MARK_SCALE;
10703
+ var LEGEND_ITEM_GAP_RATIO = 0.07;
10704
+ var LEGEND_BASELINE_GAP = 32;
10705
+ var LEGEND_BOTTOM_EXTENT = 10;
10706
+ function layoutLegend(items, viewBox, textMetrics, nodes) {
10707
+ const renderedNodes = nodes.filter(isRenderedNode);
10708
+ const startX = renderedNodes.length > 0 ? Math.min(...renderedNodes.map((node) => node.cx - node.w / 2)) : viewBox.x + VIEW_PAD;
10709
+ const diagramRight = renderedNodes.length > 0 ? Math.max(...renderedNodes.map((node) => node.cx + node.w / 2)) : viewBox.x + viewBox.w - VIEW_PAD;
10710
+ const itemGap = (diagramRight - startX) * LEGEND_ITEM_GAP_RATIO;
10711
+ const y = viewBox.y + viewBox.h - VIEW_PAD + LEGEND_BASELINE_GAP;
10712
+ let x = startX;
10713
+ const legend = items.map((item) => {
10714
+ const laid = { ...item, x, y };
10715
+ x += LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP + textWidth(item.text, LEGEND_FONT_SIZE, 0, textMetrics) + itemGap;
10716
+ return laid;
10717
+ });
10718
+ const right = x - itemGap;
10719
+ const bottom = y + LEGEND_BOTTOM_EXTENT + VIEW_PAD;
10720
+ return {
10721
+ legend,
10722
+ viewBox: {
10723
+ ...viewBox,
10724
+ w: Math.max(viewBox.w, right + VIEW_PAD - viewBox.x),
10725
+ h: Math.max(viewBox.h, bottom - viewBox.y)
10726
+ }
10727
+ };
10728
+ }
10729
+
10500
10730
  // src/layout/lanes.ts
10501
10731
  var LANE_POSITION_EPSILON = 1e-9;
10732
+ function equalizeStageColumns(lanes, nodes = []) {
10733
+ const stages = lanes.filter((lane) => lane.stage !== void 0);
10734
+ if (stages.length === 0) return lanes;
10735
+ const top = Math.min(...stages.map((lane) => lane.y));
10736
+ const stageIds = new Set(stages.map((lane) => lane.id));
10737
+ const nodeBottoms = nodes.filter((node) => stageIds.has(node.lane)).map((node) => node.cy + node.h / 2 + CONTAIN_PAD);
10738
+ const bottom = Math.max(...stages.map((lane) => lane.y + lane.height), ...nodeBottoms);
10739
+ return lanes.map(
10740
+ (lane) => lane.stage === void 0 ? lane : { ...lane, y: top, height: bottom - top }
10741
+ );
10742
+ }
10502
10743
  function layoutLanes(diag) {
10503
10744
  const laneGap = diag.viewport?.laneGap ?? diag.viewport?.gap ?? DEFAULT_LANE_GAP;
10504
10745
  let cursorX = 0;
10505
- return diag.lanes.map((lane) => {
10746
+ const lanes = diag.lanes.map((lane, index) => {
10506
10747
  if (lane.posX !== void 0 && lane.posY !== void 0) {
10507
10748
  return {
10508
10749
  ...lane,
@@ -10513,7 +10754,9 @@ function layoutLanes(diag) {
10513
10754
  };
10514
10755
  }
10515
10756
  const resolvedX = lane.x ?? cursorX;
10516
- cursorX = resolvedX + lane.width + laneGap;
10757
+ const nextLane = diag.lanes[index + 1];
10758
+ const gapAfter = lane.stage !== void 0 && nextLane?.stage !== void 0 ? 0 : laneGap;
10759
+ cursorX = resolvedX + lane.width + gapAfter;
10517
10760
  return {
10518
10761
  ...lane,
10519
10762
  x: resolvedX,
@@ -10521,6 +10764,7 @@ function layoutLanes(diag) {
10521
10764
  height: LANE_LABEL_H
10522
10765
  };
10523
10766
  });
10767
+ return equalizeStageColumns(lanes);
10524
10768
  }
10525
10769
  function expandLanesForNodes(lanes, nodes) {
10526
10770
  const targetNodeGap = targetNodeNodeClearance();
@@ -10549,7 +10793,8 @@ function expandLanesForNodes(lanes, nodes) {
10549
10793
  if (lane.posX !== void 0 && lane.posY !== void 0) continue;
10550
10794
  const inside = nodes.filter((n) => n.lane === lane.id);
10551
10795
  const maxNodeW = inside.length > 0 ? Math.max(...inside.map((n) => n.w)) : 0;
10552
- const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2);
10796
+ const stageMinWidth = lane.stage === void 0 ? 0 : maxNodeW + CONTAIN_PAD + STAGE_COLUMN_GAP;
10797
+ const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2, stageMinWidth);
10553
10798
  widthOf.set(lane.id, newWidth);
10554
10799
  deltaOf.set(lane.id, newWidth - lane.width);
10555
10800
  }
@@ -10574,6 +10819,34 @@ function expandLaneGapsForEdgeLabels(lanes, nodes, edges, textMetrics = "proport
10574
10819
  if (lanes.length < 2) return lanes;
10575
10820
  const layoutedLanes = lanes.filter((l) => !(l.posX !== void 0 && l.posY !== void 0));
10576
10821
  if (layoutedLanes.length < 2) return lanes;
10822
+ if (layoutedLanes.length === lanes.length && layoutedLanes.every((lane) => lane.stage !== void 0)) {
10823
+ const sortedStages = [...layoutedLanes].sort((a, b) => a.x - b.x);
10824
+ const stageIndexById = new Map(sortedStages.map((lane, index) => [lane.id, index]));
10825
+ const nodeLaneById = new Map(nodes.map((node) => [node.id, node.lane]));
10826
+ const requiredSurfaceGaps = new Array(sortedStages.length - 1).fill(STAGE_COLUMN_GAP);
10827
+ for (const edge of edges) {
10828
+ if (edge.routing !== "curve" || !edge.label && !edge.sub) continue;
10829
+ const fromIndex = stageIndexById.get(nodeLaneById.get(edge.from) ?? "");
10830
+ const toIndex = stageIndexById.get(nodeLaneById.get(edge.to) ?? "");
10831
+ if (fromIndex === void 0 || toIndex === void 0 || fromIndex === toIndex) continue;
10832
+ const lo = Math.min(fromIndex, toIndex);
10833
+ const hi = Math.max(fromIndex, toIndex);
10834
+ const labelWidth = computeLabelBoxW(edge.label, edge.sub, textMetrics);
10835
+ for (let pair = lo; pair < hi; pair++) {
10836
+ requiredSurfaceGaps[pair] = Math.max(requiredSurfaceGaps[pair], labelWidth);
10837
+ }
10838
+ }
10839
+ const shiftedXById2 = /* @__PURE__ */ new Map([[sortedStages[0].id, sortedStages[0].x]]);
10840
+ for (let index = 1; index < sortedStages.length; index++) {
10841
+ const previous = sortedStages[index - 1];
10842
+ const lane = sortedStages[index];
10843
+ const previousX = shiftedXById2.get(previous.id);
10844
+ const authoredGap = Math.max(0, lane.x - (previous.x + previous.width));
10845
+ const laneGap = Math.max(authoredGap, requiredSurfaceGaps[index - 1] - STAGE_COLUMN_GAP);
10846
+ shiftedXById2.set(lane.id, previousX + previous.width + laneGap);
10847
+ }
10848
+ return lanes.map((lane) => ({ ...lane, x: shiftedXById2.get(lane.id) ?? lane.x }));
10849
+ }
10577
10850
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
10578
10851
  const sortedByX = [...layoutedLanes].sort((a, b) => a.x - b.x);
10579
10852
  const laneIndexById = new Map(sortedByX.map((lane, i) => [lane.id, i]));
@@ -10645,7 +10918,7 @@ function recomputeContainedLanes(lanes, nodes) {
10645
10918
  lifelineBottom = Math.max(lifelineBottom, bottom);
10646
10919
  }
10647
10920
  }
10648
- return lanes.map((lane) => {
10921
+ const contained = lanes.map((lane) => {
10649
10922
  if (lane.lifeline) {
10650
10923
  return {
10651
10924
  ...lane,
@@ -10663,6 +10936,7 @@ function recomputeContainedLanes(lanes, nodes) {
10663
10936
  height: bottom - top + CONTAIN_PAD * 2 + LANE_LABEL_H
10664
10937
  };
10665
10938
  });
10939
+ return equalizeStageColumns(contained, nodes);
10666
10940
  }
10667
10941
 
10668
10942
  // src/layout/head-label.ts
@@ -10812,11 +11086,15 @@ function requiredStackGapForEdgeLabels(diag, allStacks) {
10812
11086
  }
10813
11087
  function layoutNodes(diag, lanes, textMetrics) {
10814
11088
  const laneMap = new Map(lanes.map((l) => [l.id, l]));
11089
+ const metroDecisionIds = new Set(
11090
+ diag.edges.filter((edge) => edge.routing === "metro").flatMap((edge) => [edge.from, edge.to]).filter((id) => diag.nodes.some((node) => node.id === id && node.kind === "decision"))
11091
+ );
11092
+ const nodeSize = (node) => metroDecisionIds.has(node.id) ? { w: METRO_DECISION_WIDTH, h: METRO_DECISION_HEIGHT } : NODE_SIZE[node.kind];
10815
11093
  const allStacks = Array.from(new Set(diag.nodes.map((n) => n.stack))).sort((a, b) => a - b);
10816
11094
  const rowH = /* @__PURE__ */ new Map();
10817
11095
  for (const stack of allStacks) {
10818
11096
  const nodesAtStack = diag.nodes.filter((n) => n.stack === stack);
10819
- const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, NODE_SIZE[n.kind].h)));
11097
+ const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, nodeSize(n).h)));
10820
11098
  rowH.set(stack, h);
10821
11099
  }
10822
11100
  const rowCy = /* @__PURE__ */ new Map();
@@ -10852,7 +11130,7 @@ function layoutNodes(diag, lanes, textMetrics) {
10852
11130
  if (!lane) {
10853
11131
  throw new Error(`cdl layout: node "${n.id}" \u306E lane "${n.lane}" \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u306A\u3044`);
10854
11132
  }
10855
- const size = NODE_SIZE[n.kind];
11133
+ const size = nodeSize(n);
10856
11134
  if (n.posX !== void 0 && n.posY !== void 0) {
10857
11135
  const w2 = n.posW ?? n.w ?? autoRowsWidth(n, size.w, textMetrics);
10858
11136
  const h2 = n.posH ?? n.h ?? rowH.get(n.stack) ?? size.h;
@@ -10975,6 +11253,9 @@ function dropUnsafeLaneDx(dxByLane, nodes) {
10975
11253
  function applyRowLevelNodeShifts(nodes, lanes, nodeShifts) {
10976
11254
  const dyByStack = aggregateRowDy(nodes, nodeShifts);
10977
11255
  const dxByLane = dropUnsafeLaneDx(aggregateLaneDx(nodes, nodeShifts), nodes);
11256
+ for (const lane of lanes) {
11257
+ if (lane.stage !== void 0) dxByLane.delete(lane.id);
11258
+ }
10978
11259
  let moved = false;
10979
11260
  const outNodes = nodes.map((n) => {
10980
11261
  const dx = dxByLane.get(n.lane) ?? 0;
@@ -11094,7 +11375,15 @@ function layout(diag) {
11094
11375
  );
11095
11376
  }
11096
11377
  }
11097
- const viewBox = computeViewBox(containedLanesFinal, nodesFinal, edges, bboxes, diag.viewport);
11378
+ const contentViewBox = computeViewBox(
11379
+ containedLanesFinal,
11380
+ nodesFinal,
11381
+ edges,
11382
+ bboxes,
11383
+ diag.viewport
11384
+ );
11385
+ const legendLayout = diag.legend?.length ? layoutLegend(diag.legend, contentViewBox, textMetrics, nodesFinal) : void 0;
11386
+ const viewBox = legendLayout?.viewBox ?? contentViewBox;
11098
11387
  return {
11099
11388
  id: diag.id,
11100
11389
  topic: diag.topic,
@@ -11121,6 +11410,7 @@ function layout(diag) {
11121
11410
  ...textMetrics === "monospace" ? { textMetrics: "monospace" } : {},
11122
11411
  // 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
11123
11412
  ...diag.type ? { type: diag.type } : {},
11413
+ ...legendLayout ? { legend: legendLayout.legend } : {},
11124
11414
  bboxes,
11125
11415
  collisions,
11126
11416
  nearCollisions,
@@ -11309,6 +11599,11 @@ function validate(diag) {
11309
11599
  \u2192 \u4FEE\u6B63\u4F8B: .lane("${n.lane}", { x: 0, width: 400 }) \u3092 node \u5BA3\u8A00\u524D\u306B\u8FFD\u52A0`
11310
11600
  );
11311
11601
  }
11602
+ if (n.leaderTo !== void 0 && !nodeIds.has(n.leaderTo)) {
11603
+ errors.push(
11604
+ `[cdl] unknown-ref: node "${n.id}" \u306E leaderTo "${n.leaderTo}" \u304C node \u306B\u5B58\u5728\u3057\u307E\u305B\u3093${suggest(n.leaderTo, nodeIds, "node")}`
11605
+ );
11606
+ }
11312
11607
  }
11313
11608
  for (const e of diag.edges) {
11314
11609
  if (!nodeIds.has(e.from)) {
@@ -11927,6 +12222,10 @@ function uniformLifelineEndY(lanes, nodes, starts = lifelineStartYs(nodes)) {
11927
12222
 
11928
12223
  // src/render/edge-head.ts
11929
12224
  var EDGE_HEAD_MUTED_MARKER_ID = "cdl-arrow-muted";
12225
+ var METRO_RETURN_ARROW_D = "M 21 0 L 4 9.5 L 4 -9.5 Z";
12226
+ function metroReturnArrowMarkerId(tone) {
12227
+ return `cdl-metro-return-arrow-${tone}`;
12228
+ }
11930
12229
  var EDGE_HEAD_SHAPE = {
11931
12230
  // 何も描かない端。 marker 自体を出さないので `d` は読まれない
11932
12231
  none: { d: "", w: 10, h: 10, refX: 0, filled: false },
@@ -11981,6 +12280,7 @@ function \u7B49\u5E45\u306E\u5B57(text, fontSize) {
11981
12280
  });
11982
12281
  return out;
11983
12282
  }
12283
+ var METRO_PENDING_ATTRIBUTE = "data-cdl-pending";
11984
12284
  function defaultEdgeWidth(active) {
11985
12285
  return active ? 2.5 : 1.25;
11986
12286
  }
@@ -11999,12 +12299,14 @@ function CdlEdgeView({
11999
12299
  drawing,
12000
12300
  progress,
12001
12301
  stateValues,
12002
- flowing = false
12302
+ flowing = false,
12303
+ pending = false
12003
12304
  }) {
12004
12305
  const color = edgeColor(edge);
12005
- const marker = edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
12006
- const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
12007
12306
  const style = edge.style ?? "solid";
12307
+ const isMetroReturn = edge.metroReturn === true;
12308
+ const marker = isMetroReturn ? metroReturnArrowMarkerId(edge.tone) : edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
12309
+ const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
12008
12310
  const isDotted = style === "dotted-flow";
12009
12311
  const isPassThrough = isDotted && Boolean(edge.dThrough);
12010
12312
  const lineDotsD = isDotted ? shrinkPathEnd(edge.d, 12) : edge.d;
@@ -12014,6 +12316,7 @@ function CdlEdgeView({
12014
12316
  return /* @__PURE__ */ jsxRuntime.jsxs(
12015
12317
  "g",
12016
12318
  {
12319
+ ...pending ? { [METRO_PENDING_ATTRIBUTE]: "true" } : {},
12017
12320
  "data-cdl-edge": edge.id,
12018
12321
  "data-cdl-routing": edge.routing === "metro" ? "metro" : void 0,
12019
12322
  "data-cdl-from": edge.from,
@@ -12039,11 +12342,12 @@ function CdlEdgeView({
12039
12342
  fill: "none",
12040
12343
  stroke: color,
12041
12344
  strokeOpacity: active ? 0.95 : 0.9,
12042
- strokeWidth: active ? 5 : 4,
12345
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH : active ? 5 : 4,
12043
12346
  strokeDasharray: "0 18",
12044
12347
  strokeLinecap: "round",
12045
12348
  style: {
12046
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
12349
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
12350
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH_CSS : void 0
12047
12351
  }
12048
12352
  }
12049
12353
  ),
@@ -12098,7 +12402,9 @@ function SolidEdgePath({
12098
12402
  const boundWidth = edge.widthBind && stateValues ? resolveNumericAttr(edge.widthBind, stateValues, defaultWidth) : defaultWidth;
12099
12403
  const boundStroke = edge.strokeBind && stateValues ? interpolate(edge.strokeBind, stateValues) : color;
12100
12404
  const boundDashOffset = edge.dashOffsetBind && stateValues ? resolveNumericAttr(edge.dashOffsetBind, stateValues, 0) : void 0;
12101
- const dashArray = dashed ? "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
12405
+ const strokeWidth = edge.routing === "metro" && edge.widthBind === void 0 ? edge.metroReturn ? 6 : METRO_LINE_WIDTH : boundWidth;
12406
+ const strokeWidthCss = edge.routing === "metro" && !edge.metroReturn ? edge.widthBind === void 0 ? METRO_LINE_WIDTH_CSS : `var(--cdl-metro-line-width, ${strokeWidth}px)` : void 0;
12407
+ const dashArray = dashed ? edge.metroReturn ? "0 9.6" : "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
12102
12408
  return /* @__PURE__ */ jsxRuntime.jsx(
12103
12409
  "path",
12104
12410
  {
@@ -12107,14 +12413,15 @@ function SolidEdgePath({
12107
12413
  fill: "none",
12108
12414
  stroke: boundStroke,
12109
12415
  strokeOpacity: active ? 0.95 : 0.9,
12110
- strokeWidth: boundWidth,
12416
+ strokeWidth,
12111
12417
  strokeDasharray: dashArray,
12112
12418
  strokeDashoffset: boundDashOffset,
12113
12419
  strokeLinecap: "round",
12114
12420
  markerEnd: marker === void 0 ? void 0 : `url(#${marker})`,
12115
12421
  markerStart: tailMarker === void 0 ? void 0 : `url(#${tailMarker})`,
12116
12422
  style: {
12117
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
12423
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
12424
+ strokeWidth: strokeWidthCss
12118
12425
  }
12119
12426
  }
12120
12427
  );
@@ -12411,7 +12718,11 @@ function \u5272\u308B\u5019\u88DC(text) {
12411
12718
  function \u5165\u3063\u3066\u3044\u308B([\u5DE6, \u53F3]) {
12412
12719
  return \u5DE6 !== "" && \u53F3 !== "";
12413
12720
  }
12414
- function CardNode({ node, active }) {
12721
+ function CardNode({
12722
+ node,
12723
+ active,
12724
+ compactTitleInset
12725
+ }) {
12415
12726
  const accent = nodeAccent(node, "var(--cdl-tone-accent, #2d6a8f)");
12416
12727
  const x = node.cx - node.w / 2;
12417
12728
  const y = node.cy - node.h / 2;
@@ -12429,12 +12740,12 @@ function CardNode({ node, active }) {
12429
12740
  });
12430
12741
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
12431
12742
  const titleY = compact ? \u5C0F\u578B\u306B\u547C\u3073\u540D ? node.h / 2 - 2 : node.h / 2 + 8 : 86 - \u305A\u3089\u3057;
12432
- const titleX = compact ? node.w / 2 : 26;
12433
- const paddingX = compact ? 16 : 52;
12743
+ const titleX = compact ? compactTitleInset ?? node.w / 2 : 26;
12744
+ const paddingX = compact ? (compactTitleInset ?? 8) * 2 : 52;
12434
12745
  const availableW = node.w - paddingX;
12435
12746
  const initialTitleSize = compact ? 20 : 24;
12436
12747
  const titleSize = fitTextSize(node.title, availableW, initialTitleSize, 11);
12437
- const textAnchor = compact ? "middle" : "start";
12748
+ const textAnchor = compact && compactTitleInset === void 0 ? "middle" : "start";
12438
12749
  const subtitleY = 124 - \u305A\u3089\u3057;
12439
12750
  const \u6700\u5927\u306E\u884C\u9001\u308A = SUBTITLE_SIZE * \u884C\u9001\u308A\u306E\u500D\u7387;
12440
12751
  const \u4E8C\u884C\u304C\u5165\u308B = !compact && subtitleY - \u6700\u5927\u306E\u884C\u9001\u308A / 2 - SUBTITLE_SIZE * ASCENT > titleY + titleSize * DESCENT && subtitleY + \u6700\u5927\u306E\u884C\u9001\u308A / 2 + SUBTITLE_SIZE * DESCENT <= node.h - 4;
@@ -13194,7 +13505,9 @@ function GenericNode({
13194
13505
  value,
13195
13506
  stateValues,
13196
13507
  \u5024\u306E\u540D\u524D,
13197
- textMetrics = "proportional"
13508
+ textMetrics = "proportional",
13509
+ showKindIcon = true,
13510
+ metroDecision = false
13198
13511
  }) {
13199
13512
  const base = STYLES[node.kind] ?? FALLBACK;
13200
13513
  const toned = toneColor3(node.tone);
@@ -13225,14 +13538,14 @@ function GenericNode({
13225
13538
  if (shape === "diamond") {
13226
13539
  const cx = w / 2;
13227
13540
  const cy = h / 2;
13228
- const pad = 8;
13541
+ const pad = metroDecision ? 0 : 8;
13229
13542
  return /* @__PURE__ */ jsxRuntime.jsx(
13230
13543
  "path",
13231
13544
  {
13232
13545
  d: `M ${cx} ${pad} L ${w - pad} ${cy} L ${cx} ${h - pad} L ${pad} ${cy} Z`,
13233
13546
  fill: "var(--cdl-node-fill, #ffffff)",
13234
13547
  stroke: active ? style.accent : "var(--cdl-divider, #d4d4d8)",
13235
- strokeWidth: active ? 3 : 1.25
13548
+ strokeWidth: metroDecision ? 1.5 : active ? 3 : 1.25
13236
13549
  }
13237
13550
  );
13238
13551
  }
@@ -13296,11 +13609,11 @@ function GenericNode({
13296
13609
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
13297
13610
  const \u7D75\u306E\u82AF = centered && STYLES[node.kind]?.shape === "cloud" ? h * CLOUD_ART_CENTER_RISE : 0;
13298
13611
  const eyebrowY = centered ? h / 2 - 26 - \u7D75\u306E\u82AF : 32 + cylinderShift + \u4F59\u308A;
13299
- const titleY = genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
13612
+ const titleY = metroDecision ? h / 2 + 9 : genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
13300
13613
  const subtitleY = centered ? h / 2 + 28 - \u7D75\u306E\u82AF : 96 + cylinderShift - \u305A\u3089\u3057;
13301
13614
  const iconX = centered ? w / 2 + 40 : w - 48;
13302
13615
  const iconY = centered ? h / 2 - 44 - \u7D75\u306E\u82AF : 18 + cylinderShift;
13303
- const dominantBaseline = centered ? "middle" : void 0;
13616
+ const dominantBaseline = centered && !metroDecision ? "middle" : void 0;
13304
13617
  return /* @__PURE__ */ jsxRuntime.jsxs(
13305
13618
  "g",
13306
13619
  {
@@ -13309,12 +13622,12 @@ function GenericNode({
13309
13622
  "data-cdl-look": style.look,
13310
13623
  children: [
13311
13624
  shapeEl,
13312
- /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
13625
+ showKindIcon && /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
13313
13626
  /* @__PURE__ */ jsxRuntime.jsx("rect", { width: 24, height: 24, fill: "transparent" }),
13314
13627
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: style.icon, fill: style.eyebrowColor, opacity: 0.7, transform: "scale(1)" })
13315
13628
  ] }),
13316
13629
  node.eyebrow && /* @__PURE__ */ jsxRuntime.jsx("text", { x: textX, y: eyebrowY, fontSize: 17, fontWeight: 700, letterSpacing: 1.5, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : style.eyebrowColor, textAnchor, dominantBaseline, children: node.eyebrow }),
13317
- /* @__PURE__ */ jsxRuntime.jsx("text", { "data-cdl-role": "node-label", x: textX, y: titleY, fontSize: 20, fontWeight: 700, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text, #1a1f2a)", textAnchor, dominantBaseline, style: node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : void 0, children: node.kind === "function" || node.kind === "api" ? \u7B49\u5E45\u306E\u5B57(node.title, 20) : node.title }),
13630
+ /* @__PURE__ */ jsxRuntime.jsx("text", { "data-cdl-role": "node-label", x: textX, y: titleY, fontSize: metroDecision ? 26 : 20, fontWeight: 700, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text, #1a1f2a)", textAnchor, dominantBaseline, style: node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : void 0, children: node.kind === "function" || node.kind === "api" ? \u7B49\u5E45\u306E\u5B57(node.title, 20) : node.title }),
13318
13631
  node.subtitle && /* @__PURE__ */ jsxRuntime.jsx("text", { x: textX, y: subtitleY, fontSize: 17, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text-dim, #5a6270)", textAnchor, dominantBaseline, style: { fontFamily: "'JetBrains Mono', monospace" }, children: \u7B49\u5E45\u306E\u5B57(node.subtitle, 17) }),
13319
13632
  resolvedValue && /* @__PURE__ */ jsxRuntime.jsx(
13320
13633
  "text",
@@ -13363,50 +13676,176 @@ function GenericNode({
13363
13676
  var MARK_INNER_RATIO = 0.56;
13364
13677
  var RING_WIDTH_ACTIVE = 6;
13365
13678
  var RING_WIDTH_IDLE = 4;
13679
+ var METRO_START_RADIUS = 17;
13680
+ var METRO_END_RADIUS = 19;
13681
+ var METRO_END_RING_WIDTH = 3.5;
13682
+ var METRO_END_INNER_RADIUS = 10;
13366
13683
  var MARK_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13367
13684
  function markRadius(node) {
13368
13685
  return Math.min(node.w, node.h) / 2;
13369
13686
  }
13370
- function MarkStartNode({ node }) {
13687
+ function MarkStartNode({
13688
+ node,
13689
+ metro = false
13690
+ }) {
13371
13691
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
13372
- return /* @__PURE__ */ jsxRuntime.jsx("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: /* @__PURE__ */ jsxRuntime.jsx(
13373
- "circle",
13692
+ return /* @__PURE__ */ jsxRuntime.jsx(
13693
+ "g",
13374
13694
  {
13375
- "data-cdl-role": "node-body",
13376
- "data-cdl-mark": "start",
13377
- cx: node.w / 2,
13378
- cy: node.h / 2,
13379
- r: markRadius(node),
13380
- fill: \u8272
13695
+ "data-cdl-role": metro ? "mark-start" : void 0,
13696
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
13697
+ children: /* @__PURE__ */ jsxRuntime.jsx(
13698
+ "circle",
13699
+ {
13700
+ "data-cdl-role": "node-body",
13701
+ "data-cdl-mark": "start",
13702
+ cx: node.w / 2,
13703
+ cy: node.h / 2,
13704
+ r: metro ? METRO_START_RADIUS : markRadius(node),
13705
+ fill: \u8272
13706
+ }
13707
+ )
13381
13708
  }
13382
- ) });
13709
+ );
13383
13710
  }
13384
- function MarkEndNode({ node, active }) {
13711
+ function MarkEndNode({
13712
+ node,
13713
+ active,
13714
+ metro = false
13715
+ }) {
13385
13716
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
13386
- const r = markRadius(node);
13387
- const \u8F2A\u306E\u592A\u3055 = active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
13388
- return /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: [
13717
+ const r = metro ? METRO_END_RADIUS : markRadius(node);
13718
+ const \u8F2A\u306E\u592A\u3055 = metro ? METRO_END_RING_WIDTH : active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
13719
+ return /* @__PURE__ */ jsxRuntime.jsxs(
13720
+ "g",
13721
+ {
13722
+ "data-cdl-role": metro ? "mark-end" : void 0,
13723
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
13724
+ children: [
13725
+ /* @__PURE__ */ jsxRuntime.jsx(
13726
+ "circle",
13727
+ {
13728
+ "data-cdl-role": "node-body",
13729
+ "data-cdl-mark": "end",
13730
+ cx: node.w / 2,
13731
+ cy: node.h / 2,
13732
+ r: metro ? r : Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
13733
+ fill: "var(--cdl-node-fill, #ffffff)",
13734
+ stroke: \u8272,
13735
+ strokeWidth: \u8F2A\u306E\u592A\u3055
13736
+ }
13737
+ ),
13738
+ /* @__PURE__ */ jsxRuntime.jsx(
13739
+ "circle",
13740
+ {
13741
+ "data-cdl-role": "node-inner",
13742
+ cx: node.w / 2,
13743
+ cy: node.h / 2,
13744
+ r: metro ? METRO_END_INNER_RADIUS : Math.max(0, r * MARK_INNER_RATIO),
13745
+ fill: \u8272
13746
+ }
13747
+ )
13748
+ ]
13749
+ }
13750
+ );
13751
+ }
13752
+ var METRO_STATION_RADIUS = 16;
13753
+ var METRO_STATION_STROKE_WIDTH = 6;
13754
+ var METRO_STATION_RADIUS_CSS = `var(--cdl-metro-station-radius, ${METRO_STATION_RADIUS}px)`;
13755
+ var METRO_STATION_STROKE_WIDTH_CSS = `var(--cdl-metro-station-stroke-width, ${METRO_STATION_STROKE_WIDTH}px)`;
13756
+ var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13757
+ var STATION_NAME_FONT_SIZE = 25;
13758
+ var STATION_NAME_GAP = 18;
13759
+ var STATION_NAME_RIGHT_GAP = 32;
13760
+ var STATION_NAME_RIGHT_BASELINE = 9;
13761
+ var STATION_SUBTITLE_FONT_SIZE = 18;
13762
+ var STATION_SUBTITLE_GAP = 22;
13763
+ function StationNode({
13764
+ node,
13765
+ nameRight = false
13766
+ }) {
13767
+ const name = node.title.trim();
13768
+ const subtitle = node.subtitle?.trim() ?? "";
13769
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
13389
13770
  /* @__PURE__ */ jsxRuntime.jsx(
13390
13771
  "circle",
13391
13772
  {
13392
13773
  "data-cdl-role": "node-body",
13393
- "data-cdl-mark": "end",
13394
- cx: node.w / 2,
13395
- cy: node.h / 2,
13396
- r: Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
13774
+ "data-cdl-mark": "station",
13775
+ cx: node.cx,
13776
+ cy: node.cy,
13777
+ r: METRO_STATION_RADIUS,
13397
13778
  fill: "var(--cdl-node-fill, #ffffff)",
13398
- stroke: \u8272,
13399
- strokeWidth: \u8F2A\u306E\u592A\u3055
13779
+ stroke: nodeAccent(node, STATION_DEFAULT),
13780
+ strokeWidth: METRO_STATION_STROKE_WIDTH,
13781
+ style: { r: METRO_STATION_RADIUS_CSS, strokeWidth: METRO_STATION_STROKE_WIDTH_CSS }
13400
13782
  }
13401
13783
  ),
13784
+ name !== "" && /* @__PURE__ */ jsxRuntime.jsx(
13785
+ "text",
13786
+ {
13787
+ "data-cdl-role": "shape-name",
13788
+ x: nameRight ? node.cx + STATION_NAME_RIGHT_GAP : node.cx,
13789
+ y: nameRight ? node.cy + STATION_NAME_RIGHT_BASELINE : node.cy - METRO_STATION_RADIUS - STATION_NAME_GAP,
13790
+ textAnchor: nameRight ? "start" : "middle",
13791
+ fontSize: STATION_NAME_FONT_SIZE,
13792
+ fontWeight: 700,
13793
+ fill: "var(--cdl-text, #23201b)",
13794
+ stroke: "var(--cdl-node-fill, #ffffff)",
13795
+ strokeWidth: 8,
13796
+ strokeLinejoin: "round",
13797
+ paintOrder: "stroke",
13798
+ children: name
13799
+ }
13800
+ ),
13801
+ subtitle !== "" && /* @__PURE__ */ jsxRuntime.jsx(
13802
+ "text",
13803
+ {
13804
+ "data-cdl-role": "shape-value",
13805
+ x: node.cx,
13806
+ y: node.cy + METRO_STATION_RADIUS + STATION_SUBTITLE_GAP,
13807
+ textAnchor: "middle",
13808
+ fontSize: STATION_SUBTITLE_FONT_SIZE,
13809
+ fill: "var(--cdl-text-dim, #5a6270)",
13810
+ children: subtitle
13811
+ }
13812
+ )
13813
+ ] });
13814
+ }
13815
+ function TimelineNumberNode({ node }) {
13816
+ const radius = Math.max(0, Math.min(node.w, node.h) / 2 - TIMELINE_NUMBER_STROKE / 2);
13817
+ const fontSize = fitTextSize(
13818
+ node.title,
13819
+ titleUsableWidth(node.kind, node.w, node.h),
13820
+ 18,
13821
+ MIN_TEXT_SIZE
13822
+ );
13823
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13402
13824
  /* @__PURE__ */ jsxRuntime.jsx(
13403
13825
  "circle",
13404
13826
  {
13405
- "data-cdl-role": "node-inner",
13406
- cx: node.w / 2,
13407
- cy: node.h / 2,
13408
- r: Math.max(0, r * MARK_INNER_RATIO),
13409
- fill: \u8272
13827
+ "data-cdl-role": "node-body",
13828
+ "data-cdl-mark": "timeline-number",
13829
+ cx: node.cx,
13830
+ cy: node.cy,
13831
+ r: radius,
13832
+ fill: "var(--cdl-node-fill, #ffffff)",
13833
+ stroke: nodeAccent(node, STATION_DEFAULT),
13834
+ strokeWidth: TIMELINE_NUMBER_STROKE
13835
+ }
13836
+ ),
13837
+ /* @__PURE__ */ jsxRuntime.jsx(
13838
+ "text",
13839
+ {
13840
+ "data-cdl-role": "node-label",
13841
+ x: node.cx,
13842
+ y: node.cy,
13843
+ textAnchor: "middle",
13844
+ dominantBaseline: "central",
13845
+ fontSize,
13846
+ fontWeight: 700,
13847
+ fill: "var(--cdl-text, #1a1f2a)",
13848
+ children: node.title
13410
13849
  }
13411
13850
  )
13412
13851
  ] });
@@ -13738,27 +14177,6 @@ function interpolateColor(template, stateValues, fallback) {
13738
14177
  if (!template) return fallback;
13739
14178
  return interpolate(template, stateValues);
13740
14179
  }
13741
- var STATION_STROKE_ACTIVE = 4;
13742
- var STATION_STROKE_IDLE = 3;
13743
- var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13744
- function StationNode({ node, active }) {
13745
- const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
13746
- const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
13747
- const \u5F62 = /* @__PURE__ */ jsxRuntime.jsx(
13748
- "circle",
13749
- {
13750
- "data-cdl-role": "node-body",
13751
- "data-cdl-mark": "station",
13752
- cx: node.cx,
13753
- cy: node.cy,
13754
- r: radius,
13755
- fill: "var(--cdl-node-fill, #ffffff)",
13756
- stroke: nodeAccent(node, STATION_DEFAULT),
13757
- strokeWidth
13758
- }
13759
- );
13760
- return renderShapeWithOutsideLabels(\u5F62, node, "middle");
13761
- }
13762
14180
 
13763
14181
  // src/kinds/label-stack.ts
13764
14182
  function \u540D\u672D\u306E\u6BB5\u3092\u307B\u3069\u304F(\u771F\u306Ey, \u9593\u9694, \u4E0A\u9650, \u4E0B\u9650) {
@@ -20920,10 +21338,14 @@ function CdlNodeView({
20920
21338
  stateValues,
20921
21339
  currentPhaseId,
20922
21340
  chartLineScale,
21341
+ compactTitleInset,
20923
21342
  \u95A2\u4FC2\u306E\u53D7\u3051\u53E3,
20924
21343
  lang,
20925
21344
  textMetrics,
20926
- \u5024\u306E\u540D\u524D
21345
+ \u5024\u306E\u540D\u524D,
21346
+ metroDiagram = false,
21347
+ metroDecision = false,
21348
+ stationNameRight = false
20927
21349
  }) {
20928
21350
  const value = node.value ? interpolate(node.value, stateValues, \u5024\u306E\u540D\u524D) : void 0;
20929
21351
  const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues, \u5024\u306E\u540D\u524D));
@@ -20964,7 +21386,7 @@ function CdlNodeView({
20964
21386
  case "event":
20965
21387
  return /* @__PURE__ */ jsxRuntime.jsx(EventNode, { node: resolvedNode, active, progress });
20966
21388
  case "card":
20967
- return /* @__PURE__ */ jsxRuntime.jsx(CardNode, { node: resolvedNode, active });
21389
+ return /* @__PURE__ */ jsxRuntime.jsx(CardNode, { node: resolvedNode, active, compactTitleInset });
20968
21390
  // 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
20969
21391
  case "sequence-board":
20970
21392
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -21251,11 +21673,13 @@ function CdlNodeView({
21251
21673
  case "shape-customer-service":
21252
21674
  return /* @__PURE__ */ jsxRuntime.jsx(ShapeCustomerServiceNode, { node: resolvedNode, active });
21253
21675
  case "mark-start":
21254
- return /* @__PURE__ */ jsxRuntime.jsx(MarkStartNode, { node: resolvedNode, active });
21676
+ return /* @__PURE__ */ jsxRuntime.jsx(MarkStartNode, { node: resolvedNode, active, metro: metroDiagram });
21255
21677
  case "mark-end":
21256
- return /* @__PURE__ */ jsxRuntime.jsx(MarkEndNode, { node: resolvedNode, active });
21678
+ return /* @__PURE__ */ jsxRuntime.jsx(MarkEndNode, { node: resolvedNode, active, metro: metroDiagram });
21257
21679
  case "station":
21258
- return /* @__PURE__ */ jsxRuntime.jsx(StationNode, { node: resolvedNode, active });
21680
+ return /* @__PURE__ */ jsxRuntime.jsx(StationNode, { node: resolvedNode, active, nameRight: stationNameRight });
21681
+ case "timeline-number":
21682
+ return /* @__PURE__ */ jsxRuntime.jsx(TimelineNumberNode, { node: resolvedNode });
21259
21683
  default:
21260
21684
  return /* @__PURE__ */ jsxRuntime.jsx(
21261
21685
  GenericNode,
@@ -21265,7 +21689,9 @@ function CdlNodeView({
21265
21689
  value: node.value,
21266
21690
  stateValues,
21267
21691
  \u5024\u306E\u540D\u524D,
21268
- textMetrics
21692
+ textMetrics,
21693
+ showKindIcon: !(metroDiagram && node.kind === "decision"),
21694
+ metroDecision
21269
21695
  }
21270
21696
  );
21271
21697
  }
@@ -21379,6 +21805,112 @@ function \u7BB1\u306E\u4E0A\u306E\u8A9E(node, textMetrics = "proportional") {
21379
21805
  )
21380
21806
  ] });
21381
21807
  }
21808
+ var ink = "var(--cdl-text-accent, #143a52)";
21809
+ var paper = "var(--cdl-stage-bg, #f8fafc)";
21810
+ var LEGEND_MARK_RENDERERS = {
21811
+ diamond: () => /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 23 -26 L 46 -8 L 23 10 L 0 -8 Z", fill: "none", stroke: ink, strokeWidth: 2 }),
21812
+ "filled-circle": () => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 11, fill: ink }),
21813
+ "double-circle": () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
21814
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 12, fill: "none", stroke: ink, strokeWidth: 2.5 }),
21815
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 6, fill: ink })
21816
+ ] }),
21817
+ "dotted-line": () => /* @__PURE__ */ jsxRuntime.jsx(
21818
+ "path",
21819
+ {
21820
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
21821
+ fill: "none",
21822
+ stroke: ink,
21823
+ strokeWidth: 6,
21824
+ strokeLinecap: "round",
21825
+ strokeDasharray: "0 9.6"
21826
+ }
21827
+ ),
21828
+ "solid-line": () => /* @__PURE__ */ jsxRuntime.jsx(
21829
+ "path",
21830
+ {
21831
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
21832
+ fill: "none",
21833
+ stroke: ink,
21834
+ strokeWidth: 4,
21835
+ strokeLinecap: "round"
21836
+ }
21837
+ ),
21838
+ "curved-line": () => /* @__PURE__ */ jsxRuntime.jsx(
21839
+ "path",
21840
+ {
21841
+ d: `M 0 6 C 12 -22, 34 -22, ${LEGEND_MARK_REFERENCE_WIDTH} 6`,
21842
+ fill: "none",
21843
+ stroke: ink,
21844
+ strokeWidth: 4,
21845
+ strokeLinecap: "round"
21846
+ }
21847
+ ),
21848
+ "rounded-label": () => /* @__PURE__ */ jsxRuntime.jsx(
21849
+ "rect",
21850
+ {
21851
+ x: 0,
21852
+ y: -23,
21853
+ width: LEGEND_MARK_REFERENCE_WIDTH,
21854
+ height: 30,
21855
+ rx: 9,
21856
+ fill: paper,
21857
+ stroke: ink,
21858
+ strokeWidth: 2
21859
+ }
21860
+ ),
21861
+ "numbered-circle": () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
21862
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 13, fill: paper, stroke: ink, strokeWidth: 2.5 }),
21863
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: 23, y: -2, textAnchor: "middle", fontSize: 17, fontWeight: 700, fill: ink, children: "1" })
21864
+ ] }),
21865
+ station: () => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 11, fill: paper, stroke: ink, strokeWidth: 4 }),
21866
+ arrow: () => /* @__PURE__ */ jsxRuntime.jsx(
21867
+ "path",
21868
+ {
21869
+ d: `M 0 -8 H 42 M 32 -18 L ${LEGEND_MARK_REFERENCE_WIDTH} -8 L 32 2`,
21870
+ fill: "none",
21871
+ stroke: ink,
21872
+ strokeWidth: 3,
21873
+ strokeLinecap: "round",
21874
+ strokeLinejoin: "round"
21875
+ }
21876
+ )
21877
+ };
21878
+ function CdlLegend({ items }) {
21879
+ return /* @__PURE__ */ jsxRuntime.jsx("g", { "data-cdl-role": "legend", children: items.map((item, index) => {
21880
+ const Mark = LEGEND_MARK_RENDERERS[item.mark];
21881
+ return /* @__PURE__ */ jsxRuntime.jsxs(
21882
+ "g",
21883
+ {
21884
+ "data-cdl-role": "legend-item",
21885
+ "data-cdl-y": item.y,
21886
+ transform: `translate(${item.x} ${item.y})`,
21887
+ children: [
21888
+ /* @__PURE__ */ jsxRuntime.jsx(
21889
+ "g",
21890
+ {
21891
+ "data-cdl-role": "legend-mark",
21892
+ "data-cdl-legend-mark": item.mark,
21893
+ transform: `scale(${LEGEND_MARK_SCALE})`,
21894
+ children: /* @__PURE__ */ jsxRuntime.jsx(Mark, {})
21895
+ }
21896
+ ),
21897
+ /* @__PURE__ */ jsxRuntime.jsx(
21898
+ "text",
21899
+ {
21900
+ "data-cdl-role": "legend-text",
21901
+ x: LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP,
21902
+ y: 0,
21903
+ fontSize: LEGEND_FONT_SIZE,
21904
+ fill: "var(--cdl-text-mute, #4d7187)",
21905
+ children: item.text
21906
+ }
21907
+ )
21908
+ ]
21909
+ },
21910
+ `${item.mark}-${index}`
21911
+ );
21912
+ }) });
21913
+ }
21382
21914
 
21383
21915
  // src/render/relation-focus.ts
21384
21916
  function relatedFocusSet(edges, nodeId) {
@@ -21391,10 +21923,17 @@ function relatedFocusSet(edges, nodeId) {
21391
21923
  }
21392
21924
  return \u96C6\u5408;
21393
21925
  }
21926
+ var METRO_LANE_BADGE_WIDTH = 200;
21927
+ var METRO_LANE_BADGE_HEIGHT = 88;
21928
+ var METRO_LANE_BADGE_INSET = 24;
21929
+ var METRO_LANE_GUIDE_GAP = 24;
21394
21930
  function \u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid) {
21931
+ if (laid.legend && laid.legend.length > 0) return true;
21395
21932
  if (laid.edges.length > 0) return true;
21396
21933
  if (laid.nodes.some((n) => isRenderedNode(n))) return true;
21397
- return laid.lanes.some((l) => l.contain === true || l.lifeline === true || Boolean(l.label));
21934
+ return laid.lanes.some(
21935
+ (l) => l.contain === true || l.lifeline === true || Boolean(l.label) || l.stage !== void 0 || l.metro !== void 0
21936
+ );
21398
21937
  }
21399
21938
  function CdlStage({
21400
21939
  laid,
@@ -21435,6 +21974,7 @@ function CdlStage({
21435
21974
  [\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B, \u7126\u70B9, laid.edges]
21436
21975
  );
21437
21976
  const activeSet = \u7126\u70B9\u306E\u96C6\u5408 ?? new Set(currentPhase?.activate ?? []);
21977
+ const \u8DEF\u7DDA\u56F3 = laid.edges.some((edge) => edge.routing === "metro") && laid.nodes.some((node) => node.kind === "station");
21438
21978
  const \u7BB1\u306E\u53D7\u3051\u53E3 = \u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B ? { \u89E6\u308C\u305F, \u96E2\u308C\u305F, \u62BC\u3057\u305F, \u7126\u70B9: \u7126\u70B9?.id ?? null } : void 0;
21439
21979
  const \u77E2\u5370\u306E\u51FA\u756A = react.useMemo(() => {
21440
21980
  const \u521D\u51FA = /* @__PURE__ */ new Map();
@@ -21463,9 +22003,15 @@ function CdlStage({
21463
22003
  const first = \u521D\u51FA.get(edgeId);
21464
22004
  if (first === void 0) return false;
21465
22005
  return \u4ECA === first;
22006
+ },
22007
+ /** 初出の段より前にある区間だけを、通過前として選べるようにする。 */
22008
+ \u307E\u3060\u304B: (edgeId) => {
22009
+ const first = \u521D\u51FA.get(edgeId);
22010
+ return first !== void 0 && \u4ECA >= 0 && \u4ECA < first;
21466
22011
  }
21467
22012
  };
21468
22013
  }, [laid.phases, currentPhase, laid.edgeReveal]);
22014
+ const \u7DDA\u3092\u63CF\u304F = (edge) => edge.routing === "metro" || \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id);
21469
22015
  const drawSet = new Set(currentPhase?.draw ?? []);
21470
22016
  const chartLineScales = react.useMemo(() => {
21471
22017
  const nodes = laid.nodes.filter((node) => node.kind === "chart-line");
@@ -21553,7 +22099,53 @@ function CdlStage({
21553
22099
  )
21554
22100
  );
21555
22101
  }, [laid]);
22102
+ const metroReturnArrowMarkers = react.useMemo(() => {
22103
+ const tones = new Set(
22104
+ laid.edges.filter((edge) => edge.metroReturn === true).map((edge) => edge.tone)
22105
+ );
22106
+ return [...tones].map((tone) => /* @__PURE__ */ jsxRuntime.jsx(
22107
+ "marker",
22108
+ {
22109
+ id: metroReturnArrowMarkerId(tone),
22110
+ viewBox: "0 -9.5 21 19",
22111
+ refX: "0",
22112
+ refY: "0",
22113
+ markerWidth: "21",
22114
+ markerHeight: "19",
22115
+ markerUnits: "userSpaceOnUse",
22116
+ orient: "auto",
22117
+ children: /* @__PURE__ */ jsxRuntime.jsx(
22118
+ "path",
22119
+ {
22120
+ "data-cdl-role": "edge-arrowhead",
22121
+ "data-cdl-mark": "metro-return-arrow",
22122
+ d: METRO_RETURN_ARROW_D,
22123
+ fill: TONE[tone]
22124
+ }
22125
+ )
22126
+ },
22127
+ tone
22128
+ ));
22129
+ }, [laid.edges]);
21556
22130
  const diagramScale = laid.diagramScale ?? 1;
22131
+ const stageLaneIds = new Set(
22132
+ laid.lanes.filter((lane) => lane.stage !== void 0).map((lane) => lane.id)
22133
+ );
22134
+ const stageNoteNodes = laid.nodes.filter(
22135
+ (node) => node.kind === "card" && (stageLaneIds.has(node.lane) || node.leaderTo !== void 0) && isRenderedNode(node) && Boolean(node.subtitle)
22136
+ );
22137
+ const stageNoteNodeIds = new Set(stageNoteNodes.map((node) => node.id));
22138
+ const nodesById = new Map(laid.nodes.map((node) => [node.id, node]));
22139
+ const metroDecisionIds = /* @__PURE__ */ new Set();
22140
+ const stationNamesRight = /* @__PURE__ */ new Set();
22141
+ for (const edge of laid.edges) {
22142
+ if (edge.routing !== "metro") continue;
22143
+ if (nodesById.get(edge.from)?.kind === "decision") metroDecisionIds.add(edge.from);
22144
+ if (nodesById.get(edge.to)?.kind === "decision") metroDecisionIds.add(edge.to);
22145
+ if (edge.toSide === "top" && nodesById.get(edge.to)?.kind === "station") {
22146
+ stationNamesRight.add(edge.to);
22147
+ }
22148
+ }
21557
22149
  if (!\u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid)) return null;
21558
22150
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: compact ? "px-3 py-3" : "px-6 py-6", style: compact ? void 0 : { minHeight: 460 }, children: [
21559
22151
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -21573,6 +22165,7 @@ function CdlStage({
21573
22165
  children: [
21574
22166
  /* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
21575
22167
  edgeHeadMarkers,
22168
+ metroReturnArrowMarkers,
21576
22169
  /* @__PURE__ */ jsxRuntime.jsx(
21577
22170
  "marker",
21578
22171
  {
@@ -21609,7 +22202,61 @@ function CdlStage({
21609
22202
  pointerEvents: "none"
21610
22203
  }
21611
22204
  ),
21612
- lane.contain && /* @__PURE__ */ jsxRuntime.jsx(
22205
+ lane.stage && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22206
+ /* @__PURE__ */ jsxRuntime.jsx(
22207
+ "rect",
22208
+ {
22209
+ "data-cdl-role": "stage-column",
22210
+ x: lane.x + STAGE_COLUMN_GAP / 2,
22211
+ y: lane.y,
22212
+ width: lane.width - STAGE_COLUMN_GAP,
22213
+ height: lane.height,
22214
+ rx: 16,
22215
+ fill: "var(--cdl-stage-bg, #fafafa)",
22216
+ fillOpacity: 0.5,
22217
+ stroke: "var(--cdl-text-mute, #8a8678)",
22218
+ strokeWidth: 1.5
22219
+ }
22220
+ ),
22221
+ /* @__PURE__ */ jsxRuntime.jsx(
22222
+ "text",
22223
+ {
22224
+ "data-cdl-role": "stage-number",
22225
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
22226
+ y: lane.y + 24,
22227
+ fontSize: 14,
22228
+ fontWeight: 500,
22229
+ letterSpacing: 1.5,
22230
+ fill: "var(--cdl-text-mute, #8a8678)",
22231
+ children: `\u6BB5\u968E ${lane.stage.number}`
22232
+ }
22233
+ ),
22234
+ /* @__PURE__ */ jsxRuntime.jsx(
22235
+ "text",
22236
+ {
22237
+ "data-cdl-role": "stage-name",
22238
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
22239
+ y: lane.y + 55,
22240
+ fontSize: 22,
22241
+ fontWeight: 700,
22242
+ fill: "var(--cdl-text, #1a1f2a)",
22243
+ children: lane.stage.name
22244
+ }
22245
+ ),
22246
+ /* @__PURE__ */ jsxRuntime.jsx(
22247
+ "line",
22248
+ {
22249
+ "data-cdl-role": "stage-divider",
22250
+ x1: lane.x + STAGE_COLUMN_GAP / 2,
22251
+ y1: lane.y + 72,
22252
+ x2: lane.x + lane.width - STAGE_COLUMN_GAP / 2,
22253
+ y2: lane.y + 72,
22254
+ stroke: "var(--cdl-divider, #d4d4d8)",
22255
+ strokeWidth: 1.5
22256
+ }
22257
+ )
22258
+ ] }),
22259
+ lane.contain && lane.stage === void 0 && /* @__PURE__ */ jsxRuntime.jsx(
21613
22260
  "rect",
21614
22261
  {
21615
22262
  "data-cdl-role": "lane-container",
@@ -21645,7 +22292,85 @@ function CdlStage({
21645
22292
  }
21646
22293
  );
21647
22294
  })(),
21648
- lane.label && /* @__PURE__ */ jsxRuntime.jsx(
22295
+ lane.timelineAxis && (() => {
22296
+ const ys = laid.nodes.filter((node) => node.lane === lane.id && isRenderedNode(node)).map((node) => node.cy);
22297
+ if (ys.length < 2) return null;
22298
+ const y1 = Math.min(...ys);
22299
+ const y2 = Math.max(...ys);
22300
+ if (y2 <= y1) return null;
22301
+ return /* @__PURE__ */ jsxRuntime.jsx(
22302
+ "line",
22303
+ {
22304
+ "data-cdl-role": "timeline-axis",
22305
+ x1: lane.x + lane.width / 2,
22306
+ y1,
22307
+ x2: lane.x + lane.width / 2,
22308
+ y2,
22309
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
22310
+ strokeWidth: TIMELINE_NUMBER_STROKE
22311
+ }
22312
+ );
22313
+ })(),
22314
+ lane.metro !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22315
+ /* @__PURE__ */ jsxRuntime.jsx(
22316
+ "line",
22317
+ {
22318
+ "data-cdl-role": "metro-lane-guide",
22319
+ x1: lane.x + METRO_LANE_BADGE_INSET + METRO_LANE_BADGE_WIDTH + METRO_LANE_GUIDE_GAP,
22320
+ y1: lane.y + lane.height / 2,
22321
+ x2: lane.x + lane.width - METRO_LANE_BADGE_INSET,
22322
+ y2: lane.y + lane.height / 2,
22323
+ stroke: "var(--cdl-line-mute, #a89d8f)",
22324
+ strokeWidth: 2,
22325
+ strokeDasharray: "22 7 5 7",
22326
+ opacity: 0.45
22327
+ }
22328
+ ),
22329
+ /* @__PURE__ */ jsxRuntime.jsxs(
22330
+ "g",
22331
+ {
22332
+ "data-cdl-role": "metro-lane-badge",
22333
+ transform: `translate(${lane.x + METRO_LANE_BADGE_INSET} ${lane.y + lane.height / 2 - METRO_LANE_BADGE_HEIGHT / 2})`,
22334
+ children: [
22335
+ /* @__PURE__ */ jsxRuntime.jsx(
22336
+ "rect",
22337
+ {
22338
+ width: METRO_LANE_BADGE_WIDTH,
22339
+ height: METRO_LANE_BADGE_HEIGHT,
22340
+ rx: 12,
22341
+ fill: "var(--cdl-node-fill, #ffffff)",
22342
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
22343
+ strokeWidth: 1.25
22344
+ }
22345
+ ),
22346
+ /* @__PURE__ */ jsxRuntime.jsx(
22347
+ "text",
22348
+ {
22349
+ x: METRO_LANE_BADGE_WIDTH / 2,
22350
+ y: lane.metro.subtitle ? 37 : 53,
22351
+ fontSize: 26,
22352
+ fontWeight: 700,
22353
+ fill: "var(--cdl-text, #1a1f2a)",
22354
+ textAnchor: "middle",
22355
+ children: lane.label
22356
+ }
22357
+ ),
22358
+ lane.metro.subtitle && /* @__PURE__ */ jsxRuntime.jsx(
22359
+ "text",
22360
+ {
22361
+ x: METRO_LANE_BADGE_WIDTH / 2,
22362
+ y: 67,
22363
+ fontSize: 18,
22364
+ fill: "var(--cdl-text-dim, #5a6270)",
22365
+ textAnchor: "middle",
22366
+ children: lane.metro.subtitle
22367
+ }
22368
+ )
22369
+ ]
22370
+ }
22371
+ )
22372
+ ] }),
22373
+ lane.label && lane.metro === void 0 && /* @__PURE__ */ jsxRuntime.jsx(
21649
22374
  "text",
21650
22375
  {
21651
22376
  "data-cdl-role": "lane-label",
@@ -21662,7 +22387,34 @@ function CdlStage({
21662
22387
  },
21663
22388
  lane.id
21664
22389
  )),
21665
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
22390
+ laid.nodes.map((node) => {
22391
+ if (node.leaderTo === void 0) return null;
22392
+ const target = nodesById.get(node.leaderTo);
22393
+ if (target === void 0) return null;
22394
+ if (!isRenderedNode(node) || !isRenderedNode(target)) return null;
22395
+ const targetIsRight = target.cx >= node.cx;
22396
+ const x1 = node.cx + (targetIsRight ? node.w / 2 : -node.w / 2);
22397
+ const x2 = target.cx + (targetIsRight ? -target.w / 2 : target.w / 2);
22398
+ if (targetIsRight ? x2 <= x1 : x1 <= x2) return null;
22399
+ return /* @__PURE__ */ jsxRuntime.jsx(
22400
+ "line",
22401
+ {
22402
+ "data-cdl-role": "timeline-leader",
22403
+ "data-cdl-leader-from": node.id,
22404
+ "data-cdl-leader-to": node.leaderTo,
22405
+ x1,
22406
+ y1: node.cy,
22407
+ x2,
22408
+ y2: target.cy,
22409
+ stroke: "var(--cdl-line-mute, #a89d8f)",
22410
+ strokeWidth: 1.5,
22411
+ strokeDasharray: "2 4",
22412
+ strokeLinecap: "round"
22413
+ },
22414
+ `timeline-leader-${node.id}`
22415
+ );
22416
+ }),
22417
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
21666
22418
  CdlEdgeView,
21667
22419
  {
21668
22420
  edge,
@@ -21670,16 +22422,19 @@ function CdlStage({
21670
22422
  flowing: \u7126\u70B9\u306E\u96C6\u5408?.has(edge.id) === true && !\u52D5\u304D\u3092\u63A7\u3048\u308B,
21671
22423
  drawing: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id),
21672
22424
  progress: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id) ? \u63CF\u304F\u9032\u307F : progress,
21673
- stateValues
22425
+ stateValues,
22426
+ pending: edge.routing === "metro" && \u77E2\u5370\u306E\u51FA\u756A.\u307E\u3060\u304B(edge.id)
21674
22427
  },
21675
22428
  edge.id
21676
22429
  )),
21677
22430
  laid.nodes.map((node) => {
21678
22431
  const \u63CF\u304F = drawSet.has(node.id);
22432
+ const \u6BB5\u968E\u306E\u88DC\u8DB3 = stageNoteNodeIds.has(node.id);
21679
22433
  const view = /* @__PURE__ */ jsxRuntime.jsx(
21680
22434
  CdlNodeView,
21681
22435
  {
21682
- node,
22436
+ node: \u6BB5\u968E\u306E\u88DC\u8DB3 ? { ...node, subtitle: void 0 } : node,
22437
+ compactTitleInset: \u6BB5\u968E\u306E\u88DC\u8DB3 ? STAGE_CARD_TEXT_INSET : void 0,
21683
22438
  active: activeSet.has(node.id),
21684
22439
  lang: laid.lang,
21685
22440
  textMetrics: laid.textMetrics,
@@ -21689,13 +22444,32 @@ function CdlStage({
21689
22444
  stateValues,
21690
22445
  \u5024\u306E\u540D\u524D,
21691
22446
  currentPhaseId: currentPhase?.id,
21692
- chartLineScale: chartLineScales.get(node.id)
22447
+ chartLineScale: chartLineScales.get(node.id),
22448
+ metroDiagram: \u8DEF\u7DDA\u56F3,
22449
+ metroDecision: metroDecisionIds.has(node.id),
22450
+ stationNameRight: stationNamesRight.has(node.id)
21693
22451
  }
21694
22452
  );
21695
22453
  return /* @__PURE__ */ jsxRuntime.jsx("g", { children: view }, node.id);
21696
22454
  }),
21697
- laid.edges.filter((edge) => Boolean(edge.dThrough) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
21698
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
22455
+ stageNoteNodes.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
22456
+ "text",
22457
+ {
22458
+ "data-cdl-role": "stage-note",
22459
+ "data-cdl-stage-node": node.id,
22460
+ x: node.cx + node.w / 2 - STAGE_CARD_TEXT_INSET,
22461
+ y: node.cy + 1,
22462
+ fontSize: 14,
22463
+ fontWeight: 500,
22464
+ textAnchor: "end",
22465
+ dominantBaseline: "middle",
22466
+ fill: "var(--cdl-text-mute, #8a8678)",
22467
+ children: interpolate(node.subtitle, stateValues, \u5024\u306E\u540D\u524D)
22468
+ },
22469
+ `stage-note-${node.id}`
22470
+ )),
22471
+ laid.edges.filter((edge) => Boolean(edge.dThrough) && \u7DDA\u3092\u63CF\u304F(edge)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
22472
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
21699
22473
  CdlEdgeLabelView,
21700
22474
  {
21701
22475
  edge,
@@ -21704,7 +22478,7 @@ function CdlStage({
21704
22478
  },
21705
22479
  `label-${edge.id}`
21706
22480
  )),
21707
- laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).flatMap(
22481
+ laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u7DDA\u3092\u63CF\u304F(edge)).flatMap(
21708
22482
  (edge) => ["head", "tail"].map((\u7AEF) => /* @__PURE__ */ jsxRuntime.jsx(
21709
22483
  CdlEdgeEndLabelView,
21710
22484
  {
@@ -21716,6 +22490,7 @@ function CdlStage({
21716
22490
  `${\u7AEF}-label-${edge.id}`
21717
22491
  ))
21718
22492
  ),
22493
+ laid.legend && /* @__PURE__ */ jsxRuntime.jsx(CdlLegend, { items: laid.legend }),
21719
22494
  debug && /* @__PURE__ */ jsxRuntime.jsxs("g", { pointerEvents: "none", children: [
21720
22495
  laid.bboxes.map((bbox, idx) => {
21721
22496
  const color = bbox.kind === "node" ? "#3b82f6" : bbox.kind === "lane-label" ? "#a855f7" : bbox.kind === "edge-label" ? "#10b981" : "#f59e0b";