@morlay/dsh-client-ui-primitives 0.0.2-alpha.8 → 0.0.2
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 +250 -14
- package/dist/client.cjs +3548 -302
- package/dist/client.d.cts +1040 -15
- package/dist/client.d.mts +1040 -16
- package/dist/rolldown-runtime-EAsCxrDo.mjs +14 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +26 -10
- package/src/client/index.ts +40 -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 +2 -1
- package/src/client/settings-form/fields.styles.ts +3 -2
- package/src/client/settings-form/fields.tsx +2 -1
- package/src/client/styling/focus.ts +9 -0
- package/src/client/theme.generated.ts +64 -7
- package/cordis.patch.yml +0 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行内值的呈现与编辑文本规则,以及字段槽注册方复用的默认值组件。
|
|
3
|
+
*
|
|
4
|
+
* 值本身仍走草稿:这个文件只决定"怎么显示"(语法色 token、下拉触发的呈现)与"怎么编辑"(一行输入框里的文本
|
|
5
|
+
* 形状——字符串把换行写成 `\n`,与一行的输入框相配)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, type ReactNode } from "react";
|
|
9
|
+
import { IconChevronDownOutlineRegular, Menu } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
10
|
+
import type { SelectOption } from "./hints.ts";
|
|
11
|
+
import type { SchemaFieldOwnerProps } from "./slot-contract.ts";
|
|
12
|
+
import { ICON_SIZE, LineValue, ValueTrigger } from "./styles.ts";
|
|
13
|
+
|
|
14
|
+
/** 编辑态文本:字符串就是原文(换行照旧),其余按 JSON 写。 */
|
|
15
|
+
export function valueText(value: unknown): string {
|
|
16
|
+
if (value === undefined || value === null) return "";
|
|
17
|
+
if (typeof value === "string") return value;
|
|
18
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 这个值要在多行输入里编辑吗(字符串带换行)。 */
|
|
23
|
+
export function isMultiline(value: unknown): boolean {
|
|
24
|
+
return typeof value === "string" && /[\r\n]/.test(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 显示态文本:字符串带引号;多行与特殊字符都按 JSON 的写法收在一行里。 */
|
|
28
|
+
export function tokenText(value: unknown): string {
|
|
29
|
+
if (value === undefined || value === null) return "null";
|
|
30
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
31
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
32
|
+
return JSON.stringify(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 值的语法色分组。 */
|
|
36
|
+
export function tokenTone(value: unknown): "string" | "number" | "boolean" | "empty" {
|
|
37
|
+
if (value === undefined || value === null) return "empty";
|
|
38
|
+
if (typeof value === "number") return "number";
|
|
39
|
+
if (typeof value === "boolean") return "boolean";
|
|
40
|
+
return "string";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 行内值的默认呈现(显示态 token)。
|
|
45
|
+
* @param props.owner - 字段上下文(值、是否 secret、只读原因)。
|
|
46
|
+
* @returns 一行里的值节点。
|
|
47
|
+
*/
|
|
48
|
+
export function InlineValue({ owner }: { owner: SchemaFieldOwnerProps }): ReactNode {
|
|
49
|
+
if (owner.options !== undefined) return <OptionSelect owner={owner} options={owner.options} />;
|
|
50
|
+
if (owner.node.meta.secret) {
|
|
51
|
+
// secret 不回显值:截断时不给全文,只给说明。
|
|
52
|
+
return (
|
|
53
|
+
<LineValue data-tone="empty" title={owner.hint}>
|
|
54
|
+
{owner.secretConfigured ? "••••••" : ""}
|
|
55
|
+
</LineValue>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<LineValue
|
|
60
|
+
data-tone={owner.node.readOnly === null ? tokenTone(owner.value) : "empty"}
|
|
61
|
+
// 值长了会截断:整份内容挂在 title 上,hover 就能看全。
|
|
62
|
+
title={tokenText(owner.value)}
|
|
63
|
+
>
|
|
64
|
+
{tokenText(owner.value)}
|
|
65
|
+
</LineValue>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 选择器:**值本身**就是下拉的触发(不再另画一个 `select` 与它并列),点开是官方菜单。
|
|
71
|
+
*
|
|
72
|
+
* 当前值不在候选里时(例如目录里已经没有那个服务商)照原样显示,选中才换成新值。
|
|
73
|
+
*/
|
|
74
|
+
function OptionSelect({
|
|
75
|
+
owner,
|
|
76
|
+
options,
|
|
77
|
+
}: {
|
|
78
|
+
owner: SchemaFieldOwnerProps;
|
|
79
|
+
options: readonly SelectOption[];
|
|
80
|
+
}): ReactNode {
|
|
81
|
+
const [open, setOpen] = useState(false);
|
|
82
|
+
const selected = options.findIndex((option) => sameValue(option.value, owner.value));
|
|
83
|
+
const current = selected < 0 ? undefined : options[selected];
|
|
84
|
+
const label =
|
|
85
|
+
current?.label ??
|
|
86
|
+
(owner.value === undefined
|
|
87
|
+
? owner.t("unconfigured")
|
|
88
|
+
: tokenText(current?.value ?? owner.value));
|
|
89
|
+
return (
|
|
90
|
+
<Menu
|
|
91
|
+
open={open}
|
|
92
|
+
dense
|
|
93
|
+
compact
|
|
94
|
+
// 浮层挂到 body:行容器与表单壳的裁剪/层级都挡不住它。
|
|
95
|
+
portal
|
|
96
|
+
align="start"
|
|
97
|
+
items={options.map((option, index) => ({
|
|
98
|
+
id: String(index),
|
|
99
|
+
label: option.label ?? tokenText(option.value),
|
|
100
|
+
}))}
|
|
101
|
+
selectedId={selected < 0 ? undefined : String(selected)}
|
|
102
|
+
onSelect={(id) => {
|
|
103
|
+
setOpen(false);
|
|
104
|
+
const option = options[Number(id)];
|
|
105
|
+
if (option !== undefined) owner.onChange(option.value);
|
|
106
|
+
}}
|
|
107
|
+
onClose={() => {
|
|
108
|
+
setOpen(false);
|
|
109
|
+
}}
|
|
110
|
+
anchor={
|
|
111
|
+
<ValueTrigger
|
|
112
|
+
type="button"
|
|
113
|
+
disabled={owner.disabled}
|
|
114
|
+
aria-label={owner.label}
|
|
115
|
+
onClick={() => {
|
|
116
|
+
setOpen(true);
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
<LineValue
|
|
120
|
+
data-tone={current === undefined ? tokenTone(owner.value) : tokenTone(current.value)}
|
|
121
|
+
>
|
|
122
|
+
{label}
|
|
123
|
+
</LineValue>
|
|
124
|
+
<IconChevronDownOutlineRegular size={ICON_SIZE} />
|
|
125
|
+
</ValueTrigger>
|
|
126
|
+
}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 值是否就是这一项(菜单的高亮判据)。 */
|
|
132
|
+
function sameValue(left: unknown, right: unknown): boolean {
|
|
133
|
+
if (Object.is(left, right)) return true;
|
|
134
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
|
|
135
|
+
return false;
|
|
136
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 字段槽注册方复用的默认值:只改文案时拿它包一层即可(`owner` 上覆盖 `label`/`hint`)。
|
|
141
|
+
*
|
|
142
|
+
* 注意行式视图里键名来自配置键本身、注释来自 `hint`,所以 `label` 只在业务自写控件时才需要。
|
|
143
|
+
*/
|
|
144
|
+
export function SchemaFieldDefault({ owner }: { owner: SchemaFieldOwnerProps }): ReactNode {
|
|
145
|
+
return <InlineValue owner={owner} />;
|
|
146
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// 换成包内 css-in-js(CSSProps + dsw);上游那份是 CSS Modules,本包不做预编译。
|
|
3
3
|
|
|
4
4
|
import type { CSSProps } from "../styling/css.ts";
|
|
5
|
+
import { focusRing } from "../styling/focus.ts";
|
|
5
6
|
import { dsw } from "../theme.ts";
|
|
6
7
|
|
|
7
8
|
// 上游引用的 --dsw-alias-label-error 不在官方主题定义集内(token 树里没有这个变量),
|
|
@@ -50,7 +51,7 @@ export const styles = {
|
|
|
50
51
|
cursor: "default",
|
|
51
52
|
},
|
|
52
53
|
"&:focus-visible": {
|
|
53
|
-
outline:
|
|
54
|
+
outline: focusRing,
|
|
54
55
|
outlineOffset: "1px",
|
|
55
56
|
},
|
|
56
57
|
},
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// 换成包内 css-in-js(CSSProps + dsw);上游那份是 CSS Modules,本包不做预编译。
|
|
3
3
|
|
|
4
4
|
import type { CSSProps } from "../styling/css.ts";
|
|
5
|
+
import { focusRing } from "../styling/focus.ts";
|
|
5
6
|
import { dsw } from "../theme.ts";
|
|
6
7
|
|
|
7
8
|
// 上游引用的 --dsw-alias-bg-layer-4 不在官方主题定义集内(token 树里没有这个变量),
|
|
@@ -64,7 +65,7 @@ export const styles = {
|
|
|
64
65
|
color: dsw.alias.label.secondary,
|
|
65
66
|
},
|
|
66
67
|
"&:focus-visible": {
|
|
67
|
-
outline:
|
|
68
|
+
outline: focusRing,
|
|
68
69
|
outlineOffset: "1px",
|
|
69
70
|
},
|
|
70
71
|
},
|
|
@@ -132,7 +133,7 @@ export const styles = {
|
|
|
132
133
|
color: dsw.alias.label.primary,
|
|
133
134
|
"&:focus-visible": {
|
|
134
135
|
outline: "none",
|
|
135
|
-
borderColor: dsw.alias.
|
|
136
|
+
borderColor: dsw.alias.state.business.primary,
|
|
136
137
|
},
|
|
137
138
|
"&:disabled": {
|
|
138
139
|
color: dsw.alias.label.tertiary,
|
|
@@ -157,6 +157,7 @@ export function SettingsValueField(
|
|
|
157
157
|
* A write-only credential control. The value never rides a response, so the
|
|
158
158
|
* control reports only whether one is configured and starts blank; a blank
|
|
159
159
|
* draft writes nothing, which keeps the stored key rather than clearing it.
|
|
160
|
+
* 控件要求浏览器不要把已存的登录密码自动填进来(上游 11c0511271)。
|
|
160
161
|
* @param props - the field's copy, its staged text, and the configured state.
|
|
161
162
|
* @returns the labelled control.
|
|
162
163
|
*/
|
|
@@ -180,7 +181,7 @@ export function SettingsSecretField(
|
|
|
180
181
|
<Input
|
|
181
182
|
id={props.id}
|
|
182
183
|
type="password"
|
|
183
|
-
autoComplete="
|
|
184
|
+
autoComplete="new-password"
|
|
184
185
|
value={props.text}
|
|
185
186
|
disabled={props.disabled}
|
|
186
187
|
onChange={(event) => {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// 焦点环口径搬上游 ui-theme 的 `focus.css`:颜色读官方 `--dsw-focus-ring-color`
|
|
2
|
+
// (主题把它按输入模态解析成蓝,指针模态下置 transparent 抑制环),变量缺失时回退业务蓝;
|
|
3
|
+
// 宽度读官方的 `--dsw-focus-ring-width`。偏移与线宽的特例仍由各控件自己声明。
|
|
4
|
+
import { dsw } from "../theme.ts";
|
|
5
|
+
import { Token } from "./token.ts";
|
|
6
|
+
|
|
7
|
+
export const focusRing = `${String(dsw.focus.ring.width)} solid ${Token.collect(
|
|
8
|
+
Token.fallbackVar(dsw.focus.ring.color, String(dsw.alias.state.business.primary)),
|
|
9
|
+
)}`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 官方主题的 `--dsw-*` token 全集(
|
|
2
|
+
* 官方主题的 `--dsw-*` token 全集(395 个),由 packages/client/ui-primitives/scripts/gen-design-tokens.mts 生成,勿手改。
|
|
3
3
|
*
|
|
4
4
|
* 叶子是该变量的**默认值**(官方 light 主题里首次出现的定义):既是类型推导的来源,也可作 fallback。
|
|
5
5
|
* `"$"` 键出现在「自身也是 token 的分支」上(命名体系里有 40 个短名同时是长名前缀的 token)。
|
|
@@ -10,7 +10,7 @@ export const designTokens = {
|
|
|
10
10
|
bg: {
|
|
11
11
|
base: "var(--dsw-static-neutral-bluish-00)",
|
|
12
12
|
document: {
|
|
13
|
-
preview: "var(--dsw-static-neutral-bluish-
|
|
13
|
+
preview: "var(--dsw-static-neutral-bluish-100)",
|
|
14
14
|
},
|
|
15
15
|
layer: {
|
|
16
16
|
"1": "var(--dsw-static-neutral-bluish-00)",
|
|
@@ -132,7 +132,7 @@ export const designTokens = {
|
|
|
132
132
|
caption: "var(--dsw-static-neutral-bluish-400)",
|
|
133
133
|
dimmed: "var(--dsw-static-neutral-bluish-200)",
|
|
134
134
|
document: {
|
|
135
|
-
preview: "var(--dsw-static-neutral-bluish-
|
|
135
|
+
preview: "var(--dsw-static-neutral-bluish-700)",
|
|
136
136
|
},
|
|
137
137
|
primary: {
|
|
138
138
|
$: "var(--dsw-static-neutral-bluish-1000)",
|
|
@@ -163,6 +163,21 @@ export const designTokens = {
|
|
|
163
163
|
placeholder: "var(--dsw-static-neutral-bluish-60)",
|
|
164
164
|
tag: "var(--dsw-static-neutral-bluish-75)",
|
|
165
165
|
},
|
|
166
|
+
menu: {
|
|
167
|
+
icon: "var(--dsw-static-neutral-bluish-800)",
|
|
168
|
+
},
|
|
169
|
+
onboarding: {
|
|
170
|
+
accent: "rgb(57, 100, 254)",
|
|
171
|
+
card: {
|
|
172
|
+
fill: "color-mix(in srgb, var(--dsw-static-neutral-bluish-00) 80%, transparent)",
|
|
173
|
+
},
|
|
174
|
+
checkbox: {
|
|
175
|
+
border: "color-mix(in srgb, var(--dsw-static-neutral-bluish-1000) 20%, transparent)",
|
|
176
|
+
},
|
|
177
|
+
secondary: {
|
|
178
|
+
fill: "var(--dsw-static-neutral-bluish-00)",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
166
181
|
scrollbar: {
|
|
167
182
|
bg: {
|
|
168
183
|
l1: "var(--dsw-static-neutral-200)",
|
|
@@ -173,6 +188,12 @@ export const designTokens = {
|
|
|
173
188
|
l2: "var(--dsw-static-neutral-300)",
|
|
174
189
|
},
|
|
175
190
|
},
|
|
191
|
+
settings: {
|
|
192
|
+
card: {
|
|
193
|
+
fill: "var(--dsw-alias-bg-layer-2)",
|
|
194
|
+
stroke: "var(--dsw-alias-border-l4)",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
176
197
|
state: {
|
|
177
198
|
business: {
|
|
178
199
|
primary: "var(--dsw-static-deepseek-500)",
|
|
@@ -199,9 +220,13 @@ export const designTokens = {
|
|
|
199
220
|
},
|
|
200
221
|
toast: {
|
|
201
222
|
bg: "var(--dsw-static-neutral-bluish-800)",
|
|
223
|
+
label: "var(--dsw-static-neutral-bluish-00)",
|
|
202
224
|
},
|
|
203
225
|
tooltip: {
|
|
204
226
|
bg: "var(--dsw-static-neutral-bluish-850)",
|
|
227
|
+
key: {
|
|
228
|
+
bg: "color-mix(in srgb, var(--dsw-alias-tooltip-bg), white 18%)",
|
|
229
|
+
},
|
|
205
230
|
},
|
|
206
231
|
},
|
|
207
232
|
corner: {
|
|
@@ -218,6 +243,12 @@ export const designTokens = {
|
|
|
218
243
|
color: "var(--dsw-alias-border-l4)",
|
|
219
244
|
},
|
|
220
245
|
},
|
|
246
|
+
focus: {
|
|
247
|
+
ring: {
|
|
248
|
+
color: "transparent",
|
|
249
|
+
width: "2px",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
221
252
|
font: {
|
|
222
253
|
base: {
|
|
223
254
|
"16": {
|
|
@@ -247,8 +278,10 @@ export const designTokens = {
|
|
|
247
278
|
},
|
|
248
279
|
},
|
|
249
280
|
},
|
|
250
|
-
family:
|
|
251
|
-
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',\n 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif",
|
|
281
|
+
family: {
|
|
282
|
+
$: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',\n 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif",
|
|
283
|
+
brand: "'Montserrat', var(--dsw-font-family)",
|
|
284
|
+
},
|
|
252
285
|
l: {
|
|
253
286
|
"20": {
|
|
254
287
|
$: "500 20px/28px var(--dsw-font-family)",
|
|
@@ -610,6 +643,19 @@ export const designTokens = {
|
|
|
610
643
|
},
|
|
611
644
|
},
|
|
612
645
|
},
|
|
646
|
+
gradient: {
|
|
647
|
+
onboarding: {
|
|
648
|
+
blue: {
|
|
649
|
+
stops: "#3964fe 18.75%, #398efe 51.78%, #6dccff 86.252%, #3964fe",
|
|
650
|
+
},
|
|
651
|
+
cyan: {
|
|
652
|
+
stops: "#0293b4 21.154%, #2dc8eb 50.954%, #aff0ff 85.326%, #0293b4",
|
|
653
|
+
},
|
|
654
|
+
violet: {
|
|
655
|
+
stops: "#2a2fb6 33.102%, #8b76f6 50.954%, #c5c0ff 85.326%, #2a2fb6",
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
},
|
|
613
659
|
linear: {
|
|
614
660
|
gradient: {
|
|
615
661
|
think: "linear-gradient(180deg, #fff 20.19%, rgba(255, 255, 255, 0) 100%)",
|
|
@@ -619,12 +665,23 @@ export const designTokens = {
|
|
|
619
665
|
},
|
|
620
666
|
},
|
|
621
667
|
mask: {
|
|
622
|
-
blur: "
|
|
668
|
+
blur: "none",
|
|
623
669
|
},
|
|
624
670
|
menu: {
|
|
625
671
|
backdrop: {
|
|
626
672
|
filter: "blur(40px) saturate(150%)",
|
|
627
673
|
},
|
|
674
|
+
surface: {
|
|
675
|
+
fill: "rgba(248, 249, 250, 0.58)",
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
radius: {
|
|
679
|
+
lg: "16px",
|
|
680
|
+
md: "12px",
|
|
681
|
+
panel: "28px",
|
|
682
|
+
sm: "8px",
|
|
683
|
+
xl: "20px",
|
|
684
|
+
xs: "4px",
|
|
628
685
|
},
|
|
629
686
|
shadow: {
|
|
630
687
|
lv1: {
|
|
@@ -645,7 +702,7 @@ export const designTokens = {
|
|
|
645
702
|
login: {
|
|
646
703
|
input: "var(--dsw-static-neutral-bluish-50)",
|
|
647
704
|
},
|
|
648
|
-
menu: "
|
|
705
|
+
menu: "var(--dsw-menu-surface-fill)",
|
|
649
706
|
selector: "var(--dsw-static-neutral-bluish-60)",
|
|
650
707
|
sidebar: {
|
|
651
708
|
fill: "var(--dsw-static-neutral-bluish-50)",
|
package/cordis.patch.yml
DELETED