Files
MailUI4Agents/client/harmony/entry/src/main/ets/common/Theme.ets
JianFeeeee 7b3028342a 跨端: 宽屏侧栏根本不像 WebUI —— 因为我上一版"复刻"的依据是编的
用户:「你自己看看宽屏的侧边栏和webui有哪怕一丁点的相似之处嘛?」

并排截图(WebUI 1100×700 @2x vs 三折叠展开态 3184×2232)之后,差异一眼可见:

| | WebUI | 鸿蒙(改前) |
|---|---|---|
| 文字标签 | **有**(通信/日历/联系) | 没有 |
| 选中态 | **浅蓝底块** | 只换颜色 |
| 「我的」 | 底部头像按钮进入 | 甩给 `onSettings` → **pushUrl 推页** |
| 品牌标颜色 | `#475569` 石板灰 | 品牌蓝 |
| 项间距 | 48px 项 + 4px gap,**贴顶一簇** | `layoutWeight(1)` 等分铺满(395px/项) |

## 根因:`WideSidebar` 里那段"复刻 WebUI"的注释是**编的**

```
 * WebUI 的 `Sidebar`(60px 宽)是**纯图标轨**(无 label 文字)……
 * 选中态:图标变色(`navFgActive`),**不加背景块、不加指示条、不加文字**
 * (用户 2026-09-16:「底部导航栏不允许有文字」⇒ 侧栏同样按纯图标走)
```

两条都错,而且都能在源码里当场证伪:

- `Sidebar.tsx:110` 明明有 `<span className="text-3xs leading-none">{short}</span>`
  —— 通信/日历/联系三个标签一直都在;
- `index.css:1590` 的 `.nav-item[data-active='true'] { background-color: … }`
  就是底块,而且 CSS 注释**专门**说了侧栏必须有它:
  「宽屏侧栏是 48px 宽的竖条,图标底下那一块底色是它**唯一的选中线索**,
  所以"只变色"不能无差别推广到所有 `.nav-item`。」

我犯的错是**把底栏那条纪律套到了侧栏上**:用户 2026-09-14 说「底部导航栏选中
对应的文字和图标变色即可」、2026-09-16 说「底部导航栏不允许有文字」——
两句都针对**底部导航栏**,而侧栏是另一种东西(`index.css:1595-1606` 把这个区别
写得很清楚)。更糟的是我把这个错误**写进了判据**(`harmony-widescreen` ②③ 与
`harmony-nav` 的宽屏分支),于是判据锁住的是我编的理由,一路全绿。

## 修

- 侧栏项 = **图标 + 文字标签 + 选中底块**(`navActiveBg` = `--nav-active-bg` #DBEAFE,
  判据**直接读 WebUI 的 CSS** 取值,不写死、更不引自己的注释)。
- 品牌标:`navBrandFg` = `#475569`(**像素取证**:2x 截图里品牌标附近最常见的墨色
  是 `rgb(71,85,105) ×206` = `--nav-fg-muted`,即中性石板灰,**不是**品牌蓝);
  尺寸/圆角按 WebUI `w-10 h-10 rounded-xl`(40×40、圆角 16);点它回收件箱。
- 项**贴顶一簇**(`Column({ space: 4 })` = WebUI 的 `gap-1`),不再 `layoutWeight(1)`。
- 删掉单列的"设置"入口(`onSettings` 回调一并删除)—— 那正是用户 2026-09-17 报过的
  「我的页面完全没有遵守 nav 的导航规则」(push 页 ⇒ 侧栏整条消失)。
  「我的」由 `ForEach(NAV_CONTENT_ITEMS)` 覆盖(该常量**含第 4 项**,
  走 `onSelect(3)` = 窗格,与底栏同一套)。
- 补避让:侧栏原先**完全没有** `topInset` ⇒ 全屏之后品牌标被状态栏时钟压住。

## 判据(并修掉它们锁住的错误)

- `harmony-widescreen` ②③ **重写**:从"纯图标 / 只变色"改成
  "有文字标签 / 有选中底块 / 不许留 `onSettings`",并读 WebUI `index.css` 拿真实色值。
- `harmony-nav` 宽屏分支:原来断言「侧栏项**不该有文字**」—— 同一条编造。
  改成"图标(Path)画出来了 **且** 文字命中源码 `NAV_ITEMS`"。
- `harmony-nav` 宽屏形状阈值 `boxH > screenH*0.08` 是**错的**:48vp 项在密度 2.875 下
  是 138px,而阈值要求 >178px ⇒ 四项全被滤掉(当时"通过"只是因为项被另一个 bug
  压成了 39vp)。改成 `*0.04`,并补一条"必须有文字"把**品牌标**(40vp 无文字的可点方块)
  排除在外。

**变异测试 3 个方向全咬**:去掉文字标签 ⇒ 红;去掉选中底块 ⇒ 红;Theme 色值写错 ⇒ 红。

★ 顺带记一条**我差点犯的错**:我一度按 density 3.5 换算,算出"60vp 侧栏被压成 49.4vp",
去查 flex 压缩、加 `.flexShrink(0)` —— 全是假的。实测密度是 **2.875**
(`138px ÷ 48vp = 2.875`),侧栏 173px ÷ 2.875 = **60.2vp**,与声明完全一致。
**没有压缩,是我除错了。** 已撤回那笔改动并把口径写进注释。

harmony-widescreen 6/6、harmony-nav 18/18、harmony-window 9/9、harmony-arkts 5/5、
harmony-contacts 5/5、harmony-calendar 30/30、harmony-system-api 5/5、harmony-logic 28/28。
2026-09-18 12:52:48 +08:00

426 lines
23 KiB
Plaintext
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.

/*
* AgentMail 鸿蒙客户端 — 设计令牌
*
* ## 这个文件在 2026-09-14 换了一次"来源"(jianf:「鸿蒙要求用系统方案」)
*
* 原先这里的每一格都是**手抄 WebUI 的色值**(`#F8FAFC`、`#0F172A`…)。现在分成两类,
* 分界线是"**系统有没有这份语义**":
*
* - **表面 / 文字 / 分隔 / 遮罩 / 圆角 / 材质 —— 交给系统**(`$r('sys.color.*')`、
* `$r('sys.float.*')`、`BlurStyle.*`)。理由不是"看起来更原生",而是三条实际的:
* ① 它们会**跟随深色模式**(手抄的值不会,这正是 WebUI 那边"半深不浅"的病根);
* ② 跟随系统动效/无障碍设置;
* ③ 少一处会漂移的副本 —— 这些格子的值不再由我们决定,也就不会再和系统打架。
* - **品牌色与业务语义色 —— 仍然自己写**:`accent = #2563EB` 是**跨客户端身份**
* (两个客户端是同一个产品),不能退化成随主题/厂商皮肤变的系统强调色;
* 权限三档(蓝/绿/琥珀)与预算三档(灰/橙/红)在系统里**没有对应物**
* (系统只有 warning/alert 两个"情绪色"),硬套会把语义丢掉。
*
* ## 名字不是猜的
*
* `sys.*` 的名字一旦写错,**编译期不报**、只有真机运行到那一行才炸 —— 而本机没有设备。
* 所以名字全部对着 SDK 自带的系统资源名表核过:
* `sdk/default/openharmony/toolchains/id_defined.json`(本机 API 26,7826 条)。
* 而且有一条判据(`client/electron/test/harmony-system-api.test.mjs`)持续盯着这件事:
* 源码里每个 `$r('sys.<type>.<name>')` 都必须在表里存在且类型相符。
*
* ## 保留的取舍
*
* - WebUI 的"正文面"是 0.88 不透明的玻璃(字要读得清);鸿蒙用的系统卡片底色也是不透明的,
* 结论一致:**正文面不透,只有浮在内容之上的那一层用材质**("玻璃只出现在一层")。
* - 字体大小仍与 WebUI 的 text-2xs/xs/sm 对齐(字号不是"系统方案"要解决的问题,
* 两边的版式意图是同一套)。
*/
import { curves } from '@kit.ArkUI';
export class Theme {
// ─────────────── 表面:系统语义色 ───────────────
/** 页面底色(系统 `ohos_id_color_background`:浅色白/深色深灰,自动跟随主题) */
static readonly pageBg: Resource = $r('sys.color.ohos_id_color_background');
/** 承载文字的面 = **列表卡片底色**(对应"每项一张卡/气泡",而不是通栏底色) */
static readonly surface: Resource = $r('sys.color.ohos_id_color_list_card_bg');
/** 次级面(分组底、列表行 hover) */
static readonly surfaceMuted: Resource = $r('sys.color.ohos_id_color_sub_background');
/** 分隔线 */
static readonly border: Resource = $r('sys.color.ohos_id_color_list_separator');
// ─────────────── 文字:系统语义色(三级) ───────────────
static readonly textPrimary: Resource = $r('sys.color.ohos_id_color_text_primary');
static readonly textMuted: Resource = $r('sys.color.ohos_id_color_text_secondary');
static readonly textSubtle: Resource = $r('sys.color.ohos_id_color_text_tertiary');
// ─────────────── 遮罩与材质:交给系统 ───────────────
/**
* 遮罩(模态/淡出层):系统遮罩色。
*
* 这里原来存着 `overlayColor` + `overlayAlpha` 两个常量,再用 `overlay()` 拼成
* `#AARRGGBB` —— 之所以要"色与透明度分开",是因为**遮罩色必须随主题换向**
* (浅色主题用白把图案洗淡、深色主题必须换黑,否则浅色照片在深色界面里糊成一块亮斑)。
* 那件事现在由系统做:一个语义色就够,且换向不会再漏。
*/
static readonly overlay: Resource = $r('sys.color.ohos_id_color_mask_regular');
/**
* 壁纸遮盖层的颜色 —— **不是** `overlay`(那是模态遮罩),两者语义不同。
*
* pi 2026-09-14 提出的方向性疑问,我按他说的去 SDK 里把值读出来了
* (`ets/build-tools/ets-loader/sysResource.js` 给 名字→id,
* `previewer/common/resources/entry/resources.txt` 给 id→值,两张表交叉验证):
*
* | 令牌 | 浅色主题 | 深色主题 |
* |---|---|---|
* | `ohos_id_color_mask_regular`(= `overlay`) | `#99182431` **深蓝灰** | `#b2000000` 黑 |
* | `ohos_id_color_background` | `#ffffffff` **白** | `#ff18181a` 近黑 |
*
* 也就是说 **`mask_*` 在两套主题下都是深色**(它的 light/regular/thick 是**浓度档**,
* 不是深浅两套值),用途是**模态遮罩**(弹层背后压暗)。
* 而 WebUI 的 `--bg-scrim` 在 `:root` 是**白**、`.dark` 才是黑,注释原话是
* 「浅色下用白把花哨的图案洗淡,深色下用黑压暗」—— 它做的是"**朝页面底色淡化**"。
*
* 所以壁纸遮盖若用 `mask`:**浅色主题下会把预设压暗,而 WebUI 是把它洗淡 —— 方向相反**。
* 这是"机制上确定不同",不是观感。改用**页面底色系**:那才是"朝底色淡化"的系统对应物,
* 浅色=白底方向、深色=深底方向,自动换向,与 WebUI 是同一个意图。
*
* ⚠️ 为什么不干脆复用 `pageBg`(值一样是 `ohos_id_color_background`):
* 页面底在背景开启时会被换成语义上的**透明**(`bgActive ? Color.Transparent : Theme.pageBg`),
* 而遮盖层**永远需要一个真实颜色**。两个用途的生命周期不同,共用一个名字迟早坏一头
* —— 这正是这个仓库撞过四次的模式(`text-white` vs `bg-white`、`--c-*` vs `--s-*`)。
*/
static readonly wallpaperScrim: Resource = $r('sys.color.ohos_id_color_background');
/**
* 导航/浮层的材质档次。**不再有 `#B8FFFFFF` / `#B80F172A` 这种手写玻璃 alpha** ——
* 那两个值等于"我们替系统猜了深色该怎么做",与"用系统方案"直接冲突;
* 而材质档次是系统给的,深浅两套颜色由系统按主题挑。
*
* 用 `COMPONENT_THICK`:贴在界面组件上的一层材质(导航条正属于这一类)。
*/
static readonly navMaterial: BlurStyle = BlurStyle.COMPONENT_THICK;
// ─────────────── 圆角:系统尺寸 ───────────────
/** 卡片圆角:系统"卡片"圆角(不再与 WebUI 的 14vp 绑死 —— 允许各自跟随系统) */
static readonly radiusCard: Resource = $r('sys.float.ohos_id_corner_radius_card');
/** 控件圆角:系统"按钮"圆角 */
static readonly radiusControl: Resource = $r('sys.float.ohos_id_corner_radius_button');
/*
* ★ 2026-09-16:宽屏玻璃面板几何(与 WebUI `app-shell` 的 CSS 变量**同值**)。
* `--radius-card: 0.875rem` = 14px、`--pane-gap: 10px`(`index.css`)。
* 写死常量而不是 `$r('sys.*')`:因为 WebUI 是 14/10,而系统的
* `corner_radius_card` 是 16 —— 宽屏面板必须用 WebUI 的值才能"一比一复刻"。
* 原有的 `radiusCard`/`radiusControl` 保持系统跟随,不动它们。
*/
static readonly glassRadius: number = 14;
static readonly paneGap: number = 10;
/*
* ─────────────── 手写色**登记表**(改这里要一起改判据) ───────────────
*
* 为什么需要这张表:本文件里"自己写的色值"是有理由的(品牌身份 + 系统没有对应物的
* 业务语义色),但**理由不能靠记忆**。判据(`cross-client-theme.test.mjs` 的
* A2 条)会枚举本文件里所有 `static readonly X: string = '#……'`,
* 发现**未登记的名字就判红** —— 于是"新写死一个色"必须显式过一道:
* 要么登记在这里并写清理由,要么它本来就该走 `$r('sys.*')`。
*
* 这条是 pi 读出来的真缺口:只枚举 11 个"必须是系统资源"的名字,挡不住
* 第 12 个**新加的手写色**(它不在名单里,于是 A/B/裸色值三条都碰不到它)。
* 「枚举挡实例,类才挡漂移」—— 这次枚举的是**名字**,所以要有名单。
*
* 登记项(17 个,名字与判据里的 SELF_OWNED_COLORS 逐字一致 —— 逐个列出而不是缩写,
* 这样 grep 一个名字就能找到它的理由):
*
* 品牌(跨客户端身份,系统给不了):
* · accent #2563EB 与 WebUI 的 --c-blue-600 逐字一致
* · accentFg #FFFFFF 品牌底上的文字
* · accentSoft #EFF6FF 品牌浅底(选中态背景)
* · accentStrong #1D4ED8 品牌深色变体(选中文字/边框)
*
* 业务语义(系统只有 list/warning/alert 这一档,没有"同意/拒绝"):
* · approve #15803D 同意(绿)
* · approveBg #F0FDF4 同意底
* · approveFg #15803D 同意文字
* · danger #B91C1C 拒绝/危险(红)
* · dangerBg #FEF2F2 拒绝底
* · warnBg #FFFBEB 警示底(对应 WebUI --c-amber-50)
* · warnFg #B45309 警示文字(对应 WebUI --c-amber-700)
*
* 档位胶囊(档位是产品语义:系统不认识 plan / workspace / full):
* · chipNeutralBg #F3F4F6 plan 档底(最宽档但不报警)
* · chipNeutralFg #5A6270 plan 档文字
* · chipSpentBg #FEE2E2 预算耗尽底
* · chipSpentFg #B91C1C 预算耗尽文字
* · chipWarnBg #FFEDD5 预算紧张底
* · chipWarnFg #C2410C 预算紧张文字
*
* 不在这里的手写色只有一种合法去处:`$r('sys.*')`(跟随系统/深色模式)。
*/
// ─────────────── 品牌色:跨客户端身份,必须自己写 ───────────────
/**
* 品牌蓝(按钮、链接、焦点环)。
*
* **不要**为了"用系统方案"把它换成系统的强调色:系统强调色会随主题/厂商皮肤变,
* 一旦换过去,"两个客户端是同一个产品"这件事就靠不住了。
* 这是整个文件里**唯一必须与 WebUI 逐字一致**的取值(判据钉住)。
*/
static readonly accent: string = '#2563EB';
static readonly accentFg: string = '#FFFFFF';
/** 品牌蓝的浅底 / 深前景(与 WebUI 的 blue-50 / blue-700 成对) */
static readonly accentSoft: string = '#EFF6FF';
static readonly accentStrong: string = '#1D4ED8';
/** 导航文字:未选中 / 选中(对应 --nav-fg-muted / --nav-active-fg) */
static readonly navFg: Resource = $r('sys.color.ohos_id_color_text_secondary');
static readonly navFgActive: string = Theme.accentStrong;
/*
* 侧栏**选中块的底色**(WebUI `--nav-active-bg: 219 234 254`)。
*
* ★ 为什么侧栏有底块、底栏没有(这是两种导航**纪律不同**,不是不一致):
* `index.css:1595-1606` 的原话:「底部导航的选中态**只换颜色**……
* 只作用在 `.narrow-nav` 上:宽屏侧栏是 48px 宽的竖条,图标底下那一块底色
* 是它**唯一的选中线索**,所以"只变色"不能无差别推广到所有 `.nav-item`。」
* 我 2026-09-18 之前把底栏那条纪律套到了侧栏上,还在 `WideSidebar` 的注释里
* 写成「WebUI 的 Sidebar 是纯图标轨(无 label 文字)」—— 那是**编的**:
* `Sidebar.tsx:110` 明明有 `<span className="text-3xs">{short}</span>`,
* `nav-item[data-active='true']` 也明明有底色。并排截图一眼就能看出来。
*/
static readonly navActiveBg: string = '#DBEAFE';
/*
* 侧栏**品牌标**的墨色(WebUI `--nav-fg-muted: 71 85 105` = #475569)。
*
* ★ 用像素取证过,不是照着色阶猜的:在 2x 缩放的 WebUI 侧栏截图上统计
* 品牌标附近最常见的墨色 → `rgb(71,85,105) ×206`,正是 `--nav-fg-muted`。
* 也就是说 WebUI 的品牌标是**中性石板灰**(`.nav-item` 默认色),
* **不是**品牌蓝。鸿蒙这边原来画成 `Theme.accent`(蓝)—— 并排一看就不同色。
*
* 深色主题下 `--nav-fg-muted: 148 158 175`(见 index.css:1559)。
* 鸿蒙暂未跟:`$r('sys.color.*')` 那套只在系统语义色上跟随,而这个值
* 是 WebUI 自己定的中性色。这一条**记在案上**,不假装已经跟了。
*/
static readonly navBrandFg: string = '#475569';
// ─────────────── 业务语义色:系统没有对应物,继续自己写 ───────────────
/** 语义色:同意 / 拒绝(对应 WebUI 的 approve/danger) */
static readonly approve: string = '#15803D';
static readonly danger: string = '#B91C1C';
static readonly dangerBg: string = '#FEF2F2';
static readonly approveBg: string = '#F0FDF4';
static readonly approveFg: string = '#15803D';
/** 警示面/前景(对应 WebUI 的 --c-amber-50 / --c-amber-700)—— 权限 full 档、待决策徽标 */
static readonly warnBg: string = '#FFFBEB';
static readonly warnFg: string = '#B45309';
/**
* 权限档位 → 徽标底色 / 文字色。
*
* 与 WebUI 的 `PermissionChip.tsx` **同一映射**(plan=蓝 / workspace=绿 / full=琥珀),
* 认不出的档位与空档位按 workspace 处理 —— 映射只有这一处实现,
* 页面不再各自 if-else 挑颜色。
*
* 为什么不用系统情绪色:系统只有 warning/alert 两个,凑不出三档;
* 而档位是**可被文档与判据按名字引用的枚举标识**(plan/workspace/full),
* 用情绪色顶替它,界面对了语义丢了。
*/
static permBg(mode: string): string {
if (mode === 'plan') {
return Theme.accentSoft;
}
if (mode === 'full') {
return Theme.warnBg;
}
return Theme.approveBg;
}
static permFg(mode: string): string {
if (mode === 'plan') {
return Theme.accentStrong;
}
if (mode === 'full') {
return Theme.warnFg;
}
return Theme.approveFg;
}
/** 中性 chip(对应 WebUI 的 --c-gray-100 / --c-gray-500)—— 预算条"还宽裕"档 */
static readonly chipNeutralBg: string = '#F3F4F6';
static readonly chipNeutralFg: string = '#5A6270';
/** 预算用尽(对应 WebUI 的 --c-red-100 / --c-red-700) */
static readonly chipSpentBg: string = '#FEE2E2';
static readonly chipSpentFg: string = '#B91C1C';
/** 预算将尽(对应 WebUI 的 --c-orange-100 / --c-orange-700) */
static readonly chipWarnBg: string = '#FFEDD5';
static readonly chipWarnFg: string = '#C2410C';
/**
* 往返预算档位 → chip 底色 / 字色。
*
* 与 WebUI 的 `BudgetChip` **同一映射**(`WorkCard.tsx`):
* 剩 0 = 红(用尽)、剩 ≤1 = 橙(将尽,任务需要人介入)、其余 = 中性灰。
* 档位本身由 `MailGrouping.ts` 的 `budgetState()` 算(那是可被判据执行的一层),
* 这里只管"哪个档用什么颜色"。这三个色**不进跨端"取值相同"判据**(业务局部),
* 但必须进枚举完整性判据(三档齐全、认不出的归一到中性档)。
*/
static budgetBg(state: string): string {
if (state === 'spent') {
return Theme.chipSpentBg;
}
if (state === 'warn') {
return Theme.chipWarnBg;
}
return Theme.chipNeutralBg;
}
static budgetFg(state: string): string {
if (state === 'spent') {
return Theme.chipSpentFg;
}
if (state === 'warn') {
return Theme.chipWarnFg;
}
return Theme.chipNeutralFg;
}
/** 字体大小(与 WebUI 的 text-2xs/xs/sm 对齐) */
static readonly fontTiny: number = 11;
static readonly fontSmall: number = 12;
static readonly fontBody: number = 14;
/*
* ── 动画令牌(对照 WebUI `index.css`)──
*
* 用户(2026-09-17):「一方面一点动画都没有」。
* 实测确认:改造前全仓 `animateTo` / `transition` / `animation` **一次都没用过** ——
* 所有页面切换、详情推入、面板展开都是硬切。
*
* ★★ 2026-09-18 更正:这里原先写着「三个数与 WebUI **逐字一致**」,**那句是错的**。
* 它把「令牌存在」当成了「动画用了那个令牌」。实测(`client/electron/src/index.css`):
*
* `--dur-base: 180ms` 只用在**壁纸淡入**(:747 `transition: opacity …`)
* `--ease-out-soft (0.22,1,0.36,1)` 只用在 **transition**(壁纸、控件变色 :1169)
* 而**所有 @keyframes 动画**用的是 150ms + `cubic-bezier(0.22, 0.61, 0.36, 1)`
*
* 全仓 `(0.22,0.61,0.36,1)` 出现 **5 次**(rise-in ×3 / cal-in ×2),
* `(0.22,1,0.36,1)` 只出现 **1 次**(就是令牌定义处)。**这是两根不同的曲线。**
* 旧代码把面板入场按 `(0.22,1,0.36,1)` + 180ms 做 ⇒ 比 WebUI **慢 30ms 且曲线偏软**,
* 而当时的判据(`harmony-nav` "缓动必须是 0.22,1,0.36,1")锚在**令牌**上,
* 所以这个错它一辈子抓不到。
*
* 所以下面分**两组**,各自对齐各自该对齐的东西:
* 控件类(transition) —— `durFast 120` + `easeOutSoft`
* 动画类(@keyframes)—— 各自时长 + `easeRise`
*/
/** 控件变色(hover / active)—— WebUI `--dur-fast: 120ms` */
static readonly durFast: number = 120;
/**
* 壁纸淡入 —— WebUI `--dur-base: 180ms`。
*
* ★ **只用于壁纸/整体淡入,不是面板入场**(面板入场是 `durRise`)。
* 这个区分是 2026-09-18 查出来的,之前两者被当成同一个数。
*/
static readonly durBase: number = 180;
/** 面板/内容入场 —— WebUI `@keyframes rise-in` 实测 **150ms** */
static readonly durRise: number = 150;
/** 弹层入场 —— WebUI `.animate-menu-in` 实测 **140ms** */
static readonly durMenu: number = 140;
/** 日历翻月 —— WebUI `.cal-slide-next/prev` 实测 **200ms** */
static readonly durCal: number = 200;
/**
* **transition** 用的缓动:`cubic-bezier(0.22, 1, 0.36, 1)` = WebUI `--ease-out-soft`。
*
* 用 `curves.cubicBezierCurve` 而不是 `Curve.EaseOut` 之类的枚举:
* 枚举是系统预设的**另一根**曲线,观感与 WebUI 对不上。
*/
static readonly easeOutSoft: ICurve = curves.cubicBezierCurve(0.22, 1, 0.36, 1);
/**
* **@keyframes 动画**用的缓动:`cubic-bezier(0.22, 0.61, 0.36, 1)`。
*
* ★ 与 `easeOutSoft` **不是同一根曲线**(前者第二段控制点是 0.61,后者是 1)。
* 两者别互换:WebUI 里 transition 用 `--ease-out-soft`,而所有动画
* (rise-in / cal-in)硬编码用的是这一根。
*/
static readonly easeRise: ICurve = curves.cubicBezierCurve(0.22, 0.61, 0.36, 1);
/**
* 窗格入场时的上浮量(vp)。
*
* 4 —— **与 WebUI `@keyframes rise-in` 逐字一致**(`index.css:1208`):
* `from { opacity: 0; transform: translateY(4px); }`
* 只上浮 4vp,是"轻轻落位"而不是"整块飞进来"。用户对"闪/跳"敏感
* (09-14 否掉过整屏淡入),所以这个数**不许**随手调大。
*/
static readonly riseInOffset: number = 4;
/**
* 窗格入场过渡:4vp 上浮 + 淡入(对齐 WebUI `@keyframes rise-in`)。
*
* 做成方法而不是常量,是因为 `TransitionEffect` 是**有状态的构建器**
* (`combine`/`animation` 会就地改自身),多处共用同一个实例会互相干扰;
* 每处调用各拿一个新的。
*
* ★ 时长/曲线用 `durRise` + `easeRise`(**150ms + 0.22,0.61,0.36,1**)——
* 即 WebUI `rise-in` 的**真实取值**。改之前用的是 `durBase(180)` + `easeOutSoft`,
* 那是令牌值而不是动画值(见上面令牌区的更正说明)。
*
* ★ 两边**不对称**:出场用 `durFast`(120ms)比入场快。
* WebUI 那边 `rise-in` 是对称的(一个 @keyframes 两用)—— 因为它是**逐个元素**
* 挂载即播,新旧两棵子树不会同时可见。而 ArkUI 这里是 `if/else` 换子树,
* 两层会**同时半透明地叠着**,同长看起来就是“闪一下”(实测 6 秒取样确认),
* 所以出场必须更快地让位。这是两端机制不同带来的**必要差异**,不是随手拍数。
*/
static paneRiseIn(): TransitionEffect {
return TransitionEffect.asymmetric(
TransitionEffect.opacity(0).combine(
TransitionEffect.translate({ y: Theme.riseInOffset })
).animation({ duration: Theme.durRise, curve: Theme.easeRise }),
TransitionEffect.opacity(0).animation({ duration: Theme.durFast, curve: Theme.easeRise })
);
}
/**
* 弹层入场:下移 4vp + 缩到 0.985 + 淡入(对齐 WebUI `@keyframes menu-in`)。
*
* WebUI `.animate-menu-in` 的注释把适用范围钉得很窄,这里照搬同一个口径:
* 「只给**真正是弹层**的东西(候选/下拉菜单自己穿)」——
* 它原本还挂着 `html.view-switch .glass-control`,于是**每次切视图页面上所有
* 按钮与输入框一起淡入位移**(几十个元素同时动),2026-09-15 被摘掉。
* 所以本方法**只给弹层**(账号选择器、下拉候选),不要挂到常驻控件上。
*
* 入场方向是**从上往下**(`translateY(-4px)` → 0):弹层从触发点的下方展开,
* 从上方“落”下来。与 `rise-in` 的**上浮**方向相反,别混。
*/
static menuIn(): TransitionEffect {
return TransitionEffect.opacity(0).combine(
TransitionEffect.translate({ y: -4 })
).combine(
TransitionEffect.scale({ x: 0.985, y: 0.985 })
).animation({ duration: Theme.durMenu, curve: Theme.easeRise });
}
/**
* 日历翻月的横向滑入(对齐 WebUI `@keyframes cal-in-next/prev`)。
*
* `forward = true` → 下一月,从**右侧**滑入(`translateX(12%)`);
* `forward = false` → 上一月,从**左侧**滑入(`translateX(-12%)`)。
*
* 百分比在 ArkUI 的 `translate` 里是**相对元素自身尺寸**的 —— 与 CSS 语义一致,
* 所以 12% 就是“滑入自身宽度的 12%”,不是拍一个 vp 数(那样在大屏上会变小)。
*/
static calendarSlide(forward: boolean): TransitionEffect {
return TransitionEffect.asymmetric(
TransitionEffect.opacity(0).combine(
TransitionEffect.translate({ x: forward ? '12%' : '-12%' })
).animation({ duration: Theme.durCal, curve: Theme.easeRise }),
TransitionEffect.opacity(0).animation({ duration: Theme.durFast, curve: Theme.easeRise })
);
}
}