input-group.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import * as React from 'react'
  2. import { cva, type VariantProps } from 'class-variance-authority'
  3. import { cn } from '../../utils'
  4. import { Button } from '../ui/button'
  5. import { Input } from '../ui/input'
  6. import { Textarea } from '../ui/textarea'
  7. function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
  8. return (
  9. <div
  10. data-slot="input-group"
  11. role="group"
  12. className={cn(
  13. 'group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]',
  14. 'h-9 has-[>textarea]:h-auto',
  15. // Variants based on alignment.
  16. 'has-[>[data-align=inline-start]]:[&>input]:pl-2',
  17. 'has-[>[data-align=inline-end]]:[&>input]:pr-2',
  18. 'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
  19. 'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
  20. // Focus state.
  21. 'has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1',
  22. // Error state.
  23. 'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
  24. className
  25. )}
  26. {...props}
  27. />
  28. )
  29. }
  30. const inputGroupAddonVariants = cva(
  31. "text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
  32. {
  33. variants: {
  34. align: {
  35. 'inline-start':
  36. 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
  37. 'inline-end':
  38. 'order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]',
  39. 'block-start':
  40. '[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5',
  41. 'block-end':
  42. '[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5',
  43. },
  44. },
  45. defaultVariants: {
  46. align: 'inline-start',
  47. },
  48. }
  49. )
  50. function InputGroupAddon({
  51. className,
  52. align = 'inline-start',
  53. ...props
  54. }: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
  55. return (
  56. <div
  57. role="group"
  58. data-slot="input-group-addon"
  59. data-align={align}
  60. className={cn(inputGroupAddonVariants({ align }), className)}
  61. onClick={e => {
  62. if ((e.target as HTMLElement).closest('button')) {
  63. return
  64. }
  65. e.currentTarget.parentElement?.querySelector('input')?.focus()
  66. }}
  67. {...props}
  68. />
  69. )
  70. }
  71. const inputGroupButtonVariants = cva(
  72. 'flex items-center gap-2 text-sm shadow-none',
  73. {
  74. variants: {
  75. size: {
  76. xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
  77. sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',
  78. 'icon-xs':
  79. 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
  80. 'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
  81. },
  82. },
  83. defaultVariants: {
  84. size: 'xs',
  85. },
  86. }
  87. )
  88. function InputGroupButton({
  89. className,
  90. type = 'button',
  91. variant = 'ghost',
  92. size = 'xs',
  93. ...props
  94. }: Omit<React.ComponentProps<typeof Button>, 'size'> &
  95. VariantProps<typeof inputGroupButtonVariants>) {
  96. return (
  97. <Button
  98. type={type}
  99. data-size={size}
  100. variant={variant}
  101. className={cn(inputGroupButtonVariants({ size }), className)}
  102. {...props}
  103. />
  104. )
  105. }
  106. function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
  107. return (
  108. <span
  109. className={cn(
  110. "text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
  111. className
  112. )}
  113. {...props}
  114. />
  115. )
  116. }
  117. function InputGroupInput({
  118. className,
  119. ...props
  120. }: React.ComponentProps<'input'>) {
  121. return (
  122. <Input
  123. data-slot="input-group-control"
  124. className={cn(
  125. 'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
  126. className
  127. )}
  128. {...props}
  129. />
  130. )
  131. }
  132. function InputGroupTextarea({
  133. className,
  134. ...props
  135. }: React.ComponentProps<'textarea'>) {
  136. return (
  137. <Textarea
  138. data-slot="input-group-control"
  139. className={cn(
  140. 'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
  141. className
  142. )}
  143. {...props}
  144. />
  145. )
  146. }
  147. export {
  148. InputGroup,
  149. InputGroupAddon,
  150. InputGroupButton,
  151. InputGroupText,
  152. InputGroupInput,
  153. InputGroupTextarea,
  154. }