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.
- package/dist/components/aspect-ratio/AspectRatio.stories.tsx +290 -1199
- package/dist/components/avatar/Avatar.stories.tsx +235 -237
- package/dist/components/badge/Badge.stories.tsx +379 -116
- package/dist/components/banner/Banner.stories.tsx +445 -391
- package/dist/components/breadcrumb/Breadcrumb.stories.tsx +453 -199
- package/dist/components/button/Button.stories.tsx +585 -230
- package/dist/components/button/index.tsx +7 -7
- package/dist/components/card/Card.stories.tsx +619 -301
- package/dist/components/char-count/CharCount.stories.tsx +350 -248
- package/dist/components/checkbox/Checkbox.stories.tsx +309 -167
- package/dist/components/chip/Chip.stories.tsx +362 -168
- package/dist/components/circular-loader/CircularLoader.stories.tsx +221 -620
- package/dist/components/clamp-lines/ClampLines.stories.tsx +246 -117
- package/dist/components/collapsible/Collapsible.stories.tsx +391 -252
- package/dist/components/command/Command.stories.tsx +533 -856
- package/dist/components/dialog/Dialog.stories.tsx +505 -949
- package/dist/components/divider/Divider.stories.tsx +265 -502
- package/dist/components/dot-loader/DotLoader.stories.tsx +256 -257
- package/dist/components/drawer/Drawer.stories.tsx +659 -993
- package/dist/components/drawer/index.tsx +3 -3
- package/dist/components/dropdown/Dropdown.stories.tsx +643 -1018
- package/dist/components/form/Form.stories.tsx +560 -274
- package/dist/components/helper-text/HelperText.stories.tsx +199 -200
- package/dist/components/hover-card/HoverCard.stories.tsx +318 -1221
- package/dist/components/icon-button/IconButton.stories.tsx +837 -194
- package/dist/components/if-else/if-else.stories.tsx +370 -83
- package/dist/components/input/Input.stories.tsx +436 -368
- package/dist/components/label/Label.stories.tsx +156 -154
- package/dist/components/list/List.stories.tsx +485 -822
- package/dist/components/marquee/Marquee.stories.tsx +356 -694
- package/dist/components/otp-inputs/OtpInputs.stories.tsx +352 -410
- package/dist/components/overlay/Overlay.stories.tsx +452 -818
- package/dist/components/overlay/index.tsx +4 -4
- package/dist/components/pagination/Pagination.stories.tsx +721 -210
- package/dist/components/popover/Popover.stories.tsx +484 -873
- package/dist/components/radio/Radio.stories.tsx +432 -124
- package/dist/components/resizable/Resizable.stories.tsx +496 -752
- package/dist/components/scroll-area/ScrollArea.stories.tsx +384 -1006
- package/dist/components/search/Search.stories.tsx +314 -575
- package/dist/components/select/Select.stories.tsx +684 -787
- package/dist/components/sheet/Sheet.stories.tsx +671 -936
- package/dist/components/skelton/Skelton.stories.tsx +230 -764
- package/dist/components/slider/Slider.stories.tsx +384 -737
- package/dist/components/stepper/Stepper.stories.tsx +371 -514
- package/dist/components/switch/Switch.stories.tsx +461 -208
- package/dist/components/switch-case/SwitchCase.stories.tsx +367 -188
- package/dist/components/table/Table.stories.tsx +770 -914
- package/dist/components/tabs/Tabs.stories.tsx +459 -1400
- package/dist/components/tag/Tag.stories.tsx +714 -542
- package/dist/components/textarea/TextArea.stories.tsx +621 -562
- package/dist/components/thumbnail-tags/ThumbnailTags.stories.tsx +228 -148
- package/dist/components/toast/Toast.stories.tsx +452 -1333
- package/dist/components/toggle/Toggle.stories.tsx +488 -909
- package/dist/components/tooltip/Tooltip.stories.tsx +344 -1372
- package/dist/components/typography/Typography.stories.tsx +406 -89
- package/dist/hooks/use-change-state/UseChangeState.stories.tsx +309 -606
- package/dist/hooks/use-previous/UsePrevious.stories.tsx +367 -917
- package/dist/hooks/use-standalone-pagination/UseStandalonePagination.stories.tsx +639 -867
- package/dist/icons/Icons.stories.tsx +0 -12
- package/dist/icons/ai-avatar-icon/AiAvatarIcon.stories.tsx +226 -1013
- package/dist/icons/alert-icon/AlertIcon.stories.tsx +109 -929
- package/dist/icons/all-icons.tsx +124 -87
- package/dist/icons/angle-down-icon/AngleDownIcon.stories.tsx +140 -971
- package/dist/icons/apple-logo-icon/AppleLogoIcon.stories.tsx +148 -888
- package/dist/icons/arrow-box-left-icon/ArrowBoxLeftIcon.stories.tsx +135 -1019
- package/dist/icons/arrow-corner-up-left-icon/ArrowCornerUpLeftIcon.stories.tsx +137 -953
- package/dist/icons/arrow-corner-up-right-icon/ArrowCornerUpRightIcon.stories.tsx +138 -997
- package/dist/icons/arrow-left-icon/ArrowLeftIcon.stories.tsx +136 -942
- package/dist/icons/arrow-right-icon/ArrowRightIcon.stories.tsx +148 -1092
- package/dist/icons/arrow-right-up-icon/ArrowRightUpIcon.stories.tsx +146 -1211
- package/dist/icons/art-board-icon/ArtBoardIcon.stories.tsx +126 -615
- package/dist/icons/audio-bar-icon/AudioBarIcon.stories.tsx +144 -1164
- package/dist/icons/backward-ten-seconds-icon/BackwardTenSecondsIcon.stories.tsx +167 -985
- package/dist/icons/bubble-check-icon/BubbleCheckIcon.stories.tsx +122 -1179
- package/dist/icons/bubble-crossed-icon/BubbleCrossedIcon.stories.tsx +124 -1168
- package/dist/icons/bubble-sparkle-icon/BubbleSparkleIcon.stories.tsx +119 -850
- package/dist/icons/camera-icon/CameraIcon.stories.tsx +112 -1213
- package/dist/icons/capital-a-letter-icon/CapitalALetterIcon.stories.tsx +117 -934
- package/dist/icons/chevron-double-left-icon/ChevronDoubleLeftIcon.stories.tsx +160 -961
- package/dist/icons/chevron-double-right-icon/ChevronDoubleRightIcon.stories.tsx +163 -961
- package/dist/icons/chevron-down-icon/ChevronDownIcon.stories.tsx +144 -942
- package/dist/icons/chevron-left-icon/ChevronLeftIcon.stories.tsx +129 -966
- package/dist/icons/chevron-right-icon/ChevronRightIcon.stories.tsx +147 -964
- package/dist/icons/chevron-up-icon/ChevronUpIcon.stories.tsx +145 -975
- package/dist/icons/circle-tick-icon/CircleTickIcon.stories.tsx +150 -1142
- package/dist/icons/circular-play-icon/CircularPlayIcon.stories.tsx +114 -461
- package/dist/icons/coin-icon/CoinIcon.stories.tsx +124 -1322
- package/dist/icons/coin-toons-icon/CoinToonsIcon.stories.tsx +117 -1318
- package/dist/icons/column-wide-add-icon/ColumnWideAddIcon.stories.tsx +114 -903
- package/dist/icons/command-icon/CommandIcon.stories.tsx +127 -1042
- package/dist/icons/copy-icon/CopyIcon.stories.tsx +123 -962
- package/dist/icons/cross-circle-icon/CrossCircleIcon.stories.tsx +147 -999
- package/dist/icons/cross-icon/CrossIcon.stories.tsx +139 -960
- package/dist/icons/download-icon/DownloadIcon.stories.tsx +126 -820
- package/dist/icons/edit-big-icon/EditBigIcon.stories.tsx +124 -1031
- package/dist/icons/email-icon/EmailIcon.stories.tsx +115 -936
- package/dist/icons/expand-icon/ExpandIcon.stories.tsx +112 -1111
- package/dist/icons/eye-close-icon/EyeCloseIcon.stories.tsx +144 -1025
- package/dist/icons/eye-open-icon/EyeOpenIcon.stories.tsx +143 -1036
- package/dist/icons/feature-shine-icon/FeatureShineIcon.stories.tsx +127 -1011
- package/dist/icons/file-chart-icon/FileChartIcon.stories.tsx +126 -1056
- package/dist/icons/file-text-icon/FileTextIcon.stories.tsx +125 -614
- package/dist/icons/filter-bar-row-icon/FilterBarRowIcon.stories.tsx +119 -1050
- package/dist/icons/forward-ten-seconds-icon/ForwardTenSecondsIcon.stories.tsx +169 -989
- package/dist/icons/git-branch-icon/GitBranchIcon.stories.tsx +115 -1145
- package/dist/icons/git-fork-icon/GitForkIcon.stories.tsx +115 -1122
- package/dist/icons/globe-icon/GlobeIcon.stories.tsx +130 -313
- package/dist/icons/google-logo-icon/GoogleLogoIcon.stories.tsx +145 -940
- package/dist/icons/grip-vertical-icon/GripVerticalIcon.stories.tsx +119 -1174
- package/dist/icons/head-icon/HeadIcon.stories.tsx +111 -916
- package/dist/icons/heart-icon/HeartIcon.stories.tsx +120 -1019
- package/dist/icons/image-avatar-sparkle-icon/ImageAvatarSparkleIcon.stories.tsx +119 -683
- package/dist/icons/image-icon/ImageIcon.stories.tsx +105 -1121
- package/dist/icons/import-folder-icon/ImportFolderIcon.stories.tsx +111 -1192
- package/dist/icons/import-left-arrow-folder-icon/ImportLeftArrowFolderIcon.stories.tsx +136 -1256
- package/dist/icons/indian-flag-icon/IndianFlagIcon.stories.tsx +159 -962
- package/dist/icons/instagram-icon/InstagramIcon.stories.tsx +161 -1385
- package/dist/icons/layout-column-icon/LayoutColumnIcon.stories.tsx +124 -972
- package/dist/icons/layout-left-icon/LayoutLeftIcon.stories.tsx +119 -948
- package/dist/icons/layout-right-icon/LayoutRightIcon.stories.tsx +119 -942
- package/dist/icons/light-bulb-simple-icon/LightBulbSimpleIcon.stories.tsx +108 -1215
- package/dist/icons/linked-in-icon/LinkedInIcon.stories.tsx +154 -1517
- package/dist/icons/magic-book-icon/MagicBookIcon.stories.tsx +110 -1188
- package/dist/icons/magic-edit-icon/MagicEditIcon.stories.tsx +119 -678
- package/dist/icons/maintenance-icon/MaintenanceIcon.stories.tsx +123 -1184
- package/dist/icons/message-icon/MessageIcon.stories.tsx +114 -538
- package/dist/icons/minimize-icon/MinimizeIcon.stories.tsx +116 -1158
- package/dist/icons/moon-icon/MoonIcon.stories.tsx +120 -536
- package/dist/icons/move-horizontal-icon/MoveHorizontalIcon.stories.tsx +109 -1184
- package/dist/icons/move-vertical-icon/MoveVerticalIcon.stories.tsx +115 -1134
- package/dist/icons/musical-note-icon/MusicalNoteIcon.stories.tsx +119 -971
- package/dist/icons/notepad-icon/NotepadIcon.stories.tsx +111 -1100
- package/dist/icons/notes-icon/NotesIcon.stories.tsx +119 -1101
- package/dist/icons/page-search-icon/PageSearchIcon.stories.tsx +109 -1111
- package/dist/icons/page-text-icon/PageTextIcon.stories.tsx +122 -684
- package/dist/icons/paint-roll-icon/PaintRollIcon.stories.tsx +113 -954
- package/dist/icons/paper-plane-icon/PaperPlaneIcon.stories.tsx +112 -877
- package/dist/icons/pause-icon/PauseIcon.stories.tsx +113 -1000
- package/dist/icons/pencil-icon/PencilIcon.stories.tsx +115 -1070
- package/dist/icons/phone-icon/PhoneIcon.stories.tsx +115 -978
- package/dist/icons/plus-icon/PlusIcon.stories.tsx +106 -1093
- package/dist/icons/pocket-studio-icon/PocketStudioIcon.stories.tsx +107 -829
- package/dist/icons/scroll-down-icon/ScrollDownIcon.stories.tsx +102 -469
- package/dist/icons/search-icon/SearchIcon.stories.tsx +111 -1124
- package/dist/icons/setting-icon/SettingIcon.stories.tsx +107 -970
- package/dist/icons/share-icon/ShareIcon.stories.tsx +120 -1025
- package/dist/icons/shield-icon/ShieldIcon.stories.tsx +117 -931
- package/dist/icons/site-logo-icon/SiteLogoIcon.stories.tsx +137 -1104
- package/dist/icons/skip-backward-icon/SkipBackwardIcon.stories.tsx +172 -982
- package/dist/icons/skip-forward-icon/SkipForwardIcon.stories.tsx +164 -983
- package/dist/icons/sparkles-soft-icon/SparklesSoftIcon.stories.tsx +105 -958
- package/dist/icons/spinner-gradient-icon/SpinnerGradientIcon.stories.tsx +158 -580
- package/dist/icons/spinner-gradient-icon/index.tsx +6 -1
- package/dist/icons/spinner-solid-icon/SpinnerSolidIcon.stories.tsx +158 -587
- package/dist/icons/spinner-solid-icon/index.tsx +6 -1
- package/dist/icons/spinner-solid-neutral-icon/SpinnerSolidINeutralcon.stories.tsx +146 -682
- package/dist/icons/spinner-solid-neutral-icon/index.tsx +1 -1
- package/dist/icons/star-icon/StarIcon.stories.tsx +124 -904
- package/dist/icons/store-coin-icon/StoreCoinIcon.stories.tsx +112 -964
- package/dist/icons/suggestion-icon/SuggestionIcon.stories.tsx +116 -852
- package/dist/icons/sun-icon/SunIcon.stories.tsx +120 -831
- package/dist/icons/text-color-icon/TextColorIcon.stories.tsx +116 -950
- package/dist/icons/text-indicator-icon/TextIndicatorIcon.stories.tsx +123 -980
- package/dist/icons/threads-icon/ThreadsIcon.stories.tsx +156 -1427
- package/dist/icons/tick-circle-icon/TickCircleIcon.stories.tsx +146 -1142
- package/dist/icons/tick-icon/TickIcon.stories.tsx +145 -1276
- package/dist/icons/trash-icon/TrashIcon.stories.tsx +108 -933
- package/dist/icons/twitter-x-icon/TwitterXIcon.stories.tsx +157 -1402
- package/dist/icons/upload-icon/UploadIcon.stories.tsx +115 -889
- package/dist/icons/vertical-menu-icon/VerticalMenuIcon.stories.tsx +118 -984
- package/dist/icons/video-play-list-icon/VideoPlaylistIcon.stories.tsx +125 -1049
- package/dist/icons/voice-playing-icon/VoicePlayingIcon.stories.tsx +123 -1356
- package/dist/icons/volume-full-icon/VolumeFullIcon.stories.tsx +110 -1171
- package/dist/icons/volume-half-icon/VolumeHalfIcon.stories.tsx +112 -1093
- package/dist/icons/volume-off-icon/VolumeOffIcon.stories.tsx +115 -1087
- package/dist/icons/warning-icon/WarningIcon.stories.tsx +122 -1046
- package/dist/icons/youtube-icon/YoutubeIcon.stories.tsx +161 -936
- package/dist/index.cjs +84 -84
- package/dist/index.js +84 -84
- package/dist/styles/aural-all-theme.css +1222 -0
- package/dist/styles/{aural-theme.css → aural-dark-theme.css} +15 -3
- package/dist/styles/aural-light-theme.css +1047 -0
- 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
|
-
|
|
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
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
-
"
|
|
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 [
|
|
246
|
-
const [
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
<p className="text-
|
|
301
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
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
|
|
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-
|
|
459
|
-
<span className="text-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
473
|
-
value={
|
|
474
|
-
onChange={
|
|
475
|
-
onSearch={
|
|
176
|
+
placeholder="Search episodes"
|
|
177
|
+
value={noResultsQuery}
|
|
178
|
+
onChange={setNoResultsQuery}
|
|
179
|
+
onSearch={setNoResultsQuery}
|
|
476
180
|
>
|
|
477
|
-
{
|
|
478
|
-
<div className="mt-2 rounded-lg border
|
|
479
|
-
<div className="
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
|
509
|
-
value={
|
|
510
|
-
onChange={
|
|
511
|
-
onSearch={
|
|
207
|
+
placeholder="Search podcasts…"
|
|
208
|
+
value={resultsQuery}
|
|
209
|
+
onChange={setResultsQuery}
|
|
210
|
+
onSearch={setResultsQuery}
|
|
211
|
+
results={filteredResults}
|
|
512
212
|
>
|
|
513
|
-
{
|
|
514
|
-
<div className="
|
|
515
|
-
<div className="
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
>
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
"
|
|
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 [
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
])
|
|
564
|
-
const
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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-
|
|
575
|
-
<div className="
|
|
576
|
-
<
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
605
|
-
|
|
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={
|
|
385
|
+
key={r.id}
|
|
608
386
|
onClick={() => {
|
|
609
|
-
|
|
610
|
-
|
|
387
|
+
setSelected(r.text)
|
|
388
|
+
setQuery(r.text)
|
|
389
|
+
setResults([])
|
|
611
390
|
}}
|
|
612
|
-
className="
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
}
|