@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,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
+ }