@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,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;
|