dsh-dock 0.13.0 → 0.14.0

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.
@@ -1,66 +1,47 @@
1
- // dsh-dock · 功能模块【手机适配】· 客户端视图(纯 Client 功能,无宿主半部)
1
+ // dsh-dock · 功能模块【手机适配】· 客户端视图
2
2
  //
3
3
  // 职责:手机/窄屏下的排版与触控兜底。dsh 布局自身在 <1024px(SIDEBAR_AUTO_COLLAPSE)
4
- // 已有窄屏抽屉,本模块只补它没管的四件事:
4
+ // 已有窄屏抽屉,本模块补它没管的几件事:
5
5
  // 1. 输入类控件字号拉到 16px —— iOS Safari 聚焦任何 <16px 的输入框都会强制放大页面,
6
6
  // 聊天输入框 14px 字号让手机端每点一次输入框整页就缩放一次,是最影响体验的一件事;
7
7
  // 2. 触控基础:去点按高亮、touch-action: manipulation(消双击缩放判定延迟),
8
8
  // 纯图标按钮(唯一子元素为 svg)面积兜底到 ≥36px(实测一批 28×28 的工具钮手机上很难点中);
9
9
  // 3. 代码块横向滚动 + 行内代码换行(窄屏不撑破版面);
10
- // 4. 浮动侧栏开关(FAB):拇指可达的 44px 圆钮,经 dsh 官方 ctx.layout.toggleSidebar()
11
- // 驱动窄屏抽屉(自己不发状态,只调官方动作,和侧栏右上角的折叠钮行为完全一致);
10
+ // 4. 手机端 chrome:底部 Tab 栏(会话/插件/设置)+ 侧栏抽屉遮罩 + 软键盘让位 +
11
+ // 窄屏排版(设置弹层纵排、侧栏浮层化)。这套 chrome 从【远程访问】整体迁到本模块
12
+ // 整体迁到本模块——远程访问只负责远程入口本身,本机窄屏同样享有手机适配。
12
13
  // 顺带给 viewport meta 补 viewport-fit=cover,standalone 模式下 env(safe-area-*) 才有值。
14
+ // 5. 滚动兜底:所有纵向滚动体声明 touch-action:pan-y + iOS 惯性滚动;设置弹层内容列
15
+ // 补 min-height:0(见 host.js 文件头对「哪都滑不动」两条根因的说明)。
16
+ //
17
+ // 宿主半部(features/mobile/host.js)经 tapIndex 内联注入同一套样式与行为(重启 dsh web
18
+ // 后生效);本客户端半部在宿主脚本缺位时自建,刷新页面即生效,两者用
19
+ // window.__dshDockMobileDrawer 旗标('host' | 'client',先到先得)互斥,绝不各建一套。
13
20
  //
14
21
  // 开关协议(关键):dsh-dock 的 css 字段会「无条件」并入全局 <style>(外壳 fullDockCss
15
- // 不区分功能启停),所以本模块所有全局样式都必须挂在 body[data-dk-mobile="1"] 属性下——
16
- // 该属性由 Overlay 在「功能启用 && 视口命中」时打上、其余情况摘除。停用功能 = 属性消失 =
17
- // 全部样式一秒还原,不需要任何 !important 互相拆台。
22
+ // 不区分功能启停),所以本模块的**全局**样式(输入字号/触控面积/代码块/滚动兜底)都挂在
23
+ // body[data-dk-mobile="1"] 属性下——该属性由 Overlay 在「功能启用 && 视口命中」时打上、
24
+ // 其余情况摘除。停用功能 = 属性消失 = 这些样式一秒还原,不需要 !important 互相拆台。
25
+ //
26
+ // 例外(有意的):窄屏 chrome 与滚动体贴底规则包在 @media (max-width:700px) 里,不挂
27
+ // body 属性——它们要么只命中本模块自建的 .dsh-mobile-* 元素(停用时元素已移除,规则空转),
28
+ // 要么是「窄屏才有意义」的排版修正(滚动体留白/贴底/chip 收起)。这层在 700px 以上完全
29
+ // 不生效,桌面与平板不受影响;与迁移前【远程访问】的同款规则行为一致。
18
30
  //
19
31
  // 命中条件:窗口宽 ≤820px,或 主指针为粗触且无悬停(真手机/平板)。桌面触屏(主指针 fine)
20
32
  // 不会误命中;桌面把窗口拖窄到 820px 以下也会命中——这是有意的,方便桌面调试手机效果。
33
+ // chrome 自身的工作带宽更窄(≤700px,与宿主半部一致):820px 平板宽度下不塞底部 Tab 栏。
21
34
  //
22
35
  // ⚠️ 命名空间用 dkmob- 前缀:dkm- 已被【模型设置】模块占用,撞了会互相覆盖样式。
23
36
  import react from "react";
24
37
  import { useEffect, useState } from "react";
38
+ import { openPanel, subscribePanel, panelNav } from "../../src/shared.js";
25
39
 
26
40
  /** 视口命中条件(任一即视为手机形态)。 */
27
41
  const MOBILE_QUERY_NARROW = "(max-width: 820px)";
28
42
  const MOBILE_QUERY_COARSE = "(pointer: coarse) and (hover: none)";
29
-
30
- // 浮动侧栏按钮的显示偏好(localStorage 持久化,按浏览器来源各自记忆——手机上关掉
31
- // 不影响桌面)。缺省显示:功能启用 + 视口命中时,首次就来一个可用的拇指开关。
32
- const MOBILE_STORE_KEY = "dsh-dock/mobile/v1";
33
- const mobilePref = { floating: true };
34
- try {
35
- if (typeof localStorage !== "undefined") {
36
- const raw = localStorage.getItem(MOBILE_STORE_KEY);
37
- const obj = raw ? JSON.parse(raw) : null;
38
- if (obj && typeof obj === "object" && typeof obj.floating === "boolean") mobilePref.floating = obj.floating;
39
- }
40
- } catch { /* localStorage 不可用时用默认值 */ }
41
-
42
- const prefListeners = new Set();
43
- function notifyPref() {
44
- for (const fn of prefListeners) {
45
- try { fn(); } catch { /* 订阅者渲染失败不影响其他 */ }
46
- }
47
- }
48
- /** 浮动侧栏按钮当前是否显示。 */
49
- export function floatingShown() {
50
- return mobilePref.floating !== false;
51
- }
52
- /** 设置浮动侧栏按钮显示偏好(立即生效并持久化)。 */
53
- export function setFloatingShown(value) {
54
- mobilePref.floating = value !== false;
55
- try {
56
- if (typeof localStorage !== "undefined") localStorage.setItem(MOBILE_STORE_KEY, JSON.stringify(mobilePref));
57
- } catch { /* 持久化失败静默 */ }
58
- notifyPref();
59
- }
60
- function subscribePref(fn) {
61
- prefListeners.add(fn);
62
- return () => { prefListeners.delete(fn); };
63
- }
43
+ /** 手机端 chrome(底部 Tab 栏/遮罩/窄屏排版)的工作带宽,与宿主编排一致。 */
44
+ const MOBILE_QUERY_CHROME = "(max-width: 700px)";
64
45
 
65
46
  /** 当前视口是否命中手机形态(SSR/无 window 时恒 false)。 */
66
47
  export function mobileActive() {
@@ -70,20 +51,27 @@ export function mobileActive() {
70
51
  } catch { return false; }
71
52
  }
72
53
 
73
- // —— Overlay:功能启用期间常驻挂载,负责属性开关、viewport meta 与浮动按钮 ——
54
+ /** 订阅一个 media query,回调即时触发一次,返回取消订阅函数。 */
55
+ function watchQuery(query, onChange) {
56
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {};
57
+ let mq = null;
58
+ try { mq = window.matchMedia(query); } catch { return () => {}; }
59
+ const update = () => onChange(mq.matches);
60
+ update();
61
+ if (typeof mq.addEventListener === "function") mq.addEventListener("change", update);
62
+ else if (typeof mq.addListener === "function") mq.addListener(update);
63
+ return () => {
64
+ if (typeof mq.removeEventListener === "function") mq.removeEventListener("change", update);
65
+ else if (typeof mq.removeListener === "function") mq.removeListener(update);
66
+ };
67
+ }
68
+
69
+ // —— Overlay:功能启用期间常驻挂载,负责属性开关、viewport meta 与手机端 chrome ——
74
70
  // 属性管理放 effect 里(含卸载清理):功能停用 → Overlay 卸载 → 属性摘除 → 全局样式失效。
75
- //
76
- // ⚠️ 与【远程访问】(mobile-relay) 的手机 chrome 互斥协调:窄屏 ≤700px 且远程访问启用时,
77
- // 它的 Overlay 会自建「抽屉把手 + 底部 Tab 栏」整套 chrome,并经 window.__dshDockMobileDrawer
78
- // 旗标('host' | 'client',先到先得)声明归属。本模块见到旗标即让出浮动按钮——侧栏开合
79
- // 已有把手负责,两个浮标并排是重复 UI;CSS 兜底层不受影响,互补生效。功能坞面板开关
80
- // 全局重渲染时本组件会跟着重渲染,旗标消失(远程访问被停用)后浮动按钮自动回来。
81
- export function MobileOverlay(props) {
82
- const ctx = props && props.ctx;
71
+ export function MobileOverlay() {
83
72
  const [active, setActive] = useState(() => mobileActive());
84
- const [floating, setFloating] = useState(() => floatingShown());
85
73
 
86
- // 视口命中监听(matchMedia change;老内核降级 addListener)
74
+ // 视口命中监听(matchMedia change;老内核降级 addListener)。两条查询任一命中即 active。
87
75
  useEffect(() => {
88
76
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return undefined;
89
77
  let mqs = [];
@@ -122,55 +110,253 @@ export function MobileOverlay(props) {
122
110
  return () => { document.body.removeAttribute("data-dk-mobile"); };
123
111
  }, [active]);
124
112
 
125
- // 偏好变化(功能设置页里的开关)实时反映到浮动按钮
126
- useEffect(() => subscribePref(() => setFloating(floatingShown())), []);
113
+ useEffect(() => mobileChrome(), []);
114
+ return null;
115
+ }
116
+
117
+ // —— 手机端 chrome(客户端自建版):窄屏 ≤700px 的底部 Tab 栏 + 抽屉遮罩 + 行为 ——
118
+ // 直接操作 document.body 下的 DOM——若作为 overlayLayer 的子元素,会落入 overlayLayer
119
+ // (z 90)的堆叠上下文,遮罩压不住侧栏抽屉(z 80)、Tab 栏也盖不住内容;挂到 body 下
120
+ // z 序才是设计值(遮罩 75 < 侧栏 80 < overlayLayer 90)。
121
+ //
122
+ // 与宿主 tapIndex 注入版(features/mobile/host.js,重启 dsh web 后才生效)互斥:
123
+ // window.__dshDockMobileDrawer 先到先得——宿主脚本在 <head> 先跑并置 'host',本组件
124
+ // 见旗标即整体让位(遮罩/底部 Tab/收起逻辑全归宿主);宿主未就位(旧版无注入、
125
+ // 或刷新早于重启)时本组件自建全套。本组件的价值是 client bundle 按内容 hash 服务、
126
+ // 刷新页面即生效,无需重启。
127
+ // ⚠️ 两个版本绝不能各建一套:曾出现双底部 Tab 栏(两套按钮叠在一起)与 CSS 互相覆盖。
128
+ function mobileChrome() {
129
+ if (typeof window === "undefined" || typeof document === "undefined") return undefined;
130
+ const mq = window.matchMedia ? window.matchMedia(MOBILE_QUERY_CHROME) : null;
131
+ if (!mq) return undefined;
132
+ // 宿主版接管中:整套 chrome 由宿主负责,本组件零副作用退出。
133
+ if (window.__dshDockMobileDrawer === "host") return undefined;
134
+ window.__dshDockMobileDrawer = "client";
135
+
136
+ const narrow = () => mq.matches;
137
+ const frameEl = () => document.querySelector('[class$="frame"]');
138
+ const sidebarCollapsed = () => { const f = frameEl(); return !!f && f.hasAttribute("data-sidebar-collapsed") };
139
+ const dialogOpen = () => !!document.querySelector('[role="dialog"][class*="panel"]');
140
+ // 右侧详情面板:dsh 0.1.7 起 detailsCol 改名 rightbarCol,窄屏原生全屏展开
141
+ // (data-sidebar-right-panel / data-sidebar-right-open,折叠钮 data-sidebar-right-toggle)。
142
+ const rightOpen = () => !!document.querySelector('[data-sidebar-right-panel][data-sidebar-right-open]');
143
+ const collapse = () => { const b = document.querySelector('button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]'); if (b) b.click() };
144
+ const expand = () => { const b = document.querySelector('button[aria-label="打开侧边栏"],button[aria-label="Open sidebar"]'); if (b) b.click() };
145
+ const closeDetails = () => {
146
+ const panel = document.querySelector('[data-sidebar-right-panel][data-sidebar-right-open]');
147
+ if (!panel) return;
148
+ const toggle = panel.querySelector('[data-sidebar-right-toggle]');
149
+ if (toggle) toggle.click();
150
+ };
151
+
152
+ // 抽屉遮罩:侧栏展开(浮层态)时盖住页面,点击经全局捕获 handler 收起。
153
+ // 它是全屏 touch-action:none 浮层,收起态必须 display:none——残留一份会让整页滑不动。
154
+ const scrim = document.createElement("div");
155
+ scrim.className = "dsh-mobile-scrim";
156
+ document.body.appendChild(scrim);
127
157
 
128
- // 远程访问手机 chrome 的工作带宽(≤700px)。旗标在但视口出了它的带(700~820px 区间)
129
- // 时它的元素只是被 sync 隐藏、仍在 DOM 里——此时浮动按钮必须回归,否则这个区间
130
- // 没有任何侧栏浮标。监听 700px 边界,跨线即重渲染。
131
- const [relayBand, setRelayBand] = useState(() => {
132
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
133
- try { return window.matchMedia("(max-width: 700px)").matches; } catch { return false; }
158
+ // ---- 仿 ZCode 手机端底部 Tab 栏:会话 / 插件 / 设置 ----
159
+ // 「任务」「功能坞」合并为一个「插件」页签(用户反馈两个入口冗余):插件面板里
160
+ // 本来就有运行状态页,任务状态以角标显示在「插件」上——进行中蓝色数量、等待确认
161
+ // 红色脉冲;有任务在跑时点「插件」直接落到运行状态页,否则进面板首页。
162
+ // 挂 body(避开 overlayLayer 堆叠上下文),z 序 65:
163
+ // 低于遮罩 75 / 侧栏抽屉 80 / 功能坞面板 200,这些打开时自然盖住 Tab 栏。
164
+ // 走到这里说明宿主未注入——兜底摘一次历史上可能残留的旧 Tab 栏,保证全程只有一份。
165
+ document.querySelectorAll(".dsh-mobile-tabbar").forEach((el) => el.remove());
166
+ const tabbar = document.createElement("nav");
167
+ tabbar.className = "dsh-mobile-tabbar";
168
+ tabbar.setAttribute("aria-label", "底部导航");
169
+ // 最近一次角标轮询看到的任务数(驱动「插件」页签的直达目标与角标显隐)。
170
+ let tasksActive = 0;
171
+ let tasksWaiting = 0;
172
+ const tabDefs = [
173
+ {
174
+ id: "sessions", label: "会话",
175
+ icon: '<svg viewBox="0 0 24 24"><path d="M21 12a8 8 0 0 1-8 8H5l-2 2V12a8 8 0 0 1 8-8h2a8 8 0 0 1 8 8z"/></svg>',
176
+ act: () => expand(),
177
+ },
178
+ {
179
+ id: "dock", label: "插件",
180
+ icon: '<svg viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg>',
181
+ act: () => {
182
+ // 始终打开、不再当开关用:若功能坞处于打开态但被原生全屏页/弹层(如从侧栏
183
+ // 进的官方插件管理页)盖住,再点一下会把功能坞关掉、露出的反而是原生页——
184
+ // 看起来就是「点插件出官方插件页」(用户实测截图)。关闭走面板自己的 ✕。
185
+ // 配合手机端把功能坞面板 z 序抬到原生弹层之上(媒体查询内 2000)。
186
+ openPanel((tasksActive > 0 || tasksWaiting > 0) ? "runstate" : "home");
187
+ },
188
+ },
189
+ {
190
+ id: "settings", label: "设置",
191
+ icon: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.51 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.23.6.86 1 1.51 1H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.51 1z"/></svg>',
192
+ act: () => {
193
+ // 中英文双匹配:aria-label 随 dsh 客户端语言本地化(英文环境是 "Settings"),
194
+ // 只认「设置」会让英文环境的页签点不动(测试实例 en 环境实测)。
195
+ const b = [...document.querySelectorAll("button")].find((x) => {
196
+ const a = x.getAttribute("aria-label") || "";
197
+ return a === "设置" || a === "Settings" || (x.textContent || "").trim() === "设置";
198
+ });
199
+ if (b) b.click()
200
+ },
201
+ },
202
+ ];
203
+ let tasksBadge = null;
204
+ const tabButtons = tabDefs.map((def) => {
205
+ const b = document.createElement("button");
206
+ b.type = "button";
207
+ b.className = "dsh-mobile-tab";
208
+ b.dataset.tab = def.id;
209
+ b.innerHTML = def.icon + "<span>" + def.label + "</span>";
210
+ b.addEventListener("click", () => def.act());
211
+ if (def.id === "dock") {
212
+ tasksBadge = document.createElement("span");
213
+ tasksBadge.className = "dsh-mobile-tab-badge";
214
+ tasksBadge.style.display = "none";
215
+ b.appendChild(tasksBadge);
216
+ }
217
+ tabbar.appendChild(b);
218
+ return b;
134
219
  });
135
- useEffect(() => {
136
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return undefined;
137
- let mq = null;
138
- try { mq = window.matchMedia("(max-width: 700px)"); } catch { return undefined; }
139
- const update = () => setRelayBand(mq.matches);
140
- update();
141
- if (typeof mq.addEventListener === "function") mq.addEventListener("change", update);
142
- else if (typeof mq.addListener === "function") mq.addListener(update);
143
- return () => {
144
- if (typeof mq.removeEventListener === "function") mq.removeEventListener("change", update);
145
- else if (typeof mq.removeListener === "function") mq.removeListener(update);
146
- };
147
- }, []);
220
+ document.body.appendChild(tabbar);
221
+ const syncTabs = () => {
222
+ const expanded = !sidebarCollapsed();
223
+ const settingsOpen = dialogOpen();
224
+ for (const b of tabButtons) {
225
+ const id = b.dataset.tab;
226
+ const on = (id === "sessions" && expanded)
227
+ || (id === "dock" && panelNav.open)
228
+ || (id === "settings" && settingsOpen);
229
+ b.classList.toggle("on", on);
230
+ }
231
+ };
232
+ const unsubPanel = subscribePanel(syncTabs);
148
233
 
149
- if (!active || !floating) return null;
150
- // 远程访问的手机 chrome 在场且视口在它的带内:把手/底部 Tab 已提供侧栏开合,让位
151
- const relayChromeActive = typeof window !== "undefined" && window.__dshDockMobileDrawer != null && relayBand;
152
- if (relayChromeActive) return null;
153
- const onToggle = () => {
154
- try {
155
- const layout = ctx && ctx.get ? ctx.get("layout") : null;
156
- if (layout && typeof layout.toggleSidebar === "function") layout.toggleSidebar();
157
- else console.warn("[dsh-dock] 手机适配:ctx.layout.toggleSidebar 不可用(dsh 版本过旧?)");
158
- } catch (err) {
159
- console.error("[dsh-dock] 手机适配:切换侧栏失败:", err);
234
+ // 「插件」角标:低频轮询 runstate 宿主路由(3s,仅窄屏 + 页面可见时发);
235
+ // 进行中 = 蓝色数量,等待确认 = 红色数量 + 脉冲。路由不存在(runstate 未启用
236
+ // 或宿主旧版)时静默停轮询并摘掉角标,不报错不打扰。
237
+ let runstateGone = false;
238
+ const pollTasks = () => {
239
+ if (runstateGone || !tasksBadge || !narrow() || document.visibilityState === "hidden" || !tabbar.isConnected) return;
240
+ fetch("dsh-dock/runstate/status", {
241
+ method: "POST", headers: { "content-type": "application/json" }, body: "{}",
242
+ }).then(async (res) => {
243
+ if (res.status === 404 || res.status === 405) { runstateGone = true; tasksActive = 0; tasksWaiting = 0; tasksBadge.style.display = "none"; return null }
244
+ const body = await res.json().catch(() => null);
245
+ if (!body) return null;
246
+ const active = (body.data && body.data.active) || [];
247
+ const waiting = active.filter((t) => t.approvals && t.approvals.length).length;
248
+ tasksActive = active.length;
249
+ tasksWaiting = waiting;
250
+ if (waiting > 0) {
251
+ tasksBadge.textContent = String(waiting);
252
+ tasksBadge.className = "dsh-mobile-tab-badge wait";
253
+ tasksBadge.style.display = "";
254
+ } else if (active.length > 0) {
255
+ tasksBadge.textContent = String(active.length);
256
+ tasksBadge.className = "dsh-mobile-tab-badge";
257
+ tasksBadge.style.display = "";
258
+ } else {
259
+ tasksBadge.style.display = "none";
260
+ }
261
+ }).catch(() => {});
262
+ };
263
+ const tasksTimer = setInterval(pollTasks, 3000);
264
+
265
+ // 软键盘判定:visualViewport 与布局视口高度差超阈值视为键盘弹出(挂
266
+ // html.dsh-dock-kbd,样式表随之隐藏 Tab 栏并收窄滚动体留白——打字时
267
+ // 这两样只会挤占键盘上方的一线空间)。老内核没有 visualViewport 就跳过。
268
+ const vv = window.visualViewport || null;
269
+ const kbdSync = () => {
270
+ if (!vv) return;
271
+ const kbd = vv.height > 0 && (window.innerHeight - vv.height) > 120;
272
+ document.documentElement.classList.toggle("dsh-dock-kbd", kbd);
273
+ };
274
+
275
+ const sync = () => {
276
+ kbdSync();
277
+ // 兜底摘除历史上可能残留的旧 Tab 栏(正常路径下走到这里说明宿主版未注入,
278
+ // 不会有第二份;保险起见 sync 周期里也保持全局只有一份)。
279
+ document.querySelectorAll(".dsh-mobile-tabbar").forEach((el) => { if (el !== tabbar) el.remove() });
280
+ const on = narrow() && !!frameEl() && !dialogOpen();
281
+ const collapsed = sidebarCollapsed();
282
+ // 右侧详情面板(窄屏原生全屏,z 40)打开时:Tab 栏 65 比它高会压边,整体让位,
283
+ // 面板收起(它自带折叠钮)后再回来。
284
+ const right = rightOpen();
285
+ // 遮罩只在抽屉真的展开时显示——它是全屏 touch-action:none 浮层,
286
+ // 收起态残留会让整页滑不动。
287
+ if (scrim) scrim.style.display = on && !collapsed ? "" : "none";
288
+ tabbar.style.display = narrow() && !!frameEl() && !panelNav.open && !right ? "" : "none";
289
+ syncTabs();
290
+ };
291
+ sync();
292
+ pollTasks();
293
+ // MutationObserver 跟属性翻转即时同步;1.5s 轮询覆盖路由重渲等一切边角
294
+ const observer = typeof MutationObserver !== "undefined" ? new MutationObserver(sync) : null;
295
+ if (observer) observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-sidebar-collapsed"], subtree: true });
296
+ const timer = setInterval(sync, 1500);
297
+ const onMq = () => sync();
298
+ if (mq.addEventListener) mq.addEventListener("change", onMq);
299
+ else if (mq.addListener) mq.addListener(onMq);
300
+ window.addEventListener("resize", kbdSync);
301
+ if (vv && vv.addEventListener) vv.addEventListener("resize", kbdSync);
302
+ const onVisible = () => { if (document.visibilityState === "visible") pollTasks() };
303
+ document.addEventListener("visibilitychange", onVisible);
304
+
305
+ // 捕获阶段统一处理:
306
+ // Tab 栏自身排除(否则页签动作会被「点外部收起」当场撤销);
307
+ // 遮罩不挂自己的 click——它的点击也走这里 collapse(),单一路径防止同一事件两次 toggle。
308
+ const onClick = (e) => {
309
+ if (!narrow()) return;
310
+ const t = e.target;
311
+ if (!t || !t.closest) return;
312
+ if (t.closest(".dsh-mobile-tabbar")) return;
313
+ const expanded = !!document.querySelector('button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]');
314
+ const col = document.querySelector('[class*="sidebarCol"]');
315
+ const inSidebar = col && col.contains(t);
316
+ if (!expanded && !inSidebar) return;
317
+ if (inSidebar) {
318
+ // 只有点「会话」才收抽屉:会话行 data-row-key 以 session: 开头(dsh 0.1.7+ 结构,
319
+ // ui-workspace Rows.tsx);工作区文件夹行是 workspace: 开头——点它只是展开/收拢
320
+ // 分组,绝不能收抽屉(用户实测「点文件夹抽屉被收起」)。搜索结果行无 data-row-key,
321
+ // 回退按无子叶判定。
322
+ const row = t.closest('[role="treeitem"]');
323
+ const rowKey = (row && row.getAttribute("data-row-key")) || "";
324
+ const sessionRow = (rowKey && rowKey.startsWith("session:"))
325
+ || (row && !rowKey && !row.querySelector('[role="treeitem"]') && row.closest('[role="tree"]'));
326
+ const fresh = t.closest('[class*="newSession"]');
327
+ if (sessionRow || fresh) { setTimeout(() => { collapse(); closeDetails(); }, 300); return; }
328
+ // 工作区文件夹:只展开/收拢分组,既不收抽屉也不做「面板弹出即收起」探测。
329
+ if (rowKey.startsWith("workspace:")) return;
330
+ // 侧栏入口(插件/自动化任务/设置/记忆/自动进化…)点击后会弹出面板,而面板
331
+ // z 序常低于展开的抽屉——被盖住等于「点了没反应」(用户实测截图)。稍候
332
+ // 探测:有面板弹出就收起抽屉让它露出来;目录折叠/搜索等不弹面板的点击不受影响。
333
+ setTimeout(() => { if (document.querySelector('[role="dialog"]')) collapse(); }, 260);
334
+ return;
160
335
  }
336
+ if (t.closest('[role="dialog"],[class*="dockm"],[class*="dgfab"],[class*="dgwin"],[class*="dgame"],[data-sidebar-right-panel]')) return;
337
+ collapse();
338
+ };
339
+ document.addEventListener("click", onClick, true);
340
+
341
+ return () => {
342
+ document.removeEventListener("click", onClick, true);
343
+ if (observer) observer.disconnect();
344
+ clearInterval(timer);
345
+ clearInterval(tasksTimer);
346
+ unsubPanel();
347
+ if (mq.removeEventListener) mq.removeEventListener("change", onMq);
348
+ else if (mq.removeListener) mq.removeListener(onMq);
349
+ window.removeEventListener("resize", kbdSync);
350
+ if (vv && vv.removeEventListener) vv.removeEventListener("resize", kbdSync);
351
+ document.removeEventListener("visibilitychange", onVisible);
352
+ document.documentElement.classList.remove("dsh-dock-kbd");
353
+ if (scrim) scrim.remove();
354
+ tabbar.remove();
355
+ if (window.__dshDockMobileDrawer === "client") delete window.__dshDockMobileDrawer;
161
356
  };
162
- return react.createElement("button", {
163
- type: "button",
164
- className: "dkmob-fab",
165
- "aria-label": "开关侧栏",
166
- title: "开关侧栏",
167
- onClick: onToggle,
168
- }, react.createElement("svg", { width: 20, height: 20, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, "aria-hidden": true },
169
- react.createElement("rect", { x: 1.5, y: 2.5, width: 13, height: 11, rx: 2 }),
170
- react.createElement("line", { x1: 6, y1: 2.5, x2: 6, y2: 13.5 })));
171
357
  }
172
358
 
173
- // —— 功能页:状态一览 + 浮动按钮开关 + 说明 ——
359
+ // —— 功能页:状态一览 + 说明 ——
174
360
  function useViewportSize() {
175
361
  const [size, setSize] = useState(() => (typeof window === "undefined" ? null : { w: window.innerWidth, h: window.innerHeight }));
176
362
  useEffect(() => {
@@ -182,29 +368,39 @@ function useViewportSize() {
182
368
  return size;
183
369
  }
184
370
 
185
- function MobileView() {
371
+ function useMobileState() {
186
372
  const [active, setActive] = useState(() => mobileActive());
187
- const [floating, setFloating] = useState(() => floatingShown());
188
- const size = useViewportSize();
373
+ const [chrome, setChrome] = useState(() => (typeof window === "undefined" || typeof window.matchMedia !== "function"
374
+ ? false
375
+ : window.matchMedia(MOBILE_QUERY_CHROME).matches));
376
+ const [owner, setOwner] = useState(() => (typeof window === "undefined" ? null : window.__dshDockMobileDrawer || null));
189
377
  useEffect(() => {
190
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return undefined;
378
+ const offChrome = watchQuery(MOBILE_QUERY_CHROME, setChrome);
191
379
  let mqs = [];
192
380
  try {
193
381
  mqs = [window.matchMedia(MOBILE_QUERY_NARROW), window.matchMedia(MOBILE_QUERY_COARSE)];
194
- } catch { return undefined; }
195
- const update = () => setActive(mqs.some((mq) => mq.matches));
196
- update();
382
+ } catch { mqs = []; }
383
+ const compute = () => setActive(mqs.some((mq) => mq.matches));
197
384
  for (const mq of mqs) {
198
- if (typeof mq.addEventListener === "function") mq.addEventListener("change", update);
199
- else if (typeof mq.addListener === "function") mq.addListener(update);
385
+ if (typeof mq.addEventListener === "function") mq.addEventListener("change", compute);
386
+ else if (typeof mq.addListener === "function") mq.addListener(compute);
200
387
  }
388
+ const timer = setInterval(() => setOwner(typeof window === "undefined" ? null : window.__dshDockMobileDrawer || null), 1000);
201
389
  return () => {
390
+ offChrome();
202
391
  for (const mq of mqs) {
203
- if (typeof mq.removeEventListener === "function") mq.removeEventListener("change", update);
204
- else if (typeof mq.removeListener === "function") mq.removeListener(update);
392
+ if (typeof mq.removeEventListener === "function") mq.removeEventListener("change", compute);
393
+ else if (typeof mq.removeListener === "function") mq.removeListener(compute);
205
394
  }
395
+ clearInterval(timer);
206
396
  };
207
397
  }, []);
398
+ return { active, chrome, owner };
399
+ }
400
+
401
+ function MobileView() {
402
+ const { active, chrome, owner } = useMobileState();
403
+ const size = useViewportSize();
208
404
  return react.createElement("div", { className: "dkmob-root" },
209
405
  react.createElement("div", { className: "dkmob-row" },
210
406
  react.createElement("span", { className: "dkmob-label" }, "手机模式"),
@@ -213,72 +409,46 @@ function MobileView() {
213
409
  size ? size.w + "×" + size.h + "px" : "",
214
410
  "(≤820px 宽,或主指针为触屏无悬停时命中;桌面把窗口拖窄也会命中,方便调试)")),
215
411
  react.createElement("div", { className: "dkmob-row" },
216
- react.createElement("span", { className: "dkmob-label" }, "浮动侧栏按钮"),
217
- react.createElement("button", {
218
- type: "button",
219
- className: "dock-sw" + (floating ? " on" : ""),
220
- role: "switch",
221
- "aria-checked": floating,
222
- "aria-label": (floating ? "隐藏" : "显示") + "浮动侧栏按钮",
223
- title: floating ? "隐藏左下角的浮动侧栏开关" : "显示左下角的浮动侧栏开关",
224
- onClick: () => setFloatingShown(!floating),
225
- }),
226
- react.createElement("span", { className: "dkmob-dim" }, "44px 圆钮固定在左下角,调用 dsh 官方 toggleSidebar 开合窄屏抽屉;不需要时可在此关闭。")),
412
+ react.createElement("span", { className: "dkmob-label" }, "手机端 chrome"),
413
+ react.createElement("span", { className: "dkmob-state" + (chrome ? " on" : "") }, chrome ? "≤700px,生效中" : "未命中"),
414
+ react.createElement("span", { className: "dkmob-dim" },
415
+ "底部 Tab 栏(会话 / 插件 / 设置)+ 侧栏抽屉遮罩 + 软键盘让位;会话入口在底部「会话」页签,不设左缘浮动按钮。",
416
+ owner === "host" ? "当前由宿主注入版驱动(重启 dsh web 后生效的那套)。" : owner === "client" ? "当前由客户端自建版驱动(刷新即生效)。" : "")),
227
417
  react.createElement("div", { className: "dkmob-note" },
228
418
  "生效中的优化:输入类控件字号拉到 16px(避免 iOS 聚焦缩放整页)· 纯图标按钮面积兜底 ≥36px · ",
229
- "去掉点按高亮与双击缩放判定 · 代码块横向滚动、行内代码自动换行 · viewport meta 补 viewport-fit=cover",
419
+ "去掉点按高亮与双击缩放判定 · 代码块横向滚动、行内代码自动换行 · 滚动体 touch-action:pan-y + iOS 惯性滚动 · ",
420
+ "设置弹层内容列 min-height:0(否则窄屏弹层被内容撑高后整片滑不动)· viewport meta 补 viewport-fit=cover",
230
421
  "(standalone 全屏时给圆角/刘海留安全区)。停用本功能即全部还原。"));
231
422
  }
232
423
 
233
424
  // —— 首页概要:一行说清当前命中状态 ——
234
425
  function MobileStat() {
235
- const [active, setActive] = useState(() => mobileActive());
236
- const [floating, setFloating] = useState(() => floatingShown());
237
- useEffect(() => {
238
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return undefined;
239
- let mqs = [];
240
- try {
241
- mqs = [window.matchMedia(MOBILE_QUERY_NARROW), window.matchMedia(MOBILE_QUERY_COARSE)];
242
- } catch { return undefined; }
243
- const update = () => setActive(mqs.some((mq) => mq.matches));
244
- update();
245
- for (const mq of mqs) {
246
- if (typeof mq.addEventListener === "function") mq.addEventListener("change", update);
247
- else if (typeof mq.addListener === "function") mq.addListener(update);
248
- }
249
- return () => {
250
- for (const mq of mqs) {
251
- if (typeof mq.removeEventListener === "function") mq.removeEventListener("change", update);
252
- else if (typeof mq.removeListener === "function") mq.removeListener(update);
253
- }
254
- };
255
- }, []);
256
- useEffect(() => subscribePref(() => setFloating(floatingShown())), []);
426
+ const { active, chrome } = useMobileState();
257
427
  if (!active) return react.createElement("span", null, "当前视口未命中手机形态,样式未注入");
258
- return react.createElement("span", null, "手机模式生效中 · 浮动侧栏按钮" + (floating ? "显示" : "已隐藏"));
428
+ return react.createElement("span", null, "手机模式生效中 · " + (chrome ? "底部 Tab 栏已接管会话入口" : "chrome 带宽未命中(≤700px 才启用)"));
259
429
  }
260
430
 
261
- // —— 样式:FAB 自身样式(随功能启停)+ 全局兜底(全部挂在 body[data-dk-mobile="1"] 下,
262
- // 见文件头的开关协议说明。!important 只用在输入字号上:dsh 组件层对输入框有自己的
263
- // 字号声明,具体度相同的层叠顺序对插件的 <style> 插入时机敏感,这里以内容正确性优先)——
431
+ // —— 样式:本模块全部样式(随功能启停;全局部分依赖 body[data-dk-mobile="1"])。
432
+ // !important 只用在输入字号与 chrome 覆盖上:dsh 组件层对输入框有自己的
433
+ // 字号声明,具体度相同的层叠顺序对插件的 <style> 插入时机敏感,这里以内容正确性优先。
434
+ //
435
+ // ⚠️ chrome 样式(.dsh-mobile-*)与宿主半部(features/mobile/host.js 的
436
+ // MOBILE_LAYOUT_CSS)保持同款,两半部改一处必须同步另一处;测试
437
+ // scripts/test-mobile-host.mjs 与 scripts/test-client-views.mjs 各自钉契约。 ——
264
438
  const css = [
265
- // 浮动侧栏按钮:44px 触控面积(Apple HIG 最低建议),token 取主题变量自动适配亮暗色;
266
- // z-index 190 压在功能坞弹层(dockm-backdrop,200)之下,弹层打开时被遮罩盖住不误触。
267
- ".dkmob-fab{position:fixed;left:calc(env(safe-area-inset-left,0px) + 62px);bottom:calc(env(safe-area-inset-bottom,0px) + 74px);z-index:190;width:44px;height:44px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;padding:0;box-shadow:0 4px 14px rgb(0 0 0 / .22);cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;transition:transform .12s var(--ds-ease-in-out),background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
268
- ".dkmob-fab:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
269
- ".dkmob-fab:active{transform:scale(.94);}",
270
439
  // 输入类控件 16px:防 iOS 聚焦自动放大(<16px 的输入框聚焦时 Safari 强制缩放页面)
271
440
  'body[data-dk-mobile="1"] :is(textarea, input:not([type]), input[type="text"], input[type="search"], input[type="url"], input[type="password"], input[type="number"], input[type="email"], [contenteditable="true"]){font-size:16px !important;}',
272
441
  // 触控基础:去点按高亮、消双击缩放判定
273
442
  'body[data-dk-mobile="1"] :is(button, [role="button"]){-webkit-tap-highlight-color:transparent;touch-action:manipulation;}',
274
443
  // 纯图标按钮面积兜底:实测窄屏下一批 28×28 的工具钮很难点中;36px 是不引起行高剧变的上限。
275
- // 只收「唯一子元素是 svg」的按钮,避免撑坏带文字的行内控件;排除远程访问的抽屉把手
276
- // (.dsh-mobile-drawer-btn 贴边设计 30px 宽,被撑到 36 会破坏边缘吸附观感)与本模块 FAB。
277
- 'body[data-dk-mobile="1"] button:has(> svg:only-child):not(.dsh-mobile-drawer-btn):not(.dkmob-fab){min-width:36px;min-height:36px;}',
444
+ // 只收「唯一子元素是 svg」的按钮,避免撑坏带文字的行内控件;排除本模块 chrome 按钮。
445
+ 'body[data-dk-mobile="1"] button:has(> svg:only-child):not(.dsh-mobile-tab){min-width:36px;min-height:36px;}',
278
446
  // 代码块横向滚动、行内代码换行:窄屏不撑破版面
279
447
  'body[data-dk-mobile="1"] pre{max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;}',
280
448
  'body[data-dk-mobile="1"] code{overflow-wrap:anywhere;}',
281
449
  'body[data-dk-mobile="1"] pre code{overflow-wrap:normal;}',
450
+ // 滚动兜底:所有纵向滚动体可纵向平移 + iOS 惯性滚动(祖先合成层/触控声明不再吃掉触摸滚动)。
451
+ 'body[data-dk-mobile="1"] :is([class*="scrollBody"], [class*="sidebarCol"] [class*="list"], [role="dialog"] [class*="options"], [class*="dockm-content"]){-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain;touch-action:pan-y;}',
282
452
  // iOS 横竖屏切换时的字号自动膨胀关掉(:has 挂到 html 上;dsh 自身样式已依赖 :has,基线足够)
283
453
  'html:has(body[data-dk-mobile="1"]){-webkit-text-size-adjust:100%;text-size-adjust:100%;}',
284
454
  // 功能页自身
@@ -289,6 +459,58 @@ const css = [
289
459
  ".dkmob-state.on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);}",
290
460
  ".dkmob-dim{color:var(--dsw-alias-label-secondary);font-size:12px;}",
291
461
  ".dkmob-note{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.7;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
462
+ // 手机端 chrome:底部 Tab 栏 + 抽屉遮罩(窄屏 ≤700px;与宿主半部同款)
463
+ "@media (max-width:700px){",
464
+ ".dsh-mobile-scrim{position:fixed;inset:0;z-index:75;background:rgba(8,10,14,.45);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);touch-action:none}",
465
+ ".dsh-mobile-tabbar{position:fixed;left:0;right:0;bottom:0;z-index:65;display:flex;align-items:stretch;justify-content:space-around;padding:6px 8px calc(8px + env(safe-area-inset-bottom,0px));background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,#1c2230) 92%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-top:1px solid var(--dsw-alias-border-l1,rgba(127,139,161,.25));touch-action:manipulation}",
466
+ ".dsh-mobile-tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;min-height:48px;padding:4px 0;border:0;background:transparent;color:var(--dsw-alias-label-tertiary,#8b95ab);font:600 11px/1.2 inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;border-radius:10px;position:relative}",
467
+ ".dsh-mobile-tab svg{width:21px;height:21px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}",
468
+ ".dsh-mobile-tab.on{color:var(--dsw-alias-label-primary,#e6eaf2)}",
469
+ ".dsh-mobile-tab.on svg{color:var(--dk-accent,#4d9fff)}",
470
+ ".dsh-mobile-tab:active{transform:scale(.94)}",
471
+ ".dsh-mobile-tab-badge{position:absolute;top:0;right:calc(50% - 26px);box-sizing:border-box;min-width:16px;height:16px;padding:0 4px;border-radius:9px;display:flex;align-items:center;justify-content:center;background:#4d9fff;color:#fff;font-size:10px;font-weight:700;line-height:1;font-variant-numeric:tabular-nums;box-shadow:0 2px 8px rgba(0,0,0,.35);pointer-events:none}",
472
+ ".dsh-mobile-tab-badge.wait{background:#ef4444;animation:dsh-badge-pulse 1.2s ease-in-out infinite}",
473
+ "@keyframes dsh-badge-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.18)}}",
474
+ "@media (prefers-reduced-motion:reduce){.dsh-mobile-tab-badge.wait{animation:none}}",
475
+ // ⚠️ 「设置页滑不动」的关键兜底(客户端无条件携带 = 刷新即生效,不必等宿主重启):
476
+ // 窄屏把设置弹层改成纵向堆叠后,内容列若是默认 min-height:auto 就会被内容撑高
477
+ // (无头 390×844 实测列高 1216px 塞在 844px 视口里)、再被 .panel 的 overflow:hidden
478
+ // 裁掉——列内滚动容器([class*=options])因此 scrollHeight==clientHeight,手指怎么滑
479
+ // 都不动。补 min-height:0 后 scrollTop 0 → 425。宿主半部 MOBILE_LAYOUT_CSS 同款;
480
+ // 两处都写是因为宿主注入要重启 dsh web 才更新,而客户端样式刷新即生效,覆盖窗口期。
481
+ '[role="dialog"][class*="panel"]>[class*="content"]{min-height:0!important}',
482
+ // 输入区吸附到 Tab 栏上方 + 滚动体留白(与宿主半部同款)。
483
+ // ⚠️ 绝不能给滚动体加 justify-content:flex-end/center 贴底:内容会被挤到滚动体
484
+ // 起点之外,Chrome 不把起点方向溢出算进 scrollHeight(手机视口实测
485
+ // scrollHeight==clientHeight、maxScroll==0),会话记录彻底滚不动、首条消息永远看不到。
486
+ // 宿主半部旧版注入里还留着那条 flex-end(要重启 dsh web 才消失),这里用更高优先级
487
+ // 的选择器显式复位:刷新页面即修好,不必等重启。
488
+ '[class*="scrollBody"][data-conversation-scroll]{justify-content:flex-start !important;padding-bottom:calc(72px + env(safe-area-inset-bottom,0px)) !important}',
489
+ 'html:not(.dsh-dock-kbd) [class*="scrollBody"]{padding-bottom:calc(72px + env(safe-area-inset-bottom,0px)) !important}',
490
+ 'html.dsh-dock-kbd [class*="scrollBody"]{padding-bottom:12px !important}',
491
+ '[class*="scrollBody"]>[class*="composerSeat"]{margin-top:auto;padding-bottom:6px}',
492
+ // 输入区工具行上的 dsh-dock chip(用量/余额):窄屏下 28cqw 的宽度上限把它们挤成
493
+ // 「…」「余..」(手机实测),既读不出数值又白占输入行空间。手机端整组收起,数值在
494
+ // 底部「插件」里看(用量/余额页信息更全);桌面端不动。
495
+ '.dockchip-row{display:none}',
496
+ '[class*="sidebarCol"]:has([class*="collapsed"]){visibility:hidden}',
497
+ // ⚠️ 列级 visibility:hidden 在 dsh 0.2.0 上挡不住:工作区浏览器给自己的
498
+ // headerActions/sectionLabel 显式声明 visibility:visible(展开/收拢动画用),visibility
499
+ // 可被后代覆盖,轨道按钮会从隐藏列里「透」出来(实测手机截图左缘游离按钮)。
500
+ // display:none 不可被后代翻转——但只能打在**轨道根节点**上(类名同时含 root 与
501
+ // collapsed 两个语义后缀),绝不能打在列上:frame 是「侧栏 0px + 中列 1fr + 右轨 0px」
502
+ // 的网格,列 display:none 会从网格流里摘掉,中列被自动放置顶进 0px 轨(实测整页空屏)。
503
+ // 弹层打开时豁免(设置弹层挂在列内部,display:none 会连坐成空气弹层),此时列退回
504
+ // visibility 机制(由下一条 [role=dialog] 强制可见),窄屏弹层全屏铺满盖住透出的轨道。
505
+ '[class*="sidebarCol"]:has([class*="collapsed"]):not(:has([role="dialog"])) [class*="root"][class*="collapsed"]{display:none !important}',
506
+ // 设置/插件管理弹层挂在侧栏列内部:上面隐藏整列会把它连坐隐藏(空气弹层)。
507
+ // visibility 可被后代覆盖,弹层关闭态整体卸载,所以只在真正打开时命中。
508
+ '[class*="sidebarCol"]:has([class*="collapsed"]) [role="dialog"]{visibility:visible}',
509
+ 'html.dsh-dock-kbd .dsh-mobile-tabbar{display:none !important}',
510
+ // 功能坞面板在手机端抬到原生弹层之上:从侧栏进的官方插件管理页等原生弹层 z 序高于
511
+ // 面板默认的 200,功能坞开在下面会被盖住——「插件」页签点了像没反应/露出原页面。
512
+ '[class*="dockm-backdrop"]{z-index:2000 !important}',
513
+ "}",
292
514
  ].join("\n");
293
515
 
294
516
  export const feature = {
@@ -296,7 +518,7 @@ export const feature = {
296
518
  name: "手机适配",
297
519
  order: 170,
298
520
  accent: "#34d399",
299
- description: "手机/窄屏排版与触控兜底:16px 输入防 iOS 缩放、图标按钮触控面积、代码块滚动、浮动侧栏开关",
521
+ description: "手机/窄屏排版与触控兜底:底部 Tab 栏 + 侧栏抽屉、16px 输入防 iOS 缩放、图标按钮触控面积、代码块滚动、滚动兜底",
300
522
  defaultEnabled: true,
301
523
  css,
302
524
  Overlay: MobileOverlay,