diff --git a/frontend/scripts/generate-icons.js b/frontend/scripts/generate-icons.js index d99414b66..ba3ce3408 100644 --- a/frontend/scripts/generate-icons.js +++ b/frontend/scripts/generate-icons.js @@ -39,7 +39,7 @@ function scanForUsedIcons() { scanDirectory(filePath); } else if (file.endsWith('.tsx') || file.endsWith('.ts')) { const content = fs.readFileSync(filePath, 'utf8'); - + // Match LocalIcon usage: const localIconMatches = content.match(/]*icon="([^"]+)"/g); if (localIconMatches) { @@ -51,7 +51,7 @@ function scanForUsedIcons() { } }); } - + // Match old material-symbols-rounded spans: icon-name const spanMatches = content.match(/]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g); if (spanMatches) { @@ -64,7 +64,7 @@ function scanForUsedIcons() { } }); } - + // Match Icon component usage: const iconMatches = content.match(/]*icon="material-symbols:([^"]+)"/g); if (iconMatches) { @@ -76,6 +76,23 @@ function scanForUsedIcons() { } }); } + + // Match icon strings in icon configuration files (iconMap.tsx, toolsTaxonomy.ts) + // Only scan these specific files to avoid false positives + if (filePath.includes('iconMap.tsx') || filePath.includes('toolsTaxonomy.ts')) { + // Pattern: : 'icon-name' or : "icon-name" (object value assignment) + const configIconMatches = content.match(/:\s*['"]([a-z][a-z0-9-]*(?:-rounded|-outline|-sharp)?)['"][,\s}]/g); + if (configIconMatches) { + configIconMatches.forEach(match => { + const iconMatch = match.match(/:\s*['"]([a-z][a-z0-9-]*(?:-rounded|-outline|-sharp)?)['"][,\s}]/); + if (iconMatch && iconMatch[1]) { + const iconName = iconMatch[1]; + usedIcons.add(iconName); + debug(` Found (config): ${iconName} in ${path.relative(srcDir, filePath)}`); + } + }); + } + } } }); } diff --git a/frontend/src/core/components/shared/LocalIcon.tsx b/frontend/src/core/components/shared/LocalIcon.tsx index a9b7198fc..2efed9376 100644 --- a/frontend/src/core/components/shared/LocalIcon.tsx +++ b/frontend/src/core/components/shared/LocalIcon.tsx @@ -5,12 +5,17 @@ import iconSet from '../../../assets/material-symbols-icons.json'; // eslint-dis // Load icons synchronously at import time - guaranteed to be ready on first render let iconsLoaded = false; let localIconCount = 0; +const availableIcons = new Set(); try { if (iconSet) { addCollection(iconSet); iconsLoaded = true; localIconCount = Object.keys(iconSet.icons || {}).length; + // Build set of available icon names for fast lookup + Object.keys(iconSet.icons || {}).forEach(iconName => { + availableIcons.add(iconName); + }); console.info(`✅ Local icons loaded: ${localIconCount} icons (${Math.round(JSON.stringify(iconSet).length / 1024)}KB)`); } } catch { @@ -30,13 +35,38 @@ interface LocalIconProps { * instead of loading from CDN */ export const LocalIcon: React.FC = ({ icon, width, height, style, ...props }) => { - // Convert our icon naming convention to the local collection format - const iconName = icon.startsWith('material-symbols:') - ? icon - : `material-symbols:${icon}`; + // Strip material-symbols: prefix if present to get the base icon name + const baseIconName = icon.startsWith('material-symbols:') + ? icon.replace('material-symbols:', '') + : icon; - // Development logging (only in dev mode) + // Convert to the full icon naming convention with prefix + const iconName = `material-symbols:${baseIconName}`; + + // Runtime validation in development mode if (process.env.NODE_ENV === 'development') { + if (iconsLoaded && !availableIcons.has(baseIconName)) { + const errorKey = `icon-error-${baseIconName}`; + + // Only log each missing icon once per session + if (!sessionStorage.getItem(errorKey)) { + console.error( + `❌ LocalIcon: Icon "${baseIconName}" not found in bundle!\n` + + ` This icon will fall back to CDN (slower, external request).\n` + + ` Run "npm run generate-icons" to add it to the bundle, or fix the icon name.\n` + + ` Search available icons at: https://fonts.google.com/icons` + ); + sessionStorage.setItem(errorKey, 'logged'); + + // Also throw error in development to make it more visible + throw new Error( + `LocalIcon: Missing icon "${baseIconName}". ` + + `Run "npm run generate-icons" to update the bundle.` + ); + } + } + + // Development logging for successful icon loads const logKey = `icon-${iconName}`; if (!sessionStorage.getItem(logKey)) { const source = iconsLoaded ? 'local' : 'CDN'; diff --git a/frontend/src/core/data/toolsTaxonomy.ts b/frontend/src/core/data/toolsTaxonomy.ts index 6d8a65c28..7416a402b 100644 --- a/frontend/src/core/data/toolsTaxonomy.ts +++ b/frontend/src/core/data/toolsTaxonomy.ts @@ -95,8 +95,8 @@ export const getSubcategoryIcon = (subcategory: SubcategoryId): React.ReactNode [SubcategoryId.DOCUMENT_SECURITY]: 'security-rounded', [SubcategoryId.VERIFICATION]: 'verified-user-rounded', [SubcategoryId.DOCUMENT_REVIEW]: 'rate-review-rounded', - [SubcategoryId.PAGE_FORMATTING]: 'view-agenda-rounded', - [SubcategoryId.EXTRACTION]: 'file-download-rounded', + [SubcategoryId.PAGE_FORMATTING]: 'view-week', + [SubcategoryId.EXTRACTION]: 'download-rounded', [SubcategoryId.REMOVAL]: 'delete-sweep-rounded', [SubcategoryId.AUTOMATION]: 'smart-toy-rounded', [SubcategoryId.GENERAL]: 'build-rounded',