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 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 (43 tests)
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(43 测试)
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('[data-mobile-nav="frame"] > :first-child');
243
+ const drawer = document.querySelector(DRAWER_SELECTOR);
209
244
  if (drawer === null || !drawer.contains(target)) return;
210
- if (target.closest('[class*="sessionRow"] button') !== null) return;
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('[data-mobile-nav="toggle"]') !== null) return;
226
- const drawer = document.querySelector('[data-mobile-nav="frame"] > :first-child');
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: 600 clears the known 500 while staying under the
482
- fixed-position banners/toasts (z 9999) that float at the viewport level. */
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: 600 !important;
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 chevrons, the logo: pure UI toggles, not navigation.
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>('[data-mobile-nav="frame"] > :first-child')
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.closest('[class*="sessionRow"] button') !== null) return
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('[data-mobile-nav="toggle"]') !== null) return
150
- const drawer = document.querySelector<HTMLElement>('[data-mobile-nav="frame"] > :first-child')
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: 600 clears the known 500 while staying under the
214
- fixed-position banners/toasts (z 9999) that float at the viewport level. */
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: 600 !important;
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
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.1.2"
83
+ "version": "2.1.4"
84
84
  }