@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,433 @@
1
+ import { useState, useMemo, useCallback } from 'react';
2
+ import { Search, ChevronDown, ChevronRight } from 'lucide-react';
3
+ import Checkbox from './Checkbox';
4
+ import Input from './Input';
5
+
6
+ export interface CheckboxListItem<T = unknown> {
7
+ key: string;
8
+ label: string;
9
+ description?: string;
10
+ group?: string;
11
+ disabled?: boolean;
12
+ data?: T;
13
+ }
14
+
15
+ export interface CheckboxListProps<T = unknown> {
16
+ /** Array of items to display */
17
+ items: CheckboxListItem<T>[];
18
+ /** Currently selected item keys */
19
+ selectedKeys: string[];
20
+ /** Callback when selection changes */
21
+ onSelectionChange: (selectedKeys: string[]) => void;
22
+
23
+ // Grouping
24
+ /** Labels for groups (key -> display label) */
25
+ groupLabels?: Record<string, string>;
26
+ /** Keys of groups that should be expanded (controlled) */
27
+ expandedGroups?: string[];
28
+ /** Default expanded groups (uncontrolled) */
29
+ defaultExpandedGroups?: string[];
30
+ /** Callback when group expansion changes */
31
+ onGroupToggle?: (groupKey: string, expanded: boolean) => void;
32
+
33
+ // Search/Filter
34
+ /** Enable search functionality */
35
+ searchable?: boolean;
36
+ /** Search input placeholder */
37
+ searchPlaceholder?: string;
38
+ /** Custom filter function */
39
+ filterFn?: (item: CheckboxListItem<T>, searchTerm: string) => boolean;
40
+ /** Debounce delay for search in ms */
41
+ debounceMs?: number;
42
+
43
+ // Display options
44
+ /** Maximum height with overflow scroll */
45
+ maxHeight?: string | number;
46
+ /** Show select all checkbox */
47
+ showSelectAll?: boolean;
48
+ /** Select all label text */
49
+ selectAllLabel?: string;
50
+ /** Show count of selected items */
51
+ showSelectedCount?: boolean;
52
+ /** Message when no items available */
53
+ emptyMessage?: string;
54
+ /** Message when search has no results */
55
+ noResultsMessage?: string;
56
+
57
+ // Styling
58
+ /** Size variant */
59
+ size?: 'sm' | 'md' | 'lg';
60
+ /** Visual variant */
61
+ variant?: 'default' | 'bordered' | 'card';
62
+ /** Additional CSS classes */
63
+ className?: string;
64
+ }
65
+
66
+ const sizeClasses = {
67
+ sm: {
68
+ item: 'py-1.5 px-2',
69
+ text: 'text-sm',
70
+ description: 'text-xs',
71
+ groupHeader: 'py-1.5 px-2 text-xs',
72
+ },
73
+ md: {
74
+ item: 'py-2 px-3',
75
+ text: 'text-sm',
76
+ description: 'text-xs',
77
+ groupHeader: 'py-2 px-3 text-xs',
78
+ },
79
+ lg: {
80
+ item: 'py-3 px-4',
81
+ text: 'text-base',
82
+ description: 'text-sm',
83
+ groupHeader: 'py-2.5 px-4 text-sm',
84
+ },
85
+ };
86
+
87
+ const variantClasses = {
88
+ default: 'bg-white',
89
+ bordered: 'bg-white border border-paper-300 rounded-lg',
90
+ card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
91
+ };
92
+
93
+ /**
94
+ * CheckboxList - Multi-select list with checkboxes, grouping, and search
95
+ *
96
+ * @example Basic usage
97
+ * ```tsx
98
+ * <CheckboxList
99
+ * items={[
100
+ * { key: '1', label: 'Option 1' },
101
+ * { key: '2', label: 'Option 2' },
102
+ * ]}
103
+ * selectedKeys={selected}
104
+ * onSelectionChange={setSelected}
105
+ * />
106
+ * ```
107
+ *
108
+ * @example With grouping and search
109
+ * ```tsx
110
+ * <CheckboxList
111
+ * items={fields}
112
+ * selectedKeys={selectedFields}
113
+ * onSelectionChange={setSelectedFields}
114
+ * groupLabels={{ table1: 'Users', table2: 'Orders' }}
115
+ * searchable
116
+ * searchPlaceholder="Search fields..."
117
+ * showSelectAll
118
+ * maxHeight="300px"
119
+ * />
120
+ * ```
121
+ */
122
+ export default function CheckboxList<T = unknown>({
123
+ items,
124
+ selectedKeys,
125
+ onSelectionChange,
126
+ groupLabels = {},
127
+ expandedGroups: controlledExpandedGroups,
128
+ defaultExpandedGroups,
129
+ onGroupToggle,
130
+ searchable = false,
131
+ searchPlaceholder = 'Search...',
132
+ filterFn,
133
+ debounceMs = 150,
134
+ maxHeight,
135
+ showSelectAll = false,
136
+ selectAllLabel = 'Select All',
137
+ showSelectedCount = false,
138
+ emptyMessage = 'No items available',
139
+ noResultsMessage = 'No items match your search',
140
+ size = 'md',
141
+ variant = 'default',
142
+ className = '',
143
+ }: CheckboxListProps<T>) {
144
+ const [searchTerm, setSearchTerm] = useState('');
145
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
146
+ const [internalExpandedGroups, setInternalExpandedGroups] = useState<Set<string>>(
147
+ new Set(defaultExpandedGroups || [])
148
+ );
149
+
150
+ // Debounce search
151
+ const handleSearchChange = useCallback((value: string) => {
152
+ setSearchTerm(value);
153
+ const timer = setTimeout(() => {
154
+ setDebouncedSearchTerm(value);
155
+ }, debounceMs);
156
+ return () => clearTimeout(timer);
157
+ }, [debounceMs]);
158
+
159
+ // Filter items based on search
160
+ const filteredItems = useMemo(() => {
161
+ if (!debouncedSearchTerm) return items;
162
+
163
+ const term = debouncedSearchTerm.toLowerCase();
164
+ return items.filter(item => {
165
+ if (filterFn) {
166
+ return filterFn(item, debouncedSearchTerm);
167
+ }
168
+ return (
169
+ item.label.toLowerCase().includes(term) ||
170
+ item.description?.toLowerCase().includes(term) ||
171
+ item.key.toLowerCase().includes(term)
172
+ );
173
+ });
174
+ }, [items, debouncedSearchTerm, filterFn]);
175
+
176
+ // Group items
177
+ const groupedItems = useMemo(() => {
178
+ const groups = new Map<string | null, CheckboxListItem<T>[]>();
179
+
180
+ filteredItems.forEach(item => {
181
+ const groupKey = item.group || null;
182
+ if (!groups.has(groupKey)) {
183
+ groups.set(groupKey, []);
184
+ }
185
+ groups.get(groupKey)!.push(item);
186
+ });
187
+
188
+ return groups;
189
+ }, [filteredItems]);
190
+
191
+ // Determine expanded groups
192
+ const expandedGroups = controlledExpandedGroups
193
+ ? new Set(controlledExpandedGroups)
194
+ : internalExpandedGroups;
195
+
196
+ const handleGroupToggle = (groupKey: string) => {
197
+ const newExpanded = !expandedGroups.has(groupKey);
198
+
199
+ if (!controlledExpandedGroups) {
200
+ setInternalExpandedGroups(prev => {
201
+ const next = new Set(prev);
202
+ if (newExpanded) {
203
+ next.add(groupKey);
204
+ } else {
205
+ next.delete(groupKey);
206
+ }
207
+ return next;
208
+ });
209
+ }
210
+
211
+ onGroupToggle?.(groupKey, newExpanded);
212
+ };
213
+
214
+ // Selection handlers
215
+ const handleItemToggle = (key: string) => {
216
+ const newSelected = selectedKeys.includes(key)
217
+ ? selectedKeys.filter(k => k !== key)
218
+ : [...selectedKeys, key];
219
+ onSelectionChange(newSelected);
220
+ };
221
+
222
+ const handleSelectAll = () => {
223
+ const enabledItems = filteredItems.filter(item => !item.disabled);
224
+ const allSelected = enabledItems.every(item => selectedKeys.includes(item.key));
225
+
226
+ if (allSelected) {
227
+ // Deselect all filtered items
228
+ const filteredKeys = new Set(enabledItems.map(item => item.key));
229
+ onSelectionChange(selectedKeys.filter(key => !filteredKeys.has(key)));
230
+ } else {
231
+ // Select all filtered items
232
+ const newKeys = new Set([...selectedKeys, ...enabledItems.map(item => item.key)]);
233
+ onSelectionChange(Array.from(newKeys));
234
+ }
235
+ };
236
+
237
+ const sizeStyle = sizeClasses[size];
238
+ const enabledItems = filteredItems.filter(item => !item.disabled);
239
+ const allSelected = enabledItems.length > 0 && enabledItems.every(item => selectedKeys.includes(item.key));
240
+ const someSelected = enabledItems.some(item => selectedKeys.includes(item.key)) && !allSelected;
241
+
242
+ // Check if we have groups
243
+ const hasGroups = groupedItems.size > 1 || (groupedItems.size === 1 && !groupedItems.has(null));
244
+
245
+ return (
246
+ <div className={`${variantClasses[variant]} ${className}`}>
247
+ {/* Search Input */}
248
+ {searchable && (
249
+ <div className={`${sizeStyle.item} border-b border-paper-200`}>
250
+ <Input
251
+ value={searchTerm}
252
+ onChange={(e) => handleSearchChange(e.target.value)}
253
+ placeholder={searchPlaceholder}
254
+ prefixIcon={<Search className="h-4 w-4" />}
255
+ size={size === 'lg' ? 'md' : 'sm'}
256
+ clearable
257
+ onClear={() => {
258
+ setSearchTerm('');
259
+ setDebouncedSearchTerm('');
260
+ }}
261
+ />
262
+ </div>
263
+ )}
264
+
265
+ {/* Select All & Count */}
266
+ {(showSelectAll || showSelectedCount) && filteredItems.length > 0 && (
267
+ <div className={`${sizeStyle.item} border-b border-paper-200 flex items-center justify-between`}>
268
+ {showSelectAll && (
269
+ <Checkbox
270
+ checked={allSelected}
271
+ indeterminate={someSelected}
272
+ onChange={handleSelectAll}
273
+ label={selectAllLabel}
274
+ size={size}
275
+ />
276
+ )}
277
+ {showSelectedCount && (
278
+ <span className={`${sizeStyle.description} text-ink-500`}>
279
+ {selectedKeys.length} selected
280
+ </span>
281
+ )}
282
+ </div>
283
+ )}
284
+
285
+ {/* List Content */}
286
+ <div
287
+ className="overflow-y-auto"
288
+ style={{ maxHeight: maxHeight || undefined }}
289
+ >
290
+ {/* Empty State */}
291
+ {items.length === 0 && (
292
+ <div className={`${sizeStyle.item} text-ink-500 ${sizeStyle.text} text-center`}>
293
+ {emptyMessage}
294
+ </div>
295
+ )}
296
+
297
+ {/* No Results */}
298
+ {items.length > 0 && filteredItems.length === 0 && (
299
+ <div className={`${sizeStyle.item} text-ink-500 ${sizeStyle.text} text-center`}>
300
+ {noResultsMessage}
301
+ </div>
302
+ )}
303
+
304
+ {/* Grouped Items */}
305
+ {hasGroups ? (
306
+ Array.from(groupedItems.entries()).map(([groupKey, groupItems]) => {
307
+ if (groupKey === null) {
308
+ // Ungrouped items
309
+ return groupItems.map(item => (
310
+ <CheckboxListItemRow
311
+ key={item.key}
312
+ item={item}
313
+ selected={selectedKeys.includes(item.key)}
314
+ onToggle={() => handleItemToggle(item.key)}
315
+ size={size}
316
+ sizeStyle={sizeStyle}
317
+ />
318
+ ));
319
+ }
320
+
321
+ const isExpanded = expandedGroups.has(groupKey);
322
+ const groupLabel = groupLabels[groupKey] || groupKey;
323
+ const groupSelectedCount = groupItems.filter(item => selectedKeys.includes(item.key)).length;
324
+
325
+ return (
326
+ <div key={groupKey}>
327
+ {/* Group Header */}
328
+ <button
329
+ type="button"
330
+ onClick={() => handleGroupToggle(groupKey)}
331
+ className={`
332
+ w-full flex items-center justify-between
333
+ ${sizeStyle.groupHeader}
334
+ font-medium text-ink-700 bg-paper-50
335
+ hover:bg-paper-100 transition-colors
336
+ border-b border-paper-200
337
+ `}
338
+ >
339
+ <div className="flex items-center gap-2">
340
+ {isExpanded ? (
341
+ <ChevronDown className="h-4 w-4 text-ink-400" />
342
+ ) : (
343
+ <ChevronRight className="h-4 w-4 text-ink-400" />
344
+ )}
345
+ <span>{groupLabel}</span>
346
+ </div>
347
+ <span className="text-ink-400 font-normal">
348
+ {groupSelectedCount > 0 && `${groupSelectedCount}/`}{groupItems.length}
349
+ </span>
350
+ </button>
351
+
352
+ {/* Group Items */}
353
+ {isExpanded && (
354
+ <div>
355
+ {groupItems.map(item => (
356
+ <CheckboxListItemRow
357
+ key={item.key}
358
+ item={item}
359
+ selected={selectedKeys.includes(item.key)}
360
+ onToggle={() => handleItemToggle(item.key)}
361
+ size={size}
362
+ sizeStyle={sizeStyle}
363
+ indented
364
+ />
365
+ ))}
366
+ </div>
367
+ )}
368
+ </div>
369
+ );
370
+ })
371
+ ) : (
372
+ // Flat list (no groups)
373
+ filteredItems.map(item => (
374
+ <CheckboxListItemRow
375
+ key={item.key}
376
+ item={item}
377
+ selected={selectedKeys.includes(item.key)}
378
+ onToggle={() => handleItemToggle(item.key)}
379
+ size={size}
380
+ sizeStyle={sizeStyle}
381
+ />
382
+ ))
383
+ )}
384
+ </div>
385
+ </div>
386
+ );
387
+ }
388
+
389
+ // Helper component for rendering individual items
390
+ function CheckboxListItemRow<T>({
391
+ item,
392
+ selected,
393
+ onToggle,
394
+ size,
395
+ sizeStyle,
396
+ indented = false,
397
+ }: {
398
+ item: CheckboxListItem<T>;
399
+ selected: boolean;
400
+ onToggle: () => void;
401
+ size: 'sm' | 'md' | 'lg';
402
+ sizeStyle: typeof sizeClasses['md'];
403
+ indented?: boolean;
404
+ }) {
405
+ return (
406
+ <div
407
+ className={`
408
+ ${sizeStyle.item}
409
+ ${indented ? 'pl-8' : ''}
410
+ hover:bg-paper-50 transition-colors
411
+ border-b border-paper-100 last:border-b-0
412
+ ${item.disabled ? 'opacity-50' : ''}
413
+ `}
414
+ >
415
+ <div className="flex items-start gap-3">
416
+ <Checkbox
417
+ checked={selected}
418
+ onChange={onToggle}
419
+ disabled={item.disabled}
420
+ size={size}
421
+ />
422
+ <div className="flex flex-col flex-1 min-w-0">
423
+ <span className={sizeStyle.text}>{item.label}</span>
424
+ {item.description && (
425
+ <span className={`${sizeStyle.description} text-ink-500`}>
426
+ {item.description}
427
+ </span>
428
+ )}
429
+ </div>
430
+ </div>
431
+ </div>
432
+ );
433
+ }