ngwr 11.1.0 → 11.2.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 (205) hide show
  1. package/circular-text/styles/_index.scss +11 -0
  2. package/command-palette/styles/_index.scss +12 -0
  3. package/fesm2022/ngwr-action-sheet-testing.mjs +313 -0
  4. package/fesm2022/ngwr-action-sheet-testing.mjs.map +1 -0
  5. package/fesm2022/ngwr-bar-chart-testing.mjs +89 -0
  6. package/fesm2022/ngwr-bar-chart-testing.mjs.map +1 -0
  7. package/fesm2022/ngwr-blur-text-testing.mjs +203 -0
  8. package/fesm2022/ngwr-blur-text-testing.mjs.map +1 -0
  9. package/fesm2022/ngwr-blur-text.mjs +15 -1
  10. package/fesm2022/ngwr-blur-text.mjs.map +1 -1
  11. package/fesm2022/ngwr-border-glow-testing.mjs +325 -0
  12. package/fesm2022/ngwr-border-glow-testing.mjs.map +1 -0
  13. package/fesm2022/ngwr-calendar-heatmap-testing.mjs +95 -0
  14. package/fesm2022/ngwr-calendar-heatmap-testing.mjs.map +1 -0
  15. package/fesm2022/ngwr-calendar-testing.mjs +368 -0
  16. package/fesm2022/ngwr-calendar-testing.mjs.map +1 -0
  17. package/fesm2022/ngwr-calendar.mjs +43 -3
  18. package/fesm2022/ngwr-calendar.mjs.map +1 -1
  19. package/fesm2022/ngwr-cascader.mjs +12 -0
  20. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  21. package/fesm2022/ngwr-checkbox.mjs +8 -0
  22. package/fesm2022/ngwr-checkbox.mjs.map +1 -1
  23. package/fesm2022/ngwr-circular-text-testing.mjs +230 -0
  24. package/fesm2022/ngwr-circular-text-testing.mjs.map +1 -0
  25. package/fesm2022/ngwr-circular-text.mjs +2 -2
  26. package/fesm2022/ngwr-circular-text.mjs.map +1 -1
  27. package/fesm2022/ngwr-color-picker-testing.mjs +432 -0
  28. package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -0
  29. package/fesm2022/ngwr-color-picker.mjs +29 -3
  30. package/fesm2022/ngwr-color-picker.mjs.map +1 -1
  31. package/fesm2022/ngwr-context-menu.mjs +25 -5
  32. package/fesm2022/ngwr-context-menu.mjs.map +1 -1
  33. package/fesm2022/ngwr-date-picker-testing.mjs +10 -79
  34. package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
  35. package/fesm2022/ngwr-decrypt-text-testing.mjs +261 -0
  36. package/fesm2022/ngwr-decrypt-text-testing.mjs.map +1 -0
  37. package/fesm2022/ngwr-decrypt-text.mjs +6 -1
  38. package/fesm2022/ngwr-decrypt-text.mjs.map +1 -1
  39. package/fesm2022/ngwr-donut-chart-testing.mjs +93 -0
  40. package/fesm2022/ngwr-donut-chart-testing.mjs.map +1 -0
  41. package/fesm2022/ngwr-drawer.mjs +36 -7
  42. package/fesm2022/ngwr-drawer.mjs.map +1 -1
  43. package/fesm2022/ngwr-dropdown.mjs +32 -2
  44. package/fesm2022/ngwr-dropdown.mjs.map +1 -1
  45. package/fesm2022/ngwr-event-calendar-testing.mjs +446 -0
  46. package/fesm2022/ngwr-event-calendar-testing.mjs.map +1 -0
  47. package/fesm2022/ngwr-event-calendar.mjs +43 -18
  48. package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
  49. package/fesm2022/ngwr-falling-text-testing.mjs +218 -0
  50. package/fesm2022/ngwr-falling-text-testing.mjs.map +1 -0
  51. package/fesm2022/ngwr-fuzzy-text-testing.mjs +134 -0
  52. package/fesm2022/ngwr-fuzzy-text-testing.mjs.map +1 -0
  53. package/fesm2022/ngwr-gauge-testing.mjs +86 -0
  54. package/fesm2022/ngwr-gauge-testing.mjs.map +1 -0
  55. package/fesm2022/ngwr-glitch-text-testing.mjs +181 -0
  56. package/fesm2022/ngwr-glitch-text-testing.mjs.map +1 -0
  57. package/fesm2022/ngwr-gradient-text-testing.mjs +223 -0
  58. package/fesm2022/ngwr-gradient-text-testing.mjs.map +1 -0
  59. package/fesm2022/ngwr-i18n-en.mjs +14 -0
  60. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  61. package/fesm2022/ngwr-i18n-ru.mjs +14 -0
  62. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  63. package/fesm2022/ngwr-image-cropper-testing.mjs +156 -0
  64. package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -0
  65. package/fesm2022/ngwr-image-cropper.mjs +10 -3
  66. package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
  67. package/fesm2022/ngwr-input-number.mjs +73 -6
  68. package/fesm2022/ngwr-input-number.mjs.map +1 -1
  69. package/fesm2022/ngwr-knob-testing.mjs +220 -0
  70. package/fesm2022/ngwr-knob-testing.mjs.map +1 -0
  71. package/fesm2022/ngwr-lightbox-testing.mjs +223 -0
  72. package/fesm2022/ngwr-lightbox-testing.mjs.map +1 -0
  73. package/fesm2022/ngwr-lightbox.mjs +9 -2
  74. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  75. package/fesm2022/ngwr-line-chart-testing.mjs +119 -0
  76. package/fesm2022/ngwr-line-chart-testing.mjs.map +1 -0
  77. package/fesm2022/ngwr-marquee-testing.mjs +336 -0
  78. package/fesm2022/ngwr-marquee-testing.mjs.map +1 -0
  79. package/fesm2022/ngwr-meter-group-testing.mjs +80 -0
  80. package/fesm2022/ngwr-meter-group-testing.mjs.map +1 -0
  81. package/fesm2022/ngwr-overlay.mjs +28 -1
  82. package/fesm2022/ngwr-overlay.mjs.map +1 -1
  83. package/fesm2022/ngwr-popover.mjs +14 -0
  84. package/fesm2022/ngwr-popover.mjs.map +1 -1
  85. package/fesm2022/ngwr-rotating-text-testing.mjs +220 -0
  86. package/fesm2022/ngwr-rotating-text-testing.mjs.map +1 -0
  87. package/fesm2022/ngwr-rotating-text.mjs +2 -2
  88. package/fesm2022/ngwr-rotating-text.mjs.map +1 -1
  89. package/fesm2022/ngwr-select.mjs +70 -11
  90. package/fesm2022/ngwr-select.mjs.map +1 -1
  91. package/fesm2022/ngwr-shiny-text-testing.mjs +237 -0
  92. package/fesm2022/ngwr-shiny-text-testing.mjs.map +1 -0
  93. package/fesm2022/ngwr-slider.mjs +44 -6
  94. package/fesm2022/ngwr-slider.mjs.map +1 -1
  95. package/fesm2022/ngwr-sparkline-testing.mjs +76 -0
  96. package/fesm2022/ngwr-sparkline-testing.mjs.map +1 -0
  97. package/fesm2022/ngwr-speed-dial-testing.mjs +279 -0
  98. package/fesm2022/ngwr-speed-dial-testing.mjs.map +1 -0
  99. package/fesm2022/ngwr-splash-cursor-testing.mjs +89 -0
  100. package/fesm2022/ngwr-splash-cursor-testing.mjs.map +1 -0
  101. package/fesm2022/ngwr-split-text-testing.mjs +224 -0
  102. package/fesm2022/ngwr-split-text-testing.mjs.map +1 -0
  103. package/fesm2022/ngwr-split-text.mjs +15 -1
  104. package/fesm2022/ngwr-split-text.mjs.map +1 -1
  105. package/fesm2022/ngwr-splitter-testing.mjs +245 -0
  106. package/fesm2022/ngwr-splitter-testing.mjs.map +1 -0
  107. package/fesm2022/ngwr-spotlight-card-testing.mjs +368 -0
  108. package/fesm2022/ngwr-spotlight-card-testing.mjs.map +1 -0
  109. package/fesm2022/ngwr-star-border-testing.mjs +172 -0
  110. package/fesm2022/ngwr-star-border-testing.mjs.map +1 -0
  111. package/fesm2022/ngwr-table.mjs +13 -3
  112. package/fesm2022/ngwr-table.mjs.map +1 -1
  113. package/fesm2022/ngwr-tabs.mjs +15 -1
  114. package/fesm2022/ngwr-tabs.mjs.map +1 -1
  115. package/fesm2022/ngwr-tilt-card-testing.mjs +190 -0
  116. package/fesm2022/ngwr-tilt-card-testing.mjs.map +1 -0
  117. package/fesm2022/ngwr-tilt-card.mjs +19 -1
  118. package/fesm2022/ngwr-tilt-card.mjs.map +1 -1
  119. package/fesm2022/ngwr-toast.mjs +36 -7
  120. package/fesm2022/ngwr-toast.mjs.map +1 -1
  121. package/fesm2022/ngwr-tour-testing.mjs +173 -0
  122. package/fesm2022/ngwr-tour-testing.mjs.map +1 -0
  123. package/fesm2022/ngwr-tour.mjs +2 -2
  124. package/fesm2022/ngwr-tour.mjs.map +1 -1
  125. package/fesm2022/ngwr-tree.mjs +10 -3
  126. package/fesm2022/ngwr-tree.mjs.map +1 -1
  127. package/fesm2022/ngwr-typewriter-testing.mjs +228 -0
  128. package/fesm2022/ngwr-typewriter-testing.mjs.map +1 -0
  129. package/fesm2022/ngwr-version.mjs +1 -1
  130. package/fesm2022/ngwr-version.mjs.map +1 -1
  131. package/fesm2022/ngwr-waves-testing.mjs +128 -0
  132. package/fesm2022/ngwr-waves-testing.mjs.map +1 -0
  133. package/fesm2022/ngwr-window-testing.mjs +361 -0
  134. package/fesm2022/ngwr-window-testing.mjs.map +1 -0
  135. package/fesm2022/ngwr-window.mjs +84 -10
  136. package/fesm2022/ngwr-window.mjs.map +1 -1
  137. package/i18n/en.json +16 -2
  138. package/i18n/ru.json +16 -2
  139. package/llms-full.txt +147 -0
  140. package/llms.txt +27 -8
  141. package/mcp/providers.js +29 -0
  142. package/mcp/tools.js +1 -29
  143. package/package.json +145 -1
  144. package/skills/ngwr/SKILL.md +115 -0
  145. package/skills/ngwr/references/catalog.md +212 -0
  146. package/skills/ngwr/references/setup.md +73 -0
  147. package/types/ngwr-action-sheet-testing.d.ts +260 -0
  148. package/types/ngwr-bar-chart-testing.d.ts +77 -0
  149. package/types/ngwr-blur-text-testing.d.ts +180 -0
  150. package/types/ngwr-blur-text.d.ts +12 -0
  151. package/types/ngwr-border-glow-testing.d.ts +219 -0
  152. package/types/ngwr-calendar-heatmap-testing.d.ts +79 -0
  153. package/types/ngwr-calendar-testing.d.ts +277 -0
  154. package/types/ngwr-calendar.d.ts +10 -0
  155. package/types/ngwr-checkbox.d.ts +13 -0
  156. package/types/ngwr-circular-text-testing.d.ts +196 -0
  157. package/types/ngwr-color-picker-testing.d.ts +291 -0
  158. package/types/ngwr-color-picker.d.ts +13 -1
  159. package/types/ngwr-context-menu.d.ts +15 -1
  160. package/types/ngwr-date-picker-testing.d.ts +6 -66
  161. package/types/ngwr-decrypt-text-testing.d.ts +222 -0
  162. package/types/ngwr-donut-chart-testing.d.ts +79 -0
  163. package/types/ngwr-drawer.d.ts +12 -0
  164. package/types/ngwr-dropdown.d.ts +9 -0
  165. package/types/ngwr-event-calendar-testing.d.ts +301 -0
  166. package/types/ngwr-event-calendar.d.ts +9 -6
  167. package/types/ngwr-falling-text-testing.d.ts +186 -0
  168. package/types/ngwr-fuzzy-text-testing.d.ts +130 -0
  169. package/types/ngwr-gauge-testing.d.ts +71 -0
  170. package/types/ngwr-glitch-text-testing.d.ts +172 -0
  171. package/types/ngwr-gradient-text-testing.d.ts +173 -0
  172. package/types/ngwr-image-cropper-testing.d.ts +134 -0
  173. package/types/ngwr-image-cropper.d.ts +1 -0
  174. package/types/ngwr-input-number.d.ts +33 -1
  175. package/types/ngwr-knob-testing.d.ts +150 -0
  176. package/types/ngwr-lightbox-testing.d.ts +154 -0
  177. package/types/ngwr-lightbox.d.ts +6 -0
  178. package/types/ngwr-line-chart-testing.d.ts +93 -0
  179. package/types/ngwr-marquee-testing.d.ts +280 -0
  180. package/types/ngwr-meter-group-testing.d.ts +67 -0
  181. package/types/ngwr-overlay.d.ts +19 -1
  182. package/types/ngwr-rotating-text-testing.d.ts +191 -0
  183. package/types/ngwr-select.d.ts +32 -0
  184. package/types/ngwr-shiny-text-testing.d.ts +165 -0
  185. package/types/ngwr-slider.d.ts +15 -0
  186. package/types/ngwr-sparkline-testing.d.ts +69 -0
  187. package/types/ngwr-speed-dial-testing.d.ts +195 -0
  188. package/types/ngwr-splash-cursor-testing.d.ts +95 -0
  189. package/types/ngwr-split-text-testing.d.ts +209 -0
  190. package/types/ngwr-split-text.d.ts +12 -0
  191. package/types/ngwr-splitter-testing.d.ts +174 -0
  192. package/types/ngwr-spotlight-card-testing.d.ts +303 -0
  193. package/types/ngwr-star-border-testing.d.ts +135 -0
  194. package/types/ngwr-table.d.ts +10 -1
  195. package/types/ngwr-tilt-card-testing.d.ts +158 -0
  196. package/types/ngwr-tilt-card.d.ts +2 -0
  197. package/types/ngwr-toast.d.ts +14 -5
  198. package/types/ngwr-tour-testing.d.ts +143 -0
  199. package/types/ngwr-tree.d.ts +7 -0
  200. package/types/ngwr-typewriter-testing.d.ts +201 -0
  201. package/types/ngwr-version.d.ts +1 -1
  202. package/types/ngwr-waves-testing.d.ts +121 -0
  203. package/types/ngwr-window-testing.d.ts +263 -0
  204. package/types/ngwr-window.d.ts +27 -10
  205. package/window/styles/_index.scss +44 -16
@@ -0,0 +1,237 @@
1
+ import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
2
+
3
+ /**
4
+ * @license
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
8
+ */
9
+ const GRADIENT = /^linear-gradient\((.*)\)$/s;
10
+ const DEGREES = /^(-?[\d.]+)deg$/;
11
+ const STOP_POSITION = /\s+-?[\d.]+%$/;
12
+ /** Split a declaration list or an argument list on top-level separators only. */
13
+ function splitTopLevel(value, separator) {
14
+ const parts = [];
15
+ let depth = 0;
16
+ let current = '';
17
+ for (const character of value) {
18
+ if (character === '(')
19
+ depth++;
20
+ if (character === ')')
21
+ depth--;
22
+ if (character === separator && depth === 0) {
23
+ parts.push(current.trim());
24
+ current = '';
25
+ }
26
+ else {
27
+ current += character;
28
+ }
29
+ }
30
+ parts.push(current.trim());
31
+ return parts;
32
+ }
33
+ /** The colour half of a `<colour> <position>` gradient stop. */
34
+ function stopColour(stop) {
35
+ return stop.replace(STOP_POSITION, '').trim();
36
+ }
37
+ /**
38
+ * Test harness for `<wr-shiny-text>`.
39
+ *
40
+ * **The duration is the assertion that matters.** The pause between sweeps is not
41
+ * `animation-delay` and not a second animation — it is folded INTO `animation-duration`,
42
+ * because the keyframe finishes at 50% and holds there. So `[speed]="2"` with
43
+ * `[delay]="3"` has to read 5 seconds, and anyone reimplementing the pause the obvious
44
+ * way passes every other check here and fails {@link getCycleDurationSeconds}.
45
+ *
46
+ * **The stripe is five stops and only one of them is the shine.** The base colour fills
47
+ * 0%, 35%, 65% and 100%; the highlight sits alone at 50%. {@link getBaseColor} and
48
+ * {@link getShineColor} are offered as a pair because swapping the two inputs is
49
+ * otherwise undetectable — the gradient stays well-formed, the angle is unchanged, and
50
+ * the text simply stops shimmering.
51
+ *
52
+ * **Both gradient reads come off the inline `style` attribute**, not through computed
53
+ * style. Unset colours fall through to the literal `var(--wr-shiny-text-base)` /
54
+ * `var(--wr-shiny-text-shine)` tokens, which is exactly what makes the dark-theme
55
+ * override work — and a computed read resolves those tokens in a real browser while
56
+ * handing them back verbatim in a unit test. Reading what the component wrote gives the
57
+ * same answer in both places.
58
+ *
59
+ * The colours still pass through the CSSOM on their way back out, and it has its own
60
+ * spelling: a hex literal is re-serialised, so `[color]="'#444'"` reads
61
+ * `rgb(68, 68, 68)`. Functional notations and `var()` tokens survive untouched. Assert
62
+ * the form the CSSOM returns, not the one the template passed in.
63
+ *
64
+ * There is no inner element to query: the template is the bare interpolation, so the text
65
+ * is a node on the host. A locator copied from the gradient-text harness finds nothing.
66
+ *
67
+ * **What the harness deliberately does not offer**, and why. There is no `isAnimating()`
68
+ * / `getBackgroundPosition()`: the sweep is a stylesheet keyframe over
69
+ * `background-position`, and a unit test neither loads the sheet nor runs the animation.
70
+ * There is no `getAnimationName()` / `getIterationCount()` / `getPlayState()`: every
71
+ * animation property except the duration lives in the stylesheet, so those reads answer
72
+ * the same empty string for a working component and a deleted one. {@link isPaused} and
73
+ * {@link pausesOnHover} claim the CLASSES and not the pauses — `:hover` is a selector and
74
+ * `animation-play-state` is a stylesheet rule, so what they honestly pin is that the
75
+ * input arrived. There is no `respectsReducedMotion()`: the handling is a `@media` block
76
+ * with no TypeScript counterpart. And there is nothing reporting the painted text colour,
77
+ * because `-webkit-text-fill-color: transparent` is a stylesheet declaration and a read
78
+ * would report the inherited colour as if the text were solid.
79
+ *
80
+ * The `[wrShimmer]` directive shipped from the same entry point has no harness: it adds
81
+ * one static class and nothing else, so a harness could only restate its own selector.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const shiny = await loader.getHarness(WrShinyTextHarness);
86
+ *
87
+ * expect(await shiny.getCycleDurationSeconds()).toBe(5);
88
+ * expect(await shiny.getShineColor()).toBe('var(--wr-shiny-text-shine)');
89
+ * ```
90
+ *
91
+ * @see https://ngwr.dev/guides/testing
92
+ */
93
+ class WrShinyTextHarness extends ComponentHarness {
94
+ static hostSelector = 'wr-shiny-text';
95
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
96
+ static with(options = {}) {
97
+ return new HarnessPredicate(WrShinyTextHarness, options)
98
+ .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
99
+ .addOption('paused', options.paused, async (harness, paused) => (await harness.isPaused()) === paused)
100
+ .addOption('direction', options.direction, async (harness, direction) => (await harness.getDirection()) === direction);
101
+ }
102
+ /** The rendered text, trimmed. It is a text node on the host — there is no inner span. */
103
+ async getText() {
104
+ return (await this.host()).text();
105
+ }
106
+ /**
107
+ * The whole stripe gradient the component computed, as written.
108
+ *
109
+ * Unresolved on purpose. With no `[color]` / `[shineColor]` given this string contains
110
+ * the theme's own custom-property tokens rather than colours, and that is the working
111
+ * state, not a gap — resolving them would need the stylesheet, and the resolved answer
112
+ * would differ between a unit test and a browser.
113
+ */
114
+ async getGradient() {
115
+ return this.require('getGradient', 'background-image');
116
+ }
117
+ /**
118
+ * The gradient's angle in degrees, from `[spread]`.
119
+ *
120
+ * The only observable effect that input has, `numAttr` coercion included. Note the name
121
+ * is inherited vocabulary and describes the ANGLE, not the width of the bright stripe:
122
+ * the stop positions are hard-coded, so no input narrows the shine.
123
+ */
124
+ async getSpreadDegrees() {
125
+ const [angle] = await this.gradientArguments('getSpreadDegrees');
126
+ const match = DEGREES.exec(angle);
127
+ if (!match) {
128
+ throw new Error(`WrShinyTextHarness.getSpreadDegrees(): the gradient starts with "${angle}", which is not an angle in ` +
129
+ 'degrees. The component writes `<spread>deg` as the first argument of the gradient; anything else ' +
130
+ 'means the angle is no longer coming from that input.');
131
+ }
132
+ return Number.parseFloat(match[1]);
133
+ }
134
+ /**
135
+ * The colour outside the bright stripe — the 0% stop.
136
+ *
137
+ * The same colour fills 35%, 65% and 100%; only the 50% stop differs. Assert it next to
138
+ * {@link getShineColor}: a `[color]` that lands in the shine slot instead leaves a
139
+ * perfectly valid gradient that no longer shimmers.
140
+ */
141
+ async getBaseColor() {
142
+ return stopColour((await this.gradientStops('getBaseColor'))[0]);
143
+ }
144
+ /** The highlight colour — the lone 50% stop the stripe is made of. */
145
+ async getShineColor() {
146
+ return stopColour((await this.gradientStops('getShineColor'))[2]);
147
+ }
148
+ /**
149
+ * One full cycle in seconds — the sweep AND the pause after it.
150
+ *
151
+ * `[delay]` is not `animation-delay` and not a second animation: the keyframe reaches
152
+ * its end at 50% and holds, so the wait is bought by making the animation twice as long.
153
+ * That is why this reads `speed + delay` and why the method is named for the cycle
154
+ * rather than for the sweep.
155
+ *
156
+ * Read from the inline declaration rather than from computed style, so a missing binding
157
+ * is reported as missing instead of being answered from the stylesheet.
158
+ */
159
+ async getCycleDurationSeconds() {
160
+ const raw = await this.require('getCycleDurationSeconds', 'animation-duration');
161
+ const seconds = Number.parseFloat(raw);
162
+ if (Number.isNaN(seconds)) {
163
+ throw new Error(`WrShinyTextHarness.getCycleDurationSeconds(): \`animation-duration\` is "${raw}", which is not a number ` +
164
+ 'of seconds. The component writes `[speed] + [delay]` there — the pause is folded into the duration ' +
165
+ 'rather than expressed as a delay.');
166
+ }
167
+ return seconds;
168
+ }
169
+ /**
170
+ * Whether the animation is stopped (`disabled`).
171
+ *
172
+ * Three names for one thing, which is why this method exists rather than a spec reading
173
+ * the class: the input is `disabled`, its JSDoc says "pause", and the DOM contract is
174
+ * `wr-shiny-text--paused`. Only the last of those is public API.
175
+ */
176
+ async isPaused() {
177
+ return (await this.host()).hasClass('wr-shiny-text--paused');
178
+ }
179
+ /** Whether the opt-in reached the class the `:hover` rule needs. */
180
+ async pausesOnHover() {
181
+ return (await this.host()).hasClass('wr-shiny-text--pause-on-hover');
182
+ }
183
+ /**
184
+ * Whether the stripe bounces instead of restarting (`yoyo`).
185
+ *
186
+ * Defaults to `false` here — the opposite of the same-named input on `wr-gradient-text`,
187
+ * which is worth pinning in a spec that uses both. Together with {@link getDirection}
188
+ * this is the whole four-way matrix the stylesheet derives `animation-direction` from.
189
+ */
190
+ async isYoyo() {
191
+ return (await this.host()).hasClass('wr-shiny-text--yoyo');
192
+ }
193
+ /** Which way the stripe travels — `'left'` is the absence of the reverse modifier. */
194
+ async getDirection() {
195
+ return (await (await this.host()).hasClass('wr-shiny-text--reverse')) ? 'right' : 'left';
196
+ }
197
+ /** The five stops of the stripe, positions attached, in gradient order. */
198
+ async gradientStops(caller) {
199
+ const [, ...stops] = await this.gradientArguments(caller);
200
+ if (stops.length !== 5) {
201
+ throw new Error(`WrShinyTextHarness.${caller}(): the gradient has ${stops.length} stops, not the five the stripe is made ` +
202
+ 'of (base at 0% / 35%, shine at 50%, base at 65% / 100%). Reading a colour out of a differently ' +
203
+ 'shaped gradient would report a stop that means something else.');
204
+ }
205
+ return stops;
206
+ }
207
+ /** The gradient's arguments — the angle first, then the stops. */
208
+ async gradientArguments(caller) {
209
+ const gradient = await this.require(caller, 'background-image');
210
+ const match = GRADIENT.exec(gradient);
211
+ if (!match) {
212
+ throw new Error(`WrShinyTextHarness.${caller}(): \`background-image\` is "${gradient}", which is not a linear-gradient(). ` +
213
+ 'The stripe IS that gradient, so anything else means the component is no longer painting one.');
214
+ }
215
+ return splitTopLevel(match[1], ',');
216
+ }
217
+ /** One of the host's two inline declarations, or a failure that says which one is missing. */
218
+ async require(caller, property) {
219
+ const style = (await (await this.host()).getAttribute('style')) ?? '';
220
+ for (const declaration of splitTopLevel(style, ';')) {
221
+ const colon = declaration.indexOf(':');
222
+ if (colon !== -1 && declaration.slice(0, colon).trim() === property) {
223
+ return declaration.slice(colon + 1).trim();
224
+ }
225
+ }
226
+ throw new Error(`WrShinyTextHarness.${caller}(): the host carries no inline \`${property}\` (style: "${style}"). The ` +
227
+ 'component writes it on every render, so its absence is a broken host binding rather than a default — ' +
228
+ 'and it is read inline, not through computed style, precisely so the stylesheet cannot answer for it.');
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Generated bundle index. Do not edit.
234
+ */
235
+
236
+ export { WrShinyTextHarness };
237
+ //# sourceMappingURL=ngwr-shiny-text-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngwr-shiny-text-testing.mjs","sources":["../../../projects/lib/shiny-text/testing/wr-shiny-text-harness.ts","../../../projects/lib/shiny-text/testing/ngwr-shiny-text-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 { WrShinyTextHarnessFilters, WrShinyTextSweepDirection } from './interfaces';\n\nconst GRADIENT = /^linear-gradient\\((.*)\\)$/s;\nconst DEGREES = /^(-?[\\d.]+)deg$/;\nconst STOP_POSITION = /\\s+-?[\\d.]+%$/;\n\n/** Split a declaration list or an argument list on top-level separators only. */\nfunction splitTopLevel(value: string, separator: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let current = '';\n\n for (const character of value) {\n if (character === '(') depth++;\n if (character === ')') depth--;\n\n if (character === separator && depth === 0) {\n parts.push(current.trim());\n current = '';\n } else {\n current += character;\n }\n }\n\n parts.push(current.trim());\n return parts;\n}\n\n/** The colour half of a `<colour> <position>` gradient stop. */\nfunction stopColour(stop: string): string {\n return stop.replace(STOP_POSITION, '').trim();\n}\n\n/**\n * Test harness for `<wr-shiny-text>`.\n *\n * **The duration is the assertion that matters.** The pause between sweeps is not\n * `animation-delay` and not a second animation — it is folded INTO `animation-duration`,\n * because the keyframe finishes at 50% and holds there. So `[speed]=\"2\"` with\n * `[delay]=\"3\"` has to read 5 seconds, and anyone reimplementing the pause the obvious\n * way passes every other check here and fails {@link getCycleDurationSeconds}.\n *\n * **The stripe is five stops and only one of them is the shine.** The base colour fills\n * 0%, 35%, 65% and 100%; the highlight sits alone at 50%. {@link getBaseColor} and\n * {@link getShineColor} are offered as a pair because swapping the two inputs is\n * otherwise undetectable — the gradient stays well-formed, the angle is unchanged, and\n * the text simply stops shimmering.\n *\n * **Both gradient reads come off the inline `style` attribute**, not through computed\n * style. Unset colours fall through to the literal `var(--wr-shiny-text-base)` /\n * `var(--wr-shiny-text-shine)` tokens, which is exactly what makes the dark-theme\n * override work — and a computed read resolves those tokens in a real browser while\n * handing them back verbatim in a unit test. Reading what the component wrote gives the\n * same answer in both places.\n *\n * The colours still pass through the CSSOM on their way back out, and it has its own\n * spelling: a hex literal is re-serialised, so `[color]=\"'#444'\"` reads\n * `rgb(68, 68, 68)`. Functional notations and `var()` tokens survive untouched. Assert\n * the form the CSSOM returns, not the one the template passed in.\n *\n * There is no inner element to query: the template is the bare interpolation, so the text\n * is a node on the host. A locator copied from the gradient-text harness finds nothing.\n *\n * **What the harness deliberately does not offer**, and why. There is no `isAnimating()`\n * / `getBackgroundPosition()`: the sweep is a stylesheet keyframe over\n * `background-position`, and a unit test neither loads the sheet nor runs the animation.\n * There is no `getAnimationName()` / `getIterationCount()` / `getPlayState()`: every\n * animation property except the duration lives in the stylesheet, so those reads answer\n * the same empty string for a working component and a deleted one. {@link isPaused} and\n * {@link pausesOnHover} claim the CLASSES and not the pauses — `:hover` is a selector and\n * `animation-play-state` is a stylesheet rule, so what they honestly pin is that the\n * input arrived. There is no `respectsReducedMotion()`: the handling is a `@media` block\n * with no TypeScript counterpart. And there is nothing reporting the painted text colour,\n * because `-webkit-text-fill-color: transparent` is a stylesheet declaration and a read\n * would report the inherited colour as if the text were solid.\n *\n * The `[wrShimmer]` directive shipped from the same entry point has no harness: it adds\n * one static class and nothing else, so a harness could only restate its own selector.\n *\n * @example\n * ```ts\n * const shiny = await loader.getHarness(WrShinyTextHarness);\n *\n * expect(await shiny.getCycleDurationSeconds()).toBe(5);\n * expect(await shiny.getShineColor()).toBe('var(--wr-shiny-text-shine)');\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrShinyTextHarness extends ComponentHarness {\n static hostSelector = 'wr-shiny-text';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrShinyTextHarnessFilters = {}): HarnessPredicate<WrShinyTextHarness> {\n return new HarnessPredicate(WrShinyTextHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))\n .addOption('paused', options.paused, async (harness, paused) => (await harness.isPaused()) === paused)\n .addOption(\n 'direction',\n options.direction,\n async (harness, direction) => (await harness.getDirection()) === direction\n );\n }\n\n /** The rendered text, trimmed. It is a text node on the host — there is no inner span. */\n async getText(): Promise<string> {\n return (await this.host()).text();\n }\n\n /**\n * The whole stripe gradient the component computed, as written.\n *\n * Unresolved on purpose. With no `[color]` / `[shineColor]` given this string contains\n * the theme's own custom-property tokens rather than colours, and that is the working\n * state, not a gap — resolving them would need the stylesheet, and the resolved answer\n * would differ between a unit test and a browser.\n */\n async getGradient(): Promise<string> {\n return this.require('getGradient', 'background-image');\n }\n\n /**\n * The gradient's angle in degrees, from `[spread]`.\n *\n * The only observable effect that input has, `numAttr` coercion included. Note the name\n * is inherited vocabulary and describes the ANGLE, not the width of the bright stripe:\n * the stop positions are hard-coded, so no input narrows the shine.\n */\n async getSpreadDegrees(): Promise<number> {\n const [angle] = await this.gradientArguments('getSpreadDegrees');\n const match = DEGREES.exec(angle);\n\n if (!match) {\n throw new Error(\n `WrShinyTextHarness.getSpreadDegrees(): the gradient starts with \"${angle}\", which is not an angle in ` +\n 'degrees. The component writes `<spread>deg` as the first argument of the gradient; anything else ' +\n 'means the angle is no longer coming from that input.'\n );\n }\n return Number.parseFloat(match[1]);\n }\n\n /**\n * The colour outside the bright stripe — the 0% stop.\n *\n * The same colour fills 35%, 65% and 100%; only the 50% stop differs. Assert it next to\n * {@link getShineColor}: a `[color]` that lands in the shine slot instead leaves a\n * perfectly valid gradient that no longer shimmers.\n */\n async getBaseColor(): Promise<string> {\n return stopColour((await this.gradientStops('getBaseColor'))[0]);\n }\n\n /** The highlight colour — the lone 50% stop the stripe is made of. */\n async getShineColor(): Promise<string> {\n return stopColour((await this.gradientStops('getShineColor'))[2]);\n }\n\n /**\n * One full cycle in seconds — the sweep AND the pause after it.\n *\n * `[delay]` is not `animation-delay` and not a second animation: the keyframe reaches\n * its end at 50% and holds, so the wait is bought by making the animation twice as long.\n * That is why this reads `speed + delay` and why the method is named for the cycle\n * rather than for the sweep.\n *\n * Read from the inline declaration rather than from computed style, so a missing binding\n * is reported as missing instead of being answered from the stylesheet.\n */\n async getCycleDurationSeconds(): Promise<number> {\n const raw = await this.require('getCycleDurationSeconds', 'animation-duration');\n const seconds = Number.parseFloat(raw);\n\n if (Number.isNaN(seconds)) {\n throw new Error(\n `WrShinyTextHarness.getCycleDurationSeconds(): \\`animation-duration\\` is \"${raw}\", which is not a number ` +\n 'of seconds. The component writes `[speed] + [delay]` there — the pause is folded into the duration ' +\n 'rather than expressed as a delay.'\n );\n }\n return seconds;\n }\n\n /**\n * Whether the animation is stopped (`disabled`).\n *\n * Three names for one thing, which is why this method exists rather than a spec reading\n * the class: the input is `disabled`, its JSDoc says \"pause\", and the DOM contract is\n * `wr-shiny-text--paused`. Only the last of those is public API.\n */\n async isPaused(): Promise<boolean> {\n return (await this.host()).hasClass('wr-shiny-text--paused');\n }\n\n /** Whether the opt-in reached the class the `:hover` rule needs. */\n async pausesOnHover(): Promise<boolean> {\n return (await this.host()).hasClass('wr-shiny-text--pause-on-hover');\n }\n\n /**\n * Whether the stripe bounces instead of restarting (`yoyo`).\n *\n * Defaults to `false` here — the opposite of the same-named input on `wr-gradient-text`,\n * which is worth pinning in a spec that uses both. Together with {@link getDirection}\n * this is the whole four-way matrix the stylesheet derives `animation-direction` from.\n */\n async isYoyo(): Promise<boolean> {\n return (await this.host()).hasClass('wr-shiny-text--yoyo');\n }\n\n /** Which way the stripe travels — `'left'` is the absence of the reverse modifier. */\n async getDirection(): Promise<WrShinyTextSweepDirection> {\n return (await (await this.host()).hasClass('wr-shiny-text--reverse')) ? 'right' : 'left';\n }\n\n /** The five stops of the stripe, positions attached, in gradient order. */\n private async gradientStops(caller: string): Promise<string[]> {\n const [, ...stops] = await this.gradientArguments(caller);\n\n if (stops.length !== 5) {\n throw new Error(\n `WrShinyTextHarness.${caller}(): the gradient has ${stops.length} stops, not the five the stripe is made ` +\n 'of (base at 0% / 35%, shine at 50%, base at 65% / 100%). Reading a colour out of a differently ' +\n 'shaped gradient would report a stop that means something else.'\n );\n }\n return stops;\n }\n\n /** The gradient's arguments — the angle first, then the stops. */\n private async gradientArguments(caller: string): Promise<string[]> {\n const gradient = await this.require(caller, 'background-image');\n const match = GRADIENT.exec(gradient);\n\n if (!match) {\n throw new Error(\n `WrShinyTextHarness.${caller}(): \\`background-image\\` is \"${gradient}\", which is not a linear-gradient(). ` +\n 'The stripe IS that gradient, so anything else means the component is no longer painting one.'\n );\n }\n return splitTopLevel(match[1], ',');\n }\n\n /** One of the host's two inline declarations, or a failure that says which one is missing. */\n private async require(caller: string, property: string): Promise<string> {\n const style = (await (await this.host()).getAttribute('style')) ?? '';\n\n for (const declaration of splitTopLevel(style, ';')) {\n const colon = declaration.indexOf(':');\n if (colon !== -1 && declaration.slice(0, colon).trim() === property) {\n return declaration.slice(colon + 1).trim();\n }\n }\n\n throw new Error(\n `WrShinyTextHarness.${caller}(): the host carries no inline \\`${property}\\` (style: \"${style}\"). The ` +\n 'component writes it on every render, so its absence is a broken host binding rather than a default — ' +\n 'and it is read inline, not through computed style, precisely so the stylesheet cannot answer for it.'\n );\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { Directionality } from '@angular/cdk/bidi';
2
2
  import { coerceNumberProperty, coerceBooleanProperty } from '@angular/cdk/coercion';
3
3
  import * as i0 from '@angular/core';
4
- import { input, computed, model, output, signal, viewChild, inject, effect, ViewEncapsulation, Component } from '@angular/core';
4
+ import { input, computed, model, output, signal, viewChild, inject, effect, untracked, ViewEncapsulation, Component } from '@angular/core';
5
5
  import { clamp, round } from 'ngwr/utils';
6
6
 
7
7
  /**
@@ -83,8 +83,23 @@ class WrSlider {
83
83
  touch = output();
84
84
  low = signal(0, /* @ts-ignore */
85
85
  ...(ngDevMode ? [{ debugName: "low" }] : /* istanbul ignore next */ []));
86
+ /**
87
+ * The high thumb's cell. Its literal default stands in for `maxValue()`,
88
+ * which cannot be read at field-initialisation time — see `highSeeded` and
89
+ * the constructor effect.
90
+ */
86
91
  high = signal(100, /* @ts-ignore */
87
92
  ...(ngDevMode ? [{ debugName: "high" }] : /* istanbul ignore next */ []));
93
+ /**
94
+ * Whether `high` has ever been told a real value by the model.
95
+ *
96
+ * `<wr-slider range [min]="200" [max]="300" />` binds no value, so the model
97
+ * holds its own scalar default and the tuple branch below never runs: `high`
98
+ * kept the literal 100, which is not inside every `[min, max]`. The thumb
99
+ * rendered off the track and the first interaction committed `[low, 100]` —
100
+ * a value the slider itself calls out of range.
101
+ */
102
+ highSeeded = false;
88
103
  track = viewChild.required('track', /* @ts-ignore */
89
104
  ...(ngDevMode ? [{ debugName: "track" }] : /* istanbul ignore next */ []));
90
105
  /**
@@ -121,16 +136,39 @@ class WrSlider {
121
136
  constructor() {
122
137
  // Keep the internal thumbs in sync with external writes to `value`
123
138
  // (the old `writeValue`): split the tuple / clamp into the low & high cells.
139
+ // The cells are clamped and the MODEL IS NOT TOUCHED, and the asymmetry is
140
+ // deliberate. A thumb cannot render outside its own track, so the display
141
+ // has no choice — but writing that clamp back into `value` would erase the
142
+ // error it represents: under `[formField]` the bounds reach this component
143
+ // only through the schema's `min()` / `max()` rules (binding `[min]` beside
144
+ // `[formField]` is an NG8022 compile error), so the write-back deleted the
145
+ // out-of-range value those rules exist to report, and marked a pristine
146
+ // form dirty on first paint besides. The first real interaction emits an
147
+ // in-range number through `emitChange` and the two agree from then on.
124
148
  effect(() => {
125
149
  const v = this.value();
126
150
  if (Array.isArray(v)) {
127
151
  const tuple = v;
128
- this.low.set(this.clampToBounds(tuple[0]));
129
- this.high.set(this.clampToBounds(tuple[1]));
130
- }
131
- else if (typeof v === 'number') {
132
- this.low.set(this.clampToBounds(v));
152
+ const lo = this.clampToBounds(tuple[0]);
153
+ const hi = this.clampToBounds(tuple[1]);
154
+ this.low.set(lo);
155
+ this.high.set(hi);
156
+ this.highSeeded = true;
157
+ return;
133
158
  }
159
+ if (typeof v !== 'number')
160
+ return;
161
+ const lo = this.clampToBounds(v);
162
+ this.low.set(lo);
163
+ if (!this.range())
164
+ return;
165
+ // Range mode holding a scalar: the high thumb has never been told
166
+ // anything, and its literal default is not inside every `[min, max]`.
167
+ // Seed it at the top of the range — which is what the 100 meant back when
168
+ // the bounds were 0–100. The MODEL is left alone here too; the shape it
169
+ // holds is the consumer's business until they move a thumb.
170
+ this.high.set(this.highSeeded ? this.clampToBounds(untracked(this.high)) : this.maxValue());
171
+ this.highSeeded = true;
134
172
  });
135
173
  }
136
174
  // Interaction
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-slider.mjs","sources":["../../../projects/lib/slider/slider.ts","../../../projects/lib/slider/slider.html","../../../projects/lib/slider/ngwr-slider.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 { Directionality } from '@angular/cdk/bidi';\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport type { ElementRef } from '@angular/core';\nimport {\n Component,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n model,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport { clamp, round } from 'ngwr/utils';\n\nimport type { WrSliderValue } from './interfaces';\n\n/** Trim float drift from step calculations. */\n\n/**\n * Numeric slider with optional dual-thumb range mode.\n *\n * A signal-forms native control: it implements `FormValueControl<WrSliderValue>`,\n * so `[formField]` binds straight to its `value` model — no\n * `ControlValueAccessor` in between. `[(value)]` works standalone. The value\n * shape depends on `range`: `number` (default) or `[number, number]` when\n * `range=\"true\"`.\n *\n * Keyboard: ← / → adjust by `step`; ↑ / ↓ same; Shift+arrow by `step × 10`;\n * Home / End jump to min / max; PageUp / PageDown by `step × 10`.\n *\n * Reading direction: the arrows follow the VISUAL track, so under `dir=\"rtl\"`\n * → moves toward the lower end and ← toward the higher one, and a drag reads\n * its ratio from the right edge. ↑ / ↓, PageUp / PageDown and Home / End are\n * unchanged — they name a value, not a side of the screen.\n *\n * @example\n * ```html\n * <!-- signal forms -->\n * <wr-slider [formField]=\"form.volume\" min=\"0\" max=\"100\" step=\"5\" />\n *\n * <!-- standalone two-way binding -->\n * <wr-slider [(value)]=\"range\" range min=\"0\" max=\"1000\" step=\"10\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/slider\n */\n@Component({\n selector: 'wr-slider',\n templateUrl: './slider.html',\n encapsulation: ViewEncapsulation.None,\n host: { '[class]': 'classes()' },\n})\nexport class WrSlider implements FormValueControl<WrSliderValue> {\n // Typed `WrSliderValue | undefined` (not `number`) to satisfy the reserved\n // `FormUiControl` min/max slots, which are keyed to the control's value type.\n // The transform still coerces to a plain number, so the bounds stay numeric\n // at runtime; `minValue`/`maxValue` narrow them back for arithmetic.\n /** Lower bound. @default 0 */\n readonly min = input<WrSliderValue | undefined>(0, { transform: (v: unknown): number => coerceNumberProperty(v, 0) });\n /** Upper bound. @default 100 */\n readonly max = input<WrSliderValue | undefined>(100, {\n transform: (v: unknown): number => coerceNumberProperty(v, 100),\n });\n\n /** Resolved numeric lower bound for internal arithmetic. */\n private readonly minValue = computed(() => {\n const m = this.min();\n return typeof m === 'number' ? m : 0;\n });\n /** Resolved numeric upper bound for internal arithmetic. */\n private readonly maxValue = computed(() => {\n const m = this.max();\n return typeof m === 'number' ? m : 100;\n });\n\n /** Step size for keyboard and drag. @default 1 */\n readonly step = input(1, { transform: (v: unknown): number => Math.max(0.0001, coerceNumberProperty(v, 1)) });\n /** Render two thumbs and emit `[low, high]`. @default false */\n readonly range = input(false, { transform: coerceBooleanProperty });\n /**\n * Disable interaction. Bound automatically from the field's disabled state\n * when used with `[formField]`.\n *\n * @default false\n */\n readonly disabled = input(false, { transform: coerceBooleanProperty });\n /** Render the current value below the track. @default true */\n readonly showLabel = input(true, { transform: coerceBooleanProperty });\n\n /**\n * Current value. Bound by `[formField]`, or two-way via `[(value)]`. Shape\n * follows `range`: a plain `number`, or `[low, high]` in range mode.\n */\n readonly value = model<WrSliderValue>(0);\n\n /** Emitted on blur so a bound field can mark itself touched. */\n readonly touch = output<void>();\n\n protected readonly low = signal(0);\n protected readonly high = signal(100);\n\n protected readonly track = viewChild.required<ElementRef<HTMLElement>>('track');\n\n /**\n * Ambient reading direction. Optional so a bare `TestBed` — or any consumer\n * that never set a direction — needs no provider; `Directionality` is\n * root-provided, so `null` only ever means \"nobody asked\", which is LTR.\n *\n * Nothing caches a direction-derived value: the thumbs are painted with\n * `inset-inline-start`, so the track mirrors from CSS alone, and `isRtl()` is\n * read inside the event handlers. A runtime flip therefore needs no\n * subscription to `Directionality.change`.\n */\n private readonly dir = inject(Directionality, { optional: true });\n\n protected readonly classes = computed(() => {\n const parts = ['wr-slider'];\n if (this.range()) parts.push('wr-slider--range');\n if (this.disabled()) parts.push('wr-slider--disabled');\n return parts.join(' ');\n });\n\n protected readonly lowPercent = computed(() => this.percentOf(this.low()));\n protected readonly highPercent = computed(() => this.percentOf(this.high()));\n\n /** Offset of the fill from the track's START edge — the left one in LTR, the right one in RTL. */\n protected readonly fillStart = computed(() => (this.range() ? this.lowPercent() : 0));\n protected readonly fillWidth = computed(() =>\n this.range() ? this.highPercent() - this.lowPercent() : this.lowPercent()\n );\n\n protected readonly label = computed(() => (this.range() ? `${this.low()} – ${this.high()}` : `${this.low()}`));\n\n constructor() {\n // Keep the internal thumbs in sync with external writes to `value`\n // (the old `writeValue`): split the tuple / clamp into the low & high cells.\n effect(() => {\n const v = this.value();\n if (Array.isArray(v)) {\n const tuple = v as readonly [number, number];\n this.low.set(this.clampToBounds(tuple[0]));\n this.high.set(this.clampToBounds(tuple[1]));\n } else if (typeof v === 'number') {\n this.low.set(this.clampToBounds(v));\n }\n });\n }\n\n // Interaction\n\n protected onPointerDown(event: PointerEvent, thumb: 'low' | 'high'): void {\n if (this.disabled()) return;\n event.preventDefault();\n const target = event.currentTarget as HTMLElement;\n target.setPointerCapture(event.pointerId);\n\n const move = (e: PointerEvent): void => this.updateFromEvent(e, thumb);\n const up = (e: PointerEvent): void => {\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n this.touch.emit();\n };\n\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n this.updateFromEvent(event, thumb);\n }\n\n protected onTrackPointerDown(event: PointerEvent): void {\n if (this.disabled()) return;\n if ((event.target as HTMLElement).closest('.wr-slider__thumb')) return;\n const thumb = this.nearestThumb(event);\n this.updateFromEvent(event, thumb);\n this.focusThumb(thumb);\n }\n\n protected onKey(event: KeyboardEvent, thumb: 'low' | 'high'): void {\n if (this.disabled()) return;\n const big = event.shiftKey || event.key === 'PageUp' || event.key === 'PageDown' ? 10 : 1;\n const delta = this.step() * big;\n const current = thumb === 'low' ? this.low() : this.high();\n // The horizontal arrows name a SIDE of the track, so they follow the\n // reading direction; every other key names a value and does not.\n const inline = this.isRtl() ? -delta : delta;\n let next: number | null = null;\n\n switch (event.key) {\n case 'ArrowRight':\n next = current + inline;\n break;\n case 'ArrowLeft':\n next = current - inline;\n break;\n case 'ArrowUp':\n case 'PageUp':\n next = current + delta;\n break;\n case 'ArrowDown':\n case 'PageDown':\n next = current - delta;\n break;\n case 'Home':\n next = this.minValue();\n break;\n case 'End':\n next = this.maxValue();\n break;\n }\n\n if (next === null) return;\n event.preventDefault();\n this.setThumb(thumb, next);\n this.emitChange();\n this.touch.emit();\n }\n\n // Internals\n\n private isRtl(): boolean {\n return this.dir?.value === 'rtl';\n }\n\n /**\n * The value under a pointer, measured along the track's INLINE axis.\n *\n * The offset is taken from whichever edge the track starts at — the left one\n * in LTR, the right one in RTL — so the whole component needs the direction\n * in exactly one place. Measuring from `rect.left` regardless would read 0 at\n * the visual maximum of a mirrored track.\n */\n private valueAt(event: PointerEvent): number {\n const rect = this.track().nativeElement.getBoundingClientRect();\n const offset = this.isRtl() ? rect.right - event.clientX : event.clientX - rect.left;\n const ratio = clamp(offset / rect.width, 0, 1);\n return this.minValue() + ratio * (this.maxValue() - this.minValue());\n }\n\n private updateFromEvent(event: PointerEvent, thumb: 'low' | 'high'): void {\n this.setThumb(thumb, this.valueAt(event));\n this.emitChange();\n }\n\n /**\n * Which thumb a track click grabs: the one nearest the pointer ON SCREEN.\n *\n * Value distance is the same thing as visual distance once {@link valueAt}\n * has mirrored the ratio — in RTL the low thumb is the RIGHT one, and a click\n * near the right edge lands close to `low` in value space too.\n */\n private nearestThumb(event: PointerEvent): 'low' | 'high' {\n if (!this.range()) return 'low';\n const raw = this.valueAt(event);\n return Math.abs(raw - this.low()) <= Math.abs(raw - this.high()) ? 'low' : 'high';\n }\n\n private focusThumb(thumb: 'low' | 'high'): void {\n queueMicrotask(() => {\n const root = this.track().nativeElement;\n const el = root.querySelector<HTMLElement>(`.wr-slider__thumb--${thumb}`);\n el?.focus();\n });\n }\n\n private setThumb(thumb: 'low' | 'high', raw: number): void {\n const stepped = this.snap(raw);\n if (thumb === 'low') {\n const next = this.range() ? Math.min(stepped, this.high()) : stepped;\n this.low.set(next);\n } else {\n this.high.set(Math.max(stepped, this.low()));\n }\n }\n\n private snap(value: number): number {\n const stepped = Math.round((value - this.minValue()) / this.step()) * this.step() + this.minValue();\n return this.clampToBounds(round(stepped, 6));\n }\n\n private clampToBounds(v: number): number {\n return clamp(v, this.minValue(), this.maxValue());\n }\n\n private percentOf(v: number): number {\n const span = this.maxValue() - this.minValue();\n if (span <= 0) return 0;\n return ((v - this.minValue()) / span) * 100;\n }\n\n private emitChange(): void {\n const value: WrSliderValue = this.range() ? [this.low(), this.high()] : this.low();\n this.value.set(value);\n }\n}\n","<div #track class=\"wr-slider__track\" (pointerdown)=\"onTrackPointerDown($event)\">\n <!--\n Positioned on the INLINE axis, not with `left`: `inset-inline-start` is the\n same edge in LTR and mirrors under `dir=\"rtl\"`, so the track paints itself\n the right way round without the component reading a direction to draw.\n -->\n <span class=\"wr-slider__fill\" [style.inset-inline-start.%]=\"fillStart()\" [style.width.%]=\"fillWidth()\"></span>\n\n <button\n type=\"button\"\n class=\"wr-slider__thumb wr-slider__thumb--low\"\n role=\"slider\"\n [style.inset-inline-start.%]=\"lowPercent()\"\n [disabled]=\"disabled()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"range() ? high() : max()\"\n [attr.aria-valuenow]=\"low()\"\n [attr.aria-label]=\"range() ? 'Lower value' : 'Value'\"\n (pointerdown)=\"onPointerDown($event, 'low')\"\n (keydown)=\"onKey($event, 'low')\"\n ></button>\n\n @if (range()) {\n <button\n type=\"button\"\n class=\"wr-slider__thumb wr-slider__thumb--high\"\n role=\"slider\"\n [style.inset-inline-start.%]=\"highPercent()\"\n [disabled]=\"disabled()\"\n [attr.aria-valuemin]=\"low()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"high()\"\n aria-label=\"Upper value\"\n (pointerdown)=\"onPointerDown($event, 'high')\"\n (keydown)=\"onKey($event, 'high')\"\n ></button>\n }\n</div>\n\n@if (showLabel()) {\n <div class=\"wr-slider__label\">{{ label() }}</div>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;;AAAA;;;;;AAKG;AAuBH;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;MAOU,QAAQ,CAAA;;;;;;AAMV,IAAA,GAAG,GAAG,KAAK,CAA4B,CAAC,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,KAAA,EAAA,8BAAA,EAAA,CAAA,EAAI,SAAS,EAAE,CAAC,CAAU,KAAa,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;;AAE5G,IAAA,GAAG,GAAG,KAAK,CAA4B,GAAG,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,KAAA,EAAA,8BAAA,EAAA,CAAA,EACjD,SAAS,EAAE,CAAC,CAAU,KAAa,oBAAoB,CAAC,CAAC,EAAE,GAAG,CAAC,GAC/D;;AAGe,IAAA,QAAQ,GAAG,QAAQ,CAAC,MAAK;AACxC,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE;AACpB,QAAA,OAAO,OAAO,CAAC,KAAK,QAAQ,GAAG,CAAC,GAAG,CAAC;IACtC,CAAC;iFAAC;;AAEe,IAAA,QAAQ,GAAG,QAAQ,CAAC,MAAK;AACxC,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE;AACpB,QAAA,OAAO,OAAO,CAAC,KAAK,QAAQ,GAAG,CAAC,GAAG,GAAG;IACxC,CAAC;iFAAC;;IAGO,IAAI,GAAG,KAAK,CAAC,CAAC,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,MAAA,EAAA,8BAAA,EAAA,CAAA,EAAI,SAAS,EAAE,CAAC,CAAU,KAAa,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAA,CAAG;;IAEpG,KAAK,GAAG,KAAK,CAAC,KAAK,6EAAI,SAAS,EAAE,qBAAqB,EAAA,CAAG;AACnE;;;;;AAKG;IACM,QAAQ,GAAG,KAAK,CAAC,KAAK,gFAAI,SAAS,EAAE,qBAAqB,EAAA,CAAG;;IAE7D,SAAS,GAAG,KAAK,CAAC,IAAI,iFAAI,SAAS,EAAE,qBAAqB,EAAA,CAAG;AAEtE;;;AAGG;IACM,KAAK,GAAG,KAAK,CAAgB,CAAC;8EAAC;;IAG/B,KAAK,GAAG,MAAM,EAAQ;IAEZ,GAAG,GAAG,MAAM,CAAC,CAAC;4EAAC;IACf,IAAI,GAAG,MAAM,CAAC,GAAG;6EAAC;AAElB,IAAA,KAAK,GAAG,SAAS,CAAC,QAAQ,CAA0B,OAAO;8EAAC;AAE/E;;;;;;;;;AASG;IACc,GAAG,GAAG,MAAM,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAE9C,IAAA,OAAO,GAAG,QAAQ,CAAC,MAAK;AACzC,QAAA,MAAM,KAAK,GAAG,CAAC,WAAW,CAAC;QAC3B,IAAI,IAAI,CAAC,KAAK,EAAE;AAAE,YAAA,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC;QAChD,IAAI,IAAI,CAAC,QAAQ,EAAE;AAAE,YAAA,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC;AACtD,QAAA,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;IACxB,CAAC;gFAAC;AAEiB,IAAA,UAAU,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;mFAAC;AACvD,IAAA,WAAW,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;oFAAC;;IAGzD,SAAS,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;kFAAC;AAClE,IAAA,SAAS,GAAG,QAAQ,CAAC,MACtC,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE;kFAC1E;AAEkB,IAAA,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA,GAAA,EAAM,IAAI,CAAC,IAAI,EAAE,CAAA,CAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;8EAAC;AAE9G,IAAA,WAAA,GAAA;;;QAGE,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE;AACtB,YAAA,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;gBACpB,MAAM,KAAK,GAAG,CAA8B;AAC5C,gBAAA,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1C,gBAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7C;AAAO,iBAAA,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE;AAChC,gBAAA,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACrC;AACF,QAAA,CAAC,CAAC;IACJ;;IAIU,aAAa,CAAC,KAAmB,EAAE,KAAqB,EAAA;QAChE,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE;QACrB,KAAK,CAAC,cAAc,EAAE;AACtB,QAAA,MAAM,MAAM,GAAG,KAAK,CAAC,aAA4B;AACjD,QAAA,MAAM,CAAC,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC;AAEzC,QAAA,MAAM,IAAI,GAAG,CAAC,CAAe,KAAW,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,KAAK,CAAC;AACtE,QAAA,MAAM,EAAE,GAAG,CAAC,CAAe,KAAU;AACnC,YAAA,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,CAAC;AACzC,YAAA,MAAM,CAAC,mBAAmB,CAAC,aAAa,EAAE,IAAI,CAAC;AAC/C,YAAA,MAAM,CAAC,mBAAmB,CAAC,WAAW,EAAE,EAAE,CAAC;AAC3C,YAAA,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;AACnB,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,aAAa,EAAE,IAAI,CAAC;AAC5C,QAAA,MAAM,CAAC,gBAAgB,CAAC,WAAW,EAAE,EAAE,CAAC;AACxC,QAAA,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC;IACpC;AAEU,IAAA,kBAAkB,CAAC,KAAmB,EAAA;QAC9C,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE;AACrB,QAAA,IAAK,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,mBAAmB,CAAC;YAAE;QAChE,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;AACtC,QAAA,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC;AAClC,QAAA,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;IACxB;IAEU,KAAK,CAAC,KAAoB,EAAE,KAAqB,EAAA;QACzD,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE;QACrB,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,KAAK,UAAU,GAAG,EAAE,GAAG,CAAC;QACzF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG;AAC/B,QAAA,MAAM,OAAO,GAAG,KAAK,KAAK,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE;;;AAG1D,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,KAAK;QAC5C,IAAI,IAAI,GAAkB,IAAI;AAE9B,QAAA,QAAQ,KAAK,CAAC,GAAG;AACf,YAAA,KAAK,YAAY;AACf,gBAAA,IAAI,GAAG,OAAO,GAAG,MAAM;gBACvB;AACF,YAAA,KAAK,WAAW;AACd,gBAAA,IAAI,GAAG,OAAO,GAAG,MAAM;gBACvB;AACF,YAAA,KAAK,SAAS;AACd,YAAA,KAAK,QAAQ;AACX,gBAAA,IAAI,GAAG,OAAO,GAAG,KAAK;gBACtB;AACF,YAAA,KAAK,WAAW;AAChB,YAAA,KAAK,UAAU;AACb,gBAAA,IAAI,GAAG,OAAO,GAAG,KAAK;gBACtB;AACF,YAAA,KAAK,MAAM;AACT,gBAAA,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE;gBACtB;AACF,YAAA,KAAK,KAAK;AACR,gBAAA,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE;gBACtB;;QAGJ,IAAI,IAAI,KAAK,IAAI;YAAE;QACnB,KAAK,CAAC,cAAc,EAAE;AACtB,QAAA,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;QAC1B,IAAI,CAAC,UAAU,EAAE;AACjB,QAAA,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;IACnB;;IAIQ,KAAK,GAAA;AACX,QAAA,OAAO,IAAI,CAAC,GAAG,EAAE,KAAK,KAAK,KAAK;IAClC;AAEA;;;;;;;AAOG;AACK,IAAA,OAAO,CAAC,KAAmB,EAAA;QACjC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,aAAa,CAAC,qBAAqB,EAAE;QAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI;AACpF,QAAA,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;AAC9C,QAAA,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;IACtE;IAEQ,eAAe,CAAC,KAAmB,EAAE,KAAqB,EAAA;AAChE,QAAA,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,UAAU,EAAE;IACnB;AAEA;;;;;;AAMG;AACK,IAAA,YAAY,CAAC,KAAmB,EAAA;AACtC,QAAA,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE;AAAE,YAAA,OAAO,KAAK;QAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;AAC/B,QAAA,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,GAAG,MAAM;IACnF;AAEQ,IAAA,UAAU,CAAC,KAAqB,EAAA;QACtC,cAAc,CAAC,MAAK;YAClB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,aAAa;YACvC,MAAM,EAAE,GAAG,IAAI,CAAC,aAAa,CAAc,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAE,CAAC;YACzE,EAAE,EAAE,KAAK,EAAE;AACb,QAAA,CAAC,CAAC;IACJ;IAEQ,QAAQ,CAAC,KAAqB,EAAE,GAAW,EAAA;QACjD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC9B,QAAA,IAAI,KAAK,KAAK,KAAK,EAAE;YACnB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO;AACpE,YAAA,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;QACpB;aAAO;AACL,YAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC9C;IACF;AAEQ,IAAA,IAAI,CAAC,KAAa,EAAA;AACxB,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE;QACnG,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC9C;AAEQ,IAAA,aAAa,CAAC,CAAS,EAAA;AAC7B,QAAA,OAAO,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;IACnD;AAEQ,IAAA,SAAS,CAAC,CAAS,EAAA;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE;QAC9C,IAAI,IAAI,IAAI,CAAC;AAAE,YAAA,OAAO,CAAC;AACvB,QAAA,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,IAAI,GAAG;IAC7C;IAEQ,UAAU,GAAA;QAChB,MAAM,KAAK,GAAkB,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE;AAClF,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC;IACvB;uGAhPW,QAAQ,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAR,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,QAAQ,knCChErB,w/CA0CA,EAAA,aAAA,EAAA,EAAA,CAAA,iBAAA,CAAA,IAAA,EAAA,CAAA;;2FDsBa,QAAQ,EAAA,UAAA,EAAA,CAAA;kBANpB,SAAS;+BACE,WAAW,EAAA,aAAA,EAEN,iBAAiB,CAAC,IAAI,QAC/B,EAAE,SAAS,EAAE,WAAW,EAAE,EAAA,QAAA,EAAA,w/CAAA,EAAA;kzBAmDuC,OAAO,EAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,EAAA,CAAA;;AEjHhF;;AAEG;;;;"}
1
+ {"version":3,"file":"ngwr-slider.mjs","sources":["../../../projects/lib/slider/slider.ts","../../../projects/lib/slider/slider.html","../../../projects/lib/slider/ngwr-slider.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 { Directionality } from '@angular/cdk/bidi';\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport type { ElementRef } from '@angular/core';\nimport {\n Component,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n model,\n output,\n signal,\n untracked,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport { clamp, round } from 'ngwr/utils';\n\nimport type { WrSliderValue } from './interfaces';\n\n/** Trim float drift from step calculations. */\n\n/**\n * Numeric slider with optional dual-thumb range mode.\n *\n * A signal-forms native control: it implements `FormValueControl<WrSliderValue>`,\n * so `[formField]` binds straight to its `value` model — no\n * `ControlValueAccessor` in between. `[(value)]` works standalone. The value\n * shape depends on `range`: `number` (default) or `[number, number]` when\n * `range=\"true\"`.\n *\n * Keyboard: ← / → adjust by `step`; ↑ / ↓ same; Shift+arrow by `step × 10`;\n * Home / End jump to min / max; PageUp / PageDown by `step × 10`.\n *\n * Reading direction: the arrows follow the VISUAL track, so under `dir=\"rtl\"`\n * → moves toward the lower end and ← toward the higher one, and a drag reads\n * its ratio from the right edge. ↑ / ↓, PageUp / PageDown and Home / End are\n * unchanged — they name a value, not a side of the screen.\n *\n * @example\n * ```html\n * <!-- signal forms -->\n * <wr-slider [formField]=\"form.volume\" min=\"0\" max=\"100\" step=\"5\" />\n *\n * <!-- standalone two-way binding -->\n * <wr-slider [(value)]=\"range\" range min=\"0\" max=\"1000\" step=\"10\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/slider\n */\n@Component({\n selector: 'wr-slider',\n templateUrl: './slider.html',\n encapsulation: ViewEncapsulation.None,\n host: { '[class]': 'classes()' },\n})\nexport class WrSlider implements FormValueControl<WrSliderValue> {\n // Typed `WrSliderValue | undefined` (not `number`) to satisfy the reserved\n // `FormUiControl` min/max slots, which are keyed to the control's value type.\n // The transform still coerces to a plain number, so the bounds stay numeric\n // at runtime; `minValue`/`maxValue` narrow them back for arithmetic.\n /** Lower bound. @default 0 */\n readonly min = input<WrSliderValue | undefined>(0, { transform: (v: unknown): number => coerceNumberProperty(v, 0) });\n /** Upper bound. @default 100 */\n readonly max = input<WrSliderValue | undefined>(100, {\n transform: (v: unknown): number => coerceNumberProperty(v, 100),\n });\n\n /** Resolved numeric lower bound for internal arithmetic. */\n private readonly minValue = computed(() => {\n const m = this.min();\n return typeof m === 'number' ? m : 0;\n });\n /** Resolved numeric upper bound for internal arithmetic. */\n private readonly maxValue = computed(() => {\n const m = this.max();\n return typeof m === 'number' ? m : 100;\n });\n\n /** Step size for keyboard and drag. @default 1 */\n readonly step = input(1, { transform: (v: unknown): number => Math.max(0.0001, coerceNumberProperty(v, 1)) });\n /** Render two thumbs and emit `[low, high]`. @default false */\n readonly range = input(false, { transform: coerceBooleanProperty });\n /**\n * Disable interaction. Bound automatically from the field's disabled state\n * when used with `[formField]`.\n *\n * @default false\n */\n readonly disabled = input(false, { transform: coerceBooleanProperty });\n /** Render the current value below the track. @default true */\n readonly showLabel = input(true, { transform: coerceBooleanProperty });\n\n /**\n * Current value. Bound by `[formField]`, or two-way via `[(value)]`. Shape\n * follows `range`: a plain `number`, or `[low, high]` in range mode.\n */\n readonly value = model<WrSliderValue>(0);\n\n /** Emitted on blur so a bound field can mark itself touched. */\n readonly touch = output<void>();\n\n protected readonly low = signal(0);\n /**\n * The high thumb's cell. Its literal default stands in for `maxValue()`,\n * which cannot be read at field-initialisation time — see `highSeeded` and\n * the constructor effect.\n */\n protected readonly high = signal(100);\n\n /**\n * Whether `high` has ever been told a real value by the model.\n *\n * `<wr-slider range [min]=\"200\" [max]=\"300\" />` binds no value, so the model\n * holds its own scalar default and the tuple branch below never runs: `high`\n * kept the literal 100, which is not inside every `[min, max]`. The thumb\n * rendered off the track and the first interaction committed `[low, 100]` —\n * a value the slider itself calls out of range.\n */\n private highSeeded = false;\n\n protected readonly track = viewChild.required<ElementRef<HTMLElement>>('track');\n\n /**\n * Ambient reading direction. Optional so a bare `TestBed` — or any consumer\n * that never set a direction — needs no provider; `Directionality` is\n * root-provided, so `null` only ever means \"nobody asked\", which is LTR.\n *\n * Nothing caches a direction-derived value: the thumbs are painted with\n * `inset-inline-start`, so the track mirrors from CSS alone, and `isRtl()` is\n * read inside the event handlers. A runtime flip therefore needs no\n * subscription to `Directionality.change`.\n */\n private readonly dir = inject(Directionality, { optional: true });\n\n protected readonly classes = computed(() => {\n const parts = ['wr-slider'];\n if (this.range()) parts.push('wr-slider--range');\n if (this.disabled()) parts.push('wr-slider--disabled');\n return parts.join(' ');\n });\n\n protected readonly lowPercent = computed(() => this.percentOf(this.low()));\n protected readonly highPercent = computed(() => this.percentOf(this.high()));\n\n /** Offset of the fill from the track's START edge — the left one in LTR, the right one in RTL. */\n protected readonly fillStart = computed(() => (this.range() ? this.lowPercent() : 0));\n protected readonly fillWidth = computed(() =>\n this.range() ? this.highPercent() - this.lowPercent() : this.lowPercent()\n );\n\n protected readonly label = computed(() => (this.range() ? `${this.low()} – ${this.high()}` : `${this.low()}`));\n\n constructor() {\n // Keep the internal thumbs in sync with external writes to `value`\n // (the old `writeValue`): split the tuple / clamp into the low & high cells.\n // The cells are clamped and the MODEL IS NOT TOUCHED, and the asymmetry is\n // deliberate. A thumb cannot render outside its own track, so the display\n // has no choice — but writing that clamp back into `value` would erase the\n // error it represents: under `[formField]` the bounds reach this component\n // only through the schema's `min()` / `max()` rules (binding `[min]` beside\n // `[formField]` is an NG8022 compile error), so the write-back deleted the\n // out-of-range value those rules exist to report, and marked a pristine\n // form dirty on first paint besides. The first real interaction emits an\n // in-range number through `emitChange` and the two agree from then on.\n effect(() => {\n const v = this.value();\n\n if (Array.isArray(v)) {\n const tuple = v as readonly [number, number];\n const lo = this.clampToBounds(tuple[0]);\n const hi = this.clampToBounds(tuple[1]);\n this.low.set(lo);\n this.high.set(hi);\n this.highSeeded = true;\n return;\n }\n\n if (typeof v !== 'number') return;\n const lo = this.clampToBounds(v);\n this.low.set(lo);\n\n if (!this.range()) return;\n\n // Range mode holding a scalar: the high thumb has never been told\n // anything, and its literal default is not inside every `[min, max]`.\n // Seed it at the top of the range — which is what the 100 meant back when\n // the bounds were 0–100. The MODEL is left alone here too; the shape it\n // holds is the consumer's business until they move a thumb.\n this.high.set(this.highSeeded ? this.clampToBounds(untracked(this.high)) : this.maxValue());\n this.highSeeded = true;\n });\n }\n\n // Interaction\n\n protected onPointerDown(event: PointerEvent, thumb: 'low' | 'high'): void {\n if (this.disabled()) return;\n event.preventDefault();\n const target = event.currentTarget as HTMLElement;\n target.setPointerCapture(event.pointerId);\n\n const move = (e: PointerEvent): void => this.updateFromEvent(e, thumb);\n const up = (e: PointerEvent): void => {\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n this.touch.emit();\n };\n\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n this.updateFromEvent(event, thumb);\n }\n\n protected onTrackPointerDown(event: PointerEvent): void {\n if (this.disabled()) return;\n if ((event.target as HTMLElement).closest('.wr-slider__thumb')) return;\n const thumb = this.nearestThumb(event);\n this.updateFromEvent(event, thumb);\n this.focusThumb(thumb);\n }\n\n protected onKey(event: KeyboardEvent, thumb: 'low' | 'high'): void {\n if (this.disabled()) return;\n const big = event.shiftKey || event.key === 'PageUp' || event.key === 'PageDown' ? 10 : 1;\n const delta = this.step() * big;\n const current = thumb === 'low' ? this.low() : this.high();\n // The horizontal arrows name a SIDE of the track, so they follow the\n // reading direction; every other key names a value and does not.\n const inline = this.isRtl() ? -delta : delta;\n let next: number | null = null;\n\n switch (event.key) {\n case 'ArrowRight':\n next = current + inline;\n break;\n case 'ArrowLeft':\n next = current - inline;\n break;\n case 'ArrowUp':\n case 'PageUp':\n next = current + delta;\n break;\n case 'ArrowDown':\n case 'PageDown':\n next = current - delta;\n break;\n case 'Home':\n next = this.minValue();\n break;\n case 'End':\n next = this.maxValue();\n break;\n }\n\n if (next === null) return;\n event.preventDefault();\n this.setThumb(thumb, next);\n this.emitChange();\n this.touch.emit();\n }\n\n // Internals\n\n private isRtl(): boolean {\n return this.dir?.value === 'rtl';\n }\n\n /**\n * The value under a pointer, measured along the track's INLINE axis.\n *\n * The offset is taken from whichever edge the track starts at — the left one\n * in LTR, the right one in RTL — so the whole component needs the direction\n * in exactly one place. Measuring from `rect.left` regardless would read 0 at\n * the visual maximum of a mirrored track.\n */\n private valueAt(event: PointerEvent): number {\n const rect = this.track().nativeElement.getBoundingClientRect();\n const offset = this.isRtl() ? rect.right - event.clientX : event.clientX - rect.left;\n const ratio = clamp(offset / rect.width, 0, 1);\n return this.minValue() + ratio * (this.maxValue() - this.minValue());\n }\n\n private updateFromEvent(event: PointerEvent, thumb: 'low' | 'high'): void {\n this.setThumb(thumb, this.valueAt(event));\n this.emitChange();\n }\n\n /**\n * Which thumb a track click grabs: the one nearest the pointer ON SCREEN.\n *\n * Value distance is the same thing as visual distance once {@link valueAt}\n * has mirrored the ratio — in RTL the low thumb is the RIGHT one, and a click\n * near the right edge lands close to `low` in value space too.\n */\n private nearestThumb(event: PointerEvent): 'low' | 'high' {\n if (!this.range()) return 'low';\n const raw = this.valueAt(event);\n return Math.abs(raw - this.low()) <= Math.abs(raw - this.high()) ? 'low' : 'high';\n }\n\n private focusThumb(thumb: 'low' | 'high'): void {\n queueMicrotask(() => {\n const root = this.track().nativeElement;\n const el = root.querySelector<HTMLElement>(`.wr-slider__thumb--${thumb}`);\n el?.focus();\n });\n }\n\n private setThumb(thumb: 'low' | 'high', raw: number): void {\n const stepped = this.snap(raw);\n if (thumb === 'low') {\n const next = this.range() ? Math.min(stepped, this.high()) : stepped;\n this.low.set(next);\n } else {\n this.high.set(Math.max(stepped, this.low()));\n }\n }\n\n private snap(value: number): number {\n const stepped = Math.round((value - this.minValue()) / this.step()) * this.step() + this.minValue();\n return this.clampToBounds(round(stepped, 6));\n }\n\n private clampToBounds(v: number): number {\n return clamp(v, this.minValue(), this.maxValue());\n }\n\n private percentOf(v: number): number {\n const span = this.maxValue() - this.minValue();\n if (span <= 0) return 0;\n return ((v - this.minValue()) / span) * 100;\n }\n\n private emitChange(): void {\n const value: WrSliderValue = this.range() ? [this.low(), this.high()] : this.low();\n this.value.set(value);\n }\n}\n","<div #track class=\"wr-slider__track\" (pointerdown)=\"onTrackPointerDown($event)\">\n <!--\n Positioned on the INLINE axis, not with `left`: `inset-inline-start` is the\n same edge in LTR and mirrors under `dir=\"rtl\"`, so the track paints itself\n the right way round without the component reading a direction to draw.\n -->\n <span class=\"wr-slider__fill\" [style.inset-inline-start.%]=\"fillStart()\" [style.width.%]=\"fillWidth()\"></span>\n\n <button\n type=\"button\"\n class=\"wr-slider__thumb wr-slider__thumb--low\"\n role=\"slider\"\n [style.inset-inline-start.%]=\"lowPercent()\"\n [disabled]=\"disabled()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"range() ? high() : max()\"\n [attr.aria-valuenow]=\"low()\"\n [attr.aria-label]=\"range() ? 'Lower value' : 'Value'\"\n (pointerdown)=\"onPointerDown($event, 'low')\"\n (keydown)=\"onKey($event, 'low')\"\n ></button>\n\n @if (range()) {\n <button\n type=\"button\"\n class=\"wr-slider__thumb wr-slider__thumb--high\"\n role=\"slider\"\n [style.inset-inline-start.%]=\"highPercent()\"\n [disabled]=\"disabled()\"\n [attr.aria-valuemin]=\"low()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"high()\"\n aria-label=\"Upper value\"\n (pointerdown)=\"onPointerDown($event, 'high')\"\n (keydown)=\"onKey($event, 'high')\"\n ></button>\n }\n</div>\n\n@if (showLabel()) {\n <div class=\"wr-slider__label\">{{ label() }}</div>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;;AAAA;;;;;AAKG;AAwBH;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;MAOU,QAAQ,CAAA;;;;;;AAMV,IAAA,GAAG,GAAG,KAAK,CAA4B,CAAC,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,KAAA,EAAA,8BAAA,EAAA,CAAA,EAAI,SAAS,EAAE,CAAC,CAAU,KAAa,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;;AAE5G,IAAA,GAAG,GAAG,KAAK,CAA4B,GAAG,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,KAAA,EAAA,8BAAA,EAAA,CAAA,EACjD,SAAS,EAAE,CAAC,CAAU,KAAa,oBAAoB,CAAC,CAAC,EAAE,GAAG,CAAC,GAC/D;;AAGe,IAAA,QAAQ,GAAG,QAAQ,CAAC,MAAK;AACxC,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE;AACpB,QAAA,OAAO,OAAO,CAAC,KAAK,QAAQ,GAAG,CAAC,GAAG,CAAC;IACtC,CAAC;iFAAC;;AAEe,IAAA,QAAQ,GAAG,QAAQ,CAAC,MAAK;AACxC,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE;AACpB,QAAA,OAAO,OAAO,CAAC,KAAK,QAAQ,GAAG,CAAC,GAAG,GAAG;IACxC,CAAC;iFAAC;;IAGO,IAAI,GAAG,KAAK,CAAC,CAAC,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,MAAA,EAAA,8BAAA,EAAA,CAAA,EAAI,SAAS,EAAE,CAAC,CAAU,KAAa,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAA,CAAG;;IAEpG,KAAK,GAAG,KAAK,CAAC,KAAK,6EAAI,SAAS,EAAE,qBAAqB,EAAA,CAAG;AACnE;;;;;AAKG;IACM,QAAQ,GAAG,KAAK,CAAC,KAAK,gFAAI,SAAS,EAAE,qBAAqB,EAAA,CAAG;;IAE7D,SAAS,GAAG,KAAK,CAAC,IAAI,iFAAI,SAAS,EAAE,qBAAqB,EAAA,CAAG;AAEtE;;;AAGG;IACM,KAAK,GAAG,KAAK,CAAgB,CAAC;8EAAC;;IAG/B,KAAK,GAAG,MAAM,EAAQ;IAEZ,GAAG,GAAG,MAAM,CAAC,CAAC;4EAAC;AAClC;;;;AAIG;IACgB,IAAI,GAAG,MAAM,CAAC,GAAG;6EAAC;AAErC;;;;;;;;AAQG;IACK,UAAU,GAAG,KAAK;AAEP,IAAA,KAAK,GAAG,SAAS,CAAC,QAAQ,CAA0B,OAAO;8EAAC;AAE/E;;;;;;;;;AASG;IACc,GAAG,GAAG,MAAM,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAE9C,IAAA,OAAO,GAAG,QAAQ,CAAC,MAAK;AACzC,QAAA,MAAM,KAAK,GAAG,CAAC,WAAW,CAAC;QAC3B,IAAI,IAAI,CAAC,KAAK,EAAE;AAAE,YAAA,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC;QAChD,IAAI,IAAI,CAAC,QAAQ,EAAE;AAAE,YAAA,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC;AACtD,QAAA,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;IACxB,CAAC;gFAAC;AAEiB,IAAA,UAAU,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;mFAAC;AACvD,IAAA,WAAW,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;oFAAC;;IAGzD,SAAS,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;kFAAC;AAClE,IAAA,SAAS,GAAG,QAAQ,CAAC,MACtC,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE;kFAC1E;AAEkB,IAAA,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA,GAAA,EAAM,IAAI,CAAC,IAAI,EAAE,CAAA,CAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;8EAAC;AAE9G,IAAA,WAAA,GAAA;;;;;;;;;;;;QAYE,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE;AAEtB,YAAA,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;gBACpB,MAAM,KAAK,GAAG,CAA8B;gBAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBACvC,MAAM,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACvC,gBAAA,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;AAChB,gBAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AACjB,gBAAA,IAAI,CAAC,UAAU,GAAG,IAAI;gBACtB;YACF;YAEA,IAAI,OAAO,CAAC,KAAK,QAAQ;gBAAE;YAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;AAChC,YAAA,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;AAEhB,YAAA,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE;gBAAE;;;;;;AAOnB,YAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;AAC3F,YAAA,IAAI,CAAC,UAAU,GAAG,IAAI;AACxB,QAAA,CAAC,CAAC;IACJ;;IAIU,aAAa,CAAC,KAAmB,EAAE,KAAqB,EAAA;QAChE,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE;QACrB,KAAK,CAAC,cAAc,EAAE;AACtB,QAAA,MAAM,MAAM,GAAG,KAAK,CAAC,aAA4B;AACjD,QAAA,MAAM,CAAC,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC;AAEzC,QAAA,MAAM,IAAI,GAAG,CAAC,CAAe,KAAW,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,KAAK,CAAC;AACtE,QAAA,MAAM,EAAE,GAAG,CAAC,CAAe,KAAU;AACnC,YAAA,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,CAAC;AACzC,YAAA,MAAM,CAAC,mBAAmB,CAAC,aAAa,EAAE,IAAI,CAAC;AAC/C,YAAA,MAAM,CAAC,mBAAmB,CAAC,WAAW,EAAE,EAAE,CAAC;AAC3C,YAAA,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;AACnB,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,aAAa,EAAE,IAAI,CAAC;AAC5C,QAAA,MAAM,CAAC,gBAAgB,CAAC,WAAW,EAAE,EAAE,CAAC;AACxC,QAAA,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC;IACpC;AAEU,IAAA,kBAAkB,CAAC,KAAmB,EAAA;QAC9C,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE;AACrB,QAAA,IAAK,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,mBAAmB,CAAC;YAAE;QAChE,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;AACtC,QAAA,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC;AAClC,QAAA,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;IACxB;IAEU,KAAK,CAAC,KAAoB,EAAE,KAAqB,EAAA;QACzD,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE;QACrB,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,KAAK,UAAU,GAAG,EAAE,GAAG,CAAC;QACzF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG;AAC/B,QAAA,MAAM,OAAO,GAAG,KAAK,KAAK,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE;;;AAG1D,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,KAAK;QAC5C,IAAI,IAAI,GAAkB,IAAI;AAE9B,QAAA,QAAQ,KAAK,CAAC,GAAG;AACf,YAAA,KAAK,YAAY;AACf,gBAAA,IAAI,GAAG,OAAO,GAAG,MAAM;gBACvB;AACF,YAAA,KAAK,WAAW;AACd,gBAAA,IAAI,GAAG,OAAO,GAAG,MAAM;gBACvB;AACF,YAAA,KAAK,SAAS;AACd,YAAA,KAAK,QAAQ;AACX,gBAAA,IAAI,GAAG,OAAO,GAAG,KAAK;gBACtB;AACF,YAAA,KAAK,WAAW;AAChB,YAAA,KAAK,UAAU;AACb,gBAAA,IAAI,GAAG,OAAO,GAAG,KAAK;gBACtB;AACF,YAAA,KAAK,MAAM;AACT,gBAAA,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE;gBACtB;AACF,YAAA,KAAK,KAAK;AACR,gBAAA,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE;gBACtB;;QAGJ,IAAI,IAAI,KAAK,IAAI;YAAE;QACnB,KAAK,CAAC,cAAc,EAAE;AACtB,QAAA,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;QAC1B,IAAI,CAAC,UAAU,EAAE;AACjB,QAAA,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;IACnB;;IAIQ,KAAK,GAAA;AACX,QAAA,OAAO,IAAI,CAAC,GAAG,EAAE,KAAK,KAAK,KAAK;IAClC;AAEA;;;;;;;AAOG;AACK,IAAA,OAAO,CAAC,KAAmB,EAAA;QACjC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,aAAa,CAAC,qBAAqB,EAAE;QAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI;AACpF,QAAA,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;AAC9C,QAAA,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;IACtE;IAEQ,eAAe,CAAC,KAAmB,EAAE,KAAqB,EAAA;AAChE,QAAA,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,UAAU,EAAE;IACnB;AAEA;;;;;;AAMG;AACK,IAAA,YAAY,CAAC,KAAmB,EAAA;AACtC,QAAA,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE;AAAE,YAAA,OAAO,KAAK;QAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;AAC/B,QAAA,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,GAAG,MAAM;IACnF;AAEQ,IAAA,UAAU,CAAC,KAAqB,EAAA;QACtC,cAAc,CAAC,MAAK;YAClB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,aAAa;YACvC,MAAM,EAAE,GAAG,IAAI,CAAC,aAAa,CAAc,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAE,CAAC;YACzE,EAAE,EAAE,KAAK,EAAE;AACb,QAAA,CAAC,CAAC;IACJ;IAEQ,QAAQ,CAAC,KAAqB,EAAE,GAAW,EAAA;QACjD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC9B,QAAA,IAAI,KAAK,KAAK,KAAK,EAAE;YACnB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO;AACpE,YAAA,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;QACpB;aAAO;AACL,YAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC9C;IACF;AAEQ,IAAA,IAAI,CAAC,KAAa,EAAA;AACxB,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE;QACnG,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC9C;AAEQ,IAAA,aAAa,CAAC,CAAS,EAAA;AAC7B,QAAA,OAAO,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;IACnD;AAEQ,IAAA,SAAS,CAAC,CAAS,EAAA;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE;QAC9C,IAAI,IAAI,IAAI,CAAC;AAAE,YAAA,OAAO,CAAC;AACvB,QAAA,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,IAAI,GAAG;IAC7C;IAEQ,UAAU,GAAA;QAChB,MAAM,KAAK,GAAkB,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE;AAClF,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC;IACvB;uGA1RW,QAAQ,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAR,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,QAAQ,knCCjErB,w/CA0CA,EAAA,aAAA,EAAA,EAAA,CAAA,iBAAA,CAAA,IAAA,EAAA,CAAA;;2FDuBa,QAAQ,EAAA,UAAA,EAAA,CAAA;kBANpB,SAAS;+BACE,WAAW,EAAA,aAAA,EAEN,iBAAiB,CAAC,IAAI,QAC/B,EAAE,SAAS,EAAE,WAAW,EAAE,EAAA,QAAA,EAAA,w/CAAA,EAAA;kzBAmEuC,OAAO,EAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,EAAA,CAAA;;AElIhF;;AAEG;;;;"}
@@ -0,0 +1,76 @@
1
+ import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
2
+
3
+ /**
4
+ * @license
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
8
+ */
9
+ /**
10
+ * Test harness for `<wr-sparkline>`.
11
+ *
12
+ * **The interesting assertion here is the ARIA one.** A sparkline is a trend line
13
+ * with no axes and no labels, so it is either given an `ariaLabel` — in which case it
14
+ * announces itself as an image — or it is decoration, and must be `aria-hidden` so a
15
+ * screen reader does not stop on a nameless graphic. The component switches between
16
+ * those two states on one input, and {@link isDecorative} is how a spec pins which
17
+ * one it is in.
18
+ *
19
+ * The line itself is an SVG path, and a path's `d` is not something a spec should
20
+ * assert: it is a rendering detail that changes with the viewBox, the stroke width
21
+ * and the smoothing. What IS worth asserting is whether the parts are there —
22
+ * {@link hasLine}, {@link hasArea}, {@link hasTip} — since each is an input the
23
+ * consumer set, and an empty dataset draws none of them.
24
+ *
25
+ * @see https://ngwr.dev/guides/testing
26
+ */
27
+ class WrSparklineHarness extends ComponentHarness {
28
+ static hostSelector = 'wr-sparkline';
29
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
30
+ static with(options = {}) {
31
+ return new HarnessPredicate(WrSparklineHarness, options).addOption('ariaLabel', options.ariaLabel, (harness, label) => HarnessPredicate.stringMatches(harness.getAccessibleName(), label));
32
+ }
33
+ svg = this.locatorFor('svg');
34
+ /** The name the chart announces, or `null` when it is decoration. */
35
+ async getAccessibleName() {
36
+ return (await this.svg()).getAttribute('aria-label');
37
+ }
38
+ /** The role the chart claims — `img` when it is named, `null` when it is not. */
39
+ async getRole() {
40
+ return (await this.svg()).getAttribute('role');
41
+ }
42
+ /**
43
+ * Whether the chart is hidden from assistive tech.
44
+ *
45
+ * The other half of {@link getAccessibleName}, and the pairing worth asserting
46
+ * together: a named sparkline must NOT be hidden, and an unnamed one must be, or a
47
+ * screen reader announces a graphic with nothing to say about it.
48
+ */
49
+ async isDecorative() {
50
+ return (await (await this.svg()).getAttribute('aria-hidden')) === 'true';
51
+ }
52
+ /** Whether a trend line was drawn — `false` for an empty or single-point dataset. */
53
+ async hasLine() {
54
+ return (await this.locatorForOptional('.wr-sparkline__line')()) !== null;
55
+ }
56
+ /** Whether the area under the line is filled (`showArea`). */
57
+ async hasArea() {
58
+ return (await this.locatorForOptional('.wr-sparkline__area')()) !== null;
59
+ }
60
+ /** Whether the last point is marked with a dot (`showTip`). */
61
+ async hasTip() {
62
+ return (await this.locatorForOptional('.wr-sparkline__tip')()) !== null;
63
+ }
64
+ /** The stroke colour as the component passed it through — a CSS value, not a resolved one. */
65
+ async getColor() {
66
+ const line = await this.locatorForOptional('.wr-sparkline__line')();
67
+ return line ? line.getAttribute('stroke') : null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Generated bundle index. Do not edit.
73
+ */
74
+
75
+ export { WrSparklineHarness };
76
+ //# sourceMappingURL=ngwr-sparkline-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngwr-sparkline-testing.mjs","sources":["../../../projects/lib/sparkline/testing/wr-sparkline-harness.ts","../../../projects/lib/sparkline/testing/ngwr-sparkline-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 { WrSparklineHarnessFilters } from './interfaces';\n\n/**\n * Test harness for `<wr-sparkline>`.\n *\n * **The interesting assertion here is the ARIA one.** A sparkline is a trend line\n * with no axes and no labels, so it is either given an `ariaLabel` — in which case it\n * announces itself as an image — or it is decoration, and must be `aria-hidden` so a\n * screen reader does not stop on a nameless graphic. The component switches between\n * those two states on one input, and {@link isDecorative} is how a spec pins which\n * one it is in.\n *\n * The line itself is an SVG path, and a path's `d` is not something a spec should\n * assert: it is a rendering detail that changes with the viewBox, the stroke width\n * and the smoothing. What IS worth asserting is whether the parts are there —\n * {@link hasLine}, {@link hasArea}, {@link hasTip} — since each is an input the\n * consumer set, and an empty dataset draws none of them.\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSparklineHarness extends ComponentHarness {\n static hostSelector = 'wr-sparkline';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrSparklineHarnessFilters = {}): HarnessPredicate<WrSparklineHarness> {\n return new HarnessPredicate(WrSparklineHarness, options).addOption(\n 'ariaLabel',\n options.ariaLabel,\n (harness, label) => HarnessPredicate.stringMatches(harness.getAccessibleName(), label)\n );\n }\n\n private readonly svg = this.locatorFor('svg');\n\n /** The name the chart announces, or `null` when it is decoration. */\n async getAccessibleName(): Promise<string | null> {\n return (await this.svg()).getAttribute('aria-label');\n }\n\n /** The role the chart claims — `img` when it is named, `null` when it is not. */\n async getRole(): Promise<string | null> {\n return (await this.svg()).getAttribute('role');\n }\n\n /**\n * Whether the chart is hidden from assistive tech.\n *\n * The other half of {@link getAccessibleName}, and the pairing worth asserting\n * together: a named sparkline must NOT be hidden, and an unnamed one must be, or a\n * screen reader announces a graphic with nothing to say about it.\n */\n async isDecorative(): Promise<boolean> {\n return (await (await this.svg()).getAttribute('aria-hidden')) === 'true';\n }\n\n /** Whether a trend line was drawn — `false` for an empty or single-point dataset. */\n async hasLine(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-sparkline__line')()) !== null;\n }\n\n /** Whether the area under the line is filled (`showArea`). */\n async hasArea(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-sparkline__area')()) !== null;\n }\n\n /** Whether the last point is marked with a dot (`showTip`). */\n async hasTip(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-sparkline__tip')()) !== null;\n }\n\n /** The stroke colour as the component passed it through — a CSS value, not a resolved one. */\n async getColor(): Promise<string | null> {\n const line = await this.locatorForOptional('.wr-sparkline__line')();\n return line ? line.getAttribute('stroke') : null;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;;;;;;;;AAiBG;AACG,MAAO,kBAAmB,SAAQ,gBAAgB,CAAA;AACtD,IAAA,OAAO,YAAY,GAAG,cAAc;;AAGpC,IAAA,OAAO,IAAI,CAAC,OAAA,GAAqC,EAAE,EAAA;AACjD,QAAA,OAAO,IAAI,gBAAgB,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC,SAAS,CAChE,WAAW,EACX,OAAO,CAAC,SAAS,EACjB,CAAC,OAAO,EAAE,KAAK,KAAK,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,KAAK,CAAC,CACvF;IACH;AAEiB,IAAA,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;;AAG7C,IAAA,MAAM,iBAAiB,GAAA;AACrB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,YAAY,CAAC;IACtD;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,MAAM,CAAC;IAChD;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,aAAa,CAAC,MAAM,MAAM;IAC1E;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,qBAAqB,CAAC,EAAE,MAAM,IAAI;IAC1E;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,qBAAqB,CAAC,EAAE,MAAM,IAAI;IAC1E;;AAGA,IAAA,MAAM,MAAM,GAAA;AACV,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,oBAAoB,CAAC,EAAE,MAAM,IAAI;IACzE;;AAGA,IAAA,MAAM,QAAQ,GAAA;QACZ,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,qBAAqB,CAAC,EAAE;AACnE,QAAA,OAAO,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,IAAI;IAClD;;;ACnFF;;AAEG;;;;"}