@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,318 @@
1
+ import type { JSX } from "react";
2
+ import type { LaidEdge } from "../types";
3
+ import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
4
+ import { hasTone, toneColor } from "./tone";
5
+ import { EDGE_LABEL_TEXT, fontFamilyAttr, type EdgeLabelLine } from "../label-text";
6
+ import { interpolate, pathSubpath, resolveNumericAttr, shrinkPathEnd } from "./utils";
7
+
8
+ /**
9
+ * 不透明度が 1 の行では属性自体を出さない。 SSOT (`EDGE_LABEL_TEXT`) を 1 未満に変えれば
10
+ * 描画にも必ず現れる = 検査が読む値と描かれる値が食い違わない。
11
+ */
12
+ export function opacityAttr(opacity: number): number | undefined {
13
+ // `opacity || undefined` と書いてはいけない。 0 は falsy なので完全に透明な指定が
14
+ // 「不透明度の指定なし」 = 不透明に化ける。 比較で判定する。
15
+ return opacity < 1 ? opacity : undefined;
16
+ }
17
+
18
+ function labelTextOpacity(line: EdgeLabelLine): number | undefined {
19
+ return opacityAttr(EDGE_LABEL_TEXT[line].opacity);
20
+ }
21
+
22
+ export function CdlEdgeView({
23
+ edge,
24
+ active,
25
+ progress,
26
+ stateValues,
27
+ }: {
28
+ edge: LaidEdge;
29
+ active: boolean;
30
+ progress: number;
31
+ stateValues?: Record<string, string>;
32
+ }): JSX.Element {
33
+ const color = toneColor(edge.tone);
34
+ // 光っていない edge も tone の色で描く。 以前は一律の灰色に落としていたため、 何も光っていない
35
+ // 静的な図では全 edge が同じ色になり、 因果の向き (警戒 = 赤 / 良好 = 緑) が線から読めなかった。
36
+ // 光っている / いないの区別は、 実線は太さ (5 対 2.5) と線種 (実線対破線)、 点線は太さ (5 対 4)
37
+ // と進行点の粒子が担うので、 色で区別する必要はない。
38
+ // 矢頭は大きさだけ従来通り分ける (光っている 14 / いない 10)。
39
+ //
40
+ // 不透明度は光っていない側を 0.9 に置く。 0.5 では実効色が紙に寄りすぎて、 色が付いても線を
41
+ // 追えない。 0.9 という下限は利用側が色をどこまで濃くできるかで決まる = cdl の既定の TONE は
42
+ // 紙 (#fcf8ee / #f8ecd0) に対してどの不透明度でも 4.5:1 に届かない (0.9 で 2.39-4.53、
43
+ // 1.0 でも 2.92-5.55)。 dragon は edge 用に濃くした色 (#0063a1 等) を CSS で当てており、
44
+ // その色なら 0.9 で 4.51-4.92 と 4.5:1 を満たす (0.8 では 3.69-4.04 で届かない)。
45
+ // 実線と点線で同じ組を使い、 光らせた側が薄くなる逆転を作らない。
46
+ const marker = hasTone(edge.tone)
47
+ ? `cdl-arrow-${edge.tone}${active ? "" : "-sm"}`
48
+ : "cdl-arrow-muted";
49
+
50
+ const style = edge.style ?? "solid";
51
+ const isDotted = style === "dotted-flow";
52
+ const isPassThrough = isDotted && Boolean(edge.dThrough);
53
+
54
+ const lineDotsD = isDotted ? shrinkPathEnd(edge.d, 12) : edge.d;
55
+ const glowPathId = `cdl-edge-${edge.id}`;
56
+ // particle path = visible path に統一 (verify:dom で particle-out-of-path 19 件を解消)。
57
+ // 旧実装は passthrough edge で dThrough (node center 直線) を particle path にして visible path
58
+ // (orthogonal routing) と乖離させていたが、 視覚的にも整合性的にも問題なので統一。
59
+ // 貫通効果が必要な場合は別途 phase activation で表現する。
60
+ const glowPathD = edge.d;
61
+ void isPassThrough;
62
+
63
+ return (
64
+ <g
65
+ data-cdl-edge={edge.id}
66
+ data-cdl-from={edge.from}
67
+ data-cdl-to={edge.to}
68
+ data-cdl-active={active ? "true" : "false"}
69
+ data-cdl-path-d={edge.d}
70
+ data-cdl-label-x={edge.labelX}
71
+ data-cdl-label-y={edge.labelY}
72
+ data-cdl-edge-label={edge.label}
73
+ data-cdl-edge-sub={edge.sub ?? ""}
74
+ data-cdl-tone={edge.tone}
75
+ >
76
+ {isDotted ? (
77
+ <g>
78
+ <path id={glowPathId} d={glowPathD} fill="none" stroke="none" />
79
+ <path
80
+ data-cdl-role="edge-line"
81
+ d={lineDotsD}
82
+ fill="none"
83
+ stroke={color}
84
+ strokeOpacity={active ? 0.95 : 0.9}
85
+ strokeWidth={active ? 5 : 4}
86
+ strokeDasharray="0 18"
87
+ strokeLinecap="round"
88
+ style={{
89
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
90
+ }}
91
+ />
92
+ {active && !isPassThrough && <EdgeGlowCircles color={color} edgeId={edge.id} />}
93
+ </g>
94
+ ) : (
95
+ <SolidEdgePath
96
+ edge={edge}
97
+ active={active}
98
+ progress={progress}
99
+ color={color}
100
+ marker={marker}
101
+ stateValues={stateValues}
102
+ />
103
+ )}
104
+ {active && style === "solid" && (
105
+ <path
106
+ data-cdl-role="edge-glow"
107
+ d={edge.d}
108
+ fill="none"
109
+ stroke={color}
110
+ strokeOpacity={0.15}
111
+ strokeWidth={14}
112
+ strokeLinecap="round"
113
+ />
114
+ )}
115
+ </g>
116
+ );
117
+ }
118
+
119
+ /**
120
+ * SolidEdgePath ... solid edge の path を progress 連動で「phase 開始時 0 → 進行と共に伸びる」 動きで描画。
121
+ * pathSubpath(d, progress) で d 自体を progress * totalLength の地点まで切り詰めて、
122
+ * SVG <path d=... markerEnd=...> で marker を path 末端 (= 現在進行位置) に atomic 配置。
123
+ */
124
+ function SolidEdgePath({
125
+ edge,
126
+ active,
127
+ progress,
128
+ color,
129
+ marker,
130
+ stateValues,
131
+ }: {
132
+ edge: LaidEdge;
133
+ active: boolean;
134
+ progress: number;
135
+ color: string;
136
+ marker: string;
137
+ stateValues?: Record<string, string>;
138
+ }): JSX.Element {
139
+ const visibleD = active ? pathSubpath(edge.d, progress) : edge.d;
140
+ // signal binding: widthBind / strokeBind / dashOffsetBind (CAR edge-signal-binding)
141
+ const defaultWidth = active ? 5 : 2.5;
142
+ const boundWidth = edge.widthBind && stateValues
143
+ ? resolveNumericAttr(edge.widthBind, stateValues, defaultWidth)
144
+ : defaultWidth;
145
+ const boundStroke = edge.strokeBind && stateValues
146
+ ? interpolate(edge.strokeBind, stateValues)
147
+ : color;
148
+ const boundDashOffset = edge.dashOffsetBind && stateValues
149
+ ? resolveNumericAttr(edge.dashOffsetBind, stateValues, 0)
150
+ : undefined;
151
+ // dashoffset は dasharray 前提、 signal 経由の流れる線を出したい時は "6 6" 等
152
+ const dashArray = active
153
+ ? boundDashOffset !== undefined ? "8 6" : undefined
154
+ : "4 5";
155
+ return (
156
+ <path
157
+ data-cdl-role="edge-line"
158
+ d={visibleD}
159
+ fill="none"
160
+ stroke={boundStroke}
161
+ strokeOpacity={active ? 0.95 : 0.9}
162
+ strokeWidth={boundWidth}
163
+ strokeDasharray={dashArray}
164
+ strokeDashoffset={boundDashOffset}
165
+ strokeLinecap="round"
166
+ markerEnd={`url(#${marker})`}
167
+ style={{
168
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
169
+ }}
170
+ />
171
+ );
172
+ }
173
+
174
+ /**
175
+ * EdgeGlowCircles ... 進行点 glow 3 重円 (28/18/11px) を mpath で path 沿い移動。
176
+ */
177
+ function EdgeGlowCircles({ color, edgeId }: { color: string; edgeId: string }): JSX.Element {
178
+ const ref = `#cdl-edge-${edgeId}`;
179
+ return (
180
+ <g data-cdl-particle={edgeId} data-cdl-particle-path-ref={ref}>
181
+ <circle r={28} fill={color}>
182
+ <animateMotion dur="2s" begin="-1s" repeatCount="indefinite" rotate="auto">
183
+ <mpath href={ref} />
184
+ </animateMotion>
185
+ <animate
186
+ attributeName="opacity"
187
+ values="0;0.18;0.18;0"
188
+ keyTimes="0;0.05;0.95;1"
189
+ dur="2s"
190
+ begin="-1s"
191
+ repeatCount="indefinite"
192
+ />
193
+ </circle>
194
+ <circle r={18} fill={color}>
195
+ <animateMotion dur="2s" begin="-1s" repeatCount="indefinite" rotate="auto">
196
+ <mpath href={ref} />
197
+ </animateMotion>
198
+ <animate
199
+ attributeName="opacity"
200
+ values="0;0.45;0.45;0"
201
+ keyTimes="0;0.05;0.95;1"
202
+ dur="2s"
203
+ begin="-1s"
204
+ repeatCount="indefinite"
205
+ />
206
+ </circle>
207
+ <circle r={11} fill={color}>
208
+ <animateMotion dur="2s" begin="-1s" repeatCount="indefinite" rotate="auto">
209
+ <mpath href={ref} />
210
+ </animateMotion>
211
+ <animate
212
+ attributeName="opacity"
213
+ values="0;1;1;0"
214
+ keyTimes="0;0.05;0.95;1"
215
+ dur="2s"
216
+ begin="-1s"
217
+ repeatCount="indefinite"
218
+ />
219
+ </circle>
220
+ </g>
221
+ );
222
+ }
223
+
224
+ /** dotted-through edge の 進行点 glow を 2nd pass で描画 (粒子が node を貫通する Legacy 表現) */
225
+ export function CdlEdgeGlowView({
226
+ edge,
227
+ active,
228
+ }: {
229
+ edge: LaidEdge;
230
+ active: boolean;
231
+ }): JSX.Element | null {
232
+ if (!active) return null;
233
+ const color = toneColor(edge.tone);
234
+ return <EdgeGlowCircles color={color} edgeId={edge.id} />;
235
+ }
236
+
237
+ /** edge の label box を最前面 (nodes より後) で描画する 2 pass 設計の component */
238
+ export function CdlEdgeLabelView({
239
+ edge,
240
+ active,
241
+ }: {
242
+ edge: LaidEdge;
243
+ active: boolean;
244
+ }): JSX.Element | null {
245
+ // routing v9 = 空 label / 空 sub の edge は DOM 描画自体を skip、
246
+ // collisions.ts 側 (bbox 生成 skip、 CAR-430 で SSOT 統一) と対称に render 層でも noop。
247
+ // 空 label でも rect + text で 12.5x12.5 の小 marker が残り、 dragon sweep で
248
+ // node bbox と数十 px² の実測 overlap を生む (mind-demo 61px² 事象)。
249
+ if (!hasRenderedLabel(edge)) return null;
250
+ const color = toneColor(edge.tone);
251
+ // SSOT = spec.ts の computeLabelBBoxWorld (CAR-431 で render / layout / predict-bbox 3 箇所の
252
+ // bbox 計算 drift を解消)。 pill h / w / padding / boxLeftX / boxTopY は本 helper 経由で取得、
253
+ // <g transform="translate(labelX, labelY)"> 内 rect は相対座標なので world x/y を helper から
254
+ // 引き算して rect x/y に渡す。
255
+ const bbox = computeLabelBBoxWorld(edge);
256
+ const boxW = bbox.w;
257
+ const boxH = bbox.h;
258
+ const boxLeftX = bbox.x - edge.labelX;
259
+ const boxTopY = bbox.y - edge.labelY;
260
+ const anchor = edge.labelAnchor;
261
+ const textX = anchor === "start" ? LABEL_PILL_PAD_X : anchor === "end" ? -LABEL_PILL_PAD_X : 0;
262
+ // 2 行 label の text y 座標。 CAR-581 で pill 中心 = labelY 対称化に統一、 main / sub 行を pill
263
+ // 中心 (translate(labelX, labelY) の原点) から上下均等に配置する:
264
+ // sub あり = line1Y = -6 (main text baseline を中心より上) / line2Y = 18 (sub text baseline を中心より下)
265
+ // sub なし = line1Y = 6 (main text 単独、 baseline 調整で中心近傍)
266
+ const line1Y = edge.sub ? -6 : 6;
267
+ const line2Y = 18;
268
+
269
+ return (
270
+ <g
271
+ transform={`translate(${edge.labelX} ${edge.labelY})`}
272
+ data-cdl-edge-label-for={edge.id}
273
+ data-cdl-edge-label-text={edge.label}
274
+ data-cdl-edge-sub-text={edge.sub ?? ""}
275
+ >
276
+ <rect
277
+ data-cdl-role="edge-label-bg"
278
+ x={boxLeftX}
279
+ y={boxTopY}
280
+ width={boxW}
281
+ height={boxH}
282
+ rx={10}
283
+ fill="var(--cdl-label-bg, #ffffff)"
284
+ stroke={active ? color : "var(--cdl-divider, #d4d4d8)"}
285
+ strokeOpacity={active ? 0.9 : 0.55}
286
+ strokeWidth={1.5}
287
+ opacity={active ? 1 : 0.92}
288
+ />
289
+ <text
290
+ data-cdl-role="edge-label"
291
+ x={textX}
292
+ y={line1Y}
293
+ fontSize={EDGE_LABEL_TEXT.main.fontSize}
294
+ fontWeight={EDGE_LABEL_TEXT.main.fontWeight}
295
+ textAnchor={anchor}
296
+ fill={active ? color : "var(--cdl-text-dim, #5a6270)"}
297
+ opacity={labelTextOpacity("main")}
298
+ >
299
+ {edge.label}
300
+ </text>
301
+ {edge.sub && (
302
+ <text
303
+ data-cdl-role="edge-label"
304
+ x={textX}
305
+ y={line2Y}
306
+ fontSize={EDGE_LABEL_TEXT.sub.fontSize}
307
+ fontWeight={EDGE_LABEL_TEXT.sub.fontWeight}
308
+ textAnchor={anchor}
309
+ fill={active ? color : "var(--cdl-text-dim, #5a6270)"}
310
+ opacity={labelTextOpacity("sub")}
311
+ style={{ fontFamily: fontFamilyAttr("sub") }}
312
+ >
313
+ {edge.sub}
314
+ </text>
315
+ )}
316
+ </g>
317
+ );
318
+ }
@@ -0,0 +1,146 @@
1
+ import type { JSX } from "react";
2
+ import type { CdlPhase } from "../types";
3
+
4
+ export function CdlHeader({
5
+ topic,
6
+ phases,
7
+ currentPhase,
8
+ phaseIdx,
9
+ stateValues,
10
+ states,
11
+ onJumpPhase,
12
+ headingLevel = 3,
13
+ }: {
14
+ topic: string;
15
+ phases: CdlPhase[];
16
+ currentPhase: CdlPhase;
17
+ phaseIdx: number;
18
+ stateValues: Record<string, string>;
19
+ states: { id: string; initial: number | string }[];
20
+ onJumpPhase: (idx: number) => void;
21
+ /** phase title 描画時の heading tag level (2 | 3 | 4)、 default 3 で従来動作維持。
22
+ * downstream で h1 直後に配置する場合は 2、 h2 直後なら 3 (default)、 h3 直後なら 4。 */
23
+ headingLevel?: 2 | 3 | 4;
24
+ }): JSX.Element {
25
+ const HeadingTag = (`h${headingLevel}` as "h2" | "h3" | "h4");
26
+ return (
27
+ <div className="border-b border-[var(--cdl-divider,#d4d4d8)]/35 px-7 py-6">
28
+ <div className="mb-5 flex items-center gap-3">
29
+ <div className="text-[10px] font-bold uppercase tracking-[0.2em]" style={{ color: "var(--cdl-text-accent, #2d6a8f)" }}>{topic}</div>
30
+ <div className="ml-auto flex items-center gap-1.5">
31
+ {phases.map((p, idx) => {
32
+ const active = idx === phaseIdx;
33
+ const done = idx < phaseIdx;
34
+ return (
35
+ <button
36
+ key={p.id}
37
+ type="button"
38
+ onClick={() => onJumpPhase(idx)}
39
+ aria-current={active ? "step" : undefined}
40
+ aria-label={`phase ${idx + 1}${p.title ? `: ${p.title}` : ""}`}
41
+ className="flex items-center gap-1.5 bg-transparent p-0"
42
+ style={{ cursor: "pointer", border: 0 }}
43
+ >
44
+ <span
45
+ className="inline-block transition-all duration-300"
46
+ style={{
47
+ width: active ? 22 : 8,
48
+ height: 8,
49
+ borderRadius: 4,
50
+ background: active ? "var(--cdl-text-accent, #2d6a8f)" : done ? "color-mix(in srgb, var(--cdl-text-accent, #2d6a8f) 50%, transparent)" : "var(--cdl-divider, #d4d4d8)",
51
+ }}
52
+ />
53
+ </button>
54
+ );
55
+ })}
56
+ <span className="ml-2 text-[11px] font-semibold text-[color:var(--cdl-text-dim,#5a6270)]">
57
+ {phaseIdx + 1} / {phases.length}
58
+ </span>
59
+ </div>
60
+ </div>
61
+ <div className="grid items-start gap-6 sm:grid-cols-[1fr_auto]">
62
+ <div className="space-y-2">
63
+ <div className="flex items-baseline gap-3">
64
+ <span
65
+ className="grid h-8 w-8 place-items-center rounded-full text-xs font-bold text-white"
66
+ style={{
67
+ background: "linear-gradient(135deg, var(--cdl-text-accent, #2d6a8f) 0%, var(--cdl-tone-accent-deep, #1f4d6e) 100%)",
68
+ boxShadow: "0 2px 4px color-mix(in srgb, var(--cdl-text-accent, #2d6a8f) 30%, transparent)",
69
+ }}
70
+ >
71
+ {String(phaseIdx + 1).padStart(2, "0")}
72
+ </span>
73
+ <HeadingTag className="text-xl font-bold text-[color:var(--cdl-text,#1a1f2a)] sm:text-2xl">{currentPhase.title}</HeadingTag>
74
+ </div>
75
+ <p className="max-w-2xl text-sm leading-relaxed text-[color:var(--cdl-text-dim,#5a6270)]">{currentPhase.body}</p>
76
+ </div>
77
+ {states.length > 0 && (
78
+ <StateDiffCard states={states} stateValues={stateValues} badge={currentPhase.badge} />
79
+ )}
80
+ </div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function StateDiffCard({
86
+ states,
87
+ stateValues,
88
+ badge,
89
+ }: {
90
+ states: { id: string; initial: number | string }[];
91
+ stateValues: Record<string, string>;
92
+ badge?: string;
93
+ }): JSX.Element {
94
+ return (
95
+ <div
96
+ className="rounded-[14px] px-4 py-3 text-xs"
97
+ style={{
98
+ background: "var(--cdl-label-bg, #ffffff)",
99
+ border: "1px solid var(--cdl-divider, #d4d4d8)",
100
+ }}
101
+ >
102
+ <div className="mb-2 flex items-center justify-between gap-3">
103
+ <span className="font-bold tracking-wider text-[color:var(--cdl-text-dim,#5a6270)]">このステップの差分</span>
104
+ {badge && (
105
+ <span
106
+ className="rounded-full px-2 py-0.5 text-[10px] font-bold"
107
+ style={{
108
+ background: "color-mix(in srgb, var(--cdl-text-accent, #2d6a8f) 10%, transparent)",
109
+ color: "var(--cdl-text-accent, #2d6a8f)",
110
+ }}
111
+ >
112
+ {badge}
113
+ </span>
114
+ )}
115
+ </div>
116
+ <table className="w-full" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
117
+ <thead>
118
+ <tr className="text-[10px] tracking-wider" style={{ color: "var(--cdl-text-mute, #8a8678)" }}>
119
+ <th className="pb-1 text-left">変数</th>
120
+ <th className="pb-1 text-right">初期</th>
121
+ <th className="pb-1 text-right">現在</th>
122
+ </tr>
123
+ </thead>
124
+ <tbody style={{ color: "var(--cdl-text, #1a1f2a)" }}>
125
+ {states.map((s) => {
126
+ const initial = String(s.initial);
127
+ const current = stateValues[s.id] ?? initial;
128
+ const changed = initial !== current;
129
+ return (
130
+ <tr key={s.id}>
131
+ <td className="py-1 text-left text-[12px]">{s.id}</td>
132
+ <td className="py-1 text-right text-[12px]" style={{ color: "var(--cdl-text-mute, #8a8678)" }}>{initial}</td>
133
+ <td
134
+ className="py-1 text-right text-[13px] font-bold"
135
+ style={{ color: changed ? "var(--cdl-text-accent, #2d6a8f)" : "var(--cdl-text, #1a1f2a)" }}
136
+ >
137
+ {current}
138
+ </td>
139
+ </tr>
140
+ );
141
+ })}
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ );
146
+ }