@exxatdesignux/ui 0.2.16 → 0.2.18

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 (111) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +149 -4
  3. package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +142 -0
  4. package/consumer-extras/cursor-skills/exxat-ds-skill/references/coach-marks.md +169 -0
  5. package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +382 -0
  6. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
  7. package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +56 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +19 -0
  9. package/consumer-extras/patterns/data-views-pattern.md +2 -0
  10. package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
  11. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +52 -0
  12. package/package.json +3 -3
  13. package/src/components/ui/banner.tsx +2 -0
  14. package/src/components/ui/chart.tsx +57 -2
  15. package/src/components/ui/sidebar.tsx +3 -2
  16. package/src/globals.css +65 -14
  17. package/src/theme.css +3 -3
  18. package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
  19. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
  20. package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
  21. package/template/AGENTS.md +27 -17
  22. package/template/app/(app)/data-list/page.tsx +2 -2
  23. package/template/app/(app)/error.tsx +22 -6
  24. package/template/app/(app)/layout.tsx +13 -6
  25. package/template/app/(app)/question-bank/layout.tsx +18 -5
  26. package/template/app/(app)/question-bank/new/page.tsx +58 -0
  27. package/template/app/global-error.tsx +63 -0
  28. package/template/app/globals.css +151 -14
  29. package/template/app/layout.tsx +43 -5
  30. package/template/components/app-sidebar.tsx +68 -33
  31. package/template/components/ask-leo-sidebar.tsx +0 -2
  32. package/template/components/brand-color-picker.tsx +344 -0
  33. package/template/components/compliance-list-view.tsx +33 -51
  34. package/template/components/compliance-table.tsx +4 -0
  35. package/template/components/data-table/index.tsx +99 -91
  36. package/template/components/data-table/pagination.tsx +0 -1
  37. package/template/components/data-table/types.ts +4 -1
  38. package/template/components/data-table/use-table-state.ts +276 -100
  39. package/template/components/data-views/data-row-list.tsx +183 -0
  40. package/template/components/data-views/index.ts +7 -3
  41. package/template/components/data-views/os-folder-glyph.tsx +8 -0
  42. package/template/components/dev-chunk-load-recovery.tsx +41 -0
  43. package/template/components/export-drawer.tsx +1 -1
  44. package/template/components/exxat-product-logo.tsx +168 -317
  45. package/template/components/invite-collaborators-drawer.tsx +5 -3
  46. package/template/components/key-metrics.tsx +122 -62
  47. package/template/components/new-placement-form.tsx +4 -2
  48. package/template/components/new-question-composer.tsx +2208 -0
  49. package/template/components/page-breadcrumb-trail.tsx +131 -0
  50. package/template/components/page-header.tsx +2 -1
  51. package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx} +2 -2
  52. package/template/components/placement-detail.tsx +1 -1
  53. package/template/components/placements-board-view.tsx +1 -1
  54. package/template/components/{data-list-client.tsx → placements-client.tsx} +9 -7
  55. package/template/components/placements-list-view.tsx +19 -133
  56. package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx} +2 -2
  57. package/template/components/{data-list-table-cells.tsx → placements-table-cells.tsx} +1 -1
  58. package/template/components/placements-table-columns.tsx +2 -2
  59. package/template/components/{data-list-table.tsx → placements-table.tsx} +42 -66
  60. package/template/components/product-switcher.tsx +24 -7
  61. package/template/components/product-wordmark.tsx +282 -0
  62. package/template/components/question-bank-client.tsx +20 -2
  63. package/template/components/question-bank-hub-client.tsx +105 -115
  64. package/template/components/question-bank-list-view.tsx +30 -54
  65. package/template/components/question-bank-new-folder-sheet.tsx +1 -1
  66. package/template/components/question-bank-secondary-nav.tsx +0 -3
  67. package/template/components/question-bank-table.tsx +19 -6
  68. package/template/components/rotations-empty-state.tsx +3 -0
  69. package/template/components/secondary-panel.tsx +23 -3
  70. package/template/components/settings-appearance-card.tsx +584 -141
  71. package/template/components/sidebar-shell.tsx +2 -1
  72. package/template/components/site-header.tsx +36 -31
  73. package/template/components/sites-list-view.tsx +31 -36
  74. package/template/components/sites-table.tsx +4 -0
  75. package/template/components/table-properties/drawer-button.tsx +38 -20
  76. package/template/components/table-properties/drawer.tsx +17 -14
  77. package/template/components/team-client.tsx +1 -1
  78. package/template/components/team-list-view.tsx +34 -50
  79. package/template/components/team-table.tsx +8 -3
  80. package/template/components/templates/list-page.tsx +12 -9
  81. package/template/components/templates/nested-secondary-panel-shell.tsx +10 -4
  82. package/template/components/ui/dot-pattern.tsx +50 -26
  83. package/template/components/ui/leo-icon.tsx +23 -3
  84. package/template/contexts/product-context.tsx +70 -7
  85. package/template/contexts/system-banner-context.tsx +112 -4
  86. package/template/docs/data-views-pattern.md +2 -0
  87. package/template/docs/kpi-flat-band-pattern.md +57 -0
  88. package/template/docs/kpi-strip-max-four-pattern.md +1 -0
  89. package/template/docs/shell-surface-elevation-pattern.md +52 -0
  90. package/template/eslint.config.mjs +18 -0
  91. package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
  92. package/template/lib/chunk-load-error.ts +13 -0
  93. package/template/lib/conditional-rule-match.ts +87 -22
  94. package/template/lib/data-list-persistence.ts +57 -257
  95. package/template/lib/data-list-view.ts +6 -0
  96. package/template/lib/dev-log.test.ts +6 -5
  97. package/template/lib/exxat-palette.json +1462 -0
  98. package/template/lib/exxat-palette.ts +136 -0
  99. package/template/lib/list-page-table-properties.ts +1 -1
  100. package/template/lib/list-status-badges.ts +1 -1
  101. package/template/lib/mailto.ts +29 -0
  102. package/template/lib/placement-board-card-layout.ts +1 -1
  103. package/template/lib/product-brand.ts +268 -0
  104. package/template/lib/question-bank-authoring.ts +308 -0
  105. package/template/lib/question-bank-nav.ts +44 -0
  106. package/template/lib/raf-throttle.ts +45 -0
  107. package/template/lib/sidebar-state-cookie.ts +9 -0
  108. package/template/lib/table-state-lifecycle.ts +521 -0
  109. package/template/next.config.mjs +156 -0
  110. package/template/package.json +3 -3
  111. package/template/stores/app-store.ts +46 -1
@@ -27,6 +27,7 @@ import * as React from "react"
27
27
  import { useTheme } from "next-themes"
28
28
  import { createPortal } from "react-dom"
29
29
  import { cn } from "@/lib/utils"
30
+ import { rafThrottle } from "@/lib/raf-throttle"
30
31
  import { Button } from "@/components/ui/button"
31
32
  import { Input } from "@/components/ui/input"
32
33
  import { Kbd, KbdGroup } from "@/components/ui/kbd"
@@ -55,7 +56,8 @@ import {
55
56
  TooltipTrigger,
56
57
  } from "@/components/ui/tooltip"
57
58
  import { OPERATOR_LABELS } from "@/components/table-properties/types"
58
- import type { ActiveFilter, FilterTextMask } from "@/components/table-properties/types"
59
+ import type { ActiveFilter } from "@/components/table-properties/types"
60
+ import { getConditionalCellBackground } from "@/lib/conditional-rule-match"
59
61
  import { formatYmdForDisplay } from "@/lib/date-filter"
60
62
  import { FilterDateCalendar } from "@/components/data-table/filter-date-calendar"
61
63
  import { FilterTextValueInput } from "@/components/data-table/filter-text-value-input"
@@ -80,26 +82,6 @@ function resolvedColumnLabel<TData>(col: ColumnDef<TData>): string {
80
82
  return defaultColumnHeaderLabel(col.key) ?? col.key
81
83
  }
82
84
 
83
- function conditionalTextMatches(
84
- cellVal: string,
85
- needle: string,
86
- op: "contains" | "not_contains",
87
- textMask: FilterTextMask | undefined,
88
- ) {
89
- const v = cellVal.trim()
90
- const n = needle.trim()
91
- if (!n) return op === "not_contains"
92
- if (textMask === "phone" || textMask === "zip") {
93
- const nd = n.replace(/\D/g, "")
94
- const hay = v.replace(/\D/g, "")
95
- if (!nd) return op === "not_contains"
96
- const hit = hay.includes(nd)
97
- return op === "contains" ? hit : !hit
98
- }
99
- const hit = v.toLowerCase().includes(n.toLowerCase())
100
- return op === "contains" ? hit : !hit
101
- }
102
-
103
85
  // ─────────────────────────────────────────────────────────────────────────────
104
86
  // Internal sub-components
105
87
  // ─────────────────────────────────────────────────────────────────────────────
@@ -734,23 +716,29 @@ function useBulkBarFixedToTableScrollEl(
734
716
  })
735
717
  }
736
718
  apply()
737
- const ro = new ResizeObserver(() => {
738
- requestAnimationFrame(apply)
739
- })
719
+ // rAF-coalesce so a single frame handles bursts of capture-phase scroll
720
+ // events plus the ResizeObserver firing — instead of N getBoundingClientRect
721
+ // + setState per second.
722
+ const scheduled = rafThrottle(apply)
723
+ const ro = new ResizeObserver(scheduled)
740
724
  ro.observe(el)
741
- window.addEventListener("resize", apply)
742
- window.addEventListener("scroll", apply, true)
725
+ window.addEventListener("resize", scheduled, { passive: true })
726
+ window.addEventListener("scroll", scheduled, { passive: true, capture: true })
743
727
  return () => {
728
+ scheduled.cancel()
744
729
  ro.disconnect()
745
- window.removeEventListener("resize", apply)
746
- window.removeEventListener("scroll", apply, true)
730
+ window.removeEventListener("resize", scheduled)
731
+ window.removeEventListener("scroll", scheduled, { capture: true })
747
732
  }
748
733
  }, [active, fullWidth, scrollRef])
749
734
  return style
750
735
  }
751
736
 
752
737
  function DataTableInner<TData extends Record<string, unknown>>({
753
- data,
738
+ // `data` / `defaultSort` flow into `useTableState` upstream; the inner table
739
+ // reads them via `state` and never directly here. Keep the prop slots so
740
+ // the public `DataTable<TData>` API stays unchanged.
741
+ data: _data,
754
742
  columns,
755
743
  getRowId: getRowIdProp,
756
744
  getRowSelectionLabel,
@@ -758,7 +746,7 @@ function DataTableInner<TData extends Record<string, unknown>>({
758
746
  searchable = true,
759
747
  emptyState,
760
748
  onRowClick,
761
- defaultSort,
749
+ defaultSort: _defaultSort,
762
750
  toolbarSlot,
763
751
  bulkActionsSlot,
764
752
  addRowLabel = false,
@@ -769,7 +757,7 @@ function DataTableInner<TData extends Record<string, unknown>>({
769
757
  state,
770
758
  }: DataTableInnerProps<TData>) {
771
759
  const {
772
- sortRules, setSortRules,
760
+ setSortRules,
773
761
  sortKey, sortDir,
774
762
  handleSortByKey,
775
763
  addFilter,
@@ -777,7 +765,6 @@ function DataTableInner<TData extends Record<string, unknown>>({
777
765
  colMenuSearch, setColMenuSearch,
778
766
  selected, setSelected, toggleRow, toggleAll, getRowId,
779
767
  colWidths, startResize,
780
- colOrder,
781
768
  colPins, lockedPins,
782
769
  pinColumn, unpinColumn,
783
770
  colWrap, toggleWrap,
@@ -785,7 +772,7 @@ function DataTableInner<TData extends Record<string, unknown>>({
785
772
  handleDragStart, handleDragOver, handleDrop, handleDragEnd,
786
773
  scrollRef, handleScroll, checkOverflow,
787
774
  isOverflowing,
788
- hoveredRow, setHoveredRow,
775
+ setHoveredRow,
789
776
  rows, pagedRows, groupedRows,
790
777
  effectivePins, displayCols,
791
778
  isReflowViewport,
@@ -796,12 +783,19 @@ function DataTableInner<TData extends Record<string, unknown>>({
796
783
  setSheetOpen,
797
784
  } = state
798
785
 
799
- // Mount overflow check
786
+ // Mount overflow check + scrollport width for sticky group headers on horizontal scroll.
800
787
  React.useEffect(() => {
801
- checkOverflow()
788
+ const syncScrollport = () => {
789
+ const el = scrollRef.current
790
+ if (el) {
791
+ el.style.setProperty("--dt-scrollport-width", `${el.clientWidth}px`)
792
+ }
793
+ checkOverflow()
794
+ }
795
+ syncScrollport()
802
796
  const el = scrollRef.current
803
797
  if (!el) return
804
- const ro = new ResizeObserver(checkOverflow)
798
+ const ro = new ResizeObserver(syncScrollport)
805
799
  ro.observe(el)
806
800
  return () => ro.disconnect()
807
801
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -843,6 +837,24 @@ function DataTableInner<TData extends Record<string, unknown>>({
843
837
  const lastLeftPinKey = [...displayCols].reverse().find(c => effectivePins[c.key] === "left")?.key
844
838
  const firstRightPinKey = displayCols.find(c => effectivePins[c.key] === "right")?.key
845
839
 
840
+ function floatingHeaderPinnedStyle(key: string): React.CSSProperties | undefined {
841
+ const pin = effectivePins[key]
842
+ if (!pin) return undefined
843
+
844
+ const visibleWidth =
845
+ typeof floatingHeaderStyle?.width === "number"
846
+ ? floatingHeaderStyle.width
847
+ : tableWrapRef.current?.clientWidth ?? floatingHeaderTableWidth
848
+ const maxScroll = Math.max(0, floatingHeaderTableWidth - visibleWidth)
849
+ const translateX = pin === "left"
850
+ ? headerScrollLeft
851
+ : headerScrollLeft - maxScroll
852
+
853
+ // The floating sticky header is horizontally translated as one table.
854
+ // Counter-translate pinned header cells so they remain locked to the viewport edge.
855
+ return { position: "relative", transform: `translateX(${translateX}px)` }
856
+ }
857
+
846
858
  // Row IDs for the current visible rows
847
859
  const allRowIds = rows.map((r, i) => getRowId(r, i, getRowIdProp))
848
860
  const allSelected = rows.length > 0 && selected.size === rows.length
@@ -864,6 +876,7 @@ function DataTableInner<TData extends Record<string, unknown>>({
864
876
  const [headerIsStuck, setHeaderIsStuck] = React.useState(false)
865
877
  const [headerScrollLeft, setHeaderScrollLeft] = React.useState(0)
866
878
  const [floatingHeaderStyle, setFloatingHeaderStyle] = React.useState<React.CSSProperties | undefined>(undefined)
879
+ const [floatingHeaderTableWidth, setFloatingHeaderTableWidth] = React.useState(totalWidth)
867
880
  const [isClient, setIsClient] = React.useState(false)
868
881
 
869
882
  React.useEffect(() => {
@@ -888,11 +901,16 @@ function DataTableInner<TData extends Record<string, unknown>>({
888
901
  }
889
902
 
890
903
  update()
891
- window.addEventListener("scroll", update, { passive: true, capture: true })
892
- window.addEventListener("resize", update)
904
+ // rAF-coalesce: capture-phase scroll fires for every ancestor (sidebar,
905
+ // dashboard panels, anchored sheets), so a single getBoundingClientRect
906
+ // per frame is more than enough to keep the sticky header aligned.
907
+ const scheduled = rafThrottle(update)
908
+ window.addEventListener("scroll", scheduled, { passive: true, capture: true })
909
+ window.addEventListener("resize", scheduled, { passive: true })
893
910
  return () => {
894
- window.removeEventListener("scroll", update, true)
895
- window.removeEventListener("resize", update)
911
+ scheduled.cancel()
912
+ window.removeEventListener("scroll", scheduled, { capture: true })
913
+ window.removeEventListener("resize", scheduled)
896
914
  }
897
915
  }, [showColumnHeaders, rows.length, displayCols.length])
898
916
 
@@ -915,6 +933,11 @@ function DataTableInner<TData extends Record<string, unknown>>({
915
933
  const borderLeft = parseFloat(cs.borderLeftWidth) || 0
916
934
  const borderRight = parseFloat(cs.borderRightWidth) || 0
917
935
  const visibleWidth = Math.max(0, wrapEl.clientWidth - borderLeft - borderRight)
936
+ const renderedTableWidth = Math.max(
937
+ totalWidth,
938
+ visibleWidth,
939
+ wrapEl.querySelector("table")?.getBoundingClientRect().width ?? 0,
940
+ )
918
941
  setFloatingHeaderStyle({
919
942
  position: "fixed",
920
943
  top: headerOffset,
@@ -922,18 +945,21 @@ function DataTableInner<TData extends Record<string, unknown>>({
922
945
  width: visibleWidth,
923
946
  zIndex: 50,
924
947
  })
948
+ setFloatingHeaderTableWidth(renderedTableWidth)
925
949
  setHeaderScrollLeft(wrapEl.scrollLeft)
926
950
  }
927
951
 
928
952
  apply()
929
- const ro = new ResizeObserver(() => requestAnimationFrame(apply))
953
+ const scheduled = rafThrottle(apply)
954
+ const ro = new ResizeObserver(scheduled)
930
955
  ro.observe(wrapEl)
931
- window.addEventListener("scroll", apply, true)
932
- window.addEventListener("resize", apply)
956
+ window.addEventListener("scroll", scheduled, { passive: true, capture: true })
957
+ window.addEventListener("resize", scheduled, { passive: true })
933
958
  return () => {
959
+ scheduled.cancel()
934
960
  ro.disconnect()
935
- window.removeEventListener("scroll", apply, true)
936
- window.removeEventListener("resize", apply)
961
+ window.removeEventListener("scroll", scheduled, { capture: true })
962
+ window.removeEventListener("resize", scheduled)
937
963
  }
938
964
  }, [headerIsStuck, showColumnHeaders, totalWidth, displayCols.length])
939
965
 
@@ -968,7 +994,7 @@ function DataTableInner<TData extends Record<string, unknown>>({
968
994
  <div style={{ transform: `translateX(${-headerScrollLeft}px)` }}>
969
995
  <table
970
996
  className="w-full text-sm border-separate border-spacing-0"
971
- style={{ tableLayout: "fixed", width: totalWidth }}
997
+ style={{ tableLayout: "fixed", width: floatingHeaderTableWidth }}
972
998
  >
973
999
  <colgroup>
974
1000
  {displayCols.map(col => (
@@ -984,6 +1010,7 @@ function DataTableInner<TData extends Record<string, unknown>>({
984
1010
  <th
985
1011
  key={col.key}
986
1012
  scope="col"
1013
+ style={floatingHeaderPinnedStyle(col.key)}
987
1014
  className={cn(
988
1015
  "h-9 px-3 text-left align-middle select-none",
989
1016
  "text-xs font-medium text-muted-foreground tracking-wide",
@@ -992,6 +1019,8 @@ function DataTableInner<TData extends Record<string, unknown>>({
992
1019
  ? "border-r border-border last:border-r-0"
993
1020
  : "last:border-r-0"),
994
1021
  isPinned ? "z-40" : "z-30",
1022
+ isPinned && "relative",
1023
+ isEdgePinCol && stickyShadow(effectivePins[col.key]),
995
1024
  )}
996
1025
  >
997
1026
  <div className="flex items-center justify-between gap-1 min-w-0">
@@ -1058,7 +1087,11 @@ function DataTableInner<TData extends Record<string, unknown>>({
1058
1087
  >
1059
1088
  <table
1060
1089
  className="w-full text-sm border-separate border-spacing-0"
1061
- style={{ tableLayout: "fixed", minWidth: totalWidth }}
1090
+ style={{
1091
+ tableLayout: "fixed",
1092
+ minWidth: totalWidth,
1093
+ width: headerIsStuck ? floatingHeaderTableWidth : undefined,
1094
+ }}
1062
1095
  >
1063
1096
  <colgroup>
1064
1097
  {displayCols.map(col => (
@@ -1306,25 +1339,25 @@ function DataTableInner<TData extends Record<string, unknown>>({
1306
1339
  <React.Fragment key={groupKey ?? "__all__"}>
1307
1340
  {groupLabel && (
1308
1341
  <tr>
1309
- <td
1310
- colSpan={displayCols.length}
1311
- className={cn(
1312
- "px-4 py-1.5 text-xs font-semibold text-muted-foreground tracking-wide bg-dt-group-bg select-none",
1313
- !isReflowViewport && "sticky left-0",
1314
- "border-b border-border",
1315
- )}
1316
- >
1317
- {groupLabel}
1318
- <span className="ml-2 font-normal normal-case opacity-60 tracking-normal">
1319
- {groupRows.length} record{groupRows.length !== 1 ? "s" : ""}
1320
- </span>
1342
+ <td colSpan={displayCols.length} className="p-0 border-b border-border bg-dt-group-bg">
1343
+ <div
1344
+ className={cn(
1345
+ "sticky left-0 z-[25] px-4 py-1.5 text-xs font-semibold text-muted-foreground tracking-wide bg-dt-group-bg select-none",
1346
+ !isReflowViewport && "shadow-[4px_0_8px_-4px_var(--sticky-edge-fade)]",
1347
+ )}
1348
+ style={{ width: "var(--dt-scrollport-width, 100%)" }}
1349
+ >
1350
+ {groupLabel}
1351
+ <span className="ml-2 font-normal normal-case opacity-60 tracking-normal">
1352
+ {groupRows.length} record{groupRows.length !== 1 ? "s" : ""}
1353
+ </span>
1354
+ </div>
1321
1355
  </td>
1322
1356
  </tr>
1323
1357
  )}
1324
1358
  {groupRows.map((row, rowIndex) => {
1325
1359
  const rowId = getRowId(row, rowIndex, getRowIdProp)
1326
1360
  const isSelected = selected.has(rowId)
1327
- const isHovered = hoveredRow === rowId
1328
1361
  const rowClickable = Boolean(onRowClick) || selectable
1329
1362
  function handleRowClick(e: React.MouseEvent<HTMLTableRowElement>) {
1330
1363
  if (!rowClickable) return
@@ -1375,37 +1408,12 @@ function DataTableInner<TData extends Record<string, unknown>>({
1375
1408
  ]
1376
1409
  )
1377
1410
 
1378
- // Conditional rule background for this cell
1379
- const conditionalBg = conditionalRules?.find(rule => {
1380
- if (rule.fieldKey !== col.key) return false
1381
- const cellVal = String(row[rule.fieldKey as keyof TData] ?? "")
1382
- const v = cellVal.trim()
1383
- const ruleCol = columns.find(c => c.key === rule.fieldKey)
1384
- const textMask =
1385
- ruleCol?.filter?.type === "text" ? ruleCol.filter.textMask : undefined
1386
- switch (rule.operator) {
1387
- case "is":
1388
- return rule.values.length > 0 && rule.values.includes(v)
1389
- case "is_not":
1390
- return rule.values.length > 0 && !rule.values.includes(v)
1391
- case "contains":
1392
- return (
1393
- rule.values.length > 0 &&
1394
- rule.values.some(val =>
1395
- conditionalTextMatches(v, val, "contains", textMask),
1396
- )
1397
- )
1398
- case "not_contains":
1399
- return (
1400
- rule.values.length > 0 &&
1401
- !rule.values.some(val =>
1402
- conditionalTextMatches(v, val, "contains", textMask),
1403
- )
1404
- )
1405
- default:
1406
- return false
1407
- }
1408
- })?.bgColor
1411
+ const conditionalBg = getConditionalCellBackground(
1412
+ row,
1413
+ col.key,
1414
+ conditionalRules,
1415
+ columns,
1416
+ )
1409
1417
 
1410
1418
  const tdStyle = conditionalBg
1411
1419
  ? { ...cs, background: conditionalBg }
@@ -201,7 +201,6 @@ export function DataTablePaginated<TData extends Record<string, unknown>>({
201
201
  {originalToolbarSlot ? originalToolbarSlot(state) : null}
202
202
  </>
203
203
  ),
204
- // eslint-disable-next-line react-hooks/exhaustive-deps
205
204
  [originalToolbarSlot],
206
205
  )
207
206
 
@@ -49,7 +49,10 @@ export interface ColumnDef<TData> {
49
49
  }
50
50
  }
51
51
 
52
- export interface CellContext<TData> {
52
+ // `TData` is part of the public surface so callers can write
53
+ // `CellContext<Placement>` for symmetry with column-def renderers, even
54
+ // though the interface body doesn't currently reference it.
55
+ export interface CellContext<_TData> {
53
56
  rowIndex: number
54
57
  selected: boolean
55
58
  onSelect: (selected: boolean) => void