field.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. 'use client';
  2. import { useMemo } from 'react';
  3. import { cva, type VariantProps } from 'class-variance-authority';
  4. import { cn } from '@/lib/utils';
  5. import { Label } from '@/components/ui/label';
  6. import { Separator } from '@/components/ui/separator';
  7. function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
  8. return (
  9. <fieldset
  10. data-slot="field-set"
  11. className={cn(
  12. 'gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col',
  13. className,
  14. )}
  15. {...props}
  16. />
  17. );
  18. }
  19. function FieldLegend({
  20. className,
  21. variant = 'legend',
  22. ...props
  23. }: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
  24. return (
  25. <legend
  26. data-slot="field-legend"
  27. data-variant={variant}
  28. className={cn(
  29. 'mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
  30. className,
  31. )}
  32. {...props}
  33. />
  34. );
  35. }
  36. function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
  37. return (
  38. <div
  39. data-slot="field-group"
  40. className={cn(
  41. 'gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4 group/field-group @container/field-group flex w-full flex-col',
  42. className,
  43. )}
  44. {...props}
  45. />
  46. );
  47. }
  48. const fieldVariants = cva(
  49. 'data-[invalid=true]:text-destructive gap-2 group/field flex w-full',
  50. {
  51. variants: {
  52. orientation: {
  53. vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
  54. horizontal:
  55. 'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
  56. responsive:
  57. 'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
  58. },
  59. },
  60. defaultVariants: {
  61. orientation: 'vertical',
  62. },
  63. },
  64. );
  65. function Field({
  66. className,
  67. orientation = 'vertical',
  68. ...props
  69. }: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
  70. return (
  71. <div
  72. role="group"
  73. data-slot="field"
  74. data-orientation={orientation}
  75. className={cn(fieldVariants({ orientation }), className)}
  76. {...props}
  77. />
  78. );
  79. }
  80. function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
  81. return (
  82. <div
  83. data-slot="field-content"
  84. className={cn(
  85. 'gap-0.5 group/field-content flex flex-1 flex-col leading-snug',
  86. className,
  87. )}
  88. {...props}
  89. />
  90. );
  91. }
  92. function FieldLabel({
  93. className,
  94. ...props
  95. }: React.ComponentProps<typeof Label>) {
  96. return (
  97. <Label
  98. data-slot="field-label"
  99. className={cn(
  100. 'has-data-checked:bg-primary/5 has-data-checked:border-primary/30 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted/50 has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:ring-ring/50 has-[>[data-slot=field]]:has-[:focus-visible]:ring-3 *:data-[slot=field]:p-2.5 group/field-label peer/field-label flex w-fit',
  101. 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
  102. className,
  103. )}
  104. {...props}
  105. />
  106. );
  107. }
  108. function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
  109. return (
  110. <div
  111. data-slot="field-label"
  112. className={cn(
  113. 'gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50 flex w-fit items-center',
  114. className,
  115. )}
  116. {...props}
  117. />
  118. );
  119. }
  120. function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
  121. return (
  122. <p
  123. data-slot="field-description"
  124. className={cn(
  125. 'text-muted-foreground text-left text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-data-horizontal/field:text-balance',
  126. 'last:mt-0 nth-last-2:-mt-1',
  127. '[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
  128. className,
  129. )}
  130. {...props}
  131. />
  132. );
  133. }
  134. function FieldSeparator({
  135. children,
  136. className,
  137. ...props
  138. }: React.ComponentProps<'div'> & {
  139. children?: React.ReactNode;
  140. }) {
  141. return (
  142. <div
  143. data-slot="field-separator"
  144. data-content={!!children}
  145. className={cn(
  146. '-my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2 relative',
  147. className,
  148. )}
  149. {...props}
  150. >
  151. <Separator className="absolute inset-0 top-1/2" />
  152. {children && (
  153. <span
  154. className="text-muted-foreground px-2 relative mx-auto block w-fit bg-background"
  155. data-slot="field-separator-content"
  156. >
  157. {children}
  158. </span>
  159. )}
  160. </div>
  161. );
  162. }
  163. function FieldError({
  164. className,
  165. children,
  166. errors,
  167. ...props
  168. }: React.ComponentProps<'div'> & {
  169. errors?: Array<{ message?: string } | undefined>;
  170. }) {
  171. const content = useMemo(() => {
  172. if (children) {
  173. return children;
  174. }
  175. if (!errors?.length) {
  176. return null;
  177. }
  178. const uniqueErrors = [
  179. ...new Map(errors.map((error) => [error?.message, error])).values(),
  180. ];
  181. if (uniqueErrors?.length == 1) {
  182. return uniqueErrors[0]?.message;
  183. }
  184. return (
  185. <ul className="ml-4 flex list-disc flex-col gap-1">
  186. {uniqueErrors.map(
  187. (error, index) =>
  188. error?.message && <li key={index}>{error.message}</li>,
  189. )}
  190. </ul>
  191. );
  192. }, [children, errors]);
  193. if (!content) {
  194. return null;
  195. }
  196. return (
  197. <div
  198. role="alert"
  199. data-slot="field-error"
  200. className={cn('text-destructive text-sm font-normal', className)}
  201. {...props}
  202. >
  203. {content}
  204. </div>
  205. );
  206. }
  207. export {
  208. Field,
  209. FieldLabel,
  210. FieldDescription,
  211. FieldError,
  212. FieldGroup,
  213. FieldLegend,
  214. FieldSeparator,
  215. FieldSet,
  216. FieldContent,
  217. FieldTitle,
  218. };