@restregular/dsh-powerup 0.2.1 → 0.2.3

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 (3) hide show
  1. package/README.md +3 -1
  2. package/client.js +265 -102
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -87,7 +87,9 @@ unload the dialog mid-flow. Use **Add plugin** with the new package version.
87
87
  `--dsw-elevation-prominent`, and the primary button uses
88
88
  `--dsw-alias-button-primary-fill` / `-hover`, so the dialog follows both
89
89
  themes instead of inventing its own palette. The registry picker and version
90
- list are custom controls built from `--dsw-alias-*` tokens.
90
+ list are custom controls built from `--dsw-alias-*` tokens; both open as
91
+ fixed-position popovers anchored to their triggers, so they never grow the
92
+ dialog or get clipped by its scroll area.
91
93
 
92
94
  ## Development
93
95
 
package/client.js CHANGED
@@ -44,7 +44,7 @@
44
44
 
45
45
  const NS = 'dsh-powerup';
46
46
  const SETTINGS_KEY = 'dsh-powerup:settings:v1';
47
- const VERSION = '0.2.1';
47
+ const VERSION = '0.2.3';
48
48
  const MENU_GAP = 8;
49
49
  const MENU_WIDTH = 248;
50
50
 
@@ -126,6 +126,8 @@
126
126
  'update.versionsFailed': '读取版本失败:{message}',
127
127
  'update.versionsEmpty': '安装源没有返回可用版本',
128
128
  'update.latest': '最新',
129
+ 'update.currentTag': '当前',
130
+ 'update.currentAndLatest': '当前 · 最新',
129
131
  'update.retry': '重试',
130
132
  'update.registry': '安装源',
131
133
  'update.registryDefault': '默认安装源',
@@ -246,6 +248,8 @@
246
248
  'update.versionsFailed': 'Could not read versions: {message}',
247
249
  'update.versionsEmpty': 'The registry returned no versions',
248
250
  'update.latest': 'latest',
251
+ 'update.currentTag': 'Current',
252
+ 'update.currentAndLatest': 'Current · latest',
249
253
  'update.retry': 'Retry',
250
254
  'update.registry': 'Registry',
251
255
  'update.registryDefault': 'Default registry',
@@ -694,12 +698,18 @@
694
698
  .dsh-powerup-update-toggle .chevron{display:inline-flex;color:var(--dsw-alias-label-tertiary);transition:transform .16s ease}
695
699
  .dsh-powerup-update-toggle[aria-expanded=true] .chevron{transform:rotate(180deg);color:var(--dsw-alias-label-secondary)}
696
700
  .dsh-powerup-update-list{display:flex;flex-direction:column;max-height:240px;overflow-y:auto;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg,10px);background:var(--dsw-alias-bg-layer-1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}
701
+ .dsh-powerup-update-popover{position:fixed;z-index:1300;box-sizing:border-box;overflow-y:auto;pointer-events:auto;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg,10px);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}
702
+ .dsh-powerup-update-popover.dsh-powerup-update-list{border-radius:var(--dsw-radius-lg,10px)}
703
+ .dsh-powerup-update-popover.dsh-powerup-update-select-menu{display:flex;flex-direction:column;padding:4px}
697
704
  .dsh-powerup-update-list-head{position:sticky;top:0;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 12px;border-bottom:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
698
705
  .dsh-powerup-update-version{display:flex;align-items:center;gap:8px;width:100%;min-height:34px;padding:0 12px;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;line-height:18px;text-align:left;cursor:pointer;font-variant-numeric:tabular-nums}
706
+ .dsh-powerup-update-version>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
699
707
  .dsh-powerup-update-version:hover{background:var(--dsw-alias-interactive-bg-hover)}
700
708
  .dsh-powerup-update-version[aria-selected=true]{background:var(--dsw-alias-interactive-bg-active)}
701
- .dsh-powerup-update-trailing{display:inline-flex;align-items:center;gap:8px;margin-left:auto}
702
- .dsh-powerup-update-tag{padding:0 6px;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:16px}
709
+ .dsh-powerup-update-version[data-current]{box-shadow:inset 2px 0 0 var(--dsw-alias-state-business-primary)}
710
+ .dsh-powerup-update-trailing{display:inline-flex;flex:none;align-items:center;gap:8px;margin-left:auto;white-space:nowrap}
711
+ .dsh-powerup-update-tag{padding:0 6px;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:16px;white-space:nowrap}
712
+ .dsh-powerup-update-tag.is-current{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 38%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 8%,transparent);color:var(--dsw-alias-state-business-primary)}
703
713
  .dsh-powerup-update-check{display:inline-flex;color:var(--dsw-alias-state-business-primary)}
704
714
  .dsh-powerup-update-status{display:flex;align-items:center;gap:10px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
705
715
  .dsh-powerup-update-spinner{flex:none;width:14px;height:14px;border:2px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 24%,transparent);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:dsh-powerup-spin .8s linear infinite}
@@ -1342,6 +1352,24 @@
1342
1352
  return list.sort(compareVersionsDesc);
1343
1353
  }
1344
1354
 
1355
+ /**
1356
+ * Which status a version row carries. The installed and the latest
1357
+ * version can be the same; that row gets one combined tag instead of two
1358
+ * chips sharing the trailing space.
1359
+ * @param {string} version
1360
+ * @param {string | null | undefined} current
1361
+ * @param {string | null | undefined} latest
1362
+ * @returns {'current-latest' | 'current' | 'latest' | null}
1363
+ */
1364
+ function versionTagKind(version, current, latest) {
1365
+ const isCurrent = typeof current === 'string' && current !== '' && version === current;
1366
+ const isLatest = typeof latest === 'string' && latest !== '' && version === latest;
1367
+ if (isCurrent && isLatest) return 'current-latest';
1368
+ if (isCurrent) return 'current';
1369
+ if (isLatest) return 'latest';
1370
+ return null;
1371
+ }
1372
+
1345
1373
  /**
1346
1374
  * Read the versions the registry publishes for one package. The
1347
1375
  * abbreviated metadata keeps the response small; both npm and npmmirror
@@ -1477,6 +1505,33 @@
1477
1505
  for (const button of [...buttons]) if (button.isConnected !== true) buttons.delete(button);
1478
1506
  }
1479
1507
 
1508
+ /**
1509
+ * Place a floating panel under an anchor, flipping above when the space
1510
+ * below is short, and clamp it to the viewport.
1511
+ * @param {{ getBoundingClientRect: () => DOMRect }} anchor
1512
+ * @param {number} minWidth
1513
+ * @param {number} preferredHeight
1514
+ * @returns {{ left: number, width: number, top?: number, bottom?: number, maxHeight: number }}
1515
+ */
1516
+ function popoverPlacement(anchor, minWidth, preferredHeight) {
1517
+ const rect = anchor.getBoundingClientRect();
1518
+ const gap = 6;
1519
+ const margin = 12;
1520
+ const viewportWidth = typeof window === 'undefined' ? 1280 : window.innerWidth;
1521
+ const viewportHeight = typeof window === 'undefined' ? 900 : window.innerHeight;
1522
+ const width = Math.min(Math.max(rect.width, minWidth), Math.max(200, viewportWidth - margin * 2));
1523
+ const left = Math.min(Math.max(margin, rect.left), Math.max(margin, viewportWidth - width - margin));
1524
+ const spaceBelow = viewportHeight - rect.bottom - margin - gap;
1525
+ const spaceAbove = rect.top - margin - gap;
1526
+ const above = spaceBelow < Math.min(preferredHeight, 180) && spaceAbove > spaceBelow;
1527
+ return {
1528
+ left,
1529
+ width,
1530
+ ...(above ? { bottom: viewportHeight - rect.top + gap } : { top: rect.bottom + gap }),
1531
+ maxHeight: Math.max(120, Math.min(preferredHeight, above ? spaceAbove : spaceBelow)),
1532
+ };
1533
+ }
1534
+
1480
1535
  /**
1481
1536
  * The Plugins-page update control. It injects one icon button into each
1482
1537
  * third-party card of the built-in "Installed plugins" group, then owns
@@ -1491,7 +1546,10 @@
1491
1546
  const abortRef = useRef(null);
1492
1547
  const buttonsRef = useRef(new Set());
1493
1548
  const inputRef = useRef(null);
1549
+ const versionRef = useRef(null);
1550
+ const versionPanelRef = useRef(null);
1494
1551
  const registryRef = useRef(null);
1552
+ const registryPanelRef = useRef(null);
1495
1553
  const versionSeq = useRef(0);
1496
1554
  const tRef = useRef((key, params) => translateCurrent(key, params));
1497
1555
  tRef.current = (key, params) => translateCurrent(key, params);
@@ -1523,6 +1581,17 @@
1523
1581
  setView(null);
1524
1582
  }, [view]);
1525
1583
 
1584
+ /** Dismiss both floating panels without touching the dialog. */
1585
+ const closePopovers = useCallback(() => {
1586
+ setView((current) => current === null ? current : {
1587
+ ...current,
1588
+ listOpen: false,
1589
+ listPopover: null,
1590
+ registryOpen: false,
1591
+ registryPopover: null,
1592
+ });
1593
+ }, []);
1594
+
1526
1595
  const loadVersions = useCallback(async (token, name, choice, choices, resetVersion) => {
1527
1596
  const t = tRef.current;
1528
1597
  const seq = versionSeq.current + 1;
@@ -1581,8 +1650,10 @@
1581
1650
  versionsError: null,
1582
1651
  versionsRegistry: null,
1583
1652
  version: '',
1584
- listOpen: true,
1653
+ listOpen: false,
1654
+ listPopover: null,
1585
1655
  registryOpen: false,
1656
+ registryPopover: null,
1586
1657
  error: null,
1587
1658
  formError: null,
1588
1659
  log: '',
@@ -1675,19 +1746,18 @@
1675
1746
  }
1676
1747
  }, [view === null ? null : view.phase]);
1677
1748
 
1678
- // Escape closes the innermost popover first: registry menu, then the
1679
- // version list, then the dialog itself.
1749
+ // Escape closes the innermost floating panel first, then the dialog.
1680
1750
  useEffect(() => {
1681
1751
  if (view === null) return undefined;
1682
1752
  const onKeyDown = (event) => {
1683
1753
  if (event.key !== 'Escape') return;
1684
1754
  event.stopPropagation();
1685
1755
  if (view.registryOpen === true) {
1686
- setView((current) => current === null ? current : { ...current, registryOpen: false });
1756
+ setView((current) => current === null ? current : { ...current, registryOpen: false, registryPopover: null });
1687
1757
  return;
1688
1758
  }
1689
1759
  if (view.phase === 'choose' && view.listOpen === true) {
1690
- setView((current) => current === null ? current : { ...current, listOpen: false });
1760
+ setView((current) => current === null ? current : { ...current, listOpen: false, listPopover: null });
1691
1761
  return;
1692
1762
  }
1693
1763
  close();
@@ -1696,17 +1766,42 @@
1696
1766
  return () => document.removeEventListener('keydown', onKeyDown, true);
1697
1767
  }, [view, close]);
1698
1768
 
1699
- // A click outside the custom registry select closes only its menu.
1769
+ // Clicking outside the open floating panel and its trigger dismisses it.
1700
1770
  useEffect(() => {
1701
- if (view === null || view.registryOpen !== true) return undefined;
1771
+ if (view === null || (view.registryOpen !== true && view.listOpen !== true)) return undefined;
1702
1772
  const onPointerDown = (event) => {
1703
- const root = registryRef.current;
1704
- if (root !== null && event.target instanceof Node && root.contains(event.target)) return;
1705
- setView((current) => current === null ? current : { ...current, registryOpen: false });
1773
+ const target = event.target;
1774
+ if (!(target instanceof Node)) return;
1775
+ const inside = [versionRef.current, versionPanelRef.current, registryRef.current, registryPanelRef.current]
1776
+ .some((root) => root !== null && root.contains(target));
1777
+ if (inside) return;
1778
+ closePopovers();
1706
1779
  };
1707
1780
  document.addEventListener('pointerdown', onPointerDown, true);
1708
1781
  return () => document.removeEventListener('pointerdown', onPointerDown, true);
1709
- }, [view === null ? null : view.registryOpen]);
1782
+ }, [view === null ? null : `${String(view.registryOpen)}:${String(view.listOpen)}`, closePopovers]);
1783
+
1784
+ // Scrolling the dialog or the page would leave a fixed panel behind;
1785
+ // dismiss it instead. Scrolling inside the panel itself is allowed.
1786
+ useEffect(() => {
1787
+ if (view === null || (view.registryOpen !== true && view.listOpen !== true)) return undefined;
1788
+ const onScroll = (event) => {
1789
+ const target = event.target;
1790
+ if (target instanceof Node) {
1791
+ const insidePanel = [versionPanelRef.current, registryPanelRef.current]
1792
+ .some((panel) => panel !== null && panel.contains(target));
1793
+ if (insidePanel) return;
1794
+ }
1795
+ closePopovers();
1796
+ };
1797
+ const onResize = () => closePopovers();
1798
+ document.addEventListener('scroll', onScroll, true);
1799
+ window.addEventListener('resize', onResize);
1800
+ return () => {
1801
+ document.removeEventListener('scroll', onScroll, true);
1802
+ window.removeEventListener('resize', onResize);
1803
+ };
1804
+ }, [view === null ? null : `${String(view.registryOpen)}:${String(view.listOpen)}`, closePopovers]);
1710
1805
 
1711
1806
  // Show pnpm output while an installation runs.
1712
1807
  useEffect(() => {
@@ -1731,7 +1826,7 @@
1731
1826
 
1732
1827
  const chooseVersion = (version) => {
1733
1828
  if (tokenRef.current === null) return;
1734
- setView((current) => current === null ? current : { ...current, version, listOpen: false, formError: null });
1829
+ setView((current) => current === null ? current : { ...current, version, listOpen: false, listPopover: null, formError: null });
1735
1830
  };
1736
1831
 
1737
1832
  const chooseRegistry = (key) => {
@@ -1741,10 +1836,48 @@
1741
1836
  if (current === null) return;
1742
1837
  const choice = current.choices.find((item) => item.key === key);
1743
1838
  if (choice === undefined) return;
1744
- setView((state) => state === null ? state : { ...state, registryKey: key, registryOpen: false, formError: null });
1839
+ setView((state) => state === null ? state : { ...state, registryKey: key, registryOpen: false, registryPopover: null, formError: null });
1745
1840
  loadVersions(token, current.name, choice, current.choices, true);
1746
1841
  };
1747
1842
 
1843
+ /** Open or close the floating version list under the input row. */
1844
+ const toggleVersionList = () => {
1845
+ const current = view;
1846
+ if (current === null || current.phase !== 'choose') return;
1847
+ if (current.listOpen === true) {
1848
+ setView((state) => state === null ? state : { ...state, listOpen: false, listPopover: null });
1849
+ return;
1850
+ }
1851
+ const anchor = versionRef.current;
1852
+ const popover = anchor === null ? null : popoverPlacement(anchor, 280, 260);
1853
+ setView((state) => state === null ? state : {
1854
+ ...state,
1855
+ listOpen: true,
1856
+ listPopover: popover,
1857
+ registryOpen: false,
1858
+ registryPopover: null,
1859
+ });
1860
+ };
1861
+
1862
+ /** Open or close the floating registry menu under its trigger. */
1863
+ const toggleRegistry = () => {
1864
+ const current = view;
1865
+ if (current === null || current.phase !== 'choose') return;
1866
+ if (current.registryOpen === true) {
1867
+ setView((state) => state === null ? state : { ...state, registryOpen: false, registryPopover: null });
1868
+ return;
1869
+ }
1870
+ const anchor = registryRef.current;
1871
+ const popover = anchor === null ? null : popoverPlacement(anchor, 280, 220);
1872
+ setView((state) => state === null ? state : {
1873
+ ...state,
1874
+ registryOpen: true,
1875
+ registryPopover: popover,
1876
+ listOpen: false,
1877
+ listPopover: null,
1878
+ });
1879
+ };
1880
+
1748
1881
  const startUpdate = async () => {
1749
1882
  const token = tokenRef.current;
1750
1883
  const current = view;
@@ -1907,6 +2040,61 @@
1907
2040
 
1908
2041
  const currentChoice = view.choices.find((item) => item.key === view.registryKey) ?? view.choices[0];
1909
2042
 
2043
+ /** Rows of the floating version list, including its loading/error states. */
2044
+ const versionRows = (() => {
2045
+ if (view.versionsState === 'loading') {
2046
+ return [h('div', { key: 'state', className: 'dsh-powerup-update-list-state' },
2047
+ h('div', { className: 'dsh-powerup-update-status' },
2048
+ h('span', { className: 'dsh-powerup-update-spinner', 'aria-hidden': 'true' }),
2049
+ t('update.loadingVersions'),
2050
+ ),
2051
+ )];
2052
+ }
2053
+ if (view.versionsState === 'error') {
2054
+ return [h('div', { key: 'state', className: 'dsh-powerup-update-list-state' },
2055
+ notice('error', h(WarnIcon, { size: 14 }), null, t('update.versionsFailed', { message: view.versionsError ?? '' })),
2056
+ h('button', {
2057
+ type: 'button',
2058
+ className: 'dsh-powerup-update-ghost',
2059
+ onClick: () => loadVersions(tokenRef.current, view.name, currentChoice, view.choices, false),
2060
+ }, t('update.retry')),
2061
+ )];
2062
+ }
2063
+ if ((view.versions ?? []).length === 0) {
2064
+ return [h('div', { key: 'state', className: 'dsh-powerup-update-list-state' },
2065
+ h('p', { className: 'dsh-powerup-update-note' }, t('update.versionsEmpty')),
2066
+ )];
2067
+ }
2068
+ return [
2069
+ h('div', { key: 'head', className: 'dsh-powerup-update-list-head' },
2070
+ h('span', null, t('update.versionsCount', { count: view.versions.length })),
2071
+ view.versionsRegistry === null || view.versionsRegistry === undefined
2072
+ ? null
2073
+ : h('span', { title: view.versionsRegistry.hint }, view.versionsRegistry.hint),
2074
+ ),
2075
+ ...view.versions.map((version) => {
2076
+ const tagKind = versionTagKind(version, view.currentVersion, view.latest);
2077
+ return h('button', {
2078
+ key: version,
2079
+ type: 'button',
2080
+ role: 'option',
2081
+ className: 'dsh-powerup-update-version',
2082
+ 'aria-selected': version === view.version ? 'true' : 'false',
2083
+ 'data-current': version === view.currentVersion ? '' : undefined,
2084
+ onClick: () => chooseVersion(version),
2085
+ },
2086
+ h('span', null, version),
2087
+ h('span', { className: 'dsh-powerup-update-trailing' },
2088
+ tagKind === null ? null : h('span', {
2089
+ className: `dsh-powerup-update-tag${tagKind === 'latest' ? '' : ' is-current'}`,
2090
+ }, t(tagKind === 'current-latest' ? 'update.currentAndLatest' : tagKind === 'current' ? 'update.currentTag' : 'update.latest')),
2091
+ version === view.version ? h('span', { className: 'dsh-powerup-update-check' }, h(CheckIcon, { size: 13 })) : null,
2092
+ ),
2093
+ );
2094
+ }),
2095
+ ];
2096
+ })();
2097
+
1910
2098
  const body = (() => {
1911
2099
  if (view.phase === 'loading') {
1912
2100
  return h('div', { className: 'dsh-powerup-update-status' },
@@ -1921,53 +2109,6 @@
1921
2109
  return notice('error', h(WarnIcon, { size: 15 }), t('update.unsupportedTitle'), view.error ?? t('update.notRemovable'));
1922
2110
  }
1923
2111
  if (view.phase === 'choose') {
1924
- const rows = (() => {
1925
- if (view.versionsState === 'loading') {
1926
- return [h('div', { key: 'state', className: 'dsh-powerup-update-list-state' },
1927
- h('div', { className: 'dsh-powerup-update-status' },
1928
- h('span', { className: 'dsh-powerup-update-spinner', 'aria-hidden': 'true' }),
1929
- t('update.loadingVersions'),
1930
- ),
1931
- )];
1932
- }
1933
- if (view.versionsState === 'error') {
1934
- return [h('div', { key: 'state', className: 'dsh-powerup-update-list-state' },
1935
- notice('error', h(WarnIcon, { size: 14 }), null, t('update.versionsFailed', { message: view.versionsError ?? '' })),
1936
- h('button', {
1937
- type: 'button',
1938
- className: 'dsh-powerup-update-ghost',
1939
- onClick: () => loadVersions(tokenRef.current, view.name, currentChoice, view.choices, false),
1940
- }, t('update.retry')),
1941
- )];
1942
- }
1943
- if ((view.versions ?? []).length === 0) {
1944
- return [h('div', { key: 'state', className: 'dsh-powerup-update-list-state' },
1945
- h('p', { className: 'dsh-powerup-update-note' }, t('update.versionsEmpty')),
1946
- )];
1947
- }
1948
- return [
1949
- h('div', { key: 'head', className: 'dsh-powerup-update-list-head' },
1950
- h('span', null, t('update.versionsCount', { count: view.versions.length })),
1951
- view.versionsRegistry === null || view.versionsRegistry === undefined
1952
- ? null
1953
- : h('span', { title: view.versionsRegistry.hint }, view.versionsRegistry.hint),
1954
- ),
1955
- ...view.versions.map((version) => h('button', {
1956
- key: version,
1957
- type: 'button',
1958
- role: 'option',
1959
- className: 'dsh-powerup-update-version',
1960
- 'aria-selected': version === view.version ? 'true' : 'false',
1961
- onClick: () => chooseVersion(version),
1962
- },
1963
- h('span', null, version),
1964
- h('span', { className: 'dsh-powerup-update-trailing' },
1965
- version === view.latest ? h('span', { className: 'dsh-powerup-update-tag' }, t('update.latest')) : null,
1966
- version === view.version ? h('span', { className: 'dsh-powerup-update-check' }, h(CheckIcon, { size: 13 })) : null,
1967
- ),
1968
- )),
1969
- ];
1970
- })();
1971
2112
  return [
1972
2113
  h('div', { key: 'summary', className: 'dsh-powerup-update-summary' },
1973
2114
  h('div', { className: 'dsh-powerup-update-now' },
@@ -1979,7 +2120,7 @@
1979
2120
  ),
1980
2121
  h('div', { key: 'version', className: 'dsh-powerup-update-field' },
1981
2122
  h('label', { className: 'dsh-powerup-update-label', htmlFor: 'dsh-powerup-update-version' }, t('update.versionLabel')),
1982
- h('div', { className: 'dsh-powerup-update-row' },
2123
+ h('div', { className: 'dsh-powerup-update-row', ref: versionRef },
1983
2124
  h('input', {
1984
2125
  id: 'dsh-powerup-update-version',
1985
2126
  ref: inputRef,
@@ -1999,7 +2140,7 @@
1999
2140
  className: 'dsh-powerup-update-toggle',
2000
2141
  'aria-expanded': showVersions ? 'true' : 'false',
2001
2142
  'aria-controls': 'dsh-powerup-update-versions',
2002
- onClick: () => setView((current) => current === null ? current : { ...current, listOpen: !(current.listOpen === true) }),
2143
+ onClick: toggleVersionList,
2003
2144
  },
2004
2145
  showVersions ? t('update.hideVersions') : t('update.chooseVersion'),
2005
2146
  h('span', { className: 'chevron' }, h(ChevronDownIcon, { size: 12 })),
@@ -2009,7 +2150,6 @@
2009
2150
  ? null
2010
2151
  : notice('error', h(WarnIcon, { size: 14 }), null, view.formError),
2011
2152
  ),
2012
- showVersions ? h('div', { key: 'versions', id: 'dsh-powerup-update-versions', className: 'dsh-powerup-update-list', role: 'listbox', 'aria-label': t('update.versionLabel') }, rows) : null,
2013
2153
  h('div', { key: 'registry', className: 'dsh-powerup-update-field' },
2014
2154
  h('span', { className: 'dsh-powerup-update-label' }, t('update.registry')),
2015
2155
  h('div', { className: 'dsh-powerup-update-select', ref: registryRef },
@@ -2018,27 +2158,12 @@
2018
2158
  className: 'dsh-powerup-update-select-trigger',
2019
2159
  'aria-haspopup': 'listbox',
2020
2160
  'aria-expanded': view.registryOpen === true ? 'true' : 'false',
2021
- onClick: () => setView((current) => current === null ? current : { ...current, registryOpen: !(current.registryOpen === true) }),
2161
+ onClick: toggleRegistry,
2022
2162
  },
2023
2163
  h('span', { className: 'dsh-powerup-update-select-value' }, currentChoice?.label ?? ''),
2024
2164
  h('span', { className: 'dsh-powerup-update-select-host' }, currentChoice?.hint ?? ''),
2025
2165
  h('span', { className: 'chevron' }, h(ChevronDownIcon, { size: 12 })),
2026
2166
  ),
2027
- view.registryOpen === true ? h('div', { className: 'dsh-powerup-update-select-menu', role: 'listbox', 'aria-label': t('update.registry') },
2028
- view.choices.map((choice) => h('button', {
2029
- key: choice.key,
2030
- type: 'button',
2031
- role: 'option',
2032
- className: 'dsh-powerup-update-option',
2033
- 'aria-selected': choice.key === view.registryKey ? 'true' : 'false',
2034
- onClick: () => chooseRegistry(choice.key),
2035
- },
2036
- h('span', { className: 'dsh-powerup-update-option-copy' },
2037
- h('span', { className: 'dsh-powerup-update-option-title' }, choice.label),
2038
- h('span', { className: 'dsh-powerup-update-option-host' }, choice.hint),
2039
- ),
2040
- choice.key === view.registryKey ? h('span', { className: 'dsh-powerup-update-check' }, h(CheckIcon, { size: 13 })) : null,
2041
- ))) : null,
2042
2167
  ),
2043
2168
  view.versionsRegistry === null || view.versionsRegistry === undefined
2044
2169
  ? null
@@ -2121,28 +2246,64 @@
2121
2246
  return null;
2122
2247
  })();
2123
2248
 
2124
- return h('div', {
2125
- className: 'dsh-powerup-update-mask',
2126
- onMouseDown: (event) => {
2127
- if (event.target === event.currentTarget) close();
2249
+ const fallbackPopover = { left: 24, top: 120, width: 320, maxHeight: 260 };
2250
+
2251
+ return h(React.Fragment, null,
2252
+ h('div', {
2253
+ className: 'dsh-powerup-update-mask',
2254
+ onMouseDown: (event) => {
2255
+ if (event.target === event.currentTarget) close();
2256
+ },
2128
2257
  },
2129
- },
2130
- h('div', {
2131
- className: 'dsh-powerup-update-dialog',
2132
- role: 'dialog',
2133
- 'aria-modal': 'true',
2134
- 'aria-labelledby': 'dsh-powerup-update-title',
2135
- },
2136
- h('div', { className: 'dsh-powerup-update-head' },
2137
- h('div', { className: 'dsh-powerup-update-heading' },
2138
- h('h2', { id: 'dsh-powerup-update-title' }, t('update.title')),
2139
- h('p', { title }, title),
2258
+ h('div', {
2259
+ className: 'dsh-powerup-update-dialog',
2260
+ role: 'dialog',
2261
+ 'aria-modal': 'true',
2262
+ 'aria-labelledby': 'dsh-powerup-update-title',
2263
+ },
2264
+ h('div', { className: 'dsh-powerup-update-head' },
2265
+ h('div', { className: 'dsh-powerup-update-heading' },
2266
+ h('h2', { id: 'dsh-powerup-update-title' }, t('update.title')),
2267
+ h('p', { title }, title),
2268
+ ),
2269
+ canClose ? h('button', { type: 'button', className: 'dsh-powerup-update-close', 'aria-label': t('update.close'), onClick: close }, '×') : null,
2140
2270
  ),
2141
- canClose ? h('button', { type: 'button', className: 'dsh-powerup-update-close', 'aria-label': t('update.close'), onClick: close }, '×') : null,
2142
- ),
2143
- h('div', { className: 'dsh-powerup-update-body' }, body),
2144
- footer.length === 0 ? null : h('div', { className: 'dsh-powerup-update-foot' }, footer),
2145
- ),
2271
+ h('div', { className: 'dsh-powerup-update-body' }, body),
2272
+ footer.length === 0 ? null : h('div', { className: 'dsh-powerup-update-foot' }, footer),
2273
+ ),
2274
+ ),
2275
+ view.listOpen === true
2276
+ ? h('div', {
2277
+ id: 'dsh-powerup-update-versions',
2278
+ ref: versionPanelRef,
2279
+ className: 'dsh-powerup-update-popover dsh-powerup-update-list',
2280
+ role: 'listbox',
2281
+ 'aria-label': t('update.versionLabel'),
2282
+ style: view.listPopover ?? fallbackPopover,
2283
+ }, versionRows)
2284
+ : null,
2285
+ view.registryOpen === true
2286
+ ? h('div', {
2287
+ ref: registryPanelRef,
2288
+ className: 'dsh-powerup-update-popover dsh-powerup-update-select-menu',
2289
+ role: 'listbox',
2290
+ 'aria-label': t('update.registry'),
2291
+ style: view.registryPopover ?? fallbackPopover,
2292
+ }, view.choices.map((choice) => h('button', {
2293
+ key: choice.key,
2294
+ type: 'button',
2295
+ role: 'option',
2296
+ className: 'dsh-powerup-update-option',
2297
+ 'aria-selected': choice.key === view.registryKey ? 'true' : 'false',
2298
+ onClick: () => chooseRegistry(choice.key),
2299
+ },
2300
+ h('span', { className: 'dsh-powerup-update-option-copy' },
2301
+ h('span', { className: 'dsh-powerup-update-option-title' }, choice.label),
2302
+ h('span', { className: 'dsh-powerup-update-option-host' }, choice.hint),
2303
+ ),
2304
+ choice.key === view.registryKey ? h('span', { className: 'dsh-powerup-update-check' }, h(CheckIcon, { size: 13 })) : null,
2305
+ )))
2306
+ : null,
2146
2307
  );
2147
2308
  }
2148
2309
 
@@ -2517,6 +2678,7 @@
2517
2678
  PluginUpdateOverlay,
2518
2679
  ensureUpdateButtons,
2519
2680
  updateButtonLabel,
2681
+ popoverPlacement,
2520
2682
  rowKeyParts,
2521
2683
  resolveForkSeq,
2522
2684
  markdownQuote,
@@ -2524,6 +2686,7 @@
2524
2686
  buildRowItems,
2525
2687
  buildSessionItems,
2526
2688
  sortVersions,
2689
+ versionTagKind,
2527
2690
  compareVersionsDesc,
2528
2691
  buildRegistryChoices,
2529
2692
  pickRegistryChoice,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@restregular/dsh-powerup",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Power-Up — small, opt-in upgrades for DeepSeek Harness. Adds a right-click action menu to the Trajectory ledger and an update control for third-party plugins installed from the Plugins page, with version lookup, uninstall-and-reinstall through the Add plugin path, and an optional DSH restart. · 为 DeepSeek Harness 提供小型增强:在轨迹页面右键记录即可复制、引用、查看详情或从该记录 Fork;并在插件页为第三方已安装插件提供带版本查询的更新按钮,沿用「添加插件」的安装流程,并可重启 DSH 使更改生效。",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",