@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,439 @@
1
+ import type { JSX } from "react";
2
+ import { shapeRegion, fitArt, labelPlan, type ArtExtent } from "./shape-region";
3
+ import type { LaidNode } from "../types";
4
+
5
+ /**
6
+ * Shape-driven people 6 kinds (CAR-1111 Phase 2-D)。
7
+ *
8
+ * auditor / regulator / notary / lawyer / trader / customer-service。
9
+ *
10
+ * 設計原則 (people-2icon-combo final)。
11
+ * 1. 人型 base = 頭 (outline circle) + 肩 (cubic Bezier curve、 顔なし、 slim body 幅 102)
12
+ * 2. 右下 halo badge (accessory 色 stroke + minimal icon) = 主 identity
13
+ * 3. 体内 accessory (幾何学 rect + mini icon) = 副 identity (右下と別要素)
14
+ * 4. label は shape の下に固定
15
+ */
16
+
17
+ type ShapeProps = {
18
+ node: LaidNode;
19
+ active: boolean;
20
+ };
21
+
22
+ /**
23
+ * 名前の塊を描く。 位置と大きさは `labelPlan` が決める (箱の下端から積み上げる)。
24
+ *
25
+ * 上から順に置くと、 箱が小さい時に肩書が絵の領域へ食い込む (#433 の review 指摘)。
26
+ */
27
+ function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPlan>) {
28
+ return (
29
+ <>
30
+ {node.eyebrow && plan.肩書 && (
31
+ <text x={cx} y={plan.肩書.y} fontSize={plan.肩書.size} fontWeight={700} letterSpacing={2} textAnchor="middle" fill="var(--cdl-tone-accent, #2d6a8f)">
32
+ {node.eyebrow}
33
+ </text>
34
+ )}
35
+ <text data-cdl-role="node-label" x={cx} y={plan.名前.y} fontSize={plan.名前.size} fontWeight={700} textAnchor="middle" fill="var(--cdl-text, #1a1f2a)">
36
+ {node.title}
37
+ </text>
38
+ {node.subtitle && plan.説明 && (
39
+ <text x={cx} y={plan.説明.y} fontSize={plan.説明.size} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
40
+ {node.subtitle}
41
+ </text>
42
+ )}
43
+ </>
44
+ );
45
+ }
46
+
47
+
48
+ /**
49
+ * 人型 base = 頭 (outline circle r=30) + 肩 (cubic Bezier curve、 slim body 幅 102、 頭 : 肩 = 1 : 1.7)。
50
+ * cx, cy = 人型全体の中心。 顔なし = 表情 dot / 口 arc は描かない。
51
+ */
52
+ /** 影の楕円を人型の底からどれだけ下に置くか、 とその縦半径。 */
53
+ const SHADOW_DY = 20;
54
+ const SHADOW_RY = 4;
55
+ const SHADOW_RX = 55;
56
+
57
+ /** 右下 badge の位置 (人型の中心から) と大きさ。 */
58
+ const BADGE_DX = 56;
59
+ const BADGE_DY = 60;
60
+ const BADGE_R = 26;
61
+ const BADGE_SW = 2.5;
62
+
63
+ /** 人型の線の太さ。 外接矩形は線の半分だけ外に出る。 */
64
+ const PERSON_SW = 3;
65
+
66
+ /**
67
+ * 記章の影がにじむ量。 `haloBadge` の `feGaussianBlur` (3) と `feOffset` (下へ 2)。
68
+ *
69
+ * にじみは描画に出るので広がりに数える。 数えないと、 狭い箱で横幅が倍率を決めた時に
70
+ * この分だけ箱の外に残る (review 指摘)。
71
+ */
72
+ const BADGE_BLUR = 3;
73
+ const BADGE_BLUR_DY = 2;
74
+
75
+ function personBase(cx: number, cy: number, active: boolean) {
76
+ const strokeColor = active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)";
77
+ const headR = 30;
78
+ const headCy = cy - 40;
79
+ const shoulderR = 51;
80
+ const bodyTopY = headCy + headR + 4;
81
+ const bodyBottomY = bodyTopY + 90;
82
+ // body path = 左底 → 肩左 cubic curve → 中央上 → 肩右 cubic curve → 右底
83
+ const bodyPath =
84
+ `M ${cx - shoulderR} ${bodyBottomY} ` +
85
+ `C ${cx - shoulderR} ${bodyTopY + 40}, ${cx - 28} ${bodyTopY}, ${cx} ${bodyTopY} ` +
86
+ `C ${cx + 28} ${bodyTopY}, ${cx + shoulderR} ${bodyTopY + 40}, ${cx + shoulderR} ${bodyBottomY}`;
87
+ return {
88
+ headR,
89
+ headCy,
90
+ shoulderR,
91
+ bodyTopY,
92
+ bodyBottomY,
93
+ strokeColor,
94
+ render: (
95
+ <>
96
+ {/* drop shadow */}
97
+ <ellipse cx={cx} cy={bodyBottomY + SHADOW_DY} rx={SHADOW_RX} ry={SHADOW_RY} fill="#000" opacity={0.08} stroke="none" />
98
+ {/* 頭 (outline circle) */}
99
+ <circle data-cdl-role="node-body" cx={cx} cy={headCy} r={headR} fill="none" stroke={strokeColor} strokeWidth={3} />
100
+ {/* body (cubic Bezier curve、 open bottom = U 字ではなく肩から下ろす line のみ) */}
101
+ <path d={bodyPath} fill="none" stroke={strokeColor} strokeWidth={3} strokeLinecap="round" />
102
+ </>
103
+ ),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * halo badge = 右下に配置する主 identity icon。
109
+ * badge 円 (白 fill + accessory 色 stroke) + drop shadow filter。
110
+ * children は 中央 (0,0) 基準の accessory 描画 g。
111
+ */
112
+ function haloBadge(cx: number, cy: number, filterId: string, strokeColor: string, children: JSX.Element) {
113
+ return (
114
+ <g>
115
+ <defs>
116
+ <filter id={filterId} x="-50%" y="-50%" width="200%" height="200%">
117
+ <feGaussianBlur in="SourceAlpha" stdDeviation={3} />
118
+ <feOffset dx={0} dy={2} result="offsetblur" />
119
+ <feFlood floodColor="#000" floodOpacity={0.15} />
120
+ <feComposite in2="offsetblur" operator="in" />
121
+ <feMerge>
122
+ <feMergeNode />
123
+ <feMergeNode in="SourceGraphic" />
124
+ </feMerge>
125
+ </filter>
126
+ </defs>
127
+ <g filter={`url(#${filterId})`}>
128
+ <circle cx={cx} cy={cy} r={BADGE_R} fill="#ffffff" stroke={strokeColor} strokeWidth={BADGE_SW} />
129
+ </g>
130
+ <g transform={`translate(${cx}, ${cy})`}>{children}</g>
131
+ </g>
132
+ );
133
+ }
134
+
135
+ // 右下 badge の位置 (人型 cx から x+56、 body 下部)
136
+ function badgePos(cx: number, personCy: number) {
137
+ return { bx: cx + BADGE_DX, by: personCy + BADGE_DY };
138
+ }
139
+
140
+ /**
141
+ * 人型 + 右下 badge + 影の広がり (領域の中心から)。
142
+ *
143
+ * **描く側の定数から出す**。 表に書き写すと、 絵を変えた時に片方だけ古くなる。
144
+ */
145
+ function personExtent(
146
+ p: { headR: number; headCy: number; shoulderR: number; bodyBottomY: number },
147
+ regionCy: number,
148
+ personCy: number,
149
+ ): ArtExtent {
150
+ const 線 = PERSON_SW / 2;
151
+ // 記章はにじみの分だけ外に出る。 下向きの offset も足す
152
+ const badge = BADGE_R + BADGE_SW / 2 + BADGE_BLUR;
153
+ const 影下 = p.bodyBottomY + SHADOW_DY + SHADOW_RY;
154
+ const 記章下 = personCy + BADGE_DY + badge + BADGE_BLUR_DY;
155
+ return {
156
+ 上: regionCy - (p.headCy - p.headR - 線),
157
+ 下: Math.max(影下, 記章下) - regionCy,
158
+ // 影は肩より広い (rx 55 対 肩 51)。 広い方を採る
159
+ 左: Math.max(p.shoulderR + 線, SHADOW_RX),
160
+ 右: Math.max(SHADOW_RX, BADGE_DX + badge),
161
+ };
162
+ }
163
+
164
+ // 体内 accessory の中央位置 (人型 cx、 body 内 y)
165
+ function chestPos(cx: number, personCy: number) {
166
+ return { chestX: cx, chestY: personCy + 45 };
167
+ }
168
+
169
+ // V 襟 (人型の首下、 body 上部の decorative V line)
170
+ function collarV(cx: number, personCy: number, strokeColor: string) {
171
+ const y = personCy + 8;
172
+ return (
173
+ <path
174
+ d={`M ${cx - 12} ${y} L ${cx} ${y + 14} L ${cx + 12} ${y}`}
175
+ fill="none"
176
+ stroke={strokeColor}
177
+ strokeWidth={2.5}
178
+ strokeLinecap="round"
179
+ strokeLinejoin="round"
180
+ />
181
+ );
182
+ }
183
+
184
+ /** shape-lawyer = 弁護士 (右下 天秤 + 体内 gavel + 台座) */
185
+ export function ShapeLawyerNode({ node, active }: ShapeProps): JSX.Element {
186
+ const r = shapeRegion(node);
187
+ const cx = r.cx;
188
+ const cy = r.cy + 20;
189
+ const p = personBase(cx, cy, active);
190
+ const { bx, by } = badgePos(cx, cy);
191
+ const { chestX, chestY } = chestPos(cx, cy);
192
+ return (
193
+ <g>
194
+ {fitArt(r, personExtent(p, r.cy, cy), (
195
+ <g>
196
+ {p.render}
197
+ {collarV(cx, cy, p.strokeColor)}
198
+ {/* 体内 gavel (rotate -20°) + 台座 */}
199
+ <g transform={`translate(${chestX}, ${chestY}) rotate(-20)`}>
200
+ <rect x={-4} y={-8} width={8} height={18} rx={1} fill="#a67c00" stroke="#5a3a10" strokeWidth={1} />
201
+ <rect x={-14} y={-16} width={28} height={12} rx={2} fill="#c9924a" stroke="#5a3a10" strokeWidth={1.5} />
202
+ <rect x={-14} y={-16} width={28} height={3} fill="#e5c07b" stroke="none" />
203
+ </g>
204
+ <ellipse cx={chestX} cy={chestY + 20} rx={26} ry={4} fill="#8b6914" stroke="#5a3a10" strokeWidth={1.5} />
205
+ <ellipse cx={chestX} cy={chestY + 18} rx={26} ry={4} fill="#c9924a" stroke="#5a3a10" strokeWidth={1.5} />
206
+ {/* 右下 天秤 badge */}
207
+ {haloBadge(bx, by, `lw-halo-${node.id}`, "#a67c00", (
208
+ <g>
209
+ <line x1={0} y1={-12} x2={0} y2={8} stroke="#a67c00" strokeWidth={2} />
210
+ <line x1={-12} y1={-8} x2={12} y2={-8} stroke="#a67c00" strokeWidth={2} strokeLinecap="round" />
211
+ <path d="M -12 -8 L -15 1 L -9 1 Z" fill="#e5c07b" stroke="#a67c00" strokeWidth={1.5} />
212
+ <path d="M 12 -8 L 15 1 L 9 1 Z" fill="#e5c07b" stroke="#a67c00" strokeWidth={1.5} />
213
+ <rect x={-5} y={6} width={10} height={3} rx={1} fill="#a67c00" />
214
+ </g>
215
+ ))}
216
+ </g>
217
+ ))}
218
+ {bottomLabel(node, r.cx, r.label)}
219
+ </g>
220
+ );
221
+ }
222
+
223
+ /** shape-auditor = 監査人 (右下 magnifier + 体内 clipboard + 3 check) */
224
+ export function ShapeAuditorNode({ node, active }: ShapeProps): JSX.Element {
225
+ const r = shapeRegion(node);
226
+ const cx = r.cx;
227
+ const cy = r.cy + 20;
228
+ const p = personBase(cx, cy, active);
229
+ const { bx, by } = badgePos(cx, cy);
230
+ const { chestX, chestY } = chestPos(cx, cy);
231
+ return (
232
+ <g>
233
+ {fitArt(r, personExtent(p, r.cy, cy), (
234
+ <g>
235
+ {p.render}
236
+ {collarV(cx, cy, p.strokeColor)}
237
+ {/* 体内 clipboard + 3 check */}
238
+ <rect x={chestX - 18} y={chestY - 6} width={36} height={42} rx={2} fill="#e5c07b" stroke="#a67c00" strokeWidth={1.5} />
239
+ <rect x={chestX - 8} y={chestY - 10} width={16} height={6} rx={1} fill="#0f3450" stroke="none" />
240
+ <rect x={chestX - 14} y={chestY} width={28} height={32} fill="#ffffff" stroke="#8b949e" strokeWidth={0.8} />
241
+ {[0, 1, 2].map((i) => {
242
+ const y = chestY + 8 + i * 10;
243
+ return (
244
+ <g key={i}>
245
+ <path d={`M ${chestX - 10} ${y} L ${chestX - 7} ${y + 3} L ${chestX - 2} ${y - 3}`} fill="none" stroke="#2a8560" strokeWidth={1.2} strokeLinecap="round" strokeLinejoin="round" />
246
+ <line x1={chestX + 2} y1={y} x2={chestX + 12} y2={y} stroke="#8b949e" strokeWidth={1} />
247
+ </g>
248
+ );
249
+ })}
250
+ {/* 右下 magnifier badge */}
251
+ {haloBadge(bx, by, `au-halo-${node.id}`, "#2a8560", (
252
+ <g>
253
+ <circle cx={-4} cy={-4} r={10} fill="none" stroke="#2a8560" strokeWidth={2.5} />
254
+ <line x1={4} y1={4} x2={13} y2={13} stroke="#2a8560" strokeWidth={2.5} strokeLinecap="round" />
255
+ <path d="M -8 -4 L -5 -1 L -1 -7" fill="none" stroke="#2a8560" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />
256
+ </g>
257
+ ))}
258
+ </g>
259
+ ))}
260
+ {bottomLabel(node, r.cx, r.label)}
261
+ </g>
262
+ );
263
+ }
264
+
265
+ /** shape-regulator = 規制当局 (右下 shield + 体内 制服 buttons + star) */
266
+ export function ShapeRegulatorNode({ node, active }: ShapeProps): JSX.Element {
267
+ const r = shapeRegion(node);
268
+ const cx = r.cx;
269
+ const cy = r.cy + 20;
270
+ const p = personBase(cx, cy, active);
271
+ const { bx, by } = badgePos(cx, cy);
272
+ const { chestX, chestY } = chestPos(cx, cy);
273
+ return (
274
+ <g>
275
+ {fitArt(r, personExtent(p, r.cy, cy), (
276
+ <g>
277
+ {p.render}
278
+ {collarV(cx, cy, p.strokeColor)}
279
+ {/* 体内 4 buttons + star */}
280
+ <circle cx={chestX - 10} cy={chestY} r={3} fill="#e5c07b" stroke="#a67c00" strokeWidth={1} />
281
+ <circle cx={chestX + 10} cy={chestY} r={3} fill="#e5c07b" stroke="#a67c00" strokeWidth={1} />
282
+ <circle cx={chestX - 10} cy={chestY + 18} r={3} fill="#e5c07b" stroke="#a67c00" strokeWidth={1} />
283
+ <circle cx={chestX + 10} cy={chestY + 18} r={3} fill="#e5c07b" stroke="#a67c00" strokeWidth={1} />
284
+ <polygon
285
+ points={`${chestX},${chestY + 30} ${chestX + 3},${chestY + 40} ${chestX + 13},${chestY + 40} ${chestX + 5},${chestY + 46} ${chestX + 8},${chestY + 56} ${chestX},${chestY + 50} ${chestX - 8},${chestY + 56} ${chestX - 5},${chestY + 46} ${chestX - 13},${chestY + 40} ${chestX - 3},${chestY + 40}`}
286
+ fill="#e5c07b"
287
+ stroke="#a67c00"
288
+ strokeWidth={1}
289
+ />
290
+ {/* 右下 shield badge */}
291
+ {haloBadge(bx, by, `rg-halo-${node.id}`, "#0f3450", (
292
+ <g>
293
+ <path d="M 0 -12 L 11 -8 L 11 3 C 11 10, 0 13, 0 13 C 0 13, -11 10, -11 3 L -11 -8 Z" fill="#0f3450" stroke="#0f3450" strokeWidth={1.5} strokeLinejoin="round" />
294
+ <polygon points="0,-6 2,-2 6,-2 3,1 4,6 0,3 -4,6 -3,1 -6,-2 -2,-2" fill="#e5c07b" />
295
+ </g>
296
+ ))}
297
+ </g>
298
+ ))}
299
+ {bottomLabel(node, r.cx, r.label)}
300
+ </g>
301
+ );
302
+ }
303
+
304
+ /** shape-notary = 公証人 (右下 印章 + 体内 契約書 + 署名) */
305
+ export function ShapeNotaryNode({ node, active }: ShapeProps): JSX.Element {
306
+ const r = shapeRegion(node);
307
+ const cx = r.cx;
308
+ const cy = r.cy + 20;
309
+ const p = personBase(cx, cy, active);
310
+ const { bx, by } = badgePos(cx, cy);
311
+ const { chestX, chestY } = chestPos(cx, cy);
312
+ return (
313
+ <g>
314
+ {fitArt(r, personExtent(p, r.cy, cy), (
315
+ <g>
316
+ {p.render}
317
+ {collarV(cx, cy, p.strokeColor)}
318
+ {/* 体内 契約書 (dog-ear + line + signature) */}
319
+ <path
320
+ d={`M ${chestX - 20} ${chestY - 2} L ${chestX - 20} ${chestY + 42} L ${chestX + 20} ${chestY + 42} L ${chestX + 20} ${chestY - 2} L ${chestX + 12} ${chestY - 10} L ${chestX - 20} ${chestY - 10} Z`}
321
+ fill="#ffffff"
322
+ stroke="#c15a4a"
323
+ strokeWidth={1.8}
324
+ strokeLinejoin="round"
325
+ />
326
+ <path
327
+ d={`M ${chestX + 12} ${chestY - 10} L ${chestX + 12} ${chestY - 2} L ${chestX + 20} ${chestY - 2}`}
328
+ fill="none"
329
+ stroke="#c15a4a"
330
+ strokeWidth={1.5}
331
+ strokeLinejoin="round"
332
+ />
333
+ <line x1={chestX - 14} y1={chestY + 6} x2={chestX + 14} y2={chestY + 6} stroke="#8b949e" strokeWidth={1} />
334
+ <line x1={chestX - 14} y1={chestY + 14} x2={chestX + 14} y2={chestY + 14} stroke="#8b949e" strokeWidth={1} />
335
+ <line x1={chestX - 14} y1={chestY + 22} x2={chestX + 8} y2={chestY + 22} stroke="#8b949e" strokeWidth={1} />
336
+ <line x1={chestX - 14} y1={chestY + 34} x2={chestX + 2} y2={chestY + 34} stroke="#0f3450" strokeWidth={1.5} />
337
+ <text x={chestX + 6} y={chestY + 37} fontSize={8} fill="#0f3450" fontFamily="cursive" fontStyle="italic">
338
+
339
+ </text>
340
+ {/* 右下 印章 badge */}
341
+ {haloBadge(bx, by, `nt-halo-${node.id}`, "#c15a4a", (
342
+ <g>
343
+ <circle cx={0} cy={0} r={12} fill="#c15a4a" stroke="none" />
344
+ <circle cx={0} cy={0} r={8} fill="none" stroke="#ffffff" strokeWidth={1.2} />
345
+ <text x={0} y={4} fontFamily="Georgia, serif" fontSize={10} fontWeight={700} fill="#ffffff" textAnchor="middle">
346
+
347
+ </text>
348
+ </g>
349
+ ))}
350
+ </g>
351
+ ))}
352
+ {bottomLabel(node, r.cx, r.label)}
353
+ </g>
354
+ );
355
+ }
356
+
357
+ /** shape-trader = トレーダー (右下 chart line + 体内 laptop + candles) */
358
+ export function ShapeTraderNode({ node, active }: ShapeProps): JSX.Element {
359
+ const r = shapeRegion(node);
360
+ const cx = r.cx;
361
+ const cy = r.cy + 20;
362
+ const p = personBase(cx, cy, active);
363
+ const { bx, by } = badgePos(cx, cy);
364
+ const { chestX, chestY } = chestPos(cx, cy);
365
+ return (
366
+ <g>
367
+ {fitArt(r, personExtent(p, r.cy, cy), (
368
+ <g>
369
+ {p.render}
370
+ {collarV(cx, cy, p.strokeColor)}
371
+ {/* 体内 mini laptop + candles */}
372
+ <rect x={chestX - 20} y={chestY} width={40} height={30} rx={2} fill="#ffffff" stroke="#4ea36a" strokeWidth={2} />
373
+ {[
374
+ { x: -16, h: 10 },
375
+ { x: -10, h: 14 },
376
+ { x: -4, h: 10, red: true },
377
+ { x: 2, h: 12 },
378
+ { x: 8, h: 10 },
379
+ { x: 14, h: 12 },
380
+ ].map((c, i) => (
381
+ <rect
382
+ key={i}
383
+ x={chestX + c.x}
384
+ y={chestY + 24 - c.h}
385
+ width={3}
386
+ height={c.h}
387
+ fill={c.red ? "#c15a4a" : "#4ea36a"}
388
+ stroke="none"
389
+ />
390
+ ))}
391
+ <path d={`M ${chestX - 14} ${chestY + 20} L ${chestX + 12} ${chestY + 6}`} fill="none" stroke="#e5c07b" strokeWidth={1.2} strokeDasharray="2 1.5" />
392
+ {/* 右下 chart line badge */}
393
+ {haloBadge(bx, by, `tr-halo-${node.id}`, "#4ea36a", (
394
+ <g>
395
+ <path d="M -11 7 L -5 -2 L 1 2 L 6 -7 L 12 -4" fill="none" stroke="#4ea36a" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
396
+ <polyline points="9,-7 12,-7 12,-4" fill="none" stroke="#4ea36a" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
397
+ <line x1={-13} y1={11} x2={13} y2={11} stroke="#4ea36a" strokeWidth={1.5} opacity={0.4} />
398
+ </g>
399
+ ))}
400
+ </g>
401
+ ))}
402
+ {bottomLabel(node, r.cx, r.label)}
403
+ </g>
404
+ );
405
+ }
406
+
407
+ /** shape-customer-service = CS (右下 headset + 体内 24H badge) */
408
+ export function ShapeCustomerServiceNode({ node, active }: ShapeProps): JSX.Element {
409
+ const r = shapeRegion(node);
410
+ const cx = r.cx;
411
+ const cy = r.cy + 20;
412
+ const p = personBase(cx, cy, active);
413
+ const { bx, by } = badgePos(cx, cy);
414
+ const { chestX, chestY } = chestPos(cx, cy);
415
+ return (
416
+ <g>
417
+ {fitArt(r, personExtent(p, r.cy, cy), (
418
+ <g>
419
+ {p.render}
420
+ {collarV(cx, cy, p.strokeColor)}
421
+ {/* 体内 24H badge */}
422
+ <rect x={chestX - 22} y={chestY + 5} width={44} height={22} rx={11} fill="#e5c07b" stroke="#a67c00" strokeWidth={2} />
423
+ <text x={chestX} y={chestY + 20} fontSize={12} fontWeight={900} fill="#5a3a10" textAnchor="middle" letterSpacing={2}>
424
+ 24H
425
+ </text>
426
+ {/* 右下 headset badge */}
427
+ {haloBadge(bx, by, `cs-halo-${node.id}`, "#a67c00", (
428
+ <g>
429
+ <path d="M -11 4 C -11 -10, -6 -12, 0 -12 C 6 -12, 11 -10, 11 4" fill="none" stroke="#a67c00" strokeWidth={2.5} strokeLinecap="round" />
430
+ <rect x={-13} y={2} width={6} height={11} rx={1.5} fill="#e5c07b" stroke="#a67c00" strokeWidth={1.5} />
431
+ <rect x={7} y={2} width={6} height={11} rx={1.5} fill="#e5c07b" strokeWidth={1.5} stroke="#a67c00" />
432
+ </g>
433
+ ))}
434
+ </g>
435
+ ))}
436
+ {bottomLabel(node, r.cx, r.label)}
437
+ </g>
438
+ );
439
+ }