design-system-structure
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesedesign-system-structure
design-system-structure
A design system is code that is incapable of holding a stray opinion: every aesthetic value lives in one directory, and every widget reads it back through a named slot. This skill owns how tokens, themes, components, and shapes are structured and consumed — not which colors or radii to pick (that belongs to your design source of truth). Getting the structure right makes a reskin a and turns a stray in feature code into a build failure instead of a style nit.
diff lib/theme/Color(0xFF…)Read the reference for the task at hand:
- — two-tier token naming, ThemeExtension mechanics, the asserting
references/token-tiers-and-themeextension.md, hand-authoredof()vsColorScheme, honestfromSeed.lerp - — the three animations Material mounts by default, why
references/motion-and-reduced-motion.mdis not enough, reduced-motion-means-zero,NoSplashbans in tests.pumpAndSettle - — bundling fonts,
references/typography-and-fonts.md,LicenseRegistrydrivesFontWeight, silentwghtno-ops, subsetting without instancing, per-script fallback cascades.FontVariation - — AA as a unit test, ≥3 grayscale-legible signals per meaning, reading a11y flags from
references/contrast-and-redundant-encoding.md.MediaQuery
Run and before a PR.
scripts/check_raw_values.shscripts/check_font_bundling.sh设计系统是一套“不允许混入游离主观意见”的代码:每个审美值都集中在同一个目录中,每个 Widget 都通过具名 slot 读回。这个 skill 负责 token、theme、component 和 shape 的结构和消费方式,而不负责决定选哪些颜色或圆角(后者属于你的设计事实来源)。结构正确后,重新换肤只需 ,功能代码中游离的 也会直接导致构建失败,而不只是留下样式瑕疵。
diff lib/theme/Color(0xFF…)针对当前任务阅读参考资料:
- ——两级 token 命名、ThemeExtension 机制、带断言的
references/token-tiers-and-themeextension.md、手写of()与ColorScheme、可靠的fromSeed。lerp - ——Material 默认挂载的三种动画、为何
references/motion-and-reduced-motion.md仍不够、reduced motion 必须归零,以及测试中禁用NoSplash。pumpAndSettle - ——字体打包、
references/typography-and-fonts.md、LicenseRegistry驱动FontWeight、静默失效的wght、无需实例化即可完成字体子集化,以及按文字系统回退的级联。FontVariation - ——将 AA 作为单元测试、每种含义至少提供 3 种灰度可辨识信号,以及从
references/contrast-and-redundant-encoding.md读取 a11y 标志。MediaQuery
提交 PR 前运行 和 。
scripts/check_raw_values.shscripts/check_font_bundling.shNon-negotiable rules
不可协商的规则
- No raw aesthetic value outside — the gate is the law. A
lib/theme/**/Color(0x…)(exceptColors.*) /transparent/Curves.*(exceptDuration(milliseconds:|seconds:) / literalDuration.zero/BorderRadius.circular(n)in feature or shared UI code failsfontSize: n. A legitimate new need is a new token slot, never ascripts/check_raw_values.sh— one place to diff is the whole point.// ignore - Two token tiers; widgets read only the semantic tier. Tier 1 = primitives named by the measured value (= OKLCH lightness ×100), never by rank (
neutral12), appearance (grey700), or brand (darkGrey). Tier 2 = semantic slots named by role (brandPrimary,surface,onSurface,hairline). A widget that reaches a Tier-1 primitive has hardcoded one theme. WHY: rank scales have no room to insert and lie in dark mode; appearance names invert catastrophically; brand names die with the brand.accent - Read tokens through a and an asserting
ThemeExtension. Never a staticof(context)class, never anconstside channel.InheritedWidgetasserts on a missing extension — it does notof(). WHY:?? fallbackdrives light/dark switching and correct-by-construction invalidation; a fallback silently ships a theme no test verified, and loud-in-debug beats wrong-in-field.MaterialApp - Hand-author ; never
ColorScheme(orColorScheme.fromSeed) for identity.dynamic_color's per-role overrides do not propagate — overridefromSeedandsurfacestays seed-derived. State every role you consume, keep the M3 role names so Material widgets theme themselves, and let unstated roles takesurfaceContainerHigh's own defaults, not a seed's opinion. WHY: "seed plus a few overrides" means owning the ~9 roles you chose and every seam of the ~40 you didn't.ColorScheme - Layer the system: tokens → theme → modifiers/components → shapes. Author light and dark by hand (dark is never an auto-flip), each passing AA independently, and attach every extension to both s. Inject the theme exactly once, at the composition root. WHY: theming the app must touch one directory; a palette-only dark theme ships light's ratios into the dark.
ThemeData - honestly or snap deliberately;
lerpis mandatory. Interpolate colors and doubles; snap non-interpolables (font families,copyWith) atShapeBorder. If your app bans theme animation and you snap everything, leave a comment — a bare step-t < 0.5reads as unfinished and the next reader will "fix" it. WHY: a new field silently forgotten inlerpis the classic design-system rot.lerp - Color is a derived token, computed LAST — never a status's only channel. Color is a pure function of a canonical value object (an enum/small class), resolved through a semantic slot at the end; status is never read from color, and the supporting color is a slot read, not a literal. The never-color-alone floor itself — ≥3 non-color signals (glyph, label, weight/position) per stateful meaning — is owned by ; this skill only enforces that the color reinforcing them is a derived slot. WHY: a color not derived from the value object drifts out of sync, and a widget that leans on color alone dies in grayscale.
accessibility-as-code - Reduced motion collapses to zero, not "gentler." Read and resolve to
MediaQuery.disableAnimationsOf(context)/ a cross-fade to the end state — never a shorter duration or a softer curve. Motion is never the only signal for a state change. WHY: a user who asked the OS to stop animations asked for stop.Duration.zero - Restore the persisted theme before first paint. Load the saved theme mode / variant before , from the same versioned settings the rest of the app uses (not an async
runAppread that lands a frame late). An unknown or corrupt stored value falls back explicitly and visibly, never to ashared_preferencesthat leavesnullasserting on a device with no debugger. WHY: a flash of the wrong theme is a sudden luminance change the user did not cause.of() - Bundle fonts; never at runtime. Declare fonts in
google_fonts, ship the license text and register it viapubspec.yaml, and drive weight withLicenseRegistry(which drives theFontWeightaxis) — not a redundantwght. Declare a per-scriptFontVariation('wght', …)cascade that ends in a known-good face. WHY:fontFamilyFallbackships an HTTP code path by default;google_fontssilently no-ops on a font that lacks the axis, so a "working" italic is invisible in review.FontVariation('opsz'|'ital', …) - Painters snapshot tokens; never reads
paint(). ResolveBuildContextonce at the widget layer and pass the values into theTheme.of(context).extension<T>()/CustomPainterfields. WHY: keepsSceneallocation-free and testable without apaint(); seeMaterialApp.custom-canvas-and-gestures - The reduced-motion flag is read from (
MediaQuery), never app state — it feedsdisableAnimationsOf(this skill's helper, below). Reading the other platform a11y flags (resolveMotion,boldTextOf,highContrastOf) fromtextScalerrather than a stale app-state copy is owned byMediaQuery.accessibility-as-code
- 之外不得存在原始审美值——门禁就是铁律。 功能代码或共享 UI 代码中出现
lib/theme/**/Color(0x…)(Colors.*除外)/transparent/Curves.*(Duration(milliseconds:|seconds:除外)/ 字面量Duration.zero/BorderRadius.circular(n),都会导致fontSize: n失败。合理的新需求应当新增 token slot,绝不能添加scripts/check_raw_values.sh——所有变更只需对比一个位置正是其核心价值。// ignore - 两级 token;Widget 只能读取语义层。 第 1 层 = 按实测值命名的 primitives(= OKLCH lightness ×100),绝不能按等级(
neutral12)、外观(grey700)或品牌(darkGrey)命名。第 2 层 = 按角色命名的 semantic slots(brandPrimary、surface、onSurface、hairline)。如果 Widget 直接访问第 1 层 primitive,就等于把一个主题写死。原因:等级色阶没有插入空间,而且在暗色模式下会“说谎”;按外观命名的色值在暗色模式下会发生灾难性反转;品牌名则会随品牌消亡。accent - 通过 和带断言的
ThemeExtension读取 token。 绝不能使用静态of(context)类,也绝不能通过const旁路读取。InheritedWidget在 extension 缺失时触发断言——绝不使用of()。原因:?? fallback负责浅色/深色切换,并从结构上保证正确地触发失效刷新;fallback 会悄悄交付一份没有任何测试验证过的主题,在调试环境中大声报错胜过在线上环境中静默出错。MaterialApp - 手写 ;具有固定视觉识别时,绝不能使用
ColorScheme(或ColorScheme.fromSeed)。dynamic_color的逐角色覆盖不会向下传播——覆盖fromSeed和surface后,其他角色仍由 seed 派生。明确写出你使用的每个角色,保留 M3 role 名称,让 Material Widget 能正确应用主题;未声明的角色采用surfaceContainerHigh自身的默认值,而不是 seed 的主观意见。原因:“seed 加少量覆盖”意味着你既要掌控所选的约 9 个角色,也要掌控未选择的约 40 个角色之间的每一处衔接。ColorScheme - 对系统分层:tokens → theme → modifiers/components → shapes。 手动编写浅色和深色主题(暗色主题绝不能自动翻转),两者各自独立通过 AA,并将每个 extension 同时附加到两个 。主题只在 composition root 注入且仅注入一次。原因:应用换肤应只触及一个目录;仅做调色板翻转的暗色主题会把浅色主题的对比度比例带进暗色模式。
ThemeData - 要么如实执行 ,要么有意按步切换;
lerp是必选项。 对颜色和 double 进行插值;对不可插值项(字体家族、copyWith)在ShapeBorder时按步切换。如果应用禁止主题动画并对所有内容执行按步切换,请留下注释——裸露的阶梯式t < 0.5看起来像尚未完成,下一位读者会把它“修好”。原因:在lerp中悄悄遗漏新字段,是设计系统腐化的经典问题。lerp - Color 是最后计算的派生 token,绝不能成为状态的唯一通道。 Color 是规范值对象(enum 或小型 class)的纯函数,并在最后通过 semantic slot 解析;绝不能从 Color 反推状态,辅助 Color 也必须读取 slot,而不能是字面量。每个状态含义至少包含 3 种非颜色信号这一下限由 负责;本 skill 只强制要求用于强化这些信号的 Color 同样是派生 slot。原因:如果 Color 不是从值对象派生,它就会逐渐失步;只依赖 Color 的 Widget 在灰度下会失效。
accessibility-as-code - Reduced motion 必须归零,而不只是“更温和”。 读取 ,并解析为
MediaQuery.disableAnimationsOf(context)/ 淡入淡出至最终状态——绝不能只缩短时长或改用更柔和的曲线。Motion 绝不能成为状态变化的唯一信号。原因:当用户要求操作系统停止动画时,他们要求的就是停止。Duration.zero - 在首次绘制前恢复持久化主题。 在 之前,从应用其余部分所用的同一版本化设置中加载已保存的 theme mode / variant(不能使用晚一帧才完成的异步
runApp读取)。对于未知或损坏的存储值,必须显式且可见地回退,绝不能回退到shared_preferences,否则在无调试器的设备上会导致null触发断言。原因:错误主题闪现会造成用户并未主动触发的突发亮度变化。of() - 打包字体;运行时绝不能使用 。 在
google_fonts中声明字体,随包提供许可证文本并通过pubspec.yaml注册,使用LicenseRegistry驱动字重(它会驱动FontWeight轴),而不是重复使用wght。声明按文字系统区分的FontVariation('wght', …)级联,并以已知可用的字体结尾。原因:fontFamilyFallback默认包含 HTTP 代码路径;google_fonts在缺少相应轴的字体上会静默失效,因此所谓的“可用斜体”在评审中根本看不出来。FontVariation('opsz'|'ital', …) - Painter 对 token 做快照;绝不读取
paint()。 在 Widget 层一次性解析BuildContext,再把值传入Theme.of(context).extension<T>()/CustomPainter的字段。原因:这样可以让Scene无需内存分配,并且无需paint()也能测试;参见MaterialApp。custom-canvas-and-gestures - Reduced-motion 标志必须来自 (
MediaQuery),绝不能来自应用状态。 它会驱动下文的disableAnimationsOfhelper。其他平台 a11y 标志(resolveMotion、boldTextOf、highContrastOf)也应从textScaler而不是过期的应用状态副本中读取;这由MediaQuery负责。accessibility-as-code
The token layering
Token 分层
tokens → theme → modifiers → shapesThemeExtensionThemeDataColorSchemedart
// lib/theme/primitives.dart — TIER 1. The ONLY file allowed a raw color literal.
// Named by MEASURED value; values are placeholders owned by your design authority.
abstract final class Primitives {
static const neutral08 = Color(0xFF141414); // L .08 — ink
static const neutral96 = Color(0xFFF4F4F4); // L .96 — paper
static const neutral40 = Color(0xFF636363);
static const accent45 = Color(0xFF3A6FF0);
}
// lib/theme/app_colors.dart — TIER 2. Widgets read THESE, never a primitive.
class AppColors extends ThemeExtension<AppColors> {
const AppColors({required this.surface, required this.onSurface, required this.accent});
final Color surface, onSurface, accent;
static AppColors of(BuildContext context) {
final ext = Theme.of(context).extension<AppColors>();
assert(ext != null, 'AppColors missing. Build ThemeData via buildAppTheme().');
return ext!; // assert, never `?? fallback` — a fallback ships an unverified theme.
}
// copyWith + lerp omitted here — see examples/app_theme.dart for the full class.
}Full worked file (primitives, a shapes extension with a factory, the hand-authored , honest , and one-shot injection): .
cardShapeColorSchemelerpexamples/app_theme.darttokens → theme → modifiers → shapesThemeExtensionThemeDataColorSchemedart
// lib/theme/primitives.dart — TIER 1. The ONLY file allowed a raw color literal.
// Named by MEASURED value; values are placeholders owned by your design authority.
abstract final class Primitives {
static const neutral08 = Color(0xFF141414); // L .08 — ink
static const neutral96 = Color(0xFFF4F4F4); // L .96 — paper
static const neutral40 = Color(0xFF636363);
static const accent45 = Color(0xFF3A6FF0);
}
// lib/theme/app_colors.dart — TIER 2. Widgets read THESE, never a primitive.
class AppColors extends ThemeExtension<AppColors> {
const AppColors({required this.surface, required this.onSurface, required this.accent});
final Color surface, onSurface, accent;
static AppColors of(BuildContext context) {
final ext = Theme.of(context).extension<AppColors>();
assert(ext != null, 'AppColors missing. Build ThemeData via buildAppTheme().');
return ext!; // assert, never `?? fallback` — a fallback ships an unverified theme.
}
// copyWith + lerp omitted here — see examples/app_theme.dart for the full class.
}完整示例文件(primitives、带 factory 的 shapes extension、手写的 、可靠的 ,以及一次性注入)见 。
cardShapeColorSchemelerpexamples/app_theme.dartHand-author ColorScheme
, keep the M3 role names
ColorScheme手写 ColorScheme
,保留 M3 role 名称
ColorSchemedart
// WRONG — surfaceContainerHigh, surfaceDim, surfaceBright etc. stay seed-derived;
// your `surface` override does not propagate to them.
ColorScheme.fromSeed(seedColor: Primitives.accent45, surface: c.surface);
// RIGHT — state every role you read. Unstated roles get ColorScheme's defaults,
// not a seed's opinion, and the M3 names mean Material widgets theme themselves.
ColorScheme(
brightness: brightness,
primary: c.accent, onPrimary: c.surface,
surface: c.surface, onSurface: c.onSurface,
surfaceContainerHighest: c.surface, onSurfaceVariant: c.onSurfaceMuted,
outline: c.hairline, error: c.danger, onError: c.surface,
);A Material reads , , , and on its own — name the roles right and it themes with zero per-widget patching that would drift out of sync on the next theme change.
TextFieldsurfaceContainerHighestonSurfaceVariantoutlineerrorInputDecorationdart
// WRONG — surfaceContainerHigh, surfaceDim, surfaceBright etc. stay seed-derived;
// your `surface` override does not propagate to them.
ColorScheme.fromSeed(seedColor: Primitives.accent45, surface: c.surface);
// RIGHT — state every role you read. Unstated roles get ColorScheme's defaults,
// not a seed's opinion, and the M3 names mean Material widgets theme themselves.
ColorScheme(
brightness: brightness,
primary: c.accent, onPrimary: c.surface,
surface: c.surface, onSurface: c.onSurface,
surfaceContainerHighest: c.surface, onSurfaceVariant: c.onSurfaceMuted,
outline: c.hairline, error: c.danger, onError: c.surface,
);Material 会自行读取 、、 和 ——只要角色命名正确,它就能自动套用主题,无需对每个 Widget 单独修补 ,也就不会在下次主题变更时发生失步。
TextFieldsurfaceContainerHighestonSurfaceVariantoutlineerrorInputDecorationReduced motion: read the flag, collapse to zero
Reduced motion:读取标志并归零
dart
// The one place a widget asks "should I animate?" — collapses under the OS flag.
Duration resolveMotion(BuildContext context, Duration full) =>
MediaQuery.disableAnimationsOf(context) ? Duration.zero : full;
AnimatedContainer(
duration: resolveMotion(context, AppMotion.of(context).medium), // token, not 300ms
curve: AppMotion.of(context).enter, // token, not Curves.easeOut
color: AppColors.of(context).surface,
child: child,
);NoSplash.splashFactoryInkHighlightMaterialAppThemeDatakThemeAnimationDurationthemeAnimationStyle: AnimationStyle.noAnimationreferences/motion-and-reduced-motion.mddart
// The one place a widget asks "should I animate?" — collapses under the OS flag.
Duration resolveMotion(BuildContext context, Duration full) =>
MediaQuery.disableAnimationsOf(context) ? Duration.zero : full;
AnimatedContainer(
duration: resolveMotion(context, AppMotion.of(context).medium), // token, not 300ms
curve: AppMotion.of(context).enter, // token, not Curves.easeOut
color: AppColors.of(context).surface,
child: child,
);仅设置 并不够——按压高亮的淡出是独立的 ;而且,除非传入 ,否则 会在 期间插值 。三个开关的完整说明见 。
NoSplash.splashFactoryInkHighlightthemeAnimationStyle: AnimationStyle.noAnimationMaterialAppkThemeAnimationDurationThemeDatareferences/motion-and-reduced-motion.mdRestore the theme before first paint
在首次绘制前恢复主题
dart
// bootstrap: settings are loaded BEFORE runApp, so the first frame is correct.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final settings = await Settings.load(); // synchronous by the time runApp runs
final mode = ThemeVariant.tryParse(settings.themeName) // unknown/corrupt -> explicit default
?? ThemeVariant.system;
runApp(App(initialVariant: mode));
}Persisting the choice but reading it back asynchronously (a bare read) lands a frame late and flashes the wrong theme. covers threading the resolved variant through a ; covers the ordering.
shared_preferencesstate-management-riverpodthemeModeProviderapp-startup-and-bootstrapmain()dart
// bootstrap: settings are loaded BEFORE runApp, so the first frame is correct.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final settings = await Settings.load(); // synchronous by the time runApp runs
final mode = ThemeVariant.tryParse(settings.themeName) // unknown/corrupt -> explicit default
?? ThemeVariant.system;
runApp(App(initialVariant: mode));
}如果持久化了用户选择,却通过异步方式读回(直接使用裸 读取),结果会晚一帧才生效并闪现错误主题。 介绍如何把解析后的 variant 通过 传递下去; 介绍 中恢复主题所需的顺序。
shared_preferencesstate-management-riverpodthemeModeProviderapp-startup-and-bootstrapmain()Redundant encoding: derive color last
冗余编码:最后派生 Color
The ≥3-non-color-signals floor is owned by ; the design-system contribution shown here is narrower — the supporting color is derived last from a canonical value object through a semantic slot, never read as the source of state:
accessibility-as-codedart
enum OrderStatus { pending, active, blocked }
extension OrderStatusView on OrderStatus {
IconData get icon => switch (this) { // signal 1: shape differs per status
OrderStatus.pending => Icons.schedule,
OrderStatus.active => Icons.play_arrow,
OrderStatus.blocked => Icons.error_outline,
};
String label(AppLocalizations l) => switch (this) { … }; // signal 2: the word
double get emphasis => this == OrderStatus.blocked ? 2 : 1; // signal 3: monotonic weight
Color color(AppColors c) => switch (this) { // color is DECORATION, derived LAST
OrderStatus.pending => c.onSurfaceMuted,
OrderStatus.active => c.accent,
OrderStatus.blocked => c.danger,
};
}Full chip widget rendering all three signals: .
examples/status_encoding.dart每个状态含义至少包含 3 种非颜色信号这一下限由 负责;这里展示的设计系统贡献范围更窄——辅助 Color 必须在最后从规范值对象经 semantic slot 派生出来,绝不能被当作状态的来源:
accessibility-as-codedart
enum OrderStatus { pending, active, blocked }
extension OrderStatusView on OrderStatus {
IconData get icon => switch (this) { // signal 1: shape differs per status
OrderStatus.pending => Icons.schedule,
OrderStatus.active => Icons.play_arrow,
OrderStatus.blocked => Icons.error_outline,
};
String label(AppLocalizations l) => switch (this) { … }; // signal 2: the word
double get emphasis => this == OrderStatus.blocked ? 2 : 1; // signal 3: monotonic weight
Color color(AppColors c) => switch (this) { // color is DECORATION, derived LAST
OrderStatus.pending => c.onSurfaceMuted,
OrderStatus.active => c.accent,
OrderStatus.blocked => c.danger,
};
}完整 chip Widget 会渲染全部三种信号,见 。
examples/status_encoding.dartAnti-patterns
反模式
- ,
Color(0xFF…),Colors.blueGrey,Curves.easeOut,Duration(milliseconds: 200), orBorderRadius.circular(16)in a widget outsidefontSize: 14— the gate fails; the fix is a slot read or a new slot, neverlib/theme/.// ignore - A static /
AppColorsDimensclass, orconstreturningAppColors.of— slots ride a?? AppColors.fallback(), and a fallback ships a palette no test ever verified.ThemeExtension - for a product with an identity, or adding
ColorScheme.fromSeed(...)— the overrides don't propagate and wallpaper-derived palettes are untestable at build time.dynamic_color - A palette-only dark theme, or extensions attached to the light only — dark silently renders defaults and theme switches snap.
ThemeData - A that returns
lerpunlabeled, or that forgets a newly added field — the theme snaps or a slot goes un-interpolated forever.this - Color as the only status channel — recoloring is fine; removing the glyph/label/weight it accompanies is a bug that vanishes in grayscale.
- Choosing a shorter duration or softer curve under reduced motion — collapse to ; the user asked for stop.
Duration.zero - A bare read for theme, corrected a frame after
shared_preferences— a flash of the wrong theme is a luminance jolt; restore before first paint.runApp - or
import 'package:google_fonts/…'— the first ships an HTTP path; the second no-ops silently on a font without the axis.FontVariation('opsz'|'ital', …) - A calling
CustomPainterinsideTheme.of(context)— snapshot tokens into painter fields at the widget layer.paint() - A token pipeline (DTCG JSON, Style Dictionary, Figma sync codegen) for a small single-package app — it buys a build-failure mode to solve a designer/engineer handoff that, when it's the same person, does not exist.
node_modules
- 、
Color(0xFF…)、Colors.blueGrey、Curves.easeOut、Duration(milliseconds: 200)或BorderRadius.circular(16)出现在fontSize: 14之外的 Widget 中——门禁会失败;修复方式是读取 slot 或新增 slot,绝不能使用lib/theme/。// ignore - 静态 /
AppColorsDimens类,或const返回AppColors.of——slot 应依附于?? AppColors.fallback();fallback 会交付一份从未经过测试验证的调色板。ThemeExtension - 对具有固定视觉识别的产品使用 ,或加入
ColorScheme.fromSeed(...)——覆盖项不会传播,而壁纸生成的调色板无法在构建时进行测试。dynamic_color - 只有调色板的暗色主题,或 extension 仅附加到浅色 ——暗色模式会悄悄渲染默认值,主题切换也会发生跳变。
ThemeData - 未加说明便返回
lerp,或忘记新增字段——主题会跳变,或某个 slot 永远得不到插值。this - Color 是状态的唯一通道——重新着色没有问题,但移除与其一同出现的 glyph/label/weight 是会在灰度环境中被掩盖的缺陷。
- 在 reduced motion 下选择更短的时长或更柔和的曲线——应归零为 ;用户要求的是停止。
Duration.zero - 用裸 读取主题,并在
shared_preferences后一帧才修正——错误主题闪现会造成一次亮度冲击;必须在首次绘制前恢复。runApp - 或
import 'package:google_fonts/…'——前者包含 HTTP 路径;后者在字体缺少相应轴时会静默失效。FontVariation('opsz'|'ital', …) - 在
CustomPainter内调用paint()——应在 Widget 层把 token 快照传入 Painter。Theme.of(context) - 为小型单包应用引入 token pipeline(DTCG JSON、Style Dictionary、Figma sync codegen)——它会引入一种 构建失败模式,去解决一个当设计师与工程师本就是同一个人时根本不存在的交接问题。
node_modules
Definition of done
完成定义
- and
scripts/check_raw_values.share clean overscripts/check_font_bundling.sh(raw values confined tolib/; nolib/theme/**).google_fonts - Every aesthetic value a widget renders traces to a named semantic slot on a , read via an asserting
ThemeExtension.of(context) - Tokens are two-tier: primitives named by measured value, semantic slots named by role; no widget reads a primitive.
- is hand-authored for both brightnesses (never
ColorSchemefor identity); M3 role names kept; every extension attached to bothfromSeeds.ThemeData - Both themes pass AA independently, verified as a unit test over declared fg/bg pairs (
computeLuminance()).references/contrast-and-redundant-encoding.md - implemented;
copyWithinterpolates colors/doubles and snaps non-interpolables (or is a commented deliberate step).lerp - Every stateful meaning carries ≥3 grayscale-legible signals; a greyscale golden still answers "what state is this?".
- Every animation reads a motion token and collapses to zero under .
MediaQuery.disableAnimationsOf - The persisted theme is restored before first paint; unknown values fall back explicitly, never to .
null - Fonts are bundled and license-registered; per-script declared; no runtime font fetch.
fontFamilyFallback - Painters receive a token snapshot; no in
Theme.of(context).paint()
- 和
scripts/check_raw_values.sh对scripts/check_font_bundling.sh检查无报错(原始值仅限lib/;不存在lib/theme/**)。google_fonts - Widget 渲染的每个审美值都能追溯到 上具名的 semantic slot,并通过带断言的
ThemeExtension读取。of(context) - Token 分为两级:primitives 按实测值命名,semantic slots 按角色命名;任何 Widget 都不读取 primitive。
- 为两种 brightness 手动编写(固定视觉识别时绝不使用
ColorScheme);保留 M3 role 名称;每个 extension 都同时附加到两个fromSeed。ThemeData - 两种主题各自独立通过 AA,并通过针对已声明前景色/背景色组合的 单元测试进行验证(
computeLuminance())。references/contrast-and-redundant-encoding.md - 已实现;
copyWith会插值颜色/double,并对不可插值项执行按步切换(或明确注明这是有意为之的阶梯行为)。lerp - 每种状态含义都带有至少 3 种灰度可辨识信号;灰度 golden 仍能回答“当前是什么状态?”
- 每个动画都读取 motion token,并在 下归零。
MediaQuery.disableAnimationsOf - 持久化主题在首次绘制前恢复;未知值显式回退,绝不回退到 。
null - 字体已打包并注册许可证;已声明按文字系统区分的 ;不存在运行时字体获取。
fontFamilyFallback - Painter 接收 token 快照;中没有
paint()。Theme.of(context)
Related skills
相关 skill
- See for reading a11y state from
accessibility-as-code, the never-color-alone rule, target sizes, and never clampingMediaQuery.textScaler - See for the painter that consumes the token snapshot.
custom-canvas-and-gestures - See for
flutter-performancesubtrees,constrebuild scoping, and.select.RepaintBoundary - See for building views from small
widget-compositionwidgets that read these slots.const - See for the
state-management-riverpodand composition-root injection.themeModeProvider - See for the
app-startup-and-bootstrapordering that restores the theme beforemain().runApp - See for per-script fonts, directional geometry, and RTL goldens.
i18n-rtl-l10n - See for promoting the no-raw-values grep into a CI gate.
lint-and-style-config - See for what the motion tokens are spent on: the moment catalog, the haptic event map, and the degradation each moment declares.
motion-and-haptics - See for the once-per-app screenshot sweep that judges the assembled system.
design-review-workflow
- 参见 ,了解如何从
accessibility-as-code读取 a11y 状态、禁止仅用颜色传达信息、目标尺寸,以及绝不限制MediaQuery。textScaler - 参见 ,了解消费 token 快照的 Painter。
custom-canvas-and-gestures - 参见 ,了解
flutter-performance子树、const重建范围和.select。RepaintBoundary - 参见 ,了解如何构建读取这些 slot 的小型
widget-compositionWidget。const - 参见 ,了解
state-management-riverpod和 composition-root 注入。themeModeProvider - 参见 ,了解在
app-startup-and-bootstrap前恢复主题的runApp顺序。main() - 参见 ,了解按文字系统区分的字体、方向性几何结构和 RTL golden。
i18n-rtl-l10n - 参见 ,了解如何把 no-raw-values grep 提升为 CI 门禁。
lint-and-style-config - 参见 ,了解 motion token 的用途:moment catalog、haptic event map,以及每个 moment 声明的降级行为。
motion-and-haptics - 参见 ,了解针对整个应用执行一次的截图审查流程,该流程用于评判组装后的系统。
design-review-workflow
References
参考资料
- Flutter API — : https://api.flutter.dev/flutter/material/ThemeExtension-class.html
ThemeExtension - Flutter API — (
ThemeData, component themes,extensions): https://api.flutter.dev/flutter/material/ThemeData-class.htmlcolorScheme - Flutter API — (roles;
ColorSchemecaveats): https://api.flutter.dev/flutter/material/ColorScheme-class.htmlfromSeed - Flutter cookbook — Use themes to share colors and font styles: https://docs.flutter.dev/cookbook/design/themes
- Flutter cookbook — Use a custom font (bundling, ): https://docs.flutter.dev/cookbook/design/fonts
fontFamilyFallback - Flutter API — /
MediaQueryData.disableAnimations: https://api.flutter.dev/flutter/widgets/MediaQueryData-class.htmltextScaler - Flutter API — (WCAG relative luminance): https://api.flutter.dev/flutter/dart-ui/Color/computeLuminance.html
Color.computeLuminance - Flutter API — : https://api.flutter.dev/flutter/foundation/LicenseRegistry-class.html
LicenseRegistry - Material Design 3 — design tokens: https://m3.material.io/foundations/design-tokens/overview
- W3C WAI — WCAG 2.2 SC 1.4.3 Contrast (Minimum): https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
- W3C WAI — WCAG 2.2 SC 1.4.1 Use of Color: https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html
- Flutter API — :https://api.flutter.dev/flutter/material/ThemeExtension-class.html
ThemeExtension - Flutter API — (
ThemeData、component themes、extensions):https://api.flutter.dev/flutter/material/ThemeData-class.htmlcolorScheme - Flutter API — (角色;
ColorScheme的注意事项):https://api.flutter.dev/flutter/material/ColorScheme-class.htmlfromSeed - Flutter cookbook — 使用主题共享颜色和字体样式:https://docs.flutter.dev/cookbook/design/themes
- Flutter cookbook — 使用自定义字体(打包、):https://docs.flutter.dev/cookbook/design/fonts
fontFamilyFallback - Flutter API — /
MediaQueryData.disableAnimations:https://api.flutter.dev/flutter/widgets/MediaQueryData-class.htmltextScaler - Flutter API — (WCAG 相对亮度):https://api.flutter.dev/flutter/dart-ui/Color/computeLuminance.html
Color.computeLuminance - Flutter API — :https://api.flutter.dev/flutter/foundation/LicenseRegistry-class.html
LicenseRegistry - Material Design 3 — design tokens:https://m3.material.io/foundations/design-tokens/overview
- W3C WAI — WCAG 2.2 SC 1.4.3 Contrast (Minimum):https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
- W3C WAI — WCAG 2.2 SC 1.4.1 Use of Color:https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html