@reforgium/presentia 1.5.0 → 2.1.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,13 +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, makeEnvironmentProviders, provideAppInitializer, input, ElementRef, Renderer2, Injector, afterNextRender, runInInjectionContext, Pipe, LOCALE_ID } from '@angular/core';
3
+ import { LruCache, MemoryStorage, deepEqual, compareRoutes, getChainedValue, 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
7
  import { HttpClient } from '@angular/common/http';
8
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
11
 
12
12
  /**
13
13
  * Default breakpoints for device type detection.
@@ -73,10 +73,10 @@ const DEVICE_BREAKPOINTS = new InjectionToken('RE_DEVICE_BREAKPOINTS', {
73
73
  */
74
74
  class AdaptiveService {
75
75
  /** @internal Signal of the current device type. */
76
- #device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : []));
76
+ #device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : /* istanbul ignore next */ []));
77
77
  /** @internal Signals of the current window width and height. */
78
- #width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] : []));
79
- #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 */ []));
80
80
  /**
81
81
  * Current device type (reactive signal).
82
82
  * Possible values: `'desktop' | 'tablet' | 'mobile'`.
@@ -99,12 +99,15 @@ class AdaptiveService {
99
99
  * Computed signal indicating whether the current device is a desktop.
100
100
  * Used for conditional rendering or layout configuration.
101
101
  */
102
- 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 */ []));
103
106
  /**
104
107
  * Computed signal determining whether the screen is in portrait orientation.
105
108
  * Returns `true` if window height is greater than width.
106
109
  */
107
- isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : []));
110
+ isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : /* istanbul ignore next */ []));
108
111
  deviceBreakpoints = inject(DEVICE_BREAKPOINTS);
109
112
  devicePriority = Object.keys(this.deviceBreakpoints);
110
113
  destroyRef = inject(DestroyRef);
@@ -148,10 +151,28 @@ class AdaptiveService {
148
151
  }
149
152
  });
150
153
  }
151
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: AdaptiveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
152
- 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' });
153
174
  }
154
- 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: [{
155
176
  type: Injectable,
156
177
  args: [{
157
178
  providedIn: 'root',
@@ -179,8 +200,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
179
200
  * the template is automatically added or removed from the DOM.
180
201
  */
181
202
  class IfDeviceDirective {
182
- deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] : []));
183
- 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 */ []));
184
207
  tpl = inject(TemplateRef);
185
208
  vcr = inject(ViewContainerRef);
186
209
  adaptive = inject(AdaptiveService);
@@ -190,20 +213,37 @@ class IfDeviceDirective {
190
213
  constructor() {
191
214
  effect(() => {
192
215
  const device = this.deviceInput();
216
+ const atLeast = this.atLeastInput();
217
+ const between = this.betweenInput();
193
218
  if (device) {
194
219
  this.allowedDevices = Array.isArray(device) ? device : [device];
195
220
  }
221
+ else if (!atLeast && !between) {
222
+ this.allowedDevices = [];
223
+ }
196
224
  this.updateView();
197
225
  });
198
226
  }
199
227
  set reIfDevice(value) {
200
228
  this.deviceInput.set(value);
201
229
  }
230
+ set reIfDeviceAtLeast(value) {
231
+ this.atLeastInput.set(value);
232
+ }
233
+ set reIfDeviceBetween(value) {
234
+ this.betweenInput.set(value);
235
+ }
202
236
  set inverse(value) {
203
237
  this.inverseInput.set(!!value);
204
238
  }
205
239
  updateView() {
206
- 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());
207
247
  const shouldShow = this.inverseInput() ? !isAllowed : isAllowed;
208
248
  if (shouldShow && !this.hasView) {
209
249
  this.vcr.createEmbeddedView(this.tpl);
@@ -214,18 +254,24 @@ class IfDeviceDirective {
214
254
  this.hasView = false;
215
255
  }
216
256
  }
217
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: IfDeviceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
218
- 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 });
219
259
  }
220
- 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: [{
221
261
  type: Directive,
222
262
  args: [{
223
- selector: '[reIfDevice]',
263
+ selector: '[reIfDevice],[reIfDeviceAtLeast],[reIfDeviceBetween]',
224
264
  standalone: true,
225
265
  }]
226
266
  }], ctorParameters: () => [], propDecorators: { reIfDevice: [{
227
267
  type: Input,
228
268
  args: ['reIfDevice']
269
+ }], reIfDeviceAtLeast: [{
270
+ type: Input,
271
+ args: ['reIfDeviceAtLeast']
272
+ }], reIfDeviceBetween: [{
273
+ type: Input,
274
+ args: ['reIfDeviceBetween']
229
275
  }], inverse: [{
230
276
  type: Input
231
277
  }] } });
@@ -234,6 +280,77 @@ const innerLangVal = Symbol('reInnerLangVal');
234
280
 
235
281
  const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER');
236
282
 
283
+ function createPresentiaStorage(strategy, storage) {
284
+ if (storage) {
285
+ return storage;
286
+ }
287
+ switch (strategy ?? 'persist') {
288
+ case 'none':
289
+ return createNoopStorage();
290
+ case 'memory':
291
+ return new MemoryStorage();
292
+ case 'session':
293
+ return createBrowserStringStorage('session');
294
+ case 'lru':
295
+ return new LruCache(1);
296
+ case 'persist':
297
+ default:
298
+ return createBrowserStringStorage('local');
299
+ }
300
+ }
301
+ function createBrowserStringStorage(kind) {
302
+ const getStorage = () => {
303
+ if (typeof globalThis === 'undefined') {
304
+ return null;
305
+ }
306
+ return kind === 'local' ? (globalThis.localStorage ?? null) : (globalThis.sessionStorage ?? null);
307
+ };
308
+ return {
309
+ get length() {
310
+ return getStorage()?.length ?? 0;
311
+ },
312
+ get(key) {
313
+ return getStorage()?.getItem(key) ?? null;
314
+ },
315
+ set(key, value) {
316
+ getStorage()?.setItem(key, value);
317
+ },
318
+ remove(key) {
319
+ getStorage()?.removeItem(key);
320
+ },
321
+ clear() {
322
+ getStorage()?.clear();
323
+ },
324
+ };
325
+ }
326
+ function createNoopStorage() {
327
+ return {
328
+ get length() {
329
+ return 0;
330
+ },
331
+ get: () => null,
332
+ set: () => undefined,
333
+ remove: () => undefined,
334
+ clear: () => undefined,
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Optional DI token for a custom persistence adapter used by `LangService`
340
+ * to store and retrieve the selected language (default key: `'lang'`).
341
+ *
342
+ * When not provided the service falls back to `localStorage` directly.
343
+ *
344
+ * Example:
345
+ * ```ts
346
+ * { provide: LANG_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
347
+ * ```
348
+ */
349
+ const defaultLangPersistenceAdapter = createPresentiaStorage('persist');
350
+ const LANG_PERSISTENCE_ADAPTER = new InjectionToken('RE_LANG_PERSISTENCE_ADAPTER', {
351
+ factory: () => defaultLangPersistenceAdapter,
352
+ });
353
+
237
354
  /**
238
355
  * Injection token for providing locale configuration to the language module.
239
356
  *
@@ -254,6 +371,341 @@ const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER
254
371
  */
255
372
  const LANG_CONFIG = new InjectionToken('RE_LANG_CONFIG');
256
373
 
374
+ function deepestActivatedRoute(route) {
375
+ let current = route;
376
+ while (current.firstChild) {
377
+ current = current.firstChild;
378
+ }
379
+ return current;
380
+ }
381
+ function deepestRouteSnapshot(snapshot) {
382
+ let current = snapshot;
383
+ while (current.firstChild) {
384
+ current = current.firstChild;
385
+ }
386
+ return current;
387
+ }
388
+ function joinUrl(segments) {
389
+ return segments.length ? segments.map((segment) => segment.path).join('/') : '';
390
+ }
391
+ function snapshotFullPath(snapshot) {
392
+ return snapshot.pathFromRoot
393
+ .map((route) => joinUrl(route.url))
394
+ .filter(Boolean)
395
+ .join('/');
396
+ }
397
+ function snapshotRoutePattern(snapshot) {
398
+ return snapshot.pathFromRoot
399
+ .map((route) => route.routeConfig?.path ?? '')
400
+ .filter(Boolean)
401
+ .join('/');
402
+ }
403
+ function snapshotMergedParams(snapshot) {
404
+ return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.params }), {});
405
+ }
406
+ function snapshotDeepestParams(snapshot) {
407
+ const mergedParams = snapshotMergedParams(snapshot);
408
+ const keys = extractRouteParamKeys(snapshot.routeConfig?.path ?? '');
409
+ if (!keys.length) {
410
+ return {};
411
+ }
412
+ return keys.reduce((acc, key) => {
413
+ const value = mergedParams[key];
414
+ if (value !== undefined) {
415
+ acc[key] = value;
416
+ }
417
+ return acc;
418
+ }, {});
419
+ }
420
+ function snapshotMergedData(snapshot) {
421
+ return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.data }), {});
422
+ }
423
+ function extractRouteParamKeys(path) {
424
+ return path
425
+ .split('/')
426
+ .filter((segment) => segment.startsWith(':'))
427
+ .map((segment) => segment.slice(1))
428
+ .filter(Boolean);
429
+ }
430
+
431
+ /**
432
+ * Reactive snapshot of the current route (the deepest active route).
433
+ * Updates on every `NavigationEnd` event.
434
+ */
435
+ class RouteWatcher {
436
+ router = inject(Router);
437
+ destroyRef = inject(DestroyRef);
438
+ #params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : /* istanbul ignore next */ []));
439
+ #deepestParams = signal({}, ...(ngDevMode ? [{ debugName: "#deepestParams" }] : /* istanbul ignore next */ []));
440
+ #query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
441
+ #data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : /* istanbul ignore next */ []));
442
+ #mergedData = signal({}, ...(ngDevMode ? [{ debugName: "#mergedData" }] : /* istanbul ignore next */ []));
443
+ #url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : /* istanbul ignore next */ []));
444
+ #routePattern = signal('', ...(ngDevMode ? [{ debugName: "#routePattern" }] : /* istanbul ignore next */ []));
445
+ #fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : /* istanbul ignore next */ []));
446
+ /** Params merged from root to deepest route. */
447
+ params = this.#params.asReadonly();
448
+ /** Params declared on the deepest route only. */
449
+ deepestParams = this.#deepestParams.asReadonly();
450
+ /** Query params from the current navigation. */
451
+ query = this.#query.asReadonly();
452
+ /** Deepest route data only. */
453
+ data = this.#data.asReadonly();
454
+ /** Route data merged from root to deepest route. */
455
+ mergedData = this.#mergedData.asReadonly();
456
+ /** Full current url path assembled from root to deepest route. */
457
+ url = this.#url.asReadonly();
458
+ /** Current route config pattern, e.g. `orgs/:orgId/users/:id`. */
459
+ routePattern = this.#routePattern.asReadonly();
460
+ /** Current url fragment without `#`. */
461
+ fragment = this.#fragment.asReadonly();
462
+ /** Combined computed snapshot. */
463
+ state = computed(() => ({
464
+ params: this.#params(),
465
+ deepestParams: this.#deepestParams(),
466
+ query: this.#query(),
467
+ data: this.#data(),
468
+ mergedData: this.#mergedData(),
469
+ url: this.#url(),
470
+ routePattern: this.#routePattern(),
471
+ fragment: this.#fragment(),
472
+ }), ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
473
+ constructor() {
474
+ const read = () => {
475
+ const snapshot = this.deepestSnapshot();
476
+ const nextUrl = snapshotFullPath(snapshot);
477
+ const nextRoutePattern = snapshotRoutePattern(snapshot);
478
+ const nextParams = snapshotMergedParams(snapshot);
479
+ const nextDeepestParams = snapshotDeepestParams(snapshot);
480
+ const nextMergedData = snapshotMergedData(snapshot);
481
+ const nextQuery = snapshot.queryParams;
482
+ const nextData = snapshot.data;
483
+ const nextFragment = snapshot.fragment ?? null;
484
+ !deepEqual(nextParams, this.#params()) && this.#params.set(nextParams);
485
+ !deepEqual(nextDeepestParams, this.#deepestParams()) && this.#deepestParams.set(nextDeepestParams);
486
+ !deepEqual(nextQuery, this.#query()) && this.#query.set(nextQuery);
487
+ !deepEqual(nextData, this.#data()) && this.#data.set(nextData);
488
+ !deepEqual(nextMergedData, this.#mergedData()) && this.#mergedData.set(nextMergedData);
489
+ this.#url() !== nextUrl && this.#url.set(nextUrl);
490
+ this.#routePattern() !== nextRoutePattern && this.#routePattern.set(nextRoutePattern);
491
+ this.#fragment() !== nextFragment && this.#fragment.set(nextFragment);
492
+ };
493
+ read();
494
+ const subscription = this.router.events.subscribe((event) => {
495
+ if (event instanceof NavigationEnd) {
496
+ read();
497
+ }
498
+ });
499
+ this.destroyRef.onDestroy(() => subscription.unsubscribe());
500
+ }
501
+ selectData(key, strategy = 'deepest') {
502
+ return computed(() => {
503
+ const source = strategy === 'merged' ? this.#mergedData() : this.#data();
504
+ return source[key];
505
+ });
506
+ }
507
+ selectParam(key, strategy = 'merged') {
508
+ return computed(() => {
509
+ const source = strategy === 'deepest' ? this.#deepestParams() : this.#params();
510
+ return source[key];
511
+ });
512
+ }
513
+ matchesPath(path) {
514
+ const current = this.#url();
515
+ return typeof path === 'string' ? current === path : path.test(current);
516
+ }
517
+ deepestSnapshot() {
518
+ return deepestRouteSnapshot(this.router.routerState.snapshot.root);
519
+ }
520
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
521
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, providedIn: 'root' });
522
+ }
523
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, decorators: [{
524
+ type: Injectable,
525
+ args: [{ providedIn: 'root' }]
526
+ }], ctorParameters: () => [] });
527
+
528
+ /**
529
+ * @deprecated Diagnostics are usually enabled through route preload config, not by consuming this service directly.
530
+ */
531
+ class RouteNamespaceDiagnosticsService {
532
+ router = inject(Router, { optional: true });
533
+ enabled = !!inject(LANG_CONFIG).routeNamespacePreload?.diagnostics;
534
+ warned = new Set();
535
+ currentUrl = '';
536
+ currentNamespaces = new Set();
537
+ navigationSettled = false;
538
+ constructor() {
539
+ if (!this.enabled || !this.router) {
540
+ return;
541
+ }
542
+ this.router.events.subscribe((event) => {
543
+ if (event instanceof NavigationEnd) {
544
+ const url = normalizeUrlPath$1(event.urlAfterRedirects || event.url);
545
+ if (url === this.currentUrl) {
546
+ this.navigationSettled = true;
547
+ }
548
+ }
549
+ });
550
+ }
551
+ registerRouteNamespaces(url, namespaces) {
552
+ if (!this.enabled) {
553
+ return;
554
+ }
555
+ this.currentUrl = normalizeUrlPath$1(url);
556
+ this.currentNamespaces = new Set(namespaces.map((ns) => ns.trim()).filter(Boolean));
557
+ this.navigationSettled = false;
558
+ }
559
+ warnLateNamespaceLoad(namespace) {
560
+ if (!this.enabled || !this.navigationSettled || typeof ngDevMode === 'undefined') {
561
+ return;
562
+ }
563
+ const normalizedNamespace = namespace.trim();
564
+ if (!normalizedNamespace || this.currentNamespaces.has(normalizedNamespace)) {
565
+ return;
566
+ }
567
+ const key = `${this.currentUrl}|${normalizedNamespace}`;
568
+ if (this.warned.has(key)) {
569
+ return;
570
+ }
571
+ this.warned.add(key);
572
+ // eslint-disable-next-line no-console
573
+ console.warn(`[presentia] Namespace "${normalizedNamespace}" was loaded after route activation for "${this.currentUrl}". ` +
574
+ 'Add it to routeNamespacePreload manifest or route data to avoid raw i18n keys on first paint.');
575
+ }
576
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
577
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, providedIn: 'root' });
578
+ }
579
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, decorators: [{
580
+ type: Injectable,
581
+ args: [{ providedIn: 'root' }]
582
+ }], ctorParameters: () => [] });
583
+ function normalizeUrlPath$1(url) {
584
+ const [path] = url.split(/[?#]/, 1);
585
+ const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
586
+ return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
587
+ }
588
+
589
+ const PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY = '__rePresentiaRouteNamespacePreload';
590
+ const PRESENTIA_ROUTE_NAMESPACES_DATA_KEY = 'presentiaNamespaces';
591
+ function providePresentiaRouteNamespacePreload() {
592
+ return makeEnvironmentProviders([
593
+ provideAppInitializer(() => {
594
+ const router = inject(Router, { optional: true });
595
+ const langConfig = inject(LANG_CONFIG);
596
+ const normalized = normalizeRouteNamespacePreloadConfig(langConfig.routeNamespacePreload);
597
+ if (!router || !normalized) {
598
+ return;
599
+ }
600
+ if (typeof ngDevMode !== 'undefined' && normalized.manifest && Object.keys(normalized.manifest).length === 0) {
601
+ // eslint-disable-next-line no-console
602
+ console.warn('[presentia] routeNamespacePreload.manifest is empty. Route-data preload still works, but manifest mode is effectively disabled.');
603
+ }
604
+ router.resetConfig(patchRoutesWithNamespacePreload(router.config, normalized));
605
+ }),
606
+ ]);
607
+ }
608
+ function normalizeRouteNamespacePreloadConfig(config) {
609
+ if (!config) {
610
+ return null;
611
+ }
612
+ return {
613
+ mode: config.mode ?? 'blocking',
614
+ dataKey: config.dataKey?.trim() || PRESENTIA_ROUTE_NAMESPACES_DATA_KEY,
615
+ manifest: config.manifest,
616
+ mergeStrategy: config.mergeStrategy ?? 'append',
617
+ onError: config.onError ?? 'continue',
618
+ };
619
+ }
620
+ function patchRoutesWithNamespacePreload(routes, config) {
621
+ return routes.map((route) => patchRouteWithNamespacePreload(route, config));
622
+ }
623
+ function patchRouteWithNamespacePreload(route, config) {
624
+ const nextChildren = route.children ? patchRoutesWithNamespacePreload(route.children, config) : route.children;
625
+ if (route.redirectTo) {
626
+ return nextChildren === route.children ? route : { ...route, children: nextChildren };
627
+ }
628
+ const nextResolve = {
629
+ ...(route.resolve ?? {}),
630
+ [PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY]: makeRouteNamespacePreloadResolver(config),
631
+ };
632
+ return {
633
+ ...route,
634
+ ...(nextChildren ? { children: nextChildren } : {}),
635
+ resolve: nextResolve,
636
+ };
637
+ }
638
+ function makeRouteNamespacePreloadResolver(config) {
639
+ return async (route, state) => {
640
+ const lang = inject(LangService);
641
+ const diagnostics = inject(RouteNamespaceDiagnosticsService);
642
+ const namespaces = resolveRouteNamespaces(route, state, config);
643
+ diagnostics.registerRouteNamespaces(state.url, namespaces);
644
+ if (!namespaces.length) {
645
+ return true;
646
+ }
647
+ if (config.mode === 'lazy') {
648
+ queueMicrotask(() => {
649
+ void lang.loadNamespaces(namespaces);
650
+ });
651
+ return true;
652
+ }
653
+ try {
654
+ await lang.loadNamespaces(namespaces);
655
+ }
656
+ catch (error) {
657
+ if (config.onError === 'throw') {
658
+ throw error;
659
+ }
660
+ // Keep navigation alive; runtime lazy loading remains a fallback.
661
+ }
662
+ return true;
663
+ };
664
+ }
665
+ function resolveRouteNamespaces(route, state, config) {
666
+ const dataNamespaces = readNamespacesFromRouteData(route, config.dataKey);
667
+ const manifestNamespaces = readNamespacesFromManifest(route, state, config.manifest);
668
+ if (config.mergeStrategy === 'replace' && dataNamespaces.length) {
669
+ return dataNamespaces;
670
+ }
671
+ return uniqueNamespaces([...manifestNamespaces, ...dataNamespaces]);
672
+ }
673
+ function readNamespacesFromRouteData(route, dataKey) {
674
+ return uniqueNamespaces(route.pathFromRoot.flatMap((snapshot) => {
675
+ const value = snapshot.data?.[dataKey];
676
+ return Array.isArray(value) ? value : [];
677
+ }));
678
+ }
679
+ function readNamespacesFromManifest(route, state, manifest) {
680
+ if (!manifest) {
681
+ return [];
682
+ }
683
+ const actualUrl = normalizeUrlPath(state.url);
684
+ const routePath = snapshotRouteConfigPath(route);
685
+ return uniqueNamespaces(Object.entries(manifest)
686
+ .filter(([key]) => matchesManifestKey(actualUrl, routePath, key))
687
+ .flatMap(([, namespaces]) => namespaces));
688
+ }
689
+ function matchesManifestKey(actualUrl, routePath, manifestKey) {
690
+ const normalizedKey = normalizeUrlPath(manifestKey);
691
+ return compareRoutes(actualUrl, normalizedKey) || (!!routePath && routePath === normalizedKey);
692
+ }
693
+ function snapshotRouteConfigPath(route) {
694
+ const templatePath = route.pathFromRoot
695
+ .map((item) => item.routeConfig?.path ?? '')
696
+ .filter(Boolean)
697
+ .join('/');
698
+ return normalizeUrlPath(templatePath);
699
+ }
700
+ function normalizeUrlPath(url) {
701
+ const [path] = url.split(/[?#]/, 1);
702
+ const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
703
+ return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
704
+ }
705
+ function uniqueNamespaces(namespaces) {
706
+ return Array.from(new Set(namespaces.map((ns) => ns.trim()).filter(Boolean)));
707
+ }
708
+
257
709
  /**
258
710
  * LangService provides functionality for managing and tracking language settings
259
711
  * and translations in the application. It is designed to handle localization needs,
@@ -266,13 +718,15 @@ class LangService {
266
718
  config = inject(LANG_CONFIG);
267
719
  http = inject(HttpClient);
268
720
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
721
+ persistence = inject(LANG_PERSISTENCE_ADAPTER);
269
722
  missingKeyHandler = inject(LANG_MISSING_KEY_HANDLER, { optional: true });
723
+ routeNamespaceDiagnostics = inject(RouteNamespaceDiagnosticsService, { optional: true });
270
724
  supportedLangSet = new Set([
271
725
  ...LangService.BUILTIN_LANGS,
272
726
  ...this.normalizeSupportedLangs(this.config.supportedLangs ?? []),
273
727
  ]);
274
- #lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : []));
275
- #cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : []));
728
+ #lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : /* istanbul ignore next */ []));
729
+ #cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : /* istanbul ignore next */ []));
276
730
  #loadedNamespaces = new Set();
277
731
  #pendingLoads = new Map();
278
732
  #pendingBatchLoads = new Map();
@@ -289,7 +743,7 @@ class LangService {
289
743
  currentLang = computed(() => {
290
744
  const lang = this.#lang();
291
745
  return lang === 'kg' ? (this.config?.kgValue ?? 'kg') : lang;
292
- }, ...(ngDevMode ? [{ debugName: "currentLang" }] : []));
746
+ }, ...(ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []));
293
747
  /**
294
748
  * Extracts readonly value from private property `#lang` and assigns it to `innerLangVal`.
295
749
  * Expected that property `#lang` has `asReadonly` method that returns immutable representation.
@@ -318,7 +772,7 @@ class LangService {
318
772
  if (langVal !== this.#lang()) {
319
773
  this.#lang.set(langVal);
320
774
  if (this.isBrowser) {
321
- localStorage.setItem('lang', langVal);
775
+ this.persistence.set('lang', langVal);
322
776
  }
323
777
  const namespaces = Array.from(this.#loadedNamespaces.values()).map((key) => this.namespaceFromKey(key));
324
778
  this.#loadedNamespaces.clear();
@@ -357,6 +811,7 @@ class LangService {
357
811
  if (this.isNamespaceValid(key)) {
358
812
  return;
359
813
  }
814
+ this.routeNamespaceDiagnostics?.warnLateNamespaceLoad(ns);
360
815
  this.tryExpireNamespace(key);
361
816
  if (this.#pendingLoads.has(key)) {
362
817
  return this.#pendingLoads.get(key);
@@ -411,14 +866,27 @@ class LangService {
411
866
  await Promise.all(toLoad.map((ns) => this.loadNamespace(ns)));
412
867
  return;
413
868
  }
414
- const batchKey = this.makeBatchKey(requestedLang, toLoad);
869
+ const maxBatchSize = this.normalizeMaxBatchSize(this.config.maxBatchSize);
870
+ const chunks = maxBatchSize && toLoad.length > maxBatchSize ? this.chunkNamespaces(toLoad, maxBatchSize) : [toLoad];
871
+ await Promise.all(chunks.map((chunk) => {
872
+ if (chunk.length < 2) {
873
+ return this.loadNamespace(chunk[0]);
874
+ }
875
+ return this.loadNamespaceBatch(chunk, requestedLang);
876
+ }));
877
+ }
878
+ async loadNamespaceBatch(namespaces, requestedLang) {
879
+ for (const ns of namespaces) {
880
+ this.routeNamespaceDiagnostics?.warnLateNamespaceLoad(ns);
881
+ }
882
+ const batchKey = this.makeBatchKey(requestedLang, namespaces);
415
883
  if (this.#pendingBatchLoads.has(batchKey)) {
416
884
  return this.#pendingBatchLoads.get(batchKey);
417
885
  }
418
886
  const promise = (async () => {
419
887
  try {
420
888
  const context = {
421
- namespaces: toLoad,
889
+ namespaces,
422
890
  lang: requestedLang,
423
891
  isFromAssets: this.config.isFromAssets,
424
892
  baseUrl: this.config.url,
@@ -430,7 +898,7 @@ class LangService {
430
898
  }
431
899
  const payloads = this.config.batchResponseAdapter(response, this.toBatchResponseContext(context));
432
900
  const merged = {};
433
- for (const ns of toLoad) {
901
+ for (const ns of namespaces) {
434
902
  const nsPayload = payloads[ns];
435
903
  if (!nsPayload) {
436
904
  continue;
@@ -450,6 +918,19 @@ class LangService {
450
918
  this.#pendingBatchLoads.set(batchKey, promise);
451
919
  return promise;
452
920
  }
921
+ normalizeMaxBatchSize(value) {
922
+ if (!value || value < 1) {
923
+ return null;
924
+ }
925
+ return Math.floor(value);
926
+ }
927
+ chunkNamespaces(namespaces, size) {
928
+ const chunks = [];
929
+ for (let i = 0; i < namespaces.length; i += size) {
930
+ chunks.push(namespaces.slice(i, i + size));
931
+ }
932
+ return chunks;
933
+ }
453
934
  evictNamespace(ns) {
454
935
  const key = this.makeNamespaceKey(ns);
455
936
  if (!this.#loadedNamespaces.has(key)) {
@@ -512,7 +993,7 @@ class LangService {
512
993
  if (!this.isBrowser) {
513
994
  return defaultLang;
514
995
  }
515
- return normalize(localStorage.getItem('lang')) ?? defaultLang;
996
+ return normalize(this.persistence.get('lang')) ?? defaultLang;
516
997
  }
517
998
  makeUrl(ns, lang) {
518
999
  if (this.config.requestBuilder) {
@@ -664,10 +1145,10 @@ class LangService {
664
1145
  .map((lang) => lang.trim().toLowerCase())
665
1146
  .filter((lang) => !!lang && LangService.LANG_CODE_RE.test(lang));
666
1147
  }
667
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
668
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangService, providedIn: 'root' });
1148
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1149
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, providedIn: 'root' });
669
1150
  }
670
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangService, decorators: [{
1151
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, decorators: [{
671
1152
  type: Injectable,
672
1153
  args: [{ providedIn: 'root' }]
673
1154
  }], ctorParameters: () => [] });
@@ -705,19 +1186,19 @@ class LangDirective {
705
1186
  * Localization mode: defines which parts of the element will be translated.
706
1187
  * @default 'all'
707
1188
  */
708
- lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : {}), alias: 'reLang' });
1189
+ lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : /* istanbul ignore next */ {}), alias: 'reLang' });
709
1190
  /**
710
1191
  * Explicit key for text content translation.
711
1192
  */
712
- reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : []));
1193
+ reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : /* istanbul ignore next */ []));
713
1194
  /**
714
1195
  * Explicit attribute-to-key map for translation.
715
1196
  */
716
- reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : []));
1197
+ reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : /* istanbul ignore next */ []));
717
1198
  /**
718
1199
  * Name of an additional attribute to localize (besides standard `title`, `label`, `placeholder`).
719
1200
  */
720
- langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : []));
1201
+ langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : /* istanbul ignore next */ []));
721
1202
  el = inject(ElementRef);
722
1203
  renderer = inject(Renderer2);
723
1204
  service = inject(LangService);
@@ -913,10 +1394,10 @@ class LangDirective {
913
1394
  }
914
1395
  return this.service.get(key);
915
1396
  }
916
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
917
- 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 });
1397
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1398
+ 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 });
918
1399
  }
919
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangDirective, decorators: [{
1400
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, decorators: [{
920
1401
  type: Directive,
921
1402
  args: [{ selector: '[reLang]', standalone: true }]
922
1403
  }], 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: [{
@@ -1024,10 +1505,10 @@ class LangPipe {
1024
1505
  // eslint-disable-next-line no-console
1025
1506
  console.warn(`LangPipe: namespace loaded but key "${query}" is unresolved`);
1026
1507
  }
1027
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1028
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.1", ngImport: i0, type: LangPipe, isStandalone: true, name: "lang", pure: false });
1508
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1509
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, isStandalone: true, name: "lang", pure: false });
1029
1510
  }
1030
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangPipe, decorators: [{
1511
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, decorators: [{
1031
1512
  type: Pipe,
1032
1513
  args: [{ name: 'lang', standalone: true, pure: false }]
1033
1514
  }], ctorParameters: () => [] });
@@ -1053,27 +1534,12 @@ const themes = {
1053
1534
  dark: 'dark',
1054
1535
  };
1055
1536
  /**
1056
- * CSS class prefix used for dark theme styling.
1057
- *
1058
- * This constant defines the prefix applied to HTML elements when the dark theme is active.
1059
- * It is typically added to the root element or specific components to enable dark theme styles.
1060
- *
1061
- * @example
1062
- * ```typescript
1063
- * document.body.classList.add(darkThemePrefix); // Applies 're-dark' class
1064
- * ```
1537
+ * @deprecated Prefer `theme.dom.darkClassName` via `providePresentia(...)`.
1065
1538
  */
1066
1539
  const darkThemePrefix = 're-dark';
1067
1540
 
1068
1541
  /**
1069
- * Default theme configuration object.
1070
- *
1071
- * Defines the initial theme settings for the application.
1072
- * By default, sets the light theme as the active theme.
1073
- *
1074
- * This configuration can be overridden when providing `THEME_CONFIG` token
1075
- * at the module or application level.
1076
- * ```
1542
+ * @deprecated Prefer configuring themes through `providePresentia({ theme: ... })`.
1077
1543
  */
1078
1544
  const defaultThemeConfig = {
1079
1545
  defaultTheme: themes.light,
@@ -1097,6 +1563,25 @@ const defaultThemeConfig = {
1097
1563
  */
1098
1564
  const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
1099
1565
 
1566
+ /**
1567
+ * @deprecated Prefer configuring theme persistence through `providePresentia({ theme: { persistence... } })`.
1568
+ */
1569
+ const defaultThemePersistenceAdapter = createPresentiaStorage('persist');
1570
+ /**
1571
+ * DI token for the persistence adapter used by `ThemeService`
1572
+ * to store and retrieve the selected theme (default key: `'theme'`).
1573
+ *
1574
+ * By default `presentia` provides a `localStorage`-backed adapter.
1575
+ *
1576
+ * Example:
1577
+ * ```ts
1578
+ * { provide: THEME_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
1579
+ * ```
1580
+ */
1581
+ const THEME_PERSISTENCE_ADAPTER = new InjectionToken('RE_THEME_PERSISTENCE_ADAPTER', {
1582
+ factory: () => defaultThemePersistenceAdapter,
1583
+ });
1584
+
1100
1585
  /**
1101
1586
  * Service for managing application theme.
1102
1587
  *
@@ -1112,11 +1597,18 @@ const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
1112
1597
  */
1113
1598
  class ThemeService {
1114
1599
  config = inject(THEME_CONFIG);
1115
- themeDefault = this.config?.defaultTheme || themes.light;
1116
- darkPrefix = this.config?.darkThemePrefix || darkThemePrefix;
1600
+ registry = this.resolveRegistry(this.config?.registry);
1601
+ themeDefault = this.resolveInitialTheme(this.config?.defaultTheme);
1602
+ domStrategy = this.config?.dom?.strategy ?? 'root-class';
1603
+ darkPrefix = this.config?.dom?.darkThemePrefix || this.config?.darkThemePrefix || darkThemePrefix;
1604
+ attributeName = this.config?.dom?.attributeName || 'data-theme';
1605
+ themeClassPrefix = this.config?.dom?.themeClassPrefix;
1606
+ classNameBuilder = this.config?.dom?.classNameBuilder;
1607
+ rootSelector = this.config?.dom?.rootSelector;
1608
+ persistence = inject(THEME_PERSISTENCE_ADAPTER);
1117
1609
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1118
1610
  document = inject(DOCUMENT);
1119
- #theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : []));
1611
+ #theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : /* istanbul ignore next */ []));
1120
1612
  /**
1121
1613
  * Current active theme (`light` or `dark`).
1122
1614
  *
@@ -1126,26 +1618,22 @@ class ThemeService {
1126
1618
  * <div [class]="themeService.theme()"></div>
1127
1619
  * ```
1128
1620
  */
1129
- theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : []));
1621
+ theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
1130
1622
  /**
1131
1623
  * Convenient flag returning `true` if the light theme is active.
1132
1624
  * Suitable for conditional style application or resource selection.
1133
1625
  */
1134
- isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : []));
1626
+ isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : /* istanbul ignore next */ []));
1627
+ isDark = computed(() => this.#theme() === themes.dark, ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
1135
1628
  constructor() {
1136
1629
  effect(() => {
1137
1630
  if (!this.isBrowser) {
1138
1631
  this.#theme.set(this.themeDefault);
1139
1632
  return;
1140
1633
  }
1141
- const theme = this.resolveTheme(localStorage.getItem('theme'));
1634
+ const theme = this.resolveTheme(this.getStoredTheme());
1142
1635
  this.switch(theme);
1143
1636
  });
1144
- effect(() => {
1145
- if (this.isBrowser) {
1146
- localStorage.setItem('theme', this.#theme());
1147
- }
1148
- });
1149
1637
  }
1150
1638
  /**
1151
1639
  * Switches theme.
@@ -1157,28 +1645,116 @@ class ThemeService {
1157
1645
  */
1158
1646
  switch(theme) {
1159
1647
  const requestedTheme = theme ? this.resolveTheme(theme) : undefined;
1160
- const newTheme = requestedTheme ?? (this.#theme() === themes.light ? themes.dark : themes.light);
1648
+ const newTheme = requestedTheme ?? this.nextTheme();
1161
1649
  if (this.isBrowser) {
1162
- const html = this.document.documentElement;
1163
- newTheme === themes.dark && html.classList.add(this.darkPrefix);
1164
- newTheme === themes.light && html.classList.remove(this.darkPrefix);
1650
+ this.applyThemeToDom(newTheme);
1651
+ this.persistTheme(newTheme);
1165
1652
  }
1166
1653
  this.#theme.set(newTheme);
1167
1654
  }
1655
+ is(theme) {
1656
+ const allowed = Array.isArray(theme) ? theme : [theme];
1657
+ return allowed.includes(this.#theme());
1658
+ }
1168
1659
  resolveTheme(theme) {
1169
- return theme === themes.dark ? themes.dark : themes.light;
1660
+ if (theme && this.registry.includes(theme)) {
1661
+ return theme;
1662
+ }
1663
+ return this.themeDefault;
1664
+ }
1665
+ resolveRegistry(registry) {
1666
+ return registry?.length ? registry : [themes.light, themes.dark];
1667
+ }
1668
+ resolveInitialTheme(theme) {
1669
+ const next = theme ?? themes.light;
1670
+ return this.registry.includes(next) ? next : (this.registry[0] ?? themes.light);
1671
+ }
1672
+ getStoredTheme() {
1673
+ return this.persistence.get('theme');
1674
+ }
1675
+ persistTheme(theme) {
1676
+ this.persistence.set('theme', theme);
1677
+ }
1678
+ applyThemeToDom(theme) {
1679
+ const root = this.resolveRootElement();
1680
+ if (!root) {
1681
+ return;
1682
+ }
1683
+ if (this.domStrategy === 'data-attribute') {
1684
+ root.setAttribute(this.attributeName, theme);
1685
+ return;
1686
+ }
1687
+ this.clearThemeClasses(root);
1688
+ if (theme === themes.dark) {
1689
+ root.classList.add(this.darkPrefix);
1690
+ }
1691
+ else {
1692
+ root.classList.remove(this.darkPrefix);
1693
+ }
1694
+ const className = this.resolveThemeClassName(theme);
1695
+ if (className) {
1696
+ root.classList.add(className);
1697
+ }
1698
+ }
1699
+ nextTheme() {
1700
+ const currentIndex = this.registry.indexOf(this.#theme());
1701
+ if (currentIndex < 0) {
1702
+ return this.themeDefault;
1703
+ }
1704
+ return this.registry[(currentIndex + 1) % this.registry.length] ?? this.themeDefault;
1705
+ }
1706
+ resolveRootElement() {
1707
+ if (!this.rootSelector) {
1708
+ return this.document.documentElement;
1709
+ }
1710
+ return this.document.querySelector(this.rootSelector);
1711
+ }
1712
+ resolveThemeClassName(theme) {
1713
+ if (this.classNameBuilder) {
1714
+ return this.classNameBuilder(theme);
1715
+ }
1716
+ if (this.themeClassPrefix) {
1717
+ return `${this.themeClassPrefix}${theme}`;
1718
+ }
1719
+ return null;
1170
1720
  }
1171
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1172
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ThemeService, providedIn: 'root' });
1721
+ clearThemeClasses(root) {
1722
+ if (!this.classNameBuilder && !this.themeClassPrefix) {
1723
+ return;
1724
+ }
1725
+ for (const registeredTheme of this.registry) {
1726
+ const className = this.resolveThemeClassName(registeredTheme);
1727
+ if (className) {
1728
+ root.classList.remove(className);
1729
+ }
1730
+ }
1731
+ }
1732
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1733
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, providedIn: 'root' });
1173
1734
  }
1174
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ThemeService, decorators: [{
1735
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, decorators: [{
1175
1736
  type: Injectable,
1176
1737
  args: [{
1177
1738
  providedIn: 'root',
1178
1739
  }]
1179
1740
  }], ctorParameters: () => [] });
1180
1741
 
1742
+ let warnedLegacyProvider = false;
1743
+ /**
1744
+ * @deprecated Prefer `providePresentia(...)` for new integrations.
1745
+ */
1181
1746
  function provideReInit(config) {
1747
+ if (typeof ngDevMode !== 'undefined' && !warnedLegacyProvider) {
1748
+ warnedLegacyProvider = true;
1749
+ // eslint-disable-next-line no-console
1750
+ console.warn('[presentia] provideReInit(...) is the legacy 1.x provider. Prefer providePresentia(...) for the grouped v2 config API.');
1751
+ }
1752
+ return buildPresentiaProviders(config);
1753
+ }
1754
+ function __resetPresentiaProviderWarningsForTests() {
1755
+ warnedLegacyProvider = false;
1756
+ }
1757
+ function buildPresentiaProviders(config) {
1182
1758
  return makeEnvironmentProviders([
1183
1759
  { provide: TRANSLATION, deps: [LangService], useFactory: (ls) => ls },
1184
1760
  { provide: SELECTED_LANG, deps: [LangService], useFactory: (ls) => ls[innerLangVal] },
@@ -1192,13 +1768,78 @@ function provideReInit(config) {
1192
1768
  { provide: CURRENT_DEVICE, deps: [AdaptiveService], useFactory: (ls) => ls.device },
1193
1769
  { provide: DEVICE_BREAKPOINTS, useValue: config.breakpoints || defaultBreakpoints },
1194
1770
  { provide: THEME_CONFIG, useValue: config.theme || defaultThemeConfig },
1771
+ { provide: THEME_PERSISTENCE_ADAPTER, useValue: defaultThemePersistenceAdapter },
1772
+ { provide: LANG_PERSISTENCE_ADAPTER, useValue: defaultLangPersistenceAdapter },
1195
1773
  { provide: LANG_CONFIG, useValue: config.locale || { defaultValue: '--------' } },
1196
1774
  { provide: LANG_PIPE_CONFIG, useValue: config.langPipe || {} },
1197
1775
  { provide: LANG_MISSING_KEY_HANDLER, useValue: config.langMissingKeyHandler ?? null },
1198
1776
  { provide: LOCALE_ID, useValue: config.locale.defaultLang ?? 'ru' },
1777
+ providePresentiaRouteNamespacePreload(),
1778
+ ]);
1779
+ }
1780
+
1781
+ function providePresentia(config) {
1782
+ const routesConfig = config.lang.preload?.routes;
1783
+ const diagnosticsEnabled = config.lang.diagnostics?.lateNamespaceLoads;
1784
+ const routeNamespacePreload = routesConfig || diagnosticsEnabled
1785
+ ? { ...routesConfig, diagnostics: diagnosticsEnabled ?? routesConfig?.diagnostics }
1786
+ : undefined;
1787
+ const extraProviders = [];
1788
+ const langStorage = createPresentiaStorage(config.lang.persistence, config.lang.storage ?? config.lang.persistenceAdapter);
1789
+ const themeStorage = createPresentiaStorage(config.theme?.persistence, config.theme?.storage ?? config.theme?.persistenceAdapter);
1790
+ extraProviders.push({ provide: LANG_PERSISTENCE_ADAPTER, useValue: langStorage });
1791
+ extraProviders.push({ provide: THEME_PERSISTENCE_ADAPTER, useValue: themeStorage });
1792
+ return makeEnvironmentProviders([
1793
+ buildPresentiaProviders({
1794
+ locale: {
1795
+ url: config.lang.source.url,
1796
+ isFromAssets: config.lang.source.fromAssets,
1797
+ defaultLang: config.lang.source.defaultLang,
1798
+ fallbackLang: config.lang.source.fallbackLang,
1799
+ supportedLangs: config.lang.source.supportedLangs,
1800
+ kgValue: config.lang.source.kgValue,
1801
+ defaultValue: config.lang.rendering?.missingValue,
1802
+ preloadNamespaces: config.lang.preload?.global,
1803
+ requestBuilder: config.lang.transport?.requestBuilder,
1804
+ requestOptionsFactory: config.lang.transport?.requestOptionsFactory,
1805
+ responseAdapter: config.lang.transport?.responseAdapter,
1806
+ batchRequestBuilder: config.lang.transport?.batchRequestBuilder,
1807
+ batchResponseAdapter: config.lang.transport?.batchResponseAdapter,
1808
+ namespaceCache: config.lang.cache,
1809
+ routeNamespacePreload,
1810
+ maxBatchSize: config.lang.transport?.maxBatchSize,
1811
+ },
1812
+ theme: config.theme
1813
+ ? {
1814
+ registry: config.theme.registry,
1815
+ defaultTheme: config.theme.defaultTheme,
1816
+ darkThemePrefix: config.theme.dom?.darkClassName,
1817
+ dom: config.theme.dom
1818
+ ? {
1819
+ strategy: config.theme.dom.strategy,
1820
+ rootSelector: config.theme.dom.rootSelector,
1821
+ darkThemePrefix: config.theme.dom.darkClassName,
1822
+ attributeName: config.theme.dom.attributeName,
1823
+ themeClassPrefix: config.theme.dom.classPrefix,
1824
+ classNameBuilder: config.theme.dom.classNameBuilder,
1825
+ }
1826
+ : undefined,
1827
+ }
1828
+ : undefined,
1829
+ breakpoints: config.adaptive?.breakpoints,
1830
+ langPipe: {
1831
+ placeholder: config.lang.rendering?.placeholder,
1832
+ },
1833
+ langMissingKeyHandler: config.lang.missingKeyHandler,
1834
+ }),
1835
+ ...extraProviders,
1199
1836
  ]);
1200
1837
  }
1201
1838
 
1839
+ /**
1840
+ * @deprecated Prefer `providePresentia(...)` for new integrations.
1841
+ */
1842
+
1202
1843
  /**
1203
1844
  * Service for managing page SEO metadata.
1204
1845
  *
@@ -1326,10 +1967,10 @@ class SeoService {
1326
1967
  link.href = href;
1327
1968
  }
1328
1969
  }
1329
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1330
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoService, providedIn: 'root' });
1970
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1971
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, providedIn: 'root' });
1331
1972
  }
1332
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoService, decorators: [{
1973
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, decorators: [{
1333
1974
  type: Injectable,
1334
1975
  args: [{ providedIn: 'root' }]
1335
1976
  }] });
@@ -1355,7 +1996,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
1355
1996
  class SeoRouteListener {
1356
1997
  router = inject(Router);
1357
1998
  seo = inject(SeoService);
1358
- ar = inject(ActivatedRoute);
1359
1999
  destroyRef = inject(DestroyRef);
1360
2000
  initialized = false;
1361
2001
  baseUrl = '';
@@ -1369,9 +2009,9 @@ class SeoRouteListener {
1369
2009
  init(baseUrl) {
1370
2010
  this.baseUrl = baseUrl.replace(/\/+$/, '');
1371
2011
  const applyRouteSeo = () => {
1372
- const route = this.deepest(this.ar);
1373
- const data = route.snapshot.data;
1374
- const url = data.canonical ?? this.baseUrl + this.router.url;
2012
+ const snapshot = deepestRouteSnapshot(this.router.routerState.snapshot.root);
2013
+ const data = snapshotMergedData(snapshot);
2014
+ const url = resolveCanonicalUrl(data.canonical, this.baseUrl, snapshotFullPath(snapshot));
1375
2015
  data.title && this.seo.setTitle(data.title);
1376
2016
  data.description && this.seo.setDescription(data.description);
1377
2017
  data.twitter && this.seo.setTwitter(data.twitter);
@@ -1397,123 +2037,33 @@ class SeoRouteListener {
1397
2037
  });
1398
2038
  this.destroyRef.onDestroy(() => subscription.unsubscribe());
1399
2039
  }
1400
- /**
1401
- * Recursively finds the deepest (most nested) activated route in the route tree.
1402
- * This is used to extract route data from the currently active leaf route.
1403
- *
1404
- * @param r - The root activated route to start traversing from.
1405
- * @returns The deepest child route in the hierarchy.
1406
- */
1407
- deepest(r) {
1408
- let cur = r;
1409
- while (cur.firstChild) {
1410
- cur = cur.firstChild;
1411
- }
1412
- return cur;
1413
- }
1414
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoRouteListener, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1415
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoRouteListener, providedIn: 'root' });
2040
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2041
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, providedIn: 'root' });
1416
2042
  }
1417
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoRouteListener, decorators: [{
2043
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, decorators: [{
1418
2044
  type: Injectable,
1419
2045
  args: [{ providedIn: 'root' }]
1420
2046
  }] });
1421
-
1422
- /**
1423
- * Reactive snapshot of the current route (the deepest active route).
1424
- * Updates on every `NavigationEnd` event.
1425
- *
1426
- * Provides:
1427
- * - `params` / `query` — strings (as in Angular Router)
1428
- * - `data` — arbitrary data (from route configuration/resolvers)
1429
- * - `url` — string assembled from `UrlSegment[]`
1430
- * - `fragment` — hash (#section)
1431
- * - `selectData(key)` — type-safe selector for `data`
1432
- * - `state` — combined computed object (convenient for single subscriber)
1433
- */
1434
- class RouteWatcher {
1435
- router = inject(Router);
1436
- destroyRef = inject(DestroyRef);
1437
- #params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : []));
1438
- #query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
1439
- #data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : []));
1440
- #url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : []));
1441
- #fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : []));
1442
- /** Signal for tracking and retrieving URL parameters */
1443
- params = this.#params.asReadonly();
1444
- /** Signal for tracking and retrieving query parameters */
1445
- query = this.#query.asReadonly();
1446
- /** Signal for tracking and retrieving route data */
1447
- data = this.#data.asReadonly();
1448
- /** Signal for tracking and retrieving URL */
1449
- url = this.#url.asReadonly();
1450
- /** Signal for tracking and retrieving URL fragment */
1451
- fragment = this.#fragment.asReadonly();
1452
- /** Combined computed snapshot (to avoid multiple effects) */
1453
- state = computed(() => ({
1454
- params: this.#params(),
1455
- query: this.#query(),
1456
- data: this.#data(),
1457
- url: this.#url(),
1458
- fragment: this.#fragment(),
1459
- }), ...(ngDevMode ? [{ debugName: "state" }] : []));
1460
- constructor() {
1461
- const read = () => {
1462
- const snap = this.deepestSnapshot();
1463
- const url = snapshotFullPath(snap);
1464
- const params = snapshotMergedParams(snap);
1465
- !deepEqual(params, this.#params()) && this.#params.set(params);
1466
- !deepEqual(snap.queryParams, this.#query()) && this.#query.set(snap.queryParams);
1467
- !deepEqual(snap.data, this.#data()) && this.#data.set(snap.data);
1468
- this.#url() !== url && this.#url.set(url);
1469
- this.#fragment() !== snap.fragment && this.#fragment.set(snap.fragment ?? null);
1470
- };
1471
- read();
1472
- const subscription = this.router.events.subscribe((event) => {
1473
- if (event instanceof NavigationEnd) {
1474
- read();
1475
- }
1476
- });
1477
- this.destroyRef.onDestroy(() => subscription.unsubscribe());
2047
+ function resolveCanonicalUrl(canonical, baseUrl, path) {
2048
+ if (!canonical) {
2049
+ return joinBaseUrl(baseUrl, path);
1478
2050
  }
1479
- /** Convenient selector for a data key with type-safe casting */
1480
- selectData(key) {
1481
- return computed(() => this.#data()[key]);
2051
+ if (/^https?:\/\//i.test(canonical)) {
2052
+ return canonical;
1482
2053
  }
1483
- deepestSnapshot() {
1484
- // work with snapshot — we need a "frozen" point at NavigationEnd moment
1485
- let snap = this.router.routerState.snapshot.root;
1486
- while (snap.firstChild) {
1487
- snap = snap.firstChild;
1488
- }
1489
- return snap;
1490
- }
1491
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1492
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, providedIn: 'root' });
1493
- }
1494
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, decorators: [{
1495
- type: Injectable,
1496
- args: [{ providedIn: 'root' }]
1497
- }], ctorParameters: () => [] });
1498
- /** Joins `UrlSegment[]` into a path string */
1499
- function joinUrl(segments) {
1500
- return segments.length ? segments.map((s) => s.path).join('/') : '';
2054
+ return joinBaseUrl(baseUrl, canonical);
1501
2055
  }
1502
- /** Builds a full route path from root to current snapshot */
1503
- function snapshotFullPath(snap) {
1504
- return snap.pathFromRoot
1505
- .map((s) => joinUrl(s.url))
1506
- .filter(Boolean)
1507
- .join('/');
1508
- }
1509
- /** Merges params from root to the deepest snapshot (child keys override parent). */
1510
- function snapshotMergedParams(snap) {
1511
- return snap.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.params }), {});
2056
+ function joinBaseUrl(baseUrl, path) {
2057
+ const normalizedPath = path.replace(/^\/+/, '');
2058
+ if (!baseUrl) {
2059
+ return normalizedPath ? `/${normalizedPath}` : '/';
2060
+ }
2061
+ return normalizedPath ? `${baseUrl}/${normalizedPath}` : baseUrl;
1512
2062
  }
1513
2063
 
1514
2064
  /**
1515
2065
  * Generated bundle index. Do not edit.
1516
2066
  */
1517
2067
 
1518
- 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 };
2068
+ 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, defaultLangPersistenceAdapter, defaultThemeConfig, defaultThemePersistenceAdapter, innerLangVal, providePresentia, providePresentiaRouteNamespacePreload, provideReInit, themes };
1519
2069
  //# sourceMappingURL=reforgium-presentia.mjs.map