@cardenelabs/cdl 0.6.1 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "CDL (Chainome Diagram Language). Mermaid-like declarative DSL that compiles to animated SVG diagrams. Built for blockchain / Solidity flows, generic enough for sequence / flow / state / ER / topology diagrams.",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
@@ -1,6 +1,7 @@
1
1
  import type { JSX } from "react";
2
2
  import type { LaidNode, Tone, MindBranchNode } from "../types";
3
3
  import { TONE } from "../render/tone";
4
+ import { resolveMindData } from "../render/payload-binding";
4
5
 
5
6
  const MIND_TONES: Tone[] = ["accent", "teal", "success", "warning", "info", "error"];
6
7
 
@@ -25,10 +26,19 @@ export const MIND_BOX_GAP = 24;
25
26
  * 深い階層は parent 方向へ扇状展開。 depth 別 tone を rotate。
26
27
  * curved edge = bezier で親子を接続、 node は rounded rect で読みやすさ優先。
27
28
  */
28
- export function MindMapNode({ node, active }: { node: LaidNode; active: boolean }): JSX.Element {
29
+ export function MindMapNode({
30
+ node,
31
+ active,
32
+ stateValues = {},
33
+ }: {
34
+ node: LaidNode;
35
+ active: boolean;
36
+ stateValues?: Record<string, string>;
37
+ }): JSX.Element {
29
38
  const x0 = node.cx - node.w / 2;
30
39
  const y0 = node.cy - node.h / 2;
31
- const mindData = node.mindData;
40
+ // 名前は状態を読める (#467)。 中心と枝の両方を解決層に通す
41
+ const mindData = node.mindData ? resolveMindData(node.mindData, stateValues) : undefined;
32
42
  if (!mindData) return <g transform={`translate(${x0} ${y0})`} />;
33
43
 
34
44
  const PAD = 40;
@@ -67,7 +77,7 @@ export function MindMapNode({ node, active }: { node: LaidNode; active: boolean
67
77
  ))}
68
78
 
69
79
  {layout.nodes.map((n) => (
70
- <g key={`n-${n.id}`}>
80
+ <g key={`n-${n.id}`} data-cdl-unresolved={n.unresolved ? "true" : undefined}>
71
81
  <rect
72
82
  x={n.x - n.w / 2}
73
83
  y={n.y - n.h / 2}
@@ -103,6 +113,8 @@ type LayoutNode = {
103
113
  h: number;
104
114
  tone?: Tone;
105
115
  isRoot: boolean;
116
+ /** 名前を状態から解決できなかった項目に付く印 (#467) */
117
+ unresolved?: true;
106
118
  };
107
119
 
108
120
  type LayoutEdge = {
@@ -130,6 +142,7 @@ export function computeMindMapLayout(
130
142
  w: rootW,
131
143
  h: rootH,
132
144
  isRoot: true,
145
+ ...((mindData as { unresolved?: true }).unresolved ? { unresolved: true as const } : {}),
133
146
  });
134
147
 
135
148
  const childrenOf = new Map<string, MindBranchNode[]>();
@@ -281,7 +294,17 @@ export function computeMindMapLayout(
281
294
  y = (ys[0]! + ys[ys.length - 1]!) / 2;
282
295
  }
283
296
 
284
- nodes.push({ id: node.id, title: node.title, x, y, w, h, tone: myTone, isRoot: false });
297
+ nodes.push({
298
+ id: node.id,
299
+ title: node.title,
300
+ x,
301
+ y,
302
+ w,
303
+ h,
304
+ tone: myTone,
305
+ isRoot: false,
306
+ ...((node as { unresolved?: true }).unresolved ? { unresolved: true as const } : {}),
307
+ });
285
308
  nodePos.set(node.id, { x, y });
286
309
  nodeSide.set(node.id, isLeft ? "left" : "right");
287
310
  nodeHalfW.set(node.id, w / 2);
@@ -1,6 +1,7 @@
1
1
  import type { JSX } from "react";
2
2
  import type { LaidNode, TreeNodePayload } from "../types";
3
3
  import { TONE } from "../render/tone";
4
+ import { resolveTreeData } from "../render/payload-binding";
4
5
 
5
6
  /**
6
7
  * tree-hierarchy kind ... 1 node に tree node 配列 (treeData) を保持し、
@@ -11,10 +12,19 @@ import { TONE } from "../render/tone";
11
12
  * - depth 1+ は tone 別 accent の left-border + 白 fill card
12
13
  * - parent-child edge は elbow line + 中間 join dot で「分岐点」 を強調
13
14
  */
14
- export function TreeNode({ node, active }: { node: LaidNode; active: boolean }): JSX.Element {
15
+ export function TreeNode({
16
+ node,
17
+ active,
18
+ stateValues = {},
19
+ }: {
20
+ node: LaidNode;
21
+ active: boolean;
22
+ stateValues?: Record<string, string>;
23
+ }): JSX.Element {
15
24
  const x0 = node.cx - node.w / 2;
16
25
  const y0 = node.cy - node.h / 2;
17
- const treeNodes = node.treeData ?? [];
26
+ // 名前は状態を読める (#467)。 解決できない名前は書いたまま残して印を付ける
27
+ const treeNodes = resolveTreeData(node.treeData ?? [], stateValues);
18
28
  const gradientId = `tree-root-grad-${node.id}`;
19
29
 
20
30
  const PAD_TOP = 40;
@@ -79,7 +89,11 @@ export function TreeNode({ node, active }: { node: LaidNode; active: boolean }):
79
89
  const accent = depthColors[Math.min(n.depth, depthColors.length - 1)] ?? depthColors[0]!;
80
90
  const isRoot = n.depth === 0;
81
91
  return (
82
- <g key={`n-${n.id}`} transform={`translate(${PAD_LEFT + n.x - n.w / 2} ${PAD_TOP + n.y - n.h / 2})`}>
92
+ <g
93
+ key={`n-${n.id}`}
94
+ data-cdl-unresolved={n.unresolved ? "true" : undefined}
95
+ transform={`translate(${PAD_LEFT + n.x - n.w / 2} ${PAD_TOP + n.y - n.h / 2})`}
96
+ >
83
97
  {isRoot ? (
84
98
  <>
85
99
  <rect
@@ -143,6 +157,8 @@ type LayoutNode = {
143
157
  w: number;
144
158
  h: number;
145
159
  depth: number;
160
+ /** 名前を状態から解決できなかった項目に付く印 (#467) */
161
+ unresolved?: true;
146
162
  };
147
163
 
148
164
  export function computeTreeLayout(
@@ -231,7 +247,16 @@ export function computeTreeLayout(
231
247
  }
232
248
  const y = depth * depthGap + depthGap / 2;
233
249
  posOf.set(n.id, { x, y });
234
- nodes.push({ id: n.id, title: n.title, x, y, w: nodeW, h: nodeH, depth });
250
+ nodes.push({
251
+ id: n.id,
252
+ title: n.title,
253
+ x,
254
+ y,
255
+ w: nodeW,
256
+ h: nodeH,
257
+ depth,
258
+ ...((n as { unresolved?: true }).unresolved ? { unresolved: true as const } : {}),
259
+ });
235
260
  return x;
236
261
  };
237
262
  for (const r of roots) if (!placed.has(r.id)) place(r, 0);
@@ -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
- // 図表 7 種は payload の数と語が `{signal}` を取れるので stateValues を渡す
153
- // (`render/payload-binding.ts` が描画直前に解決する)。 動かさない 2 種
154
- // (mind-map / tree-hierarchy) は構造だけを持つため渡さない。
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
- * 動かさない 2 種 (`mind-map` / `tree-hierarchy`) はここに resolver を持たない。
28
- * 構造だけを持ち動かせる数が無く、 動かすとしたら項目の増減になるため (Issue の実装しない条件)。
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,55 @@ 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
+ export function resolveTreeData(
231
+ nodes: TreeNodePayload[],
232
+ values: Record<string, string>,
233
+ ): ResolvedTreeNode[] {
234
+ if (!nodes.some((n) => n.title.includes("{"))) return nodes as ResolvedTreeNode[];
235
+ return nodes.map((n) => {
236
+ const title = resolveBoundText(n.title, values);
237
+ const 解決後: ResolvedTreeNode = { ...n, title: title.value };
238
+ return title.ok ? 解決後 : { ...解決後, unresolved: true as const };
239
+ });
240
+ }
241
+
242
+ /** 放射の名前を解決する。 中心の名前と枝の名前の両方を見る */
243
+ export function resolveMindData(
244
+ data: MindBranchPayload,
245
+ values: Record<string, string>,
246
+ ): ResolvedMindData {
247
+ const 読む = data.rootTitle.includes("{") || data.branches.some((b) => b.title.includes("{"));
248
+ if (!読む) return data as ResolvedMindData;
249
+
250
+ const root = resolveBoundText(data.rootTitle, values);
251
+ const branches = data.branches.map((b) => {
252
+ const title = resolveBoundText(b.title, values);
253
+ const 解決後: ResolvedMindBranch = { ...b, title: title.value };
254
+ return title.ok ? 解決後 : { ...解決後, unresolved: true as const };
255
+ });
256
+ const 解決後: ResolvedMindData = { ...data, rootTitle: root.value, branches };
257
+ return root.ok ? 解決後 : { ...解決後, unresolved: true as const };
258
+ }