@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.
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/SPEC.md +374 -0
- package/dist/index.cjs +28085 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3415 -0
- package/dist/index.d.ts +3415 -0
- package/dist/index.js +27962 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +23043 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +23041 -0
- package/dist/react.js.map +1 -0
- package/dist/render-C78lIXeC.d.cts +2449 -0
- package/dist/render-C78lIXeC.d.ts +2449 -0
- package/examples/quick-start.md +88 -0
- package/package.json +79 -0
- package/src/anim/core/easing.ts +77 -0
- package/src/anim/core/timeline.ts +335 -0
- package/src/anim/core/types.ts +42 -0
- package/src/anim/index.ts +20 -0
- package/src/anim/react/index.ts +3 -0
- package/src/anim/react/useReducedMotion.ts +27 -0
- package/src/anim/react/useTimeline.ts +48 -0
- package/src/assert-never.ts +16 -0
- package/src/author-intent-verify.ts +587 -0
- package/src/builder.ts +3740 -0
- package/src/compile.ts +12 -0
- package/src/dom-verify-core.ts +121 -0
- package/src/dom-verify-types.ts +21 -0
- package/src/dom-verify.ts +948 -0
- package/src/event-handler/index.ts +184 -0
- package/src/formula/ast.ts +46 -0
- package/src/formula/evaluator.ts +163 -0
- package/src/formula/formula-computeds.ts +83 -0
- package/src/formula/index.ts +5 -0
- package/src/formula/parser.ts +399 -0
- package/src/index.ts +284 -0
- package/src/input-signals.ts +74 -0
- package/src/kinds/actor.tsx +79 -0
- package/src/kinds/card.tsx +61 -0
- package/src/kinds/chart-bar.tsx +121 -0
- package/src/kinds/chart-line.tsx +163 -0
- package/src/kinds/chart-pie.tsx +107 -0
- package/src/kinds/compact-title.ts +164 -0
- package/src/kinds/dyn-shape.tsx +394 -0
- package/src/kinds/event.tsx +70 -0
- package/src/kinds/function.tsx +53 -0
- package/src/kinds/funnel.tsx +122 -0
- package/src/kinds/gantt.tsx +193 -0
- package/src/kinds/generic.tsx +368 -0
- package/src/kinds/mind-map.tsx +367 -0
- package/src/kinds/mind-radial.tsx +209 -0
- package/src/kinds/node-tone.ts +18 -0
- package/src/kinds/quadrant.tsx +164 -0
- package/src/kinds/row-align.ts +179 -0
- package/src/kinds/shape-basement.tsx +683 -0
- package/src/kinds/shape-blockchain.tsx +750 -0
- package/src/kinds/shape-commerce.tsx +553 -0
- package/src/kinds/shape-finance.tsx +706 -0
- package/src/kinds/shape-hardware.tsx +862 -0
- package/src/kinds/shape-people.tsx +439 -0
- package/src/kinds/shape-region.tsx +383 -0
- package/src/kinds/shape-software.tsx +859 -0
- package/src/kinds/storage.tsx +180 -0
- package/src/kinds/text-width.ts +73 -0
- package/src/kinds/tree.tsx +275 -0
- package/src/kinds/user-journey.tsx +240 -0
- package/src/label-text.ts +71 -0
- package/src/layout/clearance-constants.ts +47 -0
- package/src/layout/collisions.ts +2078 -0
- package/src/layout/edges.ts +2498 -0
- package/src/layout/footer-shape.ts +97 -0
- package/src/layout/geometry.ts +135 -0
- package/src/layout/label-shift.ts +28 -0
- package/src/layout/lanes.ts +328 -0
- package/src/layout/nodes.ts +190 -0
- package/src/layout/predict-bbox.ts +76 -0
- package/src/layout/px-projection.ts +176 -0
- package/src/layout/spec.ts +872 -0
- package/src/layout/text-width.ts +133 -0
- package/src/layout/tokens.ts +181 -0
- package/src/layout/viewbox.ts +47 -0
- package/src/layout-with-validation.ts +175 -0
- package/src/layout.ts +381 -0
- package/src/presets.ts +2108 -0
- package/src/reactive/batch.ts +48 -0
- package/src/reactive/computed.ts +85 -0
- package/src/reactive/effect.ts +145 -0
- package/src/reactive/index.ts +6 -0
- package/src/reactive/internal.ts +109 -0
- package/src/reactive/signal.ts +113 -0
- package/src/render/edges.tsx +318 -0
- package/src/render/header.tsx +146 -0
- package/src/render/interactive-panel.tsx +9620 -0
- package/src/render/nodes.tsx +319 -0
- package/src/render/stage.tsx +377 -0
- package/src/render/tone.ts +64 -0
- package/src/render/utils.ts +238 -0
- package/src/render.tsx +221 -0
- package/src/scroll-trigger/index.ts +109 -0
- package/src/scroll-trigger/progress.ts +49 -0
- package/src/thumbnail.tsx +114 -0
- package/src/types.ts +2371 -0
- package/src/validate.ts +268 -0
- package/src/visual-validate.ts +4381 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { LaidNode, Tone } from "../types";
|
|
3
|
+
import { TONE } from "../render/tone";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* funnel-stages kind ... 1 node に stage 配列 (funnelData) を保持し、 逆三角形 funnel を SVG 描画する。
|
|
7
|
+
* layout = 全体で自然な逆三角形になるように上端 100% → 下端 40% で線形補間、
|
|
8
|
+
* 各 stage は同じ縦幅 + 上下辺の幅を段階的に減らして「じょうご」 形状で表現。
|
|
9
|
+
* count / drop rate は polygon の右横 (side legend) に配置して図形と数値を分離。
|
|
10
|
+
*/
|
|
11
|
+
export function FunnelNode({ node, active }: { node: LaidNode; active: boolean }): JSX.Element {
|
|
12
|
+
const x0 = node.cx - node.w / 2;
|
|
13
|
+
const y0 = node.cy - node.h / 2;
|
|
14
|
+
const stages = node.funnelData ?? [];
|
|
15
|
+
|
|
16
|
+
const PAD_TOP = 32;
|
|
17
|
+
const PAD_RIGHT = 24;
|
|
18
|
+
const PAD_BOTTOM = 32;
|
|
19
|
+
const PAD_LEFT = 24;
|
|
20
|
+
const legendW = 160;
|
|
21
|
+
const canvasW = node.w - PAD_LEFT - PAD_RIGHT - legendW;
|
|
22
|
+
const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
|
|
23
|
+
|
|
24
|
+
const stageCount = stages.length || 1;
|
|
25
|
+
const gap = 4;
|
|
26
|
+
const stageH = (canvasH - gap * (stageCount - 1)) / stageCount;
|
|
27
|
+
|
|
28
|
+
const widthAtStep = (step: number): number => {
|
|
29
|
+
const topRatio = 1.0;
|
|
30
|
+
const bottomRatio = 0.35;
|
|
31
|
+
const t = step / stageCount;
|
|
32
|
+
return canvasW * (topRatio + (bottomRatio - topRatio) * t);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<g transform={`translate(${x0} ${y0})`}>
|
|
37
|
+
<rect
|
|
38
|
+
data-cdl-role="node-body"
|
|
39
|
+
x={0}
|
|
40
|
+
y={0}
|
|
41
|
+
width={node.w}
|
|
42
|
+
height={node.h}
|
|
43
|
+
rx={16}
|
|
44
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
45
|
+
stroke={active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)"}
|
|
46
|
+
strokeWidth={active ? 3 : 1.25}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
{stages.map((s, idx) => {
|
|
50
|
+
const prev = idx > 0 ? stages[idx - 1]! : null;
|
|
51
|
+
const dropRate = prev ? ((prev.count - s.count) / prev.count) * 100 : 0;
|
|
52
|
+
const y = PAD_TOP + idx * (stageH + gap);
|
|
53
|
+
const topW = widthAtStep(idx);
|
|
54
|
+
const bottomW = widthAtStep(idx + 1);
|
|
55
|
+
const cx = PAD_LEFT + canvasW / 2;
|
|
56
|
+
const topX = cx - topW / 2;
|
|
57
|
+
const bottomX = cx - bottomW / 2;
|
|
58
|
+
const points = `${topX},${y} ${topX + topW},${y} ${bottomX + bottomW},${y + stageH} ${bottomX},${y + stageH}`;
|
|
59
|
+
const tone = toneByIndex(idx);
|
|
60
|
+
const legendX = PAD_LEFT + canvasW + 24;
|
|
61
|
+
return (
|
|
62
|
+
<g key={`stage-${s.id}`}>
|
|
63
|
+
<polygon
|
|
64
|
+
data-cdl-role="funnel-stage"
|
|
65
|
+
points={points}
|
|
66
|
+
fill={tone}
|
|
67
|
+
fillOpacity={0.9}
|
|
68
|
+
stroke={tone}
|
|
69
|
+
strokeWidth={0}
|
|
70
|
+
/>
|
|
71
|
+
<text
|
|
72
|
+
x={cx}
|
|
73
|
+
y={y + stageH / 2 + 5}
|
|
74
|
+
fontSize={14}
|
|
75
|
+
fontWeight={700}
|
|
76
|
+
textAnchor="middle"
|
|
77
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
78
|
+
>
|
|
79
|
+
{s.title}
|
|
80
|
+
</text>
|
|
81
|
+
<text
|
|
82
|
+
x={legendX}
|
|
83
|
+
y={y + stageH / 2 - 2}
|
|
84
|
+
fontSize={13}
|
|
85
|
+
fontWeight={700}
|
|
86
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
87
|
+
>
|
|
88
|
+
{s.count.toLocaleString()}
|
|
89
|
+
</text>
|
|
90
|
+
{prev && (
|
|
91
|
+
<text
|
|
92
|
+
x={legendX}
|
|
93
|
+
y={y + stageH / 2 + 14}
|
|
94
|
+
fontSize={11}
|
|
95
|
+
fill={dropRate > 0 ? "var(--cdl-text-dim, #5a6270)" : "var(--cdl-text-mute, #8a8678)"}
|
|
96
|
+
>
|
|
97
|
+
{dropRate > 0 ? `↓ ${dropRate.toFixed(1)}%` : "±0%"}
|
|
98
|
+
</text>
|
|
99
|
+
)}
|
|
100
|
+
</g>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</g>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 段ごとに色を変える。 色は共有の表から引く (直書きすると表を直した時に取り残される)。
|
|
108
|
+
const TONE_ORDER: string[] = [
|
|
109
|
+
TONE.accent,
|
|
110
|
+
TONE.teal,
|
|
111
|
+
TONE.success,
|
|
112
|
+
TONE.warning,
|
|
113
|
+
TONE.info,
|
|
114
|
+
TONE.error,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
function toneByIndex(idx: number): string {
|
|
118
|
+
return TONE_ORDER[idx % TONE_ORDER.length]!;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const _tones: Tone = "accent";
|
|
122
|
+
void _tones;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { LaidNode, Tone } from "../types";
|
|
3
|
+
import { TONE } from "../render/tone";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* gantt-timeline kind ... 1 node に task 配列 (ganttData) を保持し、 帯状 timeline を SVG 描画する。
|
|
7
|
+
* layout = 縦軸 task lane (task ごと 1 行)、 横軸日付 (startIdx / endIdx で bar 位置と幅)。
|
|
8
|
+
* dependsOn は task 間の連結 arrow (elbow line + arrowhead) を追加。
|
|
9
|
+
*/
|
|
10
|
+
export function GanttNode({ node, active }: { node: LaidNode; active: boolean }): JSX.Element {
|
|
11
|
+
const x0 = node.cx - node.w / 2;
|
|
12
|
+
const y0 = node.cy - node.h / 2;
|
|
13
|
+
const tasks = node.ganttData ?? [];
|
|
14
|
+
|
|
15
|
+
const PAD_TOP = 32;
|
|
16
|
+
const PAD_RIGHT = 48;
|
|
17
|
+
const PAD_BOTTOM = 44;
|
|
18
|
+
const PAD_LEFT = 156;
|
|
19
|
+
const canvasW = node.w - PAD_LEFT - PAD_RIGHT;
|
|
20
|
+
const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
|
|
21
|
+
|
|
22
|
+
const rowGap = 20;
|
|
23
|
+
const rowCount = tasks.length || 1;
|
|
24
|
+
const rowH = Math.max(28, (canvasH - rowGap * (rowCount + 1)) / rowCount);
|
|
25
|
+
|
|
26
|
+
const maxIdx = Math.max(1, ...tasks.map((t) => t.endIdx + 1));
|
|
27
|
+
const cellW = canvasW / maxIdx;
|
|
28
|
+
|
|
29
|
+
const xAt = (idx: number) => PAD_LEFT + idx * cellW;
|
|
30
|
+
const yAt = (row: number) => PAD_TOP + rowGap + row * (rowH + rowGap);
|
|
31
|
+
|
|
32
|
+
const gridLabels = Array.from({ length: maxIdx }, (_, i) => {
|
|
33
|
+
const t = tasks.find((tt) => tt.startIdx === i);
|
|
34
|
+
return t?.startLabel ?? String(i);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
38
|
+
const rowOf = new Map(tasks.map((t, i) => [t.id, i]));
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<g transform={`translate(${x0} ${y0})`}>
|
|
42
|
+
<rect
|
|
43
|
+
data-cdl-role="node-body"
|
|
44
|
+
x={0}
|
|
45
|
+
y={0}
|
|
46
|
+
width={node.w}
|
|
47
|
+
height={node.h}
|
|
48
|
+
rx={16}
|
|
49
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
50
|
+
stroke={active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)"}
|
|
51
|
+
strokeWidth={active ? 3 : 1.25}
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
{gridLabels.map((label, i) => (
|
|
55
|
+
<g key={`grid-${i}`}>
|
|
56
|
+
<line
|
|
57
|
+
x1={xAt(i)}
|
|
58
|
+
y1={PAD_TOP}
|
|
59
|
+
x2={xAt(i)}
|
|
60
|
+
y2={PAD_TOP + canvasH}
|
|
61
|
+
stroke="var(--cdl-divider, #d4d4d8)"
|
|
62
|
+
strokeWidth={0.75}
|
|
63
|
+
strokeDasharray="3 3"
|
|
64
|
+
opacity={0.5}
|
|
65
|
+
/>
|
|
66
|
+
<text
|
|
67
|
+
x={xAt(i) + cellW / 2}
|
|
68
|
+
y={PAD_TOP + canvasH + 18}
|
|
69
|
+
fontSize={11}
|
|
70
|
+
textAnchor="middle"
|
|
71
|
+
fill="var(--cdl-text-dim, #5a6270)"
|
|
72
|
+
>
|
|
73
|
+
{label}
|
|
74
|
+
</text>
|
|
75
|
+
</g>
|
|
76
|
+
))}
|
|
77
|
+
|
|
78
|
+
<line
|
|
79
|
+
x1={xAt(maxIdx)}
|
|
80
|
+
y1={PAD_TOP}
|
|
81
|
+
x2={xAt(maxIdx)}
|
|
82
|
+
y2={PAD_TOP + canvasH}
|
|
83
|
+
stroke="var(--cdl-divider, #d4d4d8)"
|
|
84
|
+
strokeWidth={0.75}
|
|
85
|
+
opacity={0.5}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
{tasks.map((t, row) => (
|
|
89
|
+
<text
|
|
90
|
+
key={`label-${t.id}`}
|
|
91
|
+
x={PAD_LEFT - 12}
|
|
92
|
+
y={yAt(row) + rowH / 2 + 4}
|
|
93
|
+
fontSize={12}
|
|
94
|
+
textAnchor="end"
|
|
95
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
96
|
+
fontWeight={600}
|
|
97
|
+
>
|
|
98
|
+
{t.title}
|
|
99
|
+
</text>
|
|
100
|
+
))}
|
|
101
|
+
|
|
102
|
+
{tasks.map((t, row) => {
|
|
103
|
+
const bx = xAt(t.startIdx) + 3;
|
|
104
|
+
const by = yAt(row);
|
|
105
|
+
const bw = Math.max(cellW - 6, (t.endIdx - t.startIdx + 1) * cellW - 6);
|
|
106
|
+
return (
|
|
107
|
+
<g key={`bar-${t.id}`}>
|
|
108
|
+
<rect
|
|
109
|
+
data-cdl-role="gantt-bar"
|
|
110
|
+
x={bx}
|
|
111
|
+
y={by}
|
|
112
|
+
width={bw}
|
|
113
|
+
height={rowH}
|
|
114
|
+
rx={4}
|
|
115
|
+
fill={toneColor(t.tone)}
|
|
116
|
+
fillOpacity={0.9}
|
|
117
|
+
/>
|
|
118
|
+
{t.owner && rowH >= 22 && (
|
|
119
|
+
<text
|
|
120
|
+
x={bx + 8}
|
|
121
|
+
y={by + rowH / 2 + 4}
|
|
122
|
+
fontSize={11}
|
|
123
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
124
|
+
fontWeight={600}
|
|
125
|
+
>
|
|
126
|
+
{t.owner}
|
|
127
|
+
</text>
|
|
128
|
+
)}
|
|
129
|
+
</g>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
|
|
133
|
+
{tasks
|
|
134
|
+
.filter((t) => t.dependsOn && taskById.has(t.dependsOn))
|
|
135
|
+
.map((t) => {
|
|
136
|
+
const parent = taskById.get(t.dependsOn!)!;
|
|
137
|
+
const parentRow = rowOf.get(parent.id)!;
|
|
138
|
+
const childRow = rowOf.get(t.id)!;
|
|
139
|
+
const parentBarRight = xAt(parent.endIdx) + cellW - 3;
|
|
140
|
+
const parentMidY = yAt(parentRow) + rowH / 2;
|
|
141
|
+
const childBarLeft = xAt(t.startIdx) + 3;
|
|
142
|
+
const childMidY = yAt(childRow) + rowH / 2;
|
|
143
|
+
const arrowHeadSize = 8;
|
|
144
|
+
// arrow head 三角形の頂点 (= tipX + arrowHeadSize) が bar 左辺の 10px 手前に来る
|
|
145
|
+
// ように配置、 「食い込み」 に見える視覚エラー完全排除
|
|
146
|
+
const arrowHeadTipGap = 10;
|
|
147
|
+
const tipX = childBarLeft - arrowHeadTipGap - arrowHeadSize;
|
|
148
|
+
const tipY = childMidY;
|
|
149
|
+
const startX = parentBarRight + 4;
|
|
150
|
+
// 親 bar の右端と 矢印の手前の間に縦線を通す余裕があるかで経路を分ける。
|
|
151
|
+
//
|
|
152
|
+
// 旧実装は `elbowX = Math.max(startX + 6, tipX - 6)` で、 子 bar が親の直後に始まると
|
|
153
|
+
// `elbowX > tipX` になり、 縦線が矢印より右に引かれてから左へ戻っていた。
|
|
154
|
+
const elbowGap = 6;
|
|
155
|
+
const enterX = tipX - elbowGap;
|
|
156
|
+
const hasRoom = enterX - startX >= elbowGap;
|
|
157
|
+
const path = hasRoom
|
|
158
|
+
// 余裕がある ... 右へ → 下へ → 矢印へ
|
|
159
|
+
? `M ${startX} ${parentMidY} L ${enterX} ${parentMidY} L ${enterX} ${tipY} L ${tipX} ${tipY}`
|
|
160
|
+
// 余裕がない ... 行間を経由して回り込む (右へ → 行間へ → 左へ → 下へ → 矢印へ)
|
|
161
|
+
: `M ${startX} ${parentMidY} L ${startX + elbowGap} ${parentMidY}` +
|
|
162
|
+
` L ${startX + elbowGap} ${(parentMidY + tipY) / 2}` +
|
|
163
|
+
` L ${enterX} ${(parentMidY + tipY) / 2} L ${enterX} ${tipY} L ${tipX} ${tipY}`;
|
|
164
|
+
const arrowHeadPath = `M ${tipX + arrowHeadSize} ${tipY} L ${tipX} ${tipY - arrowHeadSize / 2} L ${tipX} ${tipY + arrowHeadSize / 2} Z`;
|
|
165
|
+
return (
|
|
166
|
+
<g key={`dep-${parent.id}-${t.id}`} data-cdl-role="gantt-arrow">
|
|
167
|
+
<path
|
|
168
|
+
d={path}
|
|
169
|
+
fill="none"
|
|
170
|
+
stroke="var(--cdl-text-mute, #8a8678)"
|
|
171
|
+
strokeWidth={1.75}
|
|
172
|
+
strokeLinejoin="round"
|
|
173
|
+
strokeLinecap="round"
|
|
174
|
+
/>
|
|
175
|
+
<path
|
|
176
|
+
d={arrowHeadPath}
|
|
177
|
+
fill="var(--cdl-text-mute, #8a8678)"
|
|
178
|
+
stroke="var(--cdl-text-mute, #8a8678)"
|
|
179
|
+
strokeWidth={0.5}
|
|
180
|
+
strokeLinejoin="round"
|
|
181
|
+
/>
|
|
182
|
+
</g>
|
|
183
|
+
);
|
|
184
|
+
})}
|
|
185
|
+
</g>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
function toneColor(tone: Tone | undefined): string {
|
|
191
|
+
if (!tone) return TONE.accent;
|
|
192
|
+
return TONE[tone];
|
|
193
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { LaidNode, NodeKind } from "../types";
|
|
3
|
+
import { toneColor } from "./node-tone";
|
|
4
|
+
import { rowBaselineY } from "../layout/spec";
|
|
5
|
+
import {
|
|
6
|
+
GENERIC_ROW_LEFT_SIZE_PX,
|
|
7
|
+
rowColumnWidth,
|
|
8
|
+
ROW_COL_GAP_PX,
|
|
9
|
+
ROW_PAD_X_PX,
|
|
10
|
+
splitRow,
|
|
11
|
+
} from "./row-align";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 汎用 node component。 新 NodeKind 25 種を共通の rect + icon + label で描画する。
|
|
15
|
+
*
|
|
16
|
+
* kind 別の差分。
|
|
17
|
+
* - color (枠線 + eyebrow 色)
|
|
18
|
+
* - icon (SVG path、 node 右上に表示)
|
|
19
|
+
* - shape (default rect、 cylinder / cloud / diamond は別途 path 描画)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
type KindStyle = {
|
|
23
|
+
/** active 枠線色 */
|
|
24
|
+
accent: string;
|
|
25
|
+
/** eyebrow 文字色 (kind 識別) */
|
|
26
|
+
eyebrowColor: string;
|
|
27
|
+
/** icon SVG path d (24x24 viewBox 内、 node 右上に 24x24 で描画) */
|
|
28
|
+
icon: string;
|
|
29
|
+
/** shape: rect (default) / cylinder / cloud / diamond / hexagon */
|
|
30
|
+
shape?: "rect" | "cylinder" | "cloud" | "diamond" | "hexagon";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// 各 kind の visual style (icon は heroicons / lucide 風の minimal SVG path)
|
|
34
|
+
const STYLES: Partial<Record<NodeKind, KindStyle>> = {
|
|
35
|
+
// 人系
|
|
36
|
+
person: {
|
|
37
|
+
accent: "var(--cdl-tone-accent, #2d6a8f)",
|
|
38
|
+
eyebrowColor: "var(--cdl-tone-accent, #2d6a8f)",
|
|
39
|
+
icon: "M12 12c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0 2c-2.7 0-8 1.3-8 4v2h16v-2c0-2.7-5.3-4-8-4z",
|
|
40
|
+
},
|
|
41
|
+
"user-group": {
|
|
42
|
+
accent: "var(--cdl-tone-accent, #2d6a8f)",
|
|
43
|
+
eyebrowColor: "var(--cdl-tone-accent, #2d6a8f)",
|
|
44
|
+
icon: "M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z",
|
|
45
|
+
},
|
|
46
|
+
admin: {
|
|
47
|
+
accent: "var(--cdl-tone-error, #c15a4a)",
|
|
48
|
+
eyebrowColor: "var(--cdl-tone-error, #c15a4a)",
|
|
49
|
+
icon: "M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z",
|
|
50
|
+
},
|
|
51
|
+
developer: {
|
|
52
|
+
accent: "var(--cdl-tone-teal, #2f8770)",
|
|
53
|
+
eyebrowColor: "var(--cdl-tone-teal, #2f8770)",
|
|
54
|
+
icon: "M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z",
|
|
55
|
+
},
|
|
56
|
+
"external-user": {
|
|
57
|
+
accent: "var(--cdl-tone-info, #5a8ec1)",
|
|
58
|
+
eyebrowColor: "var(--cdl-tone-info, #5a8ec1)",
|
|
59
|
+
icon: "M12 12c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0 2c-2.7 0-8 1.3-8 4v2h16v-2c0-2.7-5.3-4-8-4zm10-1l-3-3v2h-4v2h4v2l3-3z",
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// インフラ
|
|
63
|
+
database: {
|
|
64
|
+
accent: "var(--cdl-tone-info, #5a8ec1)",
|
|
65
|
+
eyebrowColor: "var(--cdl-tone-info, #5a8ec1)",
|
|
66
|
+
icon: "M12 3C7.58 3 4 4.79 4 7s3.58 4 8 4 8-1.79 8-4-3.58-4-8-4zM4 9v3c0 2.21 3.58 4 8 4s8-1.79 8-4V9c0 2.21-3.58 4-8 4s-8-1.79-8-4zm0 5v3c0 2.21 3.58 4 8 4s8-1.79 8-4v-3c0 2.21-3.58 4-8 4s-8-1.79-8-4z",
|
|
67
|
+
shape: "cylinder",
|
|
68
|
+
},
|
|
69
|
+
cache: {
|
|
70
|
+
accent: "var(--cdl-tone-warning, #a67a2e)",
|
|
71
|
+
eyebrowColor: "var(--cdl-tone-warning, #a67a2e)",
|
|
72
|
+
icon: "M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z",
|
|
73
|
+
},
|
|
74
|
+
queue: {
|
|
75
|
+
accent: "var(--cdl-tone-warning, #a67a2e)",
|
|
76
|
+
eyebrowColor: "var(--cdl-tone-warning, #a67a2e)",
|
|
77
|
+
icon: "M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z",
|
|
78
|
+
},
|
|
79
|
+
"message-bus": {
|
|
80
|
+
accent: "var(--cdl-tone-info, #5a8ec1)",
|
|
81
|
+
eyebrowColor: "var(--cdl-tone-info, #5a8ec1)",
|
|
82
|
+
icon: "M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z",
|
|
83
|
+
},
|
|
84
|
+
cloud: {
|
|
85
|
+
accent: "var(--cdl-tone-info, #5a8ec1)",
|
|
86
|
+
eyebrowColor: "var(--cdl-tone-info, #5a8ec1)",
|
|
87
|
+
icon: "M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z",
|
|
88
|
+
shape: "cloud",
|
|
89
|
+
},
|
|
90
|
+
cdn: {
|
|
91
|
+
accent: "var(--cdl-tone-info, #5a8ec1)",
|
|
92
|
+
eyebrowColor: "var(--cdl-tone-info, #5a8ec1)",
|
|
93
|
+
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z",
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// アプリ系
|
|
97
|
+
service: {
|
|
98
|
+
accent: "var(--cdl-tone-teal, #2f8770)",
|
|
99
|
+
eyebrowColor: "var(--cdl-tone-teal, #2f8770)",
|
|
100
|
+
icon: "M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z",
|
|
101
|
+
},
|
|
102
|
+
api: {
|
|
103
|
+
accent: "var(--cdl-tone-success, #4ea36a)",
|
|
104
|
+
eyebrowColor: "var(--cdl-tone-success, #4ea36a)",
|
|
105
|
+
icon: "M14.6 16.6l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4zm-5.2 0L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4z",
|
|
106
|
+
},
|
|
107
|
+
frontend: {
|
|
108
|
+
accent: "var(--cdl-tone-info, #5a8ec1)",
|
|
109
|
+
eyebrowColor: "var(--cdl-tone-info, #5a8ec1)",
|
|
110
|
+
icon: "M20 18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z",
|
|
111
|
+
},
|
|
112
|
+
backend: {
|
|
113
|
+
accent: "var(--cdl-tone-teal, #2f8770)",
|
|
114
|
+
eyebrowColor: "var(--cdl-tone-teal, #2f8770)",
|
|
115
|
+
icon: "M4 1h16c1.1 0 2 .9 2 2v3c0 .89-.59 1.64-1.41 1.89C20.85 8.55 21 9.26 21 10c0 1.66-1.34 3-3 3s-3-1.34-3-3c0-.74.15-1.45.41-2.11C14.59 7.64 14 6.89 14 6V3c0-1.1.9-2 2-2zm0 8h16c1.1 0 2 .9 2 2v3c0 .89-.59 1.64-1.41 1.89C20.85 16.55 21 17.26 21 18c0 1.66-1.34 3-3 3s-3-1.34-3-3c0-.74.15-1.45.41-2.11C14.59 15.64 14 14.89 14 14v-3c0-1.1.9-2 2-2z",
|
|
116
|
+
},
|
|
117
|
+
webhook: {
|
|
118
|
+
accent: "var(--cdl-tone-warning, #a67a2e)",
|
|
119
|
+
eyebrowColor: "var(--cdl-tone-warning, #a67a2e)",
|
|
120
|
+
icon: "M10 16l4-4-4-4v3H4v2h6v3z",
|
|
121
|
+
},
|
|
122
|
+
microservice: {
|
|
123
|
+
accent: "var(--cdl-tone-teal, #2f8770)",
|
|
124
|
+
eyebrowColor: "var(--cdl-tone-teal, #2f8770)",
|
|
125
|
+
icon: "M2 2h8v8H2zm10 0h8v8h-8zM2 12h8v8H2zm10 0h8v8h-8z",
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// データ / 判定
|
|
129
|
+
signer: {
|
|
130
|
+
accent: "var(--cdl-tone-accent, #2d6a8f)",
|
|
131
|
+
eyebrowColor: "var(--cdl-tone-accent, #2d6a8f)",
|
|
132
|
+
icon: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
|
|
133
|
+
},
|
|
134
|
+
oracle: {
|
|
135
|
+
accent: "var(--cdl-tone-info, #5a8ec1)",
|
|
136
|
+
eyebrowColor: "var(--cdl-tone-info, #5a8ec1)",
|
|
137
|
+
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm1-13h-2v6l5.25 3.15.75-1.23-4.5-2.67z",
|
|
138
|
+
},
|
|
139
|
+
"merkle-tree": {
|
|
140
|
+
accent: "var(--cdl-tone-teal, #2f8770)",
|
|
141
|
+
eyebrowColor: "var(--cdl-tone-teal, #2f8770)",
|
|
142
|
+
icon: "M12 3l-9 7h3v8h12v-8h3l-9-7zm-2 10h4v5h-4v-5z",
|
|
143
|
+
},
|
|
144
|
+
decision: {
|
|
145
|
+
accent: "var(--cdl-tone-warning, #a67a2e)",
|
|
146
|
+
eyebrowColor: "var(--cdl-tone-warning, #a67a2e)",
|
|
147
|
+
icon: "M12 2L2 12l10 10 10-10L12 2zm0 4l6 6-6 6-6-6 6-6z",
|
|
148
|
+
shape: "diamond",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const FALLBACK: KindStyle = {
|
|
153
|
+
accent: "var(--cdl-tone-accent, #2d6a8f)",
|
|
154
|
+
eyebrowColor: "var(--cdl-tone-accent, #2d6a8f)",
|
|
155
|
+
icon: "M12 2L2 12l10 10 10-10L12 2z",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export function GenericNode({
|
|
159
|
+
node,
|
|
160
|
+
active,
|
|
161
|
+
value,
|
|
162
|
+
stateValues,
|
|
163
|
+
}: {
|
|
164
|
+
node: LaidNode;
|
|
165
|
+
active: boolean;
|
|
166
|
+
value: string | undefined;
|
|
167
|
+
stateValues: Record<string, string>;
|
|
168
|
+
}): JSX.Element {
|
|
169
|
+
const base = STYLES[node.kind] ?? FALLBACK;
|
|
170
|
+
// `tone` 指定があれば kind の既定色を置き換える。 色を決める式は `node-tone.ts` に集約。
|
|
171
|
+
const toned = toneColor(node.tone);
|
|
172
|
+
const style: KindStyle = toned ? { ...base, accent: toned, eyebrowColor: toned } : base;
|
|
173
|
+
const x = node.cx - node.w / 2;
|
|
174
|
+
const y = node.cy - node.h / 2;
|
|
175
|
+
const w = node.w;
|
|
176
|
+
const h = node.h;
|
|
177
|
+
|
|
178
|
+
// shape 別 path
|
|
179
|
+
const shape = style.shape ?? "rect";
|
|
180
|
+
const shapeEl = (() => {
|
|
181
|
+
if (shape === "cylinder") {
|
|
182
|
+
// 円柱 (top ellipse + side + bottom ellipse)
|
|
183
|
+
const rx = w / 2;
|
|
184
|
+
const ry = 14;
|
|
185
|
+
return (
|
|
186
|
+
<g>
|
|
187
|
+
<path
|
|
188
|
+
d={`M 0 ${ry} L 0 ${h - ry} A ${rx} ${ry} 0 0 0 ${w} ${h - ry} L ${w} ${ry} A ${rx} ${ry} 0 0 0 0 ${ry} Z`}
|
|
189
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
190
|
+
stroke={active ? style.accent : "var(--cdl-divider, #d4d4d8)"}
|
|
191
|
+
strokeWidth={active ? 3 : 1.25}
|
|
192
|
+
/>
|
|
193
|
+
<ellipse cx={rx} cy={ry} rx={rx} ry={ry} fill="var(--cdl-node-fill, #ffffff)" stroke={active ? style.accent : "var(--cdl-divider, #d4d4d8)"} strokeWidth={active ? 3 : 1.25} />
|
|
194
|
+
</g>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (shape === "diamond") {
|
|
198
|
+
const cx = w / 2;
|
|
199
|
+
const cy = h / 2;
|
|
200
|
+
// diamond の上下頂点を内側に 8px padding して stroke clip を回避。
|
|
201
|
+
// 旧実装は M cx 0 で上頂点が container 上端と完全一致、 strokeWidth=3 で半分が out of bounds に。
|
|
202
|
+
const pad = 8;
|
|
203
|
+
return (
|
|
204
|
+
<path
|
|
205
|
+
d={`M ${cx} ${pad} L ${w - pad} ${cy} L ${cx} ${h - pad} L ${pad} ${cy} Z`}
|
|
206
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
207
|
+
stroke={active ? style.accent : "var(--cdl-divider, #d4d4d8)"}
|
|
208
|
+
strokeWidth={active ? 3 : 1.25}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (shape === "cloud") {
|
|
213
|
+
// 雲 shape ... <circle> を 5 つ重ねて合成し、 本物のもこもこ雲を描画。
|
|
214
|
+
// path で union するより circle 5 つを並べる方が「ふっくら感」 が出る。
|
|
215
|
+
// 各 circle の中心 y は box 中央線上 (もしくは中央より少し上)、 半径は box 高さの 0.35 ~ 0.45。
|
|
216
|
+
const cx = w / 2;
|
|
217
|
+
const cy = h / 2;
|
|
218
|
+
// 雲全体の中心 y を box の中央 (cy) より少し上に
|
|
219
|
+
const cloudCenterY = cy - h * 0.05;
|
|
220
|
+
// 5 つの円の半径と中心 x オフセット (中央が大きく、 端が小さい)
|
|
221
|
+
// 円が w 方向 80% を占めるよう配置
|
|
222
|
+
const usableW = w * 0.85;
|
|
223
|
+
const halfW = usableW / 2;
|
|
224
|
+
const circles = [
|
|
225
|
+
{ cx: cx - halfW * 0.8, cy: cloudCenterY + h * 0.08, r: h * 0.32 }, // 左端
|
|
226
|
+
{ cx: cx - halfW * 0.4, cy: cloudCenterY - h * 0.05, r: h * 0.4 }, // 左
|
|
227
|
+
{ cx: cx, cy: cloudCenterY - h * 0.1, r: h * 0.46 }, // 中央 (最大)
|
|
228
|
+
{ cx: cx + halfW * 0.4, cy: cloudCenterY - h * 0.05, r: h * 0.4 }, // 右
|
|
229
|
+
{ cx: cx + halfW * 0.8, cy: cloudCenterY + h * 0.08, r: h * 0.32 }, // 右端
|
|
230
|
+
];
|
|
231
|
+
// 各 circle を fill=雲色、 stroke=outline 色で順に描画。
|
|
232
|
+
// 全部同じ fill / stroke で描けば視覚的に union された 1 つの雲に見える。
|
|
233
|
+
// ただし stroke が内側 (重なり部分) でも描画されるとセル割線が見える → 重ね順を工夫。
|
|
234
|
+
// 解決 ... まず全 circle の fill のみ描画 (内部塗りつぶし)、 次に同じ circle の stroke のみ描画
|
|
235
|
+
// (stroke は完全に円形だが、 fill で隠れている部分は見えないので結果的に union 外周のみ stroke 表示)。
|
|
236
|
+
return (
|
|
237
|
+
<g>
|
|
238
|
+
{/* Pass 1: fill のみ (union 内部塗りつぶし) */}
|
|
239
|
+
{circles.map((c, i) => (
|
|
240
|
+
<circle key={`fill-${i}`} cx={c.cx} cy={c.cy} r={c.r} fill="var(--cdl-node-fill, #ffffff)" />
|
|
241
|
+
))}
|
|
242
|
+
{/* Pass 2: stroke のみ (各円の外周、 重なり部分は他 circle の fill で隠れる) */}
|
|
243
|
+
{circles.map((c, i) => (
|
|
244
|
+
<circle
|
|
245
|
+
key={`stroke-${i}`}
|
|
246
|
+
cx={c.cx}
|
|
247
|
+
cy={c.cy}
|
|
248
|
+
r={c.r}
|
|
249
|
+
fill="none"
|
|
250
|
+
stroke={active ? style.accent : "var(--cdl-divider, #d4d4d8)"}
|
|
251
|
+
strokeWidth={active ? 3 : 1.5}
|
|
252
|
+
/>
|
|
253
|
+
))}
|
|
254
|
+
{/* Pass 3: stroke の重なり線を消すため、 もう一度 fill で内部塗りつぶし (但し stroke よりやや内側に縮める) */}
|
|
255
|
+
{circles.map((c, i) => (
|
|
256
|
+
<circle key={`mask-${i}`} cx={c.cx} cy={c.cy} r={c.r - (active ? 1.5 : 0.75)} fill="var(--cdl-node-fill, #ffffff)" />
|
|
257
|
+
))}
|
|
258
|
+
</g>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
// default rect
|
|
262
|
+
return (
|
|
263
|
+
<rect
|
|
264
|
+
x={0}
|
|
265
|
+
y={0}
|
|
266
|
+
width={w}
|
|
267
|
+
height={h}
|
|
268
|
+
rx={18}
|
|
269
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
270
|
+
stroke={active ? style.accent : "var(--cdl-divider, #d4d4d8)"}
|
|
271
|
+
strokeWidth={active ? 3 : 1.25}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
})();
|
|
275
|
+
|
|
276
|
+
// value placeholder 解決 ({stateId} 形式)
|
|
277
|
+
const resolvedValue = value?.replace(/\{(\w+)\}/g, (_, id: string) => stateValues[id] ?? "");
|
|
278
|
+
|
|
279
|
+
// shape 別の text 配置 (diamond / cloud は中心揃え、 rect 等は左上揃え)
|
|
280
|
+
const centered = shape === "diamond" || shape === "cloud";
|
|
281
|
+
const textAnchor = centered ? ("middle" as const) : ("start" as const);
|
|
282
|
+
const textX = centered ? w / 2 : 20;
|
|
283
|
+
// cylinder は上 ellipse (ry=14) の下に text を置く必要があるため textY を +28 shift
|
|
284
|
+
const cylinderShift = shape === "cylinder" ? 28 : 0;
|
|
285
|
+
// 中心揃え時は h/2 を baseline 中心 (dominantBaseline=middle で計算するため text y を h/2 ベースに統一)
|
|
286
|
+
const eyebrowY = centered ? h / 2 - 26 : 32 + cylinderShift;
|
|
287
|
+
const titleY = centered ? h / 2 + 4 : 68 + cylinderShift;
|
|
288
|
+
const subtitleY = centered ? h / 2 + 28 : 96 + cylinderShift;
|
|
289
|
+
// icon 位置 (diamond / cloud は title の真右の少し上、 中心配置に合わせる)
|
|
290
|
+
const iconX = centered ? w / 2 + 40 : w - 48;
|
|
291
|
+
const iconY = centered ? h / 2 - 44 : 18 + cylinderShift;
|
|
292
|
+
const dominantBaseline = centered ? ("middle" as const) : undefined;
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<g transform={`translate(${x} ${y})`} data-cdl-role="node-body">
|
|
296
|
+
{shapeEl}
|
|
297
|
+
{/* icon */}
|
|
298
|
+
<g transform={`translate(${iconX} ${iconY}) scale(1)`} data-cdl-role="node-kind-icon">
|
|
299
|
+
<rect width={24} height={24} fill="transparent" />
|
|
300
|
+
<path d={style.icon} fill={style.eyebrowColor} opacity={0.7} transform="scale(1)" />
|
|
301
|
+
</g>
|
|
302
|
+
{node.eyebrow && (
|
|
303
|
+
<text x={textX} y={eyebrowY} fontSize={17} fontWeight={700} letterSpacing={1.5} fill={style.eyebrowColor} textAnchor={textAnchor} dominantBaseline={dominantBaseline}>
|
|
304
|
+
{node.eyebrow}
|
|
305
|
+
</text>
|
|
306
|
+
)}
|
|
307
|
+
<text data-cdl-role="node-label" x={textX} y={titleY} fontSize={20} fontWeight={700} fill="var(--cdl-text, #1a1f2a)" textAnchor={textAnchor} dominantBaseline={dominantBaseline} style={node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : undefined}>
|
|
308
|
+
{node.title}
|
|
309
|
+
</text>
|
|
310
|
+
{node.subtitle && (
|
|
311
|
+
<text x={textX} y={subtitleY} fontSize={17} fill="var(--cdl-text-dim, #5a6270)" textAnchor={textAnchor} dominantBaseline={dominantBaseline} style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
|
312
|
+
{node.subtitle}
|
|
313
|
+
</text>
|
|
314
|
+
)}
|
|
315
|
+
{resolvedValue && (
|
|
316
|
+
<text
|
|
317
|
+
x={w - 20}
|
|
318
|
+
y={h - 24}
|
|
319
|
+
fontSize={32}
|
|
320
|
+
fontWeight={700}
|
|
321
|
+
textAnchor="end"
|
|
322
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
323
|
+
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
324
|
+
>
|
|
325
|
+
{resolvedValue}
|
|
326
|
+
</text>
|
|
327
|
+
)}
|
|
328
|
+
{node.rows && node.rows.length > 0 && (() => {
|
|
329
|
+
// storage と同じ row-align helper で left / right を分離し、 全 row 共通の
|
|
330
|
+
// rightColX (= 左列 max 幅 + gap) で右列開始 x を揃える (DB table / UML class 風)。
|
|
331
|
+
// 旧実装は split(":") 単一 delimiter + 右 anchor (node 右端寄せ) で行毎にズレていた。
|
|
332
|
+
const resolved = node.rows.map((row) =>
|
|
333
|
+
row.replace(/\{(\w+)\}/g, (_, id: string) => stateValues[id] ?? ""),
|
|
334
|
+
);
|
|
335
|
+
const splits = resolved.map(splitRow);
|
|
336
|
+
// 左列は比例の書体で描く (`font-family` を指定していないので継承する)。 字数に一定を
|
|
337
|
+
// 掛ける形では実物に届かず、 右列が左列の字に重なる (`#425`)。 字ごとの幅で見積もる。
|
|
338
|
+
const leftMaxWidth = splits.reduce(
|
|
339
|
+
(m, s) => Math.max(m, rowColumnWidth(s.left, GENERIC_ROW_LEFT_SIZE_PX, false)),
|
|
340
|
+
0,
|
|
341
|
+
);
|
|
342
|
+
const rightColX = ROW_PAD_X_PX + leftMaxWidth + ROW_COL_GAP_PX;
|
|
343
|
+
return (
|
|
344
|
+
<g>
|
|
345
|
+
{splits.map((s, i) => (
|
|
346
|
+
<g key={i} transform={`translate(0 ${rowBaselineY(node.kind, i)})`}>
|
|
347
|
+
<text data-cdl-role="node-row" data-cdl-row-index={i} x={ROW_PAD_X_PX} y={0} fontSize={18} fill="var(--cdl-text-dim, #5a6270)">{s.left}</text>
|
|
348
|
+
<text
|
|
349
|
+
data-cdl-role="node-row"
|
|
350
|
+
data-cdl-row-index={i}
|
|
351
|
+
x={rightColX}
|
|
352
|
+
y={0}
|
|
353
|
+
fontSize={21}
|
|
354
|
+
fontWeight={700}
|
|
355
|
+
textAnchor="start"
|
|
356
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
357
|
+
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
358
|
+
>
|
|
359
|
+
{s.right}
|
|
360
|
+
</text>
|
|
361
|
+
</g>
|
|
362
|
+
))}
|
|
363
|
+
</g>
|
|
364
|
+
);
|
|
365
|
+
})()}
|
|
366
|
+
</g>
|
|
367
|
+
);
|
|
368
|
+
}
|