a95a4ac86b
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
3.4 KiB
JavaScript
75 lines
3.4 KiB
JavaScript
// 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');
|