@suphark/ui 0.1.0

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 (147) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/components.json +21 -0
  5. package/package.json +108 -0
  6. package/scripts/audit-base-ui-props.mjs +179 -0
  7. package/scripts/audit-tailwind-typography.mjs +100 -0
  8. package/src/components/design-system/design-system-showcase.tsx +138 -0
  9. package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
  10. package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
  11. package/src/components/design-system/sections/showcase-forms.tsx +373 -0
  12. package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
  13. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
  14. package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
  15. package/src/components/design-system/sections/showcase-overview.tsx +368 -0
  16. package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
  17. package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
  18. package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
  19. package/src/components/shared/button/add-new-button.tsx +19 -0
  20. package/src/components/shared/button/base/action-button.tsx +134 -0
  21. package/src/components/shared/button/base/link-button.tsx +130 -0
  22. package/src/components/shared/button/cancel-button.tsx +27 -0
  23. package/src/components/shared/button/confirm-button.tsx +26 -0
  24. package/src/components/shared/button/delete-button.tsx +36 -0
  25. package/src/components/shared/button/floating-save-button.tsx +89 -0
  26. package/src/components/shared/button/index.ts +13 -0
  27. package/src/components/shared/button/print-button.tsx +67 -0
  28. package/src/components/shared/button/refresh-button.tsx +63 -0
  29. package/src/components/shared/button/save-button.tsx +29 -0
  30. package/src/components/shared/button/submit-button.tsx +58 -0
  31. package/src/components/shared/client-redirect.tsx +11 -0
  32. package/src/components/shared/combobox.tsx +116 -0
  33. package/src/components/shared/data-list.tsx +65 -0
  34. package/src/components/shared/date/date-range-picker.tsx +106 -0
  35. package/src/components/shared/description-list.tsx +27 -0
  36. package/src/components/shared/empty-row.tsx +13 -0
  37. package/src/components/shared/empty-state.tsx +31 -0
  38. package/src/components/shared/env-snippet.tsx +62 -0
  39. package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
  40. package/src/components/shared/form/form-field-boolean.tsx +93 -0
  41. package/src/components/shared/form/form-field-color-select.tsx +86 -0
  42. package/src/components/shared/form/form-field-combobox.tsx +167 -0
  43. package/src/components/shared/form/form-field-date.tsx +55 -0
  44. package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
  45. package/src/components/shared/form/form-field-input.tsx +372 -0
  46. package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
  47. package/src/components/shared/form/form-field-radio-group.tsx +56 -0
  48. package/src/components/shared/form/form-field-select.tsx +312 -0
  49. package/src/components/shared/form/simple-select.tsx +152 -0
  50. package/src/components/shared/form-dialog.tsx +109 -0
  51. package/src/components/shared/form-message.tsx +30 -0
  52. package/src/components/shared/initials.tsx +33 -0
  53. package/src/components/shared/layout/error-state.tsx +160 -0
  54. package/src/components/shared/layout/page-shell.tsx +57 -0
  55. package/src/components/shared/link/transition-link.tsx +72 -0
  56. package/src/components/shared/multi-select-combobox.tsx +392 -0
  57. package/src/components/shared/name-id-fields.tsx +133 -0
  58. package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
  59. package/src/components/shared/notice.tsx +35 -0
  60. package/src/components/shared/number/formatted-number.tsx +133 -0
  61. package/src/components/shared/page-header.tsx +25 -0
  62. package/src/components/shared/page-skeleton.tsx +18 -0
  63. package/src/components/shared/row-actions.tsx +93 -0
  64. package/src/components/shared/row-menu.tsx +55 -0
  65. package/src/components/shared/secret-reveal.tsx +53 -0
  66. package/src/components/shared/section-card.tsx +44 -0
  67. package/src/components/shared/stat.tsx +26 -0
  68. package/src/components/shared/status-badge.tsx +47 -0
  69. package/src/components/shared/text/primary-secondary-text.tsx +186 -0
  70. package/src/components/shared/theme/index.ts +4 -0
  71. package/src/components/shared/theme/theme-menu-items.tsx +43 -0
  72. package/src/components/shared/theme/theme-provider.tsx +39 -0
  73. package/src/components/shared/theme/theme-switch.tsx +36 -0
  74. package/src/components/shared/theme/theme-toggle.tsx +76 -0
  75. package/src/components/shared/time/client-date-formatter.tsx +188 -0
  76. package/src/components/shared/ui-text.tsx +47 -0
  77. package/src/components/ui/__tests__/combobox.test.tsx +40 -0
  78. package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
  79. package/src/components/ui/__tests__/slider.test.tsx +18 -0
  80. package/src/components/ui/__tests__/tabs.test.tsx +33 -0
  81. package/src/components/ui/accordion.tsx +69 -0
  82. package/src/components/ui/alert-dialog.tsx +161 -0
  83. package/src/components/ui/alert.tsx +68 -0
  84. package/src/components/ui/aspect-ratio.tsx +22 -0
  85. package/src/components/ui/attachment.tsx +194 -0
  86. package/src/components/ui/avatar.tsx +92 -0
  87. package/src/components/ui/badge.tsx +48 -0
  88. package/src/components/ui/breadcrumb.tsx +102 -0
  89. package/src/components/ui/button-group.tsx +77 -0
  90. package/src/components/ui/button.tsx +57 -0
  91. package/src/components/ui/calendar.tsx +184 -0
  92. package/src/components/ui/card.tsx +87 -0
  93. package/src/components/ui/carousel.tsx +230 -0
  94. package/src/components/ui/chart.tsx +338 -0
  95. package/src/components/ui/checkbox.tsx +27 -0
  96. package/src/components/ui/collapsible.tsx +17 -0
  97. package/src/components/ui/combobox.tsx +272 -0
  98. package/src/components/ui/command.tsx +180 -0
  99. package/src/components/ui/context-menu.tsx +244 -0
  100. package/src/components/ui/date-picker.tsx +308 -0
  101. package/src/components/ui/dialog.tsx +137 -0
  102. package/src/components/ui/drawer.tsx +208 -0
  103. package/src/components/ui/dropdown-menu.tsx +257 -0
  104. package/src/components/ui/empty.tsx +93 -0
  105. package/src/components/ui/field.tsx +223 -0
  106. package/src/components/ui/form.tsx +161 -0
  107. package/src/components/ui/hover-card.tsx +46 -0
  108. package/src/components/ui/input-group.tsx +145 -0
  109. package/src/components/ui/input-otp.tsx +85 -0
  110. package/src/components/ui/input.tsx +19 -0
  111. package/src/components/ui/item.tsx +187 -0
  112. package/src/components/ui/kbd.tsx +26 -0
  113. package/src/components/ui/label.tsx +19 -0
  114. package/src/components/ui/menubar.tsx +266 -0
  115. package/src/components/ui/navigation-menu.tsx +160 -0
  116. package/src/components/ui/pagination.tsx +117 -0
  117. package/src/components/ui/popover.tsx +76 -0
  118. package/src/components/ui/progress.tsx +66 -0
  119. package/src/components/ui/radio-group.tsx +38 -0
  120. package/src/components/ui/relative-date.tsx +40 -0
  121. package/src/components/ui/resizable.tsx +41 -0
  122. package/src/components/ui/scroll-area.tsx +67 -0
  123. package/src/components/ui/searchable-select.tsx +124 -0
  124. package/src/components/ui/select.tsx +189 -0
  125. package/src/components/ui/separator.tsx +21 -0
  126. package/src/components/ui/sheet.tsx +124 -0
  127. package/src/components/ui/sidebar.tsx +691 -0
  128. package/src/components/ui/skeleton.tsx +13 -0
  129. package/src/components/ui/slider.tsx +55 -0
  130. package/src/components/ui/sonner.tsx +45 -0
  131. package/src/components/ui/spinner.tsx +16 -0
  132. package/src/components/ui/switch.tsx +32 -0
  133. package/src/components/ui/table.tsx +95 -0
  134. package/src/components/ui/tabs.tsx +79 -0
  135. package/src/components/ui/textarea.tsx +17 -0
  136. package/src/components/ui/toggle-group.tsx +86 -0
  137. package/src/components/ui/toggle.tsx +44 -0
  138. package/src/components/ui/tooltip.tsx +54 -0
  139. package/src/hooks/use-copy-to-clipboard.ts +27 -0
  140. package/src/hooks/use-mobile.ts +21 -0
  141. package/src/index.ts +156 -0
  142. package/src/lib/formatters.ts +44 -0
  143. package/src/lib/permission.ts +75 -0
  144. package/src/lib/status-colors.ts +52 -0
  145. package/src/lib/utils.ts +6 -0
  146. package/src/next.ts +15 -0
  147. package/src/styles.css +214 -0
@@ -0,0 +1,257 @@
1
+ "use client";
2
+
3
+ import { Menu as MenuPrimitive } from "@base-ui/react/menu";
4
+ import { cn } from "@suphark/ui/lib/utils";
5
+ import { CheckIcon, ChevronRightIcon } from "lucide-react";
6
+ import type * as React from "react";
7
+
8
+ function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
9
+ return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
10
+ }
11
+
12
+ function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
13
+ return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
14
+ }
15
+
16
+ function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
17
+ return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
18
+ }
19
+
20
+ function DropdownMenuContent({
21
+ align = "start",
22
+ alignOffset = 0,
23
+ side = "bottom",
24
+ sideOffset = 4,
25
+ className,
26
+ ...props
27
+ }: MenuPrimitive.Popup.Props &
28
+ Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
29
+ return (
30
+ <MenuPrimitive.Portal>
31
+ <MenuPrimitive.Positioner
32
+ className="isolate z-50 outline-none"
33
+ align={align}
34
+ alignOffset={alignOffset}
35
+ side={side}
36
+ sideOffset={sideOffset}
37
+ >
38
+ <MenuPrimitive.Popup
39
+ data-slot="dropdown-menu-content"
40
+ className={cn(
41
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-y-auto overflow-x-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-md outline-none ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in data-closed:overflow-hidden",
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ </MenuPrimitive.Positioner>
47
+ </MenuPrimitive.Portal>
48
+ );
49
+ }
50
+
51
+ function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
52
+ return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
53
+ }
54
+
55
+ function DropdownMenuLabel({
56
+ className,
57
+ inset,
58
+ ...props
59
+ }: MenuPrimitive.GroupLabel.Props & {
60
+ inset?: boolean;
61
+ }) {
62
+ return (
63
+ <MenuPrimitive.GroupLabel
64
+ data-slot="dropdown-menu-label"
65
+ data-inset={inset}
66
+ className={cn(
67
+ "px-1.5 py-1 font-medium text-muted-foreground text-xs data-inset:pl-7",
68
+ className,
69
+ )}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ function DropdownMenuItem({
76
+ className,
77
+ inset,
78
+ variant = "default",
79
+ ...props
80
+ }: MenuPrimitive.Item.Props & {
81
+ inset?: boolean;
82
+ variant?: "default" | "destructive";
83
+ }) {
84
+ return (
85
+ <MenuPrimitive.Item
86
+ data-slot="dropdown-menu-item"
87
+ data-inset={inset}
88
+ data-variant={variant}
89
+ className={cn(
90
+ "group/dropdown-menu-item relative flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 data-[variant=destructive]:*:[svg]:text-destructive",
91
+ className,
92
+ )}
93
+ {...props}
94
+ />
95
+ );
96
+ }
97
+
98
+ function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
99
+ return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
100
+ }
101
+
102
+ function DropdownMenuSubTrigger({
103
+ className,
104
+ inset,
105
+ children,
106
+ ...props
107
+ }: MenuPrimitive.SubmenuTrigger.Props & {
108
+ inset?: boolean;
109
+ }) {
110
+ return (
111
+ <MenuPrimitive.SubmenuTrigger
112
+ data-slot="dropdown-menu-sub-trigger"
113
+ data-inset={inset}
114
+ className={cn(
115
+ "flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-popup-open:bg-accent data-inset:pl-7 data-open:text-accent-foreground data-popup-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
116
+ className,
117
+ )}
118
+ {...props}
119
+ >
120
+ {children}
121
+ <ChevronRightIcon className="ml-auto" />
122
+ </MenuPrimitive.SubmenuTrigger>
123
+ );
124
+ }
125
+
126
+ function DropdownMenuSubContent({
127
+ align = "start",
128
+ alignOffset = -3,
129
+ side = "right",
130
+ sideOffset = 0,
131
+ className,
132
+ ...props
133
+ }: React.ComponentProps<typeof DropdownMenuContent>) {
134
+ return (
135
+ <DropdownMenuContent
136
+ data-slot="dropdown-menu-sub-content"
137
+ className={cn(
138
+ "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:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 w-auto min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
139
+ className,
140
+ )}
141
+ align={align}
142
+ alignOffset={alignOffset}
143
+ side={side}
144
+ sideOffset={sideOffset}
145
+ {...props}
146
+ />
147
+ );
148
+ }
149
+
150
+ function DropdownMenuCheckboxItem({
151
+ className,
152
+ children,
153
+ checked,
154
+ inset,
155
+ ...props
156
+ }: MenuPrimitive.CheckboxItem.Props & {
157
+ inset?: boolean;
158
+ }) {
159
+ return (
160
+ <MenuPrimitive.CheckboxItem
161
+ data-slot="dropdown-menu-checkbox-item"
162
+ data-inset={inset}
163
+ className={cn(
164
+ "relative flex cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
165
+ className,
166
+ )}
167
+ checked={checked}
168
+ {...props}
169
+ >
170
+ <span
171
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
172
+ data-slot="dropdown-menu-checkbox-item-indicator"
173
+ >
174
+ <MenuPrimitive.CheckboxItemIndicator>
175
+ <CheckIcon />
176
+ </MenuPrimitive.CheckboxItemIndicator>
177
+ </span>
178
+ {children}
179
+ </MenuPrimitive.CheckboxItem>
180
+ );
181
+ }
182
+
183
+ function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
184
+ return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
185
+ }
186
+
187
+ function DropdownMenuRadioItem({
188
+ className,
189
+ children,
190
+ inset,
191
+ ...props
192
+ }: MenuPrimitive.RadioItem.Props & {
193
+ inset?: boolean;
194
+ }) {
195
+ return (
196
+ <MenuPrimitive.RadioItem
197
+ data-slot="dropdown-menu-radio-item"
198
+ data-inset={inset}
199
+ className={cn(
200
+ "relative flex cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
201
+ className,
202
+ )}
203
+ {...props}
204
+ >
205
+ <span
206
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
207
+ data-slot="dropdown-menu-radio-item-indicator"
208
+ >
209
+ <MenuPrimitive.RadioItemIndicator>
210
+ <CheckIcon />
211
+ </MenuPrimitive.RadioItemIndicator>
212
+ </span>
213
+ {children}
214
+ </MenuPrimitive.RadioItem>
215
+ );
216
+ }
217
+
218
+ function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
219
+ return (
220
+ <MenuPrimitive.Separator
221
+ data-slot="dropdown-menu-separator"
222
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
223
+ {...props}
224
+ />
225
+ );
226
+ }
227
+
228
+ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
229
+ return (
230
+ <span
231
+ data-slot="dropdown-menu-shortcut"
232
+ className={cn(
233
+ "ml-auto text-muted-foreground text-xs tracking-widest group-focus/dropdown-menu-item:text-accent-foreground",
234
+ className,
235
+ )}
236
+ {...props}
237
+ />
238
+ );
239
+ }
240
+
241
+ export {
242
+ DropdownMenu,
243
+ DropdownMenuCheckboxItem,
244
+ DropdownMenuContent,
245
+ DropdownMenuGroup,
246
+ DropdownMenuItem,
247
+ DropdownMenuLabel,
248
+ DropdownMenuPortal,
249
+ DropdownMenuRadioGroup,
250
+ DropdownMenuRadioItem,
251
+ DropdownMenuSeparator,
252
+ DropdownMenuShortcut,
253
+ DropdownMenuSub,
254
+ DropdownMenuSubContent,
255
+ DropdownMenuSubTrigger,
256
+ DropdownMenuTrigger,
257
+ };
@@ -0,0 +1,93 @@
1
+ import { cn } from "@suphark/ui/lib/utils";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ function Empty({ className, ...props }: React.ComponentProps<"div">) {
5
+ return (
6
+ <div
7
+ data-slot="empty"
8
+ className={cn(
9
+ "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 text-balance rounded-xl border-dashed p-6 text-center",
10
+ className,
11
+ )}
12
+ {...props}
13
+ />
14
+ );
15
+ }
16
+
17
+ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
18
+ return (
19
+ <div
20
+ data-slot="empty-header"
21
+ className={cn("flex max-w-sm flex-col items-center gap-2", className)}
22
+ {...props}
23
+ />
24
+ );
25
+ }
26
+
27
+ const emptyMediaVariants = cva(
28
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
29
+ {
30
+ variants: {
31
+ variant: {
32
+ default: "bg-transparent",
33
+ icon: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ variant: "default",
38
+ },
39
+ },
40
+ );
41
+
42
+ function EmptyMedia({
43
+ className,
44
+ variant = "default",
45
+ ...props
46
+ }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
47
+ return (
48
+ <div
49
+ data-slot="empty-icon"
50
+ data-variant={variant}
51
+ className={cn(emptyMediaVariants({ variant, className }))}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
58
+ return (
59
+ <div
60
+ data-slot="empty-title"
61
+ className={cn("font-medium text-sm tracking-tight", className)}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
68
+ return (
69
+ <div
70
+ data-slot="empty-description"
71
+ className={cn(
72
+ "text-muted-foreground text-sm/relaxed [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
73
+ className,
74
+ )}
75
+ {...props}
76
+ />
77
+ );
78
+ }
79
+
80
+ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
81
+ return (
82
+ <div
83
+ data-slot="empty-content"
84
+ className={cn(
85
+ "flex w-full min-w-0 max-w-sm flex-col items-center gap-2.5 text-balance text-sm",
86
+ className,
87
+ )}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ export { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle };
@@ -0,0 +1,223 @@
1
+ "use client";
2
+
3
+ import { Label } from "@suphark/ui/components/ui/label";
4
+ import { Separator } from "@suphark/ui/components/ui/separator";
5
+ import { cn } from "@suphark/ui/lib/utils";
6
+ import { cva, type VariantProps } from "class-variance-authority";
7
+ import { useMemo } from "react";
8
+
9
+ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
10
+ return (
11
+ <fieldset
12
+ data-slot="field-set"
13
+ className={cn(
14
+ "flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function FieldLegend({
23
+ className,
24
+ variant = "legend",
25
+ ...props
26
+ }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
27
+ return (
28
+ <legend
29
+ data-slot="field-legend"
30
+ data-variant={variant}
31
+ className={cn(
32
+ "mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
33
+ className,
34
+ )}
35
+ {...props}
36
+ />
37
+ );
38
+ }
39
+
40
+ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
41
+ return (
42
+ <div
43
+ data-slot="field-group"
44
+ className={cn(
45
+ "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",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ const fieldVariants = cva("group/field flex w-full gap-2 data-[invalid=true]:text-destructive", {
54
+ variants: {
55
+ orientation: {
56
+ vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
57
+ horizontal:
58
+ "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",
59
+ responsive:
60
+ "@md/field-group:flex-row flex-col @md/field-group:items-center *:w-full @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",
61
+ },
62
+ },
63
+ defaultVariants: {
64
+ orientation: "vertical",
65
+ },
66
+ });
67
+
68
+ function Field({
69
+ className,
70
+ orientation = "vertical",
71
+ ...props
72
+ }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
73
+ return (
74
+ <div
75
+ role="group"
76
+ data-slot="field"
77
+ data-orientation={orientation}
78
+ className={cn(fieldVariants({ orientation }), className)}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
85
+ return (
86
+ <div
87
+ data-slot="field-content"
88
+ className={cn("group/field-content flex flex-1 flex-col gap-0.5 leading-snug", className)}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
95
+ return (
96
+ <Label
97
+ data-slot="field-label"
98
+ className={cn(
99
+ "group/field-label peer/field-label flex w-fit gap-2 leading-snug has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:ring-3 has-[>[data-slot=field]]:has-[:focus-visible]:ring-ring/50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted/50 *:data-[slot=field]:p-2.5 group-data-[disabled=true]/field:opacity-50 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
100
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
101
+ className,
102
+ )}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
109
+ return (
110
+ <div
111
+ data-slot="field-label"
112
+ className={cn(
113
+ "flex w-fit items-center gap-2 font-medium text-sm group-data-[disabled=true]/field:opacity-50",
114
+ className,
115
+ )}
116
+ {...props}
117
+ />
118
+ );
119
+ }
120
+
121
+ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
122
+ return (
123
+ <p
124
+ data-slot="field-description"
125
+ className={cn(
126
+ "text-left font-normal text-muted-foreground text-sm leading-normal group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
127
+ "nth-last-2:-mt-1 last:mt-0",
128
+ "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
129
+ className,
130
+ )}
131
+ {...props}
132
+ />
133
+ );
134
+ }
135
+
136
+ function FieldSeparator({
137
+ children,
138
+ className,
139
+ ...props
140
+ }: React.ComponentProps<"div"> & {
141
+ children?: React.ReactNode;
142
+ }) {
143
+ return (
144
+ <div
145
+ data-slot="field-separator"
146
+ data-content={!!children}
147
+ className={cn(
148
+ "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
149
+ className,
150
+ )}
151
+ {...props}
152
+ >
153
+ <Separator className="absolute inset-0 top-1/2" />
154
+ {children && (
155
+ <span
156
+ className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
157
+ data-slot="field-separator-content"
158
+ >
159
+ {children}
160
+ </span>
161
+ )}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ function FieldError({
167
+ className,
168
+ children,
169
+ errors,
170
+ ...props
171
+ }: React.ComponentProps<"div"> & {
172
+ errors?: Array<{ message?: string } | undefined>;
173
+ }) {
174
+ const content = useMemo(() => {
175
+ if (children) {
176
+ return children;
177
+ }
178
+
179
+ if (!errors?.length) {
180
+ return null;
181
+ }
182
+
183
+ const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()];
184
+
185
+ if (uniqueErrors?.length === 1) {
186
+ return uniqueErrors[0]?.message;
187
+ }
188
+
189
+ return (
190
+ <ul className="ml-4 flex list-disc flex-col gap-1">
191
+ {uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
192
+ </ul>
193
+ );
194
+ }, [children, errors]);
195
+
196
+ if (!content) {
197
+ return null;
198
+ }
199
+
200
+ return (
201
+ <div
202
+ role="alert"
203
+ data-slot="field-error"
204
+ className={cn("font-normal text-destructive text-sm", className)}
205
+ {...props}
206
+ >
207
+ {content}
208
+ </div>
209
+ );
210
+ }
211
+
212
+ export {
213
+ Field,
214
+ FieldContent,
215
+ FieldDescription,
216
+ FieldError,
217
+ FieldGroup,
218
+ FieldLabel,
219
+ FieldLegend,
220
+ FieldSeparator,
221
+ FieldSet,
222
+ FieldTitle,
223
+ };