@iasiv5/dsh-quota-watch 0.1.1 → 0.1.3

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,14 @@ 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
+ .dqw-rail-trigger { display: none; flex: none; align-items: center; justify-content: center; width: 36px; height: 36px; margin: 0; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--dsw-alias-label-primary, inherit); cursor: pointer; transition: background-color .12s, color .12s; }
238
+ .dqw-rail-trigger svg { display: block; width: 16px; height: 16px; }
239
+ .dqw-rail-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
240
+ .dqw-rail-trigger:active:not(:disabled), .dqw-rail-trigger[aria-expanded="true"] { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); color: var(--dsw-alias-label-primary, inherit); }
241
+ .dqw-rail-trigger:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: 2px; }
242
+ :host([data-dsh-quota-watch-sidebar-collapsed]) .dqw-card { display: none; }
243
+ :host([data-dsh-quota-watch-sidebar-collapsed]) .dqw-rail-trigger { display: inline-flex; }
244
+ @media print { :host { display: none !important; } }
244
245
  `;
245
246
  var FLOAT_STYLE_TEXT = `
246
247
  :host { position: fixed; z-index: 2147483000; }
@@ -251,15 +252,17 @@ var FLOAT_STYLE_TEXT = `
251
252
  .dqw-capsule-glm, .dqw-capsule-copilot { font-variant-numeric: tabular-nums; font-weight: 600; }
252
253
  .dqw-capsule-glm.warn, .dqw-capsule-copilot.warn { color: var(--dsw-alias-label-warning, #d29922); }
253
254
  .dqw-capsule-glm.danger, .dqw-capsule-copilot.danger { color: var(--dsw-alias-label-danger, #c93c3c); }
255
+ .dqw-capsule[hidden] { display: none; }
254
256
  @media (prefers-reduced-motion: no-preference) {
255
257
  .dqw-capsule[data-alert] { animation: dqw-pulse 1.6s ease-in-out infinite; }
256
258
  }
259
+ @media print { :host { display: none !important; } }
257
260
  @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
258
261
  .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
262
  .dqw-menu[hidden] { display: none; }
260
263
  .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
264
  .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; }
265
+ .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
266
  @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
267
  .dqw-panel[hidden] { display: none; }
265
268
  .dqw-panel *, .dqw-panel *::before, .dqw-panel *::after { box-sizing: border-box; }
@@ -271,6 +274,7 @@ var FLOAT_STYLE_TEXT = `
271
274
  .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
275
  .dqw-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
273
276
  .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
277
+ .dqw-bar { background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.38)); }
274
278
  .dqw-row-summary { display: flex; align-items: center; gap: 6px; padding: 3px 6px; margin: 0 -2px; border-radius: 8px; }
275
279
  .dqw-row-summary[data-expandable] { cursor: pointer; }
276
280
  .dqw-row-summary[data-expandable]:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.12)); }
@@ -465,7 +469,7 @@ function mountQuotaCard({
465
469
  } = {}) {
466
470
  if (!doc?.body || typeof fetchImpl !== "function") return () => {
467
471
  };
468
- if (doc.querySelector(CARD_SELECTOR)) return () => {
472
+ if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {
469
473
  };
470
474
  const locale = localeFor(win);
471
475
  const copy = COPY[locale];
@@ -485,15 +489,16 @@ function mountQuotaCard({
485
489
  const lastUpdated = text(doc, "p", "dqw-meta", copy.loading);
486
490
  lastUpdated.dataset.role = "updated";
487
491
  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);
492
+ const railTrigger = doc.createElement("button");
493
+ railTrigger.type = "button";
494
+ railTrigger.className = "dqw-rail-trigger";
495
+ railTrigger.dataset.dshQuotaWatchRail = "";
496
+ railTrigger.title = copy.title;
497
+ railTrigger.setAttribute("aria-label", copy.title);
498
+ railTrigger.setAttribute("aria-haspopup", "dialog");
499
+ railTrigger.setAttribute("aria-expanded", "false");
500
+ railTrigger.append(quotaIcon(doc));
501
+ card.append(body, railTrigger);
497
502
  cardRoot.append(style, card);
498
503
  const panel = doc.createElement("div");
499
504
  panel.className = "dqw-panel";
@@ -523,12 +528,8 @@ function mountQuotaCard({
523
528
  let surface = buildCapsule();
524
529
  floatRoot.append(surface);
525
530
  const syncFloatVisibility = () => {
526
- surface.hidden = ballSessionHidden || dataHidden;
531
+ surface.hidden = dataHidden;
527
532
  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
533
  };
533
534
  doc.documentElement.append(floatHost);
534
535
  const applyFloatGeometry = () => {
@@ -542,7 +543,6 @@ function mountQuotaCard({
542
543
  );
543
544
  floatHost.style.left = `${Math.round(point.x)}px`;
544
545
  floatHost.style.top = `${Math.round(point.y)}px`;
545
- saveFloatGeometry(win.localStorage, point);
546
546
  };
547
547
  applyFloatGeometry();
548
548
  floatRoot.append(panel);
@@ -607,18 +607,8 @@ function mountQuotaCard({
607
607
  let queuedPlace = false;
608
608
  let openKey;
609
609
  let panelNav = false;
610
- let ballSessionHidden = false;
611
610
  let dataHidden = false;
612
611
  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
612
  const setCardHidden = (hidden) => {
623
613
  surfaceFlags = { cardHidden: hidden };
624
614
  saveSurfaceFlags(win.localStorage, surfaceFlags);
@@ -813,9 +803,13 @@ function mountQuotaCard({
813
803
  }
814
804
  }
815
805
  let lastAnchor = null;
806
+ const anchorFromRow = (key) => () => body.querySelector(`[data-dsh-quota-watch-row="${key}"]`);
807
+ const anchorFromEl = (el) => () => el;
816
808
  const placePanel = () => {
817
- if (panel.hidden || !lastAnchor) return;
818
- const rect = lastAnchor.getBoundingClientRect();
809
+ if (panel.hidden) return;
810
+ const anchor = lastAnchor?.();
811
+ if (!anchor || !anchor.isConnected) return;
812
+ const rect = anchor.getBoundingClientRect();
819
813
  const viewWidth = win.innerWidth ?? 1024;
820
814
  const viewHeight = win.innerHeight ?? 768;
821
815
  const width = panel.offsetWidth || 240;
@@ -837,6 +831,7 @@ function mountQuotaCard({
837
831
  lastAnchor = null;
838
832
  panelNav = false;
839
833
  surface.setAttribute("aria-expanded", "false");
834
+ railTrigger.setAttribute("aria-expanded", "false");
840
835
  return;
841
836
  }
842
837
  if (openKey === "overview") {
@@ -847,6 +842,7 @@ function mountQuotaCard({
847
842
  panel.hidden = false;
848
843
  placePanel();
849
844
  surface.setAttribute("aria-expanded", "true");
845
+ railTrigger.setAttribute("aria-expanded", "true");
850
846
  return;
851
847
  }
852
848
  const provider = (snapshot?.providers ?? []).find((item) => item?.key === openKey);
@@ -863,6 +859,7 @@ function mountQuotaCard({
863
859
  panel.hidden = false;
864
860
  placePanel();
865
861
  surface.setAttribute("aria-expanded", "true");
862
+ railTrigger.setAttribute("aria-expanded", "true");
866
863
  };
867
864
  const renderFloatFace = () => {
868
865
  const providers = snapshot?.providers ?? [];
@@ -917,7 +914,6 @@ function mountQuotaCard({
917
914
  items.push(menuItem(doc, "toggle-card", surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard));
918
915
  }
919
916
  items.push(menuItem(doc, "refresh", copy.menu.refreshNow));
920
- items.push(menuItem(doc, "hide-ball", copy.menu.hideBall));
921
917
  menu.replaceChildren(...items);
922
918
  menu.hidden = false;
923
919
  const rect = surface.getBoundingClientRect();
@@ -935,6 +931,14 @@ function mountQuotaCard({
935
931
  event.preventDefault();
936
932
  openMenu();
937
933
  };
934
+ const onRailClick = () => {
935
+ if (openKey !== void 0 && !panel.hidden) {
936
+ openKey = void 0;
937
+ syncPop();
938
+ return;
939
+ }
940
+ openOverview(railTrigger);
941
+ };
938
942
  const onMenuClick = (event) => {
939
943
  const item = event.target?.closest?.("[data-menu]");
940
944
  if (!item) return;
@@ -943,12 +947,28 @@ function mountQuotaCard({
943
947
  setCardHidden(!surfaceFlags.cardHidden);
944
948
  } else if (action === "refresh") {
945
949
  void poll(true);
946
- } else if (action === "hide-ball") {
947
- setBallSessionHidden(true);
948
950
  }
949
951
  menu.hidden = true;
950
952
  };
951
953
  menu.addEventListener("click", onMenuClick);
954
+ let currentFoot = null;
955
+ let footTooNarrow = false;
956
+ let noDataHidden = false;
957
+ const syncFootNarrowness = () => {
958
+ footTooNarrow = currentFoot !== null && currentFoot.clientWidth > 0 && currentFoot.clientWidth < CARD_MIN_WIDTH;
959
+ };
960
+ const syncCardVisibility = () => {
961
+ syncFootNarrowness();
962
+ const cardOn = !surfaceFlags.cardHidden;
963
+ const showCard = cardOn && !sidebarCollapsed && !footTooNarrow && !noDataHidden;
964
+ const showRail = cardOn && sidebarCollapsed && !noDataHidden;
965
+ container.hidden = !showCard && !showRail;
966
+ if (showRail) container.dataset.dshQuotaWatchSidebarCollapsed = "";
967
+ else delete container.dataset.dshQuotaWatchSidebarCollapsed;
968
+ };
969
+ const footResizeObserver = typeof win.ResizeObserver === "function" ? new win.ResizeObserver(() => {
970
+ syncCardVisibility();
971
+ }) : null;
952
972
  const renderAll = () => {
953
973
  const providers = snapshot?.providers ?? [];
954
974
  const glm = providers.find((provider) => provider?.key === "glm");
@@ -960,7 +980,8 @@ function mountQuotaCard({
960
980
  if (glmParts) fragment.append(...glmParts);
961
981
  if (copilotParts) fragment.append(...copilotParts);
962
982
  body.replaceChildren(fragment);
963
- container.hidden = surfaceFlags.cardHidden || glmParts === null && copilotParts === null;
983
+ noDataHidden = glmParts === null && copilotParts === null;
984
+ syncCardVisibility();
964
985
  const updated = formatTime(snapshot?.updatedAt ? new Date(snapshot.updatedAt).toISOString() : "", locale);
965
986
  if (updated) {
966
987
  lastUpdated.hidden = true;
@@ -995,6 +1016,11 @@ function mountQuotaCard({
995
1016
  if (container.parentElement !== foot || foot.firstElementChild !== container) {
996
1017
  foot.insertBefore(container, foot.firstChild);
997
1018
  }
1019
+ if (currentFoot !== foot) {
1020
+ currentFoot = foot;
1021
+ footResizeObserver?.disconnect();
1022
+ footResizeObserver?.observe(foot);
1023
+ }
998
1024
  };
999
1025
  const schedulePlace = () => {
1000
1026
  if (queuedPlace || disposed) return;
@@ -1004,10 +1030,24 @@ function mountQuotaCard({
1004
1030
  if (!disposed) place();
1005
1031
  });
1006
1032
  };
1033
+ let sidebarCollapsed = Boolean(doc.querySelector("[data-sidebar-collapsed]"));
1034
+ const syncSidebarCollapsed = () => {
1035
+ const next = Boolean(doc.querySelector("[data-sidebar-collapsed]"));
1036
+ if (next !== sidebarCollapsed) {
1037
+ sidebarCollapsed = next;
1038
+ renderAll();
1039
+ }
1040
+ };
1007
1041
  const observer = new win.MutationObserver(() => {
1042
+ syncSidebarCollapsed();
1008
1043
  schedulePlace();
1009
1044
  });
1010
- observer.observe(doc.body, { childList: true, subtree: true });
1045
+ observer.observe(doc.body, {
1046
+ childList: true,
1047
+ subtree: true,
1048
+ attributes: true,
1049
+ attributeFilter: ["data-sidebar-collapsed"]
1050
+ });
1011
1051
  place();
1012
1052
  const startPolling = () => {
1013
1053
  if (timer !== void 0 || doc.visibilityState === "hidden" || disposed) return;
@@ -1024,15 +1064,15 @@ function mountQuotaCard({
1024
1064
  if (doc.visibilityState === "hidden") stopPolling();
1025
1065
  else startPolling();
1026
1066
  };
1027
- const toggleRow = (key, anchorEl) => {
1067
+ const toggleRow = (key) => {
1028
1068
  panelNav = false;
1029
- lastAnchor = anchorEl ?? null;
1069
+ lastAnchor = anchorFromRow(key);
1030
1070
  openKey = openKey === key ? void 0 : key;
1031
1071
  renderAll();
1032
1072
  };
1033
1073
  const openOverview = (anchorEl) => {
1034
1074
  panelNav = false;
1035
- lastAnchor = anchorEl ?? null;
1075
+ lastAnchor = anchorFromEl(anchorEl);
1036
1076
  openKey = "overview";
1037
1077
  syncPop();
1038
1078
  };
@@ -1060,7 +1100,7 @@ function mountQuotaCard({
1060
1100
  return;
1061
1101
  }
1062
1102
  const row = event.target?.closest?.("[data-dsh-quota-watch-row][data-expandable]");
1063
- if (row) toggleRow(row.dataset.dshQuotaWatchRow, row);
1103
+ if (row) toggleRow(row.dataset.dshQuotaWatchRow);
1064
1104
  };
1065
1105
  const onBodyKeydown = (event) => {
1066
1106
  if (event.key !== "Enter" && event.key !== " ") return;
@@ -1085,6 +1125,16 @@ function mountQuotaCard({
1085
1125
  };
1086
1126
  const onWinResize = () => {
1087
1127
  placePanel();
1128
+ syncCardVisibility();
1129
+ const viewWidth = win.innerWidth ?? 1024;
1130
+ const viewHeight = win.innerHeight ?? 768;
1131
+ const x = parseFloat(floatHost.style.left);
1132
+ const y = parseFloat(floatHost.style.top);
1133
+ if (Number.isFinite(x) && Number.isFinite(y)) {
1134
+ const point = clampPoint({ x, y }, { width: viewWidth, height: viewHeight }, SURFACE_SIZE);
1135
+ floatHost.style.left = `${Math.round(point.x)}px`;
1136
+ floatHost.style.top = `${Math.round(point.y)}px`;
1137
+ }
1088
1138
  };
1089
1139
  const onDocKeydown = (event) => {
1090
1140
  if (event.key !== "Escape") return;
@@ -1112,6 +1162,7 @@ function mountQuotaCard({
1112
1162
  surface.addEventListener("click", onSurfaceClick);
1113
1163
  surface.addEventListener("keydown", onSurfaceKeydown);
1114
1164
  surface.addEventListener("contextmenu", onSurfaceContext);
1165
+ railTrigger.addEventListener("click", onRailClick);
1115
1166
  let disposeDrag = attachDrag(surface);
1116
1167
  doc.addEventListener("visibilitychange", onVisibilityChange);
1117
1168
  doc.addEventListener("pointerdown", onDocPointerDown, true);
@@ -1134,6 +1185,7 @@ function mountQuotaCard({
1134
1185
  surface.removeEventListener("click", onSurfaceClick);
1135
1186
  surface.removeEventListener("keydown", onSurfaceKeydown);
1136
1187
  surface.removeEventListener("contextmenu", onSurfaceContext);
1188
+ railTrigger.removeEventListener("click", onRailClick);
1137
1189
  menu.removeEventListener("click", onMenuClick);
1138
1190
  disposeDrag();
1139
1191
  container.remove();
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.3",
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,14 @@ 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
+ .dqw-rail-trigger { display: none; flex: none; align-items: center; justify-content: center; width: 36px; height: 36px; margin: 0; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--dsw-alias-label-primary, inherit); cursor: pointer; transition: background-color .12s, color .12s; }
111
+ .dqw-rail-trigger svg { display: block; width: 16px; height: 16px; }
112
+ .dqw-rail-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
113
+ .dqw-rail-trigger:active:not(:disabled), .dqw-rail-trigger[aria-expanded="true"] { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); color: var(--dsw-alias-label-primary, inherit); }
114
+ .dqw-rail-trigger:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: 2px; }
115
+ :host([data-dsh-quota-watch-sidebar-collapsed]) .dqw-card { display: none; }
116
+ :host([data-dsh-quota-watch-sidebar-collapsed]) .dqw-rail-trigger { display: inline-flex; }
117
+ @media print { :host { display: none !important; } }
114
118
  `
115
119
 
116
120
  // Float shell (capsule) and the panel live on their own shadow host at the
@@ -127,15 +131,17 @@ const FLOAT_STYLE_TEXT = `
127
131
  .dqw-capsule-glm, .dqw-capsule-copilot { font-variant-numeric: tabular-nums; font-weight: 600; }
128
132
  .dqw-capsule-glm.warn, .dqw-capsule-copilot.warn { color: var(--dsw-alias-label-warning, #d29922); }
129
133
  .dqw-capsule-glm.danger, .dqw-capsule-copilot.danger { color: var(--dsw-alias-label-danger, #c93c3c); }
134
+ .dqw-capsule[hidden] { display: none; }
130
135
  @media (prefers-reduced-motion: no-preference) {
131
136
  .dqw-capsule[data-alert] { animation: dqw-pulse 1.6s ease-in-out infinite; }
132
137
  }
138
+ @media print { :host { display: none !important; } }
133
139
  @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
134
140
  .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
141
  .dqw-menu[hidden] { display: none; }
136
142
  .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
143
  .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; }
144
+ .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
145
  @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
146
  .dqw-panel[hidden] { display: none; }
141
147
  .dqw-panel *, .dqw-panel *::before, .dqw-panel *::after { box-sizing: border-box; }
@@ -147,6 +153,7 @@ const FLOAT_STYLE_TEXT = `
147
153
  .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
154
  .dqw-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
149
155
  .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
156
+ .dqw-bar { background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.38)); }
150
157
  .dqw-row-summary { display: flex; align-items: center; gap: 6px; padding: 3px 6px; margin: 0 -2px; border-radius: 8px; }
151
158
  .dqw-row-summary[data-expandable] { cursor: pointer; }
152
159
  .dqw-row-summary[data-expandable]:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.12)); }
@@ -363,7 +370,7 @@ export function mountQuotaCard({
363
370
  pollIntervalMs = CLIENT_POLL_INTERVAL_MS,
364
371
  } = {}) {
365
372
  if (!doc?.body || typeof fetchImpl !== 'function') return () => {}
366
- if (doc.querySelector(CARD_SELECTOR)) return () => {}
373
+ if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {}
367
374
 
368
375
  const locale = localeFor(win)
369
376
  const copy = COPY[locale]
@@ -385,13 +392,18 @@ export function mountQuotaCard({
385
392
  const lastUpdated = text(doc, 'p', 'dqw-meta', copy.loading)
386
393
  lastUpdated.dataset.role = 'updated'
387
394
  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)
395
+ // Collapsed web rail (0.0.18 behaviour, card opt-in only): a 36px gauge icon
396
+ // replaces the card body; the shadow CSS swaps them on the host's collapsed flag.
397
+ const railTrigger = doc.createElement('button')
398
+ railTrigger.type = 'button'
399
+ railTrigger.className = 'dqw-rail-trigger'
400
+ railTrigger.dataset.dshQuotaWatchRail = ''
401
+ railTrigger.title = copy.title
402
+ railTrigger.setAttribute('aria-label', copy.title)
403
+ railTrigger.setAttribute('aria-haspopup', 'dialog')
404
+ railTrigger.setAttribute('aria-expanded', 'false')
405
+ railTrigger.append(quotaIcon(doc))
406
+ card.append(body, railTrigger)
395
407
  cardRoot.append(style, card)
396
408
  // Details open in a viewport-level floating panel — the only detail surface.
397
409
  const panel = doc.createElement('div')
@@ -423,14 +435,10 @@ export function mountQuotaCard({
423
435
  // Capsule is the only floating surface (0.1.1: the round ball was retired).
424
436
  let surface = buildCapsule()
425
437
  floatRoot.append(surface)
426
- /** Session hide only hides the surface; the host stays alive for the panel. */
438
+ /** The capsule is always present unless no provider reports anything. */
427
439
  const syncFloatVisibility = () => {
428
- surface.hidden = ballSessionHidden || dataHidden
440
+ surface.hidden = dataHidden
429
441
  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
442
  }
435
443
  doc.documentElement.append(floatHost)
436
444
  const applyFloatGeometry = () => {
@@ -444,8 +452,9 @@ export function mountQuotaCard({
444
452
  )
445
453
  floatHost.style.left = `${Math.round(point.x)}px`
446
454
  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)
455
+ // Deliberately NOT persisted here: a window that starts small (Electron
456
+ // restore, split-screen) must not clobber the user's preferred coordinates.
457
+ // The visual clamp above applies every mount; storage updates on drag only.
449
458
  }
450
459
  applyFloatGeometry()
451
460
  // Panel lives inside the float shadow root from now on (契约总表「归属时序」).
@@ -515,20 +524,8 @@ export function mountQuotaCard({
515
524
  let queuedPlace = false
516
525
  let openKey
517
526
  let panelNav = false
518
- let ballSessionHidden = false
519
527
  let dataHidden = false
520
528
  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
529
  const setCardHidden = (hidden) => {
533
530
  surfaceFlags = { cardHidden: hidden }
534
531
  saveSurfaceFlags(win.localStorage, surfaceFlags)
@@ -741,10 +738,17 @@ export function mountQuotaCard({
741
738
  }
742
739
 
743
740
  /** Anchor the panel beside its opener (a card row today, the float surface later). */
741
+ // Anchor resolver: card rows are re-created on every render pass, so keep a
742
+ // "how to find the anchor" closure instead of a detached element reference
743
+ // (a detached node's getBoundingClientRect is all zeros → panel teleports).
744
744
  let lastAnchor = null
745
+ const anchorFromRow = (key) => () => body.querySelector(`[data-dsh-quota-watch-row="${key}"]`)
746
+ const anchorFromEl = (el) => () => el
745
747
  const placePanel = () => {
746
- if (panel.hidden || !lastAnchor) return
747
- const rect = lastAnchor.getBoundingClientRect()
748
+ if (panel.hidden) return
749
+ const anchor = lastAnchor?.()
750
+ if (!anchor || !anchor.isConnected) return
751
+ const rect = anchor.getBoundingClientRect()
748
752
  const viewWidth = win.innerWidth ?? 1024
749
753
  const viewHeight = win.innerHeight ?? 768
750
754
  const width = panel.offsetWidth || 240
@@ -767,6 +771,7 @@ export function mountQuotaCard({
767
771
  lastAnchor = null
768
772
  panelNav = false
769
773
  surface.setAttribute('aria-expanded', 'false')
774
+ railTrigger.setAttribute('aria-expanded', 'false')
770
775
  return
771
776
  }
772
777
  if (openKey === 'overview') {
@@ -777,6 +782,7 @@ export function mountQuotaCard({
777
782
  panel.hidden = false
778
783
  placePanel()
779
784
  surface.setAttribute('aria-expanded', 'true')
785
+ railTrigger.setAttribute('aria-expanded', 'true')
780
786
  return
781
787
  }
782
788
  const provider = (snapshot?.providers ?? []).find((item) => item?.key === openKey)
@@ -793,6 +799,7 @@ export function mountQuotaCard({
793
799
  panel.hidden = false
794
800
  placePanel()
795
801
  surface.setAttribute('aria-expanded', 'true')
802
+ railTrigger.setAttribute('aria-expanded', 'true')
796
803
  }
797
804
 
798
805
  const renderFloatFace = () => {
@@ -853,7 +860,6 @@ export function mountQuotaCard({
853
860
  items.push(menuItem(doc, 'toggle-card', surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard))
854
861
  }
855
862
  items.push(menuItem(doc, 'refresh', copy.menu.refreshNow))
856
- items.push(menuItem(doc, 'hide-ball', copy.menu.hideBall))
857
863
  menu.replaceChildren(...items)
858
864
  menu.hidden = false
859
865
  const rect = surface.getBoundingClientRect()
@@ -871,6 +877,15 @@ export function mountQuotaCard({
871
877
  event.preventDefault()
872
878
  openMenu()
873
879
  }
880
+ /** Collapsed-rail gauge icon: toggles the panel, exactly like the capsule. */
881
+ const onRailClick = () => {
882
+ if (openKey !== undefined && !panel.hidden) {
883
+ openKey = undefined
884
+ syncPop()
885
+ return
886
+ }
887
+ openOverview(railTrigger)
888
+ }
874
889
  const onMenuClick = (event) => {
875
890
  const item = event.target?.closest?.('[data-menu]')
876
891
  if (!item) return
@@ -879,13 +894,33 @@ export function mountQuotaCard({
879
894
  setCardHidden(!surfaceFlags.cardHidden)
880
895
  } else if (action === 'refresh') {
881
896
  void poll(true)
882
- } else if (action === 'hide-ball') {
883
- setBallSessionHidden(true)
884
897
  }
885
898
  menu.hidden = true
886
899
  }
887
900
  menu.addEventListener('click', onMenuClick)
888
901
 
902
+ // Cross-profile narrow-container detection: a measurably narrow foot would
903
+ // clip the card into unreadable text regardless of HOW the host signals its
904
+ // collapsed state — hide the card and let the capsule carry the entry.
905
+ // clientWidth 0 (no layout yet, jsdom) counts as "unknown", not narrow.
906
+ let currentFoot = null
907
+ let footTooNarrow = false
908
+ let noDataHidden = false
909
+ const syncFootNarrowness = () => {
910
+ footTooNarrow = currentFoot !== null && currentFoot.clientWidth > 0 && currentFoot.clientWidth < CARD_MIN_WIDTH
911
+ }
912
+ const syncCardVisibility = () => {
913
+ syncFootNarrowness()
914
+ const cardOn = !surfaceFlags.cardHidden
915
+ const showCard = cardOn && !sidebarCollapsed && !footTooNarrow && !noDataHidden
916
+ const showRail = cardOn && sidebarCollapsed && !noDataHidden
917
+ container.hidden = !showCard && !showRail
918
+ if (showRail) container.dataset.dshQuotaWatchSidebarCollapsed = ''
919
+ else delete container.dataset.dshQuotaWatchSidebarCollapsed
920
+ }
921
+ const footResizeObserver = typeof win.ResizeObserver === 'function'
922
+ ? new win.ResizeObserver(() => { syncCardVisibility() })
923
+ : null
889
924
  const renderAll = () => {
890
925
  const providers = snapshot?.providers ?? []
891
926
  const glm = providers.find((provider) => provider?.key === 'glm')
@@ -897,7 +932,8 @@ export function mountQuotaCard({
897
932
  if (glmParts) fragment.append(...glmParts)
898
933
  if (copilotParts) fragment.append(...copilotParts)
899
934
  body.replaceChildren(fragment)
900
- container.hidden = surfaceFlags.cardHidden || (glmParts === null && copilotParts === null)
935
+ noDataHidden = glmParts === null && copilotParts === null
936
+ syncCardVisibility()
901
937
  const updated = formatTime(snapshot?.updatedAt ? new Date(snapshot.updatedAt).toISOString() : '', locale)
902
938
  if (updated) {
903
939
  lastUpdated.hidden = true
@@ -939,6 +975,11 @@ export function mountQuotaCard({
939
975
  if (container.parentElement !== foot || foot.firstElementChild !== container) {
940
976
  foot.insertBefore(container, foot.firstChild)
941
977
  }
978
+ if (currentFoot !== foot) {
979
+ currentFoot = foot
980
+ footResizeObserver?.disconnect()
981
+ footResizeObserver?.observe(foot)
982
+ }
942
983
  }
943
984
 
944
985
  const schedulePlace = () => {
@@ -950,10 +991,28 @@ export function mountQuotaCard({
950
991
  })
951
992
  }
952
993
 
994
+ // Web narrow-rail collapse: the host keeps the footArea but squeezes it, which
995
+ // clips the card into garbled text — hide the card and let the capsule carry
996
+ // the entry (0.0.18 shipped dedicated rail CSS; 0.1.0 retired it, and 0.1.2
997
+ // reintroduces only this visibility signal — zero layout coupling).
998
+ let sidebarCollapsed = Boolean(doc.querySelector('[data-sidebar-collapsed]'))
999
+ const syncSidebarCollapsed = () => {
1000
+ const next = Boolean(doc.querySelector('[data-sidebar-collapsed]'))
1001
+ if (next !== sidebarCollapsed) {
1002
+ sidebarCollapsed = next
1003
+ renderAll()
1004
+ }
1005
+ }
953
1006
  const observer = new win.MutationObserver(() => {
1007
+ syncSidebarCollapsed()
954
1008
  schedulePlace()
955
1009
  })
956
- observer.observe(doc.body, { childList: true, subtree: true })
1010
+ observer.observe(doc.body, {
1011
+ childList: true,
1012
+ subtree: true,
1013
+ attributes: true,
1014
+ attributeFilter: ['data-sidebar-collapsed'],
1015
+ })
957
1016
  place()
958
1017
 
959
1018
  const startPolling = () => {
@@ -969,15 +1028,15 @@ export function mountQuotaCard({
969
1028
  if (doc.visibilityState === 'hidden') stopPolling()
970
1029
  else startPolling()
971
1030
  }
972
- const toggleRow = (key, anchorEl) => {
1031
+ const toggleRow = (key) => {
973
1032
  panelNav = false
974
- lastAnchor = anchorEl ?? null
1033
+ lastAnchor = anchorFromRow(key)
975
1034
  openKey = openKey === key ? undefined : key
976
1035
  renderAll()
977
1036
  }
978
1037
  const openOverview = (anchorEl) => {
979
1038
  panelNav = false
980
- lastAnchor = anchorEl ?? null
1039
+ lastAnchor = anchorFromEl(anchorEl)
981
1040
  openKey = 'overview'
982
1041
  syncPop()
983
1042
  }
@@ -1005,7 +1064,7 @@ export function mountQuotaCard({
1005
1064
  return
1006
1065
  }
1007
1066
  const row = event.target?.closest?.('[data-dsh-quota-watch-row][data-expandable]')
1008
- if (row) toggleRow(row.dataset.dshQuotaWatchRow, row)
1067
+ if (row) toggleRow(row.dataset.dshQuotaWatchRow)
1009
1068
  }
1010
1069
  const onBodyKeydown = (event) => {
1011
1070
  if (event.key !== 'Enter' && event.key !== ' ') return
@@ -1026,7 +1085,21 @@ export function mountQuotaCard({
1026
1085
  renderAll()
1027
1086
  }
1028
1087
  const onDocScroll = () => { placePanel() }
1029
- const onWinResize = () => { placePanel() }
1088
+ const onWinResize = () => {
1089
+ placePanel()
1090
+ syncCardVisibility()
1091
+ // Keep the capsule inside a shrunk viewport; storage keeps the user's
1092
+ // preferred coordinates — only the visual position is clamped here.
1093
+ const viewWidth = win.innerWidth ?? 1024
1094
+ const viewHeight = win.innerHeight ?? 768
1095
+ const x = parseFloat(floatHost.style.left)
1096
+ const y = parseFloat(floatHost.style.top)
1097
+ if (Number.isFinite(x) && Number.isFinite(y)) {
1098
+ const point = clampPoint({ x, y }, { width: viewWidth, height: viewHeight }, SURFACE_SIZE)
1099
+ floatHost.style.left = `${Math.round(point.x)}px`
1100
+ floatHost.style.top = `${Math.round(point.y)}px`
1101
+ }
1102
+ }
1030
1103
  const onDocKeydown = (event) => {
1031
1104
  if (event.key !== 'Escape') return
1032
1105
  if (!menu.hidden) {
@@ -1053,6 +1126,7 @@ export function mountQuotaCard({
1053
1126
  surface.addEventListener('click', onSurfaceClick)
1054
1127
  surface.addEventListener('keydown', onSurfaceKeydown)
1055
1128
  surface.addEventListener('contextmenu', onSurfaceContext)
1129
+ railTrigger.addEventListener('click', onRailClick)
1056
1130
  let disposeDrag = attachDrag(surface)
1057
1131
  doc.addEventListener('visibilitychange', onVisibilityChange)
1058
1132
  doc.addEventListener('pointerdown', onDocPointerDown, true)
@@ -1076,6 +1150,7 @@ export function mountQuotaCard({
1076
1150
  surface.removeEventListener('click', onSurfaceClick)
1077
1151
  surface.removeEventListener('keydown', onSurfaceKeydown)
1078
1152
  surface.removeEventListener('contextmenu', onSurfaceContext)
1153
+ railTrigger.removeEventListener('click', onRailClick)
1079
1154
  menu.removeEventListener('click', onMenuClick)
1080
1155
  disposeDrag()
1081
1156
  container.remove()