@hystudio/dsh-quota-pill 0.2.4 → 0.2.6

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.
Files changed (2) hide show
  1. package/client.js +107 -10
  2. package/package.json +1 -1
package/client.js CHANGED
@@ -513,6 +513,30 @@ window.__ModuleLoader__.load({
513
513
  credits: null,
514
514
  }];
515
515
  }
516
+ // Prepaid wallets (official DeepSeek): a balance, plus optional bonus
517
+ // credit. Both are money, not percentages, so each needs its own row —
518
+ // collapsing them into one label silently loses the bonus.
519
+ if (v.kind === 'payg' && v.credits && typeof v.credits.remain === 'number') {
520
+ const rows = [{
521
+ key: 'balance',
522
+ label: pick(lang, 'balanceLeft'),
523
+ pct: null,
524
+ reset: '',
525
+ credits: { remain: v.credits.remain, unit: v.credits.unit },
526
+ money: true,
527
+ }];
528
+ if (v.bonus && typeof v.bonus.amount === 'number') {
529
+ rows.push({
530
+ key: 'bonus',
531
+ label: pick(lang, 'bonusBalance'),
532
+ pct: null,
533
+ reset: '',
534
+ credits: { remain: v.bonus.amount, unit: v.bonus.unit },
535
+ money: true,
536
+ });
537
+ }
538
+ return rows;
539
+ }
516
540
  if (v.kind !== 'subscription-quota' || v.remainingPct === null || v.remainingPct === undefined) {
517
541
  return [{ key: 'kind', label: describeQuota(v, lang).text, pct: null, reset: '', credits: null }];
518
542
  }
@@ -595,6 +619,50 @@ window.__ModuleLoader__.load({
595
619
  const panelRef = React.useRef(null);
596
620
  const prevRunning = React.useRef(running);
597
621
 
622
+ // 面板位置:固定在视口内,而不是跟着触发器「贴」到哪算哪。
623
+ //
624
+ // 旧写法直接取触发器的 left,面板 340px 宽,窄窗口时右边就被切掉了;
625
+ // bottom 固定向上展开,窗口矮时上面同样被切。这里改成:
626
+ // · 水平:优先对齐触发器左边,再夹到视口内
627
+ // · 垂直:上方放得下就向上,放不下就翻到触发器下面
628
+ // · 两侧都塞不满时给 maxHeight,让面板自己滚动而不是溢出
629
+ const [panelStyle, setPanelStyle] = React.useState(null);
630
+ const GAP = 8; // 与触发器的间距
631
+ const EDGE = 12; // 距视口边缘的最小留白
632
+ const PANEL_MAX_W = 340;
633
+
634
+ /** 把面板夹进视口:水平贴触发器左边但不出界,垂直放不下就翻到下方。 */
635
+ const placePanel = React.useCallback(() => {
636
+ const el = anchorRef.current;
637
+ if (!el) return;
638
+ const r = el.getBoundingClientRect();
639
+ setPanelStyle(computePanelPlacement(
640
+ { left: r.left, top: r.top, bottom: r.bottom },
641
+ {
642
+ width: window.innerWidth || document.documentElement.clientWidth,
643
+ height: window.innerHeight || document.documentElement.clientHeight,
644
+ },
645
+ panelRef.current ? panelRef.current.offsetHeight : 0,
646
+ GAP, EDGE, PANEL_MAX_W,
647
+ ));
648
+ }, []);
649
+
650
+ React.useEffect(() => {
651
+ if (!open) { setPanelStyle(null); return undefined; }
652
+ placePanel();
653
+ // 挂载后量到真实高度再算一次,翻转判定才准
654
+ const raf = requestAnimationFrame(placePanel);
655
+ const onViewportChange = () => placePanel();
656
+ window.addEventListener('resize', onViewportChange);
657
+ // capture: 工具条自身可能滚动,滚动会改变触发器的位置
658
+ window.addEventListener('scroll', onViewportChange, true);
659
+ return () => {
660
+ cancelAnimationFrame(raf);
661
+ window.removeEventListener('resize', onViewportChange);
662
+ window.removeEventListener('scroll', onViewportChange, true);
663
+ };
664
+ }, [open, placePanel]);
665
+
598
666
  // 面板页签:默认「当前」(本会话在用的 provider),可切到「全部」。
599
667
  const [tab, setTab] = React.useState('current');
600
668
  // 切换动画从哪一侧滑入:'right' = 新内容自右来(往前进),'left' = 自左来。
@@ -926,11 +994,10 @@ window.__ModuleLoader__.load({
926
994
  role: 'dialog',
927
995
  'aria-label': 'quota',
928
996
  className: 'quota-pill-panel',
929
- style: {
930
- position: 'fixed',
931
- left: (anchorRef.current ? anchorRef.current.getBoundingClientRect().left : 0),
932
- bottom: (anchorRef.current ? window.innerHeight - anchorRef.current.getBoundingClientRect().top + 8 : 0),
933
- },
997
+ style: panelStyle
998
+ ? { position: 'fixed', ...panelStyle }
999
+ // 第一帧还没算好位置:先藏着,否则会先画在错误的地方再跳一下
1000
+ : { position: 'fixed', left: 0, top: 0, visibility: 'hidden' },
934
1001
  },
935
1002
  h('div', {
936
1003
  className: 'quota-pill-tabs',
@@ -1176,8 +1243,38 @@ window.__ModuleLoader__.load({
1176
1243
  return Math.abs(credits.total - credits.remain) > 1e-9 || Math.abs(pct - 100) > 0.01;
1177
1244
  }
1178
1245
 
1246
+ /**
1247
+ * 计算面板该放在哪,纯函数(不碰 DOM,便于测试)。
1248
+ *
1249
+ * 面板固定在视口里,不跟着触发器「贴」到哪算哪:
1250
+ * · 水平:优先对齐触发器左边,再夹进视口,宁可左移也不切边
1251
+ * · 垂直:上方放得下就向上展开,放不下就翻到触发器下方
1252
+ * · 都不够高时返回 maxHeight,让面板内部滚动,绝不溢出视口
1253
+ *
1254
+ * @param anchor 触发器的 getBoundingClientRect 形状 {left, top, bottom}
1255
+ * @param viewport {width, height}
1256
+ * @param panelHeight 面板实测高度,0 表示还没量到
1257
+ */
1258
+ function computePanelPlacement(anchor, viewport, panelHeight, gap, edge, maxWidth) {
1259
+ const vw = viewport.width;
1260
+ const vh = viewport.height;
1261
+ const width = Math.min(maxWidth, vw - edge * 2);
1262
+ const left = Math.max(edge, Math.min(anchor.left, vw - width - edge));
1263
+
1264
+ const spaceAbove = anchor.top - gap - edge;
1265
+ const spaceBelow = vh - anchor.bottom - gap - edge;
1266
+ const up = panelHeight === 0
1267
+ ? spaceAbove >= spaceBelow
1268
+ : (spaceAbove >= panelHeight || spaceAbove >= spaceBelow);
1269
+
1270
+ const base = { left, width, overflowY: 'auto' };
1271
+ return up
1272
+ ? { ...base, bottom: Math.max(edge, vh - anchor.top + gap), maxHeight: Math.max(140, spaceAbove), placement: 'up' }
1273
+ : { ...base, top: Math.max(edge, anchor.bottom + gap), maxHeight: Math.max(140, spaceBelow), placement: 'down' };
1274
+ }
1275
+
1179
1276
  function WindowRow(props) {
1180
- const { label, pct, reset, credits, lang, stale } = props;
1277
+ const { label, pct, reset, credits, lang, stale, money: isMoney } = props;
1181
1278
  const isNum = typeof pct === 'number' && Number.isFinite(pct);
1182
1279
  const tweened = useTween(isNum ? pct : null, 450);
1183
1280
  // 首帧 tween 还没拿到起点时 shown 是 null;这时按目标值渲染静态读数,
@@ -1208,7 +1305,7 @@ window.__ModuleLoader__.load({
1208
1305
  ? `${fmtAmount(credits.remain)} / ${fmtAmount(credits.total)}`
1209
1306
  + (credits.unit ? ' ' + String(credits.unit) : '')
1210
1307
  : undefined,
1211
- }, fmtCompact(credits.remain))
1308
+ }, isMoney ? money(credits.remain, credits.unit) : fmtCompact(credits.remain))
1212
1309
  : isNum
1213
1310
  ? h('span', {
1214
1311
  className: 'quota-pill-win-pct',
@@ -1220,7 +1317,7 @@ window.__ModuleLoader__.load({
1220
1317
  style: { color: remainingColor(shown) },
1221
1318
  }, typeof shown === 'number' ? `${shown.toFixed(shown < 10 ? 1 : 0)}%` : '')
1222
1319
  : null,
1223
- showCredits && unitLabel(credits.unit, lang)
1320
+ showCredits && !isMoney && unitLabel(credits.unit, lang)
1224
1321
  ? h('span', { className: 'quota-pill-win-unit' }, unitLabel(credits.unit, lang))
1225
1322
  : null,
1226
1323
 
@@ -1240,7 +1337,7 @@ window.__ModuleLoader__.load({
1240
1337
  h('div', { className: 'quota-pill-group-head' }, name),
1241
1338
  // 组内行 key 带组名,避免不同 provider 的同名窗口(都是 5h/weekly)
1242
1339
  // 被 React 当成同一个组件实例,导致 tween 跨 provider 插值。
1243
- entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale })),
1340
+ entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale, money: e.money })),
1244
1341
  );
1245
1342
  }
1246
1343
 
@@ -1315,7 +1412,7 @@ window.__ModuleLoader__.load({
1315
1412
  // key 带 provider:换 provider 时强制重挂载,让 tween 从「空」开始,
1316
1413
  // 而不是把 A 的 5h 平滑滑到 B 的 5h——那是两个不同账户的额度,
1317
1414
  // 中间的数字不代表任何真实状态。
1318
- entries.map((e) => h(WindowRow, { key: `${providerName}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale })),
1415
+ entries.map((e) => h(WindowRow, { key: `${providerName}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale, money: e.money })),
1319
1416
  ),
1320
1417
  h('dl', { className: 'quota-pill-stats' },
1321
1418
  data.account ? statRow(pick(lang, 'account'), String(data.account)) : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hystudio/dsh-quota-pill",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Quota pill for DeepSeek Harness: remaining quota for every LLM subscription you have, in one collapsible pill",