@startsimpli/ui 0.1.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/README.md +537 -0
- package/package.json +80 -0
- package/src/components/index.ts +50 -0
- package/src/components/navigation/sidebar.tsx +178 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +70 -0
- package/src/components/ui/card.tsx +68 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +12 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/index.ts +24 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +31 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/components/unified-table/UnifiedTable.tsx +553 -0
- package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
- package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
- package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
- package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
- package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
- package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
- package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
- package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
- package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
- package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
- package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
- package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
- package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
- package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
- package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
- package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
- package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
- package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
- package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
- package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
- package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
- package/src/components/unified-table/components/MobileView/README.md +411 -0
- package/src/components/unified-table/components/MobileView/index.tsx +77 -0
- package/src/components/unified-table/components/MobileView/types.ts +77 -0
- package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
- package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
- package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
- package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
- package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
- package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
- package/src/components/unified-table/hooks/index.ts +21 -0
- package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
- package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
- package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
- package/src/components/unified-table/hooks/useFilters.ts +53 -0
- package/src/components/unified-table/hooks/usePagination.ts +120 -0
- package/src/components/unified-table/hooks/useResponsive.ts +50 -0
- package/src/components/unified-table/hooks/useSelection.ts +152 -0
- package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
- package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
- package/src/components/unified-table/hooks/useTableState.ts +103 -0
- package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
- package/src/components/unified-table/hooks/useTableURL.ts +301 -0
- package/src/components/unified-table/index.ts +16 -0
- package/src/components/unified-table/types.ts +393 -0
- package/src/components/unified-table/utils/export.ts +236 -0
- package/src/components/unified-table/utils/index.ts +4 -0
- package/src/components/unified-table/utils/renderers.ts +105 -0
- package/src/components/unified-table/utils/themes.ts +87 -0
- package/src/components/unified-table/utils/validation.ts +122 -0
- package/src/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/theme/contract.ts +46 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/tailwind.config.js +70 -0
- package/src/theme/tailwind.preset.ts +93 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +91 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# MobileView Component System
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The MobileView component system provides a responsive, touch-friendly mobile interface for displaying table data as cards. It automatically switches between table and card views based on screen size and provides extensive customization options.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Responsive Breakpoint Logic**: Automatically switches at 768px (configurable)
|
|
10
|
+
- **Touch-Friendly Actions**: 44px minimum touch targets following iOS/Android guidelines
|
|
11
|
+
- **Selection Support**: Integrated checkbox selection with visual feedback
|
|
12
|
+
- **Custom Renderers**: Full control over title, subtitle, fields, and content
|
|
13
|
+
- **Loading States**: Per-row and global loading indicators
|
|
14
|
+
- **Accessibility**: ARIA labels, keyboard navigation, screen reader support
|
|
15
|
+
|
|
16
|
+
## Components
|
|
17
|
+
|
|
18
|
+
### MobileView
|
|
19
|
+
|
|
20
|
+
Main container component that renders a list of cards.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { MobileView } from '@/components/tables/unified-table/components/MobileView'
|
|
24
|
+
|
|
25
|
+
<MobileView
|
|
26
|
+
data={investors}
|
|
27
|
+
config={mobileConfig}
|
|
28
|
+
getRowId={(row) => row.id}
|
|
29
|
+
loading={false}
|
|
30
|
+
loadingRows={new Set()}
|
|
31
|
+
emptyState={<div>No data</div>}
|
|
32
|
+
/>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Card
|
|
36
|
+
|
|
37
|
+
Individual card component for displaying a single row of data.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { Card } from '@/components/tables/unified-table/components/MobileView'
|
|
41
|
+
|
|
42
|
+
<Card
|
|
43
|
+
row={investor}
|
|
44
|
+
config={mobileConfig}
|
|
45
|
+
rowId={investor.id}
|
|
46
|
+
isLoading={false}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### CardActions
|
|
51
|
+
|
|
52
|
+
Touch-friendly action buttons with overflow menu support.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { CardActions } from '@/components/tables/unified-table/components/MobileView'
|
|
56
|
+
|
|
57
|
+
<CardActions
|
|
58
|
+
actions={[
|
|
59
|
+
{
|
|
60
|
+
id: 'edit',
|
|
61
|
+
label: 'Edit',
|
|
62
|
+
icon: Edit,
|
|
63
|
+
onClick: handleEdit,
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
row={data}
|
|
67
|
+
maxVisibleActions={2}
|
|
68
|
+
/>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
### Basic Configuration
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
const basicConfig: MobileCardConfig = {
|
|
77
|
+
titleKey: 'name',
|
|
78
|
+
subtitleKey: 'email',
|
|
79
|
+
|
|
80
|
+
primaryFields: [
|
|
81
|
+
{ key: 'role', label: 'Role' },
|
|
82
|
+
{ key: 'status', label: 'Status' },
|
|
83
|
+
],
|
|
84
|
+
|
|
85
|
+
secondaryFields: [
|
|
86
|
+
{ key: 'department' },
|
|
87
|
+
{ key: 'location' },
|
|
88
|
+
],
|
|
89
|
+
|
|
90
|
+
actions: [
|
|
91
|
+
{
|
|
92
|
+
id: 'edit',
|
|
93
|
+
icon: Edit,
|
|
94
|
+
onClick: handleEdit,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Advanced Configuration
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
const advancedConfig: MobileCardConfig<Investor> = {
|
|
104
|
+
// Custom title rendering
|
|
105
|
+
titleRender: (investor) => (
|
|
106
|
+
<div className="flex items-center gap-2">
|
|
107
|
+
<span>{investor.name}</span>
|
|
108
|
+
<TierBadge tier={investor.tier} />
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
|
|
112
|
+
// Custom subtitle rendering
|
|
113
|
+
subtitleRender: (investor) => (
|
|
114
|
+
<span>{investor.title} at {investor.firm.name}</span>
|
|
115
|
+
),
|
|
116
|
+
|
|
117
|
+
// Custom image/avatar
|
|
118
|
+
imageRender: (investor) => (
|
|
119
|
+
<Avatar src={investor.avatar} name={investor.name} />
|
|
120
|
+
),
|
|
121
|
+
|
|
122
|
+
// Primary fields with custom renderers
|
|
123
|
+
primaryFields: [
|
|
124
|
+
{
|
|
125
|
+
key: 'tier',
|
|
126
|
+
render: (value) => <TierBadge tier={value} />,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: 'status',
|
|
130
|
+
render: (value) => <StatusBadge status={value} />,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
|
|
134
|
+
// Secondary fields
|
|
135
|
+
secondaryFields: [
|
|
136
|
+
{
|
|
137
|
+
key: 'checkSize',
|
|
138
|
+
label: 'Check Size',
|
|
139
|
+
render: (value) => formatCurrency(value),
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
|
|
143
|
+
// Actions with icons and variants
|
|
144
|
+
actions: [
|
|
145
|
+
{
|
|
146
|
+
id: 'enrich',
|
|
147
|
+
icon: Sparkles,
|
|
148
|
+
variant: 'default',
|
|
149
|
+
onClick: handleEnrich,
|
|
150
|
+
disabled: (row) => row.enrichmentStatus === 'pending',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'email',
|
|
154
|
+
icon: Mail,
|
|
155
|
+
variant: 'outline',
|
|
156
|
+
onClick: (row) => window.location.href = `mailto:${row.email}`,
|
|
157
|
+
hidden: (row) => !row.email,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
|
|
161
|
+
// Custom content sections
|
|
162
|
+
renderCustomContent: (investor) => (
|
|
163
|
+
<InvestorDetails investor={investor} />
|
|
164
|
+
),
|
|
165
|
+
|
|
166
|
+
renderCustomFooter: (investor) => (
|
|
167
|
+
<LastContactInfo date={investor.lastContact} />
|
|
168
|
+
),
|
|
169
|
+
|
|
170
|
+
// Selection
|
|
171
|
+
showSelection: true,
|
|
172
|
+
onSelectionChange: handleSelectionChange,
|
|
173
|
+
isSelected: (row) => selectedIds.has(row.id),
|
|
174
|
+
|
|
175
|
+
// Click handler
|
|
176
|
+
onClick: (investor) => router.push(`/investors/${investor.id}`),
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Field Renderers
|
|
181
|
+
|
|
182
|
+
### Built-in Renderers
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { commonRenderers } from '@/components/tables/unified-table/utils/renderers'
|
|
186
|
+
|
|
187
|
+
{
|
|
188
|
+
key: 'price',
|
|
189
|
+
render: commonRenderers.currency,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
key: 'percentage',
|
|
194
|
+
render: commonRenderers.percentage,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
key: 'createdAt',
|
|
199
|
+
render: commonRenderers.datetime,
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Custom Renderers
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
{
|
|
207
|
+
key: 'status',
|
|
208
|
+
render: (value, row) => {
|
|
209
|
+
const colors = {
|
|
210
|
+
active: 'text-green-600',
|
|
211
|
+
inactive: 'text-gray-500',
|
|
212
|
+
}
|
|
213
|
+
return (
|
|
214
|
+
<span className={colors[value]}>
|
|
215
|
+
{value}
|
|
216
|
+
</span>
|
|
217
|
+
)
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Actions
|
|
223
|
+
|
|
224
|
+
### Basic Actions
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
actions: [
|
|
228
|
+
{
|
|
229
|
+
id: 'edit',
|
|
230
|
+
label: 'Edit',
|
|
231
|
+
icon: Edit,
|
|
232
|
+
onClick: (row) => setEditRow(row),
|
|
233
|
+
},
|
|
234
|
+
]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Conditional Actions
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
actions: [
|
|
241
|
+
{
|
|
242
|
+
id: 'delete',
|
|
243
|
+
label: 'Delete',
|
|
244
|
+
icon: Trash2,
|
|
245
|
+
variant: 'destructive',
|
|
246
|
+
onClick: handleDelete,
|
|
247
|
+
disabled: (row) => row.isProtected,
|
|
248
|
+
hidden: (row) => !hasPermission(row),
|
|
249
|
+
},
|
|
250
|
+
]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Async Actions
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
actions: [
|
|
257
|
+
{
|
|
258
|
+
id: 'enrich',
|
|
259
|
+
label: 'Enrich',
|
|
260
|
+
icon: Sparkles,
|
|
261
|
+
onClick: async (row) => {
|
|
262
|
+
await enrichInvestor(row.id)
|
|
263
|
+
toast.success('Enrichment complete')
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Responsive Behavior
|
|
270
|
+
|
|
271
|
+
### Breakpoints
|
|
272
|
+
|
|
273
|
+
The default breakpoint is 768px, defined in `types.ts`:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
export const MOBILE_BREAKPOINT = 768
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Custom Breakpoints
|
|
280
|
+
|
|
281
|
+
Use the `useResponsive` hook for custom breakpoint logic:
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { useResponsive } from '@/components/tables/unified-table/hooks/useResponsive'
|
|
285
|
+
|
|
286
|
+
const { isMobile, isTablet, isDesktop } = useResponsive({
|
|
287
|
+
mobile: 640,
|
|
288
|
+
tablet: 1024,
|
|
289
|
+
desktop: 1280,
|
|
290
|
+
})
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Accessibility
|
|
294
|
+
|
|
295
|
+
### ARIA Labels
|
|
296
|
+
|
|
297
|
+
All interactive elements include appropriate ARIA labels:
|
|
298
|
+
|
|
299
|
+
- Checkboxes: `aria-label="Select row"`
|
|
300
|
+
- Action buttons: Include visible labels or `sr-only` text
|
|
301
|
+
- Cards: Semantic HTML structure
|
|
302
|
+
|
|
303
|
+
### Keyboard Navigation
|
|
304
|
+
|
|
305
|
+
- Tab navigation through all interactive elements
|
|
306
|
+
- Enter/Space to activate buttons and checkboxes
|
|
307
|
+
- Focus indicators on all focusable elements
|
|
308
|
+
|
|
309
|
+
### Touch Targets
|
|
310
|
+
|
|
311
|
+
All touch targets meet minimum size requirements:
|
|
312
|
+
- Buttons: 44px × 44px minimum
|
|
313
|
+
- Checkboxes: 20px × 20px (5px padding = 30px × 30px touch area)
|
|
314
|
+
|
|
315
|
+
## Performance
|
|
316
|
+
|
|
317
|
+
### Optimization Tips
|
|
318
|
+
|
|
319
|
+
1. **Memoize Renderers**: Use `useCallback` for custom renderers
|
|
320
|
+
2. **Lazy Loading**: Use `React.lazy` for heavy custom content
|
|
321
|
+
3. **Virtual Scrolling**: For large datasets, consider virtualization
|
|
322
|
+
4. **Image Optimization**: Use next/image or lazy loading for avatars
|
|
323
|
+
|
|
324
|
+
### Example
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
const titleRender = useCallback((row: Investor) => (
|
|
328
|
+
<div>{row.name}</div>
|
|
329
|
+
), [])
|
|
330
|
+
|
|
331
|
+
const mobileConfig = useMemo(() => ({
|
|
332
|
+
titleRender,
|
|
333
|
+
// ... other config
|
|
334
|
+
}), [titleRender])
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Examples
|
|
338
|
+
|
|
339
|
+
See the `examples/` directory for complete examples:
|
|
340
|
+
|
|
341
|
+
- `basic-mobile-config.tsx` - Simple user list
|
|
342
|
+
- `advanced-mobile-config.tsx` - Complex investor cards
|
|
343
|
+
|
|
344
|
+
## Best Practices
|
|
345
|
+
|
|
346
|
+
1. **Title/Subtitle**: Always provide clear, meaningful titles
|
|
347
|
+
2. **Primary Fields**: Limit to 3-4 most important fields
|
|
348
|
+
3. **Secondary Fields**: Use for supplementary information
|
|
349
|
+
4. **Actions**: Show max 2-3 primary actions, overflow the rest
|
|
350
|
+
5. **Custom Content**: Use sparingly, maintain consistency
|
|
351
|
+
6. **Loading States**: Always handle loading and error states
|
|
352
|
+
7. **Selection**: Provide clear visual feedback for selected items
|
|
353
|
+
8. **Touch Targets**: Ensure all interactive elements are easily tappable
|
|
354
|
+
|
|
355
|
+
## Type Definitions
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
interface MobileCardConfig<TData> {
|
|
359
|
+
titleKey: string
|
|
360
|
+
titleRender?: (row: TData) => ReactNode
|
|
361
|
+
|
|
362
|
+
subtitleKey?: string
|
|
363
|
+
subtitleRender?: (row: TData) => ReactNode
|
|
364
|
+
|
|
365
|
+
imageKey?: string
|
|
366
|
+
imageRender?: (row: TData) => ReactNode
|
|
367
|
+
|
|
368
|
+
primaryFields: MobileCardField[]
|
|
369
|
+
secondaryFields?: MobileCardField[]
|
|
370
|
+
|
|
371
|
+
actions?: MobileCardAction[]
|
|
372
|
+
|
|
373
|
+
renderCustomContent?: (row: TData) => ReactNode
|
|
374
|
+
renderCustomHeader?: (row: TData) => ReactNode
|
|
375
|
+
renderCustomFooter?: (row: TData) => ReactNode
|
|
376
|
+
|
|
377
|
+
showSelection?: boolean
|
|
378
|
+
onSelectionChange?: (id: string, selected: boolean) => void
|
|
379
|
+
isSelected?: (row: TData) => boolean
|
|
380
|
+
|
|
381
|
+
onClick?: (row: TData) => void
|
|
382
|
+
|
|
383
|
+
className?: string
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Troubleshooting
|
|
388
|
+
|
|
389
|
+
### Cards not rendering
|
|
390
|
+
|
|
391
|
+
- Check that `data` array is not empty
|
|
392
|
+
- Verify `titleKey` matches a property in your data
|
|
393
|
+
- Check console for validation errors
|
|
394
|
+
|
|
395
|
+
### Actions not working
|
|
396
|
+
|
|
397
|
+
- Ensure `onClick` is defined for each action
|
|
398
|
+
- Check if action is `disabled` or `hidden` for the row
|
|
399
|
+
- Verify `e.stopPropagation()` is not interfering
|
|
400
|
+
|
|
401
|
+
### Selection not working
|
|
402
|
+
|
|
403
|
+
- Provide `getRowId` to generate stable IDs
|
|
404
|
+
- Implement `onSelectionChange` handler
|
|
405
|
+
- Verify `isSelected` returns correct value
|
|
406
|
+
|
|
407
|
+
### Responsive switching not working
|
|
408
|
+
|
|
409
|
+
- Check window width is below MOBILE_BREAKPOINT
|
|
410
|
+
- Ensure component is client-side (`'use client'`)
|
|
411
|
+
- Verify no CSS conflicts with breakpoint detection
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../../../lib/utils'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
import { Card } from './Card'
|
|
6
|
+
import { MobileViewProps, MOBILE_BREAKPOINT } from './types'
|
|
7
|
+
|
|
8
|
+
export function MobileView<TData = any>({
|
|
9
|
+
data,
|
|
10
|
+
config,
|
|
11
|
+
getRowId,
|
|
12
|
+
loading = false,
|
|
13
|
+
loadingRows,
|
|
14
|
+
emptyState,
|
|
15
|
+
className
|
|
16
|
+
}: MobileViewProps<TData>) {
|
|
17
|
+
const [isMobile, setIsMobile] = useState(false)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const checkMobile = () => {
|
|
21
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
checkMobile()
|
|
25
|
+
window.addEventListener('resize', checkMobile)
|
|
26
|
+
return () => window.removeEventListener('resize', checkMobile)
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
if (data.length === 0 && !loading) {
|
|
30
|
+
return (
|
|
31
|
+
<div className={cn('flex items-center justify-center py-12', className)}>
|
|
32
|
+
{emptyState || (
|
|
33
|
+
<div className="text-center">
|
|
34
|
+
<p className="text-muted-foreground text-sm">No data to display</p>
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={cn(
|
|
44
|
+
'space-y-4',
|
|
45
|
+
isMobile && 'space-y-3',
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{data.map((row, index) => {
|
|
50
|
+
const rowId = getRowId ? getRowId(row) : String(index)
|
|
51
|
+
const isRowLoading = loadingRows?.has(rowId) || false
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Card
|
|
55
|
+
key={rowId}
|
|
56
|
+
row={row}
|
|
57
|
+
config={config}
|
|
58
|
+
rowId={rowId}
|
|
59
|
+
isLoading={isRowLoading}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
})}
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Card } from './Card'
|
|
68
|
+
export { CardActions } from './CardActions'
|
|
69
|
+
export type {
|
|
70
|
+
MobileViewProps,
|
|
71
|
+
MobileCardConfig,
|
|
72
|
+
MobileCardField,
|
|
73
|
+
MobileCardAction,
|
|
74
|
+
CardProps,
|
|
75
|
+
CardActionsProps
|
|
76
|
+
} from './types'
|
|
77
|
+
export { MOBILE_BREAKPOINT } from './types'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface MobileCardField {
|
|
4
|
+
key: string
|
|
5
|
+
label?: string
|
|
6
|
+
render?: (value: any, row: any) => ReactNode
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MobileCardAction {
|
|
11
|
+
id: string
|
|
12
|
+
label: string
|
|
13
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
14
|
+
onClick: (row: any) => void | Promise<void>
|
|
15
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
|
|
16
|
+
disabled?: (row: any) => boolean
|
|
17
|
+
hidden?: (row: any) => boolean
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MobileCardConfig<TData = any> {
|
|
22
|
+
titleKey: string
|
|
23
|
+
titleRender?: (row: TData) => ReactNode
|
|
24
|
+
|
|
25
|
+
subtitleKey?: string
|
|
26
|
+
subtitleRender?: (row: TData) => ReactNode
|
|
27
|
+
|
|
28
|
+
imageKey?: string
|
|
29
|
+
imageRender?: (row: TData) => ReactNode
|
|
30
|
+
|
|
31
|
+
primaryFields: MobileCardField[]
|
|
32
|
+
secondaryFields?: MobileCardField[]
|
|
33
|
+
|
|
34
|
+
actions?: MobileCardAction[]
|
|
35
|
+
|
|
36
|
+
renderCustomContent?: (row: TData) => ReactNode
|
|
37
|
+
renderCustomHeader?: (row: TData) => ReactNode
|
|
38
|
+
renderCustomFooter?: (row: TData) => ReactNode
|
|
39
|
+
|
|
40
|
+
showSelection?: boolean
|
|
41
|
+
onSelectionChange?: (id: string, selected: boolean) => void
|
|
42
|
+
isSelected?: (row: TData) => boolean
|
|
43
|
+
|
|
44
|
+
onClick?: (row: TData) => void
|
|
45
|
+
|
|
46
|
+
className?: string
|
|
47
|
+
headerClassName?: string
|
|
48
|
+
contentClassName?: string
|
|
49
|
+
footerClassName?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface MobileViewProps<TData = any> {
|
|
53
|
+
data: TData[]
|
|
54
|
+
config: MobileCardConfig<TData>
|
|
55
|
+
getRowId?: (row: TData) => string
|
|
56
|
+
loading?: boolean
|
|
57
|
+
loadingRows?: Set<string>
|
|
58
|
+
emptyState?: ReactNode
|
|
59
|
+
className?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CardProps<TData = any> {
|
|
63
|
+
row: TData
|
|
64
|
+
config: MobileCardConfig<TData>
|
|
65
|
+
rowId?: string
|
|
66
|
+
isLoading?: boolean
|
|
67
|
+
className?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CardActionsProps {
|
|
71
|
+
actions: MobileCardAction[]
|
|
72
|
+
row: any
|
|
73
|
+
className?: string
|
|
74
|
+
maxVisibleActions?: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const MOBILE_BREAKPOINT = 768 // px
|