@iasiv5/dsh-quota-watch 0.1.14 → 0.1.16

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
@@ -10,6 +10,7 @@
10
10
  - **面板**:点击胶囊展开浮动详情面板(半透明毛玻璃底,宽 `min(320px, 100vw−24px)`,靠边自动翻侧,滚动 / 缩放跟随;≤480px 窄屏自动改为底部弹层并避开 safe-area),头部带标题与 ✕,Esc 或点击面板外关闭。概览列两个 provider 行,点击进入详情:GLM 为今日 Tokens / 调用次数双大数字 + 五列对齐窗口网格(MCP(月)→ 5 小时 → 每月 → 每周固定排序,周窗口未下发时显示「每周额度 · ♾️(无限)」占位)+ 分模型用量行;Copilot 为可用额度、本期已用(credits 消耗 + 弱化百分比)、重置时间三行。底部一行 **等宽双按钮**(grid 1fr 1fr,宽度与文案长度和语言无关):「显示 / 隐藏侧边栏卡片」(文案随状态翻转)与「立即刷新」(触发宿主立即探测);加载 / 出错空态下按钮同样在场。接口未下发有效 `remaining` / `percent_remaining` 时该行不渲染,`credits_used` 绝不用于推算余额。
11
11
  - **侧边栏卡片(可选,默认隐藏)**:胶囊是默认界面;需要侧栏卡片时在面板底部按钮开启「显示侧边栏卡片」(Web profile 侧栏底部区块存在时才出现该按钮)。卡片显示时提供摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测。侧栏收起(Web 窄栏)或容器过窄(<160px,跨 profile 通用判据)时卡片自动隐藏,侧栏整体不存在(Desktop 收起态)同样自然缺席、无报错——入口始终由悬浮胶囊承载。
12
12
  - 凭据缺失的 provider 不渲染摘要行与胶囊分段(两者皆缺时悬浮胶囊与卡片一并隐藏);探测失败时保留最后一次成功数据并在行上加 ⚠ 过期标记(⚠ 只出现在面板与卡片行内,不在胶囊上显示)。
13
+ - **语言**:界面文案跟随 DSH「设置 → 语言」——宿主 locale 特性把解析结果实时镜像到 `<html lang>`,插件监听该属性,切换即热重渲染(无需刷新页面);宿主未同步时回退浏览器语言。面板 footer 按钮文字与行内弱化数值(如「3.8亿」)同字号(10px/14px)、同 72% 静息透明度,hover 恢复全强度。
13
14
  - 宿主默认每 60 秒探测(窗口 + 用量 + Copilot);profile patch 可将 `pollIntervalSec` 设置在 30–3600 秒。浏览器页签可见时每 30 秒刷新视图,隐藏时暂停。
14
15
 
15
16
  ## 兼容与配置
@@ -46,4 +47,4 @@ dsh plugin --profile web add @iasiv5/dsh-quota-watch
46
47
 
47
48
  ## English summary
48
49
 
49
- 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%. Dragging it is grab-accurate (transform-based, rAF-batched) and free to place anywhere on the screen, with position remembered across restarts and viewport boundaries automatically clamped. Touch devices drag directly (wider slop against accidental drags), a 500ms long-press opens the context menu (iOS has no right-click), the hit area is padded to 44px and placement respects `env(safe-area-inset-*)`. 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, and viewports ≤480px dock it to the bottom as a sheet. The panel footer holds two equal-width buttons — sidebar-card visibility and an immediate provider re-probe — replacing the retired right-click/long-press menu, so the capsule itself carries no hidden gestures. A sidebar card remains an opportunistic extra entry on web profiles, opted in from that footer button, and silently disappears whenever the sidebar does — collapsed web sidebars and the desktop profile are covered by the floating capsule alone. 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%. Dragging it is grab-accurate (transform-based, rAF-batched) and free to place anywhere on the screen, with position remembered across restarts and viewport boundaries automatically clamped. Touch devices drag directly (wider slop against accidental drags), a 500ms long-press opens the context menu (iOS has no right-click), the hit area is padded to 44px and placement respects `env(safe-area-inset-*)`. 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, and viewports ≤480px dock it to the bottom as a sheet. The panel footer holds two equal-width buttons — sidebar-card visibility and an immediate provider re-probe — replacing the retired right-click/long-press menu, so the capsule itself carries no hidden gestures. A sidebar card remains an opportunistic extra entry on web profiles, opted in from that footer button, and silently disappears whenever the sidebar does — collapsed web sidebars and the desktop profile are covered by the floating capsule alone. 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. UI copy follows the DSH language preference (mirrored live onto `<html lang>`, re-rendering on switch without a reload) and falls back to the browser language; footer buttons rest at the same size and opacity as the muted row extras. Colors ride DSH semantic theme tokens with hardcoded fallbacks; all UI lives inside a shadow root (ADR 0001).
package/lib/client.js CHANGED
@@ -205,6 +205,10 @@ var COPY = {
205
205
  weekUnlimited: "\u6BCF\u5468\u989D\u5EA6",
206
206
  unlimited: "\u267E\uFE0F",
207
207
  unlimitedNote: "\uFF08 \u65E0\u9650 \uFF09",
208
+ // Full-width paren pair for zh: both window-grid notes share the same
209
+ // glyph class so their left edges line up.
210
+ noteOpen: "\uFF08 ",
211
+ noteClose: " \uFF09",
208
212
  windows: {
209
213
  "5h": "5 \u5C0F\u65F6\u989D\u5EA6",
210
214
  week: "\u6BCF\u5468\u989D\u5EA6",
@@ -237,7 +241,13 @@ var COPY = {
237
241
  todayCalls: "Calls",
238
242
  weekUnlimited: "Weekly quota",
239
243
  unlimited: "\u267E\uFE0F",
240
- unlimitedNote: " ( unlimited )",
244
+ unlimitedNote: "( unlimited )",
245
+ // ASCII parens for en — and NO leading space: the note must start with
246
+ // the same glyph as the reset notes or the grid's left edges diverge
247
+ // (0.1.15 shipped ' ( unlimited )', which broke alignment against the
248
+ // full-width ( Resets: … ) notes the template used to emit in en too).
249
+ noteOpen: "( ",
250
+ noteClose: " )",
241
251
  windows: {
242
252
  "5h": "5-hour quota",
243
253
  week: "Weekly quota",
@@ -291,8 +301,8 @@ var FLOAT_STYLE_TEXT = `
291
301
  @media print { :host { display: none !important; } }
292
302
  @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
293
303
  .dqw-panel-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
294
- .dqw-panel-action { min-width: 0; margin: 0; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); font: inherit; font-size: 11px; line-height: 16px; text-align: center; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
295
- .dqw-panel-action:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
304
+ .dqw-panel-action { min-width: 0; margin: 0; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); font: inherit; font-size: 10px; line-height: 14px; opacity: .72; text-align: center; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
305
+ .dqw-panel-action:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); opacity: 1; }
296
306
  .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; }
297
307
  @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)); } }
298
308
  .dqw-panel--sheet { left:8px; right:8px; top:auto; bottom:calc(8px + env(safe-area-inset-bottom, 0px)); width:auto; max-width:none; max-height:calc(100dvh - 24px); }
@@ -345,6 +355,8 @@ var FLOAT_STYLE_TEXT = `
345
355
  .dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
346
356
  `;
347
357
  function localeFor(win) {
358
+ const host = String(win?.document?.documentElement?.lang ?? "").trim();
359
+ if (host !== "") return host.toLowerCase().startsWith("zh") ? "zh" : "en";
348
360
  return String(win?.navigator?.language ?? "zh").toLowerCase().startsWith("zh") ? "zh" : "en";
349
361
  }
350
362
  function formatNumber(value, locale) {
@@ -464,8 +476,8 @@ function mountQuotaCard({
464
476
  };
465
477
  if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {
466
478
  };
467
- const locale = localeFor(win);
468
- const copy = COPY[locale];
479
+ let locale = localeFor(win);
480
+ let copy = COPY[locale];
469
481
  const container = doc.createElement("div");
470
482
  container.dataset.dshQuotaWatchCard = "";
471
483
  container.dataset.dshPlugin = "quota-watch";
@@ -841,7 +853,7 @@ function mountQuotaCard({
841
853
  wins.append(text(doc, "span", "dqw-win-mid", "\xB7"));
842
854
  wins.append(text(doc, "span", "dqw-win-used", copy.windowUsed));
843
855
  wins.append(text(doc, "span", "dqw-win-val", percent !== void 0 ? formatPercent(percent, locale) : "\u2014"));
844
- wins.append(text(doc, "span", "dqw-kv-sub", reset ? `\uFF08 ${copy.reset}: ${reset} \uFF09` : ""));
856
+ wins.append(text(doc, "span", "dqw-kv-sub", reset ? `${copy.noteOpen}${copy.reset}: ${reset}${copy.noteClose}` : ""));
845
857
  }
846
858
  detail.append(wins);
847
859
  if (Array.isArray(usage?.models) && usage.models.length > 0) {
@@ -1007,6 +1019,8 @@ function mountQuotaCard({
1007
1019
  syncCardVisibility();
1008
1020
  }) : null;
1009
1021
  const renderAll = () => {
1022
+ locale = localeFor(win);
1023
+ copy = COPY[locale];
1010
1024
  const providers = snapshot?.providers ?? [];
1011
1025
  const glm = providers.find((provider) => provider?.key === "glm");
1012
1026
  const copilot = providers.find((provider) => provider?.key === "copilot");
@@ -1071,6 +1085,10 @@ function mountQuotaCard({
1071
1085
  schedulePlace();
1072
1086
  });
1073
1087
  observer.observe(doc.body, { childList: true, subtree: true });
1088
+ const langObserver = typeof win.MutationObserver === "function" ? new win.MutationObserver(() => {
1089
+ if (!disposed) renderAll();
1090
+ }) : null;
1091
+ langObserver?.observe(doc.documentElement, { attributeFilter: ["lang"] });
1074
1092
  place();
1075
1093
  const startPolling = () => {
1076
1094
  if (timer !== void 0 || doc.visibilityState === "hidden" || disposed) return;
@@ -1197,6 +1215,7 @@ function mountQuotaCard({
1197
1215
  disposed = true;
1198
1216
  stopPolling();
1199
1217
  observer.disconnect();
1218
+ langObserver?.disconnect();
1200
1219
  doc.removeEventListener("visibilitychange", onVisibilityChange);
1201
1220
  doc.removeEventListener("pointerdown", onDocPointerDown, true);
1202
1221
  doc.removeEventListener("scroll", onDocScroll, true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-quota-watch",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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",
package/src/client.mjs CHANGED
@@ -45,6 +45,10 @@ const COPY = {
45
45
  weekUnlimited: '每周额度',
46
46
  unlimited: '♾️',
47
47
  unlimitedNote: '( 无限 )',
48
+ // Full-width paren pair for zh: both window-grid notes share the same
49
+ // glyph class so their left edges line up.
50
+ noteOpen: '( ',
51
+ noteClose: ' )',
48
52
  windows: {
49
53
  '5h': '5 小时额度',
50
54
  week: '每周额度',
@@ -77,7 +81,13 @@ const COPY = {
77
81
  todayCalls: 'Calls',
78
82
  weekUnlimited: 'Weekly quota',
79
83
  unlimited: '♾️',
80
- unlimitedNote: ' ( unlimited )',
84
+ unlimitedNote: '( unlimited )',
85
+ // ASCII parens for en — and NO leading space: the note must start with
86
+ // the same glyph as the reset notes or the grid's left edges diverge
87
+ // (0.1.15 shipped ' ( unlimited )', which broke alignment against the
88
+ // full-width ( Resets: … ) notes the template used to emit in en too).
89
+ noteOpen: '( ',
90
+ noteClose: ' )',
81
91
  windows: {
82
92
  '5h': '5-hour quota',
83
93
  week: 'Weekly quota',
@@ -138,8 +148,8 @@ const FLOAT_STYLE_TEXT = `
138
148
  @media print { :host { display: none !important; } }
139
149
  @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
140
150
  .dqw-panel-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
141
- .dqw-panel-action { min-width: 0; margin: 0; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); font: inherit; font-size: 11px; line-height: 16px; text-align: center; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
142
- .dqw-panel-action:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
151
+ .dqw-panel-action { min-width: 0; margin: 0; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); font: inherit; font-size: 10px; line-height: 14px; opacity: .72; text-align: center; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
152
+ .dqw-panel-action:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); opacity: 1; }
143
153
  .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; }
144
154
  @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)); } }
145
155
  .dqw-panel--sheet { left:8px; right:8px; top:auto; bottom:calc(8px + env(safe-area-inset-bottom, 0px)); width:auto; max-width:none; max-height:calc(100dvh - 24px); }
@@ -192,7 +202,13 @@ const FLOAT_STYLE_TEXT = `
192
202
  .dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
193
203
  `
194
204
 
205
+ /** Locale resolution follows the host, not just the browser: the DSH locale
206
+ * feature mirrors its resolved preference (设置 → 语言) onto
207
+ * `<html lang>` and keeps it updated on every switch, so prefer that signal
208
+ * and fall back to navigator.language when the host has not synced yet. */
195
209
  function localeFor(win) {
210
+ const host = String(win?.document?.documentElement?.lang ?? '').trim()
211
+ if (host !== '') return host.toLowerCase().startsWith('zh') ? 'zh' : 'en'
196
212
  return String(win?.navigator?.language ?? 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en'
197
213
  }
198
214
 
@@ -361,8 +377,11 @@ export function mountQuotaCard({
361
377
  if (!doc?.body || typeof fetchImpl !== 'function') return () => {}
362
378
  if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {}
363
379
 
364
- const locale = localeFor(win)
365
- const copy = COPY[locale]
380
+ // Mutable so a host language switch (设置 → 语言 → <html lang>) re-renders
381
+ // every surface with the new dictionary on the next pass — renderAll is the
382
+ // one funnel all render paths share.
383
+ let locale = localeFor(win)
384
+ let copy = COPY[locale]
366
385
  const container = doc.createElement('div')
367
386
  container.dataset.dshQuotaWatchCard = ''
368
387
  container.dataset.dshPlugin = 'quota-watch'
@@ -796,7 +815,7 @@ export function mountQuotaCard({
796
815
  wins.append(text(doc, 'span', 'dqw-win-mid', '·'))
797
816
  wins.append(text(doc, 'span', 'dqw-win-used', copy.windowUsed))
798
817
  wins.append(text(doc, 'span', 'dqw-win-val', percent !== undefined ? formatPercent(percent, locale) : '—'))
799
- wins.append(text(doc, 'span', 'dqw-kv-sub', reset ? `( ${copy.reset}: ${reset} )` : ''))
818
+ wins.append(text(doc, 'span', 'dqw-kv-sub', reset ? `${copy.noteOpen}${copy.reset}: ${reset}${copy.noteClose}` : ''))
800
819
  }
801
820
  detail.append(wins)
802
821
  if (Array.isArray(usage?.models) && usage.models.length > 0) {
@@ -990,6 +1009,8 @@ export function mountQuotaCard({
990
1009
  ? new win.ResizeObserver(() => { syncCardVisibility() })
991
1010
  : null
992
1011
  const renderAll = () => {
1012
+ locale = localeFor(win)
1013
+ copy = COPY[locale]
993
1014
  const providers = snapshot?.providers ?? []
994
1015
  const glm = providers.find((provider) => provider?.key === 'glm')
995
1016
  const copilot = providers.find((provider) => provider?.key === 'copilot')
@@ -1063,6 +1084,13 @@ export function mountQuotaCard({
1063
1084
  schedulePlace()
1064
1085
  })
1065
1086
  observer.observe(doc.body, { childList: true, subtree: true })
1087
+ // The DSH locale feature mirrors every language switch onto <html lang>;
1088
+ // re-render on that flip so the UI follows 设置 → 语言 without a reload.
1089
+ // A no-op flip (same value) never fires, so this costs nothing at rest.
1090
+ const langObserver = typeof win.MutationObserver === 'function'
1091
+ ? new win.MutationObserver(() => { if (!disposed) renderAll() })
1092
+ : null
1093
+ langObserver?.observe(doc.documentElement, { attributeFilter: ['lang'] })
1066
1094
  place()
1067
1095
 
1068
1096
  const startPolling = () => {
@@ -1121,6 +1149,9 @@ export function mountQuotaCard({
1121
1149
  syncPop()
1122
1150
  }
1123
1151
  }
1152
+ // Both handlers ride the card's inner body element — INSIDE the card shadow
1153
+ // root — so shadow-host retargeting never applies and event.target is the
1154
+ // real row/label in every browser.
1124
1155
  const onBodyClick = (event) => {
1125
1156
  if (event.target?.closest?.('[data-action="refresh"]')) {
1126
1157
  void poll(true)
@@ -1195,6 +1226,7 @@ export function mountQuotaCard({
1195
1226
  disposed = true
1196
1227
  stopPolling()
1197
1228
  observer.disconnect()
1229
+ langObserver?.disconnect()
1198
1230
  doc.removeEventListener('visibilitychange', onVisibilityChange)
1199
1231
  doc.removeEventListener('pointerdown', onDocPointerDown, true)
1200
1232
  doc.removeEventListener('scroll', onDocScroll, true)