@yewdiel21/motion-kit 0.10.0 → 0.14.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 (365) hide show
  1. package/README.md +155 -93
  2. package/dist/auto/index.d.ts +53 -3
  3. package/dist/auto/index.js +101 -3
  4. package/dist/auto/index.js.map +1 -1
  5. package/dist/{chunk-IKQ5FODN.js → chunk-6P4P7NU6.js} +2 -218
  6. package/dist/chunk-6P4P7NU6.js.map +1 -0
  7. package/dist/chunk-CU6SXW3E.js +20334 -0
  8. package/dist/chunk-CU6SXW3E.js.map +1 -0
  9. package/dist/{chunk-XAKC2QY7.js → chunk-EY4BJKC4.js} +3 -28
  10. package/dist/chunk-EY4BJKC4.js.map +1 -0
  11. package/dist/chunk-QP4QJOEN.js +72 -0
  12. package/dist/chunk-QP4QJOEN.js.map +1 -0
  13. package/dist/css/components/activity-feed.css +240 -0
  14. package/dist/css/components/attachment.css +266 -0
  15. package/dist/css/components/breadcrumb.css +110 -0
  16. package/dist/css/components/chart-tooltip.css +168 -0
  17. package/dist/css/components/chart.css +296 -0
  18. package/dist/css/components/code-block.css +264 -0
  19. package/dist/css/components/color-area.css +82 -0
  20. package/dist/css/components/color-field.css +67 -0
  21. package/dist/css/components/color-picker.css +131 -0
  22. package/dist/css/components/color-slider.css +113 -0
  23. package/dist/css/components/command-search.css +252 -0
  24. package/dist/css/components/context-menu.css +23 -0
  25. package/dist/css/components/credit-card.css +288 -0
  26. package/dist/css/components/data-table.css +591 -0
  27. package/dist/css/components/date-range-picker.css +168 -0
  28. package/dist/css/components/date-time-picker.css +178 -0
  29. package/dist/css/components/drawer.css +58 -0
  30. package/dist/css/components/editable-table.css +201 -0
  31. package/dist/css/components/empty-state.css +61 -0
  32. package/dist/css/components/file-explorer.css +633 -0
  33. package/dist/css/components/filter-builder.css +232 -0
  34. package/dist/css/components/hold-confirm.css +87 -0
  35. package/dist/css/components/hover-card.css +13 -7
  36. package/dist/css/components/image-preview.css +195 -0
  37. package/dist/css/components/json-viewer.css +272 -0
  38. package/dist/css/components/like-button.css +78 -0
  39. package/dist/css/components/link-preview.css +275 -0
  40. package/dist/css/components/loading-button.css +77 -0
  41. package/dist/css/components/multi-select.css +267 -0
  42. package/dist/css/components/notification-bell.css +127 -0
  43. package/dist/css/components/password-strength.css +183 -0
  44. package/dist/css/components/popover.css +25 -0
  45. package/dist/css/components/progress.css +140 -0
  46. package/dist/css/components/questionnaire.css +217 -0
  47. package/dist/css/components/range-calendar.css +186 -0
  48. package/dist/css/components/range-slider.css +192 -0
  49. package/dist/css/components/resizable-panel.css +243 -0
  50. package/dist/css/components/rich-text-editor.css +284 -0
  51. package/dist/css/components/scratch-card.css +75 -0
  52. package/dist/css/components/skeleton.css +108 -0
  53. package/dist/css/components/split-view.css +19 -0
  54. package/dist/css/components/star-rating.css +80 -0
  55. package/dist/css/components/stat-card.css +164 -0
  56. package/dist/css/components/swipe-actions.css +105 -0
  57. package/dist/css/components/tag-input.css +244 -0
  58. package/dist/css/components/time-field.css +88 -0
  59. package/dist/css/components/timeline.css +219 -0
  60. package/dist/css/components/tooltip.css +79 -0
  61. package/dist/css/components/tree-view.css +210 -0
  62. package/dist/css/components/virtual-list.css +195 -0
  63. package/dist/css/tokens.css +185 -181
  64. package/dist/elements/index.d.ts +674 -5
  65. package/dist/elements/index.js +1361 -8
  66. package/dist/elements/index.js.map +1 -1
  67. package/dist/{engine-GDGwQnJE.d.ts → engine-Cn6IhE3z.d.ts} +1 -1
  68. package/dist/engine-QECQnGz1.d.ts +33 -0
  69. package/dist/index.css +9104 -189
  70. package/dist/index.d.ts +89 -5
  71. package/dist/index.js +243 -9
  72. package/dist/index.js.map +1 -1
  73. package/dist/react/index.d.ts +579 -23
  74. package/dist/react/index.js +2042 -7
  75. package/dist/react/index.js.map +1 -1
  76. package/dist/{reduced-BmBzKH94.d.ts → reduced-BBUusld6.d.ts} +9 -2
  77. package/dist/reveal-DEvp6U6T.d.ts +2381 -0
  78. package/dist/{store-DYb3tTsZ.d.ts → store-DjkTBX7f.d.ts} +2 -2
  79. package/package.json +1 -1
  80. package/registry.json +406 -1
  81. package/src/auto/auto-init.ts +98 -0
  82. package/src/components/activity-feed/README.md +56 -0
  83. package/src/components/activity-feed/activity-feed.css +239 -0
  84. package/src/components/activity-feed/element.ts +36 -0
  85. package/src/components/activity-feed/engine.ts +351 -0
  86. package/src/components/activity-feed/react.tsx +32 -0
  87. package/src/components/attachment/README.md +27 -0
  88. package/src/components/attachment/attachment.css +265 -0
  89. package/src/components/attachment/element.ts +50 -0
  90. package/src/components/attachment/engine.ts +469 -0
  91. package/src/components/attachment/react.tsx +65 -0
  92. package/src/components/breadcrumb/README.md +38 -0
  93. package/src/components/breadcrumb/breadcrumb.css +109 -0
  94. package/src/components/breadcrumb/element.ts +27 -0
  95. package/src/components/breadcrumb/engine.ts +249 -0
  96. package/src/components/breadcrumb/react.tsx +30 -0
  97. package/src/components/chart/README.md +24 -0
  98. package/src/components/chart/chart.css +295 -0
  99. package/src/components/chart/element.ts +41 -0
  100. package/src/components/chart/engine.ts +811 -0
  101. package/src/components/chart/react.tsx +50 -0
  102. package/src/components/chart-tooltip/README.md +53 -0
  103. package/src/components/chart-tooltip/chart-tooltip.css +167 -0
  104. package/src/components/chart-tooltip/element.ts +37 -0
  105. package/src/components/chart-tooltip/engine.ts +442 -0
  106. package/src/components/chart-tooltip/react.tsx +47 -0
  107. package/src/components/code-block/README.md +19 -0
  108. package/src/components/code-block/code-block.css +263 -0
  109. package/src/components/code-block/element.ts +34 -0
  110. package/src/components/code-block/engine.ts +254 -0
  111. package/src/components/code-block/highlight.ts +244 -0
  112. package/src/components/code-block/react.tsx +94 -0
  113. package/src/components/color-area/README.md +50 -0
  114. package/src/components/color-area/color-area.css +81 -0
  115. package/src/components/color-area/element.ts +26 -0
  116. package/src/components/color-area/engine.ts +246 -0
  117. package/src/components/color-area/react.tsx +42 -0
  118. package/src/components/color-field/README.md +43 -0
  119. package/src/components/color-field/color-field.css +66 -0
  120. package/src/components/color-field/element.ts +26 -0
  121. package/src/components/color-field/engine.ts +174 -0
  122. package/src/components/color-field/react.tsx +42 -0
  123. package/src/components/color-picker/README.md +45 -0
  124. package/src/components/color-picker/color-picker.css +130 -0
  125. package/src/components/color-picker/element.ts +45 -0
  126. package/src/components/color-picker/engine.ts +314 -0
  127. package/src/components/color-picker/react.tsx +47 -0
  128. package/src/components/color-slider/README.md +55 -0
  129. package/src/components/color-slider/color-slider.css +112 -0
  130. package/src/components/color-slider/element.ts +26 -0
  131. package/src/components/color-slider/engine.ts +234 -0
  132. package/src/components/color-slider/react.tsx +45 -0
  133. package/src/components/command-search/README.md +15 -0
  134. package/src/components/command-search/command-search.css +251 -0
  135. package/src/components/command-search/element.ts +36 -0
  136. package/src/components/command-search/engine.ts +356 -0
  137. package/src/components/command-search/match.ts +53 -0
  138. package/src/components/command-search/react.tsx +66 -0
  139. package/src/components/context-menu/README.md +31 -0
  140. package/src/components/context-menu/context-menu.css +22 -0
  141. package/src/components/context-menu/element.ts +40 -0
  142. package/src/components/context-menu/engine.ts +153 -0
  143. package/src/components/context-menu/react.tsx +57 -0
  144. package/src/components/credit-card/README.md +20 -0
  145. package/src/components/credit-card/card.ts +77 -0
  146. package/src/components/credit-card/credit-card.css +287 -0
  147. package/src/components/credit-card/element.ts +33 -0
  148. package/src/components/credit-card/engine.ts +410 -0
  149. package/src/components/credit-card/react.tsx +36 -0
  150. package/src/components/data-table/README.md +83 -0
  151. package/src/components/data-table/data-table.css +590 -0
  152. package/src/components/data-table/element.ts +54 -0
  153. package/src/components/data-table/engine.ts +1059 -0
  154. package/src/components/data-table/react.tsx +94 -0
  155. package/src/components/date-range-picker/README.md +51 -0
  156. package/src/components/date-range-picker/date-range-picker.css +167 -0
  157. package/src/components/date-range-picker/element.ts +52 -0
  158. package/src/components/date-range-picker/engine.ts +352 -0
  159. package/src/components/date-range-picker/react.tsx +57 -0
  160. package/src/components/date-time-picker/README.md +48 -0
  161. package/src/components/date-time-picker/date-time-picker.css +177 -0
  162. package/src/components/date-time-picker/element.ts +47 -0
  163. package/src/components/date-time-picker/engine.ts +350 -0
  164. package/src/components/date-time-picker/react.tsx +54 -0
  165. package/src/components/dialog/README.md +103 -102
  166. package/src/components/dialog/element.ts +123 -118
  167. package/src/components/dialog/engine.ts +449 -432
  168. package/src/components/drawer/README.md +28 -0
  169. package/src/components/drawer/drawer.css +57 -0
  170. package/src/components/drawer/element.ts +21 -0
  171. package/src/components/drawer/engine.ts +56 -0
  172. package/src/components/drawer/react.tsx +85 -0
  173. package/src/components/editable-table/README.md +67 -0
  174. package/src/components/editable-table/editable-table.css +200 -0
  175. package/src/components/editable-table/element.ts +45 -0
  176. package/src/components/editable-table/engine.ts +775 -0
  177. package/src/components/editable-table/react.tsx +79 -0
  178. package/src/components/empty-state/README.md +41 -0
  179. package/src/components/empty-state/element.ts +23 -0
  180. package/src/components/empty-state/empty-state.css +60 -0
  181. package/src/components/empty-state/engine.ts +232 -0
  182. package/src/components/empty-state/react.tsx +34 -0
  183. package/src/components/file-explorer/README.md +67 -0
  184. package/src/components/file-explorer/element.ts +39 -0
  185. package/src/components/file-explorer/engine.ts +1307 -0
  186. package/src/components/file-explorer/file-explorer.css +632 -0
  187. package/src/components/file-explorer/react.tsx +77 -0
  188. package/src/components/filter-builder/README.md +47 -0
  189. package/src/components/filter-builder/element.ts +34 -0
  190. package/src/components/filter-builder/engine.ts +365 -0
  191. package/src/components/filter-builder/filter-builder.css +231 -0
  192. package/src/components/filter-builder/react.tsx +36 -0
  193. package/src/components/hold-confirm/README.md +13 -0
  194. package/src/components/hold-confirm/element.ts +24 -0
  195. package/src/components/hold-confirm/engine.ts +216 -0
  196. package/src/components/hold-confirm/hold-confirm.css +86 -0
  197. package/src/components/hold-confirm/react.tsx +28 -0
  198. package/src/components/hover-card/README.md +6 -2
  199. package/src/components/hover-card/engine.ts +68 -6
  200. package/src/components/hover-card/hover-card.css +13 -7
  201. package/src/components/image-preview/README.md +16 -0
  202. package/src/components/image-preview/element.ts +26 -0
  203. package/src/components/image-preview/engine.ts +325 -0
  204. package/src/components/image-preview/image-preview.css +194 -0
  205. package/src/components/image-preview/react.tsx +43 -0
  206. package/src/components/json-viewer/README.md +56 -0
  207. package/src/components/json-viewer/element.ts +37 -0
  208. package/src/components/json-viewer/engine.ts +440 -0
  209. package/src/components/json-viewer/json-viewer.css +271 -0
  210. package/src/components/json-viewer/react.tsx +56 -0
  211. package/src/components/like-button/README.md +13 -0
  212. package/src/components/like-button/element.ts +47 -0
  213. package/src/components/like-button/engine.ts +145 -0
  214. package/src/components/like-button/like-button.css +77 -0
  215. package/src/components/like-button/react.tsx +39 -0
  216. package/src/components/link-preview/README.md +19 -0
  217. package/src/components/link-preview/element.ts +29 -0
  218. package/src/components/link-preview/engine.ts +192 -0
  219. package/src/components/link-preview/link-preview.css +274 -0
  220. package/src/components/link-preview/react.tsx +101 -0
  221. package/src/components/loading-button/README.md +45 -0
  222. package/src/components/loading-button/element.ts +34 -0
  223. package/src/components/loading-button/engine.ts +201 -0
  224. package/src/components/loading-button/loading-button.css +76 -0
  225. package/src/components/loading-button/react.tsx +43 -0
  226. package/src/components/multi-select/README.md +48 -0
  227. package/src/components/multi-select/element.ts +28 -0
  228. package/src/components/multi-select/engine.ts +294 -0
  229. package/src/components/multi-select/multi-select.css +266 -0
  230. package/src/components/multi-select/react.tsx +39 -0
  231. package/src/components/notification-bell/README.md +14 -0
  232. package/src/components/notification-bell/element.ts +48 -0
  233. package/src/components/notification-bell/engine.ts +202 -0
  234. package/src/components/notification-bell/notification-bell.css +126 -0
  235. package/src/components/notification-bell/react.tsx +57 -0
  236. package/src/components/password-strength/README.md +17 -0
  237. package/src/components/password-strength/element.ts +22 -0
  238. package/src/components/password-strength/engine.ts +212 -0
  239. package/src/components/password-strength/password-strength.css +182 -0
  240. package/src/components/password-strength/react.tsx +36 -0
  241. package/src/components/password-strength/score.ts +62 -0
  242. package/src/components/popover/README.md +44 -0
  243. package/src/components/popover/element.ts +31 -0
  244. package/src/components/popover/engine.ts +114 -0
  245. package/src/components/popover/popover.css +24 -0
  246. package/src/components/popover/react.tsx +50 -0
  247. package/src/components/progress/README.md +17 -0
  248. package/src/components/progress/element.ts +30 -0
  249. package/src/components/progress/engine.ts +173 -0
  250. package/src/components/progress/progress.css +139 -0
  251. package/src/components/progress/react.tsx +35 -0
  252. package/src/components/questionnaire/README.md +32 -0
  253. package/src/components/questionnaire/element.ts +118 -0
  254. package/src/components/questionnaire/engine.ts +255 -0
  255. package/src/components/questionnaire/questionnaire.css +216 -0
  256. package/src/components/questionnaire/react.tsx +140 -0
  257. package/src/components/range-calendar/README.md +58 -0
  258. package/src/components/range-calendar/element.ts +42 -0
  259. package/src/components/range-calendar/engine.ts +390 -0
  260. package/src/components/range-calendar/range-calendar.css +185 -0
  261. package/src/components/range-calendar/react.tsx +56 -0
  262. package/src/components/range-slider/README.md +23 -0
  263. package/src/components/range-slider/element.ts +53 -0
  264. package/src/components/range-slider/engine.ts +316 -0
  265. package/src/components/range-slider/range-slider.css +191 -0
  266. package/src/components/range-slider/react.tsx +66 -0
  267. package/src/components/resizable-panel/README.md +61 -0
  268. package/src/components/resizable-panel/element.ts +36 -0
  269. package/src/components/resizable-panel/engine.ts +551 -0
  270. package/src/components/resizable-panel/react.tsx +74 -0
  271. package/src/components/resizable-panel/resizable-panel.css +242 -0
  272. package/src/components/rich-text-editor/README.md +57 -0
  273. package/src/components/rich-text-editor/element.ts +34 -0
  274. package/src/components/rich-text-editor/engine.ts +673 -0
  275. package/src/components/rich-text-editor/react.tsx +60 -0
  276. package/src/components/rich-text-editor/rich-text-editor.css +283 -0
  277. package/src/components/scratch-card/README.md +15 -0
  278. package/src/components/scratch-card/element.ts +30 -0
  279. package/src/components/scratch-card/engine.ts +257 -0
  280. package/src/components/scratch-card/react.tsx +40 -0
  281. package/src/components/scratch-card/scratch-card.css +74 -0
  282. package/src/components/skeleton/README.md +15 -0
  283. package/src/components/skeleton/element.ts +32 -0
  284. package/src/components/skeleton/engine.ts +104 -0
  285. package/src/components/skeleton/react.tsx +41 -0
  286. package/src/components/skeleton/skeleton.css +107 -0
  287. package/src/components/split-view/README.md +50 -0
  288. package/src/components/split-view/element.ts +34 -0
  289. package/src/components/split-view/engine.ts +114 -0
  290. package/src/components/split-view/react.tsx +63 -0
  291. package/src/components/split-view/split-view.css +18 -0
  292. package/src/components/star-rating/README.md +14 -0
  293. package/src/components/star-rating/element.ts +26 -0
  294. package/src/components/star-rating/engine.ts +217 -0
  295. package/src/components/star-rating/react.tsx +33 -0
  296. package/src/components/star-rating/star-rating.css +79 -0
  297. package/src/components/stat-card/README.md +42 -0
  298. package/src/components/stat-card/element.ts +40 -0
  299. package/src/components/stat-card/engine.ts +283 -0
  300. package/src/components/stat-card/react.tsx +36 -0
  301. package/src/components/stat-card/stat-card.css +163 -0
  302. package/src/components/swipe-actions/README.md +19 -0
  303. package/src/components/swipe-actions/element.ts +26 -0
  304. package/src/components/swipe-actions/engine.ts +260 -0
  305. package/src/components/swipe-actions/react.tsx +80 -0
  306. package/src/components/swipe-actions/swipe-actions.css +104 -0
  307. package/src/components/tag-input/README.md +42 -0
  308. package/src/components/tag-input/element.ts +39 -0
  309. package/src/components/tag-input/engine.ts +313 -0
  310. package/src/components/tag-input/react.tsx +54 -0
  311. package/src/components/tag-input/tag-input.css +243 -0
  312. package/src/components/time-field/README.md +54 -0
  313. package/src/components/time-field/element.ts +38 -0
  314. package/src/components/time-field/engine.ts +110 -0
  315. package/src/components/time-field/react.tsx +45 -0
  316. package/src/components/time-field/time-field.css +87 -0
  317. package/src/components/timeline/README.md +57 -0
  318. package/src/components/timeline/element.ts +31 -0
  319. package/src/components/timeline/engine.ts +300 -0
  320. package/src/components/timeline/react.tsx +59 -0
  321. package/src/components/timeline/timeline.css +218 -0
  322. package/src/components/tooltip/README.md +46 -0
  323. package/src/components/tooltip/element.ts +22 -0
  324. package/src/components/tooltip/engine.ts +288 -0
  325. package/src/components/tooltip/react.tsx +30 -0
  326. package/src/components/tooltip/tooltip.css +78 -0
  327. package/src/components/tree-view/README.md +61 -0
  328. package/src/components/tree-view/element.ts +32 -0
  329. package/src/components/tree-view/engine.ts +569 -0
  330. package/src/components/tree-view/react.tsx +78 -0
  331. package/src/components/tree-view/tree-view.css +209 -0
  332. package/src/components/virtual-list/README.md +81 -0
  333. package/src/components/virtual-list/element.ts +42 -0
  334. package/src/components/virtual-list/engine.ts +678 -0
  335. package/src/components/virtual-list/react.tsx +101 -0
  336. package/src/components/virtual-list/virtual-list.css +194 -0
  337. package/src/core/chips.ts +196 -0
  338. package/src/core/color.ts +304 -0
  339. package/src/core/date-range.ts +67 -0
  340. package/src/core/dates.ts +11 -1
  341. package/src/core/files.ts +107 -0
  342. package/src/core/filter.ts +229 -0
  343. package/src/core/gsap.ts +80 -65
  344. package/src/core/listbox.ts +332 -0
  345. package/src/core/mark.ts +29 -0
  346. package/src/core/position.ts +89 -0
  347. package/src/core/relative-time.ts +77 -0
  348. package/src/core/segment-field.ts +373 -0
  349. package/src/core/segments.ts +153 -0
  350. package/src/core/tree.ts +116 -0
  351. package/src/core/virtual.ts +79 -0
  352. package/src/css/tokens.css +186 -182
  353. package/src/elements/index.ts +147 -0
  354. package/src/index.ts +72 -0
  355. package/src/motion/micro.ts +248 -0
  356. package/src/motion/sheet.ts +88 -65
  357. package/src/motion/surface.ts +9 -0
  358. package/src/react/index.ts +57 -0
  359. package/dist/chunk-IKQ5FODN.js.map +0 -1
  360. package/dist/chunk-J6OCFNGU.js +0 -177
  361. package/dist/chunk-J6OCFNGU.js.map +0 -1
  362. package/dist/chunk-TFNRIZHU.js +0 -3977
  363. package/dist/chunk-TFNRIZHU.js.map +0 -1
  364. package/dist/chunk-XAKC2QY7.js.map +0 -1
  365. package/dist/reveal-jsX-uwUz.d.ts +0 -625
@@ -0,0 +1,1059 @@
1
+ import { gsap, EASE, Flip, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { TIMING } from '../../core/timing';
5
+ import { inert, type Instance } from '../../core/types';
6
+ import { fromJson, safeFormat } from '../../core/dates';
7
+ import { toTime } from '../../core/relative-time';
8
+ import { foldText, markText } from '../../core/mark';
9
+ import { cloneFilter, describeFilter, emptyFilter, hasConditions, matchesFilter, type FilterField, type FilterGroup } from '../../core/filter';
10
+ import { flowInMenu } from '../../motion/entrances';
11
+ import { press, rollDigits } from '../../motion/micro';
12
+ import { createDialog } from '../dialog/engine';
13
+ import { createMenu } from '../menu/engine';
14
+ import { createEmptyState } from '../empty-state/engine';
15
+ import { createFilterBuilder } from '../filter-builder/engine';
16
+
17
+ /*
18
+ * Data table: rows and columns with sorting, a search, filters, selection,
19
+ * pagination, row actions and columns to show or hide. Sorting glides the rows
20
+ * to their new order (layout reflow, .62 s) while the header's arrow turns;
21
+ * the search marks what matches and the rows that stay glide together; filters
22
+ * come from filter-builder in a panel that grows out of its button (the table
23
+ * follows live, a sentence says what is filtered). Checks draw themselves and
24
+ * pop; the header box goes mixed; a bar with the count (rolling) and the bulk
25
+ * actions slides up out of a blur when something is chosen. A page change
26
+ * slides the rows in from the side it goes to. Rows taken away slide out and
27
+ * the rest close the gap. While loading, shimmering bones stand in; with
28
+ * nothing to show, an empty state says why and offers to clear the filters.
29
+ */
30
+
31
+ export type DataColumnType = 'text' | 'number' | 'date' | 'boolean' | 'select';
32
+ export type DataRow = Record<string, unknown>;
33
+ export type RowId = string | number;
34
+ export type SortDirection = 'asc' | 'desc';
35
+
36
+ export interface DataSort {
37
+ key: string;
38
+ direction: SortDirection;
39
+ }
40
+
41
+ export interface DataColumn<T extends DataRow = DataRow> {
42
+ key: string;
43
+ label: string;
44
+ /** For sorting, filtering and the default text (default 'text'). */
45
+ type?: DataColumnType;
46
+ /** The choices of a 'select' column (its text and its filter). */
47
+ options?: Array<{ value: string; label: string }>;
48
+ sortable?: boolean;
49
+ filterable?: boolean;
50
+ searchable?: boolean;
51
+ /** Starts hidden (it can be shown from "Columnas"). */
52
+ hidden?: boolean;
53
+ /** Can be hidden from "Columnas" (default true). */
54
+ hideable?: boolean;
55
+ align?: 'start' | 'center' | 'end';
56
+ width?: string;
57
+ /** The cell's text (default: numbers and dates in the page's language, Sí/No, the choice's label). */
58
+ format?: (value: unknown, row: T) => string;
59
+ /** Draw the cell yourself (vanilla). */
60
+ render?: (value: unknown, row: T, cell: HTMLTableCellElement) => void;
61
+ /** The value to sort, filter and search by (default row[key]). */
62
+ value?: (row: T) => unknown;
63
+ }
64
+
65
+ export interface DataAction {
66
+ id: string;
67
+ label: string;
68
+ danger?: boolean;
69
+ }
70
+
71
+ export interface DataTableOptions<T extends DataRow = DataRow> {
72
+ columns?: DataColumn<T>[];
73
+ rows?: T[];
74
+ /** The rows' id: a key (default 'id') or a function. */
75
+ rowKey?: string | ((row: T) => RowId);
76
+ /** Accessible name (default 'Tabla'). */
77
+ caption?: string;
78
+ /** Rows a page (default 10; 0: all of them). */
79
+ pageSize?: number;
80
+ pageSizes?: number[];
81
+ selectable?: boolean;
82
+ /** A search field: true, or its placeholder (default true). */
83
+ search?: boolean | string;
84
+ /** A "Filtros" button with filter-builder (default true). */
85
+ filters?: boolean;
86
+ filter?: FilterGroup;
87
+ /** A "Columnas" button to show and hide columns (default true). */
88
+ columnsMenu?: boolean;
89
+ /** A menu on each row. */
90
+ actions?: DataAction[];
91
+ /** Buttons in the bar of the chosen rows. */
92
+ bulkActions?: DataAction[];
93
+ sort?: DataSort | null;
94
+ loading?: boolean;
95
+ emptyTitle?: string;
96
+ emptyDescription?: string;
97
+ locale?: string;
98
+ onSortChange?: (sort: DataSort | null) => void;
99
+ onSelectionChange?: (ids: RowId[], rows: T[]) => void;
100
+ onAction?: (action: string, row: T) => void;
101
+ onBulkAction?: (action: string, rows: T[]) => void;
102
+ onPageChange?: (page: number) => void;
103
+ onFilterChange?: (filter: FilterGroup, query: string) => void;
104
+ /** After the rows are drawn (editable-table decorates them). */
105
+ onRender?: (body: HTMLTableSectionElement) => void;
106
+ }
107
+
108
+ export interface DataTableInstance<T extends DataRow = DataRow> extends Instance {
109
+ setRows(rows: T[]): void;
110
+ updateRow(id: RowId, patch: Partial<T>): void;
111
+ /** Rows slide out and the rest close the gap. */
112
+ removeRows(ids: RowId[]): void;
113
+ setLoading(loading: boolean): void;
114
+ sortBy(key: string | null, direction?: SortDirection): void;
115
+ setPage(page: number): void;
116
+ setPageSize(size: number): void;
117
+ setQuery(query: string): void;
118
+ setFilter(filter: FilterGroup): void;
119
+ setColumnVisible(key: string, visible: boolean): void;
120
+ select(ids: RowId[]): void;
121
+ rowOf(id: RowId): HTMLTableRowElement | null;
122
+ cellOf(id: RowId, key: string): HTMLTableCellElement | null;
123
+ /** Draw the rows again (after changing them in place). */
124
+ refresh(): void;
125
+ idOf(row: T): RowId;
126
+ readonly rows: T[];
127
+ /** The rows the search and the filters leave, in order. */
128
+ readonly results: T[];
129
+ readonly page: number;
130
+ readonly pageCount: number;
131
+ readonly selection: RowId[];
132
+ readonly sort: DataSort | null;
133
+ readonly query: string;
134
+ readonly filter: FilterGroup;
135
+ readonly columns: DataColumn<T>[];
136
+ readonly body: HTMLTableSectionElement;
137
+ }
138
+
139
+ const SVG = (paths: string, size = 16) =>
140
+ `<svg aria-hidden="true" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
141
+ const ICONS = {
142
+ search: SVG('<circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.2-4.2"/>'),
143
+ filter: SVG('<path d="M4 6h16M7 12h10M10 18h4"/>'),
144
+ columns: SVG('<rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><path d="M9.5 4.5v15M14.5 4.5v15"/>'),
145
+ more: SVG('<circle cx="5.5" cy="12" r="1.2" fill="currentColor"/><circle cx="12" cy="12" r="1.2" fill="currentColor"/><circle cx="18.5" cy="12" r="1.2" fill="currentColor"/>', 18),
146
+ previous: SVG('<path d="M15 6l-6 6 6 6"/>'),
147
+ next: SVG('<path d="M9 6l6 6-6 6"/>'),
148
+ close: SVG('<path d="M7 7l10 10M17 7L7 17"/>', 14),
149
+ arrow: SVG('<path d="M12 19V5M6 11l6-6 6 6"/>', 12),
150
+ };
151
+ const CHECK =
152
+ '<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path data-mk-dt-tick d="M5 12.5l4.5 4.5L19 7.5" pathLength="1"/><path data-mk-dt-dash d="M6 12h12"/></svg>';
153
+ const plural = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;
154
+ let sequence = 0;
155
+
156
+ export function createDataTable<T extends DataRow = DataRow>(root: HTMLElement | null, options: DataTableOptions<T> = {}): DataTableInstance<T> {
157
+ if (!root) {
158
+ return inert({
159
+ setRows() {}, updateRow() {}, removeRows() {}, setLoading() {}, sortBy() {}, setPage() {}, setPageSize() {}, setQuery() {}, setFilter() {}, setColumnVisible() {}, select() {},
160
+ rowOf: () => null, cellOf: () => null, refresh() {}, idOf: () => '',
161
+ rows: [], results: [], page: 0, pageCount: 0, selection: [], sort: null, query: '', filter: emptyFilter(), columns: [], body: null as unknown as HTMLTableSectionElement,
162
+ });
163
+ }
164
+ registerMotion();
165
+ const scope = createScope();
166
+ const uid = `mk-dt-${++sequence}`;
167
+ const locale = options.locale ?? root.closest('[lang]')?.getAttribute('lang') ?? 'es';
168
+ // JSON text too (data-mk-* through autoInit).
169
+ const columns: DataColumn<T>[] = (fromJson(options.columns) ?? []).map((column) => ({ ...column }));
170
+ let rows: T[] = [...(fromJson(options.rows) ?? [])];
171
+ const hidden = new Set(columns.filter((column) => column.hidden).map((column) => column.key));
172
+ let sort: DataSort | null = options.sort ?? null;
173
+ let query = '';
174
+ let filter: FilterGroup = options.filter ? cloneFilter(options.filter) : emptyFilter();
175
+ let pageSize = Number.isFinite(options.pageSize) ? Math.max(0, Math.floor(options.pageSize!)) : 10;
176
+ const pageSizes = options.pageSizes ?? [10, 25, 50];
177
+ let page = 0;
178
+ let selection = new Set<RowId>();
179
+ let anchor: RowId | null = null;
180
+ let loading = Boolean(options.loading);
181
+ const effects = new Set<gsap.core.Animation>();
182
+ const keep = (animation: gsap.core.Animation | null) => {
183
+ if (!animation) return;
184
+ effects.add(animation);
185
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
186
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
187
+ };
188
+
189
+ // ── Rows and values ──────────────────────────────────────────────────────
190
+ const generated = new WeakMap<object, string>();
191
+ let made = 0;
192
+ const idOf = (row: T): RowId => {
193
+ if (typeof options.rowKey === 'function') return options.rowKey(row);
194
+ const id = row[options.rowKey ?? 'id'];
195
+ if (typeof id === 'string' || typeof id === 'number') return id;
196
+ let given = generated.get(row);
197
+ if (!given) generated.set(row, (given = `${uid}-row-${++made}`));
198
+ return given;
199
+ };
200
+ const visibleColumns = () => columns.filter((column) => !hidden.has(column.key));
201
+ const valueOf = (row: T, column: DataColumn<T>) => (column.value ? column.value(row) : row[column.key]);
202
+ let number: Intl.NumberFormat;
203
+ try {
204
+ number = new Intl.NumberFormat(locale);
205
+ } catch {
206
+ number = new Intl.NumberFormat();
207
+ }
208
+ const dateFormat = safeFormat(locale, { day: 'numeric', month: 'short', year: 'numeric' });
209
+ const textOf = (row: T, column: DataColumn<T>): string => {
210
+ const value = valueOf(row, column);
211
+ if (column.format) return column.format(value, row);
212
+ if (value === null || value === undefined || value === '') return '';
213
+ switch (column.type) {
214
+ case 'number':
215
+ return typeof value === 'number' ? number.format(value) : String(value);
216
+ case 'date': {
217
+ const date = toTime(value as string | number | Date);
218
+ return date ? dateFormat.format(date) : String(value);
219
+ }
220
+ case 'boolean':
221
+ return value ? 'Sí' : 'No';
222
+ case 'select':
223
+ return column.options?.find((option) => option.value === String(value))?.label ?? String(value);
224
+ default:
225
+ return String(value);
226
+ }
227
+ };
228
+ let collator: Intl.Collator;
229
+ try {
230
+ collator = new Intl.Collator(locale, { numeric: true, sensitivity: 'base' });
231
+ } catch {
232
+ collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
233
+ }
234
+ const compare = (a: unknown, b: unknown, column: DataColumn<T>) => {
235
+ const empty = (value: unknown) => value === null || value === undefined || value === '';
236
+ // Empty cells go last whichever way.
237
+ if (empty(a) || empty(b)) return empty(a) === empty(b) ? 0 : empty(a) ? 1 : -1;
238
+ if (column.type === 'number') return Number(a) - Number(b);
239
+ if (column.type === 'date') return (toTime(a as string)?.getTime() ?? 0) - (toTime(b as string)?.getTime() ?? 0);
240
+ if (column.type === 'boolean') return Number(Boolean(a)) - Number(Boolean(b));
241
+ return collator.compare(String(a), String(b));
242
+ };
243
+ const fields = (): FilterField[] =>
244
+ columns
245
+ .filter((column) => column.filterable !== false)
246
+ .map((column) => ({ key: column.key, label: column.label, type: column.type ?? 'text', options: column.options }));
247
+ const results = (): T[] => {
248
+ const needle = foldText(query.trim());
249
+ const known = fields();
250
+ const filtering = hasConditions(filter, known);
251
+ let list = rows.filter((row) => {
252
+ if (filtering) {
253
+ const record: Record<string, unknown> = {};
254
+ for (const column of columns) record[column.key] = valueOf(row, column);
255
+ if (!matchesFilter(record, filter, known)) return false;
256
+ }
257
+ return !needle || visibleColumns().some((column) => column.searchable !== false && foldText(textOf(row, column)).includes(needle));
258
+ });
259
+ const column = sort ? columns.find((each) => each.key === sort!.key) : undefined;
260
+ if (sort && column) {
261
+ const direction = sort.direction === 'asc' ? 1 : -1;
262
+ const empty = (value: unknown) => value === null || value === undefined || value === '';
263
+ list = [...list].sort((a, b) => {
264
+ const va = valueOf(a, column);
265
+ const vb = valueOf(b, column);
266
+ // Empty cells stay at the end in both directions.
267
+ if (empty(va) || empty(vb)) return compare(va, vb, column);
268
+ return compare(va, vb, column) * direction;
269
+ });
270
+ }
271
+ return list;
272
+ };
273
+ let shown: T[] = [];
274
+ const pageCount = (count: number) => (pageSize ? Math.max(1, Math.ceil(count / pageSize)) : 1);
275
+
276
+ // ── The parts ────────────────────────────────────────────────────────────
277
+ const el = <K extends keyof HTMLElementTagNameMap>(tag: K, data?: string, text?: string) => {
278
+ const element = document.createElement(tag);
279
+ if (data) element.setAttribute(data, '');
280
+ if (text !== undefined) element.textContent = text;
281
+ return element;
282
+ };
283
+ const toolButton = (data: string, label: string, icon: string, text?: string) => {
284
+ const button = el('button', data);
285
+ button.type = 'button';
286
+ button.dataset.mkDtTool = '';
287
+ button.innerHTML = icon;
288
+ if (text) {
289
+ const span = el('span', undefined, text);
290
+ button.append(span);
291
+ } else button.setAttribute('aria-label', label);
292
+ return button;
293
+ };
294
+
295
+ const bar = el('div', 'data-mk-dt-bar');
296
+ let searchInput: HTMLInputElement | null = null;
297
+ if (options.search !== false) {
298
+ const field = el('label', 'data-mk-dt-search');
299
+ field.insertAdjacentHTML('beforeend', ICONS.search);
300
+ searchInput = el('input');
301
+ searchInput.type = 'search';
302
+ searchInput.placeholder = typeof options.search === 'string' ? options.search : 'Buscar…';
303
+ searchInput.setAttribute('aria-label', 'Buscar en la tabla');
304
+ field.append(searchInput);
305
+ bar.append(field);
306
+ }
307
+ const tools = el('div', 'data-mk-dt-tools');
308
+ const filtersButton = options.filters !== false && fields().length ? toolButton('data-mk-dt-filters', 'Filtros', ICONS.filter, 'Filtros') : null;
309
+ const filterCount = el('span', 'data-mk-dt-count');
310
+ filterCount.hidden = true;
311
+ filtersButton?.append(filterCount);
312
+ const columnsButton = options.columnsMenu !== false && columns.some((column) => column.hideable !== false) ? toolButton('data-mk-dt-columns', 'Columnas', ICONS.columns, 'Columnas') : null;
313
+ for (const button of [filtersButton, columnsButton]) if (button) tools.append(button);
314
+ bar.append(tools);
315
+
316
+ const active = el('div', 'data-mk-dt-active');
317
+ active.hidden = true;
318
+ const activeText = el('span');
319
+ const clearButton = el('button', 'data-mk-dt-clear');
320
+ clearButton.type = 'button';
321
+ clearButton.innerHTML = ICONS.close;
322
+ clearButton.append(el('span', undefined, 'Quitar filtros'));
323
+ active.append(activeText, clearButton);
324
+
325
+ const scroll = el('div', 'data-mk-dt-scroll');
326
+ const table = el('table', 'data-mk-dt-table');
327
+ table.id = `${uid}-table`;
328
+ const caption = el('caption', undefined, options.caption ?? 'Tabla');
329
+ caption.className = 'mk-sr-only';
330
+ const head = el('thead');
331
+ const headRow = el('tr');
332
+ head.append(headRow);
333
+ const body = el('tbody');
334
+ table.append(caption, head, body);
335
+ scroll.append(table);
336
+ // A region the keyboard can scroll when the table is wider than it.
337
+ scroll.setAttribute('role', 'region');
338
+ scroll.setAttribute('aria-label', `${options.caption ?? 'Tabla'} (desplazable)`);
339
+ scroll.tabIndex = 0;
340
+ const emptyHost = el('div', 'data-mk-dt-empty');
341
+ emptyHost.className = 'mk-empty-state';
342
+ emptyHost.hidden = true;
343
+
344
+ const foot = el('div', 'data-mk-dt-foot');
345
+ const range = el('span', 'data-mk-dt-range');
346
+ const sizeField = el('label', 'data-mk-dt-size');
347
+ sizeField.append(el('span', undefined, 'Filas'));
348
+ const sizeSelect = el('select');
349
+ for (const size of pageSizes) {
350
+ const option = el('option', undefined, String(size));
351
+ option.value = String(size);
352
+ sizeSelect.append(option);
353
+ }
354
+ if (!pageSizes.includes(pageSize) && pageSize) {
355
+ const option = el('option', undefined, String(pageSize));
356
+ option.value = String(pageSize);
357
+ sizeSelect.prepend(option);
358
+ }
359
+ sizeSelect.value = String(pageSize);
360
+ sizeField.append(sizeSelect);
361
+ const pager = el('nav', 'data-mk-dt-pager');
362
+ pager.setAttribute('aria-label', 'Páginas');
363
+ const previous = el('button', 'data-mk-dt-previous');
364
+ previous.type = 'button';
365
+ previous.innerHTML = ICONS.previous;
366
+ previous.setAttribute('aria-label', 'Página anterior');
367
+ const pageLabel = el('span', 'data-mk-dt-page');
368
+ const next = el('button', 'data-mk-dt-next');
369
+ next.type = 'button';
370
+ next.innerHTML = ICONS.next;
371
+ next.setAttribute('aria-label', 'Página siguiente');
372
+ pager.append(previous, pageLabel, next);
373
+ foot.append(range, sizeField, pager);
374
+
375
+ const bulk = el('div', 'data-mk-dt-bulk');
376
+ bulk.setAttribute('role', 'toolbar');
377
+ bulk.setAttribute('aria-label', 'Acciones de la selección');
378
+ bulk.hidden = true;
379
+ const bulkCount = el('span', 'data-mk-dt-bulk-count');
380
+ const bulkText = el('span');
381
+ const bulkAll = el('button', 'data-mk-dt-select-all');
382
+ bulkAll.type = 'button';
383
+ bulkAll.hidden = true;
384
+ const bulkActions = el('div', 'data-mk-dt-bulk-actions');
385
+ for (const action of options.bulkActions ?? []) {
386
+ const button = el('button', undefined, action.label);
387
+ button.type = 'button';
388
+ button.dataset.action = action.id;
389
+ if (action.danger) button.dataset.danger = '';
390
+ bulkActions.append(button);
391
+ }
392
+ const bulkClose = el('button', 'data-mk-dt-bulk-close');
393
+ bulkClose.type = 'button';
394
+ bulkClose.innerHTML = ICONS.close;
395
+ bulkClose.setAttribute('aria-label', 'Quitar la selección');
396
+ const bulkLabel = el('span', 'data-mk-dt-bulk-label');
397
+ bulkLabel.append(bulkCount, bulkText);
398
+ bulk.append(bulkLabel, bulkAll, bulkActions, bulkClose);
399
+
400
+ const status = el('span');
401
+ status.className = 'mk-sr-only';
402
+ status.setAttribute('role', 'status');
403
+ root.append(bar, active, scroll, emptyHost, foot, bulk, status);
404
+ scope.attr(root, 'data-mk-ready', '');
405
+ scope.add(() => {
406
+ for (const effect of effects) effect.kill();
407
+ effects.clear();
408
+ for (const part of [bar, active, scroll, emptyHost, foot, bulk, status]) part.remove();
409
+ });
410
+
411
+ // ── Checkboxes that draw themselves ──────────────────────────────────────
412
+ const checkbox = (label: string) => {
413
+ const box = el('span', 'data-mk-dt-check');
414
+ const input = el('input');
415
+ input.type = 'checkbox';
416
+ input.setAttribute('aria-label', label);
417
+ box.append(input);
418
+ box.insertAdjacentHTML('beforeend', CHECK);
419
+ return { box, input };
420
+ };
421
+ const paintCheck = (box: HTMLElement, state: 'true' | 'false' | 'mixed', animate: boolean) => {
422
+ const input = box.querySelector('input')!;
423
+ const before = box.dataset.state;
424
+ box.dataset.state = state;
425
+ input.checked = state === 'true';
426
+ input.indeterminate = state === 'mixed';
427
+ if (!animate || before === state || state === 'false' || reduced()) return;
428
+ keep(gsap.fromTo(box, { scale: 0.8 }, { scale: 1, duration: 0.35, ease: EASE.click, clearProps: 'transform' }));
429
+ const path = box.querySelector<SVGPathElement>(state === 'true' ? '[data-mk-dt-tick]' : '[data-mk-dt-dash]');
430
+ if (path) keep(gsap.fromTo(path, { strokeDasharray: state === 'true' ? 1 : 12, strokeDashoffset: state === 'true' ? 1 : 12 }, { strokeDashoffset: 0, duration: 0.3, ease: EASE.wipe, clearProps: 'strokeDasharray,strokeDashoffset' }));
431
+ };
432
+
433
+ // ── The header ───────────────────────────────────────────────────────────
434
+ let headCheck: HTMLElement | null = null;
435
+ const buildHead = () => {
436
+ headRow.replaceChildren();
437
+ headCheck = null;
438
+ if (options.selectable) {
439
+ const th = el('th', 'data-mk-dt-select');
440
+ th.scope = 'col';
441
+ const { box } = checkbox('Seleccionar la página');
442
+ headCheck = box;
443
+ th.append(box);
444
+ headRow.append(th);
445
+ }
446
+ for (const column of visibleColumns()) {
447
+ const th = el('th');
448
+ th.scope = 'col';
449
+ th.dataset.key = column.key;
450
+ th.dataset.align = column.align ?? (column.type === 'number' ? 'end' : 'start');
451
+ if (column.width) th.style.width = column.width;
452
+ if (column.sortable !== false) {
453
+ const button = el('button', 'data-mk-dt-sort');
454
+ button.type = 'button';
455
+ button.append(el('span', undefined, column.label));
456
+ button.insertAdjacentHTML('beforeend', ICONS.arrow);
457
+ th.append(button);
458
+ } else th.textContent = column.label;
459
+ headRow.append(th);
460
+ }
461
+ if (options.actions?.length) {
462
+ const th = el('th', 'data-mk-dt-actions');
463
+ th.scope = 'col';
464
+ th.append(Object.assign(el('span', undefined, 'Acciones'), { className: 'mk-sr-only' }));
465
+ headRow.append(th);
466
+ }
467
+ paintHead();
468
+ };
469
+ const paintHead = () => {
470
+ for (const th of headRow.querySelectorAll<HTMLElement>('th[data-key]')) {
471
+ const key = th.dataset.key!;
472
+ const on = sort?.key === key;
473
+ if (th.querySelector('[data-mk-dt-sort]')) th.setAttribute('aria-sort', on ? (sort!.direction === 'asc' ? 'ascending' : 'descending') : 'none');
474
+ th.dataset.direction = on ? sort!.direction : '';
475
+ }
476
+ };
477
+
478
+ // ── The rows ─────────────────────────────────────────────────────────────
479
+ const rowOf = (id: RowId) => [...body.rows].find((tr) => tr.dataset.id === String(id)) ?? null;
480
+ const fillCell = (cell: HTMLTableCellElement, row: T, column: DataColumn<T>) => {
481
+ cell.replaceChildren();
482
+ if (column.render) column.render(valueOf(row, column), row, cell);
483
+ else if (column.type === 'boolean') {
484
+ const pill = el('span', 'data-mk-dt-bool', textOf(row, column));
485
+ pill.dataset.value = String(Boolean(valueOf(row, column)));
486
+ cell.append(pill);
487
+ } else cell.append(markText(textOf(row, column), column.searchable === false ? '' : query));
488
+ };
489
+ const rowElement = (row: T, index: number) => {
490
+ const id = idOf(row);
491
+ const tr = el('tr');
492
+ tr.dataset.id = String(id);
493
+ tr.dataset.flipId = String(id);
494
+ const chosen = selection.has(id);
495
+ tr.toggleAttribute('data-selected', chosen);
496
+ const first = visibleColumns()[0];
497
+ const name = first ? textOf(row, first) : `fila ${index + 1}`;
498
+ if (options.selectable) {
499
+ const td = el('td', 'data-mk-dt-select');
500
+ const { box } = checkbox(`Seleccionar ${name}`);
501
+ paintCheck(box, chosen ? 'true' : 'false', false);
502
+ td.append(box);
503
+ tr.append(td);
504
+ }
505
+ for (const column of visibleColumns()) {
506
+ const td = el('td');
507
+ td.dataset.key = column.key;
508
+ td.dataset.align = column.align ?? (column.type === 'number' ? 'end' : 'start');
509
+ fillCell(td, row, column);
510
+ tr.append(td);
511
+ }
512
+ if (options.actions?.length) {
513
+ const td = el('td', 'data-mk-dt-actions');
514
+ const button = el('button', 'data-mk-dt-row-menu');
515
+ button.type = 'button';
516
+ button.innerHTML = ICONS.more;
517
+ button.setAttribute('aria-label', `Acciones de ${name}`);
518
+ button.setAttribute('aria-haspopup', 'menu');
519
+ td.append(button);
520
+ tr.append(td);
521
+ }
522
+ return tr;
523
+ };
524
+ const bones = () => {
525
+ const count = pageSize || 6;
526
+ const width = visibleColumns().length + (options.selectable ? 1 : 0) + (options.actions?.length ? 1 : 0);
527
+ return Array.from({ length: Math.min(count, 12) }, (_, r) => {
528
+ const tr = el('tr', 'data-mk-dt-loading');
529
+ for (let c = 0; c < width; c++) {
530
+ const td = el('td');
531
+ const bone = el('span', 'data-mk-dt-bone');
532
+ bone.style.width = `${45 + ((r * 7 + c * 13) % 45)}%`;
533
+ td.append(bone);
534
+ tr.append(td);
535
+ }
536
+ return tr;
537
+ });
538
+ };
539
+
540
+ let empty: ReturnType<typeof createEmptyState> | null = null;
541
+ const paintEmpty = (count: number) => {
542
+ const none = !loading && !count;
543
+ emptyHost.hidden = !none;
544
+ table.toggleAttribute('data-mk-dt-empty-table', none);
545
+ if (!none) return;
546
+ const searching = rows.length > 0;
547
+ const next = {
548
+ illustration: searching ? ('search' as const) : ('inbox' as const),
549
+ title: searching ? 'Sin resultados' : (options.emptyTitle ?? 'Sin datos todavía'),
550
+ description: searching ? 'Prueba con otra búsqueda o quita los filtros.' : (options.emptyDescription ?? 'Cuando haya filas, aparecerán aquí.'),
551
+ actions: searching ? [{ id: 'clear', label: 'Limpiar búsqueda y filtros' }] : [],
552
+ };
553
+ if (empty) empty.update(next);
554
+ else empty = createEmptyState(emptyHost, { ...next, onAction: (id) => id === 'clear' && clearAll() });
555
+ };
556
+ scope.add(() => empty?.destroy());
557
+ scope.on(emptyHost, 'mk:action', (event) => event.stopPropagation());
558
+
559
+ const paintFoot = (count: number) => {
560
+ const pages = pageCount(count);
561
+ page = Math.min(page, pages - 1);
562
+ const from = count ? page * (pageSize || count) + 1 : 0;
563
+ const to = pageSize ? Math.min(count, (page + 1) * pageSize) : count;
564
+ const text = count ? `${number.format(from)}–${number.format(to)} de ${number.format(count)}` : '0 resultados';
565
+ if (range.textContent !== text) range.textContent = text;
566
+ pageLabel.textContent = `${number.format(page + 1)} / ${number.format(pages)}`;
567
+ previous.disabled = page === 0;
568
+ next.disabled = page >= pages - 1;
569
+ pager.hidden = !pageSize || pages <= 1;
570
+ sizeField.hidden = !pageSize;
571
+ };
572
+
573
+ const paintSelection = (animate: boolean) => {
574
+ for (const tr of body.rows) {
575
+ const id = tr.dataset.id;
576
+ if (id === undefined || tr.hasAttribute('data-mk-dt-loading')) continue;
577
+ const row = shown.find((each) => String(idOf(each)) === id);
578
+ const chosen = row ? selection.has(idOf(row)) : false;
579
+ tr.toggleAttribute('data-selected', chosen);
580
+ const box = tr.querySelector<HTMLElement>('[data-mk-dt-check]');
581
+ if (box) paintCheck(box, chosen ? 'true' : 'false', animate);
582
+ }
583
+ if (headCheck) {
584
+ const onPage = shown.map(idOf);
585
+ const count = onPage.filter((id) => selection.has(id)).length;
586
+ paintCheck(headCheck, count && count === onPage.length ? 'true' : count ? 'mixed' : 'false', animate);
587
+ }
588
+ paintBulk(animate);
589
+ };
590
+
591
+ let bulkShown = false;
592
+ const paintBulk = (animate: boolean) => {
593
+ const count = selection.size;
594
+ const show = count > 0;
595
+ const text = String(count);
596
+ if (show) {
597
+ const same = bulkCount.textContent === text;
598
+ if (!same && bulkShown && animate && !reduced()) keep(rollDigits(bulkCount, text, Number(text) > Number(bulkCount.textContent || 0) ? 1 : -1, 0.3));
599
+ else bulkCount.textContent = text;
600
+ bulkText.textContent = count === 1 ? ' seleccionada' : ' seleccionadas';
601
+ // The whole page chosen and more beyond it: offer them all.
602
+ const total = results().length;
603
+ const pageIds = shown.map(idOf);
604
+ const pageFull = pageIds.length > 0 && pageIds.every((id) => selection.has(id));
605
+ bulkAll.hidden = !(pageFull && total > count);
606
+ bulkAll.textContent = `Seleccionar las ${number.format(total)}`;
607
+ }
608
+ if (show === bulkShown) return;
609
+ bulkShown = show;
610
+ gsap.killTweensOf(bulk);
611
+ if (show) {
612
+ bulk.hidden = false;
613
+ if (animate && !reduced()) keep(gsap.fromTo(bulk, { opacity: 0, y: 16, filter: 'blur(8px)' }, { opacity: 1, y: 0, filter: 'blur(0px)', duration: 0.35, ease: EASE.pop, clearProps: 'opacity,transform,filter' }));
614
+ } else if (animate && !reduced()) {
615
+ keep(gsap.to(bulk, { opacity: 0, y: 16, filter: 'blur(6px)', duration: 0.22, ease: 'power2.in', onComplete: () => {
616
+ bulk.hidden = true;
617
+ gsap.set(bulk, { clearProps: 'opacity,transform,filter' });
618
+ } }));
619
+ } else bulk.hidden = true;
620
+ };
621
+
622
+ /** Draw the page; `motion`: how the rows change (glide, slide from a side, or nothing). */
623
+ const render = (motion: 'reflow' | 'forward' | 'back' | 'none' = 'none') => {
624
+ const all = loading ? [] : results();
625
+ const pages = pageCount(all.length);
626
+ page = Math.max(0, Math.min(page, pages - 1));
627
+ shown = loading ? [] : pageSize ? all.slice(page * pageSize, (page + 1) * pageSize) : all;
628
+ const before = motion === 'reflow' && !reduced() ? [...body.rows].filter((tr) => !tr.hasAttribute('data-mk-dt-loading')) : [];
629
+ const state = before.length && before.length <= 80 ? Flip.getState(before) : null;
630
+ const wasLoading = body.querySelector('[data-mk-dt-loading]') !== null;
631
+ body.replaceChildren(...(loading ? bones() : shown.map(rowElement)));
632
+ table.setAttribute('aria-busy', String(loading));
633
+ paintEmpty(all.length);
634
+ paintFoot(all.length);
635
+ paintSelection(false);
636
+ const fresh = [...body.rows];
637
+ if (!reduced() && !loading && fresh.length) {
638
+ if (state) {
639
+ keep(
640
+ Flip.from(state, {
641
+ targets: fresh,
642
+ duration: TIMING.layoutTravel,
643
+ ease: EASE.layoutReflow,
644
+ onEnter: (entering) => gsap.fromTo(entering, { opacity: 0 }, { opacity: 1, duration: 0.3, delay: 0.1, ease: 'power1.out', clearProps: 'opacity' }),
645
+ }),
646
+ );
647
+ } else if (motion === 'forward' || motion === 'back') {
648
+ keep(gsap.fromTo(fresh, { opacity: 0, x: motion === 'forward' ? 24 : -24 }, { opacity: 1, x: 0, duration: 0.32, stagger: 0.015, ease: 'power2.out', clearProps: 'opacity,transform' }));
649
+ } else if (wasLoading) keep(gsap.fromTo(fresh, { opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.35, stagger: 0.025, ease: 'power2.out', clearProps: 'opacity,transform' }));
650
+ }
651
+ options.onRender?.(body);
652
+ };
653
+
654
+ // ── Changes ──────────────────────────────────────────────────────────────
655
+ const notifySelection = () => {
656
+ const ids = [...selection];
657
+ const chosen = rows.filter((row) => selection.has(idOf(row)));
658
+ options.onSelectionChange?.(ids, chosen);
659
+ root.dispatchEvent(new CustomEvent('mk:select', { bubbles: true, detail: { ids, rows: chosen } }));
660
+ };
661
+ const setSelection = (ids: Iterable<RowId>, notify = true) => {
662
+ const next = new Set(ids);
663
+ const same = next.size === selection.size && [...next].every((id) => selection.has(id));
664
+ selection = next;
665
+ paintSelection(true);
666
+ if (!same) {
667
+ status.textContent = selection.size ? plural(selection.size, 'fila seleccionada', 'filas seleccionadas') : 'Sin selección';
668
+ if (notify) notifySelection();
669
+ }
670
+ };
671
+ const announceResults = () => {
672
+ const count = results().length;
673
+ status.textContent = plural(count, 'resultado', 'resultados');
674
+ };
675
+ const filtersChanged = (notify = true) => {
676
+ page = 0;
677
+ const known = fields();
678
+ const conditions = hasConditions(filter, known);
679
+ const sentence = conditions ? describeFilter(filter, known) : '';
680
+ const wasHidden = active.hidden;
681
+ active.hidden = !conditions;
682
+ activeText.textContent = sentence;
683
+ if (conditions && wasHidden && !reduced()) keep(gsap.fromTo(active, { opacity: 0, y: -6, filter: 'blur(6px)' }, { opacity: 1, y: 0, filter: 'blur(0px)', duration: 0.3, ease: 'power2.out', clearProps: 'opacity,transform,filter' }));
684
+ const ruleCount = countRules(filter, known);
685
+ filterCount.hidden = !ruleCount;
686
+ if (ruleCount && filterCount.textContent !== String(ruleCount) && !reduced()) keep(gsap.fromTo(filterCount, { scale: 0.5 }, { scale: 1, duration: 0.35, ease: EASE.pop, clearProps: 'transform' }));
687
+ filterCount.textContent = String(ruleCount);
688
+ render('reflow');
689
+ if (!notify) return;
690
+ announceResults();
691
+ options.onFilterChange?.(cloneFilter(filter), query);
692
+ root.dispatchEvent(new CustomEvent('mk:filter', { bubbles: true, detail: { filter: cloneFilter(filter), query } }));
693
+ };
694
+ const countRules = (group: FilterGroup, known: FilterField[]): number =>
695
+ group.rules.reduce((sum, node) => sum + ('rules' in node ? countRules(node as FilterGroup, known) : hasConditions({ id: 'x', combinator: 'and', rules: [node] }, known) ? 1 : 0), 0);
696
+ const clearAll = () => {
697
+ query = '';
698
+ if (searchInput) searchInput.value = '';
699
+ filter = emptyFilter();
700
+ builder?.setValue(filter);
701
+ filtersChanged();
702
+ };
703
+
704
+ const sortBy = (key: string | null, direction?: SortDirection) => {
705
+ const column = key ? columns.find((each) => each.key === key) : undefined;
706
+ sort = column ? { key: column.key, direction: direction ?? 'asc' } : null;
707
+ paintHead();
708
+ render('reflow');
709
+ status.textContent = sort ? `Ordenado por ${column!.label}, ${sort.direction === 'asc' ? 'ascendente' : 'descendente'}` : 'Sin orden';
710
+ options.onSortChange?.(sort ? { ...sort } : null);
711
+ root.dispatchEvent(new CustomEvent('mk:sort', { bubbles: true, detail: { sort: sort ? { ...sort } : null } }));
712
+ };
713
+ const setPage = (next: number) => {
714
+ const pages = pageCount(results().length);
715
+ const target = Math.max(0, Math.min(pages - 1, next));
716
+ if (target === page) return;
717
+ const forward = target > page;
718
+ page = target;
719
+ render(forward ? 'forward' : 'back');
720
+ status.textContent = `Página ${page + 1} de ${pages}`;
721
+ options.onPageChange?.(page);
722
+ root.dispatchEvent(new CustomEvent('mk:page', { bubbles: true, detail: { page } }));
723
+ };
724
+
725
+ // ── Pointer and keyboard ─────────────────────────────────────────────────
726
+ scope.on(head, 'click', (event) => {
727
+ const button = (event.target as Element | null)?.closest<HTMLElement>('[data-mk-dt-sort]');
728
+ if (!button) return;
729
+ const key = button.closest<HTMLElement>('th')!.dataset.key!;
730
+ // Ascending, descending, then no order.
731
+ if (sort?.key !== key) sortBy(key, 'asc');
732
+ else if (sort.direction === 'asc') sortBy(key, 'desc');
733
+ else sortBy(null);
734
+ });
735
+ scope.on(head, 'change', (event) => {
736
+ if (!(event.target as Element).closest('[data-mk-dt-check]')) return;
737
+ const ids = shown.map(idOf);
738
+ const all = ids.every((id) => selection.has(id));
739
+ setSelection(all ? [...selection].filter((id) => !ids.includes(id)) : [...selection, ...ids]);
740
+ });
741
+ scope.on(body, 'click', (event) => {
742
+ const target = event.target as Element | null;
743
+ const input = target?.closest('[data-mk-dt-check] input');
744
+ if (input) {
745
+ const tr = input.closest<HTMLElement>('tr')!;
746
+ const row = shown.find((each) => String(idOf(each)) === tr.dataset.id);
747
+ if (!row) return;
748
+ const id = idOf(row);
749
+ const mouse = event as MouseEvent;
750
+ if (mouse.shiftKey && anchor !== null) {
751
+ const order = shown.map(idOf);
752
+ const a = order.indexOf(anchor);
753
+ const b = order.indexOf(id);
754
+ if (a >= 0 && b >= 0) {
755
+ const span = order.slice(Math.min(a, b), Math.max(a, b) + 1);
756
+ const on = !selection.has(id);
757
+ const next = new Set(selection);
758
+ for (const each of span) {
759
+ if (on) next.add(each);
760
+ else next.delete(each);
761
+ }
762
+ anchor = id;
763
+ return setSelection(next);
764
+ }
765
+ }
766
+ anchor = id;
767
+ const next = new Set(selection);
768
+ if (next.has(id)) next.delete(id);
769
+ else next.add(id);
770
+ setSelection(next);
771
+ return;
772
+ }
773
+ const menuButton = target?.closest<HTMLElement>('[data-mk-dt-row-menu]');
774
+ if (menuButton) openRowMenu(menuButton);
775
+ });
776
+ // Changes of the boxes are handled on click (it knows about Shift); keep the box in step.
777
+ scope.on(body, 'change', (event) => {
778
+ const box = (event.target as Element).closest<HTMLElement>('[data-mk-dt-check]');
779
+ if (box) paintSelection(false);
780
+ });
781
+ if (searchInput) {
782
+ const input = searchInput;
783
+ scope.on(input, 'input', () => {
784
+ query = input.value;
785
+ filtersChanged();
786
+ });
787
+ }
788
+ scope.on(previous, 'click', () => {
789
+ keep(press(previous));
790
+ setPage(page - 1);
791
+ });
792
+ scope.on(next, 'click', () => {
793
+ keep(press(next));
794
+ setPage(page + 1);
795
+ });
796
+ scope.on(sizeSelect, 'change', () => {
797
+ pageSize = Number(sizeSelect.value) || 0;
798
+ page = 0;
799
+ render('reflow');
800
+ });
801
+ scope.on(clearButton, 'click', clearAll);
802
+ scope.on(bulkClose, 'click', () => setSelection([]));
803
+ scope.on(bulkAll, 'click', () => setSelection(results().map(idOf)));
804
+ scope.on(bulkActions, 'click', (event) => {
805
+ const button = (event.target as Element | null)?.closest<HTMLElement>('[data-action]');
806
+ if (!button) return;
807
+ keep(press(button));
808
+ const chosen = rows.filter((row) => selection.has(idOf(row)));
809
+ options.onBulkAction?.(button.dataset.action!, chosen);
810
+ root.dispatchEvent(new CustomEvent('mk:bulk-action', { bubbles: true, detail: { action: button.dataset.action, rows: chosen } }));
811
+ });
812
+
813
+ // ── Panels: row menu, columns, filters ───────────────────────────────────
814
+ const panelDialog = (name: string, label: string) => {
815
+ const dialog = el('dialog');
816
+ dialog.className = 'mk-dialog mk-dt-panel';
817
+ dialog.dataset.mkDtPanel = name;
818
+ dialog.setAttribute('aria-label', label);
819
+ root.append(dialog);
820
+ scope.add(() => dialog.remove());
821
+ return dialog;
822
+ };
823
+
824
+ let rowMenu: ReturnType<typeof createMenu> | null = null;
825
+ let menuRow: T | null = null;
826
+ if (options.actions?.length) {
827
+ const dialog = panelDialog('menu', 'Acciones de la fila');
828
+ dialog.className = 'mk-dialog mk-menu mk-dt-menu';
829
+ for (const action of options.actions) {
830
+ const item = el('button', 'data-mk-item', action.label);
831
+ item.type = 'button';
832
+ item.dataset.mkValue = action.id;
833
+ if (action.danger) item.dataset.danger = '';
834
+ dialog.append(item);
835
+ }
836
+ rowMenu = createMenu(dialog, {
837
+ triggers: false,
838
+ onClose: () => {
839
+ for (const button of body.querySelectorAll('[data-mk-dt-row-menu][aria-expanded="true"]')) button.setAttribute('aria-expanded', 'false');
840
+ },
841
+ onSelect: (value) => {
842
+ const row = menuRow;
843
+ if (!row) return;
844
+ options.onAction?.(value, row);
845
+ root.dispatchEvent(new CustomEvent('mk:action', { bubbles: true, detail: { action: value, row } }));
846
+ },
847
+ });
848
+ // The menu's own events stay inside: listeners on the table hear the table's.
849
+ scope.on(dialog, 'mk:select', (event) => event.stopPropagation());
850
+ scope.add(() => rowMenu?.destroy());
851
+ }
852
+ const openRowMenu = (button: HTMLElement) => {
853
+ const tr = button.closest<HTMLElement>('tr');
854
+ menuRow = shown.find((each) => String(idOf(each)) === tr?.dataset.id) ?? null;
855
+ if (!menuRow || !rowMenu) return;
856
+ button.setAttribute('aria-expanded', 'true');
857
+ rowMenu.open(button);
858
+ };
859
+
860
+ let columnsPanel: ReturnType<typeof createDialog> | null = null;
861
+ if (columnsButton) {
862
+ const dialog = panelDialog('columns', 'Columnas');
863
+ const title = el('p', 'data-mk-dt-panel-title', 'Columnas visibles');
864
+ const list = el('div', 'data-mk-dt-column-list');
865
+ dialog.append(title, list);
866
+ const fill = () => {
867
+ list.replaceChildren(
868
+ ...columns
869
+ .filter((column) => column.hideable !== false)
870
+ .map((column) => {
871
+ const label = el('label', 'data-mk-dt-column');
872
+ const { box, input } = checkbox(column.label);
873
+ paintCheck(box, hidden.has(column.key) ? 'false' : 'true', false);
874
+ input.dataset.key = column.key;
875
+ label.append(box, el('span', undefined, column.label));
876
+ return label;
877
+ }),
878
+ );
879
+ };
880
+ columnsPanel = createDialog(dialog, {
881
+ placement: 'anchor',
882
+ backdrop: false,
883
+ drag: false,
884
+ autoResize: false,
885
+ triggers: false,
886
+ animateContent: false,
887
+ onOpen: () => {
888
+ fill();
889
+ keep(flowInMenu([title, ...list.children]));
890
+ list.querySelector<HTMLInputElement>('input')?.focus({ preventScroll: true });
891
+ },
892
+ });
893
+ scope.add(() => columnsPanel?.destroy());
894
+ scope.on(list, 'change', (event) => {
895
+ const input = event.target as HTMLInputElement;
896
+ const key = input.dataset.key;
897
+ if (!key) return;
898
+ // At least one column stays.
899
+ if (!input.checked && visibleColumns().length <= 1) {
900
+ input.checked = true;
901
+ return;
902
+ }
903
+ paintCheck(input.closest<HTMLElement>('[data-mk-dt-check]')!, input.checked ? 'true' : 'false', true);
904
+ setColumnVisible(key, input.checked);
905
+ });
906
+ scope.on(columnsButton, 'click', () => {
907
+ keep(press(columnsButton));
908
+ columnsPanel?.open(columnsButton);
909
+ });
910
+ }
911
+
912
+ let filtersPanel: ReturnType<typeof createDialog> | null = null;
913
+ let builder: ReturnType<typeof createFilterBuilder> | null = null;
914
+ if (filtersButton) {
915
+ const dialog = panelDialog('filters', 'Filtros');
916
+ dialog.classList.add('mk-dt-filters');
917
+ const holder = el('div', 'data-mk-dt-builder');
918
+ holder.className = 'mk-filter-builder';
919
+ dialog.append(holder);
920
+ builder = createFilterBuilder(holder, {
921
+ fields: fields(),
922
+ value: filter,
923
+ onChange: (next) => {
924
+ filter = next;
925
+ filtersChanged();
926
+ },
927
+ });
928
+ // The builder's own events stay inside the panel.
929
+ scope.on(dialog, 'mk:change', (event) => event.stopPropagation());
930
+ filtersPanel = createDialog(dialog, {
931
+ placement: 'anchor',
932
+ backdrop: false,
933
+ drag: false,
934
+ autoResize: false,
935
+ triggers: false,
936
+ animateContent: false,
937
+ onOpen: () => keep(flowInMenu([...holder.children])),
938
+ });
939
+ scope.add(() => {
940
+ builder?.destroy();
941
+ filtersPanel?.destroy();
942
+ });
943
+ scope.on(filtersButton, 'click', () => {
944
+ keep(press(filtersButton));
945
+ filtersPanel?.open(filtersButton);
946
+ });
947
+ }
948
+
949
+ const setColumnVisible = (key: string, visible: boolean) => {
950
+ if (!columns.some((column) => column.key === key) || hidden.has(key) === !visible) return;
951
+ if (visible) hidden.delete(key);
952
+ else hidden.add(key);
953
+ buildHead();
954
+ render();
955
+ // The column that comes in fades in; the table settles out of a light blur.
956
+ if (!reduced()) {
957
+ const cells = [...table.querySelectorAll<HTMLElement>('[data-key]')].filter((cell) => cell.dataset.key === key);
958
+ if (visible && cells.length) keep(gsap.fromTo(cells, { opacity: 0, y: -4 }, { opacity: 1, y: 0, duration: 0.3, stagger: 0.01, ease: 'power2.out', clearProps: 'opacity,transform' }));
959
+ else keep(gsap.fromTo(table, { opacity: 0.6, filter: 'blur(2px)' }, { opacity: 1, filter: 'blur(0px)', duration: 0.3, ease: 'power1.out', clearProps: 'opacity,filter' }));
960
+ }
961
+ const column = columns.find((each) => each.key === key)!;
962
+ status.textContent = `Columna ${column.label} ${visible ? 'visible' : 'oculta'}`;
963
+ };
964
+
965
+ buildHead();
966
+ filtersChanged(false);
967
+
968
+ return {
969
+ setRows(next) {
970
+ if (scope.disposed) return;
971
+ rows = [...next];
972
+ const ids = new Set(rows.map(idOf));
973
+ const kept = [...selection].filter((id) => ids.has(id));
974
+ const lost = kept.length !== selection.size;
975
+ selection = new Set(kept);
976
+ loading = false;
977
+ render('reflow');
978
+ if (lost) notifySelection();
979
+ },
980
+ updateRow(id, patch) {
981
+ const index = rows.findIndex((row) => idOf(row) === id);
982
+ if (index < 0) return;
983
+ rows[index] = { ...rows[index]!, ...patch };
984
+ render();
985
+ },
986
+ removeRows(ids) {
987
+ const gone = new Set(ids);
988
+ const leaving = [...body.rows].filter((tr) => [...gone].some((id) => String(id) === tr.dataset.id));
989
+ const apply = () => {
990
+ rows = rows.filter((row) => !gone.has(idOf(row)));
991
+ const had = selection.size;
992
+ selection = new Set([...selection].filter((id) => !gone.has(id)));
993
+ render('reflow');
994
+ if (had !== selection.size) notifySelection();
995
+ status.textContent = plural(ids.length, 'fila eliminada', 'filas eliminadas');
996
+ };
997
+ if (!leaving.length || reduced()) return apply();
998
+ keep(gsap.to(leaving, { opacity: 0, x: -24, duration: 0.22, ease: 'power2.in', onComplete: apply }));
999
+ },
1000
+ setLoading(next) {
1001
+ if (loading === next) return;
1002
+ loading = next;
1003
+ render();
1004
+ },
1005
+ sortBy,
1006
+ setPage,
1007
+ setPageSize(size) {
1008
+ pageSize = Math.max(0, Math.floor(size));
1009
+ sizeSelect.value = String(pageSize);
1010
+ page = 0;
1011
+ render('reflow');
1012
+ },
1013
+ setQuery(next) {
1014
+ query = next;
1015
+ if (searchInput) searchInput.value = next;
1016
+ filtersChanged();
1017
+ },
1018
+ setFilter(next) {
1019
+ filter = cloneFilter(next);
1020
+ builder?.setValue(filter);
1021
+ filtersChanged();
1022
+ },
1023
+ setColumnVisible,
1024
+ select: (ids) => setSelection(ids, false),
1025
+ rowOf: (id) => rowOf(id) as HTMLTableRowElement | null,
1026
+ cellOf: (id, key) => [...(rowOf(id)?.cells ?? [])].find((cell) => cell.dataset.key === key) ?? null,
1027
+ refresh: () => render(),
1028
+ idOf,
1029
+ get rows() {
1030
+ return [...rows];
1031
+ },
1032
+ get results() {
1033
+ return results();
1034
+ },
1035
+ get page() {
1036
+ return page;
1037
+ },
1038
+ get pageCount() {
1039
+ return pageCount(results().length);
1040
+ },
1041
+ get selection() {
1042
+ return [...selection];
1043
+ },
1044
+ get sort() {
1045
+ return sort ? { ...sort } : null;
1046
+ },
1047
+ get query() {
1048
+ return query;
1049
+ },
1050
+ get filter() {
1051
+ return cloneFilter(filter);
1052
+ },
1053
+ get columns() {
1054
+ return columns.map((column) => ({ ...column, hidden: hidden.has(column.key) }));
1055
+ },
1056
+ body,
1057
+ destroy: () => scope.dispose(),
1058
+ };
1059
+ }