@iasiv5/dsh-quota-watch 0.1.1 → 0.1.2

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
@@ -8,8 +8,8 @@
8
8
 
9
9
  - **悬浮胶囊**(0.1.1 起为唯一浮动形态,圆球已退役):常驻屏幕右下角(距边 16px 起步),可整体拖拽到任意位置,位置跨重启记忆,越界自动钳回视口。胶囊一行显示「GLM 13% · Copilot 24%」,已用超过 80% 转橙、超过 95% 转红,任一 provider 超 95% 时胶囊轻微脉动(尊重系统「减少动态效果」设置,开启时不脉动);颜色使用 DSH 主题语义变量,自动适配浅色 / 深色主题。悬停提示与 aria-label 实时给出摘要。
10
10
  - **面板**:点击球或胶囊展开浮动详情面板(半透明毛玻璃底,宽 `min(320px, 100vw−24px)`,靠边自动翻侧,滚动 / 缩放跟随),头部带标题与 ✕,Esc 或点击面板外关闭。概览列两个 provider 行,点击进入详情:GLM 为今日 Tokens / 调用次数双大数字 + 五列对齐窗口网格(MCP(月)→ 5 小时 → 每月 → 每周固定排序,周窗口未下发时显示「每周额度 · ♾️(无限)」占位)+ 分模型用量行;Copilot 为可用额度、本期已用(credits 消耗 + 弱化百分比)、重置时间三行。接口未下发有效 `remaining` / `percent_remaining` 时该行不渲染,`credits_used` 绝不用于推算余额。
11
- - **侧边栏卡片(渐进增强)**:Web profile 侧栏底部区块存在时,卡片照常显示(摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测),卡片右上角有一个胶囊切换按钮(悬停显形),可在卡片上隐藏 / 唤回悬浮胶囊;侧栏收起(Web 窄栏)或侧栏整体不存在(Desktop 收起态)时卡片自然缺席、无报错,由悬浮胶囊承载入口。
12
- - **右键菜单**:显示 / 隐藏侧边栏卡片 · 立即刷新 · 隐藏悬浮胶囊。菜单项随上下文增减(无侧栏卡片挂载点时不显示卡片项)。「隐藏悬浮胶囊」为**会话级**——刷新页面即恢复,卡片上的切换按钮可在会话内直接唤回,任何时刻都至少有一条找回路径。
11
+ - **侧边栏卡片(可选,默认隐藏)**:胶囊是默认界面;需要侧栏卡片时在胶囊右键菜单开启「显示侧边栏卡片」(Web profile 侧栏底部区块存在时才可选)。卡片显示时提供摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测。侧栏收起(Web 窄栏)或容器过窄(<160px,跨 profile 通用判据)时卡片自动隐藏,侧栏整体不存在(Desktop 收起态)同样自然缺席、无报错——入口始终由悬浮胶囊承载。
12
+ - **右键菜单**:显示 / 隐藏侧边栏卡片 · 立即刷新。菜单项随上下文增减(无侧栏卡片挂载点时只剩刷新项)。
13
13
  - 凭据缺失的 provider 不渲染摘要行与胶囊分段(两者皆缺时悬浮胶囊与卡片一并隐藏);探测失败时保留最后一次成功数据并在行上加 ⚠ 过期标记(⚠ 只出现在面板与卡片行内,不上球)。
14
14
  - 宿主默认每 60 秒探测(窗口 + 用量 + Copilot);profile patch 可将 `pollIntervalSec` 设置在 30–3600 秒。浏览器页签可见时每 30 秒刷新视图,隐藏时暂停。
15
15
 
package/lib/client.js CHANGED
@@ -117,14 +117,14 @@ function saveFloatGeometry(storage, point) {
117
117
  function loadSurfaceFlags(storage) {
118
118
  try {
119
119
  const raw = storage.getItem(SURFACE_FLAGS_KEY);
120
- if (typeof raw !== "string" || raw === "") return { cardHidden: false };
120
+ if (typeof raw !== "string" || raw === "") return { cardHidden: true };
121
121
  const parsed = JSON.parse(raw);
122
122
  if (typeof parsed !== "object" || parsed === null || typeof parsed.cardHidden !== "boolean") {
123
- return { cardHidden: false };
123
+ return { cardHidden: true };
124
124
  }
125
125
  return { cardHidden: parsed.cardHidden };
126
126
  } catch {
127
- return { cardHidden: false };
127
+ return { cardHidden: true };
128
128
  }
129
129
  }
130
130
  function saveSurfaceFlags(storage, flags) {
@@ -138,9 +138,11 @@ function saveSurfaceFlags(storage, flags) {
138
138
  var name = "quota-watch-client";
139
139
  var inject = [];
140
140
  var CARD_SELECTOR = "[data-dsh-quota-watch-card]";
141
+ var FLOAT_SELECTOR = "[data-dsh-quota-watch-float]";
141
142
  var FETCH_TIMEOUT_MS = 15e3;
142
143
  var SURFACE_SIZE = 38;
143
144
  var SURFACE_MARGIN = 16;
145
+ var CARD_MIN_WIDTH = 160;
144
146
  var COPY = {
145
147
  zh: {
146
148
  title: "Token\u989D\u5EA6",
@@ -149,9 +151,7 @@ var COPY = {
149
151
  menu: {
150
152
  showCard: "\u663E\u793A\u4FA7\u8FB9\u680F\u5361\u7247",
151
153
  hideCard: "\u9690\u85CF\u4FA7\u8FB9\u680F\u5361\u7247",
152
- refreshNow: "\u7ACB\u5373\u5237\u65B0",
153
- hideBall: "\u9690\u85CF\u60AC\u6D6E\u80F6\u56CA",
154
- showBall: "\u663E\u793A\u60AC\u6D6E\u80F6\u56CA"
154
+ refreshNow: "\u7ACB\u5373\u5237\u65B0"
155
155
  },
156
156
  backToOverview: "\u8FD4\u56DE\u989D\u5EA6\u6982\u89C8",
157
157
  providerNames: { glm: "GLM", copilot: "Copilot" },
@@ -184,9 +184,7 @@ var COPY = {
184
184
  menu: {
185
185
  showCard: "Show sidebar card",
186
186
  hideCard: "Hide sidebar card",
187
- refreshNow: "Refresh now",
188
- hideBall: "Hide floating capsule",
189
- showBall: "Show floating capsule"
187
+ refreshNow: "Refresh now"
190
188
  },
191
189
  backToOverview: "Back to quota overview",
192
190
  providerNames: { glm: "GLM", copilot: "Copilot" },
@@ -236,11 +234,7 @@ var STYLE_TEXT = `
236
234
  .dqw-chev { font-size: 9px; line-height: 14px; opacity: .6; flex: none; width: 10px; text-align: center; }
237
235
  .dqw-stale-mark { flex: none; font-size: 9px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); cursor: help; }
238
236
  .dqw-errtext { font-size: 10px; line-height: 14px; opacity: .7; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
239
- .dqw-ball-toggle { position: absolute; top: 2px; right: 2px; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; opacity: .4; }
240
- .dqw-card:hover .dqw-ball-toggle, .dqw-ball-toggle:focus-visible, .dqw-ball-toggle[data-ball-hidden="true"] { opacity: 1; }
241
- .dqw-ball-toggle[data-ball-hidden="true"] { color: var(--dsw-alias-button-primary-fill, #5b8def); }
242
- .dqw-ball-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
243
- .dqw-ball-toggle svg { display: block; width: 14px; height: 14px; }
237
+ @media print { :host { display: none !important; } }
244
238
  `;
245
239
  var FLOAT_STYLE_TEXT = `
246
240
  :host { position: fixed; z-index: 2147483000; }
@@ -251,15 +245,17 @@ var FLOAT_STYLE_TEXT = `
251
245
  .dqw-capsule-glm, .dqw-capsule-copilot { font-variant-numeric: tabular-nums; font-weight: 600; }
252
246
  .dqw-capsule-glm.warn, .dqw-capsule-copilot.warn { color: var(--dsw-alias-label-warning, #d29922); }
253
247
  .dqw-capsule-glm.danger, .dqw-capsule-copilot.danger { color: var(--dsw-alias-label-danger, #c93c3c); }
248
+ .dqw-capsule[hidden] { display: none; }
254
249
  @media (prefers-reduced-motion: no-preference) {
255
250
  .dqw-capsule[data-alert] { animation: dqw-pulse 1.6s ease-in-out infinite; }
256
251
  }
252
+ @media print { :host { display: none !important; } }
257
253
  @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
258
254
  .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; }
259
255
  .dqw-menu[hidden] { display: none; }
260
256
  .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; }
261
257
  .dqw-menu-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
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; }
258
+ .dqw-panel { position: fixed; z-index: 2147483000; min-width: 166px; max-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; }
263
259
  @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)); } }
264
260
  .dqw-panel[hidden] { display: none; }
265
261
  .dqw-panel *, .dqw-panel *::before, .dqw-panel *::after { box-sizing: border-box; }
@@ -270,7 +266,7 @@ var FLOAT_STYLE_TEXT = `
270
266
  .dqw-panel-back:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
271
267
  .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; }
272
268
  .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; }
269
+ .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; max-width: 240px; }
274
270
  .dqw-row-summary { display: flex; align-items: center; gap: 6px; padding: 3px 6px; margin: 0 -2px; border-radius: 8px; }
275
271
  .dqw-row-summary[data-expandable] { cursor: pointer; }
276
272
  .dqw-row-summary[data-expandable]:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.12)); }
@@ -349,36 +345,6 @@ function text(doc, tag, className, value) {
349
345
  node.textContent = String(value ?? "");
350
346
  return node;
351
347
  }
352
- function quotaIcon(doc) {
353
- const namespace = "http://www.w3.org/2000/svg";
354
- const svg = doc.createElementNS(namespace, "svg");
355
- svg.setAttribute("viewBox", "0 0 24 24");
356
- svg.setAttribute("width", "16");
357
- svg.setAttribute("height", "16");
358
- svg.setAttribute("fill", "none");
359
- svg.setAttribute("stroke", "currentColor");
360
- svg.setAttribute("stroke-width", "2");
361
- svg.setAttribute("stroke-linecap", "round");
362
- svg.setAttribute("stroke-linejoin", "round");
363
- svg.setAttribute("aria-hidden", "true");
364
- svg.setAttribute("focusable", "false");
365
- const dial = doc.createElementNS(namespace, "circle");
366
- dial.setAttribute("cx", "12");
367
- dial.setAttribute("cy", "12");
368
- dial.setAttribute("r", "10");
369
- const ticks = doc.createElementNS(namespace, "path");
370
- ticks.setAttribute("d", "M12 2.4v1.6M12 20v1.6M2.4 12h1.6M20 12h1.6M5.2 5.2l1.1 1.1M18.8 18.8l-1.1-1.1M18.8 5.2l-1.1 1.1M5.2 18.8l1.1-1.1");
371
- const needle = doc.createElementNS(namespace, "path");
372
- needle.setAttribute("d", "M12 12l4.5-4");
373
- const hub = doc.createElementNS(namespace, "circle");
374
- hub.setAttribute("cx", "12");
375
- hub.setAttribute("cy", "12");
376
- hub.setAttribute("r", "1.4");
377
- hub.setAttribute("fill", "currentColor");
378
- hub.setAttribute("stroke", "none");
379
- svg.append(dial, ticks, needle, hub);
380
- return svg;
381
- }
382
348
  function menuItem(doc, action, label) {
383
349
  const item = doc.createElement("button");
384
350
  item.type = "button";
@@ -465,7 +431,7 @@ function mountQuotaCard({
465
431
  } = {}) {
466
432
  if (!doc?.body || typeof fetchImpl !== "function") return () => {
467
433
  };
468
- if (doc.querySelector(CARD_SELECTOR)) return () => {
434
+ if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {
469
435
  };
470
436
  const locale = localeFor(win);
471
437
  const copy = COPY[locale];
@@ -485,15 +451,7 @@ function mountQuotaCard({
485
451
  const lastUpdated = text(doc, "p", "dqw-meta", copy.loading);
486
452
  lastUpdated.dataset.role = "updated";
487
453
  body.append(lastUpdated);
488
- const ballToggle = doc.createElement("button");
489
- ballToggle.type = "button";
490
- ballToggle.className = "dqw-ball-toggle";
491
- ballToggle.dataset.action = "toggle-ball";
492
- ballToggle.append(quotaIcon(doc));
493
- ballToggle.addEventListener("click", () => {
494
- setBallSessionHidden(!ballSessionHidden);
495
- });
496
- card.append(body, ballToggle);
454
+ card.append(body);
497
455
  cardRoot.append(style, card);
498
456
  const panel = doc.createElement("div");
499
457
  panel.className = "dqw-panel";
@@ -523,12 +481,8 @@ function mountQuotaCard({
523
481
  let surface = buildCapsule();
524
482
  floatRoot.append(surface);
525
483
  const syncFloatVisibility = () => {
526
- surface.hidden = ballSessionHidden || dataHidden;
484
+ surface.hidden = dataHidden;
527
485
  floatHost.hidden = dataHidden;
528
- ballToggle.setAttribute("data-ball-hidden", String(ballSessionHidden));
529
- const toggleLabel = ballSessionHidden ? copy.menu.showBall : copy.menu.hideBall;
530
- ballToggle.setAttribute("aria-label", toggleLabel);
531
- ballToggle.title = toggleLabel;
532
486
  };
533
487
  doc.documentElement.append(floatHost);
534
488
  const applyFloatGeometry = () => {
@@ -542,7 +496,6 @@ function mountQuotaCard({
542
496
  );
543
497
  floatHost.style.left = `${Math.round(point.x)}px`;
544
498
  floatHost.style.top = `${Math.round(point.y)}px`;
545
- saveFloatGeometry(win.localStorage, point);
546
499
  };
547
500
  applyFloatGeometry();
548
501
  floatRoot.append(panel);
@@ -607,18 +560,8 @@ function mountQuotaCard({
607
560
  let queuedPlace = false;
608
561
  let openKey;
609
562
  let panelNav = false;
610
- let ballSessionHidden = false;
611
563
  let dataHidden = false;
612
564
  let surfaceFlags = loadSurfaceFlags(win.localStorage);
613
- const setBallSessionHidden = (hidden) => {
614
- const wasHidden = ballSessionHidden;
615
- ballSessionHidden = hidden;
616
- if (hidden && !wasHidden && openKey !== void 0) {
617
- openKey = void 0;
618
- syncPop();
619
- }
620
- syncFloatVisibility();
621
- };
622
565
  const setCardHidden = (hidden) => {
623
566
  surfaceFlags = { cardHidden: hidden };
624
567
  saveSurfaceFlags(win.localStorage, surfaceFlags);
@@ -813,9 +756,13 @@ function mountQuotaCard({
813
756
  }
814
757
  }
815
758
  let lastAnchor = null;
759
+ const anchorFromRow = (key) => () => body.querySelector(`[data-dsh-quota-watch-row="${key}"]`);
760
+ const anchorFromEl = (el) => () => el;
816
761
  const placePanel = () => {
817
- if (panel.hidden || !lastAnchor) return;
818
- const rect = lastAnchor.getBoundingClientRect();
762
+ if (panel.hidden) return;
763
+ const anchor = lastAnchor?.();
764
+ if (!anchor || !anchor.isConnected) return;
765
+ const rect = anchor.getBoundingClientRect();
819
766
  const viewWidth = win.innerWidth ?? 1024;
820
767
  const viewHeight = win.innerHeight ?? 768;
821
768
  const width = panel.offsetWidth || 240;
@@ -917,7 +864,6 @@ function mountQuotaCard({
917
864
  items.push(menuItem(doc, "toggle-card", surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard));
918
865
  }
919
866
  items.push(menuItem(doc, "refresh", copy.menu.refreshNow));
920
- items.push(menuItem(doc, "hide-ball", copy.menu.hideBall));
921
867
  menu.replaceChildren(...items);
922
868
  menu.hidden = false;
923
869
  const rect = surface.getBoundingClientRect();
@@ -943,12 +889,23 @@ function mountQuotaCard({
943
889
  setCardHidden(!surfaceFlags.cardHidden);
944
890
  } else if (action === "refresh") {
945
891
  void poll(true);
946
- } else if (action === "hide-ball") {
947
- setBallSessionHidden(true);
948
892
  }
949
893
  menu.hidden = true;
950
894
  };
951
895
  menu.addEventListener("click", onMenuClick);
896
+ let currentFoot = null;
897
+ let footTooNarrow = false;
898
+ let noDataHidden = false;
899
+ const syncFootNarrowness = () => {
900
+ footTooNarrow = currentFoot !== null && currentFoot.clientWidth > 0 && currentFoot.clientWidth < CARD_MIN_WIDTH;
901
+ };
902
+ const syncCardVisibility = () => {
903
+ syncFootNarrowness();
904
+ container.hidden = surfaceFlags.cardHidden || sidebarCollapsed || footTooNarrow || noDataHidden;
905
+ };
906
+ const footResizeObserver = typeof win.ResizeObserver === "function" ? new win.ResizeObserver(() => {
907
+ syncCardVisibility();
908
+ }) : null;
952
909
  const renderAll = () => {
953
910
  const providers = snapshot?.providers ?? [];
954
911
  const glm = providers.find((provider) => provider?.key === "glm");
@@ -960,7 +917,8 @@ function mountQuotaCard({
960
917
  if (glmParts) fragment.append(...glmParts);
961
918
  if (copilotParts) fragment.append(...copilotParts);
962
919
  body.replaceChildren(fragment);
963
- container.hidden = surfaceFlags.cardHidden || glmParts === null && copilotParts === null;
920
+ noDataHidden = glmParts === null && copilotParts === null;
921
+ syncCardVisibility();
964
922
  const updated = formatTime(snapshot?.updatedAt ? new Date(snapshot.updatedAt).toISOString() : "", locale);
965
923
  if (updated) {
966
924
  lastUpdated.hidden = true;
@@ -995,6 +953,11 @@ function mountQuotaCard({
995
953
  if (container.parentElement !== foot || foot.firstElementChild !== container) {
996
954
  foot.insertBefore(container, foot.firstChild);
997
955
  }
956
+ if (currentFoot !== foot) {
957
+ currentFoot = foot;
958
+ footResizeObserver?.disconnect();
959
+ footResizeObserver?.observe(foot);
960
+ }
998
961
  };
999
962
  const schedulePlace = () => {
1000
963
  if (queuedPlace || disposed) return;
@@ -1004,10 +967,24 @@ function mountQuotaCard({
1004
967
  if (!disposed) place();
1005
968
  });
1006
969
  };
970
+ let sidebarCollapsed = Boolean(doc.querySelector("[data-sidebar-collapsed]"));
971
+ const syncSidebarCollapsed = () => {
972
+ const next = Boolean(doc.querySelector("[data-sidebar-collapsed]"));
973
+ if (next !== sidebarCollapsed) {
974
+ sidebarCollapsed = next;
975
+ renderAll();
976
+ }
977
+ };
1007
978
  const observer = new win.MutationObserver(() => {
979
+ syncSidebarCollapsed();
1008
980
  schedulePlace();
1009
981
  });
1010
- observer.observe(doc.body, { childList: true, subtree: true });
982
+ observer.observe(doc.body, {
983
+ childList: true,
984
+ subtree: true,
985
+ attributes: true,
986
+ attributeFilter: ["data-sidebar-collapsed"]
987
+ });
1011
988
  place();
1012
989
  const startPolling = () => {
1013
990
  if (timer !== void 0 || doc.visibilityState === "hidden" || disposed) return;
@@ -1024,15 +1001,15 @@ function mountQuotaCard({
1024
1001
  if (doc.visibilityState === "hidden") stopPolling();
1025
1002
  else startPolling();
1026
1003
  };
1027
- const toggleRow = (key, anchorEl) => {
1004
+ const toggleRow = (key) => {
1028
1005
  panelNav = false;
1029
- lastAnchor = anchorEl ?? null;
1006
+ lastAnchor = anchorFromRow(key);
1030
1007
  openKey = openKey === key ? void 0 : key;
1031
1008
  renderAll();
1032
1009
  };
1033
1010
  const openOverview = (anchorEl) => {
1034
1011
  panelNav = false;
1035
- lastAnchor = anchorEl ?? null;
1012
+ lastAnchor = anchorFromEl(anchorEl);
1036
1013
  openKey = "overview";
1037
1014
  syncPop();
1038
1015
  };
@@ -1060,7 +1037,7 @@ function mountQuotaCard({
1060
1037
  return;
1061
1038
  }
1062
1039
  const row = event.target?.closest?.("[data-dsh-quota-watch-row][data-expandable]");
1063
- if (row) toggleRow(row.dataset.dshQuotaWatchRow, row);
1040
+ if (row) toggleRow(row.dataset.dshQuotaWatchRow);
1064
1041
  };
1065
1042
  const onBodyKeydown = (event) => {
1066
1043
  if (event.key !== "Enter" && event.key !== " ") return;
@@ -1085,6 +1062,16 @@ function mountQuotaCard({
1085
1062
  };
1086
1063
  const onWinResize = () => {
1087
1064
  placePanel();
1065
+ syncCardVisibility();
1066
+ const viewWidth = win.innerWidth ?? 1024;
1067
+ const viewHeight = win.innerHeight ?? 768;
1068
+ const x = parseFloat(floatHost.style.left);
1069
+ const y = parseFloat(floatHost.style.top);
1070
+ if (Number.isFinite(x) && Number.isFinite(y)) {
1071
+ const point = clampPoint({ x, y }, { width: viewWidth, height: viewHeight }, SURFACE_SIZE);
1072
+ floatHost.style.left = `${Math.round(point.x)}px`;
1073
+ floatHost.style.top = `${Math.round(point.y)}px`;
1074
+ }
1088
1075
  };
1089
1076
  const onDocKeydown = (event) => {
1090
1077
  if (event.key !== "Escape") return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-quota-watch",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",
@@ -47,14 +47,15 @@ export function saveFloatGeometry(storage, point) {
47
47
  export function loadSurfaceFlags(storage) {
48
48
  try {
49
49
  const raw = storage.getItem(SURFACE_FLAGS_KEY)
50
- if (typeof raw !== 'string' || raw === '') return { cardHidden: false }
50
+ // The sidebar card is opt-in: the floating capsule is the default surface.
51
+ if (typeof raw !== 'string' || raw === '') return { cardHidden: true }
51
52
  const parsed = JSON.parse(raw)
52
53
  if (typeof parsed !== 'object' || parsed === null || typeof parsed.cardHidden !== 'boolean') {
53
- return { cardHidden: false }
54
+ return { cardHidden: true }
54
55
  }
55
56
  return { cardHidden: parsed.cardHidden }
56
57
  } catch {
57
- return { cardHidden: false }
58
+ return { cardHidden: true }
58
59
  }
59
60
  }
60
61
 
package/src/client.mjs CHANGED
@@ -6,9 +6,14 @@ export const name = 'quota-watch-client'
6
6
  export const inject = []
7
7
 
8
8
  const CARD_SELECTOR = '[data-dsh-quota-watch-card]'
9
+ const FLOAT_SELECTOR = '[data-dsh-quota-watch-float]'
9
10
  const FETCH_TIMEOUT_MS = 15_000
10
11
  const SURFACE_SIZE = 38
11
12
  const SURFACE_MARGIN = 16
13
+ // Below this a mounted card would clip into unreadable text (collapsed web
14
+ // rails, squeezed desktop layouts) — hide it and let the capsule carry the
15
+ // entry. clientWidth 0 (no layout yet, jsdom) counts as "unknown", not narrow.
16
+ const CARD_MIN_WIDTH = 160
12
17
 
13
18
  const COPY = {
14
19
  zh: {
@@ -19,8 +24,6 @@ const COPY = {
19
24
  showCard: '显示侧边栏卡片',
20
25
  hideCard: '隐藏侧边栏卡片',
21
26
  refreshNow: '立即刷新',
22
- hideBall: '隐藏悬浮胶囊',
23
- showBall: '显示悬浮胶囊',
24
27
  },
25
28
  backToOverview: '返回额度概览',
26
29
  providerNames: { glm: 'GLM', copilot: 'Copilot' },
@@ -54,8 +57,6 @@ const COPY = {
54
57
  showCard: 'Show sidebar card',
55
58
  hideCard: 'Hide sidebar card',
56
59
  refreshNow: 'Refresh now',
57
- hideBall: 'Hide floating capsule',
58
- showBall: 'Show floating capsule',
59
60
  },
60
61
  backToOverview: 'Back to quota overview',
61
62
  providerNames: { glm: 'GLM', copilot: 'Copilot' },
@@ -106,11 +107,7 @@ const STYLE_TEXT = `
106
107
  .dqw-chev { font-size: 9px; line-height: 14px; opacity: .6; flex: none; width: 10px; text-align: center; }
107
108
  .dqw-stale-mark { flex: none; font-size: 9px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); cursor: help; }
108
109
  .dqw-errtext { font-size: 10px; line-height: 14px; opacity: .7; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
109
- .dqw-ball-toggle { position: absolute; top: 2px; right: 2px; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; opacity: .4; }
110
- .dqw-card:hover .dqw-ball-toggle, .dqw-ball-toggle:focus-visible, .dqw-ball-toggle[data-ball-hidden="true"] { opacity: 1; }
111
- .dqw-ball-toggle[data-ball-hidden="true"] { color: var(--dsw-alias-button-primary-fill, #5b8def); }
112
- .dqw-ball-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
113
- .dqw-ball-toggle svg { display: block; width: 14px; height: 14px; }
110
+ @media print { :host { display: none !important; } }
114
111
  `
115
112
 
116
113
  // Float shell (capsule) and the panel live on their own shadow host at the
@@ -127,15 +124,17 @@ const FLOAT_STYLE_TEXT = `
127
124
  .dqw-capsule-glm, .dqw-capsule-copilot { font-variant-numeric: tabular-nums; font-weight: 600; }
128
125
  .dqw-capsule-glm.warn, .dqw-capsule-copilot.warn { color: var(--dsw-alias-label-warning, #d29922); }
129
126
  .dqw-capsule-glm.danger, .dqw-capsule-copilot.danger { color: var(--dsw-alias-label-danger, #c93c3c); }
127
+ .dqw-capsule[hidden] { display: none; }
130
128
  @media (prefers-reduced-motion: no-preference) {
131
129
  .dqw-capsule[data-alert] { animation: dqw-pulse 1.6s ease-in-out infinite; }
132
130
  }
131
+ @media print { :host { display: none !important; } }
133
132
  @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
134
133
  .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; }
135
134
  .dqw-menu[hidden] { display: none; }
136
135
  .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; }
137
136
  .dqw-menu-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
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; }
137
+ .dqw-panel { position: fixed; z-index: 2147483000; min-width: 166px; max-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; }
139
138
  @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)); } }
140
139
  .dqw-panel[hidden] { display: none; }
141
140
  .dqw-panel *, .dqw-panel *::before, .dqw-panel *::after { box-sizing: border-box; }
@@ -146,7 +145,7 @@ const FLOAT_STYLE_TEXT = `
146
145
  .dqw-panel-back:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
147
146
  .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; }
148
147
  .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; }
148
+ .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; max-width: 240px; }
150
149
  .dqw-row-summary { display: flex; align-items: center; gap: 6px; padding: 3px 6px; margin: 0 -2px; border-radius: 8px; }
151
150
  .dqw-row-summary[data-expandable] { cursor: pointer; }
152
151
  .dqw-row-summary[data-expandable]:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.12)); }
@@ -363,7 +362,7 @@ export function mountQuotaCard({
363
362
  pollIntervalMs = CLIENT_POLL_INTERVAL_MS,
364
363
  } = {}) {
365
364
  if (!doc?.body || typeof fetchImpl !== 'function') return () => {}
366
- if (doc.querySelector(CARD_SELECTOR)) return () => {}
365
+ if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {}
367
366
 
368
367
  const locale = localeFor(win)
369
368
  const copy = COPY[locale]
@@ -385,13 +384,7 @@ export function mountQuotaCard({
385
384
  const lastUpdated = text(doc, 'p', 'dqw-meta', copy.loading)
386
385
  lastUpdated.dataset.role = 'updated'
387
386
  body.append(lastUpdated)
388
- const ballToggle = doc.createElement('button')
389
- ballToggle.type = 'button'
390
- ballToggle.className = 'dqw-ball-toggle'
391
- ballToggle.dataset.action = 'toggle-ball'
392
- ballToggle.append(quotaIcon(doc))
393
- ballToggle.addEventListener('click', () => { setBallSessionHidden(!ballSessionHidden) })
394
- card.append(body, ballToggle)
387
+ card.append(body)
395
388
  cardRoot.append(style, card)
396
389
  // Details open in a viewport-level floating panel — the only detail surface.
397
390
  const panel = doc.createElement('div')
@@ -423,14 +416,10 @@ export function mountQuotaCard({
423
416
  // Capsule is the only floating surface (0.1.1: the round ball was retired).
424
417
  let surface = buildCapsule()
425
418
  floatRoot.append(surface)
426
- /** Session hide only hides the surface; the host stays alive for the panel. */
419
+ /** The capsule is always present unless no provider reports anything. */
427
420
  const syncFloatVisibility = () => {
428
- surface.hidden = ballSessionHidden || dataHidden
421
+ surface.hidden = dataHidden
429
422
  floatHost.hidden = dataHidden
430
- ballToggle.setAttribute('data-ball-hidden', String(ballSessionHidden))
431
- const toggleLabel = ballSessionHidden ? copy.menu.showBall : copy.menu.hideBall
432
- ballToggle.setAttribute('aria-label', toggleLabel)
433
- ballToggle.title = toggleLabel
434
423
  }
435
424
  doc.documentElement.append(floatHost)
436
425
  const applyFloatGeometry = () => {
@@ -444,8 +433,9 @@ export function mountQuotaCard({
444
433
  )
445
434
  floatHost.style.left = `${Math.round(point.x)}px`
446
435
  floatHost.style.top = `${Math.round(point.y)}px`
447
- // Persist the clamped coordinates so a saved off-screen position self-heals.
448
- saveFloatGeometry(win.localStorage, point)
436
+ // Deliberately NOT persisted here: a window that starts small (Electron
437
+ // restore, split-screen) must not clobber the user's preferred coordinates.
438
+ // The visual clamp above applies every mount; storage updates on drag only.
449
439
  }
450
440
  applyFloatGeometry()
451
441
  // Panel lives inside the float shadow root from now on (契约总表「归属时序」).
@@ -515,20 +505,8 @@ export function mountQuotaCard({
515
505
  let queuedPlace = false
516
506
  let openKey
517
507
  let panelNav = false
518
- let ballSessionHidden = false
519
508
  let dataHidden = false
520
509
  let surfaceFlags = loadSurfaceFlags(win.localStorage)
521
- const setBallSessionHidden = (hidden) => {
522
- const wasHidden = ballSessionHidden
523
- ballSessionHidden = hidden
524
- // Closing the panel is a one-time consequence of hiding, not a steady state:
525
- // card rows must keep opening the panel while the ball stays hidden.
526
- if (hidden && !wasHidden && openKey !== undefined) {
527
- openKey = undefined
528
- syncPop()
529
- }
530
- syncFloatVisibility()
531
- }
532
510
  const setCardHidden = (hidden) => {
533
511
  surfaceFlags = { cardHidden: hidden }
534
512
  saveSurfaceFlags(win.localStorage, surfaceFlags)
@@ -741,10 +719,17 @@ export function mountQuotaCard({
741
719
  }
742
720
 
743
721
  /** Anchor the panel beside its opener (a card row today, the float surface later). */
722
+ // Anchor resolver: card rows are re-created on every render pass, so keep a
723
+ // "how to find the anchor" closure instead of a detached element reference
724
+ // (a detached node's getBoundingClientRect is all zeros → panel teleports).
744
725
  let lastAnchor = null
726
+ const anchorFromRow = (key) => () => body.querySelector(`[data-dsh-quota-watch-row="${key}"]`)
727
+ const anchorFromEl = (el) => () => el
745
728
  const placePanel = () => {
746
- if (panel.hidden || !lastAnchor) return
747
- const rect = lastAnchor.getBoundingClientRect()
729
+ if (panel.hidden) return
730
+ const anchor = lastAnchor?.()
731
+ if (!anchor || !anchor.isConnected) return
732
+ const rect = anchor.getBoundingClientRect()
748
733
  const viewWidth = win.innerWidth ?? 1024
749
734
  const viewHeight = win.innerHeight ?? 768
750
735
  const width = panel.offsetWidth || 240
@@ -853,7 +838,6 @@ export function mountQuotaCard({
853
838
  items.push(menuItem(doc, 'toggle-card', surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard))
854
839
  }
855
840
  items.push(menuItem(doc, 'refresh', copy.menu.refreshNow))
856
- items.push(menuItem(doc, 'hide-ball', copy.menu.hideBall))
857
841
  menu.replaceChildren(...items)
858
842
  menu.hidden = false
859
843
  const rect = surface.getBoundingClientRect()
@@ -879,13 +863,28 @@ export function mountQuotaCard({
879
863
  setCardHidden(!surfaceFlags.cardHidden)
880
864
  } else if (action === 'refresh') {
881
865
  void poll(true)
882
- } else if (action === 'hide-ball') {
883
- setBallSessionHidden(true)
884
866
  }
885
867
  menu.hidden = true
886
868
  }
887
869
  menu.addEventListener('click', onMenuClick)
888
870
 
871
+ // Cross-profile narrow-container detection: a measurably narrow foot would
872
+ // clip the card into unreadable text regardless of HOW the host signals its
873
+ // collapsed state — hide the card and let the capsule carry the entry.
874
+ // clientWidth 0 (no layout yet, jsdom) counts as "unknown", not narrow.
875
+ let currentFoot = null
876
+ let footTooNarrow = false
877
+ let noDataHidden = false
878
+ const syncFootNarrowness = () => {
879
+ footTooNarrow = currentFoot !== null && currentFoot.clientWidth > 0 && currentFoot.clientWidth < CARD_MIN_WIDTH
880
+ }
881
+ const syncCardVisibility = () => {
882
+ syncFootNarrowness()
883
+ container.hidden = surfaceFlags.cardHidden || sidebarCollapsed || footTooNarrow || noDataHidden
884
+ }
885
+ const footResizeObserver = typeof win.ResizeObserver === 'function'
886
+ ? new win.ResizeObserver(() => { syncCardVisibility() })
887
+ : null
889
888
  const renderAll = () => {
890
889
  const providers = snapshot?.providers ?? []
891
890
  const glm = providers.find((provider) => provider?.key === 'glm')
@@ -897,7 +896,8 @@ export function mountQuotaCard({
897
896
  if (glmParts) fragment.append(...glmParts)
898
897
  if (copilotParts) fragment.append(...copilotParts)
899
898
  body.replaceChildren(fragment)
900
- container.hidden = surfaceFlags.cardHidden || (glmParts === null && copilotParts === null)
899
+ noDataHidden = glmParts === null && copilotParts === null
900
+ syncCardVisibility()
901
901
  const updated = formatTime(snapshot?.updatedAt ? new Date(snapshot.updatedAt).toISOString() : '', locale)
902
902
  if (updated) {
903
903
  lastUpdated.hidden = true
@@ -939,6 +939,11 @@ export function mountQuotaCard({
939
939
  if (container.parentElement !== foot || foot.firstElementChild !== container) {
940
940
  foot.insertBefore(container, foot.firstChild)
941
941
  }
942
+ if (currentFoot !== foot) {
943
+ currentFoot = foot
944
+ footResizeObserver?.disconnect()
945
+ footResizeObserver?.observe(foot)
946
+ }
942
947
  }
943
948
 
944
949
  const schedulePlace = () => {
@@ -950,10 +955,28 @@ export function mountQuotaCard({
950
955
  })
951
956
  }
952
957
 
958
+ // Web narrow-rail collapse: the host keeps the footArea but squeezes it, which
959
+ // clips the card into garbled text — hide the card and let the capsule carry
960
+ // the entry (0.0.18 shipped dedicated rail CSS; 0.1.0 retired it, and 0.1.2
961
+ // reintroduces only this visibility signal — zero layout coupling).
962
+ let sidebarCollapsed = Boolean(doc.querySelector('[data-sidebar-collapsed]'))
963
+ const syncSidebarCollapsed = () => {
964
+ const next = Boolean(doc.querySelector('[data-sidebar-collapsed]'))
965
+ if (next !== sidebarCollapsed) {
966
+ sidebarCollapsed = next
967
+ renderAll()
968
+ }
969
+ }
953
970
  const observer = new win.MutationObserver(() => {
971
+ syncSidebarCollapsed()
954
972
  schedulePlace()
955
973
  })
956
- observer.observe(doc.body, { childList: true, subtree: true })
974
+ observer.observe(doc.body, {
975
+ childList: true,
976
+ subtree: true,
977
+ attributes: true,
978
+ attributeFilter: ['data-sidebar-collapsed'],
979
+ })
957
980
  place()
958
981
 
959
982
  const startPolling = () => {
@@ -969,15 +992,15 @@ export function mountQuotaCard({
969
992
  if (doc.visibilityState === 'hidden') stopPolling()
970
993
  else startPolling()
971
994
  }
972
- const toggleRow = (key, anchorEl) => {
995
+ const toggleRow = (key) => {
973
996
  panelNav = false
974
- lastAnchor = anchorEl ?? null
997
+ lastAnchor = anchorFromRow(key)
975
998
  openKey = openKey === key ? undefined : key
976
999
  renderAll()
977
1000
  }
978
1001
  const openOverview = (anchorEl) => {
979
1002
  panelNav = false
980
- lastAnchor = anchorEl ?? null
1003
+ lastAnchor = anchorFromEl(anchorEl)
981
1004
  openKey = 'overview'
982
1005
  syncPop()
983
1006
  }
@@ -1005,7 +1028,7 @@ export function mountQuotaCard({
1005
1028
  return
1006
1029
  }
1007
1030
  const row = event.target?.closest?.('[data-dsh-quota-watch-row][data-expandable]')
1008
- if (row) toggleRow(row.dataset.dshQuotaWatchRow, row)
1031
+ if (row) toggleRow(row.dataset.dshQuotaWatchRow)
1009
1032
  }
1010
1033
  const onBodyKeydown = (event) => {
1011
1034
  if (event.key !== 'Enter' && event.key !== ' ') return
@@ -1026,7 +1049,21 @@ export function mountQuotaCard({
1026
1049
  renderAll()
1027
1050
  }
1028
1051
  const onDocScroll = () => { placePanel() }
1029
- const onWinResize = () => { placePanel() }
1052
+ const onWinResize = () => {
1053
+ placePanel()
1054
+ syncCardVisibility()
1055
+ // Keep the capsule inside a shrunk viewport; storage keeps the user's
1056
+ // preferred coordinates — only the visual position is clamped here.
1057
+ const viewWidth = win.innerWidth ?? 1024
1058
+ const viewHeight = win.innerHeight ?? 768
1059
+ const x = parseFloat(floatHost.style.left)
1060
+ const y = parseFloat(floatHost.style.top)
1061
+ if (Number.isFinite(x) && Number.isFinite(y)) {
1062
+ const point = clampPoint({ x, y }, { width: viewWidth, height: viewHeight }, SURFACE_SIZE)
1063
+ floatHost.style.left = `${Math.round(point.x)}px`
1064
+ floatHost.style.top = `${Math.round(point.y)}px`
1065
+ }
1066
+ }
1030
1067
  const onDocKeydown = (event) => {
1031
1068
  if (event.key !== 'Escape') return
1032
1069
  if (!menu.hidden) {