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

+271
View File
@@ -0,0 +1,271 @@
import { useState, useEffect, useMemo, useRef } from 'react'
import { Icon } from './Icon'
import { Badge } from './ui/Badge'
import { Kbd } from './ui/Kbd'
import type { PatternId } from './AIWorkflowSection'
export type SearchItem = {
id: string
title: string
description: string
category: 'Patterns' | 'Components' | 'Foundations' | 'Documentation'
icon: string
targetId: string
patternId?: PatternId
}
const SEARCH_INDEX: SearchItem[] = [
// Patterns
{
id: 'pat-metrics',
title: 'Metrics Dashboard',
description: 'Telemetry dashboard with StatCards, time range picker, and progress gauges',
category: 'Patterns',
icon: 'sparkles',
targetId: 'application-patterns',
patternId: 'metrics',
},
{
id: 'pat-settings',
title: 'Settings & Safeguards',
description: 'Preferences panel with API keys, toggle switches, and alert banners',
category: 'Patterns',
icon: 'settings',
targetId: 'application-patterns',
patternId: 'settings',
},
{
id: 'pat-table',
title: 'Service Monitor Table',
description: 'Real-time service health table with status filtering and search',
category: 'Patterns',
icon: 'layers',
targetId: 'application-patterns',
patternId: 'table',
},
{
id: 'pat-deploy',
title: 'Canary Deployment Flow',
description: 'Interactive rollout modal with live progress simulation',
category: 'Patterns',
icon: 'terminal',
targetId: 'application-patterns',
patternId: 'deploy',
},
// Components
{
id: 'comp-buttons',
title: 'Buttons & Action States',
description: 'Primary, secondary, destructive, ghost variants with resting, hover, active and focus states',
category: 'Components',
icon: 'arrow',
targetId: 'buttons',
},
{
id: 'comp-icons',
title: 'Lucide Icons Explorer',
description: 'Over 1,400+ tree-shaken SVG icons with search, category filtering and 1-click copy',
category: 'Components',
icon: 'sparkles',
targetId: 'icons',
},
{
id: 'comp-forms',
title: 'Form Controls & Inputs',
description: 'Accessible text inputs, secret key revealers, and toggle switches',
category: 'Components',
icon: 'eye',
targetId: 'form-controls',
},
{
id: 'comp-code',
title: 'Composable UI Primitives',
description: 'Zero-drift Dialog, Sheet, Tabs, Table, Alert, Progress, Switch, and StatCard',
category: 'Components',
icon: 'code',
targetId: 'composable-by-default',
},
// Foundations
{
id: 'found-colors',
title: 'Color Tokens (CSS Color 4 lab())',
description: 'Pitch black dark theme and pure white daylight light theme tokens',
category: 'Foundations',
icon: 'sun',
targetId: 'color-tokens',
},
{
id: 'found-type',
title: 'Typography & Scale',
description: 'Modular rem-based 9px to 70px scale with Manrope and DM Mono families',
category: 'Foundations',
icon: 'file',
targetId: 'typography',
},
// Documentation
{
id: 'doc-md',
title: 'Markdown Editorial Layer',
description: 'Changelogs, release notes, and documentation typography',
category: 'Documentation',
icon: 'file',
targetId: 'markdown-rendering',
},
{
id: 'doc-cm',
title: 'CodeMirror Editor Theme',
description: 'Syntax-highlighted code editor preview with line numbers and token palette',
category: 'Documentation',
icon: 'terminal',
targetId: 'codemirror-theme',
},
]
type SearchPaletteProps = {
isOpen: boolean
onClose: () => void
onSelect: (item: SearchItem) => void
initialQuery?: string
}
export function SearchPalette({ isOpen, onClose, onSelect, initialQuery = '' }: SearchPaletteProps) {
const [query, setQuery] = useState(initialQuery)
const [selectedIndex, setSelectedIndex] = useState(0)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (isOpen) {
inputRef.current?.focus()
}
}, [isOpen, initialQuery])
const results = useMemo(() => {
if (!query.trim()) return SEARCH_INDEX
const q = query.toLowerCase()
return SEARCH_INDEX.filter(
item =>
item.title.toLowerCase().includes(q) ||
item.description.toLowerCase().includes(q) ||
item.category.toLowerCase().includes(q),
)
}, [query])
// Keyboard navigation
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault()
onClose()
} else if (e.key === 'ArrowDown') {
e.preventDefault()
setSelectedIndex(prev => (prev + 1) % (results.length || 1))
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setSelectedIndex(prev => (prev - 1 + results.length) % (results.length || 1))
} else if (e.key === 'Enter' && results[selectedIndex]) {
e.preventDefault()
onSelect(results[selectedIndex])
onClose()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isOpen, results, selectedIndex, onClose, onSelect])
if (!isOpen) return null
return (
<div className="search-palette-backdrop" onClick={onClose}>
<div className="search-palette-modal" onClick={e => e.stopPropagation()}>
{/* Search Input Bar */}
<div className="search-palette-head">
<Icon name="search" size={18} className="search-palette-icon" />
<input
ref={inputRef}
type="text"
className="search-palette-input"
placeholder="Type a command, component, or token..."
value={query}
onChange={e => {
setQuery(e.target.value)
setSelectedIndex(0)
}}
/>
{query && (
<button className="icon-button" onClick={() => setQuery('')} aria-label="Clear search">
<Icon name="close" size={14} />
</button>
)}
<Kbd>ESC</Kbd>
</div>
{/* Results List */}
<div className="search-palette-body">
{results.length > 0 ? (
<div className="search-palette-results">
{results.map((item, idx) => {
const isSelected = idx === selectedIndex
return (
<div
key={item.id}
className={`search-palette-item ${isSelected ? 'selected' : ''}`}
onClick={() => {
onSelect(item)
onClose()
}}
onMouseEnter={() => setSelectedIndex(idx)}
>
<div className="search-item-glyph">
<Icon name={item.icon} size={16} />
</div>
<div className="search-item-content">
<div className="search-item-title-row">
<span className="search-item-title">{item.title}</span>
<Badge variant="outline">{item.category}</Badge>
</div>
<p className="search-item-desc">{item.description}</p>
</div>
{isSelected && (
<div className="search-item-action">
<Kbd>↵</Kbd>
</div>
)}
</div>
)
})}
</div>
) : (
<div className="search-palette-empty">
<Icon name="search" size={24} />
<p>No results found for &ldquo;{query}&rdquo;</p>
<span>Try searching for &ldquo;Buttons&rdquo;, &ldquo;Metrics&rdquo;, &ldquo;Tokens&rdquo;, or &ldquo;Icons&rdquo;.</span>
</div>
)}
</div>
{/* Footer info */}
<div className="search-palette-footer">
<div className="row-xs" style={{ alignItems: 'center' }}>
<Kbd>↑</Kbd>
<Kbd>↓</Kbd>
<span>to navigate</span>
</div>
<div className="row-xs" style={{ alignItems: 'center' }}>
<Kbd>↵</Kbd>
<span>to select</span>
</div>
<div className="row-xs" style={{ alignItems: 'center' }}>
<Kbd>ESC</Kbd>
<span>to close</span>
</div>
</div>
</div>
</div>
)
}