@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.
Files changed (86) hide show
  1. package/README.md +537 -0
  2. package/package.json +80 -0
  3. package/src/components/index.ts +50 -0
  4. package/src/components/navigation/sidebar.tsx +178 -0
  5. package/src/components/ui/accordion.tsx +58 -0
  6. package/src/components/ui/alert.tsx +59 -0
  7. package/src/components/ui/badge.tsx +36 -0
  8. package/src/components/ui/button.tsx +57 -0
  9. package/src/components/ui/calendar.tsx +70 -0
  10. package/src/components/ui/card.tsx +68 -0
  11. package/src/components/ui/checkbox.tsx +30 -0
  12. package/src/components/ui/collapsible.tsx +12 -0
  13. package/src/components/ui/dialog.tsx +122 -0
  14. package/src/components/ui/dropdown-menu.tsx +200 -0
  15. package/src/components/ui/index.ts +24 -0
  16. package/src/components/ui/input.tsx +25 -0
  17. package/src/components/ui/label.tsx +26 -0
  18. package/src/components/ui/popover.tsx +31 -0
  19. package/src/components/ui/progress.tsx +28 -0
  20. package/src/components/ui/scroll-area.tsx +48 -0
  21. package/src/components/ui/select.tsx +160 -0
  22. package/src/components/ui/separator.tsx +31 -0
  23. package/src/components/ui/skeleton.tsx +15 -0
  24. package/src/components/ui/table.tsx +117 -0
  25. package/src/components/ui/tabs.tsx +55 -0
  26. package/src/components/ui/textarea.tsx +24 -0
  27. package/src/components/ui/tooltip.tsx +30 -0
  28. package/src/components/unified-table/UnifiedTable.tsx +553 -0
  29. package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
  30. package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
  31. package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
  32. package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
  33. package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
  34. package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
  35. package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
  36. package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
  37. package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
  38. package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
  39. package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
  40. package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
  41. package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
  42. package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
  43. package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
  44. package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
  45. package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
  46. package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
  47. package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
  48. package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
  49. package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
  50. package/src/components/unified-table/components/MobileView/README.md +411 -0
  51. package/src/components/unified-table/components/MobileView/index.tsx +77 -0
  52. package/src/components/unified-table/components/MobileView/types.ts +77 -0
  53. package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
  54. package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
  55. package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
  56. package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
  57. package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
  58. package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
  59. package/src/components/unified-table/hooks/index.ts +21 -0
  60. package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
  61. package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
  62. package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
  63. package/src/components/unified-table/hooks/useFilters.ts +53 -0
  64. package/src/components/unified-table/hooks/usePagination.ts +120 -0
  65. package/src/components/unified-table/hooks/useResponsive.ts +50 -0
  66. package/src/components/unified-table/hooks/useSelection.ts +152 -0
  67. package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
  68. package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
  69. package/src/components/unified-table/hooks/useTableState.ts +103 -0
  70. package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
  71. package/src/components/unified-table/hooks/useTableURL.ts +301 -0
  72. package/src/components/unified-table/index.ts +16 -0
  73. package/src/components/unified-table/types.ts +393 -0
  74. package/src/components/unified-table/utils/export.ts +236 -0
  75. package/src/components/unified-table/utils/index.ts +4 -0
  76. package/src/components/unified-table/utils/renderers.ts +105 -0
  77. package/src/components/unified-table/utils/themes.ts +87 -0
  78. package/src/components/unified-table/utils/validation.ts +122 -0
  79. package/src/index.ts +6 -0
  80. package/src/lib/utils.ts +1 -0
  81. package/src/theme/contract.ts +46 -0
  82. package/src/theme/index.ts +9 -0
  83. package/src/theme/tailwind.config.js +70 -0
  84. package/src/theme/tailwind.preset.ts +93 -0
  85. package/src/utils/cn.ts +6 -0
  86. 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