@reforgium/presentia 1.4.3 → 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.
|
@@ -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
|
-
|
|
133
|
-
.
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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')
|
|
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
|
|
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.
|
|
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
|
-
{
|
|
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
|
|
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.
|
|
1351
|
-
|
|
1352
|
-
|
|
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.
|
|
@@ -1415,16 +1461,20 @@ class RouteWatcher {
|
|
|
1415
1461
|
const read = () => {
|
|
1416
1462
|
const snap = this.deepestSnapshot();
|
|
1417
1463
|
const url = snapshotFullPath(snap);
|
|
1418
|
-
|
|
1464
|
+
const params = snapshotMergedParams(snap);
|
|
1465
|
+
!deepEqual(params, this.#params()) && this.#params.set(params);
|
|
1419
1466
|
!deepEqual(snap.queryParams, this.#query()) && this.#query.set(snap.queryParams);
|
|
1420
1467
|
!deepEqual(snap.data, this.#data()) && this.#data.set(snap.data);
|
|
1421
1468
|
this.#url() !== url && this.#url.set(url);
|
|
1422
1469
|
this.#fragment() !== snap.fragment && this.#fragment.set(snap.fragment ?? null);
|
|
1423
1470
|
};
|
|
1424
1471
|
read();
|
|
1425
|
-
this.router.events
|
|
1426
|
-
|
|
1427
|
-
|
|
1472
|
+
const subscription = this.router.events.subscribe((event) => {
|
|
1473
|
+
if (event instanceof NavigationEnd) {
|
|
1474
|
+
read();
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
1428
1478
|
}
|
|
1429
1479
|
/** Convenient selector for a data key with type-safe casting */
|
|
1430
1480
|
selectData(key) {
|
|
@@ -1456,6 +1506,10 @@ function snapshotFullPath(snap) {
|
|
|
1456
1506
|
.filter(Boolean)
|
|
1457
1507
|
.join('/');
|
|
1458
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 }), {});
|
|
1512
|
+
}
|
|
1459
1513
|
|
|
1460
1514
|
/**
|
|
1461
1515
|
* Generated bundle index. Do not edit.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.5.0",
|
|
3
3
|
"name": "@reforgium/presentia",
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
28
|
-
"
|
|
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.
|