@dashforge/tw 0.6.0-beta → 0.7.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/THEME-AUDIT.md +175 -0
  3. package/dist/index.esm.js +973 -69
  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 +171 -0
  9. package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
  10. package/dist/src/components/DataGrid/dataGrid.variants.d.ts +216 -0
  11. package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
  12. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
  13. package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
  14. package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
  15. package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
  16. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
  17. package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
  18. package/dist/src/components/Table/table.types.d.ts +8 -0
  19. package/dist/src/components/Table/table.types.d.ts.map +1 -1
  20. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
  21. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
  22. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
  23. package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
  24. package/dist/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.d.ts +1 -1
  25. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
  26. package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
  27. package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
  28. package/dist/src/components/{Table/_internal → _shared/data}/useTableFilter.d.ts +1 -1
  29. package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
  30. package/dist/src/components/{Table/_internal → _shared/data}/useTableSearch.d.ts +1 -1
  31. package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
  32. package/dist/src/components/{Table/_internal → _shared/data}/useTableSelection.d.ts +1 -1
  33. package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
  34. package/dist/src/components/{Table/_internal → _shared/data}/useTableSort.d.ts +1 -1
  35. package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
  36. package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
  37. package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
  38. package/dist/src/index.d.ts +5 -2
  39. package/dist/src/index.d.ts.map +1 -1
  40. package/package.json +3 -3
  41. package/src/components/Accordion/accordion.variants.ts +5 -3
  42. package/src/components/Box/Box.test.tsx +32 -11
  43. package/src/components/Box/box.variants.ts +23 -4
  44. package/src/components/DataGrid/DataGrid.test.tsx +496 -0
  45. package/src/components/DataGrid/DataGrid.tsx +831 -0
  46. package/src/components/DataGrid/dataGrid.types.ts +189 -0
  47. package/src/components/DataGrid/dataGrid.variants.ts +144 -0
  48. package/src/components/Dialog/dialog.variants.ts +10 -6
  49. package/src/components/Divider/Divider.test.tsx +3 -1
  50. package/src/components/Divider/Divider.tsx +1 -1
  51. package/src/components/Divider/divider.variants.ts +2 -1
  52. package/src/components/Pagination/Pagination.tsx +3 -1
  53. package/src/components/Pagination/pagination.variants.ts +19 -13
  54. package/src/components/Popover/popover.variants.ts +10 -4
  55. package/src/components/Skeleton/skeleton.variants.ts +2 -1
  56. package/src/components/Table/Table.tsx +8 -8
  57. package/src/components/Table/table.types.ts +9 -0
  58. package/src/components/Tabs/tabs.variants.ts +19 -8
  59. package/src/components/Tooltip/tooltip.variants.ts +12 -2
  60. package/src/components/Typography/Typography.test.tsx +3 -1
  61. package/src/components/Typography/typography.variants.ts +6 -2
  62. package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.test.ts +1 -1
  63. package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.ts +1 -1
  64. package/src/components/{Table/_internal → _shared/data}/useTableFilter.ts +1 -1
  65. package/src/components/{Table/_internal → _shared/data}/useTableSearch.test.ts +1 -1
  66. package/src/components/{Table/_internal → _shared/data}/useTableSearch.ts +1 -1
  67. package/src/components/{Table/_internal → _shared/data}/useTableSelection.ts +1 -1
  68. package/src/components/{Table/_internal → _shared/data}/useTableSort.test.ts +1 -1
  69. package/src/components/{Table/_internal → _shared/data}/useTableSort.ts +1 -1
  70. package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
  71. package/src/components/_shared/data/useVirtualizer.ts +199 -0
  72. package/src/components/_shared/themeIdentity.test.ts +113 -0
  73. package/src/index.ts +18 -2
  74. package/dist/src/components/Table/_internal/getNestedValue.d.ts.map +0 -1
  75. package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts.map +0 -1
  76. package/dist/src/components/Table/_internal/useControllableState.d.ts.map +0 -1
  77. package/dist/src/components/Table/_internal/useDebouncedValue.d.ts.map +0 -1
  78. package/dist/src/components/Table/_internal/useTableFilter.d.ts.map +0 -1
  79. package/dist/src/components/Table/_internal/useTableSearch.d.ts.map +0 -1
  80. package/dist/src/components/Table/_internal/useTableSelection.d.ts.map +0 -1
  81. package/dist/src/components/Table/_internal/useTableSort.d.ts.map +0 -1
  82. package/src/components/Table/_internal/themeIdentity.test.ts +0 -84
  83. /package/dist/src/components/{Table/_internal → _shared/data}/getNestedValue.d.ts +0 -0
  84. /package/dist/src/components/{Table/_internal → _shared/data}/useControllableState.d.ts +0 -0
  85. /package/dist/src/components/{Table/_internal → _shared/data}/useDebouncedValue.d.ts +0 -0
  86. /package/src/components/{Table/_internal → _shared/data}/getNestedValue.test.ts +0 -0
  87. /package/src/components/{Table/_internal → _shared/data}/getNestedValue.ts +0 -0
  88. /package/src/components/{Table/_internal → _shared/data}/useControllableState.ts +0 -0
  89. /package/src/components/{Table/_internal → _shared/data}/useDebouncedValue.ts +0 -0
  90. /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
@@ -8510,7 +8576,7 @@ function inferType(value) {
8510
8576
  return col.monospace === true;
8511
8577
  }
8512
8578
 
8513
- const DEFAULT_LABELS = {
8579
+ const DEFAULT_LABELS$1 = {
8514
8580
  searchPlaceholder: 'Search...',
8515
8581
  noData: 'No data',
8516
8582
  loading: 'Loading…',
@@ -8556,7 +8622,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8556
8622
  var _slotProps_root, _slotProps_toolbar, _slotProps_search, _slotProps_scroll, _slotProps_table, _slotProps_thead, _slotProps_headerRow, _slotProps_headerCell, _slotProps_selectionCell, _slotProps_headerCell1, _slotProps_expandToggleCell, _slotProps_headerCell2, _slotProps_tbody, _slotProps_row, _slotProps_cell, _slotProps_emptyState, _slotProps_bulkActionFooter;
8557
8623
  const { rows, cols, getRowId = DEFAULT_GET_ROW_ID, // State props (controllable)
8558
8624
  sortModel: sortModelProp, onSortChange, defaultSortModel, enableSearch = false, searchQuery: searchQueryProp, onSearchQueryChange, searchPlaceholder, searchDebounceMs = 200, filterModel: filterModelProp, onFilterChange, rowSelection = 'none', selectedRowIds: selectedRowIdsProp, onSelectionChange, bulkActions, expandable, rowActions, loading = false, loadingRowCount = 5, emptyState, stickyHeader = true, caption, showCaption = false, access: tableAccess, labels: labelsProp, variant, size, density, sx, slotProps } = props;
8559
- const labels = _extends({}, DEFAULT_LABELS, labelsProp);
8625
+ const labels = _extends({}, DEFAULT_LABELS$1, labelsProp);
8560
8626
  const tableAccessState = useAccessState(tableAccess);
8561
8627
  // ───── Controllable state machinery ─────
8562
8628
  const [sortModel, setSortModel] = useControllableState({
@@ -8629,7 +8695,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8629
8695
  className: "sr-only",
8630
8696
  children: labels.searchPlaceholder
8631
8697
  }),
8632
- jsx(SearchIcon, {
8698
+ jsx(SearchIcon$1, {
8633
8699
  className: "ml-2 shrink-0 text-neutral-500"
8634
8700
  }),
8635
8701
  jsx("input", {
@@ -8681,10 +8747,10 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8681
8747
  }),
8682
8748
  visibleCols.map((col)=>{
8683
8749
  var _slotProps_headerCell;
8684
- return jsx(HeaderCell, {
8750
+ return jsx(HeaderCell$1, {
8685
8751
  col: col,
8686
8752
  sortModel: sortModel,
8687
- onSortClick: (e)=>handleSortClick(col, e, sortModel, setSortModel),
8753
+ onSortClick: (e)=>handleSortClick$1(col, e, sortModel, setSortModel),
8688
8754
  inferredType: columnTypes.get(col.field),
8689
8755
  labels: labels,
8690
8756
  className: cn(v.headerCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className),
@@ -8702,7 +8768,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8702
8768
  }),
8703
8769
  jsx("tbody", {
8704
8770
  className: cn(v.tbody(), slotProps == null ? void 0 : (_slotProps_tbody = slotProps.tbody) == null ? void 0 : _slotProps_tbody.className),
8705
- children: loading ? renderLoadingRows({
8771
+ children: loading ? renderLoadingRows$1({
8706
8772
  count: loadingRowCount,
8707
8773
  visibleCols,
8708
8774
  showSelectionColumn,
@@ -8710,7 +8776,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8710
8776
  showRowActionsColumn,
8711
8777
  rowClass: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row = slotProps.row) == null ? void 0 : _slotProps_row.className),
8712
8778
  cellClass: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell = slotProps.cell) == null ? void 0 : _slotProps_cell.className)
8713
- }) : sortedRows.length === 0 ? renderEmptyState({
8779
+ }) : sortedRows.length === 0 ? renderEmptyState$1({
8714
8780
  totalColumnCount,
8715
8781
  emptyState,
8716
8782
  fallback: labels.noData,
@@ -8782,7 +8848,7 @@ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8782
8848
  });
8783
8849
  }
8784
8850
  // ───── Header cell (with sort button) ─────
8785
- function HeaderCell(props) {
8851
+ function HeaderCell$1(props) {
8786
8852
  const { col, sortModel, onSortClick, inferredType, labels, className, buttonClassName, disabled } = props;
8787
8853
  const align = resolveAlign(col, inferredType);
8788
8854
  const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
@@ -8810,7 +8876,7 @@ function HeaderCell(props) {
8810
8876
  jsx("span", {
8811
8877
  children: headerContent
8812
8878
  }),
8813
- jsx(SortIcon, {
8879
+ jsx(SortIcon$1, {
8814
8880
  direction: sortEntry == null ? void 0 : sortEntry.direction
8815
8881
  })
8816
8882
  ]
@@ -8858,7 +8924,7 @@ function Row(props) {
8858
8924
  children: jsx(ChevronRightIcon, {})
8859
8925
  })
8860
8926
  }),
8861
- visibleCols.map((col)=>jsx(DataCell, {
8927
+ visibleCols.map((col)=>jsx(DataCell$1, {
8862
8928
  col: col,
8863
8929
  row: row,
8864
8930
  rowIndex: rowIndex,
@@ -8885,7 +8951,7 @@ function Row(props) {
8885
8951
  });
8886
8952
  }
8887
8953
  // ───── Data cell ─────
8888
- function DataCell(props) {
8954
+ function DataCell$1(props) {
8889
8955
  const { col, row, rowIndex, isSelected, isExpanded, inferredType, cellClass } = props;
8890
8956
  const value = getNestedValue(row, col.field);
8891
8957
  const align = resolveAlign(col, inferredType);
@@ -8910,7 +8976,7 @@ function DataCell(props) {
8910
8976
  isSelected,
8911
8977
  isExpanded,
8912
8978
  access: columnAccessState
8913
- }) : renderDefaultCell(value);
8979
+ }) : renderDefaultCell$1(value);
8914
8980
  return jsx("td", {
8915
8981
  className: cn(cellClass, alignClass, tabularNums && 'tabular-nums', monospace && 'font-mono'),
8916
8982
  style: {
@@ -8921,19 +8987,19 @@ function DataCell(props) {
8921
8987
  });
8922
8988
  }
8923
8989
  // ───── Helpers ─────
8924
- function renderDefaultCell(value) {
8990
+ function renderDefaultCell$1(value) {
8925
8991
  if (value == null) return null;
8926
8992
  if (typeof value === 'boolean') return value ? '✓' : '✗';
8927
8993
  if (value instanceof Date) return value.toISOString().slice(0, 10);
8928
8994
  if (typeof value === 'object') return JSON.stringify(value);
8929
8995
  return value;
8930
8996
  }
8931
- function handleSortClick(col, event, current, setSortModel) {
8997
+ function handleSortClick$1(col, event, current, setSortModel) {
8932
8998
  if (!col.sortable) return;
8933
8999
  const multi = event.shiftKey;
8934
9000
  setSortModel((prev)=>cycleSortFor(prev, col.field, multi));
8935
9001
  }
8936
- function renderLoadingRows(args) {
9002
+ function renderLoadingRows$1(args) {
8937
9003
  const { count, visibleCols, showSelectionColumn, showExpandColumn, showRowActionsColumn, rowClass, cellClass } = args;
8938
9004
  return Array.from({
8939
9005
  length: count
@@ -8975,7 +9041,7 @@ function renderLoadingRows(args) {
8975
9041
  ]
8976
9042
  }, `loading-${i}`));
8977
9043
  }
8978
- function renderEmptyState(args) {
9044
+ function renderEmptyState$1(args) {
8979
9045
  var _args_emptyState;
8980
9046
  return jsx("tr", {
8981
9047
  children: jsx("td", {
@@ -8986,7 +9052,7 @@ function renderEmptyState(args) {
8986
9052
  });
8987
9053
  }
8988
9054
  // ───── Icons ─────
8989
- function SortIcon(props) {
9055
+ function SortIcon$1(props) {
8990
9056
  return jsx("svg", {
8991
9057
  width: "0.75em",
8992
9058
  height: "0.75em",
@@ -9028,7 +9094,7 @@ function ChevronRightIcon() {
9028
9094
  })
9029
9095
  });
9030
9096
  }
9031
- function SearchIcon(props) {
9097
+ function SearchIcon$1(props) {
9032
9098
  return jsxs("svg", {
9033
9099
  width: "0.875em",
9034
9100
  height: "0.875em",
@@ -9393,6 +9459,844 @@ function RowActionMenuItem(props) {
9393
9459
  });
9394
9460
  }
9395
9461
 
9462
+ /**
9463
+ * `<DataGrid>` variant recipe.
9464
+ *
9465
+ * Sibling of `tableVariants` — shares the same visual axes (variant /
9466
+ * size / density) and the same identity-rule patterns:
9467
+ * - No `dark:` variants on neutral palette (auto-invert via the
9468
+ * dashforgePreset() CSS var swap).
9469
+ * - Selected row uses LeftNav `itemActive` pattern
9470
+ * (`bg-primary-100 text-primary-900`) — guaranteed contrast.
9471
+ * - Hover uses one-tier elevation (`bg-neutral-100`).
9472
+ *
9473
+ * DataGrid-specific slot additions vs Table:
9474
+ * - `stickyLeftCell` — column that uses `cols[i].sticky === 'left'`.
9475
+ * Z-index ordering: `thead` (z-10) < `stickyLeftCell` (z-11) <
9476
+ * `header × stickyLeft top-left corner cell` (z-20).
9477
+ * - `paginationFooter` — wraps the optional `<Pagination>`.
9478
+ */ const dataGridVariants = tv({
9479
+ slots: {
9480
+ root: 'w-full',
9481
+ toolbar: 'flex flex-wrap items-center gap-3 mb-3',
9482
+ search: [
9483
+ 'inline-flex items-center gap-2',
9484
+ 'w-full max-w-sm rounded-md',
9485
+ 'border border-neutral-300',
9486
+ 'bg-neutral-50',
9487
+ 'focus-within:ring-2 focus-within:ring-primary-500',
9488
+ 'transition-colors motion-reduce:transition-none'
9489
+ ],
9490
+ // The scroll container needs an explicit bounded height for
9491
+ // virtualization to compute the window. The consumer passes
9492
+ // `height` prop (inline style) or sets it via `sx` / parent
9493
+ // layout — DataGrid does NOT default a height (would be wrong
9494
+ // for most layouts).
9495
+ scroll: 'relative w-full overflow-auto rounded-md',
9496
+ table: 'w-full caption-bottom text-left',
9497
+ thead: 'bg-neutral-50 sticky top-0 z-10',
9498
+ tbody: '',
9499
+ headerRow: '',
9500
+ headerCell: [
9501
+ 'text-neutral-700',
9502
+ 'font-semibold whitespace-nowrap select-none',
9503
+ 'border-b border-neutral-200',
9504
+ 'bg-neutral-50'
9505
+ ],
9506
+ headerCellButton: [
9507
+ 'group inline-flex items-center gap-1 select-none',
9508
+ 'hover:text-neutral-900',
9509
+ 'focus:outline-none focus-visible:underline',
9510
+ 'transition-colors motion-reduce:transition-none'
9511
+ ],
9512
+ row: [
9513
+ 'group/row transition-colors motion-reduce:transition-none',
9514
+ 'hover:bg-neutral-100',
9515
+ 'data-[selected=true]:bg-primary-100',
9516
+ 'data-[selected=true]:text-primary-900',
9517
+ 'data-[selected=true]:font-medium'
9518
+ ],
9519
+ cell: [
9520
+ 'text-neutral-900',
9521
+ 'border-b border-neutral-200',
9522
+ 'align-middle'
9523
+ ],
9524
+ selectionCell: 'w-10 text-center',
9525
+ rowActionsCell: [
9526
+ 'w-12 text-right',
9527
+ 'opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100',
9528
+ 'transition-opacity motion-reduce:transition-none'
9529
+ ],
9530
+ emptyState: 'py-12 text-center text-neutral-500',
9531
+ loadingOverlay: 'pointer-events-none',
9532
+ bulkActionFooter: [
9533
+ 'sticky bottom-0 z-20',
9534
+ 'flex flex-wrap items-center justify-between gap-3',
9535
+ 'px-3 py-2',
9536
+ 'border-t border-neutral-200',
9537
+ 'bg-neutral-50 shadow-md'
9538
+ ],
9539
+ // Sticky left column — uses `position: sticky; left: 0` + z-index
9540
+ // so the column stays pinned during horizontal scroll. The
9541
+ // `bg-neutral-50` ensures the column doesn't bleed when content
9542
+ // scrolls underneath.
9543
+ stickyLeftCell: 'sticky left-0 z-[1] bg-neutral-50',
9544
+ // Sticky left × header intersection (top-left corner): higher
9545
+ // z-index than both header (`z-10`) and sticky col (`z-[1]`).
9546
+ stickyLeftHeaderCell: 'sticky left-0 z-20 bg-neutral-50',
9547
+ paginationFooter: 'flex justify-end mt-3'
9548
+ },
9549
+ variants: {
9550
+ variant: {
9551
+ plain: {},
9552
+ lines: {},
9553
+ striped: {
9554
+ row: 'odd:bg-neutral-100/60'
9555
+ },
9556
+ bordered: {
9557
+ table: 'border border-neutral-200',
9558
+ cell: 'border-r last:border-r-0 border-neutral-200',
9559
+ headerCell: 'border-r last:border-r-0 border-neutral-200'
9560
+ }
9561
+ },
9562
+ size: {
9563
+ sm: {
9564
+ headerCell: 'text-xs',
9565
+ cell: 'text-xs',
9566
+ search: 'text-xs'
9567
+ },
9568
+ md: {
9569
+ headerCell: 'text-sm',
9570
+ cell: 'text-sm',
9571
+ search: 'text-sm'
9572
+ },
9573
+ lg: {
9574
+ headerCell: 'text-base',
9575
+ cell: 'text-base',
9576
+ search: 'text-base'
9577
+ }
9578
+ },
9579
+ density: {
9580
+ compact: {
9581
+ headerCell: 'px-2 py-1',
9582
+ cell: 'px-2 py-1'
9583
+ },
9584
+ comfortable: {
9585
+ headerCell: 'px-3 py-2',
9586
+ cell: 'px-3 py-2'
9587
+ },
9588
+ spacious: {
9589
+ headerCell: 'px-4 py-3',
9590
+ cell: 'px-4 py-3'
9591
+ }
9592
+ }
9593
+ },
9594
+ defaultVariants: {
9595
+ variant: 'lines',
9596
+ size: 'md',
9597
+ density: 'comfortable'
9598
+ }
9599
+ });
9600
+
9601
+ /**
9602
+ * Compute the visible window of rows + the spacer heights given the
9603
+ * current scroll position. Pure function — exported for testability.
9604
+ *
9605
+ * @example
9606
+ * ```ts
9607
+ * computeWindow({ totalCount: 1000, rowHeight: 48, viewportHeight: 600,
9608
+ * scrollTop: 0, overscan: 5 });
9609
+ * // → { startIndex: 0, endIndex: 17, paddingTop: 0, paddingBottom: 47136 }
9610
+ * ```
9611
+ */ function computeWindow(args) {
9612
+ const { totalCount, rowHeight, viewportHeight, scrollTop, overscan = 5 } = args;
9613
+ if (totalCount <= 0 || rowHeight <= 0 || viewportHeight <= 0) {
9614
+ return {
9615
+ startIndex: 0,
9616
+ endIndex: -1,
9617
+ paddingTop: 0,
9618
+ paddingBottom: 0
9619
+ };
9620
+ }
9621
+ // Raw visible range from scrollTop alone.
9622
+ const rawStart = Math.floor(scrollTop / rowHeight);
9623
+ const rawEnd = Math.ceil((scrollTop + viewportHeight) / rowHeight);
9624
+ // Apply overscan + clamp to [0, totalCount - 1].
9625
+ const startIndex = Math.max(0, rawStart - overscan);
9626
+ const endIndex = Math.min(totalCount - 1, rawEnd + overscan);
9627
+ const paddingTop = startIndex * rowHeight;
9628
+ const paddingBottom = Math.max(0, (totalCount - 1 - endIndex) * rowHeight);
9629
+ return {
9630
+ startIndex,
9631
+ endIndex,
9632
+ paddingTop,
9633
+ paddingBottom
9634
+ };
9635
+ }
9636
+ /**
9637
+ * `useVirtualizer` — viewport-based row windowing for `<DataGrid>`.
9638
+ *
9639
+ * Hand-rolled (no `@tanstack/react-virtual` dep — constraint memorized
9640
+ * in `feedback_dashforge_preset_is_identity` companion: "no new
9641
+ * external runtime deps"). Uses scroll event + `requestAnimationFrame`
9642
+ * debounce + `ResizeObserver` on the container. Fixed row height
9643
+ * only in v1 — variable-height with measurement cache deferred to
9644
+ * v1-bis.
9645
+ *
9646
+ * Returns:
9647
+ * - `scrollRef`: attach to the scroll container `<div>`.
9648
+ * - `window`: which row indices to mount + top/bottom spacer heights
9649
+ * to preserve scroll-area size.
9650
+ * - `viewportHeight`: current container height (re-measured on
9651
+ * `ResizeObserver` ticks).
9652
+ *
9653
+ * The DataGrid renders `rows.slice(window.startIndex, window.endIndex + 1)`
9654
+ * between two spacer `<tr style={{ height: paddingTop }} />` elements
9655
+ * (above and below). This preserves `<table>` semantics — screen
9656
+ * readers see a continuous structure with `aria-hidden` spacers.
9657
+ *
9658
+ * @example
9659
+ * ```tsx
9660
+ * const { scrollRef, window } = useVirtualizer({
9661
+ * totalCount: rows.length,
9662
+ * rowHeight: 48,
9663
+ * overscan: 5,
9664
+ * });
9665
+ *
9666
+ * return (
9667
+ * <div ref={scrollRef} style={{ height: 600, overflow: 'auto' }}>
9668
+ * <table>
9669
+ * <tbody>
9670
+ * <tr aria-hidden="true" style={{ height: window.paddingTop }} />
9671
+ * {rows.slice(window.startIndex, window.endIndex + 1).map(...)}
9672
+ * <tr aria-hidden="true" style={{ height: window.paddingBottom }} />
9673
+ * </tbody>
9674
+ * </table>
9675
+ * </div>
9676
+ * );
9677
+ * ```
9678
+ */ function useVirtualizer(args) {
9679
+ const { totalCount, rowHeight, overscan = 5, initialViewportHeight = 400 } = args;
9680
+ const scrollRef = useRef(null);
9681
+ const [scrollTop, setScrollTop] = useState(0);
9682
+ const [viewportHeight, setViewportHeight] = useState(initialViewportHeight);
9683
+ // Debounce scroll handler via requestAnimationFrame so we never
9684
+ // re-render more than once per paint frame even when the scroll
9685
+ // event fires rapidly during fast scrolling.
9686
+ const rafIdRef = useRef(null);
9687
+ const handleScroll = useCallback(()=>{
9688
+ if (rafIdRef.current != null) return;
9689
+ rafIdRef.current = requestAnimationFrame(()=>{
9690
+ rafIdRef.current = null;
9691
+ const el = scrollRef.current;
9692
+ if (!el) return;
9693
+ setScrollTop(el.scrollTop);
9694
+ });
9695
+ }, []);
9696
+ // Attach scroll listener.
9697
+ useEffect(()=>{
9698
+ const el = scrollRef.current;
9699
+ if (!el) return;
9700
+ el.addEventListener('scroll', handleScroll, {
9701
+ passive: true
9702
+ });
9703
+ return ()=>{
9704
+ el.removeEventListener('scroll', handleScroll);
9705
+ if (rafIdRef.current != null) {
9706
+ cancelAnimationFrame(rafIdRef.current);
9707
+ rafIdRef.current = null;
9708
+ }
9709
+ };
9710
+ }, [
9711
+ handleScroll
9712
+ ]);
9713
+ // Track container viewport height via ResizeObserver — re-virtualize
9714
+ // when the container size changes (window resize, sidebar collapse,
9715
+ // etc.). Use `useLayoutEffect` so the first measurement happens
9716
+ // before paint to avoid a flash of stale window.
9717
+ //
9718
+ // **Defensive**: only accept measurements > 0. jsdom (test env)
9719
+ // returns `clientHeight === 0` for unmounted containers, which would
9720
+ // collapse the window to empty. The hook keeps the
9721
+ // `initialViewportHeight` fallback in that case.
9722
+ useLayoutEffect(()=>{
9723
+ const el = scrollRef.current;
9724
+ if (!el) return;
9725
+ if (el.clientHeight > 0) {
9726
+ setViewportHeight(el.clientHeight);
9727
+ }
9728
+ if (typeof ResizeObserver === 'undefined') return;
9729
+ const observer = new ResizeObserver((entries)=>{
9730
+ const entry = entries[0];
9731
+ if (!entry || entry.contentRect.height <= 0) return;
9732
+ setViewportHeight(entry.contentRect.height);
9733
+ });
9734
+ observer.observe(el);
9735
+ return ()=>observer.disconnect();
9736
+ }, []);
9737
+ const window = computeWindow({
9738
+ totalCount,
9739
+ rowHeight,
9740
+ viewportHeight,
9741
+ scrollTop,
9742
+ overscan
9743
+ });
9744
+ return {
9745
+ scrollRef,
9746
+ window,
9747
+ viewportHeight
9748
+ };
9749
+ }
9750
+
9751
+ const DEFAULT_LABELS = {
9752
+ searchPlaceholder: 'Search...',
9753
+ noData: 'No data',
9754
+ loading: 'Loading…',
9755
+ ariaSortNone: 'Not sorted. Click to sort ascending.',
9756
+ ariaSortAscending: 'Sorted ascending. Click to sort descending.',
9757
+ ariaSortDescending: 'Sorted descending. Click to clear sort.',
9758
+ ariaSelectRow: 'Select row',
9759
+ ariaSelectAllRows: 'Select all rows',
9760
+ ariaExpandRow: 'Expand row',
9761
+ ariaCollapseRow: 'Collapse row',
9762
+ selectedCount: '{count} selected',
9763
+ clearSelection: 'Clear selection',
9764
+ filterColumn: 'Filter',
9765
+ filterApply: 'Apply',
9766
+ filterClear: 'Clear',
9767
+ density: 'Density',
9768
+ densityCompact: 'Compact',
9769
+ densityComfortable: 'Comfortable',
9770
+ densitySpacious: 'Spacious'
9771
+ };
9772
+ /**
9773
+ * Dashforge TW `<DataGrid>` — virtualized data table.
9774
+ *
9775
+ * Companion to `<Table>` for large data sets (~500+ rows up to
9776
+ * millions via virtualization). Same column model, sort/search/
9777
+ * filter/selection logic (shared via `_shared/data/` helpers) —
9778
+ * differs in render path: only the visible window of rows is mounted
9779
+ * in DOM, with spacer `<tr>` above and below to preserve scrollbar
9780
+ * size.
9781
+ *
9782
+ * Virtualization: hand-rolled (`useVirtualizer`) using
9783
+ * `IntersectionObserver` + scroll-event + `requestAnimationFrame`
9784
+ * debounce + `ResizeObserver`. Zero new runtime deps.
9785
+ *
9786
+ * Server-side mode (4 independent opt-in flags):
9787
+ * - `serverSideSort` — emits `onSortChange` but does NOT sort locally
9788
+ * - `serverSideFilter` — emits `onFilterChange` but does NOT filter
9789
+ * - `serverSideSearch` — emits `onSearchQueryChange` (debounced)
9790
+ * - `serverSidePagination` — `totalCount` prop sizes the virtual
9791
+ * scrollbar; `rows` is the page slice from the server
9792
+ *
9793
+ * Selection at scale: `selectAllScope`:
9794
+ * - `'allLoaded'` (default) — header checkbox toggles all rows in
9795
+ * `rows` prop
9796
+ * - `'visible'` — toggles only the current viewport window
9797
+ */ function DataGrid(props) {
9798
+ var _slotProps_root, _slotProps_toolbar, _slotProps_search, _slotProps_scroll, _slotProps_table, _slotProps_thead, _slotProps_headerRow, _slotProps_headerCell, _slotProps_selectionCell, _slotProps_headerCell1, _slotProps_tbody, _slotProps_row, _slotProps_cell, _slotProps_emptyState, _slotProps_row1, _slotProps_cell1, _slotProps_cell2, _slotProps_selectionCell1, _slotProps_cell3, _slotProps_rowActionsCell, _slotProps_bulkActionFooter, _slotProps_pagination;
9799
+ const { rows, cols, getRowId, totalCount: totalCountProp, rowHeight, overscan = 5, height, sortModel: sortModelProp, onSortChange, defaultSortModel, serverSideSort = false, enableSearch = false, searchQuery: searchQueryProp, onSearchQueryChange, searchPlaceholder, searchDebounceMs = 200, serverSideSearch = false, filterModel: filterModelProp, onFilterChange, serverSideFilter = false, rowSelection = 'none', selectedRowIds: selectedRowIdsProp, onSelectionChange, bulkActions, selectAllScope = 'allLoaded', pagination, serverSidePagination = false, rowActions, loading = false, loadingRowCount = 8, emptyState, stickyHeader = true, caption, showCaption = false, access: gridAccess, labels: labelsProp, variant, size, density, sx, slotProps } = props;
9800
+ const labels = _extends({}, DEFAULT_LABELS, labelsProp);
9801
+ const gridAccessState = useAccessState(gridAccess);
9802
+ // ───── Controllable state ─────
9803
+ const [sortModel, setSortModel] = useControllableState({
9804
+ controlledValue: sortModelProp,
9805
+ defaultValue: defaultSortModel != null ? defaultSortModel : [],
9806
+ onChange: onSortChange
9807
+ });
9808
+ const [searchQuery, setSearchQuery] = useControllableState({
9809
+ controlledValue: searchQueryProp,
9810
+ defaultValue: '',
9811
+ onChange: onSearchQueryChange
9812
+ });
9813
+ const filterModel = filterModelProp != null ? filterModelProp : [];
9814
+ const [selectedRowIds, setSelectedRowIds] = useControllableState({
9815
+ controlledValue: selectedRowIdsProp,
9816
+ defaultValue: [],
9817
+ onChange: onSelectionChange
9818
+ });
9819
+ // ───── RBAC: filter columns ─────
9820
+ const visibleCols = useMemo(()=>cols.filter((col)=>!col.access || col.access.onUnauthorized !== 'hide'), [
9821
+ cols
9822
+ ]);
9823
+ // ───── Smart-default column types ─────
9824
+ const columnTypes = useColumnAutoDetect(rows, visibleCols);
9825
+ // ───── Pipeline: search → filter → sort (skip when server-side) ─────
9826
+ //
9827
+ // Each pipeline hook is ALWAYS called (React hooks rules). The
9828
+ // server-side flags decide whether to use the local-computed result
9829
+ // or pass the upstream rows through unchanged. The hooks are
9830
+ // `useMemo`-based internally so they're cheap when their inputs
9831
+ // are unchanged.
9832
+ const debouncedSearchQuery = useDebouncedValue(searchQuery, searchDebounceMs);
9833
+ const localSearched = useTableSearch(rows, visibleCols, debouncedSearchQuery);
9834
+ const searchedRows = serverSideSearch ? rows : localSearched;
9835
+ const localFiltered = useTableFilter(searchedRows, filterModel);
9836
+ const filteredRows = serverSideFilter ? searchedRows : localFiltered;
9837
+ const localSorted = useTableSort(filteredRows, visibleCols, sortModel);
9838
+ const sortedRows = serverSideSort ? filteredRows : localSorted;
9839
+ // ───── Virtualization ─────
9840
+ // Effective row count for the virtual scrollbar:
9841
+ // - Client-side: actual sorted rows count
9842
+ // - Server-side pagination: caller-provided totalCount
9843
+ const effectiveTotalCount = serverSidePagination ? totalCountProp != null ? totalCountProp : sortedRows.length : sortedRows.length;
9844
+ const initialViewportHeight = height ? parseInt(height, 10) || 400 : 400;
9845
+ const { scrollRef, window: vWindow } = useVirtualizer({
9846
+ totalCount: effectiveTotalCount,
9847
+ rowHeight,
9848
+ overscan,
9849
+ initialViewportHeight
9850
+ });
9851
+ // The rows actually mounted in DOM.
9852
+ const renderedRows = useMemo(()=>{
9853
+ if (serverSidePagination) {
9854
+ // In server-side pagination, `rows` IS the page slice; we
9855
+ // render all of it (virtualization within the page is still
9856
+ // useful for very large page sizes).
9857
+ return rows.slice(vWindow.startIndex, vWindow.endIndex + 1);
9858
+ }
9859
+ return sortedRows.slice(vWindow.startIndex, vWindow.endIndex + 1);
9860
+ }, [
9861
+ serverSidePagination,
9862
+ rows,
9863
+ sortedRows,
9864
+ vWindow.startIndex,
9865
+ vWindow.endIndex
9866
+ ]);
9867
+ // ───── Selection helpers ─────
9868
+ // For `selectAllScope='visible'`, the select-all scope is the
9869
+ // current window. For `'allLoaded'`, it's the whole sorted set.
9870
+ const selectAllPool = useMemo(()=>{
9871
+ if (selectAllScope === 'visible') {
9872
+ return renderedRows.map((row, idx)=>getRowId(row, vWindow.startIndex + idx));
9873
+ }
9874
+ return sortedRows.map((row, idx)=>getRowId(row, idx));
9875
+ }, [
9876
+ selectAllScope,
9877
+ renderedRows,
9878
+ sortedRows,
9879
+ getRowId,
9880
+ vWindow.startIndex
9881
+ ]);
9882
+ const selection = useTableSelection(rowSelection, selectedRowIds, setSelectedRowIds);
9883
+ const selectedRows = useMemo(()=>sortedRows.filter((row, idx)=>selection.isSelected(getRowId(row, idx))), [
9884
+ sortedRows,
9885
+ selection,
9886
+ getRowId
9887
+ ]);
9888
+ // ───── Variant recipe ─────
9889
+ const v = dataGridVariants({
9890
+ variant,
9891
+ size,
9892
+ density
9893
+ });
9894
+ // ───── Render ─────
9895
+ if (!gridAccessState.visible) return null;
9896
+ const isInteractive = !gridAccessState.disabled;
9897
+ const showSelectionColumn = rowSelection !== 'none';
9898
+ const showRowActionsColumn = rowActions != null;
9899
+ const totalColumnCount = visibleCols.length + (showSelectionColumn ? 1 : 0) + (showRowActionsColumn ? 1 : 0);
9900
+ return jsxs("div", {
9901
+ className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
9902
+ "data-disabled": !isInteractive ? 'true' : undefined,
9903
+ children: [
9904
+ enableSearch && jsx("div", {
9905
+ className: cn(v.toolbar(), slotProps == null ? void 0 : (_slotProps_toolbar = slotProps.toolbar) == null ? void 0 : _slotProps_toolbar.className),
9906
+ children: jsxs("label", {
9907
+ className: cn(v.search(), slotProps == null ? void 0 : (_slotProps_search = slotProps.search) == null ? void 0 : _slotProps_search.className),
9908
+ children: [
9909
+ jsx("span", {
9910
+ className: "sr-only",
9911
+ children: labels.searchPlaceholder
9912
+ }),
9913
+ jsx(SearchIcon, {
9914
+ className: "ml-2 shrink-0 text-neutral-500"
9915
+ }),
9916
+ jsx("input", {
9917
+ type: "text",
9918
+ value: searchQuery,
9919
+ onChange: (e)=>setSearchQuery(e.target.value),
9920
+ placeholder: searchPlaceholder != null ? searchPlaceholder : labels.searchPlaceholder,
9921
+ disabled: !isInteractive,
9922
+ className: cn('flex-1 bg-transparent border-0 outline-none', 'px-2 py-1.5', 'placeholder:text-neutral-400', 'disabled:opacity-50 disabled:cursor-not-allowed')
9923
+ })
9924
+ ]
9925
+ })
9926
+ }),
9927
+ jsx("div", {
9928
+ ref: scrollRef,
9929
+ className: cn(v.scroll(), slotProps == null ? void 0 : (_slotProps_scroll = slotProps.scroll) == null ? void 0 : _slotProps_scroll.className),
9930
+ style: height ? {
9931
+ height,
9932
+ maxHeight: height
9933
+ } : undefined,
9934
+ children: jsxs("table", {
9935
+ className: cn(v.table(), slotProps == null ? void 0 : (_slotProps_table = slotProps.table) == null ? void 0 : _slotProps_table.className),
9936
+ children: [
9937
+ caption != null && jsx("caption", {
9938
+ className: cn(!showCaption && 'sr-only', 'py-2 text-sm text-neutral-500'),
9939
+ children: caption
9940
+ }),
9941
+ jsx("thead", {
9942
+ className: cn(stickyHeader ? v.thead() : 'bg-neutral-50', slotProps == null ? void 0 : (_slotProps_thead = slotProps.thead) == null ? void 0 : _slotProps_thead.className),
9943
+ children: jsxs("tr", {
9944
+ className: cn(v.headerRow(), slotProps == null ? void 0 : (_slotProps_headerRow = slotProps.headerRow) == null ? void 0 : _slotProps_headerRow.className),
9945
+ children: [
9946
+ showSelectionColumn && jsx("th", {
9947
+ scope: "col",
9948
+ className: cn(v.headerCell(), v.selectionCell(), v.stickyLeftHeaderCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className, slotProps == null ? void 0 : (_slotProps_selectionCell = slotProps.selectionCell) == null ? void 0 : _slotProps_selectionCell.className),
9949
+ children: rowSelection === 'multiple' && jsx("input", {
9950
+ type: "checkbox",
9951
+ "aria-label": labels.ariaSelectAllRows,
9952
+ checked: selection.isAllSelected(selectAllPool),
9953
+ ref: (el)=>{
9954
+ if (el) el.indeterminate = selection.isIndeterminate(selectAllPool);
9955
+ },
9956
+ onChange: ()=>selection.toggleAll(selectAllPool),
9957
+ disabled: !isInteractive,
9958
+ className: "cursor-pointer"
9959
+ })
9960
+ }),
9961
+ visibleCols.map((col)=>{
9962
+ var _slotProps_headerCell;
9963
+ return jsx(HeaderCell, {
9964
+ col: col,
9965
+ sortModel: sortModel,
9966
+ onSortClick: (e)=>handleSortClick(col, e, sortModel, setSortModel),
9967
+ inferredType: columnTypes.get(col.field),
9968
+ labels: labels,
9969
+ className: cn(v.headerCell(), col.sticky === 'left' && v.stickyLeftHeaderCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className),
9970
+ buttonClassName: v.headerCellButton(),
9971
+ disabled: !isInteractive
9972
+ }, col.field);
9973
+ }),
9974
+ showRowActionsColumn && jsx("th", {
9975
+ scope: "col",
9976
+ "aria-hidden": "true",
9977
+ className: cn(v.headerCell(), 'w-12', slotProps == null ? void 0 : (_slotProps_headerCell1 = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell1.className)
9978
+ })
9979
+ ]
9980
+ })
9981
+ }),
9982
+ jsx("tbody", {
9983
+ className: cn(v.tbody(), slotProps == null ? void 0 : (_slotProps_tbody = slotProps.tbody) == null ? void 0 : _slotProps_tbody.className),
9984
+ children: loading ? renderLoadingRows({
9985
+ count: loadingRowCount,
9986
+ visibleCols,
9987
+ showSelectionColumn,
9988
+ showRowActionsColumn,
9989
+ rowHeight,
9990
+ rowClass: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row = slotProps.row) == null ? void 0 : _slotProps_row.className),
9991
+ cellClass: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell = slotProps.cell) == null ? void 0 : _slotProps_cell.className)
9992
+ }) : sortedRows.length === 0 ? renderEmptyState({
9993
+ totalColumnCount,
9994
+ emptyState,
9995
+ fallback: labels.noData,
9996
+ cellClass: cn(v.cell(), v.emptyState(), slotProps == null ? void 0 : (_slotProps_emptyState = slotProps.emptyState) == null ? void 0 : _slotProps_emptyState.className)
9997
+ }) : renderVirtualizedBody({
9998
+ vWindow,
9999
+ renderedRows,
10000
+ visibleCols,
10001
+ columnTypes,
10002
+ selection,
10003
+ rowSelection,
10004
+ showSelectionColumn,
10005
+ showRowActionsColumn,
10006
+ rowActions,
10007
+ getRowId,
10008
+ isInteractive,
10009
+ labels,
10010
+ classes: {
10011
+ row: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row1 = slotProps.row) == null ? void 0 : _slotProps_row1.className),
10012
+ cell: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell1 = slotProps.cell) == null ? void 0 : _slotProps_cell1.className),
10013
+ selectionCell: cn(v.cell(), v.selectionCell(), v.stickyLeftCell(), slotProps == null ? void 0 : (_slotProps_cell2 = slotProps.cell) == null ? void 0 : _slotProps_cell2.className, slotProps == null ? void 0 : (_slotProps_selectionCell1 = slotProps.selectionCell) == null ? void 0 : _slotProps_selectionCell1.className),
10014
+ stickyLeftCell: v.stickyLeftCell(),
10015
+ rowActionsCell: cn(v.cell(), v.rowActionsCell(), slotProps == null ? void 0 : (_slotProps_cell3 = slotProps.cell) == null ? void 0 : _slotProps_cell3.className, slotProps == null ? void 0 : (_slotProps_rowActionsCell = slotProps.rowActionsCell) == null ? void 0 : _slotProps_rowActionsCell.className)
10016
+ }
10017
+ })
10018
+ })
10019
+ ]
10020
+ })
10021
+ }),
10022
+ bulkActions && selectedRows.length > 0 && jsxs("div", {
10023
+ className: cn(v.bulkActionFooter(), slotProps == null ? void 0 : (_slotProps_bulkActionFooter = slotProps.bulkActionFooter) == null ? void 0 : _slotProps_bulkActionFooter.className),
10024
+ role: "region",
10025
+ "aria-label": labels.selectedCount.replace('{count}', String(selectedRows.length)),
10026
+ children: [
10027
+ jsx("span", {
10028
+ className: "text-sm text-neutral-700",
10029
+ children: labels.selectedCount.replace('{count}', String(selectedRows.length))
10030
+ }),
10031
+ jsxs("div", {
10032
+ className: "flex items-center gap-2",
10033
+ children: [
10034
+ bulkActions(selectedRows),
10035
+ jsx("button", {
10036
+ type: "button",
10037
+ onClick: selection.clearSelection,
10038
+ className: cn('text-sm text-neutral-500 hover:text-neutral-900', 'underline-offset-2 hover:underline', 'focus:outline-none focus-visible:underline'),
10039
+ children: labels.clearSelection
10040
+ })
10041
+ ]
10042
+ })
10043
+ ]
10044
+ }),
10045
+ pagination && jsx("div", {
10046
+ className: cn(v.paginationFooter(), slotProps == null ? void 0 : (_slotProps_pagination = slotProps.pagination) == null ? void 0 : _slotProps_pagination.className),
10047
+ children: jsx(Pagination, {
10048
+ page: pagination.page,
10049
+ pageSize: pagination.pageSize,
10050
+ totalCount: totalCountProp != null ? totalCountProp : sortedRows.length,
10051
+ onPageChange: pagination.onPageChange,
10052
+ onPageSizeChange: pagination.onPageSizeChange,
10053
+ pageSizeOptions: pagination.pageSizeOptions
10054
+ })
10055
+ })
10056
+ ]
10057
+ });
10058
+ }
10059
+ // ───── Header cell ─────
10060
+ function HeaderCell(props) {
10061
+ const { col, sortModel, onSortClick, inferredType, labels, className, buttonClassName, disabled } = props;
10062
+ const align = resolveAlign(col, inferredType);
10063
+ const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
10064
+ const sortEntry = sortModel.find((s)=>s.field === col.field);
10065
+ const ariaSort = (sortEntry == null ? void 0 : sortEntry.direction) === 'asc' ? 'ascending' : (sortEntry == null ? void 0 : sortEntry.direction) === 'desc' ? 'descending' : 'none';
10066
+ const headerContent = typeof col.header === 'function' ? col.header() : col.header;
10067
+ const cellStyle = {
10068
+ width: col.width != null ? `${col.width}px` : undefined,
10069
+ minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined,
10070
+ flex: col.flex
10071
+ };
10072
+ if (col.sortable) {
10073
+ return jsx("th", {
10074
+ scope: "col",
10075
+ "aria-sort": ariaSort,
10076
+ className: cn(className, alignClass),
10077
+ style: cellStyle,
10078
+ children: jsxs("button", {
10079
+ type: "button",
10080
+ onClick: onSortClick,
10081
+ disabled: disabled,
10082
+ "aria-label": ariaSort === 'ascending' ? labels.ariaSortAscending : ariaSort === 'descending' ? labels.ariaSortDescending : labels.ariaSortNone,
10083
+ className: cn(buttonClassName, align === 'right' && 'flex-row-reverse', 'disabled:cursor-not-allowed disabled:opacity-50'),
10084
+ children: [
10085
+ jsx("span", {
10086
+ children: headerContent
10087
+ }),
10088
+ jsx(SortIcon, {
10089
+ direction: sortEntry == null ? void 0 : sortEntry.direction
10090
+ })
10091
+ ]
10092
+ })
10093
+ });
10094
+ }
10095
+ return jsx("th", {
10096
+ scope: "col",
10097
+ className: cn(className, alignClass),
10098
+ style: cellStyle,
10099
+ children: headerContent
10100
+ });
10101
+ }
10102
+ // ───── Virtualized body ─────
10103
+ function renderVirtualizedBody(args) {
10104
+ const { vWindow, renderedRows, visibleCols, columnTypes, selection, rowSelection, showSelectionColumn, showRowActionsColumn, rowActions, getRowId, isInteractive, labels, classes } = args;
10105
+ return jsxs(Fragment$1, {
10106
+ children: [
10107
+ vWindow.paddingTop > 0 && jsx("tr", {
10108
+ "aria-hidden": "true",
10109
+ style: {
10110
+ height: vWindow.paddingTop
10111
+ }
10112
+ }),
10113
+ renderedRows.map((row, localIdx)=>{
10114
+ const absoluteIdx = vWindow.startIndex + localIdx;
10115
+ const rowId = getRowId(row, absoluteIdx);
10116
+ const isSelected = selection.isSelected(rowId);
10117
+ return jsxs("tr", {
10118
+ "data-selected": isSelected ? 'true' : 'false',
10119
+ "aria-selected": rowSelection !== 'none' ? isSelected : undefined,
10120
+ className: classes.row,
10121
+ children: [
10122
+ showSelectionColumn && jsx("td", {
10123
+ className: classes.selectionCell,
10124
+ children: jsx("input", {
10125
+ type: "checkbox",
10126
+ "aria-label": labels.ariaSelectRow,
10127
+ checked: isSelected,
10128
+ onChange: ()=>selection.toggleRow(rowId),
10129
+ disabled: !isInteractive,
10130
+ className: "cursor-pointer"
10131
+ })
10132
+ }),
10133
+ visibleCols.map((col)=>jsx(DataCell, {
10134
+ col: col,
10135
+ row: row,
10136
+ rowIndex: absoluteIdx,
10137
+ isSelected: isSelected,
10138
+ inferredType: columnTypes.get(col.field),
10139
+ cellClass: classes.cell,
10140
+ stickyLeftClass: classes.stickyLeftCell
10141
+ }, col.field)),
10142
+ showRowActionsColumn && rowActions && jsx("td", {
10143
+ className: classes.rowActionsCell,
10144
+ children: rowActions(row)
10145
+ })
10146
+ ]
10147
+ }, rowId);
10148
+ }),
10149
+ vWindow.paddingBottom > 0 && jsx("tr", {
10150
+ "aria-hidden": "true",
10151
+ style: {
10152
+ height: vWindow.paddingBottom
10153
+ }
10154
+ })
10155
+ ]
10156
+ });
10157
+ }
10158
+ // ───── Data cell ─────
10159
+ function DataCell(props) {
10160
+ const { col, row, rowIndex, isSelected, inferredType, cellClass, stickyLeftClass } = props;
10161
+ const value = getNestedValue(row, col.field);
10162
+ const align = resolveAlign(col, inferredType);
10163
+ const tabularNums = resolveTabularNums(col, inferredType);
10164
+ const monospace = resolveMonospace(col);
10165
+ const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
10166
+ const columnAccessState = col.access ? {
10167
+ hidden: col.access.onUnauthorized === 'hide',
10168
+ disabled: col.access.onUnauthorized === 'disable',
10169
+ readonly: col.access.onUnauthorized === 'readonly'
10170
+ } : null;
10171
+ const content = col.cellRenderer ? col.cellRenderer({
10172
+ row,
10173
+ value,
10174
+ rowIndex,
10175
+ isSelected,
10176
+ isExpanded: false,
10177
+ access: columnAccessState
10178
+ }) : renderDefaultCell(value);
10179
+ return jsx("td", {
10180
+ className: cn(cellClass, alignClass, tabularNums && 'tabular-nums', monospace && 'font-mono', col.sticky === 'left' && stickyLeftClass),
10181
+ style: {
10182
+ width: col.width != null ? `${col.width}px` : undefined,
10183
+ minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined
10184
+ },
10185
+ children: content
10186
+ });
10187
+ }
10188
+ // ───── Helpers ─────
10189
+ function renderDefaultCell(value) {
10190
+ if (value == null) return null;
10191
+ if (typeof value === 'boolean') return value ? '✓' : '✗';
10192
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
10193
+ if (typeof value === 'object') return JSON.stringify(value);
10194
+ return value;
10195
+ }
10196
+ function handleSortClick(col, event, current, setSortModel) {
10197
+ if (!col.sortable) return;
10198
+ const multi = event.shiftKey;
10199
+ setSortModel((prev)=>cycleSortFor(prev, col.field, multi));
10200
+ }
10201
+ function renderLoadingRows(args) {
10202
+ const { count, visibleCols, showSelectionColumn, showRowActionsColumn, rowHeight, rowClass, cellClass } = args;
10203
+ return Array.from({
10204
+ length: count
10205
+ }).map((_, i)=>jsxs("tr", {
10206
+ className: rowClass,
10207
+ "aria-busy": "true",
10208
+ style: {
10209
+ height: rowHeight
10210
+ },
10211
+ children: [
10212
+ showSelectionColumn && jsx("td", {
10213
+ className: cellClass,
10214
+ children: jsx(Skeleton, {
10215
+ variant: "rectangle",
10216
+ width: "16px",
10217
+ height: "16px"
10218
+ })
10219
+ }),
10220
+ visibleCols.map((col)=>jsx("td", {
10221
+ className: cellClass,
10222
+ children: jsx(Skeleton, {
10223
+ variant: "text",
10224
+ width: "80%"
10225
+ })
10226
+ }, col.field)),
10227
+ showRowActionsColumn && jsx("td", {
10228
+ className: cellClass,
10229
+ children: jsx(Skeleton, {
10230
+ variant: "rectangle",
10231
+ width: "24px",
10232
+ height: "24px"
10233
+ })
10234
+ })
10235
+ ]
10236
+ }, `loading-${i}`));
10237
+ }
10238
+ function renderEmptyState(args) {
10239
+ var _args_emptyState;
10240
+ return jsx("tr", {
10241
+ children: jsx("td", {
10242
+ colSpan: args.totalColumnCount,
10243
+ className: args.cellClass,
10244
+ children: (_args_emptyState = args.emptyState) != null ? _args_emptyState : args.fallback
10245
+ })
10246
+ });
10247
+ }
10248
+ // ───── Icons ─────
10249
+ function SortIcon(props) {
10250
+ return jsx("svg", {
10251
+ width: "0.75em",
10252
+ height: "0.75em",
10253
+ viewBox: "0 0 16 16",
10254
+ fill: "currentColor",
10255
+ "aria-hidden": "true",
10256
+ className: "shrink-0 opacity-60 group-hover:opacity-100",
10257
+ children: props.direction === 'asc' ? jsx("path", {
10258
+ d: "M8 4l4 6H4z"
10259
+ }) : props.direction === 'desc' ? jsx("path", {
10260
+ d: "M8 12L4 6h8z"
10261
+ }) : jsxs(Fragment$1, {
10262
+ children: [
10263
+ jsx("path", {
10264
+ d: "M8 3l3 4H5z",
10265
+ opacity: "0.5"
10266
+ }),
10267
+ jsx("path", {
10268
+ d: "M8 13l-3-4h6z",
10269
+ opacity: "0.5"
10270
+ })
10271
+ ]
10272
+ })
10273
+ });
10274
+ }
10275
+ function SearchIcon(props) {
10276
+ return jsxs("svg", {
10277
+ width: "0.875em",
10278
+ height: "0.875em",
10279
+ viewBox: "0 0 16 16",
10280
+ fill: "none",
10281
+ stroke: "currentColor",
10282
+ strokeWidth: "1.5",
10283
+ strokeLinecap: "round",
10284
+ strokeLinejoin: "round",
10285
+ "aria-hidden": "true",
10286
+ className: props.className,
10287
+ children: [
10288
+ jsx("circle", {
10289
+ cx: "7",
10290
+ cy: "7",
10291
+ r: "4.5"
10292
+ }),
10293
+ jsx("path", {
10294
+ d: "M11 11l3 3"
10295
+ })
10296
+ ]
10297
+ });
10298
+ }
10299
+
9396
10300
  /**
9397
10301
  * @dashforge/tw
9398
10302
  *
@@ -9420,6 +10324,6 @@ function RowActionMenuItem(props) {
9420
10324
  */ // ───── Components ─────
9421
10325
  /**
9422
10326
  * Package version (synced with `package.json` at publish time).
9423
- */ const VERSION = '0.6.0-beta';
10327
+ */ const VERSION = '0.7.0-beta';
9424
10328
 
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 };
10329
+ export { Accordion, AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, Container, DataGrid, DateTimePicker, Dialog, Divider, Grid, LeftNav, NumberField, OTPField, Pagination, Popover, RadioGroup, RenderButton, RenderChip, RenderText, RenderTwoLine, RowActionsMenu, Skeleton, SnackbarProvider, Stack, Switch, Table, Tabs, TextField, Textarea, Tooltip, TopBar, Typography, VERSION, VisuallyHidden, accordionVariants, appShellVariants, autocompleteVariants, boxVariants, breadcrumbsVariants, buttonVariants, checkboxVariants, cn, confirmDialogVariants, containerVariants, dataGridVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, getNestedValue, gridVariants, isoToInputValue, leftNavVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, radioGroupVariants, skeletonVariants, snackbarVariants, stackVariants, switchVariants, tableVariants, tabsVariants, textFieldVariants, textareaVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };