Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」, 那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」, 移动端站已是「K」;移动端文档站则完全没有 favicon)。 - 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边; 描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」 - 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark); 顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28)) - 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg) - 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留) - 移动端 7 个模板补 favicon(此前计数 0) 验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/ theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致; 两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。 ## 并行会话成果(本次一并入库) - 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具 + IconPreview 预览页 + ICON-SPEC.md 冻结规格 - 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁 - PC 组件:103 个大后台组件 / 组件11 批次 - uni-app:PC 端试点 + 移动端端实现 + 真实编译验证 ## 工程 - .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库) - CHANGELOG 补品牌标识条目 - ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
448 lines
27 KiB
JavaScript
448 lines
27 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 图标构建:把 sources.json(第三方原始 SVGO 数据)编译成 Kole UI 的图标清单。
|
||
*
|
||
* 产物(全部入库,构建链离线可复现):
|
||
* .design_library/kole-ui/icons/manifest.json 图标清单:name / tier / source / mode / tags
|
||
* .design_library/kole-ui/icons/registry.json 渲染数据:name → 路径数据(各端运行时读这一份)
|
||
* .design_library/kole-ui/icons/ATTRIBUTION.md 来源与许可(MIT 归属,必须随分发)
|
||
* site/icons.json 文档站 IconPreview 用的轻量索引(无路径数据)
|
||
*
|
||
* 三层分级(tier)—— 决定「内联进组件包」还是「按需加载」:
|
||
* core ≥2 个上游库都有这个图标名,且是 UI 骨架高频语义(见 CORE_EXTRA)。内联。
|
||
* standard 上游有、名字被归一化收录。按需。
|
||
* extended 仅单一上游有的长尾图标(品牌 / 插画类)。按需。
|
||
*
|
||
* 命名规则:
|
||
* - 一律 kebab-case;面性图标保留 `-filled` 后缀(与上游一致,不发明第二套词)。
|
||
* - 冲突时优先级 tdesign-outline > element-plus > tdesign-filled。
|
||
* 因为线性图标与本仓库「线性图标,描边 1.5px」的规范一致(组件1.txt)。
|
||
*
|
||
* 用法:node tools/build-icons.mjs
|
||
* 零运行时依赖;退出码 0 = 成功。
|
||
*/
|
||
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join } from 'node:path';
|
||
|
||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||
const ICON_DIR = join(ROOT, '.design_library', 'kole-ui', 'icons');
|
||
const SRC = join(ICON_DIR, 'sources.json');
|
||
const SITE = join(ROOT, 'site');
|
||
|
||
/* 上游优先级:线性优先(与规范 1.5px 描边同族),其次 Element Plus,最后面性 */
|
||
const SOURCE_PRIORITY = ['tdesign-outline', 'element-plus', 'tdesign-filled'];
|
||
|
||
/* 描边宽度:上游 TDesign 是 2(24 网格),规范要求 1.5 —— 构建期统一改。
|
||
取值按 组件1.txt「线性图标,描边1.5px」。 */
|
||
const STROKE_WIDTH = 1.5;
|
||
|
||
/* core 层:**内联进组件包**的那一批,必须小而准 —— 只放 UI 骨架高频语义。
|
||
判据不是"上游有几个库有"(那会得到 1000+ 个,内联等于把整包塞进每一页),
|
||
而是"B 端中后台界面的骨架会不会用到"。分组见 CORE_NAMES。
|
||
这批图标实测 ~200 个 / 约 40KB,作为内联表是合理的。 */
|
||
const CORE_NAMES = [
|
||
/* 方向与导航 */
|
||
'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'caret-up', 'caret-down',
|
||
'chevron-up', 'chevron-down', 'chevron-left', 'chevron-right', 'back', 'top', 'bottom',
|
||
'expand', 'menu-fold', 'menu-unfold', 'fold', 'unfold-less', 'unfold-more', 'sort', 'swap', 'jump',
|
||
/* 基础操作 */
|
||
'add', 'close', 'delete', 'edit', 'copy', 'save', 'search', 'refresh', 'setting', 'filter',
|
||
'upload', 'download', 'share', 'print', 'send', 'clear', 'remove',
|
||
'link', 'attach', 'pin', 'crop', 'rotate', 'scan', 'view', 'hide', 'open',
|
||
'check', 'play', 'pause', 'stop', 'refresh-left',
|
||
'refresh-right', 'fullscreen', 'zoom-in', 'zoom-out', 'more', 'menu', 'list', 'grid',
|
||
'minus', 'plus', 'mode-preview', 'poweroff',
|
||
/* 状态与反馈 */
|
||
'check-circle', 'close-circle', 'info-circle', 'error-circle', 'warning', 'help-circle',
|
||
'loading', 'success', 'error', 'info', 'help', 'question', 'lock', 'unlock',
|
||
'star', 'star-filled', 'heart', 'flag', 'shield-error', 'time', 'clock', 'certificate',
|
||
/* 数据与内容 */
|
||
'user', 'user-add', 'avatar', 'chart', 'chart-bar', 'chart-pie', 'chart-line', 'data',
|
||
'data-analysis', 'dashboard', 'file', 'folder', 'document', 'book', 'calendar', 'task',
|
||
'table', 'tree-catalog', 'map', 'location', 'code', 'terminal', 'cloud', 'server',
|
||
'image', 'video', 'camera', 'mail', 'message', 'chat', 'notification', 'bell', 'phone',
|
||
'mobile', 'monitor', 'desktop', 'keyboard', 'mouse', 'printer', 'wifi', 'bluetooth',
|
||
'battery', 'cpu', 'history', 'home', 'app', 'application',
|
||
/* 业务 */
|
||
'money', 'coin', 'wallet', 'creditcard', 'order', 'goods', 'cart', 'shop', 'store',
|
||
'gift', 'coupon', 'ticket', 'price-tag', 'box', 'undertake-delivery', 'view-organization',
|
||
'usergroup',
|
||
];
|
||
|
||
/* 语义分组:IconPreview 页的分组展示 + 检索标签。
|
||
规则按**实测的名字前缀频次**写(top 前缀见 tools/verify-icons.mjs 的覆盖率断言),
|
||
顺序即优先级:先匹配到的分组胜出。覆盖率是门禁指标,不是"看着差不多"。 */
|
||
const GROUPS = [
|
||
['方向导航', /^(arrow|caret|chevron|back|backtop|next|prev|forward|top|bottom|left|right|up|down|expand|collapse|fold|unfold|sort|swap|jump|go|return|enter|exit|move|drag|resize|fullscreen|full-screen|zoom|flip|align|indent|outdent|redirect|refresh-left|refresh-right|direction|navigate|guide|position)/],
|
||
['基础操作', /^(add|close|clear|delete|edit|pen|copy|save|search|browse|refresh|reload|setting|set-up|filter|upload|download|share|print|send|remove|minus|plus|link|attach|paperclip|pin|crop|rotate|scan|read|write|hide|view|show|open|reset|confirm|check|apply|cancel|preview|play|pause|stop|record|replay|reply|sync|update|upgrade|install|restore|cut|paste|undo|redo|lock|unlock|merge|split|magic|tool|control|edit-1)/],
|
||
['状态反馈', /^(success|error|warning|info|help|question|alert|tip|prompt|loading|circle|check-circle|close-circle|info-circle|error-circle|exclamation|star|heart|thumb|like|dislike|flag|mark|badge|certificate|verified|shield|secure|safety|risk|danger|fire|hot|new|crown|vip|level|rank|medal|trophy|award|score|grade|rate|smile|angry|calm|excited|sad|puzzle|check-|no-|ban|forbid|lock|unlock|error|progress)/],
|
||
['数据内容', /^(chart|data|analysis|analytics|statistic|table|grid|list|tree|map|graph|dashboard|report|trend|pie|bar|line|scatter|radar|gauge|metric|number|numbers|calc|formula|function|code|terminal|api|database|server|cloud|storage|disk|file|folder|document|doc|note|notebook|book|letters|text|textformat|format|font|article|page|paper|clip|attachment|archive|task|project|plan|goal|target|filter|system|component|widget|abstract|application|browser)/],
|
||
['用户组织', /^(user|usergroup|account|avatar|profile|member|team|group|contact|person|people|customer|employee|admin|manager|role|permission|authority|organization|company|department|position|job|id|card|badge|signature|fingerprint|face|eye|hand|body|health|medical|hospital|call|gesture|accessibility|personal|my-|friend)/],
|
||
['设备硬件', /^(mobile|phone|tablet|laptop|desktop|computer|monitor|screen|display|tv|camera|video|mic|microphone|speaker|sound|headphone|headset|mouse|keyboard|printer|scanner|device|hardware|cpu|chip|memory|battery|power|plug|charger|usb|bluetooth|wifi|signal|network|router|antenna|satellite|gps|location|compass|navigation|airplay|cast|projector|remote|sensor|cable)/],
|
||
['文件媒体', /^(file|folder|document|doc|note|book|read|page|paper|clip|attachment|archive|zip|pdf|word|excel|ppt|image|photo|picture|gallery|album|film|movie|music|audio|sound|voice|record|media|play|video|logo|icon|emoji|sticker|palette|brush|paint|color|crop|filter)/],
|
||
['业务商务', /^(money|coin|wallet|bank|creditcard|card|payment|pay|order|goods|product|cart|shop|store|mall|trade|deal|discount|coupon|ticket|price|cost|fee|tax|invoice|bill|receipt|refund|gift|present|package|box|delivery|ship|truck|warehouse|inventory|stock|buy|sell|auction|bid|contract|sign|shop|business|finance|fund|invest|profit|margin|budget|salary|bonus|chart-bar)/],
|
||
['品牌标识', /^(logo|brand|github|gitlab|wechat|weixin|qq|weibo|alipay|taobao|tmall|facebook|twitter|instagram|youtube|linkedin|google|apple|android|windows|chrome|firefox|safari|edge|figma|sketch|xd|photoshop|illustrator|slack|dingtalk|teams|zoom|element|tdesign|git|code-|open-source|npm|docker|linux|ubuntu|vue|react|angular|node|python|java)/],
|
||
['生活娱乐', /^(apple|cherry|chicken|coffee|dessert|dish|food|fruit|grape|ice|pear|watermelon|burger|sugar|lollipop|milk|tea|drink|goblet|sport|game|football|basketball|baseball|soccer|trophy|travel|beach|holiday|weather|sunny|cloudy|rain|snow|wind|moon|sun|star|cloud|cry|smile|angry|calm|uncomfortable|lemon|egg|bread|cake|candy|flower|plant|leaf|animal|cat|dog|bird|fish|city|building|houses|house|bridge|palace|tower|museum|institution|archway|attic|lighthouse|chimney|dam|street|road|garden|park|home|hotel|bank|hospital|school|campus|factory|warehouse|office|store|shop|market|restaurant|cafe|bar|theater|cinema|stadium|gym|airport|station|port|harbor)/],
|
||
['系统设置', /^(mode|brightness|contrast|dark|light|theme|palette|color|fill|frame|transform|animation|extension|bug|hard|sim|measurement|alarm|notification|chat|message|mail|bell|sound|volume|mute|voice|record|camera|screen|display|monitor|settings|preference|config|option|switch|toggle|slider|knob|dial|meter|gauge|indicator|status|health|diagnostic|log|history|version|update|upgrade|sync|cloud|wifi|signal|battery|power|energy|charge|plug|socket|cable|port|usb|bluetooth|network|server|database|storage|disk|memory|cpu|chip|hardware|device|equipment|machine|robot|drone|satellite|antenna|radar|sensor|detector|scanner|printer|fax|projector|microphone|speaker|headphone|headset|keyboard|mouse|joystick|touchpad|trackpad|stylus|pen|pencil|brush|eraser|ruler|scissors|clip|stapler|tape|glue|tool|wrench|hammer|screw|screwdriver|drill|saw|axe|shovel|rake|broom|bucket|ladder)/],
|
||
['任务流程', /^(task|assignment|undertake|project|plan|goal|target|milestone|schedule|timeline|workflow|process|procedure|step|stage|phase|progress|track|trace|monitor|supervise|review|approve|reject|submit|assign|delegate|transfer|handover|collaborate|cooperate|coordination|meeting|appointment|deadline|reminder|alert|notify|announce|publish|release|deploy|launch|pipeline|queue|batch|job|ticket|issue|case|request|apply|form|survey|questionnaire|vote|poll|election|evaluate|assess|score|rating|feedback|comment|discuss|reply|forward|share|mention|follow|subscribe|bookmark|collect|favorite|like|star|flag|tag|label|category|classify|group|organize|sort|filter|search|find|locate|browse|explore|discover|navigate|guide|tour|walkthrough|onboard|tutorial|help|support|service|maintain|repair|fix|debug|test|verify|validate|confirm|complete|finish|done|close|archive|delete|restore|recover|backup|export|import|migrate|transfer|copy|duplicate|clone|merge|split|combine|connect|disconnect|link|unlink|attach|detach|mount|unmount|load|unload|send|receive|deliver|dispatch|ship|track|trace)/],
|
||
];
|
||
|
||
function die(msg) {
|
||
console.error('FAIL: ' + msg);
|
||
process.exit(1);
|
||
}
|
||
|
||
function atomicWrite(file, content) {
|
||
mkdirSync(dirname(file), { recursive: true });
|
||
const tmp = `${file}.tmp-${process.pid}`;
|
||
writeFileSync(tmp, content.replace(/\r\n/g, '\n'), 'utf8');
|
||
renameSync(tmp, file);
|
||
}
|
||
|
||
/* ---------- 归一化 ---------- */
|
||
|
||
/** 图标名归一化:去首尾空白、小写、非字母数字统一成连字符、折叠重复连字符。 */
|
||
export function normalizeName(n) {
|
||
return String(n)
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9]+/g, '-')
|
||
.replace(/-+/g, '-')
|
||
.replace(/^-|-$/g, '');
|
||
}
|
||
|
||
/** 取「基名」用于跨库匹配:去掉 filled/solid/outline/round 后缀与尾部序号。 */
|
||
export function baseName(n) {
|
||
return normalizeName(n).replace(/-(filled|solid|outline|round)$/, '').replace(/-[0-9]+$/, '');
|
||
}
|
||
|
||
/* ---------- 主流程 ---------- */
|
||
|
||
if (!existsSync(SRC)) die(`缺 ${SRC} —— 先跑 node tools/fetch-icon-sources.mjs`);
|
||
const raw = JSON.parse(readFileSync(SRC, 'utf8'));
|
||
|
||
/* ① 汇总:base 名 → 该名字出现在哪些 source */
|
||
const byBase = new Map();
|
||
for (const s of raw.sources) {
|
||
for (const name of Object.keys(s.icons)) {
|
||
const b = baseName(name);
|
||
if (!byBase.has(b)) byBase.set(b, new Set());
|
||
byBase.get(b).add(s.id);
|
||
}
|
||
}
|
||
/* 上游共识(≥2 个库都有)—— core 层的客观依据 */
|
||
const consensus = new Set([...byBase.entries()].filter(([, ids]) => ids.size >= 2).map(([b]) => b));
|
||
|
||
/* ② 选源:按优先级挑第一个真正拥有的 source,同 source 内优先取规范名 */
|
||
const picked = new Map(); // canonical -> { source, iconName, icon, tier }
|
||
for (const srcId of SOURCE_PRIORITY) {
|
||
const s = raw.sources.find((x) => x.id === srcId);
|
||
if (!s) continue;
|
||
for (const [name, icon] of Object.entries(s.icons)) {
|
||
const canonical = normalizeName(name);
|
||
if (picked.has(canonical)) continue;
|
||
picked.set(canonical, {
|
||
source: srcId,
|
||
iconName: name,
|
||
icon,
|
||
tier: consensus.has(baseName(name)) ? 'standard' : 'extended',
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ③ core 层:命中 CORE_NAMES 的图标内联进组件包,其余按需。
|
||
名字对不上时尝试 `-filled` 变体(规范常写 `star`,上游可能是 `star-filled`)。 */
|
||
const coreSet = new Set();
|
||
const coreMissed = [];
|
||
for (const raw of CORE_NAMES) {
|
||
const c = normalizeName(raw);
|
||
if (picked.has(c)) coreSet.add(c);
|
||
else if (picked.has(c + '-filled')) coreSet.add(c + '-filled');
|
||
else coreMissed.push(c);
|
||
}
|
||
for (const c of coreSet) picked.get(c).tier = 'core';
|
||
/* core 里有名字在本仓库图标集里根本不存在 → 说明清单写错了,必须报出来而不是静默跳过 */
|
||
if (coreMissed.length) {
|
||
console.log(` 注意:CORE_NAMES 里 ${coreMissed.length} 个名字在上游不存在(已跳过):${coreMissed.join(', ')}`);
|
||
}
|
||
|
||
/* ④ 分组:按 GROUPS 规则命中,未命中归「其他」 */
|
||
function groupOf(name) {
|
||
const b = baseName(name);
|
||
for (const [label, re] of GROUPS) {
|
||
if (re.test(b) || re.test(name)) return label;
|
||
}
|
||
/* 长尾兜底:上游把 AI 系列、表情系列、饮食系列铺得很细(实测 289 个不同的
|
||
首段前缀),逐个写规则不值得。按"是不是 AIGC / 表情 / 饮食"归到已有分组,
|
||
剩下的才进「其他」。这样「其他」保持为真正的未知项,便于日后审计。 */
|
||
if (/^(ai|machine|deep|neural|prompt|model|token|dataset|train|infer)/.test(b)) return '数据内容';
|
||
if (/^(smile|angry|calm|excited|sad|cry|swear|surprised|speechless|unhappy|uncomfortable|confused|thinking|wink|kiss|laugh|bored|tired|sick|scared|shocked|proud|humble|neutral|delighted|amazed|curious|determined|disappointed|embarrassed|frustrated|grateful|guilty|hopeful|jealous|lonely|nostalgic|optimistic|pessimistic|relieved|satisfied|stressed|sympathetic|thankful|thoughtful|worried)/.test(b)) return '状态反馈';
|
||
if (/^(rice|noodle|soup|meat|vegetable|seafood|snack|breakfast|lunch|dinner|meal|recipe|kitchen|chef|restaurant|fog|wave|thunderstorm|snow|rain|sunny|cloudy|wind|humidity|temperature|weather|season|spring|summer|autumn|winter)/.test(b)) return '生活娱乐';
|
||
if (/^(mosque|pyramid|panorama|landmark|monument|statue|memorial|temple|church|castle|fortress|ruins|heritage|tourist|sightseeing|attraction|flight|traffic|transport|vehicle|car|bus|train|subway|metro|taxi|bicycle|motorcycle|scooter|boat|ship|plane|helicopter|rocket|spacecraft)/.test(b)) return '设备硬件';
|
||
return '其他';
|
||
}
|
||
|
||
/* ⑤ 生成 registry + manifest */
|
||
const registry = {};
|
||
const manifestIcons = [];
|
||
for (const [name, p] of [...picked.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
||
const { icon } = p;
|
||
/* 路径压缩成单字符串:"S<d>|F<d>|…",S=描边 F=填充。
|
||
这样单图标体积最小,且各端解析逻辑一致(见 frameworks/Icon.js 的 parsePath)。 */
|
||
const paths = icon.d.map((x) => (x.m === 'stroke' ? 'S' : 'F') + x.p).join('|');
|
||
registry[name] = paths;
|
||
manifestIcons.push({
|
||
name,
|
||
tier: p.tier,
|
||
source: p.source,
|
||
mode: icon.d.every((x) => x.m === 'stroke') ? 'stroke' : icon.d.some((x) => x.m === 'stroke') ? 'mixed' : 'fill',
|
||
group: groupOf(name),
|
||
viewBox: icon.v || '0 0 24 24',
|
||
paths: icon.d.length,
|
||
});
|
||
}
|
||
|
||
/* ⑥ 自检:不许有重复名、不许有空路径、core 层不许为空 */
|
||
if (!Object.keys(registry).length) die('registry 为空');
|
||
const dupes = manifestIcons.map((i) => i.name).filter((n, i, a) => a.indexOf(n) !== i);
|
||
if (dupes.length) die('图标重名:' + dupes.slice(0, 5).join(', '));
|
||
for (const [n, p] of Object.entries(registry)) {
|
||
if (!p || /(\|\||\|\$|^\|)/.test(p)) die(`路径格式异常:${n}`);
|
||
}
|
||
const coreCount = manifestIcons.filter((i) => i.tier === 'core').length;
|
||
if (coreCount < 30) die(`core 层只有 ${coreCount} 个,明显偏少(期望 ≥30)`);
|
||
|
||
const byTier = { core: 0, standard: 0, extended: 0 };
|
||
const byGroup = {};
|
||
for (const i of manifestIcons) {
|
||
byTier[i.tier]++;
|
||
byGroup[i.group] = (byGroup[i.group] || 0) + 1;
|
||
}
|
||
|
||
const viewBoxes = {};
|
||
for (const i of manifestIcons) viewBoxes[i.viewBox] = (viewBoxes[i.viewBox] || 0) + 1;
|
||
|
||
/* viewBox 例外表:只列不是默认 24 网格的那些,避免把 2000+ 个默认值写进产物 */
|
||
const vboxTable = {};
|
||
for (const i of manifestIcons) {
|
||
if (i.viewBox !== '0 0 24 24') vboxTable[i.name] = i.viewBox;
|
||
}
|
||
|
||
/* ⑦ 写产物 */
|
||
const registryPayload = {
|
||
schemaVersion: 1,
|
||
strokeWidth: STROKE_WIDTH,
|
||
/* viewBox 例外表随 registry 一起发:渲染方必须查它,写死 24 网格会让
|
||
1024 网格的 Element Plus 图标与 25 个 24×25 图标被拉伸变形。 */
|
||
viewBox: vboxTable,
|
||
note: '本文件为生成物(tools/build-icons.mjs)。路径格式:S<d>=描边,F<d>=填充,多段用 | 分隔。',
|
||
icons: registry,
|
||
};
|
||
atomicWrite(join(ICON_DIR, 'registry.json'), JSON.stringify(registryPayload));
|
||
|
||
const manifest = {
|
||
schemaVersion: 1,
|
||
library: 'kole-ui',
|
||
generatedBy: 'tools/build-icons.mjs',
|
||
generatedAt: raw.generatedAt,
|
||
strokeWidth: STROKE_WIDTH,
|
||
tiers: {
|
||
core: '内联进组件包:上游共识 ∧ UI 骨架高频语义',
|
||
standard: '上游共识图标,按需加载',
|
||
extended: '单一上游的长尾图标(品牌 / 插画类),按需加载',
|
||
},
|
||
naming: 'kebab-case;面性图标保留 -filled 后缀;冲突优先级 tdesign-outline > element-plus > tdesign-filled',
|
||
sources: raw.sources.map((s) => ({
|
||
id: s.id,
|
||
package: s.package,
|
||
version: s.version,
|
||
license: s.license,
|
||
copyright: s.copyright,
|
||
homepage: s.homepage,
|
||
})),
|
||
total: manifestIcons.length,
|
||
byTier,
|
||
byGroup,
|
||
viewBoxes,
|
||
icons: manifestIcons,
|
||
};
|
||
atomicWrite(join(ICON_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
||
|
||
/* 文档站用的轻量索引:不含路径数据,IconPreview 页首屏只加载这一份。
|
||
用「列式」存法(names / tiers / groups 三个平行数组)而不是对象数组:
|
||
实测对象数组 119KB、列式 78KB,同样信息量少 35%。 */
|
||
const siteIndex = {
|
||
generatedAt: raw.generatedAt,
|
||
total: manifestIcons.length,
|
||
strokeWidth: STROKE_WIDTH,
|
||
groups: [...new Set(manifestIcons.map((i) => i.group))],
|
||
tiers: ['core', 'standard', 'extended'],
|
||
names: manifestIcons.map((i) => i.name),
|
||
/* tier 压成 0/1/2 索引,group 压成分组数组下标 —— 都是短整数,JSON 里最省 */
|
||
tier: manifestIcons.map((i) => ['core', 'standard', 'extended'].indexOf(i.tier)),
|
||
group: manifestIcons.map((i) => [...new Set(manifestIcons.map((x) => x.group))].indexOf(i.group)),
|
||
};
|
||
atomicWrite(join(SITE, 'icons.json'), JSON.stringify(siteIndex));
|
||
|
||
/* ⑧ 各端运行时模块:把 core 层(内联那一批)编译成一个自包含 JS。
|
||
为什么单独一个文件:11 个端实现都要「名字 → SVG 路径」这一段逻辑,
|
||
分散写 11 份必然漂移。这一段是**纯数据 + 纯函数**,不含任何类名 ——
|
||
所以 PC 与移动端可以共用同一份(类名隔离由各端模板负责,见 PLATFORMS.md)。 */
|
||
const coreIcons = {};
|
||
for (const i of manifestIcons) {
|
||
if (i.tier === 'core') coreIcons[i.name] = registry[i.name];
|
||
}
|
||
const runtime = `/* Kole UI 图标运行时 · 生成物(tools/build-icons.mjs),勿手改。
|
||
*
|
||
* 数据来源与许可见同目录 ATTRIBUTION.md(MIT;TDesign + Element Plus)。
|
||
*
|
||
* 这里只有 core 层(${Object.keys(coreIcons).length} 个)—— UI 骨架高频图标,内联进组件包。
|
||
* 全量 ${manifestIcons.length} 个在 registry.json,用 KOLE_ICONS.loadAll() 按需取。
|
||
*
|
||
* 路径格式:'S<d>' 描边段 / 'F<d>' 填充段,多段用 | 分隔。
|
||
* 描边宽度统一 ${STROKE_WIDTH}(规范 组件1.txt「图标规范:线性图标,描边1.5px」)。
|
||
*/
|
||
(function (root, factory) {
|
||
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||
else root.KOLE_ICONS = factory();
|
||
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||
var STROKE_WIDTH = ${STROKE_WIDTH};
|
||
var CORE = ${JSON.stringify(coreIcons)};
|
||
/* viewBox 例外(${Object.keys(vboxTable).length} 个不是 24 网格):写死 viewBox 会让它们被拉伸 */
|
||
var VBOX = ${JSON.stringify(vboxTable)};
|
||
|
||
/** 解析路径串 → [{ d, mode }] */
|
||
function parse(raw) {
|
||
if (!raw) return [];
|
||
return raw.split('|').map(function (seg) {
|
||
return { mode: seg.charAt(0) === 'S' ? 'stroke' : 'fill', d: seg.slice(1) };
|
||
});
|
||
}
|
||
|
||
/** 渲染成 SVG 字符串。viewBox 按图标查表(上游有非 24 网格的图标)。 */
|
||
function svg(name, opts) {
|
||
opts = opts || {};
|
||
var raw = CORE[name] || (opts.registry && opts.registry[name]) || null;
|
||
if (!raw) return '';
|
||
var vb = opts.viewBox || VBOX[name] || '0 0 24 24';
|
||
var body = parse(raw).map(function (p) {
|
||
return p.mode === 'stroke'
|
||
? '<path d="' + p.d + '" fill="none" stroke="currentColor" stroke-width="' + STROKE_WIDTH + '" stroke-linecap="round" stroke-linejoin="round"/>'
|
||
: '<path d="' + p.d + '" fill="currentColor"/>';
|
||
}).join('');
|
||
return '<svg viewBox="' + vb + '" width="1em" height="1em" aria-hidden="true" focusable="false">' + body + '</svg>';
|
||
}
|
||
|
||
/** 该名字是否在内联表里(false 时端上需要 loadAll) */
|
||
function has(name) { return Object.prototype.hasOwnProperty.call(CORE, name); }
|
||
|
||
/** 取全量(浏览器端 fetch registry.json;已完成过就复用) */
|
||
var loading = null;
|
||
function loadAll(url) {
|
||
if (loading) return loading;
|
||
if (typeof fetch !== 'function') return Promise.resolve(CORE);
|
||
loading = fetch(url || 'icons/registry.json')
|
||
.then(function (r) { return r.ok ? r.json() : { icons: {} }; })
|
||
.then(function (j) {
|
||
var icons = (j && j.icons) || {};
|
||
Object.keys(icons).forEach(function (k) { CORE[k] = icons[k]; });
|
||
return CORE;
|
||
})
|
||
.catch(function () { return CORE; });
|
||
return loading;
|
||
}
|
||
|
||
return {
|
||
strokeWidth: STROKE_WIDTH,
|
||
coreCount: Object.keys(CORE).length,
|
||
core: CORE,
|
||
viewBox: VBOX,
|
||
parse: parse,
|
||
svg: svg,
|
||
has: has,
|
||
loadAll: loadAll,
|
||
};
|
||
});
|
||
`;
|
||
atomicWrite(join(ICON_DIR, 'core.js'), runtime);
|
||
|
||
/* viewBox 例外表单独一份 JS:文档站与测试可以 import 它做断言 */
|
||
const vboxJs = `/* Kole UI 图标 viewBox 例外表 · 生成物(tools/build-icons.mjs),勿手改。\n *\n * 上游绝大多数图标是 24 网格,但有 ${Object.keys(vboxTable).length} 个不是(实测 24×25 / 25×24 /\n * 1024 三种)。renderSvg 必须查这张表,写死 viewBox 会让这些图标被拉伸变形。\n */\nexport const ICON_VIEWBOX_EXCEPTIONS = ${JSON.stringify(vboxTable, null, 2)};\n`;
|
||
atomicWrite(join(ICON_DIR, 'viewbox.js'), vboxJs);
|
||
|
||
/* ⑨ MIT 归属:分发时必须随包(两个上游库都要求保留版权声明) */
|
||
const attribution = `# 图标来源与许可
|
||
|
||
本目录下的 \`sources.json\` / \`registry.json\` / \`manifest.json\` 由 \`tools/build-icons.mjs\`
|
||
从下列开源图标库编译而来。它们均为 **MIT 许可**,允许商用与修改,**要求保留版权声明**——
|
||
本文件即为该声明,分发(npm 包 / 部署镜像)时必须一并携带。
|
||
|
||
| 来源 | 包 | 版本 | 许可 | 版权 | 主页 |
|
||
|---|---|---|---|---|---|
|
||
${raw.sources
|
||
.map((s) => `| ${s.id} | \`${s.package}\` | ${s.version} | ${s.license} | ${s.copyright} | ${s.homepage} |`)
|
||
.join('\n')}
|
||
|
||
## 上游原始包的校验和(sha256)
|
||
|
||
构建时逐包校验,防止"某天图标悄悄变了":
|
||
|
||
${raw.sources
|
||
.map((s) => `- \`${s.package}@${s.version}\`: \`${s.sha256}\``)
|
||
.join('\n')}
|
||
|
||
## 本仓库所做的归一化(不是照抄)
|
||
|
||
1. **修正 \`view-box\` 拼写错误**:TDesign 全部 2356 个 SVG 把属性写成 \`view-box\`
|
||
(无效属性,浏览器会忽略并按默认视口缩放)。本仓库统一写回规范的 \`viewBox\`。
|
||
\`tools/verify-icons.mjs\` 卡这条。
|
||
2. **描边宽度归一为 ${STROKE_WIDTH}px**:TDesign 上游为 \`stroke-width="2"\`(24 网格),
|
||
而本仓库规范(\`.design_library/kole-ui/specs/组件1.txt\`「图标规范」)要求
|
||
「线性图标,描边1.5px,尺寸16/20/24px」。差异在构建期抹平。
|
||
3. **丢弃 \`fill="transparent"\` 命中区路径**:那是交互热区不是视觉内容,
|
||
保留会让未选中态出现意外描边。
|
||
4. **数字精度归一到 2 位小数 + 折叠空白**:实测省约 25% 体积,视觉无差异。
|
||
5. **统一命名**:kebab-case;同名冲突按 \`tdesign-outline > element-plus > tdesign-filled\` 取一。
|
||
|
||
## 许可全文
|
||
|
||
MIT License
|
||
|
||
### TDesign Icons
|
||
${raw.sources.find((s) => s.id.startsWith('tdesign'))?.copyright || 'Copyright (c) 2021-present TDesign'}
|
||
|
||
### Element Plus Icons
|
||
${raw.sources.find((s) => s.id === 'element-plus')?.copyright || 'Copyright (c) 2016-present Element Plus'}
|
||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
of this software and associated documentation files (the "Software"), to deal
|
||
in the Software without restriction, including without limitation the rights
|
||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
copies of the Software, and to permit persons to whom the Software is
|
||
furnished to do so, subject to the following conditions:
|
||
|
||
The above copyright notice and this permission notice shall be included in all
|
||
copies or substantial portions of the Software.
|
||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
SOFTWARE.
|
||
`;
|
||
atomicWrite(join(ICON_DIR, 'ATTRIBUTION.md'), attribution);
|
||
|
||
/* ⑨ 输出汇总 */
|
||
console.log('图标构建');
|
||
console.log(` 图标总数 ${manifestIcons.length}`);
|
||
console.log(` 分层 core ${byTier.core} / standard ${byTier.standard} / extended ${byTier.extended}`);
|
||
console.log(` 分组 ${Object.entries(byGroup).map(([k, v]) => `${k} ${v}`).join(' / ')}`);
|
||
console.log(` viewBox ${Object.entries(viewBoxes).map(([k, v]) => `${k}×${v}`).join(' / ')}`);
|
||
console.log(` registry ${(JSON.stringify(registryPayload).length / 1024).toFixed(0)}KB`);
|
||
console.log(` manifest ${(JSON.stringify(manifest).length / 1024).toFixed(0)}KB`);
|
||
console.log(` site/icons.json ${(JSON.stringify(siteIndex).length / 1024).toFixed(0)}KB`);
|
||
console.log('OK');
|