@morlay/dsh-client-ui-primitives 0.0.3 → 0.0.5

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.
Files changed (34) hide show
  1. package/README.md +18 -266
  2. package/dist/client.cjs +11 -612
  3. package/dist/client.d.cts +33 -658
  4. package/dist/client.d.mts +33 -658
  5. package/dist/index.mjs +1 -1
  6. package/dist/{reference-Dxy9uQYz.mjs → reference-Cjcn21uW.mjs} +0 -1
  7. package/locale/en.json +1 -1
  8. package/locale/zh.json +1 -1
  9. package/package.json +18 -18
  10. package/src/client/index.ts +7 -9
  11. package/src/client/schema-form/Editor.tsx +25 -38
  12. package/src/client/schema-form/SchemaForm.tsx +6 -12
  13. package/src/client/schema-form/controller.ts +65 -123
  14. package/src/client/schema-form/draft.ts +55 -138
  15. package/src/client/schema-form/fields.tsx +5 -14
  16. package/src/client/schema-form/hints.ts +31 -131
  17. package/src/client/schema-form/index.ts +15 -20
  18. package/src/client/schema-form/labels.ts +7 -9
  19. package/src/client/schema-form/lines.ts +36 -60
  20. package/src/client/schema-form/locales.ts +6 -8
  21. package/src/client/schema-form/rows.ts +22 -41
  22. package/src/client/schema-form/schema-node.ts +87 -150
  23. package/src/client/schema-form/slot-contract.ts +55 -76
  24. package/src/client/schema-form/styles.ts +39 -51
  25. package/src/client/schema-form/testing/fake-describe.ts +3 -9
  26. package/src/client/schema-form/testing/fake-scope.ts +8 -10
  27. package/src/client/schema-form/value.tsx +14 -26
  28. package/src/client/settings-form/SettingsForm.tsx +13 -17
  29. package/src/client/settings-form/fields.tsx +25 -35
  30. package/src/client/settings-form/form-model.ts +63 -145
  31. package/src/client/styling/styled.tsx +3 -5
  32. package/src/client/theme.generated.ts +18 -7
  33. package/src/reference-markdown.tsx +1 -1
  34. package/src/reference.ts +1 -1
@@ -1,27 +1,20 @@
1
- /**
2
- * 自动注册面:把 describe 出来的命名空间与 bundle 的行清单配对,为每个能渲染的行注册本行的配置入口。
3
- *
4
- * 注册用 `<bundle 包名>#<行 id>`(页面按同一个 key 打开这一行的配置页)与兜底 priority:手写卡片用默认
5
- * priority 0 注册,按 slots 的 cell winner 规则自然遮住这里的自动项,与加载顺序无关。
6
- */
1
+ // 自动注册面:把 describe 出来的命名空间与 bundle 的行清单配对,为每个能渲染的行注册本行的配置入口。
2
+ //
3
+ // 注册用 `<bundle 包名>#<行 id>`(页面按同一个 key 打开这一行的配置页)与兜底 priority:手写卡片用默认
4
+ // priority 0 注册,按 slots 的 cell winner 规则自然遮住这里的自动项,与加载顺序无关。
7
5
 
8
6
  import type { BundleInfo, SettingsNamespaceView } from "@deepseek-ai/dsh-api-remotes/client";
9
7
  import type { SettingsDescribeFace } from "@deepseek-ai/dsh-client-ui-settings/client";
10
8
 
11
- /** 一个该自动长出配置页的行。 */
9
+ // 一个该自动长出配置页的行。
12
10
  export interface RowRegistrationPlan {
13
- /** `plugins.row.config` 的 key:`<bundle 包名>#<行 id>`。 */
11
+ // `plugins.row.config` 的 key:`<bundle 包名>#<行 id>`。
14
12
  key: string;
15
- /** settings 命名空间(行 id)。 */
13
+ // settings 命名空间(行 id)。
16
14
  ns: string;
17
15
  }
18
16
 
19
- /**
20
- * 配对命名空间与 bundle 行清单。
21
- * @param namespaces - host 发来的命名空间视图。
22
- * @param bundles - host 的 bundle 清单(含各 bundle 声明的行 id)。
23
- * @returns 该自动注册的行,按命名空间视图的顺序。
24
- */
17
+ // 配对命名空间与 bundle 行清单:只给 `autoGenerate` 且能在清单里找到 owner 的行,顺序即命名空间视图的顺序。
25
18
  export function rowRegistrations(
26
19
  namespaces: readonly SettingsNamespaceView[],
27
20
  bundles: readonly BundleInfo[],
@@ -40,9 +33,9 @@ export function rowRegistrations(
40
33
  return plans;
41
34
  }
42
35
 
43
- /** 注册器要的外部面。 */
36
+ // 注册器要的外部面。
44
37
  export interface RowRegistrationDeps {
45
- /** 槽位注册:返回注销函数。 */
38
+ // 槽位注册:返回注销函数。
46
39
  slots: {
47
40
  register(
48
41
  options: {
@@ -54,48 +47,36 @@ export interface RowRegistrationDeps {
54
47
  component: unknown,
55
48
  ): () => void;
56
49
  };
57
- /** describe 读面:命名空间清单与变更通知。 */
50
+ // describe 读面:命名空间清单与变更通知。
58
51
  describe: SettingsDescribeFace;
59
- /** 读 bundle 清单。 */
52
+ // 读 bundle 清单。
60
53
  bundles(): Promise<readonly BundleInfo[]>;
61
- /** 订阅 bundle 清单变化。 */
54
+ // 订阅 bundle 清单变化。
62
55
  subscribeBundles(listener: () => void): () => void;
63
- /**
64
- * 一行的注册项组件;`undefined` 表示这一行渲染不了(schema rehydrate 不了或段根不是对象)。
65
- * 每次调用返回同一个实例(控制器按命名空间缓存)。
66
- */
56
+ // 一行的注册项组件;`undefined` 表示这一行渲染不了(schema rehydrate 不了或段根不是对象)。
57
+ // 每次调用返回同一个实例(控制器按命名空间缓存)。
67
58
  entryFor(ns: string): { component: unknown } | undefined;
68
- /** 槽项文案的命名空间。 */
59
+ // 槽项文案的命名空间。
69
60
  locale: string;
70
- /** 一行注销时释放它的控制器。 */
61
+ // 一行注销时释放它的控制器。
71
62
  release(ns: string): void;
72
63
  }
73
64
 
74
- /** 自动项的兜底优先级:手写卡片(默认 0)遮住它。 */
65
+ // 自动项的兜底优先级:手写卡片(默认 0)遮住它。
75
66
  const FALLBACK_PRIORITY = 100;
76
67
 
77
- /**
78
- * 自动注册的差分器:按 describe 与 bundle 清单把「该有的行」与「已在的行」对齐。
79
- */
68
+ // 自动注册的差分器:按 describe 与 bundle 清单把「该有的行」与「已在的行」对齐。
80
69
  export class RowRegistration {
81
70
  readonly #deps: RowRegistrationDeps;
82
71
  readonly #live = new Map<string, () => void>();
83
72
  #pending: Promise<void> | undefined;
84
73
  #disposed = false;
85
74
 
86
- /**
87
- * @param deps - 槽位、清单读面与每行的槽项工厂。
88
- */
89
75
  constructor(deps: RowRegistrationDeps) {
90
76
  this.#deps = deps;
91
77
  }
92
78
 
93
- /**
94
- * 对齐一次:新增该有的行,注销不该有的行。
95
- *
96
- * 并发调用折叠成一次(bundle 清单只读一趟),后到的调用等同一趟结束。
97
- * @returns 对齐完成。
98
- */
79
+ // 对齐一次:新增该有的行,注销不该有的行;并发调用折叠成一次(bundle 清单只读一趟)。
99
80
  sync(): Promise<void> {
100
81
  if (this.#disposed) return Promise.resolve();
101
82
  this.#pending ??= this.#run().finally(() => {
@@ -104,7 +85,7 @@ export class RowRegistration {
104
85
  return this.#pending;
105
86
  }
106
87
 
107
- /** 收掉自己:注销还活着的全部注册并释放它们的控制器。 */
88
+ // 收掉自己:注销还活着的全部注册并释放它们的控制器。
108
89
  dispose(): void {
109
90
  this.#disposed = true;
110
91
  this.#release();
@@ -142,7 +123,7 @@ export class RowRegistration {
142
123
  }
143
124
  }
144
125
 
145
- /** 注销全部并释放控制器。 */
126
+ // 注销全部并释放控制器。
146
127
  #release(): void {
147
128
  for (const [ns, dispose] of this.#live) {
148
129
  dispose();
@@ -1,103 +1,91 @@
1
- /**
2
- * schema 投影:把 host 发来的 volatile 表单 schema 变成渲染层要的字段树。
3
- *
4
- * 投影只描述**结构**(字段、路径、meta、只读原因),不读值、不做本地化、不碰 React:值在 `draft.ts`,
5
- * 文案在 `labels.ts`,控件在 `fields/`。动态子节点(dict 的键与值、array 的项)的路径段用
6
- * {@link DYNAMIC_SEGMENT} 占位,渲染层按真实键/索引替换后再交给字段槽。
7
- */
1
+ // schema 投影:把 host 发来的 volatile 表单 schema 变成渲染层要的字段树。
2
+ //
3
+ // 只描述结构(字段、路径、meta、只读原因),不读值、不做本地化、不碰 React;动态子节点的路径段用
4
+ // `DYNAMIC_SEGMENT` 占位,渲染层按真实键 / 索引替换后再交给字段槽。
8
5
 
9
6
  import type { SchemaNode } from "@deepseek-ai/dsh-client-ui-settings/client";
10
7
 
11
- /** 动态子节点的路径占位段(dict 的键与值、array 的项):渲染层按真实键或索引替换。 */
8
+ // 动态子节点的路径占位段(dict 的键与值、array 的项):渲染层按真实键或索引替换。
12
9
  export const DYNAMIC_SEGMENT = "*";
13
10
 
14
- /** 节点不可编辑的原因;`null` 表示可编辑。 */
11
+ // 节点不可编辑的原因;`null` 表示可编辑。
15
12
  export type ReadOnlyReason = "transform" | "function" | "constructor";
16
13
 
17
- /** 归一化后的字段元数据(schemastery `meta` 的可用子集)。 */
14
+ // 归一化后的字段元数据(schemastery `meta` 的可用子集)。
18
15
  export interface FieldMeta {
19
- /** `description`(可能是本地化字典):渲染时经 locale 解析。 */
16
+ // `description`(可能是本地化字典):渲染时经 locale 解析。
20
17
  description: string | Readonly<Record<string, string>> | undefined;
21
- /** `comment`:纯文本补充说明。 */
18
+ // `comment`:纯文本补充说明。
22
19
  comment: string | undefined;
23
- /** `link`:外部文档。 */
20
+ // `link`:外部文档。
24
21
  link: string | undefined;
25
- /** `role`:控件提示(`secret` / `slider` / `datetime` …)。 */
22
+ // `role`:控件提示(`secret` / `slider` / `datetime` …)。
26
23
  role: string | undefined;
27
- /** `role === 'secret'`:值不回传,只能写。 */
24
+ // `role === 'secret'`:值不回传,只能写。
28
25
  secret: boolean;
29
- /** `required()`。 */
26
+ // `required()`。
30
27
  required: boolean;
31
- /** `disabled()`:控件禁用。 */
28
+ // `disabled()`:控件禁用。
32
29
  disabled: boolean;
33
- /** `collapse()`:嵌套分组默认折叠。 */
30
+ // `collapse()`:嵌套分组默认折叠。
34
31
  collapse: boolean;
35
- /** `deprecated()` / `experimental()` 的徽标文本。 */
32
+ // `deprecated()` / `experimental()` 的徽标文本。
36
33
  badges: readonly string[];
37
- /** `pattern()` 的正则来源。 */
34
+ // `pattern()` 的正则来源。
38
35
  pattern: string | undefined;
39
- /** `min()`。 */
36
+ // `min()`。
40
37
  min: number | undefined;
41
- /** `max()`。 */
38
+ // `max()`。
42
39
  max: number | undefined;
43
- /** `step()`:`1` 表示整数。 */
40
+ // `step()`:`1` 表示整数。
44
41
  step: number | undefined;
45
- /** 是否声明了默认值(与「默认值是 undefined」区分开)。 */
42
+ // 是否声明了默认值(与「默认值是 undefined」区分开)。
46
43
  hasDefault: boolean;
47
- /** 声明的默认值。 */
44
+ // 声明的默认值。
48
45
  defaultValue: unknown;
49
- /** `extra()`:角色附带的任意数据,字段槽的自定义控件可用。 */
46
+ // `extra()`:角色附带的任意数据,字段槽的自定义控件可用。
50
47
  extra: unknown;
51
48
  }
52
49
 
53
- /** 每种节点共有的部分。 */
50
+ // 每种节点共有的部分。
54
51
  export interface FieldNodeBase {
55
- /** 这一层的名字:对象字段名、tuple 的索引、动态子节点的占位段。 */
52
+ // 这一层的名字:对象字段名、tuple 的索引、动态子节点的占位段。
56
53
  key: string;
57
- /** 从段根起的路径;动态段是 {@link DYNAMIC_SEGMENT}。 */
54
+ // 从段根起的路径;动态段是 `DYNAMIC_SEGMENT`。
58
55
  path: readonly string[];
59
- /**
60
- * 按值展开时算好的显示名:数组成员的**项身份**(见 {@link FieldNodeBase} 所在的 array 节点)。
61
- * 投影时没有(`undefined`),渲染层用它优先于字段名。
62
- */
56
+ // 按值展开时算好的显示名(数组成员的项身份):投影时为 `undefined`,渲染层用它优先于字段名。
63
57
  label: string | undefined;
64
- /** 归一化后的元数据。 */
58
+ // 归一化后的元数据。
65
59
  meta: FieldMeta;
66
- /** 不可编辑的原因;`null` 表示可编辑。 */
60
+ // 不可编辑的原因;`null` 表示可编辑。
67
61
  readOnly: ReadOnlyReason | null;
68
62
  }
69
63
 
70
- /** 位集的一位。 */
64
+ // 位集的一位。
71
65
  export interface BitsEntry {
72
66
  name: string;
73
67
  bit: number;
74
68
  }
75
69
 
76
- /** 渲染层要的一个字段节点。 */
70
+ // 渲染层要的一个字段节点。
77
71
  export type FieldNode =
78
72
  | (FieldNodeBase & { type: "object"; fields: readonly FieldNode[] })
79
73
  | (FieldNodeBase & { type: "dict"; keyNode: FieldNode; valueNode: FieldNode })
80
74
  | (FieldNodeBase & {
81
75
  type: "array";
82
76
  item: FieldNode;
83
- /**
84
- * 项的**身份键**:这一项的「名字」取自它自己的哪个字段。显式声明优先
85
- * (`z.array(inner).role('items', { mergeKey: 'id' })`),否则 item 上有 `id` 字段就用 `id`。
86
- * 它只决定显示名与「同名即冲突」的判据——写盘仍按索引(host 的 path op 就是索引语义)。
87
- */
77
+ // 项的**身份键**:显式声明优先(`z.array(inner).role('items', { mergeKey: 'id' })`),否则 item 上有 `id`
78
+ // 字段就用 `id`。只决定显示名与「同名即冲突」的判据——写盘仍按索引。
88
79
  mergeKey: string | undefined;
89
80
  })
90
81
  | (FieldNodeBase & { type: "tuple"; items: readonly FieldNode[] })
91
82
  | (FieldNodeBase & {
92
83
  type: "union";
93
- /** 成员全是字面量时的可选项;否则 `undefined`。 */
84
+ // 成员全是字面量时的可选项;否则 `undefined`。
94
85
  choices: readonly unknown[] | undefined;
95
86
  variants: readonly FieldNode[];
96
- /**
97
- * 判别标签:按哪个字段的值挑变体。显式声明优先
98
- * (`z.union([...]).role('union', { tag: 'kind' })`),否则取所有对象成员共有的**唯一**常量字段。
99
- * schemastery 本身没有 tag 机制(union 按声明顺序逐个试),所以这只是渲染与校验的选支依据。
100
- */
87
+ // 判别标签:按哪个字段的值挑变体。显式声明优先(`z.union([...]).role('union', { tag: 'kind' })`),
88
+ // 否则取所有对象成员共有的**唯一**常量字段——schemastery 没有 tag 机制,这只是渲染与校验的选支依据。
101
89
  tagKey: string | undefined;
102
90
  })
103
91
  | (FieldNodeBase & { type: "intersect"; members: readonly FieldNode[] })
@@ -105,10 +93,10 @@ export type FieldNode =
105
93
  | (FieldNodeBase & { type: "const"; value: unknown })
106
94
  | (FieldNodeBase & { type: "lazy"; inner: FieldNode })
107
95
  | (FieldNodeBase & { type: "string" | "number" | "boolean" | "any" | "never" | "function" })
108
- /** 自引用 schema 回到祖先处:值这一层以下不再描述,由渲染层展开或只读呈现。 */
96
+ // 自引用 schema 回到祖先处:值这一层以下不再描述,由渲染层展开或只读呈现。
109
97
  | (FieldNodeBase & { type: "recursive"; origin: string });
110
98
 
111
- /** 空的元数据:动态子节点(无声明 schema 的 dict 键)与空字段树用它。 */
99
+ // 空的元数据:动态子节点(无声明 schema 的 dict 键)与空字段树用它。
112
100
  export function emptyMeta(): FieldMeta {
113
101
  return {
114
102
  description: undefined,
@@ -130,12 +118,12 @@ export function emptyMeta(): FieldMeta {
130
118
  };
131
119
  }
132
120
 
133
- /** `badges` 只留文本:配色与文案由字典决定。 */
121
+ // `badges` 只留文本:配色与文案由字典决定。
134
122
  function badgesOf(meta: SchemaNode["meta"]): string[] {
135
123
  return (meta.badges ?? []).map((badge) => badge.text);
136
124
  }
137
125
 
138
- /** 归一化一个节点的 meta。 */
126
+ // 归一化一个节点的 meta。
139
127
  function metaOf(schema: SchemaNode): FieldMeta {
140
128
  const meta = schema.meta ?? {};
141
129
  return {
@@ -159,7 +147,7 @@ function metaOf(schema: SchemaNode): FieldMeta {
159
147
  };
160
148
  }
161
149
 
162
- /** 外层 meta 与内层 meta 合并:外层的 undefined 不覆盖内层(transform 的 inner 带着自己的角色)。 */
150
+ // 外层 meta 与内层 meta 合并:外层的 undefined 不覆盖内层(transform 的 inner 带着自己的角色)。
163
151
  function mergeMeta(inner: FieldMeta, outer: FieldMeta): FieldMeta {
164
152
  const merged: Record<string, unknown> = { ...inner };
165
153
  for (const [key, value] of Object.entries(outer)) {
@@ -168,17 +156,17 @@ function mergeMeta(inner: FieldMeta, outer: FieldMeta): FieldMeta {
168
156
  return merged as unknown as FieldMeta;
169
157
  }
170
158
 
171
- /** 节点自己的路径末段:动态段保持占位,根节点为空串。 */
159
+ // 节点自己的路径末段:动态段保持占位,根节点为空串。
172
160
  function keyOf(path: readonly string[]): string {
173
161
  return path[path.length - 1] ?? "";
174
162
  }
175
163
 
176
- /** 一个纯字符串叶子(dict 没有声明键 schema 时用它)。 */
164
+ // 一个纯字符串叶子(dict 没有声明键 schema 时用它)。
177
165
  function stringLeaf(path: readonly string[], meta: FieldMeta): FieldNode {
178
166
  return { key: keyOf(path), path, label: undefined, meta, readOnly: null, type: "string" };
179
167
  }
180
168
 
181
- /** 把 schema 的类型名收口成渲染层认识的那几种。 */
169
+ // 把 schema 的类型名收口成渲染层认识的那几种。
182
170
  function leafTypeOf(type: string): "string" | "number" | "boolean" | "any" | "never" | "function" {
183
171
  switch (type) {
184
172
  case "string":
@@ -196,12 +184,7 @@ function leafTypeOf(type: string): "string" | "number" | "boolean" | "any" | "ne
196
184
  }
197
185
  }
198
186
 
199
- /**
200
- * 投影一个 schema 节点。
201
- * @param schema - rehydrate 之后的 volatile 表单 schema(或它的子节点)。
202
- * @param path - 该节点相对段根的路径;动态子节点由调用方给占位段。
203
- * @returns 渲染层要的字段树。
204
- */
187
+ // 投影一个 schema 节点:`path` 是相对段根的路径,动态子节点由调用方给占位段。
205
188
  export function projectNode(schema: SchemaNode, path: readonly string[] = []): FieldNode {
206
189
  return project(schema, path, []);
207
190
  }
@@ -303,14 +286,14 @@ function project(
303
286
  }
304
287
  }
305
288
 
306
- /** 展开 `lazy` 包装:投影里 lazy 只是一层,值仍在同一路径上。 */
289
+ // 展开 `lazy` 包装:投影里 lazy 只是一层,值仍在同一路径上。
307
290
  export function unwrapLazy(node: FieldNode | undefined): FieldNode | undefined {
308
291
  let current = node;
309
292
  while (current?.type === "lazy") current = current.inner;
310
293
  return current;
311
294
  }
312
295
 
313
- /** 某一层可见的字段:object 直接给;union / intersect 把各变体的对象字段并起来。 */
296
+ // 某一层可见的字段:object 直接给;union / intersect 把各变体的对象字段并起来。
314
297
  export function fieldsOf(node: FieldNode | undefined): readonly FieldNode[] {
315
298
  const current = unwrapLazy(node);
316
299
  switch (current?.type) {
@@ -325,12 +308,7 @@ export function fieldsOf(node: FieldNode | undefined): readonly FieldNode[] {
325
308
  }
326
309
  }
327
310
 
328
- /**
329
- * 按具体路径找回投影节点(动态段匹配真实键或索引)。
330
- * @param root - 段根的字段树。
331
- * @param path - 具体路径。
332
- * @returns 命中的节点,或 `undefined`。
333
- */
311
+ // 按具体路径找回投影节点(动态段匹配真实键或索引)。
334
312
  export function seekNode(root: FieldNode, path: readonly string[]): FieldNode | undefined {
335
313
  let current: FieldNode | undefined = root;
336
314
  for (const segment of path) {
@@ -358,14 +336,12 @@ export function seekNode(root: FieldNode, path: readonly string[]): FieldNode |
358
336
  return unwrapLazy(current);
359
337
  }
360
338
 
361
- /** 按当前值展开出的一个字段:路径与名字已按真实键与索引替换。 */
339
+ // 按当前值展开出的一个字段:路径与名字已按真实键与索引替换。
362
340
  export interface WalkedField {
363
341
  path: readonly string[];
364
342
  node: FieldNode;
365
- /**
366
- * 这一行是某个动态容器的**成员本身**(dict 的键 / array 的项)时给出它的身份:容器路径、键、以及数组
367
- * 项的位置。渲染层用它画成员行(名字 + 移除);成员的子字段不带这个标记。
368
- */
343
+ // 这一行是某个动态容器的**成员本身**(dict 的键 / array 的项)时给出它的身份:容器路径、键、以及数组
344
+ // 项的位置。渲染层用它画成员行(名字 + 移除);成员的子字段不带这个标记。
369
345
  member?: {
370
346
  parent: readonly string[];
371
347
  key: string;
@@ -373,26 +349,16 @@ export interface WalkedField {
373
349
  };
374
350
  }
375
351
 
376
- /** 候选键的来源(业务注册;见 `hints.ts`):dict 的「可以加哪些键」。 */
352
+ // 候选键的来源(业务注册;见 `hints.ts`):dict 的「可以加哪些键」。
377
353
  export interface SuggestedKeys {
378
- /**
379
- * 读某个 dict 字段的候选键。
380
- * @param path - 该字段的具体路径。
381
- * @returns 候选键;没有就是空数组。
382
- */
354
+ // 读某个 dict 字段的候选键;没有就是空数组。
383
355
  keysFor(path: readonly string[]): readonly string[];
384
356
  }
385
357
 
386
- /**
387
- * 按当前值把字段树展开成渲染顺序的扁平列表(前序):容器在前、子项紧随其后。
388
- *
389
- * 出现的**只有值里有的东西,加上必填的**:对象的必填字段始终占行(值是空的也要让用户看见"必须填"),非必填的
390
- * 按值出现——值里没有就不占行,它在这一层的「添加属性」候选里,选中才加。数组按索引、字典按键展开;字面量
391
- * union 是叶子控件,对象 union 只展开与当前值匹配的那个变体,intersect 的各成员在同一层展开。
392
- * @param root - 段根的字段树。
393
- * @param value - 生效值(或草稿后的值)。
394
- * @returns 渲染顺序的字段列表。
395
- */
358
+ // 按当前值把字段树展开成渲染顺序的扁平列表(前序):容器在前、子项紧随其后。
359
+ //
360
+ // 出现的**只有值里有的东西,加上必填的**:非必填字段值里没有就不占行,它在这一层的「添加属性」候选里。
361
+ // 数组按索引、字典按键展开;字面量 union 是叶子控件,对象 union 只展开与当前值匹配的那个变体。
396
362
  export function walkFields(root: FieldNode, value: unknown): WalkedField[] {
397
363
  const out: WalkedField[] = [];
398
364
  visit(root, root.path, value, out, true, undefined, new Set(), undefined);
@@ -491,7 +457,7 @@ function visit(
491
457
  }
492
458
  }
493
459
 
494
- /** 数组的项身份:显式声明优先,否则 item 上的 `id` 字段。 */
460
+ // 数组的项身份:显式声明优先,否则 item 上的 `id` 字段。
495
461
  function mergeKeyOf(schema: SchemaNode, item: FieldNode): string | undefined {
496
462
  const declared = declaredExtra(schema, "mergeKey");
497
463
  if (declared !== undefined) return declared;
@@ -501,7 +467,7 @@ function mergeKeyOf(schema: SchemaNode, item: FieldNode): string | undefined {
501
467
  : undefined;
502
468
  }
503
469
 
504
- /** union 的判别标签:显式声明优先,否则所有对象成员共有的唯一常量字段。 */
470
+ // union 的判别标签:显式声明优先,否则所有对象成员共有的唯一常量字段。
505
471
  function tagKeyOf(schema: SchemaNode, variants: readonly FieldNode[]): string | undefined {
506
472
  const declared = declaredExtra(schema, "tag");
507
473
  if (declared !== undefined) return declared;
@@ -520,7 +486,7 @@ function tagKeyOf(schema: SchemaNode, variants: readonly FieldNode[]): string |
520
486
  return candidates.length === 1 ? candidates[0] : undefined;
521
487
  }
522
488
 
523
- /** `role(text, { … })` 里声明的字符串约定(项身份 `mergeKey`、判别标签 `tag`)。 */
489
+ // `role(text, { … })` 里声明的字符串约定(项身份 `mergeKey`、判别标签 `tag`)。
524
490
  function declaredExtra(schema: SchemaNode, name: string): string | undefined {
525
491
  const extra = schema.meta?.extra;
526
492
  if (typeof extra !== "object" || extra === null) return undefined;
@@ -528,7 +494,7 @@ function declaredExtra(schema: SchemaNode, name: string): string | undefined {
528
494
  return typeof value === "string" && value.length > 0 ? value : undefined;
529
495
  }
530
496
 
531
- /** 一项的项身份值(渲染与冲突检查共用的读法)。 */
497
+ // 一项的项身份值(渲染与冲突检查共用的读法)。
532
498
  function memberLabel(value: unknown, mergeKey: string): string | undefined {
533
499
  if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
534
500
  const member: unknown = Reflect.get(value, mergeKey);
@@ -537,27 +503,20 @@ function memberLabel(value: unknown, mergeKey: string): string | undefined {
537
503
  return undefined;
538
504
  }
539
505
 
540
- /** 项身份的一处冲突。 */
506
+ // 项身份的一处冲突。
541
507
  export interface MergeKeyConflict {
542
- /** `duplicate`:两个成员同名;`missing`:成员没有名字。 */
508
+ // `duplicate`:两个成员同名;`missing`:成员没有名字。
543
509
  kind: "duplicate" | "missing";
544
- /** 冲突成员的具体路径。 */
510
+ // 冲突成员的具体路径。
545
511
  path: readonly string[];
546
- /** 判据用的字段名。 */
512
+ // 判据用的字段名。
547
513
  mergeKey: string;
548
- /** 那个名字(缺名字时是空串)。 */
514
+ // 那个名字(缺名字时是空串)。
549
515
  value: string;
550
516
  }
551
517
 
552
- /**
553
- * 项身份冲突:同一层里两个成员同名,或某个成员没有名字。
554
- *
555
- * 名字是成员的「身份」,写盘仍按索引——所以这只是保存前的本地校验:重名的数组在页面上是非法草稿,
556
- * 而不是悄悄写到 host 上。
557
- * @param root - 段根的字段树。
558
- * @param value - 草稿后的段值。
559
- * @returns 第一处冲突,或 `undefined`。
560
- */
518
+ // 项身份冲突:同一层里两个成员同名,或某个成员没有名字。名字是成员的「身份」而写盘仍按索引,
519
+ // 所以这是保存前的本地校验——重名的数组在页面上是非法草稿,不会悄悄写到 host 上。
561
520
  export function mergeKeyConflict(root: FieldNode, value: unknown): MergeKeyConflict | undefined {
562
521
  return scanConflict(root, root.path, value);
563
522
  }
@@ -627,14 +586,14 @@ function scanConflict(
627
586
  }
628
587
  }
629
588
 
630
- /** 一个容器值里的成员(对象键或数组索引)。 */
589
+ // 一个容器值里的成员(对象键或数组索引)。
631
590
  function entriesOf(value: unknown): [string, unknown][] {
632
591
  if (Array.isArray(value)) return value.map((item, index) => [String(index), item]);
633
592
  if (typeof value === "object" && value !== null) return Object.entries(value);
634
593
  return [];
635
594
  }
636
595
 
637
- /** 读一个成员的值。 */
596
+ // 读一个成员的值。
638
597
  function memberOf(value: unknown, key: string | number): unknown {
639
598
  if (Array.isArray(value)) return value[Number(key)];
640
599
  if (typeof value === "object" && value !== null)
@@ -642,33 +601,28 @@ function memberOf(value: unknown, key: string | number): unknown {
642
601
  return undefined;
643
602
  }
644
603
 
645
- /** union 的变体切换:一个选项要写的值。 */
604
+ // union 的变体切换:一个选项要写的值。
646
605
  export interface VariantChoice {
647
- /** 页面上显示的名字:判别标签的值,或变体的形状。 */
606
+ // 页面上显示的名字:判别标签的值,或变体的形状。
648
607
  label: string;
649
- /** 写这个值就等于选中这一支。 */
608
+ // 写这个值就等于选中这一支。
650
609
  value: unknown;
651
610
  }
652
611
 
653
- /** 把节点收成 union:渲染层判形状与选支都从这里进。 */
612
+ // 把节点收成 union:渲染层判形状与选支都从这里进。
654
613
  export function unionOf(node: FieldNode | undefined): (FieldNode & { type: "union" }) | undefined {
655
614
  const current = unwrapLazy(node);
656
615
  return current?.type === "union" ? current : undefined;
657
616
  }
658
617
 
659
- /**
660
- * 当前值落在哪一支:与 {@link walkFields} 的展开同一判据(标签值优先,其次值的形状,最后第一支)。
661
- * @param node - 字段节点(不是 union 就原样返回)。
662
- * @param value - 这一层的值。
663
- * @returns 选中变体的节点。
664
- */
618
+ // 当前值落在哪一支:与 `walkFields` 的展开同一判据(标签值优先,其次值的形状,最后第一支)。
665
619
  export function variantOf(node: FieldNode, value: unknown): FieldNode {
666
620
  const union = unionOf(node);
667
621
  if (union === undefined) return unwrapLazy(node) ?? node;
668
622
  return pickVariant(union, value) ?? union;
669
623
  }
670
624
 
671
- /** 变体里那个判别标签字段的常量值(不是 `const` 就没有)。 */
625
+ // 变体里那个判别标签字段的常量值(不是 `const` 就没有)。
672
626
  function tagValueOf(variant: FieldNode, tagKey: string): { found: boolean; value: unknown } {
673
627
  const current = unwrapLazy(variant);
674
628
  if (current?.type !== "object" && current?.type !== "intersect") {
@@ -682,7 +636,7 @@ function tagValueOf(variant: FieldNode, tagKey: string): { found: boolean; value
682
636
  return { found: false, value: undefined };
683
637
  }
684
638
 
685
- /** 一个变体在页面上的名字:能看出"这一支是什么",而不是内部类型名。 */
639
+ // 一个变体在页面上的名字:能看出"这一支是什么",而不是内部类型名。
686
640
  function variantLabel(variant: FieldNode, tag: { found: boolean; value: unknown }): string {
687
641
  if (tag.found) return JSON.stringify(tag.value);
688
642
  switch (unwrapLazy(variant)?.type) {
@@ -704,14 +658,8 @@ function variantLabel(variant: FieldNode, tag: { found: boolean; value: unknown
704
658
  }
705
659
  }
706
660
 
707
- /**
708
- * 变体切换的选项。
709
- *
710
- * 带判别标签的 union 写**标签值**(`session-rdb` 的 `type: sqlite` ↔ `type: postgres`);没有标签的写目标变体的
711
- * 空值(`access` 在「一段文本」与「一组文本」之间切)。
712
- * @param union - 投影出来的 union 节点。
713
- * @returns 每个变体一项,顺序即声明顺序。
714
- */
661
+ // 变体切换的选项:带判别标签的 union 写**标签值**(`session-rdb` 的 `type: sqlite` ↔ `type: postgres`),
662
+ // 没有标签的写目标变体的空值(`access` 在「一段文本」与「一组文本」之间切)。
715
663
  export function variantChoices(union: FieldNode & { type: "union" }): readonly VariantChoice[] {
716
664
  return union.variants.map((variant) => {
717
665
  const tag =
@@ -725,7 +673,7 @@ export function variantChoices(union: FieldNode & { type: "union" }): readonly V
725
673
  });
726
674
  }
727
675
 
728
- /** 按路径读一个值:对象键与数组下标都认(值的读法只有这一处)。 */
676
+ // 按路径读一个值:对象键与数组下标都认(值的读法只有这一处)。
729
677
  export function readPath(root: unknown, path: readonly string[]): unknown {
730
678
  return path.reduce<unknown>((node, segment) => {
731
679
  if (Array.isArray(node)) return node[Number(segment)];
@@ -734,14 +682,8 @@ export function readPath(root: unknown, path: readonly string[]): unknown {
734
682
  }, root);
735
683
  }
736
684
 
737
- /**
738
- * 这个"默认值"其实不是值——它不该用来当新加进来的那一项的初值。
739
- *
740
- * 两种:`null`("没有值"的判断位,例如 `defaultModel` 用 `.default(null)` 表达"没配就没有这个键"),以及
741
- * schemastery 给容器类型自动塞的空壳(`{}`)。两种都该让容器把字段摆出来。
742
- * @param node - 字段节点。
743
- * @returns 是不是空壳。
744
- */
685
+ // 这个"默认值"其实不是值——`null`("没有值"的判断位)或 schemastery 给容器自动塞的空壳(`{}`),
686
+ // 两种都该让容器把字段摆出来,而不是拿来当新加项的初值。
745
687
  function isBlankDefault(node: FieldNode): boolean {
746
688
  const value = node.meta.defaultValue;
747
689
  if (value === null) return true;
@@ -749,15 +691,10 @@ function isBlankDefault(node: FieldNode): boolean {
749
691
  return typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
750
692
  }
751
693
 
752
- /**
753
- * 一个新子节点的初值:声明的默认值优先;对象把字段摆出来(见下);字典/数组给空的容器;其余标量给 **`null`**。
754
- *
755
- * `null` 在这里是"还没有值"的判断位(`null` / `undefined` 都算),页面据此把它画成一个等着输入的位子——
756
- * 而不是 `""`、`0` 这种看着像已经填过的值。host 侧也认这个语义:非必填字段拿到 `null` 回退到默认值,
757
- * 必填字段拿到 `null` 就是"必须填"。
758
- * @param node - 子节点的字段树。
759
- * @returns 初值。
760
- */
694
+ // 一个新子节点的初值:声明的默认值优先,对象把字段摆出来,字典 / 数组给空容器,其余标量给 `null`。
695
+ //
696
+ // `null` 在这里是"还没有值"的判断位(`null` / `undefined` 都算),页面据此画成等着输入的位子——而不是 `""`、
697
+ // `0` 这种看着像已填过的值;host 侧也认这个语义(非必填字段拿到 `null` 回退默认值,必填就是"必须填")。
761
698
  export function emptyValueOf(node: FieldNode): unknown {
762
699
  if (node.meta.hasDefault && !isBlankDefault(node)) {
763
700
  return structuredClone(node.meta.defaultValue);
@@ -785,7 +722,7 @@ export function emptyValueOf(node: FieldNode): unknown {
785
722
  }
786
723
  }
787
724
 
788
- /** 选中与当前值匹配的 union 变体:对象按判别标签的值判,叶子按值类型判。 */
725
+ // 选中与当前值匹配的 union 变体:对象按判别标签的值判,叶子按值类型判。
789
726
  function pickVariant(node: FieldNode & { type: "union" }, value: unknown): FieldNode | undefined {
790
727
  if (
791
728
  node.tagKey !== undefined &&