@dashforge/tw 0.5.0-beta → 0.7.0-beta
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 +526 -0
- package/THEME-AUDIT.md +175 -0
- package/dist/index.esm.js +2504 -51
- package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -1
- package/dist/src/components/Box/box.variants.d.ts.map +1 -1
- package/dist/src/components/DataGrid/DataGrid.d.ts +29 -0
- package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -0
- package/dist/src/components/DataGrid/dataGrid.types.d.ts +171 -0
- package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts +216 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
- package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
- package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
- package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
- package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
- package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
- package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
- package/dist/src/components/Table/Table.d.ts +24 -0
- package/dist/src/components/Table/Table.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderButton.d.ts +13 -0
- package/dist/src/components/Table/cells/RenderButton.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderChip.d.ts +80 -0
- package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderText.d.ts +21 -0
- package/dist/src/components/Table/cells/RenderText.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderTwoLine.d.ts +20 -0
- package/dist/src/components/Table/cells/RenderTwoLine.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RowActionsMenu.d.ts +36 -0
- package/dist/src/components/Table/cells/RowActionsMenu.d.ts.map +1 -0
- package/dist/src/components/Table/cells/index.d.ts +11 -0
- package/dist/src/components/Table/cells/index.d.ts.map +1 -0
- package/dist/src/components/Table/table.types.d.ts +312 -0
- package/dist/src/components/Table/table.types.d.ts.map +1 -0
- package/dist/src/components/Table/table.variants.d.ts +285 -0
- package/dist/src/components/Table/table.variants.d.ts.map +1 -0
- package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
- package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
- package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
- package/dist/src/components/_shared/data/getNestedValue.d.ts +20 -0
- package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts +45 -0
- package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useControllableState.d.ts +23 -0
- package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useDebouncedValue.d.ts +10 -0
- package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useTableFilter.d.ts +13 -0
- package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useTableSearch.d.ts +23 -0
- package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useTableSelection.d.ts +25 -0
- package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useTableSort.d.ts +20 -0
- package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
- package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
- package/dist/src/index.d.ts +10 -1
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/Accordion/accordion.variants.ts +5 -3
- package/src/components/Box/Box.test.tsx +32 -11
- package/src/components/Box/box.variants.ts +23 -4
- package/src/components/DataGrid/DataGrid.test.tsx +496 -0
- package/src/components/DataGrid/DataGrid.tsx +831 -0
- package/src/components/DataGrid/dataGrid.types.ts +189 -0
- package/src/components/DataGrid/dataGrid.variants.ts +144 -0
- package/src/components/Dialog/dialog.variants.ts +10 -6
- package/src/components/Divider/Divider.test.tsx +3 -1
- package/src/components/Divider/Divider.tsx +1 -1
- package/src/components/Divider/divider.variants.ts +2 -1
- package/src/components/Pagination/Pagination.tsx +3 -1
- package/src/components/Pagination/pagination.variants.ts +19 -13
- package/src/components/Popover/popover.variants.ts +10 -4
- package/src/components/Skeleton/skeleton.variants.ts +2 -1
- package/src/components/Table/Table.test.tsx +603 -0
- package/src/components/Table/Table.tsx +816 -0
- package/src/components/Table/cells/RenderButton.tsx +23 -0
- package/src/components/Table/cells/RenderChip.tsx +103 -0
- package/src/components/Table/cells/RenderText.tsx +37 -0
- package/src/components/Table/cells/RenderTwoLine.tsx +34 -0
- package/src/components/Table/cells/RowActionsMenu.tsx +147 -0
- package/src/components/Table/cells/index.ts +14 -0
- package/src/components/Table/table.types.ts +346 -0
- package/src/components/Table/table.variants.ts +207 -0
- package/src/components/Tabs/tabs.variants.ts +19 -8
- package/src/components/Tooltip/tooltip.variants.ts +12 -2
- package/src/components/Typography/Typography.test.tsx +3 -1
- package/src/components/Typography/typography.variants.ts +6 -2
- package/src/components/_shared/data/getNestedValue.test.ts +50 -0
- package/src/components/_shared/data/getNestedValue.ts +32 -0
- package/src/components/_shared/data/useColumnAutoDetect.test.ts +123 -0
- package/src/components/_shared/data/useColumnAutoDetect.ts +100 -0
- package/src/components/_shared/data/useControllableState.ts +48 -0
- package/src/components/_shared/data/useDebouncedValue.ts +24 -0
- package/src/components/_shared/data/useTableFilter.ts +37 -0
- package/src/components/_shared/data/useTableSearch.test.ts +115 -0
- package/src/components/_shared/data/useTableSearch.ts +66 -0
- package/src/components/_shared/data/useTableSelection.test.ts +100 -0
- package/src/components/_shared/data/useTableSelection.ts +100 -0
- package/src/components/_shared/data/useTableSort.test.ts +150 -0
- package/src/components/_shared/data/useTableSort.ts +141 -0
- package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
- package/src/components/_shared/data/useVirtualizer.ts +199 -0
- package/src/components/_shared/themeIdentity.test.ts +113 -0
- package/src/index.ts +59 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
2
|
-
import { useMemo, forwardRef, useContext, useId, useRef, useEffect, useCallback, useState, Fragment, createContext, Children, isValidElement, cloneElement } from 'react';
|
|
2
|
+
import { useMemo, forwardRef, useContext, useId, useRef, useEffect, useCallback, useState, Fragment, createContext, Children, isValidElement, cloneElement, useLayoutEffect } from 'react';
|
|
3
3
|
import { Slot } from '@radix-ui/react-slot';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -5292,8 +5292,12 @@ const SnackbarContext = /*#__PURE__*/ createContext(null);
|
|
|
5292
5292
|
warning: 'text-warning-700 dark:text-warning-400',
|
|
5293
5293
|
danger: 'text-danger-700 dark:text-danger-400',
|
|
5294
5294
|
info: 'text-info-700 dark:text-info-400',
|
|
5295
|
-
|
|
5296
|
-
|
|
5295
|
+
// Neutral palette auto-inverts via the dashforgePreset() CSS-var
|
|
5296
|
+
// swap — no `dark:` variants. `text-neutral-900` resolves to
|
|
5297
|
+
// dark text in light mode and light text in dark mode; same
|
|
5298
|
+
// applies to `text-neutral-600` (muted).
|
|
5299
|
+
neutral: 'text-neutral-900',
|
|
5300
|
+
muted: 'text-neutral-600'
|
|
5297
5301
|
},
|
|
5298
5302
|
/*
|
|
5299
5303
|
* `weight` overrides the variant's default weight. When unset, the
|
|
@@ -5494,7 +5498,13 @@ Typography.displayName = 'Typography';
|
|
|
5494
5498
|
*/ variant: {
|
|
5495
5499
|
plain: '',
|
|
5496
5500
|
outlined: 'border',
|
|
5497
|
-
|
|
5501
|
+
// `bg-white` is hardcoded (does NOT auto-invert via CSS vars),
|
|
5502
|
+
// so the `dark:` variant is required. Target `dark:bg-neutral-100`
|
|
5503
|
+
// resolves to `#171717` via the inversion → one elevation tier
|
|
5504
|
+
// above the page surface (`bg-neutral-50` = `#0a0a0a`) in dark
|
|
5505
|
+
// mode. `dark:bg-neutral-900` was wrong (resolved to near-white
|
|
5506
|
+
// in dark mode — overpowering elevation).
|
|
5507
|
+
elevated: 'bg-white dark:bg-neutral-100',
|
|
5498
5508
|
soft: '',
|
|
5499
5509
|
solid: ''
|
|
5500
5510
|
},
|
|
@@ -5666,7 +5676,11 @@ Typography.displayName = 'Typography';
|
|
|
5666
5676
|
{
|
|
5667
5677
|
variant: 'outlined',
|
|
5668
5678
|
color: 'neutral',
|
|
5669
|
-
|
|
5679
|
+
// border-neutral-200 auto-inverts via CSS var swap (no `dark:`
|
|
5680
|
+
// needed). bg-white is hardcoded — `dark:bg-neutral-100` keeps
|
|
5681
|
+
// elevation tier above page surface in dark mode (NOT
|
|
5682
|
+
// `dark:bg-neutral-900` which renders near-white).
|
|
5683
|
+
class: 'border-neutral-200 bg-white dark:bg-neutral-100'
|
|
5670
5684
|
},
|
|
5671
5685
|
// ─── soft × color ─────────────────────────────────────────────────
|
|
5672
5686
|
{
|
|
@@ -5702,7 +5716,9 @@ Typography.displayName = 'Typography';
|
|
|
5702
5716
|
{
|
|
5703
5717
|
variant: 'soft',
|
|
5704
5718
|
color: 'neutral',
|
|
5705
|
-
|
|
5719
|
+
// Both neutral classes auto-invert via the dashforgePreset() CSS
|
|
5720
|
+
// var swap — no `dark:` variants needed.
|
|
5721
|
+
class: 'bg-neutral-100 text-neutral-900'
|
|
5706
5722
|
},
|
|
5707
5723
|
// ─── solid × color ────────────────────────────────────────────────
|
|
5708
5724
|
{
|
|
@@ -5738,7 +5754,14 @@ Typography.displayName = 'Typography';
|
|
|
5738
5754
|
{
|
|
5739
5755
|
variant: 'solid',
|
|
5740
5756
|
color: 'neutral',
|
|
5741
|
-
|
|
5757
|
+
// Solid neutral is the high-emphasis "always-foreground accent"
|
|
5758
|
+
// variant. Both classes auto-invert: dark surface + light text
|
|
5759
|
+
// in light mode (`#171717` / `#fafafa`) flips to light surface +
|
|
5760
|
+
// dark text in dark mode (`#f5f5f5` / `#0a0a0a`). The
|
|
5761
|
+
// accent inverts WITH the page surface — consistent with the
|
|
5762
|
+
// identity rule. Previously used `text-white` (no auto-invert)
|
|
5763
|
+
// which forced double-inversion to keep readable.
|
|
5764
|
+
class: 'bg-neutral-900 text-neutral-50'
|
|
5742
5765
|
}
|
|
5743
5766
|
],
|
|
5744
5767
|
defaultVariants: {
|
|
@@ -6526,7 +6549,8 @@ Container.displayName = 'Container';
|
|
|
6526
6549
|
dotted: 'border-dotted'
|
|
6527
6550
|
},
|
|
6528
6551
|
color: {
|
|
6529
|
-
|
|
6552
|
+
// Neutral palette auto-inverts via CSS-var swap (no `dark:`).
|
|
6553
|
+
neutral: 'border-neutral-200',
|
|
6530
6554
|
primary: 'border-primary-300 dark:border-primary-700',
|
|
6531
6555
|
secondary: 'border-secondary-300 dark:border-secondary-700',
|
|
6532
6556
|
success: 'border-success-300 dark:border-success-700',
|
|
@@ -6668,7 +6692,7 @@ Container.displayName = 'Container';
|
|
|
6668
6692
|
className: cn(lineCommon, leftStubClass)
|
|
6669
6693
|
}),
|
|
6670
6694
|
jsx("span", {
|
|
6671
|
-
className: cn('shrink-0 text-sm text-neutral-500
|
|
6695
|
+
className: cn('shrink-0 text-sm text-neutral-500', labelPadding),
|
|
6672
6696
|
children: children
|
|
6673
6697
|
}),
|
|
6674
6698
|
jsx("span", {
|
|
@@ -6821,21 +6845,25 @@ VisuallyHidden.displayName = 'VisuallyHidden';
|
|
|
6821
6845
|
],
|
|
6822
6846
|
content: [
|
|
6823
6847
|
'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
|
|
6824
|
-
|
|
6825
|
-
|
|
6848
|
+
// bg-white hardcoded (doesn't auto-invert) so `dark:` is needed.
|
|
6849
|
+
// Target `dark:bg-neutral-100` = #171717 in dark mode (one
|
|
6850
|
+
// elevation tier above page surface).
|
|
6851
|
+
'w-full bg-white dark:bg-neutral-100 rounded-lg shadow-xl',
|
|
6852
|
+
// border-neutral-200 auto-inverts — no `dark:` needed.
|
|
6853
|
+
'border border-neutral-200',
|
|
6826
6854
|
'p-6 flex flex-col gap-4 max-h-[90vh]',
|
|
6827
6855
|
'focus:outline-none',
|
|
6828
6856
|
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
6829
6857
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
6830
6858
|
'motion-reduce:transition-none motion-reduce:duration-0'
|
|
6831
6859
|
],
|
|
6832
|
-
|
|
6833
|
-
|
|
6860
|
+
// All neutral text classes auto-invert via CSS-var swap.
|
|
6861
|
+
title: 'text-lg font-semibold leading-tight text-neutral-900',
|
|
6862
|
+
description: 'text-sm text-neutral-600',
|
|
6834
6863
|
closeButton: [
|
|
6835
6864
|
'absolute top-4 right-4 inline-flex items-center justify-center',
|
|
6836
6865
|
'h-8 w-8 rounded-md text-neutral-500 hover:text-neutral-900',
|
|
6837
|
-
'
|
|
6838
|
-
'hover:bg-neutral-100 dark:hover:bg-neutral-800',
|
|
6866
|
+
'hover:bg-neutral-100',
|
|
6839
6867
|
'focus:outline-none focus:ring-2 focus:ring-primary-500',
|
|
6840
6868
|
'transition-colors'
|
|
6841
6869
|
],
|
|
@@ -6952,21 +6980,32 @@ const tabsVariants = tv({
|
|
|
6952
6980
|
variants: {
|
|
6953
6981
|
variant: {
|
|
6954
6982
|
underline: {
|
|
6955
|
-
|
|
6983
|
+
// Neutral palette auto-inverts via CSS-var swap.
|
|
6984
|
+
list: 'border-b border-neutral-200',
|
|
6956
6985
|
trigger: [
|
|
6957
|
-
'px-3 py-2 border-b-2 border-transparent text-neutral-600
|
|
6958
|
-
'hover:text-neutral-900
|
|
6986
|
+
'px-3 py-2 border-b-2 border-transparent text-neutral-600',
|
|
6987
|
+
'hover:text-neutral-900',
|
|
6988
|
+
// Primary palette does NOT auto-invert — `dark:` shift is
|
|
6989
|
+
// an intentional tone refinement (kept).
|
|
6959
6990
|
'data-[state=active]:border-primary-500 data-[state=active]:text-primary-700',
|
|
6960
6991
|
'dark:data-[state=active]:text-primary-400'
|
|
6961
6992
|
]
|
|
6962
6993
|
},
|
|
6963
6994
|
pill: {
|
|
6964
|
-
|
|
6995
|
+
// Pill list bg uses the elevation tier — auto-inverts.
|
|
6996
|
+
list: 'rounded-lg bg-neutral-100 p-1',
|
|
6965
6997
|
trigger: [
|
|
6966
|
-
'px-3 py-1.5 rounded-md text-neutral-600
|
|
6967
|
-
'hover:text-neutral-900
|
|
6968
|
-
|
|
6969
|
-
|
|
6998
|
+
'px-3 py-1.5 rounded-md text-neutral-600',
|
|
6999
|
+
'hover:text-neutral-900',
|
|
7000
|
+
// Active pill is one tier above the list bg
|
|
7001
|
+
// (`bg-neutral-100`-inverted = #171717 in dark).
|
|
7002
|
+
// `bg-white` is hardcoded so a `dark:` is required —
|
|
7003
|
+
// target `dark:bg-neutral-200` (= #262626 in dark) sits
|
|
7004
|
+
// ONE TIER above the list bg, preserving the elevation
|
|
7005
|
+
// semantic from light mode (where `bg-white` sits above
|
|
7006
|
+
// `bg-neutral-100` = #f5f5f5).
|
|
7007
|
+
'data-[state=active]:bg-white dark:data-[state=active]:bg-neutral-200',
|
|
7008
|
+
'data-[state=active]:text-neutral-900',
|
|
6970
7009
|
'data-[state=active]:shadow-sm'
|
|
6971
7010
|
]
|
|
6972
7011
|
}
|
|
@@ -7042,14 +7081,24 @@ const tooltipVariants = tv({
|
|
|
7042
7081
|
slots: {
|
|
7043
7082
|
content: [
|
|
7044
7083
|
'z-50 max-w-xs rounded-md px-3 py-1.5 text-xs font-medium',
|
|
7084
|
+
// Auto-invert via the dashforgePreset() CSS-var swap:
|
|
7085
|
+
// - Light mode: `bg-neutral-900` = #171717 (dark surface) +
|
|
7086
|
+
// `text-neutral-50` = #fafafa (light text) — DARK tooltip
|
|
7087
|
+
// on the light page (high contrast).
|
|
7088
|
+
// - Dark mode: `bg-neutral-900` flips to #f5f5f5 (light) +
|
|
7089
|
+
// `text-neutral-50` flips to #0a0a0a (dark text) — LIGHT
|
|
7090
|
+
// tooltip on the dark page (high contrast against page).
|
|
7091
|
+
// Adding `dark:bg-neutral-50 dark:text-neutral-900` (the old
|
|
7092
|
+
// pattern) would force the dark-mode tooltip back to a dark
|
|
7093
|
+
// surface — invisible against the dark page.
|
|
7045
7094
|
'bg-neutral-900 text-neutral-50',
|
|
7046
|
-
'dark:bg-neutral-50 dark:text-neutral-900',
|
|
7047
7095
|
'shadow-md',
|
|
7048
7096
|
'data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95',
|
|
7049
7097
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
7050
7098
|
'motion-reduce:transition-none motion-reduce:duration-0'
|
|
7051
7099
|
],
|
|
7052
|
-
|
|
7100
|
+
// Arrow tracks the content bg — auto-inverts the same way.
|
|
7101
|
+
arrow: 'fill-neutral-900'
|
|
7053
7102
|
}
|
|
7054
7103
|
});
|
|
7055
7104
|
|
|
@@ -7114,15 +7163,21 @@ const tooltipVariants = tv({
|
|
|
7114
7163
|
const popoverVariants = tv({
|
|
7115
7164
|
slots: {
|
|
7116
7165
|
content: [
|
|
7117
|
-
|
|
7118
|
-
'
|
|
7119
|
-
|
|
7166
|
+
// Neutral border auto-inverts via CSS-var swap.
|
|
7167
|
+
'z-50 rounded-md border border-neutral-200',
|
|
7168
|
+
// bg-white hardcoded (no auto-invert) so `dark:` is required.
|
|
7169
|
+
// Target `dark:bg-neutral-100` resolves to #171717 in dark mode
|
|
7170
|
+
// (one elevation tier above the page surface).
|
|
7171
|
+
'bg-white dark:bg-neutral-100 p-4 shadow-lg',
|
|
7172
|
+
// Text auto-inverts.
|
|
7173
|
+
'text-sm text-neutral-900',
|
|
7120
7174
|
'focus:outline-none',
|
|
7121
7175
|
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
7122
7176
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
7123
7177
|
'motion-reduce:transition-none motion-reduce:duration-0'
|
|
7124
7178
|
],
|
|
7125
|
-
|
|
7179
|
+
// Arrow follows content bg: `fill-white` in light + matching dark target.
|
|
7180
|
+
arrow: 'fill-white dark:fill-neutral-100'
|
|
7126
7181
|
}
|
|
7127
7182
|
});
|
|
7128
7183
|
|
|
@@ -7173,11 +7228,13 @@ const popoverVariants = tv({
|
|
|
7173
7228
|
const accordionVariants = tv({
|
|
7174
7229
|
slots: {
|
|
7175
7230
|
root: 'w-full',
|
|
7176
|
-
|
|
7231
|
+
// Neutral palette auto-inverts via the dashforgePreset() CSS-var
|
|
7232
|
+
// swap — no `dark:` variant needed (would double-invert).
|
|
7233
|
+
item: 'border-b border-neutral-200',
|
|
7177
7234
|
header: 'flex',
|
|
7178
7235
|
trigger: [
|
|
7179
7236
|
'flex flex-1 items-center justify-between py-3 text-left',
|
|
7180
|
-
'text-sm font-medium text-neutral-900
|
|
7237
|
+
'text-sm font-medium text-neutral-900',
|
|
7181
7238
|
'transition-all hover:underline',
|
|
7182
7239
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:rounded',
|
|
7183
7240
|
'[&[data-state=open]>svg]:rotate-180',
|
|
@@ -7185,7 +7242,7 @@ const accordionVariants = tv({
|
|
|
7185
7242
|
'motion-reduce:transition-none'
|
|
7186
7243
|
],
|
|
7187
7244
|
content: [
|
|
7188
|
-
'overflow-hidden text-sm text-neutral-700
|
|
7245
|
+
'overflow-hidden text-sm text-neutral-700',
|
|
7189
7246
|
'data-[state=open]:animate-accordion-down',
|
|
7190
7247
|
'data-[state=closed]:animate-accordion-up',
|
|
7191
7248
|
'motion-reduce:animate-none'
|
|
@@ -7316,7 +7373,8 @@ const accordionVariants = tv({
|
|
|
7316
7373
|
slots: {
|
|
7317
7374
|
root: [
|
|
7318
7375
|
'block',
|
|
7319
|
-
|
|
7376
|
+
// Neutral palette auto-inverts via CSS-var swap (no `dark:`).
|
|
7377
|
+
'bg-neutral-200',
|
|
7320
7378
|
'motion-reduce:animate-none'
|
|
7321
7379
|
]
|
|
7322
7380
|
},
|
|
@@ -7430,28 +7488,34 @@ const accordionVariants = tv({
|
|
|
7430
7488
|
*/ const paginationVariants = tv({
|
|
7431
7489
|
slots: {
|
|
7432
7490
|
root: 'flex flex-wrap items-center gap-3',
|
|
7433
|
-
|
|
7491
|
+
// All neutral classes auto-invert via the dashforgePreset() CSS-var
|
|
7492
|
+
// swap. `bg-neutral-50` provides the button surface (matches page
|
|
7493
|
+
// surface; the border gives the separation). `hover:bg-neutral-100`
|
|
7494
|
+
// is the one-tier elevation that works in both modes.
|
|
7495
|
+
summary: 'text-neutral-600',
|
|
7434
7496
|
list: 'inline-flex items-center gap-1',
|
|
7435
7497
|
pageButton: [
|
|
7436
7498
|
'inline-flex items-center justify-center rounded-md',
|
|
7437
|
-
'border border-neutral-300
|
|
7438
|
-
'bg-
|
|
7439
|
-
'text-neutral-700
|
|
7440
|
-
'hover:bg-neutral-
|
|
7499
|
+
'border border-neutral-300',
|
|
7500
|
+
'bg-neutral-50',
|
|
7501
|
+
'text-neutral-700',
|
|
7502
|
+
'hover:bg-neutral-100',
|
|
7441
7503
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
7442
7504
|
'transition-colors motion-reduce:transition-none',
|
|
7443
7505
|
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none'
|
|
7444
7506
|
],
|
|
7445
7507
|
activeButton: [
|
|
7508
|
+
// Primary palette does NOT auto-invert — `dark:` shift is a
|
|
7509
|
+
// design choice for tone refinement (kept).
|
|
7446
7510
|
'bg-primary-500 text-white border-primary-500',
|
|
7447
7511
|
'hover:bg-primary-600 dark:hover:bg-primary-400'
|
|
7448
7512
|
],
|
|
7449
7513
|
navButton: [
|
|
7450
7514
|
'inline-flex items-center justify-center rounded-md',
|
|
7451
|
-
'border border-neutral-300
|
|
7452
|
-
'bg-
|
|
7453
|
-
'text-neutral-700
|
|
7454
|
-
'hover:bg-neutral-
|
|
7515
|
+
'border border-neutral-300',
|
|
7516
|
+
'bg-neutral-50',
|
|
7517
|
+
'text-neutral-700',
|
|
7518
|
+
'hover:bg-neutral-100',
|
|
7455
7519
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
7456
7520
|
'transition-colors motion-reduce:transition-none',
|
|
7457
7521
|
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none'
|
|
@@ -7459,12 +7523,12 @@ const accordionVariants = tv({
|
|
|
7459
7523
|
ellipsis: 'inline-flex items-center justify-center text-neutral-500 px-1',
|
|
7460
7524
|
pageSizeSelector: [
|
|
7461
7525
|
'inline-flex items-center gap-2',
|
|
7462
|
-
'text-neutral-700
|
|
7526
|
+
'text-neutral-700'
|
|
7463
7527
|
],
|
|
7464
7528
|
jumpInput: [
|
|
7465
|
-
'rounded-md border border-neutral-300
|
|
7466
|
-
'bg-
|
|
7467
|
-
'text-neutral-900
|
|
7529
|
+
'rounded-md border border-neutral-300',
|
|
7530
|
+
'bg-neutral-50',
|
|
7531
|
+
'text-neutral-900',
|
|
7468
7532
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
7469
7533
|
'transition-colors motion-reduce:transition-none'
|
|
7470
7534
|
]
|
|
@@ -7601,7 +7665,7 @@ function range(start, end) {
|
|
|
7601
7665
|
}, (_, i)=>start + i);
|
|
7602
7666
|
}
|
|
7603
7667
|
|
|
7604
|
-
const DEFAULT_LABELS = {
|
|
7668
|
+
const DEFAULT_LABELS$2 = {
|
|
7605
7669
|
first: 'First',
|
|
7606
7670
|
prev: 'Previous',
|
|
7607
7671
|
next: 'Next',
|
|
@@ -7649,7 +7713,7 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [
|
|
|
7649
7713
|
*/ function Pagination(props) {
|
|
7650
7714
|
var _slotProps_root, _slotProps_summary, _slotProps_summary1, _slotProps_list, _slotProps_navButton, _slotProps_navButton1, _slotProps_navButton2, _slotProps_navButton3, _slotProps_pageSizeSelector, _slotProps_jumpInput;
|
|
7651
7715
|
const { page, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS, siblingCount = 1, boundaryCount = 1, showFirstLast = true, showJumpInput = true, disabled = false, labels: labelsProp, variant = 'default', size = 'md', sx, slotProps } = props;
|
|
7652
|
-
const labels = _extends({}, DEFAULT_LABELS, labelsProp);
|
|
7716
|
+
const labels = _extends({}, DEFAULT_LABELS$2, labelsProp);
|
|
7653
7717
|
const v = paginationVariants({
|
|
7654
7718
|
variant,
|
|
7655
7719
|
size
|
|
@@ -7807,7 +7871,9 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [
|
|
|
7807
7871
|
onChange: handlePageSize,
|
|
7808
7872
|
disabled: disabled,
|
|
7809
7873
|
"aria-label": labels.perPage,
|
|
7810
|
-
className: cn(
|
|
7874
|
+
className: cn(// Auto-invert via CSS-var swap — no `dark:` variants needed
|
|
7875
|
+
// on neutral palette.
|
|
7876
|
+
'rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500'),
|
|
7811
7877
|
children: pageSizeOptions.map((opt)=>jsx("option", {
|
|
7812
7878
|
value: opt,
|
|
7813
7879
|
children: opt
|
|
@@ -7844,6 +7910,2393 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [
|
|
|
7844
7910
|
});
|
|
7845
7911
|
}
|
|
7846
7912
|
|
|
7913
|
+
/**
|
|
7914
|
+
* `<Table>` variant recipe.
|
|
7915
|
+
*
|
|
7916
|
+
* Slots:
|
|
7917
|
+
* - `root` — outer wrapper
|
|
7918
|
+
* - `toolbar` — search + filter chips row above table
|
|
7919
|
+
* - `search` — search input
|
|
7920
|
+
* - `scroll` — `<div class="overflow-auto">` wrapping the table
|
|
7921
|
+
* - `table` — `<table>` element
|
|
7922
|
+
* - `thead` — `<thead>` (sticky positioning here)
|
|
7923
|
+
* - `tbody` — `<tbody>`
|
|
7924
|
+
* - `headerRow` — `<tr>` inside `<thead>`
|
|
7925
|
+
* - `headerCell` — `<th>` cell
|
|
7926
|
+
* - `headerCellButton` — `<button>` inside sortable `<th>`
|
|
7927
|
+
* - `row` — `<tr>` inside `<tbody>` (data row)
|
|
7928
|
+
* - `cell` — `<td>` data cell
|
|
7929
|
+
* - `selectionCell` — first `<td>` when selection mode active
|
|
7930
|
+
* - `expandToggleCell` — second `<td>` when expandable active
|
|
7931
|
+
* - `expandedRow` — full-width `<tr>` rendered after an expanded row
|
|
7932
|
+
* - `rowActionsCell` — last `<td>` when `rowActions` provided
|
|
7933
|
+
* - `emptyState` — placeholder when rows is empty
|
|
7934
|
+
* - `loadingOverlay` — skeleton overlay when loading
|
|
7935
|
+
* - `bulkActionFooter` — sticky footer with bulk actions
|
|
7936
|
+
*
|
|
7937
|
+
* Variant axes:
|
|
7938
|
+
* - `variant` — visual row-division style:
|
|
7939
|
+
* - `plain` — minimal separators
|
|
7940
|
+
* - `lines` — Stripe-style thin horizontal dividers (default)
|
|
7941
|
+
* - `striped` — alternating row backgrounds (zebra)
|
|
7942
|
+
* - `bordered` — full cell borders
|
|
7943
|
+
* - `card` — each row rendered as a separate surface card
|
|
7944
|
+
* - `size` — `sm` / `md` (default) / `lg`
|
|
7945
|
+
* - `density` — `compact` (40px row) / `comfortable` (default 48px) / `spacious` (56px)
|
|
7946
|
+
*
|
|
7947
|
+
* Smart-default alignment (per column) is applied by the Table
|
|
7948
|
+
* component code, NOT here — it depends on the runtime-detected
|
|
7949
|
+
* value type, which the recipe doesn't see.
|
|
7950
|
+
*
|
|
7951
|
+
* **Dashforge identity rule** (memory: `feedback_dashforge_preset_is_identity`):
|
|
7952
|
+
* This recipe uses NO `dark:` variants on the neutral palette. The
|
|
7953
|
+
* `dashforgePreset()` CSS-variable swap auto-inverts neutral tokens
|
|
7954
|
+
* (`bg-neutral-50` is light surface in light mode, dark surface in
|
|
7955
|
+
* dark mode — same class, different physical color). Adding a
|
|
7956
|
+
* `dark:` variant on neutral palette = DOUBLE inversion = breaks
|
|
7957
|
+
* dark mode. The selected-row state uses the canonical LeftNav
|
|
7958
|
+
* `itemActive` pattern (`bg-primary-100 text-primary-900 font-medium`),
|
|
7959
|
+
* which works in both modes because the primary palette has no
|
|
7960
|
+
* inversion (both `bg` and `text` resolve to the same physical
|
|
7961
|
+
* colors regardless of mode → guaranteed contrast).
|
|
7962
|
+
*/ const tableVariants = tv({
|
|
7963
|
+
slots: {
|
|
7964
|
+
root: 'w-full',
|
|
7965
|
+
toolbar: 'flex flex-wrap items-center gap-3 mb-3',
|
|
7966
|
+
search: [
|
|
7967
|
+
'inline-flex items-center gap-2',
|
|
7968
|
+
'w-full max-w-sm rounded-md',
|
|
7969
|
+
'border border-neutral-300',
|
|
7970
|
+
// bg-neutral-50 auto-inverts via CSS var swap (light in light mode,
|
|
7971
|
+
// dark in dark mode). Same semantic as TextField wrapper.
|
|
7972
|
+
'bg-neutral-50',
|
|
7973
|
+
'focus-within:ring-2 focus-within:ring-primary-500',
|
|
7974
|
+
'transition-colors motion-reduce:transition-none'
|
|
7975
|
+
],
|
|
7976
|
+
scroll: 'relative w-full overflow-auto rounded-md',
|
|
7977
|
+
table: 'w-full caption-bottom text-left',
|
|
7978
|
+
// thead matches the page surface (auto-inverts). When sticky the
|
|
7979
|
+
// header sits opaque against the scroll content beneath it.
|
|
7980
|
+
thead: 'bg-neutral-50',
|
|
7981
|
+
tbody: '',
|
|
7982
|
+
headerRow: '',
|
|
7983
|
+
headerCell: [
|
|
7984
|
+
// Less prominent than the body — secondary-text intent
|
|
7985
|
+
// (auto-inverts: dark grey in light mode, light grey in dark).
|
|
7986
|
+
'text-neutral-700',
|
|
7987
|
+
'font-semibold whitespace-nowrap select-none',
|
|
7988
|
+
'border-b border-neutral-200'
|
|
7989
|
+
],
|
|
7990
|
+
headerCellButton: [
|
|
7991
|
+
'group inline-flex items-center gap-1 select-none',
|
|
7992
|
+
// hover increases prominence (auto-inverts).
|
|
7993
|
+
'hover:text-neutral-900',
|
|
7994
|
+
'focus:outline-none focus-visible:underline',
|
|
7995
|
+
'transition-colors motion-reduce:transition-none'
|
|
7996
|
+
],
|
|
7997
|
+
row: [
|
|
7998
|
+
'group/row transition-colors motion-reduce:transition-none',
|
|
7999
|
+
// bg-neutral-100 = "elevated above surface" in both modes via
|
|
8000
|
+
// auto-inversion. Slightly visible against the table bg.
|
|
8001
|
+
'hover:bg-neutral-100',
|
|
8002
|
+
// Canonical LeftNav `itemActive` pattern — primary palette
|
|
8003
|
+
// doesn't invert, so dark-navy text on light-blue bg in BOTH
|
|
8004
|
+
// modes. Guaranteed contrast.
|
|
8005
|
+
'data-[selected=true]:bg-primary-100',
|
|
8006
|
+
'data-[selected=true]:text-primary-900',
|
|
8007
|
+
'data-[selected=true]:font-medium'
|
|
8008
|
+
],
|
|
8009
|
+
cell: [
|
|
8010
|
+
// Auto-inverts — dark text in light mode, light text in dark mode.
|
|
8011
|
+
'text-neutral-900',
|
|
8012
|
+
'border-b border-neutral-200',
|
|
8013
|
+
'align-middle'
|
|
8014
|
+
],
|
|
8015
|
+
selectionCell: 'w-10 text-center',
|
|
8016
|
+
expandToggleCell: 'w-10 text-center',
|
|
8017
|
+
// Expanded panel uses one tier of elevation above the page surface.
|
|
8018
|
+
expandedRow: 'bg-neutral-100',
|
|
8019
|
+
rowActionsCell: [
|
|
8020
|
+
'w-12 text-right',
|
|
8021
|
+
// Row actions hidden until row is hovered or focus-within
|
|
8022
|
+
// (Pencil & Paper UX pattern: reveal-on-hover reduces visual density).
|
|
8023
|
+
'opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100',
|
|
8024
|
+
'transition-opacity motion-reduce:transition-none'
|
|
8025
|
+
],
|
|
8026
|
+
emptyState: [
|
|
8027
|
+
'py-12 text-center text-neutral-500'
|
|
8028
|
+
],
|
|
8029
|
+
loadingOverlay: [
|
|
8030
|
+
'pointer-events-none'
|
|
8031
|
+
],
|
|
8032
|
+
bulkActionFooter: [
|
|
8033
|
+
'sticky bottom-0 z-20',
|
|
8034
|
+
'flex flex-wrap items-center justify-between gap-3',
|
|
8035
|
+
'px-3 py-2 mt-0',
|
|
8036
|
+
'border-t border-neutral-200',
|
|
8037
|
+
// Surface-elevated bg + shadow ladder.
|
|
8038
|
+
'bg-neutral-50 shadow-md'
|
|
8039
|
+
]
|
|
8040
|
+
},
|
|
8041
|
+
variants: {
|
|
8042
|
+
variant: {
|
|
8043
|
+
plain: {},
|
|
8044
|
+
lines: {
|
|
8045
|
+
},
|
|
8046
|
+
striped: {
|
|
8047
|
+
// bg-neutral-100/60 = subtle elevation tier (auto-inverts).
|
|
8048
|
+
row: 'odd:bg-neutral-100/60'
|
|
8049
|
+
},
|
|
8050
|
+
bordered: {
|
|
8051
|
+
table: 'border border-neutral-200',
|
|
8052
|
+
cell: 'border-r last:border-r-0 border-neutral-200',
|
|
8053
|
+
headerCell: 'border-r last:border-r-0 border-neutral-200'
|
|
8054
|
+
},
|
|
8055
|
+
card: {
|
|
8056
|
+
scroll: 'overflow-visible',
|
|
8057
|
+
table: 'border-separate border-spacing-y-2',
|
|
8058
|
+
row: [
|
|
8059
|
+
'rounded-lg shadow-sm',
|
|
8060
|
+
// Each row becomes an elevated card above the surface.
|
|
8061
|
+
'bg-neutral-50'
|
|
8062
|
+
],
|
|
8063
|
+
cell: 'border-b-0 first:rounded-l-lg last:rounded-r-lg',
|
|
8064
|
+
headerCell: 'border-b-0'
|
|
8065
|
+
}
|
|
8066
|
+
},
|
|
8067
|
+
size: {
|
|
8068
|
+
sm: {
|
|
8069
|
+
headerCell: 'text-xs',
|
|
8070
|
+
cell: 'text-xs',
|
|
8071
|
+
search: 'text-xs'
|
|
8072
|
+
},
|
|
8073
|
+
md: {
|
|
8074
|
+
headerCell: 'text-sm',
|
|
8075
|
+
cell: 'text-sm',
|
|
8076
|
+
search: 'text-sm'
|
|
8077
|
+
},
|
|
8078
|
+
lg: {
|
|
8079
|
+
headerCell: 'text-base',
|
|
8080
|
+
cell: 'text-base',
|
|
8081
|
+
search: 'text-base'
|
|
8082
|
+
}
|
|
8083
|
+
},
|
|
8084
|
+
density: {
|
|
8085
|
+
// Pencil & Paper UX research: 40px / 48px / 56px row height.
|
|
8086
|
+
compact: {
|
|
8087
|
+
headerCell: 'px-2 py-1',
|
|
8088
|
+
cell: 'px-2 py-1'
|
|
8089
|
+
},
|
|
8090
|
+
comfortable: {
|
|
8091
|
+
headerCell: 'px-3 py-2',
|
|
8092
|
+
cell: 'px-3 py-2'
|
|
8093
|
+
},
|
|
8094
|
+
spacious: {
|
|
8095
|
+
headerCell: 'px-4 py-3',
|
|
8096
|
+
cell: 'px-4 py-3'
|
|
8097
|
+
}
|
|
8098
|
+
},
|
|
8099
|
+
stickyHeader: {
|
|
8100
|
+
true: {
|
|
8101
|
+
thead: 'sticky top-0 z-10'
|
|
8102
|
+
},
|
|
8103
|
+
false: {}
|
|
8104
|
+
}
|
|
8105
|
+
},
|
|
8106
|
+
defaultVariants: {
|
|
8107
|
+
variant: 'lines',
|
|
8108
|
+
size: 'md',
|
|
8109
|
+
density: 'comfortable',
|
|
8110
|
+
stickyHeader: true
|
|
8111
|
+
}
|
|
8112
|
+
});
|
|
8113
|
+
|
|
8114
|
+
/**
|
|
8115
|
+
* Generic "controllable" state hook — switches between controlled
|
|
8116
|
+
* (via parent-supplied value + setter) and uncontrolled (internal
|
|
8117
|
+
* `useState`).
|
|
8118
|
+
*
|
|
8119
|
+
* - If `controlledValue !== undefined`, the hook is in **controlled**
|
|
8120
|
+
* mode: the internal state is ignored; the returned setter calls
|
|
8121
|
+
* `onControlledChange` only.
|
|
8122
|
+
* - Otherwise it's in **uncontrolled** mode: internal `useState`,
|
|
8123
|
+
* defaulting to `defaultValue`. The optional `onControlledChange`
|
|
8124
|
+
* is still fired on every change so the parent can react.
|
|
8125
|
+
*
|
|
8126
|
+
* The control mode is determined per-render. Mixing modes (passing
|
|
8127
|
+
* `controlledValue` undefined sometimes) is a consumer bug — we don't
|
|
8128
|
+
* try to "remember" the last mode (React's rules-of-hooks make that
|
|
8129
|
+
* unsafe).
|
|
8130
|
+
*/ function useControllableState(args) {
|
|
8131
|
+
const { controlledValue, defaultValue, onChange } = args;
|
|
8132
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
8133
|
+
const isControlled = controlledValue !== undefined;
|
|
8134
|
+
const value = isControlled ? controlledValue : uncontrolledValue;
|
|
8135
|
+
// Stable ref to `onChange` so the setter doesn't change identity
|
|
8136
|
+
// every render (consumer can pass an inline lambda safely).
|
|
8137
|
+
const onChangeRef = useRef(onChange);
|
|
8138
|
+
onChangeRef.current = onChange;
|
|
8139
|
+
const setValue = useCallback((next)=>{
|
|
8140
|
+
const resolved = typeof next === 'function' ? next(value) : next;
|
|
8141
|
+
if (!isControlled) {
|
|
8142
|
+
setUncontrolledValue(resolved);
|
|
8143
|
+
}
|
|
8144
|
+
onChangeRef.current == null ? void 0 : onChangeRef.current.call(onChangeRef, resolved);
|
|
8145
|
+
}, [
|
|
8146
|
+
isControlled,
|
|
8147
|
+
value
|
|
8148
|
+
]);
|
|
8149
|
+
return [
|
|
8150
|
+
value,
|
|
8151
|
+
setValue
|
|
8152
|
+
];
|
|
8153
|
+
}
|
|
8154
|
+
|
|
8155
|
+
/**
|
|
8156
|
+
* Returns a debounced version of `value` — only updates after `delayMs`
|
|
8157
|
+
* of no further changes. Used by the search input so we don't re-filter
|
|
8158
|
+
* the entire row set on every keystroke.
|
|
8159
|
+
*
|
|
8160
|
+
* `delayMs <= 0` short-circuits to the input value (no debounce —
|
|
8161
|
+
* useful for tests).
|
|
8162
|
+
*/ function useDebouncedValue(value, delayMs) {
|
|
8163
|
+
const [debounced, setDebounced] = useState(value);
|
|
8164
|
+
useEffect(()=>{
|
|
8165
|
+
if (delayMs <= 0) {
|
|
8166
|
+
setDebounced(value);
|
|
8167
|
+
return;
|
|
8168
|
+
}
|
|
8169
|
+
const timer = setTimeout(()=>setDebounced(value), delayMs);
|
|
8170
|
+
return ()=>clearTimeout(timer);
|
|
8171
|
+
}, [
|
|
8172
|
+
value,
|
|
8173
|
+
delayMs
|
|
8174
|
+
]);
|
|
8175
|
+
return debounced;
|
|
8176
|
+
}
|
|
8177
|
+
|
|
8178
|
+
/**
|
|
8179
|
+
* Walk a dotted path through an object and return the value, or
|
|
8180
|
+
* `undefined` if any segment is missing / nullish.
|
|
8181
|
+
*
|
|
8182
|
+
* Bug fix from the MUI-side Table (Sprint 4.1 motivation): the
|
|
8183
|
+
* existing wrapper typed `field: NestedKeyOf<T>` but performed a
|
|
8184
|
+
* flat `row[col.field]` lookup at runtime — nested keys silently
|
|
8185
|
+
* returned `undefined`. This helper makes the runtime match the
|
|
8186
|
+
* type contract.
|
|
8187
|
+
*
|
|
8188
|
+
* @example
|
|
8189
|
+
* ```ts
|
|
8190
|
+
* getNestedValue({ user: { name: 'Jane' } }, 'user.name') // 'Jane'
|
|
8191
|
+
* getNestedValue({ user: null }, 'user.name') // undefined
|
|
8192
|
+
* getNestedValue({}, 'a.b.c') // undefined
|
|
8193
|
+
* getNestedValue({ a: { b: 0 } }, 'a.b') // 0 (NOT undefined)
|
|
8194
|
+
* ```
|
|
8195
|
+
*/ function getNestedValue(row, path) {
|
|
8196
|
+
if (!path) return undefined;
|
|
8197
|
+
const segments = path.split('.');
|
|
8198
|
+
let current = row;
|
|
8199
|
+
for (const segment of segments){
|
|
8200
|
+
if (current == null) return undefined;
|
|
8201
|
+
if (typeof current !== 'object') return undefined;
|
|
8202
|
+
current = current[segment];
|
|
8203
|
+
}
|
|
8204
|
+
return current;
|
|
8205
|
+
}
|
|
8206
|
+
|
|
8207
|
+
/**
|
|
8208
|
+
* Stringify any value into a search-comparable form. Handles all
|
|
8209
|
+
* the edge cases that bit the MUI-side Table:
|
|
8210
|
+
*
|
|
8211
|
+
* - `null` / `undefined` → `''`
|
|
8212
|
+
* - `string` → as-is
|
|
8213
|
+
* - `number` / `bigint` → `String(value)`
|
|
8214
|
+
* - `boolean` → `'true'` / `'false'`
|
|
8215
|
+
* - `Date` → ISO 8601
|
|
8216
|
+
* - `Array` / object → `JSON.stringify` (best effort)
|
|
8217
|
+
*/ function stringifyForSearch(value) {
|
|
8218
|
+
if (value == null) return '';
|
|
8219
|
+
if (typeof value === 'string') return value;
|
|
8220
|
+
if (typeof value === 'number' || typeof value === 'bigint') return String(value);
|
|
8221
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
8222
|
+
if (value instanceof Date) return value.toISOString();
|
|
8223
|
+
if (Array.isArray(value)) {
|
|
8224
|
+
try {
|
|
8225
|
+
return value.map(stringifyForSearch).join(' ');
|
|
8226
|
+
} catch (unused) {
|
|
8227
|
+
return '';
|
|
8228
|
+
}
|
|
8229
|
+
}
|
|
8230
|
+
if (typeof value === 'object') {
|
|
8231
|
+
try {
|
|
8232
|
+
return JSON.stringify(value);
|
|
8233
|
+
} catch (unused) {
|
|
8234
|
+
return '';
|
|
8235
|
+
}
|
|
8236
|
+
}
|
|
8237
|
+
return String(value);
|
|
8238
|
+
}
|
|
8239
|
+
/**
|
|
8240
|
+
* Filters `rows` by `query` against the columns flagged
|
|
8241
|
+
* `searchable: true`. Empty / whitespace-only query is a no-op
|
|
8242
|
+
* (returns the original rows).
|
|
8243
|
+
*
|
|
8244
|
+
* Case-insensitive substring match. The caller is responsible for
|
|
8245
|
+
* debouncing the input (use `useDebouncedValue`).
|
|
8246
|
+
*/ function useTableSearch(rows, cols, query) {
|
|
8247
|
+
return useMemo(()=>{
|
|
8248
|
+
const trimmed = query.trim();
|
|
8249
|
+
if (!trimmed) return rows;
|
|
8250
|
+
const needle = trimmed.toLowerCase();
|
|
8251
|
+
const searchableCols = cols.filter((c)=>c.searchable);
|
|
8252
|
+
if (searchableCols.length === 0) return rows;
|
|
8253
|
+
return rows.filter((row)=>searchableCols.some((col)=>stringifyForSearch(getNestedValue(row, col.field)).toLowerCase().includes(needle)));
|
|
8254
|
+
}, [
|
|
8255
|
+
rows,
|
|
8256
|
+
cols,
|
|
8257
|
+
query
|
|
8258
|
+
]);
|
|
8259
|
+
}
|
|
8260
|
+
|
|
8261
|
+
/**
|
|
8262
|
+
* Filters `rows` by the per-column filter model. v1 only supports
|
|
8263
|
+
* the `contains` operator (text substring) — additional ops
|
|
8264
|
+
* (`equals`, `in`, `gte`, `lte`, `between`, `before`, `after`)
|
|
8265
|
+
* land in v1-bis once the filter UI surface for each shape is
|
|
8266
|
+
* fleshed out.
|
|
8267
|
+
*
|
|
8268
|
+
* AND semantics across columns: a row must match EVERY active
|
|
8269
|
+
* filter. Within a single filter, the op decides the predicate.
|
|
8270
|
+
*/ function useTableFilter(rows, filterModel) {
|
|
8271
|
+
return useMemo(()=>{
|
|
8272
|
+
if (!filterModel || filterModel.length === 0) return rows;
|
|
8273
|
+
return rows.filter((row)=>filterModel.every((filter)=>{
|
|
8274
|
+
const value = getNestedValue(row, filter.field);
|
|
8275
|
+
const stringValue = stringifyForSearch(value).toLowerCase();
|
|
8276
|
+
const needle = filter.value.trim().toLowerCase();
|
|
8277
|
+
if (!needle) return true; // empty filter == no-op
|
|
8278
|
+
switch(filter.op){
|
|
8279
|
+
case 'contains':
|
|
8280
|
+
return stringValue.includes(needle);
|
|
8281
|
+
default:
|
|
8282
|
+
return true;
|
|
8283
|
+
}
|
|
8284
|
+
}));
|
|
8285
|
+
}, [
|
|
8286
|
+
rows,
|
|
8287
|
+
filterModel
|
|
8288
|
+
]);
|
|
8289
|
+
}
|
|
8290
|
+
|
|
8291
|
+
/**
|
|
8292
|
+
* Default comparator. Handles the common primitive types in a
|
|
8293
|
+
* sensible way:
|
|
8294
|
+
*
|
|
8295
|
+
* - `null` / `undefined` always sort LAST (stable across asc/desc)
|
|
8296
|
+
* - `number` / `bigint` → numeric comparison
|
|
8297
|
+
* - `string` → locale-aware compare via `Intl.Collator`
|
|
8298
|
+
* - `Date` / ISO-string-date → numeric compare on epoch ms
|
|
8299
|
+
* - `boolean` → `false < true`
|
|
8300
|
+
* - mixed types fall back to string compare
|
|
8301
|
+
*
|
|
8302
|
+
* The locale collator is created once per call (cheap; cached
|
|
8303
|
+
* upstream via memoization).
|
|
8304
|
+
*/ function defaultCompare(a, b) {
|
|
8305
|
+
// Numbers
|
|
8306
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
8307
|
+
return a - b;
|
|
8308
|
+
}
|
|
8309
|
+
if (typeof a === 'bigint' && typeof b === 'bigint') {
|
|
8310
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
8311
|
+
}
|
|
8312
|
+
// Dates
|
|
8313
|
+
if (a instanceof Date && b instanceof Date) {
|
|
8314
|
+
return a.getTime() - b.getTime();
|
|
8315
|
+
}
|
|
8316
|
+
// Booleans
|
|
8317
|
+
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
8318
|
+
return a === b ? 0 : a ? 1 : -1;
|
|
8319
|
+
}
|
|
8320
|
+
// Strings (incl. ISO date strings — natural lexical ordering matches chronology)
|
|
8321
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
8322
|
+
return a.localeCompare(b, undefined, {
|
|
8323
|
+
numeric: true,
|
|
8324
|
+
sensitivity: 'base'
|
|
8325
|
+
});
|
|
8326
|
+
}
|
|
8327
|
+
// Fallback: stringify and compare
|
|
8328
|
+
return String(a).localeCompare(String(b), undefined, {
|
|
8329
|
+
numeric: true,
|
|
8330
|
+
sensitivity: 'base'
|
|
8331
|
+
});
|
|
8332
|
+
}
|
|
8333
|
+
/**
|
|
8334
|
+
* Applies the `sortModel` to `rows`. Returns a NEW sorted array
|
|
8335
|
+
* (does not mutate input). Stable sort guaranteed by the underlying
|
|
8336
|
+
* `Array.prototype.sort` in modern engines.
|
|
8337
|
+
*
|
|
8338
|
+
* Multi-column sort: items earlier in `sortModel` have higher
|
|
8339
|
+
* precedence. Equal values fall through to the next sort item; if
|
|
8340
|
+
* all sort items tie, the original order is preserved (stable).
|
|
8341
|
+
*/ function useTableSort(rows, cols, sortModel) {
|
|
8342
|
+
return useMemo(()=>{
|
|
8343
|
+
if (!sortModel || sortModel.length === 0) return rows;
|
|
8344
|
+
const sortConfigs = sortModel.map((item)=>{
|
|
8345
|
+
const col = cols.find((c)=>c.field === item.field);
|
|
8346
|
+
if (!col || !col.sortable) return null;
|
|
8347
|
+
const customCompare = typeof col.sortable === 'function' ? col.sortable : undefined;
|
|
8348
|
+
return {
|
|
8349
|
+
item,
|
|
8350
|
+
col,
|
|
8351
|
+
customCompare
|
|
8352
|
+
};
|
|
8353
|
+
}).filter((x)=>x != null);
|
|
8354
|
+
if (sortConfigs.length === 0) return rows;
|
|
8355
|
+
// Decorate-sort-undecorate for stability across engines.
|
|
8356
|
+
//
|
|
8357
|
+
// Per sort item: null/undefined values always sort LAST, regardless
|
|
8358
|
+
// of direction. This invariant lives outside the comparator so it's
|
|
8359
|
+
// not inverted by `-cmp` when direction is `desc`. (Without this
|
|
8360
|
+
// separation, `desc` flipped null-handling and placed nulls FIRST,
|
|
8361
|
+
// which broke user expectations — fixed in the v1 implementation.)
|
|
8362
|
+
return [
|
|
8363
|
+
...rows
|
|
8364
|
+
].sort((a, b)=>{
|
|
8365
|
+
for (const { item, customCompare } of sortConfigs){
|
|
8366
|
+
const aVal = customCompare ? a : getNestedValue(a, item.field);
|
|
8367
|
+
const bVal = customCompare ? b : getNestedValue(b, item.field);
|
|
8368
|
+
// Null-last (direction-independent)
|
|
8369
|
+
const aNull = aVal == null;
|
|
8370
|
+
const bNull = bVal == null;
|
|
8371
|
+
if (aNull && bNull) continue;
|
|
8372
|
+
if (aNull) return 1;
|
|
8373
|
+
if (bNull) return -1;
|
|
8374
|
+
const cmp = customCompare ? customCompare(a, b) : defaultCompare(aVal, bVal);
|
|
8375
|
+
if (cmp !== 0) {
|
|
8376
|
+
return item.direction === 'desc' ? -cmp : cmp;
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
return 0;
|
|
8380
|
+
});
|
|
8381
|
+
}, [
|
|
8382
|
+
rows,
|
|
8383
|
+
cols,
|
|
8384
|
+
sortModel
|
|
8385
|
+
]);
|
|
8386
|
+
}
|
|
8387
|
+
/**
|
|
8388
|
+
* Three-state sort cycler: `none` → `asc` → `desc` → `none`.
|
|
8389
|
+
*
|
|
8390
|
+
* Used by the header click handler. Returns the next sort model
|
|
8391
|
+
* to apply, given the current model + the clicked field + whether
|
|
8392
|
+
* the user is multi-sorting (shift key).
|
|
8393
|
+
*/ function cycleSortFor(current, field, multi) {
|
|
8394
|
+
const existing = current.find((s)=>s.field === field);
|
|
8395
|
+
const nextDirection = !existing ? 'asc' : existing.direction === 'asc' ? 'desc' : null;
|
|
8396
|
+
if (multi) {
|
|
8397
|
+
// Multi-sort: keep other entries, update / add / remove this one.
|
|
8398
|
+
const rest = current.filter((s)=>s.field !== field);
|
|
8399
|
+
if (nextDirection == null) return rest;
|
|
8400
|
+
return [
|
|
8401
|
+
...rest,
|
|
8402
|
+
{
|
|
8403
|
+
field,
|
|
8404
|
+
direction: nextDirection
|
|
8405
|
+
}
|
|
8406
|
+
];
|
|
8407
|
+
}
|
|
8408
|
+
// Single-sort: replace the whole model.
|
|
8409
|
+
if (nextDirection == null) return [];
|
|
8410
|
+
return [
|
|
8411
|
+
{
|
|
8412
|
+
field,
|
|
8413
|
+
direction: nextDirection
|
|
8414
|
+
}
|
|
8415
|
+
];
|
|
8416
|
+
}
|
|
8417
|
+
|
|
8418
|
+
/**
|
|
8419
|
+
* Selection logic helper. Given the controllable selection state +
|
|
8420
|
+
* the setter (from `useControllableState`), exposes ergonomic
|
|
8421
|
+
* helpers the Table calls from its render path.
|
|
8422
|
+
*
|
|
8423
|
+
* - `single` mode: at most one row selected. Toggling another
|
|
8424
|
+
* replaces the previous.
|
|
8425
|
+
* - `multiple` mode: any subset selectable; toggling adds/removes.
|
|
8426
|
+
* - `none` mode: every toggle is a no-op (the Table shouldn't
|
|
8427
|
+
* render the checkbox cells anyway, but this is defensive).
|
|
8428
|
+
*/ function useTableSelection(mode, selectedRowIds, setSelectedRowIds) {
|
|
8429
|
+
const selectedSet = useMemo(()=>new Set(selectedRowIds), [
|
|
8430
|
+
selectedRowIds
|
|
8431
|
+
]);
|
|
8432
|
+
const isSelected = useCallback((rowId)=>selectedSet.has(rowId), [
|
|
8433
|
+
selectedSet
|
|
8434
|
+
]);
|
|
8435
|
+
const toggleRow = useCallback((rowId)=>{
|
|
8436
|
+
if (mode === 'none') return;
|
|
8437
|
+
if (mode === 'single') {
|
|
8438
|
+
setSelectedRowIds(selectedSet.has(rowId) ? [] : [
|
|
8439
|
+
rowId
|
|
8440
|
+
]);
|
|
8441
|
+
return;
|
|
8442
|
+
}
|
|
8443
|
+
// multiple
|
|
8444
|
+
setSelectedRowIds((prev)=>{
|
|
8445
|
+
const next = new Set(prev);
|
|
8446
|
+
if (next.has(rowId)) next.delete(rowId);
|
|
8447
|
+
else next.add(rowId);
|
|
8448
|
+
return [
|
|
8449
|
+
...next
|
|
8450
|
+
];
|
|
8451
|
+
});
|
|
8452
|
+
}, [
|
|
8453
|
+
mode,
|
|
8454
|
+
selectedSet,
|
|
8455
|
+
setSelectedRowIds
|
|
8456
|
+
]);
|
|
8457
|
+
const toggleAll = useCallback((allRowIds)=>{
|
|
8458
|
+
if (mode !== 'multiple') return;
|
|
8459
|
+
const allSelected = allRowIds.every((id)=>selectedSet.has(id));
|
|
8460
|
+
setSelectedRowIds(allSelected ? [] : [
|
|
8461
|
+
...allRowIds
|
|
8462
|
+
]);
|
|
8463
|
+
}, [
|
|
8464
|
+
mode,
|
|
8465
|
+
selectedSet,
|
|
8466
|
+
setSelectedRowIds
|
|
8467
|
+
]);
|
|
8468
|
+
const clearSelection = useCallback(()=>{
|
|
8469
|
+
setSelectedRowIds([]);
|
|
8470
|
+
}, [
|
|
8471
|
+
setSelectedRowIds
|
|
8472
|
+
]);
|
|
8473
|
+
const isAllSelected = useCallback((allRowIds)=>{
|
|
8474
|
+
if (allRowIds.length === 0) return false;
|
|
8475
|
+
return allRowIds.every((id)=>selectedSet.has(id));
|
|
8476
|
+
}, [
|
|
8477
|
+
selectedSet
|
|
8478
|
+
]);
|
|
8479
|
+
const isIndeterminate = useCallback((allRowIds)=>{
|
|
8480
|
+
if (allRowIds.length === 0) return false;
|
|
8481
|
+
const some = allRowIds.some((id)=>selectedSet.has(id));
|
|
8482
|
+
const all = allRowIds.every((id)=>selectedSet.has(id));
|
|
8483
|
+
return some && !all;
|
|
8484
|
+
}, [
|
|
8485
|
+
selectedSet
|
|
8486
|
+
]);
|
|
8487
|
+
return {
|
|
8488
|
+
selectedRowIds,
|
|
8489
|
+
isSelected,
|
|
8490
|
+
toggleRow,
|
|
8491
|
+
toggleAll,
|
|
8492
|
+
clearSelection,
|
|
8493
|
+
isAllSelected,
|
|
8494
|
+
isIndeterminate
|
|
8495
|
+
};
|
|
8496
|
+
}
|
|
8497
|
+
|
|
8498
|
+
/**
|
|
8499
|
+
* Auto-detects the primitive type of each column by scanning the
|
|
8500
|
+
* first non-null value across the supplied rows. Drives smart
|
|
8501
|
+
* defaults for `align` and `monospace` on the cell render path.
|
|
8502
|
+
*
|
|
8503
|
+
* Scans only the first N rows (default 50) for performance — if the
|
|
8504
|
+
* dataset's first 50 rows are unrepresentative, the consumer can
|
|
8505
|
+
* override per-column via explicit `align` / `monospace` props.
|
|
8506
|
+
*
|
|
8507
|
+
* Returns `Map<columnField, inferredType>`.
|
|
8508
|
+
*/ function useColumnAutoDetect(rows, cols, sampleSize = 50) {
|
|
8509
|
+
return useMemo(()=>{
|
|
8510
|
+
const result = new Map();
|
|
8511
|
+
const sample = rows.slice(0, sampleSize);
|
|
8512
|
+
for (const col of cols){
|
|
8513
|
+
const field = col.field;
|
|
8514
|
+
let inferred = 'unknown';
|
|
8515
|
+
for (const row of sample){
|
|
8516
|
+
const value = getNestedValue(row, field);
|
|
8517
|
+
if (value == null) continue;
|
|
8518
|
+
inferred = inferType(value);
|
|
8519
|
+
break; // first non-null wins
|
|
8520
|
+
}
|
|
8521
|
+
result.set(field, inferred);
|
|
8522
|
+
}
|
|
8523
|
+
return result;
|
|
8524
|
+
}, [
|
|
8525
|
+
rows,
|
|
8526
|
+
cols,
|
|
8527
|
+
sampleSize
|
|
8528
|
+
]);
|
|
8529
|
+
}
|
|
8530
|
+
function inferType(value) {
|
|
8531
|
+
if (typeof value === 'number') return 'number';
|
|
8532
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
8533
|
+
if (value instanceof Date) return 'date';
|
|
8534
|
+
if (typeof value === 'string') {
|
|
8535
|
+
// Heuristic: ISO date string (YYYY-MM-DD[T...]) → 'date'
|
|
8536
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) return 'date';
|
|
8537
|
+
return 'string';
|
|
8538
|
+
}
|
|
8539
|
+
return 'unknown';
|
|
8540
|
+
}
|
|
8541
|
+
/**
|
|
8542
|
+
* Resolves the effective `align` for a column, honoring explicit
|
|
8543
|
+
* `col.align` first and falling back to the inferred type.
|
|
8544
|
+
*/ function resolveAlign(col, inferred) {
|
|
8545
|
+
if (col.align) return col.align;
|
|
8546
|
+
switch(inferred){
|
|
8547
|
+
case 'number':
|
|
8548
|
+
return 'right';
|
|
8549
|
+
case 'boolean':
|
|
8550
|
+
return 'center';
|
|
8551
|
+
default:
|
|
8552
|
+
return 'left';
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
/**
|
|
8556
|
+
* Resolves the effective `tabularNums` flag for a column.
|
|
8557
|
+
*
|
|
8558
|
+
* `tabular-nums` is a `font-variant-numeric` rule that makes every
|
|
8559
|
+
* digit occupy the same width — it does NOT change the font family.
|
|
8560
|
+
* Auto-applied for `number` and `date` columns so currency values
|
|
8561
|
+
* and ISO dates align cleanly. Explicit `col.tabularNums` wins.
|
|
8562
|
+
*/ function resolveTabularNums(col, inferred) {
|
|
8563
|
+
if (col.tabularNums !== undefined) return col.tabularNums;
|
|
8564
|
+
return inferred === 'number' || inferred === 'date';
|
|
8565
|
+
}
|
|
8566
|
+
/**
|
|
8567
|
+
* Resolves the effective `monospace` flag for a column.
|
|
8568
|
+
*
|
|
8569
|
+
* `font-mono` changes the font FAMILY to a monospace stack, which
|
|
8570
|
+
* would override the consumer's theme `font-sans`. The library
|
|
8571
|
+
* never auto-applies it — only an explicit `col.monospace: true`
|
|
8572
|
+
* opts in. Consumers wanting a custom mono stack configure it
|
|
8573
|
+
* through Tailwind's `theme.extend.fontFamily.mono` in their own
|
|
8574
|
+
* preset (the dashforgePreset does not own the `fontFamily` axis).
|
|
8575
|
+
*/ function resolveMonospace(col) {
|
|
8576
|
+
return col.monospace === true;
|
|
8577
|
+
}
|
|
8578
|
+
|
|
8579
|
+
const DEFAULT_LABELS$1 = {
|
|
8580
|
+
searchPlaceholder: 'Search...',
|
|
8581
|
+
noData: 'No data',
|
|
8582
|
+
loading: 'Loading…',
|
|
8583
|
+
ariaSortNone: 'Not sorted. Click to sort ascending.',
|
|
8584
|
+
ariaSortAscending: 'Sorted ascending. Click to sort descending.',
|
|
8585
|
+
ariaSortDescending: 'Sorted descending. Click to clear sort.',
|
|
8586
|
+
ariaSelectRow: 'Select row',
|
|
8587
|
+
ariaSelectAllRows: 'Select all rows',
|
|
8588
|
+
ariaExpandRow: 'Expand row',
|
|
8589
|
+
ariaCollapseRow: 'Collapse row',
|
|
8590
|
+
selectedCount: '{count} selected',
|
|
8591
|
+
clearSelection: 'Clear selection',
|
|
8592
|
+
filterColumn: 'Filter',
|
|
8593
|
+
filterApply: 'Apply',
|
|
8594
|
+
filterClear: 'Clear',
|
|
8595
|
+
density: 'Density',
|
|
8596
|
+
densityCompact: 'Compact',
|
|
8597
|
+
densityComfortable: 'Comfortable',
|
|
8598
|
+
densitySpacious: 'Spacious'
|
|
8599
|
+
};
|
|
8600
|
+
const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
|
|
8601
|
+
/**
|
|
8602
|
+
* Dashforge TW `<Table>` — declarative-first, market-grounded table.
|
|
8603
|
+
*
|
|
8604
|
+
* Design references:
|
|
8605
|
+
* - Visual: Stripe (clean line dividers, hover row actions, sticky header)
|
|
8606
|
+
* - Density: Pencil & Paper UX research (compact / comfortable / spacious)
|
|
8607
|
+
* - A11Y: W3C WAI Table tutorial (semantic `<table>`, scope, aria-sort)
|
|
8608
|
+
* - Column model: extended from Dashforge MUI Table existing API
|
|
8609
|
+
*
|
|
8610
|
+
* Smart defaults: column types auto-detected → numbers right-aligned
|
|
8611
|
+
* + tabular-nums, dates monospace, booleans center-aligned. Override
|
|
8612
|
+
* via explicit `align` / `monospace` per column.
|
|
8613
|
+
*
|
|
8614
|
+
* State (sort / search / filter / selection / expansion) is internal
|
|
8615
|
+
* by default; pass the controlled prop + matching change handler to
|
|
8616
|
+
* lift state up — same pattern as `<TextField>` value.
|
|
8617
|
+
*
|
|
8618
|
+
* i18n: pass `header: t('users.name')` for column headers; pass
|
|
8619
|
+
* `labels={{ searchPlaceholder: t('...'), ... }}` for internal default
|
|
8620
|
+
* strings.
|
|
8621
|
+
*/ function Table(props) {
|
|
8622
|
+
var _slotProps_root, _slotProps_toolbar, _slotProps_search, _slotProps_scroll, _slotProps_table, _slotProps_thead, _slotProps_headerRow, _slotProps_headerCell, _slotProps_selectionCell, _slotProps_headerCell1, _slotProps_expandToggleCell, _slotProps_headerCell2, _slotProps_tbody, _slotProps_row, _slotProps_cell, _slotProps_emptyState, _slotProps_bulkActionFooter;
|
|
8623
|
+
const { rows, cols, getRowId = DEFAULT_GET_ROW_ID, // State props (controllable)
|
|
8624
|
+
sortModel: sortModelProp, onSortChange, defaultSortModel, enableSearch = false, searchQuery: searchQueryProp, onSearchQueryChange, searchPlaceholder, searchDebounceMs = 200, filterModel: filterModelProp, onFilterChange, rowSelection = 'none', selectedRowIds: selectedRowIdsProp, onSelectionChange, bulkActions, expandable, rowActions, loading = false, loadingRowCount = 5, emptyState, stickyHeader = true, caption, showCaption = false, access: tableAccess, labels: labelsProp, variant, size, density, sx, slotProps } = props;
|
|
8625
|
+
const labels = _extends({}, DEFAULT_LABELS$1, labelsProp);
|
|
8626
|
+
const tableAccessState = useAccessState(tableAccess);
|
|
8627
|
+
// ───── Controllable state machinery ─────
|
|
8628
|
+
const [sortModel, setSortModel] = useControllableState({
|
|
8629
|
+
controlledValue: sortModelProp,
|
|
8630
|
+
defaultValue: defaultSortModel != null ? defaultSortModel : [],
|
|
8631
|
+
onChange: onSortChange
|
|
8632
|
+
});
|
|
8633
|
+
const [searchQuery, setSearchQuery] = useControllableState({
|
|
8634
|
+
controlledValue: searchQueryProp,
|
|
8635
|
+
defaultValue: '',
|
|
8636
|
+
onChange: onSearchQueryChange
|
|
8637
|
+
});
|
|
8638
|
+
const filterModel = filterModelProp != null ? filterModelProp : [];
|
|
8639
|
+
const [selectedRowIds, setSelectedRowIds] = useControllableState({
|
|
8640
|
+
controlledValue: selectedRowIdsProp,
|
|
8641
|
+
defaultValue: [],
|
|
8642
|
+
onChange: onSelectionChange
|
|
8643
|
+
});
|
|
8644
|
+
const [expandedRowIds, setExpandedRowIds] = useControllableState({
|
|
8645
|
+
controlledValue: expandable == null ? void 0 : expandable.expandedRowIds,
|
|
8646
|
+
defaultValue: [],
|
|
8647
|
+
onChange: expandable == null ? void 0 : expandable.onExpandChange
|
|
8648
|
+
});
|
|
8649
|
+
// ───── RBAC: filter out hidden columns ─────
|
|
8650
|
+
const visibleCols = useMemo(()=>cols.filter((col)=>!col.access || col.access.onUnauthorized !== 'hide'), [
|
|
8651
|
+
cols
|
|
8652
|
+
]);
|
|
8653
|
+
// ───── Smart-default column types ─────
|
|
8654
|
+
const columnTypes = useColumnAutoDetect(rows, visibleCols);
|
|
8655
|
+
// ───── Pipeline: search → filter → sort ─────
|
|
8656
|
+
const debouncedSearchQuery = useDebouncedValue(searchQuery, searchDebounceMs);
|
|
8657
|
+
const searchedRows = useTableSearch(rows, visibleCols, debouncedSearchQuery);
|
|
8658
|
+
const filteredRows = useTableFilter(searchedRows, filterModel);
|
|
8659
|
+
const sortedRows = useTableSort(filteredRows, visibleCols, sortModel);
|
|
8660
|
+
// ───── Selection helpers ─────
|
|
8661
|
+
const visibleRowIds = useMemo(()=>sortedRows.map((row, idx)=>getRowId(row, idx)), [
|
|
8662
|
+
sortedRows,
|
|
8663
|
+
getRowId
|
|
8664
|
+
]);
|
|
8665
|
+
const selection = useTableSelection(rowSelection, selectedRowIds, setSelectedRowIds);
|
|
8666
|
+
const selectedRows = useMemo(()=>sortedRows.filter((row, idx)=>selection.isSelected(getRowId(row, idx))), [
|
|
8667
|
+
sortedRows,
|
|
8668
|
+
selection,
|
|
8669
|
+
getRowId
|
|
8670
|
+
]);
|
|
8671
|
+
// ───── Variant recipe ─────
|
|
8672
|
+
const v = tableVariants({
|
|
8673
|
+
variant,
|
|
8674
|
+
size,
|
|
8675
|
+
density,
|
|
8676
|
+
stickyHeader
|
|
8677
|
+
});
|
|
8678
|
+
// ───── Render ─────
|
|
8679
|
+
if (!tableAccessState.visible) return null;
|
|
8680
|
+
const isTableInteractive = !tableAccessState.disabled;
|
|
8681
|
+
const showSelectionColumn = rowSelection !== 'none';
|
|
8682
|
+
const showExpandColumn = expandable != null;
|
|
8683
|
+
const showRowActionsColumn = rowActions != null;
|
|
8684
|
+
const totalColumnCount = visibleCols.length + (showSelectionColumn ? 1 : 0) + (showExpandColumn ? 1 : 0) + (showRowActionsColumn ? 1 : 0);
|
|
8685
|
+
return jsxs("div", {
|
|
8686
|
+
className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
|
|
8687
|
+
"data-disabled": !isTableInteractive ? 'true' : undefined,
|
|
8688
|
+
children: [
|
|
8689
|
+
enableSearch && jsx("div", {
|
|
8690
|
+
className: cn(v.toolbar(), slotProps == null ? void 0 : (_slotProps_toolbar = slotProps.toolbar) == null ? void 0 : _slotProps_toolbar.className),
|
|
8691
|
+
children: jsxs("label", {
|
|
8692
|
+
className: cn(v.search(), slotProps == null ? void 0 : (_slotProps_search = slotProps.search) == null ? void 0 : _slotProps_search.className),
|
|
8693
|
+
children: [
|
|
8694
|
+
jsx("span", {
|
|
8695
|
+
className: "sr-only",
|
|
8696
|
+
children: labels.searchPlaceholder
|
|
8697
|
+
}),
|
|
8698
|
+
jsx(SearchIcon$1, {
|
|
8699
|
+
className: "ml-2 shrink-0 text-neutral-500"
|
|
8700
|
+
}),
|
|
8701
|
+
jsx("input", {
|
|
8702
|
+
type: "text",
|
|
8703
|
+
value: searchQuery,
|
|
8704
|
+
onChange: (e)=>setSearchQuery(e.target.value),
|
|
8705
|
+
placeholder: searchPlaceholder != null ? searchPlaceholder : labels.searchPlaceholder,
|
|
8706
|
+
disabled: !isTableInteractive,
|
|
8707
|
+
className: cn('flex-1 bg-transparent border-0 outline-none', 'px-2 py-1.5', // placeholder color auto-inverts via the CSS-var preset;
|
|
8708
|
+
// no `dark:` variant needed on neutral palette.
|
|
8709
|
+
'placeholder:text-neutral-400', 'disabled:opacity-50 disabled:cursor-not-allowed')
|
|
8710
|
+
})
|
|
8711
|
+
]
|
|
8712
|
+
})
|
|
8713
|
+
}),
|
|
8714
|
+
jsx("div", {
|
|
8715
|
+
className: cn(v.scroll(), slotProps == null ? void 0 : (_slotProps_scroll = slotProps.scroll) == null ? void 0 : _slotProps_scroll.className),
|
|
8716
|
+
children: jsxs("table", {
|
|
8717
|
+
className: cn(v.table(), slotProps == null ? void 0 : (_slotProps_table = slotProps.table) == null ? void 0 : _slotProps_table.className),
|
|
8718
|
+
children: [
|
|
8719
|
+
caption != null && jsx("caption", {
|
|
8720
|
+
className: cn(!showCaption && 'sr-only', 'py-2 text-sm text-neutral-500'),
|
|
8721
|
+
children: caption
|
|
8722
|
+
}),
|
|
8723
|
+
jsx("thead", {
|
|
8724
|
+
className: cn(v.thead(), slotProps == null ? void 0 : (_slotProps_thead = slotProps.thead) == null ? void 0 : _slotProps_thead.className),
|
|
8725
|
+
children: jsxs("tr", {
|
|
8726
|
+
className: cn(v.headerRow(), slotProps == null ? void 0 : (_slotProps_headerRow = slotProps.headerRow) == null ? void 0 : _slotProps_headerRow.className),
|
|
8727
|
+
children: [
|
|
8728
|
+
showSelectionColumn && jsx("th", {
|
|
8729
|
+
scope: "col",
|
|
8730
|
+
className: cn(v.headerCell(), v.selectionCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className, slotProps == null ? void 0 : (_slotProps_selectionCell = slotProps.selectionCell) == null ? void 0 : _slotProps_selectionCell.className),
|
|
8731
|
+
children: rowSelection === 'multiple' && jsx("input", {
|
|
8732
|
+
type: "checkbox",
|
|
8733
|
+
"aria-label": labels.ariaSelectAllRows,
|
|
8734
|
+
checked: selection.isAllSelected(visibleRowIds),
|
|
8735
|
+
ref: (el)=>{
|
|
8736
|
+
if (el) el.indeterminate = selection.isIndeterminate(visibleRowIds);
|
|
8737
|
+
},
|
|
8738
|
+
onChange: ()=>selection.toggleAll(visibleRowIds),
|
|
8739
|
+
disabled: !isTableInteractive,
|
|
8740
|
+
className: "cursor-pointer"
|
|
8741
|
+
})
|
|
8742
|
+
}),
|
|
8743
|
+
showExpandColumn && jsx("th", {
|
|
8744
|
+
scope: "col",
|
|
8745
|
+
"aria-hidden": "true",
|
|
8746
|
+
className: cn(v.headerCell(), v.expandToggleCell(), slotProps == null ? void 0 : (_slotProps_headerCell1 = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell1.className, slotProps == null ? void 0 : (_slotProps_expandToggleCell = slotProps.expandToggleCell) == null ? void 0 : _slotProps_expandToggleCell.className)
|
|
8747
|
+
}),
|
|
8748
|
+
visibleCols.map((col)=>{
|
|
8749
|
+
var _slotProps_headerCell;
|
|
8750
|
+
return jsx(HeaderCell$1, {
|
|
8751
|
+
col: col,
|
|
8752
|
+
sortModel: sortModel,
|
|
8753
|
+
onSortClick: (e)=>handleSortClick$1(col, e, sortModel, setSortModel),
|
|
8754
|
+
inferredType: columnTypes.get(col.field),
|
|
8755
|
+
labels: labels,
|
|
8756
|
+
className: cn(v.headerCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className),
|
|
8757
|
+
buttonClassName: v.headerCellButton(),
|
|
8758
|
+
disabled: !isTableInteractive
|
|
8759
|
+
}, col.field);
|
|
8760
|
+
}),
|
|
8761
|
+
showRowActionsColumn && jsx("th", {
|
|
8762
|
+
scope: "col",
|
|
8763
|
+
"aria-hidden": "true",
|
|
8764
|
+
className: cn(v.headerCell(), 'w-12', slotProps == null ? void 0 : (_slotProps_headerCell2 = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell2.className)
|
|
8765
|
+
})
|
|
8766
|
+
]
|
|
8767
|
+
})
|
|
8768
|
+
}),
|
|
8769
|
+
jsx("tbody", {
|
|
8770
|
+
className: cn(v.tbody(), slotProps == null ? void 0 : (_slotProps_tbody = slotProps.tbody) == null ? void 0 : _slotProps_tbody.className),
|
|
8771
|
+
children: loading ? renderLoadingRows$1({
|
|
8772
|
+
count: loadingRowCount,
|
|
8773
|
+
visibleCols,
|
|
8774
|
+
showSelectionColumn,
|
|
8775
|
+
showExpandColumn,
|
|
8776
|
+
showRowActionsColumn,
|
|
8777
|
+
rowClass: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row = slotProps.row) == null ? void 0 : _slotProps_row.className),
|
|
8778
|
+
cellClass: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell = slotProps.cell) == null ? void 0 : _slotProps_cell.className)
|
|
8779
|
+
}) : sortedRows.length === 0 ? renderEmptyState$1({
|
|
8780
|
+
totalColumnCount,
|
|
8781
|
+
emptyState,
|
|
8782
|
+
fallback: labels.noData,
|
|
8783
|
+
cellClass: cn(v.cell(), v.emptyState(), slotProps == null ? void 0 : (_slotProps_emptyState = slotProps.emptyState) == null ? void 0 : _slotProps_emptyState.className)
|
|
8784
|
+
}) : sortedRows.map((row, idx)=>{
|
|
8785
|
+
var _slotProps_row, _slotProps_cell, _slotProps_cell1, _slotProps_selectionCell, _slotProps_cell2, _slotProps_expandToggleCell, _slotProps_cell3, _slotProps_rowActionsCell, _slotProps_expandedRow;
|
|
8786
|
+
const rowId = getRowId(row, idx);
|
|
8787
|
+
const isSelected = selection.isSelected(rowId);
|
|
8788
|
+
const isExpanded = expandedRowIds.includes(rowId);
|
|
8789
|
+
return jsx(Row, {
|
|
8790
|
+
row: row,
|
|
8791
|
+
rowId: rowId,
|
|
8792
|
+
rowIndex: idx,
|
|
8793
|
+
isSelected: isSelected,
|
|
8794
|
+
isExpanded: isExpanded,
|
|
8795
|
+
visibleCols: visibleCols,
|
|
8796
|
+
columnTypes: columnTypes,
|
|
8797
|
+
rowSelection: rowSelection,
|
|
8798
|
+
showSelectionColumn: showSelectionColumn,
|
|
8799
|
+
showExpandColumn: showExpandColumn,
|
|
8800
|
+
showRowActionsColumn: showRowActionsColumn,
|
|
8801
|
+
expandable: expandable,
|
|
8802
|
+
rowActions: rowActions,
|
|
8803
|
+
labels: labels,
|
|
8804
|
+
toggleRow: selection.toggleRow,
|
|
8805
|
+
toggleExpand: (id)=>setExpandedRowIds((prev)=>prev.includes(id) ? prev.filter((p)=>p !== id) : [
|
|
8806
|
+
...prev,
|
|
8807
|
+
id
|
|
8808
|
+
]),
|
|
8809
|
+
isInteractive: isTableInteractive,
|
|
8810
|
+
rowClass: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row = slotProps.row) == null ? void 0 : _slotProps_row.className),
|
|
8811
|
+
cellClass: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell = slotProps.cell) == null ? void 0 : _slotProps_cell.className),
|
|
8812
|
+
selectionCellClass: cn(v.cell(), v.selectionCell(), slotProps == null ? void 0 : (_slotProps_cell1 = slotProps.cell) == null ? void 0 : _slotProps_cell1.className, slotProps == null ? void 0 : (_slotProps_selectionCell = slotProps.selectionCell) == null ? void 0 : _slotProps_selectionCell.className),
|
|
8813
|
+
expandToggleCellClass: cn(v.cell(), v.expandToggleCell(), slotProps == null ? void 0 : (_slotProps_cell2 = slotProps.cell) == null ? void 0 : _slotProps_cell2.className, slotProps == null ? void 0 : (_slotProps_expandToggleCell = slotProps.expandToggleCell) == null ? void 0 : _slotProps_expandToggleCell.className),
|
|
8814
|
+
rowActionsCellClass: cn(v.cell(), v.rowActionsCell(), slotProps == null ? void 0 : (_slotProps_cell3 = slotProps.cell) == null ? void 0 : _slotProps_cell3.className, slotProps == null ? void 0 : (_slotProps_rowActionsCell = slotProps.rowActionsCell) == null ? void 0 : _slotProps_rowActionsCell.className),
|
|
8815
|
+
expandedRowClass: cn(v.expandedRow(), slotProps == null ? void 0 : (_slotProps_expandedRow = slotProps.expandedRow) == null ? void 0 : _slotProps_expandedRow.className),
|
|
8816
|
+
totalColumnCount: totalColumnCount
|
|
8817
|
+
}, rowId);
|
|
8818
|
+
})
|
|
8819
|
+
})
|
|
8820
|
+
]
|
|
8821
|
+
})
|
|
8822
|
+
}),
|
|
8823
|
+
bulkActions && selectedRows.length > 0 && jsxs("div", {
|
|
8824
|
+
className: cn(v.bulkActionFooter(), slotProps == null ? void 0 : (_slotProps_bulkActionFooter = slotProps.bulkActionFooter) == null ? void 0 : _slotProps_bulkActionFooter.className),
|
|
8825
|
+
role: "region",
|
|
8826
|
+
"aria-label": labels.selectedCount.replace('{count}', String(selectedRows.length)),
|
|
8827
|
+
children: [
|
|
8828
|
+
jsx("span", {
|
|
8829
|
+
className: "text-sm text-neutral-700",
|
|
8830
|
+
children: labels.selectedCount.replace('{count}', String(selectedRows.length))
|
|
8831
|
+
}),
|
|
8832
|
+
jsxs("div", {
|
|
8833
|
+
className: "flex items-center gap-2",
|
|
8834
|
+
children: [
|
|
8835
|
+
bulkActions(selectedRows),
|
|
8836
|
+
jsx("button", {
|
|
8837
|
+
type: "button",
|
|
8838
|
+
onClick: selection.clearSelection,
|
|
8839
|
+
className: cn(// text-neutral-500 → text-neutral-900 on hover; both auto-invert.
|
|
8840
|
+
'text-sm text-neutral-500 hover:text-neutral-900', 'underline-offset-2 hover:underline', 'focus:outline-none focus-visible:underline'),
|
|
8841
|
+
children: labels.clearSelection
|
|
8842
|
+
})
|
|
8843
|
+
]
|
|
8844
|
+
})
|
|
8845
|
+
]
|
|
8846
|
+
})
|
|
8847
|
+
]
|
|
8848
|
+
});
|
|
8849
|
+
}
|
|
8850
|
+
// ───── Header cell (with sort button) ─────
|
|
8851
|
+
function HeaderCell$1(props) {
|
|
8852
|
+
const { col, sortModel, onSortClick, inferredType, labels, className, buttonClassName, disabled } = props;
|
|
8853
|
+
const align = resolveAlign(col, inferredType);
|
|
8854
|
+
const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
|
|
8855
|
+
const sortEntry = sortModel.find((s)=>s.field === col.field);
|
|
8856
|
+
const ariaSort = (sortEntry == null ? void 0 : sortEntry.direction) === 'asc' ? 'ascending' : (sortEntry == null ? void 0 : sortEntry.direction) === 'desc' ? 'descending' : 'none';
|
|
8857
|
+
const headerContent = typeof col.header === 'function' ? col.header() : col.header;
|
|
8858
|
+
const cellStyle = {
|
|
8859
|
+
width: col.width != null ? `${col.width}px` : undefined,
|
|
8860
|
+
minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined,
|
|
8861
|
+
flex: col.flex
|
|
8862
|
+
};
|
|
8863
|
+
if (col.sortable) {
|
|
8864
|
+
return jsx("th", {
|
|
8865
|
+
scope: "col",
|
|
8866
|
+
"aria-sort": ariaSort,
|
|
8867
|
+
className: cn(className, alignClass),
|
|
8868
|
+
style: cellStyle,
|
|
8869
|
+
children: jsxs("button", {
|
|
8870
|
+
type: "button",
|
|
8871
|
+
onClick: onSortClick,
|
|
8872
|
+
disabled: disabled,
|
|
8873
|
+
"aria-label": ariaSort === 'ascending' ? labels.ariaSortAscending : ariaSort === 'descending' ? labels.ariaSortDescending : labels.ariaSortNone,
|
|
8874
|
+
className: cn(buttonClassName, align === 'right' && 'flex-row-reverse', 'disabled:cursor-not-allowed disabled:opacity-50'),
|
|
8875
|
+
children: [
|
|
8876
|
+
jsx("span", {
|
|
8877
|
+
children: headerContent
|
|
8878
|
+
}),
|
|
8879
|
+
jsx(SortIcon$1, {
|
|
8880
|
+
direction: sortEntry == null ? void 0 : sortEntry.direction
|
|
8881
|
+
})
|
|
8882
|
+
]
|
|
8883
|
+
})
|
|
8884
|
+
});
|
|
8885
|
+
}
|
|
8886
|
+
return jsx("th", {
|
|
8887
|
+
scope: "col",
|
|
8888
|
+
className: cn(className, alignClass),
|
|
8889
|
+
style: cellStyle,
|
|
8890
|
+
children: headerContent
|
|
8891
|
+
});
|
|
8892
|
+
}
|
|
8893
|
+
// ───── Data row ─────
|
|
8894
|
+
function Row(props) {
|
|
8895
|
+
const { row, rowId, rowIndex, isSelected, isExpanded, visibleCols, columnTypes, rowSelection, showSelectionColumn, showExpandColumn, showRowActionsColumn, expandable, rowActions, labels, toggleRow, toggleExpand, isInteractive, rowClass, cellClass, selectionCellClass, expandToggleCellClass, rowActionsCellClass, expandedRowClass, totalColumnCount } = props;
|
|
8896
|
+
return jsxs(Fragment$1, {
|
|
8897
|
+
children: [
|
|
8898
|
+
jsxs("tr", {
|
|
8899
|
+
"data-selected": isSelected ? 'true' : 'false',
|
|
8900
|
+
"aria-selected": rowSelection !== 'none' ? isSelected : undefined,
|
|
8901
|
+
className: rowClass,
|
|
8902
|
+
children: [
|
|
8903
|
+
showSelectionColumn && jsx("td", {
|
|
8904
|
+
className: selectionCellClass,
|
|
8905
|
+
children: jsx("input", {
|
|
8906
|
+
type: "checkbox",
|
|
8907
|
+
"aria-label": labels.ariaSelectRow,
|
|
8908
|
+
checked: isSelected,
|
|
8909
|
+
onChange: ()=>toggleRow(rowId),
|
|
8910
|
+
disabled: !isInteractive,
|
|
8911
|
+
className: "cursor-pointer"
|
|
8912
|
+
})
|
|
8913
|
+
}),
|
|
8914
|
+
showExpandColumn && jsx("td", {
|
|
8915
|
+
className: expandToggleCellClass,
|
|
8916
|
+
children: jsx("button", {
|
|
8917
|
+
type: "button",
|
|
8918
|
+
onClick: ()=>toggleExpand(rowId),
|
|
8919
|
+
"aria-label": isExpanded ? labels.ariaCollapseRow : labels.ariaExpandRow,
|
|
8920
|
+
"aria-expanded": isExpanded,
|
|
8921
|
+
disabled: !isInteractive,
|
|
8922
|
+
className: cn('inline-flex items-center justify-center h-6 w-6 rounded', // text-neutral-500 + hover bump; both auto-invert via CSS-var preset.
|
|
8923
|
+
'text-neutral-500 hover:text-neutral-900', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', 'transition-transform motion-reduce:transition-none', isExpanded && 'rotate-90'),
|
|
8924
|
+
children: jsx(ChevronRightIcon, {})
|
|
8925
|
+
})
|
|
8926
|
+
}),
|
|
8927
|
+
visibleCols.map((col)=>jsx(DataCell$1, {
|
|
8928
|
+
col: col,
|
|
8929
|
+
row: row,
|
|
8930
|
+
rowIndex: rowIndex,
|
|
8931
|
+
isSelected: isSelected,
|
|
8932
|
+
isExpanded: isExpanded,
|
|
8933
|
+
inferredType: columnTypes.get(col.field),
|
|
8934
|
+
cellClass: cellClass
|
|
8935
|
+
}, col.field)),
|
|
8936
|
+
showRowActionsColumn && rowActions && jsx("td", {
|
|
8937
|
+
className: rowActionsCellClass,
|
|
8938
|
+
children: rowActions(row)
|
|
8939
|
+
})
|
|
8940
|
+
]
|
|
8941
|
+
}),
|
|
8942
|
+
isExpanded && expandable && jsx("tr", {
|
|
8943
|
+
className: expandedRowClass,
|
|
8944
|
+
children: jsx("td", {
|
|
8945
|
+
colSpan: totalColumnCount,
|
|
8946
|
+
className: "p-3",
|
|
8947
|
+
children: expandable.render(row)
|
|
8948
|
+
})
|
|
8949
|
+
})
|
|
8950
|
+
]
|
|
8951
|
+
});
|
|
8952
|
+
}
|
|
8953
|
+
// ───── Data cell ─────
|
|
8954
|
+
function DataCell$1(props) {
|
|
8955
|
+
const { col, row, rowIndex, isSelected, isExpanded, inferredType, cellClass } = props;
|
|
8956
|
+
const value = getNestedValue(row, col.field);
|
|
8957
|
+
const align = resolveAlign(col, inferredType);
|
|
8958
|
+
// tabularNums = digit alignment via font-variant-numeric — does NOT
|
|
8959
|
+
// change the font family. Auto-true for number / date columns.
|
|
8960
|
+
const tabularNums = resolveTabularNums(col, inferredType);
|
|
8961
|
+
// monospace = font-family override (font-mono). Library never
|
|
8962
|
+
// auto-picks a font family — only explicit consumer opt-in. The
|
|
8963
|
+
// consumer's theme font-sans is preserved by default.
|
|
8964
|
+
const monospace = resolveMonospace(col);
|
|
8965
|
+
const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
|
|
8966
|
+
// Per-column RBAC state for the cellRenderer context
|
|
8967
|
+
const columnAccessState = col.access ? {
|
|
8968
|
+
hidden: col.access.onUnauthorized === 'hide',
|
|
8969
|
+
disabled: col.access.onUnauthorized === 'disable',
|
|
8970
|
+
readonly: col.access.onUnauthorized === 'readonly'
|
|
8971
|
+
} : null;
|
|
8972
|
+
const content = col.cellRenderer ? col.cellRenderer({
|
|
8973
|
+
row,
|
|
8974
|
+
value,
|
|
8975
|
+
rowIndex,
|
|
8976
|
+
isSelected,
|
|
8977
|
+
isExpanded,
|
|
8978
|
+
access: columnAccessState
|
|
8979
|
+
}) : renderDefaultCell$1(value);
|
|
8980
|
+
return jsx("td", {
|
|
8981
|
+
className: cn(cellClass, alignClass, tabularNums && 'tabular-nums', monospace && 'font-mono'),
|
|
8982
|
+
style: {
|
|
8983
|
+
width: col.width != null ? `${col.width}px` : undefined,
|
|
8984
|
+
minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined
|
|
8985
|
+
},
|
|
8986
|
+
children: content
|
|
8987
|
+
});
|
|
8988
|
+
}
|
|
8989
|
+
// ───── Helpers ─────
|
|
8990
|
+
function renderDefaultCell$1(value) {
|
|
8991
|
+
if (value == null) return null;
|
|
8992
|
+
if (typeof value === 'boolean') return value ? '✓' : '✗';
|
|
8993
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
8994
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
8995
|
+
return value;
|
|
8996
|
+
}
|
|
8997
|
+
function handleSortClick$1(col, event, current, setSortModel) {
|
|
8998
|
+
if (!col.sortable) return;
|
|
8999
|
+
const multi = event.shiftKey;
|
|
9000
|
+
setSortModel((prev)=>cycleSortFor(prev, col.field, multi));
|
|
9001
|
+
}
|
|
9002
|
+
function renderLoadingRows$1(args) {
|
|
9003
|
+
const { count, visibleCols, showSelectionColumn, showExpandColumn, showRowActionsColumn, rowClass, cellClass } = args;
|
|
9004
|
+
return Array.from({
|
|
9005
|
+
length: count
|
|
9006
|
+
}).map((_, i)=>jsxs("tr", {
|
|
9007
|
+
className: rowClass,
|
|
9008
|
+
"aria-busy": "true",
|
|
9009
|
+
children: [
|
|
9010
|
+
showSelectionColumn && jsx("td", {
|
|
9011
|
+
className: cellClass,
|
|
9012
|
+
children: jsx(Skeleton, {
|
|
9013
|
+
variant: "rectangle",
|
|
9014
|
+
width: "16px",
|
|
9015
|
+
height: "16px"
|
|
9016
|
+
})
|
|
9017
|
+
}),
|
|
9018
|
+
showExpandColumn && jsx("td", {
|
|
9019
|
+
className: cellClass,
|
|
9020
|
+
children: jsx(Skeleton, {
|
|
9021
|
+
variant: "rectangle",
|
|
9022
|
+
width: "16px",
|
|
9023
|
+
height: "16px"
|
|
9024
|
+
})
|
|
9025
|
+
}),
|
|
9026
|
+
visibleCols.map((col)=>jsx("td", {
|
|
9027
|
+
className: cellClass,
|
|
9028
|
+
children: jsx(Skeleton, {
|
|
9029
|
+
variant: "text",
|
|
9030
|
+
width: "80%"
|
|
9031
|
+
})
|
|
9032
|
+
}, col.field)),
|
|
9033
|
+
showRowActionsColumn && jsx("td", {
|
|
9034
|
+
className: cellClass,
|
|
9035
|
+
children: jsx(Skeleton, {
|
|
9036
|
+
variant: "rectangle",
|
|
9037
|
+
width: "24px",
|
|
9038
|
+
height: "24px"
|
|
9039
|
+
})
|
|
9040
|
+
})
|
|
9041
|
+
]
|
|
9042
|
+
}, `loading-${i}`));
|
|
9043
|
+
}
|
|
9044
|
+
function renderEmptyState$1(args) {
|
|
9045
|
+
var _args_emptyState;
|
|
9046
|
+
return jsx("tr", {
|
|
9047
|
+
children: jsx("td", {
|
|
9048
|
+
colSpan: args.totalColumnCount,
|
|
9049
|
+
className: args.cellClass,
|
|
9050
|
+
children: (_args_emptyState = args.emptyState) != null ? _args_emptyState : args.fallback
|
|
9051
|
+
})
|
|
9052
|
+
});
|
|
9053
|
+
}
|
|
9054
|
+
// ───── Icons ─────
|
|
9055
|
+
function SortIcon$1(props) {
|
|
9056
|
+
return jsx("svg", {
|
|
9057
|
+
width: "0.75em",
|
|
9058
|
+
height: "0.75em",
|
|
9059
|
+
viewBox: "0 0 16 16",
|
|
9060
|
+
fill: "currentColor",
|
|
9061
|
+
"aria-hidden": "true",
|
|
9062
|
+
className: "shrink-0 opacity-60 group-hover:opacity-100",
|
|
9063
|
+
children: props.direction === 'asc' ? jsx("path", {
|
|
9064
|
+
d: "M8 4l4 6H4z"
|
|
9065
|
+
}) : props.direction === 'desc' ? jsx("path", {
|
|
9066
|
+
d: "M8 12L4 6h8z"
|
|
9067
|
+
}) : jsxs(Fragment$1, {
|
|
9068
|
+
children: [
|
|
9069
|
+
jsx("path", {
|
|
9070
|
+
d: "M8 3l3 4H5z",
|
|
9071
|
+
opacity: "0.5"
|
|
9072
|
+
}),
|
|
9073
|
+
jsx("path", {
|
|
9074
|
+
d: "M8 13l-3-4h6z",
|
|
9075
|
+
opacity: "0.5"
|
|
9076
|
+
})
|
|
9077
|
+
]
|
|
9078
|
+
})
|
|
9079
|
+
});
|
|
9080
|
+
}
|
|
9081
|
+
function ChevronRightIcon() {
|
|
9082
|
+
return jsx("svg", {
|
|
9083
|
+
width: "0.875em",
|
|
9084
|
+
height: "0.875em",
|
|
9085
|
+
viewBox: "0 0 16 16",
|
|
9086
|
+
fill: "none",
|
|
9087
|
+
stroke: "currentColor",
|
|
9088
|
+
strokeWidth: "1.5",
|
|
9089
|
+
strokeLinecap: "round",
|
|
9090
|
+
strokeLinejoin: "round",
|
|
9091
|
+
"aria-hidden": "true",
|
|
9092
|
+
children: jsx("path", {
|
|
9093
|
+
d: "M6 4l4 4-4 4"
|
|
9094
|
+
})
|
|
9095
|
+
});
|
|
9096
|
+
}
|
|
9097
|
+
function SearchIcon$1(props) {
|
|
9098
|
+
return jsxs("svg", {
|
|
9099
|
+
width: "0.875em",
|
|
9100
|
+
height: "0.875em",
|
|
9101
|
+
viewBox: "0 0 16 16",
|
|
9102
|
+
fill: "none",
|
|
9103
|
+
stroke: "currentColor",
|
|
9104
|
+
strokeWidth: "1.5",
|
|
9105
|
+
strokeLinecap: "round",
|
|
9106
|
+
strokeLinejoin: "round",
|
|
9107
|
+
"aria-hidden": "true",
|
|
9108
|
+
className: props.className,
|
|
9109
|
+
children: [
|
|
9110
|
+
jsx("circle", {
|
|
9111
|
+
cx: "7",
|
|
9112
|
+
cy: "7",
|
|
9113
|
+
r: "4.5"
|
|
9114
|
+
}),
|
|
9115
|
+
jsx("path", {
|
|
9116
|
+
d: "M11 11l3 3"
|
|
9117
|
+
})
|
|
9118
|
+
]
|
|
9119
|
+
});
|
|
9120
|
+
}
|
|
9121
|
+
|
|
9122
|
+
/**
|
|
9123
|
+
* Plain text cell renderer. Wraps the value in a `<span>` and
|
|
9124
|
+
* optionally truncates on overflow. Use this for the most common
|
|
9125
|
+
* case: render a single string value.
|
|
9126
|
+
*
|
|
9127
|
+
* The Table component does NOT use this by default for plain
|
|
9128
|
+
* string columns — it just renders `{value}` directly. Use
|
|
9129
|
+
* `RenderText` when you need the truncate / muted styling.
|
|
9130
|
+
*/ function RenderText(props) {
|
|
9131
|
+
const { value, truncate, muted, className } = props;
|
|
9132
|
+
return jsx("span", {
|
|
9133
|
+
className: cn(truncate && 'block max-w-full truncate', // Neutral palette auto-inverts via dashforgePreset() CSS-var swap.
|
|
9134
|
+
muted && 'text-neutral-500', className),
|
|
9135
|
+
children: value
|
|
9136
|
+
});
|
|
9137
|
+
}
|
|
9138
|
+
|
|
9139
|
+
/**
|
|
9140
|
+
* Two-line cell renderer: a bold primary line stacked above a muted
|
|
9141
|
+
* secondary line. Useful for `Name + email`, `Title + ID`, etc.
|
|
9142
|
+
*
|
|
9143
|
+
* Replaces the MUI-side `RenderTwoLineCell` without the magic-number
|
|
9144
|
+
* vertical-align fudges (`mt: 0.8 / 0.2`) — flexbox handles the
|
|
9145
|
+
* vertical layout cleanly.
|
|
9146
|
+
*
|
|
9147
|
+
* Dashforge identity rule: text colors auto-invert via the
|
|
9148
|
+
* `dashforgePreset()` CSS-variable swap. No `dark:` variants needed
|
|
9149
|
+
* on neutral palette.
|
|
9150
|
+
*/ function RenderTwoLine(props) {
|
|
9151
|
+
const { primary, secondary, className } = props;
|
|
9152
|
+
return jsxs("div", {
|
|
9153
|
+
className: cn('flex flex-col leading-tight', className),
|
|
9154
|
+
children: [
|
|
9155
|
+
jsx("span", {
|
|
9156
|
+
className: "font-medium text-neutral-900",
|
|
9157
|
+
children: primary
|
|
9158
|
+
}),
|
|
9159
|
+
jsx("span", {
|
|
9160
|
+
className: "text-xs text-neutral-500 mt-0.5",
|
|
9161
|
+
children: secondary
|
|
9162
|
+
})
|
|
9163
|
+
]
|
|
9164
|
+
});
|
|
9165
|
+
}
|
|
9166
|
+
|
|
9167
|
+
/**
|
|
9168
|
+
* Internal mini-chip used by the cell renderer library. Not exported
|
|
9169
|
+
* from `@dashforge/tw` as a standalone component (per Sprint 4.1
|
|
9170
|
+
* scope decision — a standalone `<Chip>` may ship in Sprint 5 if
|
|
9171
|
+
* demand emerges).
|
|
9172
|
+
*
|
|
9173
|
+
* Variants:
|
|
9174
|
+
* - `color`: 7 intents (neutral / primary / success / warning /
|
|
9175
|
+
* danger / info / secondary)
|
|
9176
|
+
* - `variant`: `soft` (default) / `solid` / `outline`
|
|
9177
|
+
* - `size`: `sm` / `md`
|
|
9178
|
+
*/ const chipVariants = tv({
|
|
9179
|
+
base: [
|
|
9180
|
+
'inline-flex items-center gap-1 rounded-full font-medium',
|
|
9181
|
+
'whitespace-nowrap'
|
|
9182
|
+
],
|
|
9183
|
+
variants: {
|
|
9184
|
+
color: {
|
|
9185
|
+
neutral: '',
|
|
9186
|
+
primary: '',
|
|
9187
|
+
success: '',
|
|
9188
|
+
warning: '',
|
|
9189
|
+
danger: '',
|
|
9190
|
+
info: '',
|
|
9191
|
+
secondary: ''
|
|
9192
|
+
},
|
|
9193
|
+
variant: {
|
|
9194
|
+
soft: '',
|
|
9195
|
+
solid: '',
|
|
9196
|
+
outline: ''
|
|
9197
|
+
},
|
|
9198
|
+
size: {
|
|
9199
|
+
sm: 'h-5 px-2 text-xs',
|
|
9200
|
+
md: 'h-6 px-2.5 text-xs'
|
|
9201
|
+
}
|
|
9202
|
+
},
|
|
9203
|
+
compoundVariants: [
|
|
9204
|
+
// soft = subtle bg + dark text.
|
|
9205
|
+
// Neutral row uses NO `dark:` variants — the dashforgePreset()
|
|
9206
|
+
// CSS-var swap auto-inverts the neutral palette already.
|
|
9207
|
+
// Color rows below KEEP `dark:` variants because color palettes
|
|
9208
|
+
// don't auto-invert: the `dark:` shift is an intentional design
|
|
9209
|
+
// choice (darker bg + lighter text in dark mode for visual cohesion).
|
|
9210
|
+
{
|
|
9211
|
+
variant: 'soft',
|
|
9212
|
+
color: 'neutral',
|
|
9213
|
+
class: 'bg-neutral-100 text-neutral-700'
|
|
9214
|
+
},
|
|
9215
|
+
{
|
|
9216
|
+
variant: 'soft',
|
|
9217
|
+
color: 'primary',
|
|
9218
|
+
class: 'bg-primary-100 dark:bg-primary-950 text-primary-700 dark:text-primary-300'
|
|
9219
|
+
},
|
|
9220
|
+
{
|
|
9221
|
+
variant: 'soft',
|
|
9222
|
+
color: 'success',
|
|
9223
|
+
class: 'bg-success-100 dark:bg-success-950 text-success-700 dark:text-success-300'
|
|
9224
|
+
},
|
|
9225
|
+
{
|
|
9226
|
+
variant: 'soft',
|
|
9227
|
+
color: 'warning',
|
|
9228
|
+
class: 'bg-warning-100 dark:bg-warning-950 text-warning-700 dark:text-warning-300'
|
|
9229
|
+
},
|
|
9230
|
+
{
|
|
9231
|
+
variant: 'soft',
|
|
9232
|
+
color: 'danger',
|
|
9233
|
+
class: 'bg-danger-100 dark:bg-danger-950 text-danger-700 dark:text-danger-300'
|
|
9234
|
+
},
|
|
9235
|
+
{
|
|
9236
|
+
variant: 'soft',
|
|
9237
|
+
color: 'info',
|
|
9238
|
+
class: 'bg-info-100 dark:bg-info-950 text-info-700 dark:text-info-300'
|
|
9239
|
+
},
|
|
9240
|
+
{
|
|
9241
|
+
variant: 'soft',
|
|
9242
|
+
color: 'secondary',
|
|
9243
|
+
class: 'bg-secondary-100 dark:bg-secondary-950 text-secondary-700 dark:text-secondary-300'
|
|
9244
|
+
},
|
|
9245
|
+
// solid = full bg + light text
|
|
9246
|
+
// solid neutral: bg + text both anchored to neutral palette
|
|
9247
|
+
// (auto-inverts). `text-white` would not invert and would break
|
|
9248
|
+
// contrast in light mode against a `bg-neutral-700` dark bg, so
|
|
9249
|
+
// we stick to `text-neutral-50` (light text in light mode, dark
|
|
9250
|
+
// in dark — wait, that's wrong too). Actually `text-neutral-50`
|
|
9251
|
+
// auto-inverts: light text in light mode (against dark bg) is
|
|
9252
|
+
// what we want; in dark mode `bg-neutral-700` becomes light AND
|
|
9253
|
+
// `text-neutral-50` becomes dark — contrast preserved.
|
|
9254
|
+
{
|
|
9255
|
+
variant: 'solid',
|
|
9256
|
+
color: 'neutral',
|
|
9257
|
+
class: 'bg-neutral-700 text-neutral-50'
|
|
9258
|
+
},
|
|
9259
|
+
{
|
|
9260
|
+
variant: 'solid',
|
|
9261
|
+
color: 'primary',
|
|
9262
|
+
class: 'bg-primary-500 text-white'
|
|
9263
|
+
},
|
|
9264
|
+
{
|
|
9265
|
+
variant: 'solid',
|
|
9266
|
+
color: 'success',
|
|
9267
|
+
class: 'bg-success-500 text-white'
|
|
9268
|
+
},
|
|
9269
|
+
{
|
|
9270
|
+
variant: 'solid',
|
|
9271
|
+
color: 'warning',
|
|
9272
|
+
class: 'bg-warning-500 text-white'
|
|
9273
|
+
},
|
|
9274
|
+
{
|
|
9275
|
+
variant: 'solid',
|
|
9276
|
+
color: 'danger',
|
|
9277
|
+
class: 'bg-danger-500 text-white'
|
|
9278
|
+
},
|
|
9279
|
+
{
|
|
9280
|
+
variant: 'solid',
|
|
9281
|
+
color: 'info',
|
|
9282
|
+
class: 'bg-info-500 text-white'
|
|
9283
|
+
},
|
|
9284
|
+
{
|
|
9285
|
+
variant: 'solid',
|
|
9286
|
+
color: 'secondary',
|
|
9287
|
+
class: 'bg-secondary-500 text-white'
|
|
9288
|
+
},
|
|
9289
|
+
// outline = border + colored text
|
|
9290
|
+
// outline neutral: both border + text auto-invert via CSS-var swap.
|
|
9291
|
+
{
|
|
9292
|
+
variant: 'outline',
|
|
9293
|
+
color: 'neutral',
|
|
9294
|
+
class: 'border border-neutral-300 text-neutral-700'
|
|
9295
|
+
},
|
|
9296
|
+
{
|
|
9297
|
+
variant: 'outline',
|
|
9298
|
+
color: 'primary',
|
|
9299
|
+
class: 'border border-primary-500 text-primary-700 dark:text-primary-300'
|
|
9300
|
+
},
|
|
9301
|
+
{
|
|
9302
|
+
variant: 'outline',
|
|
9303
|
+
color: 'success',
|
|
9304
|
+
class: 'border border-success-500 text-success-700 dark:text-success-300'
|
|
9305
|
+
},
|
|
9306
|
+
{
|
|
9307
|
+
variant: 'outline',
|
|
9308
|
+
color: 'warning',
|
|
9309
|
+
class: 'border border-warning-500 text-warning-700 dark:text-warning-300'
|
|
9310
|
+
},
|
|
9311
|
+
{
|
|
9312
|
+
variant: 'outline',
|
|
9313
|
+
color: 'danger',
|
|
9314
|
+
class: 'border border-danger-500 text-danger-700 dark:text-danger-300'
|
|
9315
|
+
},
|
|
9316
|
+
{
|
|
9317
|
+
variant: 'outline',
|
|
9318
|
+
color: 'info',
|
|
9319
|
+
class: 'border border-info-500 text-info-700 dark:text-info-300'
|
|
9320
|
+
},
|
|
9321
|
+
{
|
|
9322
|
+
variant: 'outline',
|
|
9323
|
+
color: 'secondary',
|
|
9324
|
+
class: 'border border-secondary-500 text-secondary-700 dark:text-secondary-300'
|
|
9325
|
+
}
|
|
9326
|
+
],
|
|
9327
|
+
defaultVariants: {
|
|
9328
|
+
color: 'neutral',
|
|
9329
|
+
variant: 'soft',
|
|
9330
|
+
size: 'sm'
|
|
9331
|
+
}
|
|
9332
|
+
});
|
|
9333
|
+
function RenderChip(props) {
|
|
9334
|
+
const { children, color, variant, size, className } = props;
|
|
9335
|
+
return jsx("span", {
|
|
9336
|
+
className: cn(chipVariants({
|
|
9337
|
+
color,
|
|
9338
|
+
variant,
|
|
9339
|
+
size
|
|
9340
|
+
}), className),
|
|
9341
|
+
children: children
|
|
9342
|
+
});
|
|
9343
|
+
}
|
|
9344
|
+
|
|
9345
|
+
/**
|
|
9346
|
+
* Inline button cell renderer. Thin wrapper over `<Button>` with
|
|
9347
|
+
* the size narrowed to `sm` by default (table density expects compact
|
|
9348
|
+
* controls).
|
|
9349
|
+
*/ function RenderButton(props) {
|
|
9350
|
+
const { label, onClick, variant = 'ghost', size = 'sm' } = props, rest = _object_without_properties_loose(props, [
|
|
9351
|
+
"label",
|
|
9352
|
+
"onClick",
|
|
9353
|
+
"variant",
|
|
9354
|
+
"size"
|
|
9355
|
+
]);
|
|
9356
|
+
return jsx(Button, _extends({
|
|
9357
|
+
variant: variant,
|
|
9358
|
+
size: size,
|
|
9359
|
+
onClick: onClick
|
|
9360
|
+
}, rest, {
|
|
9361
|
+
children: label
|
|
9362
|
+
}));
|
|
9363
|
+
}
|
|
9364
|
+
|
|
9365
|
+
/**
|
|
9366
|
+
* 3-dot row actions menu. Renders a trigger button that opens a
|
|
9367
|
+
* Popover (Sprint 3 component) with a list of action menu items.
|
|
9368
|
+
*
|
|
9369
|
+
* Replaces the MUI-side `RowActions.tsx` (IconButton + Menu) with
|
|
9370
|
+
* the TW equivalents. Per-action RBAC honored: hidden actions are
|
|
9371
|
+
* filtered out, disabled actions render disabled.
|
|
9372
|
+
*/ function RowActionsMenu(props) {
|
|
9373
|
+
const { row, actions, trigger, ariaLabel = 'Row actions' } = props;
|
|
9374
|
+
const [open, setOpen] = useState(false);
|
|
9375
|
+
// Filter actions by RBAC (hidden actions removed entirely)
|
|
9376
|
+
const visibleActions = actions.filter((action)=>{
|
|
9377
|
+
if (!action.access) return true;
|
|
9378
|
+
return action.access.onUnauthorized !== 'hide';
|
|
9379
|
+
});
|
|
9380
|
+
if (visibleActions.length === 0) return null;
|
|
9381
|
+
return jsx(Popover, {
|
|
9382
|
+
open: open,
|
|
9383
|
+
onOpenChange: setOpen,
|
|
9384
|
+
side: "bottom",
|
|
9385
|
+
align: "end",
|
|
9386
|
+
content: jsx("ul", {
|
|
9387
|
+
className: "flex flex-col gap-0.5 min-w-[140px]",
|
|
9388
|
+
role: "menu",
|
|
9389
|
+
children: visibleActions.map((action, idx)=>jsx(RowActionMenuItem, {
|
|
9390
|
+
action: action,
|
|
9391
|
+
row: row,
|
|
9392
|
+
onClose: ()=>setOpen(false)
|
|
9393
|
+
}, idx))
|
|
9394
|
+
}),
|
|
9395
|
+
children: trigger != null ? trigger : jsx("button", {
|
|
9396
|
+
type: "button",
|
|
9397
|
+
"aria-label": ariaLabel,
|
|
9398
|
+
"aria-haspopup": "menu",
|
|
9399
|
+
"aria-expanded": open,
|
|
9400
|
+
className: cn('inline-flex items-center justify-center h-8 w-8 rounded-md', // Neutral palette auto-inverts via the dashforgePreset()
|
|
9401
|
+
// CSS-var swap — no `dark:` variant needed.
|
|
9402
|
+
'text-neutral-500 hover:text-neutral-900', 'hover:bg-neutral-100', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', 'transition-colors motion-reduce:transition-none'),
|
|
9403
|
+
children: jsxs("svg", {
|
|
9404
|
+
width: "1em",
|
|
9405
|
+
height: "1em",
|
|
9406
|
+
viewBox: "0 0 16 16",
|
|
9407
|
+
fill: "currentColor",
|
|
9408
|
+
"aria-hidden": "true",
|
|
9409
|
+
children: [
|
|
9410
|
+
jsx("circle", {
|
|
9411
|
+
cx: "8",
|
|
9412
|
+
cy: "3",
|
|
9413
|
+
r: "1.4"
|
|
9414
|
+
}),
|
|
9415
|
+
jsx("circle", {
|
|
9416
|
+
cx: "8",
|
|
9417
|
+
cy: "8",
|
|
9418
|
+
r: "1.4"
|
|
9419
|
+
}),
|
|
9420
|
+
jsx("circle", {
|
|
9421
|
+
cx: "8",
|
|
9422
|
+
cy: "13",
|
|
9423
|
+
r: "1.4"
|
|
9424
|
+
})
|
|
9425
|
+
]
|
|
9426
|
+
})
|
|
9427
|
+
})
|
|
9428
|
+
});
|
|
9429
|
+
}
|
|
9430
|
+
function RowActionMenuItem(props) {
|
|
9431
|
+
const { action, row, onClose } = props;
|
|
9432
|
+
const accessState = useAccessState(action.access);
|
|
9433
|
+
const isDisabled = action.disabled || accessState.disabled || accessState.readonly;
|
|
9434
|
+
return jsx("li", {
|
|
9435
|
+
role: "none",
|
|
9436
|
+
children: jsxs("button", {
|
|
9437
|
+
type: "button",
|
|
9438
|
+
role: "menuitem",
|
|
9439
|
+
disabled: isDisabled,
|
|
9440
|
+
onClick: ()=>{
|
|
9441
|
+
onClose();
|
|
9442
|
+
action.onClick(row);
|
|
9443
|
+
},
|
|
9444
|
+
className: cn('flex items-center gap-2 w-full text-left', 'px-3 py-1.5 rounded-md text-sm', // Neutral palette auto-inverts; danger/warning are color
|
|
9445
|
+
// palettes (no auto-invert) so they keep their `dark:` shift
|
|
9446
|
+
// intentionally — design choice, not a bug.
|
|
9447
|
+
'hover:bg-neutral-100', 'focus:outline-none focus-visible:bg-neutral-100', 'transition-colors motion-reduce:transition-none', 'disabled:opacity-50 disabled:pointer-events-none', action.color === 'danger' && 'text-danger-700 dark:text-danger-300', action.color === 'warning' && 'text-warning-700 dark:text-warning-300'),
|
|
9448
|
+
children: [
|
|
9449
|
+
action.icon && jsx("span", {
|
|
9450
|
+
className: "inline-flex shrink-0",
|
|
9451
|
+
"aria-hidden": "true",
|
|
9452
|
+
children: action.icon
|
|
9453
|
+
}),
|
|
9454
|
+
jsx("span", {
|
|
9455
|
+
children: action.label
|
|
9456
|
+
})
|
|
9457
|
+
]
|
|
9458
|
+
})
|
|
9459
|
+
});
|
|
9460
|
+
}
|
|
9461
|
+
|
|
9462
|
+
/**
|
|
9463
|
+
* `<DataGrid>` variant recipe.
|
|
9464
|
+
*
|
|
9465
|
+
* Sibling of `tableVariants` — shares the same visual axes (variant /
|
|
9466
|
+
* size / density) and the same identity-rule patterns:
|
|
9467
|
+
* - No `dark:` variants on neutral palette (auto-invert via the
|
|
9468
|
+
* dashforgePreset() CSS var swap).
|
|
9469
|
+
* - Selected row uses LeftNav `itemActive` pattern
|
|
9470
|
+
* (`bg-primary-100 text-primary-900`) — guaranteed contrast.
|
|
9471
|
+
* - Hover uses one-tier elevation (`bg-neutral-100`).
|
|
9472
|
+
*
|
|
9473
|
+
* DataGrid-specific slot additions vs Table:
|
|
9474
|
+
* - `stickyLeftCell` — column that uses `cols[i].sticky === 'left'`.
|
|
9475
|
+
* Z-index ordering: `thead` (z-10) < `stickyLeftCell` (z-11) <
|
|
9476
|
+
* `header × stickyLeft top-left corner cell` (z-20).
|
|
9477
|
+
* - `paginationFooter` — wraps the optional `<Pagination>`.
|
|
9478
|
+
*/ const dataGridVariants = tv({
|
|
9479
|
+
slots: {
|
|
9480
|
+
root: 'w-full',
|
|
9481
|
+
toolbar: 'flex flex-wrap items-center gap-3 mb-3',
|
|
9482
|
+
search: [
|
|
9483
|
+
'inline-flex items-center gap-2',
|
|
9484
|
+
'w-full max-w-sm rounded-md',
|
|
9485
|
+
'border border-neutral-300',
|
|
9486
|
+
'bg-neutral-50',
|
|
9487
|
+
'focus-within:ring-2 focus-within:ring-primary-500',
|
|
9488
|
+
'transition-colors motion-reduce:transition-none'
|
|
9489
|
+
],
|
|
9490
|
+
// The scroll container needs an explicit bounded height for
|
|
9491
|
+
// virtualization to compute the window. The consumer passes
|
|
9492
|
+
// `height` prop (inline style) or sets it via `sx` / parent
|
|
9493
|
+
// layout — DataGrid does NOT default a height (would be wrong
|
|
9494
|
+
// for most layouts).
|
|
9495
|
+
scroll: 'relative w-full overflow-auto rounded-md',
|
|
9496
|
+
table: 'w-full caption-bottom text-left',
|
|
9497
|
+
thead: 'bg-neutral-50 sticky top-0 z-10',
|
|
9498
|
+
tbody: '',
|
|
9499
|
+
headerRow: '',
|
|
9500
|
+
headerCell: [
|
|
9501
|
+
'text-neutral-700',
|
|
9502
|
+
'font-semibold whitespace-nowrap select-none',
|
|
9503
|
+
'border-b border-neutral-200',
|
|
9504
|
+
'bg-neutral-50'
|
|
9505
|
+
],
|
|
9506
|
+
headerCellButton: [
|
|
9507
|
+
'group inline-flex items-center gap-1 select-none',
|
|
9508
|
+
'hover:text-neutral-900',
|
|
9509
|
+
'focus:outline-none focus-visible:underline',
|
|
9510
|
+
'transition-colors motion-reduce:transition-none'
|
|
9511
|
+
],
|
|
9512
|
+
row: [
|
|
9513
|
+
'group/row transition-colors motion-reduce:transition-none',
|
|
9514
|
+
'hover:bg-neutral-100',
|
|
9515
|
+
'data-[selected=true]:bg-primary-100',
|
|
9516
|
+
'data-[selected=true]:text-primary-900',
|
|
9517
|
+
'data-[selected=true]:font-medium'
|
|
9518
|
+
],
|
|
9519
|
+
cell: [
|
|
9520
|
+
'text-neutral-900',
|
|
9521
|
+
'border-b border-neutral-200',
|
|
9522
|
+
'align-middle'
|
|
9523
|
+
],
|
|
9524
|
+
selectionCell: 'w-10 text-center',
|
|
9525
|
+
rowActionsCell: [
|
|
9526
|
+
'w-12 text-right',
|
|
9527
|
+
'opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100',
|
|
9528
|
+
'transition-opacity motion-reduce:transition-none'
|
|
9529
|
+
],
|
|
9530
|
+
emptyState: 'py-12 text-center text-neutral-500',
|
|
9531
|
+
loadingOverlay: 'pointer-events-none',
|
|
9532
|
+
bulkActionFooter: [
|
|
9533
|
+
'sticky bottom-0 z-20',
|
|
9534
|
+
'flex flex-wrap items-center justify-between gap-3',
|
|
9535
|
+
'px-3 py-2',
|
|
9536
|
+
'border-t border-neutral-200',
|
|
9537
|
+
'bg-neutral-50 shadow-md'
|
|
9538
|
+
],
|
|
9539
|
+
// Sticky left column — uses `position: sticky; left: 0` + z-index
|
|
9540
|
+
// so the column stays pinned during horizontal scroll. The
|
|
9541
|
+
// `bg-neutral-50` ensures the column doesn't bleed when content
|
|
9542
|
+
// scrolls underneath.
|
|
9543
|
+
stickyLeftCell: 'sticky left-0 z-[1] bg-neutral-50',
|
|
9544
|
+
// Sticky left × header intersection (top-left corner): higher
|
|
9545
|
+
// z-index than both header (`z-10`) and sticky col (`z-[1]`).
|
|
9546
|
+
stickyLeftHeaderCell: 'sticky left-0 z-20 bg-neutral-50',
|
|
9547
|
+
paginationFooter: 'flex justify-end mt-3'
|
|
9548
|
+
},
|
|
9549
|
+
variants: {
|
|
9550
|
+
variant: {
|
|
9551
|
+
plain: {},
|
|
9552
|
+
lines: {},
|
|
9553
|
+
striped: {
|
|
9554
|
+
row: 'odd:bg-neutral-100/60'
|
|
9555
|
+
},
|
|
9556
|
+
bordered: {
|
|
9557
|
+
table: 'border border-neutral-200',
|
|
9558
|
+
cell: 'border-r last:border-r-0 border-neutral-200',
|
|
9559
|
+
headerCell: 'border-r last:border-r-0 border-neutral-200'
|
|
9560
|
+
}
|
|
9561
|
+
},
|
|
9562
|
+
size: {
|
|
9563
|
+
sm: {
|
|
9564
|
+
headerCell: 'text-xs',
|
|
9565
|
+
cell: 'text-xs',
|
|
9566
|
+
search: 'text-xs'
|
|
9567
|
+
},
|
|
9568
|
+
md: {
|
|
9569
|
+
headerCell: 'text-sm',
|
|
9570
|
+
cell: 'text-sm',
|
|
9571
|
+
search: 'text-sm'
|
|
9572
|
+
},
|
|
9573
|
+
lg: {
|
|
9574
|
+
headerCell: 'text-base',
|
|
9575
|
+
cell: 'text-base',
|
|
9576
|
+
search: 'text-base'
|
|
9577
|
+
}
|
|
9578
|
+
},
|
|
9579
|
+
density: {
|
|
9580
|
+
compact: {
|
|
9581
|
+
headerCell: 'px-2 py-1',
|
|
9582
|
+
cell: 'px-2 py-1'
|
|
9583
|
+
},
|
|
9584
|
+
comfortable: {
|
|
9585
|
+
headerCell: 'px-3 py-2',
|
|
9586
|
+
cell: 'px-3 py-2'
|
|
9587
|
+
},
|
|
9588
|
+
spacious: {
|
|
9589
|
+
headerCell: 'px-4 py-3',
|
|
9590
|
+
cell: 'px-4 py-3'
|
|
9591
|
+
}
|
|
9592
|
+
}
|
|
9593
|
+
},
|
|
9594
|
+
defaultVariants: {
|
|
9595
|
+
variant: 'lines',
|
|
9596
|
+
size: 'md',
|
|
9597
|
+
density: 'comfortable'
|
|
9598
|
+
}
|
|
9599
|
+
});
|
|
9600
|
+
|
|
9601
|
+
/**
|
|
9602
|
+
* Compute the visible window of rows + the spacer heights given the
|
|
9603
|
+
* current scroll position. Pure function — exported for testability.
|
|
9604
|
+
*
|
|
9605
|
+
* @example
|
|
9606
|
+
* ```ts
|
|
9607
|
+
* computeWindow({ totalCount: 1000, rowHeight: 48, viewportHeight: 600,
|
|
9608
|
+
* scrollTop: 0, overscan: 5 });
|
|
9609
|
+
* // → { startIndex: 0, endIndex: 17, paddingTop: 0, paddingBottom: 47136 }
|
|
9610
|
+
* ```
|
|
9611
|
+
*/ function computeWindow(args) {
|
|
9612
|
+
const { totalCount, rowHeight, viewportHeight, scrollTop, overscan = 5 } = args;
|
|
9613
|
+
if (totalCount <= 0 || rowHeight <= 0 || viewportHeight <= 0) {
|
|
9614
|
+
return {
|
|
9615
|
+
startIndex: 0,
|
|
9616
|
+
endIndex: -1,
|
|
9617
|
+
paddingTop: 0,
|
|
9618
|
+
paddingBottom: 0
|
|
9619
|
+
};
|
|
9620
|
+
}
|
|
9621
|
+
// Raw visible range from scrollTop alone.
|
|
9622
|
+
const rawStart = Math.floor(scrollTop / rowHeight);
|
|
9623
|
+
const rawEnd = Math.ceil((scrollTop + viewportHeight) / rowHeight);
|
|
9624
|
+
// Apply overscan + clamp to [0, totalCount - 1].
|
|
9625
|
+
const startIndex = Math.max(0, rawStart - overscan);
|
|
9626
|
+
const endIndex = Math.min(totalCount - 1, rawEnd + overscan);
|
|
9627
|
+
const paddingTop = startIndex * rowHeight;
|
|
9628
|
+
const paddingBottom = Math.max(0, (totalCount - 1 - endIndex) * rowHeight);
|
|
9629
|
+
return {
|
|
9630
|
+
startIndex,
|
|
9631
|
+
endIndex,
|
|
9632
|
+
paddingTop,
|
|
9633
|
+
paddingBottom
|
|
9634
|
+
};
|
|
9635
|
+
}
|
|
9636
|
+
/**
|
|
9637
|
+
* `useVirtualizer` — viewport-based row windowing for `<DataGrid>`.
|
|
9638
|
+
*
|
|
9639
|
+
* Hand-rolled (no `@tanstack/react-virtual` dep — constraint memorized
|
|
9640
|
+
* in `feedback_dashforge_preset_is_identity` companion: "no new
|
|
9641
|
+
* external runtime deps"). Uses scroll event + `requestAnimationFrame`
|
|
9642
|
+
* debounce + `ResizeObserver` on the container. Fixed row height
|
|
9643
|
+
* only in v1 — variable-height with measurement cache deferred to
|
|
9644
|
+
* v1-bis.
|
|
9645
|
+
*
|
|
9646
|
+
* Returns:
|
|
9647
|
+
* - `scrollRef`: attach to the scroll container `<div>`.
|
|
9648
|
+
* - `window`: which row indices to mount + top/bottom spacer heights
|
|
9649
|
+
* to preserve scroll-area size.
|
|
9650
|
+
* - `viewportHeight`: current container height (re-measured on
|
|
9651
|
+
* `ResizeObserver` ticks).
|
|
9652
|
+
*
|
|
9653
|
+
* The DataGrid renders `rows.slice(window.startIndex, window.endIndex + 1)`
|
|
9654
|
+
* between two spacer `<tr style={{ height: paddingTop }} />` elements
|
|
9655
|
+
* (above and below). This preserves `<table>` semantics — screen
|
|
9656
|
+
* readers see a continuous structure with `aria-hidden` spacers.
|
|
9657
|
+
*
|
|
9658
|
+
* @example
|
|
9659
|
+
* ```tsx
|
|
9660
|
+
* const { scrollRef, window } = useVirtualizer({
|
|
9661
|
+
* totalCount: rows.length,
|
|
9662
|
+
* rowHeight: 48,
|
|
9663
|
+
* overscan: 5,
|
|
9664
|
+
* });
|
|
9665
|
+
*
|
|
9666
|
+
* return (
|
|
9667
|
+
* <div ref={scrollRef} style={{ height: 600, overflow: 'auto' }}>
|
|
9668
|
+
* <table>
|
|
9669
|
+
* <tbody>
|
|
9670
|
+
* <tr aria-hidden="true" style={{ height: window.paddingTop }} />
|
|
9671
|
+
* {rows.slice(window.startIndex, window.endIndex + 1).map(...)}
|
|
9672
|
+
* <tr aria-hidden="true" style={{ height: window.paddingBottom }} />
|
|
9673
|
+
* </tbody>
|
|
9674
|
+
* </table>
|
|
9675
|
+
* </div>
|
|
9676
|
+
* );
|
|
9677
|
+
* ```
|
|
9678
|
+
*/ function useVirtualizer(args) {
|
|
9679
|
+
const { totalCount, rowHeight, overscan = 5, initialViewportHeight = 400 } = args;
|
|
9680
|
+
const scrollRef = useRef(null);
|
|
9681
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
9682
|
+
const [viewportHeight, setViewportHeight] = useState(initialViewportHeight);
|
|
9683
|
+
// Debounce scroll handler via requestAnimationFrame so we never
|
|
9684
|
+
// re-render more than once per paint frame even when the scroll
|
|
9685
|
+
// event fires rapidly during fast scrolling.
|
|
9686
|
+
const rafIdRef = useRef(null);
|
|
9687
|
+
const handleScroll = useCallback(()=>{
|
|
9688
|
+
if (rafIdRef.current != null) return;
|
|
9689
|
+
rafIdRef.current = requestAnimationFrame(()=>{
|
|
9690
|
+
rafIdRef.current = null;
|
|
9691
|
+
const el = scrollRef.current;
|
|
9692
|
+
if (!el) return;
|
|
9693
|
+
setScrollTop(el.scrollTop);
|
|
9694
|
+
});
|
|
9695
|
+
}, []);
|
|
9696
|
+
// Attach scroll listener.
|
|
9697
|
+
useEffect(()=>{
|
|
9698
|
+
const el = scrollRef.current;
|
|
9699
|
+
if (!el) return;
|
|
9700
|
+
el.addEventListener('scroll', handleScroll, {
|
|
9701
|
+
passive: true
|
|
9702
|
+
});
|
|
9703
|
+
return ()=>{
|
|
9704
|
+
el.removeEventListener('scroll', handleScroll);
|
|
9705
|
+
if (rafIdRef.current != null) {
|
|
9706
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
9707
|
+
rafIdRef.current = null;
|
|
9708
|
+
}
|
|
9709
|
+
};
|
|
9710
|
+
}, [
|
|
9711
|
+
handleScroll
|
|
9712
|
+
]);
|
|
9713
|
+
// Track container viewport height via ResizeObserver — re-virtualize
|
|
9714
|
+
// when the container size changes (window resize, sidebar collapse,
|
|
9715
|
+
// etc.). Use `useLayoutEffect` so the first measurement happens
|
|
9716
|
+
// before paint to avoid a flash of stale window.
|
|
9717
|
+
//
|
|
9718
|
+
// **Defensive**: only accept measurements > 0. jsdom (test env)
|
|
9719
|
+
// returns `clientHeight === 0` for unmounted containers, which would
|
|
9720
|
+
// collapse the window to empty. The hook keeps the
|
|
9721
|
+
// `initialViewportHeight` fallback in that case.
|
|
9722
|
+
useLayoutEffect(()=>{
|
|
9723
|
+
const el = scrollRef.current;
|
|
9724
|
+
if (!el) return;
|
|
9725
|
+
if (el.clientHeight > 0) {
|
|
9726
|
+
setViewportHeight(el.clientHeight);
|
|
9727
|
+
}
|
|
9728
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
9729
|
+
const observer = new ResizeObserver((entries)=>{
|
|
9730
|
+
const entry = entries[0];
|
|
9731
|
+
if (!entry || entry.contentRect.height <= 0) return;
|
|
9732
|
+
setViewportHeight(entry.contentRect.height);
|
|
9733
|
+
});
|
|
9734
|
+
observer.observe(el);
|
|
9735
|
+
return ()=>observer.disconnect();
|
|
9736
|
+
}, []);
|
|
9737
|
+
const window = computeWindow({
|
|
9738
|
+
totalCount,
|
|
9739
|
+
rowHeight,
|
|
9740
|
+
viewportHeight,
|
|
9741
|
+
scrollTop,
|
|
9742
|
+
overscan
|
|
9743
|
+
});
|
|
9744
|
+
return {
|
|
9745
|
+
scrollRef,
|
|
9746
|
+
window,
|
|
9747
|
+
viewportHeight
|
|
9748
|
+
};
|
|
9749
|
+
}
|
|
9750
|
+
|
|
9751
|
+
const DEFAULT_LABELS = {
|
|
9752
|
+
searchPlaceholder: 'Search...',
|
|
9753
|
+
noData: 'No data',
|
|
9754
|
+
loading: 'Loading…',
|
|
9755
|
+
ariaSortNone: 'Not sorted. Click to sort ascending.',
|
|
9756
|
+
ariaSortAscending: 'Sorted ascending. Click to sort descending.',
|
|
9757
|
+
ariaSortDescending: 'Sorted descending. Click to clear sort.',
|
|
9758
|
+
ariaSelectRow: 'Select row',
|
|
9759
|
+
ariaSelectAllRows: 'Select all rows',
|
|
9760
|
+
ariaExpandRow: 'Expand row',
|
|
9761
|
+
ariaCollapseRow: 'Collapse row',
|
|
9762
|
+
selectedCount: '{count} selected',
|
|
9763
|
+
clearSelection: 'Clear selection',
|
|
9764
|
+
filterColumn: 'Filter',
|
|
9765
|
+
filterApply: 'Apply',
|
|
9766
|
+
filterClear: 'Clear',
|
|
9767
|
+
density: 'Density',
|
|
9768
|
+
densityCompact: 'Compact',
|
|
9769
|
+
densityComfortable: 'Comfortable',
|
|
9770
|
+
densitySpacious: 'Spacious'
|
|
9771
|
+
};
|
|
9772
|
+
/**
|
|
9773
|
+
* Dashforge TW `<DataGrid>` — virtualized data table.
|
|
9774
|
+
*
|
|
9775
|
+
* Companion to `<Table>` for large data sets (~500+ rows up to
|
|
9776
|
+
* millions via virtualization). Same column model, sort/search/
|
|
9777
|
+
* filter/selection logic (shared via `_shared/data/` helpers) —
|
|
9778
|
+
* differs in render path: only the visible window of rows is mounted
|
|
9779
|
+
* in DOM, with spacer `<tr>` above and below to preserve scrollbar
|
|
9780
|
+
* size.
|
|
9781
|
+
*
|
|
9782
|
+
* Virtualization: hand-rolled (`useVirtualizer`) using
|
|
9783
|
+
* `IntersectionObserver` + scroll-event + `requestAnimationFrame`
|
|
9784
|
+
* debounce + `ResizeObserver`. Zero new runtime deps.
|
|
9785
|
+
*
|
|
9786
|
+
* Server-side mode (4 independent opt-in flags):
|
|
9787
|
+
* - `serverSideSort` — emits `onSortChange` but does NOT sort locally
|
|
9788
|
+
* - `serverSideFilter` — emits `onFilterChange` but does NOT filter
|
|
9789
|
+
* - `serverSideSearch` — emits `onSearchQueryChange` (debounced)
|
|
9790
|
+
* - `serverSidePagination` — `totalCount` prop sizes the virtual
|
|
9791
|
+
* scrollbar; `rows` is the page slice from the server
|
|
9792
|
+
*
|
|
9793
|
+
* Selection at scale: `selectAllScope`:
|
|
9794
|
+
* - `'allLoaded'` (default) — header checkbox toggles all rows in
|
|
9795
|
+
* `rows` prop
|
|
9796
|
+
* - `'visible'` — toggles only the current viewport window
|
|
9797
|
+
*/ function DataGrid(props) {
|
|
9798
|
+
var _slotProps_root, _slotProps_toolbar, _slotProps_search, _slotProps_scroll, _slotProps_table, _slotProps_thead, _slotProps_headerRow, _slotProps_headerCell, _slotProps_selectionCell, _slotProps_headerCell1, _slotProps_tbody, _slotProps_row, _slotProps_cell, _slotProps_emptyState, _slotProps_row1, _slotProps_cell1, _slotProps_cell2, _slotProps_selectionCell1, _slotProps_cell3, _slotProps_rowActionsCell, _slotProps_bulkActionFooter, _slotProps_pagination;
|
|
9799
|
+
const { rows, cols, getRowId, totalCount: totalCountProp, rowHeight, overscan = 5, height, sortModel: sortModelProp, onSortChange, defaultSortModel, serverSideSort = false, enableSearch = false, searchQuery: searchQueryProp, onSearchQueryChange, searchPlaceholder, searchDebounceMs = 200, serverSideSearch = false, filterModel: filterModelProp, onFilterChange, serverSideFilter = false, rowSelection = 'none', selectedRowIds: selectedRowIdsProp, onSelectionChange, bulkActions, selectAllScope = 'allLoaded', pagination, serverSidePagination = false, rowActions, loading = false, loadingRowCount = 8, emptyState, stickyHeader = true, caption, showCaption = false, access: gridAccess, labels: labelsProp, variant, size, density, sx, slotProps } = props;
|
|
9800
|
+
const labels = _extends({}, DEFAULT_LABELS, labelsProp);
|
|
9801
|
+
const gridAccessState = useAccessState(gridAccess);
|
|
9802
|
+
// ───── Controllable state ─────
|
|
9803
|
+
const [sortModel, setSortModel] = useControllableState({
|
|
9804
|
+
controlledValue: sortModelProp,
|
|
9805
|
+
defaultValue: defaultSortModel != null ? defaultSortModel : [],
|
|
9806
|
+
onChange: onSortChange
|
|
9807
|
+
});
|
|
9808
|
+
const [searchQuery, setSearchQuery] = useControllableState({
|
|
9809
|
+
controlledValue: searchQueryProp,
|
|
9810
|
+
defaultValue: '',
|
|
9811
|
+
onChange: onSearchQueryChange
|
|
9812
|
+
});
|
|
9813
|
+
const filterModel = filterModelProp != null ? filterModelProp : [];
|
|
9814
|
+
const [selectedRowIds, setSelectedRowIds] = useControllableState({
|
|
9815
|
+
controlledValue: selectedRowIdsProp,
|
|
9816
|
+
defaultValue: [],
|
|
9817
|
+
onChange: onSelectionChange
|
|
9818
|
+
});
|
|
9819
|
+
// ───── RBAC: filter columns ─────
|
|
9820
|
+
const visibleCols = useMemo(()=>cols.filter((col)=>!col.access || col.access.onUnauthorized !== 'hide'), [
|
|
9821
|
+
cols
|
|
9822
|
+
]);
|
|
9823
|
+
// ───── Smart-default column types ─────
|
|
9824
|
+
const columnTypes = useColumnAutoDetect(rows, visibleCols);
|
|
9825
|
+
// ───── Pipeline: search → filter → sort (skip when server-side) ─────
|
|
9826
|
+
//
|
|
9827
|
+
// Each pipeline hook is ALWAYS called (React hooks rules). The
|
|
9828
|
+
// server-side flags decide whether to use the local-computed result
|
|
9829
|
+
// or pass the upstream rows through unchanged. The hooks are
|
|
9830
|
+
// `useMemo`-based internally so they're cheap when their inputs
|
|
9831
|
+
// are unchanged.
|
|
9832
|
+
const debouncedSearchQuery = useDebouncedValue(searchQuery, searchDebounceMs);
|
|
9833
|
+
const localSearched = useTableSearch(rows, visibleCols, debouncedSearchQuery);
|
|
9834
|
+
const searchedRows = serverSideSearch ? rows : localSearched;
|
|
9835
|
+
const localFiltered = useTableFilter(searchedRows, filterModel);
|
|
9836
|
+
const filteredRows = serverSideFilter ? searchedRows : localFiltered;
|
|
9837
|
+
const localSorted = useTableSort(filteredRows, visibleCols, sortModel);
|
|
9838
|
+
const sortedRows = serverSideSort ? filteredRows : localSorted;
|
|
9839
|
+
// ───── Virtualization ─────
|
|
9840
|
+
// Effective row count for the virtual scrollbar:
|
|
9841
|
+
// - Client-side: actual sorted rows count
|
|
9842
|
+
// - Server-side pagination: caller-provided totalCount
|
|
9843
|
+
const effectiveTotalCount = serverSidePagination ? totalCountProp != null ? totalCountProp : sortedRows.length : sortedRows.length;
|
|
9844
|
+
const initialViewportHeight = height ? parseInt(height, 10) || 400 : 400;
|
|
9845
|
+
const { scrollRef, window: vWindow } = useVirtualizer({
|
|
9846
|
+
totalCount: effectiveTotalCount,
|
|
9847
|
+
rowHeight,
|
|
9848
|
+
overscan,
|
|
9849
|
+
initialViewportHeight
|
|
9850
|
+
});
|
|
9851
|
+
// The rows actually mounted in DOM.
|
|
9852
|
+
const renderedRows = useMemo(()=>{
|
|
9853
|
+
if (serverSidePagination) {
|
|
9854
|
+
// In server-side pagination, `rows` IS the page slice; we
|
|
9855
|
+
// render all of it (virtualization within the page is still
|
|
9856
|
+
// useful for very large page sizes).
|
|
9857
|
+
return rows.slice(vWindow.startIndex, vWindow.endIndex + 1);
|
|
9858
|
+
}
|
|
9859
|
+
return sortedRows.slice(vWindow.startIndex, vWindow.endIndex + 1);
|
|
9860
|
+
}, [
|
|
9861
|
+
serverSidePagination,
|
|
9862
|
+
rows,
|
|
9863
|
+
sortedRows,
|
|
9864
|
+
vWindow.startIndex,
|
|
9865
|
+
vWindow.endIndex
|
|
9866
|
+
]);
|
|
9867
|
+
// ───── Selection helpers ─────
|
|
9868
|
+
// For `selectAllScope='visible'`, the select-all scope is the
|
|
9869
|
+
// current window. For `'allLoaded'`, it's the whole sorted set.
|
|
9870
|
+
const selectAllPool = useMemo(()=>{
|
|
9871
|
+
if (selectAllScope === 'visible') {
|
|
9872
|
+
return renderedRows.map((row, idx)=>getRowId(row, vWindow.startIndex + idx));
|
|
9873
|
+
}
|
|
9874
|
+
return sortedRows.map((row, idx)=>getRowId(row, idx));
|
|
9875
|
+
}, [
|
|
9876
|
+
selectAllScope,
|
|
9877
|
+
renderedRows,
|
|
9878
|
+
sortedRows,
|
|
9879
|
+
getRowId,
|
|
9880
|
+
vWindow.startIndex
|
|
9881
|
+
]);
|
|
9882
|
+
const selection = useTableSelection(rowSelection, selectedRowIds, setSelectedRowIds);
|
|
9883
|
+
const selectedRows = useMemo(()=>sortedRows.filter((row, idx)=>selection.isSelected(getRowId(row, idx))), [
|
|
9884
|
+
sortedRows,
|
|
9885
|
+
selection,
|
|
9886
|
+
getRowId
|
|
9887
|
+
]);
|
|
9888
|
+
// ───── Variant recipe ─────
|
|
9889
|
+
const v = dataGridVariants({
|
|
9890
|
+
variant,
|
|
9891
|
+
size,
|
|
9892
|
+
density
|
|
9893
|
+
});
|
|
9894
|
+
// ───── Render ─────
|
|
9895
|
+
if (!gridAccessState.visible) return null;
|
|
9896
|
+
const isInteractive = !gridAccessState.disabled;
|
|
9897
|
+
const showSelectionColumn = rowSelection !== 'none';
|
|
9898
|
+
const showRowActionsColumn = rowActions != null;
|
|
9899
|
+
const totalColumnCount = visibleCols.length + (showSelectionColumn ? 1 : 0) + (showRowActionsColumn ? 1 : 0);
|
|
9900
|
+
return jsxs("div", {
|
|
9901
|
+
className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
|
|
9902
|
+
"data-disabled": !isInteractive ? 'true' : undefined,
|
|
9903
|
+
children: [
|
|
9904
|
+
enableSearch && jsx("div", {
|
|
9905
|
+
className: cn(v.toolbar(), slotProps == null ? void 0 : (_slotProps_toolbar = slotProps.toolbar) == null ? void 0 : _slotProps_toolbar.className),
|
|
9906
|
+
children: jsxs("label", {
|
|
9907
|
+
className: cn(v.search(), slotProps == null ? void 0 : (_slotProps_search = slotProps.search) == null ? void 0 : _slotProps_search.className),
|
|
9908
|
+
children: [
|
|
9909
|
+
jsx("span", {
|
|
9910
|
+
className: "sr-only",
|
|
9911
|
+
children: labels.searchPlaceholder
|
|
9912
|
+
}),
|
|
9913
|
+
jsx(SearchIcon, {
|
|
9914
|
+
className: "ml-2 shrink-0 text-neutral-500"
|
|
9915
|
+
}),
|
|
9916
|
+
jsx("input", {
|
|
9917
|
+
type: "text",
|
|
9918
|
+
value: searchQuery,
|
|
9919
|
+
onChange: (e)=>setSearchQuery(e.target.value),
|
|
9920
|
+
placeholder: searchPlaceholder != null ? searchPlaceholder : labels.searchPlaceholder,
|
|
9921
|
+
disabled: !isInteractive,
|
|
9922
|
+
className: cn('flex-1 bg-transparent border-0 outline-none', 'px-2 py-1.5', 'placeholder:text-neutral-400', 'disabled:opacity-50 disabled:cursor-not-allowed')
|
|
9923
|
+
})
|
|
9924
|
+
]
|
|
9925
|
+
})
|
|
9926
|
+
}),
|
|
9927
|
+
jsx("div", {
|
|
9928
|
+
ref: scrollRef,
|
|
9929
|
+
className: cn(v.scroll(), slotProps == null ? void 0 : (_slotProps_scroll = slotProps.scroll) == null ? void 0 : _slotProps_scroll.className),
|
|
9930
|
+
style: height ? {
|
|
9931
|
+
height,
|
|
9932
|
+
maxHeight: height
|
|
9933
|
+
} : undefined,
|
|
9934
|
+
children: jsxs("table", {
|
|
9935
|
+
className: cn(v.table(), slotProps == null ? void 0 : (_slotProps_table = slotProps.table) == null ? void 0 : _slotProps_table.className),
|
|
9936
|
+
children: [
|
|
9937
|
+
caption != null && jsx("caption", {
|
|
9938
|
+
className: cn(!showCaption && 'sr-only', 'py-2 text-sm text-neutral-500'),
|
|
9939
|
+
children: caption
|
|
9940
|
+
}),
|
|
9941
|
+
jsx("thead", {
|
|
9942
|
+
className: cn(stickyHeader ? v.thead() : 'bg-neutral-50', slotProps == null ? void 0 : (_slotProps_thead = slotProps.thead) == null ? void 0 : _slotProps_thead.className),
|
|
9943
|
+
children: jsxs("tr", {
|
|
9944
|
+
className: cn(v.headerRow(), slotProps == null ? void 0 : (_slotProps_headerRow = slotProps.headerRow) == null ? void 0 : _slotProps_headerRow.className),
|
|
9945
|
+
children: [
|
|
9946
|
+
showSelectionColumn && jsx("th", {
|
|
9947
|
+
scope: "col",
|
|
9948
|
+
className: cn(v.headerCell(), v.selectionCell(), v.stickyLeftHeaderCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className, slotProps == null ? void 0 : (_slotProps_selectionCell = slotProps.selectionCell) == null ? void 0 : _slotProps_selectionCell.className),
|
|
9949
|
+
children: rowSelection === 'multiple' && jsx("input", {
|
|
9950
|
+
type: "checkbox",
|
|
9951
|
+
"aria-label": labels.ariaSelectAllRows,
|
|
9952
|
+
checked: selection.isAllSelected(selectAllPool),
|
|
9953
|
+
ref: (el)=>{
|
|
9954
|
+
if (el) el.indeterminate = selection.isIndeterminate(selectAllPool);
|
|
9955
|
+
},
|
|
9956
|
+
onChange: ()=>selection.toggleAll(selectAllPool),
|
|
9957
|
+
disabled: !isInteractive,
|
|
9958
|
+
className: "cursor-pointer"
|
|
9959
|
+
})
|
|
9960
|
+
}),
|
|
9961
|
+
visibleCols.map((col)=>{
|
|
9962
|
+
var _slotProps_headerCell;
|
|
9963
|
+
return jsx(HeaderCell, {
|
|
9964
|
+
col: col,
|
|
9965
|
+
sortModel: sortModel,
|
|
9966
|
+
onSortClick: (e)=>handleSortClick(col, e, sortModel, setSortModel),
|
|
9967
|
+
inferredType: columnTypes.get(col.field),
|
|
9968
|
+
labels: labels,
|
|
9969
|
+
className: cn(v.headerCell(), col.sticky === 'left' && v.stickyLeftHeaderCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className),
|
|
9970
|
+
buttonClassName: v.headerCellButton(),
|
|
9971
|
+
disabled: !isInteractive
|
|
9972
|
+
}, col.field);
|
|
9973
|
+
}),
|
|
9974
|
+
showRowActionsColumn && jsx("th", {
|
|
9975
|
+
scope: "col",
|
|
9976
|
+
"aria-hidden": "true",
|
|
9977
|
+
className: cn(v.headerCell(), 'w-12', slotProps == null ? void 0 : (_slotProps_headerCell1 = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell1.className)
|
|
9978
|
+
})
|
|
9979
|
+
]
|
|
9980
|
+
})
|
|
9981
|
+
}),
|
|
9982
|
+
jsx("tbody", {
|
|
9983
|
+
className: cn(v.tbody(), slotProps == null ? void 0 : (_slotProps_tbody = slotProps.tbody) == null ? void 0 : _slotProps_tbody.className),
|
|
9984
|
+
children: loading ? renderLoadingRows({
|
|
9985
|
+
count: loadingRowCount,
|
|
9986
|
+
visibleCols,
|
|
9987
|
+
showSelectionColumn,
|
|
9988
|
+
showRowActionsColumn,
|
|
9989
|
+
rowHeight,
|
|
9990
|
+
rowClass: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row = slotProps.row) == null ? void 0 : _slotProps_row.className),
|
|
9991
|
+
cellClass: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell = slotProps.cell) == null ? void 0 : _slotProps_cell.className)
|
|
9992
|
+
}) : sortedRows.length === 0 ? renderEmptyState({
|
|
9993
|
+
totalColumnCount,
|
|
9994
|
+
emptyState,
|
|
9995
|
+
fallback: labels.noData,
|
|
9996
|
+
cellClass: cn(v.cell(), v.emptyState(), slotProps == null ? void 0 : (_slotProps_emptyState = slotProps.emptyState) == null ? void 0 : _slotProps_emptyState.className)
|
|
9997
|
+
}) : renderVirtualizedBody({
|
|
9998
|
+
vWindow,
|
|
9999
|
+
renderedRows,
|
|
10000
|
+
visibleCols,
|
|
10001
|
+
columnTypes,
|
|
10002
|
+
selection,
|
|
10003
|
+
rowSelection,
|
|
10004
|
+
showSelectionColumn,
|
|
10005
|
+
showRowActionsColumn,
|
|
10006
|
+
rowActions,
|
|
10007
|
+
getRowId,
|
|
10008
|
+
isInteractive,
|
|
10009
|
+
labels,
|
|
10010
|
+
classes: {
|
|
10011
|
+
row: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row1 = slotProps.row) == null ? void 0 : _slotProps_row1.className),
|
|
10012
|
+
cell: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell1 = slotProps.cell) == null ? void 0 : _slotProps_cell1.className),
|
|
10013
|
+
selectionCell: cn(v.cell(), v.selectionCell(), v.stickyLeftCell(), slotProps == null ? void 0 : (_slotProps_cell2 = slotProps.cell) == null ? void 0 : _slotProps_cell2.className, slotProps == null ? void 0 : (_slotProps_selectionCell1 = slotProps.selectionCell) == null ? void 0 : _slotProps_selectionCell1.className),
|
|
10014
|
+
stickyLeftCell: v.stickyLeftCell(),
|
|
10015
|
+
rowActionsCell: cn(v.cell(), v.rowActionsCell(), slotProps == null ? void 0 : (_slotProps_cell3 = slotProps.cell) == null ? void 0 : _slotProps_cell3.className, slotProps == null ? void 0 : (_slotProps_rowActionsCell = slotProps.rowActionsCell) == null ? void 0 : _slotProps_rowActionsCell.className)
|
|
10016
|
+
}
|
|
10017
|
+
})
|
|
10018
|
+
})
|
|
10019
|
+
]
|
|
10020
|
+
})
|
|
10021
|
+
}),
|
|
10022
|
+
bulkActions && selectedRows.length > 0 && jsxs("div", {
|
|
10023
|
+
className: cn(v.bulkActionFooter(), slotProps == null ? void 0 : (_slotProps_bulkActionFooter = slotProps.bulkActionFooter) == null ? void 0 : _slotProps_bulkActionFooter.className),
|
|
10024
|
+
role: "region",
|
|
10025
|
+
"aria-label": labels.selectedCount.replace('{count}', String(selectedRows.length)),
|
|
10026
|
+
children: [
|
|
10027
|
+
jsx("span", {
|
|
10028
|
+
className: "text-sm text-neutral-700",
|
|
10029
|
+
children: labels.selectedCount.replace('{count}', String(selectedRows.length))
|
|
10030
|
+
}),
|
|
10031
|
+
jsxs("div", {
|
|
10032
|
+
className: "flex items-center gap-2",
|
|
10033
|
+
children: [
|
|
10034
|
+
bulkActions(selectedRows),
|
|
10035
|
+
jsx("button", {
|
|
10036
|
+
type: "button",
|
|
10037
|
+
onClick: selection.clearSelection,
|
|
10038
|
+
className: cn('text-sm text-neutral-500 hover:text-neutral-900', 'underline-offset-2 hover:underline', 'focus:outline-none focus-visible:underline'),
|
|
10039
|
+
children: labels.clearSelection
|
|
10040
|
+
})
|
|
10041
|
+
]
|
|
10042
|
+
})
|
|
10043
|
+
]
|
|
10044
|
+
}),
|
|
10045
|
+
pagination && jsx("div", {
|
|
10046
|
+
className: cn(v.paginationFooter(), slotProps == null ? void 0 : (_slotProps_pagination = slotProps.pagination) == null ? void 0 : _slotProps_pagination.className),
|
|
10047
|
+
children: jsx(Pagination, {
|
|
10048
|
+
page: pagination.page,
|
|
10049
|
+
pageSize: pagination.pageSize,
|
|
10050
|
+
totalCount: totalCountProp != null ? totalCountProp : sortedRows.length,
|
|
10051
|
+
onPageChange: pagination.onPageChange,
|
|
10052
|
+
onPageSizeChange: pagination.onPageSizeChange,
|
|
10053
|
+
pageSizeOptions: pagination.pageSizeOptions
|
|
10054
|
+
})
|
|
10055
|
+
})
|
|
10056
|
+
]
|
|
10057
|
+
});
|
|
10058
|
+
}
|
|
10059
|
+
// ───── Header cell ─────
|
|
10060
|
+
function HeaderCell(props) {
|
|
10061
|
+
const { col, sortModel, onSortClick, inferredType, labels, className, buttonClassName, disabled } = props;
|
|
10062
|
+
const align = resolveAlign(col, inferredType);
|
|
10063
|
+
const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
|
|
10064
|
+
const sortEntry = sortModel.find((s)=>s.field === col.field);
|
|
10065
|
+
const ariaSort = (sortEntry == null ? void 0 : sortEntry.direction) === 'asc' ? 'ascending' : (sortEntry == null ? void 0 : sortEntry.direction) === 'desc' ? 'descending' : 'none';
|
|
10066
|
+
const headerContent = typeof col.header === 'function' ? col.header() : col.header;
|
|
10067
|
+
const cellStyle = {
|
|
10068
|
+
width: col.width != null ? `${col.width}px` : undefined,
|
|
10069
|
+
minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined,
|
|
10070
|
+
flex: col.flex
|
|
10071
|
+
};
|
|
10072
|
+
if (col.sortable) {
|
|
10073
|
+
return jsx("th", {
|
|
10074
|
+
scope: "col",
|
|
10075
|
+
"aria-sort": ariaSort,
|
|
10076
|
+
className: cn(className, alignClass),
|
|
10077
|
+
style: cellStyle,
|
|
10078
|
+
children: jsxs("button", {
|
|
10079
|
+
type: "button",
|
|
10080
|
+
onClick: onSortClick,
|
|
10081
|
+
disabled: disabled,
|
|
10082
|
+
"aria-label": ariaSort === 'ascending' ? labels.ariaSortAscending : ariaSort === 'descending' ? labels.ariaSortDescending : labels.ariaSortNone,
|
|
10083
|
+
className: cn(buttonClassName, align === 'right' && 'flex-row-reverse', 'disabled:cursor-not-allowed disabled:opacity-50'),
|
|
10084
|
+
children: [
|
|
10085
|
+
jsx("span", {
|
|
10086
|
+
children: headerContent
|
|
10087
|
+
}),
|
|
10088
|
+
jsx(SortIcon, {
|
|
10089
|
+
direction: sortEntry == null ? void 0 : sortEntry.direction
|
|
10090
|
+
})
|
|
10091
|
+
]
|
|
10092
|
+
})
|
|
10093
|
+
});
|
|
10094
|
+
}
|
|
10095
|
+
return jsx("th", {
|
|
10096
|
+
scope: "col",
|
|
10097
|
+
className: cn(className, alignClass),
|
|
10098
|
+
style: cellStyle,
|
|
10099
|
+
children: headerContent
|
|
10100
|
+
});
|
|
10101
|
+
}
|
|
10102
|
+
// ───── Virtualized body ─────
|
|
10103
|
+
function renderVirtualizedBody(args) {
|
|
10104
|
+
const { vWindow, renderedRows, visibleCols, columnTypes, selection, rowSelection, showSelectionColumn, showRowActionsColumn, rowActions, getRowId, isInteractive, labels, classes } = args;
|
|
10105
|
+
return jsxs(Fragment$1, {
|
|
10106
|
+
children: [
|
|
10107
|
+
vWindow.paddingTop > 0 && jsx("tr", {
|
|
10108
|
+
"aria-hidden": "true",
|
|
10109
|
+
style: {
|
|
10110
|
+
height: vWindow.paddingTop
|
|
10111
|
+
}
|
|
10112
|
+
}),
|
|
10113
|
+
renderedRows.map((row, localIdx)=>{
|
|
10114
|
+
const absoluteIdx = vWindow.startIndex + localIdx;
|
|
10115
|
+
const rowId = getRowId(row, absoluteIdx);
|
|
10116
|
+
const isSelected = selection.isSelected(rowId);
|
|
10117
|
+
return jsxs("tr", {
|
|
10118
|
+
"data-selected": isSelected ? 'true' : 'false',
|
|
10119
|
+
"aria-selected": rowSelection !== 'none' ? isSelected : undefined,
|
|
10120
|
+
className: classes.row,
|
|
10121
|
+
children: [
|
|
10122
|
+
showSelectionColumn && jsx("td", {
|
|
10123
|
+
className: classes.selectionCell,
|
|
10124
|
+
children: jsx("input", {
|
|
10125
|
+
type: "checkbox",
|
|
10126
|
+
"aria-label": labels.ariaSelectRow,
|
|
10127
|
+
checked: isSelected,
|
|
10128
|
+
onChange: ()=>selection.toggleRow(rowId),
|
|
10129
|
+
disabled: !isInteractive,
|
|
10130
|
+
className: "cursor-pointer"
|
|
10131
|
+
})
|
|
10132
|
+
}),
|
|
10133
|
+
visibleCols.map((col)=>jsx(DataCell, {
|
|
10134
|
+
col: col,
|
|
10135
|
+
row: row,
|
|
10136
|
+
rowIndex: absoluteIdx,
|
|
10137
|
+
isSelected: isSelected,
|
|
10138
|
+
inferredType: columnTypes.get(col.field),
|
|
10139
|
+
cellClass: classes.cell,
|
|
10140
|
+
stickyLeftClass: classes.stickyLeftCell
|
|
10141
|
+
}, col.field)),
|
|
10142
|
+
showRowActionsColumn && rowActions && jsx("td", {
|
|
10143
|
+
className: classes.rowActionsCell,
|
|
10144
|
+
children: rowActions(row)
|
|
10145
|
+
})
|
|
10146
|
+
]
|
|
10147
|
+
}, rowId);
|
|
10148
|
+
}),
|
|
10149
|
+
vWindow.paddingBottom > 0 && jsx("tr", {
|
|
10150
|
+
"aria-hidden": "true",
|
|
10151
|
+
style: {
|
|
10152
|
+
height: vWindow.paddingBottom
|
|
10153
|
+
}
|
|
10154
|
+
})
|
|
10155
|
+
]
|
|
10156
|
+
});
|
|
10157
|
+
}
|
|
10158
|
+
// ───── Data cell ─────
|
|
10159
|
+
function DataCell(props) {
|
|
10160
|
+
const { col, row, rowIndex, isSelected, inferredType, cellClass, stickyLeftClass } = props;
|
|
10161
|
+
const value = getNestedValue(row, col.field);
|
|
10162
|
+
const align = resolveAlign(col, inferredType);
|
|
10163
|
+
const tabularNums = resolveTabularNums(col, inferredType);
|
|
10164
|
+
const monospace = resolveMonospace(col);
|
|
10165
|
+
const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
|
|
10166
|
+
const columnAccessState = col.access ? {
|
|
10167
|
+
hidden: col.access.onUnauthorized === 'hide',
|
|
10168
|
+
disabled: col.access.onUnauthorized === 'disable',
|
|
10169
|
+
readonly: col.access.onUnauthorized === 'readonly'
|
|
10170
|
+
} : null;
|
|
10171
|
+
const content = col.cellRenderer ? col.cellRenderer({
|
|
10172
|
+
row,
|
|
10173
|
+
value,
|
|
10174
|
+
rowIndex,
|
|
10175
|
+
isSelected,
|
|
10176
|
+
isExpanded: false,
|
|
10177
|
+
access: columnAccessState
|
|
10178
|
+
}) : renderDefaultCell(value);
|
|
10179
|
+
return jsx("td", {
|
|
10180
|
+
className: cn(cellClass, alignClass, tabularNums && 'tabular-nums', monospace && 'font-mono', col.sticky === 'left' && stickyLeftClass),
|
|
10181
|
+
style: {
|
|
10182
|
+
width: col.width != null ? `${col.width}px` : undefined,
|
|
10183
|
+
minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined
|
|
10184
|
+
},
|
|
10185
|
+
children: content
|
|
10186
|
+
});
|
|
10187
|
+
}
|
|
10188
|
+
// ───── Helpers ─────
|
|
10189
|
+
function renderDefaultCell(value) {
|
|
10190
|
+
if (value == null) return null;
|
|
10191
|
+
if (typeof value === 'boolean') return value ? '✓' : '✗';
|
|
10192
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
10193
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
10194
|
+
return value;
|
|
10195
|
+
}
|
|
10196
|
+
function handleSortClick(col, event, current, setSortModel) {
|
|
10197
|
+
if (!col.sortable) return;
|
|
10198
|
+
const multi = event.shiftKey;
|
|
10199
|
+
setSortModel((prev)=>cycleSortFor(prev, col.field, multi));
|
|
10200
|
+
}
|
|
10201
|
+
function renderLoadingRows(args) {
|
|
10202
|
+
const { count, visibleCols, showSelectionColumn, showRowActionsColumn, rowHeight, rowClass, cellClass } = args;
|
|
10203
|
+
return Array.from({
|
|
10204
|
+
length: count
|
|
10205
|
+
}).map((_, i)=>jsxs("tr", {
|
|
10206
|
+
className: rowClass,
|
|
10207
|
+
"aria-busy": "true",
|
|
10208
|
+
style: {
|
|
10209
|
+
height: rowHeight
|
|
10210
|
+
},
|
|
10211
|
+
children: [
|
|
10212
|
+
showSelectionColumn && jsx("td", {
|
|
10213
|
+
className: cellClass,
|
|
10214
|
+
children: jsx(Skeleton, {
|
|
10215
|
+
variant: "rectangle",
|
|
10216
|
+
width: "16px",
|
|
10217
|
+
height: "16px"
|
|
10218
|
+
})
|
|
10219
|
+
}),
|
|
10220
|
+
visibleCols.map((col)=>jsx("td", {
|
|
10221
|
+
className: cellClass,
|
|
10222
|
+
children: jsx(Skeleton, {
|
|
10223
|
+
variant: "text",
|
|
10224
|
+
width: "80%"
|
|
10225
|
+
})
|
|
10226
|
+
}, col.field)),
|
|
10227
|
+
showRowActionsColumn && jsx("td", {
|
|
10228
|
+
className: cellClass,
|
|
10229
|
+
children: jsx(Skeleton, {
|
|
10230
|
+
variant: "rectangle",
|
|
10231
|
+
width: "24px",
|
|
10232
|
+
height: "24px"
|
|
10233
|
+
})
|
|
10234
|
+
})
|
|
10235
|
+
]
|
|
10236
|
+
}, `loading-${i}`));
|
|
10237
|
+
}
|
|
10238
|
+
function renderEmptyState(args) {
|
|
10239
|
+
var _args_emptyState;
|
|
10240
|
+
return jsx("tr", {
|
|
10241
|
+
children: jsx("td", {
|
|
10242
|
+
colSpan: args.totalColumnCount,
|
|
10243
|
+
className: args.cellClass,
|
|
10244
|
+
children: (_args_emptyState = args.emptyState) != null ? _args_emptyState : args.fallback
|
|
10245
|
+
})
|
|
10246
|
+
});
|
|
10247
|
+
}
|
|
10248
|
+
// ───── Icons ─────
|
|
10249
|
+
function SortIcon(props) {
|
|
10250
|
+
return jsx("svg", {
|
|
10251
|
+
width: "0.75em",
|
|
10252
|
+
height: "0.75em",
|
|
10253
|
+
viewBox: "0 0 16 16",
|
|
10254
|
+
fill: "currentColor",
|
|
10255
|
+
"aria-hidden": "true",
|
|
10256
|
+
className: "shrink-0 opacity-60 group-hover:opacity-100",
|
|
10257
|
+
children: props.direction === 'asc' ? jsx("path", {
|
|
10258
|
+
d: "M8 4l4 6H4z"
|
|
10259
|
+
}) : props.direction === 'desc' ? jsx("path", {
|
|
10260
|
+
d: "M8 12L4 6h8z"
|
|
10261
|
+
}) : jsxs(Fragment$1, {
|
|
10262
|
+
children: [
|
|
10263
|
+
jsx("path", {
|
|
10264
|
+
d: "M8 3l3 4H5z",
|
|
10265
|
+
opacity: "0.5"
|
|
10266
|
+
}),
|
|
10267
|
+
jsx("path", {
|
|
10268
|
+
d: "M8 13l-3-4h6z",
|
|
10269
|
+
opacity: "0.5"
|
|
10270
|
+
})
|
|
10271
|
+
]
|
|
10272
|
+
})
|
|
10273
|
+
});
|
|
10274
|
+
}
|
|
10275
|
+
function SearchIcon(props) {
|
|
10276
|
+
return jsxs("svg", {
|
|
10277
|
+
width: "0.875em",
|
|
10278
|
+
height: "0.875em",
|
|
10279
|
+
viewBox: "0 0 16 16",
|
|
10280
|
+
fill: "none",
|
|
10281
|
+
stroke: "currentColor",
|
|
10282
|
+
strokeWidth: "1.5",
|
|
10283
|
+
strokeLinecap: "round",
|
|
10284
|
+
strokeLinejoin: "round",
|
|
10285
|
+
"aria-hidden": "true",
|
|
10286
|
+
className: props.className,
|
|
10287
|
+
children: [
|
|
10288
|
+
jsx("circle", {
|
|
10289
|
+
cx: "7",
|
|
10290
|
+
cy: "7",
|
|
10291
|
+
r: "4.5"
|
|
10292
|
+
}),
|
|
10293
|
+
jsx("path", {
|
|
10294
|
+
d: "M11 11l3 3"
|
|
10295
|
+
})
|
|
10296
|
+
]
|
|
10297
|
+
});
|
|
10298
|
+
}
|
|
10299
|
+
|
|
7847
10300
|
/**
|
|
7848
10301
|
* @dashforge/tw
|
|
7849
10302
|
*
|
|
@@ -7871,6 +10324,6 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [
|
|
|
7871
10324
|
*/ // ───── Components ─────
|
|
7872
10325
|
/**
|
|
7873
10326
|
* Package version (synced with `package.json` at publish time).
|
|
7874
|
-
*/ const VERSION = '0.
|
|
10327
|
+
*/ const VERSION = '0.7.0-beta';
|
|
7875
10328
|
|
|
7876
|
-
export { Accordion, AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, Container, DateTimePicker, Dialog, Divider, Grid, LeftNav, NumberField, OTPField, Pagination, Popover, RadioGroup, Skeleton, SnackbarProvider, Stack, Switch, Tabs, TextField, Textarea, Tooltip, TopBar, Typography, VERSION, VisuallyHidden, accordionVariants, appShellVariants, autocompleteVariants, boxVariants, breadcrumbsVariants, buttonVariants, checkboxVariants, cn, confirmDialogVariants, containerVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, gridVariants, isoToInputValue, leftNavVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, radioGroupVariants, skeletonVariants, snackbarVariants, stackVariants, switchVariants, tabsVariants, textFieldVariants, textareaVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };
|
|
10329
|
+
export { Accordion, AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, Container, DataGrid, DateTimePicker, Dialog, Divider, Grid, LeftNav, NumberField, OTPField, Pagination, Popover, RadioGroup, RenderButton, RenderChip, RenderText, RenderTwoLine, RowActionsMenu, Skeleton, SnackbarProvider, Stack, Switch, Table, Tabs, TextField, Textarea, Tooltip, TopBar, Typography, VERSION, VisuallyHidden, accordionVariants, appShellVariants, autocompleteVariants, boxVariants, breadcrumbsVariants, buttonVariants, checkboxVariants, cn, confirmDialogVariants, containerVariants, dataGridVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, getNestedValue, gridVariants, isoToInputValue, leftNavVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, radioGroupVariants, skeletonVariants, snackbarVariants, stackVariants, switchVariants, tableVariants, tabsVariants, textFieldVariants, textareaVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };
|