@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.
- package/dist/components/ChoiceGroup.css +53 -3
- package/dist/components/ChoiceGroup.d.ts +5 -0
- package/dist/components/ChoiceGroup.js +15 -5
- package/dist/components/FormActions.css +13 -0
- package/dist/components/FormActions.d.ts +15 -0
- package/dist/components/FormActions.js +38 -0
- package/dist/components/FormRows.css +69 -0
- package/dist/components/FormRows.d.ts +14 -0
- package/dist/components/FormRows.js +11 -0
- package/dist/components/SaveBar.d.ts +0 -1
- package/dist/components/SaveBar.js +13 -5
- package/dist/components/SelectField.css +12 -4
- package/dist/components/SelectField.d.ts +1 -0
- package/dist/components/SelectField.js +9 -7
- package/dist/components/Spinner.css +6 -0
- package/dist/components/Spinner.d.ts +6 -0
- package/dist/components/Spinner.js +7 -0
- package/dist/components/TextField.css +2 -2
- package/dist/host.d.ts +3 -3
- package/dist/host.js +3 -3
- package/dist/i18n/en.js +9 -0
- package/dist/i18n/index.d.ts +10 -1
- package/dist/i18n/zh.d.ts +9 -0
- package/dist/i18n/zh.js +9 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +8 -4
- package/dist/lib/form-problem.d.ts +2 -0
- package/dist/lib/form-problem.js +20 -0
- package/package.json +2 -1
- package/src/components/ChoiceGroup.css +53 -3
- package/src/components/ChoiceGroup.test.tsx +88 -0
- package/src/components/ChoiceGroup.tsx +70 -28
- package/src/components/FormActions.css +13 -0
- package/src/components/FormActions.test.tsx +157 -0
- package/src/components/FormActions.tsx +75 -0
- package/src/components/FormRows.css +69 -0
- package/src/components/FormRows.test.tsx +138 -0
- package/src/components/FormRows.tsx +39 -0
- package/src/components/SaveBar.tsx +14 -18
- package/src/components/SelectField.css +12 -4
- package/src/components/SelectField.test.tsx +20 -2
- package/src/components/SelectField.tsx +11 -3
- package/src/components/Spinner.css +6 -0
- package/src/components/Spinner.test.tsx +35 -0
- package/src/components/Spinner.tsx +19 -0
- package/src/components/TextField.css +2 -2
- package/src/host.ts +3 -3
- package/src/i18n/en.ts +9 -0
- package/src/i18n/zh.ts +9 -0
- package/src/index.ts +8 -4
- package/src/lib/form-problem.test.ts +76 -0
- package/src/lib/form-problem.ts +22 -0
- 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
|
-
<
|
|
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,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
|
+
}
|
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
|
+
}
|