@dsh-workbench/shared 0.1.11 → 0.1.13

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 (40) hide show
  1. package/dist/components/ChoiceGroup.css +49 -0
  2. package/dist/components/ChoiceGroup.d.ts +13 -3
  3. package/dist/components/ChoiceGroup.js +38 -5
  4. package/dist/components/StateDot.d.ts +5 -0
  5. package/dist/components/StateDot.js +10 -0
  6. package/dist/host/__tests__/fakes.d.ts +41 -0
  7. package/dist/host/__tests__/fakes.js +76 -0
  8. package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
  9. package/dist/host/__tests__/preflight-fixtures.js +47 -0
  10. package/dist/host/context-inject.d.ts +24 -0
  11. package/dist/host/context-inject.js +74 -0
  12. package/dist/host/events.d.ts +9 -0
  13. package/dist/host/events.js +70 -0
  14. package/dist/host/mount-units.d.ts +52 -0
  15. package/dist/host/mount-units.js +186 -0
  16. package/dist/host/preflight.d.ts +58 -0
  17. package/dist/host/preflight.js +233 -0
  18. package/dist/host/probe-cache.d.ts +6 -0
  19. package/dist/host/probe-cache.js +19 -0
  20. package/dist/host/probe-libs.d.ts +45 -0
  21. package/dist/host/probe-libs.js +61 -0
  22. package/dist/host/probe-units.d.ts +17 -0
  23. package/dist/host/probe-units.js +102 -0
  24. package/dist/host/probe-verdict.d.ts +7 -0
  25. package/dist/host/probe-verdict.js +143 -0
  26. package/dist/host/tool-scope.d.ts +32 -0
  27. package/dist/host/tool-scope.js +84 -0
  28. package/dist/host/unit-readiness.d.ts +7 -0
  29. package/dist/host/unit-readiness.js +5 -0
  30. package/dist/host/unit-registry.d.ts +9 -0
  31. package/dist/host/unit-registry.js +33 -0
  32. package/dist/host/version-range.d.ts +2 -0
  33. package/dist/host/version-range.js +73 -0
  34. package/dist/lib/ui.js +8 -1
  35. package/package.json +1 -1
  36. package/src/components/ChoiceGroup.css +49 -0
  37. package/src/components/ChoiceGroup.test.tsx +201 -0
  38. package/src/components/ChoiceGroup.tsx +112 -19
  39. package/src/lib/ui.test.ts +8 -0
  40. package/src/lib/ui.ts +8 -1
@@ -0,0 +1,201 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Fragment, h } from '../lib/react.js';
3
+ import { ChoiceGroup } from './ChoiceGroup.js';
4
+
5
+ interface AnyElement {
6
+ readonly type: unknown;
7
+ readonly props: Record<string, unknown>;
8
+ }
9
+
10
+ function flatten(node: unknown, out: AnyElement[]): void {
11
+ if (node === null || node === void 0 || typeof node === 'boolean') return;
12
+ if (typeof node === 'string' || typeof node === 'number') return;
13
+ if (Array.isArray(node)) {
14
+ for (const one of node) flatten(one, out);
15
+ return;
16
+ }
17
+ if (typeof node !== 'object') return;
18
+ const element = node as AnyElement;
19
+ if (typeof element.type === 'function') {
20
+ flatten(
21
+ (element.type as (props: unknown) => unknown)(element.props),
22
+ out,
23
+ );
24
+ return;
25
+ }
26
+ out.push(element);
27
+ if (element.props['children'] !== void 0) {
28
+ flatten(element.props['children'], out);
29
+ }
30
+ }
31
+
32
+ function treeOf(element: unknown): readonly AnyElement[] {
33
+ const out: AnyElement[] = [];
34
+ flatten(element, out);
35
+ return out;
36
+ }
37
+
38
+ function inputsOf(element: unknown): readonly AnyElement[] {
39
+ return treeOf(element).filter((one) => one.type === 'input');
40
+ }
41
+
42
+ const OPTIONS = [
43
+ { value: 'tencent-test', label: '腾讯云 · 测试' },
44
+ { value: 'tencent-prod', label: '腾讯云 · 生产' },
45
+ ];
46
+
47
+ describe('ChoiceGroup:单选照旧,多选是新加的那条路', () => {
48
+ it('单选:选中的那一行 checked,点别的行给的是那个字符串', () => {
49
+ const seen: string[] = [];
50
+ const element = ChoiceGroup({
51
+ label: '环境',
52
+ name: 'env',
53
+ value: 'tencent-test',
54
+ options: OPTIONS,
55
+ onChange: (next) => seen.push(next),
56
+ });
57
+ const inputs = inputsOf(element);
58
+
59
+ expect(inputs).toHaveLength(2);
60
+ expect(inputs.map((one) => one.props['type'])).toEqual(['radio', 'radio']);
61
+ expect(inputs.map((one) => one.props['checked'])).toEqual([true, false]);
62
+ expect(
63
+ treeOf(element).filter((node) => node.props['className'] === 'wb-radio'),
64
+ ).toHaveLength(2);
65
+
66
+ (inputs[1]?.props['onChange'] as () => void)();
67
+ expect(seen).toEqual(['tencent-prod']);
68
+ });
69
+
70
+ it('★ 多选:勾上的再点是摘掉,没勾的是加上(给的是整份数组,不是新值)', () => {
71
+ const seen: (readonly string[])[] = [];
72
+ const element = ChoiceGroup({
73
+ multi: true,
74
+ label: '环境',
75
+ name: 'env',
76
+ values: ['tencent-test'],
77
+ options: OPTIONS,
78
+ onChange: (next) => seen.push(next),
79
+ });
80
+ const inputs = inputsOf(element);
81
+
82
+ expect(inputs.map((one) => one.props['type'])).toEqual([
83
+ 'checkbox',
84
+ 'checkbox',
85
+ ]);
86
+ expect(inputs.map((one) => one.props['checked'])).toEqual([true, false]);
87
+ expect(
88
+ treeOf(element).filter((one) => one.props['className'] === 'wb-checkbox'),
89
+ ).toHaveLength(2);
90
+
91
+ (inputs[0]?.props['onChange'] as () => void)();
92
+ (inputs[1]?.props['onChange'] as () => void)();
93
+
94
+ expect(seen).toEqual([[], ['tencent-test', 'tencent-prod']]);
95
+ });
96
+
97
+ it('★ 多选的标签收 ReactNode:两列那种行塞得进去(EnvPicker 靠这条)', () => {
98
+ const element = ChoiceGroup({
99
+ multi: true,
100
+ label: '环境',
101
+ name: 'env',
102
+ values: [],
103
+ options: [
104
+ {
105
+ value: 'tencent-test',
106
+ label: h('span', { className: 'twoColumns' }, 'tencent-test'),
107
+ },
108
+ ],
109
+ onChange: () => {},
110
+ });
111
+
112
+ const labels = treeOf(element).filter(
113
+ (one) => one.props['className'] === 'wb-choiceLabel',
114
+ );
115
+ expect(labels).toHaveLength(1);
116
+ expect(
117
+ treeOf(labels[0]?.props['children']).some(
118
+ (one) => one.props['className'] === 'twoColumns',
119
+ ),
120
+ ).toBe(true);
121
+ });
122
+
123
+ it('多选的容器是 group、单选是 radiogroup(屏幕阅读器读出来的东西不一样)', () => {
124
+ const multi = treeOf(
125
+ ChoiceGroup({
126
+ multi: true,
127
+ label: '环境',
128
+ name: 'env',
129
+ values: [],
130
+ options: OPTIONS,
131
+ onChange: () => {},
132
+ }),
133
+ ).find((one) => one.props['className'] === 'wb-choiceMenu');
134
+ const single = treeOf(
135
+ ChoiceGroup({
136
+ label: '环境',
137
+ name: 'env',
138
+ value: 'tencent-test',
139
+ options: OPTIONS,
140
+ onChange: () => {},
141
+ }),
142
+ ).find((one) => one.props['className'] === 'wb-choiceMenu');
143
+
144
+ expect(multi?.props['role']).toBe('group');
145
+ expect(single?.props['role']).toBe('radiogroup');
146
+ });
147
+
148
+ it('勾上了的行带 wb-choiceOptionOn,没勾的不带', () => {
149
+ const rows = treeOf(
150
+ ChoiceGroup({
151
+ multi: true,
152
+ label: '环境',
153
+ name: 'env',
154
+ values: ['tencent-prod'],
155
+ options: OPTIONS,
156
+ onChange: () => {},
157
+ }),
158
+ ).filter((one) => one.type === 'label');
159
+
160
+ expect(rows.map((one) => one.props['className'])).toEqual([
161
+ 'wb-choiceOption',
162
+ 'wb-choiceOption wb-choiceOptionOn',
163
+ ]);
164
+ });
165
+
166
+ it('多选里勾上了才画那颗勾(没勾就是个空盒子)', () => {
167
+ const boxes = (values: readonly string[]): readonly AnyElement[] =>
168
+ treeOf(
169
+ ChoiceGroup({
170
+ multi: true,
171
+ label: '环境',
172
+ name: 'env',
173
+ values,
174
+ options: OPTIONS,
175
+ onChange: () => {},
176
+ }),
177
+ ).filter((one) => one.props['className'] === 'wb-checkboxBox');
178
+
179
+ expect(boxes([]).map((one) => one.props['children'])).toEqual([null, null]);
180
+ expect(boxes(['tencent-test']).map((one) => one.props['children'])[0]).not.toBeNull();
181
+ });
182
+
183
+ it('禁用照旧传下去', () => {
184
+ const inputs = inputsOf(
185
+ ChoiceGroup({
186
+ multi: true,
187
+ label: '环境',
188
+ name: 'env',
189
+ values: [],
190
+ options: OPTIONS,
191
+ disabled: true,
192
+ onChange: () => {},
193
+ }),
194
+ );
195
+ expect(inputs.every((one) => one.props['disabled'] === true)).toBe(true);
196
+ });
197
+
198
+ it('Fragment 还是 React 自己的(别把 key 语义换掉)', () => {
199
+ expect(Fragment).toBeDefined();
200
+ });
201
+ });
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from '../lib/react.js';
2
2
  import { Fragment, h } from '../lib/react.js';
3
+ import { IconCheckOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';
3
4
  import './ChoiceGroup.css';
4
5
 
5
6
  export interface ChoiceGroupOption {
@@ -7,33 +8,125 @@ export interface ChoiceGroupOption {
7
8
  readonly label: ReactNode;
8
9
  }
9
10
 
10
- export interface ChoiceGroupProps {
11
+ interface ChoiceGroupCommon {
11
12
  readonly label: string;
12
13
  readonly name: string;
13
- readonly value: string;
14
14
  readonly options: readonly ChoiceGroupOption[];
15
15
  readonly disabled?: boolean | undefined;
16
+ }
17
+
18
+ export interface ChoiceGroupSingleProps extends ChoiceGroupCommon {
19
+ readonly multi?: false | undefined;
20
+ readonly value: string;
16
21
  readonly onChange: (next: string) => void;
17
22
  }
18
23
 
19
- export function ChoiceGroup({ label, name, value, options, disabled, onChange }: ChoiceGroupProps) {
24
+ export interface ChoiceGroupMultiProps extends ChoiceGroupCommon {
25
+ readonly multi: true;
26
+ readonly values: readonly string[];
27
+ readonly onChange: (next: readonly string[]) => void;
28
+ }
29
+
30
+ export type ChoiceGroupProps = ChoiceGroupSingleProps | ChoiceGroupMultiProps;
31
+
32
+ interface ChoiceRowState {
33
+ readonly checked: boolean;
34
+ readonly onSelect: () => void;
35
+ }
36
+
37
+ function ChoiceMenu(input: {
38
+ readonly label: string;
39
+ readonly name: string;
40
+ readonly options: readonly ChoiceGroupOption[];
41
+ readonly disabled: boolean;
42
+ readonly multi: boolean;
43
+ readonly rowOf: (value: string) => ChoiceRowState;
44
+ }) {
20
45
  return (
21
- <div className="wb-choiceMenu" role="radiogroup" aria-label={label}>
22
- {options.map((option) => (
23
- <Fragment key={option.value}>
24
- <label className="wb-choiceOption">
25
- <input
26
- className="wb-radio"
27
- type="radio"
28
- name={name}
29
- checked={option.value === value}
30
- disabled={disabled === true}
31
- onChange={() => onChange(option.value)}
32
- />
33
- <span className="wb-choiceLabel">{option.label}</span>
34
- </label>
35
- </Fragment>
36
- ))}
46
+ <div
47
+ className="wb-choiceMenu"
48
+ role={input.multi ? 'group' : 'radiogroup'}
49
+ aria-label={input.label}
50
+ >
51
+ {input.options.map((option) => {
52
+ const row = input.rowOf(option.value);
53
+ return (
54
+ <Fragment key={option.value}>
55
+ <label
56
+ className={`wb-choiceOption${row.checked ? ' wb-choiceOptionOn' : ''}`}
57
+ >
58
+ <input
59
+ className={input.multi ? 'wb-checkbox' : 'wb-radio'}
60
+ type={input.multi ? 'checkbox' : 'radio'}
61
+ name={input.name}
62
+ checked={row.checked}
63
+ disabled={input.disabled}
64
+ onChange={row.onSelect}
65
+ />
66
+ {input.multi ? (
67
+ <span
68
+ className="wb-checkboxBox"
69
+ aria-hidden="true"
70
+ >
71
+ {row.checked ? <IconCheckOutlineRegular size={12} /> : null}
72
+ </span>
73
+ ) : null}
74
+ <span className="wb-choiceLabel">{option.label}</span>
75
+ </label>
76
+ </Fragment>
77
+ );
78
+ })}
37
79
  </div>
38
80
  );
39
81
  }
82
+
83
+ export function ChoiceGroup(props: ChoiceGroupProps) {
84
+ const common = {
85
+ label: props.label,
86
+ name: props.name,
87
+ options: props.options,
88
+ disabled: props.disabled === true,
89
+ };
90
+
91
+ if (props.multi === true) {
92
+ const values = props.values;
93
+ const onChange = props.onChange;
94
+ return (
95
+ <ChoiceMenu
96
+ label={common.label}
97
+ name={common.name}
98
+ options={common.options}
99
+ disabled={common.disabled}
100
+ multi={true}
101
+ rowOf={(value) => ({
102
+ checked: values.includes(value),
103
+ onSelect: () => {
104
+ onChange(
105
+ values.includes(value)
106
+ ? values.filter((one) => one !== value)
107
+ : [...values, value],
108
+ );
109
+ },
110
+ })}
111
+ />
112
+ );
113
+ }
114
+
115
+ const value = props.value;
116
+ const onChange = props.onChange;
117
+ return (
118
+ <ChoiceMenu
119
+ label={common.label}
120
+ name={common.name}
121
+ options={common.options}
122
+ disabled={common.disabled}
123
+ multi={false}
124
+ rowOf={(option) => ({
125
+ checked: option === value,
126
+ onSelect: () => {
127
+ onChange(option);
128
+ },
129
+ })}
130
+ />
131
+ );
132
+ }
@@ -45,6 +45,14 @@ describe('relativeTime', () => {
45
45
  expect(relativeTime(NOW - 7 * 24 * 60 * 60_000, t)).not.toContain('time.');
46
46
  });
47
47
 
48
+ it('★ 过了 7 天要给到时分秒:只剩日子的话,"哪一次同步"根本分不出来', () => {
49
+ const old = relativeTime(NOW - 8 * 24 * 60 * 60_000, t);
50
+
51
+ expect(old).not.toContain('time.');
52
+ expect(old.match(/:/g)).toHaveLength(2);
53
+ expect(old).toContain(String(new Date(NOW).getFullYear()));
54
+ });
55
+
48
56
  it('非法时间戳给空串而不是 NaN', () => {
49
57
  expect(relativeTime(Number.NaN, t)).toBe('');
50
58
  expect(relativeTime(Number.POSITIVE_INFINITY, t)).toBe('');
package/src/lib/ui.ts CHANGED
@@ -13,7 +13,14 @@ export function relativeTime(epochMs: number, t: TimeTranslate): string {
13
13
  if (diff < 60 * MINUTE) return t('time.minutes', { n: Math.floor(diff / MINUTE) });
14
14
  if (diff < 24 * 60 * MINUTE) return t('time.hours', { n: Math.floor(diff / (60 * MINUTE)) });
15
15
  if (diff < 7 * 24 * 60 * MINUTE) return t('time.days', { n: Math.floor(diff / (24 * 60 * MINUTE)) });
16
- return new Date(epochMs).toLocaleDateString();
16
+ return new Date(epochMs).toLocaleString(undefined, {
17
+ year: 'numeric',
18
+ month: 'numeric',
19
+ day: 'numeric',
20
+ hour: '2-digit',
21
+ minute: '2-digit',
22
+ second: '2-digit',
23
+ });
17
24
  }
18
25
 
19
26
  export function shortenPath(path: unknown): string {