From ba4fea1647b254c9e771d545cd0ae112bc943f85 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Tue, 23 Dec 2025 23:54:33 +0000 Subject: [PATCH 1/4] feat: icons add cmd --- package.json | 3 +- packages/assets/build/add-icons.ts | 211 +++++++++++++++++++++++++++++ packages/assets/package.json | 3 + pnpm-lock.yaml | 11 ++ 4 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 packages/assets/build/add-icons.ts diff --git a/package.json b/package.json index f9c7d23434..9be60f6ad7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "fix:frontend": "turbo run fix --continue --parallel --filter='!@modrinth/labrinth' --filter='!@modrinth/app' --filter='!@modrinth/app-lib' --filter='!@modrinth/daedalus' --filter='!@modrinth/daedalus_client' --filter='!@modrinth/app-playground'", "fix:ancillary": "prettier --write .github *.*", "ci": "turbo run lint test --continue", - "intl:extract": "pnpm ui:intl:extract && pnpm web:intl:extract && pnpm app:intl:extract && pnpm moderation:intl:extract" + "intl:extract": "pnpm ui:intl:extract && pnpm web:intl:extract && pnpm app:intl:extract && pnpm moderation:intl:extract", + "icons:add": "pnpm --filter @modrinth/assets icons:add" }, "devDependencies": { "@modrinth/tooling-config": "workspace:*", diff --git a/packages/assets/build/add-icons.ts b/packages/assets/build/add-icons.ts new file mode 100644 index 0000000000..d0b1c32424 --- /dev/null +++ b/packages/assets/build/add-icons.ts @@ -0,0 +1,211 @@ +import fs from 'node:fs' +import path from 'node:path' +import readline from 'node:readline' + +const packageRoot = path.resolve(__dirname, '..') +const iconsDir = path.join(packageRoot, 'icons') +const lucideIconsDir = path.join(packageRoot, 'node_modules/lucide-static/icons') + +function listAvailableIcons(): string[] { + if (!fs.existsSync(lucideIconsDir)) { + return [] + } + return fs + .readdirSync(lucideIconsDir) + .filter((file) => file.endsWith('.svg')) + .map((file) => path.basename(file, '.svg')) + .sort() +} + +function paginateList(allIcons: string[], pageSize = 20): void { + let page = 0 + let search = '' + let filteredIcons = allIcons + + const getFilteredIcons = (): string[] => { + if (!search) return allIcons + return allIcons.filter((icon) => icon.includes(search)) + } + + const renderPage = (): void => { + console.clear() + filteredIcons = getFilteredIcons() + const totalPages = Math.max(1, Math.ceil(filteredIcons.length / pageSize)) + + if (page >= totalPages) page = Math.max(0, totalPages - 1) + + const start = page * pageSize + const end = Math.min(start + pageSize, filteredIcons.length) + const pageIcons = filteredIcons.slice(start, end) + + console.log(`\x1b[1mAvailable Lucide Icons\x1b[0m`) + console.log(`\x1b[2mSearch: \x1b[0m${search || '\x1b[2m(type to search)\x1b[0m'}\n`) + + if (pageIcons.length === 0) { + console.log(` \x1b[2mNo icons found matching "${search}"\x1b[0m`) + } else { + pageIcons.forEach((icon) => { + if (search) { + const highlighted = icon.replace(search, `\x1b[33m${search}\x1b[0m`) + console.log(` ${highlighted}`) + } else { + console.log(` ${icon}`) + } + }) + } + + console.log(`\n\x1b[2m${filteredIcons.length}/${allIcons.length} icons | Page ${page + 1}/${totalPages} | ← → navigate | :q quit\x1b[0m`) + } + + renderPage() + + readline.emitKeypressEvents(process.stdin) + if (process.stdin.isTTY) { + process.stdin.setRawMode(true) + } + + process.stdin.on('keypress', (str, key) => { + if (key.ctrl && key.name === 'c') { + console.clear() + process.exit(0) + } + + // :q to quit + if (search === ':' && key.name === 'q') { + console.clear() + process.exit(0) + } + + // Navigation + if (key.name === 'right') { + const totalPages = Math.max(1, Math.ceil(filteredIcons.length / pageSize)) + if (page < totalPages - 1) { + page++ + renderPage() + } + return + } + if (key.name === 'left') { + if (page > 0) { + page-- + renderPage() + } + return + } + + // Backspace + if (key.name === 'backspace') { + search = search.slice(0, -1) + page = 0 + renderPage() + return + } + + // Escape to clear search + if (key.name === 'escape') { + search = '' + page = 0 + renderPage() + return + } + + // Type to search + if (str && str.length === 1 && !key.ctrl && !key.meta) { + search += str + page = 0 + renderPage() + } + }) +} + +function addIcon(iconId: string, overwrite: boolean): boolean { + const sourcePath = path.join(lucideIconsDir, `${iconId}.svg`) + const targetPath = path.join(iconsDir, `${iconId}.svg`) + + if (!fs.existsSync(sourcePath)) { + console.error(`❌ Icon "${iconId}" not found in lucide-static`) + console.error(` Run with --list to see available icons`) + return false + } + + if (fs.existsSync(targetPath) && !overwrite) { + console.log(`⏭️ Skipping "${iconId}" (already exists, use --overwrite to replace)`) + return false + } + + fs.copyFileSync(sourcePath, targetPath) + console.log(`✅ Added "${iconId}"`) + return true +} + +function main(): void { + const args = process.argv.slice(2) + + if (args.includes('--help') || args.includes('-h')) { + console.log(` +Usage: pnpm icons:add [options] [icon_id...] + +Options: + --list, -l Browse all available Lucide icons (interactive) + --overwrite, -o Overwrite existing icons + --help, -h Show this help message + +Examples: + pnpm icons:add heart star settings-2 + pnpm icons:add --overwrite heart + pnpm icons:add --list # Interactive browser + pnpm icons:add --list | grep arrow # Pipe to grep + +Interactive controls: + Type Search icons + ← → Navigate pages + Escape Clear search + :q Quit +`) + process.exit(0) + } + + if (args.includes('--list') || args.includes('-l')) { + const icons = listAvailableIcons() + if (icons.length === 0) { + console.error('❌ lucide-static not installed. Run pnpm install first.') + process.exit(1) + } + if (process.stdout.isTTY) { + paginateList(icons) + } else { + // Non-interactive mode (piped output) + icons.forEach((icon) => console.log(icon)) + process.exit(0) + } + return + } + + const overwrite = args.includes('--overwrite') || args.includes('-o') + const iconIds = args.filter((arg) => !arg.startsWith('-')) + + if (iconIds.length === 0) { + console.error('Usage: pnpm icons:add [icon_id...]') + console.error('Example: pnpm icons:add heart star settings-2') + console.error('Run with --help for more options') + process.exit(1) + } + + if (!fs.existsSync(lucideIconsDir)) { + console.error('❌ lucide-static not installed. Run pnpm install first.') + process.exit(1) + } + + let added = 0 + for (const iconId of iconIds) { + if (addIcon(iconId, overwrite)) { + added++ + } + } + + if (added > 0) { + console.log(`\n📦 Added ${added} icon(s). Run 'pnpm icons:generate' to update exports.`) + } +} + +main() diff --git a/packages/assets/package.json b/packages/assets/package.json index 7d0a65ae61..450973c6e2 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -7,13 +7,16 @@ "scripts": { "lint": "pnpm run icons:validate && eslint . && prettier --check .", "fix": "pnpm run icons:generate && eslint . --fix && prettier --write .", + "icons:add": "jiti build/add-icons.ts", "icons:test": "jiti build/generate-exports.ts --test", "icons:validate": "jiti build/generate-exports.ts --validate", "icons:generate": "jiti build/generate-exports.ts" }, "devDependencies": { "@modrinth/tooling-config": "workspace:*", + "@types/node": "^20.1.0", "jiti": "^2.4.2", + "lucide-static": "*", "vue": "^3.5.13" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 426871fb39..19d8a777c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -428,9 +428,15 @@ importers: '@modrinth/tooling-config': specifier: workspace:* version: link:../tooling-config + '@types/node': + specifier: ^20.1.0 + version: 20.14.11 jiti: specifier: ^2.4.2 version: 2.4.2 + lucide-static: + specifier: ^0.469.0 + version: 0.469.0 vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.8.3) @@ -5623,6 +5629,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-static@0.469.0: + resolution: {integrity: sha512-ravTgZodIVLO53rJyjIq0iuCRHs+kjd3dAfOcTbA41KCDfdy0Pt6Me8akkhda3jRpbIxjKe1PpYZOg2eQnI/lA==} + magic-string-ast@0.6.2: resolution: {integrity: sha512-oN3Bcd7ZVt+0VGEs7402qR/tjgjbM7kPlH/z7ufJnzTLVBzXJITRHOJiwMmmYMgZfdoWQsfQcY+iKlxiBppnMA==} engines: {node: '>=16.14.0'} @@ -14324,6 +14333,8 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-static@0.469.0: {} + magic-string-ast@0.6.2: dependencies: magic-string: 0.30.14 From 17b2136454adc855b3d9d43dda331d2c92e8c9ba Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Tue, 23 Dec 2025 23:56:10 +0000 Subject: [PATCH 2/4] fix: dep --- packages/assets/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/assets/package.json b/packages/assets/package.json index 450973c6e2..8c5f12173e 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -16,7 +16,7 @@ "@modrinth/tooling-config": "workspace:*", "@types/node": "^20.1.0", "jiti": "^2.4.2", - "lucide-static": "*", + "lucide-static": "^0.562.0", "vue": "^3.5.13" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19d8a777c0..0a197629fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -435,8 +435,8 @@ importers: specifier: ^2.4.2 version: 2.4.2 lucide-static: - specifier: ^0.469.0 - version: 0.469.0 + specifier: ^0.562.0 + version: 0.562.0 vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.8.3) @@ -5629,8 +5629,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-static@0.469.0: - resolution: {integrity: sha512-ravTgZodIVLO53rJyjIq0iuCRHs+kjd3dAfOcTbA41KCDfdy0Pt6Me8akkhda3jRpbIxjKe1PpYZOg2eQnI/lA==} + lucide-static@0.562.0: + resolution: {integrity: sha512-TM2vNVOEsO3+ijmno7n/VmxUo0Shr9OXC/UqZc5n4xEVyXX4E4NVvXoRPAZiSsIsdvlQ7alGOcIC/QGtR+OgUQ==} magic-string-ast@0.6.2: resolution: {integrity: sha512-oN3Bcd7ZVt+0VGEs7402qR/tjgjbM7kPlH/z7ufJnzTLVBzXJITRHOJiwMmmYMgZfdoWQsfQcY+iKlxiBppnMA==} @@ -14333,7 +14333,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-static@0.469.0: {} + lucide-static@0.562.0: {} magic-string-ast@0.6.2: dependencies: From aee5bd6769f096e58c18d97a61cd7388202c24ec Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Wed, 24 Dec 2025 21:42:21 +0000 Subject: [PATCH 3/4] Update packages/assets/build/add-icons.ts Signed-off-by: Calum H. --- packages/assets/build/add-icons.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets/build/add-icons.ts b/packages/assets/build/add-icons.ts index d0b1c32424..44e8f227d9 100644 --- a/packages/assets/build/add-icons.ts +++ b/packages/assets/build/add-icons.ts @@ -204,7 +204,7 @@ Interactive controls: } if (added > 0) { - console.log(`\n📦 Added ${added} icon(s). Run 'pnpm icons:generate' to update exports.`) + console.log(`\n📦 Added ${added} icon(s). Run 'pnpm prepr:frontend:lib' to update exports.`) } } From 4b2c6f957417f7f53c428f4ea18d487c49f564a1 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Wed, 24 Dec 2025 22:08:03 +0000 Subject: [PATCH 4/4] fix: lint --- packages/assets/build/add-icons.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/assets/build/add-icons.ts b/packages/assets/build/add-icons.ts index 44e8f227d9..ed54805571 100644 --- a/packages/assets/build/add-icons.ts +++ b/packages/assets/build/add-icons.ts @@ -54,7 +54,9 @@ function paginateList(allIcons: string[], pageSize = 20): void { }) } - console.log(`\n\x1b[2m${filteredIcons.length}/${allIcons.length} icons | Page ${page + 1}/${totalPages} | ← → navigate | :q quit\x1b[0m`) + console.log( + `\n\x1b[2m${filteredIcons.length}/${allIcons.length} icons | Page ${page + 1}/${totalPages} | ← → navigate | :q quit\x1b[0m`, + ) } renderPage()