@iasiv5/dsh-quota-watch 0.1.0 → 0.1.1

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 CHANGED
@@ -2,21 +2,20 @@
2
2
 
3
3
  [English summary](#english-summary)
4
4
 
5
- 个人版 DSH 额度监控插件,以常驻悬浮球集中显示 GLM Coding Plan 窗口与 GitHub Copilot 的 provider 上报额度快照。它不统计 DSH 会话 token、不保存历史,也不需要在浏览器输入任何 provider 密钥。
5
+ 个人版 DSH 额度监控插件,以常驻悬浮胶囊集中显示 GLM Coding Plan 窗口与 GitHub Copilot 的 provider 上报额度快照。它不统计 DSH 会话 token、不保存历史,也不需要在浏览器输入任何 provider 密钥。
6
6
 
7
7
  ## 功能
8
8
 
9
- - **悬浮球**:常驻屏幕右下角(距边 16px 起步),可整体拖拽到任意位置,位置跨重启记忆,越界自动钳回视口。球面为分段进度环——GLM 占上半环、Copilot 占下半环,已用超过 80% 转橙、超过 95% 转红,任一 provider 超 95% 时整球轻微脉动(尊重系统「减少动态效果」设置,开启时不脉动);环心保留配额仪表盘图标,颜色使用 DSH 主题语义变量,自动适配浅色 / 深色主题。悬停提示与 aria-label 实时给出「GLM x% · Copilot y%」摘要。
10
- - **胶囊**:右键悬浮球可切换为一行式胶囊(`GLM 13% · Copilot 24%`),阈值变色规则相同;右键胶囊切回悬浮球。球 / 胶囊形态跨重启记忆。
9
+ - **悬浮胶囊**(0.1.1 起为唯一浮动形态,圆球已退役):常驻屏幕右下角(距边 16px 起步),可整体拖拽到任意位置,位置跨重启记忆,越界自动钳回视口。胶囊一行显示「GLM 13% · Copilot 24%」,已用超过 80% 转橙、超过 95% 转红,任一 provider 超 95% 时胶囊轻微脉动(尊重系统「减少动态效果」设置,开启时不脉动);颜色使用 DSH 主题语义变量,自动适配浅色 / 深色主题。悬停提示与 aria-label 实时给出摘要。
11
10
  - **面板**:点击球或胶囊展开浮动详情面板(半透明毛玻璃底,宽 `min(320px, 100vw−24px)`,靠边自动翻侧,滚动 / 缩放跟随),头部带标题与 ✕,Esc 或点击面板外关闭。概览列两个 provider 行,点击进入详情:GLM 为今日 Tokens / 调用次数双大数字 + 五列对齐窗口网格(MCP(月)→ 5 小时 → 每月 → 每周固定排序,周窗口未下发时显示「每周额度 · ♾️(无限)」占位)+ 分模型用量行;Copilot 为可用额度、本期已用(credits 消耗 + 弱化百分比)、重置时间三行。接口未下发有效 `remaining` / `percent_remaining` 时该行不渲染,`credits_used` 绝不用于推算余额。
12
- - **侧边栏卡片(渐进增强)**:Web profile 侧栏底部区块存在时,卡片照常显示(摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测),卡片右上角有一个悬浮球切换按钮(悬停显形),可在卡片上隐藏 / 唤回悬浮球;侧栏收起(Web 窄栏)或侧栏整体不存在(Desktop 收起态)时卡片自然缺席、无报错,由悬浮球承载入口。
13
- - **右键菜单**:切为胶囊 / 切为悬浮球 · 显示 / 隐藏侧边栏卡片 · 立即刷新 · 隐藏悬浮球。菜单项随上下文增减(无侧栏卡片挂载点时不显示卡片项)。「隐藏悬浮球」为**会话级**——刷新页面即恢复,卡片上的切换按钮可在会话内直接唤回,任何时刻都至少有一条找回路径。
14
- - 凭据缺失的 provider 不渲染摘要行、环弧塌缩为零(两者皆缺时悬浮球与卡片一并隐藏);探测失败时保留最后一次成功数据并在行上加 ⚠ 过期标记(⚠ 只出现在面板与卡片行内,不上球)。
11
+ - **侧边栏卡片(渐进增强)**:Web profile 侧栏底部区块存在时,卡片照常显示(摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测),卡片右上角有一个胶囊切换按钮(悬停显形),可在卡片上隐藏 / 唤回悬浮胶囊;侧栏收起(Web 窄栏)或侧栏整体不存在(Desktop 收起态)时卡片自然缺席、无报错,由悬浮胶囊承载入口。
12
+ - **右键菜单**:显示 / 隐藏侧边栏卡片 · 立即刷新 · 隐藏悬浮胶囊。菜单项随上下文增减(无侧栏卡片挂载点时不显示卡片项)。「隐藏悬浮胶囊」为**会话级**——刷新页面即恢复,卡片上的切换按钮可在会话内直接唤回,任何时刻都至少有一条找回路径。
13
+ - 凭据缺失的 provider 不渲染摘要行与胶囊分段(两者皆缺时悬浮胶囊与卡片一并隐藏);探测失败时保留最后一次成功数据并在行上加 ⚠ 过期标记(⚠ 只出现在面板与卡片行内,不上球)。
15
14
  - 宿主默认每 60 秒探测(窗口 + 用量 + Copilot);profile patch 可将 `pollIntervalSec` 设置在 30–3600 秒。浏览器页签可见时每 30 秒刷新视图,隐藏时暂停。
16
15
 
17
16
  ## 兼容与配置
18
17
 
19
- - 要求 DSH `0.1.7-rc.2` 或更高;适配 `web` 与 `desktop` 两种 profile——Desktop 收起侧栏(整体消失)时由悬浮球承载入口,卡片为 Web 渐进增强。
18
+ - 要求 DSH `0.1.7-rc.2` 或更高;适配 `web` 与 `desktop` 两种 profile——Desktop 收起侧栏(整体消失)时由悬浮胶囊承载入口,卡片为 Web 渐进增强。
20
19
  - GLM 从 `llm-pi-ai` profile 中配置的 provider `apiKeyEnv` credential reference 解析当前 key。支持 `zai-coding-cn`(国内)与 `zai` / `zai-coding`(国际)路由。每次探测都重新解析引用,用户轮换凭据后下一轮读取新值。
21
20
  - Copilot 使用内置 `llm-pi-ai/github-copilot` OAuth grant 的 `payload.refresh` 访问用户配额快照;不使用给模型请求的短期 `payload.access`,不使用 `COPILOT_GITHUB_TOKEN` 环境引用。每轮重新读取 grant,以支持用户重新授权或轮换。
22
21
  - 如需修改宿主周期,在 profile 的 `quota-watch` row 配置 `pollIntervalSec`;禁用插件请使用 DSH 插件管理器。
@@ -48,4 +47,4 @@ dsh plugin --profile web add @iasiv5/dsh-quota-watch
48
47
 
49
48
  ## English summary
50
49
 
51
- DSH Quota Watch is a personal DSH quota monitor built around a persistent floating ball: a segmented dual-arc ring (GLM top half, Copilot bottom half) that turns amber past 80% and red past 95%, draggable anywhere with position remembered across restarts. Right-click toggles a compact capsule showing `GLM x% · Copilot y%`; clicking the ball or capsule opens a floating glass panel with provider summaries and details, closable via ✕, Esc or an outside click. A sidebar card remains an opportunistic extra entry on web profiles (with a corner button to hide or restore the ball) and silently disappears whenever the sidebar does — collapsed web sidebars and the desktop profile are covered by the floating ball alone. A right-click menu offers mode switch, card visibility, an immediate provider re-probe and a session-scoped ball hide. Credentials are resolved on every host-side probe, only normalized values reach the browser, no usage history is kept, and a Copilot balance is never inferred from credits used. Colors ride DSH semantic theme tokens with hardcoded fallbacks; all UI lives inside a shadow root (ADR 0001).
50
+ DSH Quota Watch is a personal DSH quota monitor built around a persistent floating capsule: a one-line `GLM x% · Copilot y%` summary that turns amber past 80% and red past 95%, draggable anywhere with position remembered across restarts. Clicking the capsule opens a floating glass panel whose overview reuses the classic 0.0.18 summary rows (label · bar · percent · extra · chevron) and whose detail pages replicate the 0.0.18 big numbers, window grid and per-model list; the panel closes via ✕, Esc or an outside click. A sidebar card remains an opportunistic extra entry on web profiles (with a corner button to hide or restore the capsule) and silently disappears whenever the sidebar does — collapsed web sidebars and the desktop profile are covered by the floating capsule alone. A right-click menu offers card visibility, an immediate provider re-probe and a session-scoped capsule hide. Credentials are resolved on every host-side probe, only normalized values reach the browser, no usage history is kept, and a Copilot balance is never inferred from credits used. Colors ride DSH semantic theme tokens with hardcoded fallbacks; all UI lives inside a shadow root (ADR 0001).
package/lib/client.js CHANGED
@@ -83,7 +83,6 @@ function remainingToUsed(percentRemaining) {
83
83
 
84
84
  // src/client/prefs.mjs
85
85
  var FLOAT_GEOMETRY_KEY = "dsh-quota-watch:float-geometry";
86
- var FLOAT_MODE_KEY = "dsh-quota-watch:float-mode";
87
86
  var SURFACE_FLAGS_KEY = "dsh-quota-watch:surface-flags";
88
87
  var MARGIN = 8;
89
88
  function clampPoint(point, viewport, size = 0) {
@@ -115,21 +114,6 @@ function saveFloatGeometry(storage, point) {
115
114
  } catch {
116
115
  }
117
116
  }
118
- function loadFloatMode(storage) {
119
- try {
120
- const raw = storage.getItem(FLOAT_MODE_KEY);
121
- return raw === "capsule" ? "capsule" : "ball";
122
- } catch {
123
- return "ball";
124
- }
125
- }
126
- function saveFloatMode(storage, mode) {
127
- if (mode !== "ball" && mode !== "capsule") throw new TypeError(`invalid float mode: ${mode}`);
128
- try {
129
- storage.setItem(FLOAT_MODE_KEY, mode);
130
- } catch {
131
- }
132
- }
133
117
  function loadSurfaceFlags(storage) {
134
118
  try {
135
119
  const raw = storage.getItem(SURFACE_FLAGS_KEY);
@@ -155,24 +139,19 @@ var name = "quota-watch-client";
155
139
  var inject = [];
156
140
  var CARD_SELECTOR = "[data-dsh-quota-watch-card]";
157
141
  var FETCH_TIMEOUT_MS = 15e3;
158
- var BALL_SIZE = 38;
159
- var BALL_MARGIN = 16;
160
- var RING_RADIUS = 15;
161
- var RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
162
- var RING_HALF_GAP = 2;
142
+ var SURFACE_SIZE = 38;
143
+ var SURFACE_MARGIN = 16;
163
144
  var COPY = {
164
145
  zh: {
165
146
  title: "Token\u989D\u5EA6",
166
147
  openDetails: "\u67E5\u770B\u8BE6\u60C5",
167
148
  close: "\u5173\u95ED",
168
149
  menu: {
169
- toggleToBall: "\u5207\u4E3A\u60AC\u6D6E\u7403",
170
- toggleToCapsule: "\u5207\u4E3A\u80F6\u56CA",
171
150
  showCard: "\u663E\u793A\u4FA7\u8FB9\u680F\u5361\u7247",
172
151
  hideCard: "\u9690\u85CF\u4FA7\u8FB9\u680F\u5361\u7247",
173
152
  refreshNow: "\u7ACB\u5373\u5237\u65B0",
174
- hideBall: "\u9690\u85CF\u60AC\u6D6E\u7403",
175
- showBall: "\u663E\u793A\u60AC\u6D6E\u7403"
153
+ hideBall: "\u9690\u85CF\u60AC\u6D6E\u80F6\u56CA",
154
+ showBall: "\u663E\u793A\u60AC\u6D6E\u80F6\u56CA"
176
155
  },
177
156
  backToOverview: "\u8FD4\u56DE\u989D\u5EA6\u6982\u89C8",
178
157
  providerNames: { glm: "GLM", copilot: "Copilot" },
@@ -203,13 +182,11 @@ var COPY = {
203
182
  openDetails: "View details",
204
183
  close: "Close",
205
184
  menu: {
206
- toggleToBall: "Switch to ball",
207
- toggleToCapsule: "Switch to capsule",
208
185
  showCard: "Show sidebar card",
209
186
  hideCard: "Hide sidebar card",
210
187
  refreshNow: "Refresh now",
211
- hideBall: "Hide floating ball",
212
- showBall: "Show floating ball"
188
+ hideBall: "Hide floating capsule",
189
+ showBall: "Show floating capsule"
213
190
  },
214
191
  backToOverview: "Back to quota overview",
215
192
  providerNames: { glm: "GLM", copilot: "Copilot" },
@@ -264,43 +241,9 @@ var STYLE_TEXT = `
264
241
  .dqw-ball-toggle[data-ball-hidden="true"] { color: var(--dsw-alias-button-primary-fill, #5b8def); }
265
242
  .dqw-ball-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
266
243
  .dqw-ball-toggle svg { display: block; width: 14px; height: 14px; }
267
- .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
268
- .dqw-overview-row { display: flex; align-items: center; width: 100%; gap: 6px; margin: 0; padding: 5px 4px; border: 0; border-radius: 8px; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
269
- .dqw-overview-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
270
- .dqw-overview-row:focus-visible { outline: 1px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: -1px; }
271
- .dqw-overview-error { display: flex; align-items: center; gap: 6px; padding: 5px 4px; }
272
- .dqw-bignums { display: flex; gap: 6px; }
273
- .dqw-big { flex: 1; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); border-radius: 8px; padding: 5px 8px; }
274
- .dqw-big .dqw-big-value { font-size: 15px; font-weight: 650; line-height: 19px; font-variant-numeric: tabular-nums; }
275
- .dqw-big .dqw-big-label { font-size: 9.5px; line-height: 13px; opacity: .65; }
276
- .dqw-muted { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
277
- .dqw-models { border-top: 1px dashed var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; display: flex; flex-direction: column; gap: 3px; }
278
- .dqw-model-row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 15px; font-variant-numeric: tabular-nums; }
279
- .dqw-model-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: .85; }
280
- .dqw-model-value { flex: none; font-weight: 600; }
281
- .dqw-kv { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
282
- .dqw-kv-label { flex: none; opacity: .65; }
283
- .dqw-kv-value { min-width: 0; text-align: right; font-weight: 600; }
284
- .dqw-kv-sub { font-weight: 400; opacity: .55; }
285
- .dqw-wins { display: grid; grid-template-columns: max-content max-content max-content max-content 1fr; column-gap: 6px; row-gap: 3px; align-items: baseline; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
286
- .dqw-win-name { opacity: .65; }
287
- .dqw-win-mid { opacity: .65; white-space: nowrap; }
288
- .dqw-win-used { opacity: .65; white-space: nowrap; }
289
- .dqw-win-val { min-width: 0; font-weight: 600; }
290
- .dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
291
244
  `;
292
245
  var FLOAT_STYLE_TEXT = `
293
246
  :host { position: fixed; z-index: 2147483000; }
294
- .dqw-ball { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; margin: 0; padding: 0; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 50%; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(10px) saturate(1.2); backdrop-filter: blur(10px) saturate(1.2); box-shadow: 0 4px 14px rgba(0,0,0,.22); color: var(--dsw-alias-label-primary, inherit); font: inherit; cursor: pointer; }
295
- .dqw-ball:hover { border-color: var(--dsw-alias-border-primary, var(--dsw-alias-border-secondary, rgba(128,128,128,.35))); }
296
- .dqw-ball:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: 2px; }
297
- .dqw-ring { position: absolute; inset: 0; width: 100%; height: 100%; }
298
- .dqw-ring-track { fill: none; stroke: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); stroke-width: 3.5; }
299
- .dqw-ring-glm { fill: none; stroke: var(--dsw-alias-button-primary-fill, #5b8def); stroke-width: 3.5; stroke-linecap: round; transition: stroke-dasharray .3s ease; }
300
- .dqw-ring-copilot { fill: none; stroke: var(--dsw-alias-label-success, #3fb950); stroke-width: 3.5; stroke-linecap: round; transition: stroke-dasharray .3s ease; }
301
- .dqw-ring-glm.warn, .dqw-ring-copilot.warn { stroke: var(--dsw-alias-label-warning, #d29922); }
302
- .dqw-ring-glm.danger, .dqw-ring-copilot.danger { stroke: var(--dsw-alias-label-danger, #c93c3c); }
303
- .dqw-ball > svg:not(.dqw-ring) { position: relative; width: 16px; height: 16px; }
304
247
  .dqw-capsule { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 999px; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(10px) saturate(1.2); backdrop-filter: blur(10px) saturate(1.2); box-shadow: 0 4px 14px rgba(0,0,0,.22); color: var(--dsw-alias-label-primary, inherit); font: inherit; font-size: 11px; line-height: 24px; cursor: pointer; user-select: none; white-space: nowrap; }
305
248
  .dqw-capsule:hover { border-color: var(--dsw-alias-border-primary, var(--dsw-alias-border-secondary, rgba(128,128,128,.35))); }
306
249
  .dqw-capsule:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: 2px; }
@@ -308,14 +251,14 @@ var FLOAT_STYLE_TEXT = `
308
251
  .dqw-capsule-glm, .dqw-capsule-copilot { font-variant-numeric: tabular-nums; font-weight: 600; }
309
252
  .dqw-capsule-glm.warn, .dqw-capsule-copilot.warn { color: var(--dsw-alias-label-warning, #d29922); }
310
253
  .dqw-capsule-glm.danger, .dqw-capsule-copilot.danger { color: var(--dsw-alias-label-danger, #c93c3c); }
254
+ @media (prefers-reduced-motion: no-preference) {
255
+ .dqw-capsule[data-alert] { animation: dqw-pulse 1.6s ease-in-out infinite; }
256
+ }
257
+ @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
311
258
  .dqw-menu { position: fixed; min-width: 150px; padding: 4px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 88%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); font: inherit; display: flex; flex-direction: column; }
312
259
  .dqw-menu[hidden] { display: none; }
313
260
  .dqw-menu-item { display: flex; align-items: center; gap: 6px; margin: 0; padding: 6px 10px; border: 0; border-radius: 6px; background: transparent; color: inherit; font: inherit; font-size: 11px; line-height: 16px; text-align: left; cursor: pointer; }
314
261
  .dqw-menu-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
315
- @media (prefers-reduced-motion: no-preference) {
316
- .dqw-ball[data-alert] { animation: dqw-ball-pulse 1.6s ease-in-out infinite; }
317
- }
318
- @keyframes dqw-ball-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }
319
262
  .dqw-panel { position: fixed; width: min(320px, calc(100vw - 24px)); max-height: calc(100vh - 24px); overflow-y: auto; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); display: flex; flex-direction: column; gap: 8px; font: inherit; }
320
263
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .dqw-panel { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); } }
321
264
  .dqw-panel[hidden] { display: none; }
@@ -327,6 +270,43 @@ var FLOAT_STYLE_TEXT = `
327
270
  .dqw-panel-back:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
328
271
  .dqw-panel-close { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 24px; height: 24px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; font: inherit; font-size: 12px; line-height: 1; }
329
272
  .dqw-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
273
+ .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
274
+ .dqw-row-summary { display: flex; align-items: center; gap: 6px; padding: 3px 6px; margin: 0 -2px; border-radius: 8px; }
275
+ .dqw-row-summary[data-expandable] { cursor: pointer; }
276
+ .dqw-row-summary[data-expandable]:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.12)); }
277
+ .dqw-row-summary:focus-visible { outline: 1px solid var(--dsw-alias-button-primary-fill, #5b8def); outline-offset: -1px; }
278
+ .dqw-row-summary.is-error { cursor: default; }
279
+ .dqw-label { font-size: 11px; font-weight: 600; width: 52px; flex: none; }
280
+ .dqw-label[data-action="refresh"] { cursor: pointer; border-radius: 4px; }
281
+ .dqw-label[data-action="refresh"]:hover { opacity: .75; }
282
+ .dqw-bar { display: block; flex: 1; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
283
+ .dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
284
+ .dqw-bar-fill.warn { background: var(--dsw-alias-label-warning, #d29922); }
285
+ .dqw-bar-fill.danger { background: var(--dsw-alias-label-danger, #c93c3c); }
286
+ .dqw-pct { font-size: 10.5px; line-height: 14px; font-variant-numeric: tabular-nums; width: 30px; text-align: right; flex: none; }
287
+ .dqw-extra { font-size: 10px; line-height: 14px; opacity: .72; font-variant-numeric: tabular-nums; flex: none; min-width: 34px; text-align: right; }
288
+ .dqw-chev { font-size: 9px; line-height: 14px; opacity: .6; flex: none; width: 10px; text-align: center; }
289
+ .dqw-stale-mark { flex: none; font-size: 9px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); cursor: help; }
290
+ .dqw-errtext { font-size: 10px; line-height: 14px; opacity: .7; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
291
+ .dqw-bignums { display: flex; gap: 6px; }
292
+ .dqw-big { flex: 1; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); border-radius: 8px; padding: 5px 8px; }
293
+ .dqw-big .dqw-big-value { font-size: 15px; font-weight: 650; line-height: 19px; font-variant-numeric: tabular-nums; }
294
+ .dqw-big .dqw-big-label { font-size: 9.5px; line-height: 13px; opacity: .65; }
295
+ .dqw-muted { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
296
+ .dqw-models { border-top: 1px dashed var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; display: flex; flex-direction: column; gap: 3px; }
297
+ .dqw-model-row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 15px; font-variant-numeric: tabular-nums; }
298
+ .dqw-model-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: .85; }
299
+ .dqw-model-value { flex: none; font-weight: 600; }
300
+ .dqw-kv { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
301
+ .dqw-kv-label { flex: none; opacity: .65; }
302
+ .dqw-kv-value { min-width: 0; text-align: right; font-weight: 600; }
303
+ .dqw-kv-sub { font-weight: 400; opacity: .55; }
304
+ .dqw-wins { display: grid; grid-template-columns: max-content max-content max-content max-content 1fr; column-gap: 6px; row-gap: 3px; align-items: baseline; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
305
+ .dqw-win-name { opacity: .65; }
306
+ .dqw-win-mid { opacity: .65; white-space: nowrap; }
307
+ .dqw-win-used { opacity: .65; white-space: nowrap; }
308
+ .dqw-win-val { min-width: 0; font-weight: 600; }
309
+ .dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
330
310
  `;
331
311
  function localeFor(win) {
332
312
  return String(win?.navigator?.language ?? "zh").toLowerCase().startsWith("zh") ? "zh" : "en";
@@ -408,35 +388,6 @@ function menuItem(doc, action, label) {
408
388
  item.append(text(doc, "span", "", label));
409
389
  return item;
410
390
  }
411
- function ringSvg(doc) {
412
- const namespace = "http://www.w3.org/2000/svg";
413
- const svg = doc.createElementNS(namespace, "svg");
414
- svg.setAttribute("class", "dqw-ring");
415
- svg.setAttribute("viewBox", "0 0 38 38");
416
- svg.setAttribute("aria-hidden", "true");
417
- svg.setAttribute("focusable", "false");
418
- const track = doc.createElementNS(namespace, "circle");
419
- track.setAttribute("class", "dqw-ring-track");
420
- const glm = doc.createElementNS(namespace, "circle");
421
- glm.setAttribute("class", "dqw-ring-glm");
422
- glm.setAttribute("transform", "rotate(-90 19 19)");
423
- const copilot = doc.createElementNS(namespace, "circle");
424
- copilot.setAttribute("class", "dqw-ring-copilot");
425
- copilot.setAttribute("transform", "rotate(90 19 19)");
426
- track.setAttribute("cx", "19");
427
- track.setAttribute("cy", "19");
428
- track.setAttribute("r", String(RING_RADIUS));
429
- track.setAttribute("fill", "none");
430
- for (const circle of [glm, copilot]) {
431
- circle.setAttribute("cx", "19");
432
- circle.setAttribute("cy", "19");
433
- circle.setAttribute("r", String(RING_RADIUS));
434
- circle.setAttribute("fill", "none");
435
- circle.setAttribute("stroke-dasharray", `0 ${RING_CIRCUMFERENCE}`);
436
- }
437
- svg.append(track, glm, copilot);
438
- return svg;
439
- }
440
391
  function backIcon(doc) {
441
392
  const namespace = "http://www.w3.org/2000/svg";
442
393
  const svg = doc.createElementNS(namespace, "svg");
@@ -554,16 +505,6 @@ function mountQuotaCard({
554
505
  floatHost.dataset.dshQuotaWatchFloat = "";
555
506
  const floatRoot = floatHost.attachShadow({ mode: "open" });
556
507
  floatRoot.append(text(doc, "style", "", FLOAT_STYLE_TEXT));
557
- const buildBall = () => {
558
- const el = doc.createElement("button");
559
- el.type = "button";
560
- el.className = "dqw-ball";
561
- el.dataset.dshQuotaWatchBall = "";
562
- el.setAttribute("aria-haspopup", "dialog");
563
- el.setAttribute("aria-expanded", "false");
564
- el.append(ringSvg(doc), quotaIcon(doc));
565
- return el;
566
- };
567
508
  const buildCapsule = () => {
568
509
  const el = doc.createElement("div");
569
510
  el.className = "dqw-capsule";
@@ -579,7 +520,7 @@ function mountQuotaCard({
579
520
  );
580
521
  return el;
581
522
  };
582
- let surface = loadFloatMode(win.localStorage) === "capsule" ? buildCapsule() : buildBall();
523
+ let surface = buildCapsule();
583
524
  floatRoot.append(surface);
584
525
  const syncFloatVisibility = () => {
585
526
  surface.hidden = ballSessionHidden || dataHidden;
@@ -595,9 +536,9 @@ function mountQuotaCard({
595
536
  const viewHeight = win.innerHeight ?? 768;
596
537
  const saved = loadFloatGeometry(win.localStorage);
597
538
  const point = clampPoint(
598
- saved ?? { x: viewWidth - BALL_MARGIN - BALL_SIZE, y: viewHeight - BALL_MARGIN - BALL_SIZE },
539
+ saved ?? { x: viewWidth - SURFACE_MARGIN - SURFACE_SIZE, y: viewHeight - SURFACE_MARGIN - SURFACE_SIZE },
599
540
  { width: viewWidth, height: viewHeight },
600
- BALL_SIZE
541
+ SURFACE_SIZE
601
542
  );
602
543
  floatHost.style.left = `${Math.round(point.x)}px`;
603
544
  floatHost.style.top = `${Math.round(point.y)}px`;
@@ -643,7 +584,7 @@ function mountQuotaCard({
643
584
  const point = clampPoint(
644
585
  { x: event.clientX, y: event.clientY },
645
586
  { width: viewWidth, height: viewHeight },
646
- BALL_SIZE
587
+ SURFACE_SIZE
647
588
  );
648
589
  floatHost.style.left = `${Math.round(point.x)}px`;
649
590
  floatHost.style.top = `${Math.round(point.y)}px`;
@@ -729,44 +670,6 @@ function mountQuotaCard({
729
670
  row.append(text(doc, "span", "dqw-chev", openKey === key ? "\u25BE" : "\u25B8"));
730
671
  return [row];
731
672
  };
732
- const renderOverviewProvider = (key, model) => {
733
- if (model === null) return null;
734
- const label = text(doc, "span", "dqw-label", model.label);
735
- if (model.kind === "error") {
736
- const row2 = doc.createElement("div");
737
- row2.className = "dqw-overview-error";
738
- row2.append(label, text(doc, "span", "dqw-errtext", model.error ? `${copy.failed}: ${model.error}` : copy.failed));
739
- return row2;
740
- }
741
- const row = doc.createElement("button");
742
- row.type = "button";
743
- row.className = "dqw-overview-row";
744
- row.dataset.dshQuotaWatchPanelProvider = key;
745
- row.setAttribute("aria-expanded", "false");
746
- row.setAttribute("aria-label", `${model.label} \xB7 ${typeof model.percent === "number" ? formatPercent(model.percent, locale) : "\u2014"} \xB7 ${copy.openDetails}`);
747
- row.append(label);
748
- if (typeof model.percent === "number" && Number.isFinite(model.percent)) {
749
- const bar = doc.createElement("span");
750
- bar.className = "dqw-bar";
751
- bar.setAttribute("role", "progressbar");
752
- bar.setAttribute("aria-valuemin", "0");
753
- bar.setAttribute("aria-valuemax", "100");
754
- bar.setAttribute("aria-valuenow", String(Math.max(0, Math.min(100, model.percent))));
755
- const fill = text(doc, "span", fillClass(model.percent));
756
- fill.style.width = `${Math.max(0, Math.min(100, model.percent))}%`;
757
- bar.append(fill);
758
- row.append(bar);
759
- }
760
- if (model.stale) {
761
- const mark = text(doc, "span", "dqw-stale-mark", "\u26A0");
762
- mark.dataset.dshQuotaWatchStale = "";
763
- mark.title = model.error ?? copy.stale;
764
- row.append(mark);
765
- }
766
- row.append(text(doc, "span", "dqw-pct", typeof model.percent === "number" ? formatPercent(model.percent, locale) : "\u2014"));
767
- row.append(text(doc, "span", "dqw-chev", "\u25B8"));
768
- return row;
769
- };
770
673
  const panelHeader = ({ title, showBack = false }) => {
771
674
  const header = doc.createElement("div");
772
675
  header.className = "dqw-panel-header";
@@ -797,9 +700,9 @@ function mountQuotaCard({
797
700
  const glm = providers.find((provider) => provider?.key === "glm");
798
701
  const copilot = providers.find((provider) => provider?.key === "copilot");
799
702
  const rows = [
800
- renderOverviewProvider("glm", glmRowModel(glm)),
801
- renderOverviewProvider("copilot", copilotRowModel(copilot))
802
- ].filter(Boolean);
703
+ renderProvider("glm", glmRowModel(glm)),
704
+ renderProvider("copilot", copilotRowModel(copilot))
705
+ ].filter(Boolean).flat();
803
706
  if (rows.length === 0) {
804
707
  panel.append(text(doc, "p", "dqw-muted", lastUpdated.textContent || copy.loading));
805
708
  return;
@@ -807,6 +710,9 @@ function mountQuotaCard({
807
710
  const list = doc.createElement("div");
808
711
  list.className = "dqw-overview-list";
809
712
  list.append(...rows);
713
+ for (const row of list.querySelectorAll("[data-dsh-quota-watch-row]")) {
714
+ row.dataset.dshQuotaWatchPanelProvider = row.dataset.dshQuotaWatchRow;
715
+ }
810
716
  panel.append(list);
811
717
  };
812
718
  function renderDetail(detail, key, provider, fromPanelNav = false) {
@@ -958,13 +864,6 @@ function mountQuotaCard({
958
864
  placePanel();
959
865
  surface.setAttribute("aria-expanded", "true");
960
866
  };
961
- const applyArc = (circle, model) => {
962
- const pct = model && typeof model.percent === "number" && Number.isFinite(model.percent) ? Math.max(0, Math.min(100, model.percent)) : null;
963
- const fill = pct === null ? 0 : pct / 100 * (RING_CIRCUMFERENCE / 2 - RING_HALF_GAP);
964
- circle.setAttribute("stroke-dasharray", `${fill.toFixed(2)} ${(RING_CIRCUMFERENCE - fill).toFixed(2)}`);
965
- circle.classList.toggle("warn", pct !== null && pct > 80 && pct <= 95);
966
- circle.classList.toggle("danger", pct !== null && pct > 95);
967
- };
968
867
  const renderFloatFace = () => {
969
868
  const providers = snapshot?.providers ?? [];
970
869
  const glm = glmRowModel(providers.find((provider) => provider?.key === "glm"));
@@ -976,24 +875,19 @@ function mountQuotaCard({
976
875
  }
977
876
  syncFloatVisibility();
978
877
  if (floatHost.hidden) return;
979
- if (surface.dataset.dshQuotaWatchCapsule !== void 0) {
980
- const paint = (span, model) => {
981
- const visible = model !== null && typeof model.percent === "number" && Number.isFinite(model.percent);
982
- span.textContent = visible ? `${model.label} ${formatPercent(model.percent, locale)}` : "";
983
- span.classList.toggle("warn", visible && model.percent > 80 && model.percent <= 95);
984
- span.classList.toggle("danger", visible && model.percent > 95);
985
- return visible;
986
- };
987
- const glmVisible = paint(surface.querySelector(".dqw-capsule-glm"), glm);
988
- const copilotVisible = paint(surface.querySelector(".dqw-capsule-copilot"), copilot);
989
- surface.querySelector(".dqw-capsule-sep").hidden = !glmVisible || !copilotVisible;
990
- } else {
991
- applyArc(surface.querySelector(".dqw-ring-glm"), glm);
992
- applyArc(surface.querySelector(".dqw-ring-copilot"), copilot);
993
- const alert = [glm, copilot].some((model) => model !== null && typeof model.percent === "number" && model.percent > 95);
994
- if (alert) surface.setAttribute("data-alert", "true");
995
- else surface.removeAttribute("data-alert");
996
- }
878
+ const paint = (span, model) => {
879
+ const visible = model !== null && typeof model.percent === "number" && Number.isFinite(model.percent);
880
+ span.textContent = visible ? `${model.label} ${formatPercent(model.percent, locale)}` : "";
881
+ span.classList.toggle("warn", visible && model.percent > 80 && model.percent <= 95);
882
+ span.classList.toggle("danger", visible && model.percent > 95);
883
+ return visible;
884
+ };
885
+ const glmVisible = paint(surface.querySelector(".dqw-capsule-glm"), glm);
886
+ const copilotVisible = paint(surface.querySelector(".dqw-capsule-copilot"), copilot);
887
+ surface.querySelector(".dqw-capsule-sep").hidden = !glmVisible || !copilotVisible;
888
+ const alert = [glm, copilot].some((model) => model !== null && typeof model.percent === "number" && model.percent > 95);
889
+ if (alert) surface.setAttribute("data-alert", "true");
890
+ else surface.removeAttribute("data-alert");
997
891
  const summary = [glm, copilot].filter(Boolean).map((model) => `${model.label} ${formatPercent(model.percent, locale)}`).join(" \xB7 ");
998
892
  const label = summary === "" ? copy.title : locale === "zh" ? `${copy.title}\uFF1A${summary}` : `${copy.title}: ${summary}`;
999
893
  surface.setAttribute("aria-label", label);
@@ -1004,7 +898,7 @@ function mountQuotaCard({
1004
898
  suppressNextClick = false;
1005
899
  return;
1006
900
  }
1007
- if (openKey === "overview" && !panel.hidden) {
901
+ if (openKey !== void 0 && !panel.hidden) {
1008
902
  openKey = void 0;
1009
903
  syncPop();
1010
904
  return;
@@ -1016,25 +910,9 @@ function mountQuotaCard({
1016
910
  event.preventDefault();
1017
911
  onSurfaceClick();
1018
912
  };
1019
- const setFloatMode = (nextMode) => {
1020
- saveFloatMode(win.localStorage, nextMode);
1021
- openKey = void 0;
1022
- menu.hidden = true;
1023
- syncPop();
1024
- const previous = surface;
1025
- surface = nextMode === "capsule" ? buildCapsule() : buildBall();
1026
- previous.replaceWith(surface);
1027
- surface.addEventListener("click", onSurfaceClick);
1028
- surface.addEventListener("keydown", onSurfaceKeydown);
1029
- surface.addEventListener("contextmenu", onSurfaceContext);
1030
- disposeDrag();
1031
- disposeDrag = attachDrag(surface);
1032
- renderFloatFace();
1033
- };
1034
913
  const hasCardMount = () => Boolean(footArea(doc));
1035
914
  const openMenu = () => {
1036
- const isBall = surface.dataset.dshQuotaWatchBall !== void 0;
1037
- const items = [menuItem(doc, "toggle-mode", isBall ? copy.menu.toggleToCapsule : copy.menu.toggleToBall)];
915
+ const items = [];
1038
916
  if (hasCardMount()) {
1039
917
  items.push(menuItem(doc, "toggle-card", surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard));
1040
918
  }
@@ -1061,9 +939,7 @@ function mountQuotaCard({
1061
939
  const item = event.target?.closest?.("[data-menu]");
1062
940
  if (!item) return;
1063
941
  const action = item.dataset.menu;
1064
- if (action === "toggle-mode") {
1065
- setFloatMode(surface.dataset.dshQuotaWatchBall !== void 0 ? "capsule" : "ball");
1066
- } else if (action === "toggle-card") {
942
+ if (action === "toggle-card") {
1067
943
  setCardHidden(!surfaceFlags.cardHidden);
1068
944
  } else if (action === "refresh") {
1069
945
  void poll(true);
@@ -1224,6 +1100,15 @@ function mountQuotaCard({
1224
1100
  body.addEventListener("click", onBodyClick);
1225
1101
  body.addEventListener("keydown", onBodyKeydown);
1226
1102
  panel.addEventListener("click", onPanelClick);
1103
+ panel.addEventListener("keydown", (event) => {
1104
+ if (event.key !== "Enter" && event.key !== " ") return;
1105
+ const row = event.target?.closest?.("[data-dsh-quota-watch-panel-provider][data-expandable]");
1106
+ if (!row) return;
1107
+ event.preventDefault();
1108
+ openKey = row.dataset.dshQuotaWatchPanelProvider;
1109
+ panelNav = true;
1110
+ syncPop();
1111
+ });
1227
1112
  surface.addEventListener("click", onSurfaceClick);
1228
1113
  surface.addEventListener("keydown", onSurfaceKeydown);
1229
1114
  surface.addEventListener("contextmenu", onSurfaceContext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-quota-watch",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "DSH Web sidebar monitor for GLM Coding Plan quotas and GitHub Copilot credits",
5
5
  "type": "module",
6
6
  "main": "./src/host.mjs",
@@ -2,7 +2,6 @@
2
2
  // Pure logic: no DOM. `storage` is a localStorage-like object (getItem/setItem).
3
3
 
4
4
  export const FLOAT_GEOMETRY_KEY = 'dsh-quota-watch:float-geometry'
5
- export const FLOAT_MODE_KEY = 'dsh-quota-watch:float-mode'
6
5
  export const SURFACE_FLAGS_KEY = 'dsh-quota-watch:surface-flags'
7
6
 
8
7
  const MARGIN = 8
@@ -45,24 +44,6 @@ export function saveFloatGeometry(storage, point) {
45
44
  }
46
45
  }
47
46
 
48
- export function loadFloatMode(storage) {
49
- try {
50
- const raw = storage.getItem(FLOAT_MODE_KEY)
51
- return raw === 'capsule' ? 'capsule' : 'ball'
52
- } catch {
53
- return 'ball'
54
- }
55
- }
56
-
57
- export function saveFloatMode(storage, mode) {
58
- if (mode !== 'ball' && mode !== 'capsule') throw new TypeError(`invalid float mode: ${mode}`)
59
- try {
60
- storage.setItem(FLOAT_MODE_KEY, mode)
61
- } catch {
62
- /* storage unavailable; mode stays session-only */
63
- }
64
- }
65
-
66
47
  export function loadSurfaceFlags(storage) {
67
48
  try {
68
49
  const raw = storage.getItem(SURFACE_FLAGS_KEY)
package/src/client.mjs CHANGED
@@ -1,17 +1,14 @@
1
1
  import { CLIENT_POLL_INTERVAL_MS, CLIENT_ROUTES } from './shared.mjs'
2
2
  import { remainingToUsed } from './core/adapters.mjs'
3
- import { clampPoint, loadFloatGeometry, loadFloatMode, loadSurfaceFlags, saveFloatGeometry, saveFloatMode, saveSurfaceFlags } from './client/prefs.mjs'
3
+ import { clampPoint, loadFloatGeometry, loadSurfaceFlags, saveFloatGeometry, saveSurfaceFlags } from './client/prefs.mjs'
4
4
 
5
5
  export const name = 'quota-watch-client'
6
6
  export const inject = []
7
7
 
8
8
  const CARD_SELECTOR = '[data-dsh-quota-watch-card]'
9
9
  const FETCH_TIMEOUT_MS = 15_000
10
- const BALL_SIZE = 38
11
- const BALL_MARGIN = 16
12
- const RING_RADIUS = 15
13
- const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS
14
- const RING_HALF_GAP = 2
10
+ const SURFACE_SIZE = 38
11
+ const SURFACE_MARGIN = 16
15
12
 
16
13
  const COPY = {
17
14
  zh: {
@@ -19,13 +16,11 @@ const COPY = {
19
16
  openDetails: '查看详情',
20
17
  close: '关闭',
21
18
  menu: {
22
- toggleToBall: '切为悬浮球',
23
- toggleToCapsule: '切为胶囊',
24
19
  showCard: '显示侧边栏卡片',
25
20
  hideCard: '隐藏侧边栏卡片',
26
21
  refreshNow: '立即刷新',
27
- hideBall: '隐藏悬浮球',
28
- showBall: '显示悬浮球',
22
+ hideBall: '隐藏悬浮胶囊',
23
+ showBall: '显示悬浮胶囊',
29
24
  },
30
25
  backToOverview: '返回额度概览',
31
26
  providerNames: { glm: 'GLM', copilot: 'Copilot' },
@@ -56,13 +51,11 @@ const COPY = {
56
51
  openDetails: 'View details',
57
52
  close: 'Close',
58
53
  menu: {
59
- toggleToBall: 'Switch to ball',
60
- toggleToCapsule: 'Switch to capsule',
61
54
  showCard: 'Show sidebar card',
62
55
  hideCard: 'Hide sidebar card',
63
56
  refreshNow: 'Refresh now',
64
- hideBall: 'Hide floating ball',
65
- showBall: 'Show floating ball',
57
+ hideBall: 'Hide floating capsule',
58
+ showBall: 'Show floating capsule',
66
59
  },
67
60
  backToOverview: 'Back to quota overview',
68
61
  providerNames: { glm: 'GLM', copilot: 'Copilot' },
@@ -118,47 +111,15 @@ const STYLE_TEXT = `
118
111
  .dqw-ball-toggle[data-ball-hidden="true"] { color: var(--dsw-alias-button-primary-fill, #5b8def); }
119
112
  .dqw-ball-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
120
113
  .dqw-ball-toggle svg { display: block; width: 14px; height: 14px; }
121
- .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
122
- .dqw-overview-row { display: flex; align-items: center; width: 100%; gap: 6px; margin: 0; padding: 5px 4px; border: 0; border-radius: 8px; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
123
- .dqw-overview-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
124
- .dqw-overview-row:focus-visible { outline: 1px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: -1px; }
125
- .dqw-overview-error { display: flex; align-items: center; gap: 6px; padding: 5px 4px; }
126
- .dqw-bignums { display: flex; gap: 6px; }
127
- .dqw-big { flex: 1; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); border-radius: 8px; padding: 5px 8px; }
128
- .dqw-big .dqw-big-value { font-size: 15px; font-weight: 650; line-height: 19px; font-variant-numeric: tabular-nums; }
129
- .dqw-big .dqw-big-label { font-size: 9.5px; line-height: 13px; opacity: .65; }
130
- .dqw-muted { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
131
- .dqw-models { border-top: 1px dashed var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; display: flex; flex-direction: column; gap: 3px; }
132
- .dqw-model-row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 15px; font-variant-numeric: tabular-nums; }
133
- .dqw-model-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: .85; }
134
- .dqw-model-value { flex: none; font-weight: 600; }
135
- .dqw-kv { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
136
- .dqw-kv-label { flex: none; opacity: .65; }
137
- .dqw-kv-value { min-width: 0; text-align: right; font-weight: 600; }
138
- .dqw-kv-sub { font-weight: 400; opacity: .55; }
139
- .dqw-wins { display: grid; grid-template-columns: max-content max-content max-content max-content 1fr; column-gap: 6px; row-gap: 3px; align-items: baseline; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
140
- .dqw-win-name { opacity: .65; }
141
- .dqw-win-mid { opacity: .65; white-space: nowrap; }
142
- .dqw-win-used { opacity: .65; white-space: nowrap; }
143
- .dqw-win-val { min-width: 0; font-weight: 600; }
144
- .dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
145
114
  `
146
115
 
147
- // Float shell (ball today; capsule/menu later) and the panel live on their own
148
- // shadow host at the document root — ADR 0001 isolation, and survival without
149
- // any sidebar DOM (collapsed sidebar, desktop profile).
116
+ // Float shell (capsule) and the panel live on their own shadow host at the
117
+ // document root — ADR 0001 isolation, and survival without any sidebar DOM
118
+ // (collapsed sidebar, desktop profile). Panel content shares the card's visual
119
+ // language, so the row/detail rules are duplicated into THIS shadow root; the
120
+ // card shadow keeps its own copies for the sidebar rows.
150
121
  const FLOAT_STYLE_TEXT = `
151
122
  :host { position: fixed; z-index: 2147483000; }
152
- .dqw-ball { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; margin: 0; padding: 0; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 50%; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(10px) saturate(1.2); backdrop-filter: blur(10px) saturate(1.2); box-shadow: 0 4px 14px rgba(0,0,0,.22); color: var(--dsw-alias-label-primary, inherit); font: inherit; cursor: pointer; }
153
- .dqw-ball:hover { border-color: var(--dsw-alias-border-primary, var(--dsw-alias-border-secondary, rgba(128,128,128,.35))); }
154
- .dqw-ball:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: 2px; }
155
- .dqw-ring { position: absolute; inset: 0; width: 100%; height: 100%; }
156
- .dqw-ring-track { fill: none; stroke: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); stroke-width: 3.5; }
157
- .dqw-ring-glm { fill: none; stroke: var(--dsw-alias-button-primary-fill, #5b8def); stroke-width: 3.5; stroke-linecap: round; transition: stroke-dasharray .3s ease; }
158
- .dqw-ring-copilot { fill: none; stroke: var(--dsw-alias-label-success, #3fb950); stroke-width: 3.5; stroke-linecap: round; transition: stroke-dasharray .3s ease; }
159
- .dqw-ring-glm.warn, .dqw-ring-copilot.warn { stroke: var(--dsw-alias-label-warning, #d29922); }
160
- .dqw-ring-glm.danger, .dqw-ring-copilot.danger { stroke: var(--dsw-alias-label-danger, #c93c3c); }
161
- .dqw-ball > svg:not(.dqw-ring) { position: relative; width: 16px; height: 16px; }
162
123
  .dqw-capsule { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 999px; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(10px) saturate(1.2); backdrop-filter: blur(10px) saturate(1.2); box-shadow: 0 4px 14px rgba(0,0,0,.22); color: var(--dsw-alias-label-primary, inherit); font: inherit; font-size: 11px; line-height: 24px; cursor: pointer; user-select: none; white-space: nowrap; }
163
124
  .dqw-capsule:hover { border-color: var(--dsw-alias-border-primary, var(--dsw-alias-border-secondary, rgba(128,128,128,.35))); }
164
125
  .dqw-capsule:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: 2px; }
@@ -166,14 +127,14 @@ const FLOAT_STYLE_TEXT = `
166
127
  .dqw-capsule-glm, .dqw-capsule-copilot { font-variant-numeric: tabular-nums; font-weight: 600; }
167
128
  .dqw-capsule-glm.warn, .dqw-capsule-copilot.warn { color: var(--dsw-alias-label-warning, #d29922); }
168
129
  .dqw-capsule-glm.danger, .dqw-capsule-copilot.danger { color: var(--dsw-alias-label-danger, #c93c3c); }
130
+ @media (prefers-reduced-motion: no-preference) {
131
+ .dqw-capsule[data-alert] { animation: dqw-pulse 1.6s ease-in-out infinite; }
132
+ }
133
+ @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
169
134
  .dqw-menu { position: fixed; min-width: 150px; padding: 4px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 88%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); font: inherit; display: flex; flex-direction: column; }
170
135
  .dqw-menu[hidden] { display: none; }
171
136
  .dqw-menu-item { display: flex; align-items: center; gap: 6px; margin: 0; padding: 6px 10px; border: 0; border-radius: 6px; background: transparent; color: inherit; font: inherit; font-size: 11px; line-height: 16px; text-align: left; cursor: pointer; }
172
137
  .dqw-menu-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
173
- @media (prefers-reduced-motion: no-preference) {
174
- .dqw-ball[data-alert] { animation: dqw-ball-pulse 1.6s ease-in-out infinite; }
175
- }
176
- @keyframes dqw-ball-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }
177
138
  .dqw-panel { position: fixed; width: min(320px, calc(100vw - 24px)); max-height: calc(100vh - 24px); overflow-y: auto; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); display: flex; flex-direction: column; gap: 8px; font: inherit; }
178
139
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .dqw-panel { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); } }
179
140
  .dqw-panel[hidden] { display: none; }
@@ -185,6 +146,43 @@ const FLOAT_STYLE_TEXT = `
185
146
  .dqw-panel-back:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
186
147
  .dqw-panel-close { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 24px; height: 24px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; font: inherit; font-size: 12px; line-height: 1; }
187
148
  .dqw-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
149
+ .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
150
+ .dqw-row-summary { display: flex; align-items: center; gap: 6px; padding: 3px 6px; margin: 0 -2px; border-radius: 8px; }
151
+ .dqw-row-summary[data-expandable] { cursor: pointer; }
152
+ .dqw-row-summary[data-expandable]:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.12)); }
153
+ .dqw-row-summary:focus-visible { outline: 1px solid var(--dsw-alias-button-primary-fill, #5b8def); outline-offset: -1px; }
154
+ .dqw-row-summary.is-error { cursor: default; }
155
+ .dqw-label { font-size: 11px; font-weight: 600; width: 52px; flex: none; }
156
+ .dqw-label[data-action="refresh"] { cursor: pointer; border-radius: 4px; }
157
+ .dqw-label[data-action="refresh"]:hover { opacity: .75; }
158
+ .dqw-bar { display: block; flex: 1; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
159
+ .dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
160
+ .dqw-bar-fill.warn { background: var(--dsw-alias-label-warning, #d29922); }
161
+ .dqw-bar-fill.danger { background: var(--dsw-alias-label-danger, #c93c3c); }
162
+ .dqw-pct { font-size: 10.5px; line-height: 14px; font-variant-numeric: tabular-nums; width: 30px; text-align: right; flex: none; }
163
+ .dqw-extra { font-size: 10px; line-height: 14px; opacity: .72; font-variant-numeric: tabular-nums; flex: none; min-width: 34px; text-align: right; }
164
+ .dqw-chev { font-size: 9px; line-height: 14px; opacity: .6; flex: none; width: 10px; text-align: center; }
165
+ .dqw-stale-mark { flex: none; font-size: 9px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); cursor: help; }
166
+ .dqw-errtext { font-size: 10px; line-height: 14px; opacity: .7; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
167
+ .dqw-bignums { display: flex; gap: 6px; }
168
+ .dqw-big { flex: 1; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); border-radius: 8px; padding: 5px 8px; }
169
+ .dqw-big .dqw-big-value { font-size: 15px; font-weight: 650; line-height: 19px; font-variant-numeric: tabular-nums; }
170
+ .dqw-big .dqw-big-label { font-size: 9.5px; line-height: 13px; opacity: .65; }
171
+ .dqw-muted { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
172
+ .dqw-models { border-top: 1px dashed var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; display: flex; flex-direction: column; gap: 3px; }
173
+ .dqw-model-row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 15px; font-variant-numeric: tabular-nums; }
174
+ .dqw-model-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: .85; }
175
+ .dqw-model-value { flex: none; font-weight: 600; }
176
+ .dqw-kv { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
177
+ .dqw-kv-label { flex: none; opacity: .65; }
178
+ .dqw-kv-value { min-width: 0; text-align: right; font-weight: 600; }
179
+ .dqw-kv-sub { font-weight: 400; opacity: .55; }
180
+ .dqw-wins { display: grid; grid-template-columns: max-content max-content max-content max-content 1fr; column-gap: 6px; row-gap: 3px; align-items: baseline; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
181
+ .dqw-win-name { opacity: .65; }
182
+ .dqw-win-mid { opacity: .65; white-space: nowrap; }
183
+ .dqw-win-used { opacity: .65; white-space: nowrap; }
184
+ .dqw-win-val { min-width: 0; font-weight: 600; }
185
+ .dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
188
186
  `
189
187
 
190
188
  function localeFor(win) {
@@ -276,37 +274,7 @@ function menuItem(doc, action, label) {
276
274
  return item
277
275
  }
278
276
 
279
- /** Segmented quota ring: GLM owns the top half, Copilot the bottom half. */
280
- function ringSvg(doc) {
281
- const namespace = 'http://www.w3.org/2000/svg'
282
- const svg = doc.createElementNS(namespace, 'svg')
283
- svg.setAttribute('class', 'dqw-ring')
284
- svg.setAttribute('viewBox', '0 0 38 38')
285
- svg.setAttribute('aria-hidden', 'true')
286
- svg.setAttribute('focusable', 'false')
287
- const track = doc.createElementNS(namespace, 'circle')
288
- track.setAttribute('class', 'dqw-ring-track')
289
- const glm = doc.createElementNS(namespace, 'circle')
290
- glm.setAttribute('class', 'dqw-ring-glm')
291
- glm.setAttribute('transform', 'rotate(-90 19 19)')
292
- const copilot = doc.createElementNS(namespace, 'circle')
293
- copilot.setAttribute('class', 'dqw-ring-copilot')
294
- copilot.setAttribute('transform', 'rotate(90 19 19)')
295
- track.setAttribute('cx', '19')
296
- track.setAttribute('cy', '19')
297
- track.setAttribute('r', String(RING_RADIUS))
298
- track.setAttribute('fill', 'none')
299
- for (const circle of [glm, copilot]) {
300
- circle.setAttribute('cx', '19')
301
- circle.setAttribute('cy', '19')
302
- circle.setAttribute('r', String(RING_RADIUS))
303
- circle.setAttribute('fill', 'none')
304
- circle.setAttribute('stroke-dasharray', `0 ${RING_CIRCUMFERENCE}`)
305
- }
306
- svg.append(track, glm, copilot)
307
- return svg
308
- }
309
-
277
+ /** Double-chevron icon for the panel's back affordance. */
310
278
  function backIcon(doc) {
311
279
  const namespace = 'http://www.w3.org/2000/svg'
312
280
  const svg = doc.createElementNS(namespace, 'svg')
@@ -437,16 +405,6 @@ export function mountQuotaCard({
437
405
  floatHost.dataset.dshQuotaWatchFloat = ''
438
406
  const floatRoot = floatHost.attachShadow({ mode: 'open' })
439
407
  floatRoot.append(text(doc, 'style', '', FLOAT_STYLE_TEXT))
440
- const buildBall = () => {
441
- const el = doc.createElement('button')
442
- el.type = 'button'
443
- el.className = 'dqw-ball'
444
- el.dataset.dshQuotaWatchBall = ''
445
- el.setAttribute('aria-haspopup', 'dialog')
446
- el.setAttribute('aria-expanded', 'false')
447
- el.append(ringSvg(doc), quotaIcon(doc))
448
- return el
449
- }
450
408
  const buildCapsule = () => {
451
409
  const el = doc.createElement('div')
452
410
  el.className = 'dqw-capsule'
@@ -462,8 +420,8 @@ export function mountQuotaCard({
462
420
  )
463
421
  return el
464
422
  }
465
- // One surface at a time: ball (default) or capsule (persisted mode).
466
- let surface = loadFloatMode(win.localStorage) === 'capsule' ? buildCapsule() : buildBall()
423
+ // Capsule is the only floating surface (0.1.1: the round ball was retired).
424
+ let surface = buildCapsule()
467
425
  floatRoot.append(surface)
468
426
  /** Session hide only hides the surface; the host stays alive for the panel. */
469
427
  const syncFloatVisibility = () => {
@@ -480,9 +438,9 @@ export function mountQuotaCard({
480
438
  const viewHeight = win.innerHeight ?? 768
481
439
  const saved = loadFloatGeometry(win.localStorage)
482
440
  const point = clampPoint(
483
- saved ?? { x: viewWidth - BALL_MARGIN - BALL_SIZE, y: viewHeight - BALL_MARGIN - BALL_SIZE },
441
+ saved ?? { x: viewWidth - SURFACE_MARGIN - SURFACE_SIZE, y: viewHeight - SURFACE_MARGIN - SURFACE_SIZE },
484
442
  { width: viewWidth, height: viewHeight },
485
- BALL_SIZE,
443
+ SURFACE_SIZE,
486
444
  )
487
445
  floatHost.style.left = `${Math.round(point.x)}px`
488
446
  floatHost.style.top = `${Math.round(point.y)}px`
@@ -534,7 +492,7 @@ export function mountQuotaCard({
534
492
  const point = clampPoint(
535
493
  { x: event.clientX, y: event.clientY },
536
494
  { width: viewWidth, height: viewHeight },
537
- BALL_SIZE,
495
+ SURFACE_SIZE,
538
496
  )
539
497
  floatHost.style.left = `${Math.round(point.x)}px`
540
498
  floatHost.style.top = `${Math.round(point.y)}px`
@@ -627,45 +585,6 @@ export function mountQuotaCard({
627
585
  return [row]
628
586
  }
629
587
 
630
- const renderOverviewProvider = (key, model) => {
631
- if (model === null) return null
632
- const label = text(doc, 'span', 'dqw-label', model.label)
633
- if (model.kind === 'error') {
634
- const row = doc.createElement('div')
635
- row.className = 'dqw-overview-error'
636
- row.append(label, text(doc, 'span', 'dqw-errtext', model.error ? `${copy.failed}: ${model.error}` : copy.failed))
637
- return row
638
- }
639
- const row = doc.createElement('button')
640
- row.type = 'button'
641
- row.className = 'dqw-overview-row'
642
- row.dataset.dshQuotaWatchPanelProvider = key
643
- row.setAttribute('aria-expanded', 'false')
644
- row.setAttribute('aria-label', `${model.label} · ${typeof model.percent === 'number' ? formatPercent(model.percent, locale) : '—'} · ${copy.openDetails}`)
645
- row.append(label)
646
- if (typeof model.percent === 'number' && Number.isFinite(model.percent)) {
647
- const bar = doc.createElement('span')
648
- bar.className = 'dqw-bar'
649
- bar.setAttribute('role', 'progressbar')
650
- bar.setAttribute('aria-valuemin', '0')
651
- bar.setAttribute('aria-valuemax', '100')
652
- bar.setAttribute('aria-valuenow', String(Math.max(0, Math.min(100, model.percent))))
653
- const fill = text(doc, 'span', fillClass(model.percent))
654
- fill.style.width = `${Math.max(0, Math.min(100, model.percent))}%`
655
- bar.append(fill)
656
- row.append(bar)
657
- }
658
- if (model.stale) {
659
- const mark = text(doc, 'span', 'dqw-stale-mark', '⚠')
660
- mark.dataset.dshQuotaWatchStale = ''
661
- mark.title = model.error ?? copy.stale
662
- row.append(mark)
663
- }
664
- row.append(text(doc, 'span', 'dqw-pct', typeof model.percent === 'number' ? formatPercent(model.percent, locale) : '—'))
665
- row.append(text(doc, 'span', 'dqw-chev', '▸'))
666
- return row
667
- }
668
-
669
588
  /** Panel chrome: optional back, title, always a close affordance. */
670
589
  const panelHeader = ({ title, showBack = false }) => {
671
590
  const header = doc.createElement('div')
@@ -697,10 +616,13 @@ export function mountQuotaCard({
697
616
  const providers = snapshot?.providers ?? []
698
617
  const glm = providers.find((provider) => provider?.key === 'glm')
699
618
  const copilot = providers.find((provider) => provider?.key === 'copilot')
619
+ // The overview reuses the card's row-summary renderer verbatim so the
620
+ // panel looks exactly like the 0.0.18 sidebar rows (label · bar · percent
621
+ // · extra · chevron).
700
622
  const rows = [
701
- renderOverviewProvider('glm', glmRowModel(glm)),
702
- renderOverviewProvider('copilot', copilotRowModel(copilot)),
703
- ].filter(Boolean)
623
+ renderProvider('glm', glmRowModel(glm)),
624
+ renderProvider('copilot', copilotRowModel(copilot)),
625
+ ].filter(Boolean).flat()
704
626
  if (rows.length === 0) {
705
627
  panel.append(text(doc, 'p', 'dqw-muted', lastUpdated.textContent || copy.loading))
706
628
  return
@@ -708,6 +630,10 @@ export function mountQuotaCard({
708
630
  const list = doc.createElement('div')
709
631
  list.className = 'dqw-overview-list'
710
632
  list.append(...rows)
633
+ // Tag the reused rows for the panel's own click/keyboard routing.
634
+ for (const row of list.querySelectorAll('[data-dsh-quota-watch-row]')) {
635
+ row.dataset.dshQuotaWatchPanelProvider = row.dataset.dshQuotaWatchRow
636
+ }
711
637
  panel.append(list)
712
638
  }
713
639
 
@@ -869,16 +795,6 @@ export function mountQuotaCard({
869
795
  surface.setAttribute('aria-expanded', 'true')
870
796
  }
871
797
 
872
- const applyArc = (circle, model) => {
873
- const pct = model && typeof model.percent === 'number' && Number.isFinite(model.percent)
874
- ? Math.max(0, Math.min(100, model.percent))
875
- : null
876
- const fill = pct === null ? 0 : (pct / 100) * (RING_CIRCUMFERENCE / 2 - RING_HALF_GAP)
877
- circle.setAttribute('stroke-dasharray', `${fill.toFixed(2)} ${(RING_CIRCUMFERENCE - fill).toFixed(2)}`)
878
- circle.classList.toggle('warn', pct !== null && pct > 80 && pct <= 95)
879
- circle.classList.toggle('danger', pct !== null && pct > 95)
880
- }
881
-
882
798
  const renderFloatFace = () => {
883
799
  const providers = snapshot?.providers ?? []
884
800
  const glm = glmRowModel(providers.find((provider) => provider?.key === 'glm'))
@@ -890,24 +806,19 @@ export function mountQuotaCard({
890
806
  }
891
807
  syncFloatVisibility()
892
808
  if (floatHost.hidden) return
893
- if (surface.dataset.dshQuotaWatchCapsule !== undefined) {
894
- const paint = (span, model) => {
895
- const visible = model !== null && typeof model.percent === 'number' && Number.isFinite(model.percent)
896
- span.textContent = visible ? `${model.label} ${formatPercent(model.percent, locale)}` : ''
897
- span.classList.toggle('warn', visible && model.percent > 80 && model.percent <= 95)
898
- span.classList.toggle('danger', visible && model.percent > 95)
899
- return visible
900
- }
901
- const glmVisible = paint(surface.querySelector('.dqw-capsule-glm'), glm)
902
- const copilotVisible = paint(surface.querySelector('.dqw-capsule-copilot'), copilot)
903
- surface.querySelector('.dqw-capsule-sep').hidden = !glmVisible || !copilotVisible
904
- } else {
905
- applyArc(surface.querySelector('.dqw-ring-glm'), glm)
906
- applyArc(surface.querySelector('.dqw-ring-copilot'), copilot)
907
- const alert = [glm, copilot].some((model) => model !== null && typeof model.percent === 'number' && model.percent > 95)
908
- if (alert) surface.setAttribute('data-alert', 'true')
909
- else surface.removeAttribute('data-alert')
809
+ const paint = (span, model) => {
810
+ const visible = model !== null && typeof model.percent === 'number' && Number.isFinite(model.percent)
811
+ span.textContent = visible ? `${model.label} ${formatPercent(model.percent, locale)}` : ''
812
+ span.classList.toggle('warn', visible && model.percent > 80 && model.percent <= 95)
813
+ span.classList.toggle('danger', visible && model.percent > 95)
814
+ return visible
910
815
  }
816
+ const glmVisible = paint(surface.querySelector('.dqw-capsule-glm'), glm)
817
+ const copilotVisible = paint(surface.querySelector('.dqw-capsule-copilot'), copilot)
818
+ surface.querySelector('.dqw-capsule-sep').hidden = !glmVisible || !copilotVisible
819
+ const alert = [glm, copilot].some((model) => model !== null && typeof model.percent === 'number' && model.percent > 95)
820
+ if (alert) surface.setAttribute('data-alert', 'true')
821
+ else surface.removeAttribute('data-alert')
911
822
  const summary = [glm, copilot]
912
823
  .filter(Boolean)
913
824
  .map((model) => `${model.label} ${formatPercent(model.percent, locale)}`)
@@ -922,7 +833,8 @@ export function mountQuotaCard({
922
833
  suppressNextClick = false
923
834
  return
924
835
  }
925
- if (openKey === 'overview' && !panel.hidden) {
836
+ // Toggle semantics (same as the 0.0.18 rail): any open panel closes first.
837
+ if (openKey !== undefined && !panel.hidden) {
926
838
  openKey = undefined
927
839
  syncPop()
928
840
  return
@@ -934,26 +846,9 @@ export function mountQuotaCard({
934
846
  event.preventDefault()
935
847
  onSurfaceClick()
936
848
  }
937
- /** Swap ball ↔ capsule: persist, close the panel, rebuild and re-wire the surface. */
938
- const setFloatMode = (nextMode) => {
939
- saveFloatMode(win.localStorage, nextMode)
940
- openKey = undefined
941
- menu.hidden = true
942
- syncPop()
943
- const previous = surface
944
- surface = nextMode === 'capsule' ? buildCapsule() : buildBall()
945
- previous.replaceWith(surface)
946
- surface.addEventListener('click', onSurfaceClick)
947
- surface.addEventListener('keydown', onSurfaceKeydown)
948
- surface.addEventListener('contextmenu', onSurfaceContext)
949
- disposeDrag()
950
- disposeDrag = attachDrag(surface)
951
- renderFloatFace()
952
- }
953
849
  const hasCardMount = () => Boolean(footArea(doc))
954
850
  const openMenu = () => {
955
- const isBall = surface.dataset.dshQuotaWatchBall !== undefined
956
- const items = [menuItem(doc, 'toggle-mode', isBall ? copy.menu.toggleToCapsule : copy.menu.toggleToBall)]
851
+ const items = []
957
852
  if (hasCardMount()) {
958
853
  items.push(menuItem(doc, 'toggle-card', surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard))
959
854
  }
@@ -980,9 +875,7 @@ export function mountQuotaCard({
980
875
  const item = event.target?.closest?.('[data-menu]')
981
876
  if (!item) return
982
877
  const action = item.dataset.menu
983
- if (action === 'toggle-mode') {
984
- setFloatMode(surface.dataset.dshQuotaWatchBall !== undefined ? 'capsule' : 'ball')
985
- } else if (action === 'toggle-card') {
878
+ if (action === 'toggle-card') {
986
879
  setCardHidden(!surfaceFlags.cardHidden)
987
880
  } else if (action === 'refresh') {
988
881
  void poll(true)
@@ -1148,6 +1041,15 @@ export function mountQuotaCard({
1148
1041
  body.addEventListener('click', onBodyClick)
1149
1042
  body.addEventListener('keydown', onBodyKeydown)
1150
1043
  panel.addEventListener('click', onPanelClick)
1044
+ panel.addEventListener('keydown', (event) => {
1045
+ if (event.key !== 'Enter' && event.key !== ' ') return
1046
+ const row = event.target?.closest?.('[data-dsh-quota-watch-panel-provider][data-expandable]')
1047
+ if (!row) return
1048
+ event.preventDefault()
1049
+ openKey = row.dataset.dshQuotaWatchPanelProvider
1050
+ panelNav = true
1051
+ syncPop()
1052
+ })
1151
1053
  surface.addEventListener('click', onSurfaceClick)
1152
1054
  surface.addEventListener('keydown', onSurfaceKeydown)
1153
1055
  surface.addEventListener('contextmenu', onSurfaceContext)