@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
@@ -3,7 +3,6 @@
3
3
  :host {
4
4
  display: block;
5
5
  line-height: var(--zn-line-height-dense);
6
- max-width: var(--zn-container-lg);
7
6
  --zn-col-gap: calc(var(--zn-spacing-medium, 20px) * 3);
8
7
  --zn-form-group-sticky-top: var(--zn-spacing-medium, 20px);
9
8
  }
@@ -13,6 +12,7 @@
13
12
  border: none;
14
13
  padding: 0;
15
14
  margin: 0;
15
+ max-width: var(--zn-container-lg);
16
16
  }
17
17
 
18
18
  .form-control__label {
@@ -20,6 +20,7 @@
20
20
  }
21
21
 
22
22
  .form-control--has-label .form-control__label {
23
+ font-family: var(--zn-form-group-label-font-family, inherit);
23
24
  font-weight: var(--zn-font-weight-bold);
24
25
  font-size: var(--zn-font-size-large);
25
26
  }
@@ -32,7 +33,7 @@
32
33
  // label would scroll over the fields it labels. zn-cols wraps below the two columns' bases plus
33
34
  // the gap, so 670px = --zn-col-basis * (1 + 2) + 70px — keep in step with both values below.
34
35
  @container (min-width: 670px) {
35
- .form-control__text {
36
+ .form-control:not(.form-control--stacked) .form-control__text {
36
37
  position: sticky;
37
38
  top: var(--zn-form-group-sticky-top);
38
39
  // A stretched flex item is as tall as the row, leaving sticky nothing to travel through.
@@ -59,7 +60,7 @@
59
60
  .form-control-input {
60
61
  display: grid;
61
62
  grid-template-columns: minmax(0, 1fr);
62
- gap: var(--zn-spacing-medium) var(--zn-spacing-small);
63
+ gap: var(--zn-form-row-gap, var(--zn-spacing-medium)) var(--zn-spacing-small);
63
64
 
64
65
  // `1fr` floors each track at its min-content width, so one long label steals width from
65
66
  // the other tracks and the spans stop lining up.
@@ -110,8 +111,9 @@ zn-cols::part(base) {
110
111
  gap: var(--zn-gap);
111
112
  }
112
113
 
114
+ // layout="1" stacks the label above the fields at every width, so there is no gutter to keep.
113
115
  @container (min-width: 670px) {
114
- zn-cols::part(base) {
116
+ .form-control:not(.form-control--stacked) zn-cols::part(base) {
115
117
  gap: 70px;
116
118
  }
117
119
  }
@@ -121,3 +123,34 @@ zn-cols::part(base) {
121
123
  padding-top: var(--zn-base-gap);
122
124
  padding-left: var(--zn-base-gap);
123
125
  }
126
+
127
+ // Without a label or help text the group is only its card's field grid, so it fills the card.
128
+ // A compact stacked group labels its fields the way an input does, so it fills the same width.
129
+ .form-control--full,
130
+ .form-control--compact.form-control--stacked {
131
+ max-width: none;
132
+ }
133
+
134
+ .form-control--compact.form-control--has-label .form-control__label {
135
+ display: block;
136
+ margin-bottom: 0;
137
+ font-family: inherit;
138
+ font-weight: var(--zn-input-label-font-weight);
139
+ font-size: var(--zn-input-label-font-size-medium);
140
+ line-height: var(--zn-input-label-line-height);
141
+ }
142
+
143
+ .form-control--compact .form-control__help-text {
144
+ margin-top: var(--zn-input-label-spacing, var(--zn-spacing-2x-small));
145
+ font-size: var(--zn-input-help-text-font-size-medium);
146
+ line-height: var(--zn-input-help-text-line-height, 1.5);
147
+ }
148
+
149
+ .form-control--compact zn-cols::part(base) {
150
+ gap: var(--zn-form-row-gap, var(--zn-spacing-medium));
151
+ }
152
+
153
+ // Stacked, the label sits over its fields as an input's does
154
+ .form-control--compact.form-control--stacked zn-cols::part(base) {
155
+ gap: var(--zn-input-label-spacing, var(--zn-spacing-2x-small));
156
+ }
@@ -142,4 +142,14 @@ describe('<zn-form-group>', () => {
142
142
  const narrowCols = narrow.querySelector('zn-form-group')!.shadowRoot!.querySelector('zn-cols')!;
143
143
  expect(getComputedStyle(narrowCols.shadowRoot!.querySelector('.cols')!).rowGap).to.not.equal('70px');
144
144
  });
145
+
146
+ it('fills its container without a label, and stays capped with one', async () => {
147
+ const plain = await fixture<HTMLElement>(html`<zn-form-group><input></zn-form-group>`);
148
+ const labelled = await fixture<HTMLElement>(html`<zn-form-group label="Identity" style="--zn-container-lg: 1100px"><input></zn-form-group>`);
149
+
150
+ const fieldset = (el: HTMLElement) => el.shadowRoot!.querySelector('fieldset')!;
151
+
152
+ expect(getComputedStyle(fieldset(plain)).maxWidth).to.equal('none');
153
+ expect(getComputedStyle(fieldset(labelled)).maxWidth).to.equal("1100px");
154
+ });
145
155
  });
@@ -40,6 +40,11 @@ export default class ZnHeader extends ZincElement {
40
40
  @property({type: Boolean}) transparent: boolean = false;
41
41
 
42
42
  @property() caption: string;
43
+ /** A line of muted text under the caption. */
44
+ @property() description: string;
45
+
46
+ /** Sets the description beside the caption rather than under it, e.g. a row count. */
47
+ @property({attribute: 'inline-description', type: Boolean}) inlineDescription = false;
43
48
  @property() icon: string;
44
49
 
45
50
  @property({type: Array}) navigation = [];
@@ -114,6 +119,7 @@ export default class ZnHeader extends ZincElement {
114
119
  <div class="${classMap({
115
120
  'header': true,
116
121
  'header--transparent': this.transparent,
122
+ 'header--inline-description': this.inlineDescription,
117
123
  'header--full-width': this.fullWidth,
118
124
  'header--has-nav': hasNavigation,
119
125
  'header--has-breadcrumb': hasBreadcrumb,
@@ -139,10 +145,9 @@ export default class ZnHeader extends ZincElement {
139
145
 
140
146
  <div class="content" part="content">
141
147
  ${hasPreviousPath ? html`
142
- <a href="${this.previousPath}" class="caption__back"
143
- data-target="${this.previousTarget ? this.previousTarget : ''}">
144
- <zn-button icon="arrow-left@lu" icon-size="20" icon-button="small" plain></zn-button>
145
- </a>` : null}
148
+ <zn-button href="${this.previousPath}" class="caption__back" label="Back"
149
+ data-target="${this.previousTarget ? this.previousTarget : ''}"
150
+ icon="arrow-left@lu" icon-size="20" icon-button="small" plain></zn-button>` : null}
146
151
 
147
152
  ${hasDefaultSlot ? html`
148
153
  <div class="actions">
@@ -158,6 +163,8 @@ export default class ZnHeader extends ZincElement {
158
163
  <slot name="breadcrumb" class="breadcrumb"></slot>` : null}
159
164
  <slot name="caption">${this.caption}</slot>
160
165
  </span>
166
+ ${this.description ? html`
167
+ <span class="header__description" part="description">${this.description}</span>` : null}
161
168
  </div>
162
169
 
163
170
  <div part="header-right" class="header__right">
@@ -15,6 +15,7 @@
15
15
  }
16
16
 
17
17
  &__caption {
18
+ font-family: var(--zn-text-h1-font-family, inherit);
18
19
  font-weight: var(--zn-text-h1-font-weight);
19
20
  font-size: var(--zn-text-h1-font-size);
20
21
  text-transform: var(--zn-text-h1-transform, none);
@@ -84,7 +85,7 @@
84
85
  }
85
86
 
86
87
  zn-icon:hover {
87
- color: rgb(var(--zn-primary));
88
+ color: var(--zn-color-primary-fg, rgb(var(--zn-primary)));
88
89
  cursor: pointer;
89
90
  }
90
91
  }
@@ -131,8 +132,7 @@
131
132
  flex-grow: 1;
132
133
  flex-shrink: 1;
133
134
  // Chrome ignores overflow-clip-margin unless both axes clip
134
- overflow: clip;
135
- overflow-clip-margin: var(--zn-focus-ring-outset);
135
+ @include wc.clip-focus-room;
136
136
  min-width: 20px;
137
137
  }
138
138
 
@@ -159,7 +159,7 @@
159
159
  }
160
160
 
161
161
  &::slotted(a:hover) {
162
- color: rgb(var(--zn-primary));
162
+ color: var(--zn-color-primary-fg, rgb(var(--zn-primary)));
163
163
  }
164
164
 
165
165
  &::slotted(a)::after {
@@ -190,7 +190,7 @@
190
190
  cursor: pointer;
191
191
 
192
192
  &:hover {
193
- color: rgb(var(--zn-primary));
193
+ color: var(--zn-color-primary-fg, rgb(var(--zn-primary)));
194
194
  }
195
195
  }
196
196
  }
@@ -232,6 +232,12 @@
232
232
  flex-wrap: nowrap;
233
233
  }
234
234
 
235
+ // Controls in the actions row line up with the buttons beside them, without
236
+ // the label/help-text spacing a form field otherwise carries.
237
+ slot[name="actions"]::slotted(*) {
238
+ --zn-form-control-input-margin: 0;
239
+ }
240
+
235
241
  slot[name="actions"]::slotted(zn-button-menu) {
236
242
  flex-grow: 1;
237
243
  flex-shrink: 1;
@@ -247,3 +253,39 @@
247
253
  border-bottom: none !important;
248
254
  }
249
255
  }
256
+
257
+ .header__left:has(.header__description) {
258
+ display: flex;
259
+ flex-direction: column;
260
+ gap: var(--zn-spacing-2x-small);
261
+ }
262
+
263
+ .header--inline-description .header__left:has(.header__description) {
264
+ flex-direction: row;
265
+ flex-wrap: wrap;
266
+ justify-content: flex-start;
267
+ align-items: baseline;
268
+ gap: 0 var(--zn-spacing-x-small);
269
+ }
270
+
271
+ // Narrow headers drop the actions onto their own row instead of squeezing the caption
272
+ .header .caption {
273
+ flex-wrap: wrap;
274
+ row-gap: var(--zn-spacing-small);
275
+ }
276
+
277
+ .header__caption {
278
+ flex-shrink: 0;
279
+ min-width: min-content;
280
+ }
281
+
282
+ .header__right {
283
+ min-width: min-content;
284
+ }
285
+
286
+ .header__description {
287
+ font-size: var(--zn-header-description-font-size, var(--zn-font-size-x-small));
288
+ font-weight: normal;
289
+ line-height: var(--zn-line-height-normal, 1.45);
290
+ color: var(--zn-header-description-color, var(--zn-text-color-muted));
291
+ }
@@ -81,8 +81,9 @@ export default class ZnHoverContainer extends ZincElement {
81
81
  return triggers.includes(triggerType);
82
82
  }
83
83
 
84
- private handleBlur = () => {
85
- if (this.hasTrigger('focus')) {
84
+ // relatedTarget is retargeted to this host when focus moves into the shadow root, so contains() covers both trees
85
+ private handleBlur = (event: FocusEvent) => {
86
+ if (this.hasTrigger('focus') && !this.contains(event.relatedTarget as Node | null)) {
86
87
  this.hide();
87
88
  }
88
89
  };
@@ -204,9 +205,7 @@ export default class ZnHoverContainer extends ZincElement {
204
205
  hover-bridge>
205
206
  <slot slot="anchor"></slot>
206
207
  <div part="body" id="hover-container" class="hover-container__body"
207
- role="hover-container"
208
- aria-live=${this.open ? 'polite' : 'off'}
209
- aria-label="hover-container">
208
+ aria-live=${this.open ? 'polite' : 'off'}>
210
209
  <slot name="content">${this.content}</slot>
211
210
  </div>
212
211
  </zn-popup>`;
@@ -5,6 +5,7 @@ import {property} from 'lit/decorators.js';
5
5
  import {sha256} from '../../utilities/sha256';
6
6
  import {styleMap} from "lit/directives/style-map.js";
7
7
  import {unsafeSVG} from 'lit/directives/unsafe-svg.js';
8
+ import lineIconStyles from './lni.scss';
8
9
  import ZincElement from '../../internal/zinc-element';
9
10
  import type {IconNode, SVGProps} from 'lucide';
10
11
 
@@ -28,9 +29,27 @@ function loadLucideIcons(): Promise<void> {
28
29
  return lucidePromise;
29
30
  }
30
31
 
32
+ // Country flags (3:2 SVGs keyed by ISO 3166 alpha-2 code) load lazily like lucide
33
+ let flagIcons: Record<string, string> | undefined;
34
+ let flagPromise: Promise<void> | undefined;
35
+
36
+ function loadFlagIcons(): Promise<void> {
37
+ flagPromise ??= import('country-flag-icons/string/3x2').then(m => {
38
+ flagIcons = m as unknown as Record<string, string>;
39
+ }).catch((error: unknown) => {
40
+ flagPromise = undefined;
41
+ console.warn('<zn-icon> failed to load the flag icon chunk', error);
42
+ });
43
+ return flagPromise;
44
+ }
45
+
31
46
  export type IconLibrary = "src" | "material" | "material-outlined" | "material-round" | "material-sharp" |
32
47
  "material-two-tone" | "material-symbols-outlined" | "gravatar" | "libravatar" | "avatar" | "brands" | "line" |
33
- "lucide";
48
+ "lucide" | "flag";
49
+
50
+ export type AvatarPresence = 'online' | 'away' | 'busy' | 'meeting' | 'offline';
51
+
52
+ const namedSizes: Record<string, number> = {sm: 22, md: 28, lg: 40, xl: 56};
34
53
 
35
54
  export type IconColor =
36
55
  "default"
@@ -70,14 +89,24 @@ export type IconColor =
70
89
  *
71
90
  * @cssproperty --example - An example CSS custom property.
72
91
  */
92
+ // Lineicons' ~600 glyph rules are adopted only by icons that use them: every adopted rule is restyled per shadow
93
+ // root, and icons are the most numerous component on a page (over 1,000 on some views, seconds of style work in Safari).
94
+ const lineIcons = unsafeCSS(lineIconStyles);
95
+
73
96
  export default class ZnIcon extends ZincElement {
74
97
  static styles: CSSResultGroup = unsafeCSS(styles);
75
98
 
99
+ private _lineIconsAdopted = false;
100
+
76
101
  @property({reflect: true}) src: string;
77
102
 
78
103
  @property({reflect: true}) alt: string;
79
104
 
80
- @property({type: Number, reflect: true}) size: number = 24;
105
+ /** Size in pixels, or one of the avatar sizes `sm` (22), `md` (28), `lg` (40) and `xl` (56). */
106
+ @property({
107
+ reflect: true,
108
+ converter: {fromAttribute: (value: string | null) => namedSizes[value ?? ''] ?? (value === null ? 24 : Number(value))}
109
+ }) size: number = 24;
81
110
 
82
111
  @property({type: Boolean, reflect: true}) round = false;
83
112
  @property({type: Boolean, reflect: true}) tile = false;
@@ -94,6 +123,12 @@ export default class ZnIcon extends ZincElement {
94
123
 
95
124
  @property({type: Boolean}) squared: boolean = false;
96
125
 
126
+ /** A live-status dot on an avatar: `online`, `away`, `busy`, `meeting` or `offline`. */
127
+ @property({reflect: true}) presence: AvatarPresence;
128
+
129
+ /** Draws an avatar as a solid circle even where the theme tints avatars. */
130
+ @property({type: Boolean, reflect: true}) solid = false;
131
+
97
132
  private static readonly presetColors = new Set([
98
133
  'default', 'primary', 'accent', 'info', 'warning', 'error', 'success',
99
134
  'white', 'disabled', 'red', 'blue', 'green', 'orange', 'yellow',
@@ -148,6 +183,8 @@ export default class ZnIcon extends ZincElement {
148
183
  case "lucide":
149
184
  case "lu":
150
185
  return "lucide";
186
+ case "flag":
187
+ return "flag";
151
188
  case 'brand':
152
189
  case 'brands':
153
190
  return "brands";
@@ -191,6 +228,13 @@ export default class ZnIcon extends ZincElement {
191
228
  await super.getUpdateComplete();
192
229
  }
193
230
  }
231
+ if (this.library === 'flag' && !flagIcons) {
232
+ await loadFlagIcons();
233
+ if (flagIcons) {
234
+ this.requestUpdate();
235
+ await super.getUpdateComplete();
236
+ }
237
+ }
194
238
  return result;
195
239
  }
196
240
 
@@ -273,6 +317,26 @@ export default class ZnIcon extends ZincElement {
273
317
  return email.trim().toLowerCase();
274
318
  }
275
319
 
320
+ protected override updated(changedProperties: PropertyValues<this>) {
321
+ super.updated(changedProperties);
322
+ if (this.library === 'line') {
323
+ this.adoptLineIcons();
324
+ }
325
+ }
326
+
327
+ private adoptLineIcons() {
328
+ const root = this.shadowRoot;
329
+ if (!root || this._lineIconsAdopted) return;
330
+ this._lineIconsAdopted = true;
331
+ if (lineIcons.styleSheet) {
332
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, lineIcons.styleSheet];
333
+ } else {
334
+ const style = document.createElement('style');
335
+ style.textContent = lineIcons.cssText;
336
+ root.appendChild(style);
337
+ }
338
+ }
339
+
276
340
  render() {
277
341
  return html`
278
342
  <div class="icon-wrapper">
@@ -300,6 +364,9 @@ export default class ZnIcon extends ZincElement {
300
364
  'icon--pink': this.color === "pink",
301
365
  'icon--grey': this.color === "grey",
302
366
  'icon--avatar': this.library === "avatar",
367
+ 'icon--avatar-solid': this.library === "avatar" && this.solid,
368
+ 'icon--flag': this.library === "flag",
369
+ 'icon--has-color': Boolean(this.color),
303
370
  'icon--padded': this.padded,
304
371
  'icon--blink': this.blink,
305
372
  'icon--squared': this.squared,
@@ -309,7 +376,7 @@ export default class ZnIcon extends ZincElement {
309
376
  '--icon-size': this.size + "px",
310
377
  '--icon-color': (this.color && !this.isPresetColor(this.color)) ? this.color : null,
311
378
  '--icon-fill': (this.fill && !this.isPresetColor(this.fill)) ? this.fill : null,
312
- '--avatar-color': this.color ? null : (this.library === 'avatar' ? this.getColorForAvatar(this.getAvatarInitials(this.src)) : null)
379
+ '--avatar-color': this.color ? null : (this.library === 'avatar' ? this.getColorForAvatar(this.src) : null)
313
380
  })}">
314
381
  ${this.library && this.library !== "src" ? html`
315
382
  ${choose(this.library, [
@@ -321,12 +388,14 @@ export default class ZnIcon extends ZincElement {
321
388
  ["brands", () => html`<i part="icon" class="kb">${this.src}</i>`],
322
389
  ["line", () => html`<i part="icon" class="lni lni-${this.src}"></i>`],
323
390
  ["lucide", () => this.renderLucideIcon()],
391
+ ["flag", () => this.renderFlagIcon()],
324
392
  ["material-symbols-outlined", () => html`<i part="icon" class="mi mi--symbol-outlined">${this.src}</i>`],
325
393
  ["gravatar", () => html`<img part="icon" alt="${this.alt}"
326
394
  src="https://www.gravatar.com/avatar/${this.src}?s=${this.size}${this.gravatarOptions}"/>`],
327
395
  ["libravatar", () => html`<img part="icon" alt="${this.alt}"
328
396
  src="https://seccdn.libravatar.org/avatar/${this.src}?s=${this.size}${this.gravatarOptions}"/>`],
329
- ["avatar", () => html`<span class="avatar__text">${this.getAvatarInitials(this.src)}</span>`]
397
+ ["avatar", () => html`<span class="avatar__text" role="img"
398
+ aria-label="${this.alt || this.src}">${this.getAvatarInitials(this.src)}</span>`]
330
399
  ], () => html`Library not supported: ${this.library}`)}` : ''}
331
400
 
332
401
  ${(!this.library || this.library === 'src') && this.src ? html`
@@ -336,7 +405,10 @@ export default class ZnIcon extends ZincElement {
336
405
  ${!this.library && !this.src ? html`
337
406
  <slot></slot>` : ''}
338
407
  </div>
339
- </div>`;
408
+ </div>
409
+ ${this.presence ? html`
410
+ <span part="presence" class="presence presence--${this.presence}" role="img"
411
+ aria-label="${this.presence}"></span>` : ''}`;
340
412
  }
341
413
 
342
414
  private getAvatarInitials(avatar: string) {
@@ -376,6 +448,21 @@ export default class ZnIcon extends ZincElement {
376
448
  return unsafeSVG(this.getLucideSvg(icon));
377
449
  }
378
450
 
451
+ private renderFlagIcon() {
452
+ if (!flagIcons) {
453
+ void loadFlagIcons().then(() => {
454
+ if (flagIcons) {
455
+ this.requestUpdate();
456
+ }
457
+ });
458
+ return html``;
459
+ }
460
+
461
+ const code = (this.src ?? '').toUpperCase();
462
+ const svg = Object.prototype.hasOwnProperty.call(flagIcons, code) ? flagIcons[code] : undefined;
463
+ return svg ? html`<span part="icon" class="flag" role="img" aria-label="${this.alt || code}">${unsafeSVG(svg)}</span>` : html``;
464
+ }
465
+
379
466
  private getLucideIcon(src = ''): IconNode | undefined {
380
467
  if (!src) {
381
468
  return undefined;
@@ -438,17 +525,16 @@ export default class ZnIcon extends ZincElement {
438
525
  }
439
526
 
440
527
 
441
- protected getColorForAvatar(avatarInitials: string) {
528
+ protected getColorForAvatar(name: string) {
442
529
  const colors = [
443
530
  '#00AA55', '#0882a8', '#ed55ed', '#5eae00',
444
531
  '#09389e', '#d17300', '#5e0be2', '#5b7a13',
445
532
  '#ddb100', '#d400c2', '#DC2A2A', '#a967ff',
446
533
  ];
447
534
 
448
- // Generate a hash from the initials
449
- const hash = Array.from(avatarInitials).reduce((acc, char) => acc + char.charCodeAt(0), 0);
535
+ // Order-sensitive, so names sharing initials or letters still spread across the palette
536
+ const hash = Array.from(name.trim().toLowerCase()).reduce((acc, char) => (acc * 31 + char.charCodeAt(0)) >>> 0, 0);
450
537
 
451
- // Use the hash to select a color
452
538
  return colors[hash % colors.length];
453
539
  }
454
540
  }
@@ -1,5 +1,4 @@
1
1
  @use "../../wc";
2
- @use "./lni";
3
2
 
4
3
  $icon-colors: (
5
4
  default: --zn-text,
@@ -30,6 +29,40 @@ $icon-colors: (
30
29
  --icon-color: inherit;
31
30
  }
32
31
 
32
+ :host([presence]) {
33
+ position: relative;
34
+ }
35
+
36
+ // A live-status dot on the avatar's corner, ringed in the surface colour so it reads against the avatar.
37
+ .presence {
38
+ position: absolute;
39
+ right: -1px;
40
+ bottom: -1px;
41
+ width: 28%;
42
+ height: 28%;
43
+ min-width: 7px;
44
+ min-height: 7px;
45
+ border: 2px solid var(--zn-avatar-ring-color, rgb(var(--zn-panel)));
46
+ border-radius: 50%;
47
+ background: rgb(var(--zn-color-disabled));
48
+
49
+ &--online {
50
+ background: rgb(var(--zn-color-success));
51
+ }
52
+
53
+ &--away {
54
+ background: rgb(var(--zn-color-warning));
55
+ }
56
+
57
+ &--busy {
58
+ background: rgb(var(--zn-color-error));
59
+ }
60
+
61
+ &--meeting {
62
+ background: rgb(var(--zn-primary));
63
+ }
64
+ }
65
+
33
66
  :host([split-pane-focus]) .icon-wrapper {
34
67
  cursor: pointer;
35
68
  }
@@ -62,6 +95,19 @@ $icon-colors: (
62
95
  font-size: var(--icon-size) !important;
63
96
  }
64
97
 
98
+ &--flag {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ }
102
+
103
+ .flag svg {
104
+ display: block;
105
+ width: var(--icon-size);
106
+ height: calc(var(--icon-size) * 2 / 3);
107
+ border-radius: 2px;
108
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 0.08);
109
+ }
110
+
65
111
  // Colors
66
112
 
67
113
  &--default {
@@ -178,20 +224,38 @@ $icon-colors: (
178
224
  animation: blink-animation 1.5s steps(5, start) infinite;
179
225
  }
180
226
 
227
+ // Solid by default. A theme sets --zn-avatar-tint: 1 for a tinted circle with darker initials,
228
+ // and --zn-avatar-default-color to replace the per-name colour.
181
229
  &--avatar {
182
- background-color: var(--avatar-color, var(--icon-color));
183
- color: hsl(from var(--avatar-color, var(--icon-color)) h s 95);
230
+ --_avatar: var(--zn-avatar-default-color, var(--avatar-color));
231
+ --_tint: var(--zn-avatar-tint, 0);
232
+ --_solid-ink: hsl(from var(--_avatar) h s 95);
233
+ // Mixed towards the text colour so the initials stay readable in light and dark modes
234
+ --_tint-ink: color-mix(in srgb, var(--_avatar) 72%, rgb(var(--zn-text)));
235
+
236
+ // Mixed with the surface, not made translucent, so overlapping avatars in a group stay opaque
237
+ background-color: color-mix(in srgb, var(--_avatar) calc(100% - var(--_tint) * 86%), rgb(var(--zn-panel)));
238
+ color: color-mix(in srgb, var(--_tint-ink) calc(var(--_tint) * 100%), var(--_solid-ink));
184
239
  display: flex;
185
240
  justify-content: center;
186
241
  align-items: center;
187
242
 
188
243
  .avatar__text {
189
- font-size: calc((var(--icon-size) / 2.4));
190
- color: hsl(from var(--avatar-color, var(--icon-color)) h s 95);
191
- font-weight: var(--zn-font-weight-semibold);
244
+ font-size: calc(var(--icon-size) * var(--zn-avatar-font-scale, 0.4167));
245
+ font-weight: var(--zn-avatar-font-weight, var(--zn-font-weight-semibold));
246
+ letter-spacing: .02em;
247
+ color: inherit;
192
248
  }
193
249
  }
194
250
 
251
+ &--avatar.icon--has-color {
252
+ --_avatar: var(--icon-color);
253
+ }
254
+
255
+ &--avatar.icon--avatar-solid {
256
+ --_tint: 0;
257
+ }
258
+
195
259
  &--padded {
196
260
  padding: var(--zn-spacing-small);
197
261
  }
@@ -31,6 +31,19 @@ describe('<zn-icon>', () => {
31
31
  );
32
32
  });
33
33
 
34
+ it('should style Lineicons glyphs only on line icons', async () => {
35
+ const hasLineRules = (el: Element) =>
36
+ el.shadowRoot!.adoptedStyleSheets.some(sheet => Array.from(sheet.cssRules).some(rule => rule.cssText.includes(".lni-home-2")));
37
+
38
+ const line = await fixture<ZnIcon>(html`<zn-icon src="home-2" library="line"></zn-icon>`);
39
+ const glyph = line.shadowRoot!.querySelector('.lni')!;
40
+ expect(getComputedStyle(glyph, '::before').content).to.not.equal('none');
41
+ expect(hasLineRules(line)).to.be.true;
42
+
43
+ const lucide = await fixture<ZnIcon>(html`<zn-icon src="home@lu"></zn-icon>`);
44
+ expect(hasLineRules(lucide)).to.be.false;
45
+ });
46
+
34
47
  it('should render Lucide icons as inline SVGs', async () => {
35
48
  const el = await fixture<ZnIcon>(html`
36
49
  <zn-icon src="cloud-upload" library="lucide" size="48"></zn-icon>
@@ -55,6 +68,19 @@ describe('<zn-icon>', () => {
55
68
  expect(svg).to.exist;
56
69
  });
57
70
 
71
+ it('should render country flags from the flag library', async () => {
72
+ const el = await fixture<ZnIcon>(html`<zn-icon src="gb@flag" size="20"></zn-icon>`);
73
+
74
+ expect(el.library).to.equal('flag');
75
+ expect(el.shadowRoot?.querySelector('.flag[aria-label="GB"] svg')).to.exist;
76
+ });
77
+
78
+ it('should render nothing for an unknown flag', async () => {
79
+ const el = await fixture<ZnIcon>(html`<zn-icon src="zz@flag"></zn-icon>`);
80
+
81
+ expect(el.shadowRoot?.querySelector('.flag')).to.not.exist;
82
+ });
83
+
58
84
  it('should remove a matching library indicator from the icon src', async () => {
59
85
  const el = await fixture<ZnIcon>(html`
60
86
  <zn-icon src="home@lu" library="lucide" size="48"></zn-icon>
@@ -75,4 +101,33 @@ describe('<zn-icon>', () => {
75
101
  expect(el.library).to.equal('material-symbols-outlined');
76
102
  expect(el.src).to.equal('home@lu');
77
103
  });
104
+
105
+ it('should accept the named avatar sizes', async () => {
106
+ const el = await fixture<ZnIcon>(html`<zn-icon src="Fiona Hart" library="avatar" size="lg"></zn-icon>`);
107
+ expect(el.size).to.equal(40);
108
+ expect(el.shadowRoot!.querySelector<HTMLElement>('.icon')!.getBoundingClientRect().width).to.equal(40);
109
+ });
110
+
111
+ it('should still accept a size in pixels', async () => {
112
+ const el = await fixture<ZnIcon>(html`<zn-icon src="home" size="18"></zn-icon>`);
113
+ expect(el.size).to.equal(18);
114
+ });
115
+
116
+ it('should show a presence dot only when presence is set', async () => {
117
+ const el = await fixture<ZnIcon>(html`<zn-icon src="Ben Brooks" library="avatar" round></zn-icon>`);
118
+ expect(el.shadowRoot!.querySelector('[part="presence"]')).to.be.null;
119
+
120
+ el.presence = 'online';
121
+ await el.updateComplete;
122
+ const dot = el.shadowRoot!.querySelector('[part="presence"]')!;
123
+ expect(dot.classList.contains('presence--online')).to.be.true;
124
+ expect(dot.getAttribute('aria-label')).to.equal('online');
125
+ });
126
+
127
+ it('should name an avatar for assistive technology', async () => {
128
+ const el = await fixture<ZnIcon>(html`<zn-icon src="BB" alt="Ben Brooks" library="avatar"></zn-icon>`);
129
+ const text = el.shadowRoot!.querySelector('.avatar__text')!;
130
+ expect(text.getAttribute('role')).to.equal('img');
131
+ expect(text.getAttribute('aria-label')).to.equal('Ben Brooks');
132
+ });
78
133
  });