@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
package/src/builder.ts ADDED
@@ -0,0 +1,3740 @@
1
+ import { parseFormula } from "./formula";
2
+ import type {
3
+ CdlDiagram,
4
+ CdlEdge,
5
+ CdlEventBinding,
6
+ CdlEventTarget,
7
+ CdlFormula,
8
+ CdlInput,
9
+ CdlLane,
10
+ CdlNode,
11
+ CdlPhase,
12
+ CdlReadout,
13
+ CdlScrollTrigger,
14
+ CdlState,
15
+ EdgeStyle,
16
+ NodeKind,
17
+ Side,
18
+ Tone,
19
+ } from "./types";
20
+
21
+ /**
22
+ * repeatNodes template で `{i}` / `{i+1}` / `{i-1}` を index に置換する (CAR: repeat primitive)。
23
+ * template 経由で source: 'gas{i+1}' のように書けば、 iteration 毎に gas1 / gas2 / gas3 が生成される。
24
+ */
25
+ function substituteI(input: string, i: number): string {
26
+ return input.replace(/\{i([+-]\d+)?\}/g, (_, offset) => {
27
+ if (!offset) return String(i);
28
+ return String(i + Number(offset));
29
+ });
30
+ }
31
+
32
+ /**
33
+ * template で `{r}` / `{c}` / `{r+1}` 等の変数を置換する generic helper (grid用)。
34
+ */
35
+ function substituteVars(input: string, vars: Record<string, string>): string {
36
+ return input.replace(/\{([a-z])([+-]\d+)?\}/g, (m, k, offset) => {
37
+ if (!(k in vars)) return m;
38
+ const base = Number(vars[k]);
39
+ if (Number.isNaN(base)) return vars[k] ?? m;
40
+ if (!offset) return String(base);
41
+ return String(base + Number(offset));
42
+ });
43
+ }
44
+
45
+ function deepSubstituteVars<T>(obj: T, vars: Record<string, string>): T {
46
+ if (typeof obj === "string") return substituteVars(obj, vars) as unknown as T;
47
+ if (Array.isArray(obj)) return obj.map((el) => deepSubstituteVars(el, vars)) as unknown as T;
48
+ if (obj !== null && typeof obj === "object") {
49
+ const out: Record<string, unknown> = {};
50
+ for (const key of Object.keys(obj as Record<string, unknown>)) {
51
+ out[key] = deepSubstituteVars((obj as Record<string, unknown>)[key], vars);
52
+ }
53
+ return out as T;
54
+ }
55
+ return obj;
56
+ }
57
+
58
+ /**
59
+ * recursive に object / array 内の string field の `{i}` を substituteI で置換する。
60
+ * 非 string field はそのまま。 primitive (number / boolean / undefined) もそのまま。
61
+ */
62
+ function deepSubstituteI<T>(obj: T, i: number): T {
63
+ if (typeof obj === "string") return substituteI(obj, i) as unknown as T;
64
+ if (Array.isArray(obj)) return obj.map((el) => deepSubstituteI(el, i)) as unknown as T;
65
+ if (obj !== null && typeof obj === "object") {
66
+ const out: Record<string, unknown> = {};
67
+ for (const key of Object.keys(obj as Record<string, unknown>)) {
68
+ out[key] = deepSubstituteI((obj as Record<string, unknown>)[key], i);
69
+ }
70
+ return out as T;
71
+ }
72
+ return obj;
73
+ }
74
+
75
+ /**
76
+ * built diagram の interactive spec 部分だけを deep clone + recursive freeze する SSOT (CAR #260)。
77
+ *
78
+ * 対象は `inputs` / `formulas` / `scrollTriggers` / `eventBindings` の 4 array のみ。
79
+ * これらは interactive integration の schemaKey memoize (`useMemo(fn, [diagram])`) が
80
+ * identity dep で in-place mutation を検知できない regression の根本原因。 4 array だけを
81
+ * freeze することで:
82
+ * - `d.inputs[0].defaultValue = 999` 等の mutation は throw で検知
83
+ * - schemaKey が 4 array を JSON.stringify する経路と対応
84
+ *
85
+ * `lanes` / `nodes` / `edges` / `states` / `phases` は freeze しない。 dragon compile.ts 等の
86
+ * upstream compiler が build() 後に inline extension (`node.subtitle = ...` 等) を merge する
87
+ * 既存 API を破らないため。 これらは interactive integration の schemaKey に影響しないため
88
+ * mutation されても schemaKey stale regression は起きない (compile → layout → LaidNode に
89
+ * 変換される時点で内容が copy される、 render 時は LaidNode を参照する)。
90
+ *
91
+ * 2 step の設計:
92
+ * 1. 対象 array を `structuredClone` で完全独立 copy を作る (builder accumulator + 外部 leak 遮断)
93
+ * 2. `recursiveFreeze(clone)` で 2 段目 nested (formulas[0].expression 等) まで freeze
94
+ */
95
+ function recursiveFreeze<T>(obj: T, seen: WeakSet<object>): T {
96
+ if (obj === null || typeof obj !== "object") return obj;
97
+ const asObj = obj as unknown as object;
98
+ if (seen.has(asObj)) return obj;
99
+ seen.add(asObj);
100
+ Object.freeze(obj);
101
+ if (Array.isArray(obj)) {
102
+ for (const item of obj) {
103
+ recursiveFreeze(item, seen);
104
+ }
105
+ } else {
106
+ for (const key of Object.keys(obj as Record<string, unknown>)) {
107
+ const v = (obj as Record<string, unknown>)[key];
108
+ if (v !== null && typeof v === "object") recursiveFreeze(v, seen);
109
+ }
110
+ }
111
+ return obj;
112
+ }
113
+
114
+ function freezeInteractiveSpec<T>(arr: readonly T[] | undefined): readonly T[] | undefined {
115
+ if (!arr) return arr;
116
+ const cloned =
117
+ typeof structuredClone === "function"
118
+ ? (structuredClone(arr) as T[])
119
+ : (JSON.parse(JSON.stringify(arr)) as T[]);
120
+ return recursiveFreeze(cloned, new WeakSet<object>()) as readonly T[];
121
+ }
122
+
123
+ /**
124
+ * 有限値 (Number.isFinite) を要求する fail-fast helper (CAR #263)。 NaN / Infinity /
125
+ * -Infinity は input / scroll spec の source として無効、 受け入れると:
126
+ * - JSON.stringify(NaN) / (Infinity) が同じ "null" に潰れ、 schemaKey 衝突
127
+ * - range check (`x < 0 || x > 1`) が NaN で silent pass する
128
+ * ため builder 層で throw する。
129
+ */
130
+ function assertFiniteNumber(value: number, context: string): void {
131
+ if (typeof value !== "number" || !Number.isFinite(value)) {
132
+ throw new Error(
133
+ `[cdl] ${context} は有限数値である必要があります (got: ${String(value)})`,
134
+ );
135
+ }
136
+ }
137
+
138
+ function deepFreezeDiagram(diagram: CdlDiagram): CdlDiagram {
139
+ return {
140
+ ...diagram,
141
+ ...(diagram.inputs ? { inputs: freezeInteractiveSpec(diagram.inputs) as CdlInput[] } : {}),
142
+ ...(diagram.formulas
143
+ ? { formulas: freezeInteractiveSpec(diagram.formulas) as CdlFormula[] }
144
+ : {}),
145
+ ...(diagram.scrollTriggers
146
+ ? { scrollTriggers: freezeInteractiveSpec(diagram.scrollTriggers) as CdlScrollTrigger[] }
147
+ : {}),
148
+ ...(diagram.eventBindings
149
+ ? { eventBindings: freezeInteractiveSpec(diagram.eventBindings) as CdlEventBinding[] }
150
+ : {}),
151
+ };
152
+ }
153
+
154
+ // ─── Animation chain (scroll-driven trigger + 将来的な animation primitive、 CAR #245) ───
155
+
156
+ /**
157
+ * `.animation.scroll(id, opts)` を提供する chain-facing helper。 将来 `.animation.time(...)` 等の
158
+ * time-based primitive を追加する余地を残すため animation namespace として分離。
159
+ */
160
+ export type AnimationChain = {
161
+ scroll: (
162
+ id: string,
163
+ opts?: { start?: number; end?: number; scrub?: number; label?: string },
164
+ ) => DiagramBuilder;
165
+ };
166
+
167
+ // ─── Event handler chain (`.on.click(target, handlerId)`、 CAR #246) ───
168
+
169
+ /**
170
+ * `.on.click(target, handlerId)` / `.on.hover(target, handlerId)` を提供する chain helper。
171
+ * target は node / edge / lane / diagram の 4 種、 handlerId は string で consumer が attach 時に
172
+ * 対応関数 map を渡す (JSON DSL 互換のため関数は diagram data model に含めない)。
173
+ */
174
+ export type EventChain = {
175
+ click: (target: CdlEventTarget, handlerId: string) => DiagramBuilder;
176
+ hover: (target: CdlEventTarget, handlerId: string) => DiagramBuilder;
177
+ doubleClick: (target: CdlEventTarget, handlerId: string) => DiagramBuilder;
178
+ longPress: (target: CdlEventTarget, handlerId: string) => DiagramBuilder;
179
+ drag: (target: CdlEventTarget, handlerId: string) => DiagramBuilder;
180
+ drop: (target: CdlEventTarget, handlerId: string) => DiagramBuilder;
181
+ keydown: (target: CdlEventTarget, handlerId: string) => DiagramBuilder;
182
+ focus: (target: CdlEventTarget, handlerId: string) => DiagramBuilder;
183
+ blur: (target: CdlEventTarget, handlerId: string) => DiagramBuilder;
184
+ };
185
+
186
+ // ─── Input widget builder (interactive primitive、 CAR #243) ───
187
+
188
+ /**
189
+ * `.input.slider()` / `.number()` / `.dropdown()` / `.toggle()` を提供する chain-facing helper。
190
+ * 各 method は widget spec を `CdlInput` として累積し、 DiagramBuilder を返して chain 継続を許す。
191
+ */
192
+ /**
193
+ * ReadoutChain = visual readout widget を diagram に追加する chain (CAR: widget 拡充)。
194
+ * 各 method は CdlReadout として累積する。
195
+ */
196
+ export type ReadoutChain = {
197
+ bar: (
198
+ id: string,
199
+ opts: { source: string; min: number; max: number; color?: string; label?: string },
200
+ ) => DiagramBuilder;
201
+ gauge: (
202
+ id: string,
203
+ opts: { source: string; min: number; max: number; color?: string; label?: string },
204
+ ) => DiagramBuilder;
205
+ stat: (
206
+ id: string,
207
+ opts: { source: string; caption?: string; unit?: string; label?: string },
208
+ ) => DiagramBuilder;
209
+ sparkline: (
210
+ id: string,
211
+ opts: { source: string; history?: number; color?: string; label?: string },
212
+ ) => DiagramBuilder;
213
+ countup: (
214
+ id: string,
215
+ opts: { source: string; durationMs?: number; decimals?: number; unit?: string; label?: string },
216
+ ) => DiagramBuilder;
217
+ typewriter: (
218
+ id: string,
219
+ opts: { source: string; charMs?: number; label?: string },
220
+ ) => DiagramBuilder;
221
+ delta: (
222
+ id: string,
223
+ opts: { source: string; decimals?: number; unit?: string; label?: string },
224
+ ) => DiagramBuilder;
225
+ percentRing: (
226
+ id: string,
227
+ opts: { source: string; max: number; color?: string; label?: string },
228
+ ) => DiagramBuilder;
229
+ heatCell: (
230
+ id: string,
231
+ opts: { source: string; min: number; max: number; colors?: readonly string[]; label?: string },
232
+ ) => DiagramBuilder;
233
+ badge: (
234
+ id: string,
235
+ opts: { source: string; colorSource?: string; map?: readonly { value: string; color: string }[]; label?: string },
236
+ ) => DiagramBuilder;
237
+ statusDot: (
238
+ id: string,
239
+ opts: { source: string; map: readonly { value: string; color: string; label?: string }[]; label?: string },
240
+ ) => DiagramBuilder;
241
+ /** SVG path を signal の 0-100 進行率で stroke-dashoffset animation */
242
+ pathProgress: (
243
+ id: string,
244
+ opts: {
245
+ source: string;
246
+ pathD: string;
247
+ viewW?: number;
248
+ viewH?: number;
249
+ strokeWidth?: number;
250
+ color?: string;
251
+ max?: number;
252
+ label?: string;
253
+ },
254
+ ) => DiagramBuilder;
255
+ /** array signal を bullet list 表示 (各 element を row 化) */
256
+ arrayList: (
257
+ id: string,
258
+ opts: { source: string; itemTemplate?: string; max?: number; label?: string },
259
+ ) => DiagramBuilder;
260
+ /** array signal の各 element を bar 高さで並列表示 (histogram 的) */
261
+ arrayBar: (
262
+ id: string,
263
+ opts: { source: string; min: number; max: number; color?: string; label?: string },
264
+ ) => DiagramBuilder;
265
+ /** array signal を line chart (折れ線 + optional area fill) */
266
+ lineChart: (
267
+ id: string,
268
+ opts: {
269
+ source: string;
270
+ min: number;
271
+ max: number;
272
+ viewW?: number;
273
+ viewH?: number;
274
+ color?: string;
275
+ fill?: boolean;
276
+ label?: string;
277
+ },
278
+ ) => DiagramBuilder;
279
+ /** 2 array signal を並列 bar で比較 (A/B histogram) */
280
+ stackedBar: (
281
+ id: string,
282
+ opts: {
283
+ sourceA: string;
284
+ sourceB: string;
285
+ min: number;
286
+ max: number;
287
+ colorA?: string;
288
+ colorB?: string;
289
+ label?: string;
290
+ },
291
+ ) => DiagramBuilder;
292
+ /** array signal を左から累積、 element 差分を bar 表示 (財務 waterfall chart 用) */
293
+ waterfall: (
294
+ id: string,
295
+ opts: {
296
+ source: string;
297
+ min: number;
298
+ max: number;
299
+ viewW?: number;
300
+ viewH?: number;
301
+ colorPos?: string;
302
+ colorNeg?: string;
303
+ label?: string;
304
+ },
305
+ ) => DiagramBuilder;
306
+ /** 2D array signal を色 gradient cell で表示 (confusion matrix / heatmap) */
307
+ matrix: (
308
+ id: string,
309
+ opts: {
310
+ source: string;
311
+ min: number;
312
+ max: number;
313
+ cellSize?: number;
314
+ colors?: readonly string[];
315
+ showValue?: boolean;
316
+ label?: string;
317
+ },
318
+ ) => DiagramBuilder;
319
+ /** array signal を progress bar list として表示 (task list progress) */
320
+ progressGroup: (
321
+ id: string,
322
+ opts: {
323
+ source: string;
324
+ max: number;
325
+ labelSource?: string;
326
+ color?: string;
327
+ label?: string;
328
+ },
329
+ ) => DiagramBuilder;
330
+ /** array signal を横 timeline event marker で表示 (login → auth → response 系 flow) */
331
+ sequenceTimeline: (
332
+ id: string,
333
+ opts: {
334
+ source: string;
335
+ min: number;
336
+ max: number;
337
+ viewW?: number;
338
+ viewH?: number;
339
+ color?: string;
340
+ label?: string;
341
+ },
342
+ ) => DiagramBuilder;
343
+ /** N 次元 array を polygon spider chart で表示 */
344
+ radar: (
345
+ id: string,
346
+ opts: {
347
+ source: string;
348
+ max: number;
349
+ viewW?: number;
350
+ viewH?: number;
351
+ color?: string;
352
+ labelSource?: string;
353
+ label?: string;
354
+ },
355
+ ) => DiagramBuilder;
356
+ /** 3D data (x, y, r) を bubbles で表示 */
357
+ bubbleChart: (
358
+ id: string,
359
+ opts: {
360
+ source: string;
361
+ xMin: number;
362
+ xMax: number;
363
+ yMin: number;
364
+ yMax: number;
365
+ rMin: number;
366
+ rMax: number;
367
+ viewW?: number;
368
+ viewH?: number;
369
+ color?: string;
370
+ label?: string;
371
+ },
372
+ ) => DiagramBuilder;
373
+ /** array を multi-segment donut chart で表示 (percent-ring の N 分割版) */
374
+ donut: (
375
+ id: string,
376
+ opts: {
377
+ source: string;
378
+ viewW?: number;
379
+ viewH?: number;
380
+ colors?: readonly string[];
381
+ innerRatio?: number;
382
+ label?: string;
383
+ },
384
+ ) => DiagramBuilder;
385
+ /** 1 年分の日次 count を GitHub-style contribution heatmap で表示 (53 週 × 7 日) */
386
+ calendarHeatmap: (
387
+ id: string,
388
+ opts: {
389
+ source: string;
390
+ max: number;
391
+ cellSize?: number;
392
+ cellGap?: number;
393
+ colors?: readonly string[];
394
+ label?: string;
395
+ },
396
+ ) => DiagramBuilder;
397
+ /** 大 canvas 上の viewport rect を縮小表示 (overview 用) */
398
+ miniMap: (
399
+ id: string,
400
+ opts: {
401
+ source: string;
402
+ canvasW: number;
403
+ canvasH: number;
404
+ viewW?: number;
405
+ viewH?: number;
406
+ color?: string;
407
+ label?: string;
408
+ },
409
+ ) => DiagramBuilder;
410
+ /** 現在値 + 直前値との delta + mini sparkline を 1 tile に composite (dashboard KPI 単体) */
411
+ kpiCard: (
412
+ id: string,
413
+ opts: {
414
+ source: string;
415
+ historySource: string;
416
+ comparisonSource: string;
417
+ unit?: string;
418
+ colorPos?: string;
419
+ colorNeg?: string;
420
+ label?: string;
421
+ },
422
+ ) => DiagramBuilder;
423
+ /** OHLC array を蝋燭足 (candlestick chart) で表示 (finance 用) */
424
+ candlestick: (
425
+ id: string,
426
+ opts: {
427
+ source: string;
428
+ min: number;
429
+ max: number;
430
+ viewW?: number;
431
+ viewH?: number;
432
+ colorUp?: string;
433
+ colorDown?: string;
434
+ label?: string;
435
+ },
436
+ ) => DiagramBuilder;
437
+ /** 2-set Venn diagram (`[|A|, |B|, |A∩B|]`) */
438
+ venn: (
439
+ id: string,
440
+ opts: {
441
+ source: string;
442
+ viewW?: number;
443
+ viewH?: number;
444
+ colorA?: string;
445
+ colorB?: string;
446
+ labelA?: string;
447
+ labelB?: string;
448
+ label?: string;
449
+ },
450
+ ) => DiagramBuilder;
451
+ /** before/after slope chart (`[[before, after, name], ...]`) */
452
+ slope: (
453
+ id: string,
454
+ opts: {
455
+ source: string;
456
+ min: number;
457
+ max: number;
458
+ viewW?: number;
459
+ viewH?: number;
460
+ colorUp?: string;
461
+ colorDown?: string;
462
+ label?: string;
463
+ },
464
+ ) => DiagramBuilder;
465
+ /** conversion funnel chart (`[[stage, count], ...]`) */
466
+ funnel: (
467
+ id: string,
468
+ opts: {
469
+ source: string;
470
+ viewW?: number;
471
+ viewH?: number;
472
+ colorTop?: string;
473
+ colorBottom?: string;
474
+ label?: string;
475
+ },
476
+ ) => DiagramBuilder;
477
+ /** gantt task timeline (`[[name, start, duration], ...]`) */
478
+ gantt: (
479
+ id: string,
480
+ opts: {
481
+ source: string;
482
+ min: number;
483
+ max: number;
484
+ viewW?: number;
485
+ viewH?: number;
486
+ color?: string;
487
+ label?: string;
488
+ },
489
+ ) => DiagramBuilder;
490
+ /** treemap hierarchical rectangles (`[[name, size], ...]`) */
491
+ treemap: (
492
+ id: string,
493
+ opts: {
494
+ source: string;
495
+ viewW?: number;
496
+ viewH?: number;
497
+ colors?: readonly string[];
498
+ label?: string;
499
+ },
500
+ ) => DiagramBuilder;
501
+ /** 2 column sankey flow diagram (`[[fromName, toName, flow], ...]`) */
502
+ sankey: (
503
+ id: string,
504
+ opts: {
505
+ source: string;
506
+ viewW?: number;
507
+ viewH?: number;
508
+ colors?: readonly string[];
509
+ label?: string;
510
+ },
511
+ ) => DiagramBuilder;
512
+ /** nightingale rose polar area chart */
513
+ polarArea: (
514
+ id: string,
515
+ opts: {
516
+ source: string;
517
+ max: number;
518
+ viewW?: number;
519
+ viewH?: number;
520
+ colors?: readonly string[];
521
+ labelSource?: string;
522
+ label?: string;
523
+ },
524
+ ) => DiagramBuilder;
525
+ /** wizard step indicator (current index + step names) */
526
+ stepIndicator: (
527
+ id: string,
528
+ opts: {
529
+ source: string;
530
+ stepsSource: string;
531
+ viewW?: number;
532
+ viewH?: number;
533
+ colorActive?: string;
534
+ colorPending?: string;
535
+ label?: string;
536
+ },
537
+ ) => DiagramBuilder;
538
+ /** bullet-chart (KPI actual vs target + 3 range) */
539
+ bulletChart: (
540
+ id: string,
541
+ opts: {
542
+ source: string;
543
+ targetSource: string;
544
+ max: number;
545
+ rangeBad?: number;
546
+ rangeAvg?: number;
547
+ viewW?: number;
548
+ viewH?: number;
549
+ colorActual?: string;
550
+ label?: string;
551
+ },
552
+ ) => DiagramBuilder;
553
+ /** 大 numeric display (scoreboard style) */
554
+ numberBoard: (
555
+ id: string,
556
+ opts: {
557
+ source: string;
558
+ prefix?: string;
559
+ suffix?: string;
560
+ size?: number;
561
+ color?: string;
562
+ caption?: string;
563
+ label?: string;
564
+ },
565
+ ) => DiagramBuilder;
566
+ /** ranked list (top-N with medal decoration) */
567
+ leaderboard: (
568
+ id: string,
569
+ opts: {
570
+ source: string;
571
+ max?: number;
572
+ color?: string;
573
+ label?: string;
574
+ },
575
+ ) => DiagramBuilder;
576
+ /** 3-color status indicator (traffic light) */
577
+ trafficLight: (
578
+ id: string,
579
+ opts: {
580
+ source: string;
581
+ viewW?: number;
582
+ viewH?: number;
583
+ label?: string;
584
+ },
585
+ ) => DiagramBuilder;
586
+ /** weighted tag cloud (`[[tag, weight], ...]`) */
587
+ tagCloud: (
588
+ id: string,
589
+ opts: {
590
+ source: string;
591
+ minSize?: number;
592
+ maxSize?: number;
593
+ colors?: readonly string[];
594
+ label?: string;
595
+ },
596
+ ) => DiagramBuilder;
597
+ /** activity feed (`[[actor, action, time], ...]`) */
598
+ activityFeed: (
599
+ id: string,
600
+ opts: {
601
+ source: string;
602
+ max?: number;
603
+ color?: string;
604
+ label?: string;
605
+ },
606
+ ) => DiagramBuilder;
607
+ /** N star rating display (half-star 対応) */
608
+ rating: (
609
+ id: string,
610
+ opts: {
611
+ source: string;
612
+ count?: number;
613
+ color?: string;
614
+ label?: string;
615
+ },
616
+ ) => DiagramBuilder;
617
+ /** alert card (info/warn/error/success 4 kind + title + body) */
618
+ notification: (
619
+ id: string,
620
+ opts: {
621
+ kindSource: string;
622
+ titleSource: string;
623
+ bodySource?: string;
624
+ label?: string;
625
+ },
626
+ ) => DiagramBuilder;
627
+ /** git commit style +N / -N counter */
628
+ diffCounter: (
629
+ id: string,
630
+ opts: {
631
+ additionsSource: string;
632
+ deletionsSource: string;
633
+ colorAdd?: string;
634
+ colorDel?: string;
635
+ label?: string;
636
+ },
637
+ ) => DiagramBuilder;
638
+ /** conversation thread (`[[author, text, isSelf], ...]`) */
639
+ chatBubble: (
640
+ id: string,
641
+ opts: {
642
+ source: string;
643
+ max?: number;
644
+ colorSelf?: string;
645
+ colorOther?: string;
646
+ label?: string;
647
+ },
648
+ ) => DiagramBuilder;
649
+ /** user avatar circle (initials + color) */
650
+ avatar: (
651
+ id: string,
652
+ opts: {
653
+ source: string;
654
+ size?: number;
655
+ color?: string;
656
+ label?: string;
657
+ },
658
+ ) => DiagramBuilder;
659
+ /** checklist (`[[label, checked], ...]`) with progress % */
660
+ checklist: (
661
+ id: string,
662
+ opts: {
663
+ source: string;
664
+ color?: string;
665
+ label?: string;
666
+ },
667
+ ) => DiagramBuilder;
668
+ /** 270° dial gauge (speedometer / tachometer) */
669
+ circularGauge: (
670
+ id: string,
671
+ opts: {
672
+ source: string;
673
+ min: number;
674
+ max: number;
675
+ viewW?: number;
676
+ viewH?: number;
677
+ color?: string;
678
+ unit?: string;
679
+ label?: string;
680
+ },
681
+ ) => DiagramBuilder;
682
+ /** e-commerce price tag (old/new/discount%) */
683
+ priceTag: (
684
+ id: string,
685
+ opts: {
686
+ oldSource: string;
687
+ newSource: string;
688
+ currency?: string;
689
+ colorNew?: string;
690
+ colorOld?: string;
691
+ colorDiscount?: string;
692
+ label?: string;
693
+ },
694
+ ) => DiagramBuilder;
695
+ /** loading spinner (running/done/error) + text */
696
+ spinner: (
697
+ id: string,
698
+ opts: {
699
+ source: string;
700
+ textSource: string;
701
+ color?: string;
702
+ label?: string;
703
+ },
704
+ ) => DiagramBuilder;
705
+ /** letter grade (A/B/C/D/F) with color band */
706
+ grade: (
707
+ id: string,
708
+ opts: {
709
+ source: string;
710
+ max?: number;
711
+ label?: string;
712
+ },
713
+ ) => DiagramBuilder;
714
+ /** MM:SS.ms timer display */
715
+ stopwatch: (
716
+ id: string,
717
+ opts: {
718
+ source: string;
719
+ runningSource?: string;
720
+ size?: number;
721
+ color?: string;
722
+ label?: string;
723
+ },
724
+ ) => DiagramBuilder;
725
+ /** confidence meter (0-100% + 3 color band) */
726
+ confidenceMeter: (
727
+ id: string,
728
+ opts: {
729
+ source: string;
730
+ lowThreshold?: number;
731
+ highThreshold?: number;
732
+ viewW?: number;
733
+ viewH?: number;
734
+ label?: string;
735
+ },
736
+ ) => DiagramBuilder;
737
+ /** emoji reaction bar (`[[emoji, count], ...]`) */
738
+ reactionBar: (
739
+ id: string,
740
+ opts: {
741
+ source: string;
742
+ color?: string;
743
+ label?: string;
744
+ },
745
+ ) => DiagramBuilder;
746
+ /** small pill badges list (string array or `[[label, color], ...]`) */
747
+ pillGroup: (
748
+ id: string,
749
+ opts: {
750
+ source: string;
751
+ color?: string;
752
+ label?: string;
753
+ },
754
+ ) => DiagramBuilder;
755
+ /** segmented battery/fuel bar (0-100% + 3 color band) */
756
+ fuelBar: (
757
+ id: string,
758
+ opts: {
759
+ source: string;
760
+ segments?: number;
761
+ lowThreshold?: number;
762
+ highThreshold?: number;
763
+ viewW?: number;
764
+ viewH?: number;
765
+ label?: string;
766
+ },
767
+ ) => DiagramBuilder;
768
+ /** 2x2 stat grid (`[[name, value, unit?], ...]`) */
769
+ metricsGrid: (
770
+ id: string,
771
+ opts: {
772
+ source: string;
773
+ color?: string;
774
+ label?: string;
775
+ },
776
+ ) => DiagramBuilder;
777
+ /** vertical thermometer (min-max normalized) */
778
+ thermometer: (
779
+ id: string,
780
+ opts: {
781
+ source: string;
782
+ min: number;
783
+ max: number;
784
+ viewW?: number;
785
+ viewH?: number;
786
+ color?: string;
787
+ unit?: string;
788
+ label?: string;
789
+ },
790
+ ) => DiagramBuilder;
791
+ /** icon + label + value tiles (`[[icon, label, value], ...]`) */
792
+ iconTile: (
793
+ id: string,
794
+ opts: {
795
+ source: string;
796
+ color?: string;
797
+ label?: string;
798
+ },
799
+ ) => DiagramBuilder;
800
+ /** crypto/asset token list (`[[icon, name, amount, deltaPct], ...]`) */
801
+ tokenList: (
802
+ id: string,
803
+ opts: {
804
+ source: string;
805
+ colorUp?: string;
806
+ colorDown?: string;
807
+ label?: string;
808
+ },
809
+ ) => DiagramBuilder;
810
+ /** 2D map pins (`[[name, x, y], ...]`) */
811
+ mapPin: (
812
+ id: string,
813
+ opts: {
814
+ source: string;
815
+ xMin: number;
816
+ xMax: number;
817
+ yMin: number;
818
+ yMax: number;
819
+ viewW?: number;
820
+ viewH?: number;
821
+ color?: string;
822
+ label?: string;
823
+ },
824
+ ) => DiagramBuilder;
825
+ /** priority badge (high/med/low + optional label text) */
826
+ priorityBadge: (
827
+ id: string,
828
+ opts: {
829
+ source: string;
830
+ textSource?: string;
831
+ label?: string;
832
+ },
833
+ ) => DiagramBuilder;
834
+ /** 1st/2nd/3rd podium (competition winners) */
835
+ podium: (
836
+ id: string,
837
+ opts: {
838
+ source: string;
839
+ viewW?: number;
840
+ viewH?: number;
841
+ label?: string;
842
+ },
843
+ ) => DiagramBuilder;
844
+ /** poll option bar (`[[option, count], ...]` + winner highlight) */
845
+ pollBar: (
846
+ id: string,
847
+ opts: {
848
+ source: string;
849
+ color?: string;
850
+ colorWinner?: string;
851
+ label?: string;
852
+ },
853
+ ) => DiagramBuilder;
854
+ /** stacked circular avatars (name array) */
855
+ userStack: (
856
+ id: string,
857
+ opts: {
858
+ source: string;
859
+ max?: number;
860
+ size?: number;
861
+ label?: string;
862
+ },
863
+ ) => DiagramBuilder;
864
+ /** git commit history (`[[sha, msg, author], ...]`) */
865
+ commitList: (
866
+ id: string,
867
+ opts: {
868
+ source: string;
869
+ max?: number;
870
+ color?: string;
871
+ label?: string;
872
+ },
873
+ ) => DiagramBuilder;
874
+ /** audio/video mini player (play/pause + progress + time) */
875
+ mediaPlayer: (
876
+ id: string,
877
+ opts: {
878
+ source: string;
879
+ durationSource: string;
880
+ playingSource: string;
881
+ color?: string;
882
+ viewW?: number;
883
+ label?: string;
884
+ },
885
+ ) => DiagramBuilder;
886
+ /** event stream log (`[[timestamp, severity, msg], ...]`) */
887
+ eventLog: (
888
+ id: string,
889
+ opts: {
890
+ source: string;
891
+ max?: number;
892
+ label?: string;
893
+ },
894
+ ) => DiagramBuilder;
895
+ /** search result hits (`[[title, snippet, url], ...]`) */
896
+ searchResult: (
897
+ id: string,
898
+ opts: {
899
+ source: string;
900
+ max?: number;
901
+ color?: string;
902
+ label?: string;
903
+ },
904
+ ) => DiagramBuilder;
905
+ /** quarterly roadmap Q1-Q4 (`[[quarter, [items]], ...]`) */
906
+ roadmap: (
907
+ id: string,
908
+ opts: {
909
+ source: string;
910
+ viewW?: number;
911
+ viewH?: number;
912
+ label?: string;
913
+ },
914
+ ) => DiagramBuilder;
915
+ /** 5-day weather forecast (`[[day, icon, high, low], ...]`) */
916
+ weatherForecast: (
917
+ id: string,
918
+ opts: {
919
+ source: string;
920
+ label?: string;
921
+ },
922
+ ) => DiagramBuilder;
923
+ /** video card list (`[[emoji, title, duration, viewCount], ...]`) */
924
+ videoCard: (
925
+ id: string,
926
+ opts: {
927
+ source: string;
928
+ max?: number;
929
+ color?: string;
930
+ label?: string;
931
+ },
932
+ ) => DiagramBuilder;
933
+ /** order tracking steps (current step index + step names) */
934
+ orderStatus: (
935
+ id: string,
936
+ opts: {
937
+ source: string;
938
+ stepsSource: string;
939
+ color?: string;
940
+ label?: string;
941
+ },
942
+ ) => DiagramBuilder;
943
+ /** attendance grid (rows=days, cols=members) */
944
+ attendanceGrid: (
945
+ id: string,
946
+ opts: {
947
+ source: string;
948
+ membersSource: string;
949
+ color?: string;
950
+ label?: string;
951
+ },
952
+ ) => DiagramBuilder;
953
+ /** multi-timezone clock (`[[city, offsetHours, HH:MM], ...]`) */
954
+ timezoneClock: (
955
+ id: string,
956
+ opts: {
957
+ source: string;
958
+ color?: string;
959
+ label?: string;
960
+ },
961
+ ) => DiagramBuilder;
962
+ /** form field summary (`[[fieldName, value], ...]`) */
963
+ formSummary: (
964
+ id: string,
965
+ opts: {
966
+ source: string;
967
+ color?: string;
968
+ label?: string;
969
+ },
970
+ ) => DiagramBuilder;
971
+ /** song playlist queue with current index highlight */
972
+ songQueue: (
973
+ id: string,
974
+ opts: {
975
+ source: string;
976
+ currentSource?: string;
977
+ max?: number;
978
+ color?: string;
979
+ label?: string;
980
+ },
981
+ ) => DiagramBuilder;
982
+ /** 1 month calendar view (day cells + event dots + today border) */
983
+ calendarMonth: (
984
+ id: string,
985
+ opts: {
986
+ source: string;
987
+ monthName?: string;
988
+ color?: string;
989
+ label?: string;
990
+ },
991
+ ) => DiagramBuilder;
992
+ /** CLI terminal output (`[[prompt, cmd, output], ...]`) */
993
+ terminal: (
994
+ id: string,
995
+ opts: {
996
+ source: string;
997
+ max?: number;
998
+ color?: string;
999
+ label?: string;
1000
+ },
1001
+ ) => DiagramBuilder;
1002
+ /** 8×8 chess board with piece placement */
1003
+ chessBoard: (
1004
+ id: string,
1005
+ opts: {
1006
+ source: string;
1007
+ cellSize?: number;
1008
+ label?: string;
1009
+ },
1010
+ ) => DiagramBuilder;
1011
+ /** 3 column kanban board (Todo / In Progress / Done) */
1012
+ kanbanBoard: (
1013
+ id: string,
1014
+ opts: {
1015
+ source: string;
1016
+ columnWidth?: number;
1017
+ max?: number;
1018
+ label?: string;
1019
+ },
1020
+ ) => DiagramBuilder;
1021
+ /** navigation breadcrumb (Home > Docs > API pattern) */
1022
+ breadcrumb: (
1023
+ id: string,
1024
+ opts: {
1025
+ source: string;
1026
+ currentSource?: string;
1027
+ color?: string;
1028
+ label?: string;
1029
+ },
1030
+ ) => DiagramBuilder;
1031
+ /** 縦 timeline (dot + line + event text) */
1032
+ timelineVertical: (
1033
+ id: string,
1034
+ opts: {
1035
+ source: string;
1036
+ color?: string;
1037
+ max?: number;
1038
+ label?: string;
1039
+ },
1040
+ ) => DiagramBuilder;
1041
+ /** 時系列 status 遷移 strip (Active/Idle/Error 等) */
1042
+ statusTimeline: (
1043
+ id: string,
1044
+ opts: {
1045
+ source: string;
1046
+ colorMap?: Array<{ status: string; color: string }>;
1047
+ max?: number;
1048
+ label?: string;
1049
+ },
1050
+ ) => DiagramBuilder;
1051
+ /** 7-day mini calendar (week view) */
1052
+ calendarWeek: (
1053
+ id: string,
1054
+ opts: {
1055
+ source: string;
1056
+ cellSize?: number;
1057
+ color?: string;
1058
+ label?: string;
1059
+ },
1060
+ ) => DiagramBuilder;
1061
+ /** 2 KPI horizontal comparison bar (A vs B) */
1062
+ kpiComparison: (
1063
+ id: string,
1064
+ opts: {
1065
+ source: string;
1066
+ max?: number;
1067
+ colorA?: string;
1068
+ colorB?: string;
1069
+ label?: string;
1070
+ },
1071
+ ) => DiagramBuilder;
1072
+ /** numbered step + progress line (wizard progress) */
1073
+ stepProgress: (
1074
+ id: string,
1075
+ opts: {
1076
+ source: string;
1077
+ stepsSource: string;
1078
+ color?: string;
1079
+ label?: string;
1080
+ },
1081
+ ) => DiagramBuilder;
1082
+ /** user list + online/away/offline presence dot */
1083
+ userPresence: (
1084
+ id: string,
1085
+ opts: {
1086
+ source: string;
1087
+ max?: number;
1088
+ label?: string;
1089
+ },
1090
+ ) => DiagramBuilder;
1091
+ /** thumbs up/down rating (2 count + up %) */
1092
+ ratingThumb: (
1093
+ id: string,
1094
+ opts: {
1095
+ source: string;
1096
+ colorUp?: string;
1097
+ colorDown?: string;
1098
+ label?: string;
1099
+ },
1100
+ ) => DiagramBuilder;
1101
+ /** organization hierarchy 3 level (root/mid/leaf) */
1102
+ orgChartMini: (
1103
+ id: string,
1104
+ opts: {
1105
+ source: string;
1106
+ color?: string;
1107
+ label?: string;
1108
+ },
1109
+ ) => DiagramBuilder;
1110
+ /** KPI current + delta + sparkline を 1 tile */
1111
+ kpiTrendTile: (
1112
+ id: string,
1113
+ opts: {
1114
+ source: string;
1115
+ prevSource: string;
1116
+ historySource: string;
1117
+ unit?: string;
1118
+ colorPos?: string;
1119
+ colorNeg?: string;
1120
+ label?: string;
1121
+ },
1122
+ ) => DiagramBuilder;
1123
+ /** 3-4 emoji poll (👍/❤️/🎉 等) */
1124
+ quickPollEmoji: (
1125
+ id: string,
1126
+ opts: {
1127
+ source: string;
1128
+ colorWinner?: string;
1129
+ label?: string;
1130
+ },
1131
+ ) => DiagramBuilder;
1132
+ /** voice message playback UI (waveform bars + play + duration) */
1133
+ voiceMessage: (
1134
+ id: string,
1135
+ opts: {
1136
+ source: string;
1137
+ progressSource?: string;
1138
+ duration?: number;
1139
+ colorPlay?: string;
1140
+ colorBar?: string;
1141
+ label?: string;
1142
+ },
1143
+ ) => DiagramBuilder;
1144
+ /** thread summary card (unread + participants + last author + time ago) */
1145
+ threadSummary: (
1146
+ id: string,
1147
+ opts: {
1148
+ source: string;
1149
+ colorUnread?: string;
1150
+ label?: string;
1151
+ },
1152
+ ) => DiagramBuilder;
1153
+ /** message read receipt (single / double check) */
1154
+ readReceipt: (
1155
+ id: string,
1156
+ opts: {
1157
+ source: string;
1158
+ colorRead?: string;
1159
+ colorPending?: string;
1160
+ label?: string;
1161
+ },
1162
+ ) => DiagramBuilder;
1163
+ /** password strength 4-level meter */
1164
+ passwordStrength: (
1165
+ id: string,
1166
+ opts: {
1167
+ source: string;
1168
+ colorStrong?: string;
1169
+ colorWeak?: string;
1170
+ label?: string;
1171
+ },
1172
+ ) => DiagramBuilder;
1173
+ /** 6-digit OTP input box grid */
1174
+ otpInput: (
1175
+ id: string,
1176
+ opts: {
1177
+ source: string;
1178
+ colorFocus?: string;
1179
+ label?: string;
1180
+ },
1181
+ ) => DiagramBuilder;
1182
+ /** file drag-and-drop upload zone */
1183
+ fileDropzone: (
1184
+ id: string,
1185
+ opts: {
1186
+ source: string;
1187
+ colorActive?: string;
1188
+ label?: string;
1189
+ },
1190
+ ) => DiagramBuilder;
1191
+ /** real-time log tail (5 rows, level color) */
1192
+ logStream: (
1193
+ id: string,
1194
+ opts: {
1195
+ source: string;
1196
+ label?: string;
1197
+ },
1198
+ ) => DiagramBuilder;
1199
+ /** severity alert banner (info / success / warn / error) */
1200
+ alertBanner: (
1201
+ id: string,
1202
+ opts: {
1203
+ source: string;
1204
+ label?: string;
1205
+ },
1206
+ ) => DiagramBuilder;
1207
+ /** service health matrix (down / degraded / up) */
1208
+ serviceHealth: (
1209
+ id: string,
1210
+ opts: {
1211
+ source: string;
1212
+ label?: string;
1213
+ },
1214
+ ) => DiagramBuilder;
1215
+ /** shopping cart summary (items / subtotal / shipping / total) */
1216
+ cartSummary: (
1217
+ id: string,
1218
+ opts: {
1219
+ source: string;
1220
+ currency?: string;
1221
+ colorTotal?: string;
1222
+ label?: string;
1223
+ },
1224
+ ) => DiagramBuilder;
1225
+ /** pricing tier card (name + price + 3 features + CTA) */
1226
+ pricingTier: (
1227
+ id: string,
1228
+ opts: {
1229
+ source: string;
1230
+ colorAccent?: string;
1231
+ currency?: string;
1232
+ label?: string;
1233
+ },
1234
+ ) => DiagramBuilder;
1235
+ /** coupon code entry + apply badge */
1236
+ couponCode: (
1237
+ id: string,
1238
+ opts: {
1239
+ source: string;
1240
+ colorApplied?: string;
1241
+ label?: string;
1242
+ },
1243
+ ) => DiagramBuilder;
1244
+ /** blog article preview card (title / excerpt / author / timeAgo) */
1245
+ articlePreview: (
1246
+ id: string,
1247
+ opts: {
1248
+ source: string;
1249
+ colorAccent?: string;
1250
+ label?: string;
1251
+ },
1252
+ ) => DiagramBuilder;
1253
+ /** table of contents nav with active section highlight */
1254
+ tocNav: (
1255
+ id: string,
1256
+ opts: {
1257
+ source: string;
1258
+ colorActive?: string;
1259
+ label?: string;
1260
+ },
1261
+ ) => DiagramBuilder;
1262
+ /** social share button row */
1263
+ shareButtons: (
1264
+ id: string,
1265
+ opts: {
1266
+ source: string;
1267
+ label?: string;
1268
+ },
1269
+ ) => DiagramBuilder;
1270
+ };
1271
+
1272
+ export type InputChain = {
1273
+ slider: (
1274
+ id: string,
1275
+ opts: { min: number; max: number; step?: number; defaultValue: number; label?: string },
1276
+ ) => DiagramBuilder;
1277
+ number: (
1278
+ id: string,
1279
+ opts: { min?: number; max?: number; defaultValue: number; label?: string },
1280
+ ) => DiagramBuilder;
1281
+ dropdown: (
1282
+ id: string,
1283
+ opts: { options: readonly string[]; defaultValue: string; label?: string },
1284
+ ) => DiagramBuilder;
1285
+ toggle: (
1286
+ id: string,
1287
+ opts: { defaultValue: boolean; label?: string },
1288
+ ) => DiagramBuilder;
1289
+ /** 2 軸 slider = XY pad、 単一 id で pointer position を保持 (x/y は同 signal に格納) */
1290
+ xypad: (
1291
+ id: string,
1292
+ opts: {
1293
+ xMin: number;
1294
+ xMax: number;
1295
+ yMin: number;
1296
+ yMax: number;
1297
+ defaultX: number;
1298
+ defaultY: number;
1299
+ label?: string;
1300
+ },
1301
+ ) => DiagramBuilder;
1302
+ /** +/- ボタンで integer 増減、 slider より狭い変化範囲 (数値の細かい調整) */
1303
+ stepper: (
1304
+ id: string,
1305
+ opts: { min?: number; max?: number; step?: number; defaultValue: number; label?: string },
1306
+ ) => DiagramBuilder;
1307
+ /** 排他選択、 dropdown と同じ semantics だが視覚は横並び button */
1308
+ radio: (
1309
+ id: string,
1310
+ opts: { options: readonly string[]; defaultValue: string; label?: string },
1311
+ ) => DiagramBuilder;
1312
+ /** hex color (#RRGGBB) picker、 SVG stroke / fill に signal 経由で bind */
1313
+ color: (
1314
+ id: string,
1315
+ opts: { defaultValue: string; label?: string },
1316
+ ) => DiagramBuilder;
1317
+ /**
1318
+ * timeline = 時間軸を signal 化、 0..1 progress + play/pause/seek/speed 制御 UI。
1319
+ * SVG animation を signal に紐付けて手動制御可能に。
1320
+ */
1321
+ timeline: (
1322
+ id: string,
1323
+ opts: {
1324
+ duration: number;
1325
+ autoplay?: boolean;
1326
+ loop?: boolean;
1327
+ speeds?: readonly number[];
1328
+ defaultSpeedIdx?: number;
1329
+ label?: string;
1330
+ },
1331
+ ) => DiagramBuilder;
1332
+ range: (
1333
+ id: string,
1334
+ opts: { min: number; max: number; step?: number; defaultLo: number; defaultHi: number; label?: string },
1335
+ ) => DiagramBuilder;
1336
+ multiSelect: (
1337
+ id: string,
1338
+ opts: { options: readonly string[]; defaultValues: readonly string[]; label?: string },
1339
+ ) => DiagramBuilder;
1340
+ tabs: (
1341
+ id: string,
1342
+ opts: { options: readonly string[]; defaultValue: string; label?: string },
1343
+ ) => DiagramBuilder;
1344
+ datetime: (
1345
+ id: string,
1346
+ opts: { defaultValue: string; label?: string },
1347
+ ) => DiagramBuilder;
1348
+ text: (
1349
+ id: string,
1350
+ opts: { defaultValue: string; placeholder?: string; maxLength?: number; label?: string },
1351
+ ) => DiagramBuilder;
1352
+ };
1353
+
1354
+ // ─── Phase builder (inner fluent for activate/tween/badge) ───
1355
+
1356
+ export type PhaseBuilder = {
1357
+ activate: (...ids: string[]) => PhaseBuilder;
1358
+ /** 数値 state の線形補間 (write phase で 100→90 等) */
1359
+ tween: (stateId: string, from: number, to: number) => PhaseBuilder;
1360
+ /** 任意型 state を即時切替 (この phase 到達で value 上書き、 lerp なし) */
1361
+ set: (stateId: string, value: string | number) => PhaseBuilder;
1362
+ badge: (text: string) => PhaseBuilder;
1363
+ };
1364
+
1365
+ type PhaseAcc = {
1366
+ activate: string[];
1367
+ tweens: Array<{ stateId: string; from: number; to: number }>;
1368
+ sets: Array<{ stateId: string; value: string | number }>;
1369
+ badge?: string;
1370
+ };
1371
+
1372
+ function makePhaseBuilder(acc: PhaseAcc): PhaseBuilder {
1373
+ const api: PhaseBuilder = {
1374
+ activate(...ids) {
1375
+ acc.activate.push(...ids);
1376
+ return api;
1377
+ },
1378
+ tween(stateId, from, to) {
1379
+ acc.tweens.push({ stateId, from, to });
1380
+ return api;
1381
+ },
1382
+ set(stateId, value) {
1383
+ acc.sets.push({ stateId, value });
1384
+ return api;
1385
+ },
1386
+ badge(text) {
1387
+ acc.badge = text;
1388
+ return api;
1389
+ },
1390
+ };
1391
+ return api;
1392
+ }
1393
+
1394
+ // ─── Diagram builder (fluent) ────────────────────────────────
1395
+
1396
+ export type DiagramBuilder = {
1397
+ lane: (id: string, opts: Omit<CdlLane, "id">) => DiagramBuilder;
1398
+ node: (id: string, opts: Omit<CdlNode, "id">) => DiagramBuilder;
1399
+ edge: (
1400
+ from: string,
1401
+ to: string,
1402
+ opts: {
1403
+ /** edge id (省略時は `${from}-${to}` ベースで auto 生成、 重複時は連番付与) */
1404
+ id?: string;
1405
+ label: string;
1406
+ sub?: string;
1407
+ /** tone (default "accent") */
1408
+ tone?: Tone;
1409
+ side?: Side;
1410
+ style?: EdgeStyle;
1411
+ /** FSM guard 条件、 text-dsl v0.5 inline option (`guard: "..."`) を CdlEdge.guard へ透過 */
1412
+ guard?: string;
1413
+ /** ER 関係 cardinality、 text-dsl v0.5 inline option (`cardinality: "1:N"`) を CdlEdge.cardinality へ透過 */
1414
+ cardinality?: string;
1415
+ labelOffsetX?: number;
1416
+ labelOffsetY?: number;
1417
+ routing?: "default" | "back-detour";
1418
+ /** CAR-549 SSOT ... true で label を edge path 上に重ねる (flowchart condition label 用) */
1419
+ overlay?: boolean;
1420
+ /** signal binding: stroke-width template (CAR edge-signal-binding) */
1421
+ widthBind?: string;
1422
+ /** signal binding: stroke color template */
1423
+ strokeBind?: string;
1424
+ /** signal binding: stroke-dashoffset template */
1425
+ dashOffsetBind?: string;
1426
+ },
1427
+ ) => DiagramBuilder;
1428
+ /**
1429
+ * 複数 node を 1 行で宣言する batch helper。
1430
+ * @example .nodes([{ id: "a", lane: "l", stack: 0, kind: "actor", title: "Client" }, ...])
1431
+ */
1432
+ nodes: (defs: Array<{ id: string } & Omit<CdlNode, "id">>) => DiagramBuilder;
1433
+ /**
1434
+ * repeat = N 個の同形 node を宣言的に生成 (CAR: repeat primitive)。
1435
+ * templateFn は index (0..count-1) を受け取って node spec を返す関数、 or template で
1436
+ * `{i}` を書けば index に置換される。 array に対する map より typed で、 index-based signal
1437
+ * 参照 (source: 'gas_{i}') が組める。
1438
+ *
1439
+ * 例: EIP1559 の gas rect 3 個並び (base の 1.0x / 1.2x / 1.5x)
1440
+ * .repeatNodes(3, (i) => ({
1441
+ * id: `rect-${i}`,
1442
+ * lane: `l${i+1}`,
1443
+ * stack: 0,
1444
+ * kind: 'dyn-rect',
1445
+ * title: `Block ${i+1}`,
1446
+ * shape: { kind: 'rect', source: `{gas${i+1}}`, fillMax: 100, orient: 'up' }
1447
+ * }))
1448
+ */
1449
+ repeatNodes: (
1450
+ count: number,
1451
+ templateFn: (i: number) => { id: string } & Omit<CdlNode, "id">,
1452
+ ) => DiagramBuilder;
1453
+ /**
1454
+ * gridNodes = 2D grid で node を配置、 rows × cols で iteration index (r, c) を渡す。
1455
+ * template で `{r}` / `{c}` / `{i}` (r*cols + c) を置換可能。 grid の見出し diagram (chess board / matrix / mesh) を宣言的に。
1456
+ */
1457
+ gridNodes: (
1458
+ rows: number,
1459
+ cols: number,
1460
+ templateFn: (r: number, c: number, i: number) => { id: string } & Omit<CdlNode, "id">,
1461
+ ) => DiagramBuilder;
1462
+ /**
1463
+ * radialNodes = 中心点周りの円周上に count 個の node を宣言的に配置 (hub-and-spoke)。
1464
+ * templateFn は (i, angle_deg, x_offset, y_offset) を受け取り、 spec に stack 値と
1465
+ * `{i}` / `{deg}` template 経由で id / title を組める。 SVG 座標系 (y 下向き) 前提で
1466
+ * angle=0 が右、 90 が下、 180 が左、 270 が上。 layout 上は 1 lane に stack=i として
1467
+ * 並べつつ、 render 側 relative offset で円形配置する想定 (consumer 側で offset を適用可)。
1468
+ */
1469
+ radialNodes: (
1470
+ count: number,
1471
+ radius: number,
1472
+ templateFn: (
1473
+ i: number,
1474
+ angleDeg: number,
1475
+ offsetX: number,
1476
+ offsetY: number,
1477
+ ) => { id: string } & Omit<CdlNode, "id">,
1478
+ ) => DiagramBuilder;
1479
+ /**
1480
+ * treeNodes = 深さ (depth) × 分岐数 (branching) の完全 tree layout。
1481
+ * 各 node の (level, posInLevel, i, offsetX, offsetY) を templateFn に渡す。
1482
+ * offsetX/Y は renderOffsetX/Y として直接使える相対 pixel。 root = level 0、 leaves = level depth-1。
1483
+ */
1484
+ treeNodes: (
1485
+ depth: number,
1486
+ branching: number,
1487
+ /** horizontal spacing (px) per leaf */
1488
+ hSpacing: number,
1489
+ /** vertical spacing (px) per level */
1490
+ vSpacing: number,
1491
+ templateFn: (
1492
+ level: number,
1493
+ posInLevel: number,
1494
+ i: number,
1495
+ offsetX: number,
1496
+ offsetY: number,
1497
+ ) => { id: string } & Omit<CdlNode, "id">,
1498
+ ) => DiagramBuilder;
1499
+ /**
1500
+ * 複数 edge を 1 行で宣言する batch helper。
1501
+ * @example .edges([{ from: "a", to: "b", label: "call" }, ...])
1502
+ */
1503
+ edges: (
1504
+ defs: Array<{
1505
+ from: string;
1506
+ to: string;
1507
+ id?: string;
1508
+ label: string;
1509
+ sub?: string;
1510
+ tone?: Tone;
1511
+ side?: Side;
1512
+ style?: EdgeStyle;
1513
+ guard?: string;
1514
+ cardinality?: string;
1515
+ labelOffsetX?: number;
1516
+ labelOffsetY?: number;
1517
+ routing?: "default" | "back-detour";
1518
+ /** CAR-549 SSOT ... true で label を edge path 上に重ねる (flowchart condition label 用) */
1519
+ overlay?: boolean;
1520
+ }>,
1521
+ ) => DiagramBuilder;
1522
+ state: (id: string, opts: Omit<CdlState, "id">) => DiagramBuilder;
1523
+ /**
1524
+ * arraySignal = 配列を保持する signal の shortcut (SSOT)。
1525
+ * 内部は state と同じ格納 (initial は JSON stringify)、 template では
1526
+ * `{sig[0]}` / `{sig.length}` / `{sig.sum}` / `{sig.max}` / `{sig.min}` / `{sig.avg}`
1527
+ * で access する (`render/utils.ts:interpolate` の拡張規則)。
1528
+ * event handler で update する場合は `JSON.stringify(newArray)` を setSignal に渡す。
1529
+ */
1530
+ arraySignal: (id: string, initial: (string | number)[]) => DiagramBuilder;
1531
+ /**
1532
+ * interactive input widget entry (CAR #243)。
1533
+ * `.input.slider(id, { min, max, ... })` の様に chain して widget spec を追加する。
1534
+ * consumer app 側の React / plain HTML が対応 signal に bind して SVG と反応させる。
1535
+ */
1536
+ input: InputChain;
1537
+ /**
1538
+ * formula primitive (CAR #244)。 safe subset の algebraic expression を diagram に追加、
1539
+ * consumer が createFormulaComputeds() で computed 化して signal と reactive bind する。
1540
+ * expression syntax error は本 method 内で throw (fail-fast)。
1541
+ * 例 `.formula("gasFee", "gasBase * (1 + delta/8)")`
1542
+ */
1543
+ formula: (id: string, expression: string) => DiagramBuilder;
1544
+ /**
1545
+ * derive chain = 「連鎖形 formula」 を N 個宣言的に生成 (CAR: derive chain)。
1546
+ * 各 iteration で `{i}` 置換と、 直前 iteration の id (`prev`) を参照可能。
1547
+ *
1548
+ * 例: gas1 = base、 gas2 = gas1 * 1.2、 gas3 = gas2 * 1.2 のような chain
1549
+ * .deriveChain('gas', 3, (i) => i === 0 ? 'base' : 'prev * 1.2')
1550
+ *
1551
+ * baseId は「gas1 / gas2 / gas3」 のように `gas${i+1}` として使われる、
1552
+ * expr fn は index (0..count-1) と prev id (前 iteration の完全 id) を受け取る。
1553
+ */
1554
+ deriveChain: (
1555
+ baseId: string,
1556
+ count: number,
1557
+ exprFn: (i: number, prev: string | null) => string,
1558
+ ) => DiagramBuilder;
1559
+ /**
1560
+ * animation primitive (CAR #245)。 `.animation.scroll(id, opts)` で scroll-driven trigger を追加。
1561
+ * 将来的に `.animation.time(...)` 等の time-based primitive を追加する余地を残す namespace。
1562
+ */
1563
+ animation: AnimationChain;
1564
+ /**
1565
+ * event handler primitive (CAR #246)。 `.on.click(target, handlerId)` / `.on.hover(...)` で
1566
+ * DOM event handler を bind、 consumer app 側で attachEventHandlers(root, diagram, handlers) 経由で
1567
+ * signal update 等を実行する。
1568
+ */
1569
+ on: EventChain;
1570
+ /**
1571
+ * readout primitive (visual widget for displaying signal / state values)。
1572
+ * `.readout.bar / .gauge / .stat / .sparkline` で spec 追加、 interactive panel に描画される。
1573
+ */
1574
+ readout: ReadoutChain;
1575
+ phase: (
1576
+ id: string,
1577
+ opts: { duration?: number; title: string; body: string },
1578
+ build: (p: PhaseBuilder) => PhaseBuilder,
1579
+ ) => DiagramBuilder;
1580
+ build: () => CdlDiagram;
1581
+ };
1582
+
1583
+ export function diagram(
1584
+ id: string,
1585
+ options: {
1586
+ topic: string;
1587
+ /**
1588
+ * structured-data (schema.org) 抽出の対象とみなすか (既定 = `"extract"`)。
1589
+ * figure 1 つを見せる見本など、 文章として取り出す中身を持たせるつもりが無い図に
1590
+ * `"exclude"` を付ける。 詳細は `CdlDiagram.structuredData` の doc 参照。
1591
+ */
1592
+ structuredData?: "extract" | "exclude";
1593
+ },
1594
+ ): DiagramBuilder {
1595
+ const lanes: CdlLane[] = [];
1596
+ const nodes: CdlNode[] = [];
1597
+ const edges: CdlEdge[] = [];
1598
+ const states: CdlState[] = [];
1599
+ const phases: CdlPhase[] = [];
1600
+ const inputs: CdlInput[] = [];
1601
+ const formulas: CdlFormula[] = [];
1602
+ const scrollTriggers: CdlScrollTrigger[] = [];
1603
+ const eventBindings: CdlEventBinding[] = [];
1604
+ const readouts: CdlReadout[] = [];
1605
+ const assertUniqueReadoutId = (rid: string) => {
1606
+ if (readouts.some((r) => r.id === rid)) {
1607
+ throw new Error(`readout '${rid}' 重複、 diagram 内で同 id は不可`);
1608
+ }
1609
+ };
1610
+ let eventBindingCounter = 0;
1611
+
1612
+ const validateEventTarget = (target: CdlEventTarget, event: string) => {
1613
+ // target の kind と id を validate (存在 check は runtime attach 側で行う、 builder では shape check のみ)
1614
+ switch (target.kind) {
1615
+ case "node":
1616
+ case "edge":
1617
+ case "lane":
1618
+ if (!target.id) {
1619
+ throw new Error(`event handler '${event}': target.kind '${target.kind}' には id が必須です`);
1620
+ }
1621
+ break;
1622
+ case "diagram":
1623
+ break;
1624
+ default: {
1625
+ // exhaustiveness check for future kind additions
1626
+ const _exhaustive: never = target;
1627
+ throw new Error(
1628
+ `event handler '${event}': unsupported target kind (got ${JSON.stringify(_exhaustive)})`,
1629
+ );
1630
+ }
1631
+ }
1632
+ };
1633
+
1634
+ // input / formula / scrollTrigger の id は共通 name space。 collision があると createFormulaComputeds が
1635
+ // signals → formulas の順で resolve するため、 formula 側 declaration が silent に無視されて
1636
+ // 誤った値を返す危険 (codex Round 2 finding、 例 .input.number("x").formula("x", "x+1").formula("y", "x*2") で
1637
+ // y が formula "x" ではなく input signal "x" を参照)。 chain の add 時点で cross-category collision を throw する。
1638
+ const assertUniqueInputId = (inputId: string) => {
1639
+ if (inputs.some((existing) => existing.id === inputId)) {
1640
+ throw new Error(`input id '${inputId}' が重複しています (既存 input: ${inputs.map((i) => i.id).join(", ")})`);
1641
+ }
1642
+ if (formulas.some((existing) => existing.id === inputId)) {
1643
+ throw new Error(
1644
+ `input id '${inputId}' が formula id と衝突しています (input と formula は共通 name space、 既存 formula: ${formulas.map((f) => f.id).join(", ")})`,
1645
+ );
1646
+ }
1647
+ if (scrollTriggers.some((existing) => existing.id === inputId)) {
1648
+ throw new Error(
1649
+ `input id '${inputId}' が scroll trigger id と衝突しています (共通 name space、 既存 scroll trigger: ${scrollTriggers.map((s) => s.id).join(", ")})`,
1650
+ );
1651
+ }
1652
+ };
1653
+
1654
+ const assertUniqueFormulaId = (formulaId: string) => {
1655
+ if (formulas.some((existing) => existing.id === formulaId)) {
1656
+ throw new Error(`formula id '${formulaId}' が重複しています (既存 formula: ${formulas.map((f) => f.id).join(", ")})`);
1657
+ }
1658
+ if (inputs.some((existing) => existing.id === formulaId)) {
1659
+ throw new Error(
1660
+ `formula id '${formulaId}' が input id と衝突しています (input と formula は共通 name space、 既存 input: ${inputs.map((i) => i.id).join(", ")})`,
1661
+ );
1662
+ }
1663
+ if (scrollTriggers.some((existing) => existing.id === formulaId)) {
1664
+ throw new Error(
1665
+ `formula id '${formulaId}' が scroll trigger id と衝突しています (共通 name space、 既存 scroll trigger: ${scrollTriggers.map((s) => s.id).join(", ")})`,
1666
+ );
1667
+ }
1668
+ };
1669
+
1670
+ const assertUniqueScrollTriggerId = (triggerId: string) => {
1671
+ if (scrollTriggers.some((existing) => existing.id === triggerId)) {
1672
+ throw new Error(`scroll trigger id '${triggerId}' が重複しています (既存 scroll trigger: ${scrollTriggers.map((s) => s.id).join(", ")})`);
1673
+ }
1674
+ if (inputs.some((existing) => existing.id === triggerId)) {
1675
+ throw new Error(
1676
+ `scroll trigger id '${triggerId}' が input id と衝突しています (共通 name space、 既存 input: ${inputs.map((i) => i.id).join(", ")})`,
1677
+ );
1678
+ }
1679
+ if (formulas.some((existing) => existing.id === triggerId)) {
1680
+ throw new Error(
1681
+ `scroll trigger id '${triggerId}' が formula id と衝突しています (共通 name space、 既存 formula: ${formulas.map((f) => f.id).join(", ")})`,
1682
+ );
1683
+ }
1684
+ };
1685
+
1686
+ // 重複しない edge id を生成する helper (auto id 用)。
1687
+ // 同じ from-to ペアが複数あれば連番 suffix を付ける。
1688
+ const generateEdgeId = (from: string, to: string): string => {
1689
+ const base = `${from}-${to}`;
1690
+ if (!edges.some((e) => e.id === base)) return base;
1691
+ let i = 2;
1692
+ while (edges.some((e) => e.id === `${base}-${i}`)) i++;
1693
+ return `${base}-${i}`;
1694
+ };
1695
+
1696
+ const api: DiagramBuilder = {
1697
+ lane(laneId, opts) {
1698
+ lanes.push({ id: laneId, ...opts });
1699
+ return api;
1700
+ },
1701
+ node(nodeId, opts) {
1702
+ nodes.push({ id: nodeId, ...opts });
1703
+ return api;
1704
+ },
1705
+ edge(from, to, opts) {
1706
+ edges.push({
1707
+ id: opts.id ?? generateEdgeId(from, to),
1708
+ from,
1709
+ to,
1710
+ label: opts.label,
1711
+ sub: opts.sub,
1712
+ tone: opts.tone ?? "accent",
1713
+ side: opts.side,
1714
+ style: opts.style,
1715
+ guard: opts.guard,
1716
+ cardinality: opts.cardinality,
1717
+ labelOffsetX: opts.labelOffsetX,
1718
+ labelOffsetY: opts.labelOffsetY,
1719
+ routing: opts.routing,
1720
+ overlay: opts.overlay,
1721
+ widthBind: opts.widthBind,
1722
+ strokeBind: opts.strokeBind,
1723
+ dashOffsetBind: opts.dashOffsetBind,
1724
+ });
1725
+ return api;
1726
+ },
1727
+ nodes(defs) {
1728
+ for (const d of defs) {
1729
+ const { id, ...rest } = d;
1730
+ nodes.push({ id, ...rest });
1731
+ }
1732
+ return api;
1733
+ },
1734
+ treeNodes(depth, branching, hSpacing, vSpacing, templateFn) {
1735
+ if (!Number.isInteger(depth) || depth < 1 || depth > 8) {
1736
+ throw new Error(`treeNodes: depth (${depth}) は 1..8 の整数`);
1737
+ }
1738
+ if (!Number.isInteger(branching) || branching < 1 || branching > 8) {
1739
+ throw new Error(`treeNodes: branching (${branching}) は 1..8 の整数`);
1740
+ }
1741
+ if (!Number.isFinite(hSpacing) || hSpacing <= 0 || !Number.isFinite(vSpacing) || vSpacing <= 0) {
1742
+ throw new Error(`treeNodes: hSpacing/vSpacing は正の数値`);
1743
+ }
1744
+ // 完全 tree: level ごとの node 数 = branching^level、 総 node 数 = (b^depth - 1) / (b - 1)
1745
+ // 各 leaf の x offset は等間隔、 各 level の親 node は子の中心に配置。
1746
+ const leavesPerLevel: number[] = [];
1747
+ for (let l = 0; l < depth; l++) leavesPerLevel[l] = Math.pow(branching, depth - 1 - l);
1748
+ const totalLeaves = Math.pow(branching, depth - 1);
1749
+ const layoutWidth = (totalLeaves - 1) * hSpacing;
1750
+ let idx = 0;
1751
+ for (let level = 0; level < depth; level++) {
1752
+ const countAtLevel = Math.pow(branching, level);
1753
+ for (let pos = 0; pos < countAtLevel; pos++) {
1754
+ // level 内 node の x = (pos + 0.5) * (totalLeaves / countAtLevel) * hSpacing - layoutWidth/2
1755
+ const step = (totalLeaves * hSpacing) / countAtLevel;
1756
+ const offsetX = Math.round((pos + 0.5) * step - layoutWidth / 2 - hSpacing / 2);
1757
+ const offsetY = Math.round(level * vSpacing);
1758
+ const spec = templateFn(level, pos, idx, offsetX, offsetY);
1759
+ const { id, ...rest } = spec;
1760
+ const withI = deepSubstituteI(rest, idx);
1761
+ const withVars = deepSubstituteVars(withI, {
1762
+ r: String(level),
1763
+ c: String(pos),
1764
+ });
1765
+ nodes.push({
1766
+ id: substituteVars(substituteI(id, idx), { r: String(level), c: String(pos) }),
1767
+ ...withVars,
1768
+ } as { id: string } & Omit<CdlNode, "id">);
1769
+ idx++;
1770
+ }
1771
+ }
1772
+ return api;
1773
+ },
1774
+ radialNodes(count, radius, templateFn) {
1775
+ if (!Number.isInteger(count) || count < 1 || count > 60) {
1776
+ throw new Error(`radialNodes: count (${count}) は 1..60 の整数`);
1777
+ }
1778
+ if (!Number.isFinite(radius) || radius <= 0) {
1779
+ throw new Error(`radialNodes: radius (${radius}) は正の数値`);
1780
+ }
1781
+ for (let i = 0; i < count; i++) {
1782
+ const angleDeg = (360 * i) / count;
1783
+ const rad = (angleDeg * Math.PI) / 180;
1784
+ const offsetX = Math.round(radius * Math.cos(rad));
1785
+ const offsetY = Math.round(radius * Math.sin(rad));
1786
+ const spec = templateFn(i, angleDeg, offsetX, offsetY);
1787
+ const { id, ...rest } = spec;
1788
+ const withI = deepSubstituteI(rest, i);
1789
+ const withDeg = deepSubstituteVars(withI, { r: String(Math.round(angleDeg)) });
1790
+ nodes.push({
1791
+ id: substituteVars(substituteI(id, i), { r: String(Math.round(angleDeg)) }),
1792
+ ...withDeg,
1793
+ } as { id: string } & Omit<CdlNode, "id">);
1794
+ }
1795
+ return api;
1796
+ },
1797
+ gridNodes(rows, cols, templateFn) {
1798
+ if (!Number.isInteger(rows) || rows < 1 || rows > 30) {
1799
+ throw new Error(`gridNodes: rows (${rows}) は 1..30 の整数`);
1800
+ }
1801
+ if (!Number.isInteger(cols) || cols < 1 || cols > 30) {
1802
+ throw new Error(`gridNodes: cols (${cols}) は 1..30 の整数`);
1803
+ }
1804
+ for (let r = 0; r < rows; r++) {
1805
+ for (let c = 0; c < cols; c++) {
1806
+ const i = r * cols + c;
1807
+ const spec = templateFn(r, c, i);
1808
+ const { id, ...rest } = spec;
1809
+ const withI = deepSubstituteI(rest, i);
1810
+ const withR = deepSubstituteVars(withI, { r: String(r), c: String(c) });
1811
+ nodes.push({
1812
+ id: substituteVars(substituteI(id, i), { r: String(r), c: String(c) }),
1813
+ ...withR,
1814
+ } as { id: string } & Omit<CdlNode, "id">);
1815
+ }
1816
+ }
1817
+ return api;
1818
+ },
1819
+ repeatNodes(count, templateFn) {
1820
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
1821
+ throw new Error(`repeatNodes: count (${count}) は 1..100 の整数である必要があります`);
1822
+ }
1823
+ for (let i = 0; i < count; i++) {
1824
+ const spec = templateFn(i);
1825
+ const { id, ...rest } = spec;
1826
+ // id と string 系 field で `{i}` / `{i+1}` を index 置換する (template 経由の書き方対応)
1827
+ const substituted = { id: substituteI(id, i), ...deepSubstituteI(rest, i) } as {
1828
+ id: string;
1829
+ } & Omit<CdlNode, "id">;
1830
+ nodes.push(substituted);
1831
+ }
1832
+ return api;
1833
+ },
1834
+ edges(defs) {
1835
+ for (const d of defs) {
1836
+ edges.push({
1837
+ id: d.id ?? generateEdgeId(d.from, d.to),
1838
+ from: d.from,
1839
+ to: d.to,
1840
+ label: d.label,
1841
+ sub: d.sub,
1842
+ tone: d.tone ?? "accent",
1843
+ side: d.side,
1844
+ style: d.style,
1845
+ guard: d.guard,
1846
+ cardinality: d.cardinality,
1847
+ labelOffsetX: d.labelOffsetX,
1848
+ labelOffsetY: d.labelOffsetY,
1849
+ routing: d.routing,
1850
+ overlay: d.overlay,
1851
+ });
1852
+ }
1853
+ return api;
1854
+ },
1855
+ arraySignal(id, initial) {
1856
+ if (!Array.isArray(initial)) {
1857
+ throw new Error(`arraySignal(${id}): initial は array`);
1858
+ }
1859
+ states.push({ id, initial: JSON.stringify(initial) });
1860
+ return api;
1861
+ },
1862
+ state(stateId, opts) {
1863
+ states.push({ id: stateId, ...opts });
1864
+ return api;
1865
+ },
1866
+ input: {
1867
+ slider(inputId, opts) {
1868
+ assertUniqueInputId(inputId);
1869
+ // 非有限値 (NaN / Infinity / -Infinity) は cache key の JSON encode で "null" に
1870
+ // 潰れて別 schema が衝突する (CAR #263)、 range check (`< 0 || > 1` 等) も NaN で
1871
+ // silent pass する。 finiteness を先に validate して fail-fast する。
1872
+ assertFiniteNumber(opts.min, `input.slider '${inputId}' min`);
1873
+ assertFiniteNumber(opts.max, `input.slider '${inputId}' max`);
1874
+ if (opts.step !== undefined) assertFiniteNumber(opts.step, `input.slider '${inputId}' step`);
1875
+ assertFiniteNumber(opts.defaultValue, `input.slider '${inputId}' defaultValue`);
1876
+ // slider の defaultValue は [min, max] 範囲内であるべき、 範囲外は明示 error
1877
+ if (opts.defaultValue < opts.min || opts.defaultValue > opts.max) {
1878
+ throw new Error(
1879
+ `input.slider '${inputId}' の defaultValue (${opts.defaultValue}) が [${opts.min}, ${opts.max}] 範囲外です`,
1880
+ );
1881
+ }
1882
+ inputs.push({
1883
+ id: inputId,
1884
+ kind: "slider",
1885
+ min: opts.min,
1886
+ max: opts.max,
1887
+ step: opts.step,
1888
+ defaultValue: opts.defaultValue,
1889
+ label: opts.label,
1890
+ });
1891
+ return api;
1892
+ },
1893
+ number(inputId, opts) {
1894
+ assertUniqueInputId(inputId);
1895
+ if (opts.min !== undefined) assertFiniteNumber(opts.min, `input.number '${inputId}' min`);
1896
+ if (opts.max !== undefined) assertFiniteNumber(opts.max, `input.number '${inputId}' max`);
1897
+ assertFiniteNumber(opts.defaultValue, `input.number '${inputId}' defaultValue`);
1898
+ if (opts.min !== undefined && opts.defaultValue < opts.min) {
1899
+ throw new Error(
1900
+ `input.number '${inputId}' の defaultValue (${opts.defaultValue}) が min (${opts.min}) 未満です`,
1901
+ );
1902
+ }
1903
+ if (opts.max !== undefined && opts.defaultValue > opts.max) {
1904
+ throw new Error(
1905
+ `input.number '${inputId}' の defaultValue (${opts.defaultValue}) が max (${opts.max}) 超過です`,
1906
+ );
1907
+ }
1908
+ inputs.push({
1909
+ id: inputId,
1910
+ kind: "number",
1911
+ min: opts.min,
1912
+ max: opts.max,
1913
+ defaultValue: opts.defaultValue,
1914
+ label: opts.label,
1915
+ });
1916
+ return api;
1917
+ },
1918
+ dropdown(inputId, opts) {
1919
+ assertUniqueInputId(inputId);
1920
+ if (opts.options.length === 0) {
1921
+ throw new Error(`input.dropdown '${inputId}' の options が空です (最低 1 件必要)`);
1922
+ }
1923
+ if (!opts.options.includes(opts.defaultValue)) {
1924
+ throw new Error(
1925
+ `input.dropdown '${inputId}' の defaultValue '${opts.defaultValue}' が options に含まれません (options: ${opts.options.join(", ")})`,
1926
+ );
1927
+ }
1928
+ inputs.push({
1929
+ id: inputId,
1930
+ kind: "dropdown",
1931
+ options: opts.options,
1932
+ defaultValue: opts.defaultValue,
1933
+ label: opts.label,
1934
+ });
1935
+ return api;
1936
+ },
1937
+ toggle(inputId, opts) {
1938
+ assertUniqueInputId(inputId);
1939
+ inputs.push({
1940
+ id: inputId,
1941
+ kind: "toggle",
1942
+ defaultValue: opts.defaultValue,
1943
+ label: opts.label,
1944
+ });
1945
+ return api;
1946
+ },
1947
+ xypad(inputId, opts) {
1948
+ assertUniqueInputId(inputId);
1949
+ assertFiniteNumber(opts.xMin, `input.xypad '${inputId}' xMin`);
1950
+ assertFiniteNumber(opts.xMax, `input.xypad '${inputId}' xMax`);
1951
+ assertFiniteNumber(opts.yMin, `input.xypad '${inputId}' yMin`);
1952
+ assertFiniteNumber(opts.yMax, `input.xypad '${inputId}' yMax`);
1953
+ assertFiniteNumber(opts.defaultX, `input.xypad '${inputId}' defaultX`);
1954
+ assertFiniteNumber(opts.defaultY, `input.xypad '${inputId}' defaultY`);
1955
+ if (opts.defaultX < opts.xMin || opts.defaultX > opts.xMax) {
1956
+ throw new Error(
1957
+ `input.xypad '${inputId}' の defaultX (${opts.defaultX}) が [${opts.xMin}, ${opts.xMax}] 範囲外です`,
1958
+ );
1959
+ }
1960
+ if (opts.defaultY < opts.yMin || opts.defaultY > opts.yMax) {
1961
+ throw new Error(
1962
+ `input.xypad '${inputId}' の defaultY (${opts.defaultY}) が [${opts.yMin}, ${opts.yMax}] 範囲外です`,
1963
+ );
1964
+ }
1965
+ inputs.push({
1966
+ id: inputId,
1967
+ kind: "xypad",
1968
+ xMin: opts.xMin,
1969
+ xMax: opts.xMax,
1970
+ yMin: opts.yMin,
1971
+ yMax: opts.yMax,
1972
+ defaultX: opts.defaultX,
1973
+ defaultY: opts.defaultY,
1974
+ label: opts.label,
1975
+ });
1976
+ return api;
1977
+ },
1978
+ stepper(inputId, opts) {
1979
+ assertUniqueInputId(inputId);
1980
+ if (opts.min !== undefined) assertFiniteNumber(opts.min, `input.stepper '${inputId}' min`);
1981
+ if (opts.max !== undefined) assertFiniteNumber(opts.max, `input.stepper '${inputId}' max`);
1982
+ if (opts.step !== undefined) assertFiniteNumber(opts.step, `input.stepper '${inputId}' step`);
1983
+ assertFiniteNumber(opts.defaultValue, `input.stepper '${inputId}' defaultValue`);
1984
+ if (opts.min !== undefined && opts.defaultValue < opts.min) {
1985
+ throw new Error(
1986
+ `input.stepper '${inputId}' の defaultValue (${opts.defaultValue}) が min (${opts.min}) 未満です`,
1987
+ );
1988
+ }
1989
+ if (opts.max !== undefined && opts.defaultValue > opts.max) {
1990
+ throw new Error(
1991
+ `input.stepper '${inputId}' の defaultValue (${opts.defaultValue}) が max (${opts.max}) 超過です`,
1992
+ );
1993
+ }
1994
+ inputs.push({
1995
+ id: inputId,
1996
+ kind: "stepper",
1997
+ min: opts.min,
1998
+ max: opts.max,
1999
+ step: opts.step,
2000
+ defaultValue: opts.defaultValue,
2001
+ label: opts.label,
2002
+ });
2003
+ return api;
2004
+ },
2005
+ radio(inputId, opts) {
2006
+ assertUniqueInputId(inputId);
2007
+ if (opts.options.length === 0) {
2008
+ throw new Error(`input.radio '${inputId}' の options が空です`);
2009
+ }
2010
+ if (!opts.options.includes(opts.defaultValue)) {
2011
+ throw new Error(
2012
+ `input.radio '${inputId}' の defaultValue '${opts.defaultValue}' が options に含まれません`,
2013
+ );
2014
+ }
2015
+ inputs.push({
2016
+ id: inputId,
2017
+ kind: "radio",
2018
+ options: opts.options,
2019
+ defaultValue: opts.defaultValue,
2020
+ label: opts.label,
2021
+ });
2022
+ return api;
2023
+ },
2024
+ color(inputId, opts) {
2025
+ assertUniqueInputId(inputId);
2026
+ // #RRGGBB / #RGB を許容
2027
+ if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(opts.defaultValue)) {
2028
+ throw new Error(
2029
+ `input.color '${inputId}' の defaultValue '${opts.defaultValue}' が hex color 形式 (#RGB / #RRGGBB) ではありません`,
2030
+ );
2031
+ }
2032
+ inputs.push({
2033
+ id: inputId,
2034
+ kind: "color",
2035
+ defaultValue: opts.defaultValue,
2036
+ label: opts.label,
2037
+ });
2038
+ return api;
2039
+ },
2040
+ timeline(inputId, opts) {
2041
+ assertUniqueInputId(inputId);
2042
+ assertFiniteNumber(opts.duration, `input.timeline '${inputId}' duration`);
2043
+ if (opts.duration < 100) {
2044
+ throw new Error(`input.timeline '${inputId}' の duration は 100ms 以上必要`);
2045
+ }
2046
+ if (opts.speeds !== undefined && opts.speeds.length === 0) {
2047
+ throw new Error(`input.timeline '${inputId}' の speeds が空配列です`);
2048
+ }
2049
+ inputs.push({
2050
+ id: inputId,
2051
+ kind: "timeline",
2052
+ duration: opts.duration,
2053
+ autoplay: opts.autoplay,
2054
+ loop: opts.loop,
2055
+ speeds: opts.speeds,
2056
+ defaultSpeedIdx: opts.defaultSpeedIdx,
2057
+ label: opts.label,
2058
+ });
2059
+ return api;
2060
+ },
2061
+ range(inputId, opts) {
2062
+ assertUniqueInputId(inputId);
2063
+ assertFiniteNumber(opts.min, `input.range '${inputId}' min`);
2064
+ assertFiniteNumber(opts.max, `input.range '${inputId}' max`);
2065
+ assertFiniteNumber(opts.defaultLo, `input.range '${inputId}' defaultLo`);
2066
+ assertFiniteNumber(opts.defaultHi, `input.range '${inputId}' defaultHi`);
2067
+ inputs.push({
2068
+ id: inputId,
2069
+ kind: "range",
2070
+ min: opts.min,
2071
+ max: opts.max,
2072
+ step: opts.step,
2073
+ defaultLo: opts.defaultLo,
2074
+ defaultHi: opts.defaultHi,
2075
+ label: opts.label,
2076
+ });
2077
+ return api;
2078
+ },
2079
+ multiSelect(inputId, opts) {
2080
+ assertUniqueInputId(inputId);
2081
+ if (opts.options.length === 0) {
2082
+ throw new Error(`input.multiSelect '${inputId}' の options が空です`);
2083
+ }
2084
+ inputs.push({
2085
+ id: inputId,
2086
+ kind: "multi-select",
2087
+ options: opts.options,
2088
+ defaultValues: opts.defaultValues,
2089
+ label: opts.label,
2090
+ });
2091
+ return api;
2092
+ },
2093
+ tabs(inputId, opts) {
2094
+ assertUniqueInputId(inputId);
2095
+ if (opts.options.length === 0) {
2096
+ throw new Error(`input.tabs '${inputId}' の options が空です`);
2097
+ }
2098
+ if (!opts.options.includes(opts.defaultValue)) {
2099
+ throw new Error(`input.tabs '${inputId}' の defaultValue が options に含まれません`);
2100
+ }
2101
+ inputs.push({
2102
+ id: inputId,
2103
+ kind: "tabs",
2104
+ options: opts.options,
2105
+ defaultValue: opts.defaultValue,
2106
+ label: opts.label,
2107
+ });
2108
+ return api;
2109
+ },
2110
+ datetime(inputId, opts) {
2111
+ assertUniqueInputId(inputId);
2112
+ inputs.push({
2113
+ id: inputId,
2114
+ kind: "datetime",
2115
+ defaultValue: opts.defaultValue,
2116
+ label: opts.label,
2117
+ });
2118
+ return api;
2119
+ },
2120
+ text(inputId, opts) {
2121
+ assertUniqueInputId(inputId);
2122
+ inputs.push({
2123
+ id: inputId,
2124
+ kind: "text",
2125
+ defaultValue: opts.defaultValue,
2126
+ placeholder: opts.placeholder,
2127
+ maxLength: opts.maxLength,
2128
+ label: opts.label,
2129
+ });
2130
+ return api;
2131
+ },
2132
+ },
2133
+ formula(formulaId, expression) {
2134
+ assertUniqueFormulaId(formulaId);
2135
+ // 入口で parse を試み、 syntax error は formula id 付きで throw (fail-fast)
2136
+ try {
2137
+ parseFormula(expression);
2138
+ } catch (e) {
2139
+ const msg = e instanceof Error ? e.message : String(e);
2140
+ throw new Error(`formula '${formulaId}' の expression syntax error: ${msg}`);
2141
+ }
2142
+ formulas.push({ id: formulaId, expression });
2143
+ return api;
2144
+ },
2145
+ deriveChain(baseId, count, exprFn) {
2146
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
2147
+ throw new Error(`deriveChain: count (${count}) は 1..100 の整数である必要があります`);
2148
+ }
2149
+ let prev: string | null = null;
2150
+ for (let i = 0; i < count; i++) {
2151
+ const id = `${baseId}${i + 1}`;
2152
+ const rawExpr = exprFn(i, prev);
2153
+ // `prev` を実 id に置換
2154
+ const expr = prev === null ? rawExpr : rawExpr.replace(/\bprev\b/g, prev);
2155
+ assertUniqueFormulaId(id);
2156
+ try {
2157
+ parseFormula(expr);
2158
+ } catch (e) {
2159
+ const msg = e instanceof Error ? e.message : String(e);
2160
+ throw new Error(`deriveChain '${id}' の expression syntax error: ${msg}`);
2161
+ }
2162
+ formulas.push({ id, expression: expr });
2163
+ prev = id;
2164
+ }
2165
+ return api;
2166
+ },
2167
+ on: {
2168
+ click(target, handlerId) {
2169
+ validateEventTarget(target, "click");
2170
+ if (!handlerId) {
2171
+ throw new Error(`event handler '.on.click': handlerId が空文字です`);
2172
+ }
2173
+ eventBindings.push({
2174
+ id: `evt-${++eventBindingCounter}`,
2175
+ event: "click",
2176
+ target,
2177
+ handlerId,
2178
+ });
2179
+ return api;
2180
+ },
2181
+ hover(target, handlerId) {
2182
+ validateEventTarget(target, "hover");
2183
+ if (!handlerId) {
2184
+ throw new Error(`event handler '.on.hover': handlerId が空文字です`);
2185
+ }
2186
+ eventBindings.push({
2187
+ id: `evt-${++eventBindingCounter}`,
2188
+ event: "hover",
2189
+ target,
2190
+ handlerId,
2191
+ });
2192
+ return api;
2193
+ },
2194
+ doubleClick(target, handlerId) {
2195
+ validateEventTarget(target, "double-click");
2196
+ if (!handlerId) throw new Error(`event handler '.on.doubleClick': handlerId 空`);
2197
+ eventBindings.push({
2198
+ id: `evt-${++eventBindingCounter}`,
2199
+ event: "double-click",
2200
+ target,
2201
+ handlerId,
2202
+ });
2203
+ return api;
2204
+ },
2205
+ longPress(target, handlerId) {
2206
+ validateEventTarget(target, "long-press");
2207
+ if (!handlerId) throw new Error(`event handler '.on.longPress': handlerId 空`);
2208
+ eventBindings.push({
2209
+ id: `evt-${++eventBindingCounter}`,
2210
+ event: "long-press",
2211
+ target,
2212
+ handlerId,
2213
+ });
2214
+ return api;
2215
+ },
2216
+ drag(target, handlerId) {
2217
+ validateEventTarget(target, "drag");
2218
+ if (!handlerId) throw new Error(`event handler '.on.drag': handlerId 空`);
2219
+ eventBindings.push({
2220
+ id: `evt-${++eventBindingCounter}`,
2221
+ event: "drag",
2222
+ target,
2223
+ handlerId,
2224
+ });
2225
+ return api;
2226
+ },
2227
+ drop(target, handlerId) {
2228
+ validateEventTarget(target, "drop");
2229
+ if (!handlerId) throw new Error(`event handler '.on.drop': handlerId 空`);
2230
+ eventBindings.push({
2231
+ id: `evt-${++eventBindingCounter}`,
2232
+ event: "drop",
2233
+ target,
2234
+ handlerId,
2235
+ });
2236
+ return api;
2237
+ },
2238
+ keydown(target, handlerId) {
2239
+ validateEventTarget(target, "keydown");
2240
+ if (!handlerId) throw new Error(`event handler '.on.keydown': handlerId 空`);
2241
+ eventBindings.push({
2242
+ id: `evt-${++eventBindingCounter}`,
2243
+ event: "keydown",
2244
+ target,
2245
+ handlerId,
2246
+ });
2247
+ return api;
2248
+ },
2249
+ focus(target, handlerId) {
2250
+ validateEventTarget(target, "focus");
2251
+ if (!handlerId) throw new Error(`event handler '.on.focus': handlerId 空`);
2252
+ eventBindings.push({
2253
+ id: `evt-${++eventBindingCounter}`,
2254
+ event: "focus",
2255
+ target,
2256
+ handlerId,
2257
+ });
2258
+ return api;
2259
+ },
2260
+ blur(target, handlerId) {
2261
+ validateEventTarget(target, "blur");
2262
+ if (!handlerId) throw new Error(`event handler '.on.blur': handlerId 空`);
2263
+ eventBindings.push({
2264
+ id: `evt-${++eventBindingCounter}`,
2265
+ event: "blur",
2266
+ target,
2267
+ handlerId,
2268
+ });
2269
+ return api;
2270
+ },
2271
+ },
2272
+ readout: {
2273
+ bar(readoutId, opts) {
2274
+ assertUniqueReadoutId(readoutId);
2275
+ assertFiniteNumber(opts.min, `readout.bar '${readoutId}' min`);
2276
+ assertFiniteNumber(opts.max, `readout.bar '${readoutId}' max`);
2277
+ readouts.push({
2278
+ id: readoutId,
2279
+ kind: "bar",
2280
+ source: opts.source,
2281
+ min: opts.min,
2282
+ max: opts.max,
2283
+ color: opts.color,
2284
+ label: opts.label,
2285
+ });
2286
+ return api;
2287
+ },
2288
+ gauge(readoutId, opts) {
2289
+ assertUniqueReadoutId(readoutId);
2290
+ assertFiniteNumber(opts.min, `readout.gauge '${readoutId}' min`);
2291
+ assertFiniteNumber(opts.max, `readout.gauge '${readoutId}' max`);
2292
+ readouts.push({
2293
+ id: readoutId,
2294
+ kind: "gauge",
2295
+ source: opts.source,
2296
+ min: opts.min,
2297
+ max: opts.max,
2298
+ color: opts.color,
2299
+ label: opts.label,
2300
+ });
2301
+ return api;
2302
+ },
2303
+ stat(readoutId, opts) {
2304
+ assertUniqueReadoutId(readoutId);
2305
+ readouts.push({
2306
+ id: readoutId,
2307
+ kind: "stat",
2308
+ source: opts.source,
2309
+ caption: opts.caption,
2310
+ unit: opts.unit,
2311
+ label: opts.label,
2312
+ });
2313
+ return api;
2314
+ },
2315
+ sparkline(readoutId, opts) {
2316
+ assertUniqueReadoutId(readoutId);
2317
+ if (opts.history !== undefined && (opts.history < 2 || opts.history > 200)) {
2318
+ throw new Error(
2319
+ `readout.sparkline '${readoutId}' の history (${opts.history}) は 2..200 の範囲である必要があります`,
2320
+ );
2321
+ }
2322
+ readouts.push({
2323
+ id: readoutId,
2324
+ kind: "sparkline",
2325
+ source: opts.source,
2326
+ history: opts.history,
2327
+ color: opts.color,
2328
+ label: opts.label,
2329
+ });
2330
+ return api;
2331
+ },
2332
+ countup(readoutId, opts) {
2333
+ assertUniqueReadoutId(readoutId);
2334
+ if (opts.durationMs !== undefined && opts.durationMs < 50) {
2335
+ throw new Error(`readout.countup '${readoutId}' の durationMs は 50 以上必要`);
2336
+ }
2337
+ readouts.push({
2338
+ id: readoutId,
2339
+ kind: "countup",
2340
+ source: opts.source,
2341
+ durationMs: opts.durationMs,
2342
+ decimals: opts.decimals,
2343
+ unit: opts.unit,
2344
+ label: opts.label,
2345
+ });
2346
+ return api;
2347
+ },
2348
+ typewriter(readoutId, opts) {
2349
+ assertUniqueReadoutId(readoutId);
2350
+ readouts.push({
2351
+ id: readoutId,
2352
+ kind: "typewriter",
2353
+ source: opts.source,
2354
+ charMs: opts.charMs,
2355
+ label: opts.label,
2356
+ });
2357
+ return api;
2358
+ },
2359
+ delta(readoutId, opts) {
2360
+ assertUniqueReadoutId(readoutId);
2361
+ readouts.push({
2362
+ id: readoutId,
2363
+ kind: "delta",
2364
+ source: opts.source,
2365
+ decimals: opts.decimals,
2366
+ unit: opts.unit,
2367
+ label: opts.label,
2368
+ });
2369
+ return api;
2370
+ },
2371
+ percentRing(readoutId, opts) {
2372
+ assertUniqueReadoutId(readoutId);
2373
+ assertFiniteNumber(opts.max, `readout.percentRing '${readoutId}' max`);
2374
+ readouts.push({
2375
+ id: readoutId,
2376
+ kind: "percent-ring",
2377
+ source: opts.source,
2378
+ max: opts.max,
2379
+ color: opts.color,
2380
+ label: opts.label,
2381
+ });
2382
+ return api;
2383
+ },
2384
+ heatCell(readoutId, opts) {
2385
+ assertUniqueReadoutId(readoutId);
2386
+ assertFiniteNumber(opts.min, `readout.heatCell '${readoutId}' min`);
2387
+ assertFiniteNumber(opts.max, `readout.heatCell '${readoutId}' max`);
2388
+ readouts.push({
2389
+ id: readoutId,
2390
+ kind: "heat-cell",
2391
+ source: opts.source,
2392
+ min: opts.min,
2393
+ max: opts.max,
2394
+ colors: opts.colors,
2395
+ label: opts.label,
2396
+ });
2397
+ return api;
2398
+ },
2399
+ badge(readoutId, opts) {
2400
+ assertUniqueReadoutId(readoutId);
2401
+ readouts.push({
2402
+ id: readoutId,
2403
+ kind: "badge",
2404
+ source: opts.source,
2405
+ colorSource: opts.colorSource,
2406
+ map: opts.map,
2407
+ label: opts.label,
2408
+ });
2409
+ return api;
2410
+ },
2411
+ statusDot(readoutId, opts) {
2412
+ assertUniqueReadoutId(readoutId);
2413
+ if (opts.map.length === 0) {
2414
+ throw new Error(`readout.statusDot '${readoutId}' の map が空です`);
2415
+ }
2416
+ readouts.push({
2417
+ id: readoutId,
2418
+ kind: "status-dot",
2419
+ source: opts.source,
2420
+ map: opts.map,
2421
+ label: opts.label,
2422
+ });
2423
+ return api;
2424
+ },
2425
+ pathProgress(readoutId, opts) {
2426
+ assertUniqueReadoutId(readoutId);
2427
+ readouts.push({
2428
+ id: readoutId,
2429
+ kind: "path-progress",
2430
+ source: opts.source,
2431
+ pathD: opts.pathD,
2432
+ viewW: opts.viewW,
2433
+ viewH: opts.viewH,
2434
+ strokeWidth: opts.strokeWidth,
2435
+ color: opts.color,
2436
+ max: opts.max,
2437
+ label: opts.label,
2438
+ });
2439
+ return api;
2440
+ },
2441
+ arrayList(readoutId, opts) {
2442
+ assertUniqueReadoutId(readoutId);
2443
+ readouts.push({
2444
+ id: readoutId,
2445
+ kind: "array-list",
2446
+ source: opts.source,
2447
+ itemTemplate: opts.itemTemplate,
2448
+ max: opts.max,
2449
+ label: opts.label,
2450
+ });
2451
+ return api;
2452
+ },
2453
+ arrayBar(readoutId, opts) {
2454
+ assertUniqueReadoutId(readoutId);
2455
+ readouts.push({
2456
+ id: readoutId,
2457
+ kind: "array-bar",
2458
+ source: opts.source,
2459
+ min: opts.min,
2460
+ max: opts.max,
2461
+ color: opts.color,
2462
+ label: opts.label,
2463
+ });
2464
+ return api;
2465
+ },
2466
+ lineChart(readoutId, opts) {
2467
+ assertUniqueReadoutId(readoutId);
2468
+ readouts.push({
2469
+ id: readoutId,
2470
+ kind: "line-chart",
2471
+ source: opts.source,
2472
+ min: opts.min,
2473
+ max: opts.max,
2474
+ viewW: opts.viewW,
2475
+ viewH: opts.viewH,
2476
+ color: opts.color,
2477
+ fill: opts.fill,
2478
+ label: opts.label,
2479
+ });
2480
+ return api;
2481
+ },
2482
+ stackedBar(readoutId, opts) {
2483
+ assertUniqueReadoutId(readoutId);
2484
+ readouts.push({
2485
+ id: readoutId,
2486
+ kind: "stacked-bar",
2487
+ sourceA: opts.sourceA,
2488
+ sourceB: opts.sourceB,
2489
+ min: opts.min,
2490
+ max: opts.max,
2491
+ colorA: opts.colorA,
2492
+ colorB: opts.colorB,
2493
+ label: opts.label,
2494
+ });
2495
+ return api;
2496
+ },
2497
+ waterfall(readoutId, opts) {
2498
+ assertUniqueReadoutId(readoutId);
2499
+ readouts.push({
2500
+ id: readoutId,
2501
+ kind: "waterfall",
2502
+ source: opts.source,
2503
+ min: opts.min,
2504
+ max: opts.max,
2505
+ viewW: opts.viewW,
2506
+ viewH: opts.viewH,
2507
+ colorPos: opts.colorPos,
2508
+ colorNeg: opts.colorNeg,
2509
+ label: opts.label,
2510
+ });
2511
+ return api;
2512
+ },
2513
+ matrix(readoutId, opts) {
2514
+ assertUniqueReadoutId(readoutId);
2515
+ readouts.push({
2516
+ id: readoutId,
2517
+ kind: "matrix",
2518
+ source: opts.source,
2519
+ min: opts.min,
2520
+ max: opts.max,
2521
+ cellSize: opts.cellSize,
2522
+ colors: opts.colors,
2523
+ showValue: opts.showValue,
2524
+ label: opts.label,
2525
+ });
2526
+ return api;
2527
+ },
2528
+ progressGroup(readoutId, opts) {
2529
+ assertUniqueReadoutId(readoutId);
2530
+ readouts.push({
2531
+ id: readoutId,
2532
+ kind: "progress-group",
2533
+ source: opts.source,
2534
+ max: opts.max,
2535
+ labelSource: opts.labelSource,
2536
+ color: opts.color,
2537
+ label: opts.label,
2538
+ });
2539
+ return api;
2540
+ },
2541
+ sequenceTimeline(readoutId, opts) {
2542
+ assertUniqueReadoutId(readoutId);
2543
+ readouts.push({
2544
+ id: readoutId,
2545
+ kind: "sequence-timeline",
2546
+ source: opts.source,
2547
+ min: opts.min,
2548
+ max: opts.max,
2549
+ viewW: opts.viewW,
2550
+ viewH: opts.viewH,
2551
+ color: opts.color,
2552
+ label: opts.label,
2553
+ });
2554
+ return api;
2555
+ },
2556
+ radar(readoutId, opts) {
2557
+ assertUniqueReadoutId(readoutId);
2558
+ readouts.push({
2559
+ id: readoutId,
2560
+ kind: "radar",
2561
+ source: opts.source,
2562
+ max: opts.max,
2563
+ viewW: opts.viewW,
2564
+ viewH: opts.viewH,
2565
+ color: opts.color,
2566
+ labelSource: opts.labelSource,
2567
+ label: opts.label,
2568
+ });
2569
+ return api;
2570
+ },
2571
+ bubbleChart(readoutId, opts) {
2572
+ assertUniqueReadoutId(readoutId);
2573
+ readouts.push({
2574
+ id: readoutId,
2575
+ kind: "bubble-chart",
2576
+ source: opts.source,
2577
+ xMin: opts.xMin,
2578
+ xMax: opts.xMax,
2579
+ yMin: opts.yMin,
2580
+ yMax: opts.yMax,
2581
+ rMin: opts.rMin,
2582
+ rMax: opts.rMax,
2583
+ viewW: opts.viewW,
2584
+ viewH: opts.viewH,
2585
+ color: opts.color,
2586
+ label: opts.label,
2587
+ });
2588
+ return api;
2589
+ },
2590
+ donut(readoutId, opts) {
2591
+ assertUniqueReadoutId(readoutId);
2592
+ readouts.push({
2593
+ id: readoutId,
2594
+ kind: "donut",
2595
+ source: opts.source,
2596
+ viewW: opts.viewW,
2597
+ viewH: opts.viewH,
2598
+ colors: opts.colors,
2599
+ innerRatio: opts.innerRatio,
2600
+ label: opts.label,
2601
+ });
2602
+ return api;
2603
+ },
2604
+ calendarHeatmap(readoutId, opts) {
2605
+ assertUniqueReadoutId(readoutId);
2606
+ readouts.push({
2607
+ id: readoutId,
2608
+ kind: "calendar-heatmap",
2609
+ source: opts.source,
2610
+ max: opts.max,
2611
+ cellSize: opts.cellSize,
2612
+ cellGap: opts.cellGap,
2613
+ colors: opts.colors,
2614
+ label: opts.label,
2615
+ });
2616
+ return api;
2617
+ },
2618
+ miniMap(readoutId, opts) {
2619
+ assertUniqueReadoutId(readoutId);
2620
+ readouts.push({
2621
+ id: readoutId,
2622
+ kind: "mini-map",
2623
+ source: opts.source,
2624
+ canvasW: opts.canvasW,
2625
+ canvasH: opts.canvasH,
2626
+ viewW: opts.viewW,
2627
+ viewH: opts.viewH,
2628
+ color: opts.color,
2629
+ label: opts.label,
2630
+ });
2631
+ return api;
2632
+ },
2633
+ kpiCard(readoutId, opts) {
2634
+ assertUniqueReadoutId(readoutId);
2635
+ readouts.push({
2636
+ id: readoutId,
2637
+ kind: "kpi-card",
2638
+ source: opts.source,
2639
+ historySource: opts.historySource,
2640
+ comparisonSource: opts.comparisonSource,
2641
+ unit: opts.unit,
2642
+ colorPos: opts.colorPos,
2643
+ colorNeg: opts.colorNeg,
2644
+ label: opts.label,
2645
+ });
2646
+ return api;
2647
+ },
2648
+ candlestick(readoutId, opts) {
2649
+ assertUniqueReadoutId(readoutId);
2650
+ readouts.push({
2651
+ id: readoutId,
2652
+ kind: "candlestick",
2653
+ source: opts.source,
2654
+ min: opts.min,
2655
+ max: opts.max,
2656
+ viewW: opts.viewW,
2657
+ viewH: opts.viewH,
2658
+ colorUp: opts.colorUp,
2659
+ colorDown: opts.colorDown,
2660
+ label: opts.label,
2661
+ });
2662
+ return api;
2663
+ },
2664
+ venn(readoutId, opts) {
2665
+ assertUniqueReadoutId(readoutId);
2666
+ readouts.push({
2667
+ id: readoutId,
2668
+ kind: "venn",
2669
+ source: opts.source,
2670
+ viewW: opts.viewW,
2671
+ viewH: opts.viewH,
2672
+ colorA: opts.colorA,
2673
+ colorB: opts.colorB,
2674
+ labelA: opts.labelA,
2675
+ labelB: opts.labelB,
2676
+ label: opts.label,
2677
+ });
2678
+ return api;
2679
+ },
2680
+ slope(readoutId, opts) {
2681
+ assertUniqueReadoutId(readoutId);
2682
+ readouts.push({
2683
+ id: readoutId,
2684
+ kind: "slope",
2685
+ source: opts.source,
2686
+ min: opts.min,
2687
+ max: opts.max,
2688
+ viewW: opts.viewW,
2689
+ viewH: opts.viewH,
2690
+ colorUp: opts.colorUp,
2691
+ colorDown: opts.colorDown,
2692
+ label: opts.label,
2693
+ });
2694
+ return api;
2695
+ },
2696
+ funnel(readoutId, opts) {
2697
+ assertUniqueReadoutId(readoutId);
2698
+ readouts.push({
2699
+ id: readoutId,
2700
+ kind: "funnel",
2701
+ source: opts.source,
2702
+ viewW: opts.viewW,
2703
+ viewH: opts.viewH,
2704
+ colorTop: opts.colorTop,
2705
+ colorBottom: opts.colorBottom,
2706
+ label: opts.label,
2707
+ });
2708
+ return api;
2709
+ },
2710
+ gantt(readoutId, opts) {
2711
+ assertUniqueReadoutId(readoutId);
2712
+ readouts.push({
2713
+ id: readoutId,
2714
+ kind: "gantt",
2715
+ source: opts.source,
2716
+ min: opts.min,
2717
+ max: opts.max,
2718
+ viewW: opts.viewW,
2719
+ viewH: opts.viewH,
2720
+ color: opts.color,
2721
+ label: opts.label,
2722
+ });
2723
+ return api;
2724
+ },
2725
+ treemap(readoutId, opts) {
2726
+ assertUniqueReadoutId(readoutId);
2727
+ readouts.push({
2728
+ id: readoutId,
2729
+ kind: "treemap",
2730
+ source: opts.source,
2731
+ viewW: opts.viewW,
2732
+ viewH: opts.viewH,
2733
+ colors: opts.colors,
2734
+ label: opts.label,
2735
+ });
2736
+ return api;
2737
+ },
2738
+ sankey(readoutId, opts) {
2739
+ assertUniqueReadoutId(readoutId);
2740
+ readouts.push({
2741
+ id: readoutId,
2742
+ kind: "sankey",
2743
+ source: opts.source,
2744
+ viewW: opts.viewW,
2745
+ viewH: opts.viewH,
2746
+ colors: opts.colors,
2747
+ label: opts.label,
2748
+ });
2749
+ return api;
2750
+ },
2751
+ polarArea(readoutId, opts) {
2752
+ assertUniqueReadoutId(readoutId);
2753
+ readouts.push({
2754
+ id: readoutId,
2755
+ kind: "polar-area",
2756
+ source: opts.source,
2757
+ max: opts.max,
2758
+ viewW: opts.viewW,
2759
+ viewH: opts.viewH,
2760
+ colors: opts.colors,
2761
+ labelSource: opts.labelSource,
2762
+ label: opts.label,
2763
+ });
2764
+ return api;
2765
+ },
2766
+ stepIndicator(readoutId, opts) {
2767
+ assertUniqueReadoutId(readoutId);
2768
+ readouts.push({
2769
+ id: readoutId,
2770
+ kind: "step-indicator",
2771
+ source: opts.source,
2772
+ stepsSource: opts.stepsSource,
2773
+ viewW: opts.viewW,
2774
+ viewH: opts.viewH,
2775
+ colorActive: opts.colorActive,
2776
+ colorPending: opts.colorPending,
2777
+ label: opts.label,
2778
+ });
2779
+ return api;
2780
+ },
2781
+ bulletChart(readoutId, opts) {
2782
+ assertUniqueReadoutId(readoutId);
2783
+ readouts.push({
2784
+ id: readoutId,
2785
+ kind: "bullet-chart",
2786
+ source: opts.source,
2787
+ targetSource: opts.targetSource,
2788
+ max: opts.max,
2789
+ rangeBad: opts.rangeBad,
2790
+ rangeAvg: opts.rangeAvg,
2791
+ viewW: opts.viewW,
2792
+ viewH: opts.viewH,
2793
+ colorActual: opts.colorActual,
2794
+ label: opts.label,
2795
+ });
2796
+ return api;
2797
+ },
2798
+ numberBoard(readoutId, opts) {
2799
+ assertUniqueReadoutId(readoutId);
2800
+ readouts.push({
2801
+ id: readoutId,
2802
+ kind: "number-board",
2803
+ source: opts.source,
2804
+ prefix: opts.prefix,
2805
+ suffix: opts.suffix,
2806
+ size: opts.size,
2807
+ color: opts.color,
2808
+ caption: opts.caption,
2809
+ label: opts.label,
2810
+ });
2811
+ return api;
2812
+ },
2813
+ leaderboard(readoutId, opts) {
2814
+ assertUniqueReadoutId(readoutId);
2815
+ readouts.push({
2816
+ id: readoutId,
2817
+ kind: "leaderboard",
2818
+ source: opts.source,
2819
+ max: opts.max,
2820
+ color: opts.color,
2821
+ label: opts.label,
2822
+ });
2823
+ return api;
2824
+ },
2825
+ trafficLight(readoutId, opts) {
2826
+ assertUniqueReadoutId(readoutId);
2827
+ readouts.push({
2828
+ id: readoutId,
2829
+ kind: "traffic-light",
2830
+ source: opts.source,
2831
+ viewW: opts.viewW,
2832
+ viewH: opts.viewH,
2833
+ label: opts.label,
2834
+ });
2835
+ return api;
2836
+ },
2837
+ tagCloud(readoutId, opts) {
2838
+ assertUniqueReadoutId(readoutId);
2839
+ readouts.push({
2840
+ id: readoutId,
2841
+ kind: "tag-cloud",
2842
+ source: opts.source,
2843
+ minSize: opts.minSize,
2844
+ maxSize: opts.maxSize,
2845
+ colors: opts.colors,
2846
+ label: opts.label,
2847
+ });
2848
+ return api;
2849
+ },
2850
+ activityFeed(readoutId, opts) {
2851
+ assertUniqueReadoutId(readoutId);
2852
+ readouts.push({
2853
+ id: readoutId,
2854
+ kind: "activity-feed",
2855
+ source: opts.source,
2856
+ max: opts.max,
2857
+ color: opts.color,
2858
+ label: opts.label,
2859
+ });
2860
+ return api;
2861
+ },
2862
+ rating(readoutId, opts) {
2863
+ assertUniqueReadoutId(readoutId);
2864
+ readouts.push({
2865
+ id: readoutId,
2866
+ kind: "rating",
2867
+ source: opts.source,
2868
+ count: opts.count,
2869
+ color: opts.color,
2870
+ label: opts.label,
2871
+ });
2872
+ return api;
2873
+ },
2874
+ notification(readoutId, opts) {
2875
+ assertUniqueReadoutId(readoutId);
2876
+ readouts.push({
2877
+ id: readoutId,
2878
+ kind: "notification",
2879
+ kindSource: opts.kindSource,
2880
+ titleSource: opts.titleSource,
2881
+ bodySource: opts.bodySource,
2882
+ label: opts.label,
2883
+ });
2884
+ return api;
2885
+ },
2886
+ diffCounter(readoutId, opts) {
2887
+ assertUniqueReadoutId(readoutId);
2888
+ readouts.push({
2889
+ id: readoutId,
2890
+ kind: "diff-counter",
2891
+ additionsSource: opts.additionsSource,
2892
+ deletionsSource: opts.deletionsSource,
2893
+ colorAdd: opts.colorAdd,
2894
+ colorDel: opts.colorDel,
2895
+ label: opts.label,
2896
+ });
2897
+ return api;
2898
+ },
2899
+ chatBubble(readoutId, opts) {
2900
+ assertUniqueReadoutId(readoutId);
2901
+ readouts.push({
2902
+ id: readoutId,
2903
+ kind: "chat-bubble",
2904
+ source: opts.source,
2905
+ max: opts.max,
2906
+ colorSelf: opts.colorSelf,
2907
+ colorOther: opts.colorOther,
2908
+ label: opts.label,
2909
+ });
2910
+ return api;
2911
+ },
2912
+ avatar(readoutId, opts) {
2913
+ assertUniqueReadoutId(readoutId);
2914
+ readouts.push({
2915
+ id: readoutId,
2916
+ kind: "avatar",
2917
+ source: opts.source,
2918
+ size: opts.size,
2919
+ color: opts.color,
2920
+ label: opts.label,
2921
+ });
2922
+ return api;
2923
+ },
2924
+ checklist(readoutId, opts) {
2925
+ assertUniqueReadoutId(readoutId);
2926
+ readouts.push({
2927
+ id: readoutId,
2928
+ kind: "checklist",
2929
+ source: opts.source,
2930
+ color: opts.color,
2931
+ label: opts.label,
2932
+ });
2933
+ return api;
2934
+ },
2935
+ circularGauge(readoutId, opts) {
2936
+ assertUniqueReadoutId(readoutId);
2937
+ readouts.push({
2938
+ id: readoutId,
2939
+ kind: "circular-gauge",
2940
+ source: opts.source,
2941
+ min: opts.min,
2942
+ max: opts.max,
2943
+ viewW: opts.viewW,
2944
+ viewH: opts.viewH,
2945
+ color: opts.color,
2946
+ unit: opts.unit,
2947
+ label: opts.label,
2948
+ });
2949
+ return api;
2950
+ },
2951
+ priceTag(readoutId, opts) {
2952
+ assertUniqueReadoutId(readoutId);
2953
+ readouts.push({
2954
+ id: readoutId,
2955
+ kind: "price-tag",
2956
+ oldSource: opts.oldSource,
2957
+ newSource: opts.newSource,
2958
+ currency: opts.currency,
2959
+ colorNew: opts.colorNew,
2960
+ colorOld: opts.colorOld,
2961
+ colorDiscount: opts.colorDiscount,
2962
+ label: opts.label,
2963
+ });
2964
+ return api;
2965
+ },
2966
+ spinner(readoutId, opts) {
2967
+ assertUniqueReadoutId(readoutId);
2968
+ readouts.push({
2969
+ id: readoutId,
2970
+ kind: "spinner",
2971
+ source: opts.source,
2972
+ textSource: opts.textSource,
2973
+ color: opts.color,
2974
+ label: opts.label,
2975
+ });
2976
+ return api;
2977
+ },
2978
+ grade(readoutId, opts) {
2979
+ assertUniqueReadoutId(readoutId);
2980
+ readouts.push({
2981
+ id: readoutId,
2982
+ kind: "grade",
2983
+ source: opts.source,
2984
+ max: opts.max,
2985
+ label: opts.label,
2986
+ });
2987
+ return api;
2988
+ },
2989
+ stopwatch(readoutId, opts) {
2990
+ assertUniqueReadoutId(readoutId);
2991
+ readouts.push({
2992
+ id: readoutId,
2993
+ kind: "stopwatch",
2994
+ source: opts.source,
2995
+ runningSource: opts.runningSource,
2996
+ size: opts.size,
2997
+ color: opts.color,
2998
+ label: opts.label,
2999
+ });
3000
+ return api;
3001
+ },
3002
+ confidenceMeter(readoutId, opts) {
3003
+ assertUniqueReadoutId(readoutId);
3004
+ readouts.push({
3005
+ id: readoutId,
3006
+ kind: "confidence-meter",
3007
+ source: opts.source,
3008
+ lowThreshold: opts.lowThreshold,
3009
+ highThreshold: opts.highThreshold,
3010
+ viewW: opts.viewW,
3011
+ viewH: opts.viewH,
3012
+ label: opts.label,
3013
+ });
3014
+ return api;
3015
+ },
3016
+ reactionBar(readoutId, opts) {
3017
+ assertUniqueReadoutId(readoutId);
3018
+ readouts.push({
3019
+ id: readoutId,
3020
+ kind: "reaction-bar",
3021
+ source: opts.source,
3022
+ color: opts.color,
3023
+ label: opts.label,
3024
+ });
3025
+ return api;
3026
+ },
3027
+ pillGroup(readoutId, opts) {
3028
+ assertUniqueReadoutId(readoutId);
3029
+ readouts.push({
3030
+ id: readoutId,
3031
+ kind: "pill-group",
3032
+ source: opts.source,
3033
+ color: opts.color,
3034
+ label: opts.label,
3035
+ });
3036
+ return api;
3037
+ },
3038
+ fuelBar(readoutId, opts) {
3039
+ assertUniqueReadoutId(readoutId);
3040
+ readouts.push({
3041
+ id: readoutId,
3042
+ kind: "fuel-bar",
3043
+ source: opts.source,
3044
+ segments: opts.segments,
3045
+ lowThreshold: opts.lowThreshold,
3046
+ highThreshold: opts.highThreshold,
3047
+ viewW: opts.viewW,
3048
+ viewH: opts.viewH,
3049
+ label: opts.label,
3050
+ });
3051
+ return api;
3052
+ },
3053
+ metricsGrid(readoutId, opts) {
3054
+ assertUniqueReadoutId(readoutId);
3055
+ readouts.push({
3056
+ id: readoutId,
3057
+ kind: "metrics-grid",
3058
+ source: opts.source,
3059
+ color: opts.color,
3060
+ label: opts.label,
3061
+ });
3062
+ return api;
3063
+ },
3064
+ thermometer(readoutId, opts) {
3065
+ assertUniqueReadoutId(readoutId);
3066
+ readouts.push({
3067
+ id: readoutId,
3068
+ kind: "thermometer",
3069
+ source: opts.source,
3070
+ min: opts.min,
3071
+ max: opts.max,
3072
+ viewW: opts.viewW,
3073
+ viewH: opts.viewH,
3074
+ color: opts.color,
3075
+ unit: opts.unit,
3076
+ label: opts.label,
3077
+ });
3078
+ return api;
3079
+ },
3080
+ iconTile(readoutId, opts) {
3081
+ assertUniqueReadoutId(readoutId);
3082
+ readouts.push({
3083
+ id: readoutId,
3084
+ kind: "icon-tile",
3085
+ source: opts.source,
3086
+ color: opts.color,
3087
+ label: opts.label,
3088
+ });
3089
+ return api;
3090
+ },
3091
+ tokenList(readoutId, opts) {
3092
+ assertUniqueReadoutId(readoutId);
3093
+ readouts.push({
3094
+ id: readoutId,
3095
+ kind: "token-list",
3096
+ source: opts.source,
3097
+ colorUp: opts.colorUp,
3098
+ colorDown: opts.colorDown,
3099
+ label: opts.label,
3100
+ });
3101
+ return api;
3102
+ },
3103
+ mapPin(readoutId, opts) {
3104
+ assertUniqueReadoutId(readoutId);
3105
+ readouts.push({
3106
+ id: readoutId,
3107
+ kind: "map-pin",
3108
+ source: opts.source,
3109
+ xMin: opts.xMin,
3110
+ xMax: opts.xMax,
3111
+ yMin: opts.yMin,
3112
+ yMax: opts.yMax,
3113
+ viewW: opts.viewW,
3114
+ viewH: opts.viewH,
3115
+ color: opts.color,
3116
+ label: opts.label,
3117
+ });
3118
+ return api;
3119
+ },
3120
+ priorityBadge(readoutId, opts) {
3121
+ assertUniqueReadoutId(readoutId);
3122
+ readouts.push({
3123
+ id: readoutId,
3124
+ kind: "priority-badge",
3125
+ source: opts.source,
3126
+ textSource: opts.textSource,
3127
+ label: opts.label,
3128
+ });
3129
+ return api;
3130
+ },
3131
+ podium(readoutId, opts) {
3132
+ assertUniqueReadoutId(readoutId);
3133
+ readouts.push({
3134
+ id: readoutId,
3135
+ kind: "podium",
3136
+ source: opts.source,
3137
+ viewW: opts.viewW,
3138
+ viewH: opts.viewH,
3139
+ label: opts.label,
3140
+ });
3141
+ return api;
3142
+ },
3143
+ pollBar(readoutId, opts) {
3144
+ assertUniqueReadoutId(readoutId);
3145
+ readouts.push({
3146
+ id: readoutId,
3147
+ kind: "poll-bar",
3148
+ source: opts.source,
3149
+ color: opts.color,
3150
+ colorWinner: opts.colorWinner,
3151
+ label: opts.label,
3152
+ });
3153
+ return api;
3154
+ },
3155
+ userStack(readoutId, opts) {
3156
+ assertUniqueReadoutId(readoutId);
3157
+ readouts.push({
3158
+ id: readoutId,
3159
+ kind: "user-stack",
3160
+ source: opts.source,
3161
+ max: opts.max,
3162
+ size: opts.size,
3163
+ label: opts.label,
3164
+ });
3165
+ return api;
3166
+ },
3167
+ commitList(readoutId, opts) {
3168
+ assertUniqueReadoutId(readoutId);
3169
+ readouts.push({
3170
+ id: readoutId,
3171
+ kind: "commit-list",
3172
+ source: opts.source,
3173
+ max: opts.max,
3174
+ color: opts.color,
3175
+ label: opts.label,
3176
+ });
3177
+ return api;
3178
+ },
3179
+ mediaPlayer(readoutId, opts) {
3180
+ assertUniqueReadoutId(readoutId);
3181
+ readouts.push({
3182
+ id: readoutId,
3183
+ kind: "media-player",
3184
+ source: opts.source,
3185
+ durationSource: opts.durationSource,
3186
+ playingSource: opts.playingSource,
3187
+ color: opts.color,
3188
+ viewW: opts.viewW,
3189
+ label: opts.label,
3190
+ });
3191
+ return api;
3192
+ },
3193
+ eventLog(readoutId, opts) {
3194
+ assertUniqueReadoutId(readoutId);
3195
+ readouts.push({
3196
+ id: readoutId,
3197
+ kind: "event-log",
3198
+ source: opts.source,
3199
+ max: opts.max,
3200
+ label: opts.label,
3201
+ });
3202
+ return api;
3203
+ },
3204
+ searchResult(readoutId, opts) {
3205
+ assertUniqueReadoutId(readoutId);
3206
+ readouts.push({
3207
+ id: readoutId,
3208
+ kind: "search-result",
3209
+ source: opts.source,
3210
+ max: opts.max,
3211
+ color: opts.color,
3212
+ label: opts.label,
3213
+ });
3214
+ return api;
3215
+ },
3216
+ roadmap(readoutId, opts) {
3217
+ assertUniqueReadoutId(readoutId);
3218
+ readouts.push({
3219
+ id: readoutId,
3220
+ kind: "roadmap",
3221
+ source: opts.source,
3222
+ viewW: opts.viewW,
3223
+ viewH: opts.viewH,
3224
+ label: opts.label,
3225
+ });
3226
+ return api;
3227
+ },
3228
+ weatherForecast(readoutId, opts) {
3229
+ assertUniqueReadoutId(readoutId);
3230
+ readouts.push({
3231
+ id: readoutId,
3232
+ kind: "weather-forecast",
3233
+ source: opts.source,
3234
+ label: opts.label,
3235
+ });
3236
+ return api;
3237
+ },
3238
+ videoCard(readoutId, opts) {
3239
+ assertUniqueReadoutId(readoutId);
3240
+ readouts.push({
3241
+ id: readoutId,
3242
+ kind: "video-card",
3243
+ source: opts.source,
3244
+ max: opts.max,
3245
+ color: opts.color,
3246
+ label: opts.label,
3247
+ });
3248
+ return api;
3249
+ },
3250
+ orderStatus(readoutId, opts) {
3251
+ assertUniqueReadoutId(readoutId);
3252
+ readouts.push({
3253
+ id: readoutId,
3254
+ kind: "order-status",
3255
+ source: opts.source,
3256
+ stepsSource: opts.stepsSource,
3257
+ color: opts.color,
3258
+ label: opts.label,
3259
+ });
3260
+ return api;
3261
+ },
3262
+ attendanceGrid(readoutId, opts) {
3263
+ assertUniqueReadoutId(readoutId);
3264
+ readouts.push({
3265
+ id: readoutId,
3266
+ kind: "attendance-grid",
3267
+ source: opts.source,
3268
+ membersSource: opts.membersSource,
3269
+ color: opts.color,
3270
+ label: opts.label,
3271
+ });
3272
+ return api;
3273
+ },
3274
+ timezoneClock(readoutId, opts) {
3275
+ assertUniqueReadoutId(readoutId);
3276
+ readouts.push({
3277
+ id: readoutId,
3278
+ kind: "timezone-clock",
3279
+ source: opts.source,
3280
+ color: opts.color,
3281
+ label: opts.label,
3282
+ });
3283
+ return api;
3284
+ },
3285
+ formSummary(readoutId, opts) {
3286
+ assertUniqueReadoutId(readoutId);
3287
+ readouts.push({
3288
+ id: readoutId,
3289
+ kind: "form-summary",
3290
+ source: opts.source,
3291
+ color: opts.color,
3292
+ label: opts.label,
3293
+ });
3294
+ return api;
3295
+ },
3296
+ songQueue(readoutId, opts) {
3297
+ assertUniqueReadoutId(readoutId);
3298
+ readouts.push({
3299
+ id: readoutId,
3300
+ kind: "song-queue",
3301
+ source: opts.source,
3302
+ currentSource: opts.currentSource,
3303
+ max: opts.max,
3304
+ color: opts.color,
3305
+ label: opts.label,
3306
+ });
3307
+ return api;
3308
+ },
3309
+ calendarMonth(readoutId, opts) {
3310
+ assertUniqueReadoutId(readoutId);
3311
+ readouts.push({
3312
+ id: readoutId,
3313
+ kind: "calendar-month",
3314
+ source: opts.source,
3315
+ monthName: opts.monthName,
3316
+ color: opts.color,
3317
+ label: opts.label,
3318
+ });
3319
+ return api;
3320
+ },
3321
+ terminal(readoutId, opts) {
3322
+ assertUniqueReadoutId(readoutId);
3323
+ readouts.push({
3324
+ id: readoutId,
3325
+ kind: "terminal",
3326
+ source: opts.source,
3327
+ max: opts.max,
3328
+ color: opts.color,
3329
+ label: opts.label,
3330
+ });
3331
+ return api;
3332
+ },
3333
+ chessBoard(readoutId, opts) {
3334
+ assertUniqueReadoutId(readoutId);
3335
+ readouts.push({
3336
+ id: readoutId,
3337
+ kind: "chess-board",
3338
+ source: opts.source,
3339
+ cellSize: opts.cellSize,
3340
+ label: opts.label,
3341
+ });
3342
+ return api;
3343
+ },
3344
+ kanbanBoard(readoutId, opts) {
3345
+ assertUniqueReadoutId(readoutId);
3346
+ readouts.push({
3347
+ id: readoutId,
3348
+ kind: "kanban-board",
3349
+ source: opts.source,
3350
+ columnWidth: opts.columnWidth,
3351
+ max: opts.max,
3352
+ label: opts.label,
3353
+ });
3354
+ return api;
3355
+ },
3356
+ breadcrumb(readoutId, opts) {
3357
+ assertUniqueReadoutId(readoutId);
3358
+ readouts.push({
3359
+ id: readoutId,
3360
+ kind: "breadcrumb",
3361
+ source: opts.source,
3362
+ currentSource: opts.currentSource,
3363
+ color: opts.color,
3364
+ label: opts.label,
3365
+ });
3366
+ return api;
3367
+ },
3368
+ timelineVertical(readoutId, opts) {
3369
+ assertUniqueReadoutId(readoutId);
3370
+ readouts.push({
3371
+ id: readoutId,
3372
+ kind: "timeline-vertical",
3373
+ source: opts.source,
3374
+ color: opts.color,
3375
+ max: opts.max,
3376
+ label: opts.label,
3377
+ });
3378
+ return api;
3379
+ },
3380
+ statusTimeline(readoutId, opts) {
3381
+ assertUniqueReadoutId(readoutId);
3382
+ readouts.push({
3383
+ id: readoutId,
3384
+ kind: "status-timeline",
3385
+ source: opts.source,
3386
+ colorMap: opts.colorMap,
3387
+ max: opts.max,
3388
+ label: opts.label,
3389
+ });
3390
+ return api;
3391
+ },
3392
+ calendarWeek(readoutId, opts) {
3393
+ assertUniqueReadoutId(readoutId);
3394
+ readouts.push({
3395
+ id: readoutId,
3396
+ kind: "calendar-week",
3397
+ source: opts.source,
3398
+ cellSize: opts.cellSize,
3399
+ color: opts.color,
3400
+ label: opts.label,
3401
+ });
3402
+ return api;
3403
+ },
3404
+ kpiComparison(readoutId, opts) {
3405
+ assertUniqueReadoutId(readoutId);
3406
+ readouts.push({
3407
+ id: readoutId,
3408
+ kind: "kpi-comparison",
3409
+ source: opts.source,
3410
+ max: opts.max,
3411
+ colorA: opts.colorA,
3412
+ colorB: opts.colorB,
3413
+ label: opts.label,
3414
+ });
3415
+ return api;
3416
+ },
3417
+ stepProgress(readoutId, opts) {
3418
+ assertUniqueReadoutId(readoutId);
3419
+ readouts.push({
3420
+ id: readoutId,
3421
+ kind: "step-progress",
3422
+ source: opts.source,
3423
+ stepsSource: opts.stepsSource,
3424
+ color: opts.color,
3425
+ label: opts.label,
3426
+ });
3427
+ return api;
3428
+ },
3429
+ userPresence(readoutId, opts) {
3430
+ assertUniqueReadoutId(readoutId);
3431
+ readouts.push({
3432
+ id: readoutId,
3433
+ kind: "user-presence",
3434
+ source: opts.source,
3435
+ max: opts.max,
3436
+ label: opts.label,
3437
+ });
3438
+ return api;
3439
+ },
3440
+ ratingThumb(readoutId, opts) {
3441
+ assertUniqueReadoutId(readoutId);
3442
+ readouts.push({
3443
+ id: readoutId,
3444
+ kind: "rating-thumb",
3445
+ source: opts.source,
3446
+ colorUp: opts.colorUp,
3447
+ colorDown: opts.colorDown,
3448
+ label: opts.label,
3449
+ });
3450
+ return api;
3451
+ },
3452
+ orgChartMini(readoutId, opts) {
3453
+ assertUniqueReadoutId(readoutId);
3454
+ readouts.push({
3455
+ id: readoutId,
3456
+ kind: "org-chart-mini",
3457
+ source: opts.source,
3458
+ color: opts.color,
3459
+ label: opts.label,
3460
+ });
3461
+ return api;
3462
+ },
3463
+ kpiTrendTile(readoutId, opts) {
3464
+ assertUniqueReadoutId(readoutId);
3465
+ readouts.push({
3466
+ id: readoutId,
3467
+ kind: "kpi-trend-tile",
3468
+ source: opts.source,
3469
+ prevSource: opts.prevSource,
3470
+ historySource: opts.historySource,
3471
+ unit: opts.unit,
3472
+ colorPos: opts.colorPos,
3473
+ colorNeg: opts.colorNeg,
3474
+ label: opts.label,
3475
+ });
3476
+ return api;
3477
+ },
3478
+ quickPollEmoji(readoutId, opts) {
3479
+ assertUniqueReadoutId(readoutId);
3480
+ readouts.push({
3481
+ id: readoutId,
3482
+ kind: "quick-poll-emoji",
3483
+ source: opts.source,
3484
+ colorWinner: opts.colorWinner,
3485
+ label: opts.label,
3486
+ });
3487
+ return api;
3488
+ },
3489
+ voiceMessage(readoutId, opts) {
3490
+ assertUniqueReadoutId(readoutId);
3491
+ readouts.push({
3492
+ id: readoutId,
3493
+ kind: "voice-message",
3494
+ source: opts.source,
3495
+ progressSource: opts.progressSource,
3496
+ duration: opts.duration,
3497
+ colorPlay: opts.colorPlay,
3498
+ colorBar: opts.colorBar,
3499
+ label: opts.label,
3500
+ });
3501
+ return api;
3502
+ },
3503
+ threadSummary(readoutId, opts) {
3504
+ assertUniqueReadoutId(readoutId);
3505
+ readouts.push({
3506
+ id: readoutId,
3507
+ kind: "thread-summary",
3508
+ source: opts.source,
3509
+ colorUnread: opts.colorUnread,
3510
+ label: opts.label,
3511
+ });
3512
+ return api;
3513
+ },
3514
+ readReceipt(readoutId, opts) {
3515
+ assertUniqueReadoutId(readoutId);
3516
+ readouts.push({
3517
+ id: readoutId,
3518
+ kind: "read-receipt",
3519
+ source: opts.source,
3520
+ colorRead: opts.colorRead,
3521
+ colorPending: opts.colorPending,
3522
+ label: opts.label,
3523
+ });
3524
+ return api;
3525
+ },
3526
+ passwordStrength(readoutId, opts) {
3527
+ assertUniqueReadoutId(readoutId);
3528
+ readouts.push({
3529
+ id: readoutId,
3530
+ kind: "password-strength",
3531
+ source: opts.source,
3532
+ colorStrong: opts.colorStrong,
3533
+ colorWeak: opts.colorWeak,
3534
+ label: opts.label,
3535
+ });
3536
+ return api;
3537
+ },
3538
+ otpInput(readoutId, opts) {
3539
+ assertUniqueReadoutId(readoutId);
3540
+ readouts.push({
3541
+ id: readoutId,
3542
+ kind: "otp-input",
3543
+ source: opts.source,
3544
+ colorFocus: opts.colorFocus,
3545
+ label: opts.label,
3546
+ });
3547
+ return api;
3548
+ },
3549
+ fileDropzone(readoutId, opts) {
3550
+ assertUniqueReadoutId(readoutId);
3551
+ readouts.push({
3552
+ id: readoutId,
3553
+ kind: "file-dropzone",
3554
+ source: opts.source,
3555
+ colorActive: opts.colorActive,
3556
+ label: opts.label,
3557
+ });
3558
+ return api;
3559
+ },
3560
+ logStream(readoutId, opts) {
3561
+ assertUniqueReadoutId(readoutId);
3562
+ readouts.push({
3563
+ id: readoutId,
3564
+ kind: "log-stream",
3565
+ source: opts.source,
3566
+ label: opts.label,
3567
+ });
3568
+ return api;
3569
+ },
3570
+ alertBanner(readoutId, opts) {
3571
+ assertUniqueReadoutId(readoutId);
3572
+ readouts.push({
3573
+ id: readoutId,
3574
+ kind: "alert-banner",
3575
+ source: opts.source,
3576
+ label: opts.label,
3577
+ });
3578
+ return api;
3579
+ },
3580
+ serviceHealth(readoutId, opts) {
3581
+ assertUniqueReadoutId(readoutId);
3582
+ readouts.push({
3583
+ id: readoutId,
3584
+ kind: "service-health",
3585
+ source: opts.source,
3586
+ label: opts.label,
3587
+ });
3588
+ return api;
3589
+ },
3590
+ cartSummary(readoutId, opts) {
3591
+ assertUniqueReadoutId(readoutId);
3592
+ readouts.push({
3593
+ id: readoutId,
3594
+ kind: "cart-summary",
3595
+ source: opts.source,
3596
+ currency: opts.currency,
3597
+ colorTotal: opts.colorTotal,
3598
+ label: opts.label,
3599
+ });
3600
+ return api;
3601
+ },
3602
+ pricingTier(readoutId, opts) {
3603
+ assertUniqueReadoutId(readoutId);
3604
+ readouts.push({
3605
+ id: readoutId,
3606
+ kind: "pricing-tier",
3607
+ source: opts.source,
3608
+ colorAccent: opts.colorAccent,
3609
+ currency: opts.currency,
3610
+ label: opts.label,
3611
+ });
3612
+ return api;
3613
+ },
3614
+ couponCode(readoutId, opts) {
3615
+ assertUniqueReadoutId(readoutId);
3616
+ readouts.push({
3617
+ id: readoutId,
3618
+ kind: "coupon-code",
3619
+ source: opts.source,
3620
+ colorApplied: opts.colorApplied,
3621
+ label: opts.label,
3622
+ });
3623
+ return api;
3624
+ },
3625
+ articlePreview(readoutId, opts) {
3626
+ assertUniqueReadoutId(readoutId);
3627
+ readouts.push({
3628
+ id: readoutId,
3629
+ kind: "article-preview",
3630
+ source: opts.source,
3631
+ colorAccent: opts.colorAccent,
3632
+ label: opts.label,
3633
+ });
3634
+ return api;
3635
+ },
3636
+ tocNav(readoutId, opts) {
3637
+ assertUniqueReadoutId(readoutId);
3638
+ readouts.push({
3639
+ id: readoutId,
3640
+ kind: "toc-nav",
3641
+ source: opts.source,
3642
+ colorActive: opts.colorActive,
3643
+ label: opts.label,
3644
+ });
3645
+ return api;
3646
+ },
3647
+ shareButtons(readoutId, opts) {
3648
+ assertUniqueReadoutId(readoutId);
3649
+ readouts.push({
3650
+ id: readoutId,
3651
+ kind: "share-buttons",
3652
+ source: opts.source,
3653
+ label: opts.label,
3654
+ });
3655
+ return api;
3656
+ },
3657
+ },
3658
+ animation: {
3659
+ scroll(triggerId, opts) {
3660
+ assertUniqueScrollTriggerId(triggerId);
3661
+ const start = opts?.start ?? 1.0;
3662
+ const end = opts?.end ?? 0.0;
3663
+ const scrub = opts?.scrub ?? 1;
3664
+ // 非有限値の fail-fast (CAR #263、 NaN は < 0 / > 1 で silent pass する)
3665
+ assertFiniteNumber(start, `scroll trigger '${triggerId}' start`);
3666
+ assertFiniteNumber(end, `scroll trigger '${triggerId}' end`);
3667
+ assertFiniteNumber(scrub, `scroll trigger '${triggerId}' scrub`);
3668
+ // start / end は viewport 相対位置 (0 = viewport top、 1 = viewport bottom)、 0..1 範囲内に制限する。
3669
+ // 範囲外を許容すると attach 側の IntersectionObserver in-view gate で recompute が止まり、
3670
+ // element が viewport 外に出た瞬間に progress が中間値で stuck する (codex Round 2 finding)。
3671
+ if (start < 0 || start > 1) {
3672
+ throw new Error(
3673
+ `scroll trigger '${triggerId}': start (${start}) は 0..1 範囲 (viewport 相対位置) である必要があります`,
3674
+ );
3675
+ }
3676
+ if (end < 0 || end > 1) {
3677
+ throw new Error(
3678
+ `scroll trigger '${triggerId}': end (${end}) は 0..1 範囲 (viewport 相対位置) である必要があります`,
3679
+ );
3680
+ }
3681
+ // 注 = start === end は center-to-center 系 trigger (element height の scroll 距離で progress 0→1)、
3682
+ // valid pattern。 zero-range の degenerate case (element height 0) は runtime で判定 (builder では element 未 attach)。
3683
+ if (scrub < 0 || scrub > 1) {
3684
+ throw new Error(
3685
+ `scroll trigger '${triggerId}': scrub (${scrub}) は 0..1 範囲内である必要があります`,
3686
+ );
3687
+ }
3688
+ scrollTriggers.push({ id: triggerId, start, end, scrub, label: opts?.label });
3689
+ return api;
3690
+ },
3691
+ },
3692
+ phase(phaseId, opts, build) {
3693
+ const acc: PhaseAcc = { activate: [], tweens: [], sets: [] };
3694
+ build(makePhaseBuilder(acc));
3695
+ phases.push({
3696
+ id: phaseId,
3697
+ duration: opts.duration ?? 1800,
3698
+ title: opts.title,
3699
+ body: opts.body,
3700
+ activate: acc.activate,
3701
+ tweens: acc.tweens,
3702
+ sets: acc.sets,
3703
+ badge: acc.badge,
3704
+ });
3705
+ return api;
3706
+ },
3707
+ build() {
3708
+ // 全 array を copy して builder accumulator の参照を切る (builder 再利用時の
3709
+ // 過去 build 結果を汚さない、 codex PR #260 review MAJOR #1 対応)。
3710
+ const built = {
3711
+ id,
3712
+ topic: options.topic,
3713
+ // 既定 (extract) は省略して JSON 形状を変えない。 exclude を明示した図だけ持たせる。
3714
+ ...(options.structuredData === "exclude" ? { structuredData: "exclude" as const } : {}),
3715
+ // inputs / formulas / scrollTriggers / eventBindings は空なら省略 (static diagram の JSON 形状維持)
3716
+ ...(inputs.length > 0 ? { inputs: [...inputs] } : {}),
3717
+ ...(formulas.length > 0 ? { formulas: [...formulas] } : {}),
3718
+ ...(scrollTriggers.length > 0 ? { scrollTriggers: [...scrollTriggers] } : {}),
3719
+ ...(eventBindings.length > 0 ? { eventBindings: [...eventBindings] } : {}),
3720
+ ...(readouts.length > 0 ? { readouts: [...readouts] } : {}),
3721
+ lanes: [...lanes],
3722
+ nodes: [...nodes],
3723
+ edges: [...edges],
3724
+ states: [...states],
3725
+ phases: [...phases],
3726
+ };
3727
+ // in-place mutation guard (CAR #260)。 caller が diagram.formulas[0].expression = "..." 等の
3728
+ // mutation を試みると strict mode で throw、 non-strict でも silent 無効化される。
3729
+ // これにより interactive schemaKey memoize (identity dep) が in-place mutation で stale
3730
+ // cache に落ちる regression を builder 層で構造的に防ぐ。 immutable 契約は SSOT として
3731
+ // README / doc に明記、 diagram を差替たい場合は builder で新規生成すること。
3732
+ return deepFreezeDiagram(built);
3733
+ },
3734
+ };
3735
+
3736
+ return api;
3737
+ }
3738
+
3739
+ // 型 re-export
3740
+ export type { EdgeStyle, NodeKind, Side, Tone };