@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
@@ -4,10 +4,12 @@
4
4
  *
5
5
  * Replaces window.confirm() with a styled, accessible modal dialog.
6
6
  * Supports different variants (danger, warning, info) and customizable buttons.
7
+ *
8
+ * On mobile, automatically renders as a BottomSheet for better touch interaction.
7
9
  */
8
10
 
9
11
  import React from 'react';
10
- import Modal, { ModalFooter } from './Modal';
12
+ import Modal, { ModalFooter, ModalProps } from './Modal';
11
13
  import { AlertTriangle, Info, Trash2 } from 'lucide-react';
12
14
 
13
15
  export interface ConfirmDialogProps {
@@ -21,6 +23,10 @@ export interface ConfirmDialogProps {
21
23
  variant?: 'danger' | 'warning' | 'info';
22
24
  icon?: React.ReactNode;
23
25
  isLoading?: boolean;
26
+ /** Mobile display mode (inherited from Modal) */
27
+ mobileMode?: ModalProps['mobileMode'];
28
+ /** Height preset for BottomSheet on mobile */
29
+ mobileHeight?: ModalProps['mobileHeight'];
24
30
  }
25
31
 
26
32
  const variantStyles = {
@@ -44,6 +50,41 @@ const variantStyles = {
44
50
  },
45
51
  };
46
52
 
53
+ /**
54
+ * ConfirmDialog - Confirmation dialog with mobile support
55
+ *
56
+ * @example Basic usage
57
+ * ```tsx
58
+ * <ConfirmDialog
59
+ * isOpen={isOpen}
60
+ * onClose={handleClose}
61
+ * onConfirm={handleDelete}
62
+ * title="Delete Item"
63
+ * message="Are you sure you want to delete this item? This action cannot be undone."
64
+ * variant="danger"
65
+ * />
66
+ * ```
67
+ *
68
+ * @example With useConfirmDialog hook
69
+ * ```tsx
70
+ * const confirmDialog = useConfirmDialog();
71
+ *
72
+ * const handleDelete = () => {
73
+ * confirmDialog.show({
74
+ * title: 'Delete Item',
75
+ * message: 'Are you sure?',
76
+ * onConfirm: async () => await deleteItem(),
77
+ * });
78
+ * };
79
+ *
80
+ * return (
81
+ * <>
82
+ * <button onClick={handleDelete}>Delete</button>
83
+ * <ConfirmDialog {...confirmDialog.props} />
84
+ * </>
85
+ * );
86
+ * ```
87
+ */
47
88
  export default function ConfirmDialog({
48
89
  isOpen,
49
90
  onClose,
@@ -55,6 +96,8 @@ export default function ConfirmDialog({
55
96
  variant = 'danger',
56
97
  icon,
57
98
  isLoading = false,
99
+ mobileMode = 'auto',
100
+ mobileHeight = 'sm',
58
101
  }: ConfirmDialogProps) {
59
102
  const variantStyle = variantStyles[variant];
60
103
  const IconComponent = icon || variantStyle.icon;
@@ -65,7 +108,16 @@ export default function ConfirmDialog({
65
108
  };
66
109
 
67
110
  return (
68
- <Modal isOpen={isOpen} onClose={onClose} title={typeof title === 'string' ? title : String(title)} size="sm" showCloseButton={false}>
111
+ <Modal
112
+ isOpen={isOpen}
113
+ onClose={onClose}
114
+ title={typeof title === 'string' ? title : String(title)}
115
+ size="sm"
116
+ showCloseButton={false}
117
+ mobileMode={mobileMode}
118
+ mobileHeight={mobileHeight}
119
+ mobileShowHandle={false}
120
+ >
69
121
  <div className="flex items-start gap-4">
70
122
  {/* Icon */}
71
123
  <div className={`flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-full ${variantStyle.iconBg}`}>
@@ -89,14 +141,14 @@ export default function ConfirmDialog({
89
141
  <button
90
142
  onClick={onClose}
91
143
  disabled={isLoading}
92
- className="px-4 py-2 text-sm font-medium text-ink-700 bg-white border border-paper-300 rounded-lg hover:bg-paper-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
144
+ className="px-4 py-2 text-sm font-medium text-ink-700 bg-white border border-paper-300 rounded-lg hover:bg-paper-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors min-h-touch-sm"
93
145
  >
94
146
  {typeof cancelLabel === 'string' ? cancelLabel : String(cancelLabel)}
95
147
  </button>
96
148
  <button
97
149
  onClick={handleConfirm}
98
150
  disabled={isLoading}
99
- className={`px-4 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${variantStyle.button}`}
151
+ className={`px-4 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors min-h-touch-sm ${variantStyle.button}`}
100
152
  >
101
153
  {isLoading ? (
102
154
  <span className="flex items-center gap-2">
@@ -5,6 +5,8 @@ import { ChevronDown, ChevronRight, MoreVertical, Edit, Trash } from 'lucide-rea
5
5
  import Menu, { MenuItem } from './Menu';
6
6
  import Pagination from './Pagination';
7
7
  import Select from './Select';
8
+ import DataTableCardView, { CardViewConfig } from './DataTableCardView';
9
+ import { useIsMobile } from '../hooks/useResponsive';
8
10
 
9
11
  /**
10
12
  * Base data item interface - all data items must have an id
@@ -247,6 +249,16 @@ interface DataTableProps<T extends BaseDataItem = BaseDataItem> {
247
249
  onPageSizeChange?: (pageSize: number) => void;
248
250
  /** Show page size selector (default: true when paginated) */
249
251
  showPageSizeSelector?: boolean;
252
+
253
+ // Mobile view props
254
+ /** Mobile view mode: 'auto' (detect viewport), 'card' (always cards), 'table' (always table) */
255
+ mobileView?: 'auto' | 'card' | 'table';
256
+ /** Configuration for card view layout */
257
+ cardConfig?: CardViewConfig<T>;
258
+ /** Gap between cards in card view */
259
+ cardGap?: 'sm' | 'md' | 'lg';
260
+ /** Custom class name for cards */
261
+ cardClassName?: string;
250
262
  }
251
263
 
252
264
  /**
@@ -517,7 +529,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
517
529
  pageSizeOptions = [10, 25, 50, 100],
518
530
  onPageSizeChange,
519
531
  showPageSizeSelector = true,
532
+ // Mobile view props
533
+ mobileView = 'auto',
534
+ cardConfig,
535
+ cardGap = 'md',
536
+ cardClassName,
520
537
  }: DataTableProps<T>) {
538
+ // Mobile detection for auto mode
539
+ const isMobileViewport = useIsMobile();
521
540
  // Column resizing state
522
541
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
523
542
  const [resizingColumn, setResizingColumn] = useState<string | null>(null);
@@ -1536,11 +1555,51 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1536
1555
  );
1537
1556
  };
1538
1557
 
1558
+ // Determine if we should show card view
1559
+ const shouldShowCardView =
1560
+ mobileView === 'card' ||
1561
+ (mobileView === 'auto' && isMobileViewport);
1562
+
1563
+ // Card view content
1564
+ const cardViewContent = shouldShowCardView ? (
1565
+ <DataTableCardView
1566
+ data={data}
1567
+ columns={visibleColumns}
1568
+ cardConfig={cardConfig}
1569
+ loading={loading}
1570
+ loadingRows={loadingRows}
1571
+ emptyMessage={emptyMessage}
1572
+ onCardClick={onRowClick}
1573
+ onCardLongPress={(item, event) => {
1574
+ if (enableContextMenu && allActions.length > 0) {
1575
+ event.preventDefault();
1576
+ const clientX = 'touches' in event ? event.touches[0].clientX : (event as React.MouseEvent).clientX;
1577
+ const clientY = 'touches' in event ? event.touches[0].clientY : (event as React.MouseEvent).clientY;
1578
+ setContextMenuState({
1579
+ isOpen: true,
1580
+ position: { x: clientX, y: clientY },
1581
+ item,
1582
+ });
1583
+ }
1584
+ }}
1585
+ selectable={selectable}
1586
+ selectedRows={selectedRowsSet}
1587
+ onSelectionChange={onRowSelect ? (rows) => onRowSelect(rows) : undefined}
1588
+ keyExtractor={getRowKey}
1589
+ actions={allActions}
1590
+ onEdit={onEdit}
1591
+ onDelete={onDelete}
1592
+ className={className}
1593
+ cardClassName={cardClassName}
1594
+ gap={cardGap}
1595
+ />
1596
+ ) : null;
1597
+
1539
1598
  // Render with context menu
1540
1599
  return (
1541
1600
  <>
1542
1601
  {renderPaginationControls()}
1543
- {finalContent}
1602
+ {shouldShowCardView ? cardViewContent : finalContent}
1544
1603
  {contextMenuState.isOpen && contextMenuState.item && (
1545
1604
  <Menu
1546
1605
  items={convertActionsToMenuItems(contextMenuState.item)}
@@ -0,0 +1,307 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import DataTableCardView from './DataTableCardView';
4
+ import { DataTableColumn } from './DataTable';
5
+ import Badge from './Badge';
6
+ import { Edit, Trash, Eye, Mail } from 'lucide-react';
7
+
8
+ interface User {
9
+ id: string;
10
+ name: string;
11
+ email: string;
12
+ department: string;
13
+ role: string;
14
+ status: 'active' | 'inactive' | 'pending';
15
+ avatar?: string;
16
+ joinDate: string;
17
+ }
18
+
19
+ const sampleUsers: User[] = [
20
+ {
21
+ id: '1',
22
+ name: 'John Smith',
23
+ email: 'john.smith@example.com',
24
+ department: 'Engineering',
25
+ role: 'Senior Developer',
26
+ status: 'active',
27
+ joinDate: '2023-01-15',
28
+ },
29
+ {
30
+ id: '2',
31
+ name: 'Sarah Johnson',
32
+ email: 'sarah.j@example.com',
33
+ department: 'Design',
34
+ role: 'UX Designer',
35
+ status: 'active',
36
+ joinDate: '2023-03-22',
37
+ },
38
+ {
39
+ id: '3',
40
+ name: 'Mike Wilson',
41
+ email: 'mike.w@example.com',
42
+ department: 'Marketing',
43
+ role: 'Marketing Manager',
44
+ status: 'pending',
45
+ joinDate: '2024-01-10',
46
+ },
47
+ {
48
+ id: '4',
49
+ name: 'Emily Davis',
50
+ email: 'emily.d@example.com',
51
+ department: 'Engineering',
52
+ role: 'DevOps Engineer',
53
+ status: 'inactive',
54
+ joinDate: '2022-06-05',
55
+ },
56
+ {
57
+ id: '5',
58
+ name: 'Alex Chen',
59
+ email: 'alex.c@example.com',
60
+ department: 'Product',
61
+ role: 'Product Manager',
62
+ status: 'active',
63
+ joinDate: '2023-09-01',
64
+ },
65
+ ];
66
+
67
+ const columns: DataTableColumn<User>[] = [
68
+ { key: 'name', header: 'Name' },
69
+ { key: 'email', header: 'Email' },
70
+ { key: 'department', header: 'Department' },
71
+ { key: 'role', header: 'Role' },
72
+ {
73
+ key: 'status',
74
+ header: 'Status',
75
+ render: (item) => {
76
+ const variants = {
77
+ active: 'success',
78
+ inactive: 'default',
79
+ pending: 'warning',
80
+ } as const;
81
+ return (
82
+ <Badge variant={variants[item.status]} size="sm">
83
+ {item.status.charAt(0).toUpperCase() + item.status.slice(1)}
84
+ </Badge>
85
+ );
86
+ },
87
+ },
88
+ { key: 'joinDate', header: 'Join Date' },
89
+ ];
90
+
91
+ const meta: Meta<typeof DataTableCardView> = {
92
+ title: 'Data Display/DataTableCardView',
93
+ component: DataTableCardView,
94
+ parameters: {
95
+ layout: 'padded',
96
+ viewport: {
97
+ defaultViewport: 'mobileM',
98
+ },
99
+ docs: {
100
+ description: {
101
+ component: `
102
+ Mobile-friendly card view for data tables. Renders each row as a touchable card
103
+ with configurable title, subtitle, metadata fields, and status badge.
104
+
105
+ Features:
106
+ - Touch-optimized with active states
107
+ - Configurable card layout
108
+ - Selection mode support
109
+ - Loading skeletons
110
+ - Column render function reuse
111
+ - Long-press for context actions
112
+ `,
113
+ },
114
+ },
115
+ },
116
+ };
117
+
118
+ export default meta;
119
+ type Story = StoryObj<typeof DataTableCardView<User>>;
120
+
121
+ /**
122
+ * Default card view with basic configuration
123
+ */
124
+ export const Default: Story = {
125
+ args: {
126
+ data: sampleUsers,
127
+ columns,
128
+ cardConfig: {
129
+ titleKey: 'name',
130
+ subtitleKey: 'email',
131
+ metadataKeys: ['department', 'role'],
132
+ badgeKey: 'status',
133
+ },
134
+ onCardClick: (user) => alert(`Clicked: ${user.name}`),
135
+ },
136
+ };
137
+
138
+ /**
139
+ * Card view with avatar/initials display
140
+ */
141
+ export const WithAvatar: Story = {
142
+ args: {
143
+ data: sampleUsers,
144
+ columns,
145
+ cardConfig: {
146
+ titleKey: 'name',
147
+ subtitleKey: 'role',
148
+ metadataKeys: ['email', 'department'],
149
+ badgeKey: 'status',
150
+ avatarKey: 'name', // Will show initials since no avatar URL
151
+ },
152
+ onCardClick: (user) => alert(`Clicked: ${user.name}`),
153
+ },
154
+ };
155
+
156
+ /**
157
+ * Card view with chevron indicator for navigation
158
+ */
159
+ export const WithChevron: Story = {
160
+ args: {
161
+ data: sampleUsers,
162
+ columns,
163
+ cardConfig: {
164
+ titleKey: 'name',
165
+ subtitleKey: 'email',
166
+ metadataKeys: ['department'],
167
+ badgeKey: 'status',
168
+ showChevron: true,
169
+ },
170
+ onCardClick: (user) => alert(`Navigate to: ${user.name}`),
171
+ },
172
+ };
173
+
174
+ /**
175
+ * Card view with selection enabled
176
+ */
177
+ export const WithSelection: Story = {
178
+ render: () => {
179
+ const [selected, setSelected] = useState<Set<string>>(new Set(['1', '3']));
180
+
181
+ return (
182
+ <div className="space-y-4">
183
+ <div className="text-sm text-ink-600">
184
+ Selected: {selected.size} items
185
+ </div>
186
+ <DataTableCardView
187
+ data={sampleUsers}
188
+ columns={columns}
189
+ cardConfig={{
190
+ titleKey: 'name',
191
+ subtitleKey: 'email',
192
+ metadataKeys: ['department', 'role'],
193
+ badgeKey: 'status',
194
+ }}
195
+ selectable
196
+ selectedRows={selected}
197
+ onSelectionChange={(rows) => setSelected(new Set(rows))}
198
+ />
199
+ </div>
200
+ );
201
+ },
202
+ };
203
+
204
+ /**
205
+ * Card view with action menu
206
+ */
207
+ export const WithActions: Story = {
208
+ args: {
209
+ data: sampleUsers,
210
+ columns,
211
+ cardConfig: {
212
+ titleKey: 'name',
213
+ subtitleKey: 'email',
214
+ metadataKeys: ['department'],
215
+ badgeKey: 'status',
216
+ },
217
+ onEdit: (user) => alert(`Edit: ${user.name}`),
218
+ onDelete: (user) => alert(`Delete: ${user.name}`),
219
+ actions: [
220
+ {
221
+ label: 'View Profile',
222
+ icon: <Eye className="h-4 w-4" />,
223
+ onClick: (user) => alert(`View: ${user.name}`),
224
+ },
225
+ {
226
+ label: 'Send Email',
227
+ icon: <Mail className="h-4 w-4" />,
228
+ onClick: (user) => alert(`Email: ${user.email}`),
229
+ },
230
+ ],
231
+ onCardLongPress: (user, event) => {
232
+ // In a real app, this would open an action menu
233
+ console.log('Long press on:', user.name, event);
234
+ },
235
+ },
236
+ };
237
+
238
+ /**
239
+ * Loading state with skeleton cards
240
+ */
241
+ export const Loading: Story = {
242
+ args: {
243
+ data: [],
244
+ columns,
245
+ loading: true,
246
+ loadingRows: 5,
247
+ },
248
+ };
249
+
250
+ /**
251
+ * Empty state
252
+ */
253
+ export const Empty: Story = {
254
+ args: {
255
+ data: [],
256
+ columns,
257
+ emptyMessage: 'No users found. Try adjusting your filters.',
258
+ },
259
+ };
260
+
261
+ /**
262
+ * Compact gap spacing
263
+ */
264
+ export const CompactGap: Story = {
265
+ args: {
266
+ data: sampleUsers,
267
+ columns,
268
+ cardConfig: {
269
+ titleKey: 'name',
270
+ subtitleKey: 'email',
271
+ badgeKey: 'status',
272
+ },
273
+ gap: 'sm',
274
+ },
275
+ };
276
+
277
+ /**
278
+ * Large gap spacing
279
+ */
280
+ export const LargeGap: Story = {
281
+ args: {
282
+ data: sampleUsers,
283
+ columns,
284
+ cardConfig: {
285
+ titleKey: 'name',
286
+ subtitleKey: 'email',
287
+ metadataKeys: ['department', 'role', 'joinDate'],
288
+ badgeKey: 'status',
289
+ },
290
+ gap: 'lg',
291
+ },
292
+ };
293
+
294
+ /**
295
+ * Minimal card (title only)
296
+ */
297
+ export const MinimalCard: Story = {
298
+ args: {
299
+ data: sampleUsers,
300
+ columns,
301
+ cardConfig: {
302
+ titleKey: 'name',
303
+ showChevron: true,
304
+ },
305
+ onCardClick: (user) => alert(`Clicked: ${user.name}`),
306
+ },
307
+ };