Files
jiu/client/tool/gen_tokens.mjs
T
wangjia d3fab1857d feat(client): Phase 0.4 codegen 补主题无关 chrome 色(AppChrome)
外壳要用的状态栏/品牌/遮罩/Toast 色在 tokens.css :root,固定不随 A/B/C 主题切换,
原先没进 Dart token。扩展 gen_tokens.mjs 从 :root 生成 AppChrome 常量类
(statusbar-* / brand-* / scrim / hero-glass / toast-bg),外壳直接引用、禁硬编码。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
2026-06-25 17:45:31 +08:00

156 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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');
// ── 主题无关标量(间距/圆角/字号):解析 :root{} 共享块 → AppDims ──
function rootBlock(){
// 匹配 ":root{" 而非 ":root[data-theme=...]"(前者 { 紧跟)
const m = css.match(/:root\s*\{([^}]*)\}/);
if(!m) throw new Error('missing :root shared block');
const out = {};
for(const mm of m[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) out[mm[1]] = mm[2].trim();
return out;
}
// kebab → camelsp-1→sp1 · r-sm→rSm · fs-h1→fsH1 · fs-display→fsDisplay
function camel(name){
const [h, ...rest] = name.split('-');
return h + rest.map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
}
function px(raw){
const m = raw.trim().match(/^([\d.]+)px$/);
if(!m) throw new Error('unsupported dim value: ' + raw);
return m[1];
}
function emitDims(){
const r = rootBlock();
const groups = [
['间距', k => k.startsWith('sp-')],
['圆角', k => k.startsWith('r-')],
['字号', k => k.startsWith('fs-')],
];
const lines = [];
for(const [label, pick] of groups){
const keys = Object.keys(r).filter(pick);
if(!keys.length) continue;
lines.push(` // ${label}`);
for(const k of keys) lines.push(` static const double ${camel(k)} = ${px(r[k])};`);
}
return lines.join('\n');
}
const dimsBody = `// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
// 源:lib/core/theme/token_source/tokens.css:root 共享块,主题无关)。改尺寸请改源后重跑脚本。
// 间距/圆角/字号三套主题统一,故为 const 标量,不进 per-theme AppTokens。
class AppDims {
AppDims._();
${emitDims()}
}
`;
fs.writeFileSync(path.join(ROOT, 'lib/core/theme/app_dims.g.dart'), dimsBody);
console.log('generated app_dims.g.dart');
// ── 主题无关「chrome 色」::root 里固定不随主题切换的颜色 → AppChrome 常量类 ──
// 底部状态栏(深石板灰绿条)、品牌标识色(logo/字标)、遮罩/玻璃叠加、Toast 深底。
// 外壳(状态栏/品牌)要用,但不属任何主题,故不进 per-theme AppTokens。
const CHROME = {
// 状态栏(底部 chrome 条)
'statusbar-bg':'statusbarBg','statusbar-fg':'statusbarFg','statusbar-muted':'statusbarMuted',
'statusbar-strong':'statusbarStrong','statusbar-div':'statusbarDiv','statusbar-ok':'statusbarOk',
// 品牌标识色(固定不随主题)
'brand-deep':'brandDeep','brand-mid':'brandMid','brand-wine':'brandWine',
// 遮罩 / 玻璃叠加
'scrim':'scrim','hero-glass':'heroGlass','hero-glass-strong':'heroGlassStrong',
// Toast 深底
'toast-bg':'toastBg',
};
function emitChrome(){
const r = rootBlock();
return Object.entries(CHROME).map(([css,dart])=>{
if(r[css]===undefined) throw new Error(`:root missing --${css}`);
return ` static const Color ${dart} = ${toColor(r[css])};`;
}).join('\n');
}
const chromeBody = `// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
// 源:lib/core/theme/token_source/tokens.css:root 共享块的主题无关颜色)。改色请改源后重跑脚本。
// 外壳 chrome 色:状态栏/品牌/遮罩/Toast,固定不随 A/B/C 主题切换。
import 'package:flutter/material.dart';
class AppChrome {
AppChrome._();
${emitChrome()}
}
`;
fs.writeFileSync(path.join(ROOT, 'lib/core/theme/app_chrome.g.dart'), chromeBody);
console.log('generated app_chrome.g.dart');