/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Shared icon registry for extension commands. * * The picker (`PromoteToolDialog`) and the renderers * (`ExtensionToolbarSlot`, command palette, context menu) all read * from this single source so the icon a user picks actually shows up * everywhere their command appears. * * Manifests record only the string key — never a component reference — * so the bundle stays serialisable and portable. */ import { AlertTriangle, Beaker, Box, Brain, Calculator, Camera, CheckCircle2, ClipboardList, Download, Eye, FileBarChart, FileText, Filter, Flame, Gauge, Hammer, Layers, Lightbulb, Maximize2, Palette, Ruler, ScanSearch, Scissors, Settings, Shield, Sparkles, Tag, Target, Wrench, type LucideIcon, } from 'lucide-react'; export interface IconChoice { key: string; Icon: LucideIcon; label: string; } /** * Curated lucide subset (~29). Keys are lowercase-hyphenated so they * survive JSON round-trips in `manifest.contributes.commands[].icon`. */ export const ICON_CHOICES: readonly IconChoice[] = [ { key: 'sparkles', Icon: Sparkles, label: 'AI / magic' }, { key: 'wrench', Icon: Wrench, label: 'Tool' }, { key: 'hammer', Icon: Hammer, label: 'Build' }, { key: 'palette', Icon: Palette, label: 'Color' }, { key: 'eye', Icon: Eye, label: 'View' }, { key: 'filter', Icon: Filter, label: 'Filter' }, { key: 'shield', Icon: Shield, label: 'Compliance' }, { key: 'flame', Icon: Flame, label: 'Fire rating' }, { key: 'ruler', Icon: Ruler, label: 'Measure' }, { key: 'calculator', Icon: Calculator, label: 'Quantity' }, { key: 'box', Icon: Box, label: 'Element' }, { key: 'layers', Icon: Layers, label: 'Storey' }, { key: 'tag', Icon: Tag, label: 'Classification' }, { key: 'target', Icon: Target, label: 'Isolate' }, { key: 'scan-search', Icon: ScanSearch, label: 'Audit' }, { key: 'clipboard-list', Icon: ClipboardList, label: 'Schedule' }, { key: 'file-text', Icon: FileText, label: 'Report' }, { key: 'file-bar-chart', Icon: FileBarChart, label: 'Chart' }, { key: 'download', Icon: Download, label: 'Export' }, { key: 'camera', Icon: Camera, label: 'Snapshot' }, { key: 'scissors', Icon: Scissors, label: 'Section' }, { key: 'maximize-2', Icon: Maximize2, label: 'Fly to' }, { key: 'gauge', Icon: Gauge, label: 'Performance' }, { key: 'lightbulb', Icon: Lightbulb, label: 'Idea' }, { key: 'alert-triangle', Icon: AlertTriangle, label: 'Warning' }, { key: 'beaker', Icon: Beaker, label: 'Test' }, { key: 'brain', Icon: Brain, label: 'Memory' }, { key: 'check-circle-2', Icon: CheckCircle2, label: 'Validate' }, { key: 'settings', Icon: Settings, label: 'Setting' }, ]; const ICON_LOOKUP = new Map( ICON_CHOICES.map((c) => [c.key, c.Icon]), ); /** * Resolve a manifest icon string to a lucide component. Falls back to * `Sparkles` when the key is unknown or absent so the toolbar still * renders something rather than a missing-icon hole. Also accepts a * loose match on common variants (case, leading "icon-", underscores) * so AI-authored manifests with slight key drift still resolve. */ export function resolveExtensionIcon(key: string | undefined | null): LucideIcon { if (!key) return Sparkles; const direct = ICON_LOOKUP.get(key); if (direct) return direct; // Tolerate AI-authored variants the picker doesn't emit but the model // might guess: `Wrench`, `alertTriangle`, `Alert_Triangle`, // `icon-wrench`, etc. → all collapse to the kebab-case canonical key. const normalised = key .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .toLowerCase() .replace(/^icon[-_]/, '') .replace(/_/g, '-') .trim(); return ICON_LOOKUP.get(normalised) ?? Sparkles; }