// 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', 'toast-bg':'toastBg', }; const DARK = { a:false, b:true, c:false }; // 基础 :root{}(无 data-theme 选择器)=三主题共用默认值(如 --toast-bg), // 主题块同名变量覆盖之。 function baseBlock(){ const m = css.match(/(?(+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 → camel:sp-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','brand-dot':'brandDot', // 彩色底上的固定前景白(横幅/徽章/品牌弹窗 chrome 前景) 'on-accent':'onAccent','on-accent-faint':'onAccentFaint', // 遮罩 / 玻璃叠加 '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');