@cardenelabs/cdl 0.55.0 → 0.57.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.55.0",
3
+ "version": "0.57.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,7 +1,8 @@
1
1
  import type { JSX } from "react";
2
2
  import type { LaidNode } from "../types";
3
3
  import { nodeAccent } from "./node-tone";
4
- import { fitTextSize, isCompactBox } from "./compact-title";
4
+ import { ASCENT, DESCENT, fitTextSize, isCompactBox } from "./compact-title";
5
+ import { SUBTITLE_SIZE, 行送りの倍率, 説明文を割り付ける } from "./subtitle-lines";
5
6
  import { 余りを分ける, 目印なしの繰り上げ } from "../layout/spec";
6
7
  import { NODE_SIZE } from "../layout/tokens";
7
8
 
@@ -37,7 +38,19 @@ export function CardNode({ node, active }: { node: LaidNode; active: boolean }):
37
38
  const initialTitleSize = compact ? 20 : 24;
38
39
  const titleSize = fitTextSize(node.title, availableW, initialTitleSize, 11);
39
40
  const textAnchor = compact ? ("middle" as const) : ("start" as const);
40
- const subtitleSize = node.subtitle ? fitTextSize(node.subtitle, availableW, 21, 11) : 21;
41
+ const subtitleY = 124 - ずらし;
42
+ // 2 行を置けるのは、題名の下端に触れず箱の下端も越えない時だけ (#843)。
43
+ // 判定は最大の大きさ (21) で行う = 実際はこれ以下に収まるので、入ると判定した箱には必ず入る
44
+ const 最大の行送り = SUBTITLE_SIZE * 行送りの倍率;
45
+ const 二行が入る =
46
+ !compact &&
47
+ subtitleY - 最大の行送り / 2 - SUBTITLE_SIZE * ASCENT > titleY + titleSize * DESCENT &&
48
+ subtitleY + 最大の行送り / 2 + SUBTITLE_SIZE * DESCENT <= node.h - 4;
49
+ const 説明 = node.subtitle
50
+ ? 説明文を割り付ける(node.subtitle, availableW, { 二行まで: 二行が入る })
51
+ : undefined;
52
+ const subtitleSize = 説明?.size ?? 21;
53
+ const 行送り = subtitleSize * 行送りの倍率;
41
54
  return (
42
55
  <g transform={`translate(${x} ${y})`}>
43
56
  <rect
@@ -67,11 +80,18 @@ export function CardNode({ node, active }: { node: LaidNode; active: boolean }):
67
80
  >
68
81
  {node.title}
69
82
  </text>
70
- {!compact && node.subtitle && (
71
- <text x={26} y={124 - ずらし} fontSize={subtitleSize} fill="var(--cdl-text-dim, #5a6270)">
72
- {node.subtitle}
73
- </text>
74
- )}
83
+ {!compact &&
84
+ 説明?.lines.map((行, i) => (
85
+ <text
86
+ key={`sub-${i}`}
87
+ x={26}
88
+ y={subtitleY + (i - (説明.lines.length - 1) / 2) * 行送り}
89
+ fontSize={subtitleSize}
90
+ fill="var(--cdl-text-dim, #5a6270)"
91
+ >
92
+ {行}
93
+ </text>
94
+ ))}
75
95
  {小型に呼び名 && (
76
96
  <text
77
97
  data-cdl-role="node-subtitle"
@@ -63,8 +63,8 @@ export const MIN_TEXT_SIZE = 11;
63
63
  * 収まるかを決めるのは baseline ではなく **字の上端と下端** なので、 中央に置く時も収まるかを
64
64
  * 見る時もこの 2 つで測る。
65
65
  */
66
- const ASCENT = 0.72;
67
- const DESCENT = 0.25;
66
+ export const ASCENT = 0.72;
67
+ export const DESCENT = 0.25;
68
68
 
69
69
  /**
70
70
  * 名前が収まる最小の帯の高さ。 これより低い帯では **収めきれない**。
@@ -0,0 +1,99 @@
1
+ /**
2
+ * 箱の説明文を、読める大きさのまま何行に割るか (#843)。
3
+ *
4
+ * 説明文は幅に入らないと字を小さくして 1 行に収めていた。 下限 (11) まで縮むと、同じ図の中で
5
+ * 同じ役割の字が 2 倍近く違って見える (実測 = 待機 の説明文が 19、読み込み が 11)。
6
+ *
7
+ * 幅を広げる案は採らない。 状態を横に並べる図では図全体が横長になり、器に収めた時に
8
+ * **題名の方** が小さくなる (#357 で幅を絞った経緯)。 高さも増やさない = 説明文の下には既に
9
+ * 余白があり、題名との間と合わせて 2 行が入る。
10
+ *
11
+ * 折る位置は「読み手が区切りとして読む所」 を優先する。 `入る時: A / 出る時: B` のように
12
+ * 2 つの事柄を並べた説明文は ` / ` が区切りで、そこで折ると 1 行が 1 つの事柄になる。
13
+ */
14
+ import { estimateTextWidth, fitTextSize, MIN_TEXT_SIZE } from "./compact-title";
15
+
16
+ /** 説明文の既定の大きさ。 `card` が使ってきた値 */
17
+ export const SUBTITLE_SIZE = 21;
18
+
19
+ /**
20
+ * 読みやすさの下限。 これを下回る大きさで描くくらいなら行を増やす。
21
+ *
22
+ * 走査の `text-readability` が題名に使う下限と同じ値。
23
+ */
24
+ export const SUBTITLE_MIN_SIZE = 12;
25
+
26
+ /** 行を重ねる間隔 (字の大きさに対する倍率) */
27
+ export const 行送りの倍率 = 1.2;
28
+
29
+ export type 説明の割り付け = { size: number; lines: string[] };
30
+
31
+ /**
32
+ * 説明文を 1 行か 2 行に割り付ける。
33
+ *
34
+ * 2 行にするのは、1 行では読みやすさの下限を割る時だけ。 `二行まで` が false の時 (2 行を置く
35
+ * 余白が無い箱) は今までどおり下限 11 まで縮めて 1 行で描く。
36
+ */
37
+ export function 説明文を割り付ける(
38
+ text: string,
39
+ maxWidth: number,
40
+ opts: { 二行まで: boolean; targetSize?: number },
41
+ ): 説明の割り付け {
42
+ const 目標 = opts.targetSize ?? SUBTITLE_SIZE;
43
+ const 読める大きさ = fitTextSize(text, maxWidth, 目標, SUBTITLE_MIN_SIZE);
44
+ if (estimateTextWidth(text, 読める大きさ) <= maxWidth) return { size: 読める大きさ, lines: [text] };
45
+
46
+ if (opts.二行まで) {
47
+ for (let size = 目標; size >= SUBTITLE_MIN_SIZE; size -= 1) {
48
+ const 行 = 二行に割る(text, maxWidth, size);
49
+ if (行) return { size, lines: 行 };
50
+ }
51
+ }
52
+ // 2 行でも入らない / 2 行を置けない箱は、今までどおり縮めて 1 行で描く
53
+ return { size: fitTextSize(text, maxWidth, 目標, MIN_TEXT_SIZE), lines: [text] };
54
+ }
55
+
56
+ /** 両方の行が幅に入る割り方。 無ければ undefined */
57
+ function 二行に割る(text: string, maxWidth: number, size: number): [string, string] | undefined {
58
+ let 一番良い: [string, string] | undefined;
59
+ let 一番狭い = Infinity;
60
+ for (const [左, 右] of 割る候補(text)) {
61
+ const 幅 = Math.max(estimateTextWidth(左, size), estimateTextWidth(右, size));
62
+ if (幅 > maxWidth || 幅 >= 一番狭い) continue;
63
+ 一番狭い = 幅;
64
+ 一番良い = [左, 右];
65
+ }
66
+ return 一番良い;
67
+ }
68
+
69
+ /**
70
+ * 割る位置の候補。 区切りの語 → 空白 → 字の境目 の順に探す。
71
+ *
72
+ * 区切りと空白で割る時はその字が改行を担うので落とす。 字の境目で割る時は 1 字も落とさない
73
+ * (日本語は空白を持たないため、この形で割ることになる)。
74
+ */
75
+ function 割る候補(text: string): Array<[string, string]> {
76
+ const 区切り: Array<[string, string]> = [];
77
+ for (const m of text.matchAll(/\s*\/\s*/g)) {
78
+ 区切り.push([text.slice(0, m.index).trim(), text.slice(m.index! + m[0].length).trim()]);
79
+ }
80
+ if (区切り.length > 0) return 区切り.filter(入っている);
81
+
82
+ const 空白: Array<[string, string]> = [];
83
+ for (const m of text.matchAll(/\s+/g)) {
84
+ 空白.push([text.slice(0, m.index).trim(), text.slice(m.index! + m[0].length).trim()]);
85
+ }
86
+ if (空白.length > 0) return 空白.filter(入っている);
87
+
88
+ const 字: Array<[string, string]> = [];
89
+ const 並び = [...text];
90
+ for (let i = 1; i < 並び.length; i += 1) {
91
+ 字.push([並び.slice(0, i).join(""), 並び.slice(i).join("")]);
92
+ }
93
+ return 字.filter(入っている);
94
+ }
95
+
96
+ /** どちらの行も字を持つ割り方だけを使う */
97
+ function 入っている([左, 右]: [string, string]): boolean {
98
+ return 左 !== "" && 右 !== "";
99
+ }
@@ -66,6 +66,14 @@ export function UserJourneyNode({
66
66
  frustrated: "😕",
67
67
  angry: "😠",
68
68
  };
69
+ // 行に描く気持ちの名前 (#841)。 型の値は書き手が書く値なので変えず、描く字だけを引き替える
70
+ const emotionLabel: Record<JourneyEmotion, string> = {
71
+ delighted: "大満足",
72
+ happy: "満足",
73
+ neutral: "ふつう",
74
+ frustrated: "不満",
75
+ angry: "怒り",
76
+ };
69
77
 
70
78
  const rowH = canvasH / 5;
71
79
  // step を列の中央に置く。 端に寄せると、 step の下に置く touchpoint chip が枠からはみ出す。
@@ -142,7 +150,7 @@ export function UserJourneyNode({
142
150
  textAnchor="end"
143
151
  fill={emotionColor[e]}
144
152
  >
145
- {e}
153
+ {emotionLabel[e]}
146
154
  </text>
147
155
  </g>
148
156
  ))}