aural-ui 3.0.7 → 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 (183) hide show
  1. package/dist/components/aspect-ratio/AspectRatio.stories.tsx +290 -1199
  2. package/dist/components/avatar/Avatar.stories.tsx +235 -237
  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/button/index.tsx +7 -7
  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 -620
  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 +533 -856
  16. package/dist/components/dialog/Dialog.stories.tsx +505 -949
  17. package/dist/components/divider/Divider.stories.tsx +265 -502
  18. package/dist/components/dot-loader/DotLoader.stories.tsx +256 -257
  19. package/dist/components/drawer/Drawer.stories.tsx +659 -993
  20. package/dist/components/drawer/index.tsx +3 -3
  21. package/dist/components/dropdown/Dropdown.stories.tsx +643 -1018
  22. package/dist/components/form/Form.stories.tsx +560 -274
  23. package/dist/components/helper-text/HelperText.stories.tsx +199 -200
  24. package/dist/components/hover-card/HoverCard.stories.tsx +318 -1221
  25. package/dist/components/icon-button/IconButton.stories.tsx +837 -194
  26. package/dist/components/if-else/if-else.stories.tsx +370 -83
  27. package/dist/components/input/Input.stories.tsx +436 -368
  28. package/dist/components/label/Label.stories.tsx +156 -154
  29. package/dist/components/list/List.stories.tsx +485 -822
  30. package/dist/components/marquee/Marquee.stories.tsx +356 -694
  31. package/dist/components/otp-inputs/OtpInputs.stories.tsx +352 -410
  32. package/dist/components/overlay/Overlay.stories.tsx +452 -818
  33. package/dist/components/overlay/index.tsx +4 -4
  34. package/dist/components/pagination/Pagination.stories.tsx +721 -210
  35. package/dist/components/popover/Popover.stories.tsx +484 -873
  36. package/dist/components/radio/Radio.stories.tsx +432 -124
  37. package/dist/components/resizable/Resizable.stories.tsx +496 -752
  38. package/dist/components/scroll-area/ScrollArea.stories.tsx +384 -1006
  39. package/dist/components/search/Search.stories.tsx +314 -575
  40. package/dist/components/select/Select.stories.tsx +684 -787
  41. package/dist/components/sheet/Sheet.stories.tsx +671 -936
  42. package/dist/components/skelton/Skelton.stories.tsx +230 -764
  43. package/dist/components/slider/Slider.stories.tsx +384 -737
  44. package/dist/components/stepper/Stepper.stories.tsx +371 -514
  45. package/dist/components/switch/Switch.stories.tsx +461 -208
  46. package/dist/components/switch-case/SwitchCase.stories.tsx +367 -188
  47. package/dist/components/table/Table.stories.tsx +770 -914
  48. package/dist/components/tabs/Tabs.stories.tsx +459 -1400
  49. package/dist/components/tag/Tag.stories.tsx +714 -542
  50. package/dist/components/textarea/TextArea.stories.tsx +621 -562
  51. package/dist/components/thumbnail-tags/ThumbnailTags.stories.tsx +228 -148
  52. package/dist/components/toast/Toast.stories.tsx +452 -1333
  53. package/dist/components/toggle/Toggle.stories.tsx +488 -909
  54. package/dist/components/tooltip/Tooltip.stories.tsx +344 -1372
  55. package/dist/components/typography/Typography.stories.tsx +406 -89
  56. package/dist/hooks/use-change-state/UseChangeState.stories.tsx +309 -606
  57. package/dist/hooks/use-previous/UsePrevious.stories.tsx +367 -917
  58. package/dist/hooks/use-standalone-pagination/UseStandalonePagination.stories.tsx +639 -867
  59. package/dist/icons/Icons.stories.tsx +0 -12
  60. package/dist/icons/ai-avatar-icon/AiAvatarIcon.stories.tsx +226 -1013
  61. package/dist/icons/alert-icon/AlertIcon.stories.tsx +109 -929
  62. package/dist/icons/all-icons.tsx +124 -87
  63. package/dist/icons/angle-down-icon/AngleDownIcon.stories.tsx +140 -971
  64. package/dist/icons/apple-logo-icon/AppleLogoIcon.stories.tsx +148 -888
  65. package/dist/icons/arrow-box-left-icon/ArrowBoxLeftIcon.stories.tsx +135 -1019
  66. package/dist/icons/arrow-corner-up-left-icon/ArrowCornerUpLeftIcon.stories.tsx +137 -953
  67. package/dist/icons/arrow-corner-up-right-icon/ArrowCornerUpRightIcon.stories.tsx +138 -997
  68. package/dist/icons/arrow-left-icon/ArrowLeftIcon.stories.tsx +136 -942
  69. package/dist/icons/arrow-right-icon/ArrowRightIcon.stories.tsx +148 -1092
  70. package/dist/icons/arrow-right-up-icon/ArrowRightUpIcon.stories.tsx +146 -1211
  71. package/dist/icons/art-board-icon/ArtBoardIcon.stories.tsx +126 -615
  72. package/dist/icons/audio-bar-icon/AudioBarIcon.stories.tsx +144 -1164
  73. package/dist/icons/backward-ten-seconds-icon/BackwardTenSecondsIcon.stories.tsx +167 -985
  74. package/dist/icons/bubble-check-icon/BubbleCheckIcon.stories.tsx +122 -1179
  75. package/dist/icons/bubble-crossed-icon/BubbleCrossedIcon.stories.tsx +124 -1168
  76. package/dist/icons/bubble-sparkle-icon/BubbleSparkleIcon.stories.tsx +119 -850
  77. package/dist/icons/camera-icon/CameraIcon.stories.tsx +112 -1213
  78. package/dist/icons/capital-a-letter-icon/CapitalALetterIcon.stories.tsx +117 -934
  79. package/dist/icons/chevron-double-left-icon/ChevronDoubleLeftIcon.stories.tsx +160 -961
  80. package/dist/icons/chevron-double-right-icon/ChevronDoubleRightIcon.stories.tsx +163 -961
  81. package/dist/icons/chevron-down-icon/ChevronDownIcon.stories.tsx +144 -942
  82. package/dist/icons/chevron-left-icon/ChevronLeftIcon.stories.tsx +129 -966
  83. package/dist/icons/chevron-right-icon/ChevronRightIcon.stories.tsx +147 -964
  84. package/dist/icons/chevron-up-icon/ChevronUpIcon.stories.tsx +145 -975
  85. package/dist/icons/circle-tick-icon/CircleTickIcon.stories.tsx +150 -1142
  86. package/dist/icons/circular-play-icon/CircularPlayIcon.stories.tsx +114 -461
  87. package/dist/icons/coin-icon/CoinIcon.stories.tsx +124 -1322
  88. package/dist/icons/coin-toons-icon/CoinToonsIcon.stories.tsx +117 -1318
  89. package/dist/icons/column-wide-add-icon/ColumnWideAddIcon.stories.tsx +114 -903
  90. package/dist/icons/command-icon/CommandIcon.stories.tsx +127 -1042
  91. package/dist/icons/copy-icon/CopyIcon.stories.tsx +123 -962
  92. package/dist/icons/cross-circle-icon/CrossCircleIcon.stories.tsx +147 -999
  93. package/dist/icons/cross-icon/CrossIcon.stories.tsx +139 -960
  94. package/dist/icons/download-icon/DownloadIcon.stories.tsx +126 -820
  95. package/dist/icons/edit-big-icon/EditBigIcon.stories.tsx +124 -1031
  96. package/dist/icons/email-icon/EmailIcon.stories.tsx +115 -936
  97. package/dist/icons/expand-icon/ExpandIcon.stories.tsx +112 -1111
  98. package/dist/icons/eye-close-icon/EyeCloseIcon.stories.tsx +144 -1025
  99. package/dist/icons/eye-open-icon/EyeOpenIcon.stories.tsx +143 -1036
  100. package/dist/icons/feature-shine-icon/FeatureShineIcon.stories.tsx +127 -1011
  101. package/dist/icons/file-chart-icon/FileChartIcon.stories.tsx +126 -1056
  102. package/dist/icons/file-text-icon/FileTextIcon.stories.tsx +125 -614
  103. package/dist/icons/filter-bar-row-icon/FilterBarRowIcon.stories.tsx +119 -1050
  104. package/dist/icons/forward-ten-seconds-icon/ForwardTenSecondsIcon.stories.tsx +169 -989
  105. package/dist/icons/git-branch-icon/GitBranchIcon.stories.tsx +115 -1145
  106. package/dist/icons/git-fork-icon/GitForkIcon.stories.tsx +115 -1122
  107. package/dist/icons/globe-icon/GlobeIcon.stories.tsx +130 -313
  108. package/dist/icons/google-logo-icon/GoogleLogoIcon.stories.tsx +145 -940
  109. package/dist/icons/grip-vertical-icon/GripVerticalIcon.stories.tsx +119 -1174
  110. package/dist/icons/head-icon/HeadIcon.stories.tsx +111 -916
  111. package/dist/icons/heart-icon/HeartIcon.stories.tsx +120 -1019
  112. package/dist/icons/image-avatar-sparkle-icon/ImageAvatarSparkleIcon.stories.tsx +119 -683
  113. package/dist/icons/image-icon/ImageIcon.stories.tsx +105 -1121
  114. package/dist/icons/import-folder-icon/ImportFolderIcon.stories.tsx +111 -1192
  115. package/dist/icons/import-left-arrow-folder-icon/ImportLeftArrowFolderIcon.stories.tsx +136 -1256
  116. package/dist/icons/indian-flag-icon/IndianFlagIcon.stories.tsx +159 -962
  117. package/dist/icons/instagram-icon/InstagramIcon.stories.tsx +161 -1385
  118. package/dist/icons/layout-column-icon/LayoutColumnIcon.stories.tsx +124 -972
  119. package/dist/icons/layout-left-icon/LayoutLeftIcon.stories.tsx +119 -948
  120. package/dist/icons/layout-right-icon/LayoutRightIcon.stories.tsx +119 -942
  121. package/dist/icons/light-bulb-simple-icon/LightBulbSimpleIcon.stories.tsx +108 -1215
  122. package/dist/icons/linked-in-icon/LinkedInIcon.stories.tsx +154 -1517
  123. package/dist/icons/magic-book-icon/MagicBookIcon.stories.tsx +110 -1188
  124. package/dist/icons/magic-edit-icon/MagicEditIcon.stories.tsx +119 -678
  125. package/dist/icons/maintenance-icon/MaintenanceIcon.stories.tsx +123 -1184
  126. package/dist/icons/message-icon/MessageIcon.stories.tsx +114 -538
  127. package/dist/icons/minimize-icon/MinimizeIcon.stories.tsx +116 -1158
  128. package/dist/icons/moon-icon/MoonIcon.stories.tsx +120 -536
  129. package/dist/icons/move-horizontal-icon/MoveHorizontalIcon.stories.tsx +109 -1184
  130. package/dist/icons/move-vertical-icon/MoveVerticalIcon.stories.tsx +115 -1134
  131. package/dist/icons/musical-note-icon/MusicalNoteIcon.stories.tsx +119 -971
  132. package/dist/icons/notepad-icon/NotepadIcon.stories.tsx +111 -1100
  133. package/dist/icons/notes-icon/NotesIcon.stories.tsx +119 -1101
  134. package/dist/icons/page-search-icon/PageSearchIcon.stories.tsx +109 -1111
  135. package/dist/icons/page-text-icon/PageTextIcon.stories.tsx +122 -684
  136. package/dist/icons/paint-roll-icon/PaintRollIcon.stories.tsx +113 -954
  137. package/dist/icons/paper-plane-icon/PaperPlaneIcon.stories.tsx +112 -877
  138. package/dist/icons/pause-icon/PauseIcon.stories.tsx +113 -1000
  139. package/dist/icons/pencil-icon/PencilIcon.stories.tsx +115 -1070
  140. package/dist/icons/phone-icon/PhoneIcon.stories.tsx +115 -978
  141. package/dist/icons/plus-icon/PlusIcon.stories.tsx +106 -1093
  142. package/dist/icons/pocket-studio-icon/PocketStudioIcon.stories.tsx +107 -829
  143. package/dist/icons/scroll-down-icon/ScrollDownIcon.stories.tsx +102 -469
  144. package/dist/icons/search-icon/SearchIcon.stories.tsx +111 -1124
  145. package/dist/icons/setting-icon/SettingIcon.stories.tsx +107 -970
  146. package/dist/icons/share-icon/ShareIcon.stories.tsx +120 -1025
  147. package/dist/icons/shield-icon/ShieldIcon.stories.tsx +117 -931
  148. package/dist/icons/site-logo-icon/SiteLogoIcon.stories.tsx +137 -1104
  149. package/dist/icons/skip-backward-icon/SkipBackwardIcon.stories.tsx +172 -982
  150. package/dist/icons/skip-forward-icon/SkipForwardIcon.stories.tsx +164 -983
  151. package/dist/icons/sparkles-soft-icon/SparklesSoftIcon.stories.tsx +105 -958
  152. package/dist/icons/spinner-gradient-icon/SpinnerGradientIcon.stories.tsx +158 -580
  153. package/dist/icons/spinner-gradient-icon/index.tsx +6 -1
  154. package/dist/icons/spinner-solid-icon/SpinnerSolidIcon.stories.tsx +158 -587
  155. package/dist/icons/spinner-solid-icon/index.tsx +6 -1
  156. package/dist/icons/spinner-solid-neutral-icon/SpinnerSolidINeutralcon.stories.tsx +146 -682
  157. package/dist/icons/spinner-solid-neutral-icon/index.tsx +1 -1
  158. package/dist/icons/star-icon/StarIcon.stories.tsx +124 -904
  159. package/dist/icons/store-coin-icon/StoreCoinIcon.stories.tsx +112 -964
  160. package/dist/icons/suggestion-icon/SuggestionIcon.stories.tsx +116 -852
  161. package/dist/icons/sun-icon/SunIcon.stories.tsx +120 -831
  162. package/dist/icons/text-color-icon/TextColorIcon.stories.tsx +116 -950
  163. package/dist/icons/text-indicator-icon/TextIndicatorIcon.stories.tsx +123 -980
  164. package/dist/icons/threads-icon/ThreadsIcon.stories.tsx +156 -1427
  165. package/dist/icons/tick-circle-icon/TickCircleIcon.stories.tsx +146 -1142
  166. package/dist/icons/tick-icon/TickIcon.stories.tsx +145 -1276
  167. package/dist/icons/trash-icon/TrashIcon.stories.tsx +108 -933
  168. package/dist/icons/twitter-x-icon/TwitterXIcon.stories.tsx +157 -1402
  169. package/dist/icons/upload-icon/UploadIcon.stories.tsx +115 -889
  170. package/dist/icons/vertical-menu-icon/VerticalMenuIcon.stories.tsx +118 -984
  171. package/dist/icons/video-play-list-icon/VideoPlaylistIcon.stories.tsx +125 -1049
  172. package/dist/icons/voice-playing-icon/VoicePlayingIcon.stories.tsx +123 -1356
  173. package/dist/icons/volume-full-icon/VolumeFullIcon.stories.tsx +110 -1171
  174. package/dist/icons/volume-half-icon/VolumeHalfIcon.stories.tsx +112 -1093
  175. package/dist/icons/volume-off-icon/VolumeOffIcon.stories.tsx +115 -1087
  176. package/dist/icons/warning-icon/WarningIcon.stories.tsx +122 -1046
  177. package/dist/icons/youtube-icon/YoutubeIcon.stories.tsx +161 -936
  178. package/dist/index.cjs +84 -84
  179. package/dist/index.js +84 -84
  180. package/dist/styles/aural-all-theme.css +1222 -0
  181. package/dist/styles/{aural-theme.css → aural-dark-theme.css} +15 -3
  182. package/dist/styles/aural-light-theme.css +1047 -0
  183. package/package.json +1 -1
@@ -1,672 +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="mb-2 font-medium text-white">Basic Search Component</h3>
94
- <p className="text-sm text-white/60">
95
- Simple search input with different placeholder examples
96
- </p>
97
- </div>
98
-
99
- <div className="space-y-4">
100
- {/* Default Placeholder */}
101
- <div className="space-y-2">
102
- <label className="text-sm font-medium text-white/80">Default</label>
103
- <Search placeholder="Search episodes" />
104
- </div>
105
-
106
- {/* Custom Placeholders */}
107
- <div className="space-y-2">
108
- <label className="text-sm font-medium text-white/80">
109
- Custom Placeholders
110
- </label>
111
- <div className="space-y-3">
112
- <Search placeholder="Search podcasts..." />
113
- <Search placeholder="Find your favorite shows" />
114
- <Search placeholder="Type to search music" />
115
- </div>
116
- </div>
117
-
118
- {/* With Initial Value */}
119
- <div className="space-y-2">
120
- <label className="text-sm font-medium text-white/80">
121
- With Initial Value
122
- </label>
123
- <Search placeholder="Search episodes" initialValue="The Daily" />
124
- </div>
125
- </div>
126
- </div>
127
- ),
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 = {
128
75
  parameters: {
129
76
  docs: {
130
77
  description: {
131
78
  story:
132
- "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.",
133
80
  },
134
81
  },
135
82
  },
83
+ render: (args) => (
84
+ <div className="w-80">
85
+ <Search {...args} />
86
+ </div>
87
+ ),
136
88
  }
137
89
 
138
- // 2. Controlled Component Examples
139
- export const ControlledSearch: Story = {
140
- render: () => {
141
- const [searchValue1, setSearchValue1] = useState("")
142
- const [searchValue2, setSearchValue2] = useState("Controlled")
143
- const [searchValue3, setSearchValue3] = useState("")
144
-
145
- return (
146
- <div className="space-y-6">
147
- <div className="text-center">
148
- <h3 className="mb-2 font-medium text-white">Controlled Search</h3>
149
- <p className="text-sm text-white/60">
150
- Search components with external state control
151
- </p>
152
- </div>
153
-
154
- <div className="space-y-6">
155
- {/* Basic Controlled */}
156
- <div className="space-y-3">
157
- <div className="flex items-center justify-between">
158
- <label className="text-sm font-medium text-white/80">
159
- Basic Controlled
160
- </label>
161
- <div className="text-xs text-white/60">
162
- Value: "{searchValue1}"
163
- </div>
164
- </div>
165
- <Search
166
- placeholder="Type something..."
167
- value={searchValue1}
168
- onChange={setSearchValue1}
169
- />
170
- <div className="flex gap-2">
171
- <Button
172
- size="sm"
173
- variant="outline"
174
- onClick={() => setSearchValue1("Preset Value")}
175
- >
176
- Set Value
177
- </Button>
178
- <Button
179
- size="sm"
180
- variant="outline"
181
- onClick={() => setSearchValue1("")}
182
- >
183
- Clear
184
- </Button>
185
- </div>
186
- </div>
90
+ // ─── 2. States ────────────────────────────────────────────────────────────────
187
91
 
188
- {/* Pre-filled Controlled */}
189
- <div className="space-y-3">
190
- <div className="flex items-center justify-between">
191
- <label className="text-sm font-medium text-white/80">
192
- Pre-filled Controlled
193
- </label>
194
- <div className="text-xs text-white/60">
195
- Value: "{searchValue2}"
196
- </div>
197
- </div>
198
- <Search
199
- placeholder="Search with preset value"
200
- value={searchValue2}
201
- onChange={setSearchValue2}
202
- />
203
- </div>
204
-
205
- {/* Controlled with Validation */}
206
- <div className="space-y-3">
207
- <div className="flex items-center justify-between">
208
- <label className="text-sm font-medium text-white/80">
209
- With Validation
210
- </label>
211
- <div className="text-xs text-white/60">
212
- Length: {searchValue3.length}/20
213
- </div>
214
- </div>
215
- <Search
216
- placeholder="Max 20 characters"
217
- value={searchValue3}
218
- onChange={(value) => {
219
- if (value.length <= 20) {
220
- setSearchValue3(value)
221
- }
222
- }}
223
- />
224
- {searchValue3.length >= 20 && (
225
- <p className="text-xs text-red-400">Maximum length reached</p>
226
- )}
227
- </div>
228
- </div>
229
- </div>
230
- )
231
- },
92
+ export const States: Story = {
232
93
  parameters: {
233
94
  docs: {
234
95
  description: {
235
96
  story:
236
- "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).",
237
98
  },
238
99
  },
239
100
  },
240
- }
241
-
242
- // 3. Uncontrolled Component Examples
243
- export const UncontrolledSearch: Story = {
244
101
  render: () => {
245
- const [lastSearch1, setLastSearch1] = useState("")
246
- const [lastSearch2, setLastSearch2] = useState("")
247
-
248
- return (
249
- <div className="space-y-6">
250
- <div className="text-center">
251
- <h3 className="mb-2 font-medium text-white">Uncontrolled Search</h3>
252
- <p className="text-sm text-white/60">
253
- Search components with internal state management
254
- </p>
255
- </div>
256
-
257
- <div className="space-y-6">
258
- {/* Basic Uncontrolled */}
259
- <div className="space-y-3">
260
- <div className="flex items-center justify-between">
261
- <label className="text-sm font-medium text-white/80">
262
- Basic Uncontrolled
263
- </label>
264
- <div className="text-xs text-white/60">
265
- Last search: "{lastSearch1}"
266
- </div>
267
- </div>
268
- <Search
269
- placeholder="Search internally managed"
270
- onSearch={setLastSearch1}
271
- />
272
- </div>
102
+ const [loadingQuery, setLoadingQuery] = useState("")
103
+ const [noResultsQuery, setNoResultsQuery] = useState("nonexistentshow")
104
+ const [resultsQuery, setResultsQuery] = useState("The")
273
105
 
274
- {/* With Initial Value */}
275
- <div className="space-y-3">
276
- <div className="flex items-center justify-between">
277
- <label className="text-sm font-medium text-white/80">
278
- With Initial Value
279
- </label>
280
- <div className="text-xs text-white/60">
281
- Last search: "{lastSearch2}"
282
- </div>
283
- </div>
284
- <Search
285
- placeholder="Search with initial value"
286
- initialValue="Initial Search"
287
- onSearch={setLastSearch2}
288
- />
289
- </div>
106
+ const filteredResults: SearchResult[] = resultsQuery
107
+ ? ALL_PODCASTS.filter((p) =>
108
+ p.text.toLowerCase().includes(resultsQuery.toLowerCase())
109
+ )
110
+ : []
290
111
 
291
- {/* Multiple Independent */}
292
- <div className="space-y-3">
293
- <label className="text-sm font-medium text-white/80">
294
- Multiple Independent
295
- </label>
296
- <div className="grid grid-cols-2 gap-3">
297
- <Search placeholder="Search A" />
298
- <Search placeholder="Search B" />
299
- </div>
300
- <p className="text-xs text-white/60">
301
- 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
302
123
  </p>
303
124
  </div>
304
125
  </div>
305
- </div>
306
- )
307
- },
308
- parameters: {
309
- docs: {
310
- description: {
311
- story:
312
- "Uncontrolled search component examples with internal state management and initial values.",
313
- },
314
- },
315
- },
316
- }
317
-
318
- // 4. Interactive Search with Results
319
- export const InteractiveSearch: Story = {
320
- render: () => {
321
- const [query, setQuery] = useState("")
322
- const [results, setResults] = useState<SearchResult[]>([])
323
- const [selectedResult, setSelectedResult] = useState<string | null>(null)
324
-
325
- // Mock search data
326
- const allPodcasts = [
327
- { id: "1", text: "The Joe Rogan Experience" },
328
- { id: "2", text: "Serial" },
329
- { id: "3", text: "This American Life" },
330
- { id: "4", text: "Stuff You Should Know" },
331
- { id: "5", text: "The Daily" },
332
- { id: "6", text: "Crime Junkie" },
333
- { id: "7", text: "The Michelle Obama Podcast" },
334
- { id: "8", text: "Call Her Daddy" },
335
- { id: "9", text: "My Favorite Murder" },
336
- { id: "10", text: "The Tim Ferriss Show" },
337
- { id: "11", text: "Conan O'Brien Needs a Friend" },
338
- { id: "12", text: "The Ben Shapiro Show" },
339
- ]
340
-
341
- const handleSearch = (searchQuery: string) => {
342
- setQuery(searchQuery)
343
126
 
344
- if (!searchQuery.trim()) {
345
- setResults([])
346
- return
347
- }
348
-
349
- // Filter podcasts based on query
350
- const filteredResults = allPodcasts.filter((podcast) =>
351
- podcast.text.toLowerCase().includes(searchQuery.toLowerCase())
352
- )
353
-
354
- setResults(filteredResults)
355
- }
356
-
357
- return (
358
- <div className="w-96 space-y-4">
359
- <div className="text-center">
360
- <h3 className="mb-2 font-medium text-white">Interactive Search</h3>
361
- <p className="text-sm text-white/60">
362
- Real-time search with custom results rendering
363
- </p>
364
- </div>
365
-
366
- <Search
367
- placeholder="Search podcasts..."
368
- value={query}
369
- onChange={setQuery}
370
- onSearch={handleSearch}
371
- results={results}
372
- >
373
- {/* Custom Results Rendering */}
374
- {results.length > 0 && (
375
- <div className="mt-2 rounded-lg border border-white/10 bg-gray-800/90 shadow-xl">
376
- <div className="p-3">
377
- <div className="mb-2 flex items-center justify-between">
378
- <span className="text-xs font-medium text-white/80">
379
- Search Results
380
- </span>
381
- <span className="text-xs text-white/60">
382
- {results.length} found
383
- </span>
384
- </div>
385
- <div className="max-h-64 space-y-1 overflow-y-auto">
386
- {results.map((result) => (
387
- <button
388
- key={result.id}
389
- onClick={() => {
390
- setSelectedResult(result.text)
391
- setQuery(result.text)
392
- setResults([])
393
- }}
394
- className="w-full rounded px-3 py-2 text-left text-sm text-white hover:bg-white/10"
395
- >
396
- {result.text}
397
- </button>
398
- ))}
399
- </div>
400
- </div>
401
- </div>
402
- )}
403
- </Search>
404
-
405
- {/* Search Info */}
406
- <div className="rounded-lg border border-white/10 bg-white/5 p-4">
407
- <h4 className="mb-2 text-sm font-medium text-white">Search Info</h4>
408
- <div className="space-y-1 text-xs text-white/60">
409
- <div>Query: "{query || "(empty)"}"</div>
410
- <div>Results: {results.length}</div>
411
- <div>Selected: {selectedResult || "(none)"}</div>
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
131
+ </h4>
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>
412
137
  </div>
413
138
  </div>
414
- </div>
415
- )
416
- },
417
- parameters: {
418
- docs: {
419
- description: {
420
- story:
421
- "Interactive search example with real-time filtering, custom results rendering, and selection handling.",
422
- },
423
- },
424
- },
425
- }
426
-
427
- // 5. Search with Different States
428
- export const SearchStates: Story = {
429
- render: () => {
430
- const [loadingQuery, setLoadingQuery] = useState("")
431
- const [errorQuery, setErrorQuery] = useState("")
432
- const [emptyQuery, setEmptyQuery] = useState("nonexistent")
433
-
434
- return (
435
- <div className="space-y-6">
436
- <div className="text-center">
437
- <h3 className="mb-2 font-medium text-white">Search States</h3>
438
- <p className="text-sm text-white/60">
439
- Different search states and feedback
440
- </p>
441
- </div>
442
139
 
443
- <div className="grid gap-6">
444
- {/* Loading State */}
445
- <div className="space-y-3">
446
- <label className="text-sm font-medium text-white/80">
447
- Loading State
448
- </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">
449
146
  <Search
450
- placeholder="Search with loading..."
147
+ placeholder="Search episodes"
451
148
  value={loadingQuery}
452
149
  onChange={setLoadingQuery}
453
150
  onSearch={setLoadingQuery}
454
151
  >
455
152
  {loadingQuery && (
456
- <div className="mt-2 rounded-lg border border-white/10 bg-gray-800/90 p-4 text-center">
153
+ <div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border p-4 text-center">
457
154
  <div className="flex items-center justify-center gap-2">
458
- <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/20 border-t-white"></div>
459
- <span className="text-sm text-white/80">Searching...</span>
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…
158
+ </span>
460
159
  </div>
461
160
  </div>
462
161
  )}
463
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>
464
166
  </div>
167
+ </div>
465
168
 
466
- {/* Error State */}
467
- <div className="space-y-3">
468
- <label className="text-sm font-medium text-white/80">
469
- Error State
470
- </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">
471
175
  <Search
472
- placeholder="Search with error..."
473
- value={errorQuery}
474
- onChange={setErrorQuery}
475
- onSearch={setErrorQuery}
176
+ placeholder="Search episodes"
177
+ value={noResultsQuery}
178
+ onChange={setNoResultsQuery}
179
+ onSearch={setNoResultsQuery}
476
180
  >
477
- {errorQuery && (
478
- <div className="mt-2 rounded-lg border border-red-500/30 bg-red-900/30 p-4">
479
- <div className="flex items-center gap-2">
480
- <svg
481
- className="h-4 w-4 text-red-400"
482
- fill="none"
483
- stroke="currentColor"
484
- viewBox="0 0 24 24"
485
- >
486
- <path
487
- strokeLinecap="round"
488
- strokeLinejoin="round"
489
- strokeWidth={2}
490
- d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
491
- />
492
- </svg>
493
- <span className="text-sm text-red-400">
494
- Search failed. Please try again.
495
- </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>
496
190
  </div>
497
191
  </div>
498
192
  )}
499
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>
500
197
  </div>
198
+ </div>
501
199
 
502
- {/* Empty State */}
503
- <div className="space-y-3">
504
- <label className="text-sm font-medium text-white/80">
505
- Empty Results
506
- </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">
507
206
  <Search
508
- placeholder="Search with no results..."
509
- value={emptyQuery}
510
- onChange={setEmptyQuery}
511
- onSearch={setEmptyQuery}
207
+ placeholder="Search podcasts…"
208
+ value={resultsQuery}
209
+ onChange={setResultsQuery}
210
+ onSearch={setResultsQuery}
211
+ results={filteredResults}
512
212
  >
513
- {emptyQuery && (
514
- <div className="mt-2 rounded-lg border border-white/10 bg-gray-800/90 p-4 text-center">
515
- <div className="space-y-2">
516
- <svg
517
- className="mx-auto h-8 w-8 text-white/40"
518
- fill="none"
519
- stroke="currentColor"
520
- viewBox="0 0 24 24"
521
- >
522
- <path
523
- strokeLinecap="round"
524
- strokeLinejoin="round"
525
- strokeWidth={2}
526
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
527
- />
528
- </svg>
529
- <div>
530
- <p className="text-sm font-medium text-white">
531
- No results found
532
- </p>
533
- <p className="text-xs text-white/60">
534
- Try different keywords or check your spelling
535
- </p>
536
- </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
+ ))}
537
232
  </div>
538
233
  </div>
539
234
  )}
540
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>
541
239
  </div>
542
240
  </div>
543
241
  </div>
544
242
  )
545
243
  },
244
+ }
245
+
246
+ // ─── 3. Interactive ───────────────────────────────────────────────────────────
247
+
248
+ export const Interactive: Story = {
546
249
  parameters: {
547
250
  docs: {
548
251
  description: {
549
252
  story:
550
- "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.",
551
254
  },
552
255
  },
553
256
  },
554
- }
555
-
556
- // 6. Advanced Search Features
557
- export const AdvancedFeatures: Story = {
558
257
  render: () => {
559
- const [searchHistory, setSearchHistory] = useState<string[]>([
560
- "The Daily",
561
- "Serial",
562
- "This American Life",
563
- ])
564
- const [currentSearch, setCurrentSearch] = useState("")
565
- 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)
264
+
265
+ // Debounced search simulation
266
+ useEffect(() => {
267
+ if (!query.trim()) {
268
+ setResults([])
269
+ setDebouncedQuery("")
270
+ setIsSearching(false)
271
+ return
272
+ }
273
+
274
+ setIsSearching(true)
566
275
 
567
- const addToHistory = (query: string) => {
568
- if (query.trim() && !searchHistory.includes(query)) {
569
- setSearchHistory((prev) => [query, ...prev.slice(0, 4)])
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)
570
288
  }
571
- }
289
+ }, [query])
572
290
 
573
291
  return (
574
- <div className="w-96 space-y-6">
575
- <div className="text-center">
576
- <h3 className="mb-2 font-medium text-white">Advanced Features</h3>
577
- <p className="text-sm text-white/60">
578
- Search with history, suggestions, and shortcuts
579
- </p>
580
- </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>
581
335
 
582
- <div className="space-y-6">
583
- {/* Search with History */}
584
- <div className="space-y-3">
585
- <label className="text-sm font-medium text-white/80">
586
- Search with History
587
- </label>
588
- <Search
589
- placeholder="Search with history..."
590
- value={currentSearch}
591
- onChange={setCurrentSearch}
592
- onSearch={(query) => {
593
- addToHistory(query)
594
- setShowHistory(false)
595
- }}
596
- >
597
- {(showHistory ||
598
- (!currentSearch && searchHistory.length > 0)) && (
599
- <div className="mt-2 rounded-lg border border-white/10 bg-gray-800/90 shadow-xl">
600
- <div className="p-3">
601
- <div className="mb-2 text-xs font-medium text-white/80">
602
- 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>
603
367
  </div>
604
- <div className="space-y-1">
605
- {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) => (
606
384
  <button
607
- key={index}
385
+ key={r.id}
608
386
  onClick={() => {
609
- setCurrentSearch(item)
610
- setShowHistory(false)
387
+ setSelected(r.text)
388
+ setQuery(r.text)
389
+ setResults([])
611
390
  }}
612
- className="flex w-full items-center gap-2 rounded px-3 py-2 text-left text-sm text-white hover:bg-white/10"
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"
613
392
  >
614
- <svg
615
- className="h-3 w-3 text-white/40"
616
- fill="none"
617
- stroke="currentColor"
618
- viewBox="0 0 24 24"
619
- >
620
- <path
621
- strokeLinecap="round"
622
- strokeLinejoin="round"
623
- strokeWidth={2}
624
- d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
625
- />
626
- </svg>
627
- {item}
393
+ {r.text}
628
394
  </button>
629
395
  ))}
630
396
  </div>
631
397
  </div>
632
- </div>
633
- )}
634
- </Search>
635
- <div className="flex gap-2">
636
- <Button
637
- size="sm"
638
- variant="outline"
639
- onClick={() => setShowHistory(!showHistory)}
640
- >
641
- {showHistory ? "Hide" : "Show"} History
642
- </Button>
643
- <Button
644
- size="sm"
645
- variant="outline"
646
- onClick={() => setSearchHistory([])}
647
- >
648
- Clear History
649
- </Button>
650
- </div>
651
- </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>
652
412
 
653
- {/* Keyboard Shortcuts Info */}
654
- <div className="rounded-lg border border-white/10 bg-white/5 p-4">
655
- <h4 className="mb-2 text-sm font-medium text-white">
656
- Keyboard Shortcuts
657
- </h4>
658
- <div className="space-y-1 text-xs text-white/60">
659
- <div className="flex justify-between">
660
- <span>Focus search:</span>
661
- <kbd className="rounded bg-white/10 px-1 font-mono">Cmd+K</kbd>
662
- </div>
663
- <div className="flex justify-between">
664
- <span>Clear search:</span>
665
- <kbd className="rounded bg-white/10 px-1 font-mono">Esc</kbd>
666
- </div>
667
- <div className="flex justify-between">
668
- <span>Navigate results:</span>
669
- <kbd className="rounded bg-white/10 px-1 font-mono">↑↓</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>
670
417
  </div>
671
418
  </div>
672
419
  </div>
@@ -674,12 +421,4 @@ export const AdvancedFeatures: Story = {
674
421
  </div>
675
422
  )
676
423
  },
677
- parameters: {
678
- docs: {
679
- description: {
680
- story:
681
- "Advanced search features including search history, keyboard shortcuts, and enhanced user experience patterns.",
682
- },
683
- },
684
- },
685
424
  }