@cardenelabs/cdl 0.46.0 → 0.48.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/CHANGELOG.md CHANGED
@@ -5,6 +5,67 @@ CDL (Chainome Diagram Language) の主要変更履歴。
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.48.0] - 2026-09-11
9
+
10
+ ### Fixed
11
+
12
+ - **札の列を線が横切る時に隙間が足りないまま残るのを直した** (#805)
13
+
14
+ 同じ 2 つの箱を結ぶ弧が 4 本あると、札は 4 枚の列になる。 列をどこで上下に分けるかは
15
+ `placeAvoidingSiblingPaths` が決めるが、塊を動かす先を **兄弟の弧が通る帯** だけで決めており、
16
+ 帯の外を通る別の線については置いた後に「貫いているか」 しか見ていなかった。
17
+
18
+ 貫き (隙間 0) だけを見ると、線が札の縁をかすめる置き方が「貫いていない」 として通る。
19
+ 門 (`clearance` 軸) は 14 を要求するため、門だけが落ちる。
20
+ 実測 = 下を回る線が札の上端 10 の所を通り、要求に 4 足りなかった。
21
+
22
+ 後段の逃がし pass では直せない。 列は上下とも隣の札で埋まっており、どちらへ動かしても
23
+ 隣との要求 (36) を割る。 列ごとずらす案も測ったが、巻き込んだ札が自分の弧から 162 離れ、
24
+ `edge-label-proximity` の error 閾値 (160) を越えた。
25
+
26
+ 塊を「近すぎる線を越える所」 まで押し出した置き方を候補に足した。 押す量は探さずに測る =
27
+ 邪魔をしている線を札の横幅で切った Y 範囲の縁から、要求ぶん離れる位置までの差を取る。
28
+
29
+ | | 直す前 | 直した後 |
30
+ |---|---|---|
31
+ | 札と横切る線の隙間 | 10 (要求 14) | 14 |
32
+ | 札と自分の弧の距離 (最遠) | 158 | 130 |
33
+ | 4 枚の分かれ方 | 上 1 枚 / 下 3 枚 | 上 2 枚 / 下 2 枚 |
34
+
35
+ 押す前の候補も残すので、押しても越えられない図 (線が札の上下を挟む形) では従来の置き方が
36
+ 選ばれる。 越えられなかった置き方は候補にしない = 隙間が足りないまま位置だけが動くのを防ぐ。
37
+
38
+ ## [0.47.0] - 2026-09-11
39
+
40
+ ### Added
41
+
42
+ - **箱に触れると関係する線だけが光るようにした** (#806)
43
+
44
+ クラス図と ER 図は **順番を持たない**。 段を進めて線を 1 本ずつ引く見せ方は作り手が決めた
45
+ 読ませ方でしかなく、読み手の問い (この箱はどこと繋がっているか) には答えていなかった。
46
+
47
+ `CdlDiagram.relationFocus` に `"hover"` を書くと、箱に触れた時に その箱 ・ 繋がる線 ・
48
+ 相手の箱だけが光り、他が沈む。 押すと留まり、もう一度押すか `Esc` で外れる
49
+ (線を目で追う間に指が外れると読めないため)。
50
+
51
+ | 書いた値 | 触れた時 |
52
+ |---|---|
53
+ | 書かない / `"off"` | 何もしない (既定) |
54
+ | `"hover"` | 関係する箱と線だけが光り、繋がる線に粒が流れる |
55
+
56
+ 光る集合は「触れた箱」「繋がる線」「線の相手の箱」 の 3 つで、**線の向きは問わない** =
57
+ クラス図の「継ぐ」 は子から親へ、 ER の「持つ」 は親から子へ向くが、読み手にとっては
58
+ どちらも「この箱の関係」 だから。 集合は `activate` と同じ所に入れるので、光る / 沈むの
59
+ 見え方は既にある仕組みをそのまま使う。
60
+
61
+ **触れていない間は段が勝つ** = この指定を足しても、段を持つ図の見え方は変わらない。
62
+ 指定を書かない図では箱に属性も受け口も付かないので、`tab` の順番も DOM も動かない。
63
+
64
+ `prefers-reduced-motion` が立っている時は粒を流さない。 光る側は控えない =
65
+ 動きではなく見分けなので、控える指定とは別の軸。
66
+
67
+ 箱は `role="button"` と `tabIndex` を持ち、`Enter` と `Space` でも留められる。
68
+
8
69
  ## [0.46.0] - 2026-09-11
9
70
 
10
71
  ### Fixed
@@ -2320,7 +2381,9 @@ Initial OSS release.
2320
2381
  使わない = annotated tag を push しても GitHub Release は作られず、 Release を作っていない版で
2321
2382
  行き止まりになる。
2322
2383
 
2323
- [Unreleased]: https://github.com/cardene777/cdl/compare/v0.46.0...HEAD
2384
+ [Unreleased]: https://github.com/cardene777/cdl/compare/v0.48.0...HEAD
2385
+ [0.48.0]: https://github.com/cardene777/cdl/compare/v0.47.0...v0.48.0
2386
+ [0.47.0]: https://github.com/cardene777/cdl/compare/v0.46.0...v0.47.0
2324
2387
  [0.46.0]: https://github.com/cardene777/cdl/compare/v0.45.0...v0.46.0
2325
2388
  [0.45.0]: https://github.com/cardene777/cdl/compare/v0.44.0...v0.45.0
2326
2389
  [0.44.0]: https://github.com/cardene777/cdl/compare/v0.43.0...v0.44.0
package/dist/index.cjs CHANGED
@@ -13751,6 +13751,8 @@ var EDGE_HEADS = [
13751
13751
  var EDGE_HEAD_FILL_DEFAULT = "solid";
13752
13752
  var EDGE_HEAD_FILLS = ["solid", "hollow"];
13753
13753
  var EDGE_REVEALS = ["phase", "all"];
13754
+ var RELATION_FOCUSES = ["off", "hover"];
13755
+ var RELATION_FOCUS_DEFAULT = "off";
13754
13756
  var EDGE_REVEAL_DEFAULT = "phase";
13755
13757
 
13756
13758
  // src/kinds/text-width.ts
@@ -19576,33 +19578,73 @@ function placeAvoidingSiblingPaths(args) {
19576
19578
  }
19577
19579
  };
19578
19580
  consider([...wants]);
19581
+ const \u62BC\u3059\u91CF = (idx, side, pos) => {
19582
+ let \u8981\u308B = 0;
19583
+ for (const i of idx) {
19584
+ const box = boxes[i];
19585
+ const rect = { x: box.x, y: pos[i] - box.h / 2, w: box.w, h: box.h };
19586
+ for (const probe of probeSegs[i]) {
19587
+ if (measurePathLabelGapSegs(probe.segs, rect) >= CLEARANCE_PATH_LABEL) continue;
19588
+ let lo = Infinity;
19589
+ let hi = -Infinity;
19590
+ for (const seg of probe.segs) {
19591
+ const c = clipY(seg, box.x, box.x + box.w);
19592
+ if (!c) continue;
19593
+ lo = Math.min(lo, c.lo);
19594
+ hi = Math.max(hi, c.hi);
19595
+ }
19596
+ if (lo > hi) continue;
19597
+ const \u884C\u304D\u5148 = side === "above" ? lo - box.h / 2 - CLEARANCE_PATH_LABEL : hi + box.h / 2 + CLEARANCE_PATH_LABEL;
19598
+ \u8981\u308B = Math.max(\u8981\u308B, side === "above" ? pos[i] - \u884C\u304D\u5148 : \u884C\u304D\u5148 - pos[i]);
19599
+ }
19600
+ }
19601
+ return Math.max(0, \u8981\u308B);
19602
+ };
19579
19603
  if (bands.some((b) => b !== null)) {
19580
19604
  for (let split = 0; split <= order.length; split++) {
19581
19605
  const above = order.slice(0, split);
19582
19606
  const below = order.slice(split);
19583
- const out = new Array(members.length);
19584
- for (const [idx, side] of [
19585
- [above, "above"],
19586
- [below, "below"]
19587
- ]) {
19588
- if (idx.length === 0) continue;
19589
- const ys = spread(idx);
19590
- let shift = 0;
19591
- for (let k = 0; k < idx.length; k++) {
19592
- const band = bands[idx[k]];
19593
- if (!band) continue;
19594
- shift = side === "above" ? Math.min(shift, band.lo - ys[k]) : Math.max(shift, band.hi - ys[k]);
19595
- }
19596
- for (let k = 0; k < idx.length; k++) out[idx[k]] = ys[k] + shift;
19597
- }
19598
- if (above.length > 0 && below.length > 0) {
19607
+ const \u9593\u3092\u7A7A\u3051\u308B = (out) => {
19608
+ if (above.length === 0 || below.length === 0) return;
19599
19609
  const lastIdx = above[above.length - 1];
19600
19610
  const firstIdx = below[0];
19601
19611
  const need = (sizes[lastIdx] + sizes[firstIdx]) / 2 + CLEARANCE_LABEL_LABEL;
19602
19612
  const deficit = need - (out[firstIdx] - out[lastIdx]);
19603
- if (deficit > 0) for (const i of below) out[i] += deficit;
19604
- }
19605
- consider(out);
19613
+ if (deficit > 0) for (const i of below) out[i] = out[i] + deficit;
19614
+ };
19615
+ const \u5E2F\u3092\u8D8A\u3048\u308B = () => {
19616
+ const out = new Array(members.length);
19617
+ for (const [idx, side] of [
19618
+ [above, "above"],
19619
+ [below, "below"]
19620
+ ]) {
19621
+ if (idx.length === 0) continue;
19622
+ const ys = spread(idx);
19623
+ let shift = 0;
19624
+ for (let k = 0; k < idx.length; k++) {
19625
+ const band = bands[idx[k]];
19626
+ if (!band) continue;
19627
+ shift = side === "above" ? Math.min(shift, band.lo - ys[k]) : Math.max(shift, band.hi - ys[k]);
19628
+ }
19629
+ for (let k = 0; k < idx.length; k++) out[idx[k]] = ys[k] + shift;
19630
+ }
19631
+ \u9593\u3092\u7A7A\u3051\u308B(out);
19632
+ return out;
19633
+ };
19634
+ const \u62BC\u3057\u3066\u8D8A\u3048\u308B = () => {
19635
+ const out = \u5E2F\u3092\u8D8A\u3048\u308B();
19636
+ for (let round = 0; round < 4; round += 1) {
19637
+ const a = \u62BC\u3059\u91CF(above, "above", out);
19638
+ const b = \u62BC\u3059\u91CF(below, "below", out);
19639
+ if (a === 0 && b === 0) return out;
19640
+ for (const i of above) out[i] = out[i] - a;
19641
+ for (const i of below) out[i] = out[i] + b;
19642
+ }
19643
+ return \u62BC\u3059\u91CF(above, "above", out) === 0 && \u62BC\u3059\u91CF(below, "below", out) === 0 ? out : null;
19644
+ };
19645
+ consider(\u5E2F\u3092\u8D8A\u3048\u308B());
19646
+ const \u62BC\u3057\u305F = \u62BC\u3057\u3066\u8D8A\u3048\u308B();
19647
+ if (\u62BC\u3057\u305F) consider(\u62BC\u3057\u305F);
19606
19648
  }
19607
19649
  }
19608
19650
  const plain = spread(all);
@@ -20722,6 +20764,7 @@ function layout(diag) {
20722
20764
  ...diag.derived ? { derived: diag.derived } : {},
20723
20765
  // 矢印をいつ出すか (#582)。 描き手が読む欄なので、そのまま持ち越す
20724
20766
  ...diag.edgeReveal ? { edgeReveal: diag.edgeReveal } : {},
20767
+ ...diag.relationFocus ? { relationFocus: diag.relationFocus } : {},
20725
20768
  // どの配色で描くか (#1553)。 空文字は書かなかった扱いにする = 空の目印を出すと、
20726
20769
  // 消費側の `[data-cdl-palette]` が配色を書いていない図にも当たる。
20727
20770
  //
@@ -22809,7 +22852,8 @@ function CdlEdgeView({
22809
22852
  active,
22810
22853
  drawing,
22811
22854
  progress,
22812
- stateValues
22855
+ stateValues,
22856
+ flowing = false
22813
22857
  }) {
22814
22858
  const color = edgeColor(edge);
22815
22859
  const marker = edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
@@ -22827,6 +22871,7 @@ function CdlEdgeView({
22827
22871
  "data-cdl-from": edge.from,
22828
22872
  "data-cdl-to": edge.to,
22829
22873
  "data-cdl-active": active ? "true" : "false",
22874
+ "data-cdl-flowing": flowing ? "true" : "false",
22830
22875
  "data-cdl-drawing": drawing ? "true" : "false",
22831
22876
  "data-cdl-path-d": edge.d,
22832
22877
  "data-cdl-label-x": edge.labelX,
@@ -22879,7 +22924,11 @@ function CdlEdgeView({
22879
22924
  strokeWidth: 14,
22880
22925
  strokeLinecap: "round"
22881
22926
  }
22882
- )
22927
+ ),
22928
+ flowing && !isDotted && /* @__PURE__ */ jsxRuntime.jsxs("g", { "data-cdl-role": "edge-flow", children: [
22929
+ /* @__PURE__ */ jsxRuntime.jsx("path", { id: glowPathId, d: edge.d, fill: "none", stroke: "none" }),
22930
+ /* @__PURE__ */ jsxRuntime.jsx(EdgeGlowCircles, { color, edgeId: edge.id })
22931
+ ] })
22883
22932
  ]
22884
22933
  }
22885
22934
  );
@@ -30655,7 +30704,8 @@ function CdlNodeView({
30655
30704
  progress,
30656
30705
  stateValues,
30657
30706
  currentPhaseId,
30658
- chartLineScale
30707
+ chartLineScale,
30708
+ \u95A2\u4FC2\u306E\u53D7\u3051\u53E3
30659
30709
  }) {
30660
30710
  const value = node.value ? interpolate(node.value, stateValues) : void 0;
30661
30711
  const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues));
@@ -30977,6 +31027,23 @@ function CdlNodeView({
30977
31027
  "data-cdl-eyebrow": resolvedNode.eyebrow ?? "",
30978
31028
  "data-cdl-offset-x": offsetX || void 0,
30979
31029
  "data-cdl-offset-y": offsetY || void 0,
31030
+ ...\u95A2\u4FC2\u306E\u53D7\u3051\u53E3 === void 0 ? {} : {
31031
+ // 読み上げに「押せる」 ことを伝え、 tab でも辿れるようにする。
31032
+ role: "button",
31033
+ tabIndex: 0,
31034
+ "aria-pressed": \u95A2\u4FC2\u306E\u53D7\u3051\u53E3.\u7126\u70B9 === node.id,
31035
+ style: { cursor: "pointer" },
31036
+ onPointerEnter: () => \u95A2\u4FC2\u306E\u53D7\u3051\u53E3.\u89E6\u308C\u305F(node.id),
31037
+ onPointerLeave: () => \u95A2\u4FC2\u306E\u53D7\u3051\u53E3.\u96E2\u308C\u305F(),
31038
+ onFocus: () => \u95A2\u4FC2\u306E\u53D7\u3051\u53E3.\u89E6\u308C\u305F(node.id),
31039
+ onBlur: () => \u95A2\u4FC2\u306E\u53D7\u3051\u53E3.\u96E2\u308C\u305F(),
31040
+ onClick: () => \u95A2\u4FC2\u306E\u53D7\u3051\u53E3.\u62BC\u3057\u305F(node.id),
31041
+ onKeyDown: (e) => {
31042
+ if (e.key !== "Enter" && e.key !== " ") return;
31043
+ e.preventDefault();
31044
+ \u95A2\u4FC2\u306E\u53D7\u3051\u53E3.\u62BC\u3057\u305F(node.id);
31045
+ }
31046
+ },
30980
31047
  transform: hasOffset ? `translate(${offsetX}, ${offsetY})` : void 0,
30981
31048
  opacity: boundOpacity,
30982
31049
  "data-cdl-tone": node.tone,
@@ -31000,6 +31067,18 @@ function CdlNodeView({
31000
31067
  }
31001
31068
  );
31002
31069
  }
31070
+
31071
+ // src/render/relation-focus.ts
31072
+ function relatedFocusSet(edges, nodeId) {
31073
+ const \u96C6\u5408 = /* @__PURE__ */ new Set([nodeId]);
31074
+ for (const e of edges) {
31075
+ if (e.from !== nodeId && e.to !== nodeId) continue;
31076
+ \u96C6\u5408.add(e.id);
31077
+ \u96C6\u5408.add(e.from);
31078
+ \u96C6\u5408.add(e.to);
31079
+ }
31080
+ return \u96C6\u5408;
31081
+ }
31003
31082
  function CdlStage({
31004
31083
  laid,
31005
31084
  currentPhase,
@@ -31010,7 +31089,35 @@ function CdlStage({
31010
31089
  compact = false,
31011
31090
  svgRef
31012
31091
  }) {
31013
- const activeSet = new Set(currentPhase?.activate ?? []);
31092
+ const [\u7126\u70B9, set\u7126\u70B9] = react.useState(null);
31093
+ const \u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B = (laid.relationFocus ?? RELATION_FOCUS_DEFAULT) === "hover";
31094
+ const \u52D5\u304D\u3092\u63A7\u3048\u308B = useReducedMotion();
31095
+ react.useEffect(() => {
31096
+ if (!\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B) set\u7126\u70B9(null);
31097
+ }, [\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B]);
31098
+ react.useEffect(() => {
31099
+ if (!\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B || \u7126\u70B9?.\u7559\u3081\u305F !== true) return;
31100
+ const \u62BC\u3057\u305F2 = (e) => {
31101
+ if (e.key === "Escape") set\u7126\u70B9(null);
31102
+ };
31103
+ window.addEventListener("keydown", \u62BC\u3057\u305F2);
31104
+ return () => window.removeEventListener("keydown", \u62BC\u3057\u305F2);
31105
+ }, [\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B, \u7126\u70B9?.\u7559\u3081\u305F]);
31106
+ const \u89E6\u308C\u305F = react.useCallback((id) => {
31107
+ set\u7126\u70B9((\u524D) => \u524D?.\u7559\u3081\u305F === true ? \u524D : { id, \u7559\u3081\u305F: false });
31108
+ }, []);
31109
+ const \u96E2\u308C\u305F = react.useCallback(() => {
31110
+ set\u7126\u70B9((\u524D) => \u524D?.\u7559\u3081\u305F === true ? \u524D : null);
31111
+ }, []);
31112
+ const \u62BC\u3057\u305F = react.useCallback((id) => {
31113
+ set\u7126\u70B9((\u524D) => \u524D?.id === id && \u524D.\u7559\u3081\u305F ? null : { id, \u7559\u3081\u305F: true });
31114
+ }, []);
31115
+ const \u7126\u70B9\u306E\u96C6\u5408 = react.useMemo(
31116
+ () => \u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B && \u7126\u70B9 !== null ? relatedFocusSet(laid.edges, \u7126\u70B9.id) : null,
31117
+ [\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B, \u7126\u70B9, laid.edges]
31118
+ );
31119
+ const activeSet = \u7126\u70B9\u306E\u96C6\u5408 ?? new Set(currentPhase?.activate ?? []);
31120
+ 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;
31014
31121
  const \u77E2\u5370\u306E\u51FA\u756A = react.useMemo(() => {
31015
31122
  const \u521D\u51FA = /* @__PURE__ */ new Map();
31016
31123
  laid.phases.forEach((p, i) => {
@@ -31241,6 +31348,7 @@ function CdlStage({
31241
31348
  {
31242
31349
  edge,
31243
31350
  active: activeSet.has(edge.id),
31351
+ flowing: \u7126\u70B9\u306E\u96C6\u5408?.has(edge.id) === true && !\u52D5\u304D\u3092\u63A7\u3048\u308B,
31244
31352
  drawing: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id),
31245
31353
  progress: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id) ? \u63CF\u304F\u9032\u307F : progress,
31246
31354
  stateValues
@@ -31254,6 +31362,7 @@ function CdlStage({
31254
31362
  {
31255
31363
  node,
31256
31364
  active: activeSet.has(node.id),
31365
+ \u95A2\u4FC2\u306E\u53D7\u3051\u53E3: \u7BB1\u306E\u53D7\u3051\u53E3,
31257
31366
  drawing: \u63CF\u304F,
31258
31367
  progress: \u63CF\u304F ? \u63CF\u304F\u9032\u307F : progress,
31259
31368
  stateValues,
@@ -32881,6 +32990,8 @@ exports.NEAR_COLLISION_DEFAULT_MIN = NEAR_COLLISION_DEFAULT_MIN;
32881
32990
  exports.NEAR_COLLISION_POLICY = NEAR_COLLISION_POLICY;
32882
32991
  exports.NODE_FRAME_SELECTOR = NODE_FRAME_SELECTOR;
32883
32992
  exports.NODE_KINDS = NODE_KINDS;
32993
+ exports.RELATION_FOCUSES = RELATION_FOCUSES;
32994
+ exports.RELATION_FOCUS_DEFAULT = RELATION_FOCUS_DEFAULT;
32884
32995
  exports.ROW_GAP_VARIANCE_TOL = ROW_GAP_VARIANCE_TOL;
32885
32996
  exports.SEQUENCE_MESSAGE_LOOK = SEQUENCE_MESSAGE_LOOK;
32886
32997
  exports.SPEC_CLEARANCE_POLICY = SPEC_CLEARANCE_POLICY;
@@ -32946,6 +33057,7 @@ exports.pxToWorld = pxToWorld;
32946
33057
  exports.quadrant = quadrant;
32947
33058
  exports.rectRectClearance = rectRectClearance;
32948
33059
  exports.rectRectOverlapArea = rectRectOverlapArea;
33060
+ exports.relatedFocusSet = relatedFocusSet;
32949
33061
  exports.rendersRows = rendersRows;
32950
33062
  exports.requiredContrastRatio = requiredContrastRatio;
32951
33063
  exports.requiredNearClearance = requiredNearClearance;