@carefully-built/cli 0.1.1 → 0.1.2

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 (213) hide show
  1. package/README.md +101 -80
  2. package/dist/index.mjs +8 -5
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +3 -3
  5. package/registry/ui/avatar/manifest.json +33 -0
  6. package/registry/ui/avatar/primitives/avatar.tsx +64 -0
  7. package/registry/ui/avatar/utils/cn.ts +6 -0
  8. package/registry/ui/button/manifest.json +24 -5
  9. package/registry/ui/button/utils/cn.ts +6 -0
  10. package/registry/ui/calendar/manifest.json +35 -0
  11. package/registry/ui/calendar/primitives/button.tsx +89 -0
  12. package/registry/ui/calendar/primitives/calendar.tsx +68 -0
  13. package/registry/ui/calendar/utils/cn.ts +6 -0
  14. package/registry/ui/card/manifest.json +36 -0
  15. package/registry/ui/card/primitives/card.tsx +80 -0
  16. package/registry/ui/card/utils/cn.ts +6 -0
  17. package/registry/ui/chip/manifest.json +36 -0
  18. package/registry/ui/chip/primitives/chip-utils.ts +10 -0
  19. package/registry/ui/chip/primitives/chip.tsx +74 -0
  20. package/registry/ui/chip/utils/cn.ts +6 -0
  21. package/registry/ui/chip-utils/manifest.json +33 -0
  22. package/registry/ui/chip-utils/primitives/chip-utils.ts +10 -0
  23. package/registry/ui/chip-utils/utils/cn.ts +6 -0
  24. package/registry/ui/date-display/manifest.json +33 -0
  25. package/registry/ui/date-display/utils/cn.ts +6 -0
  26. package/registry/ui/date-display/utils/date-display.ts +61 -0
  27. package/registry/ui/dialog/manifest.json +43 -0
  28. package/registry/ui/dialog/primitives/button.tsx +89 -0
  29. package/registry/ui/dialog/primitives/dialog.tsx +147 -0
  30. package/registry/ui/dialog/utils/cn.ts +6 -0
  31. package/registry/ui/display-date/manifest.json +36 -0
  32. package/registry/ui/display-date/primitives/display-date.tsx +20 -0
  33. package/registry/ui/display-date/utils/cn.ts +6 -0
  34. package/registry/ui/display-date/utils/date-display.ts +61 -0
  35. package/registry/ui/drawer/manifest.json +37 -0
  36. package/registry/ui/drawer/primitives/drawer.tsx +99 -0
  37. package/registry/ui/drawer/utils/cn.ts +6 -0
  38. package/registry/ui/dropdown-menu/manifest.json +37 -0
  39. package/registry/ui/dropdown-menu/primitives/dropdown-menu.tsx +140 -0
  40. package/registry/ui/dropdown-menu/utils/cn.ts +6 -0
  41. package/registry/ui/empty-state/empty-state/collection-empty-state.ts +29 -0
  42. package/registry/ui/empty-state/empty-state/empty-state-card.tsx +72 -0
  43. package/registry/ui/empty-state/empty-state/index.ts +8 -0
  44. package/registry/ui/empty-state/empty-state/initial-empty-state.tsx +36 -0
  45. package/registry/ui/empty-state/empty-state/no-results-state.tsx +20 -0
  46. package/registry/ui/empty-state/manifest.json +63 -0
  47. package/registry/ui/empty-state/primitives/button.tsx +89 -0
  48. package/registry/ui/empty-state/primitives/card.tsx +80 -0
  49. package/registry/ui/empty-state/utils/cn.ts +6 -0
  50. package/registry/ui/error-page/error-page/error-code.tsx +16 -0
  51. package/registry/ui/error-page/error-page/error-page-content.ts +75 -0
  52. package/registry/ui/error-page/error-page/index.ts +19 -0
  53. package/registry/ui/error-page/error-page/posthog-error-capture.ts +83 -0
  54. package/registry/ui/error-page/error-page/saas-error-page.tsx +146 -0
  55. package/registry/ui/error-page/manifest.json +64 -0
  56. package/registry/ui/error-page/primitives/button.tsx +89 -0
  57. package/registry/ui/error-page/utils/cn.ts +6 -0
  58. package/registry/ui/field-detail-row/manifest.json +32 -0
  59. package/registry/ui/field-detail-row/primitives/field-detail-row.tsx +28 -0
  60. package/registry/ui/field-detail-row/utils/cn.ts +6 -0
  61. package/registry/ui/file-dropzone/manifest.json +35 -0
  62. package/registry/ui/file-dropzone/primitives/button.tsx +89 -0
  63. package/registry/ui/file-dropzone/primitives/file-dropzone.tsx +236 -0
  64. package/registry/ui/file-dropzone/utils/cn.ts +6 -0
  65. package/registry/ui/help-info-button/manifest.json +72 -0
  66. package/registry/ui/help-info-button/overlays/responsive-sheet.footer.tsx +88 -0
  67. package/registry/ui/help-info-button/overlays/responsive-sheet.layouts.tsx +207 -0
  68. package/registry/ui/help-info-button/overlays/responsive-sheet.shortcuts.ts +103 -0
  69. package/registry/ui/help-info-button/overlays/responsive-sheet.tsx +132 -0
  70. package/registry/ui/help-info-button/primitives/button.tsx +89 -0
  71. package/registry/ui/help-info-button/primitives/drawer.tsx +99 -0
  72. package/registry/ui/help-info-button/primitives/help-info-button.tsx +63 -0
  73. package/registry/ui/help-info-button/primitives/keyboard-shortcut-hint.tsx +40 -0
  74. package/registry/ui/help-info-button/primitives/sheet.tsx +103 -0
  75. package/registry/ui/help-info-button/primitives/tooltip.tsx +57 -0
  76. package/registry/ui/help-info-button/utils/cn.ts +6 -0
  77. package/registry/ui/help-info-button/utils/use-media-query.ts +28 -0
  78. package/registry/ui/input/manifest.json +31 -0
  79. package/registry/ui/input/primitives/input.tsx +19 -0
  80. package/registry/ui/input/utils/cn.ts +6 -0
  81. package/registry/ui/keyboard-shortcut-hint/manifest.json +32 -0
  82. package/registry/ui/keyboard-shortcut-hint/primitives/keyboard-shortcut-hint.tsx +40 -0
  83. package/registry/ui/keyboard-shortcut-hint/utils/cn.ts +6 -0
  84. package/registry/ui/label/manifest.json +31 -0
  85. package/registry/ui/label/primitives/label.tsx +21 -0
  86. package/registry/ui/label/utils/cn.ts +6 -0
  87. package/registry/ui/pagination/manifest.json +36 -0
  88. package/registry/ui/pagination/primitives/button.tsx +89 -0
  89. package/registry/ui/pagination/primitives/pagination.tsx +143 -0
  90. package/registry/ui/pagination/utils/cn.ts +6 -0
  91. package/registry/ui/popover/manifest.json +33 -0
  92. package/registry/ui/popover/primitives/popover.tsx +46 -0
  93. package/registry/ui/popover/utils/cn.ts +6 -0
  94. package/registry/ui/responsive-sheet/manifest.json +66 -0
  95. package/registry/ui/responsive-sheet/overlays/responsive-sheet.footer.tsx +88 -0
  96. package/registry/ui/responsive-sheet/overlays/responsive-sheet.layouts.tsx +207 -0
  97. package/registry/ui/responsive-sheet/overlays/responsive-sheet.shortcuts.ts +103 -0
  98. package/registry/ui/responsive-sheet/overlays/responsive-sheet.tsx +132 -0
  99. package/registry/ui/responsive-sheet/primitives/button.tsx +89 -0
  100. package/registry/ui/responsive-sheet/primitives/drawer.tsx +99 -0
  101. package/registry/ui/responsive-sheet/primitives/keyboard-shortcut-hint.tsx +40 -0
  102. package/registry/ui/responsive-sheet/primitives/sheet.tsx +103 -0
  103. package/registry/ui/responsive-sheet/utils/cn.ts +6 -0
  104. package/registry/ui/responsive-sheet/utils/use-media-query.ts +28 -0
  105. package/registry/ui/responsive-sheet.footer/manifest.json +40 -0
  106. package/registry/ui/responsive-sheet.footer/overlays/responsive-sheet.footer.tsx +88 -0
  107. package/registry/ui/responsive-sheet.footer/primitives/button.tsx +89 -0
  108. package/registry/ui/responsive-sheet.footer/primitives/keyboard-shortcut-hint.tsx +40 -0
  109. package/registry/ui/responsive-sheet.footer/utils/cn.ts +6 -0
  110. package/registry/ui/responsive-sheet.shortcuts/manifest.json +34 -0
  111. package/registry/ui/responsive-sheet.shortcuts/overlays/responsive-sheet.shortcuts.ts +103 -0
  112. package/registry/ui/responsive-sheet.shortcuts/utils/cn.ts +6 -0
  113. package/registry/ui/scroll-fade-area/manifest.json +31 -0
  114. package/registry/ui/scroll-fade-area/primitives/scroll-fade-area.tsx +295 -0
  115. package/registry/ui/scroll-fade-area/utils/cn.ts +6 -0
  116. package/registry/ui/search/manifest.json +35 -0
  117. package/registry/ui/search/utils/cn.ts +6 -0
  118. package/registry/ui/search/utils/search.ts +227 -0
  119. package/registry/ui/searchable-select/manifest.json +48 -0
  120. package/registry/ui/searchable-select/primitives/input.tsx +19 -0
  121. package/registry/ui/searchable-select/search/searchable-select-position.ts +95 -0
  122. package/registry/ui/searchable-select/search/searchable-select.tsx +431 -0
  123. package/registry/ui/searchable-select/utils/cn.ts +6 -0
  124. package/registry/ui/searchable-select/utils/search.ts +227 -0
  125. package/registry/ui/searchable-select-position/manifest.json +32 -0
  126. package/registry/ui/searchable-select-position/search/searchable-select-position.ts +95 -0
  127. package/registry/ui/searchable-select-position/utils/cn.ts +6 -0
  128. package/registry/ui/segmented-toggle/manifest.json +41 -0
  129. package/registry/ui/segmented-toggle/primitives/scroll-fade-area.tsx +295 -0
  130. package/registry/ui/segmented-toggle/primitives/segmented-toggle.tsx +106 -0
  131. package/registry/ui/segmented-toggle/primitives/tabs.tsx +97 -0
  132. package/registry/ui/segmented-toggle/utils/cn.ts +6 -0
  133. package/registry/ui/select/manifest.json +37 -0
  134. package/registry/ui/select/primitives/select.tsx +142 -0
  135. package/registry/ui/select/utils/cn.ts +6 -0
  136. package/registry/ui/sheet/manifest.json +39 -0
  137. package/registry/ui/sheet/primitives/button.tsx +89 -0
  138. package/registry/ui/sheet/primitives/sheet.tsx +103 -0
  139. package/registry/ui/sheet/utils/cn.ts +6 -0
  140. package/registry/ui/skeleton/manifest.json +31 -0
  141. package/registry/ui/skeleton/primitives/skeleton.tsx +13 -0
  142. package/registry/ui/skeleton/utils/cn.ts +6 -0
  143. package/registry/ui/smart-table/manifest.json +115 -0
  144. package/registry/ui/smart-table/primitives/button.tsx +89 -0
  145. package/registry/ui/smart-table/primitives/card.tsx +80 -0
  146. package/registry/ui/smart-table/primitives/display-date.tsx +20 -0
  147. package/registry/ui/smart-table/primitives/pagination.tsx +143 -0
  148. package/registry/ui/smart-table/primitives/skeleton.tsx +13 -0
  149. package/registry/ui/smart-table/primitives/table.tsx +92 -0
  150. package/registry/ui/smart-table/primitives/tooltip.tsx +57 -0
  151. package/registry/ui/smart-table/smart-table/DesktopView.tsx +343 -0
  152. package/registry/ui/smart-table/smart-table/MobileView.tsx +170 -0
  153. package/registry/ui/smart-table/smart-table/SmartTable.tsx +85 -0
  154. package/registry/ui/smart-table/smart-table/SmartTableActions.tsx +71 -0
  155. package/registry/ui/smart-table/smart-table/TruncatedContent.tsx +147 -0
  156. package/registry/ui/smart-table/smart-table/index.ts +15 -0
  157. package/registry/ui/smart-table/smart-table/sorting.ts +148 -0
  158. package/registry/ui/smart-table/smart-table/truncated-content.utils.ts +22 -0
  159. package/registry/ui/smart-table/smart-table/types.ts +95 -0
  160. package/registry/ui/smart-table/smart-table/utils.ts +150 -0
  161. package/registry/ui/smart-table/utils/cn.ts +6 -0
  162. package/registry/ui/smart-table/utils/date-display.ts +61 -0
  163. package/registry/ui/smart-table/utils/use-media-query.ts +28 -0
  164. package/registry/ui/switch/manifest.json +31 -0
  165. package/registry/ui/switch/primitives/switch.tsx +31 -0
  166. package/registry/ui/switch/utils/cn.ts +6 -0
  167. package/registry/ui/table/manifest.json +38 -0
  168. package/registry/ui/table/primitives/table.tsx +92 -0
  169. package/registry/ui/table/utils/cn.ts +6 -0
  170. package/registry/ui/table-toolbar/manifest.json +93 -0
  171. package/registry/ui/table-toolbar/overlays/responsive-sheet.footer.tsx +88 -0
  172. package/registry/ui/table-toolbar/overlays/responsive-sheet.layouts.tsx +207 -0
  173. package/registry/ui/table-toolbar/overlays/responsive-sheet.shortcuts.ts +103 -0
  174. package/registry/ui/table-toolbar/overlays/responsive-sheet.tsx +132 -0
  175. package/registry/ui/table-toolbar/primitives/button.tsx +89 -0
  176. package/registry/ui/table-toolbar/primitives/drawer.tsx +99 -0
  177. package/registry/ui/table-toolbar/primitives/input.tsx +19 -0
  178. package/registry/ui/table-toolbar/primitives/keyboard-shortcut-hint.tsx +40 -0
  179. package/registry/ui/table-toolbar/primitives/sheet.tsx +103 -0
  180. package/registry/ui/table-toolbar/search/searchable-select-position.ts +95 -0
  181. package/registry/ui/table-toolbar/search/searchable-select.tsx +431 -0
  182. package/registry/ui/table-toolbar/table-toolbar/index.ts +9 -0
  183. package/registry/ui/table-toolbar/table-toolbar/table-toolbar.tsx +552 -0
  184. package/registry/ui/table-toolbar/utils/cn.ts +6 -0
  185. package/registry/ui/table-toolbar/utils/search.ts +227 -0
  186. package/registry/ui/table-toolbar/utils/use-media-query.ts +28 -0
  187. package/registry/ui/tabs/manifest.json +40 -0
  188. package/registry/ui/tabs/primitives/scroll-fade-area.tsx +295 -0
  189. package/registry/ui/tabs/primitives/tabs.tsx +97 -0
  190. package/registry/ui/tabs/utils/cn.ts +6 -0
  191. package/registry/ui/textarea/manifest.json +31 -0
  192. package/registry/ui/textarea/primitives/textarea.tsx +18 -0
  193. package/registry/ui/textarea/utils/cn.ts +6 -0
  194. package/registry/ui/tooltip/manifest.json +34 -0
  195. package/registry/ui/tooltip/primitives/tooltip.tsx +57 -0
  196. package/registry/ui/tooltip/utils/cn.ts +6 -0
  197. package/registry/ui/use-media-query/manifest.json +32 -0
  198. package/registry/ui/use-media-query/utils/cn.ts +6 -0
  199. package/registry/ui/use-media-query/utils/use-media-query.ts +28 -0
  200. package/registry/ui/user-picker/manifest.json +52 -0
  201. package/registry/ui/user-picker/primitives/avatar.tsx +64 -0
  202. package/registry/ui/user-picker/primitives/button.tsx +89 -0
  203. package/registry/ui/user-picker/primitives/input.tsx +19 -0
  204. package/registry/ui/user-picker/primitives/popover.tsx +46 -0
  205. package/registry/ui/user-picker/primitives/user-picker-utils.ts +113 -0
  206. package/registry/ui/user-picker/primitives/user-picker.tsx +226 -0
  207. package/registry/ui/user-picker/utils/cn.ts +6 -0
  208. package/registry/ui/user-picker-utils/manifest.json +38 -0
  209. package/registry/ui/user-picker-utils/primitives/user-picker-utils.ts +113 -0
  210. package/registry/ui/user-picker-utils/utils/cn.ts +6 -0
  211. package/assets/hero.png +0 -0
  212. package/registry/ui/button/cn.ts +0 -6
  213. /package/registry/ui/button/{button.tsx → primitives/button.tsx} +0 -0
@@ -0,0 +1,343 @@
1
+ 'use client';
2
+
3
+ import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react';
4
+
5
+ import { SmartTableActions } from '@/components/ui/smart-table/SmartTableActions';
6
+ import { getColumnSortKey, getNextSortState } from '@/components/ui/smart-table/sorting';
7
+ import { TruncatedContent } from '@/components/ui/smart-table/TruncatedContent';
8
+ import { getColumnTooltipText, renderColumnValue } from '@/components/ui/smart-table/utils';
9
+
10
+ import type {
11
+ ActionHandlers,
12
+ ActionType,
13
+ Column,
14
+ PaginationConfig,
15
+ SortDirection,
16
+ SortState,
17
+ } from '@/components/ui/smart-table/types';
18
+ import type { ReactNode } from 'react';
19
+
20
+ import { Pagination } from '@/components/ui/pagination';
21
+ import { Skeleton } from '@/components/ui/skeleton';
22
+ import {
23
+ Table,
24
+ TableBody,
25
+ TableCell,
26
+ TableHead,
27
+ TableHeader,
28
+ TableRow,
29
+ } from '@/components/ui/table';
30
+ import { cn } from '@/lib/utils';
31
+
32
+ interface DesktopViewProps<T> {
33
+ data: T[];
34
+ columns: Column<T>[];
35
+ isLoading: boolean;
36
+ skeletonRows: number;
37
+ actions?: ActionType[];
38
+ actionHandlers?: ActionHandlers<T>;
39
+ renderActions?: (item: T) => ReactNode;
40
+ noDataMessage: string;
41
+ noDataContent?: ReactNode;
42
+ getRowKey: (item: T) => string | number;
43
+ onRowClick?: (item: T) => void;
44
+ pagination?: PaginationConfig;
45
+ stickyHeader?: boolean;
46
+ maxHeight?: string;
47
+ fullHeight?: boolean;
48
+ sortState?: SortState;
49
+ onSortChange?: (state: SortState) => void;
50
+ }
51
+
52
+ const ACTION_COLUMN_WIDTH_PX = 112;
53
+ const ACTION_CELL_CLASS_NAME = 'w-28 min-w-28 overflow-visible whitespace-nowrap text-right';
54
+
55
+ function SortIcon({
56
+ activeDirection,
57
+ }: {
58
+ readonly activeDirection?: SortDirection;
59
+ }): React.ReactElement {
60
+ if (activeDirection === 'asc') {
61
+ return <ArrowUp className="size-3.5" aria-hidden="true" />;
62
+ }
63
+
64
+ if (activeDirection === 'desc') {
65
+ return <ArrowDown className="size-3.5" aria-hidden="true" />;
66
+ }
67
+
68
+ return <ChevronsUpDown className="size-3.5 opacity-45" aria-hidden="true" />;
69
+ }
70
+
71
+ function SortableHeaderContent<T>({
72
+ column,
73
+ sortState,
74
+ onSortChange,
75
+ }: {
76
+ readonly column: Column<T>;
77
+ readonly sortState?: SortState;
78
+ readonly onSortChange?: (state: SortState) => void;
79
+ }): React.ReactElement {
80
+ const sortKey = getColumnSortKey(column);
81
+ const activeDirection = sortState?.key === sortKey ? sortState.direction : undefined;
82
+
83
+ if (!sortKey || !onSortChange) {
84
+ return <>{column.header}</>;
85
+ }
86
+
87
+ return (
88
+ <button
89
+ type="button"
90
+ className={cn(
91
+ 'hover:text-foreground focus-visible:ring-ring inline-flex max-w-full items-center gap-1.5 rounded-sm text-left font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
92
+ column.align === 'right' && 'ml-auto',
93
+ column.align === 'center' && 'mx-auto',
94
+ )}
95
+ aria-label={`Sort by ${column.header}`}
96
+ onClick={() => {
97
+ onSortChange(getNextSortState(sortState ?? null, sortKey));
98
+ }}
99
+ >
100
+ <span className="truncate">{column.header}</span>
101
+ <SortIcon activeDirection={activeDirection} />
102
+ </button>
103
+ );
104
+ }
105
+
106
+ export function DesktopView<T>({
107
+ data,
108
+ columns,
109
+ isLoading,
110
+ skeletonRows,
111
+ actions,
112
+ actionHandlers,
113
+ renderActions,
114
+ noDataMessage,
115
+ noDataContent,
116
+ getRowKey,
117
+ onRowClick,
118
+ pagination,
119
+ stickyHeader = false,
120
+ maxHeight = 'calc(100vh - 300px)',
121
+ fullHeight = false,
122
+ sortState,
123
+ onSortChange,
124
+ }: DesktopViewProps<T>): React.ReactElement {
125
+ const resolvedNoDataContent = noDataContent ?? noDataMessage;
126
+ const hasActions = (actions?.length ?? 0) > 0 || renderActions !== undefined;
127
+ const actionColumnWidth = hasActions ? `${String(ACTION_COLUMN_WIDTH_PX)}px` : undefined;
128
+ const specifiedPercentageWidth = columns.reduce((total, column) => {
129
+ if (typeof column.width === 'string' && column.width.endsWith('%')) {
130
+ const parsedWidth = Number.parseFloat(column.width);
131
+ return Number.isNaN(parsedWidth) ? total : total + parsedWidth;
132
+ }
133
+
134
+ return total;
135
+ }, 0);
136
+ const actionWidthPercentage = 0;
137
+ const remainingPercentageForDataColumns = Math.max(
138
+ 0,
139
+ 100 - specifiedPercentageWidth - actionWidthPercentage,
140
+ );
141
+ const columnsWithoutWidth = columns.filter((column) => column.width === undefined).length;
142
+ const defaultPercentageWidth =
143
+ columnsWithoutWidth > 0 && remainingPercentageForDataColumns > 0
144
+ ? `${String(remainingPercentageForDataColumns / columnsWithoutWidth)}%`
145
+ : undefined;
146
+
147
+ const columnGroup = (
148
+ <colgroup>
149
+ {columns.map((column) => (
150
+ <col key={column.header} style={{ width: column.width ?? defaultPercentageWidth }} />
151
+ ))}
152
+ {hasActions && actionColumnWidth ? (
153
+ <col style={{ width: actionColumnWidth, minWidth: actionColumnWidth }} />
154
+ ) : null}
155
+ </colgroup>
156
+ );
157
+
158
+ const tableHeader = (
159
+ <TableHeader className={cn(stickyHeader && 'bg-muted/50 sticky top-0 z-10 backdrop-blur-sm')}>
160
+ <TableRow>
161
+ {columns.map((col) => (
162
+ <TableHead
163
+ key={col.header}
164
+ style={{ width: col.width }}
165
+ aria-sort={
166
+ sortState?.key === getColumnSortKey(col)
167
+ ? sortState.direction === 'asc'
168
+ ? 'ascending'
169
+ : 'descending'
170
+ : undefined
171
+ }
172
+ className={
173
+ col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : ''
174
+ }
175
+ >
176
+ <SortableHeaderContent column={col} sortState={sortState} onSortChange={onSortChange} />
177
+ </TableHead>
178
+ ))}
179
+ {hasActions ? (
180
+ <TableHead
181
+ style={
182
+ actionColumnWidth
183
+ ? { width: actionColumnWidth, minWidth: actionColumnWidth }
184
+ : undefined
185
+ }
186
+ className="w-28 min-w-28 overflow-visible text-right whitespace-nowrap"
187
+ >
188
+ Actions
189
+ </TableHead>
190
+ ) : null}
191
+ </TableRow>
192
+ </TableHeader>
193
+ );
194
+
195
+ // Determine scrollable container styles
196
+ const scrollContainerClass = cn(
197
+ 'min-w-0 rounded-lg border overflow-x-auto',
198
+ fullHeight && 'flex-1 min-h-0 overflow-y-auto',
199
+ !fullHeight && stickyHeader && 'overflow-y-auto',
200
+ );
201
+ const scrollContainerStyle = !fullHeight && stickyHeader ? { maxHeight } : undefined;
202
+
203
+ if (isLoading) {
204
+ return (
205
+ <div className={cn('flex flex-col', fullHeight && 'min-h-0 flex-1')}>
206
+ <div className={scrollContainerClass} style={scrollContainerStyle}>
207
+ <Table className="table-fixed">
208
+ {columnGroup}
209
+ {tableHeader}
210
+ <TableBody>
211
+ {Array.from({ length: skeletonRows }).map((_, i) => (
212
+ <TableRow key={i}>
213
+ {columns.map((col) => (
214
+ <TableCell key={col.header}>
215
+ <Skeleton className="h-5 w-full" />
216
+ </TableCell>
217
+ ))}
218
+ {hasActions ? (
219
+ <TableCell className={ACTION_CELL_CLASS_NAME}>
220
+ <div className="flex w-full justify-end">
221
+ <Skeleton className="h-8 w-20" />
222
+ </div>
223
+ </TableCell>
224
+ ) : null}
225
+ </TableRow>
226
+ ))}
227
+ </TableBody>
228
+ </Table>
229
+ </div>
230
+ {pagination && (
231
+ <Pagination
232
+ currentPage={pagination.currentPage}
233
+ totalPages={pagination.totalPages}
234
+ totalItems={pagination.totalItems}
235
+ pageSize={pagination.pageSize}
236
+ startIndex={pagination.startIndex}
237
+ endIndex={pagination.endIndex}
238
+ onPageChange={pagination.onPageChange}
239
+ />
240
+ )}
241
+ </div>
242
+ );
243
+ }
244
+
245
+ if (data.length === 0) {
246
+ return (
247
+ <div className={cn('flex flex-col', fullHeight && 'min-h-0 flex-1')}>
248
+ <div className="w-full py-0">
249
+ {typeof resolvedNoDataContent === 'string' ? (
250
+ <div className="text-muted-foreground">{resolvedNoDataContent}</div>
251
+ ) : (
252
+ resolvedNoDataContent
253
+ )}
254
+ </div>
255
+ {pagination && pagination.totalItems > 0 && (
256
+ <Pagination
257
+ currentPage={pagination.currentPage}
258
+ totalPages={pagination.totalPages}
259
+ totalItems={pagination.totalItems}
260
+ pageSize={pagination.pageSize}
261
+ startIndex={pagination.startIndex}
262
+ endIndex={pagination.endIndex}
263
+ onPageChange={pagination.onPageChange}
264
+ />
265
+ )}
266
+ </div>
267
+ );
268
+ }
269
+
270
+ return (
271
+ <div className={cn('flex flex-col', fullHeight && 'min-h-0 flex-1')}>
272
+ <div className={scrollContainerClass} style={scrollContainerStyle}>
273
+ <Table className="table-fixed">
274
+ {columnGroup}
275
+ {tableHeader}
276
+ <TableBody>
277
+ {data.map((item) => (
278
+ <TableRow
279
+ key={getRowKey(item)}
280
+ className={onRowClick ? 'hover:bg-muted/50 cursor-pointer' : ''}
281
+ onClick={() => onRowClick?.(item)}
282
+ >
283
+ {columns.map((col) => (
284
+ <TableCell
285
+ key={col.header}
286
+ className={cn(
287
+ 'max-w-0',
288
+ col.align === 'right'
289
+ ? 'text-right'
290
+ : col.align === 'center'
291
+ ? 'text-center'
292
+ : '',
293
+ )}
294
+ >
295
+ {col.truncate === false ? (
296
+ <div
297
+ className={cn(
298
+ 'block w-full min-w-0',
299
+ col.align === 'right'
300
+ ? 'text-right'
301
+ : col.align === 'center'
302
+ ? 'text-center'
303
+ : 'text-left',
304
+ )}
305
+ >
306
+ {renderColumnValue(col, item)}
307
+ </div>
308
+ ) : (
309
+ <TruncatedContent align={col.align} tooltip={getColumnTooltipText(col, item)}>
310
+ {renderColumnValue(col, item)}
311
+ </TruncatedContent>
312
+ )}
313
+ </TableCell>
314
+ ))}
315
+ {hasActions ? (
316
+ <TableCell className={ACTION_CELL_CLASS_NAME}>
317
+ <SmartTableActions
318
+ item={item}
319
+ actions={actions}
320
+ actionHandlers={actionHandlers}
321
+ renderActions={renderActions}
322
+ />
323
+ </TableCell>
324
+ ) : null}
325
+ </TableRow>
326
+ ))}
327
+ </TableBody>
328
+ </Table>
329
+ </div>
330
+ {pagination && (
331
+ <Pagination
332
+ currentPage={pagination.currentPage}
333
+ totalPages={pagination.totalPages}
334
+ totalItems={pagination.totalItems}
335
+ pageSize={pagination.pageSize}
336
+ startIndex={pagination.startIndex}
337
+ endIndex={pagination.endIndex}
338
+ onPageChange={pagination.onPageChange}
339
+ />
340
+ )}
341
+ </div>
342
+ );
343
+ }
@@ -0,0 +1,170 @@
1
+ 'use client';
2
+
3
+ import { SmartTableActions } from '@/components/ui/smart-table/SmartTableActions';
4
+ import { TruncatedContent } from '@/components/ui/smart-table/TruncatedContent';
5
+ import { getColumnTooltipText, renderColumnValue } from '@/components/ui/smart-table/utils';
6
+
7
+ import type { ActionHandlers, ActionType, Column, PaginationConfig } from '@/components/ui/smart-table/types';
8
+ import type { ReactNode } from 'react';
9
+
10
+ import { Pagination } from '@/components/ui/pagination';
11
+ import { Card, CardContent } from '@/components/ui/card';
12
+ import { Skeleton } from '@/components/ui/skeleton';
13
+ import { cn } from '@/lib/utils';
14
+
15
+ interface MobileViewProps<T> {
16
+ data: T[];
17
+ columns: Column<T>[];
18
+ isLoading: boolean;
19
+ skeletonRows: number;
20
+ actions?: ActionType[];
21
+ actionHandlers?: ActionHandlers<T>;
22
+ renderActions?: (item: T) => ReactNode;
23
+ noDataMessage: string;
24
+ noDataContent?: ReactNode;
25
+ getRowKey: (item: T) => string | number;
26
+ onRowClick?: (item: T) => void;
27
+ renderMobileCard?: (item: T) => ReactNode;
28
+ pagination?: PaginationConfig;
29
+ fullHeight?: boolean;
30
+ }
31
+
32
+ export function MobileView<T>({
33
+ data,
34
+ columns,
35
+ isLoading,
36
+ skeletonRows,
37
+ actions,
38
+ actionHandlers,
39
+ renderActions,
40
+ noDataMessage,
41
+ noDataContent,
42
+ getRowKey,
43
+ onRowClick,
44
+ renderMobileCard,
45
+ pagination,
46
+ fullHeight = false,
47
+ }: MobileViewProps<T>): React.ReactElement {
48
+ const resolvedNoDataContent = noDataContent ?? noDataMessage;
49
+ const visibleColumns = columns.filter((col) => !col.hideOnMobile);
50
+ const hasActions = (actions?.length ?? 0) > 0 || renderActions !== undefined;
51
+
52
+ const paginationComponent = pagination && (
53
+ <Pagination
54
+ currentPage={pagination.currentPage}
55
+ totalPages={pagination.totalPages}
56
+ totalItems={pagination.totalItems}
57
+ pageSize={pagination.pageSize}
58
+ startIndex={pagination.startIndex}
59
+ endIndex={pagination.endIndex}
60
+ onPageChange={pagination.onPageChange}
61
+ />
62
+ );
63
+
64
+ if (isLoading) {
65
+ return (
66
+ <div className={cn('flex flex-col', fullHeight && 'flex-1 min-h-0')}>
67
+ <div className={cn('space-y-2', fullHeight && 'flex-1 min-h-0 overflow-auto px-px')}>
68
+ {Array.from({ length: skeletonRows }).map((_, i) => (
69
+ <Card key={i} size="sm" className="border border-border/80 shadow-none ring-0">
70
+ <CardContent className="px-3">
71
+ <div className="space-y-2">
72
+ {visibleColumns.map((column) => (
73
+ <div key={column.header} className="flex items-center gap-3">
74
+ <Skeleton className="h-3.5 w-20 shrink-0" />
75
+ <Skeleton className="h-4 w-full" />
76
+ </div>
77
+ ))}
78
+ {hasActions ? (
79
+ <div className="flex justify-end border-t pt-3">
80
+ <Skeleton className="h-8 w-24" />
81
+ </div>
82
+ ) : null}
83
+ </div>
84
+ </CardContent>
85
+ </Card>
86
+ ))}
87
+ </div>
88
+ {paginationComponent}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ if (data.length === 0) {
94
+ return (
95
+ <div className={cn('flex flex-col', fullHeight && 'flex-1 min-h-0')}>
96
+ <div className="w-full py-0">
97
+ {typeof resolvedNoDataContent === 'string' ? (
98
+ <div className="text-muted-foreground">{resolvedNoDataContent}</div>
99
+ ) : (
100
+ resolvedNoDataContent
101
+ )}
102
+ </div>
103
+ {pagination && pagination.totalItems > 0 && paginationComponent}
104
+ </div>
105
+ );
106
+ }
107
+
108
+ return (
109
+ <div className={cn('flex flex-col', fullHeight && 'flex-1 min-h-0')}>
110
+ <div className={cn('space-y-2', fullHeight && 'flex-1 min-h-0 overflow-auto px-px')}>
111
+ {data.map((item) => (
112
+ <Card
113
+ key={getRowKey(item)}
114
+ size="sm"
115
+ className={cn(
116
+ 'border border-border/80 shadow-none ring-0',
117
+ onRowClick ? 'cursor-pointer hover:bg-muted/50' : ''
118
+ )}
119
+ onClick={() => onRowClick?.(item)}
120
+ >
121
+ <CardContent className="px-3">
122
+ {renderMobileCard ? (
123
+ renderMobileCard(item)
124
+ ) : (
125
+ <div className="space-y-2.5">
126
+ {visibleColumns.map((col) => (
127
+ <div
128
+ key={col.header}
129
+ className="flex items-center gap-3 overflow-hidden"
130
+ >
131
+ <span className="w-20 shrink-0 truncate text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
132
+ {col.mobileLabel ?? col.header}
133
+ </span>
134
+ <div className="min-w-0 flex-1 text-sm">
135
+ {col.truncate === false ? (
136
+ <div className="block min-w-0 w-full text-right text-sm">
137
+ {renderColumnValue(col, item)}
138
+ </div>
139
+ ) : (
140
+ <TruncatedContent
141
+ align="right"
142
+ tooltip={getColumnTooltipText(col, item)}
143
+ className="text-sm"
144
+ >
145
+ {renderColumnValue(col, item)}
146
+ </TruncatedContent>
147
+ )}
148
+ </div>
149
+ </div>
150
+ ))}
151
+ {hasActions ? (
152
+ <div className="mt-3 flex justify-end border-t pt-2.5">
153
+ <SmartTableActions
154
+ item={item}
155
+ actions={actions}
156
+ actionHandlers={actionHandlers}
157
+ renderActions={renderActions}
158
+ />
159
+ </div>
160
+ ) : null}
161
+ </div>
162
+ )}
163
+ </CardContent>
164
+ </Card>
165
+ ))}
166
+ </div>
167
+ {paginationComponent}
168
+ </div>
169
+ );
170
+ }
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ import { DesktopView } from '@/components/ui/smart-table/DesktopView';
4
+ import { MobileView } from '@/components/ui/smart-table/MobileView';
5
+
6
+ import type { SmartTableProps } from '@/components/ui/smart-table/types';
7
+
8
+ import { cn } from '@/lib/utils';
9
+ import { useIsMobile } from '@/components/ui/use-media-query';
10
+
11
+ export function SmartTable<T>({
12
+ data,
13
+ columns,
14
+ isLoading,
15
+ skeletonRows = 5,
16
+ actions,
17
+ actionHandlers,
18
+ renderActions,
19
+ noDataMessage = 'No data available',
20
+ noDataContent,
21
+ getRowKey = (item) => {
22
+ // Default: try _id, id, or index
23
+ const record = item as Record<string, unknown>;
24
+ if ('_id' in record) return String(record._id);
25
+ if ('id' in record) return String(record.id);
26
+ return data.indexOf(item);
27
+ },
28
+ onRowClick,
29
+ renderMobileCard,
30
+ pagination,
31
+ stickyHeader,
32
+ maxHeight,
33
+ fullHeight,
34
+ sortState,
35
+ onSortChange,
36
+ }: SmartTableProps<T>): React.ReactElement {
37
+ const isMobile = useIsMobile();
38
+
39
+ if (isMobile) {
40
+ return (
41
+ <div className={cn('min-w-0', fullHeight && 'flex min-h-0 flex-1 flex-col')}>
42
+ <MobileView
43
+ data={data}
44
+ columns={columns}
45
+ isLoading={isLoading}
46
+ skeletonRows={skeletonRows}
47
+ actions={actions}
48
+ actionHandlers={actionHandlers}
49
+ renderActions={renderActions}
50
+ noDataMessage={noDataMessage}
51
+ noDataContent={noDataContent}
52
+ getRowKey={getRowKey}
53
+ onRowClick={onRowClick}
54
+ renderMobileCard={renderMobileCard}
55
+ pagination={pagination}
56
+ fullHeight={fullHeight}
57
+ />
58
+ </div>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <div className={cn('min-w-0', fullHeight && 'flex min-h-0 flex-1 flex-col')}>
64
+ <DesktopView
65
+ data={data}
66
+ columns={columns}
67
+ isLoading={isLoading}
68
+ skeletonRows={skeletonRows}
69
+ actions={actions}
70
+ actionHandlers={actionHandlers}
71
+ renderActions={renderActions}
72
+ noDataMessage={noDataMessage}
73
+ noDataContent={noDataContent}
74
+ getRowKey={getRowKey}
75
+ onRowClick={onRowClick}
76
+ pagination={pagination}
77
+ stickyHeader={stickyHeader}
78
+ maxHeight={maxHeight}
79
+ fullHeight={fullHeight}
80
+ sortState={sortState}
81
+ onSortChange={onSortChange}
82
+ />
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+
3
+ import { Eye, Pencil, Trash2 } from 'lucide-react';
4
+
5
+ import type { ActionHandlers, ActionType } from '@/components/ui/smart-table/types';
6
+ import type { ReactNode } from 'react';
7
+
8
+ import { Button } from '@/components/ui/button';
9
+
10
+ const SMART_TABLE_ACTIONS_CONTAINER_CLASS = 'flex w-full items-center justify-end gap-1';
11
+
12
+ interface SmartTableActionsProps<T> {
13
+ item: T;
14
+ actions?: ActionType[];
15
+ actionHandlers?: ActionHandlers<T>;
16
+ renderActions?: (item: T) => ReactNode;
17
+ }
18
+
19
+ const actionIcons: Record<ActionType, typeof Eye> = {
20
+ view: Eye,
21
+ edit: Pencil,
22
+ delete: Trash2,
23
+ };
24
+
25
+ const actionLabels: Record<ActionType, string> = {
26
+ view: 'Visualizza',
27
+ edit: 'Edit',
28
+ delete: 'Delete',
29
+ };
30
+
31
+ export function SmartTableActions<T>({
32
+ item,
33
+ actions,
34
+ actionHandlers,
35
+ renderActions,
36
+ }: SmartTableActionsProps<T>): React.ReactElement | null {
37
+ if (renderActions) {
38
+ return <div className={SMART_TABLE_ACTIONS_CONTAINER_CLASS}>{renderActions(item)}</div>;
39
+ }
40
+
41
+ if (!actions || !actionHandlers) {
42
+ return null;
43
+ }
44
+
45
+ return (
46
+ <div className={SMART_TABLE_ACTIONS_CONTAINER_CLASS}>
47
+ {actions.map((action) => {
48
+ const Icon = actionIcons[action];
49
+ const handler = actionHandlers[
50
+ `on${action.charAt(0).toUpperCase()}${action.slice(1)}` as keyof ActionHandlers<T>
51
+ ];
52
+
53
+ return (
54
+ <Button
55
+ key={action}
56
+ variant="ghost"
57
+ size="icon"
58
+ className="size-8"
59
+ aria-label={actionLabels[action]}
60
+ onClick={(event) => {
61
+ event.stopPropagation();
62
+ (handler as ((value: T) => void) | undefined)?.(item);
63
+ }}
64
+ >
65
+ <Icon className="size-4" />
66
+ </Button>
67
+ );
68
+ })}
69
+ </div>
70
+ );
71
+ }