@elasticias/core 0.0.15 → 1.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.
- package/assets/fonts.css +13 -0
- package/fesm2022/elasticias-core.mjs +650 -34
- package/fesm2022/elasticias-core.mjs.map +1 -1
- package/package.json +3 -2
- package/types/elasticias-core.d.ts +336 -20
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, inject, signal, PLATFORM_ID, computed, effect } from '@angular/core';
|
|
2
|
+
import { Injectable, inject, Injector, signal, PLATFORM_ID, computed, effect, InjectionToken, DestroyRef } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject } from 'rxjs';
|
|
4
4
|
import { StorageUtils } from '@elasticias/utils';
|
|
5
|
+
import { TranslateService } from '@ngx-translate/core';
|
|
5
6
|
import { MessageService, ConfirmationService } from 'primeng/api';
|
|
6
|
-
import { Router } from '@angular/router';
|
|
7
|
-
import {
|
|
7
|
+
import { Router, NavigationEnd } from '@angular/router';
|
|
8
|
+
import { Permissions } from '@elasticias/types';
|
|
8
9
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
9
|
-
import { definePreset } from '@primeng/themes';
|
|
10
|
+
import { palette, updatePrimaryPalette, definePreset } from '@primeng/themes';
|
|
10
11
|
import Aura from '@primeng/themes/aura';
|
|
12
|
+
import Lara from '@primeng/themes/lara';
|
|
13
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
14
|
+
import { filter } from 'rxjs/operators';
|
|
11
15
|
|
|
12
16
|
class LoaderService {
|
|
13
17
|
loadingSubject = new BehaviorSubject(false);
|
|
@@ -90,26 +94,148 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
90
94
|
args: [{ providedIn: 'root' }]
|
|
91
95
|
}] });
|
|
92
96
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Comptoir toast service — V2 successor to the legacy `ToastService`.
|
|
99
|
+
*
|
|
100
|
+
* Owns a signal-based queue (`toasts`) consumed by
|
|
101
|
+
* `<ef-toast-region>`, AND forwards every toast to PrimeNG's
|
|
102
|
+
* `MessageService` for back-compat with `<p-toast>` (still used by
|
|
103
|
+
* ClientApp v1). Either renderer picks the toasts up; both work.
|
|
104
|
+
*
|
|
105
|
+
* Default i18n keys (override per-call via `titleKey` / `textKey`):
|
|
106
|
+
* - `ef_toast_info_title`, `ef_toast_info_default`
|
|
107
|
+
* - `ef_toast_success_title`, `ef_toast_success_default`
|
|
108
|
+
* - `ef_toast_warn_title`, `ef_toast_warn_default`
|
|
109
|
+
* - `ef_toast_error_title`, `ef_toast_error_default`
|
|
110
|
+
*
|
|
111
|
+
* Default lifespans: info 5s, success 4s, warn 6s, error 8s.
|
|
112
|
+
*/
|
|
113
|
+
class EfToastService {
|
|
114
|
+
static LIFE_INFO = 5000;
|
|
115
|
+
static LIFE_SUCCESS = 4000;
|
|
116
|
+
static LIFE_WARN = 6000;
|
|
117
|
+
static LIFE_ERROR = 8000;
|
|
118
|
+
/**
|
|
119
|
+
* Lazy holders. Resolving TranslateService eagerly at construction
|
|
120
|
+
* time pulls in HttpClient → HTTP_INTERCEPTORS → AuthorizeInterceptor
|
|
121
|
+
* → AuthorizeService → ToastService → cycle. We defer to the first
|
|
122
|
+
* actual translate / message-publish call.
|
|
123
|
+
*/
|
|
124
|
+
injector = inject(Injector);
|
|
125
|
+
_translate;
|
|
126
|
+
_messageService;
|
|
127
|
+
_messageServiceResolved = false;
|
|
128
|
+
nextId = 1;
|
|
129
|
+
/** Live queue — `<ef-toast-region>` renders this. */
|
|
130
|
+
toasts = signal([], ...(ngDevMode ? [{ debugName: "toasts" }] : /* istanbul ignore next */ []));
|
|
131
|
+
/* ── Convenience methods (back-compat with the legacy
|
|
132
|
+
ToastService signature: `(message?, title?, life?)`). ── */
|
|
133
|
+
showInfo(message, title, life = EfToastService.LIFE_INFO) {
|
|
134
|
+
this.show({
|
|
135
|
+
severity: 'info',
|
|
136
|
+
title: title ?? this.t('ef_toast_info_title'),
|
|
137
|
+
text: message ?? this.t('ef_toast_info_default'),
|
|
138
|
+
life,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
showSuccess(message, title, life = EfToastService.LIFE_SUCCESS) {
|
|
142
|
+
this.show({
|
|
143
|
+
severity: 'success',
|
|
144
|
+
title: title ?? this.t('ef_toast_success_title'),
|
|
145
|
+
text: message ?? this.t('ef_toast_success_default'),
|
|
146
|
+
life,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
showWarn(message, title, life = EfToastService.LIFE_WARN) {
|
|
150
|
+
this.show({
|
|
151
|
+
severity: 'warn',
|
|
152
|
+
title: title ?? this.t('ef_toast_warn_title'),
|
|
153
|
+
text: message ?? this.t('ef_toast_warn_default'),
|
|
154
|
+
life,
|
|
155
|
+
});
|
|
99
156
|
}
|
|
100
|
-
|
|
101
|
-
this.
|
|
157
|
+
showError(message, title, life = EfToastService.LIFE_ERROR) {
|
|
158
|
+
this.show({
|
|
159
|
+
severity: 'error',
|
|
160
|
+
title: title ?? this.t('ef_toast_error_title'),
|
|
161
|
+
text: message ?? this.t('ef_toast_error_default'),
|
|
162
|
+
life,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** Generic show — opts can mix `title`/`titleKey`, `text`/`textKey`. */
|
|
166
|
+
show(opts) {
|
|
167
|
+
const severity = opts.severity ?? 'info';
|
|
168
|
+
const toast = {
|
|
169
|
+
id: this.nextId++,
|
|
170
|
+
severity,
|
|
171
|
+
title: this.resolve(opts.title, opts.titleKey, this.defaultTitleKey(severity)),
|
|
172
|
+
text: this.resolve(opts.text, opts.textKey, undefined),
|
|
173
|
+
life: opts.life ?? this.defaultLife(severity),
|
|
174
|
+
actions: opts.actions,
|
|
175
|
+
};
|
|
176
|
+
this.toasts.update(list => [...list, toast]);
|
|
177
|
+
// Forward to PrimeNG MessageService so v1's <p-toast> still
|
|
178
|
+
// catches the toast. Sticky in PrimeNG is `life: 0`.
|
|
179
|
+
this.messageService?.add({
|
|
180
|
+
severity: toast.severity,
|
|
181
|
+
summary: toast.title,
|
|
182
|
+
detail: toast.text,
|
|
183
|
+
life: toast.life || undefined,
|
|
184
|
+
sticky: toast.life === 0,
|
|
185
|
+
});
|
|
186
|
+
return toast;
|
|
187
|
+
}
|
|
188
|
+
get messageService() {
|
|
189
|
+
if (!this._messageServiceResolved) {
|
|
190
|
+
this._messageServiceResolved = true;
|
|
191
|
+
this._messageService = this.injector.get(MessageService, null, { optional: true });
|
|
192
|
+
}
|
|
193
|
+
return this._messageService ?? null;
|
|
102
194
|
}
|
|
103
|
-
|
|
104
|
-
this.
|
|
195
|
+
dismiss(id) {
|
|
196
|
+
this.toasts.update(list => list.filter(t => t.id !== id));
|
|
197
|
+
}
|
|
198
|
+
clear() {
|
|
199
|
+
this.toasts.set([]);
|
|
200
|
+
this.messageService?.clear();
|
|
201
|
+
}
|
|
202
|
+
/* (messageService getter is defined just below `show` to keep it
|
|
203
|
+
close to where it's consumed.) */
|
|
204
|
+
/* ── Internals ─────────────────────────────────────────────── */
|
|
205
|
+
resolve(literal, key, fallbackKey) {
|
|
206
|
+
if (literal != null)
|
|
207
|
+
return literal;
|
|
208
|
+
if (key)
|
|
209
|
+
return this.t(key);
|
|
210
|
+
if (fallbackKey)
|
|
211
|
+
return this.t(fallbackKey);
|
|
212
|
+
return '';
|
|
213
|
+
}
|
|
214
|
+
t(key) {
|
|
215
|
+
// Lazy-resolve TranslateService — see the field comment above
|
|
216
|
+
// for the AuthorizeService cycle this avoids.
|
|
217
|
+
this._translate ??= this.injector.get(TranslateService);
|
|
218
|
+
const value = this._translate.instant(key);
|
|
219
|
+
// `instant()` returns the key when no translation is loaded;
|
|
220
|
+
// fall through to empty string so untranslated toasts don't
|
|
221
|
+
// surface internal keys to end users.
|
|
222
|
+
return value === key ? '' : value;
|
|
223
|
+
}
|
|
224
|
+
defaultLife(severity) {
|
|
225
|
+
switch (severity) {
|
|
226
|
+
case 'info': return EfToastService.LIFE_INFO;
|
|
227
|
+
case 'success': return EfToastService.LIFE_SUCCESS;
|
|
228
|
+
case 'warn': return EfToastService.LIFE_WARN;
|
|
229
|
+
case 'error': return EfToastService.LIFE_ERROR;
|
|
230
|
+
}
|
|
105
231
|
}
|
|
106
|
-
|
|
107
|
-
|
|
232
|
+
defaultTitleKey(severity) {
|
|
233
|
+
return `ef_toast_${severity}_title`;
|
|
108
234
|
}
|
|
109
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type:
|
|
110
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type:
|
|
235
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
236
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfToastService, providedIn: 'root' });
|
|
111
237
|
}
|
|
112
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type:
|
|
238
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfToastService, decorators: [{
|
|
113
239
|
type: Injectable,
|
|
114
240
|
args: [{ providedIn: 'root' }]
|
|
115
241
|
}] });
|
|
@@ -168,7 +294,7 @@ function screenGuard(config) {
|
|
|
168
294
|
const router = inject(Router);
|
|
169
295
|
const grantsKey = config?.grantsStorageKey ?? 'CURRENT_USER_GRANTS';
|
|
170
296
|
const deniedRedirect = config?.deniedRedirect ?? '/';
|
|
171
|
-
const requiredPermission = config?.requiredPermission ??
|
|
297
|
+
const requiredPermission = config?.requiredPermission ?? Permissions.Read;
|
|
172
298
|
const storageType = config?.storageType ?? 'local';
|
|
173
299
|
const screenCode = getScreenCode(route);
|
|
174
300
|
// If no screenCode found on this route or any parent, allow access
|
|
@@ -243,35 +369,35 @@ class EfThemeConfigService {
|
|
|
243
369
|
platformId = inject(PLATFORM_ID);
|
|
244
370
|
theme = computed(() => (this.appState()?.darkTheme ? 'dark' : 'light'), ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
245
371
|
transitionComplete = signal(false, ...(ngDevMode ? [{ debugName: "transitionComplete" }] : /* istanbul ignore next */ []));
|
|
246
|
-
initialized = false;
|
|
247
372
|
constructor() {
|
|
248
373
|
const initialState = this.loadAppState();
|
|
249
374
|
this.appState.set({ ...initialState });
|
|
375
|
+
// Apply preset class + RTL synchronously so the first paint has
|
|
376
|
+
// the right theme — the effect below picks up subsequent changes
|
|
377
|
+
// (including any subclass `appState.update()` issued before its
|
|
378
|
+
// first run).
|
|
250
379
|
this.updatePresetClass(initialState);
|
|
380
|
+
if (isPlatformBrowser(this.platformId) && initialState?.RTL) {
|
|
381
|
+
this.document.documentElement.setAttribute('dir', 'rtl');
|
|
382
|
+
}
|
|
251
383
|
effect(() => {
|
|
252
384
|
const state = this.appState();
|
|
253
|
-
if (!
|
|
254
|
-
this.initialized = true;
|
|
385
|
+
if (!state)
|
|
255
386
|
return;
|
|
256
|
-
}
|
|
257
387
|
this.saveAppState(state);
|
|
258
388
|
this.updatePresetClass(state);
|
|
259
389
|
this.handleDarkModeTransition(state);
|
|
260
390
|
this.applyRTL(state);
|
|
261
391
|
});
|
|
262
|
-
// Apply RTL on initial load
|
|
263
|
-
if (isPlatformBrowser(this.platformId)) {
|
|
264
|
-
if (initialState?.RTL) {
|
|
265
|
-
this.document.documentElement.setAttribute('dir', 'rtl');
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
392
|
}
|
|
269
393
|
static PRESET_CLASS_MAP = {
|
|
270
394
|
Aura: 'theme-compact',
|
|
271
395
|
Lara: 'theme-modern',
|
|
272
396
|
Material: 'theme-material',
|
|
273
397
|
Nora: 'theme-classic',
|
|
398
|
+
Comptoir: 'theme-comptoir',
|
|
274
399
|
};
|
|
400
|
+
static TENANT_RAMP_STOPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
|
|
275
401
|
static ALL_THEME_CLASSES = Object.values(EfThemeConfigService.PRESET_CLASS_MAP);
|
|
276
402
|
updatePresetClass(state) {
|
|
277
403
|
if (isPlatformBrowser(this.platformId)) {
|
|
@@ -387,6 +513,35 @@ class EfThemeConfigService {
|
|
|
387
513
|
StorageUtils.setLocal(this.STORAGE_KEY, state);
|
|
388
514
|
}
|
|
389
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Applies a tenant's brand color across both PrimeNG's primary palette
|
|
518
|
+
* and the Comptoir `--tenant-*` CSS variables.
|
|
519
|
+
*
|
|
520
|
+
* Generates a 50–950 ramp from the input hex via PrimeNG's `palette()`
|
|
521
|
+
* helper, hands the full ramp to `updatePrimaryPalette()`, and writes
|
|
522
|
+
* stops 50–900 onto `documentElement.style` so the SCSS layer's
|
|
523
|
+
* `var(--tenant-*)` references resolve to the tenant's color.
|
|
524
|
+
*
|
|
525
|
+
* Call this whenever the active tenant changes (e.g., from an effect
|
|
526
|
+
* watching `tenantService.storeConfig().primaryColor`).
|
|
527
|
+
*/
|
|
528
|
+
setTenantAccent(hex) {
|
|
529
|
+
if (!hex)
|
|
530
|
+
return;
|
|
531
|
+
const ramp = palette(hex);
|
|
532
|
+
if (!ramp)
|
|
533
|
+
return;
|
|
534
|
+
updatePrimaryPalette(ramp);
|
|
535
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
536
|
+
const root = this.document.documentElement;
|
|
537
|
+
for (const stop of EfThemeConfigService.TENANT_RAMP_STOPS) {
|
|
538
|
+
const value = ramp[String(stop)];
|
|
539
|
+
if (value) {
|
|
540
|
+
root.style.setProperty(`--tenant-${stop}`, value);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
390
545
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfThemeConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
391
546
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfThemeConfigService, providedIn: 'root' });
|
|
392
547
|
}
|
|
@@ -397,6 +552,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
397
552
|
}]
|
|
398
553
|
}], ctorParameters: () => [] });
|
|
399
554
|
|
|
555
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
556
|
+
* LEGACY: Noir preset (Aura base, monochrome surface palette).
|
|
557
|
+
* Kept for backward-compat with apps that haven't migrated to
|
|
558
|
+
* Comptoir. Phase 5 of the design-system plan retrofits consumers
|
|
559
|
+
* to EfComptoirTheme; once that lands, Noir + EfTheme can be
|
|
560
|
+
* deleted.
|
|
561
|
+
* ────────────────────────────────────────────────────────────── */
|
|
400
562
|
const Noir = definePreset(Aura, {
|
|
401
563
|
semantic: {
|
|
402
564
|
primary: {
|
|
@@ -448,13 +610,140 @@ const Noir = definePreset(Aura, {
|
|
|
448
610
|
* Default Elasticias theme configuration for PrimeNG.
|
|
449
611
|
* Uses the Noir preset (surface-based primary colors) with dark mode support.
|
|
450
612
|
*
|
|
613
|
+
* @deprecated Migrate to {@link EfComptoirTheme} as part of Phase 5 of the
|
|
614
|
+
* design-system plan. Will be removed once all consumers have moved.
|
|
615
|
+
*/
|
|
616
|
+
const EfTheme = {
|
|
617
|
+
preset: Noir,
|
|
618
|
+
options: {
|
|
619
|
+
darkModeSelector: '.p-dark',
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
623
|
+
* COMPTOIR: ink surface + tenant primary (Lara base).
|
|
624
|
+
* Surface palette is the ink ramp from libs/tokens/colors.json.
|
|
625
|
+
* Primary palette defaults to the parfumerie sample tenant; it is
|
|
626
|
+
* runtime-replaced by EfThemeConfigService.setTenantAccent(hex)
|
|
627
|
+
* via PrimeNG's updatePrimaryPalette() API.
|
|
628
|
+
* ────────────────────────────────────────────────────────────── */
|
|
629
|
+
const ComptoirPreset = definePreset(Lara, {
|
|
630
|
+
semantic: {
|
|
631
|
+
primary: {
|
|
632
|
+
50: '#f7f0f4',
|
|
633
|
+
100: '#ecdce5',
|
|
634
|
+
200: '#d8b4c5',
|
|
635
|
+
300: '#b87a99',
|
|
636
|
+
400: '#934e74',
|
|
637
|
+
500: '#6f3257',
|
|
638
|
+
600: '#54243f',
|
|
639
|
+
700: '#401a30',
|
|
640
|
+
800: '#2c1221',
|
|
641
|
+
900: '#1a0913',
|
|
642
|
+
950: '#0d040a'
|
|
643
|
+
},
|
|
644
|
+
/* ──────────────────────────────────────────────────────────────
|
|
645
|
+
* Control sizing (ADR-009). The native "comptoir" variant is the
|
|
646
|
+
* default render path and is pinned to --hit-base (40px) in CSS;
|
|
647
|
+
* these tokens align the OPT-IN PrimeNG variant (p-select filter,
|
|
648
|
+
* p-multiselect, p-inputnumber stepper, etc.) to the same canonical
|
|
649
|
+
* heights so the two paths agree:
|
|
650
|
+
* base → 40px (--hit-base) sm → 32px (--hit) lg → 48px (--hit-touch, POS/mobile)
|
|
651
|
+
* Lara form-field height ≈ paddingY*2 + lineHeight(1.5)*fontSize(14px) + 2px border.
|
|
652
|
+
* base: 8px*2 + 21 + 2 ≈ 40px · sm: 5px*2 + ~18 + 2 ≈ 32px · lg: 12px*2 + 21 + 2 ≈ 48px
|
|
653
|
+
* NOTE: exact pixel height depends on the app's root font-size; the
|
|
654
|
+
* native default path is the verified one — confirm the PrimeNG
|
|
655
|
+
* opt-in controls visually in the running app and nudge paddingY if
|
|
656
|
+
* they read 1-2px off. Border radius matches --r-md (12px). */
|
|
657
|
+
formField: {
|
|
658
|
+
paddingX: '0.75rem',
|
|
659
|
+
paddingY: '0.5rem',
|
|
660
|
+
borderRadius: '12px',
|
|
661
|
+
sm: {
|
|
662
|
+
fontSize: '0.78rem',
|
|
663
|
+
paddingX: '0.625rem',
|
|
664
|
+
paddingY: '0.3125rem'
|
|
665
|
+
},
|
|
666
|
+
lg: {
|
|
667
|
+
fontSize: '0.9375rem',
|
|
668
|
+
paddingX: '0.875rem',
|
|
669
|
+
paddingY: '0.75rem'
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
colorScheme: {
|
|
673
|
+
light: {
|
|
674
|
+
primary: {
|
|
675
|
+
color: '{primary.500}',
|
|
676
|
+
contrastColor: '#ffffff',
|
|
677
|
+
hoverColor: '{primary.600}',
|
|
678
|
+
activeColor: '{primary.700}'
|
|
679
|
+
},
|
|
680
|
+
surface: {
|
|
681
|
+
0: '#ffffff',
|
|
682
|
+
50: '#f8f9fb',
|
|
683
|
+
100: '#f1f3f6',
|
|
684
|
+
200: '#e4e8ee',
|
|
685
|
+
300: '#cdd3dd',
|
|
686
|
+
400: '#9ca5b3',
|
|
687
|
+
500: '#6c7280',
|
|
688
|
+
600: '#4a4f5a',
|
|
689
|
+
700: '#2f3239',
|
|
690
|
+
800: '#1d1f24',
|
|
691
|
+
900: '#0f1115',
|
|
692
|
+
950: '#06070a'
|
|
693
|
+
},
|
|
694
|
+
highlight: {
|
|
695
|
+
background: '{primary.500}',
|
|
696
|
+
focusBackground: '{primary.600}',
|
|
697
|
+
color: '#ffffff',
|
|
698
|
+
focusColor: '#ffffff'
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
dark: {
|
|
702
|
+
primary: {
|
|
703
|
+
color: '{primary.400}',
|
|
704
|
+
contrastColor: '{primary.950}',
|
|
705
|
+
hoverColor: '{primary.300}',
|
|
706
|
+
activeColor: '{primary.200}'
|
|
707
|
+
},
|
|
708
|
+
surface: {
|
|
709
|
+
0: '#000000',
|
|
710
|
+
50: '#06070a',
|
|
711
|
+
100: '#0f1115',
|
|
712
|
+
200: '#1d1f24',
|
|
713
|
+
300: '#2f3239',
|
|
714
|
+
400: '#4a4f5a',
|
|
715
|
+
500: '#6c7280',
|
|
716
|
+
600: '#9ca5b3',
|
|
717
|
+
700: '#cdd3dd',
|
|
718
|
+
800: '#e4e8ee',
|
|
719
|
+
900: '#f1f3f6',
|
|
720
|
+
950: '#f8f9fb'
|
|
721
|
+
},
|
|
722
|
+
highlight: {
|
|
723
|
+
background: '{primary.400}',
|
|
724
|
+
focusBackground: '{primary.300}',
|
|
725
|
+
color: '{primary.950}',
|
|
726
|
+
focusColor: '{primary.950}'
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
/**
|
|
733
|
+
* Comptoir theme configuration for PrimeNG (Phase 1 / design-system v0.2).
|
|
734
|
+
* Surface palette = ink ramp; primary palette = tenant accent
|
|
735
|
+
* (runtime-driven via `EfThemeConfigService.setTenantAccent(hex)`).
|
|
736
|
+
*
|
|
451
737
|
* Usage with providePrimeNG:
|
|
452
738
|
* ```ts
|
|
453
|
-
* providePrimeNG({ theme:
|
|
739
|
+
* providePrimeNG({ theme: EfComptoirTheme, ripple: true })
|
|
454
740
|
* ```
|
|
741
|
+
*
|
|
742
|
+
* Pair with `state.preset = 'Comptoir'` so the body class becomes
|
|
743
|
+
* `theme-comptoir` (avoids legacy `theme-modern` radius overrides).
|
|
455
744
|
*/
|
|
456
|
-
const
|
|
457
|
-
preset:
|
|
745
|
+
const EfComptoirTheme = {
|
|
746
|
+
preset: ComptoirPreset,
|
|
458
747
|
options: {
|
|
459
748
|
darkModeSelector: '.p-dark',
|
|
460
749
|
}
|
|
@@ -847,9 +1136,336 @@ const PRIMENG_AR_LOCALE = {
|
|
|
847
1136
|
},
|
|
848
1137
|
};
|
|
849
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* Default skeleton for the eight ERP modules. Apps either consume this
|
|
1141
|
+
* directly via `EF_MODULES_TOKEN` or extend it with their own `navSections`.
|
|
1142
|
+
*
|
|
1143
|
+
* Phase 1 ships the metadata only — `ef-module-rail` (Phase 2) uses
|
|
1144
|
+
* `id` / `labelKey` / `icon` / `accent` / `defaultRoute`. `navSections`
|
|
1145
|
+
* are populated per-app as each module's screens land in Phase 4-5.
|
|
1146
|
+
*/
|
|
1147
|
+
const EF_MODULES = [
|
|
1148
|
+
{
|
|
1149
|
+
id: 'sales',
|
|
1150
|
+
labelKey: 'modules.sales',
|
|
1151
|
+
icon: 'pi pi-shopping-bag',
|
|
1152
|
+
accent: 'm-sales',
|
|
1153
|
+
defaultRoute: '/operations/sales',
|
|
1154
|
+
navSections: [],
|
|
1155
|
+
group: 'operations',
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
id: 'purchase',
|
|
1159
|
+
labelKey: 'modules.purchase',
|
|
1160
|
+
icon: 'pi pi-truck',
|
|
1161
|
+
accent: 'm-purchase',
|
|
1162
|
+
defaultRoute: '/operations/purchase',
|
|
1163
|
+
navSections: [],
|
|
1164
|
+
group: 'operations',
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
id: 'stock',
|
|
1168
|
+
labelKey: 'modules.stock',
|
|
1169
|
+
icon: 'pi pi-warehouse',
|
|
1170
|
+
accent: 'm-stock',
|
|
1171
|
+
defaultRoute: '/operations/stock',
|
|
1172
|
+
navSections: [],
|
|
1173
|
+
group: 'operations',
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
id: 'pos',
|
|
1177
|
+
labelKey: 'modules.pos',
|
|
1178
|
+
icon: 'pi pi-shop',
|
|
1179
|
+
accent: 'm-pos',
|
|
1180
|
+
defaultRoute: '/pos',
|
|
1181
|
+
navSections: [],
|
|
1182
|
+
group: 'operations',
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
id: 'marketing',
|
|
1186
|
+
labelKey: 'modules.marketing',
|
|
1187
|
+
icon: 'pi pi-megaphone',
|
|
1188
|
+
accent: 'm-marketing',
|
|
1189
|
+
defaultRoute: '/marketing',
|
|
1190
|
+
navSections: [],
|
|
1191
|
+
group: 'commerce',
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
id: 'store',
|
|
1195
|
+
labelKey: 'modules.store',
|
|
1196
|
+
icon: 'pi pi-globe',
|
|
1197
|
+
accent: 'm-store',
|
|
1198
|
+
defaultRoute: '/store',
|
|
1199
|
+
navSections: [],
|
|
1200
|
+
group: 'commerce',
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
id: 'finance',
|
|
1204
|
+
labelKey: 'modules.finance',
|
|
1205
|
+
icon: 'pi pi-chart-line',
|
|
1206
|
+
accent: 'm-finance',
|
|
1207
|
+
defaultRoute: '/finance',
|
|
1208
|
+
navSections: [],
|
|
1209
|
+
group: 'commerce',
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
id: 'admin',
|
|
1213
|
+
labelKey: 'modules.admin',
|
|
1214
|
+
icon: 'pi pi-shield',
|
|
1215
|
+
accent: 'm-admin',
|
|
1216
|
+
defaultRoute: '/admin',
|
|
1217
|
+
navSections: [],
|
|
1218
|
+
group: 'admin',
|
|
1219
|
+
},
|
|
1220
|
+
];
|
|
1221
|
+
/**
|
|
1222
|
+
* DI token that the shell components (`ef-module-rail`, `ef-module-side`)
|
|
1223
|
+
* read from. Apps provide their own definition (typically extending
|
|
1224
|
+
* `EF_MODULES` with populated `navSections`):
|
|
1225
|
+
*
|
|
1226
|
+
* ```ts
|
|
1227
|
+
* providers: [
|
|
1228
|
+
* { provide: EF_MODULES_TOKEN, useValue: APP_MODULES }
|
|
1229
|
+
* ]
|
|
1230
|
+
* ```
|
|
1231
|
+
*/
|
|
1232
|
+
const EF_MODULES_TOKEN = new InjectionToken('EF_MODULES', { providedIn: 'root', factory: () => EF_MODULES });
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Single source of truth for "which ERP module is active right now."
|
|
1236
|
+
*
|
|
1237
|
+
* Watches the Router and matches the current URL against each module's
|
|
1238
|
+
* `defaultRoute`. The longest matching prefix wins, so `/operations/sales`
|
|
1239
|
+
* resolves to `sales` even though `/operations` could in theory match
|
|
1240
|
+
* something shorter.
|
|
1241
|
+
*
|
|
1242
|
+
* Consumers — `ef-module-rail`, `ef-module-side`, `ef-app-main`,
|
|
1243
|
+
* `ef-page-head` — read `activeModule()` and derive their state from it.
|
|
1244
|
+
*
|
|
1245
|
+
* Apps that navigate programmatically without a URL change (rare) can
|
|
1246
|
+
* call `setActiveModule(id)` to override.
|
|
1247
|
+
*/
|
|
1248
|
+
class EfActiveModuleService {
|
|
1249
|
+
router = inject(Router);
|
|
1250
|
+
modules = inject(EF_MODULES_TOKEN);
|
|
1251
|
+
destroyRef = inject(DestroyRef);
|
|
1252
|
+
_activeModule = signal(null, ...(ngDevMode ? [{ debugName: "_activeModule" }] : /* istanbul ignore next */ []));
|
|
1253
|
+
_activeNavItem = signal(null, ...(ngDevMode ? [{ debugName: "_activeNavItem" }] : /* istanbul ignore next */ []));
|
|
1254
|
+
activeModule = this._activeModule.asReadonly();
|
|
1255
|
+
activeModuleId = computed(() => this._activeModule()?.id ?? null, ...(ngDevMode ? [{ debugName: "activeModuleId" }] : /* istanbul ignore next */ []));
|
|
1256
|
+
activeNavItem = this._activeNavItem.asReadonly();
|
|
1257
|
+
constructor() {
|
|
1258
|
+
this.resolveFromUrl(this.router.url);
|
|
1259
|
+
this.router.events
|
|
1260
|
+
.pipe(filter((e) => e instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
|
|
1261
|
+
.subscribe(e => this.resolveFromUrl(e.urlAfterRedirects));
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Force the active module. Most apps don't need this — the router
|
|
1265
|
+
* subscription keeps `activeModule()` in sync automatically.
|
|
1266
|
+
*/
|
|
1267
|
+
setActiveModule(id) {
|
|
1268
|
+
if (id === null) {
|
|
1269
|
+
this._activeModule.set(null);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const match = this.modules.find(m => m.id === id);
|
|
1273
|
+
if (match)
|
|
1274
|
+
this._activeModule.set(match);
|
|
1275
|
+
}
|
|
1276
|
+
resolveFromUrl(url) {
|
|
1277
|
+
const path = url.split('?')[0].split('#')[0];
|
|
1278
|
+
let bestModule = null;
|
|
1279
|
+
let bestNavItem = null;
|
|
1280
|
+
let bestLen = 0;
|
|
1281
|
+
for (const m of this.modules) {
|
|
1282
|
+
for (const route of this.routesFor(m)) {
|
|
1283
|
+
if (path === route || path.startsWith(route + '/')) {
|
|
1284
|
+
if (route.length > bestLen) {
|
|
1285
|
+
bestModule = m;
|
|
1286
|
+
bestNavItem = this.findNavItem(m, route);
|
|
1287
|
+
bestLen = route.length;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
this._activeModule.set(bestModule);
|
|
1293
|
+
this._activeNavItem.set(bestNavItem);
|
|
1294
|
+
}
|
|
1295
|
+
findNavItem(m, route) {
|
|
1296
|
+
for (const section of m.navSections) {
|
|
1297
|
+
for (const item of section.items) {
|
|
1298
|
+
if (item.route === route)
|
|
1299
|
+
return item;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Every URL prefix that should resolve back to this module: the
|
|
1306
|
+
* `defaultRoute` plus every nav item route. Modules whose nav items
|
|
1307
|
+
* span multiple URL prefixes (e.g. sales spread across `/operations/sales`
|
|
1308
|
+
* and `/parameters/sales`) need this to stay active across all of them.
|
|
1309
|
+
*/
|
|
1310
|
+
routesFor(m) {
|
|
1311
|
+
const routes = [m.defaultRoute];
|
|
1312
|
+
for (const section of m.navSections) {
|
|
1313
|
+
for (const item of section.items) {
|
|
1314
|
+
routes.push(item.route);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return routes;
|
|
1318
|
+
}
|
|
1319
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfActiveModuleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1320
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfActiveModuleService, providedIn: 'root' });
|
|
1321
|
+
}
|
|
1322
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfActiveModuleService, decorators: [{
|
|
1323
|
+
type: Injectable,
|
|
1324
|
+
args: [{ providedIn: 'root' }]
|
|
1325
|
+
}], ctorParameters: () => [] });
|
|
1326
|
+
|
|
1327
|
+
const QUERIES = [
|
|
1328
|
+
{ viewport: 'mobile', query: '(max-width: 767px)' },
|
|
1329
|
+
{ viewport: 'tablet', query: '(min-width: 768px) and (max-width: 1279px)' },
|
|
1330
|
+
{ viewport: 'desktop', query: '(min-width: 1280px)' },
|
|
1331
|
+
];
|
|
1332
|
+
/**
|
|
1333
|
+
* Emits the current viewport tier based on `window.matchMedia` breakpoints.
|
|
1334
|
+
*
|
|
1335
|
+
* Breakpoints come from `libs/tokens/targets.json`:
|
|
1336
|
+
* - mobile: ≤ 767px
|
|
1337
|
+
* - tablet: 768 – 1279px
|
|
1338
|
+
* - desktop: ≥ 1280px
|
|
1339
|
+
*
|
|
1340
|
+
* SSR-safe: returns `'desktop'` when `window` is unavailable.
|
|
1341
|
+
*
|
|
1342
|
+
* Usage:
|
|
1343
|
+
* ```ts
|
|
1344
|
+
* private viewport = inject(EfViewportService);
|
|
1345
|
+
*
|
|
1346
|
+
* isMobile = computed(() => this.viewport.current() === 'mobile');
|
|
1347
|
+
* ```
|
|
1348
|
+
*/
|
|
1349
|
+
class EfViewportService {
|
|
1350
|
+
document = inject(DOCUMENT);
|
|
1351
|
+
platformId = inject(PLATFORM_ID);
|
|
1352
|
+
destroyRef = inject(DestroyRef);
|
|
1353
|
+
_current = signal('desktop', ...(ngDevMode ? [{ debugName: "_current" }] : /* istanbul ignore next */ []));
|
|
1354
|
+
current = this._current.asReadonly();
|
|
1355
|
+
isMobile = computed(() => this._current() === 'mobile', ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
|
|
1356
|
+
isTablet = computed(() => this._current() === 'tablet', ...(ngDevMode ? [{ debugName: "isTablet" }] : /* istanbul ignore next */ []));
|
|
1357
|
+
isDesktop = computed(() => this._current() === 'desktop', ...(ngDevMode ? [{ debugName: "isDesktop" }] : /* istanbul ignore next */ []));
|
|
1358
|
+
constructor() {
|
|
1359
|
+
if (!isPlatformBrowser(this.platformId))
|
|
1360
|
+
return;
|
|
1361
|
+
const win = this.document.defaultView;
|
|
1362
|
+
if (!win || typeof win.matchMedia !== 'function')
|
|
1363
|
+
return;
|
|
1364
|
+
const lists = QUERIES.map(({ viewport, query }) => {
|
|
1365
|
+
const mql = win.matchMedia(query);
|
|
1366
|
+
const handler = (e) => {
|
|
1367
|
+
if (e.matches)
|
|
1368
|
+
this._current.set(viewport);
|
|
1369
|
+
};
|
|
1370
|
+
handler(mql);
|
|
1371
|
+
mql.addEventListener('change', handler);
|
|
1372
|
+
return { mql, handler };
|
|
1373
|
+
});
|
|
1374
|
+
this.destroyRef.onDestroy(() => {
|
|
1375
|
+
for (const { mql, handler } of lists) {
|
|
1376
|
+
mql.removeEventListener('change', handler);
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfViewportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1381
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfViewportService, providedIn: 'root' });
|
|
1382
|
+
}
|
|
1383
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfViewportService, decorators: [{
|
|
1384
|
+
type: Injectable,
|
|
1385
|
+
args: [{ providedIn: 'root' }]
|
|
1386
|
+
}], ctorParameters: () => [] });
|
|
1387
|
+
|
|
1388
|
+
const LEVEL_ORDER = {
|
|
1389
|
+
none: 0,
|
|
1390
|
+
read: 1,
|
|
1391
|
+
write: 2,
|
|
1392
|
+
admin: 3,
|
|
1393
|
+
};
|
|
1394
|
+
const ACTION_REQUIRED = {
|
|
1395
|
+
read: 'read',
|
|
1396
|
+
write: 'write',
|
|
1397
|
+
admin: 'admin',
|
|
1398
|
+
};
|
|
1399
|
+
/**
|
|
1400
|
+
* Module-scoped permission service.
|
|
1401
|
+
*
|
|
1402
|
+
* Apps populate it from their auth bootstrap once the user's profile is
|
|
1403
|
+
* loaded — typically via `setPermissions()` or by providing a custom
|
|
1404
|
+
* source signal:
|
|
1405
|
+
*
|
|
1406
|
+
* ```ts
|
|
1407
|
+
* // bootstrap.ts
|
|
1408
|
+
* const perms = inject(EfPermissionService);
|
|
1409
|
+
* perms.setPermissions(profile.permissions);
|
|
1410
|
+
* ```
|
|
1411
|
+
*
|
|
1412
|
+
* The service is the single source of truth for shell components
|
|
1413
|
+
* (`ef-module-rail`, `ef-module-side`, `*efCan` directive) and routing
|
|
1414
|
+
* defaults. Once the screens/menus → Mongo migration lands, the
|
|
1415
|
+
* permissions list will be served denormalized on the user/profile
|
|
1416
|
+
* document and consumed here without a join.
|
|
1417
|
+
*/
|
|
1418
|
+
class EfPermissionService {
|
|
1419
|
+
modules = inject(EF_MODULES_TOKEN);
|
|
1420
|
+
_permissions = signal([], ...(ngDevMode ? [{ debugName: "_permissions" }] : /* istanbul ignore next */ []));
|
|
1421
|
+
permissions = this._permissions.asReadonly();
|
|
1422
|
+
/**
|
|
1423
|
+
* Replace the current permission set. Pass `[]` to clear (e.g., on logout).
|
|
1424
|
+
*/
|
|
1425
|
+
setPermissions(perms) {
|
|
1426
|
+
this._permissions.set(perms);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* The level granted to the current user for a given module.
|
|
1430
|
+
* Returns `'none'` if the module is not in the permission set.
|
|
1431
|
+
*/
|
|
1432
|
+
level(module) {
|
|
1433
|
+
return this._permissions().find(p => p.module === module)?.level ?? 'none';
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Whether the current user can perform `action` on `module`.
|
|
1437
|
+
* Levels are hierarchical: `admin` > `write` > `read` > `none`.
|
|
1438
|
+
*/
|
|
1439
|
+
can(module, action = 'read') {
|
|
1440
|
+
return LEVEL_ORDER[this.level(module)] >= LEVEL_ORDER[ACTION_REQUIRED[action]];
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* The list of modules the user can read, in registry order.
|
|
1444
|
+
* Used by `ef-module-rail` to decide which icons render.
|
|
1445
|
+
*/
|
|
1446
|
+
visibleModules = computed(() => {
|
|
1447
|
+
const perms = this._permissions();
|
|
1448
|
+
const granted = new Set(perms.filter(p => p.level !== 'none').map(p => p.module));
|
|
1449
|
+
return this.modules.filter(m => granted.has(m.id));
|
|
1450
|
+
}, ...(ngDevMode ? [{ debugName: "visibleModules" }] : /* istanbul ignore next */ []));
|
|
1451
|
+
/**
|
|
1452
|
+
* The first visible module — the default landing module after login.
|
|
1453
|
+
* Returns `null` when the user has no modules.
|
|
1454
|
+
*/
|
|
1455
|
+
defaultModule = computed(() => {
|
|
1456
|
+
return this.visibleModules()[0] ?? null;
|
|
1457
|
+
}, ...(ngDevMode ? [{ debugName: "defaultModule" }] : /* istanbul ignore next */ []));
|
|
1458
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfPermissionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1459
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfPermissionService, providedIn: 'root' });
|
|
1460
|
+
}
|
|
1461
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfPermissionService, decorators: [{
|
|
1462
|
+
type: Injectable,
|
|
1463
|
+
args: [{ providedIn: 'root' }]
|
|
1464
|
+
}] });
|
|
1465
|
+
|
|
850
1466
|
/**
|
|
851
1467
|
* Generated bundle index. Do not edit.
|
|
852
1468
|
*/
|
|
853
1469
|
|
|
854
|
-
export { CacheService, ConfirmDialogService, DEFAULT_APP_STATE, EfTheme, EfThemeConfigService, LoaderService, PRIMENG_AR_LOCALE, PRIMENG_EN_LOCALE, PRIMENG_FR_LOCALE, ToastService, hasScreenPermission, screenGuard };
|
|
1470
|
+
export { CacheService, ConfirmDialogService, DEFAULT_APP_STATE, EF_MODULES, EF_MODULES_TOKEN, EfActiveModuleService, EfComptoirTheme, EfPermissionService, EfTheme, EfThemeConfigService, EfToastService, EfViewportService, LoaderService, PRIMENG_AR_LOCALE, PRIMENG_EN_LOCALE, PRIMENG_FR_LOCALE, EfToastService as ToastService, hasScreenPermission, screenGuard };
|
|
855
1471
|
//# sourceMappingURL=elasticias-core.mjs.map
|