dsh-sidebar-overlay 0.1.0 → 0.1.1

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 +73 -12
  2. package/client.js +192 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,6 +14,25 @@
14
14
 
15
15
  ---
16
16
 
17
+ ## 更新日志
18
+
19
+ ### 0.1.1
20
+
21
+ - **新增顶栏「插件」按钮**(风车图标,38×38,与头像/新会话同排)。点一下切到
22
+ 官方「插件」面板并收起浮层,动作走 `ctx.layout.selectPanel('plugins')` ——
23
+ 与官方点 `sidebar.panellist` 里 plugins 那一行是同一条路径。
24
+ 只为该面板可用时才渲染(读 `sidebar.panellist` 登记表判断),不占官方座位;
25
+ `showPluginsPanel: false` 可关闭。
26
+ - **收起后抑制重开**:导航类操作收起浮层后,指针若仍停在热区内不会被立刻重新
27
+ 打开(离开热区一次即恢复),消除「点了按钮浮层又弹回来」的观感。
28
+ - 测试增至 463 项;新增断言经变异测试验证(刻意改坏实现后对应断言必然失败)。
29
+
30
+ ### 0.1.0
31
+
32
+ - 首次发布。
33
+
34
+ ---
35
+
17
36
  ## 为什么要重做
18
37
 
19
38
  旧方案走的是「复用官方侧边栏 DOM + CSS 浮层化」。那条路能工作,但有两个
@@ -142,6 +161,42 @@ const shellStore = { ...shellHandle, create: () => shellInstance }
142
161
  两样都取不到时(官方改了座位名、或相关插件未启用),个人信息按钮**不渲染** ——
143
162
  而不是渲染一个点了没反应的假按钮。
144
163
 
164
+ ### 插件按钮:顶栏右侧,点了打开「插件」页
165
+
166
+ 顶栏「新会话」右边那个风车方块按钮,点一下切到官方的「插件」主面板
167
+ (Plugin Manager 那一页),随后浮层自动收起。
168
+
169
+ 它跟官方的全局面板图标是**同一个目的地、同一条路径**:
170
+
171
+ ```js
172
+ ctx.layout.selectPanel('plugins') // 官方点 sidebar.panellist 的 plugins 行也是这句
173
+ ```
174
+
175
+ 所以插件里没有任何「打开插件页」的复刻逻辑,面板内容、页内导航、返回会话
176
+ 全都由官方负责(从插件页点任意会话即可回到对话:`uiWorkspace.openSession()`
177
+ 内部会 `selectPanel(null)`)。
178
+
179
+ 图标沿用官方 `sidebar.panellist` 里那一行的同一个图标
180
+ (`IconPluginPinwheelOutlineRegular`,风车),缺失时用内置同路径 SVG 兜底。
181
+
182
+ **为什么不去占 `sidebar.panellist` 座位**:那个座位的图标是由声明者用
183
+ `renderSlot('sidebar.panellist', …, { only: id })` 交出来的,第三方拿不到;
184
+ 而且浮层是自建 DOM,根本不渲染那个座位。去占它只会在官方侧边栏里多出一个
185
+ 重复按钮,浮层里仍然画不出来。所以本插件只把那个座位当**能力登记表**读:
186
+
187
+ ```js
188
+ slots.entriesOfSlot('sidebar.panellist').some(e => e.options.id === 'plugins')
189
+ ```
190
+
191
+ 有这一行 ⇒ main 里确实注册着这个面板 ⇒ 按钮才渲染。这一步是必须的:
192
+ `layout.selectPanel` 对未注册的 key 会**直接抛错**
193
+ (`main panel "plugins" is not registered`),少了它就是一个点了报错的假按钮。
194
+ `showPluginsPanel: false` 可以关掉这个按钮。
195
+
196
+ > 收起时会置一个「抑制重开」标记:点完按钮指针通常还停在卡片原来的位置,
197
+ > 而那里可能仍在热区内;不抑制的话鼠标一动浮层就弹回来,看起来像按钮没生效。
198
+ > 指针离开热区一次即恢复正常。
199
+
145
200
  ---
146
201
 
147
202
  ## 数据来源(官方公开契约)
@@ -212,15 +267,18 @@ staticModules: () => ({
212
267
 
213
268
  ## 功能清单
214
269
 
215
- **顶栏:个人信息 + 新会话**
216
- - 顶部一行两枚按钮:**左侧个人信息(头像圆钮)→ 右侧新会话**,同高(38px)居中对齐
270
+ **顶栏:个人信息 + 新会话 + 插件**
271
+ - 顶部一行按钮:**个人信息(头像圆钮)→ 新会话 → 插件(风车方块)**,同高(38px)居中对齐
217
272
  - 个人信息按钮:**点一下直接打开官方设置弹窗**,不弹账户菜单,
218
273
  所以没有「联系我们 / 退出登录」(详见下方「个人信息按钮」一节)。
219
274
  头像与昵称读官方同一份账户数据源,未登录时降级为内置人形图标
220
275
  - 新会话按钮:38px 高、`.5px` 描边、`button-elevated-fill` 底、字重 500,
221
276
  右侧带快捷键提示(`ShortcutKeys`),`aria-keyshortcuts` 同步暴露
222
- - 两者互不影响,可分别用 `showProfile` / `showNewSession` 关掉;
223
- 关掉一个后另一个自动占满整行
277
+ - 插件按钮:**点一下切到官方「插件」面板并收起浮层**,动作走
278
+ `ctx.layout.selectPanel('plugins')`;图标与官方 `sidebar.panellist` 的
279
+ plugins 行同源(详见下方「插件按钮」一节)
280
+ - 三者互不影响,可分别用 `showProfile` / `showNewSession` / `showPluginsPanel`
281
+ 关掉;关掉中间那个时其余两个仍成立
224
282
 
225
283
  **工作区**
226
284
  - 分组展示(按工作区 / 按工作区树 / 单列表,三态可切并持久化)
@@ -378,6 +436,7 @@ window.__dshFloatingSidebarOverlay.isOpen()
378
436
  archivedFilter: 'default' # 'default' | 'show' | 'only'
379
437
  showNewSession: true # 顶部「新会话」按钮
380
438
  showProfile: true # 顶部「个人信息」(官方账户入口)按钮
439
+ showPluginsPanel: true # 顶部「插件」面板按钮(面板未注册时不渲染)
381
440
  hotkey: true
382
441
  hotkeyBinding: { code: 'KeyL', modifiers: ['primary', 'alt'] }
383
442
  debug: false
@@ -568,8 +627,8 @@ max-width: var(--dsh-chat-content-width); width: 100%; margin: 0 auto;
568
627
  无需浏览器即可跑的五套(共 433 项),全部通过:
569
628
  ```bash
570
629
  node test.mjs # 纯函数 + CSS 契约 + token 白名单 + 账号/设置绑定(206 项)
571
- node smoke.mjs # 组件渲染烟雾(70 项,真实执行组件体)
572
- node apply-check.mjs # apply 级集成(115 项,Cordis + DOM 桩)
630
+ node smoke.mjs # 组件渲染烟雾(84 项,真实执行组件体)
631
+ node apply-check.mjs # apply 级集成(131 项,Cordis + DOM 桩)
573
632
  node layout-check.mjs # CSS 选择器 × 真实 DOM 的间距契约(28 项)
574
633
  node repro-hover-flicker.mjs # 鼠标触发闪烁的逐帧复现(14 项)
575
634
  ```
@@ -675,13 +734,15 @@ node e2e-profile.mjs # 个人信息按钮端到端(19 项:座位链
675
734
  `sidebar.session.row.leading`、`.hover`、`sidebar.panellist`、
676
735
  `sidebar.settings`、`sidebar.footer.action`)**不会**出现在本插件的浮层里。
677
736
  原因是 slot 的「声明即认领」规则,见上。
678
- 2. **不含全局面板图标**:那是 `sidebar.panellist` 座位,被官方独占,折叠态
679
- 浮层里因此没有「插件」等面板按钮。
680
- 两个例外,都是**只借用数据、不占用座位**:
737
+ 2. **只搬了一个全局面板**:`sidebar.panellist` 座位仍被官方独占,浮层里没有
738
+ 完整的「全局面板」列表 —— 只有**插件**这一个入口(顶栏右侧风车按钮)。
739
+ 三个例外,都是**只借用数据/能力、不占用座位**:
681
740
  - **个人信息按钮**:读 `settings.launcher` 条目的 `account` source 画头像,
682
741
  并从 `sidebar.settings` 条目的 store 调 `actions.open()` 打开设置弹窗
683
742
  (只保留「设置」这一件事,见上文「个人信息按钮」一节)。
684
- - 设置弹窗本体仍由官方 `sidebar.settings` 渲染,本插件只是把它**叫出来**。
743
+ - **插件按钮**:读 `sidebar.panellist` 的登记表判断面板是否可用,动作走
744
+ `ctx.layout.selectPanel('plugins')`(见上文「插件按钮」一节)。
745
+ - 设置弹窗、插件页本体仍由官方渲染,本插件只是把它们**叫出来**。
685
746
  3. **依赖官方 hook 名**:`useSessions` / `useSessionStatus` / `useWorkspaces` /
686
747
  `usePanelInfo` 来自官方 `provideRoot`,官方改名会失配(此时组件降级为空列表
687
748
  并在控制台告警,不会崩)。
@@ -710,8 +771,8 @@ node e2e-profile.mjs # 个人信息按钮端到端(19 项:座位链
710
771
  | `cordis.patch.yml` | bundle patch:`insert` 条目(id `sidebar-overlay`) |
711
772
  | `package.json` | `dsh.client` 声明:web 平台、立即加载 |
712
773
  | `test.mjs` | 纯函数 + CSS 契约 + token 白名单 + 账号/设置解析(206 项) |
713
- | `smoke.mjs` | 组件渲染烟雾(70 项,含顶栏排版与点击开设置) |
714
- | `apply-check.mjs` | apply 级集成测试(115 项) |
774
+ | `smoke.mjs` | 组件渲染烟雾(84 项,含顶栏排版、点击开设置与插件按钮) |
775
+ | `apply-check.mjs` | apply 级集成测试(131 项) |
715
776
  | `layout-check.mjs` | CSS 选择器 × 真实 DOM 的间距契约测试(28 项) |
716
777
  | `repro-hover-flicker.mjs` | 鼠标触发闪烁的逐帧复现(14 项) |
717
778
  | `visual-check.mjs` | headless Chrome 真实渲染几何验证(52 项,含顶栏几何 12 项) |
package/client.js CHANGED
@@ -94,6 +94,41 @@ window.__ModuleLoader__.load({
94
94
  )
95
95
  }
96
96
 
97
+ /**
98
+ * 内置「风车」图标(官方 `IconPluginPinwheelOutlineRegular` 的等价路径)。
99
+ *
100
+ * 「插件」面板按钮靠它兜底:primitives 缺失时 `iconOf` 返回空组件,
101
+ * 按钮就成了没有任何内容的圆壳,用户看不出那是什么。
102
+ */
103
+ function PluginGlyph({ size = 16 }) {
104
+ return h('svg', {
105
+ width: size,
106
+ height: size,
107
+ viewBox: '0 0 16 16',
108
+ fill: 'none',
109
+ 'aria-hidden': 'true',
110
+ focusable: 'false',
111
+ strokeWidth: 1,
112
+ },
113
+ h('path', {
114
+ d: 'M7.84457 5.06199C11.6605 4.93876 14.7962 6.14848 14.8484 7.76397C14.8875 8.97461 13.1838 10.0696 10.7215 10.5942',
115
+ stroke: 'currentColor',
116
+ }),
117
+ h('path', {
118
+ d: 'M5.12742 8.07731C5.00419 4.26138 6.21391 1.12568 7.8294 1.07351C9.04004 1.03441 10.135 2.73808 10.6596 5.20037',
119
+ stroke: 'currentColor',
120
+ }),
121
+ h('path', {
122
+ d: 'M8.02457 10.6802C4.20865 10.8034 1.07294 9.5937 1.02077 7.97821C0.981678 6.76758 2.68535 5.67262 5.14763 5.14798',
123
+ stroke: 'currentColor',
124
+ }),
125
+ h('path', {
126
+ d: 'M10.7476 7.89535C10.8708 11.7113 9.66109 14.847 8.0456 14.8991C6.83496 14.9382 5.74 13.2346 5.21536 10.7723',
127
+ stroke: 'currentColor',
128
+ }),
129
+ )
130
+ }
131
+
97
132
  /** 官方图标(缺失时为空组件)。 */
98
133
  const Icons = {
99
134
  search: iconOf('IconSearchOutlineRegular'),
@@ -111,6 +146,9 @@ window.__ModuleLoader__.load({
111
146
  /** 个人信息按钮:未登录 / 无头像时的兜底图标。
112
147
  * 优先官方 `IconUserOutlineMedium`,缺失时用内置同名规格的 SVG。 */
113
148
  user: P.IconUserOutlineMedium ?? UserGlyph,
149
+ /** 「插件」面板按钮:官方侧边栏 `sidebar.panellist` 的 plugins 行同一个图标
150
+ * (风车)。缺失时用内置同路径 SVG,保证按钮不会变成空壳。 */
151
+ plugins: P.IconPluginPinwheelOutlineRegular ?? PluginGlyph,
114
152
  edit: iconOf('IconEditOutlineRegular'),
115
153
  trash: iconOf('IconTrashOutlineRegular'),
116
154
  archive: iconOf('IconArchiveOutlineRegular'),
@@ -136,6 +174,8 @@ window.__ModuleLoader__.load({
136
174
  const inject = ['slots']
137
175
 
138
176
  const PANEL_ID = 'sidebar-overlay'
177
+ /** 官方「插件」main 面板的 key(Plugin Manager 占用,见 ui-sidebar 的 panellist 登记)。 */
178
+ const PANEL_PLUGINS_ID = 'plugins'
139
179
  /** 语言命名空间。 */
140
180
  const NS = 'floatingSidebarOverlay'
141
181
  /** 官方存储的默认工作区标题;展示时替换为本地化名称。 */
@@ -189,6 +229,8 @@ window.__ModuleLoader__.load({
189
229
  showNewSession: true,
190
230
  /** 是否显示卡片左上角的「个人信息」头像按钮(原侧边栏底部的账户入口)。 */
191
231
  showProfile: true,
232
+ /** 是否显示顶栏的「插件」面板按钮(等价于官方 sidebar.panellist 的 plugins 行)。 */
233
+ showPluginsPanel: true,
192
234
  /** 是否注册总开关快捷键。 */
193
235
  hotkey: true,
194
236
  /** 总开关快捷键。 */
@@ -1086,6 +1128,35 @@ window.__ModuleLoader__.load({
1086
1128
  /* 头像加载失败/未登录:图标按官方尺寸(16)落在槽内。 */
1087
1129
  [data-dsh-fso-profile-avatar] > svg { flex: none; }
1088
1130
 
1131
+ /* ── 「插件」面板按钮:官方 sidebar.panellist 的 plugins 行的浮层版 ──────
1132
+ * 官方那一行是宽 36 高 36 的方形、radius-md、hover/interactive-bg-hover、
1133
+ * 选中是同一底色 + label-primary(见 ui-sidebar 的 panelRow / panelActive)。
1134
+ * 这里与「个人信息」并排,取同高 38 与同描边,只在圆角上保留官方的方形
1135
+ * 语义(radius-md),一眼能看出「这个进面板、那个进设置」。 */
1136
+ [data-dsh-fso-panel-button] {
1137
+ box-sizing: border-box;
1138
+ flex: none;
1139
+ display: inline-flex;
1140
+ align-items: center;
1141
+ justify-content: center;
1142
+ width: 38px;
1143
+ height: 38px;
1144
+ padding: 0;
1145
+ border: .5px solid var(--dsw-alias-border-l3);
1146
+ border-radius: var(--dsw-radius-md);
1147
+ background: var(--dsw-alias-button-elevated-fill);
1148
+ color: var(--dsw-alias-label-primary);
1149
+ cursor: pointer;
1150
+ }
1151
+ [data-dsh-fso-panel-button]:hover { background: var(--dsw-alias-interactive-bg-hover); }
1152
+ [data-dsh-fso-panel-button]:focus-visible {
1153
+ outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
1154
+ outline-offset: -2px;
1155
+ }
1156
+ /* 选中态:官方 panelActive 用的就是 interactive-bg-hover,不另造颜色。 */
1157
+ [data-dsh-fso-panel-button][data-active] { background: var(--dsw-alias-interactive-bg-hover); }
1158
+ [data-dsh-fso-panel-button] > svg { flex: none; }
1159
+
1089
1160
  /* ── 区头:对齐官方 sectionHeader(36px)─────────────────────────────────
1090
1161
  * 官方在非 rail 态给 sectionHeader 加了 margin-top:2px; margin-right:-4px,
1091
1162
  * 让区头的操作按钮视觉上贴到列表右边线。 */
@@ -1702,6 +1773,10 @@ window.__ModuleLoader__.load({
1702
1773
  'profile.openSettings': '打开设置',
1703
1774
  'profile.signedIn': '已登录 DeepSeek',
1704
1775
  'profile.signedOut': '未登录',
1776
+ /* 「插件」面板按钮:与官方 sidebar 命名空间 `panels.label`(全局面板)
1777
+ * 同义,但这里只有一个面板,所以文案直接点名「插件」。 */
1778
+ 'panel.plugins': '插件',
1779
+ 'panel.failed': '无法打开插件面板',
1705
1780
  'session.untitled': '未命名',
1706
1781
  'search': '搜索会话',
1707
1782
  'search.sessions.aria': '搜索会话',
@@ -1829,6 +1904,8 @@ window.__ModuleLoader__.load({
1829
1904
  'profile.openSettings': 'Open settings',
1830
1905
  'profile.signedIn': 'Signed in to DeepSeek',
1831
1906
  'profile.signedOut': 'Signed out',
1907
+ 'panel.plugins': 'Plugins',
1908
+ 'panel.failed': 'Cannot open the Plugins panel',
1832
1909
  'session.untitled': 'Untitled',
1833
1910
  'search': 'Search sessions',
1834
1911
  'search.sessions.aria': 'Search sessions',
@@ -2914,6 +2991,31 @@ window.__ModuleLoader__.load({
2914
2991
  }
2915
2992
  }
2916
2993
 
2994
+ /**
2995
+ * 判断官方 `sidebar.panellist` 里是否登记了某个全局面板。
2996
+ *
2997
+ * 浮层是自建 DOM,**渲染不出**那个座位里的图标(图标由声明者
2998
+ * `renderSlot('sidebar.panellist', …, { only: id })` 交出,第三方拿不到)。
2999
+ * 所以这里只把它当**能力登记表**读:有这一行 ⇒ 对应的 main 面板确实
3000
+ * 注册着,`ctx.layout.selectPanel(id)` 不会抛错,按钮才画出来。
3001
+ *
3002
+ * 这条检查是必须的:`selectPanel` 对未注册的 key 会**直接 throw**
3003
+ * (`layout.selectPanel: main panel "…" is not registered`),
3004
+ * 没这一步就会得到一个「点了报错」的假按钮。
3005
+ *
3006
+ * @param {object} slots - `ctx.slots`
3007
+ * @param {string} id - 面板 id(如 `'plugins'`)。
3008
+ * @returns {boolean} 座位里是否存在该 id 的登记。
3009
+ */
3010
+ function hasPanelEntry(slots, id) {
3011
+ try {
3012
+ if (slots === undefined || slots === null || typeof slots.entriesOfSlot !== 'function') return false
3013
+ return slots.entriesOfSlot('sidebar.panellist').some(entry => entry?.options?.id === id)
3014
+ } catch {
3015
+ return false
3016
+ }
3017
+ }
3018
+
2917
3019
  /**
2918
3020
  * 个人信息按钮:浮层顶部、新会话按钮左边的账户入口。
2919
3021
  *
@@ -2964,6 +3066,34 @@ window.__ModuleLoader__.load({
2964
3066
  )
2965
3067
  }
2966
3068
 
3069
+ /**
3070
+ * 「插件」面板按钮:官方 `sidebar.panellist` 里 plugins 那一行的浮层版。
3071
+ *
3072
+ * 点击 = `ctx.layout.selectPanel('plugins')`,与官方点同一个图标**走同一条
3073
+ * 路径**,所以不必在这里复制任何「打开插件页」的逻辑。
3074
+ *
3075
+ * 选中态读 `usePanelInfo`(与本组件已在用的那一个 hook 同源),高亮用官方
3076
+ * `panelActive` 的同一个 token。
3077
+ *
3078
+ * @param {{usePanelInfo: Function, label: string, onOpen: Function}} props
3079
+ */
3080
+ function PanelButton({ usePanelInfo, label, onOpen }) {
3081
+ const active = typeof usePanelInfo === 'function'
3082
+ ? usePanelInfo(info => info.activePanelId === PANEL_PLUGINS_ID)
3083
+ : false
3084
+ return h(
3085
+ P.Tooltip ?? TooltipFallback,
3086
+ { label, side: 'bottom', delayMs: 500, children: h('button', {
3087
+ type: 'button',
3088
+ 'data-dsh-fso-panel-button': '',
3089
+ ...(active === true ? { 'data-active': '' } : {}),
3090
+ 'aria-label': label,
3091
+ 'aria-current': active === true ? 'page' : undefined,
3092
+ onClick: () => { onOpen() },
3093
+ }, h(Icons.plugins, { size: 16 })) },
3094
+ )
3095
+ }
3096
+
2967
3097
  /**
2968
3098
  * 浮动侧边栏主体。
2969
3099
  *
@@ -2980,6 +3110,7 @@ window.__ModuleLoader__.load({
2980
3110
  renameWorkspace, deleteWorkspace, insertWorkspaceBefore, createWorkspace,
2981
3111
  searchSessions, searchResultLimit, pickDirectory, hostHome, notify,
2982
3112
  accountSource, accountT, openSettings,
3113
+ pluginsPanel, openPluginsPanel, closeOverlay,
2983
3114
  } = props
2984
3115
 
2985
3116
  const merged = { ...DEFAULTS, ...(config ?? {}) }
@@ -3707,7 +3838,24 @@ window.__ModuleLoader__.load({
3707
3838
  onOpenSettings: typeof openSettings === 'function' ? openSettings : () => {},
3708
3839
  })
3709
3840
  : null
3710
- const topbar = h('div', { 'data-dsh-fso-topbar': '' }, profileButton, newSessionButton)
3841
+ /* 「插件」面板按钮:与头像同排,排在「新会话」右边。
3842
+ * 只有官方 `sidebar.panellist` 里确实登记了 plugins 时才渲染 ——
3843
+ * `layout.selectPanel` 对未注册的 key 会抛错,这里不能给假按钮。 */
3844
+ const pluginsButton = merged.showPluginsPanel === true
3845
+ && pluginsPanel === true
3846
+ && typeof openPluginsPanel === 'function'
3847
+ ? h(PanelButton, {
3848
+ key: 'plugins-panel',
3849
+ usePanelInfo,
3850
+ label: t('panel.plugins'),
3851
+ onOpen: () => {
3852
+ // 先叫出面板,再收浮层:selectPanel 失败时不至于连浮层都关了。
3853
+ const opened = openPluginsPanel()
3854
+ if (opened === true && typeof closeOverlay === 'function') closeOverlay()
3855
+ },
3856
+ })
3857
+ : null
3858
+ const topbar = h('div', { 'data-dsh-fso-topbar': '' }, profileButton, newSessionButton, pluginsButton)
3711
3859
 
3712
3860
  /* ── 模态框 ───────────────────────────────────────────────────────── */
3713
3861
  const Modal = P.Modal
@@ -3872,6 +4020,15 @@ window.__ModuleLoader__.load({
3872
4020
  let open = false
3873
4021
  let enabled = merged.enabled !== false
3874
4022
  let disposed = false
4023
+ /**
4024
+ * 「刚因导航而收起」标记。
4025
+ *
4026
+ * 顶栏按钮点完会收起浮层,但此时指针往往还停在卡片原来的位置上,而热区
4027
+ * (`trigger: 'content'` 时到内容列左缘的 40%)可能覆盖那一带 —— 用户只要
4028
+ * 轻动一下鼠标,浮层就“又弹出来”,看起来像按钮没生效。所以收起后置上这个
4029
+ * 标记,直到指针离开热区一次才恢复热区触发。
4030
+ */
4031
+ let suppressReveal = false
3875
4032
 
3876
4033
  /** 取卡片节点(可能还没渲染出来)。 */
3877
4034
  const cardOf = () => document.querySelector('[data-dsh-fso-card]')
@@ -4190,6 +4347,13 @@ window.__ModuleLoader__.load({
4190
4347
  const inTrigger = x <= triggerWidth() && belowTop
4191
4348
 
4192
4349
  if (!open) {
4350
+ /* 导航后抑制:指针还有一次机会离开热区,离开即恢复。
4351
+ * 否则点完「插件」浮层马上又弹回来。 */
4352
+ if (suppressReveal) {
4353
+ if (!inTrigger) suppressReveal = false
4354
+ return
4355
+ }
4356
+
4193
4357
  /* 未命中时**低频**重测热区:中栏宽度会因用户拖拽、侧边栏开合、
4194
4358
  * 会话切换而变化(官方写入 `--dsh-chat-user-width`),只读 DOM 的
4195
4359
  * 布局也会变(header 有无 tab 时高度不同)。只靠 resize 会漏。
@@ -4326,6 +4490,31 @@ window.__ModuleLoader__.load({
4326
4490
  * `actions.open()`(与官方点「设置」同一条路径)。取不到时
4327
4491
  * 传 undefined,个人信息按钮就不渲染 —— 而不是一个点了没反应的假按钮。 */
4328
4492
  openSettings,
4493
+ /** 「插件」面板是否可用:官方 `sidebar.panellist` 里登记了 plugins
4494
+ * 才为 true。这里只读登记表,不占座位、不渲染它的图标。 */
4495
+ pluginsPanel: hasPanelEntry(slots, PANEL_PLUGINS_ID),
4496
+ /** 打开「插件」面板:与官方点同一个图标走同一条路径。
4497
+ * `selectPanel` 对未注册 key 会 throw,所以要包起来并把结果
4498
+ * 作为布尔返回,调用处据此决定是否收起浮层。 */
4499
+ openPluginsPanel: () => {
4500
+ try {
4501
+ const layout = svc('layout')
4502
+ if (typeof layout?.selectPanel !== 'function') return false
4503
+ layout.selectPanel(PANEL_PLUGINS_ID)
4504
+ return true
4505
+ } catch (error) {
4506
+ console.warn('[sidebar-overlay] 打开插件面板失败:', error?.message ?? error)
4507
+ notify('panel.failed')
4508
+ return false
4509
+ }
4510
+ },
4511
+ /** 收起浮层(供顶栏按钮在导航后调用)。
4512
+ * 置 `suppressReveal`:指针通常还停在卡片原位置,而那里仍在
4513
+ * 热区里;不抑制就会立刻又弹回来。指针离开热区一次即恢复。 */
4514
+ closeOverlay: () => {
4515
+ suppressReveal = true
4516
+ setOpen(false)
4517
+ },
4329
4518
  /** 快捷键目录:渲染器按 hooks 约定自动生成 useShortcuts prop。
4330
4519
  * source 缺失时组件内的类型检查会兜住。 */
4331
4520
  ...(shortcuts?.catalog === undefined ? {} : { hooks: { shortcuts: shortcuts.catalog } }),
@@ -4487,6 +4676,8 @@ window.__ModuleLoader__.load({
4487
4676
  exports.__resolveOpenSettings = resolveOpenSettings
4488
4677
  exports.__bindEntryFace = bindEntryFace
4489
4678
  exports.__ProfileButton = ProfileButton
4679
+ exports.__PanelButton = PanelButton
4680
+ exports.__hasPanelEntry = hasPanelEntry
4490
4681
  return module.exports
4491
4682
  },
4492
4683
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidebar-overlay",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "Floating sidebar overlay for DSH: an independently rendered workspace/session browser mounted in the official shell.overlay seat, without touching the official sidebar or grid. 独立渲染的 DSH 浮动侧边栏:不碰官方侧边栏与 grid。",
6
6
  "author": "blockman0321",