@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
@@ -1,7 +1,10 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import { expect, fixture, html, waitUntil } from '@open-wc/testing';
3
3
  import type ZnSplitPane from './split-pane.component';
4
4
 
5
+ // Narrow-parent merging is applied from the ResizeObserver, which delivers after layout and before paint.
6
+ const afterLayout = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
7
+
5
8
  describe('<zn-split-pane>', () => {
6
9
  it('should render a component', async () => {
7
10
  const el = await fixture(html` <zn-split-pane></zn-split-pane> `);
@@ -71,6 +74,7 @@ describe('<zn-split-pane>', () => {
71
74
  </zn-split-pane>
72
75
  `);
73
76
  await el.updateComplete;
77
+ await afterLayout();
74
78
  const nested = el.querySelector<ZnSplitPane>('zn-split-pane')!;
75
79
  await nested.updateComplete;
76
80
  const navItems = Array.from(el.shadowRoot!.querySelectorAll('#split-nav li'));
@@ -99,6 +103,7 @@ describe('<zn-split-pane>', () => {
99
103
  </zn-split-pane>
100
104
  `);
101
105
  await el.updateComplete;
106
+ await afterLayout();
102
107
  const nested = el.querySelector<ZnSplitPane>('zn-split-pane')!;
103
108
  await nested.updateComplete;
104
109
 
@@ -140,6 +145,7 @@ describe('<zn-split-pane>', () => {
140
145
  </zn-split-pane>
141
146
  `);
142
147
  await el.updateComplete;
148
+ await afterLayout();
143
149
  const nested = el.querySelector<ZnSplitPane>('zn-split-pane')!;
144
150
  const deepNested = nested.querySelector<ZnSplitPane>('zn-split-pane')!;
145
151
  await nested.updateComplete;
@@ -174,7 +180,7 @@ describe('<zn-split-pane>', () => {
174
180
  window.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}));
175
181
 
176
182
  expect((el.shadowRoot?.querySelector('style')?.textContent ?? '')).to.contain('--min-secondary-panel-size: 25%');
177
- expect((el.shadowRoot?.querySelector('style')?.textContent ?? '')).to.contain('--initial-size: 75%');
183
+ expect(el.shadowRoot!.querySelector<HTMLElement>('#primary-pane')!.style.getPropertyValue('--zn-split-pane-size')).to.equal('75%');
178
184
  });
179
185
 
180
186
  it('should keep the secondary pane above the configured pixel minimum', async () => {
@@ -192,6 +198,20 @@ describe('<zn-split-pane>', () => {
192
198
  window.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}));
193
199
 
194
200
  expect((el.shadowRoot?.querySelector('style')?.textContent ?? '')).to.contain('--min-secondary-panel-size: 125px');
195
- expect((el.shadowRoot?.querySelector('style')?.textContent ?? '')).to.contain('--initial-size: 275px');
201
+ expect(el.shadowRoot!.querySelector<HTMLElement>('#primary-pane')!.style.getPropertyValue('--zn-split-pane-size')).to.equal('275px');
202
+ });
203
+
204
+ it('keeps a preferred secondary pane at its pixel size when the container resizes', async () => {
205
+ const el = await fixture<ZnSplitPane>(html`
206
+ <zn-split-pane vertical secondary pixels min-size="50" initial-size="100" style="display: flex; height: 400px;">
207
+ <div slot="primary">Primary content</div>
208
+ <div slot="secondary">Secondary content</div>
209
+ </zn-split-pane>
210
+ `);
211
+ const secondaryHeight = () => el.shadowRoot!.querySelector('#secondary-pane')!.getBoundingClientRect().height;
212
+ await waitUntil(() => Math.abs(secondaryHeight() - 100) < 2);
213
+
214
+ el.style.height = '600px';
215
+ await waitUntil(() => Math.abs(secondaryHeight() - 100) < 2, 'secondary pane stays at 100px');
196
216
  });
197
217
  });
@@ -1,7 +1,8 @@
1
1
  import {classMap} from "lit/directives/class-map.js";
2
- import {type CSSResultGroup, html, unsafeCSS} from 'lit';
2
+ import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
3
3
  import {ifDefined} from "lit/directives/if-defined.js";
4
- import {property} from 'lit/decorators.js';
4
+ import {keyed} from "lit/directives/keyed.js";
5
+ import {property, state} from 'lit/decorators.js';
5
6
  import ZincElement from '../../internal/zinc-element';
6
7
  import type {IconColor} from '../icon';
7
8
 
@@ -28,8 +29,27 @@ export default class ZnStatsTile extends ZincElement {
28
29
  @property() color: 'primary' | 'error' | 'info' | 'warning' | 'success' | 'neutral' = 'primary';
29
30
 
30
31
  /** `value-first` puts the amount under the caption with the description beneath it, and tints the amount rather
31
- * than the description with `color`. */
32
- @property({reflect: true}) layout: 'default' | 'value-first' = 'default';
32
+ * than the description with `color`. `tile` is a self-contained card: caption, amount, a footer with the delta and
33
+ * `description`, an optional sparkline, and the default slot beneath. */
34
+ @property({reflect: true}) layout: 'default' | 'value-first' | 'tile' = 'default';
35
+
36
+ /** Comma-separated values drawn as a sparkline under a `tile`. */
37
+ @property() spark = '';
38
+
39
+ /** The sparkline's colour: a tone (`primary`, `info`, `success`, `warning`, `danger`, `ai`) or any CSS colour. */
40
+ @property({attribute: 'spark-color'}) sparkColor = '';
41
+
42
+ /** Treats a rise as bad news, so an increase is coloured as a loss (declines, refunds, failures). */
43
+ @property({type: Boolean, attribute: 'invert-delta'}) invertDelta = false;
44
+
45
+ /** A coloured dot before a `tile`'s caption. */
46
+ @property({reflect: true}) tone: '' | 'info' | 'success' | 'warning' | 'danger' | 'neutral' = '';
47
+
48
+ /** Renders a `tile` as a toggle button, for tiles that filter the content below them. */
49
+ @property({type: Boolean}) selectable = false;
50
+
51
+ /** Whether a `selectable` tile is pressed, or a linked tile marks the view the page is showing. */
52
+ @property({type: Boolean, reflect: true}) selected = false;
33
53
 
34
54
  /** How `show-delta` compares with `previous`: the relative change, or the difference in points. */
35
55
  @property({attribute: 'delta-format'}) deltaFormat: 'percent' | 'points' = 'percent';
@@ -46,9 +66,62 @@ export default class ZnStatsTile extends ZincElement {
46
66
  /** Pads the stat, for stats laid edge to edge in a flush container such as `zn-cols no-gap divide`. */
47
67
  @property({type: Boolean}) padded = false;
48
68
 
69
+ /** Makes the whole stat a link, marked with an arrow in its corner. */
70
+ @property() href: string;
71
+
72
+ @property({attribute: 'data-target'}) dataTarget: string;
73
+
74
+ @property() gaid: string;
75
+
76
+ /** JSON fetched once to fill the stat: any of `caption`, `amount`, `description`, `previous`, `spark` and
77
+ * `sparkColor`. The stat's own attributes show until it loads. */
78
+ @property({attribute: 'data-url'}) dataUrl = '';
79
+
80
+ /** Shows a `tile` as a placeholder. Set while `data-url` loads; the sparkline draws in once it clears. */
81
+ @property({type: Boolean, reflect: true}) loading = false;
82
+
83
+ @state() private loaded = false;
84
+
85
+ private static readonly loadableKeys = ['caption', 'amount', 'description', 'previous', 'spark', 'sparkColor'] as const;
86
+
87
+ protected willUpdate(changed: PropertyValues) {
88
+ super.willUpdate(changed);
89
+ if (changed.has('dataUrl') && this.dataUrl) this.loading = true;
90
+ }
91
+
92
+ protected updated(changed: PropertyValues) {
93
+ super.updated(changed);
94
+ if (changed.has('dataUrl') && this.dataUrl) void this.loadData(this.dataUrl);
95
+ }
96
+
97
+ private async loadData(url: string) {
98
+ try {
99
+ const res = await fetch(url, {
100
+ credentials: 'same-origin',
101
+ headers: {Accept: 'application/json', 'x-kx-fetch-style': 'raw-json'},
102
+ });
103
+ if (url !== this.dataUrl) return;
104
+ if (res.ok) {
105
+ const body = await res.json() as Partial<Record<typeof ZnStatsTile.loadableKeys[number], unknown>>;
106
+ for (const key of ZnStatsTile.loadableKeys) {
107
+ if (body[key] !== undefined && body[key] !== null) this[key] = String(body[key]);
108
+ }
109
+ }
110
+ } catch { /* fall back to the stat's own attributes */ }
111
+ if (url === this.dataUrl) {
112
+ this.loaded = true;
113
+ this.loading = false;
114
+ }
115
+ }
116
+
117
+ // Amounts are display strings, so drop separators and symbols ("48,201", "$2,941") before comparing.
118
+ private parseNumber(value: string) {
119
+ return parseFloat(String(value).replace(/[^0-9.-]/g, ''));
120
+ }
121
+
49
122
  calcPercentageDifference() {
50
- const previous = parseFloat(this.previous);
51
- const amount = parseFloat(this.amount);
123
+ const previous = this.parseNumber(this.previous);
124
+ const amount = this.parseNumber(this.amount);
52
125
 
53
126
  let diff = ((amount - previous) / previous) * 100;
54
127
  diff = Math.round(diff * 10) / 10;
@@ -105,8 +178,8 @@ export default class ZnStatsTile extends ZincElement {
105
178
  diffIcon = '-';
106
179
  }
107
180
 
108
- if (this.deltaFormat === 'points') {
109
- return html`<p class="diff">${this.pointsDelta()} on Previous Period</p>`;
181
+ if (this.deltaFormat === 'points' || this.fromZero()) {
182
+ return html`<p class="diff">${this.pointsDelta(this.deltaFormat === 'points' ? ' pts' : '')} on Previous Period</p>`;
110
183
  }
111
184
 
112
185
  return html`<p class="diff"><span class="${diffClass}">${diffIcon}${diff}%</span> on Previous Period</p>`;
@@ -115,13 +188,18 @@ export default class ZnStatsTile extends ZincElement {
115
188
  return null;
116
189
  }
117
190
 
118
- private pointsDelta() {
119
- const diff = Math.round((parseFloat(this.amount) - parseFloat(this.previous)) * 10) / 10;
191
+ // A change from zero has no percentage, so it shows as the plain difference.
192
+ private fromZero() {
193
+ return this.parseNumber(this.previous) === 0;
194
+ }
195
+
196
+ private pointsDelta(unit = ' pts') {
197
+ const diff = Math.round((this.parseNumber(this.amount) - this.parseNumber(this.previous)) * 10) / 10;
120
198
  if (diff > 0) {
121
- return html`<span class="positive">+${diff} pts</span>`;
199
+ return html`<span class="positive">+${diff}${unit}</span>`;
122
200
  }
123
201
  if (diff < 0) {
124
- return html`<span class="negative">-${Math.abs(diff)} pts</span>`;
202
+ return html`<span class="negative">-${Math.abs(diff)}${unit}</span>`;
125
203
  }
126
204
  return html`<span class="neutral">No change</span>`;
127
205
  }
@@ -152,6 +230,9 @@ export default class ZnStatsTile extends ZincElement {
152
230
  }
153
231
 
154
232
  private percentDelta() {
233
+ if (this.fromZero()) {
234
+ return this.pointsDelta('');
235
+ }
155
236
  const diff = this.calcPercentageDifference();
156
237
  if (diff > 0) {
157
238
  return html`<span class="positive">+${diff}%</span>`;
@@ -162,11 +243,125 @@ export default class ZnStatsTile extends ZincElement {
162
243
  return html`<span class="neutral">No change</span>`;
163
244
  }
164
245
 
246
+ private tileDelta() {
247
+ if (!this.showDelta) {
248
+ return null;
249
+ }
250
+
251
+ const points = this.deltaFormat === 'points';
252
+ const absolute = points || this.fromZero();
253
+ const diff = absolute
254
+ ? Math.round((this.parseNumber(this.amount) - this.parseNumber(this.previous)) * 10) / 10
255
+ : this.calcPercentageDifference();
256
+ const direction = diff > 0 ? 'up' : diff < 0 ? 'down' : 'flat';
257
+ const outcome = direction === 'flat' ? 'flat' : (direction === 'up') !== this.invertDelta ? 'good' : 'bad';
258
+ const text = direction === 'flat' && absolute ? 'No change'
259
+ : points ? `${Math.abs(diff)} pts` : absolute ? `${Math.abs(diff)}` : `${Math.abs(diff)}%`;
260
+
261
+ return html`
262
+ <span class="delta delta--${outcome}">
263
+ ${direction === 'flat' ? null : html`
264
+ <zn-icon src="${direction === 'up' ? 'arrow-up-right@lu' : 'arrow-down-right@lu'}" size="14"></zn-icon>`}
265
+ ${text}
266
+ </span>`;
267
+ }
268
+
269
+ private sparkStroke() {
270
+ const tones: Record<string, string> = {
271
+ primary: 'var(--zn-chart-color-1, rgb(var(--zn-primary)))',
272
+ info: 'var(--zn-chart-color-1, rgb(var(--zn-color-info)))',
273
+ success: 'var(--zn-chart-color-2, rgb(var(--zn-color-success)))',
274
+ ai: 'var(--zn-chart-color-3, rgb(var(--zn-color-ai)))',
275
+ warning: 'var(--zn-chart-color-4, rgb(var(--zn-color-warning)))',
276
+ danger: 'var(--zn-chart-color-5, rgb(var(--zn-color-error)))',
277
+ error: 'var(--zn-chart-color-5, rgb(var(--zn-color-error)))',
278
+ };
279
+ return tones[this.sparkColor] ?? (this.sparkColor || tones.primary);
280
+ }
281
+
282
+ private renderSpark() {
283
+ if (this.loading) {
284
+ return html`<div class="spark spark--loading" part="spark">
285
+ <span class="spark__track" style="--stat-spark: ${this.sparkStroke()}"></span>
286
+ </div>`;
287
+ }
288
+
289
+ const data = this.spark.split(',').map(v => parseFloat(v)).filter(v => !Number.isNaN(v));
290
+ if (data.length < 2) {
291
+ return null;
292
+ }
293
+
294
+ const width = 100;
295
+ const height = 28;
296
+ const max = Math.max(...data);
297
+ const min = Math.min(...data);
298
+ const range = max - min || 1;
299
+ const line = data
300
+ .map((v, i) => `${i ? 'L' : 'M'}${((i / (data.length - 1)) * width).toFixed(1)} ${(height - 3 - ((v - min) / range) * (height - 6)).toFixed(1)}`)
301
+ .join(' ');
302
+ const color = this.sparkStroke();
303
+
304
+ return html`
305
+ <div class="spark" part="spark">
306
+ <svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" aria-hidden="true">
307
+ <defs>
308
+ <linearGradient id="spark-fill" x1="0" x2="0" y1="0" y2="1">
309
+ <stop offset="0" style="stop-color: ${color}; stop-opacity: .22"></stop>
310
+ <stop offset="1" style="stop-color: ${color}; stop-opacity: 0"></stop>
311
+ </linearGradient>
312
+ </defs>
313
+ <path d="${line} L${width} ${height} L0 ${height} Z" fill="url(#spark-fill)"></path>
314
+ <path d="${line}" fill="none" style="stroke: ${color}" stroke-width="1.75"
315
+ vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round"></path>
316
+ </svg>
317
+ </div>`;
318
+ }
319
+
320
+ private renderTile() {
321
+ const delta = this.loading ? null : this.tileDelta();
322
+ const description = this.description
323
+ || (this.loading ? html`<span class="loading-text">Loading</span>` : '');
324
+ const classes = classMap({
325
+ 'stat-tile': true,
326
+ 'stat-tile--card': true,
327
+ 'stat-tile--selectable': this.selectable,
328
+ 'stat-tile--loaded': this.loaded,
329
+ [`stat-tile--tone-${this.tone}`]: !!this.tone,
330
+ });
331
+ // Keyed so the loaded content is new nodes, which replays its entrance animations
332
+ const body = keyed(this.loading, html`
333
+ <span class="caption" part="caption">${this.caption}</span>
334
+ <span class="amount" part="amount">${this.loading
335
+ ? html`<span class="amount__skeleton"></span>`
336
+ : this.renderAmount()}</span>
337
+ ${delta || description ? html`
338
+ <span class="foot" part="foot">${delta}${description ? html`<span>${description}</span>` : null}</span>` : null}
339
+ ${this.renderSpark()}
340
+ <slot></slot>`);
341
+
342
+ return this.selectable
343
+ ? html`<button type="button" part="base" class="${classes}" aria-pressed="${this.selected ? 'true' : 'false'}"
344
+ @click="${() => { this.selected = !this.selected; }}">${body}</button>`
345
+ : html`<div part="base" class="${classes}">${body}</div>`;
346
+ }
347
+
165
348
  render() {
166
- if (this.layout === 'value-first') {
167
- return this.renderValueFirst();
349
+ const tile = this.layout === 'tile'
350
+ ? this.renderTile()
351
+ : this.layout === 'value-first' ? this.renderValueFirst() : this.renderDefault();
352
+ if (!this.href) {
353
+ return tile;
168
354
  }
169
355
 
356
+ return html`
357
+ <a class="stat-link" part="link" href="${this.href}" data-target="${ifDefined(this.dataTarget)}"
358
+ gaid="${ifDefined(this.gaid)}">
359
+ ${tile}
360
+ <zn-icon class="stat-link__icon" src="arrow-up-right@lu" size="${this.layout === 'tile' ? 18 : 16}" aria-hidden="true"></zn-icon>
361
+ </a>`;
362
+ }
363
+
364
+ private renderDefault() {
170
365
  return html`
171
366
  <div class="${classMap({
172
367
  'stat-tile': true,
@@ -6,10 +6,307 @@
6
6
  }
7
7
 
8
8
  // A box of its own lets zn-cols draw its dividers between value-first stats.
9
- :host([layout="value-first"]) {
9
+ :host([layout="value-first"]),
10
+ :host([layout="tile"]) {
10
11
  display: block;
11
12
  }
12
13
 
14
+ // Doubled class out-ranks the default layout's `.stat-tile .x` rules, which the card also matches.
15
+ .stat-tile.stat-tile--card {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: 5px;
19
+ box-sizing: border-box;
20
+ width: 100%;
21
+ height: 100%;
22
+ margin: 0;
23
+ padding: var(--zn-density-card-padding, var(--zn-spacing-medium));
24
+ background: rgba(var(--zn-panel), var(--zn-panel-opacity, 1));
25
+ border: 1px solid rgb(var(--zn-border-color));
26
+ border-radius: var(--zn-panel-border-radius, var(--zn-border-radius));
27
+ box-shadow: var(--zn-panel-shadow, none);
28
+ font: inherit;
29
+ color: inherit;
30
+ text-align: left;
31
+
32
+ .caption {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 6px;
36
+ margin: 0;
37
+ font-family: var(--zn-text-chart-minor-font-family, inherit);
38
+ font-size: var(--zn-text-chart-minor-font-size);
39
+ line-height: 1.2;
40
+ font-weight: var(--zn-text-chart-minor-font-weight);
41
+ color: var(--zn-text-chart-minor-color);
42
+ }
43
+
44
+ .amount {
45
+ display: block;
46
+ flex-grow: 0;
47
+ padding: 0;
48
+ font-family: var(--zn-text-chart-major-font-family, inherit);
49
+ font-size: var(--zn-text-chart-major-font-size);
50
+ font-weight: var(--zn-text-chart-major-font-weight);
51
+ line-height: 1.1;
52
+ letter-spacing: var(--zn-text-chart-major-letter-spacing, normal);
53
+ font-variant-numeric: tabular-nums;
54
+ color: var(--zn-text-chart-major-color);
55
+ }
56
+
57
+ .foot {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 8px;
61
+ font-size: var(--zn-font-size-x-small);
62
+ color: var(--zn-text-color-muted);
63
+ }
64
+
65
+ .spark {
66
+ height: 28px;
67
+ margin-top: 2px;
68
+
69
+ svg {
70
+ display: block;
71
+ width: 100%;
72
+ height: 100%;
73
+ overflow: visible;
74
+ }
75
+ }
76
+
77
+ // Holds the line box of a loaded amount, so the tile doesn't jump when the value arrives
78
+ .amount__skeleton {
79
+ position: relative;
80
+ display: block;
81
+ width: 2.4em;
82
+ height: .85em;
83
+ margin: .125em 0;
84
+ }
85
+
86
+ .spark--loading {
87
+ display: flex;
88
+ align-items: flex-end;
89
+ }
90
+
91
+ .spark__track {
92
+ position: relative;
93
+ overflow: hidden;
94
+ width: 100%;
95
+ height: 3px;
96
+ border-radius: 2px;
97
+ background: var(--zn-skeleton-background, rgb(var(--zn-border-color)));
98
+ }
99
+
100
+ @media (prefers-reduced-motion: no-preference) {
101
+ // A soft glow with no edges, so the placeholder never reads as a box
102
+ .amount__skeleton::after {
103
+ content: "";
104
+ position: absolute;
105
+ inset: 0;
106
+ width: 60%;
107
+ background: radial-gradient(ellipse at center, rgb(var(--zn-border-color)) 0%, transparent 70%);
108
+ animation: skeleton-sweep 1.4s ease-in-out infinite;
109
+ }
110
+
111
+ .spark__track::after {
112
+ content: "";
113
+ position: absolute;
114
+ inset: 0;
115
+ width: 40%;
116
+ border-radius: inherit;
117
+ background: linear-gradient(90deg, transparent, var(--stat-spark), transparent);
118
+ animation: spark-sweep 1.4s ease-in-out infinite;
119
+ }
120
+
121
+ .loading-text::after {
122
+ content: "";
123
+ display: inline-block;
124
+ width: 1.2em;
125
+ text-align: left;
126
+ animation: loading-dots 1.2s steps(1) infinite;
127
+ }
128
+
129
+ &.stat-tile--loaded {
130
+ .amount {
131
+ animation: stat-fade .4s ease-out both;
132
+ }
133
+
134
+ .foot {
135
+ animation: stat-rise .4s .1s var(--zn-ease-out, ease-out) both;
136
+ }
137
+
138
+ .spark svg {
139
+ animation: spark-draw .8s .15s var(--zn-ease-out, ease-out) both;
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ @keyframes spark-draw {
146
+ from { clip-path: inset(-2px 100% -2px -2px); }
147
+ to { clip-path: inset(-2px); }
148
+ }
149
+
150
+ // Fades at both ends instead of being clipped, so the glow has no edge to cut off
151
+ @keyframes skeleton-sweep {
152
+ from { transform: translateX(-40%); opacity: 0; }
153
+ 25%, 75% { opacity: 1; }
154
+ to { transform: translateX(140%); opacity: 0; }
155
+ }
156
+
157
+ @keyframes spark-sweep {
158
+ from { transform: translateX(-100%); }
159
+ to { transform: translateX(250%); }
160
+ }
161
+
162
+ @keyframes stat-fade {
163
+ from { opacity: 0; }
164
+ }
165
+
166
+ @keyframes stat-rise {
167
+ from { opacity: 0; transform: translateY(6px); }
168
+ }
169
+
170
+ @keyframes loading-dots {
171
+ 0% { content: ""; }
172
+ 25% { content: "."; }
173
+ 50% { content: ".."; }
174
+ 75% { content: "..."; }
175
+ }
176
+
177
+ .stat-tile--tone-info { --stat-tone: var(--zn-chart-color-1, rgb(var(--zn-color-info))); }
178
+ .stat-tile--tone-success { --stat-tone: var(--zn-chart-color-2, rgb(var(--zn-color-success))); }
179
+ .stat-tile--tone-warning { --stat-tone: var(--zn-chart-color-4, rgb(var(--zn-color-warning))); }
180
+ .stat-tile--tone-danger { --stat-tone: var(--zn-chart-color-5, rgb(var(--zn-color-error))); }
181
+
182
+ [class*="stat-tile--tone-"] .caption::before {
183
+ content: "";
184
+ flex: none;
185
+ width: 8px;
186
+ height: 8px;
187
+ border-radius: 50%;
188
+ background: var(--stat-tone, var(--zn-color-neutral-400));
189
+ }
190
+
191
+ @mixin selected-tile {
192
+ border-color: rgb(var(--zn-primary));
193
+ background: var(--zn-color-primary-50);
194
+ // Inset, so a container clipping at the tile's edge can't cut the ring off
195
+ box-shadow: inset 0 0 0 1px rgb(var(--zn-primary));
196
+ }
197
+
198
+ .stat-tile--selectable {
199
+ cursor: pointer;
200
+
201
+ &:hover {
202
+ border-color: var(--zn-color-primary-300);
203
+ }
204
+
205
+ &:focus-visible {
206
+ outline: none;
207
+ box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
208
+ }
209
+
210
+ &[aria-pressed="true"] {
211
+ @include selected-tile;
212
+ }
213
+ }
214
+
215
+ // A linked tile marks the view it is showing, such as the filter a page is on.
216
+ :host([selected][href]) .stat-tile--card {
217
+ @include selected-tile;
218
+ }
219
+
220
+ .delta {
221
+ display: inline-flex;
222
+ align-items: center;
223
+ gap: 2px;
224
+ font-weight: var(--zn-font-weight-semibold);
225
+ font-variant-numeric: tabular-nums;
226
+
227
+ &--good { color: var(--zn-color-success-fg); }
228
+ &--bad { color: var(--zn-color-danger-fg); }
229
+ &--flat { color: var(--zn-text-color-muted); }
230
+ }
231
+
232
+ .stat-link {
233
+ position: relative;
234
+ display: block;
235
+ color: inherit;
236
+ text-decoration: none;
237
+
238
+ &:hover {
239
+ background-color: var(--zn-stat-hover-background, var(--zn-tile-hover-background, var(--zn-color-neutral-100)));
240
+ }
241
+
242
+ &:focus-visible {
243
+ outline: 2px solid rgb(var(--zn-primary));
244
+ outline-offset: -2px;
245
+ }
246
+ }
247
+
248
+ // A linked tile is a card in its own right, so it lifts rather than tinting the row.
249
+ :host([layout="tile"]) .stat-link {
250
+ height: 100%;
251
+
252
+ &:hover {
253
+ background-color: transparent;
254
+ }
255
+
256
+ // Gated so a tap doesn't leave the lift stuck on touch screens.
257
+ @media (hover: hover) and (pointer: fine) {
258
+ &:hover .stat-tile--card {
259
+ border-color: var(--zn-color-primary-400);
260
+ box-shadow: var(--zn-shadow-medium);
261
+ }
262
+ }
263
+
264
+ &:focus-visible {
265
+ outline: none;
266
+
267
+ .stat-tile--card {
268
+ border-color: rgb(var(--zn-primary));
269
+ box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
270
+ }
271
+ }
272
+
273
+ .stat-tile--card {
274
+ transition: border-color var(--zn-transition-fast), box-shadow var(--zn-transition-fast);
275
+ }
276
+ }
277
+
278
+ @media (hover: hover) and (pointer: fine) {
279
+ :host([layout="tile"][selected]) .stat-link:hover .stat-tile--card {
280
+ border-color: rgb(var(--zn-primary));
281
+ box-shadow: inset 0 0 0 1px rgb(var(--zn-primary)), var(--zn-shadow-medium);
282
+ }
283
+ }
284
+
285
+ .stat-link__icon {
286
+ position: absolute;
287
+ top: var(--zn-spacing-medium);
288
+ right: var(--zn-spacing-medium);
289
+ color: var(--zn-color-primary-fg, rgb(var(--zn-primary)));
290
+ }
291
+
292
+ :host([layout="tile"]) .stat-link__icon {
293
+ top: var(--zn-density-card-padding, var(--zn-spacing-medium));
294
+ right: var(--zn-density-card-padding, var(--zn-spacing-medium));
295
+ transition: transform var(--zn-transition-fast) var(--zn-ease-out);
296
+ }
297
+
298
+ @media (hover: hover) and (pointer: fine) {
299
+ :host([layout="tile"]) .stat-link:hover .stat-link__icon {
300
+ transform: translate(1px, -1px);
301
+ }
302
+ }
303
+
304
+ @media (prefers-reduced-motion: reduce) {
305
+ :host([layout="tile"]) .stat-link__icon {
306
+ transition: none;
307
+ }
308
+ }
309
+
13
310
  .stat-tile {
14
311
 
15
312
  .caption, .description, .amount, .bottom {
@@ -40,9 +337,12 @@
40
337
 
41
338
  .amount {
42
339
  color: var(--zn-text-chart-major-color);
340
+ font-family: var(--zn-text-chart-major-font-family, inherit);
43
341
  font-weight: var(--zn-text-chart-major-font-weight);
44
342
  font-size: var(--zn-text-chart-major-font-size);
45
343
  line-height: var(--zn-text-chart-major-line-height);
344
+ letter-spacing: var(--zn-text-chart-major-letter-spacing, normal);
345
+ font-variant-numeric: var(--zn-stat-numeric, normal);
46
346
  display: flex;
47
347
  flex-grow: 1;
48
348
  align-items: center;
@@ -61,15 +361,15 @@
61
361
 
62
362
  .diff {
63
363
  .positive {
64
- color: rgb(var(--zn-color-success));
364
+ color: var(--zn-color-success-fg);
65
365
  }
66
366
 
67
367
  .negative {
68
- color: rgb(var(--zn-color-error));
368
+ color: var(--zn-color-danger-fg);
69
369
  }
70
370
 
71
371
  .neutral {
72
- color: rgb(var(--zn-color-warning));
372
+ color: var(--zn-color-warning-fg);
73
373
  }
74
374
  }
75
375
 
@@ -145,7 +445,7 @@
145
445
  }
146
446
 
147
447
  &--primary {
148
- color: rgb(var(--zn-primary));
448
+ color: var(--zn-color-primary-fg, rgb(var(--zn-primary)));
149
449
  }
150
450
 
151
451
  &--error {