@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,422 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ function DropdownMenu({
9
+ ...props
10
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
11
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
12
+ }
13
+
14
+ function DropdownMenuPortal({
15
+ ...props
16
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
17
+ return (
18
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
19
+ )
20
+ }
21
+
22
+ function DropdownMenuTrigger({
23
+ ...props
24
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
25
+ return (
26
+ <DropdownMenuPrimitive.Trigger
27
+ data-slot="dropdown-menu-trigger"
28
+ suppressHydrationWarning
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ function DropdownMenuContent({
35
+ className,
36
+ align = "start",
37
+ sideOffset = 4,
38
+ ...props
39
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
40
+ return (
41
+ <DropdownMenuPrimitive.Portal>
42
+ <DropdownMenuPrimitive.Content
43
+ data-slot="dropdown-menu-content"
44
+ sideOffset={sideOffset}
45
+ align={align}
46
+ className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
47
+ {...props}
48
+ />
49
+ </DropdownMenuPrimitive.Portal>
50
+ )
51
+ }
52
+
53
+ function DropdownMenuGroup({
54
+ ...props
55
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
56
+ return (
57
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
58
+ )
59
+ }
60
+
61
+ function DropdownMenuItem({
62
+ className,
63
+ inset,
64
+ variant = "default",
65
+ shortcut,
66
+ children,
67
+ asChild,
68
+ ...props
69
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
70
+ inset?: boolean
71
+ variant?: "default" | "destructive"
72
+ /** Visual keyboard-shortcut hint shown on the right of the item (e.g. "⌘E", "F2").
73
+ * Purely cosmetic — to actually bind the key, render a sibling `<Shortcut keys={…} onInvoke={…} />`
74
+ * in a parent that stays mounted (menu items unmount when the menu closes). */
75
+ shortcut?: string
76
+ }) {
77
+ return (
78
+ <DropdownMenuPrimitive.Item
79
+ data-slot="dropdown-menu-item"
80
+ data-inset={inset}
81
+ data-variant={variant}
82
+ asChild={asChild}
83
+ className={cn(
84
+ "group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:ps-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
85
+ className
86
+ )}
87
+ {...props}
88
+ >
89
+ {asChild ? (
90
+ children
91
+ ) : (
92
+ <>
93
+ {children}
94
+ {shortcut ? <DropdownMenuShortcut>{shortcut}</DropdownMenuShortcut> : null}
95
+ </>
96
+ )}
97
+ </DropdownMenuPrimitive.Item>
98
+ )
99
+ }
100
+
101
+ /** Invisible component that binds a keyboard shortcut globally while mounted.
102
+ * Pair with `DropdownMenuItem shortcut="⌘E"` for the visual hint. */
103
+ function Shortcut({
104
+ keys,
105
+ onInvoke,
106
+ disabled,
107
+ }: {
108
+ keys: string
109
+ onInvoke: (e: KeyboardEvent) => void
110
+ disabled?: boolean
111
+ }) {
112
+ useShortcut(keys, onInvoke, disabled)
113
+ return null
114
+ }
115
+
116
+ function useShortcut(
117
+ keys: string,
118
+ onInvoke: (e: KeyboardEvent) => void,
119
+ disabled?: boolean,
120
+ ) {
121
+ const ref = React.useRef(onInvoke)
122
+ React.useEffect(() => { ref.current = onInvoke }, [onInvoke])
123
+ React.useEffect(() => {
124
+ if (disabled) return
125
+ const parsed = parseShortcut(keys)
126
+ if (!parsed) return
127
+ function handler(e: KeyboardEvent) {
128
+ if (!matchesShortcut(e, parsed!)) return
129
+ const t = e.target as HTMLElement | null
130
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return
131
+ // Skip when the user is activating an interactive control — Enter/Space on a
132
+ // focused button, link, or role=button is already that control's native action.
133
+ // Without this guard, a page-level `Shortcut keys="Enter"` (e.g. the wizard's
134
+ // "submit on review") races the button's own click: the Edit / Back / Cancel
135
+ // button runs its handler AND the window listener submits the form on the same
136
+ // keystroke — the classic "review auto-closes when I click Edit" bug.
137
+ if (t && (parsed!.key === "enter" || parsed!.key === " ")) {
138
+ const role = t.getAttribute("role")
139
+ if (
140
+ t.tagName === "BUTTON" ||
141
+ t.tagName === "A" ||
142
+ t.tagName === "SELECT" ||
143
+ role === "button" ||
144
+ role === "link" ||
145
+ role === "menuitem" ||
146
+ role === "tab" ||
147
+ role === "option" ||
148
+ role === "checkbox" ||
149
+ role === "radio" ||
150
+ role === "switch" ||
151
+ t.closest('[role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="option"]')
152
+ )
153
+ return
154
+ }
155
+ // If a Radix dialog/sheet/alert-dialog is open, only fire when the event
156
+ // originates inside it — page-level shortcuts must NOT bleed through
157
+ // an open dialog, but in-dialog `<Shortcut>` bindings (Export, Save)
158
+ // still need to fire.
159
+ const openDialog = document.querySelector('[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"]') as HTMLElement | null
160
+ if (openDialog && (!t || !openDialog.contains(t))) return
161
+ e.preventDefault()
162
+ e.stopPropagation()
163
+ ref.current(e)
164
+ }
165
+ window.addEventListener("keydown", handler)
166
+ return () => window.removeEventListener("keydown", handler)
167
+ }, [keys, disabled])
168
+ }
169
+
170
+ /* --------------------------------------------------------------------------
171
+ * Shortcut parsing + global binding
172
+ * ------------------------------------------------------------------------ */
173
+
174
+ type ParsedShortcut = {
175
+ meta: boolean
176
+ ctrl: boolean
177
+ shift: boolean
178
+ alt: boolean
179
+ key: string // normalized lowercase, or special like "f2", "backspace", "delete", "enter", "escape", "arrowup"…
180
+ }
181
+
182
+ function mapKey(raw: string): string {
183
+ const p = raw.toLowerCase()
184
+ if (raw === "⌫") return "backspace"
185
+ if (raw === "⌦") return "delete"
186
+ if (raw === "⏎" || p === "enter" || p === "return") return "enter"
187
+ if (raw === "␣" || p === "space") return " "
188
+ if (p === "esc" || p === "escape") return "escape"
189
+ if (p === "tab") return "tab"
190
+ if (p === "up" || raw === "↑") return "arrowup"
191
+ if (p === "down" || raw === "↓") return "arrowdown"
192
+ if (p === "left" || raw === "←") return "arrowleft"
193
+ if (p === "right" || raw === "→") return "arrowright"
194
+ return p
195
+ }
196
+
197
+ function parseShortcut(input: string): ParsedShortcut | null {
198
+ let s = input.trim()
199
+ if (!s) return null
200
+ const out: ParsedShortcut = { meta: false, ctrl: false, shift: false, alt: false, key: "" }
201
+ // Strip leading symbolic modifiers (⌘ ⌃ ⇧ ⌥) which may be glued to the key char.
202
+ while (s.length) {
203
+ const c = s[0]
204
+ if (c === "⌘") { out.meta = true; s = s.slice(1) }
205
+ else if (c === "⌃") { out.ctrl = true; s = s.slice(1) }
206
+ else if (c === "⇧") { out.shift = true; s = s.slice(1) }
207
+ else if (c === "⌥") { out.alt = true; s = s.slice(1) }
208
+ else break
209
+ }
210
+ if (!s) return null
211
+ // Word-style modifiers (Cmd+Shift+D, Alt P) joined by + or whitespace.
212
+ if (/[+\s]/.test(s)) {
213
+ for (const raw of s.split(/[+\s]+/).filter(Boolean)) {
214
+ const p = raw.toLowerCase()
215
+ if (raw === "⌘" || p === "cmd" || p === "meta" || p === "command") out.meta = true
216
+ else if (raw === "⌃" || p === "ctrl" || p === "control") out.ctrl = true
217
+ else if (raw === "⇧" || p === "shift") out.shift = true
218
+ else if (raw === "⌥" || p === "alt" || p === "opt" || p === "option") out.alt = true
219
+ else out.key = mapKey(raw)
220
+ }
221
+ } else {
222
+ out.key = mapKey(s)
223
+ }
224
+ return out.key ? out : null
225
+ }
226
+
227
+ function matchesShortcut(e: KeyboardEvent, s: ParsedShortcut): boolean {
228
+ if (e.metaKey !== s.meta) return false
229
+ if (e.ctrlKey !== s.ctrl) return false
230
+ if (e.altKey !== s.alt) return false
231
+ if (e.shiftKey !== s.shift) return false
232
+ return e.key.toLowerCase() === s.key
233
+ }
234
+
235
+
236
+ function DropdownMenuCheckboxItem({
237
+ className,
238
+ children,
239
+ checked,
240
+ inset,
241
+ ...props
242
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
243
+ inset?: boolean
244
+ }) {
245
+ return (
246
+ <DropdownMenuPrimitive.CheckboxItem
247
+ data-slot="dropdown-menu-checkbox-item"
248
+ data-inset={inset}
249
+ className={cn(
250
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4",
251
+ className
252
+ )}
253
+ checked={checked}
254
+ {...props}
255
+ >
256
+ <span
257
+ className="pointer-events-none absolute end-2 flex items-center justify-center"
258
+ data-slot="dropdown-menu-checkbox-item-indicator"
259
+ >
260
+ <DropdownMenuPrimitive.ItemIndicator>
261
+ <i className="fa-light fa-check" aria-hidden="true" />
262
+ </DropdownMenuPrimitive.ItemIndicator>
263
+ </span>
264
+ {children}
265
+ </DropdownMenuPrimitive.CheckboxItem>
266
+ )
267
+ }
268
+
269
+ function DropdownMenuRadioGroup({
270
+ ...props
271
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
272
+ return (
273
+ <DropdownMenuPrimitive.RadioGroup
274
+ data-slot="dropdown-menu-radio-group"
275
+ {...props}
276
+ />
277
+ )
278
+ }
279
+
280
+ function DropdownMenuRadioItem({
281
+ className,
282
+ children,
283
+ inset,
284
+ ...props
285
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
286
+ inset?: boolean
287
+ }) {
288
+ return (
289
+ <DropdownMenuPrimitive.RadioItem
290
+ data-slot="dropdown-menu-radio-item"
291
+ data-inset={inset}
292
+ className={cn(
293
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4",
294
+ className
295
+ )}
296
+ {...props}
297
+ >
298
+ <span
299
+ className="pointer-events-none absolute end-2 flex items-center justify-center"
300
+ data-slot="dropdown-menu-radio-item-indicator"
301
+ >
302
+ <DropdownMenuPrimitive.ItemIndicator>
303
+ <i className="fa-light fa-check" aria-hidden="true" />
304
+ </DropdownMenuPrimitive.ItemIndicator>
305
+ </span>
306
+ {children}
307
+ </DropdownMenuPrimitive.RadioItem>
308
+ )
309
+ }
310
+
311
+ function DropdownMenuLabel({
312
+ className,
313
+ inset,
314
+ ...props
315
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
316
+ inset?: boolean
317
+ }) {
318
+ return (
319
+ <DropdownMenuPrimitive.Label
320
+ data-slot="dropdown-menu-label"
321
+ data-inset={inset}
322
+ className={cn(
323
+ "px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:ps-7",
324
+ className
325
+ )}
326
+ {...props}
327
+ />
328
+ )
329
+ }
330
+
331
+ function DropdownMenuSeparator({
332
+ className,
333
+ ...props
334
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
335
+ return (
336
+ <DropdownMenuPrimitive.Separator
337
+ data-slot="dropdown-menu-separator"
338
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
339
+ {...props}
340
+ />
341
+ )
342
+ }
343
+
344
+ function DropdownMenuShortcut({
345
+ className,
346
+ ...props
347
+ }: React.ComponentProps<"span">) {
348
+ return (
349
+ <span
350
+ data-slot="dropdown-menu-shortcut"
351
+ className={cn(
352
+ "ms-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
353
+ className
354
+ )}
355
+ {...props}
356
+ />
357
+ )
358
+ }
359
+
360
+ function DropdownMenuSub({
361
+ ...props
362
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
363
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
364
+ }
365
+
366
+ function DropdownMenuSubTrigger({
367
+ className,
368
+ inset,
369
+ children,
370
+ ...props
371
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
372
+ inset?: boolean
373
+ }) {
374
+ return (
375
+ <DropdownMenuPrimitive.SubTrigger
376
+ data-slot="dropdown-menu-sub-trigger"
377
+ suppressHydrationWarning
378
+ data-inset={inset}
379
+ className={cn(
380
+ "flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:ps-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([data-product-logo]):not([class*='size-'])]:size-4",
381
+ className
382
+ )}
383
+ {...props}
384
+ >
385
+ {children}
386
+ <i className="fa-light fa-chevron-right rtl:rotate-180 ms-auto" aria-hidden="true" />
387
+ </DropdownMenuPrimitive.SubTrigger>
388
+ )
389
+ }
390
+
391
+ function DropdownMenuSubContent({
392
+ className,
393
+ ...props
394
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
395
+ return (
396
+ <DropdownMenuPrimitive.SubContent
397
+ data-slot="dropdown-menu-sub-content"
398
+ className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
399
+ {...props}
400
+ />
401
+ )
402
+ }
403
+
404
+ export {
405
+ DropdownMenu,
406
+ DropdownMenuPortal,
407
+ DropdownMenuTrigger,
408
+ DropdownMenuContent,
409
+ DropdownMenuGroup,
410
+ DropdownMenuLabel,
411
+ DropdownMenuItem,
412
+ DropdownMenuCheckboxItem,
413
+ DropdownMenuRadioGroup,
414
+ DropdownMenuRadioItem,
415
+ DropdownMenuSeparator,
416
+ DropdownMenuShortcut,
417
+ DropdownMenuSub,
418
+ DropdownMenuSubTrigger,
419
+ DropdownMenuSubContent,
420
+ Shortcut,
421
+ useShortcut,
422
+ }
@@ -0,0 +1,238 @@
1
+ "use client"
2
+
3
+ import { useMemo } from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { Label } from "./label"
8
+ import { Separator } from "./separator"
9
+
10
+ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
11
+ return (
12
+ <fieldset
13
+ data-slot="field-set"
14
+ className={cn(
15
+ "flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function FieldLegend({
24
+ className,
25
+ variant = "legend",
26
+ ...props
27
+ }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
28
+ return (
29
+ <legend
30
+ data-slot="field-legend"
31
+ data-variant={variant}
32
+ className={cn(
33
+ "mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="field-group"
45
+ className={cn(
46
+ "group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
47
+ className
48
+ )}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+
54
+ const fieldVariants = cva(
55
+ "group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
56
+ {
57
+ variants: {
58
+ orientation: {
59
+ vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
60
+ horizontal:
61
+ "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",
62
+ responsive:
63
+ "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",
64
+ },
65
+ },
66
+ defaultVariants: {
67
+ orientation: "vertical",
68
+ },
69
+ }
70
+ )
71
+
72
+ function Field({
73
+ className,
74
+ orientation = "vertical",
75
+ ...props
76
+ }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
77
+ return (
78
+ <div
79
+ role="group"
80
+ data-slot="field"
81
+ data-orientation={orientation}
82
+ className={cn(fieldVariants({ orientation }), className)}
83
+ {...props}
84
+ />
85
+ )
86
+ }
87
+
88
+ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
89
+ return (
90
+ <div
91
+ data-slot="field-content"
92
+ className={cn(
93
+ "group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
94
+ className
95
+ )}
96
+ {...props}
97
+ />
98
+ )
99
+ }
100
+
101
+ function FieldLabel({
102
+ className,
103
+ ...props
104
+ }: React.ComponentProps<typeof Label>) {
105
+ return (
106
+ <Label
107
+ data-slot="field-label"
108
+ className={cn(
109
+ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
110
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
111
+ className
112
+ )}
113
+ {...props}
114
+ />
115
+ )
116
+ }
117
+
118
+ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
119
+ return (
120
+ <div
121
+ data-slot="field-label"
122
+ className={cn(
123
+ "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
124
+ className
125
+ )}
126
+ {...props}
127
+ />
128
+ )
129
+ }
130
+
131
+ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
132
+ return (
133
+ <p
134
+ data-slot="field-description"
135
+ className={cn(
136
+ "text-start text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
137
+ "last:mt-0 nth-last-2:-mt-1",
138
+ "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
139
+ className
140
+ )}
141
+ {...props}
142
+ />
143
+ )
144
+ }
145
+
146
+ function FieldSeparator({
147
+ children,
148
+ className,
149
+ ...props
150
+ }: React.ComponentProps<"div"> & {
151
+ children?: React.ReactNode
152
+ }) {
153
+ return (
154
+ <div
155
+ data-slot="field-separator"
156
+ data-content={!!children}
157
+ className={cn(
158
+ "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
159
+ className
160
+ )}
161
+ {...props}
162
+ >
163
+ <Separator className="absolute inset-0 top-1/2" />
164
+ {children && (
165
+ <span
166
+ className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
167
+ data-slot="field-separator-content"
168
+ >
169
+ {children}
170
+ </span>
171
+ )}
172
+ </div>
173
+ )
174
+ }
175
+
176
+ function FieldError({
177
+ className,
178
+ children,
179
+ errors,
180
+ ...props
181
+ }: React.ComponentProps<"div"> & {
182
+ errors?: Array<{ message?: string } | undefined>
183
+ }) {
184
+ const content = useMemo(() => {
185
+ if (children) {
186
+ return children
187
+ }
188
+
189
+ if (!errors?.length) {
190
+ return null
191
+ }
192
+
193
+ const uniqueErrors = [
194
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
195
+ ]
196
+
197
+ if (uniqueErrors?.length == 1) {
198
+ return uniqueErrors[0]?.message
199
+ }
200
+
201
+ return (
202
+ <ul className="ms-4 flex list-disc flex-col gap-1">
203
+ {uniqueErrors.map(
204
+ (error, index) =>
205
+ error?.message && <li key={index}>{error.message}</li>
206
+ )}
207
+ </ul>
208
+ )
209
+ }, [children, errors])
210
+
211
+ if (!content) {
212
+ return null
213
+ }
214
+
215
+ return (
216
+ <div
217
+ role="alert"
218
+ data-slot="field-error"
219
+ className={cn("text-sm font-normal text-destructive", className)}
220
+ {...props}
221
+ >
222
+ {content}
223
+ </div>
224
+ )
225
+ }
226
+
227
+ export {
228
+ Field,
229
+ FieldLabel,
230
+ FieldDescription,
231
+ FieldError,
232
+ FieldGroup,
233
+ FieldLegend,
234
+ FieldSeparator,
235
+ FieldSet,
236
+ FieldContent,
237
+ FieldTitle,
238
+ }