@exxatdesignux/ui 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +72 -0
  2. package/src/components/ui/avatar.tsx +384 -0
  3. package/src/components/ui/badge.tsx +49 -0
  4. package/src/components/ui/banner.tsx +364 -0
  5. package/src/components/ui/breadcrumb.tsx +120 -0
  6. package/src/components/ui/button.tsx +66 -0
  7. package/src/components/ui/calendar.tsx +220 -0
  8. package/src/components/ui/card.tsx +136 -0
  9. package/src/components/ui/chart.tsx +378 -0
  10. package/src/components/ui/checkbox.tsx +160 -0
  11. package/src/components/ui/coach-mark.tsx +361 -0
  12. package/src/components/ui/collapsible.tsx +33 -0
  13. package/src/components/ui/command.tsx +232 -0
  14. package/src/components/ui/date-picker-field.tsx +186 -0
  15. package/src/components/ui/dialog.tsx +171 -0
  16. package/src/components/ui/drag-handle-grip.tsx +10 -0
  17. package/src/components/ui/drawer.tsx +134 -0
  18. package/src/components/ui/dropdown-menu.tsx +422 -0
  19. package/src/components/ui/field.tsx +238 -0
  20. package/src/components/ui/form.tsx +137 -0
  21. package/src/components/ui/input-group.tsx +156 -0
  22. package/src/components/ui/input-mask.tsx +135 -0
  23. package/src/components/ui/input.tsx +22 -0
  24. package/src/components/ui/kbd.tsx +55 -0
  25. package/src/components/ui/label.tsx +25 -0
  26. package/src/components/ui/payment-card-fields.tsx +65 -0
  27. package/src/components/ui/popover.tsx +46 -0
  28. package/src/components/ui/radio-group.tsx +217 -0
  29. package/src/components/ui/select.tsx +191 -0
  30. package/src/components/ui/selection-tile-grid.tsx +246 -0
  31. package/src/components/ui/separator.tsx +28 -0
  32. package/src/components/ui/sheet.tsx +147 -0
  33. package/src/components/ui/sidebar.tsx +716 -0
  34. package/src/components/ui/skeleton.tsx +13 -0
  35. package/src/components/ui/sonner.tsx +39 -0
  36. package/src/components/ui/status-badge.tsx +109 -0
  37. package/src/components/ui/table.tsx +117 -0
  38. package/src/components/ui/tabs.tsx +90 -0
  39. package/src/components/ui/textarea.tsx +18 -0
  40. package/src/components/ui/tip.tsx +21 -0
  41. package/src/components/ui/toggle-group.tsx +89 -0
  42. package/src/components/ui/toggle-switch.tsx +31 -0
  43. package/src/components/ui/toggle.tsx +48 -0
  44. package/src/components/ui/tooltip.tsx +59 -0
  45. package/src/components/ui/view-segmented-control.tsx +160 -0
  46. package/src/globals.css +1795 -0
  47. package/src/hooks/.gitkeep +0 -0
  48. package/src/hooks/use-app-theme.ts +172 -0
  49. package/src/hooks/use-coach-mark.ts +342 -0
  50. package/src/hooks/use-mobile.ts +31 -0
  51. package/src/hooks/use-mod-key-label.ts +29 -0
  52. package/src/index.ts +55 -0
  53. package/src/lib/compose-refs.ts +15 -0
  54. package/src/lib/date-filter.ts +67 -0
  55. package/src/lib/utils.ts +6 -0
  56. package/src/theme/apply-windows-contrast-theme.ts +29 -0
  57. package/src/theme/windows-contrast-theme.json +147 -0
  58. package/src/theme.css +1130 -0
  59. package/src/types/react-payment-inputs.d.ts +20 -0
@@ -0,0 +1,137 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Slot } from "radix-ui"
5
+ import {
6
+ Controller,
7
+ FormProvider,
8
+ useFormContext,
9
+ type ControllerProps,
10
+ type FieldPath,
11
+ type FieldValues,
12
+ } from "react-hook-form"
13
+
14
+ import { cn } from "../../lib/utils"
15
+ import { Label } from "./label"
16
+
17
+ const Form = FormProvider
18
+
19
+ interface FormFieldContextValue<
20
+ TFieldValues extends FieldValues = FieldValues,
21
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
22
+ > {
23
+ name: TName
24
+ }
25
+
26
+ const FormFieldContext = React.createContext<FormFieldContextValue>(
27
+ {} as FormFieldContextValue
28
+ )
29
+
30
+ function FormField<
31
+ TFieldValues extends FieldValues = FieldValues,
32
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
33
+ >({ ...props }: ControllerProps<TFieldValues, TName>) {
34
+ return (
35
+ <FormFieldContext.Provider value={{ name: props.name }}>
36
+ <Controller {...props} />
37
+ </FormFieldContext.Provider>
38
+ )
39
+ }
40
+
41
+ function useFormField() {
42
+ const fieldContext = React.useContext(FormFieldContext)
43
+ const itemContext = React.useContext(FormItemContext)
44
+ const { getFieldState, formState } = useFormContext()
45
+
46
+ const fieldState = getFieldState(fieldContext.name, formState)
47
+
48
+ if (!fieldContext) throw new Error("useFormField must be used inside <FormField>")
49
+
50
+ const { id } = itemContext
51
+
52
+ return {
53
+ id,
54
+ name: fieldContext.name,
55
+ formItemId: `${id}-form-item`,
56
+ formDescriptionId: `${id}-form-item-description`,
57
+ formMessageId: `${id}-form-item-message`,
58
+ ...fieldState,
59
+ }
60
+ }
61
+
62
+ interface FormItemContextValue { id: string }
63
+ const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
64
+
65
+ function FormItem({ className, ...props }: React.ComponentProps<"div">) {
66
+ const id = React.useId()
67
+ return (
68
+ <FormItemContext.Provider value={{ id }}>
69
+ <div data-slot="form-item" className={cn("flex flex-col gap-1.5", className)} {...props} />
70
+ </FormItemContext.Provider>
71
+ )
72
+ }
73
+
74
+ function FormLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
75
+ const { error, formItemId } = useFormField()
76
+ return (
77
+ <Label
78
+ data-slot="form-label"
79
+ data-error={!!error}
80
+ className={cn("data-[error=true]:text-destructive", className)}
81
+ htmlFor={formItemId}
82
+ {...props}
83
+ />
84
+ )
85
+ }
86
+
87
+ function FormControl({ ...props }: React.ComponentProps<typeof Slot.Root>) {
88
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
89
+ return (
90
+ <Slot.Root
91
+ data-slot="form-control"
92
+ id={formItemId}
93
+ aria-describedby={!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`}
94
+ aria-invalid={!!error}
95
+ {...props}
96
+ />
97
+ )
98
+ }
99
+
100
+ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
101
+ const { formDescriptionId } = useFormField()
102
+ return (
103
+ <p
104
+ data-slot="form-description"
105
+ id={formDescriptionId}
106
+ className={cn("text-muted-foreground text-sm", className)}
107
+ {...props}
108
+ />
109
+ )
110
+ }
111
+
112
+ function FormMessage({ className, children, ...props }: React.ComponentProps<"p">) {
113
+ const { error, formMessageId } = useFormField()
114
+ const body = error ? String(error?.message ?? "") : children
115
+ if (!body) return null
116
+ return (
117
+ <p
118
+ data-slot="form-message"
119
+ id={formMessageId}
120
+ className={cn("text-destructive text-sm", className)}
121
+ {...props}
122
+ >
123
+ {body}
124
+ </p>
125
+ )
126
+ }
127
+
128
+ export {
129
+ useFormField,
130
+ Form,
131
+ FormItem,
132
+ FormLabel,
133
+ FormControl,
134
+ FormDescription,
135
+ FormMessage,
136
+ FormField,
137
+ }
@@ -0,0 +1,156 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { Button } from "./button"
8
+ import { Input } from "./input"
9
+ import { Textarea } from "./textarea"
10
+
11
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
12
+ return (
13
+ <div
14
+ data-slot="input-group"
15
+ role="group"
16
+ className={cn(
17
+ "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pe-1.5 has-[>[data-align=inline-start]]:[&>input]:ps-1.5",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ const inputGroupAddonVariants = cva(
26
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
27
+ {
28
+ variants: {
29
+ align: {
30
+ "inline-start":
31
+ "order-first ps-2 has-[>button]:ms-[-0.3rem] has-[>kbd]:ms-[-0.15rem]",
32
+ "inline-end":
33
+ "order-last pe-2 has-[>button]:me-[-0.3rem] has-[>kbd]:me-[-0.15rem]",
34
+ "block-start":
35
+ "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
36
+ "block-end":
37
+ "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
38
+ },
39
+ },
40
+ defaultVariants: {
41
+ align: "inline-start",
42
+ },
43
+ }
44
+ )
45
+
46
+ function InputGroupAddon({
47
+ className,
48
+ align = "inline-start",
49
+ ...props
50
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
51
+ return (
52
+ <div
53
+ role="group"
54
+ data-slot="input-group-addon"
55
+ data-align={align}
56
+ className={cn(inputGroupAddonVariants({ align }), className)}
57
+ onClick={(e) => {
58
+ if ((e.target as HTMLElement).closest("button")) {
59
+ return
60
+ }
61
+ e.currentTarget.parentElement?.querySelector("input")?.focus()
62
+ }}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ const inputGroupButtonVariants = cva(
69
+ "flex items-center gap-2 text-sm shadow-none",
70
+ {
71
+ variants: {
72
+ size: {
73
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
74
+ sm: "",
75
+ "icon-xs":
76
+ "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
77
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
78
+ },
79
+ },
80
+ defaultVariants: {
81
+ size: "xs",
82
+ },
83
+ }
84
+ )
85
+
86
+ function InputGroupButton({
87
+ className,
88
+ type = "button",
89
+ variant = "ghost",
90
+ size = "xs",
91
+ ...props
92
+ }: Omit<React.ComponentProps<typeof Button>, "size"> &
93
+ VariantProps<typeof inputGroupButtonVariants>) {
94
+ return (
95
+ <Button
96
+ type={type}
97
+ data-size={size}
98
+ variant={variant}
99
+ className={cn(inputGroupButtonVariants({ size }), className)}
100
+ {...props}
101
+ />
102
+ )
103
+ }
104
+
105
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
106
+ return (
107
+ <span
108
+ className={cn(
109
+ "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
110
+ className
111
+ )}
112
+ {...props}
113
+ />
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 ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
126
+ className
127
+ )}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ function InputGroupTextarea({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<"textarea">) {
137
+ return (
138
+ <Textarea
139
+ data-slot="input-group-control"
140
+ className={cn(
141
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
142
+ className
143
+ )}
144
+ {...props}
145
+ />
146
+ )
147
+ }
148
+
149
+ export {
150
+ InputGroup,
151
+ InputGroupAddon,
152
+ InputGroupButton,
153
+ InputGroupText,
154
+ InputGroupInput,
155
+ InputGroupTextarea,
156
+ }
@@ -0,0 +1,135 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Input masking — aligned with [Shadcn Studio input-mask](https://shadcnstudio.com/docs/components/input-mask):
5
+ * `use-mask-input` for phone / date / time / custom / zip; use `PaymentCardFields` for card flows.
6
+ */
7
+
8
+ import * as React from "react"
9
+ import {
10
+ useMaskInput,
11
+ withMask,
12
+ useHookFormMask,
13
+ withHookFormMask,
14
+ useTanStackFormMask,
15
+ withTanStackFormMask,
16
+ } from "use-mask-input"
17
+ import type { Mask, Options } from "use-mask-input"
18
+
19
+ import { composeRefs } from "../../lib/compose-refs"
20
+
21
+ export { composeRefs } from "../../lib/compose-refs"
22
+ import { cn } from "../../lib/utils"
23
+ import { Input } from "./input"
24
+
25
+ /** Default mask UX per Shadcn Studio demos (underscore placeholder, no hover mask). */
26
+ export const exxatInputMaskDefaults: Options = {
27
+ placeholder: "_",
28
+ showMaskOnHover: false,
29
+ }
30
+
31
+ /** Common patterns (Inputmask `9` = digit, `a` = letter, `*` = alnum). */
32
+ export const exxatMaskPatterns = {
33
+ /** US NANP 10-digit display */
34
+ phoneUS: "(999) 999-9999",
35
+ /** US ZIP or ZIP+4 */
36
+ zipUS: "99999[-9999]",
37
+ /** Calendar-style date (validate separately). */
38
+ dateMDY: "99/99/9999",
39
+ /** Studio input-mask-01 style — UK-style plate-ish token; adjust per product. */
40
+ customReference: "AA99 AAA",
41
+ } as const satisfies Record<string, Mask>
42
+
43
+ export function exxatTimeMaskOptions(): Options & {
44
+ inputFormat: string
45
+ outputFormat: string
46
+ } {
47
+ return {
48
+ ...exxatInputMaskDefaults,
49
+ inputFormat: "HH:MM:ss",
50
+ outputFormat: "HH:MM:ss",
51
+ }
52
+ }
53
+
54
+ export function useExxatPhoneMask(options?: Options) {
55
+ return useMaskInput({
56
+ mask: exxatMaskPatterns.phoneUS,
57
+ options: { ...exxatInputMaskDefaults, ...options },
58
+ })
59
+ }
60
+
61
+ export function useExxatZipMask(options?: Options) {
62
+ return useMaskInput({
63
+ mask: exxatMaskPatterns.zipUS,
64
+ options: { ...exxatInputMaskDefaults, ...options },
65
+ })
66
+ }
67
+
68
+ export function useExxatDateMDYMask(options?: Options) {
69
+ return useMaskInput({
70
+ mask: exxatMaskPatterns.dateMDY,
71
+ options: { ...exxatInputMaskDefaults, ...options },
72
+ })
73
+ }
74
+
75
+ export function useExxatTimeMask(options?: Options) {
76
+ return useMaskInput({
77
+ mask: "datetime",
78
+ options: { ...exxatTimeMaskOptions(), ...options },
79
+ })
80
+ }
81
+
82
+ export function useExxatCustomMask(mask: Mask, options?: Options) {
83
+ return useMaskInput({
84
+ mask,
85
+ options: { ...exxatInputMaskDefaults, ...options },
86
+ })
87
+ }
88
+
89
+ /** Merge React Hook Form Controller `field.ref` with a mask ref. */
90
+ export function useExxatMaskedFieldRef(
91
+ fieldRef: React.Ref<HTMLInputElement>,
92
+ mask: Mask,
93
+ options?: Options
94
+ ): React.RefCallback<HTMLInputElement> {
95
+ const maskRef = useMaskInput({
96
+ mask,
97
+ options: { ...exxatInputMaskDefaults, ...options },
98
+ })
99
+ return React.useMemo(() => composeRefs(fieldRef, maskRef), [fieldRef, maskRef])
100
+ }
101
+
102
+ export type MaskedInputProps = React.ComponentProps<typeof Input> & {
103
+ mask: Mask
104
+ maskOptions?: Options
105
+ }
106
+
107
+ /** Standalone masked `Input` (forwardRef + merged mask ref). */
108
+ const MaskedInput = React.forwardRef<HTMLInputElement, MaskedInputProps>(function MaskedInput(
109
+ { mask, maskOptions, className, type = "text", ...props },
110
+ ref
111
+ ) {
112
+ const maskRef = useMaskInput({
113
+ mask,
114
+ options: { ...exxatInputMaskDefaults, ...maskOptions },
115
+ })
116
+ return (
117
+ <Input
118
+ ref={composeRefs(ref, maskRef)}
119
+ type={type}
120
+ className={cn(className)}
121
+ {...props}
122
+ />
123
+ )
124
+ })
125
+
126
+ export {
127
+ MaskedInput,
128
+ useMaskInput,
129
+ withMask,
130
+ useHookFormMask,
131
+ withHookFormMask,
132
+ useTanStackFormMask,
133
+ withTanStackFormMask,
134
+ }
135
+ export type { Mask, Options }
@@ -0,0 +1,22 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
+ function Input({ className, type, ...props }, ref) {
7
+ return (
8
+ <input
9
+ ref={ref}
10
+ type={type}
11
+ data-slot="input"
12
+ className={cn(
13
+ "h-8 w-full min-w-0 rounded-md border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/15 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ },
20
+ )
21
+
22
+ export { Input }
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ /**
8
+ * Kbd — display keyboard keys and shortcuts (shadcn/ui).
9
+ * @see https://ui.shadcn.com/docs/components/radix/kbd
10
+ *
11
+ * Variants:
12
+ * - "tile" (default) — filled tile with border; use in tooltips, menus, docs,
13
+ * or any surface where the Kbd sits on neutral background.
14
+ * - "bare" — no background, no border; inherits `currentColor` at 70% opacity.
15
+ * Use **inside buttons** (primary/secondary workflow actions) so the hint
16
+ * does not look like a pasted-on patch against the button fill.
17
+ */
18
+ function Kbd({
19
+ className,
20
+ variant = "tile",
21
+ "aria-hidden": ariaHidden,
22
+ ...props
23
+ }: React.ComponentProps<"kbd"> & { variant?: "tile" | "bare" }) {
24
+ // Bare variant lives inside buttons — the button already carries the
25
+ // accessible name, so the inline kbd is redundant noise for screen readers.
26
+ // Default to aria-hidden unless a consumer explicitly opts in.
27
+ const hidden = ariaHidden ?? (variant === "bare" ? true : undefined)
28
+ return (
29
+ <kbd
30
+ data-slot="kbd"
31
+ data-variant={variant}
32
+ aria-hidden={hidden}
33
+ className={cn(
34
+ "pointer-events-none inline-flex h-5 min-w-5 select-none items-center justify-center gap-1 px-1 font-sans text-xs font-medium",
35
+ variant === "tile" &&
36
+ "bg-muted text-muted-foreground rounded-sm border",
37
+ variant === "bare" && "text-current/70 px-0",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
46
+ return (
47
+ <div
48
+ data-slot="kbd-group"
49
+ className={cn("inline-flex items-center gap-1", className)}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+
55
+ export { Kbd, KbdGroup }
@@ -0,0 +1,25 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Label as LabelPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ const Label = React.forwardRef<
9
+ React.ElementRef<typeof LabelPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
11
+ >(function Label({ className, ...props }, ref) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ ref={ref}
15
+ data-slot="label"
16
+ className={cn(
17
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ })
24
+
25
+ export { Label }
@@ -0,0 +1,65 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Credit card inputs — matches Shadcn Studio input-mask 03–06 (`react-payment-inputs` + `Input`).
5
+ * One `usePaymentInputs()` instance drives number + expiry + CVC; use this group (or `usePaymentInputs` yourself in a parent).
6
+ */
7
+
8
+ import * as React from "react"
9
+ import { CreditCard } from "lucide-react"
10
+ import { usePaymentInputs } from "react-payment-inputs"
11
+ import images, { type CardImages } from "react-payment-inputs/images"
12
+
13
+ import { cn } from "../../lib/utils"
14
+ import { Input } from "./input"
15
+
16
+ export { usePaymentInputs } from "react-payment-inputs"
17
+ export type { CardImages }
18
+
19
+ /** Stacked number + expiry/CVC (Studio 06). Single hook — do not split across separate roots. */
20
+ export function PaymentCardFieldsGroup({ className }: { className?: string }) {
21
+ const id = React.useId()
22
+ const { meta, getCardNumberProps, getExpiryDateProps, getCVCProps, getCardImageProps } =
23
+ usePaymentInputs()
24
+
25
+ return (
26
+ <div className={cn("w-full max-w-xs space-y-0", className)}>
27
+ <div className="relative focus-within:z-1">
28
+ <Input
29
+ {...getCardNumberProps()}
30
+ id={`card-number-${id}`}
31
+ className="peer rounded-b-none pe-11 shadow-none"
32
+ />
33
+ <div className="text-muted-foreground pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 peer-disabled:opacity-50">
34
+ {meta.cardType ? (
35
+ <svg
36
+ className="w-6 overflow-hidden"
37
+ {...getCardImageProps({
38
+ images: images as unknown as CardImages,
39
+ })}
40
+ />
41
+ ) : (
42
+ <CreditCard className="size-4" aria-hidden />
43
+ )}
44
+ <span className="sr-only">Card network</span>
45
+ </div>
46
+ </div>
47
+ <div className="-mt-px flex">
48
+ <div className="min-w-0 flex-1 focus-within:z-1">
49
+ <Input
50
+ {...getExpiryDateProps()}
51
+ id={`card-expiry-${id}`}
52
+ className="rounded-t-none rounded-r-none shadow-none"
53
+ />
54
+ </div>
55
+ <div className="-ms-px min-w-0 flex-1 focus-within:z-1">
56
+ <Input
57
+ {...getCVCProps()}
58
+ id={`card-cvc-${id}`}
59
+ className="rounded-t-none rounded-l-none shadow-none"
60
+ />
61
+ </div>
62
+ </div>
63
+ </div>
64
+ )
65
+ }
@@ -0,0 +1,46 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Popover as PopoverPrimitive } from "radix-ui"
5
+ import { cn } from "../../lib/utils"
6
+
7
+ function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
8
+ return <PopoverPrimitive.Root {...props} />
9
+ }
10
+
11
+ function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
12
+ return <PopoverPrimitive.Trigger {...props} />
13
+ }
14
+
15
+ function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
16
+ return <PopoverPrimitive.Anchor {...props} />
17
+ }
18
+
19
+ function PopoverContent({
20
+ className,
21
+ align = "start",
22
+ sideOffset = 4,
23
+ ...props
24
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
25
+ return (
26
+ <PopoverPrimitive.Portal>
27
+ <PopoverPrimitive.Content
28
+ data-slot="popover-content"
29
+ align={align}
30
+ sideOffset={sideOffset}
31
+ className={cn(
32
+ "z-50 rounded-lg border border-border bg-popover shadow-md outline-none",
33
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
34
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
35
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
36
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
37
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ </PopoverPrimitive.Portal>
43
+ )
44
+ }
45
+
46
+ export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }