item.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import * as React from 'react';
  2. import { mergeProps } from '@base-ui/react/merge-props';
  3. import { useRender } from '@base-ui/react/use-render';
  4. import { cva, type VariantProps } from 'class-variance-authority';
  5. import { cn } from '@/lib/utils';
  6. import { Separator } from '@/components/ui/separator';
  7. function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
  8. return (
  9. <div
  10. role="list"
  11. data-slot="item-group"
  12. className={cn(
  13. 'gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2 group/item-group flex w-full flex-col',
  14. className,
  15. )}
  16. {...props}
  17. />
  18. );
  19. }
  20. function ItemSeparator({
  21. className,
  22. ...props
  23. }: React.ComponentProps<typeof Separator>) {
  24. return (
  25. <Separator
  26. data-slot="item-separator"
  27. orientation="horizontal"
  28. className={cn('my-2', className)}
  29. {...props}
  30. />
  31. );
  32. }
  33. const itemVariants = cva(
  34. '[a]:hover:bg-muted rounded-lg border text-sm group/item flex w-full flex-wrap items-center transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors',
  35. {
  36. variants: {
  37. variant: {
  38. default: 'border-transparent',
  39. outline: 'border-border',
  40. muted: 'bg-muted/50 border-transparent',
  41. },
  42. size: {
  43. default: 'gap-2.5 px-3 py-2.5',
  44. sm: 'gap-2.5 px-3 py-2.5',
  45. xs: 'gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
  46. },
  47. },
  48. defaultVariants: {
  49. variant: 'default',
  50. size: 'default',
  51. },
  52. },
  53. );
  54. function Item({
  55. className,
  56. variant = 'default',
  57. size = 'default',
  58. render,
  59. ...props
  60. }: useRender.ComponentProps<'div'> & VariantProps<typeof itemVariants>) {
  61. return useRender({
  62. defaultTagName: 'div',
  63. props: mergeProps<'div'>(
  64. {
  65. className: cn(itemVariants({ variant, size, className })),
  66. },
  67. props,
  68. ),
  69. render,
  70. state: {
  71. slot: 'item',
  72. variant,
  73. size,
  74. },
  75. });
  76. }
  77. const itemMediaVariants = cva(
  78. 'gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none',
  79. {
  80. variants: {
  81. variant: {
  82. default: 'bg-transparent',
  83. icon: "[&_svg:not([class*='size-'])]:size-4",
  84. image:
  85. 'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
  86. },
  87. },
  88. defaultVariants: {
  89. variant: 'default',
  90. },
  91. },
  92. );
  93. function ItemMedia({
  94. className,
  95. variant = 'default',
  96. ...props
  97. }: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
  98. return (
  99. <div
  100. data-slot="item-media"
  101. data-variant={variant}
  102. className={cn(itemMediaVariants({ variant, className }))}
  103. {...props}
  104. />
  105. );
  106. }
  107. function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
  108. return (
  109. <div
  110. data-slot="item-content"
  111. className={cn(
  112. 'gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none',
  113. className,
  114. )}
  115. {...props}
  116. />
  117. );
  118. }
  119. function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
  120. return (
  121. <div
  122. data-slot="item-title"
  123. className={cn(
  124. 'gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center',
  125. className,
  126. )}
  127. {...props}
  128. />
  129. );
  130. }
  131. function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
  132. return (
  133. <p
  134. data-slot="item-description"
  135. className={cn(
  136. 'text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
  137. className,
  138. )}
  139. {...props}
  140. />
  141. );
  142. }
  143. function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
  144. return (
  145. <div
  146. data-slot="item-actions"
  147. className={cn('gap-2 flex items-center', className)}
  148. {...props}
  149. />
  150. );
  151. }
  152. function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
  153. return (
  154. <div
  155. data-slot="item-header"
  156. className={cn(
  157. 'gap-2 flex basis-full items-center justify-between',
  158. className,
  159. )}
  160. {...props}
  161. />
  162. );
  163. }
  164. function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
  165. return (
  166. <div
  167. data-slot="item-footer"
  168. className={cn(
  169. 'gap-2 flex basis-full items-center justify-between',
  170. className,
  171. )}
  172. {...props}
  173. />
  174. );
  175. }
  176. export {
  177. Item,
  178. ItemMedia,
  179. ItemContent,
  180. ItemActions,
  181. ItemGroup,
  182. ItemSeparator,
  183. ItemTitle,
  184. ItemDescription,
  185. ItemHeader,
  186. ItemFooter,
  187. };