This commit is contained in:
mats committed 2026-09-12 20:29:38 +02:00
1 parent 61df5e3dcc
commit c0a94d3b6d
25 files changed
+2143 -348

No files matched your search

+233
View File
@@ -0,0 +1,233 @@
import { useState, useMemo } from 'react'
import {
Search,
Sun,
Moon,
Copy,
Check,
Eye,
ArrowRight,
ArrowLeft,
Menu,
Layers,
Sparkles,
Terminal,
AlertTriangle,
AlertCircle,
Info,
CheckCircle2,
X,
Plus,
Minus,
Trash2,
ExternalLink,
ChevronDown,
ChevronRight,
ChevronLeft,
ChevronUp,
Filter,
SlidersHorizontal,
Code2,
Cpu,
Database,
User,
Shield,
Folder,
FileText,
Calendar,
Clock,
RefreshCw,
Download,
Upload,
Settings,
HelpCircle,
type LucideIcon,
} from 'lucide-react'
import { Icon } from './Icon'
import { Kbd } from './ui/Kbd'
import { Badge } from './ui/Badge'
type IconItem = {
name: string
pascalName: string
category: 'Navigation' | 'Interface' | 'System' | 'Status'
component: LucideIcon
}
const ICONS: IconItem[] = [
// Navigation
{ name: 'arrow-right', pascalName: 'ArrowRight', category: 'Navigation', component: ArrowRight },
{ name: 'arrow-left', pascalName: 'ArrowLeft', category: 'Navigation', component: ArrowLeft },
{ name: 'chevron-right', pascalName: 'ChevronRight', category: 'Navigation', component: ChevronRight },
{ name: 'chevron-left', pascalName: 'ChevronLeft', category: 'Navigation', component: ChevronLeft },
{ name: 'chevron-down', pascalName: 'ChevronDown', category: 'Navigation', component: ChevronDown },
{ name: 'chevron-up', pascalName: 'ChevronUp', category: 'Navigation', component: ChevronUp },
{ name: 'external-link', pascalName: 'ExternalLink', category: 'Navigation', component: ExternalLink },
{ name: 'menu', pascalName: 'Menu', category: 'Navigation', component: Menu },
// System & AI
{ name: 'sparkles', pascalName: 'Sparkles', category: 'System', component: Sparkles },
{ name: 'terminal', pascalName: 'Terminal', category: 'System', component: Terminal },
{ name: 'code', pascalName: 'Code2', category: 'System', component: Code2 },
{ name: 'cpu', pascalName: 'Cpu', category: 'System', component: Cpu },
{ name: 'layers', pascalName: 'Layers', category: 'System', component: Layers },
{ name: 'sun', pascalName: 'Sun', category: 'System', component: Sun },
{ name: 'moon', pascalName: 'Moon', category: 'System', component: Moon },
{ name: 'database', pascalName: 'Database', category: 'System', component: Database },
// Interface & Actions
{ name: 'search', pascalName: 'Search', category: 'Interface', component: Search },
{ name: 'copy', pascalName: 'Copy', category: 'Interface', component: Copy },
{ name: 'settings', pascalName: 'Settings', category: 'Interface', component: Settings },
{ name: 'sliders', pascalName: 'SlidersHorizontal', category: 'Interface', component: SlidersHorizontal },
{ name: 'filter', pascalName: 'Filter', category: 'Interface', component: Filter },
{ name: 'plus', pascalName: 'Plus', category: 'Interface', component: Plus },
{ name: 'minus', pascalName: 'Minus', category: 'Interface', component: Minus },
{ name: 'trash', pascalName: 'Trash2', category: 'Interface', component: Trash2 },
{ name: 'refresh', pascalName: 'RefreshCw', category: 'Interface', component: RefreshCw },
{ name: 'download', pascalName: 'Download', category: 'Interface', component: Download },
{ name: 'upload', pascalName: 'Upload', category: 'Interface', component: Upload },
{ name: 'folder', pascalName: 'Folder', category: 'Interface', component: Folder },
{ name: 'file', pascalName: 'FileText', category: 'Interface', component: FileText },
{ name: 'user', pascalName: 'User', category: 'Interface', component: User },
{ name: 'calendar', pascalName: 'Calendar', category: 'Interface', component: Calendar },
{ name: 'clock', pascalName: 'Clock', category: 'Interface', component: Clock },
// Status & Feedback
{ name: 'check', pascalName: 'Check', category: 'Status', component: Check },
{ name: 'check-circle', pascalName: 'CheckCircle2', category: 'Status', component: CheckCircle2 },
{ name: 'warning', pascalName: 'AlertTriangle', category: 'Status', component: AlertTriangle },
{ name: 'alert', pascalName: 'AlertCircle', category: 'Status', component: AlertCircle },
{ name: 'info', pascalName: 'Info', category: 'Status', component: Info },
{ name: 'close', pascalName: 'X', category: 'Status', component: X },
{ name: 'shield', pascalName: 'Shield', category: 'Status', component: Shield },
{ name: 'help', pascalName: 'HelpCircle', category: 'Status', component: HelpCircle },
{ name: 'eye', pascalName: 'Eye', category: 'Status', component: Eye },
]
const CATEGORIES = ['All', 'Navigation', 'Interface', 'System', 'Status'] as const
type Category = (typeof CATEGORIES)[number]
export function IconMatrix() {
const [activeCategory, setActiveCategory] = useState<Category>('All')
const [search, setSearch] = useState('')
const [size, setSize] = useState<number>(20)
const [copiedName, setCopiedName] = useState<string | null>(null)
const filteredIcons = useMemo(() => {
return ICONS.filter(icon => {
const matchesCategory = activeCategory === 'All' || icon.category === activeCategory
const matchesSearch =
icon.name.toLowerCase().includes(search.toLowerCase()) ||
icon.pascalName.toLowerCase().includes(search.toLowerCase())
return matchesCategory && matchesSearch
})
}, [activeCategory, search])
const copyUsage = (icon: IconItem) => {
const code = `<${icon.pascalName} size={${size}} />`
navigator.clipboard?.writeText(code)
setCopiedName(icon.name)
setTimeout(() => setCopiedName(null), 2000)
}
return (
<div className="icon-matrix-container">
{/* Header controls */}
<div className="icon-matrix-header">
<div className="icon-search-bar">
<Icon name="search" size={16} />
<input
type="text"
placeholder="Search icons (e.g. arrow, check, sparkles)..."
value={search}
onChange={e => setSearch(e.target.value)}
className="icon-search-input"
/>
{search && (
<button className="icon-clear-btn" onClick={() => setSearch('')}>
<Icon name="close" size={14} />
</button>
)}
</div>
<div className="icon-size-picker">
<span className="mono muted">Size:</span>
{[16, 20, 24].map(s => (
<button
key={s}
className={`icon-size-btn ${size === s ? 'active' : ''}`}
onClick={() => setSize(s)}
>
{s}px
</button>
))}
</div>
</div>
{/* Category tabs */}
<div className="icon-category-tabs">
{CATEGORIES.map(cat => (
<button
key={cat}
className={`icon-cat-btn ${activeCategory === cat ? 'active' : ''}`}
onClick={() => setActiveCategory(cat)}
>
{cat}
<span className="icon-cat-count">
{cat === 'All' ? ICONS.length : ICONS.filter(i => i.category === cat).length}
</span>
</button>
))}
</div>
{/* Grid of icons */}
<div className="icon-grid">
{filteredIcons.map(icon => {
const isCopied = copiedName === icon.name
return (
<button
key={icon.name}
className={`icon-tile ${isCopied ? 'copied' : ''}`}
onClick={() => copyUsage(icon)}
title={`Click to copy <${icon.pascalName} />`}
>
<div className="icon-tile-glyph">
{isCopied ? (
<Icon name="check" size={size} className="accent" />
) : (
<icon.component size={size} strokeWidth={1.8} />
)}
</div>
<span className="icon-tile-name">{icon.name}</span>
<span className="icon-tile-action">
{isCopied ? 'Copied!' : <Kbd>Copy</Kbd>}
</span>
</button>
)
})}
</div>
{filteredIcons.length === 0 && (
<div className="empty-state">
<div className="empty-state-icon">
<Icon name="search" size={24} />
</div>
<h4 style={{ margin: '0 0 var(--space-2)' }}>No icons found</h4>
<p className="muted" style={{ margin: 0, fontSize: 'var(--text-xs)' }}>
No icon matches "{search}". You can use any of Lucide's 1,400+ icons directly via{' '}
<code>lucide-react</code>.
</p>
</div>
)}
{/* Footer usage tip */}
<div className="icon-matrix-footer">
<div className="icon-footer-pill">
<Badge variant="outline">lucide-react</Badge>
<span>Over 1,400+ SVG icons available. Fully tree-shaken and styled via <code>currentColor</code>.</span>
</div>
</div>
</div>
)
}