@reforgium/presentia 1.4.4 → 1.5.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,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
@@ -4,11 +4,10 @@ import { getChainedValue, TRANSLATION, SELECTED_LANG, CHANGE_LANG, SELECTED_THEM
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
9
  import { Title, Meta } from '@angular/platform-browser';
10
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.
@@ -111,6 +110,7 @@ class AdaptiveService {
111
110
  destroyRef = inject(DestroyRef);
112
111
  breakpointObserver = inject(BreakpointObserver);
113
112
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
113
+ resizeDebounceId = null;
114
114
  constructor() {
115
115
  if (!this.isBrowser) {
116
116
  this.#device.set('desktop');
@@ -129,14 +129,23 @@ class AdaptiveService {
129
129
  this.#device.set(device);
130
130
  }
131
131
  });
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);
132
+ const onResize = () => {
133
+ this.resizeDebounceId !== null && clearTimeout(this.resizeDebounceId);
134
+ this.resizeDebounceId = setTimeout(() => {
135
+ this.resizeDebounceId = null;
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);
140
+ }, 100);
141
+ };
142
+ window.addEventListener('resize', onResize, { passive: true });
143
+ this.destroyRef.onDestroy(() => {
144
+ window.removeEventListener('resize', onResize);
145
+ if (this.resizeDebounceId !== null) {
146
+ clearTimeout(this.resizeDebounceId);
147
+ this.resizeDebounceId = null;
148
+ }
140
149
  });
141
150
  }
142
151
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: AdaptiveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -325,6 +334,9 @@ class LangService {
325
334
  }
326
335
  return baseValue;
327
336
  }
337
+ has(query) {
338
+ return this.getChainedValue(query) !== undefined;
339
+ }
328
340
  observe(query, params) {
329
341
  const [ns] = query.split('.');
330
342
  if (!this.isNamespaceLoaded(ns)) {
@@ -931,6 +943,7 @@ const LANG_PIPE_CONFIG = new InjectionToken('RE_LANG_PIPE_CONFIG');
931
943
  */
932
944
  class LangPipe {
933
945
  cache = new Map();
946
+ warnedUnresolved = new Set();
934
947
  lang = inject(LangService);
935
948
  injector = inject(Injector);
936
949
  config;
@@ -977,6 +990,9 @@ class LangPipe {
977
990
  return placeholder;
978
991
  }
979
992
  }
993
+ if (ns && this.lang.isNamespaceLoaded(ns) && !this.lang.has(query)) {
994
+ this.warnUnresolvedKey(query);
995
+ }
980
996
  return value;
981
997
  }
982
998
  makeKey(query, params) {
@@ -997,6 +1013,17 @@ class LangPipe {
997
1013
  this.cache.delete(firstKey);
998
1014
  }
999
1015
  }
1016
+ warnUnresolvedKey(query) {
1017
+ if (typeof ngDevMode === 'undefined') {
1018
+ return;
1019
+ }
1020
+ if (this.warnedUnresolved.has(query)) {
1021
+ return;
1022
+ }
1023
+ this.warnedUnresolved.add(query);
1024
+ // eslint-disable-next-line no-console
1025
+ console.warn(`LangPipe: namespace loaded but key "${query}" is unresolved`);
1026
+ }
1000
1027
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: LangPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1001
1028
  static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.1", ngImport: i0, type: LangPipe, isStandalone: true, name: "lang", pure: false });
1002
1029
  }
@@ -1111,7 +1138,7 @@ class ThemeService {
1111
1138
  this.#theme.set(this.themeDefault);
1112
1139
  return;
1113
1140
  }
1114
- const theme = localStorage.getItem('theme') || this.themeDefault;
1141
+ const theme = this.resolveTheme(localStorage.getItem('theme'));
1115
1142
  this.switch(theme);
1116
1143
  });
1117
1144
  effect(() => {
@@ -1129,14 +1156,18 @@ class ThemeService {
1129
1156
  * @param theme — explicit theme value (`'light'` or `'dark'`).
1130
1157
  */
1131
1158
  switch(theme) {
1132
- const newTheme = theme ?? (this.#theme() === themes.light ? themes.dark : themes.light);
1159
+ const requestedTheme = theme ? this.resolveTheme(theme) : undefined;
1160
+ const newTheme = requestedTheme ?? (this.#theme() === themes.light ? themes.dark : themes.light);
1133
1161
  if (this.isBrowser) {
1134
- const html = this.document.querySelector('html');
1162
+ const html = this.document.documentElement;
1135
1163
  newTheme === themes.dark && html.classList.add(this.darkPrefix);
1136
1164
  newTheme === themes.light && html.classList.remove(this.darkPrefix);
1137
1165
  }
1138
1166
  this.#theme.set(newTheme);
1139
1167
  }
1168
+ resolveTheme(theme) {
1169
+ return theme === themes.dark ? themes.dark : themes.light;
1170
+ }
1140
1171
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1141
1172
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: ThemeService, providedIn: 'root' });
1142
1173
  }
@@ -1151,9 +1182,13 @@ function provideReInit(config) {
1151
1182
  return makeEnvironmentProviders([
1152
1183
  { provide: TRANSLATION, deps: [LangService], useFactory: (ls) => ls },
1153
1184
  { provide: SELECTED_LANG, deps: [LangService], useFactory: (ls) => ls[innerLangVal] },
1154
- { provide: CHANGE_LANG, deps: [LangService], useFactory: (ls) => ls.setLang },
1185
+ { provide: CHANGE_LANG, deps: [LangService], useFactory: (ls) => (lang) => ls.setLang(lang) },
1155
1186
  { provide: SELECTED_THEME, deps: [ThemeService], useFactory: (ls) => ls.theme },
1156
- { provide: CHANGE_THEME, deps: [ThemeService], useFactory: (ls) => ls.switch },
1187
+ {
1188
+ provide: CHANGE_THEME,
1189
+ deps: [ThemeService],
1190
+ useFactory: (ls) => (theme) => ls.switch(theme),
1191
+ },
1157
1192
  { provide: CURRENT_DEVICE, deps: [AdaptiveService], useFactory: (ls) => ls.device },
1158
1193
  { provide: DEVICE_BREAKPOINTS, useValue: config.breakpoints || defaultBreakpoints },
1159
1194
  { provide: THEME_CONFIG, useValue: config.theme || defaultThemeConfig },
@@ -1292,10 +1327,11 @@ class SeoService {
1292
1327
  }
1293
1328
  }
1294
1329
  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 });
1330
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoService, providedIn: 'root' });
1296
1331
  }
1297
1332
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoService, decorators: [{
1298
- type: Injectable
1333
+ type: Injectable,
1334
+ args: [{ providedIn: 'root' }]
1299
1335
  }] });
1300
1336
 
1301
1337
  /**
@@ -1321,6 +1357,8 @@ class SeoRouteListener {
1321
1357
  seo = inject(SeoService);
1322
1358
  ar = inject(ActivatedRoute);
1323
1359
  destroyRef = inject(DestroyRef);
1360
+ initialized = false;
1361
+ baseUrl = '';
1324
1362
  /**
1325
1363
  * Initializes the route listener to monitor navigation events and update SEO metadata.
1326
1364
  * Subscribes to router NavigationEnd events and automatically unsubscribes on component destruction.
@@ -1329,10 +1367,11 @@ class SeoRouteListener {
1329
1367
  * Trailing slashes will be removed automatically.
1330
1368
  */
1331
1369
  init(baseUrl) {
1370
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
1332
1371
  const applyRouteSeo = () => {
1333
1372
  const route = this.deepest(this.ar);
1334
1373
  const data = route.snapshot.data;
1335
- const url = data.canonical ?? baseUrl.replace(/\/+$/, '') + this.router.url;
1374
+ const url = data.canonical ?? this.baseUrl + this.router.url;
1336
1375
  data.title && this.seo.setTitle(data.title);
1337
1376
  data.description && this.seo.setDescription(data.description);
1338
1377
  data.twitter && this.seo.setTwitter(data.twitter);
@@ -1347,9 +1386,16 @@ class SeoRouteListener {
1347
1386
  });
1348
1387
  };
1349
1388
  applyRouteSeo();
1350
- this.router.events
1351
- .pipe(filter((e) => e instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
1352
- .subscribe(() => applyRouteSeo());
1389
+ if (this.initialized) {
1390
+ return;
1391
+ }
1392
+ this.initialized = true;
1393
+ const subscription = this.router.events.subscribe((event) => {
1394
+ if (event instanceof NavigationEnd) {
1395
+ applyRouteSeo();
1396
+ }
1397
+ });
1398
+ this.destroyRef.onDestroy(() => subscription.unsubscribe());
1353
1399
  }
1354
1400
  /**
1355
1401
  * Recursively finds the deepest (most nested) activated route in the route tree.
@@ -1423,9 +1469,12 @@ class RouteWatcher {
1423
1469
  this.#fragment() !== snap.fragment && this.#fragment.set(snap.fragment ?? null);
1424
1470
  };
1425
1471
  read();
1426
- this.router.events
1427
- .pipe(filter$1((e) => e instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
1428
- .subscribe(() => read());
1472
+ const subscription = this.router.events.subscribe((event) => {
1473
+ if (event instanceof NavigationEnd) {
1474
+ read();
1475
+ }
1476
+ });
1477
+ this.destroyRef.onDestroy(() => subscription.unsubscribe());
1429
1478
  }
1430
1479
  /** Convenient selector for a data key with type-safe casting */
1431
1480
  selectData(key) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "1.4.4",
2
+ "version": "1.5.0",
3
3
  "name": "@reforgium/presentia",
4
- "description": "reforgium State modules",
4
+ "description": "Angular app infrastructure for localization, theme, adaptive behavior, routes, and SEO",
5
5
  "author": "rtommievich",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -24,8 +24,10 @@
24
24
  },
25
25
  "keywords": [
26
26
  "reforgium",
27
- "state",
28
- "store",
27
+ "infrastructure",
28
+ "i18n",
29
+ "theme",
30
+ "seo",
29
31
  "signal",
30
32
  "angular"
31
33
  ],
@@ -101,6 +101,7 @@ declare class AdaptiveService {
101
101
  private destroyRef;
102
102
  private breakpointObserver;
103
103
  private isBrowser;
104
+ private resizeDebounceId;
104
105
  constructor();
105
106
  static ɵfac: i0.ɵɵFactoryDeclaration<AdaptiveService, never>;
106
107
  static ɵprov: i0.ɵɵInjectableDeclaration<AdaptiveService>;
@@ -347,6 +348,8 @@ declare class LangService {
347
348
  */
348
349
  get(query: LangKey, params?: LangParams): string;
349
350
  get(query: string, params?: LangParams): string;
351
+ has(query: LangKey): boolean;
352
+ has(query: string): boolean;
350
353
  /**
351
354
  * Observes changes to a specified translation key and dynamically computes its value.
352
355
  *
@@ -529,6 +532,7 @@ declare class LangDirective {
529
532
  */
530
533
  declare class LangPipe implements PipeTransform {
531
534
  private readonly cache;
535
+ private readonly warnedUnresolved;
532
536
  private readonly lang;
533
537
  private readonly injector;
534
538
  private readonly config;
@@ -538,6 +542,7 @@ declare class LangPipe implements PipeTransform {
538
542
  transform(query: string | null | undefined, params?: LangParams | null): string;
539
543
  private makeKey;
540
544
  private evictIfNeeded;
545
+ private warnUnresolvedKey;
541
546
  static ɵfac: i0.ɵɵFactoryDeclaration<LangPipe, never>;
542
547
  static ɵpipe: i0.ɵɵPipeDeclaration<LangPipe, "lang", true>;
543
548
  }
@@ -673,6 +678,7 @@ declare class ThemeService {
673
678
  * @param theme — explicit theme value (`'light'` or `'dark'`).
674
679
  */
675
680
  switch(theme?: Themes): void;
681
+ private resolveTheme;
676
682
  static ɵfac: i0.ɵɵFactoryDeclaration<ThemeService, never>;
677
683
  static ɵprov: i0.ɵɵInjectableDeclaration<ThemeService>;
678
684
  }
@@ -802,6 +808,8 @@ declare class SeoRouteListener {
802
808
  private seo;
803
809
  private ar;
804
810
  private destroyRef;
811
+ private initialized;
812
+ private baseUrl;
805
813
  /**
806
814
  * Initializes the route listener to monitor navigation events and update SEO metadata.
807
815
  * Subscribes to router NavigationEnd events and automatically unsubscribes on component destruction.