bubble.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  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. function BubbleGroup({ className, ...props }: React.ComponentProps<'div'>) {
  7. return (
  8. <div
  9. data-slot="bubble-group"
  10. className={cn('gap-2 flex min-w-0 flex-col', className)}
  11. {...props}
  12. />
  13. );
  14. }
  15. const bubbleVariants = cva(
  16. 'gap-1 data-[align=end]:self-end max-w-[80%] data-[variant=ghost]:max-w-full group-data-[align=end]/message:self-end group/bubble relative flex w-fit min-w-0 flex-col',
  17. {
  18. variants: {
  19. variant: {
  20. default:
  21. '*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80',
  22. secondary:
  23. '*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]',
  24. muted:
  25. '*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]',
  26. tinted:
  27. '*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]',
  28. outline:
  29. '*:data-[slot=bubble-content]:bg-background *:data-[slot=bubble-content]:border-border [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30',
  30. ghost:
  31. '*:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50 border-none',
  32. destructive:
  33. '*:data-[slot=bubble-content]:bg-destructive/10 dark:*:data-[slot=bubble-content]:bg-destructive/20 *:data-[slot=bubble-content]:text-destructive [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30',
  34. },
  35. },
  36. defaultVariants: {
  37. variant: 'default',
  38. },
  39. },
  40. );
  41. function Bubble({
  42. variant = 'default',
  43. align = 'start',
  44. className,
  45. ...props
  46. }: React.ComponentProps<'div'> &
  47. VariantProps<typeof bubbleVariants> & {
  48. align?: 'start' | 'end';
  49. }) {
  50. return (
  51. <div
  52. data-slot="bubble"
  53. data-variant={variant}
  54. data-align={align}
  55. className={cn(bubbleVariants({ variant }), className)}
  56. {...props}
  57. />
  58. );
  59. }
  60. function BubbleContent({
  61. className,
  62. render,
  63. ...props
  64. }: useRender.ComponentProps<'div'>) {
  65. return useRender({
  66. defaultTagName: 'div',
  67. props: mergeProps<'div'>(
  68. {
  69. className: cn(
  70. 'rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50 group-data-[align=end]/bubble:self-end w-fit max-w-full min-w-0 overflow-hidden wrap-break-word [button]:text-left [button,a]:transition-colors',
  71. className,
  72. ),
  73. },
  74. props,
  75. ),
  76. render,
  77. state: {
  78. slot: 'bubble-content',
  79. },
  80. });
  81. }
  82. const bubbleReactionsVariants = cva(
  83. 'rounded-full ring-3 ring-card bg-muted shrink-0 gap-1 px-1.5 py-0.5 has-[button]:p-0 text-sm absolute z-10 flex w-fit items-center justify-center',
  84. {
  85. variants: {
  86. side: {
  87. top: 'top-0 -translate-y-3/4',
  88. bottom: 'bottom-0 translate-y-3/4',
  89. },
  90. align: {
  91. start: 'left-3',
  92. end: 'right-3',
  93. },
  94. },
  95. defaultVariants: {
  96. side: 'bottom',
  97. align: 'end',
  98. },
  99. },
  100. );
  101. function BubbleReactions({
  102. side = 'bottom',
  103. align = 'end',
  104. className,
  105. ...props
  106. }: React.ComponentProps<'div'> & {
  107. align?: 'start' | 'end';
  108. side?: 'top' | 'bottom';
  109. }) {
  110. return (
  111. <div
  112. data-slot="bubble-reactions"
  113. data-align={align}
  114. data-side={side}
  115. className={cn(bubbleReactionsVariants({ side, align }), className)}
  116. {...props}
  117. />
  118. );
  119. }
  120. export { BubbleGroup, Bubble, BubbleContent, BubbleReactions };