ContextSearchResults.shared.tsx 3.4 KB

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