feat(app-iced): Ink Cat / Silver Cat 换肤 —— 双色板默认 + 规格卡组件几何(M229)

产品出厂外观从 tone 派生的 `Scheme::baseline` 切到品牌色板:深色 = Ink(淡绿)、
浅色 = Silver(钢蓝),D38 取色链(背景图 / 壁纸)仍然优先,取色失败回落到色板。

主题三档接进设置页「通用 → 主题」:Ink / Silver / 跟随系统(落盘仍是
`appearance.theme_mode` 的 dark / light / auto,不新增配置键)。

组件几何按规格卡对齐(M228 的 `inkcat` 登记项逐条落地):
- 卡片圆角 12 → 16;实例元信息卡改用新的 ELEVATED CARD(无描边、SurfaceContainer
  底、内距 24、上标注 11/600)
- Snackbar 48/4 → 52/16(双行 72)
- Checkbox 20/rx2 → 18/rx4;Radio 20 → 18 Ø(内点仍 10)
- 按钮加 XS~XL 五档(产品仍只用 S=40),内距改走 ControlSize::pad_x
- 新增 FAB 四态(40/56/80/160×56)与图标按钮第四式 outlined
- 语言与主题两行 chips 统一成规格卡 chip(32 高 / rx8)

`theme.rs` 原本正好 400 行,先把单测纯搬移到 `theme/tests.rs` 再加色板字段;
`app/tests.rs`、`settings/general.rs` 同样按纯搬移拆分守 400 行硬线。

验证:cargo fmt --all --check、cargo check -p unlucky-app-iced --all-targets、
cargo test --workspace(除 clippy 前的一轮 flake,见报告)、cargo clippy --workspace
--all-targets -- -D warnings、全仓 400 行扫描。视觉矩阵 42 张在
DevDocs/artifacts/m229/(gitignored,不进提交)。
This commit is contained in:
AskaEth committed 2026-11-11 01:49:13 +08:00
1 parent a06b7ff4ec
commit db541c820e
44 files changed
+1439 -300

No files matched your search

@@ -73,7 +73,7 @@ pub(super) fn section<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
let mut body = column![heading(
ui,
"feedback · progress / snackbar / dialog / empty state",
"12b §7/§12/§14/§15:Linear 厚 4 圆角 full · Circular 40/4 · Snackbar 48/68 上限宽 600 · \
"规格卡 + 12b §7/§12/§15:Linear 厚 4 圆角 full · Circular 40/4 · Snackbar 52/72 圆角 16 上限宽 600 · \
Dialog R28 pad24 宽 480 · EmptyState 项目自定[估](官方无此组件)"
)]
.spacing(ui.space(Space::X3));
@@ -270,7 +270,7 @@ fn snackbar_block<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
wide,
caption(
ui,
"Snackbar · 双行 68 + action InversePrimary + 关闭键(动作强制 hover 态)"
"Snackbar · 双行 72(52+20)+ action InversePrimary + 关闭键(动作强制 hover 态)"
),
two_lines,
caption(
+13 -3
View File
@@ -48,7 +48,7 @@
//! ULL_GALLERY_SECTION=selection # 只渲染 selection 段(本 mission 的取证与 probe 用它)
//! ```
//!
//! 取值:`all` / `buttons` / `divider` / `selection` / `inputs` / `feedback` / `navigation`。
//! 取值:`all` / `buttons` / `sizes` / `divider` / `selection` / `inputs` / `feedback` / `navigation`。
//! 语义是「**不构建**没被选中的段」(不是构建后隐藏):`column` 里根本不 push 它,
//! 于是该家族的 `view` 一行都不跑。未识别的值**回落 `all`** 并在 stderr 打一行提示
//! (静默回落会让人拿错图还以为拍的是那一段)。
@@ -87,6 +87,7 @@ mod probe;
#[cfg(test)]
mod scan;
mod selection;
mod sizes;
/// 段落过滤的环境变量名(语义见模块文档「段落过滤」)。
pub const SECTION_ENV: &str = "ULL_GALLERY_SECTION";
@@ -95,6 +96,7 @@ pub const SECTION_ENV: &str = "ULL_GALLERY_SECTION";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Sections {
buttons: bool,
sizes: bool,
divider: bool,
selection: bool,
inputs: bool,
@@ -106,6 +108,7 @@ impl Sections {
/// 五段全渲染(默认)。
const ALL: Self = Self {
buttons: true,
sizes: true,
divider: true,
selection: true,
inputs: true,
@@ -117,6 +120,7 @@ impl Sections {
fn only(name: &str) -> Option<Self> {
let mut sections = Self {
buttons: false,
sizes: false,
divider: false,
selection: false,
inputs: false,
@@ -125,6 +129,7 @@ impl Sections {
};
match name {
"buttons" => sections.buttons = true,
"sizes" => sections.sizes = true,
"divider" => sections.divider = true,
"selection" => sections.selection = true,
"inputs" => sections.inputs = true,
@@ -150,7 +155,7 @@ impl Sections {
return sections;
}
eprintln!(
"[gallery] {SECTION_ENV}={raw:?} 不是已知段(all|buttons|divider|selection|inputs|feedback|navigation),已回落 all"
"[gallery] {SECTION_ENV}={raw:?} 不是已知段(all|buttons|sizes|divider|selection|inputs|feedback|navigation),已回落 all"
);
Self::ALL
}
@@ -160,6 +165,7 @@ impl Sections {
let mut names = Vec::new();
for (on, name) in [
(self.buttons, "buttons"),
(self.sizes, "sizes"),
(self.divider, "divider"),
(self.selection, "selection"),
(self.inputs, "inputs"),
@@ -215,8 +221,9 @@ pub fn view<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
.font(ui.emphasized_font(TypeRole::TitleLarge))
.color(ui.color(Role::OnSurface)),
text(format!(
"M3 Expressive component base · flavor={} · 1180×760 @100% DPI · \
"M3 Expressive component base · board={} · flavor={} · 1180×760 @100% DPI · \
focus 与 pressed 同为 10%(令牌口径,图上一样是对的)",
ui.board().name(),
flavor_slug(ui)
))
.size(page.size_px)
@@ -236,6 +243,9 @@ pub fn view<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
if picked.buttons {
sections = sections.push(buttons::section(ui));
}
if picked.sizes {
sections = sections.push(sizes::section(ui));
}
if picked.divider {
sections = sections.push(divider_proof(ui));
}
@@ -0,0 +1,123 @@
//! 尺寸档矩阵:**按钮 XS~XL 五档** + **FAB 四态**(规格卡 `BUTTONS / XS - XL` 与
//! `FAB / SMALL - DEFAULT - MEDIUM - EXTENDED`)。
//!
//! ## 为什么单开一段(而不是塞进 `buttons`)
//!
//! `buttons` 段是「五式 × 五态 + 图标四式」的 40 格矩阵,一屏(1180×760)本来就贴着折叠线;
//! 五档梯子(最高 136)与四态 FAB(最宽 160)塞进去会被压到屏幕外——那样出的图**看不见**,
//! 等于没拍。所以按「一段 = 一个能一屏判完的问题」拆开:
//!
//! ```text
//! ULL_GALLERY_SECTION=sizes # 本段:尺寸梯子 + FAB 四态
//! ```
//!
//! ## 图上要能判什么
//!
//! 1. 五档高度递增、圆角全部是**半高**(药丸),标签字号跟着档位升(14/14/16/24/32);
//! 2. FAB 四态:40/56/80 三个方态 + 160×56 扩展态(图标 + 标签);
//! 3. 配色:FAB 一律 `PrimaryContainer` / `OnPrimaryContainer`(规格卡上四颗都是淡绿底深绿字)。
//!
//! 判不了:状态层(FAB 在这一段是静息态,五态矩阵在 `buttons` 段的按钮上验)、以及动效。
use iced::widget::{column, row};
use iced::{Element, Length};
use unlucky_design::color::Role;
use unlucky_design::icons::IconKind;
use unlucky_design::inkcat::size::{ControlSize, FabSize};
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use crate::theme::Ui;
use super::super::buttons::{self, fab, Button, Fab};
use super::super::interaction::Interaction;
use super::{heading, DemoMessage};
/// 整段(标题 + 五档梯子 + 四态 FAB + 图例)。
pub(super) fn section<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
let legend = ui.style(TypeRole::BodySmall);
let ladder = row(ControlSize::ALL
.iter()
.map(|size| sized(ui, *size))
.collect::<Vec<_>>())
.spacing(ui.space(Space::X4))
.align_y(iced::Alignment::Center);
let fabs = row(FabSize::ALL
.iter()
.map(|size| fab_cell(ui, *size))
.collect::<Vec<_>>())
.spacing(ui.space(Space::X4))
.align_y(iced::Alignment::Center);
column![
heading(
ui,
"sizes · buttons xs~xl + fab small~extended",
"规格卡:按钮 32/40/56/96/136(圆角 = 半高,标签 14/14/16/24/32 字重 500);\
FAB 40/r12 · 56/r16 · 80/r20 · 160×56/r16(图标 20/26/34/24,扩展态内距 28、间距 8)"
),
labelled(ui, "buttons xs - xl", ladder.into()),
labelled(ui, "fab small - extended", fabs.into()),
iced::widget::text(
"图例:产品页的按钮只用 S 档(40,= M3 control_height);M/L/XL 留给空态主行动与展示型入口。\
FAB 四颗都是静息态,配色 PrimaryContainer / OnPrimaryContainer。",
)
.size(legend.size_px)
.font(ui.font_of(legend))
.color(ui.color(Role::OnSurfaceVariant)),
]
.spacing(ui.space(Space::X2))
.into()
}
/// 一档 Filled 按钮(标签是档位名的大写:XS / S / M / L / XL。
///
/// **必须大写**:小写的 `l` 在 Inter/Segoe 里就是一根竖线,XL 会读成「x|」——第一版出图就是这么错的。
fn sized<'a>(ui: &'a Ui, size: ControlSize) -> Element<'a, DemoMessage> {
let label: &'static str = match size {
ControlSize::Xs => "XS",
ControlSize::S => "S",
ControlSize::M => "M",
ControlSize::L => "L",
ControlSize::Xl => "XL",
};
buttons::filled(
ui,
Button::new(label, DemoMessage::Pressed("size")).size(size),
)
}
/// 一颗 FAB(扩展态带标签;静息态强制,避免光标位置影响出图)。
fn fab_cell<'a>(ui: &'a Ui, size: FabSize) -> Element<'a, DemoMessage> {
let spec = if matches!(size, FabSize::Extended) {
Fab::extended(IconKind::Plus, "New", DemoMessage::Pressed("fab"))
} else {
Fab::new(IconKind::Plus, DemoMessage::Pressed("fab"))
}
.size(size)
.state(Interaction::Enabled);
fab(ui, spec)
}
/// 一行「小标题 + 内容」。
fn labelled<'a>(
ui: &'a Ui,
label: &'a str,
content: Element<'a, DemoMessage>,
) -> Element<'a, DemoMessage> {
let style = ui.style(TypeRole::LabelSmall);
column![
iced::widget::text(label)
.size(style.size_px)
.font(ui.font_of(style))
.color(ui.color(Role::OnSurfaceVariant)),
iced::widget::container(content)
.width(Length::Fill)
.padding(iced::Padding::from([ui.space(Space::X2), 0.0])),
]
.spacing(ui.space(Space::X1))
.into()
}