@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
@@ -0,0 +1,141 @@
1
+ @use "../../wc";
2
+
3
+ :host {
4
+ display: block;
5
+ }
6
+
7
+ .color-picker {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: 12px;
11
+ width: var(--zn-color-picker-width, 280px);
12
+ padding: 12px;
13
+ background: var(--zn-panel-background-color, rgb(var(--zn-panel)));
14
+ border: 1px solid rgb(var(--zn-border-color));
15
+ border-radius: var(--zn-border-radius-medium);
16
+ box-shadow: var(--zn-shadow-large);
17
+ font-family: var(--zn-font-family-sans);
18
+ }
19
+
20
+ .color-picker__area {
21
+ position: relative;
22
+ height: 152px;
23
+ border-radius: var(--zn-border-radius);
24
+ cursor: crosshair;
25
+ touch-action: none;
26
+ background-image: linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
27
+
28
+ &:focus-visible {
29
+ outline: none;
30
+ box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
31
+ }
32
+ }
33
+
34
+ // Not @extend: a merged selector list holding an unknown vendor pseudo-element is dropped whole.
35
+ @mixin thumb {
36
+ border: 2px solid #fff;
37
+ border-radius: 50%;
38
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, .35), 0 1px 3px rgba(0, 0, 0, .4);
39
+ }
40
+
41
+ .color-picker__dot {
42
+ @include thumb;
43
+ position: absolute;
44
+ width: 14px;
45
+ height: 14px;
46
+ margin: -7px 0 0 -7px;
47
+ pointer-events: none;
48
+ }
49
+
50
+ .color-picker__hue {
51
+ appearance: none;
52
+ width: 100%;
53
+ height: 12px;
54
+ margin: 0;
55
+ border-radius: 999px;
56
+ cursor: pointer;
57
+ background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
58
+
59
+ &::-webkit-slider-thumb {
60
+ @include thumb;
61
+ appearance: none;
62
+ width: 16px;
63
+ height: 16px;
64
+ background: transparent;
65
+ }
66
+
67
+ &::-moz-range-thumb {
68
+ @include thumb;
69
+ width: 12px;
70
+ height: 12px;
71
+ background: transparent;
72
+ }
73
+
74
+ &:focus-visible {
75
+ outline: none;
76
+ box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
77
+ }
78
+ }
79
+
80
+ .color-picker__values {
81
+ display: flex;
82
+ flex-direction: column;
83
+ gap: 8px;
84
+ }
85
+
86
+ .color-picker__row {
87
+ display: grid;
88
+ grid-template-columns: 48px 1fr;
89
+ align-items: center;
90
+ gap: 8px;
91
+ }
92
+
93
+ .color-picker__row-label {
94
+ font-size: 11px;
95
+ font-weight: var(--zn-font-weight-semibold);
96
+ line-height: 1;
97
+ letter-spacing: 0.04em;
98
+ color: var(--zn-input-placeholder-color);
99
+ }
100
+
101
+ .color-picker__trio {
102
+ display: grid;
103
+ grid-template-columns: repeat(3, minmax(0, 1fr));
104
+ gap: 4px;
105
+ }
106
+
107
+ .color-picker__field {
108
+ width: 100%;
109
+ min-width: 0;
110
+ height: var(--zn-input-height-small);
111
+ padding: 0 8px;
112
+ font-family: var(--zn-font-family-mono);
113
+ font-size: 12px;
114
+ font-variant-numeric: tabular-nums;
115
+ text-align: center;
116
+ color: var(--zn-input-color);
117
+ background: var(--zn-input-background-color);
118
+ border: solid var(--zn-input-border-width) var(--zn-input-border-color);
119
+ border-radius: var(--zn-input-border-radius-small);
120
+ box-shadow: var(--zn-input-shadow, none);
121
+ transition: var(--zn-transition-fast) border-color, var(--zn-transition-fast) box-shadow;
122
+
123
+ &:hover {
124
+ border-color: var(--zn-input-border-color-hover);
125
+ }
126
+
127
+ &:focus {
128
+ outline: none;
129
+ border-color: var(--zn-input-border-color-focus);
130
+ box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
131
+ }
132
+
133
+ &[aria-invalid='true'] {
134
+ color: var(--zn-color-danger-fg);
135
+ border-color: var(--zn-input-border-color-invalid);
136
+ }
137
+ }
138
+
139
+ .color-picker__row--hex .color-picker__field {
140
+ text-align: left;
141
+ }
@@ -0,0 +1,57 @@
1
+ import '../../../dist/zn.min.js';
2
+ import {expect, fixture, html, oneEvent} from '@open-wc/testing';
3
+ import type ZnColorPicker from './color-picker.component';
4
+
5
+ const field = (el: ZnColorPicker, key: string) => el.shadowRoot!.querySelector<HTMLInputElement>(`[data-k="${key}"]`)!;
6
+
7
+ const type = (input: HTMLInputElement, value: string) => {
8
+ input.focus();
9
+ input.value = value;
10
+ input.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
11
+ };
12
+
13
+ describe('<zn-color-picker>', () => {
14
+ it('shows the colour in every format', async () => {
15
+ const el = await fixture<ZnColorPicker>(html`<zn-color-picker value="rgb(8, 94, 253)"></zn-color-picker>`);
16
+
17
+ expect(el.value).to.equal('#085EFD');
18
+ expect(field(el, 'hex').value).to.equal('#085EFD');
19
+ expect([field(el, 'r').value, field(el, 'g').value, field(el, 'b').value]).to.deep.equal(['8', '94', '253']);
20
+ expect([field(el, 'h').value, field(el, 's').value, field(el, 'l').value]).to.deep.equal(['219', '98', '51']);
21
+ expect([field(el, 'ol').value, field(el, 'oc').value, field(el, 'oh').value]).to.deep.equal(['54.8', '0.247', '262']);
22
+ });
23
+
24
+ it('updates the other rows when one is edited and switches to its format', async () => {
25
+ const el = await fixture<ZnColorPicker>(html`<zn-color-picker value="#085EFD"></zn-color-picker>`);
26
+
27
+ const changed = oneEvent(el, 'zn-input');
28
+ type(field(el, 'g'), '200');
29
+ await changed;
30
+ await el.updateComplete;
31
+
32
+ expect(el.value).to.equal('#08C8FD');
33
+ expect(el.format).to.equal('rgb');
34
+ expect(field(el, 'hex').value).to.equal('#08C8FD');
35
+ expect(field(el, 'g').value).to.equal('200');
36
+ });
37
+
38
+ it('marks an out-of-range value red without changing the colour', async () => {
39
+ const el = await fixture<ZnColorPicker>(html`<zn-color-picker value="#085EFD"></zn-color-picker>`);
40
+
41
+ type(field(el, 'r'), '300');
42
+ await el.updateComplete;
43
+
44
+ expect(field(el, 'r').getAttribute('aria-invalid')).to.equal('true');
45
+ expect(el.value).to.equal('#085EFD');
46
+ });
47
+
48
+ it('moves brightness with the arrow keys', async () => {
49
+ const el = await fixture<ZnColorPicker>(html`<zn-color-picker value="#FFFFFF"></zn-color-picker>`);
50
+ const area = el.shadowRoot!.querySelector<HTMLElement>('.color-picker__area')!;
51
+
52
+ area.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'}));
53
+ await el.updateComplete;
54
+
55
+ expect(el.value).to.equal('#FCFCFC');
56
+ });
57
+ });
@@ -0,0 +1,12 @@
1
+ import ZnColorPicker from './color-picker.component';
2
+
3
+ export * from './color-picker.component';
4
+ export default ZnColorPicker;
5
+
6
+ ZnColorPicker.define('zn-color-picker');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-color-picker': ZnColorPicker;
11
+ }
12
+ }
@@ -48,6 +48,9 @@ export default class ZnCols extends ZincElement {
48
48
 
49
49
  @property({attribute: 'mc', type: Number, reflect: true}) maxColumns: number = 0;
50
50
 
51
+ /** Minimum width in px of a one-unit column before the row wraps; a column of layout `n` asks for n times this. Defaults to 250. */
52
+ @property({type: Number}) basis: number;
53
+
51
54
  @property({attribute: 'no-gap', type: Boolean}) noGap: boolean = false;
52
55
 
53
56
  @property({type: Boolean}) border: boolean = false;
@@ -81,17 +84,27 @@ export default class ZnCols extends ZincElement {
81
84
 
82
85
  protected updated(changed: PropertyValues<this>) {
83
86
  super.updated(changed);
87
+ if (changed.has('basis')) {
88
+ if (this.basis > 0) {
89
+ this.style.setProperty('--zn-col-basis', `${this.basis}px`);
90
+ } else {
91
+ this.style.removeProperty('--zn-col-basis');
92
+ }
93
+ }
84
94
  this.markRowEnds();
85
95
  }
86
96
 
87
97
  /**
88
- * Flags each child that ends its row as laid out, so `divide` only draws between neighbours on the same row
89
- * however the columns have wrapped.
98
+ * Flags each child that ends its row, and each child on the last row, as laid out, so `divide` only draws
99
+ * between neighbours however the columns have wrapped.
90
100
  */
91
101
  private markRowEnds() {
92
102
  const children = Array.from(this.querySelectorAll(':scope > *:not([slot])'));
93
103
  if (!this.divide) {
94
- children.forEach((child) => child.removeAttribute('data-row-end'));
104
+ children.forEach((child) => {
105
+ child.removeAttribute('data-row-end');
106
+ child.removeAttribute('data-last-row');
107
+ });
95
108
  return;
96
109
  }
97
110
 
@@ -101,6 +114,7 @@ export default class ZnCols extends ZincElement {
101
114
  const rowEnd = !rects.some((other) =>
102
115
  other !== rect && other.top < rect.bottom && other.bottom > rect.top && other.left > rect.left);
103
116
  child.toggleAttribute('data-row-end', rowEnd);
117
+ child.toggleAttribute('data-last-row', !rects.some((other) => other.top >= rect.bottom));
104
118
  });
105
119
  }
106
120
 
@@ -67,6 +67,22 @@
67
67
  ::slotted([data-row-end]):after {
68
68
  display: none;
69
69
  }
70
+
71
+ ::slotted(:not([data-last-row])) {
72
+ position: relative;
73
+ }
74
+ }
75
+
76
+ // Border mode owns :before for its own row rules.
77
+ &--divide:not(.cols--border) ::slotted(:not([data-last-row])):before {
78
+ content: "";
79
+ position: absolute;
80
+ display: block;
81
+ height: 1px;
82
+ left: 0;
83
+ right: 0;
84
+ bottom: calc((var(--zn-spacing-small) / 2) * -1);
85
+ background-color: rgb(var(--zn-border-color));
70
86
  }
71
87
 
72
88
  &--no-gap {
@@ -74,8 +90,14 @@
74
90
  }
75
91
 
76
92
  // Without a gap to sit in, the divider goes on the column's own edge.
77
- &--no-gap.cols--divide ::slotted(*:not(:last-child)):after {
78
- right: 0;
93
+ &--no-gap.cols--divide {
94
+ ::slotted(*:not(:last-child)):after {
95
+ right: 0;
96
+ }
97
+
98
+ ::slotted(:not([data-last-row])):before {
99
+ bottom: 0;
100
+ }
79
101
  }
80
102
 
81
103
  // The row rule below each child is drawn by a pseudo reaching to -100vw, so border mode has to clip. Clipping
@@ -155,17 +177,6 @@ $stack-sizes: (sm, smp, ph, md, lg, hd, '3k', '4k');
155
177
  .cols--divide ::slotted(*:not(:last-child)):after {
156
178
  display: none;
157
179
  }
158
-
159
- // Edge-to-edge columns keep their separation as a rule between the stacked rows
160
- .cols--no-gap.cols--divide ::slotted(*:not(:last-child)):after {
161
- display: block;
162
- top: auto;
163
- right: 0;
164
- bottom: 0;
165
- left: 0;
166
- width: auto;
167
- height: 1px;
168
- }
169
180
  }
170
181
  }
171
182
  }
@@ -165,5 +165,38 @@ describe('<zn-cols>', () => {
165
165
 
166
166
  expect(dividerOf(el.querySelector('#a'))).to.equal('none');
167
167
  });
168
+
169
+ it('should rule between wrapped rows', async () => {
170
+ const el = await cols(`
171
+ <zn-cols layout="1,1" divide style="width: 600px">
172
+ <div id="a" style="height: 20px"></div>
173
+ <div id="b" style="height: 20px"></div>
174
+ <div id="c" style="height: 20px"></div>
175
+ <div id="d" style="height: 20px"></div>
176
+ </zn-cols>`);
177
+ const ruleOf = (id: string) => getComputedStyle(el.querySelector(id)!, '::before').content;
178
+
179
+ expect(ruleOf('#a')).to.not.equal('none');
180
+ expect(ruleOf('#b')).to.not.equal('none');
181
+ expect(ruleOf('#c')).to.equal('none');
182
+ expect(ruleOf('#d')).to.equal('none');
183
+ });
184
+ });
185
+
186
+ it('sizes columns from the basis attribute', async () => {
187
+ const el = await cols(`
188
+ <zn-cols layout="1,2" basis="140">
189
+ <div id="one"></div>
190
+ <div id="two"></div>
191
+ </zn-cols>`);
192
+
193
+ expect(getComputedStyle(el.querySelector('#one')!).flexBasis).to.equal('140px');
194
+ expect(getComputedStyle(el.querySelector('#two')!).flexBasis).to.equal('280px');
195
+ });
196
+
197
+ it('keeps the default basis without the attribute', async () => {
198
+ const el = await cols(`<zn-cols layout="1"><div id="one"></div></zn-cols>`);
199
+
200
+ expect(getComputedStyle(el.querySelector('#one')!).flexBasis).to.equal('250px');
168
201
  });
169
202
  });
@@ -6,6 +6,7 @@ import {ifDefined} from "lit/directives/if-defined.js";
6
6
  import {property, query, state} from 'lit/decorators.js';
7
7
  import ZincElement from "../../internal/zinc-element";
8
8
  import ZnDialog from "../dialog";
9
+ import type ZnButton from "../button";
9
10
 
10
11
  import styles from './confirm.scss';
11
12
 
@@ -66,11 +67,11 @@ export default class ZnConfirm extends ZincElement {
66
67
 
67
68
  @property() content: string = '';
68
69
 
69
- @property() confirmText: string = "Confirm";
70
+ @property({attribute: 'confirm-text'}) confirmText: string = "Confirm";
70
71
 
71
- @property() cancelText: string = "Cancel";
72
+ @property({attribute: 'cancel-text'}) cancelText: string = "Cancel";
72
73
 
73
- @property() closeText: string = "Close";
74
+ @property({attribute: 'close-text'}) closeText: string = "Close";
74
75
 
75
76
  @property({type: Boolean, attribute: 'hide-icon'}) hideIcon: boolean = false;
76
77
 
@@ -102,6 +103,8 @@ export default class ZnConfirm extends ZincElement {
102
103
 
103
104
  private submitted: HTMLFormElement | null = null;
104
105
 
106
+ private generatedForm: HTMLFormElement | null = null;
107
+
105
108
  protected firstUpdated(_changedProperties: PropertyValues) {
106
109
  super.firstUpdated(_changedProperties);
107
110
  if (this.open) {
@@ -132,11 +135,13 @@ export default class ZnConfirm extends ZincElement {
132
135
  });
133
136
  }
134
137
 
135
- show = (event: Event | undefined = undefined) => {
138
+ show = async (event: Event | undefined = undefined) => {
136
139
  const trigger = event?.target as HTMLButtonElement
137
140
  if (trigger?.disabled) return;
138
141
  this.reset();
139
142
  this.dialog.show();
143
+ await this.updateComplete;
144
+ this.renderRoot.querySelector<ZnButton>('zn-button[dialog-closer]')?.button?.focus();
140
145
  }
141
146
 
142
147
  hide() {
@@ -169,7 +174,7 @@ export default class ZnConfirm extends ZincElement {
169
174
  * The console dispatches the pagelet lifecycle on the form it submitted. Without it the loading
170
175
  * state only ends when a response happens to reload the page, so every failure hangs the dialog.
171
176
  */
172
- private settle = (event: Event) => {
177
+ private settle = async (event: Event) => {
173
178
  const response = (event as CustomEvent<{ response?: PageletResponse }>).detail?.response;
174
179
  this.stopWatching();
175
180
  if (event.type === 'error' || this.responseFailed(response)) {
@@ -179,6 +184,8 @@ export default class ZnConfirm extends ZincElement {
179
184
  if (this.dialog?.closer) {
180
185
  this.dialog.closer.disabled = false;
181
186
  }
187
+ await this.updateComplete;
188
+ this.renderRoot.querySelector<ZnButton>('zn-button[dialog-closer]')?.button?.focus();
182
189
  return;
183
190
  }
184
191
  this.hide();
@@ -237,7 +244,7 @@ export default class ZnConfirm extends ZincElement {
237
244
  <zn-button outline color="${this.type}" slot="footer" dialog-closer disabled=${this.loading || nothing}>
238
245
  ${this.cancelText}
239
246
  </zn-button>
240
- <zn-button color="${this.type}" slot="footer" @click="${this.submitDialog}" disabled=${this.loading || nothing}>
247
+ <zn-button color="${this.type}" slot="footer" @click="${this.submitDialog}" ?processing=${this.loading}>
241
248
  ${this.confirmText}
242
249
  </zn-button>`}
243
250
 
@@ -246,10 +253,17 @@ export default class ZnConfirm extends ZincElement {
246
253
  }
247
254
 
248
255
  submitDialog() {
256
+ if (this.loading) return;
257
+
258
+ // Rebuilt per submit: a re-rendered list can reuse this element for another row with a different action
259
+ this.generatedForm?.remove();
260
+ this.generatedForm = null;
261
+
249
262
  // get the form from the slot
250
263
  let form = this.querySelector('form');
251
264
  if (!form && this.action) {
252
265
  form = document.createElement('form') as HTMLFormElement;
266
+ this.generatedForm = form;
253
267
  form.action = this.action;
254
268
  form.method = 'POST';
255
269
 
@@ -61,6 +61,15 @@ describe('<zn-confirm-modal>', () => {
61
61
  expect(confirm.shadowRoot!.textContent).to.contain('Loading');
62
62
  });
63
63
 
64
+ it('keeps the confirm button focusable while loading', async () => {
65
+ const {confirm} = await submittedConfirm();
66
+
67
+ const buttons = confirm.shadowRoot!.querySelectorAll('zn-button[slot="footer"]');
68
+ expect(buttons[0].hasAttribute('disabled')).to.be.true;
69
+ expect(buttons[1].hasAttribute('disabled')).to.be.false;
70
+ expect(buttons[1].hasAttribute('processing')).to.be.true;
71
+ });
72
+
64
73
  it('reports failure when the response carries a danger alert', async () => {
65
74
  const {confirm, form, dialog} = await submittedConfirm();
66
75
 
@@ -75,6 +84,16 @@ describe('<zn-confirm-modal>', () => {
75
84
  expect(closer(dialog).disabled).to.be.false;
76
85
  });
77
86
 
87
+ it('focuses cancel when opened', async () => {
88
+ const confirm = await fixture<HTMLElement>(html`<zn-confirm caption="Deploy"></zn-confirm>`);
89
+
90
+ await (confirm as never as {show: () => Promise<void>}).show();
91
+
92
+ const cancel = confirm.shadowRoot!.querySelector('zn-button[dialog-closer]');
93
+ expect(cancel!.textContent!.trim()).to.equal('Cancel');
94
+ expect(confirm.shadowRoot!.activeElement).to.equal(cancel);
95
+ });
96
+
78
97
  it('offers only a close button once the request has failed', async () => {
79
98
  const {confirm, form} = await submittedConfirm();
80
99
 
@@ -85,6 +104,7 @@ describe('<zn-confirm-modal>', () => {
85
104
  expect(buttons.length).to.equal(1);
86
105
  expect(buttons[0].textContent!.trim()).to.equal('Close');
87
106
  expect(buttons[0].hasAttribute('dialog-closer')).to.be.true;
107
+ expect(confirm.shadowRoot!.activeElement).to.equal(buttons[0]);
88
108
  });
89
109
 
90
110
  it('reports failure when the request errors', async () => {
@@ -117,4 +137,36 @@ describe('<zn-confirm-modal>', () => {
117
137
 
118
138
  expect(confirm.shadowRoot!.textContent).to.not.contain('Failed');
119
139
  });
140
+ it('submits to its current action after the action changes', async () => {
141
+ const confirm = await fixture<HTMLElement & {action: string; updateComplete: Promise<void>; show: () => void}>(
142
+ html`<zn-confirm caption="Delete" action="/items/1/delete"></zn-confirm>`);
143
+ const posted: string[] = [];
144
+ document.addEventListener('submit', e => {
145
+ e.preventDefault();
146
+ posted.push(new URL((e.target as HTMLFormElement).action).pathname);
147
+ });
148
+ const submit = async () => {
149
+ confirm.show();
150
+ await confirm.updateComplete;
151
+ click(confirm.shadowRoot!.querySelectorAll('zn-button[slot="footer"]')[1]);
152
+ };
153
+
154
+ await submit();
155
+ confirm.action = '/items/2/delete';
156
+ await confirm.updateComplete;
157
+ await submit();
158
+
159
+ expect(posted).to.deep.equal(['/items/1/delete', '/items/2/delete']);
160
+ });
161
+
162
+ it('labels its buttons from the dashed text attributes', async () => {
163
+ const confirm = await fixture<HTMLElement & {updateComplete: Promise<void>}>(html`
164
+ <zn-confirm confirm-text="Archive" cancel-text="Keep"></zn-confirm>
165
+ `);
166
+ await confirm.updateComplete;
167
+
168
+ const buttons = confirm.shadowRoot!.querySelectorAll('zn-button[slot="footer"]');
169
+ expect(buttons[0].textContent!.trim()).to.equal('Keep');
170
+ expect(buttons[1].textContent!.trim()).to.equal('Archive');
171
+ });
120
172
  });
@@ -3,6 +3,7 @@ import {deepQuerySelectorAll} from "../../utilities/query";
3
3
  import {HasSlotController} from "../../internal/slot";
4
4
  import {html, unsafeCSS} from 'lit';
5
5
  import {htmlToText, looksLikeHtml} from './html-text';
6
+ import {ifDefined} from "lit/directives/if-defined.js";
6
7
  import {MutationController} from '@lit-labs/observers/mutation-controller.js';
7
8
  import {property, queryAssignedNodes, queryAsync, state} from 'lit/decorators.js';
8
9
  import {sanitizeHtml} from '../../utilities/sanitize-html';
@@ -82,6 +83,7 @@ export default class ContentBlock extends ZincElement {
82
83
  connectedCallback() {
83
84
  super.connectedCallback();
84
85
  this.iframe.then((iframe) => {
86
+ if (!iframe) return;
85
87
  if (iframe.contentDocument) {
86
88
  const div = this.htmlNodes[0] as HTMLDivElement;
87
89
  if (div) {
@@ -109,6 +111,23 @@ export default class ContentBlock extends ZincElement {
109
111
  super.disconnectedCallback();
110
112
  }
111
113
 
114
+ private _handleHeaderKeyDown(e: KeyboardEvent) {
115
+ // Keys from the header's own actions belong to them
116
+ if (e.target !== e.currentTarget || (e.key !== 'Enter' && e.key !== ' ')) {
117
+ return;
118
+ }
119
+
120
+ e.preventDefault();
121
+ this._collapseContent(e);
122
+ }
123
+
124
+ private _handleReplyKeyDown(e: KeyboardEvent) {
125
+ if (e.key === 'Enter' || e.key === ' ') {
126
+ e.preventDefault();
127
+ (e.currentTarget as HTMLElement).click();
128
+ }
129
+ }
130
+
112
131
  private _collapseContent(e: Event) {
113
132
  if (this.noCollapse) {
114
133
  return;
@@ -136,7 +155,7 @@ export default class ContentBlock extends ZincElement {
136
155
  private _toggleText() {
137
156
  const textContent = this.shadowRoot?.querySelector('.text-content') as HTMLDivElement | null;
138
157
  this.iframe.then((iframe) => {
139
- if (textContent) {
158
+ if (iframe && textContent) {
140
159
  iframe.classList.add('hidden');
141
160
  textContent.classList.remove('hidden');
142
161
  }
@@ -146,7 +165,7 @@ export default class ContentBlock extends ZincElement {
146
165
  private _toggleHtml() {
147
166
  const textContent = this.shadowRoot?.querySelector('.text-content') as HTMLDivElement | null;
148
167
  this.iframe.then((iframe) => {
149
- if (textContent) {
168
+ if (iframe && textContent) {
150
169
  iframe.classList.remove('hidden');
151
170
  textContent.classList.add('hidden');
152
171
  setTimeout(() => this._resizeIframe(iframe), 0);
@@ -230,7 +249,12 @@ export default class ContentBlock extends ZincElement {
230
249
  'content-block--collapsible': !this.noCollapse
231
250
  })}">
232
251
 
233
- <div class="content-block-header" @click="${this._collapseContent}">
252
+ <div class="content-block-header"
253
+ role="${ifDefined(this.noCollapse ? undefined : 'button')}"
254
+ tabindex="${ifDefined(this.noCollapse ? undefined : '0')}"
255
+ aria-expanded="${ifDefined(this.noCollapse ? undefined : String(!this.short))}"
256
+ @click="${this._collapseContent}"
257
+ @keydown="${this._handleHeaderKeyDown}">
234
258
  <div class="content-block-header__icon">
235
259
  <slot name="icon">
236
260
  <zn-icon src="${this.avatar}"
@@ -270,13 +294,16 @@ export default class ContentBlock extends ZincElement {
270
294
  </div>
271
295
 
272
296
  <zn-sp no-gap flush-x flush-t>
273
- <iframe class="${classMap({'hidden': !initialShowHtml})}" title="Content block HTML preview"></iframe>
297
+ ${hasHtmlSlot ? html`
298
+ <iframe class="${classMap({'hidden': !initialShowHtml})}" title="Content block HTML preview"></iframe>` : ''}
274
299
  <slot class="html-content" name="html" style="display: none;"></slot>
275
300
  <slot name="text" style="display: none;"></slot>
276
301
  <div class="${classMap({'text-content': true, 'hidden': initialShowHtml})}">
277
302
  ${text.map((section) => html`
278
303
  ${section.type === 'reply' ? html`
279
- <div class="toggle-reply" @click="${this.showReply}">...</div>` : ''}
304
+ <div class="toggle-reply" role="button" tabindex="0" aria-label="Show quoted text"
305
+ @click="${this.showReply}"
306
+ @keydown="${this._handleReplyKeyDown}">...</div>` : ''}
280
307
  <div class=${classMap({
281
308
  'text-section': true,
282
309
  'text-section--reply': section.type === 'reply',
@@ -307,7 +334,7 @@ export default class ContentBlock extends ZincElement {
307
334
  protected truncateText() {
308
335
  const textContent = this.querySelector('[slot="text"]') as HTMLDivElement | null;
309
336
  if (!textContent) return '';
310
- const trimmed = htmlToText(textContent.innerText);
337
+ const trimmed = htmlToText(textContent.textContent ?? '');
311
338
  return trimmed.length > 32 ? trimmed.substring(0, 32) + '...' : trimmed;
312
339
  }
313
340
 
@@ -379,13 +406,15 @@ export default class ContentBlock extends ZincElement {
379
406
  const textContent = this.querySelector('[slot="text"]') as HTMLDivElement | null;
380
407
  const textRows: TextRow[] = [];
381
408
 
382
- if (textContent && looksLikeHtml(textContent.innerText)) {
383
- this._textRows = [{lines: [sanitizeHtml(textContent.innerText, EDITOR_TAGS, true)], type: 'text', html: true}];
409
+ // Not innerText: it forces a layout per read (~6ms each in Safari), and this slot is never rendered.
410
+ const source = textContent?.textContent ?? '';
411
+ if (textContent && looksLikeHtml(source)) {
412
+ this._textRows = [{lines: [sanitizeHtml(source, EDITOR_TAGS, true)], type: 'text', html: true}];
384
413
  return this._textRows;
385
414
  }
386
415
 
387
416
  if (textContent) {
388
- const text = textContent.innerText.replace(/<br\s*\/?>/gi, '\n');
417
+ const text = source.replace(/<br\s*\/?>/gi, '\n');
389
418
  const rows = text.split('\n');
390
419
  let previousType: TextRow['type'] | null = null;
391
420
  let forceReply = false;