@papernote/ui 1.3.1 → 1.6.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/ActionBar.d.ts +112 -0
- package/dist/components/ActionBar.d.ts.map +1 -0
- package/dist/components/BottomNavigation.d.ts +98 -0
- package/dist/components/BottomNavigation.d.ts.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -0
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/CheckboxList.d.ts +81 -0
- package/dist/components/CheckboxList.d.ts.map +1 -0
- package/dist/components/Chip.d.ts +92 -1
- package/dist/components/Chip.d.ts.map +1 -1
- package/dist/components/ConfirmDialog.d.ts +43 -1
- package/dist/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts +10 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTableCardView.d.ts +99 -0
- package/dist/components/DataTableCardView.d.ts.map +1 -0
- package/dist/components/ExpandablePanel.d.ts +142 -0
- package/dist/components/ExpandablePanel.d.ts.map +1 -0
- package/dist/components/FloatingActionButton.d.ts +98 -0
- package/dist/components/FloatingActionButton.d.ts.map +1 -0
- package/dist/components/Input.d.ts +45 -1
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/MobileHeader.d.ts +98 -0
- package/dist/components/MobileHeader.d.ts.map +1 -0
- package/dist/components/MobileLayout.d.ts +121 -0
- package/dist/components/MobileLayout.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +78 -1
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/PageHeader.d.ts +86 -0
- package/dist/components/PageHeader.d.ts.map +1 -0
- package/dist/components/PullToRefresh.d.ts +87 -0
- package/dist/components/PullToRefresh.d.ts.map +1 -0
- package/dist/components/QueryTransparency.d.ts +1 -1
- package/dist/components/QueryTransparency.d.ts.map +1 -1
- package/dist/components/SearchableList.d.ts +83 -0
- package/dist/components/SearchableList.d.ts.map +1 -0
- package/dist/components/Select.d.ts +16 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +40 -1
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/SwipeActions.d.ts +93 -0
- package/dist/components/SwipeActions.d.ts.map +1 -0
- package/dist/components/Switch.d.ts +1 -0
- package/dist/components/Switch.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +13 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +31 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/context/MobileContext.d.ts +168 -0
- package/dist/context/MobileContext.d.ts.map +1 -0
- package/dist/hooks/useResponsive.d.ts +158 -0
- package/dist/hooks/useResponsive.d.ts.map +1 -0
- package/dist/index.d.ts +1871 -51
- package/dist/index.esm.js +3025 -196
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3063 -194
- package/dist/index.js.map +1 -1
- package/dist/styles.css +434 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ActionBar.stories.tsx +246 -0
- package/src/components/ActionBar.tsx +242 -0
- package/src/components/BottomNavigation.stories.tsx +142 -0
- package/src/components/BottomNavigation.tsx +225 -0
- package/src/components/Checkbox.stories.tsx +162 -0
- package/src/components/Checkbox.tsx +22 -6
- package/src/components/CheckboxList.stories.tsx +311 -0
- package/src/components/CheckboxList.tsx +433 -0
- package/src/components/Chip.stories.tsx +389 -0
- package/src/components/Chip.tsx +182 -3
- package/src/components/ConfirmDialog.tsx +56 -4
- package/src/components/DataTable.tsx +60 -1
- package/src/components/DataTableCardView.stories.tsx +307 -0
- package/src/components/DataTableCardView.tsx +419 -0
- package/src/components/ExpandablePanel.stories.tsx +620 -0
- package/src/components/ExpandablePanel.tsx +383 -0
- package/src/components/FloatingActionButton.stories.tsx +197 -0
- package/src/components/FloatingActionButton.tsx +301 -0
- package/src/components/Grid.stories.tsx +16 -16
- package/src/components/Input.stories.tsx +214 -0
- package/src/components/Input.tsx +81 -4
- package/src/components/MobileHeader.stories.tsx +205 -0
- package/src/components/MobileHeader.tsx +233 -0
- package/src/components/MobileLayout.stories.tsx +338 -0
- package/src/components/MobileLayout.tsx +313 -0
- package/src/components/Modal.stories.tsx +388 -0
- package/src/components/Modal.tsx +122 -4
- package/src/components/PageHeader.stories.tsx +198 -0
- package/src/components/PageHeader.tsx +217 -0
- package/src/components/PullToRefresh.stories.tsx +321 -0
- package/src/components/PullToRefresh.tsx +294 -0
- package/src/components/QueryTransparency.tsx +1 -1
- package/src/components/SearchableList.stories.tsx +437 -0
- package/src/components/SearchableList.tsx +326 -0
- package/src/components/Select.stories.tsx +190 -0
- package/src/components/Select.tsx +353 -137
- package/src/components/Sidebar.tsx +193 -10
- package/src/components/SwipeActions.stories.tsx +327 -0
- package/src/components/SwipeActions.tsx +387 -0
- package/src/components/Switch.stories.tsx +158 -0
- package/src/components/Switch.tsx +12 -3
- package/src/components/Textarea.tsx +31 -1
- package/src/components/index.ts +69 -3
- package/src/context/MobileContext.tsx +296 -0
- package/src/hooks/useResponsive.ts +360 -0
- package/src/types/index.ts +4 -0
- package/tailwind.config.js +56 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback, useRef, useEffect, ReactNode } from 'react';
|
|
2
|
+
import { Search } from 'lucide-react';
|
|
3
|
+
import Input from './Input';
|
|
4
|
+
import { Loader2 } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export interface SearchableListItem<T = unknown> {
|
|
7
|
+
key: string;
|
|
8
|
+
data: T;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SearchableListProps<T = unknown> {
|
|
12
|
+
/** Array of items to display */
|
|
13
|
+
items: SearchableListItem<T>[];
|
|
14
|
+
|
|
15
|
+
// Search configuration
|
|
16
|
+
/** Search input placeholder */
|
|
17
|
+
searchPlaceholder?: string;
|
|
18
|
+
/** Controlled search value */
|
|
19
|
+
searchValue?: string;
|
|
20
|
+
/** Callback when search changes */
|
|
21
|
+
onSearchChange?: (value: string) => void;
|
|
22
|
+
/** Custom filter function */
|
|
23
|
+
filterFn?: (item: SearchableListItem<T>, searchTerm: string) => boolean;
|
|
24
|
+
/** Debounce delay for search in ms */
|
|
25
|
+
debounceMs?: number;
|
|
26
|
+
|
|
27
|
+
// Item rendering
|
|
28
|
+
/** Render function for each item */
|
|
29
|
+
renderItem: (item: SearchableListItem<T>, index: number, isSelected: boolean, isHighlighted: boolean) => ReactNode;
|
|
30
|
+
|
|
31
|
+
// Selection (optional)
|
|
32
|
+
/** Currently selected item key */
|
|
33
|
+
selectedKey?: string;
|
|
34
|
+
/** Callback when item is selected */
|
|
35
|
+
onSelect?: (item: SearchableListItem<T>) => void;
|
|
36
|
+
|
|
37
|
+
// Display
|
|
38
|
+
/** Maximum height with overflow scroll */
|
|
39
|
+
maxHeight?: string | number;
|
|
40
|
+
/** Show result count */
|
|
41
|
+
showResultCount?: boolean;
|
|
42
|
+
/** Result count format function */
|
|
43
|
+
formatResultCount?: (count: number, total: number) => string;
|
|
44
|
+
|
|
45
|
+
// Empty/Loading states
|
|
46
|
+
/** Message when no items available */
|
|
47
|
+
emptyMessage?: string | ReactNode;
|
|
48
|
+
/** Message when search has no results */
|
|
49
|
+
noResultsMessage?: string | ReactNode;
|
|
50
|
+
/** Loading state */
|
|
51
|
+
loading?: boolean;
|
|
52
|
+
/** Loading message */
|
|
53
|
+
loadingMessage?: string | ReactNode;
|
|
54
|
+
|
|
55
|
+
// Styling
|
|
56
|
+
/** Size variant */
|
|
57
|
+
size?: 'sm' | 'md' | 'lg';
|
|
58
|
+
/** Visual variant */
|
|
59
|
+
variant?: 'default' | 'bordered' | 'card';
|
|
60
|
+
/** Additional CSS classes */
|
|
61
|
+
className?: string;
|
|
62
|
+
|
|
63
|
+
// Keyboard navigation
|
|
64
|
+
/** Enable keyboard navigation (arrow keys, enter) */
|
|
65
|
+
enableKeyboardNavigation?: boolean;
|
|
66
|
+
/** Auto-focus search input */
|
|
67
|
+
autoFocus?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sizeClasses = {
|
|
71
|
+
sm: {
|
|
72
|
+
container: 'text-sm',
|
|
73
|
+
item: 'py-1.5 px-2',
|
|
74
|
+
searchPadding: 'p-2',
|
|
75
|
+
statusPadding: 'px-2 py-1.5',
|
|
76
|
+
},
|
|
77
|
+
md: {
|
|
78
|
+
container: 'text-sm',
|
|
79
|
+
item: 'py-2 px-3',
|
|
80
|
+
searchPadding: 'p-3',
|
|
81
|
+
statusPadding: 'px-3 py-2',
|
|
82
|
+
},
|
|
83
|
+
lg: {
|
|
84
|
+
container: 'text-base',
|
|
85
|
+
item: 'py-3 px-4',
|
|
86
|
+
searchPadding: 'p-4',
|
|
87
|
+
statusPadding: 'px-4 py-2.5',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const variantClasses = {
|
|
92
|
+
default: 'bg-white',
|
|
93
|
+
bordered: 'bg-white border border-paper-300 rounded-lg',
|
|
94
|
+
card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* SearchableList - List component with integrated search/filter functionality
|
|
99
|
+
*
|
|
100
|
+
* @example Basic usage
|
|
101
|
+
* ```tsx
|
|
102
|
+
* <SearchableList
|
|
103
|
+
* items={users.map(u => ({ key: u.id, data: u }))}
|
|
104
|
+
* renderItem={(item) => <div>{item.data.name}</div>}
|
|
105
|
+
* onSelect={(item) => setSelectedUser(item.data)}
|
|
106
|
+
* searchable
|
|
107
|
+
* searchPlaceholder="Search users..."
|
|
108
|
+
* />
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @example With custom filter and loading
|
|
112
|
+
* ```tsx
|
|
113
|
+
* <SearchableList
|
|
114
|
+
* items={products}
|
|
115
|
+
* renderItem={(item, index, isSelected) => (
|
|
116
|
+
* <div className={isSelected ? 'bg-accent-50' : ''}>
|
|
117
|
+
* {item.data.name} - ${item.data.price}
|
|
118
|
+
* </div>
|
|
119
|
+
* )}
|
|
120
|
+
* filterFn={(item, term) =>
|
|
121
|
+
* item.data.name.toLowerCase().includes(term.toLowerCase())
|
|
122
|
+
* }
|
|
123
|
+
* loading={isLoading}
|
|
124
|
+
* loadingMessage="Fetching products..."
|
|
125
|
+
* maxHeight="400px"
|
|
126
|
+
* />
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export default function SearchableList<T = unknown>({
|
|
130
|
+
items,
|
|
131
|
+
searchPlaceholder = 'Search...',
|
|
132
|
+
searchValue: controlledSearchValue,
|
|
133
|
+
onSearchChange,
|
|
134
|
+
filterFn,
|
|
135
|
+
debounceMs = 150,
|
|
136
|
+
renderItem,
|
|
137
|
+
selectedKey,
|
|
138
|
+
onSelect,
|
|
139
|
+
maxHeight,
|
|
140
|
+
showResultCount = false,
|
|
141
|
+
formatResultCount,
|
|
142
|
+
emptyMessage = 'No items available',
|
|
143
|
+
noResultsMessage = 'No items match your search',
|
|
144
|
+
loading = false,
|
|
145
|
+
loadingMessage = 'Loading...',
|
|
146
|
+
size = 'md',
|
|
147
|
+
variant = 'default',
|
|
148
|
+
className = '',
|
|
149
|
+
enableKeyboardNavigation = true,
|
|
150
|
+
autoFocus = false,
|
|
151
|
+
}: SearchableListProps<T>) {
|
|
152
|
+
const [internalSearchValue, setInternalSearchValue] = useState('');
|
|
153
|
+
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
|
|
154
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
155
|
+
|
|
156
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
157
|
+
const itemRefs = useRef<Map<number, HTMLDivElement>>(new Map());
|
|
158
|
+
|
|
159
|
+
const searchValue = controlledSearchValue !== undefined ? controlledSearchValue : internalSearchValue;
|
|
160
|
+
|
|
161
|
+
// Debounce search
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const timer = setTimeout(() => {
|
|
164
|
+
setDebouncedSearchTerm(searchValue);
|
|
165
|
+
}, debounceMs);
|
|
166
|
+
return () => clearTimeout(timer);
|
|
167
|
+
}, [searchValue, debounceMs]);
|
|
168
|
+
|
|
169
|
+
const handleSearchChange = useCallback((value: string) => {
|
|
170
|
+
if (controlledSearchValue === undefined) {
|
|
171
|
+
setInternalSearchValue(value);
|
|
172
|
+
}
|
|
173
|
+
onSearchChange?.(value);
|
|
174
|
+
setHighlightedIndex(-1);
|
|
175
|
+
}, [controlledSearchValue, onSearchChange]);
|
|
176
|
+
|
|
177
|
+
// Filter items based on search
|
|
178
|
+
const filteredItems = useMemo(() => {
|
|
179
|
+
if (!debouncedSearchTerm) return items;
|
|
180
|
+
|
|
181
|
+
return items.filter(item => {
|
|
182
|
+
if (filterFn) {
|
|
183
|
+
return filterFn(item, debouncedSearchTerm);
|
|
184
|
+
}
|
|
185
|
+
// Default filter: check if key includes search term
|
|
186
|
+
return item.key.toLowerCase().includes(debouncedSearchTerm.toLowerCase());
|
|
187
|
+
});
|
|
188
|
+
}, [items, debouncedSearchTerm, filterFn]);
|
|
189
|
+
|
|
190
|
+
// Keyboard navigation
|
|
191
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
192
|
+
if (!enableKeyboardNavigation || filteredItems.length === 0) return;
|
|
193
|
+
|
|
194
|
+
switch (e.key) {
|
|
195
|
+
case 'ArrowDown':
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
setHighlightedIndex(prev =>
|
|
198
|
+
prev < filteredItems.length - 1 ? prev + 1 : 0
|
|
199
|
+
);
|
|
200
|
+
break;
|
|
201
|
+
case 'ArrowUp':
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
setHighlightedIndex(prev =>
|
|
204
|
+
prev > 0 ? prev - 1 : filteredItems.length - 1
|
|
205
|
+
);
|
|
206
|
+
break;
|
|
207
|
+
case 'Enter':
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
if (highlightedIndex >= 0 && highlightedIndex < filteredItems.length) {
|
|
210
|
+
onSelect?.(filteredItems[highlightedIndex]);
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
case 'Escape':
|
|
214
|
+
setHighlightedIndex(-1);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}, [enableKeyboardNavigation, filteredItems, highlightedIndex, onSelect]);
|
|
218
|
+
|
|
219
|
+
// Scroll highlighted item into view
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (highlightedIndex >= 0) {
|
|
222
|
+
const itemEl = itemRefs.current.get(highlightedIndex);
|
|
223
|
+
if (itemEl) {
|
|
224
|
+
itemEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}, [highlightedIndex]);
|
|
228
|
+
|
|
229
|
+
const sizeStyle = sizeClasses[size];
|
|
230
|
+
|
|
231
|
+
const resultCountText = formatResultCount
|
|
232
|
+
? formatResultCount(filteredItems.length, items.length)
|
|
233
|
+
: `${filteredItems.length} of ${items.length}`;
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div
|
|
237
|
+
className={`${variantClasses[variant]} ${sizeStyle.container} ${className}`}
|
|
238
|
+
onKeyDown={handleKeyDown}
|
|
239
|
+
>
|
|
240
|
+
{/* Search Input */}
|
|
241
|
+
<div className={`${sizeStyle.searchPadding} border-b border-paper-200`}>
|
|
242
|
+
<Input
|
|
243
|
+
value={searchValue}
|
|
244
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
245
|
+
placeholder={searchPlaceholder}
|
|
246
|
+
prefixIcon={<Search className="h-4 w-4" />}
|
|
247
|
+
size={size === 'lg' ? 'md' : 'sm'}
|
|
248
|
+
clearable
|
|
249
|
+
onClear={() => handleSearchChange('')}
|
|
250
|
+
autoFocus={autoFocus}
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{/* Result Count */}
|
|
255
|
+
{showResultCount && items.length > 0 && !loading && (
|
|
256
|
+
<div className={`${sizeStyle.statusPadding} text-ink-500 text-xs border-b border-paper-100`}>
|
|
257
|
+
{resultCountText}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{/* List Content */}
|
|
262
|
+
<div
|
|
263
|
+
ref={listRef}
|
|
264
|
+
className="overflow-y-auto"
|
|
265
|
+
style={{ maxHeight: maxHeight || undefined }}
|
|
266
|
+
role="listbox"
|
|
267
|
+
aria-activedescendant={highlightedIndex >= 0 ? `item-${highlightedIndex}` : undefined}
|
|
268
|
+
>
|
|
269
|
+
{/* Loading State */}
|
|
270
|
+
{loading && (
|
|
271
|
+
<div className={`${sizeStyle.item} flex items-center justify-center gap-2 text-ink-500`}>
|
|
272
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
273
|
+
<span>{loadingMessage}</span>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{/* Empty State */}
|
|
278
|
+
{!loading && items.length === 0 && (
|
|
279
|
+
<div className={`${sizeStyle.item} text-ink-500 text-center`}>
|
|
280
|
+
{emptyMessage}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{/* No Results */}
|
|
285
|
+
{!loading && items.length > 0 && filteredItems.length === 0 && (
|
|
286
|
+
<div className={`${sizeStyle.item} text-ink-500 text-center`}>
|
|
287
|
+
{noResultsMessage}
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{/* Items */}
|
|
292
|
+
{!loading && filteredItems.map((item, index) => {
|
|
293
|
+
const isSelected = selectedKey === item.key;
|
|
294
|
+
const isHighlighted = highlightedIndex === index;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div
|
|
298
|
+
key={item.key}
|
|
299
|
+
id={`item-${index}`}
|
|
300
|
+
ref={(el) => {
|
|
301
|
+
if (el) {
|
|
302
|
+
itemRefs.current.set(index, el);
|
|
303
|
+
} else {
|
|
304
|
+
itemRefs.current.delete(index);
|
|
305
|
+
}
|
|
306
|
+
}}
|
|
307
|
+
role="option"
|
|
308
|
+
aria-selected={isSelected}
|
|
309
|
+
onClick={() => onSelect?.(item)}
|
|
310
|
+
className={`
|
|
311
|
+
${sizeStyle.item}
|
|
312
|
+
cursor-pointer transition-colors
|
|
313
|
+
${isSelected ? 'bg-accent-50' : ''}
|
|
314
|
+
${isHighlighted ? 'bg-paper-100' : ''}
|
|
315
|
+
${!isSelected && !isHighlighted ? 'hover:bg-paper-50' : ''}
|
|
316
|
+
border-b border-paper-100 last:border-b-0
|
|
317
|
+
`}
|
|
318
|
+
>
|
|
319
|
+
{renderItem(item, index, isSelected, isHighlighted)}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
})}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
@@ -388,3 +388,193 @@ export const MultiSelect: Story = {
|
|
|
388
388
|
);
|
|
389
389
|
},
|
|
390
390
|
};
|
|
391
|
+
|
|
392
|
+
// Mobile Stories
|
|
393
|
+
export const MobileBottomSheet: Story = {
|
|
394
|
+
parameters: {
|
|
395
|
+
viewport: {
|
|
396
|
+
defaultViewport: 'mobile1',
|
|
397
|
+
},
|
|
398
|
+
docs: {
|
|
399
|
+
description: {
|
|
400
|
+
story: 'On mobile viewports, Select automatically uses a BottomSheet for option selection. Resize your browser or use mobile viewport to see the effect.',
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
render: () => {
|
|
405
|
+
const [value, setValue] = useState('');
|
|
406
|
+
return (
|
|
407
|
+
<div style={{ padding: '16px' }}>
|
|
408
|
+
<Select
|
|
409
|
+
label="Mobile Select"
|
|
410
|
+
options={countryOptions}
|
|
411
|
+
value={value}
|
|
412
|
+
onChange={setValue}
|
|
413
|
+
mobileMode="auto"
|
|
414
|
+
searchable
|
|
415
|
+
placeholder="Tap to select..."
|
|
416
|
+
/>
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
export const MobileSearchable: Story = {
|
|
423
|
+
parameters: {
|
|
424
|
+
viewport: {
|
|
425
|
+
defaultViewport: 'mobile1',
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
render: () => {
|
|
429
|
+
const [value, setValue] = useState('');
|
|
430
|
+
// Generate many options to demonstrate mobile search
|
|
431
|
+
const manyOptions = Array.from({ length: 50 }, (_, i) => ({
|
|
432
|
+
value: `option-${i}`,
|
|
433
|
+
label: `Option ${i + 1}`,
|
|
434
|
+
}));
|
|
435
|
+
return (
|
|
436
|
+
<div style={{ padding: '16px' }}>
|
|
437
|
+
<Select
|
|
438
|
+
label="Searchable Mobile"
|
|
439
|
+
options={manyOptions}
|
|
440
|
+
value={value}
|
|
441
|
+
onChange={setValue}
|
|
442
|
+
mobileMode="auto"
|
|
443
|
+
searchable
|
|
444
|
+
placeholder="Search options..."
|
|
445
|
+
helperText="Opens as bottom sheet with search on mobile"
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
export const MobileNative: Story = {
|
|
453
|
+
parameters: {
|
|
454
|
+
viewport: {
|
|
455
|
+
defaultViewport: 'mobile1',
|
|
456
|
+
},
|
|
457
|
+
docs: {
|
|
458
|
+
description: {
|
|
459
|
+
story: 'Uses the native OS select picker on mobile for familiar UX.',
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
render: () => {
|
|
464
|
+
const [value, setValue] = useState('');
|
|
465
|
+
return (
|
|
466
|
+
<div style={{ padding: '16px' }}>
|
|
467
|
+
<Select
|
|
468
|
+
label="Native Select"
|
|
469
|
+
options={basicOptions}
|
|
470
|
+
value={value}
|
|
471
|
+
onChange={setValue}
|
|
472
|
+
mobileMode="native"
|
|
473
|
+
placeholder="Uses native picker on mobile..."
|
|
474
|
+
/>
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
export const MobileLargeSize: Story = {
|
|
481
|
+
parameters: {
|
|
482
|
+
viewport: {
|
|
483
|
+
defaultViewport: 'mobile1',
|
|
484
|
+
},
|
|
485
|
+
docs: {
|
|
486
|
+
description: {
|
|
487
|
+
story: 'Large size provides 44px touch targets. On mobile, md size auto-upgrades to lg.',
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
render: () => {
|
|
492
|
+
const [value, setValue] = useState('');
|
|
493
|
+
return (
|
|
494
|
+
<div style={{ padding: '16px' }}>
|
|
495
|
+
<Select
|
|
496
|
+
label="Touch-Friendly Select"
|
|
497
|
+
options={basicOptions}
|
|
498
|
+
value={value}
|
|
499
|
+
onChange={setValue}
|
|
500
|
+
size="lg"
|
|
501
|
+
clearable
|
|
502
|
+
placeholder="Large touch target..."
|
|
503
|
+
/>
|
|
504
|
+
</div>
|
|
505
|
+
);
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
export const MobileWithGroups: Story = {
|
|
510
|
+
parameters: {
|
|
511
|
+
viewport: {
|
|
512
|
+
defaultViewport: 'mobile1',
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
render: () => {
|
|
516
|
+
const [value, setValue] = useState('');
|
|
517
|
+
const groupedOptions = [
|
|
518
|
+
{
|
|
519
|
+
label: 'Fruits',
|
|
520
|
+
options: [
|
|
521
|
+
{ value: 'apple', label: 'Apple' },
|
|
522
|
+
{ value: 'banana', label: 'Banana' },
|
|
523
|
+
{ value: 'orange', label: 'Orange' },
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
label: 'Vegetables',
|
|
528
|
+
options: [
|
|
529
|
+
{ value: 'carrot', label: 'Carrot' },
|
|
530
|
+
{ value: 'broccoli', label: 'Broccoli' },
|
|
531
|
+
{ value: 'spinach', label: 'Spinach' },
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
];
|
|
535
|
+
return (
|
|
536
|
+
<div style={{ padding: '16px' }}>
|
|
537
|
+
<Select
|
|
538
|
+
label="Grouped Options"
|
|
539
|
+
groups={groupedOptions}
|
|
540
|
+
value={value}
|
|
541
|
+
onChange={setValue}
|
|
542
|
+
mobileMode="auto"
|
|
543
|
+
searchable
|
|
544
|
+
placeholder="Select food..."
|
|
545
|
+
/>
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
export const MobileWithIcons: Story = {
|
|
552
|
+
parameters: {
|
|
553
|
+
viewport: {
|
|
554
|
+
defaultViewport: 'mobile1',
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
render: () => {
|
|
558
|
+
const [value, setValue] = useState('');
|
|
559
|
+
const iconOptions = [
|
|
560
|
+
{ value: 'location1', label: 'New York', icon: <MapPin className="w-4 h-4" /> },
|
|
561
|
+
{ value: 'location2', label: 'Los Angeles', icon: <MapPin className="w-4 h-4" /> },
|
|
562
|
+
{ value: 'location3', label: 'Chicago', icon: <MapPin className="w-4 h-4" /> },
|
|
563
|
+
{ value: 'user1', label: 'John Doe', icon: <User className="w-4 h-4" /> },
|
|
564
|
+
{ value: 'user2', label: 'Jane Smith', icon: <User className="w-4 h-4" /> },
|
|
565
|
+
];
|
|
566
|
+
return (
|
|
567
|
+
<div style={{ padding: '16px' }}>
|
|
568
|
+
<Select
|
|
569
|
+
label="With Icons"
|
|
570
|
+
options={iconOptions}
|
|
571
|
+
value={value}
|
|
572
|
+
onChange={setValue}
|
|
573
|
+
mobileMode="auto"
|
|
574
|
+
clearable
|
|
575
|
+
placeholder="Select option..."
|
|
576
|
+
/>
|
|
577
|
+
</div>
|
|
578
|
+
);
|
|
579
|
+
},
|
|
580
|
+
};
|