ngwr 14.5.0 → 14.6.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 (48) hide show
  1. package/README.md +9 -3
  2. package/avatar/styles/_index.scss +46 -0
  3. package/fesm2022/ngwr-avatar.mjs +58 -7
  4. package/fesm2022/ngwr-avatar.mjs.map +1 -1
  5. package/fesm2022/ngwr-i18n-en.mjs +16 -5
  6. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  7. package/fesm2022/ngwr-i18n.mjs +13 -0
  8. package/fesm2022/ngwr-i18n.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 +98 -5
  12. package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
  13. package/fesm2022/ngwr-pagination.mjs +1 -1
  14. package/fesm2022/ngwr-pagination.mjs.map +1 -1
  15. package/fesm2022/ngwr-schema-form.mjs +1 -1
  16. package/fesm2022/ngwr-schema-form.mjs.map +1 -1
  17. package/fesm2022/ngwr-select-testing.mjs +21 -2
  18. package/fesm2022/ngwr-select-testing.mjs.map +1 -1
  19. package/fesm2022/ngwr-select.mjs +233 -35
  20. package/fesm2022/ngwr-select.mjs.map +1 -1
  21. package/fesm2022/ngwr-table.mjs +10 -2
  22. package/fesm2022/ngwr-table.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/llms-full.txt +5 -5
  27. package/llms.txt +12 -3
  28. package/mcp/providers.js +8 -2
  29. package/package.json +1 -1
  30. package/schematics/migrations/v14/index.js +90 -34
  31. package/schematics/migrations/v7/index.js +15 -4
  32. package/schematics/migrations/v8/index.js +13 -2
  33. package/schematics/migrations/v9/index.js +19 -10
  34. package/schematics/use/symbol-map.json +1 -0
  35. package/select/styles/_index.scss +34 -0
  36. package/skills/ngwr/SKILL.md +9 -4
  37. package/skills/ngwr/references/catalog.md +1 -1
  38. package/skills/ngwr/references/setup.md +2 -2
  39. package/table/styles/_index.scss +39 -12
  40. package/types/ngwr-avatar.d.ts +36 -3
  41. package/types/ngwr-i18n-en.d.ts +16 -5
  42. package/types/ngwr-i18n.d.ts +13 -0
  43. package/types/ngwr-image-cropper-testing.d.ts +25 -3
  44. package/types/ngwr-image-cropper.d.ts +88 -2
  45. package/types/ngwr-select-testing.d.ts +18 -1
  46. package/types/ngwr-select.d.ts +181 -10
  47. package/types/ngwr-table.d.ts +8 -0
  48. 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.0';
19
+ const NGWR_VERSION = '14.6.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.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;;;;"}
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.6.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;
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'`
@@ -461,7 +461,7 @@
461
461
  - import: `import { WrI18n } from 'ngwr/i18n'`
462
462
  - selector: `[wrT]`
463
463
  - exports: WrI18n, WrTPipe, WrTDirective, WR_I18N_CONFIG, DEFAULT_WR_I18N_CONFIG, WrI18nCatalog, WrI18nConfig, WrI18nConfigResolved, WrI18nParams, WrI18nMissingHandler, WR_I18N_LOADER, WrI18nLoader, WR_I18N_BASE_CATALOGS, WrI18nBaseCatalogs, WrI18nStaticLoader, WrI18nStaticCatalogs, WrI18nStaticScopedCatalogs, WrI18nHttpLoader, WrI18nHttpLoaderConfig, provideWrI18n, provideWrI18nBaseCatalogs, provideWrI18nStaticLoader, provideWrI18nHttpLoader, ProvideWrI18nOptions, useI18nText, readI18nText, useI18nFormatter
464
- - Reactive i18n service. Active locale lives on a signal, catalogs load lazily through a swappable loader (HTTP or static), and double-brace placeholders interpolate at render time. Scopes let feature modules ship and load their own catalogs.
464
+ - Reactive i18n service. Active locale lives on a signal, catalogs load lazily through a swappable loader (HTTP or static), and double-brace placeholders interpolate at render time. Scopes let feature modules ship and load their own catalogs. Keep ngwr's own strings with `provideWrI18nBaseCatalogs` rather than spreading `wrEn` into your catalog: a spread is shallow, so any namespace you share with ngwr, e.g. `common`, `validation`, `table`, keeps only the side spread last, and the other side's keys stop resolving with nothing logged — ngwr's components fall back to English and a `wrT` read of a lost key renders the raw key.
465
465
 
466
466
  ## ngwr/i18n/ar
467
467
  - import: `import { wrAr } from 'ngwr/i18n/ar'`
@@ -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/llms.txt CHANGED
@@ -84,7 +84,12 @@ Add from their entry points as needed:
84
84
  / `select`, and `linkTarget` / `copyable` on `markdown`. There is deliberately no
85
85
  `color` key — a bound `[color]="null"` means "no intent", which a default cannot
86
86
  express without inventing a sentinel.
87
- - `provideWrI18n(...)` + `provideWrI18nStaticLoader(...)` — localization.
87
+ - `provideWrI18n(...)` + `provideWrI18nBaseCatalogs({ en: wrEn })` +
88
+ `provideWrI18nStaticLoader(...)` — localization. ngwr's catalogs go in as a
89
+ BASE, never spread into yours: a spread is shallow, so any namespace you share
90
+ with ngwr, e.g. `common`, `validation`, `table`, keeps only the side spread
91
+ last, and the other side's keys stop resolving with nothing logged — ngwr's
92
+ components fall back to English and a `wrT` read renders the raw key.
88
93
  - `provideWrDateAdapter(...)` — calendar + date-picker (`ngwr/date/adapters/fns`
89
94
  or `ngwr/date/adapters/luxon`).
90
95
 
@@ -106,8 +111,12 @@ one element at a time; a step whose target is missing is skipped, not floated.
106
111
 
107
112
  ## Upgrading to v14
108
113
 
109
- Run `ng update ngwr@14`. It rewrites the renames and REPORTS the rest — read its
110
- output, because the reported half is where the silent failures are.
114
+ Run `ng update ngwr@14`, once, from whatever major the app is on — it runs every
115
+ migration between the installed version and 14, in order. Do not step through the
116
+ majors, and never target a 7.x, 8.x or 9.x release: their schematics die with
117
+ `exports is not defined in ES module scope`. It rewrites the renames and REPORTS
118
+ the rest — read its output, because the reported half is where the silent
119
+ failures are.
111
120
 
112
121
  Rewritten for you (six renames, one word per concept):
113
122
 
package/mcp/providers.js CHANGED
@@ -46,8 +46,14 @@ const REQUIRED_PROVIDERS = [
46
46
  test: /^WrTPipe$|^WrTDirective$|^WrI18n/,
47
47
  // Two entry points, and the comment has to say so: the catalog `wrEn` is
48
48
  // `ngwr/i18n/en`, not `ngwr/i18n`, which exports neither catalog.
49
- provider: "provideWrI18n() + provideWrI18nStaticLoader({ en: wrEn }) // from 'ngwr/i18n' + 'ngwr/i18n/en'",
50
- why: 'the pipe and directive read from a catalog you provide',
49
+ //
50
+ // `wrEn` goes in as a BASE and the loader carries only the app's keys. A
51
+ // loader serves only the catalog it is given, so the other way to keep both
52
+ // is a spread, and a spread is shallow: it silently drops one side of every
53
+ // namespace the two catalogs share. This line is what an agent pastes, so it
54
+ // has to be the form that survives the app adding its first `common` key.
55
+ provider: "provideWrI18n() + provideWrI18nBaseCatalogs({ en: wrEn }) + provideWrI18nStaticLoader({ en: { … } }) // from 'ngwr/i18n' + 'ngwr/i18n/en'",
56
+ why: "the pipe and directive read from a catalog you provide. Register ngwr's with `provideWrI18nBaseCatalogs`, never spread it into yours: a spread is shallow, so any namespace you share with ngwr, e.g. `common`, `validation`, `table`, keeps only the side spread last, and the other side's keys stop resolving with nothing logged — ngwr's components fall back to English and a `wrT` read of a lost key renders the raw key",
51
57
  },
52
58
  ];
53
59
  export { REQUIRED_PROVIDERS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngwr",
3
- "version": "14.5.0",
3
+ "version": "14.6.0",
4
4
  "bin": {
5
5
  "ngwr-mcp": "mcp/server.js"
6
6
  },
@@ -101,28 +101,57 @@ Object.defineProperty(exports, "__esModule", { value: true });
101
101
  * Both are now zero for an app that does not opt in.
102
102
  */
103
103
  const IGNORE_DIRS = new Set(['node_modules', 'dist', '.git', '.cache', '.angular', 'coverage', '.next', '.nuxt']);
104
+ /**
105
+ * What an open tag may hold between its name and the attribute a rule is after:
106
+ * a quoted value, consumed only as a PAIR, or any one character that is neither
107
+ * a quote nor `>`. Every element-scoped rule and attribute detector in this file
108
+ * is built on it, and both halves of "only as a pair" were earned.
109
+ *
110
+ * Stepping OVER a quoted value is what a plain `[^>]*?` got wrong: a `>` inside a
111
+ * binding looks like the end of the tag, so
112
+ * `<wr-alert [type]="n > 0 ? 'a' : 'b'" closeable>` left `closeable` behind while
113
+ * the same attributes in the other order moved. The leftover is SILENT — a static
114
+ * `closeable` / `totalItems="42"` that matches no input is an ordinary DOM
115
+ * attribute and `strictTemplates` says nothing, so the alert quietly loses its
116
+ * dismiss button. (Bracketed forms are loud: NG8002.)
117
+ *
118
+ * Never consuming a LONE quote is what the first fix got wrong. It spelled the
119
+ * fallback `[^>]`, which matches a quote as well, so the engine could backtrack
120
+ * into a single-quoted span that STARTS at the apostrophe inside `title="it's"`
121
+ * and runs past `/>` into the following element: `<wr-alert title="it's" />`
122
+ * then `<app-panel [label]="'x'" closeable />` renamed the PANEL's attribute. The
123
+ * same overlap made a miss exponential — one `<wr-alert>` holding twenty-two
124
+ * quoted attributes and nothing to rename took sixteen seconds in the bare
125
+ * `closeable` rule alone, where this form takes under a millisecond. And a miss
126
+ * did not stay inside its tag: since a pair may contain `>`, a pairing that
127
+ * started at a CLOSING quote jumped over `/>` into later elements, so an
128
+ * ordinary element without the attribute sent the search across the rest of the
129
+ * file. On a 64-line template that never finished, and because `ng update`
130
+ * commits each migration's tree separately, v12 and v13 landed while this one
131
+ * wrote nothing. The spec's timing cases are sized to fail in seconds on that form.
132
+ *
133
+ * What it guarantees: a match cannot leave the open tag it started in, as long as
134
+ * every quote in that tag opens or closes an attribute value. The one markup that
135
+ * breaks the condition is an UNQUOTED value holding a quote (`title=it's`), and a
136
+ * tag like that is skipped rather than guessed at — which costs nothing real,
137
+ * because Angular's own template parser refuses it (`Opening tag "input" not
138
+ * terminated.`).
139
+ */
140
+ const IN_TAG = String.raw `(?:"[^"]*"|'[^']*'|[^>"'])*?`;
104
141
  /**
105
142
  * An open tag, up to and including the whitespace before some attribute.
106
- * `[^>]*?` cannot cross a `>`, so a rename can never escape the element it is
107
- * scoped to, however the tag wraps across lines.
108
143
  *
109
144
  * The anchor is `(?![-\w])` and NOT `\b`: a word boundary still matches at the
110
145
  * front of a longer element name, which is how a v9-shaped rule would have
111
146
  * renamed an attribute on `<wr-table-filter>` while claiming to touch only
112
147
  * `<wr-table>`. Every one of these four has such a neighbour — `wr-table-filter`
113
148
  * and `wr-table-sort`, `wr-window-container` and `wr-window-taskbar` — or would
114
- * acquire one the moment the catalog grows.
115
- *
116
- * Quoted values are stepped over rather than excluded, and that is the half a
117
- * plain `[^>]*?` gets wrong: a `>` inside a binding expression looks like the end
118
- * of the tag, so `<wr-alert [type]="n > 0 ? 'a' : 'b'" closeable>` left
119
- * `closeable` behind while the same attributes in the other order moved. The
120
- * leftover is SILENT — a static `closeable` / `totalItems="42"` that matches no
121
- * input is an ordinary DOM attribute and `strictTemplates` says nothing, so the
122
- * alert quietly loses its dismiss button. (Bracketed forms are loud: NG8002.)
123
- * `migration-v9` carries the same anchoring and the same hole.
149
+ * acquire one the moment the catalog grows. `migration-v9` carries the same
150
+ * anchoring and the same pattern.
124
151
  */
125
- const openTag = (name) => String.raw `<${name}(?![-\w])(?:"[^"]*"|'[^']*'|[^>])*?\s`;
152
+ const openTag = (name) => String.raw `<${name}(?![-\w])${IN_TAG}\s`;
153
+ /** The two elements `[wrInput]` applies to — its selector is `input[wrInput], textarea[wrInput]`. */
154
+ const FIELD = String.raw `<(?:input|textarea)(?![-\w])`;
126
155
  const ALERT = openTag('wr-alert');
127
156
  const TABLE = openTag('wr-table');
128
157
  const PAGER = openTag('wr-pagination');
@@ -143,21 +172,15 @@ const HTML_TRANSFORMS = [
143
172
  // native attribute, so rewriting one that was never ngwr's silently rebinds a
144
173
  // character-width hint. The two orders are both real markup, hence two rules.
145
174
  {
146
- pattern: /(<(?:input|textarea)(?![-\w])(?:"[^"]*"|'[^']*'|[^>])*?\swrInput(?:"[^"]*"|'[^']*'|[^>])*?\s)\[wrSize\]/g,
175
+ pattern: new RegExp(String.raw `(${FIELD}${IN_TAG}\swrInput${IN_TAG}\s)\[wrSize\]`, 'g'),
147
176
  replacement: '$1[size]',
148
177
  },
178
+ { pattern: new RegExp(String.raw `(${FIELD}${IN_TAG}\swrInput${IN_TAG}\s)wrSize=`, 'g'), replacement: '$1size=' },
149
179
  {
150
- pattern: /(<(?:input|textarea)(?![-\w])(?:"[^"]*"|'[^']*'|[^>])*?\swrInput(?:"[^"]*"|'[^']*'|[^>])*?\s)wrSize=/g,
151
- replacement: '$1size=',
152
- },
153
- {
154
- pattern: /(<(?:input|textarea)(?![-\w])(?:"[^"]*"|'[^']*'|[^>])*?\s)\[wrSize\]((?:"[^"]*"|'[^']*'|[^>])*?\swrInput)/g,
180
+ pattern: new RegExp(String.raw `(${FIELD}${IN_TAG}\s)\[wrSize\](${IN_TAG}\swrInput)`, 'g'),
155
181
  replacement: '$1[size]$2',
156
182
  },
157
- {
158
- pattern: /(<(?:input|textarea)(?![-\w])(?:"[^"]*"|'[^']*'|[^>])*?\s)wrSize=((?:"[^"]*"|'[^']*'|[^>])*?\swrInput)/g,
159
- replacement: '$1size=$2',
160
- },
183
+ { pattern: new RegExp(String.raw `(${FIELD}${IN_TAG}\s)wrSize=(${IN_TAG}\swrInput)`, 'g'), replacement: '$1size=$2' },
161
184
  // <wr-table>, from [totalItems] to [total]
162
185
  { pattern: new RegExp(`(${TABLE})\\[totalItems\\]`, 'g'), replacement: '$1[total]' },
163
186
  { pattern: new RegExp(`(${TABLE})totalItems=`, 'g'), replacement: '$1total=' },
@@ -202,8 +225,12 @@ const GLOBAL_TRANSFORMS = [
202
225
  // keyed on it stops matching with no error — CSS does not have one.
203
226
  { pattern: /\bwr-window--chrome-normal\b/g, replacement: 'wr-window--chrome-md' },
204
227
  ];
205
- /** `<wr-tab …>` carrying a `routerLink`, however the tag is wrapped across lines. */
206
- const ROUTER_TAB = /<wr-tab\b[^>]*\brouterLink\b/;
228
+ /**
229
+ * `<wr-tab …>` carrying a `routerLink`, however the tag is wrapped across lines
230
+ * and whatever an earlier binding compares — the plain `[^>]*` this used to be
231
+ * read `[disabled]="n > 0"` as the end of the tag and reported nothing.
232
+ */
233
+ const ROUTER_TAB = new RegExp(String.raw `<wr-tab(?![-\w])${IN_TAG}\brouterLink\b`);
207
234
  const LOADING_BAR = /<wr-loading-bar\b/;
208
235
  /** `provideWrDateAdapter(` with no `locale:` before the call closes. */
209
236
  // `locale` as a key OR as shorthand, and the scan for it runs to the call's own
@@ -217,8 +244,20 @@ const DATE_ADAPTER_NO_LOCALE = /\bprovideWrDateAdapter\(\s*(?:\)|\{(?![\s\S]*?\b
217
244
  /** `provideWrI18n(` with no `defaultLocale:` before the call closes. */
218
245
  const I18N_NO_DEFAULT_LOCALE = /\bprovideWrI18n\(\s*(?:\)|\{(?![^})]*\bdefaultLocale\s*:))/;
219
246
  const DEFAULT_CONFIG = /\bDEFAULT_WR_I18N_CONFIG\b/;
220
- /** `ofLabel` on a pagination tag, bound (`[ofLabel]`) or static. */
221
- const OF_LABEL = /<wr-pagination\b[^>]*\[?ofLabel\]?\s*=/;
247
+ /** `ofLabel` on a pagination tag, bound (`[ofLabel]`) or static, wherever it sits in the tag. */
248
+ const OF_LABEL = new RegExp(String.raw `${PAGER}\[?ofLabel\]?\s*=`);
249
+ /**
250
+ * `wrSize` still on a `[wrInput]` field AFTER the renames ran, in either
251
+ * attribute order and with any spacing around `=`. The renames read `wrSize=` and
252
+ * `[wrSize]` exactly, and whatever they do not move is silent: a static `wrSize`
253
+ * matches no input, lands on the field as a plain DOM attribute, and the control
254
+ * renders at its default size. So this is the one detector that reads the
255
+ * REWRITTEN text — its whole job is what the rules left behind.
256
+ */
257
+ const LEFTOVER_WR_SIZE = [
258
+ new RegExp(String.raw `${FIELD}${IN_TAG}\swrInput${IN_TAG}\s\[?wrSize\]?\s*=`),
259
+ new RegExp(String.raw `${FIELD}${IN_TAG}\s\[?wrSize\]?\s*=${IN_TAG}\swrInput`),
260
+ ];
222
261
  // The half `OF_LABEL` cannot see. Apps that never bound the input still
223
262
  // translated the key, and `pagination.of` is gone: the range is one
224
263
  // `pagination.range` template now, so a catalog that defines the old key loses
@@ -285,6 +324,7 @@ function ngUpdateV14() {
285
324
  const defaultConfigs = [];
286
325
  const ofLabels = [];
287
326
  const staleCatalogs = [];
327
+ const leftoverSizes = [];
288
328
  let rewritten = 0;
289
329
  visit(tree, '/', filePath => {
290
330
  const lower = filePath.toLowerCase();
@@ -295,10 +335,11 @@ function ngUpdateV14() {
295
335
  // reach stylesheets too, because one of them is a BEM class.
296
336
  if (!isTs && !isHtml && !isStyle && !lower.endsWith('.json'))
297
337
  return;
298
- // Read ONCE. Every detector below then answers about the file the user
299
- // wrote, not about the file a transform left behind — none of the five
300
- // renames touches a reported token today, and relying on that is exactly
301
- // the coupling that breaks the next time one is added.
338
+ // Read ONCE. Every detector below but the leftover `wrSize` check then
339
+ // answers about the file the user wrote, not about the file a transform
340
+ // left behind — none of the six renames touches a token those detectors
341
+ // report today, and relying on that is exactly the coupling that breaks
342
+ // the next time one is added.
302
343
  const content = tree.readText(filePath);
303
344
  if (isTs || isHtml || isStyle) {
304
345
  const next = apply(content, [
@@ -310,6 +351,11 @@ function ngUpdateV14() {
310
351
  tree.overwrite(filePath, next);
311
352
  rewritten += 1;
312
353
  }
354
+ // The one detector that reads what the renames LEFT rather than what the
355
+ // user wrote: a `wrSize` surviving them is exactly what it exists to name.
356
+ if ((isTs || isHtml) && LEFTOVER_WR_SIZE.some(pattern => pattern.test(next))) {
357
+ leftoverSizes.push(filePath);
358
+ }
313
359
  }
314
360
  if (ROUTER_TAB.test(content))
315
361
  routerTabs.push(filePath);
@@ -333,10 +379,19 @@ function ngUpdateV14() {
333
379
  context.logger.info(`ngwr v14 migration: rewrote ${rewritten} file(s) for the renames: on <wr-alert> from ` +
334
380
  'closeable to closable, on <wr-table> from totalItems to total, on <wr-pagination> from ' +
335
381
  'currentPage to page (and from currentPageChange to pageChange), from isDisabledWhenLoading ' +
336
- 'to disabledWhenLoading, and the window chromeSize scale from compact/normal to sm/md ' +
337
- '(class .wr-window--chrome-compact to --chrome-sm).');
382
+ 'to disabledWhenLoading, the window chromeSize scale from compact/normal to sm/md ' +
383
+ '(class .wr-window--chrome-compact to --chrome-sm), and on [wrInput] from wrSize to size ' +
384
+ '(and from [wrSize] to [size]).');
338
385
  context.logger.info('Verify the result with `git diff` — a few edge cases may need manual touch-up.');
339
386
  }
387
+ if (leftoverSizes.length > 0) {
388
+ context.logger.warn(`ngwr v14: \`wrSize\` is still on a [wrInput] field in ${leftoverSizes.length} file(s), in a spelling ` +
389
+ "the rename did not match. [wrInput]'s input has been `size` since v14, so a static wrSize is " +
390
+ 'ignored — it lands as a plain DOM attribute and the field renders at its default size, with ' +
391
+ 'nothing said — and a bound [wrSize] fails to compile (NG8002). Rename it to `size` by hand.');
392
+ for (const file of leftoverSizes)
393
+ context.logger.warn(` ${file}`);
394
+ }
340
395
  if (staleCatalogs.length > 0) {
341
396
  context.logger.warn(`ngwr v14: a translated \`pagination.of\` found in ${staleCatalogs.length} file(s). The range is ` +
342
397
  'one template now — replace it with `pagination.range` ("{{from}}–{{to}} of {{total}}") and ' +
@@ -399,7 +454,8 @@ function ngUpdateV14() {
399
454
  i18nProviders.length === 0 &&
400
455
  defaultConfigs.length === 0 &&
401
456
  ofLabels.length === 0 &&
402
- staleCatalogs.length === 0) {
457
+ staleCatalogs.length === 0 &&
458
+ leftoverSizes.length === 0) {
403
459
  context.logger.info('ngwr v14 migration: nothing to do — no affected usage found.');
404
460
  }
405
461
  return tree;
@@ -6,6 +6,17 @@
6
6
  * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ /**
10
+ * What an open tag may hold before the attribute a rule is after. A quoted value
11
+ * is consumed only as a PAIR: the plain `[^>]*?` this replaced read a `>` inside
12
+ * a binding as the end of the tag, so
13
+ * `<wr-select [placeholder]="n > 0 ? 'a' : 'b'" multi>` kept its `multi` while the
14
+ * same attributes in the other order moved — and a lone quote is never consumed,
15
+ * so a rule cannot slide through an apostrophe in one element's value into the
16
+ * next element. `migration-v14`'s `IN_TAG` carries the full account, including
17
+ * the one shape this skips: an UNQUOTED value holding a quote (`title=it's`).
18
+ */
19
+ const IN_TAG = String.raw `(?:"[^"]*"|'[^']*'|[^>"'])*?`;
9
20
  const HTML_TRANSFORMS = [
10
21
  // wr-autocomplete → wr-select mode="search"
11
22
  { pattern: /<wr-autocomplete(\s|>|\/)/g, replacement: '<wr-select mode="search"$1' },
@@ -35,9 +46,9 @@ const HTML_TRANSFORMS = [
35
46
  { pattern: /<wr-count-up-text(\s|>|\/)/g, replacement: '<wr-count-up$1' },
36
47
  { pattern: /<\/wr-count-up-text>/g, replacement: '</wr-count-up>' },
37
48
  // wr-select's deprecated [multi] alias → mode="multi"
38
- { pattern: /(<wr-select\b[^>]*?)\s\[multi\]="true"/g, replacement: '$1 mode="multi"' },
39
- { pattern: /(<wr-select\b[^>]*?)\s\[multi\]="false"/g, replacement: '$1' },
40
- { pattern: /(<wr-select\b[^>]*?)\smulti(?=[\s>/])/g, replacement: '$1 mode="multi"' },
49
+ { pattern: new RegExp(String.raw `(<wr-select\b${IN_TAG})\s\[multi\]="true"`, 'g'), replacement: '$1 mode="multi"' },
50
+ { pattern: new RegExp(String.raw `(<wr-select\b${IN_TAG})\s\[multi\]="false"`, 'g'), replacement: '$1' },
51
+ { pattern: new RegExp(String.raw `(<wr-select\b${IN_TAG})\smulti(?=[\s>/])`, 'g'), replacement: '$1 mode="multi"' },
41
52
  // wr-image → wr-lightbox (rename — same surface)
42
53
  { pattern: /<wr-image(\s|>|\/)/g, replacement: '<wr-lightbox$1' },
43
54
  { pattern: /<\/wr-image>/g, replacement: '</wr-lightbox>' },
@@ -46,7 +57,7 @@ const HTML_TRANSFORMS = [
46
57
  // attribute form (`<h1 wr-animated-text …>`) has no 1:1 target and is
47
58
  // left for manual migration; a bound `[mode]` falls back to typewriter.
48
59
  {
49
- pattern: /<wr-animated-text\b([^>]*?)\s*(\/>|>\s*<\/wr-animated-text>)/g,
60
+ pattern: new RegExp(String.raw `<wr-animated-text\b(${IN_TAG})\s*(\/>|>\s*<\/wr-animated-text>)`, 'g'),
50
61
  replacement: (_match, attrs) => {
51
62
  const mode = /\bmode\s*=\s*"(\w+)"/.exec(attrs)?.[1] ?? 'typewriter';
52
63
  const tag = mode === 'split' ? 'wr-split-text' : mode === 'scramble' ? 'wr-decrypt-text' : 'wr-typewriter';
@@ -12,6 +12,17 @@ const DENSITY_MAP = [
12
12
  ['default', 'md'],
13
13
  ['comfortable', 'lg'],
14
14
  ];
15
+ /**
16
+ * What an open tag may hold before the attribute a rule is after. A quoted value
17
+ * is consumed only as a PAIR: the plain `[^>]*?` this replaced read a `>` inside
18
+ * a binding as the end of the tag, so `<wr-pagination [disabled]="n > 0" size="xs" />`
19
+ * kept its `xs` while the same attributes in the other order moved — and a lone
20
+ * quote is never consumed, so a rule cannot slide through an apostrophe in one
21
+ * element's value into the next element. `migration-v14`'s `IN_TAG` carries the
22
+ * full account, including the one shape this skips: an UNQUOTED value holding a
23
+ * quote (`title=it's`).
24
+ */
25
+ const IN_TAG = String.raw `(?:"[^"]*"|'[^']*'|[^>"'])*?`;
15
26
  const HTML_TRANSFORMS = [
16
27
  // density — directive attribute, string form: wrDensity="compact" → "sm"
17
28
  ...DENSITY_MAP.map(([from, to]) => ({
@@ -29,8 +40,8 @@ const HTML_TRANSFORMS = [
29
40
  replacement: `$1${to}$2`,
30
41
  })),
31
42
  // pagination size — scoped to the element so other `size="xs"` attrs are untouched
32
- { pattern: /(<wr-pagination\b[^>]*?\ssize=")xs(")/g, replacement: '$1sm$2' },
33
- { pattern: /(<wr-pagination\b[^>]*?\ssize=")xl(")/g, replacement: '$1lg$2' },
43
+ { pattern: new RegExp(String.raw `(<wr-pagination\b${IN_TAG}\ssize=")xs(")`, 'g'), replacement: '$1sm$2' },
44
+ { pattern: new RegExp(String.raw `(<wr-pagination\b${IN_TAG}\ssize=")xl(")`, 'g'), replacement: '$1lg$2' },
34
45
  ];
35
46
  const TS_TRANSFORMS = [
36
47
  // density — provideWrDensity({ defaultDensity: 'compact' }) → 'sm'
@@ -14,17 +14,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  * `FormValueControl`) and must be left alone — renaming it would break every
15
15
  * bound group.
16
16
  *
17
- * Quoted values are STEPPED OVER rather than excluded, and that is the half a
18
- * plain `[^>]*?` got wrong for two majors: a `>` inside a binding expression
19
- * looks like the end of the tag, so `<wr-checkbox [disabled]="n > 0" value="a">`
20
- * kept its `value` while the same attributes in the other order moved. The
21
- * leftover is SILENT — a static `value="a"` that matches no input is an ordinary
22
- * DOM attribute, `strictTemplates` says nothing, and every checkbox in the group
23
- * keeps the default identity `null`, so they all toggle together. That is the
24
- * exact defect this migration exists to prevent, reintroduced by the migration
25
- * itself. `migration-v14` fixed the same anchoring; this is that fix, ported.
17
+ * A quoted value is consumed only as a PAIR, and both halves of that matter.
18
+ * Stepping OVER it is what a plain `[^>]*?` got wrong for two majors: a `>`
19
+ * inside a binding expression looks like the end of the tag, so
20
+ * `<wr-checkbox [disabled]="n > 0" value="a">` kept its `value` while the same
21
+ * attributes in the other order moved. The leftover is SILENT — a static
22
+ * `value="a"` that matches no input is an ordinary DOM attribute,
23
+ * `strictTemplates` says nothing, and every checkbox in the group keeps the
24
+ * default identity `null`, so they all toggle together. That is the exact defect
25
+ * this migration exists to prevent, reintroduced by the migration itself.
26
+ *
27
+ * Never consuming a LONE quote is the half the first fix missed. It spelled the
28
+ * fallback `[^>]`, which matches a quote too, so the engine could backtrack into
29
+ * a single-quoted span starting at the apostrophe of `label="it's"` and running
30
+ * past `/>` into the next element: `<wr-checkbox label="it's" />` followed by
31
+ * `<wr-select [x]="'y'" value="z" />` renamed the SELECT's form model. Now a rename
32
+ * stays inside one open tag whenever every quote in it opens or closes a value;
33
+ * an UNQUOTED value holding a quote (`title=it's`) breaks that, and is skipped.
34
+ * `migration-v14`'s `IN_TAG` carries the same pattern and the longer account.
26
35
  */
27
- const CHECKBOX_TAG = String.raw `<wr-checkbox(?![-\w])(?:"[^"]*"|'[^']*'|[^>])*?\s`;
36
+ const CHECKBOX_TAG = String.raw `<wr-checkbox(?![-\w])(?:"[^"]*"|'[^']*'|[^>"'])*?\s`;
28
37
  const HTML_TRANSFORMS = [
29
38
  // <wr-checkbox … value="x"> → checkboxValue="x"
30
39
  { pattern: new RegExp(`(${CHECKBOX_TAG})value=`, 'g'), replacement: '$1checkboxValue=' },
@@ -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,6 +120,14 @@
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
132
  --wr-icon-size: 1rem;
125
133
  flex: 0 0 auto;
@@ -268,6 +276,17 @@
268
276
  user-select: none;
269
277
  }
270
278
 
279
+ // A selected option's leading visual — an avatar, an icon. Pulled toward the
280
+ // chip's start edge so a round avatar reads as the chip's cap rather than as a
281
+ // glyph floating in the padding, and spaced from the label by a little more
282
+ // than the chip's own gap.
283
+ &__chip-leading {
284
+ display: inline-flex;
285
+ flex: none;
286
+ align-items: center;
287
+ margin-inline: -0.25rem 0.125rem;
288
+ }
289
+
271
290
  &__chip-label {
272
291
  overflow: hidden;
273
292
  text-overflow: ellipsis;
@@ -424,6 +443,14 @@
424
443
  }
425
444
  }
426
445
 
446
+ // An option's leading visual — a sibling of the label inside the row's flex,
447
+ // so `--wr-option-inline-gap` spaces it like any projected icon.
448
+ &__leading {
449
+ display: inline-flex;
450
+ flex: none;
451
+ align-items: center;
452
+ }
453
+
427
454
  // Search-mode filter — out-of-match options collapse but stay in the
428
455
  // DOM so registration order survives the panel re-open.
429
456
  &--hidden {
@@ -461,6 +488,13 @@
461
488
  position: relative;
462
489
  }
463
490
 
491
+ // A virtual row is a block, not a flex row (one line, so it can ellipsize),
492
+ // so the leading visual takes its spacing as a margin instead of the gap.
493
+ .wr-option__leading {
494
+ vertical-align: middle;
495
+ margin-inline-end: var(--wr-option-inline-gap, 0.5rem);
496
+ }
497
+
464
498
  // Keyboard cursor highlight — the virtualized list is navigated by
465
499
  // aria-activedescendant, so real focus never lands on the row and the tint
466
500
  // is all there was. `--wr-color-hover` is translucent and lands within about
@@ -44,8 +44,13 @@ guess gets backwards.
44
44
  - **There is no `ControlValueAccessor`.** Value components implement Signal
45
45
  Forms' `FormValueControl` / `FormCheckboxControl`. Bind
46
46
  `[formField]="form.x"`, or use the two-way model standalone —
47
- `[(value)]`, `[(checked)]`. `[(ngModel)]` and reactive forms still work:
48
- Angular 22 synthesises the accessor.
47
+ `[(value)]`, `[(checked)]`. `[(ngModel)]` and reactive forms still bind,
48
+ with no accessor created: Angular's `NgModel` / `FormControlDirective` drive
49
+ the `value` or `checked` model directly. That bridge does NOT apply template
50
+ validator directives — `required`, `minlength`, `maxlength`, `pattern` or
51
+ `email` on an ngwr control is silently ignored and the form submits — so put
52
+ validators on the `FormControl`. It also ignores `updateOn`, and a
53
+ `{ emitEvent: false }` write needs `markForCheck()`.
49
54
  - **`<wr-checkbox>` group identity is `checkboxValue`, not `value`.**
50
55
  `value` is reserved for the form value, so a leftover `value="x"` lands on the
51
56
  host as a plain DOM attribute, every box in the group keeps the default
@@ -95,8 +100,8 @@ cause.
95
100
  - `provideWrLoadingBarRouter() // from 'ngwr/loading-bar/router'`
96
101
  — without it the bar never responds to navigation; it only moves for manual start() / complete()
97
102
  — needed by: `WrLoadingBar`
98
- - `provideWrI18n() + provideWrI18nStaticLoader({ en: wrEn }) // from 'ngwr/i18n' + 'ngwr/i18n/en'`
99
- — the pipe and directive read from a catalog you provide
103
+ - `provideWrI18n() + provideWrI18nBaseCatalogs({ en: wrEn }) + provideWrI18nStaticLoader({ en: { … } }) // from 'ngwr/i18n' + 'ngwr/i18n/en'`
104
+ — the pipe and directive read from a catalog you provide. Register ngwr's with `provideWrI18nBaseCatalogs`, never spread it into yours: a spread is shallow, so any namespace you share with ngwr, e.g. `common`, `validation`, `table`, keeps only the side spread last, and the other side's keys stop resolving with nothing logged — ngwr's components fall back to English and a `wrT` read of a lost key renders the raw key
100
105
  — needed by: `WrTPipe`, `WrTDirective`, `WrI18n`
101
106
 
102
107
  ## Testing
@@ -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'` | — |
@@ -60,9 +60,9 @@ Every date mode goes through an adapter; there is no built-in default. Needed by
60
60
 
61
61
  Without it the bar never responds to navigation; it only moves for manual start() / complete(). Needed by `WrLoadingBar`.
62
62
 
63
- ### `provideWrI18n() + provideWrI18nStaticLoader({ en: wrEn }) // from 'ngwr/i18n' + 'ngwr/i18n/en'`
63
+ ### `provideWrI18n() + provideWrI18nBaseCatalogs({ en: wrEn }) + provideWrI18nStaticLoader({ en: { … } }) // from 'ngwr/i18n' + 'ngwr/i18n/en'`
64
64
 
65
- The pipe and directive read from a catalog you provide. Needed by `WrTPipe`, `WrTDirective`, `WrI18n`.
65
+ The pipe and directive read from a catalog you provide. Register ngwr's with `provideWrI18nBaseCatalogs`, never spread it into yours: a spread is shallow, so any namespace you share with ngwr, e.g. `common`, `validation`, `table`, keeps only the side spread last, and the other side's keys stop resolving with nothing logged — ngwr's components fall back to English and a `wrT` read of a lost key renders the raw key. Needed by `WrTPipe`, `WrTDirective`, `WrI18n`.
66
66
 
67
67
  ## Optional, app-wide
68
68