import type { Meta, StoryObj } from '@storybook/nextjs' import React from 'react' declare const require: any type IconComponent = React.ComponentType> type IconEntry = { name: string category: string path: string Component: IconComponent } const iconContext = require.context('./src', true, /\.tsx$/) const iconEntries: IconEntry[] = iconContext .keys() .filter((key: string) => !key.endsWith('.stories.tsx') && !key.endsWith('.spec.tsx')) .map((key: string) => { const mod = iconContext(key) const Component = mod.default as IconComponent | undefined if (!Component) return null const relativePath = key.replace(/^\.\//, '') const path = `app/components/base/icons/src/${relativePath}` const parts = relativePath.split('/') const fileName = parts.pop() || '' const category = parts.length ? parts.join('/') : '(root)' const name = Component.displayName || fileName.replace(/\.tsx$/, '') return { name, category, path, Component, } }) .filter(Boolean) as IconEntry[] const sortedEntries = [...iconEntries].sort((a, b) => { if (a.category === b.category) return a.name.localeCompare(b.name) return a.category.localeCompare(b.category) }) const filterEntries = (entries: IconEntry[], query: string) => { const normalized = query.trim().toLowerCase() if (!normalized) return entries return entries.filter(entry => entry.name.toLowerCase().includes(normalized) || entry.path.toLowerCase().includes(normalized) || entry.category.toLowerCase().includes(normalized), ) } const groupByCategory = (entries: IconEntry[]) => entries.reduce((acc, entry) => { if (!acc[entry.category]) acc[entry.category] = [] acc[entry.category].push(entry) return acc }, {} as Record) const containerStyle: React.CSSProperties = { padding: 24, display: 'flex', flexDirection: 'column', gap: 24, } const headerStyle: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: 8, } const controlsStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', } const searchInputStyle: React.CSSProperties = { padding: '8px 12px', minWidth: 280, borderRadius: 6, border: '1px solid #d0d0d5', } const toggleButtonStyle: React.CSSProperties = { padding: '8px 12px', borderRadius: 6, border: '1px solid #d0d0d5', background: '#fff', cursor: 'pointer', } const emptyTextStyle: React.CSSProperties = { color: '#5f5f66' } const sectionStyle: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: 12, } const gridStyle: React.CSSProperties = { display: 'grid', gap: 12, gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', } const cardStyle: React.CSSProperties = { border: '1px solid #e1e1e8', borderRadius: 8, padding: 12, display: 'flex', flexDirection: 'column', gap: 8, minHeight: 140, } const previewBaseStyle: React.CSSProperties = { display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 48, borderRadius: 6, } const nameButtonBaseStyle: React.CSSProperties = { display: 'inline-flex', padding: 0, border: 'none', background: 'transparent', font: 'inherit', cursor: 'pointer', textAlign: 'left', fontWeight: 600, } const PREVIEW_SIZE = 40 const IconGalleryStory = () => { const [query, setQuery] = React.useState('') const [copiedPath, setCopiedPath] = React.useState(null) const [previewTheme, setPreviewTheme] = React.useState<'light' | 'dark'>('light') const filtered = React.useMemo(() => filterEntries(sortedEntries, query), [query]) const grouped = React.useMemo(() => groupByCategory(filtered), [filtered]) const categoryOrder = React.useMemo( () => Object.keys(grouped).sort((a, b) => a.localeCompare(b)), [grouped], ) React.useEffect(() => { if (!copiedPath) return undefined const timerId = window.setTimeout(() => { setCopiedPath(null) }, 1200) return () => window.clearTimeout(timerId) }, [copiedPath]) const handleCopy = React.useCallback((text: string) => { navigator.clipboard?.writeText(text) .then(() => { setCopiedPath(text) }) .catch((err) => { console.error('Failed to copy icon path:', err) }) }, []) return (

Icon Gallery

Browse all icon components sourced from app/components/base/icons/src. Use the search bar to filter by name or path.

setQuery(event.target.value)} /> {filtered.length} icons
{categoryOrder.length === 0 && (

No icons match the current filter.

)} {categoryOrder.map(category => (

{category}

{grouped[category].map(entry => (
))}
))}
) } const meta: Meta = { title: 'Base/Icons/Icon Gallery', component: IconGalleryStory, parameters: { layout: 'fullscreen', }, } export default meta type Story = StoryObj export const All: Story = { render: () => , }