@morlay/dsh-client-ui-primitives 0.0.6 → 0.0.8

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 (76) hide show
  1. package/README.md +31 -6
  2. package/dist/client.d.mts +186 -11175
  3. package/dist/client.mjs +3730 -0
  4. package/locale/en.json +1 -1
  5. package/locale/zh.json +1 -1
  6. package/package.json +23 -34
  7. package/src/client/controls/Bubble.module.css +17 -0
  8. package/src/client/controls/Bubble.tsx +17 -0
  9. package/src/client/controls/Button.module.css +10 -0
  10. package/src/client/controls/Button.tsx +29 -0
  11. package/src/client/controls/DisclosureRow.module.css +22 -0
  12. package/src/client/controls/DisclosureRow.tsx +20 -0
  13. package/src/client/controls/IconButton.module.css +50 -0
  14. package/src/client/controls/IconButton.tsx +45 -0
  15. package/src/client/controls/ModelRouteList.module.css +100 -0
  16. package/src/client/controls/ModelRouteList.tsx +118 -0
  17. package/src/client/controls/MultilineField.module.css +26 -0
  18. package/src/client/controls/MultilineField.tsx +19 -0
  19. package/src/client/controls/SearchInput.module.css +11 -0
  20. package/src/client/controls/SearchInput.tsx +22 -0
  21. package/src/client/controls/SearchSelect.module.css +34 -0
  22. package/src/client/controls/SearchSelect.tsx +138 -0
  23. package/src/client/controls/SeatButton.module.css +53 -0
  24. package/src/client/controls/SeatButton.tsx +40 -0
  25. package/src/client/controls/Spinner.module.css +18 -0
  26. package/src/client/controls/Spinner.tsx +14 -0
  27. package/src/client/controls/Tabs.module.css +48 -0
  28. package/src/client/controls/Tabs.tsx +57 -0
  29. package/src/client/controls/TagInput.module.css +105 -0
  30. package/src/client/controls/TagInput.tsx +152 -0
  31. package/src/client/controls/TimeLabel.module.css +16 -0
  32. package/src/client/controls/TimeLabel.tsx +15 -0
  33. package/src/client/controls/VisuallyHidden.module.css +9 -0
  34. package/src/client/controls/VisuallyHidden.tsx +16 -0
  35. package/src/client/controls/classes.ts +7 -0
  36. package/src/client/controls/index.ts +30 -0
  37. package/src/client/controls/tags.ts +24 -0
  38. package/src/client/index.ts +17 -9
  39. package/src/client/layout/Row.tsx +86 -0
  40. package/src/client/layout/Stack.tsx +79 -0
  41. package/src/client/layout/Text.tsx +54 -0
  42. package/src/client/layout/index.ts +16 -0
  43. package/src/client/layout/layout.module.css +217 -0
  44. package/src/client/layout/scales.ts +91 -0
  45. package/src/client/schema-form/Editor.tsx +61 -70
  46. package/src/client/schema-form/SchemaForm.tsx +4 -4
  47. package/src/client/schema-form/TruncatedTooltip.tsx +60 -0
  48. package/src/client/schema-form/controller.ts +5 -2
  49. package/src/client/schema-form/geometry.ts +3 -0
  50. package/src/client/schema-form/index.ts +11 -0
  51. package/src/client/schema-form/schema-form.module.css +335 -0
  52. package/src/client/schema-form/testing/resize-observer-stub.ts +63 -0
  53. package/src/client/schema-form/value.tsx +18 -15
  54. package/src/client/settings-form/SettingsFieldRow.module.css +93 -0
  55. package/src/client/settings-form/SettingsFieldRow.tsx +103 -0
  56. package/src/client/settings-form/index.ts +5 -18
  57. package/src/css-modules.d.ts +6 -0
  58. package/dist/client.cjs +0 -17075
  59. package/dist/client.d.cts +0 -11656
  60. package/dist/index.d.cts +0 -5
  61. package/dist/reference-BX54bj6_.d.cts +0 -30
  62. package/src/client/schema-form/styles.ts +0 -298
  63. package/src/client/settings-form/SettingsForm.styles.ts +0 -58
  64. package/src/client/settings-form/SettingsForm.tsx +0 -68
  65. package/src/client/settings-form/fields.styles.ts +0 -158
  66. package/src/client/settings-form/fields.tsx +0 -184
  67. package/src/client/settings-form/form-model.ts +0 -339
  68. package/src/client/styling/css.ts +0 -27
  69. package/src/client/styling/focus.ts +0 -9
  70. package/src/client/styling/index.ts +0 -6
  71. package/src/client/styling/styled.tsx +0 -54
  72. package/src/client/styling/styling.ts +0 -137
  73. package/src/client/styling/token.ts +0 -275
  74. package/src/client/styling/toolkit.ts +0 -37
  75. package/src/client/theme.generated.ts +0 -833
  76. package/src/client/theme.ts +0 -5
@@ -0,0 +1,335 @@
1
+ /* 行式编辑器的样式:等宽、行号、语法色、悬停行为——全部消费官方 `--dsw-*` token。
2
+ 几何只有一套:行高 22px、行内间距 6px、动作键 20px、折叠列 14px、行号列 32px,行与行之间不各自决定。 */
3
+
4
+ .editorRoot {
5
+ display: flex;
6
+ flex-direction: column;
7
+ width: 100%;
8
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
9
+ font-size: 14px;
10
+ line-height: 22px;
11
+ color: var(--dsw-alias-label-primary);
12
+ }
13
+
14
+ /* 一行:行号 + 折进 + 行内容。悬停给一层浅底。 */
15
+ .lineRow {
16
+ display: flex;
17
+ /* 顶部对齐:行里出现输入框或多行值时,内容不跟着上下跳。 */
18
+ align-items: flex-start;
19
+ gap: 6px;
20
+ min-height: 22px;
21
+ padding-right: 8px;
22
+ white-space: nowrap;
23
+ }
24
+
25
+ .lineRow:hover {
26
+ background: var(--dsw-alias-bg-layer-2);
27
+ }
28
+
29
+ /* 选中一行(点行号):整行留一层底色,便于对着行号找内容。 */
30
+ .lineRow[data-selected='true'] {
31
+ background: var(--dsw-alias-bg-layer-3);
32
+ }
33
+
34
+ /* 本页改过还没保存:左边一道强调色,和「已经存进用户层」的浅灰区分开。 */
35
+ .lineRow[data-dirty='true'] {
36
+ box-shadow: inset 2px 0 0 var(--dsw-alias-brand-primary);
37
+ }
38
+
39
+ .lineRow[data-overridden='true'] {
40
+ box-shadow: inset 2px 0 0 var(--dsw-elevation-stroke-color);
41
+ }
42
+
43
+ /* 改过的字段名用强调色(存没存看左边那道条);出错时红优先。 */
44
+ .lineRow[data-dirty='true'] [data-role='key'] {
45
+ color: var(--dsw-alias-brand-primary);
46
+ }
47
+
48
+ .lineRow[data-overridden='true'] [data-role='key'] {
49
+ color: var(--dsw-alias-brand-primary);
50
+ }
51
+
52
+ /* 有问题的行:半透明红底 + 左侧一道实红条,字段名与注释位那句都是红字。 */
53
+ .lineRow[data-invalid='true'] {
54
+ background: color-mix(in srgb, var(--dsw-alias-state-error-secondary) 12%, transparent);
55
+ box-shadow: inset 2px 0 0 var(--dsw-alias-state-error-primary);
56
+ }
57
+
58
+ .lineRow[data-invalid='true'] [data-role='key'] {
59
+ color: var(--dsw-alias-state-error-primary);
60
+ }
61
+
62
+ /* 行内的行为按钮(撤回 / 恢复默认 / 复制 / 移除):平时透明,悬停这一行才显形。 */
63
+ .lineRow:hover [data-role='actions'] {
64
+ opacity: 1;
65
+ }
66
+
67
+ /* 一行的内容区:**缩进只作用在这里**——行号列固定在左边,缩进不推它。 */
68
+ .lineBody {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 6px;
72
+ flex: 1;
73
+ min-width: 0;
74
+ min-height: 22px;
75
+ }
76
+
77
+ /* 行号列:右对齐、不可选、淡色。 */
78
+ .lineNumber {
79
+ flex: none;
80
+ width: 32px;
81
+ line-height: 22px;
82
+ text-align: right;
83
+ color: var(--dsw-alias-label-caption);
84
+ user-select: none;
85
+ cursor: pointer;
86
+ }
87
+
88
+ /* 折叠 chevron(官方箭头图标,尺寸与别的行内图标同源)。 */
89
+ .lineFold {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ flex: none;
94
+ width: 14px;
95
+ height: 22px;
96
+ padding: 0;
97
+ border: 0;
98
+ background: none;
99
+ color: var(--dsw-alias-label-caption);
100
+ cursor: pointer;
101
+ }
102
+
103
+ .lineFold:hover {
104
+ color: var(--dsw-alias-label-secondary);
105
+ }
106
+
107
+ /* 叶子行的折叠占位(保持列对齐)。 */
108
+ .lineFoldSpacer {
109
+ flex: none;
110
+ width: 14px;
111
+ }
112
+
113
+ /* 键名(配置键原样;数组下标淡一些)。 */
114
+ .lineKey {
115
+ flex: none;
116
+ color: var(--dsw-alias-label-primary);
117
+ }
118
+
119
+ .lineKey[data-index='true'] {
120
+ color: var(--dsw-alias-label-caption);
121
+ }
122
+
123
+ /* 结构符与标点(`{` `}` `[` `]` `:`)。 */
124
+ .lineToken {
125
+ flex: none;
126
+ color: var(--dsw-alias-label-caption);
127
+ }
128
+
129
+ /* 值(token):字符串 / 数字 / 布尔各有语法色。
130
+ **必须是块级盒**:`overflow` / `text-overflow` 对 inline 盒不生效,inline 的 token 无论多长都不会截断,
131
+ 只会把行撑宽。 */
132
+ .lineValue {
133
+ display: block;
134
+ min-width: 0;
135
+ overflow: hidden;
136
+ text-overflow: ellipsis;
137
+ cursor: text;
138
+ color: var(--dsw-alias-state-business-primary);
139
+ }
140
+
141
+ .lineValue[data-tone='number'] {
142
+ color: var(--dsw-alias-state-warn-primary);
143
+ }
144
+
145
+ .lineValue[data-tone='boolean'] {
146
+ color: var(--dsw-alias-state-success-primary);
147
+ }
148
+
149
+ .lineValue[data-tone='empty'] {
150
+ color: var(--dsw-alias-label-caption);
151
+ }
152
+
153
+ /* 值在行里的位置(字段槽的宿主):键名与结构符都是 `flex: none`,它是行里**唯一可收缩**的那一项。
154
+ 不生长:短值保持自然宽度,长值才收缩到行尾并自己截断。 */
155
+ .valueSlot {
156
+ display: block;
157
+ flex: 0 1 auto;
158
+ min-width: 0;
159
+ overflow: hidden;
160
+ }
161
+
162
+ /* 值上的下拉触发:**它自己就是值的呈现**(不再是「值 + 一个下拉」两遍),点开才出菜单。 */
163
+ .valueTrigger {
164
+ display: inline-flex;
165
+ align-items: center;
166
+ gap: 2px;
167
+ min-width: 0;
168
+ padding: 0;
169
+ border: 0;
170
+ background: none;
171
+ font: inherit;
172
+ text-align: left;
173
+ cursor: pointer;
174
+ color: var(--dsw-alias-label-caption);
175
+ }
176
+
177
+ .valueTrigger:disabled {
178
+ cursor: default;
179
+ }
180
+
181
+ .valueTrigger:focus-visible {
182
+ outline: none;
183
+ }
184
+
185
+ /* 行内输入的外壳:官方 `Input` 压到与值同一行高,并**吃掉剩下的宽度**。
186
+ 行内编辑与容器闭合行的添加输入共用它——两处的输入框因此长得一模一样。 */
187
+ .compactInput {
188
+ display: inline-flex;
189
+ flex: 1 1 auto;
190
+ min-width: 0;
191
+ }
192
+
193
+ .compactInput > span {
194
+ width: 100%;
195
+ height: 22px;
196
+ padding: 0 6px;
197
+ border-radius: 4px;
198
+ }
199
+
200
+ .compactInput input {
201
+ font-size: 14px;
202
+ line-height: 22px;
203
+ }
204
+
205
+ /* 多行值的行内输入:官方没有多行原子,所以这里用同一套观感的字段壳(同边框、同圆角、同焦点色),
206
+ 高度跟着内容长、宽度同样撑满剩下的位置。 */
207
+ .compactTextField {
208
+ display: inline-flex;
209
+ flex: 1 1 auto;
210
+ min-width: 0;
211
+ padding: 0 6px;
212
+ border-radius: 4px;
213
+ background: var(--dsw-alias-bg-layer-1);
214
+ /* 细线走 theme 的描边(0.5px 与颜色都由 token 给)。 */
215
+ border: 0;
216
+ box-shadow: 0 0 0 0.5px var(--dsw-elevation-stroke-color);
217
+ }
218
+
219
+ .compactTextField:focus-within {
220
+ --dsw-elevation-stroke-color: var(--dsw-alias-brand-primary);
221
+ }
222
+
223
+ .compactTextField > textarea {
224
+ flex: 1;
225
+ width: 100%;
226
+ min-height: 20px;
227
+ max-height: 240px;
228
+ padding: 0;
229
+ border: 0;
230
+ outline: none;
231
+ background: transparent;
232
+ resize: none;
233
+ field-sizing: content;
234
+ font-family: inherit;
235
+ font-size: 14px;
236
+ line-height: 22px;
237
+ color: var(--dsw-alias-label-primary);
238
+ }
239
+
240
+ /* 行内注释(schema 说明与业务文案):比配置值小一档、tertiary 色,占满这一行剩下的宽度,长了就截断——
241
+ **真的被截断时**才由官方 `Tooltip` 给全文。 */
242
+ .lineComment {
243
+ flex: 1 1 auto;
244
+ min-width: 0;
245
+ overflow: hidden;
246
+ text-overflow: ellipsis;
247
+ color: var(--dsw-alias-label-tertiary);
248
+ font-family: inherit;
249
+ font-size: 12px;
250
+ }
251
+
252
+ /* 行内的校验消息(与注释放同一档字号):可收缩、自己截断、**截断了**才由官方 `Tooltip` 给全文。
253
+ `flex: 0 1 auto`(不生长):短消息别铺满整行,添加入口那一行里也不跟输入框抢剩下的宽度。 */
254
+ .lineInvalid {
255
+ flex: 0 1 auto;
256
+ min-width: 0;
257
+ overflow: hidden;
258
+ text-overflow: ellipsis;
259
+ color: var(--dsw-alias-state-error-primary);
260
+ font-family: inherit;
261
+ font-size: 12px;
262
+ }
263
+
264
+ /* 行尾的动作组:方形图标键(默认透明,悬停时显形,显形规则写在行上)。 */
265
+ .lineActions {
266
+ display: inline-flex;
267
+ align-items: center;
268
+ gap: 4px;
269
+ flex: none;
270
+ opacity: 0;
271
+ }
272
+
273
+ .lineActions > button {
274
+ display: inline-flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ width: 20px;
278
+ height: 20px;
279
+ padding: 0;
280
+ border: 0;
281
+ border-radius: 4px;
282
+ background: none;
283
+ color: var(--dsw-alias-label-secondary);
284
+ cursor: pointer;
285
+ }
286
+
287
+ .lineActions > button:hover {
288
+ background: var(--dsw-alias-bg-layer-3);
289
+ color: var(--dsw-alias-label-primary);
290
+ }
291
+
292
+ .lineActions > button:disabled {
293
+ cursor: not-allowed;
294
+ color: var(--dsw-alias-label-dimmed);
295
+ }
296
+
297
+ /* 编辑态的两个按钮常显,而且**要有底色与描边**:行底色上只有淡色图标是看不见的,
298
+ 所以这里给实体按钮(theme 的描边 + 一层底),确认用成功档、取消用中性档。 */
299
+ .hoverActions[data-editing='true'] {
300
+ opacity: 1;
301
+ gap: 6px;
302
+ }
303
+
304
+ .hoverActions[data-editing='true'] > button {
305
+ box-shadow: 0 0 0 0.5px var(--dsw-elevation-stroke-color);
306
+ background: var(--dsw-alias-bg-layer-1);
307
+ color: var(--dsw-alias-label-primary);
308
+ }
309
+
310
+ .hoverActions[data-editing='true'] > button:first-of-type {
311
+ color: var(--dsw-alias-state-success-primary);
312
+ }
313
+
314
+ /* 添加输入:**跟在容器的闭合括号同一行**(菜单锚在它上面),宽度吃掉剩下的位置。 */
315
+ .addWrap {
316
+ position: relative;
317
+ display: inline-flex;
318
+ align-items: center;
319
+ gap: 6px;
320
+ flex: 1 1 auto;
321
+ min-width: 0;
322
+ margin-left: 6px;
323
+ }
324
+
325
+ .addWrap[data-invalid='true'] input {
326
+ border-color: var(--dsw-alias-state-error-primary);
327
+ }
328
+
329
+ /* 一行说明(不可用 / 没有可生成项时用)。 */
330
+ .hint {
331
+ margin: 0;
332
+ font-size: 12px;
333
+ line-height: 1.5;
334
+ color: var(--dsw-alias-label-tertiary);
335
+ }
@@ -0,0 +1,63 @@
1
+ // jsdom 里跑官方 `Tooltip`、以及靠 `scrollWidth` / `clientWidth` 判截断的行内文本的环境替身。
2
+ //
3
+ // 官方气泡的宽度/定位来自 `ResizeObserver` 回调(`visibility` 也只在那个回调里置为 visible),而 jsdom 没有这个
4
+ // API:缺了它,hover 之后的 effect 会直接抛 `ReferenceError`。替身做两件事:注册时喂回一次 `borderBoxSize`
5
+ // (让气泡进入可见态),并给用例一个 `notifyResize()`——真机上「盒子宽度变了」是浏览器通知的,jsdom 里没有那个
6
+ // 事件,用例只能自己发一次。
7
+ //
8
+ // 它**不是**几何证据:真尺寸与定位只能在浏览器里量(本包 `.agents/standards/how-to-verify.md` 记了这条与探针的
9
+ // 用法),所以用例的判据是「hover 出气泡、气泡里是全文」,不拿替身去断言气泡的实际尺寸。
10
+
11
+ interface StubObserver {
12
+ readonly targets: Set<Element>;
13
+ readonly notify: ResizeObserverCallback;
14
+ }
15
+
16
+ const live = new Set<StubObserver>();
17
+
18
+ export function stubResizeObserver(): void {
19
+ class ResizeObserverStub {
20
+ #notify: ResizeObserverCallback;
21
+ #targets = new Set<Element>();
22
+ #live: StubObserver;
23
+
24
+ constructor(notify: ResizeObserverCallback) {
25
+ this.#notify = notify;
26
+ this.#live = { targets: this.#targets, notify };
27
+ live.add(this.#live);
28
+ }
29
+
30
+ // 注册即回调一次(真实的 ResizeObserver 也会在首帧给一次初始尺寸)。
31
+ observe(target: Element): void {
32
+ this.#targets.add(target);
33
+ this.#notify([entry(target)], this as unknown as ResizeObserver);
34
+ }
35
+
36
+ unobserve(target: Element): void {
37
+ this.#targets.delete(target);
38
+ }
39
+
40
+ disconnect(): void {
41
+ this.#targets.clear();
42
+ live.delete(this.#live);
43
+ }
44
+ }
45
+ (globalThis as { ResizeObserver?: unknown }).ResizeObserver = ResizeObserverStub;
46
+ }
47
+
48
+ // 让还活着的替身观察者再收到一次尺寸通知:用例把元素的几何改掉之后调它,等价于浏览器通知「这个盒子变了」。
49
+ export function notifyResize(): void {
50
+ for (const observer of live) {
51
+ const entries = [...observer.targets].map((target) => entry(target));
52
+ if (entries.length > 0) {
53
+ observer.notify(entries, undefined as unknown as ResizeObserver);
54
+ }
55
+ }
56
+ }
57
+
58
+ function entry(target: Element): ResizeObserverEntry {
59
+ return {
60
+ target,
61
+ borderBoxSize: [{ blockSize: 0, inlineSize: 0 }],
62
+ } as unknown as ResizeObserverEntry;
63
+ }
@@ -7,7 +7,9 @@ import { useState, type ReactNode } from "react";
7
7
  import { IconChevronDownOutlineRegular, Menu } from "@deepseek-ai/dsh-client-ui-primitives";
8
8
  import type { SelectOption } from "./hints.ts";
9
9
  import type { SchemaFieldOwnerProps } from "./slot-contract.ts";
10
- import { ICON_SIZE, LineValue, ValueTrigger } from "./styles.ts";
10
+ import { ICON_SIZE } from "./geometry.ts";
11
+ import css from "./schema-form.module.css";
12
+ import { TruncatedTooltip } from "./TruncatedTooltip.tsx";
11
13
 
12
14
  // 编辑态文本:字符串就是原文(换行照旧),其余按 JSON 写。
13
15
  export function valueText(value: unknown): string {
@@ -42,21 +44,22 @@ export function tokenTone(value: unknown): "string" | "number" | "boolean" | "em
42
44
  export function InlineValue({ owner }: { owner: SchemaFieldOwnerProps }): ReactNode {
43
45
  if (owner.options !== undefined) return <OptionSelect owner={owner} options={owner.options} />;
44
46
  if (owner.node.meta.secret) {
45
- // secret 不回显值:截断时不给全文,只给说明。
47
+ // secret 不回显值:这里显示的是「已配置」的 `••••••`,它不会截断,`owner.hint` 也不是「被截断文本的全文」
48
+ // ——按行内浮层的口径它不该弹气泡,所以回到非侵入的原行为:挂原生 `title`。
46
49
  return (
47
- <LineValue data-tone="empty" title={owner.hint}>
50
+ <span className={css.lineValue} data-tone="empty" title={owner.hint}>
48
51
  {owner.secretConfigured ? "••••••" : ""}
49
- </LineValue>
52
+ </span>
50
53
  );
51
54
  }
55
+ // 值长了会在行里截断(值槽可收缩):确实截断时整份内容由官方 `Tooltip` 承载,放得下就不挂。
56
+ const text = tokenText(owner.value);
52
57
  return (
53
- <LineValue
54
- data-tone={owner.node.readOnly === null ? tokenTone(owner.value) : "empty"}
55
- // 值长了会截断:整份内容挂在 title 上,hover 就能看全。
56
- title={tokenText(owner.value)}
57
- >
58
- {tokenText(owner.value)}
59
- </LineValue>
58
+ <TruncatedTooltip label={text}>
59
+ <span className={css.lineValue} data-tone={owner.node.readOnly === null ? tokenTone(owner.value) : "empty"}>
60
+ {text}
61
+ </span>
62
+ </TruncatedTooltip>
60
63
  );
61
64
  }
62
65
 
@@ -99,7 +102,7 @@ function OptionSelect({
99
102
  setOpen(false);
100
103
  }}
101
104
  anchor={
102
- <ValueTrigger
105
+ <button className={css.valueTrigger}
103
106
  type="button"
104
107
  disabled={owner.disabled}
105
108
  aria-label={owner.label}
@@ -107,13 +110,13 @@ function OptionSelect({
107
110
  setOpen(true);
108
111
  }}
109
112
  >
110
- <LineValue
113
+ <span className={css.lineValue}
111
114
  data-tone={current === undefined ? tokenTone(owner.value) : tokenTone(current.value)}
112
115
  >
113
116
  {label}
114
- </LineValue>
117
+ </span>
115
118
  <IconChevronDownOutlineRegular size={ICON_SIZE} />
116
- </ValueTrigger>
119
+ </button>
117
120
  }
118
121
  />
119
122
  );
@@ -0,0 +1,93 @@
1
+ /* 字段行(`SettingsFieldRow`):官方 `SettingsValueField` 的留白口径(上下各 12px),分隔线由调用方按
2
+ 「不是这一组的第一个字段」显式声明——两种排法与官方控件的留白因此完全一致。 */
3
+ .row {
4
+ display: flex;
5
+ flex-direction: column;
6
+ }
7
+
8
+ .row[data-divider='true'] {
9
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
10
+ }
11
+
12
+ /* 上下排(输入类):标签 / 控件 / 说明同列。 */
13
+ .body {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 6px;
17
+ padding: 12px 0;
18
+ }
19
+
20
+ /* 右置排(切换类):左列标签与说明,控件贴最右。 */
21
+ .inline {
22
+ display: flex;
23
+ flex-direction: row;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ gap: 16px;
27
+ padding: 12px 0;
28
+ }
29
+
30
+ .text {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: 2px;
34
+ min-width: 0;
35
+ }
36
+
37
+ .label {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 8px;
41
+ font-size: 13px;
42
+ font-weight: 500;
43
+ line-height: 1.5;
44
+ color: var(--dsw-alias-label-primary);
45
+ }
46
+
47
+ /* 覆盖标记(「已覆盖」):照官方 badge 的 neutral 那一档。 */
48
+ .badge {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ padding: 1px 8px;
52
+ border-radius: 999px;
53
+ corner-shape: round;
54
+ font-size: 11px;
55
+ line-height: 17px;
56
+ font-weight: 500;
57
+ color: var(--dsw-alias-label-secondary);
58
+ background: var(--dsw-alias-bg-module-platform);
59
+ }
60
+
61
+ /* 恢复默认的入口:文字按钮,没有边框与底色。 */
62
+ .reset {
63
+ padding: 0;
64
+ border: none;
65
+ background: none;
66
+ font: inherit;
67
+ font-size: 12px;
68
+ line-height: 1.5;
69
+ color: var(--dsw-alias-label-secondary);
70
+ cursor: pointer;
71
+ }
72
+
73
+ .reset:hover:not(:disabled) {
74
+ color: var(--dsw-alias-label-primary);
75
+ }
76
+
77
+ .reset:disabled {
78
+ cursor: default;
79
+ }
80
+
81
+ .hint {
82
+ margin: 0;
83
+ font-size: 12px;
84
+ line-height: 1.5;
85
+ color: var(--dsw-alias-label-tertiary);
86
+ }
87
+
88
+ .invalid {
89
+ margin: 0;
90
+ font-size: 12px;
91
+ line-height: 1.5;
92
+ color: var(--dsw-alias-state-error-primary);
93
+ }
@@ -0,0 +1,103 @@
1
+ // 字段行:设置面上"一个字段"的容器——标签、控件、说明、非法提示与"已覆盖"徽标都由它摆位置。
2
+ // 两种排法按控件的宽度选:输入类(stack)是标签 / 控件 / 说明同列,切换类(inline)把控件贴最右。
3
+ // 留白与分隔线的口径与官方 `SettingsValueField` 一致(上下各 12px、相邻 0.5px 细线)。
4
+
5
+ import type { HTMLAttributes, ReactNode } from "react";
6
+ import { classes } from "../controls/classes.ts";
7
+ import css from "./SettingsFieldRow.module.css";
8
+
9
+ export interface SettingsFieldRowProps
10
+ extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "color"> {
11
+ // 字段名。
12
+ label: string;
13
+ // 字段下面那句说明("这一项到底管什么")。
14
+ hint: string;
15
+ // 控件的摆法:`stack`(同列,输入类,缺省)或 `inline`(控件贴右,切换类)。
16
+ layout?: "stack" | "inline";
17
+ // 与上一个字段之间画一条细分隔线(这一组里不是第一个字段时为 true)。
18
+ divider?: boolean;
19
+ // 字段被用户层覆盖时的徽标文案;不给就不显示。
20
+ overriddenLabel?: string;
21
+ // 恢复默认的入口:给了它与 `resetLabel` 才显示(覆盖用户层那一项)。
22
+ resetLabel?: string;
23
+ onReset?: () => void;
24
+ // 非法提示;不给就只说合法的那些。
25
+ invalid?: string;
26
+ children: ReactNode;
27
+ }
28
+
29
+ export function SettingsFieldRow({
30
+ label,
31
+ hint,
32
+ layout = "stack",
33
+ divider = false,
34
+ overriddenLabel,
35
+ resetLabel,
36
+ onReset,
37
+ invalid,
38
+ children,
39
+ className,
40
+ ...rest
41
+ }: SettingsFieldRowProps): ReactNode {
42
+ const labelNode = (
43
+ <span className={css.label}>
44
+ {label}
45
+ {overriddenLabel === undefined ? null : (
46
+ <>
47
+ <span className={css.badge}>{overriddenLabel}</span>
48
+ {resetLabel === undefined || onReset === undefined ? null : (
49
+ <button
50
+ type="button"
51
+ className={css.reset}
52
+ onClick={() => {
53
+ onReset();
54
+ }}
55
+ >
56
+ {resetLabel}
57
+ </button>
58
+ )}
59
+ </>
60
+ )}
61
+ </span>
62
+ );
63
+ const invalidNode =
64
+ invalid === undefined ? null : (
65
+ <p className={css.invalid} role="alert">
66
+ {invalid}
67
+ </p>
68
+ );
69
+ if (layout === "inline") {
70
+ return (
71
+ <div
72
+ className={classes(css.row, className)}
73
+ data-role="field-row"
74
+ data-divider={divider ? "true" : "false"}
75
+ {...rest}
76
+ >
77
+ <div className={css.inline}>
78
+ <div className={css.text}>
79
+ {labelNode}
80
+ <p className={css.hint}>{hint}</p>
81
+ </div>
82
+ {children}
83
+ </div>
84
+ {invalidNode}
85
+ </div>
86
+ );
87
+ }
88
+ return (
89
+ <div
90
+ className={classes(css.row, className)}
91
+ data-role="field-row"
92
+ data-divider={divider ? "true" : "false"}
93
+ {...rest}
94
+ >
95
+ <div className={css.body}>
96
+ {labelNode}
97
+ {children}
98
+ <p className={css.hint}>{hint}</p>
99
+ </div>
100
+ {invalidNode}
101
+ </div>
102
+ );
103
+ }
@@ -1,19 +1,6 @@
1
- // 设置表单原语的 client 出口:模型 + 控件 + 表单框,命名与上游
2
- // `@deepseek-ai/dsh-client-ui-primitives` 同形(搬迁记录见本包 .agents/adrs/)。
1
+ // 设置表单原语的 client 出口:**直接用上游那份**(同名同形的 `SettingsForm` / 两个字段控件 / 表单模型,样式
2
+ // 本来就是上游的 `.module.css`),这里只留本包自造的那一个字段行——上游的 `SettingsValueField` 摆不下
3
+ // 「标签 + 控件 + 说明 + 覆盖标记」这一整行。
3
4
 
4
- export { SettingsFormModel, settingsNumberField, settingsTextField } from "./form-model.ts";
5
- export type {
6
- SettingsFieldSpec,
7
- SettingsFieldState,
8
- SettingsFieldWrite,
9
- SettingsFormActions,
10
- SettingsFormPathOp,
11
- SettingsFormScope,
12
- SettingsFormScopeSnapshot,
13
- SettingsFormShell,
14
- SettingsSecretSpec,
15
- } from "./form-model.ts";
16
- export { SettingsSecretField, SettingsValueField } from "./fields.tsx";
17
- export type { SettingsFieldProps } from "./fields.tsx";
18
- export { SettingsForm } from "./SettingsForm.tsx";
19
- export type { SettingsFormLabels, SettingsFormProps } from "./SettingsForm.tsx";
5
+ export { SettingsFieldRow } from "./SettingsFieldRow.tsx";
6
+ export type { SettingsFieldRowProps } from "./SettingsFieldRow.tsx";
@@ -0,0 +1,6 @@
1
+ // 本包 client 半的组件样式是上游那套 CSS Modules(`*.module.css`,lightningcss 在构建期编译并内联):
2
+ // bundler 解析得动,TS 需要这份 ambient 声明把它定成「有一个 class 映射的模块」。
3
+ declare module "*.module.css" {
4
+ const classes: Readonly<Record<string, string | undefined>>;
5
+ export default classes;
6
+ }