@exxatdesignux/ui 0.2.18 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -1
- package/bin/sync-extras.mjs +116 -29
- package/consumer-extras/README.md +43 -4
- package/consumer-extras/cursor-rules/exxat-accessibility.mdc +39 -0
- package/consumer-extras/cursor-rules/exxat-board-cards.mdc +26 -0
- package/consumer-extras/cursor-rules/exxat-breadcrumbs-no-back.mdc +21 -0
- package/consumer-extras/cursor-rules/exxat-card-vs-list-rows.mdc +21 -0
- package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +44 -0
- package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +32 -0
- package/consumer-extras/cursor-rules/exxat-command-menu.mdc +22 -0
- package/consumer-extras/cursor-rules/exxat-dashboard-view-charts.mdc +53 -0
- package/consumer-extras/cursor-rules/exxat-data-tables.mdc +41 -0
- package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +25 -0
- package/consumer-extras/cursor-rules/exxat-drawer-vs-dialog.mdc +22 -0
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +56 -0
- package/consumer-extras/cursor-rules/exxat-fontawesome-icons.mdc +31 -0
- package/consumer-extras/cursor-rules/exxat-kbd-shortcuts.mdc +100 -0
- package/consumer-extras/cursor-rules/exxat-kpi-flat-band.mdc +28 -0
- package/consumer-extras/cursor-rules/exxat-kpi-max-four.mdc +21 -0
- package/consumer-extras/cursor-rules/exxat-kpi-trends.mdc +31 -0
- package/consumer-extras/cursor-rules/exxat-list-page-connected-views.mdc +24 -0
- package/consumer-extras/cursor-rules/exxat-list-page-view-shells.mdc +31 -0
- package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +30 -0
- package/consumer-extras/cursor-rules/exxat-no-slds-leakage.mdc +78 -0
- package/consumer-extras/cursor-rules/exxat-no-toast.mdc +25 -0
- package/consumer-extras/cursor-rules/exxat-page-vs-drawer.mdc +23 -0
- package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +47 -0
- package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +52 -0
- package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +28 -0
- package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +34 -0
- package/consumer-extras/cursor-rules/exxat-table-properties-drawer.mdc +77 -0
- package/consumer-extras/cursor-rules/exxat-token-discipline.mdc +103 -0
- package/consumer-extras/cursor-skills/exxat-accessibility/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +9 -9
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +1 -1
- package/consumer-extras/handbook/HANDBOOK.md +185 -0
- package/consumer-extras/handbook/glossary.md +57 -0
- package/consumer-extras/handbook/reference-implementations.md +126 -0
- package/consumer-extras/handbook/voice-and-tone.md +262 -0
- package/consumer-extras/patterns/command-menu-pattern.md +1 -1
- package/consumer-extras/patterns/data-views-pattern.md +14 -14
- package/dist/components/data-table/filter-date-calendar.d.ts +10 -0
- package/dist/components/data-table/filter-date-calendar.js +280 -0
- package/dist/components/data-table/filter-date-calendar.js.map +1 -0
- package/dist/components/data-table/filter-text-value-input.d.ts +15 -0
- package/dist/components/data-table/filter-text-value-input.js +561 -0
- package/dist/components/data-table/filter-text-value-input.js.map +1 -0
- package/dist/components/data-table/index.d.ts +45 -0
- package/dist/components/data-table/index.js +3085 -0
- package/dist/components/data-table/index.js.map +1 -0
- package/dist/components/data-table/pagination.d.ts +28 -0
- package/dist/components/data-table/pagination.js +3264 -0
- package/dist/components/data-table/pagination.js.map +1 -0
- package/dist/components/data-table/types.d.ts +84 -0
- package/dist/components/data-table/types.js +3 -0
- package/dist/components/data-table/types.js.map +1 -0
- package/dist/components/data-table/use-table-state.d.ts +116 -0
- package/dist/components/data-table/use-table-state.js +670 -0
- package/dist/components/data-table/use-table-state.js.map +1 -0
- package/dist/components/data-views/board-card-primitives.d.ts +22 -0
- package/dist/components/data-views/board-card-primitives.js +84 -0
- package/dist/components/data-views/board-card-primitives.js.map +1 -0
- package/dist/components/data-views/data-row-list.d.ts +33 -0
- package/dist/components/data-views/data-row-list.js +106 -0
- package/dist/components/data-views/data-row-list.js.map +1 -0
- package/dist/components/data-views/finder-panel-view.d.ts +54 -0
- package/dist/components/data-views/finder-panel-view.js +388 -0
- package/dist/components/data-views/finder-panel-view.js.map +1 -0
- package/dist/components/data-views/folder-grid-view.d.ts +22 -0
- package/dist/components/data-views/folder-grid-view.js +58 -0
- package/dist/components/data-views/folder-grid-view.js.map +1 -0
- package/dist/components/data-views/hub-table.d.ts +167 -0
- package/dist/components/data-views/hub-table.js +5561 -0
- package/dist/components/data-views/hub-table.js.map +1 -0
- package/dist/components/data-views/index.d.ts +27 -0
- package/dist/components/data-views/index.js +6575 -0
- package/dist/components/data-views/index.js.map +1 -0
- package/dist/components/data-views/list-page-board-card.d.ts +72 -0
- package/dist/components/data-views/list-page-board-card.js +264 -0
- package/dist/components/data-views/list-page-board-card.js.map +1 -0
- package/dist/components/data-views/list-page-board-template.d.ts +24 -0
- package/dist/components/data-views/list-page-board-template.js +137 -0
- package/dist/components/data-views/list-page-board-template.js.map +1 -0
- package/dist/components/data-views/list-page-connected-view-body.d.ts +19 -0
- package/dist/components/data-views/list-page-connected-view-body.js +116 -0
- package/dist/components/data-views/list-page-connected-view-body.js.map +1 -0
- package/dist/components/data-views/list-page-split-details-placeholder.d.ts +14 -0
- package/dist/components/data-views/list-page-split-details-placeholder.js +38 -0
- package/dist/components/data-views/list-page-split-details-placeholder.js.map +1 -0
- package/dist/components/data-views/list-page-split-hub-chrome.d.ts +17 -0
- package/dist/components/data-views/list-page-split-hub-chrome.js +54 -0
- package/dist/components/data-views/list-page-split-hub-chrome.js.map +1 -0
- package/dist/components/data-views/list-page-split-hub-tokens.d.ts +12 -0
- package/dist/components/data-views/list-page-split-hub-tokens.js +8 -0
- package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -0
- package/dist/components/data-views/list-page-tree-column-header.d.ts +15 -0
- package/dist/components/data-views/list-page-tree-column-header.js +22 -0
- package/dist/components/data-views/list-page-tree-column-header.js.map +1 -0
- package/dist/components/data-views/list-page-tree-panel-shell.d.ts +25 -0
- package/dist/components/data-views/list-page-tree-panel-shell.js +146 -0
- package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -0
- package/dist/components/data-views/os-folder-glyph.d.ts +35 -0
- package/dist/components/data-views/os-folder-glyph.js +104 -0
- package/dist/components/data-views/os-folder-glyph.js.map +1 -0
- package/dist/components/data-views/outline-tree-menu.d.ts +36 -0
- package/dist/components/data-views/outline-tree-menu.js +131 -0
- package/dist/components/data-views/outline-tree-menu.js.map +1 -0
- package/dist/components/table-properties/column-row.d.ts +22 -0
- package/dist/components/table-properties/column-row.js +153 -0
- package/dist/components/table-properties/column-row.js.map +1 -0
- package/dist/components/table-properties/draggable-list.d.ts +24 -0
- package/dist/components/table-properties/draggable-list.js +53 -0
- package/dist/components/table-properties/draggable-list.js.map +1 -0
- package/dist/components/table-properties/drawer-button.d.ts +110 -0
- package/dist/components/table-properties/drawer-button.js +2748 -0
- package/dist/components/table-properties/drawer-button.js.map +1 -0
- package/dist/components/table-properties/drawer.d.ts +100 -0
- package/dist/components/table-properties/drawer.js +2595 -0
- package/dist/components/table-properties/drawer.js.map +1 -0
- package/dist/components/table-properties/filter-card.d.ts +24 -0
- package/dist/components/table-properties/filter-card.js +854 -0
- package/dist/components/table-properties/filter-card.js.map +1 -0
- package/dist/components/table-properties/index.d.ts +14 -0
- package/dist/components/table-properties/index.js +2768 -0
- package/dist/components/table-properties/index.js.map +1 -0
- package/dist/components/table-properties/sort-card.d.ts +20 -0
- package/dist/components/table-properties/sort-card.js +102 -0
- package/dist/components/table-properties/sort-card.js.map +1 -0
- package/dist/components/templates/dedicated-search-landing-template.d.ts +21 -0
- package/dist/components/templates/dedicated-search-landing-template.js +254 -0
- package/dist/components/templates/dedicated-search-landing-template.js.map +1 -0
- package/dist/components/templates/dedicated-search-results-template.d.ts +15 -0
- package/dist/components/templates/dedicated-search-results-template.js +16 -0
- package/dist/components/templates/dedicated-search-results-template.js.map +1 -0
- package/dist/components/templates/index.d.ts +9 -0
- package/dist/components/templates/index.js +2720 -0
- package/dist/components/templates/index.js.map +1 -0
- package/dist/components/templates/list-page.d.ts +83 -0
- package/dist/components/templates/list-page.js +2433 -0
- package/dist/components/templates/list-page.js.map +1 -0
- package/dist/components/templates/nested-secondary-panel-shell.d.ts +20 -0
- package/dist/components/templates/nested-secondary-panel-shell.js +54 -0
- package/dist/components/templates/nested-secondary-panel-shell.js.map +1 -0
- package/dist/components/ui/accordion.d.ts +10 -0
- package/dist/components/ui/accordion.js +74 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +37 -0
- package/dist/components/ui/alert-dialog.js +201 -0
- package/dist/components/ui/alert-dialog.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +84 -0
- package/dist/components/ui/avatar.js +328 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +13 -0
- package/dist/components/ui/badge.js +49 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/banner.d.ts +62 -0
- package/dist/components/ui/banner.js +364 -0
- package/dist/components/ui/banner.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +14 -0
- package/dist/components/ui/breadcrumb.js +114 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +16 -0
- package/dist/components/ui/button.js +59 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/calendar.d.ts +13 -0
- package/dist/components/ui/calendar.js +238 -0
- package/dist/components/ui/calendar.js.map +1 -0
- package/dist/components/ui/card.d.ts +14 -0
- package/dist/components/ui/card.js +102 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/chart.d.ts +58 -0
- package/dist/components/ui/chart.js +292 -0
- package/dist/components/ui/chart.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +23 -0
- package/dist/components/ui/checkbox.js +155 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/coach-mark.d.ts +27 -0
- package/dist/components/ui/coach-mark.js +306 -0
- package/dist/components/ui/coach-mark.js.map +1 -0
- package/dist/components/ui/collapsible.d.ts +8 -0
- package/dist/components/ui/collapsible.js +35 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/command.d.ts +36 -0
- package/dist/components/ui/command.js +274 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +32 -0
- package/dist/components/ui/context-menu.js +245 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/date-picker-field.d.ts +38 -0
- package/dist/components/ui/date-picker-field.js +550 -0
- package/dist/components/ui/date-picker-field.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +22 -0
- package/dist/components/ui/dialog.js +200 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/dot-pattern.d.ts +21 -0
- package/dist/components/ui/dot-pattern.js +139 -0
- package/dist/components/ui/dot-pattern.js.map +1 -0
- package/dist/components/ui/drag-handle-grip.d.ts +10 -0
- package/dist/components/ui/drag-handle-grip.js +15 -0
- package/dist/components/ui/drag-handle-grip.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +16 -0
- package/dist/components/ui/drawer.js +125 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +45 -0
- package/dist/components/ui/dropdown-menu.js +353 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/export-drawer.d.ts +11 -0
- package/dist/components/ui/export-drawer.js +1658 -0
- package/dist/components/ui/export-drawer.js.map +1 -0
- package/dist/components/ui/field.d.ts +30 -0
- package/dist/components/ui/field.js +249 -0
- package/dist/components/ui/field.js.map +1 -0
- package/dist/components/ui/form.d.ts +28 -0
- package/dist/components/ui/form.js +110 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/hover-card.d.ts +9 -0
- package/dist/components/ui/hover-card.js +43 -0
- package/dist/components/ui/hover-card.js.map +1 -0
- package/dist/components/ui/input-group.d.ts +20 -0
- package/dist/components/ui/input-group.js +219 -0
- package/dist/components/ui/input-group.js.map +1 -0
- package/dist/components/ui/input-mask.d.ts +39 -0
- package/dist/components/ui/input-mask.js +118 -0
- package/dist/components/ui/input-mask.js.map +1 -0
- package/dist/components/ui/input.d.ts +5 -0
- package/dist/components/ui/input.js +30 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/kbd.d.ts +20 -0
- package/dist/components/ui/kbd.js +45 -0
- package/dist/components/ui/kbd.js.map +1 -0
- package/dist/components/ui/key-metrics-context.d.ts +19 -0
- package/dist/components/ui/key-metrics-context.js +26 -0
- package/dist/components/ui/key-metrics-context.js.map +1 -0
- package/dist/components/ui/key-metrics.d.ts +131 -0
- package/dist/components/ui/key-metrics.js +1015 -0
- package/dist/components/ui/key-metrics.js.map +1 -0
- package/dist/components/ui/label.d.ts +6 -0
- package/dist/components/ui/label.js +28 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/list-page-view-frame.d.ts +22 -0
- package/dist/components/ui/list-page-view-frame.js +24 -0
- package/dist/components/ui/list-page-view-frame.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +51 -0
- package/dist/components/ui/page-header.js +372 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/payment-card-fields.d.ts +10 -0
- package/dist/components/ui/payment-card-fields.js +80 -0
- package/dist/components/ui/payment-card-fields.js.map +1 -0
- package/dist/components/ui/popover.d.ts +10 -0
- package/dist/components/ui/popover.js +47 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +29 -0
- package/dist/components/ui/radio-group.js +190 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/resizable.d.ts +16 -0
- package/dist/components/ui/resizable.js +51 -0
- package/dist/components/ui/resizable.js.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +8 -0
- package/dist/components/ui/scroll-area.js +66 -0
- package/dist/components/ui/scroll-area.js.map +1 -0
- package/dist/components/ui/select.d.ts +18 -0
- package/dist/components/ui/select.js +186 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/selection-tile-grid.d.ts +52 -0
- package/dist/components/ui/selection-tile-grid.js +347 -0
- package/dist/components/ui/selection-tile-grid.js.map +1 -0
- package/dist/components/ui/separator.d.ts +7 -0
- package/dist/components/ui/separator.js +33 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/sheet.d.ts +18 -0
- package/dist/components/ui/sheet.js +181 -0
- package/dist/components/ui/sheet.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +94 -0
- package/dist/components/ui/sidebar.js +805 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +5 -0
- package/dist/components/ui/skeleton.js +22 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/slider.d.ts +7 -0
- package/dist/components/ui/slider.js +66 -0
- package/dist/components/ui/slider.js.map +1 -0
- package/dist/components/ui/sonner.d.ts +6 -0
- package/dist/components/ui/sonner.js +38 -0
- package/dist/components/ui/sonner.js.map +1 -0
- package/dist/components/ui/status-badge.d.ts +38 -0
- package/dist/components/ui/status-badge.js +77 -0
- package/dist/components/ui/status-badge.js.map +1 -0
- package/dist/components/ui/table.d.ts +13 -0
- package/dist/components/ui/table.js +115 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +15 -0
- package/dist/components/ui/tabs.js +93 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +6 -0
- package/dist/components/ui/textarea.js +25 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/tip.d.ts +12 -0
- package/dist/components/ui/tip.js +61 -0
- package/dist/components/ui/tip.js.map +1 -0
- package/dist/components/ui/toggle-group.d.ts +14 -0
- package/dist/components/ui/toggle-group.js +104 -0
- package/dist/components/ui/toggle-group.js.map +1 -0
- package/dist/components/ui/toggle-switch.d.ts +10 -0
- package/dist/components/ui/toggle-switch.js +33 -0
- package/dist/components/ui/toggle-switch.js.map +1 -0
- package/dist/components/ui/toggle.d.ts +13 -0
- package/dist/components/ui/toggle.js +51 -0
- package/dist/components/ui/toggle.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +10 -0
- package/dist/components/ui/tooltip.js +68 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/components/ui/view-segmented-control.d.ts +31 -0
- package/dist/components/ui/view-segmented-control.js +167 -0
- package/dist/components/ui/view-segmented-control.js.map +1 -0
- package/dist/data-list-view-registry-CyBoBML4.d.ts +73 -0
- package/dist/hooks/use-app-theme.d.ts +24 -0
- package/dist/hooks/use-app-theme.js +286 -0
- package/dist/hooks/use-app-theme.js.map +1 -0
- package/dist/hooks/use-coach-mark.d.ts +86 -0
- package/dist/hooks/use-coach-mark.js +218 -0
- package/dist/hooks/use-coach-mark.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +3 -0
- package/dist/hooks/use-mobile.js +29 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/use-mod-key-label.d.ts +6 -0
- package/dist/hooks/use-mod-key-label.js +25 -0
- package/dist/hooks/use-mod-key-label.js.map +1 -0
- package/dist/index.d.ts +120 -0
- package/dist/index.js +13324 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/compose-refs.d.ts +6 -0
- package/dist/lib/compose-refs.js +17 -0
- package/dist/lib/compose-refs.js.map +1 -0
- package/dist/lib/conditional-rule-match.d.ts +30 -0
- package/dist/lib/conditional-rule-match.js +66 -0
- package/dist/lib/conditional-rule-match.js.map +1 -0
- package/dist/lib/data-list-display-options.d.ts +26 -0
- package/dist/lib/data-list-display-options.js +14 -0
- package/dist/lib/data-list-display-options.js.map +1 -0
- package/dist/lib/data-list-view-registry.d.ts +2 -0
- package/dist/lib/data-list-view-registry.js +102 -0
- package/dist/lib/data-list-view-registry.js.map +1 -0
- package/dist/lib/data-list-view-surface.d.ts +2 -0
- package/dist/lib/data-list-view-surface.js +80 -0
- package/dist/lib/data-list-view-surface.js.map +1 -0
- package/dist/lib/data-list-view.d.ts +21 -0
- package/dist/lib/data-list-view.js +25 -0
- package/dist/lib/data-list-view.js.map +1 -0
- package/dist/lib/date-filter.d.ts +22 -0
- package/dist/lib/date-filter.js +61 -0
- package/dist/lib/date-filter.js.map +1 -0
- package/dist/lib/dev-log.d.ts +8 -0
- package/dist/lib/dev-log.js +10 -0
- package/dist/lib/dev-log.js.map +1 -0
- package/dist/lib/dropdown-menu-surface.d.ts +14 -0
- package/dist/lib/dropdown-menu-surface.js +6 -0
- package/dist/lib/dropdown-menu-surface.js.map +1 -0
- package/dist/lib/editable-target.d.ts +12 -0
- package/dist/lib/editable-target.js +12 -0
- package/dist/lib/editable-target.js.map +1 -0
- package/dist/lib/list-page-table-properties.d.ts +35 -0
- package/dist/lib/list-page-table-properties.js +81 -0
- package/dist/lib/list-page-table-properties.js.map +1 -0
- package/dist/lib/raf-throttle.d.ts +23 -0
- package/dist/lib/raf-throttle.js +27 -0
- package/dist/lib/raf-throttle.js.map +1 -0
- package/dist/lib/row-height.d.ts +16 -0
- package/dist/lib/row-height.js +10 -0
- package/dist/lib/row-height.js.map +1 -0
- package/dist/lib/table-properties-types.d.ts +83 -0
- package/dist/lib/table-properties-types.js +19 -0
- package/dist/lib/table-properties-types.js.map +1 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +11 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +83 -18
- package/src/components/data-table/filter-date-calendar.tsx +38 -0
- package/src/components/data-table/filter-text-value-input.tsx +77 -0
- package/src/components/data-table/index.tsx +1678 -0
- package/src/components/data-table/pagination.tsx +255 -0
- package/src/components/data-table/types.ts +96 -0
- package/src/components/data-table/use-table-state.ts +767 -0
- package/src/components/data-views/board-card-primitives.tsx +93 -0
- package/src/components/data-views/data-row-list.tsx +183 -0
- package/src/components/data-views/finder-panel-view.tsx +405 -0
- package/src/components/data-views/folder-grid-view.tsx +86 -0
- package/src/components/data-views/hub-table.tsx +498 -0
- package/src/components/data-views/index.ts +28 -0
- package/src/components/data-views/list-page-board-card.tsx +192 -0
- package/src/components/data-views/list-page-board-template.tsx +122 -0
- package/src/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/src/components/data-views/list-page-split-details-placeholder.tsx +39 -0
- package/src/components/data-views/list-page-split-hub-chrome.tsx +60 -0
- package/src/components/data-views/list-page-split-hub-tokens.ts +16 -0
- package/src/components/data-views/list-page-tree-column-header.tsx +31 -0
- package/src/components/data-views/list-page-tree-panel-shell.tsx +91 -0
- package/src/components/data-views/os-folder-glyph.tsx +141 -0
- package/src/components/data-views/outline-tree-menu.tsx +157 -0
- package/src/components/table-properties/column-row.tsx +90 -0
- package/src/components/table-properties/draggable-list.ts +54 -0
- package/src/components/table-properties/drawer-button.tsx +300 -0
- package/src/components/table-properties/drawer.tsx +1148 -0
- package/src/components/table-properties/filter-card.tsx +251 -0
- package/src/components/table-properties/index.ts +36 -0
- package/src/components/table-properties/sort-card.tsx +63 -0
- package/src/components/templates/dedicated-search-landing-template.tsx +124 -0
- package/src/components/templates/dedicated-search-results-template.tsx +19 -0
- package/src/components/templates/index.ts +33 -0
- package/src/components/templates/list-page.tsx +602 -0
- package/src/components/templates/nested-secondary-panel-shell.tsx +70 -0
- package/src/components/ui/accordion.tsx +92 -0
- package/src/components/ui/alert-dialog.tsx +221 -0
- package/src/components/ui/avatar.tsx +13 -2
- package/src/components/ui/banner.tsx +2 -2
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/coach-mark.tsx +1 -1
- package/src/components/ui/context-menu.tsx +291 -0
- package/src/components/ui/date-picker-field.tsx +2 -2
- package/src/components/ui/dot-pattern.tsx +183 -0
- package/src/components/ui/export-drawer.tsx +375 -0
- package/src/components/ui/hover-card.tsx +66 -0
- package/src/components/ui/key-metrics-context.tsx +78 -0
- package/src/components/ui/key-metrics.tsx +1133 -0
- package/src/components/ui/list-page-view-frame.tsx +64 -0
- package/src/components/ui/page-header.tsx +244 -0
- package/src/components/ui/payment-card-fields.tsx +2 -2
- package/src/components/ui/resizable.tsx +68 -0
- package/src/components/ui/scroll-area.tsx +72 -0
- package/src/components/ui/selection-tile-grid.tsx +9 -2
- package/src/components/ui/sidebar.tsx +84 -12
- package/src/components/ui/slider.tsx +83 -0
- package/src/globals.css +494 -151
- package/src/globals.d.ts +20 -0
- package/src/index.ts +68 -1
- package/src/lib/conditional-rule-match.ts +119 -0
- package/src/lib/data-list-display-options.ts +35 -0
- package/src/lib/data-list-view-registry.ts +104 -0
- package/src/lib/data-list-view-surface.ts +83 -0
- package/src/lib/data-list-view.ts +47 -0
- package/src/lib/dev-log.ts +10 -0
- package/src/lib/editable-target.ts +20 -0
- package/src/lib/list-page-table-properties.ts +48 -0
- package/src/lib/raf-throttle.ts +45 -0
- package/src/lib/row-height.ts +19 -0
- package/src/lib/table-properties-types.ts +98 -0
- package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +3 -3
- package/template/.cursor/rules/exxat-data-tables.mdc +1 -1
- package/template/.cursor/rules/exxat-ds-agents.mdc +2 -2
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +2 -2
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
- package/template/AGENTS.md +84 -20
- package/template/app/(app)/examples/page.tsx +0 -1
- package/template/app/(app)/layout.tsx +17 -4
- package/template/app/(app)/question-bank/layout.tsx +1 -1
- package/template/app/(app)/question-bank/new/page.tsx +11 -24
- package/template/app/globals.css +13 -1972
- package/template/components/ask-leo-sidebar.tsx +5 -1
- package/template/components/brand-color-picker.tsx +2 -2
- package/template/components/charts-overview.tsx +1 -1
- package/template/components/compliance-table.tsx +240 -384
- package/template/components/dashboard-report-charts.tsx +1 -1
- package/template/components/dashboard-tabs.tsx +1 -1
- package/template/components/data-table/filter-date-calendar.tsx +1 -38
- package/template/components/data-table/filter-text-value-input.tsx +1 -77
- package/template/components/data-table/index.tsx +1 -1634
- package/template/components/data-table/pagination.tsx +1 -255
- package/template/components/data-table/types.ts +1 -94
- package/template/components/data-table/use-table-state.test.ts +420 -0
- package/template/components/data-table/use-table-state.ts +1 -758
- package/template/components/data-view-dashboard-charts-compliance.tsx +2 -2
- package/template/components/data-view-dashboard-charts-team.tsx +2 -2
- package/template/components/data-view-dashboard-charts.tsx +2 -2
- package/template/components/data-views/board-card-primitives.tsx +1 -93
- package/template/components/data-views/data-row-list.tsx +1 -183
- package/template/components/data-views/finder-panel-view.tsx +1 -405
- package/template/components/data-views/folder-grid-view.tsx +1 -86
- package/template/components/data-views/hub-table.tsx +1 -0
- package/template/components/data-views/index.ts +42 -1
- package/template/components/data-views/list-page-board-card.tsx +1 -192
- package/template/components/data-views/list-page-board-template.tsx +1 -122
- package/template/components/data-views/list-page-connected-view-body.tsx +1 -0
- package/template/components/data-views/list-page-split-details-placeholder.tsx +1 -39
- package/template/components/data-views/list-page-split-hub-chrome.tsx +1 -60
- package/template/components/data-views/list-page-split-hub-tokens.ts +1 -16
- package/template/components/data-views/list-page-tree-column-header.tsx +1 -31
- package/template/components/data-views/list-page-tree-panel-shell.tsx +1 -91
- package/template/components/data-views/list-page-view-frame.tsx +5 -53
- package/template/components/data-views/os-folder-glyph.tsx +1 -129
- package/template/components/data-views/outline-tree-menu.tsx +1 -157
- package/template/components/export-drawer.test.tsx +71 -0
- package/template/components/export-drawer.tsx +1 -375
- package/template/components/exxat-product-logo.tsx +5 -5
- package/template/components/hub-tree-panel-view.tsx +2 -2
- package/template/components/invite-collaborators-drawer.tsx +3 -3
- package/template/components/key-metrics-ask-leo-bridge.tsx +40 -0
- package/template/components/key-metrics.tsx +1 -1063
- package/template/components/leo-insight-indicator.tsx +2 -2
- package/template/components/new-placement-back-btn.tsx +1 -1
- package/template/components/new-placement-form.tsx +63 -189
- package/template/components/new-question-composer.tsx +432 -402
- package/template/components/onboarding/index.ts +9 -0
- package/template/components/onboarding/onboarding-01.tsx +1 -1
- package/template/components/onboarding/onboarding-02.tsx +1 -1
- package/template/components/onboarding/onboarding-03.tsx +1 -1
- package/template/components/onboarding/onboarding-04.tsx +1 -1
- package/template/components/page-header.tsx +8 -226
- package/template/components/placement-board-card.tsx +71 -83
- package/template/components/placements-board-view.tsx +3 -10
- package/template/components/placements-client.tsx +10 -42
- package/template/components/placements-list-view.tsx +22 -69
- package/template/components/placements-table-columns.tsx +8 -438
- package/template/components/placements-table.tsx +588 -1296
- package/template/components/product-switcher.tsx +1 -1
- package/template/components/product-wordmark.tsx +2 -1
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-hub-client.tsx +1 -1
- package/template/components/question-bank-new-folder-sheet.tsx +2 -2
- package/template/components/question-bank-secondary-nav.tsx +3 -3
- package/template/components/question-bank-table.tsx +294 -526
- package/template/components/rotations-empty-state.tsx +1 -1
- package/template/components/rotations-panel-activator.tsx +1 -1
- package/template/components/settings-appearance-card.tsx +1 -1
- package/template/components/{app-sidebar-dynamic.tsx → sidebar/app-sidebar-dynamic.tsx} +1 -1
- package/template/components/{app-sidebar.tsx → sidebar/app-sidebar.tsx} +4 -4
- package/template/components/sidebar/index.ts +16 -0
- package/template/components/{secondary-nav.tsx → sidebar/secondary-nav.tsx} +2 -2
- package/template/components/{secondary-panel.tsx → sidebar/secondary-panel.tsx} +6 -3
- package/template/components/{sidebar-auto-collapse.tsx → sidebar/sidebar-auto-collapse.tsx} +6 -2
- package/template/components/{sidebar-shell.tsx → sidebar/sidebar-shell.tsx} +1 -1
- package/template/components/site-header.tsx +1 -1
- package/template/components/{sites-all-client.tsx → sites-client.tsx} +1 -1
- package/template/components/sites-table.tsx +124 -257
- package/template/components/table-properties/column-row.tsx +1 -90
- package/template/components/table-properties/draggable-list.ts +1 -49
- package/template/components/table-properties/drawer-button.tsx +1 -249
- package/template/components/table-properties/drawer.tsx +1 -1105
- package/template/components/table-properties/filter-card.tsx +1 -251
- package/template/components/table-properties/sort-card.tsx +1 -59
- package/template/components/table-properties/types.ts +28 -71
- package/template/components/team-table.tsx +242 -382
- package/template/components/templates/dedicated-search-landing-template.tsx +1 -124
- package/template/components/templates/dedicated-search-results-template.tsx +1 -19
- package/template/components/templates/list-page.tsx +1 -584
- package/template/components/templates/nested-secondary-panel-shell.tsx +1 -62
- package/template/components/templates/new-focus-template.tsx +659 -0
- package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
- package/template/components/ui/accordion.tsx +1 -0
- package/template/components/ui/alert-dialog.tsx +1 -0
- package/template/components/ui/context-menu.tsx +1 -0
- package/template/components/ui/dot-pattern.tsx +1 -183
- package/template/components/ui/hover-card.tsx +1 -0
- package/template/components/ui/resizable.tsx +1 -68
- package/template/components/ui/scroll-area.tsx +1 -0
- package/template/components/ui/slider.tsx +1 -0
- package/template/docs/blueprints/README.md +86 -0
- package/template/docs/blueprints/_template.md +91 -0
- package/template/docs/blueprints/board-card.md +123 -0
- package/template/docs/blueprints/data-table.md +139 -0
- package/template/docs/blueprints/key-metrics.md +128 -0
- package/template/docs/blueprints/list-page-template.md +123 -0
- package/template/docs/blueprints/page-header.md +130 -0
- package/template/docs/command-menu-pattern.md +1 -1
- package/template/docs/component-selection-guide.md +224 -0
- package/template/docs/components-audit-2026-05.md +158 -0
- package/template/docs/data-views-pattern.md +14 -14
- package/template/docs/migrations/0001-brand-deep-alias-stabilization.md +95 -0
- package/template/docs/migrations/0002-exxat-token-namespace.md +154 -0
- package/template/docs/migrations/0003-globals-css-canonical.md +110 -0
- package/template/docs/migrations/README.md +100 -0
- package/template/docs/migrations/_template.md +64 -0
- package/template/docs/token-taxonomy.md +416 -0
- package/template/eslint.config.mjs +27 -0
- package/template/hooks/use-secondary-panel-hub-nav.ts +1 -1
- package/template/lib/command-menu-config.ts +0 -1
- package/template/lib/compliance-supported-views.ts +10 -0
- package/template/lib/conditional-rule-match.ts +6 -97
- package/template/lib/data-list-display-options.ts +1 -35
- package/template/lib/data-list-view-registry.ts +1 -0
- package/template/lib/data-list-view-surface.ts +1 -69
- package/template/lib/data-list-view.ts +1 -38
- package/template/lib/dev-log.ts +1 -8
- package/template/lib/editable-target.ts +1 -10
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +1 -52
- package/template/lib/mock/navigation.tsx +0 -8
- package/template/lib/mock/placements.ts +0 -7
- package/template/lib/placement-board-card-layout.ts +41 -41
- package/template/lib/placements-supported-views.ts +12 -0
- package/template/lib/question-bank-supported-views.ts +12 -0
- package/template/lib/raf-throttle.ts +1 -45
- package/template/lib/row-height.ts +4 -10
- package/template/lib/sidebar-state-cookie.ts +11 -2
- package/template/lib/sites-supported-views.ts +10 -0
- package/template/lib/team-supported-views.ts +10 -0
- package/template/package.json +1 -0
- package/template/tests/setup.ts +25 -0
- package/src/theme.css +0 -1132
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/app/(app)/data-list/page.tsx +0 -10
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/dashboard-onboarding-gallery.tsx +0 -13
- package/template/components/dashboard-onboarding.tsx +0 -21
- package/template/components/question-bank-list-view.tsx +0 -53
- package/template/components/section-cards.tsx +0 -106
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/team-list-view.tsx +0 -59
- package/template/lib/placement-lifecycle.ts +0 -5
- /package/template/components/{getting-started.tsx → onboarding/getting-started.tsx} +0 -0
- /package/template/components/{nav-documents.tsx → sidebar/nav-documents.tsx} +0 -0
- /package/template/components/{nav-main.tsx → sidebar/nav-main.tsx} +0 -0
- /package/template/components/{nav-secondary.tsx → sidebar/nav-secondary.tsx} +0 -0
- /package/template/components/{nav-user.tsx → sidebar/nav-user.tsx} +0 -0
- /package/template/components/{sidebar-auto-open.tsx → sidebar/sidebar-auto-open.tsx} +0 -0
|
@@ -1,1634 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* DataTable<TData> — generic reusable table (no pagination)
|
|
5
|
-
*
|
|
6
|
-
* Column features:
|
|
7
|
-
* • Resizable — drag right-edge handle on any non-locked column
|
|
8
|
-
* • Drag-to-reorder — drag header cell for free (unpinned) columns
|
|
9
|
-
* • Pin Left / Pin Right / Unpin — per-column context menu
|
|
10
|
-
* • Sort Asc / Desc — per-column context menu (sortable columns)
|
|
11
|
-
* • Wrap Text / Unwrap — per-column context menu
|
|
12
|
-
* • Per-column quick search
|
|
13
|
-
* • Row selection (checkboxes + floating bulk action bar)
|
|
14
|
-
* • Group by (collapsible group rows)
|
|
15
|
-
* • Hidden columns
|
|
16
|
-
*
|
|
17
|
-
* WCAG 2.1 AA:
|
|
18
|
-
* ✓ aria-sort on sortable <th>
|
|
19
|
-
* ✓ aria-label on every icon-only button
|
|
20
|
-
* ✓ Select / Actions columns: sr-only header text + resolved labels for controls
|
|
21
|
-
* ✓ Row checkboxes: visible on row focus-within, stop row click propagation (default control size; extended hit slop on Checkbox)
|
|
22
|
-
* ✓ Bulk-action bar: role="status" aria-live="polite"
|
|
23
|
-
* ✓ Resize handles: role="separator" aria-label
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import * as React from "react"
|
|
27
|
-
import { useTheme } from "next-themes"
|
|
28
|
-
import { createPortal } from "react-dom"
|
|
29
|
-
import { cn } from "@/lib/utils"
|
|
30
|
-
import { rafThrottle } from "@/lib/raf-throttle"
|
|
31
|
-
import { Button } from "@/components/ui/button"
|
|
32
|
-
import { Input } from "@/components/ui/input"
|
|
33
|
-
import { Kbd, KbdGroup } from "@/components/ui/kbd"
|
|
34
|
-
import { Tip } from "@/components/ui/tip"
|
|
35
|
-
import { useModKeyLabel } from "@/hooks/use-mod-key-label"
|
|
36
|
-
import { isEditableTarget } from "@/lib/editable-target"
|
|
37
|
-
import { Checkbox } from "@/components/ui/checkbox"
|
|
38
|
-
import {
|
|
39
|
-
DropdownMenu,
|
|
40
|
-
DropdownMenuContent,
|
|
41
|
-
DropdownMenuItem,
|
|
42
|
-
DropdownMenuLabel,
|
|
43
|
-
DropdownMenuSeparator,
|
|
44
|
-
DropdownMenuTrigger,
|
|
45
|
-
} from "@/components/ui/dropdown-menu"
|
|
46
|
-
import {
|
|
47
|
-
Popover,
|
|
48
|
-
PopoverAnchor,
|
|
49
|
-
PopoverContent,
|
|
50
|
-
PopoverTrigger,
|
|
51
|
-
} from "@/components/ui/popover"
|
|
52
|
-
import {
|
|
53
|
-
Tooltip,
|
|
54
|
-
TooltipContent,
|
|
55
|
-
TooltipProvider,
|
|
56
|
-
TooltipTrigger,
|
|
57
|
-
} from "@/components/ui/tooltip"
|
|
58
|
-
import { OPERATOR_LABELS } from "@/components/table-properties/types"
|
|
59
|
-
import type { ActiveFilter } from "@/components/table-properties/types"
|
|
60
|
-
import { getConditionalCellBackground } from "@/lib/conditional-rule-match"
|
|
61
|
-
import { formatYmdForDisplay } from "@/lib/date-filter"
|
|
62
|
-
import { FilterDateCalendar } from "@/components/data-table/filter-date-calendar"
|
|
63
|
-
import { FilterTextValueInput } from "@/components/data-table/filter-text-value-input"
|
|
64
|
-
import type { DataTableProps, ColumnDef, SortDir } from "./types"
|
|
65
|
-
import { useTableState } from "./use-table-state"
|
|
66
|
-
|
|
67
|
-
/** When `ColumnDef.label` is empty, use a standard name for select/actions columns. */
|
|
68
|
-
function defaultColumnHeaderLabel(key: string): string | undefined {
|
|
69
|
-
switch (key) {
|
|
70
|
-
case "select":
|
|
71
|
-
return "Select"
|
|
72
|
-
case "actions":
|
|
73
|
-
return "Actions"
|
|
74
|
-
default:
|
|
75
|
-
return undefined
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function resolvedColumnLabel<TData>(col: ColumnDef<TData>): string {
|
|
80
|
-
const t = col.label?.trim()
|
|
81
|
-
if (t) return t
|
|
82
|
-
return defaultColumnHeaderLabel(col.key) ?? col.key
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
-
// Internal sub-components
|
|
87
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
const SortChevron = React.memo(function SortChevron({ dir }: { dir: SortDir }) {
|
|
90
|
-
return (
|
|
91
|
-
<i className={`fa-solid fa-arrow-${dir === "asc" ? "up" : "down"} ml-1 text-xs`} aria-hidden="true" />
|
|
92
|
-
)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
-
// FilterPill — active filter pill with inline editor popover
|
|
97
|
-
// (driven by ColumnDef.filter config rather than FILTER_FIELDS)
|
|
98
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
interface FilterPillProps<TData> {
|
|
101
|
-
filter: ActiveFilter
|
|
102
|
-
columns: ColumnDef<TData>[]
|
|
103
|
-
defaultOpen?: boolean
|
|
104
|
-
onUpdate: (id: string, patch: Partial<ActiveFilter>) => void
|
|
105
|
-
onRemove: (id: string) => void
|
|
106
|
-
/** Optional custom cell renderer for filter option values */
|
|
107
|
-
renderOptionValue?: (fieldKey: string, value: string) => React.ReactNode
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function FilterPillBase<TData>({
|
|
111
|
-
filter,
|
|
112
|
-
columns,
|
|
113
|
-
defaultOpen = false,
|
|
114
|
-
onUpdate,
|
|
115
|
-
onRemove,
|
|
116
|
-
renderOptionValue,
|
|
117
|
-
}: FilterPillProps<TData>) {
|
|
118
|
-
const [open, setOpen] = React.useState(false)
|
|
119
|
-
const [optSearch, setOptSearch] = React.useState("")
|
|
120
|
-
const justAutoOpenedRef = React.useRef(false)
|
|
121
|
-
|
|
122
|
-
React.useEffect(() => {
|
|
123
|
-
if (defaultOpen) {
|
|
124
|
-
justAutoOpenedRef.current = true
|
|
125
|
-
const t = setTimeout(() => {
|
|
126
|
-
setOpen(true)
|
|
127
|
-
setTimeout(() => { justAutoOpenedRef.current = false }, 400)
|
|
128
|
-
}, 0)
|
|
129
|
-
return () => clearTimeout(t)
|
|
130
|
-
}
|
|
131
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
|
-
}, [])
|
|
133
|
-
|
|
134
|
-
const col = columns.find(c => c.key === filter.fieldKey)
|
|
135
|
-
const filterDef = col?.filter
|
|
136
|
-
|
|
137
|
-
React.useEffect(() => {
|
|
138
|
-
if (!filterDef) return
|
|
139
|
-
if (filterDef.type !== "select" && filterDef.type !== "date") return
|
|
140
|
-
if (filter.operator !== "is" && filter.operator !== "is_not") {
|
|
141
|
-
onUpdate(filter.id, { operator: "is" })
|
|
142
|
-
}
|
|
143
|
-
}, [filter.id, filterDef, filter.operator, onUpdate])
|
|
144
|
-
|
|
145
|
-
if (!filterDef) return null
|
|
146
|
-
|
|
147
|
-
const options = filterDef.options ?? []
|
|
148
|
-
const showSearch = options.length > 8
|
|
149
|
-
const filteredOpts = optSearch
|
|
150
|
-
? options.filter(o => o.label.toLowerCase().includes(optSearch.toLowerCase()))
|
|
151
|
-
: options
|
|
152
|
-
|
|
153
|
-
const operators = filterDef.operators ?? (
|
|
154
|
-
filterDef.type === "select" || filterDef.type === "date"
|
|
155
|
-
? (["is", "is_not"] as const)
|
|
156
|
-
: (["contains", "not_contains"] as const)
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
const valueLabel = (() => {
|
|
160
|
-
if (filterDef.type === "select") {
|
|
161
|
-
if (filter.values.length === 0) return "…"
|
|
162
|
-
if (filter.values.length === 1) {
|
|
163
|
-
return options.find(o => o.value === filter.values[0])?.label ?? filter.values[0]
|
|
164
|
-
}
|
|
165
|
-
return `${filter.values.length} selected`
|
|
166
|
-
}
|
|
167
|
-
if (filterDef.type === "date") {
|
|
168
|
-
const ymd = filter.values[0]
|
|
169
|
-
return ymd ? formatYmdForDisplay(ymd) : "…"
|
|
170
|
-
}
|
|
171
|
-
return filter.values[0] || "…"
|
|
172
|
-
})()
|
|
173
|
-
|
|
174
|
-
function toggleValue(val: string) {
|
|
175
|
-
const next = filter.values.includes(val)
|
|
176
|
-
? filter.values.filter(v => v !== val)
|
|
177
|
-
: [...filter.values, val]
|
|
178
|
-
onUpdate(filter.id, { values: next })
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function cycleOperator() {
|
|
182
|
-
const idx = operators.indexOf(filter.operator as typeof operators[number])
|
|
183
|
-
const i = idx === -1 ? 0 : idx
|
|
184
|
-
onUpdate(filter.id, { operator: operators[(i + 1) % operators.length] })
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const isActive =
|
|
188
|
-
filterDef.type === "date"
|
|
189
|
-
? Boolean(filter.values[0])
|
|
190
|
-
: filter.values.length > 0
|
|
191
|
-
const hasSelection = filter.values.length > 0
|
|
192
|
-
const iconClass = filterDef.icon ? `fa-light ${filterDef.icon}` : "fa-light fa-filter"
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
196
|
-
<PopoverAnchor asChild>
|
|
197
|
-
<div
|
|
198
|
-
className={cn(
|
|
199
|
-
"inline-flex cursor-pointer items-center rounded border text-xs transition-colors",
|
|
200
|
-
isActive ? "border-brand/45 bg-brand/10" : "border-input bg-background"
|
|
201
|
-
)}
|
|
202
|
-
>
|
|
203
|
-
<PopoverTrigger asChild>
|
|
204
|
-
<button
|
|
205
|
-
type="button"
|
|
206
|
-
className={cn(
|
|
207
|
-
"inline-flex cursor-pointer items-center gap-1 h-6 pl-2 pr-1.5 rounded-l transition-colors",
|
|
208
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
209
|
-
isActive ? "hover:bg-brand/15" : "hover:bg-interactive-hover",
|
|
210
|
-
)}
|
|
211
|
-
>
|
|
212
|
-
<i
|
|
213
|
-
className={cn(iconClass, "text-xs", isActive ? "text-brand" : "text-muted-foreground")}
|
|
214
|
-
aria-hidden="true"
|
|
215
|
-
/>
|
|
216
|
-
<span className="text-foreground">{col.label}</span>
|
|
217
|
-
{isActive && <span className="text-foreground font-medium">{valueLabel}</span>}
|
|
218
|
-
</button>
|
|
219
|
-
</PopoverTrigger>
|
|
220
|
-
<button
|
|
221
|
-
type="button"
|
|
222
|
-
aria-label={`Remove ${col.label} filter`}
|
|
223
|
-
onClick={() => onRemove(filter.id)}
|
|
224
|
-
className={cn(
|
|
225
|
-
"inline-flex cursor-pointer items-center justify-center h-6 w-5 rounded-r transition-colors",
|
|
226
|
-
"text-muted-foreground hover:text-destructive",
|
|
227
|
-
isActive ? "hover:bg-brand/15" : "hover:bg-interactive-hover",
|
|
228
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
229
|
-
)}
|
|
230
|
-
>
|
|
231
|
-
<i className="fa-light fa-xmark text-xs" aria-hidden="true" />
|
|
232
|
-
</button>
|
|
233
|
-
</div>
|
|
234
|
-
</PopoverAnchor>
|
|
235
|
-
|
|
236
|
-
<PopoverContent
|
|
237
|
-
className={cn(
|
|
238
|
-
"p-0",
|
|
239
|
-
filterDef.type === "date"
|
|
240
|
-
? "w-auto max-w-[min(calc(100vw-2rem),22rem)]"
|
|
241
|
-
: "w-64",
|
|
242
|
-
)}
|
|
243
|
-
align="start"
|
|
244
|
-
onFocusOutside={e => e.preventDefault()}
|
|
245
|
-
onInteractOutside={e => {
|
|
246
|
-
if (justAutoOpenedRef.current) {
|
|
247
|
-
e.preventDefault()
|
|
248
|
-
justAutoOpenedRef.current = false
|
|
249
|
-
}
|
|
250
|
-
}}
|
|
251
|
-
>
|
|
252
|
-
<div className="flex items-center justify-between px-3 py-2 border-b border-border">
|
|
253
|
-
<div className="flex items-center gap-1 text-sm text-foreground">
|
|
254
|
-
<span className="font-medium">{col.label}</span>
|
|
255
|
-
<button
|
|
256
|
-
type="button"
|
|
257
|
-
onClick={cycleOperator}
|
|
258
|
-
className="inline-flex items-center gap-0.5 text-muted-foreground hover:text-interactive-hover-foreground transition-colors rounded px-1 py-0.5 hover:bg-interactive-hover"
|
|
259
|
-
>
|
|
260
|
-
{OPERATOR_LABELS[filter.operator]}
|
|
261
|
-
<i className="fa-light fa-chevron-down text-xs" aria-hidden="true" />
|
|
262
|
-
</button>
|
|
263
|
-
</div>
|
|
264
|
-
<button
|
|
265
|
-
type="button"
|
|
266
|
-
aria-label="Remove filter"
|
|
267
|
-
onClick={() => onRemove(filter.id)}
|
|
268
|
-
className="text-muted-foreground hover:text-destructive transition-colors p-1 rounded hover:bg-interactive-hover"
|
|
269
|
-
>
|
|
270
|
-
<i className="fa-light fa-trash text-xs" aria-hidden="true" />
|
|
271
|
-
</button>
|
|
272
|
-
</div>
|
|
273
|
-
|
|
274
|
-
{filterDef.type === "date" && (
|
|
275
|
-
<div className="p-2">
|
|
276
|
-
<FilterDateCalendar
|
|
277
|
-
label={`${col.label} — choose date`}
|
|
278
|
-
valueYmd={filter.values[0]}
|
|
279
|
-
onChangeYmd={(ymd) =>
|
|
280
|
-
onUpdate(filter.id, { values: ymd ? [ymd] : [] })
|
|
281
|
-
}
|
|
282
|
-
/>
|
|
283
|
-
</div>
|
|
284
|
-
)}
|
|
285
|
-
|
|
286
|
-
{filterDef.type === "select" && (
|
|
287
|
-
<div className="py-1 max-h-64 overflow-y-auto">
|
|
288
|
-
{showSearch && (
|
|
289
|
-
<div className="px-2 pt-1 pb-1">
|
|
290
|
-
<div className="relative">
|
|
291
|
-
<Input
|
|
292
|
-
type="text"
|
|
293
|
-
placeholder="Search options…"
|
|
294
|
-
value={optSearch}
|
|
295
|
-
onChange={e => setOptSearch(e.target.value)}
|
|
296
|
-
className={cn("h-7 text-xs", optSearch ? "pr-8" : "pr-2")}
|
|
297
|
-
autoFocus
|
|
298
|
-
/>
|
|
299
|
-
{optSearch ? (
|
|
300
|
-
<button
|
|
301
|
-
type="button"
|
|
302
|
-
aria-label="Clear option search"
|
|
303
|
-
onClick={() => setOptSearch("")}
|
|
304
|
-
className="absolute right-1 top-1/2 -translate-y-1/2 inline-flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:text-interactive-hover-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
305
|
-
>
|
|
306
|
-
<i className="fa-light fa-xmark text-xs" aria-hidden="true" />
|
|
307
|
-
</button>
|
|
308
|
-
) : null}
|
|
309
|
-
</div>
|
|
310
|
-
</div>
|
|
311
|
-
)}
|
|
312
|
-
{filteredOpts.map(opt => {
|
|
313
|
-
const checked = filter.values.includes(opt.value)
|
|
314
|
-
return (
|
|
315
|
-
<div
|
|
316
|
-
key={opt.value}
|
|
317
|
-
role="option"
|
|
318
|
-
aria-selected={checked}
|
|
319
|
-
tabIndex={0}
|
|
320
|
-
onClick={() => toggleValue(opt.value)}
|
|
321
|
-
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleValue(opt.value) } }}
|
|
322
|
-
className="flex w-full items-center gap-2.5 px-3 py-1.5 text-sm hover:bg-interactive-hover transition-colors cursor-pointer select-none focus-visible:outline-none focus-visible:bg-interactive-hover"
|
|
323
|
-
>
|
|
324
|
-
<span
|
|
325
|
-
aria-hidden="true"
|
|
326
|
-
data-slot="checkbox"
|
|
327
|
-
data-state={checked ? "checked" : "unchecked"}
|
|
328
|
-
className={cn(
|
|
329
|
-
"inline-flex items-center justify-center size-3.5 shrink-0 rounded-[4px] border transition-colors",
|
|
330
|
-
checked ? "bg-primary border-primary text-primary-foreground" : "border-input bg-background"
|
|
331
|
-
)}
|
|
332
|
-
>
|
|
333
|
-
{checked && <i className="fa-solid fa-check text-current" style={{ fontSize: "8px" }} />}
|
|
334
|
-
</span>
|
|
335
|
-
{renderOptionValue
|
|
336
|
-
? renderOptionValue(filter.fieldKey, opt.value)
|
|
337
|
-
: <span className="text-foreground">{opt.label}</span>
|
|
338
|
-
}
|
|
339
|
-
</div>
|
|
340
|
-
)
|
|
341
|
-
})}
|
|
342
|
-
{filteredOpts.length === 0 && (
|
|
343
|
-
<p className="px-3 py-2 text-xs text-muted-foreground">No options found</p>
|
|
344
|
-
)}
|
|
345
|
-
</div>
|
|
346
|
-
)}
|
|
347
|
-
|
|
348
|
-
{filterDef.type === "text" && (
|
|
349
|
-
<div className="p-2">
|
|
350
|
-
<FilterTextValueInput
|
|
351
|
-
mask={filterDef.textMask}
|
|
352
|
-
placeholder={`Enter ${col.label.toLowerCase()}…`}
|
|
353
|
-
value={filter.values[0] ?? ""}
|
|
354
|
-
onValueChange={next => onUpdate(filter.id, { values: [next] })}
|
|
355
|
-
aria-label={`${col.label} filter value`}
|
|
356
|
-
className="h-8 text-xs focus-visible:border-ring focus-visible:ring-ring/50"
|
|
357
|
-
autoFocus
|
|
358
|
-
/>
|
|
359
|
-
</div>
|
|
360
|
-
)}
|
|
361
|
-
{hasSelection ? (
|
|
362
|
-
<div className="sticky bottom-0 border-t border-border bg-popover p-2">
|
|
363
|
-
<Button
|
|
364
|
-
type="button"
|
|
365
|
-
variant="outline"
|
|
366
|
-
size="sm"
|
|
367
|
-
onClick={() => onUpdate(filter.id, { values: [] })}
|
|
368
|
-
className="w-full justify-center gap-1.5 text-xs text-muted-foreground"
|
|
369
|
-
>
|
|
370
|
-
<i className="fa-light fa-xmark text-xs" aria-hidden="true" />
|
|
371
|
-
Clear selection
|
|
372
|
-
</Button>
|
|
373
|
-
</div>
|
|
374
|
-
) : null}
|
|
375
|
-
</PopoverContent>
|
|
376
|
-
</Popover>
|
|
377
|
-
)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// React.memo wrapper — preserves generic signature via cast.
|
|
381
|
-
// FilterPillBase is a pure function of its props; memoizing it prevents
|
|
382
|
-
// re-renders when unrelated table state (hover, scroll) changes.
|
|
383
|
-
const FilterPill = React.memo(FilterPillBase) as typeof FilterPillBase
|
|
384
|
-
|
|
385
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
386
|
-
// Sticky shadow utility
|
|
387
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
388
|
-
|
|
389
|
-
function stickyShadow(pin: "left" | "right" | undefined): string {
|
|
390
|
-
if (!pin) return ""
|
|
391
|
-
const base = "after:content-[''] after:absolute after:top-0 after:bottom-0 after:w-3 after:pointer-events-none"
|
|
392
|
-
if (pin === "left") {
|
|
393
|
-
return cn(
|
|
394
|
-
base,
|
|
395
|
-
"after:left-full",
|
|
396
|
-
"after:bg-[linear-gradient(to_right,var(--sticky-edge-fade),transparent)]",
|
|
397
|
-
)
|
|
398
|
-
}
|
|
399
|
-
return cn(
|
|
400
|
-
base,
|
|
401
|
-
"after:right-full",
|
|
402
|
-
"after:bg-[linear-gradient(to_left,var(--sticky-edge-fade),transparent)]",
|
|
403
|
-
)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
407
|
-
// DataTableToolbar — search, filter bar, properties slot (shared by table + board)
|
|
408
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
409
|
-
|
|
410
|
-
export function DataTableToolbar<TData extends Record<string, unknown>>({
|
|
411
|
-
state,
|
|
412
|
-
columns,
|
|
413
|
-
searchable = true,
|
|
414
|
-
/** When false, hides filter pills, search, and filter controls (e.g. dashboard canvas edit mode). */
|
|
415
|
-
showQueryControls = true,
|
|
416
|
-
renderFilterOptionValue,
|
|
417
|
-
toolbarSlot,
|
|
418
|
-
searchAriaLabel = "Search table",
|
|
419
|
-
}: {
|
|
420
|
-
state: ReturnType<typeof useTableState<TData>>
|
|
421
|
-
columns: ColumnDef<TData>[]
|
|
422
|
-
searchable?: boolean
|
|
423
|
-
showQueryControls?: boolean
|
|
424
|
-
renderFilterOptionValue?: (fieldKey: string, value: string) => React.ReactNode
|
|
425
|
-
toolbarSlot?: (state: ReturnType<typeof useTableState<TData>>) => React.ReactNode
|
|
426
|
-
/** Passed to the search input `aria-label` (e.g. "Search placements") */
|
|
427
|
-
searchAriaLabel?: string
|
|
428
|
-
}) {
|
|
429
|
-
const {
|
|
430
|
-
search, setSearch, searchOpen, setSearchOpen, searchRef,
|
|
431
|
-
activeFilters, setActiveFilters, openFilterId,
|
|
432
|
-
filterBarVisible, setFilterBarVisible,
|
|
433
|
-
addFilter, updateFilter, removeFilter,
|
|
434
|
-
} = state
|
|
435
|
-
|
|
436
|
-
const filterableCols = columns.filter(c => c.filter)
|
|
437
|
-
const searchModLabel = useModKeyLabel()
|
|
438
|
-
const effectiveSearchable = showQueryControls && searchable
|
|
439
|
-
|
|
440
|
-
React.useEffect(() => {
|
|
441
|
-
if (!effectiveSearchable) return
|
|
442
|
-
function onGlobalKeyDown(e: KeyboardEvent) {
|
|
443
|
-
if (!e.metaKey && !e.ctrlKey) return
|
|
444
|
-
if (e.altKey) return
|
|
445
|
-
if (e.key.toLowerCase() !== "k") return
|
|
446
|
-
if (isEditableTarget(e.target)) return
|
|
447
|
-
e.preventDefault()
|
|
448
|
-
setSearchOpen(true)
|
|
449
|
-
queueMicrotask(() => searchRef.current?.focus())
|
|
450
|
-
}
|
|
451
|
-
document.addEventListener("keydown", onGlobalKeyDown)
|
|
452
|
-
return () => document.removeEventListener("keydown", onGlobalKeyDown)
|
|
453
|
-
}, [effectiveSearchable, setSearchOpen, searchRef])
|
|
454
|
-
|
|
455
|
-
return (
|
|
456
|
-
<div
|
|
457
|
-
className={cn(
|
|
458
|
-
"flex items-center gap-1.5 px-4 lg:px-6",
|
|
459
|
-
showQueryControls ? "min-h-10 pt-2 pb-2" : "min-h-0 justify-end py-1.5",
|
|
460
|
-
)}
|
|
461
|
-
>
|
|
462
|
-
|
|
463
|
-
{showQueryControls && filterBarVisible && filterableCols.length > 0 && (
|
|
464
|
-
<div className="flex flex-wrap items-center gap-1.5 flex-1 min-w-0">
|
|
465
|
-
{activeFilters.map(filter => (
|
|
466
|
-
<React.Fragment key={filter.id}>
|
|
467
|
-
<FilterPill
|
|
468
|
-
filter={filter}
|
|
469
|
-
columns={columns}
|
|
470
|
-
defaultOpen={filter.id === openFilterId}
|
|
471
|
-
onUpdate={updateFilter}
|
|
472
|
-
onRemove={removeFilter}
|
|
473
|
-
renderOptionValue={renderFilterOptionValue}
|
|
474
|
-
/>
|
|
475
|
-
</React.Fragment>
|
|
476
|
-
))}
|
|
477
|
-
|
|
478
|
-
<DropdownMenu>
|
|
479
|
-
<DropdownMenuTrigger asChild>
|
|
480
|
-
<button type="button"
|
|
481
|
-
className="inline-flex cursor-pointer items-center gap-1 h-6 px-2 rounded text-xs text-muted-foreground hover:text-interactive-hover-foreground border border-dashed border-input/70 hover:border-input hover:bg-interactive-hover-subtle transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
482
|
-
>
|
|
483
|
-
<i className="fa-light fa-plus text-xs" aria-hidden="true" />
|
|
484
|
-
Add filter
|
|
485
|
-
</button>
|
|
486
|
-
</DropdownMenuTrigger>
|
|
487
|
-
<DropdownMenuContent align="start">
|
|
488
|
-
<DropdownMenuLabel className="text-xs">Filter by field</DropdownMenuLabel>
|
|
489
|
-
<DropdownMenuSeparator />
|
|
490
|
-
{filterableCols.map(c => (
|
|
491
|
-
<DropdownMenuItem key={c.key} onClick={() => addFilter(c.key)}>
|
|
492
|
-
{c.filter?.icon && <i className={`fa-light ${c.filter.icon}`} aria-hidden="true" />}
|
|
493
|
-
{c.label}
|
|
494
|
-
</DropdownMenuItem>
|
|
495
|
-
))}
|
|
496
|
-
</DropdownMenuContent>
|
|
497
|
-
</DropdownMenu>
|
|
498
|
-
|
|
499
|
-
{activeFilters.length > 0 && (
|
|
500
|
-
<button
|
|
501
|
-
type="button"
|
|
502
|
-
onClick={() => setActiveFilters([])}
|
|
503
|
-
className="cursor-pointer text-xs text-muted-foreground hover:text-interactive-hover-foreground transition-colors px-1"
|
|
504
|
-
>
|
|
505
|
-
Clear all
|
|
506
|
-
</button>
|
|
507
|
-
)}
|
|
508
|
-
</div>
|
|
509
|
-
)}
|
|
510
|
-
|
|
511
|
-
<div
|
|
512
|
-
className={cn(
|
|
513
|
-
"flex items-center gap-1 shrink-0",
|
|
514
|
-
showQueryControls && "ml-auto",
|
|
515
|
-
)}
|
|
516
|
-
>
|
|
517
|
-
|
|
518
|
-
{effectiveSearchable && (
|
|
519
|
-
searchOpen ? (
|
|
520
|
-
<div className="relative flex items-center">
|
|
521
|
-
<i className="fa-light fa-magnifying-glass absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground text-xs pointer-events-none" aria-hidden="true" />
|
|
522
|
-
<Input
|
|
523
|
-
ref={searchRef}
|
|
524
|
-
type="text"
|
|
525
|
-
role="searchbox"
|
|
526
|
-
inputMode="search"
|
|
527
|
-
autoComplete="off"
|
|
528
|
-
placeholder="Search…"
|
|
529
|
-
value={search}
|
|
530
|
-
onChange={e => setSearch(e.target.value)}
|
|
531
|
-
onBlur={() => { if (!search) setSearchOpen(false) }}
|
|
532
|
-
onKeyDown={e => { if (e.key === "Escape") { setSearch(""); setSearchOpen(false) } }}
|
|
533
|
-
className={cn("h-8 w-48 pl-7 text-xs", search ? "pr-8" : "pr-2")}
|
|
534
|
-
aria-label={searchAriaLabel}
|
|
535
|
-
/>
|
|
536
|
-
{search ? (
|
|
537
|
-
<button
|
|
538
|
-
type="button"
|
|
539
|
-
aria-label="Clear search"
|
|
540
|
-
onClick={() => setSearch("")}
|
|
541
|
-
className="absolute right-1.5 top-1/2 -translate-y-1/2 inline-flex cursor-pointer size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:text-interactive-hover-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
542
|
-
>
|
|
543
|
-
<i className="fa-light fa-xmark text-xs" aria-hidden="true" />
|
|
544
|
-
</button>
|
|
545
|
-
) : null}
|
|
546
|
-
</div>
|
|
547
|
-
) : (
|
|
548
|
-
<TooltipProvider>
|
|
549
|
-
<Tooltip>
|
|
550
|
-
<TooltipTrigger asChild>
|
|
551
|
-
<button type="button" aria-label="Search"
|
|
552
|
-
onClick={() => { setSearchOpen(true); setTimeout(() => searchRef.current?.focus(), 10) }}
|
|
553
|
-
className="inline-flex shrink-0 cursor-pointer items-center justify-center size-8 rounded-md text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
554
|
-
>
|
|
555
|
-
<i className="fa-light fa-magnifying-glass text-[13px]" aria-hidden="true" />
|
|
556
|
-
</button>
|
|
557
|
-
</TooltipTrigger>
|
|
558
|
-
<TooltipContent side="bottom">
|
|
559
|
-
<span>{searchAriaLabel}</span>
|
|
560
|
-
<KbdGroup>
|
|
561
|
-
<Kbd>{searchModLabel}</Kbd>
|
|
562
|
-
<Kbd>K</Kbd>
|
|
563
|
-
</KbdGroup>
|
|
564
|
-
</TooltipContent>
|
|
565
|
-
</Tooltip>
|
|
566
|
-
</TooltipProvider>
|
|
567
|
-
)
|
|
568
|
-
)}
|
|
569
|
-
|
|
570
|
-
{showQueryControls && filterableCols.length > 0 && (
|
|
571
|
-
<>
|
|
572
|
-
<div className="h-4 w-px bg-border/70" aria-hidden="true" />
|
|
573
|
-
<TooltipProvider>
|
|
574
|
-
<Tooltip>
|
|
575
|
-
<TooltipTrigger asChild>
|
|
576
|
-
{activeFilters.length > 0 ? (
|
|
577
|
-
<button type="button"
|
|
578
|
-
aria-label={filterBarVisible ? "Hide filters" : "Show filters"}
|
|
579
|
-
onClick={() => setFilterBarVisible(v => !v)}
|
|
580
|
-
className={cn(
|
|
581
|
-
"inline-flex shrink-0 cursor-pointer items-center gap-1 size-8 justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
582
|
-
filterBarVisible
|
|
583
|
-
? "bg-accent text-accent-foreground hover:bg-accent/90"
|
|
584
|
-
: "text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover",
|
|
585
|
-
)}
|
|
586
|
-
>
|
|
587
|
-
<i className="fa-light fa-filter text-[13px]" aria-hidden="true" />
|
|
588
|
-
<span className="text-xs font-semibold tabular-nums">{activeFilters.length}</span>
|
|
589
|
-
</button>
|
|
590
|
-
) : (
|
|
591
|
-
<DropdownMenu>
|
|
592
|
-
<DropdownMenuTrigger asChild>
|
|
593
|
-
<button type="button" aria-label="Add filter"
|
|
594
|
-
onClick={() => setFilterBarVisible(true)}
|
|
595
|
-
className="inline-flex shrink-0 cursor-pointer items-center justify-center size-8 rounded-md text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
596
|
-
>
|
|
597
|
-
<i className="fa-light fa-filter text-[13px]" aria-hidden="true" />
|
|
598
|
-
</button>
|
|
599
|
-
</DropdownMenuTrigger>
|
|
600
|
-
<DropdownMenuContent align="end">
|
|
601
|
-
<DropdownMenuLabel className="text-xs">Filter by field</DropdownMenuLabel>
|
|
602
|
-
<DropdownMenuSeparator />
|
|
603
|
-
{filterableCols.map(c => (
|
|
604
|
-
<DropdownMenuItem key={c.key} onClick={() => addFilter(c.key)}>
|
|
605
|
-
{c.filter?.icon && <i className={`fa-light ${c.filter.icon}`} aria-hidden="true" />}
|
|
606
|
-
{c.label}
|
|
607
|
-
</DropdownMenuItem>
|
|
608
|
-
))}
|
|
609
|
-
</DropdownMenuContent>
|
|
610
|
-
</DropdownMenu>
|
|
611
|
-
)}
|
|
612
|
-
</TooltipTrigger>
|
|
613
|
-
<TooltipContent side="bottom">
|
|
614
|
-
{activeFilters.length > 0
|
|
615
|
-
? (filterBarVisible ? "Hide filters" : "Show filters")
|
|
616
|
-
: "Filter"}
|
|
617
|
-
</TooltipContent>
|
|
618
|
-
</Tooltip>
|
|
619
|
-
</TooltipProvider>
|
|
620
|
-
</>
|
|
621
|
-
)}
|
|
622
|
-
|
|
623
|
-
{toolbarSlot && toolbarSlot(state)}
|
|
624
|
-
</div>
|
|
625
|
-
</div>
|
|
626
|
-
)
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
630
|
-
// DataTable<TData>
|
|
631
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
632
|
-
|
|
633
|
-
export interface DataTableExtendedProps<TData extends Record<string, unknown>>
|
|
634
|
-
extends DataTableProps<TData> {
|
|
635
|
-
/** Slot for a toolbar drawer button + drawer itself (e.g. TablePropertiesDrawer) */
|
|
636
|
-
toolbarSlot?: (state: ReturnType<typeof useTableState<TData>>) => React.ReactNode
|
|
637
|
-
/** Slot rendered inside the floating bulk-action bar (after the "N selected" label) */
|
|
638
|
-
bulkActionsSlot?: (selected: Set<string | number>, rows: TData[]) => React.ReactNode
|
|
639
|
-
/** Optional "add new row" row text — pass false to hide */
|
|
640
|
-
addRowLabel?: string | false
|
|
641
|
-
/** Custom option-value renderer for filter pills */
|
|
642
|
-
renderFilterOptionValue?: (fieldKey: string, value: string) => React.ReactNode
|
|
643
|
-
/** When set by DataTablePaginated — drives row slicing inside useTableState */
|
|
644
|
-
paginationOverride?: { page: number; pageSize: number }
|
|
645
|
-
/** When true, removes rounded bottom corners so a pagination bar can attach flush */
|
|
646
|
-
hasFooter?: boolean
|
|
647
|
-
/** Conditional formatting rules — apply bg color to cells based on value */
|
|
648
|
-
conditionalRules?: import("./types").ConditionalRule[]
|
|
649
|
-
/** When false, the column header row is hidden (Display options). */
|
|
650
|
-
showColumnHeaders?: boolean
|
|
651
|
-
/** When set, table uses this state (e.g. shared with board view) instead of internal useTableState. */
|
|
652
|
-
state?: ReturnType<typeof useTableState<TData>>
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
type DataTableInnerProps<TData extends Record<string, unknown>> = DataTableExtendedProps<TData> & {
|
|
656
|
-
state: ReturnType<typeof useTableState<TData>>
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/** Max width for bulk bar in normal (non-reflow) zoom — ~28rem, centered in table. */
|
|
660
|
-
const BULK_BAR_MAX_PX = 448
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* When the app theme is `dark`, the bulk strip is a **light** surface; shadcn
|
|
664
|
-
* “dark:” button tokens are wrong — reapply light-look solid/outline/destructive/ghost.
|
|
665
|
-
*/
|
|
666
|
-
const BULK_BAR_ON_LIGHT_STRIP = cn(
|
|
667
|
-
"[&_button[data-variant=default]]:bg-zinc-900 [&_button[data-variant=default]]:text-zinc-50",
|
|
668
|
-
"hover:[&_button[data-variant=default]]:bg-zinc-800",
|
|
669
|
-
"[&_button[data-variant=outline]]:border-zinc-300/80 [&_button[data-variant=outline]]:bg-white [&_button[data-variant=outline]]:text-zinc-900",
|
|
670
|
-
"hover:[&_button[data-variant=outline]]:bg-zinc-100",
|
|
671
|
-
"[&_button[data-variant=destructive]]:border-rose-200/80 [&_button[data-variant=destructive]]:bg-rose-100 [&_button[data-variant=destructive]]:text-rose-800",
|
|
672
|
-
"hover:[&_button[data-variant=destructive]]:bg-rose-200/40",
|
|
673
|
-
"[&_button[data-variant=ghost]]:text-zinc-600 hover:[&_button[data-variant=ghost]]:bg-zinc-200/70 hover:[&_button[data-variant=ghost]]:text-zinc-900",
|
|
674
|
-
)
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Pins the bulk bar to the viewport bottom, aligned to the table scroll
|
|
678
|
-
* wrapper. When `fullWidth` is false (normal zoom), width is
|
|
679
|
-
* `min(tableWidth, 28rem)` and centered; when true (reflow), matches table
|
|
680
|
-
* width.
|
|
681
|
-
*/
|
|
682
|
-
function useBulkBarFixedToTableScrollEl(
|
|
683
|
-
scrollRef: React.RefObject<HTMLDivElement | null>,
|
|
684
|
-
active: boolean,
|
|
685
|
-
fullWidth: boolean,
|
|
686
|
-
): React.CSSProperties | undefined {
|
|
687
|
-
const [style, setStyle] = React.useState<React.CSSProperties | undefined>(undefined)
|
|
688
|
-
React.useLayoutEffect(() => {
|
|
689
|
-
if (!active) {
|
|
690
|
-
setStyle(undefined)
|
|
691
|
-
return
|
|
692
|
-
}
|
|
693
|
-
const el = scrollRef.current
|
|
694
|
-
if (!el) {
|
|
695
|
-
setStyle(undefined)
|
|
696
|
-
return
|
|
697
|
-
}
|
|
698
|
-
const apply = () => {
|
|
699
|
-
const r = el.getBoundingClientRect()
|
|
700
|
-
let left = r.left
|
|
701
|
-
let width = r.width
|
|
702
|
-
if (!fullWidth) {
|
|
703
|
-
const w = Math.min(r.width, BULK_BAR_MAX_PX)
|
|
704
|
-
left = r.left + (r.width - w) / 2
|
|
705
|
-
width = w
|
|
706
|
-
}
|
|
707
|
-
setStyle({
|
|
708
|
-
position: "fixed",
|
|
709
|
-
left,
|
|
710
|
-
width,
|
|
711
|
-
bottom: "max(0.5rem, env(safe-area-inset-bottom, 0px))",
|
|
712
|
-
zIndex: 50,
|
|
713
|
-
boxSizing: "border-box",
|
|
714
|
-
margin: 0,
|
|
715
|
-
right: "auto",
|
|
716
|
-
})
|
|
717
|
-
}
|
|
718
|
-
apply()
|
|
719
|
-
// rAF-coalesce so a single frame handles bursts of capture-phase scroll
|
|
720
|
-
// events plus the ResizeObserver firing — instead of N getBoundingClientRect
|
|
721
|
-
// + setState per second.
|
|
722
|
-
const scheduled = rafThrottle(apply)
|
|
723
|
-
const ro = new ResizeObserver(scheduled)
|
|
724
|
-
ro.observe(el)
|
|
725
|
-
window.addEventListener("resize", scheduled, { passive: true })
|
|
726
|
-
window.addEventListener("scroll", scheduled, { passive: true, capture: true })
|
|
727
|
-
return () => {
|
|
728
|
-
scheduled.cancel()
|
|
729
|
-
ro.disconnect()
|
|
730
|
-
window.removeEventListener("resize", scheduled)
|
|
731
|
-
window.removeEventListener("scroll", scheduled, { capture: true })
|
|
732
|
-
}
|
|
733
|
-
}, [active, fullWidth, scrollRef])
|
|
734
|
-
return style
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function DataTableInner<TData extends Record<string, unknown>>({
|
|
738
|
-
// `data` / `defaultSort` flow into `useTableState` upstream; the inner table
|
|
739
|
-
// reads them via `state` and never directly here. Keep the prop slots so
|
|
740
|
-
// the public `DataTable<TData>` API stays unchanged.
|
|
741
|
-
data: _data,
|
|
742
|
-
columns,
|
|
743
|
-
getRowId: getRowIdProp,
|
|
744
|
-
getRowSelectionLabel,
|
|
745
|
-
selectable = true,
|
|
746
|
-
searchable = true,
|
|
747
|
-
emptyState,
|
|
748
|
-
onRowClick,
|
|
749
|
-
defaultSort: _defaultSort,
|
|
750
|
-
toolbarSlot,
|
|
751
|
-
bulkActionsSlot,
|
|
752
|
-
addRowLabel = false,
|
|
753
|
-
renderFilterOptionValue,
|
|
754
|
-
hasFooter = false,
|
|
755
|
-
conditionalRules,
|
|
756
|
-
showColumnHeaders = true,
|
|
757
|
-
state,
|
|
758
|
-
}: DataTableInnerProps<TData>) {
|
|
759
|
-
const {
|
|
760
|
-
setSortRules,
|
|
761
|
-
sortKey, sortDir,
|
|
762
|
-
handleSortByKey,
|
|
763
|
-
addFilter,
|
|
764
|
-
groupBy, setGroupBy,
|
|
765
|
-
colMenuSearch, setColMenuSearch,
|
|
766
|
-
selected, setSelected, toggleRow, toggleAll, getRowId,
|
|
767
|
-
colWidths, startResize,
|
|
768
|
-
colPins, lockedPins,
|
|
769
|
-
pinColumn, unpinColumn,
|
|
770
|
-
colWrap, toggleWrap,
|
|
771
|
-
draggedKey, dragOverKey,
|
|
772
|
-
handleDragStart, handleDragOver, handleDrop, handleDragEnd,
|
|
773
|
-
scrollRef, handleScroll, checkOverflow,
|
|
774
|
-
isOverflowing,
|
|
775
|
-
setHoveredRow,
|
|
776
|
-
rows, pagedRows, groupedRows,
|
|
777
|
-
effectivePins, displayCols,
|
|
778
|
-
isReflowViewport,
|
|
779
|
-
stickyStyle,
|
|
780
|
-
totalWidth,
|
|
781
|
-
rowHeight,
|
|
782
|
-
showGridlines,
|
|
783
|
-
setSheetOpen,
|
|
784
|
-
} = state
|
|
785
|
-
|
|
786
|
-
// Mount overflow check + scrollport width for sticky group headers on horizontal scroll.
|
|
787
|
-
React.useEffect(() => {
|
|
788
|
-
const syncScrollport = () => {
|
|
789
|
-
const el = scrollRef.current
|
|
790
|
-
if (el) {
|
|
791
|
-
el.style.setProperty("--dt-scrollport-width", `${el.clientWidth}px`)
|
|
792
|
-
}
|
|
793
|
-
checkOverflow()
|
|
794
|
-
}
|
|
795
|
-
syncScrollport()
|
|
796
|
-
const el = scrollRef.current
|
|
797
|
-
if (!el) return
|
|
798
|
-
const ro = new ResizeObserver(syncScrollport)
|
|
799
|
-
ro.observe(el)
|
|
800
|
-
return () => ro.disconnect()
|
|
801
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
802
|
-
}, [])
|
|
803
|
-
|
|
804
|
-
/** One-time horizontal nudge when the grid overflows and pins are active — hints that more columns scroll (overlay scrollbars, esp. Windows, are often invisible until interaction). */
|
|
805
|
-
const pinnedScrollHintDoneRef = React.useRef(false)
|
|
806
|
-
React.useEffect(() => {
|
|
807
|
-
if (!isOverflowing || isReflowViewport || Object.keys(colPins).length === 0) return
|
|
808
|
-
if (pinnedScrollHintDoneRef.current) return
|
|
809
|
-
const el = scrollRef.current
|
|
810
|
-
if (!el) return
|
|
811
|
-
if (el.scrollLeft > 2) return
|
|
812
|
-
const maxScroll = el.scrollWidth - el.clientWidth
|
|
813
|
-
if (maxScroll < 16) return
|
|
814
|
-
if (typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
815
|
-
pinnedScrollHintDoneRef.current = true
|
|
816
|
-
return
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
pinnedScrollHintDoneRef.current = true
|
|
820
|
-
const delta = Math.min(96, Math.max(28, Math.round(maxScroll * 0.14)))
|
|
821
|
-
const startDelayMs = 320
|
|
822
|
-
const dwellMs = 520
|
|
823
|
-
|
|
824
|
-
const t1 = window.setTimeout(() => {
|
|
825
|
-
el.scrollTo({ left: delta, behavior: "smooth" })
|
|
826
|
-
}, startDelayMs)
|
|
827
|
-
const t2 = window.setTimeout(() => {
|
|
828
|
-
el.scrollTo({ left: 0, behavior: "smooth" })
|
|
829
|
-
}, startDelayMs + dwellMs)
|
|
830
|
-
|
|
831
|
-
return () => {
|
|
832
|
-
window.clearTimeout(t1)
|
|
833
|
-
window.clearTimeout(t2)
|
|
834
|
-
}
|
|
835
|
-
}, [isOverflowing, isReflowViewport, colPins, scrollRef])
|
|
836
|
-
|
|
837
|
-
const lastLeftPinKey = [...displayCols].reverse().find(c => effectivePins[c.key] === "left")?.key
|
|
838
|
-
const firstRightPinKey = displayCols.find(c => effectivePins[c.key] === "right")?.key
|
|
839
|
-
|
|
840
|
-
function floatingHeaderPinnedStyle(key: string): React.CSSProperties | undefined {
|
|
841
|
-
const pin = effectivePins[key]
|
|
842
|
-
if (!pin) return undefined
|
|
843
|
-
|
|
844
|
-
const visibleWidth =
|
|
845
|
-
typeof floatingHeaderStyle?.width === "number"
|
|
846
|
-
? floatingHeaderStyle.width
|
|
847
|
-
: tableWrapRef.current?.clientWidth ?? floatingHeaderTableWidth
|
|
848
|
-
const maxScroll = Math.max(0, floatingHeaderTableWidth - visibleWidth)
|
|
849
|
-
const translateX = pin === "left"
|
|
850
|
-
? headerScrollLeft
|
|
851
|
-
: headerScrollLeft - maxScroll
|
|
852
|
-
|
|
853
|
-
// The floating sticky header is horizontally translated as one table.
|
|
854
|
-
// Counter-translate pinned header cells so they remain locked to the viewport edge.
|
|
855
|
-
return { position: "relative", transform: `translateX(${translateX}px)` }
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Row IDs for the current visible rows
|
|
859
|
-
const allRowIds = rows.map((r, i) => getRowId(r, i, getRowIdProp))
|
|
860
|
-
const allSelected = rows.length > 0 && selected.size === rows.length
|
|
861
|
-
const someSelected = selected.size > 0 && !allSelected
|
|
862
|
-
const anySelected = selected.size > 0
|
|
863
|
-
|
|
864
|
-
const { resolvedTheme } = useTheme()
|
|
865
|
-
const isAppDark = resolvedTheme === "dark"
|
|
866
|
-
|
|
867
|
-
const bulkBarUseFixedLayout = anySelected
|
|
868
|
-
/** Reflow: bar spans table width. Normal zoom: bar centered, max 28rem. */
|
|
869
|
-
const bulkBarFixedStyle = useBulkBarFixedToTableScrollEl(
|
|
870
|
-
scrollRef,
|
|
871
|
-
bulkBarUseFixedLayout,
|
|
872
|
-
isReflowViewport,
|
|
873
|
-
)
|
|
874
|
-
const tableWrapRef = React.useRef<HTMLDivElement | null>(null)
|
|
875
|
-
const tableHeadRef = React.useRef<HTMLTableSectionElement | null>(null)
|
|
876
|
-
const [headerIsStuck, setHeaderIsStuck] = React.useState(false)
|
|
877
|
-
const [headerScrollLeft, setHeaderScrollLeft] = React.useState(0)
|
|
878
|
-
const [floatingHeaderStyle, setFloatingHeaderStyle] = React.useState<React.CSSProperties | undefined>(undefined)
|
|
879
|
-
const [floatingHeaderTableWidth, setFloatingHeaderTableWidth] = React.useState(totalWidth)
|
|
880
|
-
const [isClient, setIsClient] = React.useState(false)
|
|
881
|
-
|
|
882
|
-
React.useEffect(() => {
|
|
883
|
-
setIsClient(true)
|
|
884
|
-
}, [])
|
|
885
|
-
|
|
886
|
-
React.useEffect(() => {
|
|
887
|
-
const wrapEl = tableWrapRef.current
|
|
888
|
-
const headEl = tableHeadRef.current
|
|
889
|
-
if (!wrapEl || !headEl || !showColumnHeaders) {
|
|
890
|
-
setHeaderIsStuck(false)
|
|
891
|
-
return
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
const update = () => {
|
|
895
|
-
const wrapRect = wrapEl.getBoundingClientRect()
|
|
896
|
-
const headHeight = headEl.getBoundingClientRect().height || 0
|
|
897
|
-
const rootStyle = getComputedStyle(document.documentElement)
|
|
898
|
-
const headerOffset = Number.parseFloat(rootStyle.getPropertyValue("--header-height")) || 0
|
|
899
|
-
const stuck = wrapRect.top <= headerOffset && wrapRect.bottom > (headHeight + headerOffset + 1)
|
|
900
|
-
setHeaderIsStuck(prev => (prev === stuck ? prev : stuck))
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
update()
|
|
904
|
-
// rAF-coalesce: capture-phase scroll fires for every ancestor (sidebar,
|
|
905
|
-
// dashboard panels, anchored sheets), so a single getBoundingClientRect
|
|
906
|
-
// per frame is more than enough to keep the sticky header aligned.
|
|
907
|
-
const scheduled = rafThrottle(update)
|
|
908
|
-
window.addEventListener("scroll", scheduled, { passive: true, capture: true })
|
|
909
|
-
window.addEventListener("resize", scheduled, { passive: true })
|
|
910
|
-
return () => {
|
|
911
|
-
scheduled.cancel()
|
|
912
|
-
window.removeEventListener("scroll", scheduled, { capture: true })
|
|
913
|
-
window.removeEventListener("resize", scheduled)
|
|
914
|
-
}
|
|
915
|
-
}, [showColumnHeaders, rows.length, displayCols.length])
|
|
916
|
-
|
|
917
|
-
React.useLayoutEffect(() => {
|
|
918
|
-
if (!headerIsStuck || !showColumnHeaders) {
|
|
919
|
-
setFloatingHeaderStyle(undefined)
|
|
920
|
-
return
|
|
921
|
-
}
|
|
922
|
-
const wrapEl = tableWrapRef.current
|
|
923
|
-
if (!wrapEl) {
|
|
924
|
-
setFloatingHeaderStyle(undefined)
|
|
925
|
-
return
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
const apply = () => {
|
|
929
|
-
const rect = wrapEl.getBoundingClientRect()
|
|
930
|
-
const rootStyle = getComputedStyle(document.documentElement)
|
|
931
|
-
const headerOffset = Number.parseFloat(rootStyle.getPropertyValue("--header-height")) || 0
|
|
932
|
-
const cs = getComputedStyle(wrapEl)
|
|
933
|
-
const borderLeft = parseFloat(cs.borderLeftWidth) || 0
|
|
934
|
-
const borderRight = parseFloat(cs.borderRightWidth) || 0
|
|
935
|
-
const visibleWidth = Math.max(0, wrapEl.clientWidth - borderLeft - borderRight)
|
|
936
|
-
const renderedTableWidth = Math.max(
|
|
937
|
-
totalWidth,
|
|
938
|
-
visibleWidth,
|
|
939
|
-
wrapEl.querySelector("table")?.getBoundingClientRect().width ?? 0,
|
|
940
|
-
)
|
|
941
|
-
setFloatingHeaderStyle({
|
|
942
|
-
position: "fixed",
|
|
943
|
-
top: headerOffset,
|
|
944
|
-
left: rect.left + borderLeft,
|
|
945
|
-
width: visibleWidth,
|
|
946
|
-
zIndex: 50,
|
|
947
|
-
})
|
|
948
|
-
setFloatingHeaderTableWidth(renderedTableWidth)
|
|
949
|
-
setHeaderScrollLeft(wrapEl.scrollLeft)
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
apply()
|
|
953
|
-
const scheduled = rafThrottle(apply)
|
|
954
|
-
const ro = new ResizeObserver(scheduled)
|
|
955
|
-
ro.observe(wrapEl)
|
|
956
|
-
window.addEventListener("scroll", scheduled, { passive: true, capture: true })
|
|
957
|
-
window.addEventListener("resize", scheduled, { passive: true })
|
|
958
|
-
return () => {
|
|
959
|
-
scheduled.cancel()
|
|
960
|
-
ro.disconnect()
|
|
961
|
-
window.removeEventListener("scroll", scheduled, { capture: true })
|
|
962
|
-
window.removeEventListener("resize", scheduled)
|
|
963
|
-
}
|
|
964
|
-
}, [headerIsStuck, showColumnHeaders, totalWidth, displayCols.length])
|
|
965
|
-
|
|
966
|
-
function ariaSortAttr(colKey: string): React.AriaAttributes["aria-sort"] {
|
|
967
|
-
return sortKey !== colKey ? "none" : sortDir === "asc" ? "ascending" : "descending"
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function cellStyle(key: string): React.CSSProperties {
|
|
971
|
-
return stickyStyle(key)
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// ─── Render ───────────────────────────────────────────────────────────────
|
|
975
|
-
return (
|
|
976
|
-
<div className="flex min-w-0 w-full flex-col gap-0">
|
|
977
|
-
|
|
978
|
-
<DataTableToolbar
|
|
979
|
-
state={state}
|
|
980
|
-
columns={columns}
|
|
981
|
-
searchable={searchable}
|
|
982
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
983
|
-
toolbarSlot={toolbarSlot}
|
|
984
|
-
searchAriaLabel="Search table"
|
|
985
|
-
/>
|
|
986
|
-
|
|
987
|
-
{isClient && showColumnHeaders && headerIsStuck && floatingHeaderStyle
|
|
988
|
-
? createPortal(
|
|
989
|
-
<div
|
|
990
|
-
style={floatingHeaderStyle}
|
|
991
|
-
className="pointer-events-auto"
|
|
992
|
-
>
|
|
993
|
-
<div className="overflow-hidden border border-border bg-dt-header-bg shadow-[0_10px_18px_-14px_rgba(15,23,42,0.5)] dark:shadow-[0_12px_20px_-14px_rgba(0,0,0,0.75)]">
|
|
994
|
-
<div style={{ transform: `translateX(${-headerScrollLeft}px)` }}>
|
|
995
|
-
<table
|
|
996
|
-
className="w-full text-sm border-separate border-spacing-0"
|
|
997
|
-
style={{ tableLayout: "fixed", width: floatingHeaderTableWidth }}
|
|
998
|
-
>
|
|
999
|
-
<colgroup>
|
|
1000
|
-
{displayCols.map(col => (
|
|
1001
|
-
<col key={col.key} style={{ width: colWidths[col.key] ?? col.width ?? 100 }} />
|
|
1002
|
-
))}
|
|
1003
|
-
</colgroup>
|
|
1004
|
-
<thead className="bg-dt-header-bg">
|
|
1005
|
-
<tr>
|
|
1006
|
-
{displayCols.map(col => {
|
|
1007
|
-
const isPinned = !!effectivePins[col.key]
|
|
1008
|
-
const isEdgePinCol = col.key === lastLeftPinKey || col.key === firstRightPinKey
|
|
1009
|
-
return (
|
|
1010
|
-
<th
|
|
1011
|
-
key={col.key}
|
|
1012
|
-
scope="col"
|
|
1013
|
-
style={floatingHeaderPinnedStyle(col.key)}
|
|
1014
|
-
className={cn(
|
|
1015
|
-
"h-9 px-3 text-left align-middle select-none",
|
|
1016
|
-
"text-xs font-medium text-muted-foreground tracking-wide",
|
|
1017
|
-
"bg-dt-header-bg border-b border-border",
|
|
1018
|
-
showGridlines && (!isEdgePinCol
|
|
1019
|
-
? "border-r border-border last:border-r-0"
|
|
1020
|
-
: "last:border-r-0"),
|
|
1021
|
-
isPinned ? "z-40" : "z-30",
|
|
1022
|
-
isPinned && "relative",
|
|
1023
|
-
isEdgePinCol && stickyShadow(effectivePins[col.key]),
|
|
1024
|
-
)}
|
|
1025
|
-
>
|
|
1026
|
-
<div className="flex items-center justify-between gap-1 min-w-0">
|
|
1027
|
-
<div className="flex items-center min-w-0 flex-1">
|
|
1028
|
-
{col.key === "select" ? (
|
|
1029
|
-
selectable && (
|
|
1030
|
-
<span className="inline-flex items-center justify-center self-center">
|
|
1031
|
-
<span className="sr-only">{resolvedColumnLabel(col)}</span>
|
|
1032
|
-
<Checkbox
|
|
1033
|
-
checked={allSelected ? true : someSelected ? "indeterminate" : false}
|
|
1034
|
-
onCheckedChange={() => toggleAll(allRowIds)}
|
|
1035
|
-
aria-label="Select all rows"
|
|
1036
|
-
/>
|
|
1037
|
-
</span>
|
|
1038
|
-
)
|
|
1039
|
-
) : col.sortable && col.sortKey ? (
|
|
1040
|
-
<button
|
|
1041
|
-
type="button"
|
|
1042
|
-
onClick={() => handleSortByKey(col.key)}
|
|
1043
|
-
className={cn(
|
|
1044
|
-
"inline-flex items-center hover:text-interactive-hover-foreground transition-colors whitespace-nowrap",
|
|
1045
|
-
sortKey === col.key && "text-foreground",
|
|
1046
|
-
)}
|
|
1047
|
-
>
|
|
1048
|
-
{col.label?.trim() ? col.label : resolvedColumnLabel(col)}
|
|
1049
|
-
{sortKey === col.key ? <SortChevron dir={sortDir} /> : null}
|
|
1050
|
-
</button>
|
|
1051
|
-
) : (
|
|
1052
|
-
<span className="truncate whitespace-nowrap">
|
|
1053
|
-
{col.label?.trim()
|
|
1054
|
-
? col.label
|
|
1055
|
-
: defaultColumnHeaderLabel(col.key) ?? col.key}
|
|
1056
|
-
</span>
|
|
1057
|
-
)}
|
|
1058
|
-
</div>
|
|
1059
|
-
</div>
|
|
1060
|
-
</th>
|
|
1061
|
-
)
|
|
1062
|
-
})}
|
|
1063
|
-
</tr>
|
|
1064
|
-
</thead>
|
|
1065
|
-
</table>
|
|
1066
|
-
</div>
|
|
1067
|
-
</div>
|
|
1068
|
-
</div>,
|
|
1069
|
-
document.body,
|
|
1070
|
-
)
|
|
1071
|
-
: null}
|
|
1072
|
-
|
|
1073
|
-
{/* ── Table ────────────────────────────────────────────────────────── */}
|
|
1074
|
-
<div
|
|
1075
|
-
ref={el => {
|
|
1076
|
-
tableWrapRef.current = el
|
|
1077
|
-
scrollRef.current = el
|
|
1078
|
-
}}
|
|
1079
|
-
onScroll={e => {
|
|
1080
|
-
handleScroll()
|
|
1081
|
-
setHeaderScrollLeft((e.currentTarget as HTMLDivElement).scrollLeft)
|
|
1082
|
-
}}
|
|
1083
|
-
className={cn(
|
|
1084
|
-
"mx-4 lg:mx-6 overflow-x-auto border border-border",
|
|
1085
|
-
hasFooter ? "rounded-t-lg" : "rounded-lg",
|
|
1086
|
-
)}
|
|
1087
|
-
>
|
|
1088
|
-
<table
|
|
1089
|
-
className="w-full text-sm border-separate border-spacing-0"
|
|
1090
|
-
style={{
|
|
1091
|
-
tableLayout: "fixed",
|
|
1092
|
-
minWidth: totalWidth,
|
|
1093
|
-
width: headerIsStuck ? floatingHeaderTableWidth : undefined,
|
|
1094
|
-
}}
|
|
1095
|
-
>
|
|
1096
|
-
<colgroup>
|
|
1097
|
-
{displayCols.map(col => (
|
|
1098
|
-
<col key={col.key} style={{ width: colWidths[col.key] ?? col.width ?? 100 }} />
|
|
1099
|
-
))}
|
|
1100
|
-
</colgroup>
|
|
1101
|
-
|
|
1102
|
-
{/* ── Table head ──────────────────────────────────────────────── */}
|
|
1103
|
-
<thead
|
|
1104
|
-
ref={tableHeadRef}
|
|
1105
|
-
className={cn(
|
|
1106
|
-
"bg-dt-header-bg",
|
|
1107
|
-
headerIsStuck && "invisible",
|
|
1108
|
-
!showColumnHeaders && "hidden"
|
|
1109
|
-
)}
|
|
1110
|
-
>
|
|
1111
|
-
<tr>
|
|
1112
|
-
{displayCols.map(col => {
|
|
1113
|
-
const isPinned = !!effectivePins[col.key]
|
|
1114
|
-
const isLocked = !!lockedPins[col.key]
|
|
1115
|
-
const isFree = !colPins[col.key]
|
|
1116
|
-
const isResizable = !isLocked || (col.key !== "select")
|
|
1117
|
-
|
|
1118
|
-
const isEdgePinCol = col.key === lastLeftPinKey || col.key === firstRightPinKey
|
|
1119
|
-
|
|
1120
|
-
return (
|
|
1121
|
-
<th
|
|
1122
|
-
key={col.key}
|
|
1123
|
-
scope="col"
|
|
1124
|
-
aria-sort={col.sortable && col.sortKey ? ariaSortAttr(col.sortKey as string) : undefined}
|
|
1125
|
-
draggable={isFree}
|
|
1126
|
-
onDragStart={isFree ? e => handleDragStart(col.key, e) : undefined}
|
|
1127
|
-
onDragOver={isFree ? e => handleDragOver(col.key, e) : undefined}
|
|
1128
|
-
onDrop={isFree ? () => handleDrop(col.key) : undefined}
|
|
1129
|
-
onDragEnd={isFree ? handleDragEnd : undefined}
|
|
1130
|
-
style={stickyStyle(col.key, false)}
|
|
1131
|
-
className={cn(
|
|
1132
|
-
"group/th relative h-9 px-3 text-left align-middle select-none",
|
|
1133
|
-
"text-xs font-medium text-muted-foreground tracking-wide",
|
|
1134
|
-
"bg-dt-header-bg border-b border-border",
|
|
1135
|
-
showGridlines && (!isEdgePinCol
|
|
1136
|
-
? "border-r border-border last:border-r-0"
|
|
1137
|
-
: "last:border-r-0"),
|
|
1138
|
-
isPinned ? "z-40" : "z-30",
|
|
1139
|
-
isFree && "cursor-grab active:cursor-grabbing",
|
|
1140
|
-
dragOverKey === col.key && draggedKey.current !== col.key && "bg-accent/40",
|
|
1141
|
-
isEdgePinCol && stickyShadow(effectivePins[col.key])
|
|
1142
|
-
)}
|
|
1143
|
-
>
|
|
1144
|
-
<div className="flex items-center justify-between gap-1 min-w-0">
|
|
1145
|
-
<div className="flex items-center min-w-0 flex-1">
|
|
1146
|
-
{col.header ? (
|
|
1147
|
-
col.header()
|
|
1148
|
-
) : col.key === "select" ? (
|
|
1149
|
-
selectable && (
|
|
1150
|
-
<span className="inline-flex items-center justify-center self-center">
|
|
1151
|
-
<span className="sr-only">{resolvedColumnLabel(col)}</span>
|
|
1152
|
-
<Checkbox
|
|
1153
|
-
checked={allSelected ? true : someSelected ? "indeterminate" : false}
|
|
1154
|
-
onCheckedChange={() => toggleAll(allRowIds)}
|
|
1155
|
-
aria-label="Select all rows"
|
|
1156
|
-
/>
|
|
1157
|
-
</span>
|
|
1158
|
-
)
|
|
1159
|
-
) : col.sortable && col.sortKey ? (
|
|
1160
|
-
<Tip label={`Sort by ${resolvedColumnLabel(col)}`} side="top">
|
|
1161
|
-
<button
|
|
1162
|
-
type="button"
|
|
1163
|
-
onClick={() => handleSortByKey(col.key)}
|
|
1164
|
-
className={cn(
|
|
1165
|
-
"inline-flex items-center hover:text-interactive-hover-foreground transition-colors whitespace-nowrap",
|
|
1166
|
-
sortKey === col.key && "text-foreground"
|
|
1167
|
-
)}
|
|
1168
|
-
>
|
|
1169
|
-
{col.label?.trim() ? col.label : resolvedColumnLabel(col)}
|
|
1170
|
-
{sortKey === col.key && <SortChevron dir={sortDir} />}
|
|
1171
|
-
</button>
|
|
1172
|
-
</Tip>
|
|
1173
|
-
) : (
|
|
1174
|
-
<Tip label={resolvedColumnLabel(col)} side="top">
|
|
1175
|
-
<span className="whitespace-nowrap">
|
|
1176
|
-
{col.label?.trim() ? (
|
|
1177
|
-
col.label
|
|
1178
|
-
) : defaultColumnHeaderLabel(col.key) ? (
|
|
1179
|
-
<span className="sr-only">{defaultColumnHeaderLabel(col.key)}</span>
|
|
1180
|
-
) : (
|
|
1181
|
-
<span className="sr-only">{col.key}</span>
|
|
1182
|
-
)}
|
|
1183
|
-
</span>
|
|
1184
|
-
</Tip>
|
|
1185
|
-
)}
|
|
1186
|
-
</div>
|
|
1187
|
-
|
|
1188
|
-
{/* Column context menu — not on checkbox or locked-right columns */}
|
|
1189
|
-
{col.key !== "select" && !lockedPins[col.key]?.includes("right") && col.key !== (columns.find(c => c.lockPin && c.defaultPin === "right")?.key) && (
|
|
1190
|
-
<DropdownMenu>
|
|
1191
|
-
<Tip label="Column options" side="top">
|
|
1192
|
-
<DropdownMenuTrigger asChild>
|
|
1193
|
-
<button
|
|
1194
|
-
type="button"
|
|
1195
|
-
aria-label={`${resolvedColumnLabel(col)} column options`}
|
|
1196
|
-
onClick={e => e.stopPropagation()}
|
|
1197
|
-
className={cn(
|
|
1198
|
-
"opacity-0 group-hover/th:opacity-100 group-focus-within/th:opacity-100",
|
|
1199
|
-
"inline-flex shrink-0 items-center justify-center size-7 rounded-md",
|
|
1200
|
-
"text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover-row",
|
|
1201
|
-
"transition-opacity focus-visible:opacity-100",
|
|
1202
|
-
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
1203
|
-
)}
|
|
1204
|
-
>
|
|
1205
|
-
<i className="fa-light fa-ellipsis-vertical text-xs" aria-hidden="true" />
|
|
1206
|
-
</button>
|
|
1207
|
-
</DropdownMenuTrigger>
|
|
1208
|
-
</Tip>
|
|
1209
|
-
<DropdownMenuContent align="start">
|
|
1210
|
-
|
|
1211
|
-
{/* Column quick-search */}
|
|
1212
|
-
<div className="px-2 pt-2 pb-1">
|
|
1213
|
-
<div className="relative">
|
|
1214
|
-
<i className="fa-light fa-magnifying-glass absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground text-xs pointer-events-none" aria-hidden="true" />
|
|
1215
|
-
<Input
|
|
1216
|
-
placeholder={`Search ${resolvedColumnLabel(col)}…`}
|
|
1217
|
-
value={colMenuSearch[col.key] ?? ""}
|
|
1218
|
-
onChange={e => setColMenuSearch(prev => ({ ...prev, [col.key]: e.target.value }))}
|
|
1219
|
-
onKeyDown={e => e.stopPropagation()}
|
|
1220
|
-
className="h-7 pl-6 text-xs"
|
|
1221
|
-
/>
|
|
1222
|
-
{colMenuSearch[col.key] && (
|
|
1223
|
-
<button
|
|
1224
|
-
type="button"
|
|
1225
|
-
aria-label="Clear search"
|
|
1226
|
-
onClick={() => setColMenuSearch(prev => ({ ...prev, [col.key]: "" }))}
|
|
1227
|
-
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-interactive-hover-foreground transition-colors"
|
|
1228
|
-
>
|
|
1229
|
-
<i className="fa-light fa-xmark text-xs" aria-hidden="true" />
|
|
1230
|
-
</button>
|
|
1231
|
-
)}
|
|
1232
|
-
</div>
|
|
1233
|
-
</div>
|
|
1234
|
-
<DropdownMenuSeparator />
|
|
1235
|
-
|
|
1236
|
-
{/* Pin options */}
|
|
1237
|
-
{!isLocked && (
|
|
1238
|
-
<>
|
|
1239
|
-
<DropdownMenuItem
|
|
1240
|
-
onClick={() => pinColumn(col.key, "left")}
|
|
1241
|
-
disabled={colPins[col.key] === "left"}
|
|
1242
|
-
>
|
|
1243
|
-
<i className="fa-light fa-arrow-left-to-line" aria-hidden="true" />
|
|
1244
|
-
Pin Left
|
|
1245
|
-
</DropdownMenuItem>
|
|
1246
|
-
<DropdownMenuItem
|
|
1247
|
-
onClick={() => pinColumn(col.key, "right")}
|
|
1248
|
-
disabled={colPins[col.key] === "right"}
|
|
1249
|
-
>
|
|
1250
|
-
<i className="fa-light fa-arrow-right-to-line" aria-hidden="true" />
|
|
1251
|
-
Pin Right
|
|
1252
|
-
</DropdownMenuItem>
|
|
1253
|
-
{colPins[col.key] && (
|
|
1254
|
-
<DropdownMenuItem onClick={() => unpinColumn(col.key)}>
|
|
1255
|
-
<i className="fa-light fa-thumbtack-slash" aria-hidden="true" />
|
|
1256
|
-
Unpin
|
|
1257
|
-
</DropdownMenuItem>
|
|
1258
|
-
)}
|
|
1259
|
-
<DropdownMenuSeparator />
|
|
1260
|
-
</>
|
|
1261
|
-
)}
|
|
1262
|
-
|
|
1263
|
-
{/* Sort options */}
|
|
1264
|
-
{col.sortable && col.sortKey && (
|
|
1265
|
-
<>
|
|
1266
|
-
<DropdownMenuItem onClick={() => setSortRules(prev => {
|
|
1267
|
-
const filtered = prev.filter(r => r.fieldKey !== col.key)
|
|
1268
|
-
return [{ id: `sort-${Date.now()}`, fieldKey: col.key, direction: "asc" as const }, ...filtered]
|
|
1269
|
-
})}>
|
|
1270
|
-
<i className="fa-light fa-arrow-up-a-z text-xs shrink-0" aria-hidden="true" />
|
|
1271
|
-
Sort Ascending
|
|
1272
|
-
</DropdownMenuItem>
|
|
1273
|
-
<DropdownMenuItem onClick={() => setSortRules(prev => {
|
|
1274
|
-
const filtered = prev.filter(r => r.fieldKey !== col.key)
|
|
1275
|
-
return [{ id: `sort-${Date.now()}`, fieldKey: col.key, direction: "desc" as const }, ...filtered]
|
|
1276
|
-
})}>
|
|
1277
|
-
<i className="fa-light fa-arrow-down-a-z text-xs shrink-0" aria-hidden="true" />
|
|
1278
|
-
Sort Descending
|
|
1279
|
-
</DropdownMenuItem>
|
|
1280
|
-
<DropdownMenuSeparator />
|
|
1281
|
-
</>
|
|
1282
|
-
)}
|
|
1283
|
-
|
|
1284
|
-
{/* Text wrap toggle */}
|
|
1285
|
-
<DropdownMenuItem onClick={() => toggleWrap(col.key)}>
|
|
1286
|
-
<i className="fa-light fa-text-width" aria-hidden="true" />
|
|
1287
|
-
{colWrap[col.key] ? "Unwrap Text" : "Wrap Text"}
|
|
1288
|
-
</DropdownMenuItem>
|
|
1289
|
-
|
|
1290
|
-
{/* Filter / Group by */}
|
|
1291
|
-
<DropdownMenuSeparator />
|
|
1292
|
-
{col.filter && (
|
|
1293
|
-
<DropdownMenuItem onClick={() => addFilter(col.key)}>
|
|
1294
|
-
<i className="fa-light fa-filter" aria-hidden="true" />
|
|
1295
|
-
Filter by this column
|
|
1296
|
-
</DropdownMenuItem>
|
|
1297
|
-
)}
|
|
1298
|
-
<DropdownMenuItem
|
|
1299
|
-
onClick={() => setGroupBy(groupBy === col.key ? null : col.key)}
|
|
1300
|
-
>
|
|
1301
|
-
<i className="fa-light fa-layer-group" aria-hidden="true" />
|
|
1302
|
-
{groupBy === col.key ? "Remove Grouping" : "Group by this Column"}
|
|
1303
|
-
</DropdownMenuItem>
|
|
1304
|
-
|
|
1305
|
-
{/* Conditional rule shortcut */}
|
|
1306
|
-
<DropdownMenuSeparator />
|
|
1307
|
-
<DropdownMenuItem onClick={() => setSheetOpen(true)}>
|
|
1308
|
-
<i className="fa-light fa-palette" aria-hidden="true" />
|
|
1309
|
-
Add Conditional Rule
|
|
1310
|
-
</DropdownMenuItem>
|
|
1311
|
-
|
|
1312
|
-
</DropdownMenuContent>
|
|
1313
|
-
</DropdownMenu>
|
|
1314
|
-
)}
|
|
1315
|
-
</div>
|
|
1316
|
-
|
|
1317
|
-
{/* Resize handle */}
|
|
1318
|
-
{isResizable && col.key !== "select" && (
|
|
1319
|
-
<div
|
|
1320
|
-
role="separator"
|
|
1321
|
-
aria-label={`Resize ${resolvedColumnLabel(col)} column`}
|
|
1322
|
-
aria-orientation="vertical"
|
|
1323
|
-
onMouseDown={e => startResize(col.key, e)}
|
|
1324
|
-
className="absolute right-0 top-1 bottom-1 w-1.5 cursor-col-resize rounded-full hover:bg-interactive-hover-foreground/50 active:bg-muted-foreground/70 transition-colors"
|
|
1325
|
-
/>
|
|
1326
|
-
)}
|
|
1327
|
-
</th>
|
|
1328
|
-
)
|
|
1329
|
-
})}
|
|
1330
|
-
</tr>
|
|
1331
|
-
</thead>
|
|
1332
|
-
|
|
1333
|
-
{/* ── Table body ───────────────────────────────────────────────── */}
|
|
1334
|
-
<tbody>
|
|
1335
|
-
{(pagedRows !== rows
|
|
1336
|
-
? [{ groupKey: null as string | null, groupLabel: null as string | null, rows: pagedRows }]
|
|
1337
|
-
: groupedRows
|
|
1338
|
-
).map(({ groupKey, groupLabel, rows: groupRows }) => (
|
|
1339
|
-
<React.Fragment key={groupKey ?? "__all__"}>
|
|
1340
|
-
{groupLabel && (
|
|
1341
|
-
<tr>
|
|
1342
|
-
<td colSpan={displayCols.length} className="p-0 border-b border-border bg-dt-group-bg">
|
|
1343
|
-
<div
|
|
1344
|
-
className={cn(
|
|
1345
|
-
"sticky left-0 z-[25] px-4 py-1.5 text-xs font-semibold text-muted-foreground tracking-wide bg-dt-group-bg select-none",
|
|
1346
|
-
!isReflowViewport && "shadow-[4px_0_8px_-4px_var(--sticky-edge-fade)]",
|
|
1347
|
-
)}
|
|
1348
|
-
style={{ width: "var(--dt-scrollport-width, 100%)" }}
|
|
1349
|
-
>
|
|
1350
|
-
{groupLabel}
|
|
1351
|
-
<span className="ml-2 font-normal normal-case opacity-60 tracking-normal">
|
|
1352
|
-
{groupRows.length} record{groupRows.length !== 1 ? "s" : ""}
|
|
1353
|
-
</span>
|
|
1354
|
-
</div>
|
|
1355
|
-
</td>
|
|
1356
|
-
</tr>
|
|
1357
|
-
)}
|
|
1358
|
-
{groupRows.map((row, rowIndex) => {
|
|
1359
|
-
const rowId = getRowId(row, rowIndex, getRowIdProp)
|
|
1360
|
-
const isSelected = selected.has(rowId)
|
|
1361
|
-
const rowClickable = Boolean(onRowClick) || selectable
|
|
1362
|
-
function handleRowClick(e: React.MouseEvent<HTMLTableRowElement>) {
|
|
1363
|
-
if (!rowClickable) return
|
|
1364
|
-
const el = e.target as HTMLElement | null
|
|
1365
|
-
if (!el) return
|
|
1366
|
-
if (el.closest("button, a, input, textarea, select, label, [role='checkbox']")) return
|
|
1367
|
-
if (onRowClick) {
|
|
1368
|
-
onRowClick(row)
|
|
1369
|
-
return
|
|
1370
|
-
}
|
|
1371
|
-
if (selectable) {
|
|
1372
|
-
toggleRow(rowId)
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
return (
|
|
1376
|
-
<tr
|
|
1377
|
-
key={String(rowId)}
|
|
1378
|
-
data-state={isSelected ? "selected" : undefined}
|
|
1379
|
-
onMouseEnter={() => setHoveredRow(rowId)}
|
|
1380
|
-
onMouseLeave={() => setHoveredRow(null)}
|
|
1381
|
-
onClick={rowClickable ? handleRowClick : undefined}
|
|
1382
|
-
data-new={Boolean((row as Record<string, unknown>).isNew) || undefined}
|
|
1383
|
-
className={cn(
|
|
1384
|
-
"group/row transition-colors",
|
|
1385
|
-
"hover:bg-dt-row-hover",
|
|
1386
|
-
isSelected && "bg-dt-row-selected text-dt-row-selected-fg",
|
|
1387
|
-
rowClickable && "cursor-pointer",
|
|
1388
|
-
Boolean((row as Record<string, unknown>).isNew) && "bg-dt-new-row-bg border-l-2 border-l-dt-new-row-border"
|
|
1389
|
-
)}
|
|
1390
|
-
>
|
|
1391
|
-
{displayCols.map(col => {
|
|
1392
|
-
const isPinned = !!effectivePins[col.key]
|
|
1393
|
-
const wrap = colWrap[col.key]
|
|
1394
|
-
const isEdgePin = col.key === lastLeftPinKey || col.key === firstRightPinKey
|
|
1395
|
-
const rowPy = rowHeight === "compact" ? "py-1" : rowHeight === "comfortable" ? "py-4" : "py-2.5"
|
|
1396
|
-
const cs = cellStyle(col.key)
|
|
1397
|
-
|
|
1398
|
-
const tdBase = cn(
|
|
1399
|
-
`px-3 ${rowPy} align-middle`,
|
|
1400
|
-
showGridlines && !isEdgePin && "border-r border-border last:border-r-0",
|
|
1401
|
-
"border-b border-border group-last/row:border-b-0",
|
|
1402
|
-
isPinned && [
|
|
1403
|
-
"z-20 pinned-cell",
|
|
1404
|
-
"bg-dt-row-bg",
|
|
1405
|
-
"group-data-[state=selected]/row:bg-dt-row-selected",
|
|
1406
|
-
"group-hover/row:bg-dt-row-hover",
|
|
1407
|
-
isEdgePin && stickyShadow(effectivePins[col.key]),
|
|
1408
|
-
]
|
|
1409
|
-
)
|
|
1410
|
-
|
|
1411
|
-
const conditionalBg = getConditionalCellBackground(
|
|
1412
|
-
row,
|
|
1413
|
-
col.key,
|
|
1414
|
-
conditionalRules,
|
|
1415
|
-
columns,
|
|
1416
|
-
)
|
|
1417
|
-
|
|
1418
|
-
const tdStyle = conditionalBg
|
|
1419
|
-
? { ...cs, background: conditionalBg }
|
|
1420
|
-
: cs
|
|
1421
|
-
|
|
1422
|
-
// Special synthetic columns
|
|
1423
|
-
if (col.key === "select") {
|
|
1424
|
-
const selectionLabel = getRowSelectionLabel?.(row, rowIndex)
|
|
1425
|
-
const ariaLabel = selectionLabel
|
|
1426
|
-
? `Select row, ${selectionLabel}`
|
|
1427
|
-
: `Select row ${rowIndex + 1}`
|
|
1428
|
-
return (
|
|
1429
|
-
<td key="select" className={cn(tdBase, "text-center")} style={tdStyle}>
|
|
1430
|
-
{selectable && (
|
|
1431
|
-
// inline-flex: inline elements inside <td> are never
|
|
1432
|
-
// stretched by table-cell height in Chrome/Safari/Firefox.
|
|
1433
|
-
// Block-level flex/grid always inherits full cell height at zoom.
|
|
1434
|
-
<span
|
|
1435
|
-
className={cn(
|
|
1436
|
-
"inline-flex items-center justify-center transition-opacity",
|
|
1437
|
-
anySelected
|
|
1438
|
-
? "opacity-100"
|
|
1439
|
-
: "opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100",
|
|
1440
|
-
)}
|
|
1441
|
-
onClick={e => e.stopPropagation()}
|
|
1442
|
-
>
|
|
1443
|
-
<Checkbox
|
|
1444
|
-
checked={isSelected}
|
|
1445
|
-
onCheckedChange={() => toggleRow(rowId)}
|
|
1446
|
-
aria-label={ariaLabel}
|
|
1447
|
-
onClick={e => e.stopPropagation()}
|
|
1448
|
-
/>
|
|
1449
|
-
</span>
|
|
1450
|
-
)}
|
|
1451
|
-
</td>
|
|
1452
|
-
)
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// Custom cell renderer
|
|
1456
|
-
if (col.cell) {
|
|
1457
|
-
return (
|
|
1458
|
-
<td
|
|
1459
|
-
key={col.key}
|
|
1460
|
-
className={cn(
|
|
1461
|
-
tdBase,
|
|
1462
|
-
// When wrap is on, override truncate/overflow on any descendant
|
|
1463
|
-
wrap && "[&_.truncate]:!whitespace-normal [&_.truncate]:!overflow-visible [&_.truncate]:!text-clip",
|
|
1464
|
-
)}
|
|
1465
|
-
style={tdStyle}
|
|
1466
|
-
>
|
|
1467
|
-
{col.cell(row, {
|
|
1468
|
-
rowIndex,
|
|
1469
|
-
selected: isSelected,
|
|
1470
|
-
onSelect: checked => checked ? setSelected(prev => new Set([...prev, rowId])) : toggleRow(rowId),
|
|
1471
|
-
})}
|
|
1472
|
-
</td>
|
|
1473
|
-
)
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// Default: render string value with optional truncation
|
|
1477
|
-
const rawVal = String(row[col.key] ?? "")
|
|
1478
|
-
return (
|
|
1479
|
-
<td key={col.key} className={cn(tdBase, "text-sm text-foreground/80")} style={tdStyle}>
|
|
1480
|
-
<span className={wrap ? "whitespace-normal" : "block truncate"} title={!wrap ? rawVal : undefined}>
|
|
1481
|
-
{rawVal}
|
|
1482
|
-
</span>
|
|
1483
|
-
</td>
|
|
1484
|
-
)
|
|
1485
|
-
})}
|
|
1486
|
-
</tr>
|
|
1487
|
-
)
|
|
1488
|
-
})}
|
|
1489
|
-
</React.Fragment>
|
|
1490
|
-
))}
|
|
1491
|
-
|
|
1492
|
-
{/* Empty state */}
|
|
1493
|
-
{rows.length === 0 && (
|
|
1494
|
-
<tr>
|
|
1495
|
-
<td colSpan={displayCols.length} className="h-24 px-3 text-center text-sm text-muted-foreground">
|
|
1496
|
-
{emptyState ?? "No results match your filters."}
|
|
1497
|
-
</td>
|
|
1498
|
-
</tr>
|
|
1499
|
-
)}
|
|
1500
|
-
|
|
1501
|
-
{/* Add new row stub */}
|
|
1502
|
-
{addRowLabel !== false && (
|
|
1503
|
-
<tr
|
|
1504
|
-
role="button"
|
|
1505
|
-
tabIndex={0}
|
|
1506
|
-
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") e.preventDefault() }}
|
|
1507
|
-
className="cursor-pointer hover:bg-dt-row-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
|
|
1508
|
-
aria-label={`Add new ${addRowLabel}`}
|
|
1509
|
-
>
|
|
1510
|
-
<td colSpan={displayCols.length} className="px-3 py-2.5 align-middle">
|
|
1511
|
-
<span className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
1512
|
-
<i className="fa-light fa-plus text-xs" aria-hidden="true" />
|
|
1513
|
-
{addRowLabel}
|
|
1514
|
-
</span>
|
|
1515
|
-
</td>
|
|
1516
|
-
</tr>
|
|
1517
|
-
)}
|
|
1518
|
-
</tbody>
|
|
1519
|
-
</table>
|
|
1520
|
-
</div>
|
|
1521
|
-
|
|
1522
|
-
{/* ── Bulk selection bar — dark strip in light app; light strip in dark app.
|
|
1523
|
-
Normal zoom: max ~28rem, centered. Reflow: full table width. Inner
|
|
1524
|
-
`dark` in light app → shadcn `dark:` buttons; in dark app → explicit
|
|
1525
|
-
light-surface button overrides.
|
|
1526
|
-
*/}
|
|
1527
|
-
{anySelected && (
|
|
1528
|
-
<div
|
|
1529
|
-
role="status"
|
|
1530
|
-
aria-live="polite"
|
|
1531
|
-
aria-label={`${selected.size} row${selected.size !== 1 ? "s" : ""} selected`}
|
|
1532
|
-
data-exxat-bulk-bar=""
|
|
1533
|
-
style={bulkBarFixedStyle}
|
|
1534
|
-
className={cn(
|
|
1535
|
-
"flex min-w-0 max-w-full items-stretch overflow-hidden",
|
|
1536
|
-
isAppDark
|
|
1537
|
-
? "rounded-lg border border-zinc-300/80 bg-zinc-100 text-zinc-900 shadow-lg"
|
|
1538
|
-
: "rounded-lg border border-zinc-800 bg-zinc-900 text-zinc-100 shadow-lg",
|
|
1539
|
-
"animate-in fade-in-0 duration-150",
|
|
1540
|
-
"w-auto max-w-none",
|
|
1541
|
-
)}
|
|
1542
|
-
>
|
|
1543
|
-
<div
|
|
1544
|
-
className={cn(
|
|
1545
|
-
"flex shrink-0 items-center gap-2 border-r py-2.5 pl-3 pr-2",
|
|
1546
|
-
isAppDark ? "border-zinc-300/50" : "border-zinc-600/50",
|
|
1547
|
-
)}
|
|
1548
|
-
aria-hidden="true"
|
|
1549
|
-
>
|
|
1550
|
-
<span
|
|
1551
|
-
className={cn(
|
|
1552
|
-
"inline-flex size-8 items-center justify-center rounded-md",
|
|
1553
|
-
isAppDark ? "text-zinc-500" : "text-zinc-400",
|
|
1554
|
-
)}
|
|
1555
|
-
aria-hidden="true"
|
|
1556
|
-
>
|
|
1557
|
-
<i className="fa-light fa-clipboard-list text-[1.1rem] leading-none" />
|
|
1558
|
-
</span>
|
|
1559
|
-
<span
|
|
1560
|
-
className={cn(
|
|
1561
|
-
"min-w-6 rounded-md px-1.5 py-0.5 text-center text-xs font-semibold leading-none tabular-nums",
|
|
1562
|
-
isAppDark ? "bg-zinc-200/90 text-zinc-900" : "bg-zinc-800 text-zinc-100",
|
|
1563
|
-
)}
|
|
1564
|
-
>
|
|
1565
|
-
{selected.size}
|
|
1566
|
-
</span>
|
|
1567
|
-
</div>
|
|
1568
|
-
|
|
1569
|
-
<div
|
|
1570
|
-
className={cn(
|
|
1571
|
-
"flex min-w-0 min-h-0 flex-1 items-stretch",
|
|
1572
|
-
!isAppDark && "dark",
|
|
1573
|
-
isAppDark && BULK_BAR_ON_LIGHT_STRIP,
|
|
1574
|
-
)}
|
|
1575
|
-
>
|
|
1576
|
-
<div
|
|
1577
|
-
className={cn(
|
|
1578
|
-
"min-w-0 flex-1 self-center",
|
|
1579
|
-
"overflow-x-auto overscroll-x-contain [scrollbar-width:thin] [touch-action:pan-x]",
|
|
1580
|
-
)}
|
|
1581
|
-
>
|
|
1582
|
-
<div className="flex w-max min-w-0 max-w-full flex-nowrap items-center gap-2 py-2.5 pl-2 pr-2">
|
|
1583
|
-
{bulkActionsSlot ? (
|
|
1584
|
-
bulkActionsSlot(selected, rows)
|
|
1585
|
-
) : (
|
|
1586
|
-
<>
|
|
1587
|
-
<Button size="sm" variant="outline" className="shrink-0">
|
|
1588
|
-
<i className="fa-light fa-arrow-down-to-line" aria-hidden="true" /> Export
|
|
1589
|
-
</Button>
|
|
1590
|
-
<Button size="sm" variant="destructive" className="shrink-0">
|
|
1591
|
-
<i className="fa-light fa-trash" aria-hidden="true" /> Delete
|
|
1592
|
-
</Button>
|
|
1593
|
-
</>
|
|
1594
|
-
)}
|
|
1595
|
-
</div>
|
|
1596
|
-
</div>
|
|
1597
|
-
|
|
1598
|
-
<div
|
|
1599
|
-
className={cn(
|
|
1600
|
-
"flex shrink-0 items-center border-l py-2.5 pl-2 pr-2.5",
|
|
1601
|
-
isAppDark ? "border-zinc-300/50" : "border-zinc-600/50",
|
|
1602
|
-
)}
|
|
1603
|
-
>
|
|
1604
|
-
<Tip label="Clear selection" side="top">
|
|
1605
|
-
<Button
|
|
1606
|
-
type="button"
|
|
1607
|
-
size="icon-sm"
|
|
1608
|
-
variant="ghost"
|
|
1609
|
-
aria-label="Clear selection"
|
|
1610
|
-
onClick={() => setSelected(new Set())}
|
|
1611
|
-
className="shrink-0"
|
|
1612
|
-
>
|
|
1613
|
-
<i className="fa-light fa-xmark" aria-hidden="true" />
|
|
1614
|
-
</Button>
|
|
1615
|
-
</Tip>
|
|
1616
|
-
</div>
|
|
1617
|
-
</div>
|
|
1618
|
-
</div>
|
|
1619
|
-
)}
|
|
1620
|
-
</div>
|
|
1621
|
-
)
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
function DataTableWithInternalState<TData extends Record<string, unknown>>(props: DataTableExtendedProps<TData>) {
|
|
1625
|
-
const state = useTableState(props.data, props.columns, props.defaultSort, props.paginationOverride)
|
|
1626
|
-
return <DataTableInner {...props} state={state} />
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
export function DataTable<TData extends Record<string, unknown>>(props: DataTableExtendedProps<TData>) {
|
|
1630
|
-
if (props.state) {
|
|
1631
|
-
return <DataTableInner {...props} state={props.state} />
|
|
1632
|
-
}
|
|
1633
|
-
return <DataTableWithInternalState {...props} />
|
|
1634
|
-
}
|
|
1
|
+
export * from "@exxatdesignux/ui/components/data-table"
|