@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,419 @@
1
+ // DataTableCardView - Mobile-friendly card-based view for data tables
2
+ // Renders each row as a card with configurable primary/secondary fields
3
+
4
+ import React from 'react';
5
+ import { MoreVertical, ChevronRight } from 'lucide-react';
6
+ import { BaseDataItem, DataTableColumn, DataTableAction } from './DataTable';
7
+ import Checkbox from './Checkbox';
8
+
9
+ /**
10
+ * Configuration for how data should display in card view
11
+ */
12
+ export interface CardViewConfig<T> {
13
+ /** Column key to use as the main title */
14
+ titleKey: keyof T | string;
15
+ /** Column key to use as subtitle (optional) */
16
+ subtitleKey?: keyof T | string;
17
+ /** Column keys to show as metadata rows */
18
+ metadataKeys?: (keyof T | string)[];
19
+ /** Column key to use for badge/status display */
20
+ badgeKey?: keyof T | string;
21
+ /** Column key for avatar/image (renders circular image) */
22
+ avatarKey?: keyof T | string;
23
+ /** Custom render function for entire card content */
24
+ renderCard?: (item: T, columns: DataTableColumn<T>[]) => React.ReactNode;
25
+ /** Show chevron indicator for clickable cards */
26
+ showChevron?: boolean;
27
+ }
28
+
29
+ export interface DataTableCardViewProps<T extends BaseDataItem> {
30
+ /** Data items to display */
31
+ data: T[];
32
+ /** Column definitions (used for rendering values and getting column info) */
33
+ columns: DataTableColumn<T>[];
34
+ /** Card view configuration */
35
+ cardConfig?: CardViewConfig<T>;
36
+ /** Loading state */
37
+ loading?: boolean;
38
+ /** Number of skeleton cards to show while loading */
39
+ loadingRows?: number;
40
+ /** Empty state message */
41
+ emptyMessage?: string;
42
+ /** Click handler for card tap */
43
+ onCardClick?: (item: T) => void;
44
+ /** Long press / context menu handler */
45
+ onCardLongPress?: (item: T, event: React.TouchEvent | React.MouseEvent) => void;
46
+ /** Enable selection mode */
47
+ selectable?: boolean;
48
+ /** Currently selected row keys */
49
+ selectedRows?: Set<string>;
50
+ /** Selection change handler */
51
+ onSelectionChange?: (selectedRows: string[]) => void;
52
+ /** Function to extract unique key from row */
53
+ keyExtractor?: (row: T) => string;
54
+ /** Row actions (shown in action menu) */
55
+ actions?: DataTableAction<T>[];
56
+ /** Built-in edit handler */
57
+ onEdit?: (item: T) => void;
58
+ /** Built-in delete handler */
59
+ onDelete?: (item: T) => void;
60
+ /** Additional CSS classes */
61
+ className?: string;
62
+ /** Custom card class name */
63
+ cardClassName?: string;
64
+ /** Gap between cards */
65
+ gap?: 'sm' | 'md' | 'lg';
66
+ }
67
+
68
+ /**
69
+ * Get value from item by key path (supports nested keys like 'user.name')
70
+ */
71
+ function getValueByKey<T>(item: T, key: keyof T | string): any {
72
+ if (typeof key !== 'string') {
73
+ return item[key];
74
+ }
75
+
76
+ const keys = key.split('.');
77
+ let value: any = item;
78
+ for (const k of keys) {
79
+ if (value == null) return undefined;
80
+ value = value[k];
81
+ }
82
+ return value;
83
+ }
84
+
85
+ /**
86
+ * Skeleton card for loading state
87
+ */
88
+ function SkeletonCard({ className = '' }: { className?: string }) {
89
+ return (
90
+ <div className={`bg-white rounded-lg border border-paper-200 p-4 animate-pulse ${className}`}>
91
+ <div className="flex items-start gap-3">
92
+ {/* Avatar skeleton */}
93
+ <div className="w-10 h-10 rounded-full bg-paper-200 flex-shrink-0" />
94
+
95
+ <div className="flex-1 min-w-0">
96
+ {/* Title skeleton */}
97
+ <div className="h-5 bg-paper-200 rounded w-3/4 mb-2" />
98
+ {/* Subtitle skeleton */}
99
+ <div className="h-4 bg-paper-100 rounded w-1/2 mb-3" />
100
+ {/* Metadata skeleton */}
101
+ <div className="space-y-2">
102
+ <div className="h-3 bg-paper-100 rounded w-2/3" />
103
+ <div className="h-3 bg-paper-100 rounded w-1/2" />
104
+ </div>
105
+ </div>
106
+
107
+ {/* Badge skeleton */}
108
+ <div className="h-6 w-16 bg-paper-200 rounded-full" />
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ /**
115
+ * DataTableCardView - Mobile-friendly card view for data tables
116
+ *
117
+ * Renders data as cards instead of table rows, optimized for touch interaction.
118
+ * Automatically uses column render functions for consistent data display.
119
+ *
120
+ * @example Basic usage
121
+ * ```tsx
122
+ * <DataTableCardView
123
+ * data={users}
124
+ * columns={columns}
125
+ * cardConfig={{
126
+ * titleKey: 'name',
127
+ * subtitleKey: 'email',
128
+ * metadataKeys: ['department', 'role'],
129
+ * badgeKey: 'status',
130
+ * }}
131
+ * onCardClick={(user) => navigate(`/users/${user.id}`)}
132
+ * />
133
+ * ```
134
+ *
135
+ * @example With selection
136
+ * ```tsx
137
+ * <DataTableCardView
138
+ * data={orders}
139
+ * columns={columns}
140
+ * cardConfig={{
141
+ * titleKey: 'orderNumber',
142
+ * subtitleKey: 'customer',
143
+ * badgeKey: 'status',
144
+ * }}
145
+ * selectable
146
+ * selectedRows={selectedOrders}
147
+ * onSelectionChange={setSelectedOrders}
148
+ * />
149
+ * ```
150
+ */
151
+ export function DataTableCardView<T extends BaseDataItem>({
152
+ data,
153
+ columns,
154
+ cardConfig,
155
+ loading = false,
156
+ loadingRows = 5,
157
+ emptyMessage = 'No items to display',
158
+ onCardClick,
159
+ onCardLongPress,
160
+ selectable = false,
161
+ selectedRows = new Set(),
162
+ onSelectionChange,
163
+ keyExtractor = (row) => String(row.id),
164
+ actions,
165
+ onEdit,
166
+ onDelete,
167
+ className = '',
168
+ cardClassName = '',
169
+ gap = 'md',
170
+ }: DataTableCardViewProps<T>) {
171
+ const gapClasses = {
172
+ sm: 'gap-2',
173
+ md: 'gap-3',
174
+ lg: 'gap-4',
175
+ };
176
+
177
+ // Find column by key to use its render function
178
+ const getColumn = (key: keyof T | string): DataTableColumn<T> | undefined => {
179
+ return columns.find(col => col.key === key);
180
+ };
181
+
182
+ // Render a value using column's render function if available
183
+ const renderValue = (item: T, key: keyof T | string): React.ReactNode => {
184
+ const column = getColumn(key);
185
+ const value = getValueByKey(item, key);
186
+
187
+ if (column?.render) {
188
+ return column.render(item, value);
189
+ }
190
+
191
+ if (value == null) return '-';
192
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
193
+ if (value instanceof Date) return value.toLocaleDateString();
194
+ return String(value);
195
+ };
196
+
197
+ // Handle card selection toggle
198
+ const handleSelectionToggle = (item: T, event: React.MouseEvent) => {
199
+ event.stopPropagation();
200
+ const key = keyExtractor(item);
201
+ const newSelected = new Set(selectedRows);
202
+
203
+ if (newSelected.has(key)) {
204
+ newSelected.delete(key);
205
+ } else {
206
+ newSelected.add(key);
207
+ }
208
+
209
+ onSelectionChange?.(Array.from(newSelected));
210
+ };
211
+
212
+ // Handle card click
213
+ const handleCardClick = (item: T) => {
214
+ if (selectable && selectedRows.size > 0) {
215
+ // If in selection mode, toggle selection instead
216
+ const key = keyExtractor(item);
217
+ const newSelected = new Set(selectedRows);
218
+ if (newSelected.has(key)) {
219
+ newSelected.delete(key);
220
+ } else {
221
+ newSelected.add(key);
222
+ }
223
+ onSelectionChange?.(Array.from(newSelected));
224
+ } else {
225
+ onCardClick?.(item);
226
+ }
227
+ };
228
+
229
+ // Handle long press for context actions
230
+ const handleLongPress = (item: T, event: React.TouchEvent | React.MouseEvent) => {
231
+ onCardLongPress?.(item, event);
232
+ };
233
+
234
+ // Loading state
235
+ if (loading) {
236
+ return (
237
+ <div className={`flex flex-col ${gapClasses[gap]} ${className}`}>
238
+ {Array.from({ length: loadingRows }).map((_, i) => (
239
+ <SkeletonCard key={i} className={cardClassName} />
240
+ ))}
241
+ </div>
242
+ );
243
+ }
244
+
245
+ // Empty state
246
+ if (data.length === 0) {
247
+ return (
248
+ <div className={`flex items-center justify-center py-12 px-4 ${className}`}>
249
+ <p className="text-ink-500 text-center">{emptyMessage}</p>
250
+ </div>
251
+ );
252
+ }
253
+
254
+ // Determine default card config if not provided
255
+ const config: CardViewConfig<T> = cardConfig || {
256
+ titleKey: columns[0]?.key || 'id',
257
+ subtitleKey: columns[1]?.key,
258
+ metadataKeys: columns.slice(2, 5).map(c => c.key),
259
+ };
260
+
261
+ return (
262
+ <div className={`flex flex-col ${gapClasses[gap]} ${className}`}>
263
+ {data.map((item) => {
264
+ const key = keyExtractor(item);
265
+ const isSelected = selectedRows.has(key);
266
+
267
+ // Custom card render
268
+ if (config.renderCard) {
269
+ return (
270
+ <div
271
+ key={key}
272
+ onClick={() => handleCardClick(item)}
273
+ onContextMenu={(e) => {
274
+ e.preventDefault();
275
+ handleLongPress(item, e);
276
+ }}
277
+ className={`
278
+ cursor-pointer transition-all duration-200
279
+ ${isSelected ? 'ring-2 ring-accent-500' : ''}
280
+ ${cardClassName}
281
+ `}
282
+ >
283
+ {config.renderCard(item, columns)}
284
+ </div>
285
+ );
286
+ }
287
+
288
+ // Default card layout
289
+ const titleColumn = getColumn(config.titleKey);
290
+ const titleValue = getValueByKey(item, config.titleKey);
291
+
292
+ return (
293
+ <div
294
+ key={key}
295
+ onClick={() => handleCardClick(item)}
296
+ onContextMenu={(e) => {
297
+ e.preventDefault();
298
+ handleLongPress(item, e);
299
+ }}
300
+ className={`
301
+ bg-white rounded-lg border border-paper-200 p-4
302
+ transition-all duration-200 cursor-pointer
303
+ active:scale-[0.98] active:bg-paper-50
304
+ ${isSelected ? 'ring-2 ring-accent-500 bg-accent-50/30' : 'hover:shadow-md hover:border-paper-300'}
305
+ ${cardClassName}
306
+ `}
307
+ >
308
+ <div className="flex items-start gap-3">
309
+ {/* Selection checkbox */}
310
+ {selectable && (
311
+ <div
312
+ className="flex-shrink-0 pt-0.5"
313
+ onClick={(e) => handleSelectionToggle(item, e)}
314
+ >
315
+ <Checkbox
316
+ checked={isSelected}
317
+ onChange={() => {}}
318
+ />
319
+ </div>
320
+ )}
321
+
322
+ {/* Avatar (if configured) */}
323
+ {config.avatarKey && (
324
+ <div className="flex-shrink-0">
325
+ {(() => {
326
+ const avatarValue = getValueByKey(item, config.avatarKey!);
327
+ if (typeof avatarValue === 'string' && avatarValue.startsWith('http')) {
328
+ return (
329
+ <img
330
+ src={avatarValue}
331
+ alt=""
332
+ className="w-10 h-10 rounded-full object-cover"
333
+ />
334
+ );
335
+ }
336
+ // Render initials or placeholder
337
+ const initials = String(titleValue || '').slice(0, 2).toUpperCase();
338
+ return (
339
+ <div className="w-10 h-10 rounded-full bg-accent-100 flex items-center justify-center text-accent-700 font-medium text-sm">
340
+ {initials}
341
+ </div>
342
+ );
343
+ })()}
344
+ </div>
345
+ )}
346
+
347
+ {/* Main content */}
348
+ <div className="flex-1 min-w-0">
349
+ {/* Title */}
350
+ <div className="font-medium text-ink-900 truncate">
351
+ {titleColumn?.render
352
+ ? titleColumn.render(item, titleValue)
353
+ : String(titleValue || '-')
354
+ }
355
+ </div>
356
+
357
+ {/* Subtitle */}
358
+ {config.subtitleKey && (
359
+ <div className="text-sm text-ink-500 truncate mt-0.5">
360
+ {renderValue(item, config.subtitleKey)}
361
+ </div>
362
+ )}
363
+
364
+ {/* Metadata rows */}
365
+ {config.metadataKeys && config.metadataKeys.length > 0 && (
366
+ <div className="mt-2 space-y-1">
367
+ {config.metadataKeys.map((metaKey) => {
368
+ const column = getColumn(metaKey);
369
+ return (
370
+ <div key={String(metaKey)} className="flex items-center text-xs">
371
+ <span className="text-ink-400 w-20 flex-shrink-0 truncate">
372
+ {column?.header || String(metaKey)}:
373
+ </span>
374
+ <span className="text-ink-600 truncate">
375
+ {renderValue(item, metaKey)}
376
+ </span>
377
+ </div>
378
+ );
379
+ })}
380
+ </div>
381
+ )}
382
+ </div>
383
+
384
+ {/* Right side: Badge and/or actions */}
385
+ <div className="flex flex-col items-end gap-2 flex-shrink-0">
386
+ {/* Badge/Status */}
387
+ {config.badgeKey && (
388
+ <div>
389
+ {renderValue(item, config.badgeKey)}
390
+ </div>
391
+ )}
392
+
393
+ {/* Chevron indicator */}
394
+ {config.showChevron && onCardClick && (
395
+ <ChevronRight className="h-5 w-5 text-ink-300" />
396
+ )}
397
+
398
+ {/* Actions menu trigger */}
399
+ {(actions || onEdit || onDelete) && (
400
+ <button
401
+ onClick={(e) => {
402
+ e.stopPropagation();
403
+ handleLongPress(item, e);
404
+ }}
405
+ className="p-1 rounded hover:bg-paper-100 text-ink-400 hover:text-ink-600 -mr-1"
406
+ >
407
+ <MoreVertical className="h-5 w-5" />
408
+ </button>
409
+ )}
410
+ </div>
411
+ </div>
412
+ </div>
413
+ );
414
+ })}
415
+ </div>
416
+ );
417
+ }
418
+
419
+ export default DataTableCardView;