@iasiv5/dsh-quota-watch 0.1.11 → 0.1.13
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 +2 -2
- package/lib/client.js +36 -64
- package/package.json +1 -1
- package/src/client/drag.mjs +3 -18
- package/src/client/prefs.mjs +14 -16
- package/src/client.mjs +32 -60
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
- **悬浮胶囊**(0.1.1
|
|
9
|
+
- **悬浮胶囊**(0.1.1 起为唯一浮动形态,圆球已退役):常驻屏幕(默认右下角安全区),可自由拖拽到屏幕任意位置停放,位置跨重启记忆,越界自动钳回视口(距边 8px 安全边距)。拖拽跟手(保持抓取点,transform 渲染);触摸设备可直接拖拽(触摸阈值更宽防误拖)、长按 500ms 呼出菜单,点击热区扩展到 44px,自动避让 iPhone 刘海 / 小黑条(safe-area)。胶囊一行显示「GLM 13% · Copilot 24%」,已用超过 80% 转橙、超过 95% 转红,任一 provider 超 95% 时胶囊轻微脉动(尊重系统「减少动态效果」设置,开启时不脉动);颜色使用 DSH 主题语义变量,自动适配浅色 / 深色主题。悬停提示与 aria-label 实时给出摘要。
|
|
10
10
|
- **面板**:点击胶囊展开浮动详情面板(半透明毛玻璃底,宽 `min(320px, 100vw−24px)`,靠边自动翻侧,滚动 / 缩放跟随;≤480px 窄屏自动改为底部弹层并避开 safe-area),头部带标题与 ✕,Esc 或点击面板外关闭。概览列两个 provider 行,点击进入详情:GLM 为今日 Tokens / 调用次数双大数字 + 五列对齐窗口网格(MCP(月)→ 5 小时 → 每月 → 每周固定排序,周窗口未下发时显示「每周额度 · ♾️(无限)」占位)+ 分模型用量行;Copilot 为可用额度、本期已用(credits 消耗 + 弱化百分比)、重置时间三行。接口未下发有效 `remaining` / `percent_remaining` 时该行不渲染,`credits_used` 绝不用于推算余额。
|
|
11
11
|
- **侧边栏卡片(可选,默认隐藏)**:胶囊是默认界面;需要侧栏卡片时在胶囊右键菜单开启「显示侧边栏卡片」(Web profile 侧栏底部区块存在时才可选)。卡片显示时提供摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测。侧栏收起(Web 窄栏)或容器过窄(<160px,跨 profile 通用判据)时卡片自动隐藏,侧栏整体不存在(Desktop 收起态)同样自然缺席、无报错——入口始终由悬浮胶囊承载。
|
|
12
12
|
- **右键菜单**:显示 / 隐藏侧边栏卡片 · 立即刷新。菜单项随上下文增减(无侧栏卡片挂载点时只剩刷新项)。
|
|
@@ -47,4 +47,4 @@ dsh plugin --profile web add @iasiv5/dsh-quota-watch
|
|
|
47
47
|
|
|
48
48
|
## English summary
|
|
49
49
|
|
|
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
|
|
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).
|
package/lib/client.js
CHANGED
|
@@ -82,7 +82,7 @@ function remainingToUsed(percentRemaining) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// src/client/prefs.mjs
|
|
85
|
-
var
|
|
85
|
+
var FLOAT_POSITION_KEY = "dsh-quota-watch:float-position";
|
|
86
86
|
var SURFACE_FLAGS_KEY = "dsh-quota-watch:surface-flags";
|
|
87
87
|
var MARGIN = 8;
|
|
88
88
|
var ZERO_INSETS = { left: 0, right: 0, top: 0, bottom: 0 };
|
|
@@ -107,26 +107,25 @@ function clampPoint(point, viewport, size = 0, insets = ZERO_INSETS) {
|
|
|
107
107
|
y: Math.min(Math.max(point.y, minY), maxY)
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
function loadFloatDock(storage) {
|
|
110
|
+
function loadFloatPosition(storage) {
|
|
112
111
|
try {
|
|
113
|
-
const raw = storage.getItem(
|
|
112
|
+
const raw = storage.getItem(FLOAT_POSITION_KEY);
|
|
114
113
|
if (typeof raw !== "string" || raw === "") return null;
|
|
115
114
|
const parsed = JSON.parse(raw);
|
|
116
|
-
if (typeof parsed !== "object" || parsed === null || !
|
|
115
|
+
if (typeof parsed !== "object" || parsed === null || !Number.isFinite(parsed.x) || !Number.isFinite(parsed.y)) {
|
|
117
116
|
return null;
|
|
118
117
|
}
|
|
119
|
-
return {
|
|
118
|
+
return { x: parsed.x, y: parsed.y };
|
|
120
119
|
} catch {
|
|
121
120
|
return null;
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
|
-
function
|
|
125
|
-
if (typeof
|
|
126
|
-
throw new TypeError(`invalid float
|
|
123
|
+
function saveFloatPosition(storage, point) {
|
|
124
|
+
if (typeof point !== "object" || point === null || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
|
125
|
+
throw new TypeError(`invalid float position: ${JSON.stringify(point)}`);
|
|
127
126
|
}
|
|
128
127
|
try {
|
|
129
|
-
storage.setItem(
|
|
128
|
+
storage.setItem(FLOAT_POSITION_KEY, JSON.stringify({ x: point.x, y: point.y }));
|
|
130
129
|
} catch {
|
|
131
130
|
}
|
|
132
131
|
}
|
|
@@ -166,17 +165,6 @@ function pillTopLeft(point, grab) {
|
|
|
166
165
|
function clampFrame(point, grab, size, viewport, insets) {
|
|
167
166
|
return clampPoint(pillTopLeft(point, grab), viewport, size, insets);
|
|
168
167
|
}
|
|
169
|
-
function releaseDock(point, grab, size, viewport, insets) {
|
|
170
|
-
const topLeft = pillTopLeft(point, grab);
|
|
171
|
-
const centerX = topLeft.x + size.width / 2;
|
|
172
|
-
const edge = centerX < viewport.width / 2 ? "left" : "right";
|
|
173
|
-
return { edge, offsetY: clampPoint(topLeft, viewport, size, insets).y };
|
|
174
|
-
}
|
|
175
|
-
function dockX(edge, width, viewport, insets) {
|
|
176
|
-
const insetLeft = Number.isFinite(insets?.left) ? insets.left : 0;
|
|
177
|
-
const insetRight = Number.isFinite(insets?.right) ? insets.right : 0;
|
|
178
|
-
return edge === "left" ? MARGIN + insetLeft : viewport.width - insetRight - MARGIN - width;
|
|
179
|
-
}
|
|
180
168
|
function parseInset(computedValue) {
|
|
181
169
|
const value = Number.parseFloat(computedValue);
|
|
182
170
|
return Number.isFinite(value) ? value : 0;
|
|
@@ -296,11 +284,10 @@ var FLOAT_STYLE_TEXT = `
|
|
|
296
284
|
.dqw-capsule-glm.warn, .dqw-capsule-copilot.warn { color: var(--dsw-alias-label-warning, #d29922); }
|
|
297
285
|
.dqw-capsule-glm.danger, .dqw-capsule-copilot.danger { color: var(--dsw-alias-label-danger, #c93c3c); }
|
|
298
286
|
.dqw-capsule[hidden] { display: none; }
|
|
299
|
-
.dqw-capsule--dragging { cursor: grabbing; -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f));
|
|
287
|
+
.dqw-capsule--dragging { cursor: grabbing; -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); box-shadow: 0 8px 24px rgba(0,0,0,.35); transform: scale(1.04); transition: none; }
|
|
300
288
|
@media (prefers-reduced-motion: no-preference) {
|
|
301
289
|
.dqw-capsule[data-alert] { animation: dqw-pulse 1.6s ease-in-out infinite; }
|
|
302
290
|
.dqw-capsule { transition: border-color .12s ease-out, box-shadow .12s ease-out; }
|
|
303
|
-
.dqw-capsule--snapping { transition: transform 150ms cubic-bezier(.2,.8,.2,1); }
|
|
304
291
|
}
|
|
305
292
|
@media print { :host { display: none !important; } }
|
|
306
293
|
@keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
|
|
@@ -544,19 +531,19 @@ function mountQuotaCard({
|
|
|
544
531
|
const rect = surface.getBoundingClientRect();
|
|
545
532
|
return rect.width > 0 ? { width: rect.width, height: rect.height } : FALLBACK_SIZE;
|
|
546
533
|
};
|
|
547
|
-
const
|
|
534
|
+
const applyPosition = () => {
|
|
548
535
|
const viewWidth = win.innerWidth ?? 1024;
|
|
549
536
|
const viewHeight = win.innerHeight ?? 768;
|
|
550
537
|
const viewport = { width: viewWidth, height: viewHeight };
|
|
551
538
|
const size = measureSurface();
|
|
552
|
-
const dock = loadFloatDock(win.localStorage) ?? {
|
|
553
|
-
edge: "right",
|
|
554
|
-
offsetY: viewHeight - MARGIN - size.height
|
|
555
|
-
};
|
|
556
539
|
const insets = readInsets();
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
540
|
+
const pos = loadFloatPosition(win.localStorage) ?? {
|
|
541
|
+
x: viewWidth - MARGIN - insets.right - size.width,
|
|
542
|
+
y: viewHeight - MARGIN - insets.bottom - size.height
|
|
543
|
+
};
|
|
544
|
+
const point = clampPoint(pos, viewport, size, insets);
|
|
545
|
+
floatHost.style.left = `${Math.round(point.x)}px`;
|
|
546
|
+
floatHost.style.top = `${Math.round(point.y)}px`;
|
|
560
547
|
};
|
|
561
548
|
const safeAreaProbe = text(doc, "div", "", "");
|
|
562
549
|
safeAreaProbe.style.cssText = "position:fixed;visibility:hidden;pointer-events:none;width:0;height:0;overflow:hidden;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);";
|
|
@@ -574,7 +561,7 @@ function mountQuotaCard({
|
|
|
574
561
|
return ZERO_INSETS2;
|
|
575
562
|
}
|
|
576
563
|
};
|
|
577
|
-
|
|
564
|
+
applyPosition();
|
|
578
565
|
floatRoot.append(panel);
|
|
579
566
|
const menu = doc.createElement("div");
|
|
580
567
|
menu.className = "dqw-menu";
|
|
@@ -654,17 +641,20 @@ function mountQuotaCard({
|
|
|
654
641
|
if (!dragging || !lastPoint || disposed) return;
|
|
655
642
|
const viewWidth = win.innerWidth ?? 1024;
|
|
656
643
|
const viewHeight = win.innerHeight ?? 768;
|
|
657
|
-
const
|
|
644
|
+
const insets = readInsets();
|
|
645
|
+
const topLeft = clampFrame(lastPoint, grab, dragSize, { width: viewWidth, height: viewHeight }, insets);
|
|
658
646
|
const dx = topLeft.x - restX;
|
|
659
647
|
const dy = topLeft.y - restY;
|
|
660
|
-
|
|
648
|
+
floatHost.style.willChange = "transform";
|
|
649
|
+
floatHost.style.transform = `translate3d(${Math.round(dx)}px, ${Math.round(dy)}px, 0)`;
|
|
661
650
|
});
|
|
662
651
|
};
|
|
663
|
-
const finishDragRelease = (
|
|
664
|
-
surfaceEl.classList.remove("dqw-capsule--dragging"
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
652
|
+
const finishDragRelease = (point) => {
|
|
653
|
+
surfaceEl.classList.remove("dqw-capsule--dragging");
|
|
654
|
+
floatHost.style.willChange = "";
|
|
655
|
+
floatHost.style.transform = "";
|
|
656
|
+
saveFloatPosition(win.localStorage, point);
|
|
657
|
+
applyPosition();
|
|
668
658
|
suppressNextClick = true;
|
|
669
659
|
};
|
|
670
660
|
const onPointerUp = (event) => {
|
|
@@ -678,7 +668,7 @@ function mountQuotaCard({
|
|
|
678
668
|
const viewHeight = win.innerHeight ?? 768;
|
|
679
669
|
const insets = readInsets();
|
|
680
670
|
const viewport = { width: viewWidth, height: viewHeight };
|
|
681
|
-
const
|
|
671
|
+
const point = clampFrame(
|
|
682
672
|
{ x: event.clientX, y: event.clientY },
|
|
683
673
|
grab,
|
|
684
674
|
dragSize,
|
|
@@ -686,26 +676,7 @@ function mountQuotaCard({
|
|
|
686
676
|
insets
|
|
687
677
|
);
|
|
688
678
|
suppressNextClick = true;
|
|
689
|
-
|
|
690
|
-
if (motionAllowed) {
|
|
691
|
-
surfaceEl.classList.remove("dqw-capsule--dragging");
|
|
692
|
-
surfaceEl.classList.add("dqw-capsule--snapping");
|
|
693
|
-
const targetLeft = dockX(dock.edge, dragSize.width, viewport, insets);
|
|
694
|
-
surfaceEl.style.transform = `translate3d(${Math.round(targetLeft - restX)}px, ${Math.round(dock.offsetY - restY)}px, 0) scale(1)`;
|
|
695
|
-
let settled = false;
|
|
696
|
-
let snapTimer;
|
|
697
|
-
const settle = () => {
|
|
698
|
-
if (settled || disposed) return;
|
|
699
|
-
settled = true;
|
|
700
|
-
surfaceEl.removeEventListener("transitionend", settle);
|
|
701
|
-
win.clearTimeout(snapTimer);
|
|
702
|
-
finishDragRelease(dock);
|
|
703
|
-
};
|
|
704
|
-
surfaceEl.addEventListener("transitionend", settle);
|
|
705
|
-
snapTimer = win.setTimeout(settle, 200);
|
|
706
|
-
} else {
|
|
707
|
-
finishDragRelease(dock);
|
|
708
|
-
}
|
|
679
|
+
finishDragRelease(point);
|
|
709
680
|
};
|
|
710
681
|
const onPointerCancel = () => {
|
|
711
682
|
if (!dragging) return;
|
|
@@ -713,8 +684,9 @@ function mountQuotaCard({
|
|
|
713
684
|
clearLongPress();
|
|
714
685
|
unbindGesture();
|
|
715
686
|
doc.body.style.userSelect = "";
|
|
716
|
-
surfaceEl.classList.remove("dqw-capsule--dragging"
|
|
717
|
-
|
|
687
|
+
surfaceEl.classList.remove("dqw-capsule--dragging");
|
|
688
|
+
floatHost.style.willChange = "";
|
|
689
|
+
floatHost.style.transform = "";
|
|
718
690
|
};
|
|
719
691
|
surfaceEl.addEventListener("pointerdown", onPointerDown);
|
|
720
692
|
const disposeDrag2 = () => {
|
|
@@ -1016,7 +988,7 @@ function mountQuotaCard({
|
|
|
1016
988
|
const label = summary === "" ? copy.title : locale === "zh" ? `${copy.title}\uFF1A${summary}` : `${copy.title}: ${summary}`;
|
|
1017
989
|
surface.setAttribute("aria-label", label);
|
|
1018
990
|
surface.title = label;
|
|
1019
|
-
|
|
991
|
+
applyPosition();
|
|
1020
992
|
};
|
|
1021
993
|
const onSurfaceClick = () => {
|
|
1022
994
|
if (suppressNextClick) {
|
|
@@ -1236,7 +1208,7 @@ function mountQuotaCard({
|
|
|
1236
1208
|
const onWinResize = () => {
|
|
1237
1209
|
placePanel();
|
|
1238
1210
|
syncCardVisibility();
|
|
1239
|
-
|
|
1211
|
+
applyPosition();
|
|
1240
1212
|
};
|
|
1241
1213
|
const onDocKeydown = (event) => {
|
|
1242
1214
|
if (event.key !== "Escape") return;
|
package/package.json
CHANGED
package/src/client/drag.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// DOM-free drag state machine for @iasiv5/dsh-quota-watch
|
|
1
|
+
// DOM-free drag state machine for @iasiv5/dsh-quota-watch.
|
|
2
2
|
// Point/rect values come from the caller; nothing here touches the DOM, so the
|
|
3
3
|
// math stays unit-testable under jsdom.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { clampPoint } from './prefs.mjs'
|
|
6
6
|
|
|
7
7
|
export const DRAG_SLOP_MOUSE = 6
|
|
8
8
|
export const DRAG_SLOP_TOUCH = 10
|
|
@@ -22,26 +22,11 @@ function pillTopLeft(point, grab) {
|
|
|
22
22
|
return { x: point.x - grab.dx, y: point.y - grab.dy }
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** Live clamp for the pill top-left while the gesture is active. */
|
|
25
|
+
/** Live clamp for the pill top-left while the gesture is active or upon release. */
|
|
26
26
|
export function clampFrame(point, grab, size, viewport, insets) {
|
|
27
27
|
return clampPoint(pillTopLeft(point, grab), viewport, size, insets)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** Rest dock for a release: nearest horizontal edge by pill center, vertical offset clamped. */
|
|
31
|
-
export function releaseDock(point, grab, size, viewport, insets) {
|
|
32
|
-
const topLeft = pillTopLeft(point, grab)
|
|
33
|
-
const centerX = topLeft.x + size.width / 2
|
|
34
|
-
const edge = centerX < viewport.width / 2 ? 'left' : 'right'
|
|
35
|
-
return { edge, offsetY: clampPoint(topLeft, viewport, size, insets).y }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Resting x for a docked pill: always derived, never stored. */
|
|
39
|
-
export function dockX(edge, width, viewport, insets) {
|
|
40
|
-
const insetLeft = Number.isFinite(insets?.left) ? insets.left : 0
|
|
41
|
-
const insetRight = Number.isFinite(insets?.right) ? insets.right : 0
|
|
42
|
-
return edge === 'left' ? MARGIN + insetLeft : viewport.width - insetRight - MARGIN - width
|
|
43
|
-
}
|
|
44
|
-
|
|
45
30
|
/** Computed-style padding value ("20px" | "" | "auto") → numeric inset or 0. */
|
|
46
31
|
export function parseInset(computedValue) {
|
|
47
32
|
const value = Number.parseFloat(computedValue)
|
package/src/client/prefs.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Float-shell preferences for @iasiv5/dsh-quota-watch.
|
|
2
2
|
// Pure logic: no DOM. `storage` is a localStorage-like object (getItem/setItem).
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const FLOAT_POSITION_KEY = 'dsh-quota-watch:float-position'
|
|
5
5
|
export const SURFACE_FLAGS_KEY = 'dsh-quota-watch:surface-flags'
|
|
6
6
|
|
|
7
7
|
export const MARGIN = 8
|
|
@@ -31,40 +31,38 @@ export function clampPoint(point, viewport, size = 0, insets = ZERO_INSETS) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export function loadFloatDock(storage) {
|
|
34
|
+
export function loadFloatPosition(storage) {
|
|
37
35
|
try {
|
|
38
|
-
const raw = storage.getItem(
|
|
36
|
+
const raw = storage.getItem(FLOAT_POSITION_KEY)
|
|
39
37
|
if (typeof raw !== 'string' || raw === '') return null
|
|
40
38
|
const parsed = JSON.parse(raw)
|
|
41
39
|
if (
|
|
42
40
|
typeof parsed !== 'object' ||
|
|
43
41
|
parsed === null ||
|
|
44
|
-
!
|
|
45
|
-
!Number.isFinite(parsed.
|
|
42
|
+
!Number.isFinite(parsed.x) ||
|
|
43
|
+
!Number.isFinite(parsed.y)
|
|
46
44
|
) {
|
|
47
45
|
return null
|
|
48
46
|
}
|
|
49
|
-
return {
|
|
47
|
+
return { x: parsed.x, y: parsed.y }
|
|
50
48
|
} catch {
|
|
51
49
|
return null
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
export function
|
|
53
|
+
export function saveFloatPosition(storage, point) {
|
|
56
54
|
if (
|
|
57
|
-
typeof
|
|
58
|
-
|
|
59
|
-
!
|
|
60
|
-
!Number.isFinite(
|
|
55
|
+
typeof point !== 'object' ||
|
|
56
|
+
point === null ||
|
|
57
|
+
!Number.isFinite(point.x) ||
|
|
58
|
+
!Number.isFinite(point.y)
|
|
61
59
|
) {
|
|
62
|
-
throw new TypeError(`invalid float
|
|
60
|
+
throw new TypeError(`invalid float position: ${JSON.stringify(point)}`)
|
|
63
61
|
}
|
|
64
62
|
try {
|
|
65
|
-
storage.setItem(
|
|
63
|
+
storage.setItem(FLOAT_POSITION_KEY, JSON.stringify({ x: point.x, y: point.y }))
|
|
66
64
|
} catch {
|
|
67
|
-
/* storage unavailable;
|
|
65
|
+
/* storage unavailable; position stays session-only */
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
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
|
-
import { MARGIN, clampPoint,
|
|
4
|
-
import { DRAG_SLOP_MOUSE, DRAG_SLOP_TOUCH, LONG_PRESS_MS, parseInset, clampFrame,
|
|
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'
|
|
5
5
|
|
|
6
6
|
export const name = 'quota-watch-client'
|
|
7
7
|
export const inject = []
|
|
@@ -130,11 +130,10 @@ const FLOAT_STYLE_TEXT = `
|
|
|
130
130
|
.dqw-capsule-glm.warn, .dqw-capsule-copilot.warn { color: var(--dsw-alias-label-warning, #d29922); }
|
|
131
131
|
.dqw-capsule-glm.danger, .dqw-capsule-copilot.danger { color: var(--dsw-alias-label-danger, #c93c3c); }
|
|
132
132
|
.dqw-capsule[hidden] { display: none; }
|
|
133
|
-
.dqw-capsule--dragging { cursor: grabbing; -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f));
|
|
133
|
+
.dqw-capsule--dragging { cursor: grabbing; -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); box-shadow: 0 8px 24px rgba(0,0,0,.35); transform: scale(1.04); transition: none; }
|
|
134
134
|
@media (prefers-reduced-motion: no-preference) {
|
|
135
135
|
.dqw-capsule[data-alert] { animation: dqw-pulse 1.6s ease-in-out infinite; }
|
|
136
136
|
.dqw-capsule { transition: border-color .12s ease-out, box-shadow .12s ease-out; }
|
|
137
|
-
.dqw-capsule--snapping { transition: transform 150ms cubic-bezier(.2,.8,.2,1); }
|
|
138
137
|
}
|
|
139
138
|
@media print { :host { display: none !important; } }
|
|
140
139
|
@keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
|
|
@@ -442,23 +441,21 @@ export function mountQuotaCard({
|
|
|
442
441
|
const rect = surface.getBoundingClientRect()
|
|
443
442
|
return rect.width > 0 ? { width: rect.width, height: rect.height } : FALLBACK_SIZE
|
|
444
443
|
}
|
|
445
|
-
/**
|
|
446
|
-
const
|
|
444
|
+
/** Position = user preferred {x, y} clamped to viewport & safe-area insets. */
|
|
445
|
+
const applyPosition = () => {
|
|
447
446
|
const viewWidth = win.innerWidth ?? 1024
|
|
448
447
|
const viewHeight = win.innerHeight ?? 768
|
|
449
448
|
const viewport = { width: viewWidth, height: viewHeight }
|
|
450
449
|
const size = measureSurface()
|
|
451
|
-
const dock = loadFloatDock(win.localStorage) ?? {
|
|
452
|
-
edge: 'right',
|
|
453
|
-
offsetY: viewHeight - MARGIN - size.height,
|
|
454
|
-
}
|
|
455
|
-
// Deliberately NOT persisted here: a window that starts small (Electron
|
|
456
|
-
// restore, split-screen) must not clobber the user's stored dock — storage
|
|
457
|
-
// updates on drag release only; this is the visual clamp.
|
|
458
450
|
const insets = readInsets()
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
451
|
+
const pos = loadFloatPosition(win.localStorage) ?? {
|
|
452
|
+
x: viewWidth - MARGIN - insets.right - size.width,
|
|
453
|
+
y: viewHeight - MARGIN - insets.bottom - size.height,
|
|
454
|
+
}
|
|
455
|
+
// Clamped visually so a restored or shrunk window never lets the capsule fall outside.
|
|
456
|
+
const point = clampPoint(pos, viewport, size, insets)
|
|
457
|
+
floatHost.style.left = `${Math.round(point.x)}px`
|
|
458
|
+
floatHost.style.top = `${Math.round(point.y)}px`
|
|
462
459
|
}
|
|
463
460
|
// Safe-area probe: env() insets ride computed padding; jsdom reports '' which
|
|
464
461
|
// parseInset normalizes to 0, so tests without a stub see zero insets.
|
|
@@ -480,7 +477,7 @@ export function mountQuotaCard({
|
|
|
480
477
|
return ZERO_INSETS
|
|
481
478
|
}
|
|
482
479
|
}
|
|
483
|
-
|
|
480
|
+
applyPosition()
|
|
484
481
|
// Panel lives inside the float shadow root from now on (契约总表「归属时序」).
|
|
485
482
|
floatRoot.append(panel)
|
|
486
483
|
const menu = doc.createElement('div')
|
|
@@ -575,17 +572,20 @@ export function mountQuotaCard({
|
|
|
575
572
|
if (!dragging || !lastPoint || disposed) return
|
|
576
573
|
const viewWidth = win.innerWidth ?? 1024
|
|
577
574
|
const viewHeight = win.innerHeight ?? 768
|
|
578
|
-
const
|
|
575
|
+
const insets = readInsets()
|
|
576
|
+
const topLeft = clampFrame(lastPoint, grab, dragSize, { width: viewWidth, height: viewHeight }, insets)
|
|
579
577
|
const dx = topLeft.x - restX
|
|
580
578
|
const dy = topLeft.y - restY
|
|
581
|
-
|
|
579
|
+
floatHost.style.willChange = 'transform'
|
|
580
|
+
floatHost.style.transform = `translate3d(${Math.round(dx)}px, ${Math.round(dy)}px, 0)`
|
|
582
581
|
})
|
|
583
582
|
}
|
|
584
|
-
const finishDragRelease = (
|
|
585
|
-
surfaceEl.classList.remove('dqw-capsule--dragging'
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
583
|
+
const finishDragRelease = (point) => {
|
|
584
|
+
surfaceEl.classList.remove('dqw-capsule--dragging')
|
|
585
|
+
floatHost.style.willChange = ''
|
|
586
|
+
floatHost.style.transform = ''
|
|
587
|
+
saveFloatPosition(win.localStorage, point)
|
|
588
|
+
applyPosition()
|
|
589
589
|
suppressNextClick = true
|
|
590
590
|
}
|
|
591
591
|
const onPointerUp = (event) => {
|
|
@@ -599,53 +599,27 @@ export function mountQuotaCard({
|
|
|
599
599
|
const viewHeight = win.innerHeight ?? 768
|
|
600
600
|
const insets = readInsets()
|
|
601
601
|
const viewport = { width: viewWidth, height: viewHeight }
|
|
602
|
-
const
|
|
602
|
+
const point = clampFrame(
|
|
603
603
|
{ x: event.clientX, y: event.clientY },
|
|
604
604
|
grab,
|
|
605
605
|
dragSize,
|
|
606
606
|
viewport,
|
|
607
607
|
insets,
|
|
608
608
|
)
|
|
609
|
-
// The real click lands within ms of pointerup — suppression is armed
|
|
610
|
-
// synchronously here, never deferred to the animation's settle (E1-2).
|
|
611
609
|
suppressNextClick = true
|
|
612
|
-
|
|
613
|
-
// (direct commit) where the motion query cannot be asked at all.
|
|
614
|
-
const motionAllowed = typeof win.matchMedia === 'function'
|
|
615
|
-
&& win.matchMedia('(prefers-reduced-motion: no-preference)').matches
|
|
616
|
-
if (motionAllowed) {
|
|
617
|
-
surfaceEl.classList.remove('dqw-capsule--dragging')
|
|
618
|
-
surfaceEl.classList.add('dqw-capsule--snapping')
|
|
619
|
-
// Animate toward the NEW dock (delta from rest) — the host still sits
|
|
620
|
-
// at the old dock until settle commits it, so (0,0) would slide the
|
|
621
|
-
// pill backwards first and then teleport (E1-1).
|
|
622
|
-
const targetLeft = dockX(dock.edge, dragSize.width, viewport, insets)
|
|
623
|
-
surfaceEl.style.transform = `translate3d(${Math.round(targetLeft - restX)}px, ${Math.round(dock.offsetY - restY)}px, 0) scale(1)`
|
|
624
|
-
let settled = false
|
|
625
|
-
let snapTimer
|
|
626
|
-
const settle = () => {
|
|
627
|
-
if (settled || disposed) return
|
|
628
|
-
settled = true
|
|
629
|
-
surfaceEl.removeEventListener('transitionend', settle)
|
|
630
|
-
win.clearTimeout(snapTimer)
|
|
631
|
-
finishDragRelease(dock)
|
|
632
|
-
}
|
|
633
|
-
surfaceEl.addEventListener('transitionend', settle)
|
|
634
|
-
snapTimer = win.setTimeout(settle, 200)
|
|
635
|
-
} else {
|
|
636
|
-
finishDragRelease(dock)
|
|
637
|
-
}
|
|
610
|
+
finishDragRelease(point)
|
|
638
611
|
}
|
|
639
612
|
/** A canceled pointer (scroll takeover, incoming call, palm) resets the
|
|
640
|
-
* gesture: no
|
|
613
|
+
* gesture: no position is persisted and the trailing click stays allowed. */
|
|
641
614
|
const onPointerCancel = () => {
|
|
642
615
|
if (!dragging) return
|
|
643
616
|
dragging = false
|
|
644
617
|
clearLongPress()
|
|
645
618
|
unbindGesture()
|
|
646
619
|
doc.body.style.userSelect = ''
|
|
647
|
-
surfaceEl.classList.remove('dqw-capsule--dragging'
|
|
648
|
-
|
|
620
|
+
surfaceEl.classList.remove('dqw-capsule--dragging')
|
|
621
|
+
floatHost.style.willChange = ''
|
|
622
|
+
floatHost.style.transform = ''
|
|
649
623
|
}
|
|
650
624
|
surfaceEl.addEventListener('pointerdown', onPointerDown)
|
|
651
625
|
const disposeDrag = () => {
|
|
@@ -977,9 +951,7 @@ export function mountQuotaCard({
|
|
|
977
951
|
const label = summary === '' ? copy.title : locale === 'zh' ? `${copy.title}:${summary}` : `${copy.title}: ${summary}`
|
|
978
952
|
surface.setAttribute('aria-label', label)
|
|
979
953
|
surface.title = label
|
|
980
|
-
|
|
981
|
-
// and the stored dock only knows the edge, not the pixel (ADR 0002).
|
|
982
|
-
applyDock()
|
|
954
|
+
applyPosition()
|
|
983
955
|
}
|
|
984
956
|
|
|
985
957
|
const onSurfaceClick = () => {
|
|
@@ -1224,7 +1196,7 @@ export function mountQuotaCard({
|
|
|
1224
1196
|
const onWinResize = () => {
|
|
1225
1197
|
placePanel()
|
|
1226
1198
|
syncCardVisibility()
|
|
1227
|
-
|
|
1199
|
+
applyPosition()
|
|
1228
1200
|
}
|
|
1229
1201
|
const onDocKeydown = (event) => {
|
|
1230
1202
|
if (event.key !== 'Escape') return
|