dsh-wisp 1.33.1 → 1.33.2

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.md CHANGED
@@ -448,6 +448,34 @@ node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登
448
448
 
449
449
  ## 变更
450
450
 
451
+ ### 1.33.2
452
+
453
+ **修「她在屏幕角落时,子菜单跟着偏」。** 现象是:右下 / 左下时子面板整体**偏下**,
454
+ 右上时**压住主菜单**一部分。
455
+
456
+ 根因只有一句:**我用了菜单"请求"的位置,而不是"实际"的位置。**
457
+
458
+ 菜单在视口边缘会被**夹取**(`clamp` 进 `vw()/vh()`):
459
+
460
+ - 她在**右下 / 左下**时,菜单会被**往上推** → 实际 `top` 比请求的小 → 子面板却按请求的 `top` 算 → **偏下**;
461
+ - 她在**右上**时,菜单会被**往左推** → 实际 `left` 比请求的小 → 子面板按请求的 `left` 算 → 横向也偏 → **压住菜单**。
462
+
463
+ 而且越靠角落、夹取越多、偏得越厉害 —— 正是观察到的规律。
464
+
465
+ 改法就是读`menuEl.style.left/top`(菜单真实的图层坐标,夹取之后的值),
466
+ 横向仍用"菜单宽度 + 面板宽度"、纵向仍用"行相对菜单的偏移",这两者本来就与坐标系无关。
467
+
468
+ **顺便把这道防线补成四个角落各测一遍**(`tools/smoke.mjs`):她先站到每一角,
469
+ 再开菜单、悬停分组行、量三个矩形的相对关系,断言
470
+ 「面板在菜单**旁边**」「在视口内」「与它所属那一行**对齐**」。
471
+ 用户就是这样发现问题的,所以这条检查直接照着他的操作路径写。
472
+
473
+ 实测四个角落:面板始终在菜单旁边(右侧角落翻到左边),`panel.t` 与 `row.t` **完全相等**。
474
+
475
+ 冒烟 57 → **61 项**。
476
+
477
+ ### 1.33.1
478
+
451
479
  ### 1.33.1
452
480
 
453
481
  **修「子菜单遮住主菜单」。** 上一版的定位是错的,而且错得不显眼 —— 面板确实"在旁边",
package/lib/client.js CHANGED
@@ -51,7 +51,7 @@ window.__ModuleLoader__.load({
51
51
  var exports = module.exports
52
52
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
53
53
 
54
- const VERSION = '1.33.1'
54
+ const VERSION = '1.33.2'
55
55
 
56
56
  /* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
57
57
  忘了更新就会红,不会静默过期。 */
@@ -75,6 +75,7 @@ const WHATS_NEW = {
75
75
  '1.32.0': '分组改成鼠标悬停就展开、移开就收起(点一下则是钉住)。',
76
76
  '1.33.0': '子菜单改成在旁边弹出(真正的菜单栏形状),→/← 进出自如。',
77
77
  '1.33.1': '修子菜单位置算错、会压住主菜单的问题。',
78
+ '1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
78
79
  }
79
80
 
80
81
  /* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
@@ -2286,8 +2287,12 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
2286
2287
  const mr = menuEl && menuEl.getBoundingClientRect ? menuEl.getBoundingClientRect() : null
2287
2288
  if (mr && mr.width > 0) menuW = mr.width
2288
2289
  } catch (e) { /* 用常量兜底 */ }
2289
- const menuLeft = menuAtX
2290
- const menuTop = menuAtY
2290
+ /* 菜单的**实际**位置,不是"请求"的位置(menuAtX/menuAtY)。
2291
+ 菜单会被夹进视口:她站在右下/左下时菜单被往上推,站在右上时被往左推。
2292
+ 用请求坐标算出面板位置,就会跟着偏 —— 而且越靠角落偏得越多
2293
+ (这正是"她在右下角时子菜单更偏下、右上角时压住菜单"的原因)。 */
2294
+ const menuLeft = menuEl ? (Number.parseFloat(menuEl.style.left) || menuAtX) : menuAtX
2295
+ const menuTop = menuEl ? (Number.parseFloat(menuEl.style.top) || menuAtY) : menuAtY
2291
2296
  let x = menuLeft + menuW + 2
2292
2297
  flyoutSide = 'right'
2293
2298
  if (x + SUBMENU_W > vw() - 8) {
@@ -51,7 +51,7 @@ window.__ModuleLoader__.load({
51
51
  var exports = module.exports
52
52
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
53
53
 
54
- const VERSION = '1.33.1'
54
+ const VERSION = '1.33.2'
55
55
 
56
56
  /* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
57
57
  忘了更新就会红,不会静默过期。 */
@@ -75,6 +75,7 @@ const WHATS_NEW = {
75
75
  '1.32.0': '分组改成鼠标悬停就展开、移开就收起(点一下则是钉住)。',
76
76
  '1.33.0': '子菜单改成在旁边弹出(真正的菜单栏形状),→/← 进出自如。',
77
77
  '1.33.1': '修子菜单位置算错、会压住主菜单的问题。',
78
+ '1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
78
79
  }
79
80
 
80
81
  /* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
@@ -2235,8 +2236,12 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
2235
2236
  const mr = menuEl && menuEl.getBoundingClientRect ? menuEl.getBoundingClientRect() : null
2236
2237
  if (mr && mr.width > 0) menuW = mr.width
2237
2238
  } catch (e) { /* 用常量兜底 */ }
2238
- const menuLeft = menuAtX
2239
- const menuTop = menuAtY
2239
+ /* 菜单的**实际**位置,不是"请求"的位置(menuAtX/menuAtY)。
2240
+ 菜单会被夹进视口:她站在右下/左下时菜单被往上推,站在右上时被往左推。
2241
+ 用请求坐标算出面板位置,就会跟着偏 —— 而且越靠角落偏得越多
2242
+ (这正是"她在右下角时子菜单更偏下、右上角时压住菜单"的原因)。 */
2243
+ const menuLeft = menuEl ? (Number.parseFloat(menuEl.style.left) || menuAtX) : menuAtX
2244
+ const menuTop = menuEl ? (Number.parseFloat(menuEl.style.top) || menuAtY) : menuAtY
2240
2245
  let x = menuLeft + menuW + 2
2241
2246
  flyoutSide = 'right'
2242
2247
  if (x + SUBMENU_W > vw() - 8) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-wisp",
3
- "version": "1.33.1",
3
+ "version": "1.33.2",
4
4
  "description": "DeepSeek娘 — an unofficial floating desktop companion for the DeepSeek Harness UI. Zero dependencies; sprites inlined as data URIs; schedules on the Client timer service instead of the trapped browser timer globals.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
package/tools/smoke.mjs CHANGED
@@ -581,61 +581,64 @@ check(skinSwap.layersDuringFade === 1, 'the cross-fade layer is gone once it set
581
581
  // 每个页面都要装下这份包与解码后的精灵图(单张就是 naturalWidth*naturalHeight*4 字节),
582
582
  // 多开一个页面 = 多一份;之前正是这样把浏览器推崩的,表现为随机的
583
583
  // "Target page, context or browser has been closed",极难定位。
584
- /* 级联子菜单的几何检查:必须在**页面还活着**的时候跑(它要量真实布局),
585
- 也必须在键盘段之后(它会开关菜单、动焦点)。 */
586
- /* 级联子菜单的几何检查放在**最后**:它会开关菜单、动焦点,放在中间会扰动后面的键盘段。 */
587
- /* 级联子菜单的**几何**:它在旁边,不在上面。
588
- 这一条替身测不到(假 DOM 没有布局),而"子菜单遮住主菜单"那个 bug 正好出在这里 ——
589
- 混用了视口坐标(getBoundingClientRect)与图层坐标(菜单的 left/top)。 */
590
- const flyoutGeo = await page.evaluate(async () => {
591
- const body = document.querySelector('.wisp-body')
592
- const box = document.querySelector('.wisp-root')
593
- if (!body || !box) return { noBody: true }
594
- const r0 = box.getBoundingClientRect()
595
- body.dispatchEvent(new MouseEvent('contextmenu', {
596
- bubbles: true, cancelable: true, clientX: r0.left + r0.width / 2, clientY: r0.top + r0.height / 2,
597
- }))
598
- const menu = document.querySelector('.wisp-menu')
599
- if (!menu) return { noMenu: true }
600
- const row = [...menu.querySelectorAll('.wisp-menu-item')].find((el) => el.dataset && el.dataset.group)
601
- if (!row) return { noRow: true }
602
- row.dispatchEvent(new PointerEvent('pointerenter', { pointerType: 'mouse' }))
603
- await new Promise((resolve) => setTimeout(resolve, 350))
604
- const panel = document.querySelector('.wisp-submenu')
605
- if (!panel) return { noPanel: true }
606
- const mr = menu.getBoundingClientRect()
607
- const pr = panel.getBoundingClientRect()
608
- const rr = row.getBoundingClientRect()
609
- const layer = menu.parentNode
610
- const lr = layer.getBoundingClientRect ? layer.getBoundingClientRect() : { left: -999, top: -999, width: 0, height: 0 }
611
- return {
612
- menu: { l: Math.round(mr.left), r: Math.round(mr.right), w: Math.round(mr.width), styleL: menu.style.left, styleT: menu.style.top },
613
- panel: { l: Math.round(pr.left), r: Math.round(pr.right), t: Math.round(pr.top), w: Math.round(pr.width), styleL: panel.style.left, styleT: panel.style.top },
614
- row: { t: Math.round(rr.top), h: Math.round(rr.height) },
615
- layer: { l: Math.round(lr.left), t: Math.round(lr.top), w: Math.round(lr.width), h: Math.round(lr.height), pos: getComputedStyle(layer).position },
616
- view: { w: window.innerWidth, h: window.innerHeight },
617
- rows: panel.querySelectorAll('.wisp-menu-item').length,
618
- }
619
- })
620
- check(flyoutGeo.panel !== undefined && flyoutGeo.rows > 0,
621
- 'hovering a group row pops a submenu panel', JSON.stringify(flyoutGeo))
622
- check(flyoutGeo.panel !== undefined
623
- && (flyoutGeo.panel.l >= flyoutGeo.menu.r - 6 || flyoutGeo.panel.r <= flyoutGeo.menu.l + 6),
624
- 'the submenu sits BESIDE the menu, not on top of it — window coords and layer coords must not be mixed',
625
- flyoutGeo.panel ? `menu=${flyoutGeo.menu.l}..${flyoutGeo.menu.r} panel=${flyoutGeo.panel.l}..${flyoutGeo.panel.r}` : 'n/a')
626
- check(flyoutGeo.panel !== undefined
627
- && flyoutGeo.panel.l >= 0 && flyoutGeo.panel.r <= flyoutGeo.view.w
628
- && flyoutGeo.panel.t >= 0,
629
- 'and it fits on screen', flyoutGeo.panel ? JSON.stringify(flyoutGeo.panel) : 'n/a')
630
- check(flyoutGeo.panel !== undefined && Math.abs(flyoutGeo.panel.t - flyoutGeo.row.t) < 40,
631
- 'and lines up with the row it belongs to',
632
- flyoutGeo.panel ? `row.t=${flyoutGeo.row.t} panel.t=${flyoutGeo.panel.t}` : 'n/a')
633
- await page.evaluate(() => {
634
- /* 菜单的键盘监听挂在 **window** 上,不是 document —— 发错目标菜单就不会关,
635
- 后面的键盘段会在"一个还没关掉的菜单"上重开,焦点状态全乱(就是这么挂掉三条的)。 */
636
- window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
637
- window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
638
- })
584
+ /* 级联子菜单的几何:**四个角落各测一次**。
585
+ 她在角落里时菜单会被夹进视口(右下/左下往上推、右上往左推),
586
+ 子面板必须跟着菜单的**实际**位置走 —— 用"请求坐标"算就会越靠角落偏得越多,
587
+ 严重时压住主菜单。用户就是这样发现该 bug 的,所以这里直接把它编成回归检查。
588
+ 假 DOM 没有布局,这一整类问题只有真浏览器能测。 */
589
+ const cornerGeo = []
590
+ for (const which of ['br', 'bl', 'tr', 'tl']) {
591
+ const geo = await page.evaluate(async (corner) => {
592
+ const wisp = window.__wisp
593
+ const box = document.querySelector('.wisp-root')
594
+ const bw = parseFloat(box.style.width) || 560
595
+ const bh = parseFloat(box.style.height) || 840
596
+ const vw = window.innerWidth
597
+ const vh = window.innerHeight
598
+ const right = (corner === 'br' || corner === 'tr') ? 26 : Math.max(0, vw - 26 - bw)
599
+ const bottom = (corner === 'br' || corner === 'bl') ? 46 : Math.max(0, vh - 46 - bh)
600
+ wisp.configure({ right, bottom })
601
+ wisp.resetPosition()
602
+ await new Promise((resolve) => setTimeout(resolve, 120))
603
+ /* 主菜单的入口是 .wisp-body 上的 Enter —— contextmenu 只挂在"躲起来"之后的迷你标签上。 */
604
+ const body = document.querySelector('.wisp-body')
605
+ body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
606
+ const menu = document.querySelector('.wisp-menu')
607
+ if (!menu) return { which: corner, noMenu: true }
608
+ const row = [...menu.querySelectorAll('.wisp-menu-item')].find((el) => el.dataset && el.dataset.group)
609
+ if (!row) return { which: corner, noRow: true }
610
+ row.dispatchEvent(new PointerEvent('pointerenter', { pointerType: 'mouse' }))
611
+ await new Promise((resolve) => setTimeout(resolve, 320))
612
+ const panel = document.querySelector('.wisp-submenu')
613
+ if (!panel) return { which: corner, noPanel: true }
614
+ const mr = menu.getBoundingClientRect()
615
+ const pr = panel.getBoundingClientRect()
616
+ const rr = row.getBoundingClientRect()
617
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
618
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
619
+ return {
620
+ which: corner,
621
+ menu: { l: Math.round(mr.left), r: Math.round(mr.right), t: Math.round(mr.top) },
622
+ panel: { l: Math.round(pr.left), r: Math.round(pr.right), t: Math.round(pr.top) },
623
+ row: { t: Math.round(rr.top) },
624
+ vw, vh,
625
+ }
626
+ }, which)
627
+ cornerGeo.push(geo)
628
+ }
629
+ const withPanel = cornerGeo.filter((g) => g && g.panel)
630
+ check(withPanel.length === 4, 'the submenu opens from every corner',
631
+ cornerGeo.map((g) => g.which + (g.panel ? '' : '✗')).join(' '))
632
+ check(withPanel.every((g) => g.panel.r <= g.menu.l + 6 || g.panel.l >= g.menu.r - 6),
633
+ 'BESIDE the menu in every corner — never on top of it',
634
+ withPanel.map((g) => `${g.which} menu=${g.menu.l}..${g.menu.r} panel=${g.panel.l}..${g.panel.r}`).join(' | '))
635
+ check(withPanel.every((g) => g.panel.l >= 0 && g.panel.r <= g.vw && g.panel.t >= 0 && g.panel.t < g.vh),
636
+ 'and inside the viewport in every corner',
637
+ withPanel.map((g) => `${g.which} l=${g.panel.l} r=${g.panel.r} t=${g.panel.t}`).join(' | '))
638
+ check(withPanel.every((g) => Math.abs(g.panel.t - g.row.t) < 40),
639
+ 'and lined up with its own row in every corner',
640
+ withPanel.map((g) => `${g.which} row.t=${g.row.t} panel.t=${g.panel.t}`).join(' | '))
641
+
639
642
 
640
643
  await browser.close()
641
644