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