| 1 | "use client"; |
| 2 | |
| 3 | import { cva, type VariantProps } from "class-variance-authority"; |
| 4 | import type * as React from "react"; |
| 5 | |
| 6 | import { cn } from "~/lib/utils"; |
| 7 | import { Input, type InputProps } from "~/components/ui/input"; |
| 8 | |
| 9 | const inputGroupVariants = cva( |
| 10 | "relative inline-flex w-full min-w-0 items-center rounded-[var(--control-radius)] border text-base text-foreground ring-ring/24 transition-shadow has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] has-[input:focus-visible,textarea:focus-visible]:ring-inset sm:text-sm dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--control-radius)-1px)]", |
| 11 | { |
| 12 | defaultVariants: { |
| 13 | variant: "default", |
| 14 | size: "default", |
| 15 | }, |
| 16 | variants: { |
| 17 | size: { |
| 18 | default: null, |
| 19 | lg: "h-9", |
| 20 | }, |
| 21 | variant: { |
| 22 | default: |
| 23 | "border-input bg-background not-dark:bg-clip-padding shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--control-radius)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)]", |
| 24 | // Matches floating glass cards (no shadow); focus darkens the hairline instead of a ring. |
| 25 | popover: |
| 26 | "dropdown-glass rounded-2xl has-[input:focus-visible,textarea:focus-visible]:ring-0 has-[input:focus-visible,textarea:focus-visible]:border-[color-mix(in_srgb,var(--contrast-foreground)_18%,transparent)]", |
| 27 | ghost: |
| 28 | "border-transparent bg-transparent shadow-none hover:bg-muted/40 has-[input:focus-visible,textarea:focus-visible]:bg-background", |
| 29 | }, |
| 30 | }, |
| 31 | }, |
| 32 | ); |
| 33 | |
| 34 | function InputGroup({ |
| 35 | className, |
| 36 | variant, |
| 37 | size, |
| 38 | ...props |
| 39 | }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupVariants>) { |
| 40 | return ( |
| 41 | <div |
| 42 | className={cn(inputGroupVariants({ variant, size }), className)} |
| 43 | data-slot="input-group" |
| 44 | role="group" |
| 45 | {...props} |
| 46 | /> |
| 47 | ); |
| 48 | } |
| 49 | |
| 50 | const inputGroupAddonVariants = cva( |
| 51 | "[&_svg]:-mx-0.5 flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80", |
| 52 | { |
| 53 | defaultVariants: { |
| 54 | align: "inline-start", |
| 55 | }, |
| 56 | compoundVariants: [ |
| 57 | // Pill groups curve too tightly for buttons pulled to the edge; keep them inset. |
| 58 | { |
| 59 | align: "inline-end", |
| 60 | inset: "pill", |
| 61 | className: "has-[>button]:me-0 pe-1 [[data-size=sm]+&]:pe-1 gap-0.5", |
| 62 | }, |
| 63 | { |
| 64 | align: "inline-start", |
| 65 | inset: "pill", |
| 66 | className: "has-[>button]:ms-0 ps-1 [[data-size=sm]+&]:ps-1 gap-0.5", |
| 67 | }, |
| 68 | ], |
| 69 | variants: { |
| 70 | inset: { |
| 71 | default: null, |
| 72 | pill: "[&_button]:rounded-full", |
| 73 | }, |
| 74 | align: { |
| 75 | "block-end": |
| 76 | "order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]", |
| 77 | "block-start": |
| 78 | "order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]", |
| 79 | "inline-end": |
| 80 | "has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 order-last pe-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]", |
| 81 | "inline-start": |
| 82 | "has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 order-first ps-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]", |
| 83 | }, |
| 84 | }, |
| 85 | }, |
| 86 | ); |
| 87 | |
| 88 | function InputGroupAddon({ |
| 89 | className, |
| 90 | align = "inline-start", |
| 91 | inset, |
| 92 | ...props |
| 93 | }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) { |
| 94 | return ( |
| 95 | <div |
| 96 | className={cn(inputGroupAddonVariants({ align, inset }), className)} |
| 97 | data-align={align} |
| 98 | data-slot="input-group-addon" |
| 99 | onMouseDown={(e) => { |
| 100 | const target = e.target as HTMLElement; |
| 101 | const isInteractive = target.closest( |
| 102 | "button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']", |
| 103 | ); |
| 104 | if (isInteractive) return; |
| 105 | e.preventDefault(); |
| 106 | const parent = e.currentTarget.parentElement; |
| 107 | const input = parent?.querySelector<HTMLInputElement | HTMLTextAreaElement>( |
| 108 | "input, textarea", |
| 109 | ); |
| 110 | if (input && !parent?.querySelector("input:focus, textarea:focus")) { |
| 111 | input.focus(); |
| 112 | } |
| 113 | }} |
| 114 | {...props} |
| 115 | /> |
| 116 | ); |
| 117 | } |
| 118 | |
| 119 | function InputGroupInput({ className, ...props }: InputProps) { |
| 120 | return <Input className={className} unstyled {...props} />; |
| 121 | } |
| 122 | |
| 123 | export { InputGroup, InputGroupAddon, InputGroupInput }; |