@adia-ai/web-components 0.0.1

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