diff --git a/client/src/components/Layout/BottomNav.tsx b/client/src/components/Layout/BottomNav.tsx index 09a8c6a1b..3d318571f 100644 --- a/client/src/components/Layout/BottomNav.tsx +++ b/client/src/components/Layout/BottomNav.tsx @@ -5,7 +5,7 @@ import { useSettingsStore } from '../../store/settingsStore' import { useTranslation } from '../../i18n' import { LayoutGrid, CalendarDays, Globe, Compass, Bookmark, Plus } from 'lucide-react' import type { LucideIcon } from 'lucide-react' -import { resolvePluginIcon } from '../shared/pluginIcon' +import { resolvePluginIcon } from '../shared/PluginIcon' const ADDON_NAV: Record = { vacay: { icon: CalendarDays, labelKey: 'admin.addons.catalog.vacay.name' }, diff --git a/client/src/components/Layout/Navbar.tsx b/client/src/components/Layout/Navbar.tsx index 2fb333616..cb7cd684c 100644 --- a/client/src/components/Layout/Navbar.tsx +++ b/client/src/components/Layout/Navbar.tsx @@ -9,7 +9,7 @@ import { useTranslation } from '../../i18n' import { Plane, LogOut, Settings, ChevronDown, Shield, ArrowLeft, Users, Moon, Sun, Monitor, CalendarDays, Briefcase, Globe, Compass, BookOpen, Bookmark } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import InAppNotificationBell from './InAppNotificationBell.tsx' -import { resolvePluginIcon } from '../shared/pluginIcon' +import { resolvePluginIcon } from '../shared/PluginIcon' const ADDON_ICONS: Record = { CalendarDays, Briefcase, Globe, Compass, Bookmark } diff --git a/client/src/components/shared/PluginIcon.test.tsx b/client/src/components/shared/PluginIcon.test.tsx index f3a905f5a..cf813c48f 100644 --- a/client/src/components/shared/PluginIcon.test.tsx +++ b/client/src/components/shared/PluginIcon.test.tsx @@ -1,8 +1,7 @@ import { describe, it, expect } from 'vitest' import { render } from '@testing-library/react' import { Blocks, Stethoscope } from 'lucide-react' -import { resolvePluginIcon } from './pluginIcon' -import PluginIcon from './PluginIcon' +import PluginIcon, { resolvePluginIcon } from './PluginIcon' describe('resolvePluginIcon', () => { it('resolves a lucide name outside the old 14-icon allowlist', () => { diff --git a/client/src/components/shared/PluginIcon.tsx b/client/src/components/shared/PluginIcon.tsx index 59f2afadf..a463dd523 100644 --- a/client/src/components/shared/PluginIcon.tsx +++ b/client/src/components/shared/PluginIcon.tsx @@ -1,5 +1,26 @@ import type { CSSProperties } from 'react' -import { resolvePluginIcon } from './pluginIcon' +import * as LucideIcons from 'lucide-react' +import { Blocks, type LucideIcon } from 'lucide-react' + +// Lucide names are PascalCase. Restricting the lookup to that shape keeps a manifest +// from reaching lucide's non-icon exports (`createLucideIcon`, `icons`, `default`, +// `module.exports`), which are all lowercase-initial. +const LUCIDE_ICON_NAME = /^[A-Z][A-Za-z0-9]*$/ + +/** + * Resolves a plugin manifest's `icon` (a lucide icon name, e.g. "Stethoscope") to the + * matching lucide component, falling back to Blocks for a missing or unknown name. + * + * Note lucide icons are forwardRef objects, not functions — never narrow this with + * `typeof x === 'function'`. The lookup returns a module-level reference, so the + * component identity is stable across renders and is safe to call inline while + * building nav/tab arrays. + */ +export function resolvePluginIcon(name: string | null | undefined): LucideIcon { + if (!name || !LUCIDE_ICON_NAME.test(name)) return Blocks + const icon = (LucideIcons as unknown as Record)[name] + return icon ?? Blocks +} /** Renders the lucide icon a plugin declares in its manifest (Blocks if unknown). */ export default function PluginIcon({ name, size = 20, className, style }: { diff --git a/client/src/components/shared/pluginIcon.ts b/client/src/components/shared/pluginIcon.ts deleted file mode 100644 index b7850be8b..000000000 --- a/client/src/components/shared/pluginIcon.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as LucideIcons from 'lucide-react' -import { Blocks, type LucideIcon } from 'lucide-react' - -// Lucide names are PascalCase. Restricting the lookup to that shape keeps a manifest -// from reaching lucide's non-icon exports (`createLucideIcon`, `icons`, `default`, -// `module.exports`), which are all lowercase-initial. -const LUCIDE_ICON_NAME = /^[A-Z][A-Za-z0-9]*$/ - -/** - * Resolves a plugin manifest's `icon` (a lucide icon name, e.g. "Stethoscope") to the - * matching lucide component, falling back to Blocks for a missing or unknown name. - * - * Note lucide icons are forwardRef objects, not functions — never narrow this with - * `typeof x === 'function'`. The lookup returns a module-level reference, so the - * component identity is stable across renders and is safe to call inline while - * building nav/tab arrays. - */ -export function resolvePluginIcon(name: string | null | undefined): LucideIcon { - if (!name || !LUCIDE_ICON_NAME.test(name)) return Blocks - const icon = (LucideIcons as unknown as Record)[name] - return icon ?? Blocks -} diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts index 93e34c612..44d01957d 100644 --- a/client/src/pages/tripPlanner/useTripPlanner.ts +++ b/client/src/pages/tripPlanner/useTripPlanner.ts @@ -6,7 +6,7 @@ import { useSettingsStore } from '../../store/settingsStore' import { getCached, fetchPhoto } from '../../services/photoService' import { useToast } from '../../components/shared/Toast' import { Map, Ticket, PackageCheck, Wallet, FolderOpen, Users, Train } from 'lucide-react' -import { resolvePluginIcon } from '../../components/shared/pluginIcon' +import { resolvePluginIcon } from '../../components/shared/PluginIcon' import { useTranslation, translateApiError } from '../../i18n' import { addonsApi, accommodationsApi, authApi, tripsApi, assignmentsApi, healthApi, airtrailApi, mapsApi, placesApi } from '../../api/client' import { parsedItemToDraft, isTransportItem, type BookingReviewDraft } from '../../components/Planner/parsedItemToDraft'