release: v0.2.4 里程碑快照(孤儿提交,无父;树 = dev 68362e5)
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
This commit is contained in:
commit
a5d1dec495
1580 files changed
+320938
No files matched your search
@@ -0,0 +1,285 @@
|
||||
//! Ink Cat 双主题的**消费方视角**门禁(M228):色板完整性、双主题切换、对比度。
|
||||
//!
|
||||
//! 只用公开 API(`unlucky_design::...`),所以它同时是「iced 适配层能拿到什么」的样本:
|
||||
//! 换肤时只需要 `BrandBoard` → `Scheme` → `Role`,一个色值字面量都不用抄。
|
||||
//! 几何与字级校核在 `ink_cat_geometry.rs`,容器层可视性在 `ink_cat_visibility.rs`
|
||||
//! (拆开是单文件 ≤400 行的硬约束)。
|
||||
|
||||
use unlucky_design::color::BrandBoard::{Ink, Silver};
|
||||
use unlucky_design::color::{BrandBoard, Color, Role, SchemeFlavor};
|
||||
use unlucky_design::palette::wcag::{check, AA_TEXT};
|
||||
use unlucky_design::DesignTokens;
|
||||
|
||||
// ---------------------------------------------------------------- 色板完整性
|
||||
|
||||
#[test]
|
||||
fn every_board_covers_all_49_roles_and_reports_its_flavor() {
|
||||
for board in BrandBoard::ALL {
|
||||
let scheme = board.scheme();
|
||||
assert_eq!(scheme.flavor(), board.flavor());
|
||||
assert_eq!(scheme.iter().count(), Role::ALL.len());
|
||||
for role in Role::ALL {
|
||||
let color = scheme.get(role);
|
||||
assert!(
|
||||
color.is_opaque(),
|
||||
"{} 的 {} 不是不透明色",
|
||||
board.name(),
|
||||
role.name()
|
||||
);
|
||||
assert_eq!(color, board.get(role), "两个入口必须同源");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_anchors_survive_the_public_api() {
|
||||
// 规格卡上写死的色值(两套板各自的一批),一份都不许漂。
|
||||
let cases: [(BrandBoard, Role, &str); 12] = [
|
||||
(BrandBoard::Ink, Role::Primary, "#E8FFEF"),
|
||||
(BrandBoard::Ink, Role::OnPrimary, "#256B4B"),
|
||||
(BrandBoard::Ink, Role::Surface, "#0F150F"),
|
||||
(BrandBoard::Ink, Role::SurfaceContainerHighest, "#262B25"),
|
||||
(BrandBoard::Ink, Role::Error, "#FF716C"),
|
||||
(BrandBoard::Ink, Role::Outline, "#7B8A7B"),
|
||||
(BrandBoard::Silver, Role::Primary, "#006499"),
|
||||
(BrandBoard::Silver, Role::PrimaryContainer, "#87C7FF"),
|
||||
(BrandBoard::Silver, Role::Surface, "#F9F9FA"),
|
||||
(BrandBoard::Silver, Role::SecondaryContainer, "#D4E4F6"),
|
||||
(BrandBoard::Silver, Role::OnSurface, "#1A1C1D"),
|
||||
(BrandBoard::Silver, Role::Error, "#AC3434"),
|
||||
];
|
||||
for (board, role, hex) in cases {
|
||||
assert_eq!(
|
||||
board.scheme().get(role),
|
||||
Color::from_hex(hex).expect("规格卡色值"),
|
||||
"{} / {} 与规格卡的 {hex} 不符",
|
||||
board.name(),
|
||||
role.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 双主题切换
|
||||
|
||||
#[test]
|
||||
fn follow_system_resolves_to_the_right_board() {
|
||||
// 「跟随系统」是设置层的第三种态,令牌层只提供映射。
|
||||
assert_eq!(
|
||||
BrandBoard::for_flavor(SchemeFlavor::Dark),
|
||||
BrandBoard::Ink,
|
||||
"系统深色 → Ink"
|
||||
);
|
||||
assert_eq!(
|
||||
BrandBoard::for_flavor(SchemeFlavor::Light),
|
||||
BrandBoard::Silver,
|
||||
"系统浅色 → Silver"
|
||||
);
|
||||
for board in BrandBoard::ALL {
|
||||
assert_eq!(
|
||||
BrandBoard::for_flavor(board.flavor()),
|
||||
board,
|
||||
"映射必须可逆"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_boards_swaps_all_the_surface_roles() {
|
||||
let ink = BrandBoard::Ink.scheme();
|
||||
let silver = BrandBoard::Silver.scheme();
|
||||
// 窗口底 / 正文 / 字段底 / 分层容器:切板后全都得换。
|
||||
for role in [
|
||||
Role::Surface,
|
||||
Role::OnSurface,
|
||||
Role::SurfaceContainerLow,
|
||||
Role::SurfaceContainerHighest,
|
||||
Role::Primary,
|
||||
Role::SecondaryContainer,
|
||||
Role::Error,
|
||||
Role::InverseSurface,
|
||||
] {
|
||||
assert_ne!(
|
||||
ink.get(role),
|
||||
silver.get(role),
|
||||
"{} 在两套板之间没变——换肤会露出旧颜色",
|
||||
role.name()
|
||||
);
|
||||
}
|
||||
// 深浅判定:Ink 是真的深、Silver 是真的浅。
|
||||
assert!(ink.get(Role::Surface).relative_luminance() < 0.02);
|
||||
assert!(silver.get(Role::Surface).relative_luminance() > 0.90);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_tokens_brand_entry_is_the_product_default() {
|
||||
let ink = DesignTokens::brand(BrandBoard::Ink);
|
||||
assert_eq!(ink.flavor, SchemeFlavor::Dark);
|
||||
assert_eq!(ink.color(Role::Surface), BrandBoard::Ink.get(Role::Surface));
|
||||
// 品牌板与「取色链」两条路径互不干扰:baseline 仍是 tone 派生(D38 的锚点)。
|
||||
let baseline = DesignTokens::baseline(SchemeFlavor::Dark);
|
||||
assert_eq!(baseline.flavor, SchemeFlavor::Dark);
|
||||
assert_ne!(
|
||||
baseline.color(Role::Surface),
|
||||
ink.color(Role::Surface),
|
||||
"baseline 若已变成品牌板,D38 取色链的表面色锚点就没了"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 对比度
|
||||
|
||||
#[test]
|
||||
fn both_boards_pass_the_full_wcag_table() {
|
||||
for board in BrandBoard::ALL {
|
||||
let report = check(&board.scheme());
|
||||
assert!(
|
||||
report.is_clean(),
|
||||
"{} 有 {} 项未达 AA:{:?}",
|
||||
board.name(),
|
||||
report.failures.len(),
|
||||
report.failures
|
||||
);
|
||||
assert!(
|
||||
report.worst >= AA_TEXT,
|
||||
"{} 最紧的一对只有 {:.2}",
|
||||
board.name(),
|
||||
report.worst
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 规格卡里真实出现的「文字压在什么色上」组合(25 组,含 `on-*-container` 全族)。
|
||||
///
|
||||
/// 只列规格卡上画出来的用法——不列「有可能这么用」的组合,否则就是在替页面线编需求。
|
||||
const SPEC_TEXT_ON_SURFACE: [(BrandBoard, Role, Role, &str); 25] = [
|
||||
(Ink, Role::OnPrimary, Role::Primary, "Filled 按钮"),
|
||||
(
|
||||
Ink,
|
||||
Role::OnSecondaryContainer,
|
||||
Role::SecondaryContainer,
|
||||
"Tonal 按钮",
|
||||
),
|
||||
(Ink, Role::OnSurface, Role::Surface, "Outlined/Text 按钮"),
|
||||
(Ink, Role::Primary, Role::Surface, "Text 按钮标签"),
|
||||
(
|
||||
Ink,
|
||||
Role::Primary,
|
||||
Role::SurfaceContainerLowest,
|
||||
"Elevated 标签",
|
||||
),
|
||||
(
|
||||
Ink,
|
||||
Role::OnPrimaryContainer,
|
||||
Role::PrimaryContainer,
|
||||
"FAB 图标",
|
||||
),
|
||||
(
|
||||
Ink,
|
||||
Role::OnSurfaceVariant,
|
||||
Role::SurfaceContainerHighest,
|
||||
"字段标签",
|
||||
),
|
||||
(
|
||||
Ink,
|
||||
Role::OnSurface,
|
||||
Role::SurfaceContainerHighest,
|
||||
"字段内容",
|
||||
),
|
||||
(
|
||||
Ink,
|
||||
Role::Error,
|
||||
Role::SurfaceContainerHighest,
|
||||
"错误字段标签",
|
||||
),
|
||||
(Ink, Role::Error, Role::Surface, "错误提示"),
|
||||
(Ink, Role::OnError, Role::Error, "徽标数字"),
|
||||
(
|
||||
Ink,
|
||||
Role::OnSurfaceVariant,
|
||||
Role::Surface,
|
||||
"chip 未选 / 次要图标",
|
||||
),
|
||||
(
|
||||
Ink,
|
||||
Role::InverseOnSurface,
|
||||
Role::InverseSurface,
|
||||
"snackbar 文字",
|
||||
),
|
||||
(
|
||||
Ink,
|
||||
Role::InversePrimary,
|
||||
Role::InverseSurface,
|
||||
"snackbar 动作",
|
||||
),
|
||||
(
|
||||
Ink,
|
||||
Role::OnSurfaceVariant,
|
||||
Role::SurfaceContainer,
|
||||
"卡片副文案",
|
||||
),
|
||||
(
|
||||
Ink,
|
||||
Role::OnSurfaceVariant,
|
||||
Role::SurfaceContainerHigh,
|
||||
"对话框正文",
|
||||
),
|
||||
(Silver, Role::OnPrimary, Role::Primary, "Filled 按钮"),
|
||||
(
|
||||
Silver,
|
||||
Role::OnSecondaryContainer,
|
||||
Role::SecondaryContainer,
|
||||
"Tonal 按钮",
|
||||
),
|
||||
(Silver, Role::OnSurface, Role::Surface, "Outlined 按钮"),
|
||||
(Silver, Role::Primary, Role::Surface, "Text 按钮标签"),
|
||||
(
|
||||
Silver,
|
||||
Role::OnSurfaceVariant,
|
||||
Role::SurfaceContainerHighest,
|
||||
"字段标签",
|
||||
),
|
||||
(
|
||||
Silver,
|
||||
Role::OnSurface,
|
||||
Role::SurfaceContainerHighest,
|
||||
"字段内容",
|
||||
),
|
||||
(
|
||||
Silver,
|
||||
Role::Error,
|
||||
Role::SurfaceContainerHighest,
|
||||
"错误字段标签",
|
||||
),
|
||||
(
|
||||
Silver,
|
||||
Role::OnSurfaceVariant,
|
||||
Role::SurfaceContainer,
|
||||
"卡片副文案",
|
||||
),
|
||||
(
|
||||
Silver,
|
||||
Role::InverseOnSurface,
|
||||
Role::InverseSurface,
|
||||
"snackbar 文字",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn spec_text_pairs_meet_aa() {
|
||||
let mut failures = Vec::new();
|
||||
for (board, fg, bg, usage) in SPEC_TEXT_ON_SURFACE {
|
||||
let ratio = board.scheme().contrast(fg, bg);
|
||||
if ratio < AA_TEXT {
|
||||
failures.push(format!(
|
||||
"{} / {usage}:{} on {} = {ratio:.2}",
|
||||
board.name(),
|
||||
fg.name(),
|
||||
bg.name()
|
||||
));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"规格卡用法未达 AA(4.5:1):\n{}",
|
||||
failures.join("\n")
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user