dsh-pocket 2.1.3 → 2.2.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.
package/README.en.md CHANGED
@@ -191,7 +191,7 @@ 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 / settings (91 tests)
194
+ npm test # proxy / auth / compression / tunnel / service / RPC / settings (93 tests)
195
195
  ```
196
196
 
197
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).
package/README.md CHANGED
@@ -192,7 +192,7 @@ npx @deepseek-ai/dsh web
192
192
  ```sh
193
193
  npm install
194
194
  node client/build.mjs # 改 client/ 后重新打包
195
- npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC / 设置(91 测试)
195
+ npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC / 设置(93 测试)
196
196
  ```
197
197
 
198
198
  **改完想在本机先试?** 不用发版:把插件换成指向本地仓库的软链,重启 dsh web 就是本地代码。完整步骤(含怎么换回 npm 官方版本)见 [LOCAL-DEV.md](./LOCAL-DEV.md)。
package/client/client.js CHANGED
@@ -535,14 +535,19 @@ var MOBILE_CSS = `
535
535
  it to 500), and when the layer outranks the drawer, the backdrop paints
536
536
  ABOVE the drawer and swallows every tap \u2014 the drawer opens but no row
537
537
  can be pressed (every tap just closes it). The drawer must therefore
538
- outrank any such raise: 600 clears the known 500 while staying under the
539
- 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. */
540
545
  [data-mobile-nav="frame"] > :first-child {
541
546
  position: absolute !important;
542
547
  inset: 0 auto 0 0 !important;
543
548
  width: max-content !important;
544
549
  max-width: 92vw !important;
545
- z-index: 600 !important;
550
+ z-index: 1200 !important;
546
551
  transform: translateX(-110%);
547
552
  transition: transform .28s var(--ds-ease-in-out, ease-in-out);
548
553
  background: var(--dsw-alias-bg-base, #ffffff);
@@ -571,6 +576,32 @@ var MOBILE_CSS = `
571
576
  transform: none !important;
572
577
  }
573
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
+
574
605
  /* Drag handles are useless on touch and would float over the drawer. */
575
606
  [data-side="sidebar"],
576
607
  [data-side="details"] {
@@ -1200,8 +1231,44 @@ var en = {
1200
1231
  "files": "Files"
1201
1232
  };
1202
1233
 
1234
+ // client/mobile/layout-mode.mjs
1235
+ function resolveLayout({ urlValue, stored, narrowMatch }) {
1236
+ const url = String(urlValue ?? "").trim();
1237
+ if (url === "desktop") return "desktop";
1238
+ if (url === "mobile") return "mobile";
1239
+ if (stored === "desktop" || stored === "mobile") return stored;
1240
+ return narrowMatch ? "mobile" : "desktop";
1241
+ }
1242
+ function persistLayoutFromUrl(urlValue) {
1243
+ if (typeof localStorage === "undefined") return "";
1244
+ const v = String(urlValue ?? "").trim();
1245
+ try {
1246
+ if (v === "desktop" || v === "mobile") localStorage.setItem("dsh-pocket.layout", v);
1247
+ else if (v === "auto" || v === "") localStorage.removeItem("dsh-pocket.layout");
1248
+ } catch {
1249
+ }
1250
+ try {
1251
+ const s = localStorage.getItem("dsh-pocket.layout");
1252
+ return s === "desktop" || s === "mobile" ? s : "";
1253
+ } catch {
1254
+ return "";
1255
+ }
1256
+ }
1257
+
1203
1258
  // client/mobile/mobile-apply.tsx
1204
1259
  function mobileApply(ctx) {
1260
+ const urlValue = new URL(window.location.href).searchParams.get("dsh-layout") ?? "";
1261
+ const narrowMQ = window.matchMedia("(max-width: 1023px)");
1262
+ const stored = persistLayoutFromUrl(urlValue);
1263
+ const layout = resolveLayout({ urlValue, stored, narrowMatch: narrowMQ.matches });
1264
+ document.body?.setAttribute("data-dsh-pocket-layout", layout);
1265
+ if (layout === "desktop") return;
1266
+ let narrow = narrowMQ;
1267
+ if (layout === "mobile") {
1268
+ narrow = { matches: true, addEventListener: () => {
1269
+ }, removeEventListener: () => {
1270
+ } };
1271
+ }
1205
1272
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-mobile-nav: dictionaries");
1206
1273
  ctx.effect(() => {
1207
1274
  const tag = document.createElement("style");
@@ -1214,7 +1281,6 @@ function mobileApply(ctx) {
1214
1281
  };
1215
1282
  }, "dsh-mobile-nav: styles");
1216
1283
  ctx.effect(() => {
1217
- const narrow = window.matchMedia("(max-width: 1023px)");
1218
1284
  const viewport = document.querySelector('meta[name="viewport"]');
1219
1285
  const originalViewport = viewport?.content ?? "";
1220
1286
  const themeMeta = document.createElement("meta");
@@ -1246,7 +1312,6 @@ function mobileApply(ctx) {
1246
1312
  };
1247
1313
  }, "dsh-mobile-nav: status bar theme + viewport + zoom guard");
1248
1314
  ctx.effect(() => {
1249
- const narrow = window.matchMedia("(max-width: 1023px)");
1250
1315
  if (!narrow.matches) return () => {
1251
1316
  };
1252
1317
  const onChevronClick = (event) => {
@@ -1258,7 +1323,6 @@ function mobileApply(ctx) {
1258
1323
  return () => document.removeEventListener("click", onChevronClick, true);
1259
1324
  }, "dsh-mobile-nav: aionui explorer close marker");
1260
1325
  ctx.effect(() => {
1261
- const narrow = window.matchMedia("(max-width: 1023px)");
1262
1326
  if (!narrow.matches) return () => {
1263
1327
  };
1264
1328
  const frame = () => document.querySelector('[data-mobile-nav="frame"]');
@@ -1276,7 +1340,6 @@ function mobileApply(ctx) {
1276
1340
  };
1277
1341
  }, "dsh-mobile-nav: explorer availability (issue #48)");
1278
1342
  ctx.effect(() => {
1279
- const narrow = window.matchMedia("(max-width: 1023px)");
1280
1343
  if (!narrow.matches) return () => {
1281
1344
  };
1282
1345
  const frame = () => document.querySelector('[data-mobile-nav="frame"]');
@@ -1301,7 +1364,6 @@ function mobileApply(ctx) {
1301
1364
  };
1302
1365
  }, "dsh-mobile-nav: preview sheet open marker");
1303
1366
  ctx.effect(() => {
1304
- const narrow = window.matchMedia("(max-width: 1023px)");
1305
1367
  if (!narrow.matches) return () => {
1306
1368
  };
1307
1369
  const moveTps = (stats) => {
@@ -1335,7 +1397,6 @@ function mobileApply(ctx) {
1335
1397
  };
1336
1398
  }, "dsh-mobile-nav: stats line marker");
1337
1399
  ctx.effect(() => {
1338
- const narrow = window.matchMedia("(max-width: 1023px)");
1339
1400
  if (!narrow.matches) return () => {
1340
1401
  };
1341
1402
  const cols = ["[data-aionui-explorer-col]", "[data-aionui-preview-col]"];
@@ -0,0 +1,46 @@
1
+ // 布局模式判定(issue #74:宽屏手机/Pad 可选电脑布局)
2
+ //
3
+ // 优先级:URL 参数 (?dsh-layout=desktop|mobile|auto) > localStorage > 默认 auto。
4
+ // - 'auto' 不写 localStorage,仅按 matchMedia 走;
5
+ // - 显式 'desktop' / 'mobile' 同步到 localStorage,下次无 URL 参数也走同样布局。
6
+ //
7
+ // 纯函数,便于单测;mobile-apply.tsx 读 DOM/localStorage 喂进来。
8
+
9
+ /** @typedef {'mobile' | 'desktop'} ForcedLayout */
10
+
11
+ /** 解析输入。 */
12
+ export function resolveLayout({ urlValue, stored, narrowMatch }) {
13
+ const url = String(urlValue ?? '').trim();
14
+ if (url === 'desktop') return 'desktop';
15
+ if (url === 'mobile') return 'mobile';
16
+ // 'auto' / 空 / 非法值 → 用 localStorage / matchMedia
17
+ if (stored === 'desktop' || stored === 'mobile') return stored;
18
+ return narrowMatch ? 'mobile' : 'desktop';
19
+ }
20
+
21
+ /** 同步 localStorage 的副作用(在 mobileApply 入口跑一次)。返回最终存储值。 */
22
+ export function persistLayoutFromUrl(urlValue) {
23
+ if (typeof localStorage === 'undefined') return '';
24
+ const v = String(urlValue ?? '').trim();
25
+ try {
26
+ if (v === 'desktop' || v === 'mobile') localStorage.setItem('dsh-pocket.layout', v);
27
+ else if (v === 'auto' || v === '') localStorage.removeItem('dsh-pocket.layout');
28
+ } catch { /* 隐私模式/无 storage → 静默 */ }
29
+ try {
30
+ const s = localStorage.getItem('dsh-pocket.layout');
31
+ return s === 'desktop' || s === 'mobile' ? s : '';
32
+ } catch {
33
+ return '';
34
+ }
35
+ }
36
+
37
+ /** 读 localStorage 当前的 layout 值('desktop' | 'mobile' | '')。 */
38
+ export function readStoredLayout() {
39
+ if (typeof localStorage === 'undefined') return '';
40
+ try {
41
+ const v = localStorage.getItem('dsh-pocket.layout');
42
+ return v === 'desktop' || v === 'mobile' ? v : '';
43
+ } catch {
44
+ return '';
45
+ }
46
+ }
@@ -6,6 +6,7 @@ import { MobileDrawerFooter } from './MobileDrawerFooter.tsx'
6
6
  import { MOBILE_CSS } from './mobile.css.ts'
7
7
  import { NS, en, zh } from './locales.ts'
8
8
  import type { MobileNavKey } from './locales.ts'
9
+ import { resolveLayout, persistLayoutFromUrl } from './layout-mode.mjs'
9
10
 
10
11
  declare module '@deepseek-ai/dsh-client-ui-slots' {
11
12
  interface LocaleNamespaceMap {
@@ -23,6 +24,22 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
23
24
  * @param ctx - client root context.
24
25
  */
25
26
  export function mobileApply(ctx): void {
27
+ // 布局模式(issue #74):URL 参数 > localStorage > auto(=matchMedia)。
28
+ // desktop 模式(宽屏手机/平板强制电脑布局)下整段 mobile 效果都不挂——
29
+ // 不加 styles、不挂 slots、不跑 effects,直接走 DSH 原生桌面 UI。
30
+ const urlValue = new URL(window.location.href).searchParams.get('dsh-layout') ?? '';
31
+ const narrowMQ = window.matchMedia('(max-width: 1023px)');
32
+ const stored = persistLayoutFromUrl(urlValue);
33
+ const layout = resolveLayout({ urlValue, stored, narrowMatch: narrowMQ.matches });
34
+ document.body?.setAttribute('data-dsh-pocket-layout', layout);
35
+ if (layout === 'desktop') return;
36
+ // 强制 mobile:narrow 永远 true;宽度变化不再切换(用户已显式选 mobile)
37
+ // auto 模式:narrow 是真实的 matchMedia,宽度变化会触发 effect 挂载/卸载
38
+ let narrow: MediaQueryList = narrowMQ;
39
+ if (layout === 'mobile') {
40
+ narrow = { matches: true, addEventListener: () => {}, removeEventListener: () => {} } as MediaQueryList;
41
+ }
42
+
26
43
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-mobile-nav: dictionaries')
27
44
 
28
45
  ctx.effect(() => {
@@ -52,7 +69,6 @@ export function mobileApply(ctx): void {
52
69
  // zoom; modern browsers are covered by the stylesheet's
53
70
  // touch-action: manipulation (which keeps pan and pinch zoom).
54
71
  ctx.effect(() => {
55
- const narrow = window.matchMedia('(max-width: 1023px)')
56
72
  const viewport = document.querySelector<HTMLMetaElement>('meta[name="viewport"]')
57
73
  const originalViewport = viewport?.content ?? ''
58
74
  const themeMeta = document.createElement('meta')
@@ -96,7 +112,6 @@ export function mobileApply(ctx): void {
96
112
  // sheet's own collapse chevron is tapped, so closing is symmetric with
97
113
  // opening.
98
114
  ctx.effect(() => {
99
- const narrow = window.matchMedia('(max-width: 1023px)')
100
115
  if (!narrow.matches) return () => {}
101
116
  const onChevronClick = (event: MouseEvent) => {
102
117
  const target = event.target as HTMLElement | null
@@ -115,7 +130,6 @@ export function mobileApply(ctx): void {
115
130
  // `data-mobile-nav-explorer="1|0"` so the stylesheet can hide the entries
116
131
  // on hosts without it (dsh-web-ui installs keep the feature).
117
132
  ctx.effect(() => {
118
- const narrow = window.matchMedia('(max-width: 1023px)')
119
133
  if (!narrow.matches) return () => {}
120
134
  const frame = (): HTMLElement | null => document.querySelector('[data-mobile-nav="frame"]')
121
135
  const check = () => {
@@ -141,7 +155,6 @@ export function mobileApply(ctx): void {
141
155
  // whenever the suite hides the column again (collapse chevron / tab
142
156
  // close), so a restored-but-unwanted sheet never appears.
143
157
  ctx.effect(() => {
144
- const narrow = window.matchMedia('(max-width: 1023px)')
145
158
  if (!narrow.matches) return () => {}
146
159
  const frame = (): HTMLElement | null => document.querySelector('[data-mobile-nav="frame"]')
147
160
  const onTap = (event: MouseEvent) => {
@@ -173,7 +186,6 @@ export function mobileApply(ctx): void {
173
186
  // marked row out as ONE horizontally scrolling line with every metric
174
187
  // reachable.
175
188
  ctx.effect(() => {
176
- const narrow = window.matchMedia('(max-width: 1023px)')
177
189
  if (!narrow.matches) return () => {}
178
190
  // The composer root renders the TPS readout ("TPS 89.4 tok/s") as its
179
191
  // own row BELOW the status strip; fold it into the strip so every
@@ -218,7 +230,6 @@ export function mobileApply(ctx): void {
218
230
  // with the Web Animations API each time a column turns visible, then
219
231
  // leave the resting state to the stylesheet.
220
232
  ctx.effect(() => {
221
- const narrow = window.matchMedia('(max-width: 1023px)')
222
233
  if (!narrow.matches) return () => {}
223
234
  const cols = ['[data-aionui-explorer-col]', '[data-aionui-preview-col]']
224
235
  const seen = new Map<string, boolean>()
@@ -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"] {
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.3"
83
+ "version": "2.2.0"
84
84
  }