ngwr 14.5.0 → 14.5.1

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.
@@ -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.5.1';
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.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;;;;"}
package/llms-full.txt CHANGED
@@ -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'`
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.5.1",
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=' },
@@ -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
@@ -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
 
@@ -389,6 +389,14 @@
389
389
  padding: 0;
390
390
  cursor: pointer;
391
391
  color: inherit;
392
+ // The indicator inside is a bare `.wr-icon__svg`, which is `1em`, and a
393
+ // `<button>` does not inherit its font-size: it takes the browser's control
394
+ // font (about 13.33px) unless the app resets it. So the same glyph measured
395
+ // 13.33px on a page with no reset and 12px under `ngwr/reset`, which sets
396
+ // `font: inherit`. Inheriting the size makes `1em` the header's own text in
397
+ // every app. `--wr-icon-size` cannot do this job — only a `<wr-icon>` host
398
+ // reads it, and the rule here used to set it for nothing.
399
+ font-size: inherit;
392
400
  line-height: 0;
393
401
  }
394
402
 
@@ -741,21 +749,35 @@
741
749
 
742
750
  .wr-table-sort {
743
751
  display: inline-flex;
744
- --wr-icon-size: 0.875rem;
752
+ // Unsorted is the flat line the template draws, at full strength in the muted
753
+ // role — the same ink as the filter funnel beside it. It used to be the sort
754
+ // ARROW at half opacity, which is how an unsorted column came to read as
755
+ // "sorted ascending, dimmed". The glyph carries the state now, so the opacity
756
+ // has no job left, and dropping it is what lifts the indicator over the 3:1 a
757
+ // graphic that identifies a control is held to. Measured on the header's own
758
+ // background in Chromium, after the colour transition settled: from 1.98:1 to
759
+ // 4.78:1 in light (`#5f6c7d` on `#eff2f6`) and from 2.74:1 to 7.04:1 in dark
760
+ // (`#9aa6b8` on `#131a2b`). An SVG stroke is invisible to axe, so re-measure
761
+ // by hand if this ink or the header fill ever moves.
745
762
  color: var(--wr-color-on-surface-muted);
746
- transition:
747
- transform var(--wr-transition-base),
748
- color var(--wr-transition-base);
749
- opacity: 0.5;
750
-
751
- &--asc {
752
- opacity: 1;
763
+ // Only the colour eases back to unsorted. A transition is taken from the
764
+ // state being ENTERED, so leaving `--desc` for the base would otherwise spin
765
+ // the flat line from 180deg to 0 — motion with no direction to report. The
766
+ // turn between the two arrows still animates, since both modifiers carry it.
767
+ transition: color var(--wr-transition-base);
768
+
769
+ // The template swaps the glyph — a flat line while unsorted, an arrow here
770
+ // (table-sort.ts) — so the state reads from the shape, and the primary ink
771
+ // only says which column is the sorted one.
772
+ &--asc,
773
+ &--desc {
753
774
  color: var(--wr-color-primary);
775
+ transition:
776
+ transform var(--wr-transition-base),
777
+ color var(--wr-transition-base);
754
778
  }
755
779
 
756
780
  &--desc {
757
- opacity: 1;
758
- color: var(--wr-color-primary);
759
781
  transform: rotate(180deg);
760
782
  }
761
783
  }
@@ -781,7 +803,13 @@
781
803
  padding: 0;
782
804
  cursor: pointer;
783
805
  color: inherit;
784
- --wr-icon-size: 0.875rem;
806
+ // The funnel is the same bare `1em` svg as the sort indicator beside it, in
807
+ // the same kind of `<button>`, so it inherits the header's font-size for the
808
+ // same reason (see `.wr-table__sort-btn`). The size only, not the `font`
809
+ // shorthand: that would also bring the header's 1.25rem line-height, and the
810
+ // count pill beside the funnel — 10px type with no line-height of its own —
811
+ // grows from 12px to 20px tall under it.
812
+ font-size: inherit;
785
813
  }
786
814
 
787
815
  &__count {
@@ -855,7 +883,6 @@
855
883
  border-radius: var(--wr-border-radius-sm);
856
884
  border: 1px solid var(--wr-color-outline);
857
885
  color: transparent;
858
- --wr-icon-size: 0.75rem;
859
886
  transition: var(--wr-transition-base);
860
887
  }
861
888
 
@@ -1,16 +1,27 @@
1
1
  import { WrI18nCatalog } from 'ngwr/i18n';
2
2
 
3
3
  /**
4
- * Base English catalog for ngwr built-in component strings. Spread this
5
- * into your root catalog or pass it to `provideWrI18nStaticLoader`.
4
+ * Base English catalog for ngwr built-in component strings. Register it with
5
+ * `provideWrI18nBaseCatalogs` and keep only your own keys in the loader.
6
+ *
7
+ * Do not spread it into your own catalog. A spread is shallow, so any namespace
8
+ * you share with ngwr, e.g. `common`, `validation`, `table`, keeps only the keys
9
+ * of the side spread last, and the other side's keys stop resolving with nothing
10
+ * logged: ngwr's components fall back to their English defaults, and a `wrT`
11
+ * read of a lost key renders the raw key under the default missing handler. A
12
+ * base catalog is looked up key by key underneath yours, so both halves of a
13
+ * shared namespace resolve.
6
14
  *
7
15
  * @example
8
16
  * ```ts
17
+ * import { provideWrI18n, provideWrI18nBaseCatalogs, provideWrI18nStaticLoader } from 'ngwr/i18n';
9
18
  * import { wrEn } from 'ngwr/i18n/en';
10
19
  *
11
- * provideWrI18nStaticLoader({
12
- * en: { ...wrEn, app: { title: 'My app' } },
13
- * });
20
+ * providers: [
21
+ * provideWrI18n(),
22
+ * provideWrI18nBaseCatalogs({ en: wrEn }),
23
+ * provideWrI18nStaticLoader({ en: { app: { title: 'My app' } } }),
24
+ * ]
14
25
  * ```
15
26
  */
16
27
  declare const wrEn: WrI18nCatalog;
@@ -403,6 +403,15 @@ declare function provideWrI18n(options?: ProvideWrI18nOptions): EnvironmentProvi
403
403
  /**
404
404
  * Provide a static loader — catalogs already in memory.
405
405
  *
406
+ * A loader serves only the catalog you pass it, and ngwr registers none of its
407
+ * own. Pass only your own keys here and register ngwr's catalogs with
408
+ * {@link provideWrI18nBaseCatalogs} — not `{ ...wrRu, ...yours }`. A spread is
409
+ * shallow, so any namespace you share with ngwr, e.g. `common`, `validation`,
410
+ * `table`, keeps only the keys of the side spread last, and the other side's
411
+ * keys stop resolving with nothing logged: ngwr's components fall back to their
412
+ * English defaults, and a `wrT` read of a lost key renders the raw key under the
413
+ * default missing handler.
414
+ *
406
415
  * @param catalogs Root catalogs keyed by locale.
407
416
  * @param scopes Optional per-scope catalogs served to `registerScope(name)`.
408
417
  */
@@ -428,6 +437,10 @@ declare function provideWrI18nStaticLoader(catalogs: WrI18nStaticCatalogs, scope
428
437
  *
429
438
  * Pass only the locales you ship — the ones you leave out stay out of the
430
439
  * bundle. Your own keys always win; this is a floor, not an override.
440
+ *
441
+ * Use it instead of spreading `wrRu` into your own catalog, too: the lookup
442
+ * walks these key by key after the loader's catalog misses, so a namespace both
443
+ * define keeps both halves, where a shallow spread keeps only one.
431
444
  */
432
445
  declare function provideWrI18nBaseCatalogs(catalogs: WrI18nBaseCatalogs): Provider;
433
446
  /** Provide an HTTP loader — catalogs fetched from URLs. Requires `provideHttpClient`. */
@@ -861,6 +861,14 @@ declare class WrTableGroupHeader {
861
861
  /**
862
862
  * Sort indicator rendered in a sortable column's header.
863
863
  *
864
+ * Draws an ARROW for a sorted column — up for `asc`, the same arrow turned by
865
+ * the stylesheet for `desc` — and a flat LINE for a column that sorts but is
866
+ * not sorted. A different shape rather than a fainter arrow, on purpose: a
867
+ * dimmed up-arrow reads as "sorted ascending", which is the one thing an
868
+ * unsorted column must not say, and a state told apart by opacity and colour
869
+ * alone is lost to anyone who cannot see that difference. A line also has no
870
+ * direction to mirror under RTL.
871
+ *
864
872
  * @internal — used internally by `<wr-table>`. Parent listens for clicks
865
873
  * on the indicator to cycle direction.
866
874
  */
@@ -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.0";
19
+ declare const NGWR_VERSION = "14.5.1";
20
20
 
21
21
  /**
22
22
  * @license