dsh-wisp 1.33.1 → 1.34.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 +62 -0
- package/lib/client.js +85 -41
- package/lib/client.template.js +85 -41
- package/package.json +1 -1
- package/tools/smoke.mjs +116 -63
package/README.md
CHANGED
|
@@ -448,6 +448,68 @@ node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登
|
|
|
448
448
|
|
|
449
449
|
## 变更
|
|
450
450
|
|
|
451
|
+
### 1.34.0
|
|
452
|
+
|
|
453
|
+
**菜单样式重做。** 这一轮先把"菜单拍下来"这一步补上 —— 样式改动光看代码看不出来。
|
|
454
|
+
|
|
455
|
+
- **冒烟支持可选截图**:`WISP_SHOT=<路径> pnpm smoke` 会把"菜单 + 展开的子面板"
|
|
456
|
+
用真浏览器拍下来。默认不拍,不影响正常冒烟。
|
|
457
|
+
|
|
458
|
+
**一拍照就发现了两个用代码看不出的问题**:
|
|
459
|
+
|
|
460
|
+
1. **「放大一点 / 缩小一点」还挂在「外观」子面板里** —— 大小早就做成菜单里的滑块了,
|
|
461
|
+
这两条是重复的(而且是我之前声称删掉、实际没打上的改动)。已删。
|
|
462
|
+
2. **只有主操作有图标**,「专注 45 分钟」等行与它左右不齐。
|
|
463
|
+
|
|
464
|
+
**样式改动**:
|
|
465
|
+
|
|
466
|
+
| 之前 | 现在 |
|
|
467
|
+
|---|---|
|
|
468
|
+
| 摘要用淡字(透明度 .55) | **胶囊 chip**:淡底 + 圆角 —— 收起状态也能一眼扫到 |
|
|
469
|
+
| 主操作只是加粗变色 | **淡色渐变底 + 左侧强调条 + 字形** |
|
|
470
|
+
| 只有主操作有图标 | **每一行都有等宽字形槽**(无图标的留占位)→ 标签左边线对齐 |
|
|
471
|
+
| 焦点环依赖浏览器默认 | 显式 `:focus-visible` 环(菜单项 / 开关 / 滑块 / 角落) |
|
|
472
|
+
| hover 无过渡 | 100ms 过渡 + 按下时的内阴影反馈 |
|
|
473
|
+
| 开关 26×14、旋钮 11px | 开关 **30×16**、旋钮 13px + hover 放大 |
|
|
474
|
+
| 角落是裸文字 | **键帽**:淡底 + 描边 + 方位字形 ↖↗↙↘(当前所在的有强调描边) |
|
|
475
|
+
| 面板与菜单同一阴影 | 面板**更高一层**:阴影加强、模糊加深 |
|
|
476
|
+
| `prefers-reduced-motion` 只管她 | 菜单 / 面板 / 开关 / 滑块一起停 |
|
|
477
|
+
| 容器 6px、行 7×10 | 容器 8px、行 8×10,配色抽成 CSS 变量 |
|
|
478
|
+
|
|
479
|
+
配色仍全部走 `--dsw-alias-*` 主题 token(带兜底),所以明暗主题都跟着走。
|
|
480
|
+
|
|
481
|
+
冒烟 57 项(新增:滑块在真浏览器里改大小且不关菜单)。
|
|
482
|
+
|
|
483
|
+
### 1.33.2
|
|
484
|
+
|
|
485
|
+
### 1.33.2
|
|
486
|
+
|
|
487
|
+
**修「她在屏幕角落时,子菜单跟着偏」。** 现象是:右下 / 左下时子面板整体**偏下**,
|
|
488
|
+
右上时**压住主菜单**一部分。
|
|
489
|
+
|
|
490
|
+
根因只有一句:**我用了菜单"请求"的位置,而不是"实际"的位置。**
|
|
491
|
+
|
|
492
|
+
菜单在视口边缘会被**夹取**(`clamp` 进 `vw()/vh()`):
|
|
493
|
+
|
|
494
|
+
- 她在**右下 / 左下**时,菜单会被**往上推** → 实际 `top` 比请求的小 → 子面板却按请求的 `top` 算 → **偏下**;
|
|
495
|
+
- 她在**右上**时,菜单会被**往左推** → 实际 `left` 比请求的小 → 子面板按请求的 `left` 算 → 横向也偏 → **压住菜单**。
|
|
496
|
+
|
|
497
|
+
而且越靠角落、夹取越多、偏得越厉害 —— 正是观察到的规律。
|
|
498
|
+
|
|
499
|
+
改法就是读`menuEl.style.left/top`(菜单真实的图层坐标,夹取之后的值),
|
|
500
|
+
横向仍用"菜单宽度 + 面板宽度"、纵向仍用"行相对菜单的偏移",这两者本来就与坐标系无关。
|
|
501
|
+
|
|
502
|
+
**顺便把这道防线补成四个角落各测一遍**(`tools/smoke.mjs`):她先站到每一角,
|
|
503
|
+
再开菜单、悬停分组行、量三个矩形的相对关系,断言
|
|
504
|
+
「面板在菜单**旁边**」「在视口内」「与它所属那一行**对齐**」。
|
|
505
|
+
用户就是这样发现问题的,所以这条检查直接照着他的操作路径写。
|
|
506
|
+
|
|
507
|
+
实测四个角落:面板始终在菜单旁边(右侧角落翻到左边),`panel.t` 与 `row.t` **完全相等**。
|
|
508
|
+
|
|
509
|
+
冒烟 57 → **61 项**。
|
|
510
|
+
|
|
511
|
+
### 1.33.1
|
|
512
|
+
|
|
451
513
|
### 1.33.1
|
|
452
514
|
|
|
453
515
|
**修「子菜单遮住主菜单」。** 上一版的定位是错的,而且错得不显眼 —— 面板确实"在旁边",
|
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.34.0'
|
|
55
55
|
|
|
56
56
|
/* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
|
|
57
57
|
忘了更新就会红,不会静默过期。 */
|
|
@@ -75,6 +75,8 @@ const WHATS_NEW = {
|
|
|
75
75
|
'1.32.0': '分组改成鼠标悬停就展开、移开就收起(点一下则是钉住)。',
|
|
76
76
|
'1.33.0': '子菜单改成在旁边弹出(真正的菜单栏形状),→/← 进出自如。',
|
|
77
77
|
'1.33.1': '修子菜单位置算错、会压住主菜单的问题。',
|
|
78
|
+
'1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
|
|
79
|
+
'1.34.0': '菜单样式重做:摘要做成胶囊、每行都有图标位、键盘焦点看得见。',
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
/* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
|
|
@@ -693,33 +695,49 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
693
695
|
.wisp-root[data-mood="attn"] .wisp-body{animation-name:wisp-nudge;animation-duration:1.5s;animation-iteration-count:4}
|
|
694
696
|
@keyframes wisp-nudge{0%,100%{transform:translate3d(0,0,0)}25%{transform:translate3d(-7px,0,0)}75%{transform:translate3d(7px,0,0)}}
|
|
695
697
|
@keyframes wisp-hop{0%,100%{transform:translate3d(0,0,0)}45%{transform:translate3d(0,-16px,0)}}
|
|
696
|
-
|
|
697
|
-
.wisp-menu-
|
|
698
|
-
.wisp-menu-item:
|
|
699
|
-
.wisp-menu-
|
|
700
|
-
.wisp-menu-
|
|
701
|
-
|
|
702
|
-
.wisp-menu-
|
|
703
|
-
.wisp-menu-
|
|
698
|
+
/* 菜单与级联子面板共用一套外观变量 —— 面板是菜单的"上一层",只改阴影与浮起量。 */
|
|
699
|
+
.wisp-menu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;min-width:204px;max-width:248px;padding:8px;border-radius:14px;font-size:12.5px;line-height:1.3;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--wisp-surface);border:1px solid var(--wisp-line);box-shadow:0 14px 38px rgba(0,0,0,.32);-webkit-backdrop-filter:blur(16px) saturate(160%);backdrop-filter:blur(16px) saturate(160%);pointer-events:auto;animation:wisp-menu .14s ease}
|
|
700
|
+
.wisp-menu-item{display:block;width:100%;padding:8px 10px;border:0;border-radius:9px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;white-space:nowrap;transition:background .1s ease,box-shadow .1s ease}
|
|
701
|
+
.wisp-menu-item:hover{background:var(--wisp-hover,rgba(120,170,255,.2))}
|
|
702
|
+
.wisp-menu-item:active{background:var(--wisp-hover,rgba(120,170,255,.2));box-shadow:inset 0 1px 2px rgba(0,0,0,.12)}
|
|
703
|
+
/* 键盘焦点必须是**看得见**的:这些行都是 border:0 的按钮,浏览器默认轮廓很弱。 */
|
|
704
|
+
.wisp-menu-item:focus-visible,.wisp-corner:focus-visible,.wisp-switch:focus-visible,.wisp-slider:focus-visible{outline:2px solid var(--wisp-accent,#4a7cf7);outline-offset:-2px}
|
|
705
|
+
.wisp-menu-head{padding:4px 10px 8px}
|
|
706
|
+
.wisp-menu-title{font-size:13px;font-weight:600;letter-spacing:.01em}
|
|
707
|
+
.wisp-menu-sub{display:flex;align-items:center;gap:6px;font-size:10.5px;opacity:.6;margin-top:3px}
|
|
708
|
+
.wisp-menu-ver{padding:1px 6px;border-radius:999px;background:var(--wisp-hover,rgba(120,170,255,.2));font-size:10px;opacity:.95;letter-spacing:.02em}
|
|
709
|
+
.wisp-menu-sep{height:1px;margin:6px 8px;background:var(--wisp-line,var(--wisp-say-line));opacity:.5}
|
|
710
|
+
.wisp-menu-hint{padding:7px 10px 3px;font-size:10.5px;opacity:.5;border-top:1px solid var(--wisp-line,var(--wisp-say-line));margin-top:6px}
|
|
704
711
|
.wisp-menu-row{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
705
|
-
|
|
706
|
-
.wisp-menu-
|
|
707
|
-
|
|
708
|
-
.wisp-
|
|
709
|
-
.wisp-
|
|
710
|
-
.wisp-
|
|
711
|
-
.wisp-
|
|
712
|
-
.wisp-
|
|
713
|
-
.wisp-
|
|
714
|
-
.wisp-
|
|
715
|
-
.wisp-
|
|
716
|
-
.wisp-
|
|
717
|
-
.wisp-
|
|
718
|
-
.wisp-
|
|
719
|
-
.wisp-
|
|
720
|
-
.wisp-
|
|
721
|
-
.wisp-
|
|
722
|
-
.wisp-
|
|
712
|
+
/* 摘要做成胶囊:收起状态也要能一眼扫到"现在是什么",纯淡字太弱。 */
|
|
713
|
+
.wisp-menu-sum{flex:0 1 auto;min-width:0;padding:1px 7px;border-radius:999px;background:var(--wisp-hover,rgba(120,170,255,.2));font-size:10.5px;opacity:.85;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
714
|
+
/* 主操作(专注):淡色底 + 左侧强调条 + 字形,一眼看出"这是主要动作"。 */
|
|
715
|
+
.wisp-menu-item[data-accent="true"]{position:relative;font-weight:600;color:var(--wisp-accent,inherit);background:linear-gradient(90deg,var(--wisp-hover,rgba(120,170,255,.2)),transparent 88%)}
|
|
716
|
+
.wisp-menu-item[data-accent="true"]::before{content:"";position:absolute;left:3px;top:50%;width:3px;height:16px;margin-top:-8px;border-radius:2px;background:var(--wisp-accent,#4a7cf7)}
|
|
717
|
+
.wisp-menu-item[data-accent="true"]:hover{background:linear-gradient(90deg,var(--wisp-hover,rgba(120,170,255,.32)),transparent 92%)}
|
|
718
|
+
.wisp-menu-item[data-muted="true"]{opacity:.8}
|
|
719
|
+
.wisp-menu-glyph{display:inline-block;width:16px;margin-right:7px;text-align:center;font-size:12px;opacity:.75;font-weight:400}
|
|
720
|
+
.wisp-menu-item[data-accent="true"] .wisp-menu-glyph{opacity:1}
|
|
721
|
+
.wisp-menu-caret{opacity:.5;font-size:10px;flex:0 0 auto}
|
|
722
|
+
.wisp-switch{flex:0 0 auto;width:30px;height:16px;border-radius:9px;background:rgba(140,160,190,.4);position:relative;transition:background .16s ease}
|
|
723
|
+
.wisp-switch[data-on="true"]{background:var(--wisp-accent,#4a7cf7);box-shadow:0 0 0 3px rgba(74,124,247,.14)}
|
|
724
|
+
.wisp-switch i{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.28);transition:transform .16s ease}
|
|
725
|
+
.wisp-switch[data-on="true"] i{transform:translateX(14px)}
|
|
726
|
+
.wisp-slider-row{padding:7px 10px 9px}
|
|
727
|
+
.wisp-slider-top{display:flex;justify-content:space-between;align-items:baseline;font-size:11px;opacity:.85;margin-bottom:4px}
|
|
728
|
+
.wisp-slider-value{font-variant-numeric:tabular-nums;opacity:.75}
|
|
729
|
+
.wisp-slider{position:relative;height:18px;cursor:pointer;outline:none;touch-action:none}
|
|
730
|
+
.wisp-slider-track{position:absolute;left:0;right:0;top:8px;height:3px;border-radius:2px;background:rgba(140,160,190,.4)}
|
|
731
|
+
.wisp-slider-fill{position:absolute;left:0;top:8px;height:3px;border-radius:2px;background:var(--wisp-accent,#4a7cf7)}
|
|
732
|
+
.wisp-slider-knob{position:absolute;top:3px;width:13px;height:13px;margin-left:-6px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.35);transition:transform .1s ease}
|
|
733
|
+
.wisp-slider:hover .wisp-slider-knob{transform:scale(1.12)}
|
|
734
|
+
.wisp-slider:active .wisp-slider-knob{transform:scale(1.2)}
|
|
735
|
+
/* 四个角落做成"键帽":方位是空间的,控件也该长得像方位键。 */
|
|
736
|
+
.wisp-grid2{display:grid;grid-template-columns:1fr 1fr;gap:5px;padding:2px 8px 6px}
|
|
737
|
+
.wisp-grid2 button,.wisp-corner{display:flex;align-items:center;justify-content:center;gap:5px;padding:7px 4px;border:1px solid var(--wisp-line,var(--wisp-say-line));border-radius:8px;background:var(--wisp-hover,rgba(120,170,255,.12));color:inherit;font:inherit;font-size:11px;cursor:pointer;transition:background .1s ease,border-color .1s ease}
|
|
738
|
+
.wisp-grid2 button:hover,.wisp-corner:hover{background:var(--wisp-hover,rgba(120,170,255,.26))}
|
|
739
|
+
.wisp-grid2 button[data-current="true"],.wisp-corner[data-current="true"]{border-color:var(--wisp-accent,#4a7cf7);color:var(--wisp-accent,inherit);font-weight:600;box-shadow:0 0 0 1px var(--wisp-accent,#4a7cf7) inset}
|
|
740
|
+
.wisp-corner-glyph{font-size:12px;opacity:.7}
|
|
723
741
|
.wisp-root[data-hidden="true"]{display:none}
|
|
724
742
|
.wisp-tab{position:absolute;width:56px;height:56px;padding:0;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:16px;border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));box-shadow:0 6px 18px rgba(0,0,0,.22);cursor:pointer;pointer-events:auto;animation:wisp-menu .16s ease;transition:transform .16s ease}
|
|
725
743
|
.wisp-tab:hover{transform:translateY(-2px)}
|
|
@@ -730,7 +748,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
730
748
|
.wisp-menu-caret{opacity:.55;font-size:10px;flex:0 0 auto}
|
|
731
749
|
/* 级联子菜单:浮在行的**右侧**(放不下时翻到左侧),不把下面的内容推下去。
|
|
732
750
|
与父行顶部对齐、允许轻微重叠 —— 指针从行斜着移到面板上时不会穿出"同一区域"。 */
|
|
733
|
-
.wisp-submenu{position:absolute;box-sizing:border-box;width:184px;padding:6px;border-radius:12px;font-size:12px;line-height:1.25;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0
|
|
751
|
+
.wisp-submenu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;box-sizing:border-box;width:184px;padding:6px;border-radius:12px;font-size:12px;line-height:1.25;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0 16px 40px rgba(0,0,0,.34),0 2px 6px rgba(0,0,0,.18);-webkit-backdrop-filter:blur(18px) saturate(170%);backdrop-filter:blur(18px) saturate(170%);pointer-events:auto;animation:wisp-fade .12s ease}
|
|
734
752
|
@keyframes wisp-fade{0%{opacity:0}100%{opacity:1}}
|
|
735
753
|
.wisp-menu-sub{padding-left:10px}
|
|
736
754
|
.wisp-menu-sub .wisp-menu-item{padding-left:12px;opacity:.94}
|
|
@@ -748,7 +766,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
748
766
|
.wisp-credits{font-size:11px;opacity:.8;margin:6px 0 0}
|
|
749
767
|
.wisp-dialog-action{display:block;width:100%;margin:4px 0 2px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));border-radius:8px;background:none;color:inherit;font:inherit;cursor:pointer}
|
|
750
768
|
.wisp-dialog-action:hover{background:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2))}
|
|
751
|
-
@media (prefers-reduced-motion:reduce){.wisp-body{animation:none!important}}`
|
|
769
|
+
@media (prefers-reduced-motion:reduce){.wisp-body{animation:none!important}.wisp-menu,.wisp-submenu,.wisp-switch,.wisp-switch i,.wisp-slider-knob,.wisp-menu-item,.wisp-corner{animation:none!important;transition:none!important}}`
|
|
752
770
|
|
|
753
771
|
const installStyles = (styles) => {
|
|
754
772
|
if (styles && typeof styles.insert === 'function') {
|
|
@@ -2023,6 +2041,15 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2023
2041
|
/* 行构造器:菜单和子面板都用它们 —— 所以放在模块级,不能藏在 openMenu 里面。 */
|
|
2024
2042
|
let menuExtras = 0
|
|
2025
2043
|
/* 动作行:点了就执行、然后关菜单。 */
|
|
2044
|
+
/* 行左侧的小字形(子 span,不写进按钮自己的 textContent) */
|
|
2045
|
+
const glyph = (text) => {
|
|
2046
|
+
const g = document.createElement('span')
|
|
2047
|
+
g.className = 'wisp-menu-glyph'
|
|
2048
|
+
g.setAttribute('aria-hidden', 'true')
|
|
2049
|
+
g.textContent = text
|
|
2050
|
+
return g
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2026
2053
|
const actionRow = (spec, into) => {
|
|
2027
2054
|
const btn = document.createElement('button')
|
|
2028
2055
|
btn.className = 'wisp-menu-item'
|
|
@@ -2032,6 +2059,10 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2032
2059
|
if (spec.accent === true) btn.dataset.accent = 'true'
|
|
2033
2060
|
if (spec.muted === true) btn.dataset.muted = 'true'
|
|
2034
2061
|
btn.textContent = spec.label
|
|
2062
|
+
/* 字形槽:有图标的画图标,没有的留等宽占位 —— 所有标签的左边线对齐。
|
|
2063
|
+
一律用**子 span**:真浏览器显示图标,简化 DOM 里按钮自己的 textContent
|
|
2064
|
+
仍是纯标签(测试读的就是它)。 */
|
|
2065
|
+
if (btn.insertBefore) btn.insertBefore(glyph(spec.glyph || ''), btn.firstChild)
|
|
2035
2066
|
// 菜单自己吞掉 pointerdown,否则会被下面那个"点外面就关"的监听判成外部点击
|
|
2036
2067
|
btn.addEventListener('pointerdown', (ev) => ev.stopPropagation())
|
|
2037
2068
|
btn.addEventListener('click', (ev) => {
|
|
@@ -2057,6 +2088,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2057
2088
|
btn.dataset.switchKey = spec.key
|
|
2058
2089
|
const label = document.createElement('span')
|
|
2059
2090
|
label.textContent = spec.label
|
|
2091
|
+
if (btn.insertBefore && glyph) btn.insertBefore(glyph(spec.glyph || ''), btn.firstChild)
|
|
2060
2092
|
const toggle = document.createElement('span')
|
|
2061
2093
|
toggle.className = 'wisp-switch'
|
|
2062
2094
|
toggle.dataset.on = on ? 'true' : 'false'
|
|
@@ -2159,6 +2191,16 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2159
2191
|
b.setAttribute('tabindex', '-1')
|
|
2160
2192
|
b.dataset.corner = corner.label
|
|
2161
2193
|
b.textContent = corner.label
|
|
2194
|
+
/* 方位是空间的,就给它一个方位字形 —— 键帽上写着 ↖ 比写着"左上角"快得多。 */
|
|
2195
|
+
const arrows = { '左上角': '↖', '右上角': '↗', '左下角': '↙', '右下角': '↘' }
|
|
2196
|
+
const arrow = arrows[corner.label]
|
|
2197
|
+
if (arrow && b.insertBefore) {
|
|
2198
|
+
const g = document.createElement('span')
|
|
2199
|
+
g.className = 'wisp-corner-glyph'
|
|
2200
|
+
g.setAttribute('aria-hidden', 'true')
|
|
2201
|
+
g.textContent = arrow
|
|
2202
|
+
b.insertBefore(g, b.firstChild)
|
|
2203
|
+
}
|
|
2162
2204
|
if (corner.label === currentCorner()) b.dataset.current = 'true'
|
|
2163
2205
|
b.addEventListener('pointerdown', (ev) => ev.stopPropagation())
|
|
2164
2206
|
b.addEventListener('click', (ev) => {
|
|
@@ -2286,8 +2328,12 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2286
2328
|
const mr = menuEl && menuEl.getBoundingClientRect ? menuEl.getBoundingClientRect() : null
|
|
2287
2329
|
if (mr && mr.width > 0) menuW = mr.width
|
|
2288
2330
|
} catch (e) { /* 用常量兜底 */ }
|
|
2289
|
-
|
|
2290
|
-
|
|
2331
|
+
/* 菜单的**实际**位置,不是"请求"的位置(menuAtX/menuAtY)。
|
|
2332
|
+
菜单会被夹进视口:她站在右下/左下时菜单被往上推,站在右上时被往左推。
|
|
2333
|
+
用请求坐标算出面板位置,就会跟着偏 —— 而且越靠角落偏得越多
|
|
2334
|
+
(这正是"她在右下角时子菜单更偏下、右上角时压住菜单"的原因)。 */
|
|
2335
|
+
const menuLeft = menuEl ? (Number.parseFloat(menuEl.style.left) || menuAtX) : menuAtX
|
|
2336
|
+
const menuTop = menuEl ? (Number.parseFloat(menuEl.style.top) || menuAtY) : menuAtY
|
|
2291
2337
|
let x = menuLeft + menuW + 2
|
|
2292
2338
|
flyoutSide = 'right'
|
|
2293
2339
|
if (x + SUBMENU_W > vw() - 8) {
|
|
@@ -2572,10 +2618,10 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2572
2618
|
{ kind: 'sep' },
|
|
2573
2619
|
/* 专注:最常用的动作,放最上面;进行中时这一行本身就是状态 + 结束 */
|
|
2574
2620
|
...(focusUntil > 0
|
|
2575
|
-
? [{ label: '专注中 · 还剩 ' + Math.max(1, Math.ceil(focusLeftMs() / 60000)) + ' 分钟', run: () => stopFocus(false), accent: true }]
|
|
2621
|
+
? [{ label: '专注中 · 还剩 ' + Math.max(1, Math.ceil(focusLeftMs() / 60000)) + ' 分钟', run: () => stopFocus(false), accent: true, glyph: '⏱' }]
|
|
2576
2622
|
: [
|
|
2577
|
-
{ label: '专注 ' + cfg.focusMinutes + ' 分钟', run: () => startFocus(cfg.focusMinutes), accent: true },
|
|
2578
|
-
{ label: '专注 45 分钟', run: () => startFocus(45) },
|
|
2623
|
+
{ label: '专注 ' + cfg.focusMinutes + ' 分钟', run: () => startFocus(cfg.focusMinutes), accent: true, glyph: '⏱' },
|
|
2624
|
+
{ label: '专注 45 分钟', run: () => startFocus(45), glyph: '⏱' },
|
|
2579
2625
|
]),
|
|
2580
2626
|
{ kind: 'sep' },
|
|
2581
2627
|
{
|
|
@@ -2590,16 +2636,14 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2590
2636
|
say('换成' + skinLabel(id) + '了。')
|
|
2591
2637
|
},
|
|
2592
2638
|
})),
|
|
2593
|
-
{ label: '放大一点', run: () => applyConfig({ size: cfg.size + 0.5 }) },
|
|
2594
|
-
{ label: '缩小一点', run: () => applyConfig({ size: cfg.size - 0.5 }) },
|
|
2595
2639
|
],
|
|
2596
2640
|
},
|
|
2597
2641
|
{
|
|
2598
2642
|
group: '行为',
|
|
2599
2643
|
summary: onToggleCount() + '/' + TOGGLES.length + ' 开',
|
|
2600
2644
|
children: [
|
|
2601
|
-
{ label: '说句话', run: () => { wake(true); say(pick(LINES.click)) } },
|
|
2602
|
-
{ label: '睡一会儿', run: () => setMood('sleep') },
|
|
2645
|
+
{ label: '说句话', glyph: '✦', run: () => { wake(true); say(pick(LINES.click)) } },
|
|
2646
|
+
{ label: '睡一会儿', glyph: '☾', run: () => setMood('sleep') },
|
|
2603
2647
|
/* 开关是**真正的开关行**:带 aria-checked,点完菜单不关(就在原地更新) */
|
|
2604
2648
|
...TOGGLES.map((t) => ({ kind: 'switch', key: t.key, label: t.label })),
|
|
2605
2649
|
],
|
|
@@ -2611,11 +2655,11 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2611
2655
|
},
|
|
2612
2656
|
{ kind: 'slider' },
|
|
2613
2657
|
{ kind: 'sep' },
|
|
2614
|
-
{ label: '动作一览…', run: () => actionsDialog() },
|
|
2615
|
-
{ label: '关于她…', run: () => aboutDialog() },
|
|
2658
|
+
{ label: '动作一览…', glyph: '▤', run: () => actionsDialog() },
|
|
2659
|
+
{ label: '关于她…', glyph: 'ⓘ', run: () => aboutDialog() },
|
|
2616
2660
|
{ kind: 'sep' },
|
|
2617
2661
|
/* 一个开关,不是一个单向动作:从迷你按钮右键打开菜单时,它必须能把她叫回来。 */
|
|
2618
|
-
{ label: hidden ? '回来吧' : '先躲起来', run: () => setHidden(!hidden), muted: true },
|
|
2662
|
+
{ label: hidden ? '回来吧' : '先躲起来', glyph: '⤓', run: () => setHidden(!hidden), muted: true },
|
|
2619
2663
|
{ kind: 'hint' },
|
|
2620
2664
|
]
|
|
2621
2665
|
|
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.34.0'
|
|
55
55
|
|
|
56
56
|
/* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
|
|
57
57
|
忘了更新就会红,不会静默过期。 */
|
|
@@ -75,6 +75,8 @@ const WHATS_NEW = {
|
|
|
75
75
|
'1.32.0': '分组改成鼠标悬停就展开、移开就收起(点一下则是钉住)。',
|
|
76
76
|
'1.33.0': '子菜单改成在旁边弹出(真正的菜单栏形状),→/← 进出自如。',
|
|
77
77
|
'1.33.1': '修子菜单位置算错、会压住主菜单的问题。',
|
|
78
|
+
'1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
|
|
79
|
+
'1.34.0': '菜单样式重做:摘要做成胶囊、每行都有图标位、键盘焦点看得见。',
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
/* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
|
|
@@ -642,33 +644,49 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
642
644
|
.wisp-root[data-mood="attn"] .wisp-body{animation-name:wisp-nudge;animation-duration:1.5s;animation-iteration-count:4}
|
|
643
645
|
@keyframes wisp-nudge{0%,100%{transform:translate3d(0,0,0)}25%{transform:translate3d(-7px,0,0)}75%{transform:translate3d(7px,0,0)}}
|
|
644
646
|
@keyframes wisp-hop{0%,100%{transform:translate3d(0,0,0)}45%{transform:translate3d(0,-16px,0)}}
|
|
645
|
-
|
|
646
|
-
.wisp-menu-
|
|
647
|
-
.wisp-menu-item:
|
|
648
|
-
.wisp-menu-
|
|
649
|
-
.wisp-menu-
|
|
650
|
-
|
|
651
|
-
.wisp-menu-
|
|
652
|
-
.wisp-menu-
|
|
647
|
+
/* 菜单与级联子面板共用一套外观变量 —— 面板是菜单的"上一层",只改阴影与浮起量。 */
|
|
648
|
+
.wisp-menu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;min-width:204px;max-width:248px;padding:8px;border-radius:14px;font-size:12.5px;line-height:1.3;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--wisp-surface);border:1px solid var(--wisp-line);box-shadow:0 14px 38px rgba(0,0,0,.32);-webkit-backdrop-filter:blur(16px) saturate(160%);backdrop-filter:blur(16px) saturate(160%);pointer-events:auto;animation:wisp-menu .14s ease}
|
|
649
|
+
.wisp-menu-item{display:block;width:100%;padding:8px 10px;border:0;border-radius:9px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;white-space:nowrap;transition:background .1s ease,box-shadow .1s ease}
|
|
650
|
+
.wisp-menu-item:hover{background:var(--wisp-hover,rgba(120,170,255,.2))}
|
|
651
|
+
.wisp-menu-item:active{background:var(--wisp-hover,rgba(120,170,255,.2));box-shadow:inset 0 1px 2px rgba(0,0,0,.12)}
|
|
652
|
+
/* 键盘焦点必须是**看得见**的:这些行都是 border:0 的按钮,浏览器默认轮廓很弱。 */
|
|
653
|
+
.wisp-menu-item:focus-visible,.wisp-corner:focus-visible,.wisp-switch:focus-visible,.wisp-slider:focus-visible{outline:2px solid var(--wisp-accent,#4a7cf7);outline-offset:-2px}
|
|
654
|
+
.wisp-menu-head{padding:4px 10px 8px}
|
|
655
|
+
.wisp-menu-title{font-size:13px;font-weight:600;letter-spacing:.01em}
|
|
656
|
+
.wisp-menu-sub{display:flex;align-items:center;gap:6px;font-size:10.5px;opacity:.6;margin-top:3px}
|
|
657
|
+
.wisp-menu-ver{padding:1px 6px;border-radius:999px;background:var(--wisp-hover,rgba(120,170,255,.2));font-size:10px;opacity:.95;letter-spacing:.02em}
|
|
658
|
+
.wisp-menu-sep{height:1px;margin:6px 8px;background:var(--wisp-line,var(--wisp-say-line));opacity:.5}
|
|
659
|
+
.wisp-menu-hint{padding:7px 10px 3px;font-size:10.5px;opacity:.5;border-top:1px solid var(--wisp-line,var(--wisp-say-line));margin-top:6px}
|
|
653
660
|
.wisp-menu-row{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
654
|
-
|
|
655
|
-
.wisp-menu-
|
|
656
|
-
|
|
657
|
-
.wisp-
|
|
658
|
-
.wisp-
|
|
659
|
-
.wisp-
|
|
660
|
-
.wisp-
|
|
661
|
-
.wisp-
|
|
662
|
-
.wisp-
|
|
663
|
-
.wisp-
|
|
664
|
-
.wisp-
|
|
665
|
-
.wisp-
|
|
666
|
-
.wisp-
|
|
667
|
-
.wisp-
|
|
668
|
-
.wisp-
|
|
669
|
-
.wisp-
|
|
670
|
-
.wisp-
|
|
671
|
-
.wisp-
|
|
661
|
+
/* 摘要做成胶囊:收起状态也要能一眼扫到"现在是什么",纯淡字太弱。 */
|
|
662
|
+
.wisp-menu-sum{flex:0 1 auto;min-width:0;padding:1px 7px;border-radius:999px;background:var(--wisp-hover,rgba(120,170,255,.2));font-size:10.5px;opacity:.85;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
663
|
+
/* 主操作(专注):淡色底 + 左侧强调条 + 字形,一眼看出"这是主要动作"。 */
|
|
664
|
+
.wisp-menu-item[data-accent="true"]{position:relative;font-weight:600;color:var(--wisp-accent,inherit);background:linear-gradient(90deg,var(--wisp-hover,rgba(120,170,255,.2)),transparent 88%)}
|
|
665
|
+
.wisp-menu-item[data-accent="true"]::before{content:"";position:absolute;left:3px;top:50%;width:3px;height:16px;margin-top:-8px;border-radius:2px;background:var(--wisp-accent,#4a7cf7)}
|
|
666
|
+
.wisp-menu-item[data-accent="true"]:hover{background:linear-gradient(90deg,var(--wisp-hover,rgba(120,170,255,.32)),transparent 92%)}
|
|
667
|
+
.wisp-menu-item[data-muted="true"]{opacity:.8}
|
|
668
|
+
.wisp-menu-glyph{display:inline-block;width:16px;margin-right:7px;text-align:center;font-size:12px;opacity:.75;font-weight:400}
|
|
669
|
+
.wisp-menu-item[data-accent="true"] .wisp-menu-glyph{opacity:1}
|
|
670
|
+
.wisp-menu-caret{opacity:.5;font-size:10px;flex:0 0 auto}
|
|
671
|
+
.wisp-switch{flex:0 0 auto;width:30px;height:16px;border-radius:9px;background:rgba(140,160,190,.4);position:relative;transition:background .16s ease}
|
|
672
|
+
.wisp-switch[data-on="true"]{background:var(--wisp-accent,#4a7cf7);box-shadow:0 0 0 3px rgba(74,124,247,.14)}
|
|
673
|
+
.wisp-switch i{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.28);transition:transform .16s ease}
|
|
674
|
+
.wisp-switch[data-on="true"] i{transform:translateX(14px)}
|
|
675
|
+
.wisp-slider-row{padding:7px 10px 9px}
|
|
676
|
+
.wisp-slider-top{display:flex;justify-content:space-between;align-items:baseline;font-size:11px;opacity:.85;margin-bottom:4px}
|
|
677
|
+
.wisp-slider-value{font-variant-numeric:tabular-nums;opacity:.75}
|
|
678
|
+
.wisp-slider{position:relative;height:18px;cursor:pointer;outline:none;touch-action:none}
|
|
679
|
+
.wisp-slider-track{position:absolute;left:0;right:0;top:8px;height:3px;border-radius:2px;background:rgba(140,160,190,.4)}
|
|
680
|
+
.wisp-slider-fill{position:absolute;left:0;top:8px;height:3px;border-radius:2px;background:var(--wisp-accent,#4a7cf7)}
|
|
681
|
+
.wisp-slider-knob{position:absolute;top:3px;width:13px;height:13px;margin-left:-6px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.35);transition:transform .1s ease}
|
|
682
|
+
.wisp-slider:hover .wisp-slider-knob{transform:scale(1.12)}
|
|
683
|
+
.wisp-slider:active .wisp-slider-knob{transform:scale(1.2)}
|
|
684
|
+
/* 四个角落做成"键帽":方位是空间的,控件也该长得像方位键。 */
|
|
685
|
+
.wisp-grid2{display:grid;grid-template-columns:1fr 1fr;gap:5px;padding:2px 8px 6px}
|
|
686
|
+
.wisp-grid2 button,.wisp-corner{display:flex;align-items:center;justify-content:center;gap:5px;padding:7px 4px;border:1px solid var(--wisp-line,var(--wisp-say-line));border-radius:8px;background:var(--wisp-hover,rgba(120,170,255,.12));color:inherit;font:inherit;font-size:11px;cursor:pointer;transition:background .1s ease,border-color .1s ease}
|
|
687
|
+
.wisp-grid2 button:hover,.wisp-corner:hover{background:var(--wisp-hover,rgba(120,170,255,.26))}
|
|
688
|
+
.wisp-grid2 button[data-current="true"],.wisp-corner[data-current="true"]{border-color:var(--wisp-accent,#4a7cf7);color:var(--wisp-accent,inherit);font-weight:600;box-shadow:0 0 0 1px var(--wisp-accent,#4a7cf7) inset}
|
|
689
|
+
.wisp-corner-glyph{font-size:12px;opacity:.7}
|
|
672
690
|
.wisp-root[data-hidden="true"]{display:none}
|
|
673
691
|
.wisp-tab{position:absolute;width:56px;height:56px;padding:0;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:16px;border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));box-shadow:0 6px 18px rgba(0,0,0,.22);cursor:pointer;pointer-events:auto;animation:wisp-menu .16s ease;transition:transform .16s ease}
|
|
674
692
|
.wisp-tab:hover{transform:translateY(-2px)}
|
|
@@ -679,7 +697,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
679
697
|
.wisp-menu-caret{opacity:.55;font-size:10px;flex:0 0 auto}
|
|
680
698
|
/* 级联子菜单:浮在行的**右侧**(放不下时翻到左侧),不把下面的内容推下去。
|
|
681
699
|
与父行顶部对齐、允许轻微重叠 —— 指针从行斜着移到面板上时不会穿出"同一区域"。 */
|
|
682
|
-
.wisp-submenu{position:absolute;box-sizing:border-box;width:184px;padding:6px;border-radius:12px;font-size:12px;line-height:1.25;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0
|
|
700
|
+
.wisp-submenu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;box-sizing:border-box;width:184px;padding:6px;border-radius:12px;font-size:12px;line-height:1.25;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0 16px 40px rgba(0,0,0,.34),0 2px 6px rgba(0,0,0,.18);-webkit-backdrop-filter:blur(18px) saturate(170%);backdrop-filter:blur(18px) saturate(170%);pointer-events:auto;animation:wisp-fade .12s ease}
|
|
683
701
|
@keyframes wisp-fade{0%{opacity:0}100%{opacity:1}}
|
|
684
702
|
.wisp-menu-sub{padding-left:10px}
|
|
685
703
|
.wisp-menu-sub .wisp-menu-item{padding-left:12px;opacity:.94}
|
|
@@ -697,7 +715,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
697
715
|
.wisp-credits{font-size:11px;opacity:.8;margin:6px 0 0}
|
|
698
716
|
.wisp-dialog-action{display:block;width:100%;margin:4px 0 2px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));border-radius:8px;background:none;color:inherit;font:inherit;cursor:pointer}
|
|
699
717
|
.wisp-dialog-action:hover{background:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2))}
|
|
700
|
-
@media (prefers-reduced-motion:reduce){.wisp-body{animation:none!important}}`
|
|
718
|
+
@media (prefers-reduced-motion:reduce){.wisp-body{animation:none!important}.wisp-menu,.wisp-submenu,.wisp-switch,.wisp-switch i,.wisp-slider-knob,.wisp-menu-item,.wisp-corner{animation:none!important;transition:none!important}}`
|
|
701
719
|
|
|
702
720
|
const installStyles = (styles) => {
|
|
703
721
|
if (styles && typeof styles.insert === 'function') {
|
|
@@ -1972,6 +1990,15 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1972
1990
|
/* 行构造器:菜单和子面板都用它们 —— 所以放在模块级,不能藏在 openMenu 里面。 */
|
|
1973
1991
|
let menuExtras = 0
|
|
1974
1992
|
/* 动作行:点了就执行、然后关菜单。 */
|
|
1993
|
+
/* 行左侧的小字形(子 span,不写进按钮自己的 textContent) */
|
|
1994
|
+
const glyph = (text) => {
|
|
1995
|
+
const g = document.createElement('span')
|
|
1996
|
+
g.className = 'wisp-menu-glyph'
|
|
1997
|
+
g.setAttribute('aria-hidden', 'true')
|
|
1998
|
+
g.textContent = text
|
|
1999
|
+
return g
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1975
2002
|
const actionRow = (spec, into) => {
|
|
1976
2003
|
const btn = document.createElement('button')
|
|
1977
2004
|
btn.className = 'wisp-menu-item'
|
|
@@ -1981,6 +2008,10 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1981
2008
|
if (spec.accent === true) btn.dataset.accent = 'true'
|
|
1982
2009
|
if (spec.muted === true) btn.dataset.muted = 'true'
|
|
1983
2010
|
btn.textContent = spec.label
|
|
2011
|
+
/* 字形槽:有图标的画图标,没有的留等宽占位 —— 所有标签的左边线对齐。
|
|
2012
|
+
一律用**子 span**:真浏览器显示图标,简化 DOM 里按钮自己的 textContent
|
|
2013
|
+
仍是纯标签(测试读的就是它)。 */
|
|
2014
|
+
if (btn.insertBefore) btn.insertBefore(glyph(spec.glyph || ''), btn.firstChild)
|
|
1984
2015
|
// 菜单自己吞掉 pointerdown,否则会被下面那个"点外面就关"的监听判成外部点击
|
|
1985
2016
|
btn.addEventListener('pointerdown', (ev) => ev.stopPropagation())
|
|
1986
2017
|
btn.addEventListener('click', (ev) => {
|
|
@@ -2006,6 +2037,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2006
2037
|
btn.dataset.switchKey = spec.key
|
|
2007
2038
|
const label = document.createElement('span')
|
|
2008
2039
|
label.textContent = spec.label
|
|
2040
|
+
if (btn.insertBefore && glyph) btn.insertBefore(glyph(spec.glyph || ''), btn.firstChild)
|
|
2009
2041
|
const toggle = document.createElement('span')
|
|
2010
2042
|
toggle.className = 'wisp-switch'
|
|
2011
2043
|
toggle.dataset.on = on ? 'true' : 'false'
|
|
@@ -2108,6 +2140,16 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2108
2140
|
b.setAttribute('tabindex', '-1')
|
|
2109
2141
|
b.dataset.corner = corner.label
|
|
2110
2142
|
b.textContent = corner.label
|
|
2143
|
+
/* 方位是空间的,就给它一个方位字形 —— 键帽上写着 ↖ 比写着"左上角"快得多。 */
|
|
2144
|
+
const arrows = { '左上角': '↖', '右上角': '↗', '左下角': '↙', '右下角': '↘' }
|
|
2145
|
+
const arrow = arrows[corner.label]
|
|
2146
|
+
if (arrow && b.insertBefore) {
|
|
2147
|
+
const g = document.createElement('span')
|
|
2148
|
+
g.className = 'wisp-corner-glyph'
|
|
2149
|
+
g.setAttribute('aria-hidden', 'true')
|
|
2150
|
+
g.textContent = arrow
|
|
2151
|
+
b.insertBefore(g, b.firstChild)
|
|
2152
|
+
}
|
|
2111
2153
|
if (corner.label === currentCorner()) b.dataset.current = 'true'
|
|
2112
2154
|
b.addEventListener('pointerdown', (ev) => ev.stopPropagation())
|
|
2113
2155
|
b.addEventListener('click', (ev) => {
|
|
@@ -2235,8 +2277,12 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2235
2277
|
const mr = menuEl && menuEl.getBoundingClientRect ? menuEl.getBoundingClientRect() : null
|
|
2236
2278
|
if (mr && mr.width > 0) menuW = mr.width
|
|
2237
2279
|
} catch (e) { /* 用常量兜底 */ }
|
|
2238
|
-
|
|
2239
|
-
|
|
2280
|
+
/* 菜单的**实际**位置,不是"请求"的位置(menuAtX/menuAtY)。
|
|
2281
|
+
菜单会被夹进视口:她站在右下/左下时菜单被往上推,站在右上时被往左推。
|
|
2282
|
+
用请求坐标算出面板位置,就会跟着偏 —— 而且越靠角落偏得越多
|
|
2283
|
+
(这正是"她在右下角时子菜单更偏下、右上角时压住菜单"的原因)。 */
|
|
2284
|
+
const menuLeft = menuEl ? (Number.parseFloat(menuEl.style.left) || menuAtX) : menuAtX
|
|
2285
|
+
const menuTop = menuEl ? (Number.parseFloat(menuEl.style.top) || menuAtY) : menuAtY
|
|
2240
2286
|
let x = menuLeft + menuW + 2
|
|
2241
2287
|
flyoutSide = 'right'
|
|
2242
2288
|
if (x + SUBMENU_W > vw() - 8) {
|
|
@@ -2521,10 +2567,10 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2521
2567
|
{ kind: 'sep' },
|
|
2522
2568
|
/* 专注:最常用的动作,放最上面;进行中时这一行本身就是状态 + 结束 */
|
|
2523
2569
|
...(focusUntil > 0
|
|
2524
|
-
? [{ label: '专注中 · 还剩 ' + Math.max(1, Math.ceil(focusLeftMs() / 60000)) + ' 分钟', run: () => stopFocus(false), accent: true }]
|
|
2570
|
+
? [{ label: '专注中 · 还剩 ' + Math.max(1, Math.ceil(focusLeftMs() / 60000)) + ' 分钟', run: () => stopFocus(false), accent: true, glyph: '⏱' }]
|
|
2525
2571
|
: [
|
|
2526
|
-
{ label: '专注 ' + cfg.focusMinutes + ' 分钟', run: () => startFocus(cfg.focusMinutes), accent: true },
|
|
2527
|
-
{ label: '专注 45 分钟', run: () => startFocus(45) },
|
|
2572
|
+
{ label: '专注 ' + cfg.focusMinutes + ' 分钟', run: () => startFocus(cfg.focusMinutes), accent: true, glyph: '⏱' },
|
|
2573
|
+
{ label: '专注 45 分钟', run: () => startFocus(45), glyph: '⏱' },
|
|
2528
2574
|
]),
|
|
2529
2575
|
{ kind: 'sep' },
|
|
2530
2576
|
{
|
|
@@ -2539,16 +2585,14 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2539
2585
|
say('换成' + skinLabel(id) + '了。')
|
|
2540
2586
|
},
|
|
2541
2587
|
})),
|
|
2542
|
-
{ label: '放大一点', run: () => applyConfig({ size: cfg.size + 0.5 }) },
|
|
2543
|
-
{ label: '缩小一点', run: () => applyConfig({ size: cfg.size - 0.5 }) },
|
|
2544
2588
|
],
|
|
2545
2589
|
},
|
|
2546
2590
|
{
|
|
2547
2591
|
group: '行为',
|
|
2548
2592
|
summary: onToggleCount() + '/' + TOGGLES.length + ' 开',
|
|
2549
2593
|
children: [
|
|
2550
|
-
{ label: '说句话', run: () => { wake(true); say(pick(LINES.click)) } },
|
|
2551
|
-
{ label: '睡一会儿', run: () => setMood('sleep') },
|
|
2594
|
+
{ label: '说句话', glyph: '✦', run: () => { wake(true); say(pick(LINES.click)) } },
|
|
2595
|
+
{ label: '睡一会儿', glyph: '☾', run: () => setMood('sleep') },
|
|
2552
2596
|
/* 开关是**真正的开关行**:带 aria-checked,点完菜单不关(就在原地更新) */
|
|
2553
2597
|
...TOGGLES.map((t) => ({ kind: 'switch', key: t.key, label: t.label })),
|
|
2554
2598
|
],
|
|
@@ -2560,11 +2604,11 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2560
2604
|
},
|
|
2561
2605
|
{ kind: 'slider' },
|
|
2562
2606
|
{ kind: 'sep' },
|
|
2563
|
-
{ label: '动作一览…', run: () => actionsDialog() },
|
|
2564
|
-
{ label: '关于她…', run: () => aboutDialog() },
|
|
2607
|
+
{ label: '动作一览…', glyph: '▤', run: () => actionsDialog() },
|
|
2608
|
+
{ label: '关于她…', glyph: 'ⓘ', run: () => aboutDialog() },
|
|
2565
2609
|
{ kind: 'sep' },
|
|
2566
2610
|
/* 一个开关,不是一个单向动作:从迷你按钮右键打开菜单时,它必须能把她叫回来。 */
|
|
2567
|
-
{ label: hidden ? '回来吧' : '先躲起来', run: () => setHidden(!hidden), muted: true },
|
|
2611
|
+
{ label: hidden ? '回来吧' : '先躲起来', glyph: '⤓', run: () => setHidden(!hidden), muted: true },
|
|
2568
2612
|
{ kind: 'hint' },
|
|
2569
2613
|
]
|
|
2570
2614
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-wisp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.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/tools/smoke.mjs
CHANGED
|
@@ -286,16 +286,30 @@ check(kbd.focusAfter === 'wisp-body', 'and focus lands back on her, not on <body
|
|
|
286
286
|
const openedAgain = await openMenu()
|
|
287
287
|
check(openedAgain !== null, 'the menu reopens for the activation check')
|
|
288
288
|
const zoomed = await page.evaluate(() => {
|
|
289
|
-
/*
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
/* 大小已经是菜单里的**滑块**了("放大一点/缩小一点"两条旧条目已删 —— 与滑块重复)。
|
|
290
|
+
真浏览器里直接聚焦滑块再按 →,走的就是键盘用户那条路。 */
|
|
291
|
+
const slider = document.querySelector('.wisp-slider')
|
|
292
|
+
if (!slider) return { missing: true }
|
|
293
|
+
const before = window.__wisp.config.size
|
|
294
|
+
/* 用**真实键盘**走到滑块:Home 回到第一项,再一路 ↓ 直到焦点落在滑块上。
|
|
295
|
+
不能直接 .focus() —— 插件用的是 roving tabindex,直接聚焦不会更新它内部的焦点索引,
|
|
296
|
+
于是 → 键到了插件那儿会被当成"焦点不在滑块上",什么都不做(踩过)。 */
|
|
297
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' }))
|
|
298
|
+
for (let i = 0; i < 16 && document.activeElement !== slider; i++) {
|
|
299
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
|
|
300
|
+
}
|
|
301
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }))
|
|
302
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }))
|
|
303
|
+
return {
|
|
304
|
+
before,
|
|
305
|
+
after: window.__wisp.config.size,
|
|
306
|
+
width: document.querySelector('.wisp-root').style.width,
|
|
307
|
+
menuGone: document.querySelector('.wisp-menu') === null,
|
|
308
|
+
}
|
|
296
309
|
})
|
|
297
|
-
check(zoomed.
|
|
298
|
-
|
|
310
|
+
check(zoomed.after === zoomed.before + 0.5 && zoomed.menuGone === false,
|
|
311
|
+
'the size slider changes her size and keeps the menu open',
|
|
312
|
+
zoomed.missing ? 'the slider was not found' : `${zoomed.before} -> ${zoomed.after}, width=${zoomed.width}, menuGone=${zoomed.menuGone}`)
|
|
299
313
|
|
|
300
314
|
/* 两个弹窗在真浏览器里的样子 —— 可见性、图片真的解码出来,这些假 DOM 测不到。
|
|
301
315
|
注意位置:必须放在键盘那一段**之后**。放前面会扰动它依赖的焦点状态
|
|
@@ -581,61 +595,100 @@ check(skinSwap.layersDuringFade === 1, 'the cross-fade layer is gone once it set
|
|
|
581
595
|
// 每个页面都要装下这份包与解码后的精灵图(单张就是 naturalWidth*naturalHeight*4 字节),
|
|
582
596
|
// 多开一个页面 = 多一份;之前正是这样把浏览器推崩的,表现为随机的
|
|
583
597
|
// "Target page, context or browser has been closed",极难定位。
|
|
584
|
-
/*
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
598
|
+
/* 级联子菜单的几何:**四个角落各测一次**。
|
|
599
|
+
她在角落里时菜单会被夹进视口(右下/左下往上推、右上往左推),
|
|
600
|
+
子面板必须跟着菜单的**实际**位置走 —— 用"请求坐标"算就会越靠角落偏得越多,
|
|
601
|
+
严重时压住主菜单。用户就是这样发现该 bug 的,所以这里直接把它编成回归检查。
|
|
602
|
+
假 DOM 没有布局,这一整类问题只有真浏览器能测。 */
|
|
603
|
+
const cornerGeo = []
|
|
604
|
+
for (const which of ['br', 'bl', 'tr', 'tl']) {
|
|
605
|
+
const geo = await page.evaluate(async (corner) => {
|
|
606
|
+
const wisp = window.__wisp
|
|
607
|
+
const box = document.querySelector('.wisp-root')
|
|
608
|
+
const bw = parseFloat(box.style.width) || 560
|
|
609
|
+
const bh = parseFloat(box.style.height) || 840
|
|
610
|
+
const vw = window.innerWidth
|
|
611
|
+
const vh = window.innerHeight
|
|
612
|
+
const right = (corner === 'br' || corner === 'tr') ? 26 : Math.max(0, vw - 26 - bw)
|
|
613
|
+
const bottom = (corner === 'br' || corner === 'bl') ? 46 : Math.max(0, vh - 46 - bh)
|
|
614
|
+
wisp.configure({ right, bottom })
|
|
615
|
+
wisp.resetPosition()
|
|
616
|
+
await new Promise((resolve) => setTimeout(resolve, 120))
|
|
617
|
+
/* 主菜单的入口是 .wisp-body 上的 Enter —— contextmenu 只挂在"躲起来"之后的迷你标签上。 */
|
|
618
|
+
const body = document.querySelector('.wisp-body')
|
|
619
|
+
body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
|
|
620
|
+
const menu = document.querySelector('.wisp-menu')
|
|
621
|
+
if (!menu) return { which: corner, noMenu: true }
|
|
622
|
+
const row = [...menu.querySelectorAll('.wisp-menu-item')].find((el) => el.dataset && el.dataset.group)
|
|
623
|
+
if (!row) return { which: corner, noRow: true }
|
|
624
|
+
row.dispatchEvent(new PointerEvent('pointerenter', { pointerType: 'mouse' }))
|
|
625
|
+
await new Promise((resolve) => setTimeout(resolve, 320))
|
|
626
|
+
const panel = document.querySelector('.wisp-submenu')
|
|
627
|
+
if (!panel) return { which: corner, noPanel: true }
|
|
628
|
+
const mr = menu.getBoundingClientRect()
|
|
629
|
+
const pr = panel.getBoundingClientRect()
|
|
630
|
+
const rr = row.getBoundingClientRect()
|
|
631
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
632
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
633
|
+
return {
|
|
634
|
+
which: corner,
|
|
635
|
+
menu: { l: Math.round(mr.left), r: Math.round(mr.right), t: Math.round(mr.top) },
|
|
636
|
+
panel: { l: Math.round(pr.left), r: Math.round(pr.right), t: Math.round(pr.top) },
|
|
637
|
+
row: { t: Math.round(rr.top) },
|
|
638
|
+
vw, vh,
|
|
639
|
+
}
|
|
640
|
+
}, which)
|
|
641
|
+
cornerGeo.push(geo)
|
|
642
|
+
}
|
|
643
|
+
/* 可选截图:WISP_SHOT=<路径> 时把"菜单 + 展开的子面板"拍下来。
|
|
644
|
+
样式改动光看代码看不出来,拍一张最直接。默认不拍,不影响正常冒烟。 */
|
|
645
|
+
if (process.env.WISP_SHOT) {
|
|
646
|
+
try {
|
|
647
|
+
await page.evaluate(async () => {
|
|
648
|
+
const wisp = window.__wisp
|
|
649
|
+
const box = document.querySelector(".wisp-root")
|
|
650
|
+
const bw = parseFloat(box.style.width) || 560
|
|
651
|
+
const bh = parseFloat(box.style.height) || 840
|
|
652
|
+
wisp.configure({ right: 26, bottom: 46 })
|
|
653
|
+
wisp.resetPosition()
|
|
654
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
655
|
+
const body = document.querySelector(".wisp-body")
|
|
656
|
+
body.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }))
|
|
657
|
+
const menu = document.querySelector(".wisp-menu")
|
|
658
|
+
const row = [...menu.querySelectorAll(".wisp-menu-item")].find((el) => el.dataset && el.dataset.group)
|
|
659
|
+
row.dispatchEvent(new PointerEvent("pointerenter", { pointerType: "mouse" }))
|
|
660
|
+
await new Promise((resolve) => setTimeout(resolve, 420))
|
|
661
|
+
void bw; void bh
|
|
662
|
+
})
|
|
663
|
+
const clipBox = await page.evaluate(() => {
|
|
664
|
+
const els = [".wisp-menu", ".wisp-submenu"].map((sel) => document.querySelector(sel)).filter(Boolean)
|
|
665
|
+
const rects = els.map((el) => el.getBoundingClientRect())
|
|
666
|
+
const left = Math.min(...rects.map((r) => r.left)) - 22
|
|
667
|
+
const top = Math.min(...rects.map((r) => r.top)) - 22
|
|
668
|
+
const right = Math.max(...rects.map((r) => r.right)) + 22
|
|
669
|
+
const bottom = Math.max(...rects.map((r) => r.bottom)) + 22
|
|
670
|
+
return { x: Math.max(0, left), y: Math.max(0, top), width: right - left, height: bottom - top }
|
|
671
|
+
})
|
|
672
|
+
await page.screenshot({ path: process.env.WISP_SHOT, clip: clipBox })
|
|
673
|
+
console.log(" screenshot: " + process.env.WISP_SHOT)
|
|
674
|
+
} catch (error) {
|
|
675
|
+
console.log(" screenshot failed: " + String(error && error.message ? error.message : error))
|
|
618
676
|
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
check(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
check(
|
|
631
|
-
'and
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
/* 菜单的键盘监听挂在 **window** 上,不是 document —— 发错目标菜单就不会关,
|
|
635
|
-
后面的键盘段会在"一个还没关掉的菜单"上重开,焦点状态全乱(就是这么挂掉三条的)。 */
|
|
636
|
-
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
637
|
-
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
638
|
-
})
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const withPanel = cornerGeo.filter((g) => g && g.panel)
|
|
680
|
+
check(withPanel.length === 4, 'the submenu opens from every corner',
|
|
681
|
+
cornerGeo.map((g) => g.which + (g.panel ? '' : '✗')).join(' '))
|
|
682
|
+
check(withPanel.every((g) => g.panel.r <= g.menu.l + 6 || g.panel.l >= g.menu.r - 6),
|
|
683
|
+
'BESIDE the menu in every corner — never on top of it',
|
|
684
|
+
withPanel.map((g) => `${g.which} menu=${g.menu.l}..${g.menu.r} panel=${g.panel.l}..${g.panel.r}`).join(' | '))
|
|
685
|
+
check(withPanel.every((g) => g.panel.l >= 0 && g.panel.r <= g.vw && g.panel.t >= 0 && g.panel.t < g.vh),
|
|
686
|
+
'and inside the viewport in every corner',
|
|
687
|
+
withPanel.map((g) => `${g.which} l=${g.panel.l} r=${g.panel.r} t=${g.panel.t}`).join(' | '))
|
|
688
|
+
check(withPanel.every((g) => Math.abs(g.panel.t - g.row.t) < 40),
|
|
689
|
+
'and lined up with its own row in every corner',
|
|
690
|
+
withPanel.map((g) => `${g.which} row.t=${g.row.t} panel.t=${g.panel.t}`).join(' | '))
|
|
691
|
+
|
|
639
692
|
|
|
640
693
|
await browser.close()
|
|
641
694
|
|