@xrn1997/dsh-session-delete 0.1.0 → 0.2.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/README.md +6 -5
- package/icon.svg +18 -18
- package/lib/client.js +1448 -487
- package/lib/index.js +1185 -455
- package/lib/types/api/dispatch.d.ts +51 -0
- package/lib/types/client/batch-bar.d.ts +24 -0
- package/lib/types/client/batch-confirm.d.ts +31 -0
- package/lib/types/client/batch-menu-item.d.ts +19 -0
- package/lib/types/client/batch-select.d.ts +97 -0
- package/lib/types/client/context.d.ts +31 -0
- package/lib/types/client/copy.d.ts +167 -0
- package/lib/types/client/delete-confirm.d.ts +40 -0
- package/lib/types/client/delete-menu-item.d.ts +20 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/markdown.d.ts +11 -0
- package/lib/types/client/remote.d.ts +178 -0
- package/lib/types/client/trash-detail.d.ts +27 -0
- package/lib/types/client/trash-entry.d.ts +6 -0
- package/lib/types/client/trash-panel.d.ts +10 -0
- package/lib/types/client/trash-store.d.ts +4 -0
- package/lib/types/client/ui/Button.d.ts +21 -0
- package/lib/types/client/ui/Checkbox.d.ts +29 -0
- package/lib/types/client/ui/MenuItemButton.d.ts +32 -0
- package/lib/types/client/ui/Modal.d.ts +35 -0
- package/lib/types/client/ui/Tag.d.ts +16 -0
- package/lib/types/client/ui/Toast.d.ts +36 -0
- package/lib/types/client/ui/Tooltip.d.ts +37 -0
- package/lib/types/client/ui/format.d.ts +18 -0
- package/lib/types/client/ui/icons.d.ts +33 -0
- package/lib/types/client/ui/recipe.d.ts +11 -0
- package/lib/types/client/ui/styles.d.ts +2 -0
- package/lib/types/client/undo-toast.d.ts +49 -0
- package/lib/types/client/use-column-resize.d.ts +62 -0
- package/lib/types/client/use-panel-width.d.ts +15 -0
- package/lib/{index.d.ts → types/index.d.ts} +9 -36
- package/lib/types/log/session-log.d.ts +40 -0
- package/lib/types/log/zstd-frames.d.ts +30 -0
- package/lib/types/ports/fs-port.d.ts +16 -0
- package/lib/types/ports/host-port.d.ts +85 -0
- package/lib/types/remote/map-error.d.ts +19 -0
- package/lib/types/shared/wire.d.ts +134 -0
- package/lib/types/trash/detail.d.ts +40 -0
- package/lib/types/trash/entry.d.ts +35 -0
- package/lib/types/trash/manifest.d.ts +18 -0
- package/lib/types/trash/reconcile.d.ts +35 -0
- package/lib/types/verbs/apply.d.ts +3 -0
- package/lib/types/verbs/delete-session.d.ts +30 -0
- package/lib/types/verbs/errors.d.ts +20 -0
- package/lib/types/verbs/purge-trash.d.ts +6 -0
- package/lib/types/verbs/restore-session.d.ts +19 -0
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +14 -9
- package/lib/client.js.map +0 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器半的**取数通道**:同源 fetch 宿主 web server 上的 `/dsh-session-delete-api`,
|
|
3
|
+
* 加上信封判别与注入面那一处「展开成裸值」的适配。
|
|
4
|
+
*
|
|
5
|
+
* 通道形状(裁决):真相/账本/判据全在宿主那边,Node 半用宿主**自己的 web server** 开一条
|
|
6
|
+
* 插件专属前缀路由把五个段名(`list` / `ping` / `detail` / `apply` / `empty`)搬出来
|
|
7
|
+
* (`src/api/dispatch.ts`),浏览器半同源调用。**不是重造后端**:
|
|
8
|
+
* 路由层只做搬运与信封。为什么不用官方的 `remote.<namespace>`:那条路要**生成的 Typert 远程描述符**
|
|
9
|
+
* 加一次 `$mount`,而盒内装配清单写死在 `dsh-api-remotes` 里、全宿主没有动态发现,第三方只能自挂自,
|
|
10
|
+
* 对一个薄插件不划算。
|
|
11
|
+
*
|
|
12
|
+
* 三条约束决定了这个文件长这样:
|
|
13
|
+
*
|
|
14
|
+
* 1. **信封刻意与官方 unary 面同形**:`{ ok: true, value }` / `{ ok: false, error }`。这不是"还留着
|
|
15
|
+
* 官方通道"——它现在是**我们两半之间**的 wire 契约(Node 半按它产出,见 `src/api/dispatch.ts`),
|
|
16
|
+
* 选它是为了让组件面、`unwrapRemoteResult` 与既有用例一行都不用改。失败是**返回**而不是
|
|
17
|
+
* reject(同宿主网关原文:"Every unary call resolves to `RemoteResult<T>` … and never rejects for a
|
|
18
|
+
* carrier problem");只有往返本身失败(打不通、回的不是信封)才 reject,那是**通道缺席**。
|
|
19
|
+
* 2. **不许值 import 跨插件包**。`error` 在线上就是一段 JSON(`{code, message, details?}`),不是
|
|
20
|
+
* `RemoteError` 实例——所以判别**所依据的那一个标记位**只镜一份,家在两半共用的
|
|
21
|
+
* `src/shared/wire.ts`(`isDSHRemoteError`)。浏览器半落到本地时用 `throwRemoteFailure` 就地补上
|
|
22
|
+
* Error 本体与标记位;Node 半(`src/remote/map-error.ts`)只用它做「这已经是一次远程失败」的判别,
|
|
23
|
+
* 产出的是线上那三件(`details` 可缺席)、不造任何 Error,更不 import 宿主那个类。
|
|
24
|
+
* 3. **失败原文必须能上屏**。调用方(`callRemote`)只把**抛出来的失败**收成判别式状态——
|
|
25
|
+
* `{ ok: false, error }` 不是第三种落地:它在注入面那一处已经被 `unwrapRemoteResult` 抛成了异常,
|
|
26
|
+
* 所以到了组件面前只剩「裸值 / throw」两支,调用方只写 `if (!outcome.ok)`,正常流里没有 try/catch;
|
|
27
|
+
* 非本信封形状的异常(网络层、TypeError…)也不丢原文(见 `failureOf`)。
|
|
28
|
+
*
|
|
29
|
+
* 面(`list` / `ping` / `detail` / `apply` / `empty`)与 Node 半路由的五个段名一一对应(段名本身的家是
|
|
30
|
+
* `src/shared/wire.ts`);`list` 回一行行的条目,`ping` 只回一个探针值,`detail` 回单条正文,
|
|
31
|
+
* `apply` 回**一组逐条结果**,`empty` 回聚合回执。2026-10-17 之前是六个动作段(一条一段),
|
|
32
|
+
* 批量只能由调用方循环拼——那正是「恢复 100 条很慢」的形状来源,收成一段之后这条通道不再有
|
|
33
|
+
* "第二条执行路径"。
|
|
34
|
+
*/
|
|
35
|
+
import { type ApplyAction, type ApplyOutcome, type RemoteFailure, type RemoteResult } from '../shared/wire.js';
|
|
36
|
+
/** 回收站条目(镜像 `src/trash/entry.ts` 的 `TrashEntry`;只读面)。 */
|
|
37
|
+
export interface TrashEntryLike {
|
|
38
|
+
id: string;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
projectDir: string;
|
|
41
|
+
workspaceId: string;
|
|
42
|
+
wasArchived: boolean;
|
|
43
|
+
deletedAt: number;
|
|
44
|
+
title: string;
|
|
45
|
+
sizeBytes: number;
|
|
46
|
+
/** 组视图的三个只读数,**都由 `list` 现算现给、不住在清册里**(清册只记 `parentEntryId` 那件事实)。
|
|
47
|
+
* 缺席 = 不知道或没有 ⇒ 界面上半句都不出现,也不许拿 0 冒充"没有"。
|
|
48
|
+
* `subagentCount`:跟着这条父会话**一起进了回收站**的孩子数(清册里指向它的孩子行)。
|
|
49
|
+
* `subagentBytes`:那些孩子的字节合计——孩子不单独列行,头部合计只加父那一格就会少报。
|
|
50
|
+
* `strandedSubagents`:**还留在盘上没被带走**的孩子数(来源是宿主 `sessionQuery`),
|
|
51
|
+
* 级联落地之前删掉的那些存量才非零。与 `subagentCount` 各说各的,不许互相冒充。 */
|
|
52
|
+
subagentCount?: number;
|
|
53
|
+
subagentBytes?: number;
|
|
54
|
+
strandedSubagents?: number;
|
|
55
|
+
}
|
|
56
|
+
/** 彻底删除的回执(镜像 `PurgeResult`)。 */
|
|
57
|
+
export interface PurgeResultLike {
|
|
58
|
+
removed: number;
|
|
59
|
+
freedBytes: number;
|
|
60
|
+
}
|
|
61
|
+
/** 详情正文里的一条消息(镜像 `TrashDetailMessage`;只读面)。 */
|
|
62
|
+
export interface TrashDetailMessageLike {
|
|
63
|
+
role: 'user' | 'assistant';
|
|
64
|
+
time: number;
|
|
65
|
+
text: string;
|
|
66
|
+
truncated: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 回收站单条详情(镜像 `src/trash/detail.ts` 的 `TrashDetail`;只读面)。
|
|
70
|
+
*
|
|
71
|
+
* **线上只有"日志里读出来的那几件"**:`deletedAt` / `sizeBytes` / `wasArchived` / 清册 `title` 这些
|
|
72
|
+
* 清册已有的字段一律不过线——组件按 `entryId` 与自己手上那份清单配对,这样"清单里的大小"和
|
|
73
|
+
* "详情里的大小"永远是同一个数,不会因为两次读数不同而各说各话。
|
|
74
|
+
*/
|
|
75
|
+
export interface TrashDetailLike {
|
|
76
|
+
entryId: string;
|
|
77
|
+
cwd: string;
|
|
78
|
+
createdAt: number;
|
|
79
|
+
lastActivity: number;
|
|
80
|
+
logTitle: string;
|
|
81
|
+
models: string[];
|
|
82
|
+
turnCount: number;
|
|
83
|
+
userMessageCount: number;
|
|
84
|
+
assistantMessageCount: number;
|
|
85
|
+
toolCallCount: number;
|
|
86
|
+
injectedCount: number;
|
|
87
|
+
messages: TrashDetailMessageLike[];
|
|
88
|
+
omittedByPayloadCap: number;
|
|
89
|
+
}
|
|
90
|
+
/** 任意异常 → 失败面。非带标记的失败(网络层、TypeError…)也保留原文,绝不吞;带标记的失败把
|
|
91
|
+
* `details` 第三件一起带出。 */
|
|
92
|
+
export declare function failureOf(reason: unknown): RemoteFailure;
|
|
93
|
+
/** 注入面用的那一处展开:`ok ? value : throw Object.assign(new Error(error.message), { code })`。 */
|
|
94
|
+
export declare function unwrapRemoteResult<T>(result: RemoteResult<T>): T;
|
|
95
|
+
/** 一次远程调用的落地面:要么值,要么失败——调用方只做状态检查。 */
|
|
96
|
+
export type RemoteOutcome<T> = {
|
|
97
|
+
ok: true;
|
|
98
|
+
value: T;
|
|
99
|
+
} | {
|
|
100
|
+
ok: false;
|
|
101
|
+
failure: RemoteFailure;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* 跑一次远程调用,把两种落地(值 / 抛出的失败)收成一个状态。
|
|
105
|
+
*
|
|
106
|
+
* 「返回的失败」在这里不再是一种落地:`{ ok: false, error }` 已在注入面被 `unwrapRemoteResult`
|
|
107
|
+
* 抛成了异常,所以到这一层的只有裸值或 throw——判别留在 `failureOf` 里(带标记位的失败带出它自己的
|
|
108
|
+
* code,其余按原文兜底)。
|
|
109
|
+
*/
|
|
110
|
+
export declare function callRemote<T>(run: () => Promise<T>): Promise<RemoteOutcome<T>>;
|
|
111
|
+
/** 本插件前缀路由那套**响应信封**的窄镜像(不是宿主服务 `ctx.remote.*`——那条 typert 通道见 §10):
|
|
112
|
+
* 每个方法 resolve 成 `RemoteResult<T>`。
|
|
113
|
+
*
|
|
114
|
+
* 2026-10-17 批量重构:写动词收成**一个 `apply`**(一段管所有动作、一组 id 进、逐条结果出),
|
|
115
|
+
* 再加一段**纯探测** `ping` ⇒ 面是五段:`list` / `ping` / `detail` / `apply` / `empty`(原先六个动作段)。 */
|
|
116
|
+
export interface RawSessionDeleteRemote {
|
|
117
|
+
list(): Promise<RemoteResult<TrashEntryLike[]>>;
|
|
118
|
+
/** 通道的"在不在":一次零成本的 GET(探测专用,见 `src/api/dispatch.ts` 的 `ping` 支)。 */
|
|
119
|
+
ping(): Promise<RemoteResult<string>>;
|
|
120
|
+
detail(entryId: string): Promise<RemoteResult<TrashDetailLike>>;
|
|
121
|
+
/** 一组 id 上的同一个动作。顶层信封的 `ok:false` 只表示**整次调用**没办成(路由自检、栅栏之外);
|
|
122
|
+
* 单条动作的成败住在 `ApplyOutcome` 里——这是批量接口的判据,别在信封层找它。 */
|
|
123
|
+
apply(action: ApplyAction, ids: readonly string[]): Promise<RemoteResult<ApplyOutcome[]>>;
|
|
124
|
+
/** 清空回收站:聚合回执,不按条回。 */
|
|
125
|
+
empty(): Promise<RemoteResult<PurgeResultLike>>;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 前缀路由的 fetch 实现。**相对路径**:不拼 base、不认 origin——页面本身就是从宿主 web server
|
|
129
|
+
* 加载的,同源是这条薄线的全部前提(也是它敢不带凭据的原因:宿主那行 URL 上的 browser-trust token
|
|
130
|
+
* 不是这条通道的凭据,栅栏在 Node 半那一侧按 Origin/Referer 判)。
|
|
131
|
+
*/
|
|
132
|
+
export declare function createHttpRemote(prefix?: string): RawSessionDeleteRemote;
|
|
133
|
+
/**
|
|
134
|
+
* 取数通道探测:一次 `GET /ping` 走通(或走到)就是"通道在"。
|
|
135
|
+
*
|
|
136
|
+
* 判据是**「拿到信封」而不是「操作成功」**:`{ok:false, error}` 也是一次成功的往返(路由在、依赖接上了、
|
|
137
|
+
* 错误映射也生效了),那时该做的是把入口显示出来、让原文上屏;只有往返本身失败才算通道缺席 ⇒
|
|
138
|
+
* 一个槽位贡献都不注册(设计稿 §7「未接通态」)。
|
|
139
|
+
*
|
|
140
|
+
* 2026-10-17 第四批:探测从"读一遍清单"改成这一问——**每次页面加载都跑全表对账**是白付的钱,
|
|
141
|
+
* 而"信封到不到"这件事一次零成本的 GET 就答得出来。
|
|
142
|
+
*/
|
|
143
|
+
export declare function probeChannel(raw: RawSessionDeleteRemote): Promise<boolean>;
|
|
144
|
+
/** `shared/wire.ts` 的 `ApplyOutcome` 从这里转出去——浏览器半只认 `remote.js` 这一扇门。 */
|
|
145
|
+
export type { ApplyAction, ApplyOutcome } from '../shared/wire.js';
|
|
146
|
+
/** 一段管所有写动作的返回值(逐条)。`ApplySuccess<A>` = 某动作成功支(`value` 按动作收窄)。 */
|
|
147
|
+
export type ApplySuccess<A extends ApplyAction> = Extract<ApplyOutcome, {
|
|
148
|
+
action: A;
|
|
149
|
+
ok: true;
|
|
150
|
+
}>;
|
|
151
|
+
export interface SessionDeleteRemote {
|
|
152
|
+
list(): Promise<TrashEntryLike[]>;
|
|
153
|
+
detail(entryId: string): Promise<TrashDetailLike>;
|
|
154
|
+
/** 一段管所有写动作(2026-10-17 批量重构):一次调用把整批办完,逐条结果原样交回组件。 */
|
|
155
|
+
applyOutcomes(action: ApplyAction, ids: readonly string[]): Promise<ApplyOutcome[]>;
|
|
156
|
+
/** 清空回收站(聚合回执)。 */
|
|
157
|
+
emptyTrash(): Promise<PurgeResultLike>;
|
|
158
|
+
/** 单条便捷包装 = `applyOutcomes` 的 N=1 支。`null` 是批量里"已被同批前一条带走"的幂等值;
|
|
159
|
+
* 单条删除(N=1)永远不会命中它。 */
|
|
160
|
+
delete(sessionId: string): Promise<TrashEntryLike | null>;
|
|
161
|
+
restore(entryId: string): Promise<TrashEntryLike | null>;
|
|
162
|
+
/** 这条会话在盘上有几个后代(删除确认框那句「含 N 个子会话」)。拿不到 ⇒ 0,那一栏整句不出现。 */
|
|
163
|
+
countSubagents(sessionId: string): Promise<number>;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 注入面的**唯一一处适配**:`RemoteResult` → 裸值,外加「一段批量」→「单条便捷面」的展开。
|
|
167
|
+
*
|
|
168
|
+
* **为什么必须有这一步**:通道那头的调用 resolve 成 `RemoteResult<T>`(`{ ok: true, value }` /
|
|
169
|
+
* `{ ok: false, error }`),失败是**返回**而不是 reject。组件面要的是「拿到值 / 抛异常」,
|
|
170
|
+
* 所以在这里展开一次:`ok ? value : throw`。放在这一处而不是组件里,是为了让三个组件的 props 契约
|
|
171
|
+
* (以及钉住它们的用例)保持裸值形状不变。
|
|
172
|
+
*
|
|
173
|
+
* **单条支里只有一条落地**:没有它(幽灵 id、或路由整次没办成)就 throw——"批量只回了一条别的"
|
|
174
|
+
* 不是成功,也不是 0。
|
|
175
|
+
*/
|
|
176
|
+
export declare function buildDeps(raw: RawSessionDeleteRemote): SessionDeleteRemote;
|
|
177
|
+
/** 「删除」确认框的宿主需要的两件:提交删除的那条便捷包装,与删除前那一次计数(那句「含 N 个子会话」)。 */
|
|
178
|
+
export type DeleteConfirmDeps = Pick<SessionDeleteRemote, 'delete' | 'countSubagents'>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Translate } from './copy.js';
|
|
2
|
+
import type { TrashDetailLike, TrashEntryLike } from './remote.js';
|
|
3
|
+
export type TrashDetailState = {
|
|
4
|
+
kind: 'idle';
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'loading';
|
|
7
|
+
entry: TrashEntryLike;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'failed';
|
|
10
|
+
entry: TrashEntryLike;
|
|
11
|
+
message: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'ready';
|
|
14
|
+
entry: TrashEntryLike;
|
|
15
|
+
detail: TrashDetailLike;
|
|
16
|
+
};
|
|
17
|
+
export interface TrashDetailProps {
|
|
18
|
+
state: TrashDetailState;
|
|
19
|
+
/** 有写操作在飞(动作按钮住在清单行里,这里只用来把「重试」按住)。 */
|
|
20
|
+
busy: boolean;
|
|
21
|
+
/** 宽档不画返回——同屏就有清单,返回是多余的一步。 */
|
|
22
|
+
wide: boolean;
|
|
23
|
+
onRetry: () => void;
|
|
24
|
+
onBack: () => void;
|
|
25
|
+
t: Translate;
|
|
26
|
+
}
|
|
27
|
+
export declare function TrashDetail({ state, busy, wide, onRetry, onBack, t }: TrashDetailProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Translate } from './copy.js';
|
|
2
|
+
import { type SessionDeleteRemote } from './remote.js';
|
|
3
|
+
/** 批量重构(2026-10-17)后这份依赖面:两个读 + 一个批量口 + 清空 + 单条恢复的便捷包装。 */
|
|
4
|
+
export type TrashPanelDeps = Pick<SessionDeleteRemote, 'list' | 'detail' | 'applyOutcomes' | 'emptyTrash' | 'restore'>;
|
|
5
|
+
export interface TrashPanelProps {
|
|
6
|
+
deps: TrashPanelDeps;
|
|
7
|
+
/** 文案(缺省 = 内置 zh,见 `copy.ts`:这不是"忘了传",而是"这次没有 locale 服务")。 */
|
|
8
|
+
t?: Translate;
|
|
9
|
+
}
|
|
10
|
+
export declare function TrashPanel({ deps, t }: TrashPanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Button`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 Button,出处与三条改写规则
|
|
3
|
+
* 见 `ui.css` 头注):`<button type="button">`,`variant` × `size` 两个正交档,可带前导图标。
|
|
4
|
+
*
|
|
5
|
+
* 只收本插件用到的面:`primary` / `ghost` / `outline` 三档与 `md`(36px) / `sm`(28px) 两号;
|
|
6
|
+
* 官方还有 `toolbar` 变体与 `className` 之外的转发面,这里不铺开。
|
|
7
|
+
*
|
|
8
|
+
* **转发 `ref`**:官方把 `ref` 交给原生按钮("for focus management and overlay anchors"),
|
|
9
|
+
* `Modal` 的初始焦点与外壳的对齐测量都吃这个。React 18 的 `forwardRef` 与官方实现同形。
|
|
10
|
+
*/
|
|
11
|
+
import { type ButtonHTMLAttributes, type ReactNode } from 'react';
|
|
12
|
+
import './styles.js';
|
|
13
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
14
|
+
/** 视觉族;缺省 `ghost`。 */
|
|
15
|
+
variant?: 'primary' | 'ghost' | 'outline';
|
|
16
|
+
/** `md` = 36px 控件(12px 圆角)/`sm` = 28px 控件(8px 圆角)。 */
|
|
17
|
+
size?: 'md' | 'sm';
|
|
18
|
+
/** 前导 16px 图标节点。 */
|
|
19
|
+
icon?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 勾选框(2026-10-15 加,回收站多选态用)。
|
|
3
|
+
*
|
|
4
|
+
* 样式与行为抄自 `@deepseek-ai/dsh-client-ui-primitives` 的 `lib/Checkbox.module.css`
|
|
5
|
+
* (那四条声明逐字进了 `ui.css` 的 `.sd-checkbox`):**本体是原生 `input[type=checkbox]` +
|
|
6
|
+
* `accent-color`,不是自绘的圆框**。参考稿上那种圆形勾选框是别家 App 的形状,抄进来就成了
|
|
7
|
+
* "宿主界面里长出一个外来控件";原生这一款在深浅两套主题下都跟宿主同形,键盘与读屏也白送。
|
|
8
|
+
*
|
|
9
|
+
* 一处与官方 `Checkbox` 的差异是**结构上的**:官方那版是 `<label>` 包 input + 可见文字,
|
|
10
|
+
* 我们这一格只有框、名字走 `aria-label`(行本身就是可点的整块,再挂一份可见文字是重复)。
|
|
11
|
+
*
|
|
12
|
+
* **`stopPropagation` 不是可有可无的**:清单整行是个点击目标(多选态下"点行=切换勾选"),
|
|
13
|
+
* 勾选框住在行里。不拦住冒泡,点框这一下会被行再处理一次 ⇒ 勾上又立刻取消,看起来像"点不动"。
|
|
14
|
+
*/
|
|
15
|
+
import { type ReactElement } from 'react';
|
|
16
|
+
export interface CheckboxProps {
|
|
17
|
+
checked: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* 半选(横杠)。**它不是属性,是 DOM 上的一个 IDL 字段**——`indeterminate` 只能命令式赋值,
|
|
20
|
+
* 写进 JSX 里 React 会当未知属性丢掉。所以这里挂 ref 在 effect 里现设(组头那颗用它说
|
|
21
|
+
* "这个项目只勾了几条")。
|
|
22
|
+
*/
|
|
23
|
+
indeterminate?: boolean;
|
|
24
|
+
onChange: (checked: boolean) => void;
|
|
25
|
+
/** 只给读屏的名字(视觉上这一格没有文字)。 */
|
|
26
|
+
label: string;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function Checkbox({ checked, indeterminate, onChange, label, disabled, }: CheckboxProps): ReactElement;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MenuItemButton`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的同名组件,出处与三条
|
|
3
|
+
* 改写规则见 `ui.css` 头注):菜单里的一行,宿主菜单容器渲染它、键盘巡游也认它。
|
|
4
|
+
*
|
|
5
|
+
* **形状必须与宿主自己那四行(pin/rename/fork/archive)逐字同构**:`<div class=itemWrap>` 里先是可选
|
|
6
|
+
* 的发丝线(`role="separator"`),再是 `<button type="button" role="menuitem">`,按钮内是
|
|
7
|
+
* 图标位 + 标签位(+ 官方的快捷键位)。少一层、换个标签,这一行就会在键盘顺序、hover 底色或
|
|
8
|
+
* `.itemLabel` 的省略号上跟邻行错开。
|
|
9
|
+
*
|
|
10
|
+
* **置灰用原生 `disabled`**(不是 `aria-disabled`):宿主菜单行的契约就是原生属性——`MenuItemButton`
|
|
11
|
+
* 只把它落到 `<button>` 上,本仓用例也按 `toBeDisabled()` 判。
|
|
12
|
+
*
|
|
13
|
+
* **砍掉的**:`shortcut`(本插件这一行没有快捷键,故不带 `aria-keyshortcuts`,也不渲染快捷键位)。
|
|
14
|
+
* `danger` / `separatorBefore` / `disabled` / `icon` / `children` / `onSelect` 都在用,保留。
|
|
15
|
+
*/
|
|
16
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
17
|
+
import './styles.js';
|
|
18
|
+
export interface MenuItemButtonProps {
|
|
19
|
+
/** 可见的行标签。 */
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
/** 前导图标。 */
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
/** 这一行不能被激活。 */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
/** 用破坏性的行配色(error 字色 + danger 悬停底)。 */
|
|
26
|
+
danger?: boolean;
|
|
27
|
+
/** 这一行另起一组(上方一条发丝线)。 */
|
|
28
|
+
separatorBefore?: boolean;
|
|
29
|
+
/** 行被激活。 */
|
|
30
|
+
onSelect: () => void;
|
|
31
|
+
}
|
|
32
|
+
export declare function MenuItemButton({ children, icon, disabled, danger, separatorBefore, onSelect, }: MenuItemButtonProps): ReactElement;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Modal`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 Modal 与 useModalLayer,
|
|
3
|
+
* 出处与三条改写规则见 `ui.css` 头注):整视口蒙层 + 居中卡片,portal 到 `document.body`。
|
|
4
|
+
*
|
|
5
|
+
* **保留下来的行为**(官方"用户真的依赖"的那几条,`references/practices.md` 点名了 Modal 的焦点与
|
|
6
|
+
* Escape):`role="dialog"` + `aria-modal` + `aria-label`(标题即是可及名)、蒙层点击关闭、
|
|
7
|
+
* Escape 关闭、Tab 在框内循环、打开时把焦点送进框(首个 `[data-modal-autofocus]`,没有就首个可聚焦
|
|
8
|
+
* 元素,再没有就框本体)、关闭后把焦点还给打开它的那个元素、自动焦点不留焦点环
|
|
9
|
+
* (`data-dsh-automatic-focus`,宿主主题读它来抑制 outline——键盘导航或 blur 即恢复)。
|
|
10
|
+
*
|
|
11
|
+
* **砍掉的**(本插件用不到,砍掉是为了不摊开官方整个 Modal API):
|
|
12
|
+
* - `description` / `headless` / `className` / `contentClassName` / `onKeyDownCapture` /
|
|
13
|
+
* `backdropBlur` / `shortcutModal`:本插件两个确认框都是"标题 + 正文 + 两个按钮"的同一形状。
|
|
14
|
+
* - **官方的多层栈**(`layers` WeakMap + `closeTopModal` / `isBehindModal`):那是给"对话框里还能再开
|
|
15
|
+
* 对话框、且宿主菜单也在抢 Escape"的场景准备的。本插件同一时刻只会有一个确认框,所以这里只保留它
|
|
16
|
+
* 的**判据本身**——Escape 只在"自己就是文档里最后一个 `[role="dialog"][aria-modal="true"]`
|
|
17
|
+
* 或 `[role="menu"]`"时生效(`isTopLayer`)。这一条不能省:菜单/对话框先后打开时,
|
|
18
|
+
* "谁在最上面谁收 Escape"是用户能看见的规矩。
|
|
19
|
+
* - **输入法合成守卫**(`observeComposition`):本插件的对话框里没有文本输入框,不可能处在合成态。
|
|
20
|
+
*/
|
|
21
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
22
|
+
import './styles.js';
|
|
23
|
+
export interface ModalProps {
|
|
24
|
+
open: boolean;
|
|
25
|
+
/** 应用侧关闭(蒙层点击、Escape、关闭钮都走它)。 */
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
/** 标题:同时也是对话框的可及名。 */
|
|
28
|
+
title: string;
|
|
29
|
+
/** 关闭钮的可及名(本地化文案,由调用面给)。 */
|
|
30
|
+
closeLabel: string;
|
|
31
|
+
/** 动作行(取消/确认)。 */
|
|
32
|
+
footer?: ReactNode;
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
export declare function Modal({ open, onClose, title, closeLabel, footer, children }: ModalProps): ReactElement | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Tag`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 Tag,出处与三条改写规则见
|
|
3
|
+
* `ui.css` 头注):只读的状态胶囊。
|
|
4
|
+
*
|
|
5
|
+
* 官方那套有八个 tone(outline/solid/neutral/quiet/success/info/warning/danger),本插件只用
|
|
6
|
+
* **danger 一档**(确认框里的失败原文)⇒ 这里只有这一个 tone,`tone` 收成单值联合而不是"先铺开、
|
|
7
|
+
* 以后可能用得上"。要加第二档时,把对应那条 CSS 与这个联合一起加。
|
|
8
|
+
*/
|
|
9
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
10
|
+
import './styles.js';
|
|
11
|
+
export interface TagProps {
|
|
12
|
+
/** 唯一的色板:破坏性/失败面(error 色的 10% 透明底 + error 字)。 */
|
|
13
|
+
tone: 'danger';
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export declare function Tag({ tone, children }: TagProps): ReactElement;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Toast`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 Toast,出处与三条改写规则
|
|
3
|
+
* 见 `ui.css` 头注):顶部居中的一次性横幅——滑入、全不透明停留、淡出,然后回调 `onDone` 让 owner
|
|
4
|
+
* 卸载它。用 body portal 渲染,免得被有 transform / filter 的祖先困在它的盒子里。
|
|
5
|
+
*
|
|
6
|
+
* **保留下来的行为**:
|
|
7
|
+
* - 时长只有一个来源:`holdMs` 同时驱动卸载计时器与样式表的淡出延迟(后者以 `--dsh-toast-hold`
|
|
8
|
+
* 自定义属性读它)⇒ 两者不可能再对不上、把横幅卡在淡出中途卸载。
|
|
9
|
+
* - 同一个 `holdMs` 下,父组件重渲染**不延长**寿命(`onDone` 走 ref,不进 effect 依赖)。
|
|
10
|
+
* - 完成回调拿到的永远是最新的 `onDone`;已经淡完的动作不再接输入(CSS 里那张表面本就穿透点击)。
|
|
11
|
+
* - `tone="success"` 自带官方那枚圆圈对勾(此时忽略传入的 `icon`)。
|
|
12
|
+
*
|
|
13
|
+
* **砍掉的**:`anchor`(把横幅对齐到某个元素的水平中心——本插件的撤销提示就是窗口居中)、
|
|
14
|
+
* `actions[].prefix`(动作文案前的连接词,本插件只用「撤销」两个字)。**没砍 `text`/`icon`/`tone`/
|
|
15
|
+
* `holdMs`/`actions`/`onDone`**:唯一的调用点这几样都在用。
|
|
16
|
+
*/
|
|
17
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
18
|
+
import './styles.js';
|
|
19
|
+
export interface ToastAction {
|
|
20
|
+
label: string;
|
|
21
|
+
onClick: () => void;
|
|
22
|
+
}
|
|
23
|
+
export interface ToastProps {
|
|
24
|
+
/** 已解析好的横幅文案(本地化由调用面负责)。 */
|
|
25
|
+
text: string;
|
|
26
|
+
/** 前导 glyph;`tone="success"` 时被忽略(那一档自带对勾)。 */
|
|
27
|
+
icon?: ReactNode;
|
|
28
|
+
tone?: 'success';
|
|
29
|
+
/** 全不透明停留时长;缺省 3000。 */
|
|
30
|
+
holdMs?: number;
|
|
31
|
+
/** 句尾的行内动作(照着句子的顺序排)。 */
|
|
32
|
+
actions?: readonly ToastAction[];
|
|
33
|
+
/** 淡出完成时调用一次:在这里卸载。 */
|
|
34
|
+
onDone: () => void;
|
|
35
|
+
}
|
|
36
|
+
export declare function Toast({ text, icon, tone, holdMs, actions, onDone }: ToastProps): ReactElement;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Tooltip`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 Tooltip,出处与三条改写
|
|
3
|
+
* 规则见 `ui.css` 头注):把提示挂在锚点元素上,hover / 键盘 focus 才出。
|
|
4
|
+
*
|
|
5
|
+
* **保留下来的行为**:`side` 三个方位的定位(`right` 取锚点右缘再让出 10px,`bottom`/`top` 居中并按
|
|
6
|
+
* `gap` 让位)、视口边界收口(`edgeMargin=12`、`bottom`/`top` 装不下就翻面)、等 `ResizeObserver`
|
|
7
|
+
* 量到气泡尺寸再显示(所以不会先闪一个错位的框)、指针交互之后的 focus 不弹(见 `styles.ts` 的
|
|
8
|
+
* `isPointerModality`)、`portal` 时挂到 `document.body`(宿主菜单有裁剪与层叠上下文,锚点在菜单里
|
|
9
|
+
* 时必须 portal,否则气泡被裁掉)。
|
|
10
|
+
*
|
|
11
|
+
* **砍掉的**:`shortcutKeys`(本插件没有快捷键)、`openOnClick`/`pinned` 与随之而来的
|
|
12
|
+
* `useDismissOnOutsidePointer`、`aria-describedby` 关联(官方只在 pinned 时挂)、
|
|
13
|
+
* `TooltipSuppression`(那是给 HoverCard 预览用的互相压制)、`delayMs`/`focusDelayMs`/`maxWidth`
|
|
14
|
+
* (本插件唯一一处用的是零延迟默认值)。**没有砍掉 `side`/`gap`**:定位数学是三行分支,
|
|
15
|
+
* 留着才与官方同形。
|
|
16
|
+
*
|
|
17
|
+
* **与官方的一处差异**:官方把锚点自己的 `ref` 与它的 `ref` 合并后一起挂;这里要求锚点**不带
|
|
18
|
+
* 自己的 ref**(本插件唯一的调用点是一个裸 `<span>`),所以直接挂。要合并再补。
|
|
19
|
+
*/
|
|
20
|
+
import { type ReactElement } from 'react';
|
|
21
|
+
import './styles.js';
|
|
22
|
+
export type TooltipSide = 'right' | 'bottom' | 'top';
|
|
23
|
+
export interface TooltipProps {
|
|
24
|
+
/** 气泡文案;空串不显示文字(官方支持"只显示快捷键"的那种用法,本插件不用)。 */
|
|
25
|
+
label: string;
|
|
26
|
+
/** 相对锚点的方位;缺省 `right`。 */
|
|
27
|
+
side?: TooltipSide;
|
|
28
|
+
/** `bottom`/`top` 时锚点到气泡的距离(像素,缺省 8);`right` 不用它。 */
|
|
29
|
+
gap?: number;
|
|
30
|
+
/** 为真时不出气泡;锚点渲染完全不变(不会因为切换而重挂,进而打断它自己的 CSS 过渡)。 */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** 把气泡挂到 `document.body` 下(锚点在被裁剪的容器里时必须开)。 */
|
|
33
|
+
portal?: boolean;
|
|
34
|
+
/** 单一锚点元素。 */
|
|
35
|
+
children: ReactElement;
|
|
36
|
+
}
|
|
37
|
+
export declare function Tooltip({ label, side, gap, disabled, portal, children }: TooltipProps): ReactElement;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 两个纯格式化函数(**逐条抄自** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的
|
|
3
|
+
* `lib/index.js`;出处与改写规则见 `ui.css` 头注)。
|
|
4
|
+
*
|
|
5
|
+
* 它们是**纯函数、零 React、零 token**,所以「抄进来」在这里没有任何版本耦合的代价——真正随宿主换代
|
|
6
|
+
* 会变的是组件与样式,不是这两段算术。抄而不是自己写,为的是**输出逐字相同**:本仓的用例与真机读数
|
|
7
|
+
* 都钉在 `312B` / `4.2KB` / `1.5MB` 与「刚刚 / N分钟 / N小时 / N天 / N个月 / N年」这几个分桶上,
|
|
8
|
+
* 自己重写一遍就等于换了一套措辞。
|
|
9
|
+
*/
|
|
10
|
+
/** 时间分桶的单位词(官方 `relativeTime` 的返回联合,逐字相同)。 */
|
|
11
|
+
export type RelativeTimeUnit = 'now' | 'minutes' | 'hours' | 'days' | 'months' | 'years';
|
|
12
|
+
/** 紧凑的字节数文本(`312B`、`4.2KB`、`1.5MB`、`2.4GB`):整单位,选中单位小于十时带一位小数。 */
|
|
13
|
+
export declare function fileSizeText(bytes: number): string;
|
|
14
|
+
/** 时间分桶(`now` / 分钟 / 小时 / 天 / 月 / 年);措辞由调用面自己组(见 `trash-panel.tsx`)。 */
|
|
15
|
+
export declare function relativeTime(at: number, now: number): {
|
|
16
|
+
unit: RelativeTimeUnit;
|
|
17
|
+
n: number;
|
|
18
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本插件 UI 层要用的图标(**逐条抄自** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2`
|
|
3
|
+
* 随包发布的明文产物 `lib/index.js`;出处与改写规则见 `ui.css` 头注)。
|
|
4
|
+
*
|
|
5
|
+
* 只留本插件用到的:垃圾桶(删除动作 / 回收站)、警示(失败面)、打开的项目文件夹、
|
|
6
|
+
* 关闭叉(对话框)、圆圈对勾(撤销提示的 success 档)、对勾(行尾兜底勾选开关,2026-10-17
|
|
7
|
+
* 第十三批加;行首主勾选走原生 Checkbox)、下折与右折 chevron(清单的分组折叠头,
|
|
8
|
+
* 2026-10-15 加:`DisclosureRow` 那套折叠行的两态 glyph)。「刷新」那枚随按钮一起删了
|
|
9
|
+
* (2026-10-16,见 `trash-panel.tsx` 工具排的注释)——本文件不留无人消费的几何。
|
|
10
|
+
* **几何、`viewBox`、`strokeWidth` 逐字不动**——它们是官方图标集的形状,改了就不再与宿主同屏同形。
|
|
11
|
+
*
|
|
12
|
+
* 两条与官方实现的差异(都是减法,不是改写):
|
|
13
|
+
* - 只暴露 `size`(官方是 `{size, className, strokeWidth}` 三件套 + `Medium` 1.3px 那一档):
|
|
14
|
+
* 本插件调用处只传 `size`,加别的等于摊开官方整个图标 API。
|
|
15
|
+
* - 不做 `ICON_REGULAR_STROKE` / `ICON_MEDIUM_STROKE` 常量:只有一个档位,直接写 1。
|
|
16
|
+
*/
|
|
17
|
+
import type { ReactElement } from 'react';
|
|
18
|
+
/** 宿主图标组件的实际使用面:一个方形边长,色走 `currentColor`。 */
|
|
19
|
+
export interface IconProps {
|
|
20
|
+
size?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function IconTrashOutlineRegular({ size }: IconProps): ReactElement;
|
|
23
|
+
export declare function IconWarningOutlineRegular({ size }: IconProps): ReactElement;
|
|
24
|
+
export declare function IconFolderOpenOutlineRegular({ size }: IconProps): ReactElement;
|
|
25
|
+
export declare function IconCloseOutlineRegular({ size }: IconProps): ReactElement;
|
|
26
|
+
/** 撤销提示 success 档的前导 glyph(官方 `Toast` 在 `tone="success"` 时自带的那一枚)。 */
|
|
27
|
+
export declare function IconCheckCircleOutlineRegular({ size }: IconProps): ReactElement;
|
|
28
|
+
/** 行尾兜底「勾选开关」那枚对勾(状态点/归档行;官方 `IconCheckOutlineRegular`,`viewBox`/`d` 逐字;2026-10-17 第十三批加)。 */
|
|
29
|
+
export declare function IconCheckOutlineRegular({ size }: IconProps): ReactElement;
|
|
30
|
+
/** 分组头「展开中」那一枚(官方 `IconChevronDownOutlineRegular`,`viewBox`/`d` 逐字)。 */
|
|
31
|
+
export declare function IconChevronDownOutlineRegular({ size }: IconProps): ReactElement;
|
|
32
|
+
/** 分组头「已折叠」那一枚(官方 `IconChevronRightOutlineRegular`)。 */
|
|
33
|
+
export declare function IconChevronRightOutlineRegular({ size }: IconProps): ReactElement;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 安静的红:**只给字色、不给底**。宿主控件的 `.sd-button-ghost:hover` / `:active` 是普通类,
|
|
4
|
+
* 而作者态的内联 `background` 永远赢过类规则 ⇒ 铺了底的红按钮在点错之前不给任何视觉回应。
|
|
5
|
+
* 整页唯一那块实心红另有写法(`variant="primary"` + 就地覆盖 primary 那对 token),不在这里。
|
|
6
|
+
*/
|
|
7
|
+
export declare const QUIET_DANGER: {
|
|
8
|
+
readonly color: "var(--dsw-alias-state-error-primary)";
|
|
9
|
+
};
|
|
10
|
+
/** 单行截断。宽档那一列与可拖的分栏天然会挤文字,逐处手写这三行就会各写各的。 */
|
|
11
|
+
export declare const ELLIPSIS: CSSProperties;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Translate } from './copy.js';
|
|
2
|
+
import { type SessionDeleteRemote } from './remote.js';
|
|
3
|
+
export type UndoToastDeps = Pick<SessionDeleteRemote, 'restore' | 'applyOutcomes'>;
|
|
4
|
+
/**
|
|
5
|
+
* 一次待撤销的事。两形(第十三批②起):单条删除(主语是会话名)/批量删除(主语是条数)。
|
|
6
|
+
* 同一个 Toast 宿主、同一停留时长;撤销动作按形走——单条 `restore`,批量**一次** `applyOutcomes`。
|
|
7
|
+
*/
|
|
8
|
+
export type UndoNotice = {
|
|
9
|
+
/** 每次发布递增:Toast 靠它重挂载,同一段文案连续出现两次也能重新计一次停留。 */
|
|
10
|
+
seq: number;
|
|
11
|
+
/** 撤销失败时把原因顶上来:这时不再有「撤销」动作,只剩原文。 */
|
|
12
|
+
failure?: string;
|
|
13
|
+
} & ({
|
|
14
|
+
kind: 'one';
|
|
15
|
+
/** 回收站条目 id——撤销就是拿它去 `restore`。 */
|
|
16
|
+
entryId: string;
|
|
17
|
+
/** 删除那一刻的会话名(菜单槽投影的 `displayTitle`;取不到时是空串)。 */
|
|
18
|
+
title: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'batch';
|
|
21
|
+
/** 撤销名单:只收成功支里非 null 的条目 id(「被同批带走」的成员跟着组走,不单列)。 */
|
|
22
|
+
entryIds: readonly string[];
|
|
23
|
+
/** 横幅那句的条数 = 那一次成功支的条数(含幂等带走的)——与撤销名单是两个数,各说各的。 */
|
|
24
|
+
n: number;
|
|
25
|
+
});
|
|
26
|
+
/** 删除成功后发布一条撤销提示(单条)。 */
|
|
27
|
+
export declare function publishUndoNotice(input: {
|
|
28
|
+
entryId: string;
|
|
29
|
+
title: string;
|
|
30
|
+
}): void;
|
|
31
|
+
/** 批量删除成功后发布一条撤销提示(第十三批②):覆盖上一条,只报这一次移走的条数。 */
|
|
32
|
+
export declare function publishUndoBatchNotice(input: {
|
|
33
|
+
entryIds: readonly string[];
|
|
34
|
+
n: number;
|
|
35
|
+
}): void;
|
|
36
|
+
/** 撤销成功后把提示收掉(用户点「撤销」、或横幅自己走完停留时间)。 */
|
|
37
|
+
export declare function clearUndoNotice(): void;
|
|
38
|
+
/** 供组件订阅。 */
|
|
39
|
+
export declare function useUndoNotice(): UndoNotice | null;
|
|
40
|
+
/** 提示停留时长。设计稿 §7 定死:**6 秒**(Toast 自己的默认是 3 秒)。 */
|
|
41
|
+
export declare const UNDO_HOLD_MS = 6000;
|
|
42
|
+
/**
|
|
43
|
+
* 常驻的撤销提示宿主。挂 `shell.overlay`(框架级浮层;槽位说明原文 "a toast stack or a status
|
|
44
|
+
* pill all belong here",见 asar 里 `shell.overlay` 的声明):没有待撤销的事时什么都不渲染。
|
|
45
|
+
*/
|
|
46
|
+
export declare function UndoToast({ deps, t }: {
|
|
47
|
+
deps: UndoToastDeps;
|
|
48
|
+
t?: Translate;
|
|
49
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 清单列与详情列之间那根**可拖的宽度把手**(2026-10-15 加,设计稿 §7 状态④)。
|
|
3
|
+
*
|
|
4
|
+
* 三件事住在这里,因为它们互相咬合、分开测各自都不成立:
|
|
5
|
+
* ① **宽度夹在两个界之间**。下限由调用方给(回收站那条的来历写在 `trash-panel.tsx`:真 Chromium
|
|
6
|
+
* 上扫出来的,约束不是"两颗按钮放得下"——它们 `flex:none`,再窄也是文字列缩;而是「删除于 … ·
|
|
7
|
+
* 大小」那一行不再被省略号截断),上限 = 面板宽 − `reserve`。**`reserve` 的数值与来历都住在调用方**
|
|
8
|
+
* (`DETAIL_RESERVE`,那三件东西怎么加出来的见设计稿 2026-10-15 §7.1 与现状稿 §8 的几何读数):
|
|
9
|
+
* 这一层不复制那份算式——上一版在这里钉过一个"480 ⇒ 900 那档正好回 420",调用方改到 489 之后
|
|
10
|
+
* 它就成了假数,而 900 那档的真实读数是**清单被夹到 411**(比默认窄 9px,还能往里拖)。
|
|
11
|
+
* ② **夹在"读"的时候也做一次**。窗口变窄不触发任何写入,若只在写入时夹,之前拖出来的超宽
|
|
12
|
+
* 清单会一直留到下次交互 ⇒ `listWidth` 每次渲染都按当前 `panelWidth` 重算。
|
|
13
|
+
* ③ **按下才算在拖**。指针捕获挂在把手元素上(`setPointerCapture`),move 只可能来自这一根把手;
|
|
14
|
+
* `origin === null` 时 move 直接返回——鼠标扫过不该改布局。抬起不用显式 release:规范里
|
|
15
|
+
* pointerup/pointercancel 会隐式释放捕获,多写那一行只会引入"指针已不活跃"的抛错面。
|
|
16
|
+
* **但"结束"不能只挂在把手上**:拖动途中把手会被卸载(面板窄过断点、或清单被读空 ⇒ `split`
|
|
17
|
+
* 翻了),而卸载后的节点收不到任何事件 ⇒ `resizing` 永远挂着:面板根留着 `.sd-resizing`
|
|
18
|
+
* (关文字选中),下次变宽时把手还带着 `data-dragging` 长回来。所以拖住期间另在 window 上兜
|
|
19
|
+
* 一层 `pointerup`/`pointercancel`(复用同一个 `stop`)。move 仍然只认把手,捕获那套一点没动。
|
|
20
|
+
*
|
|
21
|
+
* 键盘那条不是装饰:`role="separator"` 可聚焦、←→ 各一格、Home 回默认、End 到上限。
|
|
22
|
+
* 这一代的 `--dsw-focus-ring-color` 是 transparent(宿主自己那份焦点环靠别的属性撑),
|
|
23
|
+
* 所以焦点态的可见性由 `ui.css` 里 `.sd-width-handle:focus-visible::after` 那一条负责。
|
|
24
|
+
*/
|
|
25
|
+
import { type KeyboardEvent, type PointerEvent } from 'react';
|
|
26
|
+
export interface ColumnResizeArgs {
|
|
27
|
+
/** 面板自己那一块的 border-box 宽(来自 `usePanelWidth`,不是 `window.innerWidth`)。 */
|
|
28
|
+
panelWidth: number;
|
|
29
|
+
/** 没拖过之后的默认宽度(也是 Home 的落点)。 */
|
|
30
|
+
defaultValue: number;
|
|
31
|
+
/** 清单列下限(读得开的下沿,来历见调用方那条注释)。 */
|
|
32
|
+
min: number;
|
|
33
|
+
/** 详情那一侧要留下的地板:正文最小可读宽 + 间隙与分隔 + 页内边距。 */
|
|
34
|
+
reserve: number;
|
|
35
|
+
/** 键盘一格的像素数。 */
|
|
36
|
+
step: number;
|
|
37
|
+
/** 读屏名字(走 `copy.ts`,这里不写字面量)。 */
|
|
38
|
+
label: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ColumnResizeHandleProps {
|
|
41
|
+
role: 'separator';
|
|
42
|
+
'aria-orientation': 'vertical';
|
|
43
|
+
'aria-label': string;
|
|
44
|
+
'aria-valuenow': number;
|
|
45
|
+
'aria-valuemin': number;
|
|
46
|
+
'aria-valuemax': number;
|
|
47
|
+
tabIndex: number;
|
|
48
|
+
'data-dragging'?: '';
|
|
49
|
+
onPointerDown: (event: PointerEvent<HTMLElement>) => void;
|
|
50
|
+
onPointerMove: (event: PointerEvent<HTMLElement>) => void;
|
|
51
|
+
onPointerUp: (event: PointerEvent<HTMLElement>) => void;
|
|
52
|
+
onPointerCancel: (event: PointerEvent<HTMLElement>) => void;
|
|
53
|
+
onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
|
54
|
+
}
|
|
55
|
+
export interface ColumnResize {
|
|
56
|
+
listWidth: number;
|
|
57
|
+
max: number;
|
|
58
|
+
/** 拖动中:面板根挂 `.sd-resizing`(关掉文字选中),把手挂 `data-dragging`。 */
|
|
59
|
+
resizing: boolean;
|
|
60
|
+
handleProps: ColumnResizeHandleProps;
|
|
61
|
+
}
|
|
62
|
+
export declare function useColumnResize({ panelWidth, defaultValue, min, reserve, step, label, }: ColumnResizeArgs): ColumnResize;
|