feat(app-iced): M79 导航家族 —— ListItem / Tabs / NavigationDrawer / AppBar

四件组件 + gallery 状态矩阵 + 渲染层几何断言,全部只落在
`components/navigation/**` 与 `gallery/navigation.rs`(不改 mod.rs / interaction / layout)。

- ListItem(12b §4):一行/两行/三行 44/60/76(A1 56/72/88,登记偏离)、leading/trailing 槽位、
  hover/focus/pressed 只叠状态层(内容色不变)、禁用是修饰器且内容角色取 A1 的 `OnSurface`@38%。
- Tabs(12b §6):Primary 3px 指示条(用 `Stack` 分层让 Fill 条拿到「该 tab 的宽」)、
  Secondary 1px `SurfaceVariant` 分隔线、带图标时容器 64、横向滚动;选中色按变体分(Primary / OnSurface)。
- NavigationDrawer(15 §21 + D33):**同一个容器两个宽度** —— rail 88 ⇄ drawer 240;
  药丸从容器宽派生(56×32 / 216×56,左右各缩 16 / 12,A1 `ItemPadding`);
  分组标题 TitleSmall、组间 1px OutlineVariant;drawer 项高 56(G24)。
- AppBar small(12b §16):页头 56(A1 64,登记偏离)、TitleLarge emphasized + LabelMedium 副标题、
  leading/trailing 槽位、滚动只换 `SurfaceContainer` + L2(A1 无 OnScroll…Height)。
- gallery:`ULL_NAV_PART=list|tabs|drawer|appbar|all` 分块出图;四块矩阵(ListItem 三行高 × 三态、
  Tabs 三个选中位置 + 五态、rail/drawer 同尺度并排、AppBar 静息/滚动)。
- `navigation/probe.rs`:读真截图量像素的 4 条断言(行高 44/60/76、禁用 OnSurface@38%、
  指示条宽 = 标签墨迹 + 2×16、rail 88 / drawer 240 与药丸内缩 16/12、AppBar 56),
  已用两次变异(行高改 56/72/88;drawer 内缩改 16)验证能被打红。

门禁:fmt ✓ · clippy -p unlucky-app-iced --all-targets -D warnings ✓ · test -p unlucky-app-iced 195 passed ✓
This commit is contained in:
AskaEth committed 2026-11-02 11:55:59 +08:00
1 parent 22636668d0
commit 81e34d77da
14 files changed
+3149 -39

No files matched your search

@@ -1,35 +1,53 @@
//! **导航类家族(桩)**:由后续 mission 填充(§4 ListItem / §6 Tabs / §21 NavigationDrawer / §16 AppBar)。
//! **导航类家族**(`research/12b` §4/§6/§16 + `research/15` §21):四件组件,一个容器。
//!
//! 本文件目前只有占位 `demo()`。**后续 mission 只写本目录内的文件,不改 `components/mod.rs`**:
//! 家族模块已经在 `mod.rs` 里声明好,`gallery/navigation.rs` 也已经在 `gallery/mod.rs` 里声明好,
//! 所以「加组件」不需要碰任何共享文件(D39 §1 的并行地基)。
//! | 组件 | 入口 | 参考章节 | 关键几何 |
//! |---|---|---|---|
//! | ListItem | [`list_item::list_item`] | 12b §4 | 行高 44 / 60 / 76(A1 56/72/88,登记偏离) |
//! | Tabs | [`tabs::tabs`] | 12b §6 | 容器 48、Primary 指示条 3、Secondary 线 1 |
//! | NavigationDrawer | [`drawer::navigation`] | 15 §21 + D33 | **同一个容器**:rail 88 ⇄ drawer 240 |
//! | AppBar small | [`app_bar::app_bar`] | 12b §16 | 页头 56、标题 TitleLarge、滚动换色 + L2 |
//!
//! 填充时请照抄按钮家族的分层:
//! ## 三条纪律(与另外四个家族同源)
//!
//! - 几何进 `navigation/geometry.rs`(数值从 `ComponentMetrics::NAVIGATION_DRAWER` /
//! `LayoutMetrics` / `Space` / `ShapeScale` 取,缺令牌登记 G 编号);
//! - 状态层只能走 [`crate::app::components::interaction`];
//! - 精确尺寸用 [`crate::app::components::layout`]——导航选中指示器 **56×32** 就是 Q8 的历史事故现场,
//! 必须用 `layout::centered_fixed`(`center_x(Length::Fill)` 会把它撑成 88 宽)。
//! 1. **对消息类型泛型**,不 import `crate::app::Message`(契约 §2);`Ui` 一律 `&'a Ui` 传入。
//! 2. **状态层只走** [`crate::app::components::interaction`]:α 与禁用色一处求值;
//! 禁用是**修饰器**(`.disabled()` 只清消息,见 `selection/chip.rs` 复审 P2-1)。
//! 3. **精确尺寸只走** [`crate::app::components::layout`]:图标一律 `layout::fixed`(Q9),
//! 居中交给父级 `align_x`(Q8:`center_x(Fill)` 会把 56 宽药丸撑成 88)。
//!
//! ## 禁用内容角色**按变体**(不许抄默认值)
//!
//! | 组件 | 禁用内容角色 | 出处 |
//! |---|---|---|
//! | ListItem | **`OnSurface`**@38%(不是 `OnSurfaceVariant`) | `[源]` `ListTokens.ListItemDisabledLabel/LeadingIcon/TrailingIconColor = OnSurface` + `…Opacity = 0.38f` |
//! | Drawer / Rail 项 | `OnSurface`@38% | `[估]`(A1 无 disabled token,与 ListItem 同表面语境) |
//! | Tabs | `OnSurface`@38% | `[估]`(同上;A1 的 Tabs 两套 token 只有 Active/Inactive) |
//! | AppBar | — (本轮页头不带禁用动作) | — |
//!
//! ## 令牌缺口(G 编号,续 `research/15` §25 的 G13;G16~G20 已被其他家族占用)
//!
//! - **G22** Tabs 容器高 48(带图标 64)——`LayoutMetrics` 无此档;
//! - **G23** Tabs 指示条高 3 / `TabMinWidth` 90(后者 iced 无法表达,登记为限制);
//! - **G24** drawer 项高 56——与 `LM.nav_item_height`(44) 的差异按 A1 取 56,见 [`geometry`];
//! - **G25** ListItem 两/三行高(60 / 76)——`G4` 要求的 `row_height_1/2/3` 仍缺,本文件按
//! `LM.row_height` + 每行 16 派生(A1 delta 相同)。
//!
//! ## 视觉证据(D30)
//!
//! `gallery/navigation.rs` 把四个组件摆成状态矩阵;`ULL_GALLERY_SECTION=navigation` 只渲染本段,
//! `ULL_NAV_PART=<list|tabs|drawer|appbar|all>` 再切成单块出图(1180×760 一屏装不下四块矩阵)。
//! 渲染层断言在 [`probe`](读 `ULL_NAV_DIR` 下的真截图量像素),纯函数断言在各组件文件的 `tests`。
use iced::widget::{container, text};
use iced::Element;
pub mod app_bar;
pub mod drawer;
pub mod geometry;
pub mod list_item;
#[cfg(test)]
mod probe;
pub mod tabs;
use unlucky_design::color::Role;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use crate::theme::Ui;
/// 占位 demo(ASCII 文案,理由同 `selection::demo`)。
pub fn demo<'a, M: 'a>(ui: &'a Ui) -> Element<'a, M> {
let style = ui.style(TypeRole::BodySmall);
container(
text("navigation · list item / tabs / drawer / app bar — pending")
.size(style.size_px)
.font(ui.font_of(style))
.color(ui.color(Role::OnSurfaceVariant)),
)
.padding(ui.space(Space::X2))
.into()
}
pub use app_bar::{app_bar, AppBar, AppBarAction};
pub use drawer::{navigation, NavGroup, NavItem, Navigation};
pub use geometry::NavLayout;
pub use list_item::{list_item, ListItem};
pub use tabs::{tabs, Tab, Tabs, TabsVariant};