@iasiv5/dsh-quota-watch 0.1.32 → 0.1.33

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/lib/client.js CHANGED
@@ -205,6 +205,14 @@ var FETCH_TIMEOUT_MS = 15e3;
205
205
  var FALLBACK_SIZE = { width: 38, height: 26 };
206
206
  var ZERO_INSETS2 = { left: 0, right: 0, top: 0, bottom: 0 };
207
207
  var CARD_MIN_WIDTH = 160;
208
+ var CARD_SLOT_NAME = "sidebar.footer.action";
209
+ function loadReact() {
210
+ try {
211
+ return typeof require === "function" ? require("react") : null;
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
208
216
  var COPY = {
209
217
  zh: {
210
218
  title: "Token\u989D\u5EA6",
@@ -658,7 +666,10 @@ function mountQuotaCard({
658
666
  outcomeNoticeMs = 5e3,
659
667
  // Shell-takeover sentinel (2026-10-13 ds-bot): inject `ctx.slots` to arm it.
660
668
  // Injectable so DOM tests and exotic hosts keep the legacy behavior.
661
- slots = null
669
+ slots = null,
670
+ // Host React for the slot-first sidebar seat; default resolves through the
671
+ // module-loader factory and stays null under node --test / DOM tests.
672
+ react = loadReact()
662
673
  } = {}) {
663
674
  if (!doc?.body || typeof fetchImpl !== "function") return () => {
664
675
  };
@@ -1458,10 +1469,14 @@ function mountQuotaCard({
1458
1469
  event.preventDefault();
1459
1470
  onSurfaceClick();
1460
1471
  };
1461
- const hasCardMount = () => Boolean(footArea(doc));
1472
+ const hasCardMount = () => slotMode ? slotParent !== null : Boolean(footArea(doc));
1462
1473
  let currentFoot = null;
1463
1474
  let footTooNarrow = false;
1464
1475
  let noDataHidden = false;
1476
+ let slotMode = false;
1477
+ let slotParent = null;
1478
+ let slotDispose = null;
1479
+ let anchorRetryTimer = null;
1465
1480
  let shellTakeover = false;
1466
1481
  const syncShellTakeover = () => {
1467
1482
  try {
@@ -1473,7 +1488,8 @@ function mountQuotaCard({
1473
1488
  }
1474
1489
  };
1475
1490
  const syncFootNarrowness = () => {
1476
- footTooNarrow = currentFoot !== null && currentFoot.clientWidth > 0 && currentFoot.clientWidth < CARD_MIN_WIDTH;
1491
+ const seat = slotParent ?? currentFoot;
1492
+ footTooNarrow = seat !== null && seat.clientWidth > 0 && seat.clientWidth < CARD_MIN_WIDTH;
1477
1493
  };
1478
1494
  const syncCardVisibility = () => {
1479
1495
  syncFootNarrowness();
@@ -1560,6 +1576,7 @@ function mountQuotaCard({
1560
1576
  }
1561
1577
  };
1562
1578
  const place = () => {
1579
+ if (slotMode) return;
1563
1580
  const foot = footArea(doc);
1564
1581
  if (!foot) return;
1565
1582
  if (container.parentElement !== foot || foot.firstElementChild !== container) {
@@ -1596,7 +1613,71 @@ function mountQuotaCard({
1596
1613
  if (!batchTouchesSeatDiscovery(records) && (seatValid() || noLiveFoot)) return;
1597
1614
  schedulePlace();
1598
1615
  });
1599
- observer.observe(doc.body, { childList: true, subtree: true });
1616
+ if (!slotMode) observer.observe(doc.body, { childList: true, subtree: true });
1617
+ if (slots && typeof slots.inject === "function" && typeof slots.register === "function" && react && typeof react.createElement === "function") {
1618
+ try {
1619
+ const SLOT_SEAT_ID = "quota-watch-card";
1620
+ const CardSeat = (props) => {
1621
+ void props;
1622
+ return react.createElement("div", {
1623
+ ref: (el) => {
1624
+ if (!slotMode) return;
1625
+ slotParent = el;
1626
+ if (el) {
1627
+ if (container.parentElement !== el) el.appendChild(container);
1628
+ footResizeObserver?.observe(el);
1629
+ } else {
1630
+ footResizeObserver?.disconnect();
1631
+ }
1632
+ },
1633
+ "data-dsh-quota-watch-slot": ""
1634
+ });
1635
+ };
1636
+ slotDispose = slots.inject(CARD_SLOT_NAME, () => slots.register(
1637
+ { name: CARD_SLOT_NAME, id: SLOT_SEAT_ID },
1638
+ CardSeat
1639
+ ));
1640
+ slotMode = true;
1641
+ syncCardVisibility();
1642
+ win.setTimeout(() => {
1643
+ if (disposed || !slotMode || slotParent) return;
1644
+ try {
1645
+ win.console?.debug?.("[dsh-quota-watch] slot seat: host never rendered the slot mount \u2014 falling back to legacy anchor seat");
1646
+ } catch {
1647
+ }
1648
+ slotMode = false;
1649
+ slotParent = null;
1650
+ slotDispose?.();
1651
+ slotDispose = null;
1652
+ schedulePlace();
1653
+ syncCardVisibility();
1654
+ }, 5e3);
1655
+ } catch (error) {
1656
+ try {
1657
+ win.console?.debug?.(`[dsh-quota-watch] slot seat unavailable (${error?.message ?? error}) \u2014 using legacy anchor seat`);
1658
+ } catch {
1659
+ }
1660
+ }
1661
+ } else {
1662
+ try {
1663
+ win.console?.debug?.("[dsh-quota-watch] slot seat: slots facade or host React unavailable \u2014 using legacy anchor seat");
1664
+ } catch {
1665
+ }
1666
+ }
1667
+ if (!slotMode) {
1668
+ let anchorMisses = 0;
1669
+ anchorRetryTimer = win.setInterval(() => {
1670
+ if (disposed || seatValid()) return;
1671
+ schedulePlace();
1672
+ anchorMisses += 1;
1673
+ if (anchorMisses === 4) {
1674
+ try {
1675
+ win.console?.warn?.("[dsh-quota-watch] sidebar anchor (sidebarCol/footArea) not found \u2014 sidebar card not mounted; capsule unaffected");
1676
+ } catch {
1677
+ }
1678
+ }
1679
+ }, 15e3);
1680
+ }
1600
1681
  const langObserver = typeof win.MutationObserver === "function" ? new win.MutationObserver(() => {
1601
1682
  if (!disposed) renderAll();
1602
1683
  }) : null;
@@ -1758,6 +1839,9 @@ function mountQuotaCard({
1758
1839
  stopPolling();
1759
1840
  if (outcomeNoticeTimer !== void 0) win.clearTimeout(outcomeNoticeTimer);
1760
1841
  outcomeNoticeTimer = void 0;
1842
+ if (anchorRetryTimer !== null) win.clearInterval(anchorRetryTimer);
1843
+ anchorRetryTimer = null;
1844
+ slotDispose?.();
1761
1845
  observer.disconnect();
1762
1846
  langObserver?.disconnect();
1763
1847
  if (offShellTakeover) offShellTakeover();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-quota-watch",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
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
@@ -23,6 +23,23 @@ const ZERO_INSETS = { left: 0, right: 0, top: 0, bottom: 0 }
23
23
  // rails, squeezed desktop layouts) — hide it and let the capsule carry the
24
24
  // entry. clientWidth 0 (no layout yet, jsdom) counts as "unknown", not narrow.
25
25
  const CARD_MIN_WIDTH = 160
26
+ // Official sidebar foot action slot (dsh-client-ui-sidebar contract). The
27
+ // slot-first seat mounts the card through it; the sidebarCol/footArea
28
+ // substring discovery stays as the legacy-host fallback.
29
+ const CARD_SLOT_NAME = 'sidebar.footer.action'
30
+
31
+ /** Host React for the sidebar.footer.action slot seat (0.2.x shells render
32
+ * registrant components with their own React). The module-loader factory
33
+ * passes `require`; a direct ESM import (node --test, DOM tests) has none, in
34
+ * which case slot mode simply stays off and the physical-insert fallback runs
35
+ * exactly as before. Never throws. */
36
+ function loadReact() {
37
+ try {
38
+ return typeof require === 'function' ? require('react') : null
39
+ } catch {
40
+ return null
41
+ }
42
+ }
26
43
 
27
44
  const COPY = {
28
45
  zh: {
@@ -564,6 +581,9 @@ export function mountQuotaCard({
564
581
  // Shell-takeover sentinel (2026-10-13 ds-bot): inject `ctx.slots` to arm it.
565
582
  // Injectable so DOM tests and exotic hosts keep the legacy behavior.
566
583
  slots = null,
584
+ // Host React for the slot-first sidebar seat; default resolves through the
585
+ // module-loader factory and stays null under node --test / DOM tests.
586
+ react = loadReact(),
567
587
  } = {}) {
568
588
  if (!doc?.body || typeof fetchImpl !== 'function') return () => {}
569
589
  if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {}
@@ -1551,7 +1571,7 @@ export function mountQuotaCard({
1551
1571
  event.preventDefault()
1552
1572
  onSurfaceClick()
1553
1573
  }
1554
- const hasCardMount = () => Boolean(footArea(doc))
1574
+ const hasCardMount = () => (slotMode ? slotParent !== null : Boolean(footArea(doc)))
1555
1575
 
1556
1576
  // Cross-profile narrow-container detection: a measurably narrow foot would
1557
1577
  // clip the card into unreadable text regardless of HOW the host signals its
@@ -1560,6 +1580,13 @@ export function mountQuotaCard({
1560
1580
  let currentFoot = null
1561
1581
  let footTooNarrow = false
1562
1582
  let noDataHidden = false
1583
+ // Slot-first seat state (2026-10-19): when the official sidebar.footer.action
1584
+ // slot carries the card, the CSS-substring discovery below is bypassed
1585
+ // entirely; it remains the fallback for hosts without the slot.
1586
+ let slotMode = false
1587
+ let slotParent = null
1588
+ let slotDispose = null
1589
+ let anchorRetryTimer = null
1563
1590
  // The sidebar foot belongs to whoever registers sidebar.workspaces
1564
1591
  // (ui-sidebar doc: "the foot is the sidebar.workspaces registrant's"). The
1565
1592
  // official workspace tree registers at default priority 0 (ui-workspace); a
@@ -1578,7 +1605,8 @@ export function mountQuotaCard({
1578
1605
  }
1579
1606
  }
1580
1607
  const syncFootNarrowness = () => {
1581
- footTooNarrow = currentFoot !== null && currentFoot.clientWidth > 0 && currentFoot.clientWidth < CARD_MIN_WIDTH
1608
+ const seat = slotParent ?? currentFoot
1609
+ footTooNarrow = seat !== null && seat.clientWidth > 0 && seat.clientWidth < CARD_MIN_WIDTH
1582
1610
  }
1583
1611
  const syncCardVisibility = () => {
1584
1612
  syncFootNarrowness()
@@ -1686,6 +1714,7 @@ export function mountQuotaCard({
1686
1714
  }
1687
1715
 
1688
1716
  const place = () => {
1717
+ if (slotMode) return
1689
1718
  const foot = footArea(doc)
1690
1719
  if (!foot) return
1691
1720
  // Claim the first slot of the sidebar footer so the card sits above the
@@ -1748,7 +1777,88 @@ export function mountQuotaCard({
1748
1777
  if (!batchTouchesSeatDiscovery(records) && (seatValid() || noLiveFoot)) return
1749
1778
  schedulePlace()
1750
1779
  })
1751
- observer.observe(doc.body, { childList: true, subtree: true })
1780
+ if (!slotMode) observer.observe(doc.body, { childList: true, subtree: true })
1781
+ // --- Slot-first seat (2026-10-19): 0.2.x shells render the official
1782
+ // sidebar.footer.action list slot (dsh-client-ui-sidebar contract: kind
1783
+ // 'list', owner passes only { wide }). Mounting through it removes the
1784
+ // sidebarCol/footArea CSS-substring anchor dependency entirely — the anchor
1785
+ // discovery below remains purely as the legacy-host fallback and engages
1786
+ // only when the slot is unavailable or not declared by the shell.
1787
+ if (slots && typeof slots.inject === 'function' && typeof slots.register === 'function'
1788
+ && react && typeof react.createElement === 'function') {
1789
+ try {
1790
+ // No declaration probe: the plugin facade's spec() proved unreliable on
1791
+ // 0.2.1-rc.2 (returns undefined even for shell-declared slots, while
1792
+ // register()+render works — the settings.section precedent). The mount
1793
+ // confirmation below is the real gate: if the shell never renders the
1794
+ // slot, the card falls back to the physical anchor seat within seconds.
1795
+ const SLOT_SEAT_ID = 'quota-watch-card'
1796
+ const CardSeat = (props) => {
1797
+ // Owner share is { wide }; the width guard keeps measuring instead
1798
+ // of trusting host collapse signals (they proved host-specific, see
1799
+ // the syncCardVisibility note).
1800
+ void props
1801
+ return react.createElement('div', {
1802
+ ref: (el) => {
1803
+ if (!slotMode) return // a late render after fallback engaged: ignore, container is physically seated
1804
+ slotParent = el
1805
+ if (el) {
1806
+ // Adopt the imperative card container: React owns this mount
1807
+ // div but none of its (foreign) children, so the manual
1808
+ // append is the standard escape hatch and reconciles cleanly.
1809
+ if (container.parentElement !== el) el.appendChild(container)
1810
+ footResizeObserver?.observe(el)
1811
+ } else {
1812
+ footResizeObserver?.disconnect()
1813
+ }
1814
+ },
1815
+ 'data-dsh-quota-watch-slot': '',
1816
+ })
1817
+ }
1818
+ slotDispose = slots.inject(CARD_SLOT_NAME, () => slots.register(
1819
+ { name: CARD_SLOT_NAME, id: SLOT_SEAT_ID },
1820
+ CardSeat,
1821
+ ))
1822
+ // Optimistic engage; the mount confirmation below is the truth test —
1823
+ // if the shell never renders the slot (declared but region absent),
1824
+ // the card would silently vanish, so fall back to the physical seat.
1825
+ slotMode = true
1826
+ syncCardVisibility()
1827
+ win.setTimeout(() => {
1828
+ if (disposed || !slotMode || slotParent) return
1829
+ try { win.console?.debug?.('[dsh-quota-watch] slot seat: host never rendered the slot mount — falling back to legacy anchor seat') } catch { /* no console */ }
1830
+ slotMode = false
1831
+ slotParent = null
1832
+ slotDispose?.()
1833
+ slotDispose = null
1834
+ schedulePlace()
1835
+ syncCardVisibility()
1836
+ }, 5_000)
1837
+ } catch (error) {
1838
+ try { win.console?.debug?.(`[dsh-quota-watch] slot seat unavailable (${error?.message ?? error}) — using legacy anchor seat`) } catch { /* no console */ }
1839
+ }
1840
+ } else {
1841
+ try { win.console?.debug?.('[dsh-quota-watch] slot seat: slots facade or host React unavailable — using legacy anchor seat') } catch { /* no console */ }
1842
+ }
1843
+ if (!slotMode) {
1844
+ // Blind-spot re-probe (fallback mode only): the seat-gate observer can
1845
+ // only schedule place() for batches carrying the anchor markers, so if
1846
+ // the shell ever renames those CSS-modules classes the gate would never
1847
+ // re-arm after a missed seat. A slow unseated-only retry bounds that
1848
+ // drift to one interval, and the warn-once makes the state diagnosable
1849
+ // instead of silent. Self-idles once seated; cleared on dispose.
1850
+ let anchorMisses = 0
1851
+ anchorRetryTimer = win.setInterval(() => {
1852
+ if (disposed || seatValid()) return
1853
+ schedulePlace()
1854
+ anchorMisses += 1
1855
+ if (anchorMisses === 4) {
1856
+ try {
1857
+ win.console?.warn?.('[dsh-quota-watch] sidebar anchor (sidebarCol/footArea) not found — sidebar card not mounted; capsule unaffected')
1858
+ } catch { /* console unavailable */ }
1859
+ }
1860
+ }, 15_000)
1861
+ }
1752
1862
  // The DSH locale feature mirrors every language switch onto <html lang>;
1753
1863
  // re-render on that flip so the UI follows 设置 → 语言 without a reload.
1754
1864
  // A no-op flip (same value) never fires, so this costs nothing at rest.
@@ -1943,6 +2053,9 @@ export function mountQuotaCard({
1943
2053
  stopPolling()
1944
2054
  if (outcomeNoticeTimer !== undefined) win.clearTimeout(outcomeNoticeTimer)
1945
2055
  outcomeNoticeTimer = undefined
2056
+ if (anchorRetryTimer !== null) win.clearInterval(anchorRetryTimer)
2057
+ anchorRetryTimer = null
2058
+ slotDispose?.()
1946
2059
  observer.disconnect()
1947
2060
  langObserver?.disconnect()
1948
2061
  if (offShellTakeover) offShellTakeover()