@iasiv5/dsh-quota-watch 0.1.13 → 0.1.14

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
@@ -6,10 +6,9 @@
6
6
 
7
7
  ## 功能
8
8
 
9
- - **悬浮胶囊**(0.1.1 起为唯一浮动形态,圆球已退役):常驻屏幕(默认右下角安全区),可自由拖拽到屏幕任意位置停放,位置跨重启记忆,越界自动钳回视口(距边 8px 安全边距)。拖拽跟手(保持抓取点,transform 渲染);触摸设备可直接拖拽(触摸阈值更宽防误拖)、长按 500ms 呼出菜单,点击热区扩展到 44px,自动避让 iPhone 刘海 / 小黑条(safe-area)。胶囊一行显示「GLM 13% · Copilot 24%」,已用超过 80% 转橙、超过 95% 转红,任一 provider 超 95% 时胶囊轻微脉动(尊重系统「减少动态效果」设置,开启时不脉动);颜色使用 DSH 主题语义变量,自动适配浅色 / 深色主题。悬停提示与 aria-label 实时给出摘要。
10
- - **面板**:点击胶囊展开浮动详情面板(半透明毛玻璃底,宽 `min(320px, 100vw−24px)`,靠边自动翻侧,滚动 / 缩放跟随;≤480px 窄屏自动改为底部弹层并避开 safe-area),头部带标题与 ✕,Esc 或点击面板外关闭。概览列两个 provider 行,点击进入详情:GLM 为今日 Tokens / 调用次数双大数字 + 五列对齐窗口网格(MCP(月)→ 5 小时 → 每月 → 每周固定排序,周窗口未下发时显示「每周额度 · ♾️(无限)」占位)+ 分模型用量行;Copilot 为可用额度、本期已用(credits 消耗 + 弱化百分比)、重置时间三行。接口未下发有效 `remaining` / `percent_remaining` 时该行不渲染,`credits_used` 绝不用于推算余额。
11
- - **侧边栏卡片(可选,默认隐藏)**:胶囊是默认界面;需要侧栏卡片时在胶囊右键菜单开启「显示侧边栏卡片」(Web profile 侧栏底部区块存在时才可选)。卡片显示时提供摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测。侧栏收起(Web 窄栏)或容器过窄(<160px,跨 profile 通用判据)时卡片自动隐藏,侧栏整体不存在(Desktop 收起态)同样自然缺席、无报错——入口始终由悬浮胶囊承载。
12
- - **右键菜单**:显示 / 隐藏侧边栏卡片 · 立即刷新。菜单项随上下文增减(无侧栏卡片挂载点时只剩刷新项)。
9
+ - **悬浮胶囊**(0.1.1 起为唯一浮动形态,圆球已退役):常驻屏幕(默认右下角安全区),可自由拖拽到屏幕任意位置停放,位置跨重启记忆,越界自动钳回视口(距边 8px 安全边距)。拖拽跟手(保持抓取点,transform 渲染);触摸设备可直接拖拽(触摸阈值更宽防误拖)、点击热区扩展到 44px,自动避让 iPhone 刘海 / 小黑条(safe-area)。胶囊一行显示「GLM 13% · Copilot 24%」,已用超过 80% 转橙、超过 95% 转红,任一 provider 超 95% 时胶囊轻微脉动(尊重系统「减少动态效果」设置,开启时不脉动);颜色使用 DSH 主题语义变量,自动适配浅色 / 深色主题。悬停提示与 aria-label 实时给出摘要。单击即面板入口——胶囊上没有任何隐藏手势(0.1.14 起右键 / 长按菜单整体退役)。
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
+ - **侧边栏卡片(可选,默认隐藏)**:胶囊是默认界面;需要侧栏卡片时在面板底部按钮开启「显示侧边栏卡片」(Web profile 侧栏底部区块存在时才出现该按钮)。卡片显示时提供摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测。侧栏收起(Web 窄栏)或容器过窄(<160px,跨 profile 通用判据)时卡片自动隐藏,侧栏整体不存在(Desktop 收起态)同样自然缺席、无报错——入口始终由悬浮胶囊承载。
13
12
  - 凭据缺失的 provider 不渲染摘要行与胶囊分段(两者皆缺时悬浮胶囊与卡片一并隐藏);探测失败时保留最后一次成功数据并在行上加 ⚠ 过期标记(⚠ 只出现在面板与卡片行内,不在胶囊上显示)。
14
13
  - 宿主默认每 60 秒探测(窗口 + 用量 + Copilot);profile patch 可将 `pollIntervalSec` 设置在 30–3600 秒。浏览器页签可见时每 30 秒刷新视图,隐藏时暂停。
15
14
 
@@ -47,4 +46,4 @@ dsh plugin --profile web add @iasiv5/dsh-quota-watch
47
46
 
48
47
  ## English summary
49
48
 
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. A sidebar card remains an opportunistic extra entry on web profiles, opt-in from the capsule's menu, and silently disappears whenever the sidebar does — collapsed web sidebars and the desktop profile are covered by the floating capsule alone. The right-click menu (long-press on touch) offers card visibility and an immediate provider re-probe. 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).
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).
package/lib/client.js CHANGED
@@ -152,7 +152,6 @@ function saveSurfaceFlags(storage, flags) {
152
152
  // src/client/drag.mjs
153
153
  var DRAG_SLOP_MOUSE = 6;
154
154
  var DRAG_SLOP_TOUCH = 10;
155
- var LONG_PRESS_MS = 500;
156
155
  function dragSlop(pointerType) {
157
156
  return pointerType === "touch" || pointerType === "pen" ? DRAG_SLOP_TOUCH : DRAG_SLOP_MOUSE;
158
157
  }
@@ -184,7 +183,7 @@ var COPY = {
184
183
  title: "Token\u989D\u5EA6",
185
184
  openDetails: "\u67E5\u770B\u8BE6\u60C5",
186
185
  close: "\u5173\u95ED",
187
- menu: {
186
+ actions: {
188
187
  showCard: "\u663E\u793A\u4FA7\u8FB9\u680F\u5361\u7247",
189
188
  hideCard: "\u9690\u85CF\u4FA7\u8FB9\u680F\u5361\u7247",
190
189
  refreshNow: "\u7ACB\u5373\u5237\u65B0"
@@ -217,7 +216,7 @@ var COPY = {
217
216
  title: "Quota Watch",
218
217
  openDetails: "View details",
219
218
  close: "Close",
220
- menu: {
219
+ actions: {
221
220
  showCard: "Show sidebar card",
222
221
  hideCard: "Hide sidebar card",
223
222
  refreshNow: "Refresh now"
@@ -291,10 +290,9 @@ var FLOAT_STYLE_TEXT = `
291
290
  }
292
291
  @media print { :host { display: none !important; } }
293
292
  @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
294
- .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; }
295
- .dqw-menu[hidden] { display: none; }
296
- .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; }
297
- .dqw-menu-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
293
+ .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); }
298
296
  .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; }
299
297
  @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)); } }
300
298
  .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); }
@@ -387,15 +385,6 @@ function text(doc, tag, className, value) {
387
385
  node.textContent = String(value ?? "");
388
386
  return node;
389
387
  }
390
- function menuItem(doc, action, label) {
391
- const item = doc.createElement("button");
392
- item.type = "button";
393
- item.className = "dqw-menu-item";
394
- item.setAttribute("role", "menuitem");
395
- item.dataset.menu = action;
396
- item.append(text(doc, "span", "", label));
397
- return item;
398
- }
399
388
  function backIcon(doc) {
400
389
  const namespace = "http://www.w3.org/2000/svg";
401
390
  const svg = doc.createElementNS(namespace, "svg");
@@ -563,12 +552,6 @@ function mountQuotaCard({
563
552
  };
564
553
  applyPosition();
565
554
  floatRoot.append(panel);
566
- const menu = doc.createElement("div");
567
- menu.className = "dqw-menu";
568
- menu.dataset.dshQuotaWatchMenu = "";
569
- menu.setAttribute("role", "menu");
570
- menu.hidden = true;
571
- floatRoot.append(menu);
572
555
  let suppressNextClick = false;
573
556
  const scheduleFrame = (fn) => typeof win.requestAnimationFrame === "function" ? win.requestAnimationFrame.call(win, fn) : fn();
574
557
  const attachDrag = (surfaceEl) => {
@@ -582,13 +565,6 @@ function mountQuotaCard({
582
565
  let restX = 0;
583
566
  let restY = 0;
584
567
  let lastPoint = null;
585
- let longPressTimer;
586
- const clearLongPress = () => {
587
- if (longPressTimer !== void 0) {
588
- win.clearTimeout(longPressTimer);
589
- longPressTimer = void 0;
590
- }
591
- };
592
568
  const bindGesture = () => {
593
569
  doc.addEventListener("pointermove", onPointerMove);
594
570
  doc.addEventListener("pointerup", onPointerUp);
@@ -606,19 +582,6 @@ function mountQuotaCard({
606
582
  originX = event.clientX;
607
583
  originY = event.clientY;
608
584
  slop = dragSlop(event.pointerType);
609
- clearLongPress();
610
- if (slop === DRAG_SLOP_TOUCH) {
611
- longPressTimer = win.setTimeout(() => {
612
- longPressTimer = void 0;
613
- if (!dragging || moved) return;
614
- try {
615
- win.navigator.vibrate?.(10);
616
- } catch {
617
- }
618
- openMenu();
619
- suppressNextClick = true;
620
- }, LONG_PRESS_MS);
621
- }
622
585
  const rect = surfaceEl.getBoundingClientRect();
623
586
  grab = grabOffset({ x: event.clientX, y: event.clientY }, rect);
624
587
  dragSize = rect.width > 0 ? { width: rect.width, height: rect.height } : FALLBACK_SIZE;
@@ -632,7 +595,6 @@ function mountQuotaCard({
632
595
  if (!moved) {
633
596
  if (Math.hypot(event.clientX - originX, event.clientY - originY) < slop) return;
634
597
  moved = true;
635
- clearLongPress();
636
598
  doc.body.style.userSelect = "none";
637
599
  surfaceEl.classList.add("dqw-capsule--dragging");
638
600
  }
@@ -660,7 +622,6 @@ function mountQuotaCard({
660
622
  const onPointerUp = (event) => {
661
623
  if (!dragging) return;
662
624
  dragging = false;
663
- clearLongPress();
664
625
  unbindGesture();
665
626
  doc.body.style.userSelect = "";
666
627
  if (!moved) return;
@@ -681,7 +642,6 @@ function mountQuotaCard({
681
642
  const onPointerCancel = () => {
682
643
  if (!dragging) return;
683
644
  dragging = false;
684
- clearLongPress();
685
645
  unbindGesture();
686
646
  doc.body.style.userSelect = "";
687
647
  surfaceEl.classList.remove("dqw-capsule--dragging");
@@ -779,6 +739,28 @@ function mountQuotaCard({
779
739
  header.append(close);
780
740
  return header;
781
741
  };
742
+ const overviewActions = () => {
743
+ const actions = doc.createElement("div");
744
+ actions.className = "dqw-panel-actions";
745
+ if (hasCardMount()) {
746
+ const toggle = doc.createElement("button");
747
+ toggle.type = "button";
748
+ toggle.className = "dqw-panel-action";
749
+ toggle.dataset.action = "toggle-card";
750
+ const toggleLabel = surfaceFlags.cardHidden ? copy.actions.showCard : copy.actions.hideCard;
751
+ toggle.textContent = toggleLabel;
752
+ toggle.setAttribute("aria-label", toggleLabel);
753
+ actions.append(toggle);
754
+ }
755
+ const refresh = doc.createElement("button");
756
+ refresh.type = "button";
757
+ refresh.className = "dqw-panel-action";
758
+ refresh.dataset.action = "panel-refresh";
759
+ refresh.textContent = copy.actions.refreshNow;
760
+ refresh.setAttribute("aria-label", copy.actions.refreshNow);
761
+ actions.append(refresh);
762
+ return actions;
763
+ };
782
764
  const renderOverview = () => {
783
765
  panel.replaceChildren(panelHeader({ title: copy.title }));
784
766
  const providers = snapshot?.providers ?? [];
@@ -790,15 +772,16 @@ function mountQuotaCard({
790
772
  ].filter(Boolean).flat();
791
773
  if (rows.length === 0) {
792
774
  panel.append(text(doc, "p", "dqw-muted", lastUpdated.textContent || copy.loading));
793
- return;
794
- }
795
- const list = doc.createElement("div");
796
- list.className = "dqw-overview-list";
797
- list.append(...rows);
798
- for (const row of list.querySelectorAll("[data-dsh-quota-watch-row]")) {
799
- row.dataset.dshQuotaWatchPanelProvider = row.dataset.dshQuotaWatchRow;
775
+ } else {
776
+ const list = doc.createElement("div");
777
+ list.className = "dqw-overview-list";
778
+ list.append(...rows);
779
+ for (const row of list.querySelectorAll("[data-dsh-quota-watch-row]")) {
780
+ row.dataset.dshQuotaWatchPanelProvider = row.dataset.dshQuotaWatchRow;
781
+ }
782
+ panel.append(list);
800
783
  }
801
- panel.append(list);
784
+ panel.append(overviewActions());
802
785
  };
803
786
  function renderDetail(detail, key, provider, fromPanelNav = false) {
804
787
  detail.replaceChildren();
@@ -1008,42 +991,6 @@ function mountQuotaCard({
1008
991
  onSurfaceClick();
1009
992
  };
1010
993
  const hasCardMount = () => Boolean(footArea(doc));
1011
- const openMenu = () => {
1012
- const items = [];
1013
- if (hasCardMount()) {
1014
- items.push(menuItem(doc, "toggle-card", surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard));
1015
- }
1016
- items.push(menuItem(doc, "refresh", copy.menu.refreshNow));
1017
- menu.replaceChildren(...items);
1018
- menu.hidden = false;
1019
- const rect = surface.getBoundingClientRect();
1020
- const viewWidth = win.innerWidth ?? 1024;
1021
- const viewHeight = win.innerHeight ?? 768;
1022
- const width = menu.offsetWidth || 150;
1023
- const height = menu.offsetHeight || 120;
1024
- let left = rect.right + 8;
1025
- if (left + width > viewWidth - 8) left = Math.max(8, rect.left - width - 8);
1026
- const top = Math.min(Math.max(rect.top, 8), Math.max(8, viewHeight - height - 8));
1027
- menu.style.left = `${Math.round(left)}px`;
1028
- menu.style.top = `${Math.round(top)}px`;
1029
- };
1030
- const onSurfaceContext = (event) => {
1031
- event.preventDefault();
1032
- if (!menu.hidden) return;
1033
- openMenu();
1034
- };
1035
- const onMenuClick = (event) => {
1036
- const item = event.target?.closest?.("[data-menu]");
1037
- if (!item) return;
1038
- const action = item.dataset.menu;
1039
- if (action === "toggle-card") {
1040
- setCardHidden(!surfaceFlags.cardHidden);
1041
- } else if (action === "refresh") {
1042
- void poll(true);
1043
- }
1044
- menu.hidden = true;
1045
- };
1046
- menu.addEventListener("click", onMenuClick);
1047
994
  let currentFoot = null;
1048
995
  let footTooNarrow = false;
1049
996
  let noDataHidden = false;
@@ -1153,6 +1100,14 @@ function mountQuotaCard({
1153
1100
  syncPop();
1154
1101
  };
1155
1102
  const onPanelClick = (event) => {
1103
+ if (event.target?.closest?.('[data-action="panel-refresh"]')) {
1104
+ void poll(true);
1105
+ return;
1106
+ }
1107
+ if (event.target?.closest?.('[data-action="toggle-card"]')) {
1108
+ setCardHidden(!surfaceFlags.cardHidden);
1109
+ return;
1110
+ }
1156
1111
  if (event.target?.closest?.('[data-action="panel-close"]')) {
1157
1112
  openKey = void 0;
1158
1113
  syncPop();
@@ -1189,9 +1144,9 @@ function mountQuotaCard({
1189
1144
  const onDocPointerDown = (event) => {
1190
1145
  if (openKey === void 0) return;
1191
1146
  const path = typeof event.composedPath === "function" ? event.composedPath() : [];
1192
- if (path.includes(panel) || path.includes(menu) || path.includes(container)) return;
1147
+ if (path.includes(panel) || path.includes(container)) return;
1193
1148
  const target = event.target;
1194
- if (target === panel || target === menu || target === container) return;
1149
+ if (target === panel || target === container) return;
1195
1150
  if (target?.getRootNode?.() === cardRoot || target?.getRootNode?.() === floatRoot) return;
1196
1151
  openKey = void 0;
1197
1152
  renderAll();
@@ -1212,10 +1167,6 @@ function mountQuotaCard({
1212
1167
  };
1213
1168
  const onDocKeydown = (event) => {
1214
1169
  if (event.key !== "Escape") return;
1215
- if (!menu.hidden) {
1216
- menu.hidden = true;
1217
- return;
1218
- }
1219
1170
  if (openKey !== void 0) {
1220
1171
  openKey = void 0;
1221
1172
  renderAll();
@@ -1235,7 +1186,6 @@ function mountQuotaCard({
1235
1186
  });
1236
1187
  surface.addEventListener("click", onSurfaceClick);
1237
1188
  surface.addEventListener("keydown", onSurfaceKeydown);
1238
- surface.addEventListener("contextmenu", onSurfaceContext);
1239
1189
  let disposeDrag = attachDrag(surface);
1240
1190
  doc.addEventListener("visibilitychange", onVisibilityChange);
1241
1191
  doc.addEventListener("pointerdown", onDocPointerDown, true);
@@ -1257,8 +1207,6 @@ function mountQuotaCard({
1257
1207
  panel.removeEventListener("click", onPanelClick);
1258
1208
  surface.removeEventListener("click", onSurfaceClick);
1259
1209
  surface.removeEventListener("keydown", onSurfaceKeydown);
1260
- surface.removeEventListener("contextmenu", onSurfaceContext);
1261
- menu.removeEventListener("click", onMenuClick);
1262
1210
  disposeDrag();
1263
1211
  doc.body.style.userSelect = "";
1264
1212
  container.remove();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-quota-watch",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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",
@@ -6,7 +6,6 @@ import { clampPoint } from './prefs.mjs'
6
6
 
7
7
  export const DRAG_SLOP_MOUSE = 6
8
8
  export const DRAG_SLOP_TOUCH = 10
9
- export const LONG_PRESS_MS = 500
10
9
 
11
10
  /** Touch/pen gestures get the larger slop so taps win over drags. */
12
11
  export function dragSlop(pointerType) {
package/src/client.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CLIENT_POLL_INTERVAL_MS, CLIENT_ROUTES } from './shared.mjs'
2
2
  import { remainingToUsed } from './core/adapters.mjs'
3
3
  import { MARGIN, clampPoint, loadFloatPosition, loadSurfaceFlags, saveFloatPosition, saveSurfaceFlags } from './client/prefs.mjs'
4
- import { DRAG_SLOP_MOUSE, DRAG_SLOP_TOUCH, LONG_PRESS_MS, parseInset, clampFrame, dragSlop, grabOffset } from './client/drag.mjs'
4
+ import { DRAG_SLOP_MOUSE, DRAG_SLOP_TOUCH, parseInset, clampFrame, dragSlop, grabOffset } from './client/drag.mjs'
5
5
 
6
6
  export const name = 'quota-watch-client'
7
7
  export const inject = []
@@ -23,7 +23,7 @@ const COPY = {
23
23
  title: 'Token额度',
24
24
  openDetails: '查看详情',
25
25
  close: '关闭',
26
- menu: {
26
+ actions: {
27
27
  showCard: '显示侧边栏卡片',
28
28
  hideCard: '隐藏侧边栏卡片',
29
29
  refreshNow: '立即刷新',
@@ -56,7 +56,7 @@ const COPY = {
56
56
  title: 'Quota Watch',
57
57
  openDetails: 'View details',
58
58
  close: 'Close',
59
- menu: {
59
+ actions: {
60
60
  showCard: 'Show sidebar card',
61
61
  hideCard: 'Hide sidebar card',
62
62
  refreshNow: 'Refresh now',
@@ -137,10 +137,9 @@ const FLOAT_STYLE_TEXT = `
137
137
  }
138
138
  @media print { :host { display: none !important; } }
139
139
  @keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
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; }
141
- .dqw-menu[hidden] { display: none; }
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; }
143
- .dqw-menu-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
140
+ .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); }
144
143
  .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; }
145
144
  @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)); } }
146
145
  .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); }
@@ -271,17 +270,6 @@ function quotaIcon(doc) {
271
270
  return svg
272
271
  }
273
272
 
274
- /** One right-click menu row. */
275
- function menuItem(doc, action, label) {
276
- const item = doc.createElement('button')
277
- item.type = 'button'
278
- item.className = 'dqw-menu-item'
279
- item.setAttribute('role', 'menuitem')
280
- item.dataset.menu = action
281
- item.append(text(doc, 'span', '', label))
282
- return item
283
- }
284
-
285
273
  /** Double-chevron icon for the panel's back affordance. */
286
274
  function backIcon(doc) {
287
275
  const namespace = 'http://www.w3.org/2000/svg'
@@ -480,12 +468,6 @@ export function mountQuotaCard({
480
468
  applyPosition()
481
469
  // Panel lives inside the float shadow root from now on (契约总表「归属时序」).
482
470
  floatRoot.append(panel)
483
- const menu = doc.createElement('div')
484
- menu.className = 'dqw-menu'
485
- menu.dataset.dshQuotaWatchMenu = ''
486
- menu.setAttribute('role', 'menu')
487
- menu.hidden = true
488
- floatRoot.append(menu)
489
471
 
490
472
  let suppressNextClick = false
491
473
  /** One rAF (or a synchronous fallback when the host has none) — drag frames
@@ -511,13 +493,6 @@ export function mountQuotaCard({
511
493
  let restX = 0
512
494
  let restY = 0
513
495
  let lastPoint = null
514
- let longPressTimer
515
- const clearLongPress = () => {
516
- if (longPressTimer !== undefined) {
517
- win.clearTimeout(longPressTimer)
518
- longPressTimer = undefined
519
- }
520
- }
521
496
  /** Doc-level move/up/cancel are bound only while a gesture is active —
522
497
  * the pill's resting state costs zero document-level listeners. */
523
498
  const bindGesture = () => {
@@ -538,18 +513,6 @@ export function mountQuotaCard({
538
513
  originY = event.clientY
539
514
  // Graded slop: touch/pen get 10px so taps win over drags (Task 6).
540
515
  slop = dragSlop(event.pointerType)
541
- // Long press opens the context menu on touch (iOS has no contextmenu);
542
- // movement past the slop cancels it in favor of the drag.
543
- clearLongPress()
544
- if (slop === DRAG_SLOP_TOUCH) {
545
- longPressTimer = win.setTimeout(() => {
546
- longPressTimer = undefined
547
- if (!dragging || moved) return
548
- try { win.navigator.vibrate?.(10) } catch { /* vibration unavailable */ }
549
- openMenu()
550
- suppressNextClick = true
551
- }, LONG_PRESS_MS)
552
- }
553
516
  const rect = surfaceEl.getBoundingClientRect()
554
517
  grab = grabOffset({ x: event.clientX, y: event.clientY }, rect)
555
518
  dragSize = rect.width > 0 ? { width: rect.width, height: rect.height } : FALLBACK_SIZE
@@ -563,7 +526,6 @@ export function mountQuotaCard({
563
526
  if (!moved) {
564
527
  if (Math.hypot(event.clientX - originX, event.clientY - originY) < slop) return
565
528
  moved = true
566
- clearLongPress()
567
529
  doc.body.style.userSelect = 'none'
568
530
  surfaceEl.classList.add('dqw-capsule--dragging')
569
531
  }
@@ -591,7 +553,6 @@ export function mountQuotaCard({
591
553
  const onPointerUp = (event) => {
592
554
  if (!dragging) return
593
555
  dragging = false
594
- clearLongPress()
595
556
  unbindGesture()
596
557
  doc.body.style.userSelect = ''
597
558
  if (!moved) return
@@ -614,7 +575,6 @@ export function mountQuotaCard({
614
575
  const onPointerCancel = () => {
615
576
  if (!dragging) return
616
577
  dragging = false
617
- clearLongPress()
618
578
  unbindGesture()
619
579
  doc.body.style.userSelect = ''
620
580
  surfaceEl.classList.remove('dqw-capsule--dragging')
@@ -719,6 +679,34 @@ export function mountQuotaCard({
719
679
  return header
720
680
  }
721
681
 
682
+ /** Panel footer: the card toggle and a manual refresh sit side by side in an
683
+ * equal-width grid (perceived width is the box, not the glyph count, so the
684
+ * 1fr/1fr columns also survive label flips and other locales). This replaces
685
+ * the retired capsule context menu: one click surface, no hidden gestures,
686
+ * and it renders even in the loading/error empty state. */
687
+ const overviewActions = () => {
688
+ const actions = doc.createElement('div')
689
+ actions.className = 'dqw-panel-actions'
690
+ if (hasCardMount()) {
691
+ const toggle = doc.createElement('button')
692
+ toggle.type = 'button'
693
+ toggle.className = 'dqw-panel-action'
694
+ toggle.dataset.action = 'toggle-card'
695
+ const toggleLabel = surfaceFlags.cardHidden ? copy.actions.showCard : copy.actions.hideCard
696
+ toggle.textContent = toggleLabel
697
+ toggle.setAttribute('aria-label', toggleLabel)
698
+ actions.append(toggle)
699
+ }
700
+ const refresh = doc.createElement('button')
701
+ refresh.type = 'button'
702
+ refresh.className = 'dqw-panel-action'
703
+ refresh.dataset.action = 'panel-refresh'
704
+ refresh.textContent = copy.actions.refreshNow
705
+ refresh.setAttribute('aria-label', copy.actions.refreshNow)
706
+ actions.append(refresh)
707
+ return actions
708
+ }
709
+
722
710
  const renderOverview = () => {
723
711
  panel.replaceChildren(panelHeader({ title: copy.title }))
724
712
  const providers = snapshot?.providers ?? []
@@ -733,16 +721,17 @@ export function mountQuotaCard({
733
721
  ].filter(Boolean).flat()
734
722
  if (rows.length === 0) {
735
723
  panel.append(text(doc, 'p', 'dqw-muted', lastUpdated.textContent || copy.loading))
736
- return
737
- }
738
- const list = doc.createElement('div')
739
- list.className = 'dqw-overview-list'
740
- list.append(...rows)
741
- // Tag the reused rows for the panel's own click/keyboard routing.
742
- for (const row of list.querySelectorAll('[data-dsh-quota-watch-row]')) {
743
- row.dataset.dshQuotaWatchPanelProvider = row.dataset.dshQuotaWatchRow
724
+ } else {
725
+ const list = doc.createElement('div')
726
+ list.className = 'dqw-overview-list'
727
+ list.append(...rows)
728
+ // Tag the reused rows for the panel's own click/keyboard routing.
729
+ for (const row of list.querySelectorAll('[data-dsh-quota-watch-row]')) {
730
+ row.dataset.dshQuotaWatchPanelProvider = row.dataset.dshQuotaWatchRow
731
+ }
732
+ panel.append(list)
744
733
  }
745
- panel.append(list)
734
+ panel.append(overviewActions())
746
735
  }
747
736
 
748
737
  function renderDetail(detail, key, provider, fromPanelNav = false) {
@@ -973,44 +962,6 @@ export function mountQuotaCard({
973
962
  onSurfaceClick()
974
963
  }
975
964
  const hasCardMount = () => Boolean(footArea(doc))
976
- const openMenu = () => {
977
- const items = []
978
- if (hasCardMount()) {
979
- items.push(menuItem(doc, 'toggle-card', surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard))
980
- }
981
- items.push(menuItem(doc, 'refresh', copy.menu.refreshNow))
982
- menu.replaceChildren(...items)
983
- menu.hidden = false
984
- const rect = surface.getBoundingClientRect()
985
- const viewWidth = win.innerWidth ?? 1024
986
- const viewHeight = win.innerHeight ?? 768
987
- const width = menu.offsetWidth || 150
988
- const height = menu.offsetHeight || 120
989
- let left = rect.right + 8
990
- if (left + width > viewWidth - 8) left = Math.max(8, rect.left - width - 8)
991
- const top = Math.min(Math.max(rect.top, 8), Math.max(8, viewHeight - height - 8))
992
- menu.style.left = `${Math.round(left)}px`
993
- menu.style.top = `${Math.round(top)}px`
994
- }
995
- const onSurfaceContext = (event) => {
996
- event.preventDefault()
997
- // Android fires the native contextmenu right after the long-press timer:
998
- // an already-open menu is kept as-is instead of being rebuilt.
999
- if (!menu.hidden) return
1000
- openMenu()
1001
- }
1002
- const onMenuClick = (event) => {
1003
- const item = event.target?.closest?.('[data-menu]')
1004
- if (!item) return
1005
- const action = item.dataset.menu
1006
- if (action === 'toggle-card') {
1007
- setCardHidden(!surfaceFlags.cardHidden)
1008
- } else if (action === 'refresh') {
1009
- void poll(true)
1010
- }
1011
- menu.hidden = true
1012
- }
1013
- menu.addEventListener('click', onMenuClick)
1014
965
 
1015
966
  // Cross-profile narrow-container detection: a measurably narrow foot would
1016
967
  // clip the card into unreadable text regardless of HOW the host signals its
@@ -1140,6 +1091,19 @@ export function mountQuotaCard({
1140
1091
  syncPop()
1141
1092
  }
1142
1093
  const onPanelClick = (event) => {
1094
+ // Panel-scoped actions (the retired context menu now lives here): buttons
1095
+ // sit inside this shadow tree, so a panel-level listener sees the real
1096
+ // target with no retargeting ambiguity.
1097
+ if (event.target?.closest?.('[data-action="panel-refresh"]')) {
1098
+ void poll(true)
1099
+ return
1100
+ }
1101
+ if (event.target?.closest?.('[data-action="toggle-card"]')) {
1102
+ // renderAll → syncPop → renderOverview re-renders the footer, so the
1103
+ // label flips in place while the panel stays open.
1104
+ setCardHidden(!surfaceFlags.cardHidden)
1105
+ return
1106
+ }
1143
1107
  if (event.target?.closest?.('[data-action="panel-close"]')) {
1144
1108
  openKey = undefined
1145
1109
  syncPop()
@@ -1176,9 +1140,9 @@ export function mountQuotaCard({
1176
1140
  const onDocPointerDown = (event) => {
1177
1141
  if (openKey === undefined) return
1178
1142
  const path = typeof event.composedPath === 'function' ? event.composedPath() : []
1179
- if (path.includes(panel) || path.includes(menu) || path.includes(container)) return
1143
+ if (path.includes(panel) || path.includes(container)) return
1180
1144
  const target = event.target
1181
- if (target === panel || target === menu || target === container) return
1145
+ if (target === panel || target === container) return
1182
1146
  if (target?.getRootNode?.() === cardRoot || target?.getRootNode?.() === floatRoot) return
1183
1147
  openKey = undefined
1184
1148
  renderAll()
@@ -1200,10 +1164,6 @@ export function mountQuotaCard({
1200
1164
  }
1201
1165
  const onDocKeydown = (event) => {
1202
1166
  if (event.key !== 'Escape') return
1203
- if (!menu.hidden) {
1204
- menu.hidden = true
1205
- return
1206
- }
1207
1167
  if (openKey !== undefined) {
1208
1168
  openKey = undefined
1209
1169
  renderAll()
@@ -1223,7 +1183,6 @@ export function mountQuotaCard({
1223
1183
  })
1224
1184
  surface.addEventListener('click', onSurfaceClick)
1225
1185
  surface.addEventListener('keydown', onSurfaceKeydown)
1226
- surface.addEventListener('contextmenu', onSurfaceContext)
1227
1186
  let disposeDrag = attachDrag(surface)
1228
1187
  doc.addEventListener('visibilitychange', onVisibilityChange)
1229
1188
  doc.addEventListener('pointerdown', onDocPointerDown, true)
@@ -1246,8 +1205,6 @@ export function mountQuotaCard({
1246
1205
  panel.removeEventListener('click', onPanelClick)
1247
1206
  surface.removeEventListener('click', onSurfaceClick)
1248
1207
  surface.removeEventListener('keydown', onSurfaceKeydown)
1249
- surface.removeEventListener('contextmenu', onSurfaceContext)
1250
- menu.removeEventListener('click', onMenuClick)
1251
1208
  disposeDrag()
1252
1209
  // A dispose landing mid-gesture must not leave the body unselectable.
1253
1210
  doc.body.style.userSelect = ''