@dsh-workbench/shared 0.3.4 → 0.3.6

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 (193) hide show
  1. package/dist/api/blocker-table.d.ts +13 -0
  2. package/dist/api/blocker-table.js +13 -0
  3. package/dist/api/blocker.d.ts +6 -0
  4. package/dist/api/context-ref.d.ts +31 -0
  5. package/dist/api/context-ref.js +28 -0
  6. package/dist/api/dsh-client.d.ts +99 -0
  7. package/dist/api/dsh.d.ts +55 -3
  8. package/dist/api/facts.d.ts +42 -0
  9. package/dist/api/facts.js +33 -0
  10. package/dist/api/host-write.d.ts +18 -0
  11. package/dist/api/host-write.js +12 -0
  12. package/dist/api/long-task.d.ts +66 -1
  13. package/dist/api/long-task.js +59 -0
  14. package/dist/api/unit-board.d.ts +25 -0
  15. package/dist/api/unit-board.js +16 -0
  16. package/dist/api/unit-declaration.d.ts +36 -0
  17. package/dist/api/unit-declaration.js +24 -0
  18. package/dist/api/unit.d.ts +32 -0
  19. package/dist/api/unit.js +5 -0
  20. package/dist/api/workbench-client.d.ts +25 -0
  21. package/dist/api/workbench-client.js +13 -0
  22. package/dist/api/workbench-contract.d.ts +12 -0
  23. package/dist/components/Activity.d.ts +33 -0
  24. package/dist/components/Activity.js +24 -0
  25. package/dist/components/ApprovalDetail.d.ts +12 -0
  26. package/dist/components/ApprovalDetail.js +9 -0
  27. package/dist/components/ApprovalPlan.d.ts +14 -0
  28. package/dist/components/ApprovalPlan.js +8 -0
  29. package/dist/components/Card.d.ts +13 -0
  30. package/dist/components/Card.js +10 -0
  31. package/dist/components/CardAnchor.d.ts +14 -0
  32. package/dist/components/CardAnchor.js +11 -0
  33. package/dist/components/CardBoundary.d.ts +20 -0
  34. package/dist/components/CardBoundary.js +14 -0
  35. package/dist/components/CardHead.d.ts +13 -0
  36. package/dist/components/CardHead.js +10 -0
  37. package/dist/components/ChoiceGroup.d.ts +24 -0
  38. package/dist/components/ChoiceGroup.js +12 -0
  39. package/dist/components/Field.d.ts +10 -0
  40. package/dist/components/Field.js +7 -0
  41. package/dist/components/Form.css +6 -0
  42. package/dist/components/Form.d.ts +20 -0
  43. package/dist/components/Form.js +58 -0
  44. package/dist/components/FormActions.d.ts +14 -0
  45. package/dist/components/FormActions.js +5 -0
  46. package/dist/components/FormNodes.d.ts +103 -0
  47. package/dist/components/FormNodes.js +153 -0
  48. package/dist/components/FormRows.css +31 -0
  49. package/dist/components/FormRows.d.ts +20 -0
  50. package/dist/components/FormRows.js +14 -0
  51. package/dist/components/FormTypes.d.ts +59 -0
  52. package/dist/components/FormTypes.js +1 -0
  53. package/dist/components/Link.d.ts +12 -0
  54. package/dist/components/Link.js +9 -0
  55. package/dist/components/LongTaskRow.css +52 -0
  56. package/dist/components/LongTaskRow.d.ts +35 -0
  57. package/dist/components/LongTaskRow.js +45 -4
  58. package/dist/components/Note.d.ts +11 -0
  59. package/dist/components/Note.js +8 -0
  60. package/dist/components/ResourceStates.d.ts +25 -0
  61. package/dist/components/ResourceStates.js +22 -0
  62. package/dist/components/Rows.d.ts +17 -0
  63. package/dist/components/Rows.js +11 -0
  64. package/dist/components/SaveBar.d.ts +12 -0
  65. package/dist/components/SaveBar.js +9 -0
  66. package/dist/components/SelectField.d.ts +26 -0
  67. package/dist/components/SelectField.js +22 -0
  68. package/dist/components/SelectFieldNodes.d.ts +24 -0
  69. package/dist/components/SelectFieldNodes.js +24 -0
  70. package/dist/components/SettingsSection.d.ts +11 -0
  71. package/dist/components/SettingsSection.js +8 -0
  72. package/dist/components/Spinner.d.ts +9 -0
  73. package/dist/components/Spinner.js +6 -0
  74. package/dist/components/Stack.d.ts +8 -0
  75. package/dist/components/Stack.js +5 -0
  76. package/dist/components/StateBlock.d.ts +13 -0
  77. package/dist/components/StateBlock.js +10 -0
  78. package/dist/components/TextField.d.ts +8 -0
  79. package/dist/components/TextField.js +5 -0
  80. package/dist/components/board-module.d.ts +30 -0
  81. package/dist/components/icons.d.ts +38 -0
  82. package/dist/components/icons.js +35 -0
  83. package/dist/config/constants.d.ts +42 -0
  84. package/dist/config/constants.js +42 -0
  85. package/dist/config/storage.d.ts +4 -0
  86. package/dist/config/storage.js +4 -0
  87. package/dist/host/long-task.d.ts +7 -0
  88. package/dist/host/long-task.js +31 -5
  89. package/dist/host/tool-approval.d.ts +1 -0
  90. package/dist/host/unit-face.d.ts +6 -1
  91. package/dist/i18n/en.d.ts +1 -0
  92. package/dist/i18n/en.js +10 -8
  93. package/dist/i18n/index.d.ts +13 -9
  94. package/dist/i18n/index.js +1 -0
  95. package/dist/i18n/merge.d.ts +5 -0
  96. package/dist/i18n/merge.js +2 -0
  97. package/dist/i18n/zh.d.ts +11 -8
  98. package/dist/i18n/zh.js +10 -8
  99. package/dist/index.d.ts +6 -2
  100. package/dist/index.js +6 -2
  101. package/dist/lib/blocker-action.d.ts +22 -0
  102. package/dist/lib/blocker-action.js +13 -0
  103. package/dist/lib/board-fact.d.ts +27 -0
  104. package/dist/lib/board-fact.js +18 -0
  105. package/dist/lib/busy.d.ts +10 -0
  106. package/dist/lib/busy.js +7 -0
  107. package/dist/lib/diy-debug.d.ts +42 -2
  108. package/dist/lib/diy-debug.js +19 -1
  109. package/dist/lib/diy-record.d.ts +41 -0
  110. package/dist/lib/diy-record.js +17 -0
  111. package/dist/lib/form-problem.d.ts +10 -0
  112. package/dist/lib/form-problem.js +10 -0
  113. package/dist/lib/panel-state.d.ts +60 -0
  114. package/dist/lib/panel-state.js +30 -0
  115. package/dist/lib/react.d.ts +39 -0
  116. package/dist/lib/react.js +36 -0
  117. package/dist/lib/text.d.ts +22 -0
  118. package/dist/lib/text.js +22 -0
  119. package/dist/lib/ui.d.ts +50 -0
  120. package/dist/lib/ui.js +41 -0
  121. package/package.json +1 -1
  122. package/src/api/__tests__/long-task.test.ts +41 -1
  123. package/src/api/blocker-table.ts +13 -0
  124. package/src/api/blocker.ts +6 -0
  125. package/src/api/context-ref.ts +31 -0
  126. package/src/api/dsh-client.ts +99 -0
  127. package/src/api/dsh.ts +56 -1
  128. package/src/api/facts.ts +42 -0
  129. package/src/api/host-write.ts +18 -0
  130. package/src/api/long-task.ts +79 -1
  131. package/src/api/unit-board.ts +25 -0
  132. package/src/api/unit-declaration.ts +36 -0
  133. package/src/api/unit.ts +32 -0
  134. package/src/api/workbench-client.ts +25 -0
  135. package/src/api/workbench-contract.ts +12 -0
  136. package/src/components/Activity.tsx +33 -0
  137. package/src/components/ApprovalDetail.tsx +12 -0
  138. package/src/components/ApprovalPlan.tsx +14 -0
  139. package/src/components/Card.tsx +13 -0
  140. package/src/components/CardAnchor.tsx +14 -0
  141. package/src/components/CardBoundary.tsx +20 -0
  142. package/src/components/CardHead.tsx +13 -0
  143. package/src/components/ChoiceGroup.tsx +24 -0
  144. package/src/components/Field.tsx +10 -0
  145. package/src/components/Form.css +6 -0
  146. package/src/components/Form.test.tsx +475 -0
  147. package/src/components/Form.tsx +78 -0
  148. package/src/components/FormActions.tsx +14 -0
  149. package/src/components/FormNodes.tsx +193 -0
  150. package/src/components/FormRows.css +31 -0
  151. package/src/components/FormRows.test.tsx +71 -1
  152. package/src/components/FormRows.tsx +20 -0
  153. package/src/components/FormTypes.ts +65 -0
  154. package/src/components/FormWiring.test.tsx +88 -0
  155. package/src/components/Link.tsx +12 -0
  156. package/src/components/LongTaskRow.css +52 -0
  157. package/src/components/LongTaskRow.tsx +76 -2
  158. package/src/components/Note.tsx +11 -0
  159. package/src/components/ResourceStates.tsx +25 -0
  160. package/src/components/Rows.tsx +17 -0
  161. package/src/components/SaveBar.tsx +12 -0
  162. package/src/components/SelectField.tsx +26 -0
  163. package/src/components/SelectFieldNodes.tsx +24 -0
  164. package/src/components/SettingsSection.tsx +11 -0
  165. package/src/components/Spinner.tsx +9 -0
  166. package/src/components/Stack.tsx +8 -0
  167. package/src/components/StateBlock.tsx +13 -0
  168. package/src/components/TextField.tsx +8 -0
  169. package/src/components/__tests__/long-task-row.test.ts +115 -0
  170. package/src/components/board-module.ts +30 -0
  171. package/src/components/icons.tsx +38 -0
  172. package/src/config/constants.ts +43 -0
  173. package/src/config/storage.ts +4 -0
  174. package/src/host/__tests__/long-task.test.ts +264 -0
  175. package/src/host/long-task.ts +43 -7
  176. package/src/host/tool-approval.ts +1 -0
  177. package/src/host/unit-face.ts +7 -1
  178. package/src/i18n/en.ts +11 -8
  179. package/src/i18n/index.ts +3 -0
  180. package/src/i18n/merge.ts +5 -0
  181. package/src/i18n/zh.ts +11 -8
  182. package/src/index.ts +6 -2
  183. package/src/lib/blocker-action.ts +22 -0
  184. package/src/lib/board-fact.ts +27 -0
  185. package/src/lib/busy.ts +10 -0
  186. package/src/lib/diy-debug.test.ts +2 -1
  187. package/src/lib/diy-debug.ts +44 -3
  188. package/src/lib/diy-record.ts +41 -0
  189. package/src/lib/form-problem.ts +10 -0
  190. package/src/lib/panel-state.ts +60 -0
  191. package/src/lib/react.ts +39 -0
  192. package/src/lib/text.ts +22 -0
  193. package/src/lib/ui.ts +50 -0
@@ -0,0 +1,103 @@
1
+ import type { ReactNode } from '../lib/react.js';
2
+ import type { FormAction } from './FormActions.js';
3
+ import type { FormField, FormValue } from './FormTypes.js';
4
+ /**
5
+ * 只剩要显示的那些格子 —— `visible: false` 的当它不存在:不渲染、不校验、不进 `values`,也不参与找第一处错误。
6
+ * @param fields - 表单的字段清单。
7
+ * @returns 保序的新数组,`visible` 不给或为 `true` 的都在;全不可见时给空数组。
8
+ */
9
+ export declare function visibleFieldsOf(fields: readonly FormField[]): readonly FormField[];
10
+ /**
11
+ * 找出装配错的键:**假值**的键(空串 / `undefined` / `null`,运行期真漏给 `key` 就是这条)每次出现都算一处,
12
+ * 以及同一张表单里出现两次的键 —— 这两样都会让 `id` 撞车、`values` 少一格。
13
+ * @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
14
+ * @returns 出问题的键的清单、按首次出现顺序去重;缺失的键一律记成空串(报的时候印成 `(空 key)`),空表给空数组。
15
+ */
16
+ export declare function duplicateKeysOf(fields: readonly FormField[]): readonly string[];
17
+ /**
18
+ * 装配错了要当场诊断:`key` 缺失或重复时给 `console.error` —— 但照渲不误,不 throw。
19
+ * 形式与理由同 `FormActions` 里那颗多出来的 `primary`:装配的小错不该让 `CardBoundary` 把用户的整张卡变空白。
20
+ * @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
21
+ */
22
+ export declare function reportDuplicateKeys(fields: readonly FormField[]): void;
23
+ /**
24
+ * 把每格的 `value` 收成一张键值表交给 `onSubmit` —— `Form` 自己一个值都不存,这张表就是从 `fields` 现取的快照。
25
+ * @param fields - 表单的字段清单;取的就是它们的 `value`,`value` 是数组的那格(多选)原样带出、不复制。
26
+ * @returns 键为字段 `key`、值为该格 `value` 的表;`fields` 为空时给空表,不是 `undefined`。
27
+ */
28
+ export declare function formValuesOf(fields: readonly FormField[]): Readonly<Record<string, FormValue>>;
29
+ /**
30
+ * 算每格现在该不该报错:还没提交过(`show` 为 false)时一格都不报 —— 不边打字边骂人。
31
+ * `validate` 返回**真值**才算错,假值(`undefined` / `null` / `false` / 空串)一律当"这格没错" ——
32
+ * 所以 `v.trim() === '' && '必填'` 这种用 `&&` 算文案的写法是对的。
33
+ * @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
34
+ * @param show - 提交过没有;false 时全给 `undefined`,true 时逐格调 `field.validate` 并按真值收。
35
+ * @returns 键为 `key`、值为那句文案或 `undefined` 的表;没给 `validate` 的格子也是 `undefined`。
36
+ */
37
+ export declare function formProblemsOf(fields: readonly FormField[], show: boolean): Readonly<Record<string, ReactNode | undefined>>;
38
+ /**
39
+ * 按字段顺序取第一处错误 —— 提交被挡住时要带进视口的就是这一格。
40
+ * 判的是**真值**(与 `formProblemsOf` 同一口径):假值那种"错误文案"渲染出来本来就是空的,不算错。
41
+ * @param fields - 表单的字段清单,顺序就是「第一处」的判据。
42
+ * @param problems - `formProblemsOf` 那张表,也可以是调用点自己拼的(同样按真值判)。
43
+ * @returns 第一格有错的字段;一格都没错时给 `undefined`。
44
+ */
45
+ export declare function formFirstProblemOf(fields: readonly FormField[], problems: Readonly<Record<string, ReactNode | undefined>>): FormField | undefined;
46
+ /**
47
+ * 提交那一拍的结果:有错就挡住(并点出第一处错在哪一格),没错就把键值表交出去。
48
+ */
49
+ export type FormSubmitOutcome = {
50
+ readonly blocked: true;
51
+ readonly first: FormField;
52
+ } | {
53
+ readonly blocked: false;
54
+ readonly values: Readonly<Record<string, FormValue>>;
55
+ };
56
+ /**
57
+ * 算提交那一拍该干什么 —— `Form` 的提交钮点下去、以及 `<form>` 的 Enter 提交,走的都是它。
58
+ * 错的判据只有一份(`formProblemsOf` + `formFirstProblemOf`),这里不重算第二遍。
59
+ * @param fields - 要提交的那些格子(通常是调用点先 `visibleFieldsOf` 过滤过的,不可见那格不进 `values` 也不算错)。
60
+ * @returns `blocked: true` 时带上是按字段顺序数第一格有错的字段(拿它去 `revealFirstProblem`);
61
+ * `blocked: false` 时带上从 `fields` 现取的键值表。空表一定给 `blocked: false` 与空 values。
62
+ */
63
+ export declare function formSubmitOf(fields: readonly FormField[]): FormSubmitOutcome;
64
+ /**
65
+ * 组表单的操作区:调用点给的其余动作在最左,取消在中间,提交永远是最右那颗 `primary`。
66
+ * @param args.extra - 其余动作(default / danger),保持调用点给的顺序;不给就是没有。
67
+ * @param args.cancel - 取消那格的 `label` 与 `onClick`;不给就少一颗钮。
68
+ * @param args.submit - 提交钮的 `label` 与 `busyLabel`。
69
+ * @param args.busy - 提交中:提交钮带转圈并换 `busyLabel`(不给就沿用 `label`),`extra` 与取消一起禁用。
70
+ * @param args.disabled - 只再禁提交钮(如没改动可存),其余动作不受影响。
71
+ * @param args.onSubmitClick - 提交钮点下去做什么,通常是把这一拍交给 `Form` 的提交处理。
72
+ * @returns 给 `FormActions` 的动作清单;没给 `extra` 也没给 `cancel` 时只剩提交那一颗。
73
+ */
74
+ export declare function formActionsOf(args: {
75
+ readonly extra: readonly FormAction[] | undefined;
76
+ readonly cancel: {
77
+ readonly label: ReactNode;
78
+ readonly onClick: () => void;
79
+ } | undefined;
80
+ readonly submit: {
81
+ readonly label: ReactNode;
82
+ readonly busyLabel?: ReactNode | undefined;
83
+ };
84
+ readonly busy: boolean;
85
+ readonly disabled: boolean;
86
+ readonly onSubmitClick: () => void;
87
+ }): readonly FormAction[];
88
+ /**
89
+ * 把整张表单的行画出来 —— 每格一个 `FormRow`,且**直接**是 `FormRows` 的孩子。
90
+ * 中间不许再包一层:`FormRow` 一旦不是 `.wb-formRows` 的网格项,`FormRows.css` 里那条 `subgrid` 就不匹配,
91
+ * 每行各自算列宽,控件左沿就参差了。
92
+ * @param args.fields - 要画的格子(通常是已经过滤过可见性的那份),顺序就是行的顺序。
93
+ * @param args.problems - `formProblemsOf` 那张表,决定哪一格显示错误文案(按**真值**判,假值不当错)。
94
+ * @param args.showProblem - 这一拍该不该显示错误(还没提交过时为 false,一格都不显示)。
95
+ * @param args.busy - 提交中:每格的绑定 `disabled` 为真。
96
+ * @returns `FormRow` 组成的数组;没有可画的格子时给空数组。
97
+ */
98
+ export declare function formRowsOf(args: {
99
+ readonly fields: readonly FormField[];
100
+ readonly problems: Readonly<Record<string, ReactNode | undefined>>;
101
+ readonly showProblem: boolean;
102
+ readonly busy: boolean;
103
+ }): readonly ReactNode[];
@@ -0,0 +1,153 @@
1
+ import { h } from '../lib/react.js';
2
+ import { FormRow } from './FormRows.js';
3
+ /**
4
+ * 只剩要显示的那些格子 —— `visible: false` 的当它不存在:不渲染、不校验、不进 `values`,也不参与找第一处错误。
5
+ * @param fields - 表单的字段清单。
6
+ * @returns 保序的新数组,`visible` 不给或为 `true` 的都在;全不可见时给空数组。
7
+ */
8
+ export function visibleFieldsOf(fields) {
9
+ return fields.filter((field) => field.visible !== false);
10
+ }
11
+ /**
12
+ * 找出装配错的键:**假值**的键(空串 / `undefined` / `null`,运行期真漏给 `key` 就是这条)每次出现都算一处,
13
+ * 以及同一张表单里出现两次的键 —— 这两样都会让 `id` 撞车、`values` 少一格。
14
+ * @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
15
+ * @returns 出问题的键的清单、按首次出现顺序去重;缺失的键一律记成空串(报的时候印成 `(空 key)`),空表给空数组。
16
+ */
17
+ export function duplicateKeysOf(fields) {
18
+ const seen = new Set();
19
+ const duplicated = new Set();
20
+ for (const field of fields) {
21
+ const key = field.key;
22
+ if (!key) {
23
+ duplicated.add('');
24
+ continue;
25
+ }
26
+ if (seen.has(key))
27
+ duplicated.add(key);
28
+ seen.add(key);
29
+ }
30
+ return [...duplicated];
31
+ }
32
+ /**
33
+ * 装配错了要当场诊断:`key` 缺失或重复时给 `console.error` —— 但照渲不误,不 throw。
34
+ * 形式与理由同 `FormActions` 里那颗多出来的 `primary`:装配的小错不该让 `CardBoundary` 把用户的整张卡变空白。
35
+ * @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
36
+ */
37
+ export function reportDuplicateKeys(fields) {
38
+ const duplicated = duplicateKeysOf(fields);
39
+ if (duplicated.length === 0)
40
+ return;
41
+ const named = duplicated.map((key) => (key === '' ? '(空 key)' : key));
42
+ console.error(`[dsh-workbench] Form 里有重复或缺失的字段 key:${named.join('、')} —— ` +
43
+ '同一张表单里 key 必须唯一且非空(它决定 id 与 values 的键),否则 id 会撞车、values 会少一格');
44
+ }
45
+ /**
46
+ * 把每格的 `value` 收成一张键值表交给 `onSubmit` —— `Form` 自己一个值都不存,这张表就是从 `fields` 现取的快照。
47
+ * @param fields - 表单的字段清单;取的就是它们的 `value`,`value` 是数组的那格(多选)原样带出、不复制。
48
+ * @returns 键为字段 `key`、值为该格 `value` 的表;`fields` 为空时给空表,不是 `undefined`。
49
+ */
50
+ export function formValuesOf(fields) {
51
+ const values = {};
52
+ for (const field of fields)
53
+ values[field.key] = field.value;
54
+ return values;
55
+ }
56
+ /**
57
+ * 算每格现在该不该报错:还没提交过(`show` 为 false)时一格都不报 —— 不边打字边骂人。
58
+ * `validate` 返回**真值**才算错,假值(`undefined` / `null` / `false` / 空串)一律当"这格没错" ——
59
+ * 所以 `v.trim() === '' && '必填'` 这种用 `&&` 算文案的写法是对的。
60
+ * @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
61
+ * @param show - 提交过没有;false 时全给 `undefined`,true 时逐格调 `field.validate` 并按真值收。
62
+ * @returns 键为 `key`、值为那句文案或 `undefined` 的表;没给 `validate` 的格子也是 `undefined`。
63
+ */
64
+ export function formProblemsOf(fields, show) {
65
+ const values = formValuesOf(fields);
66
+ const problems = {};
67
+ for (const field of fields) {
68
+ const found = show ? field.validate?.(field.value, values) : undefined;
69
+ problems[field.key] = found ? found : undefined;
70
+ }
71
+ return problems;
72
+ }
73
+ /**
74
+ * 按字段顺序取第一处错误 —— 提交被挡住时要带进视口的就是这一格。
75
+ * 判的是**真值**(与 `formProblemsOf` 同一口径):假值那种"错误文案"渲染出来本来就是空的,不算错。
76
+ * @param fields - 表单的字段清单,顺序就是「第一处」的判据。
77
+ * @param problems - `formProblemsOf` 那张表,也可以是调用点自己拼的(同样按真值判)。
78
+ * @returns 第一格有错的字段;一格都没错时给 `undefined`。
79
+ */
80
+ export function formFirstProblemOf(fields, problems) {
81
+ return fields.find((field) => Boolean(problems[field.key]));
82
+ }
83
+ /**
84
+ * 算提交那一拍该干什么 —— `Form` 的提交钮点下去、以及 `<form>` 的 Enter 提交,走的都是它。
85
+ * 错的判据只有一份(`formProblemsOf` + `formFirstProblemOf`),这里不重算第二遍。
86
+ * @param fields - 要提交的那些格子(通常是调用点先 `visibleFieldsOf` 过滤过的,不可见那格不进 `values` 也不算错)。
87
+ * @returns `blocked: true` 时带上是按字段顺序数第一格有错的字段(拿它去 `revealFirstProblem`);
88
+ * `blocked: false` 时带上从 `fields` 现取的键值表。空表一定给 `blocked: false` 与空 values。
89
+ */
90
+ export function formSubmitOf(fields) {
91
+ const problems = formProblemsOf(fields, true);
92
+ const first = formFirstProblemOf(fields, problems);
93
+ if (first !== undefined)
94
+ return { blocked: true, first };
95
+ return { blocked: false, values: formValuesOf(fields) };
96
+ }
97
+ /**
98
+ * 组表单的操作区:调用点给的其余动作在最左,取消在中间,提交永远是最右那颗 `primary`。
99
+ * @param args.extra - 其余动作(default / danger),保持调用点给的顺序;不给就是没有。
100
+ * @param args.cancel - 取消那格的 `label` 与 `onClick`;不给就少一颗钮。
101
+ * @param args.submit - 提交钮的 `label` 与 `busyLabel`。
102
+ * @param args.busy - 提交中:提交钮带转圈并换 `busyLabel`(不给就沿用 `label`),`extra` 与取消一起禁用。
103
+ * @param args.disabled - 只再禁提交钮(如没改动可存),其余动作不受影响。
104
+ * @param args.onSubmitClick - 提交钮点下去做什么,通常是把这一拍交给 `Form` 的提交处理。
105
+ * @returns 给 `FormActions` 的动作清单;没给 `extra` 也没给 `cancel` 时只剩提交那一颗。
106
+ */
107
+ export function formActionsOf(args) {
108
+ const submit = {
109
+ key: 'submit',
110
+ label: args.busy ? (args.submit.busyLabel ?? args.submit.label) : args.submit.label,
111
+ kind: 'primary',
112
+ busy: args.busy,
113
+ disabled: args.busy || args.disabled,
114
+ onClick: args.onSubmitClick,
115
+ };
116
+ const cancel = args.cancel === undefined
117
+ ? []
118
+ : [
119
+ {
120
+ key: 'cancel',
121
+ label: args.cancel.label,
122
+ kind: 'default',
123
+ disabled: args.busy,
124
+ onClick: args.cancel.onClick,
125
+ },
126
+ ];
127
+ const extra = (args.extra ?? []).map((action) => (args.busy ? { ...action, disabled: true } : action));
128
+ return [...extra, ...cancel, submit];
129
+ }
130
+ /**
131
+ * 把整张表单的行画出来 —— 每格一个 `FormRow`,且**直接**是 `FormRows` 的孩子。
132
+ * 中间不许再包一层:`FormRow` 一旦不是 `.wb-formRows` 的网格项,`FormRows.css` 里那条 `subgrid` 就不匹配,
133
+ * 每行各自算列宽,控件左沿就参差了。
134
+ * @param args.fields - 要画的格子(通常是已经过滤过可见性的那份),顺序就是行的顺序。
135
+ * @param args.problems - `formProblemsOf` 那张表,决定哪一格显示错误文案(按**真值**判,假值不当错)。
136
+ * @param args.showProblem - 这一拍该不该显示错误(还没提交过时为 false,一格都不显示)。
137
+ * @param args.busy - 提交中:每格的绑定 `disabled` 为真。
138
+ * @returns `FormRow` 组成的数组;没有可画的格子时给空数组。
139
+ */
140
+ export function formRowsOf(args) {
141
+ return args.fields.map((field) => h(FormRow, {
142
+ key: field.key,
143
+ label: field.label,
144
+ hint: field.hint,
145
+ problem: args.showProblem ? args.problems[field.key] : undefined,
146
+ layout: field.layout,
147
+ }, field.control({
148
+ id: `wb-form-${field.key}`,
149
+ name: field.key,
150
+ invalid: args.showProblem && Boolean(args.problems[field.key]),
151
+ disabled: args.busy || field.disabled === true,
152
+ })));
153
+ }
@@ -24,6 +24,10 @@
24
24
  gap: var(--wb-len-6);
25
25
  }
26
26
 
27
+ .wb-formRows > .wb-formRow[data-layout='stacked'] {
28
+ grid-template-columns: minmax(0, 1fr);
29
+ }
30
+
27
31
  .wb-formLabel {
28
32
  min-width: 0;
29
33
  font-size: var(--dsw-font-xs-13-font-size);
@@ -36,12 +40,39 @@
36
40
  white-space: normal;
37
41
  }
38
42
 
43
+ .wb-formRow[data-layout='stacked'] .wb-formHint,
44
+ .wb-formRow[data-layout='stacked'] .wb-formProblem {
45
+ grid-column: 1 / -1;
46
+ }
47
+
39
48
  .wb-formControl {
40
49
  display: flex;
41
50
  align-items: center;
42
51
  min-width: 0;
43
52
  }
44
53
 
54
+ .wb-formControl
55
+ > :where(
56
+ .wb-selectAnchor,
57
+ .wb-choiceMenu,
58
+ input:not(
59
+ [type='checkbox'],
60
+ [type='radio'],
61
+ [type='button'],
62
+ [type='submit'],
63
+ [type='reset'],
64
+ [type='image'],
65
+ [type='color'],
66
+ [type='range']
67
+ ),
68
+ select,
69
+ textarea
70
+ ) {
71
+ box-sizing: border-box;
72
+ width: 100%;
73
+ min-width: 0;
74
+ }
75
+
45
76
  .wb-formHint {
46
77
  grid-column: 2;
47
78
  font-size: var(--dsw-font-xxs-12-font-size);
@@ -1,9 +1,20 @@
1
1
  import type { ReactNode } from '../lib/react.js';
2
2
  import './FormRows.css';
3
+ /**
4
+ * `FormRows` 的契约:一列 `FormRow` 的容器。
5
+ */
3
6
  export interface FormRowsProps {
4
7
  readonly children?: ReactNode | undefined;
5
8
  }
9
+ /**
10
+ * 表单行的容器:同一份 `FormRows` 共用一个标签列(列宽由最长的标签定),行距也归它。
11
+ * @param props.children - 一串 `FormRow`;非行的孩子(如分组标题)自动通栏。
12
+ * @returns 竖排的 div。
13
+ */
6
14
  export declare function FormRows({ children }: FormRowsProps): JSX.Element;
15
+ /**
16
+ * `FormRow` 的契约:标签、提示、错误与控件的排布。
17
+ */
7
18
  export interface FormRowProps {
8
19
  readonly label: ReactNode;
9
20
  readonly hint?: ReactNode | undefined;
@@ -11,4 +22,13 @@ export interface FormRowProps {
11
22
  readonly layout?: 'inline' | 'stacked' | undefined;
12
23
  readonly children?: ReactNode | undefined;
13
24
  }
25
+ /**
26
+ * 表单一行:左边标签、右边控件,提示与错误占控件下面那一格。
27
+ * @param props.label - 这一行的标签;进了这一行,控件自己就别再给 `label`,否则会多一个标签。
28
+ * @param props.hint - 控件下的说明;给了 `problem` 就不画它 —— 同一格不叠两段话。
29
+ * @param props.problem - 校验失败那句话;给了就顶掉 `hint` 并给整行加上出错标记。
30
+ * @param props.layout - `stacked` 才是标签在上、控件在下(默认 `inline`:标签在左)。
31
+ * @param props.children - 控件本体。
32
+ * @returns 一个 `<label>`。
33
+ */
14
34
  export declare function FormRow({ label, hint, problem, layout, children }: FormRowProps): JSX.Element;
@@ -1,8 +1,22 @@
1
1
  import { h } from '../lib/react.js';
2
2
  import './FormRows.css';
3
+ /**
4
+ * 表单行的容器:同一份 `FormRows` 共用一个标签列(列宽由最长的标签定),行距也归它。
5
+ * @param props.children - 一串 `FormRow`;非行的孩子(如分组标题)自动通栏。
6
+ * @returns 竖排的 div。
7
+ */
3
8
  export function FormRows({ children }) {
4
9
  return h("div", { className: "wb-formRows" }, children);
5
10
  }
11
+ /**
12
+ * 表单一行:左边标签、右边控件,提示与错误占控件下面那一格。
13
+ * @param props.label - 这一行的标签;进了这一行,控件自己就别再给 `label`,否则会多一个标签。
14
+ * @param props.hint - 控件下的说明;给了 `problem` 就不画它 —— 同一格不叠两段话。
15
+ * @param props.problem - 校验失败那句话;给了就顶掉 `hint` 并给整行加上出错标记。
16
+ * @param props.layout - `stacked` 才是标签在上、控件在下(默认 `inline`:标签在左)。
17
+ * @param props.children - 控件本体。
18
+ * @returns 一个 `<label>`。
19
+ */
6
20
  export function FormRow({ label, hint, problem, layout = 'inline', children }) {
7
21
  return (h("label", { className: "wb-formRow", "data-layout": layout, "data-wb-problem": problem === undefined ? undefined : '' },
8
22
  h("span", { className: "wb-formLabel" }, label),
@@ -0,0 +1,59 @@
1
+ import type { ReactNode } from '../lib/react.js';
2
+ import type { FormAction } from './FormActions.js';
3
+ /**
4
+ * 一格控件的值:单值(`TextField` / `SelectField` / `ChoiceGroup` 单选),或一组值(`ChoiceGroup` 多选)。
5
+ */
6
+ export type FormValue = string | readonly string[];
7
+ /**
8
+ * `Form` 递给控件那一格的绑定:`id` / `name` / `invalid` / `disabled` 由 `Form` 定,控件只负责接上。
9
+ */
10
+ export interface FormFieldBinding {
11
+ readonly id: string;
12
+ readonly name: string;
13
+ readonly invalid: boolean;
14
+ readonly disabled: boolean;
15
+ }
16
+ /**
17
+ * 一格字段的配置:键、标签、值、怎么算错(`validate`)、控件本身 —— 排布、时机、阻断都不写在这里,那些归 `Form`。
18
+ * `validate` 是纯函数、同步、只读 `values`:返回**真值**才算这一格错,假值(`undefined` / `null` / `false` / 空串)
19
+ * 一律当"没错"—— 所以 `(value) => value.trim() === '' && '必填'` 这种用 `&&` 算文案的写法是对的,
20
+ * 它的返回值就是错误行那句话。
21
+ */
22
+ export interface FormField {
23
+ readonly key: string;
24
+ readonly label: ReactNode;
25
+ readonly hint?: ReactNode | undefined;
26
+ readonly layout?: 'inline' | 'stacked' | undefined;
27
+ readonly value: FormValue;
28
+ readonly visible?: boolean | undefined;
29
+ readonly disabled?: boolean | undefined;
30
+ readonly validate?: (value: FormValue, values: Readonly<Record<string, FormValue>>) => ReactNode | undefined;
31
+ readonly control: (binding: FormFieldBinding) => ReactNode;
32
+ }
33
+ /**
34
+ * 提交钮:文案归调用点,位置、禁用与忙态归 `Form`。
35
+ */
36
+ export interface FormSubmit {
37
+ readonly label: ReactNode;
38
+ readonly busyLabel?: ReactNode | undefined;
39
+ }
40
+ /**
41
+ * 取消钮:文案与点下去做什么,`Form` 把它排在提交钮左边。
42
+ */
43
+ export interface FormCancel {
44
+ readonly label: ReactNode;
45
+ readonly onClick: () => void;
46
+ }
47
+ /**
48
+ * `Form` 的契约:fields 配置进,排布、校验时机、提交阻断、操作区全归组件。
49
+ */
50
+ export interface FormProps {
51
+ readonly fields: readonly FormField[];
52
+ readonly onSubmit: (values: Readonly<Record<string, FormValue>>) => void;
53
+ readonly submit: FormSubmit;
54
+ readonly cancel?: FormCancel | undefined;
55
+ readonly extra?: readonly FormAction[] | undefined;
56
+ readonly busy?: boolean | undefined;
57
+ readonly disabled?: boolean | undefined;
58
+ readonly problem?: ReactNode | undefined;
59
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,8 @@
1
1
  import type { ReactNode } from '../lib/react.js';
2
2
  import './Link.css';
3
+ /**
4
+ * `Link` 的契约:目标地址、文案与外链开关。
5
+ */
3
6
  export interface LinkProps {
4
7
  readonly href: string;
5
8
  readonly children?: ReactNode | undefined;
@@ -7,4 +10,13 @@ export interface LinkProps {
7
10
  readonly external?: boolean | undefined;
8
11
  readonly className?: string | undefined;
9
12
  }
13
+ /**
14
+ * 唯一的外链写法:自带新窗口、`rel="noopener noreferrer"`、正文色与行尾那颗小箭头。
15
+ * @param props.href - 目标地址,必给。
16
+ * @param props.children - 链接文字(别自己再带 `→`)。
17
+ * @param props.title - 原生 title 提示;不给就不写这个属性。
18
+ * @param props.external - 默认 true:新窗口打开并画那颗 ↗;站内跳转给 false,箭头也一起不画。
19
+ * @param props.className - 追加在基础类名之后,不是替换。
20
+ * @returns 一个 `<a>`。
21
+ */
10
22
  export declare function Link({ href, children, title, external, className }: LinkProps): JSX.Element;
@@ -1,6 +1,15 @@
1
1
  import { h } from '../lib/react.js';
2
2
  import { IconRightUpOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';
3
3
  import './Link.css';
4
+ /**
5
+ * 唯一的外链写法:自带新窗口、`rel="noopener noreferrer"`、正文色与行尾那颗小箭头。
6
+ * @param props.href - 目标地址,必给。
7
+ * @param props.children - 链接文字(别自己再带 `→`)。
8
+ * @param props.title - 原生 title 提示;不给就不写这个属性。
9
+ * @param props.external - 默认 true:新窗口打开并画那颗 ↗;站内跳转给 false,箭头也一起不画。
10
+ * @param props.className - 追加在基础类名之后,不是替换。
11
+ * @returns 一个 `<a>`。
12
+ */
4
13
  export function Link({ href, children, title, external = true, className }) {
5
14
  return (h("a", { className: className === undefined ? 'wb-link' : `wb-link ${className}`, href: href, ...(title === undefined ? {} : { title }), ...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {}) },
6
15
  children,
@@ -45,4 +45,56 @@
45
45
  margin: var(--wb-len-4) 0 0;
46
46
  max-height: var(--wb-len-220);
47
47
  overflow: auto;
48
+ }
49
+
50
+ .wb-longTaskStop {
51
+ display: inline-flex;
52
+ flex: none;
53
+ align-items: center;
54
+ align-self: center;
55
+ justify-content: center;
56
+ width: var(--wb-len-20);
57
+ height: var(--wb-len-20);
58
+ padding: 0;
59
+ margin-left: auto;
60
+ border: 0.5px solid var(--dsw-alias-border-l2);
61
+ border-radius: var(--dsw-radius-sm);
62
+ background: var(--dsw-specific-menu);
63
+ color: var(--dsw-alias-label-secondary);
64
+ cursor: pointer;
65
+ }
66
+
67
+ .wb-longTaskStop:hover:not(:disabled),
68
+ .wb-longTaskStop:focus-visible {
69
+ border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);
70
+ color: var(--dsw-alias-state-error-primary);
71
+ }
72
+
73
+ .wb-longTaskStop:disabled {
74
+ cursor: default;
75
+ }
76
+
77
+ .wb-longTaskStopArmed,
78
+ .wb-longTaskStopArmed:hover:not(:disabled),
79
+ .wb-longTaskStopArmed:focus-visible {
80
+ gap: var(--wb-len-4);
81
+ width: auto;
82
+ padding: 0 var(--wb-len-7);
83
+ border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);
84
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);
85
+ color: var(--dsw-alias-state-error-primary);
86
+ }
87
+
88
+ .wb-longTaskStopLabel {
89
+ white-space: nowrap;
90
+ font-size: var(--dsw-font-xxxs-11-font-size);
91
+ line-height: var(--wb-line-18);
92
+ }
93
+
94
+ .wb-longTaskStopWhy {
95
+ margin: 0;
96
+ font-size: var(--dsw-font-xxxs-11-font-size);
97
+ line-height: var(--wb-line-16);
98
+ color: var(--dsw-alias-state-error-primary);
99
+ overflow-wrap: anywhere;
48
100
  }
@@ -1,9 +1,29 @@
1
1
  import { type LongTaskStatus } from '../api/long-task.js';
2
2
  import './LongTaskRow.css';
3
+ /**
4
+ * 长任务行尾的一个链接:地址与文字。
5
+ */
3
6
  export interface LongTaskRowLink {
4
7
  readonly href: string;
5
8
  readonly text: string;
6
9
  }
10
+ /**
11
+ * `LongTaskRow` 那颗两按停止钮的契约:确认之后真发的那一下、三句词、在飞与失败两态。
12
+ * `label` 是没上膛时那颗钮的 aria-label(dsh 的词:`停止任务 {label}`);`askLabel` 是上膛时那颗钮上显示的字(`确认停止`);
13
+ * `armedLabel` 是上膛时的 aria-label,不给就用 `askLabel`(`再次点击确认停止`);`busy` = 这一趟正在被停、钮禁用;
14
+ * `failed` 给了才在问题那行后面再画一行。
15
+ */
16
+ export interface LongTaskRowStop {
17
+ readonly onStop: () => void;
18
+ readonly label: string;
19
+ readonly askLabel: string;
20
+ readonly armedLabel?: string | undefined;
21
+ readonly busy?: boolean | undefined;
22
+ readonly failed?: string | null | undefined;
23
+ }
24
+ /**
25
+ * `LongTaskRow` 的契约:状态、标题、问题、链接,两处可展开的原始输出,与最右边那颗可选的停止钮。
26
+ */
7
27
  export interface LongTaskRowProps {
8
28
  readonly status: LongTaskStatus;
9
29
  readonly headline: string;
@@ -15,5 +35,20 @@ export interface LongTaskRowProps {
15
35
  readonly rawTitle?: string | undefined;
16
36
  readonly copyLabel?: string | undefined;
17
37
  readonly copiedLabel?: string | undefined;
38
+ readonly stop?: LongTaskRowStop | undefined;
18
39
  }
40
+ /**
41
+ * 长任务那一行:状态点 + 标题 + 链接,跑失败时把步骤与原始输出收在两处可展开里。
42
+ * 发布、扫描、迁移这类跑得久的事都用它,别自己画进度。
43
+ * @param props.status - `longTaskDot` 认的五态之一,决定状态点颜色与要不要画"失败"那两块。
44
+ * @param props.headline - 这一行在干什么(一句话)。
45
+ * @param props.problem - 失败原因;不给不画。
46
+ * @param props.links - 行尾的链接,通常指向产物或日志。
47
+ * @param props.steps - 步骤文本;只在失败时画,且要同时给 `stepsTitle` 才成一块可展开。
48
+ * @param props.raw - 原始输出;只在失败时画,且要同时给 `rawTitle` 才成一块可展开。
49
+ * @param props.stepsTitle - 步骤那块展开行的标题;`rawTitle` 同理是原始输出那块。
50
+ * @param props.copyLabel - 复制钮文案:给了才画复制钮;`copiedLabel` 是复制成功后那句。
51
+ * @param props.stop - 给了才在最右边画那颗两按停止钮;**不给就一个像素都不变**。只在生产方认 `signal` 时给。
52
+ * @returns 一行长任务。
53
+ */
19
54
  export declare function LongTaskRow(props: LongTaskRowProps): JSX.Element;
@@ -1,24 +1,65 @@
1
- import { longTaskDot } from '../api/long-task.js';
1
+ import { longTaskDot, longTaskStopPress, longTaskStopStage, LONG_TASK_STOP_ARM_MS } from '../api/long-task.js';
2
2
  import { Link } from './Link.js';
3
- import { Fragment, h, useState } from '../lib/react.js';
4
- import { CodeBlock, DisclosureRow, IconCodeOutlineRegular, IconFlatListOutlineRegular, StateDot, } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { Fragment, h, useEffect, useState } from '../lib/react.js';
4
+ import { CodeBlock, DisclosureRow, IconCodeOutlineRegular, IconFlatListOutlineRegular, IconStopFillRegular, StateDot, } from '@deepseek-ai/dsh-client-ui-primitives';
5
5
  import './LongTaskRow.css';
6
6
  function RowLink({ href, text }) {
7
7
  return (h(Link, { href: href, className: "wb-longTaskLink" }, text));
8
8
  }
9
+ /**
10
+ * 长任务那一行:状态点 + 标题 + 链接,跑失败时把步骤与原始输出收在两处可展开里。
11
+ * 发布、扫描、迁移这类跑得久的事都用它,别自己画进度。
12
+ * @param props.status - `longTaskDot` 认的五态之一,决定状态点颜色与要不要画"失败"那两块。
13
+ * @param props.headline - 这一行在干什么(一句话)。
14
+ * @param props.problem - 失败原因;不给不画。
15
+ * @param props.links - 行尾的链接,通常指向产物或日志。
16
+ * @param props.steps - 步骤文本;只在失败时画,且要同时给 `stepsTitle` 才成一块可展开。
17
+ * @param props.raw - 原始输出;只在失败时画,且要同时给 `rawTitle` 才成一块可展开。
18
+ * @param props.stepsTitle - 步骤那块展开行的标题;`rawTitle` 同理是原始输出那块。
19
+ * @param props.copyLabel - 复制钮文案:给了才画复制钮;`copiedLabel` 是复制成功后那句。
20
+ * @param props.stop - 给了才在最右边画那颗两按停止钮;**不给就一个像素都不变**。只在生产方认 `signal` 时给。
21
+ * @returns 一行长任务。
22
+ */
9
23
  export function LongTaskRow(props) {
10
24
  const [stepsOpen, setStepsOpen] = useState(false);
11
25
  const [rawOpen, setRawOpen] = useState(false);
26
+ const [armed, setArmed] = useState(false);
12
27
  const failed = props.status === 'failed';
13
28
  const problem = props.problem ?? '';
14
29
  const raw = (props.raw ?? '').trim();
15
30
  const steps = props.steps ?? [];
31
+ const stop = props.stop;
32
+ const busy = stop?.busy === true;
33
+ const stopWhy = stop?.failed ?? '';
34
+ useEffect(() => {
35
+ if (!armed)
36
+ return undefined;
37
+ const timer = setTimeout(() => setArmed(false), LONG_TASK_STOP_ARM_MS);
38
+ return () => clearTimeout(timer);
39
+ }, [armed]);
40
+ const onStopPress = () => {
41
+ if (stop === undefined)
42
+ return;
43
+ const press = longTaskStopPress(longTaskStopStage({ armed, busy }));
44
+ if (press === 'ignore')
45
+ return;
46
+ if (press === 'arm') {
47
+ setArmed(true);
48
+ return;
49
+ }
50
+ setArmed(false);
51
+ stop.onStop();
52
+ };
16
53
  return (h("div", { className: "wb-longTask" },
17
54
  h("p", { className: "wb-longTaskHead" },
18
55
  h(StateDot, { state: longTaskDot(props.status), size: 10 }),
19
56
  h("span", { className: "wb-longTaskHeadline" }, props.headline),
20
- (props.links ?? []).map((link) => (h(RowLink, { href: link.href, key: `${link.text}@${link.href}`, text: link.text })))),
57
+ (props.links ?? []).map((link) => (h(RowLink, { href: link.href, key: `${link.text}@${link.href}`, text: link.text }))),
58
+ stop === undefined ? null : (h("button", { type: "button", className: `wb-longTaskStop${armed ? ' wb-longTaskStopArmed' : ''}`, "data-stop-state": longTaskStopStage({ armed, busy }), "aria-label": armed ? stop.armedLabel ?? stop.askLabel : stop.label, disabled: busy, onClick: onStopPress, onBlur: () => setArmed(false) },
59
+ h(IconStopFillRegular, { size: 12 }),
60
+ armed ? h("span", { className: "wb-longTaskStopLabel" }, stop.askLabel) : null))),
21
61
  problem === '' ? null : h("p", { className: "wb-longTaskProblem" }, problem),
62
+ stopWhy === '' ? null : h("p", { className: "wb-longTaskStopWhy" }, stopWhy),
22
63
  !failed ? null : (h(Fragment, null,
23
64
  steps.length === 0 || props.stepsTitle === undefined ? null : (h(DisclosureRow, { className: "wb-longTaskDump", icon: h(IconFlatListOutlineRegular, { size: 14 }), title: props.stepsTitle, open: stepsOpen, expandable: true, expandOnRowClick: true, previewChevron: true, onToggle: () => setStepsOpen(!stepsOpen) },
24
65
  h(CodeBlock, { code: steps.join('\n'), className: "wb-longTaskDump", showHeader: props.copyLabel !== undefined, copyLabel: props.copyLabel ?? '', copiedLabel: props.copiedLabel ?? '' }))),