@cardenelabs/cdl 0.59.1 → 0.61.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.
@@ -4,7 +4,9 @@ import { CDL_TEXT, resolveLang, 字を差し込む, type CdlLang, type CdlTextKe
4
4
  import { assertNever } from "../assert-never";
5
5
  import type { CdlDiagram, CdlInput, CdlReadout } from "../types";
6
6
  import { createInputSignals, inputDefaultValue } from "../input-signals";
7
+ import { 選択肢の値, 選択肢の名前 } from "../input-option";
7
8
  import { createFormulaComputeds } from "../formula";
9
+ import { 式を名札で読む } from "../formula/parser";
8
10
  import {
9
11
  createScrollProgressSignals,
10
12
  type ScrollProgressHandle,
@@ -643,6 +645,10 @@ export function useInteractiveIntegration(
643
645
  readouts.length === 0
644
646
  )
645
647
  return null;
648
+ // 式の中身の名前を読む名札 (#853)。 名札を持つ入力欄と式だけを載せる。
649
+ // 巻き取りは載せない = 式は入力欄と前の式しか参照できず (`createFormulaComputeds` が他の名前を拒む)、巻き取りの名前は中身に出ない
650
+ const 名札 = new Map<string, string>();
651
+ for (const x of [...inputs, ...formulasList]) if (x.label) 名札.set(x.id, x.label);
646
652
  return (
647
653
  // 操作部の中の読み出しと入力欄へ、図の言語を届ける (#849)。 型の外の値は英語に落ちる
648
654
  <操作部の言語.Provider value={resolveLang(diagram.lang)}>
@@ -660,7 +666,9 @@ export function useInteractiveIntegration(
660
666
  <FormulaReadout
661
667
  key={f.id}
662
668
  id={f.id}
663
- expression={f.expression}
669
+ name={f.label || f.id}
670
+ // 名札の無い式は中身も書いたまま描く。 入力欄の名札は多くの図にあるので、置き換えると名札を付けていない式の描き方まで変わる
671
+ expression={f.label ? 式を名札で読む(f.expression, 名札) : f.expression}
664
672
  value={stateOverrides[f.id]}
665
673
  />
666
674
  ))}
@@ -669,7 +677,7 @@ export function useInteractiveIntegration(
669
677
  {scrolls.length > 0 && (
670
678
  <div className="cdl-ip-group">
671
679
  {scrolls.map((s) => (
672
- <ScrollReadout key={s.id} id={s.id} progress={stateOverrides[s.id]} />
680
+ <ScrollReadout key={s.id} id={s.id} name={s.label || s.id} progress={stateOverrides[s.id]} />
673
681
  ))}
674
682
  </div>
675
683
  )}
@@ -774,9 +782,10 @@ function InputWidget({
774
782
  data-cdl-input={input.id}
775
783
  className="cdl-ip-dropdown-select"
776
784
  >
785
+ {/* 値は綴りのまま入力欄に入れ、字だけを選択肢の名前で描く (#855) */}
777
786
  {input.options.map((opt) => (
778
- <option key={opt} value={opt}>
779
- {opt}
787
+ <option key={選択肢の値(opt)} value={選択肢の値(opt)}>
788
+ {選択肢の名前(opt)}
780
789
  </option>
781
790
  ))}
782
791
  </select>
@@ -792,19 +801,19 @@ function InputWidget({
792
801
  <legend className="cdl-ip-label">{label}</legend>
793
802
  <div className="cdl-ip-radio-options">
794
803
  {input.options.map((opt) => (
795
- <label key={opt} className="cdl-ip-radio-option">
804
+ <label key={選択肢の値(opt)} className="cdl-ip-radio-option">
796
805
  <input
797
806
  type="radio"
798
807
  name={input.id}
799
- value={opt}
800
- checked={value === opt}
808
+ value={選択肢の値(opt)}
809
+ checked={value === 選択肢の値(opt)}
801
810
  onChange={() => {
802
- signal.value = opt;
811
+ signal.value = 選択肢の値(opt);
803
812
  }}
804
813
  data-cdl-input={input.id}
805
814
  className="cdl-ip-radio-input"
806
815
  />
807
- <span className="cdl-ip-radio-label">{opt}</span>
816
+ <span className="cdl-ip-radio-label">{選択肢の名前(opt)}</span>
808
817
  </label>
809
818
  ))}
810
819
  </div>
@@ -857,16 +866,16 @@ function InputWidget({
857
866
  <div className="cdl-ip-tabs-bar" role="tablist">
858
867
  {input.options.map((opt) => (
859
868
  <button
860
- key={opt}
869
+ key={選択肢の値(opt)}
861
870
  type="button"
862
871
  role="tab"
863
- className={`cdl-ip-tabs-btn ${value === opt ? "cdl-ip-tabs-active" : ""}`}
872
+ className={`cdl-ip-tabs-btn ${value === 選択肢の値(opt) ? "cdl-ip-tabs-active" : ""}`}
864
873
  onClick={() => {
865
- signal.value = opt;
874
+ signal.value = 選択肢の値(opt);
866
875
  }}
867
876
  data-cdl-input={input.id}
868
877
  >
869
- {opt}
878
+ {選択肢の名前(opt)}
870
879
  </button>
871
880
  ))}
872
881
  </div>
@@ -966,30 +975,36 @@ function NumberInputWidget({
966
975
  );
967
976
  }
968
977
 
978
+ /** 式の行。 `name` は名札か id (#853)、目印 `data-cdl-formula` は名札の有無によらず id */
969
979
  function FormulaReadout({
970
980
  id,
981
+ name,
971
982
  expression,
972
983
  value,
973
984
  }: {
974
985
  id: string;
986
+ name: string;
975
987
  expression: string;
976
988
  value: unknown;
977
989
  }): JSX.Element {
978
990
  return (
979
991
  <div className="cdl-ip-item cdl-ip-formula" data-cdl-formula={id}>
980
- <span className="cdl-ip-label">{id}</span>
992
+ <span className="cdl-ip-label">{name}</span>
981
993
  <code className="cdl-ip-formula-expr">= {expression}</code>
982
994
  <span className="cdl-ip-formula-value">{String(value ?? "")}</span>
983
995
  </div>
984
996
  );
985
997
  }
986
998
 
987
- function ScrollReadout({ id, progress }: { id: string; progress: unknown }): JSX.Element {
999
+ /**
1000
+ * 巻き取りの進みの行。 `name` は名札か id (#853)。 型と組み立ては `label` を持っていたが、この行は id を描いていた
1001
+ */
1002
+ function ScrollReadout({ id, name, progress }: { id: string; name: string; progress: unknown }): JSX.Element {
988
1003
  const p = typeof progress === "number" ? progress : 0;
989
1004
  const pct = Math.round(p * 100);
990
1005
  return (
991
1006
  <div className="cdl-ip-item cdl-ip-scroll" data-cdl-scroll={id}>
992
- <span className="cdl-ip-label">{id}</span>
1007
+ <span className="cdl-ip-label">{name}</span>
993
1008
  <div className="cdl-ip-scroll-track">
994
1009
  <div className="cdl-ip-scroll-fill" style={{ width: `${pct}%` }} />
995
1010
  </div>
@@ -2970,14 +2985,14 @@ function MultiSelectWidget({
2970
2985
  <div className="cdl-ip-multi-select-chips">
2971
2986
  {input.options.map((opt) => (
2972
2987
  <button
2973
- key={opt}
2988
+ key={選択肢の値(opt)}
2974
2989
  type="button"
2975
- className={`cdl-ip-multi-select-chip ${selected.has(opt) ? "cdl-ip-multi-select-active" : ""}`}
2976
- onClick={() => toggle(opt)}
2990
+ className={`cdl-ip-multi-select-chip ${selected.has(選択肢の値(opt)) ? "cdl-ip-multi-select-active" : ""}`}
2991
+ onClick={() => toggle(選択肢の値(opt))}
2977
2992
  data-cdl-input={input.id}
2978
- data-cdl-multi-value={opt}
2993
+ data-cdl-multi-value={選択肢の値(opt)}
2979
2994
  >
2980
- {opt}
2995
+ {選択肢の名前(opt)}
2981
2996
  </button>
2982
2997
  ))}
2983
2998
  </div>
@@ -89,6 +89,7 @@ import {
89
89
  import type { LaidNode } from "../types";
90
90
  import type { CdlLang } from "../lang";
91
91
  import { hasUnresolvedRef, interpolate, resolveNumericAttr } from "./utils";
92
+ import type { 値の名前の表 } from "../input-option";
92
93
 
93
94
  /**
94
95
  * 触れた箱の関係を光らせる時に、箱へ付ける受け口 (#806)。
@@ -117,9 +118,15 @@ export function CdlNodeView({
117
118
  chartLineScale,
118
119
  関係の受け口,
119
120
  lang,
121
+ 値の名前,
120
122
  }: {
121
123
  node: LaidNode;
122
124
  active: boolean;
125
+ /**
126
+ * 入力欄の値ごとの名前 (#855)。 箱の字の差し込みにだけ使い、表示の出し分けと数の差し込みには使わない。
127
+ * 渡さない呼び方は値を綴りのまま差し込む
128
+ */
129
+ 値の名前?: 値の名前の表;
123
130
  /** 描画側の固定の字の言語 (#847)。 字を持つ形の部品へ渡す。 渡さない呼び方は英語で描く */
124
131
  lang?: CdlLang;
125
132
  /**
@@ -134,8 +141,8 @@ export function CdlNodeView({
134
141
  /** 触れた箱の関係を光らせる時の受け口 (#806)。 書かない図では `undefined` */
135
142
  関係の受け口?: 関係の受け口;
136
143
  }): JSX.Element {
137
- const value = node.value ? interpolate(node.value, stateValues) : undefined;
138
- const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues));
144
+ const value = node.value ? interpolate(node.value, stateValues, 値の名前) : undefined;
145
+ const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues, 値の名前));
139
146
  // visual binding = wBind / hBind の template を stateValues で resolve、 layout の
140
147
  // static w/h を fallback として保持。 signal 値変化で node の描画寸法がリアルタイム変化する
141
148
  // (arc-intro 相当の 「slider で bar が伸びる」 UX の foundation)。
@@ -159,9 +166,9 @@ export function CdlNodeView({
159
166
  }
160
167
  const resolvedNode: LaidNode = {
161
168
  ...node,
162
- title: interpolate(node.title, stateValues),
163
- subtitle: node.subtitle ? interpolate(node.subtitle, stateValues) : node.subtitle,
164
- eyebrow: node.eyebrow ? interpolate(node.eyebrow, stateValues) : node.eyebrow,
169
+ title: interpolate(node.title, stateValues, 値の名前),
170
+ subtitle: node.subtitle ? interpolate(node.subtitle, stateValues, 値の名前) : node.subtitle,
171
+ eyebrow: node.eyebrow ? interpolate(node.eyebrow, stateValues, 値の名前) : node.eyebrow,
165
172
  w: boundW,
166
173
  h: boundH,
167
174
  };
@@ -442,7 +449,7 @@ export function CdlNodeView({
442
449
  return <MarkEndNode node={resolvedNode} active={active} />;
443
450
  default:
444
451
  // 新 NodeKind 25 種は GenericNode で汎用描画 (kind 別 icon / color / shape)
445
- return <GenericNode node={resolvedNode} active={active} value={node.value} stateValues={stateValues} />;
452
+ return <GenericNode node={resolvedNode} active={active} value={node.value} stateValues={stateValues} 値の名前={値の名前} />;
446
453
  }
447
454
  })();
448
455
  // node bounding box を node.w/h に固定するための invisible rect。
@@ -26,6 +26,7 @@ import { resolveChartData } from "./payload-binding";
26
26
  import { relatedFocusSet } from "./relation-focus";
27
27
  import { TONE, TONE_HEX } from "./tone";
28
28
  import { computeStateValues } from "./utils";
29
+ import type { 値の名前の表 } from "../input-option";
29
30
 
30
31
  export function CdlStage({
31
32
  laid,
@@ -36,8 +37,11 @@ export function CdlStage({
36
37
  debug,
37
38
  compact = false,
38
39
  svgRef,
40
+ 値の名前,
39
41
  }: {
40
42
  laid: LaidDiagram;
43
+ /** 入力欄の値ごとの名前 (#855)。 箱の字の差し込みへ渡す。 渡さない呼び方は値を綴りのまま差し込む */
44
+ 値の名前?: 値の名前の表;
41
45
  currentPhase: CdlPhase | undefined;
42
46
  progress: number;
43
47
  /**
@@ -432,6 +436,7 @@ export function CdlStage({
432
436
  drawing={描く}
433
437
  progress={描く ? 描く進み : progress}
434
438
  stateValues={stateValues}
439
+ 値の名前={値の名前}
435
440
  currentPhaseId={currentPhase?.id}
436
441
  chartLineScale={chartLineScales.get(node.id)}
437
442
  />
@@ -2,6 +2,7 @@ import { lerp } from "../anim";
2
2
  import type { LaidDiagram } from "../types";
3
3
  import { withDerivedValues } from "./derived-values";
4
4
  import { TEMPLATE_LEFTOVER_SRC, TEMPLATE_REF_SRC } from "../template-name";
5
+ import { 値を名前で読む, type 値の名前の表 } from "../input-option";
5
6
 
6
7
  const TEMPLATE_REF_RE = new RegExp(TEMPLATE_REF_SRC, "g");
7
8
  // 残っている参照の判定用。 `g` を付けないので `lastIndex` を持たず、 使い回しても結果が
@@ -113,14 +114,25 @@ export function mergeStateValues(
113
114
  *
114
115
  * 名前に使える文字は `template-name.ts` が決める (engine 全体で 1 つ)。 ここだけで
115
116
  * 定義すると、 式を書ける側との間で「読めるが書けない」 「書けるが読めない」 名前ができる。
117
+ *
118
+ * `名前の表` を渡すと、 付け足しの無い `{id}` の値を入力欄の値ごとの名前で差し込む (#855)。
119
+ * 渡すのは画面に字を描く所 (箱の題・副題・見出し・値・行) だけ。 表示の出し分けと数の差し込みは
120
+ * 値を綴りのまま読む必要があるので渡さない。
116
121
  */
117
- export function interpolate(template: string, values: Record<string, string>): string {
122
+ export function interpolate(
123
+ template: string,
124
+ values: Record<string, string>,
125
+ 名前の表?: 値の名前の表,
126
+ ): string {
118
127
  return template.replace(TEMPLATE_REF_RE, (_, id: string, accessor?: string) => {
119
128
  // 自分が持つ名前だけを見る。 継承を辿ると `{constructor}` が「値がある」 と読まれ、
120
129
  // prototype の中身が図に出る (実測)
121
130
  const raw = Object.hasOwn(values, id) ? values[id] : undefined;
122
131
  if (raw === undefined) return `{${id}${accessor ?? ""}}`;
123
- if (!accessor) return raw;
132
+ if (!accessor) {
133
+ const 入力欄 = 名前の表?.get(id);
134
+ return 入力欄 ? 値を名前で読む(入力欄, raw) : raw;
135
+ }
124
136
  const arr = parseArraySignal(raw);
125
137
  if (!arr) return `{${id}${accessor}}`;
126
138
  if (accessor === ".length") return String(arr.length);
package/src/render.tsx CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  type InteractiveHandlerMap,
11
11
  } from "./render/interactive-panel";
12
12
  import { computeStateValues, mergeStateValues } from "./render/utils";
13
+ import { 値の名前の表を作る } from "./input-option";
13
14
  import { visualValidateLaid } from "./visual-validate";
14
15
  import type { CdlDiagram, LaidDiagram } from "./types";
15
16
 
@@ -165,6 +166,10 @@ export function CdlDiagramView({ diagram, laid: laidProp, hideHeader = false, hi
165
166
  return mergeStateValues(baseStateValues, interactive.stateOverrides);
166
167
  }, [isInteractive, baseStateValues, interactive.stateOverrides]);
167
168
 
169
+ // 入力欄の値ごとの名前 (#855)。 状態の値には混ぜず、箱の字の差し込みにだけ渡す
170
+ // (出し分けと綴りで読む読み出しは値を綴りのまま読む)
171
+ const 値の名前 = useMemo(() => 値の名前の表を作る(diagram.inputs), [diagram.inputs]);
172
+
168
173
  return (
169
174
  <div
170
175
  ref={isInteractive ? interactive.containerRef : undefined}
@@ -193,7 +198,7 @@ export function CdlDiagramView({ diagram, laid: laidProp, hideHeader = false, hi
193
198
  {/* interactive panel = input widget / formula readout / scroll readout (SVG の上に配置)、
194
199
  diagram に interactive primitive がなければ null (SSR / render 影響なし) */}
195
200
  {isInteractive && interactive.panel}
196
- <CdlStage laid={laid} currentPhase={currentPhase} progress={progress} drawProgress={drawProgress} stateValues={stateValues} debug={debug} compact={hideHeader} svgRef={isInteractive ? interactive.svgRef : undefined} />
201
+ <CdlStage laid={laid} currentPhase={currentPhase} progress={progress} drawProgress={drawProgress} stateValues={stateValues} 値の名前={値の名前} debug={debug} compact={hideHeader} svgRef={isInteractive ? interactive.svgRef : undefined} />
197
202
  {/* hideHeader + multi-phase 時、 phase indicator が消えると動きが読めないため
198
203
  mini indicator (dot 列 + 現在 phase title + badge) を bottom に追加 */}
199
204
  {hideHeader && !hideMiniPhaseIndicator && currentPhase && laid.phases.length > 1 && (
package/src/types.ts CHANGED
@@ -820,6 +820,15 @@ export type CdlState = {
820
820
  initial: number | string;
821
821
  };
822
822
 
823
+ /**
824
+ * 選択肢を持つ入力欄 (`dropdown` / `radio` / `tabs` / `multi-select`) の 1 つの選択肢 (#855)。
825
+ *
826
+ * 文字列は値と見せる字を兼ねる。 組は `value` を入力欄の値にし、`label` を操作部の選択肢と
827
+ * 箱の字の差し込み (`{id}`) に描く。 値は綴りのまま残るので、綴りで読む読み出し (信号の灯り等) と
828
+ * 表示の出し分け (`visibleIf`) は名前に影響されない。
829
+ */
830
+ export type CdlInputOption = string | { value: string; label: string };
831
+
823
832
  /**
824
833
  * interactive input widget spec (v0.5+、 CAR #243)。
825
834
  *
@@ -855,8 +864,8 @@ export type CdlInput =
855
864
  | {
856
865
  id: string;
857
866
  kind: "dropdown";
858
- /** 選択肢一覧 (非空必須、 defaultValue はこの中の 1 つ) */
859
- options: readonly string[];
867
+ /** 選択肢一覧 (非空必須、 defaultValue はこの中の 1 つの値) */
868
+ options: readonly CdlInputOption[];
860
869
  defaultValue: string;
861
870
  label?: string;
862
871
  }
@@ -865,6 +874,10 @@ export type CdlInput =
865
874
  kind: "toggle";
866
875
  defaultValue: boolean;
867
876
  label?: string;
877
+ /** 入の時に箱の字の差し込み (`{id}`) へ描く名前。 省略時は `true` (#855) */
878
+ onLabel?: string;
879
+ /** 切の時に箱の字の差し込み (`{id}`) へ描く名前。 省略時は `false` (#855) */
880
+ offLabel?: string;
868
881
  }
869
882
  /**
870
883
  * XY pad = 2 軸 slider、 `.input.xypad(id)` は 実は 2 個の signal (id_x / id_y) を生成する
@@ -896,7 +909,7 @@ export type CdlInput =
896
909
  | {
897
910
  id: string;
898
911
  kind: "radio";
899
- options: readonly string[];
912
+ options: readonly CdlInputOption[];
900
913
  defaultValue: string;
901
914
  label?: string;
902
915
  }
@@ -922,7 +935,7 @@ export type CdlInput =
922
935
  | {
923
936
  id: string;
924
937
  kind: "multi-select";
925
- options: readonly string[];
938
+ options: readonly CdlInputOption[];
926
939
  defaultValues: readonly string[];
927
940
  label?: string;
928
941
  }
@@ -930,7 +943,7 @@ export type CdlInput =
930
943
  | {
931
944
  id: string;
932
945
  kind: "tabs";
933
- options: readonly string[];
946
+ options: readonly CdlInputOption[];
934
947
  defaultValue: string;
935
948
  label?: string;
936
949
  }
@@ -2514,6 +2527,13 @@ export type CdlFormula = {
2514
2527
  id: string;
2515
2528
  /** safe subset expression 文字列。 例 `"gasBase * (1 + delta/8)"` */
2516
2529
  expression: string;
2530
+ /**
2531
+ * 操作部の式の行に描く名札 (#853)。 無ければ id を描く。
2532
+ *
2533
+ * 名札があると、式の中身の名前のうち名札を持つ入力欄と式の id も名札に置き換えて描く。
2534
+ * 置き換えは表示だけで、計算は `expression` を読む。
2535
+ */
2536
+ label?: string;
2517
2537
  };
2518
2538
 
2519
2539
  /**