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
@@ -1,25 +1,40 @@
1
1
  import React, { useState } from "react"
2
2
  import type { Meta, StoryObj } from "@storybook/react-vite"
3
3
 
4
+ import { AuralComponentDocsPage } from "src/ui/story-spec/components/component-story-docs-page"
5
+
4
6
  import TextArea from "."
5
7
 
8
+ // ─── Meta ─────────────────────────────────────────────────────────────────────
9
+
6
10
  const meta: Meta<typeof TextArea> = {
7
11
  title: "Components/UI/TextArea",
8
12
  component: TextArea,
9
13
  parameters: {
10
14
  layout: "centered",
11
- backgrounds: {
12
- default: "dark",
13
- values: [
14
- { name: "dark", value: "#0a0a0a" },
15
- { name: "light", value: "#ffffff" },
16
- ],
17
- },
18
15
  docs: {
19
16
  description: {
20
17
  component:
21
- "A flexible textarea component built with atomic design principles. Supports multiple decoration styles (underline, outline, filled), auto-growing height, character counting, and comprehensive accessibility features. Can be used as a complete component or composed from individual atomic parts.",
18
+ "A flexible textarea built with atomic design principles. Supports three decoration styles (underline, outline, filled), four validation variants, auto-growing height, character counting, and full ARIA accessibility. Can be used as a single convenience component or composed from individual atomic parts.",
22
19
  },
20
+ page: () => (
21
+ <AuralComponentDocsPage
22
+ features={[
23
+ {
24
+ title: "4 Validation States",
25
+ description: "Default to success",
26
+ },
27
+ {
28
+ title: "Auto-growing Height",
29
+ description: "Expands with content",
30
+ },
31
+ {
32
+ title: "3 Decoration Styles",
33
+ description: "Underline, outline, filled",
34
+ },
35
+ ]}
36
+ />
37
+ ),
23
38
  },
24
39
  },
25
40
  tags: ["autodocs"],
@@ -27,704 +42,748 @@ const meta: Meta<typeof TextArea> = {
27
42
  label: {
28
43
  control: { type: "text" },
29
44
  description: "Label text for the textarea",
30
- table: {
31
- type: { summary: "ReactNode" },
32
- defaultValue: { summary: "undefined" },
33
- },
34
45
  },
35
46
  placeholder: {
36
47
  control: { type: "text" },
37
- description: "Placeholder text shown when textarea is empty",
38
- table: {
39
- type: { summary: "string" },
40
- defaultValue: { summary: '""' },
41
- },
48
+ description: "Placeholder shown when the textarea is empty",
42
49
  },
43
50
  helperText: {
44
51
  control: { type: "text" },
45
52
  description: "Helper text displayed below the textarea",
46
- table: {
47
- type: { summary: "ReactNode" },
48
- defaultValue: { summary: "undefined" },
49
- },
50
53
  },
51
54
  variant: {
52
55
  control: { type: "select" },
53
56
  options: ["default", "error", "warning", "success"],
54
- description: "Visual variant affecting border and helper text colors",
55
- table: {
56
- type: { summary: '"default" | "error" | "warning" | "success"' },
57
- defaultValue: { summary: '"default"' },
58
- },
57
+ description: "Visual validation state",
59
58
  },
60
59
  decoration: {
61
60
  control: { type: "select" },
62
61
  options: ["underline", "outline", "filled"],
63
- description: "Visual style of the textarea border and background",
64
- table: {
65
- type: { summary: '"underline" | "outline" | "filled"' },
66
- defaultValue: { summary: '"filled"' },
67
- },
62
+ description: "Border and background style",
68
63
  },
69
64
  disabled: {
70
65
  control: { type: "boolean" },
71
- description: "Whether the textarea is disabled and non-interactive",
72
- table: {
73
- type: { summary: "boolean" },
74
- defaultValue: { summary: "false" },
75
- },
66
+ description: "Disables the textarea",
76
67
  },
77
68
  required: {
78
69
  control: { type: "boolean" },
79
- description: "Whether the textarea is required (adds asterisk to label)",
80
- table: {
81
- type: { summary: "boolean" },
82
- defaultValue: { summary: "false" },
83
- },
70
+ description: "Adds asterisk to label and aria-required",
84
71
  },
85
72
  fullWidth: {
86
73
  control: { type: "boolean" },
87
- description: "Whether the textarea takes full width of its container",
88
- table: {
89
- type: { summary: "boolean" },
90
- defaultValue: { summary: "false" },
91
- },
74
+ description: "Expands the textarea to fill its container",
92
75
  },
93
76
  showCharCount: {
94
77
  control: { type: "boolean" },
95
- description: "Whether to show character count (requires maxLength)",
96
- table: {
97
- type: { summary: "boolean" },
98
- defaultValue: { summary: "false" },
99
- },
100
- },
101
- value: {
102
- control: { type: "text" },
103
- description: "Controlled value of the textarea",
104
- table: {
105
- type: { summary: "string" },
106
- defaultValue: { summary: "undefined" },
107
- },
78
+ description: "Shows remaining character count (requires maxLength)",
108
79
  },
109
80
  maxLength: {
110
81
  control: { type: "number" },
111
82
  description: "Maximum number of characters allowed",
112
- table: {
113
- type: { summary: "number" },
114
- defaultValue: { summary: "undefined" },
115
- },
116
83
  },
117
84
  rows: {
118
85
  control: { type: "number", min: 1, max: 10 },
119
- description: "Number of visible rows when autoGrow is false",
120
- table: {
121
- type: { summary: "number" },
122
- defaultValue: { summary: "3" },
123
- },
86
+ description: "Visible rows when autoGrow is false",
124
87
  },
125
88
  autoGrow: {
126
89
  control: { type: "boolean" },
127
- description:
128
- "Whether the textarea should automatically grow/shrink based on content",
129
- table: {
130
- type: { summary: "boolean" },
131
- defaultValue: { summary: "true" },
132
- },
90
+ description: "Textarea grows/shrinks automatically with content",
133
91
  },
134
92
  minHeight: {
135
93
  control: { type: "number" },
136
- description: "Minimum height in pixels (only with autoGrow)",
137
- table: {
138
- type: { summary: "number" },
139
- defaultValue: { summary: "undefined" },
140
- },
94
+ description: "Minimum height in pixels (autoGrow only)",
141
95
  },
142
96
  maxHeight: {
143
97
  control: { type: "number" },
144
- description: "Maximum height in pixels (only with autoGrow)",
145
- table: {
146
- type: { summary: "number" },
147
- defaultValue: { summary: "undefined" },
148
- },
149
- },
150
- onChange: {
151
- action: "changed",
152
- description: "Callback fired when the value changes",
153
- table: {
154
- type: {
155
- summary: "(e: React.ChangeEvent<HTMLTextAreaElement>) => void",
156
- },
157
- },
158
- },
159
- onFocus: {
160
- action: "focused",
161
- description: "Callback fired when the textarea receives focus",
162
- table: {
163
- type: { summary: "(e: React.FocusEvent<HTMLTextAreaElement>) => void" },
164
- },
165
- },
166
- onBlur: {
167
- action: "blurred",
168
- description: "Callback fired when the textarea loses focus",
169
- table: {
170
- type: { summary: "(e: React.FocusEvent<HTMLTextAreaElement>) => void" },
171
- },
172
- },
173
- unstyled: {
174
- control: { type: "boolean" },
175
- description: "Whether to remove all default styling",
176
- table: {
177
- type: { summary: "boolean" },
178
- defaultValue: { summary: "false" },
179
- },
180
- },
181
- classes: {
182
- control: { type: "object" },
183
- description: "Override classes for different parts of the component",
184
- table: {
185
- type: {
186
- summary:
187
- "{ root?: string; label?: string; wrapper?: string; textarea?: string; helperText?: string; charCount?: string }",
188
- },
189
- defaultValue: { summary: "{}" },
190
- },
98
+ description: "Maximum height in pixels (autoGrow only)",
191
99
  },
192
100
  },
193
- }
194
-
195
- export default meta
196
- type Story = StoryObj<typeof meta>
197
-
198
- // Default story
199
- export const Default: Story = {
200
101
  args: {
201
- label: "Message",
202
- placeholder: "Enter your message here...",
102
+ variant: "default",
203
103
  decoration: "filled",
204
- fullWidth: true,
104
+ disabled: false,
105
+ required: false,
106
+ fullWidth: false,
107
+ showCharCount: false,
108
+ autoGrow: true,
109
+ label: "Label",
110
+ placeholder: "Enter text…",
205
111
  },
206
112
  }
207
113
 
208
- // Decoration variants
209
- export const DecorationVariants: Story = {
210
- render: () => (
211
- <div className="max-w-lg space-y-6">
212
- <div>
213
- <h3 className="text-fm-primary mb-2 text-sm font-medium">Underline</h3>
214
- <TextArea
215
- label="Underline Style"
216
- placeholder="Minimalist underline style..."
217
- decoration="underline"
218
- fullWidth
219
- />
220
- </div>
114
+ export default meta
115
+ type Story = StoryObj<typeof meta>
221
116
 
222
- <div>
223
- <h3 className="text-fm-primary mb-2 text-sm font-medium">Outline</h3>
224
- <TextArea
225
- label="Outline Style"
226
- placeholder="Traditional outlined style..."
227
- decoration="outline"
228
- fullWidth
229
- />
230
- </div>
117
+ // ─── 1. Playground ───────────────────────────────────────────────────────────
231
118
 
232
- <div>
233
- <h3 className="text-fm-primary mb-2 text-sm font-medium">Filled</h3>
234
- <TextArea
235
- label="Filled Style"
236
- placeholder="Modern filled style..."
237
- decoration="filled"
238
- fullWidth
239
- />
119
+ export const Playground: Story = {
120
+ parameters: {
121
+ docs: {
122
+ description: {
123
+ story:
124
+ "Use the Storybook controls panel to configure every prop. The textarea below reflects your selections live.",
125
+ },
126
+ },
127
+ },
128
+ render: (args) => (
129
+ <div className="w-96 space-y-4">
130
+ <TextArea {...args} fullWidth />
131
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border px-4 py-3">
132
+ <code className="text-fm-secondary text-fm-md leading-fm-md font-(--font-fm-mono)">
133
+ {`<TextArea variant="${args.variant}" decoration="${args.decoration}"${args.disabled ? " disabled" : ""}${args.required ? " required" : ""}${args.showCharCount ? " showCharCount" : ""} />`}
134
+ </code>
240
135
  </div>
241
136
  </div>
242
137
  ),
138
+ }
139
+
140
+ // ─── 2. AllVariants ──────────────────────────────────────────────────────────
141
+
142
+ export const AllVariants: Story = {
243
143
  parameters: {
244
144
  docs: {
245
145
  description: {
246
146
  story:
247
- "Three decoration styles available: underline (minimalist), outline (traditional), and filled (modern with background).",
147
+ "Full matrix of all four validation variants (default, error, warning, success) across all three decoration styles (underline, outline, filled).",
248
148
  },
249
149
  },
250
150
  },
251
- }
151
+ render: () => {
152
+ const variants = [
153
+ {
154
+ variant: "default" as const,
155
+ label: "Default",
156
+ helperText: "Helper text",
157
+ },
158
+ {
159
+ variant: "error" as const,
160
+ label: "Error",
161
+ helperText: "Something went wrong",
162
+ },
163
+ {
164
+ variant: "warning" as const,
165
+ label: "Warning",
166
+ helperText: "Double-check this value",
167
+ },
168
+ {
169
+ variant: "success" as const,
170
+ label: "Success",
171
+ helperText: "Looks great!",
172
+ },
173
+ ]
174
+ const decorations = [
175
+ { decoration: "underline" as const, label: "Underline" },
176
+ { decoration: "outline" as const, label: "Outline" },
177
+ { decoration: "filled" as const, label: "Filled" },
178
+ ]
252
179
 
253
- // Controlled component story
254
- export const Controlled: Story = {
255
- render: (args) => {
256
- const [value, setValue] = useState("")
257
180
  return (
258
- <TextArea
259
- {...args}
260
- value={value}
261
- onChange={(e) => setValue(e.target.value)}
262
- />
181
+ <div className="space-y-10">
182
+ {decorations.map(({ decoration, label: decLabel }) => (
183
+ <div key={decoration}>
184
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
185
+ {decLabel}
186
+ </h4>
187
+ <div className="flex flex-wrap gap-6">
188
+ {variants.map(({ variant, label, helperText }) => (
189
+ <div key={variant} className="w-52 space-y-2 text-center">
190
+ <TextArea
191
+ label={label}
192
+ placeholder={`${label} state…`}
193
+ variant={variant}
194
+ decoration={decoration}
195
+ helperText={helperText}
196
+ fullWidth
197
+ />
198
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
199
+ {label}
200
+ </p>
201
+ </div>
202
+ ))}
203
+ </div>
204
+ </div>
205
+ ))}
206
+ </div>
263
207
  )
264
208
  },
265
- args: {
266
- label: "Controlled TextArea",
267
- placeholder: "Type something...",
268
- decoration: "filled",
269
- fullWidth: true,
270
- },
271
- }
272
-
273
- // With helper text
274
- export const WithHelperText: Story = {
275
- args: {
276
- label: "Description",
277
- placeholder: "Enter a description...",
278
- helperText: "Please provide a detailed description",
279
- decoration: "outline",
280
- fullWidth: true,
281
- },
282
209
  }
283
210
 
284
- // With character count
285
- export const WithCharCount: Story = {
286
- args: {
287
- label: "Comment",
288
- placeholder: "Share your thoughts...",
289
- maxLength: 280,
290
- showCharCount: true,
291
- decoration: "filled",
292
- fullWidth: true,
293
- },
294
- }
211
+ // ─── 3. Configurations ───────────────────────────────────────────────────────
295
212
 
296
- // Error state with different decorations
297
- export const ErrorStates: Story = {
298
- render: () => (
299
- <div className="max-w-lg space-y-4">
300
- <TextArea
301
- label="Underline Error"
302
- placeholder="Error with underline..."
303
- variant="error"
304
- decoration="underline"
305
- helperText="This field is required"
306
- fullWidth
307
- />
308
- <TextArea
309
- label="Outline Error"
310
- placeholder="Error with outline..."
311
- variant="error"
312
- decoration="outline"
313
- helperText="This field is required"
314
- fullWidth
315
- />
316
- <TextArea
317
- label="Filled Error"
318
- placeholder="Error with filled..."
319
- variant="error"
320
- decoration="filled"
321
- helperText="This field is required"
322
- fullWidth
323
- />
324
- </div>
325
- ),
213
+ export const Configurations: Story = {
326
214
  parameters: {
327
215
  docs: {
328
216
  description: {
329
- story: "Error state examples across all decoration variants.",
217
+ story:
218
+ "Non-variant configuration axes: character count with maxLength, fixed height with autoGrow disabled, height constraints (minHeight + maxHeight), and required field indicator.",
330
219
  },
331
220
  },
332
221
  },
333
- }
222
+ render: () => {
223
+ const [tweetValue, setTweetValue] = useState("")
224
+ const [bioValue, setBioValue] = useState("")
334
225
 
335
- // Warning state
336
- export const WarningState: Story = {
337
- args: {
338
- label: "Message",
339
- placeholder: "Enter your message...",
340
- variant: "warning",
341
- decoration: "outline",
342
- helperText: "Please double-check your input",
343
- fullWidth: true,
344
- },
345
- }
226
+ return (
227
+ <div className="w-full max-w-sm space-y-10">
228
+ {/* Character count */}
229
+ <div>
230
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
231
+ Character Count
232
+ </h4>
233
+ <div className="space-y-2 text-center">
234
+ <TextArea
235
+ label="Tweet"
236
+ placeholder="What's happening?"
237
+ value={tweetValue}
238
+ onChange={(e) => setTweetValue(e.target.value)}
239
+ maxLength={280}
240
+ showCharCount
241
+ decoration="underline"
242
+ fullWidth
243
+ />
244
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
245
+ showCharCount + maxLength={280}
246
+ </p>
247
+ </div>
248
+ </div>
346
249
 
347
- // Success state
348
- export const SuccessState: Story = {
349
- args: {
350
- label: "Message",
351
- placeholder: "Enter your message...",
352
- variant: "success",
353
- decoration: "filled",
354
- helperText: "Looks good!",
355
- fullWidth: true,
356
- },
357
- }
250
+ {/* Fixed height */}
251
+ <div>
252
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
253
+ Fixed Height
254
+ </h4>
255
+ <div className="space-y-2 text-center">
256
+ <TextArea
257
+ label="Notes"
258
+ placeholder="Type your notes here…"
259
+ autoGrow={false}
260
+ rows={5}
261
+ decoration="outline"
262
+ fullWidth
263
+ />
264
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
265
+ autoGrow=false + rows={5}
266
+ </p>
267
+ </div>
268
+ </div>
358
269
 
359
- // Disabled state
360
- export const Disabled: Story = {
361
- args: {
362
- label: "Message",
363
- placeholder: "Enter your message...",
364
- disabled: true,
365
- value: "This textarea is disabled",
366
- decoration: "filled",
367
- fullWidth: true,
368
- },
369
- }
270
+ {/* Height constraints */}
271
+ <div>
272
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
273
+ Height Constraints
274
+ </h4>
275
+ <div className="space-y-2 text-center">
276
+ <TextArea
277
+ label="Bio"
278
+ placeholder="Tell us about yourself (grows up to 200 px)…"
279
+ value={bioValue}
280
+ onChange={(e) => setBioValue(e.target.value)}
281
+ autoGrow
282
+ minHeight={60}
283
+ maxHeight={200}
284
+ decoration="filled"
285
+ fullWidth
286
+ />
287
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
288
+ autoGrow + minHeight=60 + maxHeight=200
289
+ </p>
290
+ </div>
291
+ </div>
370
292
 
371
- // Required field
372
- export const Required: Story = {
373
- args: {
374
- label: "Required Message",
375
- placeholder: "This field is required...",
376
- required: true,
377
- helperText: "This field is required",
378
- decoration: "outline",
379
- fullWidth: true,
293
+ {/* Required */}
294
+ <div>
295
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
296
+ Required
297
+ </h4>
298
+ <div className="space-y-2 text-center">
299
+ <TextArea
300
+ label="Feedback"
301
+ placeholder="Your feedback is required…"
302
+ required
303
+ helperText="This field is required"
304
+ decoration="outline"
305
+ fullWidth
306
+ />
307
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
308
+ required — asterisk added to label
309
+ </p>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ )
380
314
  },
381
315
  }
382
316
 
383
- // With auto-grow disabled
384
- export const FixedHeight: Story = {
385
- args: {
386
- label: "Fixed Height TextArea",
387
- placeholder: "This textarea has a fixed height...",
388
- autoGrow: false,
389
- rows: 5,
390
- decoration: "outline",
391
- fullWidth: true,
392
- },
393
- }
317
+ // ─── 4. States ───────────────────────────────────────────────────────────────
394
318
 
395
- // With min and max height
396
- export const WithHeightLimits: Story = {
397
- args: {
398
- label: "Height Limited TextArea",
399
- placeholder: "This textarea has height constraints...",
400
- autoGrow: true,
401
- minHeight: 80,
402
- maxHeight: 200,
403
- decoration: "filled",
404
- fullWidth: true,
319
+ export const States: Story = {
320
+ parameters: {
321
+ docs: {
322
+ description: {
323
+ story:
324
+ "All five textarea states side by side: default (idle), disabled (non-interactive with pre-filled content), error, warning, and success.",
325
+ },
326
+ },
405
327
  },
328
+ render: () => (
329
+ <div className="w-full max-w-sm space-y-8">
330
+ {[
331
+ {
332
+ label: "Default",
333
+ props: {
334
+ label: "Default",
335
+ placeholder: "Enter your message…",
336
+ variant: "default" as const,
337
+ helperText: "Helper text",
338
+ },
339
+ },
340
+ {
341
+ label: "Disabled",
342
+ props: {
343
+ label: "Disabled",
344
+ value: "This textarea cannot be edited",
345
+ variant: "default" as const,
346
+ disabled: true,
347
+ helperText: "Read-only content",
348
+ },
349
+ },
350
+ {
351
+ label: "Error",
352
+ props: {
353
+ label: "Error",
354
+ placeholder: "Enter your message…",
355
+ variant: "error" as const,
356
+ helperText: "This field is required",
357
+ },
358
+ },
359
+ {
360
+ label: "Warning",
361
+ props: {
362
+ label: "Warning",
363
+ value: "Short",
364
+ variant: "warning" as const,
365
+ helperText: "Please provide more detail",
366
+ },
367
+ },
368
+ {
369
+ label: "Success",
370
+ props: {
371
+ label: "Success",
372
+ value:
373
+ "This is a detailed and valid message that meets all requirements.",
374
+ variant: "success" as const,
375
+ helperText: "Looks great!",
376
+ },
377
+ },
378
+ ].map(({ label, props }) => (
379
+ <div key={label}>
380
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
381
+ {label}
382
+ </h4>
383
+ <TextArea decoration="outline" fullWidth {...props} />
384
+ </div>
385
+ ))}
386
+ </div>
387
+ ),
406
388
  }
407
389
 
408
- // With character limit and warning
409
- export const CharacterLimitWithWarning: Story = {
410
- render: (args) => {
411
- const [value, setValue] = useState("")
412
- const isNearLimit = value.length > 250
413
- const isOverHalfway = value.length > 140
390
+ // ─── 5. Interactive ──────────────────────────────────────────────────────────
414
391
 
415
- return (
416
- <TextArea
417
- {...args}
418
- value={value}
419
- onChange={(e) => setValue(e.target.value)}
420
- variant={
421
- isNearLimit ? "warning" : isOverHalfway ? "default" : "default"
422
- }
423
- helperText={
424
- isNearLimit
425
- ? "You're approaching the character limit"
426
- : "Share what's on your mind"
427
- }
428
- />
429
- )
430
- },
431
- args: {
432
- label: "Tweet",
433
- placeholder: "What's happening?",
434
- maxLength: 280,
435
- showCharCount: true,
436
- decoration: "underline",
437
- fullWidth: true,
392
+ export const Interactive: Story = {
393
+ parameters: {
394
+ docs: {
395
+ description: {
396
+ story:
397
+ "Live textarea demo with real-time character counting, dynamic variant changes based on content length, and preset buttons for quickly populating different content lengths. The variant and helper text update as you type.",
398
+ },
399
+ },
438
400
  },
439
- }
440
-
441
- // Atomic composition story - demonstrates how to use individual components
442
- export const AtomicComposition: Story = {
443
- render: () => {
401
+ render: function InteractiveTextArea() {
444
402
  const [value, setValue] = useState("")
445
- const maxLength = 100
403
+ const [decoration, setDecoration] = useState<
404
+ "underline" | "outline" | "filled"
405
+ >("outline")
406
+ const maxLength = 280
407
+
408
+ const variant =
409
+ value.length === 0
410
+ ? "default"
411
+ : value.length > 240
412
+ ? "warning"
413
+ : value.length > 10
414
+ ? "success"
415
+ : "default"
416
+
417
+ const helperText =
418
+ value.length === 0
419
+ ? "Start typing…"
420
+ : value.length > 240
421
+ ? "Approaching the character limit"
422
+ : "Looks good!"
423
+
424
+ const presets = [
425
+ { label: "Short", text: "Hello world." },
426
+ {
427
+ label: "Medium",
428
+ text: "This is a medium-length message that provides some context and detail about the topic at hand.",
429
+ },
430
+ {
431
+ label: "Long",
432
+ text: "This is a longer message that demonstrates what happens when content approaches the character limit. Keep writing to see the warning state activate as you get closer to 280 characters. Almost there now — just a few more words should do it to trigger the warning.",
433
+ },
434
+ { label: "Clear", text: "" },
435
+ ]
446
436
 
447
437
  return (
448
- <TextArea.Root fullWidth className="max-w-md">
449
- <TextArea.Label htmlFor="atomic-textarea" required>
450
- Custom Composed TextArea
451
- </TextArea.Label>
452
-
453
- <TextArea.Wrapper>
454
- <TextArea.Base
455
- id="atomic-textarea"
456
- placeholder="Built with atomic components..."
457
- value={value}
458
- onChange={(e) => setValue(e.target.value)}
459
- maxLength={maxLength}
460
- variant="default"
461
- decoration="outline"
462
- />
463
- </TextArea.Wrapper>
464
-
465
- <div className="flex items-center justify-between gap-2">
466
- <TextArea.HelperText variant="default">
467
- This is built using atomic components
468
- </TextArea.HelperText>
469
-
470
- <TextArea.CharCount
471
- currentLength={value.length}
472
- maxLength={maxLength}
473
- />
438
+ <div className="w-full p-8">
439
+ <div className="mx-auto max-w-3xl space-y-6">
440
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
441
+ {/* Controls panel */}
442
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary space-y-5 rounded-xl border p-5">
443
+ <p className="text-fm-primary font-fm-brand text-fm-sm leading-fm-sm font-semibold tracking-widest uppercase">
444
+ Presets
445
+ </p>
446
+ <div className="space-y-2">
447
+ {presets.map(({ label, text }) => (
448
+ <button
449
+ key={label}
450
+ onClick={() => setValue(text)}
451
+ className="border-fm-divider-secondary text-fm-primary font-fm-text text-fm-sm leading-fm-sm hover:bg-fm-surface-primary w-full rounded-lg border px-3 py-2 text-left transition-colors"
452
+ >
453
+ {label}
454
+ </button>
455
+ ))}
456
+ </div>
457
+ <div className="border-fm-divider-secondary border-t pt-4" />
458
+ <p className="text-fm-primary font-fm-brand text-fm-sm leading-fm-sm font-semibold tracking-widest uppercase">
459
+ Decoration
460
+ </p>
461
+ <div className="space-y-2">
462
+ {(["underline", "outline", "filled"] as const).map((d) => (
463
+ <button
464
+ key={d}
465
+ onClick={() => setDecoration(d)}
466
+ className={`border-fm-divider-secondary text-fm-primary font-fm-text text-fm-sm leading-fm-sm w-full rounded-lg border px-3 py-2 text-left capitalize transition-colors ${decoration === d ? "bg-fm-surface-primary" : "hover:bg-fm-surface-primary"}`}
467
+ >
468
+ {d}
469
+ </button>
470
+ ))}
471
+ </div>
472
+ <div className="border-fm-divider-secondary border-t pt-4" />
473
+ <div className="border-fm-divider-secondary bg-fm-surface-primary rounded-lg border p-3">
474
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-1 font-medium">
475
+ Characters
476
+ </p>
477
+ <p className="text-fm-primary text-fm-sm leading-fm-sm font-(--font-fm-mono)">
478
+ {value.length} / {maxLength}
479
+ </p>
480
+ </div>
481
+ </div>
482
+
483
+ {/* Preview stage */}
484
+ <div className="flex flex-col gap-3 lg:col-span-2">
485
+ <TextArea
486
+ label="Message"
487
+ placeholder="Start typing to see real-time validation…"
488
+ value={value}
489
+ onChange={(e) => setValue(e.target.value)}
490
+ variant={variant as "default" | "warning" | "success"}
491
+ helperText={helperText}
492
+ decoration={decoration}
493
+ maxLength={maxLength}
494
+ showCharCount
495
+ autoGrow
496
+ minHeight={80}
497
+ fullWidth
498
+ />
499
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border px-4 py-3">
500
+ <code className="text-fm-secondary text-fm-md leading-fm-md font-(--font-fm-mono)">
501
+ {`variant="${variant}" | ${value.length}/${maxLength} chars`}
502
+ </code>
503
+ </div>
504
+ </div>
505
+ </div>
474
506
  </div>
475
- </TextArea.Root>
507
+ </div>
476
508
  )
477
509
  },
510
+ }
511
+
512
+ // ─── 6. Parts ────────────────────────────────────────────────────────────────
513
+
514
+ export const Parts: Story = {
478
515
  parameters: {
479
516
  docs: {
480
517
  description: {
481
518
  story:
482
- "This example shows how to compose the TextArea using individual atomic components for maximum flexibility. This pattern allows for custom layouts and advanced use cases.",
519
+ "Each atomic sub-component shown individually: TextArea (convenience wrapper), and the atomic TextArea.Root + TextArea.Label + TextArea.Wrapper + TextArea.Base + TextArea.HelperText + TextArea.CharCount composition for full layout control.",
483
520
  },
484
521
  },
485
522
  },
486
- }
487
-
488
- // Complex form example
489
- export const ComplexFormExample: Story = {
490
523
  render: () => {
491
- const [feedback, setFeedback] = useState("")
492
- const [description, setDescription] = useState("")
524
+ const [atomicValue, setAtomicValue] = useState("")
525
+ const maxLength = 100
493
526
 
494
527
  return (
495
- <div className="max-w-lg space-y-6">
496
- <TextArea
497
- label="Product Description"
498
- placeholder="Describe your product..."
499
- value={description}
500
- onChange={(e) => setDescription(e.target.value)}
501
- helperText="Provide a clear and concise description"
502
- maxLength={500}
503
- showCharCount
504
- decoration="outline"
505
- fullWidth
506
- />
528
+ <div className="w-full max-w-lg space-y-10">
529
+ {/* Convenience component */}
530
+ <div>
531
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
532
+ TextArea — convenience wrapper
533
+ </h4>
534
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
535
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-3">
536
+ The default export combines all atomic parts into a single
537
+ component with label, textarea, helper text, and optional
538
+ character count.
539
+ </p>
540
+ <TextArea
541
+ label="Description"
542
+ placeholder="Tell us more…"
543
+ helperText="Provide a clear description"
544
+ maxLength={200}
545
+ showCharCount
546
+ decoration="outline"
547
+ fullWidth
548
+ />
549
+ </div>
550
+ </div>
507
551
 
508
- <TextArea
509
- label="Customer Feedback"
510
- placeholder="Share your experience..."
511
- value={feedback}
512
- onChange={(e) => setFeedback(e.target.value)}
513
- variant={feedback.length > 10 ? "success" : "default"}
514
- helperText={
515
- feedback.length > 10
516
- ? "Thank you for the detailed feedback!"
517
- : "Please provide detailed feedback"
518
- }
519
- required
520
- autoGrow
521
- minHeight={60}
522
- maxHeight={150}
523
- decoration="filled"
524
- fullWidth
525
- />
552
+ {/* Atomic composition */}
553
+ <div>
554
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-4 font-medium">
555
+ Atomic Composition
556
+ </h4>
557
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
558
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-3">
559
+ Build the same result using individual primitives for full control
560
+ over layout and positioning.
561
+ </p>
562
+ <TextArea.Root fullWidth>
563
+ <TextArea.Label htmlFor="atomic-textarea" required>
564
+ Custom Composed TextArea
565
+ </TextArea.Label>
566
+ <TextArea.Wrapper>
567
+ <TextArea.Base
568
+ id="atomic-textarea"
569
+ placeholder="Built with atomic components…"
570
+ value={atomicValue}
571
+ onChange={(e) => setAtomicValue(e.target.value)}
572
+ maxLength={maxLength}
573
+ variant="default"
574
+ decoration="outline"
575
+ />
576
+ </TextArea.Wrapper>
577
+ <div className="flex items-center justify-between gap-2">
578
+ <TextArea.HelperText variant="default">
579
+ Full control via atomic parts
580
+ </TextArea.HelperText>
581
+ <TextArea.CharCount
582
+ currentLength={atomicValue.length}
583
+ maxLength={maxLength}
584
+ />
585
+ </div>
586
+ </TextArea.Root>
587
+ </div>
588
+ </div>
526
589
  </div>
527
590
  )
528
591
  },
592
+ }
593
+
594
+ // ─── 7. UseCases ─────────────────────────────────────────────────────────────
595
+
596
+ export const UseCases: Story = {
529
597
  parameters: {
530
598
  docs: {
531
599
  description: {
532
600
  story:
533
- "An example showing multiple TextArea components in a form with different configurations, decorations, and dynamic states based on user input.",
601
+ "Real product-shaped examples: a social post composer with character counter and dynamic warnings, a support ticket form with required feedback, and a profile bio editor with success validation.",
534
602
  },
535
603
  },
536
604
  },
537
- }
605
+ render: function UseCasesTextArea() {
606
+ const [post, setPost] = useState("")
607
+ const [ticket, setTicket] = useState("")
608
+ const [bio, setBio] = useState("")
609
+
610
+ const postVariant =
611
+ post.length === 0 ? "default" : post.length > 240 ? "warning" : "success"
612
+ const postHelper =
613
+ post.length > 240
614
+ ? "Approaching the 280-character limit"
615
+ : post.length > 0
616
+ ? "Ready to post"
617
+ : "What's on your mind?"
618
+
619
+ const bioVariant =
620
+ bio.length > 10 ? "success" : bio.length > 0 ? "default" : "default"
621
+ const bioHelper =
622
+ bio.length > 10 ? "Looking good!" : "Tell listeners a bit about yourself"
538
623
 
539
- // Sizing variations with different decorations
540
- export const SizingVariations: Story = {
541
- render: () => (
542
- <div className="text-fm-primary space-y-4">
543
- <div>
544
- <h3 className="mb-2 text-sm font-medium">Small (3 rows) - Underline</h3>
545
- <TextArea
546
- placeholder="Small textarea..."
547
- rows={3}
548
- autoGrow={false}
549
- decoration="underline"
550
- className="max-w-sm"
551
- />
552
- </div>
624
+ return (
625
+ <div className="mx-auto max-w-3xl space-y-8 p-8">
626
+ {/* Social post composer */}
627
+ <div className="space-y-4">
628
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md font-medium">
629
+ Post Composer
630
+ </h4>
631
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary max-w-md rounded-xl border p-5">
632
+ <TextArea
633
+ label="New Post"
634
+ placeholder="What's happening?"
635
+ value={post}
636
+ onChange={(e) => setPost(e.target.value)}
637
+ variant={postVariant}
638
+ helperText={postHelper}
639
+ maxLength={280}
640
+ showCharCount
641
+ decoration="underline"
642
+ fullWidth
643
+ />
644
+ </div>
645
+ </div>
553
646
 
554
- <div>
555
- <h3 className="mb-2 text-sm font-medium">Medium (5 rows) - Outline</h3>
556
- <TextArea
557
- placeholder="Medium textarea..."
558
- rows={5}
559
- autoGrow={false}
560
- decoration="outline"
561
- className="max-w-md"
562
- />
563
- </div>
647
+ {/* Support ticket */}
648
+ <div className="space-y-4">
649
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md font-medium">
650
+ Support Ticket
651
+ </h4>
652
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary max-w-md rounded-xl border p-5">
653
+ <TextArea
654
+ label="Describe your issue"
655
+ placeholder="Please describe what happened in as much detail as possible…"
656
+ value={ticket}
657
+ onChange={(e) => setTicket(e.target.value)}
658
+ variant={
659
+ ticket.length > 20
660
+ ? "success"
661
+ : ticket.length > 0
662
+ ? "default"
663
+ : "default"
664
+ }
665
+ helperText={
666
+ ticket.length > 20
667
+ ? "Thank you — our team will review this"
668
+ : "Required — minimum 20 characters for a useful report"
669
+ }
670
+ required
671
+ autoGrow
672
+ minHeight={80}
673
+ maxHeight={240}
674
+ decoration="outline"
675
+ fullWidth
676
+ />
677
+ </div>
678
+ </div>
564
679
 
565
- <div>
566
- <h3 className="mb-2 text-sm font-medium">Large (8 rows) - Filled</h3>
567
- <TextArea
568
- placeholder="Large textarea..."
569
- rows={8}
570
- autoGrow={false}
571
- decoration="filled"
572
- className="max-w-lg"
573
- />
680
+ {/* Profile bio */}
681
+ <div className="space-y-4">
682
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md font-medium">
683
+ Profile Bio Editor
684
+ </h4>
685
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary max-w-md rounded-xl border p-5">
686
+ <TextArea
687
+ label="Bio"
688
+ placeholder="I'm a podcast creator who loves…"
689
+ value={bio}
690
+ onChange={(e) => setBio(e.target.value)}
691
+ variant={bioVariant}
692
+ helperText={bioHelper}
693
+ maxLength={160}
694
+ showCharCount
695
+ decoration="filled"
696
+ fullWidth
697
+ />
698
+ </div>
699
+ </div>
574
700
  </div>
575
- </div>
576
- ),
701
+ )
702
+ },
703
+ }
704
+
705
+ // ─── 8. Accessibility ────────────────────────────────────────────────────────
706
+
707
+ export const Accessibility: Story = {
577
708
  parameters: {
578
709
  docs: {
579
710
  description: {
580
711
  story:
581
- "Different sizing options for the TextArea component with fixed heights, showcasing different decoration styles.",
712
+ "Accessibility-focused examples covering ARIA attributes, required field announcements, error state with aria-invalid, and the character count linked via aria-describedby. All interactive elements are keyboard-reachable and screen-reader friendly.",
582
713
  },
583
714
  },
584
715
  },
585
- }
586
-
587
- // All variants with all decorations
588
- export const AllVariantsAllDecorations: Story = {
589
716
  render: () => (
590
- <div className="grid max-w-6xl grid-cols-1 gap-6 lg:grid-cols-3">
591
- {/* Underline decoration */}
592
- <div className="space-y-4">
593
- <h3 className="text-fm-primary text-lg font-semibold">Underline</h3>
594
- <TextArea
595
- label="Default"
596
- placeholder="Default underline..."
597
- variant="default"
598
- decoration="underline"
599
- helperText="Default variant"
600
- fullWidth
601
- />
602
- <TextArea
603
- label="Error"
604
- placeholder="Error underline..."
605
- variant="error"
606
- decoration="underline"
607
- helperText="Something went wrong"
608
- fullWidth
609
- />
610
- <TextArea
611
- label="Warning"
612
- placeholder="Warning underline..."
613
- variant="warning"
614
- decoration="underline"
615
- helperText="Please check input"
616
- fullWidth
617
- />
717
+ <div className="w-full max-w-lg space-y-8">
718
+ {/* Required */}
719
+ <div>
720
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
721
+ Required Field
722
+ </h4>
618
723
  <TextArea
619
- label="Success"
620
- placeholder="Success underline..."
621
- variant="success"
622
- decoration="underline"
623
- helperText="Looks good!"
724
+ label="Feedback"
725
+ placeholder="Your feedback is required…"
726
+ required
727
+ helperText="Required — helps us improve the product"
728
+ decoration="outline"
624
729
  fullWidth
625
730
  />
626
731
  </div>
627
732
 
628
- {/* Outline decoration */}
629
- <div className="space-y-4">
630
- <h3 className="text-fm-primary text-lg font-semibold">Outline</h3>
631
- <TextArea
632
- label="Default"
633
- placeholder="Default outline..."
634
- variant="default"
635
- decoration="outline"
636
- helperText="Default variant"
637
- fullWidth
638
- />
733
+ {/* Error + aria-invalid */}
734
+ <div>
735
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
736
+ Error State (aria-invalid)
737
+ </h4>
639
738
  <TextArea
640
- label="Error"
641
- placeholder="Error outline..."
739
+ label="Description"
740
+ placeholder="Enter a description…"
642
741
  variant="error"
742
+ helperText="This field cannot be empty"
643
743
  decoration="outline"
644
- helperText="Something went wrong"
645
- fullWidth
646
- />
647
- <TextArea
648
- label="Warning"
649
- placeholder="Warning outline..."
650
- variant="warning"
651
- decoration="outline"
652
- helperText="Please check input"
653
744
  fullWidth
654
745
  />
746
+ </div>
747
+
748
+ {/* Character count + aria-describedby */}
749
+ <div>
750
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
751
+ Character Count (aria-describedby)
752
+ </h4>
655
753
  <TextArea
656
- label="Success"
657
- placeholder="Success outline..."
658
- variant="success"
754
+ label="Short Bio"
755
+ placeholder="A brief introduction…"
756
+ maxLength={160}
757
+ showCharCount
758
+ helperText="The character count is announced to screen readers via aria-describedby"
659
759
  decoration="outline"
660
- helperText="Looks good!"
661
760
  fullWidth
662
761
  />
663
762
  </div>
664
763
 
665
- {/* Filled decoration */}
666
- <div className="space-y-4">
667
- <h3 className="text-fm-primary text-lg font-semibold">Filled</h3>
668
- <TextArea
669
- label="Default"
670
- placeholder="Default filled..."
671
- variant="default"
672
- decoration="filled"
673
- helperText="Default variant"
674
- fullWidth
675
- />
676
- <TextArea
677
- label="Error"
678
- placeholder="Error filled..."
679
- variant="error"
680
- decoration="filled"
681
- helperText="Something went wrong"
682
- fullWidth
683
- />
684
- <TextArea
685
- label="Warning"
686
- placeholder="Warning filled..."
687
- variant="warning"
688
- decoration="filled"
689
- helperText="Please check input"
690
- fullWidth
691
- />
692
- <TextArea
693
- label="Success"
694
- placeholder="Success filled..."
695
- variant="success"
696
- decoration="filled"
697
- helperText="Looks good!"
698
- fullWidth
699
- />
764
+ {/* Accessibility best practices */}
765
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
766
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mb-2 font-medium">
767
+ Accessibility Best Practices
768
+ </p>
769
+ <ul className="space-y-1">
770
+ {[
771
+ "Always provide a label — the Label is linked via htmlFor/id",
772
+ "Use helperText to add context; it is linked via aria-describedby",
773
+ "The error variant sets aria-invalid automatically",
774
+ "required adds aria-required and an asterisk to the label",
775
+ "The character count element is also linked via aria-describedby",
776
+ "Tab moves focus into the textarea; Shift+Tab moves out",
777
+ ].map((item) => (
778
+ <li
779
+ key={item}
780
+ className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm"
781
+ >
782
+ {item}
783
+ </li>
784
+ ))}
785
+ </ul>
700
786
  </div>
701
787
  </div>
702
788
  ),
703
- parameters: {
704
- docs: {
705
- description: {
706
- story:
707
- "Complete showcase of all visual variants across all decoration styles.",
708
- },
709
- },
710
- },
711
- }
712
-
713
- // Unstyled example
714
- export const Unstyled: Story = {
715
- args: {
716
- placeholder: "Completely unstyled textarea...",
717
- unstyled: true,
718
- className:
719
- "border-2 border-dashed border-fm-divider-secondary p-4 rounded-lg bg-fm-surface-secondary",
720
- fullWidth: true,
721
- },
722
- parameters: {
723
- docs: {
724
- description: {
725
- story:
726
- "Example of using the unstyled prop to create completely custom styling.",
727
- },
728
- },
729
- },
730
789
  }