@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
@@ -4,18 +4,22 @@ import {HasSlotController} from "../../internal/slot";
4
4
  import {ifDefined} from "lit/directives/if-defined.js";
5
5
  import {property, state} from 'lit/decorators.js';
6
6
  import {ref} from "lit/directives/ref.js";
7
+ import {styleMap} from "lit/directives/style-map.js";
8
+ import {renderSortIcon, sortKeyHandler} from "../../internal/table-sort";
9
+ import {ReorderController} from "../../internal/reorder";
7
10
  import {ResizeController} from '@lit-labs/observers/resize-controller.js';
11
+ import {type TableTab, TabRowController} from "../../internal/table-tabs";
8
12
  import {Task} from "@lit/task";
9
13
  import {unsafeHTML} from "lit/directives/unsafe-html.js";
10
14
  import {type ZnFilterChangeEvent} from "../../events/zn-filter-change";
11
15
  import {type ZnSearchChangeEvent} from "../../events/zn-search-change";
12
16
  import ZincElement from '../../internal/zinc-element';
13
17
  import ZnAlert from "../alert";
18
+ import ZnBadge from "../badge";
14
19
  import ZnButton from "../button";
15
- import ZnButtonGroup from "../button-group";
16
20
  import ZnChip from "../chip";
17
21
  import ZnConfirm from "../confirm";
18
- import ZnDataTableFilter from "../data-table-filter";
22
+ import ZnDataTableFilter, {type FilterField} from "../data-table-filter";
19
23
  import ZnDataTableSearch from "../data-table-search";
20
24
  import ZnDropdown from "../dropdown";
21
25
  import ZnEmptyState from "../empty-state";
@@ -26,13 +30,17 @@ import ZnPanel from "../panel";
26
30
  import ZnSkeleton from "../skeleton";
27
31
  import ZnStyle from "../style";
28
32
  import type ZnDataSelect from "../data-select";
29
- import type ZnInput from "../input";
33
+ import ZnIcon from "../icon";
34
+ import ZnInput from "../input";
35
+ import ZnOptGroup from "../opt-group";
36
+ import ZnOption from "../option";
30
37
  import type ZnQueryBuilder from "../query-builder";
31
- import type ZnSelect from "../select";
38
+ import ZnSelect from "../select";
32
39
 
33
40
  // Tables load in such a way that the global sheet has to be re-applied inside the shadow root.
34
41
  import bootStyles from '../../../scss/boot.scss';
35
42
  import styles from './data-table.scss';
43
+ import tableControlsStyles from '../../../scss/table-controls.scss';
36
44
 
37
45
  const DEFAULT_PAGE = 1;
38
46
  const DEFAULT_PER_PAGE = 10;
@@ -53,6 +61,8 @@ interface Cell {
53
61
  hoverContent?: string;
54
62
  hoverPlacement?: string;
55
63
  chipColor?: string;
64
+ badgeType?: string;
65
+ badgeDot?: boolean;
56
66
  gaid?: string;
57
67
  sortValue?: string;
58
68
  uri?: string;
@@ -125,6 +135,7 @@ interface HeaderConfig {
125
135
  hideColumn?: boolean;
126
136
  secondary?: boolean;
127
137
  type?: string;
138
+ align?: 'left' | 'right' | 'center';
128
139
  cellTemplate?: Cell;
129
140
  ifEmpty?: Cell;
130
141
  }
@@ -171,6 +182,7 @@ type AllowedInputElement =
171
182
  * @since 1.0
172
183
  *
173
184
  * @dependency zn-alert
185
+ * @dependency zn-badge
174
186
  * @dependency zn-button
175
187
  * @dependency zn-empty-state
176
188
  * @dependency zn-chip
@@ -179,7 +191,6 @@ type AllowedInputElement =
179
191
  * @dependency zn-menu
180
192
  * @dependency zn-menu-item
181
193
  * @dependency zn-panel
182
- * @dependency zn-button-group
183
194
  * @dependency zn-confirm
184
195
  * @dependency zn-skeleton
185
196
  * @dependency zn-data-table-search
@@ -202,9 +213,10 @@ type AllowedInputElement =
202
213
  * @cssproperty --example - An example CSS custom property.
203
214
  */
204
215
  export default class ZnDataTable extends ZincElement {
205
- static styles: CSSResultGroup = [unsafeCSS(bootStyles), unsafeCSS(styles)];
216
+ static styles: CSSResultGroup = [unsafeCSS(bootStyles), unsafeCSS(styles), unsafeCSS(tableControlsStyles)];
206
217
  static dependencies = {
207
218
  'zn-alert': ZnAlert,
219
+ 'zn-badge': ZnBadge,
208
220
  'zn-button': ZnButton,
209
221
  'zn-empty-state': ZnEmptyState,
210
222
  'zn-chip': ZnChip,
@@ -213,11 +225,16 @@ export default class ZnDataTable extends ZincElement {
213
225
  'zn-menu': ZnMenu,
214
226
  'zn-menu-item': ZnMenuItem,
215
227
  'zn-panel': ZnPanel,
216
- 'zn-button-group': ZnButtonGroup,
217
228
  'zn-confirm': ZnConfirm,
218
229
  'zn-skeleton': ZnSkeleton,
219
230
  'zn-style': ZnStyle,
231
+ 'zn-icon': ZnIcon,
232
+ 'zn-input': ZnInput,
233
+ 'zn-opt-group': ZnOptGroup,
234
+ 'zn-option': ZnOption,
235
+ 'zn-select': ZnSelect,
220
236
  'zn-data-table-search': ZnDataTableSearch,
237
+ 'zn-data-table-filter': ZnDataTableFilter,
221
238
  };
222
239
 
223
240
  @property({attribute: 'data-uri'}) dataUri: string;
@@ -263,6 +280,35 @@ export default class ZnDataTable extends ZincElement {
263
280
 
264
281
  @property({attribute: "empty-state-icon"}) emptyStateIcon: string = "data_alert";
265
282
 
283
+ /** Unit for the row total shown with the caption: "publishers" reads "16 publishers". */
284
+ @property({attribute: 'total-label'}) totalLabel = '';
285
+
286
+ /** Unit used when the total is exactly one: "publisher" reads "1 publisher". Falls back to `total-label`. */
287
+ @property({attribute: 'total-label-singular'}) totalLabelSingular = '';
288
+
289
+ /** Tabs rendered inside the table. The selected tab's value is sent as `tab-param` with every request. */
290
+ @property({type: Array}) tabs: TableTab[] = [];
291
+
292
+ @property({attribute: 'tab-param'}) tabParam = 'tab';
293
+
294
+ /** Draws the tabs as a segmented control: a grey track with a pill that slides to the selected tab. */
295
+ @property({attribute: 'pill-tabs', type: Boolean, reflect: true}) pillTabs = false;
296
+
297
+ /** The selected tab's value; the first tab when empty. */
298
+ // Not the `tab` attribute: on any element that marks a zn-tabs trigger, which is clickable and switches tabs
299
+ @property({attribute: 'active-tab', reflect: true}) tab = '';
300
+
301
+ /**
302
+ * Fields for the funnel's filter builder. When set, the table renders its own zn-data-table-filter in the
303
+ * header, in place of the `filter` slot, and the applied rules show as chips under the tabs. Rules are sent
304
+ * as `filter`.
305
+ */
306
+ @property({attribute: 'filter-fields', type: Array}) filterFields: FilterField[] = [];
307
+
308
+ private readonly _fieldsFilter = document.createElement('zn-data-table-filter');
309
+
310
+ private readonly _tabRow = new TabRowController(this, {onSelect: (value: string) => this.selectTab(value)});
311
+
266
312
  @property() filters: [] = [];
267
313
 
268
314
  @property() method: 'GET' | 'POST' = 'POST';
@@ -293,6 +339,7 @@ export default class ZnDataTable extends ZincElement {
293
339
  private _hasLoadedData = false;
294
340
  private _lastLoadHadRows = false;
295
341
  private _lastTableContent: TemplateResult = html``;
342
+ private _headObserver: ResizeObserver | null = null;
296
343
 
297
344
  // Sharable (URL <-> state) sync
298
345
  private _sharableInitialised = false;
@@ -332,6 +379,7 @@ export default class ZnDataTable extends ZincElement {
332
379
  ActionSlots.filter.valueOf(),
333
380
  ActionSlots.sort.valueOf(),
334
381
  ActionSlots.inputs.valueOf(),
382
+ 'toolbar',
335
383
  'empty-state',
336
384
  'no-results'
337
385
  );
@@ -379,6 +427,11 @@ export default class ZnDataTable extends ZincElement {
379
427
  Object.assign(requestData, params);
380
428
  }
381
429
 
430
+ Object.assign(requestData, this.toolbarParams());
431
+ if (this.tabs.length > 0) {
432
+ Object.assign(requestData, {[this.tabParam]: this.activeTab()});
433
+ }
434
+
382
435
  // Add any extra request params
383
436
  const extraParams = requestParams && typeof requestParams === 'object'
384
437
  ? requestParams as Record<string, unknown>
@@ -425,7 +478,15 @@ export default class ZnDataTable extends ZincElement {
425
478
  private _hiddenCells: Map<string, Cell[]> = new Map();
426
479
  private _secondaryHeaders: HeaderConfig[];
427
480
  @state() private _deselectedColumns: Set<string> = new Set();
428
- @state() private _filtersOpen = false;
481
+ // Header keys in the order the user has dragged them into; keys not listed keep their configured order after these
482
+ @state() private _columnOrder: string[] = [];
483
+ private readonly reorderController = new ReorderController(this, {
484
+ axis: 'horizontal',
485
+ indicator: true,
486
+ // Plain arrows are left for scrolling the table
487
+ modifier: 'alt',
488
+ focus: key => this.columnCells(key)[0]?.focus(),
489
+ });
429
490
  private _columnDefaultsApplied = false;
430
491
  private _formatTemplates: Record<string, DisplayTemplate> = defaultTemplates;
431
492
 
@@ -444,11 +505,78 @@ export default class ZnDataTable extends ZincElement {
444
505
 
445
506
  // Headers the column select can toggle - everything but the permanently hidden and secondary ones
446
507
  private selectableHeaders(): HeaderConfig[] {
447
- return (Object.values(this.headers) as HeaderConfig[]).filter((header: HeaderConfig) => {
508
+ const headers = (Object.values(this.headers) as HeaderConfig[]).filter((header: HeaderConfig) => {
448
509
  if (header.hideHeader || header.hideColumn || header.secondary) return false;
449
510
 
450
511
  return !Object.values(this.hiddenColumns).includes(header.key);
451
512
  });
513
+
514
+ const rank = new Map(this._columnOrder.map((key, i) => [key, i]));
515
+ return headers.sort((a, b) => (rank.get(a.key) ?? headers.length) - (rank.get(b.key) ?? headers.length));
516
+ }
517
+
518
+ /** @internal ReorderHost: the visible columns' keys in order. */
519
+ reorderKeys() {
520
+ return this.visibleHeaders().map(h => h.key);
521
+ }
522
+
523
+ /** @internal ReorderHost: puts the visible columns in a new order, leaving deselected ones in their slots. */
524
+ applyReorder(keys: string[]) {
525
+ const visible = new Set(keys);
526
+ let i = 0;
527
+ this._columnOrder = this.selectableHeaders().map(h => visible.has(h.key) ? keys[i++] : h.key);
528
+ }
529
+
530
+ /** @internal ReorderHost: the heading drawn for a column. */
531
+ reorderRow(key: string) {
532
+ return this.columnCells(key)[0] ?? null;
533
+ }
534
+
535
+ /** @internal ReorderHost: the heading and every cell of a column. */
536
+ reorderSlides(key: string) {
537
+ return this.columnCells(key);
538
+ }
539
+
540
+ private reorderable() {
541
+ return this.visibleHeaders().length > 1;
542
+ }
543
+
544
+ // One line over the whole table, as per-cell borders break where the heading meets the body
545
+ private renderDropLine() {
546
+ const dropIndex = this.reorderController.dropIndex;
547
+ if (dropIndex === null) return nothing;
548
+
549
+ const keys = this.reorderKeys();
550
+ const heading = this.reorderRow(keys[Math.min(dropIndex, keys.length - 1)]);
551
+ const table = heading?.closest('table');
552
+ const scroll = heading?.closest<HTMLElement>('.table__scroll');
553
+ if (!heading || !table || !scroll) return nothing;
554
+
555
+ const rect = heading.getBoundingClientRect();
556
+ const bounds = table.getBoundingClientRect();
557
+ // Held inside the table: a line poking past its edge widens the scroller, which then scrolls under the drag
558
+ const half = 1; // half of .table__drop-line's width
559
+ const edge = Math.min(Math.max(dropIndex === keys.length ? rect.right : rect.left, bounds.left + half), bounds.right - half);
560
+ const left = edge - scroll.getBoundingClientRect().left + scroll.scrollLeft;
561
+ return html`<div class="table__drop-line" style=${styleMap({left: `${left}px`})}></div>`;
562
+ }
563
+
564
+ /** @internal ReorderHost: a column lands anywhere over the table, not just its headings. */
565
+ reorderArea() {
566
+ return this.shadowRoot?.querySelector<HTMLElement>('.table__scroll table') ?? null;
567
+ }
568
+
569
+ /** @internal ReorderHost: the column's label, as a copy of the whole heading is unusable for a wide or wrapped one. */
570
+ reorderGhost(key: string) {
571
+ const header = (Object.values(this.headers) as HeaderConfig[]).find(h => h.key === key);
572
+ const ghost = document.createElement('div');
573
+ ghost.className = 'table__drag-ghost';
574
+ ghost.textContent = header?.label ?? key;
575
+ return ghost;
576
+ }
577
+
578
+ private columnCells(key: string) {
579
+ return [...this.shadowRoot?.querySelectorAll<HTMLElement>(`[data-column="${CSS.escape(key)}"]`) ?? []];
452
580
  }
453
581
 
454
582
  private isColumnVisible(header: HeaderConfig): boolean {
@@ -716,7 +844,9 @@ export default class ZnDataTable extends ZincElement {
716
844
  || this.hasSlotController.test(ActionSlots.actions.valueOf())
717
845
  || this.hasSlotController.test(ActionSlots.create.valueOf())
718
846
  || this.hasSlotController.test(ActionSlots.sort.valueOf())
719
- || this.hasSlotController.test(ActionSlots.search.valueOf());
847
+ || this.hasSlotController.test(ActionSlots.search.valueOf())
848
+ || this.hasSlotController.test(ActionSlots.filter.valueOf())
849
+ || this.filterFields.length > 0;
720
850
 
721
851
  const hasInputs = this.hasSlotController.test(ActionSlots.inputs.valueOf());
722
852
  const hasControls = hasActions || this.hasColumnSelect() || this.hasRefresh();
@@ -725,14 +855,6 @@ export default class ZnDataTable extends ZincElement {
725
855
  // often bring their own panel for it, so let it stand on its own
726
856
  const bareEmptyState = emptyBody && !hasControls && !this.caption;
727
857
 
728
- // The filter bar is inline, so it gets its own row rather than a slot in the header
729
- const filters = this.hasSlotController.test(ActionSlots.filter.valueOf())
730
- ? html`
731
- <div class="table__filters" ?hidden="${!this._filtersOpen}">
732
- <slot name="${ActionSlots.filter.valueOf()}"></slot>
733
- </div>`
734
- : nothing;
735
-
736
858
  // Headers do not need to be re-rendered with new data
737
859
  return html`
738
860
  <div class="table-container" ${ref((el) => (this.tableContainer = el))}>
@@ -741,29 +863,42 @@ export default class ZnDataTable extends ZincElement {
741
863
  <slot name="${ActionSlots.filter_top.valueOf()}"></slot>
742
864
  ${this.standalone && !bareEmptyState
743
865
  ? html`
744
- <zn-panel caption="${ifDefined(this.caption || undefined)}" flush>
866
+ <zn-panel caption="${ifDefined(this.caption || undefined)}"
867
+ description="${ifDefined(this.totalText())}"
868
+ ?inline-description="${!!this.totalLabel}"
869
+ flush>
745
870
  ${hasControls ? html`
746
871
  <div slot="actions" class="table__header__right">
747
872
  ${this.getActions()}
748
873
  ${this.getHeaderControls()}
749
874
  </div>` : nothing}
750
- ${filters}
875
+ ${this.renderTabs()}
876
+ ${this.renderFilterChips()}
877
+ ${this.renderToolbar()}
751
878
  ${tableBody}
752
879
  ${this.getTableFooter('footer')}
753
880
  </zn-panel>`
754
881
  : html`
755
882
  ${hasControls || this.caption ? this.getTableHeader() : nothing}
756
- ${filters}
883
+ ${this.renderTabs()}
884
+ ${this.renderFilterChips()}
885
+ ${this.renderToolbar()}
757
886
  ${tableBody}
758
887
  ${this.getTableFooter()}`}
759
888
  </div>
760
889
  `;
761
890
  }
762
891
 
892
+ protected willUpdate(changed: PropertyValues) {
893
+ super.willUpdate(changed);
894
+ this._fieldsFilter.filters = this.filterFields;
895
+ this._fieldsFilter.value = this.filter;
896
+ }
897
+
763
898
  connectedCallback() {
764
899
  super.connectedCallback();
765
900
  this.addEventListener('zn-filter-change', this.filterChangeListener);
766
- this.addEventListener('zn-clear', this.filterClearListener);
901
+ this._fieldsFilter.addEventListener('zn-filter-change', this.fieldsFilterChange);
767
902
  this.addEventListener('zn-search-change', this.searchChangeListener);
768
903
  // Capture defaults before reading the URL so URL values are treated as deltas, then seed state.
769
904
  this._captureSharableDefaults();
@@ -816,15 +951,10 @@ export default class ZnDataTable extends ZincElement {
816
951
  disconnectedCallback() {
817
952
  super.disconnectedCallback();
818
953
  this.removeEventListener('zn-filter-change', this.filterChangeListener);
819
- this.removeEventListener('zn-clear', this.filterClearListener);
954
+ this._fieldsFilter.removeEventListener('zn-filter-change', this.fieldsFilterChange);
820
955
  this.removeEventListener('zn-search-change', this.searchChangeListener);
821
- }
822
-
823
- // Clearing the filters leaves an empty row behind, so fold it away
824
- filterClearListener = (e: Event) => {
825
- if (e.target instanceof ZnDataTableFilter) {
826
- this._filtersOpen = false;
827
- }
956
+ this._headObserver?.disconnect();
957
+ this._headObserver = null;
828
958
  }
829
959
 
830
960
  filterChangeListener = (e: ZnFilterChangeEvent) => {
@@ -873,7 +1003,7 @@ export default class ZnDataTable extends ZincElement {
873
1003
  emptyState() {
874
1004
  if (this.isNarrowed || (this.noInitialLoad && !this._initialLoad)) {
875
1005
  return html`
876
- <div class="table--empty">
1006
+ <div class="table--empty table__enter">
877
1007
  <slot name="no-results">
878
1008
  <zn-empty-state caption="No Results Found" icon="search">
879
1009
  <p>Nothing matched your search. Try adjusting your filters.</p>
@@ -883,7 +1013,7 @@ export default class ZnDataTable extends ZincElement {
883
1013
  }
884
1014
 
885
1015
  return html`
886
- <div class="table--empty">
1016
+ <div class="table--empty table__enter">
887
1017
  <slot name="empty-state">
888
1018
  <zn-empty-state
889
1019
  caption="${this.emptyStateCaption ? this.emptyStateCaption : (this.caption ? "No " + this.caption.toLowerCase() + " found" : "No data found")}"
@@ -1036,12 +1166,12 @@ export default class ZnDataTable extends ZincElement {
1036
1166
  const colCount = filteredHeaders.length + (this.rowHasActions ? 1 : 0) + (anyHidden ? 1 : 0) + (showCheckboxes ? 1 : 0);
1037
1167
 
1038
1168
  return html`
1039
- <div class="table__scroll">
1169
+ <div class="table__scroll table__enter">
1040
1170
  <table class="${classMap({
1041
1171
  'table': true,
1042
1172
  'with-hover': !this.unsortable && this.isSelectable(),
1043
1173
  })}">
1044
- <thead>
1174
+ <thead ${ref(this.observeHead)}>
1045
1175
  <tr>
1046
1176
  ${showCheckboxes ? this.renderSelectAllCheckbox() : nothing}
1047
1177
  ${anyHidden ? html`
@@ -1073,6 +1203,7 @@ export default class ZnDataTable extends ZincElement {
1073
1203
  `)}
1074
1204
  </tbody>
1075
1205
  </table>
1206
+ ${this.renderDropLine()}
1076
1207
  </div>
1077
1208
  `;
1078
1209
  }
@@ -1080,15 +1211,33 @@ export default class ZnDataTable extends ZincElement {
1080
1211
  // Pending and loaded states share this template so Lit keeps the table's DOM (and focus) across loads
1081
1212
  private tableContent(content: TemplateResult, pending: boolean) {
1082
1213
  return html`
1083
- <div class="${classMap({'reduced-opacity': pending})}">${content}</div>`;
1214
+ <div class="${classMap({'table__frame': true, 'reduced-opacity': pending})}">
1215
+ ${pending ? html`<div class="table__loader" aria-hidden="true"></div>` : nothing}
1216
+ ${content}
1217
+ </div>`;
1084
1218
  }
1085
1219
 
1220
+ private observeHead = (el: Element | undefined) => {
1221
+ this._headObserver?.disconnect();
1222
+ this._headObserver = null;
1223
+ if (!el) return;
1224
+ this._headObserver = new ResizeObserver(([entry]) => {
1225
+ const height = `${Math.round(entry.borderBoxSize[0].blockSize)}px`;
1226
+ if (this.style.getPropertyValue('--_head-height') !== height) {
1227
+ this.style.setProperty('--_head-height', height);
1228
+ }
1229
+ });
1230
+ this._headObserver.observe(el);
1231
+ };
1232
+
1086
1233
  getTableHeader() {
1087
1234
  return html`
1088
1235
  <div class="table__header">
1089
1236
  <div class="table__header__actions">
1090
1237
  ${this.caption ? html`
1091
1238
  <h3 class="table__header__caption">${this.caption}</h3>` : nothing}
1239
+ ${this.totalText() ? html`
1240
+ <span class="table__header__count">${this.totalText()}</span>` : nothing}
1092
1241
  </div>
1093
1242
  <div class="table__header__right">
1094
1243
  ${this.getActions()}
@@ -1098,13 +1247,73 @@ export default class ZnDataTable extends ZincElement {
1098
1247
  `;
1099
1248
  }
1100
1249
 
1250
+ private totalText(): string | undefined {
1251
+ if (!this.totalLabel || !this._hasLoadedData) return undefined;
1252
+ // Narrowed by a search or filter: read as a share of the selected tab, e.g. "1 of 16"
1253
+ const tabCount = this.tabs.find(tab => tab.value === this.activeTab())?.count;
1254
+ if (this.isNarrowed && tabCount !== undefined && tabCount !== null && tabCount !== '') {
1255
+ return `${this._totalRows.toLocaleString()} of ${tabCount}`;
1256
+ }
1257
+ const label = this._totalRows === 1 && this.totalLabelSingular ? this.totalLabelSingular : this.totalLabel;
1258
+ return `${this._totalRows.toLocaleString()} ${label}`;
1259
+ }
1260
+
1261
+ private activeTab(): string {
1262
+ return this.tab || this.tabs[0]?.value || '';
1263
+ }
1264
+
1265
+ private renderTabs() {
1266
+ return this._tabRow.render(this.tabs, this.activeTab(), this.pillTabs);
1267
+ }
1268
+
1269
+ private selectTab(value: string) {
1270
+ if (value === this.activeTab()) return;
1271
+ this.tab = value;
1272
+ this.page = 1;
1273
+ this._dataTask.run().then(r => r);
1274
+ }
1275
+
1276
+ private renderToolbar() {
1277
+ if (!this.hasSlotController.test('toolbar')) return nothing;
1278
+
1279
+ return html`
1280
+ <div class="table__toolbar" @zn-change="${this.toolbarChanged}" @change="${this.toolbarChanged}">
1281
+ <slot name="toolbar"></slot>
1282
+ </div>`;
1283
+ }
1284
+
1285
+ // zn-select and native fields can both announce one edit, so coalesce them into a single reload
1286
+ private _toolbarQueued = false;
1287
+ private toolbarChanged = () => {
1288
+ if (this._toolbarQueued) return;
1289
+ this._toolbarQueued = true;
1290
+ queueMicrotask(() => {
1291
+ this._toolbarQueued = false;
1292
+ this.page = 1;
1293
+ this._dataTask.run().then(r => r);
1294
+ });
1295
+ }
1296
+
1297
+ private toolbarParams(): Record<string, unknown> {
1298
+ const params: Record<string, unknown> = {};
1299
+ this.querySelectorAll('[slot="toolbar"]').forEach((slotted) => {
1300
+ [slotted, ...Array.from(slotted.querySelectorAll('[name]'))].forEach((field) => {
1301
+ const name = (field as AllowedInputElement).name || field.getAttribute('name');
1302
+ if (name && 'value' in field) {
1303
+ params[name] = (field as AllowedInputElement).value;
1304
+ }
1305
+ });
1306
+ });
1307
+ return params;
1308
+ }
1309
+
1101
1310
  private getHeaderControls() {
1102
1311
  return html`
1312
+ <slot name="${ActionSlots.search.valueOf()}"></slot>
1313
+ ${this.getFilter()}
1103
1314
  ${this.getColumnSelect()}
1104
- ${this.getFilterToggle()}
1105
1315
  ${this.getRefreshButton()}
1106
1316
  <slot name="${ActionSlots.sort.valueOf()}"></slot>
1107
- <slot name="${ActionSlots.search.valueOf()}"></slot>
1108
1317
  <slot name="${ActionSlots.create.valueOf()}"></slot>`;
1109
1318
  }
1110
1319
 
@@ -1113,42 +1322,34 @@ export default class ZnDataTable extends ZincElement {
1113
1322
  return this._hasLoadedData && this._lastLoadHadRows;
1114
1323
  }
1115
1324
 
1116
- private getFilterToggle() {
1325
+ private getFilter() {
1326
+ if (this.filterFields.length > 0) return this._fieldsFilter;
1117
1327
  if (!this.hasSlotController.test(ActionSlots.filter.valueOf())) return nothing;
1118
-
1119
- const applied = this.appliedFilterCount();
1120
-
1121
- return html`
1122
- <zn-button icon-button="small"
1123
- icon="funnel@lu"
1124
- icon-size="18"
1125
- tooltip="Filter"
1126
- aria-label="Filter"
1127
- muted-notifications
1128
- notification="${applied || nothing}"
1129
- aria-pressed="${this._filtersOpen}"
1130
- class="${classMap({'table__header__toggle--active': applied > 0})}"
1131
- @click="${this.toggleFilters}">
1132
- </zn-button>`;
1328
+ return html`<slot name="${ActionSlots.filter.valueOf()}"></slot>`;
1133
1329
  }
1134
1330
 
1135
- private toggleFilters = () => {
1136
- this._filtersOpen = !this._filtersOpen;
1331
+ private filterSource(): ZnDataTableFilter | null {
1332
+ if (this.filterFields.length > 0) return this._fieldsFilter;
1333
+ return this.querySelector<ZnDataTableFilter>(':scope > zn-data-table-filter[slot="filter"]');
1137
1334
  }
1138
1335
 
1139
- private appliedFilterCount(): number {
1140
- if (!this.filter) return 0;
1141
-
1142
- try {
1143
- const applied: unknown = JSON.parse(atob(this.filter));
1144
- return Array.isArray(applied) ? applied.length : 0;
1145
- } catch {
1146
- return 0;
1147
- }
1336
+ // The funnel sits in the header, so its chips get a filter of their own in a row under the tabs
1337
+ private renderFilterChips() {
1338
+ const source = this.filterSource();
1339
+ if (!source) return nothing;
1340
+ return html`<zn-data-table-filter class="table__chips" .source="${source}"></zn-data-table-filter>`;
1148
1341
  }
1149
1342
 
1343
+ private fieldsFilterChange = (e: Event) => {
1344
+ e.stopPropagation();
1345
+ this.filter = this._fieldsFilter.value;
1346
+ this.page = 1;
1347
+ this._dataTask.run().then(r => r);
1348
+ };
1349
+
1150
1350
  private hasColumnSelect(): boolean {
1151
- return !this.hideColumnSelect && this.hasRows() && this.selectableHeaders().length > 1;
1351
+ const headers = this.selectableHeaders();
1352
+ return !this.hideColumnSelect && this.hasRows() && headers.length > 1 && headers.some(h => h.required !== true);
1152
1353
  }
1153
1354
 
1154
1355
  private hasRefresh(): boolean {
@@ -1208,18 +1409,21 @@ export default class ZnDataTable extends ZincElement {
1208
1409
 
1209
1410
  if (rowSelected === null && pagination === null && rowsPerPage === null) return null;
1210
1411
 
1412
+ const first = (this.page - 1) * this.itemsPerPage + 1;
1413
+ const last = Math.min(this.page * this.itemsPerPage, this._totalRows);
1414
+
1211
1415
  return html`
1212
1416
  <div class="${classMap({
1213
1417
  'table__footer': true,
1214
1418
  'table__footer--panel': !!slot,
1215
1419
  })}" slot="${ifDefined(slot)}">
1216
1420
  <div class="table__footer__left">
1217
- ${pagination}
1421
+ ${this._totalRows > 0 ? html`
1422
+ <span>${first.toLocaleString()}–${last.toLocaleString()} of ${this._totalRows.toLocaleString()}</span>` : nothing}
1218
1423
  ${rowSelected}
1219
1424
  </div>
1220
- <div class="table__footer__right">
1221
- ${rowsPerPage}
1222
- </div>
1425
+ ${pagination}
1426
+ ${rowsPerPage}
1223
1427
  </div>`;
1224
1428
  }
1225
1429
 
@@ -1227,13 +1431,13 @@ export default class ZnDataTable extends ZincElement {
1227
1431
  if (this.selectedRows.length <= 0) return null;
1228
1432
 
1229
1433
  return html`
1230
- <p>${this.numberOfRowsSelected} of ${this._rows.length} rows selected</p>`
1434
+ <span>${this.numberOfRowsSelected} of ${this._rows.length} rows selected</span>`
1231
1435
  }
1232
1436
 
1233
1437
  getRowsPerPage() {
1234
1438
  if (this.hidePagination) return null;
1235
1439
 
1236
- const optionsRowsPerPage = [10, 20, 30, 40, 50];
1440
+ const optionsRowsPerPage = [10, 25, 50, 100];
1237
1441
 
1238
1442
  // Keyed to the dataset, not the current page size - raising the size past the total
1239
1443
  // must not hide the only control that can lower it again
@@ -1241,7 +1445,7 @@ export default class ZnDataTable extends ZincElement {
1241
1445
 
1242
1446
  return html`
1243
1447
  <div class="table__footer__rows-per-page">
1244
- <p>Rows</p>
1448
+ <span>Rows</span>
1245
1449
  <zn-select name="rowPerPage"
1246
1450
  size="small"
1247
1451
  value="${this.itemsPerPage}"
@@ -1255,57 +1459,59 @@ export default class ZnDataTable extends ZincElement {
1255
1459
  </div>`;
1256
1460
  }
1257
1461
 
1258
- // A window of PAGE_WINDOW pages that slides with the current page and stays inside the page
1259
- // count; the first/last buttons cover jumping to either end
1260
- private getPageRange(): number[] {
1462
+ // A window of PAGE_WINDOW pages around the current one, plus the first and last pages;
1463
+ // null marks a gap, except a gap of one page, which shows that page instead
1464
+ private getPageRange(): (number | null)[] {
1261
1465
  const size = Math.min(PAGE_WINDOW, this.totalPages);
1262
1466
  const start = Math.min(
1263
1467
  Math.max(1, this.page - Math.floor(size / 2)),
1264
1468
  this.totalPages - size + 1
1265
1469
  );
1470
+ const pages = [...new Set([1, ...Array.from({length: size}, (_, i) => start + i), this.totalPages])];
1266
1471
 
1267
- return Array.from({length: size}, (_, i) => start + i);
1472
+ return pages.flatMap((p, i) => {
1473
+ const gap = i > 0 ? p - pages[i - 1] : 1;
1474
+ if (gap === 2) return [p - 1, p];
1475
+ return gap > 2 ? [null, p] : [p];
1476
+ });
1268
1477
  }
1269
1478
 
1270
1479
  getPagination() {
1271
1480
  if (this.hidePagination || this.totalPages <= 1) return null;
1272
1481
 
1273
1482
  return html`
1274
- <zn-button-group>
1275
- <zn-button @click="${this.page !== 1 ? this.goToFirstPage : undefined}"
1276
- ?disabled="${this.page === 1}"
1277
- icon="chevrons-left@lu"
1278
- icon-button="small"
1279
- label="First page">
1280
- </zn-button>
1281
- <zn-button @click="${this.page !== 1 ? this.goToPreviousPage : undefined}"
1282
- ?disabled="${this.page === 1}"
1483
+ <div class="table__footer__pages">
1484
+ <zn-button class="table__footer__page"
1485
+ color="transparent"
1486
+ size="small"
1283
1487
  icon="chevron-left@lu"
1284
- icon-button="small"
1285
- label="Previous page">
1488
+ icon-size="16"
1489
+ ?disabled="${this.page === 1}"
1490
+ aria-label="Previous page"
1491
+ @click="${this.goToPreviousPage}">
1286
1492
  </zn-button>
1287
- ${this.getPageRange().map((p: number) => html`
1288
- <zn-button @click="${p !== this.page ? () => this.goToPage(p) : undefined}"
1289
- class="${classMap({
1290
- 'table__footer__pagination-page': true,
1291
- 'table__footer__pagination-page--active': p === this.page,
1292
- })}"
1293
- icon-button="small"
1294
- aria-current="${ifDefined(p === this.page ? 'page' : undefined)}">${p}
1295
- </zn-button>`)}
1296
- <zn-button @click="${this.page !== this.totalPages ? this.goToNextPage : undefined}"
1297
- ?disabled="${this.page === this.totalPages}"
1493
+ ${this.getPageRange().map((p) => p === null
1494
+ ? html`<span class="table__footer__gap">…</span>`
1495
+ : html`
1496
+ <zn-button class="${classMap({
1497
+ 'table__footer__page': true,
1498
+ 'table__footer__page--active': p === this.page,
1499
+ })}"
1500
+ color="transparent"
1501
+ size="small"
1502
+ aria-current="${ifDefined(p === this.page ? 'page' : undefined)}"
1503
+ @click="${p !== this.page ? () => this.goToPage(p) : undefined}">${p}
1504
+ </zn-button>`)}
1505
+ <zn-button class="table__footer__page"
1506
+ color="transparent"
1507
+ size="small"
1298
1508
  icon="chevron-right@lu"
1299
- icon-button="small"
1300
- label="Next page">
1301
- </zn-button>
1302
- <zn-button @click="${this.page !== this.totalPages ? this.goToLastPage : undefined}"
1509
+ icon-size="16"
1303
1510
  ?disabled="${this.page === this.totalPages}"
1304
- icon="chevrons-right@lu"
1305
- icon-button="small"
1306
- label="Last page">
1511
+ aria-label="Next page"
1512
+ @click="${this.goToNextPage}">
1307
1513
  </zn-button>
1308
- </zn-button-group>`;
1514
+ </div>`;
1309
1515
  }
1310
1516
 
1311
1517
  getActions() {
@@ -1360,10 +1566,17 @@ export default class ZnDataTable extends ZincElement {
1360
1566
  </zn-menu>
1361
1567
  </zn-dropdown>
1362
1568
  <div class="table__actions-source">
1363
- <slot name="${ActionSlots.actions.valueOf()}"></slot>
1569
+ <slot name="${ActionSlots.actions.valueOf()}" @slotchange="${this.hideActionSources}"></slot>
1364
1570
  </div>`;
1365
1571
  }
1366
1572
 
1573
+ // Inert per item rather than on the wrapper: an inert ancestor would also make a zn-confirm's modal dialog inert
1574
+ private hideActionSources = (event: Event) => {
1575
+ (event.target as HTMLSlotElement).assignedElements().forEach(el => {
1576
+ if (!(el instanceof ZnConfirm)) (el as HTMLElement).inert = true;
1577
+ });
1578
+ }
1579
+
1367
1580
  private actionTrigger(item: HTMLElement): HTMLElement | null {
1368
1581
  if (item instanceof ZnConfirm) {
1369
1582
  return item.querySelector<HTMLElement>(':scope > [slot="trigger"]');
@@ -1390,12 +1603,24 @@ export default class ZnDataTable extends ZincElement {
1390
1603
  }
1391
1604
 
1392
1605
  goToPage(page: number) {
1606
+ const focused = this.shadowRoot?.activeElement;
1393
1607
  this.page = page;
1394
1608
  this.selectedRows = [];
1395
1609
  this.numberOfRowsSelected = 0;
1396
1610
  this._dataTask.run().then(r => r);
1397
1611
 
1398
1612
  this.resizeObserver.observe(this);
1613
+
1614
+ if (focused instanceof ZnButton && focused.classList.contains('table__footer__page')) {
1615
+ this.keepPagerFocus(focused);
1616
+ }
1617
+ }
1618
+
1619
+ private async keepPagerFocus(button: ZnButton) {
1620
+ await this.updateComplete;
1621
+ if (button.disabled || !button.isConnected) {
1622
+ this.shadowRoot?.querySelector<ZnButton>('.table__footer__page--active')?.button?.focus();
1623
+ }
1399
1624
  }
1400
1625
 
1401
1626
  goToFirstPage() {
@@ -1559,10 +1784,14 @@ export default class ZnDataTable extends ZincElement {
1559
1784
  }
1560
1785
 
1561
1786
  if (data.uri) {
1787
+ // A browser target such as _blank opens outside the console, which only leaves links with a target alone
1788
+ const browserTarget = data.target?.startsWith('_') ? data.target : undefined;
1562
1789
  content = html`
1563
1790
  <a href="${data.uri}"
1564
1791
  title="${ifDefined(data.title)}"
1565
- data-target="${ifDefined(data.target || nothing)}"
1792
+ target="${ifDefined(browserTarget || nothing)}"
1793
+ rel="${ifDefined(browserTarget ? 'noopener noreferrer' : nothing)}"
1794
+ data-target="${ifDefined(browserTarget ? nothing : (data.target || nothing))}"
1566
1795
  gaid="${ifDefined(data.gaid || nothing)}">${content}</a>`;
1567
1796
  }
1568
1797
 
@@ -1571,6 +1800,11 @@ export default class ZnDataTable extends ZincElement {
1571
1800
  <zn-chip type="${data.chipColor}" title="${ifDefined(data.title)}">${content}</zn-chip>`;
1572
1801
  }
1573
1802
 
1803
+ if (data.badgeType) {
1804
+ content = html`
1805
+ <zn-badge type="${data.badgeType}" ?dot="${data.badgeDot}" title="${ifDefined(data.title)}">${content}</zn-badge>`;
1806
+ }
1807
+
1574
1808
 
1575
1809
  if (data.iconSrc) {
1576
1810
  const src = data.iconSrc;
@@ -1648,47 +1882,36 @@ export default class ZnDataTable extends ZincElement {
1648
1882
  }
1649
1883
  }
1650
1884
 
1651
- private getTableSortIcon(key: any) {
1652
- if (this.sortColumn !== key) {
1653
- return html`
1654
- <div class="table__head__sort">
1655
- <zn-icon src="chevrons_up_down@lu" size="14"></zn-icon>
1656
- </div>`;
1657
- }
1658
-
1659
- if (this.sortDirection === 'asc') {
1660
- return html`
1661
- <div class="table__head__sort table__head__sort--active">
1662
- <zn-icon src="chevron_down@lu" size="16"></zn-icon>
1663
- </div>`;
1664
- }
1665
-
1666
- return html`
1667
- <div class="table__head__sort table__head__sort--active">
1668
- <zn-icon src="chevron_up@lu" size="16"></zn-icon>
1669
- </div>`;
1670
- }
1671
-
1672
1885
  private renderCellHeader(header: HeaderConfig) {
1673
1886
  const sortable = !Object.values(this.unsortableHeaders).includes(header.key) && !Object.values(this.hiddenHeaders).includes(header.key) && !this.unsortable && header.sortable !== false;
1674
1887
 
1675
1888
  const lastVisibleHeaderKey = this.visibleHeaders().slice(-1)[0]?.key;
1889
+ const reorderable = this.reorderable();
1676
1890
 
1677
1891
  return html`
1678
1892
  <th
1893
+ data-column="${header.key}"
1679
1894
  class="${classMap({
1680
1895
  'table__head': true,
1681
1896
  'table__head--wide': header.key === this.wideColumn,
1682
1897
  'table__head--last': header.key === lastVisibleHeaderKey,
1683
1898
  'table__head--hidden': Object.values(this.hiddenHeaders).includes(header.key),
1899
+ 'table__head--dragging': header.key === this.reorderController.draggedKey,
1900
+ 'table__head--reorderable': reorderable,
1901
+
1902
+ [`table__head--${header.align}`]: !!header.align,
1684
1903
  })}"
1685
- tabindex="${ifDefined(sortable ? '0' : undefined)}"
1904
+ tabindex="${ifDefined(sortable || reorderable ? '0' : undefined)}"
1686
1905
  aria-sort="${ifDefined(sortable ? this.sortState(header.key) : undefined)}"
1906
+ aria-description="${ifDefined(reorderable ? 'Alt + arrow keys to move column' : undefined)}"
1687
1907
  @click="${sortable ? this.updateSort(header.key) : undefined}"
1688
- @keydown="${sortable ? (e: KeyboardEvent) => this.handleSortKeyDown(e, header.key) : undefined}">
1908
+ @keydown="${(e: KeyboardEvent) => {
1909
+ if (!this.reorderController.handleKeyDown(e, header.key) && sortable) sortKeyHandler(this.updateSort(header.key))(e);
1910
+ }}"
1911
+ @pointerdown="${reorderable ? (e: PointerEvent) => this.reorderController.handlePointerDown(e, header.key) : undefined}">
1689
1912
  <div>
1690
1913
  ${header.label}
1691
- ${sortable ? this.getTableSortIcon(header.key) : nothing}
1914
+ ${sortable ? renderSortIcon(this.sortState(header.key)) : nothing}
1692
1915
  </div>
1693
1916
  </th>
1694
1917
  `;
@@ -1699,12 +1922,6 @@ export default class ZnDataTable extends ZincElement {
1699
1922
  return this.sortDirection === 'asc' ? 'ascending' : 'descending';
1700
1923
  }
1701
1924
 
1702
- private handleSortKeyDown(e: KeyboardEvent, key: string) {
1703
- if (e.key !== 'Enter' && e.key !== ' ') return;
1704
- e.preventDefault();
1705
- this.updateSort(key)();
1706
- }
1707
-
1708
1925
  private renderCellBody(index: number, value: Cell, row: Row) {
1709
1926
  const filteredHeaders = this.visibleHeaders();
1710
1927
  const header: HeaderConfig | undefined = filteredHeaders[index];
@@ -1712,11 +1929,15 @@ export default class ZnDataTable extends ZincElement {
1712
1929
 
1713
1930
  return html`
1714
1931
  <td
1932
+ data-column="${ifDefined(headerKey)}"
1715
1933
  @click="${this.isSelectable() ? this.selectRow : undefined}"
1716
1934
  class="${classMap({
1717
1935
  'table__cell': true,
1936
+ 'table__cell--dragging': !!headerKey && headerKey === this.reorderController.draggedKey,
1937
+
1718
1938
  'table__cell--wide': headerKey === this.wideColumn,
1719
1939
  'table__cell--last': index === filteredHeaders.length - 1,
1940
+ [`table__cell--${header?.align}`]: !!header?.align,
1720
1941
  })}">
1721
1942
  <div>${this.renderCell(value, row, header)}</div>
1722
1943
  </td>`;
@@ -1801,8 +2022,9 @@ export default class ZnDataTable extends ZincElement {
1801
2022
  if (changed) this.updateKeys();
1802
2023
  }
1803
2024
 
2025
+ /** Rows are selectable when an action works on the selection; creating does not. */
1804
2026
  private isSelectable(): boolean {
1805
- return [ActionSlots.delete, ActionSlots.modify, ActionSlots.actions, ActionSlots.create]
2027
+ return [ActionSlots.delete, ActionSlots.modify, ActionSlots.actions]
1806
2028
  .some(slot => this.hasSlotController.test(slot.valueOf()));
1807
2029
  }
1808
2030
 
@@ -1926,7 +2148,7 @@ export default class ZnDataTable extends ZincElement {
1926
2148
 
1927
2149
  private loadingTable() {
1928
2150
  return html`
1929
- <div class="table__scroll">
2151
+ <div class="table__scroll table__enter">
1930
2152
  <table class="table">
1931
2153
  <thead>
1932
2154
  <tr>