command.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. "use client"
  2. // shadcn Command (cmdk 기반) — D6 §5.2 Ctrl+K 커맨드 팔레트용
  3. // 이 프로젝트는 Radix 대신 @base-ui/react 를 쓰지만 cmdk 는 독립 패키지라 그대로 사용 가능.
  4. // 팔레트 Dialog 는 기존 components/ui/dialog(@base-ui) 를 재사용해 포커스 트랩·aria-modal 확보.
  5. import * as React from "react"
  6. import { Command as CommandPrimitive } from "cmdk"
  7. import { Search } from "lucide-react"
  8. import { cn } from "@/lib/utils/client"
  9. import { Dialog, DialogContent } from "@/components/ui/dialog"
  10. const Command = React.forwardRef<
  11. React.ElementRef<typeof CommandPrimitive>,
  12. React.ComponentPropsWithoutRef<typeof CommandPrimitive>
  13. >(({ className, ...props }, ref) => (
  14. <CommandPrimitive
  15. ref={ref}
  16. className={cn(
  17. "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
  18. className
  19. )}
  20. {...props}
  21. />
  22. ))
  23. Command.displayName = CommandPrimitive.displayName
  24. type CommandDialogProps = React.ComponentPropsWithoutRef<typeof CommandPrimitive> & {
  25. open: boolean
  26. onOpenChange: (_open: boolean) => void
  27. label?: string
  28. }
  29. const CommandDialog = ({ open, onOpenChange, label, children, ...props }: CommandDialogProps) => {
  30. return (
  31. <Dialog open={open} onOpenChange={onOpenChange}>
  32. <DialogContent className="overflow-hidden p-0">
  33. <Command
  34. label={label}
  35. className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
  36. {...props}
  37. >
  38. {children}
  39. </Command>
  40. </DialogContent>
  41. </Dialog>
  42. )
  43. }
  44. const CommandInput = React.forwardRef<
  45. React.ElementRef<typeof CommandPrimitive.Input>,
  46. React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
  47. >(({ className, ...props }, ref) => (
  48. <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
  49. <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
  50. <CommandPrimitive.Input
  51. ref={ref}
  52. className={cn(
  53. "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
  54. className
  55. )}
  56. {...props}
  57. />
  58. </div>
  59. ))
  60. CommandInput.displayName = CommandPrimitive.Input.displayName
  61. const CommandList = React.forwardRef<
  62. React.ElementRef<typeof CommandPrimitive.List>,
  63. React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
  64. >(({ className, ...props }, ref) => (
  65. <CommandPrimitive.List
  66. ref={ref}
  67. className={cn("max-h-[360px] overflow-y-auto overflow-x-hidden", className)}
  68. {...props}
  69. />
  70. ))
  71. CommandList.displayName = CommandPrimitive.List.displayName
  72. const CommandEmpty = React.forwardRef<
  73. React.ElementRef<typeof CommandPrimitive.Empty>,
  74. React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
  75. >((props, ref) => (
  76. <CommandPrimitive.Empty
  77. ref={ref}
  78. className="py-6 text-center text-sm text-muted-foreground"
  79. {...props}
  80. />
  81. ))
  82. CommandEmpty.displayName = CommandPrimitive.Empty.displayName
  83. const CommandGroup = React.forwardRef<
  84. React.ElementRef<typeof CommandPrimitive.Group>,
  85. React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
  86. >(({ className, ...props }, ref) => (
  87. <CommandPrimitive.Group
  88. ref={ref}
  89. className={cn(
  90. "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
  91. className
  92. )}
  93. {...props}
  94. />
  95. ))
  96. CommandGroup.displayName = CommandPrimitive.Group.displayName
  97. const CommandSeparator = React.forwardRef<
  98. React.ElementRef<typeof CommandPrimitive.Separator>,
  99. React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
  100. >(({ className, ...props }, ref) => (
  101. <CommandPrimitive.Separator
  102. ref={ref}
  103. className={cn("-mx-1 h-px bg-border", className)}
  104. {...props}
  105. />
  106. ))
  107. CommandSeparator.displayName = CommandPrimitive.Separator.displayName
  108. const CommandItem = React.forwardRef<
  109. React.ElementRef<typeof CommandPrimitive.Item>,
  110. React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
  111. >(({ className, ...props }, ref) => (
  112. <CommandPrimitive.Item
  113. ref={ref}
  114. className={cn(
  115. "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  116. className
  117. )}
  118. {...props}
  119. />
  120. ))
  121. CommandItem.displayName = CommandPrimitive.Item.displayName
  122. const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
  123. return (
  124. <span
  125. className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
  126. {...props}
  127. />
  128. )
  129. }
  130. CommandShortcut.displayName = "CommandShortcut"
  131. export {
  132. Command,
  133. CommandDialog,
  134. CommandInput,
  135. CommandList,
  136. CommandEmpty,
  137. CommandGroup,
  138. CommandItem,
  139. CommandShortcut,
  140. CommandSeparator,
  141. }