dsh-pocket 2.1.2 → 2.1.4
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.
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/client/client.js +99 -11
- package/client/mobile/MobileNavOverlay.tsx +65 -14
- package/client/mobile/mobile.css.ts +34 -3
- package/client/mobile/nav-targets.mjs +85 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -191,9 +191,11 @@ Such tools take over all traffic and often cut cloudflared's tunnel-edge connect
|
|
|
191
191
|
```sh
|
|
192
192
|
npm install
|
|
193
193
|
node client/build.mjs # rebuild after editing client/
|
|
194
|
-
npm test # proxy / auth / compression / tunnel / service / RPC (
|
|
194
|
+
npm test # proxy / auth / compression / tunnel / service / RPC / settings (93 tests)
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
+
**Want to try your changes locally without publishing?** Point the installed plugin at your local checkout with a symlink and restart dsh web. Full steps (including switching back to the npm release) are in [LOCAL-DEV.md](./LOCAL-DEV.md).
|
|
198
|
+
|
|
197
199
|
## 🤝 Credits
|
|
198
200
|
|
|
199
201
|
- Mobile adaptation ported from [mexiaosqwq/dsh-web-mobile](https://github.com/mexiaosqwq/dsh-web-mobile) (MIT)
|
package/README.md
CHANGED
|
@@ -192,9 +192,11 @@ npx @deepseek-ai/dsh web
|
|
|
192
192
|
```sh
|
|
193
193
|
npm install
|
|
194
194
|
node client/build.mjs # 改 client/ 后重新打包
|
|
195
|
-
npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC
|
|
195
|
+
npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC / 设置(93 测试)
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
+
**改完想在本机先试?** 不用发版:把插件换成指向本地仓库的软链,重启 dsh web 就是本地代码。完整步骤(含怎么换回 npm 官方版本)见 [LOCAL-DEV.md](./LOCAL-DEV.md)。
|
|
199
|
+
|
|
198
200
|
## 🤝 致谢
|
|
199
201
|
|
|
200
202
|
- 移动端适配移植自 [mexiaosqwq/dsh-web-mobile](https://github.com/mexiaosqwq/dsh-web-mobile)(MIT)
|
package/client/client.js
CHANGED
|
@@ -140,6 +140,41 @@ function MobileNavToggle({ toggleSidebar, t }) {
|
|
|
140
140
|
// client/mobile/MobileNavOverlay.tsx
|
|
141
141
|
var import_react = require("react");
|
|
142
142
|
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
143
|
+
|
|
144
|
+
// client/mobile/nav-targets.mjs
|
|
145
|
+
var DRAWER_SELECTOR = '[data-mobile-nav="frame"] > :first-child';
|
|
146
|
+
var TOGGLE_SELECTOR = '[data-mobile-nav="toggle"]';
|
|
147
|
+
var NAV_TARGETS = [
|
|
148
|
+
"button[data-dsh-taskboard-entry]",
|
|
149
|
+
"button[data-dsh-ssh-entry]",
|
|
150
|
+
// 抽屉底部的 "文件" 入口打开的是 dsh-web-ui 的 explorer 面板,它的 z-index
|
|
151
|
+
// (55) 低于展开的抽屉 (600),且在抽屉 DOM 之外:抽屉不关就会盖住面板,点
|
|
152
|
+
// 面板里的行又会被"点抽屉外就关"吃掉。所以按导航处理,一起关掉。
|
|
153
|
+
'[data-mobile-nav="files"]',
|
|
154
|
+
'[class*="sessionRow"]',
|
|
155
|
+
'[class*="newSession"]',
|
|
156
|
+
'[class*="searchResultWorkspace"]',
|
|
157
|
+
'[class*="searchResultRow"]'
|
|
158
|
+
].join(", ");
|
|
159
|
+
var NAV_EXCLUDE = '[class*="sessionRow"] button';
|
|
160
|
+
var OVERLAY_SELECTOR = [
|
|
161
|
+
'[role="menu"]',
|
|
162
|
+
'[role="listbox"]',
|
|
163
|
+
'[role="dialog"]',
|
|
164
|
+
'[role="tooltip"]',
|
|
165
|
+
"[data-radix-popper-content-wrapper]"
|
|
166
|
+
].join(", ");
|
|
167
|
+
function navTargetFor(target) {
|
|
168
|
+
if (target == null || typeof target.closest !== "function") return null;
|
|
169
|
+
if (target.closest(NAV_EXCLUDE) !== null) return null;
|
|
170
|
+
return target.closest(NAV_TARGETS);
|
|
171
|
+
}
|
|
172
|
+
function isOverlayTap(target) {
|
|
173
|
+
if (target == null || typeof target.closest !== "function") return false;
|
|
174
|
+
return target.closest(OVERLAY_SELECTOR) !== null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// client/mobile/MobileNavOverlay.tsx
|
|
143
178
|
var MOBILE_QUERY = "(max-width: 1023px)";
|
|
144
179
|
function useMobile() {
|
|
145
180
|
const [mobile, setMobile] = (0, import_react.useState)(() => window.matchMedia(MOBILE_QUERY).matches);
|
|
@@ -205,25 +240,47 @@ function MobileNavOverlay({ toggleSidebar, t }) {
|
|
|
205
240
|
if (document.querySelector('[aria-modal="true"]') !== null) return;
|
|
206
241
|
const target = event.target;
|
|
207
242
|
if (target === null) return;
|
|
208
|
-
const drawer = document.querySelector(
|
|
243
|
+
const drawer = document.querySelector(DRAWER_SELECTOR);
|
|
209
244
|
if (drawer === null || !drawer.contains(target)) return;
|
|
210
|
-
if (target
|
|
211
|
-
const navigates = target.closest(
|
|
212
|
-
'button[data-dsh-taskboard-entry], button[data-dsh-ssh-entry], [class*="newSession"], [class*="sessionRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [data-mobile-nav="files"]'
|
|
213
|
-
);
|
|
214
|
-
if (navigates !== null) toggleSidebar();
|
|
245
|
+
if (navTargetFor(target) !== null) toggleSidebar();
|
|
215
246
|
};
|
|
216
247
|
document.addEventListener("click", onDrawerClick, true);
|
|
217
248
|
return () => document.removeEventListener("click", onDrawerClick, true);
|
|
218
249
|
}, [mobile, open, toggleSidebar]);
|
|
250
|
+
(0, import_react.useEffect)(() => {
|
|
251
|
+
if (!mobile || !open) return;
|
|
252
|
+
let timer = null;
|
|
253
|
+
const onDrawerPointerUp = (event) => {
|
|
254
|
+
if (event.pointerType !== "touch" && event.pointerType !== "pen") return;
|
|
255
|
+
const target = event.target;
|
|
256
|
+
if (!(target instanceof Element)) return;
|
|
257
|
+
const drawer = document.querySelector(DRAWER_SELECTOR);
|
|
258
|
+
if (drawer === null || !drawer.contains(target)) return;
|
|
259
|
+
if (navTargetFor(target) === null) return;
|
|
260
|
+
if (timer !== null) return;
|
|
261
|
+
timer = window.setTimeout(() => {
|
|
262
|
+
timer = null;
|
|
263
|
+
const frame = document.querySelector('[data-mobile-nav="frame"]');
|
|
264
|
+
if (frame === null || frame.hasAttribute("data-sidebar-collapsed")) return;
|
|
265
|
+
const row = navTargetFor(target);
|
|
266
|
+
row?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
267
|
+
}, 0);
|
|
268
|
+
};
|
|
269
|
+
document.addEventListener("pointerup", onDrawerPointerUp, true);
|
|
270
|
+
return () => {
|
|
271
|
+
if (timer !== null) window.clearTimeout(timer);
|
|
272
|
+
document.removeEventListener("pointerup", onDrawerPointerUp, true);
|
|
273
|
+
};
|
|
274
|
+
}, [mobile, open]);
|
|
219
275
|
(0, import_react.useEffect)(() => {
|
|
220
276
|
if (!mobile || !open) return;
|
|
221
277
|
const onOutsideClick = (event) => {
|
|
222
278
|
if (document.querySelector('[aria-modal="true"]') !== null) return;
|
|
223
279
|
const target = event.target;
|
|
224
280
|
if (target === null) return;
|
|
225
|
-
if (target.closest(
|
|
226
|
-
|
|
281
|
+
if (target.closest(TOGGLE_SELECTOR) !== null) return;
|
|
282
|
+
if (isOverlayTap(target)) return;
|
|
283
|
+
const drawer = document.querySelector(DRAWER_SELECTOR);
|
|
227
284
|
if (drawer !== null && drawer.contains(target)) return;
|
|
228
285
|
toggleSidebar();
|
|
229
286
|
};
|
|
@@ -478,14 +535,19 @@ var MOBILE_CSS = `
|
|
|
478
535
|
it to 500), and when the layer outranks the drawer, the backdrop paints
|
|
479
536
|
ABOVE the drawer and swallows every tap \u2014 the drawer opens but no row
|
|
480
537
|
can be pressed (every tap just closes it). The drawer must therefore
|
|
481
|
-
outrank any such raise
|
|
482
|
-
|
|
538
|
+
outrank any such raise.
|
|
539
|
+
1200 (was 600) clears the mobile layers shipped by
|
|
540
|
+
@linxin666/dsh-web-ui-all \u2014 its sidebar pane is z-index 1100, its
|
|
541
|
+
details pane 1000 and its full-screen frame ::after mask 1050 (issue
|
|
542
|
+
#67: that mask sat on top of the 600 drawer and ate every tap). Still
|
|
543
|
+
far under the fixed-position banners/toasts (z 9999) that float at the
|
|
544
|
+
viewport level. */
|
|
483
545
|
[data-mobile-nav="frame"] > :first-child {
|
|
484
546
|
position: absolute !important;
|
|
485
547
|
inset: 0 auto 0 0 !important;
|
|
486
548
|
width: max-content !important;
|
|
487
549
|
max-width: 92vw !important;
|
|
488
|
-
z-index:
|
|
550
|
+
z-index: 1200 !important;
|
|
489
551
|
transform: translateX(-110%);
|
|
490
552
|
transition: transform .28s var(--ds-ease-in-out, ease-in-out);
|
|
491
553
|
background: var(--dsw-alias-bg-base, #ffffff);
|
|
@@ -514,6 +576,32 @@ var MOBILE_CSS = `
|
|
|
514
576
|
transform: none !important;
|
|
515
577
|
}
|
|
516
578
|
|
|
579
|
+
/* Kill a competing full-screen mask (issue #67).
|
|
580
|
+
@linxin666/dsh-web-ui-all ships its own mobile drawer, and part of it is
|
|
581
|
+
|
|
582
|
+
[data-dsh-frame]:not([data-sidebar-collapsed])::after {
|
|
583
|
+
content: ""; position: fixed; inset: 0; z-index: 1050;
|
|
584
|
+
background: rgb(0 0 0 / 24%);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
The pseudo-element belongs to the frame we already mark, and the frame
|
|
588
|
+
carries only "position: relative" with z-index auto \u2014 no stacking context
|
|
589
|
+
\u2014 so this mask competes with the drawer in the parent stacking context
|
|
590
|
+
and, at 1050, paints over it. It covers the whole viewport, so every tap
|
|
591
|
+
on a session row lands on the mask instead: the drawer opens but nothing
|
|
592
|
+
inside it can be pressed, and the page behind cannot be scrolled.
|
|
593
|
+
Removing it is safe: the mobile stylesheet already renders its own
|
|
594
|
+
backdrop, and tapping outside the drawer is handled in JS.
|
|
595
|
+
|
|
596
|
+
The attribute selector is repeated on purpose. Their rule has the same
|
|
597
|
+
specificity (0,2,1) once ours is written the obvious way, and plugin
|
|
598
|
+
stylesheets are injected in load order, so a tie would be decided by
|
|
599
|
+
whichever plugin happened to load last. Doubling the attribute makes it
|
|
600
|
+
(0,3,1) and deterministic. */
|
|
601
|
+
[data-mobile-nav="frame"][data-mobile-nav="frame"]:not([data-sidebar-collapsed])::after {
|
|
602
|
+
content: none !important;
|
|
603
|
+
}
|
|
604
|
+
|
|
517
605
|
/* Drag handles are useless on touch and would float over the drawer. */
|
|
518
606
|
[data-side="sidebar"],
|
|
519
607
|
[data-side="details"] {
|
|
@@ -2,6 +2,12 @@ import { useEffect, useLayoutEffect, useState } from 'react'
|
|
|
2
2
|
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
3
3
|
import { IconPanelLeftOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
4
4
|
import { NS } from './locales.ts'
|
|
5
|
+
import {
|
|
6
|
+
DRAWER_SELECTOR,
|
|
7
|
+
TOGGLE_SELECTOR,
|
|
8
|
+
isOverlayTap,
|
|
9
|
+
navTargetFor,
|
|
10
|
+
} from './nav-targets.mjs'
|
|
5
11
|
|
|
6
12
|
/** Full props for the shell overlay entry. */
|
|
7
13
|
export interface MobileNavOverlayProps extends PropsRuntime<'shell.overlay'>, PropsLocale<typeof NS> {
|
|
@@ -104,7 +110,8 @@ export function MobileNavOverlay({ toggleSidebar, t }: MobileNavOverlayProps) {
|
|
|
104
110
|
// - Settings / Session log: their dialogs render INSIDE the drawer DOM
|
|
105
111
|
// (portaled into the sidebar); closing the drawer would slide the dialog
|
|
106
112
|
// off-screen with it.
|
|
107
|
-
// - Workspace folder
|
|
113
|
+
// - Workspace folder rows (YDXeBa_projectRow), the logo: pure UI toggles,
|
|
114
|
+
// not navigation — see NAV_TARGETS in nav-targets.mjs.
|
|
108
115
|
// - Anything while a modal dialog is open: the dialog owns the screen.
|
|
109
116
|
useEffect(() => {
|
|
110
117
|
if (!mobile || !open) return
|
|
@@ -112,27 +119,64 @@ export function MobileNavOverlay({ toggleSidebar, t }: MobileNavOverlayProps) {
|
|
|
112
119
|
if (document.querySelector('[aria-modal="true"]') !== null) return
|
|
113
120
|
const target = event.target as HTMLElement | null
|
|
114
121
|
if (target === null) return
|
|
115
|
-
const drawer = document.querySelector<HTMLElement>(
|
|
122
|
+
const drawer = document.querySelector<HTMLElement>(DRAWER_SELECTOR)
|
|
116
123
|
if (drawer === null || !drawer.contains(target)) return
|
|
117
124
|
// A session row's own action buttons — the "Session actions" kebab
|
|
118
125
|
// (delete / rename), revealed on hover / long-press — open an edit
|
|
119
126
|
// menu. Tapping one must NOT count as tapping the row, or the drawer
|
|
120
127
|
// would close and take the just-opened menu with it.
|
|
121
|
-
if (target
|
|
122
|
-
// The drawer footer "Files" action opens the dsh-web-ui explorer sheet,
|
|
123
|
-
// which sits at a LOWER z-index (55) than the open drawer (600) and
|
|
124
|
-
// outside the drawer DOM — leaving the drawer open would let it cover
|
|
125
|
-
// the sheet, and tapping a sheet row would be eaten by the tap-outside
|
|
126
|
-
// close handler. Close the drawer on Files, like navigation.
|
|
127
|
-
const navigates = target.closest(
|
|
128
|
-
'button[data-dsh-taskboard-entry], button[data-dsh-ssh-entry], [class*="newSession"], [class*="sessionRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [data-mobile-nav="files"]',
|
|
129
|
-
)
|
|
130
|
-
if (navigates !== null) toggleSidebar()
|
|
128
|
+
if (navTargetFor(target) !== null) toggleSidebar()
|
|
131
129
|
}
|
|
132
130
|
document.addEventListener('click', onDrawerClick, true)
|
|
133
131
|
return () => document.removeEventListener('click', onDrawerClick, true)
|
|
134
132
|
}, [mobile, open, toggleSidebar])
|
|
135
133
|
|
|
134
|
+
// iOS Safari touch self-heal (issue #72).
|
|
135
|
+
//
|
|
136
|
+
// A tap on a drawer row is delivered to the page as touchstart/touchend
|
|
137
|
+
// plus a browser-synthesized click. On iOS that click is routinely
|
|
138
|
+
// suppressed: a few px of finger drift is classified as a pan, and any DOM
|
|
139
|
+
// shift under the finger before dispatch cancels it outright. When it does
|
|
140
|
+
// not arrive, neither the row's own onClick nor the capture handler above
|
|
141
|
+
// runs — the row looks completely dead ("抽屉点了没反应").
|
|
142
|
+
//
|
|
143
|
+
// So: on touch/pen pointerup inside the drawer that hits a navigation row,
|
|
144
|
+
// arm a one-macrotask timer. When it fires:
|
|
145
|
+
// - the drawer is already closed → the real click did arrive and handled
|
|
146
|
+
// everything → do nothing (zero interference with the normal path);
|
|
147
|
+
// - otherwise the click never came → re-dispatch a bubbling click on the
|
|
148
|
+
// row ourselves. React's delegated listener runs the row's onClick (the
|
|
149
|
+
// session opens / the workspace switches) and the same click bubbles
|
|
150
|
+
// through the capture handler above, which closes the drawer.
|
|
151
|
+
//
|
|
152
|
+
// Mouse/pen-on-desktop keeps the plain click path: pointerType is 'mouse'.
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!mobile || !open) return
|
|
155
|
+
let timer: number | null = null
|
|
156
|
+
const onDrawerPointerUp = (event: PointerEvent): void => {
|
|
157
|
+
if (event.pointerType !== 'touch' && event.pointerType !== 'pen') return
|
|
158
|
+
const target = event.target
|
|
159
|
+
if (!(target instanceof Element)) return
|
|
160
|
+
const drawer = document.querySelector<HTMLElement>(DRAWER_SELECTOR)
|
|
161
|
+
if (drawer === null || !drawer.contains(target)) return
|
|
162
|
+
if (navTargetFor(target) === null) return
|
|
163
|
+
if (timer !== null) return
|
|
164
|
+
timer = window.setTimeout(() => {
|
|
165
|
+
timer = null
|
|
166
|
+
const frame = document.querySelector('[data-mobile-nav="frame"]')
|
|
167
|
+
// Real click already closed it — nothing to heal.
|
|
168
|
+
if (frame === null || frame.hasAttribute('data-sidebar-collapsed')) return
|
|
169
|
+
const row = navTargetFor(target)
|
|
170
|
+
row?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }))
|
|
171
|
+
}, 0)
|
|
172
|
+
}
|
|
173
|
+
document.addEventListener('pointerup', onDrawerPointerUp, true)
|
|
174
|
+
return () => {
|
|
175
|
+
if (timer !== null) window.clearTimeout(timer)
|
|
176
|
+
document.removeEventListener('pointerup', onDrawerPointerUp, true)
|
|
177
|
+
}
|
|
178
|
+
}, [mobile, open])
|
|
179
|
+
|
|
136
180
|
// Tap-outside closes the drawer (issue #38). The backdrop is now
|
|
137
181
|
// pointer-events: none (pure dimming layer that never steals clicks), so
|
|
138
182
|
// "tap the dimmed area to close" moves here: any click outside the drawer
|
|
@@ -146,8 +190,15 @@ export function MobileNavOverlay({ toggleSidebar, t }: MobileNavOverlayProps) {
|
|
|
146
190
|
if (document.querySelector('[aria-modal="true"]') !== null) return
|
|
147
191
|
const target = event.target as HTMLElement | null
|
|
148
192
|
if (target === null) return
|
|
149
|
-
if (target.closest(
|
|
150
|
-
|
|
193
|
+
if (target.closest(TOGGLE_SELECTOR) !== null) return
|
|
194
|
+
// Portaled overlays (issue #72): the workspace section's "视图选项" /
|
|
195
|
+
// "添加工作区" and the session row kebab all open menus that live on
|
|
196
|
+
// document.body — outside the drawer DOM. Without this exemption the
|
|
197
|
+
// first tap on a menu item is eaten: the drawer slides away, the menu
|
|
198
|
+
// unmounts with the sidebar, and the item's onClick never runs, so
|
|
199
|
+
// every workspace control reads as "点了没反应" on a phone.
|
|
200
|
+
if (isOverlayTap(target)) return
|
|
201
|
+
const drawer = document.querySelector<HTMLElement>(DRAWER_SELECTOR)
|
|
151
202
|
if (drawer !== null && drawer.contains(target)) return
|
|
152
203
|
toggleSidebar()
|
|
153
204
|
}
|
|
@@ -210,14 +210,19 @@ export const MOBILE_CSS = `
|
|
|
210
210
|
it to 500), and when the layer outranks the drawer, the backdrop paints
|
|
211
211
|
ABOVE the drawer and swallows every tap — the drawer opens but no row
|
|
212
212
|
can be pressed (every tap just closes it). The drawer must therefore
|
|
213
|
-
outrank any such raise
|
|
214
|
-
|
|
213
|
+
outrank any such raise.
|
|
214
|
+
1200 (was 600) clears the mobile layers shipped by
|
|
215
|
+
@linxin666/dsh-web-ui-all — its sidebar pane is z-index 1100, its
|
|
216
|
+
details pane 1000 and its full-screen frame ::after mask 1050 (issue
|
|
217
|
+
#67: that mask sat on top of the 600 drawer and ate every tap). Still
|
|
218
|
+
far under the fixed-position banners/toasts (z 9999) that float at the
|
|
219
|
+
viewport level. */
|
|
215
220
|
[data-mobile-nav="frame"] > :first-child {
|
|
216
221
|
position: absolute !important;
|
|
217
222
|
inset: 0 auto 0 0 !important;
|
|
218
223
|
width: max-content !important;
|
|
219
224
|
max-width: 92vw !important;
|
|
220
|
-
z-index:
|
|
225
|
+
z-index: 1200 !important;
|
|
221
226
|
transform: translateX(-110%);
|
|
222
227
|
transition: transform .28s var(--ds-ease-in-out, ease-in-out);
|
|
223
228
|
background: var(--dsw-alias-bg-base, #ffffff);
|
|
@@ -246,6 +251,32 @@ export const MOBILE_CSS = `
|
|
|
246
251
|
transform: none !important;
|
|
247
252
|
}
|
|
248
253
|
|
|
254
|
+
/* Kill a competing full-screen mask (issue #67).
|
|
255
|
+
@linxin666/dsh-web-ui-all ships its own mobile drawer, and part of it is
|
|
256
|
+
|
|
257
|
+
[data-dsh-frame]:not([data-sidebar-collapsed])::after {
|
|
258
|
+
content: ""; position: fixed; inset: 0; z-index: 1050;
|
|
259
|
+
background: rgb(0 0 0 / 24%);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
The pseudo-element belongs to the frame we already mark, and the frame
|
|
263
|
+
carries only "position: relative" with z-index auto — no stacking context
|
|
264
|
+
— so this mask competes with the drawer in the parent stacking context
|
|
265
|
+
and, at 1050, paints over it. It covers the whole viewport, so every tap
|
|
266
|
+
on a session row lands on the mask instead: the drawer opens but nothing
|
|
267
|
+
inside it can be pressed, and the page behind cannot be scrolled.
|
|
268
|
+
Removing it is safe: the mobile stylesheet already renders its own
|
|
269
|
+
backdrop, and tapping outside the drawer is handled in JS.
|
|
270
|
+
|
|
271
|
+
The attribute selector is repeated on purpose. Their rule has the same
|
|
272
|
+
specificity (0,2,1) once ours is written the obvious way, and plugin
|
|
273
|
+
stylesheets are injected in load order, so a tie would be decided by
|
|
274
|
+
whichever plugin happened to load last. Doubling the attribute makes it
|
|
275
|
+
(0,3,1) and deterministic. */
|
|
276
|
+
[data-mobile-nav="frame"][data-mobile-nav="frame"]:not([data-sidebar-collapsed])::after {
|
|
277
|
+
content: none !important;
|
|
278
|
+
}
|
|
279
|
+
|
|
249
280
|
/* Drag handles are useless on touch and would float over the drawer. */
|
|
250
281
|
[data-side="sidebar"],
|
|
251
282
|
[data-side="details"] {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// 抽屉点击规则的判定常量与纯函数 —— 抽成独立 ESM 模块,好让 node 测试直接
|
|
2
|
+
// 引入(client 侧是 esbuild 打进 bundle 的,测试里没有 DOM 可用)。
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 抽屉本体:AppFrame 的第一个网格子元素,即被拉成抽屉的侧边栏列。
|
|
6
|
+
* @type {string}
|
|
7
|
+
*/
|
|
8
|
+
export const DRAWER_SELECTOR = '[data-mobile-nav="frame"] > :first-child'
|
|
9
|
+
|
|
10
|
+
/** 抽屉开关(会话头部那颗按钮)——点它不该被"点抽屉外就关"二次触发。 */
|
|
11
|
+
export const TOGGLE_SELECTOR = '[data-mobile-nav="toggle"]'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 命中即视为导航的行:点它们要把屏幕让给刚打开的内容,所以顺带关抽屉。
|
|
15
|
+
*
|
|
16
|
+
* 类名现状(2026-08,侧边栏 qDHVXG_* / YDXeBa_* 这一代):
|
|
17
|
+
* - `YDXeBa_sessionRow` 会话行 —— 命中 [class*="sessionRow"]
|
|
18
|
+
* - `YDXeBa_projectRow` 工作区行 —— **故意不收**:实测它是折叠/展开开关
|
|
19
|
+
* (aria-expanded 来回翻,会话列表显隐),不是导航。收进来会让每次展开
|
|
20
|
+
* 工作区都把抽屉关掉(issue #72)。
|
|
21
|
+
* 旧一代侧边栏的 `_searchResultWorkspace_` / `_searchResultRow_` 保留,兼容
|
|
22
|
+
* 尚未升级的宿主。
|
|
23
|
+
*
|
|
24
|
+
* @type {string}
|
|
25
|
+
*/
|
|
26
|
+
export const NAV_TARGETS = [
|
|
27
|
+
'button[data-dsh-taskboard-entry]',
|
|
28
|
+
'button[data-dsh-ssh-entry]',
|
|
29
|
+
// 抽屉底部的 "文件" 入口打开的是 dsh-web-ui 的 explorer 面板,它的 z-index
|
|
30
|
+
// (55) 低于展开的抽屉 (600),且在抽屉 DOM 之外:抽屉不关就会盖住面板,点
|
|
31
|
+
// 面板里的行又会被"点抽屉外就关"吃掉。所以按导航处理,一起关掉。
|
|
32
|
+
'[data-mobile-nav="files"]',
|
|
33
|
+
'[class*="sessionRow"]',
|
|
34
|
+
'[class*="newSession"]',
|
|
35
|
+
'[class*="searchResultWorkspace"]',
|
|
36
|
+
'[class*="searchResultRow"]',
|
|
37
|
+
].join(', ')
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 行内操作按钮(会话行的 kebab:重命名 / 删除)——点开的是菜单不是内容,
|
|
41
|
+
* 不能按导航处理,否则抽屉一关,刚弹出的菜单跟着没了。
|
|
42
|
+
* @type {string}
|
|
43
|
+
*/
|
|
44
|
+
export const NAV_EXCLUDE = '[class*="sessionRow"] button'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 浮层(portal 到 body 的下拉菜单 / 弹窗)。
|
|
48
|
+
*
|
|
49
|
+
* 抽屉里的控件(工作区区块的"视图选项"、"添加工作区",会话行的 kebab…)
|
|
50
|
+
* 弹出的菜单在 DOM 上属于 body,不在抽屉里。"点抽屉外就关"因此会把菜单项
|
|
51
|
+
* 的第一下点击吃掉:抽屉先滑走 → 菜单随侧边栏卸载 → 菜单项的 onClick 永远
|
|
52
|
+
* 不执行。手机上表现就是工作区相关菜单"点了没反应"(issue #72)。
|
|
53
|
+
*
|
|
54
|
+
* 落在浮层内的点击一律豁免,交给浮层自己收尾。
|
|
55
|
+
*
|
|
56
|
+
* @type {string}
|
|
57
|
+
*/
|
|
58
|
+
export const OVERLAY_SELECTOR = [
|
|
59
|
+
'[role="menu"]',
|
|
60
|
+
'[role="listbox"]',
|
|
61
|
+
'[role="dialog"]',
|
|
62
|
+
'[role="tooltip"]',
|
|
63
|
+
'[data-radix-popper-content-wrapper]',
|
|
64
|
+
].join(', ')
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 抽屉内一次点击是否算导航(应关抽屉)。命中 NAV_EXCLUDE 的行内按钮不算。
|
|
68
|
+
* @param {Element | null | undefined} target - 点击目标。
|
|
69
|
+
* @returns {Element | null} 命中的导航行,未命中返回 null。
|
|
70
|
+
*/
|
|
71
|
+
export function navTargetFor(target) {
|
|
72
|
+
if (target == null || typeof target.closest !== 'function') return null
|
|
73
|
+
if (target.closest(NAV_EXCLUDE) !== null) return null
|
|
74
|
+
return target.closest(NAV_TARGETS)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 一次点击是否落在浮层内(浮层自己会关闭,不该由抽屉规则接管)。
|
|
79
|
+
* @param {Element | null | undefined} target - 点击目标。
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
export function isOverlayTap(target) {
|
|
83
|
+
if (target == null || typeof target.closest !== 'function') return false
|
|
84
|
+
return target.closest(OVERLAY_SELECTOR) !== null
|
|
85
|
+
}
|
package/package.json
CHANGED