@morlay/dsh-client-ui-primitives 0.0.6 → 0.0.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 +31 -6
- package/dist/client.d.mts +186 -11175
- package/dist/client.mjs +3730 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +23 -34
- package/src/client/controls/Bubble.module.css +17 -0
- package/src/client/controls/Bubble.tsx +17 -0
- package/src/client/controls/Button.module.css +10 -0
- package/src/client/controls/Button.tsx +29 -0
- package/src/client/controls/DisclosureRow.module.css +22 -0
- package/src/client/controls/DisclosureRow.tsx +20 -0
- package/src/client/controls/IconButton.module.css +50 -0
- package/src/client/controls/IconButton.tsx +45 -0
- package/src/client/controls/ModelRouteList.module.css +100 -0
- package/src/client/controls/ModelRouteList.tsx +118 -0
- package/src/client/controls/MultilineField.module.css +26 -0
- package/src/client/controls/MultilineField.tsx +19 -0
- package/src/client/controls/SearchInput.module.css +11 -0
- package/src/client/controls/SearchInput.tsx +22 -0
- package/src/client/controls/SearchSelect.module.css +34 -0
- package/src/client/controls/SearchSelect.tsx +138 -0
- package/src/client/controls/SeatButton.module.css +53 -0
- package/src/client/controls/SeatButton.tsx +40 -0
- package/src/client/controls/Spinner.module.css +18 -0
- package/src/client/controls/Spinner.tsx +14 -0
- package/src/client/controls/Tabs.module.css +48 -0
- package/src/client/controls/Tabs.tsx +57 -0
- package/src/client/controls/TagInput.module.css +105 -0
- package/src/client/controls/TagInput.tsx +152 -0
- package/src/client/controls/TimeLabel.module.css +16 -0
- package/src/client/controls/TimeLabel.tsx +15 -0
- package/src/client/controls/VisuallyHidden.module.css +9 -0
- package/src/client/controls/VisuallyHidden.tsx +16 -0
- package/src/client/controls/classes.ts +7 -0
- package/src/client/controls/index.ts +30 -0
- package/src/client/controls/tags.ts +24 -0
- package/src/client/index.ts +17 -9
- package/src/client/layout/Row.tsx +86 -0
- package/src/client/layout/Stack.tsx +79 -0
- package/src/client/layout/Text.tsx +54 -0
- package/src/client/layout/index.ts +16 -0
- package/src/client/layout/layout.module.css +217 -0
- package/src/client/layout/scales.ts +91 -0
- package/src/client/schema-form/Editor.tsx +61 -70
- package/src/client/schema-form/SchemaForm.tsx +4 -4
- package/src/client/schema-form/TruncatedTooltip.tsx +60 -0
- package/src/client/schema-form/controller.ts +5 -2
- package/src/client/schema-form/geometry.ts +3 -0
- package/src/client/schema-form/index.ts +11 -0
- package/src/client/schema-form/schema-form.module.css +335 -0
- package/src/client/schema-form/testing/resize-observer-stub.ts +63 -0
- package/src/client/schema-form/value.tsx +18 -15
- package/src/client/settings-form/SettingsFieldRow.module.css +93 -0
- package/src/client/settings-form/SettingsFieldRow.tsx +103 -0
- package/src/client/settings-form/index.ts +5 -18
- package/src/css-modules.d.ts +6 -0
- package/dist/client.cjs +0 -17075
- package/dist/client.d.cts +0 -11656
- package/dist/index.d.cts +0 -5
- package/dist/reference-BX54bj6_.d.cts +0 -30
- package/src/client/schema-form/styles.ts +0 -298
- package/src/client/settings-form/SettingsForm.styles.ts +0 -58
- package/src/client/settings-form/SettingsForm.tsx +0 -68
- package/src/client/settings-form/fields.styles.ts +0 -158
- package/src/client/settings-form/fields.tsx +0 -184
- package/src/client/settings-form/form-model.ts +0 -339
- package/src/client/styling/css.ts +0 -27
- package/src/client/styling/focus.ts +0 -9
- package/src/client/styling/index.ts +0 -6
- package/src/client/styling/styled.tsx +0 -54
- package/src/client/styling/styling.ts +0 -137
- package/src/client/styling/token.ts +0 -275
- package/src/client/styling/toolkit.ts +0 -37
- package/src/client/theme.generated.ts +0 -833
- package/src/client/theme.ts +0 -5
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "UI Primitives",
|
|
4
|
-
"description": "css-in-js styling layer, the
|
|
4
|
+
"description": "The shared base for the conversation UI and the settings pages: the css-in-js styling layer, the reference parser and renderer (one source shared with the host), the staged settings-form primitives forked from upstream, and the schema-generated row config pages (field-slot injection). Inlined by the client rows that use it, not an assembly row."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "UI 基础层",
|
|
4
|
-
"description": "css-in-js
|
|
4
|
+
"description": "对话 UI 与设置页共用的基础面:css-in-js 样式层、host 与 client 共用一份源码的引用解析与渲染转换、上游 fork 过来的暂存式设置表单原语,以及按 schema 自动生成的行配置页(字段槽注入)。随用到它的 client 行内联,不是装配行。"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/dsh-client-ui-primitives",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "client UI kit for this workspace: re-exports the official ui-primitives (Button / Menu / Input / Tag / Modal / Switch / SettingsForm …) plus our own layout primitives (Stack / Row / Text / Panel) and controls (TagInput / SearchSelect / ModelRouteList / Tabs / Spinner …), all styled with the same CSS Modules + `--dsw-*` tokens as upstream; also the shared reference parser/renderer (findReferences / parseReferenceToken) and the schema-generated row config page. Ships as an inlined client library (`dsh.client.inline`), not as an assembly row.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"client",
|
|
7
|
-
"css-
|
|
7
|
+
"css-modules",
|
|
8
8
|
"design-tokens",
|
|
9
9
|
"dsh",
|
|
10
10
|
"dsh-plugin",
|
|
11
|
-
"
|
|
11
|
+
"ui-kit"
|
|
12
12
|
],
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
@@ -24,11 +24,8 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
|
26
26
|
".": "./dist/index.mjs",
|
|
27
|
+
"./client": "./dist/client.mjs",
|
|
27
28
|
"./package.json": "./package.json",
|
|
28
|
-
"./client": {
|
|
29
|
-
"types": "./dist/client.d.cts",
|
|
30
|
-
"default": "./dist/client.cjs"
|
|
31
|
-
},
|
|
32
29
|
"./locale/*.json": "./locale/*.json"
|
|
33
30
|
},
|
|
34
31
|
"dependencies": {
|
|
@@ -40,46 +37,38 @@
|
|
|
40
37
|
"micromark-util-types": "^2.0.2"
|
|
41
38
|
},
|
|
42
39
|
"devDependencies": {
|
|
43
|
-
"@deepseek-ai/dsh-api-remotes": "0.2.0-rc.
|
|
44
|
-
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.
|
|
45
|
-
"@deepseek-ai/dsh-client-store": "0.2.0-rc.
|
|
46
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.
|
|
47
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.
|
|
48
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.
|
|
49
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.
|
|
50
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.
|
|
51
|
-
"@deepseek-ai/dsh-util-values": "0.2.0-rc.
|
|
40
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-util-values": "0.2.0-rc.2",
|
|
52
49
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
53
50
|
"@testing-library/react": "^16.3.0",
|
|
54
51
|
"@types/mdast": "^4.0.4",
|
|
55
52
|
"@types/react": "~18.3.1",
|
|
56
|
-
"csstype": "^3.1.3",
|
|
57
53
|
"react": "^18.2.0",
|
|
58
54
|
"react-dom": "^18.2.0"
|
|
59
55
|
},
|
|
60
56
|
"peerDependencies": {
|
|
61
57
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
62
|
-
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.
|
|
63
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.
|
|
64
|
-
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.0-rc.
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.
|
|
58
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.0-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2"
|
|
69
65
|
},
|
|
70
66
|
"dsh": {
|
|
71
67
|
"client": {
|
|
72
|
-
"
|
|
73
|
-
"inject": [
|
|
74
|
-
"@deepseek-ai/dsh-client-locale",
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-settings",
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-slots"
|
|
78
|
-
]
|
|
68
|
+
"inline": true
|
|
79
69
|
}
|
|
80
70
|
},
|
|
81
71
|
"scripts": {
|
|
82
|
-
"build": "pnpm exec tsdown"
|
|
83
|
-
"gen:tokens": "pnpm exec tsx scripts/gen-design-tokens.mts && pnpm exec oxfmt src/client/theme.generated.ts"
|
|
72
|
+
"build": "pnpm exec tsdown"
|
|
84
73
|
}
|
|
85
74
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* 对话气泡:照上游 `ui-chat/chat/MessageItem.module.css` 的 `.bubble` 逐字(含那两条决定换行行为的声明——
|
|
2
|
+
投影出来的用户文本是 inline runs,气泡自己负责折行,装饰过的单行消息因此仍是一行)。 */
|
|
3
|
+
.bubble {
|
|
4
|
+
/* 上游以宽度轴的比例表达 Figma 那个 525px 上限(525/748 ≈ 0.702):气泡跟着可拖拽/自适应的列一起变宽,
|
|
5
|
+
百分比让窄窗口仍然成立。 */
|
|
6
|
+
max-width: 100%;
|
|
7
|
+
background: var(--dsw-specific-bubble);
|
|
8
|
+
border-radius: var(--dsw-radius-xl);
|
|
9
|
+
/* 默认字号下单行气泡 42px:22 行高 + 上下各 10 内距;字号偏好经 body 发布的轴同时移动字号与行高。 */
|
|
10
|
+
padding: 10px 16px;
|
|
11
|
+
font-size: var(--dsh-content-font-size, 14px);
|
|
12
|
+
line-height: calc(22px + var(--dsh-content-font-delta, 0px));
|
|
13
|
+
color: var(--dsw-alias-label-primary);
|
|
14
|
+
/* 折行归气泡:不加这两条,投影出来的 runs 会按 `white-space: normal` 折叠空白,与上游对不上。 */
|
|
15
|
+
white-space: pre-wrap;
|
|
16
|
+
word-break: break-word;
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// 对话气泡:一条消息的内容块(底色 + 大圆角 + 内距 + 自己的最大宽度)。
|
|
2
|
+
//
|
|
3
|
+
// 宽度上限(`min(525px, 82%)`)归它:消息列多宽是对话面的口径,不该让每个调用方各写一遍。
|
|
4
|
+
|
|
5
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
6
|
+
import { classes } from "./classes.ts";
|
|
7
|
+
import css from "./Bubble.module.css";
|
|
8
|
+
|
|
9
|
+
export type BubbleProps = HTMLAttributes<HTMLDivElement>;
|
|
10
|
+
|
|
11
|
+
export function Bubble({ className, children, ...rest }: BubbleProps): ReactNode {
|
|
12
|
+
return (
|
|
13
|
+
<div className={classes(css.bubble, className)} {...rest}>
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// 按钮:官方 `Button` + 两条本包统一的修饰——**文字不换行**(按钮宽度由内容定,短词被折成两行只是没必要的
|
|
2
|
+
// 换行)、`tone="danger"` 时把填充换成错误色(危险动作的配色只在这里出现,入口保持中性)。
|
|
3
|
+
// 业务从本包取到的就是这个包装版(`../index.ts` 里显式导出它,覆盖官方同名的那份)。
|
|
4
|
+
|
|
5
|
+
import { forwardRef, type ComponentProps, type ReactNode } from "react";
|
|
6
|
+
import { Button as OfficialButton } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
7
|
+
import { classes } from "./classes.ts";
|
|
8
|
+
import css from "./Button.module.css";
|
|
9
|
+
|
|
10
|
+
export type ButtonTone = "danger";
|
|
11
|
+
|
|
12
|
+
export type ButtonProps = ComponentProps<typeof OfficialButton> & {
|
|
13
|
+
// 配色档:缺省用官方那套;`danger` 换成错误色。
|
|
14
|
+
tone?: ButtonTone;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|
18
|
+
{ className, tone, ...rest },
|
|
19
|
+
ref,
|
|
20
|
+
): ReactNode {
|
|
21
|
+
return (
|
|
22
|
+
<OfficialButton
|
|
23
|
+
ref={ref}
|
|
24
|
+
data-role="button"
|
|
25
|
+
className={classes(className, css.label, tone === "danger" ? css.danger : undefined)}
|
|
26
|
+
{...rest}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* 折叠行的两个覆盖类——官方 `DisclosureRow` 的 `contentClassName` / `contentLayoutClassName` 名字容易读错,
|
|
2
|
+
它们不是"内容区布局",而是**标题那一块**的类:
|
|
3
|
+
|
|
4
|
+
- `contentClassName` 落在 `TextShimmer` 的 `.root` 上(上游是 `display: inline-grid; width: max-content;
|
|
5
|
+
max-width: 100%`)。这里只加 flex 属性把它撑开(`flex: 1 1 auto` 不会覆盖 `display`),让右侧那一组
|
|
6
|
+
(摘要 / 删除入口)能用 `margin-left: auto` 贴到行尾。
|
|
7
|
+
- `contentLayoutClassName` 落在内层 `.content` 上(上游默认就是 `display: flex; align-items: center;
|
|
8
|
+
min-width: 0`):补上标题与右侧内容之间的间距,其余保持上游口径。
|
|
9
|
+
|
|
10
|
+
注意别往这两条里塞"卡片头布局"——它们只作用于标题文字那一层。 */
|
|
11
|
+
.content {
|
|
12
|
+
flex: 1 1 auto;
|
|
13
|
+
min-width: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.contentLayout {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
width: 100%;
|
|
21
|
+
min-width: 0;
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// 折叠行:官方 `DisclosureRow` + 两条本包固定的布局类(内容区撑开并允许收缩、标题行左右分列)。
|
|
2
|
+
//
|
|
3
|
+
// 官方那份把这两个类留成 `contentClassName` / `contentLayoutClassName` 供消费方覆盖——设置面上每次都要同样
|
|
4
|
+
// 两条,所以在这里定下来:业务只传标题、摘要与展开状态,不再传类。
|
|
5
|
+
|
|
6
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
7
|
+
import { DisclosureRow as OfficialDisclosureRow } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
8
|
+
import css from "./DisclosureRow.module.css";
|
|
9
|
+
|
|
10
|
+
export type DisclosureRowProps = ComponentProps<typeof OfficialDisclosureRow>;
|
|
11
|
+
|
|
12
|
+
export function DisclosureRow(props: DisclosureRowProps): ReactNode {
|
|
13
|
+
return (
|
|
14
|
+
<OfficialDisclosureRow
|
|
15
|
+
contentClassName={css.content}
|
|
16
|
+
contentLayoutClassName={css.contentLayout}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* 方形图标按钮:没有文字、没有边框,hover 才上底色(官方侧边栏那种 28×28 几何)。 */
|
|
2
|
+
.iconButton {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
justify-content: center;
|
|
7
|
+
flex: none;
|
|
8
|
+
width: 28px;
|
|
9
|
+
height: 28px;
|
|
10
|
+
padding: 0;
|
|
11
|
+
color: var(--dsw-alias-label-secondary);
|
|
12
|
+
background: transparent;
|
|
13
|
+
border: none;
|
|
14
|
+
border-radius: var(--dsw-radius-sm);
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.iconButton:hover:not(:disabled) {
|
|
19
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.iconButton:disabled {
|
|
23
|
+
cursor: not-allowed;
|
|
24
|
+
opacity: 0.4;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.iconButton:focus-visible {
|
|
28
|
+
outline: var(--dsw-focus-ring-width) solid
|
|
29
|
+
var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
30
|
+
outline-offset: -2px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* 行内小档:24px 高的行(上游折叠行的 `.row`)里放 28px 会顶出这一行。 */
|
|
34
|
+
.sm {
|
|
35
|
+
width: 20px;
|
|
36
|
+
height: 20px;
|
|
37
|
+
padding: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* 圆形那一档:对话动作行的 28×28 圆键(hover 才上底色,颜色走三级文本色)。 */
|
|
41
|
+
.circle {
|
|
42
|
+
padding: 6px;
|
|
43
|
+
border-radius: 28px;
|
|
44
|
+
color: var(--dsw-alias-label-tertiary);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.circle:hover:not(:disabled) {
|
|
48
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
49
|
+
color: var(--dsw-alias-label-secondary);
|
|
50
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// 图标按钮:没有文字、没有边框的方形按钮(官方侧边栏那种几何)。`label` 是它唯一的无障碍名。
|
|
2
|
+
|
|
3
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { classes } from "./classes.ts";
|
|
5
|
+
import css from "./IconButton.module.css";
|
|
6
|
+
|
|
7
|
+
export interface IconButtonProps
|
|
8
|
+
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children" | "aria-label"> {
|
|
9
|
+
// 无障碍名(图标没有文字,这个名字是它唯一的标识)。
|
|
10
|
+
label: string;
|
|
11
|
+
// 图标。
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
// 形状档:缺省是设置面那种方形圆角;`circle` 是对话动作行那种圆形。
|
|
14
|
+
shape?: "square" | "circle";
|
|
15
|
+
// 尺寸档:`md`(缺省)28×28 是独立控件的尺度;`sm` 20×20 放进 24px 高的行里
|
|
16
|
+
// (上游折叠行 `.row` 的高度就是 24px,放 28px 会被顶出来)。
|
|
17
|
+
size?: "md" | "sm";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function IconButton({
|
|
21
|
+
label,
|
|
22
|
+
children,
|
|
23
|
+
type = "button",
|
|
24
|
+
shape = "square",
|
|
25
|
+
size = "md",
|
|
26
|
+
className,
|
|
27
|
+
...rest
|
|
28
|
+
}: IconButtonProps): ReactNode {
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
type={type}
|
|
32
|
+
data-role="icon-button"
|
|
33
|
+
aria-label={label}
|
|
34
|
+
className={classes(
|
|
35
|
+
css.iconButton,
|
|
36
|
+
size === "sm" ? css.sm : undefined,
|
|
37
|
+
shape === "circle" ? css.circle : undefined,
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
{...rest}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</button>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* 模型路由清单(`ModelRouteList`):分组铺开、只选一条。数值照官方「子智能体」那张卡的模型清单。 */
|
|
2
|
+
.list {
|
|
3
|
+
display: grid;
|
|
4
|
+
gap: 6px;
|
|
5
|
+
min-width: 0;
|
|
6
|
+
max-height: 280px;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 10px;
|
|
9
|
+
overflow: auto;
|
|
10
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
11
|
+
border-radius: var(--dsw-radius-lg);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.group {
|
|
15
|
+
display: grid;
|
|
16
|
+
gap: 6px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.group + .group {
|
|
20
|
+
margin-top: 4px;
|
|
21
|
+
padding-top: 10px;
|
|
22
|
+
border-top: 0.5px solid var(--dsw-alias-border-l3);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.provider {
|
|
26
|
+
padding: 0 6px;
|
|
27
|
+
font-size: 11px;
|
|
28
|
+
font-weight: 500;
|
|
29
|
+
color: var(--dsw-alias-label-tertiary);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.item {
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 8px;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
padding: 6px;
|
|
39
|
+
border-radius: var(--dsw-radius-md);
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.item:hover {
|
|
44
|
+
/* 上游那张卡的 hover 底色引用了 `--dsw-alias-bg-layer-4`:它不在官方 token 集里,照抄引用而不是换一个近义 token。 */
|
|
45
|
+
background: var(--dsw-alias-bg-layer-4);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.item:focus-visible {
|
|
49
|
+
outline: var(--dsw-focus-ring-width) solid
|
|
50
|
+
var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
51
|
+
outline-offset: -2px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.item[aria-disabled='true'] {
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
opacity: 0.4;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* 选中的那一条:底色 + 勾(勾由组件画)。 */
|
|
60
|
+
.picked {
|
|
61
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.text {
|
|
65
|
+
min-width: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.name {
|
|
69
|
+
display: block;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
text-overflow: ellipsis;
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
font-size: 13px;
|
|
74
|
+
color: var(--dsw-alias-label-primary);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.id {
|
|
78
|
+
display: block;
|
|
79
|
+
margin-top: 2px;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
text-overflow: ellipsis;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
font-size: 11px;
|
|
84
|
+
color: var(--dsw-alias-label-tertiary);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* 清单的状态行(读目录中 / 失败 / 一条都没有)。 */
|
|
88
|
+
.notice {
|
|
89
|
+
margin: 0;
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
line-height: 1.5;
|
|
92
|
+
color: var(--dsw-alias-label-tertiary);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.error {
|
|
96
|
+
margin: 0;
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
line-height: 1.5;
|
|
99
|
+
color: var(--dsw-alias-state-error-primary);
|
|
100
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// 模型路由清单(`ModelRouteList`):把"已设置的模型"按 provider 分组铺开,**只选一条**——一条路由就是
|
|
2
|
+
// provider + model 这一对,所以选它等于把两者一起定下来。
|
|
3
|
+
//
|
|
4
|
+
// 形态照官方"子智能体"那张卡的模型清单(分组标题 + 两行的项:模型名 / `provider · provider/model`),
|
|
5
|
+
// 区别只在这里是单选。
|
|
6
|
+
|
|
7
|
+
import type { ReactNode } from "react";
|
|
8
|
+
import { IconCheckOutlineRegular } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
9
|
+
import { classes } from "./classes.ts";
|
|
10
|
+
import css from "./ModelRouteList.module.css";
|
|
11
|
+
|
|
12
|
+
// 一条可选路由:`key` 是稳定身份(调用方只用来比对,不解析它)。
|
|
13
|
+
export interface ModelRouteCandidate {
|
|
14
|
+
readonly key: string;
|
|
15
|
+
readonly provider: string;
|
|
16
|
+
readonly providerName: string;
|
|
17
|
+
readonly model: string;
|
|
18
|
+
readonly modelName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ModelRouteListProps {
|
|
22
|
+
// 清单的无障碍名(`role="radiogroup"`)。
|
|
23
|
+
label: string;
|
|
24
|
+
candidates: readonly ModelRouteCandidate[];
|
|
25
|
+
// 当前选中的那条(没选 = `undefined`)。
|
|
26
|
+
selectedKey: string | undefined;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
// 读目录的状态:读的时候说一句、失败的时候说另一句。
|
|
29
|
+
status: "loading" | "ready" | "error";
|
|
30
|
+
loadingLabel: string;
|
|
31
|
+
errorLabel: string;
|
|
32
|
+
emptyLabel: string;
|
|
33
|
+
onSelect: (candidate: ModelRouteCandidate) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ModelRouteList({
|
|
37
|
+
label,
|
|
38
|
+
candidates,
|
|
39
|
+
selectedKey,
|
|
40
|
+
disabled = false,
|
|
41
|
+
status,
|
|
42
|
+
loadingLabel,
|
|
43
|
+
errorLabel,
|
|
44
|
+
emptyLabel,
|
|
45
|
+
onSelect,
|
|
46
|
+
}: ModelRouteListProps): ReactNode {
|
|
47
|
+
if (status === "loading") {
|
|
48
|
+
return (
|
|
49
|
+
<p className={css.notice} role="status">
|
|
50
|
+
{loadingLabel}
|
|
51
|
+
</p>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (status === "error") {
|
|
55
|
+
return (
|
|
56
|
+
<p className={css.error} role="alert">
|
|
57
|
+
{errorLabel}
|
|
58
|
+
</p>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (candidates.length === 0) {
|
|
62
|
+
return <p className={css.notice}>{emptyLabel}</p>;
|
|
63
|
+
}
|
|
64
|
+
// 按 provider 分组,组的顺序就是候选来的顺序。
|
|
65
|
+
const groups = new Map<string, { name: string; items: ModelRouteCandidate[] }>();
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
const group = groups.get(candidate.provider);
|
|
68
|
+
if (group === undefined) {
|
|
69
|
+
groups.set(candidate.provider, { name: candidate.providerName, items: [candidate] });
|
|
70
|
+
} else group.items.push(candidate);
|
|
71
|
+
}
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
className={css.list}
|
|
75
|
+
data-role="model-route-list"
|
|
76
|
+
role="radiogroup"
|
|
77
|
+
aria-label={label}
|
|
78
|
+
data-control="model-routes"
|
|
79
|
+
>
|
|
80
|
+
{[...groups].map(([provider, group]) => (
|
|
81
|
+
<div key={provider} className={css.group} data-route-group={provider}>
|
|
82
|
+
<span className={css.provider}>{group.name}</span>
|
|
83
|
+
{group.items.map((candidate) => {
|
|
84
|
+
const picked = candidate.key === selectedKey;
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
key={candidate.key}
|
|
88
|
+
className={classes(css.item, picked ? css.picked : undefined)}
|
|
89
|
+
role="radio"
|
|
90
|
+
aria-checked={picked}
|
|
91
|
+
aria-label={`${candidate.modelName} ${candidate.provider}/${candidate.model}`}
|
|
92
|
+
aria-disabled={disabled ? true : undefined}
|
|
93
|
+
tabIndex={disabled ? -1 : 0}
|
|
94
|
+
data-route={candidate.key}
|
|
95
|
+
onClick={() => {
|
|
96
|
+
if (!disabled) onSelect(candidate);
|
|
97
|
+
}}
|
|
98
|
+
onKeyDown={(event) => {
|
|
99
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
if (!disabled) onSelect(candidate);
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{picked ? <IconCheckOutlineRegular /> : <span aria-hidden="true" />}
|
|
105
|
+
<span className={css.text}>
|
|
106
|
+
<span className={css.name}>{candidate.modelName}</span>
|
|
107
|
+
<span className={css.id}>
|
|
108
|
+
{`${candidate.providerName} · ${candidate.provider}/${candidate.model}`}
|
|
109
|
+
</span>
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</div>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* 多行文本:官方通用表单只有单行控件,这个按官方输入框的数值画(边框 / 圆角 / 字号 / 内距)。 */
|
|
2
|
+
.multiline {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
width: 100%;
|
|
5
|
+
min-height: 64px;
|
|
6
|
+
padding: 6px 12px;
|
|
7
|
+
font-family: inherit;
|
|
8
|
+
font-size: 13px;
|
|
9
|
+
line-height: 1.5;
|
|
10
|
+
color: var(--dsw-alias-label-primary);
|
|
11
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
12
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
13
|
+
border-radius: var(--dsw-radius-md);
|
|
14
|
+
resize: vertical;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.multiline:disabled {
|
|
18
|
+
cursor: not-allowed;
|
|
19
|
+
opacity: 0.4;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.multiline:focus-visible {
|
|
23
|
+
outline: var(--dsw-focus-ring-width) solid
|
|
24
|
+
var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
25
|
+
outline-offset: -2px;
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 多行文本:官方通用表单只有单行控件,这个按官方输入框的数值画(边框 / 圆角 / 字号 / 内距),
|
|
2
|
+
// 标签与说明由外面的字段行给(见 `settings-form` 的 `SettingsFieldRow`)。
|
|
3
|
+
|
|
4
|
+
import type { TextareaHTMLAttributes } from "react";
|
|
5
|
+
import { classes } from "./classes.ts";
|
|
6
|
+
import css from "./MultilineField.module.css";
|
|
7
|
+
|
|
8
|
+
export type MultilineFieldProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
|
|
9
|
+
|
|
10
|
+
export function MultilineField({ rows = 3, className, ...rest }: MultilineFieldProps) {
|
|
11
|
+
return (
|
|
12
|
+
<textarea
|
|
13
|
+
rows={rows}
|
|
14
|
+
data-role="multiline-field"
|
|
15
|
+
className={classes(css.multiline, className)}
|
|
16
|
+
{...rest}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// 搜索输入:官方 `Input` + 宽度档(列表与设置面的搜索框是固定一档宽)。
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
4
|
+
import { Input } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
5
|
+
import { classes } from "./classes.ts";
|
|
6
|
+
import css from "./SearchInput.module.css";
|
|
7
|
+
|
|
8
|
+
export type SearchInputWidth = "search" | "full";
|
|
9
|
+
|
|
10
|
+
export type SearchInputProps = ComponentProps<typeof Input> & {
|
|
11
|
+
// 宽度档:`search`(360px,列表的搜索行)或 `full`(撑满)。
|
|
12
|
+
width?: SearchInputWidth;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function SearchInput({ width = "search", className, ...rest }: SearchInputProps): ReactNode {
|
|
16
|
+
return (
|
|
17
|
+
<Input
|
|
18
|
+
className={classes(className, width === "search" ? css.search : css.full)}
|
|
19
|
+
{...rest}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* 与官方输入框同高(`Input.module.css` 的 32px):同行摆的控件高度必须一致。 */
|
|
2
|
+
.trigger {
|
|
3
|
+
height: 32px;
|
|
4
|
+
white-space: nowrap;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/* 菜单顶部那一行搜索框:与候选行同一个内距尺度。 */
|
|
8
|
+
.searchRow {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
padding: 4px 8px 8px;
|
|
12
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.searchInput {
|
|
16
|
+
width: 100%;
|
|
17
|
+
padding: 2px 0;
|
|
18
|
+
font-family: inherit;
|
|
19
|
+
font-size: 13px;
|
|
20
|
+
line-height: 1.5;
|
|
21
|
+
color: var(--dsw-alias-label-primary);
|
|
22
|
+
background: transparent;
|
|
23
|
+
border: none;
|
|
24
|
+
outline: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* 一个候选都没匹配上:在候选区里说一句,而不是给一张空菜单。 */
|
|
28
|
+
.empty {
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 8px;
|
|
31
|
+
font-size: 12px;
|
|
32
|
+
line-height: 1.5;
|
|
33
|
+
color: var(--dsw-alias-label-tertiary);
|
|
34
|
+
}
|