@cardenelabs/cdl 0.5.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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +343 -0
  3. package/SPEC.md +374 -0
  4. package/dist/index.cjs +28085 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +3415 -0
  7. package/dist/index.d.ts +3415 -0
  8. package/dist/index.js +27962 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/react.cjs +23043 -0
  11. package/dist/react.cjs.map +1 -0
  12. package/dist/react.d.cts +2 -0
  13. package/dist/react.d.ts +2 -0
  14. package/dist/react.js +23041 -0
  15. package/dist/react.js.map +1 -0
  16. package/dist/render-C78lIXeC.d.cts +2449 -0
  17. package/dist/render-C78lIXeC.d.ts +2449 -0
  18. package/examples/quick-start.md +88 -0
  19. package/package.json +79 -0
  20. package/src/anim/core/easing.ts +77 -0
  21. package/src/anim/core/timeline.ts +335 -0
  22. package/src/anim/core/types.ts +42 -0
  23. package/src/anim/index.ts +20 -0
  24. package/src/anim/react/index.ts +3 -0
  25. package/src/anim/react/useReducedMotion.ts +27 -0
  26. package/src/anim/react/useTimeline.ts +48 -0
  27. package/src/assert-never.ts +16 -0
  28. package/src/author-intent-verify.ts +587 -0
  29. package/src/builder.ts +3740 -0
  30. package/src/compile.ts +12 -0
  31. package/src/dom-verify-core.ts +121 -0
  32. package/src/dom-verify-types.ts +21 -0
  33. package/src/dom-verify.ts +948 -0
  34. package/src/event-handler/index.ts +184 -0
  35. package/src/formula/ast.ts +46 -0
  36. package/src/formula/evaluator.ts +163 -0
  37. package/src/formula/formula-computeds.ts +83 -0
  38. package/src/formula/index.ts +5 -0
  39. package/src/formula/parser.ts +399 -0
  40. package/src/index.ts +284 -0
  41. package/src/input-signals.ts +74 -0
  42. package/src/kinds/actor.tsx +79 -0
  43. package/src/kinds/card.tsx +61 -0
  44. package/src/kinds/chart-bar.tsx +121 -0
  45. package/src/kinds/chart-line.tsx +163 -0
  46. package/src/kinds/chart-pie.tsx +107 -0
  47. package/src/kinds/compact-title.ts +164 -0
  48. package/src/kinds/dyn-shape.tsx +394 -0
  49. package/src/kinds/event.tsx +70 -0
  50. package/src/kinds/function.tsx +53 -0
  51. package/src/kinds/funnel.tsx +122 -0
  52. package/src/kinds/gantt.tsx +193 -0
  53. package/src/kinds/generic.tsx +368 -0
  54. package/src/kinds/mind-map.tsx +367 -0
  55. package/src/kinds/mind-radial.tsx +209 -0
  56. package/src/kinds/node-tone.ts +18 -0
  57. package/src/kinds/quadrant.tsx +164 -0
  58. package/src/kinds/row-align.ts +179 -0
  59. package/src/kinds/shape-basement.tsx +683 -0
  60. package/src/kinds/shape-blockchain.tsx +750 -0
  61. package/src/kinds/shape-commerce.tsx +553 -0
  62. package/src/kinds/shape-finance.tsx +706 -0
  63. package/src/kinds/shape-hardware.tsx +862 -0
  64. package/src/kinds/shape-people.tsx +439 -0
  65. package/src/kinds/shape-region.tsx +383 -0
  66. package/src/kinds/shape-software.tsx +859 -0
  67. package/src/kinds/storage.tsx +180 -0
  68. package/src/kinds/text-width.ts +73 -0
  69. package/src/kinds/tree.tsx +275 -0
  70. package/src/kinds/user-journey.tsx +240 -0
  71. package/src/label-text.ts +71 -0
  72. package/src/layout/clearance-constants.ts +47 -0
  73. package/src/layout/collisions.ts +2078 -0
  74. package/src/layout/edges.ts +2498 -0
  75. package/src/layout/footer-shape.ts +97 -0
  76. package/src/layout/geometry.ts +135 -0
  77. package/src/layout/label-shift.ts +28 -0
  78. package/src/layout/lanes.ts +328 -0
  79. package/src/layout/nodes.ts +190 -0
  80. package/src/layout/predict-bbox.ts +76 -0
  81. package/src/layout/px-projection.ts +176 -0
  82. package/src/layout/spec.ts +872 -0
  83. package/src/layout/text-width.ts +133 -0
  84. package/src/layout/tokens.ts +181 -0
  85. package/src/layout/viewbox.ts +47 -0
  86. package/src/layout-with-validation.ts +175 -0
  87. package/src/layout.ts +381 -0
  88. package/src/presets.ts +2108 -0
  89. package/src/reactive/batch.ts +48 -0
  90. package/src/reactive/computed.ts +85 -0
  91. package/src/reactive/effect.ts +145 -0
  92. package/src/reactive/index.ts +6 -0
  93. package/src/reactive/internal.ts +109 -0
  94. package/src/reactive/signal.ts +113 -0
  95. package/src/render/edges.tsx +318 -0
  96. package/src/render/header.tsx +146 -0
  97. package/src/render/interactive-panel.tsx +9620 -0
  98. package/src/render/nodes.tsx +319 -0
  99. package/src/render/stage.tsx +377 -0
  100. package/src/render/tone.ts +64 -0
  101. package/src/render/utils.ts +238 -0
  102. package/src/render.tsx +221 -0
  103. package/src/scroll-trigger/index.ts +109 -0
  104. package/src/scroll-trigger/progress.ts +49 -0
  105. package/src/thumbnail.tsx +114 -0
  106. package/src/types.ts +2371 -0
  107. package/src/validate.ts +268 -0
  108. package/src/visual-validate.ts +4381 -0
@@ -0,0 +1,164 @@
1
+ import type { JSX } from "react";
2
+ import type { LaidNode } from "../types";
3
+ import { TONE, toneFill } from "../render/tone";
4
+ import { measureTextWidth } from "../layout/text-width";
5
+
6
+ /**
7
+ * quadrant-matrix kind ... 1 node に quadrantData (xAxis / yAxis / quadrantLabels / items)
8
+ * を保持し、 十字軸 + 4 象限 + item pin を SVG 描画する。
9
+ * layout = 中央十字線で 4 象限に分割、 各象限にほんのり色分け背景 + 象限 label、
10
+ * item は象限内に大きめの card で配置、 軸両端に矢印 + label を配置。
11
+ */
12
+ export function QuadrantNode({ node, active }: { node: LaidNode; active: boolean }): JSX.Element {
13
+ const x0 = node.cx - node.w / 2;
14
+ const y0 = node.cy - node.h / 2;
15
+ const data = node.quadrantData;
16
+ if (!data) return <g transform={`translate(${x0} ${y0})`} />;
17
+
18
+ const PAD_TOP = 40;
19
+ const PAD_BOTTOM = 56;
20
+ // 左右の余白は軸の名前が収まる幅にする。 固定値 (60) だと長い名前が枠の外に出て、 図の
21
+ // 外枠に重なって読めなくなる (実測 = `← Low effort` が枠線を跨いでいた)。
22
+ //
23
+ // 名前の前後に矢印と間隔 (12) が付くので、 その分も見込む。
24
+ const AXIS_LABEL_FONT = 12;
25
+ const PAD_MIN = 60;
26
+ const axisLabelPad = (text: string): number =>
27
+ Math.max(PAD_MIN, measureTextWidth(`← ${text}`, { fontSize: AXIS_LABEL_FONT, fontWeight: 700 }) + 12);
28
+ // 名前が長くても図そのものは残す。 上限を置かないと、 名前が箱の幅を超えた時に
29
+ // 描く場所が無くなる (実測 = 幅 640 に 50 文字の名前を左右へ置くと canvasW が -143 になった)。
30
+ // 名前は溢れるが、 読めない図を出すよりは良い。
31
+ const padBudget = Math.max(0, node.w * 0.5 - PAD_MIN * 2);
32
+ const wantLeft = axisLabelPad(data.xAxis.left) - PAD_MIN;
33
+ const wantRight = axisLabelPad(data.xAxis.right) - PAD_MIN;
34
+ const wantTotal = wantLeft + wantRight;
35
+ // 収まらない時は要求の比で分ける。 片方だけ長い形で、 短い側まで削らない。
36
+ const padScale = wantTotal > padBudget && wantTotal > 0 ? padBudget / wantTotal : 1;
37
+ const PAD_LEFT = PAD_MIN + wantLeft * padScale;
38
+ const PAD_RIGHT = PAD_MIN + wantRight * padScale;
39
+ const canvasW = node.w - PAD_LEFT - PAD_RIGHT;
40
+ const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
41
+
42
+ const cx = PAD_LEFT + canvasW / 2;
43
+ const cy = PAD_TOP + canvasH / 2;
44
+ const halfW = canvasW / 2;
45
+ const halfH = canvasH / 2;
46
+
47
+ const itemsBy = {
48
+ topLeft: data.items.filter((i) => i.quadrant === "topLeft"),
49
+ topRight: data.items.filter((i) => i.quadrant === "topRight"),
50
+ bottomLeft: data.items.filter((i) => i.quadrant === "bottomLeft"),
51
+ bottomRight: data.items.filter((i) => i.quadrant === "bottomRight"),
52
+ };
53
+
54
+ // 4 象限の淡い背景。 色は共有の表から作る
55
+ const quadrantColor = {
56
+ topLeft: toneFill("success", 0.06),
57
+ topRight: toneFill("accent", 0.09),
58
+ bottomLeft: toneFill("warning", 0.06),
59
+ bottomRight: toneFill("error", 0.06),
60
+ };
61
+ const quadrantAccent = {
62
+ topLeft: TONE.success,
63
+ topRight: TONE.accent,
64
+ bottomLeft: TONE.warning,
65
+ bottomRight: TONE.error,
66
+ };
67
+
68
+ return (
69
+ <g transform={`translate(${x0} ${y0})`}>
70
+ <rect
71
+ data-cdl-role="node-body"
72
+ x={0}
73
+ y={0}
74
+ width={node.w}
75
+ height={node.h}
76
+ rx={16}
77
+ fill="var(--cdl-node-fill, #ffffff)"
78
+ stroke={active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)"}
79
+ strokeWidth={active ? 3 : 1.25}
80
+ />
81
+
82
+ <rect data-cdl-role="quadrant-bg" x={PAD_LEFT} y={PAD_TOP} width={halfW} height={halfH} {...quadrantColor.topLeft} />
83
+ <rect data-cdl-role="quadrant-bg" x={cx} y={PAD_TOP} width={halfW} height={halfH} {...quadrantColor.topRight} />
84
+ <rect data-cdl-role="quadrant-bg" x={PAD_LEFT} y={cy} width={halfW} height={halfH} {...quadrantColor.bottomLeft} />
85
+ <rect data-cdl-role="quadrant-bg" x={cx} y={cy} width={halfW} height={halfH} {...quadrantColor.bottomRight} />
86
+
87
+ <rect
88
+ data-cdl-role="quadrant-canvas"
89
+ x={PAD_LEFT}
90
+ y={PAD_TOP}
91
+ width={canvasW}
92
+ height={canvasH}
93
+ fill="none"
94
+ stroke="var(--cdl-divider, #d4d4d8)"
95
+ strokeWidth={1}
96
+ rx={8}
97
+ />
98
+
99
+ <line x1={cx} y1={PAD_TOP} x2={cx} y2={PAD_TOP + canvasH} stroke="var(--cdl-text-mute, #8a8678)" strokeWidth={1.5} />
100
+ <line x1={PAD_LEFT} y1={cy} x2={PAD_LEFT + canvasW} y2={cy} stroke="var(--cdl-text-mute, #8a8678)" strokeWidth={1.5} />
101
+
102
+ <text x={PAD_LEFT + halfW / 2} y={PAD_TOP + 22} fontSize={13} fontWeight={700} textAnchor="middle" fill={quadrantAccent.topLeft}>
103
+ {data.quadrantLabels.topLeft}
104
+ </text>
105
+ <text x={cx + halfW / 2} y={PAD_TOP + 22} fontSize={13} fontWeight={700} textAnchor="middle" fill={quadrantAccent.topRight}>
106
+ {data.quadrantLabels.topRight}
107
+ </text>
108
+ <text x={PAD_LEFT + halfW / 2} y={PAD_TOP + canvasH - 12} fontSize={13} fontWeight={700} textAnchor="middle" fill={quadrantAccent.bottomLeft}>
109
+ {data.quadrantLabels.bottomLeft}
110
+ </text>
111
+ <text x={cx + halfW / 2} y={PAD_TOP + canvasH - 12} fontSize={13} fontWeight={700} textAnchor="middle" fill={quadrantAccent.bottomRight}>
112
+ {data.quadrantLabels.bottomRight}
113
+ </text>
114
+
115
+ <text x={cx} y={PAD_TOP - 12} fontSize={12} fontWeight={700} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
116
+ ↑ {data.yAxis.top}
117
+ </text>
118
+ <text x={cx} y={PAD_TOP + canvasH + 24} fontSize={12} fontWeight={700} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
119
+ ↓ {data.yAxis.bottom}
120
+ </text>
121
+ <text x={PAD_LEFT - 12} y={cy + 4} fontSize={AXIS_LABEL_FONT} fontWeight={700} textAnchor="end" fill="var(--cdl-text-dim, #5a6270)">
122
+ ← {data.xAxis.left}
123
+ </text>
124
+ <text x={PAD_LEFT + canvasW + 12} y={cy + 4} fontSize={AXIS_LABEL_FONT} fontWeight={700} textAnchor="start" fill="var(--cdl-text-dim, #5a6270)">
125
+ {data.xAxis.right} →
126
+ </text>
127
+
128
+ {(Object.keys(itemsBy) as Array<keyof typeof itemsBy>).map((q) => {
129
+ const items = itemsBy[q];
130
+ if (items.length === 0) return null;
131
+ const isLeft = q === "topLeft" || q === "bottomLeft";
132
+ const isTop = q === "topLeft" || q === "topRight";
133
+ const qxStart = (isLeft ? PAD_LEFT : cx) + 18;
134
+ const qyStart = (isTop ? PAD_TOP : cy) + 44;
135
+ const qHalfW = halfW - 36;
136
+ const qHalfH = halfH - 60;
137
+ const gap = 10;
138
+ const rowH = 34;
139
+ const maxRows = Math.max(1, Math.floor(qHalfH / (rowH + gap)));
140
+ return items.slice(0, maxRows).map((it, idx) => {
141
+ const py = qyStart + idx * (rowH + gap);
142
+ return (
143
+ <g key={`item-${it.id}`}>
144
+ <rect
145
+ data-cdl-role="quadrant-item"
146
+ x={qxStart}
147
+ y={py}
148
+ width={qHalfW}
149
+ height={rowH}
150
+ rx={6}
151
+ fill="var(--cdl-node-fill, #ffffff)"
152
+ stroke={quadrantAccent[q]}
153
+ strokeWidth={1.5}
154
+ />
155
+ <text x={qxStart + 12} y={py + rowH / 2 + 5} fontSize={13} fontWeight={600} fill="var(--cdl-text, #1a1f2a)">
156
+ {it.title}
157
+ </text>
158
+ </g>
159
+ );
160
+ });
161
+ })}
162
+ </g>
163
+ );
164
+ }
@@ -0,0 +1,179 @@
1
+ import { textWidth } from "./text-width";
2
+
3
+ /**
4
+ * Storage / class / generic kind の rows[] を DB table / UML class box 風に揃えるための共通 helper。
5
+ *
6
+ * 各 row を「左列 (label / field 名)」 と「右列 (value / type)」 に分割する。
7
+ * delimiter は ` -> ` / ` → ` / ` = ` / `: ` / `:` の優先順 (最長一致 + 視認性順)。
8
+ * 分割不能な row は left のみ (right="") を返す。
9
+ *
10
+ * char width 14px は font-size 26 + monospace 想定の経験値 (旧 fontSize 20 → 11px から
11
+ * cdl PR #71 で fontSize 26 → 14 に追従)。 visual ピクセル換算は近似で十分。
12
+ * 厳密な glyph 幅は SVG `getComputedTextLength()` 必要だが、 SSR (Node) では使えないため
13
+ * 文字数 × 固定 14px で代替する。 日本語など全角文字は 2 倍幅でカウント (字種判定)。
14
+ */
15
+
16
+ /**
17
+ * `generic` の行の左列を測る時に使っていた 1 文字あたりの幅。
18
+ *
19
+ * **もう使わない**。 左列は等幅ではなく比例の書体で描くため、 字数に一定を掛ける形では実物に
20
+ * 合わない (`#425`)。 いまは `rowColumnWidth` が字ごとの幅で見積もる。
21
+ *
22
+ * 値そのものは公開したまま残す = 呼出側 (`cardene777/dragon`) の検査が期待値の説明にこの数を
23
+ * 書いており、 消すと読めなくなる。
24
+ *
25
+ * @deprecated `rowColumnWidth` を使う
26
+ */
27
+ export const ROW_CHAR_WIDTH_PX = 14;
28
+
29
+ /** `generic` の行の字の大きさ。 左は比例、 右は等幅で描かれる。 */
30
+ export const GENERIC_ROW_LEFT_SIZE_PX = 18;
31
+ export const GENERIC_ROW_RIGHT_SIZE_PX = 21;
32
+
33
+ /**
34
+ * 等幅書体 (JetBrains Mono) の 1 文字の送り幅を font size 比で表した値。
35
+ *
36
+ * 実測 = font size 26 で 1 文字 15.6 (5 文字の `email` が 78)。 `ROW_CHAR_WIDTH_PX` の 14 は
37
+ * これより小さく、 列の X を決める近似としては字形の実寸に届かない。 近似のまま線を引くと
38
+ * 左列の字形に触れる (`#419`)。
39
+ *
40
+ * 列の X (`rightColX`) と箱の幅の見積りは近似のままにしてある。 直すと全ての図で列位置と
41
+ * 箱の幅が動くため、 別 Issue で扱う。
42
+ */
43
+ export const MONO_ADVANCE_RATIO = 0.6;
44
+
45
+ /**
46
+ * `storage` の 1 文字の送り幅。 font size 26 の等幅で実測 15.6。
47
+ *
48
+ * `ROW_CHAR_WIDTH_PX` の 14 は実測に届かず、 列の X も箱の幅も足りない側にずれる。 列の間に
49
+ * 縦線を引く以上、 このずれが余白を食って線と文字が触れる (`#419`)。 `storage` だけ実測に
50
+ * 合わせた値を使う。
51
+ *
52
+ * `generic` は font size が違う (左 18 / 右 21) ため本値は使えない。 そちらの見直しは
53
+ * `cardene777/cdl#425`。
54
+ */
55
+ export const STORAGE_CHAR_ADVANCE_PX = 26 * MONO_ADVANCE_RATIO;
56
+
57
+ /** row 左端 padding (storage node 左 inside) */
58
+ export const ROW_PAD_X_PX = 26;
59
+
60
+ /** 左列 / 右列の間の gap (区切り感を出す最小幅) */
61
+ export const ROW_COL_GAP_PX = 16;
62
+
63
+ /**
64
+ * `storage` の左列 / 右列の間の gap。
65
+ *
66
+ * 間に縦の区切り線を引くため (`#419`)、 線の左右に文字が触れない余白が要る。 16 だと線を
67
+ * 中央に置いて片側 4 しか残らず、 文字が線に触れて見えた (実測)。 片側 16 を確保する。
68
+ */
69
+ export const STORAGE_COL_GAP_PX = 32;
70
+
71
+ /** row 右端 padding (右列が node 右端に被らない margin) */
72
+ export const ROW_RIGHT_PAD_PX = 26;
73
+
74
+ /**
75
+ * delimiter 候補。 priority 順 (最長一致 + 視認性順)。
76
+ * 多文字 delimiter (` -> ` / ` → ` / ` = `) を先に検査し、 単文字 `:` は最後の fallback。
77
+ */
78
+ const DELIMITERS = [" -> ", " → ", " = ", ": ", ":"] as const;
79
+
80
+ /**
81
+ * row を左列 / 右列に分割。 delimiter 不在なら left のみ。
82
+ *
83
+ * 例:
84
+ * - `"client: 100"` → `{ left: "client", right: "100" }`
85
+ * - `"count = 42"` → `{ left: "count", right: "42" }`
86
+ * - `"a -> b"` → `{ left: "a", right: "b" }`
87
+ * - `"+login(): void"` → `{ left: "+login()", right: "void" }`
88
+ * - `"unparsed"` → `{ left: "unparsed", right: "" }`
89
+ *
90
+ * `split` は delimiter が在ったかを表す。 右列の中身が空でも「列に分かれている」 ことは
91
+ * 変わらない (`"status: "` や、 値が signal で空になる行)。 中身の有無で列構造を判定すると、
92
+ * 値が空になった瞬間だけ列が消える。
93
+ */
94
+ export function splitRow(row: string): { left: string; right: string; split: boolean } {
95
+ for (const d of DELIMITERS) {
96
+ const idx = row.indexOf(d);
97
+ if (idx > 0) {
98
+ return {
99
+ left: row.slice(0, idx).trim(),
100
+ right: row.slice(idx + d.length).trim(),
101
+ split: true,
102
+ };
103
+ }
104
+ }
105
+ return { left: row.trim(), right: "", split: false };
106
+ }
107
+
108
+ /**
109
+ * 行の 1 列の横幅の見積り。
110
+ *
111
+ * 等幅なら字数に送り幅を掛ける。 比例なら字ごとの幅で足す (`textWidth`)。 どちらも **実物
112
+ * 以上** に出る = 下回ると「収まっていない物を収まっていると判定」 する向きに外れる。
113
+ *
114
+ * 等幅と比例を 1 つの関数で分けるのは、 同じ `generic` の行でも左が比例 (18) / 右が等幅 (21)
115
+ * だから。 呼ぶ側が列ごとに選ぶ。
116
+ */
117
+ export function rowColumnWidth(text: string, size: number, mono: boolean): number {
118
+ return mono ? displayWidth(text) * size * MONO_ADVANCE_RATIO : textWidth(text, size);
119
+ }
120
+
121
+ /**
122
+ * 文字列の表示幅を「文字数 × 1 文字幅」 近似で計算。
123
+ * ASCII (半角) = 1、 それ以外 (全角想定 = 日本語 / 絵文字等) = 2 倍幅でカウント。
124
+ * SSR / SVG 双方で同値が出る決定的近似。
125
+ */
126
+ export function displayWidth(text: string): number {
127
+ let w = 0;
128
+ for (const ch of text) {
129
+ // 16-bit code unit が ASCII (printable 32-126) 範囲なら半角扱い、 それ以外は全角扱い。
130
+ const code = ch.codePointAt(0) ?? 0;
131
+ w += code >= 0x20 && code <= 0x7e ? 1 : 2;
132
+ }
133
+ return w;
134
+ }
135
+
136
+ /**
137
+ * rows[] から最低限必要な node width を算出。
138
+ * 左 padding + 左列 max 幅 + col gap + 右列 max 幅 + 右 padding。
139
+ *
140
+ * rows 空 / undefined なら 0 を返す (caller が default w と max() で安全 fallback)。
141
+ *
142
+ * `kind` で書体と字の大きさが変わる。 行を等幅の 26 で描くのは `storage` だけで、 列の間に
143
+ * 縦線を引くため間隔も広い (32)。 それ以外は `generic` の書式 (左が比例の 18 / 右が等幅の 21)。
144
+ *
145
+ * **`kind` を渡さない呼出には広い方を返す**。 どちらの書式で描かれるか決められないので、
146
+ * 狭い方を返すと行が箱からはみ出す。 広い方なら余白が増えるだけで済む。 呼び出し側が種別を
147
+ * 知っているなら渡した方が箱は締まる (`cardene777/dragon#1147`)。
148
+ *
149
+ * 上に丸めるのは、 送り幅が小数のため幅が小数になり、 図の座標が読みにくくなるから。 下に
150
+ * 丸めると 1 未満だけ足りない箱ができる。
151
+ */
152
+ export function requiredRowsWidth(rows: string[] | undefined, kind?: string): number {
153
+ if (!rows || rows.length === 0) return 0;
154
+ const splits = rows.map(splitRow);
155
+ const 一番広い = (取る: (s: { left: string; right: string }) => number) =>
156
+ splits.reduce((m, s) => Math.max(m, 取る(s)), 0);
157
+
158
+ /** `storage` = 左右とも等幅の 26。 列の間に縦線を引くため間隔が広い。 */
159
+ const storage =
160
+ ROW_PAD_X_PX +
161
+ 一番広い((s) => rowColumnWidth(s.left, 26, true)) +
162
+ STORAGE_COL_GAP_PX +
163
+ 一番広い((s) => rowColumnWidth(s.right, 26, true)) +
164
+ ROW_RIGHT_PAD_PX;
165
+
166
+ /** `generic` = 左が比例の 18、 右が等幅の 21。 */
167
+ const generic =
168
+ ROW_PAD_X_PX +
169
+ 一番広い((s) => rowColumnWidth(s.left, GENERIC_ROW_LEFT_SIZE_PX, false)) +
170
+ ROW_COL_GAP_PX +
171
+ 一番広い((s) => rowColumnWidth(s.right, GENERIC_ROW_RIGHT_SIZE_PX, true)) +
172
+ ROW_RIGHT_PAD_PX;
173
+
174
+ // **種別が分からない時は広い方を返す**。 どちらで描かれるか決められないので、 狭い方を返すと
175
+ // 行が箱からはみ出す。 広い方なら余白が増えるだけで済む。
176
+ if (kind === undefined) return Math.ceil(Math.max(storage, generic));
177
+ // 行を等幅の 26 で描くのは `storage` だけ。 それ以外は `generic` の書式で描かれる。
178
+ return Math.ceil(kind === "storage" ? storage : generic);
179
+ }