@plumeai/plugin-sdk 0.2.1 → 0.3.0
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/dist/client/ui/src/avatar/AgentAvatar.d.ts +2 -5
- package/dist/client/ui/src/i18n/messages/en.d.ts +3 -0
- package/dist/client/ui/src/i18n/messages/zh-CN.d.ts +3 -0
- package/dist/client/ui/src/image/AnnotateToolbar.d.ts +2 -4
- package/dist/client/ui/src/image/Annotator.d.ts +3 -2
- package/dist/client/ui/src/image/screenshot-geometry.d.ts +2 -2
- package/dist/client/ui/src/inputs/SearchField.d.ts +1 -1
- package/dist/client/ui/src/inputs/pickers.d.ts +1 -1
- package/dist/client/ui/src/layout/PageTopRow.d.ts +5 -6
- package/dist/client/ui/src/motion/RollingText.d.ts +1 -1
- package/dist/client/ui/src/motion/loaders.d.ts +4 -4
- package/dist/client/ui/src/overlay/Confirm.d.ts +1 -2
- package/dist/client/ui/src/overlay/ContextMenu.d.ts +2 -2
- package/dist/client/ui/src/overlay/Dialog.d.ts +5 -6
- package/dist/client/ui/src/overlay/Menu.d.ts +3 -4
- package/dist/client/ui/src/overlay/Popover.d.ts +1 -1
- package/dist/client/ui/src/overlay/Tooltip.d.ts +1 -1
- package/dist/client/ui/src/primitives/IconButton.d.ts +2 -2
- package/dist/client/ui/src/primitives/Text.d.ts +2 -2
- package/dist/client/ui/src/scroll/ScrollText.d.ts +1 -1
- package/dist/client/ui/src/scroll/Scroller.d.ts +2 -1
- package/dist/contract/src/diagnostics.d.ts +39 -1
- package/dist/contract/src/service.d.ts +13 -0
- package/dist/contract/src/ui.d.ts +72 -68
- package/package.json +1 -1
- package/skill/SKILL.md +86 -79
- package/skill/references/components.md +233 -1418
- package/skill/references/logic.md +240 -75
- package/skill/references/style-i18n.md +16 -12
- package/skill/references/ui.md +170 -123
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- 由 `node scripts/check-plugins.mjs --fix` 从源码生成,不要手改。名单在 packages/client/ui/plugin-api.json。 -->
|
|
4
4
|
|
|
5
|
-
界面入口能用的 `@plume/ui` 组件和 `@plume/styles` 类名组合,就是这一页列出的这些;`@plume/ui` 的其他导出是产品内部用的,`pnpm check:plugins`
|
|
5
|
+
界面入口能用的 `@plume/ui` 组件和 `@plume/styles` 类名组合,就是这一页列出的这些;`@plume/ui` 的其他导出是产品内部用的,`pnpm check:plugins` 会拦。签名和注释摘自源码:解构的参数只留有默认值的(其余的看后面的类型),`keyof typeof` 的常量只列键,注释只取第一段。
|
|
6
6
|
|
|
7
7
|
怎么选:文字一律用 `Text`(字号档位自带行高、字距和字重,不在 CSS 里写字号);按钮、输入框、开关、菜单、对话框用对应组件;工具卡片的外框用 `Card`。组件不够时用下面的类名组合,再不够才写自己的样式(见 [style-i18n.md](style-i18n.md))。
|
|
8
8
|
|
|
@@ -10,21 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
### `Text`
|
|
12
12
|
|
|
13
|
-
源码:`packages/client/ui/src/primitives/Text.tsx`
|
|
14
|
-
|
|
15
13
|
```tsx
|
|
16
14
|
/** 一段文字:`size` 选字号档位(每档带着自己的行高、字距和默认字重),`tone` 选颜色。 */
|
|
17
|
-
export function Text({
|
|
18
|
-
size = "label",
|
|
19
|
-
tone = "default",
|
|
20
|
-
weight,
|
|
21
|
-
as = "span",
|
|
22
|
-
mono = false,
|
|
23
|
-
numeric = false,
|
|
24
|
-
className = "",
|
|
25
|
-
children,
|
|
26
|
-
...rest
|
|
27
|
-
}: {
|
|
15
|
+
export function Text({ size = "label", tone = "default", as = "span", mono = false, numeric = false, className = "", ...rest }: {
|
|
28
16
|
size?: keyof typeof SIZES;
|
|
29
17
|
tone?: keyof typeof TONES;
|
|
30
18
|
/** 覆盖这一档平时的字重。 */
|
|
@@ -33,11 +21,7 @@ export function Text({
|
|
|
33
21
|
as?: "span" | "p" | "div" | "h1" | "h2" | "h3" | "label" | "code";
|
|
34
22
|
/** 等宽字体,给正文里引用的标识符和路径。 */
|
|
35
23
|
mono?: boolean;
|
|
36
|
-
/**
|
|
37
|
-
* 等宽数字。
|
|
38
|
-
*
|
|
39
|
-
* 给原地变化的数——token 数、计时、diff 统计。比例数字每跳一下字就左右挪,读起来是闪烁。
|
|
40
|
-
*/
|
|
24
|
+
/** 等宽数字,给原地变化的数——token 数、计时、diff 统计。 */
|
|
41
25
|
numeric?: boolean;
|
|
42
26
|
className?: string;
|
|
43
27
|
children?: React.ReactNode;
|
|
@@ -45,33 +29,33 @@ export function Text({
|
|
|
45
29
|
|
|
46
30
|
const SIZES = {
|
|
47
31
|
/** 页面标题——一屏一个。 */
|
|
48
|
-
display:
|
|
32
|
+
display: …,
|
|
49
33
|
/** 一屏里大分区的标题。 */
|
|
50
|
-
heading:
|
|
34
|
+
heading: …,
|
|
51
35
|
/** 小节和卡片的标题。 */
|
|
52
|
-
title:
|
|
36
|
+
title: …,
|
|
53
37
|
/** 正文:消息、描述,一句一句读的东西。 */
|
|
54
|
-
body:
|
|
38
|
+
body: …,
|
|
55
39
|
/** 控件、行、表格单元——界面的工作字号。 */
|
|
56
|
-
label:
|
|
40
|
+
label: …,
|
|
57
41
|
/** 标签下面的次要细节。 */
|
|
58
|
-
detail:
|
|
42
|
+
detail: …,
|
|
59
43
|
/** 时间戳、计数、角标。 */
|
|
60
|
-
caption:
|
|
44
|
+
caption: …,
|
|
61
45
|
} as const;
|
|
62
46
|
|
|
63
47
|
const TONES = {
|
|
64
|
-
default:
|
|
65
|
-
muted:
|
|
66
|
-
faint:
|
|
67
|
-
danger:
|
|
68
|
-
accent:
|
|
48
|
+
default: …,
|
|
49
|
+
muted: …,
|
|
50
|
+
faint: …,
|
|
51
|
+
danger: …,
|
|
52
|
+
accent: …,
|
|
69
53
|
} as const;
|
|
70
54
|
|
|
71
55
|
const WEIGHTS = {
|
|
72
|
-
normal:
|
|
73
|
-
medium:
|
|
74
|
-
semibold:
|
|
56
|
+
normal: …,
|
|
57
|
+
medium: …,
|
|
58
|
+
semibold: …,
|
|
75
59
|
} as const;
|
|
76
60
|
```
|
|
77
61
|
|
|
@@ -79,44 +63,16 @@ const WEIGHTS = {
|
|
|
79
63
|
|
|
80
64
|
### `Button`
|
|
81
65
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
export function Button({
|
|
86
|
-
children,
|
|
87
|
-
icon,
|
|
88
|
-
onClick,
|
|
89
|
-
variant = "ghost",
|
|
90
|
-
size = "md",
|
|
91
|
-
disabled,
|
|
92
|
-
loading,
|
|
93
|
-
label,
|
|
94
|
-
ariaLabel,
|
|
95
|
-
menu,
|
|
96
|
-
expanded,
|
|
97
|
-
pressed,
|
|
98
|
-
className = "",
|
|
99
|
-
type = "button",
|
|
100
|
-
...data
|
|
101
|
-
}: DataAttributes & {
|
|
102
|
-
/**
|
|
103
|
-
* 只有图标的按钮不传。
|
|
104
|
-
*
|
|
105
|
-
* 图标旁边再写一个字是把同一件事说两遍,一排这样的按钮会把工具栏变成一句话。图标说得清意思的地方,
|
|
106
|
-
* `label` 同时作悬停提示和无障碍名——读屏的人什么都没丢,丢的只是眼睛看到的那几个字。
|
|
107
|
-
*/
|
|
66
|
+
```tsx
|
|
67
|
+
export function Button({ variant = "ghost", size = "md", className = "", type = "button", ...data }: DataAttributes & {
|
|
68
|
+
/** 只有图标的按钮不传。 */
|
|
108
69
|
children?: ReactNode;
|
|
109
70
|
icon?: ReactNode;
|
|
110
71
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
111
72
|
variant?: ButtonVariant;
|
|
112
73
|
size?: ButtonSize;
|
|
113
74
|
disabled?: boolean;
|
|
114
|
-
/**
|
|
115
|
-
* 忙碌。按钮不能按、变淡,宽度不变。
|
|
116
|
-
*
|
|
117
|
-
* 每个异步操作各自做这件事时,做法都略有不同——有的把文字换成转圈,整行跟着跳;有的按钮还能按,
|
|
118
|
-
* 能被按两次。宽度不变是因为:一个看着它的时候在缩的按钮,回头找的时候可能就找不到了。
|
|
119
|
-
*/
|
|
75
|
+
/** 忙碌。按钮不能按、变淡,宽度不变。 */
|
|
120
76
|
loading?: boolean;
|
|
121
77
|
/** 悬停提示;没有可见文字时也是无障碍名。 */
|
|
122
78
|
label?: string;
|
|
@@ -141,66 +97,22 @@ export type ButtonSize = "md" | "sm" | "xs";
|
|
|
141
97
|
|
|
142
98
|
### `IconButton`
|
|
143
99
|
|
|
144
|
-
源码:`packages/client/ui/src/primitives/IconButton.tsx`
|
|
145
|
-
|
|
146
100
|
```tsx
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
* 各个工具栏各写各的,会长出五种高度、三种圆角,按下的反馈从没有到缩小 10% 都有。缩小是有意去掉的——
|
|
151
|
-
* 一个在接触那一刻从指针底下跳开的控件读起来不稳,在这么小的尺寸上那是一下晃动而不是按下。按下用底色
|
|
152
|
-
* 深一档表示,真的按钮就是这样:它待在原地,只是换了状态。
|
|
153
|
-
*
|
|
154
|
-
* 标签同时是悬停提示和无障碍名,调用方没法只给一个——只有图标的工具栏就是这样变得读屏读不出来的。
|
|
155
|
-
* 类名组合在 `@plume/styles` 的 `iconButton`。
|
|
156
|
-
*/
|
|
157
|
-
export function IconButton({
|
|
158
|
-
label,
|
|
159
|
-
ariaLabel,
|
|
160
|
-
icon,
|
|
161
|
-
onClick,
|
|
162
|
-
active,
|
|
163
|
-
disabled,
|
|
164
|
-
emphasis,
|
|
165
|
-
badge,
|
|
166
|
-
explainDisabled,
|
|
167
|
-
tone = "default",
|
|
168
|
-
size = "md",
|
|
169
|
-
tipSide = "bottom",
|
|
170
|
-
menu,
|
|
171
|
-
expanded,
|
|
172
|
-
className = "",
|
|
173
|
-
...data
|
|
174
|
-
}: DataAttributes & {
|
|
101
|
+
/** 标签是一个图标的按钮。`label` 同时是悬停提示和无障碍名。 */
|
|
102
|
+
export function IconButton({ tone = "default", size = "md", tipSide = "bottom", className = "", ...data }: DataAttributes & {
|
|
175
103
|
label: string;
|
|
176
|
-
/**
|
|
177
|
-
* 无障碍名要比悬停提示多说一点时用。
|
|
178
|
-
*
|
|
179
|
-
* 一列同样的按钮在读屏里是同一个词念了一遍又一遍;无障碍名带上是哪一行(`归档会话「a」`),
|
|
180
|
-
* 悬停提示仍然短,因为指针已经在那一行上了。不传时 `label` 两样都是,这是通常的情况。
|
|
181
|
-
*/
|
|
104
|
+
/** 无障碍名要比悬停提示多说一点时用。 */
|
|
182
105
|
ariaLabel?: string;
|
|
183
106
|
icon: React.ReactNode;
|
|
184
107
|
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
185
108
|
/** 按住 / 开着,给「区分大小写」这类开关。 */
|
|
186
109
|
active?: boolean;
|
|
187
110
|
disabled?: boolean;
|
|
188
|
-
/**
|
|
189
|
-
* 一排里现在值得按的那一个。
|
|
190
|
-
*
|
|
191
|
-
* 满色的字,对比一排平时淡灰的字,这是界面里已经用来表示「就是这个」的对比。不是实心或彩色按钮:
|
|
192
|
-
* 长出 primary 按钮的工具栏就不再是工具栏了。
|
|
193
|
-
*/
|
|
111
|
+
/** 一排里现在值得按的那一个。 */
|
|
194
112
|
emphasis?: boolean;
|
|
195
113
|
/** 角上的计数。零和 null 都画成什么都没有。 */
|
|
196
114
|
badge?: number | null;
|
|
197
|
-
/**
|
|
198
|
-
* 禁用时也显示悬停提示,因为标签说明的正是为什么禁用。
|
|
199
|
-
*
|
|
200
|
-
* 默认关,对大多数工具栏默认是对的:一个因为显而易见的原因变灰的控件,再冒一个泡重复它的名字
|
|
201
|
-
* 没有意义。原因本身才是重点的地方就不对了——「当前分支没有上游」正是悬停在一个按不动的拉取按钮上
|
|
202
|
-
* 的人想知道的,没有这个他们只能得到一片沉默,自己去猜。
|
|
203
|
-
*/
|
|
115
|
+
/** 禁用时也显示悬停提示,因为标签说明的正是为什么禁用。 */
|
|
204
116
|
explainDisabled?: boolean;
|
|
205
117
|
tone?: "default" | "danger";
|
|
206
118
|
/**
|
|
@@ -221,8 +133,6 @@ export function IconButton({
|
|
|
221
133
|
|
|
222
134
|
### `Input`
|
|
223
135
|
|
|
224
|
-
源码:`packages/client/ui/src/inputs/NativeField.tsx`
|
|
225
|
-
|
|
226
136
|
```tsx
|
|
227
137
|
/** 不带样式的原生控件:布局、ref、选区和撤销历史都留在调用方手里。 */
|
|
228
138
|
export function Input(props: ComponentPropsWithRef<"input">)
|
|
@@ -230,8 +140,6 @@ export function Input(props: ComponentPropsWithRef<"input">)
|
|
|
230
140
|
|
|
231
141
|
### `Textarea`
|
|
232
142
|
|
|
233
|
-
源码:`packages/client/ui/src/inputs/NativeField.tsx`
|
|
234
|
-
|
|
235
143
|
```tsx
|
|
236
144
|
/** 同 `Input`,多行。要带框、滚动条和渐隐的多行框用 `TextArea`。 */
|
|
237
145
|
export function Textarea(props: ComponentPropsWithRef<"textarea">)
|
|
@@ -239,35 +147,9 @@ export function Textarea(props: ComponentPropsWithRef<"textarea">)
|
|
|
239
147
|
|
|
240
148
|
### `TextArea`
|
|
241
149
|
|
|
242
|
-
源码:`packages/client/ui/src/inputs/TextArea.tsx`
|
|
243
|
-
|
|
244
150
|
```tsx
|
|
245
|
-
/**
|
|
246
|
-
|
|
247
|
-
*
|
|
248
|
-
* 各处的多行输入框各写各的类名串,会长出好几种圆角、内衬和底色——而它们要人做的是同一件事。
|
|
249
|
-
* 样子由 `.plume-textarea-shell` / `.plume-textarea` 出(见 `@plume/styles` 的 `ui/controls.css`),
|
|
250
|
-
* 调用方只说高度和位置。
|
|
251
|
-
*
|
|
252
|
-
* 三样东西是一起来的,缺一件那个框就回到从前:
|
|
253
|
-
*
|
|
254
|
-
* - **边界在外壳上**。虚化收的是元素画出的一切,描边和字同在一个元素上,滚到顶时框的上下边会跟
|
|
255
|
-
* 着字一起淡掉,像破了个口子。
|
|
256
|
-
* - **滑块是自己画的**。原生滚动条在全局被关掉了(见 `OverlayScrollbar` 的开头),不补一个,一
|
|
257
|
-
* 篇长草稿滚起来没有任何东西说它有多长。
|
|
258
|
-
* - **两头化开**。硬切的半行字读起来像渲染坏了,见 `useFieldFade`。
|
|
259
|
-
*
|
|
260
|
-
* 主输入框、消息编辑框这类框里还站着别的东西(工具栏、附件条、那排按钮)的不走这里,外壳是各自的。
|
|
261
|
-
* 共享的是同一个 `useFieldFade` 和同一条遮罩。
|
|
262
|
-
*/
|
|
263
|
-
export function TextArea({
|
|
264
|
-
value,
|
|
265
|
-
onChange,
|
|
266
|
-
shell = "",
|
|
267
|
-
className = "",
|
|
268
|
-
resizable = false,
|
|
269
|
-
...rest
|
|
270
|
-
}: {
|
|
151
|
+
/** 写很多字的地方,一种写法。 */
|
|
152
|
+
export function TextArea({ shell = "", className = "", resizable = false, ...rest }: {
|
|
271
153
|
value: string;
|
|
272
154
|
onChange: (value: string) => void;
|
|
273
155
|
/** 外壳上的额外类。高度、外边距、宽度——和这个框在页面上待在哪儿有关的事,调用方说了算。 */
|
|
@@ -281,8 +163,6 @@ export function TextArea({
|
|
|
281
163
|
|
|
282
164
|
### `Select`
|
|
283
165
|
|
|
284
|
-
源码:`packages/client/ui/src/inputs/Select.tsx`
|
|
285
|
-
|
|
286
166
|
```tsx
|
|
287
167
|
export function Select<T extends string>(props: {
|
|
288
168
|
value: T;
|
|
@@ -294,8 +174,6 @@ export function Select<T extends string>(props: {
|
|
|
294
174
|
|
|
295
175
|
### `InlineSelect`
|
|
296
176
|
|
|
297
|
-
源码:`packages/client/ui/src/inputs/Select.tsx`
|
|
298
|
-
|
|
299
177
|
```tsx
|
|
300
178
|
/** 设置行里用的紧凑行内下拉,像「Zed ˅」那样只有字和箭头。 */
|
|
301
179
|
export function InlineSelect<T extends string>(props: {
|
|
@@ -312,15 +190,8 @@ export function InlineSelect<T extends string>(props: {
|
|
|
312
190
|
|
|
313
191
|
### `Toggle`
|
|
314
192
|
|
|
315
|
-
源码:`packages/client/ui/src/inputs/Toggle.tsx`
|
|
316
|
-
|
|
317
193
|
```tsx
|
|
318
|
-
export function Toggle({
|
|
319
|
-
checked,
|
|
320
|
-
onChange,
|
|
321
|
-
ariaLabel,
|
|
322
|
-
disabled,
|
|
323
|
-
}: {
|
|
194
|
+
export function Toggle(props: {
|
|
324
195
|
checked: boolean;
|
|
325
196
|
onChange: (checked: boolean) => void;
|
|
326
197
|
ariaLabel?: string;
|
|
@@ -330,19 +201,8 @@ export function Toggle({
|
|
|
330
201
|
|
|
331
202
|
### `ComposerToggle`
|
|
332
203
|
|
|
333
|
-
源码:`packages/client/ui/src/inputs/ComposerToggle.tsx`
|
|
334
|
-
|
|
335
204
|
```tsx
|
|
336
|
-
export function ComposerToggle({
|
|
337
|
-
on,
|
|
338
|
-
onChange,
|
|
339
|
-
icon,
|
|
340
|
-
label,
|
|
341
|
-
tip,
|
|
342
|
-
size = "composer",
|
|
343
|
-
iconOnly = false,
|
|
344
|
-
...data
|
|
345
|
-
}: {
|
|
205
|
+
export function ComposerToggle({ size = "composer", iconOnly = false, ...data }: {
|
|
346
206
|
on: boolean;
|
|
347
207
|
onChange: (on: boolean) => void;
|
|
348
208
|
/** 16px 的图标,颜色跟着文字走。 */
|
|
@@ -363,15 +223,8 @@ export function ComposerToggle({
|
|
|
363
223
|
|
|
364
224
|
### `WorktreeToggle`
|
|
365
225
|
|
|
366
|
-
源码:`packages/client/ui/src/inputs/WorktreeToggle.tsx`
|
|
367
|
-
|
|
368
226
|
```tsx
|
|
369
|
-
export function WorktreeToggle({
|
|
370
|
-
on,
|
|
371
|
-
onChange,
|
|
372
|
-
size = "composer",
|
|
373
|
-
iconOnly = false,
|
|
374
|
-
}: {
|
|
227
|
+
export function WorktreeToggle({ size = "composer", iconOnly = false }: {
|
|
375
228
|
on: boolean;
|
|
376
229
|
onChange: (on: boolean) => void;
|
|
377
230
|
/** `sm` 放在 `Button size="sm"` 旁边,比如建议卡片上:26px,主输入框的控件是 28px。 */
|
|
@@ -383,23 +236,9 @@ export function WorktreeToggle({
|
|
|
383
236
|
|
|
384
237
|
### `Segmented`
|
|
385
238
|
|
|
386
|
-
源码:`packages/client/ui/src/inputs/Segmented.tsx`
|
|
387
|
-
|
|
388
239
|
```tsx
|
|
389
|
-
/**
|
|
390
|
-
|
|
391
|
-
*
|
|
392
|
-
* 一块共用的底从这里滑到那里,滑动本身就说明这几个选项是同一排里的几个位置,不是几个开关。
|
|
393
|
-
* 位置是量出来的,不按等分算:中英混排的标签不一样宽。量在 layout effect 里,赶在这一帧画出来之前——晚一帧的话,
|
|
394
|
-
* 第一次渲染就能看见那块底从左上角飞过来。
|
|
395
|
-
*
|
|
396
|
-
* 圆角外框 8px、滑块和每一格 6px:内衬 2px,两道圆弧才同心。
|
|
397
|
-
*/
|
|
398
|
-
export function Segmented<T extends string>({
|
|
399
|
-
value,
|
|
400
|
-
onChange,
|
|
401
|
-
options,
|
|
402
|
-
}: {
|
|
240
|
+
/** 一排互斥的选项,选中的那个下面垫着一块会滑过去的底。 */
|
|
241
|
+
export function Segmented<T extends string>(props: {
|
|
403
242
|
value: T;
|
|
404
243
|
onChange: (value: T) => void;
|
|
405
244
|
/**
|
|
@@ -412,16 +251,8 @@ export function Segmented<T extends string>({
|
|
|
412
251
|
|
|
413
252
|
### `TabStrip`
|
|
414
253
|
|
|
415
|
-
源码:`packages/client/ui/src/primitives/TabStrip.tsx`
|
|
416
|
-
|
|
417
254
|
```tsx
|
|
418
|
-
export function TabStrip<T extends string>({
|
|
419
|
-
items,
|
|
420
|
-
value,
|
|
421
|
-
onChange,
|
|
422
|
-
label,
|
|
423
|
-
className = "",
|
|
424
|
-
}: {
|
|
255
|
+
export function TabStrip<T extends string>({ className = "" }: {
|
|
425
256
|
items: readonly TabStripItem<T>[];
|
|
426
257
|
value: T;
|
|
427
258
|
onChange: (value: T) => void;
|
|
@@ -441,28 +272,9 @@ export interface TabStripItem<T extends string> {
|
|
|
441
272
|
|
|
442
273
|
### `SearchField`
|
|
443
274
|
|
|
444
|
-
源码:`packages/client/ui/src/inputs/SearchField.tsx`
|
|
445
|
-
|
|
446
275
|
```tsx
|
|
447
|
-
/**
|
|
448
|
-
|
|
449
|
-
*
|
|
450
|
-
* 侧栏的会话搜索、审阅面板的文件筛选、文件树的筛选共用这一个。各写各的话,每个都有自己的高度、
|
|
451
|
-
* 自己的描边、自己对放大镜在框里还是框旁的看法——一个只有一个答案的问题,得到三个答案。
|
|
452
|
-
*
|
|
453
|
-
* 两种尺寸,因为两种场合确实不同:独占一行的框(`comfortable`),和塞进面板标题行、周围的行都很矮
|
|
454
|
-
* 的框(`compact`)。
|
|
455
|
-
*/
|
|
456
|
-
export function SearchField({
|
|
457
|
-
value,
|
|
458
|
-
onChange,
|
|
459
|
-
placeholder,
|
|
460
|
-
size = "compact",
|
|
461
|
-
autoFocus,
|
|
462
|
-
onEscape,
|
|
463
|
-
onEnter,
|
|
464
|
-
className = "",
|
|
465
|
-
}: {
|
|
276
|
+
/** 搜索框。独占一行的给 `size="comfortable"`,塞进面板标题行的用默认的 `compact`。 */
|
|
277
|
+
export function SearchField({ size = "compact", className = "" }: {
|
|
466
278
|
value: string;
|
|
467
279
|
onChange: (next: string) => void;
|
|
468
280
|
placeholder?: string;
|
|
@@ -478,28 +290,9 @@ export function SearchField({
|
|
|
478
290
|
|
|
479
291
|
### `NumberField`
|
|
480
292
|
|
|
481
|
-
源码:`packages/client/ui/src/inputs/pickers.tsx`
|
|
482
|
-
|
|
483
293
|
```tsx
|
|
484
|
-
/**
|
|
485
|
-
|
|
486
|
-
*
|
|
487
|
-
* 用 `type="text"` 加数字键盘提示,不用 `type="number"`:后者的上下箭头每个浏览器画得都不一样,而且它认为不合法的
|
|
488
|
-
* 输入一律报成空字符串,`1e5`、`--3` 看着像收下了,到手却是「什么也没敲」。
|
|
489
|
-
*
|
|
490
|
-
* 存的值不是每敲一下就改:聚焦时框里留一份草稿,下限是 11 时「1」才能留到变成「16」。永远到不了范围内的字符进不来,
|
|
491
|
-
* 加减按钮停在同样的边界上。拒掉的按键不提示。
|
|
492
|
-
*/
|
|
493
|
-
export function NumberField({
|
|
494
|
-
value,
|
|
495
|
-
onChange,
|
|
496
|
-
min = Number.NEGATIVE_INFINITY,
|
|
497
|
-
max = Number.POSITIVE_INFINITY,
|
|
498
|
-
step = 1,
|
|
499
|
-
width = 76,
|
|
500
|
-
label,
|
|
501
|
-
name,
|
|
502
|
-
}: {
|
|
294
|
+
/** 带自己画的步进按钮的数字框。`onChange` 只收到 `min`–`max` 以内的数,敲到一半的草稿留在框里。 */
|
|
295
|
+
export function NumberField({ min = Number.NEGATIVE_INFINITY, max = Number.POSITIVE_INFINITY, step = 1, width = 76 }: {
|
|
503
296
|
value: number;
|
|
504
297
|
onChange: (value: number) => void;
|
|
505
298
|
min?: number;
|
|
@@ -513,22 +306,12 @@ export function NumberField({
|
|
|
513
306
|
|
|
514
307
|
### `Slider`
|
|
515
308
|
|
|
516
|
-
源码:`packages/client/ui/src/inputs/Slider.tsx`
|
|
517
|
-
|
|
518
309
|
```tsx
|
|
519
310
|
/**
|
|
520
311
|
* 轨道和把手自己画的滑块。原生 range 叠在最上面、完全透明,拖动、点轨道跳过去、方向键都是浏览器自己的,
|
|
521
312
|
* 画出来的部分只跟着 value 走。
|
|
522
313
|
*/
|
|
523
|
-
export function Slider({
|
|
524
|
-
value,
|
|
525
|
-
onChange,
|
|
526
|
-
min = 0,
|
|
527
|
-
max = 100,
|
|
528
|
-
step = 1,
|
|
529
|
-
label,
|
|
530
|
-
width = 180,
|
|
531
|
-
}: {
|
|
314
|
+
export function Slider({ min = 0, max = 100, step = 1, width = 180 }: {
|
|
532
315
|
value: number;
|
|
533
316
|
onChange: (value: number) => void;
|
|
534
317
|
min?: number;
|
|
@@ -541,18 +324,9 @@ export function Slider({
|
|
|
541
324
|
|
|
542
325
|
### `ColorField`
|
|
543
326
|
|
|
544
|
-
源码:`packages/client/ui/src/inputs/ColorField.tsx`
|
|
545
|
-
|
|
546
327
|
```tsx
|
|
547
|
-
/**
|
|
548
|
-
|
|
549
|
-
*/
|
|
550
|
-
export function ColorField({
|
|
551
|
-
value,
|
|
552
|
-
onChange,
|
|
553
|
-
label,
|
|
554
|
-
readOnly = false,
|
|
555
|
-
}: {
|
|
328
|
+
/** 那枚色块。里面的输入框是色块的一部分(底色就是它编辑的颜色),不是一个单独的输入框,所以不用 Input 的外观。 */
|
|
329
|
+
export function ColorField({ readOnly = false }: {
|
|
556
330
|
value: string;
|
|
557
331
|
onChange: (value: string) => void;
|
|
558
332
|
label: string;
|
|
@@ -562,22 +336,9 @@ export function ColorField({
|
|
|
562
336
|
|
|
563
337
|
### `TimeField`
|
|
564
338
|
|
|
565
|
-
源码:`packages/client/ui/src/inputs/pickers.tsx`
|
|
566
|
-
|
|
567
339
|
```tsx
|
|
568
|
-
/**
|
|
569
|
-
|
|
570
|
-
*
|
|
571
|
-
* `<input type="time">` 的分段、分隔符和弹出的面板都归平台:macOS 上是步进器,Windows 上是下拉。它还按系统区域解析和
|
|
572
|
-
* 显示,同一个任务在一台机器上读作 `09:00`,在另一台上读作 `9:00 AM`,存的却是同一串字。
|
|
573
|
-
*
|
|
574
|
-
* 两个列表把这些都去掉,顺便去掉了打字:要的只是某个小时的某一分,00:00–23:59 之外没有别的值要表达。
|
|
575
|
-
*/
|
|
576
|
-
export function TimeField({
|
|
577
|
-
value,
|
|
578
|
-
onChange,
|
|
579
|
-
label,
|
|
580
|
-
}: {
|
|
340
|
+
/** 时间,用两个列表挑,不用浏览器的时钟。 */
|
|
341
|
+
export function TimeField(props: {
|
|
581
342
|
/** `HH:mm`,24 小时制。 */
|
|
582
343
|
value: string;
|
|
583
344
|
onChange: (value: string) => void;
|
|
@@ -587,40 +348,22 @@ export function TimeField({
|
|
|
587
348
|
|
|
588
349
|
### `ShortcutRecorder`
|
|
589
350
|
|
|
590
|
-
源码:`packages/client/ui/src/inputs/ShortcutRecorder.tsx`
|
|
591
|
-
|
|
592
351
|
```tsx
|
|
593
|
-
/**
|
|
594
|
-
* 录一组快捷键:点一下进入录制,按下的那组键就是值(Electron accelerator 的写法)。Esc 放弃,⌫ / Delete 清空。
|
|
595
|
-
*/
|
|
352
|
+
/** 录一组快捷键:点一下进入录制,按下的那组键就是值(Electron accelerator 的写法)。Esc 放弃,⌫ / Delete 清空。 */
|
|
596
353
|
export function ShortcutRecorder({ value, onChange, placeholder }: { value?: string; onChange: (shortcut: string) => void; placeholder?: string })
|
|
597
354
|
```
|
|
598
355
|
|
|
599
356
|
### `useFieldFade`
|
|
600
357
|
|
|
601
|
-
源码:`packages/client/ui/src/inputs/useFieldFade.ts`
|
|
602
|
-
|
|
603
358
|
```tsx
|
|
604
359
|
export function useFieldFade(field: React.RefObject<HTMLElement | null>, host: React.RefObject<HTMLElement | null>): void
|
|
605
360
|
```
|
|
606
361
|
|
|
607
362
|
### `ChoiceMark`
|
|
608
363
|
|
|
609
|
-
源码:`packages/client/ui/src/primitives/ChoiceMark.tsx`
|
|
610
|
-
|
|
611
364
|
```tsx
|
|
612
|
-
/**
|
|
613
|
-
|
|
614
|
-
*
|
|
615
|
-
* 原生复选框带着宿主系统的样子:macOS 上是 Aqua,Windows 上是 Fluent,哪个都和周围 ink / accent 的
|
|
616
|
-
* 卡片对不上。一个标记,两种形状。
|
|
617
|
-
*/
|
|
618
|
-
export function ChoiceMark({
|
|
619
|
-
checked,
|
|
620
|
-
kind,
|
|
621
|
-
indeterminate = false,
|
|
622
|
-
className = "",
|
|
623
|
-
}: {
|
|
365
|
+
/** 自己画的选中标记。 */
|
|
366
|
+
export function ChoiceMark({ indeterminate = false, className = "" }: {
|
|
624
367
|
checked: boolean;
|
|
625
368
|
kind: "checkbox" | "radio";
|
|
626
369
|
indeterminate?: boolean;
|
|
@@ -632,113 +375,69 @@ export function ChoiceMark({
|
|
|
632
375
|
|
|
633
376
|
### `Popover`
|
|
634
377
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
```tsx
|
|
638
|
-
export function Popover({
|
|
639
|
-
anchor,
|
|
640
|
-
onClose,
|
|
641
|
-
children,
|
|
642
|
-
placement = "top",
|
|
643
|
-
align = "end",
|
|
644
|
-
width,
|
|
645
|
-
header,
|
|
646
|
-
footer,
|
|
647
|
-
aside,
|
|
648
|
-
maxHeight,
|
|
649
|
-
role = "menu",
|
|
650
|
-
label,
|
|
651
|
-
className = "",
|
|
652
|
-
bodyClassName = "",
|
|
653
|
-
surface: kind = "menu",
|
|
654
|
-
onMouseEnter,
|
|
655
|
-
onMouseLeave,
|
|
656
|
-
}: PopoverProps)
|
|
378
|
+
```tsx
|
|
379
|
+
export function Popover({ placement = "top", align = "end", role = "menu", className = "", bodyClassName = "", surface = "menu" }: PopoverProps)
|
|
657
380
|
|
|
658
381
|
export interface PopoverProps {
|
|
659
382
|
/** 弹层挂在哪个元素上,右键菜单则是一个点。 */
|
|
660
383
|
anchor: Anchor;
|
|
661
384
|
onClose: () => void;
|
|
662
385
|
children: React.ReactNode;
|
|
663
|
-
/**
|
|
664
|
-
* 优先放在哪一侧。地方不够时自动翻转。
|
|
665
|
-
*
|
|
666
|
-
* `right` 把菜单放在触发元素旁边而不是下面,这对列表里的一行很重要:往下掉的菜单会盖住你正要拿来
|
|
667
|
-
* 对比的那几行。
|
|
668
|
-
*/
|
|
386
|
+
/** 优先放在哪一侧。地方不够时自动翻转。 */
|
|
669
387
|
placement?: "top" | "bottom" | "right";
|
|
670
388
|
/** 弹层的哪条边和锚点对齐。 */
|
|
671
389
|
align?: "start" | "end" | "center";
|
|
672
390
|
width?: PopoverWidth;
|
|
673
|
-
/**
|
|
674
|
-
* 钉在滚动正文上面——搜索框、标题。
|
|
675
|
-
*
|
|
676
|
-
* 不放进 `children`,因为它不能滚动:一用就离开屏幕的筛选框,要改它还得滚回去。
|
|
677
|
-
*/
|
|
391
|
+
/** 钉在滚动正文上面——搜索框、标题。 */
|
|
678
392
|
header?: React.ReactNode;
|
|
679
393
|
/** 钉在它下面,给不管列表多长都要够得着的动作。 */
|
|
680
394
|
footer?: React.ReactNode;
|
|
681
|
-
/**
|
|
682
|
-
* 钉在它左边——比如一列分类。
|
|
683
|
-
*
|
|
684
|
-
* 是槽位,理由和另外两个一样:跟着被筛选的列表一起滚走的筛选,要改还得滚回去。它也不能作为 sticky
|
|
685
|
-
* 的一列放在 `children` 里,因为滚动区的内容盒会裁掉横向溢出,而祖先上的任何 `overflow` 都会让
|
|
686
|
-
* `sticky` 粘在那个祖先上而不是视口上。
|
|
687
|
-
*/
|
|
395
|
+
/** 钉在它左边——比如一列分类。 */
|
|
688
396
|
aside?: React.ReactNode;
|
|
689
397
|
/** 给否则会长满窗口的面一个上限——见 `MENU_MAX_HEIGHT`。 */
|
|
690
398
|
maxHeight?: number;
|
|
691
|
-
/**
|
|
692
|
-
* 这个面是什么,给读窗口而不是看窗口的东西。
|
|
693
|
-
*
|
|
694
|
-
* 写死成 `menu` 对大多数是对的,对不是菜单的就错了:重命名表单、一段读数、一个滑块。被播报成菜单项
|
|
695
|
-
* 的输入框,比什么都不播报更糟。
|
|
696
|
-
*/
|
|
399
|
+
/** 这个面是什么,给读窗口而不是看窗口的东西。 */
|
|
697
400
|
role?: "menu" | "listbox" | "dialog" | "group";
|
|
698
401
|
/** 触发元素没说清时,给这个面一个名字。 */
|
|
699
402
|
label?: string;
|
|
700
403
|
className?: string;
|
|
701
404
|
/** 加在滚动正文上,给自己给内容加内衬的面。 */
|
|
702
405
|
bodyClassName?: string;
|
|
703
|
-
/**
|
|
704
|
-
* 一列行,还是一样东西。
|
|
705
|
-
*
|
|
706
|
-
* `menu` 是每个菜单要的内衬:圆角需要地方,行不能跑到滚动条底下,渐隐的边说明列表在那之外还有。
|
|
707
|
-
* 这三样对只装一块自己画底色的面板的面都是错的——比如一段 diff,它的代码底色会在离卡片右边 25px、
|
|
708
|
-
* 离顶 6px 处就停了,缝里露出浮层的底。面板读起来像嵌在一张它没填满的卡片里。
|
|
709
|
-
*
|
|
710
|
-
* `panel` 把整个正文交给内容,让叠加的滑块浮在上面,这正是叠加滚动条的用途。
|
|
711
|
-
*/
|
|
406
|
+
/** 一列行(`menu`),还是一样东西(`panel`:整个正文交给内容,滑块浮在上面)。 */
|
|
712
407
|
surface?: "menu" | "panel";
|
|
713
|
-
/**
|
|
714
|
-
* 把悬停交给整个面,给悬停打开的弹层用。
|
|
715
|
-
*
|
|
716
|
-
* 调用方如果挂在自己传的 `header` 和 `children` 上,描边、滚动区自己的外边距、滚动条那一条和滑块都在
|
|
717
|
-
* 处理范围之外:指针从这些地方进入弹层,在调用方看来仍然在外面——待执行的关闭就跑了。抓住悬停预览的
|
|
718
|
-
* 滚动条会把它关掉。
|
|
719
|
-
*/
|
|
408
|
+
/** 把悬停交给整个面,给悬停打开的弹层用。 */
|
|
720
409
|
onMouseEnter?: () => void;
|
|
721
410
|
onMouseLeave?: () => void;
|
|
722
411
|
}
|
|
412
|
+
|
|
413
|
+
export type Anchor = HTMLElement | PointAnchor | null;
|
|
414
|
+
|
|
415
|
+
export type PopoverWidth = keyof typeof MENU_WIDTH | number;
|
|
416
|
+
|
|
417
|
+
interface PointAnchor {
|
|
418
|
+
x: number;
|
|
419
|
+
y: number;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const MENU_WIDTH = {
|
|
423
|
+
/** 只有动作,每个一两个词。 */
|
|
424
|
+
compact: …,
|
|
425
|
+
/** 普通的菜单——图标、文字,有时有第二行——以及所有下拉。 */
|
|
426
|
+
default: …,
|
|
427
|
+
/** 带搜索框的,或者长到要滚动的列表。 */
|
|
428
|
+
wide: …,
|
|
429
|
+
/** 不是菜单:一段读数、一个滑块、一张表单。 */
|
|
430
|
+
panel: …,
|
|
431
|
+
/** 内容多宽就多宽(`undefined` 就是「这一层别管宽度」)。 */
|
|
432
|
+
hug: …,
|
|
433
|
+
} as const;
|
|
723
434
|
```
|
|
724
435
|
|
|
725
436
|
### `ContextMenu`
|
|
726
437
|
|
|
727
|
-
源码:`packages/client/ui/src/overlay/ContextMenu.tsx`
|
|
728
|
-
|
|
729
438
|
```tsx
|
|
730
|
-
/**
|
|
731
|
-
|
|
732
|
-
*
|
|
733
|
-
* 每一行都会关掉它,在这里处理而不是每一项各自记得:右键菜单里没有不是动作的行,点完还开着的那个会是
|
|
734
|
-
* 应用里唯一这样的菜单。监听在冒泡阶段,所以行自己的处理已经先运行了。
|
|
735
|
-
*/
|
|
736
|
-
export function ContextMenu({
|
|
737
|
-
anchor,
|
|
738
|
-
onClose,
|
|
739
|
-
width = "default",
|
|
740
|
-
children,
|
|
741
|
-
}: {
|
|
439
|
+
/** 面本身。点了任何一行都会关掉它。 */
|
|
440
|
+
export function ContextMenu({ width = "default" }: {
|
|
742
441
|
anchor: { x: number; y: number } | null;
|
|
743
442
|
onClose: () => void;
|
|
744
443
|
width?: PopoverWidth;
|
|
@@ -748,59 +447,24 @@ export function ContextMenu({
|
|
|
748
447
|
|
|
749
448
|
### `MenuItem`
|
|
750
449
|
|
|
751
|
-
源码:`packages/client/ui/src/overlay/Menu.tsx`
|
|
752
|
-
|
|
753
450
|
```tsx
|
|
754
|
-
/**
|
|
755
|
-
|
|
756
|
-
*
|
|
757
|
-
* 每个菜单各自长一份的话——高度不同、圆角不同、有的干脆没有圆角——同一个手势在不同菜单里看着不一样。
|
|
758
|
-
* 视觉状态放在 `.plume-item` 里,需要别的形状的行(比如两行的选项)也能用上它们。
|
|
759
|
-
*/
|
|
760
|
-
export function MenuItem({
|
|
761
|
-
icon,
|
|
762
|
-
children,
|
|
763
|
-
detail,
|
|
764
|
-
hint,
|
|
765
|
-
trailing,
|
|
766
|
-
checked,
|
|
767
|
-
selected,
|
|
768
|
-
danger,
|
|
769
|
-
disabled,
|
|
770
|
-
title,
|
|
771
|
-
className = "",
|
|
772
|
-
onClick,
|
|
773
|
-
}: {
|
|
451
|
+
/** 菜单里的一行。 */
|
|
452
|
+
export function MenuItem({ className = "" }: {
|
|
774
453
|
icon?: React.ReactNode;
|
|
775
454
|
children: React.ReactNode;
|
|
776
|
-
/**
|
|
777
|
-
* 解释这个选项的第二行。
|
|
778
|
-
*
|
|
779
|
-
* 在这里处理,而不是每个菜单自己排两行的行:自己排的话会长出自己的高度、内边距和悬停效果,同一个
|
|
780
|
-
* 手势在不同菜单里看着不一样。
|
|
781
|
-
*/
|
|
455
|
+
/** 解释这个选项的第二行。 */
|
|
782
456
|
detail?: React.ReactNode;
|
|
783
457
|
/** 右对齐的注记:数量、快捷键数字、上下文大小。 */
|
|
784
458
|
hint?: React.ReactNode;
|
|
785
459
|
/** 右对齐的元素,放勾或箭头。 */
|
|
786
460
|
trailing?: React.ReactNode;
|
|
787
|
-
/**
|
|
788
|
-
* 让这一行成为开关设置:行尾一个小开关,`menuitemcheckbox` 语义。
|
|
789
|
-
*
|
|
790
|
-
* 文字仍是设置的名字——「自动更新」——状态由开关说。两样都用字写的话,「自动更新:开(点击关闭)」
|
|
791
|
-
* 是一句要解析的话,而本该扫一眼就够,还会跑出菜单的右边。
|
|
792
|
-
*/
|
|
461
|
+
/** 让这一行成为开关设置:行尾一个小开关,`menuitemcheckbox` 语义。 */
|
|
793
462
|
checked?: boolean;
|
|
794
463
|
selected?: boolean;
|
|
795
464
|
danger?: boolean;
|
|
796
465
|
disabled?: boolean;
|
|
797
466
|
title?: string;
|
|
798
|
-
/**
|
|
799
|
-
* 加在行本身上的类,给用别的方式标记行的菜单。
|
|
800
|
-
*
|
|
801
|
-
* 比如右边那一列已经被占了、所以选中行用填充而不是打勾的菜单。选了这种方式的行得能说出来——在这里
|
|
802
|
-
* 说,高度、圆角和按下状态仍是共用的,而这正是行只住在一个地方的意义。
|
|
803
|
-
*/
|
|
467
|
+
/** 加在行本身上的类,给用别的方式标记行的菜单。 */
|
|
804
468
|
className?: string;
|
|
805
469
|
onClick?: () => void;
|
|
806
470
|
})
|
|
@@ -808,8 +472,6 @@ export function MenuItem({
|
|
|
808
472
|
|
|
809
473
|
### `MenuSeparator`
|
|
810
474
|
|
|
811
|
-
源码:`packages/client/ui/src/overlay/Menu.tsx`
|
|
812
|
-
|
|
813
475
|
```tsx
|
|
814
476
|
/** 分开菜单里的几组行。 */
|
|
815
477
|
export function MenuSeparator()
|
|
@@ -817,8 +479,6 @@ export function MenuSeparator()
|
|
|
817
479
|
|
|
818
480
|
### `MenuLabel`
|
|
819
481
|
|
|
820
|
-
源码:`packages/client/ui/src/overlay/Menu.tsx`
|
|
821
|
-
|
|
822
482
|
```tsx
|
|
823
483
|
/** 一组行上面的小标题。 */
|
|
824
484
|
export function MenuLabel({ children }: { children: React.ReactNode })
|
|
@@ -826,25 +486,12 @@ export function MenuLabel({ children }: { children: React.ReactNode })
|
|
|
826
486
|
|
|
827
487
|
### `Dialog`
|
|
828
488
|
|
|
829
|
-
源码:`packages/client/ui/src/overlay/Dialog.tsx`
|
|
830
|
-
|
|
831
489
|
```tsx
|
|
832
490
|
/**
|
|
833
|
-
* 里面放着 `DialogFrame` 的 `Overlay
|
|
834
|
-
*
|
|
835
|
-
* `actions` 和 `children` 可以是 `dismiss` 的函数,关对话框的按钮应该用它:`dismiss()` 播完退场再运行
|
|
836
|
-
* `onClose`,`dismiss(after)` 改为运行 `after`。按钮直接调用 `onClose` 会在那一帧卸掉对话框——那样
|
|
837
|
-
* 只有 Escape 和点遮罩是怎么来的就怎么走。
|
|
491
|
+
* 里面放着 `DialogFrame` 的 `Overlay`。`actions` 和 `children` 可以是 `dismiss` 的函数,关对话框的按钮
|
|
492
|
+
* 应该用它:`dismiss()` 播完退场再运行 `onClose`,`dismiss(after)` 改为运行 `after`。
|
|
838
493
|
*/
|
|
839
|
-
export function Dialog({
|
|
840
|
-
onClose,
|
|
841
|
-
width = 480,
|
|
842
|
-
label,
|
|
843
|
-
returnFocus,
|
|
844
|
-
actions,
|
|
845
|
-
children,
|
|
846
|
-
...frame
|
|
847
|
-
}: {
|
|
494
|
+
export function Dialog({ width = 480, ...frame }: {
|
|
848
495
|
onClose: () => void;
|
|
849
496
|
width?: number;
|
|
850
497
|
label?: string;
|
|
@@ -852,34 +499,30 @@ export function Dialog({
|
|
|
852
499
|
actions: ReactNode | ((dismiss: Dismiss) => ReactNode);
|
|
853
500
|
children?: ReactNode | ((dismiss: Dismiss) => ReactNode);
|
|
854
501
|
} & Omit<Parameters<typeof DialogFrame>[0], "actions" | "children">)
|
|
502
|
+
|
|
503
|
+
export function DialogFrame({ className = "", ...rest }: {
|
|
504
|
+
title: ReactNode;
|
|
505
|
+
icon?: ReactNode;
|
|
506
|
+
detail?: ReactNode;
|
|
507
|
+
/** 放在正文里、滚动区上方——进度、警告,任何不能被滚走的东西。 */
|
|
508
|
+
status?: ReactNode;
|
|
509
|
+
children?: ReactNode;
|
|
510
|
+
actions: ReactNode;
|
|
511
|
+
height?: number;
|
|
512
|
+
/**
|
|
513
|
+
* 滚动的正文最高能长到多高。只给是表单而不是问题的对话框用——默认值按一段话加一个列表定的,
|
|
514
|
+
* 设置表单放在它下面一次只能滚出三个字段。
|
|
515
|
+
*/
|
|
516
|
+
bodyClassName?: string;
|
|
517
|
+
className?: string;
|
|
518
|
+
} & Omit<HTMLAttributes<HTMLDivElement>, "title" | "children">)
|
|
855
519
|
```
|
|
856
520
|
|
|
857
521
|
### `DialogAction`
|
|
858
522
|
|
|
859
|
-
源码:`packages/client/ui/src/overlay/Dialog.tsx`
|
|
860
|
-
|
|
861
523
|
```tsx
|
|
862
|
-
/**
|
|
863
|
-
|
|
864
|
-
*
|
|
865
|
-
* **写字,不画图标。** 这些按钮曾经有好几处只放一个 ✕ 和一个 ✓,名字挂在 tooltip 上。工具栏
|
|
866
|
-
* 里的图标按钮可以这么省——周围一排东西替它说明它属于哪一类;对话框底下这两颗没有这个周围,
|
|
867
|
-
* 它们是这次操作的结论本身,一个要说清按下去会发生什么,另一个要说清不按会怎样。一个勾在
|
|
868
|
-
* 「删除会话」「卸载插件」「导入 33 个模型」上长得一模一样,而读出区别的唯一办法是把鼠标停
|
|
869
|
-
* 上去等一个 tooltip——这正是一个「停下一切来问」的界面最不该要求的动作。
|
|
870
|
-
*
|
|
871
|
-
* `label` 留着,但只当 tooltip 用:可见的字已经是它的名字了。
|
|
872
|
-
*/
|
|
873
|
-
export function DialogAction({
|
|
874
|
-
children,
|
|
875
|
-
onClick,
|
|
876
|
-
tone = "secondary",
|
|
877
|
-
disabled,
|
|
878
|
-
autoFocus,
|
|
879
|
-
label,
|
|
880
|
-
className = "",
|
|
881
|
-
...rest
|
|
882
|
-
}: {
|
|
524
|
+
/** 对话框底下的那一颗按钮。上面写着它会做的那件事:写字,不画图标。 */
|
|
525
|
+
export function DialogAction({ tone = "secondary", className = "", ...rest }: {
|
|
883
526
|
children: ReactNode;
|
|
884
527
|
onClick?: () => void;
|
|
885
528
|
tone?: "secondary" | "primary" | "danger";
|
|
@@ -893,15 +536,8 @@ export function DialogAction({
|
|
|
893
536
|
|
|
894
537
|
### `Confirm`
|
|
895
538
|
|
|
896
|
-
源码:`packages/client/ui/src/overlay/Confirm.tsx`
|
|
897
|
-
|
|
898
539
|
```tsx
|
|
899
|
-
/**
|
|
900
|
-
* 放在应用模态面上的问题:居中、压着遮罩、Escape 或点遮罩关掉。
|
|
901
|
-
*
|
|
902
|
-
* 这三样都由 `Overlay` 提供,而且和应用里其它对话框拿到的方式一样——这正是通过它来问、而不是在每个
|
|
903
|
-
* 调用处各拼一个的意义。
|
|
904
|
-
*/
|
|
540
|
+
/** 放在应用模态面上的问题:居中、压着遮罩、Escape 或点遮罩关掉。 */
|
|
905
541
|
export function Confirm({ onCancel, ...options }: ConfirmOptions & { onCancel: () => void })
|
|
906
542
|
|
|
907
543
|
export interface ConfirmOptions {
|
|
@@ -913,27 +549,12 @@ export interface ConfirmOptions {
|
|
|
913
549
|
confirmLabel: string;
|
|
914
550
|
/** 取消按钮上的字,默认「取消」。 */
|
|
915
551
|
cancelLabel?: string;
|
|
916
|
-
/**
|
|
917
|
-
* `danger` 是默认,因为这个东西本来是给删除用的。
|
|
918
|
-
*
|
|
919
|
-
* `normal` 留给另一类问题:不是「这个删了拿不回来」,而是「以后要不要自动做这件事」。
|
|
920
|
-
* 两种都该停下一切来问,但只有前一种该是红的——把每个问题都画成危险,等于没画过危险。
|
|
921
|
-
*/
|
|
552
|
+
/** `danger` 是默认,给删除这类拿不回来的事;`normal` 给「以后要不要自动做这件事」这类问题。 */
|
|
922
553
|
tone?: "danger" | "normal";
|
|
923
554
|
onConfirm: () => void;
|
|
924
|
-
/**
|
|
925
|
-
* 取消也要做点什么的时候。
|
|
926
|
-
*
|
|
927
|
-
* 绝大多数确认框的取消就是「什么都别发生」,所以这是可选的。但有一种问题不是这样:
|
|
928
|
-
* 「以后要不要自动做这件事」——那里的取消是一个回答(不要),把它当成没问过,
|
|
929
|
-
* 意思就是下次还问。
|
|
930
|
-
*/
|
|
555
|
+
/** 取消也要做点什么的时候。 */
|
|
931
556
|
onCancel?: () => void;
|
|
932
|
-
/**
|
|
933
|
-
* 同一个动词的另一种做法,比如「删除此会话」旁边的「删除 3 个会话」。
|
|
934
|
-
*
|
|
935
|
-
* 画成次要按钮,放在主按钮左边;有它的时候「取消」挪到最左,三颗按钮不挤成一排。
|
|
936
|
-
*/
|
|
557
|
+
/** 同一个动词的另一种做法,比如「删除此会话」旁边的「删除 3 个会话」。 */
|
|
937
558
|
alternate?: { label: string; onConfirm: () => void };
|
|
938
559
|
/** 说明和按钮之间的那一块,比如「不再询问」。大多数问题用不着。 */
|
|
939
560
|
children?: React.ReactNode;
|
|
@@ -942,53 +563,27 @@ export interface ConfirmOptions {
|
|
|
942
563
|
|
|
943
564
|
### `useConfirmer`
|
|
944
565
|
|
|
945
|
-
源码:`packages/client/ui/src/overlay/Confirm.tsx`
|
|
946
|
-
|
|
947
566
|
```tsx
|
|
948
|
-
/**
|
|
949
|
-
* 给满是删除按钮的页面用的一个确认。
|
|
950
|
-
*
|
|
951
|
-
* 每颗按钮交出自己的问题和自己的后果;面、两颗按钮的措辞、取消是安全的那个,这些在这里定。否则一个
|
|
952
|
-
* 有六行可删除内容的页面要带着同一份状态的六个副本。
|
|
953
|
-
*/
|
|
567
|
+
/** 给满是删除按钮的页面用的一个确认。 */
|
|
954
568
|
export function useConfirmer()
|
|
955
569
|
```
|
|
956
570
|
|
|
957
571
|
### `useConfirmGate`
|
|
958
572
|
|
|
959
|
-
源码:`packages/client/ui/src/overlay/Confirm.tsx`
|
|
960
|
-
|
|
961
573
|
```tsx
|
|
962
|
-
/**
|
|
963
|
-
* 同一个问题,可以 await。
|
|
964
|
-
*
|
|
965
|
-
* 给在循环里问的调用方——粘贴五个文件,其中两个重名——回答得先回来,下一步才能定。写成 promise,
|
|
966
|
-
* 读起来就是普通的顺序代码,而不是每个分支一个回调。
|
|
967
|
-
*/
|
|
574
|
+
/** 同一个问题,可以 await。 */
|
|
968
575
|
export function useConfirmGate()
|
|
969
576
|
```
|
|
970
577
|
|
|
971
578
|
### `Tooltip`
|
|
972
579
|
|
|
973
|
-
源码:`packages/client/ui/src/overlay/Tooltip.tsx`
|
|
974
|
-
|
|
975
580
|
```tsx
|
|
976
|
-
/**
|
|
977
|
-
* 应用的悬停提示,写成 JSX 包装,比属性读起来顺。
|
|
978
|
-
*
|
|
979
|
-
* 它只给子元素打标记——气泡本身由 `tooltip-attribute.ts` 里那一个文档级监听器画。所以两种写法表现完全一样,
|
|
980
|
-
* 这很重要,因为第三方库自己建的按钮只能靠属性够到。
|
|
981
|
-
*
|
|
982
|
-
* 取代浏览器的 `title`:那个改不了样式,要等一秒半左右,不跟应用的字体和主题,在 macOS 上画成一个
|
|
983
|
-
* 系统面板,看着像属于另一个程序。
|
|
984
|
-
*/
|
|
581
|
+
/** 应用的悬停提示,代替浏览器的 `title`。写成 JSX 包装,比直接写 `data-plume-tip` 属性读起来顺,两种写法表现一样。 */
|
|
985
582
|
export function Tooltip({ label, children, side = "bottom" }: { label: string; children: React.ReactElement; side?: "top" | "bottom" })
|
|
986
583
|
```
|
|
987
584
|
|
|
988
585
|
### `usePopover`
|
|
989
586
|
|
|
990
|
-
源码:`packages/client/ui/src/overlay/Popover.tsx`
|
|
991
|
-
|
|
992
587
|
```tsx
|
|
993
588
|
/** 记下锚点元素和打开状态,每个弹层触发器都要的这一对。 */
|
|
994
589
|
export function usePopover()
|
|
@@ -996,20 +591,9 @@ export function usePopover()
|
|
|
996
591
|
|
|
997
592
|
### `MenuBody`
|
|
998
593
|
|
|
999
|
-
源码:`packages/client/ui/src/overlay/Menu.tsx`
|
|
1000
|
-
|
|
1001
594
|
```tsx
|
|
1002
|
-
/**
|
|
1003
|
-
|
|
1004
|
-
*
|
|
1005
|
-
* 行四周的留白在滚动区上(`--plume-menu-inset`),不在这里。这里再加一层内衬会叠在那圈外边距上,
|
|
1006
|
-
* 上下就成了左右的两倍。
|
|
1007
|
-
*/
|
|
1008
|
-
export function MenuBody({
|
|
1009
|
-
children,
|
|
1010
|
-
insetIcons = false,
|
|
1011
|
-
className = "",
|
|
1012
|
-
}: {
|
|
595
|
+
/** 每个菜单的内容所在的盒子。 */
|
|
596
|
+
export function MenuBody({ insetIcons = false, className = "" }: {
|
|
1013
597
|
children: React.ReactNode;
|
|
1014
598
|
/** 每行都留出图标列,给只有部分行有图标的菜单用。 */
|
|
1015
599
|
insetIcons?: boolean;
|
|
@@ -1019,24 +603,9 @@ export function MenuBody({
|
|
|
1019
603
|
|
|
1020
604
|
### `MenuSearch`
|
|
1021
605
|
|
|
1022
|
-
源码:`packages/client/ui/src/overlay/Menu.tsx`
|
|
1023
|
-
|
|
1024
606
|
```tsx
|
|
1025
|
-
/**
|
|
1026
|
-
|
|
1027
|
-
*
|
|
1028
|
-
* 每个菜单各写一份的话,同样的高度、同样的放大镜、同样的占位文案,每多一份就多一次走样的机会。
|
|
1029
|
-
* 给 `Popover` 的 `header` 槽位用,列表滚动时它不动;它下面那条线属于那个槽位,不在这里画。
|
|
1030
|
-
*
|
|
1031
|
-
* Escape 先清空再关闭,和 `SearchField` 一样:第一次按撤销输入,只有没东西可撤销时按才关掉菜单。
|
|
1032
|
-
*/
|
|
1033
|
-
export function MenuSearch({
|
|
1034
|
-
value,
|
|
1035
|
-
onChange,
|
|
1036
|
-
placeholder,
|
|
1037
|
-
autoFocus = true,
|
|
1038
|
-
onKeyDown,
|
|
1039
|
-
}: {
|
|
607
|
+
/** 列出超过一屏的菜单顶上的筛选框,放进 `Popover` 的 `header` 槽位,列表滚动时它不动。Escape 先清空再关闭。 */
|
|
608
|
+
export function MenuSearch({ autoFocus = true }: {
|
|
1040
609
|
value: string;
|
|
1041
610
|
onChange: (value: string) => void;
|
|
1042
611
|
placeholder?: string;
|
|
@@ -1048,31 +617,16 @@ export function MenuSearch({
|
|
|
1048
617
|
|
|
1049
618
|
### `MENU_MAX_HEIGHT`
|
|
1050
619
|
|
|
1051
|
-
源码:`packages/client/ui/src/overlay/Popover.tsx`
|
|
1052
|
-
|
|
1053
620
|
```tsx
|
|
1054
|
-
/**
|
|
1055
|
-
* 列东西的菜单最高能长到多高。
|
|
1056
|
-
*
|
|
1057
|
-
* 交给窗口决定的话,高屏幕上的一个列表菜单会从输入框一直铺到屏幕顶——技术上没错,但四十行的列表没人
|
|
1058
|
-
* 读到底。会滚动的菜单出于同一个理由各挑一个上限(280、300、320),这里把这个理由说一次。
|
|
1059
|
-
*/
|
|
621
|
+
/** 列东西的菜单最高能长到多高。 */
|
|
1060
622
|
export const MENU_MAX_HEIGHT = 340;
|
|
1061
623
|
```
|
|
1062
624
|
|
|
1063
625
|
### `Overlay`
|
|
1064
626
|
|
|
1065
|
-
源码:`packages/client/ui/src/overlay/Overlay.tsx`
|
|
1066
|
-
|
|
1067
627
|
```tsx
|
|
1068
628
|
/** Escape、点遮罩、取消和完成共用一套生命周期,嵌套的对话框也一样。 */
|
|
1069
|
-
export function Overlay({
|
|
1070
|
-
children,
|
|
1071
|
-
onClose,
|
|
1072
|
-
width = 460,
|
|
1073
|
-
label,
|
|
1074
|
-
returnFocus,
|
|
1075
|
-
}: {
|
|
629
|
+
export function Overlay({ width = 460 }: {
|
|
1076
630
|
children: React.ReactNode | ((dismiss: Dismiss) => React.ReactNode);
|
|
1077
631
|
onClose: () => void;
|
|
1078
632
|
width?: number;
|
|
@@ -1087,8 +641,6 @@ export type Dismiss = (after?: () => void) => void;
|
|
|
1087
641
|
|
|
1088
642
|
### `Card`
|
|
1089
643
|
|
|
1090
|
-
源码:`packages/client/ui/src/index.tsx`
|
|
1091
|
-
|
|
1092
644
|
```tsx
|
|
1093
645
|
export function Card({ title, subtitle, status, tone = "default", children }: CardProps)
|
|
1094
646
|
|
|
@@ -1105,8 +657,6 @@ export interface CardProps {
|
|
|
1105
657
|
|
|
1106
658
|
### `CodeBlock`
|
|
1107
659
|
|
|
1108
|
-
源码:`packages/client/ui/src/index.tsx`
|
|
1109
|
-
|
|
1110
660
|
```tsx
|
|
1111
661
|
export function CodeBlock({ children }: { children: string })
|
|
1112
662
|
```
|
|
@@ -1115,18 +665,8 @@ export function CodeBlock({ children }: { children: string })
|
|
|
1115
665
|
|
|
1116
666
|
### `ApprovalFrame`
|
|
1117
667
|
|
|
1118
|
-
源码:`packages/client/ui/src/layout/ApprovalFrame.tsx`
|
|
1119
|
-
|
|
1120
668
|
```tsx
|
|
1121
|
-
export function ApprovalFrame({
|
|
1122
|
-
requestId,
|
|
1123
|
-
icon,
|
|
1124
|
-
title,
|
|
1125
|
-
kind,
|
|
1126
|
-
expiresAt,
|
|
1127
|
-
queued = 0,
|
|
1128
|
-
children,
|
|
1129
|
-
}: {
|
|
669
|
+
export function ApprovalFrame({ queued = 0 }: {
|
|
1130
670
|
/** 这次请求的 id(`toolCallId`):换了一张卡,收起状态不带过去。 */
|
|
1131
671
|
requestId: string;
|
|
1132
672
|
/** 标题前 15px 的图标:权限请求是警告,提问是气泡。 */
|
|
@@ -1145,15 +685,10 @@ export function ApprovalFrame({
|
|
|
1145
685
|
|
|
1146
686
|
### `ApprovalExpiry`
|
|
1147
687
|
|
|
1148
|
-
源码:`packages/client/ui/src/layout/ApprovalFrame.tsx`
|
|
1149
|
-
|
|
1150
688
|
```tsx
|
|
1151
689
|
/**
|
|
1152
690
|
* 这张卡还剩多久失效,每秒走一格(「4:59 后失效」)。到期没人答,本体按拒绝收场:不说出来的截止比没有截止更糟,
|
|
1153
691
|
* 人回来时这一轮已经被一个没见过的「不」结束了。
|
|
1154
|
-
*
|
|
1155
|
-
* 单独一个组件,秒针走动不重画旁边正在打字的表单。走到零就不说了:正常情况下卡片这一刻已经收走,留下的只可能是
|
|
1156
|
-
* 一份过时的快照,「0:00 后失效」不该出现。
|
|
1157
692
|
*/
|
|
1158
693
|
export function ApprovalExpiry({ at }: { at: number })
|
|
1159
694
|
```
|
|
@@ -1162,8 +697,6 @@ export function ApprovalExpiry({ at }: { at: number })
|
|
|
1162
697
|
|
|
1163
698
|
### `SettingsSectionTitle`
|
|
1164
699
|
|
|
1165
|
-
源码:`packages/client/ui/src/layout/Settings.tsx`
|
|
1166
|
-
|
|
1167
700
|
```tsx
|
|
1168
701
|
/** 一组卡片上面的分组标题。 */
|
|
1169
702
|
export function SettingsSectionTitle({ children }: { children: React.ReactNode })
|
|
@@ -1171,30 +704,16 @@ export function SettingsSectionTitle({ children }: { children: React.ReactNode }
|
|
|
1171
704
|
|
|
1172
705
|
### `SettingsCard`
|
|
1173
706
|
|
|
1174
|
-
源码:`packages/client/ui/src/layout/Settings.tsx`
|
|
1175
|
-
|
|
1176
707
|
```tsx
|
|
1177
|
-
/**
|
|
1178
|
-
* 其余属性原样透传,卡片可以带一个 `data-` 标记给测试量它。
|
|
1179
|
-
*
|
|
1180
|
-
* 卡片用 `float` 那张纸:设置卡片和弹出菜单是同一个颜色,两个主题下都比页面亮一档的实底。边框随之用 `line-float`,
|
|
1181
|
-
* 那是给「画在自己那张纸上」的线准备的;卡片里面的线和控件怎么跟着换,见 `controls.css` 的 `.plume-settings-card`。
|
|
1182
|
-
*/
|
|
708
|
+
/** 其余属性原样透传,卡片可以带一个 `data-` 标记给测试量它。 */
|
|
1183
709
|
export function SettingsCard({ children, className = "", ...rest }: React.ComponentProps<"div">)
|
|
1184
710
|
```
|
|
1185
711
|
|
|
1186
712
|
### `SettingsRow`
|
|
1187
713
|
|
|
1188
|
-
源码:`packages/client/ui/src/layout/Settings.tsx`
|
|
1189
|
-
|
|
1190
714
|
```tsx
|
|
1191
715
|
/** 卡片里带标签的一行,控件靠右。 */
|
|
1192
|
-
export function SettingsRow({
|
|
1193
|
-
title,
|
|
1194
|
-
detail,
|
|
1195
|
-
control,
|
|
1196
|
-
children,
|
|
1197
|
-
}: {
|
|
716
|
+
export function SettingsRow(props: {
|
|
1198
717
|
title: React.ReactNode;
|
|
1199
718
|
detail?: React.ReactNode;
|
|
1200
719
|
control?: React.ReactNode;
|
|
@@ -1204,27 +723,14 @@ export function SettingsRow({
|
|
|
1204
723
|
|
|
1205
724
|
### `SettingsNotice`
|
|
1206
725
|
|
|
1207
|
-
源码:`packages/client/ui/src/layout/Settings.tsx`
|
|
1208
|
-
|
|
1209
726
|
```tsx
|
|
1210
|
-
/**
|
|
1211
|
-
|
|
1212
|
-
*
|
|
1213
|
-
* 框和语气在这里定一次,产品的设置页和插件加的设置分区共用。安静的那种不定字色:用它的页面在里面放一行偏灰的标题、
|
|
1214
|
-
* 几行更淡的说明,这里给一种颜色会把层次压平。
|
|
1215
|
-
*/
|
|
1216
|
-
export function SettingsNotice({
|
|
1217
|
-
tone,
|
|
1218
|
-
icon: Icon,
|
|
1219
|
-
className = "",
|
|
1220
|
-
children,
|
|
1221
|
-
...rest
|
|
1222
|
-
}: React.ComponentProps<"div"> & { tone: keyof typeof NOTICE_TONES; icon?: LucideIcon })
|
|
727
|
+
/** 页面要告诉你的一句话:出错了(`danger`)、有东西要看一眼(`accent`),或者只是让你知道(`muted`)。 */
|
|
728
|
+
export function SettingsNotice({ className = "", ...rest }: React.ComponentProps<"div"> & { tone: keyof typeof NOTICE_TONES; icon?: LucideIcon })
|
|
1223
729
|
|
|
1224
730
|
const NOTICE_TONES = {
|
|
1225
|
-
danger:
|
|
1226
|
-
accent:
|
|
1227
|
-
muted:
|
|
731
|
+
danger: …,
|
|
732
|
+
accent: …,
|
|
733
|
+
muted: …,
|
|
1228
734
|
} as const;
|
|
1229
735
|
```
|
|
1230
736
|
|
|
@@ -1232,23 +738,9 @@ const NOTICE_TONES = {
|
|
|
1232
738
|
|
|
1233
739
|
### `PanelEmpty`
|
|
1234
740
|
|
|
1235
|
-
源码:`packages/client/ui/src/layout/PanelEmpty.tsx`
|
|
1236
|
-
|
|
1237
741
|
```tsx
|
|
1238
|
-
/**
|
|
1239
|
-
|
|
1240
|
-
*
|
|
1241
|
-
* 居中的图标、名字、一句话说这是什么。每一种标签页都是同一个形状,面板读起来是一个东西装着几种内容,
|
|
1242
|
-
* 而不是碰巧共用一个框的几块互不相干的屏。
|
|
1243
|
-
*
|
|
1244
|
-
* 单独一个文件,因为面板外壳和它里面的东西都要用它,由外壳导出的话两边会互相 import。
|
|
1245
|
-
*/
|
|
1246
|
-
export function PanelEmpty({
|
|
1247
|
-
icon: Icon,
|
|
1248
|
-
art,
|
|
1249
|
-
title,
|
|
1250
|
-
children,
|
|
1251
|
-
}: {
|
|
742
|
+
/** 空面板正文的中间。 */
|
|
743
|
+
export function PanelEmpty(props: {
|
|
1252
744
|
icon: LucideIcon;
|
|
1253
745
|
/** 画在图标的位置上、比一枚线性图标更能说明这里该有什么的东西——子智能体面板放的是几张睡着的脸。 */
|
|
1254
746
|
art?: React.ReactNode;
|
|
@@ -1260,8 +752,6 @@ export function PanelEmpty({
|
|
|
1260
752
|
|
|
1261
753
|
### `RetainedViews`
|
|
1262
754
|
|
|
1263
|
-
源码:`packages/client/ui/src/layout/RetainedViews.tsx`
|
|
1264
|
-
|
|
1265
755
|
```tsx
|
|
1266
756
|
/** 留住少数几个看过的页面;藏起来的页面暂停 effect,保留局部状态。 */
|
|
1267
757
|
export function RetainedViews<T extends string>({ active, render, limit = 3 }: { active: T; render: (key: T) => React.ReactNode; limit?: number })
|
|
@@ -1269,31 +759,13 @@ export function RetainedViews<T extends string>({ active, render, limit = 3 }: {
|
|
|
1269
759
|
|
|
1270
760
|
### `useNarrow`
|
|
1271
761
|
|
|
1272
|
-
源码:`packages/client/ui/src/hooks/useNarrow.ts`
|
|
1273
|
-
|
|
1274
762
|
```tsx
|
|
1275
|
-
/**
|
|
1276
|
-
* 一个元素是不是窄到放不下两栏,按元素自己量。
|
|
1277
|
-
*
|
|
1278
|
-
* 面板里的内容没法问窗口自己有多宽——同一个组件既跑在 368px 的面板里,也跑在整列宽的正文里。
|
|
1279
|
-
* 这里看的是它实际拿到的那个盒子。
|
|
1280
|
-
*
|
|
1281
|
-
* 第一次测量在节点一到就做,而不是留给 observer 的首次回调。那次回调在绘制之后才到,一个从 `false`
|
|
1282
|
-
* 起步的组件会先画一帧错的布局——更糟的是,挂在这次翻转上的东西(比如列表/详情的重置)会在用户已经
|
|
1283
|
-
* 动手之后晚一拍才触发,把刚做的操作撤掉。
|
|
1284
|
-
*
|
|
1285
|
-
* 用回调 ref,不用 ref 对象。这不是风格问题:`useRef` + `useLayoutEffect` 只在 effect 第一次运行时
|
|
1286
|
-
* 挂 observer,一个先渲染了*别的*东西的组件——比如项目加载时的空状态——要么根本没挂上,要么挂到一个
|
|
1287
|
-
* 随后被换掉的节点上。两种情况下 observer 都在看一个不存在的东西,标记停在碰巧的那个值上,一个开到
|
|
1288
|
-
* 全屏的面板在一千像素宽时还在画堆叠布局。React 每次节点变化都会调用回调 ref,而节点变化正是要紧的那件事。
|
|
1289
|
-
*/
|
|
763
|
+
/** 一个元素是不是窄到放不下两栏,按元素自己量。 */
|
|
1290
764
|
export function useNarrow(threshold: number): [boolean, (node: HTMLDivElement | null) => void]
|
|
1291
765
|
```
|
|
1292
766
|
|
|
1293
767
|
### `useLiveRefresh`
|
|
1294
768
|
|
|
1295
|
-
源码:`packages/client/ui/src/hooks/useLiveRefresh.ts`
|
|
1296
|
-
|
|
1297
769
|
```tsx
|
|
1298
770
|
export function useLiveRefresh(refresh: () => void | Promise<void>, running: boolean, ms = LIVE_POLL_MS): void
|
|
1299
771
|
```
|
|
@@ -1302,84 +774,41 @@ export function useLiveRefresh(refresh: () => void | Promise<void>, running: boo
|
|
|
1302
774
|
|
|
1303
775
|
### `PageTopRow`
|
|
1304
776
|
|
|
1305
|
-
源码:`packages/client/ui/src/layout/PageTopRow.tsx`
|
|
1306
|
-
|
|
1307
777
|
```tsx
|
|
1308
778
|
/**
|
|
1309
779
|
* 自己画顶行的页面(`ui.page` 给了 `topRow: true`)每一栏最上面那一行:坐在窗口顶行的高度上,和侧栏顶上那一行齐平。
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
* no-drag:那样这一栏宽的一段就既拖不动窗口、也双击不了。Windows、Linux 上窗口顶上另有一条拖窗口的带子,宿主在根元素上
|
|
1313
|
-
* 标 `data-plume-window-header`,这一行跟着改成不拖动(`app-region` 不认 `var()`,只能按选择器切):在卡片里拖窗口会带上系统标题栏的行为(右键系统菜单、拖到屏幕边贴边)。
|
|
1314
|
-
*
|
|
1315
|
-
* `start` 是贴着页面左边的那一栏:侧栏收起时红绿灯和前进、后退浮在这里,左边让出它们的宽度(宿主经
|
|
1316
|
-
* `--plume-top-row-inset` 给,侧栏开着时是 0)。两栏页面的右栏在窄窗口里换成整页时,也给 `start`。
|
|
1317
|
-
*
|
|
780
|
+
* 整行拖得动窗口、双击能缩放,行里能点的控件(按钮、链接、输入框)自动不拖动,不用一个个标。贴着页面左边的那一栏给
|
|
781
|
+
* `start`(两栏页面的右栏在窄窗口里换成整页时也给),给侧栏收起时浮在这里的红绿灯和前进、后退让出宽度。
|
|
1318
782
|
* 高度、内边距之外的排法(间距、两端对齐)由 `className` 补;行里不折行,放不下的东西自己截断。
|
|
1319
783
|
*/
|
|
1320
|
-
export function PageTopRow({
|
|
1321
|
-
start = false,
|
|
1322
|
-
className = "",
|
|
1323
|
-
children,
|
|
1324
|
-
...rest
|
|
1325
|
-
}: { start?: boolean; className?: string; children?: ReactNode; ref?: Ref<HTMLElement> } & HTMLAttributes<HTMLElement>)
|
|
784
|
+
export function PageTopRow({ start = false, className = "", ...rest }: { start?: boolean; className?: string; children?: ReactNode; ref?: Ref<HTMLElement> } & HTMLAttributes<HTMLElement>)
|
|
1326
785
|
```
|
|
1327
786
|
|
|
1328
787
|
## 加载和占位
|
|
1329
788
|
|
|
1330
789
|
### `StatusSpinner`
|
|
1331
790
|
|
|
1332
|
-
源码:`packages/client/ui/src/motion/loaders.tsx`
|
|
1333
|
-
|
|
1334
791
|
```tsx
|
|
1335
|
-
/**
|
|
1336
|
-
* 一圈虚线,匀速转。
|
|
1337
|
-
*
|
|
1338
|
-
* 六段等分,所以转过 60° 就回到自身:没有可追踪的端点,眼睛抓不到锚点。这是它敢转、而一段
|
|
1339
|
-
* 弧不敢的原因——一段弧有头,十几个同时在屏幕上,每一个都在自己的相位上把视线勾一下。
|
|
1340
|
-
*
|
|
1341
|
-
* 颜色走 `currentColor`,默认跟着周围的文字走。要它说话大声一点的地方(任务清单里正在跑的
|
|
1342
|
-
* 那一步、流水线里排队的那一条)自己传 `text-accent`、`text-warning`。
|
|
1343
|
-
*/
|
|
792
|
+
/** 一圈虚线,匀速转:这一条正在跑,顶替的是状态图标(任务清单的一步、工具卡的运行中)。颜色跟着周围的文字。 */
|
|
1344
793
|
export function StatusSpinner({ size = 14, className = "" }: { size?: number; className?: string })
|
|
1345
794
|
```
|
|
1346
795
|
|
|
1347
796
|
### `ActionSpinner`
|
|
1348
797
|
|
|
1349
|
-
源码:`packages/client/ui/src/motion/loaders.tsx`
|
|
1350
|
-
|
|
1351
798
|
```tsx
|
|
1352
|
-
/**
|
|
1353
|
-
* 一段亮弧沿着一圈淡轨道跑。
|
|
1354
|
-
*
|
|
1355
|
-
* 轨道是这个记号的重点,不是装饰:亮弧单独转的话,图形的轮廓跟着弧一起走,在按钮那个方寸
|
|
1356
|
-
* 之间会读成「有东西在里面甩」。垫一圈淡的,形状就恒定是一个完整的圆,动的只有亮处。
|
|
1357
|
-
*/
|
|
799
|
+
/** 一段亮弧沿着一圈淡轨道跑:你按的那下正在回来,顶替的是刚按下的动作图标或角标。 */
|
|
1358
800
|
export function ActionSpinner({ size = 14, className = "" }: { size?: number; className?: string })
|
|
1359
801
|
```
|
|
1360
802
|
|
|
1361
803
|
### `BreatheLoader`
|
|
1362
804
|
|
|
1363
|
-
源码:`packages/client/ui/src/motion/loaders.tsx`
|
|
1364
|
-
|
|
1365
805
|
```tsx
|
|
1366
|
-
/**
|
|
1367
|
-
* 一圈波纹离开静止的中心,颜色一路走过调色板。
|
|
1368
|
-
*
|
|
1369
|
-
* 给侧栏的会话行(替一个会话报状态的地方也用它,同一个会话在两处是同一个记号)。那一列可能同时有好几行在跑,而你还要用它读标题——所以这里连「亮处在走」都不
|
|
1370
|
-
* 要:什么都不旋转、什么都不位移,余光里它只是缓缓涨落的一团。换成上面那两个的话,一列里并排
|
|
1371
|
-
* 三四个各自明灭,读标题时总有东西在旁边闪。
|
|
1372
|
-
*
|
|
1373
|
-
* 颜色走完 accent → violet 的那 2.4 秒,是它说「跑了多久」的方式:扫一眼知道它活着,
|
|
1374
|
-
* 多看一会儿知道它**还**活着,而没有任何东西变快或变响。
|
|
1375
|
-
*/
|
|
806
|
+
/** 一圈波纹离开静止的中心,颜色一路走过调色板:这个会话在跑,给侧栏的会话行和替一个会话报状态的地方。 */
|
|
1376
807
|
export function BreatheLoader({ size = 14, className = "" }: { size?: number; className?: string })
|
|
1377
808
|
```
|
|
1378
809
|
|
|
1379
810
|
### `SkeletonList`
|
|
1380
811
|
|
|
1381
|
-
源码:`packages/client/ui/src/primitives/Skeleton.tsx`
|
|
1382
|
-
|
|
1383
812
|
```tsx
|
|
1384
813
|
/** 一摞设置行,和它们代替的那个列表一致。 */
|
|
1385
814
|
export function SkeletonList({ count = 5, label = translate("common.reading") }: { count?: number; label?: string })
|
|
@@ -1387,8 +816,6 @@ export function SkeletonList({ count = 5, label = translate("common.reading") }:
|
|
|
1387
816
|
|
|
1388
817
|
### `SkeletonBar`
|
|
1389
818
|
|
|
1390
|
-
源码:`packages/client/ui/src/primitives/Skeleton.tsx`
|
|
1391
|
-
|
|
1392
819
|
```tsx
|
|
1393
820
|
/** 一块暗色的块。`width` 是任意 CSS 长度,一排这样的块看起来能像字而不是像条。 */
|
|
1394
821
|
export function SkeletonBar({ width, height = 10, className = "" }: { width: string; height?: number; className?: string })
|
|
@@ -1396,15 +823,8 @@ export function SkeletonBar({ width, height = 10, className = "" }: { width: str
|
|
|
1396
823
|
|
|
1397
824
|
### `useSlowLoad`
|
|
1398
825
|
|
|
1399
|
-
源码:`packages/client/ui/src/primitives/Skeleton.tsx`
|
|
1400
|
-
|
|
1401
826
|
```tsx
|
|
1402
|
-
/**
|
|
1403
|
-
* 一次加载是否已经久到值得画占位。
|
|
1404
|
-
*
|
|
1405
|
-
* 只出现两帧的骨架是一次闪烁,闪烁读起来是故障而不是进度——缓存热的时候一个列表 59ms 就填满了,
|
|
1406
|
-
* 其中占位只待了 32ms。阈值以下什么都不显示,这是对一次没人察觉的等待最诚实的描绘。
|
|
1407
|
-
*/
|
|
827
|
+
/** 一次加载是否已经久到值得画占位。 */
|
|
1408
828
|
export function useSlowLoad(loading: boolean, delay = 220): boolean
|
|
1409
829
|
```
|
|
1410
830
|
|
|
@@ -1412,24 +832,14 @@ export function useSlowLoad(loading: boolean, delay = 220): boolean
|
|
|
1412
832
|
|
|
1413
833
|
### `HoverRow`
|
|
1414
834
|
|
|
1415
|
-
源码:`packages/client/ui/src/row/HoverRow.tsx`
|
|
1416
|
-
|
|
1417
835
|
```tsx
|
|
1418
|
-
export function HoverRow({
|
|
1419
|
-
controls,
|
|
1420
|
-
className = "",
|
|
1421
|
-
style,
|
|
1422
|
-
children,
|
|
1423
|
-
...rest
|
|
1424
|
-
}: ComponentPropsWithoutRef<"div"> & {
|
|
836
|
+
export function HoverRow({ className = "", ...rest }: ComponentPropsWithoutRef<"div"> & {
|
|
1425
837
|
controls: string;
|
|
1426
838
|
})
|
|
1427
839
|
```
|
|
1428
840
|
|
|
1429
841
|
### `HoverRowMark`
|
|
1430
842
|
|
|
1431
|
-
源码:`packages/client/ui/src/row/HoverRow.tsx`
|
|
1432
|
-
|
|
1433
843
|
```tsx
|
|
1434
844
|
/** 行首的图标列:每一行 git 都是同一个 12px 的框,工作树不额外缩进。 */
|
|
1435
845
|
export function HoverRowMark({ className = "", children }: { className?: string; children: ReactNode })
|
|
@@ -1437,8 +847,6 @@ export function HoverRowMark({ className = "", children }: { className?: string;
|
|
|
1437
847
|
|
|
1438
848
|
### `HoverRowReveal`
|
|
1439
849
|
|
|
1440
|
-
源码:`packages/client/ui/src/row/HoverRow.tsx`
|
|
1441
|
-
|
|
1442
850
|
```tsx
|
|
1443
851
|
/** 按内容收缩的浮层。绝不做成定宽的一列:那样会把空着的框也画出来。 */
|
|
1444
852
|
export function HoverRowReveal({ className = "", children }: { className?: string; children: ReactNode })
|
|
@@ -1446,33 +854,15 @@ export function HoverRowReveal({ className = "", children }: { className?: strin
|
|
|
1446
854
|
|
|
1447
855
|
### `hoverSlot`
|
|
1448
856
|
|
|
1449
|
-
源码:`packages/client/ui/src/row/HoverRow.tsx`
|
|
1450
|
-
|
|
1451
857
|
```tsx
|
|
1452
|
-
/**
|
|
1453
|
-
* 控件条量好之前先写进 `--plume-row-controls` 的兜底值。
|
|
1454
|
-
*
|
|
1455
|
-
* 每个点击区约 21px(`p-1` 加 12.5 的图标)。左边 6px 对应右边的 `pr-1.5`,第一个图标的内距和
|
|
1456
|
-
* 最后一个一样。两个按钮的槽给 58px 时比置顶图标多出 17px——那就是多让出去的一段。
|
|
1457
|
-
*/
|
|
858
|
+
/** 控件条量好之前先写进 `--plume-row-controls` 的兜底值。 */
|
|
1458
859
|
export function hoverSlot(actions: 1 | 2 | 3): string
|
|
1459
860
|
```
|
|
1460
861
|
|
|
1461
862
|
### `Caret`
|
|
1462
863
|
|
|
1463
|
-
源码:`packages/client/ui/src/primitives/Caret.tsx`
|
|
1464
|
-
|
|
1465
864
|
```tsx
|
|
1466
|
-
export function Caret({
|
|
1467
|
-
open,
|
|
1468
|
-
/** 收起时箭头朝哪儿:`down` 展开到朝上(下拉),`right` 展开到朝下(树、折叠块)。 */
|
|
1469
|
-
from = "down",
|
|
1470
|
-
size = 12,
|
|
1471
|
-
strokeWidth = 2,
|
|
1472
|
-
className = "",
|
|
1473
|
-
/** 代码预览那种自带配色的表面上,颜色是算出来的而不是 token。 */
|
|
1474
|
-
style,
|
|
1475
|
-
}: {
|
|
865
|
+
export function Caret({ size = 12, strokeWidth = 2, className = "" }: {
|
|
1476
866
|
open: boolean;
|
|
1477
867
|
from?: "down" | "right";
|
|
1478
868
|
size?: number;
|
|
@@ -1484,16 +874,8 @@ export function Caret({
|
|
|
1484
874
|
|
|
1485
875
|
### `MarkNumber`
|
|
1486
876
|
|
|
1487
|
-
源码:`packages/client/ui/src/primitives/MarkNumber.tsx`
|
|
1488
|
-
|
|
1489
877
|
```tsx
|
|
1490
|
-
/**
|
|
1491
|
-
* 一枚圆形序号:把两处说的同一件东西对上号。
|
|
1492
|
-
*
|
|
1493
|
-
* 正文里划线的那段字末尾挂一枚,审批卡上那条评论前面挂同一个数——人看到卡上的「2」,回到正文里
|
|
1494
|
-
* 找的就是那枚「2」,不用再读一遍引文去认。实心强调色、白字、等宽数字:两位数也不变形,深浅两套底色上
|
|
1495
|
-
* 都看得清。
|
|
1496
|
-
*/
|
|
878
|
+
/** 一枚圆形序号:把两处说的同一件东西对上号。 */
|
|
1497
879
|
export function MarkNumber({ n, className = "", ...rest }: { n: number; className?: string } & Omit<HTMLAttributes<HTMLSpanElement>, "children">)
|
|
1498
880
|
```
|
|
1499
881
|
|
|
@@ -1501,22 +883,8 @@ export function MarkNumber({ n, className = "", ...rest }: { n: number; classNam
|
|
|
1501
883
|
|
|
1502
884
|
### `FlowRow`
|
|
1503
885
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
```tsx
|
|
1507
|
-
export function FlowRow({
|
|
1508
|
-
icon,
|
|
1509
|
-
title,
|
|
1510
|
-
summary,
|
|
1511
|
-
trailing,
|
|
1512
|
-
trailFrom,
|
|
1513
|
-
running,
|
|
1514
|
-
open,
|
|
1515
|
-
onToggle,
|
|
1516
|
-
label,
|
|
1517
|
-
danger = false,
|
|
1518
|
-
...rest
|
|
1519
|
-
}: {
|
|
886
|
+
```tsx
|
|
887
|
+
export function FlowRow({ danger = false, ...rest }: {
|
|
1520
888
|
/** 前置图标,常驻。它说的是这一行是什么,不参与展开、收起。 */
|
|
1521
889
|
icon: ReactNode;
|
|
1522
890
|
/** 这一行是什么(「自动压缩」)。不参与化开:装不下时先让出地方的永远该是摘要。 */
|
|
@@ -1541,8 +909,6 @@ export function FlowRow({
|
|
|
1541
909
|
|
|
1542
910
|
### `lookFor`
|
|
1543
911
|
|
|
1544
|
-
源码:`packages/client/ui/src/fileIcon.tsx`
|
|
1545
|
-
|
|
1546
912
|
```tsx
|
|
1547
913
|
export function lookFor(name: string, isDirectory: boolean, expanded = false): FileLook
|
|
1548
914
|
|
|
@@ -1556,79 +922,29 @@ export interface FileLook {
|
|
|
1556
922
|
|
|
1557
923
|
### `iconColour`
|
|
1558
924
|
|
|
1559
|
-
源码:`packages/client/ui/src/fileIcon.tsx`
|
|
1560
|
-
|
|
1561
925
|
```tsx
|
|
1562
926
|
/** 取在当前主题下读得清的那个色值。深浅色跟随系统,和 theme.css 的 `prefers-color-scheme` 一致。 */
|
|
1563
927
|
export function iconColour(look: FileLook): string
|
|
1564
|
-
|
|
1565
|
-
export interface FileLook {
|
|
1566
|
-
Icon: LucideIcon;
|
|
1567
|
-
/** 不走 Tailwind:行内设上,因为这些颜色不在主题推导的色阶里。 */
|
|
1568
|
-
light: string;
|
|
1569
|
-
dark: string;
|
|
1570
|
-
}
|
|
1571
928
|
```
|
|
1572
929
|
|
|
930
|
+
`FileLook` 见上文。
|
|
931
|
+
|
|
1573
932
|
## 滚动
|
|
1574
933
|
|
|
1575
934
|
### `Scroller`
|
|
1576
935
|
|
|
1577
|
-
源码:`packages/client/ui/src/scroll/Scroller.tsx`
|
|
1578
|
-
|
|
1579
936
|
```tsx
|
|
1580
|
-
/**
|
|
1581
|
-
|
|
1582
|
-
*
|
|
1583
|
-
* 原生滚动容器做不到、而参考设计处处都有的几件事:
|
|
1584
|
-
*
|
|
1585
|
-
* - 滚动条浮在内容上,而不是预留一条槽,列表一长过它的盒子,内容不会横向重排;
|
|
1586
|
-
* - 边缘说明那个方向还藏着东西,画成边界实际的样子:内容化进空白的地方是渐隐,滑到实心的东西
|
|
1587
|
-
* 底下的地方是一道细线。见 `top`——选错了,列表看起来就像坏了。
|
|
1588
|
-
*
|
|
1589
|
-
* `overscroll-contain` 让滚到头的滚轮停在这里,而不是传给后面的窗口——否则嵌套列表一到底就会带着
|
|
1590
|
-
* 父级一起滚。
|
|
1591
|
-
*
|
|
1592
|
-
* 嵌在外层滚动面里的面(比如转录里的工具卡片、错误日志、代码输出),用 `overscroll="auto"`,
|
|
1593
|
-
* 里面滚到边界之后自然地接到外层。
|
|
1594
|
-
*/
|
|
1595
|
-
export function Scroller({
|
|
1596
|
-
children,
|
|
1597
|
-
className = "",
|
|
1598
|
-
contentClassName = "",
|
|
1599
|
-
top = "fade",
|
|
1600
|
-
fadeTop,
|
|
1601
|
-
bottom = "fade",
|
|
1602
|
-
overscroll = "contain",
|
|
1603
|
-
onScroll,
|
|
1604
|
-
onResize,
|
|
1605
|
-
onUserScroll,
|
|
1606
|
-
onHold,
|
|
1607
|
-
scrollRef,
|
|
1608
|
-
}: {
|
|
937
|
+
/** 应用里唯一的滚动面。 */
|
|
938
|
+
export function Scroller({ className = "", contentClassName = "", top = "fade", bottom = "fade", overscroll = "contain" }: {
|
|
1609
939
|
children: React.ReactNode;
|
|
1610
940
|
className?: string;
|
|
1611
941
|
contentClassName?: string;
|
|
1612
942
|
/**
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
* 判断标准是这一块**是页面里的一段内容,还是一块独立的面**。一段内容——设置页里的发版说明、一列
|
|
1616
|
-
* 搜索结果——读到底了还想接着往下看页面,是很自然的一件事,那就该给 `"auto"`。一块
|
|
1617
|
-
* 独立的面——弹窗、浮层、菜单、转录本身——滚到底就该停住,不然一个嵌套列表刚到底就把它背后的整页
|
|
1618
|
-
* 带跑了,那是默认的 `"contain"` 要挡的事。
|
|
1619
|
-
*
|
|
1620
|
-
* 「滚不动的面要不要拦」不用在这里操心:它一律放行,见下面 `chain` 那段。
|
|
943
|
+
* 滚到自己的边界之后,滚轮还传不传给外面。页面里的一段内容(嵌在外层滚动面里的工具卡片、代码输出、
|
|
944
|
+
* 一列搜索结果)给 `"auto"`;独立的面(弹窗、浮层、菜单)用默认的 `"contain"`。
|
|
1621
945
|
*/
|
|
1622
946
|
overscroll?: "contain" | "auto";
|
|
1623
|
-
/**
|
|
1624
|
-
* 上沿怎么收尾。
|
|
1625
|
-
*
|
|
1626
|
-
* `"fade"` 把最后几行柔化掉,不画任何东西就说出「这边还有」。上面是什么都成立——空窗格也好、
|
|
1627
|
-
* 不透明的导航也好——因为它降的是内容自己的 alpha,而不是往上面刷颜色;为什么这一点就是全部,
|
|
1628
|
-
* 见 `ui/scroll.css` 里 `.plume-fade-y` 上面那段。
|
|
1629
|
-
*
|
|
1630
|
-
* 只在确实有东西滚到底下去之后才出现,短列表完全没有边缘处理:边界在起作用时才值得画。
|
|
1631
|
-
*/
|
|
947
|
+
/** 上沿怎么收尾。 */
|
|
1632
948
|
top?: "fade" | "none";
|
|
1633
949
|
/** 默认值不适合这个列表时,上沿渐隐伸进去多深,单位 px。 */
|
|
1634
950
|
fadeTop?: number;
|
|
@@ -1637,20 +953,9 @@ export function Scroller({
|
|
|
1637
953
|
onScroll?: (element: HTMLDivElement) => void;
|
|
1638
954
|
/** 内容或视口尺寸变了就调用。 */
|
|
1639
955
|
onResize?: (element: HTMLDivElement) => void;
|
|
1640
|
-
/**
|
|
1641
|
-
* 下面滑块的一次拖动,按它本来的手势报出来。
|
|
1642
|
-
*
|
|
1643
|
-
* 滑块是视口的兄弟节点,靠赋值 `scrollTop` 移动视口,于是拖动传到跟随方那里,是一个和浏览器
|
|
1644
|
-
* 夹住正在变短的转录分不开的滚动事件——而跟随方不能因为一次来路不明的位置变化就脱离(见
|
|
1645
|
-
* `follow.ts`),就永远发现不了读者在往外拉。这是唯一知道真相的组件把那个手势明说出来。
|
|
1646
|
-
*/
|
|
956
|
+
/** 下面滑块的一次拖动,按它本来的手势报出来。 */
|
|
1647
957
|
onUserScroll?: (direction: Direction) => void;
|
|
1648
|
-
/**
|
|
1649
|
-
* 读者刚把手指按在了什么上面。
|
|
1650
|
-
*
|
|
1651
|
-
* 给跟随底部的那一方一个机会,把接下来这一次长高认成「读者点开了手里这一块」而不是「新消息
|
|
1652
|
-
* 到了」——两者在 `onResize` 里长得一模一样,而处理方式正好相反。
|
|
1653
|
-
*/
|
|
958
|
+
/** 读者刚把手指按在了什么上面。 */
|
|
1654
959
|
onHold?: (target: EventTarget | null) => void;
|
|
1655
960
|
/** 给自己驱动滚动位置的调用方用,比如转录。 */
|
|
1656
961
|
scrollRef?: React.RefObject<HTMLDivElement | null>;
|
|
@@ -1659,14 +964,8 @@ export function Scroller({
|
|
|
1659
964
|
|
|
1660
965
|
### `OverlayScrollbar`
|
|
1661
966
|
|
|
1662
|
-
源码:`packages/client/ui/src/scroll/OverlayScrollbar.tsx`
|
|
1663
|
-
|
|
1664
967
|
```tsx
|
|
1665
|
-
export function OverlayScrollbar({
|
|
1666
|
-
viewport,
|
|
1667
|
-
orientation,
|
|
1668
|
-
placement = "overlay",
|
|
1669
|
-
}: {
|
|
968
|
+
export function OverlayScrollbar({ placement = "overlay" }: {
|
|
1670
969
|
viewport: React.RefObject<HTMLElement | null>;
|
|
1671
970
|
orientation: "vertical" | "horizontal";
|
|
1672
971
|
/**
|
|
@@ -1679,49 +978,20 @@ export function OverlayScrollbar({
|
|
|
1679
978
|
|
|
1680
979
|
### `ScrollText`
|
|
1681
980
|
|
|
1682
|
-
源码:`packages/client/ui/src/scroll/ScrollText.tsx`
|
|
1683
|
-
|
|
1684
981
|
```tsx
|
|
1685
|
-
/**
|
|
1686
|
-
|
|
1687
|
-
*
|
|
1688
|
-
* 省略号只报告标题被截了,不说截掉的是什么——每个以「帮我梳理这个项目的整体架构」开头的会话都缩成
|
|
1689
|
-
* 同一行没法用的字。渐隐说的是同一件事,但更安静,悬停滚动则真正回答了那个问题。
|
|
1690
|
-
*
|
|
1691
|
-
* 滚动只朝一个方向、连续地走。左右来回扫读起来像这一行在抽搐,而不是字在被读:眼睛得一次次重新
|
|
1692
|
-
* 找到开头。第一份后面跟着一份隐藏的副本,单向才能无缝循环——原文离开时,副本正好落在它开始的位置。
|
|
1693
|
-
*
|
|
1694
|
-
* 只有字真的读不全时才动:已经装得下的行,指针划过时不能抖。父级行带着 `plume-scroll`,悬停规则
|
|
1695
|
-
* 认的就是它。
|
|
1696
|
-
*/
|
|
1697
|
-
export function ScrollText({
|
|
1698
|
-
text,
|
|
1699
|
-
className = "",
|
|
1700
|
-
textClassName = "",
|
|
1701
|
-
}: {
|
|
982
|
+
/** 一行字,到边上渐隐而不是以省略号结尾,所在的行被悬停时自己读出来:那一行(父级)要带 `plume-scroll` 类。 */
|
|
983
|
+
export function ScrollText({ className = "", textClassName = "" }: {
|
|
1702
984
|
text: ReactNode;
|
|
1703
985
|
className?: string;
|
|
1704
|
-
/**
|
|
1705
|
-
* 加在装字形的那几个 span 上——正文和它循环用的副本——不加在盒子上。
|
|
1706
|
-
*
|
|
1707
|
-
* 给那些透过字本身画出来的效果用(`background-clip: text`、运行中的流光)。加在盒子上的话,它按
|
|
1708
|
-
* 盒子自己那一层的字形裁剪,而跑马灯的 transform 把轨道抬进了单独的一层:悬停时整行变成空白。
|
|
1709
|
-
*/
|
|
986
|
+
/** 加在装字形的那几个 span 上——正文和它循环用的副本——不加在盒子上。 */
|
|
1710
987
|
textClassName?: string;
|
|
1711
988
|
})
|
|
1712
989
|
```
|
|
1713
990
|
|
|
1714
991
|
### `Sideways`
|
|
1715
992
|
|
|
1716
|
-
源码:`packages/client/ui/src/scroll/Sideways.tsx`
|
|
1717
|
-
|
|
1718
993
|
```tsx
|
|
1719
|
-
export function Sideways({
|
|
1720
|
-
children,
|
|
1721
|
-
className = "",
|
|
1722
|
-
trackRef,
|
|
1723
|
-
...rest
|
|
1724
|
-
}: {
|
|
994
|
+
export function Sideways({ className = "", ...rest }: {
|
|
1725
995
|
children: React.ReactNode;
|
|
1726
996
|
/** 给会滚的那一层,`overflow-x-auto` 和「我在父级里占多大」的类都写在这里。 */
|
|
1727
997
|
className?: string;
|
|
@@ -1734,8 +1004,6 @@ export function Sideways({
|
|
|
1734
1004
|
|
|
1735
1005
|
### `useI18n`
|
|
1736
1006
|
|
|
1737
|
-
源码:`packages/client/ui/src/i18n/context.tsx`
|
|
1738
|
-
|
|
1739
1007
|
```tsx
|
|
1740
1008
|
/** 当前界面语言和产品的文案目录。插件只用 `resolvedLocale` 选自己 `messages.ts` 里的那一份;`t()` 查的是产品的目录,插件的文字不在里面。 */
|
|
1741
1009
|
export function useI18n(): I18nValue
|
|
@@ -1753,8 +1021,6 @@ interface I18nValue {
|
|
|
1753
1021
|
|
|
1754
1022
|
### `relativeTime`
|
|
1755
1023
|
|
|
1756
|
-
源码:`packages/client/ui/src/lib/relative-time.ts`
|
|
1757
|
-
|
|
1758
1024
|
```tsx
|
|
1759
1025
|
/** 故意说得粗:列表里没人关心一次提交的确切分钟。 */
|
|
1760
1026
|
export function relativeTime(iso: string, now = Date.now()): string
|
|
@@ -1762,8 +1028,6 @@ export function relativeTime(iso: string, now = Date.now()): string
|
|
|
1762
1028
|
|
|
1763
1029
|
### `TimeAgo`
|
|
1764
1030
|
|
|
1765
|
-
源码:`packages/client/ui/src/primitives/TimeAgo.tsx`
|
|
1766
|
-
|
|
1767
1031
|
```tsx
|
|
1768
1032
|
export function TimeAgo({ iso, className = "" }: { iso: string; className?: string })
|
|
1769
1033
|
```
|
|
@@ -1772,16 +1036,12 @@ export function TimeAgo({ iso, className = "" }: { iso: string; className?: stri
|
|
|
1772
1036
|
|
|
1773
1037
|
### `macKeyboard`
|
|
1774
1038
|
|
|
1775
|
-
源码:`packages/client/ui/src/keyboard.ts`
|
|
1776
|
-
|
|
1777
1039
|
```tsx
|
|
1778
1040
|
export function macKeyboard(platform = navigator.platform): boolean
|
|
1779
1041
|
```
|
|
1780
1042
|
|
|
1781
1043
|
### `composingKey`
|
|
1782
1044
|
|
|
1783
|
-
源码:`packages/client/ui/src/keyboard.ts`
|
|
1784
|
-
|
|
1785
1045
|
```tsx
|
|
1786
1046
|
/** 这些键归输入法和 AltGr;当成命令处理的话,打字打到一半就切了窗格。 */
|
|
1787
1047
|
export function composingKey(event: KeyboardEvent): boolean
|
|
@@ -1789,26 +1049,13 @@ export function composingKey(event: KeyboardEvent): boolean
|
|
|
1789
1049
|
|
|
1790
1050
|
### `shortcutLetter`
|
|
1791
1051
|
|
|
1792
|
-
源码:`packages/client/ui/src/keyboard.ts`
|
|
1793
|
-
|
|
1794
1052
|
```tsx
|
|
1795
|
-
/**
|
|
1796
|
-
* 快捷键按的是哪个字母:键帽上印的那个。
|
|
1797
|
-
*
|
|
1798
|
-
* `key` 是拉丁字母时用 `key`。QWERTY、AZERTY、QWERTZ、Dvorak 上它就是键帽上的字母,也就是快捷键的
|
|
1799
|
-
* 写法;改用 `code` 匹配的话,AZERTY 的 Ctrl+Alt+A 会变成印着 Q 的那个键,Dvorak 的 Ctrl+B 变成印着 X 的。
|
|
1800
|
-
*
|
|
1801
|
-
* 否则用 `code`。没有拉丁字母的布局(俄语、希腊语、希伯来语)没有印着 A 的键可按,用户会去按 A 所在
|
|
1802
|
-
* 位置的那个键——浏览器自己的快捷键和 CodeMirror 的键位表都是这样兜底的。macOS 的 Option 也落到这里:
|
|
1803
|
-
* 它是死键修饰,⌥S 到达时是「ß」,只有位置还说明它是 S。
|
|
1804
|
-
*/
|
|
1053
|
+
/** 快捷键按的是哪个字母:键帽上印的那个。 */
|
|
1805
1054
|
export function shortcutLetter(event: Pick<KeyboardEvent, "key" | "code">): string | null
|
|
1806
1055
|
```
|
|
1807
1056
|
|
|
1808
1057
|
### `shortcutLabel`
|
|
1809
1058
|
|
|
1810
|
-
源码:`packages/client/ui/src/keyboard.ts`
|
|
1811
|
-
|
|
1812
1059
|
```tsx
|
|
1813
1060
|
/** 界面文案一律用紧凑的 Mac 写法;别的键盘要把修饰键写成名字。 */
|
|
1814
1061
|
export function shortcutLabel(text: string, platform = navigator.platform): string
|
|
@@ -1816,8 +1063,6 @@ export function shortcutLabel(text: string, platform = navigator.platform): stri
|
|
|
1816
1063
|
|
|
1817
1064
|
### `acceleratorLabel`
|
|
1818
1065
|
|
|
1819
|
-
源码:`packages/client/ui/src/keyboard.ts`
|
|
1820
|
-
|
|
1821
1066
|
```tsx
|
|
1822
1067
|
/** 把 Electron 的 accelerator 写成按键文字,不把 Control、Command 和 Windows 键混为一谈。 */
|
|
1823
1068
|
export function acceleratorLabel(accelerator: string, platform = navigator.platform): string
|
|
@@ -1827,24 +1072,14 @@ export function acceleratorLabel(accelerator: string, platform = navigator.platf
|
|
|
1827
1072
|
|
|
1828
1073
|
### `motionReduced`
|
|
1829
1074
|
|
|
1830
|
-
源码:`packages/client/ui/src/motion/reduced.ts`
|
|
1831
|
-
|
|
1832
1075
|
```tsx
|
|
1833
1076
|
export function motionReduced(): boolean
|
|
1834
1077
|
```
|
|
1835
1078
|
|
|
1836
1079
|
### `CountUp`
|
|
1837
1080
|
|
|
1838
|
-
源码:`packages/client/ui/src/primitives/CountUp.tsx`
|
|
1839
|
-
|
|
1840
1081
|
```tsx
|
|
1841
|
-
export function CountUp({
|
|
1842
|
-
value,
|
|
1843
|
-
className,
|
|
1844
|
-
/** 走动中的数怎么写。默认是四舍五入的整数。 */
|
|
1845
|
-
format = plain,
|
|
1846
|
-
...rest
|
|
1847
|
-
}: {
|
|
1082
|
+
export function CountUp({ ...rest }: {
|
|
1848
1083
|
value: number;
|
|
1849
1084
|
className?: string;
|
|
1850
1085
|
format?: (shown: number) => string;
|
|
@@ -1853,50 +1088,16 @@ export function CountUp({
|
|
|
1853
1088
|
|
|
1854
1089
|
### `useRolled`
|
|
1855
1090
|
|
|
1856
|
-
源码:`packages/client/ui/src/motion/RollingText.tsx`
|
|
1857
|
-
|
|
1858
1091
|
```tsx
|
|
1859
|
-
/**
|
|
1860
|
-
* 这个值是否被替换过——首次绘制时是 false,从第一次变化起是 true。
|
|
1861
|
-
*
|
|
1862
|
-
* 有两件事靠它做对,而它们往相反的方向拉。
|
|
1863
|
-
*
|
|
1864
|
-
* 首次绘制不能动。挂上就滚的话,菜单一打开每个标签都落进位,叠在菜单自己的进场上——两个不同方向的动作,
|
|
1865
|
-
* 读起来像面板在你面前被拼起来。
|
|
1866
|
-
*
|
|
1867
|
-
* 可类也不能来来去去,这一点更隐蔽:给元素加上 `animation` 就会**开始**那个动画。一个在下一次无关渲染时
|
|
1868
|
-
* 退回 false 的标记,会在再下一次渲染时把类加回来,于是输入框里任何一个标签一变,所有标签都滚一遍。所以它
|
|
1869
|
-
* 是锁存的:一旦为 true 就一直是 true,此后动画完全由 `key` 变化引起的重新挂载来驱动。
|
|
1870
|
-
*
|
|
1871
|
-
* 在渲染期间调整状态,是从一个变了的 prop 推导状态的官方做法——React 丢掉这一遍、立刻重跑组件,于是类和新
|
|
1872
|
-
* key 落在同一次提交里,元素挂上时就已经在动。
|
|
1873
|
-
*/
|
|
1092
|
+
/** 这个值是否被替换过——首次绘制时是 false,从第一次变化起是 true。 */
|
|
1874
1093
|
export function useRolled(value: string | number): boolean
|
|
1875
1094
|
```
|
|
1876
1095
|
|
|
1877
1096
|
### `RollingText`
|
|
1878
1097
|
|
|
1879
|
-
源码:`packages/client/ui/src/motion/RollingText.tsx`
|
|
1880
|
-
|
|
1881
1098
|
```tsx
|
|
1882
|
-
/**
|
|
1883
|
-
|
|
1884
|
-
*
|
|
1885
|
-
* 原地变化的字——输入框里的推理强度、刚选的模型、权限模式、「展开显示」变成「收起」——不滚的话,眼睛除了
|
|
1886
|
-
* 字形不同以外收不到任何「发生了什么」的信号:在正拖着的控件上读起来像渲染瑕疵,在刚点过的控件上读起来像
|
|
1887
|
-
* 那一下点了没用。
|
|
1888
|
-
*
|
|
1889
|
-
* 动作是 `ui/motion.css` 里的 `.plume-roll`——总是往下,一连串的变化读起来是一个东西在被拨动,而不是字在闪。
|
|
1890
|
-
* 有了这个组件,用上它就是选对元素的事,而不必记住一个两步的写法(`key={value}` 加 `plume-roll`)。
|
|
1891
|
-
*
|
|
1892
|
-
* 给标签用,不给读数用。会跳的数——token 数、计时器、百分比——是在连续变化而不是被替换,每跳一下滚一次,
|
|
1893
|
-
* 一块安静的仪表就成了整轮里一直在余光里动的东西。那些用 `tabular-nums` 定住。
|
|
1894
|
-
*/
|
|
1895
|
-
export function RollingText({
|
|
1896
|
-
children,
|
|
1897
|
-
className = "",
|
|
1898
|
-
rollKey,
|
|
1899
|
-
}: {
|
|
1099
|
+
/** 一个值替换另一个值,不管发生在哪里。给会换成另一个值的标签用,不给连续跳的读数用。 */
|
|
1100
|
+
export function RollingText({ className = "" }: {
|
|
1900
1101
|
children: string | number;
|
|
1901
1102
|
/** 落在外层元素上,它默认是 `inline`——需要的地方给它 `block`。 */
|
|
1902
1103
|
className?: string;
|
|
@@ -1907,30 +1108,16 @@ export function RollingText({
|
|
|
1907
1108
|
|
|
1908
1109
|
### `useNow`
|
|
1909
1110
|
|
|
1910
|
-
源码:`packages/client/ui/src/motion/useNow.ts`
|
|
1911
|
-
|
|
1912
1111
|
```tsx
|
|
1913
|
-
/**
|
|
1914
|
-
* 此刻的时间,`active` 时每 `every` 毫秒重画一次:给秒表、倒计时这类在走的读数用,不走的时候一次也不重画。
|
|
1915
|
-
*
|
|
1916
|
-
* 变成 `active` 的那一下先对一次表:不对的话,挂上以后过了很久才开始走的读数,头一拍显示的是挂上那一刻。
|
|
1917
|
-
*/
|
|
1112
|
+
/** 此刻的时间,`active` 时每 `every` 毫秒重画一次:给秒表、倒计时这类在走的读数用,不走的时候一次也不重画。 */
|
|
1918
1113
|
export function useNow(active: boolean, every: number): number
|
|
1919
1114
|
```
|
|
1920
1115
|
|
|
1921
1116
|
### `Collapse`
|
|
1922
1117
|
|
|
1923
|
-
源码:`packages/client/ui/src/layout/Collapse.tsx`
|
|
1924
|
-
|
|
1925
1118
|
```tsx
|
|
1926
1119
|
/** 收起时的内容留到尺寸过渡走完,再放掉开销大的子树。 */
|
|
1927
|
-
export function Collapse({
|
|
1928
|
-
open,
|
|
1929
|
-
children,
|
|
1930
|
-
bodyClassName,
|
|
1931
|
-
className = "",
|
|
1932
|
-
keepMounted = false,
|
|
1933
|
-
}: {
|
|
1120
|
+
export function Collapse({ className = "", keepMounted = false }: {
|
|
1934
1121
|
open: boolean;
|
|
1935
1122
|
children: ReactNode;
|
|
1936
1123
|
bodyClassName?: string;
|
|
@@ -1943,17 +1130,8 @@ export function Collapse({
|
|
|
1943
1130
|
|
|
1944
1131
|
### `Disclosure`
|
|
1945
1132
|
|
|
1946
|
-
源码:`packages/client/ui/src/layout/Disclosure.tsx`
|
|
1947
|
-
|
|
1948
1133
|
```tsx
|
|
1949
|
-
export function Disclosure({
|
|
1950
|
-
title,
|
|
1951
|
-
count,
|
|
1952
|
-
trailing,
|
|
1953
|
-
children,
|
|
1954
|
-
variant = "section",
|
|
1955
|
-
...control
|
|
1956
|
-
}: {
|
|
1134
|
+
export function Disclosure({ variant = "section", ...control }: {
|
|
1957
1135
|
title: ReactNode;
|
|
1958
1136
|
/** 显示在标题后面。零值得说;undefined 不说。 */
|
|
1959
1137
|
count?: number;
|
|
@@ -1964,31 +1142,9 @@ export function Disclosure({
|
|
|
1964
1142
|
} & Control)
|
|
1965
1143
|
|
|
1966
1144
|
const LOOK = {
|
|
1967
|
-
section:
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
button: "py-2.5",
|
|
1971
|
-
title: "text-label font-medium",
|
|
1972
|
-
body: "pb-3",
|
|
1973
|
-
},
|
|
1974
|
-
compact: {
|
|
1975
|
-
root: "",
|
|
1976
|
-
head: "",
|
|
1977
|
-
button: "py-1",
|
|
1978
|
-
title: "text-detail",
|
|
1979
|
-
body: "pl-3.5",
|
|
1980
|
-
},
|
|
1981
|
-
framed: {
|
|
1982
|
-
root: "overflow-hidden rounded-lg border border-line-soft",
|
|
1983
|
-
head: "pr-3",
|
|
1984
|
-
button: "py-2 pl-3",
|
|
1985
|
-
/*
|
|
1986
|
-
* 没有自己的字号。framed 的折叠坐在页面已经定好字号的正文里——README 按 label 字号、消息按 body
|
|
1987
|
-
* 字号——标题和周围的字差一档,读起来就像贴进来的第二份文档。
|
|
1988
|
-
*/
|
|
1989
|
-
title: "",
|
|
1990
|
-
body: "px-3 pb-3",
|
|
1991
|
-
},
|
|
1145
|
+
section: …,
|
|
1146
|
+
compact: …,
|
|
1147
|
+
framed: …,
|
|
1992
1148
|
} as const;
|
|
1993
1149
|
|
|
1994
1150
|
type Control = { open: boolean; onToggle: () => void; defaultOpen?: never } | { open?: never; onToggle?: never; defaultOpen?: boolean };
|
|
@@ -1998,8 +1154,6 @@ type Control = { open: boolean; onToggle: () => void; defaultOpen?: never } | {
|
|
|
1998
1154
|
|
|
1999
1155
|
### `AgentAvatar`
|
|
2000
1156
|
|
|
2001
|
-
源码:`packages/client/ui/src/avatar/AgentAvatar.tsx`
|
|
2002
|
-
|
|
2003
1157
|
```tsx
|
|
2004
1158
|
export function AgentAvatar({ avatar, size = 20, mood = "idle", interactive = true, host, seed = "", label, cheer = null, className = "" }: AgentAvatarProps)
|
|
2005
1159
|
|
|
@@ -2008,12 +1162,9 @@ export interface AgentAvatarProps {
|
|
|
2008
1162
|
/** 像素边长。 */
|
|
2009
1163
|
size?: number;
|
|
2010
1164
|
mood?: AvatarMood;
|
|
2011
|
-
/**
|
|
2012
|
-
* 悬停时眼睛跟着指针、身子弹一下。默认开。
|
|
2013
|
-
*
|
|
2014
|
-
* `host` 是往上找的那个容器:一整行的任何地方悬停都算,而不是非得对准这 20 像素。
|
|
2015
|
-
*/
|
|
1165
|
+
/** 悬停时眼睛跟着指针、身子弹一下。默认开。 */
|
|
2016
1166
|
interactive?: boolean;
|
|
1167
|
+
/** 往上找的那个容器的选择器(`closest`):在它里面任何地方悬停都算,而不是非得对准这 20 像素。 */
|
|
2017
1168
|
host?: string;
|
|
2018
1169
|
/** 名字,决定眨眼的节奏——七张脸不该踩着同一个拍子。 */
|
|
2019
1170
|
seed?: string;
|
|
@@ -2023,29 +1174,18 @@ export interface AgentAvatarProps {
|
|
|
2023
1174
|
cheer?: string | number | null;
|
|
2024
1175
|
className?: string;
|
|
2025
1176
|
}
|
|
1177
|
+
|
|
1178
|
+
export type AvatarMood = "idle" | DispatchState;
|
|
2026
1179
|
```
|
|
2027
1180
|
|
|
2028
1181
|
### `AvatarPile`
|
|
2029
1182
|
|
|
2030
|
-
源码:`packages/client/ui/src/avatar/AvatarPile.tsx`
|
|
2031
|
-
|
|
2032
1183
|
```tsx
|
|
2033
|
-
export function AvatarPile({
|
|
2034
|
-
faces,
|
|
2035
|
-
size = 22,
|
|
2036
|
-
max = 3,
|
|
2037
|
-
focus,
|
|
2038
|
-
className = "",
|
|
2039
|
-
}: {
|
|
1184
|
+
export function AvatarPile({ size = 22, max = 3, className = "" }: {
|
|
2040
1185
|
faces: PileFace[];
|
|
2041
1186
|
size?: number;
|
|
2042
1187
|
max?: number;
|
|
2043
|
-
/**
|
|
2044
|
-
* 要完整露出来的那一张(面板顶上:正在看的那个)。
|
|
2045
|
-
*
|
|
2046
|
-
* 它排在露出来的那几张的最后——也就是最上面,紧挨着旁边写它名字的标题。放不下的时候先让出别人的
|
|
2047
|
-
* 位置,它自己一定在。
|
|
2048
|
-
*/
|
|
1188
|
+
/** 要完整露出来的那一张(面板顶上:正在看的那个)。 */
|
|
2049
1189
|
focus?: string;
|
|
2050
1190
|
className?: string;
|
|
2051
1191
|
})
|
|
@@ -2060,18 +1200,8 @@ export interface PileFace {
|
|
|
2060
1200
|
|
|
2061
1201
|
### `assignAvatars`
|
|
2062
1202
|
|
|
2063
|
-
源码:`packages/client/ui/src/lib/agent-avatar.ts`
|
|
2064
|
-
|
|
2065
1203
|
```tsx
|
|
2066
|
-
/**
|
|
2067
|
-
* 一份名单里每个人的脸,保证互不相同。
|
|
2068
|
-
*
|
|
2069
|
-
* 写明了的先落座(定义里的 `avatar`,其次是 `fallback` 给的,内置的那几个就走这条),剩下的
|
|
2070
|
-
* **按名字排好序**再逐个算——排序是为了让结果只取决于名单里有谁,不取决于名单是按什么顺序
|
|
2071
|
-
* 送来的:设置页和提及菜单拿到的是同一批人、不同的顺序,两边必须画出同一张脸。
|
|
2072
|
-
*
|
|
2073
|
-
* 写明了的两个撞了,照写的来:那是人自己挑的。
|
|
2074
|
-
*/
|
|
1204
|
+
/** 一份名单里每个人的脸,保证互不相同。 */
|
|
2075
1205
|
export function assignAvatars(
|
|
2076
1206
|
agents: readonly { name: string; avatar?: string }[],
|
|
2077
1207
|
fallback: (name: string) => string | undefined = () => undefined,
|
|
@@ -2081,62 +1211,26 @@ export interface Avatar {
|
|
|
2081
1211
|
shape: AvatarShape;
|
|
2082
1212
|
color: AvatarColor;
|
|
2083
1213
|
}
|
|
1214
|
+
|
|
1215
|
+
export type AvatarShape = (typeof AVATAR_SHAPES)[number];
|
|
1216
|
+
|
|
1217
|
+
export type AvatarColor = (typeof AVATAR_COLORS)[number];
|
|
2084
1218
|
```
|
|
2085
1219
|
|
|
2086
1220
|
### `AVATAR_SHAPES`
|
|
2087
1221
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
```tsx
|
|
2091
|
-
export const AVATAR_SHAPES = [
|
|
2092
|
-
"circle",
|
|
2093
|
-
"drop",
|
|
2094
|
-
"squircle",
|
|
2095
|
-
"blob",
|
|
2096
|
-
"cloud",
|
|
2097
|
-
"pill",
|
|
2098
|
-
"triangle",
|
|
2099
|
-
"flower",
|
|
2100
|
-
"hexagon",
|
|
2101
|
-
"ghost",
|
|
2102
|
-
"star",
|
|
2103
|
-
"bean",
|
|
2104
|
-
"arch",
|
|
2105
|
-
"diamond",
|
|
2106
|
-
"heart",
|
|
2107
|
-
"burst",
|
|
2108
|
-
] as const;
|
|
1222
|
+
```tsx
|
|
1223
|
+
export const AVATAR_SHAPES = ["circle", "drop", "squircle", "blob", "cloud", "pill", "triangle", "flower", "hexagon", "ghost", "star", "bean", "arch", "diamond", "heart", "burst"] as const;
|
|
2109
1224
|
```
|
|
2110
1225
|
|
|
2111
1226
|
### `AVATAR_COLORS`
|
|
2112
1227
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
```tsx
|
|
2116
|
-
export const AVATAR_COLORS = [
|
|
2117
|
-
"blue",
|
|
2118
|
-
"green",
|
|
2119
|
-
"pink",
|
|
2120
|
-
"teal",
|
|
2121
|
-
"violet",
|
|
2122
|
-
"brown",
|
|
2123
|
-
"orange",
|
|
2124
|
-
"red",
|
|
2125
|
-
"yellow",
|
|
2126
|
-
"indigo",
|
|
2127
|
-
"lime",
|
|
2128
|
-
"sky",
|
|
2129
|
-
"rose",
|
|
2130
|
-
"mint",
|
|
2131
|
-
"coral",
|
|
2132
|
-
"plum",
|
|
2133
|
-
] as const;
|
|
1228
|
+
```tsx
|
|
1229
|
+
export const AVATAR_COLORS = ["blue", "green", "pink", "teal", "violet", "brown", "orange", "red", "yellow", "indigo", "lime", "sky", "rose", "mint", "coral", "plum"] as const;
|
|
2134
1230
|
```
|
|
2135
1231
|
|
|
2136
1232
|
### `COLOR_ART`
|
|
2137
1233
|
|
|
2138
|
-
源码:`packages/client/ui/src/avatar/art.ts`
|
|
2139
|
-
|
|
2140
1234
|
```tsx
|
|
2141
1235
|
/**
|
|
2142
1236
|
* 色值是亮暗两种主题共用的一套中等明度:眼睛是深色的,身体太暗眼睛就看不见,太浅在白底上
|
|
@@ -2164,57 +1258,34 @@ export const COLOR_ART: Record<AvatarColor, string> = {
|
|
|
2164
1258
|
|
|
2165
1259
|
### `parseAvatar`
|
|
2166
1260
|
|
|
2167
|
-
源码:`packages/client/ui/src/lib/agent-avatar.ts`
|
|
2168
|
-
|
|
2169
1261
|
```tsx
|
|
2170
1262
|
/** `cloud-violet` → `{ shape: "cloud", color: "violet" }`;认不出来的一律 `null`,由调用方兜底。 */
|
|
2171
1263
|
export function parseAvatar(value: unknown): Avatar | null
|
|
2172
|
-
|
|
2173
|
-
export interface Avatar {
|
|
2174
|
-
shape: AvatarShape;
|
|
2175
|
-
color: AvatarColor;
|
|
2176
|
-
}
|
|
2177
1264
|
```
|
|
2178
1265
|
|
|
2179
|
-
|
|
1266
|
+
`Avatar` 见上文。
|
|
2180
1267
|
|
|
2181
|
-
|
|
1268
|
+
### `formatAvatar`
|
|
2182
1269
|
|
|
2183
1270
|
```tsx
|
|
2184
1271
|
export function formatAvatar(avatar: Avatar): string
|
|
2185
|
-
|
|
2186
|
-
export interface Avatar {
|
|
2187
|
-
shape: AvatarShape;
|
|
2188
|
-
color: AvatarColor;
|
|
2189
|
-
}
|
|
2190
1272
|
```
|
|
2191
1273
|
|
|
2192
|
-
|
|
1274
|
+
`Avatar` 见上文。
|
|
2193
1275
|
|
|
2194
|
-
|
|
1276
|
+
### `freshAvatar`
|
|
2195
1277
|
|
|
2196
1278
|
```tsx
|
|
2197
|
-
/**
|
|
2198
|
-
* 给一个新智能体挑一张没人用过的脸。
|
|
2199
|
-
*
|
|
2200
|
-
* 不只是「这个组合没人用」:形状和颜色能都不撞就都不撞。内置的几个已经把圆、水滴、方块……和
|
|
2201
|
-
* 蓝、绿、粉……占了,新来的要是个绿色的圆,组合是新的,一眼看过去却像 `general` 换了件衣服。
|
|
2202
|
-
* 两样都新的挑不出来,退一步只要形状新,再退一步只要组合新。
|
|
2203
|
-
*/
|
|
1279
|
+
/** 给一个新智能体挑一张没人用过的脸。 */
|
|
2204
1280
|
export function freshAvatar(taken: Iterable<Avatar>, random: () => number = Math.random): Avatar
|
|
2205
|
-
|
|
2206
|
-
export interface Avatar {
|
|
2207
|
-
shape: AvatarShape;
|
|
2208
|
-
color: AvatarColor;
|
|
2209
|
-
}
|
|
2210
1281
|
```
|
|
2211
1282
|
|
|
1283
|
+
`Avatar` 见上文。
|
|
1284
|
+
|
|
2212
1285
|
## 字体
|
|
2213
1286
|
|
|
2214
1287
|
### `withHanFallback`
|
|
2215
1288
|
|
|
2216
|
-
源码:`packages/client/ui/src/lib/code/font-stack.ts`
|
|
2217
|
-
|
|
2218
1289
|
```tsx
|
|
2219
1290
|
export function withHanFallback(stack: string): string
|
|
2220
1291
|
```
|
|
@@ -2223,8 +1294,6 @@ export function withHanFallback(stack: string): string
|
|
|
2223
1294
|
|
|
2224
1295
|
### `useAnnotator`
|
|
2225
1296
|
|
|
2226
|
-
源码:`packages/client/ui/src/image/Annotator.tsx`
|
|
2227
|
-
|
|
2228
1297
|
```tsx
|
|
2229
1298
|
export function useAnnotator(src: string | RawPixels | null, options?: AnnotatorOptions): Annotator
|
|
2230
1299
|
|
|
@@ -2235,21 +1304,9 @@ export interface RawPixels {
|
|
|
2235
1304
|
}
|
|
2236
1305
|
|
|
2237
1306
|
export interface AnnotatorOptions {
|
|
2238
|
-
/**
|
|
2239
|
-
* 一个随图片变化的值,哪怕字节没有变。
|
|
2240
|
-
*
|
|
2241
|
-
* 截图浮层是一张长期存在的页面——显示、隐藏,而不是建了又拆——所以 `src` 是唯一能告诉它**新的**一次
|
|
2242
|
-
* 截图开始了的东西。两次截图之间屏幕没变的话,编码出来的 PNG 逐字节相同,`src` 也就相同,第一次画上
|
|
2243
|
-
* 的标记在第二次还会在。
|
|
2244
|
-
*/
|
|
1307
|
+
/** 一个随图片变化的值,哪怕字节没有变。 */
|
|
2245
1308
|
session?: number | string;
|
|
2246
|
-
/**
|
|
2247
|
-
* 这张图的数值属于哪个色彩空间。
|
|
2248
|
-
*
|
|
2249
|
-
* 只有截图会说这件事,因为只有它拿到的是显示器帧缓冲的原始数值——一台 Display P3 的机器上,
|
|
2250
|
-
* 屏幕上的纯红在那串数里是 234,51,35。默认按 sRGB 读,那是每一张解码出来的图片的答案(PNG
|
|
2251
|
-
* 自带 profile,浏览器已经替我们转好了),也是没有色彩管理时的安全答案。
|
|
2252
|
-
*/
|
|
1309
|
+
/** 这张图的数值属于哪个色彩空间。 */
|
|
2253
1310
|
colorSpace?: PredefinedColorSpace;
|
|
2254
1311
|
}
|
|
2255
1312
|
|
|
@@ -2275,33 +1332,15 @@ export interface Annotator {
|
|
|
2275
1332
|
removeSelected: () => void;
|
|
2276
1333
|
/** 把选中的标记挪这么多个屏幕点——方向键用的。 */
|
|
2277
1334
|
nudge: (dx: number, dy: number) => void;
|
|
2278
|
-
/**
|
|
2279
|
-
* 标注好的图,PNG data URL,按图片自己的尺寸;源图还没解码完时是 null。
|
|
2280
|
-
*
|
|
2281
|
-
* 重新画一遍,而不是从屏幕上那块画布读回来:那块是按屏幕的分辨率画的,图片显示得比它本身大的时候,
|
|
2282
|
-
* 像素比图片本身还多。读回来的话,一张 1408px 的图交出去的是 2200px 的副本。两边用的是同一个
|
|
2283
|
-
* `paintAll`,所以出来的就是屏幕上的样子,尺寸和进来时一样。
|
|
2284
|
-
*/
|
|
1335
|
+
/** 标注好的图,PNG data URL,按图片自己的尺寸;源图还没解码完时是 null。 */
|
|
2285
1336
|
render: () => string | null;
|
|
2286
1337
|
/**
|
|
2287
|
-
*
|
|
2288
|
-
*
|
|
2289
|
-
* 任何要把这幅画变成一张图的操作都得先调它。文字在提交之前住在一个 `<textarea>` 里,提交它的是这个
|
|
2290
|
-
* 输入框失去焦点——可工具条故意不拿焦点(`onMouseDown` → `preventDefault`),因为写字时按「粗」必须
|
|
2291
|
-
* 是改这段字的大小,而不是结束它。于是光标还在文字里就按「完成」,什么也没提交:字在屏幕上,在画布
|
|
2292
|
-
* 上面的一个输入框里,而被裁出来的是画布。出来的图上没有那段字。
|
|
2293
|
-
*
|
|
2294
|
-
* `true` 表示提交了东西、画布**还没有**重画——重画是一个 effect,要等 React 把状态变化刷出去以后才
|
|
2295
|
-
* 落下。调用方等一帧再读。
|
|
1338
|
+
* 把正在写的那段文字提交掉,如果有的话。返回有没有。任何要把这幅画变成一张图的操作(`render`)都得先调它;
|
|
1339
|
+
* 返回 `true` 时画布还没重画,调用方等一帧再读。
|
|
2296
1340
|
*/
|
|
2297
1341
|
flushText: () => boolean;
|
|
2298
1342
|
// 下面是画布要用的内部件,不给调用方用。
|
|
2299
|
-
/**
|
|
2300
|
-
* `flushText` 怎么够到那段文字——它住在 `AnnotateCanvas` 里,不在这里。
|
|
2301
|
-
*
|
|
2302
|
-
* 用 ref 而不是 state:画布在写字时填上它、写完清空,没有任何东西因为它而渲染得不一样。为了这一次调用
|
|
2303
|
-
* 把整套编辑状态提到这里来,要搬好几百行。
|
|
2304
|
-
*/
|
|
1343
|
+
/** `flushText` 怎么够到那段文字——它住在 `AnnotateCanvas` 里,不在这里。 */
|
|
2305
1344
|
pendingText: React.RefObject<(() => void) | null>;
|
|
2306
1345
|
shapes: Shape[];
|
|
2307
1346
|
canvas: React.RefObject<HTMLCanvasElement | null>;
|
|
@@ -2310,31 +1349,11 @@ export interface Annotator {
|
|
|
2310
1349
|
/** 这个网格大小的马赛克取样用的那张平均过的图,用到时才建,建了就缓存。 */
|
|
2311
1350
|
mosaicSourceFor(block: number): HTMLCanvasElement | null;
|
|
2312
1351
|
ready: boolean;
|
|
2313
|
-
/**
|
|
2314
|
-
* 这个标注器先后解码过几张图——「源图换了,重画」唯一诚实的依赖。
|
|
2315
|
-
*
|
|
2316
|
-
* `ready` 说不出这件事。新图解码时它变成 false,落地后又变回 true,所以两次截图之间它的**值**两次都是
|
|
2317
|
-
* true——要是解码快到在 React 把那个 `false` 刷出去之前就完成了,两次更新落在同一轮里,这个标志根本没
|
|
2318
|
-
* 变,盯着它的 effect 再也不会跑。`image` 也说不出:它是 ref,所以压根不能当依赖。
|
|
2319
|
-
*
|
|
2320
|
-
* 截图浮层是这件事看得出代价的地方。它在一个以 `ready` 为依赖的 effect 里用这张位图画冻结的桌面,而
|
|
2321
|
-
* 第一次截图还开着时又截了一次,五毫秒就解码完了——于是浮层一边从这一次的图里裁,一边继续显示**上一次**
|
|
2322
|
-
* 的画面,也再没发出让窗口上屏的 `ready` 握手,窗口只好等它 1500ms 的兜底超时。
|
|
2323
|
-
*/
|
|
1352
|
+
/** 这个标注器先后解码过几张图——「源图换了,重画」唯一诚实的依赖。 */
|
|
2324
1353
|
revision: number;
|
|
2325
|
-
/**
|
|
2326
|
-
* 这张图和所有从它派生出来的 canvas 用的色彩空间。
|
|
2327
|
-
*
|
|
2328
|
-
* 传下去而不是各处自己猜:裁剪出来的那张、马赛克取样的那张、放大镜里的那张,都得跟主画布同一
|
|
2329
|
-
* 个空间,否则 `drawImage` 会在它们之间做转换——一次转换就是一次色偏,而且只偏彩色不偏灰。
|
|
2330
|
-
*/
|
|
1354
|
+
/** 这张图和所有从它派生出来的 canvas 用的色彩空间。 */
|
|
2331
1355
|
colorSpace: PredefinedColorSpace;
|
|
2332
|
-
/**
|
|
2333
|
-
* 源图的原始宽度,放在 state 里而不是从 ref 上读。
|
|
2334
|
-
*
|
|
2335
|
-
* 这里的每个尺寸——线宽、字号、马赛克格子——都由它推出来,从 `image.current` 推的话,推导就没有诚实的
|
|
2336
|
-
* 依赖:ref 当不了依赖,拿 `ready` 标志顶替它是一句谎话,lint 拒绝它是对的。
|
|
2337
|
-
*/
|
|
1356
|
+
/** 源图的原始宽度,放在 state 里而不是从 ref 上读。 */
|
|
2338
1357
|
width: number;
|
|
2339
1358
|
/** 现在画一笔马赛克会用的网格。已经画上的标记各自带着自己的。 */
|
|
2340
1359
|
block: number;
|
|
@@ -2345,12 +1364,16 @@ export interface Annotator {
|
|
|
2345
1364
|
unit: number;
|
|
2346
1365
|
setUnit: (unit: number) => void;
|
|
2347
1366
|
}
|
|
1367
|
+
|
|
1368
|
+
interface Decoded {
|
|
1369
|
+
source: CanvasImageSource;
|
|
1370
|
+
width: number;
|
|
1371
|
+
height: number;
|
|
1372
|
+
}
|
|
2348
1373
|
```
|
|
2349
1374
|
|
|
2350
1375
|
### `AnnotateCanvas`
|
|
2351
1376
|
|
|
2352
|
-
源码:`packages/client/ui/src/image/AnnotateCanvas.tsx`
|
|
2353
|
-
|
|
2354
1377
|
```tsx
|
|
2355
1378
|
/**
|
|
2356
1379
|
* @param className 替换画布自己的尺寸和外框,给需要把它摆得分毫不差的调用方。看图器要 `STAGE_FIT`
|
|
@@ -2362,74 +1385,29 @@ export function AnnotateCanvas({ annotator, zoom, className, style }: { annotato
|
|
|
2362
1385
|
|
|
2363
1386
|
### `AnnotateToolbar`
|
|
2364
1387
|
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
```tsx
|
|
2368
|
-
export function AnnotateToolbar({
|
|
2369
|
-
annotator,
|
|
2370
|
-
onCancel,
|
|
2371
|
-
onSave,
|
|
2372
|
-
onPin,
|
|
2373
|
-
onDownload,
|
|
2374
|
-
onGrab,
|
|
2375
|
-
grabbing = false,
|
|
2376
|
-
canReplace,
|
|
2377
|
-
delivers = false,
|
|
2378
|
-
saveLabel,
|
|
2379
|
-
cancelLabel,
|
|
2380
|
-
requireDirty = true,
|
|
2381
|
-
style,
|
|
2382
|
-
propertiesSide = "above",
|
|
2383
|
-
}: {
|
|
1388
|
+
```tsx
|
|
1389
|
+
export function AnnotateToolbar({ grabbing = false, delivers = false, requireDirty = true, propertiesSide = "above" }: {
|
|
2384
1390
|
annotator: Annotator;
|
|
2385
1391
|
onCancel: () => void;
|
|
2386
1392
|
onSave: () => void;
|
|
2387
|
-
/**
|
|
2388
|
-
* 把这张图留在桌面上、浮在一切之上,而不是交出去。
|
|
2389
|
-
*
|
|
2390
|
-
* 只在有意义的地方给——截图浮层——因为把一张已经开在窗口里的图的一块置顶,是给正在看的东西复制一份。
|
|
2391
|
-
*/
|
|
1393
|
+
/** 把这张图留在桌面上、浮在一切之上,而不是交出去。 */
|
|
2392
1394
|
onPin?: () => void;
|
|
2393
1395
|
/** 把这张图写进下载目录。理由同 `onPin`。 */
|
|
2394
1396
|
onDownload?: () => void;
|
|
2395
|
-
/**
|
|
2396
|
-
* 这一条要挪窝了:从哪儿按下的,按下时它在哪儿。
|
|
2397
|
-
*
|
|
2398
|
-
* 挪到哪儿是调用方的事——它飘在一张截好的屏幕上,得留在屏幕里——所以这里只说「开始了」,外加
|
|
2399
|
-
* 两个位置。带上 `origin` 而不是让调用方自己去 DOM 里量:这一条的位置只有它自己最清楚,而且
|
|
2400
|
-
* 按住按钮触发的那一次是从定时器里发出的,那会儿 React 的合成事件早就没有 `currentTarget` 了。
|
|
2401
|
-
*
|
|
2402
|
-
* 不传就是不能挪。
|
|
2403
|
-
*/
|
|
1397
|
+
/** 这一条要挪窝了:从哪儿按下的,按下时它在哪儿。挪到哪儿由调用方定;不传就是不能挪。 */
|
|
2404
1398
|
onGrab?: (grab: { from: { x: number; y: number }; origin: { x: number; y: number } }) => void;
|
|
2405
1399
|
/** 正在挪,好让整条和它上面每个控件都显示同一个光标。 */
|
|
2406
1400
|
grabbing?: boolean;
|
|
2407
|
-
/**
|
|
2408
|
-
* 当前工具的属性气泡往哪边开,以这条工具条为准。
|
|
2409
|
-
*
|
|
2410
|
-
* `above` 开在工具条上面,`below` 开在下面。由调用方决定,因为只有调用方知道工具条摆在被标注的东西的
|
|
2411
|
-
* 哪一边:气泡得往远离它的方向开,否则就把它盖住了。默认 `above`,看图器就是这样——工具条一开始贴着
|
|
2412
|
-
* 舞台底边。
|
|
2413
|
-
*/
|
|
1401
|
+
/** 当前工具的属性气泡往哪边开,以这条工具条为准。 */
|
|
2414
1402
|
propertiesSide?: "above" | "below";
|
|
2415
1403
|
/** 保存能不能替换原图,还是只能另出一份副本。 */
|
|
2416
1404
|
canReplace: boolean;
|
|
2417
|
-
/**
|
|
2418
|
-
* 副本是交出去接着用,而不是导出。
|
|
2419
|
-
*
|
|
2420
|
-
* 对话里的一张图被标注,是为了让它照着重画,所以副本进那个对话的输入框:按钮是带勾的「完成」,而不是
|
|
2421
|
-
* 一个下载箭头,许诺一个谁也没要的文件。
|
|
2422
|
-
*/
|
|
1405
|
+
/** 副本是交出去接着用,而不是导出。 */
|
|
2423
1406
|
delivers?: boolean;
|
|
2424
1407
|
/** 覆盖保存按钮上的字。不传就按 `canReplace` 的意思说。 */
|
|
2425
1408
|
saveLabel?: string;
|
|
2426
1409
|
cancelLabel?: string;
|
|
2427
|
-
/**
|
|
2428
|
-
* 保存是不是要求先画过东西。
|
|
2429
|
-
*
|
|
2430
|
-
* 对一张已经存在的图是 true——保存一份没动过的副本只会多出一个一模一样的文件,所以在有东西可存之前
|
|
2431
|
-
* 按钮是死的。对截图浮层是 false:那个按钮就是确认这次截图的方式,而一块没有标注的区域是最常见的需求。
|
|
2432
|
-
*/
|
|
1410
|
+
/** 保存是不是要求先画过东西。 */
|
|
2433
1411
|
requireDirty?: boolean;
|
|
2434
1412
|
style?: React.CSSProperties;
|
|
2435
1413
|
})
|
|
@@ -2437,16 +1415,8 @@ export function AnnotateToolbar({
|
|
|
2437
1415
|
|
|
2438
1416
|
### `useToolbarPlacement`
|
|
2439
1417
|
|
|
2440
|
-
源码:`packages/client/ui/src/image/useToolbarPlacement.ts`
|
|
2441
|
-
|
|
2442
1418
|
```tsx
|
|
2443
|
-
/**
|
|
2444
|
-
* `bounds` 可以是 null。
|
|
2445
|
-
*
|
|
2446
|
-
* 组件在拿到第一帧数据之前就早返回了,而 hook 必须在那之前调用——所以这里收一个可能还不存在的
|
|
2447
|
-
* 屏幕尺寸。它为 null 时 `selection` 必然也是 null(没有截图就没有选区),下面那两处算位置的
|
|
2448
|
-
* 地方都走不到。
|
|
2449
|
-
*/
|
|
1419
|
+
/** `bounds` 可以是 null。 */
|
|
2450
1420
|
export function useToolbarPlacement(selection: Rect | null, bounds: { width: number; height: number } | null): ToolbarPlacement
|
|
2451
1421
|
|
|
2452
1422
|
export interface ToolbarPlacement {
|
|
@@ -2461,27 +1431,21 @@ export interface ToolbarPlacement {
|
|
|
2461
1431
|
/** 新一轮截图开始时调用:忘掉用户上一轮把它拖到了哪。 */
|
|
2462
1432
|
reset: () => void;
|
|
2463
1433
|
}
|
|
1434
|
+
|
|
1435
|
+
type ToolbarAt = (Point & { side: "above" | "below" | "over" }) | null;
|
|
2464
1436
|
```
|
|
2465
1437
|
|
|
2466
1438
|
## 选区几何
|
|
2467
1439
|
|
|
2468
1440
|
### `HANDLES`
|
|
2469
1441
|
|
|
2470
|
-
源码:`packages/client/ui/src/image/screenshot-geometry.ts`
|
|
2471
|
-
|
|
2472
1442
|
```tsx
|
|
2473
|
-
/**
|
|
2474
|
-
* 八个方向,按 CSS 光标的叫法命名。
|
|
2475
|
-
*
|
|
2476
|
-
* 角在前,这样顺着这张表做的命中测试在角和边重叠的地方优先认角——它们总是重叠,而指针瞄的就是角。
|
|
2477
|
-
*/
|
|
1443
|
+
/** 八个方向,按 CSS 光标的叫法命名。 */
|
|
2478
1444
|
export const HANDLES = ["nw", "ne", "se", "sw", "n", "e", "s", "w"] as const;
|
|
2479
1445
|
```
|
|
2480
1446
|
|
|
2481
1447
|
### `HANDLE_CURSOR`
|
|
2482
1448
|
|
|
2483
|
-
源码:`packages/client/ui/src/image/screenshot-geometry.ts`
|
|
2484
|
-
|
|
2485
1449
|
```tsx
|
|
2486
1450
|
/** 每个手柄该显示的光标:它的名字加上 CSS 要的后缀。 */
|
|
2487
1451
|
export const HANDLE_CURSOR: Record<Handle, string> = {
|
|
@@ -2500,8 +1464,6 @@ export type Handle = (typeof HANDLES)[number];
|
|
|
2500
1464
|
|
|
2501
1465
|
### `handlePoint`
|
|
2502
1466
|
|
|
2503
|
-
源码:`packages/client/ui/src/image/screenshot-geometry.ts`
|
|
2504
|
-
|
|
2505
1467
|
```tsx
|
|
2506
1468
|
/** 手柄在哪,表示为选区边上的一个点。 */
|
|
2507
1469
|
export function handlePoint(rect: Rect, handle: Handle): Point
|
|
@@ -2513,147 +1475,72 @@ export interface Rect {
|
|
|
2513
1475
|
height: number;
|
|
2514
1476
|
}
|
|
2515
1477
|
|
|
2516
|
-
export type Handle = (typeof HANDLES)[number];
|
|
2517
|
-
|
|
2518
1478
|
export interface Point {
|
|
2519
1479
|
x: number;
|
|
2520
1480
|
y: number;
|
|
2521
1481
|
}
|
|
2522
1482
|
```
|
|
2523
1483
|
|
|
2524
|
-
|
|
1484
|
+
`Handle` 见上文。
|
|
2525
1485
|
|
|
2526
|
-
|
|
1486
|
+
### `hitHandle`
|
|
2527
1487
|
|
|
2528
1488
|
```tsx
|
|
2529
|
-
/**
|
|
2530
|
-
* 指针在哪个手柄上,没有就是 null。
|
|
2531
|
-
*
|
|
2532
|
-
* `tolerance` 是屏幕像素的半径,而不是手柄画出来的大小:目标得比记号大,理由和滚动条一样——八个像素见方
|
|
2533
|
-
* 的一块,手落不准。
|
|
2534
|
-
*/
|
|
1489
|
+
/** 指针在哪个手柄上,没有就是 null。`tolerance` 是屏幕像素的半径,而不是手柄画出来的大小。 */
|
|
2535
1490
|
export function hitHandle(rect: Rect, at: Point, tolerance: number): Handle | null
|
|
2536
|
-
|
|
2537
|
-
export interface Rect {
|
|
2538
|
-
x: number;
|
|
2539
|
-
y: number;
|
|
2540
|
-
width: number;
|
|
2541
|
-
height: number;
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
export interface Point {
|
|
2545
|
-
x: number;
|
|
2546
|
-
y: number;
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
export type Handle = (typeof HANDLES)[number];
|
|
2550
1491
|
```
|
|
2551
1492
|
|
|
2552
|
-
|
|
1493
|
+
`Rect`、`Point`、`Handle` 见上文。
|
|
2553
1494
|
|
|
2554
|
-
|
|
1495
|
+
### `insideRect`
|
|
2555
1496
|
|
|
2556
1497
|
```tsx
|
|
2557
1498
|
/** 点是否在选区里——在里面按下是移动,而不是重新拖一个。 */
|
|
2558
1499
|
export function insideRect(rect: Rect, at: Point): boolean
|
|
2559
|
-
|
|
2560
|
-
export interface Rect {
|
|
2561
|
-
x: number;
|
|
2562
|
-
y: number;
|
|
2563
|
-
width: number;
|
|
2564
|
-
height: number;
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
export interface Point {
|
|
2568
|
-
x: number;
|
|
2569
|
-
y: number;
|
|
2570
|
-
}
|
|
2571
1500
|
```
|
|
2572
1501
|
|
|
2573
|
-
|
|
1502
|
+
`Rect`、`Point` 见上文。
|
|
2574
1503
|
|
|
2575
|
-
|
|
1504
|
+
### `rectFromPoints`
|
|
2576
1505
|
|
|
2577
1506
|
```tsx
|
|
2578
1507
|
/** 由两个角得到矩形,两个角哪个在前都行。 */
|
|
2579
1508
|
export function rectFromPoints(a: Point, b: Point): Rect
|
|
2580
|
-
|
|
2581
|
-
export interface Point {
|
|
2582
|
-
x: number;
|
|
2583
|
-
y: number;
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
export interface Rect {
|
|
2587
|
-
x: number;
|
|
2588
|
-
y: number;
|
|
2589
|
-
width: number;
|
|
2590
|
-
height: number;
|
|
2591
|
-
}
|
|
2592
1509
|
```
|
|
2593
1510
|
|
|
2594
|
-
|
|
1511
|
+
`Point`、`Rect` 见上文。
|
|
2595
1512
|
|
|
2596
|
-
|
|
1513
|
+
### `resizeRect`
|
|
2597
1514
|
|
|
2598
1515
|
```tsx
|
|
2599
|
-
/**
|
|
2600
|
-
* 把一个手柄拖到某点之后的选区。
|
|
2601
|
-
*
|
|
2602
|
-
* 写成「挪动这个手柄管的那几条边,再规范化」,所以拖过对边时矩形会翻过来,而不是得到负的宽度——这是人在
|
|
2603
|
-
* 其他任何地方改大小时预期的样子,`rectFromPoints` 本来就是这么写的。
|
|
2604
|
-
*/
|
|
1516
|
+
/** 把一个手柄拖到某点之后的选区。 */
|
|
2605
1517
|
export function resizeRect(rect: Rect, handle: Handle, to: Point): Rect
|
|
2606
|
-
|
|
2607
|
-
export interface Rect {
|
|
2608
|
-
x: number;
|
|
2609
|
-
y: number;
|
|
2610
|
-
width: number;
|
|
2611
|
-
height: number;
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
export type Handle = (typeof HANDLES)[number];
|
|
2615
|
-
|
|
2616
|
-
export interface Point {
|
|
2617
|
-
x: number;
|
|
2618
|
-
y: number;
|
|
2619
|
-
}
|
|
2620
1518
|
```
|
|
2621
1519
|
|
|
2622
|
-
|
|
1520
|
+
`Rect`、`Handle`、`Point` 见上文。
|
|
2623
1521
|
|
|
2624
|
-
|
|
1522
|
+
### `moveRect`
|
|
2625
1523
|
|
|
2626
1524
|
```tsx
|
|
2627
1525
|
/** 选区平移一段,保持完整、保持在屏幕里。 */
|
|
2628
1526
|
export function moveRect(rect: Rect, dx: number, dy: number, within: { width: number; height: number }): Rect
|
|
2629
|
-
|
|
2630
|
-
export interface Rect {
|
|
2631
|
-
x: number;
|
|
2632
|
-
y: number;
|
|
2633
|
-
width: number;
|
|
2634
|
-
height: number;
|
|
2635
|
-
}
|
|
2636
1527
|
```
|
|
2637
1528
|
|
|
2638
|
-
|
|
1529
|
+
`Rect` 见上文。
|
|
2639
1530
|
|
|
2640
|
-
|
|
1531
|
+
### `clampRect`
|
|
2641
1532
|
|
|
2642
1533
|
```tsx
|
|
2643
1534
|
/** 选区裁到屏幕以内,给拉过了边的改大小用。 */
|
|
2644
1535
|
export function clampRect(rect: Rect, within: { width: number; height: number }): Rect
|
|
2645
|
-
|
|
2646
|
-
export interface Rect {
|
|
2647
|
-
x: number;
|
|
2648
|
-
y: number;
|
|
2649
|
-
width: number;
|
|
2650
|
-
height: number;
|
|
2651
|
-
}
|
|
2652
1536
|
```
|
|
2653
1537
|
|
|
1538
|
+
`Rect` 见上文。
|
|
1539
|
+
|
|
2654
1540
|
## `@plume/styles` 的类名组合
|
|
2655
1541
|
|
|
2656
1542
|
`import { text, card } from "@plume/styles"`,拼进 `className`。产品的 Tailwind 扫描这个包,这些类一定会生成。
|
|
1543
|
+
`button`、`iconButton`、`code` 是 `Button`、`IconButton`、`CodeBlock` 的外观,用组件,这里不列。
|
|
2657
1544
|
|
|
2658
1545
|
### `text`
|
|
2659
1546
|
|
|
@@ -2688,70 +1575,6 @@ export const fieldLabel = "flex flex-col gap-1.5 text-caption font-medium text-i
|
|
|
2688
1575
|
export const textarea = "min-w-0 resize-none bg-transparent text-body text-ink outline-none placeholder:text-ink-faint";
|
|
2689
1576
|
```
|
|
2690
1577
|
|
|
2691
|
-
### `button`
|
|
2692
|
-
|
|
2693
|
-
```ts
|
|
2694
|
-
/**
|
|
2695
|
-
* 按钮:五种外观、三档高度。焦点环是全局的(见渲染进程的 styles.css);按下不缩放,底色深一档。
|
|
2696
|
-
* 组件见 @plume/ui 的 Button,一般不直接拼这些。
|
|
2697
|
-
*/
|
|
2698
|
-
export const button = {
|
|
2699
|
-
/** 禁用时不关掉指针事件:悬停提示要能解释它为什么不能按。 */
|
|
2700
|
-
base: "flex shrink-0 cursor-pointer items-center whitespace-nowrap transition-[background-color,border-color,opacity]",
|
|
2701
|
-
/** md 32 是默认,对话框和表单底部的按钮;sm 26 给面板里的行;xs 20 给跟在一行小字后面的动作,圆角随之降到 6px。 */
|
|
2702
|
-
size: {
|
|
2703
|
-
md: "h-8 rounded-lg text-label",
|
|
2704
|
-
sm: "h-6.5 rounded-lg text-label",
|
|
2705
|
-
xs: "h-5 rounded-md text-caption",
|
|
2706
|
-
},
|
|
2707
|
-
/** 只有图标时是正方形,图标居中;有字时字从左边起。 */
|
|
2708
|
-
square: { md: "w-8 justify-center", sm: "w-6.5 justify-center", xs: "w-5 justify-center" },
|
|
2709
|
-
pad: { md: "gap-1.5 px-3", sm: "gap-1.5 px-3", xs: "gap-1 px-1.5" },
|
|
2710
|
-
variant: {
|
|
2711
|
-
/** 一处只有一个,要按的那个:前景色压在底色上,不引入新的颜色也读得出是实心的。 */
|
|
2712
|
-
primary: "bg-ink text-shell font-medium hover:opacity-90 disabled:opacity-40",
|
|
2713
|
-
/** 描边,平常的按钮,放在 primary 旁边。 */
|
|
2714
|
-
ghost: "border border-line text-ink hover:border-ink-faint hover:bg-card-hover active:bg-elevated disabled:opacity-45",
|
|
2715
|
-
/** 没有描边,一排动作不想在面板上画出格子时用。 */
|
|
2716
|
-
subtle: "text-ink-muted hover:bg-card-hover hover:text-ink active:bg-elevated disabled:opacity-45",
|
|
2717
|
-
/** 有底没描边:放在 primary 旁边、指针没过来之前就要读得出是按钮的一排选择。 */
|
|
2718
|
-
soft: "bg-card text-ink hover:bg-card-hover active:bg-elevated disabled:opacity-45",
|
|
2719
|
-
/** 破坏性操作。只在悬停时上色:一排红按钮没人会读。 */
|
|
2720
|
-
danger: "border border-line text-danger hover:border-danger/50 hover:bg-danger/10 active:bg-danger/15 disabled:opacity-45",
|
|
2721
|
-
} satisfies Record<ButtonVariant, string>,
|
|
2722
|
-
/** 一组选择里选中的那个:悬停态一直保持。只有 ghost 和 subtle 有。 */
|
|
2723
|
-
pressed: {
|
|
2724
|
-
ghost: "border border-ink-faint bg-card-hover text-ink disabled:opacity-45",
|
|
2725
|
-
subtle: "bg-card-hover text-ink disabled:opacity-45",
|
|
2726
|
-
} as Partial<Record<ButtonVariant, string>>,
|
|
2727
|
-
/** 忙碌:和 disabled 分开,读起来是“在做”,不是“不能按”。宽度不变。 */
|
|
2728
|
-
loading: "opacity-60",
|
|
2729
|
-
};
|
|
2730
|
-
```
|
|
2731
|
-
|
|
2732
|
-
### `iconButton`
|
|
2733
|
-
|
|
2734
|
-
```ts
|
|
2735
|
-
/** 图标按钮:平时是淡的字色,悬停才有底。按下底色深一档,不缩放。组件见 @plume/ui 的 IconButton。 */
|
|
2736
|
-
export const iconButton = {
|
|
2737
|
-
/** 禁用时不关掉指针事件:`explainDisabled` 要靠悬停提示说明为什么不能按。 */
|
|
2738
|
-
base: "relative flex shrink-0 items-center justify-center transition-[color,background-color,opacity] disabled:opacity-40",
|
|
2739
|
-
/**
|
|
2740
|
-
* xs 20 是标题行和一行小字的高度,圆角随之降到 6px。composer 跟着主输入框工具栏的控件边长走,
|
|
2741
|
-
* 那个尺寸变了,这一排仍然是一行。
|
|
2742
|
-
*/
|
|
2743
|
-
size: { xs: "size-5 rounded-md", sm: "size-5.5 rounded-lg", md: "size-6.5 rounded-lg", composer: "plume-composer-control plume-composer-icon rounded-lg" },
|
|
2744
|
-
tone: {
|
|
2745
|
-
default: "text-ink-faint hover:bg-card-hover hover:text-ink active:bg-elevated",
|
|
2746
|
-
danger: "text-ink-faint hover:bg-danger/10 hover:text-danger active:bg-danger/15",
|
|
2747
|
-
},
|
|
2748
|
-
/** 开着的开关,比如“区分大小写”。 */
|
|
2749
|
-
active: "bg-card-hover text-ink active:bg-elevated",
|
|
2750
|
-
/** 一排里现在值得按的那个:满色的字,不是实心按钮。 */
|
|
2751
|
-
emphasis: "text-ink hover:bg-card-hover active:bg-elevated",
|
|
2752
|
-
};
|
|
2753
|
-
```
|
|
2754
|
-
|
|
2755
1578
|
### `card`
|
|
2756
1579
|
|
|
2757
1580
|
```ts
|
|
@@ -2770,11 +1593,3 @@ export const card = {
|
|
|
2770
1593
|
},
|
|
2771
1594
|
};
|
|
2772
1595
|
```
|
|
2773
|
-
|
|
2774
|
-
### `code`
|
|
2775
|
-
|
|
2776
|
-
```ts
|
|
2777
|
-
export const code = {
|
|
2778
|
-
block: `${PRE} px-2.5 py-2 break-words whitespace-pre-wrap`,
|
|
2779
|
-
};
|
|
2780
|
-
```
|