fix(plugins): fold resolvePluginIcon into PluginIcon

pluginIcon.ts and PluginIcon.tsx resolve to the same file on the
case-insensitive filesystems dev checkouts commonly sit on (Windows,
macOS) — tsc sees both casings of one module and fails with TS1149 in
every importer. Keep the resolver and the component in one module.
This commit is contained in:
Maurice
2026-07-13 16:20:40 +02:00
parent 1ba07a2320
commit 1b4ef8e9fe
6 changed files with 26 additions and 28 deletions
+1 -1
View File
@@ -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<string, { icon: LucideIcon; labelKey: string }> = {
vacay: { icon: CalendarDays, labelKey: 'admin.addons.catalog.vacay.name' },
+1 -1
View File
@@ -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<string, LucideIcon> = { CalendarDays, Briefcase, Globe, Compass, Bookmark }
@@ -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', () => {
+22 -1
View File
@@ -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<string, LucideIcon | undefined>)[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 }: {
@@ -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<string, LucideIcon | undefined>)[name]
return icon ?? Blocks
}
@@ -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'