@dsh-workbench/shared 0.3.4 → 0.3.6
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/api/blocker-table.d.ts +13 -0
- package/dist/api/blocker-table.js +13 -0
- package/dist/api/blocker.d.ts +6 -0
- package/dist/api/context-ref.d.ts +31 -0
- package/dist/api/context-ref.js +28 -0
- package/dist/api/dsh-client.d.ts +99 -0
- package/dist/api/dsh.d.ts +55 -3
- package/dist/api/facts.d.ts +42 -0
- package/dist/api/facts.js +33 -0
- package/dist/api/host-write.d.ts +18 -0
- package/dist/api/host-write.js +12 -0
- package/dist/api/long-task.d.ts +66 -1
- package/dist/api/long-task.js +59 -0
- package/dist/api/unit-board.d.ts +25 -0
- package/dist/api/unit-board.js +16 -0
- package/dist/api/unit-declaration.d.ts +36 -0
- package/dist/api/unit-declaration.js +24 -0
- package/dist/api/unit.d.ts +32 -0
- package/dist/api/unit.js +5 -0
- package/dist/api/workbench-client.d.ts +25 -0
- package/dist/api/workbench-client.js +13 -0
- package/dist/api/workbench-contract.d.ts +12 -0
- package/dist/components/Activity.d.ts +33 -0
- package/dist/components/Activity.js +24 -0
- package/dist/components/ApprovalDetail.d.ts +12 -0
- package/dist/components/ApprovalDetail.js +9 -0
- package/dist/components/ApprovalPlan.d.ts +14 -0
- package/dist/components/ApprovalPlan.js +8 -0
- package/dist/components/Card.d.ts +13 -0
- package/dist/components/Card.js +10 -0
- package/dist/components/CardAnchor.d.ts +14 -0
- package/dist/components/CardAnchor.js +11 -0
- package/dist/components/CardBoundary.d.ts +20 -0
- package/dist/components/CardBoundary.js +14 -0
- package/dist/components/CardHead.d.ts +13 -0
- package/dist/components/CardHead.js +10 -0
- package/dist/components/ChoiceGroup.d.ts +24 -0
- package/dist/components/ChoiceGroup.js +12 -0
- package/dist/components/Field.d.ts +10 -0
- package/dist/components/Field.js +7 -0
- package/dist/components/Form.css +6 -0
- package/dist/components/Form.d.ts +20 -0
- package/dist/components/Form.js +58 -0
- package/dist/components/FormActions.d.ts +14 -0
- package/dist/components/FormActions.js +5 -0
- package/dist/components/FormNodes.d.ts +103 -0
- package/dist/components/FormNodes.js +153 -0
- package/dist/components/FormRows.css +31 -0
- package/dist/components/FormRows.d.ts +20 -0
- package/dist/components/FormRows.js +14 -0
- package/dist/components/FormTypes.d.ts +59 -0
- package/dist/components/FormTypes.js +1 -0
- package/dist/components/Link.d.ts +12 -0
- package/dist/components/Link.js +9 -0
- package/dist/components/LongTaskRow.css +52 -0
- package/dist/components/LongTaskRow.d.ts +35 -0
- package/dist/components/LongTaskRow.js +45 -4
- package/dist/components/Note.d.ts +11 -0
- package/dist/components/Note.js +8 -0
- package/dist/components/ResourceStates.d.ts +25 -0
- package/dist/components/ResourceStates.js +22 -0
- package/dist/components/Rows.d.ts +17 -0
- package/dist/components/Rows.js +11 -0
- package/dist/components/SaveBar.d.ts +12 -0
- package/dist/components/SaveBar.js +9 -0
- package/dist/components/SelectField.d.ts +26 -0
- package/dist/components/SelectField.js +22 -0
- package/dist/components/SelectFieldNodes.d.ts +24 -0
- package/dist/components/SelectFieldNodes.js +24 -0
- package/dist/components/SettingsSection.d.ts +11 -0
- package/dist/components/SettingsSection.js +8 -0
- package/dist/components/Spinner.d.ts +9 -0
- package/dist/components/Spinner.js +6 -0
- package/dist/components/Stack.d.ts +8 -0
- package/dist/components/Stack.js +5 -0
- package/dist/components/StateBlock.d.ts +13 -0
- package/dist/components/StateBlock.js +10 -0
- package/dist/components/TextField.d.ts +8 -0
- package/dist/components/TextField.js +5 -0
- package/dist/components/board-module.d.ts +30 -0
- package/dist/components/icons.d.ts +38 -0
- package/dist/components/icons.js +35 -0
- package/dist/config/constants.d.ts +42 -0
- package/dist/config/constants.js +42 -0
- package/dist/config/storage.d.ts +4 -0
- package/dist/config/storage.js +4 -0
- package/dist/host/long-task.d.ts +7 -0
- package/dist/host/long-task.js +31 -5
- package/dist/host/tool-approval.d.ts +1 -0
- package/dist/host/unit-face.d.ts +6 -1
- package/dist/i18n/en.d.ts +1 -0
- package/dist/i18n/en.js +10 -8
- package/dist/i18n/index.d.ts +13 -9
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/merge.d.ts +5 -0
- package/dist/i18n/merge.js +2 -0
- package/dist/i18n/zh.d.ts +11 -8
- package/dist/i18n/zh.js +10 -8
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/lib/blocker-action.d.ts +22 -0
- package/dist/lib/blocker-action.js +13 -0
- package/dist/lib/board-fact.d.ts +27 -0
- package/dist/lib/board-fact.js +18 -0
- package/dist/lib/busy.d.ts +10 -0
- package/dist/lib/busy.js +7 -0
- package/dist/lib/diy-debug.d.ts +42 -2
- package/dist/lib/diy-debug.js +19 -1
- package/dist/lib/diy-record.d.ts +41 -0
- package/dist/lib/diy-record.js +17 -0
- package/dist/lib/form-problem.d.ts +10 -0
- package/dist/lib/form-problem.js +10 -0
- package/dist/lib/panel-state.d.ts +60 -0
- package/dist/lib/panel-state.js +30 -0
- package/dist/lib/react.d.ts +39 -0
- package/dist/lib/react.js +36 -0
- package/dist/lib/text.d.ts +22 -0
- package/dist/lib/text.js +22 -0
- package/dist/lib/ui.d.ts +50 -0
- package/dist/lib/ui.js +41 -0
- package/package.json +1 -1
- package/src/api/__tests__/long-task.test.ts +41 -1
- package/src/api/blocker-table.ts +13 -0
- package/src/api/blocker.ts +6 -0
- package/src/api/context-ref.ts +31 -0
- package/src/api/dsh-client.ts +99 -0
- package/src/api/dsh.ts +56 -1
- package/src/api/facts.ts +42 -0
- package/src/api/host-write.ts +18 -0
- package/src/api/long-task.ts +79 -1
- package/src/api/unit-board.ts +25 -0
- package/src/api/unit-declaration.ts +36 -0
- package/src/api/unit.ts +32 -0
- package/src/api/workbench-client.ts +25 -0
- package/src/api/workbench-contract.ts +12 -0
- package/src/components/Activity.tsx +33 -0
- package/src/components/ApprovalDetail.tsx +12 -0
- package/src/components/ApprovalPlan.tsx +14 -0
- package/src/components/Card.tsx +13 -0
- package/src/components/CardAnchor.tsx +14 -0
- package/src/components/CardBoundary.tsx +20 -0
- package/src/components/CardHead.tsx +13 -0
- package/src/components/ChoiceGroup.tsx +24 -0
- package/src/components/Field.tsx +10 -0
- package/src/components/Form.css +6 -0
- package/src/components/Form.test.tsx +475 -0
- package/src/components/Form.tsx +78 -0
- package/src/components/FormActions.tsx +14 -0
- package/src/components/FormNodes.tsx +193 -0
- package/src/components/FormRows.css +31 -0
- package/src/components/FormRows.test.tsx +71 -1
- package/src/components/FormRows.tsx +20 -0
- package/src/components/FormTypes.ts +65 -0
- package/src/components/FormWiring.test.tsx +88 -0
- package/src/components/Link.tsx +12 -0
- package/src/components/LongTaskRow.css +52 -0
- package/src/components/LongTaskRow.tsx +76 -2
- package/src/components/Note.tsx +11 -0
- package/src/components/ResourceStates.tsx +25 -0
- package/src/components/Rows.tsx +17 -0
- package/src/components/SaveBar.tsx +12 -0
- package/src/components/SelectField.tsx +26 -0
- package/src/components/SelectFieldNodes.tsx +24 -0
- package/src/components/SettingsSection.tsx +11 -0
- package/src/components/Spinner.tsx +9 -0
- package/src/components/Stack.tsx +8 -0
- package/src/components/StateBlock.tsx +13 -0
- package/src/components/TextField.tsx +8 -0
- package/src/components/__tests__/long-task-row.test.ts +115 -0
- package/src/components/board-module.ts +30 -0
- package/src/components/icons.tsx +38 -0
- package/src/config/constants.ts +43 -0
- package/src/config/storage.ts +4 -0
- package/src/host/__tests__/long-task.test.ts +264 -0
- package/src/host/long-task.ts +43 -7
- package/src/host/tool-approval.ts +1 -0
- package/src/host/unit-face.ts +7 -1
- package/src/i18n/en.ts +11 -8
- package/src/i18n/index.ts +3 -0
- package/src/i18n/merge.ts +5 -0
- package/src/i18n/zh.ts +11 -8
- package/src/index.ts +6 -2
- package/src/lib/blocker-action.ts +22 -0
- package/src/lib/board-fact.ts +27 -0
- package/src/lib/busy.ts +10 -0
- package/src/lib/diy-debug.test.ts +2 -1
- package/src/lib/diy-debug.ts +44 -3
- package/src/lib/diy-record.ts +41 -0
- package/src/lib/form-problem.ts +10 -0
- package/src/lib/panel-state.ts +60 -0
- package/src/lib/react.ts +39 -0
- package/src/lib/text.ts +22 -0
- package/src/lib/ui.ts +50 -0
|
@@ -4,6 +4,9 @@ import type { ReactNode } from '../lib/react.js';
|
|
|
4
4
|
import { Component, h } from '../lib/react.js';
|
|
5
5
|
import { Note } from './Note.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* `CardBoundary` 的契约:被守的 unit、重来键与失败回调。
|
|
9
|
+
*/
|
|
7
10
|
export interface CardBoundaryProps {
|
|
8
11
|
readonly unit: string;
|
|
9
12
|
readonly resetKey: string;
|
|
@@ -12,15 +15,32 @@ export interface CardBoundaryProps {
|
|
|
12
15
|
readonly children?: ReactNode | undefined;
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
/**
|
|
19
|
+
* `CardBoundary` 的状态:这次渲染炸出来的那句话,没炸是 `null`。
|
|
20
|
+
*/
|
|
15
21
|
export interface CardBoundaryState {
|
|
16
22
|
readonly reason: string | null;
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 把抛出来的任何东西收成一句人能看的失败原因,上报与展示都用它。
|
|
27
|
+
* @param error - 抛出来的值:`Error` 取 `message`,字符串原样,其余走 JSON。
|
|
28
|
+
* @returns 一句原因文本;`Error('')` 这种没带 message 的会回空串(调用点别把它当"一定有话")。
|
|
29
|
+
*/
|
|
19
30
|
export function errorReasonOf(error: unknown): string {
|
|
20
31
|
if (error instanceof Error) return error.message;
|
|
21
32
|
return typeof error === 'string' ? error : JSON.stringify(error) ?? String(error);
|
|
22
33
|
}
|
|
23
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 卡片级错误边界:一张卡渲染炸了只画一条报错 + 重试,不让整块面板跟着塌。
|
|
37
|
+
* @param props.unit - 出事的 unit id,写进报错文案并作为上报标识。
|
|
38
|
+
* @param props.resetKey - 重来键:它一变(如切了工作区)就自动清掉报错,把卡片重新渲染一遍。
|
|
39
|
+
* @param props.t - 报错那句与重试钮文案的翻译函数。
|
|
40
|
+
* @param props.onFailure - 失败时把原因报给宿主(面板状态要用);不给就只打一条 console。
|
|
41
|
+
* @param props.children - 正常时要渲染的卡片内容。
|
|
42
|
+
* @returns 正常时是 `children`,炸过且还没重来之前是一条报错。
|
|
43
|
+
*/
|
|
24
44
|
export class CardBoundary extends Component<CardBoundaryProps, CardBoundaryState> {
|
|
25
45
|
override state: CardBoundaryState = { reason: null };
|
|
26
46
|
|
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
import { h } from '../lib/react.js';
|
|
11
11
|
import './CardHead.css';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* `CardHead` 的契约:标题、补充、刷新,外加齿轮与平台跳转两格。
|
|
15
|
+
*/
|
|
13
16
|
export interface CardHeadProps {
|
|
14
17
|
readonly t: Translate;
|
|
15
18
|
readonly title: string;
|
|
@@ -24,6 +27,16 @@ export interface CardHeadProps {
|
|
|
24
27
|
readonly platformTitle?: string | undefined;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 卡头:标题 + 补充 + 刷新钮(`Card` 内部就是它),要自己拼卡头或加齿轮 / 平台跳转时直接用。
|
|
32
|
+
* @param props.t - 刷新那两句文案(刷新 / 刷新中)的翻译函数。
|
|
33
|
+
* @param props.title - 卡标题;`props.meta` 是它右侧那一小段补充,不给不占位。
|
|
34
|
+
* @param props.refreshing - 正在刷新:刷新钮禁用并转圈。
|
|
35
|
+
* @param props.onRefresh - 点刷新钮做什么。
|
|
36
|
+
* @param props.gear - 齿轮钮的读屏名;`gearOpen` 控制它高亮、`onToggleGear` 是点击回调,三者一起给才画。
|
|
37
|
+
* @param props.platformHref - 整张卡对应系统的地址:给了就画那颗 ↗(新窗口),tooltip 用 `platformTitle`,缺了退化用 `platform`。
|
|
38
|
+
* @returns 一个 `<header>`。
|
|
39
|
+
*/
|
|
27
40
|
export function CardHead({
|
|
28
41
|
t,
|
|
29
42
|
title,
|
|
@@ -4,6 +4,9 @@ import { IconCheckOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
|
4
4
|
import { Spinner } from './Spinner.js';
|
|
5
5
|
import './ChoiceGroup.css';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 一个候选项:值、人看的标签,可带一行说明。
|
|
9
|
+
*/
|
|
7
10
|
export interface ChoiceGroupOption {
|
|
8
11
|
readonly value: string;
|
|
9
12
|
readonly label: ReactNode;
|
|
@@ -21,18 +24,27 @@ interface ChoiceGroupCommon {
|
|
|
21
24
|
readonly emptyText?: ReactNode | undefined;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 单选那一支的契约:`value` + `onChange(next)`;`multi` 不给或给 false。
|
|
29
|
+
*/
|
|
24
30
|
export interface ChoiceGroupSingleProps extends ChoiceGroupCommon {
|
|
25
31
|
readonly multi?: false | undefined;
|
|
26
32
|
readonly value: string;
|
|
27
33
|
readonly onChange: (next: string) => void;
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 多选那一支的契约:`multi: true` + `values` + `onChange(next[])`。
|
|
38
|
+
*/
|
|
30
39
|
export interface ChoiceGroupMultiProps extends ChoiceGroupCommon {
|
|
31
40
|
readonly multi: true;
|
|
32
41
|
readonly values: readonly string[];
|
|
33
42
|
readonly onChange: (next: readonly string[]) => void;
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
/**
|
|
46
|
+
* `ChoiceGroup` 的契约:单选与多选两支的联合。
|
|
47
|
+
*/
|
|
36
48
|
export type ChoiceGroupProps = ChoiceGroupSingleProps | ChoiceGroupMultiProps;
|
|
37
49
|
|
|
38
50
|
interface ChoiceRowState {
|
|
@@ -110,6 +122,18 @@ function ChoiceMenu(input: {
|
|
|
110
122
|
);
|
|
111
123
|
}
|
|
112
124
|
|
|
125
|
+
/**
|
|
126
|
+
* 一组单选或多选:候选少、要一眼看全时用它;候选多或要搜索就用 `SelectField`。
|
|
127
|
+
* @param props.multi - true 走多选那支(`values` / `onChange(string[])`);默认单选(`value` / `onChange(string)`)。
|
|
128
|
+
* @param props.label - 只给读屏的组名(`aria-label`)。
|
|
129
|
+
* @param props.name - 这一组 `input` 的共用 name。
|
|
130
|
+
* @param props.options - 候选项,按给的顺序排;空数组时画 `emptyText`(没给就什么都不画)。
|
|
131
|
+
* @param props.disabled - 禁掉整组(默认 false),不是单颗。
|
|
132
|
+
* @param props.variant - `boxed`(默认,带边框的选项格)或 `ghost`(无边框,跟着父容器走)。
|
|
133
|
+
* @param props.loading - 正在读候选:整组换成转圈,文案取 `loadingText`(不给是 `…`);默认 false。
|
|
134
|
+
* @param props.emptyText - 读到了但一个候选都没有时那句话;不给不画。
|
|
135
|
+
* @returns 一组 `radio` / `checkbox` 选项。
|
|
136
|
+
*/
|
|
113
137
|
export function ChoiceGroup(props: ChoiceGroupProps) {
|
|
114
138
|
const common = {
|
|
115
139
|
label: props.label,
|
package/src/components/Field.tsx
CHANGED
|
@@ -2,12 +2,22 @@ import type { ReactNode } from '../lib/react.js';
|
|
|
2
2
|
import { h } from '../lib/react.js';
|
|
3
3
|
import './Field.css';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* `Field` 的契约:一行"标签 + 控件 + 说明"的排布。
|
|
7
|
+
*/
|
|
5
8
|
export interface FieldProps {
|
|
6
9
|
readonly label: ReactNode;
|
|
7
10
|
readonly hint?: ReactNode | undefined;
|
|
8
11
|
readonly children?: ReactNode | undefined;
|
|
9
12
|
}
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 标签 + 控件的排布:没进 `FormRow` 时用它兜住一个字段的标签与说明。
|
|
16
|
+
* @param props.label - 左侧标签,必给。
|
|
17
|
+
* @param props.hint - 控件下面那行说明;不给不画。
|
|
18
|
+
* @param props.children - 控件本体,落在标签与说明之间。
|
|
19
|
+
* @returns 一个 `<label>`,点标签会聚焦里面的控件。
|
|
20
|
+
*/
|
|
11
21
|
export function Field({ label, hint, children }: FieldProps) {
|
|
12
22
|
return (
|
|
13
23
|
<label className="wb-field">
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { FormField, FormValue } from './FormTypes.js';
|
|
4
|
+
import {
|
|
5
|
+
duplicateKeysOf,
|
|
6
|
+
formActionsOf,
|
|
7
|
+
formFirstProblemOf,
|
|
8
|
+
formProblemsOf,
|
|
9
|
+
formRowsOf,
|
|
10
|
+
formSubmitOf,
|
|
11
|
+
formValuesOf,
|
|
12
|
+
reportDuplicateKeys,
|
|
13
|
+
visibleFieldsOf,
|
|
14
|
+
} from './FormNodes.js';
|
|
15
|
+
import { FormRow } from './FormRows.js';
|
|
16
|
+
import type { FormAction } from './FormActions.js';
|
|
17
|
+
|
|
18
|
+
function field(key: string, value: FormValue, validate?: FormField['validate']): FormField {
|
|
19
|
+
return {
|
|
20
|
+
key,
|
|
21
|
+
label: key,
|
|
22
|
+
value,
|
|
23
|
+
control: (binding) => `${key}:${binding.id}:${String(binding.invalid)}:${String(binding.disabled)}`,
|
|
24
|
+
...(validate === undefined ? {} : { validate }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function propsOf(node: unknown): Record<string, unknown> {
|
|
29
|
+
return (node as { props: Record<string, unknown> }).props;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function typeOf(node: unknown): unknown {
|
|
33
|
+
return (node as { type: unknown }).type;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function keyOf(node: unknown): unknown {
|
|
37
|
+
return (node as { key: unknown }).key;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('visibleFieldsOf', () => {
|
|
41
|
+
it('★ visible: false 的那格当它不存在(不渲染、不校验、不进 values)', () => {
|
|
42
|
+
const fields = [field('env', 'prod'), { ...field('secret', 'x'), visible: false }, field('app', 'a')];
|
|
43
|
+
|
|
44
|
+
expect(visibleFieldsOf(fields).map((one) => one.key)).toEqual(['env', 'app']);
|
|
45
|
+
expect(formValuesOf(visibleFieldsOf(fields))).toEqual({ env: 'prod', app: 'a' });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('visible 不给或给 true 都留在表里;顺序不变', () => {
|
|
49
|
+
const fields = [field('a', '1'), { ...field('b', '2'), visible: true }, { ...field('c', '3'), visible: true }];
|
|
50
|
+
|
|
51
|
+
expect(visibleFieldsOf(fields).map((one) => one.key)).toEqual(['a', 'b', 'c']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('全不可见时给空数组(当提交时的表就是空的)', () => {
|
|
55
|
+
expect(visibleFieldsOf([{ ...field('a', '1'), visible: false }])).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('duplicateKeysOf / reportDuplicateKeys', () => {
|
|
60
|
+
it('★ 重复的键只报一次、按首次出现顺序;没有重复给空数组', () => {
|
|
61
|
+
expect(duplicateKeysOf([field('a', ''), field('a', ''), field('a', ''), field('b', '')])).toEqual(['a']);
|
|
62
|
+
expect(duplicateKeysOf([field('a', ''), field('b', '')])).toEqual([]);
|
|
63
|
+
expect(duplicateKeysOf([])).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('★ 空 key 也算装配错(它会让 id 撞成同一个)', () => {
|
|
67
|
+
expect(duplicateKeysOf([field('', '')])).toEqual(['']);
|
|
68
|
+
expect(duplicateKeysOf([field('', ''), field('a', '')])).toEqual(['']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('★ 运行期真漏给 key(undefined / null)也算一处,名字印成(空 key)', () => {
|
|
72
|
+
const missing = { ...field('gone', ''), key: undefined } as unknown as FormField;
|
|
73
|
+
const alsoMissing = { ...field('gone2', ''), key: null } as unknown as FormField;
|
|
74
|
+
|
|
75
|
+
expect(duplicateKeysOf([missing])).toEqual(['']);
|
|
76
|
+
expect(duplicateKeysOf([alsoMissing])).toEqual(['']);
|
|
77
|
+
expect(duplicateKeysOf([missing, alsoMissing])).toEqual(['']);
|
|
78
|
+
|
|
79
|
+
const calls: unknown[][] = [];
|
|
80
|
+
const original = console.error;
|
|
81
|
+
console.error = (...args: unknown[]) => void calls.push(args);
|
|
82
|
+
try {
|
|
83
|
+
reportDuplicateKeys([missing]);
|
|
84
|
+
} finally {
|
|
85
|
+
console.error = original;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expect(calls).toHaveLength(1);
|
|
89
|
+
expect(String(calls[0]?.[0])).toContain('(空 key)');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('★ 装配错了只给 console.error,不 throw(照 FormActions 多一颗 primary 的形式)', () => {
|
|
93
|
+
const calls: unknown[][] = [];
|
|
94
|
+
const original = console.error;
|
|
95
|
+
console.error = (...args: unknown[]) => void calls.push(args);
|
|
96
|
+
try {
|
|
97
|
+
expect(() => reportDuplicateKeys([field('a', ''), field('a', '')])).not.toThrow();
|
|
98
|
+
} finally {
|
|
99
|
+
console.error = original;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
expect(calls).toHaveLength(1);
|
|
103
|
+
const message = String(calls[0]?.[0]);
|
|
104
|
+
expect(message).toContain('[dsh-workbench]');
|
|
105
|
+
expect(message).toContain('a');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('装配没错就一声不响', () => {
|
|
109
|
+
const calls: unknown[][] = [];
|
|
110
|
+
const original = console.error;
|
|
111
|
+
console.error = (...args: unknown[]) => void calls.push(args);
|
|
112
|
+
try {
|
|
113
|
+
reportDuplicateKeys([field('a', ''), field('b', '')]);
|
|
114
|
+
} finally {
|
|
115
|
+
console.error = original;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
expect(calls).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('formValuesOf', () => {
|
|
123
|
+
it('★ 每格的 key 与 value 落成一张表(顺序就是字段顺序)', () => {
|
|
124
|
+
const values = formValuesOf([field('env', 'prod'), field('app', 'uedyhsj')]);
|
|
125
|
+
|
|
126
|
+
expect(values).toEqual({ env: 'prod', app: 'uedyhsj' });
|
|
127
|
+
expect(Object.keys(values)).toEqual(['env', 'app']);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('★ 多选那份(一组值)原样带出,不被摊平、不被复制', () => {
|
|
131
|
+
const chosen = ['prod', 'staging'] as const;
|
|
132
|
+
|
|
133
|
+
expect(formValuesOf([field('envs', chosen)])['envs']).toBe(chosen);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('空表单给空表(不是 undefined)', () => {
|
|
137
|
+
expect(formValuesOf([])).toEqual({});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('同键后写覆盖前写(表单里键本就该唯一,重复由 reportDuplicateKeys 报出来)', () => {
|
|
141
|
+
expect(formValuesOf([field('env', 'a'), field('env', 'b')])).toEqual({ env: 'b' });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('formProblemsOf', () => {
|
|
146
|
+
const fields = [
|
|
147
|
+
field('project', '', () => '这个项目不存在'),
|
|
148
|
+
field('env', 'prod', () => undefined),
|
|
149
|
+
field('note', 'x', (value) => (value === 'x' ? '备注不能是 x' : undefined)),
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
it('★ 还没提交过:一格错都不显示(不边打字边骂人)', () => {
|
|
153
|
+
expect(formProblemsOf(fields, false)).toEqual({ project: undefined, env: undefined, note: undefined });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('★ 提交过:逐格重算,有错的给那句文案、没错的给 undefined', () => {
|
|
157
|
+
const problems = formProblemsOf(fields, true);
|
|
158
|
+
|
|
159
|
+
expect(problems['project']).toBe('这个项目不存在');
|
|
160
|
+
expect(problems['env']).toBeUndefined();
|
|
161
|
+
expect(problems['note']).toBe('备注不能是 x');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('没给 validate 的格子永远是 undefined(两层态都不报)', () => {
|
|
165
|
+
const plain = [field('env', 'prod')];
|
|
166
|
+
|
|
167
|
+
expect(formProblemsOf(plain, true)['env']).toBeUndefined();
|
|
168
|
+
expect(formProblemsOf(plain, false)['env']).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('★ 假值都算没错:undefined / null / false / 空串四种一格都不挡', () => {
|
|
172
|
+
const falsy = [undefined, null, false, ''] as const;
|
|
173
|
+
const fields = falsy.map((value, index) => field(`f${String(index)}`, 'x', () => value));
|
|
174
|
+
|
|
175
|
+
const problems = formProblemsOf(fields, true);
|
|
176
|
+
|
|
177
|
+
for (const one of fields) expect(problems[one.key]).toBeUndefined();
|
|
178
|
+
expect(formFirstProblemOf(fields, problems)).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('★ 真值才算错(`&&` 那种最顺手的写法是对的)', () => {
|
|
182
|
+
const required: FormField['validate'] = (value) =>
|
|
183
|
+
typeof value === 'string' && value.trim() === '' && '必填';
|
|
184
|
+
const empty = field('project', '', required);
|
|
185
|
+
const filled = field('note', 'ok', required);
|
|
186
|
+
|
|
187
|
+
expect(formProblemsOf([empty, filled], true)).toEqual({ project: '必填', note: undefined });
|
|
188
|
+
expect(formFirstProblemOf([empty, filled], formProblemsOf([empty, filled], true))?.key).toBe('project');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('validate 读得到整张表(第二格能看见第一格的值)', () => {
|
|
192
|
+
const cross = [
|
|
193
|
+
field('a', '1'),
|
|
194
|
+
field('b', '2', (_value, values) => (values['a'] === '1' ? 'a 是 1 就不行' : undefined)),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
expect(formProblemsOf(cross, true)['b']).toBe('a 是 1 就不行');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('★ 不可见那格不跑 validate:两条互证 —— 未过滤会被调,过滤后不被调', () => {
|
|
201
|
+
const calls: string[] = [];
|
|
202
|
+
const hidden: FormField = {
|
|
203
|
+
...field('secret', 'x', () => {
|
|
204
|
+
calls.push('secret');
|
|
205
|
+
return '永远不该被算';
|
|
206
|
+
}),
|
|
207
|
+
visible: false,
|
|
208
|
+
};
|
|
209
|
+
const fields = [field('env', 'prod'), hidden];
|
|
210
|
+
|
|
211
|
+
formProblemsOf(fields, true);
|
|
212
|
+
expect(calls).toEqual(['secret']);
|
|
213
|
+
|
|
214
|
+
calls.length = 0;
|
|
215
|
+
const problems = formProblemsOf(visibleFieldsOf(fields), true);
|
|
216
|
+
|
|
217
|
+
expect(calls).toEqual([]);
|
|
218
|
+
expect(problems).toEqual({ env: undefined });
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('formFirstProblemOf', () => {
|
|
223
|
+
it('★ 按字段顺序取第一处(不是按表里的键序)', () => {
|
|
224
|
+
const fields = [field('a', ''), field('b', ''), field('c', '')];
|
|
225
|
+
const problems = { c: 'c 错', b: 'b 错', a: 'a 错' };
|
|
226
|
+
|
|
227
|
+
expect(formFirstProblemOf(fields, problems)?.key).toBe('a');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('★ 一格都没错给 undefined(那一拍不滚、不抢焦点)', () => {
|
|
231
|
+
expect(formFirstProblemOf([field('a', 'ok')], { a: undefined })).toBeUndefined();
|
|
232
|
+
expect(formFirstProblemOf([], {})).toBeUndefined();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('formSubmitOf', () => {
|
|
237
|
+
it('★ 全过 ⇒ blocked: false,交出来的键就是那几个(值现取)', () => {
|
|
238
|
+
const outcome = formSubmitOf([field('env', 'prod'), field('app', 'uedyhsj')]);
|
|
239
|
+
|
|
240
|
+
expect(outcome.blocked).toBe(false);
|
|
241
|
+
expect(outcome.blocked ? {} : outcome.values).toEqual({ env: 'prod', app: 'uedyhsj' });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('★ 任一格有错 ⇒ blocked: true,first 是按字段顺序的第一格(不是随便一格)', () => {
|
|
245
|
+
const fields = [
|
|
246
|
+
field('a', '', () => undefined),
|
|
247
|
+
field('b', '', () => 'b 错了'),
|
|
248
|
+
field('c', '', () => 'c 也错了'),
|
|
249
|
+
];
|
|
250
|
+
const outcome = formSubmitOf(fields);
|
|
251
|
+
|
|
252
|
+
expect(outcome.blocked).toBe(true);
|
|
253
|
+
expect(outcome.blocked ? outcome.first.key : undefined).toBe('b');
|
|
254
|
+
expect(outcome.blocked ? 'values' in outcome : true).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('★ 第一格就错时 first 是它(顺序判据两头都咬得住)', () => {
|
|
258
|
+
const fields = [field('a', '', () => 'a 错了'), field('b', '', () => 'b 也错了')];
|
|
259
|
+
const outcome = formSubmitOf(fields);
|
|
260
|
+
|
|
261
|
+
expect(outcome.blocked ? outcome.first.key : undefined).toBe('a');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('★ validate 看到的是整张 values(跨字段那种校验做得了)', () => {
|
|
265
|
+
const bothFilled: FormField['validate'] = (_value, values) =>
|
|
266
|
+
values['a'] !== '' && values['b'] !== '' ? '两个不能同时填' : undefined;
|
|
267
|
+
const fields = [field('a', 'x'), field('b', 'y', bothFilled)];
|
|
268
|
+
const outcome = formSubmitOf(fields);
|
|
269
|
+
|
|
270
|
+
expect(outcome.blocked).toBe(true);
|
|
271
|
+
expect(outcome.blocked ? outcome.first.key : undefined).toBe('b');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('★ 没给 validate 的格子一律不算错(哪怕值是空串)', () => {
|
|
275
|
+
const outcome = formSubmitOf([field('a', ''), field('b', ['x'])]);
|
|
276
|
+
|
|
277
|
+
expect(outcome.blocked).toBe(false);
|
|
278
|
+
expect(outcome.blocked ? {} : outcome.values).toEqual({ a: '', b: ['x'] });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('空表 ⇒ blocked: false 与空 values(一张没有字段的表没有可挡的)', () => {
|
|
282
|
+
const outcome = formSubmitOf([]);
|
|
283
|
+
|
|
284
|
+
expect(outcome.blocked).toBe(false);
|
|
285
|
+
expect(outcome.blocked ? {} : outcome.values).toEqual({});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('★ 只算给进来的那些格子(不可见那格由调用点先过滤掉,这里看不到它)', () => {
|
|
289
|
+
const fields = [field('env', 'prod'), { ...field('secret', 'x', () => '永远不该被算'), visible: false }];
|
|
290
|
+
const outcome = formSubmitOf(visibleFieldsOf(fields));
|
|
291
|
+
|
|
292
|
+
expect(outcome.blocked).toBe(false);
|
|
293
|
+
expect(outcome.blocked ? {} : outcome.values).toEqual({ env: 'prod' });
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('formActionsOf', () => {
|
|
298
|
+
const submit = { label: '保存', busyLabel: '保存中…' };
|
|
299
|
+
const cancel = { label: '取消', onClick: () => {} };
|
|
300
|
+
|
|
301
|
+
function base(over: Partial<Parameters<typeof formActionsOf>[0]> = {}) {
|
|
302
|
+
return formActionsOf({
|
|
303
|
+
extra: undefined,
|
|
304
|
+
cancel: undefined,
|
|
305
|
+
submit,
|
|
306
|
+
busy: false,
|
|
307
|
+
disabled: false,
|
|
308
|
+
onSubmitClick: () => {},
|
|
309
|
+
...over,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
it('★ 顺序:extra → cancel → submit(primary),提交那颗永远在最右', () => {
|
|
314
|
+
const actions = base({
|
|
315
|
+
extra: [
|
|
316
|
+
{ key: 'retry', label: '重试', onClick: () => {} },
|
|
317
|
+
{ key: 'remove', label: '清除', kind: 'danger', onClick: () => {} },
|
|
318
|
+
],
|
|
319
|
+
cancel,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(actions.map((one) => [one.key, one.kind])).toEqual([
|
|
323
|
+
['retry', undefined],
|
|
324
|
+
['remove', 'danger'],
|
|
325
|
+
['cancel', 'default'],
|
|
326
|
+
['submit', 'primary'],
|
|
327
|
+
]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('没给 extra 也没给 cancel 时只剩提交那一颗(且仍是 primary)', () => {
|
|
331
|
+
expect(base().map((one) => [one.key, one.kind])).toEqual([['submit', 'primary']]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('★ busy:提交钮换 busyLabel、带 busy、禁用;cancel 与 extra 一起禁用', () => {
|
|
335
|
+
const extra: readonly FormAction[] = [{ key: 'retry', label: '重试', onClick: () => {} }];
|
|
336
|
+
const [retry, cancelAction, submitAction] = base({ extra, cancel, busy: true });
|
|
337
|
+
|
|
338
|
+
expect(submitAction?.label).toBe('保存中…');
|
|
339
|
+
expect(submitAction?.busy).toBe(true);
|
|
340
|
+
expect(submitAction?.disabled).toBe(true);
|
|
341
|
+
expect(cancelAction?.disabled).toBe(true);
|
|
342
|
+
expect(retry?.disabled).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('★ busy 不给 busyLabel 时沿用 label', () => {
|
|
346
|
+
const actions = base({ submit: { label: '保存' }, busy: true });
|
|
347
|
+
|
|
348
|
+
expect(actions[0]?.label).toBe('保存');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('★ disabled 只禁提交钮,其余动作不受影响', () => {
|
|
352
|
+
const actions = base({ extra: [{ key: 'retry', label: '重试', onClick: () => {} }], cancel, disabled: true });
|
|
353
|
+
|
|
354
|
+
expect(actions.find((one) => one.key === 'submit')?.disabled).toBe(true);
|
|
355
|
+
expect(actions.find((one) => one.key === 'cancel')?.disabled).toBe(false);
|
|
356
|
+
expect(actions.find((one) => one.key === 'retry')?.disabled).toBeUndefined();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('提交钮的 onClick 就是给进来的那一拍(顺序不在调用点手里,动作的后果在)', () => {
|
|
360
|
+
let clicked = 0;
|
|
361
|
+
const actions = base({ onSubmitClick: () => (clicked += 1) });
|
|
362
|
+
|
|
363
|
+
actions[0]?.onClick();
|
|
364
|
+
expect(clicked).toBe(1);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe('formRowsOf', () => {
|
|
369
|
+
const problems = { env: '选一个环境' };
|
|
370
|
+
|
|
371
|
+
it('★ 每格直接就是一颗 FormRow(中间不许包 div,否则 subgrid 不匹配、控件左沿参差)', () => {
|
|
372
|
+
const rows = formRowsOf({ fields: [field('env', 'prod'), field('app', 'a')], problems, showProblem: true, busy: false });
|
|
373
|
+
|
|
374
|
+
expect(rows).toHaveLength(2);
|
|
375
|
+
for (const row of rows) {
|
|
376
|
+
expect(typeOf(row)).toBe(FormRow);
|
|
377
|
+
expect(propsOf(row)['className']).toBeUndefined();
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('★ key 挂在 FormRow 自己身上(React 的列表键在这一层)', () => {
|
|
382
|
+
const rows = formRowsOf({ fields: [field('env', 'prod')], problems, showProblem: true, busy: false });
|
|
383
|
+
|
|
384
|
+
expect(keyOf(rows[0])).toBe('env');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('★ problem 透传给 FormRow:有错那格显示文案并挂 data-wb-problem,没错那格没有', () => {
|
|
388
|
+
const rows = formRowsOf({
|
|
389
|
+
fields: [field('env', 'prod'), field('app', 'a')],
|
|
390
|
+
problems,
|
|
391
|
+
showProblem: true,
|
|
392
|
+
busy: false,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
expect(propsOf(rows[0])['problem']).toBe('选一个环境');
|
|
396
|
+
expect(propsOf(rows[1])['problem']).toBeUndefined();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('★ 控件那格拿到的绑定:invalid 只在这次该显示错时为真', () => {
|
|
400
|
+
const rows = formRowsOf({ fields: [field('env', 'prod')], problems, showProblem: true, busy: false });
|
|
401
|
+
|
|
402
|
+
expect(propsOf(rows[0])['children']).toBe('env:wb-form-env:true:false');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('★ 还没提交过(showProblem: false):文案不画、invalid 也不给(done 之前一律 false)', () => {
|
|
406
|
+
const rows = formRowsOf({ fields: [field('env', 'prod')], problems, showProblem: false, busy: false });
|
|
407
|
+
|
|
408
|
+
expect(propsOf(rows[0])['problem']).toBeUndefined();
|
|
409
|
+
expect(propsOf(rows[0])['children']).toBe('env:wb-form-env:false:false');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('★ busy 让每格的绑定 disabled 为真;layout 与 hint 原样透传', () => {
|
|
413
|
+
const withHint: FormField = { ...field('env', 'prod'), hint: '只列非生产', layout: 'stacked' };
|
|
414
|
+
const rows = formRowsOf({ fields: [withHint], problems: {}, showProblem: false, busy: true });
|
|
415
|
+
|
|
416
|
+
expect(propsOf(rows[0])['children']).toBe('env:wb-form-env:false:true');
|
|
417
|
+
expect(propsOf(rows[0])['hint']).toBe('只列非生产');
|
|
418
|
+
expect(propsOf(rows[0])['layout']).toBe('stacked');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('没有可画的格子时给空数组', () => {
|
|
422
|
+
expect(formRowsOf({ fields: [], problems: {}, showProblem: false, busy: false })).toEqual([]);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const CSS = readFileSync(new URL('./Form.css', import.meta.url), 'utf8');
|
|
427
|
+
|
|
428
|
+
function declarations(selector: string): string[] {
|
|
429
|
+
for (const block of CSS.split('}')) {
|
|
430
|
+
const [head, ...rest] = block.split('{');
|
|
431
|
+
if (rest.length === 0 || head === undefined) continue;
|
|
432
|
+
if (head.trim() !== selector) continue;
|
|
433
|
+
return rest
|
|
434
|
+
.join('{')
|
|
435
|
+
.split(';')
|
|
436
|
+
.map((one) => one.replace(/\s+/g, ' ').trim())
|
|
437
|
+
.filter((one) => one !== '');
|
|
438
|
+
}
|
|
439
|
+
throw new Error(`Form.css 里没有这条规则:${selector}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
describe('整张表单的版式(spec §2.2 规则三)', () => {
|
|
443
|
+
it('★ `.wb-form` 是那个纵向栈:flex / column / 唯一一档间距', () => {
|
|
444
|
+
const rule = declarations('.wb-form');
|
|
445
|
+
|
|
446
|
+
expect(rule).toContain('display: flex');
|
|
447
|
+
expect(rule).toContain('flex-direction: column');
|
|
448
|
+
expect(rule).toContain('gap: var(--wb-len-12)');
|
|
449
|
+
expect(rule.filter((one) => one.startsWith('gap:'))).toHaveLength(1);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('★ `.wb-form` 占满自己那一格(width: 100%),操作区才贴得到右沿', () => {
|
|
453
|
+
expect(declarations('.wb-form')).toContain('width: 100%');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('★ 位置靠结构不靠扳:`Form.css` 里不许出现 `.wb-formActions`,也不许有宽度 / 外边距去扳它', () => {
|
|
457
|
+
expect(CSS).not.toMatch(/\.wb-formActions\b/);
|
|
458
|
+
expect(CSS).not.toMatch(/justify-content|margin/);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('★ 表单级那句错不自己开类名:`.wb-formProblem` 归 FormRows,这里一个都不留', () => {
|
|
462
|
+
expect(CSS).not.toMatch(/\.wb-formProblem\b/);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('★ 认的是类名,不是 `data-wb-*` 属性选择器(那个命名空间归模块)', () => {
|
|
466
|
+
expect(CSS).not.toMatch(/\[data-wb-/);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('★ 颜色 / 字号 / 长度只走令牌,不写字面值(`width: 100%` 是占比,不是尺寸字面值)', () => {
|
|
470
|
+
const values = CSS.match(/(?:color|font-size|line-height|gap|padding|margin)\s*:\s*([^;]+);/g) ?? [];
|
|
471
|
+
|
|
472
|
+
expect(values.length).toBeGreaterThan(0);
|
|
473
|
+
for (const value of values) expect(value).toMatch(/var\(--(?:dsw|wb)-/);
|
|
474
|
+
});
|
|
475
|
+
});
|