@morlay/dsh-client-ui-primitives 0.0.2-alpha.6 → 0.0.2-alpha.8
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 +30 -4
- package/dist/client.cjs +1069 -399
- package/dist/client.d.cts +366 -9
- package/dist/client.d.mts +366 -9
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +9 -6
- package/src/client/index.ts +1 -0
- package/src/client/settings-form/SettingsForm.styles.ts +57 -0
- package/src/client/settings-form/SettingsForm.tsx +72 -0
- package/src/client/settings-form/fields.styles.ts +157 -0
- package/src/client/settings-form/fields.tsx +193 -0
- package/src/client/settings-form/form-model.ts +421 -0
- package/src/client/settings-form/index.ts +19 -0
- package/src/client/theme.generated.ts +748 -715
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// 样式搬自 vendor/deepseek-harness/packages/client/ui-primitives/src/settings-form/fields.module.css,
|
|
2
|
+
// 换成包内 css-in-js(CSSProps + dsw);上游那份是 CSS Modules,本包不做预编译。
|
|
3
|
+
|
|
4
|
+
import type { CSSProps } from "../styling/css.ts";
|
|
5
|
+
import { dsw } from "../theme.ts";
|
|
6
|
+
|
|
7
|
+
// 上游引用的 --dsw-alias-bg-layer-4 不在官方主题定义集内(token 树里没有这个变量),
|
|
8
|
+
// 照抄引用而不是换一个近义 token——官方主题哪天定义它,这里自动跟着生效。
|
|
9
|
+
const helpButtonHoverBackground = "var(--dsw-alias-bg-layer-4)";
|
|
10
|
+
|
|
11
|
+
export const styles = {
|
|
12
|
+
field: {
|
|
13
|
+
display: "flex",
|
|
14
|
+
flexDirection: "column",
|
|
15
|
+
gap: "6px",
|
|
16
|
+
padding: "12px 0",
|
|
17
|
+
"& + &": {
|
|
18
|
+
borderTop: `0.5px solid ${String(dsw.alias.border.l2)}`,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
head: {
|
|
22
|
+
display: "flex",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
gap: "8px",
|
|
25
|
+
},
|
|
26
|
+
label: {
|
|
27
|
+
flex: 1,
|
|
28
|
+
minWidth: 0,
|
|
29
|
+
fontSize: "13px",
|
|
30
|
+
fontWeight: 500,
|
|
31
|
+
lineHeight: 1.5,
|
|
32
|
+
color: dsw.alias.label.primary,
|
|
33
|
+
},
|
|
34
|
+
// 标签进 labelGroup 后由外层承担伸缩,自己不再抢空间(上游的 `.labelGroup > .label`)。
|
|
35
|
+
groupLabel: {
|
|
36
|
+
flex: "0 1 auto",
|
|
37
|
+
},
|
|
38
|
+
labelGroup: {
|
|
39
|
+
display: "flex",
|
|
40
|
+
alignItems: "center",
|
|
41
|
+
gap: "4px",
|
|
42
|
+
flex: 1,
|
|
43
|
+
minWidth: 0,
|
|
44
|
+
},
|
|
45
|
+
helpButton: {
|
|
46
|
+
display: "inline-flex",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
justifyContent: "center",
|
|
49
|
+
flex: "none",
|
|
50
|
+
width: "24px",
|
|
51
|
+
height: "24px",
|
|
52
|
+
padding: 0,
|
|
53
|
+
border: 0,
|
|
54
|
+
borderRadius: "6px",
|
|
55
|
+
background: "none",
|
|
56
|
+
color: dsw.alias.label.tertiary,
|
|
57
|
+
cursor: "pointer",
|
|
58
|
+
"&:hover": {
|
|
59
|
+
background: helpButtonHoverBackground,
|
|
60
|
+
color: dsw.alias.label.secondary,
|
|
61
|
+
},
|
|
62
|
+
"&[aria-expanded='true']": {
|
|
63
|
+
background: helpButtonHoverBackground,
|
|
64
|
+
color: dsw.alias.label.secondary,
|
|
65
|
+
},
|
|
66
|
+
"&:focus-visible": {
|
|
67
|
+
outline: `2px solid ${String(dsw.alias.brand.primary)}`,
|
|
68
|
+
outlineOffset: "1px",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
help: {
|
|
72
|
+
padding: "10px 0 0",
|
|
73
|
+
fontSize: "12px",
|
|
74
|
+
lineHeight: 1.6,
|
|
75
|
+
color: dsw.alias.label.secondary,
|
|
76
|
+
"& > p": {
|
|
77
|
+
margin: 0,
|
|
78
|
+
},
|
|
79
|
+
"& > p + p": {
|
|
80
|
+
marginTop: "8px",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
badges: {
|
|
84
|
+
display: "inline-flex",
|
|
85
|
+
alignItems: "center",
|
|
86
|
+
gap: "8px",
|
|
87
|
+
},
|
|
88
|
+
// 上游用 Tag(tone="neutral" | "quiet"):这里按变体拆两个 styled,形态与颜色照抄 Tag.module.css。
|
|
89
|
+
badge: {
|
|
90
|
+
display: "inline-flex",
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
borderRadius: "999px",
|
|
93
|
+
cornerShape: "round",
|
|
94
|
+
padding: "1px 8px",
|
|
95
|
+
fontSize: "11px",
|
|
96
|
+
lineHeight: "17px",
|
|
97
|
+
fontWeight: 500,
|
|
98
|
+
whiteSpace: "nowrap",
|
|
99
|
+
},
|
|
100
|
+
badgeNeutral: {
|
|
101
|
+
background: dsw.alias.bg.module.platform,
|
|
102
|
+
color: dsw.alias.label.secondary,
|
|
103
|
+
},
|
|
104
|
+
badgeQuiet: {
|
|
105
|
+
color: dsw.alias.label.tertiary,
|
|
106
|
+
},
|
|
107
|
+
reset: {
|
|
108
|
+
border: "none",
|
|
109
|
+
background: "none",
|
|
110
|
+
padding: 0,
|
|
111
|
+
font: "inherit",
|
|
112
|
+
fontSize: "12px",
|
|
113
|
+
lineHeight: 1.5,
|
|
114
|
+
color: dsw.alias.label.secondary,
|
|
115
|
+
cursor: "pointer",
|
|
116
|
+
"&:hover:not(:disabled)": {
|
|
117
|
+
color: dsw.alias.label.primary,
|
|
118
|
+
},
|
|
119
|
+
"&:disabled": {
|
|
120
|
+
cursor: "default",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
input: {
|
|
124
|
+
height: "34px",
|
|
125
|
+
padding: "0 12px",
|
|
126
|
+
border: `0.5px solid ${String(dsw.alias.border.l4)}`,
|
|
127
|
+
borderRadius: "8px",
|
|
128
|
+
background: dsw.alias.bg.layer["3"],
|
|
129
|
+
font: "inherit",
|
|
130
|
+
fontSize: "13px",
|
|
131
|
+
lineHeight: 1.5,
|
|
132
|
+
color: dsw.alias.label.primary,
|
|
133
|
+
"&:focus-visible": {
|
|
134
|
+
outline: "none",
|
|
135
|
+
borderColor: dsw.alias.brand.primary,
|
|
136
|
+
},
|
|
137
|
+
"&:disabled": {
|
|
138
|
+
color: dsw.alias.label.tertiary,
|
|
139
|
+
cursor: "default",
|
|
140
|
+
},
|
|
141
|
+
"&[aria-invalid='true']": {
|
|
142
|
+
borderColor: dsw.alias.state.error.primary,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
invalid: {
|
|
146
|
+
margin: 0,
|
|
147
|
+
fontSize: "12px",
|
|
148
|
+
lineHeight: 1.5,
|
|
149
|
+
color: dsw.alias.state.error.primary,
|
|
150
|
+
},
|
|
151
|
+
hint: {
|
|
152
|
+
margin: 0,
|
|
153
|
+
fontSize: "12px",
|
|
154
|
+
lineHeight: 1.5,
|
|
155
|
+
color: dsw.alias.label.tertiary,
|
|
156
|
+
},
|
|
157
|
+
} satisfies Record<string, CSSProps>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// 搬自 vendor/deepseek-harness/packages/client/ui-primitives/src/settings-form/fields.tsx
|
|
2
|
+
// (上游 `@deepseek-ai/dsh-client-ui-primitives`):结构与行为同形,样式从 CSS Modules 换成包内 css-in-js。
|
|
3
|
+
// 上游用的 Tag 与 IconInfoOutlineRegular 没有一起搬——本包不依赖上游 primitives,用等价的本地徽标与图标。
|
|
4
|
+
|
|
5
|
+
import { useState, type ReactNode } from "react";
|
|
6
|
+
import { styled } from "../styling/styled.tsx";
|
|
7
|
+
import { styles } from "./fields.styles.ts";
|
|
8
|
+
|
|
9
|
+
const Field = styled("div")(styles.field);
|
|
10
|
+
const Head = styled("div")(styles.head);
|
|
11
|
+
const LabelGroup = styled("div")(styles.labelGroup);
|
|
12
|
+
const GroupLabel = styled("label")(styles.label, styles.groupLabel);
|
|
13
|
+
const HeadLabel = styled("label")(styles.label);
|
|
14
|
+
const HelpButton = styled("button")(styles.helpButton);
|
|
15
|
+
const Help = styled("div")(styles.help);
|
|
16
|
+
const Badges = styled("span")(styles.badges);
|
|
17
|
+
const NeutralBadge = styled("span")(styles.badge, styles.badgeNeutral);
|
|
18
|
+
const QuietBadge = styled("span")(styles.badge, styles.badgeQuiet);
|
|
19
|
+
const Reset = styled("button")(styles.reset);
|
|
20
|
+
const Input = styled("input")(styles.input);
|
|
21
|
+
const Invalid = styled("p")(styles.invalid);
|
|
22
|
+
const Hint = styled("p")(styles.hint);
|
|
23
|
+
|
|
24
|
+
/** 上游 icons 里的 IconInfoOutlineRegular(fill-only 几何,整份图标表不搬)。 */
|
|
25
|
+
function InfoGlyph({ size }: { size: number }) {
|
|
26
|
+
return (
|
|
27
|
+
<svg
|
|
28
|
+
width={size}
|
|
29
|
+
height={size}
|
|
30
|
+
viewBox="0 0 14 14"
|
|
31
|
+
fill="none"
|
|
32
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
>
|
|
35
|
+
<path
|
|
36
|
+
d="M12.5757 7.00012C12.5757 3.92085 10.0794 1.42463 7.00012 1.42456C3.9208 1.42456 1.42456 3.9208 1.42456 7.00012C1.42463 10.0794 3.92085 12.5757 7.00012 12.5757C10.0793 12.5756 12.5756 10.0793 12.5757 7.00012ZM13.8002 7.00012C13.8001 10.7559 10.7559 13.8001 7.00012 13.8002C3.2443 13.8002 0.199291 10.7559 0.199219 7.00012C0.199219 3.24426 3.24426 0.199219 7.00012 0.199219C10.7559 0.199291 13.8002 3.2443 13.8002 7.00012Z"
|
|
37
|
+
fill="currentColor"
|
|
38
|
+
/>
|
|
39
|
+
<path d="M7.6127 3.18921V4.55986H6.38735V3.18921H7.6127Z" fill="currentColor" />
|
|
40
|
+
<path d="M7.6127 5.68921V10.8109H6.38735V5.68921H7.6127Z" fill="currentColor" />
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What every settings field control needs regardless of its value type. */
|
|
46
|
+
export interface SettingsFieldProps {
|
|
47
|
+
/** Stable id associating the label with its control. */
|
|
48
|
+
id: string;
|
|
49
|
+
/** Visible label. */
|
|
50
|
+
label: string;
|
|
51
|
+
/** One-line explanation rendered under the control. */
|
|
52
|
+
hint: string;
|
|
53
|
+
/** Draft text this control renders. */
|
|
54
|
+
text: string;
|
|
55
|
+
/** True when saving would leave a user-layer entry for this field. */
|
|
56
|
+
overridden: boolean;
|
|
57
|
+
/** True when the draft is not a value this field accepts. */
|
|
58
|
+
invalid: boolean;
|
|
59
|
+
/** Copy for the overridden badge. */
|
|
60
|
+
overriddenLabel: string;
|
|
61
|
+
/** Copy for the reset control. */
|
|
62
|
+
resetLabel: string;
|
|
63
|
+
/** Copy shown in place of the hint while the draft is invalid. */
|
|
64
|
+
invalidLabel: string;
|
|
65
|
+
/** Disables every control (read-only document, or an unavailable namespace). */
|
|
66
|
+
disabled: boolean;
|
|
67
|
+
/** Stage draft text. */
|
|
68
|
+
onEdit: (text: string) => void;
|
|
69
|
+
/** Stage a clear so the field re-inherits the composition layer. */
|
|
70
|
+
onReset: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A staged value field. `numeric` only hints the keypad: which drafts a field
|
|
75
|
+
* accepts is decided by its spec, so the control never silently rewrites what
|
|
76
|
+
* the user typed.
|
|
77
|
+
* @param props - the field's copy, its staged text, and the edit actions.
|
|
78
|
+
* @returns the labelled control.
|
|
79
|
+
*/
|
|
80
|
+
export function SettingsValueField(
|
|
81
|
+
props: Omit<SettingsFieldProps, "hint"> & {
|
|
82
|
+
/** Optional explanation shown below the input. */
|
|
83
|
+
hint?: string;
|
|
84
|
+
/** Rules disclosed by the information button beside the label. */
|
|
85
|
+
help?: { label: string; content: ReactNode };
|
|
86
|
+
/** Hints a numeric keypad without narrowing what the control accepts. */
|
|
87
|
+
numeric?: boolean;
|
|
88
|
+
/** Placeholder shown while the draft is empty. */
|
|
89
|
+
placeholder?: string;
|
|
90
|
+
},
|
|
91
|
+
) {
|
|
92
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
93
|
+
const helpId = `${props.id}-help`;
|
|
94
|
+
const messageId = `${props.id}-message`;
|
|
95
|
+
const hasMessage = props.invalid || Boolean(props.hint);
|
|
96
|
+
const description = [hasMessage ? messageId : "", helpOpen ? helpId : ""]
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join(" ");
|
|
99
|
+
return (
|
|
100
|
+
<Field>
|
|
101
|
+
<Head>
|
|
102
|
+
<LabelGroup>
|
|
103
|
+
<GroupLabel htmlFor={props.id}>{props.label}</GroupLabel>
|
|
104
|
+
{props.help !== undefined ? (
|
|
105
|
+
<HelpButton
|
|
106
|
+
type="button"
|
|
107
|
+
aria-label={props.help.label}
|
|
108
|
+
aria-expanded={helpOpen}
|
|
109
|
+
aria-controls={helpId}
|
|
110
|
+
onClick={() => {
|
|
111
|
+
setHelpOpen(!helpOpen);
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<InfoGlyph size={12} />
|
|
115
|
+
</HelpButton>
|
|
116
|
+
) : null}
|
|
117
|
+
</LabelGroup>
|
|
118
|
+
{props.overridden ? (
|
|
119
|
+
<Badges>
|
|
120
|
+
<NeutralBadge>{props.overriddenLabel}</NeutralBadge>
|
|
121
|
+
<Reset type="button" disabled={props.disabled} onClick={props.onReset}>
|
|
122
|
+
{props.resetLabel}
|
|
123
|
+
</Reset>
|
|
124
|
+
</Badges>
|
|
125
|
+
) : null}
|
|
126
|
+
</Head>
|
|
127
|
+
<Input
|
|
128
|
+
id={props.id}
|
|
129
|
+
type="text"
|
|
130
|
+
{...(props.numeric === true ? { inputMode: "numeric" as const } : {})}
|
|
131
|
+
{...(props.invalid ? { "aria-invalid": true } : {})}
|
|
132
|
+
aria-describedby={description || undefined}
|
|
133
|
+
value={props.text}
|
|
134
|
+
placeholder={props.placeholder ?? ""}
|
|
135
|
+
disabled={props.disabled}
|
|
136
|
+
onChange={(event) => {
|
|
137
|
+
props.onEdit(event.target.value);
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
{hasMessage ? (
|
|
141
|
+
props.invalid ? (
|
|
142
|
+
<Invalid id={messageId}>{props.invalidLabel}</Invalid>
|
|
143
|
+
) : (
|
|
144
|
+
<Hint id={messageId}>{props.hint}</Hint>
|
|
145
|
+
)
|
|
146
|
+
) : null}
|
|
147
|
+
{props.help !== undefined && helpOpen ? (
|
|
148
|
+
<Help id={helpId} role="region" aria-label={props.help.label}>
|
|
149
|
+
{props.help.content}
|
|
150
|
+
</Help>
|
|
151
|
+
) : null}
|
|
152
|
+
</Field>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A write-only credential control. The value never rides a response, so the
|
|
158
|
+
* control reports only whether one is configured and starts blank; a blank
|
|
159
|
+
* draft writes nothing, which keeps the stored key rather than clearing it.
|
|
160
|
+
* @param props - the field's copy, its staged text, and the configured state.
|
|
161
|
+
* @returns the labelled control.
|
|
162
|
+
*/
|
|
163
|
+
export function SettingsSecretField(
|
|
164
|
+
props: Pick<SettingsFieldProps, "id" | "label" | "hint" | "text" | "disabled" | "onEdit"> & {
|
|
165
|
+
/** Whether the Host reports a configured credential for this reference. */
|
|
166
|
+
configured: boolean;
|
|
167
|
+
/** Copy describing the configured state. */
|
|
168
|
+
stateLabel: string;
|
|
169
|
+
},
|
|
170
|
+
) {
|
|
171
|
+
const Badge = props.configured ? NeutralBadge : QuietBadge;
|
|
172
|
+
return (
|
|
173
|
+
<Field>
|
|
174
|
+
<Head>
|
|
175
|
+
<HeadLabel htmlFor={props.id}>{props.label}</HeadLabel>
|
|
176
|
+
<Badges>
|
|
177
|
+
<Badge>{props.stateLabel}</Badge>
|
|
178
|
+
</Badges>
|
|
179
|
+
</Head>
|
|
180
|
+
<Input
|
|
181
|
+
id={props.id}
|
|
182
|
+
type="password"
|
|
183
|
+
autoComplete="off"
|
|
184
|
+
value={props.text}
|
|
185
|
+
disabled={props.disabled}
|
|
186
|
+
onChange={(event) => {
|
|
187
|
+
props.onEdit(event.target.value);
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
190
|
+
<Hint>{props.hint}</Hint>
|
|
191
|
+
</Field>
|
|
192
|
+
);
|
|
193
|
+
}
|