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
@@ -18,6 +18,45 @@ interface WrCropRect {
18
18
  type WrCropHandle = 'move' | 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
19
19
  /** Supported output MIME types for {@link WrImageCropper.toBlob}. */
20
20
  type WrImageOutputType = 'image/png' | 'image/jpeg' | 'image/webp';
21
+ /**
22
+ * Where the cropper's source stands.
23
+ *
24
+ * - `empty` — no `src`, so the placeholder is showing.
25
+ * - `loading` — a `src` has been handed to the `<img>` and the browser has not
26
+ * answered yet.
27
+ * - `loaded` — the image decoded. The crop window opens once it also has a
28
+ * measured size, which in a browser is the same moment.
29
+ * - `failed` — the browser could not decode it (a renamed `.heic`, a truncated
30
+ * file, a URL that 404s). `(loadError)` fired with the details.
31
+ */
32
+ type WrImageCropperStatus = 'empty' | 'loading' | 'loaded' | 'failed';
33
+ /**
34
+ * The payload of `(loadError)` — plain strings and numbers, so it can be logged,
35
+ * sent to an error tracker or put into a store as it is.
36
+ *
37
+ * The browser's `error` event carries no reason at all, so none is invented here:
38
+ * what the source DECLARED is the most a host has to work with, and for the common
39
+ * case (a phone photo in a format this browser cannot decode) `type` and `name`
40
+ * are what tell the two apart from a broken upload.
41
+ */
42
+ interface WrImageLoadError {
43
+ /**
44
+ * The URL the `<img>` was given — the `src` string itself, or the object URL
45
+ * made for a `File` / `Blob`. An object URL is revoked as soon as `src` changes,
46
+ * so it identifies the attempt rather than being something to fetch later.
47
+ */
48
+ readonly url: string;
49
+ /** `File.name` for a `File` source; `null` for a `Blob` or a string. */
50
+ readonly name: string | null;
51
+ /**
52
+ * The MIME type a `File` / `Blob` declares — `''` when it declares none, and
53
+ * `null` for a string source. A `File`'s type comes from its extension, so a
54
+ * renamed file declares the type of the name it was given.
55
+ */
56
+ readonly type: string | null;
57
+ /** Size in bytes for a `File` / `Blob`; `null` for a string source. */
58
+ readonly size: number | null;
59
+ }
21
60
 
22
61
  interface RectPx {
23
62
  x: number;
@@ -37,12 +76,18 @@ interface RectPx {
37
76
  * freshly-rendered `Blob` of the cropped region. For one-off reads use
38
77
  * `toBlob()` / `toDataUrl()`.
39
78
  *
79
+ * A source the browser cannot decode fires `(loadError)` once, sets `status()` to
80
+ * `'failed'` and puts `wr-image-cropper--failed` on the host. The component renders
81
+ * no message of its own for it — what went wrong and what to do next is the host's
82
+ * to say.
83
+ *
40
84
  * @example
41
85
  * ```html
42
86
  * <wr-image-cropper
43
87
  * [src]="file"
44
88
  * [aspectRatio]="1"
45
89
  * (cropped)="onBlob($event)"
90
+ * (loadError)="onUnreadable($event)"
46
91
  * />
47
92
  * ```
48
93
  *
@@ -61,8 +106,23 @@ declare class WrImageCropper {
61
106
  readonly outputType: _angular_core.InputSignal<WrImageOutputType>;
62
107
  /** JPEG / WebP quality for `(cropped)` in [0, 1]. @default 0.92 */
63
108
  readonly outputQuality: _angular_core.InputSignal<number>;
109
+ /**
110
+ * Longest side of the exported image, in pixels — for `(cropped)`, `toBlob()` and
111
+ * `toDataUrl()` alike. A crop larger than this is scaled down to fit, keeping its
112
+ * aspect ratio; a smaller one is exported as it is, never upscaled. The crop itself
113
+ * — `cropRect()`, in source pixels — is not affected. `null` (or anything that is
114
+ * not a positive number) exports at the source image's own resolution.
115
+ * @default null
116
+ */
117
+ readonly maxOutputSize: _angular_core.InputSignalWithTransform<number | null, unknown>;
64
118
  /** Emits a Blob after each drag end. */
65
119
  readonly cropped: _angular_core.OutputEmitterRef<Blob>;
120
+ /**
121
+ * Fires once when the browser cannot decode the current `src` — a format it does
122
+ * not support, a broken file, a URL that fails. Nothing is rendered for it; show
123
+ * your own message. A new `src` clears the failure.
124
+ */
125
+ readonly loadError: _angular_core.OutputEmitterRef<WrImageLoadError>;
66
126
  protected readonly imgEl: _angular_core.Signal<ElementRef<HTMLImageElement>>;
67
127
  /** Resolved object URL for `src` (so File / Blob render in `<img>`). */
68
128
  protected readonly emptyText: _angular_core.Signal<string>;
@@ -97,7 +157,20 @@ declare class WrImageCropper {
97
157
  private active;
98
158
  private startPointer;
99
159
  private startRect;
160
+ /** The element the live drag set pointer capture on, so the end releases that one. */
161
+ private captureTarget;
162
+ /** The pointer the live drag belongs to. Every other pointer's events are ignored. */
163
+ private pointerId;
164
+ /** How far past each side of the crop the live resize was pressed — see `onPointerMove`. */
165
+ private overhang;
100
166
  private readonly destroyRef;
167
+ private readonly statusState;
168
+ /**
169
+ * Where the source stands: `'empty'`, `'loading'`, `'loaded'` or `'failed'`. Reset
170
+ * to `'loading'` (or `'empty'`) whenever `src` changes, so a failure never outlives
171
+ * the source that caused it.
172
+ */
173
+ readonly status: _angular_core.Signal<WrImageCropperStatus>;
101
174
  /** Resolved crop rect in natural (source) pixel coordinates. */
102
175
  readonly cropRect: _angular_core.Signal<WrCropRect>;
103
176
  constructor();
@@ -113,6 +186,18 @@ declare class WrImageCropper {
113
186
  */
114
187
  private resetGeometry;
115
188
  protected onImageLoad(): void;
189
+ /**
190
+ * The `<img>` could not decode its source.
191
+ *
192
+ * Before this handler existed a failed decode left the component exactly as it
193
+ * looks while loading — an image with no size, no crop window, nothing emitted —
194
+ * so a host had no way to tell "still decoding" from "never going to", and a user
195
+ * who picked a `.heic` the browser cannot read saw an empty box and no reason.
196
+ *
197
+ * Once per source: `status` is reset by a new `src` and by nothing else, so a
198
+ * second `error` for the same source says nothing new and is not re-emitted.
199
+ */
200
+ protected onImageError(): void;
116
201
  /**
117
202
  * Keep `display` on the box the image is actually rendered at.
118
203
  *
@@ -138,8 +223,50 @@ declare class WrImageCropper {
138
223
  private initialCrop;
139
224
  protected readonly handles: readonly WrCropHandle[];
140
225
  protected onPointerDown(handle: WrCropHandle, event: PointerEvent): void;
226
+ /**
227
+ * A resize ENDS when the pointer leaves the image, exactly as `pointerup` would end it:
228
+ * the move that carried it out is applied — clamped to the image like any other — and
229
+ * then the drag is over. Nothing after that resumes it or changes the crop, pointer
230
+ * back over the image or not, until a new `pointerdown`. A move drag is not affected:
231
+ * its size is fixed, so it simply rests against the edge.
232
+ *
233
+ * The move that leaves is applied rather than dropped because pointer moves are
234
+ * SAMPLES. Dropped, a quick drag toward a corner would stop wherever its last sample
235
+ * inside happened to fall — a single move from a handle to past the image would change
236
+ * nothing at all — and even a slow one would stop a fraction of a pixel short of the
237
+ * edge. A drag that leaves across one edge still ends there: heading for a corner at a
238
+ * slant that crosses the bottom first, the crop's right edge stays where it was then.
239
+ *
240
+ * "The image" is the `<img>`'s own box — the bounds `applyMove` / `applyResize` clamp
241
+ * the crop to — with its edges counting as inside, since the crop may reach them. The
242
+ * box is re-measured on every move and compared with `clientX` / `clientY`: both are
243
+ * viewport coordinates, so a scroll mid-drag cannot put them out of step. The moves
244
+ * outside reach this handler at all only because the drag holds pointer capture.
245
+ *
246
+ * One allowance, fixed at `pointerdown`: the box is widened on each side by however far
247
+ * past that side of the CROP the press landed — at most as far as the pressed handle
248
+ * reaches past it. Half of an edge handle, and three quarters of a corner handle, lies
249
+ * outside the crop, so a press there already has the pointer ahead of the edge it drags.
250
+ * Measured without the allowance, that pointer would leave the image while the crop was
251
+ * still short of the image's edge by the same distance, with nothing able to close the
252
+ * gap; and a handle resting on the image's edge, pressed on its outer part, would begin
253
+ * its drag already outside. A press on a handle's inner part widens nothing.
254
+ */
141
255
  protected onPointerMove(event: PointerEvent): void;
256
+ /**
257
+ * Ends the drag — for `pointerup`, `pointercancel`, a capture lost to something else,
258
+ * and a resize whose pointer left the image. Once per drag: the capture released here
259
+ * fires `lostpointercapture` afterwards, and by then there is nothing left to end.
260
+ *
261
+ * Only for the pointer that started it. A second finger is refused a drag of its own at
262
+ * `pointerdown`, but its moves and its lifting reach these handlers all the same, and
263
+ * would otherwise drive or end the first finger's drag.
264
+ */
142
265
  protected onPointerUp(event: PointerEvent): void;
266
+ /** How far past each side of the crop window the pointer is, in CSS pixels. */
267
+ private pastCrop;
268
+ /** Whether the pointer is on the image, edges and the press's `overhang` included. */
269
+ private withinImage;
143
270
  /** Whether a key has moved the crop since the last `keyup`. */
144
271
  private keyboardDirty;
145
272
  /**
@@ -176,11 +303,18 @@ declare class WrImageCropper {
176
303
  toDataUrl(type?: WrImageOutputType, quality?: number): string;
177
304
  /** Re-emit the crop without waiting for a drag (e.g. after a programmatic change). */
178
305
  refresh(): Promise<void>;
306
+ /**
307
+ * The exported image's pixel size: the crop in source pixels, scaled down so its
308
+ * longer side fits `maxOutputSize`. `Math.min(1, …)` is the never-upscale rule — a
309
+ * crop already inside the cap keeps its own size, which is also the whole of the
310
+ * uncapped path.
311
+ */
312
+ private outputSize;
179
313
  private toCanvas;
180
314
  private emitCropped;
181
315
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<WrImageCropper, never>;
182
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrImageCropper, "wr-image-cropper", never, { "src": { "alias": "src"; "required": false; "isSignal": true; }; "aspectRatio": { "alias": "aspectRatio"; "required": false; "isSignal": true; }; "minWidth": { "alias": "minWidth"; "required": false; "isSignal": true; }; "minHeight": { "alias": "minHeight"; "required": false; "isSignal": true; }; "outputType": { "alias": "outputType"; "required": false; "isSignal": true; }; "outputQuality": { "alias": "outputQuality"; "required": false; "isSignal": true; }; }, { "cropped": "cropped"; }, never, never, true, never>;
316
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrImageCropper, "wr-image-cropper", never, { "src": { "alias": "src"; "required": false; "isSignal": true; }; "aspectRatio": { "alias": "aspectRatio"; "required": false; "isSignal": true; }; "minWidth": { "alias": "minWidth"; "required": false; "isSignal": true; }; "minHeight": { "alias": "minHeight"; "required": false; "isSignal": true; }; "outputType": { "alias": "outputType"; "required": false; "isSignal": true; }; "outputQuality": { "alias": "outputQuality"; "required": false; "isSignal": true; }; "maxOutputSize": { "alias": "maxOutputSize"; "required": false; "isSignal": true; }; }, { "cropped": "cropped"; "loadError": "loadError"; }, never, never, true, never>;
183
317
  }
184
318
 
185
319
  export { WrImageCropper };
186
- export type { WrCropHandle, WrCropRect, WrImageOutputType };
320
+ export type { WrCropHandle, WrCropRect, WrImageCropperStatus, WrImageLoadError, WrImageOutputType };
@@ -51,8 +51,25 @@ declare class WrOptionHarness extends ComponentHarness {
51
51
  static hostSelector: string;
52
52
  /** Build a predicate that narrows the query. */
53
53
  static with(options?: WrOptionHarnessFilters): HarnessPredicate<WrOptionHarness>;
54
- /** The option's label, trimmed. */
54
+ /**
55
+ * The option's text as drawn, trimmed — without its leading visual.
56
+ *
57
+ * A `wrOptionLeading` template renders INSIDE the row, so reading the whole
58
+ * host would hand back an avatar's initials as the first word of the name. It
59
+ * is excluded here for the same reason the select excludes it from the label.
60
+ *
61
+ * This is the drawn text, not the `label` input: an option whose `label` is
62
+ * shorter than what it draws still answers with what it draws. What the select
63
+ * REPORTS for it is on the select harness, as `getValueText()` or
64
+ * `getChipLabels()`.
65
+ */
55
66
  getText(): Promise<string>;
67
+ /**
68
+ * Whether the row is drawing a leading visual right now. Reads the slot the
69
+ * select renders the template into, so it answers `false` for an option that
70
+ * declares a template while the panel is closed — which is the contract.
71
+ */
72
+ hasLeading(): Promise<boolean>;
56
73
  /** Whether the option is currently selected. */
57
74
  isSelected(): Promise<boolean>;
58
75
  /** Whether the option refuses selection. */
@@ -39,6 +39,25 @@ type WrSelectTagValidator = (value: string, existing: readonly string[]) => bool
39
39
  type WrSelectSearchLoader<T> = (query: string) => Observable<readonly T[]> | Promise<readonly T[]> | readonly T[];
40
40
  /** Control size for `<wr-select>` — shares the `--wr-control-*` contract. */
41
41
  type WrSelectSize = 'sm' | 'md' | 'lg';
42
+ /**
43
+ * Where a `wrOptionLeading` template is being drawn:
44
+ *
45
+ * - `'option'` — the row in the open panel.
46
+ * - `'chip'` — a selected chip on a `mode="multi"` trigger.
47
+ * - `'value'` — the selected value on a single-mode trigger: beside the label on
48
+ * a button trigger, and before the input on a search-shaped one while that
49
+ * input is showing the label. One placement rather than two, because it is one
50
+ * surface — the same size is right on both, and splitting it would ask every
51
+ * consumer to name a case they do not have.
52
+ */
53
+ type WrOptionLeadingPlacement = 'option' | 'chip' | 'value';
54
+ /** Template context of `<ng-template wrOptionLeading>`. */
55
+ interface WrOptionLeadingContext<T = unknown> {
56
+ /** The option's value — `let-value`. */
57
+ readonly $implicit: T;
58
+ /** Where this copy is drawn, so one template can size itself per surface. */
59
+ readonly placement: WrOptionLeadingPlacement;
60
+ }
42
61
 
43
62
  /**
44
63
  * @license
@@ -67,6 +86,13 @@ interface WrSelectOptionRegistration {
67
86
  * registration that cannot supply an element simply keeps its place.
68
87
  */
69
88
  readonly host?: HTMLElement;
89
+ /**
90
+ * The option's own `wrOptionLeading` template, when it declares one. Chips
91
+ * look it up by value, since a chip is drawn by the select and not by the
92
+ * option. Optional, like `host`: a registration without one falls back to the
93
+ * select's default template.
94
+ */
95
+ readonly leading?: Signal<TemplateRef<WrOptionLeadingContext> | null>;
70
96
  }
71
97
  /**
72
98
  * Contract a `<wr-option>` uses to talk to its parent `<wr-select>`.
@@ -103,6 +129,17 @@ interface WrSelectContext {
103
129
  * `[serverSearch]`), in which case options must NOT self-hide.
104
130
  */
105
131
  readonly clientFilter: Signal<boolean>;
132
+ /**
133
+ * Whether the panel is open. An option draws its leading visual only while it
134
+ * is, because projected options exist — detached — for the whole life of the
135
+ * select, and a template instantiated there would be every avatar at once.
136
+ */
137
+ readonly panelOpen: Signal<boolean>;
138
+ /**
139
+ * The select-wide `wrOptionLeading` template — one declared as a direct child
140
+ * of `<wr-select>` — or `null`. An option with a template of its own ignores it.
141
+ */
142
+ readonly optionLeading: Signal<TemplateRef<WrOptionLeadingContext> | null>;
106
143
  /** Is the given option value currently selected? Handles both single and multi. */
107
144
  isSelected(value: unknown): boolean;
108
145
  /**
@@ -125,6 +162,8 @@ declare const WR_SELECT: InjectionToken<WrSelectContext>;
125
162
  interface SelectedChip {
126
163
  readonly value: unknown;
127
164
  readonly label: string;
165
+ /** The leading template this chip draws, or `null`. */
166
+ readonly leading: TemplateRef<WrOptionLeadingContext> | null;
128
167
  }
129
168
  /**
130
169
  * Combobox primitive. Renders a button that opens a CDK overlay
@@ -514,6 +553,61 @@ declare class WrSelect implements FormValueControl<unknown>, WrSelectContext {
514
553
  /** Id of the active option (for `aria-activedescendant`). */
515
554
  readonly activeOptionId: _angular_core.Signal<string | null>;
516
555
  protected readonly open: _angular_core.WritableSignal<boolean>;
556
+ /** WrSelectContext — whether the panel is open. Options gate their leading visual on it. */
557
+ readonly panelOpen: _angular_core.Signal<boolean>;
558
+ /**
559
+ * A `<ng-template wrOptionLeading>` declared as a DIRECT child of the select —
560
+ * `descendants: false`, so an option's own template is never mistaken for the
561
+ * select-wide default.
562
+ */
563
+ private readonly defaultLeading;
564
+ /**
565
+ * WrSelectContext — the select-wide leading template, or `null`. The only
566
+ * way to give a leading visual to the rows the select draws itself (`[options]`,
567
+ * `[loader]` results, virtual rows), and the default for projected options that
568
+ * declare none of their own.
569
+ */
570
+ readonly optionLeading: _angular_core.Signal<TemplateRef<WrOptionLeadingContext<unknown>> | null>;
571
+ /**
572
+ * The leading template for a value: the registered option's (its own, or the
573
+ * default it resolved to), else the select-wide default for a value with no
574
+ * registered option — a virtual row, or one a loader has since dropped.
575
+ */
576
+ private leadingFor;
577
+ /**
578
+ * The leading visual beside the selected value on a single-mode BUTTON trigger
579
+ * — one instance, for the one selected value. A search-shaped trigger draws the
580
+ * same visual through {@link searchLeading}, on a narrower condition.
581
+ */
582
+ protected readonly selectedLeading: _angular_core.Signal<TemplateRef<WrOptionLeadingContext<unknown>> | null>;
583
+ /**
584
+ * The same visual on a SEARCH-shaped trigger (`mode="search"`, a searchable
585
+ * single), drawn before the input — the surface an assignee picker actually
586
+ * uses, and the one that lost its avatar the moment the trigger became a field.
587
+ *
588
+ * The condition is {@link searchShowsValue} rather than `hasSelection()`, and
589
+ * the two part company on every open panel: focusing the field replaces the
590
+ * label with the live query, which starts empty, so what is on screen is the
591
+ * placeholder. An avatar left standing there names a value the field no longer
592
+ * shows, and then sits in front of whatever the user types next — the visual
593
+ * would be answering a different question from the text beside it. It comes
594
+ * back with the label, which is one signal flipping rather than two things
595
+ * kept in step: committing a choice clears the query and closes the panel.
596
+ *
597
+ * Nothing is instantiated for a search select with no selection, panel open or
598
+ * shut — `selectedLabel()` is `null` there, and this returns `null` with it.
599
+ *
600
+ * The COST of that rule, stated because it is a consumer's to weigh: stepping
601
+ * aside is a destroy, so opening the picker and dismissing it unchanged
602
+ * re-creates the visual — a template built on an `<img>` re-mounts once per
603
+ * round trip, and re-requests unless the cache answers. Kept rather than
604
+ * traded for a hidden-but-alive node: the panel's own rows already create and
605
+ * destroy one apiece on the same gesture, so holding this one back buys a
606
+ * fraction of that, and a node retained while it must not be seen is a second
607
+ * piece of state to keep in step with the field. Pinned in `option.spec.ts`
608
+ * ("re-creates it after a round trip"), so a future change here has to say so.
609
+ */
610
+ protected readonly searchLeading: _angular_core.Signal<TemplateRef<WrOptionLeadingContext<unknown>> | null>;
517
611
  /**
518
612
  * Trigger label for single mode. Multi reads `selectedChips` instead.
519
613
  *
@@ -686,6 +780,26 @@ declare class WrSelect implements FormValueControl<unknown>, WrSelectContext {
686
780
  protected onSearchFocus(): void;
687
781
  protected onSearchBlur(): void;
688
782
  protected onSearchKey(event: KeyboardEvent): void;
783
+ /**
784
+ * Whether the search field is showing the SELECTED VALUE rather than a query.
785
+ * `searchDisplay` takes its text off this, and `searchLeading` the visual that
786
+ * goes before it, so the two can never disagree about which of the two things
787
+ * the field is holding.
788
+ *
789
+ * FOCUS as well as open, and that second term is the whole `[minChars]` story.
790
+ * Backspacing under the threshold closes the panel, and falling back to the
791
+ * selected label the moment `open()` goes false rewrote the field out from
792
+ * under someone who was still typing — "sma" -> "sm" showed an empty box.
793
+ *
794
+ * A NON-EMPTY query, though: picking an option clears it and leaves focus in
795
+ * the field, and there the label is what belongs on screen.
796
+ *
797
+ * Only the search-shaped trigger reads it. A searchable MULTI binds its input
798
+ * to `searchQuery()` directly and shows its selection as chips, which carry
799
+ * their own leading visuals whatever the field holds.
800
+ * @internal
801
+ */
802
+ protected readonly searchShowsValue: _angular_core.Signal<boolean>;
689
803
  /**
690
804
  * Display text for the search input. Shows the selected option's
691
805
  * label when collapsed, or the live query while typing / panel open.
@@ -768,19 +882,25 @@ declare class WrSelect implements FormValueControl<unknown>, WrSelectContext {
768
882
  private openOverlay;
769
883
  private closeOverlay;
770
884
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<WrSelect, never>;
771
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrSelect, "wr-select", never, { "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "clearLabel": { "alias": "clearLabel"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "responsive": { "alias": "responsive"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "searchable": { "alias": "searchable"; "required": false; "isSignal": true; }; "searchQuery": { "alias": "searchQuery"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "displayWith": { "alias": "displayWith"; "required": false; "isSignal": true; }; "loader": { "alias": "loader"; "required": false; "isSignal": true; }; "serverSearch": { "alias": "serverSearch"; "required": false; "isSignal": true; }; "debounceMs": { "alias": "debounceMs"; "required": false; "isSignal": true; }; "minChars": { "alias": "minChars"; "required": false; "isSignal": true; }; "freeText": { "alias": "freeText"; "required": false; "isSignal": true; }; "virtualScroll": { "alias": "virtualScroll"; "required": false; "isSignal": true; }; "rowHeight": { "alias": "rowHeight"; "required": false; "isSignal": true; }; "viewportHeight": { "alias": "viewportHeight"; "required": false; "isSignal": true; }; "overscan": { "alias": "overscan"; "required": false; "isSignal": true; }; "noResultsText": { "alias": "noResultsText"; "required": false; "isSignal": true; }; "loadingText": { "alias": "loadingText"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "separators": { "alias": "separators"; "required": false; "isSignal": true; }; "allowDuplicates": { "alias": "allowDuplicates"; "required": false; "isSignal": true; }; "validate": { "alias": "validate"; "required": false; "isSignal": true; }; "clearable": { "alias": "clearable"; "required": false; "isSignal": true; }; "maxItems": { "alias": "maxItems"; "required": false; "isSignal": true; }; "maxTagCount": { "alias": "maxTagCount"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "searchQuery": "searchQueryChange"; "searchChange": "searchChange"; "value": "valueChange"; "touch": "touch"; }, ["projectedOptions"], ["*"], true, never>;
885
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrSelect, "wr-select", never, { "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "clearLabel": { "alias": "clearLabel"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "responsive": { "alias": "responsive"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "searchable": { "alias": "searchable"; "required": false; "isSignal": true; }; "searchQuery": { "alias": "searchQuery"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "displayWith": { "alias": "displayWith"; "required": false; "isSignal": true; }; "loader": { "alias": "loader"; "required": false; "isSignal": true; }; "serverSearch": { "alias": "serverSearch"; "required": false; "isSignal": true; }; "debounceMs": { "alias": "debounceMs"; "required": false; "isSignal": true; }; "minChars": { "alias": "minChars"; "required": false; "isSignal": true; }; "freeText": { "alias": "freeText"; "required": false; "isSignal": true; }; "virtualScroll": { "alias": "virtualScroll"; "required": false; "isSignal": true; }; "rowHeight": { "alias": "rowHeight"; "required": false; "isSignal": true; }; "viewportHeight": { "alias": "viewportHeight"; "required": false; "isSignal": true; }; "overscan": { "alias": "overscan"; "required": false; "isSignal": true; }; "noResultsText": { "alias": "noResultsText"; "required": false; "isSignal": true; }; "loadingText": { "alias": "loadingText"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "separators": { "alias": "separators"; "required": false; "isSignal": true; }; "allowDuplicates": { "alias": "allowDuplicates"; "required": false; "isSignal": true; }; "validate": { "alias": "validate"; "required": false; "isSignal": true; }; "clearable": { "alias": "clearable"; "required": false; "isSignal": true; }; "maxItems": { "alias": "maxItems"; "required": false; "isSignal": true; }; "maxTagCount": { "alias": "maxTagCount"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "searchQuery": "searchQueryChange"; "searchChange": "searchChange"; "value": "valueChange"; "touch": "touch"; }, ["defaultLeading", "projectedOptions"], ["*"], true, never>;
772
886
  }
773
887
 
774
888
  /**
775
889
  * Single option inside a `<wr-select>`.
776
890
  *
777
- * The option's display label is taken from its projected text content;
778
- * the form value is its `value` input.
891
+ * The option's display label is taken from its projected text content — or
892
+ * from `label`, when that is set — and the form value is its `value` input. A
893
+ * leading visual goes in an `<ng-template wrOptionLeading>`, which keeps it out
894
+ * of the label and uninstantiated while the panel is closed.
779
895
  *
780
896
  * @example
781
897
  * ```html
782
898
  * <wr-option value="sm">Small</wr-option>
783
899
  * <wr-option [value]="42">Forty-two</wr-option>
900
+ * <wr-option [value]="p.id" [label]="p.name">
901
+ * <ng-template wrOptionLeading><wr-avatar shape="circle" size="1.5rem">{{ p.initials }}</wr-avatar></ng-template>
902
+ * {{ p.name }}
903
+ * </wr-option>
784
904
  * ```
785
905
  */
786
906
  declare class WrOption {
@@ -788,10 +908,40 @@ declare class WrOption {
788
908
  readonly value: _angular_core.InputSignal<unknown>;
789
909
  /** Disable this option. @default false */
790
910
  readonly disabled: _angular_core.InputSignalWithTransform<boolean, any>;
911
+ /**
912
+ * The text the select reports for this option — the trigger label, the chip,
913
+ * the chip's remove label and the search filter all read it. Unset (or empty),
914
+ * the option's own text is used, as it always was.
915
+ *
916
+ * Set it when the row shows more than the name: a projected email under a
917
+ * person, a count beside a category. It does NOT rename the row itself — the
918
+ * option's accessible name stays the text it draws, since a screen reader user
919
+ * browsing the list should hear what a sighted one reads there.
920
+ *
921
+ * @default null
922
+ */
923
+ readonly label: _angular_core.InputSignal<string | null>;
791
924
  /** Stable id used for `aria-activedescendant`. */
792
925
  readonly id: string;
793
926
  private readonly host;
794
927
  private readonly parent;
928
+ /** This option's own `<ng-template wrOptionLeading>`, if it declares one. */
929
+ private readonly ownLeading;
930
+ /**
931
+ * The leading template that applies to this option: its own, else the
932
+ * select-wide default. Registered with the parent so a chip can draw it.
933
+ */
934
+ private readonly leading;
935
+ /**
936
+ * The leading template to draw in THIS row right now — `null` while the panel
937
+ * is closed. A projected option is created with the page and lives, detached,
938
+ * for as long as the select does, so gating on the template alone would
939
+ * instantiate every row's avatar the moment the page renders. An option with
940
+ * no parent select has no panel to wait for.
941
+ */
942
+ protected readonly rowLeading: Signal<TemplateRef<WrOptionLeadingContext<unknown>> | null>;
943
+ /** The wrapper the leading visual renders into — skipped when reading the text. */
944
+ private readonly leadingSlot;
795
945
  /**
796
946
  * The option's own text, as a signal — the parent's trigger label and the
797
947
  * client-side filter both read it from here.
@@ -815,7 +965,12 @@ declare class WrOption {
815
965
  * @internal
816
966
  */
817
967
  private readonly labelText;
818
- private readonly label;
968
+ /**
969
+ * `label` when set, otherwise the option's own text. What the parent reads.
970
+ * Not `??`: an EMPTY label falls back too, since a blank trigger or a nameless
971
+ * chip is never what a `[label]="person.name"` on a missing name meant.
972
+ */
973
+ private readonly resolvedLabel;
819
974
  /**
820
975
  * @internal — true when this option is currently selected. Works for
821
976
  * both single and multi-select parents via `WrSelectContext.isSelected`.
@@ -825,7 +980,8 @@ declare class WrOption {
825
980
  protected readonly active: Signal<boolean>;
826
981
  /**
827
982
  * @internal — searchable selects only. True when the parent has a query that
828
- * the option's text content does not match (case-insensitive substring).
983
+ * the option's label does not match (case-insensitive substring) — `label`
984
+ * when set, the option's text otherwise, and never its leading visual.
829
985
  * Hidden options stay in the DOM so registration order survives but
830
986
  * collapse via CSS.
831
987
  */
@@ -834,13 +990,20 @@ declare class WrOption {
834
990
  constructor();
835
991
  /**
836
992
  * Read the projected text back off the DOM. Setting the same string again is
837
- * a signal no-op, so the render hook costs a `textContent` read and nothing
838
- * else on the passes where nothing moved.
993
+ * a signal no-op, so the render hook costs a short walk over the host's
994
+ * children and nothing else on the passes where nothing moved.
995
+ *
996
+ * A walk rather than the host's `textContent`, because the leading visual
997
+ * renders INSIDE the host while the panel is open: a `textContent` read turned
998
+ * an avatar's initials into the first word of the label, so the chip and the
999
+ * search filter read "ХР Хегай Роман". Only text and element children count —
1000
+ * a comment's `textContent` is its data, and Angular anchors every
1001
+ * `<ng-template>` with one.
839
1002
  */
840
1003
  private syncLabel;
841
1004
  protected onClick(): void;
842
1005
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<WrOption, never>;
843
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrOption, "wr-option", never, { "value": { "alias": "value"; "required": true; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
1006
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrOption, "wr-option", never, { "value": { "alias": "value"; "required": true; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; }, {}, ["ownLeading"], ["*"], true, never>;
844
1007
  }
845
1008
 
846
1009
  /**
@@ -866,5 +1029,70 @@ declare class WrOptionGroup {
866
1029
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrOptionGroup, "wr-option-group", never, { "label": { "alias": "label"; "required": true; "isSignal": true; }; }, {}, never, ["*"], true, never>;
867
1030
  }
868
1031
 
869
- export { WR_SELECT, WrOption, WrOptionGroup, WrSelect };
870
- export type { WrSelectContext, WrSelectMode, WrSelectSearchLoader, WrSelectSize, WrSelectTagValidator };
1032
+ /**
1033
+ * @license
1034
+ *
1035
+ * Use of this source code is governed by an MIT-style license that can be
1036
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
1037
+ */
1038
+
1039
+ /**
1040
+ * A leading visual — an avatar, an icon, a colour dot — drawn before an
1041
+ * option's label, and kept out of that label.
1042
+ *
1043
+ * It is a TEMPLATE, and that is the point. Projected `<wr-option>` children are
1044
+ * created as soon as the page renders, panel open or not, so an avatar written
1045
+ * straight into each option is 200 components and 200 image requests for an
1046
+ * assignee picker nobody has opened. A template is only instantiated where it is
1047
+ * drawn:
1048
+ *
1049
+ * - in an option row, while the panel is OPEN — destroyed again when it closes,
1050
+ * and under `virtualScroll` only for the rows in the rendered window;
1051
+ * - in a selected chip on a `mode="multi"` trigger, one per visible chip;
1052
+ * - beside the selected value on a single-mode BUTTON trigger — one copy;
1053
+ * - before the input on a SEARCH-shaped trigger (`mode="search"`, a searchable
1054
+ * single), while that input is showing the selected label. It goes beside the
1055
+ * field rather than into it — an `<input>` holds a string — and it steps aside
1056
+ * while a query is on screen, panel open or half-typed, because the value it
1057
+ * names is not what the field is showing then. Picking an option brings it
1058
+ * back with the label. Stepping aside is a DESTROY, like a closing panel's
1059
+ * rows, so an `<img>`-backed visual re-mounts once per open-and-dismiss.
1060
+ *
1061
+ * Every copy sits in an `aria-hidden` wrapper and is skipped when the option's
1062
+ * text is read, so the trigger label, the chip, the search filter and the
1063
+ * option's accessible name all stay the option's words — never "ХР Хегай Роман".
1064
+ * Anything the visual says has to be said by the label as well.
1065
+ *
1066
+ * Two places to declare it. Inside a `<wr-option>` it belongs to that option.
1067
+ * As a DIRECT child of `<wr-select>` it is the default for every row, which is the
1068
+ * only way to reach rows the select draws itself — `[options]`, `[loader]`
1069
+ * results and virtual rows. An option's own template wins over the select's.
1070
+ *
1071
+ * @example
1072
+ * ```html
1073
+ * <wr-select mode="multi" placeholder="Assignees" [(value)]="assignees">
1074
+ * @for (p of people; track p.id) {
1075
+ * <wr-option [value]="p.id">
1076
+ * <ng-template wrOptionLeading let-placement="placement">
1077
+ * <wr-avatar shape="circle" [size]="placement === 'chip' ? 16 : 24" [url]="p.photo">{{ p.initials }}</wr-avatar>
1078
+ * </ng-template>
1079
+ * {{ p.name }}
1080
+ * </wr-option>
1081
+ * }
1082
+ * </wr-select>
1083
+ * ```
1084
+ */
1085
+ declare class WrOptionLeading {
1086
+ readonly template: TemplateRef<WrOptionLeadingContext<unknown>>;
1087
+ /**
1088
+ * Types the template's `let-` variables under `strictTemplates`. Without it
1089
+ * `let-placement` is `any`, and indexing a `Record<WrOptionLeadingPlacement, …>`
1090
+ * with it — the obvious way to size one visual per surface — does not compile.
1091
+ */
1092
+ static ngTemplateContextGuard(_dir: WrOptionLeading, _ctx: unknown): _ctx is WrOptionLeadingContext;
1093
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<WrOptionLeading, never>;
1094
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<WrOptionLeading, "ng-template[wrOptionLeading]", never, {}, {}, never, never, true, never>;
1095
+ }
1096
+
1097
+ export { WR_SELECT, WrOption, WrOptionGroup, WrOptionLeading, WrSelect };
1098
+ export type { WrOptionLeadingContext, WrOptionLeadingPlacement, WrSelectContext, WrSelectMode, WrSelectSearchLoader, WrSelectSize, WrSelectTagValidator };
@@ -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
- declare const NGWR_VERSION = "14.5.1";
19
+ declare const NGWR_VERSION = "14.7.0";
20
20
 
21
21
  /**
22
22
  * @license