dsh-wisp 1.31.0 → 1.32.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.md +28 -0
- package/lib/client.js +88 -6
- package/lib/client.template.js +88 -6
- package/package.json +1 -1
- package/verify-wisp.mjs +110 -0
package/README.md
CHANGED
|
@@ -448,6 +448,34 @@ node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登
|
|
|
448
448
|
|
|
449
449
|
## 变更
|
|
450
450
|
|
|
451
|
+
### 1.32.0
|
|
452
|
+
|
|
453
|
+
**分组改成悬停展开、移开收起**(点一下仍然是"钉住")。
|
|
454
|
+
|
|
455
|
+
这一条本来就是标准交互,上一版只做了"点开/点关",属于设计盲区。补上之后,
|
|
456
|
+
真正要小心的地方不在于"加个 mouseenter",而在于下面这几条 —— 少一条都比不做更糟:
|
|
457
|
+
|
|
458
|
+
| 细节 | 为什么必须 |
|
|
459
|
+
|---|---|
|
|
460
|
+
| **展开有延迟**(140 ms) | 鼠标只是**路过**那一行就展开 = 抖动。这就是 hover intent。 |
|
|
461
|
+
| **移开有宽限**(260 ms) | 斜着往子项移动时,中途不能收。 |
|
|
462
|
+
| **子项与标题在同一个悬停区**(`div.wisp-menu-group`) | 从标题移到子项**不该算移开**。这是这类交互最常见的翻车点。 |
|
|
463
|
+
| **点击 = 钉住** | 悬停是预览、点击是决定。点开的那一组鼠标移开也不关,再点一次才收起。 |
|
|
464
|
+
| **触屏不触发悬停** | 判 `pointerType`。不判的话手指一碰就会莫名其妙展开。 |
|
|
465
|
+
| **焦点在那一组里时不关** | 否则键盘用户正操作着,鼠标一动组就没了。 |
|
|
466
|
+
| **手风琴** | 悬停到另一组会解除上一组的钉住 —— 同时开两组看不清。 |
|
|
467
|
+
|
|
468
|
+
**顺手抓到一个真 bug**:焦点保护第一版写的是"焦点还在菜单里就不许关" ——
|
|
469
|
+
而菜单**一打开就会把焦点放到第一项**,所以那条件**永远为真**,
|
|
470
|
+
悬停收起等于**永远不会触发**。改成只看"焦点在不在**这一组**里"。
|
|
471
|
+
第一版还用了一个"组名 → 元素"的映射,实测和重画后的 DOM 对不上(组照样关);
|
|
472
|
+
现在改成从焦点**向上走**、遇到带 `data-group-block` 的祖先就算命中 —— 不依赖任何外部映射。
|
|
473
|
+
|
|
474
|
+
预检 502 → **524 项**:延迟、宽限、路过不展开、子项在同一悬停区、
|
|
475
|
+
触屏不悬停但点击可用、点击钉住、再点收起、手风琴、焦点保护、焦点离开后规则恢复。
|
|
476
|
+
|
|
477
|
+
### 1.31.0
|
|
478
|
+
|
|
451
479
|
### 1.31.0
|
|
452
480
|
|
|
453
481
|
**菜单重做。** 上一版只是"把 19 个条目收进三个分组" —— 那是分类,不是交互设计。
|
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.
|
|
54
|
+
const VERSION = '1.32.0'
|
|
55
55
|
|
|
56
56
|
/* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
|
|
57
57
|
忘了更新就会红,不会静默过期。 */
|
|
@@ -72,6 +72,7 @@ const WHATS_NEW = {
|
|
|
72
72
|
'1.29.0': '夜深了她会劝你去睡;你走开再回来她记得;连续来了几天她也记着。',
|
|
73
73
|
'1.30.0': '会陪你专注计时了;页面藏起来时她会安静下来,不再空转。',
|
|
74
74
|
'1.31.0': '菜单重做:状态一目了然,开关是开关,大小能直接拖。',
|
|
75
|
+
'1.32.0': '分组改成鼠标悬停就展开、移开就收起(点一下则是钉住)。',
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
|
|
@@ -2013,12 +2014,67 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2013
2014
|
/* 折叠分组要"在原地重开",所以记住这次的落点 */
|
|
2014
2015
|
let menuAtX = 0
|
|
2015
2016
|
let menuAtY = 0
|
|
2017
|
+
/* 悬停展开的延迟:鼠标路过一行就展开会抖(hover intent),
|
|
2018
|
+
关闭的宽限更长一点 —— 斜着往子项移动时才不会中途收起。 */
|
|
2019
|
+
const HOVER_OPEN_MS = 140
|
|
2020
|
+
const HOVER_CLOSE_MS = 260
|
|
2021
|
+
/* 点开的组会被"钉住":鼠标移开也不关。悬停是预览,点击是决定。 */
|
|
2022
|
+
let pinnedGroup = ''
|
|
2023
|
+
let groupOpenTimer = null
|
|
2024
|
+
let groupCloseTimer = null
|
|
2025
|
+
/* 判断"焦点是否在这一组里"不能笼统地看"焦点是否在菜单里" —— 菜单一打开焦点就在
|
|
2026
|
+
第一项上,那样写等于悬停收起永远不会触发。 */
|
|
2027
|
+
|
|
2028
|
+
const clearGroupTimers = () => {
|
|
2029
|
+
if (groupOpenTimer) { clock.cancel(groupOpenTimer); groupOpenTimer = null }
|
|
2030
|
+
if (groupCloseTimer) { clock.cancel(groupCloseTimer); groupCloseTimer = null }
|
|
2031
|
+
}
|
|
2032
|
+
/* 焦点是否在这**一组**里 —— 键盘用户正操作这一组时,鼠标移开不该把它收掉。
|
|
2033
|
+
焦点在别的行、或压根不在菜单里,都不该阻止收起。
|
|
2034
|
+
判法:从焦点向上走,遇到带 data-group-block 的祖先就算命中(不靠外部映射)。 */
|
|
2035
|
+
const focusInsideGroup = (name) => {
|
|
2036
|
+
let el = null
|
|
2037
|
+
try { el = document.activeElement } catch (e) { el = null }
|
|
2038
|
+
let hops = 0
|
|
2039
|
+
while (el && hops < 24) {
|
|
2040
|
+
if (el.dataset && el.dataset.groupBlock === name) return true
|
|
2041
|
+
el = el.parentNode || null
|
|
2042
|
+
hops++
|
|
2043
|
+
}
|
|
2044
|
+
return false
|
|
2045
|
+
}
|
|
2046
|
+
/* 悬停到一组:延迟后展开它,并解除别组的钉住(手风琴 —— 同时开两组会看不清)。 */
|
|
2047
|
+
const scheduleGroupOpen = (name) => {
|
|
2048
|
+
clearGroupTimers()
|
|
2049
|
+
if (activeGroup === name) return
|
|
2050
|
+
groupOpenTimer = clock.after(HOVER_OPEN_MS, () => {
|
|
2051
|
+
groupOpenTimer = null
|
|
2052
|
+
if (destroyed || menuEl === null) return
|
|
2053
|
+
pinnedGroup = ''
|
|
2054
|
+
activeGroup = name
|
|
2055
|
+
openMenu(menuAtX, menuAtY, true)
|
|
2056
|
+
})
|
|
2057
|
+
}
|
|
2058
|
+
const scheduleGroupClose = (name) => {
|
|
2059
|
+
clearGroupTimers()
|
|
2060
|
+
if (activeGroup !== name) return
|
|
2061
|
+
if (pinnedGroup === name) return // 点开的不随鼠标移开关闭
|
|
2062
|
+
groupCloseTimer = clock.after(HOVER_CLOSE_MS, () => {
|
|
2063
|
+
groupCloseTimer = null
|
|
2064
|
+
if (destroyed || menuEl === null) return
|
|
2065
|
+
if (pinnedGroup === name) return
|
|
2066
|
+
if (focusInsideGroup(name)) return // 键盘用户还在这一组里,别关
|
|
2067
|
+
activeGroup = ''
|
|
2068
|
+
openMenu(menuAtX, menuAtY, true)
|
|
2069
|
+
})
|
|
2070
|
+
}
|
|
2016
2071
|
|
|
2017
2072
|
const closeMenu = () => {
|
|
2018
2073
|
if (menuEl && menuEl.parentNode) menuEl.parentNode.removeChild(menuEl)
|
|
2019
2074
|
menuEl = null
|
|
2020
2075
|
menuButtons = []
|
|
2021
2076
|
menuFocusAt = -1
|
|
2077
|
+
clearGroupTimers()
|
|
2022
2078
|
if (menuOff) { menuOff(); menuOff = null }
|
|
2023
2079
|
}
|
|
2024
2080
|
|
|
@@ -2327,7 +2383,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2327
2383
|
const openMenu = (x, y, keepGroup) => {
|
|
2328
2384
|
menuAtX = x
|
|
2329
2385
|
menuAtY = y
|
|
2330
|
-
if (keepGroup !== true) activeGroup = ''
|
|
2386
|
+
if (keepGroup !== true) { activeGroup = ''; pinnedGroup = '' }
|
|
2331
2387
|
closeMenu()
|
|
2332
2388
|
stopGlide()
|
|
2333
2389
|
const items = menuItems()
|
|
@@ -2541,8 +2597,13 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2541
2597
|
continue
|
|
2542
2598
|
}
|
|
2543
2599
|
if (spec.group !== undefined) {
|
|
2544
|
-
/*
|
|
2600
|
+
/* 折叠分组:标题行 + (展开时的)子项,**包在同一个悬停区里**。
|
|
2601
|
+
这是悬停展开能做对的前提 —— 从标题移到子项不该算"移开",
|
|
2602
|
+
否则鼠标一动子菜单就没了(那是这类交互最常见的翻车点)。 */
|
|
2545
2603
|
const open = activeGroup === spec.group
|
|
2604
|
+
const block = document.createElement('div')
|
|
2605
|
+
block.className = 'wisp-menu-group'
|
|
2606
|
+
block.dataset.groupBlock = spec.group
|
|
2546
2607
|
const btn = document.createElement('button')
|
|
2547
2608
|
btn.className = 'wisp-menu-item wisp-menu-row'
|
|
2548
2609
|
btn.setAttribute('type', 'button')
|
|
@@ -2564,16 +2625,37 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2564
2625
|
btn.addEventListener('pointerdown', (ev) => ev.stopPropagation())
|
|
2565
2626
|
btn.addEventListener('click', (ev) => {
|
|
2566
2627
|
ev.preventDefault(); ev.stopPropagation()
|
|
2567
|
-
activeGroup
|
|
2628
|
+
if (activeGroup === spec.group && pinnedGroup === spec.group) {
|
|
2629
|
+
/* 已经点开过:再点一次收起 */
|
|
2630
|
+
pinnedGroup = ''
|
|
2631
|
+
activeGroup = ''
|
|
2632
|
+
} else {
|
|
2633
|
+
/* 点开 = 钉住:之后鼠标移开也不关(键盘、触屏、以及"我就想它开着"都靠这条) */
|
|
2634
|
+
pinnedGroup = spec.group
|
|
2635
|
+
activeGroup = spec.group
|
|
2636
|
+
}
|
|
2568
2637
|
openMenu(menuAtX, menuAtY, true)
|
|
2569
2638
|
})
|
|
2570
|
-
|
|
2639
|
+
/* 悬停展开 / 移开收起。pointerType 不为 mouse 的一律不管 ——
|
|
2640
|
+
触屏没有悬停,不判这个的话手指一碰就会莫名其妙展开。 */
|
|
2641
|
+
const isMouse = (ev) => !(ev && ev.pointerType && ev.pointerType !== 'mouse')
|
|
2642
|
+
block.addEventListener('pointerenter', (ev) => {
|
|
2643
|
+
if (!isMouse(ev)) return
|
|
2644
|
+
scheduleGroupOpen(spec.group)
|
|
2645
|
+
})
|
|
2646
|
+
block.addEventListener('pointerleave', (ev) => {
|
|
2647
|
+
if (!isMouse(ev)) return
|
|
2648
|
+
scheduleGroupClose(spec.group)
|
|
2649
|
+
})
|
|
2650
|
+
block.appendChild(btn)
|
|
2651
|
+
menu.appendChild(block)
|
|
2571
2652
|
menuButtons.push(btn)
|
|
2572
2653
|
if (!open) continue
|
|
2573
2654
|
const wrap = document.createElement('div')
|
|
2574
2655
|
wrap.className = 'wisp-menu-sub'
|
|
2575
2656
|
renderChildren(spec.children, wrap)
|
|
2576
|
-
menu
|
|
2657
|
+
/* 子项进 block(悬停区),不是直接进 menu */
|
|
2658
|
+
block.appendChild(wrap)
|
|
2577
2659
|
continue
|
|
2578
2660
|
}
|
|
2579
2661
|
actionRow(spec, menu)
|
package/lib/client.template.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.
|
|
54
|
+
const VERSION = '1.32.0'
|
|
55
55
|
|
|
56
56
|
/* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
|
|
57
57
|
忘了更新就会红,不会静默过期。 */
|
|
@@ -72,6 +72,7 @@ const WHATS_NEW = {
|
|
|
72
72
|
'1.29.0': '夜深了她会劝你去睡;你走开再回来她记得;连续来了几天她也记着。',
|
|
73
73
|
'1.30.0': '会陪你专注计时了;页面藏起来时她会安静下来,不再空转。',
|
|
74
74
|
'1.31.0': '菜单重做:状态一目了然,开关是开关,大小能直接拖。',
|
|
75
|
+
'1.32.0': '分组改成鼠标悬停就展开、移开就收起(点一下则是钉住)。',
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
|
|
@@ -1962,12 +1963,67 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1962
1963
|
/* 折叠分组要"在原地重开",所以记住这次的落点 */
|
|
1963
1964
|
let menuAtX = 0
|
|
1964
1965
|
let menuAtY = 0
|
|
1966
|
+
/* 悬停展开的延迟:鼠标路过一行就展开会抖(hover intent),
|
|
1967
|
+
关闭的宽限更长一点 —— 斜着往子项移动时才不会中途收起。 */
|
|
1968
|
+
const HOVER_OPEN_MS = 140
|
|
1969
|
+
const HOVER_CLOSE_MS = 260
|
|
1970
|
+
/* 点开的组会被"钉住":鼠标移开也不关。悬停是预览,点击是决定。 */
|
|
1971
|
+
let pinnedGroup = ''
|
|
1972
|
+
let groupOpenTimer = null
|
|
1973
|
+
let groupCloseTimer = null
|
|
1974
|
+
/* 判断"焦点是否在这一组里"不能笼统地看"焦点是否在菜单里" —— 菜单一打开焦点就在
|
|
1975
|
+
第一项上,那样写等于悬停收起永远不会触发。 */
|
|
1976
|
+
|
|
1977
|
+
const clearGroupTimers = () => {
|
|
1978
|
+
if (groupOpenTimer) { clock.cancel(groupOpenTimer); groupOpenTimer = null }
|
|
1979
|
+
if (groupCloseTimer) { clock.cancel(groupCloseTimer); groupCloseTimer = null }
|
|
1980
|
+
}
|
|
1981
|
+
/* 焦点是否在这**一组**里 —— 键盘用户正操作这一组时,鼠标移开不该把它收掉。
|
|
1982
|
+
焦点在别的行、或压根不在菜单里,都不该阻止收起。
|
|
1983
|
+
判法:从焦点向上走,遇到带 data-group-block 的祖先就算命中(不靠外部映射)。 */
|
|
1984
|
+
const focusInsideGroup = (name) => {
|
|
1985
|
+
let el = null
|
|
1986
|
+
try { el = document.activeElement } catch (e) { el = null }
|
|
1987
|
+
let hops = 0
|
|
1988
|
+
while (el && hops < 24) {
|
|
1989
|
+
if (el.dataset && el.dataset.groupBlock === name) return true
|
|
1990
|
+
el = el.parentNode || null
|
|
1991
|
+
hops++
|
|
1992
|
+
}
|
|
1993
|
+
return false
|
|
1994
|
+
}
|
|
1995
|
+
/* 悬停到一组:延迟后展开它,并解除别组的钉住(手风琴 —— 同时开两组会看不清)。 */
|
|
1996
|
+
const scheduleGroupOpen = (name) => {
|
|
1997
|
+
clearGroupTimers()
|
|
1998
|
+
if (activeGroup === name) return
|
|
1999
|
+
groupOpenTimer = clock.after(HOVER_OPEN_MS, () => {
|
|
2000
|
+
groupOpenTimer = null
|
|
2001
|
+
if (destroyed || menuEl === null) return
|
|
2002
|
+
pinnedGroup = ''
|
|
2003
|
+
activeGroup = name
|
|
2004
|
+
openMenu(menuAtX, menuAtY, true)
|
|
2005
|
+
})
|
|
2006
|
+
}
|
|
2007
|
+
const scheduleGroupClose = (name) => {
|
|
2008
|
+
clearGroupTimers()
|
|
2009
|
+
if (activeGroup !== name) return
|
|
2010
|
+
if (pinnedGroup === name) return // 点开的不随鼠标移开关闭
|
|
2011
|
+
groupCloseTimer = clock.after(HOVER_CLOSE_MS, () => {
|
|
2012
|
+
groupCloseTimer = null
|
|
2013
|
+
if (destroyed || menuEl === null) return
|
|
2014
|
+
if (pinnedGroup === name) return
|
|
2015
|
+
if (focusInsideGroup(name)) return // 键盘用户还在这一组里,别关
|
|
2016
|
+
activeGroup = ''
|
|
2017
|
+
openMenu(menuAtX, menuAtY, true)
|
|
2018
|
+
})
|
|
2019
|
+
}
|
|
1965
2020
|
|
|
1966
2021
|
const closeMenu = () => {
|
|
1967
2022
|
if (menuEl && menuEl.parentNode) menuEl.parentNode.removeChild(menuEl)
|
|
1968
2023
|
menuEl = null
|
|
1969
2024
|
menuButtons = []
|
|
1970
2025
|
menuFocusAt = -1
|
|
2026
|
+
clearGroupTimers()
|
|
1971
2027
|
if (menuOff) { menuOff(); menuOff = null }
|
|
1972
2028
|
}
|
|
1973
2029
|
|
|
@@ -2276,7 +2332,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2276
2332
|
const openMenu = (x, y, keepGroup) => {
|
|
2277
2333
|
menuAtX = x
|
|
2278
2334
|
menuAtY = y
|
|
2279
|
-
if (keepGroup !== true) activeGroup = ''
|
|
2335
|
+
if (keepGroup !== true) { activeGroup = ''; pinnedGroup = '' }
|
|
2280
2336
|
closeMenu()
|
|
2281
2337
|
stopGlide()
|
|
2282
2338
|
const items = menuItems()
|
|
@@ -2490,8 +2546,13 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2490
2546
|
continue
|
|
2491
2547
|
}
|
|
2492
2548
|
if (spec.group !== undefined) {
|
|
2493
|
-
/*
|
|
2549
|
+
/* 折叠分组:标题行 + (展开时的)子项,**包在同一个悬停区里**。
|
|
2550
|
+
这是悬停展开能做对的前提 —— 从标题移到子项不该算"移开",
|
|
2551
|
+
否则鼠标一动子菜单就没了(那是这类交互最常见的翻车点)。 */
|
|
2494
2552
|
const open = activeGroup === spec.group
|
|
2553
|
+
const block = document.createElement('div')
|
|
2554
|
+
block.className = 'wisp-menu-group'
|
|
2555
|
+
block.dataset.groupBlock = spec.group
|
|
2495
2556
|
const btn = document.createElement('button')
|
|
2496
2557
|
btn.className = 'wisp-menu-item wisp-menu-row'
|
|
2497
2558
|
btn.setAttribute('type', 'button')
|
|
@@ -2513,16 +2574,37 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2513
2574
|
btn.addEventListener('pointerdown', (ev) => ev.stopPropagation())
|
|
2514
2575
|
btn.addEventListener('click', (ev) => {
|
|
2515
2576
|
ev.preventDefault(); ev.stopPropagation()
|
|
2516
|
-
activeGroup
|
|
2577
|
+
if (activeGroup === spec.group && pinnedGroup === spec.group) {
|
|
2578
|
+
/* 已经点开过:再点一次收起 */
|
|
2579
|
+
pinnedGroup = ''
|
|
2580
|
+
activeGroup = ''
|
|
2581
|
+
} else {
|
|
2582
|
+
/* 点开 = 钉住:之后鼠标移开也不关(键盘、触屏、以及"我就想它开着"都靠这条) */
|
|
2583
|
+
pinnedGroup = spec.group
|
|
2584
|
+
activeGroup = spec.group
|
|
2585
|
+
}
|
|
2517
2586
|
openMenu(menuAtX, menuAtY, true)
|
|
2518
2587
|
})
|
|
2519
|
-
|
|
2588
|
+
/* 悬停展开 / 移开收起。pointerType 不为 mouse 的一律不管 ——
|
|
2589
|
+
触屏没有悬停,不判这个的话手指一碰就会莫名其妙展开。 */
|
|
2590
|
+
const isMouse = (ev) => !(ev && ev.pointerType && ev.pointerType !== 'mouse')
|
|
2591
|
+
block.addEventListener('pointerenter', (ev) => {
|
|
2592
|
+
if (!isMouse(ev)) return
|
|
2593
|
+
scheduleGroupOpen(spec.group)
|
|
2594
|
+
})
|
|
2595
|
+
block.addEventListener('pointerleave', (ev) => {
|
|
2596
|
+
if (!isMouse(ev)) return
|
|
2597
|
+
scheduleGroupClose(spec.group)
|
|
2598
|
+
})
|
|
2599
|
+
block.appendChild(btn)
|
|
2600
|
+
menu.appendChild(block)
|
|
2520
2601
|
menuButtons.push(btn)
|
|
2521
2602
|
if (!open) continue
|
|
2522
2603
|
const wrap = document.createElement('div')
|
|
2523
2604
|
wrap.className = 'wisp-menu-sub'
|
|
2524
2605
|
renderChildren(spec.children, wrap)
|
|
2525
|
-
menu
|
|
2606
|
+
/* 子项进 block(悬停区),不是直接进 menu */
|
|
2607
|
+
block.appendChild(wrap)
|
|
2526
2608
|
continue
|
|
2527
2609
|
}
|
|
2528
2610
|
actionRow(spec, menu)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-wisp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.0",
|
|
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/verify-wisp.mjs
CHANGED
|
@@ -2454,6 +2454,116 @@ if (clientSrc !== null) {
|
|
|
2454
2454
|
'and the corner she is actually in is marked',
|
|
2455
2455
|
menuOf().querySelectorAll('.wisp-corner').map((b) => b.textContent + (b.dataset.current ? '*' : '')).join(' '))
|
|
2456
2456
|
|
|
2457
|
+
/* ---- 悬停展开 / 移开收起 ------------------------------------------------
|
|
2458
|
+
这是"指针类"交互。替身里没有真鼠标,但它有**事件派发 + 虚拟时钟**,
|
|
2459
|
+
所以这套逻辑能被完整测到:派 pointerenter、推进时钟、看菜单结构变没变。
|
|
2460
|
+
测不到的是浏览器的命中测试本身("指针真的在这一行上")—— 那部分由冒烟里的
|
|
2461
|
+
真 Chromium 兜着,这里测的是**状态机**:延迟、宽限、钉住、触屏、焦点保护。 */
|
|
2462
|
+
const hoverBlock = (name) => menuHost().querySelectorAll('.wisp-menu-group')
|
|
2463
|
+
.find((el) => el.dataset && el.dataset.groupBlock === name && el.removed !== true)
|
|
2464
|
+
const hoverIn = (name, pointerType) => hoverBlock(name).dispatch('pointerenter', { pointerType })
|
|
2465
|
+
const hoverOut = (name, pointerType) => hoverBlock(name).dispatch('pointerleave', { pointerType })
|
|
2466
|
+
const isOpen = (name) => {
|
|
2467
|
+
const row = groupRow(name)
|
|
2468
|
+
return row !== undefined && row.dataset.open === 'true'
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
ensureCollapsed('外观'); ensureCollapsed('行为'); ensureCollapsed('位置')
|
|
2472
|
+
check(!isOpen('外观') && !isOpen('行为'), 'every group starts collapsed')
|
|
2473
|
+
|
|
2474
|
+
/* 悬停要**有延迟**:鼠标只是路过就展开会抖 —— 这是 hover intent,不是可有可无的润色 */
|
|
2475
|
+
hoverIn('外观', 'mouse')
|
|
2476
|
+
check(!isOpen('外观'), 'hovering does not open it instantly (a passing mouse would twitch it)')
|
|
2477
|
+
h.advance(200, 40)
|
|
2478
|
+
check(isOpen('外观'), 'after the intent delay it opens')
|
|
2479
|
+
|
|
2480
|
+
/* 移开也要有宽限:斜着往子项移动时,中途不能收 */
|
|
2481
|
+
hoverOut('外观', 'mouse')
|
|
2482
|
+
h.advance(80, 40)
|
|
2483
|
+
check(isOpen('外观'), 'leaving does not close it instantly either')
|
|
2484
|
+
h.advance(400, 40)
|
|
2485
|
+
check(!isOpen('外观'), 'after the grace period it closes')
|
|
2486
|
+
|
|
2487
|
+
/* 路过不展开:进去又马上出来,推进很久也不该开过 */
|
|
2488
|
+
hoverIn('行为', 'mouse')
|
|
2489
|
+
hoverOut('行为', 'mouse')
|
|
2490
|
+
h.advance(800, 40)
|
|
2491
|
+
check(!isOpen('行为'), 'a mouse that just passes over never opens a group')
|
|
2492
|
+
|
|
2493
|
+
/* 子项在**同一个悬停区**里 —— 这是"从标题移到子项不会关"的实现方式 */
|
|
2494
|
+
hoverIn('外观', 'mouse')
|
|
2495
|
+
h.advance(200, 40)
|
|
2496
|
+
const blockEl = hoverBlock('外观')
|
|
2497
|
+
const kids = blockEl.querySelector ? blockEl.querySelector('.wisp-menu-sub') : null
|
|
2498
|
+
check(kids !== null && kids.parentNode === blockEl,
|
|
2499
|
+
'the children sit inside the same hover area, so moving into them is not leaving',
|
|
2500
|
+
kids === null ? '(没有子项容器)' : String(kids.parentNode === blockEl))
|
|
2501
|
+
|
|
2502
|
+
/* 触屏没有悬停:不判 pointerType 的话,手指一碰就会莫名其妙展开 */
|
|
2503
|
+
hoverOut('外观', 'mouse')
|
|
2504
|
+
h.advance(400, 40)
|
|
2505
|
+
hoverIn('外观', 'touch')
|
|
2506
|
+
h.advance(600, 40)
|
|
2507
|
+
check(!isOpen('外观'), 'a touch pointer never opens a group by hovering')
|
|
2508
|
+
groupRow('外观').dispatch('click', itemEvent())
|
|
2509
|
+
check(isOpen('外观'), 'but tapping it still works')
|
|
2510
|
+
|
|
2511
|
+
/* 点击 = 钉住:鼠标移开也不关(悬停是预览,点击是决定) */
|
|
2512
|
+
hoverOut('外观', 'mouse')
|
|
2513
|
+
h.advance(800, 40)
|
|
2514
|
+
check(isOpen('外观'), 'a group you clicked stays open when the mouse leaves')
|
|
2515
|
+
groupRow('外观').dispatch('click', itemEvent())
|
|
2516
|
+
check(!isOpen('外观'), 'clicking it again collapses it')
|
|
2517
|
+
|
|
2518
|
+
/* 手风琴:悬停到另一组时,被钉住的那一组会让位 */
|
|
2519
|
+
groupRow('外观').dispatch('click', itemEvent())
|
|
2520
|
+
check(isOpen('外观'), 'pin the appearance group again')
|
|
2521
|
+
hoverIn('行为', 'mouse')
|
|
2522
|
+
h.advance(200, 40)
|
|
2523
|
+
check(isOpen('行为') && !isOpen('外观'),
|
|
2524
|
+
'hovering another group opens it and lets the pinned one go',
|
|
2525
|
+
`外观=${isOpen('外观')} 行为=${isOpen('行为')}`)
|
|
2526
|
+
hoverOut('行为', 'mouse')
|
|
2527
|
+
h.advance(600, 40)
|
|
2528
|
+
check(!isOpen('行为'), 'and it closes again after the grace')
|
|
2529
|
+
|
|
2530
|
+
/* 焦点在这一组里时,鼠标移开不该把它收掉(否则键盘用户正操作着,鼠标一动就没了) */
|
|
2531
|
+
hoverIn('外观', 'mouse')
|
|
2532
|
+
h.advance(200, 40)
|
|
2533
|
+
check(isOpen('外观'), 'hover it open again (not pinned this time)')
|
|
2534
|
+
const focusGroupRow = () => {
|
|
2535
|
+
pressKey('Home')
|
|
2536
|
+
for (let i = 0; i < 16; i++) {
|
|
2537
|
+
const el = h.document.activeElement
|
|
2538
|
+
if (el && el.dataset && el.dataset.group === '外观') return true
|
|
2539
|
+
pressKey('ArrowDown')
|
|
2540
|
+
}
|
|
2541
|
+
return false
|
|
2542
|
+
}
|
|
2543
|
+
check(focusGroupRow(), 'keyboard focus can land on the group row')
|
|
2544
|
+
const focusOnGroupRow = () => {
|
|
2545
|
+
const el = h.document.activeElement
|
|
2546
|
+
return Boolean(el && el.dataset && el.dataset.group === '外观')
|
|
2547
|
+
}
|
|
2548
|
+
check(focusOnGroupRow(), 'the focus really is on that row')
|
|
2549
|
+
hoverOut('外观', 'mouse')
|
|
2550
|
+
h.advance(300, 40)
|
|
2551
|
+
check(focusOnGroupRow(), 'hovering out does not move the focus')
|
|
2552
|
+
h.advance(500, 40)
|
|
2553
|
+
const activeNow = h.document.activeElement
|
|
2554
|
+
check(isOpen('外观'), 'while focus is inside that group, leaving with the mouse does not close it',
|
|
2555
|
+
`focus=${activeNow ? String(activeNow.className) + '/' + JSON.stringify(activeNow.dataset) : '(无)'}`)
|
|
2556
|
+
/* 焦点挪走之后,它就该按规则收起了 */
|
|
2557
|
+
pressKey('Home')
|
|
2558
|
+
hoverOut('外观', 'mouse')
|
|
2559
|
+
h.advance(800, 40)
|
|
2560
|
+
check(!isOpen('外观'), 'once focus leaves, the hover rules apply again')
|
|
2561
|
+
pressKey('Escape')
|
|
2562
|
+
check(menuOf() === null, 'Escape still closes the menu after all that hovering')
|
|
2563
|
+
/* 后面的检查要看菜单,重新打开 */
|
|
2564
|
+
body5.dispatch('contextmenu', rightClickOnHer().ev)
|
|
2565
|
+
check(menuOf() !== null, 'the menu reopens for the remaining structure checks')
|
|
2566
|
+
|
|
2457
2567
|
/* ---- 菜单里不该再有"版本与更新":版本号在头部,检查更新在弹窗里 ---- */
|
|
2458
2568
|
check(!itemsOf().some((i) => i.textContent.includes('版本与更新')),
|
|
2459
2569
|
'the version entry is gone from the menu (the header shows it now)')
|