kimi-code/apps/kimi-web/scripts/gen-icon-data.mjs
liruifengv ac5b5e4cbf
fix(kimi-web): keep tool components from jumping on expand or collapse (#1433)
* fix(kimi-web): keep tool components from jumping on expand or collapse

Also show the scroll-to-bottom button whenever scrolled up, and render a
fallback icon for tools without a dedicated glyph.

* fix(kimi-web): preserve bottom follow during content-only resizes

Late-loading media can grow after scrollKey has run; keep chasing the
bottom on content growth, and only suppress follow during the pinned
expand/collapse window.
2026-07-06 22:19:02 +08:00

156 lines
5.5 KiB
JavaScript

#!/usr/bin/env node
// scripts/gen-icon-data.mjs — generate src/lib/icon-data.ts, the tree-shaken
// Remix Icon (ri) subset that backs apps/kimi-web's icon registry.
//
// Single source of truth for "which existing icon name maps to which Remix
// icon" is the NAME_TO_REMIX map below. The SVG bytes are pulled straight from
// @iconify-json/ri at generation time, so the registry stays library-sourced
// (no hand-copied SVG) yet fully offline and tree-shaken (only the icons we
// list here end up in the bundle).
//
// Run after changing the map: pnpm gen:icons
import { writeFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import ri from '@iconify-json/ri/icons.json' with { type: 'json' };
import { getIconData } from '@iconify/utils';
const __dirname = dirname(fileURLToPath(import.meta.url));
const OUT = resolve(__dirname, '../src/lib/icon-data.ts');
// Existing icon name → Remix icon name (ri:<name>, prefix omitted here).
// Keep keys sorted within their group; the generated IconName union follows
// this order. Every value must exist in @iconify-json/ri (validated below).
const GROUPS = [
['Actions', {
plus: 'add-line',
'chat-new': 'chat-new-line',
close: 'close-line',
check: 'check-line',
search: 'search-line',
copy: 'file-copy-line',
link: 'links-line',
'external-link': 'external-link-line',
download: 'download-line',
undo: 'arrow-go-back-line',
send: 'arrow-up-line',
image: 'image-line',
settings: 'settings-3-line',
sliders: 'equalizer-line',
'log-in': 'login-box-line',
}],
['Navigation & layout', {
'chevron-down': 'arrow-down-s-line',
'chevron-right': 'arrow-right-s-line',
'arrow-up': 'arrow-up-line',
'arrow-down': 'arrow-down-line',
'arrow-right': 'arrow-right-line',
minus: 'subtract-line',
'panel-collapse': 'contract-left-line',
'panel-expand': 'expand-right-line',
expand: 'expand-diagonal-line',
collapse: 'collapse-diagonal-line',
list: 'list-unordered',
sort: 'sort-desc',
grip: 'draggable',
}],
['Files & tools', {
folder: 'folder-open-line',
'folder-closed': 'folder-line',
'folder-plus': 'folder-add-line',
'folder-solid': 'folder-fill',
file: 'file-line',
'file-text': 'file-text-line',
'file-plus': 'file-add-line',
'file-off': 'file-line',
'image-off': 'image-line',
code: 'code-line',
terminal: 'terminal-box-line',
pencil: 'pencil-line',
tool: 'tools-line',
glob: 'braces-line',
globe: 'global-line',
'check-list': 'list-check',
bolt: 'flashlight-line',
'git-pull-request': 'git-pull-request-line',
}],
['Communication', {
message: 'message-line',
mail: 'mail-line',
user: 'user-line',
}],
['Status & media', {
info: 'information-line',
'help-circle': 'question-line',
'alert-triangle': 'alert-line',
clock: 'time-line',
sparkles: 'sparkling-line',
play: 'play-fill',
stop: 'stop-fill',
star: 'star-fill',
'star-outline': 'star-line',
'dots-horizontal': 'more-line',
}],
];
const NAME_TO_REMIX = Object.assign({}, ...GROUPS.map(([, m]) => m));
// --- resolve + validate ----------------------------------------------------
const missing = [];
const data = {};
for (const [name, riName] of Object.entries(NAME_TO_REMIX)) {
const icon = getIconData(ri, riName);
if (!icon) {
missing.push(`${name} → ri:${riName}`);
continue;
}
data[name] = { body: icon.body, width: icon.width ?? 24, height: icon.height ?? 24 };
}
if (missing.length) {
console.error('gen-icon-data: the following Remix icons were not found in @iconify-json/ri:');
for (const m of missing) console.error(` ${m}`);
process.exit(1);
}
// --- emit ------------------------------------------------------------------
const names = Object.keys(data);
const keyOf = (n) => (/^[a-zA-Z_$][\w$]*$/.test(n) ? n : JSON.stringify(n));
const lines = [];
lines.push('// GENERATED FILE — do not edit by hand.');
lines.push('// Source of truth: scripts/gen-icon-data.mjs (run `pnpm gen:icons`).');
lines.push('// Icons are Remix Icon (ri) — https://remixicon.com/ — Apache-2.0.');
lines.push('');
lines.push('export type IconName =');
for (let i = 0; i < names.length; i++) {
const suffix = i === names.length - 1 ? ';' : '';
lines.push(` | ${JSON.stringify(names[i])}${suffix}`);
}
lines.push('');
lines.push('export interface IconData {');
lines.push(' /** Inner SVG markup (paths/shapes), rendered inside our <svg> wrapper. */');
lines.push(' body: string;');
lines.push(' /** Source grid width in px. Remix icons are 24. */');
lines.push(' width?: number;');
lines.push(' /** Source grid height in px. Remix icons are 24. */');
lines.push(' height?: number;');
lines.push('}');
lines.push('');
lines.push('/** Existing name → fully-qualified Remix icon id. */');
lines.push('export const NAME_TO_REMIX: Record<IconName, string> = {');
for (const name of names) lines.push(` ${keyOf(name)}: ${JSON.stringify('ri:' + NAME_TO_REMIX[name])},`);
lines.push('};');
lines.push('');
lines.push('/** Per-icon SVG data, pulled from @iconify-json/ri. */');
lines.push('export const ICON_DATA: Record<IconName, IconData> = {');
for (const name of names) {
const { body, width, height } = data[name];
lines.push(` ${keyOf(name)}: { body: ${JSON.stringify(body)}, width: ${width}, height: ${height} },`);
}
lines.push('};');
lines.push('');
writeFileSync(OUT, lines.join('\n'));
console.log(`gen-icon-data: wrote ${names.length} icons to src/lib/icon-data.ts`);