@object-ui/components 3.1.5 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +21 -1
- package/dist/index.css +6339 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +31896 -31523
- package/dist/index.umd.cjs +40 -40
- package/dist/{src → packages/components/src}/custom/empty.d.ts +12 -1
- package/dist/{src → packages/components/src}/custom/mobile-dialog-content.d.ts +1 -1
- package/dist/{src → packages/components/src}/renderers/action/action-bar.d.ts +12 -1
- package/dist/{src → packages/components/src}/ui/accordion.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/alert-dialog.d.ts +1 -1
- package/dist/packages/components/src/ui/aspect-ratio.d.ts +10 -0
- package/dist/{src → packages/components/src}/ui/avatar.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/button.d.ts +1 -1
- package/dist/packages/components/src/ui/chart.d.ts +56 -0
- package/dist/{src → packages/components/src}/ui/checkbox.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/collapsible.d.ts +8 -1
- package/dist/{src → packages/components/src}/ui/command.d.ts +2 -2
- package/dist/{src → packages/components/src}/ui/context-menu.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/dialog.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/drawer.d.ts +6 -10
- package/dist/{src → packages/components/src}/ui/dropdown-menu.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/form.d.ts +2 -2
- package/dist/{src → packages/components/src}/ui/hover-card.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/label.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/menubar.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/navigation-menu.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/popover.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/progress.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/radio-group.d.ts +1 -1
- package/dist/packages/components/src/ui/resizable.d.ts +7 -0
- package/dist/{src → packages/components/src}/ui/scroll-area.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/select.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/separator.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/sheet.d.ts +1 -2
- package/dist/{src → packages/components/src}/ui/sidebar.d.ts +1 -8
- package/dist/{src → packages/components/src}/ui/slider.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/switch.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/tabs.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/toast.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/toggle-group.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/toggle.d.ts +1 -1
- package/dist/{src → packages/components/src}/ui/tooltip.d.ts +1 -1
- package/package.json +68 -20
- package/.turbo/turbo-build.log +0 -64
- package/README_SHADCN_SYNC.md +0 -281
- package/TESTING.md +0 -335
- package/dist/src/ui/aspect-ratio.d.ts +0 -3
- package/dist/src/ui/chart.d.ts +0 -68
- package/dist/src/ui/resizable.d.ts +0 -14
- package/docs/FilterBuilder.md +0 -268
- package/metadata/Chart.component.yml +0 -30
- package/metadata/FilterBuilder.component.yml +0 -39
- package/metadata/GridLayout.component.yml +0 -27
- package/metadata/Menu.component.yml +0 -31
- package/metadata/ObjectForm.component.yml +0 -34
- package/metadata/ObjectGrid.component.yml +0 -72
- package/metadata/Page.component.yml +0 -24
- package/postcss.config.js +0 -14
- package/shadcn-components.json +0 -440
- package/src/SchemaRenderer.tsx +0 -28
- package/src/__tests__/PageRendererRegions.test.tsx +0 -668
- package/src/__tests__/README.md +0 -124
- package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +0 -811
- package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +0 -327
- package/src/__tests__/accessibility.test.tsx +0 -137
- package/src/__tests__/action-bar.test.tsx +0 -206
- package/src/__tests__/api-consistency.test.tsx +0 -596
- package/src/__tests__/basic-renderers.test.tsx +0 -255
- package/src/__tests__/color-contrast.test.tsx +0 -212
- package/src/__tests__/complex-disclosure-renderers.test.tsx +0 -302
- package/src/__tests__/compliance.test.tsx +0 -72
- package/src/__tests__/config-field-renderer.test.tsx +0 -307
- package/src/__tests__/config-panel-renderer.test.tsx +0 -580
- package/src/__tests__/config-primitives.test.tsx +0 -106
- package/src/__tests__/edge-cases.test.tsx +0 -285
- package/src/__tests__/feedback-overlay-renderers.test.tsx +0 -349
- package/src/__tests__/filter-builder.test.tsx +0 -409
- package/src/__tests__/form-renderers.test.tsx +0 -364
- package/src/__tests__/layout-data-renderers.test.tsx +0 -340
- package/src/__tests__/mobile-accessibility.test.tsx +0 -120
- package/src/__tests__/navigation-overlay.test.tsx +0 -370
- package/src/__tests__/snapshot-critical.test.tsx +0 -317
- package/src/__tests__/snapshot.test.tsx +0 -205
- package/src/__tests__/test-utils.tsx +0 -190
- package/src/__tests__/use-config-draft.test.tsx +0 -295
- package/src/__tests__/view-compliance.test.tsx +0 -153
- package/src/__tests__/wcag-audit.test.tsx +0 -493
- package/src/custom/action-param-dialog.tsx +0 -264
- package/src/custom/button-group.tsx +0 -91
- package/src/custom/combobox.tsx +0 -104
- package/src/custom/config-field-renderer.tsx +0 -276
- package/src/custom/config-panel-renderer.tsx +0 -306
- package/src/custom/config-row.tsx +0 -50
- package/src/custom/date-picker.tsx +0 -61
- package/src/custom/empty.tsx +0 -112
- package/src/custom/field.tsx +0 -81
- package/src/custom/filter-builder.tsx +0 -418
- package/src/custom/index.ts +0 -21
- package/src/custom/input-group.tsx +0 -53
- package/src/custom/item.tsx +0 -201
- package/src/custom/kbd.tsx +0 -36
- package/src/custom/mobile-dialog-content.tsx +0 -67
- package/src/custom/native-select.tsx +0 -33
- package/src/custom/navigation-overlay.tsx +0 -334
- package/src/custom/section-header.tsx +0 -68
- package/src/custom/sort-builder.tsx +0 -129
- package/src/custom/spinner.tsx +0 -26
- package/src/custom/view-skeleton.tsx +0 -243
- package/src/custom/view-states.tsx +0 -153
- package/src/debug/DebugPanel.tsx +0 -313
- package/src/debug/__tests__/DebugPanel.test.tsx +0 -134
- package/src/debug/index.ts +0 -10
- package/src/hooks/use-config-draft.ts +0 -127
- package/src/hooks/use-mobile.tsx +0 -27
- package/src/index.css +0 -131
- package/src/index.ts +0 -47
- package/src/lib/use-sync-external-store-shim.ts +0 -10
- package/src/lib/use-sync-external-store-with-selector-shim.ts +0 -90
- package/src/lib/utils.tsx +0 -35
- package/src/new-components.test.ts +0 -73
- package/src/renderers/action/action-bar.tsx +0 -221
- package/src/renderers/action/action-button.tsx +0 -158
- package/src/renderers/action/action-group.tsx +0 -270
- package/src/renderers/action/action-icon.tsx +0 -150
- package/src/renderers/action/action-menu.tsx +0 -203
- package/src/renderers/action/index.ts +0 -19
- package/src/renderers/action/resolve-icon.ts +0 -35
- package/src/renderers/basic/button-group.tsx +0 -79
- package/src/renderers/basic/div.tsx +0 -60
- package/src/renderers/basic/html.tsx +0 -43
- package/src/renderers/basic/icon.tsx +0 -89
- package/src/renderers/basic/image.tsx +0 -49
- package/src/renderers/basic/index.ts +0 -18
- package/src/renderers/basic/navigation-menu.tsx +0 -81
- package/src/renderers/basic/pagination.tsx +0 -109
- package/src/renderers/basic/separator.tsx +0 -57
- package/src/renderers/basic/span.tsx +0 -63
- package/src/renderers/basic/text.tsx +0 -52
- package/src/renderers/complex/README-KANBAN.md +0 -208
- package/src/renderers/complex/TIMELINE.md +0 -353
- package/src/renderers/complex/__tests__/data-table-airtable-ux.test.tsx +0 -239
- package/src/renderers/complex/__tests__/data-table-batch-editing.test.tsx +0 -275
- package/src/renderers/complex/__tests__/data-table-cell-renderer.test.tsx +0 -120
- package/src/renderers/complex/__tests__/data-table-editing.test.tsx +0 -221
- package/src/renderers/complex/__tests__/data-table.test.ts +0 -76
- package/src/renderers/complex/carousel.tsx +0 -69
- package/src/renderers/complex/data-table.tsx +0 -1243
- package/src/renderers/complex/filter-builder.tsx +0 -77
- package/src/renderers/complex/index.ts +0 -16
- package/src/renderers/complex/resizable.tsx +0 -66
- package/src/renderers/complex/scroll-area.tsx +0 -58
- package/src/renderers/complex/table.tsx +0 -95
- package/src/renderers/data-display/alert.tsx +0 -46
- package/src/renderers/data-display/avatar.tsx +0 -38
- package/src/renderers/data-display/badge.tsx +0 -55
- package/src/renderers/data-display/breadcrumb.tsx +0 -61
- package/src/renderers/data-display/index.ts +0 -18
- package/src/renderers/data-display/kbd.tsx +0 -50
- package/src/renderers/data-display/list.tsx +0 -75
- package/src/renderers/data-display/statistic.tsx +0 -95
- package/src/renderers/data-display/table.tsx +0 -78
- package/src/renderers/data-display/tree-view.tsx +0 -176
- package/src/renderers/disclosure/accordion.tsx +0 -69
- package/src/renderers/disclosure/collapsible.tsx +0 -53
- package/src/renderers/disclosure/index.ts +0 -11
- package/src/renderers/disclosure/toggle-group.tsx +0 -79
- package/src/renderers/feedback/empty.tsx +0 -49
- package/src/renderers/feedback/index.ts +0 -16
- package/src/renderers/feedback/loading.tsx +0 -78
- package/src/renderers/feedback/progress.tsx +0 -29
- package/src/renderers/feedback/skeleton.tsx +0 -31
- package/src/renderers/feedback/sonner.tsx +0 -56
- package/src/renderers/feedback/spinner.tsx +0 -55
- package/src/renderers/feedback/toast.tsx +0 -59
- package/src/renderers/feedback/toaster.tsx +0 -23
- package/src/renderers/form/button.tsx +0 -103
- package/src/renderers/form/calendar.tsx +0 -34
- package/src/renderers/form/checkbox.tsx +0 -71
- package/src/renderers/form/combobox.tsx +0 -48
- package/src/renderers/form/command.tsx +0 -58
- package/src/renderers/form/date-picker.tsx +0 -84
- package/src/renderers/form/file-upload.tsx +0 -184
- package/src/renderers/form/form.tsx +0 -533
- package/src/renderers/form/index.ts +0 -26
- package/src/renderers/form/input-otp.tsx +0 -51
- package/src/renderers/form/input.tsx +0 -121
- package/src/renderers/form/label.tsx +0 -45
- package/src/renderers/form/radio-group.tsx +0 -63
- package/src/renderers/form/select.tsx +0 -94
- package/src/renderers/form/slider.tsx +0 -61
- package/src/renderers/form/switch.tsx +0 -48
- package/src/renderers/form/textarea.tsx +0 -76
- package/src/renderers/form/toggle.tsx +0 -42
- package/src/renderers/index.ts +0 -18
- package/src/renderers/layout/aspect-ratio.tsx +0 -51
- package/src/renderers/layout/card.tsx +0 -85
- package/src/renderers/layout/container.tsx +0 -122
- package/src/renderers/layout/flex.tsx +0 -132
- package/src/renderers/layout/grid.tsx +0 -178
- package/src/renderers/layout/index.ts +0 -19
- package/src/renderers/layout/page.tsx +0 -466
- package/src/renderers/layout/semantic.tsx +0 -48
- package/src/renderers/layout/stack.tsx +0 -132
- package/src/renderers/layout/tabs.tsx +0 -97
- package/src/renderers/navigation/header-bar.tsx +0 -118
- package/src/renderers/navigation/index.ts +0 -10
- package/src/renderers/navigation/sidebar.tsx +0 -208
- package/src/renderers/overlay/alert-dialog.tsx +0 -72
- package/src/renderers/overlay/context-menu.tsx +0 -100
- package/src/renderers/overlay/dialog.tsx +0 -77
- package/src/renderers/overlay/drawer.tsx +0 -77
- package/src/renderers/overlay/dropdown-menu.tsx +0 -99
- package/src/renderers/overlay/hover-card.tsx +0 -55
- package/src/renderers/overlay/index.ts +0 -18
- package/src/renderers/overlay/menubar.tsx +0 -76
- package/src/renderers/overlay/popover.tsx +0 -56
- package/src/renderers/overlay/sheet.tsx +0 -77
- package/src/renderers/overlay/tooltip.tsx +0 -67
- package/src/renderers/placeholders.tsx +0 -107
- package/src/stories/CRMApp.stories.tsx +0 -706
- package/src/stories/ConfigPanel.stories.tsx +0 -232
- package/src/stories/Guide.mdx +0 -55
- package/src/stories/MockedData.stories.tsx +0 -121
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +0 -1
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +0 -1
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +0 -1
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +0 -1
- package/src/stories/assets/youtube.svg +0 -1
- package/src/stories/button.css +0 -30
- package/src/stories/header.css +0 -32
- package/src/stories/page.css +0 -68
- package/src/stories-json/Accessibility.mdx +0 -297
- package/src/stories-json/EdgeCases.stories.tsx +0 -160
- package/src/stories-json/GettingStarted.mdx +0 -89
- package/src/stories-json/Introduction.mdx +0 -127
- package/src/stories-json/accordion.stories.tsx +0 -43
- package/src/stories-json/aggrid.stories.tsx +0 -103
- package/src/stories-json/alert.stories.tsx +0 -39
- package/src/stories-json/aspect-ratio.stories.tsx +0 -34
- package/src/stories-json/avatar.stories.tsx +0 -38
- package/src/stories-json/badge.stories.tsx +0 -53
- package/src/stories-json/breadcrumb.stories.tsx +0 -30
- package/src/stories-json/button-group.stories.tsx +0 -43
- package/src/stories-json/button.stories.tsx +0 -73
- package/src/stories-json/calendar.stories.tsx +0 -85
- package/src/stories-json/card.stories.tsx +0 -48
- package/src/stories-json/carousel.stories.tsx +0 -33
- package/src/stories-json/charts.stories.tsx +0 -195
- package/src/stories-json/chatbot.stories.tsx +0 -248
- package/src/stories-json/code-editor.stories.tsx +0 -92
- package/src/stories-json/collapsible.stories.tsx +0 -40
- package/src/stories-json/controls.stories.tsx +0 -36
- package/src/stories-json/crm-live-data.stories.tsx +0 -154
- package/src/stories-json/dashboard.stories.tsx +0 -318
- package/src/stories-json/data-table.stories.tsx +0 -136
- package/src/stories-json/data_display_extras.stories.tsx +0 -102
- package/src/stories-json/date-picker.stories.tsx +0 -28
- package/src/stories-json/detail-view.stories.tsx +0 -258
- package/src/stories-json/dialog.stories.tsx +0 -43
- package/src/stories-json/feedback_extras.stories.tsx +0 -40
- package/src/stories-json/feedback_others.stories.tsx +0 -46
- package/src/stories-json/form-variants.stories.tsx +0 -210
- package/src/stories-json/form_advanced.stories.tsx +0 -117
- package/src/stories-json/form_extras.stories.tsx +0 -123
- package/src/stories-json/grid.stories.tsx +0 -56
- package/src/stories-json/icon.stories.tsx +0 -36
- package/src/stories-json/input.stories.tsx +0 -52
- package/src/stories-json/kanban.stories.tsx +0 -295
- package/src/stories-json/layout_extended.stories.tsx +0 -76
- package/src/stories-json/layout_flex.stories.tsx +0 -107
- package/src/stories-json/list-view.stories.tsx +0 -97
- package/src/stories-json/markdown.stories.tsx +0 -129
- package/src/stories-json/menus.stories.tsx +0 -63
- package/src/stories-json/metric-card.stories.tsx +0 -143
- package/src/stories-json/navigation-menu.stories.tsx +0 -37
- package/src/stories-json/object-aggrid-advanced.stories.tsx +0 -389
- package/src/stories-json/object-aggrid.stories.tsx +0 -252
- package/src/stories-json/object-form.stories.tsx +0 -130
- package/src/stories-json/object-gantt.stories.tsx +0 -114
- package/src/stories-json/object-grid.stories.tsx +0 -315
- package/src/stories-json/object-map.stories.tsx +0 -116
- package/src/stories-json/object-view.stories.tsx +0 -118
- package/src/stories-json/overlay_extras.stories.tsx +0 -113
- package/src/stories-json/overlay_others.stories.tsx +0 -76
- package/src/stories-json/page.stories.tsx +0 -55
- package/src/stories-json/reports.stories.tsx +0 -163
- package/src/stories-json/resizable.stories.tsx +0 -44
- package/src/stories-json/select.stories.tsx +0 -34
- package/src/stories-json/separator.stories.tsx +0 -41
- package/src/stories-json/sidebar.stories.tsx +0 -147
- package/src/stories-json/statistic.stories.tsx +0 -44
- package/src/stories-json/tabs.stories.tsx +0 -51
- package/src/stories-json/timeline.stories.tsx +0 -188
- package/src/stories-json/typography.stories.tsx +0 -45
- package/src/types/config-panel.ts +0 -101
- package/src/ui/accordion.tsx +0 -66
- package/src/ui/alert-dialog.tsx +0 -149
- package/src/ui/alert.tsx +0 -67
- package/src/ui/aspect-ratio.tsx +0 -15
- package/src/ui/avatar.tsx +0 -58
- package/src/ui/badge.tsx +0 -44
- package/src/ui/breadcrumb.tsx +0 -123
- package/src/ui/button.tsx +0 -66
- package/src/ui/calendar.tsx +0 -221
- package/src/ui/card.tsx +0 -87
- package/src/ui/carousel.tsx +0 -270
- package/src/ui/chart.tsx +0 -367
- package/src/ui/checkbox.tsx +0 -38
- package/src/ui/collapsible.tsx +0 -19
- package/src/ui/command.tsx +0 -161
- package/src/ui/context-menu.tsx +0 -208
- package/src/ui/dialog.tsx +0 -147
- package/src/ui/drawer.tsx +0 -126
- package/src/ui/dropdown-menu.tsx +0 -208
- package/src/ui/form.tsx +0 -186
- package/src/ui/hover-card.tsx +0 -37
- package/src/ui/index.ts +0 -56
- package/src/ui/input-otp.tsx +0 -79
- package/src/ui/input.tsx +0 -30
- package/src/ui/label.tsx +0 -34
- package/src/ui/menubar.tsx +0 -264
- package/src/ui/navigation-menu.tsx +0 -136
- package/src/ui/pagination.tsx +0 -125
- package/src/ui/popover.tsx +0 -39
- package/src/ui/progress.tsx +0 -36
- package/src/ui/radio-group.tsx +0 -52
- package/src/ui/resizable.tsx +0 -53
- package/src/ui/scroll-area.tsx +0 -56
- package/src/ui/select.tsx +0 -168
- package/src/ui/separator.tsx +0 -39
- package/src/ui/sheet.tsx +0 -151
- package/src/ui/sidebar.tsx +0 -866
- package/src/ui/skeleton.tsx +0 -23
- package/src/ui/slider.tsx +0 -40
- package/src/ui/sonner.tsx +0 -53
- package/src/ui/switch.tsx +0 -37
- package/src/ui/table.tsx +0 -125
- package/src/ui/tabs.tsx +0 -63
- package/src/ui/textarea.tsx +0 -30
- package/src/ui/toast.tsx +0 -137
- package/src/ui/toggle-group.tsx +0 -69
- package/src/ui/toggle.tsx +0 -53
- package/src/ui/tooltip.tsx +0 -38
- package/src/ui/typography.tsx +0 -85
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -70
- package/vitest.config.ts +0 -5
- /package/dist/{src → packages/components/src}/SchemaRenderer.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/action-param-dialog.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/button-group.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/combobox.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/config-field-renderer.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/config-panel-renderer.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/config-row.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/date-picker.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/field.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/filter-builder.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/input-group.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/item.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/kbd.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/native-select.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/navigation-overlay.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/section-header.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/sort-builder.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/spinner.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/view-skeleton.d.ts +0 -0
- /package/dist/{src → packages/components/src}/custom/view-states.d.ts +0 -0
- /package/dist/{src → packages/components/src}/debug/DebugPanel.d.ts +0 -0
- /package/dist/{src → packages/components/src}/debug/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/hooks/use-config-draft.d.ts +0 -0
- /package/dist/{src → packages/components/src}/hooks/use-mobile.d.ts +0 -0
- /package/dist/{src → packages/components/src}/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/lib/use-sync-external-store-shim.d.ts +0 -0
- /package/dist/{src → packages/components/src}/lib/use-sync-external-store-with-selector-shim.d.ts +0 -0
- /package/dist/{src → packages/components/src}/lib/utils.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/action/action-button.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/action/action-group.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/action/action-icon.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/action/action-menu.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/action/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/action/resolve-icon.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/button-group.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/div.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/html.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/icon.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/image.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/navigation-menu.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/pagination.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/separator.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/span.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/basic/text.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/complex/carousel.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/complex/data-table.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/complex/filter-builder.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/complex/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/complex/resizable.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/complex/scroll-area.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/complex/table.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/alert.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/avatar.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/badge.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/breadcrumb.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/kbd.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/list.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/statistic.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/table.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/data-display/tree-view.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/disclosure/accordion.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/disclosure/collapsible.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/disclosure/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/disclosure/toggle-group.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/feedback/empty.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/feedback/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/feedback/loading.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/feedback/progress.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/feedback/skeleton.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/feedback/sonner.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/feedback/spinner.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/feedback/toast.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/feedback/toaster.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/button.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/calendar.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/checkbox.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/combobox.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/command.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/date-picker.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/file-upload.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/form.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/input-otp.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/input.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/label.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/radio-group.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/select.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/slider.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/switch.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/textarea.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/form/toggle.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/aspect-ratio.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/card.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/container.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/flex.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/grid.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/page.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/semantic.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/stack.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/layout/tabs.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/navigation/header-bar.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/navigation/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/navigation/sidebar.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/alert-dialog.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/context-menu.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/dialog.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/drawer.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/dropdown-menu.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/hover-card.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/menubar.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/popover.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/sheet.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/overlay/tooltip.d.ts +0 -0
- /package/dist/{src → packages/components/src}/renderers/placeholders.d.ts +0 -0
- /package/dist/{src → packages/components/src}/types/config-panel.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/alert.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/badge.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/breadcrumb.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/calendar.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/card.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/carousel.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/index.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/input-otp.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/input.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/pagination.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/skeleton.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/sonner.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/table.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/textarea.d.ts +0 -0
- /package/dist/{src → packages/components/src}/ui/typography.d.ts +0 -0
|
@@ -1,1243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Enterprise-level DataTable Component (Airtable-like)
|
|
10
|
-
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
|
11
|
-
import { cn } from '../../lib/utils';
|
|
12
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
13
|
-
import type { DataTableSchema } from '@object-ui/types';
|
|
14
|
-
import { useObjectTranslation } from '@object-ui/react';
|
|
15
|
-
import {
|
|
16
|
-
Table,
|
|
17
|
-
TableHeader,
|
|
18
|
-
TableBody,
|
|
19
|
-
TableFooter,
|
|
20
|
-
TableHead,
|
|
21
|
-
TableRow,
|
|
22
|
-
TableCell,
|
|
23
|
-
TableCaption
|
|
24
|
-
} from '../../ui/table';
|
|
25
|
-
import { Button } from '../../ui/button';
|
|
26
|
-
import { Input } from '../../ui/input';
|
|
27
|
-
import { Checkbox } from '../../ui/checkbox';
|
|
28
|
-
import {
|
|
29
|
-
Select,
|
|
30
|
-
SelectContent,
|
|
31
|
-
SelectItem,
|
|
32
|
-
SelectTrigger,
|
|
33
|
-
SelectValue,
|
|
34
|
-
} from '../../ui/select';
|
|
35
|
-
import {
|
|
36
|
-
ChevronUp,
|
|
37
|
-
ChevronDown,
|
|
38
|
-
ChevronsUpDown,
|
|
39
|
-
Search,
|
|
40
|
-
Download,
|
|
41
|
-
Edit,
|
|
42
|
-
Trash2,
|
|
43
|
-
ChevronLeft,
|
|
44
|
-
ChevronRight,
|
|
45
|
-
ChevronsLeft,
|
|
46
|
-
ChevronsRight,
|
|
47
|
-
GripVertical,
|
|
48
|
-
Save,
|
|
49
|
-
X,
|
|
50
|
-
Plus,
|
|
51
|
-
Expand,
|
|
52
|
-
} from 'lucide-react';
|
|
53
|
-
|
|
54
|
-
type SortDirection = 'asc' | 'desc' | null;
|
|
55
|
-
|
|
56
|
-
/** Number of skeleton rows shown when the table has no data */
|
|
57
|
-
const GHOST_ROW_COUNT = 3;
|
|
58
|
-
|
|
59
|
-
/** Returns a Tailwind width class for ghost cell placeholders to create visual variety */
|
|
60
|
-
function ghostCellWidth(columnIndex: number, totalColumns: number): string {
|
|
61
|
-
if (columnIndex === 0) return 'w-3/4';
|
|
62
|
-
if (columnIndex === totalColumns - 1) return 'w-1/3';
|
|
63
|
-
return 'w-1/2';
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Default English fallback translations for the data table
|
|
67
|
-
const TABLE_DEFAULT_TRANSLATIONS: Record<string, string> = {
|
|
68
|
-
'table.rowsPerPage': 'Rows per page',
|
|
69
|
-
'table.pageInfo': 'Page {{current}} of {{total}}',
|
|
70
|
-
'table.totalRecords': '{{count}} total',
|
|
71
|
-
'table.noResults': 'No results found',
|
|
72
|
-
'table.noResultsHint': 'Try adjusting your filters or search query.',
|
|
73
|
-
'table.sortAsc': 'Sort ascending',
|
|
74
|
-
'table.sortDesc': 'Sort descending',
|
|
75
|
-
'table.hideColumn': 'Hide column',
|
|
76
|
-
'table.cancelAll': 'Cancel All',
|
|
77
|
-
'table.saveAll': 'Save All ({{count}})',
|
|
78
|
-
'table.exportCSV': 'Export CSV',
|
|
79
|
-
'table.addRecord': 'Add record',
|
|
80
|
-
'table.open': 'Open',
|
|
81
|
-
'table.search': 'Search...',
|
|
82
|
-
'table.modified': '{{count}} row modified',
|
|
83
|
-
'table.selected': '{{count}} selected',
|
|
84
|
-
'common.actions': 'Actions',
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Safe wrapper for useObjectTranslation that falls back to English defaults
|
|
89
|
-
* when I18nProvider is not available (e.g., standalone usage).
|
|
90
|
-
*/
|
|
91
|
-
function useTableTranslation() {
|
|
92
|
-
try {
|
|
93
|
-
const result = useObjectTranslation();
|
|
94
|
-
const testValue = result.t('table.rowsPerPage');
|
|
95
|
-
if (testValue === 'table.rowsPerPage') {
|
|
96
|
-
return {
|
|
97
|
-
t: (key: string, options?: Record<string, unknown>) => {
|
|
98
|
-
let value = TABLE_DEFAULT_TRANSLATIONS[key] || key;
|
|
99
|
-
if (options) {
|
|
100
|
-
for (const [k, v] of Object.entries(options)) {
|
|
101
|
-
value = value.replace(`{{${k}}}`, String(v));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return value;
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
return { t: result.t };
|
|
109
|
-
} catch {
|
|
110
|
-
return {
|
|
111
|
-
t: (key: string, options?: Record<string, unknown>) => {
|
|
112
|
-
let value = TABLE_DEFAULT_TRANSLATIONS[key] || key;
|
|
113
|
-
if (options) {
|
|
114
|
-
for (const [k, v] of Object.entries(options)) {
|
|
115
|
-
value = value.replace(`{{${k}}}`, String(v));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return value;
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Enterprise-level data table component with Airtable-like features.
|
|
126
|
-
*
|
|
127
|
-
* Provides comprehensive table functionality including:
|
|
128
|
-
* - Multi-column sorting (ascending/descending/none)
|
|
129
|
-
* - Real-time search across all columns
|
|
130
|
-
* - Pagination with configurable page sizes
|
|
131
|
-
* - Row selection with persistence across pages
|
|
132
|
-
* - CSV export of filtered/sorted data
|
|
133
|
-
* - Row action buttons (edit/delete)
|
|
134
|
-
*
|
|
135
|
-
* @example
|
|
136
|
-
* ```json
|
|
137
|
-
* {
|
|
138
|
-
* "type": "data-table",
|
|
139
|
-
* "pagination": true,
|
|
140
|
-
* "searchable": true,
|
|
141
|
-
* "selectable": true,
|
|
142
|
-
* "sortable": true,
|
|
143
|
-
* "exportable": true,
|
|
144
|
-
* "rowActions": true,
|
|
145
|
-
* "columns": [
|
|
146
|
-
* { "header": "ID", "accessorKey": "id", "width": "80px" },
|
|
147
|
-
* { "header": "Name", "accessorKey": "name" }
|
|
148
|
-
* ],
|
|
149
|
-
* "data": [
|
|
150
|
-
* { "id": 1, "name": "John Doe" }
|
|
151
|
-
* ]
|
|
152
|
-
* }
|
|
153
|
-
* ```
|
|
154
|
-
*
|
|
155
|
-
* @param {Object} props - Component props
|
|
156
|
-
* @param {DataTableSchema} props.schema - Table schema configuration
|
|
157
|
-
* @returns {JSX.Element} Rendered data table component
|
|
158
|
-
*/
|
|
159
|
-
const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
160
|
-
const {
|
|
161
|
-
caption,
|
|
162
|
-
columns: rawColumns = [],
|
|
163
|
-
data: rawData = [],
|
|
164
|
-
pagination = true,
|
|
165
|
-
pageSize: initialPageSize = 10,
|
|
166
|
-
searchable = true,
|
|
167
|
-
selectable = false,
|
|
168
|
-
sortable = true,
|
|
169
|
-
exportable = false,
|
|
170
|
-
rowActions = false,
|
|
171
|
-
resizableColumns = true,
|
|
172
|
-
reorderableColumns = true,
|
|
173
|
-
editable = false,
|
|
174
|
-
singleClickEdit = false,
|
|
175
|
-
selectionStyle = 'always',
|
|
176
|
-
rowClassName,
|
|
177
|
-
rowStyle,
|
|
178
|
-
className,
|
|
179
|
-
frozenColumns = 0,
|
|
180
|
-
showRowNumbers = false,
|
|
181
|
-
showAddRow = false,
|
|
182
|
-
} = schema;
|
|
183
|
-
|
|
184
|
-
// i18n support for pagination labels
|
|
185
|
-
const { t } = useTableTranslation();
|
|
186
|
-
|
|
187
|
-
// Ensure data is always an array – provider config objects or null/undefined
|
|
188
|
-
// must not reach array operations like .filter() / .some()
|
|
189
|
-
const data = Array.isArray(rawData) ? rawData : [];
|
|
190
|
-
|
|
191
|
-
// Normalize columns to support legacy keys (label/name) from existing JSONs
|
|
192
|
-
const initialColumns = useMemo(() => {
|
|
193
|
-
return rawColumns.map((col: any) => ({
|
|
194
|
-
...col,
|
|
195
|
-
header: col.header || col.label,
|
|
196
|
-
accessorKey: col.accessorKey || col.name
|
|
197
|
-
}));
|
|
198
|
-
}, [rawColumns]);
|
|
199
|
-
|
|
200
|
-
// Auto-size columns: estimate width from header and data content for columns without explicit widths
|
|
201
|
-
const autoSizedWidths = useMemo(() => {
|
|
202
|
-
const widths: Record<string, number> = {};
|
|
203
|
-
const cols = rawColumns.map((col: any) => ({
|
|
204
|
-
header: col.header || col.label,
|
|
205
|
-
accessorKey: col.accessorKey || col.name,
|
|
206
|
-
width: col.width,
|
|
207
|
-
}));
|
|
208
|
-
for (const col of cols) {
|
|
209
|
-
if (col.width) continue; // Skip columns with explicit widths
|
|
210
|
-
const headerLen = (col.header || '').length;
|
|
211
|
-
let maxLen = headerLen;
|
|
212
|
-
// Sample up to 50 rows for content width estimation
|
|
213
|
-
const sampleRows = data.slice(0, 50);
|
|
214
|
-
for (const row of sampleRows) {
|
|
215
|
-
const val = row[col.accessorKey];
|
|
216
|
-
const len = val != null ? String(val).length : 0;
|
|
217
|
-
if (len > maxLen) maxLen = len;
|
|
218
|
-
}
|
|
219
|
-
// Estimate pixel width: ~8px per character + 48px padding, min 80, max 400
|
|
220
|
-
widths[col.accessorKey] = Math.min(400, Math.max(80, maxLen * 8 + 48));
|
|
221
|
-
}
|
|
222
|
-
return widths;
|
|
223
|
-
}, [rawColumns, data]);
|
|
224
|
-
|
|
225
|
-
// State management
|
|
226
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
227
|
-
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
228
|
-
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
|
229
|
-
const [selectedRowIds, setSelectedRowIds] = useState<Set<any>>(new Set());
|
|
230
|
-
const [currentPage, setCurrentPage] = useState(1);
|
|
231
|
-
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
232
|
-
const [columns, setColumns] = useState(initialColumns);
|
|
233
|
-
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
|
234
|
-
const [draggedColumn, setDraggedColumn] = useState<number | null>(null);
|
|
235
|
-
const [dragOverColumn, setDragOverColumn] = useState<number | null>(null);
|
|
236
|
-
const [editingCell, setEditingCell] = useState<{ rowIndex: number; columnKey: string } | null>(null);
|
|
237
|
-
const [editValue, setEditValue] = useState<any>('');
|
|
238
|
-
// Track pending changes for multi-cell editing: rowIndex -> { columnKey -> newValue }
|
|
239
|
-
const [pendingChanges, setPendingChanges] = useState<Map<number, Record<string, any>>>(new Map());
|
|
240
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
241
|
-
// Column header context menu state
|
|
242
|
-
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; columnKey: string } | null>(null);
|
|
243
|
-
|
|
244
|
-
// Refs for column resizing
|
|
245
|
-
const resizingColumn = useRef<string | null>(null);
|
|
246
|
-
const startX = useRef<number>(0);
|
|
247
|
-
const startWidth = useRef<number>(0);
|
|
248
|
-
const editInputRef = useRef<HTMLInputElement>(null);
|
|
249
|
-
|
|
250
|
-
// Update columns when schema changes
|
|
251
|
-
useEffect(() => {
|
|
252
|
-
setColumns(initialColumns);
|
|
253
|
-
}, [initialColumns]);
|
|
254
|
-
|
|
255
|
-
// Filtering
|
|
256
|
-
const filteredData = useMemo(() => {
|
|
257
|
-
if (!searchQuery) return data;
|
|
258
|
-
|
|
259
|
-
return data.filter((row) =>
|
|
260
|
-
columns.some((col) => {
|
|
261
|
-
const value = row[col.accessorKey];
|
|
262
|
-
return value?.toString().toLowerCase().includes(searchQuery.toLowerCase());
|
|
263
|
-
})
|
|
264
|
-
);
|
|
265
|
-
}, [data, searchQuery, columns]);
|
|
266
|
-
|
|
267
|
-
// Sorting
|
|
268
|
-
const sortedData = useMemo(() => {
|
|
269
|
-
if (!sortColumn || !sortDirection) return filteredData;
|
|
270
|
-
|
|
271
|
-
return [...filteredData].sort((a, b) => {
|
|
272
|
-
const aValue = a[sortColumn];
|
|
273
|
-
const bValue = b[sortColumn];
|
|
274
|
-
|
|
275
|
-
if (aValue === bValue) return 0;
|
|
276
|
-
|
|
277
|
-
const comparison = aValue < bValue ? -1 : 1;
|
|
278
|
-
return sortDirection === 'asc' ? comparison : -comparison;
|
|
279
|
-
});
|
|
280
|
-
}, [filteredData, sortColumn, sortDirection]);
|
|
281
|
-
|
|
282
|
-
// Pagination
|
|
283
|
-
const totalPages = Math.ceil(sortedData.length / pageSize);
|
|
284
|
-
const paginatedData = pagination
|
|
285
|
-
? sortedData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
|
286
|
-
: sortedData;
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Generates a unique identifier for each row to maintain stable selection state
|
|
290
|
-
* across pagination and sorting operations.
|
|
291
|
-
*
|
|
292
|
-
* @param {any} row - The data row object
|
|
293
|
-
* @param {number} index - The row's index in the dataset
|
|
294
|
-
* @returns {string | number} Unique row identifier (uses 'id' field if available, falls back to index)
|
|
295
|
-
*/
|
|
296
|
-
const getRowId = (row: any, index: number) => {
|
|
297
|
-
// Try to use 'id' field, fall back to index
|
|
298
|
-
return row.id !== undefined ? row.id : `row-${index}`;
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
// Handlers
|
|
302
|
-
const handleSort = (columnKey: string) => {
|
|
303
|
-
if (!sortable) return;
|
|
304
|
-
|
|
305
|
-
if (sortColumn === columnKey) {
|
|
306
|
-
if (sortDirection === 'asc') {
|
|
307
|
-
setSortDirection('desc');
|
|
308
|
-
} else if (sortDirection === 'desc') {
|
|
309
|
-
setSortDirection(null);
|
|
310
|
-
setSortColumn(null);
|
|
311
|
-
}
|
|
312
|
-
} else {
|
|
313
|
-
setSortColumn(columnKey);
|
|
314
|
-
setSortDirection('asc');
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
// Column header context menu handler
|
|
319
|
-
const handleColumnContextMenu = (e: React.MouseEvent, columnKey: string) => {
|
|
320
|
-
e.preventDefault();
|
|
321
|
-
setContextMenu({ x: e.clientX, y: e.clientY, columnKey });
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
const hideColumn = (columnKey: string) => {
|
|
325
|
-
setColumns(prev => prev.filter(c => c.accessorKey !== columnKey));
|
|
326
|
-
setContextMenu(null);
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// Close context menu on outside click
|
|
330
|
-
useEffect(() => {
|
|
331
|
-
if (!contextMenu) return;
|
|
332
|
-
const close = () => setContextMenu(null);
|
|
333
|
-
document.addEventListener('click', close);
|
|
334
|
-
return () => document.removeEventListener('click', close);
|
|
335
|
-
}, [contextMenu]);
|
|
336
|
-
|
|
337
|
-
const handleSelectAll = (checked: boolean) => {
|
|
338
|
-
const newSelected = new Set<any>();
|
|
339
|
-
if (checked) {
|
|
340
|
-
paginatedData.forEach((row, idx) => {
|
|
341
|
-
const globalIndex = (currentPage - 1) * pageSize + idx;
|
|
342
|
-
const rowId = getRowId(row, globalIndex);
|
|
343
|
-
newSelected.add(rowId);
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
setSelectedRowIds(newSelected);
|
|
347
|
-
|
|
348
|
-
// Call callback if provided
|
|
349
|
-
if (schema.onSelectionChange) {
|
|
350
|
-
const selectedData = sortedData.filter((row, idx) => {
|
|
351
|
-
const rowId = getRowId(row, idx);
|
|
352
|
-
return newSelected.has(rowId);
|
|
353
|
-
});
|
|
354
|
-
schema.onSelectionChange(selectedData);
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
const handleSelectRow = (rowId: any, checked: boolean) => {
|
|
359
|
-
const newSelected = new Set(selectedRowIds);
|
|
360
|
-
if (checked) {
|
|
361
|
-
newSelected.add(rowId);
|
|
362
|
-
} else {
|
|
363
|
-
newSelected.delete(rowId);
|
|
364
|
-
}
|
|
365
|
-
setSelectedRowIds(newSelected);
|
|
366
|
-
|
|
367
|
-
// Call callback if provided
|
|
368
|
-
if (schema.onSelectionChange) {
|
|
369
|
-
const selectedData = sortedData.filter((row, idx) => {
|
|
370
|
-
const id = getRowId(row, idx);
|
|
371
|
-
return newSelected.has(id);
|
|
372
|
-
});
|
|
373
|
-
schema.onSelectionChange(selectedData);
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const handleExport = () => {
|
|
378
|
-
const csvContent = [
|
|
379
|
-
columns.map(col => col.header).join(','),
|
|
380
|
-
...sortedData.map(row =>
|
|
381
|
-
columns.map(col => JSON.stringify(row[col.accessorKey] || '')).join(',')
|
|
382
|
-
)
|
|
383
|
-
].join('\n');
|
|
384
|
-
|
|
385
|
-
const blob = new Blob([csvContent], { type: 'text/csv' });
|
|
386
|
-
const url = window.URL.createObjectURL(blob);
|
|
387
|
-
const a = document.createElement('a');
|
|
388
|
-
a.href = url;
|
|
389
|
-
a.download = 'table-export.csv';
|
|
390
|
-
a.click();
|
|
391
|
-
window.URL.revokeObjectURL(url);
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const getSortIcon = (columnKey: string) => {
|
|
395
|
-
if (sortColumn !== columnKey) {
|
|
396
|
-
return <ChevronsUpDown className="h-3 w-3 ml-0.5 opacity-0 group-hover:opacity-50 transition-opacity" />;
|
|
397
|
-
}
|
|
398
|
-
if (sortDirection === 'asc') {
|
|
399
|
-
return <ChevronUp className="h-3 w-3 ml-0.5 text-primary" />;
|
|
400
|
-
}
|
|
401
|
-
return <ChevronDown className="h-3 w-3 ml-0.5 text-primary" />;
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
// Column resizing handlers
|
|
405
|
-
const handleResizeStart = (e: React.MouseEvent, columnKey: string) => {
|
|
406
|
-
if (!resizableColumns) return;
|
|
407
|
-
e.preventDefault();
|
|
408
|
-
e.stopPropagation();
|
|
409
|
-
|
|
410
|
-
resizingColumn.current = columnKey;
|
|
411
|
-
startX.current = e.clientX;
|
|
412
|
-
|
|
413
|
-
const headerCell = (e.target as HTMLElement).closest('th');
|
|
414
|
-
if (headerCell) {
|
|
415
|
-
startWidth.current = headerCell.offsetWidth;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
document.addEventListener('mousemove', handleResizeMove);
|
|
419
|
-
document.addEventListener('mouseup', handleResizeEnd);
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
const handleResizeMove = (e: MouseEvent) => {
|
|
423
|
-
if (!resizingColumn.current) return;
|
|
424
|
-
|
|
425
|
-
const diff = e.clientX - startX.current;
|
|
426
|
-
const newWidth = Math.max(50, startWidth.current + diff); // Min width 50px
|
|
427
|
-
|
|
428
|
-
setColumnWidths(prev => ({
|
|
429
|
-
...prev,
|
|
430
|
-
[resizingColumn.current!]: newWidth
|
|
431
|
-
}));
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const handleResizeEnd = () => {
|
|
435
|
-
resizingColumn.current = null;
|
|
436
|
-
document.removeEventListener('mousemove', handleResizeMove);
|
|
437
|
-
document.removeEventListener('mouseup', handleResizeEnd);
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
// Column reordering handlers
|
|
441
|
-
const handleColumnDragStart = (e: React.DragEvent, index: number) => {
|
|
442
|
-
if (!reorderableColumns) return;
|
|
443
|
-
setDraggedColumn(index);
|
|
444
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
const handleColumnDragOver = (e: React.DragEvent, index: number) => {
|
|
448
|
-
if (!reorderableColumns) return;
|
|
449
|
-
e.preventDefault();
|
|
450
|
-
e.dataTransfer.dropEffect = 'move';
|
|
451
|
-
setDragOverColumn(index);
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
const handleColumnDrop = (e: React.DragEvent, dropIndex: number) => {
|
|
455
|
-
if (!reorderableColumns || draggedColumn === null) return;
|
|
456
|
-
e.preventDefault();
|
|
457
|
-
|
|
458
|
-
if (draggedColumn === dropIndex) {
|
|
459
|
-
setDraggedColumn(null);
|
|
460
|
-
setDragOverColumn(null);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const newColumns = [...columns];
|
|
465
|
-
const [removed] = newColumns.splice(draggedColumn, 1);
|
|
466
|
-
newColumns.splice(dropIndex, 0, removed);
|
|
467
|
-
|
|
468
|
-
setColumns(newColumns);
|
|
469
|
-
setDraggedColumn(null);
|
|
470
|
-
setDragOverColumn(null);
|
|
471
|
-
|
|
472
|
-
// Call callback if provided
|
|
473
|
-
if (schema.onColumnsReorder) {
|
|
474
|
-
schema.onColumnsReorder(newColumns);
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
const handleColumnDragEnd = () => {
|
|
479
|
-
setDraggedColumn(null);
|
|
480
|
-
setDragOverColumn(null);
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
// Cell editing handlers
|
|
484
|
-
const startEdit = (rowIndex: number, columnKey: string) => {
|
|
485
|
-
if (!editable) return;
|
|
486
|
-
|
|
487
|
-
const column = columns.find(col => col.accessorKey === columnKey);
|
|
488
|
-
if (column?.editable === false) return;
|
|
489
|
-
|
|
490
|
-
setEditingCell({ rowIndex, columnKey });
|
|
491
|
-
|
|
492
|
-
// Check if there's a pending change for this cell, otherwise use current data value
|
|
493
|
-
const rowChanges = pendingChanges.get(rowIndex);
|
|
494
|
-
const currentValue = paginatedData[rowIndex][columnKey];
|
|
495
|
-
const valueToEdit = rowChanges?.[columnKey] ?? currentValue ?? '';
|
|
496
|
-
setEditValue(valueToEdit);
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
const saveEdit = (force: boolean = false) => {
|
|
500
|
-
if (!editingCell) return;
|
|
501
|
-
|
|
502
|
-
// Don't save if we're in cancelled state (unless forced)
|
|
503
|
-
if (!force && editingCell === null) return;
|
|
504
|
-
|
|
505
|
-
const { rowIndex, columnKey } = editingCell;
|
|
506
|
-
const globalIndex = (currentPage - 1) * pageSize + rowIndex;
|
|
507
|
-
const row = sortedData[globalIndex];
|
|
508
|
-
|
|
509
|
-
// Update pending changes
|
|
510
|
-
const newPendingChanges = new Map(pendingChanges);
|
|
511
|
-
const rowChanges = newPendingChanges.get(rowIndex) || {};
|
|
512
|
-
rowChanges[columnKey] = editValue;
|
|
513
|
-
newPendingChanges.set(rowIndex, rowChanges);
|
|
514
|
-
setPendingChanges(newPendingChanges);
|
|
515
|
-
|
|
516
|
-
// Call the legacy onCellChange callback if provided
|
|
517
|
-
if (schema.onCellChange) {
|
|
518
|
-
schema.onCellChange(globalIndex, columnKey, editValue, row);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
setEditingCell(null);
|
|
522
|
-
setEditValue('');
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
const cancelEdit = () => {
|
|
526
|
-
setEditingCell(null);
|
|
527
|
-
setEditValue('');
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
const saveRow = async (rowIndex: number) => {
|
|
531
|
-
const globalIndex = (currentPage - 1) * pageSize + rowIndex;
|
|
532
|
-
const row = sortedData[globalIndex];
|
|
533
|
-
const rowChanges = pendingChanges.get(rowIndex);
|
|
534
|
-
|
|
535
|
-
if (!rowChanges || Object.keys(rowChanges).length === 0) return;
|
|
536
|
-
|
|
537
|
-
setIsSaving(true);
|
|
538
|
-
try {
|
|
539
|
-
if (schema.onRowSave) {
|
|
540
|
-
await schema.onRowSave(globalIndex, rowChanges, row);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Clear pending changes for this row
|
|
544
|
-
const newPendingChanges = new Map(pendingChanges);
|
|
545
|
-
newPendingChanges.delete(rowIndex);
|
|
546
|
-
setPendingChanges(newPendingChanges);
|
|
547
|
-
} catch (error) {
|
|
548
|
-
console.error('Failed to save row:', error);
|
|
549
|
-
} finally {
|
|
550
|
-
setIsSaving(false);
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
const cancelRowChanges = (rowIndex: number) => {
|
|
555
|
-
const newPendingChanges = new Map(pendingChanges);
|
|
556
|
-
newPendingChanges.delete(rowIndex);
|
|
557
|
-
setPendingChanges(newPendingChanges);
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
const saveBatch = async () => {
|
|
561
|
-
if (pendingChanges.size === 0) return;
|
|
562
|
-
|
|
563
|
-
setIsSaving(true);
|
|
564
|
-
try {
|
|
565
|
-
const changesToSave = Array.from(pendingChanges.entries()).map(([rowIndex, changes]) => {
|
|
566
|
-
const globalIndex = (currentPage - 1) * pageSize + rowIndex;
|
|
567
|
-
const row = sortedData[globalIndex];
|
|
568
|
-
return { rowIndex: globalIndex, changes, row };
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
if (schema.onBatchSave) {
|
|
572
|
-
await schema.onBatchSave(changesToSave);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Clear all pending changes
|
|
576
|
-
setPendingChanges(new Map());
|
|
577
|
-
} catch (error) {
|
|
578
|
-
console.error('Failed to save batch:', error);
|
|
579
|
-
} finally {
|
|
580
|
-
setIsSaving(false);
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
const cancelAllChanges = () => {
|
|
585
|
-
setPendingChanges(new Map());
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const handleCellKeyDown = (e: React.KeyboardEvent, rowIndex: number, columnKey: string) => {
|
|
589
|
-
// Copy cell value with Ctrl+C / Cmd+C
|
|
590
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'c' && !editingCell) {
|
|
591
|
-
e.preventDefault();
|
|
592
|
-
const globalIdx = (currentPage - 1) * pageSize + rowIndex;
|
|
593
|
-
const row = sortedData[globalIdx];
|
|
594
|
-
if (row) {
|
|
595
|
-
const value = row[columnKey];
|
|
596
|
-
const text = value != null ? String(value) : '';
|
|
597
|
-
navigator.clipboard.writeText(text).catch(() => {
|
|
598
|
-
// Fallback for environments without clipboard API
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (!editable) return;
|
|
605
|
-
|
|
606
|
-
const column = columns.find(col => col.accessorKey === columnKey);
|
|
607
|
-
if (column?.editable === false) return;
|
|
608
|
-
|
|
609
|
-
if (e.key === 'Enter' && !editingCell) {
|
|
610
|
-
e.preventDefault();
|
|
611
|
-
startEdit(rowIndex, columnKey);
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
const handleEditKeyDown = (e: React.KeyboardEvent) => {
|
|
616
|
-
if (e.key === 'Enter') {
|
|
617
|
-
e.preventDefault();
|
|
618
|
-
saveEdit(true);
|
|
619
|
-
} else if (e.key === 'Escape') {
|
|
620
|
-
e.preventDefault();
|
|
621
|
-
cancelEdit();
|
|
622
|
-
}
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
// Auto-focus on edit input when entering edit mode
|
|
626
|
-
useEffect(() => {
|
|
627
|
-
if (editingCell && editInputRef.current) {
|
|
628
|
-
editInputRef.current.focus();
|
|
629
|
-
editInputRef.current.select();
|
|
630
|
-
}
|
|
631
|
-
}, [editingCell]);
|
|
632
|
-
|
|
633
|
-
// Cleanup on unmount
|
|
634
|
-
useEffect(() => {
|
|
635
|
-
return () => {
|
|
636
|
-
document.removeEventListener('mousemove', handleResizeMove);
|
|
637
|
-
document.removeEventListener('mouseup', handleResizeEnd);
|
|
638
|
-
};
|
|
639
|
-
}, []);
|
|
640
|
-
|
|
641
|
-
// Check if all rows on current page are selected
|
|
642
|
-
const allPageRowsSelected = paginatedData.length > 0 && paginatedData.every((row, idx) => {
|
|
643
|
-
const globalIndex = (currentPage - 1) * pageSize + idx;
|
|
644
|
-
const rowId = getRowId(row, globalIndex);
|
|
645
|
-
return selectedRowIds.has(rowId);
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
const somePageRowsSelected = paginatedData.some((row, idx) => {
|
|
649
|
-
const globalIndex = (currentPage - 1) * pageSize + idx;
|
|
650
|
-
const rowId = getRowId(row, globalIndex);
|
|
651
|
-
return selectedRowIds.has(rowId);
|
|
652
|
-
}) && !allPageRowsSelected;
|
|
653
|
-
|
|
654
|
-
const hasPendingChanges = pendingChanges.size > 0;
|
|
655
|
-
const showToolbar = searchable || exportable || (selectable && selectedRowIds.size > 0) || hasPendingChanges;
|
|
656
|
-
|
|
657
|
-
return (
|
|
658
|
-
<div className={`flex flex-col h-full gap-2 sm:gap-4 ${className || ''}`}>
|
|
659
|
-
{/* Toolbar */}
|
|
660
|
-
{showToolbar && (
|
|
661
|
-
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-2 sm:gap-4 flex-none">
|
|
662
|
-
<div className="flex items-center gap-2 flex-1">
|
|
663
|
-
{searchable && (
|
|
664
|
-
<div className="relative w-full sm:max-w-sm flex-1">
|
|
665
|
-
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
666
|
-
<Input
|
|
667
|
-
placeholder={t('table.search')}
|
|
668
|
-
value={searchQuery}
|
|
669
|
-
onChange={(e) => {
|
|
670
|
-
setSearchQuery(e.target.value);
|
|
671
|
-
setCurrentPage(1);
|
|
672
|
-
}}
|
|
673
|
-
className="pl-8"
|
|
674
|
-
/>
|
|
675
|
-
</div>
|
|
676
|
-
)}
|
|
677
|
-
</div>
|
|
678
|
-
|
|
679
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
680
|
-
{hasPendingChanges && (
|
|
681
|
-
<>
|
|
682
|
-
<div className="text-sm text-muted-foreground">
|
|
683
|
-
{t('table.modified', { count: pendingChanges.size })}
|
|
684
|
-
</div>
|
|
685
|
-
<Button
|
|
686
|
-
variant="outline"
|
|
687
|
-
size="sm"
|
|
688
|
-
onClick={cancelAllChanges}
|
|
689
|
-
disabled={isSaving}
|
|
690
|
-
>
|
|
691
|
-
<X className="h-4 w-4 mr-2" />
|
|
692
|
-
{t('table.cancelAll')}
|
|
693
|
-
</Button>
|
|
694
|
-
<Button
|
|
695
|
-
variant="default"
|
|
696
|
-
size="sm"
|
|
697
|
-
onClick={saveBatch}
|
|
698
|
-
disabled={isSaving}
|
|
699
|
-
>
|
|
700
|
-
<Save className="h-4 w-4 mr-2" />
|
|
701
|
-
{t('table.saveAll', { count: pendingChanges.size })}
|
|
702
|
-
</Button>
|
|
703
|
-
</>
|
|
704
|
-
)}
|
|
705
|
-
|
|
706
|
-
{exportable && (
|
|
707
|
-
<Button
|
|
708
|
-
variant="outline"
|
|
709
|
-
size="sm"
|
|
710
|
-
onClick={handleExport}
|
|
711
|
-
disabled={sortedData.length === 0}
|
|
712
|
-
>
|
|
713
|
-
<Download className="h-4 w-4 mr-2" />
|
|
714
|
-
{t('table.exportCSV')}
|
|
715
|
-
</Button>
|
|
716
|
-
)}
|
|
717
|
-
|
|
718
|
-
{selectable && selectedRowIds.size > 0 && (
|
|
719
|
-
<div className="text-sm text-muted-foreground">
|
|
720
|
-
{t('table.selected', { count: selectedRowIds.size })}
|
|
721
|
-
</div>
|
|
722
|
-
)}
|
|
723
|
-
</div>
|
|
724
|
-
</div>
|
|
725
|
-
)}
|
|
726
|
-
|
|
727
|
-
{/* Table - horizontal scroll indicator via inset shadow on mobile */}
|
|
728
|
-
<div className="rounded-md border flex-1 min-h-0 overflow-auto relative bg-background [-webkit-overflow-scrolling:touch] shadow-[inset_-8px_0_8px_-8px_rgba(0,0,0,0.08)]">
|
|
729
|
-
<Table>
|
|
730
|
-
{caption && <TableCaption>{caption}</TableCaption>}
|
|
731
|
-
<TableHeader className="sticky top-0 bg-muted/30 z-10">
|
|
732
|
-
<TableRow>
|
|
733
|
-
{selectable && (
|
|
734
|
-
<TableHead className={cn("w-10 bg-muted/30", frozenColumns > 0 && "sticky left-0 z-20")}>
|
|
735
|
-
<Checkbox
|
|
736
|
-
checked={allPageRowsSelected ? true : somePageRowsSelected ? 'indeterminate' : false}
|
|
737
|
-
onCheckedChange={handleSelectAll}
|
|
738
|
-
/>
|
|
739
|
-
</TableHead>
|
|
740
|
-
)}
|
|
741
|
-
{showRowNumbers && (
|
|
742
|
-
<TableHead className={cn("w-10 bg-muted/30 text-center", frozenColumns > 0 && "sticky z-20")} style={frozenColumns > 0 ? { left: selectable ? 40 : 0 } : undefined}>
|
|
743
|
-
<span className="text-xs text-muted-foreground">#</span>
|
|
744
|
-
</TableHead>
|
|
745
|
-
)}
|
|
746
|
-
{columns.map((col, index) => {
|
|
747
|
-
const columnWidth = columnWidths[col.accessorKey] || col.width || autoSizedWidths[col.accessorKey];
|
|
748
|
-
const isDragging = draggedColumn === index;
|
|
749
|
-
const isDragOver = dragOverColumn === index;
|
|
750
|
-
const isFrozen = frozenColumns > 0 && index < frozenColumns;
|
|
751
|
-
const frozenOffset = isFrozen
|
|
752
|
-
? columns.slice(0, index).reduce((sum, c, i) => {
|
|
753
|
-
if (i < frozenColumns) {
|
|
754
|
-
const w = columnWidths[c.accessorKey] || c.width || autoSizedWidths[c.accessorKey];
|
|
755
|
-
return sum + (typeof w === 'number' ? w : w ? parseInt(String(w), 10) || 150 : 150);
|
|
756
|
-
}
|
|
757
|
-
return sum;
|
|
758
|
-
}, (selectable ? 40 : 0) + (showRowNumbers ? 40 : 0))
|
|
759
|
-
: undefined;
|
|
760
|
-
|
|
761
|
-
return (
|
|
762
|
-
<TableHead
|
|
763
|
-
key={col.accessorKey}
|
|
764
|
-
className={cn(
|
|
765
|
-
col.className,
|
|
766
|
-
sortable && col.sortable !== false && 'cursor-pointer select-none',
|
|
767
|
-
isDragging && 'opacity-50',
|
|
768
|
-
isDragOver && 'border-l-2 border-primary',
|
|
769
|
-
col.align === 'right' && 'text-right',
|
|
770
|
-
col.align === 'center' && 'text-center',
|
|
771
|
-
'relative group bg-muted/30',
|
|
772
|
-
isFrozen && 'sticky z-20',
|
|
773
|
-
isFrozen && index === frozenColumns - 1 && 'border-r-2 border-border shadow-[2px_0_4px_-2px_rgba(0,0,0,0.1)]',
|
|
774
|
-
)}
|
|
775
|
-
style={{
|
|
776
|
-
width: columnWidth,
|
|
777
|
-
minWidth: columnWidth,
|
|
778
|
-
...(isFrozen && { left: frozenOffset }),
|
|
779
|
-
}}
|
|
780
|
-
draggable={reorderableColumns}
|
|
781
|
-
onDragStart={(e) => handleColumnDragStart(e, index)}
|
|
782
|
-
onDragOver={(e) => handleColumnDragOver(e, index)}
|
|
783
|
-
onDrop={(e) => handleColumnDrop(e, index)}
|
|
784
|
-
onDragEnd={handleColumnDragEnd}
|
|
785
|
-
onClick={() => sortable && col.sortable !== false && handleSort(col.accessorKey)}
|
|
786
|
-
onContextMenu={(e) => handleColumnContextMenu(e, col.accessorKey)}
|
|
787
|
-
>
|
|
788
|
-
<div className={cn(
|
|
789
|
-
"flex items-center",
|
|
790
|
-
col.align === 'right' ? 'justify-end' : 'justify-between'
|
|
791
|
-
)}>
|
|
792
|
-
<div className="flex items-center gap-1">
|
|
793
|
-
{reorderableColumns && (
|
|
794
|
-
<GripVertical className="h-4 w-4 opacity-0 group-hover:opacity-50 cursor-grab active:cursor-grabbing flex-shrink-0" />
|
|
795
|
-
)}
|
|
796
|
-
{col.headerIcon && (
|
|
797
|
-
<span className="text-muted-foreground flex-shrink-0">{col.headerIcon}</span>
|
|
798
|
-
)}
|
|
799
|
-
<span className="text-xs font-normal text-muted-foreground whitespace-nowrap truncate">{col.header}</span>
|
|
800
|
-
{sortable && col.sortable !== false && getSortIcon(col.accessorKey)}
|
|
801
|
-
</div>
|
|
802
|
-
{resizableColumns && col.resizable !== false && (
|
|
803
|
-
<div
|
|
804
|
-
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary opacity-0 hover:opacity-100 transition-opacity"
|
|
805
|
-
onMouseDown={(e) => handleResizeStart(e, col.accessorKey)}
|
|
806
|
-
onClick={(e) => e.stopPropagation()}
|
|
807
|
-
/>
|
|
808
|
-
)}
|
|
809
|
-
</div>
|
|
810
|
-
</TableHead>
|
|
811
|
-
);
|
|
812
|
-
})}
|
|
813
|
-
{rowActions && (
|
|
814
|
-
<TableHead className="w-24 text-right bg-muted/30">{t('common.actions')}</TableHead>
|
|
815
|
-
)}
|
|
816
|
-
</TableRow>
|
|
817
|
-
</TableHeader>
|
|
818
|
-
<TableBody>
|
|
819
|
-
{paginatedData.length === 0 ? (
|
|
820
|
-
<>
|
|
821
|
-
<TableRow>
|
|
822
|
-
<TableCell
|
|
823
|
-
colSpan={columns.length + (selectable ? 1 : 0) + (showRowNumbers ? 1 : 0) + (rowActions ? 1 : 0)}
|
|
824
|
-
className="h-24 text-center text-muted-foreground"
|
|
825
|
-
>
|
|
826
|
-
<div className="flex flex-col items-center justify-center gap-2">
|
|
827
|
-
<Search className="h-8 w-8 text-muted-foreground/50" />
|
|
828
|
-
<p>{t('table.noResults')}</p>
|
|
829
|
-
<p className="text-xs text-muted-foreground/50">{t('table.noResultsHint')}</p>
|
|
830
|
-
</div>
|
|
831
|
-
</TableCell>
|
|
832
|
-
</TableRow>
|
|
833
|
-
{/* Ghost placeholder rows – visual skeleton to maintain table height when empty */}
|
|
834
|
-
{Array.from({ length: GHOST_ROW_COUNT }).map((_, i) => (
|
|
835
|
-
<TableRow key={`ghost-${i}`} className="hover:bg-transparent opacity-[0.15] pointer-events-none" data-testid="ghost-row">
|
|
836
|
-
{selectable && <TableCell className="p-3"><div className="h-4 w-4 rounded border border-muted-foreground/30" /></TableCell>}
|
|
837
|
-
{showRowNumbers && <TableCell className="text-center p-3"><div className="h-3 w-6 mx-auto rounded bg-muted-foreground/30" /></TableCell>}
|
|
838
|
-
{columns.map((_col, ci) => (
|
|
839
|
-
<TableCell key={ci} className="p-3">
|
|
840
|
-
<div className={cn("h-3 rounded bg-muted-foreground/30", ghostCellWidth(ci, columns.length))} />
|
|
841
|
-
</TableCell>
|
|
842
|
-
))}
|
|
843
|
-
{rowActions && <TableCell className="p-3"><div className="h-3 w-8 rounded bg-muted-foreground/30" /></TableCell>}
|
|
844
|
-
</TableRow>
|
|
845
|
-
))}
|
|
846
|
-
</>
|
|
847
|
-
) : (
|
|
848
|
-
<>
|
|
849
|
-
{paginatedData.map((row, rowIndex) => {
|
|
850
|
-
const globalIndex = (currentPage - 1) * pageSize + rowIndex;
|
|
851
|
-
const rowId = getRowId(row, globalIndex);
|
|
852
|
-
const isSelected = selectedRowIds.has(rowId);
|
|
853
|
-
const rowHasChanges = pendingChanges.has(rowIndex);
|
|
854
|
-
const rowChanges = pendingChanges.get(rowIndex) || {};
|
|
855
|
-
|
|
856
|
-
return (
|
|
857
|
-
<TableRow
|
|
858
|
-
key={rowId}
|
|
859
|
-
data-state={isSelected ? 'selected' : undefined}
|
|
860
|
-
className={cn(
|
|
861
|
-
"bg-background border-b border-border hover:bg-muted/50 group/row",
|
|
862
|
-
schema.onRowClick && "cursor-pointer",
|
|
863
|
-
rowHasChanges && "bg-amber-50 dark:bg-amber-950/20",
|
|
864
|
-
rowClassName && rowClassName(row, rowIndex)
|
|
865
|
-
)}
|
|
866
|
-
style={rowStyle ? rowStyle(row, rowIndex) : undefined}
|
|
867
|
-
onClick={(e) => {
|
|
868
|
-
if (schema.onRowClick && !e.defaultPrevented) {
|
|
869
|
-
// Simple heuristic to avoid triggering on interactive elements if they didn't stop propagation
|
|
870
|
-
const target = e.target as HTMLElement;
|
|
871
|
-
if (target.closest('button') || target.closest('[role="checkbox"]') || target.closest('a')) {
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
schema.onRowClick(row);
|
|
875
|
-
}
|
|
876
|
-
}}
|
|
877
|
-
>
|
|
878
|
-
{selectable && (
|
|
879
|
-
<TableCell className={cn(frozenColumns > 0 && "sticky left-0 z-10 bg-background", selectionStyle === 'hover' && "relative")}>
|
|
880
|
-
{selectionStyle === 'hover' ? (
|
|
881
|
-
<div className={cn("transition-opacity", isSelected ? "opacity-100" : "opacity-0 group-hover/row:opacity-100")}>
|
|
882
|
-
<Checkbox
|
|
883
|
-
checked={isSelected}
|
|
884
|
-
onCheckedChange={(checked) => handleSelectRow(rowId, checked as boolean)}
|
|
885
|
-
/>
|
|
886
|
-
</div>
|
|
887
|
-
) : (
|
|
888
|
-
<Checkbox
|
|
889
|
-
checked={isSelected}
|
|
890
|
-
onCheckedChange={(checked) => handleSelectRow(rowId, checked as boolean)}
|
|
891
|
-
/>
|
|
892
|
-
)}
|
|
893
|
-
</TableCell>
|
|
894
|
-
)}
|
|
895
|
-
{showRowNumbers && (
|
|
896
|
-
<TableCell className={cn("text-center w-10 relative", frozenColumns > 0 && "sticky z-10 bg-background")} style={frozenColumns > 0 ? { left: selectable ? 40 : 0 } : undefined}>
|
|
897
|
-
<span className={cn("text-xs text-muted-foreground tabular-nums select-none", selectable ? "group-hover/row:hidden" : "group-hover/row:invisible")}>
|
|
898
|
-
{globalIndex + 1}
|
|
899
|
-
</span>
|
|
900
|
-
{selectable ? (
|
|
901
|
-
<div className="absolute inset-0 hidden group-hover/row:flex items-center justify-center">
|
|
902
|
-
<Checkbox
|
|
903
|
-
checked={isSelected}
|
|
904
|
-
onCheckedChange={(checked) => handleSelectRow(rowId, checked as boolean)}
|
|
905
|
-
data-testid="row-hover-checkbox"
|
|
906
|
-
/>
|
|
907
|
-
</div>
|
|
908
|
-
) : schema.onRowClick && (
|
|
909
|
-
<button
|
|
910
|
-
type="button"
|
|
911
|
-
className="absolute inset-0 hidden group-hover/row:flex items-center justify-center gap-0.5 text-xs font-medium text-primary hover:text-primary/80"
|
|
912
|
-
data-testid="row-expand-button"
|
|
913
|
-
onClick={(e) => {
|
|
914
|
-
e.stopPropagation();
|
|
915
|
-
schema.onRowClick?.(row);
|
|
916
|
-
}}
|
|
917
|
-
title="Open record"
|
|
918
|
-
>
|
|
919
|
-
<span>{t('table.open')}</span>
|
|
920
|
-
<ChevronRight className="h-3 w-3" />
|
|
921
|
-
</button>
|
|
922
|
-
)}
|
|
923
|
-
</TableCell>
|
|
924
|
-
)}
|
|
925
|
-
{columns.map((col, colIndex) => {
|
|
926
|
-
const columnWidth = columnWidths[col.accessorKey] || col.width || autoSizedWidths[col.accessorKey];
|
|
927
|
-
const originalValue = row[col.accessorKey];
|
|
928
|
-
const hasPendingChange = rowChanges[col.accessorKey] !== undefined;
|
|
929
|
-
const cellValue = hasPendingChange ? rowChanges[col.accessorKey] : originalValue;
|
|
930
|
-
const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.columnKey === col.accessorKey;
|
|
931
|
-
const isEditable = editable && col.editable !== false;
|
|
932
|
-
const isFrozen = frozenColumns > 0 && colIndex < frozenColumns;
|
|
933
|
-
const frozenOffset = isFrozen
|
|
934
|
-
? columns.slice(0, colIndex).reduce((sum, c, i) => {
|
|
935
|
-
if (i < frozenColumns) {
|
|
936
|
-
const w = columnWidths[c.accessorKey] || c.width || autoSizedWidths[c.accessorKey];
|
|
937
|
-
return sum + (typeof w === 'number' ? w : w ? parseInt(String(w), 10) || 150 : 150);
|
|
938
|
-
}
|
|
939
|
-
return sum;
|
|
940
|
-
}, (selectable ? 40 : 0) + (showRowNumbers ? 40 : 0))
|
|
941
|
-
: undefined;
|
|
942
|
-
|
|
943
|
-
return (
|
|
944
|
-
<TableCell
|
|
945
|
-
key={colIndex}
|
|
946
|
-
className={cn(
|
|
947
|
-
col.cellClassName,
|
|
948
|
-
col.align === 'right' && 'text-right',
|
|
949
|
-
col.align === 'center' && 'text-center',
|
|
950
|
-
isEditable && !isEditing && "cursor-text hover:bg-muted/50",
|
|
951
|
-
hasPendingChange && "font-semibold text-amber-700 dark:text-amber-400",
|
|
952
|
-
isFrozen && 'sticky z-10 bg-background',
|
|
953
|
-
isFrozen && colIndex === frozenColumns - 1 && 'border-r-2 border-border shadow-[2px_0_4px_-2px_rgba(0,0,0,0.1)]',
|
|
954
|
-
)}
|
|
955
|
-
style={{
|
|
956
|
-
width: columnWidth,
|
|
957
|
-
minWidth: columnWidth,
|
|
958
|
-
maxWidth: columnWidth,
|
|
959
|
-
...(isFrozen && { left: frozenOffset }),
|
|
960
|
-
}}
|
|
961
|
-
onDoubleClick={() => isEditable && !singleClickEdit && startEdit(rowIndex, col.accessorKey)}
|
|
962
|
-
onClick={() => isEditable && singleClickEdit && startEdit(rowIndex, col.accessorKey)}
|
|
963
|
-
onKeyDown={(e) => handleCellKeyDown(e, rowIndex, col.accessorKey)}
|
|
964
|
-
tabIndex={0}
|
|
965
|
-
>
|
|
966
|
-
{isEditing ? (
|
|
967
|
-
<Input
|
|
968
|
-
ref={editInputRef}
|
|
969
|
-
value={editValue}
|
|
970
|
-
onChange={(e) => setEditValue(e.target.value)}
|
|
971
|
-
onKeyDown={handleEditKeyDown}
|
|
972
|
-
className="h-8 px-2 py-1"
|
|
973
|
-
/>
|
|
974
|
-
) : typeof col.cell === 'function' ? (
|
|
975
|
-
col.cell(cellValue, row)
|
|
976
|
-
) : (
|
|
977
|
-
cellValue
|
|
978
|
-
)}
|
|
979
|
-
</TableCell>
|
|
980
|
-
);
|
|
981
|
-
})}
|
|
982
|
-
{rowActions && (
|
|
983
|
-
<TableCell className="text-right">
|
|
984
|
-
<div className="flex items-center justify-end gap-1">
|
|
985
|
-
{rowHasChanges && (schema.onRowSave || schema.onBatchSave) ? (
|
|
986
|
-
<>
|
|
987
|
-
<Button
|
|
988
|
-
variant="ghost"
|
|
989
|
-
size="icon-sm"
|
|
990
|
-
onClick={() => cancelRowChanges(rowIndex)}
|
|
991
|
-
disabled={isSaving}
|
|
992
|
-
title="Cancel changes"
|
|
993
|
-
>
|
|
994
|
-
<X className="h-4 w-4" />
|
|
995
|
-
</Button>
|
|
996
|
-
<Button
|
|
997
|
-
variant="ghost"
|
|
998
|
-
size="icon-sm"
|
|
999
|
-
onClick={() => saveRow(rowIndex)}
|
|
1000
|
-
disabled={isSaving}
|
|
1001
|
-
title="Save row"
|
|
1002
|
-
>
|
|
1003
|
-
<Save className="h-4 w-4 text-green-600" />
|
|
1004
|
-
</Button>
|
|
1005
|
-
</>
|
|
1006
|
-
) : (
|
|
1007
|
-
<>
|
|
1008
|
-
<Button
|
|
1009
|
-
variant="ghost"
|
|
1010
|
-
size="icon-sm"
|
|
1011
|
-
onClick={() => schema.onRowEdit?.(row)}
|
|
1012
|
-
>
|
|
1013
|
-
<Edit className="h-4 w-4" />
|
|
1014
|
-
</Button>
|
|
1015
|
-
<Button
|
|
1016
|
-
variant="ghost"
|
|
1017
|
-
size="icon-sm"
|
|
1018
|
-
onClick={() => schema.onRowDelete?.(row)}
|
|
1019
|
-
>
|
|
1020
|
-
<Trash2 className="h-4 w-4 text-destructive" />
|
|
1021
|
-
</Button>
|
|
1022
|
-
</>
|
|
1023
|
-
)}
|
|
1024
|
-
</div>
|
|
1025
|
-
</TableCell>
|
|
1026
|
-
)}
|
|
1027
|
-
</TableRow>
|
|
1028
|
-
);
|
|
1029
|
-
})}
|
|
1030
|
-
{/* Add record row (Airtable-style) */}
|
|
1031
|
-
{showAddRow && (
|
|
1032
|
-
<TableRow
|
|
1033
|
-
className="hover:bg-muted/30 cursor-pointer border-b border-border"
|
|
1034
|
-
data-testid="add-record-row"
|
|
1035
|
-
onClick={() => schema.onAddRecord?.()}
|
|
1036
|
-
>
|
|
1037
|
-
<TableCell
|
|
1038
|
-
colSpan={columns.length + (selectable ? 1 : 0) + (showRowNumbers ? 1 : 0) + (rowActions ? 1 : 0)}
|
|
1039
|
-
className="h-9 px-3 py-1.5"
|
|
1040
|
-
>
|
|
1041
|
-
<span className="flex items-center gap-1.5 text-muted-foreground text-sm hover:text-foreground transition-colors">
|
|
1042
|
-
<Plus className="h-3.5 w-3.5" />
|
|
1043
|
-
{t('table.addRecord')}
|
|
1044
|
-
</span>
|
|
1045
|
-
</TableCell>
|
|
1046
|
-
</TableRow>
|
|
1047
|
-
)}
|
|
1048
|
-
{/* Filler rows to maintain height consistency (only when pagination is enabled) */}
|
|
1049
|
-
{pagination && paginatedData.length > 0 && Array.from({ length: Math.max(0, pageSize - paginatedData.length) }).map((_, i) => (
|
|
1050
|
-
<TableRow key={`empty-${i}`} className="hover:bg-transparent">
|
|
1051
|
-
<TableCell colSpan={columns.length + (selectable ? 1 : 0) + (showRowNumbers ? 1 : 0) + (rowActions ? 1 : 0)} className="h-[52px] p-0" />
|
|
1052
|
-
</TableRow>
|
|
1053
|
-
))}
|
|
1054
|
-
</>
|
|
1055
|
-
)}
|
|
1056
|
-
</TableBody>
|
|
1057
|
-
</Table>
|
|
1058
|
-
</div>
|
|
1059
|
-
|
|
1060
|
-
{/* Pagination */}
|
|
1061
|
-
{pagination && sortedData.length > 0 && (
|
|
1062
|
-
<div className="flex flex-col sm:flex-row items-center justify-between gap-2">
|
|
1063
|
-
<div className="flex items-center gap-2">
|
|
1064
|
-
<span className="text-xs sm:text-sm text-muted-foreground">{t('table.rowsPerPage')}:</span>
|
|
1065
|
-
<Select
|
|
1066
|
-
value={pageSize.toString()}
|
|
1067
|
-
onValueChange={(value) => {
|
|
1068
|
-
setPageSize(Number(value));
|
|
1069
|
-
setCurrentPage(1);
|
|
1070
|
-
}}
|
|
1071
|
-
>
|
|
1072
|
-
<SelectTrigger className="w-20">
|
|
1073
|
-
<SelectValue />
|
|
1074
|
-
</SelectTrigger>
|
|
1075
|
-
<SelectContent>
|
|
1076
|
-
<SelectItem value="5">5</SelectItem>
|
|
1077
|
-
<SelectItem value="10">10</SelectItem>
|
|
1078
|
-
<SelectItem value="20">20</SelectItem>
|
|
1079
|
-
<SelectItem value="50">50</SelectItem>
|
|
1080
|
-
<SelectItem value="100">100</SelectItem>
|
|
1081
|
-
</SelectContent>
|
|
1082
|
-
</Select>
|
|
1083
|
-
</div>
|
|
1084
|
-
|
|
1085
|
-
<div className="flex items-center gap-2">
|
|
1086
|
-
<span className="text-xs sm:text-sm text-muted-foreground">
|
|
1087
|
-
{t('table.pageInfo', { current: currentPage, total: totalPages })} <span className="hidden sm:inline">({t('table.totalRecords', { count: sortedData.length })})</span>
|
|
1088
|
-
</span>
|
|
1089
|
-
<div className="flex items-center gap-1">
|
|
1090
|
-
<Button
|
|
1091
|
-
variant="outline"
|
|
1092
|
-
size="icon-sm"
|
|
1093
|
-
onClick={() => setCurrentPage(1)}
|
|
1094
|
-
disabled={currentPage === 1}
|
|
1095
|
-
>
|
|
1096
|
-
<ChevronsLeft className="h-4 w-4" />
|
|
1097
|
-
</Button>
|
|
1098
|
-
<Button
|
|
1099
|
-
variant="outline"
|
|
1100
|
-
size="icon-sm"
|
|
1101
|
-
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
1102
|
-
disabled={currentPage === 1}
|
|
1103
|
-
>
|
|
1104
|
-
<ChevronLeft className="h-4 w-4" />
|
|
1105
|
-
</Button>
|
|
1106
|
-
<Button
|
|
1107
|
-
variant="outline"
|
|
1108
|
-
size="icon-sm"
|
|
1109
|
-
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
|
1110
|
-
disabled={currentPage === totalPages}
|
|
1111
|
-
>
|
|
1112
|
-
<ChevronRight className="h-4 w-4" />
|
|
1113
|
-
</Button>
|
|
1114
|
-
<Button
|
|
1115
|
-
variant="outline"
|
|
1116
|
-
size="icon-sm"
|
|
1117
|
-
onClick={() => setCurrentPage(totalPages)}
|
|
1118
|
-
disabled={currentPage === totalPages}
|
|
1119
|
-
>
|
|
1120
|
-
<ChevronsRight className="h-4 w-4" />
|
|
1121
|
-
</Button>
|
|
1122
|
-
</div>
|
|
1123
|
-
</div>
|
|
1124
|
-
</div>
|
|
1125
|
-
)}
|
|
1126
|
-
|
|
1127
|
-
{/* Column header context menu */}
|
|
1128
|
-
{contextMenu && (
|
|
1129
|
-
<div
|
|
1130
|
-
className="fixed z-50 min-w-[160px] rounded-md border bg-popover p-1 shadow-md"
|
|
1131
|
-
style={{ left: contextMenu.x, top: contextMenu.y }}
|
|
1132
|
-
data-testid="column-context-menu"
|
|
1133
|
-
onClick={(e) => e.stopPropagation()}
|
|
1134
|
-
>
|
|
1135
|
-
{sortable && (
|
|
1136
|
-
<>
|
|
1137
|
-
<button
|
|
1138
|
-
type="button"
|
|
1139
|
-
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
|
1140
|
-
onClick={() => {
|
|
1141
|
-
setSortColumn(contextMenu.columnKey);
|
|
1142
|
-
setSortDirection('asc');
|
|
1143
|
-
setContextMenu(null);
|
|
1144
|
-
}}
|
|
1145
|
-
>
|
|
1146
|
-
<ChevronUp className="h-3.5 w-3.5" />
|
|
1147
|
-
{t('table.sortAsc')}
|
|
1148
|
-
</button>
|
|
1149
|
-
<button
|
|
1150
|
-
type="button"
|
|
1151
|
-
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
|
1152
|
-
onClick={() => {
|
|
1153
|
-
setSortColumn(contextMenu.columnKey);
|
|
1154
|
-
setSortDirection('desc');
|
|
1155
|
-
setContextMenu(null);
|
|
1156
|
-
}}
|
|
1157
|
-
>
|
|
1158
|
-
<ChevronDown className="h-3.5 w-3.5" />
|
|
1159
|
-
{t('table.sortDesc')}
|
|
1160
|
-
</button>
|
|
1161
|
-
<div className="my-1 h-px bg-border" />
|
|
1162
|
-
</>
|
|
1163
|
-
)}
|
|
1164
|
-
<button
|
|
1165
|
-
type="button"
|
|
1166
|
-
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
|
1167
|
-
onClick={() => hideColumn(contextMenu.columnKey)}
|
|
1168
|
-
>
|
|
1169
|
-
<X className="h-3.5 w-3.5" />
|
|
1170
|
-
{t('table.hideColumn')}
|
|
1171
|
-
</button>
|
|
1172
|
-
</div>
|
|
1173
|
-
)}
|
|
1174
|
-
</div>
|
|
1175
|
-
);
|
|
1176
|
-
};
|
|
1177
|
-
|
|
1178
|
-
// Register the component
|
|
1179
|
-
ComponentRegistry.register('data-table', DataTableRenderer, {
|
|
1180
|
-
namespace: 'ui',
|
|
1181
|
-
label: 'Data Table',
|
|
1182
|
-
icon: 'table',
|
|
1183
|
-
inputs: [
|
|
1184
|
-
{ name: 'caption', type: 'string', label: 'Caption' },
|
|
1185
|
-
{
|
|
1186
|
-
name: 'columns',
|
|
1187
|
-
type: 'array',
|
|
1188
|
-
label: 'Columns',
|
|
1189
|
-
description: 'Array of { header, accessorKey, className, width, sortable, filterable, resizable }',
|
|
1190
|
-
required: true,
|
|
1191
|
-
},
|
|
1192
|
-
{
|
|
1193
|
-
name: 'data',
|
|
1194
|
-
type: 'array',
|
|
1195
|
-
label: 'Data',
|
|
1196
|
-
description: 'Array of data objects',
|
|
1197
|
-
required: true,
|
|
1198
|
-
},
|
|
1199
|
-
{ name: 'pagination', type: 'boolean', label: 'Enable Pagination', defaultValue: true },
|
|
1200
|
-
{ name: 'pageSize', type: 'number', label: 'Page Size', defaultValue: 10 },
|
|
1201
|
-
{ name: 'searchable', type: 'boolean', label: 'Enable Search', defaultValue: true },
|
|
1202
|
-
{ name: 'selectable', type: 'boolean', label: 'Enable Row Selection', defaultValue: false },
|
|
1203
|
-
{ name: 'sortable', type: 'boolean', label: 'Enable Sorting', defaultValue: true },
|
|
1204
|
-
{ name: 'exportable', type: 'boolean', label: 'Enable Export', defaultValue: false },
|
|
1205
|
-
{ name: 'rowActions', type: 'boolean', label: 'Show Row Actions', defaultValue: false },
|
|
1206
|
-
{ name: 'resizableColumns', type: 'boolean', label: 'Enable Column Resizing', defaultValue: true },
|
|
1207
|
-
{ name: 'reorderableColumns', type: 'boolean', label: 'Enable Column Reordering', defaultValue: true },
|
|
1208
|
-
{ name: 'className', type: 'string', label: 'CSS Class' },
|
|
1209
|
-
],
|
|
1210
|
-
defaultProps: {
|
|
1211
|
-
caption: 'Enterprise Data Table',
|
|
1212
|
-
pagination: true,
|
|
1213
|
-
pageSize: 10,
|
|
1214
|
-
searchable: true,
|
|
1215
|
-
selectable: true,
|
|
1216
|
-
sortable: true,
|
|
1217
|
-
exportable: true,
|
|
1218
|
-
rowActions: true,
|
|
1219
|
-
resizableColumns: true,
|
|
1220
|
-
reorderableColumns: true,
|
|
1221
|
-
columns: [
|
|
1222
|
-
{ header: 'ID', accessorKey: 'id', width: '80px' },
|
|
1223
|
-
{ header: 'Name', accessorKey: 'name' },
|
|
1224
|
-
{ header: 'Email', accessorKey: 'email' },
|
|
1225
|
-
{ header: 'Status', accessorKey: 'status' },
|
|
1226
|
-
{ header: 'Role', accessorKey: 'role' },
|
|
1227
|
-
],
|
|
1228
|
-
data: [
|
|
1229
|
-
{ id: 1, name: 'John Doe', email: 'john@example.com', status: 'Active', role: 'Admin' },
|
|
1230
|
-
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'Active', role: 'User' },
|
|
1231
|
-
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', status: 'Inactive', role: 'User' },
|
|
1232
|
-
{ id: 4, name: 'Alice Williams', email: 'alice@example.com', status: 'Active', role: 'Manager' },
|
|
1233
|
-
{ id: 5, name: 'Charlie Brown', email: 'charlie@example.com', status: 'Active', role: 'User' },
|
|
1234
|
-
{ id: 6, name: 'Diana Prince', email: 'diana@example.com', status: 'Active', role: 'Admin' },
|
|
1235
|
-
{ id: 7, name: 'Ethan Hunt', email: 'ethan@example.com', status: 'Inactive', role: 'User' },
|
|
1236
|
-
{ id: 8, name: 'Fiona Gallagher', email: 'fiona@example.com', status: 'Active', role: 'User' },
|
|
1237
|
-
{ id: 9, name: 'George Wilson', email: 'george@example.com', status: 'Active', role: 'Manager' },
|
|
1238
|
-
{ id: 10, name: 'Hannah Montana', email: 'hannah@example.com', status: 'Active', role: 'User' },
|
|
1239
|
-
{ id: 11, name: 'Ivan Drago', email: 'ivan@example.com', status: 'Inactive', role: 'User' },
|
|
1240
|
-
{ id: 12, name: 'Julia Roberts', email: 'julia@example.com', status: 'Active', role: 'Admin' },
|
|
1241
|
-
],
|
|
1242
|
-
},
|
|
1243
|
-
});
|