aural-ui 4.0.1 → 4.1.0

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