feat(client): codegen 单源生成三主题 AppTokens(禁手写 hex)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
|
||||
// 源:lib/core/theme/token_source/tokens.css(从原型同步)。改色请改源后重跑脚本。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_tokens.dart';
|
||||
|
||||
const AppTokens kTokensA = AppTokens(
|
||||
page: Color(0xFFE7EBF1),
|
||||
bg: Color(0xFFF5F7FA),
|
||||
surface: Color(0xFFFFFFFF),
|
||||
border: Color(0xFFDCE2EB),
|
||||
borderSubtle: Color(0xFFECEFF4),
|
||||
text: Color(0xFF232934),
|
||||
heading: Color(0xFF1B2430),
|
||||
title: Color(0xFF1B2430),
|
||||
muted: Color(0xFF6E7888),
|
||||
faint: Color(0xFF99A3B3),
|
||||
primary: Color(0xFF2563AC),
|
||||
primaryDark: Color(0xFF154072),
|
||||
onPrimary: Color(0xFFFFFFFF),
|
||||
brand400: Color(0xFF4F86C6),
|
||||
brand50: Color(0xFFEEF4FB),
|
||||
accent: Color(0xFF8B2331),
|
||||
success: Color(0xFF2E8B57),
|
||||
danger: Color(0xFFD14343),
|
||||
warn: Color(0xFFE08E00),
|
||||
infoSoft: Color(0xFFEEF4FB),
|
||||
okSoft: Color(0xFFE8F5EE),
|
||||
accentSoft: Color(0xFFFAEEF0),
|
||||
successBg: Color(0xFFE8F5EE),
|
||||
dangerBg: Color(0xFFFDECEC),
|
||||
warnBg: Color(0xFFFFF4DB),
|
||||
thBg: Color(0xFFF7F9FC),
|
||||
rowHover: Color(0xFFEEF4FB),
|
||||
shadow: Color(0x33142846),
|
||||
topBg: Color(0xFF2563AC),
|
||||
topFg: Color(0xFFFFFFFF),
|
||||
topFaint: Color(0xb8FFFFFF),
|
||||
topBorder: Color(0xFF1E5594),
|
||||
topControl: Color(0x29FFFFFF),
|
||||
topMarkBg: Color(0x38FFFFFF),
|
||||
topMarkFg: Color(0xFFFFFFFF),
|
||||
sideBg: Color(0xFF163A6C),
|
||||
sideFg: Color(0xFFC4D6ED),
|
||||
sideMuted: Color(0xFF7C9FCB),
|
||||
sideActiveBg: Color(0xFF2C6BB3),
|
||||
sideActiveFg: Color(0xFFFFFFFF),
|
||||
sideBorder: Color(0xFF1E4A86),
|
||||
sideHover: Color(0xFF1C4680),
|
||||
menuUserBg: Color(0xFF3C79C0),
|
||||
menuUserFg: Color(0xFFFFFFFF),
|
||||
menuUserHover: Color(0xFF4D88CC),
|
||||
isDark: false,
|
||||
);
|
||||
|
||||
const AppTokens kTokensB = AppTokens(
|
||||
page: Color(0xFF0B0B10),
|
||||
bg: Color(0xFF15151C),
|
||||
surface: Color(0xFF1E1E27),
|
||||
border: Color(0xFF2B2B37),
|
||||
borderSubtle: Color(0xFF26262F),
|
||||
text: Color(0xFFE8E8EE),
|
||||
heading: Color(0xFFF6F6F9),
|
||||
title: Color(0xFFF6F6F9),
|
||||
muted: Color(0xFF9A9AA8),
|
||||
faint: Color(0xFF6E6E7E),
|
||||
primary: Color(0xFFF5A300),
|
||||
primaryDark: Color(0xFFD98C00),
|
||||
onPrimary: Color(0xFF1A1408),
|
||||
brand400: Color(0xFFE0A23A),
|
||||
brand50: Color(0xFF2A2212),
|
||||
accent: Color(0xFFF87171),
|
||||
success: Color(0xFF34D399),
|
||||
danger: Color(0xFFF87171),
|
||||
warn: Color(0xFFFBBF24),
|
||||
infoSoft: Color(0xFF1F2530),
|
||||
okSoft: Color(0xFF16312A),
|
||||
accentSoft: Color(0xFF33181A),
|
||||
successBg: Color(0xFF16312A),
|
||||
dangerBg: Color(0xFF3A1A1C),
|
||||
warnBg: Color(0xFF332811),
|
||||
thBg: Color(0xFF1A1A22),
|
||||
rowHover: Color(0xFF24242F),
|
||||
shadow: Color(0x8c000000),
|
||||
topBg: Color(0xFF1A1A22),
|
||||
topFg: Color(0xFFE8E8EE),
|
||||
topFaint: Color(0xFF6E6E7E),
|
||||
topBorder: Color(0xFF2B2B37),
|
||||
topControl: Color(0xFF15151C),
|
||||
topMarkBg: Color(0xFFF5A300),
|
||||
topMarkFg: Color(0xFF1A1408),
|
||||
sideBg: Color(0xFF121219),
|
||||
sideFg: Color(0xFF9A9AA8),
|
||||
sideMuted: Color(0xFF5E5E6C),
|
||||
sideActiveBg: Color(0xFF2A2212),
|
||||
sideActiveFg: Color(0xFFF5A300),
|
||||
sideBorder: Color(0xFF24242E),
|
||||
sideHover: Color(0xFF1E1E27),
|
||||
menuUserBg: Color(0xFF3A3120),
|
||||
menuUserFg: Color(0xFFF0E7D6),
|
||||
menuUserHover: Color(0xFF473B27),
|
||||
isDark: true,
|
||||
);
|
||||
|
||||
const AppTokens kTokensC = AppTokens(
|
||||
page: Color(0xFFE9DECB),
|
||||
bg: Color(0xFFFAF5EC),
|
||||
surface: Color(0xFFFFFDF8),
|
||||
border: Color(0xFFEADDC9),
|
||||
borderSubtle: Color(0xFFF1E8D8),
|
||||
text: Color(0xFF33271F),
|
||||
heading: Color(0xFF241A14),
|
||||
title: Color(0xFF9E3B30),
|
||||
muted: Color(0xFF917A64),
|
||||
faint: Color(0xFFB9A488),
|
||||
primary: Color(0xFF7C1F2C),
|
||||
primaryDark: Color(0xFF5F1621),
|
||||
onPrimary: Color(0xFFF7E9D6),
|
||||
brand400: Color(0xFFB5862F),
|
||||
brand50: Color(0xFFF3E7D2),
|
||||
accent: Color(0xFFA23522),
|
||||
success: Color(0xFF2E6B43),
|
||||
danger: Color(0xFFB23A3A),
|
||||
warn: Color(0xFFB07A12),
|
||||
infoSoft: Color(0xFFF1E6D2),
|
||||
okSoft: Color(0xFFE7F0E6),
|
||||
accentSoft: Color(0xFFF3E0D5),
|
||||
successBg: Color(0xFFE7F0E6),
|
||||
dangerBg: Color(0xFFF6E4DD),
|
||||
warnBg: Color(0xFFFBEFCF),
|
||||
thBg: Color(0xFFF6EEDF),
|
||||
rowHover: Color(0xFFF5ECDB),
|
||||
shadow: Color(0x384A1420),
|
||||
topBg: Color(0xFF7C1F2C),
|
||||
topFg: Color(0xFFF7E9D6),
|
||||
topFaint: Color(0xb8F7E9D6),
|
||||
topBorder: Color(0xFF671E2C),
|
||||
topControl: Color(0x24FFFFFF),
|
||||
topMarkBg: Color(0x33FFFFFF),
|
||||
topMarkFg: Color(0xFFF7E9D6),
|
||||
sideBg: Color(0xFF4A1420),
|
||||
sideFg: Color(0xFFE0C9B2),
|
||||
sideMuted: Color(0xFFB58E76),
|
||||
sideActiveBg: Color(0xFF671E2C),
|
||||
sideActiveFg: Color(0xFFF7E9D6),
|
||||
sideBorder: Color(0xFF5A2330),
|
||||
sideHover: Color(0xFF5A1B28),
|
||||
menuUserBg: Color(0xFF7E2B3B),
|
||||
menuUserFg: Color(0xFFF7E9D6),
|
||||
menuUserHover: Color(0xFF8E3344),
|
||||
isDark: false,
|
||||
);
|
||||
|
||||
AppTokens appTokensOf(String key) {
|
||||
switch (key) {
|
||||
case 'b': return kTokensB;
|
||||
case 'c': return kTokensC;
|
||||
default: return kTokensA;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/* ============================================================
|
||||
岩美酒库 · 令牌层(单一真源)
|
||||
颜色按主题切换;间距/圆角/阴影/字号 全主题统一。
|
||||
规则:屏幕与组件只引用 var(--token),禁硬编码 hex。
|
||||
============================================================ */
|
||||
|
||||
:root{
|
||||
/* 间距(4px 基) */
|
||||
--sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px; --sp-6:24px; --sp-8:32px; --sp-10:40px;
|
||||
/* 圆角 */
|
||||
--r-sm:4px; --r-md:6px; --r-lg:10px; --r-xl:14px; --r-pill:999px;
|
||||
/* 字号梯度 */
|
||||
--fs-display:28px; --fs-h1:21px; --fs-h2:17px; --fs-title:15px; --fs-body:13px; --fs-sm:12px; --fs-xs:11px;
|
||||
/* 字体族 */
|
||||
--font:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei","Inter",sans-serif;
|
||||
--font-mono:"JetBrains Mono","SF Mono",ui-monospace,monospace;
|
||||
/* 阴影系统(用各主题 --shadow 染色) */
|
||||
--sh-1:0 1px 3px var(--shadow); --sh-2:0 4px 14px var(--shadow); --sh-3:0 18px 50px var(--shadow);
|
||||
/* 遮罩 / 玻璃叠加(主题无关:弹窗·抽屉背板、hero 上的半透明白) */
|
||||
--scrim:rgba(15,20,30,.42); --hero-glass:rgba(255,255,255,.16); --hero-glass-strong:rgba(255,255,255,.24);
|
||||
/* 品牌标识色(logo / 门店字标,固定不随主题) */
|
||||
--brand-deep:#0F3057; --brand-mid:#1B4F8E; --brand-wine:#8B2331;
|
||||
/* 底部状态栏(深石板灰绿,主题无关的 chrome 条) */
|
||||
--statusbar-bg:#37423D; --statusbar-fg:rgba(255,255,255,.82); --statusbar-muted:rgba(255,255,255,.46); --statusbar-strong:#FFFFFF; --statusbar-div:rgba(255,255,255,.13); --statusbar-ok:#7BC49A;
|
||||
/* Toast 深底(主题无关) */
|
||||
--toast-bg:#1F2330;
|
||||
/* z-index 层级阶梯(全局唯一来源,禁止裸写 z-index≥10) */
|
||||
--z-sticky:20; --z-shell-scrim:30; --z-shell-panel:35; --z-header:40; --z-fab:45;
|
||||
--z-drawer-scrim:65; --z-drawer:70; --z-overlay:80; --z-pop:85; --z-menu:88; --z-toast:90;
|
||||
}
|
||||
|
||||
/* ---------- A 经典蓝(浅 · 基准/默认) ---------- */
|
||||
:root[data-theme="a"]{
|
||||
--page:#E7EBF1; --bg:#F5F7FA; --surface:#FFFFFF; --border:#DCE2EB; --border-subtle:#ECEFF4;
|
||||
--text:#232934; --heading:#1B2430; --title:#1B2430; --muted:#6E7888; --faint:#99A3B3;
|
||||
--primary:#2563AC; --primary-dark:#154072; --on-primary:#FFFFFF; --brand400:#4F86C6; --brand50:#EEF4FB; --accent:#8B2331;
|
||||
--success:#2E8B57; --danger:#D14343; --warn:#E08E00;
|
||||
--info-soft:#EEF4FB; --ok-soft:#E8F5EE; --accent-soft:#FAEEF0;
|
||||
--success-bg:#E8F5EE; --danger-bg:#FDECEC; --warn-bg:#FFF4DB;
|
||||
--th-bg:#F7F9FC; --row-hover:#EEF4FB; --shadow:rgba(20,40,70,.20);
|
||||
--top-bg:#2563AC; --top-fg:#FFFFFF; --top-faint:rgba(255,255,255,.72); --top-border:#1E5594; --top-control:rgba(255,255,255,.16); --top-mark-bg:rgba(255,255,255,.22); --top-mark-fg:#FFFFFF;
|
||||
--side-bg:#163A6C; --side-fg:#C4D6ED; --side-muted:#7C9FCB; --side-active-bg:#2C6BB3; --side-active-fg:#FFFFFF; --side-border:#1E4A86; --side-hover:#1C4680;
|
||||
--menu-user-bg:#3C79C0; --menu-user-fg:#FFFFFF; --menu-user-hover:#4D88CC;
|
||||
}
|
||||
/* ---------- B 琥珀(深) ---------- */
|
||||
:root[data-theme="b"]{
|
||||
--page:#0B0B10; --bg:#15151C; --surface:#1E1E27; --border:#2B2B37; --border-subtle:#26262F;
|
||||
--text:#E8E8EE; --heading:#F6F6F9; --title:#F6F6F9; --muted:#9A9AA8; --faint:#6E6E7E;
|
||||
--primary:#F5A300; --primary-dark:#D98C00; --on-primary:#1A1408; --brand400:#E0A23A; --brand50:#2A2212; --accent:#F87171;
|
||||
--success:#34D399; --danger:#F87171; --warn:#FBBF24;
|
||||
--info-soft:#1F2530; --ok-soft:#16312A; --accent-soft:#33181A;
|
||||
--success-bg:#16312A; --danger-bg:#3A1A1C; --warn-bg:#332811;
|
||||
--th-bg:#1A1A22; --row-hover:#24242F; --shadow:rgba(0,0,0,.55);
|
||||
--top-bg:#1A1A22; --top-fg:#E8E8EE; --top-faint:#6E6E7E; --top-border:#2B2B37; --top-control:#15151C; --top-mark-bg:linear-gradient(135deg,#F5A300,#D98C00); --top-mark-fg:#1A1408;
|
||||
--side-bg:#121219; --side-fg:#9A9AA8; --side-muted:#5E5E6C; --side-active-bg:#2A2212; --side-active-fg:#F5A300; --side-border:#24242E; --side-hover:#1E1E27;
|
||||
--menu-user-bg:#3A3120; --menu-user-fg:#F0E7D6; --menu-user-hover:#473B27;
|
||||
}
|
||||
/* ---------- C 酒窖(暖浅) ---------- */
|
||||
:root[data-theme="c"]{
|
||||
--page:#E9DECB; --bg:#FAF5EC; --surface:#FFFDF8; --border:#EADDC9; --border-subtle:#F1E8D8;
|
||||
--text:#33271F; --heading:#241A14; --title:#9E3B30; --muted:#917A64; --faint:#B9A488;
|
||||
--primary:#7C1F2C; --primary-dark:#5F1621; --on-primary:#F7E9D6; --brand400:#B5862F; --brand50:#F3E7D2; --accent:#A23522;
|
||||
--success:#2E6B43; --danger:#B23A3A; --warn:#B07A12;
|
||||
--info-soft:#F1E6D2; --ok-soft:#E7F0E6; --accent-soft:#F3E0D5;
|
||||
--success-bg:#E7F0E6; --danger-bg:#F6E4DD; --warn-bg:#FBEFCF;
|
||||
--th-bg:#F6EEDF; --row-hover:#F5ECDB; --shadow:rgba(74,20,32,.22);
|
||||
--top-bg:#7C1F2C; --top-fg:#F7E9D6; --top-faint:rgba(247,233,214,.72); --top-border:#671E2C; --top-control:rgba(255,255,255,.14); --top-mark-bg:rgba(255,255,255,.20); --top-mark-fg:#F7E9D6;
|
||||
--side-bg:#4A1420; --side-fg:#E0C9B2; --side-muted:#B58E76; --side-active-bg:#671E2C; --side-active-fg:#F7E9D6; --side-border:#5A2330; --side-hover:#5A1B28;
|
||||
--menu-user-bg:#7E2B3B; --menu-user-fg:#F7E9D6; --menu-user-hover:#8E3344;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// client/test/theme/app_tokens_values_test.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jiu_client/core/theme/app_tokens.g.dart';
|
||||
|
||||
void main() {
|
||||
test('A 经典蓝基准色正确', () {
|
||||
expect(kTokensA.primary, const Color(0xFF2563AC));
|
||||
expect(kTokensA.surface, const Color(0xFFFFFFFF));
|
||||
expect(kTokensA.warn, const Color(0xFFE08E00));
|
||||
expect(kTokensA.isDark, false);
|
||||
});
|
||||
test('B 琥珀为深色且主色为琥珀金', () {
|
||||
expect(kTokensB.primary, const Color(0xFFF5A300));
|
||||
expect(kTokensB.isDark, true);
|
||||
// top-mark-bg 是 linear-gradient → 取首个 hex
|
||||
expect(kTokensB.topMarkBg, const Color(0xFFF5A300));
|
||||
});
|
||||
test('C 酒窖暖浅主色为酒红', () {
|
||||
expect(kTokensC.primary, const Color(0xFF7C1F2C));
|
||||
expect(kTokensC.isDark, false);
|
||||
});
|
||||
test('shadow rgba 带 alpha 正确解析', () {
|
||||
// a: rgba(20,40,70,.20) → 0x33142846
|
||||
expect(kTokensA.shadow, const Color(0x33142846));
|
||||
});
|
||||
test('appTokensOf 回退到 A', () {
|
||||
expect(appTokensOf('x'), kTokensA);
|
||||
expect(appTokensOf('b'), kTokensB);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// client/tool/gen_tokens.mjs
|
||||
// 解析 token_source/tokens.css 的三套主题块 → 生成 app_tokens.g.dart。
|
||||
// 用法:node tool/gen_tokens.mjs (在 client/ 下运行)
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const css = fs.readFileSync(path.join(ROOT, 'lib/core/theme/token_source/tokens.css'), 'utf8');
|
||||
|
||||
// CSS 变量名(kebab) → dart 字段(camel)。仅这些进 AppTokens。
|
||||
const FIELDS = {
|
||||
'page':'page','bg':'bg','surface':'surface','border':'border','border-subtle':'borderSubtle',
|
||||
'text':'text','heading':'heading','title':'title','muted':'muted','faint':'faint',
|
||||
'primary':'primary','primary-dark':'primaryDark','on-primary':'onPrimary','brand400':'brand400',
|
||||
'brand50':'brand50','accent':'accent','success':'success','danger':'danger','warn':'warn',
|
||||
'info-soft':'infoSoft','ok-soft':'okSoft','accent-soft':'accentSoft','success-bg':'successBg',
|
||||
'danger-bg':'dangerBg','warn-bg':'warnBg','th-bg':'thBg','row-hover':'rowHover','shadow':'shadow',
|
||||
'top-bg':'topBg','top-fg':'topFg','top-faint':'topFaint','top-border':'topBorder','top-control':'topControl',
|
||||
'top-mark-bg':'topMarkBg','top-mark-fg':'topMarkFg','side-bg':'sideBg','side-fg':'sideFg',
|
||||
'side-muted':'sideMuted','side-active-bg':'sideActiveBg','side-active-fg':'sideActiveFg',
|
||||
'side-border':'sideBorder','side-hover':'sideHover','menu-user-bg':'menuUserBg',
|
||||
'menu-user-fg':'menuUserFg','menu-user-hover':'menuUserHover',
|
||||
};
|
||||
const DARK = { a:false, b:true, c:false };
|
||||
|
||||
function block(key){
|
||||
const m = css.match(new RegExp(`\\[data-theme="${key}"\\]\\s*\\{([^}]*)\\}`));
|
||||
if(!m) throw new Error('missing theme block: '+key);
|
||||
const out = {};
|
||||
for(const mm of m[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) out[mm[1]] = mm[2].trim();
|
||||
return out;
|
||||
}
|
||||
function toColor(raw){
|
||||
let v = raw.trim();
|
||||
if(v.startsWith('linear-gradient')){ const h = v.match(/#([0-9a-fA-F]{6})/); v = '#'+h[1]; } // 取首个 hex
|
||||
let m = v.match(/^#([0-9a-fA-F]{6})$/);
|
||||
if(m) return `Color(0xFF${m[1].toUpperCase()})`;
|
||||
m = v.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)$/);
|
||||
if(m){ const a = Math.round((m[4]===undefined?1:parseFloat(m[4]))*255).toString(16).padStart(2,'0');
|
||||
const hex = [m[1],m[2],m[3]].map(n=>(+n).toString(16).padStart(2,'0')).join(''); return `Color(0x${a}${hex.toUpperCase()})`; }
|
||||
throw new Error('unsupported color value: '+raw);
|
||||
}
|
||||
function emit(key){
|
||||
const b = block(key);
|
||||
const lines = Object.entries(FIELDS).map(([css,dart])=>{
|
||||
if(b[css]===undefined) throw new Error(`theme ${key} missing --${css}`);
|
||||
return ` ${dart}: ${toColor(b[css])},`;
|
||||
});
|
||||
lines.push(` isDark: ${DARK[key]},`);
|
||||
return `const AppTokens kTokens${key.toUpperCase()} = AppTokens(\n${lines.join('\n')}\n);`;
|
||||
}
|
||||
|
||||
const body = `// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
|
||||
// 源:lib/core/theme/token_source/tokens.css(从原型同步)。改色请改源后重跑脚本。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_tokens.dart';
|
||||
|
||||
${emit('a')}
|
||||
|
||||
${emit('b')}
|
||||
|
||||
${emit('c')}
|
||||
|
||||
AppTokens appTokensOf(String key) {
|
||||
switch (key) {
|
||||
case 'b': return kTokensB;
|
||||
case 'c': return kTokensC;
|
||||
default: return kTokensA;
|
||||
}
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(path.join(ROOT, 'lib/core/theme/app_tokens.g.dart'), body);
|
||||
console.log('generated app_tokens.g.dart');
|
||||
Reference in New Issue
Block a user