@hystudio/dsh-quota-pill 0.2.4 → 0.2.5

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 +78 -5
  2. package/package.json +1 -1
package/client.js CHANGED
@@ -595,6 +595,50 @@ window.__ModuleLoader__.load({
595
595
  const panelRef = React.useRef(null);
596
596
  const prevRunning = React.useRef(running);
597
597
 
598
+ // 面板位置:固定在视口内,而不是跟着触发器「贴」到哪算哪。
599
+ //
600
+ // 旧写法直接取触发器的 left,面板 340px 宽,窄窗口时右边就被切掉了;
601
+ // bottom 固定向上展开,窗口矮时上面同样被切。这里改成:
602
+ // · 水平:优先对齐触发器左边,再夹到视口内
603
+ // · 垂直:上方放得下就向上,放不下就翻到触发器下面
604
+ // · 两侧都塞不满时给 maxHeight,让面板自己滚动而不是溢出
605
+ const [panelStyle, setPanelStyle] = React.useState(null);
606
+ const GAP = 8; // 与触发器的间距
607
+ const EDGE = 12; // 距视口边缘的最小留白
608
+ const PANEL_MAX_W = 340;
609
+
610
+ /** 把面板夹进视口:水平贴触发器左边但不出界,垂直放不下就翻到下方。 */
611
+ const placePanel = React.useCallback(() => {
612
+ const el = anchorRef.current;
613
+ if (!el) return;
614
+ const r = el.getBoundingClientRect();
615
+ setPanelStyle(computePanelPlacement(
616
+ { left: r.left, top: r.top, bottom: r.bottom },
617
+ {
618
+ width: window.innerWidth || document.documentElement.clientWidth,
619
+ height: window.innerHeight || document.documentElement.clientHeight,
620
+ },
621
+ panelRef.current ? panelRef.current.offsetHeight : 0,
622
+ GAP, EDGE, PANEL_MAX_W,
623
+ ));
624
+ }, []);
625
+
626
+ React.useEffect(() => {
627
+ if (!open) { setPanelStyle(null); return undefined; }
628
+ placePanel();
629
+ // 挂载后量到真实高度再算一次,翻转判定才准
630
+ const raf = requestAnimationFrame(placePanel);
631
+ const onViewportChange = () => placePanel();
632
+ window.addEventListener('resize', onViewportChange);
633
+ // capture: 工具条自身可能滚动,滚动会改变触发器的位置
634
+ window.addEventListener('scroll', onViewportChange, true);
635
+ return () => {
636
+ cancelAnimationFrame(raf);
637
+ window.removeEventListener('resize', onViewportChange);
638
+ window.removeEventListener('scroll', onViewportChange, true);
639
+ };
640
+ }, [open, placePanel]);
641
+
598
642
  // 面板页签:默认「当前」(本会话在用的 provider),可切到「全部」。
599
643
  const [tab, setTab] = React.useState('current');
600
644
  // 切换动画从哪一侧滑入:'right' = 新内容自右来(往前进),'left' = 自左来。
@@ -926,11 +970,10 @@ window.__ModuleLoader__.load({
926
970
  role: 'dialog',
927
971
  'aria-label': 'quota',
928
972
  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
- },
973
+ style: panelStyle
974
+ ? { position: 'fixed', ...panelStyle }
975
+ // 第一帧还没算好位置:先藏着,否则会先画在错误的地方再跳一下
976
+ : { position: 'fixed', left: 0, top: 0, visibility: 'hidden' },
934
977
  },
935
978
  h('div', {
936
979
  className: 'quota-pill-tabs',
@@ -1176,6 +1219,36 @@ window.__ModuleLoader__.load({
1176
1219
  return Math.abs(credits.total - credits.remain) > 1e-9 || Math.abs(pct - 100) > 0.01;
1177
1220
  }
1178
1221
 
1222
+ /**
1223
+ * 计算面板该放在哪,纯函数(不碰 DOM,便于测试)。
1224
+ *
1225
+ * 面板固定在视口里,不跟着触发器「贴」到哪算哪:
1226
+ * · 水平:优先对齐触发器左边,再夹进视口,宁可左移也不切边
1227
+ * · 垂直:上方放得下就向上展开,放不下就翻到触发器下方
1228
+ * · 都不够高时返回 maxHeight,让面板内部滚动,绝不溢出视口
1229
+ *
1230
+ * @param anchor 触发器的 getBoundingClientRect 形状 {left, top, bottom}
1231
+ * @param viewport {width, height}
1232
+ * @param panelHeight 面板实测高度,0 表示还没量到
1233
+ */
1234
+ function computePanelPlacement(anchor, viewport, panelHeight, gap, edge, maxWidth) {
1235
+ const vw = viewport.width;
1236
+ const vh = viewport.height;
1237
+ const width = Math.min(maxWidth, vw - edge * 2);
1238
+ const left = Math.max(edge, Math.min(anchor.left, vw - width - edge));
1239
+
1240
+ const spaceAbove = anchor.top - gap - edge;
1241
+ const spaceBelow = vh - anchor.bottom - gap - edge;
1242
+ const up = panelHeight === 0
1243
+ ? spaceAbove >= spaceBelow
1244
+ : (spaceAbove >= panelHeight || spaceAbove >= spaceBelow);
1245
+
1246
+ const base = { left, width, overflowY: 'auto' };
1247
+ return up
1248
+ ? { ...base, bottom: Math.max(edge, vh - anchor.top + gap), maxHeight: Math.max(140, spaceAbove), placement: 'up' }
1249
+ : { ...base, top: Math.max(edge, anchor.bottom + gap), maxHeight: Math.max(140, spaceBelow), placement: 'down' };
1250
+ }
1251
+
1179
1252
  function WindowRow(props) {
1180
1253
  const { label, pct, reset, credits, lang, stale } = props;
1181
1254
  const isNum = typeof pct === 'number' && Number.isFinite(pct);
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.5",
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",