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,694 +1,419 @@
1
- import React, { useState } from "react"
2
- import { Button } from "@components/button"
1
+ import React, { useEffect, useRef, useState } from "react"
3
2
  import type { Meta, StoryObj } from "@storybook/react-vite"
4
3
 
4
+ import { AuralComponentDocsPage } from "src/ui/story-spec/components/component-story-docs-page"
5
+
5
6
  import Search, { SearchResult } from "."
6
7
 
8
+ // ─── Meta ─────────────────────────────────────────────────────────────────────
9
+
7
10
  const meta: Meta<typeof Search> = {
8
11
  title: "Components/UI/Search",
9
12
  component: Search,
10
13
  parameters: {
11
14
  layout: "centered",
12
- backgrounds: {
13
- default: "dark",
14
- values: [
15
- { name: "dark", value: "#0a0a0a" },
16
- { name: "light", value: "#ffffff" },
17
- ],
18
- },
19
15
  docs: {
20
16
  description: {
21
- component: `
22
- # Search Component
23
-
24
- A comprehensive search input component with both controlled and uncontrolled modes, built with dark theme optimization and flexible content rendering.
25
-
26
- ## Features
27
-
28
- - **Dual Mode Support**: Works as both controlled and uncontrolled component
29
- - **Search Icon**: Built-in search icon with proper theming
30
- - **Clear Functionality**: X button to clear search input
31
- - **Flexible Results**: Custom children rendering for search results
32
- - **Dark Theme Optimized**: Default styling for dark interfaces
33
- - **Accessibility**: Full keyboard navigation and screen reader support
34
- - **Event Handling**: Separate onChange and onSearch callbacks
35
- - **Input Ref Access**: Easily focus or manage the input programmatically
36
-
37
- ## Component Modes
38
-
39
- ### Controlled Component
40
- Use \`value\`, \`onChange\`, and \`onSearch\` for external state management:
41
-
42
- \`\`\`tsx
43
- const [searchValue, setSearchValue] = useState("")
44
- const inputRef = useRef<HTMLInputElement>(null)
45
-
46
- <Search
47
- value={searchValue}
48
- onChange={setSearchValue}
49
- onSearch={(query) => handleSearch(query)}
50
- inputRef={inputRef}
51
- />
52
- \`\`\`
53
-
54
- ### Uncontrolled Component
55
- Use \`initialValue\` and \`onSearch\` for internal state management:
56
-
57
- \`\`\`tsx
58
- const inputRef = useRef<HTMLInputElement>(null)
59
-
60
- <Search
61
- initialValue="initial search"
62
- onSearch={(query) => handleSearch(query)}
63
- inputRef={inputRef}
64
- />
65
- \`\`\`
66
-
67
- ## Props Overview
68
-
69
- - **value**: Current value (controlled mode)
70
- - **onChange**: Value change handler (controlled mode)
71
- - **initialValue**: Initial value (uncontrolled mode)
72
- - **onSearch**: Search handler (both modes)
73
- - **results**: Array of search results
74
- - **children**: Custom content for rendering results
75
- - **placeholder**: Input placeholder text
76
- - **className**: Additional CSS classes
77
- - **inputRef**: Ref to access or focus the underlying input element
78
- `,
17
+ component:
18
+ "A search input component with built-in search and clear icons, controlled and uncontrolled modes, and flexible children-based results rendering. Supports placeholder customisation, autoFocus, onChange/onSearch callbacks, and programmatic input access via inputRef.",
79
19
  },
20
+ page: () => (
21
+ <AuralComponentDocsPage
22
+ features={[
23
+ {
24
+ title: "Search & Clear Icons",
25
+ description: "Built-in icon slots",
26
+ },
27
+ {
28
+ title: "Controlled Mode",
29
+ description: "onChange and onSearch",
30
+ },
31
+ {
32
+ title: "Results Slot",
33
+ description: "Children-based results",
34
+ },
35
+ ]}
36
+ />
37
+ ),
80
38
  },
81
39
  },
82
40
  tags: ["autodocs"],
41
+ argTypes: {
42
+ placeholder: { control: { type: "text" } },
43
+ autoFocus: { control: { type: "boolean" } },
44
+ value: { control: { type: "text" } },
45
+ },
46
+ args: {
47
+ placeholder: "Search episodes",
48
+ autoFocus: false,
49
+ },
83
50
  }
84
51
 
85
52
  export default meta
86
53
  type Story = StoryObj<typeof Search>
87
54
 
88
- // 1. Basic Search Component Examples
89
- export const BasicSearch: Story = {
90
- render: () => (
91
- <div className="space-y-6">
92
- <div className="text-center">
93
- <h3 className="text-fm-primary mb-2 font-medium">
94
- Basic Search Component
95
- </h3>
96
- <p className="text-fm-secondary text-sm">
97
- Simple search input with different placeholder examples
98
- </p>
99
- </div>
100
-
101
- <div className="space-y-4">
102
- {/* Default Placeholder */}
103
- <div className="space-y-2">
104
- <label className="text-fm-tertiary text-sm font-medium">
105
- Default
106
- </label>
107
- <Search placeholder="Search episodes" />
108
- </div>
109
-
110
- {/* Custom Placeholders */}
111
- <div className="space-y-2">
112
- <label className="text-fm-tertiary text-sm font-medium">
113
- Custom Placeholders
114
- </label>
115
- <div className="space-y-3">
116
- <Search placeholder="Search podcasts..." />
117
- <Search placeholder="Find your favorite shows" />
118
- <Search placeholder="Type to search music" />
119
- </div>
120
- </div>
121
-
122
- {/* With Initial Value */}
123
- <div className="space-y-2">
124
- <label className="text-fm-tertiary text-sm font-medium">
125
- With Initial Value
126
- </label>
127
- <Search placeholder="Search episodes" initialValue="The Daily" />
128
- </div>
129
- </div>
130
- </div>
131
- ),
55
+ // ─── Mock data ────────────────────────────────────────────────────────────────
56
+
57
+ const ALL_PODCASTS = [
58
+ { id: "1", text: "The Daily" },
59
+ { id: "2", text: "Serial" },
60
+ { id: "3", text: "This American Life" },
61
+ { id: "4", text: "Stuff You Should Know" },
62
+ { id: "5", text: "Crime Junkie" },
63
+ { id: "6", text: "Conan O'Brien Needs a Friend" },
64
+ { id: "7", text: "The Tim Ferriss Show" },
65
+ { id: "8", text: "My Favorite Murder" },
66
+ { id: "9", text: "Hidden Brain" },
67
+ { id: "10", text: "Radiolab" },
68
+ { id: "11", text: "How I Built This" },
69
+ { id: "12", text: "Fresh Air" },
70
+ ]
71
+
72
+ // ─── 1. Playground ────────────────────────────────────────────────────────────
73
+
74
+ export const Playground: Story = {
132
75
  parameters: {
133
76
  docs: {
134
77
  description: {
135
78
  story:
136
- "Basic search component examples with different placeholder text and initial values.",
79
+ "Controls-driven story. Use the Storybook sidebar to adjust placeholder, autoFocus, and value. The search input renders with the current args applied.",
137
80
  },
138
81
  },
139
82
  },
83
+ render: (args) => (
84
+ <div className="w-80">
85
+ <Search {...args} />
86
+ </div>
87
+ ),
140
88
  }
141
89
 
142
- // 2. Controlled Component Examples
143
- export const ControlledSearch: Story = {
144
- render: () => {
145
- const [searchValue1, setSearchValue1] = useState("")
146
- const [searchValue2, setSearchValue2] = useState("Controlled")
147
- const [searchValue3, setSearchValue3] = useState("")
148
-
149
- return (
150
- <div className="space-y-6">
151
- <div className="text-center">
152
- <h3 className="text-fm-primary mb-2 font-medium">
153
- Controlled Search
154
- </h3>
155
- <p className="text-fm-secondary text-sm">
156
- Search components with external state control
157
- </p>
158
- </div>
159
-
160
- <div className="space-y-6">
161
- {/* Basic Controlled */}
162
- <div className="space-y-3">
163
- <div className="flex items-center justify-between">
164
- <label className="text-fm-tertiary text-sm font-medium">
165
- Basic Controlled
166
- </label>
167
- <div className="text-fm-secondary text-xs">
168
- Value: "{searchValue1}"
169
- </div>
170
- </div>
171
- <Search
172
- placeholder="Type something..."
173
- value={searchValue1}
174
- onChange={setSearchValue1}
175
- />
176
- <div className="flex gap-2">
177
- <Button
178
- size="sm"
179
- variant="outline"
180
- onClick={() => setSearchValue1("Preset Value")}
181
- >
182
- Set Value
183
- </Button>
184
- <Button
185
- size="sm"
186
- variant="outline"
187
- onClick={() => setSearchValue1("")}
188
- >
189
- Clear
190
- </Button>
191
- </div>
192
- </div>
193
-
194
- {/* Pre-filled Controlled */}
195
- <div className="space-y-3">
196
- <div className="flex items-center justify-between">
197
- <label className="text-fm-tertiary text-sm font-medium">
198
- Pre-filled Controlled
199
- </label>
200
- <div className="text-fm-secondary text-xs">
201
- Value: "{searchValue2}"
202
- </div>
203
- </div>
204
- <Search
205
- placeholder="Search with preset value"
206
- value={searchValue2}
207
- onChange={setSearchValue2}
208
- />
209
- </div>
90
+ // ─── 2. States ────────────────────────────────────────────────────────────────
210
91
 
211
- {/* Controlled with Validation */}
212
- <div className="space-y-3">
213
- <div className="flex items-center justify-between">
214
- <label className="text-fm-tertiary text-sm font-medium">
215
- With Validation
216
- </label>
217
- <div className="text-fm-secondary text-xs">
218
- Length: {searchValue3.length}/20
219
- </div>
220
- </div>
221
- <Search
222
- placeholder="Max 20 characters"
223
- value={searchValue3}
224
- onChange={(value) => {
225
- if (value.length <= 20) {
226
- setSearchValue3(value)
227
- }
228
- }}
229
- />
230
- {searchValue3.length >= 20 && (
231
- <p className="text-fm-negative text-xs">Maximum length reached</p>
232
- )}
233
- </div>
234
- </div>
235
- </div>
236
- )
237
- },
92
+ export const States: Story = {
238
93
  parameters: {
239
94
  docs: {
240
95
  description: {
241
96
  story:
242
- "Controlled search component examples showing external state management, pre-filled values, and validation.",
97
+ "Five distinct states: empty (idle), has query (clear button visible), loading (async in-progress), no results (empty results set), and with results (list rendered below).",
243
98
  },
244
99
  },
245
100
  },
246
- }
247
-
248
- // 3. Uncontrolled Component Examples
249
- export const UncontrolledSearch: Story = {
250
101
  render: () => {
251
- const [lastSearch1, setLastSearch1] = useState("")
252
- const [lastSearch2, setLastSearch2] = useState("")
102
+ const [loadingQuery, setLoadingQuery] = useState("")
103
+ const [noResultsQuery, setNoResultsQuery] = useState("nonexistentshow")
104
+ const [resultsQuery, setResultsQuery] = useState("The")
253
105
 
254
- return (
255
- <div className="space-y-6">
256
- <div className="text-center">
257
- <h3 className="text-fm-primary mb-2 font-medium">
258
- Uncontrolled Search
259
- </h3>
260
- <p className="text-fm-secondary text-sm">
261
- Search components with internal state management
262
- </p>
263
- </div>
106
+ const filteredResults: SearchResult[] = resultsQuery
107
+ ? ALL_PODCASTS.filter((p) =>
108
+ p.text.toLowerCase().includes(resultsQuery.toLowerCase())
109
+ )
110
+ : []
264
111
 
265
- <div className="space-y-6">
266
- {/* Basic Uncontrolled */}
267
- <div className="space-y-3">
268
- <div className="flex items-center justify-between">
269
- <label className="text-fm-tertiary text-sm font-medium">
270
- Basic Uncontrolled
271
- </label>
272
- <div className="text-fm-secondary text-xs">
273
- Last search: "{lastSearch1}"
274
- </div>
275
- </div>
276
- <Search
277
- placeholder="Search internally managed"
278
- onSearch={setLastSearch1}
279
- />
280
- </div>
281
-
282
- {/* With Initial Value */}
283
- <div className="space-y-3">
284
- <div className="flex items-center justify-between">
285
- <label className="text-fm-tertiary text-sm font-medium">
286
- With Initial Value
287
- </label>
288
- <div className="text-fm-secondary text-xs">
289
- Last search: "{lastSearch2}"
290
- </div>
291
- </div>
292
- <Search
293
- placeholder="Search with initial value"
294
- initialValue="Initial Search"
295
- onSearch={setLastSearch2}
296
- />
297
- </div>
298
-
299
- {/* Multiple Independent */}
300
- <div className="space-y-3">
301
- <label className="text-fm-tertiary text-sm font-medium">
302
- Multiple Independent
303
- </label>
304
- <div className="grid grid-cols-2 gap-3">
305
- <Search placeholder="Search A" />
306
- <Search placeholder="Search B" />
307
- </div>
308
- <p className="text-fm-secondary text-xs">
309
- Each search maintains its own independent state
112
+ return (
113
+ <div className="w-80 space-y-8">
114
+ {/* Empty */}
115
+ <div>
116
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
117
+ Empty
118
+ </h4>
119
+ <div className="space-y-2 text-center">
120
+ <Search placeholder="Search episodes" />
121
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
122
+ Idle — no input yet
310
123
  </p>
311
124
  </div>
312
125
  </div>
313
- </div>
314
- )
315
- },
316
- parameters: {
317
- docs: {
318
- description: {
319
- story:
320
- "Uncontrolled search component examples with internal state management and initial values.",
321
- },
322
- },
323
- },
324
- }
325
-
326
- // 4. Interactive Search with Results
327
- export const InteractiveSearch: Story = {
328
- render: () => {
329
- const [query, setQuery] = useState("")
330
- const [results, setResults] = useState<SearchResult[]>([])
331
- const [selectedResult, setSelectedResult] = useState<string | null>(null)
332
-
333
- // Mock search data
334
- const allPodcasts = [
335
- { id: "1", text: "The Joe Rogan Experience" },
336
- { id: "2", text: "Serial" },
337
- { id: "3", text: "This American Life" },
338
- { id: "4", text: "Stuff You Should Know" },
339
- { id: "5", text: "The Daily" },
340
- { id: "6", text: "Crime Junkie" },
341
- { id: "7", text: "The Michelle Obama Podcast" },
342
- { id: "8", text: "Call Her Daddy" },
343
- { id: "9", text: "My Favorite Murder" },
344
- { id: "10", text: "The Tim Ferriss Show" },
345
- { id: "11", text: "Conan O'Brien Needs a Friend" },
346
- { id: "12", text: "The Ben Shapiro Show" },
347
- ]
348
-
349
- const handleSearch = (searchQuery: string) => {
350
- setQuery(searchQuery)
351
-
352
- if (!searchQuery.trim()) {
353
- setResults([])
354
- return
355
- }
356
-
357
- // Filter podcasts based on query
358
- const filteredResults = allPodcasts.filter((podcast) =>
359
- podcast.text.toLowerCase().includes(searchQuery.toLowerCase())
360
- )
361
-
362
- setResults(filteredResults)
363
- }
364
-
365
- return (
366
- <div className="w-96 space-y-4">
367
- <div className="text-center">
368
- <h3 className="text-fm-primary mb-2 font-medium">
369
- Interactive Search
370
- </h3>
371
- <p className="text-fm-secondary text-sm">
372
- Real-time search with custom results rendering
373
- </p>
374
- </div>
375
126
 
376
- <Search
377
- placeholder="Search podcasts..."
378
- value={query}
379
- onChange={setQuery}
380
- onSearch={handleSearch}
381
- results={results}
382
- >
383
- {/* Custom Results Rendering */}
384
- {results.length > 0 && (
385
- <div className="border-fm-divider-secondary bg-fm-surface-primary mt-2 rounded-lg border shadow-xl">
386
- <div className="p-3">
387
- <div className="mb-2 flex items-center justify-between">
388
- <span className="text-fm-tertiary text-xs font-medium">
389
- Search Results
390
- </span>
391
- <span className="text-fm-secondary text-xs">
392
- {results.length} found
393
- </span>
394
- </div>
395
- <div className="max-h-64 space-y-1 overflow-y-auto">
396
- {results.map((result) => (
397
- <button
398
- key={result.id}
399
- onClick={() => {
400
- setSelectedResult(result.text)
401
- setQuery(result.text)
402
- setResults([])
403
- }}
404
- className="text-fm-primary hover:bg-fm-surface-secondary w-full rounded px-3 py-2 text-left text-sm"
405
- >
406
- {result.text}
407
- </button>
408
- ))}
409
- </div>
410
- </div>
411
- </div>
412
- )}
413
- </Search>
414
-
415
- {/* Search Info */}
416
- <div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
417
- <h4 className="text-fm-primary mb-2 text-sm font-medium">
418
- Search Info
127
+ {/* Has query */}
128
+ <div>
129
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
130
+ Has Query
419
131
  </h4>
420
- <div className="text-fm-secondary space-y-1 text-xs">
421
- <div>Query: "{query || "(empty)"}"</div>
422
- <div>Results: {results.length}</div>
423
- <div>Selected: {selectedResult || "(none)"}</div>
132
+ <div className="space-y-2 text-center">
133
+ <Search placeholder="Search episodes" initialValue="The Daily" />
134
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
135
+ Clear button visible when input has value
136
+ </p>
424
137
  </div>
425
138
  </div>
426
- </div>
427
- )
428
- },
429
- parameters: {
430
- docs: {
431
- description: {
432
- story:
433
- "Interactive search example with real-time filtering, custom results rendering, and selection handling.",
434
- },
435
- },
436
- },
437
- }
438
-
439
- // 5. Search with Different States
440
- export const SearchStates: Story = {
441
- render: () => {
442
- const [loadingQuery, setLoadingQuery] = useState("")
443
- const [errorQuery, setErrorQuery] = useState("")
444
- const [emptyQuery, setEmptyQuery] = useState("nonexistent")
445
139
 
446
- return (
447
- <div className="space-y-6">
448
- <div className="text-center">
449
- <h3 className="text-fm-primary mb-2 font-medium">Search States</h3>
450
- <p className="text-fm-secondary text-sm">
451
- Different search states and feedback
452
- </p>
453
- </div>
454
-
455
- <div className="grid gap-6">
456
- {/* Loading State */}
457
- <div className="space-y-3">
458
- <label className="text-fm-tertiary text-sm font-medium">
459
- Loading State
460
- </label>
140
+ {/* Loading */}
141
+ <div>
142
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
143
+ Loading
144
+ </h4>
145
+ <div className="space-y-2 text-center">
461
146
  <Search
462
- placeholder="Search with loading..."
147
+ placeholder="Search episodes"
463
148
  value={loadingQuery}
464
149
  onChange={setLoadingQuery}
465
150
  onSearch={setLoadingQuery}
466
151
  >
467
152
  {loadingQuery && (
468
- <div className="border-fm-divider-secondary bg-fm-surface-primary mt-2 rounded-lg border p-4 text-center">
153
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border p-4 text-center">
469
154
  <div className="flex items-center justify-center gap-2">
470
- <div className="border-fm-divider-secondary h-4 w-4 animate-spin rounded-full border-2 border-t-white"></div>
471
- <span className="text-fm-tertiary text-sm">
472
- Searching...
155
+ <div className="border-fm-divider-contrast h-4 w-4 animate-spin rounded-full border-2 border-t-transparent" />
156
+ <span className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
157
+ Searching
473
158
  </span>
474
159
  </div>
475
160
  </div>
476
161
  )}
477
162
  </Search>
163
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
164
+ Type anything to trigger loading state
165
+ </p>
478
166
  </div>
167
+ </div>
479
168
 
480
- {/* Error State */}
481
- <div className="space-y-3">
482
- <label className="text-fm-tertiary text-sm font-medium">
483
- Error State
484
- </label>
169
+ {/* No results */}
170
+ <div>
171
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
172
+ No Results
173
+ </h4>
174
+ <div className="space-y-2 text-center">
485
175
  <Search
486
- placeholder="Search with error..."
487
- value={errorQuery}
488
- onChange={setErrorQuery}
489
- onSearch={setErrorQuery}
176
+ placeholder="Search episodes"
177
+ value={noResultsQuery}
178
+ onChange={setNoResultsQuery}
179
+ onSearch={setNoResultsQuery}
490
180
  >
491
- {errorQuery && (
492
- <div className="border-fm-divider-secondary bg-fm-surface-negative-sec mt-2 rounded-lg border p-4">
493
- <div className="flex items-center gap-2">
494
- <svg
495
- className="text-fm-icon-negative h-4 w-4"
496
- fill="none"
497
- stroke="currentColor"
498
- viewBox="0 0 24 24"
499
- >
500
- <path
501
- strokeLinecap="round"
502
- strokeLinejoin="round"
503
- strokeWidth={2}
504
- d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
505
- />
506
- </svg>
507
- <span className="text-fm-negative text-sm">
508
- Search failed. Please try again.
509
- </span>
181
+ {noResultsQuery && (
182
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border p-4 text-center">
183
+ <div className="space-y-1">
184
+ <p className="text-fm-primary font-fm-text text-fm-sm leading-fm-sm font-medium">
185
+ No results found
186
+ </p>
187
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
188
+ Try different keywords or check your spelling.
189
+ </p>
510
190
  </div>
511
191
  </div>
512
192
  )}
513
193
  </Search>
194
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
195
+ Pre-filled with a term that matches nothing
196
+ </p>
514
197
  </div>
198
+ </div>
515
199
 
516
- {/* Empty State */}
517
- <div className="space-y-3">
518
- <label className="text-fm-tertiary text-sm font-medium">
519
- Empty Results
520
- </label>
200
+ {/* With results */}
201
+ <div>
202
+ <h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
203
+ With Results
204
+ </h4>
205
+ <div className="space-y-2 text-center">
521
206
  <Search
522
- placeholder="Search with no results..."
523
- value={emptyQuery}
524
- onChange={setEmptyQuery}
525
- onSearch={setEmptyQuery}
207
+ placeholder="Search podcasts…"
208
+ value={resultsQuery}
209
+ onChange={setResultsQuery}
210
+ onSearch={setResultsQuery}
211
+ results={filteredResults}
526
212
  >
527
- {emptyQuery && (
528
- <div className="border-fm-divider-secondary bg-fm-surface-primary mt-2 rounded-lg border p-4 text-center">
529
- <div className="space-y-2">
530
- <svg
531
- className="text-fm-secondary mx-auto h-8 w-8"
532
- fill="none"
533
- stroke="currentColor"
534
- viewBox="0 0 24 24"
535
- >
536
- <path
537
- strokeLinecap="round"
538
- strokeLinejoin="round"
539
- strokeWidth={2}
540
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
541
- />
542
- </svg>
543
- <div>
544
- <p className="text-fm-primary text-sm font-medium">
545
- No results found
546
- </p>
547
- <p className="text-fm-secondary text-xs">
548
- Try different keywords or check your spelling
549
- </p>
550
- </div>
213
+ {filteredResults.length > 0 && (
214
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border">
215
+ <div className="flex items-center justify-between px-3 pt-3 pb-1">
216
+ <span className="text-fm-tertiary font-fm-text text-fm-sm leading-fm-sm">
217
+ Results
218
+ </span>
219
+ <span className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
220
+ {filteredResults.length} found
221
+ </span>
222
+ </div>
223
+ <div className="max-h-48 overflow-y-auto pb-2">
224
+ {filteredResults.map((r) => (
225
+ <button
226
+ key={r.id}
227
+ className="text-fm-primary font-fm-text text-fm-sm leading-fm-sm hover:bg-fm-surface-primary w-full px-3 py-2 text-left transition-colors"
228
+ >
229
+ {r.text}
230
+ </button>
231
+ ))}
551
232
  </div>
552
233
  </div>
553
234
  )}
554
235
  </Search>
236
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
237
+ Pre-filled with "The" — shows matching results
238
+ </p>
555
239
  </div>
556
240
  </div>
557
241
  </div>
558
242
  )
559
243
  },
244
+ }
245
+
246
+ // ─── 3. Interactive ───────────────────────────────────────────────────────────
247
+
248
+ export const Interactive: Story = {
560
249
  parameters: {
561
250
  docs: {
562
251
  description: {
563
252
  story:
564
- "Search component examples showing different states: loading, error, and empty results with appropriate feedback.",
253
+ "Live search with debounce simulation. Type in the search box to filter the podcast list. Results appear after a short delay (300 ms) to mimic a real API call. Selecting a result populates the input.",
565
254
  },
566
255
  },
567
256
  },
568
- }
569
-
570
- // 6. Advanced Search Features
571
- export const AdvancedFeatures: Story = {
572
257
  render: () => {
573
- const [searchHistory, setSearchHistory] = useState<string[]>([
574
- "The Daily",
575
- "Serial",
576
- "This American Life",
577
- ])
578
- const [currentSearch, setCurrentSearch] = useState("")
579
- const [showHistory, setShowHistory] = useState(false)
258
+ const [query, setQuery] = useState("")
259
+ const [debouncedQuery, setDebouncedQuery] = useState("")
260
+ const [isSearching, setIsSearching] = useState(false)
261
+ const [results, setResults] = useState<SearchResult[]>([])
262
+ const [selected, setSelected] = useState<string | null>(null)
263
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
580
264
 
581
- const addToHistory = (query: string) => {
582
- if (query.trim() && !searchHistory.includes(query)) {
583
- setSearchHistory((prev) => [query, ...prev.slice(0, 4)])
265
+ // Debounced search simulation
266
+ useEffect(() => {
267
+ if (!query.trim()) {
268
+ setResults([])
269
+ setDebouncedQuery("")
270
+ setIsSearching(false)
271
+ return
584
272
  }
585
- }
273
+
274
+ setIsSearching(true)
275
+
276
+ if (timerRef.current) clearTimeout(timerRef.current)
277
+ timerRef.current = setTimeout(() => {
278
+ const filtered = ALL_PODCASTS.filter((p) =>
279
+ p.text.toLowerCase().includes(query.toLowerCase())
280
+ )
281
+ setResults(filtered)
282
+ setDebouncedQuery(query)
283
+ setIsSearching(false)
284
+ }, 300)
285
+
286
+ return () => {
287
+ if (timerRef.current) clearTimeout(timerRef.current)
288
+ }
289
+ }, [query])
586
290
 
587
291
  return (
588
- <div className="w-96 space-y-6">
589
- <div className="text-center">
590
- <h3 className="text-fm-primary mb-2 font-medium">
591
- Advanced Features
592
- </h3>
593
- <p className="text-fm-secondary text-sm">
594
- Search with history, suggestions, and shortcuts
595
- </p>
596
- </div>
292
+ <div className="w-full p-8">
293
+ <div className="mx-auto max-w-3xl space-y-6">
294
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
295
+ {/* Controls panel */}
296
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary space-y-5 rounded-xl border p-5">
297
+ <p className="text-fm-primary font-fm-brand text-fm-sm leading-fm-sm font-semibold tracking-widest uppercase">
298
+ Search State
299
+ </p>
300
+
301
+ <div className="border-fm-divider-secondary bg-fm-surface-primary space-y-3 rounded-lg border p-3">
302
+ <div>
303
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm font-medium">
304
+ Query
305
+ </p>
306
+ <p className="text-fm-primary font--(--font-fm-mono) text-fm-sm leading-fm-sm break-all">
307
+ {query || "(empty)"}
308
+ </p>
309
+ </div>
310
+ <div>
311
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm font-medium">
312
+ Debounced
313
+ </p>
314
+ <p className="text-fm-primary font--(--font-fm-mono) text-fm-sm leading-fm-sm break-all">
315
+ {debouncedQuery || "(empty)"}
316
+ </p>
317
+ </div>
318
+ <div>
319
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm font-medium">
320
+ Results
321
+ </p>
322
+ <p className="text-fm-primary font--(--font-fm-mono) text-fm-sm leading-fm-sm">
323
+ {isSearching ? "searching…" : results.length}
324
+ </p>
325
+ </div>
326
+ <div>
327
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm font-medium">
328
+ Selected
329
+ </p>
330
+ <p className="text-fm-primary font--(--font-fm-mono) text-fm-sm leading-fm-sm break-all">
331
+ {selected ?? "(none)"}
332
+ </p>
333
+ </div>
334
+ </div>
597
335
 
598
- <div className="space-y-6">
599
- {/* Search with History */}
600
- <div className="space-y-3">
601
- <label className="text-fm-tertiary text-sm font-medium">
602
- Search with History
603
- </label>
604
- <Search
605
- placeholder="Search with history..."
606
- value={currentSearch}
607
- onChange={setCurrentSearch}
608
- onSearch={(query) => {
609
- addToHistory(query)
610
- setShowHistory(false)
611
- }}
612
- >
613
- {(showHistory ||
614
- (!currentSearch && searchHistory.length > 0)) && (
615
- <div className="border-fm-divider-secondary bg-fm-surface-primary mt-2 rounded-lg border shadow-xl">
616
- <div className="p-3">
617
- <div className="text-fm-tertiary mb-2 text-xs font-medium">
618
- Recent Searches
336
+ <div className="border-fm-divider-secondary border-t pt-4" />
337
+
338
+ <button
339
+ onClick={() => {
340
+ setQuery("")
341
+ setResults([])
342
+ setSelected(null)
343
+ }}
344
+ 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"
345
+ >
346
+ Clear all
347
+ </button>
348
+ </div>
349
+
350
+ {/* Preview stage */}
351
+ <div className="flex flex-col gap-3 lg:col-span-2">
352
+ <Search
353
+ placeholder="Search podcasts…"
354
+ value={query}
355
+ onChange={setQuery}
356
+ onSearch={setQuery}
357
+ results={results}
358
+ >
359
+ {/* Loading state */}
360
+ {isSearching && (
361
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border p-4 text-center">
362
+ <div className="flex items-center justify-center gap-2">
363
+ <div className="border-fm-divider-contrast h-4 w-4 animate-spin rounded-full border-2 border-t-transparent" />
364
+ <span className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
365
+ Searching…
366
+ </span>
619
367
  </div>
620
- <div className="space-y-1">
621
- {searchHistory.map((item, index) => (
368
+ </div>
369
+ )}
370
+
371
+ {/* Results list */}
372
+ {!isSearching && query && results.length > 0 && (
373
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border">
374
+ <div className="flex items-center justify-between px-3 pt-3 pb-1">
375
+ <span className="text-fm-tertiary font-fm-text text-fm-sm leading-fm-sm">
376
+ Podcasts
377
+ </span>
378
+ <span className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
379
+ {results.length} result{results.length !== 1 ? "s" : ""}
380
+ </span>
381
+ </div>
382
+ <div className="max-h-56 overflow-y-auto pb-2">
383
+ {results.map((r) => (
622
384
  <button
623
- key={index}
385
+ key={r.id}
624
386
  onClick={() => {
625
- setCurrentSearch(item)
626
- setShowHistory(false)
387
+ setSelected(r.text)
388
+ setQuery(r.text)
389
+ setResults([])
627
390
  }}
628
- className="text-fm-primary hover:bg-fm-surface-secondary flex w-full items-center gap-2 rounded px-3 py-2 text-left text-sm"
391
+ className="text-fm-primary font-fm-text text-fm-sm leading-fm-sm hover:bg-fm-surface-primary w-full px-3 py-2.5 text-left transition-colors"
629
392
  >
630
- <svg
631
- className="text-fm-secondary h-3 w-3"
632
- fill="none"
633
- stroke="currentColor"
634
- viewBox="0 0 24 24"
635
- >
636
- <path
637
- strokeLinecap="round"
638
- strokeLinejoin="round"
639
- strokeWidth={2}
640
- d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
641
- />
642
- </svg>
643
- {item}
393
+ {r.text}
644
394
  </button>
645
395
  ))}
646
396
  </div>
647
397
  </div>
648
- </div>
649
- )}
650
- </Search>
651
- <div className="flex gap-2">
652
- <Button
653
- size="sm"
654
- variant="outline"
655
- onClick={() => setShowHistory(!showHistory)}
656
- >
657
- {showHistory ? "Hide" : "Show"} History
658
- </Button>
659
- <Button
660
- size="sm"
661
- variant="outline"
662
- onClick={() => setSearchHistory([])}
663
- >
664
- Clear History
665
- </Button>
666
- </div>
667
- </div>
398
+ )}
399
+
400
+ {/* No results */}
401
+ {!isSearching && query && results.length === 0 && (
402
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border p-4 text-center">
403
+ <p className="text-fm-primary font-fm-text text-fm-sm leading-fm-sm font-medium">
404
+ No results for "{query}"
405
+ </p>
406
+ <p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mt-1">
407
+ Try a different search term.
408
+ </p>
409
+ </div>
410
+ )}
411
+ </Search>
668
412
 
669
- {/* Keyboard Shortcuts Info */}
670
- <div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
671
- <h4 className="text-fm-primary mb-2 text-sm font-medium">
672
- Keyboard Shortcuts
673
- </h4>
674
- <div className="text-fm-secondary space-y-1 text-xs">
675
- <div className="flex justify-between">
676
- <span>Focus search:</span>
677
- <kbd className="bg-fm-surface-secondary rounded px-1 font-mono">
678
- Cmd+K
679
- </kbd>
680
- </div>
681
- <div className="flex justify-between">
682
- <span>Clear search:</span>
683
- <kbd className="bg-fm-surface-secondary rounded px-1 font-mono">
684
- Esc
685
- </kbd>
686
- </div>
687
- <div className="flex justify-between">
688
- <span>Navigate results:</span>
689
- <kbd className="bg-fm-surface-secondary rounded px-1 font-mono">
690
- ↑↓
691
- </kbd>
413
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border px-4 py-3">
414
+ <code className="text-fm-secondary font--(--font-fm-mono) text-fm-md leading-fm-md">
415
+ {`<Search value="${query}" onChange={setQuery} results={[…]} />`}
416
+ </code>
692
417
  </div>
693
418
  </div>
694
419
  </div>
@@ -696,12 +421,4 @@ export const AdvancedFeatures: Story = {
696
421
  </div>
697
422
  )
698
423
  },
699
- parameters: {
700
- docs: {
701
- description: {
702
- story:
703
- "Advanced search features including search history, keyboard shortcuts, and enhanced user experience patterns.",
704
- },
705
- },
706
- },
707
424
  }