@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,218 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Card as UICard, CardContent, CardHeader, CardFooter } from '../../../ui/card'
|
|
4
|
+
import { Checkbox } from '../../../ui/checkbox'
|
|
5
|
+
import { cn } from '../../../../lib/utils'
|
|
6
|
+
import { Loader2 } from 'lucide-react'
|
|
7
|
+
import { CardProps, MobileCardField } from './types'
|
|
8
|
+
import { CardActions } from './CardActions'
|
|
9
|
+
|
|
10
|
+
function getNestedValue(obj: any, path: string): any {
|
|
11
|
+
if (!path || !obj) return obj
|
|
12
|
+
|
|
13
|
+
const keys = path.split('.')
|
|
14
|
+
let value = obj
|
|
15
|
+
|
|
16
|
+
for (const key of keys) {
|
|
17
|
+
if (value === null || value === undefined) return undefined
|
|
18
|
+
value = value[key]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function renderField(field: MobileCardField, row: any) {
|
|
25
|
+
const value = getNestedValue(row, field.key)
|
|
26
|
+
|
|
27
|
+
if (field.render) {
|
|
28
|
+
return field.render(value, row)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (value === null || value === undefined) {
|
|
32
|
+
return <span className="text-muted-foreground">-</span>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof value === 'boolean') {
|
|
36
|
+
return <span>{value ? 'Yes' : 'No'}</span>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return <span>{String(value)}</span>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Card<TData = any>({
|
|
43
|
+
row,
|
|
44
|
+
config,
|
|
45
|
+
rowId,
|
|
46
|
+
isLoading = false,
|
|
47
|
+
className
|
|
48
|
+
}: CardProps<TData>) {
|
|
49
|
+
const {
|
|
50
|
+
titleKey,
|
|
51
|
+
titleRender,
|
|
52
|
+
subtitleKey,
|
|
53
|
+
subtitleRender,
|
|
54
|
+
imageKey,
|
|
55
|
+
imageRender,
|
|
56
|
+
primaryFields,
|
|
57
|
+
secondaryFields,
|
|
58
|
+
actions,
|
|
59
|
+
renderCustomContent,
|
|
60
|
+
renderCustomHeader,
|
|
61
|
+
renderCustomFooter,
|
|
62
|
+
showSelection,
|
|
63
|
+
onSelectionChange,
|
|
64
|
+
isSelected,
|
|
65
|
+
onClick,
|
|
66
|
+
headerClassName,
|
|
67
|
+
contentClassName,
|
|
68
|
+
footerClassName,
|
|
69
|
+
} = config
|
|
70
|
+
|
|
71
|
+
const handleClick = () => {
|
|
72
|
+
if (onClick && !isLoading) {
|
|
73
|
+
onClick(row)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const handleSelectionChange = (checked: boolean) => {
|
|
78
|
+
if (onSelectionChange && rowId) {
|
|
79
|
+
onSelectionChange(rowId, checked)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const selected = isSelected ? isSelected(row) : false
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<UICard
|
|
87
|
+
className={cn(
|
|
88
|
+
'relative transition-all duration-200',
|
|
89
|
+
onClick && !isLoading && 'cursor-pointer hover:shadow-md hover:border-primary/50',
|
|
90
|
+
selected && 'ring-2 ring-primary border-primary',
|
|
91
|
+
isLoading && 'opacity-60 pointer-events-none',
|
|
92
|
+
className,
|
|
93
|
+
config.className
|
|
94
|
+
)}
|
|
95
|
+
onClick={handleClick}
|
|
96
|
+
>
|
|
97
|
+
{isLoading && (
|
|
98
|
+
<div className="absolute inset-0 flex items-center justify-center bg-background/50 rounded-lg z-10">
|
|
99
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<CardHeader className={cn('pb-3', headerClassName)}>
|
|
104
|
+
{renderCustomHeader ? (
|
|
105
|
+
renderCustomHeader(row)
|
|
106
|
+
) : (
|
|
107
|
+
<div className="flex items-start gap-3">
|
|
108
|
+
{showSelection && (
|
|
109
|
+
<div
|
|
110
|
+
className="flex items-center pt-1"
|
|
111
|
+
onClick={(e) => e.stopPropagation()}
|
|
112
|
+
>
|
|
113
|
+
<Checkbox
|
|
114
|
+
checked={selected}
|
|
115
|
+
onCheckedChange={handleSelectionChange}
|
|
116
|
+
aria-label="Select row"
|
|
117
|
+
className="h-5 w-5"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{(imageKey || imageRender) && (
|
|
123
|
+
<div className="flex-shrink-0">
|
|
124
|
+
{imageRender ? imageRender(row) : (
|
|
125
|
+
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center">
|
|
126
|
+
<span className="text-lg font-semibold text-primary">
|
|
127
|
+
{String(getNestedValue(row, titleKey) || '?').charAt(0).toUpperCase()}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
<div className="flex-1 min-w-0">
|
|
135
|
+
<div className="flex items-start justify-between gap-2">
|
|
136
|
+
<div className="flex-1 min-w-0">
|
|
137
|
+
<h3 className="font-semibold text-base leading-tight truncate">
|
|
138
|
+
{titleRender ? titleRender(row) : getNestedValue(row, titleKey)}
|
|
139
|
+
</h3>
|
|
140
|
+
{subtitleKey && (
|
|
141
|
+
<p className="text-sm text-muted-foreground mt-1 truncate">
|
|
142
|
+
{subtitleRender ? subtitleRender(row) : getNestedValue(row, subtitleKey)}
|
|
143
|
+
</p>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{actions && actions.length > 0 && (
|
|
148
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
149
|
+
<CardActions
|
|
150
|
+
actions={actions}
|
|
151
|
+
row={row}
|
|
152
|
+
maxVisibleActions={2}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</CardHeader>
|
|
161
|
+
|
|
162
|
+
<CardContent className={cn('pt-0 space-y-4', contentClassName)}>
|
|
163
|
+
{renderCustomContent ? (
|
|
164
|
+
renderCustomContent(row)
|
|
165
|
+
) : (
|
|
166
|
+
<>
|
|
167
|
+
{primaryFields && primaryFields.length > 0 && (
|
|
168
|
+
<div className="space-y-3">
|
|
169
|
+
{primaryFields.map((field) => (
|
|
170
|
+
<div
|
|
171
|
+
key={field.key}
|
|
172
|
+
className={cn(
|
|
173
|
+
'flex justify-between items-center gap-2',
|
|
174
|
+
field.className
|
|
175
|
+
)}
|
|
176
|
+
>
|
|
177
|
+
<span className="text-sm font-medium text-muted-foreground flex-shrink-0">
|
|
178
|
+
{field.label || field.key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}:
|
|
179
|
+
</span>
|
|
180
|
+
<div className="text-sm font-medium text-right truncate">
|
|
181
|
+
{renderField(field, row)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{secondaryFields && secondaryFields.length > 0 && (
|
|
189
|
+
<div className="border-t pt-3">
|
|
190
|
+
<div className="grid grid-cols-2 gap-3">
|
|
191
|
+
{secondaryFields.map((field) => (
|
|
192
|
+
<div
|
|
193
|
+
key={field.key}
|
|
194
|
+
className={cn('space-y-1', field.className)}
|
|
195
|
+
>
|
|
196
|
+
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
197
|
+
{field.label || field.key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
|
|
198
|
+
</div>
|
|
199
|
+
<div className="text-sm font-semibold truncate">
|
|
200
|
+
{renderField(field, row)}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
209
|
+
</CardContent>
|
|
210
|
+
|
|
211
|
+
{renderCustomFooter && (
|
|
212
|
+
<CardFooter className={cn('pt-0', footerClassName)}>
|
|
213
|
+
{renderCustomFooter(row)}
|
|
214
|
+
</CardFooter>
|
|
215
|
+
)}
|
|
216
|
+
</UICard>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Button } from '../../../ui/button'
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from '../../../ui/dropdown-menu'
|
|
11
|
+
import { cn } from '../../../../lib/utils'
|
|
12
|
+
import { MoreVertical } from 'lucide-react'
|
|
13
|
+
import { useState } from 'react'
|
|
14
|
+
import { CardActionsProps } from './types'
|
|
15
|
+
|
|
16
|
+
export function CardActions({
|
|
17
|
+
actions,
|
|
18
|
+
row,
|
|
19
|
+
className,
|
|
20
|
+
maxVisibleActions = 2
|
|
21
|
+
}: CardActionsProps) {
|
|
22
|
+
const [isExecuting, setIsExecuting] = useState<string | null>(null)
|
|
23
|
+
|
|
24
|
+
const visibleActions = actions.filter((action) => {
|
|
25
|
+
return !(action.hidden && action.hidden(row))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (visibleActions.length === 0) {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleActionClick = async (action: any) => {
|
|
33
|
+
if (action.disabled && action.disabled(row)) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setIsExecuting(action.id)
|
|
38
|
+
try {
|
|
39
|
+
await action.onClick(row)
|
|
40
|
+
} finally {
|
|
41
|
+
setIsExecuting(null)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const primaryActions = visibleActions.slice(0, maxVisibleActions)
|
|
46
|
+
const overflowActions = visibleActions.slice(maxVisibleActions)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={cn('flex items-center gap-1', className)}>
|
|
50
|
+
{primaryActions.map((action) => {
|
|
51
|
+
const Icon = action.icon
|
|
52
|
+
const isDisabled = action.disabled ? action.disabled(row) : false
|
|
53
|
+
const isLoading = isExecuting === action.id
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Button
|
|
57
|
+
key={action.id}
|
|
58
|
+
variant={action.variant || 'ghost'}
|
|
59
|
+
size="sm"
|
|
60
|
+
className={cn(
|
|
61
|
+
'h-9 min-w-[44px]',
|
|
62
|
+
Icon && !action.label && 'w-9 p-0',
|
|
63
|
+
action.className
|
|
64
|
+
)}
|
|
65
|
+
onClick={(e) => {
|
|
66
|
+
e.stopPropagation()
|
|
67
|
+
handleActionClick(action)
|
|
68
|
+
}}
|
|
69
|
+
disabled={isDisabled || isLoading}
|
|
70
|
+
>
|
|
71
|
+
{Icon && (
|
|
72
|
+
<Icon className={cn(
|
|
73
|
+
'h-4 w-4',
|
|
74
|
+
action.label && 'mr-1.5'
|
|
75
|
+
)} />
|
|
76
|
+
)}
|
|
77
|
+
{action.label && (
|
|
78
|
+
<span className="text-xs font-medium">{action.label}</span>
|
|
79
|
+
)}
|
|
80
|
+
</Button>
|
|
81
|
+
)
|
|
82
|
+
})}
|
|
83
|
+
|
|
84
|
+
{overflowActions.length > 0 && (
|
|
85
|
+
<DropdownMenu>
|
|
86
|
+
<DropdownMenuTrigger asChild>
|
|
87
|
+
<Button
|
|
88
|
+
variant="ghost"
|
|
89
|
+
size="sm"
|
|
90
|
+
className="h-9 w-9 p-0"
|
|
91
|
+
onClick={(e) => e.stopPropagation()}
|
|
92
|
+
>
|
|
93
|
+
<MoreVertical className="h-4 w-4" />
|
|
94
|
+
<span className="sr-only">More actions</span>
|
|
95
|
+
</Button>
|
|
96
|
+
</DropdownMenuTrigger>
|
|
97
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
98
|
+
{overflowActions.map((action, index) => {
|
|
99
|
+
const Icon = action.icon
|
|
100
|
+
const isDisabled = action.disabled ? action.disabled(row) : false
|
|
101
|
+
const isLoading = isExecuting === action.id
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<DropdownMenuItem
|
|
105
|
+
key={action.id}
|
|
106
|
+
onClick={(e) => {
|
|
107
|
+
e.stopPropagation()
|
|
108
|
+
handleActionClick(action)
|
|
109
|
+
}}
|
|
110
|
+
disabled={isDisabled || isLoading}
|
|
111
|
+
className={cn(
|
|
112
|
+
action.variant === 'destructive' && 'text-destructive focus:text-destructive',
|
|
113
|
+
action.className
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
|
117
|
+
<span>{action.label || 'Action'}</span>
|
|
118
|
+
</DropdownMenuItem>
|
|
119
|
+
)
|
|
120
|
+
})}
|
|
121
|
+
</DropdownMenuContent>
|
|
122
|
+
</DropdownMenu>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|