dsh-codinput 0.1.1 → 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/CHANGELOG.md +31 -0
- package/README.en.md +30 -26
- package/README.md +32 -28
- package/TESTING.md +10 -2
- package/assets/icon-white.svg +11 -0
- package/assets/icon.png +0 -0
- package/assets/icon.svg +11 -1
- package/assets/preview-ball-detail.png +0 -0
- package/assets/preview-float.png +0 -0
- package/assets/preview-normal.png +0 -0
- package/assets/preview-side.png +0 -0
- package/assets/preview-split.png +0 -0
- package/lib/client.js +57 -37
- package/package.json +1 -1
- package/src/client/attachments.ts +34 -0
- package/src/client/command.ts +52 -12
- package/src/client/components/AttachmentRail.tsx +20 -1
- package/src/client/components/CodinputBar.tsx +13 -35
- package/src/client/components/CodinputSurface.tsx +42 -8
- package/src/client/components/ImageLightbox.tsx +61 -0
- package/src/client/components/StatsRow.tsx +76 -43
- package/src/client/components/Toolbar.tsx +12 -36
- package/src/client/host-types.ts +6 -1
- package/src/client/i18n.ts +16 -2
- package/src/client/icons.tsx +33 -27
- package/src/client/sidebar.tsx +66 -7
- package/src/client/sideinput.ts +33 -7
- package/src/client/stats.ts +156 -53
- package/src/client/styles.ts +27 -7
package/src/client/sideinput.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface SidebarCarryState {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface SidebarCarry {
|
|
24
|
-
/** 本次承载的令牌(同会话多 body
|
|
24
|
+
/** 本次承载的令牌(同会话多 body 时,可见者优先、同可见最早者渲染编辑面)。 */
|
|
25
25
|
readonly token: number;
|
|
26
26
|
/** 可见性/展开态变化时更新(同一 body 生命周期内)。 */
|
|
27
27
|
set(state: SidebarCarryState): void;
|
|
@@ -53,11 +53,12 @@ function recompute(): void {
|
|
|
53
53
|
for (const [token, claim] of claims) {
|
|
54
54
|
const prev = next.get(claim.sessionId);
|
|
55
55
|
// 同一会话多 body(分栏里复制出第二个 Codinput 标签)时取「最可见」的一份,
|
|
56
|
-
//
|
|
56
|
+
// 编辑面归属可见者(owner 随 visible 走——否则编辑面会留在看不见的标签里,
|
|
57
|
+
// 正在看的标签只剩提示),同可见时归最早声明者,杜绝双编辑面。
|
|
57
58
|
if (!prev) {
|
|
58
59
|
next.set(claim.sessionId, { visible: claim.visible, expanded: claim.expanded, owner: token });
|
|
59
60
|
} else if (!prev.visible && claim.visible) {
|
|
60
|
-
next.set(claim.sessionId, {
|
|
61
|
+
next.set(claim.sessionId, { visible: true, expanded: claim.expanded, owner: token });
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
if (sameSnapshot(next)) return;
|
|
@@ -93,6 +94,11 @@ export function isSidebarInputCarried(sessionId: string | undefined): boolean {
|
|
|
93
94
|
return sessionId !== undefined && snapshot.has(sessionId);
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/** 当前被侧栏承载的会话 id 列表(/codinput 退出时广播关闭标签用)。 */
|
|
98
|
+
export function carriedSessionIds(): string[] {
|
|
99
|
+
return [...snapshot.keys()];
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
/** 任一会话被侧栏承载(主遮蔽条目需保持挂载以执行隐藏)。 */
|
|
97
103
|
export function isAnySidebarInputCarried(): boolean {
|
|
98
104
|
return snapshot.size > 0;
|
|
@@ -105,14 +111,30 @@ export function sidebarCarryState(sessionId: string | undefined): SidebarCarrySt
|
|
|
105
111
|
|
|
106
112
|
/**
|
|
107
113
|
* 编辑面归属:同会话被多个 body 承载(分栏里复制出第二个 Codinput 标签)时,
|
|
108
|
-
*
|
|
109
|
-
*
|
|
114
|
+
* 可见者优先,同可见归最早声明者。`token === 0`(尚未声明)一律返回 true——
|
|
115
|
+
* 避免首帧把还没拿到令牌的自己判成非归属者。
|
|
110
116
|
*/
|
|
111
117
|
export function isSidebarCarryOwner(sessionId: string | undefined, token: number): boolean {
|
|
112
118
|
if (token === 0 || sessionId === undefined) return true;
|
|
113
119
|
return snapshot.get(sessionId)?.owner === token;
|
|
114
120
|
}
|
|
115
121
|
|
|
122
|
+
|
|
123
|
+
/** 标签关闭请求总线:/codinput 从侧栏退出时,命令侧请求 body 关掉自己的标签。 */
|
|
124
|
+
const tabCloseListeners = new Set<(sessionId: string) => void>();
|
|
125
|
+
|
|
126
|
+
/** 请求关闭某会话的侧栏 Codinput 标签(无 body 在听时无害)。 */
|
|
127
|
+
export function requestSidebarTabClose(sessionId: string): void {
|
|
128
|
+
for (const fn of tabCloseListeners) fn(sessionId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function subscribeSidebarTabClose(fn: (sessionId: string) => void): () => void {
|
|
132
|
+
tabCloseListeners.add(fn);
|
|
133
|
+
return () => {
|
|
134
|
+
tabCloseListeners.delete(fn);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
116
138
|
/** 归属订阅(body 用):令牌变化、其他 body 挂载/卸载都会重渲染。 */
|
|
117
139
|
export function useSidebarCarryOwner(sessionId: string | undefined, token: number): boolean {
|
|
118
140
|
const [owner, setOwner] = useState(() => isSidebarCarryOwner(sessionId, token));
|
|
@@ -150,12 +172,16 @@ export function useSidebarInputCarried(sessionId: string | undefined): boolean {
|
|
|
150
172
|
|
|
151
173
|
/**
|
|
152
174
|
* 侧栏承载态订阅(小球用):`carried` = 本会话输入在侧栏里,
|
|
153
|
-
* `collapsed` =
|
|
175
|
+
* `collapsed` = 输入面不可见(承载但侧栏已收起 → 输入入口需要小球唤出)。
|
|
176
|
+
* 浮动面板 visible=true——输入就在眼前,即使侧栏恰好收起也不出球。
|
|
154
177
|
*/
|
|
155
178
|
export function useSidebarCarry(sessionId: string | undefined): { carried: boolean; collapsed: boolean } {
|
|
156
179
|
const read = (): { carried: boolean; collapsed: boolean } => {
|
|
157
180
|
const state = sidebarCarryState(sessionId);
|
|
158
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
carried: state !== undefined,
|
|
183
|
+
collapsed: state !== undefined && state.expanded === false && state.visible === false,
|
|
184
|
+
};
|
|
159
185
|
};
|
|
160
186
|
const [value, setValue] = useState(read);
|
|
161
187
|
useEffect(() => {
|
package/src/client/stats.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* 数据行数据源(卡片下方):逐值复刻官方 StatsPills 的取数与计算——
|
|
3
|
+
* `sessionStats` 投影优先(turns/steps/llm/tool/ttft/decode 全量),缺席时
|
|
4
|
+
* 从 chat legacy 节点按官方 deriveStats 派生;tok/s = decodeTokens ÷
|
|
5
|
+
* decodeMs(会话级持久值,非流式采样),缓存命中/用量分桶同官方公式。
|
|
6
6
|
* 注意:useProjection 是真实 hook,键探测必须是固定次数的顶层调用。
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -19,18 +19,24 @@ const PROJECTION_KEYS = [
|
|
|
19
19
|
export interface SessionStats {
|
|
20
20
|
turns?: number;
|
|
21
21
|
steps?: number;
|
|
22
|
-
/**
|
|
22
|
+
/** 输出速度(tok/s)= decodeTokens ÷ (decodeMs/1000);decodeMs>0 才有。 */
|
|
23
23
|
tokensPerSecond?: number;
|
|
24
24
|
totalTokens?: number;
|
|
25
|
-
|
|
25
|
+
/** 官方 formatCacheHitPercent 文本("88"/"99.9"/"100")。 */
|
|
26
|
+
cacheHitText?: string;
|
|
26
27
|
/** 上下文占用百分比(projectedTokens ?? pressureTokens,除以 contextWindow)。 */
|
|
27
28
|
contextPct?: number;
|
|
28
|
-
/**
|
|
29
|
-
|
|
29
|
+
/** 会话统计面板:模型用时 / 工具调用用时 / TTFT 均值(官方格式化)。 */
|
|
30
|
+
llmMs?: number;
|
|
31
|
+
toolMs?: number;
|
|
30
32
|
ttftMs?: number;
|
|
31
|
-
|
|
33
|
+
ttftSteps?: number;
|
|
34
|
+
decodeMs?: number;
|
|
35
|
+
decodeTokens?: number;
|
|
36
|
+
/** Token 用量面板:原始分桶。 */
|
|
32
37
|
uncachedInputTokens?: number;
|
|
33
38
|
cacheReadTokens?: number;
|
|
39
|
+
cacheWriteTokens?: number;
|
|
34
40
|
outputTokens?: number;
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -47,6 +53,18 @@ interface ContextPressure {
|
|
|
47
53
|
projectedTokens?: number;
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/** 官方 sessionStats 投影/deriveStats 的输出形状。 */
|
|
57
|
+
interface ChatStats {
|
|
58
|
+
turns?: number;
|
|
59
|
+
steps?: number;
|
|
60
|
+
llmMs?: number;
|
|
61
|
+
toolMs?: number;
|
|
62
|
+
ttftMs?: number;
|
|
63
|
+
ttftSteps?: number;
|
|
64
|
+
decodeMs?: number;
|
|
65
|
+
decodeTokens?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
function num(value: unknown): number | undefined {
|
|
51
69
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
52
70
|
}
|
|
@@ -74,47 +92,105 @@ function readContextPressure(raw: unknown): ContextPressure | undefined {
|
|
|
74
92
|
return cp.contextWindow !== undefined && cp.pressureTokens !== undefined ? cp : undefined;
|
|
75
93
|
}
|
|
76
94
|
|
|
77
|
-
function
|
|
78
|
-
if (!raw || typeof raw !== 'object') return
|
|
95
|
+
function readSessionStats(raw: unknown): ChatStats | undefined {
|
|
96
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
79
97
|
const r = raw as Record<string, unknown>;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
const stats: ChatStats = {
|
|
99
|
+
turns: num(r['turns']),
|
|
100
|
+
steps: num(r['steps']),
|
|
101
|
+
llmMs: num(r['llmMs']),
|
|
102
|
+
toolMs: num(r['toolMs']),
|
|
103
|
+
ttftMs: num(r['ttftMs']),
|
|
104
|
+
ttftSteps: num(r['ttftSteps']),
|
|
105
|
+
decodeMs: num(r['decodeMs']),
|
|
106
|
+
decodeTokens: num(r['decodeTokens']),
|
|
107
|
+
};
|
|
108
|
+
return Object.values(stats).some((v) => v !== undefined) ? stats : undefined;
|
|
83
109
|
}
|
|
84
110
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
let
|
|
96
|
-
let
|
|
97
|
-
let
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
/**
|
|
112
|
+
* 官方 deriveStats 逐行复刻:assistant 节点计轮/步并带 timing(stepStartTime
|
|
113
|
+
* →firstTokenTime→completedTime)与 usage.outputTokens;tool-result 以
|
|
114
|
+
* callTime 起算工具用时。
|
|
115
|
+
*/
|
|
116
|
+
export function deriveChatStats(nodes: unknown): ChatStats | undefined {
|
|
117
|
+
if (!Array.isArray(nodes)) return undefined;
|
|
118
|
+
const turns = new Set<unknown>();
|
|
119
|
+
let steps = 0;
|
|
120
|
+
let llmMs = 0;
|
|
121
|
+
let toolMs = 0;
|
|
122
|
+
let ttftMs = 0;
|
|
123
|
+
let ttftSteps = 0;
|
|
124
|
+
let decodeMs = 0;
|
|
125
|
+
let decodeTokens = 0;
|
|
126
|
+
for (const raw of nodes) {
|
|
127
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
128
|
+
const node = raw as Record<string, unknown>;
|
|
129
|
+
if (node['kind'] === 'tool-result') {
|
|
130
|
+
const callTime = num(node['callTime']);
|
|
131
|
+
const time = num(node['time']);
|
|
132
|
+
if (callTime !== undefined && time !== undefined) toolMs += Math.max(0, time - callTime);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (node['kind'] !== 'assistant') continue;
|
|
136
|
+
turns.add(node['turn']);
|
|
137
|
+
steps += 1;
|
|
138
|
+
const timing = node['timing'] as Record<string, unknown> | undefined;
|
|
139
|
+
const stepStartTime = num(timing?.['stepStartTime']);
|
|
140
|
+
const firstTokenTime = num(timing?.['firstTokenTime']);
|
|
141
|
+
const completedTime = num(timing?.['completedTime']);
|
|
142
|
+
if (stepStartTime !== undefined && completedTime !== undefined) {
|
|
143
|
+
llmMs += Math.max(0, completedTime - stepStartTime);
|
|
144
|
+
}
|
|
145
|
+
if (stepStartTime !== undefined && firstTokenTime !== undefined) {
|
|
146
|
+
ttftMs += Math.max(0, firstTokenTime - stepStartTime);
|
|
147
|
+
ttftSteps += 1;
|
|
148
|
+
}
|
|
149
|
+
if (firstTokenTime !== undefined && completedTime !== undefined) {
|
|
150
|
+
decodeMs += Math.max(0, completedTime - firstTokenTime);
|
|
151
|
+
}
|
|
152
|
+
const usage = node['usage'] as Record<string, unknown> | undefined;
|
|
153
|
+
const out = num(usage?.['outputTokens']);
|
|
154
|
+
if (out !== undefined && out >= 0) decodeTokens += out;
|
|
102
155
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
156
|
+
if (nodes.length === 0) return undefined;
|
|
157
|
+
return { turns: turns.size, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 官方 formatTokensPerSecond:≥10 取整,<10 保留 1 位小数。 */
|
|
161
|
+
export function formatTokensPerSecond(tps: number): string {
|
|
162
|
+
const clamped = Math.max(0, tps);
|
|
163
|
+
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 官方 formatCacheHitPercent 语义:整除 100 → "100";整数舍入 <100 → 整数;
|
|
168
|
+
* 否则递增小数位直到舍入仍 <100(99.9 / 99.99 …)。
|
|
169
|
+
*/
|
|
170
|
+
export function formatCacheHitPercent(cacheReadTokens: number, promptTokens: number): string | null {
|
|
171
|
+
if (promptTokens <= 0) return null;
|
|
172
|
+
if (cacheReadTokens >= promptTokens) return '100';
|
|
173
|
+
for (let places = 0; places < 20; places++) {
|
|
174
|
+
const factor = 10 ** places;
|
|
175
|
+
const rounded = Math.round((cacheReadTokens / promptTokens) * 100 * factor) / factor;
|
|
176
|
+
if (rounded < 100) return places === 0 ? String(rounded) : rounded.toFixed(places);
|
|
109
177
|
}
|
|
110
|
-
|
|
111
|
-
|
|
178
|
+
return '99.99';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 官方 formatDuration:<60s 一位小数秒,否则 分+秒。 */
|
|
182
|
+
export function formatDuration(ms: number): string {
|
|
183
|
+
const s = ms / 1000;
|
|
184
|
+
if (s < 60) return `${Math.round(s * 10) / 10}s`;
|
|
185
|
+
const whole = Math.round(s);
|
|
186
|
+
return `${Math.floor(whole / 60)}分${whole % 60}秒`;
|
|
112
187
|
}
|
|
113
188
|
|
|
114
189
|
/** 每个键一个固定位置的 hook 调用——跨渲染的调用次数与顺序恒定。 */
|
|
115
190
|
export function useSessionStats(
|
|
116
191
|
useProjection: UseProjection | undefined,
|
|
117
192
|
sessionId: string | undefined,
|
|
193
|
+
chatNodes: unknown,
|
|
118
194
|
): SessionStats | null {
|
|
119
195
|
const k0 = useProjection ? useProjection(PROJECTION_KEYS[0]) : undefined;
|
|
120
196
|
const k1 = useProjection ? useProjection(PROJECTION_KEYS[1]) : undefined;
|
|
@@ -124,14 +200,25 @@ export function useSessionStats(
|
|
|
124
200
|
|
|
125
201
|
const usage = readTokenUsage(k0);
|
|
126
202
|
const pressure = readContextPressure(k1);
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const alt2 = readTurnsSteps(k4) ?? {};
|
|
203
|
+
// 官方:sessionStats 投影优先,缺席从 chat legacy 节点派生。
|
|
204
|
+
const chat = readSessionStats(k4) ?? deriveChatStats(chatNodes) ?? readTurnsSteps(k2, k3);
|
|
130
205
|
|
|
131
206
|
const stats: SessionStats = {};
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
207
|
+
if (chat) {
|
|
208
|
+
if (chat.turns !== undefined || chat.steps !== undefined) {
|
|
209
|
+
stats.turns = chat.turns;
|
|
210
|
+
stats.steps = chat.steps;
|
|
211
|
+
}
|
|
212
|
+
if (chat.llmMs !== undefined) stats.llmMs = chat.llmMs;
|
|
213
|
+
if (chat.toolMs !== undefined) stats.toolMs = chat.toolMs;
|
|
214
|
+
if (chat.ttftMs !== undefined) stats.ttftMs = chat.ttftMs;
|
|
215
|
+
if (chat.ttftSteps !== undefined) stats.ttftSteps = chat.ttftSteps;
|
|
216
|
+
if (chat.decodeMs !== undefined) stats.decodeMs = chat.decodeMs;
|
|
217
|
+
if (chat.decodeTokens !== undefined) stats.decodeTokens = chat.decodeTokens;
|
|
218
|
+
if ((chat.decodeMs ?? 0) > 0) {
|
|
219
|
+
stats.tokensPerSecond = (chat.decodeTokens ?? 0) / (chat.decodeMs! / 1000);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
135
222
|
if (usage) {
|
|
136
223
|
const total =
|
|
137
224
|
(usage.cacheReadTokens ?? 0) +
|
|
@@ -142,14 +229,15 @@ export function useSessionStats(
|
|
|
142
229
|
stats.totalTokens = total;
|
|
143
230
|
stats.uncachedInputTokens = usage.uncachedInputTokens;
|
|
144
231
|
stats.cacheReadTokens = usage.cacheReadTokens;
|
|
232
|
+
stats.cacheWriteTokens = usage.cacheWriteTokens;
|
|
145
233
|
stats.outputTokens = usage.outputTokens;
|
|
146
234
|
}
|
|
235
|
+
// 官方:分母 = 三段输入桶(缓存读+写+未缓存输入)。
|
|
147
236
|
const inputTotal = (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0) + (usage.uncachedInputTokens ?? 0);
|
|
148
|
-
if (inputTotal > 0)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (sample.ttftMs !== undefined && sample.ttftMs > 0) stats.ttftMs = sample.ttftMs;
|
|
237
|
+
if (inputTotal > 0) {
|
|
238
|
+
const text = formatCacheHitPercent(usage.cacheReadTokens ?? 0, inputTotal);
|
|
239
|
+
if (text !== null) stats.cacheHitText = text;
|
|
240
|
+
}
|
|
153
241
|
}
|
|
154
242
|
const window_ = pressure?.contextWindow;
|
|
155
243
|
// 官方 contextOccupancy:优先 projectedTokens。
|
|
@@ -157,11 +245,26 @@ export function useSessionStats(
|
|
|
157
245
|
if (window_ !== undefined && window_ > 0 && used !== undefined) {
|
|
158
246
|
stats.contextPct = (used / window_) * 100;
|
|
159
247
|
}
|
|
248
|
+
void sessionId;
|
|
160
249
|
return Object.keys(stats).length > 0 ? stats : null;
|
|
161
250
|
}
|
|
162
251
|
|
|
252
|
+
function readTurnsSteps(k2: unknown, k3: unknown): ChatStats | undefined {
|
|
253
|
+
const pick = (raw: unknown): ChatStats | undefined => {
|
|
254
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
255
|
+
const r = raw as Record<string, unknown>;
|
|
256
|
+
const turns = num(r['turns']) ?? num(r['turnCount']);
|
|
257
|
+
const steps = num(r['steps']) ?? num(r['stepCount']);
|
|
258
|
+
return turns !== undefined || steps !== undefined ? { turns, steps } : undefined;
|
|
259
|
+
};
|
|
260
|
+
return pick(k2) ?? pick(k3);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 官方 formatTokens:<1e3 原值,否则 K/M(≥100 取整、<100 一位小数)。 */
|
|
163
264
|
export function formatTokens(tokens: number): string {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return `${tokens} tok`;
|
|
265
|
+
const scaled = (candidate: number): string =>
|
|
266
|
+
candidate >= 100 ? String(Math.round(candidate)) : String(Math.round(candidate * 10) / 10);
|
|
267
|
+
if (tokens < 1e3) return `${tokens} tok`;
|
|
268
|
+
if (tokens < 1e6) return `${scaled(tokens / 1e3)}K tok`;
|
|
269
|
+
return `${scaled(tokens / 1e6)}M tok`;
|
|
167
270
|
}
|
package/src/client/styles.ts
CHANGED
|
@@ -21,8 +21,13 @@ const CSS = `
|
|
|
21
21
|
会话态(无包裹约束)不再被拉满,左右居中;拖边调节后由内联 max-width 接管。 */
|
|
22
22
|
.dci-card { box-sizing: border-box; width: 100%; max-width: var(--dsh-composer-card-max-width, 760px); margin-inline: auto;
|
|
23
23
|
background: var(--dsw-specific-input-major, color-mix(in srgb, currentColor 4%, transparent));
|
|
24
|
-
box-shadow: var(--dsw-elevation-soft, none); font-size:
|
|
25
|
-
|
|
24
|
+
box-shadow: var(--dsw-elevation-soft, none); font-size: var(--dsh-content-font-size, 14px);
|
|
25
|
+
line-height: calc(24px + var(--dsh-content-font-delta, 0px));
|
|
26
|
+
border: 0; border-radius: var(--dsw-radius-panel, 22px); flex-direction: column; gap: 0; padding-top: 4px; display: flex; position: relative; min-width: 0; }
|
|
27
|
+
/* 主模式 surface 复刻官方 composer root:侧向 clearance 内缩 + 底部 4px,
|
|
28
|
+
卡片与数据行由数据行自身 padding-top 定距(官方 root 无 gap,dock 上边距 4px)。
|
|
29
|
+
side/float 形态自带铺满布局,不参与。 */
|
|
30
|
+
.dci-surface:not([data-side]):not([data-float]) { padding: 0 var(--dsh-composer-side-clearance, 0px) 4px; align-items: center; gap: 0; }
|
|
26
31
|
/* 卡片边缘调节把手:左右调宽、上下调高(对称灵敏度),悬停显形。 */
|
|
27
32
|
.dci-rip { position: absolute; z-index: 60; touch-action: none; }
|
|
28
33
|
.dci-rip[data-axis="y"] { left: 12px; right: 12px; height: 9px; cursor: ns-resize; }
|
|
@@ -388,10 +393,25 @@ button.dci-attach-body { color: inherit; font: inherit; text-align: left; backgr
|
|
|
388
393
|
background: var(--dsw-alias-label-primary-foreground, #fff); transition: transform 120ms ease; }
|
|
389
394
|
.dci-switch[aria-checked="true"]::after { transform: translateX(16px); }
|
|
390
395
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
396
|
+
|
|
397
|
+
/* ===== 原图预览(官方 primitives ImageLightbox 逐值复刻)===== */
|
|
398
|
+
.dci-lightbox { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 40px; }
|
|
399
|
+
.dci-lightbox-mask { position: absolute; inset: 0; background: var(--dsw-alias-bg-mask-1, rgba(0, 0, 0, 0.55));
|
|
400
|
+
backdrop-filter: var(--dsw-mask-blur, blur(4px)); }
|
|
401
|
+
.dci-lightbox-image { position: relative; max-width: min(100%, 1600px); max-height: calc(100vh - 80px);
|
|
402
|
+
object-fit: contain; border-radius: var(--dsw-radius-lg, 16px); background: var(--dsw-specific-input-major);
|
|
403
|
+
border: 0; box-shadow: var(--dsw-elevation-prominent, 0 10px 34px rgba(0, 0, 0, 0.35)); }
|
|
404
|
+
.dci-lightbox-close { position: fixed; top: 20px; right: 20px; z-index: 1; display: grid; place-items: center;
|
|
405
|
+
width: 36px; height: 36px; border: 0.5px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, transparent));
|
|
406
|
+
border-radius: 999px; corner-shape: round; background: var(--dsw-specific-input-major);
|
|
407
|
+
color: var(--dsw-alias-label-primary, currentColor); cursor: pointer; }
|
|
408
|
+
.dci-lightbox-close:focus-visible { outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary, currentColor)); outline-offset: 3px; }
|
|
409
|
+
|
|
410
|
+
/* ===== 数据行(官方 uV2eYG_dock 复刻:root 内居中、无横向 padding)===== */
|
|
411
|
+
.dci-stat-root { max-width: 100%; box-sizing: border-box; width: 100%;
|
|
412
|
+
padding: 4px 0 0; font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 1px);
|
|
413
|
+
line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
|
|
414
|
+
justify-content: center; gap: 12px; display: flex; }
|
|
395
415
|
.dci-stat-anchor { min-width: 0; display: inline-flex; position: relative; }
|
|
396
416
|
|
|
397
417
|
/* 数据行详情面板(官方 bRhRbq 复刻:锚点上方弹出) */
|
|
@@ -411,7 +431,7 @@ button.dci-attach-body { color: inherit; font: inherit; text-align: left; backgr
|
|
|
411
431
|
gap: 6px 16px; margin: 0; display: grid; }
|
|
412
432
|
.dci-stat-details dt, .dci-stat-details dd { min-width: 0; margin: 0; }
|
|
413
433
|
.dci-stat-details dd { color: var(--dsw-alias-label-secondary, inherit); font-variant-numeric: tabular-nums; text-align: right; }
|
|
414
|
-
.dci-stat-pill { box-sizing: border-box; max-width: 100%; color: var(--dsw-alias-label-tertiary, currentColor);
|
|
434
|
+
.dci-stat-pill { font: inherit; box-sizing: border-box; max-width: 100%; color: var(--dsw-alias-label-tertiary, currentColor);
|
|
415
435
|
white-space: nowrap; background: transparent; border: none; border-radius: 24px; align-items: center; gap: 6px;
|
|
416
436
|
padding: 1px 8px; display: inline-flex; cursor: pointer; font-variant-numeric: tabular-nums; }
|
|
417
437
|
.dci-stat-pill svg { flex: none; width: 14px; height: 14px; }
|