@hexclave/dashboard-ui-components 1.0.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 (331) hide show
  1. package/LICENSE +5 -0
  2. package/dist/chunk-BE-pF4vm.js +34 -0
  3. package/dist/components/alert.d.ts +22 -0
  4. package/dist/components/alert.d.ts.map +1 -0
  5. package/dist/components/alert.js +75 -0
  6. package/dist/components/alert.js.map +1 -0
  7. package/dist/components/analytics-chart/analytics-chart-pie.d.ts +67 -0
  8. package/dist/components/analytics-chart/analytics-chart-pie.d.ts.map +1 -0
  9. package/dist/components/analytics-chart/analytics-chart-pie.js +253 -0
  10. package/dist/components/analytics-chart/analytics-chart-pie.js.map +1 -0
  11. package/dist/components/analytics-chart/analytics-chart.d.ts +554 -0
  12. package/dist/components/analytics-chart/analytics-chart.d.ts.map +1 -0
  13. package/dist/components/analytics-chart/analytics-chart.js +1021 -0
  14. package/dist/components/analytics-chart/analytics-chart.js.map +1 -0
  15. package/dist/components/analytics-chart/default-analytics-chart-tooltip.d.ts +66 -0
  16. package/dist/components/analytics-chart/default-analytics-chart-tooltip.d.ts.map +1 -0
  17. package/dist/components/analytics-chart/default-analytics-chart-tooltip.js +179 -0
  18. package/dist/components/analytics-chart/default-analytics-chart-tooltip.js.map +1 -0
  19. package/dist/components/analytics-chart/format.d.ts +13 -0
  20. package/dist/components/analytics-chart/format.d.ts.map +1 -0
  21. package/dist/components/analytics-chart/format.js +138 -0
  22. package/dist/components/analytics-chart/format.js.map +1 -0
  23. package/dist/components/analytics-chart/index.d.ts +8 -0
  24. package/dist/components/analytics-chart/index.js +184 -0
  25. package/dist/components/analytics-chart/palette.d.ts +15 -0
  26. package/dist/components/analytics-chart/palette.d.ts.map +1 -0
  27. package/dist/components/analytics-chart/palette.js +60 -0
  28. package/dist/components/analytics-chart/palette.js.map +1 -0
  29. package/dist/components/analytics-chart/render-data-series.d.ts +28 -0
  30. package/dist/components/analytics-chart/render-data-series.d.ts.map +1 -0
  31. package/dist/components/analytics-chart/render-data-series.js +109 -0
  32. package/dist/components/analytics-chart/render-data-series.js.map +1 -0
  33. package/dist/components/analytics-chart/state.d.ts +54 -0
  34. package/dist/components/analytics-chart/state.d.ts.map +1 -0
  35. package/dist/components/analytics-chart/state.js +142 -0
  36. package/dist/components/analytics-chart/state.js.map +1 -0
  37. package/dist/components/analytics-chart/strings.d.ts +33 -0
  38. package/dist/components/analytics-chart/strings.d.ts.map +1 -0
  39. package/dist/components/analytics-chart/strings.js +37 -0
  40. package/dist/components/analytics-chart/strings.js.map +1 -0
  41. package/dist/components/analytics-chart/types.d.ts +157 -0
  42. package/dist/components/analytics-chart/types.d.ts.map +1 -0
  43. package/dist/components/analytics-chart/types.js +21 -0
  44. package/dist/components/analytics-chart/types.js.map +1 -0
  45. package/dist/components/badge.d.ts +40 -0
  46. package/dist/components/badge.d.ts.map +1 -0
  47. package/dist/components/badge.js +75 -0
  48. package/dist/components/badge.js.map +1 -0
  49. package/dist/components/button.d.ts +46 -0
  50. package/dist/components/button.d.ts.map +1 -0
  51. package/dist/components/button.js +82 -0
  52. package/dist/components/button.js.map +1 -0
  53. package/dist/components/card.d.ts +87 -0
  54. package/dist/components/card.d.ts.map +1 -0
  55. package/dist/components/card.js +161 -0
  56. package/dist/components/card.js.map +1 -0
  57. package/dist/components/chart-card.d.ts +50 -0
  58. package/dist/components/chart-card.d.ts.map +1 -0
  59. package/dist/components/chart-card.js +83 -0
  60. package/dist/components/chart-card.js.map +1 -0
  61. package/dist/components/chart-container.d.ts +51 -0
  62. package/dist/components/chart-container.d.ts.map +1 -0
  63. package/dist/components/chart-container.js +77 -0
  64. package/dist/components/chart-container.js.map +1 -0
  65. package/dist/components/chart-legend.d.ts +11 -0
  66. package/dist/components/chart-legend.d.ts.map +1 -0
  67. package/dist/components/chart-legend.js +37 -0
  68. package/dist/components/chart-legend.js.map +1 -0
  69. package/dist/components/chart-theme.d.ts +26 -0
  70. package/dist/components/chart-theme.d.ts.map +1 -0
  71. package/dist/components/chart-theme.js +64 -0
  72. package/dist/components/chart-theme.js.map +1 -0
  73. package/dist/components/chart-tooltip.d.ts +41 -0
  74. package/dist/components/chart-tooltip.d.ts.map +1 -0
  75. package/dist/components/chart-tooltip.js +89 -0
  76. package/dist/components/chart-tooltip.js.map +1 -0
  77. package/dist/components/cursor-blast-effect.d.ts +27 -0
  78. package/dist/components/cursor-blast-effect.d.ts.map +1 -0
  79. package/dist/components/cursor-blast-effect.js +244 -0
  80. package/dist/components/cursor-blast-effect.js.map +1 -0
  81. package/dist/components/data-grid/data-grid-sizing.d.ts +13 -0
  82. package/dist/components/data-grid/data-grid-sizing.d.ts.map +1 -0
  83. package/dist/components/data-grid/data-grid-sizing.js +44 -0
  84. package/dist/components/data-grid/data-grid-sizing.js.map +1 -0
  85. package/dist/components/data-grid/data-grid-toolbar.d.ts +31 -0
  86. package/dist/components/data-grid/data-grid-toolbar.d.ts.map +1 -0
  87. package/dist/components/data-grid/data-grid-toolbar.js +229 -0
  88. package/dist/components/data-grid/data-grid-toolbar.js.map +1 -0
  89. package/dist/components/data-grid/data-grid.d.ts +47 -0
  90. package/dist/components/data-grid/data-grid.d.ts.map +1 -0
  91. package/dist/components/data-grid/data-grid.js +927 -0
  92. package/dist/components/data-grid/data-grid.js.map +1 -0
  93. package/dist/components/data-grid/data-grid.test.d.ts +1 -0
  94. package/dist/components/data-grid/data-grid.test.js +297 -0
  95. package/dist/components/data-grid/data-grid.test.js.map +1 -0
  96. package/dist/components/data-grid/index.d.ts +9 -0
  97. package/dist/components/data-grid/index.js +148 -0
  98. package/dist/components/data-grid/state.d.ts +34 -0
  99. package/dist/components/data-grid/state.d.ts.map +1 -0
  100. package/dist/components/data-grid/state.js +199 -0
  101. package/dist/components/data-grid/state.js.map +1 -0
  102. package/dist/components/data-grid/strings.d.ts +8 -0
  103. package/dist/components/data-grid/strings.d.ts.map +1 -0
  104. package/dist/components/data-grid/strings.js +42 -0
  105. package/dist/components/data-grid/strings.js.map +1 -0
  106. package/dist/components/data-grid/types.d.ts +278 -0
  107. package/dist/components/data-grid/types.d.ts.map +1 -0
  108. package/dist/components/data-grid/types.js +0 -0
  109. package/dist/components/data-grid/use-data-source.d.ts +85 -0
  110. package/dist/components/data-grid/use-data-source.d.ts.map +1 -0
  111. package/dist/components/data-grid/use-data-source.js +244 -0
  112. package/dist/components/data-grid/use-data-source.js.map +1 -0
  113. package/dist/components/data-grid/use-url-state.d.ts +38 -0
  114. package/dist/components/data-grid/use-url-state.d.ts.map +1 -0
  115. package/dist/components/data-grid/use-url-state.js +214 -0
  116. package/dist/components/data-grid/use-url-state.js.map +1 -0
  117. package/dist/components/data-grid/use-url-state.test.d.ts +1 -0
  118. package/dist/components/data-grid/use-url-state.test.js +91 -0
  119. package/dist/components/data-grid/use-url-state.test.js.map +1 -0
  120. package/dist/components/dialog.d.ts +67 -0
  121. package/dist/components/dialog.d.ts.map +1 -0
  122. package/dist/components/dialog.js +100 -0
  123. package/dist/components/dialog.js.map +1 -0
  124. package/dist/components/edit-mode.d.ts +12 -0
  125. package/dist/components/edit-mode.d.ts.map +1 -0
  126. package/dist/components/edit-mode.js +23 -0
  127. package/dist/components/edit-mode.js.map +1 -0
  128. package/dist/components/empty-state.d.ts +37 -0
  129. package/dist/components/empty-state.d.ts.map +1 -0
  130. package/dist/components/empty-state.js +53 -0
  131. package/dist/components/empty-state.js.map +1 -0
  132. package/dist/components/input.d.ts +18 -0
  133. package/dist/components/input.d.ts.map +1 -0
  134. package/dist/components/input.js +53 -0
  135. package/dist/components/input.js.map +1 -0
  136. package/dist/components/metric-card.d.ts +55 -0
  137. package/dist/components/metric-card.d.ts.map +1 -0
  138. package/dist/components/metric-card.js +112 -0
  139. package/dist/components/metric-card.js.map +1 -0
  140. package/dist/components/pill-toggle.d.ts +33 -0
  141. package/dist/components/pill-toggle.d.ts.map +1 -0
  142. package/dist/components/pill-toggle.js +86 -0
  143. package/dist/components/pill-toggle.js.map +1 -0
  144. package/dist/components/progress-bar.d.ts +35 -0
  145. package/dist/components/progress-bar.d.ts.map +1 -0
  146. package/dist/components/progress-bar.js +54 -0
  147. package/dist/components/progress-bar.js.map +1 -0
  148. package/dist/components/separator.d.ts +23 -0
  149. package/dist/components/separator.d.ts.map +1 -0
  150. package/dist/components/separator.js +29 -0
  151. package/dist/components/separator.js.map +1 -0
  152. package/dist/components/skeleton.d.ts +24 -0
  153. package/dist/components/skeleton.d.ts.map +1 -0
  154. package/dist/components/skeleton.js +30 -0
  155. package/dist/components/skeleton.js.map +1 -0
  156. package/dist/components/table.d.ts +49 -0
  157. package/dist/components/table.d.ts.map +1 -0
  158. package/dist/components/table.js +84 -0
  159. package/dist/components/table.js.map +1 -0
  160. package/dist/components/tabs.d.ts +38 -0
  161. package/dist/components/tabs.d.ts.map +1 -0
  162. package/dist/components/tabs.js +113 -0
  163. package/dist/components/tabs.js.map +1 -0
  164. package/dist/dashboard-ui-components.global.js +25539 -0
  165. package/dist/dashboard-ui-components.global.js.map +7 -0
  166. package/dist/esm/components/alert.d.ts +22 -0
  167. package/dist/esm/components/alert.d.ts.map +1 -0
  168. package/dist/esm/components/alert.js +73 -0
  169. package/dist/esm/components/alert.js.map +1 -0
  170. package/dist/esm/components/analytics-chart/analytics-chart-pie.d.ts +67 -0
  171. package/dist/esm/components/analytics-chart/analytics-chart-pie.d.ts.map +1 -0
  172. package/dist/esm/components/analytics-chart/analytics-chart-pie.js +251 -0
  173. package/dist/esm/components/analytics-chart/analytics-chart-pie.js.map +1 -0
  174. package/dist/esm/components/analytics-chart/analytics-chart.d.ts +554 -0
  175. package/dist/esm/components/analytics-chart/analytics-chart.d.ts.map +1 -0
  176. package/dist/esm/components/analytics-chart/analytics-chart.js +1019 -0
  177. package/dist/esm/components/analytics-chart/analytics-chart.js.map +1 -0
  178. package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.d.ts +66 -0
  179. package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.d.ts.map +1 -0
  180. package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.js +176 -0
  181. package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.js.map +1 -0
  182. package/dist/esm/components/analytics-chart/format.d.ts +13 -0
  183. package/dist/esm/components/analytics-chart/format.d.ts.map +1 -0
  184. package/dist/esm/components/analytics-chart/format.js +133 -0
  185. package/dist/esm/components/analytics-chart/format.js.map +1 -0
  186. package/dist/esm/components/analytics-chart/index.d.ts +8 -0
  187. package/dist/esm/components/analytics-chart/index.js +9 -0
  188. package/dist/esm/components/analytics-chart/palette.d.ts +15 -0
  189. package/dist/esm/components/analytics-chart/palette.d.ts.map +1 -0
  190. package/dist/esm/components/analytics-chart/palette.js +55 -0
  191. package/dist/esm/components/analytics-chart/palette.js.map +1 -0
  192. package/dist/esm/components/analytics-chart/render-data-series.d.ts +28 -0
  193. package/dist/esm/components/analytics-chart/render-data-series.d.ts.map +1 -0
  194. package/dist/esm/components/analytics-chart/render-data-series.js +107 -0
  195. package/dist/esm/components/analytics-chart/render-data-series.js.map +1 -0
  196. package/dist/esm/components/analytics-chart/state.d.ts +54 -0
  197. package/dist/esm/components/analytics-chart/state.d.ts.map +1 -0
  198. package/dist/esm/components/analytics-chart/state.js +126 -0
  199. package/dist/esm/components/analytics-chart/state.js.map +1 -0
  200. package/dist/esm/components/analytics-chart/strings.d.ts +33 -0
  201. package/dist/esm/components/analytics-chart/strings.d.ts.map +1 -0
  202. package/dist/esm/components/analytics-chart/strings.js +34 -0
  203. package/dist/esm/components/analytics-chart/strings.js.map +1 -0
  204. package/dist/esm/components/analytics-chart/types.d.ts +157 -0
  205. package/dist/esm/components/analytics-chart/types.d.ts.map +1 -0
  206. package/dist/esm/components/analytics-chart/types.js +18 -0
  207. package/dist/esm/components/analytics-chart/types.js.map +1 -0
  208. package/dist/esm/components/badge.d.ts +40 -0
  209. package/dist/esm/components/badge.d.ts.map +1 -0
  210. package/dist/esm/components/badge.js +73 -0
  211. package/dist/esm/components/badge.js.map +1 -0
  212. package/dist/esm/components/button.d.ts +46 -0
  213. package/dist/esm/components/button.d.ts.map +1 -0
  214. package/dist/esm/components/button.js +79 -0
  215. package/dist/esm/components/button.js.map +1 -0
  216. package/dist/esm/components/card.d.ts +87 -0
  217. package/dist/esm/components/card.d.ts.map +1 -0
  218. package/dist/esm/components/card.js +155 -0
  219. package/dist/esm/components/card.js.map +1 -0
  220. package/dist/esm/components/chart-card.d.ts +50 -0
  221. package/dist/esm/components/chart-card.d.ts.map +1 -0
  222. package/dist/esm/components/chart-card.js +80 -0
  223. package/dist/esm/components/chart-card.js.map +1 -0
  224. package/dist/esm/components/chart-container.d.ts +51 -0
  225. package/dist/esm/components/chart-container.d.ts.map +1 -0
  226. package/dist/esm/components/chart-container.js +70 -0
  227. package/dist/esm/components/chart-container.js.map +1 -0
  228. package/dist/esm/components/chart-legend.d.ts +11 -0
  229. package/dist/esm/components/chart-legend.d.ts.map +1 -0
  230. package/dist/esm/components/chart-legend.js +34 -0
  231. package/dist/esm/components/chart-legend.js.map +1 -0
  232. package/dist/esm/components/chart-theme.d.ts +26 -0
  233. package/dist/esm/components/chart-theme.d.ts.map +1 -0
  234. package/dist/esm/components/chart-theme.js +59 -0
  235. package/dist/esm/components/chart-theme.js.map +1 -0
  236. package/dist/esm/components/chart-tooltip.d.ts +41 -0
  237. package/dist/esm/components/chart-tooltip.d.ts.map +1 -0
  238. package/dist/esm/components/chart-tooltip.js +84 -0
  239. package/dist/esm/components/chart-tooltip.js.map +1 -0
  240. package/dist/esm/components/cursor-blast-effect.d.ts +27 -0
  241. package/dist/esm/components/cursor-blast-effect.d.ts.map +1 -0
  242. package/dist/esm/components/cursor-blast-effect.js +242 -0
  243. package/dist/esm/components/cursor-blast-effect.js.map +1 -0
  244. package/dist/esm/components/data-grid/data-grid-sizing.d.ts +13 -0
  245. package/dist/esm/components/data-grid/data-grid-sizing.d.ts.map +1 -0
  246. package/dist/esm/components/data-grid/data-grid-sizing.js +38 -0
  247. package/dist/esm/components/data-grid/data-grid-sizing.js.map +1 -0
  248. package/dist/esm/components/data-grid/data-grid-toolbar.d.ts +31 -0
  249. package/dist/esm/components/data-grid/data-grid-toolbar.d.ts.map +1 -0
  250. package/dist/esm/components/data-grid/data-grid-toolbar.js +226 -0
  251. package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -0
  252. package/dist/esm/components/data-grid/data-grid.d.ts +47 -0
  253. package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -0
  254. package/dist/esm/components/data-grid/data-grid.js +923 -0
  255. package/dist/esm/components/data-grid/data-grid.js.map +1 -0
  256. package/dist/esm/components/data-grid/data-grid.test.d.ts +1 -0
  257. package/dist/esm/components/data-grid/data-grid.test.js +297 -0
  258. package/dist/esm/components/data-grid/data-grid.test.js.map +1 -0
  259. package/dist/esm/components/data-grid/index.d.ts +9 -0
  260. package/dist/esm/components/data-grid/index.js +9 -0
  261. package/dist/esm/components/data-grid/state.d.ts +34 -0
  262. package/dist/esm/components/data-grid/state.d.ts.map +1 -0
  263. package/dist/esm/components/data-grid/state.js +187 -0
  264. package/dist/esm/components/data-grid/state.js.map +1 -0
  265. package/dist/esm/components/data-grid/strings.d.ts +8 -0
  266. package/dist/esm/components/data-grid/strings.d.ts.map +1 -0
  267. package/dist/esm/components/data-grid/strings.js +39 -0
  268. package/dist/esm/components/data-grid/strings.js.map +1 -0
  269. package/dist/esm/components/data-grid/types.d.ts +278 -0
  270. package/dist/esm/components/data-grid/types.d.ts.map +1 -0
  271. package/dist/esm/components/data-grid/types.js +1 -0
  272. package/dist/esm/components/data-grid/use-data-source.d.ts +85 -0
  273. package/dist/esm/components/data-grid/use-data-source.d.ts.map +1 -0
  274. package/dist/esm/components/data-grid/use-data-source.js +242 -0
  275. package/dist/esm/components/data-grid/use-data-source.js.map +1 -0
  276. package/dist/esm/components/data-grid/use-url-state.d.ts +38 -0
  277. package/dist/esm/components/data-grid/use-url-state.d.ts.map +1 -0
  278. package/dist/esm/components/data-grid/use-url-state.js +212 -0
  279. package/dist/esm/components/data-grid/use-url-state.js.map +1 -0
  280. package/dist/esm/components/data-grid/use-url-state.test.d.ts +1 -0
  281. package/dist/esm/components/data-grid/use-url-state.test.js +91 -0
  282. package/dist/esm/components/data-grid/use-url-state.test.js.map +1 -0
  283. package/dist/esm/components/dialog.d.ts +67 -0
  284. package/dist/esm/components/dialog.d.ts.map +1 -0
  285. package/dist/esm/components/dialog.js +92 -0
  286. package/dist/esm/components/dialog.js.map +1 -0
  287. package/dist/esm/components/edit-mode.d.ts +12 -0
  288. package/dist/esm/components/edit-mode.d.ts.map +1 -0
  289. package/dist/esm/components/edit-mode.js +20 -0
  290. package/dist/esm/components/edit-mode.js.map +1 -0
  291. package/dist/esm/components/empty-state.d.ts +37 -0
  292. package/dist/esm/components/empty-state.d.ts.map +1 -0
  293. package/dist/esm/components/empty-state.js +50 -0
  294. package/dist/esm/components/empty-state.js.map +1 -0
  295. package/dist/esm/components/input.d.ts +18 -0
  296. package/dist/esm/components/input.d.ts.map +1 -0
  297. package/dist/esm/components/input.js +50 -0
  298. package/dist/esm/components/input.js.map +1 -0
  299. package/dist/esm/components/metric-card.d.ts +55 -0
  300. package/dist/esm/components/metric-card.d.ts.map +1 -0
  301. package/dist/esm/components/metric-card.js +109 -0
  302. package/dist/esm/components/metric-card.js.map +1 -0
  303. package/dist/esm/components/pill-toggle.d.ts +33 -0
  304. package/dist/esm/components/pill-toggle.d.ts.map +1 -0
  305. package/dist/esm/components/pill-toggle.js +84 -0
  306. package/dist/esm/components/pill-toggle.js.map +1 -0
  307. package/dist/esm/components/progress-bar.d.ts +35 -0
  308. package/dist/esm/components/progress-bar.d.ts.map +1 -0
  309. package/dist/esm/components/progress-bar.js +52 -0
  310. package/dist/esm/components/progress-bar.js.map +1 -0
  311. package/dist/esm/components/separator.d.ts +23 -0
  312. package/dist/esm/components/separator.d.ts.map +1 -0
  313. package/dist/esm/components/separator.js +27 -0
  314. package/dist/esm/components/separator.js.map +1 -0
  315. package/dist/esm/components/skeleton.d.ts +24 -0
  316. package/dist/esm/components/skeleton.d.ts.map +1 -0
  317. package/dist/esm/components/skeleton.js +28 -0
  318. package/dist/esm/components/skeleton.js.map +1 -0
  319. package/dist/esm/components/table.d.ts +49 -0
  320. package/dist/esm/components/table.d.ts.map +1 -0
  321. package/dist/esm/components/table.js +76 -0
  322. package/dist/esm/components/table.js.map +1 -0
  323. package/dist/esm/components/tabs.d.ts +38 -0
  324. package/dist/esm/components/tabs.d.ts.map +1 -0
  325. package/dist/esm/components/tabs.js +111 -0
  326. package/dist/esm/components/tabs.js.map +1 -0
  327. package/dist/esm/index.d.ts +24 -0
  328. package/dist/esm/index.js +27 -0
  329. package/dist/index.d.ts +37 -0
  330. package/dist/index.js +291 -0
  331. package/package.json +73 -0
@@ -0,0 +1,242 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { applyQuickSearch, buildRowComparator, defaultMatchRow, paginateRows } from "./state.js";
3
+
4
+ //#region src/components/data-grid/use-data-source.ts
5
+ function useClientDataSource(opts) {
6
+ const { data, columns, sorting, quickSearch, matchRow, pagination, paginationMode } = opts;
7
+ const processed = useMemo(() => {
8
+ const searched = applyQuickSearch(data, quickSearch, columns, matchRow);
9
+ const comparator = buildRowComparator(sorting, columns);
10
+ const sorted = comparator ? [...searched].sort(comparator) : searched;
11
+ const totalRowCount = sorted.length;
12
+ return {
13
+ rows: paginationMode === "client" ? paginateRows(sorted, pagination) : sorted,
14
+ totalRowCount
15
+ };
16
+ }, [
17
+ data,
18
+ JSON.stringify(sorting),
19
+ quickSearch,
20
+ matchRow,
21
+ pagination.pageIndex,
22
+ pagination.pageSize,
23
+ paginationMode,
24
+ columns
25
+ ]);
26
+ return useMemo(() => ({
27
+ rows: processed.rows,
28
+ totalRowCount: processed.totalRowCount,
29
+ isLoading: false,
30
+ isRefetching: false,
31
+ isLoadingMore: false,
32
+ loadMore: () => {},
33
+ hasMore: false,
34
+ reload: () => {},
35
+ error: null
36
+ }), [processed]);
37
+ }
38
+ function useAsyncDataSource(opts) {
39
+ const { dataSource, getRowId, sorting, quickSearch, pagination, paginationMode } = opts;
40
+ const [rows, setRows] = useState([]);
41
+ const [totalRowCount, setTotalRowCount] = useState(void 0);
42
+ const [isLoading, setIsLoading] = useState(true);
43
+ const [isRefetching, setIsRefetching] = useState(false);
44
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
45
+ const [hasMore, setHasMore] = useState(true);
46
+ const [error, setError] = useState(null);
47
+ const cursorRef = useRef(void 0);
48
+ const abortRef = useRef(null);
49
+ const pageIndexRef = useRef(0);
50
+ const hasDataRef = useRef(false);
51
+ const hasMountedServerPaginationRef = useRef(false);
52
+ const latestArgsRef = useRef({
53
+ dataSource,
54
+ getRowId,
55
+ sorting,
56
+ quickSearch,
57
+ pagination
58
+ });
59
+ latestArgsRef.current = {
60
+ dataSource,
61
+ getRowId,
62
+ sorting,
63
+ quickSearch,
64
+ pagination
65
+ };
66
+ const sortingKey = JSON.stringify(sorting);
67
+ const quickSearchKey = quickSearch;
68
+ const fetchPage = useCallback(async (append) => {
69
+ const { dataSource: currentDataSource, getRowId: currentGetRowId, sorting: currentSorting, quickSearch: currentQuickSearch, pagination: currentPagination } = latestArgsRef.current;
70
+ abortRef.current?.abort();
71
+ const controller = new AbortController();
72
+ abortRef.current = controller;
73
+ if (append) setIsLoadingMore(true);
74
+ else {
75
+ if (hasDataRef.current) setIsRefetching(true);
76
+ else setIsLoading(true);
77
+ cursorRef.current = void 0;
78
+ pageIndexRef.current = 0;
79
+ }
80
+ setError(null);
81
+ try {
82
+ const gen = currentDataSource({
83
+ sorting: currentSorting,
84
+ quickSearch: currentQuickSearch,
85
+ pagination: append ? {
86
+ pageIndex: pageIndexRef.current,
87
+ pageSize: currentPagination.pageSize
88
+ } : currentPagination,
89
+ cursor: cursorRef.current
90
+ });
91
+ for await (const result of gen) {
92
+ if (controller.signal.aborted) return;
93
+ if (result.totalRowCount != null) setTotalRowCount(result.totalRowCount);
94
+ if (result.nextCursor !== void 0) cursorRef.current = result.nextCursor;
95
+ setHasMore(result.hasMore !== false);
96
+ if (append) setRows((prev) => {
97
+ const existingIds = new Set(prev.map(currentGetRowId));
98
+ const newRows = result.rows.filter((r) => !existingIds.has(currentGetRowId(r)));
99
+ return [...prev, ...newRows];
100
+ });
101
+ else setRows(result.rows);
102
+ hasDataRef.current = true;
103
+ pageIndexRef.current++;
104
+ }
105
+ } catch (err) {
106
+ if (controller.signal.aborted) return;
107
+ console.error("[DataGrid] Data source error:", err);
108
+ setError(err instanceof Error ? err : new Error(String(err)));
109
+ } finally {
110
+ if (!controller.signal.aborted) {
111
+ setIsLoading(false);
112
+ setIsRefetching(false);
113
+ setIsLoadingMore(false);
114
+ }
115
+ }
116
+ }, []);
117
+ useEffect(() => {
118
+ fetchPage(false).catch(() => {});
119
+ return () => abortRef.current?.abort();
120
+ }, [
121
+ fetchPage,
122
+ dataSource,
123
+ sortingKey,
124
+ quickSearchKey,
125
+ pagination.pageSize
126
+ ]);
127
+ useEffect(() => {
128
+ if (paginationMode !== "server") {
129
+ hasMountedServerPaginationRef.current = false;
130
+ return;
131
+ }
132
+ if (!hasMountedServerPaginationRef.current) {
133
+ hasMountedServerPaginationRef.current = true;
134
+ return;
135
+ }
136
+ fetchPage(false).catch(() => {});
137
+ }, [
138
+ fetchPage,
139
+ paginationMode,
140
+ pagination.pageIndex
141
+ ]);
142
+ return {
143
+ rows,
144
+ totalRowCount,
145
+ isLoading,
146
+ isRefetching,
147
+ isLoadingMore,
148
+ loadMore: useCallback(() => {
149
+ if (!isLoadingMore && hasMore && paginationMode === "infinite") fetchPage(true).catch(() => {});
150
+ }, [
151
+ isLoadingMore,
152
+ hasMore,
153
+ paginationMode,
154
+ fetchPage
155
+ ]),
156
+ hasMore,
157
+ reload: useCallback(() => {
158
+ fetchPage(false).catch(() => {});
159
+ }, [fetchPage]),
160
+ error
161
+ };
162
+ }
163
+ const NOOP_DATA_SOURCE = async function* () {};
164
+ const NOOP_GET_ROW_ID = () => "";
165
+ /**
166
+ * Hook that processes raw data through the grid's sort/pagination state
167
+ * and returns the `rows` slice ready to pass to `DataGrid`. This is the
168
+ * only correct way to feed client-side data into a grid.
169
+ *
170
+ * Two modes, picked by which prop you pass:
171
+ * - `data: TRow[]` → client-side mode. In-memory sort + paginate.
172
+ * - `dataSource: (params) => AsyncGenerator` → server / infinite mode.
173
+ * The generator yields pages as you scroll or change pages.
174
+ *
175
+ * ```tsx
176
+ * // Client-side (most common):
177
+ * const gridData = useDataSource({
178
+ * data: users,
179
+ * columns,
180
+ * getRowId: (row) => row.id,
181
+ * sorting: gridState.sorting,
182
+ * quickSearch: gridState.quickSearch,
183
+ * pagination: gridState.pagination,
184
+ * paginationMode: "client",
185
+ * });
186
+ *
187
+ * <DataGrid
188
+ * columns={columns}
189
+ * rows={gridData.rows}
190
+ * totalRowCount={gridData.totalRowCount}
191
+ * isLoading={gridData.isLoading}
192
+ * state={gridState}
193
+ * onChange={setGridState}
194
+ * getRowId={(row) => row.id}
195
+ * />
196
+ * ```
197
+ *
198
+ * Rules:
199
+ * - Call this hook unconditionally at the top level, before any early return.
200
+ * - `rows` on `DataGrid` must ALWAYS be `gridData.rows`, never your raw array.
201
+ * - For server or infinite pagination, use `dataSource` — see the
202
+ * `DataGridDataSource` type for the generator signature.
203
+ *
204
+ * Quick search:
205
+ * - Client mode (`data` prop): the hook auto-filters rows via
206
+ * `applyQuickSearch` using a default case-insensitive substring match
207
+ * across every column. Override with `matchRow` for custom matching
208
+ * (fuzzy, weighted, field-specific, etc.).
209
+ * - Async mode (`dataSource` prop): the hook passes `quickSearch` into
210
+ * `params.quickSearch` and re-runs the generator whenever the search
211
+ * string changes. The consumer owns the matching logic (typically by
212
+ * folding it into a backend query). The grid performs NO client-side
213
+ * filtering in async mode.
214
+ */
215
+ function useDataSource(opts) {
216
+ const { data, dataSource, columns, getRowId, sorting, quickSearch, matchRow = defaultMatchRow, pagination, paginationMode } = opts;
217
+ const isClientMode = data != null && !dataSource;
218
+ if (process.env.NODE_ENV !== "production" && data == null && dataSource == null) console.warn("[useDataSource] neither `data` nor `dataSource` was provided — the grid will render empty indefinitely. Pass one or the other.");
219
+ if (process.env.NODE_ENV !== "production" && isClientMode && paginationMode === "infinite" && data.length > 0) console.warn("[useDataSource] `paginationMode: \"infinite\"` with a `data` array skips pagination entirely. Prefer `\"client\"` for in-memory lists or `\"server\"` + a `dataSource` generator for real paging.");
220
+ const clientResult = useClientDataSource({
221
+ data: data ?? [],
222
+ columns,
223
+ sorting,
224
+ quickSearch,
225
+ matchRow,
226
+ pagination,
227
+ paginationMode
228
+ });
229
+ const asyncResult = useAsyncDataSource({
230
+ dataSource: dataSource ?? NOOP_DATA_SOURCE,
231
+ getRowId: dataSource ? getRowId : NOOP_GET_ROW_ID,
232
+ sorting,
233
+ quickSearch,
234
+ pagination,
235
+ paginationMode
236
+ });
237
+ return isClientMode ? clientResult : asyncResult;
238
+ }
239
+
240
+ //#endregion
241
+ export { useDataSource };
242
+ //# sourceMappingURL=use-data-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-data-source.js","names":[],"sources":["../../../../src/components/data-grid/use-data-source.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n DataGridColumnDef,\n DataGridDataSource,\n DataGridFetchParams,\n DataGridDataPaginationMode,\n DataGridPaginationModel,\n DataGridSortModel,\n RowId,\n} from \"./types\";\nimport {\n applyQuickSearch,\n buildRowComparator,\n defaultMatchRow,\n paginateRows,\n} from \"./state\";\n\nexport type UseDataSourceResult<TRow> = {\n /** All rows currently loaded (for infinite mode, the accumulated set). */\n rows: readonly TRow[];\n /** Total row count if known. */\n totalRowCount: number | undefined;\n /** Whether the initial load is in progress (no data at all yet). */\n isLoading: boolean;\n /** Whether a background refetch is happening (data already shown). */\n isRefetching: boolean;\n /** Whether more rows are being fetched (infinite scroll). */\n isLoadingMore: boolean;\n /** Request the next page (infinite scroll). */\n loadMore: () => void;\n /** Whether there are more pages to load. */\n hasMore: boolean;\n /** Reload from scratch. */\n reload: () => void;\n /**\n * Error from the most recent async fetch, if any. Consumers should render\n * an error UI and offer a `reload()` button when this is non-null. Client\n * mode never sets this (no fetching). Cleared on next successful fetch.\n */\n error: Error | null;\n};\n\n// ─── Client-side hook ────────────────────────────────────────────────\n// Memoised so resize / selection / other unrelated state changes\n// don't recompute or create new array references.\n\nfunction useClientDataSource<TRow>(opts: {\n data: readonly TRow[];\n columns: readonly DataGridColumnDef<TRow>[];\n sorting: DataGridSortModel;\n quickSearch: string;\n matchRow: (\n row: TRow,\n query: string,\n columns: readonly DataGridColumnDef<TRow>[],\n ) => boolean;\n pagination: DataGridPaginationModel;\n paginationMode: DataGridDataPaginationMode;\n}): UseDataSourceResult<TRow> {\n const { data, columns, sorting, quickSearch, matchRow, pagination, paginationMode } = opts;\n\n // Stable serialised keys so useMemo only fires on real changes\n const sortingKey = JSON.stringify(sorting);\n\n const processed = useMemo(() => {\n // Quick search is applied FIRST, on the full input. If nothing is\n // typed this is a zero-cost no-op (applyQuickSearch returns the\n // original array reference). Sort and paginate operate on the\n // already-filtered set so the result counts are search-aware.\n const searched = applyQuickSearch(data, quickSearch, columns, matchRow);\n const comparator = buildRowComparator(sorting, columns);\n const sorted = comparator ? [...searched].sort(comparator) : searched;\n const totalRowCount = sorted.length;\n const paged =\n paginationMode === \"client\"\n ? paginateRows(sorted as readonly TRow[], pagination)\n : sorted;\n return { rows: paged, totalRowCount };\n }, [data, sortingKey, quickSearch, matchRow, pagination.pageIndex, pagination.pageSize, paginationMode, columns]);\n\n return useMemo(() => ({\n rows: processed.rows,\n totalRowCount: processed.totalRowCount,\n isLoading: false,\n isRefetching: false,\n isLoadingMore: false,\n loadMore: () => {},\n hasMore: false,\n reload: () => {},\n error: null,\n }), [processed]);\n}\n\n// ─── Async data source hook ──────────────────────────────────────────\n// Key behaviour: when refetching (sort change), we keep showing the old\n// rows and set `isRefetching` instead of `isLoading`. This avoids the\n// jarring flash-to-skeleton on every sort toggle.\n\nfunction useAsyncDataSource<TRow>(opts: {\n dataSource: DataGridDataSource<TRow>;\n getRowId: (row: TRow) => RowId;\n sorting: DataGridSortModel;\n quickSearch: string;\n pagination: DataGridPaginationModel;\n paginationMode: DataGridDataPaginationMode;\n}): UseDataSourceResult<TRow> {\n const {\n dataSource,\n getRowId,\n sorting,\n quickSearch,\n pagination,\n paginationMode,\n } = opts;\n\n const [rows, setRows] = useState<TRow[]>([]);\n const [totalRowCount, setTotalRowCount] = useState<number | undefined>(undefined);\n const [isLoading, setIsLoading] = useState(true);\n const [isRefetching, setIsRefetching] = useState(false);\n const [isLoadingMore, setIsLoadingMore] = useState(false);\n const [hasMore, setHasMore] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const cursorRef = useRef<unknown>(undefined);\n const abortRef = useRef<AbortController | null>(null);\n const pageIndexRef = useRef(0);\n const hasDataRef = useRef(false);\n const hasMountedServerPaginationRef = useRef(false);\n\n const latestArgsRef = useRef({\n dataSource,\n getRowId,\n sorting,\n quickSearch,\n pagination,\n });\n latestArgsRef.current = { dataSource, getRowId, sorting, quickSearch, pagination };\n\n const sortingKey = JSON.stringify(sorting);\n const quickSearchKey = quickSearch;\n\n const fetchPage = useCallback(\n async (append: boolean) => {\n const {\n dataSource: currentDataSource,\n getRowId: currentGetRowId,\n sorting: currentSorting,\n quickSearch: currentQuickSearch,\n pagination: currentPagination,\n } = latestArgsRef.current;\n\n abortRef.current?.abort();\n const controller = new AbortController();\n abortRef.current = controller;\n\n if (append) {\n setIsLoadingMore(true);\n } else {\n // First load → skeleton. Subsequent → subtle refetch indicator.\n if (hasDataRef.current) {\n setIsRefetching(true);\n } else {\n setIsLoading(true);\n }\n cursorRef.current = undefined;\n pageIndexRef.current = 0;\n }\n // Clear previous error at the start of a new attempt; we'll set it\n // again if this attempt fails.\n setError(null);\n\n try {\n const params: DataGridFetchParams = {\n sorting: currentSorting,\n quickSearch: currentQuickSearch,\n pagination: append\n ? { pageIndex: pageIndexRef.current, pageSize: currentPagination.pageSize }\n : currentPagination,\n cursor: cursorRef.current,\n };\n\n const gen = currentDataSource(params);\n\n for await (const result of gen) {\n if (controller.signal.aborted) return;\n\n if (result.totalRowCount != null) {\n setTotalRowCount(result.totalRowCount);\n }\n if (result.nextCursor !== undefined) {\n cursorRef.current = result.nextCursor;\n }\n setHasMore(result.hasMore !== false);\n\n if (append) {\n setRows((prev) => {\n const existingIds = new Set(prev.map(currentGetRowId));\n const newRows = result.rows.filter(\n (r) => !existingIds.has(currentGetRowId(r)),\n );\n return [...prev, ...newRows];\n });\n } else {\n setRows(result.rows);\n }\n\n hasDataRef.current = true;\n pageIndexRef.current++;\n }\n } catch (err) {\n if (controller.signal.aborted) return;\n // Surface the error on the result so consumers can render retry UI.\n // Still log to console so it's visible in dev without forcing every\n // consumer to wire up error rendering.\n // eslint-disable-next-line no-console\n console.error(\"[DataGrid] Data source error:\", err);\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n if (!controller.signal.aborted) {\n setIsLoading(false);\n setIsRefetching(false);\n setIsLoadingMore(false);\n }\n }\n },\n [],\n );\n\n useEffect(() => {\n fetchPage(false).catch(() => {});\n return () => abortRef.current?.abort();\n // Also refetches when `dataSource` identity changes — consumers encode\n // external filter state into the generator's closure, so a new\n // generator reference is the signal that the query changed.\n }, [fetchPage, dataSource, sortingKey, quickSearchKey, pagination.pageSize]);\n\n useEffect(() => {\n if (paginationMode !== \"server\") {\n hasMountedServerPaginationRef.current = false;\n return;\n }\n if (!hasMountedServerPaginationRef.current) {\n hasMountedServerPaginationRef.current = true;\n return;\n }\n fetchPage(false).catch(() => {});\n }, [fetchPage, paginationMode, pagination.pageIndex]);\n\n const loadMore = useCallback(() => {\n if (!isLoadingMore && hasMore && paginationMode === \"infinite\") {\n fetchPage(true).catch(() => {});\n }\n }, [isLoadingMore, hasMore, paginationMode, fetchPage]);\n\n const reload = useCallback(() => {\n fetchPage(false).catch(() => {});\n }, [fetchPage]);\n\n return {\n rows,\n totalRowCount,\n isLoading,\n isRefetching,\n isLoadingMore,\n loadMore,\n hasMore,\n reload,\n error,\n };\n}\n\n// ─── Noop data source (stable reference) ─────────────────────────────\nconst NOOP_DATA_SOURCE: DataGridDataSource<any> = async function* () {};\nconst NOOP_GET_ROW_ID = () => \"\";\n\n// ─── Public hook ─────────────────────────────────────────────────────\n// Both inner hooks are always called (React rules-of-hooks) but only\n// one provides the returned result.\n\n/**\n * Hook that processes raw data through the grid's sort/pagination state\n * and returns the `rows` slice ready to pass to `DataGrid`. This is the\n * only correct way to feed client-side data into a grid.\n *\n * Two modes, picked by which prop you pass:\n * - `data: TRow[]` → client-side mode. In-memory sort + paginate.\n * - `dataSource: (params) => AsyncGenerator` → server / infinite mode.\n * The generator yields pages as you scroll or change pages.\n *\n * ```tsx\n * // Client-side (most common):\n * const gridData = useDataSource({\n * data: users,\n * columns,\n * getRowId: (row) => row.id,\n * sorting: gridState.sorting,\n * quickSearch: gridState.quickSearch,\n * pagination: gridState.pagination,\n * paginationMode: \"client\",\n * });\n *\n * <DataGrid\n * columns={columns}\n * rows={gridData.rows}\n * totalRowCount={gridData.totalRowCount}\n * isLoading={gridData.isLoading}\n * state={gridState}\n * onChange={setGridState}\n * getRowId={(row) => row.id}\n * />\n * ```\n *\n * Rules:\n * - Call this hook unconditionally at the top level, before any early return.\n * - `rows` on `DataGrid` must ALWAYS be `gridData.rows`, never your raw array.\n * - For server or infinite pagination, use `dataSource` — see the\n * `DataGridDataSource` type for the generator signature.\n *\n * Quick search:\n * - Client mode (`data` prop): the hook auto-filters rows via\n * `applyQuickSearch` using a default case-insensitive substring match\n * across every column. Override with `matchRow` for custom matching\n * (fuzzy, weighted, field-specific, etc.).\n * - Async mode (`dataSource` prop): the hook passes `quickSearch` into\n * `params.quickSearch` and re-runs the generator whenever the search\n * string changes. The consumer owns the matching logic (typically by\n * folding it into a backend query). The grid performs NO client-side\n * filtering in async mode.\n */\nexport function useDataSource<TRow>(opts: {\n data?: readonly TRow[];\n dataSource?: DataGridDataSource<TRow>;\n columns: readonly DataGridColumnDef<TRow>[];\n getRowId: (row: TRow) => RowId;\n sorting: DataGridSortModel;\n /** Current quick-search text, typically `gridState.quickSearch`. */\n quickSearch: string;\n /** Override the default client-mode matcher. Ignored in async mode\n * (there the generator is the matcher). */\n matchRow?: (\n row: TRow,\n query: string,\n columns: readonly DataGridColumnDef<TRow>[],\n ) => boolean;\n pagination: DataGridPaginationModel;\n paginationMode: DataGridDataPaginationMode;\n}): UseDataSourceResult<TRow> {\n const {\n data,\n dataSource,\n columns,\n getRowId,\n sorting,\n quickSearch,\n matchRow = defaultMatchRow,\n pagination,\n paginationMode,\n } = opts;\n\n const isClientMode = data != null && !dataSource;\n\n if (process.env.NODE_ENV !== \"production\" && data == null && dataSource == null) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[useDataSource] neither `data` nor `dataSource` was provided — \"\n + \"the grid will render empty indefinitely. Pass one or the other.\"\n );\n }\n\n // Common footgun: consumers pass `data` as a fully-materialized array and\n // set `paginationMode: \"infinite\"` expecting the grid to page through it.\n // In client mode \"infinite\" skips `paginateRows` and returns every row;\n // `hasMore` / `loadMore` on the result are always false/no-ops. If you\n // want real paging, switch to `paginationMode: \"server\"` with a\n // `dataSource` generator. If you want in-memory slicing, use `\"client\"`.\n // If you're manually accumulating rows into `data` and driving the grid's\n // sentinel via your own `hasMore`/`onLoadMore`, this warning is a hint —\n // but current behavior (full list + external sentinel) is intentional.\n if (\n process.env.NODE_ENV !== \"production\"\n && isClientMode\n && paginationMode === \"infinite\"\n && data.length > 0\n ) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[useDataSource] `paginationMode: \\\"infinite\\\"` with a `data` array \"\n + \"skips pagination entirely. Prefer `\\\"client\\\"` for in-memory lists \"\n + \"or `\\\"server\\\"` + a `dataSource` generator for real paging.\"\n );\n }\n\n const clientResult = useClientDataSource({\n data: data ?? [],\n columns,\n sorting,\n quickSearch,\n matchRow,\n pagination,\n paginationMode,\n });\n\n const asyncResult = useAsyncDataSource({\n dataSource: dataSource ?? NOOP_DATA_SOURCE,\n getRowId: dataSource ? getRowId : NOOP_GET_ROW_ID,\n sorting,\n quickSearch,\n pagination,\n paginationMode,\n });\n\n return isClientMode ? clientResult : asyncResult;\n}\n"],"mappings":";;;;AA8CA,SAAS,oBAA0B,MAYL;CAC5B,MAAM,EAAE,MAAM,SAAS,SAAS,aAAa,UAAU,YAAY,mBAAmB;CAKtF,MAAM,YAAY,cAAc;EAK9B,MAAM,WAAW,iBAAiB,MAAM,aAAa,SAAS,SAAS;EACvE,MAAM,aAAa,mBAAmB,SAAS,QAAQ;EACvD,MAAM,SAAS,aAAa,CAAC,GAAG,SAAS,CAAC,KAAK,WAAW,GAAG;EAC7D,MAAM,gBAAgB,OAAO;AAK7B,SAAO;GAAE,MAHP,mBAAmB,WACf,aAAa,QAA2B,WAAW,GACnD;GACgB;GAAe;IACpC;EAAC;EAhBe,KAAK,UAAU,QAAQ;EAgBpB;EAAa;EAAU,WAAW;EAAW,WAAW;EAAU;EAAgB;EAAQ,CAAC;AAEjH,QAAO,eAAe;EACpB,MAAM,UAAU;EAChB,eAAe,UAAU;EACzB,WAAW;EACX,cAAc;EACd,eAAe;EACf,gBAAgB;EAChB,SAAS;EACT,cAAc;EACd,OAAO;EACR,GAAG,CAAC,UAAU,CAAC;;AAQlB,SAAS,mBAAyB,MAOJ;CAC5B,MAAM,EACJ,YACA,UACA,SACA,aACA,YACA,mBACE;CAEJ,MAAM,CAAC,MAAM,WAAW,SAAiB,EAAE,CAAC;CAC5C,MAAM,CAAC,eAAe,oBAAoB,SAA6B,OAAU;CACjF,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,cAAc,mBAAmB,SAAS,MAAM;CACvD,MAAM,CAAC,eAAe,oBAAoB,SAAS,MAAM;CACzD,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,CAAC,OAAO,YAAY,SAAuB,KAAK;CAEtD,MAAM,YAAY,OAAgB,OAAU;CAC5C,MAAM,WAAW,OAA+B,KAAK;CACrD,MAAM,eAAe,OAAO,EAAE;CAC9B,MAAM,aAAa,OAAO,MAAM;CAChC,MAAM,gCAAgC,OAAO,MAAM;CAEnD,MAAM,gBAAgB,OAAO;EAC3B;EACA;EACA;EACA;EACA;EACD,CAAC;AACF,eAAc,UAAU;EAAE;EAAY;EAAU;EAAS;EAAa;EAAY;CAElF,MAAM,aAAa,KAAK,UAAU,QAAQ;CAC1C,MAAM,iBAAiB;CAEvB,MAAM,YAAY,YAChB,OAAO,WAAoB;EACzB,MAAM,EACJ,YAAY,mBACZ,UAAU,iBACV,SAAS,gBACT,aAAa,oBACb,YAAY,sBACV,cAAc;AAElB,WAAS,SAAS,OAAO;EACzB,MAAM,aAAa,IAAI,iBAAiB;AACxC,WAAS,UAAU;AAEnB,MAAI,OACF,kBAAiB,KAAK;OACjB;AAEL,OAAI,WAAW,QACb,iBAAgB,KAAK;OAErB,cAAa,KAAK;AAEpB,aAAU,UAAU;AACpB,gBAAa,UAAU;;AAIzB,WAAS,KAAK;AAEd,MAAI;GAUF,MAAM,MAAM,kBATwB;IAClC,SAAS;IACT,aAAa;IACb,YAAY,SACR;KAAE,WAAW,aAAa;KAAS,UAAU,kBAAkB;KAAU,GACzE;IACJ,QAAQ,UAAU;IACnB,CAEoC;AAErC,cAAW,MAAM,UAAU,KAAK;AAC9B,QAAI,WAAW,OAAO,QAAS;AAE/B,QAAI,OAAO,iBAAiB,KAC1B,kBAAiB,OAAO,cAAc;AAExC,QAAI,OAAO,eAAe,OACxB,WAAU,UAAU,OAAO;AAE7B,eAAW,OAAO,YAAY,MAAM;AAEpC,QAAI,OACF,UAAS,SAAS;KAChB,MAAM,cAAc,IAAI,IAAI,KAAK,IAAI,gBAAgB,CAAC;KACtD,MAAM,UAAU,OAAO,KAAK,QACzB,MAAM,CAAC,YAAY,IAAI,gBAAgB,EAAE,CAAC,CAC5C;AACD,YAAO,CAAC,GAAG,MAAM,GAAG,QAAQ;MAC5B;QAEF,SAAQ,OAAO,KAAK;AAGtB,eAAW,UAAU;AACrB,iBAAa;;WAER,KAAK;AACZ,OAAI,WAAW,OAAO,QAAS;AAK/B,WAAQ,MAAM,iCAAiC,IAAI;AACnD,YAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,CAAC;YACrD;AACR,OAAI,CAAC,WAAW,OAAO,SAAS;AAC9B,iBAAa,MAAM;AACnB,oBAAgB,MAAM;AACtB,qBAAiB,MAAM;;;IAI7B,EAAE,CACH;AAED,iBAAgB;AACd,YAAU,MAAM,CAAC,YAAY,GAAG;AAChC,eAAa,SAAS,SAAS,OAAO;IAIrC;EAAC;EAAW;EAAY;EAAY;EAAgB,WAAW;EAAS,CAAC;AAE5E,iBAAgB;AACd,MAAI,mBAAmB,UAAU;AAC/B,iCAA8B,UAAU;AACxC;;AAEF,MAAI,CAAC,8BAA8B,SAAS;AAC1C,iCAA8B,UAAU;AACxC;;AAEF,YAAU,MAAM,CAAC,YAAY,GAAG;IAC/B;EAAC;EAAW;EAAgB,WAAW;EAAU,CAAC;AAYrD,QAAO;EACL;EACA;EACA;EACA;EACA;EACA,UAhBe,kBAAkB;AACjC,OAAI,CAAC,iBAAiB,WAAW,mBAAmB,WAClD,WAAU,KAAK,CAAC,YAAY,GAAG;KAEhC;GAAC;GAAe;GAAS;GAAgB;GAAU,CAAC;EAarD;EACA,QAZa,kBAAkB;AAC/B,aAAU,MAAM,CAAC,YAAY,GAAG;KAC/B,CAAC,UAAU,CAAC;EAWb;EACD;;AAIH,MAAM,mBAA4C,mBAAmB;AACrE,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwD9B,SAAgB,cAAoB,MAiBN;CAC5B,MAAM,EACJ,MACA,YACA,SACA,UACA,SACA,aACA,WAAW,iBACX,YACA,mBACE;CAEJ,MAAM,eAAe,QAAQ,QAAQ,CAAC;AAEtC,KAAI,QAAQ,IAAI,aAAa,gBAAgB,QAAQ,QAAQ,cAAc,KAEzE,SAAQ,KACN,iIAED;AAYH,KACE,QAAQ,IAAI,aAAa,gBACtB,gBACA,mBAAmB,cACnB,KAAK,SAAS,EAGjB,SAAQ,KACN,oMAGD;CAGH,MAAM,eAAe,oBAAoB;EACvC,MAAM,QAAQ,EAAE;EAChB;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;CAEF,MAAM,cAAc,mBAAmB;EACrC,YAAY,cAAc;EAC1B,UAAU,aAAa,WAAW;EAClC;EACA;EACA;EACA;EACD,CAAC;AAEF,QAAO,eAAe,eAAe"}
@@ -0,0 +1,38 @@
1
+ import { DataGridColumnDef, DataGridState } from "./types";
2
+
3
+ //#region src/components/data-grid/use-url-state.d.ts
4
+ type UrlStateOptions = {
5
+ /** Disambiguates URL params when multiple grids share a page. Defaults
6
+ * to `"grid"`. Use unique prefixes per-grid (e.g. `"users"`, `"teams"`). */
7
+ paramPrefix?: string;
8
+ /** Overrides for default state used when the URL has no value for a
9
+ * given key. Useful for things like a sensible initial sort (e.g.
10
+ * "newest signups first") that should appear on first load but be
11
+ * overridden when the user navigates to a bookmarked URL. */
12
+ initial?: Partial<Pick<DataGridState, "sorting" | "quickSearch" | "columnVisibility">>;
13
+ };
14
+ /**
15
+ * Drop-in replacement for `useState(() => createDefaultDataGridState(columns))`
16
+ * that persists user view preferences to URL search params, so a view
17
+ * can be bookmarked / shared / restored on reload.
18
+ *
19
+ * **Persisted:** column widths, hidden columns, sort model, quick-search.
20
+ * **Not persisted** (deliberately): pagination scroll position, selection,
21
+ * column pinning/order, date display mode — these are session-scoped.
22
+ *
23
+ * ```tsx
24
+ * const [gridState, setGridState] = useDataGridUrlState(columns, {
25
+ * paramPrefix: "users",
26
+ * initial: { sorting: [{ columnId: "signedUpAt", direction: "desc" }] },
27
+ * });
28
+ * ```
29
+ *
30
+ * URL encoding: `?{prefix}_w=...&{prefix}_h=...&{prefix}_s=...&{prefix}_q=...`.
31
+ * Default values are omitted so URLs stay clean. Updates use
32
+ * `history.replaceState` (not pushState) so back/forward isn't polluted,
33
+ * and `popstate` is observed so external URL changes flow back into state.
34
+ */
35
+ declare function useDataGridUrlState<TRow>(columns: readonly DataGridColumnDef<TRow>[], opts?: UrlStateOptions): [DataGridState, React.Dispatch<React.SetStateAction<DataGridState>>];
36
+ //#endregion
37
+ export { useDataGridUrlState };
38
+ //# sourceMappingURL=use-url-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-url-state.d.ts","names":[],"sources":["../../../../src/components/data-grid/use-url-state.ts"],"mappings":";;;KAqJK,eAAA;;AA3IY;EA8If,WAAA;;;;;EAKA,OAAA,GAAU,OAAA,CAAQ,IAAA,CAAK,aAAA;AAAA;;;;;;;;AAwBzB;;;;;;;;;;;;;;iBAAgB,mBAAA,MAAA,CACd,OAAA,WAAkB,iBAAA,CAAkB,IAAA,KACpC,IAAA,GAAO,eAAA,IACL,aAAA,EAAe,KAAA,CAAM,QAAA,CAAS,KAAA,CAAM,cAAA,CAAe,aAAA"}
@@ -0,0 +1,212 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { createDefaultDataGridState } from "./state.js";
5
+ import { DEFAULT_COL_WIDTH, clampColumnWidth } from "./data-grid-sizing.js";
6
+
7
+ //#region src/components/data-grid/use-url-state.ts
8
+ function serializeWidths(widths, columns) {
9
+ const parts = [];
10
+ for (const col of columns) {
11
+ const w = widths[col.id];
12
+ if (typeof w !== "number" || !Number.isFinite(w)) continue;
13
+ const defaultW = clampColumnWidth(col, col.width ?? DEFAULT_COL_WIDTH);
14
+ if (Math.round(w) === Math.round(defaultW)) continue;
15
+ parts.push(`${encodeURIComponent(col.id)}:${Math.round(w)}`);
16
+ }
17
+ return parts.join(",");
18
+ }
19
+ function parseWidths(raw, fallback, columns) {
20
+ if (!raw) return fallback;
21
+ const colMap = new Map(columns.map((c) => [c.id, c]));
22
+ const out = { ...fallback };
23
+ for (const part of raw.split(",")) {
24
+ const colonIdx = part.indexOf(":");
25
+ if (colonIdx <= 0) continue;
26
+ const encodedId = part.slice(0, colonIdx);
27
+ const num = part.slice(colonIdx + 1);
28
+ let id;
29
+ try {
30
+ id = decodeURIComponent(encodedId);
31
+ } catch {
32
+ continue;
33
+ }
34
+ if (!id || !num) continue;
35
+ const col = colMap.get(id);
36
+ if (!col) continue;
37
+ const n = Number(num);
38
+ if (!Number.isFinite(n)) continue;
39
+ out[id] = clampColumnWidth(col, n);
40
+ }
41
+ return out;
42
+ }
43
+ function serializeHidden(visibility) {
44
+ return Object.entries(visibility).filter(([, v]) => v === false).map(([id]) => encodeURIComponent(id)).join(",");
45
+ }
46
+ function parseHidden(raw, columns) {
47
+ if (!raw) return {};
48
+ const known = new Set(columns.map((c) => c.id));
49
+ const out = {};
50
+ for (const encodedId of raw.split(",")) {
51
+ let id;
52
+ try {
53
+ id = decodeURIComponent(encodedId);
54
+ } catch {
55
+ continue;
56
+ }
57
+ if (id && known.has(id)) out[id] = false;
58
+ }
59
+ return out;
60
+ }
61
+ function serializeSort(sort) {
62
+ return sort.map((s) => `${encodeURIComponent(s.columnId)}:${s.direction === "desc" ? "desc" : "asc"}`).join(",");
63
+ }
64
+ function sortEqual(a, b) {
65
+ if (a.length !== b.length) return false;
66
+ for (let i = 0; i < a.length; i++) if (a[i].columnId !== b[i].columnId || a[i].direction !== b[i].direction) return false;
67
+ return true;
68
+ }
69
+ function parseSort(raw, fallback, columns) {
70
+ if (raw == null) return fallback;
71
+ if (raw === "") return [];
72
+ const known = new Set(columns.map((c) => c.id));
73
+ const out = [];
74
+ for (const part of raw.split(",")) {
75
+ const colonIdx = part.indexOf(":");
76
+ if (colonIdx <= 0) continue;
77
+ const encodedId = part.slice(0, colonIdx);
78
+ const dir = part.slice(colonIdx + 1);
79
+ if (dir !== "asc" && dir !== "desc") continue;
80
+ let id;
81
+ try {
82
+ id = decodeURIComponent(encodedId);
83
+ } catch {
84
+ continue;
85
+ }
86
+ if (!id || !known.has(id)) continue;
87
+ out.push({
88
+ columnId: id,
89
+ direction: dir
90
+ });
91
+ }
92
+ return out;
93
+ }
94
+ function parseQuickSearch(raw, fallback) {
95
+ if (raw == null) return fallback;
96
+ return raw;
97
+ }
98
+ /**
99
+ * Drop-in replacement for `useState(() => createDefaultDataGridState(columns))`
100
+ * that persists user view preferences to URL search params, so a view
101
+ * can be bookmarked / shared / restored on reload.
102
+ *
103
+ * **Persisted:** column widths, hidden columns, sort model, quick-search.
104
+ * **Not persisted** (deliberately): pagination scroll position, selection,
105
+ * column pinning/order, date display mode — these are session-scoped.
106
+ *
107
+ * ```tsx
108
+ * const [gridState, setGridState] = useDataGridUrlState(columns, {
109
+ * paramPrefix: "users",
110
+ * initial: { sorting: [{ columnId: "signedUpAt", direction: "desc" }] },
111
+ * });
112
+ * ```
113
+ *
114
+ * URL encoding: `?{prefix}_w=...&{prefix}_h=...&{prefix}_s=...&{prefix}_q=...`.
115
+ * Default values are omitted so URLs stay clean. Updates use
116
+ * `history.replaceState` (not pushState) so back/forward isn't polluted,
117
+ * and `popstate` is observed so external URL changes flow back into state.
118
+ */
119
+ function useDataGridUrlState(columns, opts) {
120
+ const prefix = opts?.paramPrefix ?? "grid";
121
+ const widthsKey = `${prefix}_w`;
122
+ const hiddenKey = `${prefix}_h`;
123
+ const sortKey = `${prefix}_s`;
124
+ const searchKey = `${prefix}_q`;
125
+ const columnsRef = useRef(columns);
126
+ columnsRef.current = columns;
127
+ const initialRef = useRef(opts?.initial);
128
+ const [state, setState] = useState(() => {
129
+ const base = createDefaultDataGridState(columns);
130
+ const initial = initialRef.current ?? {};
131
+ const baseWithInitial = {
132
+ ...base,
133
+ sorting: initial.sorting ?? base.sorting,
134
+ quickSearch: initial.quickSearch ?? base.quickSearch,
135
+ columnVisibility: initial.columnVisibility ?? base.columnVisibility
136
+ };
137
+ if (typeof window === "undefined") return baseWithInitial;
138
+ const params = new URLSearchParams(window.location.search);
139
+ return {
140
+ ...baseWithInitial,
141
+ columnWidths: parseWidths(params.get(widthsKey), base.columnWidths, columns),
142
+ columnVisibility: params.get(hiddenKey) != null ? parseHidden(params.get(hiddenKey), columns) : baseWithInitial.columnVisibility,
143
+ sorting: parseSort(params.get(sortKey), baseWithInitial.sorting, columns),
144
+ quickSearch: parseQuickSearch(params.get(searchKey), baseWithInitial.quickSearch)
145
+ };
146
+ });
147
+ useEffect(() => {
148
+ if (typeof window === "undefined") return;
149
+ const timer = setTimeout(() => {
150
+ const params = new URLSearchParams(window.location.search);
151
+ const before = params.toString();
152
+ const cols = columnsRef.current;
153
+ const initial = initialRef.current ?? {};
154
+ const widthsStr = serializeWidths(state.columnWidths, cols);
155
+ if (widthsStr) params.set(widthsKey, widthsStr);
156
+ else params.delete(widthsKey);
157
+ const hiddenStr = serializeHidden(state.columnVisibility);
158
+ const initialHidden = initial.columnVisibility ? serializeHidden(initial.columnVisibility) : "";
159
+ if (hiddenStr) params.set(hiddenKey, hiddenStr);
160
+ else if (initialHidden) params.set(hiddenKey, "");
161
+ else params.delete(hiddenKey);
162
+ const initialSort = initial.sorting ?? [];
163
+ if (sortEqual(state.sorting, initialSort)) params.delete(sortKey);
164
+ else params.set(sortKey, serializeSort(state.sorting));
165
+ const initialSearch = initial.quickSearch ?? "";
166
+ if (state.quickSearch === initialSearch) params.delete(searchKey);
167
+ else params.set(searchKey, state.quickSearch);
168
+ const after = params.toString();
169
+ if (before === after) return;
170
+ const url = `${window.location.pathname}${after ? `?${after}` : ""}${window.location.hash}`;
171
+ window.history.replaceState(window.history.state, "", url);
172
+ }, 100);
173
+ return () => clearTimeout(timer);
174
+ }, [
175
+ state.columnWidths,
176
+ state.columnVisibility,
177
+ state.sorting,
178
+ state.quickSearch,
179
+ widthsKey,
180
+ hiddenKey,
181
+ sortKey,
182
+ searchKey
183
+ ]);
184
+ useEffect(() => {
185
+ if (typeof window === "undefined") return;
186
+ const onPop = () => {
187
+ const params = new URLSearchParams(window.location.search);
188
+ const cols = columnsRef.current;
189
+ const initial = initialRef.current ?? {};
190
+ const defaults = createDefaultDataGridState(cols);
191
+ setState((prev) => ({
192
+ ...prev,
193
+ columnWidths: parseWidths(params.get(widthsKey), defaults.columnWidths, cols),
194
+ columnVisibility: params.get(hiddenKey) != null ? parseHidden(params.get(hiddenKey), cols) : initial.columnVisibility ?? defaults.columnVisibility,
195
+ sorting: parseSort(params.get(sortKey), initial.sorting ?? defaults.sorting, cols),
196
+ quickSearch: parseQuickSearch(params.get(searchKey), initial.quickSearch ?? defaults.quickSearch)
197
+ }));
198
+ };
199
+ window.addEventListener("popstate", onPop);
200
+ return () => window.removeEventListener("popstate", onPop);
201
+ }, [
202
+ widthsKey,
203
+ hiddenKey,
204
+ sortKey,
205
+ searchKey
206
+ ]);
207
+ return [state, setState];
208
+ }
209
+
210
+ //#endregion
211
+ export { useDataGridUrlState };
212
+ //# sourceMappingURL=use-url-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-url-state.js","names":[],"sources":["../../../../src/components/data-grid/use-url-state.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { DEFAULT_COL_WIDTH, clampColumnWidth } from \"./data-grid-sizing\";\nimport { createDefaultDataGridState } from \"./state\";\nimport type {\n DataGridColumnDef,\n DataGridSortItem,\n DataGridSortModel,\n DataGridState,\n} from \"./types\";\n\n// ─── URL <-> state encoding ──────────────────────────────────────────\n// Compact, human-readable formats so URLs stay short. Each piece of\n// state gets its own param so unrelated changes don't churn shared keys:\n//\n// ?{prefix}_w=name:200,email:300 column widths (only non-defaults)\n// ?{prefix}_h=createdAt,role hidden column ids\n// ?{prefix}_s=signedUpAt:desc,name:asc multi-column sort\n// ?{prefix}_q=alice quick-search text\n//\n// Column ids are URL-encoded so ids containing `,` `:` or other reserved\n// characters round-trip safely. Without encoding, an id like \"user:name\"\n// would silently break the parser.\n\n// ── widths ─────────────────────────────────────────────────────────\nfunction serializeWidths(\n widths: Record<string, number>,\n columns: readonly DataGridColumnDef<any>[],\n): string {\n const parts: string[] = [];\n for (const col of columns) {\n const w = widths[col.id];\n if (typeof w !== \"number\" || !Number.isFinite(w)) continue;\n const defaultW = clampColumnWidth(col, col.width ?? DEFAULT_COL_WIDTH);\n if (Math.round(w) === Math.round(defaultW)) continue;\n parts.push(`${encodeURIComponent(col.id)}:${Math.round(w)}`);\n }\n return parts.join(\",\");\n}\n\nfunction parseWidths(\n raw: string | null,\n fallback: Record<string, number>,\n columns: readonly DataGridColumnDef<any>[],\n): Record<string, number> {\n if (!raw) return fallback;\n const colMap = new Map(columns.map((c) => [c.id, c]));\n const out: Record<string, number> = { ...fallback };\n for (const part of raw.split(\",\")) {\n // Only split on the FIRST colon — id-side is always pre-encoded so it\n // can't contain a literal `:`, but width-side is always numeric.\n const colonIdx = part.indexOf(\":\");\n if (colonIdx <= 0) continue;\n const encodedId = part.slice(0, colonIdx);\n const num = part.slice(colonIdx + 1);\n let id: string;\n try {\n id = decodeURIComponent(encodedId);\n } catch {\n continue;\n }\n if (!id || !num) continue;\n const col = colMap.get(id);\n if (!col) continue;\n const n = Number(num);\n if (!Number.isFinite(n)) continue;\n out[id] = clampColumnWidth(col, n);\n }\n return out;\n}\n\n// ── hidden columns ─────────────────────────────────────────────────\nfunction serializeHidden(visibility: Record<string, boolean>): string {\n return Object.entries(visibility)\n .filter(([, v]) => v === false)\n .map(([id]) => encodeURIComponent(id))\n .join(\",\");\n}\n\nfunction parseHidden(\n raw: string | null,\n columns: readonly DataGridColumnDef<any>[],\n): Record<string, boolean> {\n if (!raw) return {};\n const known = new Set(columns.map((c) => c.id));\n const out: Record<string, boolean> = {};\n for (const encodedId of raw.split(\",\")) {\n let id: string;\n try {\n id = decodeURIComponent(encodedId);\n } catch {\n continue;\n }\n if (id && known.has(id)) out[id] = false;\n }\n return out;\n}\n\n// ── sort model ─────────────────────────────────────────────────────\nfunction serializeSort(sort: DataGridSortModel): string {\n return sort\n .map((s) => `${encodeURIComponent(s.columnId)}:${s.direction === \"desc\" ? \"desc\" : \"asc\"}`)\n .join(\",\");\n}\n\nfunction sortEqual(a: DataGridSortModel, b: DataGridSortModel): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i].columnId !== b[i].columnId || a[i].direction !== b[i].direction) return false;\n }\n return true;\n}\n\nfunction parseSort(\n raw: string | null,\n fallback: DataGridSortModel,\n columns: readonly DataGridColumnDef<any>[],\n): DataGridSortModel {\n if (raw == null) return fallback;\n if (raw === \"\") return [];\n const known = new Set(columns.map((c) => c.id));\n const out: DataGridSortItem[] = [];\n for (const part of raw.split(\",\")) {\n const colonIdx = part.indexOf(\":\");\n if (colonIdx <= 0) continue;\n const encodedId = part.slice(0, colonIdx);\n const dir = part.slice(colonIdx + 1);\n if (dir !== \"asc\" && dir !== \"desc\") continue;\n let id: string;\n try {\n id = decodeURIComponent(encodedId);\n } catch {\n continue;\n }\n if (!id || !known.has(id)) continue;\n out.push({ columnId: id, direction: dir });\n }\n return out;\n}\n\n// ── quick search ───────────────────────────────────────────────────\nfunction parseQuickSearch(raw: string | null, fallback: string): string {\n if (raw == null) return fallback;\n return raw;\n}\n\n// ─── Hook ────────────────────────────────────────────────────────────\n\ntype UrlStateOptions = {\n /** Disambiguates URL params when multiple grids share a page. Defaults\n * to `\"grid\"`. Use unique prefixes per-grid (e.g. `\"users\"`, `\"teams\"`). */\n paramPrefix?: string,\n /** Overrides for default state used when the URL has no value for a\n * given key. Useful for things like a sensible initial sort (e.g.\n * \"newest signups first\") that should appear on first load but be\n * overridden when the user navigates to a bookmarked URL. */\n initial?: Partial<Pick<DataGridState, \"sorting\" | \"quickSearch\" | \"columnVisibility\">>,\n};\n\n/**\n * Drop-in replacement for `useState(() => createDefaultDataGridState(columns))`\n * that persists user view preferences to URL search params, so a view\n * can be bookmarked / shared / restored on reload.\n *\n * **Persisted:** column widths, hidden columns, sort model, quick-search.\n * **Not persisted** (deliberately): pagination scroll position, selection,\n * column pinning/order, date display mode — these are session-scoped.\n *\n * ```tsx\n * const [gridState, setGridState] = useDataGridUrlState(columns, {\n * paramPrefix: \"users\",\n * initial: { sorting: [{ columnId: \"signedUpAt\", direction: \"desc\" }] },\n * });\n * ```\n *\n * URL encoding: `?{prefix}_w=...&{prefix}_h=...&{prefix}_s=...&{prefix}_q=...`.\n * Default values are omitted so URLs stay clean. Updates use\n * `history.replaceState` (not pushState) so back/forward isn't polluted,\n * and `popstate` is observed so external URL changes flow back into state.\n */\nexport function useDataGridUrlState<TRow>(\n columns: readonly DataGridColumnDef<TRow>[],\n opts?: UrlStateOptions,\n): [DataGridState, React.Dispatch<React.SetStateAction<DataGridState>>] {\n const prefix = opts?.paramPrefix ?? \"grid\";\n const widthsKey = `${prefix}_w`;\n const hiddenKey = `${prefix}_h`;\n const sortKey = `${prefix}_s`;\n const searchKey = `${prefix}_q`;\n\n const columnsRef = useRef(columns);\n columnsRef.current = columns;\n\n // `initial` snapshots are captured once on first render. Re-running them\n // each render would clobber the user's interactions.\n const initialRef = useRef(opts?.initial);\n\n const [state, setState] = useState<DataGridState>(() => {\n const base = createDefaultDataGridState(columns);\n const initial = initialRef.current ?? {};\n const baseWithInitial: DataGridState = {\n ...base,\n sorting: initial.sorting ?? base.sorting,\n quickSearch: initial.quickSearch ?? base.quickSearch,\n columnVisibility: initial.columnVisibility ?? base.columnVisibility,\n };\n if (typeof window === \"undefined\") return baseWithInitial;\n const params = new URLSearchParams(window.location.search);\n return {\n ...baseWithInitial,\n columnWidths: parseWidths(params.get(widthsKey), base.columnWidths, columns),\n columnVisibility: params.get(hiddenKey) != null\n ? parseHidden(params.get(hiddenKey), columns)\n : baseWithInitial.columnVisibility,\n sorting: parseSort(params.get(sortKey), baseWithInitial.sorting, columns),\n quickSearch: parseQuickSearch(params.get(searchKey), baseWithInitial.quickSearch),\n };\n });\n\n // Sync state -> URL. Debounced so that high-frequency state changes\n // (e.g. dragging a column resize handle, typing in the search box)\n // don't fire a URL write per pixel / per keystroke.\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const timer = setTimeout(() => {\n const params = new URLSearchParams(window.location.search);\n const before = params.toString();\n const cols = columnsRef.current;\n const initial = initialRef.current ?? {};\n\n const widthsStr = serializeWidths(state.columnWidths, cols);\n if (widthsStr) params.set(widthsKey, widthsStr);\n else params.delete(widthsKey);\n\n const hiddenStr = serializeHidden(state.columnVisibility);\n // If the consumer supplied initial visibility, \"no hidden cols\" must\n // be encoded explicitly (empty string) so a bookmark with no `_h=`\n // doesn't silently re-hide a column the user just un-hid.\n const initialHidden = initial.columnVisibility\n ? serializeHidden(initial.columnVisibility)\n : \"\";\n if (hiddenStr) params.set(hiddenKey, hiddenStr);\n else if (initialHidden) params.set(hiddenKey, \"\");\n else params.delete(hiddenKey);\n\n const initialSort = initial.sorting ?? [];\n if (sortEqual(state.sorting, initialSort)) params.delete(sortKey);\n else params.set(sortKey, serializeSort(state.sorting));\n\n const initialSearch = initial.quickSearch ?? \"\";\n if (state.quickSearch === initialSearch) params.delete(searchKey);\n else params.set(searchKey, state.quickSearch);\n\n const after = params.toString();\n if (before === after) return;\n const url = `${window.location.pathname}${after ? `?${after}` : \"\"}${window.location.hash}`;\n window.history.replaceState(window.history.state, \"\", url);\n }, 100);\n return () => clearTimeout(timer);\n }, [\n state.columnWidths,\n state.columnVisibility,\n state.sorting,\n state.quickSearch,\n widthsKey,\n hiddenKey,\n sortKey,\n searchKey,\n ]);\n\n // React to back/forward navigation.\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const onPop = () => {\n const params = new URLSearchParams(window.location.search);\n const cols = columnsRef.current;\n const initial = initialRef.current ?? {};\n // When the URL no longer has a value for a key, reset to defaults\n // rather than preserving the previous in-memory state — otherwise\n // navigating back to a clean URL leaves stale state.\n const defaults = createDefaultDataGridState(cols);\n setState((prev) => ({\n ...prev,\n columnWidths: parseWidths(params.get(widthsKey), defaults.columnWidths, cols),\n columnVisibility: params.get(hiddenKey) != null\n ? parseHidden(params.get(hiddenKey), cols)\n : (initial.columnVisibility ?? defaults.columnVisibility),\n sorting: parseSort(params.get(sortKey), initial.sorting ?? defaults.sorting, cols),\n quickSearch: parseQuickSearch(params.get(searchKey), initial.quickSearch ?? defaults.quickSearch),\n }));\n };\n window.addEventListener(\"popstate\", onPop);\n return () => window.removeEventListener(\"popstate\", onPop);\n }, [widthsKey, hiddenKey, sortKey, searchKey]);\n\n return [state, setState];\n}\n"],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { useDataGridUrlState } from "./use-url-state.js";
4
+
5
+ //#region src/components/data-grid/use-url-state.test.tsx
6
+ // @vitest-environment jsdom
7
+ const columns = [{
8
+ id: "name",
9
+ header: "Name",
10
+ accessor: () => "",
11
+ width: 160,
12
+ minWidth: 80,
13
+ type: "string"
14
+ }, {
15
+ id: "email",
16
+ header: "Email",
17
+ accessor: () => "",
18
+ width: 200,
19
+ minWidth: 80,
20
+ type: "string"
21
+ }];
22
+ function setUrl(search) {
23
+ window.history.replaceState(null, "", `/${search ? `?${search}` : ""}`);
24
+ }
25
+ beforeEach(() => {
26
+ setUrl("");
27
+ });
28
+ afterEach(() => {
29
+ setUrl("");
30
+ });
31
+ describe("useDataGridUrlState", () => {
32
+ it("initializes from existing URL params", () => {
33
+ setUrl("grid_w=name:240&grid_h=email");
34
+ const { result } = renderHook(() => useDataGridUrlState(columns));
35
+ const [state] = result.current;
36
+ expect(state.columnWidths.name).toBe(240);
37
+ expect(state.columnVisibility.email).toBe(false);
38
+ });
39
+ it("writes width changes back to the URL (after debounce)", async () => {
40
+ const { result } = renderHook(() => useDataGridUrlState(columns));
41
+ act(() => {
42
+ const [, setState] = result.current;
43
+ setState((prev) => ({
44
+ ...prev,
45
+ columnWidths: {
46
+ ...prev.columnWidths,
47
+ name: 250
48
+ }
49
+ }));
50
+ });
51
+ await new Promise((resolve) => setTimeout(resolve, 150));
52
+ expect(new URLSearchParams(window.location.search).get("grid_w")).toBe("name:250");
53
+ });
54
+ it("resets column widths to defaults when a popstate clears the URL", () => {
55
+ setUrl("grid_w=name:300");
56
+ const { result } = renderHook(() => useDataGridUrlState(columns));
57
+ expect(result.current[0].columnWidths.name).toBe(300);
58
+ act(() => {
59
+ setUrl("");
60
+ window.dispatchEvent(new PopStateEvent("popstate"));
61
+ });
62
+ expect(result.current[0].columnWidths.name).toBe(160);
63
+ });
64
+ it("isolates two grids on the same page via paramPrefix", async () => {
65
+ const { result: a } = renderHook(() => useDataGridUrlState(columns, { paramPrefix: "users" }));
66
+ const { result: b } = renderHook(() => useDataGridUrlState(columns, { paramPrefix: "teams" }));
67
+ act(() => {
68
+ a.current[1]((prev) => ({
69
+ ...prev,
70
+ columnWidths: {
71
+ ...prev.columnWidths,
72
+ name: 222
73
+ }
74
+ }));
75
+ });
76
+ await new Promise((resolve) => setTimeout(resolve, 150));
77
+ const params = new URLSearchParams(window.location.search);
78
+ expect(params.get("users_w")).toBe("name:222");
79
+ expect(params.get("teams_w")).toBeNull();
80
+ expect(b.current[0].columnWidths.name).toBe(160);
81
+ });
82
+ it("ignores malformed entries in the URL param without throwing", () => {
83
+ setUrl("grid_w=:,name:abc,name:240,bogusid:99");
84
+ const { result } = renderHook(() => useDataGridUrlState(columns));
85
+ expect(result.current[0].columnWidths.name).toBe(240);
86
+ });
87
+ });
88
+
89
+ //#endregion
90
+ export { };
91
+ //# sourceMappingURL=use-url-state.test.js.map