@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
@@ -5,6 +5,8 @@
5
5
  // 与设置页其余部分同一套视觉与键盘行为。
6
6
  //
7
7
  // 字段槽仍然生效:命中时该字段**值**的位置换成注册方的组件,键名、注释、行号与行为按钮仍由这里画。
8
+ //
9
+ // 被 `ellipsis` 截断的文本(注释行、值)由官方 `Tooltip` 承载全文——**只有真的截断了才挂**(`TruncatedTooltip`)。
8
10
 
9
11
  import { useMemo, useState, type ReactNode } from "react";
10
12
  import {
@@ -29,27 +31,12 @@ import {
29
31
  } from "./lines.ts";
30
32
  import { parseFor } from "./fields.tsx";
31
33
  import type { ResolveText } from "./labels.ts";
34
+ import { TruncatedTooltip } from "./TruncatedTooltip.tsx";
32
35
  import { isMultiline, valueText } from "./value.tsx";
33
36
  import type { SchemaFieldOwnerProps, SchemaFormTranslate } from "./slot-contract.ts";
34
- import {
35
- AddWrap,
36
- CompactInput,
37
- CompactTextField,
38
- EditorRoot,
39
- HoverActions,
40
- ICON_SIZE,
41
- LineBody,
42
- LineComment,
43
- LineFold,
44
- LineFoldSpacer,
45
- LineInvalid,
46
- LineKey,
47
- LineNumber,
48
- LineRow,
49
- LineToken,
50
- LineValue,
51
- ValueTrigger,
52
- } from "./styles.ts";
37
+ import { classes } from "../controls/classes.ts";
38
+ import { ICON_SIZE } from "./geometry.ts";
39
+ import css from "./schema-form.module.css";
53
40
 
54
41
  // 编辑器要的一切。
55
42
  export interface EditorProps {
@@ -81,7 +68,7 @@ export function Editor(props: EditorProps): ReactNode {
81
68
  );
82
69
  const disabled = !state.writable || !state.available;
83
70
  return (
84
- <EditorRoot data-editor="schema-form">
71
+ <div className={css.editorRoot} data-editor="schema-form">
85
72
  {lines.map((line, index) => (
86
73
  <Row
87
74
  key={`${line.kind}:${fieldKey(line.path)}:${String(index)}`}
@@ -120,7 +107,7 @@ export function Editor(props: EditorProps): ReactNode {
120
107
  renderField={renderField}
121
108
  />
122
109
  ))}
123
- </EditorRoot>
110
+ </div>
124
111
  );
125
112
  }
126
113
 
@@ -201,17 +188,20 @@ function Row({
201
188
  const body =
202
189
  line.kind === "comment" ? (
203
190
  <>
204
- <LineFoldSpacer data-role="fold" />
205
- {/* 这一行有问题时注释位装的就是那条消息:红字,别整行铺红。 */}
206
- {line.invalid ? (
207
- <LineInvalid title={line.text}>{`// ${line.text}`}</LineInvalid>
208
- ) : (
209
- <LineComment title={line.text}>{`// ${line.text}`}</LineComment>
210
- )}
191
+ <span className={css.lineFoldSpacer} data-role="fold" />
192
+ {/* 这一行有问题时注释位装的就是那条消息:红字,别整行铺红。
193
+ 注释被 `ellipsis` 截断时全文交给官方 `Tooltip`——**只有真的截断才挂**,放得下就原样画。 */}
194
+ <TruncatedTooltip label={line.text}>
195
+ {line.invalid ? (
196
+ <span className={css.lineInvalid} data-role="comment">{`// ${line.text}`}</span>
197
+ ) : (
198
+ <span className={css.lineComment} data-role="comment">{`// ${line.text}`}</span>
199
+ )}
200
+ </TruncatedTooltip>
211
201
  </>
212
202
  ) : line.kind === "open" ? (
213
203
  <>
214
- <LineFold
204
+ <button className={css.lineFold}
215
205
  data-role="fold"
216
206
  type="button"
217
207
  aria-expanded={!line.collapsed}
@@ -225,11 +215,11 @@ function Row({
225
215
  ) : (
226
216
  <IconChevronDownOutlineRegular size={ICON_SIZE} />
227
217
  )}
228
- </LineFold>
218
+ </button>
229
219
  <LinePrefix line={line} face={face} t={t} disabled={disabled} />
230
- <LineToken>
220
+ <span className={css.lineToken}>
231
221
  {line.shape === "object" ? (line.collapsed ? "{…}" : "{") : line.collapsed ? "[…]" : "["}
232
- </LineToken>
222
+ </span>
233
223
  <VariantSelect
234
224
  control={line.variants}
235
225
  path={line.path}
@@ -240,8 +230,8 @@ function Row({
240
230
  </>
241
231
  ) : line.kind === "close" ? (
242
232
  <>
243
- <LineFoldSpacer data-role="fold" />
244
- <LineToken>{line.shape === "object" ? "}" : "]"}</LineToken>
233
+ <span className={css.lineFoldSpacer} data-role="fold" />
234
+ <span className={css.lineToken}>{line.shape === "object" ? "}" : "]"}</span>
245
235
  <AddLine
246
236
  line={line}
247
237
  state={state}
@@ -268,7 +258,7 @@ function Row({
268
258
  />
269
259
  );
270
260
  return (
271
- <LineRow
261
+ <div className={css.lineRow}
272
262
  data-line={line.kind}
273
263
  data-field-path={line.path.join(".")}
274
264
  data-selected={selected === key ? "true" : undefined}
@@ -282,18 +272,18 @@ function Row({
282
272
  : undefined
283
273
  }
284
274
  >
285
- <LineNumber
275
+ <span className={css.lineNumber}
286
276
  data-role="number"
287
277
  onClick={() => {
288
278
  onSelect(line.path);
289
279
  }}
290
280
  >
291
281
  {n}
292
- </LineNumber>
293
- <LineBody data-role="body" style={indent}>
282
+ </span>
283
+ <div className={css.lineBody} data-role="body" style={indent}>
294
284
  {body}
295
- </LineBody>
296
- </LineRow>
285
+ </div>
286
+ </div>
297
287
  );
298
288
  }
299
289
 
@@ -371,19 +361,19 @@ function FieldLine({
371
361
  const multiline = isMultiline(line.field.value) || /[\r\n]/.test(text);
372
362
  return (
373
363
  <>
374
- <LineFoldSpacer data-role="fold" />
375
- <LineKey
364
+ <span className={css.lineFoldSpacer} data-role="fold" />
365
+ <span className={css.lineKey}
376
366
  data-role="key"
377
367
  data-index={member?.index === undefined ? undefined : "true"}
378
368
  title={member?.key ?? line.node.key}
379
369
  >
380
370
  {member?.key ?? line.node.key}
381
- </LineKey>
382
- <LineToken>{member?.index === undefined ? ": " : " "}</LineToken>
371
+ </span>
372
+ <span className={css.lineToken}>{member?.index === undefined ? ": " : " "}</span>
383
373
  {isEditing ? (
384
374
  // 带换行的值给多行输入(Enter 换行、Esc 撤销),其余是一行的官方输入框。
385
375
  multiline ? (
386
- <CompactTextField>
376
+ <span className={css.compactTextField}>
387
377
  <textarea
388
378
  autoFocus={!missing}
389
379
  rows={1}
@@ -399,9 +389,9 @@ function FieldLine({
399
389
  }
400
390
  }}
401
391
  />
402
- </CompactTextField>
392
+ </span>
403
393
  ) : (
404
- <CompactInput>
394
+ <span className={css.compactInput}>
405
395
  <Input
406
396
  autoFocus={!missing}
407
397
  type="text"
@@ -422,12 +412,13 @@ function FieldLine({
422
412
  }
423
413
  }}
424
414
  />
425
- </CompactInput>
415
+ </span>
426
416
  )
427
417
  ) : null}
428
418
  {/* 编辑态只留输入框:原值不再在它旁边画一遍。 */}
429
419
  {isEditing || line.variantsStandIn ? null : (
430
- <span
420
+ <span className={css.valueSlot}
421
+ data-role="value"
431
422
  onClick={() => {
432
423
  if (editable) onEdit(key);
433
424
  }}
@@ -444,7 +435,7 @@ function FieldLine({
444
435
  />
445
436
  {isEditing ? (
446
437
  // 编辑态:确认收起这一格的编辑、取消把值退回去——都在输入框旁边,不用记快捷键。
447
- <HoverActions data-role="actions" data-editing="true">
438
+ <span className={classes(css.lineActions, css.hoverActions)} data-role="actions" data-editing="true">
448
439
  <button
449
440
  type="button"
450
441
  aria-label={t("confirmEdit")}
@@ -468,9 +459,9 @@ function FieldLine({
468
459
  >
469
460
  <IconCloseOutlineRegular size={ICON_SIZE} />
470
461
  </button>
471
- </HoverActions>
462
+ </span>
472
463
  ) : null}
473
- <HoverActions data-role="actions">
464
+ <span className={classes(css.lineActions, css.hoverActions)} data-role="actions">
474
465
  {line.field.staged && !disabled ? (
475
466
  <button
476
467
  type="button"
@@ -510,7 +501,7 @@ function FieldLine({
510
501
  {member === undefined ? null : (
511
502
  <RemoveLineButton member={member} face={face} t={t} disabled={disabled} />
512
503
  )}
513
- </HoverActions>
504
+ </span>
514
505
  </>
515
506
  );
516
507
  }
@@ -558,7 +549,7 @@ function VariantSelect({
558
549
  setOpen(false);
559
550
  }}
560
551
  anchor={
561
- <ValueTrigger
552
+ <button className={css.valueTrigger}
562
553
  type="button"
563
554
  data-role="variant"
564
555
  disabled={disabled}
@@ -567,9 +558,9 @@ function VariantSelect({
567
558
  setOpen(true);
568
559
  }}
569
560
  >
570
- <LineValue data-tone="empty">{current?.label ?? ""}</LineValue>
561
+ <span className={css.lineValue} data-tone="empty">{current?.label ?? ""}</span>
571
562
  <IconChevronDownOutlineRegular size={ICON_SIZE} />
572
- </ValueTrigger>
563
+ </button>
573
564
  }
574
565
  />
575
566
  );
@@ -594,15 +585,15 @@ function LinePrefix({
594
585
  const indexed = member !== undefined && member.index !== undefined;
595
586
  return (
596
587
  <>
597
- <LineKey data-role="key" data-index={indexed ? "true" : undefined} title={key}>
588
+ <span className={css.lineKey} data-role="key" data-index={indexed ? "true" : undefined} title={key}>
598
589
  {key}
599
- </LineKey>
600
- <LineToken>{indexed ? " " : ": "}</LineToken>
590
+ </span>
591
+ <span className={css.lineToken}>{indexed ? " " : ": "}</span>
601
592
  {/* 值本身是容器(对象/数组)的成员:它也要能移除,否则这一项删不掉。 */}
602
593
  {member === undefined ? null : (
603
- <HoverActions data-role="actions">
594
+ <span className={classes(css.lineActions, css.hoverActions)} data-role="actions">
604
595
  <RemoveLineButton member={member} face={face} t={t} disabled={disabled} />
605
- </HoverActions>
596
+ </span>
606
597
  )}
607
598
  </>
608
599
  );
@@ -705,7 +696,7 @@ function AddLine({
705
696
  setOpen(false);
706
697
  };
707
698
  return (
708
- <AddWrap data-add={spec.kind} data-invalid={invalid ? "true" : undefined}>
699
+ <span className={css.addWrap} data-add={spec.kind} data-invalid={invalid ? "true" : undefined}>
709
700
  <Menu
710
701
  open={open && spec.kind !== "item" && matched.length > 0}
711
702
  dense
@@ -720,7 +711,7 @@ function AddLine({
720
711
  setOpen(false);
721
712
  }}
722
713
  anchor={
723
- <CompactInput>
714
+ <span className={css.compactInput}>
724
715
  <Input
725
716
  value={text}
726
717
  placeholder={label}
@@ -749,16 +740,16 @@ function AddLine({
749
740
  }
750
741
  }}
751
742
  />
752
- </CompactInput>
743
+ </span>
753
744
  }
754
745
  />
755
- {invalid ? <LineInvalid>{t("unknownProperty")}</LineInvalid> : null}
756
- {/* 候选的说明:选中之前先看清"要加的是什么"。 */}
746
+ {invalid ? <span className={css.lineInvalid}>{t("unknownProperty")}</span> : null}
747
+ {/* 候选的说明:选中之前先看清"要加的是什么"(长了同样由 `Tooltip` 给全文,只有截断时才挂)。 */}
757
748
  {!invalid && matched.length === 1 && matched[0]?.description !== undefined ? (
758
- <LineComment title={resolveText(matched[0].description)}>
759
- {resolveText(matched[0].description)}
760
- </LineComment>
749
+ <TruncatedTooltip label={resolveText(matched[0].description)}>
750
+ <span className={css.lineComment}>{resolveText(matched[0].description)}</span>
751
+ </TruncatedTooltip>
761
752
  ) : null}
762
- </AddWrap>
753
+ </span>
763
754
  );
764
755
  }
@@ -8,7 +8,7 @@ import { SettingsForm, type SettingsFormLabels } from "@deepseek-ai/dsh-client-u
8
8
  import { Editor } from "./Editor.tsx";
9
9
  import { parseFor } from "./fields.tsx";
10
10
  import type { SchemaFieldOwnerProps, SchemaFormComponentProps } from "./slot-contract.ts";
11
- import { Hint } from "./styles.ts";
11
+ import css from "./schema-form.module.css";
12
12
  import { SchemaFieldDefault } from "./value.tsx";
13
13
 
14
14
  // 渲染一行的配置页。
@@ -26,7 +26,7 @@ export function SchemaForm(props: SchemaFormComponentProps): ReactNode {
26
26
  save: t("save"),
27
27
  saving: t("saving"),
28
28
  };
29
- if (!state.configured) return <Hint role="status">{t("noSchema")}</Hint>;
29
+ if (!state.configured) return <p className={css.hint} role="status">{t("noSchema")}</p>;
30
30
  return (
31
31
  <SettingsForm
32
32
  labels={labels}
@@ -52,9 +52,9 @@ export function SchemaForm(props: SchemaFormComponentProps): ReactNode {
52
52
  />
53
53
  {/* 本地整段校验挡下的保存要说出来:否则点保存"没有反应"看起来像按钮坏了。 */}
54
54
  {state.violation === undefined ? null : (
55
- <Hint role="alert" data-violation="true">
55
+ <p className={css.hint} role="alert" data-violation="true">
56
56
  {t("notSaved", { message: state.violation.message })}
57
- </Hint>
57
+ </p>
58
58
  )}
59
59
  </SettingsForm>
60
60
  );
@@ -0,0 +1,60 @@
1
+ // 行内文本的全文浮层:**只有确实被截断**时才给官方 `Tooltip`,放得下就一个浮层也不挂。
2
+ //
3
+ // 判据是元素自己的几何——`scrollWidth > clientWidth` 就是「这一格正被 `ellipsis` 截断」,不用字符数之类的估计。
4
+ // 宽度变化(窗口、面板、字段槽)由 `ResizeObserver` 盯着这个元素重测;盒子没变、只是换了内容(值改了、注释换
5
+ // 了)不会被它通知,所以每次提交再重量一次。
6
+
7
+ import {
8
+ cloneElement,
9
+ useCallback,
10
+ useLayoutEffect,
11
+ useRef,
12
+ useState,
13
+ type ReactElement,
14
+ type ReactNode,
15
+ type Ref,
16
+ } from "react";
17
+ import { Tooltip } from "@deepseek-ai/dsh-client-ui-primitives";
18
+
19
+ export function TruncatedTooltip({
20
+ label,
21
+ children,
22
+ }: {
23
+ label: string;
24
+ children: ReactElement<{ ref?: Ref<HTMLElement> }>;
25
+ }): ReactNode {
26
+ const node = useRef<HTMLElement | null>(null);
27
+ const watch = useRef<ResizeObserver | null>(null);
28
+ const [truncated, setTruncated] = useState(false);
29
+
30
+ const measure = useCallback(() => {
31
+ const el = node.current;
32
+ if (el === null) return;
33
+ setTruncated(el.scrollWidth > el.clientWidth);
34
+ }, []);
35
+
36
+ // ref 一直挂在子元素上(没截断时也挂着):挂上就盯着它的宽度,卸载时退订。
37
+ const anchor = useCallback(
38
+ (el: HTMLElement | null) => {
39
+ watch.current?.disconnect();
40
+ watch.current = null;
41
+ node.current = el;
42
+ if (el === null) return;
43
+ const observer = new ResizeObserver(measure);
44
+ observer.observe(el);
45
+ watch.current = observer;
46
+ },
47
+ [measure],
48
+ );
49
+
50
+ // 每次提交都重量一次:`ResizeObserver` 只对盒子尺寸发通知,换了文本的同尺寸盒子要靠这里。
51
+ useLayoutEffect(measure);
52
+
53
+ // 没截断时走 `Tooltip` 自己的 `disabled`:气泡不渲染、hover 也不弹,锚元素同时不重挂
54
+ //(换成「有截断才包 `Tooltip`」会在截断状态翻转时把锚换掉,白丢一次 DOM)。
55
+ return (
56
+ <Tooltip label={label} side="bottom" portal disabled={!truncated}>
57
+ {cloneElement(children, { ref: anchor })}
58
+ </Tooltip>
59
+ );
60
+ }
@@ -304,9 +304,12 @@ export class SchemaFormController {
304
304
  return this.#ns;
305
305
  }
306
306
 
307
- // 外部提示(候选键这类)变了:重新投影一次,字段树与草稿都不动。
307
+ // 外部读数变了(提示面的候选、或 describe 里的 schema 本身):**重读一次 describe** 再投影。
308
+ //
309
+ // 只重投影是不够的:行页在 describe 变化时会重建控制器,而"这一行常驻、只订阅 describe"的消费方(bundle 配置页)
310
+ // 依赖这次重读——否则视图晚到、通知没落上时,字段树会停在"还没有"的那一次投影上。
308
311
  refresh(): void {
309
- this.#store.set(this.#project());
312
+ this.#sync();
310
313
  }
311
314
 
312
315
  // 释放订阅。
@@ -0,0 +1,3 @@
1
+ // 行内图标的边长:与 `schema-form.module.css` 里行高 / 折叠列同一档(14px)。组件用 `size={ICON_SIZE}`,
2
+ // 样式表用像素——两处同源,改就一起改。
3
+ export const ICON_SIZE = 14;
@@ -74,7 +74,18 @@ export type { SuggestedKeys } from "./schema-node.ts";
74
74
  type RowConfigEntryProps = PropsRuntime<"plugins.row.config"> & PropsRenderFactories;
75
75
 
76
76
  // 装上本包:字典、Factory(字段槽的声明者)与按行自动注册。
77
+ //
78
+ // 调用方是**内联了本包的 client 行**(本包不再是装配行):inline 后每个消费行的产物里各带一份这份代码、
79
+ // 都会调到这里,所以先等依赖服务到齐(调用方不必自己排顺序),并保证同一运行时只装一次。
77
80
  export function apply(ctx: Context): void {
81
+ ctx.inject([...inject], (scope) => {
82
+ // 提示面已在场 = 另一份副本装过了:字典 / 槽 / 行入口都不再重复注册。
83
+ if (scope.get("schemaFormHints") !== undefined) return;
84
+ install(scope);
85
+ });
86
+ }
87
+
88
+ function install(ctx: Context): void {
78
89
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "ui-schema-form: dictionaries");
79
90
  // 提示面(dict 的候选键):同一次 apply 里提供,业务插件注册读数,本包的行投影读它。
80
91
  const hints = new SchemaFormHints(ctx);