@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
@@ -8,6 +8,14 @@ category: agent
8
8
  version: 1
9
9
  description: Data table with sorting, selection, pagination, search, column resize, keyboard nav,
10
10
  cell types, and CSV export. CSS grid + ARIA grid roles.
11
+ # Per ADR-0027 — primitives that programmatically create other primitives
12
+ # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
13
+ composes:
14
+ - check-ui
15
+ - icon-ui
16
+ - progress-ui
17
+ - pagination-ui
18
+ - badge-ui
11
19
  props:
12
20
  columns:
13
21
  description: Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, accessor?, format?, render?, sortFn?, filterType?, meta?}. Alternative to declarative <col-def> children.
@@ -0,0 +1,680 @@
1
+ /**
2
+ * Non-side-effect class export for `<table-toolbar-ui>`.
3
+ *
4
+ * Importing this file gives you the class(es) without auto-registering the tag.
5
+ * Useful for test isolation, subclassing with tag-name override, or selective
6
+ * composition.
7
+ *
8
+ * The auto-register path stays at `@adia-ai/web-components/components/table-toolbar`
9
+ * (which imports this file + calls `defineIfFree()`).
10
+ *
11
+ * @see ../../USAGE.md#registration--auto-vs-explicit
12
+ */
13
+
14
+ /**
15
+ * <table-toolbar-ui for="emps" text="All Employees" count="32"></table-toolbar-ui>
16
+ *
17
+ * Companion / header bar for a sibling table-ui. Renders:
18
+ * • title + optional count badge
19
+ * • filter / sort / columns popover buttons
20
+ * • search input
21
+ * • optional [slot="actions"] trailing region
22
+ *
23
+ * [for] resolution mirrors chart-legend-ui — the toolbar mounts an element by
24
+ * id, then dispatches state changes against it. When [for] is absent, falls
25
+ * back to the first table-ui sibling under the same parent.
26
+ *
27
+ * Popovers use the platform Popover API + core/anchor.js, the same primitives
28
+ * that menu-ui / popover-ui / toolbar-ui already use in this package.
29
+ *
30
+ * State flow:
31
+ * search → table.search (string property)
32
+ * filters → table.setFilter() (per-key)
33
+ * sort → simulated click on table's [data-sort-key] header
34
+ * column hidden → table.columns = (clone with hidden flag flipped)
35
+ */
36
+
37
+ import { UIElement } from '../../core/element.js';
38
+ import { anchorPopover } from '../../core/anchor.js';
39
+
40
+ const SEARCH_DEBOUNCE = 200;
41
+
42
+ function emptyHint(text) {
43
+ const el = document.createElement('text-ui');
44
+ el.setAttribute('data-popover-empty', '');
45
+ el.setAttribute('color', 'subtle');
46
+ el.setAttribute('variant', 'caption');
47
+ el.textContent = text;
48
+ return el;
49
+ }
50
+
51
+ function popoverHead(text) {
52
+ const el = document.createElement('text-ui');
53
+ el.setAttribute('data-popover-head', '');
54
+ el.setAttribute('variant', 'kicker');
55
+ el.setAttribute('color', 'subtle');
56
+ el.textContent = text;
57
+ return el;
58
+ }
59
+
60
+ const SELECT_THRESHOLD = 50;
61
+ const SAMPLE_LIMIT = 500;
62
+
63
+ function getCellValue(row, col) {
64
+ if (typeof col?.accessor === 'function') return col.accessor(row);
65
+ const path = col?.key;
66
+ if (!path || row == null) return undefined;
67
+ const parts = String(path).split('.');
68
+ let cur = row;
69
+ for (const p of parts) {
70
+ if (cur == null) return undefined;
71
+ cur = cur[p];
72
+ }
73
+ return cur;
74
+ }
75
+
76
+ function looksLikeIdKey(key) {
77
+ if (!key) return false;
78
+ const k = String(key);
79
+ if (k === 'id' || k === 'ID' || k === 'uuid' || k === 'guid') return true;
80
+ if (/[a-z](Id|ID)$/.test(k)) return true;
81
+ if (/_id$/i.test(k)) return true;
82
+ return false;
83
+ }
84
+
85
+ function detectFilterShape(col, data) {
86
+ if (col?.filter === 'select' || col?.filter === 'text') return col.filter;
87
+ if (looksLikeIdKey(col?.key)) return 'text';
88
+
89
+ const values = [];
90
+ const limit = Math.min(data?.length ?? 0, SAMPLE_LIMIT);
91
+ const unique = new Set();
92
+ for (let i = 0; i < limit; i++) {
93
+ const v = getCellValue(data[i], col);
94
+ if (v == null || v === '') continue;
95
+ values.push(v);
96
+ unique.add(String(v));
97
+ if (unique.size > SELECT_THRESHOLD) return 'text';
98
+ }
99
+ if (!values.length) return 'text';
100
+ return 'select';
101
+ }
102
+
103
+ function collectUniqueValues(col, data) {
104
+ const set = new Set();
105
+ const limit = Math.min(data?.length ?? 0, SAMPLE_LIMIT);
106
+ for (let i = 0; i < limit; i++) {
107
+ const v = getCellValue(data[i], col);
108
+ if (v == null || v === '') continue;
109
+ set.add(String(v));
110
+ }
111
+ return [...set].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
112
+ }
113
+
114
+ export class UITableToolbar extends UIElement {
115
+ static properties = {
116
+ for: { type: String, default: '', reflect: true },
117
+ text: { type: String, default: '', reflect: false },
118
+ count: { type: String, default: '', reflect: false },
119
+ noFilter: { type: Boolean, default: false, reflect: true, attribute: 'no-filter' },
120
+ noSort: { type: Boolean, default: false, reflect: true, attribute: 'no-sort' },
121
+ noColumns: { type: Boolean, default: false, reflect: true, attribute: 'no-columns' },
122
+ noSearch: { type: Boolean, default: false, reflect: true, attribute: 'no-search' },
123
+ placeholder: { type: String, default: 'Search...', reflect: false },
124
+ variant: { type: String, default: 'default', reflect: true },
125
+ };
126
+
127
+ static template = () => null;
128
+
129
+ #target = null;
130
+ #targetListeners = [];
131
+ #activePopover = null; // { btn, panel, cleanup }
132
+ #docListenersBound = false;
133
+ #docListenerRaf = null;
134
+ #sortIndicatorRafs = new Set();
135
+
136
+ // ── Lifecycle ────────────────────────────────────────────────────────────
137
+
138
+ connected() {
139
+ this.setAttribute('role', 'toolbar');
140
+ this.#stamp();
141
+ this.#resolveTarget();
142
+ this.#syncFromTarget();
143
+ }
144
+
145
+ disconnected() {
146
+ if (this.#docListenerRaf != null) {
147
+ cancelAnimationFrame(this.#docListenerRaf);
148
+ this.#docListenerRaf = null;
149
+ }
150
+ for (const id of this.#sortIndicatorRafs) cancelAnimationFrame(id);
151
+ this.#sortIndicatorRafs.clear();
152
+ this.#closePopover();
153
+ this.#detachTarget();
154
+ }
155
+
156
+ render() {
157
+ // [for] / count / text changes come through here.
158
+ this.#resolveTarget();
159
+ this.#syncFromTarget();
160
+ this.#updateTitle();
161
+ }
162
+
163
+ // ── Target resolution ────────────────────────────────────────────────────
164
+
165
+ #resolveTarget() {
166
+ const next = this.#findTarget();
167
+ if (next === this.#target) return;
168
+ this.#detachTarget();
169
+ if (!next) return;
170
+ this.#target = next;
171
+
172
+ const onSort = () => this.#refreshSortPanel();
173
+ const onFilter = () => this.#refreshFilterPanel();
174
+ next.addEventListener('sort', onSort);
175
+ next.addEventListener('filter-change', onFilter);
176
+ this.#targetListeners.push(['sort', onSort], ['filter-change', onFilter]);
177
+ }
178
+
179
+ #findTarget() {
180
+ if (this.for) {
181
+ const root = this.getRootNode?.();
182
+ const byId = root?.getElementById?.(this.for) || document.getElementById(this.for);
183
+ if (byId && byId.tagName?.toLowerCase() === 'table-ui') return byId;
184
+ return null;
185
+ }
186
+ // Fallback — first sibling table-ui in the same parent.
187
+ const parent = this.parentElement;
188
+ if (!parent) return null;
189
+ return parent.querySelector(':scope table-ui') || null;
190
+ }
191
+
192
+ #detachTarget() {
193
+ if (this.#target) {
194
+ for (const [evt, fn] of this.#targetListeners) {
195
+ this.#target.removeEventListener(evt, fn);
196
+ }
197
+ }
198
+ this.#targetListeners = [];
199
+ this.#target = null;
200
+ }
201
+
202
+ // ── DOM stamp ────────────────────────────────────────────────────────────
203
+
204
+ #stamp() {
205
+ if (this.querySelector(':scope > [data-toolbar]')) return;
206
+
207
+ const root = document.createElement('div');
208
+ root.setAttribute('data-toolbar', '');
209
+
210
+ // Title cluster
211
+ const title = document.createElement('div');
212
+ title.setAttribute('data-title', '');
213
+
214
+ const heading = document.createElement('span');
215
+ heading.setAttribute('data-heading', '');
216
+ title.appendChild(heading);
217
+
218
+ const badge = document.createElement('badge-ui');
219
+ badge.setAttribute('data-count-badge', '');
220
+ badge.setAttribute('size', 'sm');
221
+ badge.setAttribute('variant', 'muted');
222
+ badge.hidden = true;
223
+ title.appendChild(badge);
224
+
225
+ // Controls cluster
226
+ const controls = document.createElement('div');
227
+ controls.setAttribute('data-controls', '');
228
+ controls.appendChild(this.#mkButton('filter', 'Filter', 'funnel-simple'));
229
+ controls.appendChild(this.#mkButton('sort', 'Sort', 'arrows-down-up'));
230
+ controls.appendChild(this.#mkButton('columns', 'Columns', 'columns'));
231
+
232
+ // Search — compose <search-ui>, which already stamps input-ui with
233
+ // the magnifying-glass prefix + clear suffix and debounces a `search`
234
+ // event. Rolling our own from input-ui would re-derive that wiring
235
+ // and (as discovered) hit a first-paint timing race where the icon
236
+ // name renders as literal text before the icon registry resolves.
237
+ const search = document.createElement('search-ui');
238
+ search.setAttribute('data-search', '');
239
+ search.setAttribute('placeholder', this.placeholder);
240
+ search.setAttribute('debounce', String(SEARCH_DEBOUNCE));
241
+ search.addEventListener('search', this.#onSearch);
242
+
243
+ // Actions slot passthrough — we move any pre-existing [slot="actions"] children here
244
+ const actionsSlot = document.createElement('div');
245
+ actionsSlot.setAttribute('data-actions', '');
246
+ for (const node of [...this.children]) {
247
+ if (node === root) continue;
248
+ if (node.getAttribute?.('slot') === 'actions') {
249
+ actionsSlot.appendChild(node);
250
+ }
251
+ }
252
+
253
+ root.appendChild(title);
254
+ root.appendChild(controls);
255
+ root.appendChild(search);
256
+ root.appendChild(actionsSlot);
257
+
258
+ this.appendChild(root);
259
+ this.#updateTitle();
260
+ this.#updateControlVisibility();
261
+ }
262
+
263
+ #mkPopoverAction(label, onClick) {
264
+ const btn = document.createElement('button-ui');
265
+ btn.setAttribute('data-popover-action', '');
266
+ btn.setAttribute('text', label);
267
+ btn.setAttribute('variant', 'ghost');
268
+ btn.setAttribute('size', 'sm');
269
+ btn.setAttribute('stretch', '');
270
+ btn.addEventListener('click', onClick);
271
+ return btn;
272
+ }
273
+
274
+ #mkButton(kind, label, icon) {
275
+ const btn = document.createElement('button-ui');
276
+ btn.setAttribute('data-toolbar-btn', kind);
277
+ btn.setAttribute('icon', icon);
278
+ btn.setAttribute('text', label);
279
+ btn.setAttribute('variant', 'outline');
280
+ btn.setAttribute('size', 'sm');
281
+ btn.setAttribute('aria-haspopup', 'menu');
282
+ btn.addEventListener('click', (e) => {
283
+ e.stopPropagation();
284
+ this.#togglePopover(kind, btn);
285
+ });
286
+ return btn;
287
+ }
288
+
289
+ #updateTitle() {
290
+ const heading = this.querySelector(':scope [data-heading]');
291
+ if (!heading) return;
292
+ if (this.text) {
293
+ heading.textContent = this.text;
294
+ heading.hidden = false;
295
+ } else if (heading.textContent.trim()) {
296
+ heading.hidden = false;
297
+ } else {
298
+ heading.hidden = true;
299
+ }
300
+
301
+ const badge = this.querySelector(':scope [data-count-badge]');
302
+ if (!badge) return;
303
+ const explicit = this.count?.toString().trim();
304
+ const fallback = this.#target?.data?.length;
305
+ const value = explicit || (Number.isFinite(fallback) ? String(fallback) : '');
306
+ if (value) {
307
+ badge.setAttribute('text', value);
308
+ badge.hidden = false;
309
+ } else {
310
+ badge.hidden = true;
311
+ }
312
+ }
313
+
314
+ #updateControlVisibility() {
315
+ const root = this.querySelector(':scope > [data-toolbar]');
316
+ if (!root) return;
317
+
318
+ const setHidden = (sel, hidden) => {
319
+ const el = root.querySelector(sel);
320
+ if (el) el.hidden = hidden;
321
+ };
322
+
323
+ setHidden('[data-toolbar-btn="filter"]', this.noFilter);
324
+ setHidden('[data-toolbar-btn="sort"]', this.noSort);
325
+ setHidden('[data-toolbar-btn="columns"]', this.noColumns);
326
+ setHidden('[data-search]', this.noSearch);
327
+ }
328
+
329
+ // Re-run on attribute changes for boolean flags.
330
+ attributeChanged(name) {
331
+ if (['no-filter', 'no-sort', 'no-columns', 'no-search'].includes(name)) {
332
+ this.#updateControlVisibility();
333
+ }
334
+ if (name === 'placeholder') {
335
+ const search = this.querySelector(':scope [data-search]');
336
+ search?.setAttribute('placeholder', this.placeholder);
337
+ }
338
+ }
339
+
340
+ // ── Search ───────────────────────────────────────────────────────────────
341
+
342
+ #onSearch = (e) => {
343
+ const value = e.detail?.value ?? '';
344
+ if (this.#target) this.#target.search = value;
345
+ this.dispatchEvent(new CustomEvent('search', {
346
+ bubbles: true,
347
+ detail: { value },
348
+ }));
349
+ };
350
+
351
+ // ── Sync from target (initial paint) ─────────────────────────────────────
352
+
353
+ #syncFromTarget() {
354
+ if (!this.#target) return;
355
+ const search = this.querySelector(':scope [data-search]');
356
+ if (search && this.#target.search) {
357
+ search.value = this.#target.search;
358
+ }
359
+ this.#updateTitle();
360
+ }
361
+
362
+ // ── Popovers ─────────────────────────────────────────────────────────────
363
+
364
+ #togglePopover(kind, btn) {
365
+ if (this.#activePopover?.kind === kind) {
366
+ this.#closePopover();
367
+ return;
368
+ }
369
+ this.#closePopover();
370
+
371
+ const panel = document.createElement('div');
372
+ panel.setAttribute('data-toolbar-popover', kind);
373
+ panel.setAttribute('popover', 'manual');
374
+ panel.setAttribute('role', 'menu');
375
+
376
+ if (kind === 'filter') this.#fillFilterPanel(panel);
377
+ if (kind === 'sort') this.#fillSortPanel(panel);
378
+ if (kind === 'columns') this.#fillColumnsPanel(panel);
379
+
380
+ document.body.appendChild(panel);
381
+
382
+ try { panel.showPopover(); } catch { /* popover API unavailable */ }
383
+ const cleanup = anchorPopover(btn, panel, { placement: 'bottom-start', gap: 4 });
384
+
385
+ this.#activePopover = { kind, btn, panel, cleanup };
386
+
387
+ if (!this.#docListenersBound) {
388
+ this.#docListenersBound = true;
389
+ this.#docListenerRaf = requestAnimationFrame(() => {
390
+ this.#docListenerRaf = null;
391
+ if (!this.isConnected || !this.#docListenersBound) return;
392
+ document.addEventListener('pointerdown', this.#onDocDown, true);
393
+ document.addEventListener('keydown', this.#onDocKey, true);
394
+ });
395
+ }
396
+ }
397
+
398
+ #closePopover() {
399
+ const ap = this.#activePopover;
400
+ if (!ap) return;
401
+ ap.cleanup?.();
402
+ if (ap.panel?.matches?.(':popover-open')) {
403
+ try { ap.panel.hidePopover(); } catch { /* noop */ }
404
+ }
405
+ ap.panel?.remove();
406
+ this.#activePopover = null;
407
+ if (this.#docListenersBound) {
408
+ this.#docListenersBound = false;
409
+ document.removeEventListener('pointerdown', this.#onDocDown, true);
410
+ document.removeEventListener('keydown', this.#onDocKey, true);
411
+ }
412
+ }
413
+
414
+ #onDocDown = (e) => {
415
+ const ap = this.#activePopover;
416
+ if (!ap) return;
417
+ if (ap.btn.contains(e.target)) return;
418
+ if (ap.panel.contains(e.target)) return;
419
+ this.#closePopover();
420
+ };
421
+
422
+ #onDocKey = (e) => {
423
+ if (e.key !== 'Escape') return;
424
+ e.stopPropagation();
425
+ this.#closePopover();
426
+ this.#activePopover?.btn?.focus?.({ preventScroll: true });
427
+ };
428
+
429
+ // ── Filter panel ─────────────────────────────────────────────────────────
430
+
431
+ #fillFilterPanel(panel) {
432
+ const target = this.#target;
433
+ if (!target?.columns?.length) {
434
+ panel.appendChild(emptyHint('No filterable columns'));
435
+ return;
436
+ }
437
+ const filters = target.filters || {};
438
+
439
+ panel.appendChild(popoverHead('Filter rows'));
440
+
441
+ const list = document.createElement('div');
442
+ list.setAttribute('data-popover-list', '');
443
+
444
+ const data = target.data || [];
445
+ for (const col of target.columns) {
446
+ if (col.hidden) continue;
447
+ // <field-ui inline label="…"><…control…></…></field-ui> — canonical
448
+ // label-binds-to-control pair, mints id + [for]. The CONTROL is
449
+ // chosen per-column from the column's filter shape:
450
+ // • 'select' → <select-ui multiple searchable> populated from data
451
+ // • 'text' → <input-ui type="text"> (contains match)
452
+ // Shape comes from col.filter when set, otherwise auto-detected
453
+ // from the data (id-like keys → text; ≤ 50 distinct values → select).
454
+ const row = document.createElement('field-ui');
455
+ row.setAttribute('data-filter-row', '');
456
+ row.setAttribute('inline', '');
457
+ row.setAttribute('label', col.label || col.key);
458
+
459
+ const shape = detectFilterShape(col, data);
460
+ const current = filters[col.key];
461
+
462
+ if (shape === 'select') {
463
+ const sel = document.createElement('select-ui');
464
+ sel.setAttribute('data-filter-input', '');
465
+ sel.setAttribute('multiple', '');
466
+ sel.setAttribute('placeholder', '—');
467
+
468
+ const uniqueValues = collectUniqueValues(col, data);
469
+ // Searchable kicks in at 12+ options — below that the listbox is
470
+ // fully scannable and a search field is overhead.
471
+ if (uniqueValues.length >= 12) sel.setAttribute('searchable', '');
472
+
473
+ for (const val of uniqueValues) {
474
+ const opt = document.createElement('option');
475
+ opt.setAttribute('value', val);
476
+ opt.textContent = val;
477
+ sel.appendChild(opt);
478
+ }
479
+ if (current?.op === 'select' && current.value) {
480
+ sel.value = current.value;
481
+ }
482
+ sel.addEventListener('change', () => {
483
+ const v = sel.value || '';
484
+ if (v) target.setFilter(col.key, v, 'select');
485
+ else target.setFilter(col.key, null);
486
+ this.dispatchEvent(new CustomEvent('filter-change', {
487
+ bubbles: true,
488
+ detail: { filters: target.filters },
489
+ }));
490
+ });
491
+ row.appendChild(sel);
492
+ } else {
493
+ const input = document.createElement('input-ui');
494
+ input.setAttribute('type', 'text');
495
+ input.setAttribute('size', 'sm');
496
+ input.setAttribute('data-filter-input', '');
497
+ input.setAttribute('placeholder', '—');
498
+ if (current?.op === 'contains') input.value = current.value ?? '';
499
+ input.addEventListener('input', () => {
500
+ const v = input.value;
501
+ if (v) target.setFilter(col.key, v, 'contains');
502
+ else target.setFilter(col.key, null);
503
+ this.dispatchEvent(new CustomEvent('filter-change', {
504
+ bubbles: true,
505
+ detail: { filters: target.filters },
506
+ }));
507
+ });
508
+ row.appendChild(input);
509
+ }
510
+
511
+ list.appendChild(row);
512
+ }
513
+
514
+ panel.appendChild(list);
515
+
516
+ if (Object.keys(filters).length) {
517
+ const clear = this.#mkPopoverAction('Clear all filters', () => {
518
+ target.clearFilters();
519
+ this.dispatchEvent(new CustomEvent('filter-change', {
520
+ bubbles: true,
521
+ detail: { filters: {} },
522
+ }));
523
+ this.#refreshFilterPanel();
524
+ });
525
+ panel.appendChild(clear);
526
+ }
527
+ }
528
+
529
+ #refreshFilterPanel() {
530
+ const ap = this.#activePopover;
531
+ if (!ap || ap.kind !== 'filter') return;
532
+ ap.panel.replaceChildren();
533
+ this.#fillFilterPanel(ap.panel);
534
+ }
535
+
536
+ // ── Sort panel ───────────────────────────────────────────────────────────
537
+
538
+ #fillSortPanel(panel) {
539
+ const target = this.#target;
540
+ if (!target?.columns?.length) {
541
+ panel.appendChild(emptyHint('No sortable columns'));
542
+ return;
543
+ }
544
+
545
+ panel.appendChild(popoverHead('Sort by'));
546
+
547
+ const sortState = target.sortState || [];
548
+ const dirByKey = new Map(sortState.map((s) => [s.key, s.dir]));
549
+
550
+ const list = document.createElement('div');
551
+ list.setAttribute('data-popover-list', '');
552
+
553
+ for (const col of target.columns) {
554
+ if (col.hidden) continue;
555
+ if (col.sortable === false) continue;
556
+
557
+ // <menu-item-ui> is the canonical action-row primitive (role="menuitem",
558
+ // built-in icon + text slots, hover/focus tokens, danger variant). Append
559
+ // a trailing <icon-ui> after the auto-stamped [slot="text"] — its flex:1
560
+ // pushes any later child to the trailing edge.
561
+ const dir = dirByKey.get(col.key);
562
+ const row = document.createElement('menu-item-ui');
563
+ row.setAttribute('data-sort-row', '');
564
+ row.setAttribute('text', col.label || col.key);
565
+ row.dataset.key = col.key;
566
+ if (dir) row.dataset.active = dir;
567
+
568
+ // Trailing direction indicator — append AFTER menu-item-ui's connected()
569
+ // has run its #stamp() (defers via rAF so the [slot="text"] span exists).
570
+ const rafId = requestAnimationFrame(() => {
571
+ this.#sortIndicatorRafs.delete(rafId);
572
+ if (!this.isConnected) return;
573
+ const indicator = document.createElement('icon-ui');
574
+ indicator.setAttribute('data-sort-indicator', '');
575
+ indicator.setAttribute('size', 'xs');
576
+ indicator.setAttribute('name', dir === 'asc' ? 'arrow-up' : dir === 'desc' ? 'arrow-down' : 'caret-up-down');
577
+ row.appendChild(indicator);
578
+ });
579
+ this.#sortIndicatorRafs.add(rafId);
580
+
581
+ row.addEventListener('click', (e) => {
582
+ // Forward to the bound table by simulating a header click — the table
583
+ // already owns the asc / desc / clear cycle, so we don't duplicate state.
584
+ const headerCell = target.querySelector(`:scope > [data-header] [data-sort-key="${col.key}"]`);
585
+ if (!headerCell) return;
586
+ const evt = new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: e.shiftKey });
587
+ headerCell.dispatchEvent(evt);
588
+ this.dispatchEvent(new CustomEvent('sort-change', {
589
+ bubbles: true,
590
+ detail: { sortState: target.sortState },
591
+ }));
592
+ this.#refreshSortPanel();
593
+ });
594
+
595
+ list.appendChild(row);
596
+ }
597
+
598
+ panel.appendChild(list);
599
+
600
+ if (sortState.length) {
601
+ const clear = this.#mkPopoverAction('Clear sort', () => {
602
+ // Simulate cycle-through clicks until empty — but the table only stores
603
+ // one sort entry per key, and a non-shift click on an already-active key
604
+ // cycles to the opposite dir before clearing. Cleanest: clone columns and
605
+ // clear via a microtask using clicks on each active key until empty.
606
+ for (const s of [...sortState]) {
607
+ const headerCell = target.querySelector(`:scope > [data-header] [data-sort-key="${s.key}"]`);
608
+ if (!headerCell) continue;
609
+ // Two clicks toggle through asc → desc → clear when single-sort. With
610
+ // multi-sort entries we shift-click to remove individually.
611
+ const fire = (shift) => headerCell.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: shift }));
612
+ if (sortState.length > 1) {
613
+ // Shift-clicking through default→opposite→remove.
614
+ fire(true); fire(true);
615
+ } else {
616
+ // Single-sort: click twice non-shift to cycle through both dirs and clear.
617
+ fire(false); fire(false);
618
+ }
619
+ }
620
+ this.dispatchEvent(new CustomEvent('sort-change', {
621
+ bubbles: true,
622
+ detail: { sortState: target.sortState },
623
+ }));
624
+ this.#refreshSortPanel();
625
+ });
626
+ panel.appendChild(clear);
627
+ }
628
+ }
629
+
630
+ #refreshSortPanel() {
631
+ const ap = this.#activePopover;
632
+ if (!ap || ap.kind !== 'sort') return;
633
+ ap.panel.replaceChildren();
634
+ this.#fillSortPanel(ap.panel);
635
+ }
636
+
637
+ // ── Columns panel ────────────────────────────────────────────────────────
638
+
639
+ #fillColumnsPanel(panel) {
640
+ const target = this.#target;
641
+ if (!target?.columns?.length) {
642
+ panel.appendChild(emptyHint('No columns'));
643
+ return;
644
+ }
645
+
646
+ panel.appendChild(popoverHead('Visible columns'));
647
+
648
+ const list = document.createElement('div');
649
+ list.setAttribute('data-popover-list', '');
650
+
651
+ for (const col of target.columns) {
652
+ // <field-ui inline label="…"><check-ui></check-ui></field-ui> —
653
+ // canonical label-binds-to-control pair, mints id + [for] so clicking
654
+ // the label toggles the check. Same primitive used for filter rows.
655
+ const row = document.createElement('field-ui');
656
+ row.setAttribute('data-columns-row', '');
657
+ row.setAttribute('inline', '');
658
+ row.setAttribute('label', col.label || col.key);
659
+
660
+ const check = document.createElement('check-ui');
661
+ if (!col.hidden) check.setAttribute('checked', '');
662
+ check.dataset.key = col.key;
663
+ check.addEventListener('change', () => {
664
+ const next = target.columns.map((c) => (
665
+ c.key === col.key ? { ...c, hidden: !check.hasAttribute('checked') } : { ...c }
666
+ ));
667
+ target.columns = next;
668
+ this.dispatchEvent(new CustomEvent('columns-change', {
669
+ bubbles: true,
670
+ detail: { hiddenColumns: next.filter((c) => c.hidden).map((c) => c.key) },
671
+ }));
672
+ });
673
+ row.appendChild(check);
674
+
675
+ list.appendChild(row);
676
+ }
677
+
678
+ panel.appendChild(list);
679
+ }
680
+ }