Files
kvitt/web/scripts/gen-icons.mjs
T
jacobh2 2ae65a5a9f
Build Kvitt images / build (push) Failing after 1h14m5s
W0 web foundation: Vite + React SPA under /app/, sync-pull ledger, auth, tokens, shared components
web/ is the browser client's scaffold: Vite 8, React 19, TypeScript 5.9 strict, React Router 7 (basename /app, every
fixed route with a lazy placeholder page), TanStack Query 5 (refetch on focus and every 60 s), openapi-typescript
types generated from contracts/openapi.yaml with openapi-fetch. Reads go through GET /sync (no REST reads exist for
the ledger, SPEC §7): useLedger() pulls from cursor 0 and then incrementally, restarting on a new epoch or
cursor_ahead. Login pairs the browser as "Web · <browser>" with an invite code (E-3); every call sends the SPEC
§14.3.1 headers, a 401 signs out. kvitt-domain's split, balances, allocation and settlement rules are ported to
web/src/lib and pass every case of contracts/test-vectors. Design tokens from design-spec §2 in tokens.css
(light-dark()), Material Symbols Rounded (Apache-2.0) as generated inline SVG, sv/en catalogs generated from
docs/ux/strings.csv plus web-strings.csv. Gate: cd web && npm run check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-21 21:03:25 +02:00

191 lines
4.8 KiB
JavaScript

// Builds src/design/icons.generated.ts from Material Symbols Rounded (design-spec §2.12; Apache-2.0):
// outline = @material-symbols/svg-500 (FILL 0, weight 500), filled = @material-symbols/svg-600 (FILL 1, weight 600).
// Only the icons listed here are embedded; a feature plan adds a name and runs `npm run icons:import`.
// `--check` compares with the file on disk instead of writing.
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
/** The 33 category icon tokens of SPEC §9.7, in CategoryIcons.ALL order. Both variants (picker and tiles). */
export const CATEGORY_ICONS = [
'shopping_cart',
'restaurant',
'local_cafe',
'home',
'bolt',
'water_drop',
'wifi',
'directions_car',
'train',
'local_gas_station',
'flight',
'hotel',
'luggage',
'movie',
'music_note',
'sports_esports',
'fitness_center',
'medical_services',
'medication',
'shopping_bag',
'checkroom',
'pets',
'child_care',
'school',
'card_giftcard',
'celebration',
'build',
'chair',
'subscriptions',
'category',
'key',
'shield',
'account_balance',
];
/** UI icons of design-spec §2.12 plus the web's own (search, keyboard, logout, language, theme). Outline variant. */
const UI_OUTLINE = [
'home',
'bar_chart',
'settings',
'add',
'close',
'arrow_back',
'more_vert',
'edit',
'delete',
'check',
'swap_horiz',
'handshake',
'cloud_upload',
'cloud_off',
'cloud_done',
'sync',
'sync_problem',
'warning',
'error',
'info',
'check_circle',
'chevron_right',
'expand_more',
'expand_less',
'inventory_2',
'unarchive',
'event',
'call_split',
'notes',
'person',
'apps',
'backspace',
'calculate',
'date_range',
'content_paste',
'link',
'dns',
'person_add',
'add_a_photo',
'photo_library',
'receipt_long',
'schedule',
'pause',
'event_repeat',
'drag_indicator',
'restart_alt',
'lock',
'download',
'refresh',
'phonelink_erase',
'smartphone',
'block',
'devices',
'add_link',
'key',
'share',
'content_copy',
'memory',
'open_in_new',
'arrow_forward',
'search',
'keyboard',
'logout',
'language',
'contrast',
'lightbulb',
];
/** Filled UI icons of design-spec §2.12 (selected nav, status, handshake, app mark). */
const UI_FILLED = [
'home',
'bar_chart',
'settings',
'handshake',
'cloud_done',
'warning',
'error',
'check_circle',
'receipt_long',
'search',
'lightbulb',
];
/**
* Spec token names that Material Symbols 0.48 ships under another file name (the glyph is the same). The spec's name
* stays the key in the generated tables. The APK-update icons of S-17 are not embedded: the web has no APK update.
*/
const FILE_NAMES = {
card_giftcard: 'redeem',
expand_more: 'keyboard_arrow_down',
expand_less: 'keyboard_arrow_up',
phonelink_erase: 'mobile_cancel',
smartphone: 'mobile',
};
const root = new URL('../', import.meta.url);
function pathOf(pkg, file) {
const url = new URL(`node_modules/@material-symbols/${pkg}/rounded/${file}.svg`, root);
const svg = readFileSync(url, 'utf-8');
if (!svg.includes('viewBox="0 -960 960 960"')) throw new Error(`${pkg}/${file}: unexpected viewBox`);
const paths = [...svg.matchAll(/<path d="([^"]+)"\/>/g)].map((m) => m[1]);
if (paths.length !== 1) throw new Error(`${pkg}/${file}: expected one path, found ${paths.length}`);
return paths[0];
}
function table(names, pkg, suffix) {
const unique = [...new Set(names)].sort();
return unique.map((name) => ` ${name}: '${pathOf(pkg, (FILE_NAMES[name] ?? name) + suffix)}',`).join('\n');
}
const text = `// Generated by scripts/gen-icons.mjs (npm run icons:import). Do not edit.
// Material Symbols Rounded, viewBox 0 -960 960 960; outline from @material-symbols/svg-500, filled from
// @material-symbols/svg-600. Licence: Apache License 2.0 (Google).
export const OUTLINE = {
${table([...CATEGORY_ICONS, ...UI_OUTLINE], 'svg-500', '')}
} as const;
export const FILLED = {
${table([...CATEGORY_ICONS, ...UI_FILLED], 'svg-600', '-fill')}
} as const;
export const CATEGORY_ICON_TOKENS = [
${CATEGORY_ICONS.map((n) => ` '${n}',`).join('\n')}
] as const;
export type OutlineIconName = keyof typeof OUTLINE;
export type FilledIconName = keyof typeof FILLED;
export type CategoryIconToken = (typeof CATEGORY_ICON_TOKENS)[number];
`;
const target = new URL('src/design/icons.generated.ts', root);
if (process.argv.includes('--check')) {
const current = existsSync(target) ? readFileSync(target, 'utf-8') : '';
if (current !== text) {
console.error('src/design/icons.generated.ts is stale: run npm run icons:import');
process.exit(1);
}
} else {
writeFileSync(target, text);
console.log(
`src/design/icons.generated.ts: ${new Set([...CATEGORY_ICONS, ...UI_OUTLINE]).size} outline, ${new Set([...CATEGORY_ICONS, ...UI_FILLED]).size} filled`,
);
}