feat(webui): 导航合并 + 悬浮玻璃 + 页面动画 + 控件档透明度 + 圆角无条件生效

用户四封信(同一线索)提的六件事,都在这里:

## ① 导航合并(「导航栏的内容有点多了」)

收件箱/发件箱/授权 → 一项「通信」,进去由**内部页签**区分(CommTabs);
新建 → 「通信」页内**悬浮圆形加号**(ComposeFab);导航只剩 通信/日历/联系人;
管理 → 合进「我的」,管理员在页面**最下面**看得到(非管理员不渲染,不是禁用)。

`viewMode` 仍是原来那三个值(几十处调用点不用改),新增 `commTab` 记住上次用的子页签;
通信项的徽标 = 未读 + 待决策(授权信息不能因为合并而消失)。

## ② 窄屏底部导航:悬浮玻璃(「还是固定底部延伸,没有玻璃效果,也没有悬浮」)

原来是 `border-t bg-chrome-900` 通栏贴底。改成:左右/离底各留 10px、圆角、
深色半透明 + `blur(18px)`、投影,安全区并入下外边距。

## ③ 页面动画(「页面极度缺少动画,所有页面都是直接出现」)

在 `<html>` 上挂一个短命类触发外壳直接子面板的入场动画。**没有用 key 重挂载**:
面板是 flex 链上的一环,套 wrapper 会改掉 flex 传递(窄屏覆盖层最先坏)。
并且尊重 `prefers-reduced-motion`(关了就一点都不动)。

## ④ 控件档透明度(「复选框和地址猜测项…他们才是真正需要拉低透明度的地方」)

新增第三档 `--bg-glass-control`(0.5/0.55):授权多选胶囊、地址建议菜单。
三档递减:正文面 0.88 > 嵌套 0.82 > 控件 0.5。

## ⑤ 打底 vs 透明(上一封「该打底的地方透了、该透的空白处糊了」)

正文面回到 0.88(读得清优先)、空白/页面底透明**且不模糊**、面板模糊降到 10px。

## ⑥ 圆角与玻璃**无条件**生效(「大面积缺失圆角与玻璃效果…都是硬截断」)

根因:`.app-shell` 的留缝/圆角/投影原先全写在 `html[data-bg='on']` 里
⇒ **没开壁纸的账号**看到的是硬边不透明面板。现在几何下沉为无条件基线,
壁纸相关的加强仍叠加在上面。窄屏内容面板同样浮起来。

## 判据

- `test/nav-merge.test.mjs` 8 条(含自检:重构前的写法必须判红)——
  改完跑老套件 254 条**全绿却一条都没测导航结构**,结构改动必须自带判据。
- `test/manual/nav-restructure-verify.mjs`:真浏览器 9 条(导航项数、页签切换真的换内容、
  加号 56×56 且 `elementFromPoint` 命中自己、我的页底部管理入口在退出登录之后、
  控件档 α=0.5 对正文面 α=0.88 的反向对照)。
- `test/manual/narrow-glass-verify.mjs` 6 条:悬浮几何(三边留缝 10px、圆角 14px、
  缝隙处命中的是页面底 ⇒ 真的浮着)、玻璃(α=0.82 + blur18)、触摸高度最小 49px、
  动画真的在跑(静止时 `animationName=none`,切换后 =pane-in)且 reduced-motion 下不动。
- `test/background.test.mjs`:把三条**过时判据**(早前那版"越透越好")按现行契约重写 ——
  旧判据留着只会把我拽回错误方向。30 条全绿。

## 顺带

窄屏探针原先只调视口、**没模拟触屏**,于是 `(hover: hover)` 仍为真,
把「悬停才显形的次要动作」误报成透明 —— 差点去"修"一个本来就对的规则。
已改成 hasTouch + isMobile + DPR 的触屏上下文,且收尾只关自己的 context
(CDP 连的是共享 Chromium,`browser.close()` 会把别人的标签页一起关掉)。
This commit is contained in:
2026-09-14 10:35:05 +08:00
parent 552fbc731e
commit 76ce201c3a
15 changed files with 911 additions and 107 deletions

View File

@ -2,10 +2,23 @@ import { create } from 'zustand';
export type ViewMode = 'inbox' | 'sent' | 'permissions' | 'calendar' | 'contacts' | 'admin' | 'account';
/**
* 「通信」这一个导航项内部的三个页签。
*
* 2026-09-14 用户:「导航栏的内容有点多了,收件发件授权改为一个导航项,通过内部导航区分」。
* 于是 `viewMode` 仍然是那三个值(几十处调用点不用改),但导航栏只画一项「通信」;
* 这一项点击时回到**上次用的那个页签**(`commTab`),进入后再由内部页签切换。
*/
export type CommTab = 'inbox' | 'sent' | 'permissions';
interface UIState {
viewMode: ViewMode;
setViewMode: (mode: ViewMode) => void;
/** 「通信」导航项记忆的子页签(收件箱/发件箱/授权) */
commTab: CommTab;
setCommTab: (tab: CommTab) => void;
/** 右侧主区域是否处于「新建邮件」整页编写态 */
composing: boolean;
composePrefill: { to?: string; cc?: string } | null;
@ -29,7 +42,7 @@ interface UIState {
reset: () => void;
}
export const useUIStore = create<UIState>(set => ({
export const useUIStore = create<UIState>((set, get) => ({
viewMode: 'inbox',
// 切换主视图时回到列表栏:窄屏下停在上一封邮件的详情页会让人不知道自己在哪
setViewMode: mode =>
@ -37,9 +50,21 @@ export const useUIStore = create<UIState>(set => ({
viewMode: mode,
composing: false,
composePrefill: null,
narrowPane: 'list'
narrowPane: 'list',
// 通信的三个子页签要记住:点「通信」时回到上次那个,而不是永远回收件箱
...(mode === 'inbox' || mode === 'sent' || mode === 'permissions'
? { commTab: mode as CommTab }
: {})
}),
commTab: 'inbox',
// 点内部页签 = 走同一条切换路径(这样"记住上次"的语义只有一处实现)
setCommTab: (tab: CommTab) => {
const st = get();
if (st.viewMode === tab && !st.composing) return;
st.setViewMode(tab);
},
composing: false,
composePrefill: null,
// 写信占满整个主区域,窄屏下等价于切到 detail 栏
@ -54,6 +79,7 @@ export const useUIStore = create<UIState>(set => ({
reset: () =>
set({
viewMode: 'inbox',
commTab: 'inbox',
composing: false,
composePrefill: null,
narrowPane: 'list'