@cardenelabs/cdl 0.12.1 → 0.13.1

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.
@@ -293,6 +293,14 @@ type GanttTaskPayload = {
293
293
  type MindBranchPayload = {
294
294
  rootId: string;
295
295
  rootTitle: string;
296
+ /**
297
+ * 中心の箱の 2 行目に出す補足 (#530)。
298
+ *
299
+ * 枝 (`MindBranchNode`) は元から `subtitle` を持つのに中心だけ渡す口が無く、呼出側が
300
+ * 名前と補足を連結して `rootTitle` に入れていた。 1 行に全部入るため箱幅を超え、
301
+ * 末尾が切られる (実測 = 箱 120px に対し文字 163px)。
302
+ */
303
+ rootSubtitle?: string;
296
304
  branches: MindBranchNode[];
297
305
  };
298
306
  /** 1 branch node = id + title + parent + optional tone + subtitle */
@@ -293,6 +293,14 @@ type GanttTaskPayload = {
293
293
  type MindBranchPayload = {
294
294
  rootId: string;
295
295
  rootTitle: string;
296
+ /**
297
+ * 中心の箱の 2 行目に出す補足 (#530)。
298
+ *
299
+ * 枝 (`MindBranchNode`) は元から `subtitle` を持つのに中心だけ渡す口が無く、呼出側が
300
+ * 名前と補足を連結して `rootTitle` に入れていた。 1 行に全部入るため箱幅を超え、
301
+ * 末尾が切られる (実測 = 箱 120px に対し文字 163px)。
302
+ */
303
+ rootSubtitle?: string;
296
304
  branches: MindBranchNode[];
297
305
  };
298
306
  /** 1 branch node = id + title + parent + optional tone + subtitle */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
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,4 +1,5 @@
1
1
  import { textWidth } from "./text-width";
2
+ import { MIN_TEXT_SIZE } from "./compact-title";
2
3
 
3
4
  /**
4
5
  * 箱の中に文字の行を積む (#471)。
@@ -84,6 +85,40 @@ export function 幅で切る(
84
85
  return `${出}${省略}`;
85
86
  }
86
87
 
88
+ /**
89
+ * 字を小さくする下限。
90
+ *
91
+ * `compact-title.ts` の `MIN_TEXT_SIZE` を共有する。 小型の箱の名前が既にこの値まで縮んでおり、
92
+ * 読める下限として使われている。 同じ値を参照すれば、同じ図の中で名前の下限が場所によって
93
+ * 変わらない。
94
+ */
95
+ export const 縮めの下限 = MIN_TEXT_SIZE;
96
+
97
+ /**
98
+ * 幅に入るまで字を小さくし、下限まで縮めても入らなければ切る (#534)。
99
+ *
100
+ * **切るのは最後の手段にする**。 字の大きさを保ったまま切ると、全角 7 文字の名前
101
+ * (`新プロジェクト`) が `新プロジェ…` になって何の箱か読めない。 1 段小さくすれば
102
+ * 入る名前を切っていた。
103
+ *
104
+ * 縮めるのは **入るところまで** で、一律に下限へ落とさない。 落とすと 1 段で足りる名前まで
105
+ * 最小になり、同じ図の中で名前の大きさがばらつく。
106
+ *
107
+ * 元から下限以下の字は縮めない (補足の 9px がここに当たる)。 巻き込むと補足が読めなくなる。
108
+ *
109
+ * 箱の大きさは変えない。 高さの都合は呼出側 (`箱に積む`) が見る。
110
+ */
111
+ export function 幅に収める(
112
+ text: string,
113
+ fontSize: number,
114
+ 使える幅: number,
115
+ 測る: (s: string, size: number) => number = textWidth,
116
+ ): { text: string; fontSize: number } {
117
+ let size = fontSize;
118
+ while (size > 縮めの下限 && 測る(text, size) > 使える幅) size -= 1;
119
+ return { text: 幅で切る(text, size, 使える幅, 測る), fontSize: size };
120
+ }
121
+
87
122
  /**
88
123
  * 見た目の 1 字 (書記素クラスタ) に分ける。
89
124
  *
@@ -115,16 +150,22 @@ export function 箱に積む(行: 積む行[], 箱の幅: number, 箱の高さ:
115
150
  const 使える高さ = 箱の高さ - 上下の余白 * 2;
116
151
  const 高さ = (rs: 積む行[]): number => rs.reduce((a, r) => a + r.fontSize * 行送りの比, 0);
117
152
 
118
- // 入るまで後ろから落とす。 先頭は落とさない
153
+ // 入るまで後ろから落とす。 先頭は落とさない。
154
+ // **判定は縮める前の大きさで行う** (#534)。 縮めた後で決めると、名前が長い箱でだけ
155
+ // 補足が復活する = 同じ高さの箱でも中身の長さで行数が変わり、挙動が読みにくくなる
119
156
  const 残す = [...候補];
120
157
  while (残す.length > 1 && 高さ(残す) > 使える高さ) 残す.pop();
121
158
 
122
159
  const 幅 = 使える幅(箱の幅);
123
- const 総高 = 高さ(残す);
160
+ // **幅に収めるのを先に済ませる** (#534)。 縮めると行の高さも変わるため、後から縮めると
161
+ // `dy` が縮める前の大きさで出て、行の塊が箱の上下の中央からずれる
162
+ const 収めた = 残す.map((r) => ({ ...r, ...幅に収める(r.text, r.fontSize, 幅) }));
163
+
164
+ const 総高 = 高さ(収めた);
124
165
  let 積み = -総高 / 2;
125
- return 残す.map((r) => {
166
+ return 収めた.map((r) => {
126
167
  const dy = 積み + ((行送りの比 + 字の高さの比) / 2) * r.fontSize;
127
168
  積み += r.fontSize * 行送りの比;
128
- return { ...r, text: 幅で切る(r.text, r.fontSize, 幅), dy };
169
+ return { ...r, dy };
129
170
  });
130
171
  }
@@ -54,8 +54,8 @@ export const COMPACT_MAX_H = 100;
54
54
  /** 小型で名前の左右に空ける幅の合計。 `card` の compact と同じ。 */
55
55
  const COMPACT_PADDING_X = 16;
56
56
 
57
- /** 小型で名前を縮める下限。 `card` と同じ。 */
58
- const MIN_SIZE = 11;
57
+ /** 小型で名前を縮める共通の下限。 `card` tree / mind-map で同じ値を使う。 */
58
+ export const MIN_TEXT_SIZE = 11;
59
59
 
60
60
  /**
61
61
  * 字が baseline から上下へどれだけ張り出すかの見積り。
@@ -69,10 +69,10 @@ const DESCENT = 0.25;
69
69
  /**
70
70
  * 名前が収まる最小の帯の高さ。 これより低い帯では **収めきれない**。
71
71
  *
72
- * 読める下限 (`MIN_SIZE`) を割ってまで縮めない。 その場合の張り出しは上下へ均等に出す =
72
+ * 読める下限 (`MIN_TEXT_SIZE`) を割ってまで縮めない。 その場合の張り出しは上下へ均等に出す =
73
73
  * 下だけに落ちる形にはしない。
74
74
  */
75
- export const MIN_BAND_H = MIN_SIZE * (ASCENT + DESCENT);
75
+ export const MIN_BAND_H = MIN_TEXT_SIZE * (ASCENT + DESCENT);
76
76
 
77
77
  /** 目印 (`eyebrow`) の baseline と大きさ。 4 種で同じ値を使っている。 */
78
78
  const EYEBROW_BASELINE = 38;
@@ -144,10 +144,10 @@ export function titlePlacement(
144
144
  node.title,
145
145
  Math.max(0, node.w - COMPACT_PADDING_X),
146
146
  通常.size,
147
- MIN_SIZE,
147
+ MIN_TEXT_SIZE,
148
148
  );
149
149
  const 帯で = Math.floor(帯 / (ASCENT + DESCENT));
150
- const size = Math.max(MIN_SIZE, Math.min(幅で, 帯で));
150
+ const size = Math.max(MIN_TEXT_SIZE, Math.min(幅で, 帯で));
151
151
  const 字高 = size * (ASCENT + DESCENT);
152
152
 
153
153
  // 収まるなら帯の中央。 収まらない時の逃がし方は目印の有無で変える
@@ -1,6 +1,6 @@
1
1
  import type { JSX } from "react";
2
- import { 使える幅, 幅で切る, 箱に積む } from "./box-lines";
3
- import type { LaidNode, Tone, MindBranchNode } from "../types";
2
+ import { 使える幅, 幅に収める, 箱に積む } from "./box-lines";
3
+ import type { LaidNode, Tone, MindBranchNode, MindBranchPayload } from "../types";
4
4
  import { TONE } from "../render/tone";
5
5
  import { resolveMindData } from "../render/payload-binding";
6
6
  import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
@@ -115,6 +115,10 @@ export function MindMapNode({
115
115
  );
116
116
  // 判定は行数でなく「補足を渡したか」 で行う (理由は tree.tsx と同じ、 #471 Round 1)
117
117
  const 補足あり = (n.subtitle ?? "").trim() !== "";
118
+ // 補足ありでは `箱に積む` が既に同じ処理を済ませているため、1 行の経路だけ計算する
119
+ const 収めた名前 = 補足あり
120
+ ? undefined
121
+ : 幅に収める(n.title, n.isRoot ? 15 : 13, 使える幅(n.w));
118
122
  const 文字の色 = n.isRoot
119
123
  ? "var(--cdl-text-on-tone, #ffffff)"
120
124
  : "var(--cdl-chip-text, #1a1f2a)";
@@ -130,7 +134,7 @@ export function MindMapNode({
130
134
  stroke={n.isRoot ? toneColor(n.tone ?? "accent") : toneColor(n.tone)}
131
135
  strokeWidth={n.isRoot ? 2 : 1.75}
132
136
  />
133
- {補足あり ? (
137
+ {収めた名前 === undefined ? (
134
138
  行.map((r) => (
135
139
  <text
136
140
  key={r.役割}
@@ -149,16 +153,17 @@ export function MindMapNode({
149
153
  ) : (
150
154
  // 補足が無くても **幅に収める** (#526)。 `箱に積む` を通らないこの経路だけが
151
155
  // 幅を見ておらず、長い名前がそのまま箱の外へ出て枝の線と重なっていた
152
- // (実測 = 中心の箱で文字 163px に対し箱 120px)
156
+ // (実測 = 中心の箱で文字 163px に対し箱 120px)
157
+ // 収める手段は縮めるのが先で、切るのは下限まで縮めても入らない時だけ (#534)
153
158
  <text
154
159
  x={n.x}
155
160
  y={n.y + 5}
156
- fontSize={n.isRoot ? 15 : 13}
161
+ fontSize={収めた名前.fontSize}
157
162
  fontWeight={n.isRoot ? 700 : 600}
158
163
  textAnchor="middle"
159
164
  fill={文字の色}
160
165
  >
161
- {幅で切る(n.title, n.isRoot ? 15 : 13, 使える幅(n.w))}
166
+ {収めた名前.text}
162
167
  </text>
163
168
  )}
164
169
  </g>
@@ -195,7 +200,7 @@ type LayoutEdge = {
195
200
  };
196
201
 
197
202
  export function computeMindMapLayout(
198
- mindData: { rootId: string; rootTitle: string; branches: MindBranchNode[] },
203
+ mindData: MindBranchPayload,
199
204
  cx: number,
200
205
  cy: number,
201
206
  canvasW: number,
@@ -209,6 +214,8 @@ export function computeMindMapLayout(
209
214
  nodes.push({
210
215
  id: mindData.rootId,
211
216
  title: mindData.rootTitle,
217
+ // 中心も枝と同じく 2 行に積める (#530)。 高さ 52 に 15px + 9px の 2 行が入る
218
+ ...(mindData.rootSubtitle === undefined ? {} : { subtitle: mindData.rootSubtitle }),
212
219
  x: cx,
213
220
  y: cy,
214
221
  w: rootW,
@@ -2,7 +2,7 @@ import type { JSX } from "react";
2
2
  import type { LaidNode, TreeNodePayload } from "../types";
3
3
  import { TONE } from "../render/tone";
4
4
  import { resolveTreeData } from "../render/payload-binding";
5
- import { 使える幅, 幅で切る, 箱に積む, type 積んだ行 } from "../kinds/box-lines";
5
+ import { 使える幅, 幅に収める, 箱に積む, type 積んだ行 } from "../kinds/box-lines";
6
6
  import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
7
7
 
8
8
  /**
@@ -164,6 +164,11 @@ export function TreeNode({
164
164
  // **判定は行数でなく「補足を渡したか」 で行う**。 行数で見ると、 箱が低くて補足が落ちた時に
165
165
  // 従来の 1 行の経路へ戻り、 幅で切っていない名前が箱から横にはみ出す (#471 Round 1)
166
166
  const 補足あり = (n.subtitle ?? "").trim() !== "";
167
+ // 補足ありでは `箱に積む` が既に同じ処理を済ませているため、1 行の経路だけ計算する。
168
+ // 収める手段は縮めるのが先で、切るのは下限まで縮めても入らない時だけ (#534)
169
+ const 収めた名前 = 補足あり
170
+ ? undefined
171
+ : 幅に収める(n.title, isRoot ? 13 : 12, 使える幅(n.w));
167
172
  return (
168
173
  <g
169
174
  key={`n-${n.id}`}
@@ -181,7 +186,7 @@ export function TreeNode({
181
186
  rx={12}
182
187
  fill={`url(#${gradientId})`}
183
188
  />
184
- {補足あり ? (
189
+ {収めた名前 === undefined ? (
185
190
  箱の文字({
186
191
  行,
187
192
  x: n.w / 2,
@@ -195,12 +200,12 @@ export function TreeNode({
195
200
  <text
196
201
  x={n.w / 2}
197
202
  y={n.h / 2 + 5}
198
- fontSize={13}
203
+ fontSize={収めた名前.fontSize}
199
204
  fontWeight={700}
200
205
  textAnchor="middle"
201
206
  fill="var(--cdl-text-on-tone, #ffffff)"
202
207
  >
203
- {幅で切る(n.title, 13, 使える幅(n.w))}
208
+ {収めた名前.text}
204
209
  </text>
205
210
  )}
206
211
  </>
@@ -218,7 +223,7 @@ export function TreeNode({
218
223
  strokeWidth={1.5}
219
224
  />
220
225
  <rect x={0} y={0} width={4} height={n.h} rx={2} fill={accent} />
221
- {補足あり ? (
226
+ {収めた名前 === undefined ? (
222
227
  箱の文字({
223
228
  行,
224
229
  x: n.w / 2 + 2,
@@ -230,12 +235,12 @@ export function TreeNode({
230
235
  <text
231
236
  x={n.w / 2 + 2}
232
237
  y={n.h / 2 + 4}
233
- fontSize={12}
238
+ fontSize={収めた名前.fontSize}
234
239
  fontWeight={600}
235
240
  textAnchor="middle"
236
241
  fill="var(--cdl-chip-text, #1a1f2a)"
237
242
  >
238
- {幅で切る(n.title, 12, 使える幅(n.w))}
243
+ {収めた名前.text}
239
244
  </text>
240
245
  )}
241
246
  </>
package/src/presets.ts CHANGED
@@ -1292,6 +1292,8 @@ export type MindMapPreset = {
1292
1292
  /** 中心 topic ID + label */
1293
1293
  rootId: string;
1294
1294
  rootTitle: string;
1295
+ /** 中心の箱の 2 行目に出す補足 (#530)。 枝の `subtitle` と同じ扱い */
1296
+ rootSubtitle?: string;
1295
1297
  branchWidth?: number;
1296
1298
  defaultTone?: Tone;
1297
1299
  };
@@ -1338,6 +1340,7 @@ export function mindMap(preset: MindMapPreset): MindMapBuilder {
1338
1340
  mindData: {
1339
1341
  rootId: preset.rootId,
1340
1342
  rootTitle: preset.rootTitle,
1343
+ ...(preset.rootSubtitle === undefined ? {} : { rootSubtitle: preset.rootSubtitle }),
1341
1344
  branches: branches.map((br) => ({
1342
1345
  id: br.id,
1343
1346
  title: br.title,
@@ -252,9 +252,9 @@ export function resolveTreeData(
252
252
  }
253
253
 
254
254
  /**
255
- * 放射の名前と補足を解決する。 中心の名前と枝の名前 / 補足を見る。
255
+ * 放射の名前と補足を解決する。 中心と枝の名前 / 補足を見る。
256
256
  *
257
- * 補足を入れた理由は `resolveTreeData` と同じ (#471)。 中心は補足の欄を持たない。
257
+ * 補足を入れた理由は `resolveTreeData` と同じ (#471 / #530)。
258
258
  */
259
259
  export function resolveMindData(
260
260
  data: MindBranchPayload,
@@ -262,10 +262,13 @@ export function resolveMindData(
262
262
  ): ResolvedMindData {
263
263
  const 読む =
264
264
  data.rootTitle.includes("{") ||
265
+ (data.rootSubtitle?.includes("{") ?? false) ||
265
266
  data.branches.some((b) => b.title.includes("{") || (b.subtitle?.includes("{") ?? false));
266
267
  if (!読む) return data as ResolvedMindData;
267
268
 
268
269
  const root = resolveBoundText(data.rootTitle, values);
270
+ const rootSubtitle =
271
+ data.rootSubtitle === undefined ? undefined : resolveBoundText(data.rootSubtitle, values);
269
272
  const branches = data.branches.map((b) => {
270
273
  const title = resolveBoundText(b.title, values);
271
274
  const subtitle = b.subtitle === undefined ? undefined : resolveBoundText(b.subtitle, values);
@@ -276,6 +279,13 @@ export function resolveMindData(
276
279
  };
277
280
  return title.ok && (subtitle?.ok ?? true) ? 解決後 : { ...解決後, unresolved: true as const };
278
281
  });
279
- const 解決後: ResolvedMindData = { ...data, rootTitle: root.value, branches };
280
- return root.ok ? 解決後 : { ...解決後, unresolved: true as const };
282
+ const 解決後: ResolvedMindData = {
283
+ ...data,
284
+ rootTitle: root.value,
285
+ ...(rootSubtitle ? { rootSubtitle: rootSubtitle.value } : {}),
286
+ branches,
287
+ };
288
+ return root.ok && (rootSubtitle?.ok ?? true)
289
+ ? 解決後
290
+ : { ...解決後, unresolved: true as const };
281
291
  }
@@ -179,6 +179,7 @@ function payloadTexts(node: CdlNode): string[] {
179
179
  }
180
180
  if (node.mindData) {
181
181
  足す(node.mindData.rootTitle);
182
+ 足す(node.mindData.rootSubtitle);
182
183
  for (const b of node.mindData.branches) {
183
184
  足す(b.title);
184
185
  足す(b.subtitle);
package/src/types.ts CHANGED
@@ -359,6 +359,14 @@ export type GanttTaskPayload = {
359
359
  export type MindBranchPayload = {
360
360
  rootId: string;
361
361
  rootTitle: string;
362
+ /**
363
+ * 中心の箱の 2 行目に出す補足 (#530)。
364
+ *
365
+ * 枝 (`MindBranchNode`) は元から `subtitle` を持つのに中心だけ渡す口が無く、呼出側が
366
+ * 名前と補足を連結して `rootTitle` に入れていた。 1 行に全部入るため箱幅を超え、
367
+ * 末尾が切られる (実測 = 箱 120px に対し文字 163px)。
368
+ */
369
+ rootSubtitle?: string;
362
370
  branches: MindBranchNode[];
363
371
  };
364
372