ngwr 14.5.1 → 14.7.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 (41) hide show
  1. package/alert/styles/_index.scss +14 -3
  2. package/avatar/styles/_index.scss +90 -0
  3. package/cascader/styles/_rules.scss +0 -4
  4. package/collapse/styles/_index.scss +0 -1
  5. package/fesm2022/ngwr-avatar.mjs +76 -8
  6. package/fesm2022/ngwr-avatar.mjs.map +1 -1
  7. package/fesm2022/ngwr-cascader.mjs +2 -2
  8. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  9. package/fesm2022/ngwr-image-cropper-testing.mjs +34 -3
  10. package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -1
  11. package/fesm2022/ngwr-image-cropper.mjs +245 -65
  12. package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
  13. package/fesm2022/ngwr-lightbox.mjs +2 -2
  14. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  15. package/fesm2022/ngwr-pagination.mjs +1 -1
  16. package/fesm2022/ngwr-pagination.mjs.map +1 -1
  17. package/fesm2022/ngwr-schema-form.mjs +1 -1
  18. package/fesm2022/ngwr-schema-form.mjs.map +1 -1
  19. package/fesm2022/ngwr-select-testing.mjs +21 -2
  20. package/fesm2022/ngwr-select-testing.mjs.map +1 -1
  21. package/fesm2022/ngwr-select.mjs +299 -50
  22. package/fesm2022/ngwr-select.mjs.map +1 -1
  23. package/fesm2022/ngwr-version.mjs +1 -1
  24. package/fesm2022/ngwr-version.mjs.map +1 -1
  25. package/image-cropper/styles/_index.scss +7 -0
  26. package/input/styles/_index.scss +8 -2
  27. package/lightbox/styles/_rules.scss +7 -1
  28. package/llms-full.txt +4 -4
  29. package/package.json +1 -1
  30. package/schematics/use/symbol-map.json +1 -0
  31. package/select/styles/_index.scss +50 -4
  32. package/skills/ngwr/references/catalog.md +1 -1
  33. package/table/styles/_index.scss +55 -0
  34. package/toast/styles/_index.scss +9 -3
  35. package/tree/styles/_index.scss +0 -3
  36. package/types/ngwr-avatar.d.ts +51 -4
  37. package/types/ngwr-image-cropper-testing.d.ts +25 -3
  38. package/types/ngwr-image-cropper.d.ts +136 -2
  39. package/types/ngwr-select-testing.d.ts +18 -1
  40. package/types/ngwr-select.d.ts +238 -10
  41. package/types/ngwr-version.d.ts +1 -1
@@ -16,7 +16,7 @@ import { InjectionToken } from '@angular/core';
16
16
  * Distinct from `Version` exported by `@angular/core`, which reports the
17
17
  * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.
18
18
  */
19
- const NGWR_VERSION = '14.5.1';
19
+ const NGWR_VERSION = '14.7.0';
20
20
 
21
21
  /**
22
22
  * @license
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-version.mjs","sources":["../../../projects/lib/version/version.ts","../../../projects/lib/version/version.token.ts","../../../projects/lib/version/ngwr-version.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\n/**\n * The ngwr library version string, kept in sync with\n * `projects/lib/package.json` by `scripts/release-prepare.ts`.\n *\n * Prefer this over reading `package.json` at runtime — that requires a JSON\n * import or `require`, neither of which tree-shakes well in Angular builds.\n *\n * Distinct from `Version` exported by `@angular/core`, which reports the\n * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.\n */\nexport const NGWR_VERSION = '14.5.1';\n","/**\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 { InjectionToken } from '@angular/core';\n\nimport { NGWR_VERSION } from './version';\n\n/**\n * Injection token holding the ngwr library version string.\n *\n * Default factory returns {@link NGWR_VERSION}. Override it in tests or in\n * apps that want to surface a custom build identifier.\n *\n * @example\n * ```ts\n * @Component({...})\n * export class FooterComponent {\n * readonly version = inject(NGWR_VERSION_TOKEN);\n * }\n * ```\n */\nexport const NGWR_VERSION_TOKEN = new InjectionToken<string>('NGWR_VERSION_TOKEN', {\n providedIn: 'root',\n factory: () => NGWR_VERSION,\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAEH;;;;;;;;;AASG;AACI,MAAM,YAAY,GAAG;;ACjB5B;;;;;AAKG;AAMH;;;;;;;;;;;;;AAaG;MACU,kBAAkB,GAAG,IAAI,cAAc,CAAS,oBAAoB,EAAE;AACjF,IAAA,UAAU,EAAE,MAAM;AAClB,IAAA,OAAO,EAAE,MAAM,YAAY;AAC5B,CAAA;;AC5BD;;AAEG;;;;"}
1
+ {"version":3,"file":"ngwr-version.mjs","sources":["../../../projects/lib/version/version.ts","../../../projects/lib/version/version.token.ts","../../../projects/lib/version/ngwr-version.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\n/**\n * The ngwr library version string, kept in sync with\n * `projects/lib/package.json` by `scripts/release-prepare.ts`.\n *\n * Prefer this over reading `package.json` at runtime — that requires a JSON\n * import or `require`, neither of which tree-shakes well in Angular builds.\n *\n * Distinct from `Version` exported by `@angular/core`, which reports the\n * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.\n */\nexport const NGWR_VERSION = '14.7.0';\n","/**\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 { InjectionToken } from '@angular/core';\n\nimport { NGWR_VERSION } from './version';\n\n/**\n * Injection token holding the ngwr library version string.\n *\n * Default factory returns {@link NGWR_VERSION}. Override it in tests or in\n * apps that want to surface a custom build identifier.\n *\n * @example\n * ```ts\n * @Component({...})\n * export class FooterComponent {\n * readonly version = inject(NGWR_VERSION_TOKEN);\n * }\n * ```\n */\nexport const NGWR_VERSION_TOKEN = new InjectionToken<string>('NGWR_VERSION_TOKEN', {\n providedIn: 'root',\n factory: () => NGWR_VERSION,\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAEH;;;;;;;;;AASG;AACI,MAAM,YAAY,GAAG;;ACjB5B;;;;;AAKG;AAMH;;;;;;;;;;;;;AAaG;MACU,kBAAkB,GAAG,IAAI,cAAc,CAAS,oBAAoB,EAAE;AACjF,IAAA,UAAU,EAAE,MAAM;AAClB,IAAA,OAAO,EAAE,MAAM,YAAY;AAC5B,CAAA;;AC5BD;;AAEG;;;;"}
@@ -23,6 +23,13 @@
23
23
  user-select: none;
24
24
  }
25
25
 
26
+ // A source the browser could not decode. The `<img>` stays in the DOM, since the
27
+ // next `src` goes through the same element, but some engines draw a broken-image
28
+ // glyph for it — and the failure is the host's to explain, not a stray icon's.
29
+ &--failed &__image {
30
+ display: none;
31
+ }
32
+
26
33
  &__backdrop {
27
34
  position: absolute;
28
35
  inset: 0;
@@ -199,8 +199,14 @@
199
199
  color: var(--wr-input-group-affix);
200
200
  cursor: pointer;
201
201
  transition: color var(--wr-transition-base);
202
-
203
- --wr-icon-size: 1rem;
202
+ // The eye is a bare `.wr-icon__svg`, which is `1em`, and a `<button>` does
203
+ // not inherit its font-size — so it drew the browser's control font (about
204
+ // 13.33px) with no reset and the surrounding text's size under `ngwr/reset`.
205
+ // Inheriting the size makes `1em` the text around the field in every app.
206
+ // The size only, not the `font` shorthand, which would also hand the button
207
+ // the group's line-height. `--wr-icon-size` only drives `<wr-icon>` hosts, so
208
+ // the declaration that used to sit here drove nothing.
209
+ font-size: inherit;
204
210
 
205
211
  &:hover:not(:disabled) {
206
212
  color: var(--wr-color-primary);
@@ -132,7 +132,13 @@
132
132
  touch-action: none;
133
133
 
134
134
  &__close {
135
- --wr-icon-size: 1.25rem;
135
+ // A bare `.wr-icon__svg` in a `<button>`: `1em`, and a button inherits no
136
+ // font-size — so the ✕ came out at the browser's control font, about
137
+ // 13.33px, inside a 36px circle, and at the surrounding text's size under
138
+ // `ngwr/reset`. Inheriting the size gives the same glyph either way. The
139
+ // size only, not the `font` shorthand. `--wr-icon-size` was never going to
140
+ // do it: only a `<wr-icon>` host reads that hook.
141
+ font-size: inherit;
136
142
  position: absolute;
137
143
  top: 1rem;
138
144
  inset-inline-end: 1rem;
package/llms-full.txt CHANGED
@@ -45,7 +45,7 @@
45
45
  - import: `import { WrAvatar } from 'ngwr/avatar'`
46
46
  - selector: `wr-avatar`
47
47
  - exports: WrAvatar, WrAvatarSize, WrAvatarShape
48
- - Image avatar with a fixed square or rounded shape. Shows a spinner while the image loads.
48
+ - Image avatar with a square, rounded, circle or squircle shape. Projected initials stand in while the image loads and when it fails.
49
49
 
50
50
  ## ngwr/back-top
51
51
  - import: `import { WrBackTop } from 'ngwr/back-top'`
@@ -568,7 +568,7 @@
568
568
  ## ngwr/image-cropper
569
569
  - import: `import { WrImageCropper } from 'ngwr/image-cropper'`
570
570
  - selector: `wr-image-cropper`
571
- - exports: WrImageCropper, WrCropRect, WrCropHandle, WrImageOutputType
571
+ - exports: WrImageCropper, WrCropRect, WrCropHandle, WrImageCropperStatus, WrImageLoadError, WrImageOutputType
572
572
  - DOM-based crop UI. Drag the box to move, drag any of the eight handles to resize. Optional `[aspectRatio]` locks the box shape. `(cropped)` fires with a `Blob` after each drag end — wire to upload or preview.
573
573
 
574
574
  ## ngwr/image-cropper/testing
@@ -840,8 +840,8 @@
840
840
 
841
841
  ## ngwr/select
842
842
  - import: `import { WrSelect } from 'ngwr/select'`
843
- - selector: `wr-option-group`, `wr-option`, `wr-select`
844
- - exports: WrSelect, WrOption, WrOptionGroup, WR_SELECT, WrSelectContext, WrSelectMode, WrSelectSearchLoader, WrSelectTagValidator, WrSelectSize
843
+ - selector: `wr-option-group`, `ng-template[wrOptionLeading]`, `wr-option`, `wr-select`
844
+ - exports: WrSelect, WrOption, WrOptionGroup, WrOptionLeading, WR_SELECT, WrSelectContext, WrOptionLeadingContext, WrOptionLeadingPlacement, WrSelectMode, WrSelectSearchLoader, WrSelectTagValidator, WrSelectSize
845
845
  - Native-like select built on CDK Overlay. A signal-forms native control — it implements `FormValueControl`, so `[formField]` binds straight to its `value` model. `[(value)]` works standalone, and `[(ngModel)]` / reactive forms keep working through [Angular 22's forms bridge](/guides/forms).
846
846
 
847
847
  ## ngwr/select/testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngwr",
3
- "version": "14.5.1",
3
+ "version": "14.7.0",
4
4
  "bin": {
5
5
  "ngwr-mcp": "mcp/server.js"
6
6
  },
@@ -165,6 +165,7 @@
165
165
  "WrSelect": "ngwr/select",
166
166
  "WrOption": "ngwr/select",
167
167
  "WrOptionGroup": "ngwr/select",
168
+ "WrOptionLeading": "ngwr/select",
168
169
  "WrShinyText": "ngwr/shiny-text",
169
170
  "WrShimmer": "ngwr/shiny-text",
170
171
  "WrSidebar": "ngwr/sidebar",
@@ -120,8 +120,15 @@
120
120
  color: var(--wr-color-text-muted);
121
121
  }
122
122
 
123
+ // The selected option's leading visual on a single-mode button trigger. Never
124
+ // shrinks: the label beside it is the part that truncates.
125
+ &__value-leading {
126
+ display: inline-flex;
127
+ flex: none;
128
+ align-items: center;
129
+ }
130
+
123
131
  &__chevron {
124
- --wr-icon-size: 1rem;
125
132
  flex: 0 0 auto;
126
133
  color: var(--wr-color-on-surface-muted);
127
134
  transition: transform var(--wr-transition-base);
@@ -171,6 +178,22 @@
171
178
  }
172
179
  }
173
180
 
181
+ // A leading visual inside a search trigger. The FIELD owns the leading
182
+ // padding here (see `--search` above), so the inset has to move with the
183
+ // visual — otherwise the avatar sits on the border. Only the trigger's own
184
+ // 0.5rem `gap` is then left between the two, which is the spacing the button
185
+ // trigger already has between `__value-leading` and `__value`.
186
+ &__trigger--search &__value-leading {
187
+ padding-inline-start: calc(var(--wr-select-padding-x) * var(--wr-density-x, 1));
188
+ }
189
+
190
+ // …and the field drops the inset it just handed over. Keyed on the sibling
191
+ // rather than on a modifier class so the two can never be set apart: the
192
+ // padding moves exactly when the element it moved to exists.
193
+ &__value-leading + &__search-input {
194
+ padding-inline-start: 0;
195
+ }
196
+
174
197
  &__search-input {
175
198
  flex: 1 1 auto;
176
199
  min-width: 0;
@@ -268,6 +291,17 @@
268
291
  user-select: none;
269
292
  }
270
293
 
294
+ // A selected option's leading visual — an avatar, an icon. Pulled toward the
295
+ // chip's start edge so a round avatar reads as the chip's cap rather than as a
296
+ // glyph floating in the padding, and spaced from the label by a little more
297
+ // than the chip's own gap.
298
+ &__chip-leading {
299
+ display: inline-flex;
300
+ flex: none;
301
+ align-items: center;
302
+ margin-inline: -0.25rem 0.125rem;
303
+ }
304
+
271
305
  &__chip-label {
272
306
  overflow: hidden;
273
307
  text-overflow: ellipsis;
@@ -275,7 +309,6 @@
275
309
  }
276
310
 
277
311
  &__chip-remove {
278
- --wr-icon-size: 0.875rem;
279
312
  display: inline-flex;
280
313
  align-items: center;
281
314
  justify-content: center;
@@ -301,7 +334,6 @@
301
334
  }
302
335
 
303
336
  &__clear {
304
- --wr-icon-size: 0.875rem;
305
337
  // Never shrink: the chips box is allowed to grow into the whole row, so the
306
338
  // × has to hold its own size against it.
307
339
  flex: 0 0 auto;
@@ -424,6 +456,14 @@
424
456
  }
425
457
  }
426
458
 
459
+ // An option's leading visual — a sibling of the label inside the row's flex,
460
+ // so `--wr-option-inline-gap` spaces it like any projected icon.
461
+ &__leading {
462
+ display: inline-flex;
463
+ flex: none;
464
+ align-items: center;
465
+ }
466
+
427
467
  // Search-mode filter — out-of-match options collapse but stay in the
428
468
  // DOM so registration order survives the panel re-open.
429
469
  &--hidden {
@@ -461,6 +501,13 @@
461
501
  position: relative;
462
502
  }
463
503
 
504
+ // A virtual row is a block, not a flex row (one line, so it can ellipsize),
505
+ // so the leading visual takes its spacing as a margin instead of the gap.
506
+ .wr-option__leading {
507
+ vertical-align: middle;
508
+ margin-inline-end: var(--wr-option-inline-gap, 0.5rem);
509
+ }
510
+
464
511
  // Keyboard cursor highlight — the virtualized list is navigated by
465
512
  // aria-activedescendant, so real focus never lands on the row and the tint
466
513
  // is all there was. `--wr-color-hover` is translucent and lands within about
@@ -534,7 +581,6 @@
534
581
 
535
582
  .wr-select__chip-remove,
536
583
  .wr-select__clear {
537
- --wr-icon-size: 1.125rem;
538
584
  width: 2rem;
539
585
  height: 2rem;
540
586
  }
@@ -171,7 +171,7 @@ root barrel for components.
171
171
  | `ngwr/scroll` | `import { WrScroll } from 'ngwr/scroll'` | — |
172
172
  | `ngwr/segmented` | `import { WrSegmented } from 'ngwr/segmented'` | `wr-segmented` |
173
173
  | `ngwr/segmented/testing` | `import { WrSegmentedHarness } from 'ngwr/segmented/testing'` | — |
174
- | `ngwr/select` | `import { WrSelect } from 'ngwr/select'` | `wr-option-group` `wr-option` `wr-select` |
174
+ | `ngwr/select` | `import { WrSelect } from 'ngwr/select'` | `wr-option-group` `ng-template[wrOptionLeading]` `wr-option` `wr-select` |
175
175
  | `ngwr/select/testing` | `import { WrOptionHarness } from 'ngwr/select/testing'` | — |
176
176
  | `ngwr/shiny-text` | `import { WrShinyText } from 'ngwr/shiny-text'` | `[wrShimmer]` `wr-shiny-text` |
177
177
  | `ngwr/shiny-text/testing` | `import { WrShinyTextHarness } from 'ngwr/shiny-text/testing'` | — |
@@ -376,6 +376,28 @@
376
376
  display: inline-flex;
377
377
  align-items: center;
378
378
  gap: 0.375rem;
379
+
380
+ // The spacing of the header's controls, and it moves for a COARSE pointer
381
+ // only. A column that is both `sortable` and `filterable` puts two 12px
382
+ // controls in this row 0.375rem apart, so their centres are 18px apart —
383
+ // while the touch halo each one grows below is 24px wide and centred on its
384
+ // control. Two 24px boxes with centres 18px apart OVERLAP by 6px, and the
385
+ // later one in the DOM (the filter) wins the hit test, so the sort button
386
+ // would hand over a strip of the slack it just gained. 0.75rem puts the
387
+ // centres 24px apart, which is the closest the two halos can come: at rest
388
+ // they abut exactly, and a filter showing a count pill is 32px wide, which
389
+ // only separates them further. A fine pointer keeps the 0.375rem this row
390
+ // has always drawn.
391
+ //
392
+ // This gap is also the whole of what a COARSE-pointer `target-size` run
393
+ // measures. axe never sees the halo — an `::after` enters no rect, so it
394
+ // reads 12x12 either way — but 24px between centres satisfies WCAG 2.5.8's
395
+ // spacing exception exactly, and the pair passes. Narrow this and a coarse
396
+ // run reports both nodes again; `table-header-target.spec.ts` pins the
397
+ // value here so a PR catches it without a browser.
398
+ @media (pointer: coarse) {
399
+ gap: 0.75rem;
400
+ }
379
401
  }
380
402
 
381
403
  &__title {
@@ -398,6 +420,24 @@
398
420
  // reads it, and the rule here used to set it for nothing.
399
421
  font-size: inherit;
400
422
  line-height: 0;
423
+ // The pointer target, widened for a COARSE pointer and for nothing else.
424
+ // WCAG 2.5.8 asks 24x24 of every control, a mouse included, and this glyph
425
+ // is 12x12: a lone one passes on the criterion's spacing exception, and a
426
+ // column carrying both this button and a filter trigger 6px away does not.
427
+ // A real 24px box on every pointer is what satisfies the criterion outright,
428
+ // and it was written, measured and REFUSED — it moves the caret and the
429
+ // funnel apart on a mouse, and the maintainer chose to keep the geometry
430
+ // 14.6.0 draws. So the coarse half ships, the fine-pointer failure is a
431
+ // decided trade recorded in AGENTS.md and in `scripts/contrast-baseline.json`,
432
+ // and the table's docs page says so to the reader. Do not re-report it.
433
+ //
434
+ // 24px rather than the mixin's 44px default, which is a real constraint and
435
+ // not a preference: the halo is centred on the control, so 44 reaches 22px
436
+ // either side and swallows the filter trigger beside it — the mixin's own
437
+ // docblock warns off tightly-packed rows for exactly this. 24 is the
438
+ // criterion's number, and `__th-inner` opens its gap under the same media
439
+ // query so the two halos cannot overlap.
440
+ @include theme.touch-target($size: 24px);
401
441
  }
402
442
 
403
443
  &__tr {
@@ -810,6 +850,13 @@
810
850
  // count pill beside the funnel — 10px type with no line-height of its own —
811
851
  // grows from 12px to 20px tall under it.
812
852
  font-size: inherit;
853
+ // The same coarse-pointer target as the sort button beside it, where the
854
+ // whole reasoning — why 24 and not 44, and why a fine pointer keeps the
855
+ // 12px glyph — is written out. It goes on the TRIGGER and never on the
856
+ // `<wr-table-filter>` host: the halo belongs to the element that owns the
857
+ // click, and a tap landing on the host's own pseudo-element would target
858
+ // the host and open nothing.
859
+ @include theme.touch-target($size: 24px);
813
860
  }
814
861
 
815
862
  &__count {
@@ -822,6 +869,14 @@
822
869
  background: var(--wr-color-primary);
823
870
  color: var(--wr-color-primary-contrast);
824
871
  font-size: 0.625rem; // 10px — intentional micro-count badge, below --wr-text-xs
872
+ // The pill has no padding on the block axis, so its height IS its line box,
873
+ // and leaving that at `normal` handed the box to whatever the page
874
+ // inherited: 12px in an app with no reset, 20px under `ngwr/reset`, whose
875
+ // `font: inherit` on a `<button>` brings the header's 1.25rem down with the
876
+ // font-size. A ratio of its own makes it 12px in both — and a ratio rather
877
+ // than a length, so a consumer who enlarges the numeral gets a pill that
878
+ // grows with it.
879
+ line-height: 1.2;
825
880
  font-weight: 600;
826
881
  }
827
882
 
@@ -205,8 +205,15 @@ $toast-colors: (
205
205
  transition:
206
206
  color var(--wr-transition-base),
207
207
  background var(--wr-transition-base);
208
-
209
- --wr-icon-size: 0.875rem;
208
+ // The glyph is a bare `.wr-icon__svg` — `1em` — and a `<button>` does not
209
+ // inherit its font-size, so it drew the browser's control font (about
210
+ // 13.33px) with no reset and the surrounding text's size under `ngwr/reset`.
211
+ // Inheriting the size makes `1em` the toast's own text in every app; only a
212
+ // `<wr-icon>` host reads `--wr-icon-size`, so the hook never applied here.
213
+ // The SIZE only, never the `font` shorthand: that would also bring the
214
+ // surrounding line-height, which is what stretched the table's filter count
215
+ // pill from 12px to 20px when it was tried there.
216
+ font-size: inherit;
210
217
 
211
218
  &:hover {
212
219
  color: var(--wr-color-on-surface);
@@ -323,7 +330,6 @@ $toast-colors: (
323
330
  width: 2rem;
324
331
  height: 2rem;
325
332
  padding: 0;
326
- --wr-icon-size: 1.125rem;
327
333
  }
328
334
 
329
335
  .wr-toast-host__close-all {
@@ -249,7 +249,6 @@
249
249
  }
250
250
 
251
251
  &__chevron {
252
- --wr-icon-size: 1rem;
253
252
  flex: 0 0 auto;
254
253
  color: var(--wr-color-on-surface-muted);
255
254
  transition: transform var(--wr-transition-base);
@@ -260,7 +259,6 @@
260
259
  }
261
260
 
262
261
  &__clear {
263
- --wr-icon-size: 0.875rem;
264
262
  display: inline-flex;
265
263
  align-items: center;
266
264
  justify-content: center;
@@ -329,7 +327,6 @@
329
327
  }
330
328
 
331
329
  &__chip-remove {
332
- --wr-icon-size: 0.875rem;
333
330
  display: inline-flex;
334
331
  align-items: center;
335
332
  justify-content: center;
@@ -40,19 +40,38 @@ type WrAvatarSize = number | `${number}` | `${number}px` | `${number}rem`;
40
40
  /**
41
41
  * Image avatar with configurable corner treatment.
42
42
  *
43
- * A spinner is shown while the image loads, then crossfades in.
43
+ * Projected content (typically initials) is the FALLBACK: it shows with no
44
+ * `url`, while the image loads and after it fails, and leaves the paint once the
45
+ * image has loaded. It is projected into a `.wr-avatar__content` wrapper, so a
46
+ * `wr-avatar > …` child selector no longer reaches it. While it is there, no
47
+ * spinner is drawn over it; with nothing projected, the spinner marks the
48
+ * loading state instead. See `alt` for what a screen reader hears.
49
+ *
50
+ * Projected initials take the text size around the avatar, capped at
51
+ * `--wr-avatar-initials-scale` (`0.375`) of the box, so they shrink only where
52
+ * they would not fit: a 16px avatar in a 14px chip draws 6px letters, while a
53
+ * `6rem` one keeps the surrounding size up to 36px and is capped only past it.
54
+ * Anything projected that is sized in `em` follows the cap too — a `<wr-icon>`
55
+ * at its default `1.2em` included — so pin a `px` or `rem` size on the projected
56
+ * element, or override the hook, to keep it. A `font-size` rule on the avatar
57
+ * itself still wins, unless it sits in a cascade layer (Tailwind v4's utilities
58
+ * do), which ngwr's unlayered stylesheet beats whatever the selector.
59
+ *
60
+ * The initials are inert: `pointer-events: none` hands a click on them to whatever
61
+ * holds the avatar, and `user-select: none` keeps them out of a text selection.
44
62
  *
45
63
  * @example
46
64
  * ```html
47
65
  * <wr-avatar url="/me.png" alt="Roman" size="3rem" shape="circle" />
48
- * <wr-avatar url="/me.png" alt="Roman" [size]="48" />
66
+ * <wr-avatar [url]="photo" alt="Roman Khegay" [size]="48">RK</wr-avatar>
49
67
  * ```
50
68
  *
51
69
  * @see https://ngwr.dev/reference/components/avatar
52
70
  */
53
71
  declare class WrAvatar {
54
72
  /**
55
- * Image URL. When unset, only projected content (e.g. initials) renders.
73
+ * Image URL. Projected content (e.g. initials) shows while it is unset,
74
+ * loading or broken, and gives way once the image has loaded.
56
75
  *
57
76
  * @default null
58
77
  */
@@ -61,6 +80,11 @@ declare class WrAvatar {
61
80
  * Alt text for the image. Falls back to `avatar.alt`, then `'Avatar'` — pass
62
81
  * the person's name where you have it, since that is what a reader wants.
63
82
  *
83
+ * With an `alt`, projected initials are hidden from assistive technology while
84
+ * an image is in play, so the name is announced once. Without one, the image
85
+ * carries only the generic name, so the initials stay announced beside it —
86
+ * visually hidden once it has loaded, never dropped.
87
+ *
64
88
  * @default null
65
89
  */
66
90
  readonly alt: _angular_core.InputSignal<string | null>;
@@ -73,7 +97,8 @@ declare class WrAvatar {
73
97
  */
74
98
  readonly shape: _angular_core.InputSignal<WrAvatarShape>;
75
99
  /**
76
- * Box size. See {@link WrAvatarSize} for accepted values.
100
+ * Box size. See {@link WrAvatarSize} for accepted values. The resolved value is
101
+ * published on the host as `--wr-avatar-size`.
77
102
  *
78
103
  * @default '6rem'
79
104
  */
@@ -85,8 +110,30 @@ declare class WrAvatar {
85
110
  * worth showing instead.
86
111
  */
87
112
  protected readonly failed: _angular_core.WritableSignal<boolean>;
113
+ private readonly img;
88
114
  constructor();
115
+ /**
116
+ * A prerendered `<img>` starts loading when the browser parses the HTML, and
117
+ * `(load)` / `(error)` are only attached at hydration — so on a slow bundle the
118
+ * image settles first, both events go nowhere, and the avatar sits on its
119
+ * fallback over a photo it already has (or names a broken image it never
120
+ * learns about). Only the FIRST render can race: every later `<img>` is
121
+ * created on the client with its listeners in place before `src` is set.
122
+ */
123
+ private adoptSettledImage;
89
124
  protected readonly resolved: _angular_core.Signal<ResolvedCssSize>;
125
+ /**
126
+ * Whether the projected fallback is `aria-hidden`: an `<img>` is in the DOM
127
+ * (loading or loaded) AND the consumer named it. Then the image's `alt` owns
128
+ * the name, announced once and unchanged at the moment the photo arrives.
129
+ * Without a real `alt` the image is only "Avatar", and hiding the initials
130
+ * would drop the one thing that says who this is.
131
+ *
132
+ * The stylesheet keys on the same attribute once `--loaded`: a hidden fallback
133
+ * leaves the paint through `visibility`, a named-by-initials one is clipped and
134
+ * stays in the accessibility tree. While loading it is painted either way.
135
+ */
136
+ protected readonly fallbackHidden: _angular_core.Signal<boolean>;
90
137
  protected readonly cssSize: _angular_core.Signal<string>;
91
138
  protected readonly pxSize: _angular_core.Signal<number | null>;
92
139
  protected readonly classes: _angular_core.Signal<string>;
@@ -72,9 +72,10 @@ interface WrCropWindowBox {
72
72
  * arrives on the `(cropped)` output, which emits a `Blob` after each drag end or
73
73
  * each run of arrow keys, and a consumer who wants a one-off read calls `toBlob()` /
74
74
  * `toDataUrl()` — or `cropRect()` for the geometry — on the component through a
75
- * `viewChild`. An output and three instance members: component API rather than DOM,
76
- * so all of it is outside what a harness can reach at all, which is worth knowing
77
- * rather than discovering.
75
+ * `viewChild`. The same goes for `(loadError)` and `status()`, which is why
76
+ * {@link isFailed} reads the failure off the host class instead. Outputs and instance
77
+ * members are component API rather than DOM, so all of it is outside what a harness
78
+ * can reach at all, which is worth knowing rather than discovering.
78
79
  *
79
80
  * @example
80
81
  * ```ts
@@ -127,6 +128,27 @@ declare class WrImageCropperHarness extends ComponentHarness {
127
128
  * a zero-sized image.
128
129
  */
129
130
  dispatchImageLoad(): Promise<void>;
131
+ /**
132
+ * Fire the image's `error` event — what a browser does with a source it cannot
133
+ * decode, and the only way to get one in a unit test, where nothing is decoded.
134
+ *
135
+ * Afterwards {@link isFailed} is `true` and the component has emitted
136
+ * `(loadError)`; a spec that wants the payload binds that output on its host.
137
+ */
138
+ dispatchImageError(): Promise<void>;
139
+ /**
140
+ * Whether the browser failed to decode the current source.
141
+ *
142
+ * Read off the `wr-image-cropper--failed` host modifier — the part of the failure
143
+ * the DOM shows. It is a third answer beside {@link isEmpty} and {@link isReady},
144
+ * not a combination of them: a failed cropper has a `src` (so it is not empty) and
145
+ * no crop window (so it is not ready), which is also exactly what a cropper still
146
+ * LOADING looks like. Asking those two would call a broken image a slow one.
147
+ *
148
+ * The component renders no message for it, so there is no text to read here; the
149
+ * host's own message is the host's to assert.
150
+ */
151
+ isFailed(): Promise<boolean>;
130
152
  /**
131
153
  * Where the crop window sits over the displayed image, in CSS pixels.
132
154
  *