@morlay/dsh-client-ui-primitives 0.0.7 → 0.0.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 +32 -9
- package/dist/client-DYp99CgT.mjs +3730 -0
- package/dist/client-uyVkC7mu.cjs +17197 -0
- package/dist/client.cjs +54 -0
- package/dist/client.d.cts +4 -0
- package/dist/client.d.mts +3 -11680
- package/dist/client.mjs +3 -4575
- package/dist/index-AMHtcyMt.d.mts +647 -0
- package/dist/index.cjs +54 -0
- package/dist/index.d.mts +4 -5
- package/dist/index.mjs +4 -5
- package/dist/reference-hchCWm3z.cjs +482 -0
- package/dist/reference.cjs +4 -0
- package/dist/reference.d.mts +3 -0
- package/dist/reference.mjs +1 -0
- package/package.json +31 -35
- 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 +47 -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/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 +39 -0
- package/src/client/controls/Spinner.module.css +18 -0
- package/src/client/controls/Spinner.tsx +14 -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 +5 -0
- package/src/client/controls/index.ts +24 -0
- package/src/client/controls/tags.ts +24 -0
- package/src/client/index.ts +16 -2
- package/src/client/layout/Row.tsx +89 -0
- package/src/client/layout/Stack.tsx +82 -0
- package/src/client/layout/Text.tsx +61 -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 +65 -65
- package/src/client/schema-form/SchemaForm.tsx +9 -4
- 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 +5 -0
- package/src/client/schema-form/schema-form.module.css +335 -0
- package/src/client/schema-form/testing/fake-scope.ts +0 -1
- package/src/client/schema-form/value.tsx +15 -9
- package/src/client/settings-form/SettingsFieldRow.module.css +93 -0
- package/src/client/settings-form/SettingsFieldRow.tsx +105 -0
- package/src/client/settings-form/index.ts +5 -18
- package/src/css-modules.d.ts +6 -0
- package/src/index.ts +6 -2
- package/src/reference-markdown.tsx +2 -6
- package/dist/rolldown-runtime-EAsCxrDo.mjs +0 -14
- package/locale/en.json +0 -6
- package/locale/zh.json +0 -6
- package/src/client/schema-form/styles.ts +0 -321
- 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 -142
- package/src/client/styling/token.ts +0 -275
- package/src/client/styling/toolkit.ts +0 -37
- package/src/client/theme.generated.ts +0 -846
- package/src/client/theme.ts +0 -5
- /package/src/{client/markdown-text.ts → markdown-text.ts} +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* 座位按钮:胶囊形(16px 圆角、28px 高、8px 内距),标签以省略号收窄,图标与箭头贴住剩余文本。 */
|
|
2
|
+
.seat {
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: 4px;
|
|
6
|
+
min-width: 0;
|
|
7
|
+
max-width: min(100%, 240px);
|
|
8
|
+
min-height: 28px;
|
|
9
|
+
padding: 0 8px;
|
|
10
|
+
border: none;
|
|
11
|
+
border-radius: var(--dsw-radius-sm);
|
|
12
|
+
background: transparent;
|
|
13
|
+
color: var(--dsw-alias-label-primary);
|
|
14
|
+
font-size: 13px;
|
|
15
|
+
line-height: 20px;
|
|
16
|
+
font-weight: 500;
|
|
17
|
+
white-space: nowrap;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.seat:not(:disabled):hover,
|
|
22
|
+
.seat[aria-expanded="true"],
|
|
23
|
+
.expanded {
|
|
24
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.seat:disabled {
|
|
28
|
+
cursor: default;
|
|
29
|
+
color: var(--dsw-alias-label-quaternary);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* 图标直接是 flex item(上游给图标自己挂类,不再套一层):套一层会让它在 inline-flex 里按基线对齐,视觉上偏。 */
|
|
33
|
+
.icon {
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
flex: none;
|
|
37
|
+
color: var(--dsw-alias-label-primary);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.label {
|
|
41
|
+
min-width: 0;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
text-overflow: ellipsis;
|
|
44
|
+
white-space: nowrap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.chevron {
|
|
48
|
+
/* inline-flex 而不是 inline:inline 的座位会在 svg 下面预留 baseline descent,把箭头顶得不在 28px 的
|
|
49
|
+
座位里居中(上游 `PermissionSelect.module.css` 的 `.chevron` 记着同一条)。 */
|
|
50
|
+
display: inline-flex;
|
|
51
|
+
flex: 0 0 auto;
|
|
52
|
+
color: var(--dsw-alias-label-caption);
|
|
53
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// 座位按钮:工具行里的一个"座位"——图标 + 文本(+ 可展开的箭头),胶囊形,展开时保持底色。
|
|
2
|
+
// composer 里的模式座位、以及任何"点开是一份清单"的行内入口都用它。
|
|
3
|
+
|
|
4
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
5
|
+
import { classes } from "./classes.ts";
|
|
6
|
+
import css from "./SeatButton.module.css";
|
|
7
|
+
|
|
8
|
+
export interface SeatButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
|
9
|
+
// 座位上的图标(16px 那一档)。
|
|
10
|
+
icon: ReactNode;
|
|
11
|
+
// 座位上的文本;放不下就以省略号收窄。
|
|
12
|
+
label: string;
|
|
13
|
+
// 展开态:菜单开着时保持底色(与 `aria-expanded` 同一件事,这里只管观感)。
|
|
14
|
+
expanded?: boolean;
|
|
15
|
+
// 尾部箭头(可展开的座位给)。
|
|
16
|
+
chevron?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SeatButton({
|
|
20
|
+
icon,
|
|
21
|
+
label,
|
|
22
|
+
expanded = false,
|
|
23
|
+
chevron,
|
|
24
|
+
type = "button",
|
|
25
|
+
className,
|
|
26
|
+
...rest
|
|
27
|
+
}: SeatButtonProps): ReactNode {
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
type={type}
|
|
31
|
+
className={classes(css.seat, expanded ? css.expanded : undefined, className)}
|
|
32
|
+
{...rest}
|
|
33
|
+
>
|
|
34
|
+
<span className={css.icon}>{icon}</span>
|
|
35
|
+
<span className={css.label}>{label}</span>
|
|
36
|
+
{chevron === undefined ? null : <span className={css.chevron}>{chevron}</span>}
|
|
37
|
+
</button>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* 加载指示:一圈细描边 + 一段主色弧,转起来。 */
|
|
2
|
+
.spinner {
|
|
3
|
+
width: 28px;
|
|
4
|
+
height: 28px;
|
|
5
|
+
border: 2px solid var(--dsw-alias-border-l2);
|
|
6
|
+
border-top-color: var(--dsw-alias-label-primary);
|
|
7
|
+
border-radius: 50%;
|
|
8
|
+
animation: spin 900ms linear infinite;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@keyframes spin {
|
|
12
|
+
from {
|
|
13
|
+
transform: rotate(0deg);
|
|
14
|
+
}
|
|
15
|
+
to {
|
|
16
|
+
transform: rotate(360deg);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// 加载指示:官方那套没有这个原子(`StateDot` 是状态点、`TextShimmer` 是文字微光),
|
|
2
|
+
// 列表与设置面的「正在读」都用它。
|
|
3
|
+
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import css from "./Spinner.module.css";
|
|
6
|
+
|
|
7
|
+
export interface SpinnerProps {
|
|
8
|
+
// 无障碍名(转圈本身没有文字)。
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Spinner({ label }: SpinnerProps): ReactNode {
|
|
13
|
+
return <span className={css.spinner} role="status" aria-label={label} />;
|
|
14
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* 标签输入与它右边的候选菜单:一行,顶部对齐(框会随标签换行变高)。 */
|
|
2
|
+
.row {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: flex-start;
|
|
5
|
+
gap: 8px;
|
|
6
|
+
width: 100%;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* 候选菜单包锚点的那层 wrapper:官方 `Menu.module.css` 的 `.root` 是 `display: inline-flex`(宽度按内容收缩),
|
|
10
|
+
框因此只有内容那么宽。这一条把它撑满——框的宽度归外面那行,不归锚点自己的内容。 */
|
|
11
|
+
.anchor {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex: 1 1 auto;
|
|
14
|
+
width: 100%;
|
|
15
|
+
min-width: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* 框:与官方输入框同一种边框 / 背景 / 高度,标签与内联输入都在框里,放不下就换行。 */
|
|
19
|
+
.box {
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-flow: row wrap;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 4px 6px;
|
|
25
|
+
width: 100%;
|
|
26
|
+
min-height: 32px;
|
|
27
|
+
padding: 4px 8px;
|
|
28
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
29
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
30
|
+
border-radius: var(--dsw-radius-md);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* 框内的裸输入:边框、背景与焦点环都交给外面那个框。 */
|
|
34
|
+
.input {
|
|
35
|
+
flex: 1 1 120px;
|
|
36
|
+
min-width: 120px;
|
|
37
|
+
padding: 2px 0;
|
|
38
|
+
font-family: inherit;
|
|
39
|
+
font-size: 13px;
|
|
40
|
+
line-height: 1.5;
|
|
41
|
+
color: var(--dsw-alias-label-primary);
|
|
42
|
+
background: transparent;
|
|
43
|
+
border: none;
|
|
44
|
+
outline: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* 可移除的胶囊:几何与配色照官方 `Tag` 的 neutral(999px 圆角、11px/17px、wt500、`bg-module-platform`
|
|
48
|
+
+ 二级文本色),只是里面多一个「移除」按钮——官方 `Tag` 是只读的,嵌不进按钮。 */
|
|
49
|
+
.chip {
|
|
50
|
+
display: inline-flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 2px;
|
|
53
|
+
padding: 1px 4px 1px 8px;
|
|
54
|
+
border-radius: 999px;
|
|
55
|
+
corner-shape: round;
|
|
56
|
+
font-size: 11px;
|
|
57
|
+
line-height: 17px;
|
|
58
|
+
font-weight: 500;
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
color: var(--dsw-alias-label-secondary);
|
|
61
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* 胶囊里的移除按钮:图标本身很小,给它一块贴着字号的点击区,不给可见的边框与背景。 */
|
|
65
|
+
.chipRemove {
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
flex: none;
|
|
71
|
+
width: 14px;
|
|
72
|
+
height: 14px;
|
|
73
|
+
padding: 0;
|
|
74
|
+
color: var(--dsw-alias-label-tertiary);
|
|
75
|
+
background: transparent;
|
|
76
|
+
border: none;
|
|
77
|
+
border-radius: 999px;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
line-height: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.chipRemove:hover:not(:disabled) {
|
|
83
|
+
color: var(--dsw-alias-label-primary);
|
|
84
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.chipRemove:disabled {
|
|
88
|
+
cursor: not-allowed;
|
|
89
|
+
opacity: 0.4;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.chipRemove:focus-visible {
|
|
93
|
+
outline: var(--dsw-focus-ring-width) solid
|
|
94
|
+
var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
95
|
+
outline-offset: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* 一个候选都没匹配上:在候选区里说一句,而不是给一张空菜单。 */
|
|
99
|
+
.empty {
|
|
100
|
+
margin: 0;
|
|
101
|
+
padding: 8px;
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
line-height: 1.5;
|
|
104
|
+
color: var(--dsw-alias-label-tertiary);
|
|
105
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// 标签输入(multi-input 的 searchable 形态):一个框包住所有标签与那一个内联输入框,**聚焦即整体下拉候选**,
|
|
2
|
+
// 输入同时过滤候选;回车确认一个(可以是候选里没有的自定义值),点候选也加一个。
|
|
3
|
+
//
|
|
4
|
+
// 已经在名单里的值不再出现在候选里(见 `remaining`);粘贴一串会拆成多个(见 `./tags.ts` 的分隔符口径)。
|
|
5
|
+
|
|
6
|
+
import { useMemo, useState, type ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
IconCloseOutlineRegular,
|
|
9
|
+
Menu,
|
|
10
|
+
MenuItemButton,
|
|
11
|
+
rankByName,
|
|
12
|
+
} from "@deepseek-ai/dsh-client-ui-primitives";
|
|
13
|
+
import { type SearchSelectOption } from "./SearchSelect.tsx";
|
|
14
|
+
import { mergeTags, parseTagList } from "./tags.ts";
|
|
15
|
+
import css from "./TagInput.module.css";
|
|
16
|
+
|
|
17
|
+
export interface TagInputProps {
|
|
18
|
+
// 当前名单。
|
|
19
|
+
value: readonly string[];
|
|
20
|
+
// 整段替换(一次粘贴 / 回车 / 点候选 / 移除就是一次编辑动作)。
|
|
21
|
+
onChange: (next: string[]) => void;
|
|
22
|
+
// 候选(可空):不空时聚焦就下拉;已经在名单里的项不再列出。
|
|
23
|
+
options?: readonly SearchSelectOption[] | undefined;
|
|
24
|
+
// 空框时的提示。
|
|
25
|
+
placeholder: string;
|
|
26
|
+
// 框内输入框的无障碍名。
|
|
27
|
+
label: string;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
// 候选都没匹配上时的那句话。
|
|
30
|
+
noMatchLabel?: string;
|
|
31
|
+
// 一个标签的移除按钮名(文案归调用方:它要带标签名)。
|
|
32
|
+
removeLabel: (name: string) => string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function TagInput({
|
|
36
|
+
value,
|
|
37
|
+
onChange,
|
|
38
|
+
options,
|
|
39
|
+
placeholder,
|
|
40
|
+
label,
|
|
41
|
+
disabled = false,
|
|
42
|
+
noMatchLabel = "",
|
|
43
|
+
removeLabel,
|
|
44
|
+
}: TagInputProps): ReactNode {
|
|
45
|
+
const [draft, setDraft] = useState("");
|
|
46
|
+
const [open, setOpen] = useState(false);
|
|
47
|
+
const commit = (text: string): void => {
|
|
48
|
+
const names = parseTagList(text);
|
|
49
|
+
if (names.length === 0) return;
|
|
50
|
+
onChange(mergeTags(value, names));
|
|
51
|
+
setDraft("");
|
|
52
|
+
};
|
|
53
|
+
// 没加进去的那些才出现在候选里(加过的、以及手输过的自定义值都不再重复出现)。
|
|
54
|
+
const remaining = useMemo(
|
|
55
|
+
() =>
|
|
56
|
+
(options ?? [])
|
|
57
|
+
.filter((option) => !value.includes(option.value))
|
|
58
|
+
.map((option) => ({
|
|
59
|
+
value: option.value,
|
|
60
|
+
name: option.label ?? option.value,
|
|
61
|
+
label: option.value,
|
|
62
|
+
})),
|
|
63
|
+
[options, value],
|
|
64
|
+
);
|
|
65
|
+
const matches = useMemo(() => rankByName(remaining, draft), [remaining, draft]);
|
|
66
|
+
return (
|
|
67
|
+
<div className={css.row} data-role="tag-input" data-control="tags">
|
|
68
|
+
<Menu
|
|
69
|
+
open={open && remaining.length > 0}
|
|
70
|
+
// 官方那份 wrapper 是 `inline-flex`:不撑满的话框只有内容那么宽(见 `TagInput.module.css` 的 `.anchor`)。
|
|
71
|
+
className={css.anchor}
|
|
72
|
+
anchor={
|
|
73
|
+
<div className={css.box}>
|
|
74
|
+
{value.map((name) => (
|
|
75
|
+
// 标签与里面的移除按钮是同一个整体:× 在框内,点它才移除。
|
|
76
|
+
<span key={name} className={css.chip} data-tag={name}>
|
|
77
|
+
{name}
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
className={css.chipRemove}
|
|
81
|
+
data-action="remove-tag"
|
|
82
|
+
disabled={disabled}
|
|
83
|
+
aria-label={removeLabel(name)}
|
|
84
|
+
onClick={() => {
|
|
85
|
+
onChange(value.filter((candidate) => candidate !== name));
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<IconCloseOutlineRegular size={10} />
|
|
89
|
+
</button>
|
|
90
|
+
</span>
|
|
91
|
+
))}
|
|
92
|
+
{/* 裸输入:边框与背景归外面那个框,标签与它一起换行。 */}
|
|
93
|
+
<input
|
|
94
|
+
className={css.input}
|
|
95
|
+
value={draft}
|
|
96
|
+
placeholder={value.length === 0 ? placeholder : ""}
|
|
97
|
+
aria-label={label}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
onFocus={() => {
|
|
100
|
+
setOpen(true);
|
|
101
|
+
}}
|
|
102
|
+
onBlur={() => {
|
|
103
|
+
setOpen(false);
|
|
104
|
+
}}
|
|
105
|
+
onChange={(event) => {
|
|
106
|
+
setDraft(event.currentTarget.value);
|
|
107
|
+
}}
|
|
108
|
+
onKeyDown={(event) => {
|
|
109
|
+
if (event.key !== "Enter") return;
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
commit(draft);
|
|
112
|
+
}}
|
|
113
|
+
onPaste={(event) => {
|
|
114
|
+
// 只有真的一串才拦:单个名字照旧走普通输入(粘进去还能接着改)。
|
|
115
|
+
const text = event.clipboardData.getData("text");
|
|
116
|
+
if (parseTagList(text).length < 2) return;
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
commit(text);
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
}
|
|
123
|
+
onClose={() => {
|
|
124
|
+
setOpen(false);
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
{matches.map((candidate) => (
|
|
128
|
+
// 按下就保持输入框焦点(否则 blur 先把下拉收起来,点不到这一行),选中后继续留在框里加下一个。
|
|
129
|
+
<span
|
|
130
|
+
key={candidate.value}
|
|
131
|
+
onMouseDown={(event) => {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<MenuItemButton
|
|
136
|
+
onSelect={() => {
|
|
137
|
+
commit(candidate.value);
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
{candidate.name}
|
|
141
|
+
</MenuItemButton>
|
|
142
|
+
</span>
|
|
143
|
+
))}
|
|
144
|
+
{matches.length === 0 ? (
|
|
145
|
+
<p className={css.empty} data-role="tag-input-empty">
|
|
146
|
+
{noMatchLabel}
|
|
147
|
+
</p>
|
|
148
|
+
) : null}
|
|
149
|
+
</Menu>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* 悬停显形:所在行带 `data-time-hover-root` 时,指针悬停或焦点落在行内都露出来。
|
|
2
|
+
`:global()` 是因为这个选择器要读到行上的属性——它不属于本组件自己。 */
|
|
3
|
+
.label {
|
|
4
|
+
padding: 0;
|
|
5
|
+
font-size: 14px;
|
|
6
|
+
line-height: 24px;
|
|
7
|
+
color: var(--dsw-alias-label-tertiary);
|
|
8
|
+
white-space: nowrap;
|
|
9
|
+
opacity: 0;
|
|
10
|
+
transition: opacity 80ms ease;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
:global([data-time-hover-root]:hover) .label,
|
|
14
|
+
:global([data-time-hover-root]:focus-within) .label {
|
|
15
|
+
opacity: 1;
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// 对话里的时间标签:平时透明,指针悬停或用键盘进入所在那一行(`[data-time-hover-root]`)才显形。
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { classes } from "./classes.ts";
|
|
5
|
+
import css from "./TimeLabel.module.css";
|
|
6
|
+
|
|
7
|
+
export type TimeLabelProps = HTMLAttributes<HTMLSpanElement>;
|
|
8
|
+
|
|
9
|
+
export function TimeLabel({ className, children, ...rest }: TimeLabelProps): ReactNode {
|
|
10
|
+
return (
|
|
11
|
+
<span className={classes(css.label, className)} {...rest}>
|
|
12
|
+
{children}
|
|
13
|
+
</span>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// 无障碍隐藏文本:给屏幕阅读器的一句话,视觉上不占位(图标按钮旁边的状态说明就用它)。
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import css from "./VisuallyHidden.module.css";
|
|
5
|
+
|
|
6
|
+
export interface VisuallyHiddenProps extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function VisuallyHidden({ children, ...rest }: VisuallyHiddenProps): ReactNode {
|
|
11
|
+
return (
|
|
12
|
+
<span className={css.hidden} {...rest}>
|
|
13
|
+
{children}
|
|
14
|
+
</span>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// 拼 class 名:CSS Modules 的映射查表给 `string | undefined`(`exactOptionalPropertyTypes` 下没命中的键是
|
|
2
|
+
// undefined),一个没命中的类名不该在 DOM 上留一个空串。
|
|
3
|
+
export function classes(...names: readonly (string | false | null | undefined)[]): string {
|
|
4
|
+
return names.filter((name): name is string => typeof name === "string" && name !== "").join(" ");
|
|
5
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// 设置面通用控件:按钮、图标按钮、选择器、标签输入、多行文本。
|
|
2
|
+
//
|
|
3
|
+
// 官方那套基础组件(`Menu` / `Modal` / `Switch` / …)由本包统一 re-export(见 `../index.ts`),业务包只从这里取控件。
|
|
4
|
+
// 两类"带候选"的控件共用同一套「搜索 + 候选」交互:
|
|
5
|
+
// - **multi-input**(`TagInput`):多值 + 自由输入,聚焦即整体下拉候选、输入即过滤,候选只是加速;
|
|
6
|
+
// - **searchable**(`SearchSelect`):单值,从候选里搜着选。
|
|
7
|
+
|
|
8
|
+
export { Bubble, type BubbleProps } from "./Bubble.tsx";
|
|
9
|
+
export { Button, type ButtonProps, type ButtonTone } from "./Button.tsx";
|
|
10
|
+
export { DisclosureRow, type DisclosureRowProps } from "./DisclosureRow.tsx";
|
|
11
|
+
export { SeatButton, type SeatButtonProps } from "./SeatButton.tsx";
|
|
12
|
+
export { Spinner, type SpinnerProps } from "./Spinner.tsx";
|
|
13
|
+
export { TimeLabel, type TimeLabelProps } from "./TimeLabel.tsx";
|
|
14
|
+
export { VisuallyHidden, type VisuallyHiddenProps } from "./VisuallyHidden.tsx";
|
|
15
|
+
export { IconButton, type IconButtonProps } from "./IconButton.tsx";
|
|
16
|
+
export {
|
|
17
|
+
ModelRouteList,
|
|
18
|
+
type ModelRouteCandidate,
|
|
19
|
+
type ModelRouteListProps,
|
|
20
|
+
} from "./ModelRouteList.tsx";
|
|
21
|
+
export { MultilineField, type MultilineFieldProps } from "./MultilineField.tsx";
|
|
22
|
+
export { SearchSelect, type SearchSelectOption, type SearchSelectProps } from "./SearchSelect.tsx";
|
|
23
|
+
export { TagInput, type TagInputProps } from "./TagInput.tsx";
|
|
24
|
+
export { mergeTags, parseTagList } from "./tags.ts";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// 名单类字段的文本解析:把粘贴或输入的一串名字折成标签列表。
|
|
2
|
+
|
|
3
|
+
// 分隔符:中英逗号、分号、制表符与换行;去空白、去重、保序。
|
|
4
|
+
export function parseTagList(text: string): string[] {
|
|
5
|
+
const seen = new Set<string>();
|
|
6
|
+
const names: string[] = [];
|
|
7
|
+
for (const raw of text.split(/[,,;;\t\n\r]+/u)) {
|
|
8
|
+
const name = raw.trim();
|
|
9
|
+
if (name === "" || seen.has(name)) continue;
|
|
10
|
+
seen.add(name);
|
|
11
|
+
names.push(name);
|
|
12
|
+
}
|
|
13
|
+
return names;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 把新名字并进现有名单:去重、保序(现有的在前),返回新数组(一次替换就是一次编辑动作)。
|
|
17
|
+
export function mergeTags(current: readonly string[], incoming: readonly string[]): string[] {
|
|
18
|
+
const merged = [...current];
|
|
19
|
+
for (const name of incoming) {
|
|
20
|
+
if (name === "" || merged.includes(name)) continue;
|
|
21
|
+
merged.push(name);
|
|
22
|
+
}
|
|
23
|
+
return merged;
|
|
24
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import type { Context } from "@deepseek-ai/cordis";
|
|
2
2
|
import * as schemaForm from "./schema-form/index.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// 官方那套基础组件(`Button` / `Menu` / `Input` / `Tag` / `Modal` / `Switch` / `SegmentedControl` /
|
|
5
|
+
// `SettingsForm` 与两个字段控件 / `DisclosureRow` / 图标 …)统一从这里转出:业务包只向本包取控件,不必各自去认
|
|
6
|
+
// 官方包名。官方那份**直接用**(同形同名、样式本来就是上游的 CSS Modules),本包只补官方没有的那几件。
|
|
7
|
+
export * from "@deepseek-ai/dsh-client-ui-primitives";
|
|
8
|
+
|
|
6
9
|
export * from "./markdown-labels.ts";
|
|
10
|
+
export * from "./layout/index.ts";
|
|
7
11
|
export * from "./settings-form/index.ts";
|
|
12
|
+
export * from "./controls/index.ts";
|
|
13
|
+
// `Button` 与 `DisclosureRow` 是本包包装版(官方那份 + 文字不换行 / 危险档 / 两条固定的布局类):
|
|
14
|
+
// 两个星号导出之间必须显式点名,否则判成歧义。
|
|
15
|
+
export {
|
|
16
|
+
Button,
|
|
17
|
+
DisclosureRow,
|
|
18
|
+
type ButtonProps,
|
|
19
|
+
type ButtonTone,
|
|
20
|
+
type DisclosureRowProps,
|
|
21
|
+
} from "./controls/index.ts";
|
|
8
22
|
// 按行 schema 自动生成的行配置页(原 `@morlay/dsh-client-ui-schema-form/client`,2026-09-28 合并进来):
|
|
9
23
|
// 两者都是"对话 UI / 设置页的基础面",拆成两行会各自注册一遍、还要求每个 bundle 都插齐两行。
|
|
10
24
|
// 只**具名导出**它那一面的门面:`export *` 会与 settings-form 的样式 / token 重名。
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// `Row`:横向行 + 间距档。标题行的「左列文本 + 右侧控件」(`justify="between"`)、按钮排、标签排都用它。
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type ElementType, type HTMLAttributes, type ReactNode } from "react";
|
|
4
|
+
import { classes } from "../controls/classes.ts";
|
|
5
|
+
import css from "./layout.module.css";
|
|
6
|
+
import {
|
|
7
|
+
alignClass,
|
|
8
|
+
gapClass,
|
|
9
|
+
justifyClass,
|
|
10
|
+
padClass,
|
|
11
|
+
type LayoutAlign,
|
|
12
|
+
type LayoutGap,
|
|
13
|
+
type LayoutJustify,
|
|
14
|
+
type LayoutPad,
|
|
15
|
+
} from "./scales.ts";
|
|
16
|
+
|
|
17
|
+
export interface RowProps extends HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
// 渲染成哪个元素(行内摘要用 `span`)。
|
|
19
|
+
as?: ElementType;
|
|
20
|
+
// 子项之间的横向间距。
|
|
21
|
+
gap?: LayoutGap;
|
|
22
|
+
// 交叉轴对齐;缺省 `stretch`。
|
|
23
|
+
align?: LayoutAlign;
|
|
24
|
+
// 主轴分布;缺省从起点排。
|
|
25
|
+
justify?: LayoutJustify;
|
|
26
|
+
// 放不下就换行。
|
|
27
|
+
wrap?: boolean;
|
|
28
|
+
// 撑开:吃掉剩下的宽度并允许收缩。
|
|
29
|
+
grow?: boolean;
|
|
30
|
+
// 卡片底座(边框 + 圆角 + 底色)。
|
|
31
|
+
boxed?: boolean;
|
|
32
|
+
// 不被压缩(页头、动作排这类固定项)。
|
|
33
|
+
fixed?: boolean;
|
|
34
|
+
// 窄列宽度(对话消息那一列)。
|
|
35
|
+
narrow?: boolean;
|
|
36
|
+
// 贴右:把这一项推到行尾。
|
|
37
|
+
push?: boolean;
|
|
38
|
+
// 列表(配 `as="ul"`):去掉列表标记与内距。
|
|
39
|
+
plain?: boolean;
|
|
40
|
+
// 内距档;缺省不留内距。
|
|
41
|
+
pad?: LayoutPad;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Row = forwardRef<HTMLElement, RowProps>(function Row(
|
|
45
|
+
{
|
|
46
|
+
as: Element = "div",
|
|
47
|
+
gap,
|
|
48
|
+
align,
|
|
49
|
+
justify,
|
|
50
|
+
wrap = false,
|
|
51
|
+
grow = false,
|
|
52
|
+
boxed = false,
|
|
53
|
+
fixed = false,
|
|
54
|
+
narrow = false,
|
|
55
|
+
push = false,
|
|
56
|
+
plain = false,
|
|
57
|
+
pad,
|
|
58
|
+
className,
|
|
59
|
+
children,
|
|
60
|
+
...rest
|
|
61
|
+
},
|
|
62
|
+
ref,
|
|
63
|
+
): ReactNode {
|
|
64
|
+
return (
|
|
65
|
+
<Element
|
|
66
|
+
ref={ref}
|
|
67
|
+
className={classes(
|
|
68
|
+
css.row,
|
|
69
|
+
gapClass(css, gap),
|
|
70
|
+
alignClass(css, align),
|
|
71
|
+
justifyClass(css, justify),
|
|
72
|
+
padClass(css, pad),
|
|
73
|
+
wrap ? css.wrap : undefined,
|
|
74
|
+
grow ? css.grow : undefined,
|
|
75
|
+
boxed ? css.boxed : undefined,
|
|
76
|
+
fixed ? css.fixed : undefined,
|
|
77
|
+
narrow ? css.narrow : undefined,
|
|
78
|
+
push ? css.push : undefined,
|
|
79
|
+
boxed ? css.boxed : undefined,
|
|
80
|
+
fixed ? css.fixed : undefined,
|
|
81
|
+
plain ? css.plain : undefined,
|
|
82
|
+
className,
|
|
83
|
+
)}
|
|
84
|
+
{...rest}
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
</Element>
|
|
88
|
+
);
|
|
89
|
+
});
|