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