@adia-ai/web-components 0.4.6 → 0.4.8

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 (399) hide show
  1. package/README.md +39 -0
  2. package/USAGE.md +29 -9
  3. package/components/accordion/accordion.a2ui.json +3 -0
  4. package/components/accordion/accordion.d.ts +27 -0
  5. package/components/accordion/accordion.js +10 -117
  6. package/components/accordion/accordion.yaml +4 -0
  7. package/components/accordion/class.js +132 -0
  8. package/components/action-list/action-list.a2ui.json +3 -0
  9. package/components/action-list/action-list.d.ts +25 -0
  10. package/components/action-list/action-list.js +9 -140
  11. package/components/action-list/action-list.yaml +4 -0
  12. package/components/action-list/class.js +156 -0
  13. package/components/agent-artifact/agent-artifact.a2ui.json +4 -0
  14. package/components/agent-artifact/agent-artifact.d.ts +35 -0
  15. package/components/agent-artifact/agent-artifact.js +8 -181
  16. package/components/agent-artifact/agent-artifact.yaml +5 -0
  17. package/components/agent-artifact/class.js +200 -0
  18. package/components/agent-feedback-bar/agent-feedback-bar.a2ui.json +3 -0
  19. package/components/agent-feedback-bar/agent-feedback-bar.d.ts +33 -0
  20. package/components/agent-feedback-bar/agent-feedback-bar.js +8 -143
  21. package/components/agent-feedback-bar/agent-feedback-bar.yaml +4 -0
  22. package/components/agent-feedback-bar/class.js +162 -0
  23. package/components/agent-questions/agent-questions.a2ui.json +3 -0
  24. package/components/agent-questions/agent-questions.d.ts +33 -0
  25. package/components/agent-questions/agent-questions.js +8 -180
  26. package/components/agent-questions/agent-questions.yaml +4 -0
  27. package/components/agent-questions/class.js +199 -0
  28. package/components/agent-reasoning/agent-reasoning.a2ui.json +4 -0
  29. package/components/agent-reasoning/agent-reasoning.d.ts +37 -0
  30. package/components/agent-reasoning/agent-reasoning.js +8 -494
  31. package/components/agent-reasoning/agent-reasoning.yaml +5 -0
  32. package/components/agent-reasoning/class.js +513 -0
  33. package/components/agent-suggestions/agent-suggestions.a2ui.json +3 -0
  34. package/components/agent-suggestions/agent-suggestions.d.ts +31 -0
  35. package/components/agent-suggestions/agent-suggestions.js +8 -78
  36. package/components/agent-suggestions/agent-suggestions.yaml +4 -0
  37. package/components/agent-suggestions/class.js +97 -0
  38. package/components/agent-trace/agent-trace.a2ui.json +1 -0
  39. package/components/agent-trace/agent-trace.d.ts +29 -0
  40. package/components/alert/alert.a2ui.json +1 -0
  41. package/components/alert/alert.d.ts +39 -0
  42. package/components/alert/alert.js +8 -175
  43. package/components/alert/class.js +194 -0
  44. package/components/aside/aside.a2ui.json +1 -0
  45. package/components/avatar/avatar.a2ui.json +3 -0
  46. package/components/avatar/avatar.d.ts +28 -0
  47. package/components/avatar/avatar.js +9 -159
  48. package/components/avatar/avatar.yaml +4 -0
  49. package/components/avatar/class.js +173 -0
  50. package/components/badge/badge.a2ui.json +3 -0
  51. package/components/badge/badge.d.ts +28 -0
  52. package/components/badge/badge.js +9 -75
  53. package/components/badge/badge.yaml +4 -0
  54. package/components/badge/class.js +93 -0
  55. package/components/block/block.a2ui.json +1 -0
  56. package/components/block/block.d.ts +20 -0
  57. package/components/block/block.js +9 -15
  58. package/components/block/class.js +33 -0
  59. package/components/breadcrumb/breadcrumb.a2ui.json +5 -0
  60. package/components/breadcrumb/breadcrumb.d.ts +24 -0
  61. package/components/breadcrumb/breadcrumb.js +8 -113
  62. package/components/breadcrumb/breadcrumb.yaml +6 -0
  63. package/components/breadcrumb/class.js +132 -0
  64. package/components/button/button.a2ui.json +3 -0
  65. package/components/button/button.d.ts +44 -0
  66. package/components/button/button.js +15 -66
  67. package/components/button/button.yaml +5 -0
  68. package/components/button/class.js +80 -0
  69. package/components/calendar-picker/calendar-picker.a2ui.json +7 -1
  70. package/components/calendar-picker/calendar-picker.js +8 -332
  71. package/components/calendar-picker/calendar-picker.yaml +51 -177
  72. package/components/calendar-picker/class.js +351 -0
  73. package/components/canvas/canvas.a2ui.json +7 -1
  74. package/components/canvas/canvas.d.ts +33 -0
  75. package/components/canvas/canvas.yaml +29 -36
  76. package/components/card/card.a2ui.json +4 -0
  77. package/components/card/card.d.ts +37 -0
  78. package/components/card/card.js +9 -50
  79. package/components/card/card.yaml +171 -433
  80. package/components/card/class.js +68 -0
  81. package/components/chart/chart.a2ui.json +1 -0
  82. package/components/chart/chart.d.ts +55 -0
  83. package/components/chart/chart.js +8 -2131
  84. package/components/chart/class.js +2150 -0
  85. package/components/chart-legend/chart-legend.a2ui.json +4 -0
  86. package/components/chart-legend/chart-legend.d.ts +37 -0
  87. package/components/chart-legend/chart-legend.js +8 -197
  88. package/components/chart-legend/chart-legend.yaml +5 -0
  89. package/components/chart-legend/class.js +215 -0
  90. package/components/chat-thread/chat-thread.a2ui.json +1 -0
  91. package/components/chat-thread/chat-thread.d.ts +27 -0
  92. package/components/chat-thread/chat-thread.js +8 -157
  93. package/components/chat-thread/class.js +176 -0
  94. package/components/check/check.a2ui.json +1 -0
  95. package/components/check/check.js +11 -52
  96. package/components/check/class.js +68 -0
  97. package/components/code/class.js +501 -0
  98. package/components/code/code.a2ui.json +1 -0
  99. package/components/code/code.js +8 -482
  100. package/components/col/class.js +30 -0
  101. package/components/col/col.a2ui.json +1 -0
  102. package/components/col/col.d.ts +24 -0
  103. package/components/col/col.js +10 -13
  104. package/components/color-picker/class.js +550 -0
  105. package/components/color-picker/color-picker.a2ui.json +3 -0
  106. package/components/color-picker/color-picker.js +8 -531
  107. package/components/color-picker/color-picker.yaml +4 -0
  108. package/components/command/class.js +364 -0
  109. package/components/command/command.a2ui.json +4 -0
  110. package/components/command/command.d.ts +31 -0
  111. package/components/command/command.js +8 -345
  112. package/components/command/command.yaml +105 -124
  113. package/components/demo-toggle/class.js +153 -0
  114. package/components/demo-toggle/demo-toggle.a2ui.json +1 -0
  115. package/components/demo-toggle/demo-toggle.d.ts +33 -0
  116. package/components/demo-toggle/demo-toggle.js +8 -135
  117. package/components/description-list/class.js +86 -0
  118. package/components/description-list/description-list.a2ui.json +1 -0
  119. package/components/description-list/description-list.d.ts +22 -0
  120. package/components/description-list/description-list.js +8 -67
  121. package/components/divider/class.js +57 -0
  122. package/components/divider/divider.a2ui.json +1 -0
  123. package/components/divider/divider.d.ts +20 -0
  124. package/components/divider/divider.js +10 -40
  125. package/components/drawer/class.js +306 -0
  126. package/components/drawer/drawer.a2ui.json +1 -0
  127. package/components/drawer/drawer.d.ts +35 -0
  128. package/components/drawer/drawer.js +8 -287
  129. package/components/embed/class.js +73 -0
  130. package/components/embed/embed.a2ui.json +1 -0
  131. package/components/embed/embed.d.ts +24 -0
  132. package/components/embed/embed.js +9 -55
  133. package/components/empty-state/class.js +108 -0
  134. package/components/empty-state/empty-state.a2ui.json +3 -0
  135. package/components/empty-state/empty-state.d.ts +22 -0
  136. package/components/empty-state/empty-state.js +9 -90
  137. package/components/empty-state/empty-state.yaml +4 -0
  138. package/components/feed/class.js +381 -0
  139. package/components/feed/feed.a2ui.json +9 -1
  140. package/components/feed/feed.d.ts +29 -0
  141. package/components/feed/feed.js +9 -367
  142. package/components/feed/feed.yaml +8 -1
  143. package/components/field/class.js +266 -0
  144. package/components/field/field.a2ui.json +1 -0
  145. package/components/field/field.d.ts +24 -0
  146. package/components/field/field.js +8 -247
  147. package/components/fields/class.js +106 -0
  148. package/components/fields/fields.a2ui.json +1 -0
  149. package/components/fields/fields.d.ts +20 -0
  150. package/components/fields/fields.js +8 -87
  151. package/components/footer/footer.a2ui.json +1 -0
  152. package/components/grid/class.js +31 -0
  153. package/components/grid/grid.a2ui.json +1 -0
  154. package/components/grid/grid.d.ts +24 -0
  155. package/components/grid/grid.js +10 -14
  156. package/components/header/header.a2ui.json +1 -0
  157. package/components/heatmap/class.js +305 -0
  158. package/components/heatmap/heatmap.a2ui.json +1 -0
  159. package/components/heatmap/heatmap.d.ts +43 -0
  160. package/components/heatmap/heatmap.js +8 -286
  161. package/components/icon/class.js +54 -0
  162. package/components/icon/icon.a2ui.json +1 -0
  163. package/components/icon/icon.d.ts +24 -0
  164. package/components/icon/icon.js +13 -40
  165. package/components/image/class.js +112 -0
  166. package/components/image/image.a2ui.json +3 -0
  167. package/components/image/image.d.ts +34 -0
  168. package/components/image/image.js +9 -94
  169. package/components/image/image.yaml +4 -0
  170. package/components/index.js +8 -0
  171. package/components/input/class.js +773 -0
  172. package/components/input/input.a2ui.json +7 -0
  173. package/components/input/input.js +8 -755
  174. package/components/input/input.yaml +177 -442
  175. package/components/inspector/class.js +142 -0
  176. package/components/inspector/inspector.a2ui.json +13 -1
  177. package/components/inspector/inspector.d.ts +18 -0
  178. package/components/inspector/inspector.js +8 -124
  179. package/components/inspector/inspector.yaml +21 -30
  180. package/components/kbd/class.js +34 -0
  181. package/components/kbd/kbd.a2ui.json +4 -0
  182. package/components/kbd/kbd.d.ts +18 -0
  183. package/components/kbd/kbd.js +10 -17
  184. package/components/kbd/kbd.yaml +54 -185
  185. package/components/link/class.js +187 -0
  186. package/components/link/link.a2ui.json +1 -0
  187. package/components/link/link.d.ts +65 -0
  188. package/components/link/link.js +8 -168
  189. package/components/list/class.js +249 -0
  190. package/components/list/list.a2ui.json +3 -0
  191. package/components/list/list.d.ts +33 -0
  192. package/components/list/list.js +9 -231
  193. package/components/list/list.yaml +4 -0
  194. package/components/menu/class.js +332 -0
  195. package/components/menu/menu.a2ui.json +3 -0
  196. package/components/menu/menu.d.ts +31 -0
  197. package/components/menu/menu.js +11 -316
  198. package/components/menu/menu.yaml +4 -0
  199. package/components/modal/class.js +231 -0
  200. package/components/modal/modal.a2ui.json +6 -1
  201. package/components/modal/modal.d.ts +33 -0
  202. package/components/modal/modal.js +8 -212
  203. package/components/modal/modal.yaml +19 -39
  204. package/components/nav/class.js +150 -0
  205. package/components/nav/nav.a2ui.json +1 -0
  206. package/components/nav/nav.d.ts +41 -0
  207. package/components/nav/nav.js +8 -131
  208. package/components/nav-group/class.js +152 -0
  209. package/components/nav-group/nav-group.a2ui.json +1 -0
  210. package/components/nav-group/nav-group.d.ts +45 -0
  211. package/components/nav-group/nav-group.js +9 -134
  212. package/components/nav-item/class.js +86 -0
  213. package/components/nav-item/nav-item.a2ui.json +1 -0
  214. package/components/nav-item/nav-item.d.ts +47 -0
  215. package/components/nav-item/nav-item.js +10 -69
  216. package/components/noodles/class.js +510 -0
  217. package/components/noodles/noodles.a2ui.json +1 -0
  218. package/components/noodles/noodles.d.ts +47 -0
  219. package/components/noodles/noodles.js +9 -493
  220. package/components/option-card/class.js +167 -0
  221. package/components/option-card/option-card.a2ui.json +3 -0
  222. package/components/option-card/option-card.js +8 -149
  223. package/components/option-card/option-card.yaml +4 -0
  224. package/components/otp-input/class.js +180 -0
  225. package/components/otp-input/otp-input.a2ui.json +6 -1
  226. package/components/otp-input/otp-input.js +9 -162
  227. package/components/otp-input/otp-input.yaml +45 -174
  228. package/components/page/class.js +97 -0
  229. package/components/page/page.a2ui.json +1 -0
  230. package/components/page/page.d.ts +47 -0
  231. package/components/page/page.js +8 -79
  232. package/components/pagination/class.js +195 -0
  233. package/components/pagination/pagination.a2ui.json +1 -0
  234. package/components/pagination/pagination.d.ts +33 -0
  235. package/components/pagination/pagination.js +9 -177
  236. package/components/pane/class.js +186 -0
  237. package/components/pane/pane.a2ui.json +20 -2
  238. package/components/pane/pane.d.ts +41 -0
  239. package/components/pane/pane.js +8 -167
  240. package/components/pane/pane.yaml +64 -158
  241. package/components/pipeline-status/class.js +189 -0
  242. package/components/pipeline-status/pipeline-status.a2ui.json +8 -1
  243. package/components/pipeline-status/pipeline-status.d.ts +22 -0
  244. package/components/pipeline-status/pipeline-status.js +9 -172
  245. package/components/pipeline-status/pipeline-status.yaml +34 -72
  246. package/components/popover/class.js +194 -0
  247. package/components/popover/popover.a2ui.json +1 -0
  248. package/components/popover/popover.d.ts +24 -0
  249. package/components/popover/popover.js +9 -176
  250. package/components/progress/class.js +74 -0
  251. package/components/progress/progress.a2ui.json +4 -0
  252. package/components/progress/progress.d.ts +20 -0
  253. package/components/progress/progress.js +10 -57
  254. package/components/progress/progress.yaml +124 -287
  255. package/components/progress-row/class.js +110 -0
  256. package/components/progress-row/progress-row.a2ui.json +3 -0
  257. package/components/progress-row/progress-row.d.ts +24 -0
  258. package/components/progress-row/progress-row.js +8 -92
  259. package/components/progress-row/progress-row.yaml +4 -0
  260. package/components/radio/class.js +83 -0
  261. package/components/radio/radio.a2ui.json +1 -0
  262. package/components/radio/radio.js +11 -67
  263. package/components/range/class.js +194 -0
  264. package/components/range/range.a2ui.json +1 -0
  265. package/components/range/range.js +9 -176
  266. package/components/rating/class.js +148 -0
  267. package/components/rating/rating.a2ui.json +1 -0
  268. package/components/rating/rating.js +9 -130
  269. package/components/richtext/class.js +87 -0
  270. package/components/richtext/richtext.a2ui.json +8 -1
  271. package/components/richtext/richtext.d.ts +20 -0
  272. package/components/richtext/richtext.js +8 -68
  273. package/components/richtext/richtext.yaml +30 -65
  274. package/components/row/class.js +50 -0
  275. package/components/row/row.a2ui.json +1 -0
  276. package/components/row/row.d.ts +37 -0
  277. package/components/row/row.js +10 -33
  278. package/components/search/class.js +134 -0
  279. package/components/search/search.a2ui.json +1 -0
  280. package/components/search/search.js +10 -117
  281. package/components/section/section.a2ui.json +1 -0
  282. package/components/segment/class.js +62 -0
  283. package/components/segment/segment.a2ui.json +3 -0
  284. package/components/segment/segment.d.ts +26 -0
  285. package/components/segment/segment.js +10 -45
  286. package/components/segment/segment.yaml +4 -0
  287. package/components/segmented/class.js +165 -0
  288. package/components/segmented/segmented.a2ui.json +5 -0
  289. package/components/segmented/segmented.js +10 -148
  290. package/components/segmented/segmented.yaml +41 -59
  291. package/components/select/class.js +408 -0
  292. package/components/select/select.a2ui.json +3 -0
  293. package/components/select/select.js +15 -396
  294. package/components/select/select.yaml +4 -0
  295. package/components/skeleton/class.js +52 -0
  296. package/components/skeleton/skeleton.a2ui.json +1 -0
  297. package/components/skeleton/skeleton.d.ts +24 -0
  298. package/components/skeleton/skeleton.js +8 -34
  299. package/components/slider/class.js +184 -0
  300. package/components/slider/slider.a2ui.json +1 -0
  301. package/components/slider/slider.js +9 -166
  302. package/components/stack/class.js +28 -0
  303. package/components/stack/stack.a2ui.json +1 -0
  304. package/components/stack/stack.d.ts +18 -0
  305. package/components/stack/stack.js +10 -11
  306. package/components/stat/stat.a2ui.json +1 -0
  307. package/components/step-progress/class.js +98 -0
  308. package/components/step-progress/step-progress.a2ui.json +1 -0
  309. package/components/step-progress/step-progress.d.ts +28 -0
  310. package/components/step-progress/step-progress.js +8 -79
  311. package/components/stepper/class.js +126 -0
  312. package/components/stepper/stepper.a2ui.json +3 -0
  313. package/components/stepper/stepper.d.ts +20 -0
  314. package/components/stepper/stepper.js +9 -112
  315. package/components/stepper/stepper.yaml +4 -0
  316. package/components/stream/class.js +109 -0
  317. package/components/stream/stream.a2ui.json +1 -0
  318. package/components/stream/stream.d.ts +33 -0
  319. package/components/stream/stream.js +8 -90
  320. package/components/swatch/class.js +131 -0
  321. package/components/swatch/swatch.a2ui.json +1 -0
  322. package/components/swatch/swatch.d.ts +29 -0
  323. package/components/swatch/swatch.js +8 -112
  324. package/components/swiper/class.js +373 -0
  325. package/components/swiper/swiper.a2ui.json +7 -0
  326. package/components/swiper/swiper.d.ts +45 -0
  327. package/components/swiper/swiper.js +8 -354
  328. package/components/swiper/swiper.yaml +72 -212
  329. package/components/switch/class.js +63 -0
  330. package/components/switch/switch.a2ui.json +7 -1
  331. package/components/switch/switch.js +11 -47
  332. package/components/switch/switch.yaml +70 -265
  333. package/components/table/class.js +1453 -0
  334. package/components/table/table.a2ui.json +7 -0
  335. package/components/table/table.d.ts +55 -0
  336. package/components/table/table.js +8 -1435
  337. package/components/table/table.yaml +8 -0
  338. package/components/table-toolbar/class.js +680 -0
  339. package/components/table-toolbar/table-toolbar.a2ui.json +12 -0
  340. package/components/table-toolbar/table-toolbar.d.ts +49 -0
  341. package/components/table-toolbar/table-toolbar.js +8 -689
  342. package/components/table-toolbar/table-toolbar.yaml +13 -0
  343. package/components/tabs/class.js +242 -0
  344. package/components/tabs/tabs.a2ui.json +3 -0
  345. package/components/tabs/tabs.d.ts +31 -0
  346. package/components/tabs/tabs.js +8 -223
  347. package/components/tabs/tabs.yaml +4 -0
  348. package/components/tag/class.js +99 -0
  349. package/components/tag/tag.a2ui.json +1 -0
  350. package/components/tag/tag.d.ts +37 -0
  351. package/components/tag/tag.js +8 -80
  352. package/components/text/class.js +46 -0
  353. package/components/text/text.a2ui.json +1 -0
  354. package/components/text/text.d.ts +26 -0
  355. package/components/text/text.js +9 -28
  356. package/components/textarea/class.js +134 -0
  357. package/components/textarea/textarea.a2ui.json +1 -0
  358. package/components/textarea/textarea.js +11 -118
  359. package/components/timeline/class.js +176 -0
  360. package/components/timeline/timeline.a2ui.json +18 -1
  361. package/components/timeline/timeline.d.ts +36 -0
  362. package/components/timeline/timeline.js +9 -162
  363. package/components/timeline/timeline.yaml +14 -1
  364. package/components/toast/class.js +92 -0
  365. package/components/toast/toast.a2ui.json +1 -0
  366. package/components/toast/toast.d.ts +33 -0
  367. package/components/toast/toast.js +9 -76
  368. package/components/toggle-group/class.js +154 -0
  369. package/components/toggle-group/toggle-group.a2ui.json +1 -0
  370. package/components/toggle-group/toggle-group.d.ts +29 -0
  371. package/components/toggle-group/toggle-group.js +11 -140
  372. package/components/toggle-scheme/class.js +286 -0
  373. package/components/toggle-scheme/toggle-scheme.a2ui.json +3 -0
  374. package/components/toggle-scheme/toggle-scheme.d.ts +51 -0
  375. package/components/toggle-scheme/toggle-scheme.js +8 -268
  376. package/components/toggle-scheme/toggle-scheme.yaml +4 -0
  377. package/components/toolbar/class.js +388 -0
  378. package/components/toolbar/toolbar.a2ui.json +3 -0
  379. package/components/toolbar/toolbar.d.ts +24 -0
  380. package/components/toolbar/toolbar.js +10 -376
  381. package/components/toolbar/toolbar.yaml +4 -0
  382. package/components/tooltip/class.js +299 -0
  383. package/components/tooltip/tooltip.a2ui.json +1 -0
  384. package/components/tooltip/tooltip.d.ts +28 -0
  385. package/components/tooltip/tooltip.js +8 -280
  386. package/components/tree/class.js +245 -0
  387. package/components/tree/tree.a2ui.json +3 -0
  388. package/components/tree/tree.d.ts +25 -0
  389. package/components/tree/tree.js +9 -244
  390. package/components/tree/tree.yaml +4 -0
  391. package/components/upload/class.js +199 -0
  392. package/components/upload/upload.a2ui.json +1 -0
  393. package/components/upload/upload.js +11 -183
  394. package/core/icons-phosphor.js +93 -0
  395. package/core/icons.js +92 -90
  396. package/core/index.js +5 -0
  397. package/index.d.ts +160 -5
  398. package/index.js +7 -0
  399. package/package.json +7 -2
@@ -1,1444 +1,17 @@
1
1
  /**
2
- * <table-ui>Data table with sorting, selection, pagination, search,
3
- * column resize, keyboard navigation, cell types, and CSV export.
2
+ * `<table-ui>`auto-registers the tag on import.
4
3
  *
5
- * Renders as CSS grid + subgrid rows with ARIA grid roles (no <table> element).
4
+ * For non-side-effect class import (test isolation, tag override), use
5
+ * the `class` subpath:
6
6
  *
7
- * Attributes:
8
- * sortable — enable column sorting on header click
9
- * selectable — enable row selection via checkboxes
10
- * striped — alternate row background
11
- * density — 'compact' | 'standard' | 'comfortable'
12
- * paginate — rows per page (0 = no pagination)
13
- * loading — show loading overlay
14
- * search — global search filter string
7
+ * import { UITable } from '@adia-ai/web-components/components/table/class';
15
8
  *
16
- * Declarative columns via <col-def> children:
17
- * <col-def key="name" label="Name" type="text" width="200"
18
- * min-width="100" max-width="400" flex="2" pinned="left"
19
- * sortable hidden filter-type="text"></col-def>
20
- *
21
- * JS API:
22
- * .columns = [{key, label, type, width, minWidth, maxWidth, flex,
23
- * sortable, filterable, resizable, pinned, hidden,
24
- * accessor, format, render, sortFn, sortDescFirst,
25
- * filterType, filterFn, aggregate, meta}]
26
- * .data = [{...}, ...]
27
- * .selected → [...indices] (read-only)
28
- * .sortState → [{key, dir}] (read-only)
29
- * .exportCSV(filename?)
30
- *
31
- * Events:
32
- * sort — { detail: { key, dir, sortState } }
33
- * select — { detail: { selected: [...indices] } }
34
- * page — { detail: { page } }
35
- * resize — { detail: { key, width } }
36
- * cell-click — { detail: { key, row, value, dataIndex } }
37
- */
38
-
39
- import { UIElement } from '../../core/element.js';
40
- import { cellTypes, sortFns } from './cell-types.js';
41
-
42
- // ── Helpers ──────────────────────────────────────────────────────────────────
43
-
44
- /**
45
- * Resolve a dot-notation path on an object.
46
- * getNestedValue({user: {name: 'Jo'}}, 'user.name') → 'Jo'
9
+ * @see ../../USAGE.md#registration--auto-vs-explicit
47
10
  */
48
- function getNestedValue(obj, path) {
49
- if (!path || !obj) return undefined;
50
- const parts = path.split('.');
51
- let cur = obj;
52
- for (const p of parts) {
53
- if (cur == null) return undefined;
54
- cur = cur[p];
55
- }
56
- return cur;
57
- }
58
-
59
- /**
60
- * Get cell value for a column from a row, respecting accessor and dot notation.
61
- */
62
- function getCellValue(row, col) {
63
- if (typeof col.accessor === 'function') return col.accessor(row);
64
- return getNestedValue(row, col.key);
65
- }
66
-
67
- /**
68
- * Escape a value for CSV output.
69
- */
70
- function csvEscape(val) {
71
- const str = String(val ?? '');
72
- if (str.includes(',') || str.includes('"') || str.includes('\n')) {
73
- return '"' + str.replace(/"/g, '""') + '"';
74
- }
75
- return str;
76
- }
77
-
78
- // ── Component ────────────────────────────────────────────────────────────────
79
-
80
- class UITable extends UIElement {
81
- static properties = {
82
- sortable: { type: Boolean, default: false, reflect: true },
83
- selectable: { type: Boolean, default: false, reflect: true },
84
- expandable: { type: Boolean, default: false, reflect: true },
85
- striped: { type: Boolean, default: false, reflect: true },
86
- raw: { type: Boolean, default: false, reflect: true },
87
- density: { type: String, default: 'standard', reflect: true },
88
- paginate: { type: Number, default: 0, reflect: true },
89
- loading: { type: Boolean, default: false, reflect: true },
90
- search: { type: String, default: '', reflect: true },
91
- };
92
-
93
- static template = () => null;
94
-
95
- // ── Private state ──
96
-
97
- #columns = [];
98
- #data = [];
99
- #sortState = []; // [{key, dir}] for multi-sort
100
- #filters = new Map(); // key → { op, value }
101
- #expanded = new Set(); // row indices
102
- #editingCell = null; // {rowIndex, colKey} or null
103
- #page = 0;
104
- #selected = new Set();
105
- #columnWidths = new Map(); // key → current width in px
106
- #focusedCell = null; // {row, col}
107
- #bound = false;
108
- #lastSelectedIndex = -1; // for shift-click range select
109
- #openFilter = null; // column key of open filter dropdown
110
- #filterFocusRaf = null;
111
-
112
- // ── Public API: columns ──
113
-
114
- set columns(arr) {
115
- this.#columns = Array.isArray(arr) ? arr : [];
116
- this.#requestRender();
117
- }
118
-
119
- get columns() { return this.#columns; }
120
-
121
- // ── Public API: data ──
122
-
123
- set data(arr) {
124
- this.#data = Array.isArray(arr) ? arr : [];
125
- this.#selected.clear();
126
- this.#page = 0;
127
- this.#requestRender();
128
- }
129
-
130
- get data() { return this.#data; }
131
-
132
- // ── Public API: selection ──
133
-
134
- /**
135
- * Indices of currently-selected rows, ascending.
136
- * @returns {number[]}
137
- */
138
- get selected() { return [...this.#selected].sort((a, b) => a - b); }
139
-
140
- /**
141
- * Replace the selection set programmatically. Out-of-range indices are
142
- * silently dropped. Pair with `selectable` mode; on a non-selectable
143
- * table the indices are stored but not rendered as checked rows.
144
- * @param {Iterable<number>} indices
145
- */
146
- set selected(indices) {
147
- this.#selected.clear();
148
- if (indices) {
149
- for (const i of indices) {
150
- if (Number.isInteger(i) && i >= 0 && i < this.#data.length) {
151
- this.#selected.add(i);
152
- }
153
- }
154
- }
155
- this.#requestRender();
156
- this.dispatchEvent(new CustomEvent('select', {
157
- detail: { selected: this.selected },
158
- bubbles: true,
159
- }));
160
- }
161
-
162
- /**
163
- * Empty the selection set. Equivalent to `el.selected = []` but doesn't
164
- * require constructing an empty array; the most common selection-write
165
- * call by far (after a "delete" or "archive" bulk action), so worth
166
- * having a one-token verb form.
167
- */
168
- clearSelection() {
169
- if (this.#selected.size === 0) return;
170
- this.#selected.clear();
171
- this.#requestRender();
172
- this.dispatchEvent(new CustomEvent('select', {
173
- detail: { selected: [] },
174
- bubbles: true,
175
- }));
176
- }
177
-
178
- // ── Public API: read-only getters ──
179
-
180
- get sortState() { return this.#sortState.map(s => ({ ...s })); }
181
-
182
- // ── Public API: filters ──
183
-
184
- setFilter(key, value, op = 'contains') {
185
- if (value === null || value === undefined || value === '') {
186
- this.#filters.delete(key);
187
- } else {
188
- this.#filters.set(key, { op, value });
189
- }
190
- this.#page = 0;
191
- this.#requestRender();
192
- this.dispatchEvent(new CustomEvent('filter-change', {
193
- detail: { filters: Object.fromEntries(this.#filters) },
194
- bubbles: true,
195
- }));
196
- }
197
-
198
- clearFilters() {
199
- this.#filters.clear();
200
- this.#openFilter = null;
201
- this.#page = 0;
202
- this.#requestRender();
203
- this.dispatchEvent(new CustomEvent('filter-change', {
204
- detail: { filters: {} },
205
- bubbles: true,
206
- }));
207
- }
208
-
209
- get filters() { return Object.fromEntries(this.#filters); }
210
-
211
- // ── Public API: expansion ──
212
-
213
- toggleExpand(index) {
214
- if (this.#expanded.has(index)) {
215
- this.#expanded.delete(index);
216
- this.dispatchEvent(new CustomEvent('row-collapse', { detail: { index, row: this.#data[index] }, bubbles: true }));
217
- } else {
218
- this.#expanded.add(index);
219
- this.dispatchEvent(new CustomEvent('row-expand', { detail: { index, row: this.#data[index] }, bubbles: true }));
220
- }
221
- this.#requestRender();
222
- }
223
-
224
- get expanded() { return [...this.#expanded]; }
225
-
226
- /** @type {((row: object, index: number) => HTMLElement)|null} */
227
- expandRenderer = null;
228
-
229
- // ── Public API: state persistence ──
230
-
231
- getState() {
232
- return {
233
- sort: this.#sortState.map(s => ({ ...s })),
234
- filters: Object.fromEntries(this.#filters),
235
- columnWidths: Object.fromEntries(this.#columnWidths),
236
- hiddenColumns: this.#columns.filter(c => c.hidden).map(c => c.key),
237
- page: this.#page,
238
- density: this.density,
239
- };
240
- }
241
-
242
- setState(state) {
243
- if (!state) return;
244
- if (state.sort) this.#sortState = state.sort;
245
- if (state.filters) {
246
- this.#filters.clear();
247
- for (const [key, val] of Object.entries(state.filters)) {
248
- this.#filters.set(key, val);
249
- }
250
- }
251
- if (state.columnWidths) {
252
- this.#columnWidths.clear();
253
- for (const [key, val] of Object.entries(state.columnWidths)) {
254
- this.#columnWidths.set(key, val);
255
- }
256
- }
257
- if (state.hiddenColumns) {
258
- for (const col of this.#columns) {
259
- col.hidden = state.hiddenColumns.includes(col.key);
260
- }
261
- }
262
- if (state.page != null) this.#page = state.page;
263
- if (state.density) this.density = state.density;
264
- this.#requestRender();
265
- }
266
-
267
- #persistState() {
268
- const key = this.getAttribute('state-key');
269
- if (!key) return;
270
- try {
271
- localStorage.setItem(`table-state:${key}`, JSON.stringify(this.getState()));
272
- } catch {}
273
- }
274
-
275
- #restoreState() {
276
- const key = this.getAttribute('state-key');
277
- if (!key) return;
278
- try {
279
- const saved = localStorage.getItem(`table-state:${key}`);
280
- if (saved) this.setState(JSON.parse(saved));
281
- } catch {}
282
- }
283
-
284
- // ── Lifecycle ──────────────────────────────────────────────────────────────
285
-
286
- connected() {
287
- // Parse declarative <col-def> children
288
- this.#parseColDefs();
289
-
290
- this.setAttribute('role', 'grid');
291
- this.setAttribute('tabindex', '0');
292
-
293
- // Restore persisted state
294
- this.#restoreState();
295
-
296
- if (!this.#bound) {
297
- this.#bound = true;
298
- this.addEventListener('click', this.#onClick);
299
- this.addEventListener('keydown', this.#onKeydown);
300
- }
301
- }
302
-
303
- disconnected() {
304
- this.removeEventListener('click', this.#onClick);
305
- this.removeEventListener('keydown', this.#onKeydown);
306
- this.#bound = false;
307
- this.#cleanupResize();
308
- if (this.#renderRaf) {
309
- cancelAnimationFrame(this.#renderRaf);
310
- this.#renderRaf = null;
311
- }
312
- if (this.#filterFocusRaf != null) {
313
- cancelAnimationFrame(this.#filterFocusRaf);
314
- this.#filterFocusRaf = null;
315
- }
316
- }
317
-
318
- // ── <col-def> Parsing ──────────────────────────────────────────────────────
319
-
320
- #parseColDefs() {
321
- const defs = this.querySelectorAll(':scope > col-def');
322
- if (!defs.length) return;
323
-
324
- const cols = [];
325
- for (const el of defs) {
326
- const col = {
327
- key: el.getAttribute('key') || '',
328
- label: el.getAttribute('label') || el.getAttribute('key') || '',
329
- type: el.getAttribute('type') || 'text',
330
- pinned: el.getAttribute('pinned') || null, // 'left' | 'right' | null
331
- hidden: el.hasAttribute('hidden'),
332
- sortable: el.hasAttribute('sortable'),
333
- resizable: el.hasAttribute('resizable'),
334
- filterable: el.hasAttribute('filterable'),
335
- filterType: el.getAttribute('filter-type') || null,
336
- };
337
-
338
- const w = el.getAttribute('width');
339
- if (w) col.width = Number(w);
340
-
341
- const min = el.getAttribute('min-width');
342
- if (min) col.minWidth = Number(min);
343
-
344
- const max = el.getAttribute('max-width');
345
- if (max) col.maxWidth = Number(max);
346
-
347
- const flex = el.getAttribute('flex');
348
- if (flex) col.flex = Number(flex);
349
-
350
- cols.push(col);
351
- el.remove();
352
- }
353
-
354
- // Only set if we actually found col-defs and columns aren't already set via JS
355
- if (cols.length && !this.#columns.length) {
356
- this.#columns = cols;
357
- }
358
- }
359
-
360
- // ── Render Batching ────────────────────────────────────────────────────────
361
-
362
- #renderRaf = null;
363
-
364
- #requestRender() {
365
- if (!this.isConnected || this.#renderRaf) return;
366
- this.#renderRaf = requestAnimationFrame(() => {
367
- this.#renderRaf = null;
368
- this.render();
369
- this.#persistState();
370
- });
371
- }
372
-
373
- // ── Visible Columns Helper ─────────────────────────────────────────────────
374
-
375
- get #visibleColumns() {
376
- return this.#columns.filter(c => !c.hidden);
377
- }
378
-
379
- // ── Grid Template Columns ──────────────────────────────────────────────────
380
-
381
- #buildGridTemplate() {
382
- const parts = [];
383
-
384
- if (this.expandable) {
385
- parts.push('2.5rem');
386
- }
387
-
388
- if (this.selectable) {
389
- parts.push('2.5rem');
390
- }
391
-
392
- for (const col of this.#visibleColumns) {
393
- // User-resized width takes priority
394
- if (this.#columnWidths.has(col.key)) {
395
- parts.push(`${this.#columnWidths.get(col.key)}px`);
396
- } else if (col.width) {
397
- parts.push(`${col.width}px`);
398
- } else if (col.flex) {
399
- parts.push(`minmax(6rem, ${col.flex}fr)`);
400
- } else {
401
- parts.push('minmax(6rem, 1fr)');
402
- }
403
- }
404
-
405
- return parts.join(' ');
406
- }
407
-
408
- // ── Data Processing Pipeline ───────────────────────────────────────────────
409
-
410
- /**
411
- * Apply search → column filters → sort → return array of original data indices.
412
- */
413
- #getProcessedIndices() {
414
- let indices = this.#data.map((_, i) => i);
415
-
416
- // 1. Global search filter
417
- if (this.search) {
418
- const q = this.search.toLowerCase();
419
- indices = indices.filter(i => {
420
- const row = this.#data[i];
421
- return this.#visibleColumns.some(col => {
422
- const val = getCellValue(row, col);
423
- return val != null && String(val).toLowerCase().includes(q);
424
- });
425
- });
426
- }
427
-
428
- // 2. Column filters
429
- if (this.#filters.size) {
430
- indices = indices.filter(i => {
431
- const row = this.#data[i];
432
- for (const [key, { op, value }] of this.#filters) {
433
- const col = this.#columns.find(c => c.key === key);
434
- const cellVal = getCellValue(row, col || { key });
435
- const str = String(cellVal ?? '').toLowerCase();
436
- const target = String(value).toLowerCase();
437
- switch (op) {
438
- case 'contains': if (!str.includes(target)) return false; break;
439
- case 'equals': if (str !== target) return false; break;
440
- case 'startsWith': if (!str.startsWith(target)) return false; break;
441
- case 'gt': if (Number(cellVal) <= Number(value)) return false; break;
442
- case 'lt': if (Number(cellVal) >= Number(value)) return false; break;
443
- case 'gte': if (Number(cellVal) < Number(value)) return false; break;
444
- case 'lte': if (Number(cellVal) > Number(value)) return false; break;
445
- case 'between': {
446
- const [lo, hi] = String(value).split(',').map(Number);
447
- const n = Number(cellVal);
448
- if (n < lo || n > hi) return false;
449
- break;
450
- }
451
- case 'select': {
452
- const selected = target.split(',').map(s => s.trim());
453
- if (!selected.includes(str)) return false;
454
- break;
455
- }
456
- default: if (!str.includes(target)) return false;
457
- }
458
- }
459
- return true;
460
- });
461
- }
462
-
463
- // 3. Multi-sort
464
- if (this.#sortState.length) {
465
- indices.sort((a, b) => {
466
- for (const { key, dir } of this.#sortState) {
467
- const col = this.#columns.find(c => c.key === key);
468
- const va = getCellValue(this.#data[a], col || { key });
469
- const vb = getCellValue(this.#data[b], col || { key });
470
-
471
- // Determine sort function
472
- let fn;
473
- if (typeof col?.sortFn === 'function') {
474
- fn = col.sortFn;
475
- } else {
476
- const typeDef = cellTypes[col?.type || 'text'];
477
- const fnName = col?.sortFn || typeDef?.sortFn || 'alphanumeric';
478
- fn = sortFns[fnName] || sortFns.alphanumeric;
479
- }
480
-
481
- // Nulls always sort last
482
- if (va == null && vb == null) continue;
483
- if (va == null) return 1;
484
- if (vb == null) return -1;
485
-
486
- const cmp = fn(va, vb);
487
- if (cmp !== 0) return dir === 'asc' ? cmp : -cmp;
488
- }
489
- return 0;
490
- });
491
- }
492
-
493
- return indices;
494
- }
495
-
496
- /**
497
- * Paginate a set of indices.
498
- */
499
- #getPageSlice(indices) {
500
- if (!this.paginate || this.paginate <= 0) return indices;
501
- const start = this.#page * this.paginate;
502
- return indices.slice(start, start + this.paginate);
503
- }
504
-
505
- get #pageCount() {
506
- if (!this.paginate || this.paginate <= 0) return 1;
507
- // pageCount should reflect filtered data, not raw data
508
- const filteredCount = this.#getProcessedIndices().length;
509
- return Math.max(1, Math.ceil(filteredCount / this.paginate));
510
- }
511
-
512
- // ── Render ─────────────────────────────────────────────────────────────────
513
-
514
- render() {
515
- const visCols = this.#visibleColumns;
516
-
517
- // Set grid template
518
- this.style.gridTemplateColumns = this.#buildGridTemplate();
519
-
520
- // ── Header row ──
521
-
522
- let header = this.querySelector(':scope > [data-header]');
523
- if (!header) {
524
- header = document.createElement('div');
525
- header.setAttribute('role', 'row');
526
- header.setAttribute('data-header', '');
527
- this.prepend(header);
528
- }
529
-
530
- this.#renderHeader(header, visCols);
531
-
532
- // ── Filter chips bar ──
533
-
534
- this.#renderFilterChips();
535
-
536
- // ── Body rowgroup ──
537
-
538
- let body = this.querySelector(':scope > [data-body]');
539
- if (!body) {
540
- body = document.createElement('div');
541
- body.setAttribute('role', 'rowgroup');
542
- body.setAttribute('data-body', '');
543
- header.after(body);
544
- }
545
-
546
- // Process data: search → sort → paginate
547
- const allProcessed = this.#getProcessedIndices();
548
- const pageIndices = this.#getPageSlice(allProcessed);
549
-
550
- // Reconcile body rows (with expansion support)
551
- const bodyChildren = [];
552
- for (const idx of pageIndices) {
553
- const existing = body.querySelector(`:scope > [role="row"][data-index="${idx}"]`);
554
- const row = existing || this.#createRow(idx, visCols);
555
- if (existing) this.#updateRow(existing, idx, visCols);
556
-
557
- // Expand toggle in first cell
558
- if (this.expandable) {
559
- const isExpanded = this.#expanded.has(idx);
560
- if (isExpanded) row.setAttribute('data-expanded', '');
561
- else row.removeAttribute('data-expanded');
562
- }
563
-
564
- bodyChildren.push(row);
565
-
566
- // Detail row (expansion)
567
- if (this.expandable && this.#expanded.has(idx)) {
568
- let detail = body.querySelector(`:scope > [data-detail-row][data-for="${idx}"]`);
569
- if (!detail) {
570
- detail = document.createElement('div');
571
- detail.setAttribute('data-detail-row', '');
572
- detail.dataset.for = idx;
573
- if (this.expandRenderer) {
574
- const content = this.expandRenderer(this.#data[idx], idx);
575
- if (content instanceof Node) detail.appendChild(content);
576
- else detail.textContent = String(content);
577
- }
578
- }
579
- bodyChildren.push(detail);
580
- }
581
- }
582
-
583
- // Reconcile: replace body contents
584
- const existingChildren = [...body.children];
585
- for (const child of existingChildren) {
586
- if (!bodyChildren.includes(child)) child.remove();
587
- }
588
- for (let i = 0; i < bodyChildren.length; i++) {
589
- if (body.children[i] !== bodyChildren[i]) {
590
- if (body.children[i]) body.insertBefore(bodyChildren[i], body.children[i]);
591
- else body.appendChild(bodyChildren[i]);
592
- }
593
- }
594
-
595
- // ── Overlays ──
596
-
597
- this.#renderOverlays(body);
598
-
599
- // ── Aggregation footer ──
600
-
601
- this.#renderAggregation(body, allProcessed, visCols);
602
-
603
- // ── Pagination footer ──
604
-
605
- const showPagination = this.paginate > 0 && this.#data.length > 0;
606
- let footer = this.querySelector(':scope > [data-footer]');
607
-
608
- if (showPagination) {
609
- if (!footer) {
610
- footer = document.createElement('div');
611
- footer.setAttribute('data-footer', '');
612
- this.appendChild(footer);
613
- }
614
- this.#renderPagination(footer, allProcessed.length);
615
- } else if (footer) {
616
- footer.remove();
617
- }
618
- }
619
-
620
- // ── Header Rendering ───────────────────────────────────────────────────────
621
-
622
- #renderHeader(header, visCols) {
623
- const allSelected = this.#data.length > 0 && this.#selected.size === this.#data.length;
624
- const cells = [];
625
-
626
- // Checkbox column header
627
- if (this.selectable) {
628
- const cell = document.createElement('div');
629
- cell.setAttribute('role', 'columnheader');
630
- cell.setAttribute('data-check-col', '');
631
- const check = document.createElement('check-ui');
632
- if (allSelected) check.setAttribute('checked', '');
633
- check.setAttribute('aria-label', 'Select all rows');
634
- cell.appendChild(check);
635
- cells.push(cell);
636
- }
637
-
638
- // Data columns
639
- for (const col of visCols) {
640
- const cell = document.createElement('div');
641
- cell.setAttribute('role', 'columnheader');
642
-
643
- // Label
644
- const label = document.createElement('span');
645
- label.textContent = col.label || col.key;
646
- cell.appendChild(label);
647
-
648
- // Sortable
649
- const colSortable = col.sortable !== false && this.sortable;
650
- if (colSortable) {
651
- cell.dataset.sortKey = col.key;
652
- cell.setAttribute('aria-label', `Sort by ${col.label || col.key}`);
653
-
654
- const icon = document.createElement('icon-ui');
655
- icon.setAttribute('data-sort-icon', '');
656
-
657
- const sortEntry = this.#sortState.find(s => s.key === col.key);
658
- if (sortEntry) {
659
- cell.setAttribute('aria-sort', sortEntry.dir === 'asc' ? 'ascending' : 'descending');
660
- icon.setAttribute('name', sortEntry.dir === 'asc' ? 'arrow-up' : 'arrow-down');
661
- } else {
662
- icon.setAttribute('name', 'caret-up-down');
663
- }
664
-
665
- cell.appendChild(icon);
666
- }
667
-
668
- // Pinned
669
- if (col.pinned) {
670
- cell.setAttribute('data-pinned', col.pinned);
671
- }
672
-
673
- // Resize handle
674
- if (col.resizable !== false) {
675
- const handle = document.createElement('div');
676
- handle.setAttribute('data-resize-handle', '');
677
- handle.dataset.resizeKey = col.key;
678
- cell.appendChild(handle);
679
- }
680
-
681
- // Filter button
682
- if (col.filter) {
683
- const filterBtn = document.createElement('button');
684
- filterBtn.setAttribute('data-filter-btn', '');
685
- filterBtn.dataset.filterKey = col.key;
686
- filterBtn.setAttribute('aria-label', `Filter ${col.label || col.key}`);
687
- const filterIcon = document.createElement('icon-ui');
688
- filterIcon.setAttribute('name', this.#filters.has(col.key) ? 'funnel-simple-fill' : 'funnel-simple');
689
- filterIcon.setAttribute('size', 'xs');
690
- filterBtn.appendChild(filterIcon);
691
- cell.appendChild(filterBtn);
692
-
693
- // Open filter dropdown
694
- if (this.#openFilter === col.key) {
695
- const dropdown = this.#buildFilterDropdown(col);
696
- cell.appendChild(dropdown);
697
- cell.setAttribute('data-filter-open', '');
698
- }
699
- }
700
-
701
- cells.push(cell);
702
- }
703
-
704
- // Reconcile header cells
705
- while (header.children.length > cells.length) header.lastChild.remove();
706
- for (let i = 0; i < cells.length; i++) {
707
- if (header.children[i]) header.replaceChild(cells[i], header.children[i]);
708
- else header.appendChild(cells[i]);
709
- }
710
- }
711
-
712
- // ── Row Builders ───────────────────────────────────────────────────────────
713
-
714
- #createRow(dataIndex, visCols) {
715
- const row = document.createElement('div');
716
- row.setAttribute('role', 'row');
717
- this.#updateRow(row, dataIndex, visCols);
718
- return row;
719
- }
720
-
721
- #updateRow(row, dataIndex, visCols) {
722
- const data = this.#data[dataIndex];
723
- const isSelected = this.#selected.has(dataIndex);
724
- row.dataset.index = dataIndex;
725
-
726
- if (isSelected) {
727
- row.setAttribute('data-selected', '');
728
- row.setAttribute('aria-selected', 'true');
729
- } else {
730
- row.removeAttribute('data-selected');
731
- row.removeAttribute('aria-selected');
732
- }
733
-
734
- const cells = [];
735
-
736
- // Expand toggle cell
737
- if (this.expandable) {
738
- const cell = document.createElement('div');
739
- cell.setAttribute('role', 'gridcell');
740
- cell.setAttribute('data-expand-col', '');
741
- const btn = document.createElement('button');
742
- btn.setAttribute('data-expand-toggle', '');
743
- btn.setAttribute('aria-label', 'Expand row');
744
- const icon = document.createElement('icon-ui');
745
- icon.setAttribute('name', 'caret-right');
746
- icon.setAttribute('size', 'xs');
747
- btn.appendChild(icon);
748
- cell.appendChild(btn);
749
- cells.push(cell);
750
- }
751
-
752
- // Checkbox cell
753
- if (this.selectable) {
754
- const cell = document.createElement('div');
755
- cell.setAttribute('role', 'gridcell');
756
- cell.setAttribute('data-check-col', '');
757
- const check = document.createElement('check-ui');
758
- if (isSelected) check.setAttribute('checked', '');
759
- check.setAttribute('aria-label', `Select row ${dataIndex + 1}`);
760
- cell.appendChild(check);
761
- cells.push(cell);
762
- }
763
-
764
- // Data cells
765
- for (const col of visCols) {
766
- const cell = document.createElement('div');
767
- cell.setAttribute('role', 'gridcell');
768
- cell.dataset.key = col.key;
769
-
770
- const value = getCellValue(data, col);
771
-
772
- // Render priority: column.render > column.format > cellType.render > text
773
- if (typeof col.render === 'function') {
774
- const result = col.render(value, data, cell, dataIndex);
775
- if (result instanceof Node) {
776
- cell.replaceChildren(result);
777
- } else if (typeof result === 'string') {
778
- cell.innerHTML = result;
779
- }
780
- } else if (typeof col.format === 'function') {
781
- cell.textContent = col.format(value, data);
782
- } else {
783
- const typeDef = cellTypes[col.type || 'text'];
784
- if (typeDef?.render) {
785
- typeDef.render(value, data, cell, col.meta);
786
- } else {
787
- cell.textContent = value != null ? value : '';
788
- }
789
- }
790
-
791
- // Alignment from cell type
792
- const typeDef = cellTypes[col.type || 'text'];
793
- if (typeDef?.align) {
794
- cell.dataset.align = typeDef.align;
795
- }
796
-
797
- // Pinned
798
- if (col.pinned) {
799
- cell.setAttribute('data-pinned', col.pinned);
800
- }
801
-
802
- cells.push(cell);
803
- }
804
-
805
- // Reconcile cells in row
806
- while (row.children.length > cells.length) row.lastChild.remove();
807
- for (let i = 0; i < cells.length; i++) {
808
- if (row.children[i]) row.replaceChild(cells[i], row.children[i]);
809
- else row.appendChild(cells[i]);
810
- }
811
- }
812
-
813
- // ── Overlays ───────────────────────────────────────────────────────────────
814
-
815
- #renderOverlays(body) {
816
- let emptyEl = this.querySelector(':scope > [data-empty]');
817
- let loadingEl = this.querySelector(':scope > [data-loading]');
818
-
819
- if (this.loading) {
820
- // Show loading overlay
821
- if (!loadingEl) {
822
- loadingEl = document.createElement('div');
823
- loadingEl.setAttribute('data-loading', '');
824
- const prog = document.createElement('progress-ui');
825
- prog.setAttribute('indeterminate', '');
826
- loadingEl.appendChild(prog);
827
- body.after(loadingEl);
828
- }
829
- if (emptyEl) emptyEl.remove();
830
- } else if (this.#data.length === 0) {
831
- // Show empty state
832
- if (!emptyEl) {
833
- emptyEl = document.createElement('div');
834
- emptyEl.setAttribute('data-empty', '');
835
- const icon = document.createElement('icon-ui');
836
- icon.setAttribute('name', 'table');
837
- const span = document.createElement('span');
838
- span.textContent = 'No data';
839
- emptyEl.appendChild(icon);
840
- emptyEl.appendChild(span);
841
- body.after(emptyEl);
842
- }
843
- if (loadingEl) loadingEl.remove();
844
- } else {
845
- // Remove both overlays
846
- if (emptyEl) emptyEl.remove();
847
- if (loadingEl) loadingEl.remove();
848
- }
849
- }
850
-
851
- // ── Aggregation ────────────────────────────────────────────────────────────
852
-
853
- #renderAggregation(body, processedIndices, visCols) {
854
- const hasAgg = visCols.some(c => c.aggregate);
855
- let aggRow = this.querySelector(':scope > [data-agg-row]');
856
-
857
- if (!hasAgg || this.#data.length === 0) {
858
- if (aggRow) aggRow.remove();
859
- return;
860
- }
861
-
862
- if (!aggRow) {
863
- aggRow = document.createElement('div');
864
- aggRow.setAttribute('role', 'row');
865
- aggRow.setAttribute('data-agg-row', '');
866
- body.after(aggRow);
867
- }
868
-
869
- const cells = [];
870
-
871
- // Expand column spacer
872
- if (this.expandable) {
873
- const spacer = document.createElement('div');
874
- spacer.setAttribute('role', 'gridcell');
875
- cells.push(spacer);
876
- }
877
-
878
- // Checkbox column spacer
879
- if (this.selectable) {
880
- const spacer = document.createElement('div');
881
- spacer.setAttribute('role', 'gridcell');
882
- cells.push(spacer);
883
- }
884
-
885
- for (const col of visCols) {
886
- const cell = document.createElement('div');
887
- cell.setAttribute('role', 'gridcell');
888
-
889
- if (col.aggregate) {
890
- const values = processedIndices.map(i => {
891
- const val = getCellValue(this.#data[i], col);
892
- return val != null ? Number(val) : NaN;
893
- }).filter(n => !isNaN(n));
894
-
895
- let result;
896
- switch (col.aggregate) {
897
- case 'sum': result = values.reduce((a, b) => a + b, 0); break;
898
- case 'avg': result = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; break;
899
- case 'min': result = values.length ? Math.min(...values) : 0; break;
900
- case 'max': result = values.length ? Math.max(...values) : 0; break;
901
- case 'count': result = processedIndices.length; break;
902
- default: result = '';
903
- }
904
-
905
- // Format using cell type if available
906
- const typeDef = cellTypes[col.type || 'text'];
907
- if (typeof result === 'number' && typeDef?.render) {
908
- typeDef.render(result, {}, cell, col.meta);
909
- } else {
910
- cell.textContent = typeof result === 'number' ? result.toLocaleString() : result;
911
- }
912
- }
913
-
914
- cells.push(cell);
915
- }
916
-
917
- // Reconcile cells
918
- while (aggRow.children.length > cells.length) aggRow.lastChild.remove();
919
- for (let i = 0; i < cells.length; i++) {
920
- if (aggRow.children[i]) aggRow.replaceChild(cells[i], aggRow.children[i]);
921
- else aggRow.appendChild(cells[i]);
922
- }
923
- }
924
-
925
- // ── Pagination ─────────────────────────────────────────────────────────────
926
-
927
- #renderPagination(footer, filteredTotal) {
928
- let pag = footer.querySelector('pagination-ui');
929
- if (!pag) {
930
- footer.innerHTML = '';
931
- pag = document.createElement('pagination-ui');
932
- pag.addEventListener('page-change', (e) => {
933
- this.#page = e.detail.page - 1; // pagination-ui is 1-based
934
- this.render();
935
- this.dispatchEvent(new CustomEvent('page', {
936
- bubbles: true,
937
- detail: { page: this.#page },
938
- }));
939
- });
940
- footer.appendChild(pag);
941
- }
942
- const pageCount = this.paginate > 0
943
- ? Math.max(1, Math.ceil(filteredTotal / this.paginate))
944
- : 1;
945
- pag.setAttribute('page', String(this.#page + 1));
946
- pag.setAttribute('total', String(pageCount));
947
- }
948
-
949
- // ── Filter UI ──────────────────────────────────────────────────────────────
950
-
951
- #buildFilterDropdown(col) {
952
- const dropdown = document.createElement('div');
953
- dropdown.setAttribute('data-filter-dropdown', '');
954
-
955
- const currentFilter = this.#filters.get(col.key);
956
-
957
- if (col.filter === 'select') {
958
- // Build checkbox list from unique values
959
- const uniqueVals = [...new Set(this.#data.map(row => {
960
- const val = getCellValue(row, col);
961
- return val != null ? String(val) : '';
962
- }))].filter(Boolean).sort();
963
-
964
- const selectedSet = new Set(
965
- currentFilter?.op === 'select' ? String(currentFilter.value).split(',') : []
966
- );
967
-
968
- for (const val of uniqueVals) {
969
- const label = document.createElement('label');
970
- label.setAttribute('data-filter-option', '');
971
- const check = document.createElement('input');
972
- check.type = 'checkbox';
973
- check.value = val;
974
- check.checked = selectedSet.has(val.toLowerCase());
975
- check.addEventListener('change', () => {
976
- const checked = [...dropdown.querySelectorAll('input[type=checkbox]:checked')].map(c => c.value);
977
- if (checked.length) {
978
- this.setFilter(col.key, checked.join(','), 'select');
979
- } else {
980
- this.setFilter(col.key, null);
981
- }
982
- });
983
- label.appendChild(check);
984
- label.appendChild(document.createTextNode(` ${val}`));
985
- dropdown.appendChild(label);
986
- }
987
- } else if (col.filter === 'number') {
988
- const input = document.createElement('input');
989
- input.type = 'number';
990
- input.placeholder = 'Filter...';
991
- input.value = currentFilter?.value ?? '';
992
- input.setAttribute('data-filter-input', '');
993
-
994
- const opSelect = document.createElement('select');
995
- opSelect.setAttribute('data-filter-op', '');
996
- for (const [val, label] of [['gte', '≥'], ['lte', '≤'], ['gt', '>'], ['lt', '<'], ['equals', '=']]) {
997
- const opt = document.createElement('option');
998
- opt.value = val;
999
- opt.textContent = label;
1000
- if (currentFilter?.op === val) opt.selected = true;
1001
- opSelect.appendChild(opt);
1002
- }
1003
-
1004
- const apply = () => {
1005
- if (input.value) this.setFilter(col.key, input.value, opSelect.value);
1006
- else this.setFilter(col.key, null);
1007
- };
1008
- input.addEventListener('input', apply);
1009
- opSelect.addEventListener('change', apply);
1010
-
1011
- dropdown.appendChild(opSelect);
1012
- dropdown.appendChild(input);
1013
- } else {
1014
- // Default: text filter
1015
- const input = document.createElement('input');
1016
- input.type = 'text';
1017
- input.placeholder = `Filter ${col.label || col.key}...`;
1018
- input.value = currentFilter?.value ?? '';
1019
- input.setAttribute('data-filter-input', '');
1020
- input.addEventListener('input', () => {
1021
- if (input.value) this.setFilter(col.key, input.value, 'contains');
1022
- else this.setFilter(col.key, null);
1023
- });
1024
- dropdown.appendChild(input);
1025
- }
1026
-
1027
- // Clear button
1028
- if (currentFilter) {
1029
- const clearBtn = document.createElement('button');
1030
- clearBtn.textContent = 'Clear';
1031
- clearBtn.setAttribute('data-filter-clear', '');
1032
- clearBtn.addEventListener('click', () => {
1033
- this.setFilter(col.key, null);
1034
- this.#openFilter = null;
1035
- this.#requestRender();
1036
- });
1037
- dropdown.appendChild(clearBtn);
1038
- }
1039
-
1040
- // Auto-focus input
1041
- if (this.#filterFocusRaf != null) cancelAnimationFrame(this.#filterFocusRaf);
1042
- this.#filterFocusRaf = requestAnimationFrame(() => {
1043
- this.#filterFocusRaf = null;
1044
- if (!this.isConnected) return;
1045
- const input = dropdown.querySelector('input');
1046
- if (input) input.focus();
1047
- });
1048
-
1049
- return dropdown;
1050
- }
1051
-
1052
- #renderFilterChips() {
1053
- let bar = this.querySelector(':scope > [data-filter-bar]');
1054
-
1055
- if (this.#filters.size === 0) {
1056
- if (bar) bar.remove();
1057
- return;
1058
- }
1059
-
1060
- if (!bar) {
1061
- bar = document.createElement('div');
1062
- bar.setAttribute('data-filter-bar', '');
1063
- bar.setAttribute('role', 'status');
1064
- // Insert after header
1065
- const header = this.querySelector(':scope > [data-header]');
1066
- if (header) header.after(bar);
1067
- else this.prepend(bar);
1068
- }
1069
-
1070
- bar.innerHTML = '';
1071
- for (const [key, { op, value }] of this.#filters) {
1072
- const col = this.#columns.find(c => c.key === key);
1073
- const label = col?.label || key;
1074
- const chip = document.createElement('badge-ui');
1075
- const displayVal = op === 'select' ? `${value.split(',').length} selected` : value;
1076
- chip.setAttribute('text', `${label}: ${displayVal}`);
1077
- chip.setAttribute('size', 'xs');
1078
- chip.setAttribute('data-filter-chip', key);
1079
- // Dismiss button
1080
- const dismiss = document.createElement('button');
1081
- dismiss.textContent = '×';
1082
- dismiss.setAttribute('data-chip-dismiss', '');
1083
- dismiss.addEventListener('click', (e) => {
1084
- e.stopPropagation();
1085
- this.setFilter(key, null);
1086
- });
1087
- chip.appendChild(dismiss);
1088
- bar.appendChild(chip);
1089
- }
1090
-
1091
- // Clear all button
1092
- const clearAll = document.createElement('button');
1093
- clearAll.textContent = 'Clear all';
1094
- clearAll.setAttribute('data-filter-clear-all', '');
1095
- clearAll.addEventListener('click', () => this.clearFilters());
1096
- bar.appendChild(clearAll);
1097
- }
1098
-
1099
- // ── Event Handling: Click ──────────────────────────────────────────────────
1100
-
1101
- #onClick = (e) => {
1102
- const target = e.target;
1103
-
1104
- // ── Filter button click ──
1105
- const filterBtn = target.closest('[data-filter-btn]');
1106
- if (filterBtn && this.contains(filterBtn)) {
1107
- const key = filterBtn.dataset.filterKey;
1108
- this.#openFilter = this.#openFilter === key ? null : key;
1109
- this.#requestRender();
1110
- return;
1111
- }
1112
-
1113
- // ── Click outside filter dropdown closes it ──
1114
- if (this.#openFilter && !target.closest('[data-filter-dropdown]') && !target.closest('[data-filter-btn]')) {
1115
- this.#openFilter = null;
1116
- this.#requestRender();
1117
- }
1118
-
1119
- // ── Expand toggle click ──
1120
- const expandToggle = target.closest('[data-expand-toggle]');
1121
- if (expandToggle && this.expandable && this.contains(expandToggle)) {
1122
- const row = expandToggle.closest('[role="row"]');
1123
- if (row?.dataset?.index != null) {
1124
- this.toggleExpand(Number(row.dataset.index));
1125
- }
1126
- return;
1127
- }
1128
-
1129
- // ── Resize handle mousedown is handled separately ──
1130
- const resizeHandle = target.closest('[data-resize-handle]');
1131
- if (resizeHandle && this.contains(resizeHandle)) {
1132
- this.#startResize(e, resizeHandle);
1133
- return;
1134
- }
1135
-
1136
- // ── Header sort click ──
1137
- const headerCell = target.closest('[data-sort-key]');
1138
- if (headerCell && this.sortable && this.contains(headerCell)) {
1139
- const key = headerCell.dataset.sortKey;
1140
- const existing = this.#sortState.findIndex(s => s.key === key);
1141
- const col = this.#columns.find(c => c.key === key);
1142
- const defaultDir = col?.sortDescFirst ? 'desc' : 'asc';
1143
- const altDir = defaultDir === 'asc' ? 'desc' : 'asc';
1144
-
1145
- if (e.shiftKey) {
1146
- // Multi-sort: add or toggle
1147
- if (existing >= 0) {
1148
- const cur = this.#sortState[existing];
1149
- if (cur.dir === altDir) {
1150
- // Remove from sort state (third click)
1151
- this.#sortState.splice(existing, 1);
1152
- } else {
1153
- cur.dir = cur.dir === defaultDir ? altDir : defaultDir;
1154
- }
1155
- } else {
1156
- this.#sortState.push({ key, dir: defaultDir });
1157
- }
1158
- } else {
1159
- // Single sort: replace
1160
- if (existing >= 0 && this.#sortState.length === 1) {
1161
- const cur = this.#sortState[0];
1162
- if (cur.dir === altDir) {
1163
- this.#sortState = [];
1164
- } else {
1165
- cur.dir = cur.dir === defaultDir ? altDir : defaultDir;
1166
- }
1167
- } else {
1168
- this.#sortState = [{ key, dir: defaultDir }];
1169
- }
1170
- }
1171
-
1172
- this.render();
1173
- this.dispatchEvent(new CustomEvent('sort', {
1174
- bubbles: true,
1175
- detail: {
1176
- key,
1177
- dir: this.#sortState.find(s => s.key === key)?.dir || null,
1178
- sortState: this.sortState,
1179
- },
1180
- }));
1181
- return;
1182
- }
1183
-
1184
- // ── Header checkbox (select all) ──
1185
- const headerCheck = target.closest('[data-header] check-ui');
1186
- if (headerCheck && this.selectable) {
1187
- const checked = headerCheck.hasAttribute('checked');
1188
- if (checked) {
1189
- for (let i = 0; i < this.#data.length; i++) this.#selected.add(i);
1190
- } else {
1191
- this.#selected.clear();
1192
- }
1193
- this.render();
1194
- this.dispatchEvent(new CustomEvent('select', {
1195
- bubbles: true, detail: { selected: this.selected },
1196
- }));
1197
- return;
1198
- }
1199
-
1200
- // ── Row checkbox ──
1201
- const rowCheck = target.closest('[role="row"]:not([data-header]) check-ui');
1202
- if (rowCheck && this.selectable) {
1203
- const row = rowCheck.closest('[role="row"]');
1204
- const idx = row ? +row.dataset.index : -1;
1205
- if (idx >= 0) {
1206
- const checked = rowCheck.hasAttribute('checked');
1207
-
1208
- if (e.shiftKey && this.#lastSelectedIndex >= 0) {
1209
- // Range select
1210
- const from = Math.min(this.#lastSelectedIndex, idx);
1211
- const to = Math.max(this.#lastSelectedIndex, idx);
1212
- for (let i = from; i <= to; i++) {
1213
- if (checked) this.#selected.add(i);
1214
- else this.#selected.delete(i);
1215
- }
1216
- } else {
1217
- if (checked) this.#selected.add(idx);
1218
- else this.#selected.delete(idx);
1219
- }
1220
-
1221
- this.#lastSelectedIndex = idx;
1222
- this.render();
1223
- this.dispatchEvent(new CustomEvent('select', {
1224
- bubbles: true, detail: { selected: this.selected },
1225
- }));
1226
- }
1227
- return;
1228
- }
1229
-
1230
- // ── Cell click ──
1231
- const gridcell = target.closest('[role="gridcell"][data-key]');
1232
- if (gridcell && this.contains(gridcell)) {
1233
- const row = gridcell.closest('[role="row"]');
1234
- const dataIndex = row ? +row.dataset.index : -1;
1235
- if (dataIndex >= 0) {
1236
- const key = gridcell.dataset.key;
1237
- const rowData = this.#data[dataIndex];
1238
- const col = this.#columns.find(c => c.key === key);
1239
- const value = col ? getCellValue(rowData, col) : undefined;
1240
- this.dispatchEvent(new CustomEvent('cell-click', {
1241
- bubbles: true,
1242
- detail: { key, row: rowData, value, dataIndex },
1243
- }));
1244
- }
1245
- }
1246
- };
1247
-
1248
- // ── Column Resize ──────────────────────────────────────────────────────────
1249
-
1250
- #resizeState = null;
1251
-
1252
- #startResize(e, handle) {
1253
- e.preventDefault();
1254
- e.stopPropagation();
1255
-
1256
- const key = handle.dataset.resizeKey;
1257
- const col = this.#columns.find(c => c.key === key);
1258
- if (!col) return;
1259
-
1260
- // Get current column width
1261
- const headerCell = handle.closest('[role="columnheader"]');
1262
- const startWidth = headerCell ? headerCell.getBoundingClientRect().width : 100;
1263
- const startX = e.clientX;
1264
-
1265
- this.setAttribute('data-resizing', '');
1266
-
1267
- this.#resizeState = { key, col, startX, startWidth };
1268
-
1269
- document.addEventListener('mousemove', this.#onResizeMove);
1270
- document.addEventListener('mouseup', this.#onResizeEnd);
1271
- }
1272
-
1273
- #onResizeMove = (e) => {
1274
- if (!this.#resizeState) return;
1275
- const { key, col, startX, startWidth } = this.#resizeState;
1276
- const delta = e.clientX - startX;
1277
- let newWidth = startWidth + delta;
1278
-
1279
- // Clamp to min/max
1280
- const min = col.minWidth || 48;
1281
- const max = col.maxWidth || Infinity;
1282
- newWidth = Math.max(min, Math.min(max, newWidth));
1283
-
1284
- this.#columnWidths.set(key, Math.round(newWidth));
1285
- this.style.gridTemplateColumns = this.#buildGridTemplate();
1286
- };
1287
-
1288
- #onResizeEnd = (e) => {
1289
- if (!this.#resizeState) return;
1290
- const { key } = this.#resizeState;
1291
-
1292
- document.removeEventListener('mousemove', this.#onResizeMove);
1293
- document.removeEventListener('mouseup', this.#onResizeEnd);
1294
-
1295
- this.removeAttribute('data-resizing');
1296
-
1297
- const width = this.#columnWidths.get(key);
1298
- this.#resizeState = null;
1299
-
1300
- this.dispatchEvent(new CustomEvent('resize', {
1301
- bubbles: true,
1302
- detail: { key, width },
1303
- }));
1304
-
1305
- this.render();
1306
- };
1307
-
1308
- #cleanupResize() {
1309
- document.removeEventListener('mousemove', this.#onResizeMove);
1310
- document.removeEventListener('mouseup', this.#onResizeEnd);
1311
- this.#resizeState = null;
1312
- }
1313
-
1314
- // ── Keyboard Navigation ────────────────────────────────────────────────────
1315
-
1316
- #onKeydown = (e) => {
1317
- const visCols = this.#visibleColumns;
1318
- const totalCols = visCols.length + (this.selectable ? 1 : 0);
1319
- const body = this.querySelector(':scope > [data-body]');
1320
- const totalRows = body ? body.children.length : 0;
1321
-
1322
- // Include header as row -1 conceptually, body rows 0..n-1
1323
- if (!this.#focusedCell) {
1324
- this.#focusedCell = { row: 0, col: 0 };
1325
- }
1326
-
1327
- let { row, col } = this.#focusedCell;
1328
- let handled = true;
1329
-
1330
- switch (e.key) {
1331
- case 'ArrowDown':
1332
- row = Math.min(row + 1, totalRows - 1);
1333
- break;
1334
- case 'ArrowUp':
1335
- row = Math.max(row - 1, -1); // -1 = header
1336
- break;
1337
- case 'ArrowRight':
1338
- col = Math.min(col + 1, totalCols - 1);
1339
- break;
1340
- case 'ArrowLeft':
1341
- col = Math.max(col - 1, 0);
1342
- break;
1343
- case 'Tab':
1344
- if (e.shiftKey) {
1345
- col--;
1346
- if (col < 0) { col = totalCols - 1; row--; }
1347
- } else {
1348
- col++;
1349
- if (col >= totalCols) { col = 0; row++; }
1350
- }
1351
- if (row < -1 || row >= totalRows) { handled = false; break; }
1352
- break;
1353
- case 'Enter':
1354
- // If on header row, trigger sort
1355
- if (row === -1) {
1356
- const header = this.querySelector(':scope > [data-header]');
1357
- const cell = header?.children[col];
1358
- const sortKey = cell?.dataset.sortKey;
1359
- if (sortKey) cell.click();
1360
- }
1361
- break;
1362
- default:
1363
- handled = false;
1364
- }
1365
-
1366
- if (handled) {
1367
- e.preventDefault();
1368
- this.#focusedCell = { row, col };
1369
- this.#updateFocus();
1370
- }
1371
- };
1372
-
1373
- #updateFocus() {
1374
- // Remove old focus
1375
- const oldFocused = this.querySelector('[data-focused]');
1376
- if (oldFocused) oldFocused.removeAttribute('data-focused');
1377
-
1378
- if (!this.#focusedCell) return;
1379
- const { row, col } = this.#focusedCell;
1380
-
1381
- let cell;
1382
- if (row === -1) {
1383
- // Header
1384
- const header = this.querySelector(':scope > [data-header]');
1385
- cell = header?.children[col];
1386
- } else {
1387
- // Body
1388
- const body = this.querySelector(':scope > [data-body]');
1389
- const rowEl = body?.children[row];
1390
- cell = rowEl?.children[col];
1391
- }
1392
-
1393
- if (cell) {
1394
- cell.setAttribute('data-focused', '');
1395
- cell.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
1396
- }
1397
- }
1398
-
1399
- // ── CSV Export ─────────────────────────────────────────────────────────────
1400
-
1401
- exportCSV(filename = 'export.csv') {
1402
- const visCols = this.#visibleColumns;
1403
- const processed = this.#getProcessedIndices();
1404
-
1405
- // Header row
1406
- const headerRow = visCols.map(c => csvEscape(c.label || c.key));
1407
-
1408
- // Data rows
1409
- const dataRows = processed.map(idx => {
1410
- const row = this.#data[idx];
1411
- return visCols.map(col => {
1412
- const value = getCellValue(row, col);
1413
-
1414
- // Use column format, then cell type format, then String
1415
- if (typeof col.format === 'function') {
1416
- return csvEscape(col.format(value, row));
1417
- }
1418
- const typeDef = cellTypes[col.type || 'text'];
1419
- if (typeDef?.format) {
1420
- return csvEscape(typeDef.format(value, row, col.meta));
1421
- }
1422
- return csvEscape(value);
1423
- });
1424
- });
1425
-
1426
- const csv = [headerRow.join(','), ...dataRows.map(r => r.join(','))].join('\n');
1427
11
 
1428
- // Trigger download
1429
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
1430
- const url = URL.createObjectURL(blob);
1431
- const link = document.createElement('a');
1432
- link.href = url;
1433
- link.download = filename;
1434
- link.style.display = 'none';
1435
- document.body.appendChild(link);
1436
- link.click();
1437
- document.body.removeChild(link);
1438
- URL.revokeObjectURL(url);
1439
- }
1440
- }
12
+ import { defineIfFree } from '../../core/register.js';
13
+ import { UITable } from './class.js';
1441
14
 
1442
- customElements.define('table-ui', UITable);
15
+ defineIfFree('table-ui', UITable);
1443
16
 
1444
17
  export { UITable };