fix(design): M228 复审 r1 三条(字段底角色对齐、四处小疵、产品默认口径)

P2-1 字段底角色不对称(塔裁决方案 a):Silver 的 #dcddde 从 surface-container-high 改判到
surface-container-highest,与 Ink 的 #262b25 同位——component_map 的 TextField 口径就是最高
容器层,单一角色接法才不会一边掉层。Silver 的 high 改取推导值 #e4e4e5(tone 90),阶梯
94.12 → 90.61 → 88.07 保持单调、步长 ≥1.74 L*。实测:Error on 新 highest = 4.686(原按单一
角色接到 high 时的 4.379 失守已消除)、OnSurfaceVariant = 6.383、OnSurface = 12.576。
测试同步:Silver 三条字段用法 pin 从 high 改 highest(与 Ink 对称),并新增
`field_backdrop_is_the_same_role_on_both_boards` 把这个不对称钉住(顺带断言 high 比 highest
更亮、highest 就是规格卡的 #dcddde)。

P2-2 四处小疵:
① inkcat/size.rs 扩展 FAB 起始内距 16 → **28**(规格卡画的数):框 x=292 → 图标盒 320(`+`
   font-size 24 居中于 x=332),28 才是图上的距离;16 是 M3 官方口径,已在 doc 里登记为偏离,
  并说明「要回 M3 的 16 得同时把图标中心挪回 316,那是要出截图的视觉改动」。测试补一条自洽
   断言:292 + 28 + 24/2 = 332。
② inkcat/metrics.rs 的 SWITCH_THUMB_INSET 注释重写(原句断成碎片且读起来像算错):写明
   轨道 y=1060..1092 / 拇指 y=1064..1088 ⇒ 上下各 4、右端 116−112=4,以及行程
   52−24−2×4 = 20 的推导。
③ Silver「状态容器 / 容器层」数字统一成实测口径:boards/mod.rs 与 palette/visibility.rs 都写
   「最低 1.02」(Ink 1.54),并点名最差的一组是 tertiary-container 压 surface-container-high
   = 1.016;visibility 表的 Silver 阶梯步长改写 −1.7 ~ −4.1。
④ inkcat/type_scale.rs 正文清单只列 11 个**文字**字号(去掉 FAB 字形 20/26/34),并注明那三个
   归 FabSize::icon_px、SpecText 没有 20 这一档。

P2-3 产品默认口径统一:palettes.rs 的「两个入口」表与 Palettes::baseline 文档、color/mod.rs
的「出厂默认色」小节、Scheme::baseline 文档统一写明「M228 起出厂外观入口是
DesignTokens::brand,本入口是 D38 取色链中性锚点」,与 lib.rs 同口径;顺带把 BASELINE_SEED /
DEFAULT_SEED / DEFAULT_NEUTRAL_CHROMA 的「产品默认」措辞改成取色链口径。

另:tests/ink_cat_boards.rs 已 400 行贴线,把 4 条容器层可视性测试拆到新文件
tests/ink_cat_visibility.rs(纯搬移 + 补 import);palettes.rs 因新增口径说明到 403 行,按
「合并相邻段落、不删信息」压回 399 行;size.rs 现在 323 行。

五步(design 口径):fmt --all --check=0;check -p unlucky-design --all-targets=0(零 warning);
test -p unlucky-design --all-targets=0(267 passed);clippy -p unlucky-design --all-targets
-- -D warnings=0;400 行扫描只剩 README.md(豁免)、无 <2 字节 .rs。
This commit is contained in:
AskaEth committed 2026-11-10 22:34:05 +08:00
1 parent 0169d4c8fb
commit b8003177d7
11 files changed
+203 -118

No files matched your search

+3 -2
View File
@@ -46,8 +46,9 @@
//! **1.057:1**(m223 视觉复审),Ink 板抬到 **1.97:1**、Silver 板 **1.23:1**; //! **1.057:1**(m223 视觉复审),Ink 板抬到 **1.97:1**、Silver 板 **1.23:1**;
//! - **层级容器**(`surface-container-*`)不参与那条数字门禁,只要求 L\\* 沿阶梯单调、相邻步长 //! - **层级容器**(`surface-container-*`)不参与那条数字门禁,只要求 L\\* 沿阶梯单调、相邻步长
//! ≥ [`crate::palette::visibility::LAYER_STEP_MIN_LSTAR`]。M3 的容器层天生贴在一起 //! ≥ [`crate::palette::visibility::LAYER_STEP_MIN_LSTAR`]。M3 的容器层天生贴在一起
//! (Silver 的状态容器对 `surface-container-high` 只有 1.05~1.13),所以裁决第 3 条是 //! (Silver 的状态容器压在容器层上实测最低 **1.02**、Ink 最低 1.54,见 `palette::visibility`
//! **构成要求**:选中态必须「容器底 + 文字对比 + 形状/图标/描边」三件套,不许只靠底色差。 //! 的实测表),所以裁决第 3 条是**构成要求**:选中态必须「容器底 + 文字对比 + 形状/图标/描边」
//! 三件套,不许只靠底色差。
mod ink; mod ink;
mod silver; mod silver;
+9 -3
View File
@@ -10,6 +10,12 @@ use crate::color::{Color, Role};
/// 出处分三处:SILVER CAT / LIGHT SCHEME 的色板行、同页 Filled/Tonal/Outlined 按钮与两个字段 /// 出处分三处:SILVER CAT / LIGHT SCHEME 的色板行、同页 Filled/Tonal/Outlined 按钮与两个字段
/// (`on-primary` 取自 Filled 的文字 `#f6f9ff`、`on-secondary-container` 取自 Tonal 的文字 /// (`on-primary` 取自 Filled 的文字 `#f6f9ff`、`on-secondary-container` 取自 Tonal 的文字
/// `#0d1d2a`、`outline-variant` 取自色板方块的描边 `#bec8cd`)。 /// `#0d1d2a`、`outline-variant` 取自色板方块的描边 `#bec8cd`)。
///
/// ⚠️ 字段底 `#dcddde` 落在 **`surface-container-highest`**(不是 high):它与 Ink 的字段底
/// `#262b25` 是同一层角色(`component_map` 的 `TextField` 口径 = 最高容器层)。浅色板的
/// `surface-container-high` 因此是**推导值** `#e4e4e5`(tone 90),只为让阶梯在 94.1 → 88.1
/// 之间保持单调、步长 ≥1.0 L\\*;若把 `#dcddde` 定在 high,Silver 的字段底会比 Ink 低一层,
/// 单一角色接法(只读 Highest)会读到 4.38:1 的 `Error` 标签(掉 AA)。
pub const ANCHORS: [(Role, &str); 13] = [ pub const ANCHORS: [(Role, &str); 13] = [
(Role::Primary, "#006499"), (Role::Primary, "#006499"),
(Role::OnPrimary, "#F6F9FF"), (Role::OnPrimary, "#F6F9FF"),
@@ -18,7 +24,7 @@ pub const ANCHORS: [(Role, &str); 13] = [
(Role::OnSecondaryContainer, "#0D1D2A"), (Role::OnSecondaryContainer, "#0D1D2A"),
(Role::Surface, "#F9F9FA"), (Role::Surface, "#F9F9FA"),
(Role::SurfaceContainer, "#EEEEEF"), (Role::SurfaceContainer, "#EEEEEF"),
(Role::SurfaceContainerHigh, "#DCDDDE"), (Role::SurfaceContainerHighest, "#DCDDDE"),
(Role::OnSurface, "#1A1C1D"), (Role::OnSurface, "#1A1C1D"),
(Role::OnSurfaceVariant, "#434D51"), (Role::OnSurfaceVariant, "#434D51"),
(Role::Outline, "#6F797D"), (Role::Outline, "#6F797D"),
@@ -55,8 +61,8 @@ pub const COLORS: [Color; 49] = [
Color::rgb(0xFF, 0xFF, 0xFF), // 25 surface-container-lowest [derive] tone 100 Color::rgb(0xFF, 0xFF, 0xFF), // 25 surface-container-lowest [derive] tone 100
Color::rgb(0xF3, 0xF3, 0xF4), // 26 surface-container-low [derive] tone 96 Color::rgb(0xF3, 0xF3, 0xF4), // 26 surface-container-low [derive] tone 96
Color::rgb(0xEE, 0xEE, 0xEF), // 27 surface-container [spec] 卡片/列表行底 Color::rgb(0xEE, 0xEE, 0xEF), // 27 surface-container [spec] 卡片/列表行底
Color::rgb(0xDC, 0xDD, 0xDE), // 28 surface-container-high [spec] 输入框底 Color::rgb(0xE4, 0xE4, 0xE5), // 28 surface-container-high [derive] tone 90(比 highest 高 2.5 L*,阶梯步长 3.5)
Color::rgb(0xD5, 0xD6, 0xD7), // 29 surface-container-highest [derive] tone 86 Color::rgb(0xDC, 0xDD, 0xDE), // 29 surface-container-highest [spec] 输入框底(与 Ink 的 #262B25 同位:字段底就是最高容器层)
Color::rgb(0x6F, 0x79, 0x7D), // 30 outline [spec] 字段描边 Color::rgb(0x6F, 0x79, 0x7D), // 30 outline [spec] 字段描边
Color::rgb(0xBE, 0xC8, 0xCD), // 31 outline-variant [spec] 分隔线、chip 描边 Color::rgb(0xBE, 0xC8, 0xCD), // 31 outline-variant [spec] 分隔线、chip 描边
Color::rgb(0x2F, 0x31, 0x33), // 32 inverse-surface [derive] tone 20 Color::rgb(0x2F, 0x31, 0x33), // 32 inverse-surface [derive] tone 20
+4
View File
@@ -53,6 +53,10 @@
//! //!
//! ## 出厂默认色(M70:浅冷天蓝)——含**两处**有意偏离(同一处偏离的两半) //! ## 出厂默认色(M70:浅冷天蓝)——含**两处**有意偏离(同一处偏离的两半)
//! //!
//! > **口径更新(M228)**:出厂外观的入口是 `DesignTokens::brand`(Ink / Silver 两套固化色板,
//! > 见 [`crate::color::boards`])。本节描述的「浅冷天蓝」**仍是 D38 取色链的中性锚点**
//! > (`Scheme::baseline` / `from_accent` 的表面色基准),不再是「产品出厂看起来长什么样」。
//!
//! 产品默认不走官方紫,而是「浅冷天蓝」。默认值收在两个常量里,**换默认只改这两行** //! 产品默认不走官方紫,而是「浅冷天蓝」。默认值收在两个常量里,**换默认只改这两行**
//! (用户 2026-11-01 拍板采用候选 **④ 天蓝**;③ 浅冷天蓝 `#87CEEB` 留作备选,值仍 pin 在 //! (用户 2026-11-01 拍板采用候选 **④ 天蓝**;③ 浅冷天蓝 `#87CEEB` 留作备选,值仍 pin 在
//! `tests/skyseed_candidates.rs`): //! `tests/skyseed_candidates.rs`):
+11 -11
View File
@@ -9,12 +9,12 @@
//! 注意 primary 的 36 与种子自身的彩度无关——这是官方口径:Tonal Spot 是「镇静」变体, //! 注意 primary 的 36 与种子自身的彩度无关——这是官方口径:Tonal Spot 是「镇静」变体,
//! 再鲜艳的种子也会被收到 36,再灰的种子也会被提到 36。 //! 再鲜艳的种子也会被收到 36,再灰的种子也会被提到 36。
//! //!
//! ## 两个入口:官方口径 vs 产品默认(M70) //! ## 两个入口:官方口径 vs 取色链锚点(M70 / M228 口径更新)
//! //!
//! | 入口 | 种子 | 中性彩度 | 用途 | //! | 入口 | 种子 | 中性彩度 | 用途 |
//! |---|---|---|---| //! |---|---|---|---|
//! | [`Palettes::official_baseline`] / [`super::Scheme::official_baseline`] | [`BASELINE_SEED`](`#6750A4`) | 6(官方) | 与官方 Java 实现逐角色对齐的**锚点**,`tests/official_baseline.rs` 用它 | //! | [`Palettes::official_baseline`] / [`super::Scheme::official_baseline`] | [`BASELINE_SEED`](`#6750A4`) | 6(官方) | 与官方 Java 实现逐角色对齐的**锚点**,`tests/official_baseline.rs` 用它 |
//! | [`Palettes::baseline`] / [`super::Scheme::baseline`] | [`DEFAULT_SEED`] | [`DEFAULT_NEUTRAL_CHROMA`] | **产品出厂默认**(消费方拿到的就是它) | //! | [`Palettes::baseline`] / [`super::Scheme::baseline`] | [`DEFAULT_SEED`] | [`DEFAULT_NEUTRAL_CHROMA`] | **D38 取色链的中性锚点**(`from_accent` 的表面色基准)。M228 起**出厂外观**的入口是 `DesignTokens::brand`(Ink / Silver 固化板),不是这里 |
//! //!
//! 换默认色只改 [`DEFAULT_SEED`] 与 [`DEFAULT_NEUTRAL_CHROMA`] 两行常量(中性彩度不动就只改一行), //! 换默认色只改 [`DEFAULT_SEED`] 与 [`DEFAULT_NEUTRAL_CHROMA`] 两行常量(中性彩度不动就只改一行),
//! 然后重跑 `tests/skyseed_candidates.rs` 的 pin(它会打印新值并指出哪几行要同步)。 //! 然后重跑 `tests/skyseed_candidates.rs` 的 pin(它会打印新值并指出哪几行要同步)。
@@ -26,12 +26,12 @@
use super::tonal::TonalPalette; use super::tonal::TonalPalette;
use super::Color; use super::Color;
/// M3 baseline 种子色(与 material-color-utilities 的默认基线同值)。 /// M3 baseline 种子色(与 material-color-utilities 的默认基线同值);**不是**产品色——
/// /// 产品外观见 `DesignTokens::brand`,取色链锚点见 [`DEFAULT_SEED`]。
/// **不是**产品默认色——产品默认见 [`DEFAULT_SEED`]。
pub const BASELINE_SEED: Color = Color::rgb(0x67, 0x50, 0xA4); pub const BASELINE_SEED: Color = Color::rgb(0x67, 0x50, 0xA4);
/// 产品出厂默认种子色 —— M70 候选④「天蓝」,**用户 2026-11-01 拍板采用**。 /// 取色链的锚点种子色 —— M70 候选④「天蓝」,**用户 2026-11-01 拍板采用**;M228 起产品出厂
/// 外观走 `DesignTokens::brand`(Ink / Silver 固化板),本常量只影响 D38 取色链。
/// ///
/// `#3C8DBC`(CAM16 hue ≈ 239.5°,主色 `#236488`)。注意种子的彩度(42.6)会被 Tonal Spot /// `#3C8DBC`(CAM16 hue ≈ 239.5°,主色 `#236488`)。注意种子的彩度(42.6)会被 Tonal Spot
/// 丢掉——主色板的彩度恒为 36,种子只贡献**色相**。 /// 丢掉——主色板的彩度恒为 36,种子只贡献**色相**。
@@ -41,12 +41,11 @@ pub const BASELINE_SEED: Color = Color::rgb(0x67, 0x50, 0xA4);
/// 用户定了三级优先的配色来源:① 启动器背景图取色(Monet)→ ② 桌面壁纸取色 → ③ 莫奈关闭 /// 用户定了三级优先的配色来源:① 启动器背景图取色(Monet)→ ② 桌面壁纸取色 → ③ 莫奈关闭
/// (默认)时用户从内置多套色卡里选。本常量 + [`DEFAULT_NEUTRAL_CHROMA`] 只定义**第三级里 /// (默认)时用户从内置多套色卡里选。本常量 + [`DEFAULT_NEUTRAL_CHROMA`] 只定义**第三级里
/// 出厂预选的那一套**(首次启动、用户没选过色卡时的那一套;`palette::DEFAULT_CARD` 的种子即本值)。 /// 出厂预选的那一套**(首次启动、用户没选过色卡时的那一套;`palette::DEFAULT_CARD` 的种子即本值)。
///
/// 取色(量化 + 挑主色)、内置色卡库与三级回落都在 [`crate::palette`](M87,分层见 D40); /// 取色(量化 + 挑主色)、内置色卡库与三级回落都在 [`crate::palette`](M87,分层见 D40);
/// 本模块只负责「给定种子 + 中性彩度,派生出 49 个角色」。 /// 本模块只负责「给定种子 + 中性彩度,派生出 49 个角色」。
pub const DEFAULT_SEED: Color = Color::rgb(0x3C, 0x8D, 0xBC); pub const DEFAULT_SEED: Color = Color::rgb(0x3C, 0x8D, 0xBC);
/// 产品出厂默认的中性调色板彩度(官方 Tonal Spot 是 6,这里 12 = **有意偏离**)。 /// 取色链的中性调色板彩度(官方 Tonal Spot 是 6,这里 12 = **有意偏离**)。
/// ///
/// 官方 6 让表面几乎无色;12 让 surface / surface-container-* 带一层淡蓝调, /// 官方 6 让表面几乎无色;12 让 surface / surface-container-* 带一层淡蓝调,
/// 这是「浅冷天蓝」唯一能落在表面上的手段(primary 的 tone 被官方钉在 40, /// 这是「浅冷天蓝」唯一能落在表面上的手段(primary 的 tone 被官方钉在 40,
@@ -143,10 +142,11 @@ pub struct Palettes {
} }
impl Palettes { impl Palettes {
/// **产品出厂默认**:种子 [`DEFAULT_SEED`] + 中性彩度 [`DEFAULT_NEUTRAL_CHROMA`]。 /// **D38 取色链的中性锚点**:种子 [`DEFAULT_SEED`] + 中性彩度 [`DEFAULT_NEUTRAL_CHROMA`]。
/// ///
/// 消费方(`DesignTokens::baseline`、iced 适配层)拿到的就是这套,所以产品默认色 /// 用途只剩一条:给「莫奈关闭时的机内色卡」与 [`super::Scheme::from_accent`] 提供中性表面色
/// 只需要改那两个常量。 /// 基准。**M228 起产品出厂外观走 `DesignTokens::brand`**([`super::boards::BrandBoard`] 的
/// Ink / Silver 板),改这两个常量**不再改变用户看到的默认外观**,只影响取色链。
pub fn baseline() -> Self { pub fn baseline() -> Self {
Self::from_seed_with_neutral_chroma(DEFAULT_SEED, DEFAULT_NEUTRAL_CHROMA) Self::from_seed_with_neutral_chroma(DEFAULT_SEED, DEFAULT_NEUTRAL_CHROMA)
} }
+5 -1
View File
@@ -179,8 +179,12 @@ pub struct Scheme {
} }
impl Scheme { impl Scheme {
/// **产品出厂默认方案**(种子 [`super::palettes::DEFAULT_SEED`] + 中性彩度 /// **D38 取色链的中性锚点**(种子 [`super::palettes::DEFAULT_SEED`] + 中性彩度
/// [`super::palettes::DEFAULT_NEUTRAL_CHROMA`],见 [`Palettes::baseline`])。 /// [`super::palettes::DEFAULT_NEUTRAL_CHROMA`],见 [`Palettes::baseline`])。
///
/// M228 起**产品出厂外观**走 [`Scheme::from_board`](Ink / Silver 固化色板);本入口保留给
/// 取色链与 [`Scheme::from_accent`] 的表面色基准,两者并列而不是互相替换(理由见
/// [`super::boards`] 的模块文档)。
pub fn baseline(flavor: SchemeFlavor) -> Self { pub fn baseline(flavor: SchemeFlavor) -> Self {
Self::from_palettes(&Palettes::baseline(), flavor) Self::from_palettes(&Palettes::baseline(), flavor)
} }
+5 -1
View File
@@ -138,7 +138,11 @@ pub const SWITCH_TRACK: SpecBox = SpecBox {
}; };
/// 开关拇指直径(px)。 /// 开关拇指直径(px)。
pub const SWITCH_THUMB: f32 = 24.0; pub const SWITCH_THUMB: f32 = 24.0;
/// 拇指相对轨道的内缩(px):拇指 `x=88` − 轨道 `x=64` − 描边 …= 实测 0 起点 24 偏移、右端留 4。 /// 拇指相对轨道的内缩(px):规格卡轨道 `y=1060..1092`、拇指 `y=1064..1088`,上下各 **4**;
/// 右端 `116−112` 同为 4。
///
/// 位移行程(拇指可从左滑到右的距离)= 轨道宽 52 − 拇指 24 − 2×内缩 4 = **20**(规格卡没写行程,
/// 这是由前三项推出来的唯一值;写死 20 就是第四份副本)。
pub const SWITCH_THUMB_INSET: f32 = 4.0; pub const SWITCH_THUMB_INSET: f32 = 4.0;
/// 关态拇指直径(px):规格卡 `16×16 rx8`。 /// 关态拇指直径(px):规格卡 `16×16 rx8`。
pub const SWITCH_THUMB_OFF: f32 = 16.0; pub const SWITCH_THUMB_OFF: f32 = 16.0;
+14 -3
View File
@@ -219,10 +219,18 @@ impl FabSize {
} }
} }
/// 起始内距(px;只有 Extended 有:图标左边 = 16)。 /// 起始内距(px;只有 Extended 有):**28**,规格卡画的就这个数。
///
/// 推导(可逐位核):框 `x=292` 起、宽 160;`+` 是 `font-size=24`、`text-anchor=middle`、
/// 中心 `x=332` ⇒ 图标盒 = 320..344 ⇒ 内距 = 320 − 292 = **28**;标签 `x=352` 起,
/// 于是图标盒右沿 344 到标签 352 = [`FabSize::gap_px`] 的 8。
///
/// ⚠️ 与官方口径的偏离已登记:M3 ExtendedFAB 的起始内距是 **16**(图标盒 292+16=308)。
/// 规格卡画得更宽松,本表按**规格卡**取值(A 级依据 = 图中坐标,不是猜的);M229 若要用
/// M3 的 16,得同时把图标中心从 332 挪回 316 —— 那是一处视觉改动,要出截图,别悄悄改数。
pub const fn pad_start_px(self) -> f32 { pub const fn pad_start_px(self) -> f32 {
match self { match self {
FabSize::Extended => 16.0, FabSize::Extended => 28.0,
_ => 0.0, _ => 0.0,
} }
} }
@@ -290,7 +298,10 @@ mod tests {
assert_eq!(FabSize::Extended.width_px(), 160.0); assert_eq!(FabSize::Extended.width_px(), 160.0);
assert_eq!(FabSize::Extended.height_px(), 56.0); assert_eq!(FabSize::Extended.height_px(), 56.0);
assert_eq!(FabSize::Extended.label_px(), 15.0); assert_eq!(FabSize::Extended.label_px(), 15.0);
assert_eq!(FabSize::Extended.pad_start_px(), 16.0); // 内距 28 = 规格卡图中「框 292 → 图标盒 320」的实测距离(不是 M3 的 16)。
assert_eq!(FabSize::Extended.pad_start_px(), 28.0);
// 图标中心落点必须自洽:292 + 28 + 24/2 = 332,与规格卡 `x=332` 对得上。
assert_eq!(FabSize::Extended.pad_start_px() + 24.0 / 2.0 + 292.0, 332.0);
assert_eq!(FabSize::Extended.gap_px(), 8.0); assert_eq!(FabSize::Extended.gap_px(), 8.0);
// 图标字号随档递增(扩展态回到 24,因为它跟标签同一行)。 // 图标字号随档递增(扩展态回到 24,因为它跟标签同一行)。
assert!(FabSize::Small.icon_px() < FabSize::Default.icon_px()); assert!(FabSize::Small.icon_px() < FabSize::Default.icon_px());
+5 -1
View File
@@ -1,9 +1,13 @@
//! 规格卡字号 → 官方 type scale 的**校核**(不是又一张字号表)。 //! 规格卡字号 → 官方 type scale 的**校核**(不是又一张字号表)。
//! //!
//! 规格卡上每一处文字都写了 `font-size`:40 / 32 / 24 / 20 / 16 / 15 / 14 / 13 / 12 / 11 / 10 / 9。 //! 规格卡上每一处**文字**都写了 `font-size`,去重共 11 档:
//! 40 / 32 / 24 / 16 / 15 / 14 / 13 / 12 / 11 / 10 / 9。
//! 其中一部分**命中** M3 的 15 档(14 / 16 / 24 / 32 / 12 / 11),一部分**落在两档之间** //! 其中一部分**命中** M3 的 15 档(14 / 16 / 24 / 32 / 12 / 11),一部分**落在两档之间**
//! (13 / 15 / 40),还有一部分**低于本项目允许的 UI 下限**(10 / 9)。 //! (13 / 15 / 40),还有一部分**低于本项目允许的 UI 下限**(10 / 9)。
//! //!
//! 规格卡上另有 20 / 26 / 34 三个字号,但它们是 **FAB 的图形符号(`+` 字形)**,不是文字——
//! 归 [`crate::inkcat::size::FabSize::icon_px`] 管,本模块不收录(`SpecText` 里没有 20 这一档)。
//!
//! 本模块的职责就是把「规格卡字级」翻译成这三类结论,让换肤时不用逐个拿卷尺量: //! 本模块的职责就是把「规格卡字级」翻译成这三类结论,让换肤时不用逐个拿卷尺量:
//! //!
//! | 结论 | 含义 | 处置 | //! | 结论 | 含义 | 处置 |
+8 -6
View File
@@ -14,25 +14,27 @@
//! `surface` 上时必须数得出来。这是唯一一条**数字门禁**,[`SurfaceHierarchy::state_failures`] //! `surface` 上时必须数得出来。这是唯一一条**数字门禁**,[`SurfaceHierarchy::state_failures`]
//! 守它。 //! 守它。
//! 3. **容器层之间的亮度差不设门禁,改为「组合指示」要求**:M3 的容器层天生贴在一起 //! 3. **容器层之间的亮度差不设门禁,改为「组合指示」要求**:M3 的容器层天生贴在一起
//! (官方口径浅色 `secondary-container` 对 `surface-container-high` 只有 1.05:1, //! (官方口径浅色 `secondary-container` 对 `surface-container-high` 只有 1.06:1,
//! 深色也只有 1.54:1 —— 这是配色结构的必然,不是哪套板的失误)。所以 //! 本项目 Ink 板最低也只有 1.54:1 —— 这是配色结构的必然,不是哪套板的失误)。所以
//! **禁止**把「容器底色差异」当作唯一的选中 / 层级指示:选中态必须是 //! **禁止**把「容器底色差异」当作唯一的选中 / 层级指示:选中态必须是
//! 「容器底(过第 2 条)+ 文字对比(第 1 条)+ 形状 / 图标 / 描边指示」三件套。 //! 「容器底(过第 2 条)+ 文字对比(第 1 条)+ 形状 / 图标 / 描边指示」三件套。
//! 另一半同理:`surface-container-*` 只表达「几乎看不见的抬升」, //! 另一半同理:`surface-container-*` 只表达「几乎看不见的抬升」,
//! 拿它当选中态用连第 2 条都过不了。 //! 拿它当选中态用连第 2 条都过不了。
//! //!
//! 第 3 条为什么不是「把阈值降到 1.05 就完事」:那样等于承认「选中态可以看不见」, //! 第 3 条为什么不是「把阈值降到 1.02 就完事」:那样等于承认「选中态可以看不见」,
//! 而用户抱怨的正是这个。把不可测的那一半转成**构成要求**,才是能落地的那一半。 //! 而用户抱怨的正是这个。把不可测的那一半转成**构成要求**,才是能落地的那一半。
//! //!
//! ## 实测(`tests/ink_cat_boards.rs` 守住第 2 条与阶梯) //! ## 实测(`tests/ink_cat_boards.rs` 守住第 2 条与阶梯)
//! //!
//! | 板 | 最弱状态容器 / `surface` | 状态容器 / 最亮(深)或最暗(浅)的容器层 | 阶梯 L\\* 步长 | //! | 板 | 最弱状态容器 / `surface` | 状态容器 / 容器层(最低 ~ 最高) | 阶梯 L\\* 步长 |
//! |---|---|---|---| //! |---|---|---|---|
//! | Ink | **1.97**(secondary-container) | 1.54 ~ 12.13 | +1.4 ~ +4.4 | //! | Ink | **1.97**(secondary-container) | 1.54 ~ 12.13 | +1.4 ~ +4.4 |
//! | Silver | **1.23**(tertiary/error-container) | 1.05 ~ 1.24 | −1.8 ~ −6.1 | //! | Silver | **1.23**(tertiary/error-container) | **1.02** ~ 1.80 | −1.7 ~ −4.1 |
//! | 旧 baseline(债务现场) | 1.23 | **1.057**(对卡片,m223 实测) | — | //! | 旧 baseline(债务现场) | 1.23 | **1.057**(对卡片,m223 实测) | — |
//! //!
//! 第三列就是第 3 条的由来:它过不了任何有意义的门禁,所以不设门禁、只设组合要求。 //! 第三列就是第 3 条的由来:它过不了任何有意义的门禁(Silver 最差的一组是
//! `tertiary-container` 压 `surface-container-high`,1.016 —— 两者几乎同亮度),
//! 所以不设门禁、只设组合要求。
//! //!
//! ## 阶梯本身还是要查([`SurfaceHierarchy::layer_violations`]) //! ## 阶梯本身还是要查([`SurfaceHierarchy::layer_violations`])
//! //!
+6 -90
View File
@@ -1,14 +1,12 @@
//! Ink Cat 双主题的**消费方视角**门禁(M228):色板完整性、双主题切换、对比度、容器层可视性。 //! Ink Cat 双主题的**消费方视角**门禁(M228):色板完整性、双主题切换、对比度。
//! //!
//! 只用公开 API(`unlucky_design::...`),所以它同时是「iced 适配层能拿到什么」的样本: //! 只用公开 API(`unlucky_design::...`),所以它同时是「iced 适配层能拿到什么」的样本:
//! 换肤时只需要 `BrandBoard` → `Scheme` → `Role`,一个色值字面量都不用抄。 //! 换肤时只需要 `BrandBoard` → `Scheme` → `Role`,一个色值字面量都不用抄。
//! 规格卡的几何与字级校核在同目录的 `ink_cat_geometry.rs`(单文件 ≤400 行)。 //! 几何与字级校核在 `ink_cat_geometry.rs`,容器层可视性在 `ink_cat_visibility.rs`
//! (拆开是单文件 ≤400 行的硬约束)。
use unlucky_design::color::BrandBoard::{Ink, Silver}; use unlucky_design::color::BrandBoard::{Ink, Silver};
use unlucky_design::color::{BrandBoard, Color, Role, SchemeFlavor}; use unlucky_design::color::{BrandBoard, Color, Role, SchemeFlavor};
use unlucky_design::palette::visibility::{
check_surface_hierarchy, LAYER_ROLES, STATE_CONTAINERS, STATE_CONTAINER_MIN,
};
use unlucky_design::palette::wcag::{check, AA_TEXT}; use unlucky_design::palette::wcag::{check, AA_TEXT};
use unlucky_design::DesignTokens; use unlucky_design::DesignTokens;
@@ -236,19 +234,19 @@ const SPEC_TEXT_ON_SURFACE: [(BrandBoard, Role, Role, &str); 25] = [
( (
Silver, Silver,
Role::OnSurfaceVariant, Role::OnSurfaceVariant,
Role::SurfaceContainerHigh, Role::SurfaceContainerHighest,
"字段标签", "字段标签",
), ),
( (
Silver, Silver,
Role::OnSurface, Role::OnSurface,
Role::SurfaceContainerHigh, Role::SurfaceContainerHighest,
"字段内容", "字段内容",
), ),
( (
Silver, Silver,
Role::Error, Role::Error,
Role::SurfaceContainerHigh, Role::SurfaceContainerHighest,
"错误字段标签", "错误字段标签",
), ),
( (
@@ -285,85 +283,3 @@ fn spec_text_pairs_meet_aa() {
failures.join("\n") failures.join("\n")
); );
} }
#[test]
fn state_containers_clear_the_visibility_floor() {
// 债务台账「SecondaryContainer 对 Surface 仅 1.06」的令牌层裁决:状态容器 ≥1.20:1。
for board in BrandBoard::ALL {
let report = check_surface_hierarchy(&board.scheme());
assert!(
report.state_failures().is_empty(),
"{} 的状态容器未达 {STATE_CONTAINER_MIN}:1:{:?}",
board.name(),
report.state_failures()
);
let (role, ratio) = report.worst_state();
assert!(
ratio >= STATE_CONTAINER_MIN,
"{} 最弱的状态容器 {} 只有 {ratio:.3}",
board.name(),
role.name()
);
// 旧 baseline 的现场值 1.057 必须被显著甩开,否则「修了但没修好」。
assert!(
ratio > 1.20,
"{} 只是刚好卡线({ratio:.3}),等于没修",
board.name()
);
}
// 两套板覆盖 4 个状态容器角色,一个不落。
assert_eq!(STATE_CONTAINERS.len(), 4);
}
#[test]
fn surface_container_ladder_is_monotone_with_visible_steps() {
for board in BrandBoard::ALL {
let report = check_surface_hierarchy(&board.scheme());
assert!(
report.layer_violations().is_empty(),
"{} 的容器阶梯有断层或方向错:{:?}",
board.name(),
report.layer_violations()
);
assert!(report.is_clean(), "{} 的容器层体检没过", board.name());
// 方向:深色板越靠上越亮、浅色板越靠上越暗。
let steps = report.layer_steps();
for step in steps {
let expect_positive = board.flavor() == SchemeFlavor::Dark;
assert_eq!(
step.delta_lstar > 0.0,
expect_positive,
"{} 的 {} 阶梯方向错",
board.name(),
step.role.name()
);
}
assert_eq!(LAYER_ROLES.len(), 5);
}
}
// ---------------------------------------------------------------- 规则:容器层不当状态用
#[test]
fn container_layers_are_not_state_indicators() {
// 裁决第 3 条的可测化一半:把「状态容器压在最亮容器层上」的实际数字钉住,
// 提醒后来者别把它当唯一指示(浅色板这里必然贴到 1.05~1.24)。
let silver = BrandBoard::Silver.scheme();
let worst = Role::ALL
.iter()
.filter(|role| STATE_CONTAINERS.contains(role))
.map(|role| silver.contrast(*role, Role::SurfaceContainerHigh))
.fold(f32::INFINITY, f32::min);
assert!(
(1.0..1.5).contains(&worst),
"Silver 的状态容器对 surface-container-high = {worst:.3},与「浅色必然贴近」的登记不符"
);
// Ink 相反:深色板的状态容器与容器层拉得开,所以深色可以靠底色差做弱指示。
let ink = BrandBoard::Ink.scheme();
let ink_worst = Role::ALL
.iter()
.filter(|role| STATE_CONTAINERS.contains(role))
.map(|role| ink.contrast(*role, Role::SurfaceContainerHighest))
.fold(f32::INFINITY, f32::min);
assert!(ink_worst >= 1.5, "Ink 最弱的状态容器 = {ink_worst:.3}");
}
+133
View File
@@ -0,0 +1,133 @@
//! Ink Cat 容器层可视性门禁(M228 债务裁决的可测一半):状态容器 1.20 门禁、阶梯单调、
//! 以及「容器层不能当状态指示」的实测钉子。
//!
//! 与 `ink_cat_boards.rs` / `ink_cat_geometry.rs` 分开是单文件 ≤400 行的硬约束。
use unlucky_design::color::{BrandBoard, Color, Role, SchemeFlavor};
use unlucky_design::palette::visibility::{
check_surface_hierarchy, LAYER_ROLES, STATE_CONTAINERS, STATE_CONTAINER_MIN,
};
use unlucky_design::palette::wcag::AA_TEXT;
#[test]
fn state_containers_clear_the_visibility_floor() {
// 债务台账「SecondaryContainer 对 Surface 仅 1.06」的令牌层裁决:状态容器 ≥1.20:1。
for board in BrandBoard::ALL {
let report = check_surface_hierarchy(&board.scheme());
assert!(
report.state_failures().is_empty(),
"{} 的状态容器未达 {STATE_CONTAINER_MIN}:1:{:?}",
board.name(),
report.state_failures()
);
let (role, ratio) = report.worst_state();
assert!(
ratio >= STATE_CONTAINER_MIN,
"{} 最弱的状态容器 {} 只有 {ratio:.3}",
board.name(),
role.name()
);
// 旧 baseline 的现场值 1.057 必须被显著甩开,否则「修了但没修好」。
assert!(
ratio > 1.20,
"{} 只是刚好卡线({ratio:.3}),等于没修",
board.name()
);
}
// 两套板覆盖 4 个状态容器角色,一个不落。
assert_eq!(STATE_CONTAINERS.len(), 4);
}
#[test]
fn surface_container_ladder_is_monotone_with_visible_steps() {
for board in BrandBoard::ALL {
let report = check_surface_hierarchy(&board.scheme());
assert!(
report.layer_violations().is_empty(),
"{} 的容器阶梯有断层或方向错:{:?}",
board.name(),
report.layer_violations()
);
assert!(report.is_clean(), "{} 的容器层体检没过", board.name());
// 方向:深色板越靠上越亮、浅色板越靠上越暗。
let steps = report.layer_steps();
for step in steps {
let expect_positive = board.flavor() == SchemeFlavor::Dark;
assert_eq!(
step.delta_lstar > 0.0,
expect_positive,
"{} 的 {} 阶梯方向错",
board.name(),
step.role.name()
);
}
assert_eq!(LAYER_ROLES.len(), 5);
}
}
#[test]
fn container_layers_are_not_state_indicators() {
// 裁决第 3 条的可测化一半:把「状态容器压在容器层上」的最差值钉住,提醒后来者
// 别把它当唯一指示(浅色板必然贴到 1.02,深色板 1.54)。
let worst_over_layers = |board: BrandBoard| {
let scheme = board.scheme();
STATE_CONTAINERS
.iter()
.flat_map(|role| {
LAYER_ROLES
.iter()
.map(move |layer| (*role, *layer, scheme.contrast(*role, *layer)))
})
.map(|(_, _, ratio)| ratio)
.fold(f32::INFINITY, f32::min)
};
let silver = worst_over_layers(BrandBoard::Silver);
assert!(
(1.0..1.1).contains(&silver),
"Silver 的状态容器对容器层最低 = {silver:.3},与「浅色 pale-on-pale 必然贴近」的登记(1.02)不符"
);
// Ink 相反:深色板的状态容器与容器层拉得开,所以深色可以靠底色差做弱指示。
let ink = worst_over_layers(BrandBoard::Ink);
assert!(
ink >= 1.5,
"Ink 最弱的状态容器对容器层 = {ink:.3},应明显高于浅色板"
);
// 两条一起说明「为什么这不是调色问题而是构成问题」:同一套算法下深色能做的弱指示,
// 浅色做不到——所以门禁不能按容器层设,只能按 surface 设。
assert!(silver < ink);
}
#[test]
fn field_backdrop_is_the_same_role_on_both_boards() {
// P2-1 的回归钉子:字段底在两套板都是 `surface-container-highest`。
// 若把 Silver 的字段底挪回 `surface-container-high`(#E4E4E5),错误标签会从 4.69 掉到 4.38(AA 失守)。
for board in BrandBoard::ALL {
let scheme = board.scheme();
for (fg, usage) in [
(Role::OnSurfaceVariant, "字段标签"),
(Role::OnSurface, "字段内容"),
(Role::Error, "错误字段标签"),
] {
let ratio = scheme.contrast(fg, Role::SurfaceContainerHighest);
assert!(
ratio >= AA_TEXT,
"{} 的{usage}({} on surface-container-highest)= {ratio:.3}",
board.name(),
fg.name()
);
}
}
// 浅色板的 high 必须比 highest 更亮、且是另一个色(阶梯方向 + 不许两档同值)。
let silver = BrandBoard::Silver.scheme();
let high = silver.get(Role::SurfaceContainerHigh);
let highest = silver.get(Role::SurfaceContainerHighest);
assert_ne!(high, highest);
assert!(
high.lstar() > highest.lstar(),
"Silver 的 surface-container-high 应比 highest 更亮(高 {} vs 最高 {})",
high.lstar(),
highest.lstar()
);
// 规格卡给的字段底就是 highest 那个值。
assert_eq!(highest, Color::from_hex("#DCDDDE").expect("规格卡字段底"));
}