attachment.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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 { Button } from '@/components/ui/button';
  7. const attachmentVariants = cva(
  8. 'rounded-xl w-fit focus-within:ring-1 focus-within:ring-ring/50 group/attachment relative flex max-w-full min-w-0 shrink-0 flex-wrap border bg-card text-card-foreground transition-colors has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed',
  9. {
  10. variants: {
  11. size: {
  12. default:
  13. 'gap-2 has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2 text-sm',
  14. sm: 'gap-2.5 has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5 text-xs',
  15. xs: 'gap-1.5 has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1 text-xs rounded-lg',
  16. },
  17. orientation: {
  18. horizontal: 'min-w-40 items-center',
  19. vertical: 'w-24 has-data-[slot=attachment-content]:w-30 flex-col',
  20. },
  21. },
  22. },
  23. );
  24. function Attachment({
  25. className,
  26. state = 'done',
  27. size = 'default',
  28. orientation = 'horizontal',
  29. ...props
  30. }: React.ComponentProps<'div'> &
  31. VariantProps<typeof attachmentVariants> & {
  32. state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done';
  33. }) {
  34. return (
  35. <div
  36. data-slot="attachment"
  37. data-state={state}
  38. data-size={size}
  39. data-orientation={orientation}
  40. className={cn(attachmentVariants({ size, orientation }), className)}
  41. {...props}
  42. />
  43. );
  44. }
  45. const attachmentMediaVariants = cva(
  46. "bg-muted text-foreground w-10 rounded-lg group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md [&_svg:not([class*='size-'])]:size-4 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5 group-data-[orientation=vertical]/attachment:w-full group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! relative flex aspect-square shrink-0 items-center justify-center overflow-hidden group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive [&_svg]:pointer-events-none",
  47. {
  48. variants: {
  49. variant: {
  50. icon: '',
  51. image:
  52. 'opacity-60 group-data-[state=idle]/attachment:opacity-100 group-data-[state=done]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover',
  53. },
  54. },
  55. defaultVariants: {
  56. variant: 'icon',
  57. },
  58. },
  59. );
  60. function AttachmentMedia({
  61. className,
  62. variant = 'icon',
  63. ...props
  64. }: React.ComponentProps<'div'> & VariantProps<typeof attachmentMediaVariants>) {
  65. return (
  66. <div
  67. data-slot="attachment-media"
  68. data-variant={variant}
  69. className={cn(attachmentMediaVariants({ variant }), className)}
  70. {...props}
  71. />
  72. );
  73. }
  74. function AttachmentContent({
  75. className,
  76. ...props
  77. }: React.ComponentProps<'div'>) {
  78. return (
  79. <div
  80. data-slot="attachment-content"
  81. className={cn(
  82. 'leading-tight group-data-[orientation=vertical]/attachment:px-1 max-w-full min-w-0 flex-1',
  83. className,
  84. )}
  85. {...props}
  86. />
  87. );
  88. }
  89. function AttachmentTitle({
  90. className,
  91. ...props
  92. }: React.ComponentProps<'span'>) {
  93. return (
  94. <span
  95. data-slot="attachment-title"
  96. className={cn(
  97. 'font-medium block max-w-full min-w-0 truncate group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer',
  98. className,
  99. )}
  100. {...props}
  101. />
  102. );
  103. }
  104. function AttachmentDescription({
  105. className,
  106. ...props
  107. }: React.ComponentProps<'span'>) {
  108. return (
  109. <span
  110. data-slot="attachment-description"
  111. className={cn(
  112. 'mt-0.5 text-xs block min-w-0 truncate text-muted-foreground group-data-[state=error]/attachment:text-destructive/80',
  113. 'max-w-full',
  114. className,
  115. )}
  116. {...props}
  117. />
  118. );
  119. }
  120. function AttachmentActions({
  121. className,
  122. ...props
  123. }: React.ComponentProps<'div'>) {
  124. return (
  125. <div
  126. data-slot="attachment-actions"
  127. className={cn(
  128. 'group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 relative z-20 group-data-[orientation=vertical]/attachment:gap-1 flex shrink-0 items-center',
  129. className,
  130. )}
  131. {...props}
  132. />
  133. );
  134. }
  135. function AttachmentAction({
  136. className,
  137. variant,
  138. size = 'icon-xs',
  139. ...props
  140. }: React.ComponentProps<typeof Button>) {
  141. return (
  142. <Button
  143. data-slot="attachment-action"
  144. variant={variant ?? 'ghost'}
  145. size={size}
  146. className={cn(className)}
  147. {...props}
  148. />
  149. );
  150. }
  151. function AttachmentTrigger({
  152. className,
  153. render,
  154. type,
  155. ...props
  156. }: useRender.ComponentProps<'button'>) {
  157. return useRender({
  158. defaultTagName: 'button',
  159. props: mergeProps<'button'>(
  160. {
  161. type: render ? type : (type ?? 'button'),
  162. className: cn('absolute inset-0 z-10 outline-none', className),
  163. },
  164. props,
  165. ),
  166. render,
  167. state: {
  168. slot: 'attachment-trigger',
  169. },
  170. });
  171. }
  172. function AttachmentGroup({ className, ...props }: React.ComponentProps<'div'>) {
  173. return (
  174. <div
  175. data-slot="attachment-group"
  176. className={cn(
  177. 'gap-3 scroll-px-1 py-1 flex min-w-0 scroll-fade-x snap-x snap-mandatory scrollbar-none overflow-x-auto overscroll-x-contain *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start',
  178. className,
  179. )}
  180. {...props}
  181. />
  182. );
  183. }
  184. export {
  185. Attachment,
  186. AttachmentGroup,
  187. AttachmentMedia,
  188. AttachmentContent,
  189. AttachmentTitle,
  190. AttachmentDescription,
  191. AttachmentActions,
  192. AttachmentAction,
  193. AttachmentTrigger,
  194. };