@kubex/zinc 1.1.185 → 2.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 (358) hide show
  1. package/.browserslistrc +2 -0
  2. package/.github/workflows/js_build_and_deploy.yaml +54 -1
  3. package/.github/workflows/test.yaml +27 -0
  4. package/dist/chunks/zn.UWJLIXDT.js +1 -0
  5. package/dist/custom-elements.json +10319 -2666
  6. package/dist/vscode.html-custom-data.json +1012 -57
  7. package/dist/web-types.json +2097 -145
  8. package/dist/zn.d.ts +2294 -416
  9. package/dist/zn.min.css +1 -1
  10. package/dist/zn.min.js +2310 -1637
  11. package/docs/_includes/default.njk +4 -1
  12. package/docs/assets/scripts/docs.js +22 -6
  13. package/docs/assets/styles/docs.css +9 -2
  14. package/docs/data/report-table-day-compare.json +380 -0
  15. package/docs/data/report-table.json +314 -0
  16. package/docs/pages/components/avatar-group.md +57 -0
  17. package/docs/pages/components/badge.md +67 -0
  18. package/docs/pages/components/button.md +41 -1
  19. package/docs/pages/components/chat-message.md +16 -0
  20. package/docs/pages/components/checkbox.md +1 -1
  21. package/docs/pages/components/chip.md +25 -13
  22. package/docs/pages/components/color-picker.md +37 -0
  23. package/docs/pages/components/copy-button.md +2 -2
  24. package/docs/pages/components/data-table-filter.md +33 -50
  25. package/docs/pages/components/data-table-search.md +4 -4
  26. package/docs/pages/components/data-table.md +27 -3
  27. package/docs/pages/components/date-range.md +116 -0
  28. package/docs/pages/components/dialog.md +29 -37
  29. package/docs/pages/components/diff-row.md +28 -0
  30. package/docs/pages/components/diff.md +46 -0
  31. package/docs/pages/components/editor.md +55 -0
  32. package/docs/pages/components/icon.md +46 -1
  33. package/docs/pages/components/input.md +30 -11
  34. package/docs/pages/components/item.md +35 -0
  35. package/docs/pages/components/key-value.md +25 -0
  36. package/docs/pages/components/menu.md +74 -7
  37. package/docs/pages/components/nav-item.md +15 -0
  38. package/docs/pages/components/nav-list.md +132 -0
  39. package/docs/pages/components/navbar.md +100 -1
  40. package/docs/pages/components/page.md +242 -74
  41. package/docs/pages/components/priority-list.md +43 -5
  42. package/docs/pages/components/query-builder.md +23 -0
  43. package/docs/pages/components/radio-group.md +33 -1
  44. package/docs/pages/components/radio.md +1 -1
  45. package/docs/pages/components/report-table.md +202 -0
  46. package/docs/pages/components/stat.md +30 -0
  47. package/docs/pages/components/tile.md +11 -0
  48. package/docs/pages/components/tooltip.md +1 -1
  49. package/docs/pages/components/translation-grid.md +86 -0
  50. package/docs/pages/components/vertical-stepper.md +108 -64
  51. package/package.json +2 -1
  52. package/scss/_global-spacing.scss +2 -2
  53. package/scss/_root.scss +11 -171
  54. package/scss/boot.scss +14 -0
  55. package/scss/mixins/_helper-mixins.scss +24 -2
  56. package/scss/mixins/_motion-mixins.scss +126 -0
  57. package/scss/mixins/_spacing-mixins.scss +4 -3
  58. package/scss/mixins/_tab-indicator-mixins.scss +12 -0
  59. package/scss/mixins/_typography-mixins.scss +1 -1
  60. package/scss/mixins/index.scss +14 -0
  61. package/scss/shared/_base.scss +32 -1
  62. package/scss/shared/_button-mixin.scss +7 -5
  63. package/scss/shared/_table.scss +35 -4
  64. package/scss/shared/nav.scss +4 -0
  65. package/scss/shared/shimmer.scss +1 -1
  66. package/scss/table-controls.scss +206 -0
  67. package/scss/themes/_dark.scss +1 -3
  68. package/scss/themes/_index.scss +1 -1
  69. package/scss/themes/_light.scss +46 -6
  70. package/scss/themes/_modes.scss +11 -11
  71. package/scss/themes/_openbyte.scss +944 -0
  72. package/scss/themes/_typography.scss +4 -2
  73. package/scss/variables/index.scss +0 -9
  74. package/src/components/alert/alert.component.ts +14 -4
  75. package/src/components/alert/alert.scss +55 -69
  76. package/src/components/alert/alert.test.ts +8 -0
  77. package/src/components/animated-button/animated-button.component.ts +3 -2
  78. package/src/components/animated-button/animated-button.scss +6 -23
  79. package/src/components/animated-button/animated-button.test.ts +4 -3
  80. package/src/components/avatar-group/avatar-group.component.ts +84 -0
  81. package/src/components/avatar-group/avatar-group.scss +54 -0
  82. package/src/components/avatar-group/avatar-group.test.ts +59 -0
  83. package/src/components/avatar-group/index.ts +12 -0
  84. package/src/components/badge/badge.component.ts +49 -0
  85. package/src/components/badge/badge.scss +80 -0
  86. package/src/components/badge/badge.test.ts +34 -0
  87. package/src/components/badge/index.ts +12 -0
  88. package/src/components/bulk-actions/bulk-actions.component.ts +14 -2
  89. package/src/components/button/button.component.ts +228 -8
  90. package/src/components/button/button.scss +115 -32
  91. package/src/components/button/button.test.ts +122 -0
  92. package/src/components/button-menu/button-menu.component.ts +32 -11
  93. package/src/components/button-menu/button-menu.scss +5 -1
  94. package/src/components/channel-tile/channel-tile.component.ts +34 -27
  95. package/src/components/channel-tile/channel-tile.scss +76 -43
  96. package/src/components/chart/builders.test.ts +46 -0
  97. package/src/components/chart/builders.ts +27 -3
  98. package/src/components/chart/chart-live.test.ts +4 -6
  99. package/src/components/chart/chart.component.ts +125 -5
  100. package/src/components/chart/chart.scss +121 -0
  101. package/src/components/chart/chart.test.ts +81 -0
  102. package/src/components/chat-message/chat-message.component.ts +21 -1
  103. package/src/components/chat-message/chat-message.scss +13 -2
  104. package/src/components/chat-message/chat-message.test.ts +9 -0
  105. package/src/components/checkbox/checkbox.component.ts +61 -27
  106. package/src/components/checkbox/checkbox.scss +58 -25
  107. package/src/components/checkbox-group/checkbox-group.component.ts +7 -1
  108. package/src/components/chip/chip.component.ts +11 -1
  109. package/src/components/chip/chip.scss +91 -26
  110. package/src/components/chip/chip.test.ts +17 -0
  111. package/src/components/collapsible/collapsible.component.ts +17 -3
  112. package/src/components/collapsible/collapsible.scss +19 -10
  113. package/src/components/color-picker/color-picker.component.ts +277 -0
  114. package/src/components/color-picker/color-picker.scss +141 -0
  115. package/src/components/color-picker/color-picker.test.ts +57 -0
  116. package/src/components/color-picker/index.ts +12 -0
  117. package/src/components/cols/cols.component.ts +10 -0
  118. package/src/components/cols/cols.test.ts +17 -0
  119. package/src/components/confirm/confirm.component.ts +20 -6
  120. package/src/components/confirm/confirm.test.ts +52 -0
  121. package/src/components/content-block/content-block.component.ts +38 -9
  122. package/src/components/content-block/content-block.scss +11 -5
  123. package/src/components/content-block/content-block.test.ts +26 -0
  124. package/src/components/copy-button/copy-button.component.ts +2 -2
  125. package/src/components/copy-button/copy-button.scss +17 -4
  126. package/src/components/data-select/data-select.component.ts +3 -1
  127. package/src/components/data-table/data-table.component.ts +362 -140
  128. package/src/components/data-table/data-table.scss +136 -60
  129. package/src/components/data-table/data-table.test.ts +356 -4
  130. package/src/components/data-table-filter/data-table-filter.component.ts +426 -406
  131. package/src/components/data-table-filter/data-table-filter.scss +176 -148
  132. package/src/components/data-table-filter/data-table-filter.test.ts +271 -273
  133. package/src/components/data-table-search/data-table-search.component.ts +4 -2
  134. package/src/components/data-table-search/data-table-search.scss +8 -1
  135. package/src/components/data-table-search/data-table-search.test.ts +29 -0
  136. package/src/components/date-range/date-range-math.test.ts +78 -0
  137. package/src/components/date-range/date-range-math.ts +105 -0
  138. package/src/components/date-range/date-range.component.ts +375 -0
  139. package/src/components/date-range/date-range.scss +272 -0
  140. package/src/components/date-range/date-range.test.ts +247 -0
  141. package/src/components/date-range/index.ts +12 -0
  142. package/src/components/datepicker/datepicker.component.ts +44 -7
  143. package/src/components/datepicker/datepicker.scss +9 -0
  144. package/src/components/datepicker/datepicker.test.ts +2 -2
  145. package/src/components/dialog/dialog.component.ts +38 -19
  146. package/src/components/dialog/dialog.scss +65 -36
  147. package/src/components/dialog/dialog.test.ts +14 -0
  148. package/src/components/diff/diff.component.ts +55 -0
  149. package/src/components/diff/diff.scss +46 -0
  150. package/src/components/diff/diff.test.ts +15 -0
  151. package/src/components/diff/index.ts +12 -0
  152. package/src/components/diff-row/diff-row.component.ts +75 -0
  153. package/src/components/diff-row/diff-row.scss +70 -0
  154. package/src/components/diff-row/diff-row.test.ts +37 -0
  155. package/src/components/diff-row/index.ts +12 -0
  156. package/src/components/dropdown/dropdown.component.ts +12 -10
  157. package/src/components/dropdown/dropdown.scss +2 -0
  158. package/src/components/editor/editor.component.ts +159 -11
  159. package/src/components/editor/editor.scss +250 -1
  160. package/src/components/editor/editor.test.ts +31 -3
  161. package/src/components/editor/modules/ai/panel/ai-panel.scss +0 -1
  162. package/src/components/editor/modules/attachment/attachment.ts +53 -31
  163. package/src/components/editor/modules/context-menu/context-menu.ts +17 -0
  164. package/src/components/editor/modules/dialog/dialog.component.ts +23 -1
  165. package/src/components/editor/modules/dialog/dialog.test.ts +28 -0
  166. package/src/components/editor/modules/emoji/emoji.ts +4 -5
  167. package/src/components/editor/modules/merge-field/merge-field.ts +83 -0
  168. package/src/components/editor/modules/toolbar/toolbar.component.ts +121 -68
  169. package/src/components/editor/modules/toolbar/toolbar.scss +38 -0
  170. package/src/components/editor/modules/toolbar/toolbar.ts +25 -1
  171. package/src/components/empty-state/empty-state.component.ts +6 -1
  172. package/src/components/empty-state/empty-state.scss +21 -4
  173. package/src/components/expanding-action/expanding-action.component.ts +22 -34
  174. package/src/components/expanding-action/expanding-action.scss +3 -45
  175. package/src/components/expanding-action/expanding-action.test.ts +2 -1
  176. package/src/components/file/file.component.ts +30 -2
  177. package/src/components/file/file.scss +6 -0
  178. package/src/components/flow-builder/flow-builder.component.ts +36 -3
  179. package/src/components/flow-builder/flow-builder.scss +0 -6
  180. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +8 -0
  181. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +10 -5
  182. package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +17 -0
  183. package/src/components/flow-builder/modules/flow-node/flow-node.scss +10 -0
  184. package/src/components/form-actions/form-actions.component.ts +48 -2
  185. package/src/components/form-actions/form-actions.scss +1 -1
  186. package/src/components/form-group/form-group.component.ts +10 -3
  187. package/src/components/form-group/form-group.scss +37 -4
  188. package/src/components/form-group/form-group.test.ts +10 -0
  189. package/src/components/header/header.component.ts +11 -4
  190. package/src/components/header/header.scss +47 -5
  191. package/src/components/hover-container/hover-container.component.ts +4 -5
  192. package/src/components/icon/icon.component.ts +95 -9
  193. package/src/components/icon/icon.scss +70 -6
  194. package/src/components/icon/icon.test.ts +55 -0
  195. package/src/components/icon-picker/icon-picker.component.ts +35 -1
  196. package/src/components/inline-edit/inline-edit.component.ts +38 -8
  197. package/src/components/inline-edit/inline-edit.scss +128 -19
  198. package/src/components/input/input.component.ts +173 -297
  199. package/src/components/input/input.scss +65 -43
  200. package/src/components/input/input.test.ts +86 -4
  201. package/src/components/item/item.component.ts +12 -0
  202. package/src/components/item/item.scss +102 -3
  203. package/src/components/item/item.test.ts +23 -0
  204. package/src/components/key/key.component.ts +2 -2
  205. package/src/components/key/key.scss +11 -0
  206. package/src/components/key-value/index.ts +12 -0
  207. package/src/components/key-value/key-value.component.ts +160 -0
  208. package/src/components/key-value/key-value.scss +20 -0
  209. package/src/components/key-value/key-value.test.ts +22 -0
  210. package/src/components/markdown-editor/markdown-editor.component.ts +8 -1
  211. package/src/components/menu/menu.component.ts +21 -2
  212. package/src/components/menu/menu.scss +43 -23
  213. package/src/components/menu/menu.test.ts +33 -0
  214. package/src/components/menu-item/menu-item.component.ts +13 -2
  215. package/src/components/menu-item/menu-item.scss +56 -23
  216. package/src/components/menu-item/menu-item.test.ts +31 -0
  217. package/src/components/menu-item/submenu-controller.ts +25 -15
  218. package/src/components/menu-label/index.ts +12 -0
  219. package/src/components/menu-label/menu-label.component.ts +30 -0
  220. package/src/components/menu-label/menu-label.scss +18 -0
  221. package/src/components/nav-item/index.ts +12 -0
  222. package/src/components/nav-item/nav-item.component.ts +83 -0
  223. package/src/components/nav-item/nav-item.scss +128 -0
  224. package/src/components/nav-list/index.ts +12 -0
  225. package/src/components/nav-list/nav-list.component.ts +313 -0
  226. package/src/components/nav-list/nav-list.scss +87 -0
  227. package/src/components/nav-list/nav-list.test.ts +250 -0
  228. package/src/components/navbar/navbar.component.ts +235 -171
  229. package/src/components/navbar/navbar.scss +333 -169
  230. package/src/components/navbar/navbar.test.ts +63 -143
  231. package/src/components/page/page.component.ts +114 -29
  232. package/src/components/page/page.scss +223 -95
  233. package/src/components/page/page.test.ts +214 -3
  234. package/src/components/page-builder/page-builder.component.ts +51 -5
  235. package/src/components/page-builder/page-builder.scss +6 -1
  236. package/src/components/page-nav/page-nav.component.ts +25 -3
  237. package/src/components/page-nav/page-nav.scss +10 -2
  238. package/src/components/pagination/pagination.scss +1 -1
  239. package/src/components/panel/panel.component.ts +14 -13
  240. package/src/components/panel/panel.scss +14 -10
  241. package/src/components/panel/panel.test.ts +42 -0
  242. package/src/components/popup/popup.component.ts +20 -13
  243. package/src/components/popup/popup.test.ts +21 -1
  244. package/src/components/priority-list/priority-list.component.ts +157 -172
  245. package/src/components/priority-list/priority-list.scss +113 -17
  246. package/src/components/priority-list/priority-list.test.ts +301 -3
  247. package/src/components/progress-bar/progress-bar.component.ts +1 -1
  248. package/src/components/progress-bar/progress-bar.scss +16 -4
  249. package/src/components/progress-tile/progress-tile.scss +1 -1
  250. package/src/components/query-builder/query-builder.component.ts +85 -10
  251. package/src/components/query-builder/query-builder.scss +7 -1
  252. package/src/components/query-builder/query-builder.test.ts +40 -0
  253. package/src/components/radio/radio.component.ts +57 -18
  254. package/src/components/radio/radio.scss +101 -26
  255. package/src/components/radio-group/radio-group.component.ts +45 -7
  256. package/src/components/radio-group/radio-group.scss +17 -0
  257. package/src/components/radio-group/radio-group.test.ts +63 -2
  258. package/src/components/rating/rating.component.ts +23 -0
  259. package/src/components/rating/rating.scss +1 -1
  260. package/src/components/remarkd-editor/remarkd-editor.component.ts +60 -5
  261. package/src/components/remarkd-editor/remarkd-editor.scss +12 -4
  262. package/src/components/report-table/index.ts +12 -0
  263. package/src/components/report-table/report-format.test.ts +86 -0
  264. package/src/components/report-table/report-format.ts +72 -0
  265. package/src/components/report-table/report-math.test.ts +74 -0
  266. package/src/components/report-table/report-math.ts +252 -0
  267. package/src/components/report-table/report-table.component.ts +681 -0
  268. package/src/components/report-table/report-table.scss +328 -0
  269. package/src/components/report-table/report-table.test.ts +702 -0
  270. package/src/components/reveal/reveal.component.ts +15 -1
  271. package/src/components/reveal/reveal.scss +5 -0
  272. package/src/components/schedule-builder/schedule-builder.component.ts +23 -8
  273. package/src/components/scroll-container/scroll-container.component.ts +19 -1
  274. package/src/components/scroll-container/scroll-container.scss +6 -1
  275. package/src/components/select/select.component.ts +52 -36
  276. package/src/components/select/select.scss +42 -25
  277. package/src/components/select/select.test.ts +14 -2
  278. package/src/components/settings-container/settings-container.scss +18 -5
  279. package/src/components/sidebar/sidebar.component.ts +8 -1
  280. package/src/components/sidebar/sidebar.scss +6 -1
  281. package/src/components/skeleton/skeleton.component.ts +3 -3
  282. package/src/components/skeleton/skeleton.scss +6 -6
  283. package/src/components/skeleton/skeleton.test.ts +9 -0
  284. package/src/components/slash-menu/slash-menu.scss +1 -1
  285. package/src/components/slideout/slideout.component.ts +20 -3
  286. package/src/components/slideout/slideout.scss +13 -6
  287. package/src/components/slideout/slideout.test.ts +19 -0
  288. package/src/components/split-button/split-button.component.ts +1 -1
  289. package/src/components/split-pane/split-pane.component.ts +71 -8
  290. package/src/components/split-pane/split-pane.scss +145 -41
  291. package/src/components/split-pane/split-pane.test.ts +23 -3
  292. package/src/components/stat/stat.component.ts +209 -14
  293. package/src/components/stat/stat.scss +305 -5
  294. package/src/components/stat/stat.test.ts +143 -2
  295. package/src/components/status-indicator/status-indicator.scss +22 -21
  296. package/src/components/stepper/stepper.scss +3 -3
  297. package/src/components/tab/tab.component.ts +1 -0
  298. package/src/components/table/table.component.ts +12 -1
  299. package/src/components/table/table.scss +21 -13
  300. package/src/components/table/table.test.ts +17 -0
  301. package/src/components/tabs/tabs.component.ts +150 -37
  302. package/src/components/tabs/tabs.scss +48 -7
  303. package/src/components/tabs/tabs.test.ts +169 -0
  304. package/src/components/textarea/textarea.component.ts +25 -3
  305. package/src/components/textarea/textarea.scss +10 -0
  306. package/src/components/textarea/textarea.test.ts +8 -4
  307. package/src/components/theme-editor/theme-editor.scss +0 -6
  308. package/src/components/tile/tile.component.ts +32 -2
  309. package/src/components/tile/tile.scss +40 -7
  310. package/src/components/tile/tile.test.ts +10 -0
  311. package/src/components/toggle/toggle.component.ts +23 -7
  312. package/src/components/toggle/toggle.scss +36 -29
  313. package/src/components/toggle/toggle.test.ts +17 -0
  314. package/src/components/tooltip/tooltip.component.ts +3 -0
  315. package/src/components/tooltip/tooltip.scss +11 -6
  316. package/src/components/tooltip/tooltip.test.ts +12 -0
  317. package/src/components/translation-field/index.ts +12 -0
  318. package/src/components/translation-field/translation-field.component.ts +55 -0
  319. package/src/components/translation-field/translation-field.scss +101 -0
  320. package/src/components/translation-fields/index.ts +12 -0
  321. package/src/components/translation-fields/translation-fields.component.ts +80 -0
  322. package/src/components/translation-fields/translation-fields.scss +60 -0
  323. package/src/components/translation-grid/index.ts +12 -0
  324. package/src/components/translation-grid/translation-grid.component.ts +157 -0
  325. package/src/components/translation-grid/translation-grid.scss +138 -0
  326. package/src/components/translation-grid/translation-grid.test.ts +28 -0
  327. package/src/components/vertical-stepper/vertical-stepper.component.ts +35 -9
  328. package/src/components/vertical-stepper/vertical-stepper.scss +110 -39
  329. package/src/components/vertical-stepper/vertical-stepper.test.ts +35 -0
  330. package/src/components/well/well.scss +2 -2
  331. package/src/events/events.ts +2 -0
  332. package/src/events/zn-chart-click.ts +13 -0
  333. package/src/events/zn-report-change.ts +7 -0
  334. package/src/form-control.scss +36 -3
  335. package/src/internal/anchored-popover.ts +175 -0
  336. package/src/internal/animate.ts +19 -5
  337. package/src/internal/cosmic-border.ts +15 -0
  338. package/src/internal/form.ts +1 -1
  339. package/src/internal/hover-intent.ts +15 -0
  340. package/src/internal/input-modality.ts +15 -0
  341. package/src/internal/pointer-intent.ts +37 -0
  342. package/src/internal/reorder.ts +632 -0
  343. package/src/internal/scroll.ts +51 -3
  344. package/src/internal/selection-card.ts +1 -1
  345. package/src/internal/tab-indicator.ts +92 -0
  346. package/src/internal/table-sort.ts +25 -0
  347. package/src/internal/table-tabs.ts +108 -0
  348. package/src/internal/test-drag.ts +35 -0
  349. package/src/internal/theme-openbyte.test.ts +126 -0
  350. package/src/internal/theme.ts +4 -4
  351. package/src/internal/zinc-element.ts +2 -2
  352. package/src/selection-card.scss +25 -0
  353. package/src/types/web-test-runner-commands.d.ts +1 -0
  354. package/src/utilities/color.test.ts +43 -0
  355. package/src/utilities/color.ts +189 -0
  356. package/src/zinc.ts +14 -0
  357. package/.github/workflows/eleventy_build.yml +0 -42
  358. package/scss/themes/_aura.scss +0 -114
@@ -186,6 +186,8 @@ export default class ZnDataTableSearch extends ZincElement implements ZincFormCo
186
186
  <div class="data-table-search">
187
187
  <zn-input
188
188
  type="search"
189
+ size="small"
190
+ autocomplete="off"
189
191
  name="${this.name}"
190
192
  placeholder="${this.placeholder}"
191
193
  help-text="${this.helpText}"
@@ -193,11 +195,11 @@ export default class ZnDataTableSearch extends ZincElement implements ZincFormCo
193
195
  clearable
194
196
  @zn-input="${this.handleInput}"
195
197
  @zn-clear="${this.handleClear}">
198
+ <zn-icon slot="prefix" src="search@lu" size="16"></zn-icon>
196
199
  ${this.helpText ? html`
197
200
  <zn-tooltip slot="suffix" class="data-table-search__tooltip" content="${this.helpText}">
198
- <zn-icon src="info@lu" size="20"></zn-icon>
201
+ <zn-icon src="info@lu" size="16"></zn-icon>
199
202
  </zn-tooltip>` : nothing}
200
- <zn-icon slot="suffix" src="search@lu" size="20"></zn-icon>
201
203
  </zn-input>
202
204
  <slot style="display: none"></slot>
203
205
  </div>
@@ -2,13 +2,20 @@
2
2
 
3
3
  :host {
4
4
  display: block;
5
+ min-width: 0;
5
6
  }
6
7
 
7
8
  .data-table-search {
9
+ width: 100%;
8
10
  display: flex;
9
11
  align-items: center;
10
12
  gap: var(--zn-spacing-small);
11
- min-width: 200px;
13
+ min-width: 0;
14
+ }
15
+
16
+ zn-input {
17
+ flex: 1;
18
+ min-width: 0;
12
19
  }
13
20
 
14
21
  // Keeps the focus ring inside the component's own box, so a toolbar or header that
@@ -1,6 +1,7 @@
1
1
  import '../../../dist/zn.min.js';
2
2
  import {expect, fixture, html} from '@open-wc/testing';
3
3
  import type ZnDataTableSearch from './data-table-search.component';
4
+ import type ZnInput from '../input/input.component';
4
5
  import type ZnSelect from '../select/select.component';
5
6
 
6
7
  describe('<zn-data-table-search>', () => {
@@ -32,4 +33,32 @@ describe('<zn-data-table-search>', () => {
32
33
 
33
34
  expect(el.getFormData().status).to.equal('closed');
34
35
  });
36
+
37
+ it('puts the search icon in the prefix slot, before the input', async () => {
38
+ const el = await fixture<ZnDataTableSearch>(html`<zn-data-table-search></zn-data-table-search>`);
39
+ const input = el.shadowRoot!.querySelector<HTMLElement>('zn-input')!;
40
+ const icon = input.querySelector<HTMLElement>('zn-icon[slot="prefix"]')!;
41
+ expect(icon).to.exist;
42
+ expect(input.querySelector('zn-icon[slot="suffix"]')).to.not.exist;
43
+ await (input as unknown as ZnInput).updateComplete;
44
+ const control = input.shadowRoot!.querySelector('input')!;
45
+ expect(icon.getBoundingClientRect().right).to.be.lessThanOrEqual(control.getBoundingClientRect().left + 1);
46
+ });
47
+
48
+ it('can shrink below 200px in a narrow container', async () => {
49
+ const wrap = await fixture<HTMLElement>(html`
50
+ <div style="display: flex; width: 120px"><zn-data-table-search></zn-data-table-search></div>`);
51
+ const el = wrap.querySelector<ZnDataTableSearch>('zn-data-table-search')!;
52
+ await el.updateComplete;
53
+ expect(getComputedStyle(el).minWidth).to.not.equal('200px');
54
+ expect(el.getBoundingClientRect().width).to.be.at.most(120);
55
+ });
56
+
57
+ it('turns off the browser autofill history on the native input', async () => {
58
+ const el = await fixture<ZnDataTableSearch>(html`<zn-data-table-search></zn-data-table-search>`);
59
+ const input = el.shadowRoot!.querySelector('zn-input') as unknown as ZnInput;
60
+ await input.updateComplete;
61
+
62
+ expect(input.shadowRoot!.querySelector('input')!.getAttribute('autocomplete')).to.equal('off');
63
+ });
35
64
  });
@@ -0,0 +1,78 @@
1
+ import {addMonths, clampRange, defaultPresets, isIso, monthGrid, pick, rangeLabel, todayIso} from './date-range-math';
2
+ import {expect} from '@open-wc/testing';
3
+
4
+ describe('date-range-math', () => {
5
+ it('moves by months, clamping to the shorter month', () => {
6
+ expect(addMonths('2026-01-31', 1)).to.equal('2026-02-28');
7
+ expect(addMonths('2026-03-15', -1)).to.equal('2026-02-15');
8
+ expect(addMonths('2026-12-10', 1)).to.equal('2027-01-10');
9
+ expect(addMonths('2024-02-29', 12)).to.equal('2025-02-28');
10
+ });
11
+
12
+ it('lists the eight quick ranges relative to today', () => {
13
+ const presets = defaultPresets('2026-10-20');
14
+ const byLabel = Object.fromEntries(presets.map(p => [p.label, {from: p.from, to: p.to}]));
15
+
16
+ expect(presets.map(p => p.label)).to.deep.equal([
17
+ 'Today', 'Yesterday', 'Last 7 days', 'Last 14 days', 'Last 30 days', 'This month', 'Last month', 'Last 90 days'
18
+ ]);
19
+ expect(byLabel['Today']).to.deep.equal({from: '2026-10-20', to: '2026-10-20'});
20
+ expect(byLabel['Yesterday']).to.deep.equal({from: '2026-10-19', to: '2026-10-19'});
21
+ expect(byLabel['Last 7 days']).to.deep.equal({from: '2026-10-13', to: '2026-10-20'});
22
+ expect(byLabel['Last 90 days']).to.deep.equal({from: '2026-07-22', to: '2026-10-20'});
23
+ expect(byLabel['This month']).to.deep.equal({from: '2026-10-01', to: '2026-10-20'});
24
+ expect(byLabel['Last month']).to.deep.equal({from: '2026-09-01', to: '2026-09-30'});
25
+ });
26
+
27
+ it('handles last month across a year boundary', () => {
28
+ const lastMonth = defaultPresets('2026-01-15').find(p => p.label === 'Last month');
29
+ expect(lastMonth).to.deep.include({from: '2025-12-01', to: '2025-12-31'});
30
+ });
31
+
32
+ it('builds a 42-cell Monday-first month grid', () => {
33
+ const grid = monthGrid(2026, 9);
34
+ expect(grid).to.have.length(42);
35
+ expect(grid.slice(0, 3)).to.deep.equal([null, null, null]);
36
+ expect(grid[3]).to.equal('2026-10-01');
37
+ expect(grid[33]).to.equal('2026-10-31');
38
+ expect(grid[34]).to.equal(null);
39
+ });
40
+
41
+ it('formats a range label, or a single date when both ends match', () => {
42
+ expect(rangeLabel('2026-10-19', '2026-10-20')).to.equal('19 Oct 2026 to 20 Oct 2026');
43
+ expect(rangeLabel('2026-10-20', '2026-10-20')).to.equal('20 Oct 2026');
44
+ expect(rangeLabel('2026-09-01', '2026-09-30')).to.equal('1 Sep 2026 to 30 Sep 2026');
45
+ });
46
+
47
+ it('picks a start, then an end, and restarts on an earlier day or after a full range', () => {
48
+ let state = pick({from: '', to: ''}, '2026-10-10');
49
+ expect(state).to.deep.equal({from: '2026-10-10', to: ''});
50
+
51
+ state = pick(state, '2026-10-15');
52
+ expect(state).to.deep.equal({from: '2026-10-10', to: '2026-10-15'});
53
+
54
+ state = pick(state, '2026-10-12');
55
+ expect(state).to.deep.equal({from: '2026-10-12', to: ''});
56
+
57
+ state = pick(state, '2026-10-05');
58
+ expect(state).to.deep.equal({from: '2026-10-05', to: ''});
59
+
60
+ expect(pick({from: '2026-10-05', to: ''}, '2026-10-05')).to.deep.equal({from: '2026-10-05', to: '2026-10-05'});
61
+ });
62
+
63
+ it('clamps a range to min and max, or returns null when it lies wholly outside', () => {
64
+ expect(clampRange({from: '2026-10-13', to: '2026-10-20'}, '', '2026-10-18')).to.deep.equal({from: '2026-10-13', to: '2026-10-18'});
65
+ expect(clampRange({from: '2026-10-13', to: '2026-10-20'}, '2026-10-15', '')).to.deep.equal({from: '2026-10-15', to: '2026-10-20'});
66
+ expect(clampRange({from: '2026-10-19', to: '2026-10-20'}, '', '2026-10-18')).to.equal(null);
67
+ });
68
+
69
+ it('rejects partial years a date input reports while typing', () => {
70
+ expect(isIso('0202-10-20')).to.be.false;
71
+ expect(isIso('2026-02-30')).to.be.false;
72
+ expect(isIso('2026-10-20')).to.be.true;
73
+ });
74
+
75
+ it('uses the local date for today', () => {
76
+ expect(todayIso(new Date(2026, 9, 20, 23, 59))).to.equal('2026-10-20');
77
+ });
78
+ });
@@ -0,0 +1,105 @@
1
+ export type Iso = string;
2
+
3
+ export interface DatePreset {
4
+ label: string;
5
+ from: Iso;
6
+ to: Iso;
7
+ }
8
+
9
+ export interface RangeState {
10
+ from: Iso | '';
11
+ to: Iso | '';
12
+ }
13
+
14
+ const pad = (n: number) => String(n).padStart(2, '0');
15
+
16
+ // Day arithmetic runs in UTC so DST changes can't shift a date
17
+ export function toUtc(iso: Iso): Date {
18
+ const [y, m, d] = iso.split('-').map(Number);
19
+ return new Date(Date.UTC(y, m - 1, d));
20
+ }
21
+
22
+ export function fromUtc(date: Date): Iso {
23
+ return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
24
+ }
25
+
26
+ // Years below 1000 are rejected: Chromium's date input reports partial years like 0202 while typing
27
+ export function isIso(value: unknown): value is Iso {
28
+ return typeof value === 'string' && /^[1-9]\d{3}-\d{2}-\d{2}$/.test(value) && fromUtc(toUtc(value)) === value;
29
+ }
30
+
31
+ export function addDays(iso: Iso, days: number): Iso {
32
+ const date = toUtc(iso);
33
+ date.setUTCDate(date.getUTCDate() + days);
34
+ return fromUtc(date);
35
+ }
36
+
37
+ /** The same day `months` away, clamped to the target month's last day. */
38
+ export function addMonths(iso: Iso, months: number): Iso {
39
+ const date = toUtc(iso);
40
+ const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));
41
+ const lastDay = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
42
+ target.setUTCDate(Math.min(date.getUTCDate(), lastDay));
43
+ return fromUtc(target);
44
+ }
45
+
46
+ export function daysBetween(from: Iso, to: Iso): number {
47
+ return Math.round((toUtc(to).getTime() - toUtc(from).getTime()) / 86400000);
48
+ }
49
+
50
+ export function todayIso(now: Date = new Date()): Iso {
51
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
52
+ }
53
+
54
+ export function defaultPresets(today: Iso): DatePreset[] {
55
+ const [y, m] = today.split('-').map(Number);
56
+ const lastDayPrev = fromUtc(new Date(Date.UTC(y, m - 1, 0)));
57
+ const days = (n: number) => ({label: `Last ${n} days`, from: addDays(today, -n), to: today});
58
+
59
+ return [
60
+ {label: 'Today', from: today, to: today},
61
+ {label: 'Yesterday', from: addDays(today, -1), to: addDays(today, -1)},
62
+ days(7),
63
+ days(14),
64
+ days(30),
65
+ {label: 'This month', from: `${today.slice(0, 7)}-01`, to: today},
66
+ {label: 'Last month', from: `${lastDayPrev.slice(0, 7)}-01`, to: lastDayPrev},
67
+ days(90),
68
+ ];
69
+ }
70
+
71
+ export function monthGrid(year: number, month0: number, weekStart = 1): (Iso | null)[] {
72
+ const first = new Date(Date.UTC(year, month0, 1));
73
+ const length = new Date(Date.UTC(year, month0 + 1, 0)).getUTCDate();
74
+ const lead = (first.getUTCDay() - weekStart + 7) % 7;
75
+
76
+ return Array.from({length: 42}, (_, i) => {
77
+ const day = i - lead + 1;
78
+ return day >= 1 && day <= length ? fromUtc(new Date(Date.UTC(year, month0, day))) : null;
79
+ });
80
+ }
81
+
82
+ // Fixed rather than Intl: ICU builds disagree on short months (en-GB gives "Sept" in Chromium)
83
+ const SHORT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
84
+
85
+ export function formatDate(iso: Iso): string {
86
+ const [y, m, d] = iso.split('-').map(Number);
87
+ return `${d} ${SHORT_MONTHS[m - 1]} ${y}`;
88
+ }
89
+
90
+ export function rangeLabel(from: Iso, to: Iso): string {
91
+ return from === to ? formatDate(from) : `${formatDate(from)} to ${formatDate(to)}`;
92
+ }
93
+
94
+ export function clampRange(range: {from: Iso; to: Iso}, min: Iso | '', max: Iso | ''): {from: Iso; to: Iso} | null {
95
+ const from = min && range.from < min ? min : range.from;
96
+ const to = max && range.to > max ? max : range.to;
97
+ return from <= to ? {from, to} : null;
98
+ }
99
+
100
+ export function pick(state: RangeState, day: Iso): {from: Iso; to: Iso | ''} {
101
+ if (!state.from || state.to || day < state.from) {
102
+ return {from: day, to: ''};
103
+ }
104
+ return {from: state.from, to: day};
105
+ }
@@ -0,0 +1,375 @@
1
+ import {
2
+ addDays,
3
+ addMonths,
4
+ clampRange,
5
+ type DatePreset,
6
+ daysBetween,
7
+ defaultPresets,
8
+ formatDate,
9
+ isIso,
10
+ type Iso,
11
+ monthGrid,
12
+ pick,
13
+ rangeLabel,
14
+ todayIso,
15
+ toUtc
16
+ } from './date-range-math';
17
+ import {AnchoredPopoverController} from '../../internal/anchored-popover';
18
+ import {classMap} from 'lit/directives/class-map.js';
19
+ import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from 'lit';
20
+ import {property, state} from 'lit/decorators.js';
21
+ import ZincElement from '../../internal/zinc-element';
22
+ import ZnButton from '../button';
23
+ import ZnIcon from '../icon';
24
+ import ZnInput from '../input';
25
+
26
+ import styles from './date-range.scss';
27
+
28
+ const WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
29
+ const POP_WIDTH = 736;
30
+ const NARROW = '(max-width: 560px)';
31
+ const KEY_STEPS: Record<string, number> = {ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7};
32
+
33
+ const presetsConverter = {
34
+ fromAttribute: (value: string | null): DatePreset[] | undefined => {
35
+ try {
36
+ const parsed: unknown = value ? JSON.parse(value) : undefined;
37
+ return Array.isArray(parsed) ? parsed as DatePreset[] : undefined;
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ },
42
+ toAttribute: (value: DatePreset[] | undefined) => value ? JSON.stringify(value) : null,
43
+ };
44
+
45
+ /**
46
+ * @summary A button showing a date range that opens a two-month calendar with quick ranges.
47
+ * @documentation https://zinc.style/components/date-range
48
+ * @status experimental
49
+ * @since 1.0
50
+ *
51
+ * @dependency zn-button
52
+ * @dependency zn-icon
53
+ * @dependency zn-input
54
+ *
55
+ * @event zn-change - Emitted when a range is applied, with `{from, to}` as its detail.
56
+ *
57
+ * @csspart trigger - The button that opens the picker.
58
+ * @csspart popover - The picker popover.
59
+ */
60
+ export default class ZnDateRange extends ZincElement {
61
+ static styles: CSSResultGroup = unsafeCSS(styles);
62
+ static formAssociated = true;
63
+ static dependencies = {
64
+ 'zn-button': ZnButton,
65
+ 'zn-icon': ZnIcon,
66
+ 'zn-input': ZnInput,
67
+ };
68
+
69
+ /** The first day of the range, `YYYY-MM-DD`. Defaults to today. */
70
+ @property({reflect: true}) from = '';
71
+
72
+ /** The last day of the range, `YYYY-MM-DD`. Defaults to today. */
73
+ @property({reflect: true}) to = '';
74
+
75
+ /** The earliest day that can be picked. */
76
+ @property() min = '';
77
+
78
+ /** The latest day that can be picked. */
79
+ @property() max = '';
80
+
81
+ /** Quick ranges as a JSON array of `{label, from, to}`, replacing the default ones. */
82
+ @property({converter: presetsConverter}) presets: DatePreset[] | undefined;
83
+
84
+ /** A short line shown under the calendar, such as how the range is used. */
85
+ @property() note = '';
86
+
87
+ /** Submits `name[from]` and `name[to]` with the form. */
88
+ @property() name = '';
89
+
90
+ /** The trigger button's size. */
91
+ @property() size: 'small' | 'medium' | 'large' = 'small';
92
+
93
+ @state() private draft: {from: Iso | ''; to: Iso | ''} = {from: '', to: ''};
94
+ @state() private view = {year: 0, month0: 0};
95
+ @state() private focusDay: Iso = '';
96
+ private readonly internals: ElementInternals | null;
97
+ // Not `popover`: that is the native Popover API attribute on every HTMLElement
98
+ private readonly anchor = new AnchoredPopoverController(this, {
99
+ trigger: '.date-range__trigger',
100
+ popover: '.date-range__pop',
101
+ width: () => Math.min(POP_WIDTH, window.innerWidth * 0.94),
102
+ align: 'start',
103
+ onHide: reason => {
104
+ if (reason === 'escape') this.focusTrigger();
105
+ },
106
+ });
107
+
108
+ constructor() {
109
+ super();
110
+ this.internals = typeof this.attachInternals === 'function' ? this.attachInternals() : null;
111
+ }
112
+
113
+ protected updated(changed: PropertyValues) {
114
+ if (changed.has('from') || changed.has('to') || changed.has('name')) {
115
+ this.syncFormValue();
116
+ }
117
+ }
118
+
119
+ private get rangeFrom(): Iso {
120
+ return isIso(this.from) ? this.from : todayIso();
121
+ }
122
+
123
+ private get rangeTo(): Iso {
124
+ return isIso(this.to) ? this.to : this.rangeFrom;
125
+ }
126
+
127
+ // A class rather than a media query: WebKit doesn't re-evaluate media queries in adopted sheets on resize
128
+ private get narrow(): boolean {
129
+ return window.matchMedia(NARROW).matches;
130
+ }
131
+
132
+ private get bounds(): [Iso | '', Iso | ''] {
133
+ return [isIso(this.min) ? this.min : '', isIso(this.max) ? this.max : ''];
134
+ }
135
+
136
+ private syncFormValue() {
137
+ if (!this.internals) return;
138
+ if (!this.name) {
139
+ this.internals.setFormValue(null);
140
+ return;
141
+ }
142
+ const data = new FormData();
143
+ data.append(`${this.name}[from]`, this.rangeFrom);
144
+ data.append(`${this.name}[to]`, this.rangeTo);
145
+ this.internals.setFormValue(data);
146
+ }
147
+
148
+ /** Opens the picker. */
149
+ async show() {
150
+ this.draft = {from: this.rangeFrom, to: this.rangeTo};
151
+ this.showMonth(this.draft.from);
152
+ this.focusDay = this.draft.from;
153
+ this.anchor.show();
154
+ await this.updateComplete;
155
+ this.shadowRoot?.querySelector<HTMLElement>('.date-range__day[tabindex="0"]')?.focus();
156
+ }
157
+
158
+ /** Closes the picker without applying. */
159
+ hide() {
160
+ this.anchor.hide();
161
+ }
162
+
163
+ private close() {
164
+ this.hide();
165
+ this.focusTrigger();
166
+ }
167
+
168
+ private toggle = () => {
169
+ if (this.anchor.open) {
170
+ this.hide();
171
+ } else {
172
+ void this.show();
173
+ }
174
+ };
175
+
176
+ private presetRange(preset: DatePreset) {
177
+ return clampRange(preset, ...this.bounds);
178
+ }
179
+
180
+ private focusTrigger() {
181
+ const trigger = this.anchor.trigger();
182
+ (trigger?.shadowRoot?.querySelector<HTMLElement>('button, a') ?? trigger)?.focus();
183
+ }
184
+
185
+ private applyRange(range: {from: Iso | ''; to: Iso | ''}) {
186
+ if (!range.from || !range.to || this.isDisabled(range.from) || this.isDisabled(range.to)) return;
187
+ this.from = range.from;
188
+ this.to = range.to;
189
+ this.close();
190
+ // Dispatched directly: zn-change's registered type carries no detail
191
+ this.dispatchEvent(new CustomEvent('zn-change', {
192
+ bubbles: true,
193
+ composed: true,
194
+ detail: {from: this.from, to: this.to}
195
+ }));
196
+ }
197
+
198
+ private showMonth(iso: Iso) {
199
+ const [y, m] = iso.split('-').map(Number);
200
+ this.view = {year: y, month0: m - 1};
201
+ }
202
+
203
+ private shiftMonth(by: number) {
204
+ this.shiftTo(this.view.year, this.view.month0 + by);
205
+ }
206
+
207
+ private shiftTo(year: number, month0: number) {
208
+ const date = new Date(Date.UTC(year, month0, 1));
209
+ this.view = {year: date.getUTCFullYear(), month0: date.getUTCMonth()};
210
+ }
211
+
212
+ private isDisabled(iso: Iso): boolean {
213
+ const [min, max] = this.bounds;
214
+ return (!!min && iso < min) || (!!max && iso > max);
215
+ }
216
+
217
+ private choose(iso: Iso) {
218
+ if (this.isDisabled(iso)) return;
219
+ this.draft = pick(this.draft, iso);
220
+ this.focusDay = iso;
221
+ }
222
+
223
+ private visible(iso: Iso): boolean {
224
+ const months = this.narrow ? 1 : 2;
225
+ const first = Date.UTC(this.view.year, this.view.month0, 1);
226
+ const end = Date.UTC(this.view.year, this.view.month0 + months, 1);
227
+ const at = toUtc(iso).getTime();
228
+ return at >= first && at < end;
229
+ }
230
+
231
+ private dayKeydown = async (e: KeyboardEvent) => {
232
+ const current = (e.target as HTMLElement).dataset.date;
233
+ if (!current) return;
234
+ // Weeks start on Monday
235
+ const weekday = (toUtc(current).getUTCDay() + 6) % 7;
236
+ const pageBy = e.shiftKey ? 12 : 1;
237
+ const next = KEY_STEPS[e.key] ? addDays(current, KEY_STEPS[e.key])
238
+ : e.key === 'Home' ? addDays(current, -weekday)
239
+ : e.key === 'End' ? addDays(current, 6 - weekday)
240
+ : e.key === 'PageUp' ? addMonths(current, -pageBy)
241
+ : e.key === 'PageDown' ? addMonths(current, pageBy)
242
+ : null;
243
+ if (!next) return;
244
+ e.preventDefault();
245
+ if (!this.visible(next)) {
246
+ // Backwards lands in the first visible month, forwards in the last
247
+ const lastVisible = next < current ? 0 : (this.narrow ? 0 : 1);
248
+ const at = toUtc(next);
249
+ this.shiftTo(at.getUTCFullYear(), at.getUTCMonth() - lastVisible);
250
+ }
251
+ this.focusDay = next;
252
+ await this.updateComplete;
253
+ this.shadowRoot?.querySelector<HTMLElement>(`[data-date="${next}"]`)?.focus();
254
+ };
255
+
256
+ // On commit (blur or Enter), not per keystroke: a date input reports each segment edit as a new valid value
257
+ private typed(end: 'from' | 'to', e: Event) {
258
+ const value = (e.target as ZnInput).value as string;
259
+ if (!isIso(value) || this.isDisabled(value)) return;
260
+
261
+ if (end === 'from') {
262
+ this.draft = this.draft.to && value <= this.draft.to ? {from: value, to: this.draft.to} : {from: value, to: ''};
263
+ } else {
264
+ this.draft = this.draft.from && value >= this.draft.from ? {from: this.draft.from, to: value} : {from: value, to: ''};
265
+ }
266
+ this.focusDay = value;
267
+ this.showMonth(value);
268
+ }
269
+
270
+ private stop = (e: Event) => e.stopPropagation();
271
+
272
+ render() {
273
+ return html`
274
+ <zn-button class="date-range__trigger" part="trigger" panel-bg size="${this.size}" icon="calendar@lu"
275
+ icon-size="16" aria-haspopup="dialog" aria-expanded="${this.anchor.open}" @click="${this.toggle}">
276
+ ${rangeLabel(this.rangeFrom, this.rangeTo)}
277
+ <zn-icon class="date-range__chevron" src="chevron-down@lu" size="14"></zn-icon>
278
+ </zn-button>
279
+ ${this.anchor.open ? this.renderPopover() : nothing}`;
280
+ }
281
+
282
+ private renderPopover() {
283
+ const presets = this.presets ?? defaultPresets(todayIso());
284
+ const {from, to} = this.draft;
285
+ const next = new Date(Date.UTC(this.view.year, this.view.month0 + 1, 1));
286
+
287
+ return html`
288
+ <div class="${classMap({'date-range__pop': true, 'is-narrow': this.narrow})}" part="popover" role="dialog" aria-label="Date range"
289
+ style="${this.anchor.style}" @zn-change="${this.stop}" @zn-input="${this.stop}"
290
+ @zn-focus="${this.stop}" @zn-blur="${this.stop}">
291
+ <div class="date-range__presets" role="group" aria-label="Quick ranges">
292
+ ${presets.map(preset => {
293
+ const range = this.presetRange(preset);
294
+ const active = !!range && range.from === from && range.to === to;
295
+ return html`
296
+ <button type="button" class="${classMap({'date-range__preset': true, 'is-active': active})}"
297
+ aria-pressed="${active}" aria-disabled="${!range}"
298
+ @click="${() => range && this.applyRange(range)}">${preset.label}</button>`;
299
+ })}
300
+ </div>
301
+ <div class="date-range__main">
302
+ <div class="date-range__inputs">
303
+ <zn-input class="date-range__from" type="date" label="From" .value="${from}"
304
+ min="${this.min || nothing}" max="${this.max || nothing}"
305
+ @zn-blur="${(e: Event) => this.typed('from', e)}"
306
+ @keydown="${(e: KeyboardEvent) => e.key === 'Enter' && this.typed('from', e)}"></zn-input>
307
+ <span class="date-range__dash">to</span>
308
+ <zn-input class="date-range__to" type="date" label="To" .value="${to}"
309
+ min="${this.min || nothing}" max="${this.max || nothing}"
310
+ @zn-blur="${(e: Event) => this.typed('to', e)}"
311
+ @keydown="${(e: KeyboardEvent) => e.key === 'Enter' && this.typed('to', e)}"></zn-input>
312
+ </div>
313
+ <div class="date-range__months">
314
+ <zn-button class="date-range__nav date-range__nav--prev" icon="chevron-left@lu" icon-size="16"
315
+ icon-button plain aria-label="Previous month"
316
+ @click="${() => this.shiftMonth(-1)}"></zn-button>
317
+ ${this.renderMonth(this.view.year, this.view.month0)}
318
+ ${this.renderMonth(next.getUTCFullYear(), next.getUTCMonth())}
319
+ <zn-button class="date-range__nav date-range__nav--next" icon="chevron-right@lu" icon-size="16"
320
+ icon-button plain aria-label="Next month"
321
+ @click="${() => this.shiftMonth(1)}"></zn-button>
322
+ </div>
323
+ ${this.note ? html`<p class="date-range__note">${this.note}</p>` : nothing}
324
+ <div class="date-range__foot">
325
+ <span class="date-range__summary">
326
+ ${from && to ? `${rangeLabel(from, to)} · ${daysBetween(from, to) + 1} days` : 'Pick a last day'}
327
+ </span>
328
+ <zn-button color="transparent" @click="${() => this.close()}">Cancel</zn-button>
329
+ <zn-button class="date-range__apply" color="primary" ?disabled="${!from || !to}"
330
+ @click="${() => this.applyRange(this.draft)}">Apply
331
+ </zn-button>
332
+ </div>
333
+ </div>
334
+ </div>`;
335
+ }
336
+
337
+ private renderMonth(year: number, month0: number) {
338
+ const cells = monthGrid(year, month0);
339
+ const rows = Array.from({length: 6}, (_, i) => cells.slice(i * 7, i * 7 + 7)).filter(row => row.some(Boolean));
340
+ const title = new Intl.DateTimeFormat('en-GB', {month: 'long', year: 'numeric', timeZone: 'UTC'})
341
+ .format(new Date(Date.UTC(year, month0, 1)));
342
+ const today = todayIso();
343
+ const {from, to} = this.draft;
344
+ const tabStop = [this.focusDay, from].find(d => d && this.visible(d))
345
+ ?? `${this.view.year}-${String(this.view.month0 + 1).padStart(2, '0')}-01`;
346
+
347
+ return html`
348
+ <div class="date-range__month">
349
+ <div class="date-range__head">${title}</div>
350
+ <div class="date-range__grid" role="grid" aria-label="${title}" @keydown="${this.dayKeydown}">
351
+ <div class="date-range__week" role="row">
352
+ ${WEEKDAYS.map(d => html`<span role="columnheader">${d}</span>`)}
353
+ </div>
354
+ ${rows.map(row => html`
355
+ <div class="date-range__row" role="row">
356
+ ${row.map(iso => iso ? html`
357
+ <button type="button" role="gridcell" data-date="${iso}"
358
+ class="${classMap({
359
+ 'date-range__day': true,
360
+ 'is-start': iso === from,
361
+ 'is-end': !!to && iso === to,
362
+ 'is-in': !!from && !!to && iso > from && iso < to,
363
+ 'is-today': iso === today,
364
+ })}"
365
+ tabindex="${iso === tabStop ? 0 : -1}"
366
+ aria-label="${formatDate(iso)}"
367
+ aria-selected="${!!from && iso >= from && iso <= (to || from)}"
368
+ aria-disabled="${this.isDisabled(iso)}"
369
+ @click="${() => this.choose(iso)}">${Number(iso.slice(8))}</button>` : html`
370
+ <span class="date-range__pad" role="gridcell"></span>`)}
371
+ </div>`)}
372
+ </div>
373
+ </div>`;
374
+ }
375
+ }