Files
MailUI4Agents/client/electron/src/components/NarrowNav.tsx
JianFeeeee 9aa702b8cc fix(webui): 导航改"按主题的玻璃"(浅色白玻璃+深字)+ 去掉整屏闪的入场动画
用户两句话:
  「那你为什么不把白字换成黑字或者自动反色或者描边呢?」
  「部分动画十分不合理,会导致页面大范围的闪动,且不能让人自然的把注意力集中在
    将要出现的页面上」

## ① 导航:他说得对,我之前是用错误的方式解决对比度

这个应用是**浅色底 + 深字**,导航却是全页唯一一块黑的 —— 那才是"割裂"。我上一轮
为了"白字对比度"把它做成深玻璃,等于**把一块地方永久压黑**来回避问题。

现在导航底色/文字全部走令牌,按主题切换:

    浅色:白玻璃 rgb(255 255 255 / .72) + 深字 #475569   → 对比度 7.48:1
    深色:深玻璃 rgb(15 23 42 / .72)   + 亮字 #94a3b8   → 自动反色

组件侧改成 `.nav-rail` / `.nav-item[data-active]` 令牌类(Sidebar 与 NarrowNav 同一套)。
同时删掉壁纸模式里写死的深玻璃规则 —— 那条正是"为什么还是黑色"。

## ② 动画:整面板入场删掉

先是从"所有面板一起动"改成"只动主内容区",试下来仍然不对:页面级淡入会把
**已经在那儿的框架**也一起暗一下,观感还是闪。所以这一档整体删掉,只保留局部、
有明确语义的动效(日历翻页、菜单展开)。实测切视图时 `animatedOnSwitch = 0`。
骨架不动,注意力自然落在变化的那块内容上。

## ③ 一条我自己的误判(值得记下)

深色主题下我量到导航文字对比度只有 2.36,一度以为是变量/级联的问题,还写死了一份
深色字面值。**那是误判**:`.nav-item` 有 `transition: color .15s`,我在切主题后
**立刻**读 computed color,拿到的是过渡的**起点**(浅色值)。决定性证据是连内联
`style.color` 都"改不动"它 —— 级联不可能这样,只可能是还在过渡中。等 400ms 再量就正常。
写死的那份已撤掉,并在 CSS 里留下说明;新判据 `test/manual/nav-contrast-verify.mjs`
用 WCAG 比值量对比度(不是"颜色是不是黑的"),每次读之前等 400ms。

背景套件 32 条全绿(含"导航走令牌 + 两套令牌都在")。
2026-09-14 11:57:41 +08:00

122 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useUIStore, type ViewMode } from '../stores/uiStore';
import { useMailStore } from '../stores/mailStore';
import { useContactStore } from '../stores/contactStore';
import { useAuthStore } from '../stores/authStore';
import {
InboxIcon,
ContactsIcon,
PersonIcon,
CalendarIcon
} from './icons';
import { ConnectionIndicator } from './ConnectionIndicator';
import { countPendingPermissions, splitByPermission } from '../lib/mailGroups';
/**
* 窄屏底部导航。
*
* 移动端把主导航放底部而不是顶部:拇指够得到。
* 宽屏用的是左侧竖条(Sidebar),两者共用 uiStore 的 viewMode,
* 所以从窄拖到宽不会丢失当前位置。
*
* 这里只放最常用的几项 + 一个「更多」入口(打开抽屉式 Sidebar)——
* 底部塞满图标会挤成一排看不懂的小方块。
*/
const items: {
short: string;
mode: ViewMode;
/** 哪些 viewMode 算这一项选中(「通信」含三个子页签) */
modes?: ViewMode[];
Icon: (p: { className?: string }) => JSX.Element;
adminOnly?: boolean;
}[] = [
// 与桌面侧栏同一套合并(用户:「导航项就只剩通信,日历,联系人」):
// 收件/发件/授权 = 一项「通信」,进去由 CommTabs 区分;管理移到「我的」页底部。
{ short: '通信', mode: 'inbox', modes: ['inbox', 'sent', 'permissions'], Icon: InboxIcon },
{ short: '日历', mode: 'calendar', Icon: CalendarIcon },
{ short: '联系人', mode: 'contacts', Icon: ContactsIcon }
];
export default function NarrowNav() {
const viewMode = useUIStore(s => s.viewMode);
const setViewMode = useUIStore(s => s.setViewMode);
const composing = useUIStore(s => s.composing);
const narrowPane = useUIStore(s => s.narrowPane);
const inbox = useMailStore(s => s.inbox);
// 与 Sidebar 同一判据:权限请求归「授权」,不算进收件箱未读
const { normal, permissions } = splitByPermission(inbox);
const unread = normal.filter(m => m.status === 'unread').length;
const pendingPerms = countPendingPermissions(permissions);
const contacts = useContactStore(s => s.contacts);
const user = useAuthStore(s => s.user);
const isAdmin = user?.role === 'admin';
const visible = items.filter(n => !n.adminOnly || isAdmin);
// 点「通信」回到上次用的子页签(与桌面侧栏同一语义)
const commTab = useUIStore(s => s.commTab);
return (
<nav
// 悬浮玻璃条的样式全在 index.css 的 .narrow-nav 里(含安全区外边距):
// 这里不再写 border-t/bg-chrome-900 —— 那是"贴底通栏"的写法,与悬浮冲突
className="narrow-nav shrink-0 flex items-stretch"
>
{visible.map(({ short, mode, modes, Icon }) => {
const itemModes = modes ?? [mode];
// 详情栏打开时不高亮任何导航项:此刻用户看的是某封邮件
const active =
itemModes.includes(viewMode) && !composing && narrowPane === 'list';
// 「通信」合并了三项:徽标 = 未读 + 待决策(授权信息不能因为合并而消失)
const isComm = itemModes.includes('inbox');
const badge = isComm
? unread + pendingPerms
: itemModes.includes('contacts')
? contacts.length
: 0;
const badgeTone = isComm && pendingPerms > 0 ? 'perm' : isComm ? 'unread' : 'plain';
return (
<button
key={short}
onClick={() => setViewMode(isComm ? commTab : mode)}
data-active={active}
className="nav-item relative flex-1 py-2 flex flex-col items-center justify-center gap-0.5"
>
<Icon />
<span className="text-3xs leading-none">{short}</span>
{badge > 0 && (
<span
className={`absolute top-1 right-[22%] min-w-[15px] h-[15px] px-1 rounded-full text-3xs font-bold flex items-center justify-center ${
badgeTone === 'unread'
? 'bg-red-600 text-white'
: badgeTone === 'perm'
? 'bg-orange-700 text-white'
: 'bg-chrome-600 text-chrome-100'
}`}
>
{badge > 99 ? '99+' : badge}
</span>
)}
{active && <span className="absolute top-0 left-1/4 right-1/4 h-0.5 bg-blue-400" />}
</button>
);
})}
{/* 新建改到列表栏的悬浮圆钮(ComposeFab),与桌面一致 */}
<button
onClick={() => setViewMode('account')}
data-active={viewMode === 'account' && !composing}
className="nav-item flex-1 py-2 flex flex-col items-center justify-center gap-0.5"
>
<div className="relative">
<PersonIcon />
<span className="absolute -top-0.5 -right-1.5">
<ConnectionIndicator />
</span>
</div>
<span className="text-3xs leading-none">我的</span>
</button>
</nav>
);
}