Design System Guard / guard (push) Failing after 7m11s
本提交收口本轮四件事。三者改动在同一批文件里交织(改名触及 theme.dart、
深色化也在 theme.dart),按文件无法干净切分,故合为一个可审计的提交,
下面按事由分述。
一、命名统一:Pmon* → Inkwatch*(合入 V2 线后的收尾)
· 包名 pmon_client → inkwatch_client,Rust crate pmon-core → inkwatch-core。
改名后必须 flutter pub get 重建 package_config,否则 dart analyze 会报
1688 个"找不到 package:pmon_client"的假错误。
· 符号:PmonColors→InkwatchColors、PmonPalette→InkwatchPalette、
PmonTextStyles→InkwatchTextStyles、PmonRadius→InkwatchRadius、
PmonEinkText→InkwatchEinkText、PMON_LOG_*→INKWATCH_LOG_*。
· 覆盖 server/ client/ rust-core/ tests/ 全树;已用
git grep -In 'Pmon\|PMON\|pmon' 复核,零残留(tokens.css 里最后一处
§2.8 的类名注释也在本提交修掉)。
· rust-core/Cargo.lock 的变动只是包名变化导致 [[package]] 字典序重排。
二、深色主题:58 键同名覆写 + Flutter 整屏深色化
· tokens.css 增 dark 块(58 键),键名与浅色**逐一相同**——
这是 token_parity_test.dart 的深色 parity 组在守的约束。
· theme.dart:新增 InkwatchDarkColors / _assemble(palette, brightness),
把语义色从"常量"改为"随 brightness 装配"。
· main.dart:darkTheme + 显式 themeMode(ValueNotifier 持久化,不用
ThemeMode.system —— 水墨屏用户要的是自己定,不是跟系统)。
· theme_guard_test.dart C-15 → C-15′:从"禁止出现深色"改为"允许深色,
但必须 58 键同名",护栏跟着设计意图一起改,而不是删掉。
· 新增 dark_theme_test.dart / dark_ui_test.dart 覆盖装配与整屏渲染。
三、状态符号图标化:Unicode 字形 → Material Icons
· symbol_guard_test.dart 按**码位**扫 lib/(注释之外),命中 5 处:
transfer_card.dart 的 ⚠️/✓/✓/⚠,viewer_page.dart 的 ✓。
· 只删字形会留下不可见的 U+FE0F,源码看不出、界面渲染成豆腐块,
且按 emoji 区间匹配的旧护栏看不见它 —— 故护栏按码位判,且判据选
"扫注释之外全部字符"(错了只会多报,不会漏)。
· 改法:状态图标单独一槽,不拼进文案。
- transfer_card.dart:_offerHint 由 String? 改为
({IconData? icon, Color? tint, String text})?,渲染为 Row(Icon+Text);
「已收下」结果块同理(check_circle / error_outline)。
图标着色:warning_amber→_p.suspect,check_circle→_p.online。
- viewer_page.dart:_lanSummary() 返回 ({IconData? icon, String text}),
诊断行由 ('对端内网', _lanSummary(), null) 改为 (…, lan.text, lan.icon)。
调用点提到 return 之前只算一次。
· 理由不只是"好看":字符符号在水墨屏上会渲染成方框,且无法随主题着色。
验证
· client/scripts/test.sh:674 项全过(+674,0 失败,23s)
· tests/run-guards.sh(RUN_AXE=1):
web-token-guard.js PASS 21/0
phase3b-static-guard.js PASS 87/0
dockerfile-coverage-test.js PASS 53/0
schema-consistency-test.js PASS 116/0
client Dart 全量 PASS 674 项
admin-axe-audit.mjs PASS 42/42 格 viol=0(inc=12 有台账)
6 套件 PASS 6 / FAIL 0 / SKIPPED 0
· dart analyze:9 issues / 0 error(全部为既有 info/warning,非本次引入)
· 两处 tokens.css(仓库内 admin/assets 与设计系统交付目录)逐字节一致
sha256 088873cf6ece8da78135f96ca4633a537ac7802ea5f0fc8bd9c9294bb8a7bcaa
已知未做(写下来,不藏)
· flutter analyze 在本机因仓库路径含中文(水墨屏工具)触发 analysis_server
的 LSP 分帧 UTF-8 错误(FormatException: Unterminated string)。
绕法是用 dart analyze,或把工作树放到纯 ASCII 路径下。
· 窄屏降级态 + 失败态的组合真机复核:仍未做。
· VoiceOver / NVDA 实机验证:NOT RUN。
· 管理台登录框仍缺持久 <label>(现靠 aria-labelledby 指向标题)。
953 lines
41 KiB
Dart
953 lines
41 KiB
Dart
/// 视觉令牌(**浅色极简**)—— 全客户端界面样式的**唯一真相源**
|
||
///
|
||
/// ── 为什么要有这个文件 ────────────────────────────────────────────
|
||
/// 改版前 UI 共约 3500 行,里面散着 **70+ 处硬编码 `Color(0xFF…)`**(十几种互不成体系的
|
||
/// 灰度与蓝),间距/圆角/字号也全是字面量。后果不是"丑",而是**同一类元素在不同卡片里
|
||
/// 长得不一样**:主按钮在 A 卡是 0xFF2563EB、在 B 卡是 0xFF3B82F6;说明文字一会儿 11
|
||
/// 一会儿 11.5;卡片内边距 9/12/16 混用。用户读到的是"这里好像没那么整齐",
|
||
/// 但没人能说清哪里不一致 —— 因为**根本没有"一致"的定义**。
|
||
///
|
||
/// 这和本项目在其他地方反复踩的坑是同一个病(指标字段清单、配对码契约、显示目录、
|
||
/// 单位换算都各写过两遍)。故此处立**三条规矩**:
|
||
///
|
||
/// 1. **颜色/间距/圆角/字号只在这里定义**。页面里出现 `Color(0x…)` 即判红
|
||
/// (由 `test/theme_guard_test.dart` 源码结构断言锁定)。
|
||
/// 2. **语义优先于色值**:写 `InkwatchColors.online` 而不是 `Colors.green` ——
|
||
/// 否则"在线"换成别的色时,要满地找哪几处是它。
|
||
/// 3. **只提供必要刻度**,不做无限可调的间距体系(4/8/12/16/20/24 够用)。
|
||
/// 刻度一多,同一个视觉决策又会有三种写法。
|
||
///
|
||
/// ── 浅色极简的三条取向(与旧版深色 header 的差别)────────────────
|
||
/// · **层次靠留白与发丝线,不靠阴影**:卡片 = 白面 + 1px 描边,去掉浮夸投影;
|
||
/// · **颜色只用于表意**:整屏以中性灰为底,彩色仅出现在状态、主操作、告警上;
|
||
/// · **文字三级**(主/次/说明)代替"到处 11px 灰"。
|
||
library;
|
||
|
||
import 'package:flutter/material.dart';
|
||
|
||
/// 色板与语义色
|
||
abstract final class InkwatchColors {
|
||
// ── 中性(浅色极简的骨架)──────────────────────────────────────
|
||
/// 页面底色(比卡片略深,让白卡片浮起来 —— 用明度差代替阴影)
|
||
static const page = Color(0xFFF6F7F9);
|
||
|
||
/// 卡片/表面
|
||
static const surface = Color(0xFFFFFFFF);
|
||
|
||
/// 发丝线(描边、分隔)—— 只用这一种描边色,别再写第二种灰
|
||
static const line = Color(0xFFE5E7EB);
|
||
|
||
/// 次级表面(chip 未选中、代码块、折叠块头)
|
||
static const surfaceMuted = Color(0xFFF3F4F6);
|
||
|
||
// ── 文字三级 ──────────────────────────────────────────────────
|
||
/// 主要文字(标题、数值)
|
||
static const textPrimary = Color(0xFF111827);
|
||
|
||
/// 次要文字(正文说明)
|
||
static const textSecondary = Color(0xFF4B5563);
|
||
|
||
/// 辅助文字(单位、时间戳、脚注)
|
||
static const textTertiary = Color(0xFF646D7D);
|
||
|
||
// ── 主色(唯一强调色:主操作 + 选中态 + 链接)──────────────────
|
||
static const primary = Color(0xFF2563EB);
|
||
|
||
/// 主色的浅底(选中项背景、信息条背景)
|
||
static const primarySoft = Color(0xFFEFF4FF);
|
||
|
||
// ── 状态语义色(**只用于表意**,不要拿它当装饰)────────────────
|
||
/// 在线 / 正常
|
||
static const online = Color(0xFF16A34A);
|
||
static const onlineSoft = Color(0xFFEAF7EF);
|
||
|
||
/// 疑似离线 / 需注意
|
||
static const suspect = Color(0xFFD97706);
|
||
static const suspectSoft = Color(0xFFFEF6E7);
|
||
|
||
/// 离线 / 未知(中性灰,**不是**危险色 —— 离线不等于出错)
|
||
static const offline = Color(0xFF9CA3AF);
|
||
|
||
/// 危险 / 破坏性操作(停止采集、解除授权)
|
||
static const danger = Color(0xFFDC2626);
|
||
static const dangerSoft = Color(0xFFFDECEC);
|
||
|
||
// ── 状态文字色(软底上的字形/文字,对比度 AA 以上)──────────────
|
||
/// 在线徽标文字(onlineSoft 底 4.68:1)
|
||
static const onlineText = Color(0xFF117E39);
|
||
|
||
/// 疑似离线徽标文字(suspectSoft 底 4.88:1)
|
||
static const suspectText = Color(0xFFA45904);
|
||
|
||
/// 危险提示文字(dangerSoft 底 5.67:1)
|
||
static const dangerText = Color(0xFFB81E1E);
|
||
|
||
/// 离线徽标文字。**与 [textSecondary] 同值也必须独立命名**:
|
||
/// 语义优先于色值,将来离线文字换色不必满地找哪些地方借用了正文色。
|
||
static const offlineText = Color(0xFF4B5563);
|
||
|
||
/// 未知徽标文字(同上,语义独立别名,不复用 offlineText 的名字)
|
||
static const unknownText = Color(0xFF4B5563);
|
||
|
||
/// 信息条 / 选中态文字(primarySoft 底 6.73:1)
|
||
static const infoText = Color(0xFF124BC4);
|
||
|
||
/// 图表主色(与主色同源,单独命名以便日后独立调整)
|
||
static const chartLine = primary;
|
||
|
||
/// 断点虚线与"数据缺口"标记(中性,**不是**告警 —— 缺口是事实,不是错误)
|
||
static const gap = Color(0xFFB6BCC8);
|
||
}
|
||
|
||
/// 深色皮肤语义色 —— 与管理台唯一真相源 `tokens.css` 的
|
||
/// `html[data-iw-theme="dark"]` 块(05 §1.4,58 个同名键覆写)一一对应。
|
||
///
|
||
/// ── 只转录「Flutter 端确实会消费」的键 ─────────────────────────────
|
||
/// 颜色 / 状态 / 图表 / 品牌 / 反馈里,凡 Web 专属的交互态与预混实色一律
|
||
/// **保持缺省、不写进 Dart**,理由与浅色侧 `kWebOnlyTokens` 完全一致:
|
||
/// · hover/active/五种 `*-border` 预混实色 → Flutter 用 Material `WidgetState`
|
||
/// 处理交互态、用 `withValues(alpha:)` 动态算描边(见 [InkwatchCard.notice]);
|
||
/// · `series-2/3/4`、`chart-area/grid/axis`、`*-gap-band/threshold-*` →
|
||
/// `viewer_chart.dart` 只画单序列,无多序列 / 预混面积需求;
|
||
/// · `logo-*` 四色 → Dart 侧没有消费点;
|
||
/// · `mask` / `overlay-shadow` / `focus-ring` 是 CSS 复合值,Flutter 无对应概念。
|
||
/// 未转录的 28 个键在 `test/token_parity_test.dart` 的深色豁免表里逐项登记
|
||
/// (反向缺项判红),防止有人"好心"补一套用不到的常量。
|
||
///
|
||
/// 几何 / 排印 / 描边宽度 / 动效 / 端侧派生 / E-Ink 六色**本就不在 dark 块里**
|
||
/// (05 §1.4「应保持缺省」),Flutter 已有 `InkwatchSpace` / `InkwatchRadius` /
|
||
/// `InkwatchText` 语义档位,一套都不新增。
|
||
///
|
||
/// ⚠️ [textTertiary] 保持 `#8A93A3`(深底 5.02–6.24:1 达标),**显式不同步**
|
||
/// 浅色点修值 `#646D7D` —— 同步会让深底对比跌到 2.98–3.70:1,制造新违规。
|
||
abstract final class InkwatchDarkColors {
|
||
// ── 中性面(--iw-page / surface / surface-muted / line)──────────
|
||
static const page = Color(0xFF0B0E14);
|
||
static const surface = Color(0xFF151A23);
|
||
static const line = Color(0xFF333B4C);
|
||
static const surfaceMuted = Color(0xFF1E2430);
|
||
|
||
// ── 文字(--iw-text-primary / secondary / tertiary / on-accent)──
|
||
static const textPrimary = Color(0xFFF2F4F9);
|
||
static const textSecondary = Color(0xFFB9C2D0);
|
||
static const textTertiary = Color(0xFF8A93A3);
|
||
/// D3 反相:亮蓝主色底上的文字改深墨色(7.23:1)
|
||
static const textOnAccent = Color(0xFF0B0E14);
|
||
|
||
// ── 主色(--iw-primary / primary-soft / info-text)───────────────
|
||
static const primary = Color(0xFF6E9CFF);
|
||
static const primarySoft = Color(0xFF18243D);
|
||
|
||
// ── 状态(--iw-online / suspect / offline / danger 各 base+soft+text)──
|
||
static const online = Color(0xFF5CE0A0);
|
||
static const onlineSoft = Color(0xFF0D2117);
|
||
static const onlineText = Color(0xFF9AEAC3);
|
||
static const suspect = Color(0xFFD8790E);
|
||
static const suspectSoft = Color(0xFF261B0D);
|
||
static const suspectText = Color(0xFFF2A32C);
|
||
static const offline = Color(0xFF9CA3AF);
|
||
static const offlineText = Color(0xFFB9C2D0);
|
||
static const unknownText = Color(0xFFB9C2D0);
|
||
static const danger = Color(0xFFF87171);
|
||
static const dangerSoft = Color(0xFF2E1518);
|
||
static const dangerText = Color(0xFFFCA5A5);
|
||
static const infoText = Color(0xFFA8C5FF);
|
||
|
||
// ── 图表(--iw-chart-line / series-1 / gap)─────────────────────
|
||
/// 与 [primary] 同源(= `--iw-chart-line` / `--iw-series-1` 深值)
|
||
static const chartLine = primary;
|
||
static const gap = Color(0xFF6B7488);
|
||
|
||
// ── 反馈(--iw-snackbar-bg)────────────────────────────────────
|
||
/// D3 反相:深色 Toast 底变浅,文字自动用深 [surface](15.85:1 AAA)
|
||
static const snackbarBg = Color(0xFFF2F4F9);
|
||
}
|
||
|
||
/// 间距刻度(只此七档;新增刻度前先问"是否真的需要")
|
||
abstract final class InkwatchSpace {
|
||
static const xs = 4.0;
|
||
static const sm = 8.0;
|
||
static const md = 12.0;
|
||
static const lg = 16.0;
|
||
static const xl = 20.0;
|
||
static const xxl = 24.0;
|
||
}
|
||
|
||
/// 圆角刻度
|
||
abstract final class InkwatchRadius {
|
||
static const sm = 8.0;
|
||
static const md = 12.0;
|
||
static const lg = 16.0;
|
||
static const pill = 999.0;
|
||
}
|
||
|
||
/// 文字样式(三级 + 标题 + 数值)
|
||
abstract final class InkwatchText {
|
||
/// 页面/卡片大标题
|
||
static const title = TextStyle(
|
||
fontSize: 17, fontWeight: FontWeight.w600, color: InkwatchColors.textPrimary, height: 1.3);
|
||
|
||
/// 分区标题(卡片头)
|
||
static const section = TextStyle(
|
||
fontSize: 14, fontWeight: FontWeight.w600, color: InkwatchColors.textPrimary, height: 1.35);
|
||
|
||
/// 正文
|
||
static const body = TextStyle(
|
||
fontSize: 13, color: InkwatchColors.textPrimary, height: 1.5);
|
||
|
||
/// 正文(次要)
|
||
static const bodyMuted = TextStyle(
|
||
fontSize: 13, color: InkwatchColors.textSecondary, height: 1.5);
|
||
|
||
/// 说明文字(脚注、解释、诚实边界文案)
|
||
static const caption = TextStyle(
|
||
fontSize: 12, color: InkwatchColors.textTertiary, height: 1.5);
|
||
|
||
/// 图表轴标签 / Tooltip 的最小档(11px)。
|
||
/// 与 E-Ink 分支同纪律:界面里不写就地字号,需要小字就走这一档
|
||
/// (`viewer_chart.dart` 的 10 / 10.5 / 11.5 全部收敛到此处)。
|
||
static const micro = TextStyle(
|
||
fontSize: 11, color: InkwatchColors.textTertiary, height: 1.4);
|
||
|
||
/// 指标数值(等宽数字,避免跳动)
|
||
static const metric = TextStyle(
|
||
fontSize: 22,
|
||
fontWeight: FontWeight.w700,
|
||
color: InkwatchColors.textPrimary,
|
||
height: 1.15,
|
||
fontFeatures: [FontFeature.tabularFigures()]);
|
||
|
||
/// 指标单位/后缀
|
||
static const metricUnit = TextStyle(
|
||
fontSize: 12, fontWeight: FontWeight.w500, color: InkwatchColors.textTertiary);
|
||
|
||
/// 小统计块数值
|
||
static const stat = TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w600,
|
||
color: InkwatchColors.textPrimary,
|
||
fontFeatures: [FontFeature.tabularFigures()]);
|
||
}
|
||
|
||
/// 卡片与容器装饰(**统一在这里造**,页面不要自己拼 BoxDecoration)
|
||
abstract final class InkwatchCard {
|
||
/// 标准卡片:面 + 发丝线 + 大圆角。不用阴影(极简取向,且阴影在深色/低端屏上更难控)
|
||
///
|
||
/// [palette] 缺省时回退 [InkwatchPalette.light](与改造前逐字段一致),
|
||
/// 页面自绘一律传 `InkwatchPalette.of(context)` —— 深色下卡片才真正变深。
|
||
static BoxDecoration standard({Color? tint, InkwatchPalette? palette}) {
|
||
final p = palette ?? InkwatchPalette.light;
|
||
return BoxDecoration(
|
||
color: tint ?? p.surface,
|
||
borderRadius: BorderRadius.circular(InkwatchRadius.md),
|
||
border: Border.all(color: p.line),
|
||
);
|
||
}
|
||
|
||
/// 信息条(提示/错误/成功)—— 语义色浅底 + 同色描边。
|
||
/// [base] / [soft] 由调用方按当前色板传入(见 [statusStyle])。
|
||
static BoxDecoration notice(Color base, Color soft) => BoxDecoration(
|
||
color: soft,
|
||
borderRadius: BorderRadius.circular(InkwatchRadius.sm),
|
||
border: Border.all(color: base.withValues(alpha: 0.28)),
|
||
);
|
||
|
||
/// 选中态容器(主色底 + 主色描边)
|
||
static BoxDecoration selected({InkwatchPalette? palette}) {
|
||
final p = palette ?? InkwatchPalette.light;
|
||
return BoxDecoration(
|
||
color: p.primarySoft,
|
||
borderRadius: BorderRadius.circular(InkwatchRadius.md),
|
||
border: Border.all(color: p.primary, width: 1.5),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Material 主题装配。**只在这里 new ThemeData** —— 保证控件(输入框/菜单/SnackBar/
|
||
/// 对话框)与自绘卡片是同一套颜色与圆角,不会出现"系统控件是旧蓝、自绘是新品"。
|
||
///
|
||
/// 浅色 / 深色共用**同一段控件装配**([_assemble]),只传入不同的语义色板
|
||
/// ([_lightPalette] / [_darkPalette])—— 不另写一套分叉的控件配置。
|
||
/// `buildInkwatchDarkTheme()` 也走这里,故「只在 theme.dart 内 new ThemeData」的
|
||
/// 规矩对深色同样成立(页面里就地 `ThemeData(` 由 theme_guard_test.dart 判红)。
|
||
ThemeData buildInkwatchTheme() => _assemble(_lightPalette, Brightness.light)
|
||
.copyWith(
|
||
extensions: const <ThemeExtension<dynamic>>[InkwatchPalette.light]);
|
||
|
||
/// 深色皮肤主题。色值来自 `tokens.css` 的 `html[data-iw-theme="dark"]`
|
||
/// (05 §1.4 的 58 个同名键覆写),由 [InkwatchDarkColors] 逐项转录。
|
||
///
|
||
/// 装配结构与 [buildInkwatchTheme] 完全相同,仅语义色替换为深色板:
|
||
/// 圆角 / 间距 / 字号刻度一律沿用 InkwatchSpace / InkwatchRadius / InkwatchText,
|
||
/// 不新增任何刻度。E-Ink 看板走独立的 [einkTheme],不受本主题影响。
|
||
ThemeData buildInkwatchDarkTheme() => _assemble(_darkPalette, Brightness.dark)
|
||
.copyWith(
|
||
extensions: const <ThemeExtension<dynamic>>[InkwatchPalette.dark]);
|
||
|
||
/// 控件装配用到的语义色子集。**不是新令牌** —— 只是把两套色板喂给同一段装配,
|
||
/// 避免把 Web 的「预混实色 / 交互态」概念搬进 Dart。
|
||
class _InkwatchPalette {
|
||
const _InkwatchPalette({
|
||
required this.page,
|
||
required this.surface,
|
||
required this.line,
|
||
required this.surfaceMuted,
|
||
required this.textPrimary,
|
||
required this.textSecondary,
|
||
required this.textTertiary,
|
||
required this.textOnAccent,
|
||
required this.primary,
|
||
required this.primarySoft,
|
||
required this.snackbarBg,
|
||
required this.snackbarText,
|
||
});
|
||
|
||
final Color page;
|
||
final Color surface;
|
||
final Color line;
|
||
final Color surfaceMuted;
|
||
final Color textPrimary;
|
||
final Color textSecondary;
|
||
final Color textTertiary;
|
||
final Color textOnAccent;
|
||
final Color primary;
|
||
final Color primarySoft;
|
||
final Color snackbarBg;
|
||
final Color snackbarText;
|
||
}
|
||
|
||
const _lightPalette = _InkwatchPalette(
|
||
page: InkwatchColors.page,
|
||
surface: InkwatchColors.surface,
|
||
line: InkwatchColors.line,
|
||
surfaceMuted: InkwatchColors.surfaceMuted,
|
||
textPrimary: InkwatchColors.textPrimary,
|
||
textSecondary: InkwatchColors.textSecondary,
|
||
textTertiary: InkwatchColors.textTertiary,
|
||
textOnAccent: Colors.white,
|
||
primary: InkwatchColors.primary,
|
||
primarySoft: InkwatchColors.primarySoft,
|
||
// 浅色 Toast:深底白字(--iw-snackbar-bg 浅色 = textPrimary #111827)
|
||
snackbarBg: InkwatchColors.textPrimary,
|
||
snackbarText: Colors.white,
|
||
);
|
||
|
||
const _darkPalette = _InkwatchPalette(
|
||
page: InkwatchDarkColors.page,
|
||
surface: InkwatchDarkColors.surface,
|
||
line: InkwatchDarkColors.line,
|
||
surfaceMuted: InkwatchDarkColors.surfaceMuted,
|
||
textPrimary: InkwatchDarkColors.textPrimary,
|
||
textSecondary: InkwatchDarkColors.textSecondary,
|
||
textTertiary: InkwatchDarkColors.textTertiary,
|
||
textOnAccent: InkwatchDarkColors.textOnAccent,
|
||
primary: InkwatchDarkColors.primary,
|
||
primarySoft: InkwatchDarkColors.primarySoft,
|
||
// D3 反相:深色 Toast 底 = 浅色 #F2F4F9,文字改用深 surface(15.85:1 AAA)
|
||
snackbarBg: InkwatchDarkColors.snackbarBg,
|
||
snackbarText: InkwatchDarkColors.surface,
|
||
);
|
||
|
||
ThemeData _assemble(_InkwatchPalette p, Brightness brightness) {
|
||
final baseScheme = ColorScheme.fromSeed(
|
||
seedColor: p.primary,
|
||
brightness: brightness,
|
||
surface: p.surface,
|
||
);
|
||
// 深底必须让主色严格落在语义令牌上:fromSeed 会把 primary 推到 tonal 近似值
|
||
// (实测 #6E9CFF → #AFC6FF),FilledButton / 选中态就会用到非令牌蓝。
|
||
// 浅色分支保持 fromSeed 原始结果,确保 buildInkwatchTheme() 逐字段不变。
|
||
final colorScheme = brightness == Brightness.dark
|
||
? baseScheme.copyWith(
|
||
primary: p.primary,
|
||
onPrimary: p.textOnAccent,
|
||
onSurface: p.textPrimary,
|
||
)
|
||
: baseScheme;
|
||
final base = ThemeData(
|
||
useMaterial3: true,
|
||
brightness: brightness,
|
||
colorScheme: colorScheme,
|
||
scaffoldBackgroundColor: p.page,
|
||
splashFactory: InkSparkle.splashFactory,
|
||
);
|
||
|
||
return base.copyWith(
|
||
appBarTheme: AppBarTheme(
|
||
backgroundColor: p.surface,
|
||
surfaceTintColor: Colors.transparent,
|
||
foregroundColor: p.textPrimary,
|
||
elevation: 0,
|
||
scrolledUnderElevation: 0,
|
||
centerTitle: false,
|
||
titleTextStyle: InkwatchText.title.copyWith(color: p.textPrimary),
|
||
),
|
||
dividerTheme: DividerThemeData(color: p.line, thickness: 1, space: 1),
|
||
cardTheme: base.cardTheme.copyWith(
|
||
color: p.surface,
|
||
surfaceTintColor: Colors.transparent,
|
||
elevation: 0,
|
||
margin: EdgeInsets.zero,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(InkwatchRadius.md),
|
||
side: BorderSide(color: p.line),
|
||
),
|
||
),
|
||
inputDecorationTheme: InputDecorationTheme(
|
||
isDense: true,
|
||
filled: true,
|
||
fillColor: p.surface,
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: InkwatchSpace.md, vertical: InkwatchSpace.md),
|
||
hintStyle: InkwatchText.caption.copyWith(color: p.textTertiary),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(InkwatchRadius.sm),
|
||
borderSide: BorderSide(color: p.line),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(InkwatchRadius.sm),
|
||
borderSide: BorderSide(color: p.line),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(InkwatchRadius.sm),
|
||
borderSide: BorderSide(color: p.primary, width: 1.5),
|
||
),
|
||
),
|
||
snackBarTheme: base.snackBarTheme.copyWith(
|
||
behavior: SnackBarBehavior.floating,
|
||
backgroundColor: p.snackbarBg,
|
||
contentTextStyle: TextStyle(fontSize: 13, color: p.snackbarText),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(InkwatchRadius.sm)),
|
||
),
|
||
popupMenuTheme: base.popupMenuTheme.copyWith(
|
||
color: p.surface,
|
||
surfaceTintColor: Colors.transparent,
|
||
elevation: 2,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(InkwatchRadius.sm),
|
||
side: BorderSide(color: p.line),
|
||
),
|
||
textStyle: InkwatchText.body.copyWith(color: p.textPrimary),
|
||
),
|
||
navigationBarTheme: NavigationBarThemeData(
|
||
backgroundColor: p.surface,
|
||
surfaceTintColor: Colors.transparent,
|
||
elevation: 0,
|
||
// 选中态用主色浅底 + 主色图标/文字。
|
||
// 不设的话 M3 会按 seed 自动生成一个偏紫的 secondaryContainer,
|
||
// 与我们的蓝主色不搭(真机截图里底部导航是紫色的)。
|
||
indicatorColor: p.primarySoft,
|
||
labelTextStyle: WidgetStateProperty.resolveWith((states) => TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: states.contains(WidgetState.selected)
|
||
? p.primary
|
||
: p.textTertiary,
|
||
)),
|
||
iconTheme: WidgetStateProperty.resolveWith((states) => IconThemeData(
|
||
size: 22,
|
||
color: states.contains(WidgetState.selected)
|
||
? p.primary
|
||
: p.textTertiary,
|
||
)),
|
||
),
|
||
textButtonTheme: TextButtonThemeData(
|
||
style: TextButton.styleFrom(
|
||
foregroundColor: p.primary,
|
||
textStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||
),
|
||
),
|
||
listTileTheme: ListTileThemeData(
|
||
iconColor: p.textTertiary,
|
||
textColor: p.textPrimary,
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 状态 → (文字, 颜色, 浅底, 文字色)的**唯一映射**。
|
||
///
|
||
/// 为什么单独抽出来:状态颜色曾在采集页/查看页/设备列表三处各写一套 if-else,
|
||
/// 于是"疑似离线"在一处是橙、在另一处是黄。这类"同一语义两处判定"是本仓库的老病,
|
||
/// 故收敛成一条函数,新增状态只改这里。
|
||
///
|
||
/// 只扩到 **4 字段、不带 `border`**:Flutter 的语义描边由
|
||
/// `base.withValues(alpha: 0.28)` 动态生成(见 [InkwatchCard.notice]),
|
||
/// 没有 Web 端「预混实色描边」这一需求,不要把 CSS 的实现概念搬进 Dart。
|
||
/// [palette] 缺省时回退 [InkwatchPalette.light](与改造前逐字段一致);
|
||
/// 页面自绘一律传当前色板,深色 / E-Ink 下状态色才跟着换。
|
||
({String label, Color color, Color soft, Color text}) statusStyle(
|
||
String key, [InkwatchPalette? palette]) {
|
||
final p = palette ?? InkwatchPalette.light;
|
||
switch (key) {
|
||
case 'online':
|
||
return (label: '在线', color: p.online,
|
||
soft: p.onlineSoft, text: p.onlineText);
|
||
case 'suspect':
|
||
return (label: '疑似离线', color: p.suspect,
|
||
soft: p.suspectSoft, text: p.suspectText);
|
||
case 'offline':
|
||
return (label: '离线', color: p.offline,
|
||
soft: p.surfaceMuted, text: p.offlineText);
|
||
case 'unknown':
|
||
// 未知**必须**是中性灰:把"不知道"染成告警色等于编造结论(诚实边界)
|
||
return (label: '状态未知', color: p.offline,
|
||
soft: p.surfaceMuted, text: p.unknownText);
|
||
case 'info':
|
||
return (label: '提示', color: p.primary,
|
||
soft: p.primarySoft, text: p.infoText);
|
||
default:
|
||
return (label: '状态未知', color: p.offline,
|
||
soft: p.surfaceMuted, text: p.unknownText);
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// 水墨屏(E-Ink)令牌 —— iReader Neo 2 这类 6″ 黑白 256 灰阶设备
|
||
// ══════════════════════════════════════════════════════════════════
|
||
|
||
/// 水墨屏配色。
|
||
///
|
||
/// ── 为什么不能照搬上面的浅色主题(2026-09-15)──────────────────────
|
||
/// 墨水屏的显示物理与彩色 LCD 完全不同,上面那套令牌搬过去会"糊":
|
||
///
|
||
/// | 浅色主题的写法 | 在墨水屏上的后果 |
|
||
/// |---|---|
|
||
/// | 浅色辅助文字色(`textTertiary` 浅灰字) | 300PPI 墨水屏下也发虚,且**黑色残影**明显 |
|
||
/// | `page = #F6F7F9` 浅灰底 | 大面积灰底要全屏重绘(慢),且灰底上再写灰字几乎看不清 |
|
||
/// | `line = #E5E7EB` 发丝线 | 灰阶下与底几乎同色 → 等于没有分隔 |
|
||
/// | `online` 绿 / `suspect` 橙 | **亮度接近**,灰阶化后两个状态长得一样 |
|
||
/// | 任何动画(转圈、呼吸) | 持续局部刷新 → 残影、闪、发热、耗电(墨水屏最忌) |
|
||
///
|
||
/// 故墨水屏只用**三档明度**(纯黑 / 深灰 / 纯白),分层靠**字重与描边**而不是底色,
|
||
/// 状态靠**形状**而不是颜色(见 [statusGlyph])。
|
||
class InkwatchEink {
|
||
InkwatchEink._();
|
||
|
||
/// 底:纯白。墨水屏的"原生"是白纸,浅灰底只会浪费对比度
|
||
static const page = Color(0xFFFFFFFF);
|
||
|
||
/// 主文字:纯黑(不要用 #111827 —— 深灰在黑字旁边会显得发虚)
|
||
static const ink = Color(0xFF000000);
|
||
|
||
/// 次级文字/单位:深灰。**不要用浅灰** —— 那是 LCD 的习惯,墨水屏上会糊
|
||
static const inkSoft = Color(0xFF444444);
|
||
|
||
/// 描边与分隔:黑细线(墨水屏上灰线等于没有)
|
||
static const rule = Color(0xFF000000);
|
||
|
||
/// 卡片/格子:白底 + 黑描边(不靠灰底分区)
|
||
static const box = Color(0xFFFFFFFF);
|
||
|
||
/// 需要"填充对比"的地方(条形/进度),与白底形成明确两级
|
||
static const fill = Color(0xFF000000);
|
||
}
|
||
|
||
/// 水墨屏文字样式 —— **全分支唯一的字号刻度**。
|
||
///
|
||
/// ── 两条纪律(2026-09-16 立,为了让"字号不一"不可能再出现)──────────
|
||
/// 1. **界面里不得写就地字号**(`fontSize: 数字`)。要新尺寸就在下面加一档,
|
||
/// 并写清它对应什么语义 —— 否则同一类元素很快会变成 13/13.5/14 三种写法
|
||
/// (彩色主题当年就是这样烂掉的,见 `theme.dart` 文件头)。
|
||
/// 2. **同一屏的数值必须共用同一字号**。数值缩放要按**全屏最长的那条**统一算
|
||
/// (`einkUniformValueScale`),不能用 `FittedBox` 逐格各自缩 ——
|
||
/// 逐格缩的结果是 `310 kb/s` 比 `24.8%` 小一号,一屏里字号参差。
|
||
///
|
||
/// 为什么全部加粗(最低 w600):墨水屏墨滴扩散会让细笔画发虚,
|
||
/// w400 正文在 6″ 300PPI 上远看会断笔。
|
||
class InkwatchEinkText {
|
||
InkwatchEinkText._();
|
||
|
||
/// 数值(**基准字号**)。实际字号 = 本档 × `einkUniformValueScale()`
|
||
/// 算出的全屏统一比例(只会缩、不会放)。
|
||
static const value = TextStyle(
|
||
fontSize: 40, fontWeight: FontWeight.w700, color: InkwatchEink.ink, height: 1.0);
|
||
|
||
/// 状态形状(●/◐/○/◌)—— 与设备名同行,比它略大以便一眼可辨
|
||
static const glyph = TextStyle(
|
||
fontSize: 22, fontWeight: FontWeight.w700, color: InkwatchEink.ink, height: 1.0);
|
||
|
||
/// 设备名
|
||
static const title = TextStyle(
|
||
fontSize: 20, fontWeight: FontWeight.w700, color: InkwatchEink.ink, height: 1.15);
|
||
|
||
/// 指标标签 / 按钮文字(同号,语义不同则用字重区分,不靠字号)
|
||
static const label = TextStyle(
|
||
fontSize: 14, fontWeight: FontWeight.w600, color: InkwatchEink.inkSoft, height: 1.2);
|
||
|
||
/// 按钮文字:与 [label] **同号**,只提字重与颜色(不靠改字号来表达重要)
|
||
static const action = TextStyle(
|
||
fontSize: 14, fontWeight: FontWeight.w700, color: InkwatchEink.ink, height: 1.2);
|
||
|
||
/// 角落说明(刷新时刻、异常时的末帧提示)
|
||
static const caption = TextStyle(
|
||
fontSize: 12, fontWeight: FontWeight.w600, color: InkwatchEink.inkSoft, height: 1.3);
|
||
}
|
||
|
||
/// 状态 → **形状**的唯一映射(水墨屏用)。
|
||
///
|
||
/// 为什么必须有它:24 位色下"在线绿 / 疑似橙 / 离线灰"是三个可辨的颜色,
|
||
/// 而 256 灰阶下 `#16A34A`(绿) 与 `#D97706`(橙) 的**相对亮度接近**(0.42 vs 0.45),
|
||
/// 灰阶化后几乎同色 —— 界面就在说谎(用户分不清设备到底在不在线)。
|
||
///
|
||
/// 故同一语义在墨水屏上必须换一条**不依赖颜色**的表达通道:
|
||
/// 实心=在线、半实心=疑似离线、空心=离线(与颜色版语义一一对应,由测试钉住)。
|
||
String statusGlyph(String key) {
|
||
switch (key) {
|
||
case 'online':
|
||
return '●';
|
||
case 'suspect':
|
||
return '◐';
|
||
case 'offline':
|
||
return '○';
|
||
default:
|
||
// 状态未知(服务端没告知在线态)—— 用"虚线圆"与"确定离线"的实线圆区分开。
|
||
// 这两者处置完全不同:未知要等/查,离线要叫对方开机。
|
||
return '◌';
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// 页面自绘色板(ThemeExtension)—— 整屏深浅 / E-Ink 的唯一查询入口
|
||
// ══════════════════════════════════════════════════════════════════
|
||
|
||
/// 页面自绘语义色板。
|
||
///
|
||
/// ── 为什么需要它(整屏深色化)────────────────────────────────────
|
||
/// [ThemeData] 只负责系统控件(输入框 / 菜单 / SnackBar / 对话框…),
|
||
/// 而本项目的卡片、状态灯、说明文字是**自绘**的(BoxDecoration / TextStyle /
|
||
/// Icon color)。它们过去直接引用 [InkwatchColors] 静态常量,于是深色下
|
||
/// 「系统控件变深了、卡片和文字仍是浅色」—— 整屏并没有真正深色。
|
||
///
|
||
/// 故把页面自绘会消费的全部语义色收进一份 [ThemeExtension],
|
||
/// 由 [buildInkwatchTheme] / [buildInkwatchDarkTheme] / [einkTheme] 各挂一份实例。
|
||
/// 页面统一用 [InkwatchPalette.of] 取当前色板:
|
||
/// · [light] 逐项等于 [InkwatchColors] —— 浅色视觉一字不变;
|
||
/// · [dark] 逐项等于 [InkwatchDarkColors] —— 深色整屏生效;
|
||
/// · [eink] 全部映射到 [InkwatchEink] 六色 —— 纯黑白、无彩色。
|
||
///
|
||
/// 色值**定义源**仍是 [InkwatchColors] / [InkwatchDarkColors] / [InkwatchEink] 静态常量
|
||
/// (parity / 护栏测试依赖,一个不动);本类只做「按主题选取」,不新增色值、
|
||
/// 不新增圆角 / 间距 / 字号刻度。
|
||
class InkwatchPalette extends ThemeExtension<InkwatchPalette> {
|
||
const InkwatchPalette({
|
||
required this.page,
|
||
required this.surface,
|
||
required this.surfaceMuted,
|
||
required this.line,
|
||
required this.textPrimary,
|
||
required this.textSecondary,
|
||
required this.textTertiary,
|
||
required this.primary,
|
||
required this.primarySoft,
|
||
required this.online,
|
||
required this.onlineSoft,
|
||
required this.onlineText,
|
||
required this.suspect,
|
||
required this.suspectSoft,
|
||
required this.suspectText,
|
||
required this.offline,
|
||
required this.offlineText,
|
||
required this.unknownText,
|
||
required this.danger,
|
||
required this.dangerSoft,
|
||
required this.dangerText,
|
||
required this.infoText,
|
||
required this.chartLine,
|
||
required this.gap,
|
||
});
|
||
|
||
final Color page;
|
||
final Color surface;
|
||
final Color surfaceMuted;
|
||
final Color line;
|
||
final Color textPrimary;
|
||
final Color textSecondary;
|
||
final Color textTertiary;
|
||
final Color primary;
|
||
final Color primarySoft;
|
||
final Color online;
|
||
final Color onlineSoft;
|
||
final Color onlineText;
|
||
final Color suspect;
|
||
final Color suspectSoft;
|
||
final Color suspectText;
|
||
final Color offline;
|
||
final Color offlineText;
|
||
final Color unknownText;
|
||
final Color danger;
|
||
final Color dangerSoft;
|
||
final Color dangerText;
|
||
final Color infoText;
|
||
final Color chartLine;
|
||
final Color gap;
|
||
|
||
/// 当前主题的色板。裸 MaterialApp(widget 测试未挂 extensions)回退 [light],
|
||
/// 与改造前视觉一致,也不会因 extension 缺失而崩。
|
||
static InkwatchPalette of(BuildContext context) =>
|
||
Theme.of(context).extension<InkwatchPalette>() ?? light;
|
||
|
||
/// 浅色:逐项 = [InkwatchColors]。
|
||
static const light = InkwatchPalette(
|
||
page: InkwatchColors.page,
|
||
surface: InkwatchColors.surface,
|
||
surfaceMuted: InkwatchColors.surfaceMuted,
|
||
line: InkwatchColors.line,
|
||
textPrimary: InkwatchColors.textPrimary,
|
||
textSecondary: InkwatchColors.textSecondary,
|
||
textTertiary: InkwatchColors.textTertiary,
|
||
primary: InkwatchColors.primary,
|
||
primarySoft: InkwatchColors.primarySoft,
|
||
online: InkwatchColors.online,
|
||
onlineSoft: InkwatchColors.onlineSoft,
|
||
onlineText: InkwatchColors.onlineText,
|
||
suspect: InkwatchColors.suspect,
|
||
suspectSoft: InkwatchColors.suspectSoft,
|
||
suspectText: InkwatchColors.suspectText,
|
||
offline: InkwatchColors.offline,
|
||
offlineText: InkwatchColors.offlineText,
|
||
unknownText: InkwatchColors.unknownText,
|
||
danger: InkwatchColors.danger,
|
||
dangerSoft: InkwatchColors.dangerSoft,
|
||
dangerText: InkwatchColors.dangerText,
|
||
infoText: InkwatchColors.infoText,
|
||
chartLine: InkwatchColors.chartLine,
|
||
gap: InkwatchColors.gap,
|
||
);
|
||
|
||
/// 深色:逐项 = [InkwatchDarkColors]。
|
||
static const dark = InkwatchPalette(
|
||
page: InkwatchDarkColors.page,
|
||
surface: InkwatchDarkColors.surface,
|
||
surfaceMuted: InkwatchDarkColors.surfaceMuted,
|
||
line: InkwatchDarkColors.line,
|
||
textPrimary: InkwatchDarkColors.textPrimary,
|
||
textSecondary: InkwatchDarkColors.textSecondary,
|
||
textTertiary: InkwatchDarkColors.textTertiary,
|
||
primary: InkwatchDarkColors.primary,
|
||
primarySoft: InkwatchDarkColors.primarySoft,
|
||
online: InkwatchDarkColors.online,
|
||
onlineSoft: InkwatchDarkColors.onlineSoft,
|
||
onlineText: InkwatchDarkColors.onlineText,
|
||
suspect: InkwatchDarkColors.suspect,
|
||
suspectSoft: InkwatchDarkColors.suspectSoft,
|
||
suspectText: InkwatchDarkColors.suspectText,
|
||
offline: InkwatchDarkColors.offline,
|
||
offlineText: InkwatchDarkColors.offlineText,
|
||
unknownText: InkwatchDarkColors.unknownText,
|
||
danger: InkwatchDarkColors.danger,
|
||
dangerSoft: InkwatchDarkColors.dangerSoft,
|
||
dangerText: InkwatchDarkColors.dangerText,
|
||
infoText: InkwatchDarkColors.infoText,
|
||
chartLine: InkwatchDarkColors.chartLine,
|
||
gap: InkwatchDarkColors.gap,
|
||
);
|
||
|
||
/// 水墨屏:全部映射到 [InkwatchEink] 六色(白 / 黑 / 深灰,R=G=B 无彩色)。
|
||
/// 看板状态靠字形([statusGlyph])而非颜色,此处只需保证绝无彩色。
|
||
static const eink = InkwatchPalette(
|
||
page: InkwatchEink.page,
|
||
surface: InkwatchEink.box,
|
||
surfaceMuted: InkwatchEink.page,
|
||
line: InkwatchEink.rule,
|
||
textPrimary: InkwatchEink.ink,
|
||
textSecondary: InkwatchEink.inkSoft,
|
||
textTertiary: InkwatchEink.inkSoft,
|
||
primary: InkwatchEink.ink,
|
||
primarySoft: InkwatchEink.page,
|
||
online: InkwatchEink.ink,
|
||
onlineSoft: InkwatchEink.page,
|
||
onlineText: InkwatchEink.ink,
|
||
suspect: InkwatchEink.ink,
|
||
suspectSoft: InkwatchEink.page,
|
||
suspectText: InkwatchEink.ink,
|
||
offline: InkwatchEink.inkSoft,
|
||
offlineText: InkwatchEink.inkSoft,
|
||
unknownText: InkwatchEink.inkSoft,
|
||
danger: InkwatchEink.ink,
|
||
dangerSoft: InkwatchEink.page,
|
||
dangerText: InkwatchEink.ink,
|
||
infoText: InkwatchEink.ink,
|
||
chartLine: InkwatchEink.ink,
|
||
gap: InkwatchEink.inkSoft,
|
||
);
|
||
|
||
@override
|
||
InkwatchPalette copyWith({
|
||
Color? page,
|
||
Color? surface,
|
||
Color? surfaceMuted,
|
||
Color? line,
|
||
Color? textPrimary,
|
||
Color? textSecondary,
|
||
Color? textTertiary,
|
||
Color? primary,
|
||
Color? primarySoft,
|
||
Color? online,
|
||
Color? onlineSoft,
|
||
Color? onlineText,
|
||
Color? suspect,
|
||
Color? suspectSoft,
|
||
Color? suspectText,
|
||
Color? offline,
|
||
Color? offlineText,
|
||
Color? unknownText,
|
||
Color? danger,
|
||
Color? dangerSoft,
|
||
Color? dangerText,
|
||
Color? infoText,
|
||
Color? chartLine,
|
||
Color? gap,
|
||
}) =>
|
||
InkwatchPalette(
|
||
page: page ?? this.page,
|
||
surface: surface ?? this.surface,
|
||
surfaceMuted: surfaceMuted ?? this.surfaceMuted,
|
||
line: line ?? this.line,
|
||
textPrimary: textPrimary ?? this.textPrimary,
|
||
textSecondary: textSecondary ?? this.textSecondary,
|
||
textTertiary: textTertiary ?? this.textTertiary,
|
||
primary: primary ?? this.primary,
|
||
primarySoft: primarySoft ?? this.primarySoft,
|
||
online: online ?? this.online,
|
||
onlineSoft: onlineSoft ?? this.onlineSoft,
|
||
onlineText: onlineText ?? this.onlineText,
|
||
suspect: suspect ?? this.suspect,
|
||
suspectSoft: suspectSoft ?? this.suspectSoft,
|
||
suspectText: suspectText ?? this.suspectText,
|
||
offline: offline ?? this.offline,
|
||
offlineText: offlineText ?? this.offlineText,
|
||
unknownText: unknownText ?? this.unknownText,
|
||
danger: danger ?? this.danger,
|
||
dangerSoft: dangerSoft ?? this.dangerSoft,
|
||
dangerText: dangerText ?? this.dangerText,
|
||
infoText: infoText ?? this.infoText,
|
||
chartLine: chartLine ?? this.chartLine,
|
||
gap: gap ?? this.gap,
|
||
);
|
||
|
||
@override
|
||
InkwatchPalette lerp(covariant InkwatchPalette? other, double t) {
|
||
if (other == null) return this;
|
||
return InkwatchPalette(
|
||
page: Color.lerp(page, other.page, t)!,
|
||
surface: Color.lerp(surface, other.surface, t)!,
|
||
surfaceMuted: Color.lerp(surfaceMuted, other.surfaceMuted, t)!,
|
||
line: Color.lerp(line, other.line, t)!,
|
||
textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!,
|
||
textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!,
|
||
textTertiary: Color.lerp(textTertiary, other.textTertiary, t)!,
|
||
primary: Color.lerp(primary, other.primary, t)!,
|
||
primarySoft: Color.lerp(primarySoft, other.primarySoft, t)!,
|
||
online: Color.lerp(online, other.online, t)!,
|
||
onlineSoft: Color.lerp(onlineSoft, other.onlineSoft, t)!,
|
||
onlineText: Color.lerp(onlineText, other.onlineText, t)!,
|
||
suspect: Color.lerp(suspect, other.suspect, t)!,
|
||
suspectSoft: Color.lerp(suspectSoft, other.suspectSoft, t)!,
|
||
suspectText: Color.lerp(suspectText, other.suspectText, t)!,
|
||
offline: Color.lerp(offline, other.offline, t)!,
|
||
offlineText: Color.lerp(offlineText, other.offlineText, t)!,
|
||
unknownText: Color.lerp(unknownText, other.unknownText, t)!,
|
||
danger: Color.lerp(danger, other.danger, t)!,
|
||
dangerSoft: Color.lerp(dangerSoft, other.dangerSoft, t)!,
|
||
dangerText: Color.lerp(dangerText, other.dangerText, t)!,
|
||
infoText: Color.lerp(infoText, other.infoText, t)!,
|
||
chartLine: Color.lerp(chartLine, other.chartLine, t)!,
|
||
gap: Color.lerp(gap, other.gap, t)!,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 文字样式出口:把 [InkwatchText] 的档位(含浅色 color)按当前色板覆盖文字色。
|
||
///
|
||
/// 页面自绘统一走 `InkwatchTextStyles.of(p).caption`,避免在几十处散写
|
||
/// `InkwatchText.caption.copyWith(color: ...)` —— 那是同一份映射散落多处。
|
||
/// [InkwatchText] 静态常量本身**保留不动**(护栏 / parity 测试依赖它)。
|
||
class InkwatchTextStyles {
|
||
const InkwatchTextStyles(this.p);
|
||
|
||
final InkwatchPalette p;
|
||
|
||
static InkwatchTextStyles of(InkwatchPalette p) => InkwatchTextStyles(p);
|
||
|
||
TextStyle get title => InkwatchText.title.copyWith(color: p.textPrimary);
|
||
TextStyle get section => InkwatchText.section.copyWith(color: p.textPrimary);
|
||
TextStyle get body => InkwatchText.body.copyWith(color: p.textPrimary);
|
||
TextStyle get bodyMuted =>
|
||
InkwatchText.bodyMuted.copyWith(color: p.textSecondary);
|
||
TextStyle get caption => InkwatchText.caption.copyWith(color: p.textTertiary);
|
||
TextStyle get micro => InkwatchText.micro.copyWith(color: p.textTertiary);
|
||
TextStyle get metric => InkwatchText.metric.copyWith(color: p.textPrimary);
|
||
TextStyle get metricUnit =>
|
||
InkwatchText.metricUnit.copyWith(color: p.textTertiary);
|
||
TextStyle get stat => InkwatchText.stat.copyWith(color: p.textPrimary);
|
||
}
|
||
|
||
/// 水墨屏专用 Material 主题。
|
||
///
|
||
/// 与彩色版的三点不同,都是为了**减少刷新**:
|
||
/// 1. `splashFactory: NoSplash` —— 去掉点击水波纹(那是每帧重绘的动画);
|
||
/// 2. 全部 `elevation: 0` —— 阴影在墨水屏上只会变成一圈脏灰;
|
||
/// 3. 颜色走 [InkwatchEink] 三档明度。
|
||
///
|
||
/// **只在 `theme.dart` 里 new ThemeData**(与彩色版同一条规矩)——
|
||
/// 页面里就地 `ThemeData(` 会被 `test/theme_guard_test.dart` 判红。
|
||
///
|
||
/// 挂 [InkwatchPalette.eink] 是必须的:看板内部若改用 [InkwatchPalette.of] 查询色板,
|
||
/// 缺了这一份会取到 null 而崩。
|
||
ThemeData einkTheme() => ThemeData(
|
||
useMaterial3: true,
|
||
extensions: const <ThemeExtension<dynamic>>[InkwatchPalette.eink],
|
||
scaffoldBackgroundColor: InkwatchEink.page,
|
||
colorScheme: ColorScheme.fromSeed(
|
||
seedColor: InkwatchEink.ink,
|
||
brightness: Brightness.light,
|
||
).copyWith(
|
||
surface: InkwatchEink.page,
|
||
onSurface: InkwatchEink.ink,
|
||
primary: InkwatchEink.ink,
|
||
onPrimary: InkwatchEink.page,
|
||
),
|
||
splashFactory: NoSplash.splashFactory,
|
||
splashColor: const Color(0x00000000),
|
||
highlightColor: const Color(0x00000000),
|
||
appBarTheme: const AppBarTheme(
|
||
backgroundColor: InkwatchEink.page,
|
||
foregroundColor: InkwatchEink.ink,
|
||
elevation: 0,
|
||
scrolledUnderElevation: 0,
|
||
centerTitle: false,
|
||
),
|
||
cardTheme: const CardThemeData(
|
||
color: InkwatchEink.box,
|
||
elevation: 0,
|
||
margin: EdgeInsets.zero,
|
||
),
|
||
dividerTheme: const DividerThemeData(
|
||
color: InkwatchEink.rule,
|
||
thickness: 1,
|
||
space: 1,
|
||
),
|
||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: InkwatchEink.ink,
|
||
side: const BorderSide(color: InkwatchEink.rule, width: 1.5),
|
||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||
textStyle: InkwatchEinkText.action,
|
||
),
|
||
),
|
||
textButtonTheme: TextButtonThemeData(
|
||
style: TextButton.styleFrom(
|
||
foregroundColor: InkwatchEink.ink,
|
||
textStyle: InkwatchEinkText.action,
|
||
),
|
||
),
|
||
snackBarTheme: SnackBarThemeData(
|
||
backgroundColor: InkwatchEink.ink,
|
||
contentTextStyle: InkwatchEinkText.label.copyWith(color: InkwatchEink.page),
|
||
behavior: SnackBarBehavior.floating,
|
||
elevation: 0,
|
||
),
|
||
);
|