ContextSearchResults.shared.tsx111 lines · main
1'use client'
2
3import { cn, CommandList } from 'ui'
4import { ShimmeringLoader } from 'ui-patterns'
5import { TextHighlighter } from 'ui-patterns/CommandMenu'
6import { CommandMenuGroup } from 'ui-patterns/CommandMenu/internal/CommandMenuGroup'
7import { CommandMenuItem } from 'ui-patterns/CommandMenu/internal/CommandMenuItem'
8import type { IActionCommand, IRouteCommand } from 'ui-patterns/CommandMenu/internal/types'
9
10export interface SearchResult {
11 id: string
12 name: string
13 description?: string
14}
15
16export function SkeletonResults() {
17 return (
18 <div className="p-2 space-y-1">
19 {[0, 1, 2, 3].map((i) => (
20 <div key={i} className="flex items-center gap-3 px-2 py-2">
21 <ShimmeringLoader className="w-4! h-4! py-0! rounded-sm" delayIndex={i} />
22 <div className="flex-1 space-y-1">
23 <ShimmeringLoader className="w-32! py-1.5!" delayIndex={i} />
24 <ShimmeringLoader className="w-48! py-1!" delayIndex={i + 1} />
25 </div>
26 </div>
27 ))}
28 </div>
29 )
30}
31
32interface EmptyStateProps {
33 icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
34 label: string
35 query: string
36}
37
38export function EmptyState({ icon: Icon, label, query }: EmptyStateProps) {
39 return (
40 <div className="h-full flex flex-col items-center justify-center py-12 px-4 gap-4 text-center text-foreground-lighter">
41 <Icon className="h-6 w-6" strokeWidth={1.5} />
42 <p className="text-sm">
43 {query ? `No results found for "${query}"` : `Type to search in ${label}`}
44 </p>
45 </div>
46 )
47}
48
49interface ResultsListProps {
50 results: SearchResult[]
51 icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
52 getIcon?: (result: SearchResult) => React.ComponentType<React.SVGProps<SVGSVGElement>>
53 onResultClick?: (result: SearchResult) => void
54 getRoute?: (result: SearchResult) => `/${string}` | `http${string}`
55 className?: string
56}
57
58export function ResultsList({
59 results,
60 icon: Icon,
61 getIcon,
62 onResultClick,
63 getRoute,
64 className,
65}: ResultsListProps) {
66 const commands = results.map((result): IRouteCommand | IActionCommand => {
67 const ResultIcon = getIcon ? getIcon(result) : Icon
68 const baseCommand = {
69 id: result.id,
70 name: result.name,
71 value: result.description ? `${result.name} ${result.description}` : result.name,
72 icon: () => <ResultIcon className="h-4 w-4" strokeWidth={1.5} />,
73 }
74
75 if (getRoute) {
76 return {
77 ...baseCommand,
78 route: getRoute(result),
79 } as IRouteCommand
80 }
81
82 return {
83 ...baseCommand,
84 action: () => onResultClick?.(result),
85 } as IActionCommand
86 })
87
88 return (
89 <CommandList
90 className={cn(
91 'max-h-full! flex-1 min-h-0 overflow-y-auto overflow-x-hidden bg-transparent',
92 className
93 )}
94 >
95 <CommandMenuGroup>
96 {commands.map((command) => (
97 <CommandMenuItem key={command.id} command={command}>
98 <div className="flex flex-col min-w-0 text-foreground-light">
99 <TextHighlighter>{command.name}</TextHighlighter>
100 {command.value && command.value !== command.name && (
101 <p className="text-xs text-foreground-lighter/70 truncate mt-0.5">
102 {command.value.replace(command.name, '').trim()}
103 </p>
104 )}
105 </div>
106 </CommandMenuItem>
107 ))}
108 </CommandMenuGroup>
109 </CommandList>
110 )
111}