@shipfox/react-ui 0.7.0 → 0.8.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.
- package/.turbo/turbo-build.log +5 -5
- package/.turbo/turbo-check.log +2 -2
- package/.turbo/turbo-type.log +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/components/button/button.d.ts +2 -1
- package/dist/components/button/button.d.ts.map +1 -1
- package/dist/components/button/button.js +17 -2
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/button.stories.js +25 -0
- package/dist/components/button/button.stories.js.map +1 -1
- package/dist/components/button/icon-button.d.ts +2 -1
- package/dist/components/button/icon-button.d.ts.map +1 -1
- package/dist/components/button/icon-button.js +17 -2
- package/dist/components/button/icon-button.js.map +1 -1
- package/dist/components/button/icon-button.stories.js +90 -0
- package/dist/components/button/icon-button.stories.js.map +1 -1
- package/dist/components/form/form.d.ts +11 -0
- package/dist/components/form/form.d.ts.map +1 -0
- package/dist/components/form/form.js +106 -0
- package/dist/components/form/form.js.map +1 -0
- package/dist/components/form/form.stories.js +582 -0
- package/dist/components/form/form.stories.js.map +1 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -0
- package/dist/components/form/index.js +3 -0
- package/dist/components/form/index.js.map +1 -0
- package/dist/components/icon/custom/spinner.d.ts +1 -1
- package/dist/components/icon/custom/spinner.d.ts.map +1 -1
- package/dist/components/icon/custom/spinner.js +84 -30
- package/dist/components/icon/custom/spinner.js.map +1 -1
- package/dist/components/icon/icon.d.ts +19 -18
- package/dist/components/icon/icon.d.ts.map +1 -1
- package/dist/components/icon/icon.js +17 -17
- package/dist/components/icon/icon.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -1
- package/src/components/button/button.stories.tsx +18 -0
- package/src/components/button/button.tsx +27 -2
- package/src/components/button/icon-button.stories.tsx +46 -0
- package/src/components/button/icon-button.tsx +26 -1
- package/src/components/form/form.stories.tsx +500 -0
- package/src/components/form/form.tsx +154 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/icon/custom/spinner.tsx +64 -18
- package/src/components/icon/icon.tsx +18 -18
- package/src/components/index.ts +1 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import {zodResolver} from '@hookform/resolvers/zod';
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react';
|
|
3
|
+
import {Button} from 'components/button';
|
|
4
|
+
import {Checkbox, CheckboxLabel, CheckboxLinks} from 'components/checkbox';
|
|
5
|
+
import {Input} from 'components/input';
|
|
6
|
+
import {Textarea} from 'components/textarea';
|
|
7
|
+
import {Code, Header} from 'components/typography';
|
|
8
|
+
import {useForm} from 'react-hook-form';
|
|
9
|
+
import {z} from 'zod';
|
|
10
|
+
import {
|
|
11
|
+
Form,
|
|
12
|
+
FormControl,
|
|
13
|
+
FormDescription,
|
|
14
|
+
FormField,
|
|
15
|
+
FormItem,
|
|
16
|
+
FormLabel,
|
|
17
|
+
FormMessage,
|
|
18
|
+
} from './form';
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
21
|
+
title: 'Components/Form',
|
|
22
|
+
tags: ['autodocs'],
|
|
23
|
+
} satisfies Meta;
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
28
|
+
|
|
29
|
+
const basicFormSchema = z.object({
|
|
30
|
+
username: z.string().min(3, 'Username must be at least 3 characters'),
|
|
31
|
+
email: z.string().email('Invalid email address'),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
type BasicFormValues = z.infer<typeof basicFormSchema>;
|
|
35
|
+
|
|
36
|
+
function BasicFormExample() {
|
|
37
|
+
const form = useForm<BasicFormValues>({
|
|
38
|
+
resolver: zodResolver(basicFormSchema),
|
|
39
|
+
defaultValues: {
|
|
40
|
+
username: '',
|
|
41
|
+
email: '',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function onSubmit(data: BasicFormValues) {
|
|
46
|
+
// biome-ignore lint/suspicious/noConsole: <we need to log the data for the story>
|
|
47
|
+
console.log(data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Form {...form}>
|
|
52
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full max-w-md">
|
|
53
|
+
<FormField
|
|
54
|
+
control={form.control}
|
|
55
|
+
name="username"
|
|
56
|
+
render={({field}) => (
|
|
57
|
+
<FormItem>
|
|
58
|
+
<FormLabel>Username</FormLabel>
|
|
59
|
+
<FormControl>
|
|
60
|
+
<Input placeholder="shadcn" {...field} />
|
|
61
|
+
</FormControl>
|
|
62
|
+
<FormDescription>This is your public display name.</FormDescription>
|
|
63
|
+
<FormMessage />
|
|
64
|
+
</FormItem>
|
|
65
|
+
)}
|
|
66
|
+
/>
|
|
67
|
+
<FormField
|
|
68
|
+
control={form.control}
|
|
69
|
+
name="email"
|
|
70
|
+
render={({field}) => (
|
|
71
|
+
<FormItem>
|
|
72
|
+
<FormLabel>Email</FormLabel>
|
|
73
|
+
<FormControl>
|
|
74
|
+
<Input type="email" placeholder="email@example.com" {...field} />
|
|
75
|
+
</FormControl>
|
|
76
|
+
<FormDescription>We'll never share your email.</FormDescription>
|
|
77
|
+
<FormMessage />
|
|
78
|
+
</FormItem>
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
<Button type="submit">Submit</Button>
|
|
82
|
+
</form>
|
|
83
|
+
</Form>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const Basic: Story = {
|
|
88
|
+
render: () => <BasicFormExample />,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const componentsFormSchema = z.object({
|
|
92
|
+
example: z.string().min(1, 'This field is required'),
|
|
93
|
+
exampleTextarea: z.string().min(1, 'This field is required'),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
type ComponentsFormValues = z.infer<typeof componentsFormSchema>;
|
|
97
|
+
|
|
98
|
+
function FormComponentsExample() {
|
|
99
|
+
const form = useForm<ComponentsFormValues>({
|
|
100
|
+
resolver: zodResolver(componentsFormSchema),
|
|
101
|
+
defaultValues: {
|
|
102
|
+
example: '',
|
|
103
|
+
exampleTextarea: '',
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Form {...form}>
|
|
109
|
+
<div className="flex flex-col gap-32 w-full max-w-md">
|
|
110
|
+
<div className="flex flex-col gap-16">
|
|
111
|
+
<Header variant="h3">Form Components</Header>
|
|
112
|
+
<Code variant="label" className="text-foreground-neutral-subtle">
|
|
113
|
+
Individual form components work together within FormField
|
|
114
|
+
</Code>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<form className="space-y-8">
|
|
118
|
+
<FormField
|
|
119
|
+
control={form.control}
|
|
120
|
+
name="example"
|
|
121
|
+
render={({field}) => (
|
|
122
|
+
<FormItem>
|
|
123
|
+
<FormLabel>Form Label</FormLabel>
|
|
124
|
+
<FormControl>
|
|
125
|
+
<Input placeholder="Form Control with Input" {...field} />
|
|
126
|
+
</FormControl>
|
|
127
|
+
<FormDescription>This is a form description.</FormDescription>
|
|
128
|
+
<FormMessage />
|
|
129
|
+
</FormItem>
|
|
130
|
+
)}
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
<FormField
|
|
134
|
+
control={form.control}
|
|
135
|
+
name="exampleTextarea"
|
|
136
|
+
render={({field}) => (
|
|
137
|
+
<FormItem>
|
|
138
|
+
<FormLabel>Form Label</FormLabel>
|
|
139
|
+
<FormControl>
|
|
140
|
+
<Textarea placeholder="Form Control with Textarea" {...field} />
|
|
141
|
+
</FormControl>
|
|
142
|
+
<FormDescription>This is a form description.</FormDescription>
|
|
143
|
+
<FormMessage />
|
|
144
|
+
</FormItem>
|
|
145
|
+
)}
|
|
146
|
+
/>
|
|
147
|
+
</form>
|
|
148
|
+
</div>
|
|
149
|
+
</Form>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const Components: Story = {
|
|
154
|
+
render: () => <FormComponentsExample />,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const checkboxFormSchema = z.object({
|
|
158
|
+
username: z.string().min(3, 'Username must be at least 3 characters'),
|
|
159
|
+
email: z.string().email('Invalid email address'),
|
|
160
|
+
newsletter: z.boolean(),
|
|
161
|
+
notifications: z.boolean(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
type CheckboxFormValues = z.infer<typeof checkboxFormSchema>;
|
|
165
|
+
|
|
166
|
+
function FormWithCheckboxExample() {
|
|
167
|
+
const form = useForm<CheckboxFormValues>({
|
|
168
|
+
resolver: zodResolver(checkboxFormSchema),
|
|
169
|
+
defaultValues: {
|
|
170
|
+
username: '',
|
|
171
|
+
email: '',
|
|
172
|
+
newsletter: false,
|
|
173
|
+
notifications: false,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
function onSubmit(data: CheckboxFormValues) {
|
|
178
|
+
// biome-ignore lint/suspicious/noConsole: <we need to log the data for the story>
|
|
179
|
+
console.log(data);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<Form {...form}>
|
|
184
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full max-w-md">
|
|
185
|
+
<FormField
|
|
186
|
+
control={form.control}
|
|
187
|
+
name="username"
|
|
188
|
+
render={({field}) => (
|
|
189
|
+
<FormItem>
|
|
190
|
+
<FormLabel>Username</FormLabel>
|
|
191
|
+
<FormControl>
|
|
192
|
+
<Input placeholder="shadcn" {...field} />
|
|
193
|
+
</FormControl>
|
|
194
|
+
<FormDescription>This is your public display name.</FormDescription>
|
|
195
|
+
<FormMessage />
|
|
196
|
+
</FormItem>
|
|
197
|
+
)}
|
|
198
|
+
/>
|
|
199
|
+
<FormField
|
|
200
|
+
control={form.control}
|
|
201
|
+
name="email"
|
|
202
|
+
render={({field}) => (
|
|
203
|
+
<FormItem>
|
|
204
|
+
<FormLabel>Email</FormLabel>
|
|
205
|
+
<FormControl>
|
|
206
|
+
<Input type="email" placeholder="email@example.com" {...field} />
|
|
207
|
+
</FormControl>
|
|
208
|
+
<FormDescription>We'll never share your email.</FormDescription>
|
|
209
|
+
<FormMessage />
|
|
210
|
+
</FormItem>
|
|
211
|
+
)}
|
|
212
|
+
/>
|
|
213
|
+
<FormField
|
|
214
|
+
control={form.control}
|
|
215
|
+
name="newsletter"
|
|
216
|
+
render={({field}) => (
|
|
217
|
+
<FormItem className="flex flex-row items-center gap-8 pt-4">
|
|
218
|
+
<FormControl>
|
|
219
|
+
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
|
220
|
+
</FormControl>
|
|
221
|
+
<FormLabel>Subscribe to newsletter</FormLabel>
|
|
222
|
+
</FormItem>
|
|
223
|
+
)}
|
|
224
|
+
/>
|
|
225
|
+
<FormField
|
|
226
|
+
control={form.control}
|
|
227
|
+
name="notifications"
|
|
228
|
+
render={({field}) => (
|
|
229
|
+
<FormItem className="flex flex-row items-center gap-8 pt-4 pb-8">
|
|
230
|
+
<FormControl>
|
|
231
|
+
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
|
232
|
+
</FormControl>
|
|
233
|
+
<FormLabel>Enable notifications</FormLabel>
|
|
234
|
+
</FormItem>
|
|
235
|
+
)}
|
|
236
|
+
/>
|
|
237
|
+
<Button type="submit">Submit</Button>
|
|
238
|
+
</form>
|
|
239
|
+
</Form>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const WithCheckbox: Story = {
|
|
244
|
+
render: () => <FormWithCheckboxExample />,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const checkboxLabelSchema = z.object({
|
|
248
|
+
username: z.string().min(1, 'Username is required'),
|
|
249
|
+
email: z.string().email('Invalid email address'),
|
|
250
|
+
terms: z.boolean().refine((val) => val === true, {
|
|
251
|
+
message: 'You must accept the terms and conditions',
|
|
252
|
+
}),
|
|
253
|
+
marketing: z.boolean(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
type CheckboxLabelFormValues = z.infer<typeof checkboxLabelSchema>;
|
|
257
|
+
|
|
258
|
+
function FormWithCheckboxLabelExample() {
|
|
259
|
+
const form = useForm<CheckboxLabelFormValues>({
|
|
260
|
+
resolver: zodResolver(checkboxLabelSchema),
|
|
261
|
+
defaultValues: {
|
|
262
|
+
username: '',
|
|
263
|
+
email: '',
|
|
264
|
+
terms: false,
|
|
265
|
+
marketing: false,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
function onSubmit(data: CheckboxLabelFormValues) {
|
|
270
|
+
// biome-ignore lint/suspicious/noConsole: <we need to log the data for the story>
|
|
271
|
+
console.log(data);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<Form {...form}>
|
|
276
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full max-w-md">
|
|
277
|
+
<FormField
|
|
278
|
+
control={form.control}
|
|
279
|
+
name="username"
|
|
280
|
+
render={({field}) => (
|
|
281
|
+
<FormItem>
|
|
282
|
+
<FormLabel>Username</FormLabel>
|
|
283
|
+
<FormControl>
|
|
284
|
+
<Input placeholder="shadcn" {...field} />
|
|
285
|
+
</FormControl>
|
|
286
|
+
<FormMessage />
|
|
287
|
+
</FormItem>
|
|
288
|
+
)}
|
|
289
|
+
/>
|
|
290
|
+
<FormField
|
|
291
|
+
control={form.control}
|
|
292
|
+
name="email"
|
|
293
|
+
render={({field}) => (
|
|
294
|
+
<FormItem>
|
|
295
|
+
<FormLabel>Email</FormLabel>
|
|
296
|
+
<FormControl>
|
|
297
|
+
<Input type="email" placeholder="email@example.com" {...field} />
|
|
298
|
+
</FormControl>
|
|
299
|
+
<FormMessage />
|
|
300
|
+
</FormItem>
|
|
301
|
+
)}
|
|
302
|
+
/>
|
|
303
|
+
<FormField
|
|
304
|
+
control={form.control}
|
|
305
|
+
name="terms"
|
|
306
|
+
render={({field}) => (
|
|
307
|
+
<FormItem>
|
|
308
|
+
<FormControl>
|
|
309
|
+
<CheckboxLabel
|
|
310
|
+
label="I agree to the terms and conditions"
|
|
311
|
+
description="By checking this box, you agree to our terms of service and privacy policy."
|
|
312
|
+
checked={field.value}
|
|
313
|
+
onCheckedChange={field.onChange}
|
|
314
|
+
/>
|
|
315
|
+
</FormControl>
|
|
316
|
+
<FormMessage />
|
|
317
|
+
</FormItem>
|
|
318
|
+
)}
|
|
319
|
+
/>
|
|
320
|
+
<FormField
|
|
321
|
+
control={form.control}
|
|
322
|
+
name="marketing"
|
|
323
|
+
render={({field}) => (
|
|
324
|
+
<FormItem>
|
|
325
|
+
<FormControl>
|
|
326
|
+
<CheckboxLabel
|
|
327
|
+
label="I want to receive marketing emails"
|
|
328
|
+
description="Stay updated with our latest products and offers."
|
|
329
|
+
optional
|
|
330
|
+
checked={field.value}
|
|
331
|
+
onCheckedChange={field.onChange}
|
|
332
|
+
/>
|
|
333
|
+
</FormControl>
|
|
334
|
+
<FormMessage />
|
|
335
|
+
</FormItem>
|
|
336
|
+
)}
|
|
337
|
+
/>
|
|
338
|
+
<Button type="submit">Submit</Button>
|
|
339
|
+
</form>
|
|
340
|
+
</Form>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export const WithCheckboxLabel: Story = {
|
|
345
|
+
render: () => <FormWithCheckboxLabelExample />,
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const checkboxLinksSchema = z.object({
|
|
349
|
+
username: z.string().min(1, 'Username is required'),
|
|
350
|
+
email: z.string().email('Invalid email address'),
|
|
351
|
+
acceptPolicies: z.boolean().refine((val) => val === true, {
|
|
352
|
+
message: 'You must accept the policies to continue',
|
|
353
|
+
}),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
type CheckboxLinksFormValues = z.infer<typeof checkboxLinksSchema>;
|
|
357
|
+
|
|
358
|
+
function FormWithCheckboxLinksExample() {
|
|
359
|
+
const form = useForm<CheckboxLinksFormValues>({
|
|
360
|
+
resolver: zodResolver(checkboxLinksSchema),
|
|
361
|
+
defaultValues: {
|
|
362
|
+
username: '',
|
|
363
|
+
email: '',
|
|
364
|
+
acceptPolicies: false,
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
function onSubmit(data: CheckboxLinksFormValues) {
|
|
369
|
+
// biome-ignore lint/suspicious/noConsole: <we need to log the data for the story>
|
|
370
|
+
console.log(data);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<Form {...form}>
|
|
375
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full max-w-md">
|
|
376
|
+
<FormField
|
|
377
|
+
control={form.control}
|
|
378
|
+
name="username"
|
|
379
|
+
render={({field}) => (
|
|
380
|
+
<FormItem>
|
|
381
|
+
<FormLabel>Username</FormLabel>
|
|
382
|
+
<FormControl>
|
|
383
|
+
<Input placeholder="shadcn" {...field} />
|
|
384
|
+
</FormControl>
|
|
385
|
+
<FormMessage />
|
|
386
|
+
</FormItem>
|
|
387
|
+
)}
|
|
388
|
+
/>
|
|
389
|
+
<FormField
|
|
390
|
+
control={form.control}
|
|
391
|
+
name="email"
|
|
392
|
+
render={({field}) => (
|
|
393
|
+
<FormItem>
|
|
394
|
+
<FormLabel>Email</FormLabel>
|
|
395
|
+
<FormControl>
|
|
396
|
+
<Input type="email" placeholder="email@example.com" {...field} />
|
|
397
|
+
</FormControl>
|
|
398
|
+
<FormMessage />
|
|
399
|
+
</FormItem>
|
|
400
|
+
)}
|
|
401
|
+
/>
|
|
402
|
+
<FormField
|
|
403
|
+
control={form.control}
|
|
404
|
+
name="acceptPolicies"
|
|
405
|
+
render={({field}) => (
|
|
406
|
+
<FormItem>
|
|
407
|
+
<FormControl>
|
|
408
|
+
<CheckboxLinks
|
|
409
|
+
label="Accept policies"
|
|
410
|
+
links={[
|
|
411
|
+
{label: 'Terms of use', href: '#'},
|
|
412
|
+
{label: 'Privacy Policy', href: '#'},
|
|
413
|
+
]}
|
|
414
|
+
checked={field.value}
|
|
415
|
+
onCheckedChange={field.onChange}
|
|
416
|
+
/>
|
|
417
|
+
</FormControl>
|
|
418
|
+
<FormMessage />
|
|
419
|
+
</FormItem>
|
|
420
|
+
)}
|
|
421
|
+
/>
|
|
422
|
+
<Button type="submit">Submit</Button>
|
|
423
|
+
</form>
|
|
424
|
+
</Form>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export const WithCheckboxLinks: Story = {
|
|
429
|
+
render: () => <FormWithCheckboxLinksExample />,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const checkboxLabelBorderSchema = z.object({
|
|
433
|
+
plan: z.string().min(1, 'Plan name is required'),
|
|
434
|
+
newsletter: z.boolean(),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
type CheckboxLabelBorderFormValues = z.infer<typeof checkboxLabelBorderSchema>;
|
|
438
|
+
|
|
439
|
+
function FormWithCheckboxLabelBorderExample() {
|
|
440
|
+
const form = useForm<CheckboxLabelBorderFormValues>({
|
|
441
|
+
resolver: zodResolver(checkboxLabelBorderSchema),
|
|
442
|
+
defaultValues: {
|
|
443
|
+
plan: '',
|
|
444
|
+
newsletter: false,
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
function onSubmit(data: CheckboxLabelBorderFormValues) {
|
|
449
|
+
// biome-ignore lint/suspicious/noConsole: <we need to log the data for the story>
|
|
450
|
+
console.log(data);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<Form {...form}>
|
|
455
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full max-w-md">
|
|
456
|
+
<FormField
|
|
457
|
+
control={form.control}
|
|
458
|
+
name="plan"
|
|
459
|
+
render={({field}) => (
|
|
460
|
+
<FormItem>
|
|
461
|
+
<FormLabel>Select a plan</FormLabel>
|
|
462
|
+
<FormControl>
|
|
463
|
+
<Input placeholder="Enter plan name" {...field} />
|
|
464
|
+
</FormControl>
|
|
465
|
+
<FormDescription>Choose the plan that best fits your needs.</FormDescription>
|
|
466
|
+
<FormMessage />
|
|
467
|
+
</FormItem>
|
|
468
|
+
)}
|
|
469
|
+
/>
|
|
470
|
+
<FormField
|
|
471
|
+
control={form.control}
|
|
472
|
+
name="newsletter"
|
|
473
|
+
render={({field}) => (
|
|
474
|
+
<FormItem>
|
|
475
|
+
<FormControl>
|
|
476
|
+
<CheckboxLabel
|
|
477
|
+
label="Subscribe to our newsletter"
|
|
478
|
+
description="Get weekly updates, tips, and exclusive content delivered to your inbox."
|
|
479
|
+
optional
|
|
480
|
+
showInfoIcon
|
|
481
|
+
border
|
|
482
|
+
checked={field.value}
|
|
483
|
+
onCheckedChange={field.onChange}
|
|
484
|
+
/>
|
|
485
|
+
</FormControl>
|
|
486
|
+
<FormMessage />
|
|
487
|
+
</FormItem>
|
|
488
|
+
)}
|
|
489
|
+
/>
|
|
490
|
+
<Button type="submit" className="mt-4">
|
|
491
|
+
Submit
|
|
492
|
+
</Button>
|
|
493
|
+
</form>
|
|
494
|
+
</Form>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export const WithCheckboxLabelBorder: Story = {
|
|
499
|
+
render: () => <FormWithCheckboxLabelBorderExample />,
|
|
500
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {Slot} from '@radix-ui/react-slot';
|
|
2
|
+
import {Label} from 'components/label';
|
|
3
|
+
import type {ComponentProps} from 'react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import type {ControllerProps, FieldPath, FieldValues} from 'react-hook-form';
|
|
6
|
+
import {Controller, FormProvider, useFormContext} from 'react-hook-form';
|
|
7
|
+
import {cn} from 'utils/cn';
|
|
8
|
+
|
|
9
|
+
const Form = FormProvider;
|
|
10
|
+
|
|
11
|
+
type FormFieldContextValue<
|
|
12
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
13
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
14
|
+
> = {
|
|
15
|
+
name: TName;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type FormItemContextValue = {
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
|
|
23
|
+
|
|
24
|
+
const FormItemContext = React.createContext<FormItemContextValue | null>(null);
|
|
25
|
+
|
|
26
|
+
const FormField = <
|
|
27
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
28
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
29
|
+
>({
|
|
30
|
+
...props
|
|
31
|
+
}: ControllerProps<TFieldValues, TName>) => {
|
|
32
|
+
return (
|
|
33
|
+
<FormFieldContext.Provider value={{name: props.name}}>
|
|
34
|
+
<Controller {...props} />
|
|
35
|
+
</FormFieldContext.Provider>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const useFormField = () => {
|
|
40
|
+
const fieldContext = React.useContext(FormFieldContext);
|
|
41
|
+
const itemContext = React.useContext(FormItemContext);
|
|
42
|
+
const {getFieldState, formState} = useFormContext();
|
|
43
|
+
|
|
44
|
+
if (!fieldContext) {
|
|
45
|
+
throw new Error('useFormField should be used within <FormField>');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!itemContext) {
|
|
49
|
+
throw new Error('useFormField should be used within <FormItem>');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fieldState = getFieldState(fieldContext.name, formState);
|
|
53
|
+
const {id} = itemContext;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
name: fieldContext.name,
|
|
58
|
+
formItemId: `${id}-form-item`,
|
|
59
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
60
|
+
formMessageId: `${id}-form-item-message`,
|
|
61
|
+
...fieldState,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const FormItem = React.forwardRef<HTMLDivElement, ComponentProps<'div'>>(
|
|
66
|
+
({className, ...props}, ref) => {
|
|
67
|
+
const id = React.useId();
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<FormItemContext.Provider value={{id}}>
|
|
71
|
+
<div ref={ref} className={className} {...props} />
|
|
72
|
+
</FormItemContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
FormItem.displayName = 'FormItem';
|
|
77
|
+
|
|
78
|
+
const FormLabel = React.forwardRef<React.ElementRef<typeof Label>, ComponentProps<typeof Label>>(
|
|
79
|
+
({className, ...props}, ref) => {
|
|
80
|
+
const {error, formItemId} = useFormField();
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Label
|
|
84
|
+
ref={ref}
|
|
85
|
+
className={cn(error && 'text-foreground-error', className)}
|
|
86
|
+
htmlFor={formItemId}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
FormLabel.displayName = 'FormLabel';
|
|
93
|
+
|
|
94
|
+
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, ComponentProps<typeof Slot>>(
|
|
95
|
+
({...props}, ref) => {
|
|
96
|
+
const {error, formItemId, formDescriptionId, formMessageId} = useFormField();
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Slot
|
|
100
|
+
ref={ref}
|
|
101
|
+
id={formItemId}
|
|
102
|
+
aria-describedby={error ? formMessageId : formDescriptionId}
|
|
103
|
+
aria-invalid={!!error}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
FormControl.displayName = 'FormControl';
|
|
110
|
+
|
|
111
|
+
const FormDescription = React.forwardRef<HTMLParagraphElement, ComponentProps<'p'>>(
|
|
112
|
+
({className, ...props}, ref) => {
|
|
113
|
+
const {error, formDescriptionId} = useFormField();
|
|
114
|
+
|
|
115
|
+
if (error) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<p
|
|
121
|
+
ref={ref}
|
|
122
|
+
id={formDescriptionId}
|
|
123
|
+
className={cn('text-sm text-foreground-neutral-muted', className)}
|
|
124
|
+
{...props}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
FormDescription.displayName = 'FormDescription';
|
|
130
|
+
|
|
131
|
+
const FormMessage = React.forwardRef<HTMLParagraphElement, ComponentProps<'p'>>(
|
|
132
|
+
({className, children, ...props}, ref) => {
|
|
133
|
+
const {error, formMessageId} = useFormField();
|
|
134
|
+
const body = error ? String(error?.message ?? '') : children;
|
|
135
|
+
|
|
136
|
+
if (!body) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<p
|
|
142
|
+
ref={ref}
|
|
143
|
+
id={formMessageId}
|
|
144
|
+
className={cn('text-sm font-medium text-foreground-highlight-error', className)}
|
|
145
|
+
{...props}
|
|
146
|
+
>
|
|
147
|
+
{body}
|
|
148
|
+
</p>
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
FormMessage.displayName = 'FormMessage';
|
|
153
|
+
|
|
154
|
+
export {Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './form';
|