@reforgium/presentia 1.4.4 → 2.0.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.
@@ -1,14 +1,13 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, computed, inject, DestroyRef, PLATFORM_ID, afterRenderEffect, Injectable, TemplateRef, ViewContainerRef, effect, Input, Directive, input, ElementRef, Renderer2, Injector, afterNextRender, runInInjectionContext, Pipe, makeEnvironmentProviders, LOCALE_ID } from '@angular/core';
3
- import { getChainedValue, TRANSLATION, SELECTED_LANG, CHANGE_LANG, SELECTED_THEME, CHANGE_THEME, CURRENT_DEVICE, deepEqual } from '@reforgium/internal';
2
+ import { InjectionToken, signal, computed, inject, DestroyRef, PLATFORM_ID, afterRenderEffect, Injectable, TemplateRef, ViewContainerRef, effect, Input, Directive, input, ElementRef, Renderer2, Injector, afterNextRender, runInInjectionContext, Pipe, makeEnvironmentProviders, provideAppInitializer, LOCALE_ID } from '@angular/core';
3
+ import { getChainedValue, deepEqual, compareRoutes, TRANSLATION, SELECTED_LANG, CHANGE_LANG, SELECTED_THEME, CHANGE_THEME, CURRENT_DEVICE } from '@reforgium/internal';
4
4
  import { BreakpointObserver } from '@angular/cdk/layout';
5
5
  import { isPlatformBrowser, DOCUMENT } from '@angular/common';
6
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
- import { fromEvent, debounceTime, firstValueFrom, tap, filter as filter$1 } from 'rxjs';
8
7
  import { HttpClient } from '@angular/common/http';
8
+ import { firstValueFrom, tap } from 'rxjs';
9
+ import { Router, NavigationEnd } from '@angular/router';
9
10
  import { Title, Meta } from '@angular/platform-browser';
10
- import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
11
- import { filter } from 'rxjs/operators';
12
11
 
13
12
  /**
14
13
  * Default breakpoints for device type detection.
@@ -74,10 +73,10 @@ const DEVICE_BREAKPOINTS = new InjectionToken('RE_DEVICE_BREAKPOINTS', {
74
73
  */
75
74
  class AdaptiveService {
76
75
  /** @internal Signal of the current device type. */
77
- #device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : []));
76
+ #device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : /* istanbul ignore next */ []));
78
77
  /** @internal Signals of the current window width and height. */
79
- #width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] : []));
80
- #height = signal(0, ...(ngDevMode ? [{ debugName: "#height" }] : []));
78
+ #width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] : /* istanbul ignore next */ []));
79
+ #height = signal(0, ...(ngDevMode ? [{ debugName: "#height" }] : /* istanbul ignore next */ []));
81
80
  /**
82
81
  * Current device type (reactive signal).
83
82
  * Possible values: `'desktop' | 'tablet' | 'mobile'`.
@@ -100,17 +99,21 @@ class AdaptiveService {
100
99
  * Computed signal indicating whether the current device is a desktop.
101
100
  * Used for conditional rendering or layout configuration.
102
101
  */
103
- isDesktop = computed(() => this.#device() === 'desktop', ...(ngDevMode ? [{ debugName: "isDesktop" }] : []));
102
+ isDesktop = computed(() => this.#device() === 'desktop', ...(ngDevMode ? [{ debugName: "isDesktop" }] : /* istanbul ignore next */ []));
103
+ isMobile = computed(() => this.#device() === 'mobile', ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
104
+ isTablet = computed(() => this.#device() === 'tablet', ...(ngDevMode ? [{ debugName: "isTablet" }] : /* istanbul ignore next */ []));
105
+ isDesktopSmall = computed(() => this.#device() === 'desktop-s', ...(ngDevMode ? [{ debugName: "isDesktopSmall" }] : /* istanbul ignore next */ []));
104
106
  /**
105
107
  * Computed signal determining whether the screen is in portrait orientation.
106
108
  * Returns `true` if window height is greater than width.
107
109
  */
108
- isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : []));
110
+ isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : /* istanbul ignore next */ []));
109
111
  deviceBreakpoints = inject(DEVICE_BREAKPOINTS);
110
112
  devicePriority = Object.keys(this.deviceBreakpoints);
111
113
  destroyRef = inject(DestroyRef);
112
114
  breakpointObserver = inject(BreakpointObserver);
113
115
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
116
+ resizeDebounceId = null;
114
117
  constructor() {
115
118
  if (!this.isBrowser) {
116
119
  this.#device.set('desktop');
@@ -129,20 +132,47 @@ class AdaptiveService {
129
132
  this.#device.set(device);
130
133
  }
131
134
  });
132
- fromEvent(window, 'resize')
133
- .pipe(debounceTime(100))
134
- .pipe(takeUntilDestroyed(this.destroyRef))
135
- .subscribe(() => {
136
- const width = window.innerWidth;
137
- const height = window.innerHeight;
138
- this.#width() !== width && this.#width.set(width);
139
- this.#height() !== height && this.#height.set(height);
135
+ const onResize = () => {
136
+ this.resizeDebounceId !== null && clearTimeout(this.resizeDebounceId);
137
+ this.resizeDebounceId = setTimeout(() => {
138
+ this.resizeDebounceId = null;
139
+ const width = window.innerWidth;
140
+ const height = window.innerHeight;
141
+ this.#width() !== width && this.#width.set(width);
142
+ this.#height() !== height && this.#height.set(height);
143
+ }, 100);
144
+ };
145
+ window.addEventListener('resize', onResize, { passive: true });
146
+ this.destroyRef.onDestroy(() => {
147
+ window.removeEventListener('resize', onResize);
148
+ if (this.resizeDebounceId !== null) {
149
+ clearTimeout(this.resizeDebounceId);
150
+ this.resizeDebounceId = null;
151
+ }
140
152
  });
141
153
  }
142
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: AdaptiveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
143
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: AdaptiveService, providedIn: 'root' });
154
+ breakpoint() {
155
+ return this.#device();
156
+ }
157
+ is(device) {
158
+ const allowed = Array.isArray(device) ? device : [device];
159
+ return allowed.includes(this.#device());
160
+ }
161
+ isAtLeast(device) {
162
+ return this.deviceRank(this.#device()) >= this.deviceRank(device);
163
+ }
164
+ isBetween(min, max) {
165
+ const current = this.deviceRank(this.#device());
166
+ return current >= this.deviceRank(min) && current <= this.deviceRank(max);
167
+ }
168
+ deviceRank(device) {
169
+ const index = this.devicePriority.indexOf(device);
170
+ return index >= 0 ? index : Number.MAX_SAFE_INTEGER;
171
+ }
172
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AdaptiveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
173
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AdaptiveService, providedIn: 'root' });
144
174
  }
145
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: AdaptiveService, decorators: [{
175
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AdaptiveService, decorators: [{
146
176
  type: Injectable,
147
177
  args: [{
148
178
  providedIn: 'root',
@@ -170,8 +200,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
170
200
  * the template is automatically added or removed from the DOM.
171
201
  */
172
202
  class IfDeviceDirective {
173
- deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] : []));
174
- inverseInput = signal(false, ...(ngDevMode ? [{ debugName: "inverseInput" }] : []));
203
+ deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] : /* istanbul ignore next */ []));
204
+ atLeastInput = signal(undefined, ...(ngDevMode ? [{ debugName: "atLeastInput" }] : /* istanbul ignore next */ []));
205
+ betweenInput = signal(undefined, ...(ngDevMode ? [{ debugName: "betweenInput" }] : /* istanbul ignore next */ []));
206
+ inverseInput = signal(false, ...(ngDevMode ? [{ debugName: "inverseInput" }] : /* istanbul ignore next */ []));
175
207
  tpl = inject(TemplateRef);
176
208
  vcr = inject(ViewContainerRef);
177
209
  adaptive = inject(AdaptiveService);
@@ -181,20 +213,37 @@ class IfDeviceDirective {
181
213
  constructor() {
182
214
  effect(() => {
183
215
  const device = this.deviceInput();
216
+ const atLeast = this.atLeastInput();
217
+ const between = this.betweenInput();
184
218
  if (device) {
185
219
  this.allowedDevices = Array.isArray(device) ? device : [device];
186
220
  }
221
+ else if (!atLeast && !between) {
222
+ this.allowedDevices = [];
223
+ }
187
224
  this.updateView();
188
225
  });
189
226
  }
190
227
  set reIfDevice(value) {
191
228
  this.deviceInput.set(value);
192
229
  }
230
+ set reIfDeviceAtLeast(value) {
231
+ this.atLeastInput.set(value);
232
+ }
233
+ set reIfDeviceBetween(value) {
234
+ this.betweenInput.set(value);
235
+ }
193
236
  set inverse(value) {
194
237
  this.inverseInput.set(!!value);
195
238
  }
196
239
  updateView() {
197
- const isAllowed = this.allowedDevices.includes(this.currentDevice());
240
+ const between = this.betweenInput();
241
+ const atLeast = this.atLeastInput();
242
+ const isAllowed = between
243
+ ? this.adaptive.isBetween(between[0], between[1])
244
+ : atLeast
245
+ ? this.adaptive.isAtLeast(atLeast)
246
+ : this.allowedDevices.includes(this.currentDevice());
198
247
  const shouldShow = this.inverseInput() ? !isAllowed : isAllowed;
199
248
  if (shouldShow && !this.hasView) {
200
249
  this.vcr.createEmbeddedView(this.tpl);
@@ -205,18 +254,24 @@ class IfDeviceDirective {
205
254
  this.hasView = false;
206
255
  }
207
256
  }
208
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: IfDeviceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
209
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.1", type: IfDeviceDirective, isStandalone: true, selector: "[reIfDevice]", inputs: { reIfDevice: "reIfDevice", inverse: "inverse" }, ngImport: i0 });
257
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IfDeviceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
258
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.4", type: IfDeviceDirective, isStandalone: true, selector: "[reIfDevice],[reIfDeviceAtLeast],[reIfDeviceBetween]", inputs: { reIfDevice: "reIfDevice", reIfDeviceAtLeast: "reIfDeviceAtLeast", reIfDeviceBetween: "reIfDeviceBetween", inverse: "inverse" }, ngImport: i0 });
210
259
  }
211
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: IfDeviceDirective, decorators: [{
260
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IfDeviceDirective, decorators: [{
212
261
  type: Directive,
213
262
  args: [{
214
- selector: '[reIfDevice]',
263
+ selector: '[reIfDevice],[reIfDeviceAtLeast],[reIfDeviceBetween]',
215
264
  standalone: true,
216
265
  }]
217
266
  }], ctorParameters: () => [], propDecorators: { reIfDevice: [{
218
267
  type: Input,
219
268
  args: ['reIfDevice']
269
+ }], reIfDeviceAtLeast: [{
270
+ type: Input,
271
+ args: ['reIfDeviceAtLeast']
272
+ }], reIfDeviceBetween: [{
273
+ type: Input,
274
+ args: ['reIfDeviceBetween']
220
275
  }], inverse: [{
221
276
  type: Input
222
277
  }] } });
@@ -225,6 +280,19 @@ const innerLangVal = Symbol('reInnerLangVal');
225
280
 
226
281
  const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER');
227
282
 
283
+ /**
284
+ * Optional DI token for a custom persistence adapter used by `LangService`
285
+ * to store and retrieve the selected language (default key: `'lang'`).
286
+ *
287
+ * When not provided the service falls back to `localStorage` directly.
288
+ *
289
+ * Example:
290
+ * ```ts
291
+ * { provide: LANG_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
292
+ * ```
293
+ */
294
+ const LANG_PERSISTENCE_ADAPTER = new InjectionToken('RE_LANG_PERSISTENCE_ADAPTER');
295
+
228
296
  /**
229
297
  * Injection token for providing locale configuration to the language module.
230
298
  *
@@ -245,6 +313,67 @@ const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER
245
313
  */
246
314
  const LANG_CONFIG = new InjectionToken('RE_LANG_CONFIG');
247
315
 
316
+ /**
317
+ * @deprecated Diagnostics are usually enabled through route preload config, not by consuming this service directly.
318
+ */
319
+ class RouteNamespaceDiagnosticsService {
320
+ router = inject(Router, { optional: true });
321
+ enabled = !!inject(LANG_CONFIG).routeNamespacePreload?.diagnostics;
322
+ warned = new Set();
323
+ currentUrl = '';
324
+ currentNamespaces = new Set();
325
+ navigationSettled = false;
326
+ constructor() {
327
+ if (!this.enabled || !this.router) {
328
+ return;
329
+ }
330
+ this.router.events.subscribe((event) => {
331
+ if (event instanceof NavigationEnd) {
332
+ const url = normalizeUrlPath$1(event.urlAfterRedirects || event.url);
333
+ if (url === this.currentUrl) {
334
+ this.navigationSettled = true;
335
+ }
336
+ }
337
+ });
338
+ }
339
+ registerRouteNamespaces(url, namespaces) {
340
+ if (!this.enabled) {
341
+ return;
342
+ }
343
+ this.currentUrl = normalizeUrlPath$1(url);
344
+ this.currentNamespaces = new Set(namespaces.map((ns) => ns.trim()).filter(Boolean));
345
+ this.navigationSettled = false;
346
+ }
347
+ warnLateNamespaceLoad(namespace) {
348
+ if (!this.enabled || !this.navigationSettled || typeof ngDevMode === 'undefined') {
349
+ return;
350
+ }
351
+ const normalizedNamespace = namespace.trim();
352
+ if (!normalizedNamespace || this.currentNamespaces.has(normalizedNamespace)) {
353
+ return;
354
+ }
355
+ const key = `${this.currentUrl}|${normalizedNamespace}`;
356
+ if (this.warned.has(key)) {
357
+ return;
358
+ }
359
+ this.warned.add(key);
360
+ // eslint-disable-next-line no-console
361
+ console.warn(`[presentia] Namespace "${normalizedNamespace}" was loaded after route activation for "${this.currentUrl}". ` +
362
+ 'Add it to routeNamespacePreload manifest or route data to avoid raw i18n keys on first paint.');
363
+ }
364
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
365
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, providedIn: 'root' });
366
+ }
367
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, decorators: [{
368
+ type: Injectable,
369
+ args: [{ providedIn: 'root' }]
370
+ }], ctorParameters: () => [] });
371
+ function normalizeUrlPath$1(url) {
372
+ const [path] = url.split(/[?#]/, 1);
373
+ const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
374
+ return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
375
+ }
376
+
248
377
  /**
249
378
  * LangService provides functionality for managing and tracking language settings
250
379
  * and translations in the application. It is designed to handle localization needs,
@@ -258,12 +387,13 @@ class LangService {
258
387
  http = inject(HttpClient);
259
388
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
260
389
  missingKeyHandler = inject(LANG_MISSING_KEY_HANDLER, { optional: true });
390
+ routeNamespaceDiagnostics = inject(RouteNamespaceDiagnosticsService, { optional: true });
261
391
  supportedLangSet = new Set([
262
392
  ...LangService.BUILTIN_LANGS,
263
393
  ...this.normalizeSupportedLangs(this.config.supportedLangs ?? []),
264
394
  ]);
265
- #lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : []));
266
- #cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : []));
395
+ #lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : /* istanbul ignore next */ []));
396
+ #cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : /* istanbul ignore next */ []));
267
397
  #loadedNamespaces = new Set();
268
398
  #pendingLoads = new Map();
269
399
  #pendingBatchLoads = new Map();
@@ -280,7 +410,7 @@ class LangService {
280
410
  currentLang = computed(() => {
281
411
  const lang = this.#lang();
282
412
  return lang === 'kg' ? (this.config?.kgValue ?? 'kg') : lang;
283
- }, ...(ngDevMode ? [{ debugName: "currentLang" }] : []));
413
+ }, ...(ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []));
284
414
  /**
285
415
  * Extracts readonly value from private property `#lang` and assigns it to `innerLangVal`.
286
416
  * Expected that property `#lang` has `asReadonly` method that returns immutable representation.
@@ -325,6 +455,9 @@ class LangService {
325
455
  }
326
456
  return baseValue;
327
457
  }
458
+ has(query) {
459
+ return this.getChainedValue(query) !== undefined;
460
+ }
328
461
  observe(query, params) {
329
462
  const [ns] = query.split('.');
330
463
  if (!this.isNamespaceLoaded(ns)) {
@@ -345,6 +478,7 @@ class LangService {
345
478
  if (this.isNamespaceValid(key)) {
346
479
  return;
347
480
  }
481
+ this.routeNamespaceDiagnostics?.warnLateNamespaceLoad(ns);
348
482
  this.tryExpireNamespace(key);
349
483
  if (this.#pendingLoads.has(key)) {
350
484
  return this.#pendingLoads.get(key);
@@ -399,14 +533,27 @@ class LangService {
399
533
  await Promise.all(toLoad.map((ns) => this.loadNamespace(ns)));
400
534
  return;
401
535
  }
402
- const batchKey = this.makeBatchKey(requestedLang, toLoad);
536
+ const maxBatchSize = this.normalizeMaxBatchSize(this.config.maxBatchSize);
537
+ const chunks = maxBatchSize && toLoad.length > maxBatchSize ? this.chunkNamespaces(toLoad, maxBatchSize) : [toLoad];
538
+ await Promise.all(chunks.map((chunk) => {
539
+ if (chunk.length < 2) {
540
+ return this.loadNamespace(chunk[0]);
541
+ }
542
+ return this.loadNamespaceBatch(chunk, requestedLang);
543
+ }));
544
+ }
545
+ async loadNamespaceBatch(namespaces, requestedLang) {
546
+ for (const ns of namespaces) {
547
+ this.routeNamespaceDiagnostics?.warnLateNamespaceLoad(ns);
548
+ }
549
+ const batchKey = this.makeBatchKey(requestedLang, namespaces);
403
550
  if (this.#pendingBatchLoads.has(batchKey)) {
404
551
  return this.#pendingBatchLoads.get(batchKey);
405
552
  }
406
553
  const promise = (async () => {
407
554
  try {
408
555
  const context = {
409
- namespaces: toLoad,
556
+ namespaces,
410
557
  lang: requestedLang,
411
558
  isFromAssets: this.config.isFromAssets,
412
559
  baseUrl: this.config.url,
@@ -418,7 +565,7 @@ class LangService {
418
565
  }
419
566
  const payloads = this.config.batchResponseAdapter(response, this.toBatchResponseContext(context));
420
567
  const merged = {};
421
- for (const ns of toLoad) {
568
+ for (const ns of namespaces) {
422
569
  const nsPayload = payloads[ns];
423
570
  if (!nsPayload) {
424
571
  continue;
@@ -438,6 +585,19 @@ class LangService {
438
585
  this.#pendingBatchLoads.set(batchKey, promise);
439
586
  return promise;
440
587
  }
588
+ normalizeMaxBatchSize(value) {
589
+ if (!value || value < 1) {
590
+ return null;
591
+ }
592
+ return Math.floor(value);
593
+ }
594
+ chunkNamespaces(namespaces, size) {
595
+ const chunks = [];
596
+ for (let i = 0; i < namespaces.length; i += size) {
597
+ chunks.push(namespaces.slice(i, i + size));
598
+ }
599
+ return chunks;
600
+ }
441
601
  evictNamespace(ns) {
442
602
  const key = this.makeNamespaceKey(ns);
443
603
  if (!this.#loadedNamespaces.has(key)) {
@@ -652,10 +812,10 @@ class LangService {
652
812
  .map((lang) => lang.trim().toLowerCase())
653
813
  .filter((lang) => !!lang && LangService.LANG_CODE_RE.test(lang));
654
814
  }
655
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
656
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangService, providedIn: 'root' });
815
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
816
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, providedIn: 'root' });
657
817
  }
658
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangService, decorators: [{
818
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, decorators: [{
659
819
  type: Injectable,
660
820
  args: [{ providedIn: 'root' }]
661
821
  }], ctorParameters: () => [] });
@@ -693,19 +853,19 @@ class LangDirective {
693
853
  * Localization mode: defines which parts of the element will be translated.
694
854
  * @default 'all'
695
855
  */
696
- lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : {}), alias: 'reLang' });
856
+ lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : /* istanbul ignore next */ {}), alias: 'reLang' });
697
857
  /**
698
858
  * Explicit key for text content translation.
699
859
  */
700
- reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : []));
860
+ reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : /* istanbul ignore next */ []));
701
861
  /**
702
862
  * Explicit attribute-to-key map for translation.
703
863
  */
704
- reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : []));
864
+ reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : /* istanbul ignore next */ []));
705
865
  /**
706
866
  * Name of an additional attribute to localize (besides standard `title`, `label`, `placeholder`).
707
867
  */
708
- langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : []));
868
+ langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : /* istanbul ignore next */ []));
709
869
  el = inject(ElementRef);
710
870
  renderer = inject(Renderer2);
711
871
  service = inject(LangService);
@@ -901,10 +1061,10 @@ class LangDirective {
901
1061
  }
902
1062
  return this.service.get(key);
903
1063
  }
904
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
905
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.1", type: LangDirective, isStandalone: true, selector: "[reLang]", inputs: { lang: { classPropertyName: "lang", publicName: "reLang", isSignal: true, isRequired: false, transformFunction: null }, langForAttr: { classPropertyName: "langForAttr", publicName: "langForAttr", isSignal: true, isRequired: false, transformFunction: null }, reLangKey: { classPropertyName: "reLangKey", publicName: "reLangKey", isSignal: false, isRequired: false, transformFunction: null }, reLangAttrs: { classPropertyName: "reLangAttrs", publicName: "reLangAttrs", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0 });
1064
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1065
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.4", type: LangDirective, isStandalone: true, selector: "[reLang]", inputs: { lang: { classPropertyName: "lang", publicName: "reLang", isSignal: true, isRequired: false, transformFunction: null }, langForAttr: { classPropertyName: "langForAttr", publicName: "langForAttr", isSignal: true, isRequired: false, transformFunction: null }, reLangKey: { classPropertyName: "reLangKey", publicName: "reLangKey", isSignal: false, isRequired: false, transformFunction: null }, reLangAttrs: { classPropertyName: "reLangAttrs", publicName: "reLangAttrs", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0 });
906
1066
  }
907
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangDirective, decorators: [{
1067
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, decorators: [{
908
1068
  type: Directive,
909
1069
  args: [{ selector: '[reLang]', standalone: true }]
910
1070
  }], ctorParameters: () => [], propDecorators: { lang: [{ type: i0.Input, args: [{ isSignal: true, alias: "reLang", required: false }] }], langForAttr: [{ type: i0.Input, args: [{ isSignal: true, alias: "langForAttr", required: false }] }], reLangKey: [{
@@ -931,6 +1091,7 @@ const LANG_PIPE_CONFIG = new InjectionToken('RE_LANG_PIPE_CONFIG');
931
1091
  */
932
1092
  class LangPipe {
933
1093
  cache = new Map();
1094
+ warnedUnresolved = new Set();
934
1095
  lang = inject(LangService);
935
1096
  injector = inject(Injector);
936
1097
  config;
@@ -977,6 +1138,9 @@ class LangPipe {
977
1138
  return placeholder;
978
1139
  }
979
1140
  }
1141
+ if (ns && this.lang.isNamespaceLoaded(ns) && !this.lang.has(query)) {
1142
+ this.warnUnresolvedKey(query);
1143
+ }
980
1144
  return value;
981
1145
  }
982
1146
  makeKey(query, params) {
@@ -997,14 +1161,299 @@ class LangPipe {
997
1161
  this.cache.delete(firstKey);
998
1162
  }
999
1163
  }
1000
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1001
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.1", ngImport: i0, type: LangPipe, isStandalone: true, name: "lang", pure: false });
1164
+ warnUnresolvedKey(query) {
1165
+ if (typeof ngDevMode === 'undefined') {
1166
+ return;
1167
+ }
1168
+ if (this.warnedUnresolved.has(query)) {
1169
+ return;
1170
+ }
1171
+ this.warnedUnresolved.add(query);
1172
+ // eslint-disable-next-line no-console
1173
+ console.warn(`LangPipe: namespace loaded but key "${query}" is unresolved`);
1174
+ }
1175
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1176
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, isStandalone: true, name: "lang", pure: false });
1002
1177
  }
1003
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangPipe, decorators: [{
1178
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, decorators: [{
1004
1179
  type: Pipe,
1005
1180
  args: [{ name: 'lang', standalone: true, pure: false }]
1006
1181
  }], ctorParameters: () => [] });
1007
1182
 
1183
+ function deepestActivatedRoute(route) {
1184
+ let current = route;
1185
+ while (current.firstChild) {
1186
+ current = current.firstChild;
1187
+ }
1188
+ return current;
1189
+ }
1190
+ function deepestRouteSnapshot(snapshot) {
1191
+ let current = snapshot;
1192
+ while (current.firstChild) {
1193
+ current = current.firstChild;
1194
+ }
1195
+ return current;
1196
+ }
1197
+ function joinUrl(segments) {
1198
+ return segments.length ? segments.map((segment) => segment.path).join('/') : '';
1199
+ }
1200
+ function snapshotFullPath(snapshot) {
1201
+ return snapshot.pathFromRoot
1202
+ .map((route) => joinUrl(route.url))
1203
+ .filter(Boolean)
1204
+ .join('/');
1205
+ }
1206
+ function snapshotRoutePattern(snapshot) {
1207
+ return snapshot.pathFromRoot
1208
+ .map((route) => route.routeConfig?.path ?? '')
1209
+ .filter(Boolean)
1210
+ .join('/');
1211
+ }
1212
+ function snapshotMergedParams(snapshot) {
1213
+ return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.params }), {});
1214
+ }
1215
+ function snapshotDeepestParams(snapshot) {
1216
+ const mergedParams = snapshotMergedParams(snapshot);
1217
+ const keys = extractRouteParamKeys(snapshot.routeConfig?.path ?? '');
1218
+ if (!keys.length) {
1219
+ return {};
1220
+ }
1221
+ return keys.reduce((acc, key) => {
1222
+ const value = mergedParams[key];
1223
+ if (value !== undefined) {
1224
+ acc[key] = value;
1225
+ }
1226
+ return acc;
1227
+ }, {});
1228
+ }
1229
+ function snapshotMergedData(snapshot) {
1230
+ return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.data }), {});
1231
+ }
1232
+ function extractRouteParamKeys(path) {
1233
+ return path
1234
+ .split('/')
1235
+ .filter((segment) => segment.startsWith(':'))
1236
+ .map((segment) => segment.slice(1))
1237
+ .filter(Boolean);
1238
+ }
1239
+
1240
+ /**
1241
+ * Reactive snapshot of the current route (the deepest active route).
1242
+ * Updates on every `NavigationEnd` event.
1243
+ */
1244
+ class RouteWatcher {
1245
+ router = inject(Router);
1246
+ destroyRef = inject(DestroyRef);
1247
+ #params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : /* istanbul ignore next */ []));
1248
+ #deepestParams = signal({}, ...(ngDevMode ? [{ debugName: "#deepestParams" }] : /* istanbul ignore next */ []));
1249
+ #query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
1250
+ #data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : /* istanbul ignore next */ []));
1251
+ #mergedData = signal({}, ...(ngDevMode ? [{ debugName: "#mergedData" }] : /* istanbul ignore next */ []));
1252
+ #url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : /* istanbul ignore next */ []));
1253
+ #routePattern = signal('', ...(ngDevMode ? [{ debugName: "#routePattern" }] : /* istanbul ignore next */ []));
1254
+ #fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : /* istanbul ignore next */ []));
1255
+ /** Params merged from root to deepest route. */
1256
+ params = this.#params.asReadonly();
1257
+ /** Params declared on the deepest route only. */
1258
+ deepestParams = this.#deepestParams.asReadonly();
1259
+ /** Query params from the current navigation. */
1260
+ query = this.#query.asReadonly();
1261
+ /** Deepest route data only. */
1262
+ data = this.#data.asReadonly();
1263
+ /** Route data merged from root to deepest route. */
1264
+ mergedData = this.#mergedData.asReadonly();
1265
+ /** Full current url path assembled from root to deepest route. */
1266
+ url = this.#url.asReadonly();
1267
+ /** Current route config pattern, e.g. `orgs/:orgId/users/:id`. */
1268
+ routePattern = this.#routePattern.asReadonly();
1269
+ /** Current url fragment without `#`. */
1270
+ fragment = this.#fragment.asReadonly();
1271
+ /** Combined computed snapshot. */
1272
+ state = computed(() => ({
1273
+ params: this.#params(),
1274
+ deepestParams: this.#deepestParams(),
1275
+ query: this.#query(),
1276
+ data: this.#data(),
1277
+ mergedData: this.#mergedData(),
1278
+ url: this.#url(),
1279
+ routePattern: this.#routePattern(),
1280
+ fragment: this.#fragment(),
1281
+ }), ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
1282
+ constructor() {
1283
+ const read = () => {
1284
+ const snapshot = this.deepestSnapshot();
1285
+ const nextUrl = snapshotFullPath(snapshot);
1286
+ const nextRoutePattern = snapshotRoutePattern(snapshot);
1287
+ const nextParams = snapshotMergedParams(snapshot);
1288
+ const nextDeepestParams = snapshotDeepestParams(snapshot);
1289
+ const nextMergedData = snapshotMergedData(snapshot);
1290
+ const nextQuery = snapshot.queryParams;
1291
+ const nextData = snapshot.data;
1292
+ const nextFragment = snapshot.fragment ?? null;
1293
+ !deepEqual(nextParams, this.#params()) && this.#params.set(nextParams);
1294
+ !deepEqual(nextDeepestParams, this.#deepestParams()) && this.#deepestParams.set(nextDeepestParams);
1295
+ !deepEqual(nextQuery, this.#query()) && this.#query.set(nextQuery);
1296
+ !deepEqual(nextData, this.#data()) && this.#data.set(nextData);
1297
+ !deepEqual(nextMergedData, this.#mergedData()) && this.#mergedData.set(nextMergedData);
1298
+ this.#url() !== nextUrl && this.#url.set(nextUrl);
1299
+ this.#routePattern() !== nextRoutePattern && this.#routePattern.set(nextRoutePattern);
1300
+ this.#fragment() !== nextFragment && this.#fragment.set(nextFragment);
1301
+ };
1302
+ read();
1303
+ const subscription = this.router.events.subscribe((event) => {
1304
+ if (event instanceof NavigationEnd) {
1305
+ read();
1306
+ }
1307
+ });
1308
+ this.destroyRef.onDestroy(() => subscription.unsubscribe());
1309
+ }
1310
+ selectData(key, strategy = 'deepest') {
1311
+ return computed(() => {
1312
+ const source = strategy === 'merged' ? this.#mergedData() : this.#data();
1313
+ return source[key];
1314
+ });
1315
+ }
1316
+ selectParam(key, strategy = 'merged') {
1317
+ return computed(() => {
1318
+ const source = strategy === 'deepest' ? this.#deepestParams() : this.#params();
1319
+ return source[key];
1320
+ });
1321
+ }
1322
+ matchesPath(path) {
1323
+ const current = this.#url();
1324
+ return typeof path === 'string' ? current === path : path.test(current);
1325
+ }
1326
+ deepestSnapshot() {
1327
+ return deepestRouteSnapshot(this.router.routerState.snapshot.root);
1328
+ }
1329
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1330
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, providedIn: 'root' });
1331
+ }
1332
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, decorators: [{
1333
+ type: Injectable,
1334
+ args: [{ providedIn: 'root' }]
1335
+ }], ctorParameters: () => [] });
1336
+
1337
+ const PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY = '__rePresentiaRouteNamespacePreload';
1338
+ const PRESENTIA_ROUTE_NAMESPACES_DATA_KEY = 'presentiaNamespaces';
1339
+ function providePresentiaRouteNamespacePreload() {
1340
+ return makeEnvironmentProviders([
1341
+ provideAppInitializer(() => {
1342
+ const router = inject(Router, { optional: true });
1343
+ const langConfig = inject(LANG_CONFIG);
1344
+ const normalized = normalizeRouteNamespacePreloadConfig(langConfig.routeNamespacePreload);
1345
+ if (!router || !normalized) {
1346
+ return;
1347
+ }
1348
+ if (typeof ngDevMode !== 'undefined' && normalized.manifest && Object.keys(normalized.manifest).length === 0) {
1349
+ // eslint-disable-next-line no-console
1350
+ console.warn('[presentia] routeNamespacePreload.manifest is empty. Route-data preload still works, but manifest mode is effectively disabled.');
1351
+ }
1352
+ router.resetConfig(patchRoutesWithNamespacePreload(router.config, normalized));
1353
+ }),
1354
+ ]);
1355
+ }
1356
+ function normalizeRouteNamespacePreloadConfig(config) {
1357
+ if (!config) {
1358
+ return null;
1359
+ }
1360
+ return {
1361
+ mode: config.mode ?? 'blocking',
1362
+ dataKey: config.dataKey?.trim() || PRESENTIA_ROUTE_NAMESPACES_DATA_KEY,
1363
+ manifest: config.manifest,
1364
+ mergeStrategy: config.mergeStrategy ?? 'append',
1365
+ onError: config.onError ?? 'continue',
1366
+ };
1367
+ }
1368
+ function patchRoutesWithNamespacePreload(routes, config) {
1369
+ return routes.map((route) => patchRouteWithNamespacePreload(route, config));
1370
+ }
1371
+ function patchRouteWithNamespacePreload(route, config) {
1372
+ const nextChildren = route.children ? patchRoutesWithNamespacePreload(route.children, config) : route.children;
1373
+ if (route.redirectTo) {
1374
+ return nextChildren === route.children ? route : { ...route, children: nextChildren };
1375
+ }
1376
+ const nextResolve = {
1377
+ ...(route.resolve ?? {}),
1378
+ [PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY]: makeRouteNamespacePreloadResolver(config),
1379
+ };
1380
+ return {
1381
+ ...route,
1382
+ ...(nextChildren ? { children: nextChildren } : {}),
1383
+ resolve: nextResolve,
1384
+ };
1385
+ }
1386
+ function makeRouteNamespacePreloadResolver(config) {
1387
+ return async (route, state) => {
1388
+ const lang = inject(LangService);
1389
+ const diagnostics = inject(RouteNamespaceDiagnosticsService);
1390
+ const namespaces = resolveRouteNamespaces(route, state, config);
1391
+ diagnostics.registerRouteNamespaces(state.url, namespaces);
1392
+ if (!namespaces.length) {
1393
+ return true;
1394
+ }
1395
+ if (config.mode === 'lazy') {
1396
+ queueMicrotask(() => {
1397
+ void lang.loadNamespaces(namespaces);
1398
+ });
1399
+ return true;
1400
+ }
1401
+ try {
1402
+ await lang.loadNamespaces(namespaces);
1403
+ }
1404
+ catch (error) {
1405
+ if (config.onError === 'throw') {
1406
+ throw error;
1407
+ }
1408
+ // Keep navigation alive; runtime lazy loading remains a fallback.
1409
+ }
1410
+ return true;
1411
+ };
1412
+ }
1413
+ function resolveRouteNamespaces(route, state, config) {
1414
+ const dataNamespaces = readNamespacesFromRouteData(route, config.dataKey);
1415
+ const manifestNamespaces = readNamespacesFromManifest(route, state, config.manifest);
1416
+ if (config.mergeStrategy === 'replace' && dataNamespaces.length) {
1417
+ return dataNamespaces;
1418
+ }
1419
+ return uniqueNamespaces([...manifestNamespaces, ...dataNamespaces]);
1420
+ }
1421
+ function readNamespacesFromRouteData(route, dataKey) {
1422
+ return uniqueNamespaces(route.pathFromRoot.flatMap((snapshot) => {
1423
+ const value = snapshot.data?.[dataKey];
1424
+ return Array.isArray(value) ? value : [];
1425
+ }));
1426
+ }
1427
+ function readNamespacesFromManifest(route, state, manifest) {
1428
+ if (!manifest) {
1429
+ return [];
1430
+ }
1431
+ const actualUrl = normalizeUrlPath(state.url);
1432
+ const routePath = snapshotRouteConfigPath(route);
1433
+ return uniqueNamespaces(Object.entries(manifest)
1434
+ .filter(([key]) => matchesManifestKey(actualUrl, routePath, key))
1435
+ .flatMap(([, namespaces]) => namespaces));
1436
+ }
1437
+ function matchesManifestKey(actualUrl, routePath, manifestKey) {
1438
+ const normalizedKey = normalizeUrlPath(manifestKey);
1439
+ return compareRoutes(actualUrl, normalizedKey) || (!!routePath && routePath === normalizedKey);
1440
+ }
1441
+ function snapshotRouteConfigPath(route) {
1442
+ const templatePath = route.pathFromRoot
1443
+ .map((item) => item.routeConfig?.path ?? '')
1444
+ .filter(Boolean)
1445
+ .join('/');
1446
+ return normalizeUrlPath(templatePath);
1447
+ }
1448
+ function normalizeUrlPath(url) {
1449
+ const [path] = url.split(/[?#]/, 1);
1450
+ const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
1451
+ return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
1452
+ }
1453
+ function uniqueNamespaces(namespaces) {
1454
+ return Array.from(new Set(namespaces.map((ns) => ns.trim()).filter(Boolean)));
1455
+ }
1456
+
1008
1457
  /**
1009
1458
  * Type-safe mapping of available theme names.
1010
1459
  *
@@ -1026,27 +1475,12 @@ const themes = {
1026
1475
  dark: 'dark',
1027
1476
  };
1028
1477
  /**
1029
- * CSS class prefix used for dark theme styling.
1030
- *
1031
- * This constant defines the prefix applied to HTML elements when the dark theme is active.
1032
- * It is typically added to the root element or specific components to enable dark theme styles.
1033
- *
1034
- * @example
1035
- * ```typescript
1036
- * document.body.classList.add(darkThemePrefix); // Applies 're-dark' class
1037
- * ```
1478
+ * @deprecated Prefer `theme.dom.darkClassName` via `providePresentia(...)`.
1038
1479
  */
1039
1480
  const darkThemePrefix = 're-dark';
1040
1481
 
1041
1482
  /**
1042
- * Default theme configuration object.
1043
- *
1044
- * Defines the initial theme settings for the application.
1045
- * By default, sets the light theme as the active theme.
1046
- *
1047
- * This configuration can be overridden when providing `THEME_CONFIG` token
1048
- * at the module or application level.
1049
- * ```
1483
+ * @deprecated Prefer configuring themes through `providePresentia({ theme: ... })`.
1050
1484
  */
1051
1485
  const defaultThemeConfig = {
1052
1486
  defaultTheme: themes.light,
@@ -1070,6 +1504,28 @@ const defaultThemeConfig = {
1070
1504
  */
1071
1505
  const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
1072
1506
 
1507
+ /**
1508
+ * @deprecated Prefer configuring theme persistence through `providePresentia({ theme: { persistence... } })`.
1509
+ */
1510
+ const defaultThemePersistenceAdapter = {
1511
+ getItem: (key) => localStorage.getItem(key),
1512
+ setItem: (key, value) => localStorage.setItem(key, value),
1513
+ };
1514
+ /**
1515
+ * DI token for the persistence adapter used by `ThemeService`
1516
+ * to store and retrieve the selected theme (default key: `'theme'`).
1517
+ *
1518
+ * By default `presentia` provides a `localStorage`-backed adapter.
1519
+ *
1520
+ * Example:
1521
+ * ```ts
1522
+ * { provide: THEME_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
1523
+ * ```
1524
+ */
1525
+ const THEME_PERSISTENCE_ADAPTER = new InjectionToken('RE_THEME_PERSISTENCE_ADAPTER', {
1526
+ factory: () => defaultThemePersistenceAdapter,
1527
+ });
1528
+
1073
1529
  /**
1074
1530
  * Service for managing application theme.
1075
1531
  *
@@ -1085,11 +1541,18 @@ const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
1085
1541
  */
1086
1542
  class ThemeService {
1087
1543
  config = inject(THEME_CONFIG);
1088
- themeDefault = this.config?.defaultTheme || themes.light;
1089
- darkPrefix = this.config?.darkThemePrefix || darkThemePrefix;
1544
+ registry = this.resolveRegistry(this.config?.registry);
1545
+ themeDefault = this.resolveInitialTheme(this.config?.defaultTheme);
1546
+ domStrategy = this.config?.dom?.strategy ?? 'root-class';
1547
+ darkPrefix = this.config?.dom?.darkThemePrefix || this.config?.darkThemePrefix || darkThemePrefix;
1548
+ attributeName = this.config?.dom?.attributeName || 'data-theme';
1549
+ themeClassPrefix = this.config?.dom?.themeClassPrefix;
1550
+ classNameBuilder = this.config?.dom?.classNameBuilder;
1551
+ rootSelector = this.config?.dom?.rootSelector;
1552
+ persistence = inject(THEME_PERSISTENCE_ADAPTER);
1090
1553
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1091
1554
  document = inject(DOCUMENT);
1092
- #theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : []));
1555
+ #theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : /* istanbul ignore next */ []));
1093
1556
  /**
1094
1557
  * Current active theme (`light` or `dark`).
1095
1558
  *
@@ -1099,26 +1562,22 @@ class ThemeService {
1099
1562
  * <div [class]="themeService.theme()"></div>
1100
1563
  * ```
1101
1564
  */
1102
- theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : []));
1565
+ theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
1103
1566
  /**
1104
1567
  * Convenient flag returning `true` if the light theme is active.
1105
1568
  * Suitable for conditional style application or resource selection.
1106
1569
  */
1107
- isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : []));
1570
+ isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : /* istanbul ignore next */ []));
1571
+ isDark = computed(() => this.#theme() === themes.dark, ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
1108
1572
  constructor() {
1109
1573
  effect(() => {
1110
1574
  if (!this.isBrowser) {
1111
1575
  this.#theme.set(this.themeDefault);
1112
1576
  return;
1113
1577
  }
1114
- const theme = localStorage.getItem('theme') || this.themeDefault;
1578
+ const theme = this.resolveTheme(this.getStoredTheme());
1115
1579
  this.switch(theme);
1116
1580
  });
1117
- effect(() => {
1118
- if (this.isBrowser) {
1119
- localStorage.setItem('theme', this.#theme());
1120
- }
1121
- });
1122
1581
  }
1123
1582
  /**
1124
1583
  * Switches theme.
@@ -1129,41 +1588,212 @@ class ThemeService {
1129
1588
  * @param theme — explicit theme value (`'light'` or `'dark'`).
1130
1589
  */
1131
1590
  switch(theme) {
1132
- const newTheme = theme ?? (this.#theme() === themes.light ? themes.dark : themes.light);
1591
+ const requestedTheme = theme ? this.resolveTheme(theme) : undefined;
1592
+ const newTheme = requestedTheme ?? this.nextTheme();
1133
1593
  if (this.isBrowser) {
1134
- const html = this.document.querySelector('html');
1135
- newTheme === themes.dark && html.classList.add(this.darkPrefix);
1136
- newTheme === themes.light && html.classList.remove(this.darkPrefix);
1594
+ this.applyThemeToDom(newTheme);
1595
+ this.persistTheme(newTheme);
1137
1596
  }
1138
1597
  this.#theme.set(newTheme);
1139
1598
  }
1140
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1141
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ThemeService, providedIn: 'root' });
1599
+ is(theme) {
1600
+ const allowed = Array.isArray(theme) ? theme : [theme];
1601
+ return allowed.includes(this.#theme());
1602
+ }
1603
+ resolveTheme(theme) {
1604
+ if (theme && this.registry.includes(theme)) {
1605
+ return theme;
1606
+ }
1607
+ return this.themeDefault;
1608
+ }
1609
+ resolveRegistry(registry) {
1610
+ return registry?.length ? registry : [themes.light, themes.dark];
1611
+ }
1612
+ resolveInitialTheme(theme) {
1613
+ const next = theme ?? themes.light;
1614
+ return this.registry.includes(next) ? next : (this.registry[0] ?? themes.light);
1615
+ }
1616
+ getStoredTheme() {
1617
+ return this.persistence.getItem('theme');
1618
+ }
1619
+ persistTheme(theme) {
1620
+ this.persistence.setItem('theme', theme);
1621
+ }
1622
+ applyThemeToDom(theme) {
1623
+ const root = this.resolveRootElement();
1624
+ if (!root) {
1625
+ return;
1626
+ }
1627
+ if (this.domStrategy === 'data-attribute') {
1628
+ root.setAttribute(this.attributeName, theme);
1629
+ return;
1630
+ }
1631
+ this.clearThemeClasses(root);
1632
+ if (theme === themes.dark) {
1633
+ root.classList.add(this.darkPrefix);
1634
+ }
1635
+ else {
1636
+ root.classList.remove(this.darkPrefix);
1637
+ }
1638
+ const className = this.resolveThemeClassName(theme);
1639
+ if (className) {
1640
+ root.classList.add(className);
1641
+ }
1642
+ }
1643
+ nextTheme() {
1644
+ const currentIndex = this.registry.indexOf(this.#theme());
1645
+ if (currentIndex < 0) {
1646
+ return this.themeDefault;
1647
+ }
1648
+ return this.registry[(currentIndex + 1) % this.registry.length] ?? this.themeDefault;
1649
+ }
1650
+ resolveRootElement() {
1651
+ if (!this.rootSelector) {
1652
+ return this.document.documentElement;
1653
+ }
1654
+ return this.document.querySelector(this.rootSelector);
1655
+ }
1656
+ resolveThemeClassName(theme) {
1657
+ if (this.classNameBuilder) {
1658
+ return this.classNameBuilder(theme);
1659
+ }
1660
+ if (this.themeClassPrefix) {
1661
+ return `${this.themeClassPrefix}${theme}`;
1662
+ }
1663
+ return null;
1664
+ }
1665
+ clearThemeClasses(root) {
1666
+ if (!this.classNameBuilder && !this.themeClassPrefix) {
1667
+ return;
1668
+ }
1669
+ for (const registeredTheme of this.registry) {
1670
+ const className = this.resolveThemeClassName(registeredTheme);
1671
+ if (className) {
1672
+ root.classList.remove(className);
1673
+ }
1674
+ }
1675
+ }
1676
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1677
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, providedIn: 'root' });
1142
1678
  }
1143
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ThemeService, decorators: [{
1679
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, decorators: [{
1144
1680
  type: Injectable,
1145
1681
  args: [{
1146
1682
  providedIn: 'root',
1147
1683
  }]
1148
1684
  }], ctorParameters: () => [] });
1149
1685
 
1686
+ let warnedLegacyProvider = false;
1687
+ /**
1688
+ * @deprecated Prefer `providePresentia(...)` for new integrations.
1689
+ */
1150
1690
  function provideReInit(config) {
1691
+ if (typeof ngDevMode !== 'undefined' && !warnedLegacyProvider) {
1692
+ warnedLegacyProvider = true;
1693
+ // eslint-disable-next-line no-console
1694
+ console.warn('[presentia] provideReInit(...) is the legacy 1.x provider. Prefer providePresentia(...) for the grouped v2 config API.');
1695
+ }
1696
+ return buildPresentiaProviders(config);
1697
+ }
1698
+ function __resetPresentiaProviderWarningsForTests() {
1699
+ warnedLegacyProvider = false;
1700
+ }
1701
+ function buildPresentiaProviders(config) {
1151
1702
  return makeEnvironmentProviders([
1152
1703
  { provide: TRANSLATION, deps: [LangService], useFactory: (ls) => ls },
1153
1704
  { provide: SELECTED_LANG, deps: [LangService], useFactory: (ls) => ls[innerLangVal] },
1154
- { provide: CHANGE_LANG, deps: [LangService], useFactory: (ls) => ls.setLang },
1705
+ { provide: CHANGE_LANG, deps: [LangService], useFactory: (ls) => (lang) => ls.setLang(lang) },
1155
1706
  { provide: SELECTED_THEME, deps: [ThemeService], useFactory: (ls) => ls.theme },
1156
- { provide: CHANGE_THEME, deps: [ThemeService], useFactory: (ls) => ls.switch },
1707
+ {
1708
+ provide: CHANGE_THEME,
1709
+ deps: [ThemeService],
1710
+ useFactory: (ls) => (theme) => ls.switch(theme),
1711
+ },
1157
1712
  { provide: CURRENT_DEVICE, deps: [AdaptiveService], useFactory: (ls) => ls.device },
1158
1713
  { provide: DEVICE_BREAKPOINTS, useValue: config.breakpoints || defaultBreakpoints },
1159
1714
  { provide: THEME_CONFIG, useValue: config.theme || defaultThemeConfig },
1715
+ { provide: THEME_PERSISTENCE_ADAPTER, useValue: defaultThemePersistenceAdapter },
1160
1716
  { provide: LANG_CONFIG, useValue: config.locale || { defaultValue: '--------' } },
1161
1717
  { provide: LANG_PIPE_CONFIG, useValue: config.langPipe || {} },
1162
1718
  { provide: LANG_MISSING_KEY_HANDLER, useValue: config.langMissingKeyHandler ?? null },
1163
1719
  { provide: LOCALE_ID, useValue: config.locale.defaultLang ?? 'ru' },
1720
+ providePresentiaRouteNamespacePreload(),
1721
+ ]);
1722
+ }
1723
+
1724
+ function providePresentia(config) {
1725
+ const routesConfig = config.lang.preload?.routes;
1726
+ const diagnosticsEnabled = config.lang.diagnostics?.lateNamespaceLoads;
1727
+ const routeNamespacePreload = routesConfig || diagnosticsEnabled
1728
+ ? { ...routesConfig, diagnostics: diagnosticsEnabled ?? routesConfig?.diagnostics }
1729
+ : undefined;
1730
+ const extraProviders = [];
1731
+ if (config.theme?.persistence === 'none') {
1732
+ extraProviders.push({
1733
+ provide: THEME_PERSISTENCE_ADAPTER,
1734
+ useValue: {
1735
+ getItem: () => null,
1736
+ setItem: () => undefined,
1737
+ },
1738
+ });
1739
+ }
1740
+ else if (config.theme?.persistenceAdapter) {
1741
+ extraProviders.push({
1742
+ provide: THEME_PERSISTENCE_ADAPTER,
1743
+ useValue: config.theme.persistenceAdapter,
1744
+ });
1745
+ }
1746
+ return makeEnvironmentProviders([
1747
+ buildPresentiaProviders({
1748
+ locale: {
1749
+ url: config.lang.source.url,
1750
+ isFromAssets: config.lang.source.fromAssets,
1751
+ defaultLang: config.lang.source.defaultLang,
1752
+ fallbackLang: config.lang.source.fallbackLang,
1753
+ supportedLangs: config.lang.source.supportedLangs,
1754
+ kgValue: config.lang.source.kgValue,
1755
+ defaultValue: config.lang.rendering?.missingValue,
1756
+ preloadNamespaces: config.lang.preload?.global,
1757
+ requestBuilder: config.lang.transport?.requestBuilder,
1758
+ requestOptionsFactory: config.lang.transport?.requestOptionsFactory,
1759
+ responseAdapter: config.lang.transport?.responseAdapter,
1760
+ batchRequestBuilder: config.lang.transport?.batchRequestBuilder,
1761
+ batchResponseAdapter: config.lang.transport?.batchResponseAdapter,
1762
+ namespaceCache: config.lang.cache,
1763
+ routeNamespacePreload,
1764
+ maxBatchSize: config.lang.transport?.maxBatchSize,
1765
+ },
1766
+ theme: config.theme
1767
+ ? {
1768
+ registry: config.theme.registry,
1769
+ defaultTheme: config.theme.defaultTheme,
1770
+ darkThemePrefix: config.theme.dom?.darkClassName,
1771
+ dom: config.theme.dom
1772
+ ? {
1773
+ strategy: config.theme.dom.strategy,
1774
+ rootSelector: config.theme.dom.rootSelector,
1775
+ darkThemePrefix: config.theme.dom.darkClassName,
1776
+ attributeName: config.theme.dom.attributeName,
1777
+ themeClassPrefix: config.theme.dom.classPrefix,
1778
+ classNameBuilder: config.theme.dom.classNameBuilder,
1779
+ }
1780
+ : undefined,
1781
+ }
1782
+ : undefined,
1783
+ breakpoints: config.adaptive?.breakpoints,
1784
+ langPipe: {
1785
+ placeholder: config.lang.rendering?.placeholder,
1786
+ },
1787
+ langMissingKeyHandler: config.lang.missingKeyHandler,
1788
+ }),
1789
+ ...extraProviders,
1164
1790
  ]);
1165
1791
  }
1166
1792
 
1793
+ /**
1794
+ * @deprecated Prefer `providePresentia(...)` for new integrations.
1795
+ */
1796
+
1167
1797
  /**
1168
1798
  * Service for managing page SEO metadata.
1169
1799
  *
@@ -1291,11 +1921,12 @@ class SeoService {
1291
1921
  link.href = href;
1292
1922
  }
1293
1923
  }
1294
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1295
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoService });
1924
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1925
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, providedIn: 'root' });
1296
1926
  }
1297
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoService, decorators: [{
1298
- type: Injectable
1927
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, decorators: [{
1928
+ type: Injectable,
1929
+ args: [{ providedIn: 'root' }]
1299
1930
  }] });
1300
1931
 
1301
1932
  /**
@@ -1319,8 +1950,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
1319
1950
  class SeoRouteListener {
1320
1951
  router = inject(Router);
1321
1952
  seo = inject(SeoService);
1322
- ar = inject(ActivatedRoute);
1323
1953
  destroyRef = inject(DestroyRef);
1954
+ initialized = false;
1955
+ baseUrl = '';
1324
1956
  /**
1325
1957
  * Initializes the route listener to monitor navigation events and update SEO metadata.
1326
1958
  * Subscribes to router NavigationEnd events and automatically unsubscribes on component destruction.
@@ -1329,10 +1961,11 @@ class SeoRouteListener {
1329
1961
  * Trailing slashes will be removed automatically.
1330
1962
  */
1331
1963
  init(baseUrl) {
1964
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
1332
1965
  const applyRouteSeo = () => {
1333
- const route = this.deepest(this.ar);
1334
- const data = route.snapshot.data;
1335
- const url = data.canonical ?? baseUrl.replace(/\/+$/, '') + this.router.url;
1966
+ const snapshot = deepestRouteSnapshot(this.router.routerState.snapshot.root);
1967
+ const data = snapshotMergedData(snapshot);
1968
+ const url = resolveCanonicalUrl(data.canonical, this.baseUrl, snapshotFullPath(snapshot));
1336
1969
  data.title && this.seo.setTitle(data.title);
1337
1970
  data.description && this.seo.setDescription(data.description);
1338
1971
  data.twitter && this.seo.setTwitter(data.twitter);
@@ -1347,124 +1980,44 @@ class SeoRouteListener {
1347
1980
  });
1348
1981
  };
1349
1982
  applyRouteSeo();
1350
- this.router.events
1351
- .pipe(filter((e) => e instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
1352
- .subscribe(() => applyRouteSeo());
1353
- }
1354
- /**
1355
- * Recursively finds the deepest (most nested) activated route in the route tree.
1356
- * This is used to extract route data from the currently active leaf route.
1357
- *
1358
- * @param r - The root activated route to start traversing from.
1359
- * @returns The deepest child route in the hierarchy.
1360
- */
1361
- deepest(r) {
1362
- let cur = r;
1363
- while (cur.firstChild) {
1364
- cur = cur.firstChild;
1983
+ if (this.initialized) {
1984
+ return;
1365
1985
  }
1366
- return cur;
1986
+ this.initialized = true;
1987
+ const subscription = this.router.events.subscribe((event) => {
1988
+ if (event instanceof NavigationEnd) {
1989
+ applyRouteSeo();
1990
+ }
1991
+ });
1992
+ this.destroyRef.onDestroy(() => subscription.unsubscribe());
1367
1993
  }
1368
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoRouteListener, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1369
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoRouteListener, providedIn: 'root' });
1994
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1995
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, providedIn: 'root' });
1370
1996
  }
1371
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoRouteListener, decorators: [{
1997
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, decorators: [{
1372
1998
  type: Injectable,
1373
1999
  args: [{ providedIn: 'root' }]
1374
2000
  }] });
1375
-
1376
- /**
1377
- * Reactive snapshot of the current route (the deepest active route).
1378
- * Updates on every `NavigationEnd` event.
1379
- *
1380
- * Provides:
1381
- * - `params` / `query` — strings (as in Angular Router)
1382
- * - `data` — arbitrary data (from route configuration/resolvers)
1383
- * - `url` — string assembled from `UrlSegment[]`
1384
- * - `fragment` — hash (#section)
1385
- * - `selectData(key)` — type-safe selector for `data`
1386
- * - `state` — combined computed object (convenient for single subscriber)
1387
- */
1388
- class RouteWatcher {
1389
- router = inject(Router);
1390
- destroyRef = inject(DestroyRef);
1391
- #params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : []));
1392
- #query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
1393
- #data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : []));
1394
- #url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : []));
1395
- #fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : []));
1396
- /** Signal for tracking and retrieving URL parameters */
1397
- params = this.#params.asReadonly();
1398
- /** Signal for tracking and retrieving query parameters */
1399
- query = this.#query.asReadonly();
1400
- /** Signal for tracking and retrieving route data */
1401
- data = this.#data.asReadonly();
1402
- /** Signal for tracking and retrieving URL */
1403
- url = this.#url.asReadonly();
1404
- /** Signal for tracking and retrieving URL fragment */
1405
- fragment = this.#fragment.asReadonly();
1406
- /** Combined computed snapshot (to avoid multiple effects) */
1407
- state = computed(() => ({
1408
- params: this.#params(),
1409
- query: this.#query(),
1410
- data: this.#data(),
1411
- url: this.#url(),
1412
- fragment: this.#fragment(),
1413
- }), ...(ngDevMode ? [{ debugName: "state" }] : []));
1414
- constructor() {
1415
- const read = () => {
1416
- const snap = this.deepestSnapshot();
1417
- const url = snapshotFullPath(snap);
1418
- const params = snapshotMergedParams(snap);
1419
- !deepEqual(params, this.#params()) && this.#params.set(params);
1420
- !deepEqual(snap.queryParams, this.#query()) && this.#query.set(snap.queryParams);
1421
- !deepEqual(snap.data, this.#data()) && this.#data.set(snap.data);
1422
- this.#url() !== url && this.#url.set(url);
1423
- this.#fragment() !== snap.fragment && this.#fragment.set(snap.fragment ?? null);
1424
- };
1425
- read();
1426
- this.router.events
1427
- .pipe(filter$1((e) => e instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
1428
- .subscribe(() => read());
2001
+ function resolveCanonicalUrl(canonical, baseUrl, path) {
2002
+ if (!canonical) {
2003
+ return joinBaseUrl(baseUrl, path);
1429
2004
  }
1430
- /** Convenient selector for a data key with type-safe casting */
1431
- selectData(key) {
1432
- return computed(() => this.#data()[key]);
2005
+ if (/^https?:\/\//i.test(canonical)) {
2006
+ return canonical;
1433
2007
  }
1434
- deepestSnapshot() {
1435
- // work with snapshot — we need a "frozen" point at NavigationEnd moment
1436
- let snap = this.router.routerState.snapshot.root;
1437
- while (snap.firstChild) {
1438
- snap = snap.firstChild;
1439
- }
1440
- return snap;
1441
- }
1442
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1443
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, providedIn: 'root' });
1444
- }
1445
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, decorators: [{
1446
- type: Injectable,
1447
- args: [{ providedIn: 'root' }]
1448
- }], ctorParameters: () => [] });
1449
- /** Joins `UrlSegment[]` into a path string */
1450
- function joinUrl(segments) {
1451
- return segments.length ? segments.map((s) => s.path).join('/') : '';
2008
+ return joinBaseUrl(baseUrl, canonical);
1452
2009
  }
1453
- /** Builds a full route path from root to current snapshot */
1454
- function snapshotFullPath(snap) {
1455
- return snap.pathFromRoot
1456
- .map((s) => joinUrl(s.url))
1457
- .filter(Boolean)
1458
- .join('/');
1459
- }
1460
- /** Merges params from root to the deepest snapshot (child keys override parent). */
1461
- function snapshotMergedParams(snap) {
1462
- return snap.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.params }), {});
2010
+ function joinBaseUrl(baseUrl, path) {
2011
+ const normalizedPath = path.replace(/^\/+/, '');
2012
+ if (!baseUrl) {
2013
+ return normalizedPath ? `/${normalizedPath}` : '/';
2014
+ }
2015
+ return normalizedPath ? `${baseUrl}/${normalizedPath}` : baseUrl;
1463
2016
  }
1464
2017
 
1465
2018
  /**
1466
2019
  * Generated bundle index. Do not edit.
1467
2020
  */
1468
2021
 
1469
- export { AdaptiveService, DEVICE_BREAKPOINTS, IfDeviceDirective, LANG_CONFIG, LANG_MISSING_KEY_HANDLER, LANG_PIPE_CONFIG, LangDirective, LangPipe, LangService, RouteWatcher, SeoRouteListener, SeoService, THEME_CONFIG, ThemeService, darkThemePrefix, defaultBreakpoints, defaultThemeConfig, innerLangVal, provideReInit, themes };
2022
+ export { AdaptiveService, DEVICE_BREAKPOINTS, IfDeviceDirective, LANG_CONFIG, LANG_MISSING_KEY_HANDLER, LANG_PERSISTENCE_ADAPTER, LANG_PIPE_CONFIG, LangDirective, LangPipe, LangService, PRESENTIA_ROUTE_NAMESPACES_DATA_KEY, RouteNamespaceDiagnosticsService, RouteWatcher, SeoRouteListener, SeoService, THEME_CONFIG, THEME_PERSISTENCE_ADAPTER, ThemeService, darkThemePrefix, defaultBreakpoints, defaultThemeConfig, defaultThemePersistenceAdapter, innerLangVal, providePresentia, providePresentiaRouteNamespacePreload, provideReInit, themes };
1470
2023
  //# sourceMappingURL=reforgium-presentia.mjs.map