ngwr 14.3.1 → 14.5.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 (368) hide show
  1. package/README.md +4 -4
  2. package/avatar/styles/_index.scss +4 -2
  3. package/badge/styles/_index.scss +4 -1
  4. package/badge/styles/_tag.scss +3 -0
  5. package/cascader/styles/_rules.scss +3 -0
  6. package/command-palette/styles/_index.scss +23 -0
  7. package/context-menu/styles/_index.scss +13 -1
  8. package/fesm2022/ngwr-action-sheet.mjs +3 -3
  9. package/fesm2022/ngwr-affix.mjs +17 -4
  10. package/fesm2022/ngwr-affix.mjs.map +1 -1
  11. package/fesm2022/ngwr-alert.mjs +4 -4
  12. package/fesm2022/ngwr-alert.mjs.map +1 -1
  13. package/fesm2022/ngwr-anchor.mjs +3 -3
  14. package/fesm2022/ngwr-aurora.mjs +3 -3
  15. package/fesm2022/ngwr-avatar.mjs +3 -3
  16. package/fesm2022/ngwr-back-top.mjs +3 -3
  17. package/fesm2022/ngwr-badge.mjs +6 -6
  18. package/fesm2022/ngwr-bar-chart-testing.mjs +18 -2
  19. package/fesm2022/ngwr-bar-chart-testing.mjs.map +1 -1
  20. package/fesm2022/ngwr-bar-chart.mjs +3 -3
  21. package/fesm2022/ngwr-blur-text.mjs +3 -3
  22. package/fesm2022/ngwr-border-glow.mjs +3 -3
  23. package/fesm2022/ngwr-breadcrumbs.mjs +7 -7
  24. package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
  25. package/fesm2022/ngwr-burger.mjs +3 -3
  26. package/fesm2022/ngwr-button.mjs +6 -6
  27. package/fesm2022/ngwr-calendar-heatmap-testing.mjs +12 -9
  28. package/fesm2022/ngwr-calendar-heatmap-testing.mjs.map +1 -1
  29. package/fesm2022/ngwr-calendar-heatmap.mjs +35 -7
  30. package/fesm2022/ngwr-calendar-heatmap.mjs.map +1 -1
  31. package/fesm2022/ngwr-calendar.mjs +32 -4
  32. package/fesm2022/ngwr-calendar.mjs.map +1 -1
  33. package/fesm2022/ngwr-card.mjs +9 -9
  34. package/fesm2022/ngwr-carousel.mjs +6 -6
  35. package/fesm2022/ngwr-cascader-testing.mjs +87 -17
  36. package/fesm2022/ngwr-cascader-testing.mjs.map +1 -1
  37. package/fesm2022/ngwr-cascader.mjs +76 -5
  38. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  39. package/fesm2022/ngwr-checkbox-testing.mjs +13 -2
  40. package/fesm2022/ngwr-checkbox-testing.mjs.map +1 -1
  41. package/fesm2022/ngwr-checkbox.mjs +42 -7
  42. package/fesm2022/ngwr-checkbox.mjs.map +1 -1
  43. package/fesm2022/ngwr-circular-text.mjs +3 -3
  44. package/fesm2022/ngwr-click-spark.mjs +3 -3
  45. package/fesm2022/ngwr-clipboard.mjs +3 -3
  46. package/fesm2022/ngwr-collapse.mjs +9 -9
  47. package/fesm2022/ngwr-color-picker.mjs +24 -8
  48. package/fesm2022/ngwr-color-picker.mjs.map +1 -1
  49. package/fesm2022/ngwr-command-palette.mjs +16 -3
  50. package/fesm2022/ngwr-command-palette.mjs.map +1 -1
  51. package/fesm2022/ngwr-compare.mjs +3 -3
  52. package/fesm2022/ngwr-confetti.mjs +3 -3
  53. package/fesm2022/ngwr-context-menu.mjs +47 -15
  54. package/fesm2022/ngwr-context-menu.mjs.map +1 -1
  55. package/fesm2022/ngwr-cookie.mjs +3 -3
  56. package/fesm2022/ngwr-counter.mjs +6 -6
  57. package/fesm2022/ngwr-date-adapters-fns.mjs +3 -3
  58. package/fesm2022/ngwr-date-adapters-luxon.mjs +3 -3
  59. package/fesm2022/ngwr-date-picker.mjs +111 -28
  60. package/fesm2022/ngwr-date-picker.mjs.map +1 -1
  61. package/fesm2022/ngwr-date.mjs +3 -3
  62. package/fesm2022/ngwr-decrypt-text.mjs +3 -3
  63. package/fesm2022/ngwr-density.mjs +6 -6
  64. package/fesm2022/ngwr-descriptions.mjs +6 -6
  65. package/fesm2022/ngwr-dialog.mjs +27 -16
  66. package/fesm2022/ngwr-dialog.mjs.map +1 -1
  67. package/fesm2022/ngwr-directives.mjs +12 -12
  68. package/fesm2022/ngwr-divider.mjs +3 -3
  69. package/fesm2022/ngwr-donut-chart-testing.mjs +18 -2
  70. package/fesm2022/ngwr-donut-chart-testing.mjs.map +1 -1
  71. package/fesm2022/ngwr-donut-chart.mjs +4 -4
  72. package/fesm2022/ngwr-donut-chart.mjs.map +1 -1
  73. package/fesm2022/ngwr-drag-drop.mjs +7 -7
  74. package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
  75. package/fesm2022/ngwr-drawer.mjs +66 -20
  76. package/fesm2022/ngwr-drawer.mjs.map +1 -1
  77. package/fesm2022/ngwr-dropdown.mjs +54 -16
  78. package/fesm2022/ngwr-dropdown.mjs.map +1 -1
  79. package/fesm2022/ngwr-empty.mjs +3 -3
  80. package/fesm2022/ngwr-event-calendar.mjs +6 -6
  81. package/fesm2022/ngwr-falling-text.mjs +3 -3
  82. package/fesm2022/ngwr-file-upload.mjs +18 -5
  83. package/fesm2022/ngwr-file-upload.mjs.map +1 -1
  84. package/fesm2022/ngwr-form.mjs +66 -14
  85. package/fesm2022/ngwr-form.mjs.map +1 -1
  86. package/fesm2022/ngwr-fuzzy-text.mjs +3 -3
  87. package/fesm2022/ngwr-gauge-testing.mjs +16 -2
  88. package/fesm2022/ngwr-gauge-testing.mjs.map +1 -1
  89. package/fesm2022/ngwr-gauge.mjs +3 -3
  90. package/fesm2022/ngwr-glitch-text.mjs +3 -3
  91. package/fesm2022/ngwr-gradient-text.mjs +3 -3
  92. package/fesm2022/ngwr-graph-testing.mjs +243 -0
  93. package/fesm2022/ngwr-graph-testing.mjs.map +1 -0
  94. package/fesm2022/ngwr-graph.mjs +1112 -0
  95. package/fesm2022/ngwr-graph.mjs.map +1 -0
  96. package/fesm2022/ngwr-hotkey.mjs +6 -6
  97. package/fesm2022/ngwr-i18n-ar.mjs +7 -0
  98. package/fesm2022/ngwr-i18n-ar.mjs.map +1 -1
  99. package/fesm2022/ngwr-i18n-cs.mjs +7 -0
  100. package/fesm2022/ngwr-i18n-cs.mjs.map +1 -1
  101. package/fesm2022/ngwr-i18n-de.mjs +7 -0
  102. package/fesm2022/ngwr-i18n-de.mjs.map +1 -1
  103. package/fesm2022/ngwr-i18n-en.mjs +23 -2
  104. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  105. package/fesm2022/ngwr-i18n-es.mjs +7 -0
  106. package/fesm2022/ngwr-i18n-es.mjs.map +1 -1
  107. package/fesm2022/ngwr-i18n-fr.mjs +7 -0
  108. package/fesm2022/ngwr-i18n-fr.mjs.map +1 -1
  109. package/fesm2022/ngwr-i18n-he.mjs +7 -0
  110. package/fesm2022/ngwr-i18n-he.mjs.map +1 -1
  111. package/fesm2022/ngwr-i18n-hi.mjs +7 -0
  112. package/fesm2022/ngwr-i18n-hi.mjs.map +1 -1
  113. package/fesm2022/ngwr-i18n-id.mjs +7 -0
  114. package/fesm2022/ngwr-i18n-id.mjs.map +1 -1
  115. package/fesm2022/ngwr-i18n-it.mjs +7 -0
  116. package/fesm2022/ngwr-i18n-it.mjs.map +1 -1
  117. package/fesm2022/ngwr-i18n-ja.mjs +7 -0
  118. package/fesm2022/ngwr-i18n-ja.mjs.map +1 -1
  119. package/fesm2022/ngwr-i18n-ko.mjs +7 -0
  120. package/fesm2022/ngwr-i18n-ko.mjs.map +1 -1
  121. package/fesm2022/ngwr-i18n-nl.mjs +7 -0
  122. package/fesm2022/ngwr-i18n-nl.mjs.map +1 -1
  123. package/fesm2022/ngwr-i18n-pl.mjs +7 -0
  124. package/fesm2022/ngwr-i18n-pl.mjs.map +1 -1
  125. package/fesm2022/ngwr-i18n-pt.mjs +7 -0
  126. package/fesm2022/ngwr-i18n-pt.mjs.map +1 -1
  127. package/fesm2022/ngwr-i18n-ru.mjs +7 -0
  128. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  129. package/fesm2022/ngwr-i18n-sv.mjs +7 -0
  130. package/fesm2022/ngwr-i18n-sv.mjs.map +1 -1
  131. package/fesm2022/ngwr-i18n-tr.mjs +7 -0
  132. package/fesm2022/ngwr-i18n-tr.mjs.map +1 -1
  133. package/fesm2022/ngwr-i18n-uk.mjs +7 -0
  134. package/fesm2022/ngwr-i18n-uk.mjs.map +1 -1
  135. package/fesm2022/ngwr-i18n-vi.mjs +7 -0
  136. package/fesm2022/ngwr-i18n-vi.mjs.map +1 -1
  137. package/fesm2022/ngwr-i18n-zh-TW.mjs +7 -0
  138. package/fesm2022/ngwr-i18n-zh-TW.mjs.map +1 -1
  139. package/fesm2022/ngwr-i18n-zh.mjs +7 -0
  140. package/fesm2022/ngwr-i18n-zh.mjs.map +1 -1
  141. package/fesm2022/ngwr-i18n.mjs +9 -9
  142. package/fesm2022/ngwr-icon.mjs +6 -6
  143. package/fesm2022/ngwr-image-cropper.mjs +3 -3
  144. package/fesm2022/ngwr-input-number.mjs +15 -6
  145. package/fesm2022/ngwr-input-number.mjs.map +1 -1
  146. package/fesm2022/ngwr-input-otp.mjs +38 -6
  147. package/fesm2022/ngwr-input-otp.mjs.map +1 -1
  148. package/fesm2022/ngwr-input.mjs +15 -15
  149. package/fesm2022/ngwr-keyboard.mjs +3 -3
  150. package/fesm2022/ngwr-knob-testing.mjs +11 -3
  151. package/fesm2022/ngwr-knob-testing.mjs.map +1 -1
  152. package/fesm2022/ngwr-knob.mjs +19 -4
  153. package/fesm2022/ngwr-knob.mjs.map +1 -1
  154. package/fesm2022/ngwr-layout.mjs +15 -15
  155. package/fesm2022/ngwr-lightbox.mjs +27 -6
  156. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  157. package/fesm2022/ngwr-line-chart-testing.mjs +18 -2
  158. package/fesm2022/ngwr-line-chart-testing.mjs.map +1 -1
  159. package/fesm2022/ngwr-line-chart.mjs +22 -4
  160. package/fesm2022/ngwr-line-chart.mjs.map +1 -1
  161. package/fesm2022/ngwr-list.mjs +7 -7
  162. package/fesm2022/ngwr-list.mjs.map +1 -1
  163. package/fesm2022/ngwr-loading-bar.mjs +6 -6
  164. package/fesm2022/ngwr-markdown.mjs +6 -6
  165. package/fesm2022/ngwr-marquee.mjs +3 -3
  166. package/fesm2022/ngwr-media.mjs +3 -3
  167. package/fesm2022/ngwr-mention.mjs +24 -8
  168. package/fesm2022/ngwr-mention.mjs.map +1 -1
  169. package/fesm2022/ngwr-meta.mjs +6 -6
  170. package/fesm2022/ngwr-meter-group.mjs +3 -3
  171. package/fesm2022/ngwr-overlay.mjs +125 -10
  172. package/fesm2022/ngwr-overlay.mjs.map +1 -1
  173. package/fesm2022/ngwr-page-header.mjs +3 -3
  174. package/fesm2022/ngwr-pagination.mjs +33 -6
  175. package/fesm2022/ngwr-pagination.mjs.map +1 -1
  176. package/fesm2022/ngwr-pipes.mjs +21 -21
  177. package/fesm2022/ngwr-platform.mjs +9 -9
  178. package/fesm2022/ngwr-popconfirm.mjs +25 -9
  179. package/fesm2022/ngwr-popconfirm.mjs.map +1 -1
  180. package/fesm2022/ngwr-popover-testing.mjs +22 -5
  181. package/fesm2022/ngwr-popover-testing.mjs.map +1 -1
  182. package/fesm2022/ngwr-popover.mjs +243 -37
  183. package/fesm2022/ngwr-popover.mjs.map +1 -1
  184. package/fesm2022/ngwr-progress.mjs +3 -3
  185. package/fesm2022/ngwr-pull-to-refresh.mjs +3 -3
  186. package/fesm2022/ngwr-qr.mjs +3 -3
  187. package/fesm2022/ngwr-radio.mjs +6 -6
  188. package/fesm2022/ngwr-rating-testing.mjs +12 -3
  189. package/fesm2022/ngwr-rating-testing.mjs.map +1 -1
  190. package/fesm2022/ngwr-rating.mjs +19 -4
  191. package/fesm2022/ngwr-rating.mjs.map +1 -1
  192. package/fesm2022/ngwr-result.mjs +13 -13
  193. package/fesm2022/ngwr-result.mjs.map +1 -1
  194. package/fesm2022/ngwr-rotating-text.mjs +3 -3
  195. package/fesm2022/ngwr-schema-form.mjs +3 -3
  196. package/fesm2022/ngwr-scroll.mjs +3 -3
  197. package/fesm2022/ngwr-segmented.mjs +37 -4
  198. package/fesm2022/ngwr-segmented.mjs.map +1 -1
  199. package/fesm2022/ngwr-select.mjs +24 -10
  200. package/fesm2022/ngwr-select.mjs.map +1 -1
  201. package/fesm2022/ngwr-shiny-text.mjs +6 -6
  202. package/fesm2022/ngwr-sidebar.mjs +3 -3
  203. package/fesm2022/ngwr-skeleton.mjs +3 -3
  204. package/fesm2022/ngwr-slider.mjs +18 -5
  205. package/fesm2022/ngwr-slider.mjs.map +1 -1
  206. package/fesm2022/ngwr-sparkline.mjs +3 -3
  207. package/fesm2022/ngwr-speed-dial.mjs +3 -3
  208. package/fesm2022/ngwr-spinner.mjs +3 -3
  209. package/fesm2022/ngwr-splash-cursor.mjs +3 -3
  210. package/fesm2022/ngwr-split-text.mjs +3 -3
  211. package/fesm2022/ngwr-splitter.mjs +3 -3
  212. package/fesm2022/ngwr-spotlight-card.mjs +6 -6
  213. package/fesm2022/ngwr-squircle.mjs +28 -9
  214. package/fesm2022/ngwr-squircle.mjs.map +1 -1
  215. package/fesm2022/ngwr-star-border.mjs +3 -3
  216. package/fesm2022/ngwr-statistic.mjs +20 -13
  217. package/fesm2022/ngwr-statistic.mjs.map +1 -1
  218. package/fesm2022/ngwr-stepper.mjs +6 -6
  219. package/fesm2022/ngwr-storage.mjs +3 -3
  220. package/fesm2022/ngwr-switch.mjs +3 -3
  221. package/fesm2022/ngwr-table.mjs +18 -18
  222. package/fesm2022/ngwr-tabs-router.mjs +3 -3
  223. package/fesm2022/ngwr-tabs.mjs +6 -6
  224. package/fesm2022/ngwr-textarea.mjs +3 -3
  225. package/fesm2022/ngwr-theme.mjs +3 -3
  226. package/fesm2022/ngwr-tilt-card.mjs +6 -6
  227. package/fesm2022/ngwr-timeline.mjs +6 -6
  228. package/fesm2022/ngwr-toast.mjs +29 -12
  229. package/fesm2022/ngwr-toast.mjs.map +1 -1
  230. package/fesm2022/ngwr-toolbar.mjs +3 -3
  231. package/fesm2022/ngwr-tour.mjs +26 -12
  232. package/fesm2022/ngwr-tour.mjs.map +1 -1
  233. package/fesm2022/ngwr-transfer.mjs +88 -11
  234. package/fesm2022/ngwr-transfer.mjs.map +1 -1
  235. package/fesm2022/ngwr-tree-testing.mjs +24 -0
  236. package/fesm2022/ngwr-tree-testing.mjs.map +1 -1
  237. package/fesm2022/ngwr-tree.mjs +81 -9
  238. package/fesm2022/ngwr-tree.mjs.map +1 -1
  239. package/fesm2022/ngwr-typewriter.mjs +3 -3
  240. package/fesm2022/ngwr-typography.mjs +3 -3
  241. package/fesm2022/ngwr-version.mjs +1 -1
  242. package/fesm2022/ngwr-version.mjs.map +1 -1
  243. package/fesm2022/ngwr-virtual-scroll.mjs +3 -3
  244. package/fesm2022/ngwr-waves.mjs +3 -3
  245. package/fesm2022/ngwr-window.mjs +30 -13
  246. package/fesm2022/ngwr-window.mjs.map +1 -1
  247. package/form/styles/_index.scss +63 -0
  248. package/graph/styles/_index.scss +113 -0
  249. package/i18n/ar.json +9 -2
  250. package/i18n/cs.json +9 -2
  251. package/i18n/de.json +9 -2
  252. package/i18n/en.json +11 -4
  253. package/i18n/es.json +9 -2
  254. package/i18n/fr.json +9 -2
  255. package/i18n/he.json +9 -2
  256. package/i18n/hi.json +9 -2
  257. package/i18n/id.json +9 -2
  258. package/i18n/it.json +9 -2
  259. package/i18n/ja.json +9 -2
  260. package/i18n/ko.json +9 -2
  261. package/i18n/nl.json +9 -2
  262. package/i18n/pl.json +9 -2
  263. package/i18n/pt.json +9 -2
  264. package/i18n/ru.json +9 -2
  265. package/i18n/sv.json +9 -2
  266. package/i18n/tr.json +9 -2
  267. package/i18n/uk.json +9 -2
  268. package/i18n/vi.json +9 -2
  269. package/i18n/zh-TW.json +9 -2
  270. package/i18n/zh.json +9 -2
  271. package/input-otp/styles/_index.scss +5 -1
  272. package/line-chart/styles/_index.scss +5 -3
  273. package/llms-full.txt +13 -3
  274. package/llms.txt +8 -6
  275. package/markdown/styles/_index.scss +4 -1
  276. package/mention/styles/_index.scss +22 -1
  277. package/package.json +10 -1
  278. package/radio/styles/_index.scss +3 -0
  279. package/result/styles/_index.scss +12 -0
  280. package/schematics/migrations/v14/index.js +6 -3
  281. package/schematics/migrations.json +1 -1
  282. package/schematics/ng-add/index.js +34 -5
  283. package/schematics/use/symbol-map.json +2 -0
  284. package/segmented/styles/_index.scss +12 -1
  285. package/select/styles/_index.scss +20 -1
  286. package/skills/ngwr/SKILL.md +1 -1
  287. package/skills/ngwr/references/catalog.md +2 -0
  288. package/statistic/styles/_index.scss +4 -0
  289. package/stepper/styles/_index.scss +36 -0
  290. package/styles.scss +1 -0
  291. package/switch/styles/_index.scss +3 -0
  292. package/table/styles/_index.scss +12 -0
  293. package/tabs/styles/_index.scss +4 -1
  294. package/textarea/styles/_index.scss +6 -1
  295. package/theme/styles/_colors.scss +11 -0
  296. package/theme/styles/_contrast.scss +136 -0
  297. package/theme/styles/_focus.scss +7 -0
  298. package/theme/styles/_index.scss +1 -0
  299. package/theme/styles/_mixins.scss +15 -8
  300. package/timeline/styles/_index.scss +28 -1
  301. package/toast/styles/_index.scss +11 -0
  302. package/tree/styles/_index.scss +27 -1
  303. package/types/ngwr-badge.d.ts +1 -1
  304. package/types/ngwr-bar-chart-testing.d.ts +11 -1
  305. package/types/ngwr-button.d.ts +1 -1
  306. package/types/ngwr-calendar-heatmap-testing.d.ts +10 -5
  307. package/types/ngwr-calendar-heatmap.d.ts +22 -0
  308. package/types/ngwr-calendar.d.ts +21 -0
  309. package/types/ngwr-cascader-testing.d.ts +74 -17
  310. package/types/ngwr-cascader.d.ts +35 -0
  311. package/types/ngwr-checkbox-testing.d.ts +12 -1
  312. package/types/ngwr-checkbox.d.ts +10 -0
  313. package/types/ngwr-clipboard.d.ts +1 -1
  314. package/types/ngwr-color-picker.d.ts +8 -0
  315. package/types/ngwr-confetti.d.ts +1 -1
  316. package/types/ngwr-context-menu.d.ts +10 -0
  317. package/types/ngwr-cookie.d.ts +1 -1
  318. package/types/ngwr-date-adapters-fns.d.ts +1 -1
  319. package/types/ngwr-date-adapters-luxon.d.ts +1 -1
  320. package/types/ngwr-date-picker.d.ts +16 -0
  321. package/types/ngwr-date.d.ts +9 -3
  322. package/types/ngwr-density.d.ts +1 -1
  323. package/types/ngwr-dialog.d.ts +2 -1
  324. package/types/ngwr-donut-chart-testing.d.ts +11 -1
  325. package/types/ngwr-drawer.d.ts +29 -1
  326. package/types/ngwr-dropdown.d.ts +25 -0
  327. package/types/ngwr-file-upload.d.ts +11 -0
  328. package/types/ngwr-form.d.ts +25 -0
  329. package/types/ngwr-gauge-testing.d.ts +9 -1
  330. package/types/ngwr-graph-testing.d.ts +190 -0
  331. package/types/ngwr-graph.d.ts +203 -0
  332. package/types/ngwr-hotkey.d.ts +1 -1
  333. package/types/ngwr-i18n.d.ts +1 -1
  334. package/types/ngwr-input-number.d.ts +12 -2
  335. package/types/ngwr-input-otp.d.ts +12 -0
  336. package/types/ngwr-knob-testing.d.ts +11 -3
  337. package/types/ngwr-knob.d.ts +13 -0
  338. package/types/ngwr-lightbox.d.ts +15 -0
  339. package/types/ngwr-line-chart-testing.d.ts +11 -1
  340. package/types/ngwr-line-chart.d.ts +18 -0
  341. package/types/ngwr-loading-bar.d.ts +1 -1
  342. package/types/ngwr-markdown.d.ts +1 -1
  343. package/types/ngwr-media.d.ts +1 -1
  344. package/types/ngwr-mention.d.ts +7 -0
  345. package/types/ngwr-meta.d.ts +1 -1
  346. package/types/ngwr-overlay.d.ts +64 -6
  347. package/types/ngwr-pagination.d.ts +16 -0
  348. package/types/ngwr-platform.d.ts +3 -3
  349. package/types/ngwr-popconfirm.d.ts +1 -0
  350. package/types/ngwr-popover-testing.d.ts +22 -5
  351. package/types/ngwr-popover.d.ts +44 -8
  352. package/types/ngwr-rating-testing.d.ts +12 -3
  353. package/types/ngwr-rating.d.ts +13 -0
  354. package/types/ngwr-scroll.d.ts +1 -1
  355. package/types/ngwr-segmented.d.ts +16 -0
  356. package/types/ngwr-select.d.ts +7 -0
  357. package/types/ngwr-squircle.d.ts +22 -3
  358. package/types/ngwr-statistic.d.ts +0 -1
  359. package/types/ngwr-storage.d.ts +1 -1
  360. package/types/ngwr-theme.d.ts +1 -1
  361. package/types/ngwr-toast.d.ts +3 -1
  362. package/types/ngwr-tour.d.ts +7 -1
  363. package/types/ngwr-transfer.d.ts +52 -0
  364. package/types/ngwr-tree-testing.d.ts +20 -0
  365. package/types/ngwr-tree.d.ts +43 -1
  366. package/types/ngwr-version.d.ts +1 -1
  367. package/types/ngwr-window.d.ts +2 -1
  368. package/typography/styles/_index.scss +26 -4
@@ -1,11 +1,12 @@
1
1
  import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
2
+ import { Directionality } from '@angular/cdk/bidi';
2
3
  import { coerceBooleanProperty } from '@angular/cdk/coercion';
3
4
  import { TemplatePortal } from '@angular/cdk/portal';
4
5
  import * as i0 from '@angular/core';
5
- import { input, computed, signal, viewChild, inject, ViewContainerRef, DestroyRef, effect, ViewEncapsulation, Component } from '@angular/core';
6
+ import { input, computed, signal, viewChild, inject, ViewContainerRef, DestroyRef, Injector, EnvironmentInjector, effect, ViewEncapsulation, Component } from '@angular/core';
6
7
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
8
  import { useI18nText } from 'ngwr/i18n';
8
- import { WR_OVERLAY } from 'ngwr/overlay';
9
+ import { WR_OVERLAY, wrFollowDirection } from 'ngwr/overlay';
9
10
 
10
11
  /**
11
12
  * @license
@@ -106,7 +107,22 @@ class WrLightbox {
106
107
  overlay = inject(WR_OVERLAY);
107
108
  vcr = inject(ViewContainerRef);
108
109
  destroyRef = inject(DestroyRef);
110
+ injector = inject(Injector);
109
111
  focusTrapFactory = inject(ConfigurableFocusTrapFactory);
112
+ /**
113
+ * The reading direction the open viewer has to keep up with, resolved off the
114
+ * ENVIRONMENT injector rather than this component's — the same instance the
115
+ * CDK read when it stamped the overlay, since `createOverlayRef` takes
116
+ * `Directionality` from the `Overlay`'s injector and no node-level `[dir]` is
117
+ * in that chain. The viewer is appended to the overlay container on `<body>`,
118
+ * not into whatever region the thumbnail sits in, so reading the node
119
+ * injector would rewrite a correctly-stamped panel to an island's direction
120
+ * one pass after it opened rather than follow the application at all.
121
+ *
122
+ * `null` never happens in an app — `Directionality` is root-provided — but a
123
+ * bare `TestBed` should still get a viewer that opens.
124
+ */
125
+ dir = inject(EnvironmentInjector).get(Directionality, null);
110
126
  focusTrap = null;
111
127
  previouslyFocused = null;
112
128
  overlayRef = null;
@@ -194,6 +210,11 @@ class WrLightbox {
194
210
  scrollStrategy: this.overlay.scrollStrategies.block(),
195
211
  panelClass: 'wr-lightbox-overlay',
196
212
  });
213
+ // Centred on both axes, so no geometry rides on the direction — but the
214
+ // viewer's own chrome does. The ✕ is placed with `inset-inline-end` and the
215
+ // caption reads as prose, both against the `dir` the CDK froze at create, so
216
+ // a flip while the viewer is open leaves the button on the wrong corner.
217
+ wrFollowDirection(this.overlayRef, this.dir, this.injector);
197
218
  const portal = new TemplatePortal(this.panelTpl(), this.vcr);
198
219
  this.overlayRef.attach(portal);
199
220
  // The viewer declares role="dialog" + aria-modal="true", which promises the
@@ -231,15 +252,15 @@ class WrLightbox {
231
252
  if (restore && typeof restore.focus === 'function')
232
253
  restore.focus();
233
254
  }
234
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: WrLightbox, deps: [], target: i0.ɵɵFactoryTarget.Component });
235
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: WrLightbox, isStandalone: true, selector: "wr-lightbox", inputs: { openLabel: { classPropertyName: "openLabel", publicName: "openLabel", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: true, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, preview: { classPropertyName: "preview", publicName: "preview", isSignal: true, isRequired: false, transformFunction: null }, disablePreview: { classPropertyName: "disablePreview", publicName: "disablePreview", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "style.aspect-ratio": "aspectRatio()" } }, viewQueries: [{ propertyName: "panelTpl", first: true, predicate: ["viewerTpl"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (disablePreview()) {\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n} @else {\n <!-- The trigger opens a modal dialog, and used to say so nowhere: no hint that it\n opens anything, and nothing on the page tying it to the viewer it opened. -->\n <button\n type=\"button\"\n class=\"wr-lightbox__trigger\"\n aria-haspopup=\"dialog\"\n [attr.aria-controls]=\"open() ? viewerId : null\"\n [attr.aria-label]=\"resolvedOpenLabel()\"\n (click)=\"onClick()\"\n >\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n </button>\n}\n\n<ng-template #viewerTpl>\n <div\n #viewer\n class=\"wr-lightbox-viewer\"\n [id]=\"viewerId\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"resolvedViewerLabel()\"\n (touchstart)=\"onViewerSwipeStart($event)\"\n (touchmove)=\"onViewerSwipeMove($event, viewer)\"\n (touchend)=\"onViewerSwipeEnd($event, viewer)\"\n (touchcancel)=\"onViewerSwipeEnd($event, viewer)\"\n >\n <button\n type=\"button\"\n class=\"wr-lightbox-viewer__close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"onClose()\"\n (keydown.enter)=\"onClose()\"\n (keydown.space)=\"onClose()\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </button>\n <!--\n Styled `cursor: zoom-out`, which is a promise: clicking the image closes. A\n REDUNDANT mouse affordance \u2014 the close button, Escape, the backdrop and\n swipe-down all do the same thing and all reach a keyboard \u2014 so the image stays\n out of the tab order rather than becoming a second close stop.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <img class=\"wr-lightbox-viewer__full\" [src]=\"src()\" [alt]=\"alt()\" (click)=\"onClose()\" />\n @if (caption()) {\n <div class=\"wr-lightbox-viewer__caption\">{{ caption() }}</div>\n }\n </div>\n</ng-template>\n", styles: [".wr-btn:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-table__sort-btn:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-tabs__tab:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-sidebar__entry:focus-visible,.wr-sidebar__group-toggle:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-sidebar__item:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-anchor__link:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-collapse__header:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-command-palette__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-mention-panel__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-segmented__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-back-top:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-carousel__nav:focus-visible,.wr-carousel__dot:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-speed-dial__trigger:focus-visible,.wr-speed-dial__action:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-stepper__header:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-window__chrome-action:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-date-picker__trigger:focus-visible,.wr-time-picker__step:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-dropdown-item:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-alert__close:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-lightbox{display:inline-block;position:relative;overflow:hidden;border-radius:var(--wr-lightbox-radius, var(--wr-border-radius-base))}@supports (corner-shape: squircle){.wr-lightbox{corner-shape:squircle;border-radius:calc(var(--wr-lightbox-radius, var(--wr-border-radius-base)) * 1.85)}}.wr-lightbox{background:var(--wr-color-hover)}.wr-lightbox--loading{min-height:var(--wr-lightbox-loading-min-height, 4rem)}.wr-lightbox--loading:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,rgba(var(--wr-color-surface-rgb),0),rgba(var(--wr-color-surface-rgb),.45),rgba(var(--wr-color-surface-rgb),0));background-size:200% 100%;animation:wr-lightbox-shimmer 1.4s linear infinite;pointer-events:none;z-index:1}.wr-lightbox__trigger{display:block;width:100%;height:100%;padding:0;margin:0;border:0;background:transparent;cursor:zoom-in}.wr-lightbox__trigger:focus-visible{outline:none}.wr-lightbox__thumb{display:block;width:100%;height:100%;object-fit:cover;-webkit-user-select:none;user-select:none;-webkit-user-drag:none;position:relative;z-index:2;opacity:1;transition:opacity .25s ease}.wr-lightbox--loading .wr-lightbox__thumb{opacity:0}.wr-lightbox:has(.wr-lightbox__trigger:focus-visible){outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}@keyframes wr-lightbox-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.wr-lightbox-backdrop{background:rgba(var(--wr-color-backdrop-rgb),.85)}.wr-lightbox-overlay{pointer-events:auto}.wr-lightbox-viewer{display:flex;flex-direction:column;align-items:center;gap:1rem;max-width:95vw;max-height:95vh;max-height:95dvh;padding:1rem;animation:wr-lightbox-viewer-in .18s ease;outline:none;touch-action:none}.wr-lightbox-viewer__close{--wr-icon-size: 1.25rem;position:absolute;top:1rem;inset-inline-end:1rem;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;padding:0;border:0;border-radius:999px;background:rgba(var(--wr-color-surface-rgb),.12);color:rgba(var(--wr-color-surface-rgb),.9);cursor:pointer;transition:background .15s ease,transform .15s ease}@media(pointer:coarse){.wr-lightbox-viewer__close:after{content:\"\";position:absolute;top:50%;left:50%;width:100%;height:100%;min-width:44px;min-height:44px;transform:translate(-50%,-50%)}}.wr-lightbox-viewer__close:hover{background:rgba(var(--wr-color-surface-rgb),.2)}.wr-lightbox-viewer__close:active{transform:scale(.95)}.wr-lightbox-viewer__close:focus-visible{outline:2px solid rgba(var(--wr-color-surface-rgb),.9);outline-offset:2px}.wr-lightbox-viewer__full{display:block;max-width:100%;max-height:calc(95vh - 4rem);max-height:calc(95dvh - 4rem);object-fit:contain;cursor:zoom-out;border-radius:.5rem}@supports (corner-shape: squircle){.wr-lightbox-viewer__full{corner-shape:squircle;border-radius:.925rem}}.wr-lightbox-viewer__caption{color:rgba(var(--wr-color-surface-rgb),.85);font-family:var(--wr-font-family-base);font-size:var(--wr-text-sm);text-align:center;max-width:60ch}@keyframes wr-lightbox-viewer-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}\n"], encapsulation: i0.ViewEncapsulation.None });
255
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrLightbox, deps: [], target: i0.ɵɵFactoryTarget.Component });
256
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrLightbox, isStandalone: true, selector: "wr-lightbox", inputs: { openLabel: { classPropertyName: "openLabel", publicName: "openLabel", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: true, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, preview: { classPropertyName: "preview", publicName: "preview", isSignal: true, isRequired: false, transformFunction: null }, disablePreview: { classPropertyName: "disablePreview", publicName: "disablePreview", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "style.aspect-ratio": "aspectRatio()" } }, viewQueries: [{ propertyName: "panelTpl", first: true, predicate: ["viewerTpl"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (disablePreview()) {\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n} @else {\n <!-- The trigger opens a modal dialog, and used to say so nowhere: no hint that it\n opens anything, and nothing on the page tying it to the viewer it opened. -->\n <button\n type=\"button\"\n class=\"wr-lightbox__trigger\"\n aria-haspopup=\"dialog\"\n [attr.aria-controls]=\"open() ? viewerId : null\"\n [attr.aria-label]=\"resolvedOpenLabel()\"\n (click)=\"onClick()\"\n >\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n </button>\n}\n\n<ng-template #viewerTpl>\n <div\n #viewer\n class=\"wr-lightbox-viewer\"\n [id]=\"viewerId\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"resolvedViewerLabel()\"\n (touchstart)=\"onViewerSwipeStart($event)\"\n (touchmove)=\"onViewerSwipeMove($event, viewer)\"\n (touchend)=\"onViewerSwipeEnd($event, viewer)\"\n (touchcancel)=\"onViewerSwipeEnd($event, viewer)\"\n >\n <button\n type=\"button\"\n class=\"wr-lightbox-viewer__close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"onClose()\"\n (keydown.enter)=\"onClose()\"\n (keydown.space)=\"onClose()\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </button>\n <!--\n Styled `cursor: zoom-out`, which is a promise: clicking the image closes. A\n REDUNDANT mouse affordance \u2014 the close button, Escape, the backdrop and\n swipe-down all do the same thing and all reach a keyboard \u2014 so the image stays\n out of the tab order rather than becoming a second close stop.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <img class=\"wr-lightbox-viewer__full\" [src]=\"src()\" [alt]=\"alt()\" (click)=\"onClose()\" />\n @if (caption()) {\n <div class=\"wr-lightbox-viewer__caption\">{{ caption() }}</div>\n }\n </div>\n</ng-template>\n", styles: [".wr-btn:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-table__sort-btn:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-tabs__tab:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-sidebar__entry:focus-visible,.wr-sidebar__group-toggle:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-sidebar__item:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-anchor__link:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-collapse__header:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-command-palette__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-mention-panel__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-segmented__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-back-top:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-carousel__nav:focus-visible,.wr-carousel__dot:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-speed-dial__trigger:focus-visible,.wr-speed-dial__action:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-stepper__header:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-window__chrome-action:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-date-picker__trigger:focus-visible,.wr-time-picker__step:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-dropdown-item:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-alert__close:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-graph__viewport:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-lightbox{display:inline-block;position:relative;overflow:hidden;border-radius:var(--wr-lightbox-radius, var(--wr-border-radius-base))}@supports (corner-shape: squircle){.wr-lightbox{corner-shape:squircle;border-radius:calc(var(--wr-lightbox-radius, var(--wr-border-radius-base)) * 1.85)}}.wr-lightbox{background:var(--wr-color-hover)}.wr-lightbox--loading{min-height:var(--wr-lightbox-loading-min-height, 4rem)}.wr-lightbox--loading:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,rgba(var(--wr-color-surface-rgb),0),rgba(var(--wr-color-surface-rgb),.45),rgba(var(--wr-color-surface-rgb),0));background-size:200% 100%;animation:wr-lightbox-shimmer 1.4s linear infinite;pointer-events:none;z-index:1}.wr-lightbox__trigger{display:block;width:100%;height:100%;padding:0;margin:0;border:0;background:transparent;cursor:zoom-in}.wr-lightbox__trigger:focus-visible{outline:none}.wr-lightbox__thumb{display:block;width:100%;height:100%;object-fit:cover;-webkit-user-select:none;user-select:none;-webkit-user-drag:none;position:relative;z-index:2;opacity:1;transition:opacity .25s ease}.wr-lightbox--loading .wr-lightbox__thumb{opacity:0}.wr-lightbox:has(.wr-lightbox__trigger:focus-visible){outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}@keyframes wr-lightbox-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.wr-lightbox-backdrop{background:rgba(var(--wr-color-backdrop-rgb),.85)}.wr-lightbox-overlay{pointer-events:auto}.wr-lightbox-viewer{display:flex;flex-direction:column;align-items:center;gap:1rem;max-width:95vw;max-height:95vh;max-height:95dvh;padding:1rem;animation:wr-lightbox-viewer-in .18s ease;outline:none;touch-action:none}.wr-lightbox-viewer__close{--wr-icon-size: 1.25rem;position:absolute;top:1rem;inset-inline-end:1rem;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;padding:0;border:0;border-radius:999px;background:rgba(var(--wr-color-surface-rgb),.12);color:rgba(var(--wr-color-surface-rgb),.9);cursor:pointer;transition:background .15s ease,transform .15s ease}@media(pointer:coarse){.wr-lightbox-viewer__close:after{content:\"\";position:absolute;top:50%;left:50%;width:100%;height:100%;min-width:44px;min-height:44px;transform:translate(-50%,-50%)}}.wr-lightbox-viewer__close:hover{background:rgba(var(--wr-color-surface-rgb),.2)}.wr-lightbox-viewer__close:active{transform:scale(.95)}.wr-lightbox-viewer__close:focus-visible{outline:2px solid rgba(var(--wr-color-surface-rgb),.9);outline-offset:2px}.wr-lightbox-viewer__full{display:block;max-width:100%;max-height:calc(95vh - 4rem);max-height:calc(95dvh - 4rem);object-fit:contain;cursor:zoom-out;border-radius:.5rem}@supports (corner-shape: squircle){.wr-lightbox-viewer__full{corner-shape:squircle;border-radius:.925rem}}.wr-lightbox-viewer__caption{color:rgba(var(--wr-color-surface-rgb),.85);font-family:var(--wr-font-family-base);font-size:var(--wr-text-sm);text-align:center;max-width:60ch}@keyframes wr-lightbox-viewer-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}\n"], encapsulation: i0.ViewEncapsulation.None });
236
257
  }
237
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: WrLightbox, decorators: [{
258
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrLightbox, decorators: [{
238
259
  type: Component,
239
260
  args: [{ selector: 'wr-lightbox', encapsulation: ViewEncapsulation.None, host: {
240
261
  '[class]': 'classes()',
241
262
  '[style.aspect-ratio]': 'aspectRatio()',
242
- }, template: "@if (disablePreview()) {\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n} @else {\n <!-- The trigger opens a modal dialog, and used to say so nowhere: no hint that it\n opens anything, and nothing on the page tying it to the viewer it opened. -->\n <button\n type=\"button\"\n class=\"wr-lightbox__trigger\"\n aria-haspopup=\"dialog\"\n [attr.aria-controls]=\"open() ? viewerId : null\"\n [attr.aria-label]=\"resolvedOpenLabel()\"\n (click)=\"onClick()\"\n >\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n </button>\n}\n\n<ng-template #viewerTpl>\n <div\n #viewer\n class=\"wr-lightbox-viewer\"\n [id]=\"viewerId\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"resolvedViewerLabel()\"\n (touchstart)=\"onViewerSwipeStart($event)\"\n (touchmove)=\"onViewerSwipeMove($event, viewer)\"\n (touchend)=\"onViewerSwipeEnd($event, viewer)\"\n (touchcancel)=\"onViewerSwipeEnd($event, viewer)\"\n >\n <button\n type=\"button\"\n class=\"wr-lightbox-viewer__close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"onClose()\"\n (keydown.enter)=\"onClose()\"\n (keydown.space)=\"onClose()\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </button>\n <!--\n Styled `cursor: zoom-out`, which is a promise: clicking the image closes. A\n REDUNDANT mouse affordance \u2014 the close button, Escape, the backdrop and\n swipe-down all do the same thing and all reach a keyboard \u2014 so the image stays\n out of the tab order rather than becoming a second close stop.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <img class=\"wr-lightbox-viewer__full\" [src]=\"src()\" [alt]=\"alt()\" (click)=\"onClose()\" />\n @if (caption()) {\n <div class=\"wr-lightbox-viewer__caption\">{{ caption() }}</div>\n }\n </div>\n</ng-template>\n", styles: [".wr-btn:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-table__sort-btn:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-tabs__tab:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-sidebar__entry:focus-visible,.wr-sidebar__group-toggle:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-sidebar__item:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-anchor__link:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-collapse__header:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-command-palette__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-mention-panel__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-segmented__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-back-top:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-carousel__nav:focus-visible,.wr-carousel__dot:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-speed-dial__trigger:focus-visible,.wr-speed-dial__action:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-stepper__header:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-window__chrome-action:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-date-picker__trigger:focus-visible,.wr-time-picker__step:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-dropdown-item:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-alert__close:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-lightbox{display:inline-block;position:relative;overflow:hidden;border-radius:var(--wr-lightbox-radius, var(--wr-border-radius-base))}@supports (corner-shape: squircle){.wr-lightbox{corner-shape:squircle;border-radius:calc(var(--wr-lightbox-radius, var(--wr-border-radius-base)) * 1.85)}}.wr-lightbox{background:var(--wr-color-hover)}.wr-lightbox--loading{min-height:var(--wr-lightbox-loading-min-height, 4rem)}.wr-lightbox--loading:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,rgba(var(--wr-color-surface-rgb),0),rgba(var(--wr-color-surface-rgb),.45),rgba(var(--wr-color-surface-rgb),0));background-size:200% 100%;animation:wr-lightbox-shimmer 1.4s linear infinite;pointer-events:none;z-index:1}.wr-lightbox__trigger{display:block;width:100%;height:100%;padding:0;margin:0;border:0;background:transparent;cursor:zoom-in}.wr-lightbox__trigger:focus-visible{outline:none}.wr-lightbox__thumb{display:block;width:100%;height:100%;object-fit:cover;-webkit-user-select:none;user-select:none;-webkit-user-drag:none;position:relative;z-index:2;opacity:1;transition:opacity .25s ease}.wr-lightbox--loading .wr-lightbox__thumb{opacity:0}.wr-lightbox:has(.wr-lightbox__trigger:focus-visible){outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}@keyframes wr-lightbox-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.wr-lightbox-backdrop{background:rgba(var(--wr-color-backdrop-rgb),.85)}.wr-lightbox-overlay{pointer-events:auto}.wr-lightbox-viewer{display:flex;flex-direction:column;align-items:center;gap:1rem;max-width:95vw;max-height:95vh;max-height:95dvh;padding:1rem;animation:wr-lightbox-viewer-in .18s ease;outline:none;touch-action:none}.wr-lightbox-viewer__close{--wr-icon-size: 1.25rem;position:absolute;top:1rem;inset-inline-end:1rem;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;padding:0;border:0;border-radius:999px;background:rgba(var(--wr-color-surface-rgb),.12);color:rgba(var(--wr-color-surface-rgb),.9);cursor:pointer;transition:background .15s ease,transform .15s ease}@media(pointer:coarse){.wr-lightbox-viewer__close:after{content:\"\";position:absolute;top:50%;left:50%;width:100%;height:100%;min-width:44px;min-height:44px;transform:translate(-50%,-50%)}}.wr-lightbox-viewer__close:hover{background:rgba(var(--wr-color-surface-rgb),.2)}.wr-lightbox-viewer__close:active{transform:scale(.95)}.wr-lightbox-viewer__close:focus-visible{outline:2px solid rgba(var(--wr-color-surface-rgb),.9);outline-offset:2px}.wr-lightbox-viewer__full{display:block;max-width:100%;max-height:calc(95vh - 4rem);max-height:calc(95dvh - 4rem);object-fit:contain;cursor:zoom-out;border-radius:.5rem}@supports (corner-shape: squircle){.wr-lightbox-viewer__full{corner-shape:squircle;border-radius:.925rem}}.wr-lightbox-viewer__caption{color:rgba(var(--wr-color-surface-rgb),.85);font-family:var(--wr-font-family-base);font-size:var(--wr-text-sm);text-align:center;max-width:60ch}@keyframes wr-lightbox-viewer-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}\n"] }]
263
+ }, template: "@if (disablePreview()) {\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n} @else {\n <!-- The trigger opens a modal dialog, and used to say so nowhere: no hint that it\n opens anything, and nothing on the page tying it to the viewer it opened. -->\n <button\n type=\"button\"\n class=\"wr-lightbox__trigger\"\n aria-haspopup=\"dialog\"\n [attr.aria-controls]=\"open() ? viewerId : null\"\n [attr.aria-label]=\"resolvedOpenLabel()\"\n (click)=\"onClick()\"\n >\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n </button>\n}\n\n<ng-template #viewerTpl>\n <div\n #viewer\n class=\"wr-lightbox-viewer\"\n [id]=\"viewerId\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"resolvedViewerLabel()\"\n (touchstart)=\"onViewerSwipeStart($event)\"\n (touchmove)=\"onViewerSwipeMove($event, viewer)\"\n (touchend)=\"onViewerSwipeEnd($event, viewer)\"\n (touchcancel)=\"onViewerSwipeEnd($event, viewer)\"\n >\n <button\n type=\"button\"\n class=\"wr-lightbox-viewer__close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"onClose()\"\n (keydown.enter)=\"onClose()\"\n (keydown.space)=\"onClose()\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </button>\n <!--\n Styled `cursor: zoom-out`, which is a promise: clicking the image closes. A\n REDUNDANT mouse affordance \u2014 the close button, Escape, the backdrop and\n swipe-down all do the same thing and all reach a keyboard \u2014 so the image stays\n out of the tab order rather than becoming a second close stop.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <img class=\"wr-lightbox-viewer__full\" [src]=\"src()\" [alt]=\"alt()\" (click)=\"onClose()\" />\n @if (caption()) {\n <div class=\"wr-lightbox-viewer__caption\">{{ caption() }}</div>\n }\n </div>\n</ng-template>\n", styles: [".wr-btn:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-table__sort-btn:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-tabs__tab:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-sidebar__entry:focus-visible,.wr-sidebar__group-toggle:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-sidebar__item:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-anchor__link:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-collapse__header:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo);outline-offset:-2px}.wr-command-palette__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-mention-panel__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-segmented__option:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-back-top:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-carousel__nav:focus-visible,.wr-carousel__dot:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-speed-dial__trigger:focus-visible,.wr-speed-dial__action:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-stepper__header:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-window__chrome-action:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-date-picker__trigger:focus-visible,.wr-time-picker__step:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-dropdown-item:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-alert__close:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-graph__viewport:focus-visible{outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}.wr-lightbox{display:inline-block;position:relative;overflow:hidden;border-radius:var(--wr-lightbox-radius, var(--wr-border-radius-base))}@supports (corner-shape: squircle){.wr-lightbox{corner-shape:squircle;border-radius:calc(var(--wr-lightbox-radius, var(--wr-border-radius-base)) * 1.85)}}.wr-lightbox{background:var(--wr-color-hover)}.wr-lightbox--loading{min-height:var(--wr-lightbox-loading-min-height, 4rem)}.wr-lightbox--loading:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,rgba(var(--wr-color-surface-rgb),0),rgba(var(--wr-color-surface-rgb),.45),rgba(var(--wr-color-surface-rgb),0));background-size:200% 100%;animation:wr-lightbox-shimmer 1.4s linear infinite;pointer-events:none;z-index:1}.wr-lightbox__trigger{display:block;width:100%;height:100%;padding:0;margin:0;border:0;background:transparent;cursor:zoom-in}.wr-lightbox__trigger:focus-visible{outline:none}.wr-lightbox__thumb{display:block;width:100%;height:100%;object-fit:cover;-webkit-user-select:none;user-select:none;-webkit-user-drag:none;position:relative;z-index:2;opacity:1;transition:opacity .25s ease}.wr-lightbox--loading .wr-lightbox__thumb{opacity:0}.wr-lightbox:has(.wr-lightbox__trigger:focus-visible){outline:var(--wr-focus-ring-width) solid var(--wr-focus-ring-color);outline-offset:var(--wr-focus-ring-offset);box-shadow:0 0 0 var(--wr-focus-ring-offset) var(--wr-focus-ring-halo)}@keyframes wr-lightbox-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.wr-lightbox-backdrop{background:rgba(var(--wr-color-backdrop-rgb),.85)}.wr-lightbox-overlay{pointer-events:auto}.wr-lightbox-viewer{display:flex;flex-direction:column;align-items:center;gap:1rem;max-width:95vw;max-height:95vh;max-height:95dvh;padding:1rem;animation:wr-lightbox-viewer-in .18s ease;outline:none;touch-action:none}.wr-lightbox-viewer__close{--wr-icon-size: 1.25rem;position:absolute;top:1rem;inset-inline-end:1rem;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;padding:0;border:0;border-radius:999px;background:rgba(var(--wr-color-surface-rgb),.12);color:rgba(var(--wr-color-surface-rgb),.9);cursor:pointer;transition:background .15s ease,transform .15s ease}@media(pointer:coarse){.wr-lightbox-viewer__close:after{content:\"\";position:absolute;top:50%;left:50%;width:100%;height:100%;min-width:44px;min-height:44px;transform:translate(-50%,-50%)}}.wr-lightbox-viewer__close:hover{background:rgba(var(--wr-color-surface-rgb),.2)}.wr-lightbox-viewer__close:active{transform:scale(.95)}.wr-lightbox-viewer__close:focus-visible{outline:2px solid rgba(var(--wr-color-surface-rgb),.9);outline-offset:2px}.wr-lightbox-viewer__full{display:block;max-width:100%;max-height:calc(95vh - 4rem);max-height:calc(95dvh - 4rem);object-fit:contain;cursor:zoom-out;border-radius:.5rem}@supports (corner-shape: squircle){.wr-lightbox-viewer__full{corner-shape:squircle;border-radius:.925rem}}.wr-lightbox-viewer__caption{color:rgba(var(--wr-color-surface-rgb),.85);font-family:var(--wr-font-family-base);font-size:var(--wr-text-sm);text-align:center;max-width:60ch}@keyframes wr-lightbox-viewer-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}\n"] }]
243
264
  }], ctorParameters: () => [], propDecorators: { openLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "openLabel", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: true }] }], alt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alt", required: false }] }], preview: [{ type: i0.Input, args: [{ isSignal: true, alias: "preview", required: false }] }], disablePreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "disablePreview", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], panelTpl: [{ type: i0.ViewChild, args: ['viewerTpl', { isSignal: true }] }] } });
244
265
 
245
266
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-lightbox.mjs","sources":["../../../projects/lib/lightbox/lightbox.ts","../../../projects/lib/lightbox/lightbox.html","../../../projects/lib/lightbox/ngwr-lightbox.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { type ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';\nimport { coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { type OverlayRef } from '@angular/cdk/overlay';\nimport { TemplatePortal } from '@angular/cdk/portal';\nimport type { TemplateRef } from '@angular/core';\nimport {\n Component,\n DestroyRef,\n ViewContainerRef,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n signal,\n viewChild,\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\n\nimport { useI18nText } from 'ngwr/i18n';\nimport { WR_OVERLAY } from 'ngwr/overlay';\n\nlet viewerUid = 0;\n\n/**\n * Image with click-to-zoom lightbox. The thumbnail is a plain `<img>`\n * with whatever sizing/`object-fit` you give it; clicking opens a CDK\n * overlay showing the full image, with Escape + close button + backdrop\n * to dismiss.\n *\n * @example\n * ```html\n * <wr-lightbox\n * src=\"/photo.jpg\"\n * alt=\"Mountain\"\n * style=\"width: 16rem; height: 10rem; object-fit: cover\"\n * />\n * ```\n *\n * @see https://ngwr.dev/reference/components/lightbox\n */\n@Component({\n selector: 'wr-lightbox',\n templateUrl: './lightbox.html',\n styleUrl: './lightbox.scss',\n encapsulation: ViewEncapsulation.None,\n host: {\n '[class]': 'classes()',\n '[style.aspect-ratio]': 'aspectRatio()',\n },\n})\nexport class WrLightbox {\n /** Accessible name. Falls back to `image.close`, then `'Close preview'`. */\n /** Accessible name of the thumbnail button. Falls back to `alt`, then `image.open`. */\n readonly openLabel = input<string | null>(null);\n\n readonly closeLabel = input<string | null>(null);\n\n protected readonly resolvedCloseLabel = useI18nText(this.closeLabel, 'image.close', 'Close preview');\n\n /** Image source. */\n readonly src = input.required<string>();\n\n /** Alt text. Required for a11y; falls back to an empty string. @default '' */\n readonly alt = input<string>('');\n\n /**\n * Lighter source for the THUMBNAIL — a small or blurred copy, so the grid does not\n * pull full-size images. The viewer always shows `src`; the thumbnail does not swap\n * to it later.\n */\n readonly preview = input<string | null>(null);\n\n // Both of these were hard-coded English literals in the template, next to a catalog\n // that already carried `image.open` translated and unread. `useI18nText` already\n // does the whole chain — a non-empty binding wins, then the catalog, then the\n // English default — so the alt text (which describes the actual picture) is what\n // gets forwarded, with `openLabel` ahead of it as the explicit override.\n protected readonly resolvedOpenLabel = useI18nText(\n computed(() => {\n // Not `??`: an empty `openLabel` is no label at all, so it has to fall through\n // to the alt text rather than winning as an empty string.\n const explicit = this.openLabel();\n if (explicit) return explicit;\n return this.alt();\n }),\n 'image.open',\n 'Open preview'\n );\n\n protected readonly resolvedViewerLabel = useI18nText(this.alt, 'image.viewer', 'Image preview');\n\n /** Disable the click-to-zoom lightbox. @default false */\n readonly disablePreview = input(false, { transform: coerceBooleanProperty });\n\n /** Caption shown under the full image in the lightbox. */\n readonly caption = input<string>('');\n\n /**\n * Reserve space before the image resolves by fixing the thumbnail's\n * `aspect-ratio` (e.g. `'16 / 9'`, `'4 / 3'`, or a number like `1.5`).\n * Prevents the layout jump when the intrinsic image size isn't known up\n * front — pair it with a `width` and the box keeps its height from first\n * paint. @default null\n */\n readonly aspectRatio = input<string | number | null>(null);\n\n protected readonly open = signal(false);\n protected readonly loaded = signal(false);\n\n /**\n * Id the trigger's `aria-controls` points at while the viewer is open.\n *\n * @internal Public so the host bindings can read it.\n */\n protected readonly viewerId = `wr-lightbox-viewer-${++viewerUid}`;\n\n protected readonly classes = computed(() => {\n const parts = ['wr-lightbox'];\n if (this.open()) parts.push('wr-lightbox--open');\n if (!this.loaded()) parts.push('wr-lightbox--loading');\n return parts.join(' ');\n });\n\n // Query by name — `@if` blocks register their own TemplateRefs, so an\n // unqualified TemplateRef query would grab the thumbnail branch instead.\n protected readonly panelTpl = viewChild.required<TemplateRef<unknown>>('viewerTpl');\n\n private readonly overlay = inject(WR_OVERLAY);\n private readonly vcr = inject(ViewContainerRef);\n private readonly destroyRef = inject(DestroyRef);\n private readonly focusTrapFactory = inject(ConfigurableFocusTrapFactory);\n private focusTrap: ConfigurableFocusTrap | null = null;\n private previouslyFocused: HTMLElement | null = null;\n private overlayRef: OverlayRef | null = null;\n\n constructor() {\n // Destroying the component while the viewer is open removes the projected\n // content (the portal uses this component's ViewContainerRef) but leaves the\n // overlay pane and its full-screen backdrop behind, with page scroll still\n // blocked. Dispose the overlay explicitly.\n this.destroyRef.onDestroy(() => this.closeOverlay());\n\n effect(() => {\n if (this.open()) this.openOverlay();\n else this.closeOverlay();\n });\n }\n\n protected onClick(): void {\n if (this.disablePreview()) return;\n this.open.set(true);\n }\n\n protected onClose(): void {\n this.open.set(false);\n }\n\n // Swipe-down-to-dismiss on the open viewer. The image follows the finger and\n // fades; releasing past ~20% of the viewer height closes it.\n private viewerStartY = 0;\n private viewerSwiping = false;\n\n protected onViewerSwipeStart(event: TouchEvent): void {\n const touch = event.touches[0];\n if (!touch) return;\n this.viewerStartY = touch.clientY;\n this.viewerSwiping = true;\n }\n\n protected onViewerSwipeMove(event: TouchEvent, viewer: HTMLElement): void {\n if (!this.viewerSwiping) return;\n const touch = event.touches[0];\n if (!touch) return;\n const dy = touch.clientY - this.viewerStartY;\n if (dy <= 0) {\n viewer.style.transform = '';\n viewer.style.opacity = '';\n return;\n }\n event.preventDefault();\n viewer.style.transition = 'none';\n viewer.style.transform = `translateY(${dy}px)`;\n viewer.style.opacity = String(Math.max(0.3, 1 - dy / (viewer.offsetHeight || 1)));\n }\n\n protected onViewerSwipeEnd(event: TouchEvent, viewer: HTMLElement): void {\n if (!this.viewerSwiping) return;\n this.viewerSwiping = false;\n const touch = event.changedTouches[0];\n const dy = touch ? touch.clientY - this.viewerStartY : 0;\n if (dy > (viewer.offsetHeight || 0) * 0.2) {\n this.onClose();\n } else {\n viewer.style.transition = '';\n viewer.style.transform = '';\n viewer.style.opacity = '';\n }\n }\n\n /**\n * The thumbnail has settled, one way or the other. A failure counts: `--loading`\n * animates a shimmer AND sets `opacity: 0` on the image, so a broken `src` with no\n * error path left an invisible box shimmering for ever instead of showing its alt\n * text.\n */\n protected onSettled(): void {\n this.loaded.set(true);\n }\n\n // Overlay\n\n private openOverlay(): void {\n if (this.overlayRef) return;\n\n this.overlayRef = this.overlay.create({\n hasBackdrop: true,\n backdropClass: 'wr-lightbox-backdrop',\n positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),\n scrollStrategy: this.overlay.scrollStrategies.block(),\n panelClass: 'wr-lightbox-overlay',\n });\n\n const portal = new TemplatePortal(this.panelTpl(), this.vcr);\n this.overlayRef.attach(portal);\n\n // The viewer declares role=\"dialog\" + aria-modal=\"true\", which promises the\n // rest of the page is inert. Without a focus trap that promise is a lie:\n // Tab walks straight out of the viewer into the page behind the backdrop.\n const active = document.activeElement;\n this.previouslyFocused = active instanceof HTMLElement ? active : null;\n this.focusTrap = this.focusTrapFactory.create(this.overlayRef.overlayElement);\n void this.focusTrap.focusInitialElementWhenReady();\n\n this.overlayRef\n .backdropClick()\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(() => this.open.set(false));\n\n this.overlayRef\n .keydownEvents()\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(event => {\n if (event.key === 'Escape') {\n event.preventDefault();\n this.open.set(false);\n }\n });\n }\n\n private closeOverlay(): void {\n if (!this.overlayRef) return;\n this.focusTrap?.destroy();\n this.focusTrap = null;\n const restore = this.previouslyFocused;\n this.previouslyFocused = null;\n this.overlayRef.dispose();\n this.overlayRef = null;\n // Send focus back where it came from, or it lands on <body> and the user\n // has to tab from the top of the page again.\n if (restore && typeof restore.focus === 'function') restore.focus();\n }\n}\n","@if (disablePreview()) {\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n} @else {\n <!-- The trigger opens a modal dialog, and used to say so nowhere: no hint that it\n opens anything, and nothing on the page tying it to the viewer it opened. -->\n <button\n type=\"button\"\n class=\"wr-lightbox__trigger\"\n aria-haspopup=\"dialog\"\n [attr.aria-controls]=\"open() ? viewerId : null\"\n [attr.aria-label]=\"resolvedOpenLabel()\"\n (click)=\"onClick()\"\n >\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n </button>\n}\n\n<ng-template #viewerTpl>\n <div\n #viewer\n class=\"wr-lightbox-viewer\"\n [id]=\"viewerId\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"resolvedViewerLabel()\"\n (touchstart)=\"onViewerSwipeStart($event)\"\n (touchmove)=\"onViewerSwipeMove($event, viewer)\"\n (touchend)=\"onViewerSwipeEnd($event, viewer)\"\n (touchcancel)=\"onViewerSwipeEnd($event, viewer)\"\n >\n <button\n type=\"button\"\n class=\"wr-lightbox-viewer__close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"onClose()\"\n (keydown.enter)=\"onClose()\"\n (keydown.space)=\"onClose()\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </button>\n <!--\n Styled `cursor: zoom-out`, which is a promise: clicking the image closes. A\n REDUNDANT mouse affordance — the close button, Escape, the backdrop and\n swipe-down all do the same thing and all reach a keyboard — so the image stays\n out of the tab order rather than becoming a second close stop.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <img class=\"wr-lightbox-viewer__full\" [src]=\"src()\" [alt]=\"alt()\" (click)=\"onClose()\" />\n @if (caption()) {\n <div class=\"wr-lightbox-viewer__caption\">{{ caption() }}</div>\n }\n </div>\n</ng-template>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ngwr-lightbox.mjs","sources":["../../../projects/lib/lightbox/lightbox.ts","../../../projects/lib/lightbox/lightbox.html","../../../projects/lib/lightbox/ngwr-lightbox.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { type ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';\nimport { Directionality } from '@angular/cdk/bidi';\nimport { coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { type OverlayRef } from '@angular/cdk/overlay';\nimport { TemplatePortal } from '@angular/cdk/portal';\nimport type { TemplateRef } from '@angular/core';\nimport {\n Component,\n DestroyRef,\n EnvironmentInjector,\n Injector,\n ViewContainerRef,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n signal,\n viewChild,\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\n\nimport { useI18nText } from 'ngwr/i18n';\nimport { WR_OVERLAY, wrFollowDirection } from 'ngwr/overlay';\n\nlet viewerUid = 0;\n\n/**\n * Image with click-to-zoom lightbox. The thumbnail is a plain `<img>`\n * with whatever sizing/`object-fit` you give it; clicking opens a CDK\n * overlay showing the full image, with Escape + close button + backdrop\n * to dismiss.\n *\n * @example\n * ```html\n * <wr-lightbox\n * src=\"/photo.jpg\"\n * alt=\"Mountain\"\n * style=\"width: 16rem; height: 10rem; object-fit: cover\"\n * />\n * ```\n *\n * @see https://ngwr.dev/reference/components/lightbox\n */\n@Component({\n selector: 'wr-lightbox',\n templateUrl: './lightbox.html',\n styleUrl: './lightbox.scss',\n encapsulation: ViewEncapsulation.None,\n host: {\n '[class]': 'classes()',\n '[style.aspect-ratio]': 'aspectRatio()',\n },\n})\nexport class WrLightbox {\n /** Accessible name. Falls back to `image.close`, then `'Close preview'`. */\n /** Accessible name of the thumbnail button. Falls back to `alt`, then `image.open`. */\n readonly openLabel = input<string | null>(null);\n\n readonly closeLabel = input<string | null>(null);\n\n protected readonly resolvedCloseLabel = useI18nText(this.closeLabel, 'image.close', 'Close preview');\n\n /** Image source. */\n readonly src = input.required<string>();\n\n /** Alt text. Required for a11y; falls back to an empty string. @default '' */\n readonly alt = input<string>('');\n\n /**\n * Lighter source for the THUMBNAIL — a small or blurred copy, so the grid does not\n * pull full-size images. The viewer always shows `src`; the thumbnail does not swap\n * to it later.\n */\n readonly preview = input<string | null>(null);\n\n // Both of these were hard-coded English literals in the template, next to a catalog\n // that already carried `image.open` translated and unread. `useI18nText` already\n // does the whole chain — a non-empty binding wins, then the catalog, then the\n // English default — so the alt text (which describes the actual picture) is what\n // gets forwarded, with `openLabel` ahead of it as the explicit override.\n protected readonly resolvedOpenLabel = useI18nText(\n computed(() => {\n // Not `??`: an empty `openLabel` is no label at all, so it has to fall through\n // to the alt text rather than winning as an empty string.\n const explicit = this.openLabel();\n if (explicit) return explicit;\n return this.alt();\n }),\n 'image.open',\n 'Open preview'\n );\n\n protected readonly resolvedViewerLabel = useI18nText(this.alt, 'image.viewer', 'Image preview');\n\n /** Disable the click-to-zoom lightbox. @default false */\n readonly disablePreview = input(false, { transform: coerceBooleanProperty });\n\n /** Caption shown under the full image in the lightbox. */\n readonly caption = input<string>('');\n\n /**\n * Reserve space before the image resolves by fixing the thumbnail's\n * `aspect-ratio` (e.g. `'16 / 9'`, `'4 / 3'`, or a number like `1.5`).\n * Prevents the layout jump when the intrinsic image size isn't known up\n * front — pair it with a `width` and the box keeps its height from first\n * paint. @default null\n */\n readonly aspectRatio = input<string | number | null>(null);\n\n protected readonly open = signal(false);\n protected readonly loaded = signal(false);\n\n /**\n * Id the trigger's `aria-controls` points at while the viewer is open.\n *\n * @internal Public so the host bindings can read it.\n */\n protected readonly viewerId = `wr-lightbox-viewer-${++viewerUid}`;\n\n protected readonly classes = computed(() => {\n const parts = ['wr-lightbox'];\n if (this.open()) parts.push('wr-lightbox--open');\n if (!this.loaded()) parts.push('wr-lightbox--loading');\n return parts.join(' ');\n });\n\n // Query by name — `@if` blocks register their own TemplateRefs, so an\n // unqualified TemplateRef query would grab the thumbnail branch instead.\n protected readonly panelTpl = viewChild.required<TemplateRef<unknown>>('viewerTpl');\n\n private readonly overlay = inject(WR_OVERLAY);\n private readonly vcr = inject(ViewContainerRef);\n private readonly destroyRef = inject(DestroyRef);\n private readonly injector = inject(Injector);\n private readonly focusTrapFactory = inject(ConfigurableFocusTrapFactory);\n /**\n * The reading direction the open viewer has to keep up with, resolved off the\n * ENVIRONMENT injector rather than this component's — the same instance the\n * CDK read when it stamped the overlay, since `createOverlayRef` takes\n * `Directionality` from the `Overlay`'s injector and no node-level `[dir]` is\n * in that chain. The viewer is appended to the overlay container on `<body>`,\n * not into whatever region the thumbnail sits in, so reading the node\n * injector would rewrite a correctly-stamped panel to an island's direction\n * one pass after it opened rather than follow the application at all.\n *\n * `null` never happens in an app — `Directionality` is root-provided — but a\n * bare `TestBed` should still get a viewer that opens.\n */\n private readonly dir = inject(EnvironmentInjector).get(Directionality, null);\n private focusTrap: ConfigurableFocusTrap | null = null;\n private previouslyFocused: HTMLElement | null = null;\n private overlayRef: OverlayRef | null = null;\n\n constructor() {\n // Destroying the component while the viewer is open removes the projected\n // content (the portal uses this component's ViewContainerRef) but leaves the\n // overlay pane and its full-screen backdrop behind, with page scroll still\n // blocked. Dispose the overlay explicitly.\n this.destroyRef.onDestroy(() => this.closeOverlay());\n\n effect(() => {\n if (this.open()) this.openOverlay();\n else this.closeOverlay();\n });\n }\n\n protected onClick(): void {\n if (this.disablePreview()) return;\n this.open.set(true);\n }\n\n protected onClose(): void {\n this.open.set(false);\n }\n\n // Swipe-down-to-dismiss on the open viewer. The image follows the finger and\n // fades; releasing past ~20% of the viewer height closes it.\n private viewerStartY = 0;\n private viewerSwiping = false;\n\n protected onViewerSwipeStart(event: TouchEvent): void {\n const touch = event.touches[0];\n if (!touch) return;\n this.viewerStartY = touch.clientY;\n this.viewerSwiping = true;\n }\n\n protected onViewerSwipeMove(event: TouchEvent, viewer: HTMLElement): void {\n if (!this.viewerSwiping) return;\n const touch = event.touches[0];\n if (!touch) return;\n const dy = touch.clientY - this.viewerStartY;\n if (dy <= 0) {\n viewer.style.transform = '';\n viewer.style.opacity = '';\n return;\n }\n event.preventDefault();\n viewer.style.transition = 'none';\n viewer.style.transform = `translateY(${dy}px)`;\n viewer.style.opacity = String(Math.max(0.3, 1 - dy / (viewer.offsetHeight || 1)));\n }\n\n protected onViewerSwipeEnd(event: TouchEvent, viewer: HTMLElement): void {\n if (!this.viewerSwiping) return;\n this.viewerSwiping = false;\n const touch = event.changedTouches[0];\n const dy = touch ? touch.clientY - this.viewerStartY : 0;\n if (dy > (viewer.offsetHeight || 0) * 0.2) {\n this.onClose();\n } else {\n viewer.style.transition = '';\n viewer.style.transform = '';\n viewer.style.opacity = '';\n }\n }\n\n /**\n * The thumbnail has settled, one way or the other. A failure counts: `--loading`\n * animates a shimmer AND sets `opacity: 0` on the image, so a broken `src` with no\n * error path left an invisible box shimmering for ever instead of showing its alt\n * text.\n */\n protected onSettled(): void {\n this.loaded.set(true);\n }\n\n // Overlay\n\n private openOverlay(): void {\n if (this.overlayRef) return;\n\n this.overlayRef = this.overlay.create({\n hasBackdrop: true,\n backdropClass: 'wr-lightbox-backdrop',\n positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),\n scrollStrategy: this.overlay.scrollStrategies.block(),\n panelClass: 'wr-lightbox-overlay',\n });\n\n // Centred on both axes, so no geometry rides on the direction — but the\n // viewer's own chrome does. The ✕ is placed with `inset-inline-end` and the\n // caption reads as prose, both against the `dir` the CDK froze at create, so\n // a flip while the viewer is open leaves the button on the wrong corner.\n wrFollowDirection(this.overlayRef, this.dir, this.injector);\n\n const portal = new TemplatePortal(this.panelTpl(), this.vcr);\n this.overlayRef.attach(portal);\n\n // The viewer declares role=\"dialog\" + aria-modal=\"true\", which promises the\n // rest of the page is inert. Without a focus trap that promise is a lie:\n // Tab walks straight out of the viewer into the page behind the backdrop.\n const active = document.activeElement;\n this.previouslyFocused = active instanceof HTMLElement ? active : null;\n this.focusTrap = this.focusTrapFactory.create(this.overlayRef.overlayElement);\n void this.focusTrap.focusInitialElementWhenReady();\n\n this.overlayRef\n .backdropClick()\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(() => this.open.set(false));\n\n this.overlayRef\n .keydownEvents()\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(event => {\n if (event.key === 'Escape') {\n event.preventDefault();\n this.open.set(false);\n }\n });\n }\n\n private closeOverlay(): void {\n if (!this.overlayRef) return;\n this.focusTrap?.destroy();\n this.focusTrap = null;\n const restore = this.previouslyFocused;\n this.previouslyFocused = null;\n this.overlayRef.dispose();\n this.overlayRef = null;\n // Send focus back where it came from, or it lands on <body> and the user\n // has to tab from the top of the page again.\n if (restore && typeof restore.focus === 'function') restore.focus();\n }\n}\n","@if (disablePreview()) {\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n} @else {\n <!-- The trigger opens a modal dialog, and used to say so nowhere: no hint that it\n opens anything, and nothing on the page tying it to the viewer it opened. -->\n <button\n type=\"button\"\n class=\"wr-lightbox__trigger\"\n aria-haspopup=\"dialog\"\n [attr.aria-controls]=\"open() ? viewerId : null\"\n [attr.aria-label]=\"resolvedOpenLabel()\"\n (click)=\"onClick()\"\n >\n <img\n class=\"wr-lightbox__thumb\"\n [src]=\"preview() || src()\"\n [alt]=\"alt()\"\n (load)=\"onSettled()\"\n (error)=\"onSettled()\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n </button>\n}\n\n<ng-template #viewerTpl>\n <div\n #viewer\n class=\"wr-lightbox-viewer\"\n [id]=\"viewerId\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"resolvedViewerLabel()\"\n (touchstart)=\"onViewerSwipeStart($event)\"\n (touchmove)=\"onViewerSwipeMove($event, viewer)\"\n (touchend)=\"onViewerSwipeEnd($event, viewer)\"\n (touchcancel)=\"onViewerSwipeEnd($event, viewer)\"\n >\n <button\n type=\"button\"\n class=\"wr-lightbox-viewer__close\"\n [attr.aria-label]=\"resolvedCloseLabel()\"\n (click)=\"onClose()\"\n (keydown.enter)=\"onClose()\"\n (keydown.space)=\"onClose()\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </button>\n <!--\n Styled `cursor: zoom-out`, which is a promise: clicking the image closes. A\n REDUNDANT mouse affordance — the close button, Escape, the backdrop and\n swipe-down all do the same thing and all reach a keyboard — so the image stays\n out of the tab order rather than becoming a second close stop.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <img class=\"wr-lightbox-viewer__full\" [src]=\"src()\" [alt]=\"alt()\" (click)=\"onClose()\" />\n @if (caption()) {\n <div class=\"wr-lightbox-viewer__caption\">{{ caption() }}</div>\n }\n </div>\n</ng-template>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -105,9 +105,25 @@ class WrLineChartHarness extends ComponentHarness {
105
105
  value: (await values[index]?.text()) ?? '',
106
106
  })));
107
107
  }
108
- /** The plot's height in pixels, as the component writes it inline. */
108
+ /**
109
+ * The plot's height in pixels, read off the `style` ATTRIBUTE.
110
+ *
111
+ * NOT `getCssValue()`, which is `getComputedStyle()` — the same rule this
112
+ * file's class note already states and this method alone broke. A computed
113
+ * read answers from the LAID-OUT box, so it agrees with the inline value in
114
+ * jsdom (which lays nothing out and echoes the declaration) and disagrees in a
115
+ * browser, where sub-pixel rounding turns a declared 240 into 239.5. A harness
116
+ * that answers differently in the two places it runs is worse than one that
117
+ * refuses.
118
+ */
109
119
  async getPlotHeight() {
110
- return Number.parseFloat(await (await this.locatorFor('.wr-line-chart__plot')()).getCssValue('height'));
120
+ const style = await (await this.locatorFor('.wr-line-chart__plot')()).getAttribute('style');
121
+ const match = /height:\s*([\d.]+)px/.exec(style ?? '');
122
+ if (!match) {
123
+ throw new Error(`WrLineChartHarness.getPlotHeight(): no inline height on .wr-line-chart__plot (style: "${style ?? ''}"). ` +
124
+ 'The component writes one unconditionally, so its absence is the bug rather than a zero.');
125
+ }
126
+ return Number.parseFloat(match[1]);
111
127
  }
112
128
  }
113
129
 
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-line-chart-testing.mjs","sources":["../../../projects/lib/line-chart/testing/wr-line-chart-harness.ts","../../../projects/lib/line-chart/testing/ngwr-line-chart-testing.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrLineChartHarnessFilters, WrLineChartTooltipRow } from './interfaces';\n\n/**\n * Test harness for `<wr-line-chart>`.\n *\n * **The lines are paths and the paths are not assertable.** A `d` attribute is a\n * rendering detail — it moves with the viewBox, the padding and the value range — so\n * this harness counts the lines and reads everything TEXTUAL instead: the legend, the\n * axis ticks, the x labels and the tooltip. Those are what a reader gets, and they\n * are what breaks when the data wiring does.\n *\n * **The tooltip cannot be opened from a spec, and the reason is worth knowing.** It\n * follows a `pointermove` over the plot, and the component turns the cursor's x into\n * an index by measuring the plot's box — which jsdom reports as 0×0, so every\n * synthetic move resolves to the same index or to none at all. There is deliberately\n * no `hoverAt()`; {@link getTooltipRows} reads the tooltip when something else has\n * opened it, which in practice means a browser-run harness.\n *\n * @example\n * ```ts\n * const chart = await loader.getHarness(WrLineChartHarness);\n *\n * expect(await chart.getSeriesLabels()).toEqual(['Revenue', 'Costs']);\n * expect(await chart.getLineCount()).toBe(2);\n * expect(await chart.getXLabels()).toEqual(['Jan', 'Feb', 'Mar']);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrLineChartHarness extends ComponentHarness {\n static hostSelector = 'wr-line-chart';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrLineChartHarnessFilters = {}): HarnessPredicate<WrLineChartHarness> {\n return new HarnessPredicate(WrLineChartHarness, options)\n .addOption('ariaLabel', options.ariaLabel, (harness, label) =>\n HarnessPredicate.stringMatches(harness.getAccessibleName(), label)\n )\n .addOption('seriesLabel', options.seriesLabel, async (harness, label) => {\n for (const series of await harness.getSeriesLabels()) {\n if (await HarnessPredicate.stringMatches(series, label)) return true;\n }\n return false;\n });\n }\n\n /** The plot's accessible name — the only name the drawing has. */\n async getAccessibleName(): Promise<string | null> {\n return (await this.locatorFor('.wr-line-chart__plot')()).getAttribute('aria-label');\n }\n\n /** Whether the legend is rendered (`showLegend`, and at least one series). */\n async hasLegend(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-line-chart__legend')()) !== null;\n }\n\n /** The series names from the legend, in order. */\n async getSeriesLabels(): Promise<string[]> {\n const items = await this.locatorForAll('.wr-line-chart__legend-item')();\n return Promise.all(items.map(item => item.text()));\n }\n\n /**\n * How many lines were drawn.\n *\n * Worth comparing with {@link getSeriesLabels}: the legend and the paths come from\n * the same list, so a mismatch is a series that got filtered out of one of them.\n */\n async getLineCount(): Promise<number> {\n return (await this.locatorForAll('.wr-line-chart__line')()).length;\n }\n\n /** How many point dots were drawn across every series (`showDots`). */\n async getDotCount(): Promise<number> {\n return (await this.locatorForAll('.wr-line-chart__dot')()).length;\n }\n\n /** Whether the horizontal grid lines are drawn (`showGrid`). */\n async hasGrid(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-line-chart__grid-line')()) !== null;\n }\n\n /** The y-axis tick labels, top to bottom as drawn. */\n async getYTicks(): Promise<string[]> {\n const ticks = await this.locatorForAll('.wr-line-chart__tick')();\n return Promise.all(ticks.map(tick => tick.text()));\n }\n\n /** The x-axis labels under the plot, in order. */\n async getXLabels(): Promise<string[]> {\n const labels = await this.locatorForAll('.wr-line-chart__x-label')();\n return Promise.all(labels.map(label => label.text()));\n }\n\n /** Whether the hover tooltip is showing. */\n async hasTooltip(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-line-chart__tooltip')()) !== null;\n }\n\n /** The tooltip's heading — the x label of the hovered point — or `null` while it is closed. */\n async getTooltipLabel(): Promise<string | null> {\n const label = await this.locatorForOptional('.wr-line-chart__tooltip-label')();\n return label ? label.text() : null;\n }\n\n /** The tooltip's rows, one per series, or `[]` while it is closed. */\n async getTooltipRows(): Promise<WrLineChartTooltipRow[]> {\n const names = await this.locatorForAll('.wr-line-chart__tooltip-name')();\n const values = await this.locatorForAll('.wr-line-chart__tooltip-value')();\n\n return Promise.all(\n names.map(async (name, index) => ({\n label: await name.text(),\n value: (await values[index]?.text()) ?? '',\n }))\n );\n }\n\n /** The plot's height in pixels, as the component writes it inline. */\n async getPlotHeight(): Promise<number> {\n return Number.parseFloat(await (await this.locatorFor('.wr-line-chart__plot')()).getCssValue('height'));\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BG;AACG,MAAO,kBAAmB,SAAQ,gBAAgB,CAAA;AACtD,IAAA,OAAO,YAAY,GAAG,eAAe;;AAGrC,IAAA,OAAO,IAAI,CAAC,OAAA,GAAqC,EAAE,EAAA;AACjD,QAAA,OAAO,IAAI,gBAAgB,CAAC,kBAAkB,EAAE,OAAO;aACpD,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,KAAK,KACxD,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,KAAK,CAAC;AAEnE,aAAA,SAAS,CAAC,aAAa,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,OAAO,EAAE,KAAK,KAAI;YACtE,KAAK,MAAM,MAAM,IAAI,MAAM,OAAO,CAAC,eAAe,EAAE,EAAE;gBACpD,IAAI,MAAM,gBAAgB,CAAC,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC;AAAE,oBAAA,OAAO,IAAI;YACtE;AACA,YAAA,OAAO,KAAK;AACd,QAAA,CAAC,CAAC;IACN;;AAGA,IAAA,MAAM,iBAAiB,GAAA;AACrB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,sBAAsB,CAAC,EAAE,EAAE,YAAY,CAAC,YAAY,CAAC;IACrF;;AAGA,IAAA,MAAM,SAAS,GAAA;AACb,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,wBAAwB,CAAC,EAAE,MAAM,IAAI;IAC7E;;AAGA,IAAA,MAAM,eAAe,GAAA;QACnB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,6BAA6B,CAAC,EAAE;AACvE,QAAA,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACpD;AAEA;;;;;AAKG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,sBAAsB,CAAC,EAAE,EAAE,MAAM;IACpE;;AAGA,IAAA,MAAM,WAAW,GAAA;AACf,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,qBAAqB,CAAC,EAAE,EAAE,MAAM;IACnE;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,2BAA2B,CAAC,EAAE,MAAM,IAAI;IAChF;;AAGA,IAAA,MAAM,SAAS,GAAA;QACb,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,sBAAsB,CAAC,EAAE;AAChE,QAAA,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACpD;;AAGA,IAAA,MAAM,UAAU,GAAA;QACd,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,yBAAyB,CAAC,EAAE;AACpE,QAAA,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACvD;;AAGA,IAAA,MAAM,UAAU,GAAA;AACd,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,yBAAyB,CAAC,EAAE,MAAM,IAAI;IAC9E;;AAGA,IAAA,MAAM,eAAe,GAAA;QACnB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,+BAA+B,CAAC,EAAE;AAC9E,QAAA,OAAO,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,GAAG,IAAI;IACpC;;AAGA,IAAA,MAAM,cAAc,GAAA;QAClB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,8BAA8B,CAAC,EAAE;QACxE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,+BAA+B,CAAC,EAAE;AAE1E,QAAA,OAAO,OAAO,CAAC,GAAG,CAChB,KAAK,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,KAAK,MAAM;AAChC,YAAA,KAAK,EAAE,MAAM,IAAI,CAAC,IAAI,EAAE;AACxB,YAAA,KAAK,EAAE,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE;SAC3C,CAAC,CAAC,CACJ;IACH;;AAGA,IAAA,MAAM,aAAa,GAAA;QACjB,OAAO,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,sBAAsB,CAAC,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IACzG;;;AClIF;;AAEG;;;;"}
1
+ {"version":3,"file":"ngwr-line-chart-testing.mjs","sources":["../../../projects/lib/line-chart/testing/wr-line-chart-harness.ts","../../../projects/lib/line-chart/testing/ngwr-line-chart-testing.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrLineChartHarnessFilters, WrLineChartTooltipRow } from './interfaces';\n\n/**\n * Test harness for `<wr-line-chart>`.\n *\n * **The lines are paths and the paths are not assertable.** A `d` attribute is a\n * rendering detail — it moves with the viewBox, the padding and the value range — so\n * this harness counts the lines and reads everything TEXTUAL instead: the legend, the\n * axis ticks, the x labels and the tooltip. Those are what a reader gets, and they\n * are what breaks when the data wiring does.\n *\n * **The tooltip cannot be opened from a spec, and the reason is worth knowing.** It\n * follows a `pointermove` over the plot, and the component turns the cursor's x into\n * an index by measuring the plot's box — which jsdom reports as 0×0, so every\n * synthetic move resolves to the same index or to none at all. There is deliberately\n * no `hoverAt()`; {@link getTooltipRows} reads the tooltip when something else has\n * opened it, which in practice means a browser-run harness.\n *\n * @example\n * ```ts\n * const chart = await loader.getHarness(WrLineChartHarness);\n *\n * expect(await chart.getSeriesLabels()).toEqual(['Revenue', 'Costs']);\n * expect(await chart.getLineCount()).toBe(2);\n * expect(await chart.getXLabels()).toEqual(['Jan', 'Feb', 'Mar']);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrLineChartHarness extends ComponentHarness {\n static hostSelector = 'wr-line-chart';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrLineChartHarnessFilters = {}): HarnessPredicate<WrLineChartHarness> {\n return new HarnessPredicate(WrLineChartHarness, options)\n .addOption('ariaLabel', options.ariaLabel, (harness, label) =>\n HarnessPredicate.stringMatches(harness.getAccessibleName(), label)\n )\n .addOption('seriesLabel', options.seriesLabel, async (harness, label) => {\n for (const series of await harness.getSeriesLabels()) {\n if (await HarnessPredicate.stringMatches(series, label)) return true;\n }\n return false;\n });\n }\n\n /** The plot's accessible name — the only name the drawing has. */\n async getAccessibleName(): Promise<string | null> {\n return (await this.locatorFor('.wr-line-chart__plot')()).getAttribute('aria-label');\n }\n\n /** Whether the legend is rendered (`showLegend`, and at least one series). */\n async hasLegend(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-line-chart__legend')()) !== null;\n }\n\n /** The series names from the legend, in order. */\n async getSeriesLabels(): Promise<string[]> {\n const items = await this.locatorForAll('.wr-line-chart__legend-item')();\n return Promise.all(items.map(item => item.text()));\n }\n\n /**\n * How many lines were drawn.\n *\n * Worth comparing with {@link getSeriesLabels}: the legend and the paths come from\n * the same list, so a mismatch is a series that got filtered out of one of them.\n */\n async getLineCount(): Promise<number> {\n return (await this.locatorForAll('.wr-line-chart__line')()).length;\n }\n\n /** How many point dots were drawn across every series (`showDots`). */\n async getDotCount(): Promise<number> {\n return (await this.locatorForAll('.wr-line-chart__dot')()).length;\n }\n\n /** Whether the horizontal grid lines are drawn (`showGrid`). */\n async hasGrid(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-line-chart__grid-line')()) !== null;\n }\n\n /** The y-axis tick labels, top to bottom as drawn. */\n async getYTicks(): Promise<string[]> {\n const ticks = await this.locatorForAll('.wr-line-chart__tick')();\n return Promise.all(ticks.map(tick => tick.text()));\n }\n\n /** The x-axis labels under the plot, in order. */\n async getXLabels(): Promise<string[]> {\n const labels = await this.locatorForAll('.wr-line-chart__x-label')();\n return Promise.all(labels.map(label => label.text()));\n }\n\n /** Whether the hover tooltip is showing. */\n async hasTooltip(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-line-chart__tooltip')()) !== null;\n }\n\n /** The tooltip's heading — the x label of the hovered point — or `null` while it is closed. */\n async getTooltipLabel(): Promise<string | null> {\n const label = await this.locatorForOptional('.wr-line-chart__tooltip-label')();\n return label ? label.text() : null;\n }\n\n /** The tooltip's rows, one per series, or `[]` while it is closed. */\n async getTooltipRows(): Promise<WrLineChartTooltipRow[]> {\n const names = await this.locatorForAll('.wr-line-chart__tooltip-name')();\n const values = await this.locatorForAll('.wr-line-chart__tooltip-value')();\n\n return Promise.all(\n names.map(async (name, index) => ({\n label: await name.text(),\n value: (await values[index]?.text()) ?? '',\n }))\n );\n }\n\n /**\n * The plot's height in pixels, read off the `style` ATTRIBUTE.\n *\n * NOT `getCssValue()`, which is `getComputedStyle()` — the same rule this\n * file's class note already states and this method alone broke. A computed\n * read answers from the LAID-OUT box, so it agrees with the inline value in\n * jsdom (which lays nothing out and echoes the declaration) and disagrees in a\n * browser, where sub-pixel rounding turns a declared 240 into 239.5. A harness\n * that answers differently in the two places it runs is worse than one that\n * refuses.\n */\n async getPlotHeight(): Promise<number> {\n const style = await (await this.locatorFor('.wr-line-chart__plot')()).getAttribute('style');\n const match = /height:\\s*([\\d.]+)px/.exec(style ?? '');\n if (!match) {\n throw new Error(\n `WrLineChartHarness.getPlotHeight(): no inline height on .wr-line-chart__plot (style: \"${style ?? ''}\"). ` +\n 'The component writes one unconditionally, so its absence is the bug rather than a zero.'\n );\n }\n return Number.parseFloat(match[1]);\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -65,8 +65,26 @@ class WrLineChart {
65
65
  /** Hovered point index (across all series) — null when no hover. */
66
66
  hoveredIndex = signal(null, /* @ts-ignore */
67
67
  ...(ngDevMode ? [{ debugName: "hoveredIndex" }] : /* istanbul ignore next */ []));
68
+ /**
69
+ * A dash pattern per series, so the lines are told apart by SHAPE and not
70
+ * only by hue.
71
+ *
72
+ * The default palette's colours sit within about 1.06:1 of each other in
73
+ * relative luminance — that is the whole nine-intent scale's constraint, and
74
+ * it means a reader with red-green colour blindness sees several of these
75
+ * strokes as one grey. A legend that pairs a coloured square with a name does
76
+ * not rescue it: matching the square to the line is exactly the step that
77
+ * needs hue. Distinguishing series by dash is the standard remedy and the
78
+ * only one that survives greyscale, printing and a monochrome display.
79
+ *
80
+ * The FIRST entry is solid on purpose. A single-series chart is the common
81
+ * case and there is nothing to tell apart in it, so it is left exactly as it
82
+ * was; the patterns only appear once a second series does.
83
+ */
84
+ static DASHES = ['', '7 4', '2 3', '10 4 2 4', '1 4', '6 3 2 3'];
68
85
  resolvedSeries = computed(() => this.series().map((s, i) => ({
69
86
  label: s.label,
87
+ dash: WrLineChart.DASHES[i % WrLineChart.DASHES.length],
70
88
  // A non-finite point becomes a HOLE at its own index, not a missing element.
71
89
  // It cannot stay a number: `Math.min`/`Math.max` over the pooled data are both
72
90
  // NaN as soon as one datum is, so every coordinate in EVERY series came out
@@ -240,12 +258,12 @@ class WrLineChart {
240
258
  ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
241
259
  // Convenience accessor so the host can be referenced from the template.
242
260
  hostEl = this.host.nativeElement;
243
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: WrLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
244
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: WrLineChart, isStandalone: true, selector: "wr-line-chart", inputs: { series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, xLabels: { classPropertyName: "xLabels", publicName: "xLabels", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showDots: { classPropertyName: "showDots", publicName: "showDots", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "wr-line-chart" }, ngImport: i0, template: "@let hovered = hoveredIndex();\n@if (showLegend() && resolvedSeries().length > 0) {\n <ul class=\"wr-line-chart__legend\">\n @for (s of resolvedSeries(); track s.label) {\n <li class=\"wr-line-chart__legend-item\">\n <span class=\"wr-line-chart__swatch\" [style.background]=\"s.color\"></span>\n <span>{{ s.label }}</span>\n </li>\n }\n </ul>\n}\n\n<div class=\"wr-line-chart__plot\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\" [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"viewBox()\"\n preserveAspectRatio=\"none\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerleave)=\"onPointerLeave()\"\n >\n @if (showGrid()) {\n @for (tick of yTicks(); track tick.value) {\n <line\n class=\"wr-line-chart__grid-line\"\n [attr.x1]=\"padding.left\"\n [attr.x2]=\"vbW - padding.right\"\n [attr.y1]=\"tick.y\"\n [attr.y2]=\"tick.y\"\n stroke=\"rgba(var(--wr-color-light-rgb), 0.7)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n <text\n class=\"wr-line-chart__tick\"\n [attr.x]=\"padding.left - 6\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n fill=\"var(--wr-color-on-surface-muted)\"\n font-size=\"10\"\n font-family=\"var(--wr-font-family-base)\"\n >\n {{ formatTick(tick.value) }}\n </text>\n }\n }\n\n @for (s of resolvedSeries(); track s.label) {\n <path\n class=\"wr-line-chart__line\"\n [attr.d]=\"pathFor(s)\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n vector-effect=\"non-scaling-stroke\"\n />\n @if (showDots()) {\n @for (v of s.data; track $index; let i = $index) {\n <!-- A hole in the data gets no dot: the index still belongs to it, so the\n points after it stay on their own x. -->\n @if (v !== null) {\n <circle\n class=\"wr-line-chart__dot\"\n [attr.cx]=\"pointX(i)\"\n [attr.cy]=\"pointY(v)\"\n r=\"3\"\n [attr.fill]=\"s.color\"\n />\n }\n }\n }\n }\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <line\n class=\"wr-line-chart__crosshair\"\n [attr.x1]=\"pointX(hovered)\"\n [attr.x2]=\"pointX(hovered)\"\n [attr.y1]=\"padding.top\"\n [attr.y2]=\"vbH - padding.bottom\"\n stroke=\"var(--wr-color-medium)\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n vector-effect=\"non-scaling-stroke\"\n />\n @for (p of hoverPoints(); track p.label) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"5\"\n fill=\"var(--wr-color-white)\"\n [attr.stroke]=\"p.color\"\n stroke-width=\"2\"\n />\n }\n }\n </svg>\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <div class=\"wr-line-chart__tooltip\" [style.left.%]=\"(pointX(hovered) / vbW) * 100\" [style.top.px]=\"padding.top\">\n <div class=\"wr-line-chart__tooltip-label\">{{ hoverLabel() }}</div>\n @for (p of hoverPoints(); track p.label) {\n <div class=\"wr-line-chart__tooltip-row\">\n <span class=\"wr-line-chart__swatch\" [style.background]=\"p.color\"></span>\n <span class=\"wr-line-chart__tooltip-name\">{{ p.label }}</span>\n <span class=\"wr-line-chart__tooltip-value\">{{ p.value }}</span>\n </div>\n }\n </div>\n }\n</div>\n\n@if (xLabels().length > 0) {\n <div class=\"wr-line-chart__x-labels\">\n @for (label of xLabels(); track $index) {\n <!-- Each label hands the stylesheet two numbers, both percentages of the host's\n width \u2014 the space the stretching SVG is in, so they track the drawing at any\n size. The x of the point it names, which the stylesheet centres it on; and the\n widest it may be before it reaches a neighbour or the end of the strip, past\n which the stylesheet ellipsises it. Laying the labels out as equal centred flex\n slots instead is bar-chart geometry: only the middle label landed on its dot,\n and the outer ones sat up to half a column away. -->\n <span\n class=\"wr-line-chart__x-label\"\n [style.--wr-line-chart-x]=\"xPercent($index)\"\n [style.--wr-line-chart-x-max]=\"xMaxPercent($index)\"\n >{{ label }}</span\n >\n }\n </div>\n}\n", encapsulation: i0.ViewEncapsulation.None });
261
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
262
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrLineChart, isStandalone: true, selector: "wr-line-chart", inputs: { series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, xLabels: { classPropertyName: "xLabels", publicName: "xLabels", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showDots: { classPropertyName: "showDots", publicName: "showDots", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "wr-line-chart" }, ngImport: i0, template: "@let hovered = hoveredIndex();\n@if (showLegend() && resolvedSeries().length > 0) {\n <ul class=\"wr-line-chart__legend\">\n @for (s of resolvedSeries(); track s.label) {\n <li class=\"wr-line-chart__legend-item\">\n <!-- The swatch draws the series' own dash, not a solid block. A legend\n that pairs a coloured square with a name leaves the reader to match\n the square to the LINE, and matching by colour is the step the\n patterns exist to remove. -->\n <svg class=\"wr-line-chart__swatch\" viewBox=\"0 0 16 2\" aria-hidden=\"true\">\n <line\n x1=\"0\"\n y1=\"1\"\n x2=\"16\"\n y2=\"1\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-dasharray]=\"s.dash || null\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n <span>{{ s.label }}</span>\n </li>\n }\n </ul>\n}\n\n<div class=\"wr-line-chart__plot\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\" [style.height.px]=\"height()\">\n <!-- Named, where it was the only unnamed element in the component. The legend\n swatch is an `<svg>` too now, so `querySelector('svg')` no longer means\n \"the chart\" \u2014 and a plot that nothing can address by name is one a\n consumer's stylesheet and a spec both have to reach by position. -->\n <svg\n class=\"wr-line-chart__svg\"\n [attr.viewBox]=\"viewBox()\"\n preserveAspectRatio=\"none\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerleave)=\"onPointerLeave()\"\n >\n @if (showGrid()) {\n @for (tick of yTicks(); track tick.value) {\n <line\n class=\"wr-line-chart__grid-line\"\n [attr.x1]=\"padding.left\"\n [attr.x2]=\"vbW - padding.right\"\n [attr.y1]=\"tick.y\"\n [attr.y2]=\"tick.y\"\n stroke=\"rgba(var(--wr-color-light-rgb), 0.7)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n <text\n class=\"wr-line-chart__tick\"\n [attr.x]=\"padding.left - 6\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n fill=\"var(--wr-color-on-surface-muted)\"\n font-size=\"10\"\n font-family=\"var(--wr-font-family-base)\"\n >\n {{ formatTick(tick.value) }}\n </text>\n }\n }\n\n @for (s of resolvedSeries(); track s.label) {\n <path\n class=\"wr-line-chart__line\"\n [attr.d]=\"pathFor(s)\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-dasharray]=\"s.dash || null\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n vector-effect=\"non-scaling-stroke\"\n />\n @if (showDots()) {\n @for (v of s.data; track $index; let i = $index) {\n <!-- A hole in the data gets no dot: the index still belongs to it, so the\n points after it stay on their own x. -->\n @if (v !== null) {\n <circle\n class=\"wr-line-chart__dot\"\n [attr.cx]=\"pointX(i)\"\n [attr.cy]=\"pointY(v)\"\n r=\"3\"\n [attr.fill]=\"s.color\"\n />\n }\n }\n }\n }\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <line\n class=\"wr-line-chart__crosshair\"\n [attr.x1]=\"pointX(hovered)\"\n [attr.x2]=\"pointX(hovered)\"\n [attr.y1]=\"padding.top\"\n [attr.y2]=\"vbH - padding.bottom\"\n stroke=\"var(--wr-color-medium)\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n vector-effect=\"non-scaling-stroke\"\n />\n @for (p of hoverPoints(); track p.label) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"5\"\n fill=\"var(--wr-color-white)\"\n [attr.stroke]=\"p.color\"\n stroke-width=\"2\"\n />\n }\n }\n </svg>\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <div class=\"wr-line-chart__tooltip\" [style.left.%]=\"(pointX(hovered) / vbW) * 100\" [style.top.px]=\"padding.top\">\n <div class=\"wr-line-chart__tooltip-label\">{{ hoverLabel() }}</div>\n @for (p of hoverPoints(); track p.label) {\n <div class=\"wr-line-chart__tooltip-row\">\n <span class=\"wr-line-chart__swatch\" [style.background]=\"p.color\"></span>\n <span class=\"wr-line-chart__tooltip-name\">{{ p.label }}</span>\n <span class=\"wr-line-chart__tooltip-value\">{{ p.value }}</span>\n </div>\n }\n </div>\n }\n</div>\n\n@if (xLabels().length > 0) {\n <div class=\"wr-line-chart__x-labels\">\n @for (label of xLabels(); track $index) {\n <!-- Each label hands the stylesheet two numbers, both percentages of the host's\n width \u2014 the space the stretching SVG is in, so they track the drawing at any\n size. The x of the point it names, which the stylesheet centres it on; and the\n widest it may be before it reaches a neighbour or the end of the strip, past\n which the stylesheet ellipsises it. Laying the labels out as equal centred flex\n slots instead is bar-chart geometry: only the middle label landed on its dot,\n and the outer ones sat up to half a column away. -->\n <span\n class=\"wr-line-chart__x-label\"\n [style.--wr-line-chart-x]=\"xPercent($index)\"\n [style.--wr-line-chart-x-max]=\"xMaxPercent($index)\"\n >{{ label }}</span\n >\n }\n </div>\n}\n", encapsulation: i0.ViewEncapsulation.None });
245
263
  }
246
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: WrLineChart, decorators: [{
264
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrLineChart, decorators: [{
247
265
  type: Component,
248
- args: [{ selector: 'wr-line-chart', encapsulation: ViewEncapsulation.None, host: { class: 'wr-line-chart' }, template: "@let hovered = hoveredIndex();\n@if (showLegend() && resolvedSeries().length > 0) {\n <ul class=\"wr-line-chart__legend\">\n @for (s of resolvedSeries(); track s.label) {\n <li class=\"wr-line-chart__legend-item\">\n <span class=\"wr-line-chart__swatch\" [style.background]=\"s.color\"></span>\n <span>{{ s.label }}</span>\n </li>\n }\n </ul>\n}\n\n<div class=\"wr-line-chart__plot\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\" [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"viewBox()\"\n preserveAspectRatio=\"none\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerleave)=\"onPointerLeave()\"\n >\n @if (showGrid()) {\n @for (tick of yTicks(); track tick.value) {\n <line\n class=\"wr-line-chart__grid-line\"\n [attr.x1]=\"padding.left\"\n [attr.x2]=\"vbW - padding.right\"\n [attr.y1]=\"tick.y\"\n [attr.y2]=\"tick.y\"\n stroke=\"rgba(var(--wr-color-light-rgb), 0.7)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n <text\n class=\"wr-line-chart__tick\"\n [attr.x]=\"padding.left - 6\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n fill=\"var(--wr-color-on-surface-muted)\"\n font-size=\"10\"\n font-family=\"var(--wr-font-family-base)\"\n >\n {{ formatTick(tick.value) }}\n </text>\n }\n }\n\n @for (s of resolvedSeries(); track s.label) {\n <path\n class=\"wr-line-chart__line\"\n [attr.d]=\"pathFor(s)\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n vector-effect=\"non-scaling-stroke\"\n />\n @if (showDots()) {\n @for (v of s.data; track $index; let i = $index) {\n <!-- A hole in the data gets no dot: the index still belongs to it, so the\n points after it stay on their own x. -->\n @if (v !== null) {\n <circle\n class=\"wr-line-chart__dot\"\n [attr.cx]=\"pointX(i)\"\n [attr.cy]=\"pointY(v)\"\n r=\"3\"\n [attr.fill]=\"s.color\"\n />\n }\n }\n }\n }\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <line\n class=\"wr-line-chart__crosshair\"\n [attr.x1]=\"pointX(hovered)\"\n [attr.x2]=\"pointX(hovered)\"\n [attr.y1]=\"padding.top\"\n [attr.y2]=\"vbH - padding.bottom\"\n stroke=\"var(--wr-color-medium)\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n vector-effect=\"non-scaling-stroke\"\n />\n @for (p of hoverPoints(); track p.label) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"5\"\n fill=\"var(--wr-color-white)\"\n [attr.stroke]=\"p.color\"\n stroke-width=\"2\"\n />\n }\n }\n </svg>\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <div class=\"wr-line-chart__tooltip\" [style.left.%]=\"(pointX(hovered) / vbW) * 100\" [style.top.px]=\"padding.top\">\n <div class=\"wr-line-chart__tooltip-label\">{{ hoverLabel() }}</div>\n @for (p of hoverPoints(); track p.label) {\n <div class=\"wr-line-chart__tooltip-row\">\n <span class=\"wr-line-chart__swatch\" [style.background]=\"p.color\"></span>\n <span class=\"wr-line-chart__tooltip-name\">{{ p.label }}</span>\n <span class=\"wr-line-chart__tooltip-value\">{{ p.value }}</span>\n </div>\n }\n </div>\n }\n</div>\n\n@if (xLabels().length > 0) {\n <div class=\"wr-line-chart__x-labels\">\n @for (label of xLabels(); track $index) {\n <!-- Each label hands the stylesheet two numbers, both percentages of the host's\n width \u2014 the space the stretching SVG is in, so they track the drawing at any\n size. The x of the point it names, which the stylesheet centres it on; and the\n widest it may be before it reaches a neighbour or the end of the strip, past\n which the stylesheet ellipsises it. Laying the labels out as equal centred flex\n slots instead is bar-chart geometry: only the middle label landed on its dot,\n and the outer ones sat up to half a column away. -->\n <span\n class=\"wr-line-chart__x-label\"\n [style.--wr-line-chart-x]=\"xPercent($index)\"\n [style.--wr-line-chart-x-max]=\"xMaxPercent($index)\"\n >{{ label }}</span\n >\n }\n </div>\n}\n" }]
266
+ args: [{ selector: 'wr-line-chart', encapsulation: ViewEncapsulation.None, host: { class: 'wr-line-chart' }, template: "@let hovered = hoveredIndex();\n@if (showLegend() && resolvedSeries().length > 0) {\n <ul class=\"wr-line-chart__legend\">\n @for (s of resolvedSeries(); track s.label) {\n <li class=\"wr-line-chart__legend-item\">\n <!-- The swatch draws the series' own dash, not a solid block. A legend\n that pairs a coloured square with a name leaves the reader to match\n the square to the LINE, and matching by colour is the step the\n patterns exist to remove. -->\n <svg class=\"wr-line-chart__swatch\" viewBox=\"0 0 16 2\" aria-hidden=\"true\">\n <line\n x1=\"0\"\n y1=\"1\"\n x2=\"16\"\n y2=\"1\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-dasharray]=\"s.dash || null\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n <span>{{ s.label }}</span>\n </li>\n }\n </ul>\n}\n\n<div class=\"wr-line-chart__plot\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\" [style.height.px]=\"height()\">\n <!-- Named, where it was the only unnamed element in the component. The legend\n swatch is an `<svg>` too now, so `querySelector('svg')` no longer means\n \"the chart\" \u2014 and a plot that nothing can address by name is one a\n consumer's stylesheet and a spec both have to reach by position. -->\n <svg\n class=\"wr-line-chart__svg\"\n [attr.viewBox]=\"viewBox()\"\n preserveAspectRatio=\"none\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerleave)=\"onPointerLeave()\"\n >\n @if (showGrid()) {\n @for (tick of yTicks(); track tick.value) {\n <line\n class=\"wr-line-chart__grid-line\"\n [attr.x1]=\"padding.left\"\n [attr.x2]=\"vbW - padding.right\"\n [attr.y1]=\"tick.y\"\n [attr.y2]=\"tick.y\"\n stroke=\"rgba(var(--wr-color-light-rgb), 0.7)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n <text\n class=\"wr-line-chart__tick\"\n [attr.x]=\"padding.left - 6\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n fill=\"var(--wr-color-on-surface-muted)\"\n font-size=\"10\"\n font-family=\"var(--wr-font-family-base)\"\n >\n {{ formatTick(tick.value) }}\n </text>\n }\n }\n\n @for (s of resolvedSeries(); track s.label) {\n <path\n class=\"wr-line-chart__line\"\n [attr.d]=\"pathFor(s)\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-dasharray]=\"s.dash || null\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n vector-effect=\"non-scaling-stroke\"\n />\n @if (showDots()) {\n @for (v of s.data; track $index; let i = $index) {\n <!-- A hole in the data gets no dot: the index still belongs to it, so the\n points after it stay on their own x. -->\n @if (v !== null) {\n <circle\n class=\"wr-line-chart__dot\"\n [attr.cx]=\"pointX(i)\"\n [attr.cy]=\"pointY(v)\"\n r=\"3\"\n [attr.fill]=\"s.color\"\n />\n }\n }\n }\n }\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <line\n class=\"wr-line-chart__crosshair\"\n [attr.x1]=\"pointX(hovered)\"\n [attr.x2]=\"pointX(hovered)\"\n [attr.y1]=\"padding.top\"\n [attr.y2]=\"vbH - padding.bottom\"\n stroke=\"var(--wr-color-medium)\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n vector-effect=\"non-scaling-stroke\"\n />\n @for (p of hoverPoints(); track p.label) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"5\"\n fill=\"var(--wr-color-white)\"\n [attr.stroke]=\"p.color\"\n stroke-width=\"2\"\n />\n }\n }\n </svg>\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <div class=\"wr-line-chart__tooltip\" [style.left.%]=\"(pointX(hovered) / vbW) * 100\" [style.top.px]=\"padding.top\">\n <div class=\"wr-line-chart__tooltip-label\">{{ hoverLabel() }}</div>\n @for (p of hoverPoints(); track p.label) {\n <div class=\"wr-line-chart__tooltip-row\">\n <span class=\"wr-line-chart__swatch\" [style.background]=\"p.color\"></span>\n <span class=\"wr-line-chart__tooltip-name\">{{ p.label }}</span>\n <span class=\"wr-line-chart__tooltip-value\">{{ p.value }}</span>\n </div>\n }\n </div>\n }\n</div>\n\n@if (xLabels().length > 0) {\n <div class=\"wr-line-chart__x-labels\">\n @for (label of xLabels(); track $index) {\n <!-- Each label hands the stylesheet two numbers, both percentages of the host's\n width \u2014 the space the stretching SVG is in, so they track the drawing at any\n size. The x of the point it names, which the stylesheet centres it on; and the\n widest it may be before it reaches a neighbour or the end of the strip, past\n which the stylesheet ellipsises it. Laying the labels out as equal centred flex\n slots instead is bar-chart geometry: only the middle label landed on its dot,\n and the outer ones sat up to half a column away. -->\n <span\n class=\"wr-line-chart__x-label\"\n [style.--wr-line-chart-x]=\"xPercent($index)\"\n [style.--wr-line-chart-x-max]=\"xMaxPercent($index)\"\n >{{ label }}</span\n >\n }\n </div>\n}\n" }]
249
267
  }], propDecorators: { series: [{ type: i0.Input, args: [{ isSignal: true, alias: "series", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], xLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "xLabels", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showGrid: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGrid", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], showDots: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDots", required: false }] }] } });
250
268
 
251
269
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-line-chart.mjs","sources":["../../../projects/lib/line-chart/line-chart.ts","../../../projects/lib/line-chart/line-chart.html","../../../projects/lib/line-chart/ngwr-line-chart.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport { Component, ElementRef, LOCALE_ID, ViewEncapsulation, computed, inject, input, signal } from '@angular/core';\n\nimport { useI18nFormatter, useI18nText } from 'ngwr/i18n';\n\nimport type { WrLineSeries } from './interfaces';\n\nconst FALLBACK_COLORS = [\n 'var(--wr-color-primary)',\n 'var(--wr-color-secondary)',\n 'var(--wr-color-success)',\n 'var(--wr-color-warning)',\n 'var(--wr-color-danger)',\n];\n\n/**\n * Multi-series line chart with axes, gridlines, and a hover tooltip.\n * SVG-only — no external dependency.\n *\n * @example\n * ```html\n * <wr-line-chart\n * [series]=\"[\n * { label: 'Visits', data: [12, 18, 9, 22, 30, 27, 35] },\n * { label: 'Signups', data: [3, 5, 4, 8, 11, 9, 14], color: 'var(--wr-color-success)' }\n * ]\"\n * [xLabels]=\"['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\"\n * />\n * ```\n *\n * @see https://ngwr.dev/reference/components/line-chart\n */\n@Component({\n selector: 'wr-line-chart',\n templateUrl: './line-chart.html',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-line-chart' },\n})\nexport class WrLineChart {\n readonly series = input<readonly WrLineSeries[]>([]);\n\n /**\n * Accessible name of the chart. The legend carries the series NAMES only — the numbers\n * are nowhere in text — so without this the plot is nothing at all to a screen reader.\n * Falls back to `lineChart.label`.\n */\n readonly ariaLabel = input<string | null>(null);\n\n protected readonly resolvedAriaLabel = useI18nText(this.ariaLabel, 'lineChart.label', 'Line chart');\n\n private readonly locale = inject(LOCALE_ID);\n private readonly thousandsText = useI18nFormatter('lineChart.thousands', '{{value}}k');\n\n /** Labels for the X axis (one per data point). */\n readonly xLabels = input<readonly string[]>([]);\n\n /** Chart pixel height. @default 240 */\n readonly height = input(240, { transform: (v: unknown): number => Math.max(80, coerceNumberProperty(v, 240)) });\n\n /** Show gridlines + axis ticks. @default true */\n readonly showGrid = input(true, { transform: coerceBooleanProperty });\n\n /** Show the legend above the chart. @default true */\n readonly showLegend = input(true, { transform: coerceBooleanProperty });\n\n /** Show dots at each data point. @default true */\n readonly showDots = input(true, { transform: coerceBooleanProperty });\n\n // Drawn in a 600×300 viewBox with reserved space for axis labels.\n protected readonly vbW = 600;\n protected readonly vbH = 300;\n protected readonly padding = { top: 16, right: 16, bottom: 28, left: 36 } as const;\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n\n /** Hovered point index (across all series) — null when no hover. */\n protected readonly hoveredIndex = signal<number | null>(null);\n\n protected readonly resolvedSeries = computed(() =>\n this.series().map((s, i) => ({\n label: s.label,\n // A non-finite point becomes a HOLE at its own index, not a missing element.\n // It cannot stay a number: `Math.min`/`Math.max` over the pooled data are both\n // NaN as soon as one datum is, so every coordinate in EVERY series came out\n // `NaN` — invalid path geometry, and the whole chart vanished rather than the\n // one bad point. But filtering it out closed the gap and slid every LATER point\n // one x-slot to the left, so each of them was drawn under, and reported in the\n // tooltip as, the wrong x label. A gap is kept as a gap and the line is broken\n // across it: interpolating would invent a reading the data never took.\n data: s.data.map(v => (Number.isFinite(v) ? v : null)),\n color: s.color ?? FALLBACK_COLORS[i % FALLBACK_COLORS.length],\n }))\n );\n\n protected readonly bounds = computed(() => {\n const all = this.resolvedSeries().flatMap(s => s.data.filter(v => v !== null));\n if (all.length === 0) return { min: 0, max: 1 };\n const min = Math.min(...all);\n const max = Math.max(...all);\n if (min === max) return { min: min - 1, max: max + 1 };\n // Pad upper bound by ~10% for breathing room.\n return { min: Math.min(0, min), max: max + (max - min) * 0.1 };\n });\n\n protected readonly pointCount = computed(() => {\n const maxLen = this.resolvedSeries().reduce((acc, s) => Math.max(acc, s.data.length), 0);\n return Math.max(maxLen, this.xLabels().length);\n });\n\n /** Y-axis tick marks (5 of them). */\n protected readonly yTicks = computed(() => {\n const { min, max } = this.bounds();\n const step = (max - min) / 4;\n return [0, 1, 2, 3, 4].map(i => {\n const value = max - i * step;\n return { value, y: this.padding.top + ((max - value) / (max - min)) * this.plotHeight() };\n });\n });\n\n protected readonly plotWidth = (): number => this.vbW - this.padding.left - this.padding.right;\n protected readonly plotHeight = (): number => this.vbH - this.padding.top - this.padding.bottom;\n\n /**\n * The SVG path for a series — one subpath per unbroken run, so a hole in the data\n * leaves a hole in the line rather than a straight segment across it.\n */\n protected pathFor(series: { data: readonly (number | null)[] }): string {\n const { min, max } = this.bounds();\n const count = this.pointCount();\n if (count <= 1) return '';\n const pw = this.plotWidth();\n const ph = this.plotHeight();\n\n const commands: string[] = [];\n let run = 0;\n let last = '';\n // A subpath of one `M` is not stroked at all, so a point with a gap on both sides\n // would simply vanish when `showDots` is off. Repeating it gives the round linecap\n // something to paint.\n const closeRun = (): void => {\n if (run === 1) commands.push(`L ${last}`);\n run = 0;\n };\n\n series.data.forEach((v, i) => {\n if (v === null) {\n closeRun();\n return;\n }\n const x = this.padding.left + (i / (count - 1)) * pw;\n const y = this.padding.top + ((max - v) / (max - min)) * ph;\n last = `${x.toFixed(2)} ${y.toFixed(2)}`;\n commands.push(`${run === 0 ? 'M' : 'L'} ${last}`);\n run++;\n });\n closeRun();\n\n return commands.join(' ');\n }\n\n protected pointX(index: number): number {\n const count = this.pointCount();\n if (count <= 1) return this.padding.left;\n return this.padding.left + (index / (count - 1)) * this.plotWidth();\n }\n\n protected pointY(value: number): number {\n const { min, max } = this.bounds();\n return this.padding.top + ((max - value) / (max - min)) * this.plotHeight();\n }\n\n /**\n * A point's x as a percentage of the host's width. The x-label strip and the SVG share\n * that width — the drawing stretches to it — so this is the same space the tooltip and\n * the labels are placed in, at any size.\n */\n protected xPercent(index: number): number {\n return (this.pointX(index) / this.vbW) * 100;\n }\n\n /**\n * How wide an x label may be, as a percentage of the host's width: its own slot — half\n * the distance to a neighbour on each side — cut back to what is left between its point\n * and the end of the strip. Centring a label on its point says nothing about its WIDTH,\n * and with no bound at all twelve month names printed over each other and the last one\n * hung outside the chart. Past this the stylesheet ellipsises.\n */\n protected xMaxPercent(index: number): number {\n const count = this.pointCount();\n // A single label has no neighbour to collide with and the whole strip to sit in.\n if (count <= 1) return 100;\n const slot = (this.plotWidth() / (count - 1) / this.vbW) * 100;\n const x = this.xPercent(index);\n return Math.min(slot, x + slot / 2, 100 - x + slot / 2);\n }\n\n protected readonly hoverPoints = computed(() => {\n const i = this.hoveredIndex();\n if (i === null) return [];\n return this.resolvedSeries().flatMap(s => {\n // Past the end of a short series, or on a hole in a longer one — either way there\n // is no reading at this x, so the series contributes neither a marker nor a row.\n const value = s.data[i];\n if (value === undefined || value === null) return [];\n return [{ label: s.label, value, color: s.color, x: this.pointX(i), y: this.pointY(value) }];\n });\n });\n\n protected readonly hoverLabel = computed(() => {\n const i = this.hoveredIndex();\n if (i === null) return '';\n return this.xLabels()[i] ?? String(i);\n });\n\n protected onPointerMove(event: PointerEvent): void {\n const svg = (event.currentTarget as SVGElement).getBoundingClientRect();\n const ratio = (event.clientX - svg.left) / svg.width;\n const vbX = ratio * this.vbW;\n const count = this.pointCount();\n if (count === 0) return;\n const i = Math.round(((vbX - this.padding.left) / this.plotWidth()) * (count - 1));\n if (i >= 0 && i < count) this.hoveredIndex.set(i);\n }\n\n protected onPointerLeave(): void {\n this.hoveredIndex.set(null);\n }\n\n /**\n * A Y-axis tick, in the reader's own language.\n *\n * Two halves and both were English. The `k` was a hardcoded Latin\n * abbreviation — `тыс.` in Russian — so it goes through the catalog like every\n * other word the library paints. The digits went through `toFixed`, which\n * always writes a full stop, so a German axis read `1.5` where the rest of the\n * page read `1,5`; they go through `Intl` now, the way `wrNumber`, `wrDate`\n * and `wrPlural` already did.\n *\n * Grouping is OFF deliberately: a tick is a short axis label, and `5,000k`\n * would be both wider and a change to what every existing chart draws.\n */\n protected formatTick(v: number): string {\n const digits = (n: number, fraction: number): string =>\n new Intl.NumberFormat(this.locale, {\n useGrouping: false,\n minimumFractionDigits: fraction,\n maximumFractionDigits: fraction,\n }).format(n);\n\n if (Math.abs(v) >= 1000) return this.thousandsText({ value: digits(v / 1000, v % 1000 === 0 ? 0 : 1) });\n return digits(v, v % 1 === 0 ? 0 : 1);\n }\n\n protected readonly viewBox = computed(() => `0 0 ${this.vbW} ${this.vbH}`);\n\n // Convenience accessor so the host can be referenced from the template.\n protected readonly hostEl = this.host.nativeElement;\n}\n\nexport type { WrLineSeries } from './interfaces';\n","@let hovered = hoveredIndex();\n@if (showLegend() && resolvedSeries().length > 0) {\n <ul class=\"wr-line-chart__legend\">\n @for (s of resolvedSeries(); track s.label) {\n <li class=\"wr-line-chart__legend-item\">\n <span class=\"wr-line-chart__swatch\" [style.background]=\"s.color\"></span>\n <span>{{ s.label }}</span>\n </li>\n }\n </ul>\n}\n\n<div class=\"wr-line-chart__plot\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\" [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"viewBox()\"\n preserveAspectRatio=\"none\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerleave)=\"onPointerLeave()\"\n >\n @if (showGrid()) {\n @for (tick of yTicks(); track tick.value) {\n <line\n class=\"wr-line-chart__grid-line\"\n [attr.x1]=\"padding.left\"\n [attr.x2]=\"vbW - padding.right\"\n [attr.y1]=\"tick.y\"\n [attr.y2]=\"tick.y\"\n stroke=\"rgba(var(--wr-color-light-rgb), 0.7)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n <text\n class=\"wr-line-chart__tick\"\n [attr.x]=\"padding.left - 6\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n fill=\"var(--wr-color-on-surface-muted)\"\n font-size=\"10\"\n font-family=\"var(--wr-font-family-base)\"\n >\n {{ formatTick(tick.value) }}\n </text>\n }\n }\n\n @for (s of resolvedSeries(); track s.label) {\n <path\n class=\"wr-line-chart__line\"\n [attr.d]=\"pathFor(s)\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n vector-effect=\"non-scaling-stroke\"\n />\n @if (showDots()) {\n @for (v of s.data; track $index; let i = $index) {\n <!-- A hole in the data gets no dot: the index still belongs to it, so the\n points after it stay on their own x. -->\n @if (v !== null) {\n <circle\n class=\"wr-line-chart__dot\"\n [attr.cx]=\"pointX(i)\"\n [attr.cy]=\"pointY(v)\"\n r=\"3\"\n [attr.fill]=\"s.color\"\n />\n }\n }\n }\n }\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <line\n class=\"wr-line-chart__crosshair\"\n [attr.x1]=\"pointX(hovered)\"\n [attr.x2]=\"pointX(hovered)\"\n [attr.y1]=\"padding.top\"\n [attr.y2]=\"vbH - padding.bottom\"\n stroke=\"var(--wr-color-medium)\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n vector-effect=\"non-scaling-stroke\"\n />\n @for (p of hoverPoints(); track p.label) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"5\"\n fill=\"var(--wr-color-white)\"\n [attr.stroke]=\"p.color\"\n stroke-width=\"2\"\n />\n }\n }\n </svg>\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <div class=\"wr-line-chart__tooltip\" [style.left.%]=\"(pointX(hovered) / vbW) * 100\" [style.top.px]=\"padding.top\">\n <div class=\"wr-line-chart__tooltip-label\">{{ hoverLabel() }}</div>\n @for (p of hoverPoints(); track p.label) {\n <div class=\"wr-line-chart__tooltip-row\">\n <span class=\"wr-line-chart__swatch\" [style.background]=\"p.color\"></span>\n <span class=\"wr-line-chart__tooltip-name\">{{ p.label }}</span>\n <span class=\"wr-line-chart__tooltip-value\">{{ p.value }}</span>\n </div>\n }\n </div>\n }\n</div>\n\n@if (xLabels().length > 0) {\n <div class=\"wr-line-chart__x-labels\">\n @for (label of xLabels(); track $index) {\n <!-- Each label hands the stylesheet two numbers, both percentages of the host's\n width — the space the stretching SVG is in, so they track the drawing at any\n size. The x of the point it names, which the stylesheet centres it on; and the\n widest it may be before it reaches a neighbour or the end of the strip, past\n which the stylesheet ellipsises it. Laying the labels out as equal centred flex\n slots instead is bar-chart geometry: only the middle label landed on its dot,\n and the outer ones sat up to half a column away. -->\n <span\n class=\"wr-line-chart__x-label\"\n [style.--wr-line-chart-x]=\"xPercent($index)\"\n [style.--wr-line-chart-x-max]=\"xMaxPercent($index)\"\n >{{ label }}</span\n >\n }\n </div>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ngwr-line-chart.mjs","sources":["../../../projects/lib/line-chart/line-chart.ts","../../../projects/lib/line-chart/line-chart.html","../../../projects/lib/line-chart/ngwr-line-chart.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport { Component, ElementRef, LOCALE_ID, ViewEncapsulation, computed, inject, input, signal } from '@angular/core';\n\nimport { useI18nFormatter, useI18nText } from 'ngwr/i18n';\n\nimport type { WrLineSeries } from './interfaces';\n\nconst FALLBACK_COLORS = [\n 'var(--wr-color-primary)',\n 'var(--wr-color-secondary)',\n 'var(--wr-color-success)',\n 'var(--wr-color-warning)',\n 'var(--wr-color-danger)',\n];\n\n/**\n * Multi-series line chart with axes, gridlines, and a hover tooltip.\n * SVG-only — no external dependency.\n *\n * @example\n * ```html\n * <wr-line-chart\n * [series]=\"[\n * { label: 'Visits', data: [12, 18, 9, 22, 30, 27, 35] },\n * { label: 'Signups', data: [3, 5, 4, 8, 11, 9, 14], color: 'var(--wr-color-success)' }\n * ]\"\n * [xLabels]=\"['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\"\n * />\n * ```\n *\n * @see https://ngwr.dev/reference/components/line-chart\n */\n@Component({\n selector: 'wr-line-chart',\n templateUrl: './line-chart.html',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-line-chart' },\n})\nexport class WrLineChart {\n readonly series = input<readonly WrLineSeries[]>([]);\n\n /**\n * Accessible name of the chart. The legend carries the series NAMES only — the numbers\n * are nowhere in text — so without this the plot is nothing at all to a screen reader.\n * Falls back to `lineChart.label`.\n */\n readonly ariaLabel = input<string | null>(null);\n\n protected readonly resolvedAriaLabel = useI18nText(this.ariaLabel, 'lineChart.label', 'Line chart');\n\n private readonly locale = inject(LOCALE_ID);\n private readonly thousandsText = useI18nFormatter('lineChart.thousands', '{{value}}k');\n\n /** Labels for the X axis (one per data point). */\n readonly xLabels = input<readonly string[]>([]);\n\n /** Chart pixel height. @default 240 */\n readonly height = input(240, { transform: (v: unknown): number => Math.max(80, coerceNumberProperty(v, 240)) });\n\n /** Show gridlines + axis ticks. @default true */\n readonly showGrid = input(true, { transform: coerceBooleanProperty });\n\n /** Show the legend above the chart. @default true */\n readonly showLegend = input(true, { transform: coerceBooleanProperty });\n\n /** Show dots at each data point. @default true */\n readonly showDots = input(true, { transform: coerceBooleanProperty });\n\n // Drawn in a 600×300 viewBox with reserved space for axis labels.\n protected readonly vbW = 600;\n protected readonly vbH = 300;\n protected readonly padding = { top: 16, right: 16, bottom: 28, left: 36 } as const;\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n\n /** Hovered point index (across all series) — null when no hover. */\n protected readonly hoveredIndex = signal<number | null>(null);\n\n /**\n * A dash pattern per series, so the lines are told apart by SHAPE and not\n * only by hue.\n *\n * The default palette's colours sit within about 1.06:1 of each other in\n * relative luminance — that is the whole nine-intent scale's constraint, and\n * it means a reader with red-green colour blindness sees several of these\n * strokes as one grey. A legend that pairs a coloured square with a name does\n * not rescue it: matching the square to the line is exactly the step that\n * needs hue. Distinguishing series by dash is the standard remedy and the\n * only one that survives greyscale, printing and a monochrome display.\n *\n * The FIRST entry is solid on purpose. A single-series chart is the common\n * case and there is nothing to tell apart in it, so it is left exactly as it\n * was; the patterns only appear once a second series does.\n */\n private static readonly DASHES = ['', '7 4', '2 3', '10 4 2 4', '1 4', '6 3 2 3'] as const;\n\n protected readonly resolvedSeries = computed(() =>\n this.series().map((s, i) => ({\n label: s.label,\n dash: WrLineChart.DASHES[i % WrLineChart.DASHES.length],\n // A non-finite point becomes a HOLE at its own index, not a missing element.\n // It cannot stay a number: `Math.min`/`Math.max` over the pooled data are both\n // NaN as soon as one datum is, so every coordinate in EVERY series came out\n // `NaN` — invalid path geometry, and the whole chart vanished rather than the\n // one bad point. But filtering it out closed the gap and slid every LATER point\n // one x-slot to the left, so each of them was drawn under, and reported in the\n // tooltip as, the wrong x label. A gap is kept as a gap and the line is broken\n // across it: interpolating would invent a reading the data never took.\n data: s.data.map(v => (Number.isFinite(v) ? v : null)),\n color: s.color ?? FALLBACK_COLORS[i % FALLBACK_COLORS.length],\n }))\n );\n\n protected readonly bounds = computed(() => {\n const all = this.resolvedSeries().flatMap(s => s.data.filter(v => v !== null));\n if (all.length === 0) return { min: 0, max: 1 };\n const min = Math.min(...all);\n const max = Math.max(...all);\n if (min === max) return { min: min - 1, max: max + 1 };\n // Pad upper bound by ~10% for breathing room.\n return { min: Math.min(0, min), max: max + (max - min) * 0.1 };\n });\n\n protected readonly pointCount = computed(() => {\n const maxLen = this.resolvedSeries().reduce((acc, s) => Math.max(acc, s.data.length), 0);\n return Math.max(maxLen, this.xLabels().length);\n });\n\n /** Y-axis tick marks (5 of them). */\n protected readonly yTicks = computed(() => {\n const { min, max } = this.bounds();\n const step = (max - min) / 4;\n return [0, 1, 2, 3, 4].map(i => {\n const value = max - i * step;\n return { value, y: this.padding.top + ((max - value) / (max - min)) * this.plotHeight() };\n });\n });\n\n protected readonly plotWidth = (): number => this.vbW - this.padding.left - this.padding.right;\n protected readonly plotHeight = (): number => this.vbH - this.padding.top - this.padding.bottom;\n\n /**\n * The SVG path for a series — one subpath per unbroken run, so a hole in the data\n * leaves a hole in the line rather than a straight segment across it.\n */\n protected pathFor(series: { data: readonly (number | null)[] }): string {\n const { min, max } = this.bounds();\n const count = this.pointCount();\n if (count <= 1) return '';\n const pw = this.plotWidth();\n const ph = this.plotHeight();\n\n const commands: string[] = [];\n let run = 0;\n let last = '';\n // A subpath of one `M` is not stroked at all, so a point with a gap on both sides\n // would simply vanish when `showDots` is off. Repeating it gives the round linecap\n // something to paint.\n const closeRun = (): void => {\n if (run === 1) commands.push(`L ${last}`);\n run = 0;\n };\n\n series.data.forEach((v, i) => {\n if (v === null) {\n closeRun();\n return;\n }\n const x = this.padding.left + (i / (count - 1)) * pw;\n const y = this.padding.top + ((max - v) / (max - min)) * ph;\n last = `${x.toFixed(2)} ${y.toFixed(2)}`;\n commands.push(`${run === 0 ? 'M' : 'L'} ${last}`);\n run++;\n });\n closeRun();\n\n return commands.join(' ');\n }\n\n protected pointX(index: number): number {\n const count = this.pointCount();\n if (count <= 1) return this.padding.left;\n return this.padding.left + (index / (count - 1)) * this.plotWidth();\n }\n\n protected pointY(value: number): number {\n const { min, max } = this.bounds();\n return this.padding.top + ((max - value) / (max - min)) * this.plotHeight();\n }\n\n /**\n * A point's x as a percentage of the host's width. The x-label strip and the SVG share\n * that width — the drawing stretches to it — so this is the same space the tooltip and\n * the labels are placed in, at any size.\n */\n protected xPercent(index: number): number {\n return (this.pointX(index) / this.vbW) * 100;\n }\n\n /**\n * How wide an x label may be, as a percentage of the host's width: its own slot — half\n * the distance to a neighbour on each side — cut back to what is left between its point\n * and the end of the strip. Centring a label on its point says nothing about its WIDTH,\n * and with no bound at all twelve month names printed over each other and the last one\n * hung outside the chart. Past this the stylesheet ellipsises.\n */\n protected xMaxPercent(index: number): number {\n const count = this.pointCount();\n // A single label has no neighbour to collide with and the whole strip to sit in.\n if (count <= 1) return 100;\n const slot = (this.plotWidth() / (count - 1) / this.vbW) * 100;\n const x = this.xPercent(index);\n return Math.min(slot, x + slot / 2, 100 - x + slot / 2);\n }\n\n protected readonly hoverPoints = computed(() => {\n const i = this.hoveredIndex();\n if (i === null) return [];\n return this.resolvedSeries().flatMap(s => {\n // Past the end of a short series, or on a hole in a longer one — either way there\n // is no reading at this x, so the series contributes neither a marker nor a row.\n const value = s.data[i];\n if (value === undefined || value === null) return [];\n return [{ label: s.label, value, color: s.color, x: this.pointX(i), y: this.pointY(value) }];\n });\n });\n\n protected readonly hoverLabel = computed(() => {\n const i = this.hoveredIndex();\n if (i === null) return '';\n return this.xLabels()[i] ?? String(i);\n });\n\n protected onPointerMove(event: PointerEvent): void {\n const svg = (event.currentTarget as SVGElement).getBoundingClientRect();\n const ratio = (event.clientX - svg.left) / svg.width;\n const vbX = ratio * this.vbW;\n const count = this.pointCount();\n if (count === 0) return;\n const i = Math.round(((vbX - this.padding.left) / this.plotWidth()) * (count - 1));\n if (i >= 0 && i < count) this.hoveredIndex.set(i);\n }\n\n protected onPointerLeave(): void {\n this.hoveredIndex.set(null);\n }\n\n /**\n * A Y-axis tick, in the reader's own language.\n *\n * Two halves and both were English. The `k` was a hardcoded Latin\n * abbreviation — `тыс.` in Russian — so it goes through the catalog like every\n * other word the library paints. The digits went through `toFixed`, which\n * always writes a full stop, so a German axis read `1.5` where the rest of the\n * page read `1,5`; they go through `Intl` now, the way `wrNumber`, `wrDate`\n * and `wrPlural` already did.\n *\n * Grouping is OFF deliberately: a tick is a short axis label, and `5,000k`\n * would be both wider and a change to what every existing chart draws.\n */\n protected formatTick(v: number): string {\n const digits = (n: number, fraction: number): string =>\n new Intl.NumberFormat(this.locale, {\n useGrouping: false,\n minimumFractionDigits: fraction,\n maximumFractionDigits: fraction,\n }).format(n);\n\n if (Math.abs(v) >= 1000) return this.thousandsText({ value: digits(v / 1000, v % 1000 === 0 ? 0 : 1) });\n return digits(v, v % 1 === 0 ? 0 : 1);\n }\n\n protected readonly viewBox = computed(() => `0 0 ${this.vbW} ${this.vbH}`);\n\n // Convenience accessor so the host can be referenced from the template.\n protected readonly hostEl = this.host.nativeElement;\n}\n\nexport type { WrLineSeries } from './interfaces';\n","@let hovered = hoveredIndex();\n@if (showLegend() && resolvedSeries().length > 0) {\n <ul class=\"wr-line-chart__legend\">\n @for (s of resolvedSeries(); track s.label) {\n <li class=\"wr-line-chart__legend-item\">\n <!-- The swatch draws the series' own dash, not a solid block. A legend\n that pairs a coloured square with a name leaves the reader to match\n the square to the LINE, and matching by colour is the step the\n patterns exist to remove. -->\n <svg class=\"wr-line-chart__swatch\" viewBox=\"0 0 16 2\" aria-hidden=\"true\">\n <line\n x1=\"0\"\n y1=\"1\"\n x2=\"16\"\n y2=\"1\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-dasharray]=\"s.dash || null\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n <span>{{ s.label }}</span>\n </li>\n }\n </ul>\n}\n\n<div class=\"wr-line-chart__plot\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\" [style.height.px]=\"height()\">\n <!-- Named, where it was the only unnamed element in the component. The legend\n swatch is an `<svg>` too now, so `querySelector('svg')` no longer means\n \"the chart\" — and a plot that nothing can address by name is one a\n consumer's stylesheet and a spec both have to reach by position. -->\n <svg\n class=\"wr-line-chart__svg\"\n [attr.viewBox]=\"viewBox()\"\n preserveAspectRatio=\"none\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerleave)=\"onPointerLeave()\"\n >\n @if (showGrid()) {\n @for (tick of yTicks(); track tick.value) {\n <line\n class=\"wr-line-chart__grid-line\"\n [attr.x1]=\"padding.left\"\n [attr.x2]=\"vbW - padding.right\"\n [attr.y1]=\"tick.y\"\n [attr.y2]=\"tick.y\"\n stroke=\"rgba(var(--wr-color-light-rgb), 0.7)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n <text\n class=\"wr-line-chart__tick\"\n [attr.x]=\"padding.left - 6\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n fill=\"var(--wr-color-on-surface-muted)\"\n font-size=\"10\"\n font-family=\"var(--wr-font-family-base)\"\n >\n {{ formatTick(tick.value) }}\n </text>\n }\n }\n\n @for (s of resolvedSeries(); track s.label) {\n <path\n class=\"wr-line-chart__line\"\n [attr.d]=\"pathFor(s)\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-dasharray]=\"s.dash || null\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n vector-effect=\"non-scaling-stroke\"\n />\n @if (showDots()) {\n @for (v of s.data; track $index; let i = $index) {\n <!-- A hole in the data gets no dot: the index still belongs to it, so the\n points after it stay on their own x. -->\n @if (v !== null) {\n <circle\n class=\"wr-line-chart__dot\"\n [attr.cx]=\"pointX(i)\"\n [attr.cy]=\"pointY(v)\"\n r=\"3\"\n [attr.fill]=\"s.color\"\n />\n }\n }\n }\n }\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <line\n class=\"wr-line-chart__crosshair\"\n [attr.x1]=\"pointX(hovered)\"\n [attr.x2]=\"pointX(hovered)\"\n [attr.y1]=\"padding.top\"\n [attr.y2]=\"vbH - padding.bottom\"\n stroke=\"var(--wr-color-medium)\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n vector-effect=\"non-scaling-stroke\"\n />\n @for (p of hoverPoints(); track p.label) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"5\"\n fill=\"var(--wr-color-white)\"\n [attr.stroke]=\"p.color\"\n stroke-width=\"2\"\n />\n }\n }\n </svg>\n\n @if (hovered !== null && hoverPoints().length > 0) {\n <div class=\"wr-line-chart__tooltip\" [style.left.%]=\"(pointX(hovered) / vbW) * 100\" [style.top.px]=\"padding.top\">\n <div class=\"wr-line-chart__tooltip-label\">{{ hoverLabel() }}</div>\n @for (p of hoverPoints(); track p.label) {\n <div class=\"wr-line-chart__tooltip-row\">\n <span class=\"wr-line-chart__swatch\" [style.background]=\"p.color\"></span>\n <span class=\"wr-line-chart__tooltip-name\">{{ p.label }}</span>\n <span class=\"wr-line-chart__tooltip-value\">{{ p.value }}</span>\n </div>\n }\n </div>\n }\n</div>\n\n@if (xLabels().length > 0) {\n <div class=\"wr-line-chart__x-labels\">\n @for (label of xLabels(); track $index) {\n <!-- Each label hands the stylesheet two numbers, both percentages of the host's\n width — the space the stretching SVG is in, so they track the drawing at any\n size. The x of the point it names, which the stylesheet centres it on; and the\n widest it may be before it reaches a neighbour or the end of the strip, past\n which the stylesheet ellipsises it. Laying the labels out as equal centred flex\n slots instead is bar-chart geometry: only the middle label landed on its dot,\n and the outer ones sat up to half a column away. -->\n <span\n class=\"wr-line-chart__x-label\"\n [style.--wr-line-chart-x]=\"xPercent($index)\"\n [style.--wr-line-chart-x-max]=\"xMaxPercent($index)\"\n >{{ label }}</span\n >\n }\n </div>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}