mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(sdk): support for plugin icons
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
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'
|
||||
|
||||
describe('resolvePluginIcon', () => {
|
||||
it('resolves a lucide name outside the old 14-icon allowlist', () => {
|
||||
expect(resolvePluginIcon('Stethoscope')).toBe(Stethoscope)
|
||||
})
|
||||
|
||||
it('falls back to Blocks for a missing or unknown name', () => {
|
||||
expect(resolvePluginIcon(null)).toBe(Blocks)
|
||||
expect(resolvePluginIcon(undefined)).toBe(Blocks)
|
||||
expect(resolvePluginIcon('')).toBe(Blocks)
|
||||
expect(resolvePluginIcon('Stethscope')).toBe(Blocks)
|
||||
})
|
||||
|
||||
// A manifest must not be able to reach lucide's non-icon exports.
|
||||
it.each(['icons', 'createLucideIcon', 'default', 'module.exports'])(
|
||||
'falls back to Blocks for the non-icon export %s',
|
||||
(name) => expect(resolvePluginIcon(name)).toBe(Blocks),
|
||||
)
|
||||
|
||||
it('returns a stable reference so nav arrays do not remount the icon', () => {
|
||||
expect(resolvePluginIcon('Stethoscope')).toBe(resolvePluginIcon('Stethoscope'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('PluginIcon', () => {
|
||||
it('renders the plugin-declared icon', () => {
|
||||
const { container } = render(<PluginIcon name="Stethoscope" />)
|
||||
expect(container.querySelector('.lucide-stethoscope')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders Blocks when the plugin declares nothing', () => {
|
||||
const { container } = render(<PluginIcon name={null} />)
|
||||
expect(container.querySelector('.lucide-blocks')).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import { resolvePluginIcon } from './pluginIcon'
|
||||
|
||||
/** Renders the lucide icon a plugin declares in its manifest (Blocks if unknown). */
|
||||
export default function PluginIcon({ name, size = 20, className, style }: {
|
||||
name: string | null | undefined
|
||||
size?: number
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
}) {
|
||||
const Icon = resolvePluginIcon(name)
|
||||
return <Icon size={size} className={className} style={style} />
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user