@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,437 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import SearchableList from './SearchableList';
|
|
4
|
+
import { SearchableListItem } from './SearchableList';
|
|
5
|
+
import { User, Package, Mail } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Forms/SearchableList',
|
|
9
|
+
component: SearchableList,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'centered',
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component: `
|
|
15
|
+
A list component with integrated search/filter functionality, keyboard navigation, and customizable item rendering.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
- **Search/Filter**: Built-in search input with debounce
|
|
19
|
+
- **Keyboard Navigation**: Arrow keys and Enter for selection
|
|
20
|
+
- **Custom Rendering**: Full control over item display
|
|
21
|
+
- **Loading State**: Show spinner during async operations
|
|
22
|
+
- **Empty States**: Customizable empty and no-results messages
|
|
23
|
+
- **Result Count**: Optional filtered/total count display
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
\`\`\`tsx
|
|
28
|
+
import { SearchableList } from 'notebook-ui';
|
|
29
|
+
|
|
30
|
+
<SearchableList
|
|
31
|
+
items={users.map(u => ({ key: u.id, data: u }))}
|
|
32
|
+
renderItem={(item) => <div>{item.data.name}</div>}
|
|
33
|
+
onSelect={(item) => setSelectedUser(item.data)}
|
|
34
|
+
searchPlaceholder="Search users..."
|
|
35
|
+
maxHeight="300px"
|
|
36
|
+
/>
|
|
37
|
+
\`\`\`
|
|
38
|
+
`,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
tags: ['autodocs'],
|
|
43
|
+
argTypes: {
|
|
44
|
+
size: {
|
|
45
|
+
control: 'select',
|
|
46
|
+
options: ['sm', 'md', 'lg'],
|
|
47
|
+
description: 'Size variant',
|
|
48
|
+
},
|
|
49
|
+
variant: {
|
|
50
|
+
control: 'select',
|
|
51
|
+
options: ['default', 'bordered', 'card'],
|
|
52
|
+
description: 'Visual variant',
|
|
53
|
+
},
|
|
54
|
+
loading: {
|
|
55
|
+
control: 'boolean',
|
|
56
|
+
description: 'Show loading state',
|
|
57
|
+
},
|
|
58
|
+
showResultCount: {
|
|
59
|
+
control: 'boolean',
|
|
60
|
+
description: 'Show result count',
|
|
61
|
+
},
|
|
62
|
+
enableKeyboardNavigation: {
|
|
63
|
+
control: 'boolean',
|
|
64
|
+
description: 'Enable arrow key navigation',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
decorators: [
|
|
68
|
+
(Story) => (
|
|
69
|
+
<div style={{ minWidth: '400px' }}>
|
|
70
|
+
<Story />
|
|
71
|
+
</div>
|
|
72
|
+
),
|
|
73
|
+
],
|
|
74
|
+
} satisfies Meta<typeof SearchableList>;
|
|
75
|
+
|
|
76
|
+
export default meta;
|
|
77
|
+
type Story = StoryObj<typeof meta>;
|
|
78
|
+
|
|
79
|
+
interface UserData {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
email: string;
|
|
83
|
+
role: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ProductData {
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
price: number;
|
|
90
|
+
category: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const users: SearchableListItem<UserData>[] = [
|
|
94
|
+
{ key: '1', data: { id: '1', name: 'John Doe', email: 'john@example.com', role: 'Admin' } },
|
|
95
|
+
{ key: '2', data: { id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'Editor' } },
|
|
96
|
+
{ key: '3', data: { id: '3', name: 'Bob Johnson', email: 'bob@example.com', role: 'Viewer' } },
|
|
97
|
+
{ key: '4', data: { id: '4', name: 'Alice Brown', email: 'alice@example.com', role: 'Admin' } },
|
|
98
|
+
{ key: '5', data: { id: '5', name: 'Charlie Wilson', email: 'charlie@example.com', role: 'Editor' } },
|
|
99
|
+
{ key: '6', data: { id: '6', name: 'Diana Martinez', email: 'diana@example.com', role: 'Viewer' } },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const products: SearchableListItem<ProductData>[] = [
|
|
103
|
+
{ key: 'p1', data: { id: 'p1', name: 'Laptop Pro', price: 1299, category: 'Electronics' } },
|
|
104
|
+
{ key: 'p2', data: { id: 'p2', name: 'Wireless Mouse', price: 49, category: 'Electronics' } },
|
|
105
|
+
{ key: 'p3', data: { id: 'p3', name: 'Office Chair', price: 299, category: 'Furniture' } },
|
|
106
|
+
{ key: 'p4', data: { id: 'p4', name: 'Standing Desk', price: 599, category: 'Furniture' } },
|
|
107
|
+
{ key: 'p5', data: { id: 'p5', name: 'Monitor 27"', price: 399, category: 'Electronics' } },
|
|
108
|
+
{ key: 'p6', data: { id: 'p6', name: 'Keyboard', price: 129, category: 'Electronics' } },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
export const Default: Story = {
|
|
112
|
+
render: () => {
|
|
113
|
+
const [selectedKey, setSelectedKey] = useState<string | undefined>();
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<SearchableList
|
|
117
|
+
items={users}
|
|
118
|
+
renderItem={(item) => (
|
|
119
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
120
|
+
<User className="h-4 w-4 text-ink-400" />
|
|
121
|
+
<div>
|
|
122
|
+
<div style={{ fontWeight: 500 }}>{item.data.name}</div>
|
|
123
|
+
<div style={{ fontSize: '0.75rem', color: '#666' }}>{item.data.email}</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
filterFn={(item, term) =>
|
|
128
|
+
item.data.name.toLowerCase().includes(term.toLowerCase()) ||
|
|
129
|
+
item.data.email.toLowerCase().includes(term.toLowerCase())
|
|
130
|
+
}
|
|
131
|
+
selectedKey={selectedKey}
|
|
132
|
+
onSelect={(item) => setSelectedKey(item.key)}
|
|
133
|
+
searchPlaceholder="Search users..."
|
|
134
|
+
variant="bordered"
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const WithResultCount: Story = {
|
|
141
|
+
render: () => {
|
|
142
|
+
const [selectedKey, setSelectedKey] = useState<string | undefined>();
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<SearchableList
|
|
146
|
+
items={users}
|
|
147
|
+
renderItem={(item) => (
|
|
148
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
149
|
+
<span>{item.data.name}</span>
|
|
150
|
+
<span style={{ fontSize: '0.75rem', color: '#666', backgroundColor: '#f5f5f4', padding: '0.125rem 0.5rem', borderRadius: '9999px' }}>
|
|
151
|
+
{item.data.role}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
filterFn={(item, term) =>
|
|
156
|
+
item.data.name.toLowerCase().includes(term.toLowerCase()) ||
|
|
157
|
+
item.data.role.toLowerCase().includes(term.toLowerCase())
|
|
158
|
+
}
|
|
159
|
+
selectedKey={selectedKey}
|
|
160
|
+
onSelect={(item) => setSelectedKey(item.key)}
|
|
161
|
+
searchPlaceholder="Search by name or role..."
|
|
162
|
+
showResultCount
|
|
163
|
+
variant="card"
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const ProductList: Story = {
|
|
170
|
+
render: () => {
|
|
171
|
+
const [selectedKey, setSelectedKey] = useState<string | undefined>();
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<SearchableList
|
|
175
|
+
items={products}
|
|
176
|
+
renderItem={(item, _index, isSelected) => (
|
|
177
|
+
<div style={{
|
|
178
|
+
display: 'flex',
|
|
179
|
+
justifyContent: 'space-between',
|
|
180
|
+
alignItems: 'center',
|
|
181
|
+
color: isSelected ? '#1e40af' : undefined,
|
|
182
|
+
}}>
|
|
183
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
184
|
+
<Package className="h-4 w-4" />
|
|
185
|
+
<div>
|
|
186
|
+
<div style={{ fontWeight: 500 }}>{item.data.name}</div>
|
|
187
|
+
<div style={{ fontSize: '0.75rem', color: '#666' }}>{item.data.category}</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div style={{ fontWeight: 600 }}>${item.data.price}</div>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
filterFn={(item, term) =>
|
|
194
|
+
item.data.name.toLowerCase().includes(term.toLowerCase()) ||
|
|
195
|
+
item.data.category.toLowerCase().includes(term.toLowerCase())
|
|
196
|
+
}
|
|
197
|
+
selectedKey={selectedKey}
|
|
198
|
+
onSelect={(item) => setSelectedKey(item.key)}
|
|
199
|
+
searchPlaceholder="Search products..."
|
|
200
|
+
showResultCount
|
|
201
|
+
formatResultCount={(count, total) => `Showing ${count} of ${total} products`}
|
|
202
|
+
maxHeight="300px"
|
|
203
|
+
variant="card"
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const Loading: Story = {
|
|
210
|
+
render: () => {
|
|
211
|
+
return (
|
|
212
|
+
<SearchableList
|
|
213
|
+
items={[]}
|
|
214
|
+
renderItem={() => null}
|
|
215
|
+
loading
|
|
216
|
+
loadingMessage="Fetching users..."
|
|
217
|
+
searchPlaceholder="Search users..."
|
|
218
|
+
variant="bordered"
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const EmptyState: Story = {
|
|
225
|
+
render: () => {
|
|
226
|
+
return (
|
|
227
|
+
<SearchableList
|
|
228
|
+
items={[]}
|
|
229
|
+
renderItem={() => null}
|
|
230
|
+
emptyMessage="No users found. Try adding some users first."
|
|
231
|
+
searchPlaceholder="Search users..."
|
|
232
|
+
variant="card"
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const NoResults: Story = {
|
|
239
|
+
render: () => {
|
|
240
|
+
const [selectedKey, setSelectedKey] = useState<string | undefined>();
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
244
|
+
<p style={{ fontSize: '0.875rem', color: '#666' }}>
|
|
245
|
+
Try searching for "xyz" to see the no results state
|
|
246
|
+
</p>
|
|
247
|
+
<SearchableList
|
|
248
|
+
items={users}
|
|
249
|
+
renderItem={(item) => <span>{item.data.name}</span>}
|
|
250
|
+
filterFn={(item, term) =>
|
|
251
|
+
item.data.name.toLowerCase().includes(term.toLowerCase())
|
|
252
|
+
}
|
|
253
|
+
selectedKey={selectedKey}
|
|
254
|
+
onSelect={(item) => setSelectedKey(item.key)}
|
|
255
|
+
searchPlaceholder="Search users..."
|
|
256
|
+
noResultsMessage="No users match your search. Try a different term."
|
|
257
|
+
variant="bordered"
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export const Small: Story = {
|
|
265
|
+
render: () => {
|
|
266
|
+
const [selectedKey, setSelectedKey] = useState<string | undefined>();
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<SearchableList
|
|
270
|
+
items={users}
|
|
271
|
+
renderItem={(item) => item.data.name}
|
|
272
|
+
filterFn={(item, term) =>
|
|
273
|
+
item.data.name.toLowerCase().includes(term.toLowerCase())
|
|
274
|
+
}
|
|
275
|
+
selectedKey={selectedKey}
|
|
276
|
+
onSelect={(item) => setSelectedKey(item.key)}
|
|
277
|
+
searchPlaceholder="Search..."
|
|
278
|
+
size="sm"
|
|
279
|
+
variant="bordered"
|
|
280
|
+
/>
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const Large: Story = {
|
|
286
|
+
render: () => {
|
|
287
|
+
const [selectedKey, setSelectedKey] = useState<string | undefined>();
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<SearchableList
|
|
291
|
+
items={users}
|
|
292
|
+
renderItem={(item) => (
|
|
293
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
|
294
|
+
<div style={{
|
|
295
|
+
width: '40px',
|
|
296
|
+
height: '40px',
|
|
297
|
+
borderRadius: '50%',
|
|
298
|
+
backgroundColor: '#e5e5e5',
|
|
299
|
+
display: 'flex',
|
|
300
|
+
alignItems: 'center',
|
|
301
|
+
justifyContent: 'center',
|
|
302
|
+
}}>
|
|
303
|
+
<User className="h-5 w-5 text-ink-500" />
|
|
304
|
+
</div>
|
|
305
|
+
<div>
|
|
306
|
+
<div style={{ fontWeight: 500, fontSize: '1rem' }}>{item.data.name}</div>
|
|
307
|
+
<div style={{ fontSize: '0.875rem', color: '#666' }}>{item.data.email}</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
filterFn={(item, term) =>
|
|
312
|
+
item.data.name.toLowerCase().includes(term.toLowerCase())
|
|
313
|
+
}
|
|
314
|
+
selectedKey={selectedKey}
|
|
315
|
+
onSelect={(item) => setSelectedKey(item.key)}
|
|
316
|
+
searchPlaceholder="Search users..."
|
|
317
|
+
size="lg"
|
|
318
|
+
variant="card"
|
|
319
|
+
/>
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const KeyboardNavigation: Story = {
|
|
325
|
+
parameters: {
|
|
326
|
+
docs: {
|
|
327
|
+
description: {
|
|
328
|
+
story: 'Use arrow keys to navigate and Enter to select. Focus the search input and try it out!',
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
render: () => {
|
|
333
|
+
const [selectedKey, setSelectedKey] = useState<string | undefined>();
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
337
|
+
<p style={{ fontSize: '0.875rem', color: '#666' }}>
|
|
338
|
+
Focus the search input, then use ↑↓ arrows to navigate and Enter to select
|
|
339
|
+
</p>
|
|
340
|
+
<SearchableList
|
|
341
|
+
items={users}
|
|
342
|
+
renderItem={(item, _index, isSelected, isHighlighted) => (
|
|
343
|
+
<div style={{
|
|
344
|
+
display: 'flex',
|
|
345
|
+
alignItems: 'center',
|
|
346
|
+
gap: '0.75rem',
|
|
347
|
+
fontWeight: isHighlighted ? 500 : 400,
|
|
348
|
+
}}>
|
|
349
|
+
<Mail className="h-4 w-4" />
|
|
350
|
+
<div>
|
|
351
|
+
<div>{item.data.name}</div>
|
|
352
|
+
<div style={{ fontSize: '0.75rem', color: '#666' }}>{item.data.email}</div>
|
|
353
|
+
</div>
|
|
354
|
+
{isSelected && (
|
|
355
|
+
<span style={{ marginLeft: 'auto', color: '#22c55e' }}>✓</span>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
filterFn={(item, term) =>
|
|
360
|
+
item.data.name.toLowerCase().includes(term.toLowerCase()) ||
|
|
361
|
+
item.data.email.toLowerCase().includes(term.toLowerCase())
|
|
362
|
+
}
|
|
363
|
+
selectedKey={selectedKey}
|
|
364
|
+
onSelect={(item) => setSelectedKey(item.key)}
|
|
365
|
+
searchPlaceholder="Search users..."
|
|
366
|
+
enableKeyboardNavigation
|
|
367
|
+
autoFocus
|
|
368
|
+
variant="card"
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
export const TableSelector: Story = {
|
|
376
|
+
parameters: {
|
|
377
|
+
docs: {
|
|
378
|
+
description: {
|
|
379
|
+
story: 'Example of a table selector for a report builder.',
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
render: () => {
|
|
384
|
+
interface TableData {
|
|
385
|
+
name: string;
|
|
386
|
+
displayName: string;
|
|
387
|
+
rowCount: number;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const tables: SearchableListItem<TableData>[] = [
|
|
391
|
+
{ key: 'users', data: { name: 'users', displayName: 'Users', rowCount: 1250 } },
|
|
392
|
+
{ key: 'orders', data: { name: 'orders', displayName: 'Orders', rowCount: 8420 } },
|
|
393
|
+
{ key: 'products', data: { name: 'products', displayName: 'Products', rowCount: 342 } },
|
|
394
|
+
{ key: 'categories', data: { name: 'categories', displayName: 'Categories', rowCount: 24 } },
|
|
395
|
+
{ key: 'reviews', data: { name: 'reviews', displayName: 'Reviews', rowCount: 15680 } },
|
|
396
|
+
{ key: 'inventory', data: { name: 'inventory', displayName: 'Inventory', rowCount: 890 } },
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
const [selectedKey, setSelectedKey] = useState<string | undefined>('users');
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
403
|
+
<h3 style={{ fontSize: '1rem', fontWeight: 600 }}>Select Base Table</h3>
|
|
404
|
+
<SearchableList
|
|
405
|
+
items={tables}
|
|
406
|
+
renderItem={(item, _index, isSelected) => (
|
|
407
|
+
<div style={{
|
|
408
|
+
display: 'flex',
|
|
409
|
+
justifyContent: 'space-between',
|
|
410
|
+
alignItems: 'center',
|
|
411
|
+
}}>
|
|
412
|
+
<div>
|
|
413
|
+
<div style={{ fontWeight: isSelected ? 600 : 500 }}>{item.data.displayName}</div>
|
|
414
|
+
<div style={{ fontSize: '0.75rem', color: '#666' }}>{item.data.name}</div>
|
|
415
|
+
</div>
|
|
416
|
+
<div style={{ fontSize: '0.75rem', color: '#666' }}>
|
|
417
|
+
{item.data.rowCount.toLocaleString()} rows
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
filterFn={(item, term) =>
|
|
422
|
+
item.data.name.toLowerCase().includes(term.toLowerCase()) ||
|
|
423
|
+
item.data.displayName.toLowerCase().includes(term.toLowerCase())
|
|
424
|
+
}
|
|
425
|
+
selectedKey={selectedKey}
|
|
426
|
+
onSelect={(item) => setSelectedKey(item.key)}
|
|
427
|
+
searchPlaceholder="Search tables..."
|
|
428
|
+
showResultCount
|
|
429
|
+
variant="card"
|
|
430
|
+
/>
|
|
431
|
+
<div style={{ fontSize: '0.875rem', color: '#666' }}>
|
|
432
|
+
Selected: {selectedKey || 'None'}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
},
|
|
437
|
+
};
|