Files
jiu/client/tool/gen_tokens.mjs
T
wangjia 6238b86dcb feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏
- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服
  pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验;
  登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段,
  已记 CONTRACT,screens.mjs 留存根)
- ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/
  DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态
- 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、
  h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、
  BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast)
- 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后
  失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览
- 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/
  SelectProductDialog/tabStateProvider
- golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打;
  修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 09:58:14 +08:00

168 lines
7.3 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',
'toast-bg':'toastBg',
};
const DARK = { a:false, b:true, c:false };
// 基础 :root{}(无 data-theme 选择器)=三主题共用默认值(如 --toast-bg),
// 主题块同名变量覆盖之。
function baseBlock(){
const m = css.match(/(?<!\])\n:root\s*\{([^}]*)\}/) || css.match(/^:root\s*\{([^}]*)\}/m);
const out = {};
if(m) for(const mm of m[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) out[mm[1]] = mm[2].trim();
return out;
}
const BASE = null; // 惰性求值(css 定义在下方时避免顺序问题)
function block(key){
const m = css.match(new RegExp(`\\[data-theme="${key}"\\]\\s*\\{([^}]*)\\}`));
if(!m) throw new Error('missing theme block: '+key);
const out = baseBlock();
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','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');