aural-ui 4.0.1 → 4.2.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/README.md +8 -1
- package/dist/components/aspect-ratio/AspectRatio.stories.tsx +290 -1228
- package/dist/components/avatar/Avatar.stories.tsx +219 -235
- package/dist/components/badge/Badge.stories.tsx +379 -116
- package/dist/components/banner/Banner.stories.tsx +445 -391
- package/dist/components/breadcrumb/Breadcrumb.stories.tsx +453 -199
- package/dist/components/button/Button.stories.tsx +585 -230
- package/dist/components/card/Card.stories.tsx +619 -301
- package/dist/components/char-count/CharCount.stories.tsx +350 -248
- package/dist/components/checkbox/Checkbox.stories.tsx +309 -167
- package/dist/components/chip/Chip.stories.tsx +362 -168
- package/dist/components/circular-loader/CircularLoader.stories.tsx +221 -636
- package/dist/components/clamp-lines/ClampLines.stories.tsx +246 -117
- package/dist/components/collapsible/Collapsible.stories.tsx +391 -252
- package/dist/components/command/Command.stories.tsx +530 -867
- package/dist/components/dialog/Dialog.stories.tsx +501 -950
- package/dist/components/divider/Divider.stories.tsx +264 -527
- package/dist/components/dot-loader/DotLoader.stories.tsx +256 -257
- package/dist/components/drawer/Drawer.stories.tsx +659 -1023
- package/dist/components/dropdown/Dropdown.stories.tsx +643 -1028
- package/dist/components/form/Form.stories.tsx +560 -274
- package/dist/components/helper-text/HelperText.stories.tsx +199 -200
- package/dist/components/hover-card/HoverCard.stories.tsx +318 -1254
- package/dist/components/icon-button/IconButton.stories.tsx +837 -194
- package/dist/components/if-else/if-else.stories.tsx +370 -83
- package/dist/components/input/Input.stories.tsx +436 -368
- package/dist/components/label/Label.stories.tsx +156 -154
- package/dist/components/list/List.stories.tsx +484 -835
- package/dist/components/marquee/Marquee.stories.tsx +356 -712
- package/dist/components/otp-inputs/OtpInputs.stories.tsx +352 -422
- package/dist/components/overlay/Overlay.stories.tsx +452 -824
- package/dist/components/pagination/Pagination.stories.tsx +721 -210
- package/dist/components/popover/Popover.stories.tsx +481 -896
- package/dist/components/radio/Radio.stories.tsx +432 -124
- package/dist/components/resizable/Resizable.stories.tsx +495 -799
- package/dist/components/scroll-area/ScrollArea.stories.tsx +383 -1059
- package/dist/components/search/Search.stories.tsx +312 -595
- package/dist/components/select/Select.stories.tsx +684 -789
- package/dist/components/sheet/Sheet.stories.tsx +671 -950
- package/dist/components/skelton/Skelton.stories.tsx +230 -764
- package/dist/components/slider/Slider.stories.tsx +383 -760
- package/dist/components/stepper/Stepper.stories.tsx +371 -514
- package/dist/components/switch/Switch.stories.tsx +461 -208
- package/dist/components/switch-case/SwitchCase.stories.tsx +367 -188
- package/dist/components/table/Table.stories.tsx +770 -916
- package/dist/components/tabs/Tabs.stories.tsx +458 -1455
- package/dist/components/tag/Tag.stories.tsx +714 -542
- package/dist/components/textarea/TextArea.stories.tsx +621 -562
- package/dist/components/thumbnail-tags/ThumbnailTags.stories.tsx +228 -154
- package/dist/components/toast/Toast.stories.tsx +452 -1339
- package/dist/components/toggle/Toggle.stories.tsx +488 -931
- package/dist/components/tooltip/Tooltip.stories.tsx +344 -1388
- package/dist/components/typography/Typography.stories.tsx +406 -89
- package/dist/hooks/use-change-state/UseChangeState.stories.tsx +309 -606
- package/dist/hooks/use-previous/UsePrevious.stories.tsx +367 -917
- package/dist/hooks/use-standalone-pagination/UseStandalonePagination.stories.tsx +639 -867
- package/dist/icons/Icons.stories.tsx +0 -12
- package/dist/icons/ai-avatar-icon/AiAvatarIcon.stories.tsx +223 -1060
- package/dist/icons/alert-icon/AlertIcon.stories.tsx +106 -968
- package/dist/icons/all-icons.tsx +37 -16
- package/dist/icons/angle-down-icon/AngleDownIcon.stories.tsx +137 -1010
- package/dist/icons/apple-logo-icon/AppleLogoIcon.stories.tsx +145 -935
- package/dist/icons/arrow-box-left-icon/ArrowBoxLeftIcon.stories.tsx +132 -1046
- package/dist/icons/arrow-corner-up-left-icon/ArrowCornerUpLeftIcon.stories.tsx +134 -986
- package/dist/icons/arrow-corner-up-right-icon/ArrowCornerUpRightIcon.stories.tsx +135 -1028
- package/dist/icons/arrow-left-icon/ArrowLeftIcon.stories.tsx +133 -971
- package/dist/icons/arrow-right-icon/ArrowRightIcon.stories.tsx +145 -1123
- package/dist/icons/arrow-right-up-icon/ArrowRightUpIcon.stories.tsx +143 -1252
- package/dist/icons/art-board-icon/ArtBoardIcon.stories.tsx +123 -632
- package/dist/icons/audio-bar-icon/AudioBarIcon.stories.tsx +141 -1223
- package/dist/icons/backward-ten-seconds-icon/BackwardTenSecondsIcon.stories.tsx +164 -1018
- package/dist/icons/bubble-check-icon/BubbleCheckIcon.stories.tsx +121 -1236
- package/dist/icons/bubble-crossed-icon/BubbleCrossedIcon.stories.tsx +121 -1213
- package/dist/icons/bubble-sparkle-icon/BubbleSparkleIcon.stories.tsx +116 -893
- package/dist/icons/camera-icon/CameraIcon.stories.tsx +109 -1254
- package/dist/icons/capital-a-letter-icon/CapitalALetterIcon.stories.tsx +114 -975
- package/dist/icons/chevron-double-left-icon/ChevronDoubleLeftIcon.stories.tsx +157 -994
- package/dist/icons/chevron-double-right-icon/ChevronDoubleRightIcon.stories.tsx +160 -992
- package/dist/icons/chevron-down-icon/ChevronDownIcon.stories.tsx +140 -970
- package/dist/icons/chevron-left-icon/ChevronLeftIcon.stories.tsx +126 -993
- package/dist/icons/chevron-right-icon/ChevronRightIcon.stories.tsx +144 -987
- package/dist/icons/chevron-up-icon/ChevronUpIcon.stories.tsx +141 -1007
- package/dist/icons/circle-tick-icon/CircleTickIcon.stories.tsx +147 -1187
- package/dist/icons/circular-play-icon/CircularPlayIcon.stories.tsx +110 -476
- package/dist/icons/coin-icon/CoinIcon.stories.tsx +120 -1364
- package/dist/icons/coin-toons-icon/CoinToonsIcon.stories.tsx +113 -1360
- package/dist/icons/column-wide-add-icon/ColumnWideAddIcon.stories.tsx +111 -942
- package/dist/icons/command-icon/CommandIcon.stories.tsx +124 -1087
- package/dist/icons/copy-icon/CopyIcon.stories.tsx +119 -996
- package/dist/icons/cross-circle-icon/CrossCircleIcon.stories.tsx +144 -1046
- package/dist/icons/cross-icon/CrossIcon.stories.tsx +136 -999
- package/dist/icons/download-icon/DownloadIcon.stories.tsx +123 -857
- package/dist/icons/edit-big-icon/EditBigIcon.stories.tsx +121 -1080
- package/dist/icons/email-icon/EmailIcon.stories.tsx +112 -979
- package/dist/icons/expand-icon/ExpandIcon.stories.tsx +109 -1146
- package/dist/icons/eye-close-icon/EyeCloseIcon.stories.tsx +141 -1068
- package/dist/icons/eye-open-icon/EyeOpenIcon.stories.tsx +140 -1081
- package/dist/icons/feature-shine-icon/FeatureShineIcon.stories.tsx +124 -1050
- package/dist/icons/file-chart-icon/FileChartIcon.stories.tsx +123 -1091
- package/dist/icons/file-text-icon/FileTextIcon.stories.tsx +122 -633
- package/dist/icons/filter-bar-row-icon/FilterBarRowIcon.stories.tsx +116 -1087
- package/dist/icons/forward-ten-seconds-icon/ForwardTenSecondsIcon.stories.tsx +166 -1020
- package/dist/icons/git-branch-icon/GitBranchIcon.stories.tsx +112 -1182
- package/dist/icons/git-fork-icon/GitForkIcon.stories.tsx +112 -1155
- package/dist/icons/globe-icon/GlobeIcon.stories.tsx +127 -325
- package/dist/icons/google-logo-icon/GoogleLogoIcon.stories.tsx +142 -985
- package/dist/icons/grip-vertical-icon/GripVerticalIcon.stories.tsx +116 -1217
- package/dist/icons/head-icon/HeadIcon.stories.tsx +108 -953
- package/dist/icons/heart-icon/HeartIcon.stories.tsx +117 -1060
- package/dist/icons/image-avatar-sparkle-icon/ImageAvatarSparkleIcon.stories.tsx +116 -716
- package/dist/icons/image-icon/ImageIcon.stories.tsx +102 -1164
- package/dist/icons/import-folder-icon/ImportFolderIcon.stories.tsx +108 -1233
- package/dist/icons/import-left-arrow-folder-icon/ImportLeftArrowFolderIcon.stories.tsx +133 -1289
- package/dist/icons/indian-flag-icon/IndianFlagIcon.stories.tsx +155 -1012
- package/dist/icons/instagram-icon/InstagramIcon.stories.tsx +158 -1438
- package/dist/icons/layout-column-icon/LayoutColumnIcon.stories.tsx +121 -1011
- package/dist/icons/layout-left-icon/LayoutLeftIcon.stories.tsx +116 -981
- package/dist/icons/layout-right-icon/LayoutRightIcon.stories.tsx +116 -979
- package/dist/icons/light-bulb-simple-icon/LightBulbSimpleIcon.stories.tsx +105 -1252
- package/dist/icons/linked-in-icon/LinkedInIcon.stories.tsx +151 -1554
- package/dist/icons/magic-book-icon/MagicBookIcon.stories.tsx +107 -1227
- package/dist/icons/magic-edit-icon/MagicEditIcon.stories.tsx +116 -707
- package/dist/icons/maintenance-icon/MaintenanceIcon.stories.tsx +119 -1226
- package/dist/icons/message-icon/MessageIcon.stories.tsx +111 -557
- package/dist/icons/minimize-icon/MinimizeIcon.stories.tsx +112 -1198
- package/dist/icons/moon-icon/MoonIcon.stories.tsx +117 -557
- package/dist/icons/move-horizontal-icon/MoveHorizontalIcon.stories.tsx +106 -1235
- package/dist/icons/move-vertical-icon/MoveVerticalIcon.stories.tsx +112 -1185
- package/dist/icons/musical-note-icon/MusicalNoteIcon.stories.tsx +116 -1012
- package/dist/icons/notepad-icon/NotepadIcon.stories.tsx +108 -1137
- package/dist/icons/notes-icon/NotesIcon.stories.tsx +116 -1138
- package/dist/icons/page-search-icon/PageSearchIcon.stories.tsx +106 -1146
- package/dist/icons/page-text-icon/PageTextIcon.stories.tsx +119 -719
- package/dist/icons/paint-roll-icon/PaintRollIcon.stories.tsx +110 -999
- package/dist/icons/paper-plane-icon/PaperPlaneIcon.stories.tsx +109 -912
- package/dist/icons/pause-icon/PauseIcon.stories.tsx +110 -1041
- package/dist/icons/pencil-icon/PencilIcon.stories.tsx +112 -1109
- package/dist/icons/phone-icon/PhoneIcon.stories.tsx +112 -1023
- package/dist/icons/plus-icon/PlusIcon.stories.tsx +103 -1132
- package/dist/icons/pocket-studio-icon/PocketStudioIcon.stories.tsx +104 -870
- package/dist/icons/scroll-down-icon/ScrollDownIcon.stories.tsx +99 -476
- package/dist/icons/search-icon/SearchIcon.stories.tsx +108 -1161
- package/dist/icons/setting-icon/SettingIcon.stories.tsx +104 -1009
- package/dist/icons/share-icon/ShareIcon.stories.tsx +117 -1064
- package/dist/icons/shield-icon/ShieldIcon.stories.tsx +114 -974
- package/dist/icons/site-logo-icon/SiteLogoIcon.stories.tsx +134 -1160
- package/dist/icons/skip-backward-icon/SkipBackwardIcon.stories.tsx +169 -1017
- package/dist/icons/skip-forward-icon/SkipForwardIcon.stories.tsx +161 -1016
- package/dist/icons/sparkles-soft-icon/SparklesSoftIcon.stories.tsx +102 -1001
- package/dist/icons/spinner-gradient-icon/SpinnerGradientIcon.stories.tsx +155 -593
- package/dist/icons/spinner-solid-icon/SpinnerSolidIcon.stories.tsx +155 -608
- package/dist/icons/spinner-solid-neutral-icon/SpinnerSolidINeutralcon.stories.tsx +142 -712
- package/dist/icons/star-icon/StarIcon.stories.tsx +120 -946
- package/dist/icons/store-coin-icon/StoreCoinIcon.stories.tsx +109 -1013
- package/dist/icons/suggestion-icon/SuggestionIcon.stories.tsx +113 -891
- package/dist/icons/sun-icon/SunIcon.stories.tsx +117 -864
- package/dist/icons/text-color-icon/TextColorIcon.stories.tsx +113 -989
- package/dist/icons/text-indicator-icon/TextIndicatorIcon.stories.tsx +120 -1027
- package/dist/icons/threads-icon/ThreadsIcon.stories.tsx +153 -1476
- package/dist/icons/tick-circle-icon/TickCircleIcon.stories.tsx +143 -1187
- package/dist/icons/tick-icon/TickIcon.stories.tsx +142 -1322
- package/dist/icons/trash-icon/TrashIcon.stories.tsx +105 -970
- package/dist/icons/twitter-x-icon/TwitterXIcon.stories.tsx +154 -1457
- package/dist/icons/upload-icon/UploadIcon.stories.tsx +112 -930
- package/dist/icons/vertical-menu-icon/VerticalMenuIcon.stories.tsx +115 -1019
- package/dist/icons/video-play-list-icon/VideoPlaylistIcon.stories.tsx +122 -1092
- package/dist/icons/voice-playing-icon/VoicePlayingIcon.stories.tsx +120 -1401
- package/dist/icons/volume-full-icon/VolumeFullIcon.stories.tsx +107 -1212
- package/dist/icons/volume-half-icon/VolumeHalfIcon.stories.tsx +109 -1122
- package/dist/icons/volume-off-icon/VolumeOffIcon.stories.tsx +112 -1124
- package/dist/icons/warning-icon/WarningIcon.stories.tsx +119 -1083
- package/dist/icons/youtube-icon/YoutubeIcon.stories.tsx +158 -983
- package/dist/index.cjs +90 -90
- package/dist/index.js +90 -90
- package/package.json +8 -3
|
@@ -2,11 +2,14 @@ import React, { useEffect } from "react"
|
|
|
2
2
|
import { Button } from "@components/button"
|
|
3
3
|
import { Checkbox } from "@components/checkbox"
|
|
4
4
|
import Input from "@components/input"
|
|
5
|
+
import TextArea from "@components/textarea"
|
|
5
6
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
6
7
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
7
8
|
import { useForm } from "react-hook-form"
|
|
8
9
|
import { z } from "zod"
|
|
9
10
|
|
|
11
|
+
import { AuralComponentDocsPage } from "src/ui/story-spec/components/component-story-docs-page"
|
|
12
|
+
|
|
10
13
|
import {
|
|
11
14
|
Form,
|
|
12
15
|
FormControl,
|
|
@@ -17,311 +20,594 @@ import {
|
|
|
17
20
|
FormMessage,
|
|
18
21
|
} from "."
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
// ─── Schema ───────────────────────────────────────────────────────────────────
|
|
21
24
|
|
|
22
|
-
const
|
|
23
|
-
username: z
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
email: z.string().email({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const profileSchema = z.object({
|
|
26
|
+
username: z
|
|
27
|
+
.string()
|
|
28
|
+
.min(2, { message: "Username must be at least 2 characters." }),
|
|
29
|
+
email: z.string().email({ message: "Please enter a valid email address." }),
|
|
30
|
+
bio: z
|
|
31
|
+
.string()
|
|
32
|
+
.max(160, { message: "Bio must be 160 characters or less." })
|
|
33
|
+
.optional(),
|
|
30
34
|
terms: z.boolean().refine((val) => val === true, {
|
|
31
35
|
message: "You must accept the terms and conditions.",
|
|
32
36
|
}),
|
|
33
37
|
})
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
onSubmit = () => {},
|
|
37
|
-
}: {
|
|
38
|
-
onSubmit?: (values: z.infer<typeof formSchema>) => void
|
|
39
|
-
}) => {
|
|
40
|
-
const form = useForm<z.infer<typeof formSchema>>({
|
|
41
|
-
resolver: zodResolver(formSchema),
|
|
42
|
-
defaultValues: {
|
|
43
|
-
username: "",
|
|
44
|
-
email: "",
|
|
45
|
-
bio: "",
|
|
46
|
-
terms: false,
|
|
47
|
-
},
|
|
48
|
-
})
|
|
39
|
+
type ProfileFormValues = z.infer<typeof profileSchema>
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
onSubmit(values)
|
|
52
|
-
}
|
|
41
|
+
// ─── Meta ─────────────────────────────────────────────────────────────────────
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
<Form {...form}>
|
|
56
|
-
<form
|
|
57
|
-
onSubmit={form.handleSubmit(handleSubmit)}
|
|
58
|
-
className="text-fm-primary w-full max-w-md space-y-6"
|
|
59
|
-
>
|
|
60
|
-
<FormField
|
|
61
|
-
control={form.control}
|
|
62
|
-
name="username"
|
|
63
|
-
render={({ field, fieldState }) => (
|
|
64
|
-
<FormItem>
|
|
65
|
-
<FormLabel>Username</FormLabel>
|
|
66
|
-
<FormControl>
|
|
67
|
-
<Input
|
|
68
|
-
placeholder="username"
|
|
69
|
-
variant={fieldState.error ? "error" : "default"}
|
|
70
|
-
{...field}
|
|
71
|
-
/>
|
|
72
|
-
</FormControl>
|
|
73
|
-
<FormMessage />
|
|
74
|
-
<FormDescription>
|
|
75
|
-
This is your public display name.
|
|
76
|
-
</FormDescription>
|
|
77
|
-
</FormItem>
|
|
78
|
-
)}
|
|
79
|
-
/>
|
|
80
|
-
<FormField
|
|
81
|
-
control={form.control}
|
|
82
|
-
name="email"
|
|
83
|
-
render={({ field, fieldState }) => {
|
|
84
|
-
return (
|
|
85
|
-
<FormItem>
|
|
86
|
-
<FormLabel>Email</FormLabel>
|
|
87
|
-
<FormControl>
|
|
88
|
-
<Input
|
|
89
|
-
type="email"
|
|
90
|
-
placeholder="email@example.com"
|
|
91
|
-
helperText={
|
|
92
|
-
fieldState.error
|
|
93
|
-
? fieldState.error.message
|
|
94
|
-
: "We'll never share your email with anyone else."
|
|
95
|
-
}
|
|
96
|
-
variant={fieldState.error ? "error" : "default"}
|
|
97
|
-
{...field}
|
|
98
|
-
/>
|
|
99
|
-
</FormControl>
|
|
100
|
-
</FormItem>
|
|
101
|
-
)
|
|
102
|
-
}}
|
|
103
|
-
/>
|
|
104
|
-
<FormField
|
|
105
|
-
control={form.control}
|
|
106
|
-
name="bio"
|
|
107
|
-
render={({ field }) => (
|
|
108
|
-
<FormItem>
|
|
109
|
-
<FormControl>
|
|
110
|
-
<Input
|
|
111
|
-
placeholder="Tell us about yourself"
|
|
112
|
-
label="Bio"
|
|
113
|
-
maxLength={MAX_DESCRIPTION_LENGTH}
|
|
114
|
-
helperText="Your bio will appear on your profile."
|
|
115
|
-
{...field}
|
|
116
|
-
/>
|
|
117
|
-
</FormControl>
|
|
118
|
-
<FormMessage />
|
|
119
|
-
</FormItem>
|
|
120
|
-
)}
|
|
121
|
-
/>
|
|
122
|
-
<FormField
|
|
123
|
-
control={form.control}
|
|
124
|
-
name="terms"
|
|
125
|
-
render={({ field }) => (
|
|
126
|
-
<FormItem className="flex flex-row flex-wrap items-start space-y-0 space-x-3 rounded-md border p-4">
|
|
127
|
-
<FormControl>
|
|
128
|
-
<Checkbox
|
|
129
|
-
checked={field.value}
|
|
130
|
-
onCheckedChange={field.onChange}
|
|
131
|
-
/>
|
|
132
|
-
</FormControl>
|
|
133
|
-
<div className="space-y-1 leading-none">
|
|
134
|
-
<FormLabel>Accept terms and conditions</FormLabel>
|
|
135
|
-
<FormDescription>
|
|
136
|
-
You agree to our Terms of Service and Privacy Policy.
|
|
137
|
-
</FormDescription>
|
|
138
|
-
</div>
|
|
139
|
-
<FormMessage />
|
|
140
|
-
</FormItem>
|
|
141
|
-
)}
|
|
142
|
-
/>
|
|
143
|
-
<Button type="submit">Submit</Button>
|
|
144
|
-
</form>
|
|
145
|
-
</Form>
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const meta: Meta<typeof FormExample> = {
|
|
43
|
+
const meta: Meta = {
|
|
150
44
|
title: "Components/UI/Form",
|
|
151
|
-
component: FormExample,
|
|
152
45
|
parameters: {
|
|
153
46
|
layout: "centered",
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
47
|
+
docs: {
|
|
48
|
+
description: {
|
|
49
|
+
component:
|
|
50
|
+
"A set of compound components for building accessible, validated forms with React Hook Form. Provides FormField, FormItem, FormLabel, FormControl, FormDescription, and FormMessage — each wired to form state automatically via context.",
|
|
51
|
+
},
|
|
52
|
+
page: () => (
|
|
53
|
+
<AuralComponentDocsPage
|
|
54
|
+
features={[
|
|
55
|
+
{
|
|
56
|
+
title: "React Hook Form",
|
|
57
|
+
description: "Context-wired validation",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: "Zod Schema Support",
|
|
61
|
+
description: "Resolver built in",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: "Compound Parts",
|
|
65
|
+
description: "Label, control, message",
|
|
66
|
+
},
|
|
67
|
+
]}
|
|
68
|
+
/>
|
|
69
|
+
),
|
|
160
70
|
},
|
|
161
71
|
},
|
|
162
72
|
tags: ["autodocs"],
|
|
163
|
-
argTypes: {
|
|
164
|
-
onSubmit: { action: "submitted" },
|
|
165
|
-
},
|
|
166
73
|
}
|
|
167
74
|
|
|
168
75
|
export default meta
|
|
169
|
-
type Story = StoryObj<typeof
|
|
76
|
+
type Story = StoryObj<typeof meta>
|
|
170
77
|
|
|
171
|
-
|
|
78
|
+
// ─── 1. Parts ────────────────────────────────────────────────────────────────
|
|
172
79
|
|
|
173
|
-
export const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
email: "",
|
|
180
|
-
bio: "",
|
|
181
|
-
terms: false,
|
|
80
|
+
export const Parts: Story = {
|
|
81
|
+
parameters: {
|
|
82
|
+
docs: {
|
|
83
|
+
description: {
|
|
84
|
+
story:
|
|
85
|
+
"Each sub-component of the Form system shown individually with its role and usage. FormField wraps a Controller, FormItem groups related elements, FormLabel renders a label tied to the field, FormControl slots the actual input, FormDescription adds helper text, and FormMessage surfaces validation errors.",
|
|
182
86
|
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
render: () => {
|
|
90
|
+
const form = useForm<ProfileFormValues>({
|
|
91
|
+
resolver: zodResolver(profileSchema),
|
|
92
|
+
defaultValues: { username: "", email: "", bio: "", terms: false },
|
|
183
93
|
})
|
|
184
94
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
95
|
+
return (
|
|
96
|
+
<div className="w-full max-w-lg space-y-10">
|
|
97
|
+
{/* FormItem + FormLabel */}
|
|
98
|
+
<div>
|
|
99
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
|
|
100
|
+
FormItem & FormLabel
|
|
101
|
+
</h4>
|
|
102
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
|
|
103
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-3">
|
|
104
|
+
FormItem provides the accessible group wrapper. FormLabel renders
|
|
105
|
+
a <code className="font-(--font-fm-mono)">{"<label>"}</code> tied
|
|
106
|
+
to the input via{" "}
|
|
107
|
+
<code className="font-(--font-fm-mono)">htmlFor</code>.
|
|
108
|
+
</p>
|
|
109
|
+
<Form {...form}>
|
|
110
|
+
<FormField
|
|
111
|
+
control={form.control}
|
|
112
|
+
name="username"
|
|
113
|
+
render={({ field }) => (
|
|
114
|
+
<FormItem>
|
|
115
|
+
<FormLabel>Username</FormLabel>
|
|
116
|
+
<FormControl>
|
|
117
|
+
<Input placeholder="your_handle" {...field} />
|
|
118
|
+
</FormControl>
|
|
119
|
+
</FormItem>
|
|
120
|
+
)}
|
|
121
|
+
/>
|
|
122
|
+
</Form>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* FormControl */}
|
|
127
|
+
<div>
|
|
128
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
|
|
129
|
+
FormControl
|
|
130
|
+
</h4>
|
|
131
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
|
|
132
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-3">
|
|
133
|
+
FormControl is a{" "}
|
|
134
|
+
<code className="font-(--font-fm-mono)">Slot</code> that forwards{" "}
|
|
135
|
+
<code className="font-(--font-fm-mono)">id</code>,{" "}
|
|
136
|
+
<code className="font-(--font-fm-mono)">aria-describedby</code>,
|
|
137
|
+
and <code className="font-(--font-fm-mono)">aria-invalid</code> to
|
|
138
|
+
the wrapped input automatically.
|
|
139
|
+
</p>
|
|
140
|
+
<Form {...form}>
|
|
141
|
+
<FormField
|
|
142
|
+
control={form.control}
|
|
143
|
+
name="email"
|
|
144
|
+
render={({ field }) => (
|
|
145
|
+
<FormItem>
|
|
146
|
+
<FormLabel>Email</FormLabel>
|
|
147
|
+
<FormControl>
|
|
148
|
+
<Input
|
|
149
|
+
type="email"
|
|
150
|
+
placeholder="you@example.com"
|
|
151
|
+
{...field}
|
|
152
|
+
/>
|
|
153
|
+
</FormControl>
|
|
154
|
+
</FormItem>
|
|
155
|
+
)}
|
|
156
|
+
/>
|
|
157
|
+
</Form>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
190
160
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
161
|
+
{/* FormDescription */}
|
|
162
|
+
<div>
|
|
163
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
|
|
164
|
+
FormDescription
|
|
165
|
+
</h4>
|
|
166
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
|
|
167
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-3">
|
|
168
|
+
FormDescription renders a{" "}
|
|
169
|
+
<code className="font-(--font-fm-mono)">HelperText</code> with a
|
|
170
|
+
stable ID linked to the input via{" "}
|
|
171
|
+
<code className="font-(--font-fm-mono)">aria-describedby</code>.
|
|
172
|
+
</p>
|
|
173
|
+
<Form {...form}>
|
|
174
|
+
<FormField
|
|
175
|
+
control={form.control}
|
|
176
|
+
name="username"
|
|
177
|
+
render={({ field }) => (
|
|
178
|
+
<FormItem>
|
|
179
|
+
<FormLabel>Username</FormLabel>
|
|
180
|
+
<FormControl>
|
|
181
|
+
<Input placeholder="your_handle" {...field} />
|
|
182
|
+
</FormControl>
|
|
183
|
+
<FormDescription>
|
|
184
|
+
This is your public display name.
|
|
185
|
+
</FormDescription>
|
|
186
|
+
</FormItem>
|
|
187
|
+
)}
|
|
188
|
+
/>
|
|
189
|
+
</Form>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
196
192
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
</FormItem>
|
|
214
|
-
)}
|
|
215
|
-
/>
|
|
216
|
-
<FormField
|
|
217
|
-
control={form.control}
|
|
218
|
-
name="email"
|
|
219
|
-
render={({ field }) => (
|
|
220
|
-
<FormItem>
|
|
221
|
-
<FormLabel>Email</FormLabel>
|
|
222
|
-
<FormControl>
|
|
223
|
-
<Input
|
|
224
|
-
type="email"
|
|
225
|
-
variant="error"
|
|
226
|
-
placeholder="email@example.com"
|
|
227
|
-
{...field}
|
|
228
|
-
/>
|
|
229
|
-
</FormControl>
|
|
230
|
-
<FormDescription>
|
|
231
|
-
We'll never share your email with anyone else.
|
|
232
|
-
</FormDescription>
|
|
233
|
-
<FormMessage />
|
|
234
|
-
</FormItem>
|
|
235
|
-
)}
|
|
236
|
-
/>
|
|
237
|
-
</form>
|
|
238
|
-
</Form>
|
|
193
|
+
{/* FormMessage */}
|
|
194
|
+
<div>
|
|
195
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
|
|
196
|
+
FormMessage
|
|
197
|
+
</h4>
|
|
198
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
|
|
199
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-3">
|
|
200
|
+
FormMessage reads the field error from form state and renders it
|
|
201
|
+
in a styled error{" "}
|
|
202
|
+
<code className="font-(--font-fm-mono)">HelperText</code>. It
|
|
203
|
+
renders nothing when there is no error.
|
|
204
|
+
</p>
|
|
205
|
+
<ErrorMessageDemo />
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
239
209
|
)
|
|
240
210
|
},
|
|
241
211
|
}
|
|
242
212
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
213
|
+
// Helper: shows FormMessage in error state without triggering a full form submit
|
|
214
|
+
function ErrorMessageDemo() {
|
|
215
|
+
const form = useForm<ProfileFormValues>({
|
|
216
|
+
resolver: zodResolver(profileSchema),
|
|
217
|
+
defaultValues: { username: "", email: "", bio: "", terms: false },
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
form.setError("username", {
|
|
222
|
+
type: "manual",
|
|
223
|
+
message: "Username is already taken.",
|
|
253
224
|
})
|
|
225
|
+
}, [form])
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<Form {...form}>
|
|
229
|
+
<FormField
|
|
230
|
+
control={form.control}
|
|
231
|
+
name="username"
|
|
232
|
+
render={({ field }) => (
|
|
233
|
+
<FormItem>
|
|
234
|
+
<FormLabel>Username</FormLabel>
|
|
235
|
+
<FormControl>
|
|
236
|
+
<Input placeholder="your_handle" variant="error" {...field} />
|
|
237
|
+
</FormControl>
|
|
238
|
+
<FormMessage />
|
|
239
|
+
</FormItem>
|
|
240
|
+
)}
|
|
241
|
+
/>
|
|
242
|
+
</Form>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── 2. States ───────────────────────────────────────────────────────────────
|
|
254
247
|
|
|
248
|
+
export const States: Story = {
|
|
249
|
+
parameters: {
|
|
250
|
+
docs: {
|
|
251
|
+
description: {
|
|
252
|
+
story:
|
|
253
|
+
"FormField in four distinct states: pristine (no interaction), valid (passes validation), error (validation failure with message), and warning (advisory notice via FormDescription).",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
render: () => {
|
|
255
258
|
return (
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
control={form.control}
|
|
290
|
-
name="bio"
|
|
291
|
-
render={({ field }) => (
|
|
292
|
-
<FormItem>
|
|
293
|
-
<FormLabel>Bio</FormLabel>
|
|
294
|
-
<FormControl>
|
|
295
|
-
<Input {...field} />
|
|
296
|
-
</FormControl>
|
|
297
|
-
<FormDescription>
|
|
298
|
-
Your bio will appear on your profile.
|
|
299
|
-
</FormDescription>
|
|
300
|
-
</FormItem>
|
|
301
|
-
)}
|
|
302
|
-
/>
|
|
303
|
-
<FormField
|
|
304
|
-
control={form.control}
|
|
305
|
-
name="terms"
|
|
306
|
-
render={({ field }) => (
|
|
307
|
-
<FormItem className="flex flex-row items-start space-y-0 space-x-3 rounded-md border p-4">
|
|
308
|
-
<FormControl>
|
|
309
|
-
<Checkbox
|
|
310
|
-
checked={field.value}
|
|
311
|
-
onCheckedChange={field.onChange}
|
|
312
|
-
/>
|
|
313
|
-
</FormControl>
|
|
314
|
-
<div className="space-y-1 leading-none">
|
|
315
|
-
<FormLabel>Accept terms and conditions</FormLabel>
|
|
316
|
-
<FormDescription>
|
|
317
|
-
You agree to our Terms of Service and Privacy Policy.
|
|
318
|
-
</FormDescription>
|
|
319
|
-
</div>
|
|
320
|
-
</FormItem>
|
|
321
|
-
)}
|
|
322
|
-
/>
|
|
323
|
-
</form>
|
|
324
|
-
</Form>
|
|
259
|
+
<div className="w-full max-w-md space-y-8">
|
|
260
|
+
{/* Pristine */}
|
|
261
|
+
<div>
|
|
262
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
|
|
263
|
+
Pristine
|
|
264
|
+
</h4>
|
|
265
|
+
<PristineField />
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Valid */}
|
|
269
|
+
<div>
|
|
270
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
|
|
271
|
+
Valid
|
|
272
|
+
</h4>
|
|
273
|
+
<ValidField />
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{/* Error */}
|
|
277
|
+
<div>
|
|
278
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
|
|
279
|
+
Error
|
|
280
|
+
</h4>
|
|
281
|
+
<ErrorField />
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{/* Warning */}
|
|
285
|
+
<div>
|
|
286
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
|
|
287
|
+
Warning (advisory)
|
|
288
|
+
</h4>
|
|
289
|
+
<WarningField />
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
325
292
|
)
|
|
326
293
|
},
|
|
327
294
|
}
|
|
295
|
+
|
|
296
|
+
function PristineField() {
|
|
297
|
+
const form = useForm({ defaultValues: { email: "" } })
|
|
298
|
+
return (
|
|
299
|
+
<Form {...form}>
|
|
300
|
+
<FormField
|
|
301
|
+
control={form.control}
|
|
302
|
+
name="email"
|
|
303
|
+
render={({ field }) => (
|
|
304
|
+
<FormItem>
|
|
305
|
+
<FormLabel>Email</FormLabel>
|
|
306
|
+
<FormControl>
|
|
307
|
+
<Input
|
|
308
|
+
type="email"
|
|
309
|
+
placeholder="you@example.com"
|
|
310
|
+
fullWidth
|
|
311
|
+
{...field}
|
|
312
|
+
/>
|
|
313
|
+
</FormControl>
|
|
314
|
+
<FormDescription>
|
|
315
|
+
Enter the email address associated with your account.
|
|
316
|
+
</FormDescription>
|
|
317
|
+
</FormItem>
|
|
318
|
+
)}
|
|
319
|
+
/>
|
|
320
|
+
</Form>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function ValidField() {
|
|
325
|
+
const form = useForm({ defaultValues: { email: "user@example.com" } })
|
|
326
|
+
return (
|
|
327
|
+
<Form {...form}>
|
|
328
|
+
<FormField
|
|
329
|
+
control={form.control}
|
|
330
|
+
name="email"
|
|
331
|
+
render={({ field }) => (
|
|
332
|
+
<FormItem>
|
|
333
|
+
<FormLabel>Email</FormLabel>
|
|
334
|
+
<FormControl>
|
|
335
|
+
<Input type="email" variant="success" fullWidth {...field} />
|
|
336
|
+
</FormControl>
|
|
337
|
+
<FormDescription>Email address is valid.</FormDescription>
|
|
338
|
+
</FormItem>
|
|
339
|
+
)}
|
|
340
|
+
/>
|
|
341
|
+
</Form>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function ErrorField() {
|
|
346
|
+
const form = useForm<{ email: string }>({
|
|
347
|
+
resolver: zodResolver(z.object({ email: z.string().email() })),
|
|
348
|
+
defaultValues: { email: "" },
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
form.setError("email", {
|
|
353
|
+
type: "manual",
|
|
354
|
+
message: "Please enter a valid email address.",
|
|
355
|
+
})
|
|
356
|
+
}, [form])
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<Form {...form}>
|
|
360
|
+
<FormField
|
|
361
|
+
control={form.control}
|
|
362
|
+
name="email"
|
|
363
|
+
render={({ field }) => (
|
|
364
|
+
<FormItem>
|
|
365
|
+
<FormLabel>Email</FormLabel>
|
|
366
|
+
<FormControl>
|
|
367
|
+
<Input
|
|
368
|
+
type="email"
|
|
369
|
+
placeholder="you@example.com"
|
|
370
|
+
variant="error"
|
|
371
|
+
fullWidth
|
|
372
|
+
{...field}
|
|
373
|
+
/>
|
|
374
|
+
</FormControl>
|
|
375
|
+
<FormMessage />
|
|
376
|
+
</FormItem>
|
|
377
|
+
)}
|
|
378
|
+
/>
|
|
379
|
+
</Form>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function WarningField() {
|
|
384
|
+
const form = useForm({ defaultValues: { password: "abc" } })
|
|
385
|
+
return (
|
|
386
|
+
<Form {...form}>
|
|
387
|
+
<FormField
|
|
388
|
+
control={form.control}
|
|
389
|
+
name="password"
|
|
390
|
+
render={({ field }) => (
|
|
391
|
+
<FormItem>
|
|
392
|
+
<FormLabel>Password</FormLabel>
|
|
393
|
+
<FormControl>
|
|
394
|
+
<Input type="password" variant="warning" fullWidth {...field} />
|
|
395
|
+
</FormControl>
|
|
396
|
+
<FormDescription>
|
|
397
|
+
Password is weak — use at least 8 characters including a number.
|
|
398
|
+
</FormDescription>
|
|
399
|
+
</FormItem>
|
|
400
|
+
)}
|
|
401
|
+
/>
|
|
402
|
+
</Form>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ─── 3. Interactive ──────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
export const Interactive: Story = {
|
|
409
|
+
parameters: {
|
|
410
|
+
docs: {
|
|
411
|
+
description: {
|
|
412
|
+
story:
|
|
413
|
+
"A live profile-edit form with Zod + React Hook Form. Errors appear in real time as you interact with each field. Try submitting with empty or invalid values to see FormMessage in action.",
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
render: () => <ProfileForm />,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function ProfileForm() {
|
|
421
|
+
const form = useForm<ProfileFormValues>({
|
|
422
|
+
resolver: zodResolver(profileSchema),
|
|
423
|
+
defaultValues: { username: "", email: "", bio: "", terms: false },
|
|
424
|
+
mode: "onChange",
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
const [submitted, setSubmitted] = React.useState<ProfileFormValues | null>(
|
|
428
|
+
null
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
function handleSubmit(values: ProfileFormValues) {
|
|
432
|
+
setSubmitted(values)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<div className="w-full p-8">
|
|
437
|
+
<div className="mx-auto max-w-3xl space-y-6">
|
|
438
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
439
|
+
{/* Controls panel */}
|
|
440
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary space-y-5 rounded-xl border p-5">
|
|
441
|
+
<p className="text-fm-primary font-fm-brand text-fm-sm leading-fm-sm font-semibold tracking-widest uppercase">
|
|
442
|
+
Form State
|
|
443
|
+
</p>
|
|
444
|
+
<div className="space-y-3">
|
|
445
|
+
<div className="border-fm-divider-secondary bg-fm-surface-primary rounded-lg border p-3">
|
|
446
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-1 font-medium">
|
|
447
|
+
Is Valid
|
|
448
|
+
</p>
|
|
449
|
+
<p className="text-fm-primary text-fm-sm leading-fm-sm font-(--font-fm-mono)">
|
|
450
|
+
{form.formState.isValid ? "true" : "false"}
|
|
451
|
+
</p>
|
|
452
|
+
</div>
|
|
453
|
+
<div className="border-fm-divider-secondary bg-fm-surface-primary rounded-lg border p-3">
|
|
454
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-1 font-medium">
|
|
455
|
+
Is Submitted
|
|
456
|
+
</p>
|
|
457
|
+
<p className="text-fm-primary text-fm-sm leading-fm-sm font-(--font-fm-mono)">
|
|
458
|
+
{form.formState.isSubmitted ? "true" : "false"}
|
|
459
|
+
</p>
|
|
460
|
+
</div>
|
|
461
|
+
<div className="border-fm-divider-secondary bg-fm-surface-primary rounded-lg border p-3">
|
|
462
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-1 font-medium">
|
|
463
|
+
Error Count
|
|
464
|
+
</p>
|
|
465
|
+
<p className="text-fm-primary text-fm-sm leading-fm-sm font-(--font-fm-mono)">
|
|
466
|
+
{Object.keys(form.formState.errors).length}
|
|
467
|
+
</p>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
<div className="border-fm-divider-secondary border-t pt-4" />
|
|
471
|
+
{submitted && (
|
|
472
|
+
<div className="border-fm-divider-secondary bg-fm-surface-primary rounded-lg border p-3">
|
|
473
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-1 font-medium">
|
|
474
|
+
Last Submission
|
|
475
|
+
</p>
|
|
476
|
+
<p className="text-fm-primary text-fm-sm leading-fm-sm font-(--font-fm-mono) break-all">
|
|
477
|
+
{submitted.username} / {submitted.email}
|
|
478
|
+
</p>
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
<Button
|
|
482
|
+
variant="outline"
|
|
483
|
+
size="sm"
|
|
484
|
+
onClick={() => {
|
|
485
|
+
form.reset()
|
|
486
|
+
setSubmitted(null)
|
|
487
|
+
}}
|
|
488
|
+
className="w-full"
|
|
489
|
+
>
|
|
490
|
+
Reset Form
|
|
491
|
+
</Button>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
{/* Preview stage */}
|
|
495
|
+
<div className="flex flex-col gap-3 lg:col-span-2">
|
|
496
|
+
<Form {...form}>
|
|
497
|
+
<form
|
|
498
|
+
onSubmit={form.handleSubmit(handleSubmit)}
|
|
499
|
+
className="space-y-5"
|
|
500
|
+
>
|
|
501
|
+
{/* Username */}
|
|
502
|
+
<FormField
|
|
503
|
+
control={form.control}
|
|
504
|
+
name="username"
|
|
505
|
+
render={({ field, fieldState }) => (
|
|
506
|
+
<FormItem>
|
|
507
|
+
<FormLabel>Username</FormLabel>
|
|
508
|
+
<FormControl>
|
|
509
|
+
<Input
|
|
510
|
+
placeholder="your_handle"
|
|
511
|
+
variant={
|
|
512
|
+
fieldState.error
|
|
513
|
+
? "error"
|
|
514
|
+
: fieldState.isDirty && !fieldState.error
|
|
515
|
+
? "success"
|
|
516
|
+
: "default"
|
|
517
|
+
}
|
|
518
|
+
fullWidth
|
|
519
|
+
{...field}
|
|
520
|
+
/>
|
|
521
|
+
</FormControl>
|
|
522
|
+
<FormDescription>
|
|
523
|
+
Your public display name (min 2 characters).
|
|
524
|
+
</FormDescription>
|
|
525
|
+
<FormMessage />
|
|
526
|
+
</FormItem>
|
|
527
|
+
)}
|
|
528
|
+
/>
|
|
529
|
+
|
|
530
|
+
{/* Email */}
|
|
531
|
+
<FormField
|
|
532
|
+
control={form.control}
|
|
533
|
+
name="email"
|
|
534
|
+
render={({ field, fieldState }) => (
|
|
535
|
+
<FormItem>
|
|
536
|
+
<FormLabel>Email</FormLabel>
|
|
537
|
+
<FormControl>
|
|
538
|
+
<Input
|
|
539
|
+
type="email"
|
|
540
|
+
placeholder="you@example.com"
|
|
541
|
+
variant={
|
|
542
|
+
fieldState.error
|
|
543
|
+
? "error"
|
|
544
|
+
: fieldState.isDirty && !fieldState.error
|
|
545
|
+
? "success"
|
|
546
|
+
: "default"
|
|
547
|
+
}
|
|
548
|
+
fullWidth
|
|
549
|
+
{...field}
|
|
550
|
+
/>
|
|
551
|
+
</FormControl>
|
|
552
|
+
<FormMessage />
|
|
553
|
+
</FormItem>
|
|
554
|
+
)}
|
|
555
|
+
/>
|
|
556
|
+
|
|
557
|
+
{/* Bio */}
|
|
558
|
+
<FormField
|
|
559
|
+
control={form.control}
|
|
560
|
+
name="bio"
|
|
561
|
+
render={({ field, fieldState }) => (
|
|
562
|
+
<FormItem>
|
|
563
|
+
<FormControl>
|
|
564
|
+
<TextArea
|
|
565
|
+
label="Bio"
|
|
566
|
+
placeholder="Tell us about yourself (optional, max 160 chars)"
|
|
567
|
+
variant={fieldState.error ? "error" : "default"}
|
|
568
|
+
maxLength={160}
|
|
569
|
+
showCharCount
|
|
570
|
+
decoration="outline"
|
|
571
|
+
fullWidth
|
|
572
|
+
{...field}
|
|
573
|
+
/>
|
|
574
|
+
</FormControl>
|
|
575
|
+
<FormMessage />
|
|
576
|
+
</FormItem>
|
|
577
|
+
)}
|
|
578
|
+
/>
|
|
579
|
+
|
|
580
|
+
{/* Terms */}
|
|
581
|
+
<FormField
|
|
582
|
+
control={form.control}
|
|
583
|
+
name="terms"
|
|
584
|
+
render={({ field }) => (
|
|
585
|
+
<FormItem className="border-fm-divider-secondary flex flex-row flex-wrap items-start space-y-0 space-x-3 rounded-md border p-4">
|
|
586
|
+
<FormControl>
|
|
587
|
+
<Checkbox
|
|
588
|
+
checked={field.value}
|
|
589
|
+
onCheckedChange={field.onChange}
|
|
590
|
+
/>
|
|
591
|
+
</FormControl>
|
|
592
|
+
<div className="space-y-1 leading-none">
|
|
593
|
+
<FormLabel>Accept terms and conditions</FormLabel>
|
|
594
|
+
<FormDescription>
|
|
595
|
+
You agree to our Terms of Service and Privacy Policy.
|
|
596
|
+
</FormDescription>
|
|
597
|
+
</div>
|
|
598
|
+
<FormMessage />
|
|
599
|
+
</FormItem>
|
|
600
|
+
)}
|
|
601
|
+
/>
|
|
602
|
+
|
|
603
|
+
<Button type="submit" className="w-full">
|
|
604
|
+
Save Profile
|
|
605
|
+
</Button>
|
|
606
|
+
</form>
|
|
607
|
+
</Form>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
)
|
|
613
|
+
}
|