@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.
- package/package.json +72 -0
- package/src/components/ui/avatar.tsx +384 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/banner.tsx +364 -0
- package/src/components/ui/breadcrumb.tsx +120 -0
- package/src/components/ui/button.tsx +66 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +136 -0
- package/src/components/ui/chart.tsx +378 -0
- package/src/components/ui/checkbox.tsx +160 -0
- package/src/components/ui/coach-mark.tsx +361 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +232 -0
- package/src/components/ui/date-picker-field.tsx +186 -0
- package/src/components/ui/dialog.tsx +171 -0
- package/src/components/ui/drag-handle-grip.tsx +10 -0
- package/src/components/ui/drawer.tsx +134 -0
- package/src/components/ui/dropdown-menu.tsx +422 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/form.tsx +137 -0
- package/src/components/ui/input-group.tsx +156 -0
- package/src/components/ui/input-mask.tsx +135 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +55 -0
- package/src/components/ui/label.tsx +25 -0
- package/src/components/ui/payment-card-fields.tsx +65 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/radio-group.tsx +217 -0
- package/src/components/ui/select.tsx +191 -0
- package/src/components/ui/selection-tile-grid.tsx +246 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +147 -0
- package/src/components/ui/sidebar.tsx +716 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +39 -0
- package/src/components/ui/status-badge.tsx +109 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tip.tsx +21 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle-switch.tsx +31 -0
- package/src/components/ui/toggle.tsx +48 -0
- package/src/components/ui/tooltip.tsx +59 -0
- package/src/components/ui/view-segmented-control.tsx +160 -0
- package/src/globals.css +1795 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-app-theme.ts +172 -0
- package/src/hooks/use-coach-mark.ts +342 -0
- package/src/hooks/use-mobile.ts +31 -0
- package/src/hooks/use-mod-key-label.ts +29 -0
- package/src/index.ts +55 -0
- package/src/lib/compose-refs.ts +15 -0
- package/src/lib/date-filter.ts +67 -0
- package/src/lib/utils.ts +6 -0
- package/src/theme/apply-windows-contrast-theme.ts +29 -0
- package/src/theme/windows-contrast-theme.json +147 -0
- package/src/theme.css +1130 -0
- 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 }
|