feat(command-palette): open SearchCategory to arbitrary strings
All checks were successful
Build & Publish / publish (push) Successful in 1m33s
All checks were successful
Build & Publish / publish (push) Successful in 1m33s
Widen SearchCategory from a closed union to string. Known categories (application, exchange, attribute, route, agent) keep their labels. Unknown categories render with title-cased labels and appear as dynamic tabs derived from the data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,8 +19,7 @@ interface CommandPaletteProps {
|
||||
onSubmit?: (query: string) => void
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS: Record<SearchCategory | 'all', string> = {
|
||||
all: 'All',
|
||||
const KNOWN_CATEGORY_LABELS: Record<string, string> = {
|
||||
application: 'Applications',
|
||||
exchange: 'Exchanges',
|
||||
attribute: 'Attributes',
|
||||
@@ -28,8 +27,8 @@ const CATEGORY_LABELS: Record<SearchCategory | 'all', string> = {
|
||||
agent: 'Agents',
|
||||
}
|
||||
|
||||
const ALL_CATEGORIES: Array<SearchCategory | 'all'> = [
|
||||
'all',
|
||||
/** Preferred display order for known categories */
|
||||
const KNOWN_CATEGORY_ORDER: string[] = [
|
||||
'application',
|
||||
'exchange',
|
||||
'attribute',
|
||||
@@ -37,6 +36,13 @@ const ALL_CATEGORIES: Array<SearchCategory | 'all'> = [
|
||||
'agent',
|
||||
]
|
||||
|
||||
function categoryLabel(cat: string): string {
|
||||
if (cat === 'all') return 'All'
|
||||
if (KNOWN_CATEGORY_LABELS[cat]) return KNOWN_CATEGORY_LABELS[cat]
|
||||
// Title-case unknown categories: "my-thing" → "My Thing", "foo_bar" → "Foo Bar"
|
||||
return cat.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
function highlightText(text: string, query: string, matchRanges?: [number, number][]): ReactNode {
|
||||
if (!query && (!matchRanges || matchRanges.length === 0)) return text
|
||||
|
||||
@@ -69,7 +75,7 @@ function highlightText(text: string, query: string, matchRanges?: [number, numbe
|
||||
|
||||
export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryChange, onSubmit }: CommandPaletteProps) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [activeCategory, setActiveCategory] = useState<SearchCategory | 'all'>('all')
|
||||
const [activeCategory, setActiveCategory] = useState<string>('all')
|
||||
const [scopeFilters, setScopeFilters] = useState<ScopeFilter[]>([])
|
||||
const [focusedIdx, setFocusedIdx] = useState(0)
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
@@ -128,7 +134,7 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
|
||||
|
||||
// Group results by category
|
||||
const grouped = useMemo(() => {
|
||||
const map = new Map<SearchCategory, SearchResult[]>()
|
||||
const map = new Map<string, SearchResult[]>()
|
||||
for (const r of filtered) {
|
||||
if (!map.has(r.category)) map.set(r.category, [])
|
||||
map.get(r.category)!.push(r)
|
||||
@@ -148,6 +154,19 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
|
||||
return counts
|
||||
}, [queryFiltered])
|
||||
|
||||
// Build tab list dynamically: 'all' + known categories (in order) + any unknown categories found in data
|
||||
const visibleCategories = useMemo(() => {
|
||||
const dataCategories = new Set(data.map((r) => r.category))
|
||||
const tabs: string[] = ['all']
|
||||
for (const cat of KNOWN_CATEGORY_ORDER) {
|
||||
if (dataCategories.has(cat)) tabs.push(cat)
|
||||
}
|
||||
for (const cat of dataCategories) {
|
||||
if (!tabs.includes(cat)) tabs.push(cat)
|
||||
}
|
||||
return tabs
|
||||
}, [data])
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
@@ -246,7 +265,7 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
|
||||
|
||||
{/* Category tabs */}
|
||||
<div className={styles.tabs} role="tablist">
|
||||
{ALL_CATEGORIES.map((cat) => (
|
||||
{visibleCategories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
role="tab"
|
||||
@@ -262,7 +281,7 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
|
||||
setFocusedIdx(0)
|
||||
}}
|
||||
>
|
||||
{CATEGORY_LABELS[cat]}
|
||||
{categoryLabel(cat)}
|
||||
{categoryCounts[cat] != null && (
|
||||
<span className={styles.tabCount}>{categoryCounts[cat]}</span>
|
||||
)}
|
||||
@@ -283,7 +302,7 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
|
||||
Array.from(grouped.entries()).map(([category, items]) => (
|
||||
<div key={category} className={styles.group}>
|
||||
<div className={styles.groupHeader}>
|
||||
<SectionHeader>{CATEGORY_LABELS[category]}</SectionHeader>
|
||||
<SectionHeader>{categoryLabel(category)}</SectionHeader>
|
||||
</div>
|
||||
{items.map((result) => {
|
||||
const flatIdx = flatResults.indexOf(result)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export type SearchCategory = 'application' | 'exchange' | 'attribute' | 'route' | 'agent'
|
||||
/** Known categories: 'application' | 'exchange' | 'attribute' | 'route' | 'agent'. Custom categories are rendered with title-cased labels and a default icon. */
|
||||
export type SearchCategory = string
|
||||
|
||||
export interface SearchResult {
|
||||
id: string
|
||||
|
||||
Reference in New Issue
Block a user