@theruntime/components 0.0.0 → 0.0.1

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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +113 -4
  3. package/src/accordion.tsx +62 -0
  4. package/src/alert-dialog.tsx +177 -0
  5. package/src/alert.tsx +60 -0
  6. package/src/aspect-ratio.tsx +7 -0
  7. package/src/attachment.tsx +192 -0
  8. package/src/avatar.tsx +94 -0
  9. package/src/badge.tsx +46 -0
  10. package/src/breadcrumb.tsx +102 -0
  11. package/src/bubble.tsx +125 -0
  12. package/src/button-group.tsx +82 -0
  13. package/src/button.tsx +62 -0
  14. package/src/calendar.tsx +178 -0
  15. package/src/card.tsx +75 -0
  16. package/src/carousel.tsx +228 -0
  17. package/src/chart/container.tsx +72 -0
  18. package/src/chart/context.tsx +39 -0
  19. package/src/chart/index.ts +4 -0
  20. package/src/chart/legend.tsx +63 -0
  21. package/src/chart/payload.ts +31 -0
  22. package/src/chart/tooltip.tsx +149 -0
  23. package/src/checkbox.tsx +27 -0
  24. package/src/cn.ts +36 -0
  25. package/src/collapsible.tsx +19 -0
  26. package/src/combobox/chips.tsx +71 -0
  27. package/src/combobox/content.tsx +123 -0
  28. package/src/combobox/index.ts +13 -0
  29. package/src/combobox/input.tsx +39 -0
  30. package/src/combobox/trigger.tsx +44 -0
  31. package/src/command.tsx +153 -0
  32. package/src/context-menu.tsx +222 -0
  33. package/src/dialog.tsx +142 -0
  34. package/src/direction.tsx +18 -0
  35. package/src/drawer.tsx +122 -0
  36. package/src/dropdown-menu.tsx +226 -0
  37. package/src/empty.tsx +94 -0
  38. package/src/field.tsx +232 -0
  39. package/src/form.tsx +153 -0
  40. package/src/hover-card.tsx +36 -0
  41. package/src/index.ts +90 -0
  42. package/src/input-group.tsx +156 -0
  43. package/src/input-otp.tsx +68 -0
  44. package/src/input.tsx +21 -0
  45. package/src/item.tsx +172 -0
  46. package/src/kbd.tsx +28 -0
  47. package/src/label.tsx +19 -0
  48. package/src/marker.tsx +66 -0
  49. package/src/menubar.tsx +250 -0
  50. package/src/message-scroller.tsx +128 -0
  51. package/src/message.tsx +85 -0
  52. package/src/native-select.tsx +56 -0
  53. package/src/navigation-menu.tsx +161 -0
  54. package/src/pagination.tsx +106 -0
  55. package/src/popover.tsx +72 -0
  56. package/src/progress.tsx +26 -0
  57. package/src/questionnaire/actions.tsx +118 -0
  58. package/src/questionnaire/choices.tsx +113 -0
  59. package/src/questionnaire/index.ts +21 -0
  60. package/src/questionnaire/root.tsx +75 -0
  61. package/src/radio-group.tsx +43 -0
  62. package/src/resizable.tsx +45 -0
  63. package/src/scroll-area.tsx +54 -0
  64. package/src/select.tsx +173 -0
  65. package/src/separator.tsx +26 -0
  66. package/src/sheet.tsx +132 -0
  67. package/src/shell/app-tab.tsx +67 -0
  68. package/src/shell/entry-row.tsx +137 -0
  69. package/src/shell/field.tsx +83 -0
  70. package/src/shell/group-head.tsx +19 -0
  71. package/src/shell/scope-chip.tsx +32 -0
  72. package/src/shell/suggest-panel.tsx +92 -0
  73. package/src/sidebar/context.tsx +128 -0
  74. package/src/sidebar/index.ts +23 -0
  75. package/src/sidebar/menu.tsx +239 -0
  76. package/src/sidebar/sections.tsx +118 -0
  77. package/src/sidebar/sidebar.tsx +183 -0
  78. package/src/skeleton.tsx +13 -0
  79. package/src/slider.tsx +56 -0
  80. package/src/sonner.tsx +41 -0
  81. package/src/spinner.tsx +16 -0
  82. package/src/styles.css +149 -0
  83. package/src/switch.tsx +33 -0
  84. package/src/table.tsx +90 -0
  85. package/src/tabs.tsx +79 -0
  86. package/src/textarea.tsx +18 -0
  87. package/src/toggle-group.tsx +81 -0
  88. package/src/toggle.tsx +44 -0
  89. package/src/tooltip.tsx +51 -0
  90. package/src/use-mobile.ts +21 -0
  91. package/README.md +0 -3
@@ -0,0 +1,226 @@
1
+ import * as React from "react";
2
+ import { CheckIcon, CaretRightIcon, CircleIcon } from "@phosphor-icons/react";
3
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
4
+
5
+ import { cn } from "./cn";
6
+
7
+ function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
8
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
9
+ }
10
+
11
+ function DropdownMenuPortal({
12
+ ...props
13
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
14
+ return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
15
+ }
16
+
17
+ function DropdownMenuTrigger({
18
+ ...props
19
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
20
+ return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
21
+ }
22
+
23
+ function DropdownMenuContent({
24
+ className,
25
+ sideOffset = 4,
26
+ ...props
27
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
28
+ return (
29
+ <DropdownMenuPrimitive.Portal>
30
+ <DropdownMenuPrimitive.Content
31
+ data-slot="dropdown-menu-content"
32
+ sideOffset={sideOffset}
33
+ className={cn(
34
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
35
+ className,
36
+ )}
37
+ {...props}
38
+ />
39
+ </DropdownMenuPrimitive.Portal>
40
+ );
41
+ }
42
+
43
+ function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
44
+ return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
45
+ }
46
+
47
+ function DropdownMenuItem({
48
+ className,
49
+ inset,
50
+ variant = "default",
51
+ ...props
52
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
53
+ inset?: boolean;
54
+ variant?: "default" | "destructive";
55
+ }) {
56
+ return (
57
+ <DropdownMenuPrimitive.Item
58
+ data-slot="dropdown-menu-item"
59
+ data-inset={inset}
60
+ data-variant={variant}
61
+ className={cn(
62
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
63
+ className,
64
+ )}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function DropdownMenuCheckboxItem({
71
+ className,
72
+ children,
73
+ checked,
74
+ ...props
75
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
76
+ return (
77
+ <DropdownMenuPrimitive.CheckboxItem
78
+ data-slot="dropdown-menu-checkbox-item"
79
+ className={cn(
80
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
81
+ className,
82
+ )}
83
+ checked={checked}
84
+ {...props}
85
+ >
86
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
87
+ <DropdownMenuPrimitive.ItemIndicator>
88
+ <CheckIcon className="size-4" />
89
+ </DropdownMenuPrimitive.ItemIndicator>
90
+ </span>
91
+ {children}
92
+ </DropdownMenuPrimitive.CheckboxItem>
93
+ );
94
+ }
95
+
96
+ function DropdownMenuRadioGroup({
97
+ ...props
98
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
99
+ return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
100
+ }
101
+
102
+ function DropdownMenuRadioItem({
103
+ className,
104
+ children,
105
+ ...props
106
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
107
+ return (
108
+ <DropdownMenuPrimitive.RadioItem
109
+ data-slot="dropdown-menu-radio-item"
110
+ className={cn(
111
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
112
+ className,
113
+ )}
114
+ {...props}
115
+ >
116
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
117
+ <DropdownMenuPrimitive.ItemIndicator>
118
+ <CircleIcon className="size-2 fill-current" />
119
+ </DropdownMenuPrimitive.ItemIndicator>
120
+ </span>
121
+ {children}
122
+ </DropdownMenuPrimitive.RadioItem>
123
+ );
124
+ }
125
+
126
+ function DropdownMenuLabel({
127
+ className,
128
+ inset,
129
+ ...props
130
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
131
+ inset?: boolean;
132
+ }) {
133
+ return (
134
+ <DropdownMenuPrimitive.Label
135
+ data-slot="dropdown-menu-label"
136
+ data-inset={inset}
137
+ className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
138
+ {...props}
139
+ />
140
+ );
141
+ }
142
+
143
+ function DropdownMenuSeparator({
144
+ className,
145
+ ...props
146
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
147
+ return (
148
+ <DropdownMenuPrimitive.Separator
149
+ data-slot="dropdown-menu-separator"
150
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
151
+ {...props}
152
+ />
153
+ );
154
+ }
155
+
156
+ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
157
+ return (
158
+ <span
159
+ data-slot="dropdown-menu-shortcut"
160
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
161
+ {...props}
162
+ />
163
+ );
164
+ }
165
+
166
+ function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
167
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
168
+ }
169
+
170
+ function DropdownMenuSubTrigger({
171
+ className,
172
+ inset,
173
+ children,
174
+ ...props
175
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
176
+ inset?: boolean;
177
+ }) {
178
+ return (
179
+ <DropdownMenuPrimitive.SubTrigger
180
+ data-slot="dropdown-menu-sub-trigger"
181
+ data-inset={inset}
182
+ className={cn(
183
+ "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
184
+ className,
185
+ )}
186
+ {...props}
187
+ >
188
+ {children}
189
+ <CaretRightIcon className="ml-auto size-4" />
190
+ </DropdownMenuPrimitive.SubTrigger>
191
+ );
192
+ }
193
+
194
+ function DropdownMenuSubContent({
195
+ className,
196
+ ...props
197
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
198
+ return (
199
+ <DropdownMenuPrimitive.SubContent
200
+ data-slot="dropdown-menu-sub-content"
201
+ className={cn(
202
+ "z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
203
+ className,
204
+ )}
205
+ {...props}
206
+ />
207
+ );
208
+ }
209
+
210
+ export {
211
+ DropdownMenu,
212
+ DropdownMenuPortal,
213
+ DropdownMenuTrigger,
214
+ DropdownMenuContent,
215
+ DropdownMenuGroup,
216
+ DropdownMenuLabel,
217
+ DropdownMenuItem,
218
+ DropdownMenuCheckboxItem,
219
+ DropdownMenuRadioGroup,
220
+ DropdownMenuRadioItem,
221
+ DropdownMenuSeparator,
222
+ DropdownMenuShortcut,
223
+ DropdownMenuSub,
224
+ DropdownMenuSubTrigger,
225
+ DropdownMenuSubContent,
226
+ };
package/src/empty.tsx ADDED
@@ -0,0 +1,94 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ import { cn } from "./cn";
4
+
5
+ function Empty({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="empty"
9
+ className={cn(
10
+ "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="empty-header"
22
+ className={cn("flex max-w-sm flex-col items-center gap-2 text-center", className)}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ const emptyMediaVariants = cva(
29
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
30
+ {
31
+ variants: {
32
+ variant: {
33
+ default: "bg-transparent",
34
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ },
40
+ },
41
+ );
42
+
43
+ function EmptyMedia({
44
+ className,
45
+ variant = "default",
46
+ ...props
47
+ }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
48
+ return (
49
+ <div
50
+ data-slot="empty-icon"
51
+ data-variant={variant}
52
+ className={cn(emptyMediaVariants({ variant, className }))}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
59
+ return (
60
+ <div
61
+ data-slot="empty-title"
62
+ className={cn("text-lg font-medium tracking-tight", className)}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+
68
+ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
69
+ return (
70
+ <div
71
+ data-slot="empty-description"
72
+ className={cn(
73
+ "text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
74
+ className,
75
+ )}
76
+ {...props}
77
+ />
78
+ );
79
+ }
80
+
81
+ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
82
+ return (
83
+ <div
84
+ data-slot="empty-content"
85
+ className={cn(
86
+ "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia };
package/src/field.tsx ADDED
@@ -0,0 +1,232 @@
1
+ import { useMemo } from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "./cn";
5
+ import { Label } from "./label";
6
+ import { Separator } from "./separator";
7
+
8
+ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
9
+ return (
10
+ <fieldset
11
+ data-slot="field-set"
12
+ className={cn(
13
+ "flex flex-col gap-6",
14
+ "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-3 font-medium",
33
+ "data-[variant=legend]:text-base",
34
+ "data-[variant=label]:text-sm",
35
+ className,
36
+ )}
37
+ {...props}
38
+ />
39
+ );
40
+ }
41
+
42
+ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
43
+ return (
44
+ <div
45
+ data-slot="field-group"
46
+ className={cn(
47
+ "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ const fieldVariants = cva("group/field flex w-full gap-3 data-[invalid=true]:text-destructive", {
56
+ variants: {
57
+ orientation: {
58
+ vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
59
+ horizontal: [
60
+ "flex-row items-center",
61
+ "[&>[data-slot=field-label]]:flex-auto",
62
+ "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
63
+ ],
64
+ responsive: [
65
+ "flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
66
+ "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
67
+ "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
68
+ ],
69
+ },
70
+ },
71
+ defaultVariants: {
72
+ orientation: "vertical",
73
+ },
74
+ });
75
+
76
+ function Field({
77
+ className,
78
+ orientation = "vertical",
79
+ ...props
80
+ }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
81
+ return (
82
+ <div
83
+ role="group"
84
+ data-slot="field"
85
+ data-orientation={orientation}
86
+ className={cn(fieldVariants({ orientation }), className)}
87
+ {...props}
88
+ />
89
+ );
90
+ }
91
+
92
+ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
93
+ return (
94
+ <div
95
+ data-slot="field-content"
96
+ className={cn("group/field-content flex flex-1 flex-col gap-1.5 leading-snug", className)}
97
+ {...props}
98
+ />
99
+ );
100
+ }
101
+
102
+ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
103
+ return (
104
+ <Label
105
+ data-slot="field-label"
106
+ className={cn(
107
+ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
108
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
109
+ "has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
110
+ className,
111
+ )}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
118
+ return (
119
+ <div
120
+ data-slot="field-label"
121
+ className={cn(
122
+ "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
123
+ className,
124
+ )}
125
+ {...props}
126
+ />
127
+ );
128
+ }
129
+
130
+ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
131
+ return (
132
+ <p
133
+ data-slot="field-description"
134
+ className={cn(
135
+ "text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
136
+ "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
137
+ "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
138
+ className,
139
+ )}
140
+ {...props}
141
+ />
142
+ );
143
+ }
144
+
145
+ function FieldSeparator({
146
+ children,
147
+ className,
148
+ ...props
149
+ }: React.ComponentProps<"div"> & {
150
+ children?: React.ReactNode;
151
+ }) {
152
+ return (
153
+ <div
154
+ data-slot="field-separator"
155
+ data-content={!!children}
156
+ className={cn(
157
+ "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
158
+ className,
159
+ )}
160
+ {...props}
161
+ >
162
+ <Separator className="absolute inset-0 top-1/2" />
163
+ {children && (
164
+ <span
165
+ className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
166
+ data-slot="field-separator-content"
167
+ >
168
+ {children}
169
+ </span>
170
+ )}
171
+ </div>
172
+ );
173
+ }
174
+
175
+ function FieldError({
176
+ className,
177
+ children,
178
+ errors,
179
+ ...props
180
+ }: React.ComponentProps<"div"> & {
181
+ errors?: Array<{ message?: string } | undefined>;
182
+ }) {
183
+ const content = useMemo(() => {
184
+ if (children) {
185
+ return children;
186
+ }
187
+
188
+ if (!errors?.length) {
189
+ return null;
190
+ }
191
+
192
+ const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()];
193
+
194
+ if (uniqueErrors?.length == 1) {
195
+ return uniqueErrors[0]?.message;
196
+ }
197
+
198
+ return (
199
+ <ul className="ml-4 flex list-disc flex-col gap-1">
200
+ {uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
201
+ </ul>
202
+ );
203
+ }, [children, errors]);
204
+
205
+ if (!content) {
206
+ return null;
207
+ }
208
+
209
+ return (
210
+ <div
211
+ role="alert"
212
+ data-slot="field-error"
213
+ className={cn("text-sm font-normal text-destructive", className)}
214
+ {...props}
215
+ >
216
+ {content}
217
+ </div>
218
+ );
219
+ }
220
+
221
+ export {
222
+ Field,
223
+ FieldLabel,
224
+ FieldDescription,
225
+ FieldError,
226
+ FieldGroup,
227
+ FieldLegend,
228
+ FieldSeparator,
229
+ FieldSet,
230
+ FieldContent,
231
+ FieldTitle,
232
+ };
package/src/form.tsx ADDED
@@ -0,0 +1,153 @@
1
+ import * as React from "react";
2
+ import type { Label as LabelPrimitive } from "radix-ui";
3
+ import { Slot } from "radix-ui";
4
+ import {
5
+ Controller,
6
+ FormProvider,
7
+ useFormContext,
8
+ useFormState,
9
+ type ControllerProps,
10
+ type FieldPath,
11
+ type FieldValues,
12
+ } from "react-hook-form";
13
+
14
+ import { cn } from "./cn";
15
+ import { Label } from "./label";
16
+
17
+ const Form = FormProvider;
18
+
19
+ type 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 | null>(null);
27
+
28
+ const FormField = <
29
+ TFieldValues extends FieldValues = FieldValues,
30
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
31
+ >({
32
+ ...props
33
+ }: ControllerProps<TFieldValues, TName>) => {
34
+ return (
35
+ <FormFieldContext.Provider value={{ name: props.name }}>
36
+ <Controller {...props} />
37
+ </FormFieldContext.Provider>
38
+ );
39
+ };
40
+
41
+ const useFormField = () => {
42
+ const fieldContext = React.useContext(FormFieldContext);
43
+ const itemContext = React.useContext(FormItemContext);
44
+
45
+ if (!fieldContext) {
46
+ throw new Error("useFormField should be used within <FormField>");
47
+ }
48
+ if (!itemContext) {
49
+ throw new Error("useFormField should be used within <FormItem>");
50
+ }
51
+
52
+ const { getFieldState } = useFormContext();
53
+ const formState = useFormState({ name: fieldContext.name });
54
+ const fieldState = getFieldState(fieldContext.name, formState);
55
+ const { id } = itemContext;
56
+
57
+ return {
58
+ id,
59
+ name: fieldContext.name,
60
+ formItemId: `${id}-form-item`,
61
+ formDescriptionId: `${id}-form-item-description`,
62
+ formMessageId: `${id}-form-item-message`,
63
+ ...fieldState,
64
+ };
65
+ };
66
+
67
+ type FormItemContextValue = {
68
+ id: string;
69
+ };
70
+
71
+ const FormItemContext = React.createContext<FormItemContextValue | null>(null);
72
+
73
+ function FormItem({ className, ...props }: React.ComponentProps<"div">) {
74
+ const id = React.useId();
75
+
76
+ return (
77
+ <FormItemContext.Provider value={{ id }}>
78
+ <div data-slot="form-item" className={cn("grid gap-2", className)} {...props} />
79
+ </FormItemContext.Provider>
80
+ );
81
+ }
82
+
83
+ function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
84
+ const { error, formItemId } = useFormField();
85
+
86
+ return (
87
+ <Label
88
+ data-slot="form-label"
89
+ data-error={!!error}
90
+ className={cn("data-[error=true]:text-destructive", className)}
91
+ htmlFor={formItemId}
92
+ {...props}
93
+ />
94
+ );
95
+ }
96
+
97
+ function FormControl({ ...props }: React.ComponentProps<typeof Slot.Root>) {
98
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
99
+
100
+ return (
101
+ <Slot.Root
102
+ data-slot="form-control"
103
+ id={formItemId}
104
+ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
105
+ aria-invalid={!!error}
106
+ {...props}
107
+ />
108
+ );
109
+ }
110
+
111
+ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
112
+ const { formDescriptionId } = useFormField();
113
+
114
+ return (
115
+ <p
116
+ data-slot="form-description"
117
+ id={formDescriptionId}
118
+ className={cn("text-sm text-muted-foreground", className)}
119
+ {...props}
120
+ />
121
+ );
122
+ }
123
+
124
+ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
125
+ const { error, formMessageId } = useFormField();
126
+ const body = error ? String(error?.message ?? "") : props.children;
127
+
128
+ if (!body) {
129
+ return null;
130
+ }
131
+
132
+ return (
133
+ <p
134
+ data-slot="form-message"
135
+ id={formMessageId}
136
+ className={cn("text-sm text-destructive", className)}
137
+ {...props}
138
+ >
139
+ {body}
140
+ </p>
141
+ );
142
+ }
143
+
144
+ export {
145
+ useFormField,
146
+ Form,
147
+ FormItem,
148
+ FormLabel,
149
+ FormControl,
150
+ FormDescription,
151
+ FormMessage,
152
+ FormField,
153
+ };