@nucleus-suite-pe/core 0.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.
@@ -0,0 +1,1111 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, inject, Injector, signal, computed, Injectable, effect, input, ChangeDetectionStrategy, Component, output, untracked } from '@angular/core';
3
+ import { HttpContextToken, HttpClient, HttpContext, HttpErrorResponse } from '@angular/common/http';
4
+ import { firstValueFrom, catchError, throwError, from, switchMap } from 'rxjs';
5
+ import { definePreset } from '@primeuix/themes';
6
+ import Lara from '@primeuix/themes/lara';
7
+ import { providePrimeNG } from 'primeng/config';
8
+ import * as i7 from '@angular/forms';
9
+ import { FormsModule } from '@angular/forms';
10
+ import { Router } from '@angular/router';
11
+ import * as i1 from 'primeng/avatar';
12
+ import { AvatarModule } from 'primeng/avatar';
13
+ import * as i2 from 'primeng/button';
14
+ import { ButtonModule } from 'primeng/button';
15
+ import * as i3 from 'primeng/drawer';
16
+ import { DrawerModule } from 'primeng/drawer';
17
+ import * as i4 from 'primeng/listbox';
18
+ import { ListboxModule } from 'primeng/listbox';
19
+ import * as i5 from 'primeng/popover';
20
+ import { PopoverModule } from 'primeng/popover';
21
+ import * as i6 from 'primeng/tooltip';
22
+ import { TooltipModule } from 'primeng/tooltip';
23
+
24
+ const NUCLEUS_CONFIG = new InjectionToken('NUCLEUS_CONFIG');
25
+ const CONFIG_URL = 'config.json';
26
+ function provideNucleus(config) {
27
+ return makeEnvironmentProviders([{ provide: NUCLEUS_CONFIG, useValue: normalize(config) }]);
28
+ }
29
+ async function loadNucleusConfig(url = CONFIG_URL) {
30
+ const response = await fetch(url, { cache: 'no-store' });
31
+ if (!response.ok) {
32
+ throw new Error(`No se pudo cargar ${url}: HTTP ${response.status}`);
33
+ }
34
+ return normalize((await response.json()));
35
+ }
36
+ function isNucleusConfigured(config) {
37
+ return config.securityApiUrl.length > 0 && config.appId.length > 0;
38
+ }
39
+ function ownsUrl(config, url) {
40
+ if (config.securityApiUrl.length > 0 && url.startsWith(config.securityApiUrl)) {
41
+ return true;
42
+ }
43
+ return config.appApiUrl !== null && config.appApiUrl.length > 0 && url.startsWith(config.appApiUrl);
44
+ }
45
+ function normalize(config) {
46
+ return {
47
+ appId: (config.appId ?? '').trim().toLowerCase(),
48
+ securityApiUrl: trimSlash(config.securityApiUrl),
49
+ appApiUrl: config.appApiUrl ? trimSlash(config.appApiUrl) : null,
50
+ landingUrl: config.landingUrl ? trimSlash(config.landingUrl) : null,
51
+ };
52
+ }
53
+ function trimSlash(value) {
54
+ return (value ?? '').trim().replace(/\/+$/, '');
55
+ }
56
+
57
+ class NucleusAuth {
58
+ }
59
+
60
+ const NUCLEUS_FORCE_BEARER = new HttpContextToken(() => false);
61
+ const NUCLEUS_SKIP_AUTH = new HttpContextToken(() => false);
62
+
63
+ const SESSION_PATH = '/auth/session';
64
+ const LOGOUT_PATH = '/auth/logout';
65
+ class NucleusSessionAuth {
66
+ http = inject(HttpClient);
67
+ config = inject(NUCLEUS_CONFIG);
68
+ injector = inject(Injector);
69
+ _ticket = signal(null, ...(ngDevMode ? [{ debugName: "_ticket" }] : /* istanbul ignore next */ []));
70
+ _failed = signal(false, ...(ngDevMode ? [{ debugName: "_failed" }] : /* istanbul ignore next */ []));
71
+ pending = null;
72
+ ticket = this._ticket.asReadonly();
73
+ failed = this._failed.asReadonly();
74
+ established = computed(() => this._ticket() !== null, ...(ngDevMode ? [{ debugName: "established" }] : /* istanbul ignore next */ []));
75
+ establish(force = false) {
76
+ if (!force && this._ticket() !== null)
77
+ return Promise.resolve(true);
78
+ this.pending ??= this.request().finally(() => {
79
+ this.pending = null;
80
+ });
81
+ return this.pending;
82
+ }
83
+ async clear() {
84
+ this._ticket.set(null);
85
+ this._failed.set(false);
86
+ if (!isNucleusConfigured(this.config))
87
+ return;
88
+ try {
89
+ await firstValueFrom(this.http.post(`${this.config.securityApiUrl}${LOGOUT_PATH}`, {}, {
90
+ withCredentials: true,
91
+ context: new HttpContext().set(NUCLEUS_SKIP_AUTH, true),
92
+ }));
93
+ }
94
+ catch (error) {
95
+ console.error('[auth] no se pudo cerrar la sesión de cookie', error);
96
+ }
97
+ }
98
+ async request() {
99
+ if (!isNucleusConfigured(this.config))
100
+ return false;
101
+ if (this.injector.get(NucleusAuth, null) === null)
102
+ return false;
103
+ try {
104
+ const ticket = await firstValueFrom(this.http.post(`${this.config.securityApiUrl}${SESSION_PATH}`, {}, {
105
+ withCredentials: true,
106
+ context: new HttpContext().set(NUCLEUS_FORCE_BEARER, true),
107
+ }));
108
+ this._ticket.set(ticket);
109
+ this._failed.set(false);
110
+ return true;
111
+ }
112
+ catch (error) {
113
+ this._ticket.set(null);
114
+ this._failed.set(true);
115
+ console.error('[auth] no se pudo abrir la sesión de cookie', error);
116
+ return false;
117
+ }
118
+ }
119
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusSessionAuth, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
120
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusSessionAuth, providedIn: 'root' });
121
+ }
122
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusSessionAuth, decorators: [{
123
+ type: Injectable,
124
+ args: [{ providedIn: 'root' }]
125
+ }] });
126
+
127
+ const nucleusAuthInterceptor = (req, next) => {
128
+ const config = inject(NUCLEUS_CONFIG);
129
+ if (!ownsUrl(config, req.url) || req.context.get(NUCLEUS_SKIP_AUTH)) {
130
+ return next(req);
131
+ }
132
+ const session = inject(NucleusSessionAuth);
133
+ const auth = inject(NucleusAuth, { optional: true });
134
+ const request = req.clone({ withCredentials: true });
135
+ if (!req.context.get(NUCLEUS_FORCE_BEARER) && session.established()) {
136
+ return next(request).pipe(catchError((error) => {
137
+ if (!(error instanceof HttpErrorResponse) || error.status !== 401) {
138
+ return throwError(() => error);
139
+ }
140
+ return from(session.establish(true)).pipe(switchMap((renewed) => (renewed ? next(request) : throwError(() => error))));
141
+ }));
142
+ }
143
+ if (!auth) {
144
+ return next(request);
145
+ }
146
+ return from(auth.idToken()).pipe(switchMap((token) => next(token ? request.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : request)));
147
+ };
148
+
149
+ const AUTH_SYNC_PATH = '/auth/sync';
150
+ class UserSyncService {
151
+ http = inject(HttpClient);
152
+ config = inject(NUCLEUS_CONFIG);
153
+ appUser = signal(null, ...(ngDevMode ? [{ debugName: "appUser" }] : /* istanbul ignore next */ []));
154
+ failed = signal(false, ...(ngDevMode ? [{ debugName: "failed" }] : /* istanbul ignore next */ []));
155
+ async sync(session, idToken) {
156
+ if (!isNucleusConfigured(this.config)) {
157
+ console.info('[auth] sincronización omitida: securityApiUrl o appId vacíos en config.json');
158
+ return;
159
+ }
160
+ const token = await idToken();
161
+ if (!token) {
162
+ this.failed.set(true);
163
+ console.error('[auth] no hay ID token con el que autenticar la sincronización');
164
+ return;
165
+ }
166
+ const body = {
167
+ provider: session.provider,
168
+ providerUserId: session.providerUserId,
169
+ email: session.email,
170
+ displayName: session.displayName,
171
+ photoUrl: session.photoUrl,
172
+ };
173
+ try {
174
+ const user = await firstValueFrom(this.http.post(`${this.config.securityApiUrl}${AUTH_SYNC_PATH}`, body));
175
+ this.appUser.set(user);
176
+ this.failed.set(false);
177
+ }
178
+ catch (error) {
179
+ this.failed.set(true);
180
+ console.error('[auth] no se pudo sincronizar el usuario con el backend', error);
181
+ }
182
+ }
183
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: UserSyncService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
184
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: UserSyncService, providedIn: 'root' });
185
+ }
186
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: UserSyncService, decorators: [{
187
+ type: Injectable,
188
+ args: [{ providedIn: 'root' }]
189
+ }] });
190
+
191
+ class SessionService {
192
+ http = inject(HttpClient);
193
+ auth = inject(NucleusAuth, { optional: true });
194
+ userSync = inject(UserSyncService);
195
+ sessionAuth = inject(NucleusSessionAuth);
196
+ config = inject(NUCLEUS_CONFIG);
197
+ _me = signal(null, ...(ngDevMode ? [{ debugName: "_me" }] : /* istanbul ignore next */ []));
198
+ _menu = signal([], ...(ngDevMode ? [{ debugName: "_menu" }] : /* istanbul ignore next */ []));
199
+ _access = signal(null, ...(ngDevMode ? [{ debugName: "_access" }] : /* istanbul ignore next */ []));
200
+ _scope = signal(null, ...(ngDevMode ? [{ debugName: "_scope" }] : /* istanbul ignore next */ []));
201
+ _apps = signal([], ...(ngDevMode ? [{ debugName: "_apps" }] : /* istanbul ignore next */ []));
202
+ _loading = signal(false, ...(ngDevMode ? [{ debugName: "_loading" }] : /* istanbul ignore next */ []));
203
+ _error = signal(null, ...(ngDevMode ? [{ debugName: "_error" }] : /* istanbul ignore next */ []));
204
+ inFlight = false;
205
+ loadedKey = null;
206
+ me = this._me.asReadonly();
207
+ menu = this._menu.asReadonly();
208
+ access = this._access.asReadonly();
209
+ scope = this._scope.asReadonly();
210
+ apps = this._apps.asReadonly();
211
+ loading = this._loading.asReadonly();
212
+ error = this._error.asReadonly();
213
+ roleName = computed(() => this._access()?.roleName ?? null, ...(ngDevMode ? [{ debugName: "roleName" }] : /* istanbul ignore next */ []));
214
+ siteName = computed(() => this._access()?.siteName ?? null, ...(ngDevMode ? [{ debugName: "siteName" }] : /* istanbul ignore next */ []));
215
+ companies = computed(() => {
216
+ const me = this._me();
217
+ if (!me)
218
+ return [];
219
+ return [...me.ownedCompanies, ...me.sharedCompanies];
220
+ }, ...(ngDevMode ? [{ debugName: "companies" }] : /* istanbul ignore next */ []));
221
+ async load(language, force = false) {
222
+ if (!isNucleusConfigured(this.config))
223
+ return;
224
+ const identity = this.auth?.user() ?? null;
225
+ if (this.auth && !identity)
226
+ return;
227
+ const key = `${identity?.uid ?? 'cookie'}|${language}`;
228
+ if (this.inFlight)
229
+ return;
230
+ if (!force && this.loadedKey === key)
231
+ return;
232
+ this.inFlight = true;
233
+ this.loadedKey = key;
234
+ this._loading.set(true);
235
+ this._error.set(null);
236
+ try {
237
+ if (identity && this.auth) {
238
+ await this.userSync.sync(identity, () => this.auth.idToken());
239
+ await this.sessionAuth.establish(force);
240
+ }
241
+ const [me, catalog] = await Promise.all([
242
+ firstValueFrom(this.http.get(`${this.config.securityApiUrl}/me`)),
243
+ firstValueFrom(this.http.get(`${this.config.securityApiUrl}/apps`)),
244
+ ]);
245
+ this._me.set(me);
246
+ this._apps.set(this.buildLauncher(me, catalog));
247
+ const access = [...me.ownedCompanies, ...me.sharedCompanies]
248
+ .flatMap((company) => company.myAccess)
249
+ .find((entry) => entry.appId.toLowerCase() === this.config.appId);
250
+ if (access) {
251
+ this._access.set(access);
252
+ this._scope.set('site');
253
+ this._menu.set(await this.siteMenu(access.siteId, language));
254
+ return;
255
+ }
256
+ this._access.set(null);
257
+ this._scope.set('account');
258
+ this._menu.set(await this.accountMenu(language));
259
+ }
260
+ catch (error) {
261
+ this._menu.set([]);
262
+ this._error.set(describe(error));
263
+ console.error('[session] no se pudo cargar la sesión del backend', error);
264
+ }
265
+ finally {
266
+ this.inFlight = false;
267
+ this._loading.set(false);
268
+ }
269
+ }
270
+ reload(language) {
271
+ return this.load(language, true);
272
+ }
273
+ buildLauncher(me, catalog) {
274
+ const granted = new Set([...me.ownedCompanies, ...me.sharedCompanies]
275
+ .flatMap((company) => company.myAccess)
276
+ .map((entry) => entry.appId.toLowerCase()));
277
+ return catalog
278
+ .filter((app) => app.id.toLowerCase() !== this.config.appId)
279
+ .map((app) => ({ ...app, hasAccess: granted.has(app.id.toLowerCase()) }))
280
+ .sort((a, b) => a.sortOrder - b.sortOrder);
281
+ }
282
+ siteMenu(siteId, language) {
283
+ return firstValueFrom(this.http.get(`${this.config.securityApiUrl}/sites/${siteId}/apps/${this.config.appId}/menu`, { params: { language } }));
284
+ }
285
+ accountMenu(language) {
286
+ return firstValueFrom(this.http.get(`${this.config.securityApiUrl}/me/menu`, {
287
+ params: { appId: this.config.appId, language },
288
+ }));
289
+ }
290
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
291
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SessionService, providedIn: 'root' });
292
+ }
293
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SessionService, decorators: [{
294
+ type: Injectable,
295
+ args: [{ providedIn: 'root' }]
296
+ }] });
297
+ function describe(error) {
298
+ if (error instanceof HttpErrorResponse) {
299
+ const problem = error.error;
300
+ return problem?.detail ?? problem?.title ?? `HTTP ${error.status}`;
301
+ }
302
+ return 'Error desconocido';
303
+ }
304
+
305
+ const NUCLEUS_LANGS = ['es', 'en'];
306
+ const STORAGE_KEY$1 = 'nucleus_lang';
307
+ function isLang(value) {
308
+ return value === 'es' || value === 'en';
309
+ }
310
+ function readStored() {
311
+ try {
312
+ const stored = localStorage.getItem(STORAGE_KEY$1);
313
+ if (isLang(stored))
314
+ return stored;
315
+ }
316
+ catch { }
317
+ return 'es';
318
+ }
319
+ class NucleusLanguage {
320
+ _lang = signal(readStored(), ...(ngDevMode ? [{ debugName: "_lang" }] : /* istanbul ignore next */ []));
321
+ lang = this._lang.asReadonly();
322
+ constructor() {
323
+ effect(() => {
324
+ const lang = this._lang();
325
+ document.documentElement.lang = lang;
326
+ try {
327
+ localStorage.setItem(STORAGE_KEY$1, lang);
328
+ }
329
+ catch { }
330
+ });
331
+ }
332
+ setLang(lang) {
333
+ this._lang.set(lang);
334
+ }
335
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusLanguage, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
336
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusLanguage, providedIn: 'root' });
337
+ }
338
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusLanguage, decorators: [{
339
+ type: Injectable,
340
+ args: [{ providedIn: 'root' }]
341
+ }], ctorParameters: () => [] });
342
+
343
+ const THEMES = [
344
+ {
345
+ id: 'nucleus',
346
+ scheme: 'dark',
347
+ swatch: '#7c6cf5',
348
+ starfield: true,
349
+ tokens: {
350
+ bg: '#050508',
351
+ surface: '#0d0d15',
352
+ surfaceAlt: '#16161f',
353
+ text: '#ffffff',
354
+ textMuted: 'rgba(255, 255, 255, 0.6)',
355
+ border: 'rgba(255, 255, 255, 0.14)',
356
+ accent: '#7c6cf5',
357
+ onAccent: '#ffffff',
358
+ },
359
+ },
360
+ {
361
+ id: 'light',
362
+ scheme: 'light',
363
+ swatch: '#f5f5f5',
364
+ starfield: false,
365
+ tokens: {
366
+ bg: '#ffffff',
367
+ surface: '#f5f5f5',
368
+ surfaceAlt: '#eeeeee',
369
+ text: '#1e1e1e',
370
+ textMuted: '#5a5a5a',
371
+ border: '#cccedb',
372
+ accent: '#0078d4',
373
+ onAccent: '#ffffff',
374
+ },
375
+ },
376
+ {
377
+ id: 'dark',
378
+ scheme: 'dark',
379
+ swatch: '#1e1e1e',
380
+ starfield: false,
381
+ tokens: {
382
+ bg: '#1e1e1e',
383
+ surface: '#252526',
384
+ surfaceAlt: '#2d2d30',
385
+ text: '#f1f1f1',
386
+ textMuted: '#a0a0a0',
387
+ border: '#3f3f46',
388
+ accent: '#0097fb',
389
+ onAccent: '#ffffff',
390
+ },
391
+ },
392
+ {
393
+ id: 'bubblegum',
394
+ scheme: 'light',
395
+ swatch: '#f7b8d4',
396
+ starfield: false,
397
+ tokens: {
398
+ bg: '#fff0f6',
399
+ surface: '#ffe0ed',
400
+ surfaceAlt: '#ffd0e4',
401
+ text: '#3d1029',
402
+ textMuted: '#8a4c6c',
403
+ border: '#f0aac9',
404
+ accent: '#d6337f',
405
+ onAccent: '#ffffff',
406
+ },
407
+ },
408
+ {
409
+ id: 'cool-breeze',
410
+ scheme: 'light',
411
+ swatch: '#a8d5e8',
412
+ starfield: false,
413
+ tokens: {
414
+ bg: '#eef7fb',
415
+ surface: '#dcecf5',
416
+ surfaceAlt: '#cbe2ef',
417
+ text: '#102a38',
418
+ textMuted: '#4a6b7d',
419
+ border: '#a5c9db',
420
+ accent: '#0f7ba8',
421
+ onAccent: '#ffffff',
422
+ },
423
+ },
424
+ {
425
+ id: 'cool-slate',
426
+ scheme: 'dark',
427
+ swatch: '#3b4a5c',
428
+ starfield: false,
429
+ tokens: {
430
+ bg: '#232b36',
431
+ surface: '#2c3644',
432
+ surfaceAlt: '#354152',
433
+ text: '#e6ecf4',
434
+ textMuted: '#9fb0c4',
435
+ border: '#44546a',
436
+ accent: '#5fa8e8',
437
+ onAccent: '#0b1118',
438
+ },
439
+ },
440
+ {
441
+ id: 'icy-mint',
442
+ scheme: 'light',
443
+ swatch: '#c4ece5',
444
+ starfield: false,
445
+ tokens: {
446
+ bg: '#f0fbf8',
447
+ surface: '#dcf3ee',
448
+ surfaceAlt: '#c9eae3',
449
+ text: '#0d2f2a',
450
+ textMuted: '#4a706a',
451
+ border: '#a3d6cd',
452
+ accent: '#0d8f7d',
453
+ onAccent: '#ffffff',
454
+ },
455
+ },
456
+ {
457
+ id: 'juicy-plum',
458
+ scheme: 'dark',
459
+ swatch: '#6b3a7a',
460
+ starfield: false,
461
+ tokens: {
462
+ bg: '#2a1633',
463
+ surface: '#372042',
464
+ surfaceAlt: '#452a52',
465
+ text: '#f3e8f7',
466
+ textMuted: '#b795c4',
467
+ border: '#57366a',
468
+ accent: '#c86dd7',
469
+ onAccent: '#1a0d20',
470
+ },
471
+ },
472
+ {
473
+ id: 'mango-paradise',
474
+ scheme: 'light',
475
+ swatch: '#f7c48a',
476
+ starfield: false,
477
+ tokens: {
478
+ bg: '#fff6ec',
479
+ surface: '#ffe9d4',
480
+ surfaceAlt: '#ffdcbc',
481
+ text: '#3d2410',
482
+ textMuted: '#8a5f38',
483
+ border: '#f0c096',
484
+ accent: '#e07314',
485
+ onAccent: '#ffffff',
486
+ },
487
+ },
488
+ {
489
+ id: 'moonlight-glow',
490
+ scheme: 'dark',
491
+ swatch: '#1f3a5f',
492
+ starfield: false,
493
+ tokens: {
494
+ bg: '#131f33',
495
+ surface: '#1c2b45',
496
+ surfaceAlt: '#243657',
497
+ text: '#e3ecfa',
498
+ textMuted: '#94a9c9',
499
+ border: '#33496e',
500
+ accent: '#6ea8f0',
501
+ onAccent: '#0a1020',
502
+ },
503
+ },
504
+ {
505
+ id: 'mystical-forest',
506
+ scheme: 'dark',
507
+ swatch: '#1f4a2e',
508
+ starfield: false,
509
+ tokens: {
510
+ bg: '#132419',
511
+ surface: '#1b3223',
512
+ surfaceAlt: '#24422e',
513
+ text: '#e4f2e8',
514
+ textMuted: '#93b7a0',
515
+ border: '#325a41',
516
+ accent: '#4caf70',
517
+ onAccent: '#0a1610',
518
+ },
519
+ },
520
+ {
521
+ id: 'silky-pink',
522
+ scheme: 'light',
523
+ swatch: '#f3c2d6',
524
+ starfield: false,
525
+ tokens: {
526
+ bg: '#fdf2f7',
527
+ surface: '#fae3ed',
528
+ surfaceAlt: '#f6d3e2',
529
+ text: '#3a1626',
530
+ textMuted: '#87566a',
531
+ border: '#eeb4cb',
532
+ accent: '#c14b82',
533
+ onAccent: '#ffffff',
534
+ },
535
+ },
536
+ {
537
+ id: 'spicy-red',
538
+ scheme: 'dark',
539
+ swatch: '#8f1f24',
540
+ starfield: false,
541
+ tokens: {
542
+ bg: '#2a1113',
543
+ surface: '#3a181b',
544
+ surfaceAlt: '#4a1f23',
545
+ text: '#f8e6e7',
546
+ textMuted: '#c79398',
547
+ border: '#5f2a2f',
548
+ accent: '#e8555e',
549
+ onAccent: '#1a0809',
550
+ },
551
+ },
552
+ {
553
+ id: 'sunny-day',
554
+ scheme: 'light',
555
+ swatch: '#efe4b0',
556
+ starfield: false,
557
+ tokens: {
558
+ bg: '#fdf9e8',
559
+ surface: '#f7efd2',
560
+ surfaceAlt: '#f0e5bd',
561
+ text: '#332d10',
562
+ textMuted: '#736a38',
563
+ border: '#ddcf94',
564
+ accent: '#a8860b',
565
+ onAccent: '#ffffff',
566
+ },
567
+ },
568
+ ];
569
+ const THEME_LABELS = {
570
+ nucleus: 'Nucleus',
571
+ light: 'Light',
572
+ dark: 'Dark',
573
+ bubblegum: 'Bubblegum',
574
+ 'cool-breeze': 'Cool Breeze',
575
+ 'cool-slate': 'Cool Slate',
576
+ 'icy-mint': 'Icy Mint',
577
+ 'juicy-plum': 'Juicy Plum',
578
+ 'mango-paradise': 'Mango Paradise',
579
+ 'moonlight-glow': 'Moonlight Glow',
580
+ 'mystical-forest': 'Mystical Forest',
581
+ 'silky-pink': 'Silky Pink',
582
+ 'spicy-red': 'Spicy Red',
583
+ 'sunny-day': 'Sunny Day',
584
+ };
585
+ const DEFAULT_THEME = 'nucleus';
586
+ const BY_ID = new Map(THEMES.map((theme) => [theme.id, theme]));
587
+ function themeById(id) {
588
+ return BY_ID.get(id) ?? BY_ID.get(DEFAULT_THEME);
589
+ }
590
+ function isThemePreference(value) {
591
+ return value === 'system' || (typeof value === 'string' && BY_ID.has(value));
592
+ }
593
+
594
+ const STORAGE_KEY = 'nucleus_theme';
595
+ function readStoredPreference() {
596
+ try {
597
+ const stored = localStorage.getItem(STORAGE_KEY);
598
+ if (isThemePreference(stored))
599
+ return stored;
600
+ }
601
+ catch { }
602
+ return DEFAULT_THEME;
603
+ }
604
+ function prefersDark() {
605
+ return globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
606
+ }
607
+ class ThemeService {
608
+ _preference = signal(readStoredPreference(), ...(ngDevMode ? [{ debugName: "_preference" }] : /* istanbul ignore next */ []));
609
+ systemDark = signal(prefersDark(), ...(ngDevMode ? [{ debugName: "systemDark" }] : /* istanbul ignore next */ []));
610
+ preference = this._preference.asReadonly();
611
+ theme = computed(() => {
612
+ const preference = this._preference();
613
+ if (preference === 'system')
614
+ return themeById(this.systemDark() ? 'dark' : 'light');
615
+ return themeById(preference);
616
+ }, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
617
+ starfield = computed(() => this.theme().starfield, ...(ngDevMode ? [{ debugName: "starfield" }] : /* istanbul ignore next */ []));
618
+ constructor() {
619
+ globalThis
620
+ .matchMedia?.('(prefers-color-scheme: dark)')
621
+ .addEventListener('change', (event) => this.systemDark.set(event.matches));
622
+ effect(() => {
623
+ const preference = this._preference();
624
+ const theme = this.theme();
625
+ this.applyToDom(theme);
626
+ try {
627
+ localStorage.setItem(STORAGE_KEY, preference);
628
+ }
629
+ catch { }
630
+ });
631
+ }
632
+ setPreference(preference) {
633
+ this._preference.set(preference);
634
+ }
635
+ applyToDom(theme) {
636
+ const root = document.documentElement;
637
+ for (const [name, value] of Object.entries(theme.tokens)) {
638
+ root.style.setProperty(`--ac-${name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`, value);
639
+ }
640
+ root.dataset['theme'] = theme.id;
641
+ root.classList.toggle('ns-dark', theme.scheme === 'dark');
642
+ root.style.colorScheme = theme.scheme;
643
+ }
644
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
645
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ThemeService, providedIn: 'root' });
646
+ }
647
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ThemeService, decorators: [{
648
+ type: Injectable,
649
+ args: [{ providedIn: 'root' }]
650
+ }], ctorParameters: () => [] });
651
+
652
+ const MASK = {
653
+ background: 'rgba(0, 0, 0, 0.55)',
654
+ color: 'var(--ac-text)',
655
+ };
656
+ const SHADOW = '0 10px 40px rgba(0, 0, 0, 0.45)';
657
+ const PANEL = {
658
+ background: 'var(--ac-surface)',
659
+ borderColor: 'var(--ac-border)',
660
+ color: 'var(--ac-text)',
661
+ };
662
+ const schemeTokens = {
663
+ primary: {
664
+ color: 'var(--ac-accent)',
665
+ contrastColor: 'var(--ac-on-accent)',
666
+ hoverColor: 'color-mix(in srgb, var(--ac-accent) 85%, var(--ac-text))',
667
+ activeColor: 'color-mix(in srgb, var(--ac-accent) 70%, var(--ac-text))',
668
+ },
669
+ highlight: {
670
+ background: 'color-mix(in srgb, var(--ac-accent) 16%, transparent)',
671
+ focusBackground: 'color-mix(in srgb, var(--ac-accent) 24%, transparent)',
672
+ color: 'var(--ac-text)',
673
+ focusColor: 'var(--ac-text)',
674
+ },
675
+ text: {
676
+ color: 'var(--ac-text)',
677
+ hoverColor: 'var(--ac-text)',
678
+ mutedColor: 'var(--ac-text-muted)',
679
+ hoverMutedColor: 'var(--ac-text)',
680
+ },
681
+ content: {
682
+ background: 'var(--ac-surface)',
683
+ hoverBackground: 'var(--ac-surface-alt)',
684
+ borderColor: 'var(--ac-border)',
685
+ color: 'var(--ac-text)',
686
+ hoverColor: 'var(--ac-text)',
687
+ },
688
+ overlay: {
689
+ select: {
690
+ background: 'var(--ac-surface)',
691
+ borderColor: 'var(--ac-border)',
692
+ color: 'var(--ac-text)',
693
+ },
694
+ popover: {
695
+ background: 'var(--ac-surface)',
696
+ borderColor: 'var(--ac-border)',
697
+ color: 'var(--ac-text)',
698
+ },
699
+ modal: {
700
+ background: 'var(--ac-surface)',
701
+ borderColor: 'var(--ac-border)',
702
+ color: 'var(--ac-text)',
703
+ },
704
+ },
705
+ formField: {
706
+ background: 'var(--ac-bg)',
707
+ disabledBackground: 'var(--ac-surface-alt)',
708
+ filledBackground: 'var(--ac-surface-alt)',
709
+ filledHoverBackground: 'var(--ac-surface-alt)',
710
+ filledFocusBackground: 'var(--ac-surface-alt)',
711
+ borderColor: 'var(--ac-border)',
712
+ hoverBorderColor: 'var(--ac-accent)',
713
+ focusBorderColor: 'var(--ac-accent)',
714
+ color: 'var(--ac-text)',
715
+ disabledColor: 'var(--ac-text-muted)',
716
+ placeholderColor: 'var(--ac-text-muted)',
717
+ floatLabelColor: 'var(--ac-text-muted)',
718
+ floatLabelFocusColor: 'var(--ac-accent)',
719
+ iconColor: 'var(--ac-text-muted)',
720
+ },
721
+ list: {
722
+ option: {
723
+ focusBackground: 'var(--ac-surface-alt)',
724
+ selectedBackground: 'color-mix(in srgb, var(--ac-accent) 20%, transparent)',
725
+ selectedFocusBackground: 'color-mix(in srgb, var(--ac-accent) 28%, transparent)',
726
+ color: 'var(--ac-text)',
727
+ focusColor: 'var(--ac-text)',
728
+ selectedColor: 'var(--ac-text)',
729
+ selectedFocusColor: 'var(--ac-text)',
730
+ icon: {
731
+ color: 'var(--ac-text-muted)',
732
+ focusColor: 'var(--ac-text)',
733
+ },
734
+ },
735
+ },
736
+ navigation: {
737
+ item: {
738
+ focusBackground: 'var(--ac-surface-alt)',
739
+ activeBackground: 'color-mix(in srgb, var(--ac-accent) 20%, transparent)',
740
+ color: 'var(--ac-text)',
741
+ focusColor: 'var(--ac-text)',
742
+ activeColor: 'var(--ac-text)',
743
+ icon: {
744
+ color: 'var(--ac-text-muted)',
745
+ focusColor: 'var(--ac-text)',
746
+ activeColor: 'var(--ac-accent)',
747
+ },
748
+ },
749
+ },
750
+ };
751
+ const NucleusPreset = definePreset(Lara, {
752
+ semantic: {
753
+ colorScheme: {
754
+ light: { ...schemeTokens, mask: MASK },
755
+ dark: { ...schemeTokens, mask: MASK },
756
+ },
757
+ },
758
+ components: {
759
+ drawer: { root: { ...PANEL, shadow: SHADOW } },
760
+ popover: { root: { ...PANEL, shadow: SHADOW } },
761
+ menu: { root: { ...PANEL, shadow: SHADOW } },
762
+ listbox: { root: PANEL },
763
+ },
764
+ });
765
+
766
+ const NUCLEUS_DARK_SELECTOR = '.ns-dark';
767
+ function provideNucleusTheme() {
768
+ return providePrimeNG({
769
+ theme: {
770
+ preset: NucleusPreset,
771
+ options: { darkModeSelector: NUCLEUS_DARK_SELECTOR },
772
+ },
773
+ });
774
+ }
775
+
776
+ const ORBIT = { rx: 78, ry: 196 };
777
+ class NucleusMark {
778
+ accent = input('#7C6CF5', ...(ngDevMode ? [{ debugName: "accent" }] : /* istanbul ignore next */ []));
779
+ size = input(28, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
780
+ orbit = ORBIT;
781
+ rotations = [0, 60, 120];
782
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusMark, deps: [], target: i0.ɵɵFactoryTarget.Component });
783
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: NucleusMark, isStandalone: true, selector: "nucleus-mark", inputs: { accent: { classPropertyName: "accent", publicName: "accent", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
784
+ <svg
785
+ [attr.width]="size()"
786
+ [attr.height]="size()"
787
+ viewBox="-230 -230 460 460"
788
+ aria-hidden="true"
789
+ >
790
+ @for (r of rotations; track $index) {
791
+ <ellipse
792
+ [attr.rx]="orbit.rx"
793
+ [attr.ry]="orbit.ry"
794
+ [attr.transform]="'rotate(' + r + ')'"
795
+ fill="none"
796
+ stroke="currentColor"
797
+ stroke-width="18"
798
+ />
799
+ }
800
+ <circle r="42" [attr.fill]="accent()" />
801
+ </svg>
802
+ `, isInline: true, styles: [":host{display:block}svg{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
803
+ }
804
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusMark, decorators: [{
805
+ type: Component,
806
+ args: [{ selector: 'nucleus-mark', changeDetection: ChangeDetectionStrategy.OnPush, template: `
807
+ <svg
808
+ [attr.width]="size()"
809
+ [attr.height]="size()"
810
+ viewBox="-230 -230 460 460"
811
+ aria-hidden="true"
812
+ >
813
+ @for (r of rotations; track $index) {
814
+ <ellipse
815
+ [attr.rx]="orbit.rx"
816
+ [attr.ry]="orbit.ry"
817
+ [attr.transform]="'rotate(' + r + ')'"
818
+ fill="none"
819
+ stroke="currentColor"
820
+ stroke-width="18"
821
+ />
822
+ }
823
+ <circle r="42" [attr.fill]="accent()" />
824
+ </svg>
825
+ `, styles: [":host{display:block}svg{display:block}\n"] }]
826
+ }], propDecorators: { accent: [{ type: i0.Input, args: [{ isSignal: true, alias: "accent", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
827
+
828
+ const W = 1280;
829
+ const H = 720;
830
+ const STARS = Array.from({ length: 130 }, (_, i) => {
831
+ const a = i * 2.399963;
832
+ const big = i % 11 === 0;
833
+ const sway = Math.sin(a * 1.7);
834
+ const lift = Math.cos(a * 2.3);
835
+ return {
836
+ cx: +((Math.sin(a * 7.13) * 0.5 + 0.5) * W).toFixed(2),
837
+ cy: +((Math.cos(a * 3.77) * 0.5 + 0.5) * H).toFixed(2),
838
+ r: +((big ? 2.4 : 0.9) + (i % 4) * 0.5).toFixed(2),
839
+ opacity: ((big ? 0.85 : 0.42) + (i % 5) * 0.09).toFixed(2),
840
+ dx: `${(sway * 26).toFixed(1)}px`,
841
+ dy: `${(lift * 18).toFixed(1)}px`,
842
+ duration: `${(11 + (i % 9) * 1.7).toFixed(1)}s`,
843
+ delay: `-${((i % 13) * 0.85).toFixed(2)}s`,
844
+ };
845
+ });
846
+ class NucleusStarfield {
847
+ W = W;
848
+ H = H;
849
+ stars = STARS;
850
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusStarfield, deps: [], target: i0.ɵɵFactoryTarget.Component });
851
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: NucleusStarfield, isStandalone: true, selector: "nucleus-starfield", ngImport: i0, template: `
852
+ <svg
853
+ [attr.viewBox]="'0 0 ' + W + ' ' + H"
854
+ preserveAspectRatio="xMidYMid slice"
855
+ aria-hidden="true"
856
+ focusable="false"
857
+ >
858
+ <defs>
859
+ <radialGradient id="ns-starfield-glow" cx="50%" cy="0%" r="75%">
860
+ <stop offset="0%" stop-color="var(--ac-accent)" stop-opacity="0.18" />
861
+ <stop offset="70%" stop-color="var(--ac-accent)" stop-opacity="0" />
862
+ </radialGradient>
863
+ </defs>
864
+
865
+ <rect width="100%" height="100%" fill="url(#ns-starfield-glow)" />
866
+
867
+ @for (star of stars; track $index) {
868
+ <circle
869
+ [attr.cx]="star.cx"
870
+ [attr.cy]="star.cy"
871
+ [attr.r]="star.r"
872
+ [style.--o]="star.opacity"
873
+ [style.--dx]="star.dx"
874
+ [style.--dy]="star.dy"
875
+ [style.--dur]="star.duration"
876
+ [style.--delay]="star.delay"
877
+ fill="#ffffff"
878
+ />
879
+ }
880
+ </svg>
881
+ `, isInline: true, styles: [":host{position:fixed;inset:0;z-index:-1;display:block;pointer-events:none;background:var(--ac-bg)}svg{display:block;width:100%;height:100%}circle{opacity:var(--o);animation:ns-drift var(--dur) ease-in-out var(--delay) infinite alternate}@keyframes ns-drift{0%{transform:translate(0);opacity:var(--o)}to{transform:translate(var(--dx),var(--dy));opacity:calc(var(--o) * .3)}}@media(prefers-reduced-motion:reduce){circle{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
882
+ }
883
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusStarfield, decorators: [{
884
+ type: Component,
885
+ args: [{ selector: 'nucleus-starfield', changeDetection: ChangeDetectionStrategy.OnPush, template: `
886
+ <svg
887
+ [attr.viewBox]="'0 0 ' + W + ' ' + H"
888
+ preserveAspectRatio="xMidYMid slice"
889
+ aria-hidden="true"
890
+ focusable="false"
891
+ >
892
+ <defs>
893
+ <radialGradient id="ns-starfield-glow" cx="50%" cy="0%" r="75%">
894
+ <stop offset="0%" stop-color="var(--ac-accent)" stop-opacity="0.18" />
895
+ <stop offset="70%" stop-color="var(--ac-accent)" stop-opacity="0" />
896
+ </radialGradient>
897
+ </defs>
898
+
899
+ <rect width="100%" height="100%" fill="url(#ns-starfield-glow)" />
900
+
901
+ @for (star of stars; track $index) {
902
+ <circle
903
+ [attr.cx]="star.cx"
904
+ [attr.cy]="star.cy"
905
+ [attr.r]="star.r"
906
+ [style.--o]="star.opacity"
907
+ [style.--dx]="star.dx"
908
+ [style.--dy]="star.dy"
909
+ [style.--dur]="star.duration"
910
+ [style.--delay]="star.delay"
911
+ fill="#ffffff"
912
+ />
913
+ }
914
+ </svg>
915
+ `, styles: [":host{position:fixed;inset:0;z-index:-1;display:block;pointer-events:none;background:var(--ac-bg)}svg{display:block;width:100%;height:100%}circle{opacity:var(--o);animation:ns-drift var(--dur) ease-in-out var(--delay) infinite alternate}@keyframes ns-drift{0%{transform:translate(0);opacity:var(--o)}to{transform:translate(var(--dx),var(--dy));opacity:calc(var(--o) * .3)}}@media(prefers-reduced-motion:reduce){circle{animation:none}}\n"] }]
916
+ }] });
917
+
918
+ const SHELL_COPY = {
919
+ es: {
920
+ navToggle: 'Mostrar u ocultar el menú',
921
+ appsLauncher: 'Aplicaciones',
922
+ appsEmpty: 'No hay otras aplicaciones',
923
+ settingsTitle: 'Ajustes',
924
+ settingsTheme: 'Tema',
925
+ settingsThemeHint: 'Elige cómo se ve la suite en este dispositivo.',
926
+ themeSystem: 'Del sistema',
927
+ signOut: 'Cerrar sesión',
928
+ accountManage: 'Gestionar mi cuenta',
929
+ accountSwitch: 'Cambiar de cuenta',
930
+ accessLoading: 'Cargando accesos...',
931
+ accessEmpty: 'Sin accesos asignados',
932
+ accountScope: 'Mi cuenta',
933
+ hintNoAccess: 'Sin acceso',
934
+ hintComingSoon: 'Próximamente',
935
+ hintNotDeployed: 'Sin publicar',
936
+ },
937
+ en: {
938
+ navToggle: 'Show or hide the menu',
939
+ appsLauncher: 'Applications',
940
+ appsEmpty: 'No other applications',
941
+ settingsTitle: 'Settings',
942
+ settingsTheme: 'Theme',
943
+ settingsThemeHint: 'Choose how the suite looks on this device.',
944
+ themeSystem: 'System',
945
+ signOut: 'Sign out',
946
+ accountManage: 'Manage my account',
947
+ accountSwitch: 'Switch account',
948
+ accessLoading: 'Loading access...',
949
+ accessEmpty: 'No access granted',
950
+ accountScope: 'My account',
951
+ hintNoAccess: 'No access',
952
+ hintComingSoon: 'Coming soon',
953
+ hintNotDeployed: 'Not deployed',
954
+ },
955
+ };
956
+
957
+ class NucleusShell {
958
+ session = inject(SessionService);
959
+ sessionAuth = inject(NucleusSessionAuth);
960
+ themes = inject(ThemeService);
961
+ language = inject(NucleusLanguage);
962
+ config = inject(NUCLEUS_CONFIG);
963
+ injector = inject(Injector);
964
+ router = inject(Router);
965
+ brand = input('SUITE', ...(ngDevMode ? [{ debugName: "brand" }] : /* istanbul ignore next */ []));
966
+ heading = input('', ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
967
+ lead = input('', ...(ngDevMode ? [{ debugName: "lead" }] : /* istanbul ignore next */ []));
968
+ navItems = input([], ...(ngDevMode ? [{ debugName: "navItems" }] : /* istanbul ignore next */ []));
969
+ activeNavId = input(null, ...(ngDevMode ? [{ debugName: "activeNavId" }] : /* istanbul ignore next */ []));
970
+ selectedMenuId = input(null, ...(ngDevMode ? [{ debugName: "selectedMenuId" }] : /* istanbul ignore next */ []));
971
+ navSelect = output();
972
+ menuSelect = output();
973
+ lang = this.language.lang;
974
+ copy = computed(() => SHELL_COPY[this.lang()], ...(ngDevMode ? [{ debugName: "copy" }] : /* istanbul ignore next */ []));
975
+ starfield = this.themes.starfield;
976
+ themePreference = this.themes.preference;
977
+ menu = this.session.menu;
978
+ menuLoading = this.session.loading;
979
+ menuError = this.session.error;
980
+ launcherApps = this.session.apps;
981
+ sidebarOpen = signal(true, ...(ngDevMode ? [{ debugName: "sidebarOpen" }] : /* istanbul ignore next */ []));
982
+ settingsOpen = signal(false, ...(ngDevMode ? [{ debugName: "settingsOpen" }] : /* istanbul ignore next */ []));
983
+ auth = this.injector.get(NucleusAuth, null);
984
+ identity = computed(() => {
985
+ const me = this.session.me();
986
+ if (me) {
987
+ return {
988
+ name: me.displayName ?? me.userId,
989
+ email: me.userId,
990
+ photoUrl: me.photoUrl,
991
+ };
992
+ }
993
+ const user = this.auth?.user() ?? null;
994
+ if (!user)
995
+ return null;
996
+ return {
997
+ name: user.displayName ?? user.email ?? user.providerUserId,
998
+ email: user.email ?? user.providerUserId,
999
+ photoUrl: user.photoUrl,
1000
+ };
1001
+ }, ...(ngDevMode ? [{ debugName: "identity" }] : /* istanbul ignore next */ []));
1002
+ initials = computed(() => initialsOf(this.identity()?.name ?? ''), ...(ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []));
1003
+ canSwitchAccount = computed(() => this.auth?.switchAccount !== undefined, ...(ngDevMode ? [{ debugName: "canSwitchAccount" }] : /* istanbul ignore next */ []));
1004
+ accessCaption = computed(() => {
1005
+ const role = this.session.roleName();
1006
+ const site = this.session.siteName();
1007
+ if (role && site)
1008
+ return `${role} · ${site}`;
1009
+ if (this.session.scope() === 'account')
1010
+ return this.copy().accountScope;
1011
+ return '';
1012
+ }, ...(ngDevMode ? [{ debugName: "accessCaption" }] : /* istanbul ignore next */ []));
1013
+ themeOptions = computed(() => [
1014
+ ...THEMES.map((theme) => ({
1015
+ value: theme.id,
1016
+ label: THEME_LABELS[theme.id],
1017
+ swatch: theme.swatch,
1018
+ })),
1019
+ { value: 'system', label: this.copy().themeSystem, swatch: null },
1020
+ ], ...(ngDevMode ? [{ debugName: "themeOptions" }] : /* istanbul ignore next */ []));
1021
+ loadSession = effect(() => {
1022
+ const language = this.lang();
1023
+ const user = this.auth?.user() ?? null;
1024
+ if (this.auth && !user)
1025
+ return;
1026
+ untracked(() => void this.session.load(language));
1027
+ }, ...(ngDevMode ? [{ debugName: "loadSession" }] : /* istanbul ignore next */ []));
1028
+ setLang(lang) {
1029
+ this.language.setLang(lang);
1030
+ }
1031
+ toggleSidebar() {
1032
+ this.sidebarOpen.update((open) => !open);
1033
+ }
1034
+ selectTheme(preference) {
1035
+ if (preference)
1036
+ this.themes.setPreference(preference);
1037
+ }
1038
+ selectNav(id) {
1039
+ this.navSelect.emit(id);
1040
+ }
1041
+ selectMenu(node) {
1042
+ this.menuSelect.emit(node);
1043
+ if (node.route)
1044
+ void this.router.navigateByUrl(node.route);
1045
+ }
1046
+ isSelected(node) {
1047
+ return this.selectedMenuId() === node.id;
1048
+ }
1049
+ canOpen(app) {
1050
+ return app.hasAccess && app.status === 1 && !!app.url;
1051
+ }
1052
+ launch(app) {
1053
+ if (!this.canOpen(app))
1054
+ return;
1055
+ window.location.href = app.url;
1056
+ }
1057
+ appInitials(app) {
1058
+ return initialsOf(app.name);
1059
+ }
1060
+ launchHint(app) {
1061
+ const t = this.copy();
1062
+ if (!app.hasAccess)
1063
+ return t.hintNoAccess;
1064
+ if (app.status !== 1)
1065
+ return t.hintComingSoon;
1066
+ if (!app.url)
1067
+ return t.hintNotDeployed;
1068
+ return '';
1069
+ }
1070
+ async switchAccount() {
1071
+ await this.auth?.switchAccount?.();
1072
+ }
1073
+ async signOut() {
1074
+ await this.sessionAuth.clear();
1075
+ await this.auth?.signOut?.();
1076
+ const landing = this.config.landingUrl;
1077
+ if (landing)
1078
+ window.location.href = landing;
1079
+ }
1080
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
1081
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: NucleusShell, isStandalone: true, selector: "nucleus-shell", inputs: { brand: { classPropertyName: "brand", publicName: "brand", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, lead: { classPropertyName: "lead", publicName: "lead", isSignal: true, isRequired: false, transformFunction: null }, navItems: { classPropertyName: "navItems", publicName: "navItems", isSignal: true, isRequired: false, transformFunction: null }, activeNavId: { classPropertyName: "activeNavId", publicName: "activeNavId", isSignal: true, isRequired: false, transformFunction: null }, selectedMenuId: { classPropertyName: "selectedMenuId", publicName: "selectedMenuId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { navSelect: "navSelect", menuSelect: "menuSelect" }, ngImport: i0, template: "@let t = copy();\n@let me = identity();\n\n@if (starfield()) {\n <nucleus-starfield />\n}\n\n<header class=\"topbar\">\n <div class=\"side\">\n <p-button\n icon=\"pi pi-bars\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [rounded]=\"true\"\n [ariaLabel]=\"t.navToggle\"\n [pTooltip]=\"t.navToggle\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSidebar()\"\n />\n\n <div class=\"brand\">\n <nucleus-mark [size]=\"20\" />\n <div class=\"brand-name\">\n <span class=\"brand-primary\">NUCLEUS</span>\n <span class=\"brand-secondary\">{{ brand() }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"side\">\n <div class=\"lang-switch\" role=\"group\" aria-label=\"Idioma / Language\">\n <button\n type=\"button\"\n class=\"lang-pill\"\n [class.is-active]=\"lang() === 'es'\"\n [attr.aria-pressed]=\"lang() === 'es'\"\n (click)=\"setLang('es')\"\n >\n ES\n </button>\n <button\n type=\"button\"\n class=\"lang-pill\"\n [class.is-active]=\"lang() === 'en'\"\n [attr.aria-pressed]=\"lang() === 'en'\"\n (click)=\"setLang('en')\"\n >\n EN\n </button>\n </div>\n\n <p-button\n icon=\"pi pi-th-large\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [rounded]=\"true\"\n [ariaLabel]=\"t.appsLauncher\"\n [pTooltip]=\"t.appsLauncher\"\n tooltipPosition=\"bottom\"\n (onClick)=\"launcher.toggle($event)\"\n />\n\n <p-popover #launcher>\n <div class=\"launcher\">\n <p class=\"launcher-title\">{{ t.appsLauncher }}</p>\n\n @if (!launcherApps().length) {\n <p class=\"launcher-empty\">{{ t.appsEmpty }}</p>\n }\n\n <div class=\"launcher-grid\">\n @for (app of launcherApps(); track app.id) {\n <button\n type=\"button\"\n class=\"launcher-item\"\n [class.is-disabled]=\"!canOpen(app)\"\n [disabled]=\"!canOpen(app)\"\n [pTooltip]=\"launchHint(app)\"\n tooltipPosition=\"bottom\"\n (click)=\"launch(app); launcher.hide()\"\n >\n <span class=\"launcher-icon\" [style.background]=\"app.color ?? '#64748b'\">\n @if (app.icon) {\n <i [class]=\"app.icon\" aria-hidden=\"true\"></i>\n } @else {\n <span class=\"launcher-initials\">{{ appInitials(app) }}</span>\n }\n </span>\n <span class=\"launcher-name\">{{ app.name }}</span>\n @if (launchHint(app)) {\n <span class=\"launcher-badge\">{{ launchHint(app) }}</span>\n }\n </button>\n }\n </div>\n </div>\n </p-popover>\n\n <p-button\n icon=\"pi pi-cog\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [rounded]=\"true\"\n [ariaLabel]=\"t.settingsTitle\"\n [pTooltip]=\"t.settingsTitle\"\n tooltipPosition=\"bottom\"\n (onClick)=\"settingsOpen.set(true)\"\n />\n\n @if (me) {\n <p-button\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [rounded]=\"true\"\n styleClass=\"avatar-trigger\"\n [ariaLabel]=\"me.name\"\n [pTooltip]=\"me.name\"\n tooltipPosition=\"bottom\"\n (onClick)=\"account.toggle($event)\"\n >\n <p-avatar\n [image]=\"me.photoUrl ?? undefined\"\n [label]=\"me.photoUrl ? undefined : initials()\"\n shape=\"circle\"\n size=\"normal\"\n />\n </p-button>\n }\n </div>\n</header>\n\n<p-popover #account>\n @if (me) {\n <div class=\"account\">\n <p class=\"account-email\">{{ me.email }}</p>\n\n <div class=\"account-head\">\n @if (me.photoUrl) {\n <img class=\"account-photo\" [src]=\"me.photoUrl\" alt=\"\" width=\"72\" height=\"72\" />\n } @else {\n <span class=\"account-photo account-initials\">{{ initials() }}</span>\n }\n <p class=\"account-hi\">{{ me.name }}</p>\n </div>\n\n <div class=\"account-actions\">\n @if (canSwitchAccount()) {\n <p-button\n [label]=\"t.accountSwitch\"\n icon=\"pi pi-users\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [fluid]=\"true\"\n (onClick)=\"switchAccount(); account.hide()\"\n />\n }\n <p-button\n [label]=\"t.signOut\"\n icon=\"pi pi-sign-out\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [fluid]=\"true\"\n (onClick)=\"signOut(); account.hide()\"\n />\n </div>\n </div>\n }\n</p-popover>\n\n<div class=\"body\">\n <aside class=\"sidebar\" [class.is-collapsed]=\"!sidebarOpen()\">\n @if (navItems().length) {\n <nav>\n @for (item of navItems(); track item.id) {\n <button\n type=\"button\"\n class=\"nav-item\"\n [class.is-active]=\"activeNavId() === item.id\"\n [attr.aria-current]=\"activeNavId() === item.id ? 'page' : null\"\n [pTooltip]=\"sidebarOpen() ? '' : item.label\"\n tooltipPosition=\"right\"\n (click)=\"selectNav(item.id)\"\n >\n <i [class]=\"item.icon\" aria-hidden=\"true\"></i>\n <span class=\"nav-label\">{{ item.label }}</span>\n </button>\n }\n </nav>\n }\n\n <nav class=\"access-menu\">\n @if (accessCaption()) {\n <p class=\"access-caption\">{{ accessCaption() }}</p>\n }\n\n @if (menuLoading()) {\n <p class=\"access-state\">{{ t.accessLoading }}</p>\n } @else if (menuError()) {\n <p class=\"access-state is-error\">{{ menuError() }}</p>\n } @else if (!menu().length) {\n <p class=\"access-state\">{{ t.accessEmpty }}</p>\n } @else {\n @for (node of menu(); track node.id) {\n <button\n type=\"button\"\n class=\"nav-item\"\n [class.is-active]=\"isSelected(node)\"\n [pTooltip]=\"sidebarOpen() ? '' : node.label\"\n tooltipPosition=\"right\"\n (click)=\"selectMenu(node)\"\n >\n <i [class]=\"node.icon ?? 'pi pi-circle'\" aria-hidden=\"true\"></i>\n <span class=\"nav-label\">{{ node.label }}</span>\n </button>\n\n @for (child of node.children; track child.id) {\n <button\n type=\"button\"\n class=\"nav-item is-child\"\n [class.is-active]=\"isSelected(child)\"\n [pTooltip]=\"sidebarOpen() ? '' : child.label\"\n tooltipPosition=\"right\"\n (click)=\"selectMenu(child)\"\n >\n <i [class]=\"child.icon ?? 'pi pi-circle'\" aria-hidden=\"true\"></i>\n <span class=\"nav-label\">{{ child.label }}</span>\n </button>\n }\n }\n }\n </nav>\n </aside>\n\n <main class=\"content\">\n @if (heading()) {\n <h1>{{ heading() }}</h1>\n }\n @if (lead()) {\n <p class=\"lead\">{{ lead() }}</p>\n }\n\n <ng-content />\n </main>\n</div>\n\n<p-drawer\n [visible]=\"settingsOpen()\"\n (visibleChange)=\"settingsOpen.set($event)\"\n position=\"right\"\n [header]=\"t.settingsTitle\"\n [style]=\"{ width: '340px' }\"\n>\n <h3 class=\"settings-heading\">{{ t.settingsTheme }}</h3>\n <p class=\"settings-hint\">{{ t.settingsThemeHint }}</p>\n\n <p-listbox\n [options]=\"themeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n [ngModel]=\"themePreference()\"\n (ngModelChange)=\"selectTheme($event)\"\n [ariaLabel]=\"t.settingsTheme\"\n >\n <ng-template #item let-option>\n <span class=\"theme-option\">\n @if (option.swatch) {\n <span class=\"theme-swatch\" [style.background]=\"option.swatch\" aria-hidden=\"true\"></span>\n } @else {\n <i class=\"pi pi-desktop theme-swatch-icon\" aria-hidden=\"true\"></i>\n }\n <span>{{ option.label }}</span>\n </span>\n </ng-template>\n </p-listbox>\n</p-drawer>\n", styles: [":host{display:flex;flex-direction:column;min-height:100dvh;background:var(--ac-bg);color:var(--ac-text)}.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:16px;height:56px;padding:0 12px 0 8px;background:var(--ac-surface);border-bottom:1px solid var(--ac-border)}.side{display:flex;align-items:center;gap:4px}.brand{display:flex;align-items:center;gap:9px;margin-left:4px}.brand-name{display:flex;align-items:baseline;gap:7px}.brand-primary{font-size:13px;font-weight:700;letter-spacing:.15em}.brand-secondary{font-size:9px;font-weight:500;letter-spacing:.22em;color:var(--ac-text-muted)}.lang-switch{display:flex;align-items:center;gap:2px;margin-right:6px;border:1px solid var(--ac-border);border-radius:999px;padding:2px}.lang-pill{font-size:10px;font-weight:600;letter-spacing:.08em;padding:4px 10px;border:none;border-radius:999px;background:transparent;color:var(--ac-text-muted);cursor:pointer}.lang-pill.is-active{background:var(--ac-accent);color:var(--ac-on-accent)}.launcher{width:320px;padding:4px}.launcher-title{margin:0 0 10px;font-size:12px;font-weight:600;color:var(--ac-text-muted)}.launcher-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px}.launcher-item{position:relative;display:flex;flex-direction:column;align-items:center;gap:7px;padding:12px 6px;border:none;border-radius:8px;background:transparent;color:var(--ac-text);cursor:pointer}.launcher-item:hover:not(:disabled){background:var(--ac-surface-alt)}.launcher-item.is-disabled{opacity:.5;cursor:default}.launcher-icon{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:10px;color:#fff}.launcher-icon i{font-size:18px}.launcher-name{font-size:11px;font-weight:500;line-height:1.3;text-align:center}.launcher-badge{font-size:9px;font-weight:500;color:var(--ac-text-muted)}.account{width:300px;padding:4px;text-align:center}.account-email{margin:0 0 12px;font-size:12px;color:var(--ac-text-muted)}.account-head{display:flex;flex-direction:column;align-items:center;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--ac-border)}.account-photo{width:72px;height:72px;border-radius:999px;object-fit:cover}.account-initials{display:inline-flex;align-items:center;justify-content:center;background:var(--ac-accent);color:var(--ac-on-accent);font-size:26px;font-weight:600}.account-hi{margin:0;font-size:16px;font-weight:500}.account-actions{display:flex;flex-direction:column;gap:2px;padding-top:8px}.body{display:flex;flex:1;min-height:0}.sidebar{flex:none;width:224px;padding:12px 10px;background:var(--ac-surface);border-right:1px solid var(--ac-border);transition:width .18s ease}.sidebar.is-collapsed{width:60px}.sidebar nav{display:flex;flex-direction:column;gap:3px}.nav-item{display:flex;align-items:center;gap:12px;width:100%;padding:9px 12px;border:none;border-radius:6px;background:transparent;color:var(--ac-text-muted);font-size:13px;font-weight:500;text-align:left;white-space:nowrap;cursor:pointer}.nav-item i{flex:none;font-size:15px}.nav-item:hover{background:var(--ac-surface-alt);color:var(--ac-text)}.nav-item.is-active{background:color-mix(in srgb,var(--ac-accent) 18%,transparent);color:var(--ac-text)}.nav-item.is-active i{color:var(--ac-accent)}.sidebar.is-collapsed .nav-label{display:none}.sidebar.is-collapsed .nav-item{justify-content:center;padding-inline:0}.content{flex:1;min-width:0;padding:28px 28px 64px;display:flex;flex-direction:column;gap:14px}h1{margin:0;font-size:26px;line-height:1.2;font-weight:500;letter-spacing:-.02em}.lead{margin:0;font-size:14px;line-height:1.6;font-weight:300;color:var(--ac-text-muted)}.settings-heading{margin:0 0 4px;font-size:14px;font-weight:600}.settings-hint{margin:0 0 14px;font-size:12px;font-weight:300;color:var(--ac-text-muted)}.theme-option{display:flex;align-items:center;gap:10px}.theme-swatch{flex:none;width:16px;height:16px;border-radius:3px;border:1px solid var(--ac-border)}.theme-swatch-icon{flex:none;width:16px;font-size:14px;color:var(--ac-text-muted)}.access-menu{display:flex;flex-direction:column;gap:.15rem;margin-top:.75rem;padding-top:.75rem;border-top:1px solid color-mix(in srgb,currentColor 12%,transparent)}.access-caption{margin:0 0 .4rem;padding:0 .75rem;font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;opacity:.65;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.access-state{margin:0;padding:.35rem .75rem;font-size:.78rem;opacity:.7}.access-state.is-error{color:var(--p-red-400, #f87171);opacity:1}.nav-item.is-child{padding-left:2.1rem;font-size:.86rem}.launcher-initials{font-size:.85rem;font-weight:600;letter-spacing:.02em;line-height:1;color:#fff}.launcher-empty{margin:.25rem 0 .5rem;font-size:.82rem;opacity:.7}@media(max-width:720px){.sidebar{width:60px}.sidebar .nav-label{display:none}.sidebar .nav-item{justify-content:center;padding-inline:0}.brand-secondary{display:none}.content{padding:20px 16px 48px}h1{font-size:22px}.launcher{width:260px}}\n"], dependencies: [{ kind: "ngmodule", type: AvatarModule }, { kind: "component", type: i1.Avatar, selector: "p-avatar", inputs: ["label", "icon", "image", "size", "shape", "styleClass", "ariaLabel", "ariaLabelledBy"], outputs: ["onImageError"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DrawerModule }, { kind: "component", type: i3.Drawer, selector: "p-drawer", inputs: ["appendTo", "motionOptions", "blockScroll", "style", "styleClass", "ariaCloseLabel", "autoZIndex", "baseZIndex", "modal", "closeButtonProps", "dismissible", "showCloseIcon", "closeOnEscape", "transitionOptions", "visible", "position", "fullScreen", "header", "maskStyle", "closable"], outputs: ["onShow", "onHide", "visibleChange"] }, { kind: "ngmodule", type: ListboxModule }, { kind: "component", type: i4.Listbox, selector: "p-listbox, p-listBox, p-list-box", inputs: ["hostName", "id", "searchMessage", "emptySelectionMessage", "selectionMessage", "autoOptionFocus", "ariaLabel", "selectOnFocus", "searchLocale", "focusOnHover", "filterMessage", "filterFields", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "scrollHeight", "tabindex", "multiple", "styleClass", "listStyle", "listStyleClass", "readonly", "checkbox", "filter", "filterBy", "filterMatchMode", "filterLocale", "metaKeySelection", "dataKey", "showToggleAll", "optionLabel", "optionValue", "optionGroupChildren", "optionGroupLabel", "optionDisabled", "ariaFilterLabel", "filterPlaceHolder", "emptyFilterMessage", "emptyMessage", "group", "options", "filterValue", "selectAll", "striped", "highlightOnSelect", "checkmark", "dragdrop", "dropListData", "fluid"], outputs: ["onChange", "onClick", "onDblClick", "onFilter", "onFocus", "onBlur", "onSelectAllChange", "onLazyLoad", "onDrop"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i5.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i6.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i7.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: NucleusMark, selector: "nucleus-mark", inputs: ["accent", "size"] }, { kind: "component", type: NucleusStarfield, selector: "nucleus-starfield" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1082
+ }
1083
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NucleusShell, decorators: [{
1084
+ type: Component,
1085
+ args: [{ selector: 'nucleus-shell', imports: [
1086
+ AvatarModule,
1087
+ ButtonModule,
1088
+ DrawerModule,
1089
+ ListboxModule,
1090
+ PopoverModule,
1091
+ TooltipModule,
1092
+ FormsModule,
1093
+ NucleusMark,
1094
+ NucleusStarfield,
1095
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let t = copy();\n@let me = identity();\n\n@if (starfield()) {\n <nucleus-starfield />\n}\n\n<header class=\"topbar\">\n <div class=\"side\">\n <p-button\n icon=\"pi pi-bars\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [rounded]=\"true\"\n [ariaLabel]=\"t.navToggle\"\n [pTooltip]=\"t.navToggle\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSidebar()\"\n />\n\n <div class=\"brand\">\n <nucleus-mark [size]=\"20\" />\n <div class=\"brand-name\">\n <span class=\"brand-primary\">NUCLEUS</span>\n <span class=\"brand-secondary\">{{ brand() }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"side\">\n <div class=\"lang-switch\" role=\"group\" aria-label=\"Idioma / Language\">\n <button\n type=\"button\"\n class=\"lang-pill\"\n [class.is-active]=\"lang() === 'es'\"\n [attr.aria-pressed]=\"lang() === 'es'\"\n (click)=\"setLang('es')\"\n >\n ES\n </button>\n <button\n type=\"button\"\n class=\"lang-pill\"\n [class.is-active]=\"lang() === 'en'\"\n [attr.aria-pressed]=\"lang() === 'en'\"\n (click)=\"setLang('en')\"\n >\n EN\n </button>\n </div>\n\n <p-button\n icon=\"pi pi-th-large\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [rounded]=\"true\"\n [ariaLabel]=\"t.appsLauncher\"\n [pTooltip]=\"t.appsLauncher\"\n tooltipPosition=\"bottom\"\n (onClick)=\"launcher.toggle($event)\"\n />\n\n <p-popover #launcher>\n <div class=\"launcher\">\n <p class=\"launcher-title\">{{ t.appsLauncher }}</p>\n\n @if (!launcherApps().length) {\n <p class=\"launcher-empty\">{{ t.appsEmpty }}</p>\n }\n\n <div class=\"launcher-grid\">\n @for (app of launcherApps(); track app.id) {\n <button\n type=\"button\"\n class=\"launcher-item\"\n [class.is-disabled]=\"!canOpen(app)\"\n [disabled]=\"!canOpen(app)\"\n [pTooltip]=\"launchHint(app)\"\n tooltipPosition=\"bottom\"\n (click)=\"launch(app); launcher.hide()\"\n >\n <span class=\"launcher-icon\" [style.background]=\"app.color ?? '#64748b'\">\n @if (app.icon) {\n <i [class]=\"app.icon\" aria-hidden=\"true\"></i>\n } @else {\n <span class=\"launcher-initials\">{{ appInitials(app) }}</span>\n }\n </span>\n <span class=\"launcher-name\">{{ app.name }}</span>\n @if (launchHint(app)) {\n <span class=\"launcher-badge\">{{ launchHint(app) }}</span>\n }\n </button>\n }\n </div>\n </div>\n </p-popover>\n\n <p-button\n icon=\"pi pi-cog\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [rounded]=\"true\"\n [ariaLabel]=\"t.settingsTitle\"\n [pTooltip]=\"t.settingsTitle\"\n tooltipPosition=\"bottom\"\n (onClick)=\"settingsOpen.set(true)\"\n />\n\n @if (me) {\n <p-button\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [rounded]=\"true\"\n styleClass=\"avatar-trigger\"\n [ariaLabel]=\"me.name\"\n [pTooltip]=\"me.name\"\n tooltipPosition=\"bottom\"\n (onClick)=\"account.toggle($event)\"\n >\n <p-avatar\n [image]=\"me.photoUrl ?? undefined\"\n [label]=\"me.photoUrl ? undefined : initials()\"\n shape=\"circle\"\n size=\"normal\"\n />\n </p-button>\n }\n </div>\n</header>\n\n<p-popover #account>\n @if (me) {\n <div class=\"account\">\n <p class=\"account-email\">{{ me.email }}</p>\n\n <div class=\"account-head\">\n @if (me.photoUrl) {\n <img class=\"account-photo\" [src]=\"me.photoUrl\" alt=\"\" width=\"72\" height=\"72\" />\n } @else {\n <span class=\"account-photo account-initials\">{{ initials() }}</span>\n }\n <p class=\"account-hi\">{{ me.name }}</p>\n </div>\n\n <div class=\"account-actions\">\n @if (canSwitchAccount()) {\n <p-button\n [label]=\"t.accountSwitch\"\n icon=\"pi pi-users\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [fluid]=\"true\"\n (onClick)=\"switchAccount(); account.hide()\"\n />\n }\n <p-button\n [label]=\"t.signOut\"\n icon=\"pi pi-sign-out\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [fluid]=\"true\"\n (onClick)=\"signOut(); account.hide()\"\n />\n </div>\n </div>\n }\n</p-popover>\n\n<div class=\"body\">\n <aside class=\"sidebar\" [class.is-collapsed]=\"!sidebarOpen()\">\n @if (navItems().length) {\n <nav>\n @for (item of navItems(); track item.id) {\n <button\n type=\"button\"\n class=\"nav-item\"\n [class.is-active]=\"activeNavId() === item.id\"\n [attr.aria-current]=\"activeNavId() === item.id ? 'page' : null\"\n [pTooltip]=\"sidebarOpen() ? '' : item.label\"\n tooltipPosition=\"right\"\n (click)=\"selectNav(item.id)\"\n >\n <i [class]=\"item.icon\" aria-hidden=\"true\"></i>\n <span class=\"nav-label\">{{ item.label }}</span>\n </button>\n }\n </nav>\n }\n\n <nav class=\"access-menu\">\n @if (accessCaption()) {\n <p class=\"access-caption\">{{ accessCaption() }}</p>\n }\n\n @if (menuLoading()) {\n <p class=\"access-state\">{{ t.accessLoading }}</p>\n } @else if (menuError()) {\n <p class=\"access-state is-error\">{{ menuError() }}</p>\n } @else if (!menu().length) {\n <p class=\"access-state\">{{ t.accessEmpty }}</p>\n } @else {\n @for (node of menu(); track node.id) {\n <button\n type=\"button\"\n class=\"nav-item\"\n [class.is-active]=\"isSelected(node)\"\n [pTooltip]=\"sidebarOpen() ? '' : node.label\"\n tooltipPosition=\"right\"\n (click)=\"selectMenu(node)\"\n >\n <i [class]=\"node.icon ?? 'pi pi-circle'\" aria-hidden=\"true\"></i>\n <span class=\"nav-label\">{{ node.label }}</span>\n </button>\n\n @for (child of node.children; track child.id) {\n <button\n type=\"button\"\n class=\"nav-item is-child\"\n [class.is-active]=\"isSelected(child)\"\n [pTooltip]=\"sidebarOpen() ? '' : child.label\"\n tooltipPosition=\"right\"\n (click)=\"selectMenu(child)\"\n >\n <i [class]=\"child.icon ?? 'pi pi-circle'\" aria-hidden=\"true\"></i>\n <span class=\"nav-label\">{{ child.label }}</span>\n </button>\n }\n }\n }\n </nav>\n </aside>\n\n <main class=\"content\">\n @if (heading()) {\n <h1>{{ heading() }}</h1>\n }\n @if (lead()) {\n <p class=\"lead\">{{ lead() }}</p>\n }\n\n <ng-content />\n </main>\n</div>\n\n<p-drawer\n [visible]=\"settingsOpen()\"\n (visibleChange)=\"settingsOpen.set($event)\"\n position=\"right\"\n [header]=\"t.settingsTitle\"\n [style]=\"{ width: '340px' }\"\n>\n <h3 class=\"settings-heading\">{{ t.settingsTheme }}</h3>\n <p class=\"settings-hint\">{{ t.settingsThemeHint }}</p>\n\n <p-listbox\n [options]=\"themeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n [ngModel]=\"themePreference()\"\n (ngModelChange)=\"selectTheme($event)\"\n [ariaLabel]=\"t.settingsTheme\"\n >\n <ng-template #item let-option>\n <span class=\"theme-option\">\n @if (option.swatch) {\n <span class=\"theme-swatch\" [style.background]=\"option.swatch\" aria-hidden=\"true\"></span>\n } @else {\n <i class=\"pi pi-desktop theme-swatch-icon\" aria-hidden=\"true\"></i>\n }\n <span>{{ option.label }}</span>\n </span>\n </ng-template>\n </p-listbox>\n</p-drawer>\n", styles: [":host{display:flex;flex-direction:column;min-height:100dvh;background:var(--ac-bg);color:var(--ac-text)}.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:16px;height:56px;padding:0 12px 0 8px;background:var(--ac-surface);border-bottom:1px solid var(--ac-border)}.side{display:flex;align-items:center;gap:4px}.brand{display:flex;align-items:center;gap:9px;margin-left:4px}.brand-name{display:flex;align-items:baseline;gap:7px}.brand-primary{font-size:13px;font-weight:700;letter-spacing:.15em}.brand-secondary{font-size:9px;font-weight:500;letter-spacing:.22em;color:var(--ac-text-muted)}.lang-switch{display:flex;align-items:center;gap:2px;margin-right:6px;border:1px solid var(--ac-border);border-radius:999px;padding:2px}.lang-pill{font-size:10px;font-weight:600;letter-spacing:.08em;padding:4px 10px;border:none;border-radius:999px;background:transparent;color:var(--ac-text-muted);cursor:pointer}.lang-pill.is-active{background:var(--ac-accent);color:var(--ac-on-accent)}.launcher{width:320px;padding:4px}.launcher-title{margin:0 0 10px;font-size:12px;font-weight:600;color:var(--ac-text-muted)}.launcher-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px}.launcher-item{position:relative;display:flex;flex-direction:column;align-items:center;gap:7px;padding:12px 6px;border:none;border-radius:8px;background:transparent;color:var(--ac-text);cursor:pointer}.launcher-item:hover:not(:disabled){background:var(--ac-surface-alt)}.launcher-item.is-disabled{opacity:.5;cursor:default}.launcher-icon{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:10px;color:#fff}.launcher-icon i{font-size:18px}.launcher-name{font-size:11px;font-weight:500;line-height:1.3;text-align:center}.launcher-badge{font-size:9px;font-weight:500;color:var(--ac-text-muted)}.account{width:300px;padding:4px;text-align:center}.account-email{margin:0 0 12px;font-size:12px;color:var(--ac-text-muted)}.account-head{display:flex;flex-direction:column;align-items:center;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--ac-border)}.account-photo{width:72px;height:72px;border-radius:999px;object-fit:cover}.account-initials{display:inline-flex;align-items:center;justify-content:center;background:var(--ac-accent);color:var(--ac-on-accent);font-size:26px;font-weight:600}.account-hi{margin:0;font-size:16px;font-weight:500}.account-actions{display:flex;flex-direction:column;gap:2px;padding-top:8px}.body{display:flex;flex:1;min-height:0}.sidebar{flex:none;width:224px;padding:12px 10px;background:var(--ac-surface);border-right:1px solid var(--ac-border);transition:width .18s ease}.sidebar.is-collapsed{width:60px}.sidebar nav{display:flex;flex-direction:column;gap:3px}.nav-item{display:flex;align-items:center;gap:12px;width:100%;padding:9px 12px;border:none;border-radius:6px;background:transparent;color:var(--ac-text-muted);font-size:13px;font-weight:500;text-align:left;white-space:nowrap;cursor:pointer}.nav-item i{flex:none;font-size:15px}.nav-item:hover{background:var(--ac-surface-alt);color:var(--ac-text)}.nav-item.is-active{background:color-mix(in srgb,var(--ac-accent) 18%,transparent);color:var(--ac-text)}.nav-item.is-active i{color:var(--ac-accent)}.sidebar.is-collapsed .nav-label{display:none}.sidebar.is-collapsed .nav-item{justify-content:center;padding-inline:0}.content{flex:1;min-width:0;padding:28px 28px 64px;display:flex;flex-direction:column;gap:14px}h1{margin:0;font-size:26px;line-height:1.2;font-weight:500;letter-spacing:-.02em}.lead{margin:0;font-size:14px;line-height:1.6;font-weight:300;color:var(--ac-text-muted)}.settings-heading{margin:0 0 4px;font-size:14px;font-weight:600}.settings-hint{margin:0 0 14px;font-size:12px;font-weight:300;color:var(--ac-text-muted)}.theme-option{display:flex;align-items:center;gap:10px}.theme-swatch{flex:none;width:16px;height:16px;border-radius:3px;border:1px solid var(--ac-border)}.theme-swatch-icon{flex:none;width:16px;font-size:14px;color:var(--ac-text-muted)}.access-menu{display:flex;flex-direction:column;gap:.15rem;margin-top:.75rem;padding-top:.75rem;border-top:1px solid color-mix(in srgb,currentColor 12%,transparent)}.access-caption{margin:0 0 .4rem;padding:0 .75rem;font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;opacity:.65;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.access-state{margin:0;padding:.35rem .75rem;font-size:.78rem;opacity:.7}.access-state.is-error{color:var(--p-red-400, #f87171);opacity:1}.nav-item.is-child{padding-left:2.1rem;font-size:.86rem}.launcher-initials{font-size:.85rem;font-weight:600;letter-spacing:.02em;line-height:1;color:#fff}.launcher-empty{margin:.25rem 0 .5rem;font-size:.82rem;opacity:.7}@media(max-width:720px){.sidebar{width:60px}.sidebar .nav-label{display:none}.sidebar .nav-item{justify-content:center;padding-inline:0}.brand-secondary{display:none}.content{padding:20px 16px 48px}h1{font-size:22px}.launcher{width:260px}}\n"] }]
1096
+ }], propDecorators: { brand: [{ type: i0.Input, args: [{ isSignal: true, alias: "brand", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], lead: [{ type: i0.Input, args: [{ isSignal: true, alias: "lead", required: false }] }], navItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "navItems", required: false }] }], activeNavId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeNavId", required: false }] }], selectedMenuId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedMenuId", required: false }] }], navSelect: [{ type: i0.Output, args: ["navSelect"] }], menuSelect: [{ type: i0.Output, args: ["menuSelect"] }] } });
1097
+ function initialsOf(source) {
1098
+ const parts = source.trim().split(/\s+/).filter(Boolean);
1099
+ if (!parts.length)
1100
+ return '?';
1101
+ if (parts.length === 1)
1102
+ return parts[0].slice(0, 2).toUpperCase();
1103
+ return (parts[0][0] + parts[1][0]).toUpperCase();
1104
+ }
1105
+
1106
+ /**
1107
+ * Generated bundle index. Do not edit.
1108
+ */
1109
+
1110
+ export { DEFAULT_THEME, NUCLEUS_CONFIG, NUCLEUS_DARK_SELECTOR, NUCLEUS_FORCE_BEARER, NUCLEUS_LANGS, NUCLEUS_SKIP_AUTH, NucleusAuth, NucleusLanguage, NucleusMark, NucleusPreset, NucleusSessionAuth, NucleusShell, NucleusStarfield, SHELL_COPY, SessionService, THEMES, THEME_LABELS, ThemeService, UserSyncService, isNucleusConfigured, isThemePreference, loadNucleusConfig, nucleusAuthInterceptor, ownsUrl, provideNucleus, provideNucleusTheme, themeById };
1111
+ //# sourceMappingURL=nucleus-suite-pe-core.mjs.map