aural-ui 4.0.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/components/aspect-ratio/AspectRatio.stories.tsx +290 -1228
- package/dist/components/avatar/Avatar.stories.tsx +219 -235
- 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/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 -636
- 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 +530 -867
- package/dist/components/dialog/Dialog.stories.tsx +501 -950
- package/dist/components/divider/Divider.stories.tsx +264 -527
- package/dist/components/dot-loader/DotLoader.stories.tsx +256 -257
- package/dist/components/drawer/Drawer.stories.tsx +659 -1023
- package/dist/components/dropdown/Dropdown.stories.tsx +643 -1028
- 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 -1254
- 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 +484 -835
- package/dist/components/marquee/Marquee.stories.tsx +356 -712
- package/dist/components/otp-inputs/OtpInputs.stories.tsx +352 -422
- package/dist/components/overlay/Overlay.stories.tsx +452 -824
- package/dist/components/pagination/Pagination.stories.tsx +721 -210
- package/dist/components/popover/Popover.stories.tsx +481 -896
- package/dist/components/radio/Radio.stories.tsx +432 -124
- package/dist/components/resizable/Resizable.stories.tsx +495 -799
- package/dist/components/scroll-area/ScrollArea.stories.tsx +383 -1059
- package/dist/components/search/Search.stories.tsx +312 -595
- package/dist/components/select/Select.stories.tsx +684 -789
- package/dist/components/sheet/Sheet.stories.tsx +671 -950
- package/dist/components/skelton/Skelton.stories.tsx +230 -764
- package/dist/components/slider/Slider.stories.tsx +383 -760
- 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 -916
- package/dist/components/tabs/Tabs.stories.tsx +458 -1455
- 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 -154
- package/dist/components/toast/Toast.stories.tsx +452 -1339
- package/dist/components/toggle/Toggle.stories.tsx +488 -931
- package/dist/components/tooltip/Tooltip.stories.tsx +344 -1388
- 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 +223 -1060
- package/dist/icons/alert-icon/AlertIcon.stories.tsx +106 -968
- package/dist/icons/all-icons.tsx +37 -16
- package/dist/icons/angle-down-icon/AngleDownIcon.stories.tsx +137 -1010
- package/dist/icons/apple-logo-icon/AppleLogoIcon.stories.tsx +145 -935
- package/dist/icons/arrow-box-left-icon/ArrowBoxLeftIcon.stories.tsx +132 -1046
- package/dist/icons/arrow-corner-up-left-icon/ArrowCornerUpLeftIcon.stories.tsx +134 -986
- package/dist/icons/arrow-corner-up-right-icon/ArrowCornerUpRightIcon.stories.tsx +135 -1028
- package/dist/icons/arrow-left-icon/ArrowLeftIcon.stories.tsx +133 -971
- package/dist/icons/arrow-right-icon/ArrowRightIcon.stories.tsx +145 -1123
- package/dist/icons/arrow-right-up-icon/ArrowRightUpIcon.stories.tsx +143 -1252
- package/dist/icons/art-board-icon/ArtBoardIcon.stories.tsx +123 -632
- package/dist/icons/audio-bar-icon/AudioBarIcon.stories.tsx +141 -1223
- package/dist/icons/backward-ten-seconds-icon/BackwardTenSecondsIcon.stories.tsx +164 -1018
- package/dist/icons/bubble-check-icon/BubbleCheckIcon.stories.tsx +121 -1236
- package/dist/icons/bubble-crossed-icon/BubbleCrossedIcon.stories.tsx +121 -1213
- package/dist/icons/bubble-sparkle-icon/BubbleSparkleIcon.stories.tsx +116 -893
- package/dist/icons/camera-icon/CameraIcon.stories.tsx +109 -1254
- package/dist/icons/capital-a-letter-icon/CapitalALetterIcon.stories.tsx +114 -975
- package/dist/icons/chevron-double-left-icon/ChevronDoubleLeftIcon.stories.tsx +157 -994
- package/dist/icons/chevron-double-right-icon/ChevronDoubleRightIcon.stories.tsx +160 -992
- package/dist/icons/chevron-down-icon/ChevronDownIcon.stories.tsx +140 -970
- package/dist/icons/chevron-left-icon/ChevronLeftIcon.stories.tsx +126 -993
- package/dist/icons/chevron-right-icon/ChevronRightIcon.stories.tsx +144 -987
- package/dist/icons/chevron-up-icon/ChevronUpIcon.stories.tsx +141 -1007
- package/dist/icons/circle-tick-icon/CircleTickIcon.stories.tsx +147 -1187
- package/dist/icons/circular-play-icon/CircularPlayIcon.stories.tsx +110 -476
- package/dist/icons/coin-icon/CoinIcon.stories.tsx +120 -1364
- package/dist/icons/coin-toons-icon/CoinToonsIcon.stories.tsx +113 -1360
- package/dist/icons/column-wide-add-icon/ColumnWideAddIcon.stories.tsx +111 -942
- package/dist/icons/command-icon/CommandIcon.stories.tsx +124 -1087
- package/dist/icons/copy-icon/CopyIcon.stories.tsx +119 -996
- package/dist/icons/cross-circle-icon/CrossCircleIcon.stories.tsx +144 -1046
- package/dist/icons/cross-icon/CrossIcon.stories.tsx +136 -999
- package/dist/icons/download-icon/DownloadIcon.stories.tsx +123 -857
- package/dist/icons/edit-big-icon/EditBigIcon.stories.tsx +121 -1080
- package/dist/icons/email-icon/EmailIcon.stories.tsx +112 -979
- package/dist/icons/expand-icon/ExpandIcon.stories.tsx +109 -1146
- package/dist/icons/eye-close-icon/EyeCloseIcon.stories.tsx +141 -1068
- package/dist/icons/eye-open-icon/EyeOpenIcon.stories.tsx +140 -1081
- package/dist/icons/feature-shine-icon/FeatureShineIcon.stories.tsx +124 -1050
- package/dist/icons/file-chart-icon/FileChartIcon.stories.tsx +123 -1091
- package/dist/icons/file-text-icon/FileTextIcon.stories.tsx +122 -633
- package/dist/icons/filter-bar-row-icon/FilterBarRowIcon.stories.tsx +116 -1087
- package/dist/icons/forward-ten-seconds-icon/ForwardTenSecondsIcon.stories.tsx +166 -1020
- package/dist/icons/git-branch-icon/GitBranchIcon.stories.tsx +112 -1182
- package/dist/icons/git-fork-icon/GitForkIcon.stories.tsx +112 -1155
- package/dist/icons/globe-icon/GlobeIcon.stories.tsx +127 -325
- package/dist/icons/google-logo-icon/GoogleLogoIcon.stories.tsx +142 -985
- package/dist/icons/grip-vertical-icon/GripVerticalIcon.stories.tsx +116 -1217
- package/dist/icons/head-icon/HeadIcon.stories.tsx +108 -953
- package/dist/icons/heart-icon/HeartIcon.stories.tsx +117 -1060
- package/dist/icons/image-avatar-sparkle-icon/ImageAvatarSparkleIcon.stories.tsx +116 -716
- package/dist/icons/image-icon/ImageIcon.stories.tsx +102 -1164
- package/dist/icons/import-folder-icon/ImportFolderIcon.stories.tsx +108 -1233
- package/dist/icons/import-left-arrow-folder-icon/ImportLeftArrowFolderIcon.stories.tsx +133 -1289
- package/dist/icons/indian-flag-icon/IndianFlagIcon.stories.tsx +155 -1012
- package/dist/icons/instagram-icon/InstagramIcon.stories.tsx +158 -1438
- package/dist/icons/layout-column-icon/LayoutColumnIcon.stories.tsx +121 -1011
- package/dist/icons/layout-left-icon/LayoutLeftIcon.stories.tsx +116 -981
- package/dist/icons/layout-right-icon/LayoutRightIcon.stories.tsx +116 -979
- package/dist/icons/light-bulb-simple-icon/LightBulbSimpleIcon.stories.tsx +105 -1252
- package/dist/icons/linked-in-icon/LinkedInIcon.stories.tsx +151 -1554
- package/dist/icons/magic-book-icon/MagicBookIcon.stories.tsx +107 -1227
- package/dist/icons/magic-edit-icon/MagicEditIcon.stories.tsx +116 -707
- package/dist/icons/maintenance-icon/MaintenanceIcon.stories.tsx +119 -1226
- package/dist/icons/message-icon/MessageIcon.stories.tsx +111 -557
- package/dist/icons/minimize-icon/MinimizeIcon.stories.tsx +112 -1198
- package/dist/icons/moon-icon/MoonIcon.stories.tsx +117 -557
- package/dist/icons/move-horizontal-icon/MoveHorizontalIcon.stories.tsx +106 -1235
- package/dist/icons/move-vertical-icon/MoveVerticalIcon.stories.tsx +112 -1185
- package/dist/icons/musical-note-icon/MusicalNoteIcon.stories.tsx +116 -1012
- package/dist/icons/notepad-icon/NotepadIcon.stories.tsx +108 -1137
- package/dist/icons/notes-icon/NotesIcon.stories.tsx +116 -1138
- package/dist/icons/page-search-icon/PageSearchIcon.stories.tsx +106 -1146
- package/dist/icons/page-text-icon/PageTextIcon.stories.tsx +119 -719
- package/dist/icons/paint-roll-icon/PaintRollIcon.stories.tsx +110 -999
- package/dist/icons/paper-plane-icon/PaperPlaneIcon.stories.tsx +109 -912
- package/dist/icons/pause-icon/PauseIcon.stories.tsx +110 -1041
- package/dist/icons/pencil-icon/PencilIcon.stories.tsx +112 -1109
- package/dist/icons/phone-icon/PhoneIcon.stories.tsx +112 -1023
- package/dist/icons/plus-icon/PlusIcon.stories.tsx +103 -1132
- package/dist/icons/pocket-studio-icon/PocketStudioIcon.stories.tsx +104 -870
- package/dist/icons/scroll-down-icon/ScrollDownIcon.stories.tsx +99 -476
- package/dist/icons/search-icon/SearchIcon.stories.tsx +108 -1161
- package/dist/icons/setting-icon/SettingIcon.stories.tsx +104 -1009
- package/dist/icons/share-icon/ShareIcon.stories.tsx +117 -1064
- package/dist/icons/shield-icon/ShieldIcon.stories.tsx +114 -974
- package/dist/icons/site-logo-icon/SiteLogoIcon.stories.tsx +134 -1160
- package/dist/icons/skip-backward-icon/SkipBackwardIcon.stories.tsx +169 -1017
- package/dist/icons/skip-forward-icon/SkipForwardIcon.stories.tsx +161 -1016
- package/dist/icons/sparkles-soft-icon/SparklesSoftIcon.stories.tsx +102 -1001
- package/dist/icons/spinner-gradient-icon/SpinnerGradientIcon.stories.tsx +155 -593
- package/dist/icons/spinner-solid-icon/SpinnerSolidIcon.stories.tsx +155 -608
- package/dist/icons/spinner-solid-neutral-icon/SpinnerSolidINeutralcon.stories.tsx +142 -712
- package/dist/icons/star-icon/StarIcon.stories.tsx +120 -946
- package/dist/icons/store-coin-icon/StoreCoinIcon.stories.tsx +109 -1013
- package/dist/icons/suggestion-icon/SuggestionIcon.stories.tsx +113 -891
- package/dist/icons/sun-icon/SunIcon.stories.tsx +117 -864
- package/dist/icons/text-color-icon/TextColorIcon.stories.tsx +113 -989
- package/dist/icons/text-indicator-icon/TextIndicatorIcon.stories.tsx +120 -1027
- package/dist/icons/threads-icon/ThreadsIcon.stories.tsx +153 -1476
- package/dist/icons/tick-circle-icon/TickCircleIcon.stories.tsx +143 -1187
- package/dist/icons/tick-icon/TickIcon.stories.tsx +142 -1322
- package/dist/icons/trash-icon/TrashIcon.stories.tsx +105 -970
- package/dist/icons/twitter-x-icon/TwitterXIcon.stories.tsx +154 -1457
- package/dist/icons/upload-icon/UploadIcon.stories.tsx +112 -930
- package/dist/icons/vertical-menu-icon/VerticalMenuIcon.stories.tsx +115 -1019
- package/dist/icons/video-play-list-icon/VideoPlaylistIcon.stories.tsx +122 -1092
- package/dist/icons/voice-playing-icon/VoicePlayingIcon.stories.tsx +120 -1401
- package/dist/icons/volume-full-icon/VolumeFullIcon.stories.tsx +107 -1212
- package/dist/icons/volume-half-icon/VolumeHalfIcon.stories.tsx +109 -1122
- package/dist/icons/volume-off-icon/VolumeOffIcon.stories.tsx +112 -1124
- package/dist/icons/warning-icon/WarningIcon.stories.tsx +119 -1083
- package/dist/icons/youtube-icon/YoutubeIcon.stories.tsx +158 -983
- package/dist/index.cjs +90 -90
- package/dist/index.js +90 -90
- package/package.json +8 -3
|
@@ -1,694 +1,419 @@
|
|
|
1
|
-
import React, { useState } from "react"
|
|
2
|
-
import { Button } from "@components/button"
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react"
|
|
3
2
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
4
3
|
|
|
4
|
+
import { AuralComponentDocsPage } from "src/ui/story-spec/components/component-story-docs-page"
|
|
5
|
+
|
|
5
6
|
import Search, { SearchResult } from "."
|
|
6
7
|
|
|
8
|
+
// ─── Meta ─────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
7
10
|
const meta: Meta<typeof Search> = {
|
|
8
11
|
title: "Components/UI/Search",
|
|
9
12
|
component: Search,
|
|
10
13
|
parameters: {
|
|
11
14
|
layout: "centered",
|
|
12
|
-
backgrounds: {
|
|
13
|
-
default: "dark",
|
|
14
|
-
values: [
|
|
15
|
-
{ name: "dark", value: "#0a0a0a" },
|
|
16
|
-
{ name: "light", value: "#ffffff" },
|
|
17
|
-
],
|
|
18
|
-
},
|
|
19
15
|
docs: {
|
|
20
16
|
description: {
|
|
21
|
-
component:
|
|
22
|
-
|
|
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
|
-
</div>
|
|
109
|
-
|
|
110
|
-
{/* Custom Placeholders */}
|
|
111
|
-
<div className="space-y-2">
|
|
112
|
-
<label className="text-fm-tertiary text-sm font-medium">
|
|
113
|
-
Custom Placeholders
|
|
114
|
-
</label>
|
|
115
|
-
<div className="space-y-3">
|
|
116
|
-
<Search placeholder="Search podcasts..." />
|
|
117
|
-
<Search placeholder="Find your favorite shows" />
|
|
118
|
-
<Search placeholder="Type to search music" />
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
{/* With Initial Value */}
|
|
123
|
-
<div className="space-y-2">
|
|
124
|
-
<label className="text-fm-tertiary text-sm font-medium">
|
|
125
|
-
With Initial Value
|
|
126
|
-
</label>
|
|
127
|
-
<Search placeholder="Search episodes" initialValue="The Daily" />
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
),
|
|
55
|
+
// ─── Mock data ────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const ALL_PODCASTS = [
|
|
58
|
+
{ id: "1", text: "The Daily" },
|
|
59
|
+
{ id: "2", text: "Serial" },
|
|
60
|
+
{ id: "3", text: "This American Life" },
|
|
61
|
+
{ id: "4", text: "Stuff You Should Know" },
|
|
62
|
+
{ id: "5", text: "Crime Junkie" },
|
|
63
|
+
{ id: "6", text: "Conan O'Brien Needs a Friend" },
|
|
64
|
+
{ id: "7", text: "The Tim Ferriss Show" },
|
|
65
|
+
{ id: "8", text: "My Favorite Murder" },
|
|
66
|
+
{ id: "9", text: "Hidden Brain" },
|
|
67
|
+
{ id: "10", text: "Radiolab" },
|
|
68
|
+
{ id: "11", text: "How I Built This" },
|
|
69
|
+
{ id: "12", text: "Fresh Air" },
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
// ─── 1. Playground ────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export const Playground: Story = {
|
|
132
75
|
parameters: {
|
|
133
76
|
docs: {
|
|
134
77
|
description: {
|
|
135
78
|
story:
|
|
136
|
-
"
|
|
79
|
+
"Controls-driven story. Use the Storybook sidebar to adjust placeholder, autoFocus, and value. The search input renders with the current args applied.",
|
|
137
80
|
},
|
|
138
81
|
},
|
|
139
82
|
},
|
|
83
|
+
render: (args) => (
|
|
84
|
+
<div className="w-80">
|
|
85
|
+
<Search {...args} />
|
|
86
|
+
</div>
|
|
87
|
+
),
|
|
140
88
|
}
|
|
141
89
|
|
|
142
|
-
// 2.
|
|
143
|
-
export const ControlledSearch: Story = {
|
|
144
|
-
render: () => {
|
|
145
|
-
const [searchValue1, setSearchValue1] = useState("")
|
|
146
|
-
const [searchValue2, setSearchValue2] = useState("Controlled")
|
|
147
|
-
const [searchValue3, setSearchValue3] = useState("")
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
<div className="space-y-6">
|
|
151
|
-
<div className="text-center">
|
|
152
|
-
<h3 className="text-fm-primary mb-2 font-medium">
|
|
153
|
-
Controlled Search
|
|
154
|
-
</h3>
|
|
155
|
-
<p className="text-fm-secondary text-sm">
|
|
156
|
-
Search components with external state control
|
|
157
|
-
</p>
|
|
158
|
-
</div>
|
|
159
|
-
|
|
160
|
-
<div className="space-y-6">
|
|
161
|
-
{/* Basic Controlled */}
|
|
162
|
-
<div className="space-y-3">
|
|
163
|
-
<div className="flex items-center justify-between">
|
|
164
|
-
<label className="text-fm-tertiary text-sm font-medium">
|
|
165
|
-
Basic Controlled
|
|
166
|
-
</label>
|
|
167
|
-
<div className="text-fm-secondary text-xs">
|
|
168
|
-
Value: "{searchValue1}"
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
<Search
|
|
172
|
-
placeholder="Type something..."
|
|
173
|
-
value={searchValue1}
|
|
174
|
-
onChange={setSearchValue1}
|
|
175
|
-
/>
|
|
176
|
-
<div className="flex gap-2">
|
|
177
|
-
<Button
|
|
178
|
-
size="sm"
|
|
179
|
-
variant="outline"
|
|
180
|
-
onClick={() => setSearchValue1("Preset Value")}
|
|
181
|
-
>
|
|
182
|
-
Set Value
|
|
183
|
-
</Button>
|
|
184
|
-
<Button
|
|
185
|
-
size="sm"
|
|
186
|
-
variant="outline"
|
|
187
|
-
onClick={() => setSearchValue1("")}
|
|
188
|
-
>
|
|
189
|
-
Clear
|
|
190
|
-
</Button>
|
|
191
|
-
</div>
|
|
192
|
-
</div>
|
|
193
|
-
|
|
194
|
-
{/* Pre-filled Controlled */}
|
|
195
|
-
<div className="space-y-3">
|
|
196
|
-
<div className="flex items-center justify-between">
|
|
197
|
-
<label className="text-fm-tertiary text-sm font-medium">
|
|
198
|
-
Pre-filled Controlled
|
|
199
|
-
</label>
|
|
200
|
-
<div className="text-fm-secondary text-xs">
|
|
201
|
-
Value: "{searchValue2}"
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
<Search
|
|
205
|
-
placeholder="Search with preset value"
|
|
206
|
-
value={searchValue2}
|
|
207
|
-
onChange={setSearchValue2}
|
|
208
|
-
/>
|
|
209
|
-
</div>
|
|
90
|
+
// ─── 2. States ────────────────────────────────────────────────────────────────
|
|
210
91
|
|
|
211
|
-
|
|
212
|
-
<div className="space-y-3">
|
|
213
|
-
<div className="flex items-center justify-between">
|
|
214
|
-
<label className="text-fm-tertiary text-sm font-medium">
|
|
215
|
-
With Validation
|
|
216
|
-
</label>
|
|
217
|
-
<div className="text-fm-secondary text-xs">
|
|
218
|
-
Length: {searchValue3.length}/20
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
221
|
-
<Search
|
|
222
|
-
placeholder="Max 20 characters"
|
|
223
|
-
value={searchValue3}
|
|
224
|
-
onChange={(value) => {
|
|
225
|
-
if (value.length <= 20) {
|
|
226
|
-
setSearchValue3(value)
|
|
227
|
-
}
|
|
228
|
-
}}
|
|
229
|
-
/>
|
|
230
|
-
{searchValue3.length >= 20 && (
|
|
231
|
-
<p className="text-fm-negative text-xs">Maximum length reached</p>
|
|
232
|
-
)}
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
)
|
|
237
|
-
},
|
|
92
|
+
export const States: Story = {
|
|
238
93
|
parameters: {
|
|
239
94
|
docs: {
|
|
240
95
|
description: {
|
|
241
96
|
story:
|
|
242
|
-
"
|
|
97
|
+
"Five distinct states: empty (idle), has query (clear button visible), loading (async in-progress), no results (empty results set), and with results (list rendered below).",
|
|
243
98
|
},
|
|
244
99
|
},
|
|
245
100
|
},
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// 3. Uncontrolled Component Examples
|
|
249
|
-
export const UncontrolledSearch: Story = {
|
|
250
101
|
render: () => {
|
|
251
|
-
const [
|
|
252
|
-
const [
|
|
102
|
+
const [loadingQuery, setLoadingQuery] = useState("")
|
|
103
|
+
const [noResultsQuery, setNoResultsQuery] = useState("nonexistentshow")
|
|
104
|
+
const [resultsQuery, setResultsQuery] = useState("The")
|
|
253
105
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
</h3>
|
|
260
|
-
<p className="text-fm-secondary text-sm">
|
|
261
|
-
Search components with internal state management
|
|
262
|
-
</p>
|
|
263
|
-
</div>
|
|
106
|
+
const filteredResults: SearchResult[] = resultsQuery
|
|
107
|
+
? ALL_PODCASTS.filter((p) =>
|
|
108
|
+
p.text.toLowerCase().includes(resultsQuery.toLowerCase())
|
|
109
|
+
)
|
|
110
|
+
: []
|
|
264
111
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<Search
|
|
277
|
-
placeholder="Search internally managed"
|
|
278
|
-
onSearch={setLastSearch1}
|
|
279
|
-
/>
|
|
280
|
-
</div>
|
|
281
|
-
|
|
282
|
-
{/* With Initial Value */}
|
|
283
|
-
<div className="space-y-3">
|
|
284
|
-
<div className="flex items-center justify-between">
|
|
285
|
-
<label className="text-fm-tertiary text-sm font-medium">
|
|
286
|
-
With Initial Value
|
|
287
|
-
</label>
|
|
288
|
-
<div className="text-fm-secondary text-xs">
|
|
289
|
-
Last search: "{lastSearch2}"
|
|
290
|
-
</div>
|
|
291
|
-
</div>
|
|
292
|
-
<Search
|
|
293
|
-
placeholder="Search with initial value"
|
|
294
|
-
initialValue="Initial Search"
|
|
295
|
-
onSearch={setLastSearch2}
|
|
296
|
-
/>
|
|
297
|
-
</div>
|
|
298
|
-
|
|
299
|
-
{/* Multiple Independent */}
|
|
300
|
-
<div className="space-y-3">
|
|
301
|
-
<label className="text-fm-tertiary text-sm font-medium">
|
|
302
|
-
Multiple Independent
|
|
303
|
-
</label>
|
|
304
|
-
<div className="grid grid-cols-2 gap-3">
|
|
305
|
-
<Search placeholder="Search A" />
|
|
306
|
-
<Search placeholder="Search B" />
|
|
307
|
-
</div>
|
|
308
|
-
<p className="text-fm-secondary text-xs">
|
|
309
|
-
Each search maintains its own independent state
|
|
112
|
+
return (
|
|
113
|
+
<div className="w-80 space-y-8">
|
|
114
|
+
{/* Empty */}
|
|
115
|
+
<div>
|
|
116
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
|
|
117
|
+
Empty
|
|
118
|
+
</h4>
|
|
119
|
+
<div className="space-y-2 text-center">
|
|
120
|
+
<Search placeholder="Search episodes" />
|
|
121
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
122
|
+
Idle — no input yet
|
|
310
123
|
</p>
|
|
311
124
|
</div>
|
|
312
125
|
</div>
|
|
313
|
-
</div>
|
|
314
|
-
)
|
|
315
|
-
},
|
|
316
|
-
parameters: {
|
|
317
|
-
docs: {
|
|
318
|
-
description: {
|
|
319
|
-
story:
|
|
320
|
-
"Uncontrolled search component examples with internal state management and initial values.",
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// 4. Interactive Search with Results
|
|
327
|
-
export const InteractiveSearch: Story = {
|
|
328
|
-
render: () => {
|
|
329
|
-
const [query, setQuery] = useState("")
|
|
330
|
-
const [results, setResults] = useState<SearchResult[]>([])
|
|
331
|
-
const [selectedResult, setSelectedResult] = useState<string | null>(null)
|
|
332
|
-
|
|
333
|
-
// Mock search data
|
|
334
|
-
const allPodcasts = [
|
|
335
|
-
{ id: "1", text: "The Joe Rogan Experience" },
|
|
336
|
-
{ id: "2", text: "Serial" },
|
|
337
|
-
{ id: "3", text: "This American Life" },
|
|
338
|
-
{ id: "4", text: "Stuff You Should Know" },
|
|
339
|
-
{ id: "5", text: "The Daily" },
|
|
340
|
-
{ id: "6", text: "Crime Junkie" },
|
|
341
|
-
{ id: "7", text: "The Michelle Obama Podcast" },
|
|
342
|
-
{ id: "8", text: "Call Her Daddy" },
|
|
343
|
-
{ id: "9", text: "My Favorite Murder" },
|
|
344
|
-
{ id: "10", text: "The Tim Ferriss Show" },
|
|
345
|
-
{ id: "11", text: "Conan O'Brien Needs a Friend" },
|
|
346
|
-
{ id: "12", text: "The Ben Shapiro Show" },
|
|
347
|
-
]
|
|
348
|
-
|
|
349
|
-
const handleSearch = (searchQuery: string) => {
|
|
350
|
-
setQuery(searchQuery)
|
|
351
|
-
|
|
352
|
-
if (!searchQuery.trim()) {
|
|
353
|
-
setResults([])
|
|
354
|
-
return
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Filter podcasts based on query
|
|
358
|
-
const filteredResults = allPodcasts.filter((podcast) =>
|
|
359
|
-
podcast.text.toLowerCase().includes(searchQuery.toLowerCase())
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
setResults(filteredResults)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return (
|
|
366
|
-
<div className="w-96 space-y-4">
|
|
367
|
-
<div className="text-center">
|
|
368
|
-
<h3 className="text-fm-primary mb-2 font-medium">
|
|
369
|
-
Interactive Search
|
|
370
|
-
</h3>
|
|
371
|
-
<p className="text-fm-secondary text-sm">
|
|
372
|
-
Real-time search with custom results rendering
|
|
373
|
-
</p>
|
|
374
|
-
</div>
|
|
375
126
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
onSearch={handleSearch}
|
|
381
|
-
results={results}
|
|
382
|
-
>
|
|
383
|
-
{/* Custom Results Rendering */}
|
|
384
|
-
{results.length > 0 && (
|
|
385
|
-
<div className="border-fm-divider-secondary bg-fm-surface-primary mt-2 rounded-lg border shadow-xl">
|
|
386
|
-
<div className="p-3">
|
|
387
|
-
<div className="mb-2 flex items-center justify-between">
|
|
388
|
-
<span className="text-fm-tertiary text-xs font-medium">
|
|
389
|
-
Search Results
|
|
390
|
-
</span>
|
|
391
|
-
<span className="text-fm-secondary text-xs">
|
|
392
|
-
{results.length} found
|
|
393
|
-
</span>
|
|
394
|
-
</div>
|
|
395
|
-
<div className="max-h-64 space-y-1 overflow-y-auto">
|
|
396
|
-
{results.map((result) => (
|
|
397
|
-
<button
|
|
398
|
-
key={result.id}
|
|
399
|
-
onClick={() => {
|
|
400
|
-
setSelectedResult(result.text)
|
|
401
|
-
setQuery(result.text)
|
|
402
|
-
setResults([])
|
|
403
|
-
}}
|
|
404
|
-
className="text-fm-primary hover:bg-fm-surface-secondary w-full rounded px-3 py-2 text-left text-sm"
|
|
405
|
-
>
|
|
406
|
-
{result.text}
|
|
407
|
-
</button>
|
|
408
|
-
))}
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
|
-
</div>
|
|
412
|
-
)}
|
|
413
|
-
</Search>
|
|
414
|
-
|
|
415
|
-
{/* Search Info */}
|
|
416
|
-
<div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border p-4">
|
|
417
|
-
<h4 className="text-fm-primary mb-2 text-sm font-medium">
|
|
418
|
-
Search Info
|
|
127
|
+
{/* Has query */}
|
|
128
|
+
<div>
|
|
129
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
|
|
130
|
+
Has Query
|
|
419
131
|
</h4>
|
|
420
|
-
<div className="
|
|
421
|
-
<
|
|
422
|
-
<
|
|
423
|
-
|
|
132
|
+
<div className="space-y-2 text-center">
|
|
133
|
+
<Search placeholder="Search episodes" initialValue="The Daily" />
|
|
134
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
135
|
+
Clear button visible when input has value
|
|
136
|
+
</p>
|
|
424
137
|
</div>
|
|
425
138
|
</div>
|
|
426
|
-
</div>
|
|
427
|
-
)
|
|
428
|
-
},
|
|
429
|
-
parameters: {
|
|
430
|
-
docs: {
|
|
431
|
-
description: {
|
|
432
|
-
story:
|
|
433
|
-
"Interactive search example with real-time filtering, custom results rendering, and selection handling.",
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
|
-
},
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// 5. Search with Different States
|
|
440
|
-
export const SearchStates: Story = {
|
|
441
|
-
render: () => {
|
|
442
|
-
const [loadingQuery, setLoadingQuery] = useState("")
|
|
443
|
-
const [errorQuery, setErrorQuery] = useState("")
|
|
444
|
-
const [emptyQuery, setEmptyQuery] = useState("nonexistent")
|
|
445
139
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
</p>
|
|
453
|
-
</div>
|
|
454
|
-
|
|
455
|
-
<div className="grid gap-6">
|
|
456
|
-
{/* Loading State */}
|
|
457
|
-
<div className="space-y-3">
|
|
458
|
-
<label className="text-fm-tertiary text-sm font-medium">
|
|
459
|
-
Loading State
|
|
460
|
-
</label>
|
|
140
|
+
{/* Loading */}
|
|
141
|
+
<div>
|
|
142
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
|
|
143
|
+
Loading
|
|
144
|
+
</h4>
|
|
145
|
+
<div className="space-y-2 text-center">
|
|
461
146
|
<Search
|
|
462
|
-
placeholder="Search
|
|
147
|
+
placeholder="Search episodes"
|
|
463
148
|
value={loadingQuery}
|
|
464
149
|
onChange={setLoadingQuery}
|
|
465
150
|
onSearch={setLoadingQuery}
|
|
466
151
|
>
|
|
467
152
|
{loadingQuery && (
|
|
468
|
-
<div className="border-fm-divider-secondary bg-fm-surface-
|
|
153
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border p-4 text-center">
|
|
469
154
|
<div className="flex items-center justify-center gap-2">
|
|
470
|
-
<div className="border-fm-divider-
|
|
471
|
-
<span className="text-fm-
|
|
472
|
-
Searching
|
|
155
|
+
<div className="border-fm-divider-contrast h-4 w-4 animate-spin rounded-full border-2 border-t-transparent" />
|
|
156
|
+
<span className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
157
|
+
Searching…
|
|
473
158
|
</span>
|
|
474
159
|
</div>
|
|
475
160
|
</div>
|
|
476
161
|
)}
|
|
477
162
|
</Search>
|
|
163
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
164
|
+
Type anything to trigger loading state
|
|
165
|
+
</p>
|
|
478
166
|
</div>
|
|
167
|
+
</div>
|
|
479
168
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
169
|
+
{/* No results */}
|
|
170
|
+
<div>
|
|
171
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
|
|
172
|
+
No Results
|
|
173
|
+
</h4>
|
|
174
|
+
<div className="space-y-2 text-center">
|
|
485
175
|
<Search
|
|
486
|
-
placeholder="Search
|
|
487
|
-
value={
|
|
488
|
-
onChange={
|
|
489
|
-
onSearch={
|
|
176
|
+
placeholder="Search episodes"
|
|
177
|
+
value={noResultsQuery}
|
|
178
|
+
onChange={setNoResultsQuery}
|
|
179
|
+
onSearch={setNoResultsQuery}
|
|
490
180
|
>
|
|
491
|
-
{
|
|
492
|
-
<div className="border-fm-divider-secondary bg-fm-surface-
|
|
493
|
-
<div className="
|
|
494
|
-
<
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
>
|
|
500
|
-
<path
|
|
501
|
-
strokeLinecap="round"
|
|
502
|
-
strokeLinejoin="round"
|
|
503
|
-
strokeWidth={2}
|
|
504
|
-
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
505
|
-
/>
|
|
506
|
-
</svg>
|
|
507
|
-
<span className="text-fm-negative text-sm">
|
|
508
|
-
Search failed. Please try again.
|
|
509
|
-
</span>
|
|
181
|
+
{noResultsQuery && (
|
|
182
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border p-4 text-center">
|
|
183
|
+
<div className="space-y-1">
|
|
184
|
+
<p className="text-fm-primary font-fm-text text-fm-sm leading-fm-sm font-medium">
|
|
185
|
+
No results found
|
|
186
|
+
</p>
|
|
187
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
188
|
+
Try different keywords or check your spelling.
|
|
189
|
+
</p>
|
|
510
190
|
</div>
|
|
511
191
|
</div>
|
|
512
192
|
)}
|
|
513
193
|
</Search>
|
|
194
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
195
|
+
Pre-filled with a term that matches nothing
|
|
196
|
+
</p>
|
|
514
197
|
</div>
|
|
198
|
+
</div>
|
|
515
199
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
200
|
+
{/* With results */}
|
|
201
|
+
<div>
|
|
202
|
+
<h4 className="text-fm-secondary font-fm-text text-fm-md leading-fm-md mb-3 font-medium">
|
|
203
|
+
With Results
|
|
204
|
+
</h4>
|
|
205
|
+
<div className="space-y-2 text-center">
|
|
521
206
|
<Search
|
|
522
|
-
placeholder="Search
|
|
523
|
-
value={
|
|
524
|
-
onChange={
|
|
525
|
-
onSearch={
|
|
207
|
+
placeholder="Search podcasts…"
|
|
208
|
+
value={resultsQuery}
|
|
209
|
+
onChange={setResultsQuery}
|
|
210
|
+
onSearch={setResultsQuery}
|
|
211
|
+
results={filteredResults}
|
|
526
212
|
>
|
|
527
|
-
{
|
|
528
|
-
<div className="border-fm-divider-secondary bg-fm-surface-
|
|
529
|
-
<div className="
|
|
530
|
-
<
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
>
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
</p>
|
|
547
|
-
<p className="text-fm-secondary text-xs">
|
|
548
|
-
Try different keywords or check your spelling
|
|
549
|
-
</p>
|
|
550
|
-
</div>
|
|
213
|
+
{filteredResults.length > 0 && (
|
|
214
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border">
|
|
215
|
+
<div className="flex items-center justify-between px-3 pt-3 pb-1">
|
|
216
|
+
<span className="text-fm-tertiary font-fm-text text-fm-sm leading-fm-sm">
|
|
217
|
+
Results
|
|
218
|
+
</span>
|
|
219
|
+
<span className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
220
|
+
{filteredResults.length} found
|
|
221
|
+
</span>
|
|
222
|
+
</div>
|
|
223
|
+
<div className="max-h-48 overflow-y-auto pb-2">
|
|
224
|
+
{filteredResults.map((r) => (
|
|
225
|
+
<button
|
|
226
|
+
key={r.id}
|
|
227
|
+
className="text-fm-primary font-fm-text text-fm-sm leading-fm-sm hover:bg-fm-surface-primary w-full px-3 py-2 text-left transition-colors"
|
|
228
|
+
>
|
|
229
|
+
{r.text}
|
|
230
|
+
</button>
|
|
231
|
+
))}
|
|
551
232
|
</div>
|
|
552
233
|
</div>
|
|
553
234
|
)}
|
|
554
235
|
</Search>
|
|
236
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
237
|
+
Pre-filled with "The" — shows matching results
|
|
238
|
+
</p>
|
|
555
239
|
</div>
|
|
556
240
|
</div>
|
|
557
241
|
</div>
|
|
558
242
|
)
|
|
559
243
|
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── 3. Interactive ───────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
export const Interactive: Story = {
|
|
560
249
|
parameters: {
|
|
561
250
|
docs: {
|
|
562
251
|
description: {
|
|
563
252
|
story:
|
|
564
|
-
"
|
|
253
|
+
"Live search with debounce simulation. Type in the search box to filter the podcast list. Results appear after a short delay (300 ms) to mimic a real API call. Selecting a result populates the input.",
|
|
565
254
|
},
|
|
566
255
|
},
|
|
567
256
|
},
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// 6. Advanced Search Features
|
|
571
|
-
export const AdvancedFeatures: Story = {
|
|
572
257
|
render: () => {
|
|
573
|
-
const [
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
])
|
|
578
|
-
const
|
|
579
|
-
const [showHistory, setShowHistory] = useState(false)
|
|
258
|
+
const [query, setQuery] = useState("")
|
|
259
|
+
const [debouncedQuery, setDebouncedQuery] = useState("")
|
|
260
|
+
const [isSearching, setIsSearching] = useState(false)
|
|
261
|
+
const [results, setResults] = useState<SearchResult[]>([])
|
|
262
|
+
const [selected, setSelected] = useState<string | null>(null)
|
|
263
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
580
264
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
265
|
+
// Debounced search simulation
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (!query.trim()) {
|
|
268
|
+
setResults([])
|
|
269
|
+
setDebouncedQuery("")
|
|
270
|
+
setIsSearching(false)
|
|
271
|
+
return
|
|
584
272
|
}
|
|
585
|
-
|
|
273
|
+
|
|
274
|
+
setIsSearching(true)
|
|
275
|
+
|
|
276
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
277
|
+
timerRef.current = setTimeout(() => {
|
|
278
|
+
const filtered = ALL_PODCASTS.filter((p) =>
|
|
279
|
+
p.text.toLowerCase().includes(query.toLowerCase())
|
|
280
|
+
)
|
|
281
|
+
setResults(filtered)
|
|
282
|
+
setDebouncedQuery(query)
|
|
283
|
+
setIsSearching(false)
|
|
284
|
+
}, 300)
|
|
285
|
+
|
|
286
|
+
return () => {
|
|
287
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
288
|
+
}
|
|
289
|
+
}, [query])
|
|
586
290
|
|
|
587
291
|
return (
|
|
588
|
-
<div className="w-
|
|
589
|
-
<div className="
|
|
590
|
-
<
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
292
|
+
<div className="w-full p-8">
|
|
293
|
+
<div className="mx-auto max-w-3xl space-y-6">
|
|
294
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
295
|
+
{/* Controls panel */}
|
|
296
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary space-y-5 rounded-xl border p-5">
|
|
297
|
+
<p className="text-fm-primary font-fm-brand text-fm-sm leading-fm-sm font-semibold tracking-widest uppercase">
|
|
298
|
+
Search State
|
|
299
|
+
</p>
|
|
300
|
+
|
|
301
|
+
<div className="border-fm-divider-secondary bg-fm-surface-primary space-y-3 rounded-lg border p-3">
|
|
302
|
+
<div>
|
|
303
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm font-medium">
|
|
304
|
+
Query
|
|
305
|
+
</p>
|
|
306
|
+
<p className="text-fm-primary font--(--font-fm-mono) text-fm-sm leading-fm-sm break-all">
|
|
307
|
+
{query || "(empty)"}
|
|
308
|
+
</p>
|
|
309
|
+
</div>
|
|
310
|
+
<div>
|
|
311
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm font-medium">
|
|
312
|
+
Debounced
|
|
313
|
+
</p>
|
|
314
|
+
<p className="text-fm-primary font--(--font-fm-mono) text-fm-sm leading-fm-sm break-all">
|
|
315
|
+
{debouncedQuery || "(empty)"}
|
|
316
|
+
</p>
|
|
317
|
+
</div>
|
|
318
|
+
<div>
|
|
319
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm font-medium">
|
|
320
|
+
Results
|
|
321
|
+
</p>
|
|
322
|
+
<p className="text-fm-primary font--(--font-fm-mono) text-fm-sm leading-fm-sm">
|
|
323
|
+
{isSearching ? "searching…" : results.length}
|
|
324
|
+
</p>
|
|
325
|
+
</div>
|
|
326
|
+
<div>
|
|
327
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm font-medium">
|
|
328
|
+
Selected
|
|
329
|
+
</p>
|
|
330
|
+
<p className="text-fm-primary font--(--font-fm-mono) text-fm-sm leading-fm-sm break-all">
|
|
331
|
+
{selected ?? "(none)"}
|
|
332
|
+
</p>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
597
335
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
336
|
+
<div className="border-fm-divider-secondary border-t pt-4" />
|
|
337
|
+
|
|
338
|
+
<button
|
|
339
|
+
onClick={() => {
|
|
340
|
+
setQuery("")
|
|
341
|
+
setResults([])
|
|
342
|
+
setSelected(null)
|
|
343
|
+
}}
|
|
344
|
+
className="border-fm-divider-secondary text-fm-primary font-fm-text text-fm-sm leading-fm-sm hover:bg-fm-surface-primary w-full rounded-lg border px-3 py-2 text-left transition-colors"
|
|
345
|
+
>
|
|
346
|
+
Clear all
|
|
347
|
+
</button>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{/* Preview stage */}
|
|
351
|
+
<div className="flex flex-col gap-3 lg:col-span-2">
|
|
352
|
+
<Search
|
|
353
|
+
placeholder="Search podcasts…"
|
|
354
|
+
value={query}
|
|
355
|
+
onChange={setQuery}
|
|
356
|
+
onSearch={setQuery}
|
|
357
|
+
results={results}
|
|
358
|
+
>
|
|
359
|
+
{/* Loading state */}
|
|
360
|
+
{isSearching && (
|
|
361
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border p-4 text-center">
|
|
362
|
+
<div className="flex items-center justify-center gap-2">
|
|
363
|
+
<div className="border-fm-divider-contrast h-4 w-4 animate-spin rounded-full border-2 border-t-transparent" />
|
|
364
|
+
<span className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
365
|
+
Searching…
|
|
366
|
+
</span>
|
|
619
367
|
</div>
|
|
620
|
-
|
|
621
|
-
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
{/* Results list */}
|
|
372
|
+
{!isSearching && query && results.length > 0 && (
|
|
373
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border">
|
|
374
|
+
<div className="flex items-center justify-between px-3 pt-3 pb-1">
|
|
375
|
+
<span className="text-fm-tertiary font-fm-text text-fm-sm leading-fm-sm">
|
|
376
|
+
Podcasts
|
|
377
|
+
</span>
|
|
378
|
+
<span className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm">
|
|
379
|
+
{results.length} result{results.length !== 1 ? "s" : ""}
|
|
380
|
+
</span>
|
|
381
|
+
</div>
|
|
382
|
+
<div className="max-h-56 overflow-y-auto pb-2">
|
|
383
|
+
{results.map((r) => (
|
|
622
384
|
<button
|
|
623
|
-
key={
|
|
385
|
+
key={r.id}
|
|
624
386
|
onClick={() => {
|
|
625
|
-
|
|
626
|
-
|
|
387
|
+
setSelected(r.text)
|
|
388
|
+
setQuery(r.text)
|
|
389
|
+
setResults([])
|
|
627
390
|
}}
|
|
628
|
-
className="text-fm-primary hover:bg-fm-surface-
|
|
391
|
+
className="text-fm-primary font-fm-text text-fm-sm leading-fm-sm hover:bg-fm-surface-primary w-full px-3 py-2.5 text-left transition-colors"
|
|
629
392
|
>
|
|
630
|
-
|
|
631
|
-
className="text-fm-secondary h-3 w-3"
|
|
632
|
-
fill="none"
|
|
633
|
-
stroke="currentColor"
|
|
634
|
-
viewBox="0 0 24 24"
|
|
635
|
-
>
|
|
636
|
-
<path
|
|
637
|
-
strokeLinecap="round"
|
|
638
|
-
strokeLinejoin="round"
|
|
639
|
-
strokeWidth={2}
|
|
640
|
-
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
641
|
-
/>
|
|
642
|
-
</svg>
|
|
643
|
-
{item}
|
|
393
|
+
{r.text}
|
|
644
394
|
</button>
|
|
645
395
|
))}
|
|
646
396
|
</div>
|
|
647
397
|
</div>
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
onClick={() => setSearchHistory([])}
|
|
663
|
-
>
|
|
664
|
-
Clear History
|
|
665
|
-
</Button>
|
|
666
|
-
</div>
|
|
667
|
-
</div>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
{/* No results */}
|
|
401
|
+
{!isSearching && query && results.length === 0 && (
|
|
402
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary mt-2 rounded-lg border p-4 text-center">
|
|
403
|
+
<p className="text-fm-primary font-fm-text text-fm-sm leading-fm-sm font-medium">
|
|
404
|
+
No results for "{query}"
|
|
405
|
+
</p>
|
|
406
|
+
<p className="text-fm-secondary font-fm-text text-fm-sm leading-fm-sm mt-1">
|
|
407
|
+
Try a different search term.
|
|
408
|
+
</p>
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
</Search>
|
|
668
412
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
</h4>
|
|
674
|
-
<div className="text-fm-secondary space-y-1 text-xs">
|
|
675
|
-
<div className="flex justify-between">
|
|
676
|
-
<span>Focus search:</span>
|
|
677
|
-
<kbd className="bg-fm-surface-secondary rounded px-1 font-mono">
|
|
678
|
-
Cmd+K
|
|
679
|
-
</kbd>
|
|
680
|
-
</div>
|
|
681
|
-
<div className="flex justify-between">
|
|
682
|
-
<span>Clear search:</span>
|
|
683
|
-
<kbd className="bg-fm-surface-secondary rounded px-1 font-mono">
|
|
684
|
-
Esc
|
|
685
|
-
</kbd>
|
|
686
|
-
</div>
|
|
687
|
-
<div className="flex justify-between">
|
|
688
|
-
<span>Navigate results:</span>
|
|
689
|
-
<kbd className="bg-fm-surface-secondary rounded px-1 font-mono">
|
|
690
|
-
↑↓
|
|
691
|
-
</kbd>
|
|
413
|
+
<div className="border-fm-divider-secondary bg-fm-surface-secondary rounded-lg border px-4 py-3">
|
|
414
|
+
<code className="text-fm-secondary font--(--font-fm-mono) text-fm-md leading-fm-md">
|
|
415
|
+
{`<Search value="${query}" onChange={setQuery} results={[…]} />`}
|
|
416
|
+
</code>
|
|
692
417
|
</div>
|
|
693
418
|
</div>
|
|
694
419
|
</div>
|
|
@@ -696,12 +421,4 @@ export const AdvancedFeatures: Story = {
|
|
|
696
421
|
</div>
|
|
697
422
|
)
|
|
698
423
|
},
|
|
699
|
-
parameters: {
|
|
700
|
-
docs: {
|
|
701
|
-
description: {
|
|
702
|
-
story:
|
|
703
|
-
"Advanced search features including search history, keyboard shortcuts, and enhanced user experience patterns.",
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
},
|
|
707
424
|
}
|