## 问题
用户:「现在窗口外观还是 ele 默认外观,很原始」。
查证了两端在 PC 上的实际做法,差距是**结构性的**:
| | HarmonyOS (PC/2in1) | Electron(改之前)|
|---|---|---|
| 窗口装饰 | `setWindowDecorVisible(false)` 隐掉系统标题栏 | 系统默认标题栏 |
| 顶栏 | 自绘 `AppHeader`(圆角/材质/标题/可选返回键) | 无 |
| 避让 | `Insets` 三量:`statusBar`/`navIndicator`/`windowDecor` | 无这个概念 |
| 拖动 | 系统 | 系统 |
⇒ `grep app-region|titleBarStyle` 在整个 `client/electron/` 命中 **0**。
鸿蒙那边早就走完「内容铺满 + 自绘顶栏」,Electron 还停在最原始的系统窗口。
## 修法
* `frame: false` + `titleBarStyle: 'hidden'` —— 两个**一起**。
只给 `titleBarStyle` 在 Win/Linux 上仍留着系统边框(可拖动、可双击),
观感还是「系统窗口 + 一条自己画的头」;既然自绘窗口按钮 = 框架整个接管,
那圈系统边框就是多余的一层。
★ 不用 `titleBarOverlay`(官方「留系统按钮」那条):它留的是**系统**按钮,
观感仍由系统决定,与「自绘」目标相反,且 Linux 支持不齐。
本项目只有 win/linux target(无 mac)⇒ 统一一条路,不做两套形态。
* 新增 `src/components/TitleBar.tsx`:固定顶栏 + 左标题 + 右三键。
★ 挂点选在 `main.tsx`、与 `.app-backdrop` 同层,**不在 App 里面** ——
App 的根节点有四个 return 分支(宽屏/窄屏/独页/…),
塞进去就得改四处,漏一处就是「某个页面没有标题栏」。
它自己是 `position: fixed`,与 App 布局零耦合。
## 三条实现纪律(都写进注释了)
① **拖动靠 CSS `-webkit-app-region: drag`,不靠 JS 鼠标事件。**
`drag` 区域由浏览器/系统处理,不受页面重排影响(sandbox 下那种
mousemove 算窗口位置的写法既慢又脆)。
★ 代价:**drag 区域里的交互元素收不到点击** ⇒ 按钮与标题文字都显式
`no-drag`。实测 `elementFromPoint` 命中 `BUTTON.titlebar-btn`(不是拖拽层)。
② **最大化状态是「订阅」来的,不是「查」来的。**
最大化有三条**不经过按钮**的路径:双击拖拽区(系统处理,JS 收不到事件)、
`Win+↑↓`、拖到屏幕边缘的 Snap Layouts ⇒ 只在点按钮时查一次,
图标必然与真实状态脱节。所以主进程用 `pushMaxState` 主动推
(`maximize`/`unmaximize`/进退全屏四个事件),渲染层只订阅。
另:`toggleMaximize` 刻意**不**拆成 maximize/unmaximize ——
双击时序上会多一次异步往返,IPC 往返期间用户可能又双击了一次。
⇒ 读状态与决定动作在主进程侧原子完成。
③ **浏览器里整条不渲染。**
没有 bridge 时 `TitleBar` 返回 `null`;高度占位(`html.titlebar-on`)
由 `main.tsx` 用**同一个** `__AGENTMAIL_SHELL__` 判据挂上 ——
CSS 不会看 bridge,不挂则网页端白丢 36px。
## 尺寸为什么是 36px
鸿蒙 PC/2in1 实测 `windowDecor=37`(见 `MainPage.ets` 的 insets 日志
statusBar=38.6 navIndicator=27.8 windowDecor=37)⇒ 两端窗口控件高度对齐同一量级,
免得并排摆两个应用时一个头厚一个头薄。这里取 36:桌面端按物理像素算,
1x 下更接近常见做法,且 12px 字号不出血。**要改就两端一起改。**
## 顺带记一条踩过的坑(它就在这批代码里)
`preload.cjs` 是 `.cjs`,**不能写 TS 类型标注**。第一版写了
`(cb: (maximized: boolean) => void)` ⇒ 整个 preload **静默**加载失败 ⇒
`window.agentmail === undefined` ⇒ 账号读不到(回退到网页版登录页,
显示用户名+密码,桌面壳里注定失败)+ 标题栏不渲染。
★ 而 `npm run typecheck`(`tsc --noEmit`)**不检查 .cjs**,照常全绿。
已在 preload 注释里写明,并在 `main-process-security.test.mjs` 加了加载闸门
(下一个提交)。
## 实测
`DISPLAY` 起真窗口 + CDP 取证:
shell=desktop hasBridge=true hasWin=true titlebar=true h=36 appRegion=drag
btns=[最小化, 还原, 关闭] btnHitTarget=BUTTON.titlebar-btn
errs=[](渲染层零异常)
标题栏实测截图含深浅两态,面板圆角与阴影不变。
## 未验(本机无 GUI 交互,只能取证不能点)
最大化/还原按钮点击、双击标题栏、**关闭进托盘**(这个最需要小心,
点错会把应用整个退出)。上一条留待有人手上有真桌面时验。
1934 lines
80 KiB
CSS
1934 lines
80 KiB
CSS
@tailwind base;
|
||
@tailwind components;
|
||
@tailwind utilities;
|
||
|
||
/*
|
||
* ─── 主题色板 ───
|
||
*
|
||
* 全站颜色都经 tailwind.config.js 指向这些变量,因此深色模式**不需要**
|
||
* 在组件里写 dark: 前缀。逐处加前缀的方案在这里必然失败:约 700 处颜色
|
||
* 散在 21 个组件里,漏一处就是深色下的白底白字,而且只有肉眼能发现。
|
||
*
|
||
* 深色模式的做法是**反转灰阶**:white → 近黑、gray-900 → 近白。
|
||
* 这套代码里灰阶本身就是语义色阶(表面层次 / 分隔线 / 文字主次),
|
||
* 反转之后 `bg-white text-gray-900` 自动变成「深色卡片 + 浅色文字」。
|
||
* 新加的组件照常写浅色类名也自动适配。
|
||
*
|
||
* 值存 RGB 三元组而不是 #hex:代码里有 bg-blue-50/70 这类透明度修饰符,
|
||
* Tailwind 会生成 rgb(var(--x) / 0.7),而 rgb(#f9fafb / 0.7) 是无效 CSS
|
||
* —— 那些半透明高亮会静默失效(不报错,只是不透明)。
|
||
*/
|
||
:root {
|
||
/*
|
||
* 表面色(卡片 / 输入框底)。深色模式下变暗。
|
||
*/
|
||
--c-white: 255 255 255;
|
||
/*
|
||
* ★ 玻璃的"基材":**永远是白色**(2026-09-14 用户:「什么叫深色模式也是玻璃?
|
||
* 深色模式不应该是浅色玻璃吗?」)。
|
||
*
|
||
* 他是对的,我原先的说法("深色玻璃 = rgb(15 23 42 / .72)")是概念错误:
|
||
* 磨砂玻璃是**白色材料**,深色模式下要做的是**降低这层白的透明度**让深背景透出来,
|
||
* 而不是换成一整层深色 —— 后者不是玻璃,是把面板压黑。
|
||
*
|
||
* 之前所有玻璃面都写成 `rgb(var(--glass-base) / α)`,而 `.dark` 把 `--c-white`
|
||
* 改成了 `24 27 33`(近黑)⇒ 深色模式下**整个玻璃层系**变成深色,
|
||
* 而 Tailwind 的浅色工具类照旧 ⇒ 界面半深不浅。玻璃与"白"这个颜色概念的耦合就在这里。
|
||
*/
|
||
--glass-base: 255 255 255;
|
||
|
||
/*
|
||
* 彩色按钮与深色框架上的文字。**不随主题反转。**
|
||
*
|
||
* 它与 --c-white 必须分开,因为 `white` 在这套代码里服务两种互相冲突的用途:
|
||
* - `bg-white` = 卡片表面 → 深色模式必须变暗
|
||
* - `text-white` = 按钮上文字 → 深色模式必须保持浅色
|
||
*
|
||
* 共用一个变量时后者跟着变暗,实测激活导航项的「收件」在 bg-chrome-700 上
|
||
* 只剩 1.34:1 —— 几乎看不见。见 tailwind.config.js 的 textColor 覆盖。
|
||
*/
|
||
--c-on-accent: 255 255 255;
|
||
|
||
--c-gray-50: 249 250 251;
|
||
--c-gray-100: 243 244 246;
|
||
/*
|
||
* 分隔线刻意比「正牌 gray-200」再淡一档。
|
||
*
|
||
* 全站 67 处 `border-*-gray-200` 是「盒子感」的主要来源:硬线把界面切成
|
||
* 一块块,看着像 2015 年的后台。现代做法是让**留白与表面色差**承担分组,
|
||
* 线条只留一丝提示。改在令牌上而不是逐个类名,67 处(含 8 处作为底色的
|
||
* 进度条/骨架)一次性生效,且不会漏。
|
||
*/
|
||
--c-gray-200: 234 236 241;
|
||
--c-gray-300: 209 213 219;
|
||
/* 最小字号与 placeholder 也会使用这一档;在白底上保持至少 4.5:1。 */
|
||
--c-gray-400: 107 114 128;
|
||
/*
|
||
* gray-500 必须比 gray-400 **深一档**。
|
||
*
|
||
* 原来两者都是 107 114 128(完全相同)—— 调色板里因而不存在「比次要文字
|
||
* 再深一点的中间色」,于是时间戳这类要压在高亮行淡蓝底上的文字没得选,
|
||
* 只能停在 gray-400:实测在 bg-blue-50 上只有 4.44:1,低于 AA 4.5
|
||
* (真实渲染量得)。拉开一档后为 5.65:1。
|
||
*/
|
||
--c-gray-500: 90 98 112;
|
||
--c-gray-600: 75 85 99;
|
||
--c-gray-700: 55 65 81;
|
||
--c-gray-800: 31 41 55;
|
||
--c-gray-900: 17 24 39;
|
||
--c-gray-950: 3 7 18;
|
||
|
||
/*
|
||
* 应用框架(侧栏 / 底部导航)。
|
||
*
|
||
* 独立成一条色阶而不跟 gray 走:这两块**在浅色模式下本来就是深色的**
|
||
* (深色侧栏配浅色内容区是这套 UI 的原本设计)。并入反转的 gray 之后,
|
||
* 深色模式下 bg-slate-900 会变成近白色 —— 侧栏比内容区还亮,
|
||
* 整个层次翻过来(实测 rgb(243,245,248) vs 内容区 rgb(17,19,24))。
|
||
*/
|
||
--c-chrome-100: 241 245 249;
|
||
--c-chrome-200: 226 232 240;
|
||
--c-chrome-400: 148 163 184;
|
||
--c-chrome-600: 71 85 105;
|
||
--c-chrome-700: 51 65 85;
|
||
--c-chrome-800: 30 41 59;
|
||
--c-chrome-900: 15 23 42;
|
||
|
||
/*
|
||
* ─── 强调色(浅色)───
|
||
*
|
||
* 值就是 Tailwind 的官方色阶,浅色模式下视觉零变化。
|
||
*
|
||
* 这条色阶与灰阶一样是**语义色阶**,而且两段的用途相反:
|
||
* - 50–300 = 表面(chip 底、提示条底、徽标底、边框)→ 深色下变暗
|
||
* - 400–900 = 前景(文字、图标) → 深色下变亮
|
||
*
|
||
* 上一版把它们写成 tailwind.config.js 里的固定 hex,同时又留了一份
|
||
* accent() 定义 —— JS 对象字面量重复键后者胜出,而当时 index.css 里
|
||
* 没有对应的 --c-red-* 等变量。`rgb(var(--c-red-600) / 1)` 里变量未定义会让
|
||
* **整条声明失效**,于是 bg-red-600 退回透明、白字落在白卡片上:
|
||
* 按钮看不见但点得动(生产实测,32 个变量全部缺失)。
|
||
*/
|
||
/* blue */
|
||
--c-blue-50: 239 246 255;
|
||
--c-blue-100: 219 234 254;
|
||
--c-blue-200: 191 219 254;
|
||
--c-blue-300: 147 197 253;
|
||
--c-blue-400: 96 165 250;
|
||
--c-blue-500: 59 130 246;
|
||
--c-blue-600: 37 99 235;
|
||
--c-blue-700: 29 78 216;
|
||
--c-blue-800: 30 64 175;
|
||
--c-blue-900: 30 58 138;
|
||
/* red */
|
||
--c-red-50: 254 242 242;
|
||
--c-red-100: 254 226 226;
|
||
--c-red-200: 254 202 202;
|
||
--c-red-300: 252 165 165;
|
||
--c-red-400: 248 113 113;
|
||
--c-red-500: 239 68 68;
|
||
/* 比 Tailwind 官方的 220 38 38 略暗:官方值落在 red-50 上只有 4.41:1,
|
||
而 `bg-red-50 text-red-600` 正是错误提示条 —— 差 2% 也是差。
|
||
这一档同时用在白卡片上(4.83 → 5.10),压暗后两处都过线。 */
|
||
--c-red-600: 213 37 37;
|
||
--c-red-700: 185 28 28;
|
||
--c-red-800: 153 27 27;
|
||
--c-red-900: 127 29 29;
|
||
/* green */
|
||
--c-green-50: 240 253 244;
|
||
--c-green-100: 220 252 231;
|
||
--c-green-200: 187 247 208;
|
||
--c-green-300: 134 239 172;
|
||
--c-green-400: 74 222 128;
|
||
--c-green-500: 34 197 94;
|
||
--c-green-600: 22 163 74;
|
||
--c-green-700: 21 128 61;
|
||
--c-green-800: 22 101 52;
|
||
--c-green-900: 20 83 45;
|
||
/* amber */
|
||
--c-amber-50: 255 251 235;
|
||
--c-amber-100: 254 243 199;
|
||
--c-amber-200: 253 230 138;
|
||
--c-amber-300: 252 211 77;
|
||
--c-amber-400: 251 191 36;
|
||
--c-amber-500: 245 158 11;
|
||
--c-amber-600: 217 119 6;
|
||
--c-amber-700: 180 83 9;
|
||
--c-amber-800: 146 64 14;
|
||
--c-amber-900: 120 53 15;
|
||
/* orange */
|
||
--c-orange-50: 255 247 237;
|
||
--c-orange-100: 255 237 213;
|
||
--c-orange-200: 254 215 170;
|
||
--c-orange-300: 253 186 116;
|
||
--c-orange-400: 251 146 60;
|
||
--c-orange-500: 249 115 22;
|
||
--c-orange-600: 234 88 12;
|
||
--c-orange-700: 194 65 12;
|
||
--c-orange-800: 154 52 18;
|
||
--c-orange-900: 124 45 18;
|
||
/* yellow */
|
||
--c-yellow-50: 254 252 232;
|
||
--c-yellow-100: 254 249 195;
|
||
--c-yellow-200: 254 240 138;
|
||
--c-yellow-300: 253 224 71;
|
||
--c-yellow-400: 250 204 21;
|
||
--c-yellow-500: 234 179 8;
|
||
--c-yellow-600: 202 138 4;
|
||
--c-yellow-700: 161 98 7;
|
||
--c-yellow-800: 133 77 14;
|
||
--c-yellow-900: 113 63 18;
|
||
/*
|
||
* ─── 实心按钮/徽标的底色 ───
|
||
*
|
||
* 与上面 --c-* 的 400–900 **同值,但两种模式下都不变**。
|
||
*
|
||
* 为什么要单独一组:那六档在深色模式下被提亮成了浅色(red-600 →
|
||
* rgb(246,141,141)),因为 `text-red-600` 得在深底上读得动。而
|
||
* `bg-red-600 text-white` 的白字落在那个浅红上只有 1.6:1。
|
||
* 一个名字服务两种语义就必然坏掉一头 —— 与 text-white/bg-white 那次同理。
|
||
*
|
||
* 只有 backgroundColor 走这组(见 tailwind.config.js)。
|
||
*/
|
||
--s-blue-400: 96 165 250;
|
||
--s-blue-500: 59 130 246;
|
||
--s-blue-600: 37 99 235;
|
||
--s-blue-700: 29 78 216;
|
||
--s-blue-800: 30 64 175;
|
||
--s-blue-900: 30 58 138;
|
||
--s-red-400: 248 113 113;
|
||
--s-red-500: 239 68 68;
|
||
--s-red-600: 220 38 38;
|
||
--s-red-700: 185 28 28;
|
||
--s-red-800: 153 27 27;
|
||
--s-red-900: 127 29 29;
|
||
--s-green-400: 74 222 128;
|
||
--s-green-500: 34 197 94;
|
||
--s-green-600: 22 163 74;
|
||
--s-green-700: 21 128 61;
|
||
--s-green-800: 22 101 52;
|
||
--s-green-900: 20 83 45;
|
||
--s-amber-400: 251 191 36;
|
||
--s-amber-500: 245 158 11;
|
||
--s-amber-600: 217 119 6;
|
||
--s-amber-700: 180 83 9;
|
||
--s-amber-800: 146 64 14;
|
||
--s-amber-900: 120 53 15;
|
||
--s-orange-400: 251 146 60;
|
||
--s-orange-500: 249 115 22;
|
||
--s-orange-600: 234 88 12;
|
||
--s-orange-700: 194 65 12;
|
||
--s-orange-800: 154 52 18;
|
||
--s-orange-900: 124 45 18;
|
||
--s-yellow-400: 250 204 21;
|
||
--s-yellow-500: 234 179 8;
|
||
--s-yellow-600: 202 138 4;
|
||
--s-yellow-700: 161 98 7;
|
||
--s-yellow-800: 133 77 14;
|
||
--s-yellow-900: 113 63 18;
|
||
|
||
/*
|
||
* ─── 背景层 ───
|
||
*
|
||
* 自定义背景是**装饰层**,它的取值空间与主题色板无关,所以不复用 --c-*:
|
||
* --c-* 是要被主题反转的语义色,而这里只是「叠在图片/渐变上的一层遮罩」。
|
||
*
|
||
* --bg-scrim 是遮罩颜色:浅色下用白(把花哨的图案洗淡),
|
||
* 深色下用黑(压暗)。两者都在 .dark 里换值。
|
||
* --bg-dim 与 --bg-blur 由 backgroundStore 在运行时写入,这里是初值。
|
||
*/
|
||
--bg-scrim: 255 255 255;
|
||
--bg-dim: 12%;
|
||
/*
|
||
* ★★ 2026-09-17:深色下的**压暗下限**(浅色下是 0 = 不干预)。
|
||
*
|
||
* 为什么需要它:遮罩是"黑 56%"这种**比例**,而比例压不住一张**浅**壁纸。
|
||
* 用户 jianf 的壁纸均值 RGB 213(浅照片)、dim=56 ⇒ 遮罩后底 ≈ 213×0.44 = 94,
|
||
* 仍是中灰;再叠各层玻璃白之后 108–134。而深色正文是近白(243),次要文字
|
||
* gray-400 是 rgb(138,146,161) ⇒ 实测**最差 1.19:1**(日历页":root"那些日期)。
|
||
* 这不是"文字颜色没跟上"(`--c-gray-*` 一直是反的),而是**底根本没暗下来**。
|
||
*
|
||
* 契约(background.test:"玻璃是白色材料"):深色下玻璃之所以能维持近白基材,
|
||
* 前提就是"背后是深底"。所以深色必须保证这个前提 —— 壁纸是浅的时候,
|
||
* 用户的 56% 不够,得抬到下限。
|
||
*
|
||
* 代价(明写在案):深色下浅壁纸会被压得很淡(92% ⇒ 只剩 8% 亮度)。
|
||
* 这是**可读性优先**的取舍:用户报的正是"深色模式可读性极差",
|
||
* 而且壁纸仍在(8% + 玻璃质感 + 模糊),只是不再是主体。
|
||
*/
|
||
--bg-dim-min: 0%;
|
||
--bg-blur: 4px;
|
||
/* 玻璃面板的不透明度:背景越花,面板需要越实才读得动。 */
|
||
--bg-glass: 0.88;
|
||
/*
|
||
* 第三层嵌套:浅色下**仍然是透明**(你当初要的效果 —— 不要再叠白、别把壁纸吃掉)。
|
||
* 深色下它必须有个小值,见 .dark 里的同名令牌与那一段的注释。
|
||
*/
|
||
--bg-glass-nested3: 0;
|
||
/*
|
||
* 嵌套面板那一层的不透明度。
|
||
*
|
||
* ★ 为什么必须有这个变量(2026-09-13 用户报的缺陷):「壁纸底上叠了太多
|
||
* 不透明层,导致壁纸效果很差,几乎看不出来」。根因就是**每一层 bg-white
|
||
* 都用同一个 0.82**,而嵌套是相乘的:0.82 × 0.82 = 0.97,再叠一层就到 0.995
|
||
* —— 壁纸被彻底盖住,而且每层各做一次模糊,图案被糊成一片灰。
|
||
*
|
||
* 原则是「玻璃只该出现一次」:最外层负责遮罩与模糊,里面几层只留一点点
|
||
* 色调(保住正文对比度),第三层起直接透明(见下面 data-bg='on' 的规则)。
|
||
*/
|
||
--bg-glass-inner: 0.82;
|
||
/*
|
||
* 控件级透度:复选框、选项胶囊、地址建议菜单这类**小控件**。
|
||
*
|
||
* 用户第三轮的原话:「复选框和地址猜测项的透明度问题还没改,他们才是真正需要
|
||
* 拉低透明度的地方」。也就是说:承载正文的大面必须够实(读得清),
|
||
* 而这些小块本身没有大段文字,透一点反而好看 —— 壁纸透过它们,界面才有层次。
|
||
* 三档从实到透:正文面(--bg-glass) > 嵌套卡(--bg-glass-inner) > 控件(--bg-glass-control)。
|
||
*/
|
||
--bg-glass-control: 0.5;
|
||
--glass-card-a: 0.92;
|
||
--glass-card-hover-a: 0.99;
|
||
--glass-card-wall-a: 0.78;
|
||
--glass-card-wall-hover-a: 0.88;
|
||
/* 卡片边框:浅色用深色细线,深色用亮线(见 .dark,深色下层次靠边框不靠阴影)。 */
|
||
--hairline-fg: 15 23 42 / 0.07;
|
||
--hairline-fg-strong: 15 23 42 / 0.12;
|
||
/* 面板的朦胧感(与壁纸自身的 --bg-blur 分开:那层给照片打底,这层给面板) */
|
||
--bg-blur-panel: 10px;
|
||
/* 浮动面板之间的缝隙:壁纸从缝隙里露出来,是圆角化玻璃化的关键 */
|
||
--pane-gap: 10px;
|
||
|
||
/*
|
||
* ─── 尺寸与动效 ───
|
||
*
|
||
* 提取成变量的理由与颜色相同:一处定义、全站一致。写死数值时「圆角"
|
||
* 会随组件作者的随手一写而漂移(本仓库曾同时存在 4 种圆角),
|
||
* 而动效时长不一致会让界面显得格崩。
|
||
*/
|
||
--radius-card: 0.875rem;
|
||
--radius-control: 0.5rem;
|
||
--ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
|
||
--dur-fast: 120ms;
|
||
--dur-base: 180ms;
|
||
|
||
/*
|
||
* 抬高阴影。
|
||
*
|
||
* 浅色下靠阴影表达层次;深色下黑色阴影几乎看不见,层次改由更亮的边框
|
||
* 与表层面亮度差承担 —— 因此深色主题里换一整组值(见下),
|
||
* 而不是继续加深同一个阴影。
|
||
*
|
||
* 注:本注释刻意不写出深色选择器的字面量 —— theme.test.mjs 用
|
||
* indexOf(选择器) 切分两个颜色块,在 :root 的注释里出现那个字符串
|
||
* 会把它提前截断(已踩过一次)。该测试现已先剥注释,但这里仍避开。
|
||
*/
|
||
--shadow-1: 0 1px 2px rgb(15 23 42 / 0.06), 0 1px 3px rgb(15 23 42 / 0.1);
|
||
--shadow-2: 0 2px 4px rgb(15 23 42 / 0.05), 0 4px 12px rgb(15 23 42 / 0.1);
|
||
--shadow-3: 0 8px 24px rgb(15 23 42 / 0.12), 0 2px 6px rgb(15 23 42 / 0.08);
|
||
/*
|
||
* 侧边面板专用:横向偏移 + 大扩散,模拟「两块面板叠在一起」而不是「被线分开」。
|
||
* 竖向几乎不偏移,否则整列会看起来浮在半空(面板是全高的)。
|
||
*/
|
||
--shadow-panel: 1px 0 2px rgb(15 23 42 / 0.04), 4px 0 16px -4px rgb(15 23 42 / 0.08);
|
||
--hairline: 0 0 0;
|
||
color-scheme: light;
|
||
}
|
||
|
||
/*
|
||
* 深色模式。
|
||
*
|
||
* # 灰阶整体反转
|
||
*
|
||
* - `white`(卡片底)→ 近黑的深灰。**不用纯黑**:纯黑上的浅色文字
|
||
* 对比过强,长时间看更累,也看不出层次。
|
||
* - `gray-50`(页面底)→ 比卡片**更暗**。浅色下页面底比卡片浅,
|
||
* 深色下必须反过来,否则卡片会陷进背景失去边界。
|
||
* - `gray-200/300`(分隔线)→ 中低亮度灰。照搬浅色值会得到刺眼的白线。
|
||
* - `gray-400/500`(次要文字)→ **提亮**。深底上的浅色 gray-400 只有
|
||
* 约 2:1 对比度,远低于 WCAG AA 的 4.5:1 —— 看得见但读不动。
|
||
*
|
||
* # 强调色不反转
|
||
*
|
||
* blue/red/green/... 在 tailwind.config.js 里是**固定值**,不走变量。
|
||
* 按钮底色在深色模式下依然是 blue-600 那样的彩色,跟着变会让主按钮
|
||
* 在深色页面上失去「这是主操作」的视觉重量。
|
||
*
|
||
* # 框架色阶只微调
|
||
*
|
||
* 见下面 --c-chrome-* 的注释。
|
||
*/
|
||
.dark {
|
||
--c-white: 24 27 33;
|
||
|
||
/*
|
||
* 近白而非纯白:深色页面上纯白字偏刺眼。
|
||
*
|
||
* 但它同时是**实心彩底按钮上的文字**,而那儿的对比度是硬指标:实测 244
|
||
* 时「红底白字」只有 4.46:1,低于 WCAG AA 的 4.5(真实渲染量出,见
|
||
* test/manual/background-verify.mjs)。抬到 250 后为 4.65:1,
|
||
* 仍不是纯白,观感上仍保留了「不刺眼」的初衷。
|
||
*/
|
||
--c-on-accent: 250 250 252;
|
||
|
||
--c-gray-50: 17 19 24;
|
||
--c-gray-100: 32 36 44;
|
||
--c-gray-200: 39 43 52;
|
||
--c-gray-300: 61 68 81;
|
||
--c-gray-400: 138 146 161;
|
||
--c-gray-500: 165 173 186;
|
||
--c-gray-600: 190 197 208;
|
||
--c-gray-700: 212 217 225;
|
||
--c-gray-800: 231 235 240;
|
||
--c-gray-900: 243 245 248;
|
||
--c-gray-950: 250 251 253;
|
||
|
||
/*
|
||
* 框架色阶**不反转,只微调**:比内容区(--c-gray-50 = 17 19 24)
|
||
* 再深一档,保持「框架比内容更沉」这个浅色下就有的关系。
|
||
* 文字档位相应提亮 —— 底色变深后原来的 chrome-400 只剩约 2.9:1。
|
||
*/
|
||
--c-chrome-100: 236 240 246;
|
||
--c-chrome-200: 214 221 232;
|
||
--c-chrome-400: 148 158 175;
|
||
--c-chrome-600: 58 65 78;
|
||
--c-chrome-700: 44 50 61;
|
||
--c-chrome-800: 30 35 44;
|
||
--c-chrome-900: 12 14 18;
|
||
|
||
/*
|
||
* 背景遮罩换成黑:浅色下用白把图案洗淡,深色下必须压暗,
|
||
* 否则一张浅色照片会在深色界面里舗成一块亮斑,正文完全读不动。
|
||
*/
|
||
--bg-scrim: 0 0 0;
|
||
/*
|
||
* ★★ 2026-09-17:这三个值原本是 0.9 / 0.84 / 0.55 —— 壁纸模式下的**白底白字**。
|
||
*
|
||
* 机制:每个玻璃面都是 `rgb(var(--glass-base) / α)`,而 `--glass-base` **恒为白**
|
||
* (用户定的契约:「玻璃是白色材料」,见 background.test 那组判据)。
|
||
* 所以 α 就是「这层白有多实」,而它必须**跟主题反着走**:
|
||
* · 浅色主题:页面底是白的、文字是深的 ⇒ 白面板实一点无所谓(底本来白);
|
||
* · 深色主题:页面底是深的、文字是近白的 ⇒ 白面板**必须很透**,
|
||
* 否则就是「白色面板 + 近白文字」。
|
||
*
|
||
* 实测事故(用户 jianf 账号:`bg_kind=image`、dim=56、blur=3,
|
||
* 壁纸均值 RGB 213,212,225 —— 一张**浅**照片):只把 alpha 调到 0.08/0.05/0.04
|
||
* 之后仍然**不够** —— 内层透明那一档让浅壁纸直接透上来,
|
||
* 1280×800 真实渲染采样(逐个叶子文本节点采样其实际底色)最差 **1.19:1**
|
||
* (日历页的日期/农历字:「廿五」fg=rgb(138,146,161) bg=rgb(134,132,132))。
|
||
* 用户原话:「webui 只有通信页面的深色模式正常了,剩下的三个页面深色模式可读性都极差」。
|
||
*
|
||
* ★ 真正的根因不是 alpha,而是**底没暗下来**:dim 是比例(暗 56%),
|
||
* 而 56% 压不住一张 213 的浅壁纸(213×0.44 ≈ 94,仍是中灰)。
|
||
* 近白正文对 94 只有约 3.6:1,次要文字 gray-400 对 94/134 只有 1.2–1.4:1
|
||
* —— **没有任何文字颜色能救**。所以深色下加了 `--bg-dim-min`(见 :root)。
|
||
* 加了下限之后复测:四页最差 通信 6.14 / 日历 4.90 / 联系 4.96 / 我的 5.38,全部 ≥ 4.5:1。
|
||
*
|
||
* 为什么旧判据没拦住:它们只判 `--c-*` 色板(那套确实是反的),
|
||
* 而这三个是 **alpha 档**,不在色板里 —— 与 `.glass-card` 写死白色是同一类盲区。
|
||
* 现在 alpha 档也由判据**按实际合成**验算(theme.test 的「合成对比度」一条)。
|
||
*
|
||
* 低到什么程度:让深底主导,但保留一点点白当「玻璃的质感」
|
||
* (全透明就失去玻璃层了,而且会让壁纸完全穿透面板)。
|
||
*/
|
||
--bg-glass: 0.04;
|
||
--bg-glass-inner: 0.03;
|
||
--bg-glass-control: 0.04;
|
||
/* 第三层嵌套:深色下不能是透明(浅壁纸会透上来),给一点白压住即可。 */
|
||
--bg-glass-nested3: 0.02;
|
||
/*
|
||
* ★★ 2026-09-17:深色下压暗下限 92%(浅色下是 0)。见 `:root` 里同一令牌的注释。
|
||
* 实测:56% 时三页最差 1.19:1,92% 后四页全部 ≥ 4.5:1(1280×800 真渲染采样)。
|
||
*/
|
||
--bg-dim-min: 92%;
|
||
|
||
/*
|
||
* 深色下的层次靠「边框亮于底」而不是阴影。
|
||
* 保留一行极淡的黑色阴影只为了与浅色统一接管机制,真正的边界感来自 --hairline。
|
||
*/
|
||
--shadow-1: 0 1px 2px rgb(0 0 0 / 0.4);
|
||
--shadow-2: 0 2px 6px rgb(0 0 0 / 0.45);
|
||
--shadow-3: 0 10px 30px rgb(0 0 0 / 0.55);
|
||
/*
|
||
* 深色下层次比浅色更难感知,所以面板阴影要更实一些;
|
||
* 但仍以「方向性」为主(横偏移),避免整列看起来悬空。
|
||
*/
|
||
--shadow-panel: 1px 0 2px rgb(0 0 0 / 0.35), 4px 0 16px -4px rgb(0 0 0 / 0.5);
|
||
--hairline: 0 0 0 1px rgb(255 255 255 / 0.06);
|
||
|
||
/*
|
||
* ─── 强调色(深色)───
|
||
*
|
||
* 两段走向相反,理由都是实测出来的:
|
||
*
|
||
* **表面段 50–300 变暗**(朝色相方向偏移卡片色)。照搬浅色值的话
|
||
* red-50 (#fef2f2) 在深色页面上是一块近白亮斑 —— 那是「错误提示条」的底,
|
||
* 结果比正文还抢眼,而它上面的红字反而读不动。
|
||
*
|
||
* **前景段 400–900 变亮**。照搬浅色值时 red-700 (#b91c1c) 落在深色卡片上
|
||
* 只有 2.67:1、amber-900 只有 1.90:1 —— 远低于 WCAG AA 的 4.5:1。
|
||
* 这里每一档都拉到 ≥4.5(实测最低 red-400 = 5.93),
|
||
* 由 test/theme.test.mjs 逐档断言。
|
||
*
|
||
* 实心按钮底不在这里 —— 那组是 --s-*,定义在 :root 且两种模式同值。
|
||
*/
|
||
/* blue */
|
||
--c-blue-50: 28 37 54;
|
||
--c-blue-100: 30 43 67;
|
||
--c-blue-200: 32 52 84;
|
||
--c-blue-300: 36 62 105;
|
||
--c-blue-400: 92 152 247;
|
||
--c-blue-500: 108 162 248;
|
||
--c-blue-600: 128 175 249;
|
||
--c-blue-700: 154 191 250;
|
||
--c-blue-800: 183 210 251;
|
||
--c-blue-900: 213 228 253;
|
||
/* red */
|
||
--c-red-50: 46 31 37;
|
||
--c-red-100: 58 34 39;
|
||
--c-red-200: 76 37 41;
|
||
--c-red-300: 97 41 45;
|
||
--c-red-400: 243 109 109;
|
||
--c-red-500: 244 124 124;
|
||
--c-red-600: 246 141 141;
|
||
--c-red-700: 248 164 164;
|
||
--c-red-800: 250 191 191;
|
||
--c-red-900: 252 217 217;
|
||
/* green */
|
||
--c-green-50: 25 44 39;
|
||
--c-green-100: 26 54 43;
|
||
--c-green-200: 26 68 48;
|
||
--c-green-300: 27 85 54;
|
||
--c-green-400: 34 197 94;
|
||
--c-green-500: 56 203 110;
|
||
--c-green-600: 83 210 129;
|
||
--c-green-700: 118 219 155;
|
||
--c-green-800: 158 229 184;
|
||
--c-green-900: 198 240 213;
|
||
/* amber */
|
||
--c-amber-50: 46 40 31;
|
||
--c-amber-100: 59 48 29;
|
||
--c-amber-200: 77 58 28;
|
||
--c-amber-300: 99 72 26;
|
||
--c-amber-400: 245 158 11;
|
||
--c-amber-500: 246 168 35;
|
||
--c-amber-600: 247 179 65;
|
||
--c-amber-700: 249 195 104;
|
||
--c-amber-800: 251 212 148;
|
||
--c-amber-900: 252 230 192;
|
||
/* orange */
|
||
--c-orange-50: 47 36 32;
|
||
--c-orange-100: 60 41 31;
|
||
--c-orange-200: 78 48 30;
|
||
--c-orange-300: 101 57 29;
|
||
--c-orange-400: 249 115 22;
|
||
--c-orange-500: 250 129 45;
|
||
--c-orange-600: 250 146 73;
|
||
--c-orange-700: 251 168 111;
|
||
--c-orange-800: 252 193 152;
|
||
--c-orange-900: 253 219 194;
|
||
/* yellow */
|
||
--c-yellow-50: 45 42 31;
|
||
--c-yellow-100: 58 51 29;
|
||
--c-yellow-200: 74 63 27;
|
||
--c-yellow-300: 95 79 25;
|
||
--c-yellow-400: 234 179 8;
|
||
--c-yellow-500: 236 187 33;
|
||
--c-yellow-600: 239 196 62;
|
||
--c-yellow-700: 242 208 102;
|
||
--c-yellow-800: 246 222 146;
|
||
--c-yellow-900: 250 235 191;
|
||
/* 让浏览器把滚动条、表单控件、autofill 一并切深色 */
|
||
color-scheme: dark;
|
||
}
|
||
|
||
@layer base {
|
||
:root {
|
||
/* main.tsx 会用 Visual Viewport 覆盖;这里保证脚本执行前也有正确高度。 */
|
||
--app-height: 100vh;
|
||
}
|
||
@supports (height: 100dvh) {
|
||
:root {
|
||
--app-height: 100dvh;
|
||
}
|
||
}
|
||
html, body, #root {
|
||
height: var(--app-height);
|
||
min-height: 0;
|
||
}
|
||
body {
|
||
overflow: hidden;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||
/*
|
||
* 显式给 body 底色。移动端橡皮筋回弹时露出的是 body 背景 ——
|
||
* 不设的话深色模式下滑到边界会闪出一条白边。
|
||
*/
|
||
background-color: rgb(var(--c-gray-50));
|
||
color: rgb(var(--c-gray-900));
|
||
}
|
||
|
||
/* 统一作者样式,避免 Chromium/Electron autofill 在深色卡片上画白底。 */
|
||
input:not([type='checkbox']):not([type='radio']):not([type='file']),
|
||
textarea,
|
||
select {
|
||
background-color: rgb(var(--c-white));
|
||
color: rgb(var(--c-gray-900));
|
||
}
|
||
input::placeholder,
|
||
textarea::placeholder {
|
||
color: rgb(var(--c-gray-400));
|
||
}
|
||
input:-webkit-autofill,
|
||
input:-webkit-autofill:hover,
|
||
input:-webkit-autofill:focus {
|
||
-webkit-text-fill-color: rgb(var(--c-gray-900));
|
||
box-shadow: 0 0 0 1000px rgb(var(--c-white)) inset;
|
||
transition: background-color 9999s ease-out;
|
||
}
|
||
}
|
||
|
||
@layer components {
|
||
/* react-markdown 的本地排版层;不依赖固定色板的 typography 插件。 */
|
||
.markdown {
|
||
color: rgb(var(--c-gray-800));
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.markdown > * + * { margin-top: 0.75rem; }
|
||
.markdown h1 { font-size: 1.5rem; line-height: 2rem; font-weight: 700; }
|
||
.markdown h2 { font-size: 1.25rem; line-height: 1.75rem; font-weight: 700; }
|
||
.markdown h3 { font-size: 1.05rem; line-height: 1.5rem; font-weight: 600; }
|
||
.markdown ul { list-style: disc; padding-left: 1.35rem; }
|
||
.markdown ol { list-style: decimal; padding-left: 1.35rem; }
|
||
.markdown li + li { margin-top: 0.25rem; }
|
||
.markdown a { color: rgb(var(--c-blue-600)); text-decoration: underline; }
|
||
.markdown blockquote {
|
||
border-left: 3px solid rgb(var(--c-gray-300));
|
||
padding-left: 0.75rem;
|
||
color: rgb(var(--c-gray-600));
|
||
}
|
||
.markdown code {
|
||
border-radius: 0.25rem;
|
||
background: rgb(var(--c-gray-100));
|
||
padding: 0.1rem 0.3rem;
|
||
font-size: 0.875em;
|
||
}
|
||
.markdown pre {
|
||
overflow-x: auto;
|
||
border: 1px solid rgb(var(--c-gray-200));
|
||
border-radius: 0.5rem;
|
||
background: rgb(var(--c-gray-100));
|
||
padding: 0.75rem;
|
||
}
|
||
.markdown pre code { background: transparent; padding: 0; }
|
||
.markdown table { width: 100%; border-collapse: collapse; display: block; overflow-x: auto; }
|
||
.markdown th, .markdown td { border: 1px solid rgb(var(--c-gray-200)); padding: 0.4rem 0.55rem; }
|
||
.markdown th { background: rgb(var(--c-gray-100)); text-align: left; }
|
||
}
|
||
|
||
@layer utilities {
|
||
/*
|
||
* .tap —— 保证 44x44 的触摸命中区,但不改变视觉尺寸。
|
||
*
|
||
* 移动端 44x44 是通行下限(Apple HIG 与 Material 都取这个数),而这些图标/
|
||
* 小字按钮视觉上只有 15-24px 高 —— 实测「标记已读」48x16、「转发」42x16、
|
||
* 「抄送」20x15。直接加 padding 会把本来就挤的头部撑散,在 320px 屏上还会换行。
|
||
*
|
||
* 改用居中的透明伪元素扩大命中区:视觉一像素不动,手指够得到。
|
||
*
|
||
* 只在窄屏生效:桌面用鼠标,精度足够;而扩大后的命中区在密排的工具栏里
|
||
* 会互相重叠,点一个可能命中隔壁那个。
|
||
*/
|
||
/* 与 useIsNarrow / Tailwind lg 统一:竖屏平板也需要触摸命中区。 */
|
||
@media (max-width: 1023px) {
|
||
.safe-frame {
|
||
padding-top: env(safe-area-inset-top);
|
||
padding-left: env(safe-area-inset-left);
|
||
padding-right: env(safe-area-inset-right);
|
||
}
|
||
.narrow-nav {
|
||
padding-left: env(safe-area-inset-left);
|
||
padding-right: env(safe-area-inset-right);
|
||
}
|
||
/*
|
||
* ★ 2026-09-15 用户:「输入框弹出后,底部导航栏会消失,再次点击界面导航才会出现」。
|
||
*
|
||
* 原先这里是 `.form-editing .narrow-nav { display: none }` —— `form-editing` 由
|
||
* main.tsx 在**任何输入框获得焦点**时挂上,而回复框一展开就会自动聚焦它的 textarea
|
||
* ⇒ 导航立刻消失;只有点到别处(失焦)才回来。
|
||
*
|
||
* 但它已经没必要了:viewport 里声明了 `interactive-widget=resizes-content`,
|
||
* 键盘弹出时浏览器会重排布局(导航自然落在键盘上方),不需要靠隐藏来腾地方。
|
||
* 而"弹个回复框导航就没了、还要再点一下才回来"是纯损失。
|
||
*/
|
||
/* 编辑态不再隐藏底部导航:见上 */
|
||
.tap {
|
||
position: relative;
|
||
}
|
||
.tap::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 100%;
|
||
height: 100%;
|
||
min-width: 44px;
|
||
min-height: 44px;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* .reveal —— 悬停才显形的次要动作(写信 / 归档)。
|
||
*
|
||
* 原先直接写 `opacity-0 group-hover:opacity-100`。**触摸设备没有 hover**,
|
||
* 于是这些按钮永远是透明的,却仍然接收点击 —— 实测在联系人列表上
|
||
* elementFromPoint 命中的就是那个看不见的「归档」。一个看不见却按得动的
|
||
* 破坏性按钮比没有按钮更糟:人以为自己点的是卡片,实际归档了一条会话。
|
||
*
|
||
* 因此默认可见,只在**真的支持悬停**的设备上才隐藏。判据用
|
||
* `(hover: hover) and (pointer: fine)`:单看 hover 会把带触摸板的平板算进去。
|
||
*/
|
||
.reveal {
|
||
opacity: 1;
|
||
}
|
||
@media (hover: hover) and (pointer: fine) {
|
||
.reveal {
|
||
opacity: 0;
|
||
transition: opacity 150ms;
|
||
}
|
||
.group:hover .reveal,
|
||
.reveal:focus-within {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* ═══════════════════════════════════════════════════════════════════
|
||
* 自定义背景 + 现代化打磨
|
||
* ═══════════════════════════════════════════════════════════════════
|
||
*
|
||
* # 为什么这一段放在所有 @layer 之外
|
||
*
|
||
* Tailwind 的层序是 base < components < utilities,而这里要覆盖的正是
|
||
* **utilities 生成的** `.bg-white` / `.bg-gray-50`。写进 @layer components
|
||
* 会被 utilities 压过去(静默失效,只有肉眼能看出「背景没生效」);
|
||
* 写进 @layer utilities 则取决于 Tailwind 内部的合并顺序,不可靠。
|
||
* 未分层的 CSS 在层叠中恒胜过分层 CSS —— 这是唯一稳定可靠的位置。
|
||
*/
|
||
|
||
/* ─── 背景层本体 ─── */
|
||
|
||
.app-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
/*
|
||
* 必须是**负** z-index。
|
||
*
|
||
* 层叠顺序里 z-index:0/auto 的定位元素画在常规流内容**之上**,用它会把
|
||
* 整个界面盖住(背板是 fixed 全屏的,连点击区都会挡住 —— 虽然有
|
||
* pointer-events:none 兵底,但视觉上完全遮住)。负值才画在常规流内容
|
||
* 之下、同时在父元素背景之上,正是「缓景」该在的位置。
|
||
*
|
||
* 它能露出来还依赖另一条:背景开启时把所有不透明的页面底
|
||
* (bg-gray-50 / bg-slate-100 / body)改成透明,见下面。
|
||
*/
|
||
z-index: -1;
|
||
pointer-events: none;
|
||
/* 默认不可见;背景开启时才铺开(data-bg 由 backgroundStore 写在 <html> 上)。 */
|
||
background-image: var(--bg-image, none);
|
||
background-size: cover;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
filter: blur(var(--bg-blur));
|
||
/* 模糊会把边缘拖进画面,放大 2% 抵消。 */
|
||
transform: scale(1.02);
|
||
opacity: 0;
|
||
transition: opacity var(--dur-base) var(--ease-out-soft);
|
||
will-change: opacity;
|
||
}
|
||
|
||
html[data-bg='on'] .app-backdrop {
|
||
opacity: 1;
|
||
}
|
||
|
||
/*
|
||
* 遮罩:把图案压下去,让正文读得动。
|
||
*
|
||
* 用单独一层而不是直接调低 background 的透明度:透明度会让背景变淡但仍与
|
||
* 正文争夺对比度,而遮罩是**在两者之间插一层**,颜色随主题反转(浅色用白、
|
||
* 深色用黑),因此两种模式下都是「背景退后、内容在前」。
|
||
*/
|
||
.app-backdrop::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
/*
|
||
* `max()` 而不是直接 `var(--bg-dim)`:深色主题下用户把压暗调到 56% 时,
|
||
* 浅壁纸仍会透成中灰(实测最差 1.19:1)。下限制在深色里给 92%,
|
||
* 浅色里给 0(不干预)。取 max 而非覆盖,是为了**不下调用户的选择**:
|
||
* 用户调得比下限更高时以用户的为准。
|
||
*/
|
||
background-color: rgb(var(--bg-scrim) / max(var(--bg-dim), var(--bg-dim-min)));
|
||
}
|
||
|
||
/*
|
||
* ─── 预设渐变 ───
|
||
*
|
||
* 全部用现有调色板的**表面段**(50–300)拼出来,因此:
|
||
* - 无需新增任何写死的十六进制值
|
||
* - 自动随主题变化 —— 那一段在深色下本来就是暗的(见 index.css 的 .dark
|
||
* 与 theme.test.mjs 第 19 条),浅色得到柔和pastel、深色得到低沉暗调
|
||
*/
|
||
.bg-preset-aurora {
|
||
--bg-image: radial-gradient(at 18% 22%, rgb(var(--c-blue-200)) 0%, transparent 55%),
|
||
radial-gradient(at 82% 12%, rgb(var(--c-green-100)) 0%, transparent 50%),
|
||
radial-gradient(at 68% 82%, rgb(var(--c-blue-100)) 0%, transparent 55%);
|
||
}
|
||
.bg-preset-dusk {
|
||
--bg-image: radial-gradient(at 12% 80%, rgb(var(--c-orange-100)) 0%, transparent 55%),
|
||
radial-gradient(at 85% 25%, rgb(var(--c-blue-200)) 0%, transparent 55%);
|
||
}
|
||
.bg-preset-mint {
|
||
--bg-image: radial-gradient(at 25% 30%, rgb(var(--c-green-100)) 0%, transparent 55%),
|
||
radial-gradient(at 78% 70%, rgb(var(--c-blue-100)) 0%, transparent 55%);
|
||
}
|
||
.bg-preset-sand {
|
||
--bg-image: radial-gradient(at 20% 25%, rgb(var(--c-amber-100)) 0%, transparent 60%),
|
||
radial-gradient(at 80% 75%, rgb(var(--c-orange-100)) 0%, transparent 55%);
|
||
}
|
||
.bg-preset-ink {
|
||
--bg-image: radial-gradient(at 30% 20%, rgb(var(--c-gray-200)) 0%, transparent 60%),
|
||
linear-gradient(160deg, rgb(var(--c-gray-100)), rgb(var(--c-gray-200)));
|
||
}
|
||
.bg-preset-mesh {
|
||
--bg-image: repeating-linear-gradient(
|
||
0deg,
|
||
rgb(var(--c-gray-200) / 0.55) 0 1px,
|
||
transparent 1px 28px
|
||
),
|
||
repeating-linear-gradient(90deg, rgb(var(--c-gray-200) / 0.55) 0 1px, transparent 1px 28px);
|
||
}
|
||
|
||
/*
|
||
* ─── 背景开启时的表面处理 ───
|
||
*
|
||
* 不改 27 个组件的 class:背景是全局装饰,逐个组件加 class 必然漏(漏掉的那块
|
||
* 就是一张不透明卡片浮在背景上)。这里按 Tailwind 生成的实际类名统一接管。
|
||
*
|
||
* 被接管的是三类:
|
||
* - 页面底(bg-gray-50 / bg-slate-100)→ 完全透明,让出背景
|
||
* - 卡片/面板(bg-white) → 半透明 + 背景模糊(玻璃)
|
||
* - 应用框架(bg-chrome-*) → 半透明,保持「框架比内容沉」
|
||
*/
|
||
html[data-bg='on'] body,
|
||
html[data-bg='on'] .bg-gray-50,
|
||
html[data-bg='on'] .bg-slate-100 {
|
||
background-color: transparent;
|
||
/* 空白区不模糊:壁纸在这里要清晰可辨(用户:"真正该透明的地方加了很浓的模糊") */
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
/*
|
||
* .glass-control —— 小控件用的"更透"档。
|
||
*
|
||
* 必须显式加在元素上、并且**不要同时写 bg-white**:我的 `html[data-bg='on'] .bg-white`
|
||
* 接管规则会把任何 bg-white 拉回正文面那一档(0.88),写在这儿的透度会被它盖掉。
|
||
*
|
||
* ★ 只给**贴着面板、背后是自己人**的小控件用(按钮、输入框底)。
|
||
* 浮在别的内容**之上**的弹层不能用它 —— 见 .popup-surface。
|
||
*/
|
||
html[data-bg='on'] .glass-control {
|
||
background-color: rgb(var(--glass-base) / var(--bg-glass-control));
|
||
backdrop-filter: blur(8px) saturate(1.1);
|
||
-webkit-backdrop-filter: blur(8px) saturate(1.1);
|
||
}
|
||
|
||
/*
|
||
* .popup-surface —— 浮在**别的内容之上**的弹层表面(候选菜单、下拉)。
|
||
*
|
||
* ★ 2026-09-15 用户(看着抄送/转发两处的候选列表):
|
||
* 「你在选择框的模糊逻辑上用力过猛,导致只能显示一条信息」
|
||
*
|
||
* 实测(壁纸开、生产构建 4c2bf26,在浏览器里读计算样式):
|
||
* 菜单 `background-color: rgba(255,255,255,0.5)` + `backdrop-filter: blur(8px)`,
|
||
* 而它压着的是**表单自己**(主题输入框、各段标签)⇒ 背后的控件被糊成一片灰、
|
||
* 从半透明的列表底下透上来。列表越往下,越分不清哪行是候选、哪行是背后的输入框。
|
||
*
|
||
* 根因与 2026-09-14 那次「模糊叠模糊」同族:把**控件档**(0.5 透 + 8px 模糊)
|
||
* 用在了**弹层**上。控件背后是面板自身,透一点好看;弹层背后是别的内容,
|
||
* 透就是把两份内容叠在一起。所以弹层给**不透明**表面,且不取背景模糊。
|
||
*/
|
||
.popup-surface {
|
||
background-color: rgb(var(--c-white));
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
/* 壁纸开着也**不透明**:弹层不是壁纸的一部分,没有人希望壁纸从候选列表里透出来 */
|
||
html[data-bg='on'] .popup-surface {
|
||
background-color: rgb(var(--c-white));
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
html[data-bg='on'] .bg-white {
|
||
background-color: rgb(var(--glass-base) / var(--bg-glass));
|
||
}
|
||
|
||
/*
|
||
* ★ 嵌在玻璃面板**里面**的面板不再各叠一次不透明度(2026-09-13 用户报的缺陷)。
|
||
*
|
||
* 相乘是这里的关键:两层 0.82 得 0.97、三层 0.995 —— 壁纸就是这么被吃掉的,
|
||
* 而且每层各做一次 16px 模糊,图案被糊成一片灰。
|
||
* 第二层只留一点色调(保住正文对比度),第三层起透明,且都**不再叠加模糊**。
|
||
*/
|
||
html[data-bg='on'] .bg-white .bg-white {
|
||
background-color: rgb(var(--glass-base) / var(--bg-glass-inner));
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
/*
|
||
* ★★ 2026-09-17:第三层原本是 `background-color: transparent` —— **深色下的白底白字**。
|
||
*
|
||
* 那条规则的初衷是对的(2026-09-13 用户报「壁纸底上叠了太多不透明层,导致壁纸效果
|
||
* 很差,几乎看不出来」):三层 0.82 相乘 = 0.995,壁纸被吃光了。
|
||
* 但它的**代价在深色主题下不同**:浅色下第三层透明没关系(底本来就是浅的、字是深的),
|
||
* 而深色下"透明"意味着**浅色壁纸直接透上来**,而上层文字是近白的 ⇒ 白底白字。
|
||
*
|
||
* 实测事故(用户 jianf 账号:壁纸 `image`、dim=56、blur=3,均值 RGB 213,212,225
|
||
* 是一张**浅**照片):日历 / 联系人 / 我的 三页最差 **1.06:1**,而通信页正常 ——
|
||
* 差别恰好是三页的面板都是「面板 > 卡片 > 内部」三层嵌套,最内层正文面被这条规则
|
||
* 设成了全透明。用户原话:「webui 只有通信页面的深色模式正常了,剩下的三个页面
|
||
* 深色模式可读性都极差」。
|
||
*
|
||
* 修法:保留"不再层层叠白"的本意,但**透明换成"很淡的白"**(深色下 0.04,
|
||
* 浅色下仍是你当初要的透明)—— 壁纸在深色下不至于被吃光(0.08→0.05→0.04 三层
|
||
* 合成约 0.16,仍看得见图案),而最内层正文面又能压住浅壁纸。
|
||
*/
|
||
html[data-bg='on'] .bg-white .bg-white .bg-white {
|
||
background-color: rgb(var(--glass-base) / var(--bg-glass-nested3));
|
||
}
|
||
|
||
/* 侧栏里的玻璃面板同理:框架已经有一层底色,里面不能再叠。 */
|
||
html[data-bg='on'] .bg-chrome-900 .bg-white,
|
||
html[data-bg='on'] .bg-chrome-800 .bg-white {
|
||
background-color: rgb(var(--glass-base) / var(--bg-glass-inner));
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
/* 悬停态也要接管:不接管的话鼠标一进面板就会闪成不透明(明显跳动)。 */
|
||
html[data-bg='on'] .hover\:bg-gray-50:hover,
|
||
html[data-bg='on'] .hover\:bg-gray-100:hover {
|
||
background-color: rgb(var(--c-gray-100) / var(--bg-glass));
|
||
}
|
||
|
||
/*
|
||
* ★ 浮动面板布局(2026-09-14 用户要求:导航栏完全不透明、整个界面圆角化玻璃化)。
|
||
*
|
||
* 三件事:
|
||
* ① 外壳留缝:面板之间露出壁纸 —— 没有缝隙的"玻璃"看上去仍是一整块板;
|
||
* ② 面板圆角:圆角 + 缝隙才是"浮动玻璃"的形状语言;
|
||
* ③ 导航栏**完全不透明**:它是框架,不该跟着壁纸一起虚化。
|
||
* 这与内容面板要通透是**两个方向**的要求,别合并成一条规则。
|
||
*/
|
||
/*
|
||
* ★ 两类面要分开处理(2026-09-14 用户第二轮批评):
|
||
*
|
||
* 「你把大量需要打底的场景(弹窗正文等)改为了透明。真正该透明的地方(空白区域)
|
||
* 加了很浓的模糊」
|
||
*
|
||
* 也就是我上一版把两类搞反了:
|
||
* - **承载文字的面**(正文卡、弹窗、列表、输入条):必须够实 + 轻模糊,
|
||
* 否则字压在壁纸上读不动 —— 这是"打底",不是"玻璃";
|
||
* - **空白/页面底**(详情区空态、面板缝隙、page 底):透明且**不模糊**,
|
||
* 壁纸在这里应该是清晰的照片,不是一团糊。
|
||
*
|
||
* 规则落点:`--bg-glass` 只作用在 `.bg-white` 这一族(承载内容的那些面),
|
||
* 而 `.bg-gray-50` / 页面底保持 transparent 且不参与模糊。
|
||
*/
|
||
/*
|
||
* 浮动面板的几何 **始终生效**(圆角 + 缝隙 + 投影 + 一点朦胧),不只在壁纸模式下。
|
||
*
|
||
* 用户(2026-09-14):「通信页面大面积缺失圆角与玻璃效果,所有有内容与无内容区域
|
||
* 都是硬截断」—— 根因是这些规则原先**全写在 `html[data-bg='on']` 里**:
|
||
* 没开壁纸的账号看到的是硬边、不透明、贴在一起的面板,硬截断就是这么来的。
|
||
*
|
||
* 所以几何部分下沉到这里(无条件的基线),壁纸相关的加强(朝向照片的透明度、
|
||
* 强模糊、面板间露出壁纸)仍留在下面的 data-bg 段里 —— 两者是叠加关系,不是替代。
|
||
*/
|
||
.app-shell {
|
||
padding: var(--pane-gap);
|
||
gap: var(--pane-gap);
|
||
}
|
||
|
||
.app-shell > * {
|
||
border-radius: var(--radius-card);
|
||
/*
|
||
* ★ 这里**不能**写 `overflow: hidden`(2026-09-14 用户:「通信页面完全无法上下滑动」)。
|
||
*
|
||
* 面板自己就是滚动容器(列表/详情都是 overflow-y-auto),而这条规则的特异性
|
||
* 比 Tailwind 的 `.overflow-y-auto` 高 ⇒ 滚动被静默干掉:实测当时**一个可滚动
|
||
* 容器都不存在**(scrollerCount=0),内容是直接被裁掉的,连"滚到底"都做不到。
|
||
*
|
||
* 圆角仍然生效(border-radius 不影响滚动);角落的方角残影改用
|
||
* background-clip 处理,代价是溢出内容在圆角处可能露一点点 —— 比不能滚动好得多。
|
||
*/
|
||
box-shadow: 0 8px 24px rgb(15 23 42 / 0.08);
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
/* 基线朦胧:没开壁纸时也有一点玻璃感,而不是一块硬塑料 */
|
||
/*
|
||
* ★ 面板**不再各自打模糊**(2026-09-14 用户:「你又犯了模糊叠模糊的毛病,
|
||
* 整体的模糊是由壁纸那一层模糊确定的,而你在每一个卡片又打了固定的模糊底」)。
|
||
*
|
||
* 浮在壁纸上的大面(列表栏/详情栏/卡片)背后只有**已经模糊过的壁纸**,
|
||
* 再 backdrop-filter 一次纯属叠加:不会更"玻璃",只会更脏更糊,而且每层都要
|
||
* 重新采样一次背景(滚动时明显掉帧)。
|
||
*
|
||
* 模糊只留给**真正悬浮在内容之上**的层:底部导航条、地址建议菜单、弹层 ——
|
||
* 它们背后是会滚动的内容,模糊在那里才有信息遮蔽的意义。
|
||
*/
|
||
.narrow-shell > *:not(.narrow-nav) {
|
||
}
|
||
|
||
/* 窄屏:内容面板同样浮起来(与底部导航那条悬浮玻璃一致) */
|
||
.narrow-shell {
|
||
padding: var(--pane-gap) var(--pane-gap) 0;
|
||
gap: var(--pane-gap);
|
||
}
|
||
|
||
.narrow-shell > *:not(.narrow-nav) {
|
||
/* 同样不写 overflow: hidden —— 见上面 app-shell 那段(会杀掉滚动) */
|
||
border-radius: var(--radius-card);
|
||
}
|
||
|
||
/*
|
||
* ★ 窄屏「页面覆盖」容器(NarrowStack 的根节点)
|
||
* 用户(2026-09-14):「我这边看还是方角」—— 上一轮我只修了宽屏那条分支。
|
||
*
|
||
* 它是窄屏上**唯一**同时具备两个条件的层:
|
||
* ① 自己 `overflow: hidden`(能裁);
|
||
* ② 包着真正有底色的那一层(底层页 / 滑入的覆盖层)。
|
||
*
|
||
* 而 `.narrow-shell > *` 那条圆角落在它**外面**那层透明 flex 容器上
|
||
* (App.tsx 的 `<div className="flex-1 min-h-0 flex">`),那层 `overflow: visible`
|
||
* 裁不住任何东西 —— 于是白底页面的直角原样露出来。
|
||
* 实测 390x844 日历页:左上角像素纯白(方角),面板 computed radius = 0px。
|
||
*
|
||
* 所以圆角必须加在这一层:加给它,底层与覆盖层就**一起**被裁到圆角里。
|
||
* (日历与收件箱详情都走这个容器,一条规则同时修好;收件箱详情以前也是方角。)
|
||
*/
|
||
.narrow-stack {
|
||
border-radius: var(--radius-card);
|
||
}
|
||
|
||
/*
|
||
* ★ 窄屏「单页」容器(没有列表栏的页面:我的 / 管理 / 写信)
|
||
* 用户(2026-09-14):「webui的『我的』页面,还是没有圆角」。
|
||
*
|
||
* 与 `.narrow-stack` 那次**同一个根因**,只是另一条分支:
|
||
* `.narrow-shell > *` 那条圆角落在这一层(透明、overflow: visible),
|
||
* 而真正有底色的是它里面的页面根节点(`AccountPage` 的 `… bg-white`)——
|
||
* 内层的直角从透明外壳里原样戳出来。
|
||
* 实测 390×844「我的」页:面板 computed radius = **0px**,左下/右下都是白像素。
|
||
*
|
||
* 修法照 `cal-panes` 的做法:**圆角给到有底色的那一层**(`> *`),
|
||
* 并且不写 `overflow: hidden` —— 这一层下面的子元素自己就是滚动容器。
|
||
*/
|
||
.narrow-solo > * {
|
||
border-radius: var(--radius-card);
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
/*
|
||
* 横屏紧凑的两栏(列表 + 主区域并排):两栏之间只有 1px 分隔线,
|
||
* 所以只能给**外沿** —— 左边那块给左两角、右边那块给右两角;
|
||
* 内侧也给会露出底色缺口(这一条与 `.cal-panes` 是同一套判断)。
|
||
*/
|
||
.narrow-duo > :first-child {
|
||
border-start-start-radius: var(--radius-card);
|
||
border-end-start-radius: var(--radius-card);
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
.narrow-duo > :last-child {
|
||
border-start-end-radius: var(--radius-card);
|
||
border-end-end-radius: var(--radius-card);
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
html[data-bg='on'] .app-shell {
|
||
padding: var(--pane-gap);
|
||
gap: var(--pane-gap);
|
||
}
|
||
|
||
html[data-bg='on'] .app-shell > * {
|
||
border-radius: var(--radius-card);
|
||
overflow: hidden;
|
||
box-shadow: 0 8px 28px rgb(0 0 0 / 0.16);
|
||
}
|
||
|
||
/*
|
||
* 导航栏改**深色玻璃**(2026-09-14 用户:「导航栏不也应该改为玻璃样式吗,为什么还是黑色」)。
|
||
*
|
||
* 这条规则原本是"完全不透明 + 不参与模糊" —— 那是更早一轮的要求(当时面板太透,
|
||
* 壁纸从导航里透出来显得脏)。现在整套语言已经统一到玻璃,导航继续做一块实心黑
|
||
* 就成了全页唯一的例外,也就是用户说的"割裂"。
|
||
*
|
||
* 用深色玻璃而不是白色玻璃:白字压在深色上才有对比度。α=0.72 + blur(18px)。
|
||
*/
|
||
/*
|
||
* 导航在壁纸模式下不再单独着色:它走 `--nav-bg` 令牌
|
||
* (浅色主题=白玻璃、深色主题=深玻璃,见文件末尾的令牌块)。
|
||
* 原先这里写死深玻璃 —— 那正是用户说的"导航栏为什么还是黑色"。
|
||
*/
|
||
html[data-bg='on'] .nav-rail {
|
||
/* 侧栏浮在壁纸上:模糊由壁纸层负责,这里不叠 */
|
||
}
|
||
|
||
|
||
|
||
/*
|
||
* chrome-600/700 **刻意保持不透明**。
|
||
*
|
||
* 它们不是大面板,而是导航项与 15px 的计数徽标(如「收件 12」)。给它们加
|
||
* 透明度有两个代价:一是看不出背景(本来就太小),二是**正文对比度被拉低**
|
||
* ——实测徽标上的数字从原值降到 4.46:1,低于 WCAG AA 的 4.5(真实渲染量得,
|
||
* 不是估算)。小控件的可读性优先于装饰效果。
|
||
*/
|
||
|
||
/*
|
||
* ─── 现代化打磨 ───
|
||
*/
|
||
|
||
/* 滚动条:桌面应用里它常驻可见,系统默认样式(尤其窄屏上的粗条)很旧。 */
|
||
* {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgb(var(--c-gray-300)) transparent;
|
||
}
|
||
*::-webkit-scrollbar {
|
||
width: 10px;
|
||
height: 10px;
|
||
}
|
||
*::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
*::-webkit-scrollbar-thumb {
|
||
background-color: rgb(var(--c-gray-300));
|
||
border-radius: 9999px;
|
||
/* 透明边框 + background-clip:让滑块比轨道窄,不贴边,观感更轻。 */
|
||
border: 3px solid transparent;
|
||
background-clip: content-box;
|
||
}
|
||
*::-webkit-scrollbar-thumb:hover {
|
||
background-color: rgb(var(--c-gray-400));
|
||
}
|
||
.dark * {
|
||
scrollbar-color: rgb(var(--c-gray-600)) transparent;
|
||
}
|
||
.dark *::-webkit-scrollbar-thumb {
|
||
background-color: rgb(var(--c-gray-600));
|
||
}
|
||
.dark *::-webkit-scrollbar-thumb:hover {
|
||
background-color: rgb(var(--c-gray-500));
|
||
}
|
||
|
||
/*
|
||
* 键盘焦点环。
|
||
*
|
||
* 只在**键盘**导航时出现(:focus-visible),鼠标点击不画 —— 常驻焦点框会让
|
||
* 界面显得脏。这也是可访问性的硬要求:没有可见焦点,键盘用户无法定位。
|
||
*/
|
||
:focus-visible {
|
||
outline: 2px solid rgb(var(--c-blue-500));
|
||
outline-offset: 2px;
|
||
border-radius: 0.25rem;
|
||
}
|
||
|
||
/*
|
||
* 交互元素统一过渡;只过渡颜色类属性,避免布局抖动。
|
||
*
|
||
* ★ 2026-09-15 用户:「主页点击按钮有明显的卡顿」。
|
||
* 原先这里还带一条 `box-shadow var(--dur-base)` —— **阴影是绘制属性,不是合成器属性**:
|
||
* 每次 hover/active 都要把元素连同阴影覆盖区重绘一遍,而且它挂在**所有** button/a/input 上。
|
||
* 列表里几十个按钮 + 玻璃卡片背景时,这笔重绘正好落在点击那一帧上,就是"点按钮卡一下"。
|
||
* 阴影现在直接跟随状态切换(瞬切),不再补间;观感差别很小,代价差很多。
|
||
* 真要动画阴影的个别表面,自己显式写 transition。
|
||
*/
|
||
button,
|
||
a,
|
||
input,
|
||
textarea,
|
||
select,
|
||
[role='button'] {
|
||
transition: background-color var(--dur-fast) var(--ease-out-soft),
|
||
border-color var(--dur-fast) var(--ease-out-soft),
|
||
color var(--dur-fast) var(--ease-out-soft);
|
||
}
|
||
|
||
/* 尊重系统的「减弱动态效果」:前庭功能障碍者会因动画不适。 */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* ═══════════════════════════════════════════════════════════════════
|
||
* 窄屏底部导航:悬浮玻璃条
|
||
*
|
||
* 用户(2026-09-14):「窄屏布局的导航栏还是固定底部延伸,没有玻璃效果,也没有悬浮」。
|
||
* 原来它是 `border-t bg-chrome-900` 的通栏 —— 贴着屏幕底边铺满宽度,与宽屏那套
|
||
* 「浮动面板 + 留缝」的观感完全脱节(宽屏侧栏虽然不是玻璃,但内容面板是浮起来的)。
|
||
*
|
||
* 改成:离底边留 10px、左右各留 10px、圆角、深色半透明 + 背景模糊,落在壁纸之上。
|
||
* 安全区(iPhone 手势条)并入下外边距,否则悬浮条会被手势条压住。
|
||
*/
|
||
.narrow-nav {
|
||
margin: 0 10px calc(10px + env(safe-area-inset-bottom));
|
||
border-radius: var(--radius-card, 14px);
|
||
border: 1px solid rgb(255 255 255 / 0.12);
|
||
/* 底色走 --nav-bg(浅色主题=白玻璃,深色主题=深玻璃) */
|
||
background-color: rgb(var(--nav-bg));
|
||
border: 1px solid rgb(var(--nav-border));
|
||
backdrop-filter: blur(18px) saturate(1.5);
|
||
-webkit-backdrop-filter: blur(18px) saturate(1.5);
|
||
box-shadow: 0 10px 30px rgb(0 0 0 / 0.35);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/*
|
||
* 视图切换动画。
|
||
*
|
||
* 用户:「页面极度缺少动画,所有页面都是直接出现」。
|
||
*
|
||
* 做法:只在 <html> 上挂一个短命的类(由 App 的 effect 在 viewMode/页签变化时重新触发),
|
||
* 由它给外壳的直接子面板加一次入场动画。**不改 DOM 结构** —— 面板本身是 flex 链上的
|
||
* 一环,套一层动画 wrapper 会把 flex 传递改掉(这类改动最容易把窄屏覆盖层弄坏)。
|
||
*/
|
||
/*
|
||
* ★ 只动**主内容区**,不动侧栏与列表(2026-09-14 用户:「部分动画十分不合理,
|
||
* 会导致页面大范围的闪动,且不能让人自然的把注意力集中在将要出现的页面上」)。
|
||
*
|
||
* 原先这条把外壳的**所有**直接子面板一起做入场动画 ⇒ 切一次视图整屏都在动,
|
||
* 眼睛不知道该看哪里,观感就是"闪"。现在只让将要出现的那一页淡入,
|
||
* 而且幅度从 translateY(6px)+scale 收到 4px、不加缩放(缩放最容易让人觉得闪)。
|
||
*/
|
||
|
||
/*
|
||
* ★ 视图切换**不再做整面板入场动画**(2026-09-14 用户:「部分动画十分不合理,
|
||
* 会导致页面大范围的闪动,且不能让人自然的把注意力集中在将要出现的页面上」)。
|
||
*
|
||
* 我先是从"所有面板一起动"改成"只动主内容区",但试下来仍然不对:页面级淡入
|
||
* 会让**已经在那儿的框架**也一起暗一下,观感还是闪。所以这一档整体删掉 ——
|
||
* 只保留**局部**、有明确语义的动效(日历翻页、菜单展开、背景图淡入)。
|
||
* 骨架不动,注意力自然落在变化的那块内容上。
|
||
*/
|
||
/*
|
||
* ★ 2026-09-15 全量盘点(用户:「全面检查整体的动画」):这里原本挂着
|
||
* `html.view-switch .pane-enter { animation: pane-in … }`,而 **`.pane-enter` 没有任何
|
||
* 组件在穿** —— 9aa702b 删掉整面板入场时留下的死规则(连 @keyframes pane-in 一起删了,
|
||
* 留着就是下一次"看起来有动画其实没人用"的源头)。
|
||
* 页面级入场仍不做:用户 09-14 明确否掉过"整屏一起淡";局部入场走 .rise-in / .pane-rise。
|
||
*/
|
||
|
||
/* 菜单/建议列表:轻微放大淡入,避免"啪"地出现 */
|
||
@keyframes menu-in {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-4px) scale(0.985);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 只给**真正是弹层**的东西:`.animate-menu-in`(候选/下拉菜单自己穿)。
|
||
* ★ 2026-09-15 盘点:原先这条还带着 `html.view-switch .glass-control` —— 那是把
|
||
* "菜单入场"错当成了"控件档入场",于是每次切视图,页面上**所有**按钮与输入框一起
|
||
* 淡入位移(几十个元素同时动)。控件不需要入场动画,要动的是"刚弹出来的那张列表"。
|
||
*/
|
||
.animate-menu-in {
|
||
animation: menu-in 140ms ease-out both;
|
||
}
|
||
|
||
/* 尊重系统设置:关了动画就一点都不动(前庭功能敏感的人会被位移动画伤到) */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
/* 2026-09-15:`.rise-in`(挂载即播那档)原先漏在这儿 —— 关掉动画的人照样会看到它 */
|
||
.rise-in,
|
||
html.view-switch .app-shell > *,
|
||
html.view-switch .narrow-shell > *,
|
||
html.view-switch .rise-in,
|
||
html.view-switch .pane-rise,
|
||
html.view-switch .glass-control,
|
||
.animate-menu-in {
|
||
animation: none !important;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* .rise-in —— **局部**入场(写信页、回复框、转发面板)。
|
||
*
|
||
* ★ 2026-09-15 用户:「从发信按钮到发信页面,从聊天按钮到聊天输入,以及转发拉起
|
||
* 输入框都没有对应的动画,所有的动画都消失了,是不是我批评一下用力过猛你就
|
||
* 全给删了」—— 他猜对了。
|
||
*
|
||
* 9aa702b(2026-09-14)把 `html.view-switch .app-shell > *, .narrow-shell > *`
|
||
* 整条删掉、换成 `.pane-enter`,而 **`.pane-enter` 没有任何组件在穿**(已 grep 确认)
|
||
* ⇒ 这一档从「全部一起动」直接变成「一条都不动」,连这三处**局部**、
|
||
* 有明确语义的入场也一起没了。当时的批评针对的是**整面板/整屏**一起淡入
|
||
* (骨架跟着暗一下 = 闪),不是针对局部入场。
|
||
*
|
||
* 所以这里只补回"刚出现的那一块":4px + 不缩放,150ms,骨架一动不动。
|
||
* 位移与时长跟 pane-in/menu-in 同一套尺子(用户对"闪"的容忍度很低,
|
||
* 缩放与长位移是最容易读出"闪"的两项,已经拿掉了)。
|
||
*/
|
||
@keyframes rise-in {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(4px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* ★ 2026-09-15 用户:「回复邮件那个按钮还是没有动画」(我上一版只给回复框/转发面板挂了
|
||
* 类,却没让触发窗口在那两个动作上出现 —— view-switch 只由 视图/页签/窄屏/写信 触发,
|
||
* 于是类挂着、动画永远不播)。
|
||
*
|
||
* 所以拆成两档,各管各的:
|
||
* .rise-in —— **挂载即播**(无条件)。给"点了才出现"的面:写信页、回复框、转发面板。
|
||
* 它们只在用户动作时挂载,首屏不会有一堆东西同时淡入。
|
||
* .pane-rise —— 由 `html.view-switch` 窗口触发。给**常驻**面板(列表栏、日历):
|
||
* 它们一直挂在 DOM 上,只能靠切换窗口重放动画。
|
||
*/
|
||
.rise-in,
|
||
.pane-rise {
|
||
animation: none;
|
||
}
|
||
|
||
.rise-in {
|
||
animation: rise-in 150ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||
will-change: transform, opacity;
|
||
}
|
||
|
||
html.view-switch .rise-in {
|
||
animation: rise-in 150ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||
will-change: transform, opacity;
|
||
}
|
||
|
||
html.view-switch .pane-rise {
|
||
animation: rise-in 150ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||
/*
|
||
* ★ 2026-09-15 用户:「动画卡顿严重」。
|
||
*
|
||
* 这一档只动 opacity + transform(合成器可做,不触发 layout/paint),
|
||
* 但没提层的时候浏览器不一定把它提到独立图层 —— 一提就不可能在主线程上抖动。
|
||
* `will-change` 写在 view-switch 窗口里(只在切换的 ~400ms 内有效),
|
||
* 不是写在元素上:常驻 will-change 会把每个面板都变成常驻图层,内存白吃。
|
||
*/
|
||
will-change: transform, opacity;
|
||
}
|
||
|
||
|
||
/*
|
||
* ★ 通信中栏里的直接子面板必须能被压缩(2026-09-14 用户:「通信页面的各个子页面
|
||
* 还是无法滑动啊,我真服了」)。
|
||
*
|
||
* 根因:我把「页签 + 列表 + 悬浮加号」包进了一个 flex 列 wrapper 来挂内部导航,
|
||
* 而 **flex 子项默认 min-height: auto** ⇒ 子面板不能被压到比内容矮 ⇒ 列表被撑高 ⇒
|
||
* 外壳的 overflow:hidden 把它整段裁掉:表现出来就是"完全无法上下滑动",
|
||
* 而且因为容器自己没溢出,连滚动条都不会出现。
|
||
*
|
||
* 为什么以前没坏:列表原先直接挂在窄屏覆盖层(`absolute inset-0 flex`,**行**方向)下,
|
||
* 行方向的高度来自 align-items: stretch,不需要 min-height:0;换成列方向后就必需了。
|
||
*
|
||
* 这条规则对所有子页面一视同仁(收件/发件/授权/联系人),避免只修好一个。
|
||
*/
|
||
/*
|
||
* 内层面板自己也要圆角。
|
||
*
|
||
* 之前圆角只加在外壳(.app-shell > * 与 .narrow-shell > *)上,靠 overflow:hidden 裁掉
|
||
* 内层直角 —— 但那个 overflow 会杀掉滚动(见上),必须去掉。于是内层白底面板的
|
||
* **直角从圆角外壳里戳出来**,看起来就是"大面积的非圆角元素"。
|
||
* 把圆角直接给内层面板,两边就对齐了,而且不依赖裁剪 ⇒ 与滚动不冲突。
|
||
*/
|
||
.comm-pane > *:not([data-testid='comm-tabs']) {
|
||
border-radius: var(--radius-card);
|
||
/*
|
||
* 既要能压缩(min-height:0),也要**填满**这一列(flex:1 1 0%):
|
||
* 子面板(如 MailList 的根)原本写着 `shrink-0` —— 那是给"行"方向布局写的,
|
||
* 放进列方向后它会连高度都不肯让,内部 `flex-1 overflow-y-auto` 永远拿不到
|
||
* 可用高度,于是"完全无法上下滑动"。
|
||
*/
|
||
flex: 1 1 0%;
|
||
min-height: 0;
|
||
}
|
||
|
||
/*
|
||
* ★ 日历:网格栏与右栏拼成**一张卡**(中间是分隔线,不是外壳间距)
|
||
* 用户(2026-09-14):「日历页面还没有添加圆角」。
|
||
*
|
||
* 为什么它一直没圆角:外壳那条 `.app-shell > *` 的圆角加在日历**根节点**上,
|
||
* 而根节点是 `flex-1 min-w-0 flex min-h-0` —— **它自己没有底色**。
|
||
* 真正白底的是里面那两块面板,直角从透明外壳里戳出来,看上去就是没圆角。
|
||
*
|
||
* 两个不能照搬收件箱做法的地方:
|
||
*
|
||
* ① 这两块面板**不是两张卡**(中间只有 1px 分隔线、没有外壳间距),
|
||
* 所以只能给**外沿**:左边那块给左两角、右边那块给右两角。
|
||
* 内侧要是也圆了,分隔线两边会露出底色缺口。
|
||
* ② 右栏外面还套了一层容器(管宽度 `lg:w-[400px]` 与 `border-l`,自己不上色),
|
||
* 真正上色的是它里面的 DayAgendaPane / CalendarEventEditor。
|
||
* 圆角只加在容器上会被内层的直角**原地盖掉** —— 所以要再往下给一层。
|
||
* (左栏本身就是白底面板,不能再往下给:它的第一个子元素是工具条,
|
||
* 给下去变成"工具条圆角",那不是我们要的东西。)
|
||
*/
|
||
.cal-panes > :first-child {
|
||
border-start-start-radius: var(--radius-card);
|
||
border-end-start-radius: var(--radius-card);
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
.cal-panes > :last-child,
|
||
.cal-panes > :last-child > * {
|
||
border-start-end-radius: var(--radius-card);
|
||
border-end-end-radius: var(--radius-card);
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
|
||
/*
|
||
* 桌面侧栏:无论有没有壁纸都做深色玻璃 —— 否则它是全页唯一一块实心黑。
|
||
* α=0.82 比壁纸模式的 0.72 实一点:此时它后面是页面底色而不是照片,太透只会显脏。
|
||
*/
|
||
/*
|
||
* 这里曾有一条 `.app-shell > .bg-chrome-900 { background-color: rgb(var(--c-chrome-900) / 0.82) }`。
|
||
*
|
||
* 用户报「宽屏 ui 你是一点没修复啊」「导航栏为什么还是黑色」——它就是元凶:
|
||
* 侧栏改成 `.nav-rail`(走 --nav-bg 令牌,浅色主题=白玻璃)之后,这条更早的规则
|
||
* **特异性更高**((0,2,0) > (0,1,0)),继续把侧栏压成深色,看起来就像"根本没改"。
|
||
* 现在整条删掉:侧栏底色只有 --nav-bg 一个来源。
|
||
*/
|
||
|
||
|
||
/*
|
||
* 日历翻页过渡(2026-09-14 用户:「日历滑动页面为什么没有切换动画?」)。
|
||
*
|
||
* 方向由 shift() 记录:下一段从右边推进来、上一段从左边推进来 —— 与手势方向一致。
|
||
* 位移比整屏小(12% / 上限 56px):日历是密排网格,整屏平移会显得晃。
|
||
*/
|
||
@keyframes cal-in-next {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(12%);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
@keyframes cal-in-prev {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(-12%);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
.cal-slide-next {
|
||
animation: cal-in-next 200ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||
}
|
||
|
||
.cal-slide-prev {
|
||
animation: cal-in-prev 200ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.cal-slide-next,
|
||
.cal-slide-prev {
|
||
animation: none;
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* 紧凑双栏(手机横屏)下,列表栏固定 320px。
|
||
*
|
||
* 为什么不能靠 Tailwind:列表栏的宽度写的是 `lg:w-[320px]`,而 lg 断点是 1024px ——
|
||
* 844 宽的横屏**不满足** ⇒ 它退回 `w-full` ⇒ 列表吃掉整宽、详情被挤成一条缝
|
||
* (这正是用户最早说"横屏没自适应"背后的一半原因)。
|
||
* 这里按"紧凑双栏"这个语义条件给宽度,而不是再猜一个像素断点。
|
||
*/
|
||
.compact-2pane .comm-pane {
|
||
flex: 0 0 320px;
|
||
max-width: 320px;
|
||
}
|
||
|
||
/* 详情栏占满剩下的宽度 */
|
||
.compact-2pane > .flex-1 > :last-child {
|
||
flex: 1 1 0%;
|
||
min-width: 0;
|
||
}
|
||
|
||
|
||
/*
|
||
* ═══════════════════════════════════════════════════════════════════
|
||
* 导航配色令牌(浅色玻璃 + 深字,深色主题自动反色)
|
||
*
|
||
* 用户(2026-09-14):「那你为什么不把白字换成黑字或者自动反色或者描边呢?」
|
||
* —— 他是对的。这个应用是浅色底 + 深字,导航却是全页唯一一块黑的:
|
||
* 我上一轮为了"白字对比度"把它做成深玻璃,那是**用错误的方式解决对比度**,
|
||
* 真正的割裂就在这儿。深色底只应该是深色主题下的样子。
|
||
*
|
||
/*
|
||
* 所以导航底色/文字都走令牌:
|
||
* - 浅色主题:白玻璃(0.72)+ 深字;
|
||
* - 深色主题(.dark):深玻璃(0.72)+ 亮字。
|
||
* 这样"对比度"是**按主题算出来的**,而不是靠把某一块永久压黑。
|
||
*/
|
||
:root {
|
||
--nav-bg: 255 255 255 / 0.72;
|
||
--nav-fg: 15 23 42;
|
||
--nav-fg-muted: 71 85 105;
|
||
--nav-hover-bg: 15 23 42 / 0.07;
|
||
--nav-active-bg: 219 234 254;
|
||
--nav-active-fg: 30 64 175;
|
||
--nav-border: 15 23 42 / 0.08;
|
||
}
|
||
|
||
/*
|
||
* ★ 深色主题:**暂不单独给导航换色**(2026-09-14 用户:「导航栏为什么还是黑色」)。
|
||
*
|
||
* 根因不是令牌抄错,而是**整个应用还没有深色主题**:
|
||
* `tailwind.config.js` 里 `darkMode: 'class'` 是配好的,但**没有任何组件写 `dark:` 变体**
|
||
* ⇒ 挂上 `.dark` 只会切换我手写的 CSS 变量,Tailwind 工具类一律照旧。
|
||
* 于是系统是深色时:导航(有深色令牌)变深、正文(没有深色样式)仍是浅色 ——
|
||
* 界面半深不浅,用户看到的就是"导航栏还是黑色"。
|
||
*
|
||
* 在真正的深色主题做出来之前,导航跟随**内容的实际形态**(浅色)。
|
||
* 要做深色主题,正确做法是给组件补齐 `dark:` 变体(那是独立一件事),
|
||
* 而不是先把导航单独压深。
|
||
*/
|
||
/*
|
||
* 深色主题 —— **2026-09-17 落地**(用户报「深色模式可读性差」)。
|
||
*
|
||
* 这里原先是有意留空的,理由是「组件侧没有 dark: 变体,只换令牌会半深不浅」。
|
||
* 实测证明那个担心方向反了:组件写的是**语义色阶**(`bg-white` / `text-gray-900`),
|
||
* 灰阶反转后它们自动适配;真正没适配的是**手写 CSS 里那几处硬编码白色**。
|
||
*
|
||
* 实测(深色、1280×800,读页面计算值):
|
||
* .nav-rail 合成 rgb(188,189,190) 浅灰条;--nav-fg-muted 文字只有 4.03:1
|
||
* .glass-card 合成 rgb(237,237,237) 白卡;--c-gray-900 文字 1.07:1(白底白字)
|
||
* 也就是「深色下整片白」—— 这正是可读性差的来源。
|
||
*
|
||
* 修法遵守既有契约(background.test.mjs「玻璃是白色材料」):**基材不改**
|
||
* (--glass-base 仍只有一处定义、仍是白),主题之间只差 **alpha** ——
|
||
* 深色下把白的 alpha 压到 0.06,让深底透上来;文字令牌本来就已反转。
|
||
*/
|
||
.dark {
|
||
/*
|
||
* 导航:深玻璃 + 亮字。
|
||
*
|
||
* 这里曾有一条判据禁止 `.dark` 出现 `--nav-*`(背景是**当时还没有深色主题**
|
||
* ⇒ 只压深导航就会「导航黑、正文白」,比全浅更割裂)。那条判据的注释写明
|
||
* 「真做深色主题时,这条要连同 dark: 变体一起改」—— 本行即那个时刻,
|
||
* 判据已同步改成断言这套深色令牌**存在且对比度达标**。
|
||
*/
|
||
--nav-bg: 30 35 44 / 0.72;
|
||
--nav-fg: 236 240 246;
|
||
--nav-fg-muted: 148 158 175;
|
||
--nav-hover-bg: 255 255 255 / 0.08;
|
||
--nav-active-bg: 30 58 95;
|
||
--nav-active-fg: 154 191 250;
|
||
--nav-border: 255 255 255 / 0.08;
|
||
|
||
/* 卡片面:白基材只降 alpha(0.92 → 0.06),深底便透上来。 */
|
||
--glass-card-a: 0.06;
|
||
--glass-card-hover-a: 0.1;
|
||
--glass-card-wall-a: 0.04;
|
||
--glass-card-wall-hover-a: 0.07;
|
||
/* 深色下靠「边框亮于底」而不是阴影来分层。 */
|
||
--hairline-fg: 255 255 255 / 0.09;
|
||
--hairline-fg-strong: 255 255 255 / 0.16;
|
||
}
|
||
|
||
.nav-rail {
|
||
background-color: rgb(var(--nav-bg));
|
||
border-right: 1px solid rgb(var(--nav-border));
|
||
}
|
||
|
||
.nav-item {
|
||
color: rgb(var(--nav-fg-muted));
|
||
transition: background-color 150ms, color 150ms;
|
||
}
|
||
|
||
.nav-item:hover {
|
||
background-color: rgb(var(--nav-hover-bg));
|
||
color: rgb(var(--nav-fg));
|
||
}
|
||
|
||
.nav-item[data-active='true'] {
|
||
background-color: rgb(var(--nav-active-bg));
|
||
color: rgb(var(--nav-active-fg));
|
||
}
|
||
|
||
/*
|
||
* 底部导航的选中态**只换颜色**。
|
||
*
|
||
* 用户(2026-09-14):「同时底部导航栏选中对应的文字和图标变色即可」。
|
||
* 之前底部这一栏的选中同时被三样东西表达:`--nav-active-bg` 底色、一条
|
||
* `bg-blue-400` 顶部指示条、以及颜色。底部条只有 51px 高,多出来的两个"形状"
|
||
* 只是把图标与文字挤小。
|
||
*
|
||
* 只作用在 `.narrow-nav` 上:宽屏侧栏是 48px 宽的竖条,图标底下那一块底色是它
|
||
* 唯一的选中线索(窄屏还有文字标签与当前页面对应,宽屏没有可参照的文字),
|
||
* 所以"只变色"不能无差别推广到所有 `.nav-item`。
|
||
*/
|
||
.narrow-nav .nav-item[data-active='true'] {
|
||
background-color: transparent;
|
||
}
|
||
|
||
/* 底部导航条:与侧栏同一套令牌(深色主题才变深) */
|
||
.narrow-nav {
|
||
background-color: rgb(var(--nav-bg)) !important;
|
||
}
|
||
|
||
|
||
/*
|
||
* ⚠ 这里曾经写死过一份深色导航的字面值(因为实测"深色下文字没变")。
|
||
* 那是**误判**:`.nav-item` 有 `transition: color .15s`,我在切换 .dark 之后
|
||
* 立刻读 computed color,拿到的是过渡的**起点**(浅色值)⇒ 对比度算出来只有 2.36。
|
||
* 决定性证据是:连内联 style 都"改不动"它 —— 级联不可能这样,只可能是还在过渡中。
|
||
* 等 400ms 再量就是正常的(见 test/manual/nav-contrast-verify.mjs)。
|
||
* 所以深色这一档仍然只用上面的 .dark 令牌块,一处定义。
|
||
*/
|
||
|
||
|
||
/*
|
||
* ═══════════════════════════════════════════════════════════════════
|
||
* 列表项自己的玻璃卡(.glass-card)
|
||
*
|
||
* 用户(2026-09-14):「你为什么是给所有列表项一起套了一个玻璃外框,
|
||
* 而不是每个列表项单独套外框?」
|
||
*
|
||
* 他说得对。之前的做法是「面板 = 一张玻璃,行躺在里面」—— 那是把列表当成
|
||
* 一个整体容器,行只是它的内容。但界面上其它地方(正文卡、弹层)都是"每一项
|
||
* 自己是一块面",列表却是个例外,观感上就少了一层层次。
|
||
*
|
||
* 改成:面板退成透明,**每一项自己是一张玻璃卡**(圆角 + 半透明 + 细边框 +
|
||
* 悬停加深)。这样每项都是独立可点、可拖、可单独高亮的一块。
|
||
*/
|
||
html[data-bg='on'] .comm-pane > .bg-white {
|
||
/* 列表/详情的外框在壁纸模式下让位给"每项一张卡",但**工具条与头部**仍要
|
||
有底色,否则会直接压在壁纸上读不清 —— 所以只把列表容器那一层放透明。 */
|
||
background-color: transparent;
|
||
}
|
||
|
||
.glass-card {
|
||
border-radius: var(--radius-card);
|
||
border: 1px solid rgb(var(--hairline-fg));
|
||
/*
|
||
* 基材**恒为白**,主题之间只差 alpha(--glass-card-a)。
|
||
*
|
||
* 这里原先写死 `rgb(255 255 255 / 0.92)` —— 深色下它就是一块白卡,
|
||
* 而文字走的是反转后的近白令牌 ⇒ 实测 1.07:1 的白底白字。
|
||
* 写死白色的地方,深色主题一律改不成 —— 走令牌才有得改。
|
||
*/
|
||
background-color: rgb(var(--glass-base) / var(--glass-card-a));
|
||
transition: background-color 150ms, border-color 150ms;
|
||
}
|
||
|
||
.glass-card:hover {
|
||
background-color: rgb(var(--glass-base) / var(--glass-card-hover-a));
|
||
border-color: rgb(var(--hairline-fg-strong));
|
||
}
|
||
|
||
html[data-bg='on'] .glass-card {
|
||
background-color: rgb(var(--glass-base) / var(--glass-card-wall-a));
|
||
border-color: rgb(var(--hairline-fg));
|
||
}
|
||
|
||
html[data-bg='on'] .glass-card:hover {
|
||
background-color: rgb(var(--glass-base) / var(--glass-card-wall-hover-a));
|
||
}
|
||
|
||
/*
|
||
* (这里曾有 `.dark .glass-card { background: rgba(255,255,255,0.06) }`。
|
||
* 它当年被撤掉的理由是「组件还没有 dark 变体,这条只会让卡片在浅色页面上
|
||
* 几乎消失」—— 2026-09-17 实测确认组件侧其实是自洽的(灰阶已反转),
|
||
* 缺的正是这一档 alpha,于是它现在以 `--glass-card-a` 的形态回来了。)
|
||
*/
|
||
|
||
|
||
/*
|
||
* ★ 宽屏下通信中栏必须**贴住列表面板的宽度**(2026-09-14 用户截图指出:
|
||
* 「外框列表和右边正文那么大一个空隙」)。
|
||
*
|
||
* 根因是我为了挂内部页签与悬浮加号加的包装层:它写着 `flex-1`,
|
||
* 而里面的列表面板是 `lg:w-[320px]` 固定宽 ⇒ 包装层把剩余宽度全占了,
|
||
* 列表右边到详情左边就空出一条大缝(缝在包装层里,不在任何面板里,
|
||
* 所以看代码很难发现)。
|
||
*
|
||
* 窄屏不用改:那边外壳是列方向,`flex-1` 管的是**高度**,宽度由 `w-full` 决定。
|
||
*/
|
||
.app-shell .comm-pane {
|
||
flex: 0 0 auto;
|
||
width: 320px;
|
||
}
|
||
|
||
|
||
/*
|
||
* ★ 滚动容器的边缘淡出(2026-09-14 用户:「内容项上下滑动会直接被切断」)。
|
||
*
|
||
* 滚动条本身是"一刀切":卡片滚到边缘就被硬生生截断。给滚动容器加一层
|
||
* 上下渐隐的遮罩,切断处变成渐隐 —— 切得有交代,观感也不再像 bug。
|
||
*
|
||
* 只作用在**纵向**滚动容器(`.overflow-y-auto`):周视图那种**横向**滚动
|
||
* 有自己的横向滚动条,加纵向遮罩会跟它打架(用户刚提过手势冲突那件事)。
|
||
* 上下各 12px 与列表内边距(10px)接近,静止时几乎看不出,滚动时才起作用。
|
||
*
|
||
* ★ 2026-09-14 追加(用户:「你有的地方渐变用的过猛了,比如收件人候选那里」):
|
||
* 那个「12px」是拿**长列表**的内边距(10px)当尺子量的,可它作用在**所有**
|
||
* `overflow-y-auto` 上。收件人候选菜单整块才 58px 高(提示行 22px + 一条候选 34px),
|
||
* 上下各淡 12px 共 24px —— 小半个菜单是渐变的,那条唯一的候选底部直接被洗白。
|
||
* 「过猛」的根因不是 12px 太大,而是**固定像素用在了一个高度不固定的东西上**。
|
||
* 所以改成按容器可见高度封顶:min(12px, 10%)。
|
||
* - 高列表(≥120px)拿到的还是 12px,原观感不变;
|
||
* - 矮容器按比例收敛(58px 的菜单 → 5.8px)。
|
||
*/
|
||
.overflow-y-auto {
|
||
-webkit-mask-image: linear-gradient(
|
||
to bottom,
|
||
transparent 0,
|
||
#000 min(12px, 10%),
|
||
#000 calc(100% - min(12px, 10%)),
|
||
transparent 100%
|
||
);
|
||
mask-image: linear-gradient(
|
||
to bottom,
|
||
transparent 0,
|
||
#000 min(12px, 10%),
|
||
#000 calc(100% - min(12px, 10%)),
|
||
transparent 100%
|
||
);
|
||
}
|
||
|
||
/*
|
||
* 弹层(控件档的滚动区)**不淡**:它是圆角 + 边框的独立表面,内容被边框截住
|
||
* 已经「有交代」了 —— 那个淡出本来是为了不让长列表的卡片显得被硬切断的。
|
||
* 而弹层高度本来就小,淡出只会把首尾项洗白(实测:候选菜单 58px,淡掉 24px)。
|
||
*/
|
||
.overflow-y-auto.glass-control,
|
||
.overflow-y-auto.popup-surface {
|
||
-webkit-mask-image: none;
|
||
mask-image: none;
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
自绘窗口标题栏(2026-10-04)
|
||
══════════════════════════════════════════════════════════════════════
|
||
|
||
主进程已改成 `frame: false`(见 electron/main.cjs 的 BrowserWindow 配置),
|
||
所以这一条是**窗口框架本身**,不是装饰。
|
||
|
||
★ 高度为什么定 36px:鸿蒙侧 PC/2in1 的 `windowDecor` 实测 37vp
|
||
(见 `MainPage.ets` 的 insets 日志:statusBar=38.6 navIndicator=27.8
|
||
**windowDecor=37**),两端的窗口控件高度对齐到同一量级,
|
||
免得并排摆两个应用时一个头厚一个头薄。
|
||
这里取 36 而不是 37:桌面端按物理像素算,36 在 1x 下更接近常见做法,
|
||
且能让标题文字在 12px 字号下不出血。**要改就两个一起改**(本仓只有 win/linux)。
|
||
|
||
★ 为什么用 `position: fixed` 而不是布局里的一行:
|
||
它必须覆盖**所有**路由(登录页、加载页、初始化向导、写信全屏),
|
||
而那些分支各自 `return` 自己的根 div —— 塞进布局就得改每一处,
|
||
漏一处就是「某个页面没有标题栏」。见 main.tsx 的接入点说明。
|
||
*/
|
||
.titlebar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 36px;
|
||
z-index: 40;
|
||
|
||
/*
|
||
* ★ 拖动区。整条是 drag,但里面的按钮与文字必须 no-drag ——
|
||
* `drag` 区域里的交互元素**收不到鼠标事件**(这是 Chromium 的规定,
|
||
* 不是本项目的取舍)。漏了 no-drag 的症状是「按钮画得出来、点不动」。
|
||
*/
|
||
-webkit-app-region: drag;
|
||
app-region: drag;
|
||
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
/* 标题文字不参与拖拽命中:否则双击标题想最大化会被当成选中文字 */
|
||
user-select: none;
|
||
|
||
/*
|
||
* 底色跟着主题走:浅色用 chrome-100、深色用 chrome-900 ——
|
||
* 与侧栏同族(见上面 --c-chrome-* 的色阶说明),
|
||
* 这样标题栏与侧栏看起来是**同一块** chrome,不会多出一条"外来"的带子。
|
||
*/
|
||
background: rgb(var(--c-chrome-100));
|
||
/* 底下那条 1px 分隔线用面板分隔线色,不用黑色半透明 —— 深色下后者会脏。 */
|
||
border-bottom: 1px solid rgb(var(--c-gray-200) / 0.9);
|
||
}
|
||
|
||
.dark .titlebar {
|
||
background: rgb(var(--c-chrome-900));
|
||
border-bottom-color: rgb(var(--c-chrome-800));
|
||
}
|
||
|
||
/*
|
||
* 标题文字。
|
||
*
|
||
* ★ `-webkit-app-region: no-drag` 在这里**不是**为了让文字可点,
|
||
* 而是为了让**双击标题栏最大化**生效:drag 区域的双击由系统接管,
|
||
* 而文字层如果自己是 no-drag,双击就会被它吃掉、系统收不到。
|
||
* (按钮也 no-drag,但按钮上的双击不构成「双击标题栏」,语义不同。)
|
||
*/
|
||
.titlebar-title {
|
||
-webkit-app-region: no-drag;
|
||
app-region: no-drag;
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding-left: 12px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: rgb(var(--c-gray-600));
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.dark .titlebar-title {
|
||
color: rgb(var(--c-chrome-400));
|
||
}
|
||
|
||
/* 右侧三个按钮 */
|
||
.titlebar-btns {
|
||
-webkit-app-region: no-drag;
|
||
app-region: no-drag;
|
||
display: flex;
|
||
align-items: stretch;
|
||
height: 100%;
|
||
flex: none;
|
||
}
|
||
|
||
.titlebar-btn {
|
||
/*
|
||
* 命中区 46×36:宽高都过了通行下限(Windows 标题栏按钮 46×32,
|
||
* 这里跟随宽度、把高度补满整条,视觉上更整)。
|
||
*/
|
||
width: 46px;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 0;
|
||
background: transparent;
|
||
color: rgb(var(--c-gray-700));
|
||
cursor: default; /* 系统标题栏按钮不是 pointer */
|
||
transition: background 0.12s ease;
|
||
}
|
||
|
||
.dark .titlebar-btn {
|
||
color: rgb(var(--c-chrome-400));
|
||
}
|
||
|
||
.titlebar-btn:hover {
|
||
background: rgb(var(--c-gray-900) / 0.08);
|
||
}
|
||
|
||
.dark .titlebar-btn:hover {
|
||
background: rgb(var(--c-chrome-400) / 0.12);
|
||
}
|
||
|
||
/* 关闭 hover 用红,与 Windows 一致 —— 这是"唯一危险动作"的通用语言 */
|
||
.titlebar-btn-close:hover {
|
||
background: #e81123;
|
||
color: #fff;
|
||
}
|
||
.dark .titlebar-btn-close:hover {
|
||
background: #c42b1c;
|
||
color: #fff;
|
||
}
|
||
|
||
/* 焦点可见性:键盘用户必须能看到焦点在哪(见 G-1 无障碍欠账的同族要求) */
|
||
.titlebar-btn:focus-visible {
|
||
outline: 2px solid rgb(var(--c-accent, 37 99 235));
|
||
outline-offset: -2px;
|
||
}
|
||
|
||
/*
|
||
* 图标:12×12 的 viewBox,用 stroke 画(与 icons.tsx 同一套 `currentColor` 约定)。
|
||
* `fill: none` 必须显式给 —— 否则 SVG 默认 fill=black,叉会变成一个黑块。
|
||
*/
|
||
.tb-glyph {
|
||
width: 12px;
|
||
height: 12px;
|
||
fill: none;
|
||
stroke: currentColor;
|
||
stroke-width: 1;
|
||
stroke-linecap: round;
|
||
pointer-events: none; /* 图标不参与命中,命中归按钮 */
|
||
}
|
||
|
||
/*
|
||
* ══ 标题栏占位:把内容整体往下让出 36px ══
|
||
*
|
||
* ★ 为什么用 padding 而不是把 App 往下挪:
|
||
* App 的三个分支(宽屏 / 窄屏 / 无列表的独页)各自是 `h-full`,
|
||
* 改它们的根节点要改三处且都与各自的布局判断耦合。
|
||
* 而 `html.titlebar-on` 上加 padding-top 是**一处**、对所有分支一致 ——
|
||
* 与 `.safe-frame` 处理窄屏安全区是同一形状(那里也是在外层加 padding)。
|
||
*
|
||
* ⚠️ 但 `html, body, #root { height: var(--app-height) }` 是 `height` 不是 `min-height`
|
||
* ⇒ 在 html 上加 padding 会让**总高超出视口**(100% + 36px),
|
||
* 于是内容底部被裁掉 36px。所以下面这条必须把高度改成 `calc`:
|
||
* 见 `.titlebar-on { height: ... }` 那段。
|
||
*/
|
||
html.titlebar-on {
|
||
height: calc(var(--app-height) - 36px);
|
||
/* 上面减了 36,下面补 36 ⇒ 布局视口仍是满的,只有内容被推下去 */
|
||
padding-top: 36px;
|
||
}
|
||
|
||
html.titlebar-on body,
|
||
html.titlebar-on #root {
|
||
height: 100%;
|
||
}
|
||
|
||
/*
|
||
* 开机动画:`App.tsx` 根上那个 `animation` 会做位移/缩放
|
||
* (见它 `viewMode`/`commTab` 变化时 cancelAnimationFrame 那段)。
|
||
* 标题栏是 fixed 的、与内容无关 ⇒ 不参与那些动画,否则每切一次路由
|
||
* 标题栏自己也要淡入一次(看着像闪)。
|
||
*/
|
||
|
||
/*
|
||
* ══ 最大化时:标题栏仍然在(Win/Linux 的系统标题栏最大化后也在),
|
||
* 但不再需要那条分隔线 —— 面板顶边就是分界。 ══
|
||
*/
|