@dashforge/tw 0.6.0-beta → 0.8.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +451 -0
  2. package/THEME-AUDIT.md +175 -0
  3. package/dist/index.esm.js +2009 -87
  4. package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -1
  5. package/dist/src/components/Box/box.variants.d.ts.map +1 -1
  6. package/dist/src/components/DataGrid/DataGrid.d.ts +29 -0
  7. package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -0
  8. package/dist/src/components/DataGrid/dataGrid.types.d.ts +214 -0
  9. package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
  10. package/dist/src/components/DataGrid/dataGrid.variants.d.ts +222 -0
  11. package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
  12. package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts +23 -0
  13. package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts.map +1 -0
  14. package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts +50 -0
  15. package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts.map +1 -0
  16. package/dist/src/components/DataGrid/resize/useColumnResize.d.ts +32 -0
  17. package/dist/src/components/DataGrid/resize/useColumnResize.d.ts.map +1 -0
  18. package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts +33 -0
  19. package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts.map +1 -0
  20. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
  21. package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
  22. package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
  23. package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
  24. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
  25. package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
  26. package/dist/src/components/Table/Table.d.ts.map +1 -1
  27. package/dist/src/components/Table/table.types.d.ts +77 -5
  28. package/dist/src/components/Table/table.types.d.ts.map +1 -1
  29. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
  30. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
  31. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
  32. package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
  33. package/dist/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.d.ts +1 -1
  34. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
  35. package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
  36. package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
  37. package/dist/src/components/_shared/data/useTableFilter.d.ts +36 -0
  38. package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
  39. package/dist/src/components/{Table/_internal → _shared/data}/useTableSearch.d.ts +1 -1
  40. package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
  41. package/dist/src/components/{Table/_internal → _shared/data}/useTableSelection.d.ts +1 -1
  42. package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
  43. package/dist/src/components/{Table/_internal → _shared/data}/useTableSort.d.ts +1 -1
  44. package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
  45. package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
  46. package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
  47. package/dist/src/index.d.ts +6 -3
  48. package/dist/src/index.d.ts.map +1 -1
  49. package/package.json +5 -5
  50. package/src/components/Accordion/accordion.variants.ts +5 -3
  51. package/src/components/Box/Box.test.tsx +32 -11
  52. package/src/components/Box/box.variants.ts +23 -4
  53. package/src/components/DataGrid/DataGrid.test.tsx +917 -0
  54. package/src/components/DataGrid/DataGrid.tsx +1199 -0
  55. package/src/components/DataGrid/dataGrid.types.ts +238 -0
  56. package/src/components/DataGrid/dataGrid.variants.ts +152 -0
  57. package/src/components/DataGrid/filters/ColumnFilters.tsx +432 -0
  58. package/src/components/DataGrid/reorder/useColumnReorder.test.ts +38 -0
  59. package/src/components/DataGrid/reorder/useColumnReorder.ts +144 -0
  60. package/src/components/DataGrid/resize/useColumnResize.ts +106 -0
  61. package/src/components/DataGrid/visibility/ColumnVisibilityDialog.tsx +200 -0
  62. package/src/components/Dialog/dialog.variants.ts +10 -6
  63. package/src/components/Divider/Divider.test.tsx +3 -1
  64. package/src/components/Divider/Divider.tsx +1 -1
  65. package/src/components/Divider/divider.variants.ts +2 -1
  66. package/src/components/Pagination/Pagination.tsx +3 -1
  67. package/src/components/Pagination/pagination.variants.ts +19 -13
  68. package/src/components/Popover/popover.variants.ts +10 -4
  69. package/src/components/Skeleton/skeleton.variants.ts +2 -1
  70. package/src/components/Table/Table.tsx +22 -8
  71. package/src/components/Table/table.types.ts +83 -5
  72. package/src/components/Tabs/tabs.variants.ts +19 -8
  73. package/src/components/Tooltip/tooltip.variants.ts +12 -2
  74. package/src/components/Typography/Typography.test.tsx +3 -1
  75. package/src/components/Typography/typography.variants.ts +6 -2
  76. package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.test.ts +1 -1
  77. package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.ts +1 -1
  78. package/src/components/_shared/data/useTableFilter.test.ts +166 -0
  79. package/src/components/_shared/data/useTableFilter.ts +118 -0
  80. package/src/components/{Table/_internal → _shared/data}/useTableSearch.test.ts +1 -1
  81. package/src/components/{Table/_internal → _shared/data}/useTableSearch.ts +1 -1
  82. package/src/components/{Table/_internal → _shared/data}/useTableSelection.ts +1 -1
  83. package/src/components/{Table/_internal → _shared/data}/useTableSort.test.ts +1 -1
  84. package/src/components/{Table/_internal → _shared/data}/useTableSort.ts +1 -1
  85. package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
  86. package/src/components/_shared/data/useVirtualizer.ts +199 -0
  87. package/src/components/_shared/themeIdentity.test.ts +113 -0
  88. package/src/index.ts +20 -2
  89. package/dist/src/components/Table/_internal/getNestedValue.d.ts.map +0 -1
  90. package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts.map +0 -1
  91. package/dist/src/components/Table/_internal/useControllableState.d.ts.map +0 -1
  92. package/dist/src/components/Table/_internal/useDebouncedValue.d.ts.map +0 -1
  93. package/dist/src/components/Table/_internal/useTableFilter.d.ts +0 -13
  94. package/dist/src/components/Table/_internal/useTableFilter.d.ts.map +0 -1
  95. package/dist/src/components/Table/_internal/useTableSearch.d.ts.map +0 -1
  96. package/dist/src/components/Table/_internal/useTableSelection.d.ts.map +0 -1
  97. package/dist/src/components/Table/_internal/useTableSort.d.ts.map +0 -1
  98. package/src/components/Table/_internal/themeIdentity.test.ts +0 -84
  99. package/src/components/Table/_internal/useTableFilter.ts +0 -37
  100. /package/dist/src/components/{Table/_internal → _shared/data}/getNestedValue.d.ts +0 -0
  101. /package/dist/src/components/{Table/_internal → _shared/data}/useControllableState.d.ts +0 -0
  102. /package/dist/src/components/{Table/_internal → _shared/data}/useDebouncedValue.d.ts +0 -0
  103. /package/src/components/{Table/_internal → _shared/data}/getNestedValue.test.ts +0 -0
  104. /package/src/components/{Table/_internal → _shared/data}/getNestedValue.ts +0 -0
  105. /package/src/components/{Table/_internal → _shared/data}/useControllableState.ts +0 -0
  106. /package/src/components/{Table/_internal → _shared/data}/useDebouncedValue.ts +0 -0
  107. /package/src/components/{Table/_internal → _shared/data}/useTableSelection.test.ts +0 -0
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
- neutral: 'text-neutral-900 dark:text-neutral-100',
5296
- muted: 'text-neutral-600 dark:text-neutral-400'
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
- elevated: 'bg-white dark:bg-neutral-900',
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
- class: 'border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900'
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
- class: 'bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'
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
- class: 'bg-neutral-900 text-white dark:bg-neutral-100 dark:text-neutral-900'
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
- neutral: 'border-neutral-200 dark:border-neutral-800',
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 dark:text-neutral-400', labelPadding),
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
- 'w-full bg-white dark:bg-neutral-900 rounded-lg shadow-xl',
6825
- 'border border-neutral-200 dark:border-neutral-800',
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
- title: 'text-lg font-semibold leading-tight text-neutral-900 dark:text-neutral-50',
6833
- description: 'text-sm text-neutral-600 dark:text-neutral-400',
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
- 'dark:text-neutral-400 dark:hover:text-neutral-50',
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
- list: 'border-b border-neutral-200 dark:border-neutral-800',
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 dark:text-neutral-400',
6958
- 'hover:text-neutral-900 dark:hover:text-neutral-50',
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
- list: 'rounded-lg bg-neutral-100 dark:bg-neutral-800 p-1',
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 dark:text-neutral-400',
6967
- 'hover:text-neutral-900 dark:hover:text-neutral-50',
6968
- 'data-[state=active]:bg-white dark:data-[state=active]:bg-neutral-900',
6969
- 'data-[state=active]:text-neutral-900 dark:data-[state=active]:text-neutral-50',
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
- arrow: 'fill-neutral-900 dark:fill-neutral-50'
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
- 'z-50 rounded-md border border-neutral-200 dark:border-neutral-800',
7118
- 'bg-white dark:bg-neutral-900 p-4 shadow-lg',
7119
- 'text-sm text-neutral-900 dark:text-neutral-50',
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
- arrow: 'fill-white dark:fill-neutral-900'
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
- item: 'border-b border-neutral-200 dark:border-neutral-800',
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 dark:text-neutral-50',
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 dark:text-neutral-300',
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
- 'bg-neutral-200 dark:bg-neutral-800',
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
- summary: 'text-neutral-600 dark:text-neutral-400',
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 dark:border-neutral-700',
7438
- 'bg-white dark:bg-neutral-900',
7439
- 'text-neutral-700 dark:text-neutral-300',
7440
- 'hover:bg-neutral-50 dark:hover:bg-neutral-800',
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 dark:border-neutral-700',
7452
- 'bg-white dark:bg-neutral-900',
7453
- 'text-neutral-700 dark:text-neutral-300',
7454
- 'hover:bg-neutral-50 dark:hover:bg-neutral-800',
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 dark:text-neutral-300'
7526
+ 'text-neutral-700'
7463
7527
  ],
7464
7528
  jumpInput: [
7465
- 'rounded-md border border-neutral-300 dark:border-neutral-700',
7466
- 'bg-white dark:bg-neutral-900',
7467
- 'text-neutral-900 dark:text-neutral-50',
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$1 = {
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$1, 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('rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-2 py-1', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500'),
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
@@ -8193,28 +8259,99 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [
8193
8259
  }
8194
8260
 
8195
8261
  /**
8196
- * Filters `rows` by the per-column filter model. v1 only supports
8197
- * the `contains` operator (text substring) — additional ops
8198
- * (`equals`, `in`, `gte`, `lte`, `between`, `before`, `after`)
8199
- * land in v1-bis once the filter UI surface for each shape is
8200
- * fleshed out.
8201
- *
8202
- * AND semantics across columns: a row must match EVERY active
8203
- * filter. Within a single filter, the op decides the predicate.
8262
+ * Coerce a value to a number for range comparison. Returns
8263
+ * `null` if not coercible.
8264
+ */ function toNumber(value) {
8265
+ if (value == null) return null;
8266
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
8267
+ if (typeof value === 'bigint') return Number(value);
8268
+ if (typeof value === 'string') {
8269
+ const n = Number(value);
8270
+ return Number.isFinite(n) ? n : null;
8271
+ }
8272
+ return null;
8273
+ }
8274
+ /**
8275
+ * Coerce a value to a Date's epoch ms for date-range comparison.
8276
+ * Accepts `Date` instances + ISO 8601 strings. Returns `null` if
8277
+ * not coercible.
8278
+ */ function toDateMs(value) {
8279
+ if (value == null) return null;
8280
+ if (value instanceof Date) return value.getTime();
8281
+ if (typeof value === 'string') {
8282
+ const d = new Date(value);
8283
+ const t = d.getTime();
8284
+ return Number.isNaN(t) ? null : t;
8285
+ }
8286
+ return null;
8287
+ }
8288
+ /**
8289
+ * Test one row × one filter. Returns `true` if the row passes the
8290
+ * filter. The operator decides the predicate:
8291
+ *
8292
+ * - `contains` → substring match on stringified value (text filter)
8293
+ * - `equals` → strict equality (boolean filter mostly)
8294
+ * - `between` → `value: [min, max]` tuple. Each end can be `null`
8295
+ * for an open range. Used by number range + date range filters.
8296
+ *
8297
+ * Empty filter is a no-op (passes every row).
8298
+ */ function passesFilter(rowValue, op, filterValue) {
8299
+ switch(op){
8300
+ case 'contains':
8301
+ {
8302
+ const needle = typeof filterValue === 'string' ? filterValue.trim().toLowerCase() : '';
8303
+ if (!needle) return true;
8304
+ return stringifyForSearch(rowValue).toLowerCase().includes(needle);
8305
+ }
8306
+ case 'equals':
8307
+ {
8308
+ if (filterValue == null) return true;
8309
+ return rowValue === filterValue;
8310
+ }
8311
+ case 'between':
8312
+ {
8313
+ if (!Array.isArray(filterValue) || filterValue.length !== 2) return true;
8314
+ const [min, max] = filterValue;
8315
+ if (min == null && max == null) return true;
8316
+ // Try number range first.
8317
+ const rowNum = toNumber(rowValue);
8318
+ const minNum = min == null ? null : toNumber(min);
8319
+ const maxNum = max == null ? null : toNumber(max);
8320
+ if (rowNum != null && (minNum != null || maxNum != null)) {
8321
+ if (minNum != null && rowNum < minNum) return false;
8322
+ if (maxNum != null && rowNum > maxNum) return false;
8323
+ return true;
8324
+ }
8325
+ // Fall through to date range.
8326
+ const rowMs = toDateMs(rowValue);
8327
+ const minMs = min == null ? null : toDateMs(min);
8328
+ const maxMs = max == null ? null : toDateMs(max);
8329
+ if (rowMs != null && (minMs != null || maxMs != null)) {
8330
+ if (minMs != null && rowMs < minMs) return false;
8331
+ if (maxMs != null && rowMs > maxMs) return false;
8332
+ return true;
8333
+ }
8334
+ // If neither coercion worked, let the row pass (defensive).
8335
+ return true;
8336
+ }
8337
+ default:
8338
+ return true;
8339
+ }
8340
+ }
8341
+ /**
8342
+ * Filters `rows` by the per-column filter model. AND semantics
8343
+ * across columns — a row must pass EVERY active filter.
8344
+ *
8345
+ * Each filter item specifies an `op` (`'contains'` / `'equals'` /
8346
+ * `'between'`) and a `value` whose shape matches the operator
8347
+ * (string for contains; primitive for equals; `[min, max]` tuple
8348
+ * for between).
8204
8349
  */ function useTableFilter(rows, filterModel) {
8205
8350
  return useMemo(()=>{
8206
8351
  if (!filterModel || filterModel.length === 0) return rows;
8207
8352
  return rows.filter((row)=>filterModel.every((filter)=>{
8208
8353
  const value = getNestedValue(row, filter.field);
8209
- const stringValue = stringifyForSearch(value).toLowerCase();
8210
- const needle = filter.value.trim().toLowerCase();
8211
- if (!needle) return true; // empty filter == no-op
8212
- switch(filter.op){
8213
- case 'contains':
8214
- return stringValue.includes(needle);
8215
- default:
8216
- return true;
8217
- }
8354
+ return passesFilter(value, filter.op, filter.value);
8218
8355
  }));
8219
8356
  }, [
8220
8357
  rows,
@@ -8510,7 +8647,7 @@ function inferType(value) {
8510
8647
  return col.monospace === true;
8511
8648
  }
8512
8649
 
8513
- const DEFAULT_LABELS = {
8650
+ const DEFAULT_LABELS$1 = {
8514
8651
  searchPlaceholder: 'Search...',
8515
8652
  noData: 'No data',
8516
8653
  loading: 'Loading…',
@@ -8526,10 +8663,23 @@ const DEFAULT_LABELS = {
8526
8663
  filterColumn: 'Filter',
8527
8664
  filterApply: 'Apply',
8528
8665
  filterClear: 'Clear',
8666
+ filterMin: 'Min',
8667
+ filterMax: 'Max',
8668
+ filterFrom: 'From',
8669
+ filterTo: 'To',
8670
+ filterAll: 'All',
8671
+ filterTrue: 'True',
8672
+ filterFalse: 'False',
8529
8673
  density: 'Density',
8530
8674
  densityCompact: 'Compact',
8531
8675
  densityComfortable: 'Comfortable',
8532
- densitySpacious: 'Spacious'
8676
+ densitySpacious: 'Spacious',
8677
+ columnsButton: 'Columns',
8678
+ columnsTitle: 'Manage columns',
8679
+ columnsDescription: 'Show or hide columns. Required columns cannot be toggled.',
8680
+ columnsShowAll: 'Show all',
8681
+ columnsHideAll: 'Hide all',
8682
+ columnsDone: 'Done'
8533
8683
  };
8534
8684
  const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8535
8685
  /**
@@ -8556,7 +8706,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8556
8706
  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;
8557
8707
  const { rows, cols, getRowId = DEFAULT_GET_ROW_ID, // State props (controllable)
8558
8708
  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;
8559
- const labels = _extends({}, DEFAULT_LABELS, labelsProp);
8709
+ const labels = _extends({}, DEFAULT_LABELS$1, labelsProp);
8560
8710
  const tableAccessState = useAccessState(tableAccess);
8561
8711
  // ───── Controllable state machinery ─────
8562
8712
  const [sortModel, setSortModel] = useControllableState({
@@ -8629,7 +8779,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8629
8779
  className: "sr-only",
8630
8780
  children: labels.searchPlaceholder
8631
8781
  }),
8632
- jsx(SearchIcon, {
8782
+ jsx(SearchIcon$1, {
8633
8783
  className: "ml-2 shrink-0 text-neutral-500"
8634
8784
  }),
8635
8785
  jsx("input", {
@@ -8681,10 +8831,10 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8681
8831
  }),
8682
8832
  visibleCols.map((col)=>{
8683
8833
  var _slotProps_headerCell;
8684
- return jsx(HeaderCell, {
8834
+ return jsx(HeaderCell$1, {
8685
8835
  col: col,
8686
8836
  sortModel: sortModel,
8687
- onSortClick: (e)=>handleSortClick(col, e, sortModel, setSortModel),
8837
+ onSortClick: (e)=>handleSortClick$1(col, e, sortModel, setSortModel),
8688
8838
  inferredType: columnTypes.get(col.field),
8689
8839
  labels: labels,
8690
8840
  className: cn(v.headerCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className),
@@ -8702,7 +8852,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8702
8852
  }),
8703
8853
  jsx("tbody", {
8704
8854
  className: cn(v.tbody(), slotProps == null ? void 0 : (_slotProps_tbody = slotProps.tbody) == null ? void 0 : _slotProps_tbody.className),
8705
- children: loading ? renderLoadingRows({
8855
+ children: loading ? renderLoadingRows$1({
8706
8856
  count: loadingRowCount,
8707
8857
  visibleCols,
8708
8858
  showSelectionColumn,
@@ -8710,7 +8860,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8710
8860
  showRowActionsColumn,
8711
8861
  rowClass: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row = slotProps.row) == null ? void 0 : _slotProps_row.className),
8712
8862
  cellClass: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell = slotProps.cell) == null ? void 0 : _slotProps_cell.className)
8713
- }) : sortedRows.length === 0 ? renderEmptyState({
8863
+ }) : sortedRows.length === 0 ? renderEmptyState$1({
8714
8864
  totalColumnCount,
8715
8865
  emptyState,
8716
8866
  fallback: labels.noData,
@@ -8782,7 +8932,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8782
8932
  });
8783
8933
  }
8784
8934
  // ───── Header cell (with sort button) ─────
8785
- function HeaderCell(props) {
8935
+ function HeaderCell$1(props) {
8786
8936
  const { col, sortModel, onSortClick, inferredType, labels, className, buttonClassName, disabled } = props;
8787
8937
  const align = resolveAlign(col, inferredType);
8788
8938
  const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
@@ -8810,7 +8960,7 @@ function HeaderCell(props) {
8810
8960
  jsx("span", {
8811
8961
  children: headerContent
8812
8962
  }),
8813
- jsx(SortIcon, {
8963
+ jsx(SortIcon$1, {
8814
8964
  direction: sortEntry == null ? void 0 : sortEntry.direction
8815
8965
  })
8816
8966
  ]
@@ -8858,7 +9008,7 @@ function Row(props) {
8858
9008
  children: jsx(ChevronRightIcon, {})
8859
9009
  })
8860
9010
  }),
8861
- visibleCols.map((col)=>jsx(DataCell, {
9011
+ visibleCols.map((col)=>jsx(DataCell$1, {
8862
9012
  col: col,
8863
9013
  row: row,
8864
9014
  rowIndex: rowIndex,
@@ -8885,7 +9035,7 @@ function Row(props) {
8885
9035
  });
8886
9036
  }
8887
9037
  // ───── Data cell ─────
8888
- function DataCell(props) {
9038
+ function DataCell$1(props) {
8889
9039
  const { col, row, rowIndex, isSelected, isExpanded, inferredType, cellClass } = props;
8890
9040
  const value = getNestedValue(row, col.field);
8891
9041
  const align = resolveAlign(col, inferredType);
@@ -8910,7 +9060,7 @@ function DataCell(props) {
8910
9060
  isSelected,
8911
9061
  isExpanded,
8912
9062
  access: columnAccessState
8913
- }) : renderDefaultCell(value);
9063
+ }) : renderDefaultCell$1(value);
8914
9064
  return jsx("td", {
8915
9065
  className: cn(cellClass, alignClass, tabularNums && 'tabular-nums', monospace && 'font-mono'),
8916
9066
  style: {
@@ -8921,19 +9071,19 @@ function DataCell(props) {
8921
9071
  });
8922
9072
  }
8923
9073
  // ───── Helpers ─────
8924
- function renderDefaultCell(value) {
9074
+ function renderDefaultCell$1(value) {
8925
9075
  if (value == null) return null;
8926
9076
  if (typeof value === 'boolean') return value ? '✓' : '✗';
8927
9077
  if (value instanceof Date) return value.toISOString().slice(0, 10);
8928
9078
  if (typeof value === 'object') return JSON.stringify(value);
8929
9079
  return value;
8930
9080
  }
8931
- function handleSortClick(col, event, current, setSortModel) {
9081
+ function handleSortClick$1(col, event, current, setSortModel) {
8932
9082
  if (!col.sortable) return;
8933
9083
  const multi = event.shiftKey;
8934
9084
  setSortModel((prev)=>cycleSortFor(prev, col.field, multi));
8935
9085
  }
8936
- function renderLoadingRows(args) {
9086
+ function renderLoadingRows$1(args) {
8937
9087
  const { count, visibleCols, showSelectionColumn, showExpandColumn, showRowActionsColumn, rowClass, cellClass } = args;
8938
9088
  return Array.from({
8939
9089
  length: count
@@ -8975,7 +9125,7 @@ function renderLoadingRows(args) {
8975
9125
  ]
8976
9126
  }, `loading-${i}`));
8977
9127
  }
8978
- function renderEmptyState(args) {
9128
+ function renderEmptyState$1(args) {
8979
9129
  var _args_emptyState;
8980
9130
  return jsx("tr", {
8981
9131
  children: jsx("td", {
@@ -8986,7 +9136,7 @@ function renderEmptyState(args) {
8986
9136
  });
8987
9137
  }
8988
9138
  // ───── Icons ─────
8989
- function SortIcon(props) {
9139
+ function SortIcon$1(props) {
8990
9140
  return jsx("svg", {
8991
9141
  width: "0.75em",
8992
9142
  height: "0.75em",
@@ -9028,7 +9178,7 @@ function ChevronRightIcon() {
9028
9178
  })
9029
9179
  });
9030
9180
  }
9031
- function SearchIcon(props) {
9181
+ function SearchIcon$1(props) {
9032
9182
  return jsxs("svg", {
9033
9183
  width: "0.875em",
9034
9184
  height: "0.875em",
@@ -9393,6 +9543,1778 @@ function RowActionMenuItem(props) {
9393
9543
  });
9394
9544
  }
9395
9545
 
9546
+ /**
9547
+ * `<DataGrid>` variant recipe.
9548
+ *
9549
+ * Sibling of `tableVariants` — shares the same visual axes (variant /
9550
+ * size / density) and the same identity-rule patterns:
9551
+ * - No `dark:` variants on neutral palette (auto-invert via the
9552
+ * dashforgePreset() CSS var swap).
9553
+ * - Selected row uses LeftNav `itemActive` pattern
9554
+ * (`bg-primary-100 text-primary-900`) — guaranteed contrast.
9555
+ * - Hover uses one-tier elevation (`bg-neutral-100`).
9556
+ *
9557
+ * DataGrid-specific slot additions vs Table:
9558
+ * - `stickyLeftCell` — column that uses `cols[i].sticky === 'left'`.
9559
+ * Z-index ordering: `thead` (z-10) < `stickyLeftCell` (z-11) <
9560
+ * `header × stickyLeft top-left corner cell` (z-20).
9561
+ * - `paginationFooter` — wraps the optional `<Pagination>`.
9562
+ */ const dataGridVariants = tv({
9563
+ slots: {
9564
+ root: 'w-full',
9565
+ toolbar: 'flex flex-wrap items-center gap-3 mb-3',
9566
+ search: [
9567
+ 'inline-flex items-center gap-2',
9568
+ 'w-full max-w-sm rounded-md',
9569
+ 'border border-neutral-300',
9570
+ 'bg-neutral-50',
9571
+ 'focus-within:ring-2 focus-within:ring-primary-500',
9572
+ 'transition-colors motion-reduce:transition-none'
9573
+ ],
9574
+ // The scroll container needs an explicit bounded height for
9575
+ // virtualization to compute the window. The consumer passes
9576
+ // `height` prop (inline style) or sets it via `sx` / parent
9577
+ // layout — DataGrid does NOT default a height (would be wrong
9578
+ // for most layouts).
9579
+ scroll: 'relative w-full overflow-auto rounded-md',
9580
+ table: 'w-full caption-bottom text-left',
9581
+ thead: 'bg-neutral-50 sticky top-0 z-10',
9582
+ tbody: '',
9583
+ headerRow: '',
9584
+ headerCell: [
9585
+ 'text-neutral-700',
9586
+ 'font-semibold whitespace-nowrap select-none',
9587
+ 'border-b border-neutral-200',
9588
+ 'bg-neutral-50'
9589
+ ],
9590
+ headerCellButton: [
9591
+ 'group inline-flex items-center gap-1 select-none',
9592
+ 'hover:text-neutral-900',
9593
+ 'focus:outline-none focus-visible:underline',
9594
+ 'transition-colors motion-reduce:transition-none'
9595
+ ],
9596
+ row: [
9597
+ 'group/row transition-colors motion-reduce:transition-none',
9598
+ 'hover:bg-neutral-100',
9599
+ 'data-[selected=true]:bg-primary-100',
9600
+ 'data-[selected=true]:text-primary-900',
9601
+ 'data-[selected=true]:font-medium'
9602
+ ],
9603
+ cell: [
9604
+ 'text-neutral-900',
9605
+ 'border-b border-neutral-200',
9606
+ 'align-middle'
9607
+ ],
9608
+ selectionCell: 'w-10 text-center',
9609
+ rowActionsCell: [
9610
+ 'w-12 text-right',
9611
+ 'opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100',
9612
+ 'transition-opacity motion-reduce:transition-none'
9613
+ ],
9614
+ emptyState: 'py-12 text-center text-neutral-500',
9615
+ loadingOverlay: 'pointer-events-none',
9616
+ bulkActionFooter: [
9617
+ 'sticky bottom-0 z-20',
9618
+ 'flex flex-wrap items-center justify-between gap-3',
9619
+ 'px-3 py-2',
9620
+ 'border-t border-neutral-200',
9621
+ 'bg-neutral-50 shadow-md'
9622
+ ],
9623
+ // Sticky LEFT column — uses `position: sticky; left: 0` + z-index
9624
+ // so the column stays pinned during horizontal scroll. The
9625
+ // `bg-neutral-50` ensures the column doesn't bleed when content
9626
+ // scrolls underneath.
9627
+ stickyLeftCell: 'sticky left-0 z-[1] bg-neutral-50',
9628
+ // Sticky left × header intersection (top-left corner): higher
9629
+ // z-index than both header (`z-10`) and sticky col (`z-[1]`).
9630
+ stickyLeftHeaderCell: 'sticky left-0 z-20 bg-neutral-50',
9631
+ // Sticky RIGHT column — mirror of stickyLeftCell. The first
9632
+ // right-sticky column gets a `border-l` to separate it visually
9633
+ // from the scrollable area (the consumer can override via
9634
+ // slotProps if they don't want it).
9635
+ stickyRightCell: 'sticky right-0 z-[1] bg-neutral-50 border-l border-neutral-200',
9636
+ stickyRightHeaderCell: 'sticky right-0 z-20 bg-neutral-50 border-l border-neutral-200',
9637
+ paginationFooter: 'flex justify-end mt-3'
9638
+ },
9639
+ variants: {
9640
+ variant: {
9641
+ plain: {},
9642
+ lines: {},
9643
+ striped: {
9644
+ row: 'odd:bg-neutral-100/60'
9645
+ },
9646
+ bordered: {
9647
+ table: 'border border-neutral-200',
9648
+ cell: 'border-r last:border-r-0 border-neutral-200',
9649
+ headerCell: 'border-r last:border-r-0 border-neutral-200'
9650
+ }
9651
+ },
9652
+ size: {
9653
+ sm: {
9654
+ headerCell: 'text-xs',
9655
+ cell: 'text-xs',
9656
+ search: 'text-xs'
9657
+ },
9658
+ md: {
9659
+ headerCell: 'text-sm',
9660
+ cell: 'text-sm',
9661
+ search: 'text-sm'
9662
+ },
9663
+ lg: {
9664
+ headerCell: 'text-base',
9665
+ cell: 'text-base',
9666
+ search: 'text-base'
9667
+ }
9668
+ },
9669
+ density: {
9670
+ compact: {
9671
+ headerCell: 'px-2 py-1',
9672
+ cell: 'px-2 py-1'
9673
+ },
9674
+ comfortable: {
9675
+ headerCell: 'px-3 py-2',
9676
+ cell: 'px-3 py-2'
9677
+ },
9678
+ spacious: {
9679
+ headerCell: 'px-4 py-3',
9680
+ cell: 'px-4 py-3'
9681
+ }
9682
+ }
9683
+ },
9684
+ defaultVariants: {
9685
+ variant: 'lines',
9686
+ size: 'md',
9687
+ density: 'comfortable'
9688
+ }
9689
+ });
9690
+
9691
+ /**
9692
+ * Compute the visible window of rows + the spacer heights given the
9693
+ * current scroll position. Pure function — exported for testability.
9694
+ *
9695
+ * @example
9696
+ * ```ts
9697
+ * computeWindow({ totalCount: 1000, rowHeight: 48, viewportHeight: 600,
9698
+ * scrollTop: 0, overscan: 5 });
9699
+ * // → { startIndex: 0, endIndex: 17, paddingTop: 0, paddingBottom: 47136 }
9700
+ * ```
9701
+ */ function computeWindow(args) {
9702
+ const { totalCount, rowHeight, viewportHeight, scrollTop, overscan = 5 } = args;
9703
+ if (totalCount <= 0 || rowHeight <= 0 || viewportHeight <= 0) {
9704
+ return {
9705
+ startIndex: 0,
9706
+ endIndex: -1,
9707
+ paddingTop: 0,
9708
+ paddingBottom: 0
9709
+ };
9710
+ }
9711
+ // Raw visible range from scrollTop alone.
9712
+ const rawStart = Math.floor(scrollTop / rowHeight);
9713
+ const rawEnd = Math.ceil((scrollTop + viewportHeight) / rowHeight);
9714
+ // Apply overscan + clamp to [0, totalCount - 1].
9715
+ const startIndex = Math.max(0, rawStart - overscan);
9716
+ const endIndex = Math.min(totalCount - 1, rawEnd + overscan);
9717
+ const paddingTop = startIndex * rowHeight;
9718
+ const paddingBottom = Math.max(0, (totalCount - 1 - endIndex) * rowHeight);
9719
+ return {
9720
+ startIndex,
9721
+ endIndex,
9722
+ paddingTop,
9723
+ paddingBottom
9724
+ };
9725
+ }
9726
+ /**
9727
+ * `useVirtualizer` — viewport-based row windowing for `<DataGrid>`.
9728
+ *
9729
+ * Hand-rolled (no `@tanstack/react-virtual` dep — constraint memorized
9730
+ * in `feedback_dashforge_preset_is_identity` companion: "no new
9731
+ * external runtime deps"). Uses scroll event + `requestAnimationFrame`
9732
+ * debounce + `ResizeObserver` on the container. Fixed row height
9733
+ * only in v1 — variable-height with measurement cache deferred to
9734
+ * v1-bis.
9735
+ *
9736
+ * Returns:
9737
+ * - `scrollRef`: attach to the scroll container `<div>`.
9738
+ * - `window`: which row indices to mount + top/bottom spacer heights
9739
+ * to preserve scroll-area size.
9740
+ * - `viewportHeight`: current container height (re-measured on
9741
+ * `ResizeObserver` ticks).
9742
+ *
9743
+ * The DataGrid renders `rows.slice(window.startIndex, window.endIndex + 1)`
9744
+ * between two spacer `<tr style={{ height: paddingTop }} />` elements
9745
+ * (above and below). This preserves `<table>` semantics — screen
9746
+ * readers see a continuous structure with `aria-hidden` spacers.
9747
+ *
9748
+ * @example
9749
+ * ```tsx
9750
+ * const { scrollRef, window } = useVirtualizer({
9751
+ * totalCount: rows.length,
9752
+ * rowHeight: 48,
9753
+ * overscan: 5,
9754
+ * });
9755
+ *
9756
+ * return (
9757
+ * <div ref={scrollRef} style={{ height: 600, overflow: 'auto' }}>
9758
+ * <table>
9759
+ * <tbody>
9760
+ * <tr aria-hidden="true" style={{ height: window.paddingTop }} />
9761
+ * {rows.slice(window.startIndex, window.endIndex + 1).map(...)}
9762
+ * <tr aria-hidden="true" style={{ height: window.paddingBottom }} />
9763
+ * </tbody>
9764
+ * </table>
9765
+ * </div>
9766
+ * );
9767
+ * ```
9768
+ */ function useVirtualizer(args) {
9769
+ const { totalCount, rowHeight, overscan = 5, initialViewportHeight = 400 } = args;
9770
+ const scrollRef = useRef(null);
9771
+ const [scrollTop, setScrollTop] = useState(0);
9772
+ const [viewportHeight, setViewportHeight] = useState(initialViewportHeight);
9773
+ // Debounce scroll handler via requestAnimationFrame so we never
9774
+ // re-render more than once per paint frame even when the scroll
9775
+ // event fires rapidly during fast scrolling.
9776
+ const rafIdRef = useRef(null);
9777
+ const handleScroll = useCallback(()=>{
9778
+ if (rafIdRef.current != null) return;
9779
+ rafIdRef.current = requestAnimationFrame(()=>{
9780
+ rafIdRef.current = null;
9781
+ const el = scrollRef.current;
9782
+ if (!el) return;
9783
+ setScrollTop(el.scrollTop);
9784
+ });
9785
+ }, []);
9786
+ // Attach scroll listener.
9787
+ useEffect(()=>{
9788
+ const el = scrollRef.current;
9789
+ if (!el) return;
9790
+ el.addEventListener('scroll', handleScroll, {
9791
+ passive: true
9792
+ });
9793
+ return ()=>{
9794
+ el.removeEventListener('scroll', handleScroll);
9795
+ if (rafIdRef.current != null) {
9796
+ cancelAnimationFrame(rafIdRef.current);
9797
+ rafIdRef.current = null;
9798
+ }
9799
+ };
9800
+ }, [
9801
+ handleScroll
9802
+ ]);
9803
+ // Track container viewport height via ResizeObserver — re-virtualize
9804
+ // when the container size changes (window resize, sidebar collapse,
9805
+ // etc.). Use `useLayoutEffect` so the first measurement happens
9806
+ // before paint to avoid a flash of stale window.
9807
+ //
9808
+ // **Defensive**: only accept measurements > 0. jsdom (test env)
9809
+ // returns `clientHeight === 0` for unmounted containers, which would
9810
+ // collapse the window to empty. The hook keeps the
9811
+ // `initialViewportHeight` fallback in that case.
9812
+ useLayoutEffect(()=>{
9813
+ const el = scrollRef.current;
9814
+ if (!el) return;
9815
+ if (el.clientHeight > 0) {
9816
+ setViewportHeight(el.clientHeight);
9817
+ }
9818
+ if (typeof ResizeObserver === 'undefined') return;
9819
+ const observer = new ResizeObserver((entries)=>{
9820
+ const entry = entries[0];
9821
+ if (!entry || entry.contentRect.height <= 0) return;
9822
+ setViewportHeight(entry.contentRect.height);
9823
+ });
9824
+ observer.observe(el);
9825
+ return ()=>observer.disconnect();
9826
+ }, []);
9827
+ const window = computeWindow({
9828
+ totalCount,
9829
+ rowHeight,
9830
+ viewportHeight,
9831
+ scrollTop,
9832
+ overscan
9833
+ });
9834
+ return {
9835
+ scrollRef,
9836
+ window,
9837
+ viewportHeight
9838
+ };
9839
+ }
9840
+
9841
+ /**
9842
+ * Per-column filter UI for DataGrid.
9843
+ *
9844
+ * `<ColumnFilterTrigger>` renders the filter icon button in the
9845
+ * column header — when clicked, opens a Popover with the
9846
+ * appropriate filter UI for the column type:
9847
+ *
9848
+ * - `text` → `<TextFilter>` with single input (op: `contains`)
9849
+ * - `number` → `<NumberRangeFilter>` with min/max inputs (op: `between`)
9850
+ * - `boolean` → `<BooleanFilter>` with true/false/all radio (op: `equals`)
9851
+ * - `date` → `<DateRangeFilter>` with min/max date inputs (op: `between`)
9852
+ *
9853
+ * Each filter UI has Apply + Clear actions; Apply commits the
9854
+ * filter to the model, Clear removes any existing filter for that
9855
+ * column.
9856
+ */ // ───── Helper: read/write filter model for a single column ─────
9857
+ function findFilter(model, field) {
9858
+ return model.find((f)=>f.field === field);
9859
+ }
9860
+ function setFilter(model, field, next) {
9861
+ const rest = model.filter((f)=>f.field !== field);
9862
+ if (next == null) return rest;
9863
+ return [
9864
+ ...rest,
9865
+ next
9866
+ ];
9867
+ }
9868
+ const DEFAULT_FILTER_LABELS = {
9869
+ filterColumn: 'Filter',
9870
+ filterApply: 'Apply',
9871
+ filterClear: 'Clear',
9872
+ filterMin: 'Min',
9873
+ filterMax: 'Max',
9874
+ filterFrom: 'From',
9875
+ filterTo: 'To',
9876
+ filterAll: 'All',
9877
+ filterTrue: 'True',
9878
+ filterFalse: 'False'
9879
+ };
9880
+ function ColumnFilterTrigger(props) {
9881
+ const { field, filterType, filterModel, onFilterChange, disabled, labels: labelsProp } = props;
9882
+ const labels = _extends({}, DEFAULT_FILTER_LABELS, labelsProp);
9883
+ const [open, setOpen] = useState(false);
9884
+ const active = findFilter(filterModel, field) != null;
9885
+ return jsx(Popover, {
9886
+ open: open,
9887
+ onOpenChange: setOpen,
9888
+ side: "bottom",
9889
+ align: "end",
9890
+ content: jsx(FilterPanel, {
9891
+ field: field,
9892
+ filterType: filterType,
9893
+ filterModel: filterModel,
9894
+ onFilterChange: (next)=>{
9895
+ onFilterChange(next);
9896
+ setOpen(false);
9897
+ },
9898
+ labels: labels
9899
+ }),
9900
+ children: jsx("button", {
9901
+ type: "button",
9902
+ "aria-label": labels.filterColumn,
9903
+ "aria-pressed": active,
9904
+ disabled: disabled,
9905
+ className: cn('inline-flex items-center justify-center h-6 w-6 rounded', 'text-neutral-500 hover:text-neutral-900', active && 'text-primary-700', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', 'transition-colors motion-reduce:transition-none', 'disabled:opacity-50 disabled:cursor-not-allowed'),
9906
+ children: jsx(FilterIcon, {})
9907
+ })
9908
+ });
9909
+ }
9910
+ // ───── Panel — dispatches to the right filter UI ─────
9911
+ function FilterPanel(props) {
9912
+ const { field, filterType, filterModel, onFilterChange, labels } = props;
9913
+ const current = findFilter(filterModel, field);
9914
+ const commit = (next)=>{
9915
+ onFilterChange(setFilter(filterModel, field, next));
9916
+ };
9917
+ switch(filterType){
9918
+ case 'number':
9919
+ return jsx(NumberRangeFilter, {
9920
+ field: field,
9921
+ current: current,
9922
+ onCommit: commit,
9923
+ labels: labels
9924
+ });
9925
+ case 'boolean':
9926
+ return jsx(BooleanFilter, {
9927
+ field: field,
9928
+ current: current,
9929
+ onCommit: commit,
9930
+ labels: labels
9931
+ });
9932
+ case 'date':
9933
+ return jsx(DateRangeFilter, {
9934
+ field: field,
9935
+ current: current,
9936
+ onCommit: commit,
9937
+ labels: labels
9938
+ });
9939
+ case 'text':
9940
+ default:
9941
+ return jsx(TextFilter, {
9942
+ field: field,
9943
+ current: current,
9944
+ onCommit: commit,
9945
+ labels: labels
9946
+ });
9947
+ }
9948
+ }
9949
+ // ───── Text filter ─────
9950
+ function TextFilter(props) {
9951
+ const { field, current, onCommit, labels } = props;
9952
+ const [value, setValue] = useState(typeof (current == null ? void 0 : current.value) === 'string' ? current.value : '');
9953
+ return jsxs("div", {
9954
+ className: "flex flex-col gap-2 min-w-[200px]",
9955
+ children: [
9956
+ jsx("input", {
9957
+ type: "text",
9958
+ value: value,
9959
+ onChange: (e)=>setValue(e.target.value),
9960
+ placeholder: "\u2026",
9961
+ autoFocus: true,
9962
+ className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
9963
+ }),
9964
+ jsx(FilterActions, {
9965
+ onApply: ()=>onCommit(value.trim() ? {
9966
+ field,
9967
+ op: 'contains',
9968
+ value
9969
+ } : null),
9970
+ onClear: ()=>{
9971
+ setValue('');
9972
+ onCommit(null);
9973
+ },
9974
+ labels: labels
9975
+ })
9976
+ ]
9977
+ });
9978
+ }
9979
+ // ───── Number range filter ─────
9980
+ function NumberRangeFilter(props) {
9981
+ const { field, current, onCommit, labels } = props;
9982
+ const currentValue = current == null ? void 0 : current.value;
9983
+ const currentTuple = Array.isArray(currentValue) ? currentValue : [
9984
+ null,
9985
+ null
9986
+ ];
9987
+ const [min, setMin] = useState(currentTuple[0] != null ? String(currentTuple[0]) : '');
9988
+ const [max, setMax] = useState(currentTuple[1] != null ? String(currentTuple[1]) : '');
9989
+ const apply = ()=>{
9990
+ const minN = min.trim() ? Number(min) : null;
9991
+ const maxN = max.trim() ? Number(max) : null;
9992
+ if ((minN == null || Number.isFinite(minN)) && (maxN == null || Number.isFinite(maxN))) {
9993
+ if (minN == null && maxN == null) {
9994
+ onCommit(null);
9995
+ } else {
9996
+ onCommit({
9997
+ field,
9998
+ op: 'between',
9999
+ value: [
10000
+ minN,
10001
+ maxN
10002
+ ]
10003
+ });
10004
+ }
10005
+ }
10006
+ };
10007
+ return jsxs("div", {
10008
+ className: "flex flex-col gap-2 min-w-[220px]",
10009
+ children: [
10010
+ jsxs("div", {
10011
+ className: "grid grid-cols-2 gap-2",
10012
+ children: [
10013
+ jsxs("label", {
10014
+ className: "flex flex-col gap-1 text-xs text-neutral-700",
10015
+ children: [
10016
+ jsx("span", {
10017
+ children: labels.filterMin
10018
+ }),
10019
+ jsx("input", {
10020
+ type: "number",
10021
+ value: min,
10022
+ onChange: (e)=>setMin(e.target.value),
10023
+ autoFocus: true,
10024
+ className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
10025
+ })
10026
+ ]
10027
+ }),
10028
+ jsxs("label", {
10029
+ className: "flex flex-col gap-1 text-xs text-neutral-700",
10030
+ children: [
10031
+ jsx("span", {
10032
+ children: labels.filterMax
10033
+ }),
10034
+ jsx("input", {
10035
+ type: "number",
10036
+ value: max,
10037
+ onChange: (e)=>setMax(e.target.value),
10038
+ className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
10039
+ })
10040
+ ]
10041
+ })
10042
+ ]
10043
+ }),
10044
+ jsx(FilterActions, {
10045
+ onApply: apply,
10046
+ onClear: ()=>{
10047
+ setMin('');
10048
+ setMax('');
10049
+ onCommit(null);
10050
+ },
10051
+ labels: labels
10052
+ })
10053
+ ]
10054
+ });
10055
+ }
10056
+ // ───── Boolean filter ─────
10057
+ function BooleanFilter(props) {
10058
+ const { field, current, onCommit, labels } = props;
10059
+ const initial = (current == null ? void 0 : current.value) === true ? 'true' : (current == null ? void 0 : current.value) === false ? 'false' : 'all';
10060
+ const [value, setValue] = useState(initial);
10061
+ const apply = ()=>{
10062
+ if (value === 'all') onCommit(null);
10063
+ else onCommit({
10064
+ field,
10065
+ op: 'equals',
10066
+ value: value === 'true'
10067
+ });
10068
+ };
10069
+ return jsxs("div", {
10070
+ className: "flex flex-col gap-2 min-w-[160px]",
10071
+ children: [
10072
+ jsx("fieldset", {
10073
+ className: "flex flex-col gap-1",
10074
+ children: [
10075
+ 'all',
10076
+ 'true',
10077
+ 'false'
10078
+ ].map((opt)=>jsxs("label", {
10079
+ className: "flex items-center gap-2 text-sm cursor-pointer",
10080
+ children: [
10081
+ jsx("input", {
10082
+ type: "radio",
10083
+ name: `filter-${field}`,
10084
+ value: opt,
10085
+ checked: value === opt,
10086
+ onChange: ()=>setValue(opt),
10087
+ className: "cursor-pointer"
10088
+ }),
10089
+ jsx("span", {
10090
+ children: opt === 'all' ? labels.filterAll : opt === 'true' ? labels.filterTrue : labels.filterFalse
10091
+ })
10092
+ ]
10093
+ }, opt))
10094
+ }),
10095
+ jsx(FilterActions, {
10096
+ onApply: apply,
10097
+ onClear: ()=>{
10098
+ setValue('all');
10099
+ onCommit(null);
10100
+ },
10101
+ labels: labels
10102
+ })
10103
+ ]
10104
+ });
10105
+ }
10106
+ // ───── Date range filter ─────
10107
+ function DateRangeFilter(props) {
10108
+ const { field, current, onCommit, labels } = props;
10109
+ const currentValue = current == null ? void 0 : current.value;
10110
+ const currentTuple = Array.isArray(currentValue) ? currentValue : [
10111
+ null,
10112
+ null
10113
+ ];
10114
+ const [from, setFrom] = useState(typeof currentTuple[0] === 'string' ? currentTuple[0] : '');
10115
+ const [to, setTo] = useState(typeof currentTuple[1] === 'string' ? currentTuple[1] : '');
10116
+ const apply = ()=>{
10117
+ if (!from && !to) {
10118
+ onCommit(null);
10119
+ } else {
10120
+ onCommit({
10121
+ field,
10122
+ op: 'between',
10123
+ value: [
10124
+ from || null,
10125
+ to || null
10126
+ ]
10127
+ });
10128
+ }
10129
+ };
10130
+ return jsxs("div", {
10131
+ className: "flex flex-col gap-2 min-w-[220px]",
10132
+ children: [
10133
+ jsxs("div", {
10134
+ className: "grid grid-cols-2 gap-2",
10135
+ children: [
10136
+ jsxs("label", {
10137
+ className: "flex flex-col gap-1 text-xs text-neutral-700",
10138
+ children: [
10139
+ jsx("span", {
10140
+ children: labels.filterFrom
10141
+ }),
10142
+ jsx("input", {
10143
+ type: "date",
10144
+ value: from,
10145
+ onChange: (e)=>setFrom(e.target.value),
10146
+ autoFocus: true,
10147
+ className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
10148
+ })
10149
+ ]
10150
+ }),
10151
+ jsxs("label", {
10152
+ className: "flex flex-col gap-1 text-xs text-neutral-700",
10153
+ children: [
10154
+ jsx("span", {
10155
+ children: labels.filterTo
10156
+ }),
10157
+ jsx("input", {
10158
+ type: "date",
10159
+ value: to,
10160
+ onChange: (e)=>setTo(e.target.value),
10161
+ className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
10162
+ })
10163
+ ]
10164
+ })
10165
+ ]
10166
+ }),
10167
+ jsx(FilterActions, {
10168
+ onApply: apply,
10169
+ onClear: ()=>{
10170
+ setFrom('');
10171
+ setTo('');
10172
+ onCommit(null);
10173
+ },
10174
+ labels: labels
10175
+ })
10176
+ ]
10177
+ });
10178
+ }
10179
+ // ───── Apply + Clear actions ─────
10180
+ function FilterActions(props) {
10181
+ return jsxs("div", {
10182
+ className: "flex justify-end gap-2 pt-1 border-t border-neutral-200",
10183
+ children: [
10184
+ jsx("button", {
10185
+ type: "button",
10186
+ onClick: props.onClear,
10187
+ className: "text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline px-2 py-1",
10188
+ children: props.labels.filterClear
10189
+ }),
10190
+ jsx("button", {
10191
+ type: "button",
10192
+ onClick: props.onApply,
10193
+ className: "text-xs bg-primary-500 text-white rounded-md hover:bg-primary-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 px-3 py-1",
10194
+ children: props.labels.filterApply
10195
+ })
10196
+ ]
10197
+ });
10198
+ }
10199
+ // ───── Icon ─────
10200
+ function FilterIcon() {
10201
+ return jsx("svg", {
10202
+ width: "0.875em",
10203
+ height: "0.875em",
10204
+ viewBox: "0 0 16 16",
10205
+ fill: "none",
10206
+ stroke: "currentColor",
10207
+ strokeWidth: "1.5",
10208
+ strokeLinecap: "round",
10209
+ strokeLinejoin: "round",
10210
+ "aria-hidden": "true",
10211
+ children: jsx("path", {
10212
+ d: "M2 3h12M4 8h8M6 13h4"
10213
+ })
10214
+ });
10215
+ }
10216
+
10217
+ const DEFAULT_VISIBILITY_LABELS = {
10218
+ columnsButton: 'Columns',
10219
+ columnsTitle: 'Manage columns',
10220
+ columnsDescription: 'Show or hide columns. Required columns cannot be toggled.',
10221
+ columnsShowAll: 'Show all',
10222
+ columnsHideAll: 'Hide all',
10223
+ columnsDone: 'Done'
10224
+ };
10225
+ function ColumnVisibilityTrigger(props) {
10226
+ const { cols, hiddenColumns, onChange, disabled, labels: labelsProp } = props;
10227
+ const labels = _extends({}, DEFAULT_VISIBILITY_LABELS, labelsProp);
10228
+ const [open, setOpen] = useState(false);
10229
+ // Hideable cols are the only ones the user can toggle. `hideable`
10230
+ // defaults to `true` — a column is hideable unless explicitly opted
10231
+ // out with `hideable: false` (structurally required column).
10232
+ const hideableCols = cols.filter((c)=>c.hideable !== false);
10233
+ return jsxs(Fragment$1, {
10234
+ children: [
10235
+ jsxs("button", {
10236
+ type: "button",
10237
+ onClick: ()=>setOpen(true),
10238
+ disabled: disabled,
10239
+ className: cn('inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-md', 'text-sm text-neutral-700 hover:text-neutral-900', 'border border-neutral-300 bg-neutral-50 hover:bg-neutral-100', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', 'transition-colors motion-reduce:transition-none', 'disabled:opacity-50 disabled:cursor-not-allowed'),
10240
+ children: [
10241
+ jsx(ColumnsIcon, {}),
10242
+ jsx("span", {
10243
+ children: labels.columnsButton
10244
+ })
10245
+ ]
10246
+ }),
10247
+ jsx(Dialog, {
10248
+ open: open,
10249
+ onOpenChange: setOpen,
10250
+ title: labels.columnsTitle,
10251
+ description: labels.columnsDescription,
10252
+ size: "sm",
10253
+ children: jsx(ColumnVisibilityBody, {
10254
+ cols: hideableCols,
10255
+ hiddenColumns: hiddenColumns,
10256
+ onChange: onChange,
10257
+ onDone: ()=>setOpen(false),
10258
+ labels: labels
10259
+ })
10260
+ })
10261
+ ]
10262
+ });
10263
+ }
10264
+ // ───── Dialog body ─────
10265
+ function ColumnVisibilityBody(props) {
10266
+ const { cols, hiddenColumns, onChange, onDone, labels } = props;
10267
+ // Local draft so the user can stage changes; commit on Done.
10268
+ const [draft, setDraft] = useState(hiddenColumns);
10269
+ // Re-sync when the dialog re-opens with a different external state.
10270
+ useEffect(()=>{
10271
+ setDraft(hiddenColumns);
10272
+ }, [
10273
+ hiddenColumns
10274
+ ]);
10275
+ const toggle = (field)=>{
10276
+ setDraft((prev)=>prev.includes(field) ? prev.filter((f)=>f !== field) : [
10277
+ ...prev,
10278
+ field
10279
+ ]);
10280
+ };
10281
+ const showAll = ()=>setDraft([]);
10282
+ const hideAll = ()=>setDraft(cols.map((c)=>c.field));
10283
+ return jsxs("div", {
10284
+ className: "flex flex-col gap-3",
10285
+ children: [
10286
+ jsxs("div", {
10287
+ className: "flex items-center gap-2 pb-2 border-b border-neutral-200",
10288
+ children: [
10289
+ jsx("button", {
10290
+ type: "button",
10291
+ onClick: showAll,
10292
+ className: "text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline",
10293
+ children: labels.columnsShowAll
10294
+ }),
10295
+ jsx("span", {
10296
+ className: "text-neutral-300",
10297
+ children: "\u00B7"
10298
+ }),
10299
+ jsx("button", {
10300
+ type: "button",
10301
+ onClick: hideAll,
10302
+ className: "text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline",
10303
+ children: labels.columnsHideAll
10304
+ })
10305
+ ]
10306
+ }),
10307
+ jsx("ul", {
10308
+ className: "flex flex-col gap-1.5 max-h-72 overflow-auto",
10309
+ children: cols.map((col)=>{
10310
+ const field = col.field;
10311
+ const hidden = draft.includes(field);
10312
+ const header = typeof col.header === 'function' ? col.header() : col.header;
10313
+ return jsx("li", {
10314
+ children: jsxs("label", {
10315
+ className: "flex items-center gap-2 cursor-pointer py-1 px-1 rounded hover:bg-neutral-100",
10316
+ children: [
10317
+ jsx("input", {
10318
+ type: "checkbox",
10319
+ checked: !hidden,
10320
+ onChange: ()=>toggle(field),
10321
+ className: "cursor-pointer"
10322
+ }),
10323
+ jsx("span", {
10324
+ className: "text-sm text-neutral-900",
10325
+ children: header
10326
+ })
10327
+ ]
10328
+ })
10329
+ }, field);
10330
+ })
10331
+ }),
10332
+ jsx("div", {
10333
+ className: "flex justify-end gap-2 pt-2 border-t border-neutral-200",
10334
+ children: jsx("button", {
10335
+ type: "button",
10336
+ onClick: ()=>{
10337
+ onChange(draft);
10338
+ onDone();
10339
+ },
10340
+ className: "text-sm bg-primary-500 text-white rounded-md hover:bg-primary-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 px-3 py-1.5",
10341
+ children: labels.columnsDone
10342
+ })
10343
+ })
10344
+ ]
10345
+ });
10346
+ }
10347
+ // ───── Icon ─────
10348
+ function ColumnsIcon() {
10349
+ return jsxs("svg", {
10350
+ width: "0.875em",
10351
+ height: "0.875em",
10352
+ viewBox: "0 0 16 16",
10353
+ fill: "none",
10354
+ stroke: "currentColor",
10355
+ strokeWidth: "1.5",
10356
+ strokeLinecap: "round",
10357
+ strokeLinejoin: "round",
10358
+ "aria-hidden": "true",
10359
+ children: [
10360
+ jsx("rect", {
10361
+ x: "2",
10362
+ y: "3",
10363
+ width: "3.5",
10364
+ height: "10",
10365
+ rx: "0.5"
10366
+ }),
10367
+ jsx("rect", {
10368
+ x: "6.25",
10369
+ y: "3",
10370
+ width: "3.5",
10371
+ height: "10",
10372
+ rx: "0.5"
10373
+ }),
10374
+ jsx("rect", {
10375
+ x: "10.5",
10376
+ y: "3",
10377
+ width: "3.5",
10378
+ height: "10",
10379
+ rx: "0.5"
10380
+ })
10381
+ ]
10382
+ });
10383
+ }
10384
+
10385
+ function useColumnResize(args) {
10386
+ const { widths, onChange, minWidth = 40, maxWidth = 1200 } = args;
10387
+ // We need the LATEST widths inside the pointermove closure. Stick
10388
+ // them in a ref so the closure captured at pointerdown stays valid
10389
+ // for the duration of the drag.
10390
+ const widthsRef = useRef(widths);
10391
+ widthsRef.current = widths;
10392
+ const startResize = useCallback((field, startWidth, clampMin, clampMax)=>{
10393
+ return (e)=>{
10394
+ // Only respond to the primary mouse button (or touch / pen).
10395
+ if (e.pointerType === 'mouse' && e.button !== 0) return;
10396
+ e.preventDefault();
10397
+ e.stopPropagation();
10398
+ const startX = e.clientX;
10399
+ const target = e.currentTarget;
10400
+ try {
10401
+ target.setPointerCapture(e.pointerId);
10402
+ } catch (unused) {
10403
+ // jsdom or older browsers — fine to fall back to global listeners.
10404
+ }
10405
+ const effectiveMin = Math.max(minWidth, clampMin);
10406
+ const effectiveMax = Math.min(maxWidth, clampMax);
10407
+ const handleMove = (ev)=>{
10408
+ const delta = ev.clientX - startX;
10409
+ const next = clamp(startWidth + delta, effectiveMin, effectiveMax);
10410
+ onChange(_extends({}, widthsRef.current, {
10411
+ [field]: next
10412
+ }));
10413
+ };
10414
+ const handleEnd = (ev)=>{
10415
+ try {
10416
+ target.releasePointerCapture(ev.pointerId);
10417
+ } catch (unused) {
10418
+ /* noop */ }
10419
+ target.removeEventListener('pointermove', handleMove);
10420
+ target.removeEventListener('pointerup', handleEnd);
10421
+ target.removeEventListener('pointercancel', handleEnd);
10422
+ };
10423
+ target.addEventListener('pointermove', handleMove);
10424
+ target.addEventListener('pointerup', handleEnd);
10425
+ target.addEventListener('pointercancel', handleEnd);
10426
+ };
10427
+ }, [
10428
+ onChange,
10429
+ minWidth,
10430
+ maxWidth
10431
+ ]);
10432
+ return {
10433
+ startResize
10434
+ };
10435
+ }
10436
+ function clamp(n, min, max) {
10437
+ if (n < min) return min;
10438
+ if (n > max) return max;
10439
+ return n;
10440
+ }
10441
+
10442
+ function useColumnReorder(args) {
10443
+ const { order, onChange } = args;
10444
+ const sourceRef = useRef(null);
10445
+ const [dropTarget, setDropTarget] = useState(null);
10446
+ const [draggingField, setDraggingField] = useState(null);
10447
+ const dragStart = useCallback((field)=>(e)=>{
10448
+ sourceRef.current = field;
10449
+ setDraggingField(field);
10450
+ try {
10451
+ e.dataTransfer.effectAllowed = 'move';
10452
+ // Some browsers refuse to start a drag if no data is set.
10453
+ e.dataTransfer.setData('text/plain', field);
10454
+ } catch (unused) {
10455
+ /* noop — jsdom doesn't implement dataTransfer fully. */ }
10456
+ }, []);
10457
+ const dragOver = useCallback((field)=>(e)=>{
10458
+ if (sourceRef.current == null) return;
10459
+ if (field === sourceRef.current) return;
10460
+ e.preventDefault();
10461
+ e.dataTransfer.dropEffect = 'move';
10462
+ // Determine LEFT vs RIGHT half of the target.
10463
+ const rect = e.currentTarget.getBoundingClientRect();
10464
+ const midpoint = rect.left + rect.width / 2;
10465
+ const side = e.clientX < midpoint ? 'left' : 'right';
10466
+ setDropTarget((prev)=>prev && prev.field === field && prev.side === side ? prev : {
10467
+ field,
10468
+ side
10469
+ });
10470
+ }, []);
10471
+ const drop = useCallback((field)=>(e)=>{
10472
+ e.preventDefault();
10473
+ const source = sourceRef.current;
10474
+ sourceRef.current = null;
10475
+ setDraggingField(null);
10476
+ const target = dropTarget;
10477
+ setDropTarget(null);
10478
+ if (!source || source === field) return;
10479
+ const side = (target == null ? void 0 : target.field) === field ? target.side : 'right';
10480
+ const next = moveColumn(order, source, field, side);
10481
+ if (!arraysEqual(next, order)) onChange(next);
10482
+ }, [
10483
+ order,
10484
+ onChange,
10485
+ dropTarget
10486
+ ]);
10487
+ const dragEnd = useCallback(()=>{
10488
+ sourceRef.current = null;
10489
+ setDraggingField(null);
10490
+ setDropTarget(null);
10491
+ }, []);
10492
+ return {
10493
+ dropTarget,
10494
+ draggingField,
10495
+ dragStart,
10496
+ dragOver,
10497
+ drop,
10498
+ dragEnd
10499
+ };
10500
+ }
10501
+ /**
10502
+ * Move `source` next to `target` in the `order` list.
10503
+ * `side='left'` inserts before; `side='right'` inserts after.
10504
+ * Exposed for unit testing.
10505
+ */ function moveColumn(order, source, target, side) {
10506
+ const withoutSource = order.filter((f)=>f !== source);
10507
+ const targetIndex = withoutSource.indexOf(target);
10508
+ if (targetIndex === -1) return order;
10509
+ const insertAt = side === 'left' ? targetIndex : targetIndex + 1;
10510
+ return [
10511
+ ...withoutSource.slice(0, insertAt),
10512
+ source,
10513
+ ...withoutSource.slice(insertAt)
10514
+ ];
10515
+ }
10516
+ function arraysEqual(a, b) {
10517
+ if (a === b) return true;
10518
+ if (a.length !== b.length) return false;
10519
+ for(let i = 0; i < a.length; i++){
10520
+ if (a[i] !== b[i]) return false;
10521
+ }
10522
+ return true;
10523
+ }
10524
+
10525
+ const DEFAULT_LABELS = {
10526
+ searchPlaceholder: 'Search...',
10527
+ noData: 'No data',
10528
+ loading: 'Loading…',
10529
+ ariaSortNone: 'Not sorted. Click to sort ascending.',
10530
+ ariaSortAscending: 'Sorted ascending. Click to sort descending.',
10531
+ ariaSortDescending: 'Sorted descending. Click to clear sort.',
10532
+ ariaSelectRow: 'Select row',
10533
+ ariaSelectAllRows: 'Select all rows',
10534
+ ariaExpandRow: 'Expand row',
10535
+ ariaCollapseRow: 'Collapse row',
10536
+ selectedCount: '{count} selected',
10537
+ clearSelection: 'Clear selection',
10538
+ filterColumn: 'Filter',
10539
+ filterApply: 'Apply',
10540
+ filterClear: 'Clear',
10541
+ filterMin: 'Min',
10542
+ filterMax: 'Max',
10543
+ filterFrom: 'From',
10544
+ filterTo: 'To',
10545
+ filterAll: 'All',
10546
+ filterTrue: 'True',
10547
+ filterFalse: 'False',
10548
+ density: 'Density',
10549
+ densityCompact: 'Compact',
10550
+ densityComfortable: 'Comfortable',
10551
+ densitySpacious: 'Spacious',
10552
+ columnsButton: 'Columns',
10553
+ columnsTitle: 'Manage columns',
10554
+ columnsDescription: 'Show or hide columns. Required columns cannot be toggled.',
10555
+ columnsShowAll: 'Show all',
10556
+ columnsHideAll: 'Hide all',
10557
+ columnsDone: 'Done'
10558
+ };
10559
+ /**
10560
+ * Dashforge TW `<DataGrid>` — virtualized data table.
10561
+ *
10562
+ * Companion to `<Table>` for large data sets (~500+ rows up to
10563
+ * millions via virtualization). Same column model, sort/search/
10564
+ * filter/selection logic (shared via `_shared/data/` helpers) —
10565
+ * differs in render path: only the visible window of rows is mounted
10566
+ * in DOM, with spacer `<tr>` above and below to preserve scrollbar
10567
+ * size.
10568
+ *
10569
+ * Virtualization: hand-rolled (`useVirtualizer`) using
10570
+ * `IntersectionObserver` + scroll-event + `requestAnimationFrame`
10571
+ * debounce + `ResizeObserver`. Zero new runtime deps.
10572
+ *
10573
+ * Server-side mode (4 independent opt-in flags):
10574
+ * - `serverSideSort` — emits `onSortChange` but does NOT sort locally
10575
+ * - `serverSideFilter` — emits `onFilterChange` but does NOT filter
10576
+ * - `serverSideSearch` — emits `onSearchQueryChange` (debounced)
10577
+ * - `serverSidePagination` — `totalCount` prop sizes the virtual
10578
+ * scrollbar; `rows` is the page slice from the server
10579
+ *
10580
+ * Selection at scale: `selectAllScope`:
10581
+ * - `'allLoaded'` (default) — header checkbox toggles all rows in
10582
+ * `rows` prop
10583
+ * - `'visible'` — toggles only the current viewport window
10584
+ */ function DataGrid(props) {
10585
+ 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;
10586
+ 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, hiddenColumns: hiddenColumnsProp, onHiddenColumnsChange, enableColumnVisibility = true, columnWidths: columnWidthsProp, onColumnWidthsChange, enableColumnResize = true, columnOrder: columnOrderProp, onColumnOrderChange, enableColumnReorder = true, 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;
10587
+ const labels = _extends({}, DEFAULT_LABELS, labelsProp);
10588
+ const gridAccessState = useAccessState(gridAccess);
10589
+ // ───── Controllable state ─────
10590
+ const [sortModel, setSortModel] = useControllableState({
10591
+ controlledValue: sortModelProp,
10592
+ defaultValue: defaultSortModel != null ? defaultSortModel : [],
10593
+ onChange: onSortChange
10594
+ });
10595
+ const [searchQuery, setSearchQuery] = useControllableState({
10596
+ controlledValue: searchQueryProp,
10597
+ defaultValue: '',
10598
+ onChange: onSearchQueryChange
10599
+ });
10600
+ // filterModel: in v1-bis the per-column header UI commits filter
10601
+ // items to this controllable state. Consumer can lift it via
10602
+ // filterModelProp + onFilterChange to drive server-side filtering.
10603
+ const [filterModel, setFilterModel] = useControllableState({
10604
+ controlledValue: filterModelProp,
10605
+ defaultValue: [],
10606
+ onChange: onFilterChange
10607
+ });
10608
+ const [selectedRowIds, setSelectedRowIds] = useControllableState({
10609
+ controlledValue: selectedRowIdsProp,
10610
+ defaultValue: [],
10611
+ onChange: onSelectionChange
10612
+ });
10613
+ // ───── Column visibility ─────
10614
+ // Seed default hidden set from `defaultHidden` flag on columns.
10615
+ // Memoized to avoid the seed shifting on every render — the
10616
+ // controllable state keeps internal state stable thereafter.
10617
+ const defaultHiddenColumns = useMemo(()=>cols.filter((c)=>c.defaultHidden === true).map((c)=>c.field), [
10618
+ cols
10619
+ ]);
10620
+ const [hiddenColumns, setHiddenColumns] = useControllableState({
10621
+ controlledValue: hiddenColumnsProp,
10622
+ defaultValue: defaultHiddenColumns,
10623
+ onChange: onHiddenColumnsChange
10624
+ });
10625
+ // ───── Column widths (resize state) ─────
10626
+ // Seed from any `col.width` declared statically. Internal state is
10627
+ // applied only when the user actually starts dragging; until then
10628
+ // we fall back to the static width from the column def.
10629
+ const defaultColumnWidths = useMemo(()=>{
10630
+ const map = {};
10631
+ for (const c of cols){
10632
+ if (typeof c.width === 'number') {
10633
+ map[c.field] = c.width;
10634
+ }
10635
+ }
10636
+ return map;
10637
+ }, [
10638
+ cols
10639
+ ]);
10640
+ const [columnWidths, setColumnWidths] = useControllableState({
10641
+ controlledValue: columnWidthsProp,
10642
+ defaultValue: defaultColumnWidths,
10643
+ onChange: onColumnWidthsChange
10644
+ });
10645
+ const { startResize } = useColumnResize({
10646
+ widths: columnWidths,
10647
+ onChange: setColumnWidths
10648
+ });
10649
+ // ───── Column order (reorder state) ─────
10650
+ const defaultColumnOrder = useMemo(()=>cols.map((c)=>c.field), [
10651
+ cols
10652
+ ]);
10653
+ const [columnOrder, setColumnOrder] = useControllableState({
10654
+ controlledValue: columnOrderProp,
10655
+ defaultValue: defaultColumnOrder,
10656
+ onChange: onColumnOrderChange
10657
+ });
10658
+ const reorderHandlers = useColumnReorder({
10659
+ order: columnOrder,
10660
+ onChange: setColumnOrder
10661
+ });
10662
+ // Apply the active order to the columns list. Cols not in the
10663
+ // order array (e.g. newly-added columns at runtime) are appended
10664
+ // in their original position from `cols`.
10665
+ const orderedCols = useMemo(()=>{
10666
+ if (!columnOrder || columnOrder.length === 0) return cols;
10667
+ const byField = new Map(cols.map((c)=>[
10668
+ c.field,
10669
+ c
10670
+ ]));
10671
+ const seen = new Set();
10672
+ const result = [];
10673
+ for (const field of columnOrder){
10674
+ const c = byField.get(field);
10675
+ if (c) {
10676
+ result.push(c);
10677
+ seen.add(field);
10678
+ }
10679
+ }
10680
+ for (const c of cols){
10681
+ if (!seen.has(c.field)) result.push(c);
10682
+ }
10683
+ return result;
10684
+ }, [
10685
+ cols,
10686
+ columnOrder
10687
+ ]);
10688
+ // ───── RBAC + visibility + reorder: derive final column list ─────
10689
+ // - `access.onUnauthorized === 'hide'` removes a column entirely.
10690
+ // - `hiddenColumns` is the user-toggled hidden set from the
10691
+ // column-visibility dialog.
10692
+ // - `orderedCols` is the user-reordered version of `cols`.
10693
+ // NOTE: filtering happens on `orderedCols` so the active order is
10694
+ // preserved through visibility changes.
10695
+ const visibleCols = useMemo(()=>orderedCols.filter((col)=>{
10696
+ if (col.access && col.access.onUnauthorized === 'hide') return false;
10697
+ if (hiddenColumns.includes(col.field)) return false;
10698
+ return true;
10699
+ }), [
10700
+ orderedCols,
10701
+ hiddenColumns
10702
+ ]);
10703
+ // ───── Smart-default column types ─────
10704
+ const columnTypes = useColumnAutoDetect(rows, visibleCols);
10705
+ // ───── Pipeline: search → filter → sort (skip when server-side) ─────
10706
+ //
10707
+ // Each pipeline hook is ALWAYS called (React hooks rules). The
10708
+ // server-side flags decide whether to use the local-computed result
10709
+ // or pass the upstream rows through unchanged. The hooks are
10710
+ // `useMemo`-based internally so they're cheap when their inputs
10711
+ // are unchanged.
10712
+ const debouncedSearchQuery = useDebouncedValue(searchQuery, searchDebounceMs);
10713
+ const localSearched = useTableSearch(rows, visibleCols, debouncedSearchQuery);
10714
+ const searchedRows = serverSideSearch ? rows : localSearched;
10715
+ const localFiltered = useTableFilter(searchedRows, filterModel);
10716
+ const filteredRows = serverSideFilter ? searchedRows : localFiltered;
10717
+ const localSorted = useTableSort(filteredRows, visibleCols, sortModel);
10718
+ const sortedRows = serverSideSort ? filteredRows : localSorted;
10719
+ // ───── Virtualization ─────
10720
+ // Effective row count for the virtual scrollbar:
10721
+ // - Client-side: actual sorted rows count
10722
+ // - Server-side pagination: caller-provided totalCount
10723
+ const effectiveTotalCount = serverSidePagination ? totalCountProp != null ? totalCountProp : sortedRows.length : sortedRows.length;
10724
+ const initialViewportHeight = height ? parseInt(height, 10) || 400 : 400;
10725
+ const { scrollRef, window: vWindow } = useVirtualizer({
10726
+ totalCount: effectiveTotalCount,
10727
+ rowHeight,
10728
+ overscan,
10729
+ initialViewportHeight
10730
+ });
10731
+ // The rows actually mounted in DOM.
10732
+ const renderedRows = useMemo(()=>{
10733
+ if (serverSidePagination) {
10734
+ // In server-side pagination, `rows` IS the page slice; we
10735
+ // render all of it (virtualization within the page is still
10736
+ // useful for very large page sizes).
10737
+ return rows.slice(vWindow.startIndex, vWindow.endIndex + 1);
10738
+ }
10739
+ return sortedRows.slice(vWindow.startIndex, vWindow.endIndex + 1);
10740
+ }, [
10741
+ serverSidePagination,
10742
+ rows,
10743
+ sortedRows,
10744
+ vWindow.startIndex,
10745
+ vWindow.endIndex
10746
+ ]);
10747
+ // ───── Selection helpers ─────
10748
+ // For `selectAllScope='visible'`, the select-all scope is the
10749
+ // current window. For `'allLoaded'`, it's the whole sorted set.
10750
+ const selectAllPool = useMemo(()=>{
10751
+ if (selectAllScope === 'visible') {
10752
+ return renderedRows.map((row, idx)=>getRowId(row, vWindow.startIndex + idx));
10753
+ }
10754
+ return sortedRows.map((row, idx)=>getRowId(row, idx));
10755
+ }, [
10756
+ selectAllScope,
10757
+ renderedRows,
10758
+ sortedRows,
10759
+ getRowId,
10760
+ vWindow.startIndex
10761
+ ]);
10762
+ const selection = useTableSelection(rowSelection, selectedRowIds, setSelectedRowIds);
10763
+ const selectedRows = useMemo(()=>sortedRows.filter((row, idx)=>selection.isSelected(getRowId(row, idx))), [
10764
+ sortedRows,
10765
+ selection,
10766
+ getRowId
10767
+ ]);
10768
+ // ───── Variant recipe ─────
10769
+ const v = dataGridVariants({
10770
+ variant,
10771
+ size,
10772
+ density
10773
+ });
10774
+ // ───── Render ─────
10775
+ if (!gridAccessState.visible) return null;
10776
+ const isInteractive = !gridAccessState.disabled;
10777
+ const showSelectionColumn = rowSelection !== 'none';
10778
+ const showRowActionsColumn = rowActions != null;
10779
+ const totalColumnCount = visibleCols.length + (showSelectionColumn ? 1 : 0) + (showRowActionsColumn ? 1 : 0);
10780
+ return jsxs("div", {
10781
+ className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
10782
+ "data-disabled": !isInteractive ? 'true' : undefined,
10783
+ children: [
10784
+ (enableSearch || enableColumnVisibility) && jsxs("div", {
10785
+ className: cn(v.toolbar(), slotProps == null ? void 0 : (_slotProps_toolbar = slotProps.toolbar) == null ? void 0 : _slotProps_toolbar.className),
10786
+ children: [
10787
+ enableSearch && jsxs("label", {
10788
+ className: cn(v.search(), slotProps == null ? void 0 : (_slotProps_search = slotProps.search) == null ? void 0 : _slotProps_search.className),
10789
+ children: [
10790
+ jsx("span", {
10791
+ className: "sr-only",
10792
+ children: labels.searchPlaceholder
10793
+ }),
10794
+ jsx(SearchIcon, {
10795
+ className: "ml-2 shrink-0 text-neutral-500"
10796
+ }),
10797
+ jsx("input", {
10798
+ type: "text",
10799
+ value: searchQuery,
10800
+ onChange: (e)=>setSearchQuery(e.target.value),
10801
+ placeholder: searchPlaceholder != null ? searchPlaceholder : labels.searchPlaceholder,
10802
+ disabled: !isInteractive,
10803
+ 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')
10804
+ })
10805
+ ]
10806
+ }),
10807
+ enableColumnVisibility && jsx(ColumnVisibilityTrigger, {
10808
+ cols: cols.filter((c)=>// RBAC-hidden columns are never offered in the dialog.
10809
+ !c.access || c.access.onUnauthorized !== 'hide'),
10810
+ hiddenColumns: hiddenColumns,
10811
+ onChange: setHiddenColumns,
10812
+ disabled: !isInteractive,
10813
+ labels: {
10814
+ columnsButton: labels.columnsButton,
10815
+ columnsTitle: labels.columnsTitle,
10816
+ columnsDescription: labels.columnsDescription,
10817
+ columnsShowAll: labels.columnsShowAll,
10818
+ columnsHideAll: labels.columnsHideAll,
10819
+ columnsDone: labels.columnsDone
10820
+ }
10821
+ })
10822
+ ]
10823
+ }),
10824
+ jsx("div", {
10825
+ ref: scrollRef,
10826
+ className: cn(v.scroll(), slotProps == null ? void 0 : (_slotProps_scroll = slotProps.scroll) == null ? void 0 : _slotProps_scroll.className),
10827
+ style: height ? {
10828
+ height,
10829
+ maxHeight: height
10830
+ } : undefined,
10831
+ children: jsxs("table", {
10832
+ className: cn(v.table(), slotProps == null ? void 0 : (_slotProps_table = slotProps.table) == null ? void 0 : _slotProps_table.className),
10833
+ children: [
10834
+ caption != null && jsx("caption", {
10835
+ className: cn(!showCaption && 'sr-only', 'py-2 text-sm text-neutral-500'),
10836
+ children: caption
10837
+ }),
10838
+ jsx("thead", {
10839
+ className: cn(stickyHeader ? v.thead() : 'bg-neutral-50', slotProps == null ? void 0 : (_slotProps_thead = slotProps.thead) == null ? void 0 : _slotProps_thead.className),
10840
+ children: jsxs("tr", {
10841
+ className: cn(v.headerRow(), slotProps == null ? void 0 : (_slotProps_headerRow = slotProps.headerRow) == null ? void 0 : _slotProps_headerRow.className),
10842
+ children: [
10843
+ showSelectionColumn && jsx("th", {
10844
+ scope: "col",
10845
+ 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),
10846
+ children: rowSelection === 'multiple' && jsx("input", {
10847
+ type: "checkbox",
10848
+ "aria-label": labels.ariaSelectAllRows,
10849
+ checked: selection.isAllSelected(selectAllPool),
10850
+ ref: (el)=>{
10851
+ if (el) el.indeterminate = selection.isIndeterminate(selectAllPool);
10852
+ },
10853
+ onChange: ()=>selection.toggleAll(selectAllPool),
10854
+ disabled: !isInteractive,
10855
+ className: "cursor-pointer"
10856
+ })
10857
+ }),
10858
+ visibleCols.map((col)=>{
10859
+ var _slotProps_headerCell;
10860
+ return jsx(HeaderCell, {
10861
+ col: col,
10862
+ sortModel: sortModel,
10863
+ onSortClick: (e)=>handleSortClick(col, e, sortModel, setSortModel),
10864
+ inferredType: columnTypes.get(col.field),
10865
+ filterModel: filterModel,
10866
+ onFilterChange: setFilterModel,
10867
+ effectiveWidth: resolveColumnWidth(col, columnWidths),
10868
+ resizable: enableColumnResize && col.resizable !== false,
10869
+ startResize: startResize,
10870
+ reorderable: enableColumnReorder && col.reorderable !== false,
10871
+ reorderHandlers: reorderHandlers,
10872
+ labels: labels,
10873
+ className: cn(v.headerCell(), col.sticky === 'left' && v.stickyLeftHeaderCell(), col.sticky === 'right' && v.stickyRightHeaderCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className),
10874
+ buttonClassName: v.headerCellButton(),
10875
+ disabled: !isInteractive
10876
+ }, col.field);
10877
+ }),
10878
+ showRowActionsColumn && jsx("th", {
10879
+ scope: "col",
10880
+ "aria-hidden": "true",
10881
+ className: cn(v.headerCell(), 'w-12', slotProps == null ? void 0 : (_slotProps_headerCell1 = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell1.className)
10882
+ })
10883
+ ]
10884
+ })
10885
+ }),
10886
+ jsx("tbody", {
10887
+ className: cn(v.tbody(), slotProps == null ? void 0 : (_slotProps_tbody = slotProps.tbody) == null ? void 0 : _slotProps_tbody.className),
10888
+ children: loading ? renderLoadingRows({
10889
+ count: loadingRowCount,
10890
+ visibleCols,
10891
+ showSelectionColumn,
10892
+ showRowActionsColumn,
10893
+ rowHeight,
10894
+ rowClass: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row = slotProps.row) == null ? void 0 : _slotProps_row.className),
10895
+ cellClass: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell = slotProps.cell) == null ? void 0 : _slotProps_cell.className)
10896
+ }) : sortedRows.length === 0 ? renderEmptyState({
10897
+ totalColumnCount,
10898
+ emptyState,
10899
+ fallback: labels.noData,
10900
+ cellClass: cn(v.cell(), v.emptyState(), slotProps == null ? void 0 : (_slotProps_emptyState = slotProps.emptyState) == null ? void 0 : _slotProps_emptyState.className)
10901
+ }) : renderVirtualizedBody({
10902
+ vWindow,
10903
+ renderedRows,
10904
+ visibleCols,
10905
+ columnTypes,
10906
+ columnWidths,
10907
+ selection,
10908
+ rowSelection,
10909
+ showSelectionColumn,
10910
+ showRowActionsColumn,
10911
+ rowActions,
10912
+ getRowId,
10913
+ isInteractive,
10914
+ labels,
10915
+ classes: {
10916
+ row: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row1 = slotProps.row) == null ? void 0 : _slotProps_row1.className),
10917
+ cell: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell1 = slotProps.cell) == null ? void 0 : _slotProps_cell1.className),
10918
+ 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),
10919
+ stickyLeftCell: v.stickyLeftCell(),
10920
+ stickyRightCell: v.stickyRightCell(),
10921
+ 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)
10922
+ }
10923
+ })
10924
+ })
10925
+ ]
10926
+ })
10927
+ }),
10928
+ bulkActions && selectedRows.length > 0 && jsxs("div", {
10929
+ className: cn(v.bulkActionFooter(), slotProps == null ? void 0 : (_slotProps_bulkActionFooter = slotProps.bulkActionFooter) == null ? void 0 : _slotProps_bulkActionFooter.className),
10930
+ role: "region",
10931
+ "aria-label": labels.selectedCount.replace('{count}', String(selectedRows.length)),
10932
+ children: [
10933
+ jsx("span", {
10934
+ className: "text-sm text-neutral-700",
10935
+ children: labels.selectedCount.replace('{count}', String(selectedRows.length))
10936
+ }),
10937
+ jsxs("div", {
10938
+ className: "flex items-center gap-2",
10939
+ children: [
10940
+ bulkActions(selectedRows),
10941
+ jsx("button", {
10942
+ type: "button",
10943
+ onClick: selection.clearSelection,
10944
+ className: cn('text-sm text-neutral-500 hover:text-neutral-900', 'underline-offset-2 hover:underline', 'focus:outline-none focus-visible:underline'),
10945
+ children: labels.clearSelection
10946
+ })
10947
+ ]
10948
+ })
10949
+ ]
10950
+ }),
10951
+ pagination && jsx("div", {
10952
+ className: cn(v.paginationFooter(), slotProps == null ? void 0 : (_slotProps_pagination = slotProps.pagination) == null ? void 0 : _slotProps_pagination.className),
10953
+ children: jsx(Pagination, {
10954
+ page: pagination.page,
10955
+ pageSize: pagination.pageSize,
10956
+ totalCount: totalCountProp != null ? totalCountProp : sortedRows.length,
10957
+ onPageChange: pagination.onPageChange,
10958
+ onPageSizeChange: pagination.onPageSizeChange,
10959
+ pageSizeOptions: pagination.pageSizeOptions
10960
+ })
10961
+ })
10962
+ ]
10963
+ });
10964
+ }
10965
+ // ───── Header cell ─────
10966
+ function HeaderCell(props) {
10967
+ const { col, sortModel, onSortClick, inferredType, filterModel, onFilterChange, effectiveWidth, resizable, startResize, reorderable, reorderHandlers, labels, className, buttonClassName, disabled } = props;
10968
+ const align = resolveAlign(col, inferredType);
10969
+ const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
10970
+ const sortEntry = sortModel.find((s)=>s.field === col.field);
10971
+ const ariaSort = (sortEntry == null ? void 0 : sortEntry.direction) === 'asc' ? 'ascending' : (sortEntry == null ? void 0 : sortEntry.direction) === 'desc' ? 'descending' : 'none';
10972
+ const headerContent = typeof col.header === 'function' ? col.header() : col.header;
10973
+ const cellStyle = {
10974
+ width: effectiveWidth != null ? `${effectiveWidth}px` : undefined,
10975
+ minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined,
10976
+ flex: effectiveWidth != null ? undefined : col.flex,
10977
+ position: resizable || reorderable ? 'relative' : undefined
10978
+ };
10979
+ // Drop indicator for reorder: a 2px vertical bar at the LEFT or
10980
+ // RIGHT edge of this th when it's the active drop target.
10981
+ const dropTarget = reorderHandlers.dropTarget;
10982
+ const dropIndicatorClass = dropTarget != null && dropTarget.field === col.field ? dropTarget.side === 'left' ? 'before:absolute before:top-0 before:bottom-0 before:left-0 before:w-0.5 before:bg-primary-500' : 'after:absolute after:top-0 after:bottom-0 after:right-0 after:w-0.5 after:bg-primary-500' : '';
10983
+ const isDragging = reorderHandlers.draggingField === col.field;
10984
+ // Drag attributes only attach when reorderable; we DO NOT make the
10985
+ // entire th `draggable` if it isn't — keeps the keyboard focus
10986
+ // behavior unchanged.
10987
+ const dragAttrs = reorderable ? {
10988
+ draggable: true,
10989
+ onDragStart: reorderHandlers.dragStart(col.field),
10990
+ onDragOver: reorderHandlers.dragOver(col.field),
10991
+ onDrop: reorderHandlers.drop(col.field),
10992
+ onDragEnd: reorderHandlers.dragEnd
10993
+ } : {};
10994
+ const resizeHandle = resizable ? jsx("span", {
10995
+ role: "separator",
10996
+ "aria-orientation": "vertical",
10997
+ "aria-label": `Resize ${typeof col.header === 'string' ? col.header : col.field}`,
10998
+ onPointerDown: (e)=>{
10999
+ var _ref, _col_minWidth, _col_maxWidth;
11000
+ const handle = e.currentTarget;
11001
+ const th = handle.parentElement;
11002
+ const startWidth = (_ref = effectiveWidth != null ? effectiveWidth : th == null ? void 0 : th.getBoundingClientRect().width) != null ? _ref : 120;
11003
+ const min = (_col_minWidth = col.minWidth) != null ? _col_minWidth : 40;
11004
+ const max = (_col_maxWidth = col.maxWidth) != null ? _col_maxWidth : 1200;
11005
+ startResize(col.field, startWidth, min, max)(e);
11006
+ },
11007
+ className: cn('absolute top-0 bottom-0 right-0 w-1.5', 'cursor-col-resize select-none touch-none', 'hover:bg-primary-300 active:bg-primary-400', 'transition-colors motion-reduce:transition-none')
11008
+ }) : null;
11009
+ const filterType = col.filterable ? resolveFilterType(col, inferredType) : null;
11010
+ const filterTrigger = col.filterable && filterType ? jsx(ColumnFilterTrigger, {
11011
+ field: col.field,
11012
+ filterType: filterType,
11013
+ filterModel: filterModel,
11014
+ onFilterChange: onFilterChange,
11015
+ disabled: disabled,
11016
+ labels: {
11017
+ filterColumn: labels.filterColumn,
11018
+ filterApply: labels.filterApply,
11019
+ filterClear: labels.filterClear,
11020
+ filterMin: labels.filterMin,
11021
+ filterMax: labels.filterMax,
11022
+ filterFrom: labels.filterFrom,
11023
+ filterTo: labels.filterTo,
11024
+ filterAll: labels.filterAll,
11025
+ filterTrue: labels.filterTrue,
11026
+ filterFalse: labels.filterFalse
11027
+ }
11028
+ }) : null;
11029
+ // Layout strategy:
11030
+ // - sortable only → sort button is the cell content
11031
+ // - filterable only → header text + filter trigger in a flex row
11032
+ // - both → sort button + filter trigger in a flex row
11033
+ // - neither → plain header text
11034
+ const wrapperJustify = align === 'right' ? 'justify-end' : align === 'center' ? 'justify-center' : 'justify-start';
11035
+ if (col.sortable) {
11036
+ const sortButton = jsxs("button", {
11037
+ type: "button",
11038
+ onClick: onSortClick,
11039
+ disabled: disabled,
11040
+ "aria-label": ariaSort === 'ascending' ? labels.ariaSortAscending : ariaSort === 'descending' ? labels.ariaSortDescending : labels.ariaSortNone,
11041
+ className: cn(buttonClassName, align === 'right' && 'flex-row-reverse', 'disabled:cursor-not-allowed disabled:opacity-50'),
11042
+ children: [
11043
+ jsx("span", {
11044
+ children: headerContent
11045
+ }),
11046
+ jsx(SortIcon, {
11047
+ direction: sortEntry == null ? void 0 : sortEntry.direction
11048
+ })
11049
+ ]
11050
+ });
11051
+ return jsxs("th", _extends({
11052
+ scope: "col",
11053
+ "aria-sort": ariaSort,
11054
+ className: cn(className, alignClass, dropIndicatorClass, isDragging && 'opacity-50'),
11055
+ style: cellStyle
11056
+ }, dragAttrs, {
11057
+ children: [
11058
+ filterTrigger ? jsxs("div", {
11059
+ className: cn('flex items-center gap-1', wrapperJustify),
11060
+ children: [
11061
+ sortButton,
11062
+ filterTrigger
11063
+ ]
11064
+ }) : sortButton,
11065
+ resizeHandle
11066
+ ]
11067
+ }));
11068
+ }
11069
+ return jsxs("th", _extends({
11070
+ scope: "col",
11071
+ className: cn(className, alignClass, dropIndicatorClass, isDragging && 'opacity-50'),
11072
+ style: cellStyle
11073
+ }, dragAttrs, {
11074
+ children: [
11075
+ filterTrigger ? jsxs("div", {
11076
+ className: cn('flex items-center gap-1', wrapperJustify),
11077
+ children: [
11078
+ jsx("span", {
11079
+ children: headerContent
11080
+ }),
11081
+ filterTrigger
11082
+ ]
11083
+ }) : headerContent,
11084
+ resizeHandle
11085
+ ]
11086
+ }));
11087
+ }
11088
+ /**
11089
+ * Resolve the effective width (px) for a column. User-driven resize
11090
+ * (`columnWidths[field]`) wins over the static `col.width`. Returns
11091
+ * `undefined` if neither source has a width set — the consumer falls
11092
+ * back to the column's `flex` value or browser auto-layout.
11093
+ */ function resolveColumnWidth(col, columnWidths) {
11094
+ const resized = columnWidths[col.field];
11095
+ if (resized != null) return resized;
11096
+ return col.width;
11097
+ }
11098
+ /**
11099
+ * Resolve the filter UI type for a column. Explicit `col.filterType`
11100
+ * wins; otherwise we map the auto-detected primitive type:
11101
+ * - `number` → number range
11102
+ * - `boolean` → true/false/all select
11103
+ * - `date` → date range
11104
+ * - anything else (string / unknown) → text contains
11105
+ */ function resolveFilterType(col, inferred) {
11106
+ if (col.filterType) return col.filterType;
11107
+ switch(inferred){
11108
+ case 'number':
11109
+ return 'number';
11110
+ case 'boolean':
11111
+ return 'boolean';
11112
+ case 'date':
11113
+ return 'date';
11114
+ default:
11115
+ return 'text';
11116
+ }
11117
+ }
11118
+ // ───── Virtualized body ─────
11119
+ function renderVirtualizedBody(args) {
11120
+ const { vWindow, renderedRows, visibleCols, columnTypes, columnWidths, selection, rowSelection, showSelectionColumn, showRowActionsColumn, rowActions, getRowId, isInteractive, labels, classes } = args;
11121
+ return jsxs(Fragment$1, {
11122
+ children: [
11123
+ vWindow.paddingTop > 0 && jsx("tr", {
11124
+ "aria-hidden": "true",
11125
+ style: {
11126
+ height: vWindow.paddingTop
11127
+ }
11128
+ }),
11129
+ renderedRows.map((row, localIdx)=>{
11130
+ const absoluteIdx = vWindow.startIndex + localIdx;
11131
+ const rowId = getRowId(row, absoluteIdx);
11132
+ const isSelected = selection.isSelected(rowId);
11133
+ return jsxs("tr", {
11134
+ "data-selected": isSelected ? 'true' : 'false',
11135
+ "aria-selected": rowSelection !== 'none' ? isSelected : undefined,
11136
+ className: classes.row,
11137
+ children: [
11138
+ showSelectionColumn && jsx("td", {
11139
+ className: classes.selectionCell,
11140
+ children: jsx("input", {
11141
+ type: "checkbox",
11142
+ "aria-label": labels.ariaSelectRow,
11143
+ checked: isSelected,
11144
+ onChange: ()=>selection.toggleRow(rowId),
11145
+ disabled: !isInteractive,
11146
+ className: "cursor-pointer"
11147
+ })
11148
+ }),
11149
+ visibleCols.map((col)=>jsx(DataCell, {
11150
+ col: col,
11151
+ row: row,
11152
+ rowIndex: absoluteIdx,
11153
+ isSelected: isSelected,
11154
+ inferredType: columnTypes.get(col.field),
11155
+ effectiveWidth: resolveColumnWidth(col, columnWidths),
11156
+ cellClass: classes.cell,
11157
+ stickyLeftClass: classes.stickyLeftCell,
11158
+ stickyRightClass: classes.stickyRightCell
11159
+ }, col.field)),
11160
+ showRowActionsColumn && rowActions && jsx("td", {
11161
+ className: classes.rowActionsCell,
11162
+ children: rowActions(row)
11163
+ })
11164
+ ]
11165
+ }, rowId);
11166
+ }),
11167
+ vWindow.paddingBottom > 0 && jsx("tr", {
11168
+ "aria-hidden": "true",
11169
+ style: {
11170
+ height: vWindow.paddingBottom
11171
+ }
11172
+ })
11173
+ ]
11174
+ });
11175
+ }
11176
+ // ───── Data cell ─────
11177
+ function DataCell(props) {
11178
+ const { col, row, rowIndex, isSelected, inferredType, effectiveWidth, cellClass, stickyLeftClass, stickyRightClass } = props;
11179
+ const value = getNestedValue(row, col.field);
11180
+ const align = resolveAlign(col, inferredType);
11181
+ const tabularNums = resolveTabularNums(col, inferredType);
11182
+ const monospace = resolveMonospace(col);
11183
+ const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
11184
+ const columnAccessState = col.access ? {
11185
+ hidden: col.access.onUnauthorized === 'hide',
11186
+ disabled: col.access.onUnauthorized === 'disable',
11187
+ readonly: col.access.onUnauthorized === 'readonly'
11188
+ } : null;
11189
+ const content = col.cellRenderer ? col.cellRenderer({
11190
+ row,
11191
+ value,
11192
+ rowIndex,
11193
+ isSelected,
11194
+ isExpanded: false,
11195
+ access: columnAccessState
11196
+ }) : renderDefaultCell(value);
11197
+ return jsx("td", {
11198
+ className: cn(cellClass, alignClass, tabularNums && 'tabular-nums', monospace && 'font-mono', col.sticky === 'left' && stickyLeftClass, col.sticky === 'right' && stickyRightClass),
11199
+ style: {
11200
+ width: effectiveWidth != null ? `${effectiveWidth}px` : undefined,
11201
+ minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined
11202
+ },
11203
+ children: content
11204
+ });
11205
+ }
11206
+ // ───── Helpers ─────
11207
+ function renderDefaultCell(value) {
11208
+ if (value == null) return null;
11209
+ if (typeof value === 'boolean') return value ? '✓' : '✗';
11210
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
11211
+ if (typeof value === 'object') return JSON.stringify(value);
11212
+ return value;
11213
+ }
11214
+ function handleSortClick(col, event, current, setSortModel) {
11215
+ if (!col.sortable) return;
11216
+ const multi = event.shiftKey;
11217
+ setSortModel((prev)=>cycleSortFor(prev, col.field, multi));
11218
+ }
11219
+ function renderLoadingRows(args) {
11220
+ const { count, visibleCols, showSelectionColumn, showRowActionsColumn, rowHeight, rowClass, cellClass } = args;
11221
+ return Array.from({
11222
+ length: count
11223
+ }).map((_, i)=>jsxs("tr", {
11224
+ className: rowClass,
11225
+ "aria-busy": "true",
11226
+ style: {
11227
+ height: rowHeight
11228
+ },
11229
+ children: [
11230
+ showSelectionColumn && jsx("td", {
11231
+ className: cellClass,
11232
+ children: jsx(Skeleton, {
11233
+ variant: "rectangle",
11234
+ width: "16px",
11235
+ height: "16px"
11236
+ })
11237
+ }),
11238
+ visibleCols.map((col)=>jsx("td", {
11239
+ className: cellClass,
11240
+ children: jsx(Skeleton, {
11241
+ variant: "text",
11242
+ width: "80%"
11243
+ })
11244
+ }, col.field)),
11245
+ showRowActionsColumn && jsx("td", {
11246
+ className: cellClass,
11247
+ children: jsx(Skeleton, {
11248
+ variant: "rectangle",
11249
+ width: "24px",
11250
+ height: "24px"
11251
+ })
11252
+ })
11253
+ ]
11254
+ }, `loading-${i}`));
11255
+ }
11256
+ function renderEmptyState(args) {
11257
+ var _args_emptyState;
11258
+ return jsx("tr", {
11259
+ children: jsx("td", {
11260
+ colSpan: args.totalColumnCount,
11261
+ className: args.cellClass,
11262
+ children: (_args_emptyState = args.emptyState) != null ? _args_emptyState : args.fallback
11263
+ })
11264
+ });
11265
+ }
11266
+ // ───── Icons ─────
11267
+ function SortIcon(props) {
11268
+ return jsx("svg", {
11269
+ width: "0.75em",
11270
+ height: "0.75em",
11271
+ viewBox: "0 0 16 16",
11272
+ fill: "currentColor",
11273
+ "aria-hidden": "true",
11274
+ className: "shrink-0 opacity-60 group-hover:opacity-100",
11275
+ children: props.direction === 'asc' ? jsx("path", {
11276
+ d: "M8 4l4 6H4z"
11277
+ }) : props.direction === 'desc' ? jsx("path", {
11278
+ d: "M8 12L4 6h8z"
11279
+ }) : jsxs(Fragment$1, {
11280
+ children: [
11281
+ jsx("path", {
11282
+ d: "M8 3l3 4H5z",
11283
+ opacity: "0.5"
11284
+ }),
11285
+ jsx("path", {
11286
+ d: "M8 13l-3-4h6z",
11287
+ opacity: "0.5"
11288
+ })
11289
+ ]
11290
+ })
11291
+ });
11292
+ }
11293
+ function SearchIcon(props) {
11294
+ return jsxs("svg", {
11295
+ width: "0.875em",
11296
+ height: "0.875em",
11297
+ viewBox: "0 0 16 16",
11298
+ fill: "none",
11299
+ stroke: "currentColor",
11300
+ strokeWidth: "1.5",
11301
+ strokeLinecap: "round",
11302
+ strokeLinejoin: "round",
11303
+ "aria-hidden": "true",
11304
+ className: props.className,
11305
+ children: [
11306
+ jsx("circle", {
11307
+ cx: "7",
11308
+ cy: "7",
11309
+ r: "4.5"
11310
+ }),
11311
+ jsx("path", {
11312
+ d: "M11 11l3 3"
11313
+ })
11314
+ ]
11315
+ });
11316
+ }
11317
+
9396
11318
  /**
9397
11319
  * @dashforge/tw
9398
11320
  *
@@ -9420,6 +11342,6 @@ function RowActionMenuItem(props) {
9420
11342
  */ // ───── Components ─────
9421
11343
  /**
9422
11344
  * Package version (synced with `package.json` at publish time).
9423
- */ const VERSION = '0.6.0-beta';
11345
+ */ const VERSION = '0.7.0-beta';
9424
11346
 
9425
- export { Accordion, AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, Container, 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, 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 };
11347
+ 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 };