@dsh-workbench/shared 0.2.12 → 0.2.14

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 (53) hide show
  1. package/dist/components/ChoiceGroup.css +53 -3
  2. package/dist/components/ChoiceGroup.d.ts +5 -0
  3. package/dist/components/ChoiceGroup.js +15 -5
  4. package/dist/components/FormActions.css +13 -0
  5. package/dist/components/FormActions.d.ts +15 -0
  6. package/dist/components/FormActions.js +38 -0
  7. package/dist/components/FormRows.css +69 -0
  8. package/dist/components/FormRows.d.ts +14 -0
  9. package/dist/components/FormRows.js +11 -0
  10. package/dist/components/SaveBar.d.ts +0 -1
  11. package/dist/components/SaveBar.js +13 -5
  12. package/dist/components/SelectField.css +12 -4
  13. package/dist/components/SelectField.d.ts +1 -0
  14. package/dist/components/SelectField.js +9 -7
  15. package/dist/components/Spinner.css +6 -0
  16. package/dist/components/Spinner.d.ts +6 -0
  17. package/dist/components/Spinner.js +7 -0
  18. package/dist/components/TextField.css +2 -2
  19. package/dist/host.d.ts +3 -3
  20. package/dist/host.js +3 -3
  21. package/dist/i18n/en.js +9 -0
  22. package/dist/i18n/index.d.ts +10 -1
  23. package/dist/i18n/zh.d.ts +9 -0
  24. package/dist/i18n/zh.js +9 -0
  25. package/dist/index.d.ts +8 -4
  26. package/dist/index.js +8 -4
  27. package/dist/lib/form-problem.d.ts +2 -0
  28. package/dist/lib/form-problem.js +20 -0
  29. package/package.json +2 -1
  30. package/src/components/ChoiceGroup.css +53 -3
  31. package/src/components/ChoiceGroup.test.tsx +88 -0
  32. package/src/components/ChoiceGroup.tsx +70 -28
  33. package/src/components/FormActions.css +13 -0
  34. package/src/components/FormActions.test.tsx +157 -0
  35. package/src/components/FormActions.tsx +75 -0
  36. package/src/components/FormRows.css +69 -0
  37. package/src/components/FormRows.test.tsx +138 -0
  38. package/src/components/FormRows.tsx +39 -0
  39. package/src/components/SaveBar.tsx +14 -18
  40. package/src/components/SelectField.css +12 -4
  41. package/src/components/SelectField.test.tsx +20 -2
  42. package/src/components/SelectField.tsx +11 -3
  43. package/src/components/Spinner.css +6 -0
  44. package/src/components/Spinner.test.tsx +35 -0
  45. package/src/components/Spinner.tsx +19 -0
  46. package/src/components/TextField.css +2 -2
  47. package/src/host.ts +3 -3
  48. package/src/i18n/en.ts +9 -0
  49. package/src/i18n/zh.ts +9 -0
  50. package/src/index.ts +8 -4
  51. package/src/lib/form-problem.test.ts +76 -0
  52. package/src/lib/form-problem.ts +22 -0
  53. package/src/components/SaveBar.css +0 -7
@@ -5,11 +5,11 @@ import {
5
5
  IconChevronDownOutlineRegular,
6
6
  IconSearchOutlineRegular,
7
7
  Menu,
8
- StateDot,
9
8
  TextShimmer,
10
9
  } from '@deepseek-ai/dsh-client-ui-primitives';
11
10
  import { Field } from './Field.js';
12
11
  import { TextField } from './TextField.js';
12
+ import { Spinner } from './Spinner.js';
13
13
  import './SelectField.css';
14
14
 
15
15
  export interface SelectFieldOption {
@@ -48,6 +48,7 @@ export interface SelectFieldProps {
48
48
  readonly open?: boolean | undefined;
49
49
  readonly onOpenChange?: ((open: boolean) => void) | undefined;
50
50
  readonly disabled?: boolean | undefined;
51
+ readonly invalid?: boolean | undefined;
51
52
  readonly loading?: boolean | undefined;
52
53
  readonly loadingText?: string | undefined;
53
54
  readonly fallback?: SelectFieldFallback | undefined;
@@ -83,6 +84,7 @@ function triggerNode(args: {
83
84
  readonly id: string | undefined;
84
85
  readonly className: string;
85
86
  readonly disabled: boolean;
87
+ readonly invalid: boolean;
86
88
  readonly open: boolean;
87
89
  readonly ariaLabel: string | undefined;
88
90
  readonly title: string | undefined;
@@ -98,6 +100,7 @@ function triggerNode(args: {
98
100
  disabled={args.disabled}
99
101
  aria-haspopup="listbox"
100
102
  aria-expanded={args.open}
103
+ aria-invalid={args.invalid || undefined}
101
104
  aria-label={args.ariaLabel}
102
105
  title={args.title}
103
106
  onClick={args.onOpen}
@@ -114,6 +117,7 @@ function triggerNode(args: {
114
117
  function loadingNode(args: {
115
118
  readonly id: string | undefined;
116
119
  readonly className: string;
120
+ readonly invalid: boolean;
117
121
  readonly ariaLabel: string | undefined;
118
122
  readonly loadingText: string | undefined;
119
123
  }): ReactNode {
@@ -124,6 +128,7 @@ function loadingNode(args: {
124
128
  className={`${args.className} wb-selectLoading`}
125
129
  disabled
126
130
  aria-busy="true"
131
+ aria-invalid={args.invalid || undefined}
127
132
  aria-label={args.ariaLabel}
128
133
  title={args.loadingText}
129
134
  >
@@ -131,7 +136,7 @@ function loadingNode(args: {
131
136
  <TextShimmer active>{args.loadingText ?? '…'}</TextShimmer>
132
137
  </span>
133
138
  <span className="wb-selectChevron">
134
- <StateDot state="ongoing" size={14} />
139
+ <Spinner size={14} />
135
140
  </span>
136
141
  </button>
137
142
  );
@@ -357,6 +362,7 @@ export function SelectField(props: SelectFieldProps) {
357
362
  footer,
358
363
  onFooter,
359
364
  disabled = false,
365
+ invalid = false,
360
366
  loading = false,
361
367
  loadingText,
362
368
  fallback,
@@ -379,7 +385,7 @@ export function SelectField(props: SelectFieldProps) {
379
385
  label,
380
386
  hint,
381
387
  className,
382
- loadingNode({ id, className: triggerClass, ariaLabel, loadingText }),
388
+ loadingNode({ id, className: triggerClass, invalid, ariaLabel, loadingText }),
383
389
  );
384
390
  }
385
391
 
@@ -392,6 +398,7 @@ export function SelectField(props: SelectFieldProps) {
392
398
  id={id}
393
399
  name={name}
394
400
  className="wb-selectFallback"
401
+ invalid={invalid}
395
402
  aria-label={ariaLabel}
396
403
  value={fallback.value}
397
404
  onChange={(event) => fallback.onChange(event.currentTarget.value)}
@@ -403,6 +410,7 @@ export function SelectField(props: SelectFieldProps) {
403
410
  id,
404
411
  className: triggerClass,
405
412
  disabled,
413
+ invalid,
406
414
  open: state.open,
407
415
  ariaLabel,
408
416
  title,
@@ -0,0 +1,6 @@
1
+
2
+ .wb-spinner {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ flex: none;
6
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('../lib/react.js', async () => {
4
+ const actual = await vi.importActual<typeof import('../lib/react.js')>('../lib/react.js');
5
+ return { ...actual, h: (type: unknown, props?: unknown, ...children: unknown[]) => ({ type, props: props ?? {}, children }) };
6
+ });
7
+ vi.mock('./Spinner.css', () => ({ default: {} }));
8
+
9
+ import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives';
10
+ import { Spinner } from './Spinner.js';
11
+
12
+ describe('Spinner(loading 那颗)', () => {
13
+ it('画的是 dsh 的 ongoing 状态点(视觉只有这一处实现)', () => {
14
+ const node = Spinner({ size: 12 }) as unknown as { children: readonly { type: unknown; props: Record<string, unknown> }[] };
15
+
16
+ expect(node.children[0]?.type).toBe(StateDot);
17
+ expect(node.children[0]?.props['state']).toBe('ongoing');
18
+ expect(node.children[0]?.props['size']).toBe(12);
19
+ });
20
+
21
+ it('没给 label 时对读屏隐藏(它是装饰,话由旁边的文案说)', () => {
22
+ const node = Spinner({}) as unknown as { props: Record<string, unknown> };
23
+
24
+ expect(node.props['aria-hidden']).toBe(true);
25
+ expect(node.props['role']).toBeUndefined();
26
+ });
27
+
28
+ it('给了 label 就是一条状态播报', () => {
29
+ const node = Spinner({ label: '正在读环境' }) as unknown as { props: Record<string, unknown> };
30
+
31
+ expect(node.props['role']).toBe('status');
32
+ expect(node.props['aria-label']).toBe('正在读环境');
33
+ expect(node.props['aria-hidden']).toBeUndefined();
34
+ });
35
+ });
@@ -0,0 +1,19 @@
1
+ import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives';
2
+ import { h } from '../lib/react.js';
3
+ import './Spinner.css';
4
+
5
+ export interface SpinnerProps {
6
+ readonly size?: number;
7
+ readonly label?: string | undefined;
8
+ }
9
+
10
+ export function Spinner({ size = 14, label }: SpinnerProps) {
11
+ return (
12
+ <span
13
+ className="wb-spinner"
14
+ {...(label === undefined ? { 'aria-hidden': true } : { role: 'status', 'aria-label': label })}
15
+ >
16
+ <StateDot state="ongoing" size={size} />
17
+ </span>
18
+ );
19
+ }
@@ -27,6 +27,6 @@
27
27
  }
28
28
 
29
29
  :where(.wb-textField) {
30
- min-height: var(--wb-len-32);
31
- padding: var(--wb-len-6) var(--wb-len-12);
30
+ min-height: var(--wb-len-28);
31
+ padding: var(--wb-len-4) var(--wb-len-10);
32
32
  }
package/src/host.ts CHANGED
@@ -8,17 +8,17 @@ export * from './lib/text.js';
8
8
  export * from './lib/ui.js';
9
9
  export * from './lib/panel-state.js';
10
10
  export * from './api/blocker-table.js';
11
+ export * from './host/browser-find.js';
12
+ export * from './host/browser-read.js';
11
13
  export * from './host/cli-error.js';
12
14
  export * from './host/data-root.js';
13
- export * from './host/browser-read.js';
14
- export * from './host/browser-find.js';
15
15
  export * from './host/facts.js';
16
16
  export * from './host/git.js';
17
17
  export * from './host/http.js';
18
18
  export * from './host/long-task.js';
19
19
  export * from './host/opencli-runtime.js';
20
- export * from './host/opencli.js';
21
20
  export * from './host/opencli-window.js';
21
+ export * from './host/opencli.js';
22
22
  export * from './host/plugin-ledger.js';
23
23
  export * from './host/plugin-source.js';
24
24
  export * from './host/provision.js';
package/src/i18n/en.ts CHANGED
@@ -48,6 +48,15 @@ export const en: Record<keyof WorkbenchMessages, string> = {
48
48
  'status.completed': 'Completed',
49
49
  'status.subagentsRunning.one': '{n} subagent running',
50
50
  'status.subagentsRunning.other': '{n} subagents running',
51
+ 'project.attention.warning.one': '1 session waiting for you',
52
+ 'project.attention.warning.other': '{n} sessions waiting for you',
53
+ 'project.attention.running.one': '1 session running',
54
+ 'project.attention.running.other': '{n} sessions running',
55
+ 'project.attention.done.one': '1 session finished, unread',
56
+ 'project.attention.done.other': '{n} sessions finished, unread',
57
+ 'project.attention.state.warning': 'Approval',
58
+ 'project.attention.state.running': 'Running',
59
+ 'project.attention.state.done': 'New',
51
60
 
52
61
  'hover.copied': 'Copied',
53
62
  'time.now': 'just now',
package/src/i18n/zh.ts CHANGED
@@ -46,6 +46,15 @@ export const zh = {
46
46
  'status.completed': '已完成',
47
47
  'status.subagentsRunning.one': '{n} 个子代理运行中',
48
48
  'status.subagentsRunning.other': '{n} 个子代理运行中',
49
+ 'project.attention.warning.one': '1 个会话在等你点头',
50
+ 'project.attention.warning.other': '{n} 个会话在等你点头',
51
+ 'project.attention.running.one': '1 个会话在跑',
52
+ 'project.attention.running.other': '{n} 个会话在跑',
53
+ 'project.attention.done.one': '1 个会话跑完没看',
54
+ 'project.attention.done.other': '{n} 个会话跑完没看',
55
+ 'project.attention.state.warning': '待审批',
56
+ 'project.attention.state.running': '在跑',
57
+ 'project.attention.state.done': '新消息',
49
58
 
50
59
  'hover.copied': '已复制',
51
60
  'time.now': '刚刚',
package/src/index.ts CHANGED
@@ -7,8 +7,8 @@ export * from './api/facts.js';
7
7
  export * from './api/host-write.js';
8
8
  export * from './api/long-task.js';
9
9
  export * from './api/unit-board.js';
10
- export * from './api/unit.js';
11
10
  export * from './api/unit-declaration.js';
11
+ export * from './api/unit.js';
12
12
  export * from './api/workbench-client.js';
13
13
  export * from './api/workbench-contract.js';
14
14
  export * from './components/Activity.js';
@@ -19,18 +19,21 @@ export * from './components/CardAnchor.js';
19
19
  export * from './components/CardBoundary.js';
20
20
  export * from './components/CardHead.js';
21
21
  export * from './components/ChoiceGroup.js';
22
+ export * from './components/Field.js';
23
+ export * from './components/FormActions.js';
24
+ export * from './components/FormRows.js';
25
+ export * from './components/Link.js';
22
26
  export * from './components/LongTaskRow.js';
23
27
  export * from './components/Note.js';
24
28
  export * from './components/ResourceStates.js';
25
29
  export * from './components/Rows.js';
26
30
  export * from './components/SaveBar.js';
27
- export * from './components/SettingsSection.js';
28
31
  export * from './components/SelectField.js';
32
+ export * from './components/SettingsSection.js';
33
+ export * from './components/Spinner.js';
29
34
  export * from './components/Stack.js';
30
35
  export * from './components/StateBlock.js';
31
36
  export * from './components/TextField.js';
32
- export * from './components/Field.js';
33
- export * from './components/Link.js';
34
37
  export * from './components/board-module.js';
35
38
  export * from './components/icons.js';
36
39
  export * from './config/constants.js';
@@ -41,6 +44,7 @@ export * from './i18n/merge.js';
41
44
  export * from './i18n/zh.js';
42
45
  export * from './lib/blocker-action.js';
43
46
  export * from './lib/busy.js';
47
+ export * from './lib/form-problem.js';
44
48
  export * from './lib/panel-state.js';
45
49
  export * from './lib/react.js';
46
50
  export * from './lib/text.js';
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { firstProblemOf, revealFirstProblem } from './form-problem.js';
3
+
4
+ function rootWith(problem: unknown): { readonly root: ParentNode; readonly queries: string[] } {
5
+ const queries: string[] = [];
6
+ const root = {
7
+ querySelector: (selector: string) => {
8
+ queries.push(selector);
9
+ return selector === '[data-wb-problem]' ? problem : null;
10
+ },
11
+ } as unknown as ParentNode;
12
+ return { root, queries };
13
+ }
14
+
15
+ interface ProblemStub {
16
+ readonly element: Element;
17
+ readonly queries: string[];
18
+ readonly scrollIntoView: ReturnType<typeof vi.fn>;
19
+ readonly focus: ReturnType<typeof vi.fn>;
20
+ }
21
+
22
+ function problemStub(options: { readonly scroll: boolean }): ProblemStub {
23
+ const queries: string[] = [];
24
+ const scrollIntoView = vi.fn();
25
+ const focus = vi.fn();
26
+ const element = {
27
+ scrollIntoView,
28
+ querySelector: (selector: string) => {
29
+ queries.push(selector);
30
+ return { focus };
31
+ },
32
+ } as unknown as Element;
33
+ if (!options.scroll) {
34
+ delete (element as { scrollIntoView?: unknown }).scrollIntoView;
35
+ }
36
+ return { element, queries, scrollIntoView, focus };
37
+ }
38
+
39
+ describe('firstProblemOf / revealFirstProblem', () => {
40
+ it('没有错误行就返回 null / false,什么也不碰', () => {
41
+ const empty = rootWith(null);
42
+ const missing = problemStub({ scroll: true });
43
+
44
+ expect(firstProblemOf(empty.root)).toBeNull();
45
+ expect(revealFirstProblem(rootWith(null).root)).toBe(false);
46
+ expect(missing.focus).not.toHaveBeenCalled();
47
+ expect(missing.scrollIntoView).not.toHaveBeenCalled();
48
+ });
49
+
50
+ it('第一处 [data-wb-problem] 就是它', () => {
51
+ const problem = problemStub({ scroll: true });
52
+ const found = rootWith(problem.element);
53
+
54
+ expect(firstProblemOf(found.root)).toBe(problem.element);
55
+ expect(found.queries).toEqual(['[data-wb-problem]']);
56
+ });
57
+
58
+ it('带进视口 + 聚焦里面第一个可聚焦控件,返回 true', () => {
59
+ const problem = problemStub({ scroll: true });
60
+
61
+ expect(revealFirstProblem(rootWith(problem.element).root)).toBe(true);
62
+ expect(problem.scrollIntoView).toHaveBeenCalledTimes(1);
63
+ expect(problem.scrollIntoView).toHaveBeenCalledWith({ block: 'nearest' });
64
+ expect(problem.queries).toHaveLength(1);
65
+ expect(problem.queries[0]).toContain('input:not([disabled])');
66
+ expect(problem.focus).toHaveBeenCalledTimes(1);
67
+ });
68
+
69
+ it('没有 scrollIntoView 也不炸(jsdom 缺它):照样聚焦、照样返回 true', () => {
70
+ const problem = problemStub({ scroll: false });
71
+
72
+ expect(() => revealFirstProblem(rootWith(problem.element).root)).not.toThrow();
73
+ expect(revealFirstProblem(rootWith(problem.element).root)).toBe(true);
74
+ expect(problem.focus).toHaveBeenCalledTimes(2);
75
+ });
76
+ });
@@ -0,0 +1,22 @@
1
+ const PROBLEM_SELECTOR = '[data-wb-problem]';
2
+
3
+ const FOCUSABLE_SELECTOR = [
4
+ 'input:not([disabled])',
5
+ 'select:not([disabled])',
6
+ 'textarea:not([disabled])',
7
+ 'button:not([disabled])',
8
+ 'a[href]',
9
+ '[tabindex]:not([tabindex="-1"])',
10
+ ].join(', ');
11
+
12
+ export function firstProblemOf(root: ParentNode): Element | null {
13
+ return root.querySelector(PROBLEM_SELECTOR);
14
+ }
15
+
16
+ export function revealFirstProblem(root: ParentNode): boolean {
17
+ const problem = firstProblemOf(root);
18
+ if (problem === null) return false;
19
+ problem.scrollIntoView?.({ block: 'nearest' });
20
+ problem.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)?.focus?.();
21
+ return true;
22
+ }
@@ -1,7 +0,0 @@
1
-
2
- .wb-formActions {
3
- display: flex;
4
- align-items: center;
5
- justify-content: flex-end;
6
- gap: var(--wb-len-8);
7
- }