@restregular/dsh-powerup 0.2.2 → 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 +237 -106
  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.2';
47
+ const VERSION = '0.2.3';
48
48
  const MENU_GAP = 8;
49
49
  const MENU_WIDTH = 248;
50
50
 
@@ -698,6 +698,9 @@
698
698
  .dsh-powerup-update-toggle .chevron{display:inline-flex;color:var(--dsw-alias-label-tertiary);transition:transform .16s ease}
699
699
  .dsh-powerup-update-toggle[aria-expanded=true] .chevron{transform:rotate(180deg);color:var(--dsw-alias-label-secondary)}
700
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}
701
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}
702
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}
703
706
  .dsh-powerup-update-version>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -1502,6 +1505,33 @@
1502
1505
  for (const button of [...buttons]) if (button.isConnected !== true) buttons.delete(button);
1503
1506
  }
1504
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
+
1505
1535
  /**
1506
1536
  * The Plugins-page update control. It injects one icon button into each
1507
1537
  * third-party card of the built-in "Installed plugins" group, then owns
@@ -1516,7 +1546,10 @@
1516
1546
  const abortRef = useRef(null);
1517
1547
  const buttonsRef = useRef(new Set());
1518
1548
  const inputRef = useRef(null);
1549
+ const versionRef = useRef(null);
1550
+ const versionPanelRef = useRef(null);
1519
1551
  const registryRef = useRef(null);
1552
+ const registryPanelRef = useRef(null);
1520
1553
  const versionSeq = useRef(0);
1521
1554
  const tRef = useRef((key, params) => translateCurrent(key, params));
1522
1555
  tRef.current = (key, params) => translateCurrent(key, params);
@@ -1548,6 +1581,17 @@
1548
1581
  setView(null);
1549
1582
  }, [view]);
1550
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
+
1551
1595
  const loadVersions = useCallback(async (token, name, choice, choices, resetVersion) => {
1552
1596
  const t = tRef.current;
1553
1597
  const seq = versionSeq.current + 1;
@@ -1606,8 +1650,10 @@
1606
1650
  versionsError: null,
1607
1651
  versionsRegistry: null,
1608
1652
  version: '',
1609
- listOpen: true,
1653
+ listOpen: false,
1654
+ listPopover: null,
1610
1655
  registryOpen: false,
1656
+ registryPopover: null,
1611
1657
  error: null,
1612
1658
  formError: null,
1613
1659
  log: '',
@@ -1700,19 +1746,18 @@
1700
1746
  }
1701
1747
  }, [view === null ? null : view.phase]);
1702
1748
 
1703
- // Escape closes the innermost popover first: registry menu, then the
1704
- // version list, then the dialog itself.
1749
+ // Escape closes the innermost floating panel first, then the dialog.
1705
1750
  useEffect(() => {
1706
1751
  if (view === null) return undefined;
1707
1752
  const onKeyDown = (event) => {
1708
1753
  if (event.key !== 'Escape') return;
1709
1754
  event.stopPropagation();
1710
1755
  if (view.registryOpen === true) {
1711
- setView((current) => current === null ? current : { ...current, registryOpen: false });
1756
+ setView((current) => current === null ? current : { ...current, registryOpen: false, registryPopover: null });
1712
1757
  return;
1713
1758
  }
1714
1759
  if (view.phase === 'choose' && view.listOpen === true) {
1715
- setView((current) => current === null ? current : { ...current, listOpen: false });
1760
+ setView((current) => current === null ? current : { ...current, listOpen: false, listPopover: null });
1716
1761
  return;
1717
1762
  }
1718
1763
  close();
@@ -1721,17 +1766,42 @@
1721
1766
  return () => document.removeEventListener('keydown', onKeyDown, true);
1722
1767
  }, [view, close]);
1723
1768
 
1724
- // A click outside the custom registry select closes only its menu.
1769
+ // Clicking outside the open floating panel and its trigger dismisses it.
1725
1770
  useEffect(() => {
1726
- if (view === null || view.registryOpen !== true) return undefined;
1771
+ if (view === null || (view.registryOpen !== true && view.listOpen !== true)) return undefined;
1727
1772
  const onPointerDown = (event) => {
1728
- const root = registryRef.current;
1729
- if (root !== null && event.target instanceof Node && root.contains(event.target)) return;
1730
- 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();
1731
1779
  };
1732
1780
  document.addEventListener('pointerdown', onPointerDown, true);
1733
1781
  return () => document.removeEventListener('pointerdown', onPointerDown, true);
1734
- }, [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]);
1735
1805
 
1736
1806
  // Show pnpm output while an installation runs.
1737
1807
  useEffect(() => {
@@ -1756,7 +1826,7 @@
1756
1826
 
1757
1827
  const chooseVersion = (version) => {
1758
1828
  if (tokenRef.current === null) return;
1759
- 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 });
1760
1830
  };
1761
1831
 
1762
1832
  const chooseRegistry = (key) => {
@@ -1766,10 +1836,48 @@
1766
1836
  if (current === null) return;
1767
1837
  const choice = current.choices.find((item) => item.key === key);
1768
1838
  if (choice === undefined) return;
1769
- 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 });
1770
1840
  loadVersions(token, current.name, choice, current.choices, true);
1771
1841
  };
1772
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
+
1773
1881
  const startUpdate = async () => {
1774
1882
  const token = tokenRef.current;
1775
1883
  const current = view;
@@ -1932,6 +2040,61 @@
1932
2040
 
1933
2041
  const currentChoice = view.choices.find((item) => item.key === view.registryKey) ?? view.choices[0];
1934
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
+
1935
2098
  const body = (() => {
1936
2099
  if (view.phase === 'loading') {
1937
2100
  return h('div', { className: 'dsh-powerup-update-status' },
@@ -1946,59 +2109,6 @@
1946
2109
  return notice('error', h(WarnIcon, { size: 15 }), t('update.unsupportedTitle'), view.error ?? t('update.notRemovable'));
1947
2110
  }
1948
2111
  if (view.phase === 'choose') {
1949
- const rows = (() => {
1950
- if (view.versionsState === 'loading') {
1951
- return [h('div', { key: 'state', className: 'dsh-powerup-update-list-state' },
1952
- h('div', { className: 'dsh-powerup-update-status' },
1953
- h('span', { className: 'dsh-powerup-update-spinner', 'aria-hidden': 'true' }),
1954
- t('update.loadingVersions'),
1955
- ),
1956
- )];
1957
- }
1958
- if (view.versionsState === 'error') {
1959
- return [h('div', { key: 'state', className: 'dsh-powerup-update-list-state' },
1960
- notice('error', h(WarnIcon, { size: 14 }), null, t('update.versionsFailed', { message: view.versionsError ?? '' })),
1961
- h('button', {
1962
- type: 'button',
1963
- className: 'dsh-powerup-update-ghost',
1964
- onClick: () => loadVersions(tokenRef.current, view.name, currentChoice, view.choices, false),
1965
- }, t('update.retry')),
1966
- )];
1967
- }
1968
- if ((view.versions ?? []).length === 0) {
1969
- return [h('div', { key: 'state', className: 'dsh-powerup-update-list-state' },
1970
- h('p', { className: 'dsh-powerup-update-note' }, t('update.versionsEmpty')),
1971
- )];
1972
- }
1973
- return [
1974
- h('div', { key: 'head', className: 'dsh-powerup-update-list-head' },
1975
- h('span', null, t('update.versionsCount', { count: view.versions.length })),
1976
- view.versionsRegistry === null || view.versionsRegistry === undefined
1977
- ? null
1978
- : h('span', { title: view.versionsRegistry.hint }, view.versionsRegistry.hint),
1979
- ),
1980
- ...view.versions.map((version) => {
1981
- const tagKind = versionTagKind(version, view.currentVersion, view.latest);
1982
- return h('button', {
1983
- key: version,
1984
- type: 'button',
1985
- role: 'option',
1986
- className: 'dsh-powerup-update-version',
1987
- 'aria-selected': version === view.version ? 'true' : 'false',
1988
- 'data-current': version === view.currentVersion ? '' : undefined,
1989
- onClick: () => chooseVersion(version),
1990
- },
1991
- h('span', null, version),
1992
- h('span', { className: 'dsh-powerup-update-trailing' },
1993
- tagKind === null ? null : h('span', {
1994
- className: `dsh-powerup-update-tag${tagKind === 'latest' ? '' : ' is-current'}`,
1995
- }, t(tagKind === 'current-latest' ? 'update.currentAndLatest' : tagKind === 'current' ? 'update.currentTag' : 'update.latest')),
1996
- version === view.version ? h('span', { className: 'dsh-powerup-update-check' }, h(CheckIcon, { size: 13 })) : null,
1997
- ),
1998
- );
1999
- }),
2000
- ];
2001
- })();
2002
2112
  return [
2003
2113
  h('div', { key: 'summary', className: 'dsh-powerup-update-summary' },
2004
2114
  h('div', { className: 'dsh-powerup-update-now' },
@@ -2010,7 +2120,7 @@
2010
2120
  ),
2011
2121
  h('div', { key: 'version', className: 'dsh-powerup-update-field' },
2012
2122
  h('label', { className: 'dsh-powerup-update-label', htmlFor: 'dsh-powerup-update-version' }, t('update.versionLabel')),
2013
- h('div', { className: 'dsh-powerup-update-row' },
2123
+ h('div', { className: 'dsh-powerup-update-row', ref: versionRef },
2014
2124
  h('input', {
2015
2125
  id: 'dsh-powerup-update-version',
2016
2126
  ref: inputRef,
@@ -2030,7 +2140,7 @@
2030
2140
  className: 'dsh-powerup-update-toggle',
2031
2141
  'aria-expanded': showVersions ? 'true' : 'false',
2032
2142
  'aria-controls': 'dsh-powerup-update-versions',
2033
- onClick: () => setView((current) => current === null ? current : { ...current, listOpen: !(current.listOpen === true) }),
2143
+ onClick: toggleVersionList,
2034
2144
  },
2035
2145
  showVersions ? t('update.hideVersions') : t('update.chooseVersion'),
2036
2146
  h('span', { className: 'chevron' }, h(ChevronDownIcon, { size: 12 })),
@@ -2040,7 +2150,6 @@
2040
2150
  ? null
2041
2151
  : notice('error', h(WarnIcon, { size: 14 }), null, view.formError),
2042
2152
  ),
2043
- showVersions ? h('div', { key: 'versions', id: 'dsh-powerup-update-versions', className: 'dsh-powerup-update-list', role: 'listbox', 'aria-label': t('update.versionLabel') }, rows) : null,
2044
2153
  h('div', { key: 'registry', className: 'dsh-powerup-update-field' },
2045
2154
  h('span', { className: 'dsh-powerup-update-label' }, t('update.registry')),
2046
2155
  h('div', { className: 'dsh-powerup-update-select', ref: registryRef },
@@ -2049,27 +2158,12 @@
2049
2158
  className: 'dsh-powerup-update-select-trigger',
2050
2159
  'aria-haspopup': 'listbox',
2051
2160
  'aria-expanded': view.registryOpen === true ? 'true' : 'false',
2052
- onClick: () => setView((current) => current === null ? current : { ...current, registryOpen: !(current.registryOpen === true) }),
2161
+ onClick: toggleRegistry,
2053
2162
  },
2054
2163
  h('span', { className: 'dsh-powerup-update-select-value' }, currentChoice?.label ?? ''),
2055
2164
  h('span', { className: 'dsh-powerup-update-select-host' }, currentChoice?.hint ?? ''),
2056
2165
  h('span', { className: 'chevron' }, h(ChevronDownIcon, { size: 12 })),
2057
2166
  ),
2058
- view.registryOpen === true ? h('div', { className: 'dsh-powerup-update-select-menu', role: 'listbox', 'aria-label': t('update.registry') },
2059
- view.choices.map((choice) => h('button', {
2060
- key: choice.key,
2061
- type: 'button',
2062
- role: 'option',
2063
- className: 'dsh-powerup-update-option',
2064
- 'aria-selected': choice.key === view.registryKey ? 'true' : 'false',
2065
- onClick: () => chooseRegistry(choice.key),
2066
- },
2067
- h('span', { className: 'dsh-powerup-update-option-copy' },
2068
- h('span', { className: 'dsh-powerup-update-option-title' }, choice.label),
2069
- h('span', { className: 'dsh-powerup-update-option-host' }, choice.hint),
2070
- ),
2071
- choice.key === view.registryKey ? h('span', { className: 'dsh-powerup-update-check' }, h(CheckIcon, { size: 13 })) : null,
2072
- ))) : null,
2073
2167
  ),
2074
2168
  view.versionsRegistry === null || view.versionsRegistry === undefined
2075
2169
  ? null
@@ -2152,28 +2246,64 @@
2152
2246
  return null;
2153
2247
  })();
2154
2248
 
2155
- return h('div', {
2156
- className: 'dsh-powerup-update-mask',
2157
- onMouseDown: (event) => {
2158
- 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
+ },
2159
2257
  },
2160
- },
2161
- h('div', {
2162
- className: 'dsh-powerup-update-dialog',
2163
- role: 'dialog',
2164
- 'aria-modal': 'true',
2165
- 'aria-labelledby': 'dsh-powerup-update-title',
2166
- },
2167
- h('div', { className: 'dsh-powerup-update-head' },
2168
- h('div', { className: 'dsh-powerup-update-heading' },
2169
- h('h2', { id: 'dsh-powerup-update-title' }, t('update.title')),
2170
- 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,
2171
2270
  ),
2172
- canClose ? h('button', { type: 'button', className: 'dsh-powerup-update-close', 'aria-label': t('update.close'), onClick: close }, '×') : null,
2173
- ),
2174
- h('div', { className: 'dsh-powerup-update-body' }, body),
2175
- footer.length === 0 ? null : h('div', { className: 'dsh-powerup-update-foot' }, footer),
2176
- ),
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,
2177
2307
  );
2178
2308
  }
2179
2309
 
@@ -2548,6 +2678,7 @@
2548
2678
  PluginUpdateOverlay,
2549
2679
  ensureUpdateButtons,
2550
2680
  updateButtonLabel,
2681
+ popoverPlacement,
2551
2682
  rowKeyParts,
2552
2683
  resolveForkSeq,
2553
2684
  markdownQuote,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@restregular/dsh-powerup",
3
- "version": "0.2.2",
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",