@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.
Files changed (108) hide show
  1. package/dist/components/ActionBar.d.ts +112 -0
  2. package/dist/components/ActionBar.d.ts.map +1 -0
  3. package/dist/components/BottomNavigation.d.ts +98 -0
  4. package/dist/components/BottomNavigation.d.ts.map +1 -0
  5. package/dist/components/Checkbox.d.ts +2 -0
  6. package/dist/components/Checkbox.d.ts.map +1 -1
  7. package/dist/components/CheckboxList.d.ts +81 -0
  8. package/dist/components/CheckboxList.d.ts.map +1 -0
  9. package/dist/components/Chip.d.ts +92 -1
  10. package/dist/components/Chip.d.ts.map +1 -1
  11. package/dist/components/ConfirmDialog.d.ts +43 -1
  12. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  13. package/dist/components/DataTable.d.ts +10 -1
  14. package/dist/components/DataTable.d.ts.map +1 -1
  15. package/dist/components/DataTableCardView.d.ts +99 -0
  16. package/dist/components/DataTableCardView.d.ts.map +1 -0
  17. package/dist/components/ExpandablePanel.d.ts +142 -0
  18. package/dist/components/ExpandablePanel.d.ts.map +1 -0
  19. package/dist/components/FloatingActionButton.d.ts +98 -0
  20. package/dist/components/FloatingActionButton.d.ts.map +1 -0
  21. package/dist/components/Input.d.ts +45 -1
  22. package/dist/components/Input.d.ts.map +1 -1
  23. package/dist/components/MobileHeader.d.ts +98 -0
  24. package/dist/components/MobileHeader.d.ts.map +1 -0
  25. package/dist/components/MobileLayout.d.ts +121 -0
  26. package/dist/components/MobileLayout.d.ts.map +1 -0
  27. package/dist/components/Modal.d.ts +78 -1
  28. package/dist/components/Modal.d.ts.map +1 -1
  29. package/dist/components/PageHeader.d.ts +86 -0
  30. package/dist/components/PageHeader.d.ts.map +1 -0
  31. package/dist/components/PullToRefresh.d.ts +87 -0
  32. package/dist/components/PullToRefresh.d.ts.map +1 -0
  33. package/dist/components/QueryTransparency.d.ts +1 -1
  34. package/dist/components/QueryTransparency.d.ts.map +1 -1
  35. package/dist/components/SearchableList.d.ts +83 -0
  36. package/dist/components/SearchableList.d.ts.map +1 -0
  37. package/dist/components/Select.d.ts +16 -2
  38. package/dist/components/Select.d.ts.map +1 -1
  39. package/dist/components/Sidebar.d.ts +40 -1
  40. package/dist/components/Sidebar.d.ts.map +1 -1
  41. package/dist/components/SwipeActions.d.ts +93 -0
  42. package/dist/components/SwipeActions.d.ts.map +1 -0
  43. package/dist/components/Switch.d.ts +1 -0
  44. package/dist/components/Switch.d.ts.map +1 -1
  45. package/dist/components/Textarea.d.ts +13 -0
  46. package/dist/components/Textarea.d.ts.map +1 -1
  47. package/dist/components/index.d.ts +31 -3
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/context/MobileContext.d.ts +168 -0
  50. package/dist/context/MobileContext.d.ts.map +1 -0
  51. package/dist/hooks/useResponsive.d.ts +158 -0
  52. package/dist/hooks/useResponsive.d.ts.map +1 -0
  53. package/dist/index.d.ts +1871 -51
  54. package/dist/index.esm.js +3025 -196
  55. package/dist/index.esm.js.map +1 -1
  56. package/dist/index.js +3063 -194
  57. package/dist/index.js.map +1 -1
  58. package/dist/styles.css +434 -1
  59. package/dist/types/index.d.ts +2 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/components/ActionBar.stories.tsx +246 -0
  63. package/src/components/ActionBar.tsx +242 -0
  64. package/src/components/BottomNavigation.stories.tsx +142 -0
  65. package/src/components/BottomNavigation.tsx +225 -0
  66. package/src/components/Checkbox.stories.tsx +162 -0
  67. package/src/components/Checkbox.tsx +22 -6
  68. package/src/components/CheckboxList.stories.tsx +311 -0
  69. package/src/components/CheckboxList.tsx +433 -0
  70. package/src/components/Chip.stories.tsx +389 -0
  71. package/src/components/Chip.tsx +182 -3
  72. package/src/components/ConfirmDialog.tsx +56 -4
  73. package/src/components/DataTable.tsx +60 -1
  74. package/src/components/DataTableCardView.stories.tsx +307 -0
  75. package/src/components/DataTableCardView.tsx +419 -0
  76. package/src/components/ExpandablePanel.stories.tsx +620 -0
  77. package/src/components/ExpandablePanel.tsx +383 -0
  78. package/src/components/FloatingActionButton.stories.tsx +197 -0
  79. package/src/components/FloatingActionButton.tsx +301 -0
  80. package/src/components/Grid.stories.tsx +16 -16
  81. package/src/components/Input.stories.tsx +214 -0
  82. package/src/components/Input.tsx +81 -4
  83. package/src/components/MobileHeader.stories.tsx +205 -0
  84. package/src/components/MobileHeader.tsx +233 -0
  85. package/src/components/MobileLayout.stories.tsx +338 -0
  86. package/src/components/MobileLayout.tsx +313 -0
  87. package/src/components/Modal.stories.tsx +388 -0
  88. package/src/components/Modal.tsx +122 -4
  89. package/src/components/PageHeader.stories.tsx +198 -0
  90. package/src/components/PageHeader.tsx +217 -0
  91. package/src/components/PullToRefresh.stories.tsx +321 -0
  92. package/src/components/PullToRefresh.tsx +294 -0
  93. package/src/components/QueryTransparency.tsx +1 -1
  94. package/src/components/SearchableList.stories.tsx +437 -0
  95. package/src/components/SearchableList.tsx +326 -0
  96. package/src/components/Select.stories.tsx +190 -0
  97. package/src/components/Select.tsx +353 -137
  98. package/src/components/Sidebar.tsx +193 -10
  99. package/src/components/SwipeActions.stories.tsx +327 -0
  100. package/src/components/SwipeActions.tsx +387 -0
  101. package/src/components/Switch.stories.tsx +158 -0
  102. package/src/components/Switch.tsx +12 -3
  103. package/src/components/Textarea.tsx +31 -1
  104. package/src/components/index.ts +69 -3
  105. package/src/context/MobileContext.tsx +296 -0
  106. package/src/hooks/useResponsive.ts +360 -0
  107. package/src/types/index.ts +4 -0
  108. 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
+ };