@multiplatform.one/table 6.4.3 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/dist/cjs/components/ChildTable/index.native.js +1 -1
  2. package/dist/cjs/components/ChildTable/index.native.js.map +1 -1
  3. package/dist/cjs/components/ColumnHeaderFilter/index.cjs +1 -0
  4. package/dist/cjs/components/ColumnHeaderFilter/index.native.js +2 -1
  5. package/dist/cjs/components/ColumnHeaderFilter/index.native.js.map +1 -1
  6. package/dist/cjs/components/DataTable/DataTableCardControls.native.js +1 -1
  7. package/dist/cjs/components/DataTable/DataTableCardControls.native.js.map +1 -1
  8. package/dist/cjs/components/DataTable/DataTableContext.native.js.map +1 -1
  9. package/dist/cjs/components/DataTable/DataTableMobileCards.cjs +9 -3
  10. package/dist/cjs/components/DataTable/DataTableMobileCards.native.js +12 -3
  11. package/dist/cjs/components/DataTable/DataTableMobileCards.native.js.map +1 -1
  12. package/dist/cjs/components/DataTable/DataTableRoot.cjs +133 -13
  13. package/dist/cjs/components/DataTable/DataTableRoot.native.js +166 -20
  14. package/dist/cjs/components/DataTable/DataTableRoot.native.js.map +1 -1
  15. package/dist/cjs/components/DataTable/DataTableStateContext.native.js.map +1 -1
  16. package/dist/cjs/components/DataTable/DataTableVirtualized.cjs +3 -1
  17. package/dist/cjs/components/DataTable/DataTableVirtualized.native.js +3 -1
  18. package/dist/cjs/components/DataTable/DataTableVirtualized.native.js.map +1 -1
  19. package/dist/cjs/components/DataTable/index.native.js +49 -12
  20. package/dist/cjs/components/DataTable/index.native.js.map +1 -1
  21. package/dist/cjs/components/DataTableBulkActions/index.cjs +16 -1
  22. package/dist/cjs/components/DataTableBulkActions/index.native.js +20 -1
  23. package/dist/cjs/components/DataTableBulkActions/index.native.js.map +1 -1
  24. package/dist/cjs/components/DataTableCell/CellModalEditor.cjs +4 -8
  25. package/dist/cjs/components/DataTableCell/CellOptionsEditor.native.js.map +1 -1
  26. package/dist/cjs/components/DataTableCell/index.cjs +1 -33
  27. package/dist/cjs/components/DataTableCell/index.native.js +1 -34
  28. package/dist/cjs/components/DataTableCell/index.native.js.map +1 -1
  29. package/dist/cjs/components/DataTableColumnPinning/index.native.js.map +1 -1
  30. package/dist/cjs/components/DataTableDateFilter/index.native.js.map +1 -1
  31. package/dist/cjs/components/DataTableExport/index.cjs +1 -0
  32. package/dist/cjs/components/DataTableExport/index.native.js +1 -0
  33. package/dist/cjs/components/DataTableExport/index.native.js.map +1 -1
  34. package/dist/cjs/components/DataTableFilterButton/index.cjs +1 -1
  35. package/dist/cjs/components/DataTableFilterButton/index.native.js +5 -2
  36. package/dist/cjs/components/DataTableFilterButton/index.native.js.map +1 -1
  37. package/dist/cjs/components/DataTableFilterChips/index.native.js.map +1 -1
  38. package/dist/cjs/components/DataTableFilters/index.native.js.map +1 -1
  39. package/dist/cjs/components/DataTableGrouping/index.cjs +1 -0
  40. package/dist/cjs/components/DataTableGrouping/index.native.js +1 -0
  41. package/dist/cjs/components/DataTableGrouping/index.native.js.map +1 -1
  42. package/dist/cjs/components/DataTablePagination/index.native.js.map +1 -1
  43. package/dist/cjs/components/DataTableSearch/index.native.js.map +1 -1
  44. package/dist/cjs/components/DataTableToolbar/index.cjs +1 -0
  45. package/dist/cjs/components/DataTableToolbar/index.native.js +2 -1
  46. package/dist/cjs/components/DataTableToolbar/index.native.js.map +1 -1
  47. package/dist/cjs/components/DataTableViewOptions/index.native.js.map +1 -1
  48. package/dist/cjs/components/FilterManager.cjs +18 -14
  49. package/dist/cjs/components/FilterManager.native.js +18 -14
  50. package/dist/cjs/components/FilterManager.native.js.map +1 -1
  51. package/dist/cjs/components/FilterOperatorValueEditor/index.native.js.map +1 -1
  52. package/dist/cjs/components/KnobDialog.native.js.map +1 -1
  53. package/dist/cjs/components/SavedFilterDropdown.native.js.map +1 -1
  54. package/dist/cjs/components/SimpleTable/index.native.js.map +1 -1
  55. package/dist/cjs/components/TableInput/index.native.js +4 -1
  56. package/dist/cjs/components/TableInput/index.native.js.map +1 -1
  57. package/dist/cjs/components/TableInput/shared.native.js.map +1 -1
  58. package/dist/cjs/hooks/useDataTable.cjs +17 -1
  59. package/dist/cjs/hooks/useDataTable.native.js +19 -1
  60. package/dist/cjs/hooks/useDataTable.native.js.map +1 -1
  61. package/dist/cjs/hooks/useDataTableKeyboard.native.js.map +1 -1
  62. package/dist/cjs/hooks/useDragScroll.native.js.map +1 -1
  63. package/dist/cjs/hooks/useInfiniteRows.native.js.map +1 -1
  64. package/dist/cjs/hooks/useSavedFilters.native.js.map +1 -1
  65. package/dist/cjs/hooks/useTableColumnPinning.native.js.map +1 -1
  66. package/dist/cjs/hooks/useTableFiltering.native.js.map +1 -1
  67. package/dist/cjs/hooks/useTableGrouping.native.js.map +1 -1
  68. package/dist/cjs/hooks/useTableKeyboard.native.js.map +1 -1
  69. package/dist/cjs/hooks/useTablePagination.native.js.map +1 -1
  70. package/dist/cjs/hooks/useTableSearch.native.js.map +1 -1
  71. package/dist/cjs/hooks/useTableSelection.native.js.map +1 -1
  72. package/dist/cjs/hooks/useTableSorting.native.js.map +1 -1
  73. package/dist/cjs/hooks/useTableUrlSync.native.js.map +1 -1
  74. package/dist/cjs/hooks/useTableViews.native.js.map +1 -1
  75. package/dist/cjs/shared/t.native.js.map +1 -1
  76. package/dist/cjs/shims/primitives.native.js.map +1 -1
  77. package/dist/cjs/shims/tableCellContext.native.js.map +1 -1
  78. package/dist/cjs/types.native.js.map +1 -1
  79. package/dist/cjs/utils/columnAlign.native.js.map +1 -1
  80. package/dist/cjs/utils/filterFields.native.js.map +1 -1
  81. package/dist/cjs/utils/filterUtils.native.js.map +1 -1
  82. package/dist/cjs/utils/paginationDefaults.native.js.map +1 -1
  83. package/dist/cjs/utils/rowPress.cjs +44 -0
  84. package/dist/cjs/utils/rowPress.native.js +56 -0
  85. package/dist/cjs/utils/rowPress.native.js.map +1 -0
  86. package/dist/cjs/utils/urlStateUtils.native.js.map +1 -1
  87. package/dist/cjs/utils/viewData.native.js.map +1 -1
  88. package/dist/cjs/utils/withInterp.native.js.map +1 -1
  89. package/dist/esm/components/ColumnHeaderFilter/index.mjs +1 -0
  90. package/dist/esm/components/ColumnHeaderFilter/index.mjs.map +1 -1
  91. package/dist/esm/components/ColumnHeaderFilter/index.native.js +1 -0
  92. package/dist/esm/components/ColumnHeaderFilter/index.native.js.map +1 -1
  93. package/dist/esm/components/DataTable/DataTableMobileCards.mjs +9 -3
  94. package/dist/esm/components/DataTable/DataTableMobileCards.mjs.map +1 -1
  95. package/dist/esm/components/DataTable/DataTableMobileCards.native.js +12 -3
  96. package/dist/esm/components/DataTable/DataTableMobileCards.native.js.map +1 -1
  97. package/dist/esm/components/DataTable/DataTableRoot.mjs +133 -13
  98. package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
  99. package/dist/esm/components/DataTable/DataTableRoot.native.js +159 -13
  100. package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
  101. package/dist/esm/components/DataTable/DataTableVirtualized.mjs +3 -1
  102. package/dist/esm/components/DataTable/DataTableVirtualized.mjs.map +1 -1
  103. package/dist/esm/components/DataTable/DataTableVirtualized.native.js +3 -1
  104. package/dist/esm/components/DataTable/DataTableVirtualized.native.js.map +1 -1
  105. package/dist/esm/components/DataTable/index.native.js +39 -2
  106. package/dist/esm/components/DataTable/index.native.js.map +1 -1
  107. package/dist/esm/components/DataTableBulkActions/index.mjs +16 -1
  108. package/dist/esm/components/DataTableBulkActions/index.mjs.map +1 -1
  109. package/dist/esm/components/DataTableBulkActions/index.native.js +20 -1
  110. package/dist/esm/components/DataTableBulkActions/index.native.js.map +1 -1
  111. package/dist/esm/components/DataTableCell/CellModalEditor.mjs +4 -8
  112. package/dist/esm/components/DataTableCell/CellModalEditor.mjs.map +1 -1
  113. package/dist/esm/components/DataTableCell/index.mjs +1 -33
  114. package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
  115. package/dist/esm/components/DataTableCell/index.native.js +1 -34
  116. package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
  117. package/dist/esm/components/DataTableExport/index.mjs +1 -0
  118. package/dist/esm/components/DataTableExport/index.mjs.map +1 -1
  119. package/dist/esm/components/DataTableExport/index.native.js +1 -0
  120. package/dist/esm/components/DataTableExport/index.native.js.map +1 -1
  121. package/dist/esm/components/DataTableFilterButton/index.mjs +1 -1
  122. package/dist/esm/components/DataTableFilterButton/index.mjs.map +1 -1
  123. package/dist/esm/components/DataTableFilterButton/index.native.js +4 -1
  124. package/dist/esm/components/DataTableFilterButton/index.native.js.map +1 -1
  125. package/dist/esm/components/DataTableGrouping/index.mjs +1 -0
  126. package/dist/esm/components/DataTableGrouping/index.mjs.map +1 -1
  127. package/dist/esm/components/DataTableGrouping/index.native.js +1 -0
  128. package/dist/esm/components/DataTableGrouping/index.native.js.map +1 -1
  129. package/dist/esm/components/DataTableToolbar/index.mjs +1 -0
  130. package/dist/esm/components/DataTableToolbar/index.mjs.map +1 -1
  131. package/dist/esm/components/DataTableToolbar/index.native.js +1 -0
  132. package/dist/esm/components/DataTableToolbar/index.native.js.map +1 -1
  133. package/dist/esm/components/FilterManager.mjs +18 -14
  134. package/dist/esm/components/FilterManager.mjs.map +1 -1
  135. package/dist/esm/components/FilterManager.native.js +18 -14
  136. package/dist/esm/components/FilterManager.native.js.map +1 -1
  137. package/dist/esm/components/TableInput/index.native.js +3 -0
  138. package/dist/esm/components/TableInput/index.native.js.map +1 -1
  139. package/dist/esm/hooks/useDataTable.mjs +17 -1
  140. package/dist/esm/hooks/useDataTable.mjs.map +1 -1
  141. package/dist/esm/hooks/useDataTable.native.js +19 -1
  142. package/dist/esm/hooks/useDataTable.native.js.map +1 -1
  143. package/dist/esm/utils/rowPress.mjs +19 -0
  144. package/dist/esm/utils/rowPress.mjs.map +1 -0
  145. package/dist/esm/utils/rowPress.native.js +28 -0
  146. package/dist/esm/utils/rowPress.native.js.map +1 -0
  147. package/dist/jsx/components/ColumnHeaderFilter/index.mjs +1 -0
  148. package/dist/jsx/components/ColumnHeaderFilter/index.mjs.map +1 -1
  149. package/dist/jsx/components/ColumnHeaderFilter/index.native.js +1 -0
  150. package/dist/jsx/components/ColumnHeaderFilter/index.native.js.map +1 -1
  151. package/dist/jsx/components/DataTable/DataTableMobileCards.mjs +9 -3
  152. package/dist/jsx/components/DataTable/DataTableMobileCards.mjs.map +1 -1
  153. package/dist/jsx/components/DataTable/DataTableMobileCards.native.js +12 -3
  154. package/dist/jsx/components/DataTable/DataTableMobileCards.native.js.map +1 -1
  155. package/dist/jsx/components/DataTable/DataTableRoot.mjs +133 -13
  156. package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
  157. package/dist/jsx/components/DataTable/DataTableRoot.native.js +159 -13
  158. package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
  159. package/dist/jsx/components/DataTable/DataTableVirtualized.mjs +3 -1
  160. package/dist/jsx/components/DataTable/DataTableVirtualized.mjs.map +1 -1
  161. package/dist/jsx/components/DataTable/DataTableVirtualized.native.js +3 -1
  162. package/dist/jsx/components/DataTable/DataTableVirtualized.native.js.map +1 -1
  163. package/dist/jsx/components/DataTable/index.native.js +39 -2
  164. package/dist/jsx/components/DataTable/index.native.js.map +1 -1
  165. package/dist/jsx/components/DataTableBulkActions/index.mjs +16 -1
  166. package/dist/jsx/components/DataTableBulkActions/index.mjs.map +1 -1
  167. package/dist/jsx/components/DataTableBulkActions/index.native.js +20 -1
  168. package/dist/jsx/components/DataTableBulkActions/index.native.js.map +1 -1
  169. package/dist/jsx/components/DataTableCell/CellModalEditor.mjs +4 -8
  170. package/dist/jsx/components/DataTableCell/CellModalEditor.mjs.map +1 -1
  171. package/dist/jsx/components/DataTableCell/index.mjs +1 -33
  172. package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
  173. package/dist/jsx/components/DataTableCell/index.native.js +1 -34
  174. package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
  175. package/dist/jsx/components/DataTableExport/index.mjs +1 -0
  176. package/dist/jsx/components/DataTableExport/index.mjs.map +1 -1
  177. package/dist/jsx/components/DataTableExport/index.native.js +1 -0
  178. package/dist/jsx/components/DataTableExport/index.native.js.map +1 -1
  179. package/dist/jsx/components/DataTableFilterButton/index.mjs +1 -1
  180. package/dist/jsx/components/DataTableFilterButton/index.mjs.map +1 -1
  181. package/dist/jsx/components/DataTableFilterButton/index.native.js +4 -1
  182. package/dist/jsx/components/DataTableFilterButton/index.native.js.map +1 -1
  183. package/dist/jsx/components/DataTableGrouping/index.mjs +1 -0
  184. package/dist/jsx/components/DataTableGrouping/index.mjs.map +1 -1
  185. package/dist/jsx/components/DataTableGrouping/index.native.js +1 -0
  186. package/dist/jsx/components/DataTableGrouping/index.native.js.map +1 -1
  187. package/dist/jsx/components/DataTableToolbar/index.mjs +1 -0
  188. package/dist/jsx/components/DataTableToolbar/index.mjs.map +1 -1
  189. package/dist/jsx/components/DataTableToolbar/index.native.js +1 -0
  190. package/dist/jsx/components/DataTableToolbar/index.native.js.map +1 -1
  191. package/dist/jsx/components/FilterManager.mjs +18 -14
  192. package/dist/jsx/components/FilterManager.mjs.map +1 -1
  193. package/dist/jsx/components/FilterManager.native.js +18 -14
  194. package/dist/jsx/components/FilterManager.native.js.map +1 -1
  195. package/dist/jsx/components/TableInput/index.native.js +3 -0
  196. package/dist/jsx/components/TableInput/index.native.js.map +1 -1
  197. package/dist/jsx/hooks/useDataTable.mjs +17 -1
  198. package/dist/jsx/hooks/useDataTable.mjs.map +1 -1
  199. package/dist/jsx/hooks/useDataTable.native.js +19 -1
  200. package/dist/jsx/hooks/useDataTable.native.js.map +1 -1
  201. package/dist/jsx/utils/rowPress.mjs +19 -0
  202. package/dist/jsx/utils/rowPress.mjs.map +1 -0
  203. package/dist/jsx/utils/rowPress.native.js +56 -0
  204. package/dist/jsx/utils/rowPress.native.js.map +1 -0
  205. package/package.json +12 -12
  206. package/src/components/ColumnHeaderFilter/index.tsx +1 -0
  207. package/src/components/DataTable/DataTable.spec.tsx +4 -1
  208. package/src/components/DataTable/DataTable.stories.tsx +190 -5
  209. package/src/components/DataTable/DataTableMobileCards.tsx +30 -4
  210. package/src/components/DataTable/DataTableRoot.tsx +195 -8
  211. package/src/components/DataTable/DataTableVirtualized.tsx +8 -0
  212. package/src/components/DataTable/cardLayout.spec.tsx +4 -1
  213. package/src/components/DataTable/customCellAlign.spec.tsx +124 -0
  214. package/src/components/DataTable/disabledContract.spec.tsx +4 -3
  215. package/src/components/DataTable/index.native.tsx +47 -1
  216. package/src/components/DataTable/rowPress.spec.tsx +229 -0
  217. package/src/components/DataTable/selectionSemantics.spec.tsx +156 -0
  218. package/src/components/DataTableBulkActions/index.tsx +16 -2
  219. package/src/components/DataTableBulkActions/selectionCountI18n.spec.tsx +94 -0
  220. package/src/components/DataTableCell/CellModalEditor.tsx +3 -1
  221. package/src/components/DataTableCell/cellDraftPolicy.spec.tsx +10 -2
  222. package/src/components/DataTableCell/dataTableCell.spec.tsx +10 -2
  223. package/src/components/DataTableCell/index.tsx +5 -36
  224. package/src/components/DataTableColumnPinning/DataTableColumnPinning.stories.tsx +49 -17
  225. package/src/components/DataTableDateFilter/DataTableDateFilter.stories.tsx +4 -3
  226. package/src/components/DataTableExport/DataTableExport.stories.tsx +3 -2
  227. package/src/components/DataTableExport/index.tsx +1 -0
  228. package/src/components/DataTableFilterButton/index.tsx +4 -1
  229. package/src/components/DataTableGrouping/DataTableGrouping.spec.tsx +59 -2
  230. package/src/components/DataTableGrouping/DataTableGrouping.stories.tsx +17 -5
  231. package/src/components/DataTableGrouping/index.tsx +1 -0
  232. package/src/components/DataTablePagination/DataTablePagination.stories.tsx +34 -20
  233. package/src/components/DataTableToolbar/index.tsx +1 -0
  234. package/src/components/DataTableViewOptions/DataTableViewOptions.stories.tsx +2 -1
  235. package/src/components/FilterManager.tsx +21 -13
  236. package/src/components/TableInput/index.native.tsx +3 -0
  237. package/src/components/__scratch__/scratch.spec.tsx +46 -0
  238. package/src/hooks/useDataTable.ts +27 -1
  239. package/src/types.ts +25 -0
  240. package/src/utils/rowPress.ts +58 -0
  241. package/types/components/ColumnHeaderFilter/index.d.ts.map +1 -1
  242. package/types/components/DataTable/DataTableMobileCards.d.ts +13 -1
  243. package/types/components/DataTable/DataTableMobileCards.d.ts.map +1 -1
  244. package/types/components/DataTable/DataTableRoot.d.ts +1 -1
  245. package/types/components/DataTable/DataTableRoot.d.ts.map +1 -1
  246. package/types/components/DataTable/DataTableVirtualized.d.ts +7 -1
  247. package/types/components/DataTable/DataTableVirtualized.d.ts.map +1 -1
  248. package/types/components/DataTable/index.native.d.ts +1 -1
  249. package/types/components/DataTable/index.native.d.ts.map +1 -1
  250. package/types/components/DataTableBulkActions/index.d.ts.map +1 -1
  251. package/types/components/DataTableCell/CellModalEditor.d.ts.map +1 -1
  252. package/types/components/DataTableCell/index.d.ts.map +1 -1
  253. package/types/components/DataTableFilterButton/index.d.ts.map +1 -1
  254. package/types/components/DataTableGrouping/index.d.ts.map +1 -1
  255. package/types/components/DataTableToolbar/index.d.ts.map +1 -1
  256. package/types/components/FilterManager.d.ts +7 -1
  257. package/types/components/FilterManager.d.ts.map +1 -1
  258. package/types/components/TableInput/index.native.d.ts.map +1 -1
  259. package/types/hooks/useDataTable.d.ts.map +1 -1
  260. package/types/types.d.ts +23 -0
  261. package/types/types.d.ts.map +1 -1
  262. package/types/utils/rowPress.d.ts +20 -0
  263. package/types/utils/rowPress.d.ts.map +1 -0
@@ -8,6 +8,7 @@ import {
8
8
  useReactTable,
9
9
  } from "@tanstack/react-table";
10
10
  import { useState } from "react";
11
+ import { formatCurrency, useResolvedKnobs } from "@multiplatform.one/theme";
11
12
  import { Button } from "@multiplatform.one/forms";
12
13
  import { Card, H3, Paragraph, ScrollView, Separator, Text, View, XStack, YStack } from "tamagui";
13
14
  import { DataTableColumnPinning, useColumnPinning } from "./index";
@@ -25,10 +26,25 @@ interface Person {
25
26
  performance: number;
26
27
  }
27
28
 
29
+ /**
30
+ * Seeded PRNG (mulberry32). The fixture must produce byte-identical rows on
31
+ * every page load: `Math.random()` here redrew Salary and the performance-bar
32
+ * width per capture, which is what flapped the visual baselines for this
33
+ * family. Determinism at the source keeps every pixel under comparison, where
34
+ * a screenshot mask or delay would not.
35
+ */
36
+ const seededRandom = (seed: number) => () => {
37
+ seed = (seed + 0x6d2b79f5) | 0;
38
+ let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
39
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
40
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
41
+ };
42
+
28
43
  const generateData = (count: number): Person[] => {
29
44
  const departments = ["Engineering", "Sales", "Marketing", "HR", "Finance"];
30
45
  const roles = ["Manager", "Senior", "Junior", "Lead", "Intern"];
31
46
  const statuses: ("active" | "inactive" | "pending")[] = ["active", "inactive", "pending"];
47
+ const random = seededRandom(0x5eed);
32
48
 
33
49
  return Array.from({ length: count }, (_, i) => ({
34
50
  id: i + 1,
@@ -37,16 +53,44 @@ const generateData = (count: number): Person[] => {
37
53
  email: `user${i + 1}@example.com`,
38
54
  department: departments[i % departments.length],
39
55
  role: roles[i % roles.length],
40
- salary: 50000 + Math.floor(Math.random() * 100000),
56
+ salary: 50000 + Math.floor(random() * 100000),
41
57
  startDate: new Date(2020 + (i % 4), i % 12, (i % 28) + 1).toISOString().split("T")[0],
42
58
  status: statuses[i % statuses.length],
43
- performance: Math.floor(Math.random() * 100),
59
+ performance: Math.floor(random() * 100),
44
60
  }));
45
61
  };
46
62
 
47
63
  const defaultData = generateData(50);
48
64
  const columnHelper = createColumnHelper<Person>();
49
65
 
66
+ /**
67
+ * Mini performance track. A component rather than an inline cell renderer so
68
+ * the track's radius can follow the knob (LC-60): the previous pinned `$2`
69
+ * outlived `borderRadius:none` on an 8px-tall bar, which reads as an
70
+ * undeclared knob-immune shape.
71
+ */
72
+ function PerformanceBar({ value }: { value: number }) {
73
+ const { knobProps } = useResolvedKnobs();
74
+ return (
75
+ <XStack alignItems="center" gap="$2">
76
+ <View
77
+ backgroundColor="$gray5"
78
+ height={8}
79
+ flex={1}
80
+ {...knobProps.containerRadius}
81
+ overflow="hidden"
82
+ >
83
+ <View
84
+ backgroundColor={value > 70 ? "$green10" : value > 40 ? "$yellow10" : "$red10"}
85
+ height="100%"
86
+ width={`${value}%`}
87
+ />
88
+ </View>
89
+ <Text fontSize="$1">{value}%</Text>
90
+ </XStack>
91
+ );
92
+ }
93
+
50
94
  const defaultColumns = [
51
95
  columnHelper.accessor("id", {
52
96
  header: "ID",
@@ -76,7 +120,8 @@ const defaultColumns = [
76
120
  }),
77
121
  columnHelper.accessor("salary", {
78
122
  header: "Salary",
79
- cell: (info) => `$${info.getValue().toLocaleString()}`,
123
+ cell: (info) =>
124
+ formatCurrency(info.getValue() as number, { symbol: "$", fractionDigits: "auto" }),
80
125
  }),
81
126
  columnHelper.accessor("startDate", {
82
127
  header: "Start Date",
@@ -100,20 +145,7 @@ const defaultColumns = [
100
145
  }),
101
146
  columnHelper.accessor("performance", {
102
147
  header: "Performance",
103
- cell: (info) => (
104
- <XStack alignItems="center" gap="$2">
105
- <View backgroundColor="$gray5" height={8} flex={1} borderRadius="$2" overflow="hidden">
106
- <View
107
- backgroundColor={
108
- info.getValue() > 70 ? "$green10" : info.getValue() > 40 ? "$yellow10" : "$red10"
109
- }
110
- height="100%"
111
- width={`${info.getValue()}%`}
112
- />
113
- </View>
114
- <Text fontSize="$1">{info.getValue()}%</Text>
115
- </XStack>
116
- ),
148
+ cell: (info) => <PerformanceBar value={info.getValue()} />,
117
149
  }),
118
150
  ];
119
151
 
@@ -1,5 +1,6 @@
1
1
  import { action } from "@multiplatform.one/storybook";
2
2
  import React from "react";
3
+ import { formatAbsoluteDate } from "@multiplatform.one/theme";
3
4
  import { Card, Text, XStack, YStack } from "tamagui";
4
5
  import { DataTableDateFilter, type DateFilterValue, applyDateFilter } from "./index";
5
6
 
@@ -200,10 +201,10 @@ export const WithDateConstraints = () => {
200
201
 
201
202
  <YStack gap="$2">
202
203
  <Text fontSize="$3" color="$gray11">
203
- Min Date: {minDate.toLocaleDateString()}
204
+ Min Date: {formatAbsoluteDate(minDate, { year: "always" })}
204
205
  </Text>
205
206
  <Text fontSize="$3" color="$gray11">
206
- Max Date: {maxDate.toLocaleDateString()}
207
+ Max Date: {formatAbsoluteDate(maxDate, { year: "always" })}
207
208
  </Text>
208
209
  </YStack>
209
210
  </YStack>
@@ -302,7 +303,7 @@ export const WithDataIntegration = () => {
302
303
  {task.title}
303
304
  </Text>
304
305
  <Text fontSize="$2" color="$gray11">
305
- Created: {new Date(task.createdAt).toLocaleDateString()}
306
+ Created: {formatAbsoluteDate(task.createdAt)}
306
307
  </Text>
307
308
  </YStack>
308
309
  <Text
@@ -1,4 +1,5 @@
1
1
  import { action } from "@multiplatform.one/storybook";
2
+ import { formatAbsoluteDate, formatNumber } from "@multiplatform.one/theme";
2
3
  import { Card, Text, YStack } from "tamagui";
3
4
  import { Table } from "@multiplatform.one/table-primitives";
4
5
  import { DataTableExport } from "./index";
@@ -97,7 +98,7 @@ const productColumns = [
97
98
  {
98
99
  accessorKey: "createdAt",
99
100
  header: "Created",
100
- cell: (info: any) => new Date(info.getValue()).toLocaleDateString(),
101
+ cell: (info: any) => formatAbsoluteDate(info.getValue()),
101
102
  },
102
103
  ];
103
104
 
@@ -424,7 +425,7 @@ export const WithComplexData = () => {
424
425
  {
425
426
  accessorKey: "metrics",
426
427
  header: "Users",
427
- cell: (info: any) => info.getValue().users.toLocaleString(),
428
+ cell: (info: any) => formatNumber(info.getValue().users),
428
429
  },
429
430
  {
430
431
  accessorKey: "features",
@@ -298,6 +298,7 @@ function ExportOptionsPopover({
298
298
  {...knobProps.borderRadius}
299
299
  borderWidth={1}
300
300
  borderColor="$borderColor"
301
+ transition={knobProps.transition}
301
302
  enterStyle={{ x: 0, y: -10, opacity: 0 }}
302
303
  exitStyle={{ x: 0, y: -10, opacity: 0 }}
303
304
  minWidth={popoverMinWidth}
@@ -46,7 +46,10 @@ function FieldRow({ field, highlighted, onSelect }: FieldRowProps) {
46
46
  cursor="pointer"
47
47
  paddingHorizontal="$2.5"
48
48
  paddingVertical="$2"
49
- {...knobProps.borderRadius}
49
+ // LC-55 FRAGMENT-LICENSE (D-03, G-08 arm): a hoverable option row is
50
+ // wayfinding, not chrome-earning — the full borderRadius fragment
51
+ // carries borderWidth and drew a permanent ring under the hover fill.
52
+ {...knobProps.containerRadius}
50
53
  backgroundColor={highlighted ? "$color4" : "transparent"}
51
54
  hoverStyle={{ backgroundColor: "$color4" }}
52
55
  pressStyle={{ backgroundColor: "$color5" }}
@@ -1,9 +1,15 @@
1
1
  import { fireEvent, screen } from "@testing-library/react";
2
2
  import { renderWithProviders } from "@multiplatform.one/test-utils";
3
+ import { formatNumber } from "@multiplatform.one/theme";
3
4
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
5
  import "@testing-library/jest-dom";
5
6
  import { DataTable } from "../DataTable";
6
- import { type AggregationConfig, DataTableGrouping, groupData } from "./index";
7
+ import {
8
+ type AggregationConfig,
9
+ DataTableGrouping,
10
+ formatAggregateValue,
11
+ groupData,
12
+ } from "./index";
7
13
 
8
14
  // Test data type
9
15
  type SalesRecord = {
@@ -115,7 +121,58 @@ describe("DataTableGrouping", () => {
115
121
 
116
122
  // Should show aggregation values
117
123
  expect(screen.getByText("North America")).toBeInTheDocument();
118
- // The actual aggregation display would be tested in integration
124
+ // LC-12 render channel: the sum reaches the slot through the house
125
+ // number register, so it reads like the cells it summarises.
126
+ expect(screen.getByText(formatNumber(15400))).toBeInTheDocument();
127
+ expect(screen.queryByText("15400")).not.toBeInTheDocument();
128
+ });
129
+
130
+ it("routes the header item count through the house number register (LC-12)", () => {
131
+ // Same data class as the aggregates: a large group's count must read
132
+ // localized ("(1,000 items)"), never raw beside channel-formatted sums.
133
+ const bigData = Array.from({ length: 1000 }, (_, i) => ({
134
+ id: String(i),
135
+ region: "North America",
136
+ }));
137
+ renderWithProviders(
138
+ <DataTableGrouping
139
+ data={bigData}
140
+ columns={[{ accessorKey: "region", header: "Region", type: "string" as const }]}
141
+ groupBy={["region"]}
142
+ />,
143
+ );
144
+
145
+ const expected = `(${formatNumber(1000, { maximumFractionDigits: 0 })} items)`;
146
+ expect(screen.getByText(expected)).toBeInTheDocument();
147
+ expect(screen.queryByText("(1000 items)")).not.toBeInTheDocument();
148
+ });
149
+
150
+ describe("formatAggregateValue (LC-12 number channel)", () => {
151
+ const sum: AggregationConfig = { column: "totalRevenue", function: "sum" };
152
+ const count: AggregationConfig = { column: "quantity", function: "count" };
153
+
154
+ it("routes aggregates through the house number register", () => {
155
+ expect(formatAggregateValue(15400, sum)).toBe(formatNumber(15400));
156
+ expect(formatAggregateValue(5133.33, sum)).toBe(formatNumber(5133.33));
157
+ });
158
+
159
+ it("keeps counts whole", () => {
160
+ expect(formatAggregateValue(3, count)).toBe("3");
161
+ });
162
+
163
+ it("formats a numeric string from a custom aggregation", () => {
164
+ expect(formatAggregateValue("15400", sum)).toBe(formatNumber(15400));
165
+ });
166
+
167
+ it("passes non-numeric custom results through as their own text", () => {
168
+ expect(formatAggregateValue("mixed", sum)).toBe("mixed");
169
+ });
170
+
171
+ it("keeps the zero floor for missing and non-finite values", () => {
172
+ expect(formatAggregateValue(undefined, sum)).toBe("0");
173
+ expect(formatAggregateValue(Number.NaN, sum)).toBe("0");
174
+ expect(formatAggregateValue(0, sum)).toBe("0");
175
+ });
119
176
  });
120
177
 
121
178
  it("calls onGroupingChange when grouping config changes", () => {
@@ -442,19 +442,31 @@ export const WithGroupingControls = () => {
442
442
  );
443
443
  };
444
444
 
445
+ /**
446
+ * Seeded PRNG (mulberry32). The aggregate rows this story renders (Total
447
+ * Revenue, Item Count) are derived from the fixture, so `Math.random()` made
448
+ * the shot differ on every capture and flapped the visual baseline. Seeding
449
+ * keeps the whole surface under pixel comparison.
450
+ */
451
+ const seededRandom = (seed: number) => () => {
452
+ seed = (seed + 0x6d2b79f5) | 0;
453
+ let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
454
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
455
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
456
+ };
457
+
445
458
  export const PerformanceTest = () => {
446
459
  // Generate larger dataset
460
+ const random = seededRandom(0x5eed);
447
461
  const largeDataset = Array.from({ length: 1000 }, (_, i) => ({
448
462
  id: `item-${i + 1}`,
449
463
  region: ["North America", "Europe", "Asia Pacific", "South America"][i % 4],
450
464
  category: ["Electronics", "Furniture", "Appliances", "Books"][i % 4],
451
465
  salesperson: `Person ${(i % 10) + 1}`,
452
- quantity: Math.floor(Math.random() * 20) + 1,
453
- unitPrice: Math.floor(Math.random() * 1000) + 100,
466
+ quantity: Math.floor(random() * 20) + 1,
467
+ unitPrice: Math.floor(random() * 1000) + 100,
454
468
  totalRevenue: 0, // Will be calculated
455
- date: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000)
456
- .toISOString()
457
- .split("T")[0],
469
+ date: new Date(Date.now() - random() * 365 * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
458
470
  })).map((item) => ({
459
471
  ...item,
460
472
  totalRevenue: item.quantity * item.unitPrice,
@@ -559,6 +559,7 @@ export function DataTableGroupingConfig<T = any>(props: DataTableGroupingConfigP
559
559
  {...knobProps.borderRadius}
560
560
  borderWidth={1}
561
561
  borderColor="$borderColor"
562
+ transition={knobProps.transition}
562
563
  enterStyle={{ x: 0, y: -10, opacity: 0 }}
563
564
  exitStyle={{ x: 0, y: -10, opacity: 0 }}
564
565
  minWidth={popoverMinWidth}
@@ -1,8 +1,10 @@
1
1
  import { action } from "@multiplatform.one/storybook";
2
+ import type { GuardrailSpecimenParameter } from "@multiplatform.one/theme";
2
3
  import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
4
  import React from "react";
4
- import { Button } from "@multiplatform.one/forms";
5
- import { Input, Text, View, XStack, YStack } from "tamagui";
5
+ // Catalog Button + Input (LC-56 STORY-HONESTY — no raw interactive primitives).
6
+ import { Button, Input } from "@multiplatform.one/forms";
7
+ import { Text, View, XStack, YStack } from "tamagui";
6
8
  import type {
7
9
  DataTableColumn,
8
10
  PageLoadQuery,
@@ -80,7 +82,20 @@ export const Compact: Story = {
80
82
  },
81
83
  };
82
84
 
85
+ // LC-57 allowlist: the boundary stories below EXIST to demo pager edges —
86
+ // first/last/single/empty page rest with the unreachable-direction buttons
87
+ // disabled by design (the honest boundary state, LC-40-washed). The warns
88
+ // still fire; the harvester tolerates exactly these codes on these ids.
89
+ const pagerBoundarySpecimen = {
90
+ guardrailSpecimen: {
91
+ warns: ["bare-disabled"],
92
+ reason:
93
+ "Pager-boundary specimen: unreachable nav directions rest disabled by design at page edges.",
94
+ } satisfies GuardrailSpecimenParameter,
95
+ };
96
+
83
97
  export const FirstPage: Story = {
98
+ parameters: pagerBoundarySpecimen,
84
99
  args: {
85
100
  ...Basic.args,
86
101
  pagination: {
@@ -101,6 +116,7 @@ export const MiddlePage: Story = {
101
116
  };
102
117
 
103
118
  export const LastPage: Story = {
119
+ parameters: pagerBoundarySpecimen,
104
120
  args: {
105
121
  ...Basic.args,
106
122
  pagination: {
@@ -172,6 +188,7 @@ export const SmallDataSet: Story = {
172
188
  };
173
189
 
174
190
  export const SinglePage: Story = {
191
+ parameters: pagerBoundarySpecimen,
175
192
  args: {
176
193
  ...Basic.args,
177
194
  pagination: {
@@ -406,6 +423,7 @@ export const PageJumpDemo: Story = {
406
423
  ...samplePagination,
407
424
  pageIndex: 5,
408
425
  });
426
+ const [jumpValue, setJumpValue] = React.useState("");
409
427
 
410
428
  const handlePaginationChange = (newPagination: TablePagination) => {
411
429
  setPagination({
@@ -445,24 +463,19 @@ export const PageJumpDemo: Story = {
445
463
  </Text>
446
464
  <XStack gap="$2" marginTop="$2" alignItems="center">
447
465
  <Text>Go to page:</Text>
448
- <Input
449
- keyboardType="numeric"
450
- placeholder={`1-${pagination.totalPages}`}
451
- onSubmitEditing={(e) => {
452
- handlePageJump(e.nativeEvent.text);
453
- }}
454
- padding="$1"
455
- width={80}
456
- />
457
- <Button
458
- onPress={() => {
459
- // For demo purposes, we'll use a simple approach
460
- // In a real app, you'd use a ref to get the input value
461
- handlePageJump("1");
462
- }}
463
- size="$2"
464
- padding="$1"
465
- >
466
+ <YStack width={110}>
467
+ <Input
468
+ keyboardType="numeric"
469
+ placeholder={`1-${pagination.totalPages}`}
470
+ onChangeText={setJumpValue}
471
+ inputProps={{
472
+ "aria-label": "Go to page",
473
+ onSubmitEditing: (e: { nativeEvent: { text: string } }) =>
474
+ handlePageJump(e.nativeEvent.text),
475
+ }}
476
+ />
477
+ </YStack>
478
+ <Button onPress={() => handlePageJump(jumpValue)} size="$2" padding="$1">
466
479
  Go
467
480
  </Button>
468
481
  </XStack>
@@ -571,6 +584,7 @@ export const ErrorStates: Story = {
571
584
  };
572
585
 
573
586
  export const EdgeCases: Story = {
587
+ parameters: pagerBoundarySpecimen,
574
588
  args: {
575
589
  ...Basic.args,
576
590
  pagination: {
@@ -176,6 +176,7 @@ export function DataTableToolbar(props: DataTableToolbarProps) {
176
176
  <Popover.Content
177
177
  {...knobProps.borderRadius}
178
178
  borderColor="$color6"
179
+ transition={knobProps.transition}
179
180
  enterStyle={{ x: 0, y: -10, opacity: 0 }}
180
181
  exitStyle={{ x: 0, y: -10, opacity: 0 }}
181
182
  >
@@ -1,5 +1,6 @@
1
1
  import { action } from "@multiplatform.one/storybook";
2
2
  import React from "react";
3
+ import { formatAbsoluteDate } from "@multiplatform.one/theme";
3
4
  import { Card, Text, View, XStack, YStack } from "tamagui";
4
5
  import { DataTableViewOptions } from "./index";
5
6
 
@@ -41,7 +42,7 @@ const allColumns = [
41
42
  {
42
43
  accessorKey: "createdAt",
43
44
  header: "Created",
44
- cell: (info: any) => new Date(info.getValue()).toLocaleDateString(),
45
+ cell: (info: any) => formatAbsoluteDate(info.getValue()),
45
46
  },
46
47
  {
47
48
  accessorKey: "description",
@@ -24,6 +24,12 @@ export interface FilterManagerProps {
24
24
  * "no filters applied" disable carries its own reason.
25
25
  */
26
26
  disabledReason?: string;
27
+ /**
28
+ * Reason placement forwarded to the trigger Button — `"inline"` keeps the
29
+ * reason on the control line in toolbar rows (the stacked default breaks
30
+ * the row's shared control height). See the Button contract.
31
+ */
32
+ disabledReasonPlacement?: "below" | "inline";
27
33
  canCreateGlobal?: boolean;
28
34
  trigger?: React.ReactNode;
29
35
  }
@@ -33,6 +39,7 @@ export function FilterManager({
33
39
  onSaveFilter,
34
40
  disabled = false,
35
41
  disabledReason,
42
+ disabledReasonPlacement,
36
43
  canCreateGlobal = false,
37
44
  trigger,
38
45
  }: FilterManagerProps) {
@@ -49,7 +56,7 @@ export function FilterManager({
49
56
 
50
57
  const handleSave = async () => {
51
58
  if (!filterName.trim()) {
52
- setError("Filter name is required");
59
+ setError(t("Filter name is required"));
53
60
  return;
54
61
  }
55
62
 
@@ -62,7 +69,7 @@ export function FilterManager({
62
69
  setIsGlobal(false);
63
70
  setOpen(false);
64
71
  } catch (e) {
65
- setError(e instanceof Error ? e.message : "Failed to save filter");
72
+ setError(e instanceof Error ? e.message : t("Failed to save filter"));
66
73
  } finally {
67
74
  setIsSaving(false);
68
75
  }
@@ -83,31 +90,32 @@ export function FilterManager({
83
90
  disabledReason={
84
91
  disabled ? disabledReason : !hasFilters ? t("Apply filters to save them") : undefined
85
92
  }
86
- aria-label="Save filter"
93
+ disabledReasonPlacement={disabledReasonPlacement}
94
+ aria-label={t("Save filter")}
87
95
  >
88
- Save Filter
96
+ {t("Save Filter")}
89
97
  </Button>
90
98
  )}
91
99
  </KnobDialogTrigger>
92
100
 
93
101
  <KnobDialogContent minWidth={dialogMinWidth}>
94
- <KnobDialogTitle>Save Filter</KnobDialogTitle>
102
+ <KnobDialogTitle>{t("Save Filter")}</KnobDialogTitle>
95
103
  <KnobDialogDescription>
96
- Save the current filter configuration for quick access later.
104
+ {t("Save the current filter configuration for quick access later.")}
97
105
  </KnobDialogDescription>
98
106
 
99
107
  <YStack gap="$3">
100
108
  <Input
101
- label="Filter Name"
109
+ label={t("Filter Name")}
102
110
  value={filterName}
103
111
  onChangeText={setFilterName}
104
- placeholder="e.g., Big Pokemon"
112
+ placeholder={t("e.g., Big Pokemon")}
105
113
  disabled={isSaving}
106
114
  />
107
115
 
108
116
  {canCreateGlobal && (
109
117
  <Checkbox
110
- label="Make this filter global (visible to all users)"
118
+ label={t("Make this filter global (visible to all users)")}
111
119
  checked={isGlobal}
112
120
  onCheckedChange={(checked) => setIsGlobal(checked === true)}
113
121
  disabled={isSaving}
@@ -121,7 +129,7 @@ export function FilterManager({
121
129
  )}
122
130
 
123
131
  <Text color="$color10" fontSize="$2">
124
- Current filters: {currentFilters.length} active
132
+ {t("Current filters: {{count}} active", { count: currentFilters.length })}
125
133
  </Text>
126
134
  </YStack>
127
135
 
@@ -129,7 +137,7 @@ export function FilterManager({
129
137
  <KnobDialogClose asChild>
130
138
  {/* DG-ACT-05: the cancel action never renders disabled. */}
131
139
  <Button size={knobProps.sizeToken} variant="outlined">
132
- Cancel
140
+ {t("Cancel")}
133
141
  </Button>
134
142
  </KnobDialogClose>
135
143
  {/* Stays pressable on an empty name — handleSave surfaces the
@@ -143,7 +151,7 @@ export function FilterManager({
143
151
  onPress={handleSave}
144
152
  loading={isSaving}
145
153
  >
146
- Save Filter
154
+ {t("Save Filter")}
147
155
  </Button>
148
156
  </XStack>
149
157
 
@@ -155,7 +163,7 @@ export function FilterManager({
155
163
  size="$2"
156
164
  circular
157
165
  icon={XIcon}
158
- aria-label="Close dialog"
166
+ aria-label={t("Close dialog")}
159
167
  />
160
168
  </KnobDialogClose>
161
169
  </KnobDialogContent>
@@ -149,6 +149,9 @@ export function TableInput<TData extends DataTableData = DataTableData>({
149
149
  <Field form={resolvedForm} name={name} validators={resolvedValidators}>
150
150
  {(field) => (
151
151
  <Inner
152
+ // Form state wins; the `data` fallback covers only the uninitialized
153
+ // instant before the field registers. A `data` prop diverging from
154
+ // form state is never load-bearing here (D-16 rider).
152
155
  rows={(field.state.value as TData[] | undefined) ?? data}
153
156
  commit={(next) => {
154
157
  field.handleChange(next);
@@ -0,0 +1,46 @@
1
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
2
+ import { describe, expect, it } from "vitest";
3
+ import type { DataTableColumn } from "../../types";
4
+ import { DataTable } from "../DataTable";
5
+
6
+ type Row = { id: number; name: string; amount: number };
7
+ const data: Row[] = [
8
+ { id: 1, name: "Alpha", amount: 10 },
9
+ { id: 2, name: "Beta", amount: 20 },
10
+ ];
11
+ const columns: DataTableColumn<Row>[] = [
12
+ { id: "name", accessorKey: "name", header: "Name" },
13
+ {
14
+ id: "amount",
15
+ accessorKey: "amount",
16
+ header: "Amount",
17
+ align: "right",
18
+ cell: (info: any) => info.getValue(),
19
+ },
20
+ ];
21
+
22
+ describe("scratch", () => {
23
+ it("dumps selection checkbox DOM", () => {
24
+ const { container } = renderWithProviders(
25
+ <DataTable
26
+ data={data}
27
+ columns={columns}
28
+ enableRowSelection
29
+ layout="table"
30
+ enablePagination={false}
31
+ />,
32
+ );
33
+ const labeled = container.querySelectorAll('[aria-label^="Select row"]');
34
+ console.log("SELECT-LABELED COUNT:", labeled.length);
35
+ labeled.forEach((el, i) => {
36
+ if (i < 2) console.log("EL", i, el.outerHTML.slice(0, 800));
37
+ });
38
+ const roleCheckbox = container.querySelectorAll('[role="checkbox"]');
39
+ console.log("ROLE CHECKBOX COUNT:", roleCheckbox.length);
40
+ const ariaChecked = container.querySelectorAll("[aria-checked]");
41
+ console.log("ARIA-CHECKED COUNT:", ariaChecked.length);
42
+ const firstCell = container.querySelector('[role="row"][aria-rowindex="1"]');
43
+ console.log("FIRST DATA ROW:", firstCell ? firstCell.outerHTML.slice(0, 500) : "none");
44
+ expect(true).toBe(true);
45
+ });
46
+ });
@@ -408,9 +408,35 @@ export function useDataTable<TData extends DataTableData = DataTableData>({
408
408
  return String(index);
409
409
  }, []);
410
410
 
411
+ // Map the package column aliases onto TanStack's per-column switches:
412
+ // `sortable`/`filterable` were documented API but previously reached no
413
+ // TanStack option, so `sortable: false` columns still rendered a live sort
414
+ // header (caret, tab stop, aria-sort) — chrome asserting an interaction
415
+ // the column declined (Axiom 6 HONEST STATE). Explicit TanStack props win.
416
+ const normalizedColumns = useMemo(
417
+ () =>
418
+ columns.map((column) => {
419
+ const { sortable, filterable } = column as {
420
+ sortable?: boolean;
421
+ filterable?: boolean;
422
+ };
423
+ if (sortable === undefined && filterable === undefined) return column;
424
+ return {
425
+ ...column,
426
+ ...(sortable !== undefined && column.enableSorting === undefined
427
+ ? { enableSorting: sortable }
428
+ : {}),
429
+ ...(filterable !== undefined && column.enableColumnFilter === undefined
430
+ ? { enableColumnFilter: filterable }
431
+ : {}),
432
+ };
433
+ }),
434
+ [columns],
435
+ );
436
+
411
437
  const table = useReactTable({
412
438
  data: preFilteredData,
413
- columns: columns as ColumnDef<TData>[],
439
+ columns: normalizedColumns as ColumnDef<TData>[],
414
440
  defaultColumn: { filterFn: operatorFilterFn as any },
415
441
  getCoreRowModel: getCoreRowModel(),
416
442
  getExpandedRowModel: enableRowExpansion ? getExpandedRowModel() : undefined,