@adia-ai/web-components 0.6.33 → 0.6.35

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 (391) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/color/index.js +1 -1
  3. package/components/accordion/accordion-item.yaml +2 -2
  4. package/components/accordion/accordion.css +2 -2
  5. package/components/accordion/accordion.js +1 -1
  6. package/components/action-list/action-item.yaml +2 -2
  7. package/components/action-list/action-list.css +2 -2
  8. package/components/action-list/action-list.js +1 -1
  9. package/components/agent-artifact/{class.js → agent-artifact.class.js} +1 -1
  10. package/components/agent-artifact/agent-artifact.css +31 -31
  11. package/components/agent-artifact/agent-artifact.js +1 -1
  12. package/components/agent-feedback-bar/agent-feedback-bar.css +10 -10
  13. package/components/agent-feedback-bar/agent-feedback-bar.js +1 -1
  14. package/components/agent-questions/agent-questions.css +57 -57
  15. package/components/agent-questions/agent-questions.js +1 -1
  16. package/components/agent-reasoning/agent-reasoning.css +62 -62
  17. package/components/agent-reasoning/agent-reasoning.js +1 -1
  18. package/components/agent-suggestions/agent-suggestions.css +4 -4
  19. package/components/agent-suggestions/agent-suggestions.js +1 -1
  20. package/components/agent-trace/agent-trace.css +53 -53
  21. package/components/alert/alert.a2ui.json +64 -1
  22. package/components/alert/{class.js → alert.class.js} +189 -2
  23. package/components/alert/alert.css +119 -41
  24. package/components/alert/alert.d.ts +14 -0
  25. package/components/alert/alert.js +1 -1
  26. package/components/alert/alert.test.js +184 -0
  27. package/components/alert/alert.yaml +114 -1
  28. package/components/avatar/avatar-group.yaml +2 -2
  29. package/components/avatar/avatar.css +27 -27
  30. package/components/avatar/avatar.js +1 -1
  31. package/components/badge/badge.css +27 -27
  32. package/components/badge/badge.js +1 -1
  33. package/components/block/block.css +16 -16
  34. package/components/block/block.js +1 -1
  35. package/components/breadcrumb/breadcrumb.css +23 -23
  36. package/components/breadcrumb/breadcrumb.js +1 -1
  37. package/components/button/button.css +101 -91
  38. package/components/button/button.js +1 -1
  39. package/components/calendar-grid/calendar-grid.a2ui.json +146 -0
  40. package/components/calendar-grid/calendar-grid.class.js +326 -0
  41. package/components/calendar-grid/calendar-grid.css +246 -0
  42. package/components/calendar-grid/calendar-grid.d.ts +41 -0
  43. package/components/calendar-grid/calendar-grid.js +17 -0
  44. package/components/calendar-grid/calendar-grid.yaml +136 -0
  45. package/components/calendar-picker/calendar-picker.css +139 -139
  46. package/components/calendar-picker/calendar-picker.js +1 -1
  47. package/components/canvas/canvas.css +12 -12
  48. package/components/card/card.css +83 -83
  49. package/components/card/card.js +1 -1
  50. package/components/chart/chart.css +224 -224
  51. package/components/chart/chart.js +1 -1
  52. package/components/chart-legend/chart-legend.css +26 -26
  53. package/components/chart-legend/chart-legend.js +1 -1
  54. package/components/chat-thread/chat-input.a2ui.json +1 -1
  55. package/components/chat-thread/chat-input.js +6 -1
  56. package/components/chat-thread/chat-input.yaml +4 -1
  57. package/components/chat-thread/chat-thread.js +1 -1
  58. package/components/check/check.css +40 -40
  59. package/components/check/check.js +1 -1
  60. package/components/code/code.css +125 -125
  61. package/components/code/code.js +1 -1
  62. package/components/col/col.css +15 -15
  63. package/components/col/col.js +1 -1
  64. package/components/color-input/color-input.js +1 -1
  65. package/components/color-picker/color-picker.css +55 -55
  66. package/components/color-picker/color-picker.js +1 -1
  67. package/components/combobox/combobox.a2ui.json +363 -0
  68. package/components/combobox/combobox.class.js +861 -0
  69. package/components/combobox/combobox.css +244 -0
  70. package/components/combobox/combobox.d.ts +113 -0
  71. package/components/combobox/combobox.examples.md +59 -0
  72. package/components/combobox/combobox.js +17 -0
  73. package/components/combobox/combobox.test.js +181 -0
  74. package/components/combobox/combobox.yaml +369 -0
  75. package/components/command/command.css +90 -90
  76. package/components/command/command.js +1 -1
  77. package/components/date-range-picker/date-range-picker.a2ui.json +300 -0
  78. package/components/date-range-picker/date-range-picker.class.js +791 -0
  79. package/components/date-range-picker/date-range-picker.css +224 -0
  80. package/components/date-range-picker/date-range-picker.d.ts +82 -0
  81. package/components/date-range-picker/date-range-picker.examples.md +37 -0
  82. package/components/date-range-picker/date-range-picker.js +17 -0
  83. package/components/date-range-picker/date-range-picker.test.js +387 -0
  84. package/components/date-range-picker/date-range-picker.yaml +285 -0
  85. package/components/datetime-picker/datetime-picker.a2ui.json +334 -0
  86. package/components/datetime-picker/datetime-picker.class.js +706 -0
  87. package/components/datetime-picker/datetime-picker.css +150 -0
  88. package/components/datetime-picker/datetime-picker.d.ts +86 -0
  89. package/components/datetime-picker/datetime-picker.examples.md +46 -0
  90. package/components/datetime-picker/datetime-picker.js +17 -0
  91. package/components/datetime-picker/datetime-picker.test.js +454 -0
  92. package/components/datetime-picker/datetime-picker.yaml +332 -0
  93. package/components/demo-toggle/demo-toggle.css +27 -27
  94. package/components/demo-toggle/demo-toggle.js +1 -1
  95. package/components/description-list/description-list.css +18 -18
  96. package/components/description-list/description-list.js +1 -1
  97. package/components/divider/divider.css +24 -24
  98. package/components/divider/divider.js +1 -1
  99. package/components/drawer/drawer.js +1 -1
  100. package/components/embed/embed.css +6 -6
  101. package/components/embed/embed.js +1 -1
  102. package/components/empty-state/empty-state.css +27 -27
  103. package/components/empty-state/empty-state.js +1 -1
  104. package/components/feed/feed.css +12 -12
  105. package/components/feed/feed.js +1 -1
  106. package/components/field/field.css +28 -28
  107. package/components/field/field.js +1 -1
  108. package/components/field/field.test.js +1 -1
  109. package/components/fields/fields.css +5 -5
  110. package/components/fields/fields.js +1 -1
  111. package/components/grid/grid.css +5 -5
  112. package/components/grid/grid.js +1 -1
  113. package/components/heatmap/heatmap.css +63 -63
  114. package/components/heatmap/heatmap.js +1 -1
  115. package/components/icon/icon.css +12 -12
  116. package/components/icon/icon.js +1 -1
  117. package/components/image/image.css +14 -14
  118. package/components/image/image.js +1 -1
  119. package/components/index.js +11 -0
  120. package/components/inline-message/inline-message.a2ui.json +143 -0
  121. package/components/inline-message/inline-message.class.js +169 -0
  122. package/components/inline-message/inline-message.css +75 -0
  123. package/components/inline-message/inline-message.d.ts +31 -0
  124. package/components/inline-message/inline-message.examples.md +19 -0
  125. package/components/inline-message/inline-message.js +17 -0
  126. package/components/inline-message/inline-message.test.js +203 -0
  127. package/components/inline-message/inline-message.yaml +205 -0
  128. package/components/input/input.css +67 -67
  129. package/components/input/input.js +1 -1
  130. package/components/input/input.yaml +5 -4
  131. package/components/inspector/inspector.css +6 -6
  132. package/components/inspector/inspector.js +1 -1
  133. package/components/integration-card/integration-card.a2ui.json +268 -0
  134. package/components/integration-card/integration-card.class.js +410 -0
  135. package/components/integration-card/integration-card.css +169 -0
  136. package/components/integration-card/integration-card.d.ts +63 -0
  137. package/components/integration-card/integration-card.examples.md +41 -0
  138. package/components/integration-card/integration-card.js +17 -0
  139. package/components/integration-card/integration-card.test.js +306 -0
  140. package/components/integration-card/integration-card.yaml +280 -0
  141. package/components/kbd/kbd.css +32 -32
  142. package/components/kbd/kbd.js +1 -1
  143. package/components/link/link.css +12 -12
  144. package/components/link/link.js +1 -1
  145. package/components/list/list-item.yaml +2 -2
  146. package/components/list/list.css +8 -8
  147. package/components/list/list.js +1 -1
  148. package/components/list-window/list-window.a2ui.json +277 -0
  149. package/components/list-window/list-window.class.js +688 -0
  150. package/components/list-window/list-window.css +124 -0
  151. package/components/list-window/list-window.d.ts +84 -0
  152. package/components/list-window/list-window.examples.md +73 -0
  153. package/components/list-window/list-window.js +17 -0
  154. package/components/list-window/list-window.test.js +303 -0
  155. package/components/list-window/list-window.yaml +270 -0
  156. package/components/loading-overlay/loading-overlay.a2ui.json +176 -0
  157. package/components/loading-overlay/loading-overlay.class.js +203 -0
  158. package/components/loading-overlay/loading-overlay.css +81 -0
  159. package/components/loading-overlay/loading-overlay.d.ts +24 -0
  160. package/components/loading-overlay/loading-overlay.examples.md +50 -0
  161. package/components/loading-overlay/loading-overlay.js +17 -0
  162. package/components/loading-overlay/loading-overlay.test.js +257 -0
  163. package/components/loading-overlay/loading-overlay.yaml +260 -0
  164. package/components/menu/menu-divider.yaml +1 -1
  165. package/components/menu/menu-item.yaml +1 -1
  166. package/components/menu/menu.a2ui.json +3 -0
  167. package/components/menu/menu.css +8 -8
  168. package/components/menu/menu.js +1 -1
  169. package/components/menu/menu.yaml +7 -0
  170. package/components/modal/{class.js → modal.class.js} +12 -1
  171. package/components/modal/modal.css +54 -44
  172. package/components/modal/modal.js +1 -1
  173. package/components/nav/nav.css +40 -40
  174. package/components/nav/nav.js +1 -1
  175. package/components/nav-group/nav-group.css +52 -52
  176. package/components/nav-group/nav-group.js +1 -1
  177. package/components/nav-item/nav-item.css +44 -44
  178. package/components/nav-item/nav-item.js +1 -1
  179. package/components/noodles/noodles.css +31 -31
  180. package/components/noodles/noodles.js +1 -1
  181. package/components/option-card/option-card.css +69 -69
  182. package/components/option-card/option-card.js +1 -1
  183. package/components/otp-input/otp-input.css +30 -30
  184. package/components/otp-input/otp-input.js +1 -1
  185. package/components/page/page.css +18 -18
  186. package/components/page/page.js +1 -1
  187. package/components/pagination/pagination.css +61 -61
  188. package/components/pagination/pagination.js +1 -1
  189. package/components/pane/pane.css +57 -57
  190. package/components/pane/pane.js +1 -1
  191. package/components/pipeline-status/pipeline-status.css +65 -65
  192. package/components/pipeline-status/pipeline-status.js +1 -1
  193. package/components/popover/popover.a2ui.json +8 -1
  194. package/components/popover/popover.css +17 -17
  195. package/components/popover/popover.js +1 -1
  196. package/components/popover/popover.yaml +14 -1
  197. package/components/progress/progress.css +23 -23
  198. package/components/progress/progress.js +1 -1
  199. package/components/progress-row/progress-row.css +17 -17
  200. package/components/progress-row/progress-row.js +1 -1
  201. package/components/radio/radio.css +39 -39
  202. package/components/radio/radio.js +1 -1
  203. package/components/range/range.css +55 -55
  204. package/components/range/range.js +1 -1
  205. package/components/rating/rating.css +28 -28
  206. package/components/rating/rating.js +1 -1
  207. package/components/richtext/richtext.css +133 -133
  208. package/components/richtext/richtext.js +1 -1
  209. package/components/row/row.css +19 -19
  210. package/components/row/row.js +1 -1
  211. package/components/search/search.css +5 -5
  212. package/components/search/search.js +1 -1
  213. package/components/segment/segment.css +24 -24
  214. package/components/segment/segment.js +1 -1
  215. package/components/segmented/segmented.css +25 -25
  216. package/components/segmented/segmented.js +1 -1
  217. package/components/select/select.a2ui.json +58 -4
  218. package/components/select/{class.js → select.class.js} +415 -6
  219. package/components/select/select.css +242 -84
  220. package/components/select/select.d.ts +31 -1
  221. package/components/select/select.js +1 -1
  222. package/components/select/select.test.js +202 -0
  223. package/components/select/select.yaml +126 -5
  224. package/components/skeleton/skeleton.css +14 -14
  225. package/components/skeleton/skeleton.js +1 -1
  226. package/components/slider/slider.css +46 -46
  227. package/components/slider/slider.js +1 -1
  228. package/components/spinner/spinner.a2ui.json +198 -0
  229. package/components/spinner/spinner.class.js +99 -0
  230. package/components/spinner/spinner.css +221 -0
  231. package/components/spinner/spinner.d.ts +26 -0
  232. package/components/spinner/spinner.examples.md +26 -0
  233. package/components/spinner/spinner.js +17 -0
  234. package/components/spinner/spinner.test.js +272 -0
  235. package/components/spinner/spinner.yaml +238 -0
  236. package/components/stack/stack.css +11 -11
  237. package/components/stack/stack.js +1 -1
  238. package/components/stat/stat.css +25 -25
  239. package/components/step-progress/step-progress.css +20 -20
  240. package/components/step-progress/step-progress.js +1 -1
  241. package/components/stepper/stepper-item.yaml +1 -1
  242. package/components/stepper/stepper.css +29 -29
  243. package/components/stepper/stepper.js +1 -1
  244. package/components/stream/stream.css +12 -12
  245. package/components/stream/stream.js +1 -1
  246. package/components/swatch/swatch.css +68 -68
  247. package/components/swatch/swatch.js +1 -1
  248. package/components/swiper/swiper.css +57 -57
  249. package/components/swiper/swiper.js +1 -1
  250. package/components/switch/switch.css +52 -52
  251. package/components/switch/switch.js +1 -1
  252. package/components/table/table.css +163 -163
  253. package/components/table/table.js +1 -1
  254. package/components/table-toolbar/{class.js → table-toolbar.class.js} +1 -1
  255. package/components/table-toolbar/table-toolbar.css +32 -32
  256. package/components/table-toolbar/table-toolbar.js +1 -1
  257. package/components/tabs/tab.yaml +2 -2
  258. package/components/tabs/tabs.css +51 -51
  259. package/components/tabs/tabs.js +1 -1
  260. package/components/tag/tag.css +48 -48
  261. package/components/tag/tag.js +1 -1
  262. package/components/tags-input/tags-input.a2ui.json +337 -0
  263. package/components/tags-input/tags-input.class.js +776 -0
  264. package/components/tags-input/tags-input.css +201 -0
  265. package/components/tags-input/tags-input.d.ts +120 -0
  266. package/components/tags-input/tags-input.examples.md +92 -0
  267. package/components/tags-input/tags-input.js +17 -0
  268. package/components/tags-input/tags-input.test.js +368 -0
  269. package/components/tags-input/tags-input.yaml +367 -0
  270. package/components/text/text.css +44 -44
  271. package/components/text/text.js +1 -1
  272. package/components/textarea/textarea.a2ui.json +1 -1
  273. package/components/textarea/textarea.css +46 -46
  274. package/components/textarea/textarea.js +1 -1
  275. package/components/textarea/textarea.yaml +11 -8
  276. package/components/time-picker/time-picker.a2ui.json +267 -0
  277. package/components/time-picker/time-picker.class.js +693 -0
  278. package/components/time-picker/time-picker.css +122 -0
  279. package/components/time-picker/time-picker.d.ts +75 -0
  280. package/components/time-picker/time-picker.examples.md +35 -0
  281. package/components/time-picker/time-picker.js +17 -0
  282. package/components/time-picker/time-picker.test.js +287 -0
  283. package/components/time-picker/time-picker.yaml +256 -0
  284. package/components/timeline/timeline-item.yaml +2 -2
  285. package/components/timeline/{class.js → timeline.class.js} +1 -1
  286. package/components/timeline/timeline.css +50 -50
  287. package/components/timeline/timeline.js +1 -1
  288. package/components/toast/toast.css +58 -58
  289. package/components/toast/toast.js +1 -1
  290. package/components/toggle-group/toggle-group.css +6 -6
  291. package/components/toggle-group/toggle-group.js +1 -1
  292. package/components/toggle-group/toggle-option.yaml +1 -1
  293. package/components/toggle-scheme/toggle-scheme.css +2 -2
  294. package/components/toggle-scheme/toggle-scheme.js +1 -1
  295. package/components/toolbar/toolbar-group.yaml +1 -1
  296. package/components/toolbar/toolbar.css +17 -17
  297. package/components/toolbar/toolbar.js +1 -1
  298. package/components/tooltip/tooltip.css +2 -2
  299. package/components/tooltip/tooltip.js +1 -1
  300. package/components/tree/tree-item.yaml +1 -1
  301. package/components/tree/tree.css +37 -37
  302. package/components/tree/tree.js +1 -1
  303. package/components/upload/upload.css +49 -49
  304. package/components/upload/upload.js +1 -1
  305. package/dist/web-components.min.css +1 -1
  306. package/dist/web-components.min.js +146 -87
  307. package/package.json +3 -3
  308. package/styles/components.css +11 -0
  309. /package/components/accordion/{class.js → accordion.class.js} +0 -0
  310. /package/components/action-list/{class.js → action-list.class.js} +0 -0
  311. /package/components/agent-feedback-bar/{class.js → agent-feedback-bar.class.js} +0 -0
  312. /package/components/agent-questions/{class.js → agent-questions.class.js} +0 -0
  313. /package/components/agent-reasoning/{class.js → agent-reasoning.class.js} +0 -0
  314. /package/components/agent-suggestions/{class.js → agent-suggestions.class.js} +0 -0
  315. /package/components/avatar/{class.js → avatar.class.js} +0 -0
  316. /package/components/badge/{class.js → badge.class.js} +0 -0
  317. /package/components/block/{class.js → block.class.js} +0 -0
  318. /package/components/breadcrumb/{class.js → breadcrumb.class.js} +0 -0
  319. /package/components/button/{class.js → button.class.js} +0 -0
  320. /package/components/calendar-picker/{class.js → calendar-picker.class.js} +0 -0
  321. /package/components/card/{class.js → card.class.js} +0 -0
  322. /package/components/chart/{class.js → chart.class.js} +0 -0
  323. /package/components/chart-legend/{class.js → chart-legend.class.js} +0 -0
  324. /package/components/chat-thread/{class.js → chat-thread.class.js} +0 -0
  325. /package/components/check/{class.js → check.class.js} +0 -0
  326. /package/components/code/{class.js → code.class.js} +0 -0
  327. /package/components/col/{class.js → col.class.js} +0 -0
  328. /package/components/color-input/{class.js → color-input.class.js} +0 -0
  329. /package/components/color-picker/{class.js → color-picker.class.js} +0 -0
  330. /package/components/command/{class.js → command.class.js} +0 -0
  331. /package/components/demo-toggle/{class.js → demo-toggle.class.js} +0 -0
  332. /package/components/description-list/{class.js → description-list.class.js} +0 -0
  333. /package/components/divider/{class.js → divider.class.js} +0 -0
  334. /package/components/drawer/{class.js → drawer.class.js} +0 -0
  335. /package/components/embed/{class.js → embed.class.js} +0 -0
  336. /package/components/empty-state/{class.js → empty-state.class.js} +0 -0
  337. /package/components/feed/{class.js → feed.class.js} +0 -0
  338. /package/components/field/{class.js → field.class.js} +0 -0
  339. /package/components/fields/{class.js → fields.class.js} +0 -0
  340. /package/components/grid/{class.js → grid.class.js} +0 -0
  341. /package/components/heatmap/{class.js → heatmap.class.js} +0 -0
  342. /package/components/icon/{class.js → icon.class.js} +0 -0
  343. /package/components/image/{class.js → image.class.js} +0 -0
  344. /package/components/input/{class.js → input.class.js} +0 -0
  345. /package/components/inspector/{class.js → inspector.class.js} +0 -0
  346. /package/components/kbd/{class.js → kbd.class.js} +0 -0
  347. /package/components/link/{class.js → link.class.js} +0 -0
  348. /package/components/list/{class.js → list.class.js} +0 -0
  349. /package/components/menu/{class.js → menu.class.js} +0 -0
  350. /package/components/nav/{class.js → nav.class.js} +0 -0
  351. /package/components/nav-group/{class.js → nav-group.class.js} +0 -0
  352. /package/components/nav-item/{class.js → nav-item.class.js} +0 -0
  353. /package/components/noodles/{class.js → noodles.class.js} +0 -0
  354. /package/components/option-card/{class.js → option-card.class.js} +0 -0
  355. /package/components/otp-input/{class.js → otp-input.class.js} +0 -0
  356. /package/components/page/{class.js → page.class.js} +0 -0
  357. /package/components/pagination/{class.js → pagination.class.js} +0 -0
  358. /package/components/pane/{class.js → pane.class.js} +0 -0
  359. /package/components/pipeline-status/{class.js → pipeline-status.class.js} +0 -0
  360. /package/components/popover/{class.js → popover.class.js} +0 -0
  361. /package/components/progress/{class.js → progress.class.js} +0 -0
  362. /package/components/progress-row/{class.js → progress-row.class.js} +0 -0
  363. /package/components/radio/{class.js → radio.class.js} +0 -0
  364. /package/components/range/{class.js → range.class.js} +0 -0
  365. /package/components/rating/{class.js → rating.class.js} +0 -0
  366. /package/components/richtext/{class.js → richtext.class.js} +0 -0
  367. /package/components/row/{class.js → row.class.js} +0 -0
  368. /package/components/search/{class.js → search.class.js} +0 -0
  369. /package/components/segment/{class.js → segment.class.js} +0 -0
  370. /package/components/segmented/{class.js → segmented.class.js} +0 -0
  371. /package/components/skeleton/{class.js → skeleton.class.js} +0 -0
  372. /package/components/slider/{class.js → slider.class.js} +0 -0
  373. /package/components/stack/{class.js → stack.class.js} +0 -0
  374. /package/components/step-progress/{class.js → step-progress.class.js} +0 -0
  375. /package/components/stepper/{class.js → stepper.class.js} +0 -0
  376. /package/components/stream/{class.js → stream.class.js} +0 -0
  377. /package/components/swatch/{class.js → swatch.class.js} +0 -0
  378. /package/components/swiper/{class.js → swiper.class.js} +0 -0
  379. /package/components/switch/{class.js → switch.class.js} +0 -0
  380. /package/components/table/{class.js → table.class.js} +0 -0
  381. /package/components/tabs/{class.js → tabs.class.js} +0 -0
  382. /package/components/tag/{class.js → tag.class.js} +0 -0
  383. /package/components/text/{class.js → text.class.js} +0 -0
  384. /package/components/textarea/{class.js → textarea.class.js} +0 -0
  385. /package/components/toast/{class.js → toast.class.js} +0 -0
  386. /package/components/toggle-group/{class.js → toggle-group.class.js} +0 -0
  387. /package/components/toggle-scheme/{class.js → toggle-scheme.class.js} +0 -0
  388. /package/components/toolbar/{class.js → toolbar.class.js} +0 -0
  389. /package/components/tooltip/{class.js → tooltip.class.js} +0 -0
  390. /package/components/tree/{class.js → tree.class.js} +0 -0
  391. /package/components/upload/{class.js → upload.class.js} +0 -0
@@ -0,0 +1,791 @@
1
+ /**
2
+ * Non-side-effect class export for `<date-range-picker-ui>`.
3
+ *
4
+ * Importing this file gives you the class 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/date-range-picker`
9
+ * (which imports this file + calls `defineIfFree()`).
10
+ *
11
+ * @see ../../USAGE.md#registration--auto-vs-explicit
12
+ */
13
+
14
+ /**
15
+ * <date-range-picker-ui>
16
+ *
17
+ * Compound form primitive for selecting a start + end date pair with
18
+ * optional preset shortcuts. Composes <calendar-picker-ui> (two
19
+ * instances — start + end pane) + <popover-ui> + <button-ui> for the
20
+ * trigger and preset rail.
21
+ *
22
+ * Form participation:
23
+ * ElementInternals serializes the selected range as JSON:
24
+ * `{"from":"2026-01-01","to":"2026-01-07"}`
25
+ * under the [name] attribute. With [comparison] set, a secondary
26
+ * field `<name>-compare` carries the comparison range.
27
+ *
28
+ * Keyboard:
29
+ * Enter/Space (trigger) — open popover, focus start pane
30
+ * Escape (open) — close popover (no commit)
31
+ * Tab (open) — cycle trigger → start cal → end cal → presets → footer
32
+ * Arrows (calendar) — navigate days (delegated to <calendar-picker-ui>)
33
+ *
34
+ * A11y:
35
+ * role=combobox on the host; role=dialog on the popover; focus moves
36
+ * to the start calendar's grid on open and returns to the trigger on
37
+ * close. Tab is trapped inside the popover while open.
38
+ */
39
+
40
+ import { UIFormElement } from '../../core/form.js';
41
+ import { anchorPopover } from '../../core/anchor.js';
42
+ import { untracked } from '../../core/signals.js';
43
+
44
+ const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
45
+ const MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
46
+
47
+ function pad(n) { return String(n).padStart(2, '0'); }
48
+
49
+ function toISO(d) {
50
+ if (!d) return '';
51
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
52
+ }
53
+
54
+ function parseISO(str) {
55
+ if (!str) return null;
56
+ const m = String(str).match(/^(\d{4})-(\d{2})-(\d{2})$/);
57
+ if (!m) return null;
58
+ const d = new Date(+m[1], +m[2] - 1, +m[3]);
59
+ return Number.isNaN(d.getTime()) ? null : d;
60
+ }
61
+
62
+ function parseRange(v) {
63
+ if (v == null) return null;
64
+ if (typeof v === 'object') {
65
+ if (!v.from || !v.to) return null;
66
+ return { from: String(v.from), to: String(v.to) };
67
+ }
68
+ if (typeof v !== 'string' || !v.trim()) return null;
69
+ try {
70
+ const obj = JSON.parse(v);
71
+ if (obj && obj.from && obj.to) return { from: String(obj.from), to: String(obj.to) };
72
+ } catch {}
73
+ return null;
74
+ }
75
+
76
+ function rangeIsOrdered(r) {
77
+ if (!r) return false;
78
+ return r.from <= r.to;
79
+ }
80
+
81
+ function formatDate(iso, fmt) {
82
+ const d = parseISO(iso);
83
+ if (!d) return '';
84
+ switch (fmt) {
85
+ case 'long':
86
+ return `${MONTHS_LONG[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
87
+ case 'iso':
88
+ return iso;
89
+ case 'short':
90
+ default:
91
+ return `${MONTHS_SHORT[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
92
+ }
93
+ }
94
+
95
+ /* Default presets (today is computed lazily on first render). */
96
+ function defaultPresets(today = new Date()) {
97
+ const t0 = new Date(today.getFullYear(), today.getMonth(), today.getDate());
98
+ const isoToday = toISO(t0);
99
+ const sub = (days) => {
100
+ const d = new Date(t0);
101
+ d.setDate(d.getDate() - days);
102
+ return toISO(d);
103
+ };
104
+ const startOfMonth = toISO(new Date(t0.getFullYear(), t0.getMonth(), 1));
105
+ const endOfMonth = toISO(new Date(t0.getFullYear(), t0.getMonth() + 1, 0));
106
+ const startOfYear = toISO(new Date(t0.getFullYear(), 0, 1));
107
+ return [
108
+ { label: 'Today', range: { from: isoToday, to: isoToday } },
109
+ { label: 'Last 7 days', range: { from: sub(6), to: isoToday } },
110
+ { label: 'Last 30 days', range: { from: sub(29), to: isoToday } },
111
+ { label: 'This month', range: { from: startOfMonth, to: endOfMonth } },
112
+ { label: 'Year to date', range: { from: startOfYear, to: isoToday } },
113
+ ];
114
+ }
115
+
116
+ /* Default comparison presets (analytics surfaces). */
117
+ function defaultComparisonPresets() {
118
+ return [
119
+ { label: 'Previous period' },
120
+ { label: 'Same period last year' },
121
+ { label: 'Same period last month' },
122
+ ];
123
+ }
124
+
125
+ export class UIDateRangePicker extends UIFormElement {
126
+ // Per AGENTS.md §170: label is NOT first-class here — use <field-ui label>.
127
+ static labelDeprecated = true;
128
+
129
+ // §154: Phosphor icons this primitive auto-stamps (without consumer
130
+ // markup). Aggregated by installIconLoadersForRegistered() across all
131
+ // defined elements. Audited by check-required-icons.mjs.
132
+ static requiredIcons = ['calendar', 'caret-down', 'arrow-right'];
133
+
134
+ static get properties() {
135
+ return {
136
+ ...UIFormElement.properties,
137
+ // value/compareValue are STRINGS at the attribute boundary (JSON
138
+ // serialized); typed accessors expose the parsed object form.
139
+ compareValue: { type: String, default: '', attribute: 'compare-value' },
140
+ comparison: { type: Boolean, default: false, reflect: true },
141
+ min: { type: String, default: '', reflect: true },
142
+ max: { type: String, default: '', reflect: true },
143
+ open: { type: Boolean, default: false, reflect: true },
144
+ placeholder: { type: String, default: 'Select range', reflect: true },
145
+ format: { type: String, default: 'short', reflect: true },
146
+ noPresets: { type: Boolean, default: false, attribute: 'no-presets', reflect: true },
147
+ };
148
+ }
149
+
150
+ // Static parts — fixed structural skeleton stamped on first connect.
151
+ // The popover is a `popover="manual"` element so we control showPopover()
152
+ // / hidePopover() programmatically. Two calendar instances + a preset
153
+ // rail + a calendar area sit inside.
154
+ static parts = {
155
+ trigger: '<button-ui slot="trigger" variant="outline" type="button" icon="calendar" trailing-icon="caret-down" aria-label="Open date range picker"></button-ui>',
156
+ popover: '<div slot="popover" popover="manual" role="dialog" aria-label="Date range picker"></div>',
157
+ presets: '<div data-preset-rail role="group" aria-label="Date range presets"></div>',
158
+ calArea: '<div data-calendar-area></div>',
159
+ // Substrate primitive — bare grid, no trigger / no popover (added §FB-Wave1-QA).
160
+ calFrom: '<calendar-grid-ui data-cal-from aria-label="Start date"></calendar-grid-ui>',
161
+ calTo: '<calendar-grid-ui data-cal-to aria-label="End date"></calendar-grid-ui>',
162
+ };
163
+
164
+ // No template — date-range-picker composes from authored light-DOM
165
+ // children + ensured parts. An empty html`` result would trigger
166
+ // stamp() → replaceChildren(), wiping authored [slot="presets|footer"]
167
+ // before render() can migrate them.
168
+ static template = () => null;
169
+
170
+ // ── State ─────────────────────────────────────────────────────────
171
+ #presetsAttr = null; // explicit override via property
172
+ #pending = null; // partial range during selection
173
+ #previousFocus = null; // focus restoration target on close
174
+ #bound = false;
175
+ #popoverShown = false; // local mirror of popover open state
176
+ #triggerRef = null;
177
+ #popoverRef = null;
178
+ #presetRailRef = null;
179
+ #calFromRef = null;
180
+ #calToRef = null;
181
+ #anchorCleanup = null; // §FB-Wave1-QA — popover anchor positioning cleanup
182
+
183
+ // ── Public accessors ──────────────────────────────────────────────
184
+
185
+ /** Parsed `{from, to}` object form of the value attribute (or null). */
186
+ get rangeValue() {
187
+ return parseRange(this.value);
188
+ }
189
+ set rangeValue(v) {
190
+ if (v == null) {
191
+ this.value = '';
192
+ } else {
193
+ this.value = JSON.stringify({ from: String(v.from), to: String(v.to) });
194
+ }
195
+ }
196
+
197
+ /** Parsed `{from, to}` object form of compare-value (or null). */
198
+ get rangeCompareValue() {
199
+ return parseRange(this.compareValue);
200
+ }
201
+ set rangeCompareValue(v) {
202
+ if (v == null) {
203
+ this.compareValue = '';
204
+ } else {
205
+ this.compareValue = JSON.stringify({ from: String(v.from), to: String(v.to) });
206
+ }
207
+ }
208
+
209
+ /** Override the preset list. Setter accepts an array of `{label, range}`. */
210
+ get presets() {
211
+ if (Array.isArray(this.#presetsAttr)) return this.#presetsAttr;
212
+ return defaultPresets();
213
+ }
214
+ set presets(arr) {
215
+ // Wrap in untracked() so reactive-property reads in this setter don't
216
+ // leak subscriptions upward (per MEMORY entry: custom setter `untracked` wrap).
217
+ untracked(() => {
218
+ this.#presetsAttr = Array.isArray(arr) ? arr.slice() : null;
219
+ if (this.isConnected) this.#renderPresets();
220
+ });
221
+ }
222
+
223
+ // ── Imperative API ────────────────────────────────────────────────
224
+
225
+ open$() { this.open = true; }
226
+ close$() { this.open = false; }
227
+
228
+ /**
229
+ * Programmatically open the popover (matches the spec §4 method shape
230
+ * `open()`). The reactive prop `open` is already exposed via the
231
+ * setter; this method is the public commit point that also wires
232
+ * focus + dispatches `open` event with trigger='programmatic'.
233
+ */
234
+ openPopover() {
235
+ if (this.disabled || this.readonly) return;
236
+ if (!this.open) {
237
+ this.#previousFocus = document.activeElement;
238
+ this.open = true;
239
+ this.dispatchEvent(new CustomEvent('open', { bubbles: true, detail: { trigger: 'programmatic' } }));
240
+ }
241
+ }
242
+
243
+ /** Programmatically close. */
244
+ closePopover(reason = 'programmatic') {
245
+ if (!this.open) return;
246
+ this.open = false;
247
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, detail: { reason } }));
248
+ }
249
+
250
+ /** Reset value (and compareValue when comparison is set). */
251
+ clear() {
252
+ this.value = '';
253
+ if (this.comparison) this.compareValue = '';
254
+ this.#pending = null;
255
+ this.syncValue('');
256
+ }
257
+
258
+ /** Look up a preset by label and apply it. Returns true on hit. */
259
+ applyPreset(label) {
260
+ const preset = this.presets.find((p) => p.label === label);
261
+ if (!preset || !preset.range) return false;
262
+ this.#commitRange(preset.range, { presetLabel: preset.label });
263
+ return true;
264
+ }
265
+
266
+ // ── Constraint validation ─────────────────────────────────────────
267
+
268
+ /**
269
+ * Override `syncValue` so the form value reflects the JSON-serialized
270
+ * range. The base UIFormElement::syncValue writes raw `value` to the
271
+ * form; we additionally write the comparison field if applicable.
272
+ */
273
+ syncValue(val) {
274
+ const baseVal = val ?? this.value ?? '';
275
+ const range = parseRange(baseVal);
276
+ if (this.comparison && this.name) {
277
+ // Compose a FormData so two named fields land in the form.
278
+ const fd = new FormData();
279
+ if (range) fd.append(this.name, baseVal);
280
+ const cmp = parseRange(this.compareValue);
281
+ if (cmp) fd.append(`${this.name}-compare`, this.compareValue);
282
+ this.internals.setFormValue(fd);
283
+ } else {
284
+ this.internals.setFormValue(range ? baseVal : '');
285
+ }
286
+ this.#runRangeConstraints(range);
287
+ }
288
+
289
+ #runRangeConstraints(range) {
290
+ if (this.required && !range) {
291
+ this.internals.setValidity(
292
+ { valueMissing: true },
293
+ this.getAttribute('data-msg-required') || 'Please select a date range.',
294
+ this,
295
+ );
296
+ return false;
297
+ }
298
+ if (range && !rangeIsOrdered(range)) {
299
+ this.internals.setValidity(
300
+ { rangeUnderflow: true },
301
+ 'End date must be on or after start date.',
302
+ this,
303
+ );
304
+ return false;
305
+ }
306
+ if (range && this.min && range.from < this.min) {
307
+ this.internals.setValidity(
308
+ { rangeUnderflow: true },
309
+ `Earliest selectable date is ${this.min}.`,
310
+ this,
311
+ );
312
+ return false;
313
+ }
314
+ if (range && this.max && range.to > this.max) {
315
+ this.internals.setValidity(
316
+ { rangeOverflow: true },
317
+ `Latest selectable date is ${this.max}.`,
318
+ this,
319
+ );
320
+ return false;
321
+ }
322
+ this.internals.setValidity({});
323
+ return true;
324
+ }
325
+
326
+ // ── Lifecycle ─────────────────────────────────────────────────────
327
+
328
+ connected() {
329
+ super.connected();
330
+ this.setAttribute('role', 'combobox');
331
+ this.setAttribute('aria-haspopup', 'dialog');
332
+ this.setAttribute('aria-expanded', this.open ? 'true' : 'false');
333
+
334
+ // Stamp parts via this.ensure() — idempotent. Wire listeners once.
335
+ if (!this.#bound) {
336
+ this.#bound = true;
337
+ this.#ensureParts();
338
+ this.#triggerRef.addEventListener('click', this.#onTriggerClick);
339
+ // Register keydown in CAPTURE phase so we set the keyboard-origin
340
+ // flag BEFORE <button-ui>'s own keydown handler synthesizes a
341
+ // click via `this.click()` (see button/button.class.js:148).
342
+ this.#triggerRef.addEventListener('keydown', this.#onTriggerKey, true);
343
+ this.#popoverRef.addEventListener('click', this.#onPopoverClick);
344
+ this.#popoverRef.addEventListener('keydown', this.#onPopoverKey);
345
+ this.#calFromRef.addEventListener('change', this.#onCalFromChange);
346
+ this.#calToRef.addEventListener('change', this.#onCalToChange);
347
+ }
348
+ // Wire form value on first connect.
349
+ this.syncValue();
350
+ }
351
+
352
+ disconnected() {
353
+ super.disconnected();
354
+ if (this.#triggerRef) {
355
+ this.#triggerRef.removeEventListener('click', this.#onTriggerClick);
356
+ // Capture-phase removal must mirror the capture-phase registration.
357
+ this.#triggerRef.removeEventListener('keydown', this.#onTriggerKey, true);
358
+ }
359
+ if (this.#popoverRef) {
360
+ this.#popoverRef.removeEventListener('click', this.#onPopoverClick);
361
+ this.#popoverRef.removeEventListener('keydown', this.#onPopoverKey);
362
+ this.#popoverRef.hidePopover?.();
363
+ }
364
+ if (this.#calFromRef) this.#calFromRef.removeEventListener('change', this.#onCalFromChange);
365
+ if (this.#calToRef) this.#calToRef.removeEventListener('change', this.#onCalToChange);
366
+ document.removeEventListener('pointerdown', this.#onOutside);
367
+ document.removeEventListener('keydown', this.#onDocKey);
368
+ this.#anchorCleanup?.();
369
+ this.#anchorCleanup = null;
370
+ this.#bound = false;
371
+ this.#popoverShown = false;
372
+ this.#triggerRef = null;
373
+ this.#popoverRef = null;
374
+ this.#presetRailRef = null;
375
+ this.#calFromRef = null;
376
+ this.#calToRef = null;
377
+ }
378
+
379
+ #ensureParts() {
380
+ // Trigger — light DOM. Default <button-ui> stamped unless author
381
+ // supplied [slot="trigger"].
382
+ this.#triggerRef = this.ensure('trigger');
383
+ // Popover is a div hosted in light DOM but lifted to the top layer
384
+ // via the Popover API on showPopover().
385
+ this.#popoverRef = this.ensure('popover');
386
+ if (this.id) this.#popoverRef.setAttribute('aria-labelledby', this.id);
387
+ // Calendar area lives inside the popover.
388
+ if (!this.#popoverRef.querySelector(':scope > [data-calendar-area]')) {
389
+ const calArea = this.constructor._pp.calArea.cloneNode(true);
390
+ this.#popoverRef.appendChild(calArea);
391
+ }
392
+ // Preset rail (unless author supplied [slot="presets"] OR no-presets is set).
393
+ const authorPresets = this.querySelector(':scope > [slot="presets"]');
394
+ if (authorPresets && authorPresets.parentElement !== this.#popoverRef) {
395
+ this.#popoverRef.appendChild(authorPresets);
396
+ this.#presetRailRef = authorPresets;
397
+ } else if (!this.noPresets && !authorPresets) {
398
+ if (!this.#popoverRef.querySelector(':scope > [data-preset-rail]')) {
399
+ const rail = this.constructor._pp.presets.cloneNode(true);
400
+ this.#popoverRef.insertBefore(rail, this.#popoverRef.querySelector(':scope > [data-calendar-area]'));
401
+ }
402
+ this.#presetRailRef = this.#popoverRef.querySelector(':scope > [data-preset-rail]');
403
+ }
404
+ // Two calendar panes — start + end.
405
+ const calArea = this.#popoverRef.querySelector(':scope > [data-calendar-area]');
406
+ if (!calArea.querySelector(':scope > [data-cal-from]')) {
407
+ const calFrom = this.constructor._pp.calFrom.cloneNode(true);
408
+ calArea.appendChild(calFrom);
409
+ }
410
+ if (!calArea.querySelector(':scope > [data-cal-to]')) {
411
+ const calTo = this.constructor._pp.calTo.cloneNode(true);
412
+ calArea.appendChild(calTo);
413
+ }
414
+ this.#calFromRef = calArea.querySelector(':scope > [data-cal-from]');
415
+ this.#calToRef = calArea.querySelector(':scope > [data-cal-to]');
416
+ // Footer slot — author-supplied; lift into popover after calendar area.
417
+ const authorFooter = this.querySelector(':scope > [slot="footer"]');
418
+ if (authorFooter && authorFooter.parentElement !== this.#popoverRef) {
419
+ this.#popoverRef.appendChild(authorFooter);
420
+ }
421
+ }
422
+
423
+ render() {
424
+ // Mirror reactive props onto the part nodes — runs in the element's
425
+ // effect every time a tracked prop changes.
426
+ if (!this.#triggerRef) return;
427
+ this.#triggerRef.setAttribute('text', this.#displayText());
428
+ this.#triggerRef.toggleAttribute('disabled', this.disabled);
429
+ this.#triggerRef.setAttribute('aria-haspopup', 'dialog');
430
+ this.#triggerRef.setAttribute('aria-expanded', this.open ? 'true' : 'false');
431
+ this.setAttribute('aria-expanded', this.open ? 'true' : 'false');
432
+
433
+ // Cascade min/max + selection state into the calendar panes.
434
+ const range = parseRange(this.value);
435
+ const pending = this.#pending;
436
+ // Effective endpoints — pending (mid-click) takes precedence over
437
+ // value. Push the SAME from/to onto BOTH grids so each independently
438
+ // lights up its in-range cells via `[data-in-range]`. Without this,
439
+ // only the two endpoint cells got `[data-selected]` and the days
440
+ // between them rendered as plain background — the "fill the cells
441
+ // between start and end dates" trap.
442
+ const effFrom = pending?.from || range?.from || '';
443
+ const effTo = pending?.to || range?.to || '';
444
+ const setRangeAttrs = (cal) => {
445
+ if (effFrom) cal.setAttribute('range-start', effFrom);
446
+ else cal.removeAttribute('range-start');
447
+ if (effTo) cal.setAttribute('range-end', effTo);
448
+ else cal.removeAttribute('range-end');
449
+ };
450
+ if (this.#calFromRef) {
451
+ if (this.min) this.#calFromRef.setAttribute('min', this.min);
452
+ if (this.max) this.#calFromRef.setAttribute('max', this.max);
453
+ this.#calFromRef.value = pending?.from || range?.from || '';
454
+ setRangeAttrs(this.#calFromRef);
455
+ }
456
+ if (this.#calToRef) {
457
+ if (this.min) this.#calToRef.setAttribute('min', this.min);
458
+ if (this.max) this.#calToRef.setAttribute('max', this.max);
459
+ this.#calToRef.value = pending?.to || range?.to || '';
460
+ setRangeAttrs(this.#calToRef);
461
+ }
462
+
463
+ // Render the preset rail (unless overridden by [slot="presets"] or
464
+ // hidden via [no-presets]).
465
+ if (!this.querySelector(':scope > [slot="presets"]')) {
466
+ if (this.noPresets) {
467
+ this.#presetRailRef?.remove();
468
+ this.#presetRailRef = null;
469
+ } else {
470
+ if (!this.#presetRailRef) this.#ensureParts();
471
+ this.#renderPresets();
472
+ }
473
+ }
474
+
475
+ // Toggle the popover via the Popover API. We track open state
476
+ // locally so the toggle works under happy-dom (where `:popover-open`
477
+ // selector matching is unreliable) and in real browsers.
478
+ if (this.#popoverRef) {
479
+ if (this.open && !this.#popoverShown) {
480
+ if (!this.disabled && !this.readonly) {
481
+ this.#popoverShown = true;
482
+ this.#popoverRef.showPopover?.();
483
+ // §FB-Wave1-QA — anchor the popover to the trigger via the canonical
484
+ // helper. Without this, the popover renders at viewport (0,0).
485
+ // Matches select-ui + calendar-picker-ui's anchorPopover pattern.
486
+ this.#anchorCleanup?.();
487
+ this.#anchorCleanup = anchorPopover(this.#triggerRef, this.#popoverRef, {
488
+ placement: this.getAttribute('placement') || 'bottom-start',
489
+ gap: 4,
490
+ });
491
+ document.addEventListener('pointerdown', this.#onOutside);
492
+ document.addEventListener('keydown', this.#onDocKey);
493
+ // Defer focus to next microtask so the grid is paint-stable.
494
+ queueMicrotask(() => {
495
+ if (!this.open) return;
496
+ const grid = this.#calFromRef?.querySelector?.('[data-cal-day]:not([disabled]):not([data-outside])');
497
+ grid?.focus?.();
498
+ });
499
+ } else {
500
+ // Disabled or readonly — refuse to open.
501
+ this.open = false;
502
+ }
503
+ } else if (!this.open && this.#popoverShown) {
504
+ this.#popoverShown = false;
505
+ this.#anchorCleanup?.();
506
+ this.#anchorCleanup = null;
507
+ this.#popoverRef.hidePopover?.();
508
+ document.removeEventListener('pointerdown', this.#onOutside);
509
+ document.removeEventListener('keydown', this.#onDocKey);
510
+ // Return focus to the trigger.
511
+ this.#triggerRef?.focus?.();
512
+ }
513
+ }
514
+ }
515
+
516
+ // ── Trigger text formatter ────────────────────────────────────────
517
+
518
+ #displayText() {
519
+ const range = parseRange(this.value);
520
+ if (!range) return this.placeholder;
521
+ const from = formatDate(range.from, this.format);
522
+ const to = formatDate(range.to, this.format);
523
+ if (!from || !to) return this.placeholder;
524
+ return `${from} → ${to}`;
525
+ }
526
+
527
+ // ── Preset rail rendering ─────────────────────────────────────────
528
+
529
+ #renderPresets() {
530
+ if (!this.#presetRailRef) return;
531
+ // Wipe + re-stamp; preset cardinality is small (≤10).
532
+ this.#presetRailRef.replaceChildren();
533
+ for (const preset of this.presets) {
534
+ const btn = document.createElement('button-ui');
535
+ btn.setAttribute('variant', 'ghost');
536
+ btn.setAttribute('type', 'button');
537
+ btn.setAttribute('text', preset.label);
538
+ btn.setAttribute('size', 'sm');
539
+ btn.dataset.presetLabel = preset.label;
540
+ this.#presetRailRef.appendChild(btn);
541
+ }
542
+ // Comparison cluster (when [comparison] is set).
543
+ if (this.comparison) {
544
+ const divider = document.createElement('divider-ui');
545
+ divider.dataset.presetDivider = '';
546
+ this.#presetRailRef.appendChild(divider);
547
+ for (const preset of defaultComparisonPresets()) {
548
+ const btn = document.createElement('button-ui');
549
+ btn.setAttribute('variant', 'ghost');
550
+ btn.setAttribute('type', 'button');
551
+ btn.setAttribute('text', preset.label);
552
+ btn.setAttribute('size', 'sm');
553
+ btn.dataset.comparisonPresetLabel = preset.label;
554
+ this.#presetRailRef.appendChild(btn);
555
+ }
556
+ }
557
+ }
558
+
559
+ // ── Event handlers ────────────────────────────────────────────────
560
+
561
+ // Flag set by #onTriggerKey to mark the *next* click as keyboard-originated.
562
+ // <button-ui> calls `this.click()` from its own Enter/Space keydown handler
563
+ // (see button/button.class.js:148), so the click that follows a keyboard press is
564
+ // synthetic. We use this flag to disambiguate the `trigger:` field on the
565
+ // `open` event.
566
+ #keyboardOpen = false;
567
+
568
+ #onTriggerClick = (e) => {
569
+ if (this.disabled) return;
570
+ if (this.readonly) return;
571
+ e.stopPropagation();
572
+ const fromKeyboard = this.#keyboardOpen;
573
+ this.#keyboardOpen = false;
574
+ if (this.open) {
575
+ this.closePopover('outside');
576
+ } else {
577
+ this.#previousFocus = document.activeElement;
578
+ this.open = true;
579
+ this.dispatchEvent(new CustomEvent('open', {
580
+ bubbles: true,
581
+ detail: { trigger: fromKeyboard ? 'keyboard' : 'click' },
582
+ }));
583
+ }
584
+ };
585
+
586
+ #onTriggerKey = (e) => {
587
+ if (this.disabled) return;
588
+ if (e.key === 'Enter' || e.key === ' ') {
589
+ if (this.readonly) {
590
+ e.preventDefault();
591
+ return;
592
+ }
593
+ // Flag the synthetic click that <button-ui>'s own keydown handler
594
+ // will fire via `this.click()` — see comment on #keyboardOpen.
595
+ this.#keyboardOpen = true;
596
+ }
597
+ };
598
+
599
+ #onPopoverClick = (e) => {
600
+ if (this.readonly) {
601
+ // Block preset / day clicks but allow the calendar widgets to
602
+ // remain focusable for screen-reader inspection.
603
+ const presetBtn = e.target.closest('[data-preset-label],[data-comparison-preset-label]');
604
+ const dayBtn = e.target.closest('[data-cal-day]');
605
+ if (presetBtn || dayBtn) {
606
+ e.stopPropagation();
607
+ e.preventDefault();
608
+ return;
609
+ }
610
+ }
611
+ const presetBtn = e.target.closest('[data-preset-label]');
612
+ if (presetBtn) {
613
+ e.stopPropagation();
614
+ this.applyPreset(presetBtn.dataset.presetLabel);
615
+ this.closePopover('apply');
616
+ return;
617
+ }
618
+ const cmpPresetBtn = e.target.closest('[data-comparison-preset-label]');
619
+ if (cmpPresetBtn) {
620
+ e.stopPropagation();
621
+ // For v1, comparison-presets are tagged but compute their range
622
+ // from the current primary range. If no primary is set, no-op.
623
+ const primary = parseRange(this.value);
624
+ if (!primary) return;
625
+ const cmp = this.#computeComparisonRange(cmpPresetBtn.dataset.comparisonPresetLabel, primary);
626
+ if (cmp) this.rangeCompareValue = cmp;
627
+ return;
628
+ }
629
+ };
630
+
631
+ #onPopoverKey = (e) => {
632
+ if (e.key === 'Escape') {
633
+ e.preventDefault();
634
+ e.stopPropagation();
635
+ this.closePopover('escape');
636
+ return;
637
+ }
638
+ if (e.key === 'Tab') {
639
+ // Trap focus inside the popover. Build the tab-cycle by querying
640
+ // focusable descendants of the popover; the trigger is *outside*
641
+ // the popover and Tab cycles inside until Escape.
642
+ const focusables = this.#getFocusables();
643
+ if (focusables.length === 0) return;
644
+ const idx = focusables.indexOf(document.activeElement);
645
+ if (e.shiftKey) {
646
+ if (idx <= 0) {
647
+ e.preventDefault();
648
+ focusables[focusables.length - 1].focus();
649
+ }
650
+ } else {
651
+ if (idx === -1 || idx === focusables.length - 1) {
652
+ e.preventDefault();
653
+ focusables[0].focus();
654
+ }
655
+ }
656
+ }
657
+ };
658
+
659
+ #onDocKey = (e) => {
660
+ // Belt-and-braces: Escape closes even if focus is outside the popover
661
+ // (e.g., the trigger).
662
+ if (e.key === 'Escape' && this.open) {
663
+ this.closePopover('escape');
664
+ }
665
+ };
666
+
667
+ #onCalFromChange = (e) => {
668
+ if (this.readonly) return;
669
+ e.stopPropagation();
670
+ const iso = e.detail?.value || this.#calFromRef?.value || '';
671
+ if (!iso) return;
672
+ const existing = parseRange(this.value);
673
+ if (!this.#pending) this.#pending = { from: '', to: '' };
674
+ this.#pending.from = iso;
675
+ // Live emit `input` for pending state.
676
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: iso, to: null } } }));
677
+ // Attempt commit when both halves are set (in either order). The
678
+ // commit path handles reversed-range invalidation.
679
+ const toCandidate = this.#pending.to || existing?.to || null;
680
+ if (toCandidate) {
681
+ this.#commitRange({ from: iso, to: toCandidate });
682
+ }
683
+ };
684
+
685
+ #onCalToChange = (e) => {
686
+ if (this.readonly) return;
687
+ e.stopPropagation();
688
+ const iso = e.detail?.value || this.#calToRef?.value || '';
689
+ if (!iso) return;
690
+ const existing = parseRange(this.value);
691
+ if (!this.#pending) this.#pending = { from: '', to: '' };
692
+ this.#pending.to = iso;
693
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: this.#pending.from || existing?.from || null, to: iso } } }));
694
+ const fromCandidate = this.#pending.from || existing?.from || null;
695
+ if (fromCandidate) {
696
+ this.#commitRange({ from: fromCandidate, to: iso });
697
+ }
698
+ };
699
+
700
+ #onOutside = (e) => {
701
+ if (!this.open) return;
702
+ // Click inside the host or the popover doesn't close.
703
+ if (this.contains(e.target)) return;
704
+ if (this.#popoverRef && e.composedPath?.().includes(this.#popoverRef)) return;
705
+ this.closePopover('outside');
706
+ };
707
+
708
+ // ── Commit + invalid emission ─────────────────────────────────────
709
+
710
+ #commitRange(range, extras = {}) {
711
+ if (!range || !range.from || !range.to) return;
712
+ if (!rangeIsOrdered(range)) {
713
+ this.dispatchEvent(new CustomEvent('invalid', {
714
+ bubbles: true,
715
+ detail: { value: range, reason: 'reversed' },
716
+ }));
717
+ return;
718
+ }
719
+ if (this.min && range.from < this.min) {
720
+ this.dispatchEvent(new CustomEvent('invalid', {
721
+ bubbles: true,
722
+ detail: { value: range, reason: 'below-min' },
723
+ }));
724
+ return;
725
+ }
726
+ if (this.max && range.to > this.max) {
727
+ this.dispatchEvent(new CustomEvent('invalid', {
728
+ bubbles: true,
729
+ detail: { value: range, reason: 'above-max' },
730
+ }));
731
+ return;
732
+ }
733
+ const serialized = JSON.stringify({ from: range.from, to: range.to });
734
+ this.value = serialized;
735
+ this.#pending = null;
736
+ this.syncValue(serialized);
737
+ this.dispatchEvent(new CustomEvent('change', {
738
+ bubbles: true,
739
+ detail: {
740
+ value: { from: range.from, to: range.to },
741
+ compareValue: parseRange(this.compareValue),
742
+ presetLabel: extras.presetLabel || null,
743
+ },
744
+ }));
745
+ }
746
+
747
+ #computeComparisonRange(label, primary) {
748
+ const from = parseISO(primary.from);
749
+ const to = parseISO(primary.to);
750
+ if (!from || !to) return null;
751
+ const days = Math.round((to - from) / 86400000) + 1;
752
+ switch (label) {
753
+ case 'Previous period': {
754
+ const cmpTo = new Date(from);
755
+ cmpTo.setDate(cmpTo.getDate() - 1);
756
+ const cmpFrom = new Date(cmpTo);
757
+ cmpFrom.setDate(cmpFrom.getDate() - (days - 1));
758
+ return { from: toISO(cmpFrom), to: toISO(cmpTo) };
759
+ }
760
+ case 'Same period last year': {
761
+ const cmpFrom = new Date(from);
762
+ cmpFrom.setFullYear(cmpFrom.getFullYear() - 1);
763
+ const cmpTo = new Date(to);
764
+ cmpTo.setFullYear(cmpTo.getFullYear() - 1);
765
+ return { from: toISO(cmpFrom), to: toISO(cmpTo) };
766
+ }
767
+ case 'Same period last month': {
768
+ const cmpFrom = new Date(from);
769
+ cmpFrom.setMonth(cmpFrom.getMonth() - 1);
770
+ const cmpTo = new Date(to);
771
+ cmpTo.setMonth(cmpTo.getMonth() - 1);
772
+ return { from: toISO(cmpFrom), to: toISO(cmpTo) };
773
+ }
774
+ default:
775
+ return null;
776
+ }
777
+ }
778
+
779
+ #getFocusables() {
780
+ if (!this.#popoverRef) return [];
781
+ const SEL = [
782
+ 'button-ui:not([disabled])',
783
+ 'button:not([disabled])',
784
+ 'calendar-grid-ui:not([disabled])',
785
+ '[data-cal-day]:not([disabled]):not([data-outside])',
786
+ '[tabindex="0"]',
787
+ ].join(',');
788
+ return Array.from(this.#popoverRef.querySelectorAll(SEL))
789
+ .filter((el) => el.offsetParent !== null || el.matches(':popover-open *'));
790
+ }
791
+ }