@cardenelabs/cdl 0.6.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +401 -0
- package/SPEC.md +32 -17
- package/dist/index.cjs +820 -364
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -14
- package/dist/index.d.ts +68 -14
- package/dist/index.js +820 -364
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +692 -254
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +692 -254
- package/dist/react.js.map +1 -1
- package/dist/{render-DquCvgOB.d.cts → render-hmFRAabt.d.cts} +33 -2
- package/dist/{render-DquCvgOB.d.ts → render-hmFRAabt.d.ts} +33 -2
- package/package.json +6 -3
- package/src/kinds/box-lines.ts +120 -0
- package/src/kinds/gantt.tsx +152 -24
- package/src/kinds/mind-map.tsx +77 -16
- package/src/kinds/quadrant.tsx +28 -2
- package/src/kinds/shape-blockchain.tsx +65 -12
- package/src/kinds/shape-region.tsx +152 -34
- package/src/kinds/tree.tsx +121 -24
- package/src/layout/collisions.ts +10 -2
- package/src/layout/footer-shape.ts +86 -24
- package/src/layout/lifeline.ts +80 -0
- package/src/layout/spec.ts +21 -2
- package/src/layout/text-width.ts +92 -4
- package/src/layout/viewbox.ts +0 -8
- package/src/layout.ts +8 -1
- package/src/presets.ts +120 -22
- package/src/render/header.tsx +3 -1
- package/src/render/nodes.tsx +6 -5
- package/src/render/payload-binding.ts +88 -3
- package/src/render/stage.tsx +17 -34
- package/src/render/template-fields.ts +212 -0
- package/src/render/utils.ts +32 -2
- package/src/render.tsx +2 -9
- package/src/types.ts +33 -2
- package/src/validate.ts +27 -9
- package/src/visual-validate.ts +34 -1
- package/src/layout/label-shift.ts +0 -28
package/src/presets.ts
CHANGED
|
@@ -2,7 +2,40 @@ import { diagram } from "./builder";
|
|
|
2
2
|
import type { DiagramBuilder } from "./builder";
|
|
3
3
|
import { LANE_PAD_MIN, NODE_SIZE } from "./layout/tokens";
|
|
4
4
|
import { requiredRowsWidth } from "./kinds/row-align";
|
|
5
|
-
import type { NodeKind, Tone, EdgeStyle, JourneyEmotion, QuadrantKey } from "./types";
|
|
5
|
+
import type { NodeKind, Tone, EdgeStyle, JourneyEmotion, QuadrantKey, BoundNumber } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 状態を読む書き方 (`{名前}`)。
|
|
9
|
+
*
|
|
10
|
+
* 中身の型 (`BoundNumber` / `BoundEnum`) は任意の文字列を受けるが、 **入口はこの形だけに
|
|
11
|
+
* 限る** (#468 Round 1)。 広いままだと `value: "abc"` や `quadrant: "topLfet"` のような
|
|
12
|
+
* 書き間違いが、 これまで組み立ての時点で止まっていたのに実行時まで進み、 解決側の既定値
|
|
13
|
+
* (0 / 左下 / 普通) に落ちた図が出る。
|
|
14
|
+
*/
|
|
15
|
+
type 名前の頭 =
|
|
16
|
+
| "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m"
|
|
17
|
+
| "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
|
|
18
|
+
| "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M"
|
|
19
|
+
| "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
|
|
20
|
+
| "_";
|
|
21
|
+
|
|
22
|
+
export type BindingRef = `{${名前の頭}${string}}`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* その文字列が **全体で** 状態を読む書き方かどうか (#468 Round 5)。
|
|
26
|
+
*
|
|
27
|
+
* `hasUnresolvedRef` は「参照らしい部分を含むか」 を見るため、 `Phase {A}` のような通常の
|
|
28
|
+
* 名前まで参照とみなす。 工程表の入口は名前と参照を同じ欄で受けるので、 部分一致で判定すると
|
|
29
|
+
* **正当な名前が黙って消える** (実測 = `start: "Phase {A}"` が位置 `"Phase {A}"` になり、
|
|
30
|
+
* 名前の欄が空になった)。
|
|
31
|
+
*
|
|
32
|
+
* 型 (`BindingRef`) が「文字列全体が `{名前}`」 を要求しているので、 判定もそれに揃える。
|
|
33
|
+
*
|
|
34
|
+
* **中身に中括弧を許す**。 型は `{${名前の頭}${string}}` で、 `string` が中括弧を含む形
|
|
35
|
+
* (`{a}tail}` / `{a{b}`) も受ける。 実装だけ狭めると、 **型では通るのに名前として扱われる**
|
|
36
|
+
* 食い違いができる (Round 6 の指摘)。 型で中括弧を除く条件は書けないため、 実装を型に合わせる。
|
|
37
|
+
*/
|
|
38
|
+
const 参照そのもの = (v: string): boolean => /^\{[A-Za-z_][\s\S]*\}$/.test(v);
|
|
6
39
|
|
|
7
40
|
/**
|
|
8
41
|
* 図の格子の目 (world unit)。 `grid-alignment` (軸 21) が見る単位と同じ。
|
|
@@ -213,11 +246,32 @@ export type FlowPreset = {
|
|
|
213
246
|
/** edge tone / style の default (各 step → next step の edge) */
|
|
214
247
|
defaultTone?: Tone;
|
|
215
248
|
defaultStyle?: EdgeStyle;
|
|
249
|
+
/**
|
|
250
|
+
* step 間の edge を自動生成するか (既定 true)。
|
|
251
|
+
*
|
|
252
|
+
* `false` にすると `step()` は node だけを置き、矢印は呼び手が `edge()` で宣言する。
|
|
253
|
+
* **並び順と違う流れ** (`alpha` から `gamma` へ直接繋いで `beta` を経由しない等) を
|
|
254
|
+
* 書きたい時に使う。 自動生成しか無かった間、その形は本 preset では作れなかった (#474)。
|
|
255
|
+
*
|
|
256
|
+
* 既定を `true` にしているので、既存の呼び出しは 1 行も変えずに同じ図になる。
|
|
257
|
+
*/
|
|
258
|
+
autoChain?: boolean;
|
|
216
259
|
};
|
|
217
260
|
|
|
218
261
|
export type FlowBuilder = {
|
|
219
|
-
/** step を順番に追加、 自動で前 step から edge 接続 */
|
|
262
|
+
/** step を順番に追加、 自動で前 step から edge 接続 (`autoChain: false` なら node だけ) */
|
|
220
263
|
step: (node: FlowStepInput, edgeLabel?: string) => FlowBuilder;
|
|
264
|
+
/**
|
|
265
|
+
* 矢印を宣言する (#474)。 tone / style は preset の既定を引き継ぐ。
|
|
266
|
+
*
|
|
267
|
+
* ここで作った矢印も `build()` の phase の `activate` に載る (自動生成分と同じ扱い)。
|
|
268
|
+
* 載せないと animation で光らず、自動生成と見た目が揃わない。
|
|
269
|
+
*/
|
|
270
|
+
edge: (
|
|
271
|
+
from: string,
|
|
272
|
+
to: string,
|
|
273
|
+
opts?: { id?: string; label?: string; tone?: Tone; style?: EdgeStyle },
|
|
274
|
+
) => FlowBuilder;
|
|
221
275
|
build: () => ReturnType<DiagramBuilder["build"]>;
|
|
222
276
|
};
|
|
223
277
|
|
|
@@ -238,6 +292,8 @@ export function flow(preset: FlowPreset): FlowBuilder {
|
|
|
238
292
|
b.lane("flow", { width: w, ...(preset.laneLabel ? { label: preset.laneLabel } : {}) });
|
|
239
293
|
const tone: Tone = preset.defaultTone ?? "accent";
|
|
240
294
|
const style: EdgeStyle = preset.defaultStyle ?? "dotted-flow";
|
|
295
|
+
// 既定は自動生成。 省略した既存の呼び出しの挙動を変えない (#474)
|
|
296
|
+
const autoChain = preset.autoChain ?? true;
|
|
241
297
|
let prevId: string | null = null;
|
|
242
298
|
let stackIdx = 0;
|
|
243
299
|
const phaseActivate: string[] = [];
|
|
@@ -253,7 +309,7 @@ export function flow(preset: FlowPreset): FlowBuilder {
|
|
|
253
309
|
...(node.subtitle ? { subtitle: node.subtitle } : {}),
|
|
254
310
|
});
|
|
255
311
|
phaseActivate.push(node.id);
|
|
256
|
-
if (prevId !== null) {
|
|
312
|
+
if (autoChain && prevId !== null) {
|
|
257
313
|
const edgeId = `e-${prevId}-${node.id}`;
|
|
258
314
|
b.edge(prevId, node.id, {
|
|
259
315
|
id: edgeId,
|
|
@@ -266,6 +322,17 @@ export function flow(preset: FlowPreset): FlowBuilder {
|
|
|
266
322
|
prevId = node.id;
|
|
267
323
|
return api;
|
|
268
324
|
},
|
|
325
|
+
edge(from, to, opts) {
|
|
326
|
+
const edgeId = opts?.id ?? `e-${from}-${to}`;
|
|
327
|
+
b.edge(from, to, {
|
|
328
|
+
id: edgeId,
|
|
329
|
+
label: opts?.label ?? "→",
|
|
330
|
+
tone: opts?.tone ?? tone,
|
|
331
|
+
style: opts?.style ?? style,
|
|
332
|
+
});
|
|
333
|
+
phaseActivate.push(edgeId);
|
|
334
|
+
return api;
|
|
335
|
+
},
|
|
269
336
|
build() {
|
|
270
337
|
b.phase(
|
|
271
338
|
"flow",
|
|
@@ -398,7 +465,8 @@ const actorW = Math.max(140, label.length * 22 + 52);
|
|
|
398
465
|
// warning suppress、 ASCII long name (AuthenticationService 21 char) は 514 になり
|
|
399
466
|
// 過大化との trade-off だが 「警告出るのに対応できない」 の user feedback 優先。
|
|
400
467
|
const actorW = Math.max(140, label.length * 22 + 52);
|
|
401
|
-
|
|
468
|
+
// `role` を付けるのは、 描画側が「どれが下端の名札か」 を位置で推測しないため (#458)
|
|
469
|
+
b.node(footerId, { lane: laneId, stack: footerStack, kind: "card", title: label, w: actorW, h: 72, role: "lifeline-footer" });
|
|
402
470
|
phaseActivate.push(footerId);
|
|
403
471
|
});
|
|
404
472
|
b.phase(
|
|
@@ -1052,8 +1120,8 @@ export type TreeNode = {
|
|
|
1052
1120
|
parent?: string;
|
|
1053
1121
|
/** 表示 kind (default "card") */
|
|
1054
1122
|
kind?: NodeKind;
|
|
1123
|
+
/** 箱の 2 行目に出す補足。 上の小ラベルは木では受けない (#471、 `TreeNodePayload` 参照) */
|
|
1055
1124
|
subtitle?: string;
|
|
1056
|
-
eyebrow?: string;
|
|
1057
1125
|
};
|
|
1058
1126
|
|
|
1059
1127
|
export type TreePreset = {
|
|
@@ -1109,7 +1177,6 @@ export function tree(preset: TreePreset): TreeBuilder {
|
|
|
1109
1177
|
title: n.title,
|
|
1110
1178
|
parent: n.parent,
|
|
1111
1179
|
subtitle: n.subtitle,
|
|
1112
|
-
eyebrow: n.eyebrow,
|
|
1113
1180
|
})),
|
|
1114
1181
|
});
|
|
1115
1182
|
b.phase(
|
|
@@ -1133,7 +1200,8 @@ export type { JourneyEmotion } from "./types";
|
|
|
1133
1200
|
export type JourneyStep = {
|
|
1134
1201
|
id: string;
|
|
1135
1202
|
title: string;
|
|
1136
|
-
|
|
1203
|
+
/** 起伏の段階。 `{名前}` を書くと状態から読む (#468) */
|
|
1204
|
+
emotion: JourneyEmotion | BindingRef;
|
|
1137
1205
|
/** どの touchpoint (例 "Website", "Email", "Support") */
|
|
1138
1206
|
touchpoint?: string;
|
|
1139
1207
|
/** opportunity (改善余地のメモ) */
|
|
@@ -1296,8 +1364,8 @@ export function mindMap(preset: MindMapPreset): MindMapBuilder {
|
|
|
1296
1364
|
export type FunnelStage = {
|
|
1297
1365
|
id: string;
|
|
1298
1366
|
title: string;
|
|
1299
|
-
/** この stage の人数 /
|
|
1300
|
-
count: number;
|
|
1367
|
+
/** この stage の人数 / 件数。 `{名前}` を書くと状態から読む (#468) */
|
|
1368
|
+
count: number | BindingRef;
|
|
1301
1369
|
/** stage 詳細 */
|
|
1302
1370
|
subtitle?: string;
|
|
1303
1371
|
};
|
|
@@ -1376,8 +1444,8 @@ export type QuadrantQuadrantLabel = QuadrantKey;
|
|
|
1376
1444
|
export type QuadrantItem = {
|
|
1377
1445
|
id: string;
|
|
1378
1446
|
title: string;
|
|
1379
|
-
/** どの象限 (4 象限) */
|
|
1380
|
-
quadrant: QuadrantQuadrantLabel;
|
|
1447
|
+
/** どの象限 (4 象限)。 `{名前}` を書くと状態から読む (#468) */
|
|
1448
|
+
quadrant: QuadrantQuadrantLabel | BindingRef;
|
|
1381
1449
|
subtitle?: string;
|
|
1382
1450
|
};
|
|
1383
1451
|
|
|
@@ -1472,7 +1540,8 @@ export type ChartType = "pie" | "bar" | "line";
|
|
|
1472
1540
|
export type ChartDatum = {
|
|
1473
1541
|
id: string;
|
|
1474
1542
|
label: string;
|
|
1475
|
-
|
|
1543
|
+
/** 棒 / 折れ線 / 扇の大きさ。 `{名前}` を書くと状態から読む (#468) */
|
|
1544
|
+
value: number | BindingRef;
|
|
1476
1545
|
/** 個別 tone (default は preset の defaultTone) */
|
|
1477
1546
|
tone?: Tone;
|
|
1478
1547
|
};
|
|
@@ -1553,9 +1622,15 @@ export function chart(preset: ChartPreset): ChartBuilder {
|
|
|
1553
1622
|
export type GanttTask = {
|
|
1554
1623
|
id: string;
|
|
1555
1624
|
title: string;
|
|
1556
|
-
/**
|
|
1625
|
+
/**
|
|
1626
|
+
* 開始の目盛りの名前 (例 "2026-Q1" / "Day 1")。
|
|
1627
|
+
*
|
|
1628
|
+
* `{名前}` を書くと **位置そのもの** を状態から読む (#468)。 目盛りの名前の対応付けは
|
|
1629
|
+
* 飛ばす = 名前の並びに `{名前}` が混ざると、 位置が状態で動くたびに目盛りの並びが
|
|
1630
|
+
* 変わってしまう。
|
|
1631
|
+
*/
|
|
1557
1632
|
start: string;
|
|
1558
|
-
/**
|
|
1633
|
+
/** 終了の目盛りの名前。 `{名前}` の扱いは `start` と同じ (#468) */
|
|
1559
1634
|
end: string;
|
|
1560
1635
|
/** owner / assignee */
|
|
1561
1636
|
owner?: string;
|
|
@@ -1568,6 +1643,17 @@ export type GanttPreset = {
|
|
|
1568
1643
|
topic: string;
|
|
1569
1644
|
defaultTone?: Tone;
|
|
1570
1645
|
laneWidth?: number;
|
|
1646
|
+
/**
|
|
1647
|
+
* 横軸を何目盛り分にするか (`#483`)。
|
|
1648
|
+
*
|
|
1649
|
+
* 書かないと今までどおり全 task の最大値から自動で決まるため、 `{名前}` で動く task が
|
|
1650
|
+
* 1 つあると **動かしていない task の帯まで伸び縮みする** (実測 = 端を 3 から 9 に動かすと
|
|
1651
|
+
* 静的な task の帯が 252 から 97.2 になった)。
|
|
1652
|
+
*
|
|
1653
|
+
* 書くと尺がその値で固定され、 他の task の帯の位置と幅が変わらない。 範囲から外れる task の
|
|
1654
|
+
* 帯は右端の目盛りに寄せて描く (`kinds/gantt.tsx` SSOT)。
|
|
1655
|
+
*/
|
|
1656
|
+
axisMax?: number;
|
|
1571
1657
|
};
|
|
1572
1658
|
|
|
1573
1659
|
export type GanttBuilder = {
|
|
@@ -1579,8 +1665,11 @@ export type GanttBuilder = {
|
|
|
1579
1665
|
* gantt preset ... Gantt chart / sprint planning / release timeline。
|
|
1580
1666
|
* task ごとに 1 lane を作り、 start - end + owner を subtitle、 dependsOn で前 task → 次 task の edge を auto 生成。
|
|
1581
1667
|
*
|
|
1668
|
+
* 横軸の範囲は既定では全 task の最大値から自動で決まる。 `axisMax` を書くと尺がその値で
|
|
1669
|
+
* 固定され、 `{名前}` で動く task があっても他の task の帯が動かない (`#483`)。
|
|
1670
|
+
*
|
|
1582
1671
|
* @example
|
|
1583
|
-
* gantt({ id: "release", topic: "Release timeline" })
|
|
1672
|
+
* gantt({ id: "release", topic: "Release timeline", axisMax: 4 })
|
|
1584
1673
|
* .task({ id: "design", title: "Design", start: "Q1", end: "Q1" })
|
|
1585
1674
|
* .task({ id: "build", title: "Build", start: "Q2", end: "Q2", dependsOn: "design" })
|
|
1586
1675
|
* .task({ id: "test", title: "Test", start: "Q3", end: "Q3", dependsOn: "build" })
|
|
@@ -1599,12 +1688,19 @@ export function gantt(preset: GanttPreset): GanttBuilder {
|
|
|
1599
1688
|
return api;
|
|
1600
1689
|
},
|
|
1601
1690
|
build() {
|
|
1602
|
-
// start / end label を index に変換 (unique label を順に並べる)
|
|
1691
|
+
// start / end label を index に変換 (unique label を順に並べる)。
|
|
1692
|
+
//
|
|
1693
|
+
// `{名前}` を書いた分は **並びに入れない** (#468)。 入れると位置が状態で動くたびに
|
|
1694
|
+
// 目盛りの並びが変わり、 動かしていない task の位置まで動く。
|
|
1603
1695
|
const labelSeq: string[] = [];
|
|
1604
1696
|
for (const t of tasks) {
|
|
1605
|
-
if (!labelSeq.includes(t.start)) labelSeq.push(t.start);
|
|
1606
|
-
if (!labelSeq.includes(t.end)) labelSeq.push(t.end);
|
|
1697
|
+
if (!参照そのもの(t.start) && !labelSeq.includes(t.start)) labelSeq.push(t.start);
|
|
1698
|
+
if (!参照そのもの(t.end) && !labelSeq.includes(t.end)) labelSeq.push(t.end);
|
|
1607
1699
|
}
|
|
1700
|
+
// `{名前}` は位置そのものとして通す。 名前の欄は空にする = 目盛りは位置にしか無く、
|
|
1701
|
+
// 状態から来る位置に付ける名前が無い (空の目盛りは描かれない)
|
|
1702
|
+
const 位置 = (v: string): BoundNumber => (参照そのもの(v) ? v : labelSeq.indexOf(v));
|
|
1703
|
+
const 名札 = (v: string): string => (参照そのもの(v) ? "" : v);
|
|
1608
1704
|
b.lane("gantt", { width: canvasW });
|
|
1609
1705
|
const nodeId = `${preset.id}-gantt`;
|
|
1610
1706
|
b.node(nodeId, {
|
|
@@ -1615,13 +1711,15 @@ export function gantt(preset: GanttPreset): GanttBuilder {
|
|
|
1615
1711
|
eyebrow: "gantt",
|
|
1616
1712
|
w: canvasW,
|
|
1617
1713
|
h: canvasH,
|
|
1714
|
+
// 宣言していない図には欄自体を載せない = 自動で決める経路をそのまま通す
|
|
1715
|
+
...(preset.axisMax !== undefined ? { ganttAxisMax: preset.axisMax } : {}),
|
|
1618
1716
|
ganttData: tasks.map((t) => ({
|
|
1619
1717
|
id: t.id,
|
|
1620
1718
|
title: t.title,
|
|
1621
|
-
startIdx:
|
|
1622
|
-
endIdx:
|
|
1623
|
-
startLabel: t.start,
|
|
1624
|
-
endLabel: t.end,
|
|
1719
|
+
startIdx: 位置(t.start),
|
|
1720
|
+
endIdx: 位置(t.end),
|
|
1721
|
+
startLabel: 名札(t.start),
|
|
1722
|
+
endLabel: 名札(t.end),
|
|
1625
1723
|
owner: t.owner,
|
|
1626
1724
|
dependsOn: t.dependsOn,
|
|
1627
1725
|
tone,
|
package/src/render/header.tsx
CHANGED
|
@@ -124,7 +124,9 @@ function StateDiffCard({
|
|
|
124
124
|
<tbody style={{ color: "var(--cdl-text, #1a1f2a)" }}>
|
|
125
125
|
{states.map((s) => {
|
|
126
126
|
const initial = String(s.initial);
|
|
127
|
-
|
|
127
|
+
// 自分が持つ名前だけを見る (#456)。 通常の object を渡された場合、 `toString` の
|
|
128
|
+
// ような名前の状態で prototype の関数がそのまま表に出る
|
|
129
|
+
const current = Object.hasOwn(stateValues, s.id) ? stateValues[s.id]! : initial;
|
|
128
130
|
const changed = initial !== current;
|
|
129
131
|
return (
|
|
130
132
|
<tr key={s.id}>
|
package/src/render/nodes.tsx
CHANGED
|
@@ -149,9 +149,10 @@ export function CdlNodeView({
|
|
|
149
149
|
return <EventNode node={resolvedNode} active={active} progress={progress} />;
|
|
150
150
|
case "card":
|
|
151
151
|
return <CardNode node={resolvedNode} active={active} />;
|
|
152
|
-
// 図表
|
|
153
|
-
// (`render/payload-binding.ts` が描画直前に解決する)。
|
|
154
|
-
//
|
|
152
|
+
// 図表 9 種は payload が `{signal}` を取れるので stateValues を渡す
|
|
153
|
+
// (`render/payload-binding.ts` が描画直前に解決する)。 取れる欄は種別で違い、
|
|
154
|
+
// 数と語を取る 7 種に対し、mind-map / tree-hierarchy は **名前だけ** を取る (#467)。
|
|
155
|
+
// 親子の繋がりと、描かれない欄 (補足) は解決しない。
|
|
155
156
|
case "chart-line":
|
|
156
157
|
return <ChartLineNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
157
158
|
case "chart-pie":
|
|
@@ -161,13 +162,13 @@ export function CdlNodeView({
|
|
|
161
162
|
case "gantt-timeline":
|
|
162
163
|
return <GanttNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
163
164
|
case "mind-map":
|
|
164
|
-
return <MindMapNode node={resolvedNode} active={active} />;
|
|
165
|
+
return <MindMapNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
165
166
|
case "funnel-stages":
|
|
166
167
|
return <FunnelNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
167
168
|
case "quadrant-matrix":
|
|
168
169
|
return <QuadrantNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
169
170
|
case "tree-hierarchy":
|
|
170
|
-
return <TreeHierarchyNode node={resolvedNode} active={active} />;
|
|
171
|
+
return <TreeHierarchyNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
171
172
|
case "journey-map":
|
|
172
173
|
return <UserJourneyNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
173
174
|
case "shape-file":
|
|
@@ -6,11 +6,14 @@ import type {
|
|
|
6
6
|
GanttTaskPayload,
|
|
7
7
|
JourneyEmotion,
|
|
8
8
|
JourneyStepPayload,
|
|
9
|
+
MindBranchNode,
|
|
10
|
+
MindBranchPayload,
|
|
9
11
|
QuadrantItemPayload,
|
|
10
12
|
QuadrantKey,
|
|
11
13
|
QuadrantPayload,
|
|
14
|
+
TreeNodePayload,
|
|
12
15
|
} from "../types";
|
|
13
|
-
import { interpolate } from "./utils";
|
|
16
|
+
import { hasUnresolvedRef, interpolate } from "./utils";
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* 図表 payload が状態を読むための解決層 (dragon#1161 段 3)。
|
|
@@ -24,8 +27,15 @@ import { interpolate } from "./utils";
|
|
|
24
27
|
* (spec § 4.2 が輪について同じ判断をしている = 1 箇所の壊れで図が消えると原因が分からない)。
|
|
25
28
|
* 印は各 kind が `data-cdl-unresolved="true"` として DOM に出す。
|
|
26
29
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
* `mind-map` / `tree-hierarchy` の 2 種は **名前だけ** を解決する (#467)。 この 2 種が持つのは
|
|
31
|
+
* 数ではなく構造と名前で、動かせるのは名前の側だけ。
|
|
32
|
+
*
|
|
33
|
+
* 解決するのは **描かれる欄だけ** にする。 この 2 種は補足 (`subtitle`) を描かないため
|
|
34
|
+
* (実測で確認)、解決しても画面には出ず、解決できない時だけ印が付く = 見た目は何も変わらない
|
|
35
|
+
* のに印だけ出る状態になる。 描く方を足すかは別 Issue (#471)。
|
|
36
|
+
*
|
|
37
|
+
* 親子の繋がり (`parent`) も解決しない = 状態で繋ぎ替えると木の形そのものが変わり、
|
|
38
|
+
* 配置の計算と印の付け方が別問題になる。
|
|
29
39
|
*/
|
|
30
40
|
|
|
31
41
|
/** 解決の結果。 `ok` が false なら既定値に落ちたことを表す */
|
|
@@ -194,3 +204,78 @@ export function resolveJourneyData(
|
|
|
194
204
|
return e.ok ? { ...s, emotion: e.value } : { ...s, emotion: e.value, unresolved: true as const };
|
|
195
205
|
});
|
|
196
206
|
}
|
|
207
|
+
|
|
208
|
+
/** 名前の欄を解決した結果。 解決できなければ書いたまま残して印を付ける */
|
|
209
|
+
export type ResolvedTreeNode = TreeNodePayload & UnresolvedMark;
|
|
210
|
+
export type ResolvedMindBranch = MindBranchNode & UnresolvedMark;
|
|
211
|
+
export type ResolvedMindData = Omit<MindBranchPayload, "branches"> & {
|
|
212
|
+
branches: ResolvedMindBranch[];
|
|
213
|
+
} & UnresolvedMark;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 名前の欄を状態で埋める (#467)。
|
|
217
|
+
*
|
|
218
|
+
* **解決できなかった時も文字を消さない**。 数の欄は既定値 (0 等) に落とすが、名前は落とす先が
|
|
219
|
+
* 無く、空にすると図から項目の存在まで消えてしまう。 書いた `{名前}` をそのまま残せば、
|
|
220
|
+
* どの名前が解決できなかったかが図の上で読める。 印は他の欄と同じ `data-cdl-unresolved`。
|
|
221
|
+
*/
|
|
222
|
+
function resolveBoundText(raw: string, values: Record<string, string>): BindingResult<string> {
|
|
223
|
+
// 状態を読まない名前は組み直さない (毎 frame の割当てを避ける近道、 他の resolver と同じ)
|
|
224
|
+
if (!raw.includes("{")) return { value: raw, ok: true };
|
|
225
|
+
const resolved = interpolate(raw, values);
|
|
226
|
+
return hasUnresolvedRef(resolved) ? { value: resolved, ok: false } : { value: resolved, ok: true };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 木の名前と補足を解決する。 親子の繋がりは触らない。
|
|
231
|
+
*
|
|
232
|
+
* 補足を解決対象に入れたのは #471 で **補足を描くようにしたため**。 描かない欄を解決すると、
|
|
233
|
+
* 画面は何も変わらないのに解決できない時だけ印が付く状態になる = 印の意味が読めない。
|
|
234
|
+
* 逆に描くようになった欄を解決しないと、 図に `{名前}` が素のまま出る。
|
|
235
|
+
*/
|
|
236
|
+
export function resolveTreeData(
|
|
237
|
+
nodes: TreeNodePayload[],
|
|
238
|
+
values: Record<string, string>,
|
|
239
|
+
): ResolvedTreeNode[] {
|
|
240
|
+
const 読む = nodes.some((n) => n.title.includes("{") || (n.subtitle?.includes("{") ?? false));
|
|
241
|
+
if (!読む) return nodes as ResolvedTreeNode[];
|
|
242
|
+
return nodes.map((n) => {
|
|
243
|
+
const title = resolveBoundText(n.title, values);
|
|
244
|
+
const subtitle = n.subtitle === undefined ? undefined : resolveBoundText(n.subtitle, values);
|
|
245
|
+
const 解決後: ResolvedTreeNode = {
|
|
246
|
+
...n,
|
|
247
|
+
title: title.value,
|
|
248
|
+
...(subtitle ? { subtitle: subtitle.value } : {}),
|
|
249
|
+
};
|
|
250
|
+
return title.ok && (subtitle?.ok ?? true) ? 解決後 : { ...解決後, unresolved: true as const };
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 放射の名前と補足を解決する。 中心の名前と枝の名前 / 補足を見る。
|
|
256
|
+
*
|
|
257
|
+
* 補足を入れた理由は `resolveTreeData` と同じ (#471)。 中心は補足の欄を持たない。
|
|
258
|
+
*/
|
|
259
|
+
export function resolveMindData(
|
|
260
|
+
data: MindBranchPayload,
|
|
261
|
+
values: Record<string, string>,
|
|
262
|
+
): ResolvedMindData {
|
|
263
|
+
const 読む =
|
|
264
|
+
data.rootTitle.includes("{") ||
|
|
265
|
+
data.branches.some((b) => b.title.includes("{") || (b.subtitle?.includes("{") ?? false));
|
|
266
|
+
if (!読む) return data as ResolvedMindData;
|
|
267
|
+
|
|
268
|
+
const root = resolveBoundText(data.rootTitle, values);
|
|
269
|
+
const branches = data.branches.map((b) => {
|
|
270
|
+
const title = resolveBoundText(b.title, values);
|
|
271
|
+
const subtitle = b.subtitle === undefined ? undefined : resolveBoundText(b.subtitle, values);
|
|
272
|
+
const 解決後: ResolvedMindBranch = {
|
|
273
|
+
...b,
|
|
274
|
+
title: title.value,
|
|
275
|
+
...(subtitle ? { subtitle: subtitle.value } : {}),
|
|
276
|
+
};
|
|
277
|
+
return title.ok && (subtitle?.ok ?? true) ? 解決後 : { ...解決後, unresolved: true as const };
|
|
278
|
+
});
|
|
279
|
+
const 解決後: ResolvedMindData = { ...data, rootTitle: root.value, branches };
|
|
280
|
+
return root.ok ? 解決後 : { ...解決後, unresolved: true as const };
|
|
281
|
+
}
|
package/src/render/stage.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { CdlEdgeView, CdlEdgeGlowView, CdlEdgeLabelView } from "./edges";
|
|
|
4
4
|
import { CdlNodeView } from "./nodes";
|
|
5
5
|
import { TONE, TONE_HEX } from "./tone";
|
|
6
6
|
import { buildDiagramAltText } from "../layout/spec";
|
|
7
|
-
import {
|
|
7
|
+
import { lifelineStartYs, uniformLifelineEndY } from "../layout/lifeline";
|
|
8
8
|
|
|
9
9
|
export function CdlStage({
|
|
10
10
|
laid,
|
|
@@ -25,28 +25,22 @@ export function CdlStage({
|
|
|
25
25
|
svgRef?: React.RefObject<SVGSVGElement | null>;
|
|
26
26
|
}): JSX.Element {
|
|
27
27
|
const activeSet = new Set(currentPhase?.activate ?? []);
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// footer.top を集めた配列を作り、 min を uniformFooterTop とする。 配列空 (全 lane が
|
|
31
|
-
// 1 node 以下 or 0 node) の場合、 最初の lifeline lane の (lane.y + lane.height) を fallback。
|
|
32
|
-
const lifelineLanes = laid.lanes.filter((l) => l.lifeline);
|
|
33
|
-
// 下端の名札は 2 つの用途に使う。 縦線の終点 (uniformFooterTop) と、 絵を下げる量の表。
|
|
28
|
+
// 縦線の終点は全 lane で 1 つに揃える (等長保証)。 値の決め方と、 1 lane の異常が図全体から
|
|
29
|
+
// 縦線を消していた形の対処は `uniformLifelineEndY` の説明。
|
|
34
30
|
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
31
|
+
// 下端の名札で絵が箱の上へ出る種別を下げるのは **組み立て側の責務** (`applyFooterShapeDrops`)。
|
|
32
|
+
// ここで `<g transform>` を掛ける形だった頃は、 描画だけが動いて座標が動かず、 座標を読む側
|
|
33
|
+
// (縦線の終点 / 図の下端 / 外接矩形) が実際の位置を知らなかった (#478)。
|
|
37
34
|
//
|
|
38
|
-
//
|
|
39
|
-
// 動きの 1 コマごとに描き直されるため、
|
|
40
|
-
const {
|
|
41
|
-
const
|
|
35
|
+
// 始点と終点は 1 度だけ求め、 配置が変わらない限り作り直さない。 この component は
|
|
36
|
+
// 動きの 1 コマごとに描き直されるため、 毎コマ求め直すと lane 数 × node 数を毎コマ払う
|
|
37
|
+
const { lifelineStarts, uniformFooterTop } = useMemo(() => {
|
|
38
|
+
const starts = lifelineStartYs(laid.nodes);
|
|
42
39
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
lifelineStarts: starts,
|
|
41
|
+
uniformFooterTop: uniformLifelineEndY(laid.lanes, laid.nodes, starts),
|
|
45
42
|
};
|
|
46
43
|
}, [laid]);
|
|
47
|
-
const uniformFooterTop = footerTops.length > 0
|
|
48
|
-
? Math.min(...footerTops)
|
|
49
|
-
: (lifelineLanes[0] ? lifelineLanes[0].y + lifelineLanes[0].height : 0);
|
|
50
44
|
const diagramScale = laid.diagramScale ?? 1;
|
|
51
45
|
return (
|
|
52
46
|
<div className={compact ? "px-3 py-3" : "px-6 py-6"} style={compact ? undefined : { minHeight: 460 }}>
|
|
@@ -167,14 +161,10 @@ export function CdlStage({
|
|
|
167
161
|
/>
|
|
168
162
|
)}
|
|
169
163
|
{lane.lifeline && (() => {
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (laneNodes.length === 0) return null;
|
|
175
|
-
const sortedByCy = laneNodes.slice().sort((a, b) => a.cy - b.cy);
|
|
176
|
-
const header = sortedByCy[0]!;
|
|
177
|
-
const y1 = header.cy + header.h / 2;
|
|
164
|
+
// 縦線は lane の一番上の node の下端から、 全 lane 共通の終点までを結ぶ。
|
|
165
|
+
// node が 1 つの lane も 2 つ以上の lane も同じ終点を使うことで高さが揃う。
|
|
166
|
+
const y1 = lifelineStarts.get(lane.id);
|
|
167
|
+
if (y1 === undefined) return null;
|
|
178
168
|
const y2 = uniformFooterTop;
|
|
179
169
|
if (y2 <= y1) return null;
|
|
180
170
|
return (
|
|
@@ -223,14 +213,7 @@ export function CdlStage({
|
|
|
223
213
|
currentPhaseId={currentPhase?.id}
|
|
224
214
|
/>
|
|
225
215
|
);
|
|
226
|
-
|
|
227
|
-
// 下端の名札で絵が上へ出る種別だけ、 絵の上端が箱の上端に来るまで下げる。
|
|
228
|
-
// node の座標そのものは動かさない = 矢印の接続先や当たり判定は箱のままにする
|
|
229
|
-
return dy === undefined ? (
|
|
230
|
-
<g key={node.id}>{view}</g>
|
|
231
|
-
) : (
|
|
232
|
-
<g key={node.id} transform={`translate(0, ${dy})`}>{view}</g>
|
|
233
|
-
);
|
|
216
|
+
return <g key={node.id}>{view}</g>;
|
|
234
217
|
})}
|
|
235
218
|
|
|
236
219
|
{/* 経由 node 貫通する dotted-flow edge の進行点 glow (3rd pass) */}
|