@delightui/components 0.1.109 → 0.1.111
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/cjs/components/molecules/Search/Search.d.ts +18 -0
- package/dist/cjs/components/molecules/Search/Search.presenter.d.ts +320 -0
- package/dist/cjs/components/molecules/Search/Search.types.d.ts +53 -0
- package/dist/cjs/components/molecules/Search/index.d.ts +2 -0
- package/dist/cjs/components/molecules/index.d.ts +2 -0
- package/dist/cjs/components/utils/RenderStateView/RenderStateView.types.d.ts +4 -0
- package/dist/cjs/components/utils/RenderStateView/usePresenter.d.ts +1 -0
- package/dist/cjs/components/utils/index.d.ts +2 -0
- package/dist/cjs/components/utils/useDebounce/index.d.ts +1 -0
- package/dist/cjs/components/utils/useDebounce/useDebounce.d.ts +10 -0
- package/dist/cjs/components/utils/useInflateView/index.d.ts +0 -1
- package/dist/cjs/components/utils/useInflateView/useInflateView.d.ts +1 -1
- package/dist/cjs/library.css +66 -0
- package/dist/cjs/library.js +3 -3
- package/dist/cjs/library.js.map +1 -1
- package/dist/esm/components/molecules/Search/Search.d.ts +18 -0
- package/dist/esm/components/molecules/Search/Search.presenter.d.ts +320 -0
- package/dist/esm/components/molecules/Search/Search.types.d.ts +53 -0
- package/dist/esm/components/molecules/Search/index.d.ts +2 -0
- package/dist/esm/components/molecules/index.d.ts +2 -0
- package/dist/esm/components/utils/RenderStateView/RenderStateView.types.d.ts +4 -0
- package/dist/esm/components/utils/RenderStateView/usePresenter.d.ts +1 -0
- package/dist/esm/components/utils/index.d.ts +2 -0
- package/dist/esm/components/utils/useDebounce/index.d.ts +1 -0
- package/dist/esm/components/utils/useDebounce/useDebounce.d.ts +10 -0
- package/dist/esm/components/utils/useInflateView/index.d.ts +0 -1
- package/dist/esm/components/utils/useInflateView/useInflateView.d.ts +1 -1
- package/dist/esm/library.css +66 -0
- package/dist/esm/library.js +3 -3
- package/dist/esm/library.js.map +1 -1
- package/dist/index.d.ts +83 -10
- package/docs/README.md +6 -0
- package/docs/components/atoms/Input.md +0 -63
- package/docs/components/molecules/Search.md +710 -0
- package/docs/components/utils/RenderStateView.md +137 -38
- package/docs/components/utils/useDebounce.md +576 -0
- package/package.json +1 -1
- package/dist/cjs/components/utils/useInflateView/useInflateView.types.d.ts +0 -12
- package/dist/esm/components/utils/useInflateView/useInflateView.types.d.ts +0 -12
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
# Search
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
A versatile search input component that supports both automatic and manual search modes. In auto mode, searches are triggered automatically with debouncing as the user types. In manual mode, searches are triggered only when the user presses Enter or clicks the search button. The component includes built-in search, clear, and loading states with full keyboard navigation support.
|
|
6
|
+
|
|
7
|
+
## Aliases
|
|
8
|
+
|
|
9
|
+
- Search
|
|
10
|
+
- SearchInput
|
|
11
|
+
- SearchField
|
|
12
|
+
- SearchBox
|
|
13
|
+
- QueryInput
|
|
14
|
+
|
|
15
|
+
## Props Breakdown
|
|
16
|
+
|
|
17
|
+
**Extends:** `ControlledFormComponentProps<string>` & `Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'value'>`
|
|
18
|
+
|
|
19
|
+
| Prop | Type | Default | Required | Description |
|
|
20
|
+
|------|------|---------|----------|-------------|
|
|
21
|
+
| `mode` | `'Auto' \| 'Manual'` | `'Auto'` | No | Search mode - 'Auto' for debounced search on input, 'Manual' for search on enter/submit |
|
|
22
|
+
| `onSearch` | `SearchCallback` | - | Yes | Callback function to handle search with the query string |
|
|
23
|
+
| `debounceMs` | `number` | `300` | No | Debounce delay in milliseconds for auto mode |
|
|
24
|
+
| `minCharacters` | `number` | `1` | No | Minimum characters required to trigger search in auto mode |
|
|
25
|
+
| `showSubmitButton` | `boolean` | `true` | No | Show submit button in manual mode |
|
|
26
|
+
| `showClearButton` | `boolean` | `true` | No | Show clear button when there is text |
|
|
27
|
+
| `loading` | `boolean` | `false` | No | Loading state while search is in progress |
|
|
28
|
+
| `component-variant` | `string` | - | No | Provide a way to override the styling |
|
|
29
|
+
|
|
30
|
+
## Examples
|
|
31
|
+
|
|
32
|
+
### Basic Auto Search
|
|
33
|
+
```tsx
|
|
34
|
+
import { Search } from '@delightui/components';
|
|
35
|
+
import { useState } from 'react';
|
|
36
|
+
|
|
37
|
+
function BasicAutoSearch() {
|
|
38
|
+
const [results, setResults] = useState([]);
|
|
39
|
+
const [loading, setLoading] = useState(false);
|
|
40
|
+
|
|
41
|
+
const handleSearch = async (query: string) => {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
try {
|
|
44
|
+
// Simulate API call
|
|
45
|
+
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
setResults(data.results);
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<Search
|
|
56
|
+
mode="Auto"
|
|
57
|
+
onSearch={handleSearch}
|
|
58
|
+
loading={loading}
|
|
59
|
+
placeholder="Search products..."
|
|
60
|
+
minCharacters={2}
|
|
61
|
+
debounceMs={500}
|
|
62
|
+
/>
|
|
63
|
+
{results.length > 0 && (
|
|
64
|
+
<div className="search-results">
|
|
65
|
+
{results.map(result => (
|
|
66
|
+
<div key={result.id}>{result.name}</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Manual Search with Validation
|
|
76
|
+
```tsx
|
|
77
|
+
function ManualSearchExample() {
|
|
78
|
+
const [query, setQuery] = useState('');
|
|
79
|
+
const [results, setResults] = useState([]);
|
|
80
|
+
const [error, setError] = useState('');
|
|
81
|
+
|
|
82
|
+
const handleSearch = async (searchQuery: string) => {
|
|
83
|
+
if (searchQuery.length < 3) {
|
|
84
|
+
setError('Search query must be at least 3 characters');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setError('');
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`);
|
|
91
|
+
if (!response.ok) throw new Error('Search failed');
|
|
92
|
+
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
setResults(data.results);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
setError('Search failed. Please try again.');
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div>
|
|
102
|
+
<Search
|
|
103
|
+
mode="Manual"
|
|
104
|
+
value={query}
|
|
105
|
+
onValueChange={setQuery}
|
|
106
|
+
onSearch={handleSearch}
|
|
107
|
+
placeholder="Enter search terms and press Enter..."
|
|
108
|
+
minCharacters={3}
|
|
109
|
+
showSubmitButton={true}
|
|
110
|
+
/>
|
|
111
|
+
{error && <div className="error">{error}</div>}
|
|
112
|
+
{results.length > 0 && (
|
|
113
|
+
<div className="results-count">
|
|
114
|
+
Found {results.length} results
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Advanced Search with Filters
|
|
123
|
+
```tsx
|
|
124
|
+
function AdvancedSearchExample() {
|
|
125
|
+
const [searchState, setSearchState] = useState({
|
|
126
|
+
query: '',
|
|
127
|
+
filters: {
|
|
128
|
+
category: '',
|
|
129
|
+
priceRange: '',
|
|
130
|
+
sortBy: 'relevance'
|
|
131
|
+
},
|
|
132
|
+
results: [],
|
|
133
|
+
loading: false,
|
|
134
|
+
hasSearched: false
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const performSearch = async (query: string) => {
|
|
138
|
+
setSearchState(prev => ({ ...prev, loading: true }));
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const params = new URLSearchParams({
|
|
142
|
+
q: query,
|
|
143
|
+
category: searchState.filters.category,
|
|
144
|
+
priceRange: searchState.filters.priceRange,
|
|
145
|
+
sortBy: searchState.filters.sortBy
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const response = await fetch(`/api/search?${params}`);
|
|
149
|
+
const data = await response.json();
|
|
150
|
+
|
|
151
|
+
setSearchState(prev => ({
|
|
152
|
+
...prev,
|
|
153
|
+
results: data.results,
|
|
154
|
+
loading: false,
|
|
155
|
+
hasSearched: true
|
|
156
|
+
}));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
setSearchState(prev => ({
|
|
159
|
+
...prev,
|
|
160
|
+
loading: false,
|
|
161
|
+
hasSearched: true,
|
|
162
|
+
results: []
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const updateFilter = (key: string, value: string) => {
|
|
168
|
+
setSearchState(prev => ({
|
|
169
|
+
...prev,
|
|
170
|
+
filters: { ...prev.filters, [key]: value }
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
// Re-search if we have a query
|
|
174
|
+
if (searchState.query) {
|
|
175
|
+
performSearch(searchState.query);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="advanced-search">
|
|
181
|
+
<div className="search-header">
|
|
182
|
+
<Search
|
|
183
|
+
mode="Auto"
|
|
184
|
+
value={searchState.query}
|
|
185
|
+
onValueChange={(query) => setSearchState(prev => ({ ...prev, query }))}
|
|
186
|
+
onSearch={performSearch}
|
|
187
|
+
loading={searchState.loading}
|
|
188
|
+
placeholder="Search for products..."
|
|
189
|
+
debounceMs={400}
|
|
190
|
+
minCharacters={2}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="search-filters">
|
|
195
|
+
<Select
|
|
196
|
+
value={searchState.filters.category}
|
|
197
|
+
onValueChange={(value) => updateFilter('category', value)}
|
|
198
|
+
placeholder="All Categories"
|
|
199
|
+
>
|
|
200
|
+
<Option value="">All Categories</Option>
|
|
201
|
+
<Option value="electronics">Electronics</Option>
|
|
202
|
+
<Option value="clothing">Clothing</Option>
|
|
203
|
+
<Option value="books">Books</Option>
|
|
204
|
+
</Select>
|
|
205
|
+
|
|
206
|
+
<Select
|
|
207
|
+
value={searchState.filters.priceRange}
|
|
208
|
+
onValueChange={(value) => updateFilter('priceRange', value)}
|
|
209
|
+
placeholder="Any Price"
|
|
210
|
+
>
|
|
211
|
+
<Option value="">Any Price</Option>
|
|
212
|
+
<Option value="0-25">Under $25</Option>
|
|
213
|
+
<Option value="25-100">$25 - $100</Option>
|
|
214
|
+
<Option value="100+">Over $100</Option>
|
|
215
|
+
</Select>
|
|
216
|
+
|
|
217
|
+
<Select
|
|
218
|
+
value={searchState.filters.sortBy}
|
|
219
|
+
onValueChange={(value) => updateFilter('sortBy', value)}
|
|
220
|
+
>
|
|
221
|
+
<Option value="relevance">Relevance</Option>
|
|
222
|
+
<Option value="price-low">Price: Low to High</Option>
|
|
223
|
+
<Option value="price-high">Price: High to Low</Option>
|
|
224
|
+
<Option value="newest">Newest First</Option>
|
|
225
|
+
</Select>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div className="search-results">
|
|
229
|
+
{searchState.loading && <Spinner />}
|
|
230
|
+
|
|
231
|
+
{searchState.hasSearched && !searchState.loading && (
|
|
232
|
+
<>
|
|
233
|
+
<div className="results-summary">
|
|
234
|
+
{searchState.results.length > 0 ? (
|
|
235
|
+
<Text type="BodyMedium">
|
|
236
|
+
Found {searchState.results.length} results for "{searchState.query}"
|
|
237
|
+
</Text>
|
|
238
|
+
) : (
|
|
239
|
+
<Text type="BodyMedium">
|
|
240
|
+
No results found for "{searchState.query}"
|
|
241
|
+
</Text>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div className="results-grid">
|
|
246
|
+
{searchState.results.map(product => (
|
|
247
|
+
<ProductCard key={product.id} product={product} />
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
</>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### User/Contact Search
|
|
259
|
+
```tsx
|
|
260
|
+
function UserSearchExample() {
|
|
261
|
+
const [selectedUsers, setSelectedUsers] = useState([]);
|
|
262
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
263
|
+
const [searching, setSearching] = useState(false);
|
|
264
|
+
|
|
265
|
+
const searchUsers = async (query: string) => {
|
|
266
|
+
if (!query.trim()) {
|
|
267
|
+
setSearchResults([]);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
setSearching(true);
|
|
272
|
+
try {
|
|
273
|
+
const response = await fetch(`/api/users/search?q=${encodeURIComponent(query)}`);
|
|
274
|
+
const users = await response.json();
|
|
275
|
+
|
|
276
|
+
// Filter out already selected users
|
|
277
|
+
const availableUsers = users.filter(
|
|
278
|
+
user => !selectedUsers.find(selected => selected.id === user.id)
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
setSearchResults(availableUsers);
|
|
282
|
+
} finally {
|
|
283
|
+
setSearching(false);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const addUser = (user) => {
|
|
288
|
+
setSelectedUsers(prev => [...prev, user]);
|
|
289
|
+
setSearchResults([]);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const removeUser = (userId) => {
|
|
293
|
+
setSelectedUsers(prev => prev.filter(user => user.id !== userId));
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="user-search">
|
|
298
|
+
<div className="search-section">
|
|
299
|
+
<Text type="Heading6">Add Team Members</Text>
|
|
300
|
+
<Search
|
|
301
|
+
mode="Auto"
|
|
302
|
+
onSearch={searchUsers}
|
|
303
|
+
loading={searching}
|
|
304
|
+
placeholder="Search by name or email..."
|
|
305
|
+
minCharacters={2}
|
|
306
|
+
debounceMs={300}
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{searchResults.length > 0 && (
|
|
311
|
+
<div className="search-results">
|
|
312
|
+
<Text type="BodySmall">Search Results:</Text>
|
|
313
|
+
{searchResults.map(user => (
|
|
314
|
+
<div key={user.id} className="user-result">
|
|
315
|
+
<div className="user-info">
|
|
316
|
+
<img src={user.avatar} alt={user.name} className="avatar" />
|
|
317
|
+
<div>
|
|
318
|
+
<Text type="BodyMedium">{user.name}</Text>
|
|
319
|
+
<Text type="BodySmall">{user.email}</Text>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
<Button size="Small" onClick={() => addUser(user)}>
|
|
323
|
+
Add
|
|
324
|
+
</Button>
|
|
325
|
+
</div>
|
|
326
|
+
))}
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{selectedUsers.length > 0 && (
|
|
331
|
+
<div className="selected-users">
|
|
332
|
+
<Text type="BodySmall">Selected Members ({selectedUsers.length}):</Text>
|
|
333
|
+
<div className="user-chips">
|
|
334
|
+
{selectedUsers.map(user => (
|
|
335
|
+
<Chip
|
|
336
|
+
key={user.id}
|
|
337
|
+
onRemove={() => removeUser(user.id)}
|
|
338
|
+
>
|
|
339
|
+
{user.name}
|
|
340
|
+
</Chip>
|
|
341
|
+
))}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Document/File Search
|
|
351
|
+
```tsx
|
|
352
|
+
function DocumentSearchExample() {
|
|
353
|
+
const [searchState, setSearchState] = useState({
|
|
354
|
+
query: '',
|
|
355
|
+
documents: [],
|
|
356
|
+
loading: false,
|
|
357
|
+
selectedDoc: null
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const searchDocuments = async (query: string) => {
|
|
361
|
+
setSearchState(prev => ({ ...prev, loading: true }));
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const response = await fetch(`/api/documents/search`, {
|
|
365
|
+
method: 'POST',
|
|
366
|
+
headers: { 'Content-Type': 'application/json' },
|
|
367
|
+
body: JSON.stringify({
|
|
368
|
+
query,
|
|
369
|
+
filters: {
|
|
370
|
+
fileTypes: ['pdf', 'doc', 'txt'],
|
|
371
|
+
dateRange: 'last-month'
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const results = await response.json();
|
|
377
|
+
setSearchState(prev => ({
|
|
378
|
+
...prev,
|
|
379
|
+
documents: results.documents,
|
|
380
|
+
loading: false
|
|
381
|
+
}));
|
|
382
|
+
} catch (error) {
|
|
383
|
+
setSearchState(prev => ({
|
|
384
|
+
...prev,
|
|
385
|
+
documents: [],
|
|
386
|
+
loading: false
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const openDocument = (doc) => {
|
|
392
|
+
setSearchState(prev => ({ ...prev, selectedDoc: doc }));
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<div className="document-search">
|
|
397
|
+
<div className="search-bar">
|
|
398
|
+
<Search
|
|
399
|
+
mode="Auto"
|
|
400
|
+
value={searchState.query}
|
|
401
|
+
onValueChange={(query) => setSearchState(prev => ({ ...prev, query }))}
|
|
402
|
+
onSearch={searchDocuments}
|
|
403
|
+
loading={searchState.loading}
|
|
404
|
+
placeholder="Search documents, files, and content..."
|
|
405
|
+
debounceMs={600}
|
|
406
|
+
minCharacters={3}
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<div className="search-results">
|
|
411
|
+
{searchState.documents.length > 0 && (
|
|
412
|
+
<>
|
|
413
|
+
<div className="results-header">
|
|
414
|
+
<Text type="BodyMedium">
|
|
415
|
+
{searchState.documents.length} documents found
|
|
416
|
+
</Text>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div className="document-list">
|
|
420
|
+
{searchState.documents.map(doc => (
|
|
421
|
+
<div key={doc.id} className="document-item">
|
|
422
|
+
<div className="doc-icon">
|
|
423
|
+
<Icon icon={getFileIcon(doc.type)} />
|
|
424
|
+
</div>
|
|
425
|
+
<div className="doc-info">
|
|
426
|
+
<Text type="BodyMedium">{doc.title}</Text>
|
|
427
|
+
<Text type="BodySmall">{doc.summary}</Text>
|
|
428
|
+
<div className="doc-meta">
|
|
429
|
+
<Text type="BodySmall">
|
|
430
|
+
{doc.type.toUpperCase()} • {formatFileSize(doc.size)} • {formatDate(doc.modified)}
|
|
431
|
+
</Text>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
<div className="doc-actions">
|
|
435
|
+
<Button size="Small" onClick={() => openDocument(doc)}>
|
|
436
|
+
Open
|
|
437
|
+
</Button>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
))}
|
|
441
|
+
</div>
|
|
442
|
+
</>
|
|
443
|
+
)}
|
|
444
|
+
|
|
445
|
+
{searchState.query && searchState.documents.length === 0 && !searchState.loading && (
|
|
446
|
+
<div className="no-results">
|
|
447
|
+
<Text type="BodyMedium">No documents found for "{searchState.query}"</Text>
|
|
448
|
+
<Text type="BodySmall">Try different keywords or check your spelling</Text>
|
|
449
|
+
</div>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{searchState.selectedDoc && (
|
|
454
|
+
<Modal onClose={() => setSearchState(prev => ({ ...prev, selectedDoc: null }))}>
|
|
455
|
+
<ModalHeader>
|
|
456
|
+
<Text type="Heading4">{searchState.selectedDoc.title}</Text>
|
|
457
|
+
</ModalHeader>
|
|
458
|
+
<div className="document-preview">
|
|
459
|
+
{/* Document content preview */}
|
|
460
|
+
</div>
|
|
461
|
+
</Modal>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Search with Recent History
|
|
469
|
+
```tsx
|
|
470
|
+
function SearchWithHistoryExample() {
|
|
471
|
+
const [searchHistory, setSearchHistory] = useState([]);
|
|
472
|
+
const [showHistory, setShowHistory] = useState(false);
|
|
473
|
+
const [currentQuery, setCurrentQuery] = useState('');
|
|
474
|
+
const [results, setResults] = useState([]);
|
|
475
|
+
|
|
476
|
+
const addToHistory = (query: string) => {
|
|
477
|
+
if (!query.trim()) return;
|
|
478
|
+
|
|
479
|
+
setSearchHistory(prev => {
|
|
480
|
+
const filtered = prev.filter(item => item !== query);
|
|
481
|
+
return [query, ...filtered].slice(0, 5); // Keep last 5 searches
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const performSearch = async (query: string) => {
|
|
486
|
+
if (!query.trim()) return;
|
|
487
|
+
|
|
488
|
+
addToHistory(query);
|
|
489
|
+
setShowHistory(false);
|
|
490
|
+
|
|
491
|
+
// Perform actual search
|
|
492
|
+
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
|
493
|
+
const data = await response.json();
|
|
494
|
+
setResults(data.results);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const selectFromHistory = (query: string) => {
|
|
498
|
+
setCurrentQuery(query);
|
|
499
|
+
performSearch(query);
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const clearHistory = () => {
|
|
503
|
+
setSearchHistory([]);
|
|
504
|
+
setShowHistory(false);
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
return (
|
|
508
|
+
<div className="search-with-history">
|
|
509
|
+
<div className="search-container">
|
|
510
|
+
<Search
|
|
511
|
+
mode="Manual"
|
|
512
|
+
value={currentQuery}
|
|
513
|
+
onValueChange={setCurrentQuery}
|
|
514
|
+
onSearch={performSearch}
|
|
515
|
+
placeholder="Search... (Press Enter)"
|
|
516
|
+
onFocus={() => setShowHistory(true)}
|
|
517
|
+
/>
|
|
518
|
+
|
|
519
|
+
{showHistory && searchHistory.length > 0 && (
|
|
520
|
+
<div className="search-history">
|
|
521
|
+
<div className="history-header">
|
|
522
|
+
<Text type="BodySmall">Recent Searches</Text>
|
|
523
|
+
<Button size="Small" type="Ghost" onClick={clearHistory}>
|
|
524
|
+
Clear
|
|
525
|
+
</Button>
|
|
526
|
+
</div>
|
|
527
|
+
<div className="history-items">
|
|
528
|
+
{searchHistory.map((query, index) => (
|
|
529
|
+
<div
|
|
530
|
+
key={index}
|
|
531
|
+
className="history-item"
|
|
532
|
+
onClick={() => selectFromHistory(query)}
|
|
533
|
+
>
|
|
534
|
+
<Icon icon="Search" size="Small" />
|
|
535
|
+
<Text type="BodyMedium">{query}</Text>
|
|
536
|
+
</div>
|
|
537
|
+
))}
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div className="search-results">
|
|
544
|
+
{results.map(result => (
|
|
545
|
+
<div key={result.id} className="result-item">
|
|
546
|
+
{/* Result content */}
|
|
547
|
+
</div>
|
|
548
|
+
))}
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Real-time Search with Suggestions
|
|
556
|
+
```tsx
|
|
557
|
+
function RealTimeSearchExample() {
|
|
558
|
+
const [searchState, setSearchState] = useState({
|
|
559
|
+
query: '',
|
|
560
|
+
suggestions: [],
|
|
561
|
+
results: [],
|
|
562
|
+
showSuggestions: false,
|
|
563
|
+
loading: false
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const fetchSuggestions = async (query: string) => {
|
|
567
|
+
if (query.length < 2) {
|
|
568
|
+
setSearchState(prev => ({ ...prev, suggestions: [], showSuggestions: false }));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
const response = await fetch(`/api/search/suggestions?q=${encodeURIComponent(query)}`);
|
|
574
|
+
const data = await response.json();
|
|
575
|
+
|
|
576
|
+
setSearchState(prev => ({
|
|
577
|
+
...prev,
|
|
578
|
+
suggestions: data.suggestions,
|
|
579
|
+
showSuggestions: true
|
|
580
|
+
}));
|
|
581
|
+
} catch (error) {
|
|
582
|
+
console.error('Failed to fetch suggestions:', error);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const performFullSearch = async (query: string) => {
|
|
587
|
+
setSearchState(prev => ({
|
|
588
|
+
...prev,
|
|
589
|
+
loading: true,
|
|
590
|
+
showSuggestions: false
|
|
591
|
+
}));
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
|
595
|
+
const data = await response.json();
|
|
596
|
+
|
|
597
|
+
setSearchState(prev => ({
|
|
598
|
+
...prev,
|
|
599
|
+
results: data.results,
|
|
600
|
+
loading: false
|
|
601
|
+
}));
|
|
602
|
+
} catch (error) {
|
|
603
|
+
setSearchState(prev => ({
|
|
604
|
+
...prev,
|
|
605
|
+
results: [],
|
|
606
|
+
loading: false
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const handleQueryChange = (query: string) => {
|
|
612
|
+
setSearchState(prev => ({ ...prev, query }));
|
|
613
|
+
fetchSuggestions(query);
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const selectSuggestion = (suggestion: string) => {
|
|
617
|
+
setSearchState(prev => ({
|
|
618
|
+
...prev,
|
|
619
|
+
query: suggestion,
|
|
620
|
+
showSuggestions: false
|
|
621
|
+
}));
|
|
622
|
+
performFullSearch(suggestion);
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
return (
|
|
626
|
+
<div className="realtime-search">
|
|
627
|
+
<div className="search-input-container">
|
|
628
|
+
<Search
|
|
629
|
+
mode="Auto"
|
|
630
|
+
value={searchState.query}
|
|
631
|
+
onValueChange={handleQueryChange}
|
|
632
|
+
onSearch={performFullSearch}
|
|
633
|
+
loading={searchState.loading}
|
|
634
|
+
placeholder="Start typing to see suggestions..."
|
|
635
|
+
debounceMs={200}
|
|
636
|
+
minCharacters={2}
|
|
637
|
+
/>
|
|
638
|
+
|
|
639
|
+
{searchState.showSuggestions && searchState.suggestions.length > 0 && (
|
|
640
|
+
<div className="suggestions-dropdown">
|
|
641
|
+
{searchState.suggestions.map((suggestion, index) => (
|
|
642
|
+
<div
|
|
643
|
+
key={index}
|
|
644
|
+
className="suggestion-item"
|
|
645
|
+
onClick={() => selectSuggestion(suggestion.text)}
|
|
646
|
+
>
|
|
647
|
+
<Icon icon="Search" size="Small" />
|
|
648
|
+
<div className="suggestion-content">
|
|
649
|
+
<Text type="BodyMedium">{suggestion.text}</Text>
|
|
650
|
+
{suggestion.category && (
|
|
651
|
+
<Text type="BodySmall">in {suggestion.category}</Text>
|
|
652
|
+
)}
|
|
653
|
+
</div>
|
|
654
|
+
{suggestion.count && (
|
|
655
|
+
<Text type="BodySmall">{suggestion.count} results</Text>
|
|
656
|
+
)}
|
|
657
|
+
</div>
|
|
658
|
+
))}
|
|
659
|
+
</div>
|
|
660
|
+
)}
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<div className="search-results">
|
|
664
|
+
{searchState.results.map(result => (
|
|
665
|
+
<div key={result.id} className="search-result">
|
|
666
|
+
<Text type="Heading6">{result.title}</Text>
|
|
667
|
+
<Text type="BodyMedium">{result.description}</Text>
|
|
668
|
+
<Text type="BodySmall">{result.url}</Text>
|
|
669
|
+
</div>
|
|
670
|
+
))}
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
## Search Modes
|
|
678
|
+
|
|
679
|
+
### Auto Mode
|
|
680
|
+
- Searches automatically as user types
|
|
681
|
+
- Uses debouncing to prevent excessive API calls
|
|
682
|
+
- Configurable minimum character threshold
|
|
683
|
+
- Ideal for real-time search experiences
|
|
684
|
+
|
|
685
|
+
### Manual Mode
|
|
686
|
+
- Searches only when user presses Enter or clicks search button
|
|
687
|
+
- Better for expensive search operations
|
|
688
|
+
- Gives users full control over when to search
|
|
689
|
+
- Includes visual submit button
|
|
690
|
+
|
|
691
|
+
## Accessibility Features
|
|
692
|
+
|
|
693
|
+
- Full keyboard navigation support
|
|
694
|
+
- ARIA labels for screen readers
|
|
695
|
+
- Focus management for search and clear buttons
|
|
696
|
+
- Semantic form structure in manual mode
|
|
697
|
+
|
|
698
|
+
## Performance Considerations
|
|
699
|
+
|
|
700
|
+
- Built-in debouncing for auto mode prevents API spam
|
|
701
|
+
- Duplicate search prevention
|
|
702
|
+
- Efficient state management with presenter pattern
|
|
703
|
+
- Memoized components to prevent unnecessary re-renders
|
|
704
|
+
|
|
705
|
+
## Related Components
|
|
706
|
+
|
|
707
|
+
- **[Input](Input.md)** - Base input component
|
|
708
|
+
- **[Button](../atoms/Button.md)** - Submit button component
|
|
709
|
+
- **[IconButton](../atoms/IconButton.md)** - Clear button component
|
|
710
|
+
- **[Icon](../atoms/Icon.md)** - Search and close icons
|