@object-ui/components 0.3.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 (295) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +170 -0
  4. package/dist/index.css +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +46186 -0
  7. package/dist/index.umd.cjs +92 -0
  8. package/dist/src/hooks/use-mobile.d.ts +1 -0
  9. package/dist/src/index.d.ts +2 -0
  10. package/dist/src/index.test.d.ts +1 -0
  11. package/dist/src/lib/utils.d.ts +4 -0
  12. package/dist/src/new-components.test.d.ts +1 -0
  13. package/dist/src/renderers/basic/div.d.ts +1 -0
  14. package/dist/src/renderers/basic/html.d.ts +1 -0
  15. package/dist/src/renderers/basic/icon.d.ts +1 -0
  16. package/dist/src/renderers/basic/image.d.ts +1 -0
  17. package/dist/src/renderers/basic/index.d.ts +0 -0
  18. package/dist/src/renderers/basic/separator.d.ts +1 -0
  19. package/dist/src/renderers/basic/span.d.ts +1 -0
  20. package/dist/src/renderers/basic/text.d.ts +1 -0
  21. package/dist/src/renderers/complex/__tests__/data-table.test.d.ts +0 -0
  22. package/dist/src/renderers/complex/calendar-view.d.ts +1 -0
  23. package/dist/src/renderers/complex/carousel.d.ts +1 -0
  24. package/dist/src/renderers/complex/chatbot.d.ts +1 -0
  25. package/dist/src/renderers/complex/chatbot.test.d.ts +1 -0
  26. package/dist/src/renderers/complex/data-table.d.ts +1 -0
  27. package/dist/src/renderers/complex/filter-builder.d.ts +1 -0
  28. package/dist/src/renderers/complex/index.d.ts +0 -0
  29. package/dist/src/renderers/complex/resizable.d.ts +1 -0
  30. package/dist/src/renderers/complex/scroll-area.d.ts +1 -0
  31. package/dist/src/renderers/complex/table.d.ts +1 -0
  32. package/dist/src/renderers/complex/timeline.d.ts +1 -0
  33. package/dist/src/renderers/data-display/alert.d.ts +1 -0
  34. package/dist/src/renderers/data-display/avatar.d.ts +1 -0
  35. package/dist/src/renderers/data-display/badge.d.ts +1 -0
  36. package/dist/src/renderers/data-display/index.d.ts +0 -0
  37. package/dist/src/renderers/data-display/list.d.ts +1 -0
  38. package/dist/src/renderers/data-display/statistic.d.ts +1 -0
  39. package/dist/src/renderers/data-display/tree-view.d.ts +1 -0
  40. package/dist/src/renderers/disclosure/accordion.d.ts +1 -0
  41. package/dist/src/renderers/disclosure/collapsible.d.ts +1 -0
  42. package/dist/src/renderers/disclosure/index.d.ts +0 -0
  43. package/dist/src/renderers/feedback/index.d.ts +0 -0
  44. package/dist/src/renderers/feedback/loading.d.ts +1 -0
  45. package/dist/src/renderers/feedback/progress.d.ts +1 -0
  46. package/dist/src/renderers/feedback/skeleton.d.ts +1 -0
  47. package/dist/src/renderers/feedback/toaster.d.ts +1 -0
  48. package/dist/src/renderers/form/button.d.ts +1 -0
  49. package/dist/src/renderers/form/calendar.d.ts +1 -0
  50. package/dist/src/renderers/form/checkbox.d.ts +1 -0
  51. package/dist/src/renderers/form/date-picker.d.ts +1 -0
  52. package/dist/src/renderers/form/file-upload.d.ts +1 -0
  53. package/dist/src/renderers/form/form.d.ts +1 -0
  54. package/dist/src/renderers/form/index.d.ts +0 -0
  55. package/dist/src/renderers/form/input-otp.d.ts +1 -0
  56. package/dist/src/renderers/form/input.d.ts +1 -0
  57. package/dist/src/renderers/form/label.d.ts +1 -0
  58. package/dist/src/renderers/form/radio-group.d.ts +1 -0
  59. package/dist/src/renderers/form/select.d.ts +1 -0
  60. package/dist/src/renderers/form/slider.d.ts +1 -0
  61. package/dist/src/renderers/form/switch.d.ts +1 -0
  62. package/dist/src/renderers/form/textarea.d.ts +1 -0
  63. package/dist/src/renderers/form/toggle.d.ts +1 -0
  64. package/dist/src/renderers/index.d.ts +0 -0
  65. package/dist/src/renderers/layout/card.d.ts +1 -0
  66. package/dist/src/renderers/layout/container.d.ts +1 -0
  67. package/dist/src/renderers/layout/flex.d.ts +1 -0
  68. package/dist/src/renderers/layout/grid.d.ts +1 -0
  69. package/dist/src/renderers/layout/index.d.ts +0 -0
  70. package/dist/src/renderers/layout/page.d.ts +7 -0
  71. package/dist/src/renderers/layout/semantic.d.ts +1 -0
  72. package/dist/src/renderers/layout/stack.d.ts +1 -0
  73. package/dist/src/renderers/layout/tabs.d.ts +1 -0
  74. package/dist/src/renderers/navigation/header-bar.d.ts +1 -0
  75. package/dist/src/renderers/navigation/index.d.ts +0 -0
  76. package/dist/src/renderers/navigation/sidebar.d.ts +1 -0
  77. package/dist/src/renderers/overlay/alert-dialog.d.ts +1 -0
  78. package/dist/src/renderers/overlay/context-menu.d.ts +1 -0
  79. package/dist/src/renderers/overlay/dialog.d.ts +1 -0
  80. package/dist/src/renderers/overlay/drawer.d.ts +1 -0
  81. package/dist/src/renderers/overlay/dropdown-menu.d.ts +1 -0
  82. package/dist/src/renderers/overlay/hover-card.d.ts +1 -0
  83. package/dist/src/renderers/overlay/index.d.ts +0 -0
  84. package/dist/src/renderers/overlay/popover.d.ts +1 -0
  85. package/dist/src/renderers/overlay/sheet.d.ts +1 -0
  86. package/dist/src/renderers/overlay/tooltip.d.ts +1 -0
  87. package/dist/src/ui/accordion.d.ts +7 -0
  88. package/dist/src/ui/alert-dialog.d.ts +14 -0
  89. package/dist/src/ui/alert.d.ts +9 -0
  90. package/dist/src/ui/aspect-ratio.d.ts +3 -0
  91. package/dist/src/ui/avatar.d.ts +6 -0
  92. package/dist/src/ui/badge.d.ts +9 -0
  93. package/dist/src/ui/breadcrumb.d.ts +11 -0
  94. package/dist/src/ui/button-group.d.ts +11 -0
  95. package/dist/src/ui/button.d.ts +13 -0
  96. package/dist/src/ui/calendar-view.d.ts +21 -0
  97. package/dist/src/ui/calendar.d.ts +8 -0
  98. package/dist/src/ui/card.d.ts +9 -0
  99. package/dist/src/ui/carousel.d.ts +19 -0
  100. package/dist/src/ui/chatbot.d.ts +36 -0
  101. package/dist/src/ui/checkbox.d.ts +4 -0
  102. package/dist/src/ui/collapsible.d.ts +5 -0
  103. package/dist/src/ui/command.d.ts +18 -0
  104. package/dist/src/ui/context-menu.d.ts +25 -0
  105. package/dist/src/ui/dialog.d.ts +15 -0
  106. package/dist/src/ui/drawer.d.ts +13 -0
  107. package/dist/src/ui/dropdown-menu.d.ts +25 -0
  108. package/dist/src/ui/empty.d.ts +11 -0
  109. package/dist/src/ui/field.d.ts +24 -0
  110. package/dist/src/ui/filter-builder.d.ts +31 -0
  111. package/dist/src/ui/form.d.ts +24 -0
  112. package/dist/src/ui/hover-card.d.ts +6 -0
  113. package/dist/src/ui/index.d.ts +56 -0
  114. package/dist/src/ui/input-group.d.ts +16 -0
  115. package/dist/src/ui/input-otp.d.ts +11 -0
  116. package/dist/src/ui/input.d.ts +3 -0
  117. package/dist/src/ui/item.d.ts +23 -0
  118. package/dist/src/ui/kbd.d.ts +3 -0
  119. package/dist/src/ui/label.d.ts +4 -0
  120. package/dist/src/ui/menubar.d.ts +26 -0
  121. package/dist/src/ui/navigation-menu.d.ts +14 -0
  122. package/dist/src/ui/pagination.d.ts +13 -0
  123. package/dist/src/ui/popover.d.ts +7 -0
  124. package/dist/src/ui/progress.d.ts +4 -0
  125. package/dist/src/ui/radio-group.d.ts +5 -0
  126. package/dist/src/ui/resizable.d.ts +10 -0
  127. package/dist/src/ui/scroll-area.d.ts +5 -0
  128. package/dist/src/ui/select.d.ts +15 -0
  129. package/dist/src/ui/separator.d.ts +4 -0
  130. package/dist/src/ui/sheet.d.ts +13 -0
  131. package/dist/src/ui/sidebar.d.ts +69 -0
  132. package/dist/src/ui/skeleton.d.ts +2 -0
  133. package/dist/src/ui/slider.d.ts +4 -0
  134. package/dist/src/ui/sonner.d.ts +3 -0
  135. package/dist/src/ui/spinner.d.ts +3 -0
  136. package/dist/src/ui/switch.d.ts +4 -0
  137. package/dist/src/ui/table.d.ts +10 -0
  138. package/dist/src/ui/tabs.d.ts +7 -0
  139. package/dist/src/ui/textarea.d.ts +3 -0
  140. package/dist/src/ui/timeline.d.ts +25 -0
  141. package/dist/src/ui/toggle-group.d.ts +9 -0
  142. package/dist/src/ui/toggle.d.ts +9 -0
  143. package/dist/src/ui/tooltip.d.ts +7 -0
  144. package/docs/FilterBuilder.md +268 -0
  145. package/metadata/Chart.component.yml +30 -0
  146. package/metadata/FilterBuilder.component.yml +39 -0
  147. package/metadata/GridLayout.component.yml +27 -0
  148. package/metadata/Menu.component.yml +31 -0
  149. package/metadata/ObjectForm.component.yml +34 -0
  150. package/metadata/ObjectTable.component.yml +41 -0
  151. package/metadata/Page.component.yml +24 -0
  152. package/package.json +87 -0
  153. package/postcss.config.js +6 -0
  154. package/src/hooks/use-mobile.tsx +19 -0
  155. package/src/index.css +76 -0
  156. package/src/index.test.ts +7 -0
  157. package/src/index.ts +10 -0
  158. package/src/lib/utils.tsx +27 -0
  159. package/src/new-components.test.ts +74 -0
  160. package/src/renderers/basic/div.tsx +41 -0
  161. package/src/renderers/basic/html.tsx +34 -0
  162. package/src/renderers/basic/icon.tsx +25 -0
  163. package/src/renderers/basic/image.tsx +37 -0
  164. package/src/renderers/basic/index.ts +7 -0
  165. package/src/renderers/basic/separator.tsx +48 -0
  166. package/src/renderers/basic/span.tsx +44 -0
  167. package/src/renderers/basic/text.tsx +42 -0
  168. package/src/renderers/complex/README-KANBAN.md +208 -0
  169. package/src/renderers/complex/TIMELINE.md +353 -0
  170. package/src/renderers/complex/__tests__/data-table.test.ts +52 -0
  171. package/src/renderers/complex/calendar-view.tsx +219 -0
  172. package/src/renderers/complex/carousel.tsx +60 -0
  173. package/src/renderers/complex/chatbot.test.ts +44 -0
  174. package/src/renderers/complex/chatbot.tsx +185 -0
  175. package/src/renderers/complex/data-table.tsx +650 -0
  176. package/src/renderers/complex/filter-builder.tsx +68 -0
  177. package/src/renderers/complex/index.ts +10 -0
  178. package/src/renderers/complex/resizable.tsx +54 -0
  179. package/src/renderers/complex/scroll-area.tsx +32 -0
  180. package/src/renderers/complex/table.tsx +86 -0
  181. package/src/renderers/complex/timeline.tsx +466 -0
  182. package/src/renderers/data-display/alert.tsx +37 -0
  183. package/src/renderers/data-display/avatar.tsx +29 -0
  184. package/src/renderers/data-display/badge.tsx +46 -0
  185. package/src/renderers/data-display/index.ts +6 -0
  186. package/src/renderers/data-display/list.tsx +95 -0
  187. package/src/renderers/data-display/statistic.tsx +98 -0
  188. package/src/renderers/data-display/tree-view.tsx +180 -0
  189. package/src/renderers/disclosure/accordion.tsx +60 -0
  190. package/src/renderers/disclosure/collapsible.tsx +44 -0
  191. package/src/renderers/disclosure/index.ts +2 -0
  192. package/src/renderers/feedback/index.ts +4 -0
  193. package/src/renderers/feedback/loading.tsx +69 -0
  194. package/src/renderers/feedback/progress.tsx +20 -0
  195. package/src/renderers/feedback/skeleton.tsx +22 -0
  196. package/src/renderers/feedback/toaster.tsx +26 -0
  197. package/src/renderers/form/button.tsx +61 -0
  198. package/src/renderers/form/calendar.tsx +25 -0
  199. package/src/renderers/form/checkbox.tsx +41 -0
  200. package/src/renderers/form/date-picker.tsx +75 -0
  201. package/src/renderers/form/file-upload.tsx +175 -0
  202. package/src/renderers/form/form.tsx +417 -0
  203. package/src/renderers/form/index.ts +16 -0
  204. package/src/renderers/form/input-otp.tsx +31 -0
  205. package/src/renderers/form/input.tsx +79 -0
  206. package/src/renderers/form/label.tsx +36 -0
  207. package/src/renderers/form/radio-group.tsx +54 -0
  208. package/src/renderers/form/select.tsx +66 -0
  209. package/src/renderers/form/slider.tsx +45 -0
  210. package/src/renderers/form/switch.tsx +39 -0
  211. package/src/renderers/form/textarea.tsx +45 -0
  212. package/src/renderers/form/toggle.tsx +76 -0
  213. package/src/renderers/index.ts +9 -0
  214. package/src/renderers/layout/card.tsx +69 -0
  215. package/src/renderers/layout/container.tsx +113 -0
  216. package/src/renderers/layout/flex.tsx +123 -0
  217. package/src/renderers/layout/grid.tsx +155 -0
  218. package/src/renderers/layout/index.ts +10 -0
  219. package/src/renderers/layout/page.tsx +82 -0
  220. package/src/renderers/layout/semantic.tsx +39 -0
  221. package/src/renderers/layout/stack.tsx +123 -0
  222. package/src/renderers/layout/tabs.tsx +63 -0
  223. package/src/renderers/navigation/header-bar.tsx +50 -0
  224. package/src/renderers/navigation/index.ts +2 -0
  225. package/src/renderers/navigation/sidebar.tsx +189 -0
  226. package/src/renderers/overlay/alert-dialog.tsx +63 -0
  227. package/src/renderers/overlay/context-menu.tsx +91 -0
  228. package/src/renderers/overlay/dialog.tsx +68 -0
  229. package/src/renderers/overlay/drawer.tsx +68 -0
  230. package/src/renderers/overlay/dropdown-menu.tsx +90 -0
  231. package/src/renderers/overlay/hover-card.tsx +46 -0
  232. package/src/renderers/overlay/index.ts +9 -0
  233. package/src/renderers/overlay/popover.tsx +47 -0
  234. package/src/renderers/overlay/sheet.tsx +68 -0
  235. package/src/renderers/overlay/tooltip.tsx +58 -0
  236. package/src/ui/accordion.tsx +64 -0
  237. package/src/ui/alert-dialog.tsx +155 -0
  238. package/src/ui/alert.tsx +78 -0
  239. package/src/ui/aspect-ratio.tsx +11 -0
  240. package/src/ui/avatar.tsx +51 -0
  241. package/src/ui/badge.tsx +46 -0
  242. package/src/ui/breadcrumb.tsx +109 -0
  243. package/src/ui/button-group.tsx +83 -0
  244. package/src/ui/button.tsx +65 -0
  245. package/src/ui/calendar-view.tsx +503 -0
  246. package/src/ui/calendar.tsx +237 -0
  247. package/src/ui/card.tsx +138 -0
  248. package/src/ui/carousel.tsx +239 -0
  249. package/src/ui/chatbot.tsx +240 -0
  250. package/src/ui/checkbox.tsx +32 -0
  251. package/src/ui/collapsible.tsx +31 -0
  252. package/src/ui/command.tsx +182 -0
  253. package/src/ui/context-menu.tsx +247 -0
  254. package/src/ui/dialog.tsx +141 -0
  255. package/src/ui/drawer.tsx +135 -0
  256. package/src/ui/dropdown-menu.tsx +254 -0
  257. package/src/ui/empty.tsx +104 -0
  258. package/src/ui/field.tsx +246 -0
  259. package/src/ui/filter-builder.tsx +359 -0
  260. package/src/ui/form.tsx +167 -0
  261. package/src/ui/hover-card.tsx +44 -0
  262. package/src/ui/index.ts +56 -0
  263. package/src/ui/input-group.tsx +170 -0
  264. package/src/ui/input-otp.tsx +81 -0
  265. package/src/ui/input.tsx +24 -0
  266. package/src/ui/item.tsx +193 -0
  267. package/src/ui/kbd.tsx +28 -0
  268. package/src/ui/label.tsx +24 -0
  269. package/src/ui/menubar.tsx +274 -0
  270. package/src/ui/navigation-menu.tsx +168 -0
  271. package/src/ui/pagination.tsx +127 -0
  272. package/src/ui/popover.tsx +48 -0
  273. package/src/ui/progress.tsx +41 -0
  274. package/src/ui/radio-group.tsx +45 -0
  275. package/src/ui/resizable.tsx +55 -0
  276. package/src/ui/scroll-area.tsx +58 -0
  277. package/src/ui/select.tsx +188 -0
  278. package/src/ui/separator.tsx +31 -0
  279. package/src/ui/sheet.tsx +137 -0
  280. package/src/ui/sidebar.tsx +726 -0
  281. package/src/ui/skeleton.tsx +20 -0
  282. package/src/ui/slider.tsx +63 -0
  283. package/src/ui/sonner.tsx +43 -0
  284. package/src/ui/spinner.tsx +38 -0
  285. package/src/ui/switch.tsx +31 -0
  286. package/src/ui/table.tsx +120 -0
  287. package/src/ui/tabs.tsx +86 -0
  288. package/src/ui/textarea.tsx +18 -0
  289. package/src/ui/timeline.tsx +266 -0
  290. package/src/ui/toggle-group.tsx +87 -0
  291. package/src/ui/toggle.tsx +50 -0
  292. package/src/ui/tooltip.tsx +61 -0
  293. package/tailwind.config.js +75 -0
  294. package/tsconfig.json +18 -0
  295. package/vite.config.ts +44 -0
@@ -0,0 +1,650 @@
1
+ // Enterprise-level DataTable Component (Airtable-like)
2
+ import React, { useState, useMemo, useRef, useEffect } from 'react';
3
+ import { ComponentRegistry } from '@object-ui/core';
4
+ import type { DataTableSchema } from '@object-ui/types';
5
+ import {
6
+ Table,
7
+ TableHeader,
8
+ TableBody,
9
+ TableFooter,
10
+ TableHead,
11
+ TableRow,
12
+ TableCell,
13
+ TableCaption
14
+ } from '../../ui/table';
15
+ import { Button } from '../../ui/button';
16
+ import { Input } from '../../ui/input';
17
+ import { Checkbox } from '../../ui/checkbox';
18
+ import {
19
+ Select,
20
+ SelectContent,
21
+ SelectItem,
22
+ SelectTrigger,
23
+ SelectValue,
24
+ } from '../../ui/select';
25
+ import {
26
+ ChevronUp,
27
+ ChevronDown,
28
+ ChevronsUpDown,
29
+ Search,
30
+ Download,
31
+ Edit,
32
+ Trash2,
33
+ ChevronLeft,
34
+ ChevronRight,
35
+ ChevronsLeft,
36
+ ChevronsRight,
37
+ GripVertical,
38
+ } from 'lucide-react';
39
+
40
+ type SortDirection = 'asc' | 'desc' | null;
41
+
42
+ /**
43
+ * Enterprise-level data table component with Airtable-like features.
44
+ *
45
+ * Provides comprehensive table functionality including:
46
+ * - Multi-column sorting (ascending/descending/none)
47
+ * - Real-time search across all columns
48
+ * - Pagination with configurable page sizes
49
+ * - Row selection with persistence across pages
50
+ * - CSV export of filtered/sorted data
51
+ * - Row action buttons (edit/delete)
52
+ *
53
+ * @example
54
+ * ```json
55
+ * {
56
+ * "type": "data-table",
57
+ * "pagination": true,
58
+ * "searchable": true,
59
+ * "selectable": true,
60
+ * "sortable": true,
61
+ * "exportable": true,
62
+ * "rowActions": true,
63
+ * "columns": [
64
+ * { "header": "ID", "accessorKey": "id", "width": "80px" },
65
+ * { "header": "Name", "accessorKey": "name" }
66
+ * ],
67
+ * "data": [
68
+ * { "id": 1, "name": "John Doe" }
69
+ * ]
70
+ * }
71
+ * ```
72
+ *
73
+ * @param {Object} props - Component props
74
+ * @param {DataTableSchema} props.schema - Table schema configuration
75
+ * @returns {JSX.Element} Rendered data table component
76
+ */
77
+ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
78
+ const {
79
+ caption,
80
+ columns: initialColumns = [],
81
+ data = [],
82
+ pagination = true,
83
+ pageSize: initialPageSize = 10,
84
+ searchable = true,
85
+ selectable = false,
86
+ sortable = true,
87
+ exportable = false,
88
+ rowActions = false,
89
+ resizableColumns = true,
90
+ reorderableColumns = true,
91
+ className,
92
+ } = schema;
93
+
94
+ // State management
95
+ const [searchQuery, setSearchQuery] = useState('');
96
+ const [sortColumn, setSortColumn] = useState<string | null>(null);
97
+ const [sortDirection, setSortDirection] = useState<SortDirection>(null);
98
+ const [selectedRowIds, setSelectedRowIds] = useState<Set<any>>(new Set());
99
+ const [currentPage, setCurrentPage] = useState(1);
100
+ const [pageSize, setPageSize] = useState(initialPageSize);
101
+ const [columns, setColumns] = useState(initialColumns);
102
+ const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
103
+ const [draggedColumn, setDraggedColumn] = useState<number | null>(null);
104
+ const [dragOverColumn, setDragOverColumn] = useState<number | null>(null);
105
+
106
+ // Refs for column resizing
107
+ const resizingColumn = useRef<string | null>(null);
108
+ const startX = useRef<number>(0);
109
+ const startWidth = useRef<number>(0);
110
+
111
+ // Update columns when schema changes
112
+ useEffect(() => {
113
+ setColumns(initialColumns);
114
+ }, [initialColumns]);
115
+
116
+ // Filtering
117
+ const filteredData = useMemo(() => {
118
+ if (!searchQuery) return data;
119
+
120
+ return data.filter((row) =>
121
+ columns.some((col) => {
122
+ const value = row[col.accessorKey];
123
+ return value?.toString().toLowerCase().includes(searchQuery.toLowerCase());
124
+ })
125
+ );
126
+ }, [data, searchQuery, columns]);
127
+
128
+ // Sorting
129
+ const sortedData = useMemo(() => {
130
+ if (!sortColumn || !sortDirection) return filteredData;
131
+
132
+ return [...filteredData].sort((a, b) => {
133
+ const aValue = a[sortColumn];
134
+ const bValue = b[sortColumn];
135
+
136
+ if (aValue === bValue) return 0;
137
+
138
+ const comparison = aValue < bValue ? -1 : 1;
139
+ return sortDirection === 'asc' ? comparison : -comparison;
140
+ });
141
+ }, [filteredData, sortColumn, sortDirection]);
142
+
143
+ // Pagination
144
+ const totalPages = Math.ceil(sortedData.length / pageSize);
145
+ const paginatedData = pagination
146
+ ? sortedData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
147
+ : sortedData;
148
+
149
+ /**
150
+ * Generates a unique identifier for each row to maintain stable selection state
151
+ * across pagination and sorting operations.
152
+ *
153
+ * @param {any} row - The data row object
154
+ * @param {number} index - The row's index in the dataset
155
+ * @returns {string | number} Unique row identifier (uses 'id' field if available, falls back to index)
156
+ */
157
+ const getRowId = (row: any, index: number) => {
158
+ // Try to use 'id' field, fall back to index
159
+ return row.id !== undefined ? row.id : `row-${index}`;
160
+ };
161
+
162
+ // Handlers
163
+ const handleSort = (columnKey: string) => {
164
+ if (!sortable) return;
165
+
166
+ if (sortColumn === columnKey) {
167
+ if (sortDirection === 'asc') {
168
+ setSortDirection('desc');
169
+ } else if (sortDirection === 'desc') {
170
+ setSortDirection(null);
171
+ setSortColumn(null);
172
+ }
173
+ } else {
174
+ setSortColumn(columnKey);
175
+ setSortDirection('asc');
176
+ }
177
+ };
178
+
179
+ const handleSelectAll = (checked: boolean) => {
180
+ const newSelected = new Set<any>();
181
+ if (checked) {
182
+ paginatedData.forEach((row, idx) => {
183
+ const globalIndex = (currentPage - 1) * pageSize + idx;
184
+ const rowId = getRowId(row, globalIndex);
185
+ newSelected.add(rowId);
186
+ });
187
+ }
188
+ setSelectedRowIds(newSelected);
189
+
190
+ // Call callback if provided
191
+ if (schema.onSelectionChange) {
192
+ const selectedData = sortedData.filter((row, idx) => {
193
+ const rowId = getRowId(row, idx);
194
+ return newSelected.has(rowId);
195
+ });
196
+ schema.onSelectionChange(selectedData);
197
+ }
198
+ };
199
+
200
+ const handleSelectRow = (rowId: any, checked: boolean) => {
201
+ const newSelected = new Set(selectedRowIds);
202
+ if (checked) {
203
+ newSelected.add(rowId);
204
+ } else {
205
+ newSelected.delete(rowId);
206
+ }
207
+ setSelectedRowIds(newSelected);
208
+
209
+ // Call callback if provided
210
+ if (schema.onSelectionChange) {
211
+ const selectedData = sortedData.filter((row, idx) => {
212
+ const id = getRowId(row, idx);
213
+ return newSelected.has(id);
214
+ });
215
+ schema.onSelectionChange(selectedData);
216
+ }
217
+ };
218
+
219
+ const handleExport = () => {
220
+ const csvContent = [
221
+ columns.map(col => col.header).join(','),
222
+ ...sortedData.map(row =>
223
+ columns.map(col => JSON.stringify(row[col.accessorKey] || '')).join(',')
224
+ )
225
+ ].join('\n');
226
+
227
+ const blob = new Blob([csvContent], { type: 'text/csv' });
228
+ const url = window.URL.createObjectURL(blob);
229
+ const a = document.createElement('a');
230
+ a.href = url;
231
+ a.download = 'table-export.csv';
232
+ a.click();
233
+ window.URL.revokeObjectURL(url);
234
+ };
235
+
236
+ const getSortIcon = (columnKey: string) => {
237
+ if (sortColumn !== columnKey) {
238
+ return <ChevronsUpDown className="h-4 w-4 ml-1 opacity-50" />;
239
+ }
240
+ if (sortDirection === 'asc') {
241
+ return <ChevronUp className="h-4 w-4 ml-1" />;
242
+ }
243
+ return <ChevronDown className="h-4 w-4 ml-1" />;
244
+ };
245
+
246
+ // Column resizing handlers
247
+ const handleResizeStart = (e: React.MouseEvent, columnKey: string) => {
248
+ if (!resizableColumns) return;
249
+ e.preventDefault();
250
+ e.stopPropagation();
251
+
252
+ resizingColumn.current = columnKey;
253
+ startX.current = e.clientX;
254
+
255
+ const headerCell = (e.target as HTMLElement).closest('th');
256
+ if (headerCell) {
257
+ startWidth.current = headerCell.offsetWidth;
258
+ }
259
+
260
+ document.addEventListener('mousemove', handleResizeMove);
261
+ document.addEventListener('mouseup', handleResizeEnd);
262
+ };
263
+
264
+ const handleResizeMove = (e: MouseEvent) => {
265
+ if (!resizingColumn.current) return;
266
+
267
+ const diff = e.clientX - startX.current;
268
+ const newWidth = Math.max(50, startWidth.current + diff); // Min width 50px
269
+
270
+ setColumnWidths(prev => ({
271
+ ...prev,
272
+ [resizingColumn.current!]: newWidth
273
+ }));
274
+ };
275
+
276
+ const handleResizeEnd = () => {
277
+ resizingColumn.current = null;
278
+ document.removeEventListener('mousemove', handleResizeMove);
279
+ document.removeEventListener('mouseup', handleResizeEnd);
280
+ };
281
+
282
+ // Column reordering handlers
283
+ const handleColumnDragStart = (e: React.DragEvent, index: number) => {
284
+ if (!reorderableColumns) return;
285
+ setDraggedColumn(index);
286
+ e.dataTransfer.effectAllowed = 'move';
287
+ };
288
+
289
+ const handleColumnDragOver = (e: React.DragEvent, index: number) => {
290
+ if (!reorderableColumns) return;
291
+ e.preventDefault();
292
+ e.dataTransfer.dropEffect = 'move';
293
+ setDragOverColumn(index);
294
+ };
295
+
296
+ const handleColumnDrop = (e: React.DragEvent, dropIndex: number) => {
297
+ if (!reorderableColumns || draggedColumn === null) return;
298
+ e.preventDefault();
299
+
300
+ if (draggedColumn === dropIndex) {
301
+ setDraggedColumn(null);
302
+ setDragOverColumn(null);
303
+ return;
304
+ }
305
+
306
+ const newColumns = [...columns];
307
+ const [removed] = newColumns.splice(draggedColumn, 1);
308
+ newColumns.splice(dropIndex, 0, removed);
309
+
310
+ setColumns(newColumns);
311
+ setDraggedColumn(null);
312
+ setDragOverColumn(null);
313
+
314
+ // Call callback if provided
315
+ if (schema.onColumnsReorder) {
316
+ schema.onColumnsReorder(newColumns);
317
+ }
318
+ };
319
+
320
+ const handleColumnDragEnd = () => {
321
+ setDraggedColumn(null);
322
+ setDragOverColumn(null);
323
+ };
324
+
325
+ // Cleanup on unmount
326
+ useEffect(() => {
327
+ return () => {
328
+ document.removeEventListener('mousemove', handleResizeMove);
329
+ document.removeEventListener('mouseup', handleResizeEnd);
330
+ };
331
+ }, []);
332
+
333
+ // Check if all rows on current page are selected
334
+ const allPageRowsSelected = paginatedData.length > 0 && paginatedData.every((row, idx) => {
335
+ const globalIndex = (currentPage - 1) * pageSize + idx;
336
+ const rowId = getRowId(row, globalIndex);
337
+ return selectedRowIds.has(rowId);
338
+ });
339
+
340
+ const somePageRowsSelected = paginatedData.some((row, idx) => {
341
+ const globalIndex = (currentPage - 1) * pageSize + idx;
342
+ const rowId = getRowId(row, globalIndex);
343
+ return selectedRowIds.has(rowId);
344
+ }) && !allPageRowsSelected;
345
+
346
+ return (
347
+ <div className={`space-y-4 ${className || ''}`}>
348
+ {/* Toolbar */}
349
+ <div className="flex items-center justify-between gap-4">
350
+ <div className="flex items-center gap-2 flex-1">
351
+ {searchable && (
352
+ <div className="relative max-w-sm flex-1">
353
+ <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
354
+ <Input
355
+ placeholder="Search..."
356
+ value={searchQuery}
357
+ onChange={(e) => {
358
+ setSearchQuery(e.target.value);
359
+ setCurrentPage(1);
360
+ }}
361
+ className="pl-8"
362
+ />
363
+ </div>
364
+ )}
365
+ </div>
366
+
367
+ <div className="flex items-center gap-2">
368
+ {exportable && (
369
+ <Button
370
+ variant="outline"
371
+ size="sm"
372
+ onClick={handleExport}
373
+ disabled={sortedData.length === 0}
374
+ >
375
+ <Download className="h-4 w-4 mr-2" />
376
+ Export CSV
377
+ </Button>
378
+ )}
379
+
380
+ {selectable && selectedRowIds.size > 0 && (
381
+ <div className="text-sm text-muted-foreground">
382
+ {selectedRowIds.size} selected
383
+ </div>
384
+ )}
385
+ </div>
386
+ </div>
387
+
388
+ {/* Table */}
389
+ <div className="border rounded-lg">
390
+ <Table>
391
+ {caption && <TableCaption>{caption}</TableCaption>}
392
+ <TableHeader>
393
+ <TableRow>
394
+ {selectable && (
395
+ <TableHead className="w-12">
396
+ <Checkbox
397
+ checked={allPageRowsSelected ? true : somePageRowsSelected ? 'indeterminate' : false}
398
+ onCheckedChange={handleSelectAll}
399
+ />
400
+ </TableHead>
401
+ )}
402
+ {columns.map((col, index) => {
403
+ const columnWidth = columnWidths[col.accessorKey] || col.width;
404
+ const isDragging = draggedColumn === index;
405
+ const isDragOver = dragOverColumn === index;
406
+
407
+ return (
408
+ <TableHead
409
+ key={col.accessorKey}
410
+ className={`${col.className || ''} ${sortable && col.sortable !== false ? 'cursor-pointer select-none' : ''} ${isDragging ? 'opacity-50' : ''} ${isDragOver ? 'border-l-2 border-primary' : ''} relative group`}
411
+ style={{
412
+ width: columnWidth,
413
+ minWidth: columnWidth
414
+ }}
415
+ draggable={reorderableColumns}
416
+ onDragStart={(e) => handleColumnDragStart(e, index)}
417
+ onDragOver={(e) => handleColumnDragOver(e, index)}
418
+ onDrop={(e) => handleColumnDrop(e, index)}
419
+ onDragEnd={handleColumnDragEnd}
420
+ onClick={() => sortable && col.sortable !== false && handleSort(col.accessorKey)}
421
+ >
422
+ <div className="flex items-center justify-between">
423
+ <div className="flex items-center gap-1">
424
+ {reorderableColumns && (
425
+ <GripVertical className="h-4 w-4 opacity-0 group-hover:opacity-50 cursor-grab active:cursor-grabbing flex-shrink-0" />
426
+ )}
427
+ <span>{col.header}</span>
428
+ {sortable && col.sortable !== false && getSortIcon(col.accessorKey)}
429
+ </div>
430
+ {resizableColumns && col.resizable !== false && (
431
+ <div
432
+ className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary opacity-0 hover:opacity-100 transition-opacity"
433
+ onMouseDown={(e) => handleResizeStart(e, col.accessorKey)}
434
+ onClick={(e) => e.stopPropagation()}
435
+ />
436
+ )}
437
+ </div>
438
+ </TableHead>
439
+ );
440
+ })}
441
+ {rowActions && (
442
+ <TableHead className="w-24 text-right">Actions</TableHead>
443
+ )}
444
+ </TableRow>
445
+ </TableHeader>
446
+ <TableBody>
447
+ {paginatedData.length === 0 ? (
448
+ <TableRow>
449
+ <TableCell
450
+ colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)}
451
+ className="h-24 text-center text-muted-foreground"
452
+ >
453
+ No data available
454
+ </TableCell>
455
+ </TableRow>
456
+ ) : (
457
+ paginatedData.map((row, rowIndex) => {
458
+ const globalIndex = (currentPage - 1) * pageSize + rowIndex;
459
+ const rowId = getRowId(row, globalIndex);
460
+ const isSelected = selectedRowIds.has(rowId);
461
+
462
+ return (
463
+ <TableRow key={rowId} data-state={isSelected ? 'selected' : undefined}>
464
+ {selectable && (
465
+ <TableCell>
466
+ <Checkbox
467
+ checked={isSelected}
468
+ onCheckedChange={(checked) => handleSelectRow(rowId, checked as boolean)}
469
+ />
470
+ </TableCell>
471
+ )}
472
+ {columns.map((col, colIndex) => {
473
+ const columnWidth = columnWidths[col.accessorKey] || col.width;
474
+ return (
475
+ <TableCell
476
+ key={colIndex}
477
+ className={col.cellClassName}
478
+ style={{
479
+ width: columnWidth,
480
+ minWidth: columnWidth,
481
+ maxWidth: columnWidth
482
+ }}
483
+ >
484
+ {row[col.accessorKey]}
485
+ </TableCell>
486
+ );
487
+ })}
488
+ {rowActions && (
489
+ <TableCell className="text-right">
490
+ <div className="flex items-center justify-end gap-1">
491
+ <Button
492
+ variant="ghost"
493
+ size="icon-sm"
494
+ onClick={() => schema.onRowEdit?.(row)}
495
+ >
496
+ <Edit className="h-4 w-4" />
497
+ </Button>
498
+ <Button
499
+ variant="ghost"
500
+ size="icon-sm"
501
+ onClick={() => schema.onRowDelete?.(row)}
502
+ >
503
+ <Trash2 className="h-4 w-4 text-destructive" />
504
+ </Button>
505
+ </div>
506
+ </TableCell>
507
+ )}
508
+ </TableRow>
509
+ );
510
+ })
511
+ )}
512
+ </TableBody>
513
+ </Table>
514
+ </div>
515
+
516
+ {/* Pagination */}
517
+ {pagination && sortedData.length > 0 && (
518
+ <div className="flex items-center justify-between">
519
+ <div className="flex items-center gap-2">
520
+ <span className="text-sm text-muted-foreground">Rows per page:</span>
521
+ <Select
522
+ value={pageSize.toString()}
523
+ onValueChange={(value) => {
524
+ setPageSize(Number(value));
525
+ setCurrentPage(1);
526
+ }}
527
+ >
528
+ <SelectTrigger className="w-20">
529
+ <SelectValue />
530
+ </SelectTrigger>
531
+ <SelectContent>
532
+ <SelectItem value="5">5</SelectItem>
533
+ <SelectItem value="10">10</SelectItem>
534
+ <SelectItem value="20">20</SelectItem>
535
+ <SelectItem value="50">50</SelectItem>
536
+ <SelectItem value="100">100</SelectItem>
537
+ </SelectContent>
538
+ </Select>
539
+ </div>
540
+
541
+ <div className="flex items-center gap-2">
542
+ <span className="text-sm text-muted-foreground">
543
+ Page {currentPage} of {totalPages} ({sortedData.length} total)
544
+ </span>
545
+ <div className="flex items-center gap-1">
546
+ <Button
547
+ variant="outline"
548
+ size="icon-sm"
549
+ onClick={() => setCurrentPage(1)}
550
+ disabled={currentPage === 1}
551
+ >
552
+ <ChevronsLeft className="h-4 w-4" />
553
+ </Button>
554
+ <Button
555
+ variant="outline"
556
+ size="icon-sm"
557
+ onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
558
+ disabled={currentPage === 1}
559
+ >
560
+ <ChevronLeft className="h-4 w-4" />
561
+ </Button>
562
+ <Button
563
+ variant="outline"
564
+ size="icon-sm"
565
+ onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
566
+ disabled={currentPage === totalPages}
567
+ >
568
+ <ChevronRight className="h-4 w-4" />
569
+ </Button>
570
+ <Button
571
+ variant="outline"
572
+ size="icon-sm"
573
+ onClick={() => setCurrentPage(totalPages)}
574
+ disabled={currentPage === totalPages}
575
+ >
576
+ <ChevronsRight className="h-4 w-4" />
577
+ </Button>
578
+ </div>
579
+ </div>
580
+ </div>
581
+ )}
582
+ </div>
583
+ );
584
+ };
585
+
586
+ // Register the component
587
+ ComponentRegistry.register('data-table', DataTableRenderer, {
588
+ label: 'Data Table',
589
+ icon: 'table',
590
+ inputs: [
591
+ { name: 'caption', type: 'string', label: 'Caption' },
592
+ {
593
+ name: 'columns',
594
+ type: 'array',
595
+ label: 'Columns',
596
+ description: 'Array of { header, accessorKey, className, width, sortable, filterable, resizable }',
597
+ required: true,
598
+ },
599
+ {
600
+ name: 'data',
601
+ type: 'array',
602
+ label: 'Data',
603
+ description: 'Array of data objects',
604
+ required: true,
605
+ },
606
+ { name: 'pagination', type: 'boolean', label: 'Enable Pagination', defaultValue: true },
607
+ { name: 'pageSize', type: 'number', label: 'Page Size', defaultValue: 10 },
608
+ { name: 'searchable', type: 'boolean', label: 'Enable Search', defaultValue: true },
609
+ { name: 'selectable', type: 'boolean', label: 'Enable Row Selection', defaultValue: false },
610
+ { name: 'sortable', type: 'boolean', label: 'Enable Sorting', defaultValue: true },
611
+ { name: 'exportable', type: 'boolean', label: 'Enable Export', defaultValue: false },
612
+ { name: 'rowActions', type: 'boolean', label: 'Show Row Actions', defaultValue: false },
613
+ { name: 'resizableColumns', type: 'boolean', label: 'Enable Column Resizing', defaultValue: true },
614
+ { name: 'reorderableColumns', type: 'boolean', label: 'Enable Column Reordering', defaultValue: true },
615
+ { name: 'className', type: 'string', label: 'CSS Class' },
616
+ ],
617
+ defaultProps: {
618
+ caption: 'Enterprise Data Table',
619
+ pagination: true,
620
+ pageSize: 10,
621
+ searchable: true,
622
+ selectable: true,
623
+ sortable: true,
624
+ exportable: true,
625
+ rowActions: true,
626
+ resizableColumns: true,
627
+ reorderableColumns: true,
628
+ columns: [
629
+ { header: 'ID', accessorKey: 'id', width: '80px' },
630
+ { header: 'Name', accessorKey: 'name' },
631
+ { header: 'Email', accessorKey: 'email' },
632
+ { header: 'Status', accessorKey: 'status' },
633
+ { header: 'Role', accessorKey: 'role' },
634
+ ],
635
+ data: [
636
+ { id: 1, name: 'John Doe', email: 'john@example.com', status: 'Active', role: 'Admin' },
637
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'Active', role: 'User' },
638
+ { id: 3, name: 'Bob Johnson', email: 'bob@example.com', status: 'Inactive', role: 'User' },
639
+ { id: 4, name: 'Alice Williams', email: 'alice@example.com', status: 'Active', role: 'Manager' },
640
+ { id: 5, name: 'Charlie Brown', email: 'charlie@example.com', status: 'Active', role: 'User' },
641
+ { id: 6, name: 'Diana Prince', email: 'diana@example.com', status: 'Active', role: 'Admin' },
642
+ { id: 7, name: 'Ethan Hunt', email: 'ethan@example.com', status: 'Inactive', role: 'User' },
643
+ { id: 8, name: 'Fiona Gallagher', email: 'fiona@example.com', status: 'Active', role: 'User' },
644
+ { id: 9, name: 'George Wilson', email: 'george@example.com', status: 'Active', role: 'Manager' },
645
+ { id: 10, name: 'Hannah Montana', email: 'hannah@example.com', status: 'Active', role: 'User' },
646
+ { id: 11, name: 'Ivan Drago', email: 'ivan@example.com', status: 'Inactive', role: 'User' },
647
+ { id: 12, name: 'Julia Roberts', email: 'julia@example.com', status: 'Active', role: 'Admin' },
648
+ ],
649
+ },
650
+ });