@object-ui/components 0.3.1 → 2.0.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 (326) hide show
  1. package/.turbo/turbo-build.log +34 -0
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +13 -0
  4. package/dist/index.css +1 -1
  5. package/dist/index.js +36430 -25529
  6. package/dist/index.umd.cjs +53 -32
  7. package/dist/src/SchemaRenderer.d.ts +3 -0
  8. package/dist/src/custom/action-param-dialog.d.ts +21 -0
  9. package/dist/src/{ui → custom}/button-group.d.ts +1 -1
  10. package/dist/src/custom/field.d.ts +19 -0
  11. package/dist/src/custom/index.d.ts +14 -0
  12. package/dist/src/custom/input-group.d.ts +14 -0
  13. package/dist/src/{ui → custom}/item.d.ts +1 -1
  14. package/dist/src/custom/native-select.d.ts +12 -0
  15. package/dist/src/custom/navigation-overlay.d.ts +50 -0
  16. package/dist/src/custom/sort-builder.d.ts +22 -0
  17. package/dist/src/index.d.ts +2 -0
  18. package/dist/src/renderers/action/action-button.d.ts +11 -0
  19. package/dist/src/renderers/action/action-group.d.ts +25 -0
  20. package/dist/src/renderers/action/action-icon.d.ts +10 -0
  21. package/dist/src/renderers/action/action-menu.d.ts +19 -0
  22. package/dist/src/renderers/action/index.d.ts +0 -0
  23. package/dist/src/renderers/action/resolve-icon.d.ts +6 -0
  24. package/dist/src/renderers/data-display/table.d.ts +1 -1
  25. package/dist/src/renderers/layout/page.d.ts +1 -1
  26. package/dist/src/renderers/placeholders.d.ts +1 -1
  27. package/dist/src/ui/accordion.d.ts +4 -4
  28. package/dist/src/ui/alert-dialog.d.ts +17 -11
  29. package/dist/src/ui/alert.d.ts +4 -5
  30. package/dist/src/ui/aspect-ratio.d.ts +1 -1
  31. package/dist/src/ui/avatar.d.ts +3 -3
  32. package/dist/src/ui/badge.d.ts +3 -3
  33. package/dist/src/ui/breadcrumb.d.ts +16 -8
  34. package/dist/src/ui/calendar.d.ts +7 -7
  35. package/dist/src/ui/card.d.ts +7 -8
  36. package/dist/src/ui/carousel.d.ts +5 -6
  37. package/dist/src/ui/chart.d.ts +62 -0
  38. package/dist/src/ui/checkbox.d.ts +1 -1
  39. package/dist/src/ui/collapsible.d.ts +3 -3
  40. package/dist/src/ui/command.d.ts +78 -16
  41. package/dist/src/ui/context-menu.d.ts +14 -12
  42. package/dist/src/ui/dialog.d.ts +17 -13
  43. package/dist/src/ui/drawer.d.ts +19 -10
  44. package/dist/src/ui/dropdown-menu.d.ts +20 -18
  45. package/dist/src/ui/form.d.ts +6 -7
  46. package/dist/src/ui/hover-card.d.ts +3 -3
  47. package/dist/src/ui/index.d.ts +2 -8
  48. package/dist/src/ui/input-otp.d.ts +30 -7
  49. package/dist/src/ui/label.d.ts +2 -1
  50. package/dist/src/ui/menubar.d.ts +19 -17
  51. package/dist/src/ui/navigation-menu.d.ts +9 -11
  52. package/dist/src/ui/pagination.d.ts +25 -10
  53. package/dist/src/ui/popover.d.ts +4 -5
  54. package/dist/src/ui/progress.d.ts +1 -1
  55. package/dist/src/ui/radio-group.d.ts +2 -2
  56. package/dist/src/ui/resizable.d.ts +5 -8
  57. package/dist/src/ui/scroll-area.d.ts +2 -2
  58. package/dist/src/ui/select.d.ts +11 -13
  59. package/dist/src/ui/sheet.d.ts +23 -11
  60. package/dist/src/ui/sidebar.d.ts +27 -29
  61. package/dist/src/ui/skeleton.d.ts +1 -1
  62. package/dist/src/ui/slider.d.ts +1 -1
  63. package/dist/src/ui/sonner.d.ts +2 -1
  64. package/dist/src/ui/switch.d.ts +2 -2
  65. package/dist/src/ui/tabs.d.ts +1 -1
  66. package/dist/src/ui/textarea.d.ts +1 -1
  67. package/dist/src/ui/toast.d.ts +22 -0
  68. package/dist/src/ui/toggle-group.d.ts +8 -3
  69. package/dist/src/ui/toggle.d.ts +4 -1
  70. package/dist/src/ui/tooltip.d.ts +4 -4
  71. package/dist/src/ui/typography.d.ts +21 -0
  72. package/package.json +20 -9
  73. package/shadcn-components.json +52 -47
  74. package/src/SchemaRenderer.tsx +28 -0
  75. package/src/__tests__/PageRendererRegions.test.tsx +668 -0
  76. package/src/__tests__/Registry.test.ts +21 -0
  77. package/src/__tests__/basic-renderers.test.tsx +1 -1
  78. package/src/__tests__/complex-disclosure-renderers.test.tsx +3 -2
  79. package/src/__tests__/compliance.test.tsx +72 -0
  80. package/src/__tests__/feedback-overlay-renderers.test.tsx +1 -1
  81. package/src/__tests__/form-renderers.test.tsx +1 -1
  82. package/src/__tests__/layout-data-renderers.test.tsx +1 -1
  83. package/src/__tests__/navigation-overlay.test.tsx +273 -0
  84. package/src/__tests__/view-compliance.test.tsx +153 -0
  85. package/src/custom/action-param-dialog.tsx +264 -0
  86. package/src/{ui → custom}/button-group.tsx +1 -1
  87. package/src/{ui → custom}/combobox.tsx +3 -3
  88. package/src/{ui → custom}/date-picker.tsx +3 -3
  89. package/src/custom/field.tsx +81 -0
  90. package/src/{ui → custom}/filter-builder.tsx +3 -3
  91. package/src/custom/index.ts +14 -0
  92. package/src/custom/input-group.tsx +53 -0
  93. package/src/{ui → custom}/item.tsx +1 -1
  94. package/src/custom/native-select.tsx +33 -0
  95. package/src/custom/navigation-overlay.tsx +296 -0
  96. package/src/custom/sort-builder.tsx +129 -0
  97. package/src/index.css +20 -1
  98. package/src/index.ts +2 -0
  99. package/src/renderers/action/action-button.tsx +147 -0
  100. package/src/renderers/action/action-group.tsx +270 -0
  101. package/src/renderers/action/action-icon.tsx +150 -0
  102. package/src/renderers/action/action-menu.tsx +203 -0
  103. package/src/renderers/action/index.ts +18 -0
  104. package/src/renderers/action/resolve-icon.ts +35 -0
  105. package/src/renderers/basic/button-group.tsx +1 -0
  106. package/src/renderers/basic/div.tsx +12 -1
  107. package/src/renderers/basic/html.tsx +1 -0
  108. package/src/renderers/basic/icon.tsx +1 -0
  109. package/src/renderers/basic/image.tsx +1 -0
  110. package/src/renderers/basic/navigation-menu.tsx +1 -0
  111. package/src/renderers/basic/pagination.tsx +31 -4
  112. package/src/renderers/basic/separator.tsx +1 -0
  113. package/src/renderers/basic/span.tsx +12 -1
  114. package/src/renderers/basic/text.tsx +4 -2
  115. package/src/renderers/complex/__tests__/data-table-batch-editing.test.tsx +275 -0
  116. package/src/renderers/complex/__tests__/data-table-cell-renderer.test.tsx +120 -0
  117. package/src/renderers/complex/__tests__/data-table-editing.test.tsx +221 -0
  118. package/src/renderers/complex/carousel.tsx +1 -0
  119. package/src/renderers/complex/data-table.tsx +355 -95
  120. package/src/renderers/complex/filter-builder.tsx +2 -1
  121. package/src/renderers/complex/resizable.tsx +2 -1
  122. package/src/renderers/complex/scroll-area.tsx +25 -7
  123. package/src/renderers/complex/table.tsx +1 -0
  124. package/src/renderers/data-display/alert.tsx +1 -0
  125. package/src/renderers/data-display/avatar.tsx +1 -0
  126. package/src/renderers/data-display/badge.tsx +1 -0
  127. package/src/renderers/data-display/breadcrumb.tsx +1 -0
  128. package/src/renderers/data-display/kbd.tsx +1 -0
  129. package/src/renderers/data-display/list.tsx +21 -49
  130. package/src/renderers/data-display/statistic.tsx +21 -5
  131. package/src/renderers/data-display/table.tsx +21 -11
  132. package/src/renderers/data-display/tree-view.tsx +7 -1
  133. package/src/renderers/disclosure/accordion.tsx +1 -0
  134. package/src/renderers/disclosure/collapsible.tsx +1 -0
  135. package/src/renderers/disclosure/toggle-group.tsx +2 -0
  136. package/src/renderers/feedback/empty.tsx +1 -0
  137. package/src/renderers/feedback/loading.tsx +2 -1
  138. package/src/renderers/feedback/progress.tsx +1 -0
  139. package/src/renderers/feedback/skeleton.tsx +1 -0
  140. package/src/renderers/feedback/sonner.tsx +1 -0
  141. package/src/renderers/feedback/spinner.tsx +1 -0
  142. package/src/renderers/feedback/toast.tsx +1 -0
  143. package/src/renderers/feedback/toaster.tsx +1 -0
  144. package/src/renderers/form/button.tsx +35 -1
  145. package/src/renderers/form/calendar.tsx +1 -0
  146. package/src/renderers/form/checkbox.tsx +38 -16
  147. package/src/renderers/form/combobox.tsx +2 -1
  148. package/src/renderers/form/command.tsx +1 -0
  149. package/src/renderers/form/date-picker.tsx +1 -0
  150. package/src/renderers/form/file-upload.tsx +1 -0
  151. package/src/renderers/form/form.tsx +115 -19
  152. package/src/renderers/form/input-otp.tsx +1 -0
  153. package/src/renderers/form/input.tsx +3 -0
  154. package/src/renderers/form/label.tsx +1 -0
  155. package/src/renderers/form/radio-group.tsx +1 -0
  156. package/src/renderers/form/select.tsx +35 -15
  157. package/src/renderers/form/slider.tsx +1 -0
  158. package/src/renderers/form/switch.tsx +1 -0
  159. package/src/renderers/form/textarea.tsx +50 -27
  160. package/src/renderers/form/toggle.tsx +3 -45
  161. package/src/renderers/index.ts +1 -0
  162. package/src/renderers/layout/aspect-ratio.tsx +2 -1
  163. package/src/renderers/layout/card.tsx +10 -2
  164. package/src/renderers/layout/container.tsx +1 -0
  165. package/src/renderers/layout/flex.tsx +1 -0
  166. package/src/renderers/layout/grid.tsx +23 -8
  167. package/src/renderers/layout/page.tsx +433 -57
  168. package/src/renderers/layout/semantic.tsx +1 -0
  169. package/src/renderers/layout/stack.tsx +2 -1
  170. package/src/renderers/layout/tabs.tsx +43 -17
  171. package/src/renderers/navigation/header-bar.tsx +1 -0
  172. package/src/renderers/navigation/sidebar.tsx +11 -0
  173. package/src/renderers/overlay/alert-dialog.tsx +1 -0
  174. package/src/renderers/overlay/context-menu.tsx +1 -0
  175. package/src/renderers/overlay/dialog.tsx +1 -0
  176. package/src/renderers/overlay/drawer.tsx +1 -0
  177. package/src/renderers/overlay/dropdown-menu.tsx +1 -0
  178. package/src/renderers/overlay/hover-card.tsx +1 -0
  179. package/src/renderers/overlay/menubar.tsx +1 -0
  180. package/src/renderers/overlay/popover.tsx +1 -0
  181. package/src/renderers/overlay/sheet.tsx +1 -0
  182. package/src/renderers/overlay/tooltip.tsx +1 -0
  183. package/src/renderers/placeholders.tsx +4 -4
  184. package/src/stories/CRMApp.stories.tsx +706 -0
  185. package/src/stories/Guide.mdx +55 -0
  186. package/src/stories/Introduction.mdx +61 -0
  187. package/src/stories/MockedData.stories.tsx +121 -0
  188. package/src/stories/assets/accessibility.png +0 -0
  189. package/src/stories/assets/accessibility.svg +1 -0
  190. package/src/stories/assets/addon-library.png +0 -0
  191. package/src/stories/assets/assets.png +0 -0
  192. package/src/stories/assets/avif-test-image.avif +0 -0
  193. package/src/stories/assets/context.png +0 -0
  194. package/src/stories/assets/discord.svg +1 -0
  195. package/src/stories/assets/docs.png +0 -0
  196. package/src/stories/assets/figma-plugin.png +0 -0
  197. package/src/stories/assets/github.svg +1 -0
  198. package/src/stories/assets/share.png +0 -0
  199. package/src/stories/assets/styling.png +0 -0
  200. package/src/stories/assets/testing.png +0 -0
  201. package/src/stories/assets/theming.png +0 -0
  202. package/src/stories/assets/tutorials.svg +1 -0
  203. package/src/stories/assets/youtube.svg +1 -0
  204. package/src/stories/button.css +30 -0
  205. package/src/stories/header.css +32 -0
  206. package/src/stories/page.css +68 -0
  207. package/src/stories-json/accordion.stories.tsx +43 -0
  208. package/src/stories-json/aggrid.stories.tsx +103 -0
  209. package/src/stories-json/alert.stories.tsx +39 -0
  210. package/src/stories-json/aspect-ratio.stories.tsx +34 -0
  211. package/src/stories-json/avatar.stories.tsx +38 -0
  212. package/src/stories-json/badge.stories.tsx +53 -0
  213. package/src/stories-json/breadcrumb.stories.tsx +30 -0
  214. package/src/stories-json/button-group.stories.tsx +43 -0
  215. package/src/stories-json/button.stories.tsx +73 -0
  216. package/src/stories-json/calendar.stories.tsx +85 -0
  217. package/src/stories-json/card.stories.tsx +48 -0
  218. package/src/stories-json/carousel.stories.tsx +33 -0
  219. package/src/stories-json/charts.stories.tsx +195 -0
  220. package/src/stories-json/chatbot.stories.tsx +248 -0
  221. package/src/stories-json/code-editor.stories.tsx +92 -0
  222. package/src/stories-json/collapsible.stories.tsx +40 -0
  223. package/src/stories-json/controls.stories.tsx +36 -0
  224. package/src/stories-json/crm-live-data.stories.tsx +154 -0
  225. package/src/stories-json/dashboard.stories.tsx +318 -0
  226. package/src/stories-json/data-table.stories.tsx +136 -0
  227. package/src/stories-json/data_display_extras.stories.tsx +102 -0
  228. package/src/stories-json/date-picker.stories.tsx +28 -0
  229. package/src/stories-json/detail-view.stories.tsx +258 -0
  230. package/src/stories-json/dialog.stories.tsx +43 -0
  231. package/src/stories-json/feedback_extras.stories.tsx +40 -0
  232. package/src/stories-json/feedback_others.stories.tsx +46 -0
  233. package/src/stories-json/form-variants.stories.tsx +210 -0
  234. package/src/stories-json/form_advanced.stories.tsx +117 -0
  235. package/src/stories-json/form_extras.stories.tsx +123 -0
  236. package/src/stories-json/grid.stories.tsx +56 -0
  237. package/src/stories-json/icon.stories.tsx +36 -0
  238. package/src/stories-json/input.stories.tsx +52 -0
  239. package/src/stories-json/kanban.stories.tsx +295 -0
  240. package/src/stories-json/layout_extended.stories.tsx +76 -0
  241. package/src/stories-json/layout_flex.stories.tsx +107 -0
  242. package/src/stories-json/list-view.stories.tsx +97 -0
  243. package/src/stories-json/markdown.stories.tsx +129 -0
  244. package/src/stories-json/menus.stories.tsx +63 -0
  245. package/src/stories-json/metric-card.stories.tsx +143 -0
  246. package/src/stories-json/navigation-menu.stories.tsx +37 -0
  247. package/src/stories-json/object-aggrid-advanced.stories.tsx +389 -0
  248. package/src/stories-json/object-aggrid.stories.tsx +252 -0
  249. package/src/stories-json/object-form.stories.tsx +130 -0
  250. package/src/stories-json/object-gantt.stories.tsx +114 -0
  251. package/src/stories-json/object-grid.stories.tsx +315 -0
  252. package/src/stories-json/object-map.stories.tsx +116 -0
  253. package/src/stories-json/object-view.stories.tsx +118 -0
  254. package/src/stories-json/overlay_extras.stories.tsx +113 -0
  255. package/src/stories-json/overlay_others.stories.tsx +76 -0
  256. package/src/stories-json/page.stories.tsx +55 -0
  257. package/src/stories-json/reports.stories.tsx +163 -0
  258. package/src/stories-json/resizable.stories.tsx +44 -0
  259. package/src/stories-json/select.stories.tsx +34 -0
  260. package/src/stories-json/separator.stories.tsx +41 -0
  261. package/src/stories-json/sidebar.stories.tsx +147 -0
  262. package/src/stories-json/statistic.stories.tsx +44 -0
  263. package/src/stories-json/tabs.stories.tsx +51 -0
  264. package/src/stories-json/timeline.stories.tsx +188 -0
  265. package/src/stories-json/typography.stories.tsx +45 -0
  266. package/src/ui/accordion.tsx +47 -53
  267. package/src/ui/alert-dialog.tsx +103 -117
  268. package/src/ui/alert.tsx +35 -36
  269. package/src/ui/aspect-ratio.tsx +1 -5
  270. package/src/ui/avatar.tsx +41 -42
  271. package/src/ui/badge.tsx +6 -15
  272. package/src/ui/breadcrumb.tsx +81 -75
  273. package/src/ui/button.tsx +10 -11
  274. package/src/ui/calendar.tsx +178 -51
  275. package/src/ui/card.tsx +51 -110
  276. package/src/ui/carousel.tsx +136 -113
  277. package/src/ui/chart.tsx +367 -0
  278. package/src/ui/checkbox.tsx +20 -22
  279. package/src/ui/collapsible.tsx +5 -25
  280. package/src/ui/command.tsx +106 -135
  281. package/src/ui/context-menu.tsx +69 -116
  282. package/src/ui/dialog.tsx +94 -113
  283. package/src/ui/drawer.tsx +82 -99
  284. package/src/ui/dropdown-menu.tsx +134 -188
  285. package/src/ui/form.tsx +51 -40
  286. package/src/ui/hover-card.tsx +18 -33
  287. package/src/ui/index.ts +2 -8
  288. package/src/ui/input-otp.tsx +42 -52
  289. package/src/ui/input.tsx +13 -15
  290. package/src/ui/label.tsx +17 -15
  291. package/src/ui/menubar.tsx +188 -206
  292. package/src/ui/navigation-menu.tsx +96 -136
  293. package/src/ui/pagination.tsx +86 -96
  294. package/src/ui/popover.tsx +24 -41
  295. package/src/ui/progress.tsx +21 -22
  296. package/src/ui/radio-group.tsx +19 -20
  297. package/src/ui/resizable.tsx +32 -42
  298. package/src/ui/scroll-area.tsx +38 -48
  299. package/src/ui/select.tsx +129 -157
  300. package/src/ui/separator.tsx +2 -2
  301. package/src/ui/sheet.tsx +110 -107
  302. package/src/ui/sidebar.tsx +442 -408
  303. package/src/ui/skeleton.tsx +6 -11
  304. package/src/ui/slider.tsx +19 -54
  305. package/src/ui/sonner.tsx +19 -1
  306. package/src/ui/switch.tsx +19 -21
  307. package/src/ui/tabs.tsx +6 -37
  308. package/src/ui/textarea.tsx +8 -4
  309. package/src/ui/toast.tsx +137 -0
  310. package/src/ui/toggle-group.tsx +28 -37
  311. package/src/ui/toggle.tsx +19 -19
  312. package/src/ui/tooltip.tsx +21 -52
  313. package/src/ui/typography.tsx +85 -0
  314. package/tsconfig.json +1 -1
  315. package/vite.config.ts +9 -1
  316. package/vitest.config.ts +5 -0
  317. package/ISSUES_FOUND.md +0 -128
  318. /package/dist/src/{ui → custom}/combobox.d.ts +0 -0
  319. /package/dist/src/{ui → custom}/date-picker.d.ts +0 -0
  320. /package/dist/src/{ui → custom}/empty.d.ts +0 -0
  321. /package/dist/src/{ui → custom}/filter-builder.d.ts +0 -0
  322. /package/dist/src/{ui → custom}/kbd.d.ts +0 -0
  323. /package/dist/src/{ui → custom}/spinner.d.ts +0 -0
  324. /package/src/{ui → custom}/empty.tsx +0 -0
  325. /package/src/{ui → custom}/kbd.tsx +0 -0
  326. /package/src/{ui → custom}/spinner.tsx +0 -0
@@ -8,6 +8,7 @@
8
8
 
9
9
  // Enterprise-level DataTable Component (Airtable-like)
10
10
  import React, { useState, useMemo, useRef, useEffect } from 'react';
11
+ import { cn } from '../../lib/utils';
11
12
  import { ComponentRegistry } from '@object-ui/core';
12
13
  import type { DataTableSchema } from '@object-ui/types';
13
14
  import {
@@ -43,6 +44,8 @@ import {
43
44
  ChevronsLeft,
44
45
  ChevronsRight,
45
46
  GripVertical,
47
+ Save,
48
+ X,
46
49
  } from 'lucide-react';
47
50
 
48
51
  type SortDirection = 'asc' | 'desc' | null;
@@ -96,6 +99,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
96
99
  rowActions = false,
97
100
  resizableColumns = true,
98
101
  reorderableColumns = true,
102
+ editable = false,
99
103
  className,
100
104
  } = schema;
101
105
 
@@ -119,11 +123,17 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
119
123
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
120
124
  const [draggedColumn, setDraggedColumn] = useState<number | null>(null);
121
125
  const [dragOverColumn, setDragOverColumn] = useState<number | null>(null);
126
+ const [editingCell, setEditingCell] = useState<{ rowIndex: number; columnKey: string } | null>(null);
127
+ const [editValue, setEditValue] = useState<any>('');
128
+ // Track pending changes for multi-cell editing: rowIndex -> { columnKey -> newValue }
129
+ const [pendingChanges, setPendingChanges] = useState<Map<number, Record<string, any>>>(new Map());
130
+ const [isSaving, setIsSaving] = useState(false);
122
131
 
123
132
  // Refs for column resizing
124
133
  const resizingColumn = useRef<string | null>(null);
125
134
  const startX = useRef<number>(0);
126
135
  const startWidth = useRef<number>(0);
136
+ const editInputRef = useRef<HTMLInputElement>(null);
127
137
 
128
138
  // Update columns when schema changes
129
139
  useEffect(() => {
@@ -339,6 +349,141 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
339
349
  setDragOverColumn(null);
340
350
  };
341
351
 
352
+ // Cell editing handlers
353
+ const startEdit = (rowIndex: number, columnKey: string) => {
354
+ if (!editable) return;
355
+
356
+ const column = columns.find(col => col.accessorKey === columnKey);
357
+ if (column?.editable === false) return;
358
+
359
+ setEditingCell({ rowIndex, columnKey });
360
+
361
+ // Check if there's a pending change for this cell, otherwise use current data value
362
+ const rowChanges = pendingChanges.get(rowIndex);
363
+ const currentValue = paginatedData[rowIndex][columnKey];
364
+ const valueToEdit = rowChanges?.[columnKey] ?? currentValue ?? '';
365
+ setEditValue(valueToEdit);
366
+ };
367
+
368
+ const saveEdit = (force: boolean = false) => {
369
+ if (!editingCell) return;
370
+
371
+ // Don't save if we're in cancelled state (unless forced)
372
+ if (!force && editingCell === null) return;
373
+
374
+ const { rowIndex, columnKey } = editingCell;
375
+ const globalIndex = (currentPage - 1) * pageSize + rowIndex;
376
+ const row = sortedData[globalIndex];
377
+
378
+ // Update pending changes
379
+ const newPendingChanges = new Map(pendingChanges);
380
+ const rowChanges = newPendingChanges.get(rowIndex) || {};
381
+ rowChanges[columnKey] = editValue;
382
+ newPendingChanges.set(rowIndex, rowChanges);
383
+ setPendingChanges(newPendingChanges);
384
+
385
+ // Call the legacy onCellChange callback if provided
386
+ if (schema.onCellChange) {
387
+ schema.onCellChange(globalIndex, columnKey, editValue, row);
388
+ }
389
+
390
+ setEditingCell(null);
391
+ setEditValue('');
392
+ };
393
+
394
+ const cancelEdit = () => {
395
+ setEditingCell(null);
396
+ setEditValue('');
397
+ };
398
+
399
+ const saveRow = async (rowIndex: number) => {
400
+ const globalIndex = (currentPage - 1) * pageSize + rowIndex;
401
+ const row = sortedData[globalIndex];
402
+ const rowChanges = pendingChanges.get(rowIndex);
403
+
404
+ if (!rowChanges || Object.keys(rowChanges).length === 0) return;
405
+
406
+ setIsSaving(true);
407
+ try {
408
+ if (schema.onRowSave) {
409
+ await schema.onRowSave(globalIndex, rowChanges, row);
410
+ }
411
+
412
+ // Clear pending changes for this row
413
+ const newPendingChanges = new Map(pendingChanges);
414
+ newPendingChanges.delete(rowIndex);
415
+ setPendingChanges(newPendingChanges);
416
+ } catch (error) {
417
+ console.error('Failed to save row:', error);
418
+ } finally {
419
+ setIsSaving(false);
420
+ }
421
+ };
422
+
423
+ const cancelRowChanges = (rowIndex: number) => {
424
+ const newPendingChanges = new Map(pendingChanges);
425
+ newPendingChanges.delete(rowIndex);
426
+ setPendingChanges(newPendingChanges);
427
+ };
428
+
429
+ const saveBatch = async () => {
430
+ if (pendingChanges.size === 0) return;
431
+
432
+ setIsSaving(true);
433
+ try {
434
+ const changesToSave = Array.from(pendingChanges.entries()).map(([rowIndex, changes]) => {
435
+ const globalIndex = (currentPage - 1) * pageSize + rowIndex;
436
+ const row = sortedData[globalIndex];
437
+ return { rowIndex: globalIndex, changes, row };
438
+ });
439
+
440
+ if (schema.onBatchSave) {
441
+ await schema.onBatchSave(changesToSave);
442
+ }
443
+
444
+ // Clear all pending changes
445
+ setPendingChanges(new Map());
446
+ } catch (error) {
447
+ console.error('Failed to save batch:', error);
448
+ } finally {
449
+ setIsSaving(false);
450
+ }
451
+ };
452
+
453
+ const cancelAllChanges = () => {
454
+ setPendingChanges(new Map());
455
+ };
456
+
457
+ const handleCellKeyDown = (e: React.KeyboardEvent, rowIndex: number, columnKey: string) => {
458
+ if (!editable) return;
459
+
460
+ const column = columns.find(col => col.accessorKey === columnKey);
461
+ if (column?.editable === false) return;
462
+
463
+ if (e.key === 'Enter' && !editingCell) {
464
+ e.preventDefault();
465
+ startEdit(rowIndex, columnKey);
466
+ }
467
+ };
468
+
469
+ const handleEditKeyDown = (e: React.KeyboardEvent) => {
470
+ if (e.key === 'Enter') {
471
+ e.preventDefault();
472
+ saveEdit(true);
473
+ } else if (e.key === 'Escape') {
474
+ e.preventDefault();
475
+ cancelEdit();
476
+ }
477
+ };
478
+
479
+ // Auto-focus on edit input when entering edit mode
480
+ useEffect(() => {
481
+ if (editingCell && editInputRef.current) {
482
+ editInputRef.current.focus();
483
+ editInputRef.current.select();
484
+ }
485
+ }, [editingCell]);
486
+
342
487
  // Cleanup on unmount
343
488
  useEffect(() => {
344
489
  return () => {
@@ -360,55 +505,87 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
360
505
  return selectedRowIds.has(rowId);
361
506
  }) && !allPageRowsSelected;
362
507
 
508
+ const hasPendingChanges = pendingChanges.size > 0;
509
+ const showToolbar = searchable || exportable || (selectable && selectedRowIds.size > 0) || hasPendingChanges;
510
+
363
511
  return (
364
- <div className={`space-y-4 ${className || ''}`}>
512
+ <div className={`flex flex-col h-full gap-4 ${className || ''}`}>
365
513
  {/* Toolbar */}
366
- <div className="flex items-center justify-between gap-4">
367
- <div className="flex items-center gap-2 flex-1">
368
- {searchable && (
369
- <div className="relative max-w-sm flex-1">
370
- <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
371
- <Input
372
- placeholder="Search..."
373
- value={searchQuery}
374
- onChange={(e) => {
375
- setSearchQuery(e.target.value);
376
- setCurrentPage(1);
377
- }}
378
- className="pl-8"
379
- />
380
- </div>
381
- )}
382
- </div>
383
-
384
- <div className="flex items-center gap-2">
385
- {exportable && (
386
- <Button
387
- variant="outline"
388
- size="sm"
389
- onClick={handleExport}
390
- disabled={sortedData.length === 0}
391
- >
392
- <Download className="h-4 w-4 mr-2" />
393
- Export CSV
394
- </Button>
395
- )}
514
+ {showToolbar && (
515
+ <div className="flex items-center justify-between gap-4 flex-none">
516
+ <div className="flex items-center gap-2 flex-1">
517
+ {searchable && (
518
+ <div className="relative max-w-sm flex-1">
519
+ <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
520
+ <Input
521
+ placeholder="Search..."
522
+ value={searchQuery}
523
+ onChange={(e) => {
524
+ setSearchQuery(e.target.value);
525
+ setCurrentPage(1);
526
+ }}
527
+ className="pl-8"
528
+ />
529
+ </div>
530
+ )}
531
+ </div>
396
532
 
397
- {selectable && selectedRowIds.size > 0 && (
398
- <div className="text-sm text-muted-foreground">
399
- {selectedRowIds.size} selected
400
- </div>
401
- )}
533
+ <div className="flex items-center gap-2">
534
+ {hasPendingChanges && (
535
+ <>
536
+ <div className="text-sm text-muted-foreground">
537
+ {pendingChanges.size} row{pendingChanges.size > 1 ? 's' : ''} modified
538
+ </div>
539
+ <Button
540
+ variant="outline"
541
+ size="sm"
542
+ onClick={cancelAllChanges}
543
+ disabled={isSaving}
544
+ >
545
+ <X className="h-4 w-4 mr-2" />
546
+ Cancel All
547
+ </Button>
548
+ <Button
549
+ variant="default"
550
+ size="sm"
551
+ onClick={saveBatch}
552
+ disabled={isSaving}
553
+ >
554
+ <Save className="h-4 w-4 mr-2" />
555
+ Save All ({pendingChanges.size})
556
+ </Button>
557
+ </>
558
+ )}
559
+
560
+ {exportable && (
561
+ <Button
562
+ variant="outline"
563
+ size="sm"
564
+ onClick={handleExport}
565
+ disabled={sortedData.length === 0}
566
+ >
567
+ <Download className="h-4 w-4 mr-2" />
568
+ Export CSV
569
+ </Button>
570
+ )}
571
+
572
+ {selectable && selectedRowIds.size > 0 && (
573
+ <div className="text-sm text-muted-foreground">
574
+ {selectedRowIds.size} selected
575
+ </div>
576
+ )}
577
+ </div>
402
578
  </div>
403
- </div>
579
+ )}
404
580
 
405
581
  {/* Table */}
406
- <Table>
582
+ <div className="rounded-md border flex-1 min-h-0 overflow-auto relative bg-background">
583
+ <Table>
407
584
  {caption && <TableCaption>{caption}</TableCaption>}
408
- <TableHeader>
585
+ <TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
409
586
  <TableRow>
410
587
  {selectable && (
411
- <TableHead className="w-12">
588
+ <TableHead className="w-12 bg-background">
412
589
  <Checkbox
413
590
  checked={allPageRowsSelected ? true : somePageRowsSelected ? 'indeterminate' : false}
414
591
  onCheckedChange={handleSelectAll}
@@ -423,7 +600,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
423
600
  return (
424
601
  <TableHead
425
602
  key={col.accessorKey}
426
- className={`${col.className || ''} ${sortable && col.sortable !== false ? 'cursor-pointer select-none' : ''} ${isDragging ? 'opacity-50' : ''} ${isDragOver ? 'border-l-2 border-primary' : ''} relative group`}
603
+ className={`${col.className || ''} ${sortable && col.sortable !== false ? 'cursor-pointer select-none' : ''} ${isDragging ? 'opacity-50' : ''} ${isDragOver ? 'border-l-2 border-primary' : ''} relative group bg-background`}
427
604
  style={{
428
605
  width: columnWidth,
429
606
  minWidth: columnWidth
@@ -455,7 +632,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
455
632
  );
456
633
  })}
457
634
  {rowActions && (
458
- <TableHead className="w-24 text-right">Actions</TableHead>
635
+ <TableHead className="w-24 text-right bg-background">Actions</TableHead>
459
636
  )}
460
637
  </TableRow>
461
638
  </TableHeader>
@@ -464,69 +641,151 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
464
641
  <TableRow>
465
642
  <TableCell
466
643
  colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)}
467
- className="h-24 text-center text-muted-foreground"
644
+ className="h-96 text-center text-muted-foreground"
468
645
  >
469
- No data available
646
+ <div className="flex flex-col items-center justify-center gap-2">
647
+ <Search className="h-8 w-8 text-muted-foreground/50" />
648
+ <p>No results found</p>
649
+ <p className="text-xs text-muted-foreground/50">Try adjusting your filters or search query.</p>
650
+ </div>
470
651
  </TableCell>
471
652
  </TableRow>
472
653
  ) : (
473
- paginatedData.map((row, rowIndex) => {
474
- const globalIndex = (currentPage - 1) * pageSize + rowIndex;
475
- const rowId = getRowId(row, globalIndex);
476
- const isSelected = selectedRowIds.has(rowId);
477
-
478
- return (
479
- <TableRow key={rowId} data-state={isSelected ? 'selected' : undefined}>
480
- {selectable && (
481
- <TableCell>
482
- <Checkbox
483
- checked={isSelected}
484
- onCheckedChange={(checked) => handleSelectRow(rowId, checked as boolean)}
485
- />
486
- </TableCell>
487
- )}
488
- {columns.map((col, colIndex) => {
489
- const columnWidth = columnWidths[col.accessorKey] || col.width;
490
- return (
491
- <TableCell
492
- key={colIndex}
493
- className={col.cellClassName}
494
- style={{
495
- width: columnWidth,
496
- minWidth: columnWidth,
497
- maxWidth: columnWidth
498
- }}
499
- >
500
- {row[col.accessorKey]}
654
+ <>
655
+ {paginatedData.map((row, rowIndex) => {
656
+ const globalIndex = (currentPage - 1) * pageSize + rowIndex;
657
+ const rowId = getRowId(row, globalIndex);
658
+ const isSelected = selectedRowIds.has(rowId);
659
+ const rowHasChanges = pendingChanges.has(rowIndex);
660
+ const rowChanges = pendingChanges.get(rowIndex) || {};
661
+
662
+ return (
663
+ <TableRow
664
+ key={rowId}
665
+ data-state={isSelected ? 'selected' : undefined}
666
+ className={cn(
667
+ schema.onRowClick && "cursor-pointer",
668
+ rowHasChanges && "bg-amber-50 dark:bg-amber-950/20"
669
+ )}
670
+ onClick={(e) => {
671
+ if (schema.onRowClick && !e.defaultPrevented) {
672
+ // Simple heuristic to avoid triggering on interactive elements if they didn't stop propagation
673
+ const target = e.target as HTMLElement;
674
+ if (target.closest('button') || target.closest('[role="checkbox"]') || target.closest('a')) {
675
+ return;
676
+ }
677
+ schema.onRowClick(row);
678
+ }
679
+ }}
680
+ >
681
+ {selectable && (
682
+ <TableCell>
683
+ <Checkbox
684
+ checked={isSelected}
685
+ onCheckedChange={(checked) => handleSelectRow(rowId, checked as boolean)}
686
+ />
501
687
  </TableCell>
502
- );
503
- })}
504
- {rowActions && (
505
- <TableCell className="text-right">
506
- <div className="flex items-center justify-end gap-1">
507
- <Button
508
- variant="ghost"
509
- size="icon-sm"
510
- onClick={() => schema.onRowEdit?.(row)}
511
- >
512
- <Edit className="h-4 w-4" />
513
- </Button>
514
- <Button
515
- variant="ghost"
516
- size="icon-sm"
517
- onClick={() => schema.onRowDelete?.(row)}
688
+ )}
689
+ {columns.map((col, colIndex) => {
690
+ const columnWidth = columnWidths[col.accessorKey] || col.width;
691
+ const originalValue = row[col.accessorKey];
692
+ const hasPendingChange = rowChanges[col.accessorKey] !== undefined;
693
+ const cellValue = hasPendingChange ? rowChanges[col.accessorKey] : originalValue;
694
+ const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.columnKey === col.accessorKey;
695
+ const isEditable = editable && col.editable !== false;
696
+
697
+ return (
698
+ <TableCell
699
+ key={colIndex}
700
+ className={cn(
701
+ col.cellClassName,
702
+ isEditable && !isEditing && "cursor-text hover:bg-muted/50",
703
+ hasPendingChange && "font-semibold text-amber-700 dark:text-amber-400"
704
+ )}
705
+ style={{
706
+ width: columnWidth,
707
+ minWidth: columnWidth,
708
+ maxWidth: columnWidth
709
+ }}
710
+ onDoubleClick={() => isEditable && startEdit(rowIndex, col.accessorKey)}
711
+ onKeyDown={(e) => handleCellKeyDown(e, rowIndex, col.accessorKey)}
712
+ tabIndex={0}
518
713
  >
519
- <Trash2 className="h-4 w-4 text-destructive" />
520
- </Button>
521
- </div>
522
- </TableCell>
523
- )}
714
+ {isEditing ? (
715
+ <Input
716
+ ref={editInputRef}
717
+ value={editValue}
718
+ onChange={(e) => setEditValue(e.target.value)}
719
+ onKeyDown={handleEditKeyDown}
720
+ className="h-8 px-2 py-1"
721
+ />
722
+ ) : typeof col.cell === 'function' ? (
723
+ col.cell(cellValue, row)
724
+ ) : (
725
+ cellValue
726
+ )}
727
+ </TableCell>
728
+ );
729
+ })}
730
+ {rowActions && (
731
+ <TableCell className="text-right">
732
+ <div className="flex items-center justify-end gap-1">
733
+ {rowHasChanges && (schema.onRowSave || schema.onBatchSave) ? (
734
+ <>
735
+ <Button
736
+ variant="ghost"
737
+ size="icon-sm"
738
+ onClick={() => cancelRowChanges(rowIndex)}
739
+ disabled={isSaving}
740
+ title="Cancel changes"
741
+ >
742
+ <X className="h-4 w-4" />
743
+ </Button>
744
+ <Button
745
+ variant="ghost"
746
+ size="icon-sm"
747
+ onClick={() => saveRow(rowIndex)}
748
+ disabled={isSaving}
749
+ title="Save row"
750
+ >
751
+ <Save className="h-4 w-4 text-green-600" />
752
+ </Button>
753
+ </>
754
+ ) : (
755
+ <>
756
+ <Button
757
+ variant="ghost"
758
+ size="icon-sm"
759
+ onClick={() => schema.onRowEdit?.(row)}
760
+ >
761
+ <Edit className="h-4 w-4" />
762
+ </Button>
763
+ <Button
764
+ variant="ghost"
765
+ size="icon-sm"
766
+ onClick={() => schema.onRowDelete?.(row)}
767
+ >
768
+ <Trash2 className="h-4 w-4 text-destructive" />
769
+ </Button>
770
+ </>
771
+ )}
772
+ </div>
773
+ </TableCell>
774
+ )}
775
+ </TableRow>
776
+ );
777
+ })}
778
+ {/* Filler rows to maintain height consistency */}
779
+ {paginatedData.length > 0 && Array.from({ length: Math.max(0, pageSize - paginatedData.length) }).map((_, i) => (
780
+ <TableRow key={`empty-${i}`} className="hover:bg-transparent">
781
+ <TableCell colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} className="h-[52px] p-0" />
524
782
  </TableRow>
525
- );
526
- })
783
+ ))}
784
+ </>
527
785
  )}
528
786
  </TableBody>
529
787
  </Table>
788
+ </div>
530
789
 
531
790
  {/* Pagination */}
532
791
  {pagination && sortedData.length > 0 && (
@@ -600,6 +859,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
600
859
 
601
860
  // Register the component
602
861
  ComponentRegistry.register('data-table', DataTableRenderer, {
862
+ namespace: 'ui',
603
863
  label: 'Data Table',
604
864
  icon: 'table',
605
865
  inputs: [
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { ComponentRegistry } from '@object-ui/core';
10
10
  import type { FilterBuilderSchema, FilterGroup } from '@object-ui/types';
11
- import { FilterBuilder } from '../../ui/filter-builder';
11
+ import { FilterBuilder } from '../../custom/filter-builder';
12
12
 
13
13
  ComponentRegistry.register('filter-builder',
14
14
  ({ schema, className, onChange, ...props }: { schema: FilterBuilderSchema; className?: string; onChange?: (event: any) => void; [key: string]: any }) => {
@@ -39,6 +39,7 @@ ComponentRegistry.register('filter-builder',
39
39
  );
40
40
  },
41
41
  {
42
+ namespace: 'ui',
42
43
  label: 'Filter Builder',
43
44
  inputs: [
44
45
  { name: 'label', type: 'string', label: 'Label' },
@@ -19,7 +19,7 @@ import { renderChildren } from '../../lib/utils';
19
19
  ComponentRegistry.register('resizable',
20
20
  ({ schema, className, ...props }: { schema: ResizableSchema; className?: string; [key: string]: any }) => (
21
21
  <ResizablePanelGroup
22
- direction={schema.direction || 'horizontal'}
22
+ orientation={(schema.direction || 'horizontal') as "horizontal" | "vertical"}
23
23
  className={className}
24
24
  {...props}
25
25
  style={{ minHeight: schema.minHeight || '200px' }}
@@ -35,6 +35,7 @@ ComponentRegistry.register('resizable',
35
35
  </ResizablePanelGroup>
36
36
  ),
37
37
  {
38
+ namespace: 'ui',
38
39
  label: 'Resizable Panel Group',
39
40
  inputs: [
40
41
  { name: 'direction', type: 'enum', enum: ['horizontal', 'vertical'], defaultValue: 'horizontal', label: 'Direction' },
@@ -12,26 +12,44 @@ import { ScrollArea, ScrollBar } from '../../ui';
12
12
  import { renderChildren } from '../../lib/utils';
13
13
 
14
14
  ComponentRegistry.register('scroll-area',
15
- ({ schema, className, ...props }: { schema: ScrollAreaSchema; className?: string; [key: string]: any }) => (
16
- <ScrollArea className={className} style={{ height: schema.height, width: schema.width }} {...props}>
17
- {renderChildren(schema.content || schema.children)}
18
- {schema.orientation === 'horizontal' && <ScrollBar orientation="horizontal" />}
15
+ ({ schema, className, ...props }: { schema: ScrollAreaSchema; className?: string; [key: string]: any }) => {
16
+ // Extract designer-related props
17
+ const {
18
+ 'data-obj-id': dataObjId,
19
+ 'data-obj-type': dataObjType,
20
+ style,
21
+ ...scrollAreaProps
22
+ } = props;
23
+
24
+ const orientation = schema.orientation || 'vertical';
25
+
26
+ return (
27
+ <ScrollArea
28
+ className={className}
29
+ style={{ height: schema.height, width: schema.width, ...style }}
30
+ {...scrollAreaProps}
31
+ data-obj-id={dataObjId}
32
+ data-obj-type={dataObjType}
33
+ >
34
+ {renderChildren(schema.children)}
35
+ {(orientation === 'horizontal' || orientation === 'both') && <ScrollBar orientation="horizontal" />}
36
+ {(orientation === 'vertical' || orientation === 'both') && <ScrollBar orientation="vertical" />}
19
37
  </ScrollArea>
20
- ),
38
+ )},
21
39
  {
40
+ namespace: 'ui',
22
41
  label: 'Scroll Area',
23
42
  inputs: [
24
43
  { name: 'height', type: 'string', label: 'Height (e.g. 200px)' },
25
44
  { name: 'width', type: 'string', label: 'Width' },
26
45
  { name: 'orientation', type: 'enum', enum: ['vertical', 'horizontal', 'both'], defaultValue: 'vertical', label: 'Orientation' },
27
- { name: 'content', type: 'slot', label: 'Content' },
28
46
  { name: 'className', type: 'string', label: 'CSS Class' }
29
47
  ],
30
48
  defaultProps: {
31
49
  height: '200px',
32
50
  width: '100%',
33
51
  orientation: 'vertical',
34
- content: [
52
+ children: [
35
53
  { type: 'div', className: 'p-4', body: [{ type: 'text', content: 'Scrollable content goes here. Add more content to see scrolling behavior.' }] }
36
54
  ],
37
55
  className: 'rounded-md border'
@@ -59,6 +59,7 @@ ComponentRegistry.register('table',
59
59
  </Table>
60
60
  ),
61
61
  {
62
+ namespace: 'ui',
62
63
  label: 'Table',
63
64
  inputs: [
64
65
  { name: 'caption', type: 'string', label: 'Caption' },
@@ -23,6 +23,7 @@ ComponentRegistry.register('alert',
23
23
  </Alert>
24
24
  ),
25
25
  {
26
+ namespace: 'ui',
26
27
  label: 'Alert',
27
28
  inputs: [
28
29
  { name: 'title', type: 'string', label: 'Title', required: true },
@@ -22,6 +22,7 @@ ComponentRegistry.register('avatar',
22
22
  </Avatar>
23
23
  ),
24
24
  {
25
+ namespace: 'ui',
25
26
  label: 'Avatar',
26
27
  inputs: [
27
28
  { name: 'src', type: 'string', label: 'Image URL' },
@@ -34,6 +34,7 @@ ComponentRegistry.register('badge',
34
34
  );
35
35
  },
36
36
  {
37
+ namespace: 'ui',
37
38
  label: 'Badge',
38
39
  inputs: [
39
40
  { name: 'label', type: 'string', label: 'Label' },