@morlay/dsh-client-ui-primitives 0.0.2-alpha.7 → 0.0.2-alpha.9
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/README.md +275 -13
- package/dist/client.cjs +4959 -1057
- package/dist/client.d.cts +1366 -16
- package/dist/client.d.mts +1365 -16
- package/dist/rolldown-runtime-EAsCxrDo.mjs +14 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +28 -11
- package/src/client/index.ts +41 -2
- package/src/client/schema-form/Editor.tsx +777 -0
- package/src/client/schema-form/SchemaForm.tsx +77 -0
- package/src/client/schema-form/controller.ts +457 -0
- package/src/client/schema-form/draft.ts +487 -0
- package/src/client/schema-form/fields.tsx +100 -0
- package/src/client/schema-form/hints.ts +286 -0
- package/src/client/schema-form/index.ts +188 -0
- package/src/client/schema-form/labels.ts +30 -0
- package/src/client/schema-form/lines.ts +334 -0
- package/src/client/schema-form/locales.ts +128 -0
- package/src/client/schema-form/rows.ts +153 -0
- package/src/client/schema-form/schema-node.ts +812 -0
- package/src/client/schema-form/slot-contract.ts +156 -0
- package/src/client/schema-form/styles.ts +310 -0
- package/src/client/schema-form/testing/fake-describe.ts +45 -0
- package/src/client/schema-form/testing/fake-scope.ts +97 -0
- package/src/client/schema-form/value.tsx +146 -0
- package/src/client/settings-form/SettingsForm.styles.ts +58 -0
- package/src/client/settings-form/SettingsForm.tsx +72 -0
- package/src/client/settings-form/fields.styles.ts +158 -0
- package/src/client/settings-form/fields.tsx +194 -0
- package/src/client/settings-form/form-model.ts +421 -0
- package/src/client/settings-form/index.ts +19 -0
- package/src/client/styling/focus.ts +9 -0
- package/src/client/theme.generated.ts +802 -728
- package/cordis.patch.yml +0 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 槽契约:字段级自定义输入与文案的注入点,以及承载它的 Factory 契约。
|
|
3
|
+
*
|
|
4
|
+
* 字段槽是 **chain**:注册项在 `select` 里按 `node.meta.role` 或 `ns + path` 决定认领哪个字段,命中即渲染自己的
|
|
5
|
+
* 组件(收到 `matched`),未命中落到本包的默认控件。链式槽的注册项还可以只声明 `label`/`hint`(不改控件)——
|
|
6
|
+
* 默认控件仍会渲染,文案以它为准。
|
|
7
|
+
*
|
|
8
|
+
* 字段槽声明在 Factory 上(`children`),而不是每个行注册项上:slots 的 children 声明 per key 只能一次,而本包
|
|
9
|
+
* 会给每个可渲染的行各注册一个 `plugins.row.config` 项——那些项经 `renderFactorySlot` 渲染同一个 Factory,
|
|
10
|
+
* 于是字段槽只需声明一次,所有行共用。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {} from "@deepseek-ai/dsh-client-ui-slots";
|
|
14
|
+
import type {
|
|
15
|
+
FactoryComponentPropsOf,
|
|
16
|
+
PropsRuntime,
|
|
17
|
+
TranslateNS,
|
|
18
|
+
} from "@deepseek-ai/dsh-client-ui-slots";
|
|
19
|
+
import type { SchemaFormActions, SchemaFormFace, SchemaFormState } from "./controller.ts";
|
|
20
|
+
import type { TextParse } from "./draft.ts";
|
|
21
|
+
import type { SelectOption } from "./hints.ts";
|
|
22
|
+
import type { ResolveText } from "./labels.ts";
|
|
23
|
+
import type { SchemaFormLocaleKey } from "./locales.ts";
|
|
24
|
+
import type { FieldNode } from "./schema-node.ts";
|
|
25
|
+
|
|
26
|
+
/** `plugins.row.config` 的 key:`<bundle 包名>#<行 id>`(与页面 `rowConfigKey` 同拼法)。 */
|
|
27
|
+
export type RowConfigKey = string;
|
|
28
|
+
|
|
29
|
+
/** 字段槽的 owner props:路由输入 + 显示状态 + 受控动作(默认控件要的一切都在这里)。 */
|
|
30
|
+
export interface SchemaFieldOwnerProps {
|
|
31
|
+
/** settings 命名空间(行 id)。 */
|
|
32
|
+
ns: string;
|
|
33
|
+
/** 字段的具体路径(动态段已按真实键或索引替换)。 */
|
|
34
|
+
path: readonly string[];
|
|
35
|
+
/** 字段节点(类型、`role`、`extra`、界限、只读原因)。 */
|
|
36
|
+
node: FieldNode;
|
|
37
|
+
/** 显示标签:默认是字段名拆词,业务方经槽给时会覆盖。 */
|
|
38
|
+
label: string;
|
|
39
|
+
/** 显示说明(`description` 本地化 + `comment`)。 */
|
|
40
|
+
hint: string | undefined;
|
|
41
|
+
/** 当前生效值(草稿优先)。 */
|
|
42
|
+
value: unknown;
|
|
43
|
+
/**
|
|
44
|
+
* 这个字段的可选项:给了就是选择器(点值不再进文本编辑)。来源是字面量集合的 union,或业务经提示面注册的
|
|
45
|
+
* 候选——候选依赖的兄弟字段一变,这里就换一批。
|
|
46
|
+
*/
|
|
47
|
+
options: readonly SelectOption[] | undefined;
|
|
48
|
+
/** 文本草稿:文本控件显示它,而不是重新格式化生效值。 */
|
|
49
|
+
text: string | undefined;
|
|
50
|
+
/** secret 槽是否已配置(值本身不回传)。 */
|
|
51
|
+
secretConfigured: boolean;
|
|
52
|
+
/** 嵌套深度:默认控件按它缩进。 */
|
|
53
|
+
depth: number;
|
|
54
|
+
/** 保存后这一字段是否会留下用户层条目。 */
|
|
55
|
+
overridden: boolean;
|
|
56
|
+
/** 草稿不合法时的消息。 */
|
|
57
|
+
invalid: string | undefined;
|
|
58
|
+
/** 只读部署或不可用命名空间时为 true。 */
|
|
59
|
+
disabled: boolean;
|
|
60
|
+
/** 本包字典(复刻或包装默认控件时用)。 */
|
|
61
|
+
t: SchemaFormTranslate;
|
|
62
|
+
/** 容器动作(增删数组项、字典键、丢弃、保存)。 */
|
|
63
|
+
actions: SchemaFormActions;
|
|
64
|
+
/**
|
|
65
|
+
* 分组节点的折叠交互(object / dict / array / tuple);叶子节点是 `undefined`。
|
|
66
|
+
* 默认折叠与否由 schema 的 `collapse()` 决定,用户点一下就是反转。
|
|
67
|
+
*/
|
|
68
|
+
group: { collapsed: boolean; toggle: () => void } | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* 这一行是动态容器的成员本身(dict 的键 / array 的项)时的身份:容器路径、键、数组项的位置。容器成员把它画进
|
|
71
|
+
* 标题行(名字 + 移除),叶子成员由渲染层另起一行——所以「哪个字段属于哪个键/哪一项」在页面上一眼可见。
|
|
72
|
+
*/
|
|
73
|
+
member:
|
|
74
|
+
| { parent: readonly string[]; key: string; index: number | undefined; pending?: boolean }
|
|
75
|
+
| undefined;
|
|
76
|
+
/** 暂存一个合法值。 */
|
|
77
|
+
onChange: (value: unknown) => void;
|
|
78
|
+
/**
|
|
79
|
+
* 暂存一段文本:按字段类型的默认规则解析;给了 `parse` 就用它(例如 secret 留空=不写)。
|
|
80
|
+
* @param text - 用户输入。
|
|
81
|
+
* @param parse - 覆盖默认解析规则。
|
|
82
|
+
*/
|
|
83
|
+
onEditText: (text: string, parse?: TextParse) => void;
|
|
84
|
+
/** 暂存「恢复默认」。 */
|
|
85
|
+
onReset: () => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 字段槽注册项的完整 props:框架注入的 owner props({@link SchemaFieldOwnerProps})+它的 `select` 结果。
|
|
90
|
+
*
|
|
91
|
+
* 注册方的组件按这个类型声明参数(**不要**自己重写 owner props 的类型:注册时框架会把它读成 inject 面)。
|
|
92
|
+
*/
|
|
93
|
+
export type SchemaFieldComponentProps<M extends SchemaFieldMatch = SchemaFieldMatch> =
|
|
94
|
+
PropsRuntime<"settings.schema-form.field"> & { matched: M };
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 字段槽注册项的 `select`:返回认领结果,或 `null` 表示不认领(chain 槽的约定是 `null` 放行给下一项,
|
|
98
|
+
* `undefined` 不会被当成"不认领")。
|
|
99
|
+
*/
|
|
100
|
+
export type SchemaFieldSelect = (field: SchemaFieldOwnerProps) => SchemaFieldMatch | null;
|
|
101
|
+
|
|
102
|
+
/** 字段槽 `select` 可以只给文案:控件仍由注册方渲染(拿本包的默认控件包一层即可)。 */
|
|
103
|
+
export interface SchemaFieldMatch {
|
|
104
|
+
/** 覆盖显示标签。 */
|
|
105
|
+
label?: string;
|
|
106
|
+
/** 覆盖显示说明。 */
|
|
107
|
+
hint?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Factory 的输入 props:一行的配置页。 */
|
|
111
|
+
export interface SchemaFormProps {
|
|
112
|
+
/** settings 命名空间(行 id)。 */
|
|
113
|
+
ns: string;
|
|
114
|
+
/** 该行的控制器动作与状态。 */
|
|
115
|
+
face: SchemaFormFace;
|
|
116
|
+
/** schema 里的本地化文本(`description`)按当前语言解析。 */
|
|
117
|
+
resolveText: ResolveText;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Factory 组件收到的完整 props。 */
|
|
121
|
+
export type SchemaFormComponentProps = FactoryComponentPropsOf<"settings.schema-form.form">;
|
|
122
|
+
|
|
123
|
+
/** 本包字典的翻译函数(默认控件与表单壳使用)。 */
|
|
124
|
+
export type SchemaFormTranslate = TranslateNS<"settings.schema-form">;
|
|
125
|
+
|
|
126
|
+
/** 渲染层读一行状态的便捷别名。 */
|
|
127
|
+
export type { SchemaFormState };
|
|
128
|
+
|
|
129
|
+
declare module "@deepseek-ai/dsh-client-ui-slots" {
|
|
130
|
+
interface LocaleNamespaceMap {
|
|
131
|
+
/** 表单壳、字段控件与只读呈现的文案。 */
|
|
132
|
+
"settings.schema-form": SchemaFormLocaleKey;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface SlotMap {
|
|
136
|
+
/**
|
|
137
|
+
* 字段级自定义输入与文案:按 `role` 或 `ns + path` 认领一个字段,命中即渲染注册方的组件(未命中落到默认
|
|
138
|
+
* 控件)。注册项由业务插件用 `ctx.slots.inject('settings.schema-form.field', …)` 注册,声明由本包的
|
|
139
|
+
* Factory 给出。
|
|
140
|
+
*/
|
|
141
|
+
"settings.schema-form.field": { kind: "chain"; scope: "root"; owner: SchemaFieldOwnerProps };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface SlotFactoryMap {
|
|
145
|
+
/**
|
|
146
|
+
* 一行配置页的渲染体:每个可渲染的行注册项经 `renderFactorySlot` 渲染同一个 Factory,字段槽因此只需声明
|
|
147
|
+
* 一次(slots 的 children 声明 per key 唯一)。
|
|
148
|
+
*/
|
|
149
|
+
"settings.schema-form.form": {
|
|
150
|
+
scope: "root";
|
|
151
|
+
props: SchemaFormProps;
|
|
152
|
+
locale: "settings.schema-form";
|
|
153
|
+
children: { "settings.schema-form.field": { kind: "chain"; scope: "root" } };
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行式编辑器的样式:等宽、行号、语法色、悬停行为——全部消费官方 `--dsw-*` token。
|
|
3
|
+
*
|
|
4
|
+
* 几何只有一套:行高、行内间距、动作按钮尺寸都由下面几个常量给,行与行之间不各自决定。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { styled } from "../styling/index.ts";
|
|
8
|
+
import { dsw } from "../theme.ts";
|
|
9
|
+
|
|
10
|
+
/** 等宽字体栈:配置值的观感与代码一致。 */
|
|
11
|
+
const MONO = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
|
|
12
|
+
|
|
13
|
+
/** 行高:行内容、行内输入、折叠箭头、按钮排都在这一档上对齐。 */
|
|
14
|
+
const ROW = "22px";
|
|
15
|
+
|
|
16
|
+
/** 行内元素之间的间距。 */
|
|
17
|
+
const GAP = "6px";
|
|
18
|
+
|
|
19
|
+
/** 动作按钮:方形图标键,尺寸与行高同一档的紧凑版。 */
|
|
20
|
+
const ACTION = "20px";
|
|
21
|
+
|
|
22
|
+
/** 动作按钮之间的间距(比行内间距紧一档)。 */
|
|
23
|
+
const ACTION_GAP = "4px";
|
|
24
|
+
|
|
25
|
+
/** 图标边长(行内图标与动作键同一档)。 */
|
|
26
|
+
export const ICON_SIZE = 14;
|
|
27
|
+
|
|
28
|
+
/** 折叠列(chevron 与它的占位)宽度。 */
|
|
29
|
+
const FOLD = "14px";
|
|
30
|
+
|
|
31
|
+
/** 行号列宽度。 */
|
|
32
|
+
const NUMBER = "32px";
|
|
33
|
+
|
|
34
|
+
/** 行尾留白。 */
|
|
35
|
+
const PAD = "8px";
|
|
36
|
+
|
|
37
|
+
/** 细线:theme 给的 0.5px 描边(`0 0 0 0.5px var(--dsw-elevation-stroke-color)`)。 */
|
|
38
|
+
const HAIRLINE = dsw.elevation.stroke;
|
|
39
|
+
|
|
40
|
+
/** 描边色:theme 里给描边用的那一档(与官方 `Menu` 面板同源)。 */
|
|
41
|
+
const LINE = dsw.elevation.stroke.color;
|
|
42
|
+
|
|
43
|
+
/** 行左侧那道「改过」的标记宽度。 */
|
|
44
|
+
const EDGE = "2px";
|
|
45
|
+
|
|
46
|
+
/** 出错那一行的底色:theme 的红掺进透明里(半透明红),比整行实红轻、又比只有一道条明显。 */
|
|
47
|
+
const ERROR_WASH = `color-mix(in srgb, ${String(dsw.alias.state.error.secondary)} 12%, transparent)`;
|
|
48
|
+
|
|
49
|
+
/** 编辑器的根:纵向排列的行。 */
|
|
50
|
+
export const EditorRoot = styled("div")({
|
|
51
|
+
display: "flex",
|
|
52
|
+
flexDirection: "column",
|
|
53
|
+
width: "100%",
|
|
54
|
+
fontFamily: MONO,
|
|
55
|
+
fontSize: "14px",
|
|
56
|
+
lineHeight: ROW,
|
|
57
|
+
color: dsw.alias.label.primary,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** 一行:行号 + 折进 + 行内容。悬停给一层浅底。 */
|
|
61
|
+
export const LineRow = styled("div")({
|
|
62
|
+
display: "flex",
|
|
63
|
+
// 顶部对齐:行里出现输入框或多行值时,内容不跟着上下跳。
|
|
64
|
+
alignItems: "flex-start",
|
|
65
|
+
gap: GAP,
|
|
66
|
+
minHeight: ROW,
|
|
67
|
+
paddingRight: PAD,
|
|
68
|
+
whiteSpace: "nowrap",
|
|
69
|
+
"&:hover": { background: dsw.alias.bg.layer["2"] },
|
|
70
|
+
// 选中一行(点行号):整行留一层底色,便于对着行号找内容。
|
|
71
|
+
"&[data-selected='true']": { background: dsw.alias.bg.layer["3"] },
|
|
72
|
+
// 本页改过还没保存:左边一道强调色,和「已经存进用户层」的浅灰区分开。
|
|
73
|
+
"&[data-dirty='true']": { boxShadow: `inset ${EDGE} 0 0 ${String(dsw.alias.brand.primary)}` },
|
|
74
|
+
"&[data-overridden='true']": { boxShadow: `inset ${EDGE} 0 0 ${String(LINE)}` },
|
|
75
|
+
// 改过的字段名用强调色(存没存看左边那道条);出错时红优先。
|
|
76
|
+
"&[data-dirty='true'] [data-role='key']": { color: dsw.alias.brand.primary },
|
|
77
|
+
"&[data-overridden='true'] [data-role='key']": { color: dsw.alias.brand.primary },
|
|
78
|
+
// 有问题的行:半透明红底 + 左侧一道实红条,字段名与注释位那句都是红字。
|
|
79
|
+
"&[data-invalid='true']": {
|
|
80
|
+
background: ERROR_WASH,
|
|
81
|
+
boxShadow: `inset ${EDGE} 0 0 ${String(dsw.alias.state.error.primary)}`,
|
|
82
|
+
},
|
|
83
|
+
"&[data-invalid='true'] [data-role='key']": { color: dsw.alias.state.error.primary },
|
|
84
|
+
// 行内的行为按钮(撤回 / 恢复默认 / 复制 / 移除):平时透明,悬停这一行才显形。
|
|
85
|
+
"&:hover [data-role='actions']": { opacity: 1 },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** 一行的内容区:**缩进只作用在这里**——行号列固定在左边,缩进不推它。 */
|
|
89
|
+
export const LineBody = styled("div")({
|
|
90
|
+
display: "flex",
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
gap: GAP,
|
|
93
|
+
flex: 1,
|
|
94
|
+
minWidth: 0,
|
|
95
|
+
minHeight: ROW,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/** 行号列:右对齐、不可选、淡色。 */
|
|
99
|
+
export const LineNumber = styled("span")({
|
|
100
|
+
flex: "none",
|
|
101
|
+
width: NUMBER,
|
|
102
|
+
lineHeight: ROW,
|
|
103
|
+
textAlign: "right",
|
|
104
|
+
color: dsw.alias.label.caption,
|
|
105
|
+
userSelect: "none",
|
|
106
|
+
cursor: "pointer",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** 折叠 chevron(官方箭头图标,尺寸与别的行内图标同源)。 */
|
|
110
|
+
export const LineFold = styled("button")({
|
|
111
|
+
display: "inline-flex",
|
|
112
|
+
alignItems: "center",
|
|
113
|
+
justifyContent: "center",
|
|
114
|
+
flex: "none",
|
|
115
|
+
width: FOLD,
|
|
116
|
+
height: ROW,
|
|
117
|
+
padding: 0,
|
|
118
|
+
border: 0,
|
|
119
|
+
background: "none",
|
|
120
|
+
color: dsw.alias.label.caption,
|
|
121
|
+
cursor: "pointer",
|
|
122
|
+
"&:hover": { color: dsw.alias.label.secondary },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/** 叶子行的折叠占位(保持列对齐)。 */
|
|
126
|
+
export const LineFoldSpacer = styled("span")({
|
|
127
|
+
flex: "none",
|
|
128
|
+
width: FOLD,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** 键名(配置键原样;数组下标淡一些)。 */
|
|
132
|
+
export const LineKey = styled("span")({
|
|
133
|
+
flex: "none",
|
|
134
|
+
color: dsw.alias.label.primary,
|
|
135
|
+
"&[data-index='true']": { color: dsw.alias.label.caption },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/** 结构符与标点(`{` `}` `[` `]` `:`)。 */
|
|
139
|
+
export const LineToken = styled("span")({
|
|
140
|
+
flex: "none",
|
|
141
|
+
color: dsw.alias.label.caption,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/** 值(token):字符串 / 数字 / 布尔各有语法色。 */
|
|
145
|
+
export const LineValue = styled("span")({
|
|
146
|
+
minWidth: 0,
|
|
147
|
+
overflow: "hidden",
|
|
148
|
+
textOverflow: "ellipsis",
|
|
149
|
+
cursor: "text",
|
|
150
|
+
color: dsw.alias.state.business.primary,
|
|
151
|
+
"&[data-tone='number']": { color: dsw.alias.state.warn.primary },
|
|
152
|
+
"&[data-tone='boolean']": { color: dsw.alias.state.success.primary },
|
|
153
|
+
"&[data-tone='empty']": { color: dsw.alias.label.caption },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 值上的下拉触发:**它自己就是值的呈现**(不再是「值 + 一个下拉」两遍),点开才出菜单。
|
|
158
|
+
*/
|
|
159
|
+
export const ValueTrigger = styled("button")({
|
|
160
|
+
display: "inline-flex",
|
|
161
|
+
alignItems: "center",
|
|
162
|
+
gap: "2px",
|
|
163
|
+
minWidth: 0,
|
|
164
|
+
padding: 0,
|
|
165
|
+
border: 0,
|
|
166
|
+
background: "none",
|
|
167
|
+
font: "inherit",
|
|
168
|
+
textAlign: "left",
|
|
169
|
+
cursor: "pointer",
|
|
170
|
+
color: dsw.alias.label.caption,
|
|
171
|
+
"&:disabled": { cursor: "default" },
|
|
172
|
+
"&:focus-visible": { outline: "none" },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 行内输入的外壳:官方 `Input` 压到与值同一行高,并**吃掉剩下的宽度**。
|
|
177
|
+
*
|
|
178
|
+
* 行内编辑与容器闭合行的添加输入共用它——两处的输入框因此长得一模一样。
|
|
179
|
+
*/
|
|
180
|
+
export const CompactInput = styled("span")({
|
|
181
|
+
display: "inline-flex",
|
|
182
|
+
flex: "1 1 auto",
|
|
183
|
+
minWidth: 0,
|
|
184
|
+
"& > span": {
|
|
185
|
+
width: "100%",
|
|
186
|
+
height: ROW,
|
|
187
|
+
padding: "0 6px",
|
|
188
|
+
borderRadius: "4px",
|
|
189
|
+
},
|
|
190
|
+
"& input": {
|
|
191
|
+
fontSize: "14px",
|
|
192
|
+
lineHeight: ROW,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 多行值的行内输入:官方没有多行原子,所以这里用同一套观感的字段壳(同边框、同圆角、同焦点色),
|
|
198
|
+
* 高度跟着内容长、宽度同样撑满剩下的位置。
|
|
199
|
+
*/
|
|
200
|
+
export const CompactTextField = styled("span")({
|
|
201
|
+
display: "inline-flex",
|
|
202
|
+
flex: "1 1 auto",
|
|
203
|
+
minWidth: 0,
|
|
204
|
+
padding: "0 6px",
|
|
205
|
+
borderRadius: "4px",
|
|
206
|
+
background: dsw.alias.bg.layer["1"],
|
|
207
|
+
// 细线走 theme 的描边(0.5px 与颜色都由 token 给)。
|
|
208
|
+
border: 0,
|
|
209
|
+
boxShadow: HAIRLINE,
|
|
210
|
+
"&:focus-within": { "--dsw-elevation-stroke-color": dsw.alias.brand.primary },
|
|
211
|
+
"& > textarea": {
|
|
212
|
+
flex: 1,
|
|
213
|
+
width: "100%",
|
|
214
|
+
minHeight: ACTION,
|
|
215
|
+
maxHeight: "240px",
|
|
216
|
+
padding: 0,
|
|
217
|
+
border: 0,
|
|
218
|
+
outline: "none",
|
|
219
|
+
background: "transparent",
|
|
220
|
+
resize: "none",
|
|
221
|
+
fieldSizing: "content",
|
|
222
|
+
fontFamily: "inherit",
|
|
223
|
+
fontSize: "14px",
|
|
224
|
+
lineHeight: ROW,
|
|
225
|
+
color: dsw.alias.label.primary,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/** 行内注释(schema 说明与业务文案):比配置值小一档,占满这一行剩下的宽度,长了就截断(hover 看全文)。 */
|
|
230
|
+
export const LineComment = styled("span")({
|
|
231
|
+
flex: "1 1 auto",
|
|
232
|
+
minWidth: 0,
|
|
233
|
+
overflow: "hidden",
|
|
234
|
+
textOverflow: "ellipsis",
|
|
235
|
+
color: dsw.alias.label.tertiary,
|
|
236
|
+
fontFamily: "inherit",
|
|
237
|
+
fontSize: "12px",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/** 行内的校验消息(与注释放同一档字号)。 */
|
|
241
|
+
export const LineInvalid = styled("span")({
|
|
242
|
+
flex: "none",
|
|
243
|
+
color: dsw.alias.state.error.primary,
|
|
244
|
+
fontFamily: "inherit",
|
|
245
|
+
fontSize: "12px",
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/** 行尾的动作组:方形图标键(默认透明,悬停时显形)。 */
|
|
249
|
+
export const LineActions = styled("span")({
|
|
250
|
+
display: "inline-flex",
|
|
251
|
+
alignItems: "center",
|
|
252
|
+
gap: ACTION_GAP,
|
|
253
|
+
flex: "none",
|
|
254
|
+
opacity: 0,
|
|
255
|
+
"& > button": {
|
|
256
|
+
display: "inline-flex",
|
|
257
|
+
alignItems: "center",
|
|
258
|
+
justifyContent: "center",
|
|
259
|
+
width: ACTION,
|
|
260
|
+
height: ACTION,
|
|
261
|
+
padding: 0,
|
|
262
|
+
border: 0,
|
|
263
|
+
borderRadius: "4px",
|
|
264
|
+
background: "none",
|
|
265
|
+
color: dsw.alias.label.secondary,
|
|
266
|
+
cursor: "pointer",
|
|
267
|
+
"&:hover": { background: dsw.alias.bg.layer["3"], color: dsw.alias.label.primary },
|
|
268
|
+
"&:disabled": { cursor: "not-allowed", color: dsw.alias.label.dimmed },
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 悬停行时才显示这一行的动作按钮:显形规则写在行上(`[data-role='actions']`),组件只需要带这个属性。
|
|
274
|
+
*/
|
|
275
|
+
export const HoverActions = styled(LineActions)({
|
|
276
|
+
/**
|
|
277
|
+
* 编辑态的两个按钮常显,而且**要有底色与描边**:行底色上只有淡色图标是看不见的,
|
|
278
|
+
* 所以这里给实体按钮(theme 的描边 + 一层底),确认用成功档、取消用中性档。
|
|
279
|
+
*/
|
|
280
|
+
"&[data-editing='true']": {
|
|
281
|
+
opacity: 1,
|
|
282
|
+
gap: GAP,
|
|
283
|
+
"& > button": {
|
|
284
|
+
boxShadow: HAIRLINE,
|
|
285
|
+
background: dsw.alias.bg.layer["1"],
|
|
286
|
+
color: dsw.alias.label.primary,
|
|
287
|
+
"&:first-of-type": { color: dsw.alias.state.success.primary },
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
/** 添加输入:**跟在容器的闭合括号同一行**(菜单锚在它上面),宽度吃掉剩下的位置。 */
|
|
293
|
+
export const AddWrap = styled("span")({
|
|
294
|
+
position: "relative",
|
|
295
|
+
display: "inline-flex",
|
|
296
|
+
alignItems: "center",
|
|
297
|
+
gap: GAP,
|
|
298
|
+
flex: "1 1 auto",
|
|
299
|
+
minWidth: 0,
|
|
300
|
+
marginLeft: GAP,
|
|
301
|
+
"&[data-invalid='true'] input": { borderColor: dsw.alias.state.error.primary },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
/** 一行说明(不可用 / 没有可生成项时用)。 */
|
|
305
|
+
export const Hint = styled("p")({
|
|
306
|
+
margin: 0,
|
|
307
|
+
fontSize: "12px",
|
|
308
|
+
lineHeight: 1.5,
|
|
309
|
+
color: dsw.alias.label.tertiary,
|
|
310
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 测试用的 describe 读面替身:可推动命名空间清单变化,形状与上游 mirror 的读面一致。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { SettingsNamespaceView } from "@deepseek-ai/dsh-api-remotes/client";
|
|
6
|
+
import type { SettingsDescribeFace } from "@deepseek-ai/dsh-client-ui-settings/client";
|
|
7
|
+
|
|
8
|
+
/** 一个可以推动变更的 describe 读面。 */
|
|
9
|
+
export interface FakeDescribe extends SettingsDescribeFace {
|
|
10
|
+
/** 换掉命名空间清单并通知订阅者。 */
|
|
11
|
+
publish(namespaces: readonly SettingsNamespaceView[]): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param initial - 初始命名空间清单。
|
|
16
|
+
* @returns 可读可推的 describe 替身。
|
|
17
|
+
*/
|
|
18
|
+
export function fakeDescribe(initial: readonly SettingsNamespaceView[] = []): FakeDescribe {
|
|
19
|
+
const listeners = new Set<() => void>();
|
|
20
|
+
let namespaces = initial;
|
|
21
|
+
return {
|
|
22
|
+
getSnapshot: () => ({
|
|
23
|
+
status: "ready" as const,
|
|
24
|
+
view: { namespaces, writable: true, hasDocument: false },
|
|
25
|
+
error: null,
|
|
26
|
+
}),
|
|
27
|
+
subscribe: (listener: () => void) => {
|
|
28
|
+
listeners.add(listener);
|
|
29
|
+
return () => {
|
|
30
|
+
listeners.delete(listener);
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
ensure: () => Promise.resolve(),
|
|
34
|
+
// 写回答折进读数:同一命名空间替换,未出现过的追加(与上游 mirror 同一语义)。
|
|
35
|
+
acceptView: (view) => {
|
|
36
|
+
namespaces = namespaces.some((row) => row.ns === view.ns)
|
|
37
|
+
? namespaces.map((row) => (row.ns === view.ns ? view : row))
|
|
38
|
+
: [...namespaces, view];
|
|
39
|
+
},
|
|
40
|
+
publish: (next: readonly SettingsNamespaceView[]) => {
|
|
41
|
+
namespaces = next;
|
|
42
|
+
for (const listener of listeners) listener();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 测试用的共享配置表单:内存里模拟 host 的读写两层(快照 / 订阅 / 一次带 revision 栅栏的写)。
|
|
3
|
+
*
|
|
4
|
+
* 它让控制器与草稿模型的用例不碰远程面、不碰装配,只盯「草稿怎么变成写」与「状态怎么投影」。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SettingsPathOpView } from "@deepseek-ai/dsh-api-remotes/client";
|
|
8
|
+
import type { ConfigForm, ConfigFormSnapshot } from "@deepseek-ai/dsh-client-ui-settings/client";
|
|
9
|
+
|
|
10
|
+
/** 命名空间段的值。 */
|
|
11
|
+
export type Section = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
export class FakeScope implements ConfigForm<Section> {
|
|
14
|
+
/** 每次保存发出的 path op(按保存顺序)。 */
|
|
15
|
+
readonly writes: (readonly SettingsPathOpView[])[] = [];
|
|
16
|
+
/** 每次保存带的 revision 栅栏。 */
|
|
17
|
+
readonly fences: (number | undefined)[] = [];
|
|
18
|
+
/** 让 host 拒绝下一次写。 */
|
|
19
|
+
accepted = true;
|
|
20
|
+
|
|
21
|
+
#snapshot: ConfigFormSnapshot<Section>;
|
|
22
|
+
readonly #listeners = new Set<() => void>();
|
|
23
|
+
|
|
24
|
+
constructor(overrides: Partial<ConfigFormSnapshot<Section>> = {}) {
|
|
25
|
+
this.#snapshot = {
|
|
26
|
+
status: "ready",
|
|
27
|
+
value: {},
|
|
28
|
+
base: {},
|
|
29
|
+
user: undefined,
|
|
30
|
+
writable: true,
|
|
31
|
+
revision: 7,
|
|
32
|
+
mode: "host",
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getSnapshot(): ConfigFormSnapshot<Section> {
|
|
38
|
+
return this.#snapshot;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
subscribe(listener: () => void): () => void {
|
|
42
|
+
this.#listeners.add(listener);
|
|
43
|
+
return () => {
|
|
44
|
+
this.#listeners.delete(listener);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
set(field: string, value: unknown): Promise<boolean> {
|
|
49
|
+
return this.mutate([{ op: "set", path: [field], value: value as never }]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
unset(field: string): Promise<boolean> {
|
|
53
|
+
return this.mutate([{ op: "unset", path: [field] }]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mutate(ops: readonly SettingsPathOpView[], expectedRevision?: number): Promise<boolean> {
|
|
57
|
+
this.writes.push(ops);
|
|
58
|
+
this.fences.push(expectedRevision);
|
|
59
|
+
if (!this.accepted) return Promise.resolve(false);
|
|
60
|
+
const value = structuredClone(this.#snapshot.value ?? {});
|
|
61
|
+
for (const op of ops) {
|
|
62
|
+
const parent = op.path
|
|
63
|
+
.slice(0, -1)
|
|
64
|
+
.reduce<unknown>(
|
|
65
|
+
(node, key) => (node as Record<string, unknown> | undefined)?.[key],
|
|
66
|
+
value,
|
|
67
|
+
);
|
|
68
|
+
const leaf = op.path[op.path.length - 1] ?? "";
|
|
69
|
+
if (Array.isArray(parent)) {
|
|
70
|
+
if (op.op === "set") parent[Number(leaf)] = op.value;
|
|
71
|
+
else parent.splice(Number(leaf), 1);
|
|
72
|
+
} else if (op.op === "set") {
|
|
73
|
+
(parent as Section)[leaf] = op.value;
|
|
74
|
+
} else {
|
|
75
|
+
Reflect.deleteProperty(parent as Section, leaf);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
this.#snapshot = {
|
|
79
|
+
...this.#snapshot,
|
|
80
|
+
value,
|
|
81
|
+
user: value,
|
|
82
|
+
revision: (this.#snapshot.revision ?? 0) + 1,
|
|
83
|
+
};
|
|
84
|
+
this.#publish();
|
|
85
|
+
return Promise.resolve(true);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 从外部改一次读数(模拟另一个界面或 host 的改动)。 */
|
|
89
|
+
publish(overrides: Partial<ConfigFormSnapshot<Section>> = {}): void {
|
|
90
|
+
this.#snapshot = { ...this.#snapshot, ...overrides };
|
|
91
|
+
this.#publish();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#publish(): void {
|
|
95
|
+
for (const listener of this.#listeners) listener();
|
|
96
|
+
}
|
|
97
|
+
}
|