@morlay/dsh-client-ui-primitives 0.0.4 → 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 +1 -611
  3. package/dist/client.d.cts +11 -658
  4. package/dist/client.d.mts +11 -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 +2 -2
  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 +6 -7
  33. package/src/reference-markdown.tsx +1 -1
  34. package/src/reference.ts +1 -1
@@ -1,12 +1,6 @@
1
- /**
2
- * 暂存式草稿模型:把用户在这一页上的编辑收成一组 path op,保存时一次提交。
3
- *
4
- * 每次保存都是 host 上一次带 revision 栅栏的持久写,所以控件**不直接写盘**:编辑先落在草稿上(屏幕上看到的
5
- * 就是要保存的内容),`save()` 是唯一的写盘点,且先过一遍整段校验——schema 挡下的值不发请求。字段的「已覆盖」
6
- * 由用户层的 presence 决定(覆盖值恰好等于默认值也是覆盖),不是值比较。
7
- *
8
- * 与渲染无关:控件读 `field(path)`、调 `set/setText/clear/appendItem/…`;文案与控件形状在 `fields/`。
9
- */
1
+ // 暂存式草稿模型:编辑先落草稿,`save()` 是唯一的写盘点(先过整段校验,schema 挡下的值不发请求)。
2
+ //
3
+ // 「已覆盖」由用户层 presence 决定(值恰好等于默认值也算覆盖);本模块与渲染无关,文案与控件形状在 `fields/`。
10
4
 
11
5
  import type { SettingsPathOpView } from "@deepseek-ai/dsh-api-remotes/client";
12
6
  import { createSnapshotStore, type SnapshotStore } from "@deepseek-ai/dsh-client-store";
@@ -14,37 +8,30 @@ import type { ConfigForm, ConfigFormSnapshot } from "@deepseek-ai/dsh-client-ui-
14
8
  import type { SettingsFormShell } from "@deepseek-ai/dsh-client-ui-primitives";
15
9
  import { emptyValueOf, fieldsOf, readPath, seekNode, type FieldNode } from "./schema-node.ts";
16
10
 
17
- /** 命名空间段(一段配置)的值。 */
11
+ // 命名空间段(一段配置)的值。
18
12
  export type Section = Record<string, unknown>;
19
13
 
20
- /** 文本草稿的解析结果:控件把用户输入交给保存那一刻解析。 */
14
+ // 文本草稿的解析结果:控件把用户输入交给保存那一刻解析。
21
15
  export type TextParseResult =
22
- /** 得到一个值。 */
16
+ // 得到一个值。
23
17
  | { kind: "value"; value: unknown }
24
- /** 这段文本不产生写(例如 secret 留空=保留已存的值)。 */
18
+ // 这段文本不产生写(例如 secret 留空=保留已存的值)。
25
19
  | { kind: "skip" }
26
- /** 不是这个字段接受的值:留在屏幕上并挡住保存。 */
20
+ // 不是这个字段接受的值:留在屏幕上并挡住保存。
27
21
  | { kind: "invalid"; message: string };
28
22
 
29
- /** 控件给文本草稿的解析规则。 */
23
+ // 控件给文本草稿的解析规则。
30
24
  export type TextParse = (text: string) => TextParseResult;
31
25
 
32
- /** 整段校验失败:消息 + 它落在哪一层(页面把消息画在**出错的那一行**上)。 */
26
+ // 整段校验失败:消息 + 它落在哪一层(页面把消息画在**出错的那一行**上)。
33
27
  export interface ValidationFailure {
34
- /** 面向用户的消息(已去掉 schemastery 拼的 `$…` 位置前缀)。 */
28
+ // 面向用户的消息(已去掉 schemastery 拼的 `$…` 位置前缀)。
35
29
  message: string;
36
- /** 具体路径(段根起);空数组表示错误在整段上。 */
30
+ // 具体路径(段根起);空数组表示错误在整段上。
37
31
  path: readonly string[];
38
32
  }
39
33
 
40
- /**
41
- * 把校验器抛出的错误收成一条失败。
42
- *
43
- * schemastery 的 `ValidationError` 把路径留在 `options.path` 上(消息里的 `$…` 前缀是给命令行看的),所以这里
44
- * 取路径、去前缀——行内报错据此知道挂在哪一行。
45
- * @param error - 校验器抛出的错误。
46
- * @returns 消息与路径。
47
- */
34
+ // 把校验器抛出的错误收成一条失败:路径取 `options.path`,并去掉 schemastery 拼的 `$…` 位置前缀。
48
35
  export function failureOf(error: unknown): ValidationFailure {
49
36
  const raw = error instanceof Error ? error.message : String(error);
50
37
  const options = (error as { options?: { path?: readonly (string | number | symbol)[] } }).options;
@@ -58,51 +45,49 @@ export function failureOf(error: unknown): ValidationFailure {
58
45
  return { message: message === "" ? raw : message, path };
59
46
  }
60
47
 
61
- /** 一字段的草稿。 */
48
+ // 一字段的草稿。
62
49
  type DraftEdit =
63
- /** 控件已经给出合法值(开关、选择器、数字步进、增删项)。 */
50
+ // 控件已经给出合法值(开关、选择器、数字步进、增删项)。
64
51
  | { kind: "value"; value: unknown }
65
- /** 文本输入:`parse` 在保存时跑,解析失败挡住保存。 */
52
+ // 文本输入:`parse` 在保存时跑,解析失败挡住保存。
66
53
  | { kind: "text"; text: string; parse: TextParse }
67
- /** 恢复默认:把字段交回组成层(用户层没有它时不算写)。 */
54
+ // 恢复默认:把字段交回组成层(用户层没有它时不算写)。
68
55
  | { kind: "clear" }
69
- /** 移除:无条件 unset(数组项与字典键,host 语义是删掉这一项)。 */
56
+ // 移除:无条件 unset(数组项与字典键,host 语义是删掉这一项)。
70
57
  | { kind: "drop" };
71
58
 
72
- /** 一字段渲染时要读的状态。 */
59
+ // 一字段渲染时要读的状态。
73
60
  export interface DraftFieldState {
74
- /** 控件要显示的值:草稿优先,否则生效层。 */
61
+ // 控件要显示的值:草稿优先,否则生效层。
75
62
  value: unknown;
76
- /** 文本草稿(文本控件显示它而不是重新格式化值)。 */
63
+ // 文本草稿(文本控件显示它而不是重新格式化值)。
77
64
  text: string | undefined;
78
- /** 草稿不合法时的消息(控件显示,同时挡住保存)。 */
65
+ // 草稿不合法时的消息(控件显示,同时挡住保存)。
79
66
  invalid: string | undefined;
80
- /** 保存后这一字段是否会留下用户层条目。 */
67
+ // 保存后这一字段是否会留下用户层条目。
81
68
  overridden: boolean;
82
- /** 这一字段在本页有还没保存的编辑(页面上给它一处高亮与一个「撤回」)。 */
69
+ // 这一字段在本页有还没保存的编辑(页面上给它一处高亮与一个「撤回」)。
83
70
  staged: boolean;
84
71
  }
85
72
 
86
- /** 模型要读要写的那个命名空间。 */
73
+ // 模型要读要写的那个命名空间。
87
74
  export type DraftScope = ConfigForm<Section>;
88
75
 
89
- /** 构造参数。 */
76
+ // 构造参数。
90
77
  export interface SchemaDraftOptions {
91
78
  scope: DraftScope;
92
- /** 段根的字段树(`projectNode` 的结果):增删项要按它取节点默认值。 */
79
+ // 段根的字段树(`projectNode` 的结果):增删项要按它取节点默认值。
93
80
  root: FieldNode;
94
- /** 整段校验:返回失败(消息 + 位置),或 `undefined`。 */
81
+ // 整段校验:返回失败(消息 + 位置),或 `undefined`。
95
82
  validate: (value: Section) => ValidationFailure | undefined;
96
83
  }
97
84
 
98
- /** 一条草稿编辑要写的 op(`undefined` 表示这条草稿不该产生写)。 */
85
+ // 一条草稿编辑要写的 op(`undefined` 表示这条草稿不该产生写)。
99
86
  interface PlannedWrite {
100
87
  op: SettingsPathOpView | undefined;
101
88
  }
102
89
 
103
- /**
104
- * 一页配置的草稿。一个命名空间一个实例:它订阅共享配置表单,投影给渲染层读。
105
- */
90
+ // 一页配置的草稿。一个命名空间一个实例:它订阅共享配置表单,投影给渲染层读。
106
91
  export class SchemaDraftModel {
107
92
  readonly #scope: DraftScope;
108
93
  #root: FieldNode;
@@ -115,9 +100,6 @@ export class SchemaDraftModel {
115
100
  #failed = false;
116
101
  #violation: ValidationFailure | undefined;
117
102
 
118
- /**
119
- * @param options - 本页的共享配置表单、字段树与整段校验。
120
- */
121
103
  constructor(options: SchemaDraftOptions) {
122
104
  this.#scope = options.scope;
123
105
  this.#root = options.root;
@@ -127,22 +109,12 @@ export class SchemaDraftModel {
127
109
  });
128
110
  }
129
111
 
130
- /**
131
- * 换一份字段树。
132
- *
133
- * 增删项要按它取节点默认值,而 schema 可能随 describe 变(行的 Config 改了)——所以控制器每次投影都同步一次,
134
- * 否则加进来的项会取不到默认值。
135
- * @param root - 段根的字段树。
136
- */
112
+ // 换一份字段树:schema 可能随 describe 变,控制器每次投影同步一次(增删项按它取节点默认值)。
137
113
  setRoot(root: FieldNode): void {
138
114
  this.#root = root;
139
115
  }
140
116
 
141
- /**
142
- * 观察草稿与读数的变化。
143
- * @param listener - 每次投影都调一次的回调。
144
- * @returns 取消订阅。
145
- */
117
+ // 观察草稿与读数的变化;返回取消订阅。
146
118
  subscribe(listener: () => void): () => void {
147
119
  this.#listeners.add(listener);
148
120
  return () => {
@@ -150,11 +122,7 @@ export class SchemaDraftModel {
150
122
  };
151
123
  }
152
124
 
153
- /**
154
- * 投影一份读数给渲染层。
155
- * @param project - 用模型的当前读数造渲染要的状态。
156
- * @returns 渲染层按选择器读的 store。
157
- */
125
+ // 投影一份读数给渲染层;返回渲染层按选择器读的 store。
158
126
  bind<S>(project: () => S): SnapshotStore<S> {
159
127
  const store = createSnapshotStore(project());
160
128
  this.subscribe(() => {
@@ -163,10 +131,7 @@ export class SchemaDraftModel {
163
131
  return store;
164
132
  }
165
133
 
166
- /**
167
- * 表单级状态(与上游 `SettingsForm` 的壳同形)。
168
- * @returns 可用性、可写性、脏、非法、保存中、上次保存失败。
169
- */
134
+ // 表单级状态(与上游 `SettingsForm` 的壳同形):可用性、可写性、脏、非法、保存中、上次保存失败。
170
135
  shell(): SettingsFormShell {
171
136
  const { writes, blocked } = this.#plan();
172
137
  return {
@@ -179,23 +144,12 @@ export class SchemaDraftModel {
179
144
  };
180
145
  }
181
146
 
182
- /**
183
- * 保存前的整段校验(保存被 schema 挡下时非空)。
184
- * @returns 失败消息与它的位置,或 `undefined`。
185
- */
147
+ // 保存前的整段校验(保存被 schema 挡下时非空)。
186
148
  violation(): ValidationFailure | undefined {
187
149
  return this.#violation;
188
150
  }
189
151
 
190
- /**
191
- * 读一个字段的草稿状态。
192
- * @param path - 具体路径(动态段已按真实键或索引替换)。
193
- * @param preview - 本轮已经算好的读数(`preview()` 的结果)。
194
- *
195
- * **容器的值要跟着别处的草稿走**:判别式 union 的选支就是容器的值,若这里读生效层,`type` 的切换控件会显示旧
196
- * 的那一支(字段树却已经换了)。所以有 `preview` 就用它——控制器一次投影里只算一份。
197
- * @returns 显示值、文本草稿、非法消息、是否覆盖。
198
- */
152
+ // 读一个字段的草稿状态:容器值要跟着别处的草稿走(判别式 union 的选支就是容器值),所以传了 `preview` 就用它。
199
153
  field(path: readonly string[], preview?: Section): DraftFieldState {
200
154
  const snapshot = this.#scope.getSnapshot();
201
155
  const staged = this.#staged.get(pathKey(path));
@@ -247,37 +201,22 @@ export class SchemaDraftModel {
247
201
  }
248
202
  }
249
203
 
250
- /**
251
- * 暂存一个合法值。
252
- * @param path - 具体路径。
253
- * @param value - 控件给出的值。
254
- */
204
+ // 暂存一个合法值。
255
205
  set(path: readonly string[], value: unknown): void {
256
206
  this.#stage(path, { kind: "value", value });
257
207
  }
258
208
 
259
- /**
260
- * 暂存一段文本,保存时才解析。
261
- * @param path - 具体路径。
262
- * @param text - 用户输入的原文。
263
- * @param parse - 解析规则;失败时给出消息并挡住保存。
264
- */
209
+ // 暂存一段文本,保存时才解析(解析失败给消息并挡住保存)。
265
210
  setText(path: readonly string[], text: string, parse: TextParse): void {
266
211
  this.#stage(path, { kind: "text", text, parse });
267
212
  }
268
213
 
269
- /**
270
- * 暂存「恢复默认」。
271
- * @param path - 具体路径。
272
- */
214
+ // 暂存「恢复默认」。
273
215
  clear(path: readonly string[]): void {
274
216
  this.#stage(path, { kind: "clear" });
275
217
  }
276
218
 
277
- /**
278
- * 追加一个数组项,值取该项节点的默认值。
279
- * @param path - 数组字段的具体路径。
280
- */
219
+ // 追加一个数组项,值取该项节点的默认值。
281
220
  appendItem(path: readonly string[]): void {
282
221
  const node = seekNode(this.#root, path);
283
222
  if (node?.type !== "array") return;
@@ -286,22 +225,12 @@ export class SchemaDraftModel {
286
225
  this.set([...path, String(index)], emptyValueOf(node.item));
287
226
  }
288
227
 
289
- /**
290
- * 移除一个数组项。
291
- * @param path - 数组字段的具体路径。
292
- * @param index - 项在数组里的位置。
293
- */
228
+ // 移除一个数组项。
294
229
  removeItem(path: readonly string[], index: number): void {
295
230
  this.#stage([...path, String(index)], { kind: "drop" });
296
231
  }
297
232
 
298
- /**
299
- * 给一层加一项,值取它的默认值。
300
- *
301
- * 字典认任意键;对象只认 schema 声明过的字段(写一个没声明的键会被 host 挡下,所以这里直接不写)。
302
- * @param path - 容器的具体路径。
303
- * @param key - 要加的键。
304
- */
233
+ // 给一层加一项(值取它的默认值):字典认任意键,对象只认 schema 声明过的字段(没声明的键 host 会挡下,直接不写)。
305
234
  addKey(path: readonly string[], key: string): void {
306
235
  const node = seekNode(this.#root, path);
307
236
  if (node === undefined) return;
@@ -314,19 +243,12 @@ export class SchemaDraftModel {
314
243
  this.set([...path, key], emptyValueOf(field));
315
244
  }
316
245
 
317
- /**
318
- * 删一个字典键。
319
- * @param path - 字典字段的具体路径。
320
- * @param key - 要删的键。
321
- */
246
+ // 删一个字典键。
322
247
  removeKey(path: readonly string[], key: string): void {
323
248
  this.#stage([...path, key], { kind: "drop" });
324
249
  }
325
250
 
326
- /**
327
- * 撤销一个字段的草稿(回到它当前的生效值)——行内编辑取消时用。
328
- * @param path - 具体路径。
329
- */
251
+ // 撤销一个字段的草稿(回到它当前的生效值)——行内编辑取消时用。
330
252
  revert(path: readonly string[]): void {
331
253
  if (!this.#staged.delete(pathKey(path))) return;
332
254
  this.#failed = false;
@@ -334,7 +256,7 @@ export class SchemaDraftModel {
334
256
  this.#publish();
335
257
  }
336
258
 
337
- /** 丢弃全部草稿,回到 host 的读数。 */
259
+ // 丢弃全部草稿,回到 host 的读数。
338
260
  discard(): void {
339
261
  if (this.#staged.size === 0 && !this.#failed && this.#violation === undefined) return;
340
262
  this.#staged.clear();
@@ -344,13 +266,8 @@ export class SchemaDraftModel {
344
266
  this.#publish();
345
267
  }
346
268
 
347
- /**
348
- * 把全部草稿写成一次带 revision 栅栏的提交。
349
- *
350
- * host 是「值是否被接受」的唯一权威(有些约束 schema 表达不出来),所以落盘与否读回来决定,而不是在这里
351
- * 预测;没落盘的保存保留草稿,让用户改而不是重打。
352
- * @returns 写完与读回之后的收束。
353
- */
269
+ // 把全部草稿写成一次带 revision 栅栏的提交:落盘与否读回来决定(host 是值是否被接受的唯一权威),
270
+ // 没落盘的保存保留草稿让用户改,而不是在这里预测。
354
271
  async save(): Promise<void> {
355
272
  const snapshot = this.#scope.getSnapshot();
356
273
  if (this.#saving || !snapshot.writable) return;
@@ -387,13 +304,13 @@ export class SchemaDraftModel {
387
304
  }
388
305
  }
389
306
 
390
- /** 释放对共享配置表单的订阅。 */
307
+ // 释放对共享配置表单的订阅。
391
308
  dispose(): void {
392
309
  this.#unsubscribe();
393
310
  this.#listeners.clear();
394
311
  }
395
312
 
396
- /** 每条草稿这一轮会写什么;被 schema 挡下的草稿没有 op,但仍然是脏的。 */
313
+ // 每条草稿这一轮会写什么;被 schema 挡下的草稿没有 op,但仍然是脏的。
397
314
  #plan(): { writes: PlannedWrite[]; blocked: boolean } {
398
315
  const snapshot = this.#scope.getSnapshot();
399
316
  const writes: PlannedWrite[] = [];
@@ -423,7 +340,7 @@ export class SchemaDraftModel {
423
340
  return { writes, blocked };
424
341
  }
425
342
 
426
- /** 生效值套上本轮草稿后的段值:字段树按它展开(追加的数组项立刻出现),整段校验也看它。 */
343
+ // 生效值套上本轮草稿后的段值:字段树按它展开(追加的数组项立刻出现),整段校验也看它。
427
344
  preview(): Section {
428
345
  const ops = this.#plan().writes.flatMap((item) => (item.op === undefined ? [] : [item.op]));
429
346
  return applyOps(this.#scope.getSnapshot().value ?? {}, ops);
@@ -442,7 +359,7 @@ export class SchemaDraftModel {
442
359
  }
443
360
  }
444
361
 
445
- /** 深比较:值相等就不写(避免把「改回原值」也当成一次写)。 */
362
+ // 深比较:值相等就不写(避免把「改回原值」也当成一次写)。
446
363
  function sameValue(left: unknown, right: unknown): boolean {
447
364
  if (Object.is(left, right)) return true;
448
365
  if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
@@ -450,12 +367,12 @@ function sameValue(left: unknown, right: unknown): boolean {
450
367
  return JSON.stringify(left) === JSON.stringify(right);
451
368
  }
452
369
 
453
- /** 路径的 map 键。 */
370
+ // 路径的 map 键。
454
371
  function pathKey(path: readonly string[]): string {
455
372
  return JSON.stringify(path);
456
373
  }
457
374
 
458
- /** 路径末段是否在用户层里存在(覆盖判据)。 */
375
+ // 路径末段是否在用户层里存在(覆盖判据)。
459
376
  function hasPath(root: unknown, path: readonly string[]): boolean {
460
377
  if (path.length === 0) return root !== undefined;
461
378
  const parent = readPath(root, path.slice(0, -1));
@@ -465,7 +382,7 @@ function hasPath(root: unknown, path: readonly string[]): boolean {
465
382
  return leaf in parent;
466
383
  }
467
384
 
468
- /** 把一组 op 套到一个段值上(本地读数用,不写 host)。 */
385
+ // 把一组 op 套到一个段值上(本地读数用,不写 host)。
469
386
  function applyOps(section: Section, ops: readonly SettingsPathOpView[]): Section {
470
387
  const next = structuredClone(section);
471
388
  for (const op of ops) {
@@ -483,5 +400,5 @@ function applyOps(section: Section, ops: readonly SettingsPathOpView[]): Section
483
400
  return next;
484
401
  }
485
402
 
486
- /** 一个新子节点的初值:字段树的读法在 `schema-node.ts`(变体切换也要它)。 */
403
+ // 一个新子节点的初值:字段树的读法在 `schema-node.ts`(变体切换也要它)。
487
404
  export { emptyValueOf } from "./schema-node.ts";
@@ -1,8 +1,6 @@
1
- /**
2
- * 字段默认值与文本解析:默认值就是行内值(`value.tsx`),这里只剩"什么文本算这个字段接受的值"的规则。
3
- *
4
- * 行式视图里值在行内点开编辑,编辑的文本交给草稿模型、保存那一刻才按这里的规则解析。
5
- */
1
+ // 字段默认值与文本解析:默认值就是行内值(`value.tsx`),这里只剩"什么文本算这个字段接受的值"的规则。
2
+ //
3
+ // 行式视图里值在行内点开编辑,编辑的文本交给草稿模型、保存那一刻才按这里的规则解析。
6
4
 
7
5
  export {
8
6
  InlineValue,
@@ -16,15 +14,8 @@ import type { TextParse } from "./draft.ts";
16
14
  import type { FieldNode } from "./schema-node.ts";
17
15
  import type { SchemaFormTranslate } from "./slot-contract.ts";
18
16
 
19
- /**
20
- * 各类型的文本解析规则(消息来自本包字典)。
21
- *
22
- * 解析不只看类型:schema 上声明的界限(`pattern` / `min` / `max` / `step`)与字面量集合都在这里说话——
23
- * 用户敲完当场就知道对不对,而不是等保存时被整段校验退回。
24
- * @param node - 字段节点(`meta` 是约束的来源)。
25
- * @param t - 本包字典。
26
- * @returns 这段文本的解析规则。
27
- */
17
+ // 各类型的文本解析规则(消息来自本包字典):schema 上声明的界限(`pattern` / `min` / `max` / `step`)与字面量集合
18
+ // 也在这里判,用户敲完当场就知道对不对,而不是等保存时被整段校验退回。
28
19
  export function parseFor(node: FieldNode, t: SchemaFormTranslate): TextParse {
29
20
  switch (node.type) {
30
21
  case "number":