@kubex/zinc 1.1.184 → 2.0.0

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 (362) 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 +10344 -2583
  6. package/dist/vscode.html-custom-data.json +1102 -125
  7. package/dist/web-types.json +2217 -224
  8. package/dist/zn.d.ts +2306 -417
  9. package/dist/zn.min.css +1 -1
  10. package/dist/zn.min.js +2313 -1635
  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/filter-wrapper.md +20 -0
  33. package/docs/pages/components/icon.md +46 -1
  34. package/docs/pages/components/input.md +30 -11
  35. package/docs/pages/components/item.md +35 -0
  36. package/docs/pages/components/key-value.md +25 -0
  37. package/docs/pages/components/menu.md +74 -7
  38. package/docs/pages/components/nav-item.md +15 -0
  39. package/docs/pages/components/nav-list.md +132 -0
  40. package/docs/pages/components/navbar.md +100 -1
  41. package/docs/pages/components/page.md +242 -74
  42. package/docs/pages/components/priority-list.md +43 -5
  43. package/docs/pages/components/query-builder.md +23 -0
  44. package/docs/pages/components/radio-group.md +33 -1
  45. package/docs/pages/components/radio.md +1 -1
  46. package/docs/pages/components/report-table.md +202 -0
  47. package/docs/pages/components/stat.md +30 -0
  48. package/docs/pages/components/tile.md +11 -0
  49. package/docs/pages/components/tooltip.md +1 -1
  50. package/docs/pages/components/translation-grid.md +86 -0
  51. package/docs/pages/components/vertical-stepper.md +108 -64
  52. package/package.json +2 -1
  53. package/scss/_global-spacing.scss +2 -2
  54. package/scss/_root.scss +11 -171
  55. package/scss/boot.scss +14 -0
  56. package/scss/mixins/_helper-mixins.scss +24 -2
  57. package/scss/mixins/_motion-mixins.scss +126 -0
  58. package/scss/mixins/_spacing-mixins.scss +4 -3
  59. package/scss/mixins/_tab-indicator-mixins.scss +12 -0
  60. package/scss/mixins/_typography-mixins.scss +1 -1
  61. package/scss/mixins/index.scss +14 -0
  62. package/scss/shared/_base.scss +32 -1
  63. package/scss/shared/_button-mixin.scss +7 -5
  64. package/scss/shared/_table.scss +35 -4
  65. package/scss/shared/nav.scss +4 -0
  66. package/scss/shared/shimmer.scss +1 -1
  67. package/scss/table-controls.scss +206 -0
  68. package/scss/themes/_dark.scss +1 -3
  69. package/scss/themes/_index.scss +1 -1
  70. package/scss/themes/_light.scss +46 -6
  71. package/scss/themes/_modes.scss +11 -11
  72. package/scss/themes/_openbyte.scss +944 -0
  73. package/scss/themes/_typography.scss +4 -2
  74. package/scss/variables/index.scss +0 -9
  75. package/src/components/alert/alert.component.ts +14 -4
  76. package/src/components/alert/alert.scss +55 -69
  77. package/src/components/alert/alert.test.ts +8 -0
  78. package/src/components/animated-button/animated-button.component.ts +3 -2
  79. package/src/components/animated-button/animated-button.scss +6 -23
  80. package/src/components/animated-button/animated-button.test.ts +4 -3
  81. package/src/components/avatar-group/avatar-group.component.ts +84 -0
  82. package/src/components/avatar-group/avatar-group.scss +54 -0
  83. package/src/components/avatar-group/avatar-group.test.ts +59 -0
  84. package/src/components/avatar-group/index.ts +12 -0
  85. package/src/components/badge/badge.component.ts +49 -0
  86. package/src/components/badge/badge.scss +80 -0
  87. package/src/components/badge/badge.test.ts +34 -0
  88. package/src/components/badge/index.ts +12 -0
  89. package/src/components/bulk-actions/bulk-actions.component.ts +14 -2
  90. package/src/components/button/button.component.ts +242 -11
  91. package/src/components/button/button.scss +133 -25
  92. package/src/components/button/button.test.ts +122 -0
  93. package/src/components/button-menu/button-menu.component.ts +32 -11
  94. package/src/components/button-menu/button-menu.scss +5 -1
  95. package/src/components/channel-tile/channel-tile.component.ts +34 -27
  96. package/src/components/channel-tile/channel-tile.scss +76 -43
  97. package/src/components/chart/builders.test.ts +46 -0
  98. package/src/components/chart/builders.ts +27 -3
  99. package/src/components/chart/chart-live.test.ts +4 -6
  100. package/src/components/chart/chart.component.ts +125 -5
  101. package/src/components/chart/chart.scss +121 -0
  102. package/src/components/chart/chart.test.ts +81 -0
  103. package/src/components/chat-message/chat-message.component.ts +21 -1
  104. package/src/components/chat-message/chat-message.scss +13 -2
  105. package/src/components/chat-message/chat-message.test.ts +9 -0
  106. package/src/components/checkbox/checkbox.component.ts +61 -27
  107. package/src/components/checkbox/checkbox.scss +58 -25
  108. package/src/components/checkbox-group/checkbox-group.component.ts +7 -1
  109. package/src/components/chip/chip.component.ts +11 -1
  110. package/src/components/chip/chip.scss +91 -26
  111. package/src/components/chip/chip.test.ts +17 -0
  112. package/src/components/collapsible/collapsible.component.ts +17 -3
  113. package/src/components/collapsible/collapsible.scss +19 -10
  114. package/src/components/color-picker/color-picker.component.ts +277 -0
  115. package/src/components/color-picker/color-picker.scss +141 -0
  116. package/src/components/color-picker/color-picker.test.ts +57 -0
  117. package/src/components/color-picker/index.ts +12 -0
  118. package/src/components/cols/cols.component.ts +17 -3
  119. package/src/components/cols/cols.scss +24 -13
  120. package/src/components/cols/cols.test.ts +33 -0
  121. package/src/components/confirm/confirm.component.ts +20 -6
  122. package/src/components/confirm/confirm.test.ts +52 -0
  123. package/src/components/content-block/content-block.component.ts +38 -9
  124. package/src/components/content-block/content-block.scss +11 -5
  125. package/src/components/content-block/content-block.test.ts +26 -0
  126. package/src/components/copy-button/copy-button.component.ts +2 -2
  127. package/src/components/copy-button/copy-button.scss +17 -4
  128. package/src/components/data-select/data-select.component.ts +3 -1
  129. package/src/components/data-table/data-table.component.ts +364 -142
  130. package/src/components/data-table/data-table.scss +133 -60
  131. package/src/components/data-table/data-table.test.ts +356 -4
  132. package/src/components/data-table-filter/data-table-filter.component.ts +426 -406
  133. package/src/components/data-table-filter/data-table-filter.scss +176 -148
  134. package/src/components/data-table-filter/data-table-filter.test.ts +271 -273
  135. package/src/components/data-table-search/data-table-search.component.ts +4 -2
  136. package/src/components/data-table-search/data-table-search.scss +8 -1
  137. package/src/components/data-table-search/data-table-search.test.ts +29 -0
  138. package/src/components/date-range/date-range-math.test.ts +78 -0
  139. package/src/components/date-range/date-range-math.ts +105 -0
  140. package/src/components/date-range/date-range.component.ts +375 -0
  141. package/src/components/date-range/date-range.scss +272 -0
  142. package/src/components/date-range/date-range.test.ts +247 -0
  143. package/src/components/date-range/index.ts +12 -0
  144. package/src/components/datepicker/datepicker.component.ts +44 -7
  145. package/src/components/datepicker/datepicker.scss +9 -0
  146. package/src/components/datepicker/datepicker.test.ts +2 -2
  147. package/src/components/dialog/dialog.component.ts +38 -19
  148. package/src/components/dialog/dialog.scss +65 -36
  149. package/src/components/dialog/dialog.test.ts +14 -0
  150. package/src/components/diff/diff.component.ts +55 -0
  151. package/src/components/diff/diff.scss +46 -0
  152. package/src/components/diff/diff.test.ts +15 -0
  153. package/src/components/diff/index.ts +12 -0
  154. package/src/components/diff-row/diff-row.component.ts +75 -0
  155. package/src/components/diff-row/diff-row.scss +70 -0
  156. package/src/components/diff-row/diff-row.test.ts +37 -0
  157. package/src/components/diff-row/index.ts +12 -0
  158. package/src/components/dropdown/dropdown.component.ts +12 -10
  159. package/src/components/dropdown/dropdown.scss +2 -0
  160. package/src/components/editor/editor.component.ts +159 -11
  161. package/src/components/editor/editor.scss +250 -1
  162. package/src/components/editor/editor.test.ts +31 -3
  163. package/src/components/editor/modules/ai/panel/ai-panel.scss +0 -1
  164. package/src/components/editor/modules/attachment/attachment.ts +53 -31
  165. package/src/components/editor/modules/context-menu/context-menu.ts +17 -0
  166. package/src/components/editor/modules/dialog/dialog.component.ts +23 -1
  167. package/src/components/editor/modules/dialog/dialog.test.ts +28 -0
  168. package/src/components/editor/modules/emoji/emoji.ts +4 -5
  169. package/src/components/editor/modules/merge-field/merge-field.ts +83 -0
  170. package/src/components/editor/modules/toolbar/toolbar.component.ts +121 -68
  171. package/src/components/editor/modules/toolbar/toolbar.scss +38 -0
  172. package/src/components/editor/modules/toolbar/toolbar.ts +25 -1
  173. package/src/components/empty-state/empty-state.component.ts +6 -1
  174. package/src/components/empty-state/empty-state.scss +21 -4
  175. package/src/components/expanding-action/expanding-action.component.ts +22 -34
  176. package/src/components/expanding-action/expanding-action.scss +3 -45
  177. package/src/components/expanding-action/expanding-action.test.ts +2 -1
  178. package/src/components/file/file.component.ts +30 -2
  179. package/src/components/file/file.scss +6 -0
  180. package/src/components/filter-wrapper/filter-wrapper.component.ts +45 -10
  181. package/src/components/filter-wrapper/filter-wrapper.test.ts +58 -2
  182. package/src/components/flow-builder/flow-builder.component.ts +36 -3
  183. package/src/components/flow-builder/flow-builder.scss +0 -6
  184. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +8 -0
  185. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +10 -5
  186. package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +17 -0
  187. package/src/components/flow-builder/modules/flow-node/flow-node.scss +10 -0
  188. package/src/components/form-actions/form-actions.component.ts +48 -2
  189. package/src/components/form-actions/form-actions.scss +1 -1
  190. package/src/components/form-group/form-group.component.ts +10 -3
  191. package/src/components/form-group/form-group.scss +37 -4
  192. package/src/components/form-group/form-group.test.ts +10 -0
  193. package/src/components/header/header.component.ts +11 -4
  194. package/src/components/header/header.scss +47 -5
  195. package/src/components/hover-container/hover-container.component.ts +4 -5
  196. package/src/components/icon/icon.component.ts +95 -9
  197. package/src/components/icon/icon.scss +70 -6
  198. package/src/components/icon/icon.test.ts +55 -0
  199. package/src/components/icon-picker/icon-picker.component.ts +35 -1
  200. package/src/components/inline-edit/inline-edit.component.ts +38 -8
  201. package/src/components/inline-edit/inline-edit.scss +128 -19
  202. package/src/components/input/input.component.ts +173 -297
  203. package/src/components/input/input.scss +65 -43
  204. package/src/components/input/input.test.ts +86 -4
  205. package/src/components/item/item.component.ts +12 -0
  206. package/src/components/item/item.scss +102 -3
  207. package/src/components/item/item.test.ts +23 -0
  208. package/src/components/key/key.component.ts +2 -2
  209. package/src/components/key/key.scss +11 -0
  210. package/src/components/key-value/index.ts +12 -0
  211. package/src/components/key-value/key-value.component.ts +160 -0
  212. package/src/components/key-value/key-value.scss +20 -0
  213. package/src/components/key-value/key-value.test.ts +22 -0
  214. package/src/components/markdown-editor/markdown-editor.component.ts +8 -1
  215. package/src/components/menu/menu.component.ts +21 -2
  216. package/src/components/menu/menu.scss +43 -23
  217. package/src/components/menu/menu.test.ts +33 -0
  218. package/src/components/menu-item/menu-item.component.ts +13 -2
  219. package/src/components/menu-item/menu-item.scss +56 -23
  220. package/src/components/menu-item/menu-item.test.ts +31 -0
  221. package/src/components/menu-item/submenu-controller.ts +25 -15
  222. package/src/components/menu-label/index.ts +12 -0
  223. package/src/components/menu-label/menu-label.component.ts +30 -0
  224. package/src/components/menu-label/menu-label.scss +18 -0
  225. package/src/components/nav-item/index.ts +12 -0
  226. package/src/components/nav-item/nav-item.component.ts +83 -0
  227. package/src/components/nav-item/nav-item.scss +128 -0
  228. package/src/components/nav-list/index.ts +12 -0
  229. package/src/components/nav-list/nav-list.component.ts +313 -0
  230. package/src/components/nav-list/nav-list.scss +87 -0
  231. package/src/components/nav-list/nav-list.test.ts +250 -0
  232. package/src/components/navbar/navbar.component.ts +235 -171
  233. package/src/components/navbar/navbar.scss +333 -169
  234. package/src/components/navbar/navbar.test.ts +63 -143
  235. package/src/components/page/page.component.ts +114 -29
  236. package/src/components/page/page.scss +223 -95
  237. package/src/components/page/page.test.ts +214 -3
  238. package/src/components/page-builder/page-builder.component.ts +51 -5
  239. package/src/components/page-builder/page-builder.scss +6 -1
  240. package/src/components/page-nav/page-nav.component.ts +25 -3
  241. package/src/components/page-nav/page-nav.scss +10 -2
  242. package/src/components/pagination/pagination.scss +1 -1
  243. package/src/components/panel/panel.component.ts +14 -13
  244. package/src/components/panel/panel.scss +14 -10
  245. package/src/components/panel/panel.test.ts +42 -0
  246. package/src/components/popup/popup.component.ts +20 -13
  247. package/src/components/popup/popup.test.ts +21 -1
  248. package/src/components/priority-list/priority-list.component.ts +157 -172
  249. package/src/components/priority-list/priority-list.scss +113 -17
  250. package/src/components/priority-list/priority-list.test.ts +301 -3
  251. package/src/components/progress-bar/progress-bar.component.ts +1 -1
  252. package/src/components/progress-bar/progress-bar.scss +16 -4
  253. package/src/components/progress-tile/progress-tile.scss +1 -1
  254. package/src/components/query-builder/query-builder.component.ts +85 -10
  255. package/src/components/query-builder/query-builder.scss +7 -1
  256. package/src/components/query-builder/query-builder.test.ts +40 -0
  257. package/src/components/radio/radio.component.ts +57 -18
  258. package/src/components/radio/radio.scss +101 -26
  259. package/src/components/radio-group/radio-group.component.ts +45 -7
  260. package/src/components/radio-group/radio-group.scss +17 -0
  261. package/src/components/radio-group/radio-group.test.ts +63 -2
  262. package/src/components/rating/rating.component.ts +23 -0
  263. package/src/components/rating/rating.scss +1 -1
  264. package/src/components/remarkd-editor/remarkd-editor.component.ts +60 -5
  265. package/src/components/remarkd-editor/remarkd-editor.scss +12 -4
  266. package/src/components/report-table/index.ts +12 -0
  267. package/src/components/report-table/report-format.test.ts +86 -0
  268. package/src/components/report-table/report-format.ts +72 -0
  269. package/src/components/report-table/report-math.test.ts +74 -0
  270. package/src/components/report-table/report-math.ts +252 -0
  271. package/src/components/report-table/report-table.component.ts +681 -0
  272. package/src/components/report-table/report-table.scss +328 -0
  273. package/src/components/report-table/report-table.test.ts +702 -0
  274. package/src/components/reveal/reveal.component.ts +15 -1
  275. package/src/components/reveal/reveal.scss +5 -0
  276. package/src/components/schedule-builder/schedule-builder.component.ts +23 -8
  277. package/src/components/scroll-container/scroll-container.component.ts +19 -1
  278. package/src/components/scroll-container/scroll-container.scss +6 -1
  279. package/src/components/select/select.component.ts +52 -36
  280. package/src/components/select/select.scss +42 -25
  281. package/src/components/select/select.test.ts +14 -2
  282. package/src/components/settings-container/settings-container.scss +18 -5
  283. package/src/components/sidebar/sidebar.component.ts +8 -1
  284. package/src/components/sidebar/sidebar.scss +6 -1
  285. package/src/components/skeleton/skeleton.component.ts +3 -3
  286. package/src/components/skeleton/skeleton.scss +6 -6
  287. package/src/components/skeleton/skeleton.test.ts +9 -0
  288. package/src/components/slash-menu/slash-menu.scss +1 -1
  289. package/src/components/slideout/slideout.component.ts +20 -3
  290. package/src/components/slideout/slideout.scss +13 -6
  291. package/src/components/slideout/slideout.test.ts +19 -0
  292. package/src/components/split-button/split-button.component.ts +1 -1
  293. package/src/components/split-pane/split-pane.component.ts +71 -8
  294. package/src/components/split-pane/split-pane.scss +145 -41
  295. package/src/components/split-pane/split-pane.test.ts +23 -3
  296. package/src/components/stat/stat.component.ts +209 -14
  297. package/src/components/stat/stat.scss +305 -5
  298. package/src/components/stat/stat.test.ts +143 -2
  299. package/src/components/status-indicator/status-indicator.scss +22 -21
  300. package/src/components/stepper/stepper.scss +3 -3
  301. package/src/components/tab/tab.component.ts +1 -0
  302. package/src/components/table/table.component.ts +12 -1
  303. package/src/components/table/table.scss +21 -13
  304. package/src/components/table/table.test.ts +17 -0
  305. package/src/components/tabs/tabs.component.ts +150 -37
  306. package/src/components/tabs/tabs.scss +48 -7
  307. package/src/components/tabs/tabs.test.ts +169 -0
  308. package/src/components/textarea/textarea.component.ts +25 -3
  309. package/src/components/textarea/textarea.scss +10 -0
  310. package/src/components/textarea/textarea.test.ts +8 -4
  311. package/src/components/theme-editor/theme-editor.scss +0 -6
  312. package/src/components/tile/tile.component.ts +32 -2
  313. package/src/components/tile/tile.scss +40 -7
  314. package/src/components/tile/tile.test.ts +10 -0
  315. package/src/components/toggle/toggle.component.ts +23 -7
  316. package/src/components/toggle/toggle.scss +36 -29
  317. package/src/components/toggle/toggle.test.ts +17 -0
  318. package/src/components/tooltip/tooltip.component.ts +3 -0
  319. package/src/components/tooltip/tooltip.scss +11 -6
  320. package/src/components/tooltip/tooltip.test.ts +12 -0
  321. package/src/components/translation-field/index.ts +12 -0
  322. package/src/components/translation-field/translation-field.component.ts +55 -0
  323. package/src/components/translation-field/translation-field.scss +101 -0
  324. package/src/components/translation-fields/index.ts +12 -0
  325. package/src/components/translation-fields/translation-fields.component.ts +80 -0
  326. package/src/components/translation-fields/translation-fields.scss +60 -0
  327. package/src/components/translation-grid/index.ts +12 -0
  328. package/src/components/translation-grid/translation-grid.component.ts +157 -0
  329. package/src/components/translation-grid/translation-grid.scss +138 -0
  330. package/src/components/translation-grid/translation-grid.test.ts +28 -0
  331. package/src/components/vertical-stepper/vertical-stepper.component.ts +35 -9
  332. package/src/components/vertical-stepper/vertical-stepper.scss +110 -39
  333. package/src/components/vertical-stepper/vertical-stepper.test.ts +35 -0
  334. package/src/components/well/well.scss +2 -2
  335. package/src/events/events.ts +2 -0
  336. package/src/events/zn-chart-click.ts +13 -0
  337. package/src/events/zn-report-change.ts +7 -0
  338. package/src/form-control.scss +36 -3
  339. package/src/internal/anchored-popover.ts +175 -0
  340. package/src/internal/animate.ts +19 -5
  341. package/src/internal/cosmic-border.ts +15 -0
  342. package/src/internal/form.ts +1 -1
  343. package/src/internal/hover-intent.ts +15 -0
  344. package/src/internal/input-modality.ts +15 -0
  345. package/src/internal/pointer-intent.ts +37 -0
  346. package/src/internal/reorder.ts +625 -0
  347. package/src/internal/scroll.ts +51 -3
  348. package/src/internal/selection-card.ts +1 -1
  349. package/src/internal/tab-indicator.ts +92 -0
  350. package/src/internal/table-sort.ts +25 -0
  351. package/src/internal/table-tabs.ts +108 -0
  352. package/src/internal/test-drag.ts +35 -0
  353. package/src/internal/theme-openbyte.test.ts +126 -0
  354. package/src/internal/theme.ts +4 -4
  355. package/src/internal/zinc-element.ts +2 -2
  356. package/src/selection-card.scss +25 -0
  357. package/src/types/web-test-runner-commands.d.ts +1 -0
  358. package/src/utilities/color.test.ts +43 -0
  359. package/src/utilities/color.ts +189 -0
  360. package/src/zinc.ts +14 -0
  361. package/.github/workflows/eleventy_build.yml +0 -42
  362. package/scss/themes/_aura.scss +0 -114
@@ -1,10 +1,151 @@
1
1
  import '../../../dist/zn.min.js';
2
2
  import { expect, fixture, html } from '@open-wc/testing';
3
+ import type ZnStatsTile from './stat.component';
3
4
 
4
- describe('<zn-stats-tile>', () => {
5
+ describe('<zn-stat>', () => {
5
6
  it('should render a component', async () => {
6
- const el = await fixture(html` <zn-stats-tile></zn-stats-tile> `);
7
+ const el = await fixture(html` <zn-stat></zn-stat> `);
7
8
 
8
9
  expect(el).to.exist;
10
+ expect(el.shadowRoot).to.exist;
11
+ });
12
+
13
+ it('renders a positive delta in the success text colour', async () => {
14
+ const el = await fixture<HTMLElement>(html`
15
+ <zn-stat amount="110" previous="100" show-delta style="--zn-color-success-fg: rgb(1, 2, 3)"></zn-stat>`);
16
+ const positive = el.shadowRoot!.querySelector('.positive')!;
17
+
18
+ expect(positive).to.exist;
19
+ expect(getComputedStyle(positive).color).to.equal('rgb(1, 2, 3)');
20
+ });
21
+
22
+ it('renders the whole stat as a link with an arrow when given an href', async () => {
23
+ const el = await fixture<HTMLElement>(html`
24
+ <zn-stat layout="value-first" caption="Fails" amount="3" href="/fails" data-target="primary"></zn-stat>`);
25
+ await (el as unknown as {updateComplete: Promise<unknown>}).updateComplete;
26
+ const link = el.shadowRoot!.querySelector('a.stat-link')!;
27
+
28
+ expect(link).to.exist;
29
+ expect(link.getAttribute('href')).to.equal('/fails');
30
+ expect(link.getAttribute('data-target')).to.equal('primary');
31
+ expect(link.querySelector('.caption')!.textContent).to.equal('Fails');
32
+ expect(link.querySelector('zn-icon.stat-link__icon')).to.exist;
33
+ });
34
+
35
+ it('renders no link without an href', async () => {
36
+ const el = await fixture<HTMLElement>(html` <zn-stat amount="3"></zn-stat> `);
37
+
38
+ expect(el.shadowRoot!.querySelector('a')).to.not.exist;
39
+ });
40
+
41
+ it('compares amounts written with separators and symbols', async () => {
42
+ const el = await fixture<HTMLElement>(html`
43
+ <zn-stat layout="tile" amount="$2,941" previous="$2,875" show-delta></zn-stat>`);
44
+ await (el as unknown as {updateComplete: Promise<unknown>}).updateComplete;
45
+ const delta = el.shadowRoot!.querySelector('.delta')!;
46
+
47
+ expect(delta.classList.contains('delta--good')).to.be.true;
48
+ expect(delta.textContent!.trim()).to.equal('2.3%');
49
+ });
50
+
51
+ it('shows the plain difference from a previous zero', async () => {
52
+ const el = await fixture<HTMLElement>(html`
53
+ <zn-stat layout="tile" amount="5" previous="0" show-delta invert-delta></zn-stat>`);
54
+ await (el as unknown as {updateComplete: Promise<unknown>}).updateComplete;
55
+ const delta = el.shadowRoot!.querySelector('.delta')!;
56
+
57
+ expect(delta.classList.contains('delta--bad')).to.be.true;
58
+ expect(delta.textContent!.trim()).to.equal('5');
59
+ });
60
+
61
+ it('shows no change from a previous zero that stays zero', async () => {
62
+ const el = await fixture<HTMLElement>(html`
63
+ <zn-stat layout="tile" amount="0" previous="0" show-delta></zn-stat>`);
64
+ await (el as unknown as {updateComplete: Promise<unknown>}).updateComplete;
65
+ const delta = el.shadowRoot!.querySelector('.delta')!;
66
+
67
+ expect(delta.classList.contains('delta--flat')).to.be.true;
68
+ expect(delta.textContent!.trim()).to.equal('No change');
69
+ });
70
+
71
+ it('colours a rise as bad news with invert-delta', async () => {
72
+ const el = await fixture<HTMLElement>(html`
73
+ <zn-stat layout="tile" amount="148" previous="140" show-delta invert-delta></zn-stat>`);
74
+ await (el as unknown as {updateComplete: Promise<unknown>}).updateComplete;
75
+
76
+ expect(el.shadowRoot!.querySelector('.delta')!.classList.contains('delta--bad')).to.be.true;
77
+ });
78
+
79
+ it('draws a sparkline from the spark values', async () => {
80
+ const el = await fixture<HTMLElement>(html`<zn-stat layout="tile" amount="1" spark="1,3,2"></zn-stat>`);
81
+ await (el as unknown as {updateComplete: Promise<unknown>}).updateComplete;
82
+
83
+ expect(el.shadowRoot!.querySelectorAll('.spark path')).to.have.length(2);
84
+ });
85
+
86
+ it('toggles a selectable tile', async () => {
87
+ const el = await fixture<HTMLElement & {selected: boolean; updateComplete: Promise<unknown>}>(html`
88
+ <zn-stat layout="tile" selectable caption="Declined" amount="148"></zn-stat>`);
89
+ await el.updateComplete;
90
+ const button = el.shadowRoot!.querySelector('button')!;
91
+
92
+ expect(button.getAttribute('aria-pressed')).to.equal('false');
93
+ button.click();
94
+ await el.updateComplete;
95
+ expect(el.selected).to.be.true;
96
+ expect(button.getAttribute('aria-pressed')).to.equal('true');
97
+ });
98
+ });
99
+
100
+ describe('<zn-stat> data-url', () => {
101
+ const original = window.fetch;
102
+ afterEach(() => { window.fetch = original; });
103
+
104
+ it('keeps its own attributes until the data loads, then takes the response', async () => {
105
+ let resolve!: (r: Response) => void;
106
+ window.fetch = (() => new Promise<Response>(r => { resolve = r; })) as typeof fetch;
107
+ const el = await fixture<ZnStatsTile>(html`
108
+ <zn-stat layout="tile" caption="Sent" amount="—" data-url="/metrics/stats"></zn-stat>`);
109
+
110
+ expect(el.amount).to.equal('—');
111
+ resolve(new Response(JSON.stringify({amount: '1,204', description: '80 in 24 hours', spark: '1,2,3'})));
112
+ await new Promise(r => setTimeout(r, 50));
113
+ await el.updateComplete;
114
+
115
+ expect(el.caption).to.equal('Sent');
116
+ expect(el.amount).to.equal('1,204');
117
+ expect(el.description).to.equal('80 in 24 hours');
118
+ expect(el.spark).to.equal('1,2,3');
119
+ });
120
+
121
+ it('shows a placeholder while loading and the sparkline once loaded', async () => {
122
+ let resolve!: (r: Response) => void;
123
+ window.fetch = (() => new Promise<Response>(r => { resolve = r; })) as typeof fetch;
124
+ const el = await fixture<ZnStatsTile>(html`
125
+ <zn-stat layout="tile" caption="Sent" data-url="/metrics/stats"></zn-stat>`);
126
+
127
+ expect(el.loading).to.equal(true);
128
+ expect(el.shadowRoot!.querySelector('.amount__skeleton')).to.exist;
129
+ expect(el.shadowRoot!.querySelector('.loading-text')).to.exist;
130
+ expect(el.shadowRoot!.querySelector('.spark__track')).to.exist;
131
+
132
+ resolve(new Response(JSON.stringify({amount: '12', spark: '1,2,3'})));
133
+ await new Promise(r => setTimeout(r, 50));
134
+ await el.updateComplete;
135
+
136
+ expect(el.loading).to.equal(false);
137
+ expect(el.shadowRoot!.querySelector('.amount__skeleton')).to.not.exist;
138
+ expect(el.shadowRoot!.querySelector('.spark svg')).to.exist;
139
+ });
140
+
141
+ it('stops loading when the request fails', async () => {
142
+ window.fetch = (() => Promise.resolve(new Response('', {status: 500}))) as typeof fetch;
143
+ const el = await fixture<ZnStatsTile>(html`
144
+ <zn-stat layout="tile" caption="Sent" amount="0" data-url="/metrics/stats"></zn-stat>`);
145
+ await new Promise(r => setTimeout(r, 50));
146
+ await el.updateComplete;
147
+
148
+ expect(el.loading).to.equal(false);
149
+ expect(el.amount).to.equal('0');
9
150
  });
10
151
  });
@@ -1,65 +1,66 @@
1
1
  @use "../../wc";
2
2
 
3
3
  :host {
4
- --zn-indicator-info: 100, 117, 242;
5
- --zn-indicator-warning: 255, 136, 0;
6
- --zn-indicator-error: 237, 84, 109;
7
- --zn-indicator-success: 43, 192, 145;
8
- --zn-indicator-disabled: 220, 220, 220;
9
-
10
- --indicator-color: var(--zn-indicator-disabled);
11
- --indicator-alt-color: var(--zn-indicator-disabled);
4
+ // Read through fallbacks so a theme can set --zn-indicator-* on the root.
5
+ --indicator-info: var(--zn-indicator-info, 100, 117, 242);
6
+ --indicator-warning: var(--zn-indicator-warning, 255, 136, 0);
7
+ --indicator-error: var(--zn-indicator-error, 237, 84, 109);
8
+ --indicator-success: var(--zn-indicator-success, 43, 192, 145);
9
+ --indicator-disabled: var(--zn-indicator-disabled, 220, 220, 220);
10
+
11
+ --indicator-color: var(--indicator-disabled);
12
+ --indicator-alt-color: var(--indicator-disabled);
12
13
  white-space: nowrap;
13
14
  display: inline-block;
14
15
  }
15
16
 
16
17
  .indicator {
17
18
  display: inline-block;
18
- background-color: rgba(var(--indicator-color), 0.7);
19
+ background-color: rgba(var(--indicator-color), var(--zn-status-indicator-fill-alpha, 0.7));
19
20
  border: 1px solid rgb(var(--indicator-color));
20
- width: 10px;
21
- height: 10px;
21
+ width: var(--zn-status-indicator-size, 10px);
22
+ height: var(--zn-status-indicator-size, 10px);
22
23
  border-radius: 50%;
23
24
 
24
25
  &--info {
25
- --indicator-color: var(--zn-indicator-info);
26
+ --indicator-color: var(--indicator-info);
26
27
  }
27
28
 
28
29
  &--error {
29
- --indicator-color: var(--zn-indicator-error);
30
+ --indicator-color: var(--indicator-error);
30
31
  }
31
32
 
32
33
  &--warning {
33
- --indicator-color: var(--zn-indicator-warning);
34
+ --indicator-color: var(--indicator-warning);
34
35
  }
35
36
 
36
37
  &--success {
37
- --indicator-color: var(--zn-indicator-success);
38
+ --indicator-color: var(--indicator-success);
38
39
  }
39
40
 
40
41
  &--disabled {
41
- --indicator-color: var(--zn-indicator-disabled);
42
+ --indicator-color: var(--indicator-disabled);
42
43
  }
43
44
 
44
45
  // Alternate (secondary) colour, used by the alternating animation.
45
46
  &--alt-info {
46
- --indicator-alt-color: var(--zn-indicator-info);
47
+ --indicator-alt-color: var(--indicator-info);
47
48
  }
48
49
 
49
50
  &--alt-error {
50
- --indicator-alt-color: var(--zn-indicator-error);
51
+ --indicator-alt-color: var(--indicator-error);
51
52
  }
52
53
 
53
54
  &--alt-warning {
54
- --indicator-alt-color: var(--zn-indicator-warning);
55
+ --indicator-alt-color: var(--indicator-warning);
55
56
  }
56
57
 
57
58
  &--alt-success {
58
- --indicator-alt-color: var(--zn-indicator-success);
59
+ --indicator-alt-color: var(--indicator-success);
59
60
  }
60
61
 
61
62
  &--alt-disabled {
62
- --indicator-alt-color: var(--zn-indicator-disabled);
63
+ --indicator-alt-color: var(--indicator-disabled);
63
64
  }
64
65
 
65
66
  &--glow {
@@ -51,11 +51,11 @@
51
51
  width: 100%;
52
52
  left: 2px;
53
53
  height: 2px;
54
- background-color: rgb(var(--zn-border-color));
54
+ background-color: var(--zn-stepper-track-color, rgb(var(--zn-border-color)));
55
55
  }
56
56
 
57
57
  .step-progress {
58
- background-color: rgb(var(--zn-primary));
58
+ background-color: var(--zn-stepper-progress-color, rgb(var(--zn-primary)));
59
59
  height: 8px;
60
60
  position: absolute;
61
61
  top: 2px;
@@ -77,7 +77,7 @@
77
77
  width: 17px;
78
78
  height: 17px;
79
79
  border-radius: 50%;
80
- background-color: rgb(var(--zn-border-color));
80
+ background-color: var(--zn-stepper-track-color, rgb(var(--zn-border-color)));
81
81
  display: flex;
82
82
  align-items: center;
83
83
  justify-content: center;
@@ -16,6 +16,7 @@ export default class ZnTab extends ZincElement {
16
16
  static styles: CSSResultGroup = unsafeCSS(styles);
17
17
 
18
18
  @property() caption: string;
19
+ @property() icon: string;
19
20
  @property({type: Number}) priority: number;
20
21
  @property() uri: string;
21
22
  /** When present, zn-page opens this tab on load instead of the first tab. */
@@ -231,7 +231,8 @@ export default class ZnTable extends ZincElement {
231
231
  'table-row--info': color === 'info',
232
232
  'table-row--warning': color === 'warning',
233
233
  'table-row--success': color === 'success',
234
- })}">${rowHtml}
234
+ 'table-row--link': typeof uri === 'string' && uri !== '',
235
+ })}" @click="${this.handleRowClick}">${rowHtml}
235
236
  </tr>`);
236
237
  });
237
238
 
@@ -239,6 +240,16 @@ export default class ZnTable extends ZincElement {
239
240
  <tbody>${rows}</tbody>`;
240
241
  }
241
242
 
243
+ // A row with a uri opens it from anywhere in the row, through its caption link so the link's target applies.
244
+ // Clicks on the row's own controls, and drags that select text, are left to them.
245
+ private handleRowClick = (e: MouseEvent) => {
246
+ const row = e.currentTarget as HTMLElement;
247
+ if (!row.classList.contains('table-row--link') || window.getSelection()?.toString()) return;
248
+ const control = e.composedPath().find(el => el instanceof HTMLElement && el.matches('a, button, input, select, textarea, zn-dropdown, zn-button'));
249
+ if (control) return;
250
+ row.querySelector<HTMLAnchorElement>('td.capcol a')?.click();
251
+ };
252
+
242
253
  columnContent(col: any) {
243
254
  if (typeof col !== 'object' || col === null) {
244
255
  if (this.boolIcons && (col.toString() === 'true' || col.toString() === 'false')) {
@@ -40,7 +40,7 @@ table, thead, tbody, tr, td, th {
40
40
 
41
41
  tr td {
42
42
  min-height: 40px;
43
- height: 52px;
43
+ height: var(--zn-table-row-height, 52px);
44
44
  }
45
45
 
46
46
  tr.with-summary td {
@@ -49,27 +49,27 @@ tr.with-summary td {
49
49
  }
50
50
 
51
51
  .table-row.table-row--error {
52
- color: rgb(var(--zn-color-error));
53
- border-color: rgb(var(--zn-color-error));
54
- background: rgba(var(--zn-color-error), .10);
52
+ color: var(--zn-color-danger-fg);
53
+ border-color: var(--zn-color-danger-solid);
54
+ background: var(--zn-color-danger-bg);
55
55
  }
56
56
 
57
57
  .table-row.table-row--info {
58
- color: rgb(var(--zn-color-info));
59
- border-color: rgb(var(--zn-color-info));
60
- background: rgba(var(--zn-color-info), .10);
58
+ color: var(--zn-color-info-fg);
59
+ border-color: var(--zn-color-info-solid);
60
+ background: var(--zn-color-info-bg);
61
61
  }
62
62
 
63
63
  .table-row.table-row--warning {
64
- color: rgb(var(--zn-color-warning));
65
- border-color: rgb(var(--zn-color-warning));
66
- background: rgba(var(--zn-color-warning), .10);
64
+ color: var(--zn-color-warning-fg);
65
+ border-color: var(--zn-color-warning-solid);
66
+ background: var(--zn-color-warning-bg);
67
67
  }
68
68
 
69
69
  .table-row.table-row--success {
70
- color: rgb(var(--zn-color-success)) !important;
71
- border-color: rgb(var(--zn-color-success));
72
- background: rgba(var(--zn-color-success), .10);
70
+ color: var(--zn-color-success-fg) !important;
71
+ border-color: var(--zn-color-success-solid);
72
+ background: var(--zn-color-success-bg);
73
73
  }
74
74
 
75
75
  .table-row.table-row--nested td:first-of-type {
@@ -204,3 +204,11 @@ zn-table[fixed] {
204
204
  table-layout: fixed;
205
205
  }
206
206
  }
207
+
208
+ .table-row--link {
209
+ cursor: pointer;
210
+
211
+ &:hover td {
212
+ background-color: var(--zn-table-row-hover-background, rgba(var(--zn-primary), 0.05));
213
+ }
214
+ }
@@ -7,4 +7,21 @@ describe('<zn-table>', () => {
7
7
 
8
8
  expect(el).to.exist;
9
9
  });
10
+
11
+ it('opens a row with a uri from anywhere in the row through its caption link', async () => {
12
+ const data = {header: ['Type', 'Connector'], items: [{uri: '/tx/1', target: 'slide', caption: 'AUTH', data: ['sandbanx']}]};
13
+ const el = await fixture<HTMLElement>(html`<zn-table data="${JSON.stringify(data)}"></zn-table>`);
14
+ await (el as unknown as {updateComplete: Promise<unknown>}).updateComplete;
15
+
16
+ const link = el.shadowRoot!.querySelector<HTMLAnchorElement>('td.capcol a')!;
17
+ let clicks = 0;
18
+ link.addEventListener('click', e => { e.preventDefault(); clicks++; });
19
+
20
+ const row = el.shadowRoot!.querySelector<HTMLElement>('tr.table-row--link')!;
21
+ row.querySelectorAll('td')[1].click();
22
+ expect(clicks).to.equal(1);
23
+
24
+ link.click();
25
+ expect(clicks).to.equal(2);
26
+ });
10
27
  });
@@ -9,7 +9,7 @@ import {
9
9
  replaceHistoryTab,
10
10
  TAB_STORE_PREFIX
11
11
  } from './tabs-navigation';
12
- import {getScrolledAncestors, scrollToTop} from "../../internal/scroll";
12
+ import {getScrolledAncestors, revealTop} from "../../internal/scroll";
13
13
  import {HasSlotController} from "../../internal/slot";
14
14
  import {ifDefined} from "lit/directives/if-defined.js";
15
15
  import {md5} from "../../utilities/md5";
@@ -17,15 +17,17 @@ import {MutationController} from '@lit-labs/observers/mutation-controller.js';
17
17
  import {property} from 'lit/decorators.js';
18
18
  import {Store} from "../../internal/storage";
19
19
  import ZincElement from '../../internal/zinc-element';
20
+ import {onHoverIntent} from '../../internal/hover-intent';
21
+ import {pointerInitiated} from '../../internal/input-modality';
20
22
 
21
23
  // Tabs load in such a way that the global sheet has to be re-applied inside the shadow root.
22
24
  import bootStyles from '../../../scss/boot.scss';
23
25
  import styles from './tabs.scss';
24
26
 
25
- // Every element that manages its own tabs (ZnTabs and its subclasses). Tab
26
- // registration must stop at these so an outer container never claims a nested
27
- // container's tabs.
28
- export const tabContainerSelector = 'zn-tabs, zn-page, zn-page-nav';
27
+ // Every element that manages its own tabs (ZnTabs and its subclasses), plus a navbar
28
+ // linked to a container elsewhere. Tab registration must stop at these so an outer
29
+ // container never claims a nested or linked container's tabs.
30
+ export const tabContainerSelector = 'zn-tabs, zn-page, zn-page-nav, zn-navbar[tabs]';
29
31
 
30
32
  /**
31
33
  * @summary Short summary of the component's intended use.
@@ -49,6 +51,7 @@ export default class ZnTabs extends ZincElement {
49
51
  @property({attribute: 'master-id', reflect: true}) masterId: string;
50
52
  @property({attribute: 'default-uri', reflect: true}) defaultUri = '';
51
53
  @property({attribute: 'active', reflect: true}) _current = '';
54
+ private _hasSelected = false;
52
55
  @property({attribute: 'split', type: Number, reflect: true}) _split: number;
53
56
  @property({attribute: 'split-min', type: Number, reflect: true}) _splitMin = 60;
54
57
  @property({attribute: 'split-min-secondary', type: Number, reflect: true}) _splitMinSecondary: number;
@@ -62,6 +65,14 @@ export default class ZnTabs extends ZincElement {
62
65
  @property({attribute: 'store-key'}) storeKey: string;
63
66
  @property({attribute: 'store-ttl', type: Number, reflect: true}) storeTtl = 0;
64
67
  @property({attribute: 'padded', type: Boolean, reflect: true}) padded = false;
68
+ /**
69
+ * With `store-key`, opens a tab that has appeared since the page was last shown, such as one for an item just
70
+ * added, in place of the remembered tab.
71
+ */
72
+ @property({attribute: 'select-added', type: Boolean, reflect: true}) selectAdded = false;
73
+
74
+ /** Spaces a left or right slot from the content, for side navigation laid out as its own card. */
75
+ @property({type: Boolean, reflect: true}) gap = false;
65
76
  @property({attribute: 'fetch-style', type: String, reflect: true}) fetchStyle = "";
66
77
  @property({attribute: 'full-width', type: Boolean, reflect: true}) fullWidth = false;
67
78
  @property({attribute: 'padded-right', type: Boolean, reflect: true}) paddedRight = false;
@@ -232,9 +243,43 @@ export default class ZnTabs extends ZincElement {
232
243
  }
233
244
 
234
245
  this._prepareTab(stored);
246
+ // The stored tab can be gone, say after deleting what it showed; fall back to the tab the container starts on.
247
+ // A tab still on screen keeps its selection even before its panel loads.
248
+ const gone = !this._panels.has(stored) && !this._tabs.some(tab => tab.getAttribute('tab') === stored);
249
+ if (gone && this._defaultTab !== '' && this._panels.has(this._defaultTab)) {
250
+ this.setActiveTab(this._defaultTab, true, false);
251
+ return;
252
+ }
235
253
  this.setActiveTab(stored, true, false);
236
254
  }
237
255
 
256
+ /**
257
+ * The tabs present now that were not when the page was last shown, oldest first, remembering the current set for
258
+ * next time. Empty without `select-added`, or on the first showing.
259
+ */
260
+ private takeAddedTabs(): string[] {
261
+ const key = this.getTabStoreKey();
262
+ if (!this.selectAdded || key === null || !this._store) {
263
+ return [];
264
+ }
265
+
266
+ const storeKey = locationScopedKey(`${key}:tabs`);
267
+ const current = this._tabs.map(tab => tab.getAttribute('tab')).filter((id): id is string => id !== null);
268
+ const known = this._store.get(storeKey);
269
+ this._store.set(storeKey, JSON.stringify(current));
270
+ if (known === null) {
271
+ return [];
272
+ }
273
+
274
+ let knownIds: Set<string>;
275
+ try {
276
+ knownIds = new Set(JSON.parse(known) as string[]);
277
+ } catch {
278
+ return [];
279
+ }
280
+ return current.filter(id => !knownIds.has(id));
281
+ }
282
+
238
283
  /** Activates the tab the container starts on, ignoring any stored selection. */
239
284
  protected activateDefaultTab() {
240
285
  if (!this._panels.has(this._defaultTab) && this._tabs.length > 0) {
@@ -300,11 +345,43 @@ export default class ZnTabs extends ZincElement {
300
345
  }
301
346
  this._tabs.push(tab);
302
347
  if (this.preload) {
303
- tab.addEventListener('mouseover', this.fetchUriTab.bind(this, tab));
348
+ onHoverIntent(tab, () => this.fetchUriTab(tab));
304
349
  }
305
350
  tab.addEventListener('click', this._handleClick.bind(this));
351
+
352
+ // Plain elements (usually <li>) are not focusable; custom elements and tabs wrapping a control manage their own
353
+ const focusable = 'a[href], button, input, select, textarea, [tabindex]';
354
+ if (!tab.localName.includes('-') && !tab.matches(focusable) && !tab.querySelector(focusable)) {
355
+ tab.tabIndex = 0;
356
+ tab.addEventListener('keydown', this._handleTabKeyDown);
357
+ }
306
358
  }
307
359
 
360
+ private readonly _handleTabKeyDown = (event: KeyboardEvent) => {
361
+ const tab = event.currentTarget as HTMLElement;
362
+ if (event.key === 'Enter' || event.key === ' ') {
363
+ event.preventDefault();
364
+ tab.click();
365
+ return;
366
+ }
367
+
368
+ const siblings = this._tabs.filter(t => t.parentElement === tab.parentElement && t.tabIndex >= 0);
369
+ const index = siblings.indexOf(tab);
370
+ const target = {
371
+ ArrowRight: index + 1,
372
+ ArrowDown: index + 1,
373
+ ArrowLeft: index - 1,
374
+ ArrowUp: index - 1,
375
+ Home: 0,
376
+ End: siblings.length - 1
377
+ }[event.key];
378
+ if (target === undefined) {
379
+ return;
380
+ }
381
+ event.preventDefault();
382
+ siblings[(target + siblings.length) % siblings.length].focus();
383
+ };
384
+
308
385
  reRegisterTabs = () => {
309
386
  this._registerTabs();
310
387
  }
@@ -314,6 +391,16 @@ export default class ZnTabs extends ZincElement {
314
391
  setTimeout(() => {
315
392
  this._registerTabs();
316
393
  this._defaultTab = this._current || '';
394
+ // A linked navbar renders its items after this container can be ready
395
+ this._linkedNavbars().forEach(navbar => {
396
+ void (navbar as ZincElement).updateComplete?.then(this.reRegisterTabs);
397
+ });
398
+
399
+ const added = this.takeAddedTabs();
400
+ if (added.length > 0) {
401
+ this.setActiveTab(added[added.length - 1], true, false);
402
+ return;
403
+ }
317
404
 
318
405
  const storedValue = this.getStoredTab();
319
406
  if (storedValue !== null) {
@@ -361,7 +448,8 @@ export default class ZnTabs extends ZincElement {
361
448
  }
362
449
  }
363
450
 
364
- for (const uriTab of deepQuerySelectorAll("[tab-uri]", this, tabContainerSelector)) {
451
+ const uriTabs = [this, ...this._linkedNavbars()].flatMap(root => deepQuerySelectorAll("[tab-uri]", root, tabContainerSelector));
452
+ for (const uriTab of uriTabs) {
365
453
  const uri: string = uriTab.getAttribute("tab-uri")!;
366
454
  const eleTabId = this._uriToId(uri);
367
455
  if (eleTabId === tabId) {
@@ -412,21 +500,14 @@ export default class ZnTabs extends ZincElement {
412
500
 
413
501
  _handleClick(event: PointerEvent) {
414
502
  // ts-ignore
415
- const target = (event.relatedTarget ?? event.target) as HTMLElement;
503
+ // The listener is on the tab itself; event.target could be an icon or tooltip inside it
504
+ const target = (event.relatedTarget ?? event.currentTarget) as HTMLElement;
416
505
  if (!target) {
417
506
  return;
418
507
  }
419
508
 
420
- if (!('startViewTransition' in document)) {
421
- this.clickTab(target, event.altKey, true);
422
- return;
423
- }
424
-
425
- const transition = document.startViewTransition(() => this.clickTab(target, event.altKey, true));
426
-
427
- void transition.ready.catch(() => undefined);
428
- void transition.finished.catch(() => undefined);
429
- void transition.updateCallbackDone.catch(() => undefined);
509
+ // No view transition: a root transition snapshots the whole page, which costs Safari hundreds of ms per switch.
510
+ this.clickTab(target, event.altKey, true);
430
511
  }
431
512
 
432
513
  fetchUriTab(target: HTMLElement) {
@@ -555,15 +636,36 @@ export default class ZnTabs extends ZincElement {
555
636
  this._current = tabName;
556
637
  };
557
638
 
558
- const scrolled = sameTab ? [] : getScrolledAncestors(this._panel);
639
+ // Reading scroll positions forces a layout; the first selection has no reader's place to reset.
640
+ const firstSelection = !this._hasSelected;
641
+ const scrolled = sameTab || firstSelection ? [] : getScrolledAncestors(this._panel);
642
+ this._hasSelected = true;
559
643
 
560
644
  updateTabs();
561
645
 
562
- scrollToTop(scrolled);
646
+ // Only a pointer press on these tabs animates: keyboard, history and first-render selections stay instant, as
647
+ // does a nested tab group picking its default while an outer tab is opened.
648
+ if (!sameTab && !firstSelection && pointerInitiated(this)) {
649
+ this._panels.get(tabName)?.forEach(panel => this._enterPanel(panel));
650
+ }
651
+
652
+ revealTop(scrolled, this);
563
653
 
564
654
  return true;
565
655
  }
566
656
 
657
+ // Started here rather than as a CSS animation, which would replay in every nested tab panel whenever an ancestor
658
+ // panel is shown again. Opacity and translate only, so the compositor runs it without restyling the page.
659
+ private _enterPanel(panel: Element) {
660
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
661
+ // keep in sync with --zn-ease-out
662
+ // Offset upward: overflow past the top edge isn't scrollable, so a panel that just fits never flashes a scrollbar
663
+ panel.animate(reduceMotion ? [{opacity: 0}, {}] : [{opacity: 0, translate: '0 -4px'}, {}], {
664
+ duration: reduceMotion ? 150 : 160,
665
+ easing: 'cubic-bezier(0.23, 1, 0.32, 1)',
666
+ });
667
+ }
668
+
567
669
  getActiveTab(): Element[] {
568
670
  return this._panels.get(this._current) || [];
569
671
  }
@@ -608,26 +710,37 @@ export default class ZnTabs extends ZincElement {
608
710
  }
609
711
  }
610
712
 
713
+ /** Navbars elsewhere in this container's root that drive it through `tabs="<this id>"`. */
714
+ private _linkedNavbars(): Element[] {
715
+ if (!this.id) {
716
+ return [];
717
+ }
718
+ const root = this.getRootNode() as Document | ShadowRoot;
719
+ return Array.from(root.querySelectorAll(`zn-navbar[tabs="${CSS.escape(this.id)}"]`));
720
+ }
721
+
611
722
  _registerTabs = () => {
612
- deepQuerySelectorAll('[tab]', this, tabContainerSelector).forEach(ele => {
613
- this._addTab(ele as HTMLElement);
614
- });
723
+ for (const root of [this, ...this._linkedNavbars()]) {
724
+ deepQuerySelectorAll('[tab]', root, tabContainerSelector).forEach(ele => {
725
+ this._addTab(ele as HTMLElement);
726
+ });
615
727
 
616
- deepQuerySelectorAll('[tab-uri]', this, tabContainerSelector).forEach(ele => {
617
- if (ele.getAttribute('tab-uri') === "") {
618
- ele.setAttribute('tab', "");
619
- ele.removeAttribute('tab-uri');
620
- }
621
- this._addTab(ele as HTMLElement);
622
- });
728
+ deepQuerySelectorAll('[tab-uri]', root, tabContainerSelector).forEach(ele => {
729
+ if (ele.getAttribute('tab-uri') === "") {
730
+ ele.setAttribute('tab', "");
731
+ ele.removeAttribute('tab-uri');
732
+ }
733
+ this._addTab(ele as HTMLElement);
734
+ });
623
735
 
624
- deepQuerySelectorAll('[ref-tab-uri]', this, tabContainerSelector).forEach(ele => {
625
- if (!ele.hasAttribute('ref-tab')) {
626
- ele.setAttribute('ref-tab', this._uriToId(ele.getAttribute('ref-tab-uri')!));
627
- ele.removeAttribute('ref-tab-uri');
628
- this._actions.push(ele as HTMLElement);
629
- }
630
- });
736
+ deepQuerySelectorAll('[ref-tab-uri]', root, tabContainerSelector).forEach(ele => {
737
+ if (!ele.hasAttribute('ref-tab')) {
738
+ ele.setAttribute('ref-tab', this._uriToId(ele.getAttribute('ref-tab-uri')!));
739
+ ele.removeAttribute('ref-tab-uri');
740
+ this._actions.push(ele as HTMLElement);
741
+ }
742
+ });
743
+ }
631
744
  }
632
745
 
633
746
  render() {