aural-ui 4.0.1 → 4.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 (174) hide show
  1. package/dist/components/aspect-ratio/AspectRatio.stories.tsx +290 -1228
  2. package/dist/components/avatar/Avatar.stories.tsx +219 -235
  3. package/dist/components/badge/Badge.stories.tsx +379 -116
  4. package/dist/components/banner/Banner.stories.tsx +445 -391
  5. package/dist/components/breadcrumb/Breadcrumb.stories.tsx +453 -199
  6. package/dist/components/button/Button.stories.tsx +585 -230
  7. package/dist/components/card/Card.stories.tsx +619 -301
  8. package/dist/components/char-count/CharCount.stories.tsx +350 -248
  9. package/dist/components/checkbox/Checkbox.stories.tsx +309 -167
  10. package/dist/components/chip/Chip.stories.tsx +362 -168
  11. package/dist/components/circular-loader/CircularLoader.stories.tsx +221 -636
  12. package/dist/components/clamp-lines/ClampLines.stories.tsx +246 -117
  13. package/dist/components/collapsible/Collapsible.stories.tsx +391 -252
  14. package/dist/components/command/Command.stories.tsx +530 -867
  15. package/dist/components/dialog/Dialog.stories.tsx +501 -950
  16. package/dist/components/divider/Divider.stories.tsx +264 -527
  17. package/dist/components/dot-loader/DotLoader.stories.tsx +256 -257
  18. package/dist/components/drawer/Drawer.stories.tsx +659 -1023
  19. package/dist/components/dropdown/Dropdown.stories.tsx +643 -1028
  20. package/dist/components/form/Form.stories.tsx +560 -274
  21. package/dist/components/helper-text/HelperText.stories.tsx +199 -200
  22. package/dist/components/hover-card/HoverCard.stories.tsx +318 -1254
  23. package/dist/components/icon-button/IconButton.stories.tsx +837 -194
  24. package/dist/components/if-else/if-else.stories.tsx +370 -83
  25. package/dist/components/input/Input.stories.tsx +436 -368
  26. package/dist/components/label/Label.stories.tsx +156 -154
  27. package/dist/components/list/List.stories.tsx +484 -835
  28. package/dist/components/marquee/Marquee.stories.tsx +356 -712
  29. package/dist/components/otp-inputs/OtpInputs.stories.tsx +352 -422
  30. package/dist/components/overlay/Overlay.stories.tsx +452 -824
  31. package/dist/components/pagination/Pagination.stories.tsx +721 -210
  32. package/dist/components/popover/Popover.stories.tsx +481 -896
  33. package/dist/components/radio/Radio.stories.tsx +432 -124
  34. package/dist/components/resizable/Resizable.stories.tsx +495 -799
  35. package/dist/components/scroll-area/ScrollArea.stories.tsx +383 -1059
  36. package/dist/components/search/Search.stories.tsx +312 -595
  37. package/dist/components/select/Select.stories.tsx +684 -789
  38. package/dist/components/sheet/Sheet.stories.tsx +671 -950
  39. package/dist/components/skelton/Skelton.stories.tsx +230 -764
  40. package/dist/components/slider/Slider.stories.tsx +383 -760
  41. package/dist/components/stepper/Stepper.stories.tsx +371 -514
  42. package/dist/components/switch/Switch.stories.tsx +461 -208
  43. package/dist/components/switch-case/SwitchCase.stories.tsx +367 -188
  44. package/dist/components/table/Table.stories.tsx +770 -916
  45. package/dist/components/tabs/Tabs.stories.tsx +458 -1455
  46. package/dist/components/tag/Tag.stories.tsx +714 -542
  47. package/dist/components/textarea/TextArea.stories.tsx +621 -562
  48. package/dist/components/thumbnail-tags/ThumbnailTags.stories.tsx +228 -154
  49. package/dist/components/toast/Toast.stories.tsx +452 -1339
  50. package/dist/components/toggle/Toggle.stories.tsx +488 -931
  51. package/dist/components/tooltip/Tooltip.stories.tsx +344 -1388
  52. package/dist/components/typography/Typography.stories.tsx +406 -89
  53. package/dist/hooks/use-change-state/UseChangeState.stories.tsx +309 -606
  54. package/dist/hooks/use-previous/UsePrevious.stories.tsx +367 -917
  55. package/dist/hooks/use-standalone-pagination/UseStandalonePagination.stories.tsx +639 -867
  56. package/dist/icons/Icons.stories.tsx +0 -12
  57. package/dist/icons/ai-avatar-icon/AiAvatarIcon.stories.tsx +223 -1060
  58. package/dist/icons/alert-icon/AlertIcon.stories.tsx +106 -968
  59. package/dist/icons/all-icons.tsx +37 -16
  60. package/dist/icons/angle-down-icon/AngleDownIcon.stories.tsx +137 -1010
  61. package/dist/icons/apple-logo-icon/AppleLogoIcon.stories.tsx +145 -935
  62. package/dist/icons/arrow-box-left-icon/ArrowBoxLeftIcon.stories.tsx +132 -1046
  63. package/dist/icons/arrow-corner-up-left-icon/ArrowCornerUpLeftIcon.stories.tsx +134 -986
  64. package/dist/icons/arrow-corner-up-right-icon/ArrowCornerUpRightIcon.stories.tsx +135 -1028
  65. package/dist/icons/arrow-left-icon/ArrowLeftIcon.stories.tsx +133 -971
  66. package/dist/icons/arrow-right-icon/ArrowRightIcon.stories.tsx +145 -1123
  67. package/dist/icons/arrow-right-up-icon/ArrowRightUpIcon.stories.tsx +143 -1252
  68. package/dist/icons/art-board-icon/ArtBoardIcon.stories.tsx +123 -632
  69. package/dist/icons/audio-bar-icon/AudioBarIcon.stories.tsx +141 -1223
  70. package/dist/icons/backward-ten-seconds-icon/BackwardTenSecondsIcon.stories.tsx +164 -1018
  71. package/dist/icons/bubble-check-icon/BubbleCheckIcon.stories.tsx +121 -1236
  72. package/dist/icons/bubble-crossed-icon/BubbleCrossedIcon.stories.tsx +121 -1213
  73. package/dist/icons/bubble-sparkle-icon/BubbleSparkleIcon.stories.tsx +116 -893
  74. package/dist/icons/camera-icon/CameraIcon.stories.tsx +109 -1254
  75. package/dist/icons/capital-a-letter-icon/CapitalALetterIcon.stories.tsx +114 -975
  76. package/dist/icons/chevron-double-left-icon/ChevronDoubleLeftIcon.stories.tsx +157 -994
  77. package/dist/icons/chevron-double-right-icon/ChevronDoubleRightIcon.stories.tsx +160 -992
  78. package/dist/icons/chevron-down-icon/ChevronDownIcon.stories.tsx +140 -970
  79. package/dist/icons/chevron-left-icon/ChevronLeftIcon.stories.tsx +126 -993
  80. package/dist/icons/chevron-right-icon/ChevronRightIcon.stories.tsx +144 -987
  81. package/dist/icons/chevron-up-icon/ChevronUpIcon.stories.tsx +141 -1007
  82. package/dist/icons/circle-tick-icon/CircleTickIcon.stories.tsx +147 -1187
  83. package/dist/icons/circular-play-icon/CircularPlayIcon.stories.tsx +110 -476
  84. package/dist/icons/coin-icon/CoinIcon.stories.tsx +120 -1364
  85. package/dist/icons/coin-toons-icon/CoinToonsIcon.stories.tsx +113 -1360
  86. package/dist/icons/column-wide-add-icon/ColumnWideAddIcon.stories.tsx +111 -942
  87. package/dist/icons/command-icon/CommandIcon.stories.tsx +124 -1087
  88. package/dist/icons/copy-icon/CopyIcon.stories.tsx +119 -996
  89. package/dist/icons/cross-circle-icon/CrossCircleIcon.stories.tsx +144 -1046
  90. package/dist/icons/cross-icon/CrossIcon.stories.tsx +136 -999
  91. package/dist/icons/download-icon/DownloadIcon.stories.tsx +123 -857
  92. package/dist/icons/edit-big-icon/EditBigIcon.stories.tsx +121 -1080
  93. package/dist/icons/email-icon/EmailIcon.stories.tsx +112 -979
  94. package/dist/icons/expand-icon/ExpandIcon.stories.tsx +109 -1146
  95. package/dist/icons/eye-close-icon/EyeCloseIcon.stories.tsx +141 -1068
  96. package/dist/icons/eye-open-icon/EyeOpenIcon.stories.tsx +140 -1081
  97. package/dist/icons/feature-shine-icon/FeatureShineIcon.stories.tsx +124 -1050
  98. package/dist/icons/file-chart-icon/FileChartIcon.stories.tsx +123 -1091
  99. package/dist/icons/file-text-icon/FileTextIcon.stories.tsx +122 -633
  100. package/dist/icons/filter-bar-row-icon/FilterBarRowIcon.stories.tsx +116 -1087
  101. package/dist/icons/forward-ten-seconds-icon/ForwardTenSecondsIcon.stories.tsx +166 -1020
  102. package/dist/icons/git-branch-icon/GitBranchIcon.stories.tsx +112 -1182
  103. package/dist/icons/git-fork-icon/GitForkIcon.stories.tsx +112 -1155
  104. package/dist/icons/globe-icon/GlobeIcon.stories.tsx +127 -325
  105. package/dist/icons/google-logo-icon/GoogleLogoIcon.stories.tsx +142 -985
  106. package/dist/icons/grip-vertical-icon/GripVerticalIcon.stories.tsx +116 -1217
  107. package/dist/icons/head-icon/HeadIcon.stories.tsx +108 -953
  108. package/dist/icons/heart-icon/HeartIcon.stories.tsx +117 -1060
  109. package/dist/icons/image-avatar-sparkle-icon/ImageAvatarSparkleIcon.stories.tsx +116 -716
  110. package/dist/icons/image-icon/ImageIcon.stories.tsx +102 -1164
  111. package/dist/icons/import-folder-icon/ImportFolderIcon.stories.tsx +108 -1233
  112. package/dist/icons/import-left-arrow-folder-icon/ImportLeftArrowFolderIcon.stories.tsx +133 -1289
  113. package/dist/icons/indian-flag-icon/IndianFlagIcon.stories.tsx +155 -1012
  114. package/dist/icons/instagram-icon/InstagramIcon.stories.tsx +158 -1438
  115. package/dist/icons/layout-column-icon/LayoutColumnIcon.stories.tsx +121 -1011
  116. package/dist/icons/layout-left-icon/LayoutLeftIcon.stories.tsx +116 -981
  117. package/dist/icons/layout-right-icon/LayoutRightIcon.stories.tsx +116 -979
  118. package/dist/icons/light-bulb-simple-icon/LightBulbSimpleIcon.stories.tsx +105 -1252
  119. package/dist/icons/linked-in-icon/LinkedInIcon.stories.tsx +151 -1554
  120. package/dist/icons/magic-book-icon/MagicBookIcon.stories.tsx +107 -1227
  121. package/dist/icons/magic-edit-icon/MagicEditIcon.stories.tsx +116 -707
  122. package/dist/icons/maintenance-icon/MaintenanceIcon.stories.tsx +119 -1226
  123. package/dist/icons/message-icon/MessageIcon.stories.tsx +111 -557
  124. package/dist/icons/minimize-icon/MinimizeIcon.stories.tsx +112 -1198
  125. package/dist/icons/moon-icon/MoonIcon.stories.tsx +117 -557
  126. package/dist/icons/move-horizontal-icon/MoveHorizontalIcon.stories.tsx +106 -1235
  127. package/dist/icons/move-vertical-icon/MoveVerticalIcon.stories.tsx +112 -1185
  128. package/dist/icons/musical-note-icon/MusicalNoteIcon.stories.tsx +116 -1012
  129. package/dist/icons/notepad-icon/NotepadIcon.stories.tsx +108 -1137
  130. package/dist/icons/notes-icon/NotesIcon.stories.tsx +116 -1138
  131. package/dist/icons/page-search-icon/PageSearchIcon.stories.tsx +106 -1146
  132. package/dist/icons/page-text-icon/PageTextIcon.stories.tsx +119 -719
  133. package/dist/icons/paint-roll-icon/PaintRollIcon.stories.tsx +110 -999
  134. package/dist/icons/paper-plane-icon/PaperPlaneIcon.stories.tsx +109 -912
  135. package/dist/icons/pause-icon/PauseIcon.stories.tsx +110 -1041
  136. package/dist/icons/pencil-icon/PencilIcon.stories.tsx +112 -1109
  137. package/dist/icons/phone-icon/PhoneIcon.stories.tsx +112 -1023
  138. package/dist/icons/plus-icon/PlusIcon.stories.tsx +103 -1132
  139. package/dist/icons/pocket-studio-icon/PocketStudioIcon.stories.tsx +104 -870
  140. package/dist/icons/scroll-down-icon/ScrollDownIcon.stories.tsx +99 -476
  141. package/dist/icons/search-icon/SearchIcon.stories.tsx +108 -1161
  142. package/dist/icons/setting-icon/SettingIcon.stories.tsx +104 -1009
  143. package/dist/icons/share-icon/ShareIcon.stories.tsx +117 -1064
  144. package/dist/icons/shield-icon/ShieldIcon.stories.tsx +114 -974
  145. package/dist/icons/site-logo-icon/SiteLogoIcon.stories.tsx +134 -1160
  146. package/dist/icons/skip-backward-icon/SkipBackwardIcon.stories.tsx +169 -1017
  147. package/dist/icons/skip-forward-icon/SkipForwardIcon.stories.tsx +161 -1016
  148. package/dist/icons/sparkles-soft-icon/SparklesSoftIcon.stories.tsx +102 -1001
  149. package/dist/icons/spinner-gradient-icon/SpinnerGradientIcon.stories.tsx +155 -593
  150. package/dist/icons/spinner-solid-icon/SpinnerSolidIcon.stories.tsx +155 -608
  151. package/dist/icons/spinner-solid-neutral-icon/SpinnerSolidINeutralcon.stories.tsx +142 -712
  152. package/dist/icons/star-icon/StarIcon.stories.tsx +120 -946
  153. package/dist/icons/store-coin-icon/StoreCoinIcon.stories.tsx +109 -1013
  154. package/dist/icons/suggestion-icon/SuggestionIcon.stories.tsx +113 -891
  155. package/dist/icons/sun-icon/SunIcon.stories.tsx +117 -864
  156. package/dist/icons/text-color-icon/TextColorIcon.stories.tsx +113 -989
  157. package/dist/icons/text-indicator-icon/TextIndicatorIcon.stories.tsx +120 -1027
  158. package/dist/icons/threads-icon/ThreadsIcon.stories.tsx +153 -1476
  159. package/dist/icons/tick-circle-icon/TickCircleIcon.stories.tsx +143 -1187
  160. package/dist/icons/tick-icon/TickIcon.stories.tsx +142 -1322
  161. package/dist/icons/trash-icon/TrashIcon.stories.tsx +105 -970
  162. package/dist/icons/twitter-x-icon/TwitterXIcon.stories.tsx +154 -1457
  163. package/dist/icons/upload-icon/UploadIcon.stories.tsx +112 -930
  164. package/dist/icons/vertical-menu-icon/VerticalMenuIcon.stories.tsx +115 -1019
  165. package/dist/icons/video-play-list-icon/VideoPlaylistIcon.stories.tsx +122 -1092
  166. package/dist/icons/voice-playing-icon/VoicePlayingIcon.stories.tsx +120 -1401
  167. package/dist/icons/volume-full-icon/VolumeFullIcon.stories.tsx +107 -1212
  168. package/dist/icons/volume-half-icon/VolumeHalfIcon.stories.tsx +109 -1122
  169. package/dist/icons/volume-off-icon/VolumeOffIcon.stories.tsx +112 -1124
  170. package/dist/icons/warning-icon/WarningIcon.stories.tsx +119 -1083
  171. package/dist/icons/youtube-icon/YoutubeIcon.stories.tsx +158 -983
  172. package/dist/index.cjs +1 -1
  173. package/dist/index.js +1 -1
  174. package/package.json +1 -1
@@ -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
- const MAX_DESCRIPTION_LENGTH = 160
23
+ // ─── Schema ───────────────────────────────────────────────────────────────────
21
24
 
22
- const formSchema = z.object({
23
- username: z.string().min(2, {
24
- message: "Username must be at least 2 characters.",
25
- }),
26
- email: z.string().email({
27
- message: "Please enter a valid email address.",
28
- }),
29
- bio: z.string().max(MAX_DESCRIPTION_LENGTH).optional(),
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
- const FormExample = ({
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
- function handleSubmit(values: z.infer<typeof formSchema>) {
51
- onSubmit(values)
52
- }
41
+ // ─── Meta ─────────────────────────────────────────────────────────────────────
53
42
 
54
- return (
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
- backgrounds: {
155
- default: "dark",
156
- values: [
157
- { name: "dark", value: "#0a0a0a" },
158
- { name: "light", value: "#ffffff" },
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 FormExample>
76
+ type Story = StoryObj<typeof meta>
170
77
 
171
- export const Default: Story = {}
78
+ // ─── 1. Parts ────────────────────────────────────────────────────────────────
172
79
 
173
- export const WithErrors: Story = {
174
- render: () => {
175
- const form = useForm<z.infer<typeof formSchema>>({
176
- resolver: zodResolver(formSchema),
177
- defaultValues: {
178
- username: "",
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
- useEffect(() => {
186
- form.setError("username", {
187
- type: "manual",
188
- message: "Username is already taken.",
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 &amp; 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
- form.setError("email", {
192
- type: "manual",
193
- message: "Invalid email address.",
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
- return (
198
- <Form {...form}>
199
- <form className="w-full max-w-md space-y-6">
200
- <FormField
201
- control={form.control}
202
- name="username"
203
- render={({ field }) => (
204
- <FormItem>
205
- <FormLabel>Username</FormLabel>
206
- <FormControl>
207
- <Input placeholder="username" variant="error" {...field} />
208
- </FormControl>
209
- <FormDescription>
210
- This is your public display name.
211
- </FormDescription>
212
- <FormMessage />
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
- export const Filled: Story = {
244
- render: () => {
245
- const form = useForm<z.infer<typeof formSchema>>({
246
- resolver: zodResolver(formSchema),
247
- defaultValues: {
248
- username: "johndoe",
249
- email: "john.doe@example.com",
250
- bio: "Frontend developer with 5+ years of experience",
251
- terms: true,
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
- <Form {...form}>
257
- <form className="w-full max-w-md space-y-6">
258
- <FormField
259
- control={form.control}
260
- name="username"
261
- render={({ field }) => (
262
- <FormItem>
263
- <FormLabel>Username</FormLabel>
264
- <FormControl>
265
- <Input {...field} />
266
- </FormControl>
267
- <FormDescription>
268
- This is your public display name.
269
- </FormDescription>
270
- </FormItem>
271
- )}
272
- />
273
- <FormField
274
- control={form.control}
275
- name="email"
276
- render={({ field }) => (
277
- <FormItem>
278
- <FormLabel>Email</FormLabel>
279
- <FormControl>
280
- <Input type="email" {...field} />
281
- </FormControl>
282
- <FormDescription>
283
- We'll never share your email with anyone else.
284
- </FormDescription>
285
- </FormItem>
286
- )}
287
- />
288
- <FormField
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
+ }