@trismegisto/spine 2.0.1 → 2.1.1
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/README.md +35 -5
- package/fesm2022/trismegisto-spine-audit.mjs +1 -1
- package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-ClMKB9cK.mjs +48 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-ClMKB9cK.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-Cf5SG-oQ.mjs +55 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-Cf5SG-oQ.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-audit.technical-C901aIIQ.mjs +42 -0
- package/fesm2022/trismegisto-spine-auto-service-audit.technical-C901aIIQ.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-audit.usage-GIJ2RwuH.mjs +63 -0
- package/fesm2022/trismegisto-spine-auto-service-audit.usage-GIJ2RwuH.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-C7SPavrX.mjs +784 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-C7SPavrX.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-6Bk46MOf.mjs +39 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-6Bk46MOf.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-B5tyJ2zY.mjs → trismegisto-spine-auto-service-auto-service.component-BLN49F4a.mjs} +32 -7
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-BLN49F4a.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-client.technical-BHWhpyWv.mjs +46 -0
- package/fesm2022/trismegisto-spine-auto-service-client.technical-BHWhpyWv.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-client.usage-CM2WB2e2.mjs +80 -0
- package/fesm2022/trismegisto-spine-auto-service-client.usage-CM2WB2e2.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-jQlwaQPn.mjs +39 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-jQlwaQPn.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Bh7ig2zZ.mjs +54 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Bh7ig2zZ.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs +35 -0
- package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-home.usage-D6cb4B3Z.mjs +43 -0
- package/fesm2022/trismegisto-spine-auto-service-home.usage-D6cb4B3Z.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service.mjs +82 -40
- package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-layout.mjs +988 -46
- package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-shell.mjs +3 -1
- package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
- package/fesm2022/trismegisto-spine.mjs +591 -16
- package/fesm2022/trismegisto-spine.mjs.map +1 -1
- package/package.json +6 -11
- package/types/trismegisto-spine-auto-service.d.ts +41 -2
- package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
- package/types/trismegisto-spine-layout.d.ts +214 -13
- package/types/trismegisto-spine-layout.d.ts.map +1 -1
- package/types/trismegisto-spine-shell.d.ts +1 -0
- package/types/trismegisto-spine-shell.d.ts.map +1 -1
- package/types/trismegisto-spine.d.ts +214 -8
- package/types/trismegisto-spine.d.ts.map +1 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-BYkUOwWL.mjs +0 -595
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-BYkUOwWL.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-B5tyJ2zY.mjs.map +0 -1
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, ChangeDetectorRef, ChangeDetectionStrategy, Component, InjectionToken, input, signal, computed, Optional, Inject, Injectable, Injector, afterNextRender, ViewChild, effect, HostListener, DOCUMENT, isDevMode, DestroyRef, output, ElementRef,
|
|
2
|
+
import { inject, ChangeDetectorRef, ChangeDetectionStrategy, Component, InjectionToken, input, signal, computed, Optional, Inject, Injectable, Injector, afterNextRender, ViewChild, effect, HostListener, DOCUMENT, Directive, isDevMode, DestroyRef, output, ElementRef, ViewEncapsulation } from '@angular/core';
|
|
3
3
|
import * as i3 from '@angular/router';
|
|
4
4
|
import { RouterLink, Router, NavigationEnd, NavigationSkipped, RouterOutlet } from '@angular/router';
|
|
5
5
|
import { BehaviorSubject, filter, take, Subject, firstValueFrom } from 'rxjs';
|
|
6
|
-
import { IconComponent, IconButtonComponent, LoadingSquareComponent, HermesShortcutService, HERMES_MESSAGES, HeaderComponent as HeaderComponent$1, InputComponent, MenuButtonComponent, AccountAvatarComponent, ModalComponent, createPersistedWorkspaceStore, HERMES_WORKSPACE_PERSISTENCE_CONFIG, findPanelTabs, SidebarComponent as SidebarComponent$1, SidebarNodeComponent, ButtonComponent, CarouselComponent, ShortcutListComponent, HermesToastService, SegmentedControlComponent, HermesThemeService, HermesSidebarService, ComboboxComponent, SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent, SelectComponent, ThemePickerComponent, BreadcrumbComponent, TooltipDirective,
|
|
6
|
+
import { IconComponent, IconButtonComponent, LoadingSquareComponent, HermesShortcutService, HERMES_MESSAGES, HeaderComponent as HeaderComponent$1, InputComponent, MenuButtonComponent, AccountAvatarComponent, ModalComponent, createPersistedWorkspaceStore, HERMES_WORKSPACE_PERSISTENCE_CONFIG, findPanelTabs, SidebarComponent as SidebarComponent$1, SidebarNodeComponent, ButtonComponent, CarouselComponent, ShortcutListComponent, CodeInputComponent, RadioComponent, AlertComponent, HermesToastService, TagComponent, SegmentedControlComponent, HermesThemeService, HermesSidebarService, ComboboxComponent, SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent, SelectComponent, ThemePickerComponent, BreadcrumbComponent, TooltipDirective, SkeletonComponent, WorkspaceComponent } from '@trismegisto/angular';
|
|
7
7
|
import { ModalComponent as ModalComponent$1 } from '@trismegisto/spine/modal';
|
|
8
8
|
import { NotificationComponent } from '@trismegisto/spine/notification';
|
|
9
9
|
import { SidePanelComponent } from '@trismegisto/spine/side-panel';
|
|
10
10
|
import * as i2$1 from '@trismegisto/spine';
|
|
11
|
-
import { SPINE_MESSAGES, SPINE_CONFIG, ThemeService, resolveSpineLogo, SPINE_NEWS, spineRoutesOf, routerIdOf,
|
|
11
|
+
import { SPINE_MESSAGES, SPINE_CONFIG, ThemeService, resolveSpineLogo, SPINE_NEWS, SpineUserStorage, SPINE_USER_STORAGE_KEYS, spineRoutesOf, routerIdOf, SpinePreferencesService, SPINE_ADMIN_API, ThemePresetService, DensityService, APP_PACKAGE_INFO, SpineQuickAccessService } from '@trismegisto/spine';
|
|
12
12
|
import * as i2 from '@angular/common';
|
|
13
13
|
import { CommonModule, NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
|
|
14
14
|
import * as i1$1 from '@angular/forms';
|
|
15
15
|
import { FormsModule, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
16
16
|
import * as i1 from '@orfeu/angular/core';
|
|
17
|
-
import { ViewService, AuthorizationService, RealmAppsService } from '@orfeu/angular/core';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import { ViewService, AuthorizationService, RealmAppsService, AUTH_CONFIG } from '@orfeu/angular/core';
|
|
18
|
+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
19
|
+
import { formatMessage, pluralMessage, urlPath, formatMessageHtml, HERMES_FONTS } from '@trismegisto/primitives';
|
|
20
20
|
import { resolveEffectiveRouteChain, ComponentDistributorService, componentRouterId, LoggerService } from '@trismegisto/spine/routing';
|
|
21
21
|
import { renderIconSvg } from '@trismegisto/icons';
|
|
22
|
-
import { HttpClient,
|
|
22
|
+
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
|
23
23
|
import { Title, DomSanitizer } from '@angular/platform-browser';
|
|
24
|
-
import DOMPurify from 'dompurify';
|
|
25
24
|
import { Marked } from 'marked';
|
|
25
|
+
import DOMPurify from 'dompurify';
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* A grade de apps não cabe no `MenuItem` de linha única do `hm-menu` (ícone grande + nome
|
|
@@ -179,6 +179,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
179
179
|
|
|
180
180
|
// Entra na chave de "já visto": trocar a versão faz o tour aparecer de novo para todo mundo.
|
|
181
181
|
const ONBOARDING_VERSION = 'v3';
|
|
182
|
+
// Espera antes de procurar os alvos na entrada automática: header/breadcrumb renderizados.
|
|
183
|
+
const START_DELAY_MS = 700;
|
|
182
184
|
// Partes do shell chegam depois do login (abas, barra de página): o tour espera por elas, mas não
|
|
183
185
|
// para sempre. Os passos cujo alvo não apareceu no prazo ficam de fora.
|
|
184
186
|
const TARGET_WAIT_MS = 3000;
|
|
@@ -203,10 +205,15 @@ function findTourTarget(selector) {
|
|
|
203
205
|
* Aparece uma vez por máquina (flag em localStorage), namespaced por cliente.
|
|
204
206
|
*/
|
|
205
207
|
class OnboardingService {
|
|
208
|
+
/** Fase atual, síncrona. */
|
|
209
|
+
get state() { return this.stateSubject.value; }
|
|
206
210
|
constructor(config) {
|
|
207
211
|
this.text = inject(SPINE_MESSAGES).tour;
|
|
208
212
|
/** Passo atual (null = tour inativo). O overlay component reage a isto. */
|
|
209
213
|
this.current$ = new BehaviorSubject(null);
|
|
214
|
+
this.stateSubject = new BehaviorSubject('idle');
|
|
215
|
+
/** Fase do tour (ver `OnboardingState`). Emite o valor atual ao assinar. */
|
|
216
|
+
this.state$ = this.stateSubject.asObservable();
|
|
210
217
|
this.steps = [];
|
|
211
218
|
this.index = 0;
|
|
212
219
|
this.waiting = null;
|
|
@@ -227,16 +234,34 @@ class OnboardingService {
|
|
|
227
234
|
}
|
|
228
235
|
catch { /* sem localStorage → não insiste */ }
|
|
229
236
|
}
|
|
230
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* Dispara o tour só se ainda não foi visto nesta máquina. Chamar após o login. Quando é devido, o
|
|
239
|
+
* estado vira `pending` NA HORA (síncrono), antes da espera pelo shell — é isso que segura as
|
|
240
|
+
* novidades; quando não é, continua `idle`. Chamar de novo com o tour já em andamento não faz nada.
|
|
241
|
+
*/
|
|
231
242
|
maybeStart() {
|
|
232
|
-
if (this.done)
|
|
243
|
+
if (this.done || this.state !== 'idle')
|
|
233
244
|
return;
|
|
234
|
-
|
|
245
|
+
this.setState('pending');
|
|
246
|
+
this.clearWaiting();
|
|
247
|
+
this.waiting = setTimeout(() => this.start(), START_DELAY_MS);
|
|
235
248
|
}
|
|
236
|
-
/** Reabre o tour manualmente (menu Ajuda), ignorando a flag. */
|
|
249
|
+
/** Reabre o tour manualmente (menu Ajuda), ignorando a flag. Também passa por `pending` → `running`. */
|
|
237
250
|
restart() {
|
|
251
|
+
this.setState('pending');
|
|
238
252
|
this.start();
|
|
239
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Interrompe o tour (pendente ou na tela) SEM marcar como visto: o automático volta na próxima entrada.
|
|
256
|
+
* Usado quando a pessoa abre outra coisa que disputaria a tela com ele (as novidades pelo menu).
|
|
257
|
+
*/
|
|
258
|
+
stop() {
|
|
259
|
+
if (this.state === 'idle')
|
|
260
|
+
return;
|
|
261
|
+
this.clearWaiting();
|
|
262
|
+
this.current$.next(null);
|
|
263
|
+
this.setState('idle');
|
|
264
|
+
}
|
|
240
265
|
/** Passos no idioma ativo (o texto é lido do catálogo ao começar o tour). Descrições com `<b>`. */
|
|
241
266
|
allSteps() {
|
|
242
267
|
const t = this.text;
|
|
@@ -262,9 +287,7 @@ class OnboardingService {
|
|
|
262
287
|
* de novo a cada `TARGET_POLL_MS`.
|
|
263
288
|
*/
|
|
264
289
|
start() {
|
|
265
|
-
|
|
266
|
-
clearTimeout(this.waiting);
|
|
267
|
-
this.waiting = null;
|
|
290
|
+
this.clearWaiting();
|
|
268
291
|
const steps = this.allSteps();
|
|
269
292
|
const present = (step) => !step.target || findTourTarget(step.target) !== null;
|
|
270
293
|
const attempt = (waited) => {
|
|
@@ -277,15 +300,26 @@ class OnboardingService {
|
|
|
277
300
|
this.index = 0;
|
|
278
301
|
if (!this.steps.length) {
|
|
279
302
|
this.markDone();
|
|
303
|
+
this.setState('idle');
|
|
280
304
|
return;
|
|
281
305
|
}
|
|
282
306
|
this.emit();
|
|
307
|
+
this.setState('running');
|
|
283
308
|
};
|
|
284
309
|
attempt(0);
|
|
285
310
|
}
|
|
286
311
|
emit() {
|
|
287
312
|
this.current$.next({ step: this.steps[this.index], index: this.index, total: this.steps.length });
|
|
288
313
|
}
|
|
314
|
+
setState(state) {
|
|
315
|
+
if (this.stateSubject.value !== state)
|
|
316
|
+
this.stateSubject.next(state);
|
|
317
|
+
}
|
|
318
|
+
clearWaiting() {
|
|
319
|
+
if (this.waiting !== null)
|
|
320
|
+
clearTimeout(this.waiting);
|
|
321
|
+
this.waiting = null;
|
|
322
|
+
}
|
|
289
323
|
next() {
|
|
290
324
|
if (this.index >= this.steps.length - 1) {
|
|
291
325
|
this.finish();
|
|
@@ -302,8 +336,10 @@ class OnboardingService {
|
|
|
302
336
|
}
|
|
303
337
|
/** Encerra o tour (concluído ou pulado) e marca como visto. */
|
|
304
338
|
finish() {
|
|
339
|
+
this.clearWaiting();
|
|
305
340
|
this.markDone();
|
|
306
341
|
this.current$.next(null);
|
|
342
|
+
this.setState('idle');
|
|
307
343
|
}
|
|
308
344
|
get isFirst() { return this.index === 0; }
|
|
309
345
|
get isLast() { return this.index === this.steps.length - 1; }
|
|
@@ -323,13 +359,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
323
359
|
/** Intervalo entre o fim do tour (ou a entrada, sem tour) e o painel de novidades, para os dois não se
|
|
324
360
|
* emendarem na tela. */
|
|
325
361
|
const AFTER_TOUR_MS = 800;
|
|
326
|
-
/** Tempo mínimo desde a entrada antes de oferecer — o tour, se devido, já começou (ele espera 700ms). */
|
|
327
|
-
const FIRST_CHECK_MS = 1500;
|
|
328
362
|
/**
|
|
329
363
|
* Painel de novidades do app (ver `provideSpineNews`). Decide o que está devido — ainda não visto
|
|
330
364
|
* NESTE navegador e ainda dentro do `until` — e abre sozinho uma vez, depois do tour de boas-vindas.
|
|
331
365
|
* O "já visto" é por app (`hermes_<clientId>_news_seen`): novidade de um app não some por ter sido
|
|
332
366
|
* vista em outro no mesmo domínio.
|
|
367
|
+
*
|
|
368
|
+
* Tour e novidades nunca dividem a tela: na entrada, as novidades esperam o tour ficar `idle` (ver
|
|
369
|
+
* `OnboardingState`); fora dela, vale a última ação explícita da pessoa — o tour reaberto pelo menu
|
|
370
|
+
* Ajuda fecha o painel de novidades, e as novidades abertas pelo menu interrompem o tour (sem marcá-lo
|
|
371
|
+
* como visto).
|
|
333
372
|
*/
|
|
334
373
|
class SpineNewsService {
|
|
335
374
|
constructor() {
|
|
@@ -342,16 +381,31 @@ class SpineNewsService {
|
|
|
342
381
|
this.hasAny = computed(() => this.current().length > 0, ...(ngDevMode ? [{ debugName: "hasAny" }] : /* istanbul ignore next */ []));
|
|
343
382
|
const ns = inject(SPINE_CONFIG, { optional: true })?.clientId;
|
|
344
383
|
this.seenKey = `${ns ? `hermes_${ns}` : 'hermes'}_news_seen`;
|
|
384
|
+
// O tour (re)começando — inclusive pelo menu Ajuda — fecha o painel: ele é a ação mais recente.
|
|
385
|
+
this.onboarding.state$
|
|
386
|
+
.pipe(filter((state) => state === 'pending'), takeUntilDestroyed())
|
|
387
|
+
.subscribe(() => this.close());
|
|
345
388
|
}
|
|
346
|
-
/**
|
|
389
|
+
/**
|
|
390
|
+
* Chamado uma vez pelo layout na entrada: abre as devidas quando o tour estiver `idle` (não devido,
|
|
391
|
+
* concluído, pulado ou sem passo), mais `AFTER_TOUR_MS` para não emendar. Se o tour começar nesse
|
|
392
|
+
* intervalo, volta a esperar — por isso a ordem com `OnboardingService.maybeStart()` não importa,
|
|
393
|
+
* desde que ele seja chamado dentro desse intervalo (o layout chama os dois juntos, tour primeiro).
|
|
394
|
+
*/
|
|
347
395
|
scheduleOnEntry() {
|
|
348
396
|
if (this.due().length === 0)
|
|
349
397
|
return;
|
|
350
|
-
|
|
351
|
-
this.onboarding.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
398
|
+
const whenIdle = () => {
|
|
399
|
+
this.onboarding.state$.pipe(filter((state) => state === 'idle'), take(1)).subscribe(() => {
|
|
400
|
+
setTimeout(() => {
|
|
401
|
+
if (this.onboarding.state !== 'idle')
|
|
402
|
+
whenIdle();
|
|
403
|
+
else
|
|
404
|
+
this.showDue();
|
|
405
|
+
}, AFTER_TOUR_MS);
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
whenIdle();
|
|
355
409
|
}
|
|
356
410
|
/** Marca como vistas todas as novidades devidas e as abre num único painel. */
|
|
357
411
|
showDue() {
|
|
@@ -361,9 +415,19 @@ class SpineNewsService {
|
|
|
361
415
|
this.markSeen(due.map((i) => i.id));
|
|
362
416
|
this.open(due);
|
|
363
417
|
}
|
|
364
|
-
/**
|
|
418
|
+
/**
|
|
419
|
+
* Reabre todas as vigentes (menu do perfil), vistas ou não; as devidas entre elas passam a vistas.
|
|
420
|
+
* Interrompe o tour se ele estiver pendente ou na tela (ação mais recente da pessoa).
|
|
421
|
+
*/
|
|
365
422
|
showAll() {
|
|
366
|
-
this.
|
|
423
|
+
const all = this.current();
|
|
424
|
+
if (all.length === 0)
|
|
425
|
+
return;
|
|
426
|
+
this.onboarding.stop();
|
|
427
|
+
const due = this.due();
|
|
428
|
+
if (due.length)
|
|
429
|
+
this.markSeen(due.map((i) => i.id));
|
|
430
|
+
this.open(all);
|
|
367
431
|
}
|
|
368
432
|
close() {
|
|
369
433
|
this.isOpen.set(false);
|
|
@@ -416,6 +480,7 @@ const SPINE_SETTINGS_KEYS = {
|
|
|
416
480
|
region: 'regiao',
|
|
417
481
|
shortcuts: 'atalhos',
|
|
418
482
|
account: 'conta',
|
|
483
|
+
security: 'seguranca',
|
|
419
484
|
about: 'sobre',
|
|
420
485
|
};
|
|
421
486
|
const SPINE_SETTINGS_SECTIONS = new InjectionToken('SPINE_SETTINGS_SECTIONS');
|
|
@@ -473,7 +538,7 @@ class ShortcutService {
|
|
|
473
538
|
constructor() {
|
|
474
539
|
this.actions = {};
|
|
475
540
|
this.removers = [];
|
|
476
|
-
this.
|
|
541
|
+
this.userStorage = inject(SpineUserStorage);
|
|
477
542
|
this.hermes = inject(HermesShortcutService);
|
|
478
543
|
this.lib = inject(HERMES_MESSAGES);
|
|
479
544
|
this.text = inject(SPINE_MESSAGES).shortcuts;
|
|
@@ -579,7 +644,7 @@ class ShortcutService {
|
|
|
579
644
|
return false;
|
|
580
645
|
let url = null;
|
|
581
646
|
try {
|
|
582
|
-
const list = JSON.parse(localStorage.getItem(this.
|
|
647
|
+
const list = JSON.parse(localStorage.getItem(this.userStorage.key(SPINE_USER_STORAGE_KEYS.favorites)) || '[]');
|
|
583
648
|
const fav = Array.isArray(list) ? list[Number(digit) - 1] : null;
|
|
584
649
|
url = fav?.redirect || (typeof fav?.id === 'string' && fav.id.startsWith('/') ? fav.id : null);
|
|
585
650
|
}
|
|
@@ -608,6 +673,10 @@ function toSpineGroup(group) {
|
|
|
608
673
|
|
|
609
674
|
const MAX_RECENT_SEARCH = 6;
|
|
610
675
|
class HeaderComponent {
|
|
676
|
+
/** Por usuário (`SpineUserStorage`): `<clientId>_header_recent_searches_<sub>`; apagada no logout. */
|
|
677
|
+
get recentSearchKey() {
|
|
678
|
+
return this.userStorage.key(SPINE_USER_STORAGE_KEYS.recentSearches);
|
|
679
|
+
}
|
|
611
680
|
constructor(authService, themeService, themePresetService, densityService, viewService, router, onboarding, shortcuts, config) {
|
|
612
681
|
this.authService = authService;
|
|
613
682
|
this.themeService = themeService;
|
|
@@ -628,6 +697,7 @@ class HeaderComponent {
|
|
|
628
697
|
this.fmt = formatMessage;
|
|
629
698
|
this.settings = inject(SpineSettingsService);
|
|
630
699
|
this.news = inject(SpineNewsService);
|
|
700
|
+
this.userStorage = inject(SpineUserStorage);
|
|
631
701
|
this.quickQuery = '';
|
|
632
702
|
this.quickOpen = false;
|
|
633
703
|
this.quickResults = [];
|
|
@@ -638,8 +708,6 @@ class HeaderComponent {
|
|
|
638
708
|
/** Verdadeiro quando o painel está exibindo o histórico (campo vazio). */
|
|
639
709
|
this.showingRecent = false;
|
|
640
710
|
this.appName = config?.appName ?? 'Hermes';
|
|
641
|
-
const ns = config?.clientId ?? 'app';
|
|
642
|
-
this.recentSearchKey = `${ns}_header_recent_searches`;
|
|
643
711
|
}
|
|
644
712
|
async ngOnInit() {
|
|
645
713
|
this.updateView(this.viewService.currentView);
|
|
@@ -708,7 +776,10 @@ class HeaderComponent {
|
|
|
708
776
|
async switchBadge(badgeTypeId) {
|
|
709
777
|
await this.authService.switchBadge(badgeTypeId);
|
|
710
778
|
}
|
|
711
|
-
logout() {
|
|
779
|
+
logout() {
|
|
780
|
+
this.userStorage.clearHistory();
|
|
781
|
+
this.authService.logout();
|
|
782
|
+
}
|
|
712
783
|
/** Reabre o tour guiado de onboarding. */
|
|
713
784
|
startTour() { this.onboarding.restart(); }
|
|
714
785
|
/** Abre o painel de atalhos de teclado. */
|
|
@@ -1438,6 +1509,792 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1438
1509
|
args: [{ selector: 'spine-shortcuts-cheatsheet', imports: [ModalComponent, ButtonComponent, ShortcutListComponent], changeDetection: ChangeDetectionStrategy.Eager, template: "<hm-modal [open]=\"open\" (openChange)=\"open = $event\" [heading]=\"text.heading\" size=\"large\">\n <!-- Com a tecla \u00FAnica desligada (WCAG 2.1.4) o \"?\" n\u00E3o abre o painel: a dica some junto. -->\n @if (characterKeys()) {\n <p class=\"sc-hint\">\n {{ hintParts[0] }}<kbd class=\"sc-chip\">?</kbd>{{ hintParts[1] }}\n </p>\n }\n\n <hm-shortcut-list searchable characterKeysToggle [registered]=\"false\" [items]=\"items()\" />\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" icon=\"play\" (clicked)=\"replayTour()\">{{ text.replayTour }}</hm-button>\n <hm-button (clicked)=\"open = false\">{{ text.dismiss }}</hm-button>\n </ng-container>\n</hm-modal>\n", styles: ["@charset \"UTF-8\";.sc-hint{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);margin:0 0 var(--hm-space-md);padding:var(--hm-space-xxxs) var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.sc-chip{display:inline-flex;align-items:center;justify-content:center;min-inline-size:1.75rem;padding:0 var(--hm-space-micro);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-micro);background:var(--hm-surface-raised);color:var(--hm-text);font-family:var(--hm-font-family);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-bold);line-height:var(--hm-line-height-normal)}@media(forced-colors:active){.sc-chip{border-color:ButtonBorder;background:ButtonFace;color:ButtonText}}\n"] }]
|
|
1439
1510
|
}] });
|
|
1440
1511
|
|
|
1512
|
+
/**
|
|
1513
|
+
* Base da API da conta no Argos: `{authority}/api/realms/{realm}/account` — o mesmo `{authority}/api` que o
|
|
1514
|
+
* `@orfeu/angular` usa para o OIDC (a `authority` já traz o contexto do serviço, ex.: `https://host/argos`).
|
|
1515
|
+
* Sem `authority`/`realm` (ou com o `CHANGE_ME` de um environment ainda não preenchido) devolve `null`.
|
|
1516
|
+
*/
|
|
1517
|
+
function argosAccountApi(authority, realm) {
|
|
1518
|
+
const realmId = realm?.trim();
|
|
1519
|
+
if (authority == null || !realmId || authority.includes('CHANGE_ME') || realmId.includes('CHANGE_ME'))
|
|
1520
|
+
return null;
|
|
1521
|
+
return `${authority.trim().replace(/\/+$/, '')}/api/realms/${encodeURIComponent(realmId)}/account`;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Conta do usuário no Argos (verificação em duas etapas, códigos de recuperação, sessões) pela API da conta
|
|
1525
|
+
* com Bearer: o access token da sessão do app (`@orfeu/angular`), no header `Authorization` — o mesmo jeito
|
|
1526
|
+
* que o Orfeu fala com o Argos (ex.: `switchBadge`). O Argos reconhece a sessão SSO pelo `sid` do token; não
|
|
1527
|
+
* há cookie envolvido. Token vencido (`401 invalid_token`): renova uma vez (`silentRenew`) e repete.
|
|
1528
|
+
* `available` é falso sem `AUTH_CONFIG` utilizável.
|
|
1529
|
+
*/
|
|
1530
|
+
class SpineAccountSecurityService {
|
|
1531
|
+
constructor() {
|
|
1532
|
+
this.http = inject(HttpClient);
|
|
1533
|
+
this.auth = inject(AuthorizationService);
|
|
1534
|
+
this.config = inject(AUTH_CONFIG, { optional: true });
|
|
1535
|
+
this.baseUrl = argosAccountApi(this.config?.authority, this.config?.realmId);
|
|
1536
|
+
this.available = !!this.baseUrl;
|
|
1537
|
+
}
|
|
1538
|
+
overview() {
|
|
1539
|
+
return this.send('GET', '/security');
|
|
1540
|
+
}
|
|
1541
|
+
reauthenticate(proof) {
|
|
1542
|
+
return this.send('POST', '/reauth', proof);
|
|
1543
|
+
}
|
|
1544
|
+
requestReauthCode(methodId) {
|
|
1545
|
+
return this.send('POST', '/reauth/request', { method_id: methodId });
|
|
1546
|
+
}
|
|
1547
|
+
configure(methodId, email) {
|
|
1548
|
+
return this.send('POST', '/mfa/configure', email ? { method_id: methodId, email } : { method_id: methodId });
|
|
1549
|
+
}
|
|
1550
|
+
register(methodId, code) {
|
|
1551
|
+
return this.send('POST', '/mfa/register', { method_id: methodId, code });
|
|
1552
|
+
}
|
|
1553
|
+
remove(methodId) {
|
|
1554
|
+
return this.send('POST', '/mfa/remove', { method_id: methodId });
|
|
1555
|
+
}
|
|
1556
|
+
regenerateRecoveryCodes() {
|
|
1557
|
+
return this.send('POST', '/recovery-codes', {});
|
|
1558
|
+
}
|
|
1559
|
+
sessions() {
|
|
1560
|
+
return this.send('GET', '/sessions');
|
|
1561
|
+
}
|
|
1562
|
+
endSession(sid) {
|
|
1563
|
+
return this.send('DELETE', `/sessions/${encodeURIComponent(sid)}`);
|
|
1564
|
+
}
|
|
1565
|
+
endOtherSessions() {
|
|
1566
|
+
return this.send('DELETE', '/sessions/others');
|
|
1567
|
+
}
|
|
1568
|
+
async send(method, path, body) {
|
|
1569
|
+
try {
|
|
1570
|
+
return await this.request(method, path, body);
|
|
1571
|
+
}
|
|
1572
|
+
catch (err) {
|
|
1573
|
+
if (!(err instanceof HttpErrorResponse && err.status === 401 && err.error?.error === 'invalid_token'))
|
|
1574
|
+
throw err;
|
|
1575
|
+
await this.auth.silentRenew();
|
|
1576
|
+
return this.request(method, path, body);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
request(method, path, body) {
|
|
1580
|
+
if (!this.baseUrl)
|
|
1581
|
+
return Promise.reject(new Error('[hermes-spine] API da conta sem AUTH_CONFIG (authority/realmId).'));
|
|
1582
|
+
const headers = new HttpHeaders({ Authorization: `Bearer ${this.auth.getToken() ?? ''}` });
|
|
1583
|
+
return firstValueFrom(this.http.request(method, `${this.baseUrl}${path}`, { body, headers }));
|
|
1584
|
+
}
|
|
1585
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountSecurityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1586
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountSecurityService, providedIn: 'root' }); }
|
|
1587
|
+
}
|
|
1588
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountSecurityService, decorators: [{
|
|
1589
|
+
type: Injectable,
|
|
1590
|
+
args: [{ providedIn: 'root' }]
|
|
1591
|
+
}] });
|
|
1592
|
+
/** Código de erro que o Argos devolveu (`{ error: 'invalid_code' }`), ou `''`. */
|
|
1593
|
+
function accountErrorCode(err) {
|
|
1594
|
+
if (!(err instanceof HttpErrorResponse))
|
|
1595
|
+
return '';
|
|
1596
|
+
const code = err.error?.error;
|
|
1597
|
+
return typeof code === 'string' ? code : err.status === 0 ? 'network' : '';
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/** Métodos que o painel trata (o Argos só tem fluxo para estes; o SMS aparece como indisponível). */
|
|
1601
|
+
const SUPPORTED = ['totp', 'email', 'sms'];
|
|
1602
|
+
const nameOf = (m) => m?.method_name?.toLowerCase() ?? '';
|
|
1603
|
+
const isTotp = (m) => nameOf(m) === 'totp';
|
|
1604
|
+
const isEmail = (m) => nameOf(m) === 'email';
|
|
1605
|
+
const isSms = (m) => nameOf(m) === 'sms';
|
|
1606
|
+
/**
|
|
1607
|
+
* Estado da seção "Conta e segurança" do painel de configurações: visão geral do Argos e o fluxo de uma
|
|
1608
|
+
* alteração (reautenticação → cadastro/remoção/códigos). Mesmo roteiro da tela "Segurança da conta" do
|
|
1609
|
+
* Argos: toda alteração pede antes a prova de identidade recente (senha ou um fator ativo, 5 minutos no
|
|
1610
|
+
* servidor); a recusa `reauthentication_required` no meio volta para a prova e a ação segue depois dela.
|
|
1611
|
+
*
|
|
1612
|
+
* Uma instância por abertura do painel (`spineAccountSecurity` no `hm-settings-section`): os códigos de
|
|
1613
|
+
* recuperação e o QR Code somem quando o painel fecha.
|
|
1614
|
+
*/
|
|
1615
|
+
class SpineAccountSecurityState {
|
|
1616
|
+
constructor() {
|
|
1617
|
+
this.api = inject(SpineAccountSecurityService);
|
|
1618
|
+
this.available = this.api.available;
|
|
1619
|
+
this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1620
|
+
/** Código do erro ao carregar (`login_required`, `network`...); `''` = carregou. */
|
|
1621
|
+
this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
|
|
1622
|
+
this.overview = signal(null, ...(ngDevMode ? [{ debugName: "overview" }] : /* istanbul ignore next */ []));
|
|
1623
|
+
this.step = signal('idle', ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
|
|
1624
|
+
this.action = signal(null, ...(ngDevMode ? [{ debugName: "action" }] : /* istanbul ignore next */ []));
|
|
1625
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
1626
|
+
/** Código do último erro de uma ação (`invalid_code`...). */
|
|
1627
|
+
this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1628
|
+
this.notice = signal(null, ...(ngDevMode ? [{ debugName: "notice" }] : /* istanbul ignore next */ []));
|
|
1629
|
+
/** Como provar a identidade: `password`, o id de um método ativo ou o id do código de recuperação. */
|
|
1630
|
+
this.reauthMode = signal('password', ...(ngDevMode ? [{ debugName: "reauthMode" }] : /* istanbul ignore next */ []));
|
|
1631
|
+
this.reauthCodeSent = signal(false, ...(ngDevMode ? [{ debugName: "reauthCodeSent" }] : /* istanbul ignore next */ []));
|
|
1632
|
+
/** Até quando (epoch ms) a reautenticação vale, pelo que o servidor informou. */
|
|
1633
|
+
this.reauthUntil = 0;
|
|
1634
|
+
this.totpSetup = signal(null, ...(ngDevMode ? [{ debugName: "totpSetup" }] : /* istanbul ignore next */ []));
|
|
1635
|
+
this.enrollEmail = signal('', ...(ngDevMode ? [{ debugName: "enrollEmail" }] : /* istanbul ignore next */ []));
|
|
1636
|
+
/** Códigos de recuperação recém-gerados: só existem aqui, até a pessoa dizer que guardou. */
|
|
1637
|
+
this.issuedCodes = signal(null, ...(ngDevMode ? [{ debugName: "issuedCodes" }] : /* istanbul ignore next */ []));
|
|
1638
|
+
this.methods = computed(() => (this.overview()?.methods ?? []).filter((m) => SUPPORTED.includes(nameOf(m))), ...(ngDevMode ? [{ debugName: "methods" }] : /* istanbul ignore next */ []));
|
|
1639
|
+
this.activeMethods = computed(() => this.methods().filter((m) => m.active), ...(ngDevMode ? [{ debugName: "activeMethods" }] : /* istanbul ignore next */ []));
|
|
1640
|
+
this.remainingCodes = computed(() => this.overview()?.recovery_codes.remaining ?? 0, ...(ngDevMode ? [{ debugName: "remainingCodes" }] : /* istanbul ignore next */ []));
|
|
1641
|
+
this.recoveryMethodId = computed(() => this.overview()?.recovery_method_id ?? '', ...(ngDevMode ? [{ debugName: "recoveryMethodId" }] : /* istanbul ignore next */ []));
|
|
1642
|
+
/** Linha da última ação (métodos ou códigos): é ela que mostra o fluxo, o erro e o aviso. */
|
|
1643
|
+
this.owner = signal('methods', ...(ngDevMode ? [{ debugName: "owner" }] : /* istanbul ignore next */ []));
|
|
1644
|
+
if (this.available)
|
|
1645
|
+
void this.load();
|
|
1646
|
+
}
|
|
1647
|
+
async load() {
|
|
1648
|
+
this.loading.set(true);
|
|
1649
|
+
try {
|
|
1650
|
+
this.apply(await this.api.overview());
|
|
1651
|
+
this.loadError.set('');
|
|
1652
|
+
}
|
|
1653
|
+
catch (err) {
|
|
1654
|
+
this.loadError.set(accountErrorCode(err) || 'generic');
|
|
1655
|
+
}
|
|
1656
|
+
finally {
|
|
1657
|
+
this.loading.set(false);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
/** Com MFA obrigatório, o último método ativo não sai (o servidor também recusa: `mfa_last_method`). */
|
|
1661
|
+
canRemove(method) {
|
|
1662
|
+
return !(this.overview()?.policy.required && this.activeMethods().length <= 1 && method.active);
|
|
1663
|
+
}
|
|
1664
|
+
// ── Início de uma ação: reautentica se preciso, depois segue ───────────────
|
|
1665
|
+
start(action) {
|
|
1666
|
+
this.error.set('');
|
|
1667
|
+
this.notice.set(null);
|
|
1668
|
+
this.issuedCodes.set(null);
|
|
1669
|
+
this.action.set(action);
|
|
1670
|
+
this.owner.set(action.kind === 'codes' ? 'codes' : 'methods');
|
|
1671
|
+
if (Date.now() < this.reauthUntil)
|
|
1672
|
+
void this.run(() => this.proceed());
|
|
1673
|
+
else
|
|
1674
|
+
this.openReauth();
|
|
1675
|
+
}
|
|
1676
|
+
cancel() {
|
|
1677
|
+
this.action.set(null);
|
|
1678
|
+
this.step.set('idle');
|
|
1679
|
+
this.error.set('');
|
|
1680
|
+
this.totpSetup.set(null);
|
|
1681
|
+
// Códigos de recuperação não ficam na memória da tela depois que o fluxo é abandonado.
|
|
1682
|
+
this.issuedCodes.set(null);
|
|
1683
|
+
}
|
|
1684
|
+
openReauth() {
|
|
1685
|
+
this.reauthCodeSent.set(false);
|
|
1686
|
+
this.reauthMode.set('password');
|
|
1687
|
+
this.step.set('reauth');
|
|
1688
|
+
}
|
|
1689
|
+
chooseReauth(mode) {
|
|
1690
|
+
this.error.set('');
|
|
1691
|
+
this.notice.set(null);
|
|
1692
|
+
this.reauthCodeSent.set(false);
|
|
1693
|
+
this.reauthMode.set(mode);
|
|
1694
|
+
}
|
|
1695
|
+
sendReauthCode() {
|
|
1696
|
+
return this.run(async () => {
|
|
1697
|
+
await this.api.requestReauthCode(this.reauthMode());
|
|
1698
|
+
this.reauthCodeSent.set(true);
|
|
1699
|
+
this.notice.set('reauthCodeSent');
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
confirmReauth(proof) {
|
|
1703
|
+
return this.run(async () => {
|
|
1704
|
+
const response = await this.api.reauthenticate(proof);
|
|
1705
|
+
this.reauthUntil = Date.now() + (response?.reauth_expires_in ?? 0) * 1000;
|
|
1706
|
+
this.notice.set(null);
|
|
1707
|
+
await this.proceed();
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
// ── Depois da reautenticação (sempre de dentro de run()) ───────────────────
|
|
1711
|
+
async proceed() {
|
|
1712
|
+
const action = this.action();
|
|
1713
|
+
if (!action)
|
|
1714
|
+
return;
|
|
1715
|
+
if (action.kind === 'remove') {
|
|
1716
|
+
this.step.set('confirm-remove');
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
if (action.kind === 'codes') {
|
|
1720
|
+
const result = await this.api.regenerateRecoveryCodes();
|
|
1721
|
+
this.issuedCodes.set(result.recovery_codes);
|
|
1722
|
+
this.step.set('codes');
|
|
1723
|
+
await this.refresh();
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
if (isTotp(action.method)) {
|
|
1727
|
+
const setup = await this.api.configure(action.method.method_id);
|
|
1728
|
+
this.totpSetup.set({ secret: setup.secret ?? '', qrcode: setup.qrcode ?? '' });
|
|
1729
|
+
this.step.set('enroll-totp');
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
this.step.set('enroll-email');
|
|
1733
|
+
}
|
|
1734
|
+
sendEnrollEmail(email) {
|
|
1735
|
+
const action = this.action();
|
|
1736
|
+
if (!action || action.kind === 'codes')
|
|
1737
|
+
return Promise.resolve();
|
|
1738
|
+
return this.run(async () => {
|
|
1739
|
+
await this.api.configure(action.method.method_id, email);
|
|
1740
|
+
this.enrollEmail.set(email);
|
|
1741
|
+
this.step.set('enroll-email-code');
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
confirmEnroll(code) {
|
|
1745
|
+
const action = this.action();
|
|
1746
|
+
if (!action || action.kind === 'codes' || action.kind === 'remove' || !/^\d{6}$/.test(code))
|
|
1747
|
+
return Promise.resolve();
|
|
1748
|
+
return this.run(async () => {
|
|
1749
|
+
const overview = await this.api.register(action.method.method_id, code);
|
|
1750
|
+
this.apply(overview);
|
|
1751
|
+
this.totpSetup.set(null);
|
|
1752
|
+
if (overview.recovery_codes_issued?.length) {
|
|
1753
|
+
// Primeiro método: os códigos aparecem uma vez, na linha deles.
|
|
1754
|
+
this.action.set({ kind: 'codes' });
|
|
1755
|
+
this.owner.set('codes');
|
|
1756
|
+
this.issuedCodes.set(overview.recovery_codes_issued);
|
|
1757
|
+
this.step.set('codes');
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
this.finish(action.kind === 'replace' ? 'deviceReplaced' : 'methodAdded');
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
confirmRemove() {
|
|
1764
|
+
const action = this.action();
|
|
1765
|
+
if (!action || action.kind !== 'remove')
|
|
1766
|
+
return Promise.resolve();
|
|
1767
|
+
return this.run(async () => {
|
|
1768
|
+
this.apply(await this.api.remove(action.method.method_id));
|
|
1769
|
+
this.finish('methodRemoved');
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
codesSaved() {
|
|
1773
|
+
this.issuedCodes.set(null);
|
|
1774
|
+
this.finish('codesRegenerated');
|
|
1775
|
+
}
|
|
1776
|
+
finish(notice) {
|
|
1777
|
+
this.action.set(null);
|
|
1778
|
+
this.step.set('idle');
|
|
1779
|
+
this.notice.set(notice);
|
|
1780
|
+
}
|
|
1781
|
+
apply(overview) {
|
|
1782
|
+
this.overview.set(overview);
|
|
1783
|
+
if (overview.reauthenticated)
|
|
1784
|
+
this.reauthUntil = Date.now() + overview.reauth_expires_in * 1000;
|
|
1785
|
+
}
|
|
1786
|
+
async refresh() {
|
|
1787
|
+
try {
|
|
1788
|
+
this.apply(await this.api.overview());
|
|
1789
|
+
}
|
|
1790
|
+
catch {
|
|
1791
|
+
// A ação já foi feita; a contagem volta certa na próxima abertura do painel.
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
/** Executa uma chamada com o estado de ocupado e o erro na tela. A reautenticação vencida no meio volta
|
|
1795
|
+
* para a prova de identidade (e a ação continua depois dela). */
|
|
1796
|
+
async run(fn) {
|
|
1797
|
+
if (this.busy())
|
|
1798
|
+
return;
|
|
1799
|
+
this.busy.set(true);
|
|
1800
|
+
this.error.set('');
|
|
1801
|
+
try {
|
|
1802
|
+
await fn();
|
|
1803
|
+
}
|
|
1804
|
+
catch (err) {
|
|
1805
|
+
const code = accountErrorCode(err) || 'generic';
|
|
1806
|
+
if (code === 'reauthentication_required') {
|
|
1807
|
+
this.reauthUntil = 0;
|
|
1808
|
+
this.openReauth();
|
|
1809
|
+
}
|
|
1810
|
+
this.error.set(code);
|
|
1811
|
+
}
|
|
1812
|
+
finally {
|
|
1813
|
+
this.busy.set(false);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountSecurityState, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1817
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountSecurityState }); }
|
|
1818
|
+
}
|
|
1819
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountSecurityState, decorators: [{
|
|
1820
|
+
type: Injectable
|
|
1821
|
+
}], ctorParameters: () => [] });
|
|
1822
|
+
/**
|
|
1823
|
+
* Escopo do estado da conta: no `hm-settings-section` da "Conta e segurança", para as linhas da seção
|
|
1824
|
+
* (métodos e códigos de recuperação) dividirem o mesmo estado e ele morrer junto com o painel.
|
|
1825
|
+
*/
|
|
1826
|
+
class SpineAccountSecurityScopeDirective {
|
|
1827
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountSecurityScopeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1828
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.24", type: SpineAccountSecurityScopeDirective, isStandalone: true, selector: "[spineAccountSecurity]", providers: [SpineAccountSecurityState], ngImport: i0 }); }
|
|
1829
|
+
}
|
|
1830
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountSecurityScopeDirective, decorators: [{
|
|
1831
|
+
type: Directive,
|
|
1832
|
+
args: [{
|
|
1833
|
+
selector: '[spineAccountSecurity]',
|
|
1834
|
+
providers: [SpineAccountSecurityState],
|
|
1835
|
+
}]
|
|
1836
|
+
}] });
|
|
1837
|
+
|
|
1838
|
+
let sequence$3 = 0;
|
|
1839
|
+
/**
|
|
1840
|
+
* Prova de identidade antes de uma alteração na conta (senha, código do autenticador, código enviado ao
|
|
1841
|
+
* e-mail cadastrado ou código de recuperação). Vale 5 minutos no Argos, presa à sessão do login; depois
|
|
1842
|
+
* dela a ação pedida continua sozinha (`SpineAccountSecurityState.confirmReauth`).
|
|
1843
|
+
*/
|
|
1844
|
+
class AccountReauthComponent {
|
|
1845
|
+
constructor() {
|
|
1846
|
+
this.state = inject(SpineAccountSecurityState);
|
|
1847
|
+
this.t = inject(SPINE_MESSAGES).accountSecurity;
|
|
1848
|
+
this.titleId = `sp-account-reauth-${++sequence$3}`;
|
|
1849
|
+
this.codeLabelId = `${this.titleId}-code`;
|
|
1850
|
+
this.password = new FormControl('', { nonNullable: true, validators: [Validators.required] });
|
|
1851
|
+
this.recoveryCode = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.minLength(10)] });
|
|
1852
|
+
/** Senha, os métodos ativos que servem de prova (o SMS não tem envio) e, com códigos restantes, um código de recuperação. */
|
|
1853
|
+
this.choices = computed(() => {
|
|
1854
|
+
const options = [{ value: 'password', label: this.t.reauthPassword }];
|
|
1855
|
+
for (const m of this.state.activeMethods()) {
|
|
1856
|
+
if (isSms(m))
|
|
1857
|
+
continue;
|
|
1858
|
+
options.push({ value: m.method_id, label: isTotp(m) ? this.t.methodTotp : isEmail(m) ? this.t.methodEmail : m.method_name });
|
|
1859
|
+
}
|
|
1860
|
+
if (this.state.activeMethods().length && this.state.remainingCodes() > 0) {
|
|
1861
|
+
options.push({ value: this.state.recoveryMethodId(), label: this.t.reauthRecoveryCode });
|
|
1862
|
+
}
|
|
1863
|
+
return options;
|
|
1864
|
+
}, ...(ngDevMode ? [{ debugName: "choices" }] : /* istanbul ignore next */ []));
|
|
1865
|
+
this.kind = computed(() => {
|
|
1866
|
+
const mode = this.state.reauthMode();
|
|
1867
|
+
if (mode === 'password')
|
|
1868
|
+
return 'password';
|
|
1869
|
+
if (mode === this.state.recoveryMethodId())
|
|
1870
|
+
return 'recovery';
|
|
1871
|
+
const method = this.state.activeMethods().find((m) => m.method_id === mode);
|
|
1872
|
+
return isEmail(method) && !this.state.reauthCodeSent() ? 'email-request' : 'code';
|
|
1873
|
+
}, ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
|
|
1874
|
+
}
|
|
1875
|
+
choose(mode) {
|
|
1876
|
+
if (mode)
|
|
1877
|
+
this.state.chooseReauth(mode);
|
|
1878
|
+
}
|
|
1879
|
+
submitPassword() {
|
|
1880
|
+
this.password.markAsTouched();
|
|
1881
|
+
if (this.password.invalid)
|
|
1882
|
+
return;
|
|
1883
|
+
void this.state.confirmReauth({ password: this.password.value });
|
|
1884
|
+
}
|
|
1885
|
+
submitRecovery() {
|
|
1886
|
+
this.recoveryCode.markAsTouched();
|
|
1887
|
+
if (this.recoveryCode.invalid)
|
|
1888
|
+
return;
|
|
1889
|
+
void this.state.confirmReauth({ method_id: this.state.reauthMode(), code: this.recoveryCode.value.trim() });
|
|
1890
|
+
}
|
|
1891
|
+
submitCode(code) {
|
|
1892
|
+
void this.state.confirmReauth({ method_id: this.state.reauthMode(), code });
|
|
1893
|
+
}
|
|
1894
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountReauthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1895
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AccountReauthComponent, isStandalone: true, selector: "spine-account-reauth", ngImport: i0, template: `
|
|
1896
|
+
<section class="sp-account__panel" [attr.aria-labelledby]="titleId">
|
|
1897
|
+
<h4 class="sp-account__title" [id]="titleId">{{ t.reauthTitle }}</h4>
|
|
1898
|
+
<p class="sp-account__hint">{{ t.reauthHint }}</p>
|
|
1899
|
+
|
|
1900
|
+
@if (choices().length > 1) {
|
|
1901
|
+
<hm-radio layout="inline" [label]="t.reauthHow" [options]="choices()" [value]="state.reauthMode()" (valueChange)="choose($event)" />
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
@switch (kind()) {
|
|
1905
|
+
@case ('password') {
|
|
1906
|
+
<form class="sp-account__form" (submit)="$event.preventDefault(); submitPassword()">
|
|
1907
|
+
<hm-input type="password" autocomplete="current-password" [label]="t.currentPassword" [formControl]="password" [messages]="{ required: t.passwordRequired }" />
|
|
1908
|
+
<div class="sp-account__actions">
|
|
1909
|
+
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
1910
|
+
<hm-button emphasis="minimal" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1911
|
+
</div>
|
|
1912
|
+
</form>
|
|
1913
|
+
}
|
|
1914
|
+
@case ('recovery') {
|
|
1915
|
+
<form class="sp-account__form" (submit)="$event.preventDefault(); submitRecovery()">
|
|
1916
|
+
<hm-input autocomplete="off" [label]="t.recoveryCode" [placeholder]="t.recoveryCodePlaceholder" [formControl]="recoveryCode"
|
|
1917
|
+
[messages]="{ required: t.recoveryCodeRequired, minlength: t.recoveryCodeLength }" />
|
|
1918
|
+
<div class="sp-account__actions">
|
|
1919
|
+
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
1920
|
+
<hm-button emphasis="minimal" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1921
|
+
</div>
|
|
1922
|
+
</form>
|
|
1923
|
+
}
|
|
1924
|
+
@case ('email-request') {
|
|
1925
|
+
<div class="sp-account__actions">
|
|
1926
|
+
<hm-button emphasis="high" icon="mail" [loading]="state.busy()" (clicked)="state.sendReauthCode()">{{ t.sendEmailCode }}</hm-button>
|
|
1927
|
+
<hm-button emphasis="minimal" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1928
|
+
</div>
|
|
1929
|
+
}
|
|
1930
|
+
@default {
|
|
1931
|
+
<div class="sp-account__code" role="group" [attr.aria-labelledby]="codeLabelId">
|
|
1932
|
+
<span class="sp-account__label" [id]="codeLabelId">{{ t.sixDigitCode }}</span>
|
|
1933
|
+
<hm-code-input (completed)="submitCode($event)" />
|
|
1934
|
+
</div>
|
|
1935
|
+
<div class="sp-account__actions">
|
|
1936
|
+
<hm-button emphasis="minimal" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1937
|
+
</div>
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
</section>
|
|
1941
|
+
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%;min-width:0}.sp-account__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__icon{display:inline-flex;flex:none;color:var(--hm-muted)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__form,.sp-account__code{display:flex;flex-direction:column;gap:var(--hm-space-xxxs)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:Geist Mono,ui-monospace,monospace;overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CodeInputComponent, selector: "hm-code-input", inputs: ["length"], outputs: ["completed"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: RadioComponent, selector: "hm-radio", inputs: ["value", "label", "options", "appearance", "layout", "hint", "help"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1942
|
+
}
|
|
1943
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountReauthComponent, decorators: [{
|
|
1944
|
+
type: Component,
|
|
1945
|
+
args: [{ selector: 'spine-account-reauth', imports: [ReactiveFormsModule, ButtonComponent, CodeInputComponent, InputComponent, RadioComponent], template: `
|
|
1946
|
+
<section class="sp-account__panel" [attr.aria-labelledby]="titleId">
|
|
1947
|
+
<h4 class="sp-account__title" [id]="titleId">{{ t.reauthTitle }}</h4>
|
|
1948
|
+
<p class="sp-account__hint">{{ t.reauthHint }}</p>
|
|
1949
|
+
|
|
1950
|
+
@if (choices().length > 1) {
|
|
1951
|
+
<hm-radio layout="inline" [label]="t.reauthHow" [options]="choices()" [value]="state.reauthMode()" (valueChange)="choose($event)" />
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
@switch (kind()) {
|
|
1955
|
+
@case ('password') {
|
|
1956
|
+
<form class="sp-account__form" (submit)="$event.preventDefault(); submitPassword()">
|
|
1957
|
+
<hm-input type="password" autocomplete="current-password" [label]="t.currentPassword" [formControl]="password" [messages]="{ required: t.passwordRequired }" />
|
|
1958
|
+
<div class="sp-account__actions">
|
|
1959
|
+
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
1960
|
+
<hm-button emphasis="minimal" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1961
|
+
</div>
|
|
1962
|
+
</form>
|
|
1963
|
+
}
|
|
1964
|
+
@case ('recovery') {
|
|
1965
|
+
<form class="sp-account__form" (submit)="$event.preventDefault(); submitRecovery()">
|
|
1966
|
+
<hm-input autocomplete="off" [label]="t.recoveryCode" [placeholder]="t.recoveryCodePlaceholder" [formControl]="recoveryCode"
|
|
1967
|
+
[messages]="{ required: t.recoveryCodeRequired, minlength: t.recoveryCodeLength }" />
|
|
1968
|
+
<div class="sp-account__actions">
|
|
1969
|
+
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
1970
|
+
<hm-button emphasis="minimal" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1971
|
+
</div>
|
|
1972
|
+
</form>
|
|
1973
|
+
}
|
|
1974
|
+
@case ('email-request') {
|
|
1975
|
+
<div class="sp-account__actions">
|
|
1976
|
+
<hm-button emphasis="high" icon="mail" [loading]="state.busy()" (clicked)="state.sendReauthCode()">{{ t.sendEmailCode }}</hm-button>
|
|
1977
|
+
<hm-button emphasis="minimal" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1978
|
+
</div>
|
|
1979
|
+
}
|
|
1980
|
+
@default {
|
|
1981
|
+
<div class="sp-account__code" role="group" [attr.aria-labelledby]="codeLabelId">
|
|
1982
|
+
<span class="sp-account__label" [id]="codeLabelId">{{ t.sixDigitCode }}</span>
|
|
1983
|
+
<hm-code-input (completed)="submitCode($event)" />
|
|
1984
|
+
</div>
|
|
1985
|
+
<div class="sp-account__actions">
|
|
1986
|
+
<hm-button emphasis="minimal" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1987
|
+
</div>
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
</section>
|
|
1991
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%;min-width:0}.sp-account__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__icon{display:inline-flex;flex:none;color:var(--hm-muted)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__form,.sp-account__code{display:flex;flex-direction:column;gap:var(--hm-space-xxxs)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:Geist Mono,ui-monospace,monospace;overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"] }]
|
|
1992
|
+
}] });
|
|
1993
|
+
|
|
1994
|
+
let sequence$2 = 0;
|
|
1995
|
+
/**
|
|
1996
|
+
* Verificação em duas etapas da conta no Argos, dentro do painel de configurações: os métodos (ativo,
|
|
1997
|
+
* não cadastrado, indisponível) e as ações — adicionar (TOTP com QR Code e confirmação pelo código; e-mail
|
|
1998
|
+
* com código no endereço informado), trocar o aparelho do autenticador e remover. Cada ação passa antes pela
|
|
1999
|
+
* reautenticação (`spine-account-reauth`).
|
|
2000
|
+
*/
|
|
2001
|
+
class AccountMfaComponent {
|
|
2002
|
+
constructor() {
|
|
2003
|
+
this.state = inject(SpineAccountSecurityState);
|
|
2004
|
+
this.t = inject(SPINE_MESSAGES).accountSecurity;
|
|
2005
|
+
this.fmt = formatMessage;
|
|
2006
|
+
this.isTotp = isTotp;
|
|
2007
|
+
this.isSms = isSms;
|
|
2008
|
+
this.ids = `sp-account-mfa-${++sequence$2}`;
|
|
2009
|
+
this.email = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] });
|
|
2010
|
+
}
|
|
2011
|
+
label(method) {
|
|
2012
|
+
if (!method)
|
|
2013
|
+
return '';
|
|
2014
|
+
if (isTotp(method))
|
|
2015
|
+
return this.t.methodTotp;
|
|
2016
|
+
if (isEmail(method))
|
|
2017
|
+
return this.t.methodEmail;
|
|
2018
|
+
if (isSms(method))
|
|
2019
|
+
return this.t.methodSms;
|
|
2020
|
+
return method.method_name;
|
|
2021
|
+
}
|
|
2022
|
+
icon(method) {
|
|
2023
|
+
return isTotp(method) ? 'authenticator' : isEmail(method) ? 'mail' : 'sms';
|
|
2024
|
+
}
|
|
2025
|
+
status(method) {
|
|
2026
|
+
if (isSms(method))
|
|
2027
|
+
return this.t.unavailable;
|
|
2028
|
+
if (!method.active)
|
|
2029
|
+
return this.t.notRegistered;
|
|
2030
|
+
return method.secret ? formatMessage(this.t.activeWithDetail, { detail: method.secret }) : this.t.active;
|
|
2031
|
+
}
|
|
2032
|
+
/** Método da ação em andamento (para o título "Remover ...?"). */
|
|
2033
|
+
actionMethod() {
|
|
2034
|
+
const action = this.state.action();
|
|
2035
|
+
return action && action.kind !== 'codes' ? action.method : null;
|
|
2036
|
+
}
|
|
2037
|
+
errorText(code) {
|
|
2038
|
+
return this.t.errors[code] ?? this.t.errors.generic;
|
|
2039
|
+
}
|
|
2040
|
+
startAdd(method) {
|
|
2041
|
+
this.email.reset('');
|
|
2042
|
+
this.state.start({ kind: 'add', method });
|
|
2043
|
+
}
|
|
2044
|
+
sendEmail() {
|
|
2045
|
+
this.email.markAsTouched();
|
|
2046
|
+
if (this.email.invalid)
|
|
2047
|
+
return;
|
|
2048
|
+
void this.state.sendEnrollEmail(this.email.value.trim());
|
|
2049
|
+
}
|
|
2050
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountMfaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2051
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AccountMfaComponent, isStandalone: true, selector: "spine-account-mfa", ngImport: i0, template: "@if (state.loading() && !state.overview()) {\n <p class=\"sp-account__hint\" role=\"status\">{{ t.loading }}</p>\n} @else if (state.loadError()) {\n <hm-alert variant=\"error\">{{ errorText(state.loadError()) }}</hm-alert>\n @if (state.loadError() !== 'login_required' && state.loadError() !== 'invalid_token') {\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" (clicked)=\"state.load()\">{{ t.retry }}</hm-button>\n </div>\n }\n} @else if (state.overview(); as data) {\n @if (state.owner() === 'methods') {\n @if (state.notice(); as notice) {\n <hm-alert variant=\"success\">{{ t.notices[notice] }}</hm-alert>\n }\n @if (state.error(); as error) {\n <hm-alert variant=\"error\">{{ errorText(error) }}</hm-alert>\n }\n }\n\n @if (state.owner() === 'methods' && state.step() !== 'idle') {\n @switch (state.step()) {\n @case ('reauth') {\n <spine-account-reauth />\n }\n\n @case ('confirm-remove') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-remove'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-remove'\">{{ fmt(t.removeTitle, { method: label(actionMethod()) }) }}</h4>\n <p class=\"sp-account__hint\">\n {{ t.removeHint }}\n @if (state.activeMethods().length <= 1) { {{ t.removeLastHint }} }\n </p>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"high\" intent=\"danger\" [loading]=\"state.busy()\" (clicked)=\"state.confirmRemove()\">{{ t.remove }}</hm-button>\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n\n @case ('enroll-totp') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-totp'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-totp'\">{{ t.totpTitle }}</h4>\n @if (state.totpSetup(); as setup) {\n @if (setup.qrcode) {\n <div class=\"sp-account__qr\"><img [src]=\"setup.qrcode\" [alt]=\"t.qrAlt\" /></div>\n }\n <p class=\"sp-account__hint\">{{ t.manualKey }} <code class=\"sp-account__secret\">{{ setup.secret }}</code></p>\n }\n <div class=\"sp-account__code\" role=\"group\" [attr.aria-labelledby]=\"ids + '-totp-code'\">\n <span class=\"sp-account__label\" [id]=\"ids + '-totp-code'\">{{ t.appCode }}</span>\n <hm-code-input (completed)=\"state.confirmEnroll($event)\" />\n </div>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n\n @case ('enroll-email') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-email'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email'\">{{ t.emailTitle }}</h4>\n <form class=\"sp-account__form\" (submit)=\"$event.preventDefault(); sendEmail()\">\n <hm-input type=\"email\" autocomplete=\"email\" [label]=\"t.email\" [placeholder]=\"t.emailPlaceholder\" [formControl]=\"email\"\n [messages]=\"{ required: t.emailRequired, email: t.emailInvalid }\" />\n <div class=\"sp-account__actions\">\n <hm-button type=\"submit\" emphasis=\"high\" icon=\"mail\" [loading]=\"state.busy()\">{{ t.sendCode }}</hm-button>\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </form>\n </section>\n }\n\n @case ('enroll-email-code') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-email-code'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email-code'\">{{ t.emailCodeTitle }}</h4>\n <p class=\"sp-account__hint\" [id]=\"ids + '-email-code-hint'\">{{ fmt(t.emailCodeHint, { email: state.enrollEmail() }) }}</p>\n <div class=\"sp-account__code\" role=\"group\" [attr.aria-labelledby]=\"ids + '-email-code-label'\" [attr.aria-describedby]=\"ids + '-email-code-hint'\">\n <span class=\"sp-account__label\" [id]=\"ids + '-email-code-label'\">{{ t.sixDigitCode }}</span>\n <hm-code-input (completed)=\"state.confirmEnroll($event)\" />\n </div>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n }\n } @else {\n @if (data.policy.required) {\n <p class=\"sp-account__hint\">{{ t.requiredHint }}</p>\n }\n <ul class=\"sp-account__list\">\n @for (m of state.methods(); track m.method_id) {\n <li class=\"sp-account__item\">\n <span class=\"sp-account__icon\"><hm-icon [shape]=\"icon(m)\" /></span>\n <span class=\"sp-account__item-main\">\n <strong>{{ label(m) }}</strong>\n <span class=\"sp-account__meta\">{{ status(m) }}</span>\n </span>\n @if (!isSms(m)) {\n <span class=\"sp-account__item-actions\">\n @if (!m.active) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"plus\" [disabled]=\"state.busy() || state.step() !== 'idle'\" (clicked)=\"startAdd(m)\">{{ t.add }}</hm-button>\n } @else {\n @if (isTotp(m)) {\n <hm-button size=\"small\" emphasis=\"medium\" [disabled]=\"state.busy() || state.step() !== 'idle'\" (clicked)=\"state.start({ kind: 'replace', method: m })\">{{ t.replaceDevice }}</hm-button>\n }\n <hm-button size=\"small\" emphasis=\"minimal\" intent=\"danger\" [disabled]=\"state.busy() || state.step() !== 'idle' || !state.canRemove(m)\" (clicked)=\"state.start({ kind: 'remove', method: m })\">{{ t.remove }}</hm-button>\n }\n </span>\n }\n </li>\n }\n </ul>\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%;min-width:0}.sp-account__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__icon{display:inline-flex;flex:none;color:var(--hm-muted)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__form,.sp-account__code{display:flex;flex-direction:column;gap:var(--hm-space-xxxs)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:Geist Mono,ui-monospace,monospace;overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CodeInputComponent, selector: "hm-code-input", inputs: ["length"], outputs: ["completed"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: AccountReauthComponent, selector: "spine-account-reauth" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2052
|
+
}
|
|
2053
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountMfaComponent, decorators: [{
|
|
2054
|
+
type: Component,
|
|
2055
|
+
args: [{ selector: 'spine-account-mfa', imports: [ReactiveFormsModule, AlertComponent, ButtonComponent, CodeInputComponent, IconComponent, InputComponent, AccountReauthComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (state.loading() && !state.overview()) {\n <p class=\"sp-account__hint\" role=\"status\">{{ t.loading }}</p>\n} @else if (state.loadError()) {\n <hm-alert variant=\"error\">{{ errorText(state.loadError()) }}</hm-alert>\n @if (state.loadError() !== 'login_required' && state.loadError() !== 'invalid_token') {\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" (clicked)=\"state.load()\">{{ t.retry }}</hm-button>\n </div>\n }\n} @else if (state.overview(); as data) {\n @if (state.owner() === 'methods') {\n @if (state.notice(); as notice) {\n <hm-alert variant=\"success\">{{ t.notices[notice] }}</hm-alert>\n }\n @if (state.error(); as error) {\n <hm-alert variant=\"error\">{{ errorText(error) }}</hm-alert>\n }\n }\n\n @if (state.owner() === 'methods' && state.step() !== 'idle') {\n @switch (state.step()) {\n @case ('reauth') {\n <spine-account-reauth />\n }\n\n @case ('confirm-remove') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-remove'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-remove'\">{{ fmt(t.removeTitle, { method: label(actionMethod()) }) }}</h4>\n <p class=\"sp-account__hint\">\n {{ t.removeHint }}\n @if (state.activeMethods().length <= 1) { {{ t.removeLastHint }} }\n </p>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"high\" intent=\"danger\" [loading]=\"state.busy()\" (clicked)=\"state.confirmRemove()\">{{ t.remove }}</hm-button>\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n\n @case ('enroll-totp') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-totp'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-totp'\">{{ t.totpTitle }}</h4>\n @if (state.totpSetup(); as setup) {\n @if (setup.qrcode) {\n <div class=\"sp-account__qr\"><img [src]=\"setup.qrcode\" [alt]=\"t.qrAlt\" /></div>\n }\n <p class=\"sp-account__hint\">{{ t.manualKey }} <code class=\"sp-account__secret\">{{ setup.secret }}</code></p>\n }\n <div class=\"sp-account__code\" role=\"group\" [attr.aria-labelledby]=\"ids + '-totp-code'\">\n <span class=\"sp-account__label\" [id]=\"ids + '-totp-code'\">{{ t.appCode }}</span>\n <hm-code-input (completed)=\"state.confirmEnroll($event)\" />\n </div>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n\n @case ('enroll-email') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-email'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email'\">{{ t.emailTitle }}</h4>\n <form class=\"sp-account__form\" (submit)=\"$event.preventDefault(); sendEmail()\">\n <hm-input type=\"email\" autocomplete=\"email\" [label]=\"t.email\" [placeholder]=\"t.emailPlaceholder\" [formControl]=\"email\"\n [messages]=\"{ required: t.emailRequired, email: t.emailInvalid }\" />\n <div class=\"sp-account__actions\">\n <hm-button type=\"submit\" emphasis=\"high\" icon=\"mail\" [loading]=\"state.busy()\">{{ t.sendCode }}</hm-button>\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </form>\n </section>\n }\n\n @case ('enroll-email-code') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-email-code'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email-code'\">{{ t.emailCodeTitle }}</h4>\n <p class=\"sp-account__hint\" [id]=\"ids + '-email-code-hint'\">{{ fmt(t.emailCodeHint, { email: state.enrollEmail() }) }}</p>\n <div class=\"sp-account__code\" role=\"group\" [attr.aria-labelledby]=\"ids + '-email-code-label'\" [attr.aria-describedby]=\"ids + '-email-code-hint'\">\n <span class=\"sp-account__label\" [id]=\"ids + '-email-code-label'\">{{ t.sixDigitCode }}</span>\n <hm-code-input (completed)=\"state.confirmEnroll($event)\" />\n </div>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n }\n } @else {\n @if (data.policy.required) {\n <p class=\"sp-account__hint\">{{ t.requiredHint }}</p>\n }\n <ul class=\"sp-account__list\">\n @for (m of state.methods(); track m.method_id) {\n <li class=\"sp-account__item\">\n <span class=\"sp-account__icon\"><hm-icon [shape]=\"icon(m)\" /></span>\n <span class=\"sp-account__item-main\">\n <strong>{{ label(m) }}</strong>\n <span class=\"sp-account__meta\">{{ status(m) }}</span>\n </span>\n @if (!isSms(m)) {\n <span class=\"sp-account__item-actions\">\n @if (!m.active) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"plus\" [disabled]=\"state.busy() || state.step() !== 'idle'\" (clicked)=\"startAdd(m)\">{{ t.add }}</hm-button>\n } @else {\n @if (isTotp(m)) {\n <hm-button size=\"small\" emphasis=\"medium\" [disabled]=\"state.busy() || state.step() !== 'idle'\" (clicked)=\"state.start({ kind: 'replace', method: m })\">{{ t.replaceDevice }}</hm-button>\n }\n <hm-button size=\"small\" emphasis=\"minimal\" intent=\"danger\" [disabled]=\"state.busy() || state.step() !== 'idle' || !state.canRemove(m)\" (clicked)=\"state.start({ kind: 'remove', method: m })\">{{ t.remove }}</hm-button>\n }\n </span>\n }\n </li>\n }\n </ul>\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%;min-width:0}.sp-account__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__icon{display:inline-flex;flex:none;color:var(--hm-muted)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__form,.sp-account__code{display:flex;flex-direction:column;gap:var(--hm-space-xxxs)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:Geist Mono,ui-monospace,monospace;overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"] }]
|
|
2056
|
+
}] });
|
|
2057
|
+
|
|
2058
|
+
let sequence$1 = 0;
|
|
2059
|
+
/**
|
|
2060
|
+
* Códigos de recuperação da conta no Argos: quantos restam e "gerar novos" (com reautenticação). O lote
|
|
2061
|
+
* novo aparece UMA vez (o Argos só guarda o hash), com copiar e baixar em .txt, até a pessoa dizer que
|
|
2062
|
+
* guardou — e some quando o painel fecha.
|
|
2063
|
+
*/
|
|
2064
|
+
class AccountRecoveryCodesComponent {
|
|
2065
|
+
constructor() {
|
|
2066
|
+
this.state = inject(SpineAccountSecurityState);
|
|
2067
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
2068
|
+
this.t = this.messages.accountSecurity;
|
|
2069
|
+
this.toast = inject(HermesToastService);
|
|
2070
|
+
this.titleId = `sp-account-codes-${++sequence$1}`;
|
|
2071
|
+
this.remaining = computed(() => pluralMessage(this.t.codesRemaining, this.state.remainingCodes(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "remaining" }] : /* istanbul ignore next */ []));
|
|
2072
|
+
}
|
|
2073
|
+
errorText(code) {
|
|
2074
|
+
return this.t.errors[code] ?? this.t.errors.generic;
|
|
2075
|
+
}
|
|
2076
|
+
asText(codes) {
|
|
2077
|
+
return `${this.t.codesFileHeader}\n\n${codes.join('\n')}\n`;
|
|
2078
|
+
}
|
|
2079
|
+
async copy(codes) {
|
|
2080
|
+
try {
|
|
2081
|
+
await navigator.clipboard.writeText(this.asText(codes));
|
|
2082
|
+
this.toast.success(this.t.codesCopied);
|
|
2083
|
+
}
|
|
2084
|
+
catch {
|
|
2085
|
+
this.toast.error(this.t.copyFailed);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
download(codes) {
|
|
2089
|
+
const url = URL.createObjectURL(new Blob([this.asText(codes)], { type: 'text/plain;charset=utf-8' }));
|
|
2090
|
+
const a = document.createElement('a');
|
|
2091
|
+
a.href = url;
|
|
2092
|
+
a.download = this.t.codesFileName;
|
|
2093
|
+
a.click();
|
|
2094
|
+
// Revogar no mesmo tick pode cancelar o download em alguns navegadores (o clique só agenda a navegação).
|
|
2095
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
2096
|
+
}
|
|
2097
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountRecoveryCodesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2098
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AccountRecoveryCodesComponent, isStandalone: true, selector: "spine-account-recovery-codes", ngImport: i0, template: `
|
|
2099
|
+
@if (state.overview()) {
|
|
2100
|
+
@if (state.owner() === 'codes') {
|
|
2101
|
+
@if (state.notice(); as notice) {
|
|
2102
|
+
<hm-alert variant="success">{{ t.notices[notice] }}</hm-alert>
|
|
2103
|
+
}
|
|
2104
|
+
@if (state.error(); as error) {
|
|
2105
|
+
<hm-alert variant="error">{{ errorText(error) }}</hm-alert>
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
@if (state.owner() === 'codes' && state.step() === 'reauth') {
|
|
2110
|
+
<spine-account-reauth />
|
|
2111
|
+
} @else if (state.owner() === 'codes' && state.step() === 'codes' && state.issuedCodes(); as codes) {
|
|
2112
|
+
<section class="sp-account__panel sp-account__panel--success" [attr.aria-labelledby]="titleId">
|
|
2113
|
+
<h4 class="sp-account__title" [id]="titleId">{{ t.codesTitle }}</h4>
|
|
2114
|
+
<p class="sp-account__hint">{{ t.codesShownOnce }}</p>
|
|
2115
|
+
<ol class="sp-account__codes">
|
|
2116
|
+
@for (code of codes; track code) {
|
|
2117
|
+
<li><code>{{ code }}</code></li>
|
|
2118
|
+
}
|
|
2119
|
+
</ol>
|
|
2120
|
+
<div class="sp-account__actions">
|
|
2121
|
+
<hm-button emphasis="medium" icon="copy" (clicked)="copy(codes)">{{ t.copyCodes }}</hm-button>
|
|
2122
|
+
<hm-button emphasis="medium" icon="download" (clicked)="download(codes)">{{ t.downloadCodes }}</hm-button>
|
|
2123
|
+
<hm-button emphasis="high" (clicked)="state.codesSaved()">{{ t.codesSaved }}</hm-button>
|
|
2124
|
+
</div>
|
|
2125
|
+
</section>
|
|
2126
|
+
} @else if (state.activeMethods().length) {
|
|
2127
|
+
<p class="sp-account__hint">{{ remaining() }} {{ t.codesHint }}</p>
|
|
2128
|
+
<div class="sp-account__actions">
|
|
2129
|
+
<hm-button emphasis="medium" icon="key" [disabled]="state.busy() || state.step() !== 'idle'" (clicked)="state.start({ kind: 'codes' })">
|
|
2130
|
+
{{ state.remainingCodes() ? t.regenerateCodes : t.generateCodes }}
|
|
2131
|
+
</hm-button>
|
|
2132
|
+
</div>
|
|
2133
|
+
} @else {
|
|
2134
|
+
<p class="sp-account__hint">{{ t.codesNeedMethod }}</p>
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%;min-width:0}.sp-account__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__icon{display:inline-flex;flex:none;color:var(--hm-muted)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__form,.sp-account__code{display:flex;flex-direction:column;gap:var(--hm-space-xxxs)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:Geist Mono,ui-monospace,monospace;overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"], dependencies: [{ kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: AccountReauthComponent, selector: "spine-account-reauth" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2138
|
+
}
|
|
2139
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountRecoveryCodesComponent, decorators: [{
|
|
2140
|
+
type: Component,
|
|
2141
|
+
args: [{ selector: 'spine-account-recovery-codes', imports: [AlertComponent, ButtonComponent, AccountReauthComponent], template: `
|
|
2142
|
+
@if (state.overview()) {
|
|
2143
|
+
@if (state.owner() === 'codes') {
|
|
2144
|
+
@if (state.notice(); as notice) {
|
|
2145
|
+
<hm-alert variant="success">{{ t.notices[notice] }}</hm-alert>
|
|
2146
|
+
}
|
|
2147
|
+
@if (state.error(); as error) {
|
|
2148
|
+
<hm-alert variant="error">{{ errorText(error) }}</hm-alert>
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
@if (state.owner() === 'codes' && state.step() === 'reauth') {
|
|
2153
|
+
<spine-account-reauth />
|
|
2154
|
+
} @else if (state.owner() === 'codes' && state.step() === 'codes' && state.issuedCodes(); as codes) {
|
|
2155
|
+
<section class="sp-account__panel sp-account__panel--success" [attr.aria-labelledby]="titleId">
|
|
2156
|
+
<h4 class="sp-account__title" [id]="titleId">{{ t.codesTitle }}</h4>
|
|
2157
|
+
<p class="sp-account__hint">{{ t.codesShownOnce }}</p>
|
|
2158
|
+
<ol class="sp-account__codes">
|
|
2159
|
+
@for (code of codes; track code) {
|
|
2160
|
+
<li><code>{{ code }}</code></li>
|
|
2161
|
+
}
|
|
2162
|
+
</ol>
|
|
2163
|
+
<div class="sp-account__actions">
|
|
2164
|
+
<hm-button emphasis="medium" icon="copy" (clicked)="copy(codes)">{{ t.copyCodes }}</hm-button>
|
|
2165
|
+
<hm-button emphasis="medium" icon="download" (clicked)="download(codes)">{{ t.downloadCodes }}</hm-button>
|
|
2166
|
+
<hm-button emphasis="high" (clicked)="state.codesSaved()">{{ t.codesSaved }}</hm-button>
|
|
2167
|
+
</div>
|
|
2168
|
+
</section>
|
|
2169
|
+
} @else if (state.activeMethods().length) {
|
|
2170
|
+
<p class="sp-account__hint">{{ remaining() }} {{ t.codesHint }}</p>
|
|
2171
|
+
<div class="sp-account__actions">
|
|
2172
|
+
<hm-button emphasis="medium" icon="key" [disabled]="state.busy() || state.step() !== 'idle'" (clicked)="state.start({ kind: 'codes' })">
|
|
2173
|
+
{{ state.remainingCodes() ? t.regenerateCodes : t.generateCodes }}
|
|
2174
|
+
</hm-button>
|
|
2175
|
+
</div>
|
|
2176
|
+
} @else {
|
|
2177
|
+
<p class="sp-account__hint">{{ t.codesNeedMethod }}</p>
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%;min-width:0}.sp-account__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__icon{display:inline-flex;flex:none;color:var(--hm-muted)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__form,.sp-account__code{display:flex;flex-direction:column;gap:var(--hm-space-xxxs)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:Geist Mono,ui-monospace,monospace;overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"] }]
|
|
2181
|
+
}] });
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* Sessões e dispositivos da conta no Argos, dentro do painel de configurações: onde a conta está
|
|
2185
|
+
* conectada (navegador, IP, aplicativos, último uso), com a sessão deste login marcada. Encerra uma
|
|
2186
|
+
* sessão (segundo clique confirma) ou todas as outras. A sessão atual não tem "Encerrar" aqui: sair dela é
|
|
2187
|
+
* o "Sair" da seção Conta, que também encerra o app.
|
|
2188
|
+
*/
|
|
2189
|
+
class AccountSessionsComponent {
|
|
2190
|
+
constructor() {
|
|
2191
|
+
this.api = inject(SpineAccountSecurityService);
|
|
2192
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
2193
|
+
this.t = this.messages.accountSecurity;
|
|
2194
|
+
this.preferences = inject(SpinePreferencesService);
|
|
2195
|
+
this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2196
|
+
this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
|
|
2197
|
+
this.sessions = signal([], ...(ngDevMode ? [{ debugName: "sessions" }] : /* istanbul ignore next */ []));
|
|
2198
|
+
this.notice = signal('', ...(ngDevMode ? [{ debugName: "notice" }] : /* istanbul ignore next */ []));
|
|
2199
|
+
this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
2200
|
+
/** Encerramento aguardando o segundo clique: id da sessão, `others` ou null. */
|
|
2201
|
+
this.confirming = signal(null, ...(ngDevMode ? [{ debugName: "confirming" }] : /* istanbul ignore next */ []));
|
|
2202
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
2203
|
+
/** A deste login primeiro; depois as outras, da usada mais recentemente. */
|
|
2204
|
+
this.ordered = computed(() => [...this.sessions()].sort((a, b) => Number(b.current) - Number(a.current) || b.last_seen_at.localeCompare(a.last_seen_at)), ...(ngDevMode ? [{ debugName: "ordered" }] : /* istanbul ignore next */ []));
|
|
2205
|
+
this.others = computed(() => this.sessions().filter((s) => !s.current).length, ...(ngDevMode ? [{ debugName: "others" }] : /* istanbul ignore next */ []));
|
|
2206
|
+
this.othersConfirm = computed(() => pluralMessage(this.t.endOthersConfirm, this.others(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "othersConfirm" }] : /* istanbul ignore next */ []));
|
|
2207
|
+
if (this.api.available)
|
|
2208
|
+
void this.load();
|
|
2209
|
+
else
|
|
2210
|
+
this.loading.set(false);
|
|
2211
|
+
}
|
|
2212
|
+
async load() {
|
|
2213
|
+
this.loading.set(true);
|
|
2214
|
+
try {
|
|
2215
|
+
const { sessions } = await this.api.sessions();
|
|
2216
|
+
this.sessions.set(sessions ?? []);
|
|
2217
|
+
this.loadError.set('');
|
|
2218
|
+
}
|
|
2219
|
+
catch (err) {
|
|
2220
|
+
this.loadError.set(accountErrorCode(err) || 'generic');
|
|
2221
|
+
}
|
|
2222
|
+
finally {
|
|
2223
|
+
this.loading.set(false);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
ask(target) {
|
|
2227
|
+
this.notice.set('');
|
|
2228
|
+
this.error.set('');
|
|
2229
|
+
this.confirming.set(target);
|
|
2230
|
+
}
|
|
2231
|
+
cancel() {
|
|
2232
|
+
this.confirming.set(null);
|
|
2233
|
+
}
|
|
2234
|
+
async end(session) {
|
|
2235
|
+
if (this.confirming() !== session.id) {
|
|
2236
|
+
this.ask(session.id);
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
await this.run(async () => {
|
|
2240
|
+
await this.api.endSession(session.id);
|
|
2241
|
+
this.notice.set(this.t.sessionEndedNotice);
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
async endOthers() {
|
|
2245
|
+
await this.run(async () => {
|
|
2246
|
+
const { ended } = await this.api.endOtherSessions();
|
|
2247
|
+
this.notice.set(pluralMessage(this.t.othersEndedNotice, ended, this.messages.meta.locale));
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
async run(fn) {
|
|
2251
|
+
if (this.busy())
|
|
2252
|
+
return;
|
|
2253
|
+
this.busy.set(true);
|
|
2254
|
+
this.error.set('');
|
|
2255
|
+
try {
|
|
2256
|
+
await fn();
|
|
2257
|
+
}
|
|
2258
|
+
catch (err) {
|
|
2259
|
+
const code = accountErrorCode(err) || 'generic';
|
|
2260
|
+
if (code === 'login_required' || code === 'invalid_token') {
|
|
2261
|
+
this.loadError.set(code);
|
|
2262
|
+
}
|
|
2263
|
+
else {
|
|
2264
|
+
this.error.set(this.errorText(code));
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
finally {
|
|
2268
|
+
this.confirming.set(null);
|
|
2269
|
+
this.busy.set(false);
|
|
2270
|
+
}
|
|
2271
|
+
if (!this.loadError())
|
|
2272
|
+
await this.load();
|
|
2273
|
+
}
|
|
2274
|
+
errorText(code) {
|
|
2275
|
+
return this.t.errors[code] ?? this.t.errors.generic;
|
|
2276
|
+
}
|
|
2277
|
+
detail(session) {
|
|
2278
|
+
return formatMessage(this.t.sessionDetail, { ip: session.ip || this.t.unknownIp, date: this.date(session.last_seen_at) });
|
|
2279
|
+
}
|
|
2280
|
+
since(session) {
|
|
2281
|
+
return formatMessage(this.t.sessionSince, { date: this.date(session.created_at) });
|
|
2282
|
+
}
|
|
2283
|
+
apps(session) {
|
|
2284
|
+
return formatMessage(this.t.sessionApps, { apps: session.clients.map((c) => c.name ?? this.t.unnamedApp).join(', ') });
|
|
2285
|
+
}
|
|
2286
|
+
date(value) {
|
|
2287
|
+
return new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeStyle: 'short', timeZone: this.preferences.effectiveTimeZone() })
|
|
2288
|
+
.format(new Date(value));
|
|
2289
|
+
}
|
|
2290
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountSessionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2291
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AccountSessionsComponent, isStandalone: true, selector: "spine-account-sessions", ngImport: i0, template: "@if (loading() && !sessions().length) {\n <p class=\"sp-account__hint\" role=\"status\">{{ t.loading }}</p>\n} @else if (loadError()) {\n <hm-alert variant=\"error\">{{ errorText(loadError()) }}</hm-alert>\n @if (loadError() !== 'login_required' && loadError() !== 'invalid_token') {\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" (clicked)=\"load()\">{{ t.retry }}</hm-button>\n </div>\n }\n} @else {\n @if (notice()) {\n <hm-alert variant=\"success\">{{ notice() }}</hm-alert>\n }\n @if (error()) {\n <hm-alert variant=\"error\">{{ error() }}</hm-alert>\n }\n\n <ul class=\"sp-account__list\">\n @for (s of ordered(); track s.id) {\n <li class=\"sp-account__item\">\n <span class=\"sp-account__icon\"><hm-icon [shape]=\"s.device.mobile ? 'mobile' : 'computer'\" /></span>\n <span class=\"sp-account__item-main\">\n <span class=\"sp-account__item-title\">\n <strong>{{ s.device.label }}</strong>\n @if (s.current) {\n <hm-tag [name]=\"t.thisSession\" color=\"success\" />\n } @else if (s.this_device) {\n <hm-tag [name]=\"t.thisBrowser\" color=\"info\" />\n }\n @if (!s.sso_active) {\n <hm-tag [name]=\"t.appsOnly\" color=\"warning\" />\n }\n </span>\n <span class=\"sp-account__meta\">{{ detail(s) }}</span>\n <span class=\"sp-account__meta\">{{ since(s) }}</span>\n @if (s.clients.length) {\n <span class=\"sp-account__meta\">{{ apps(s) }}</span>\n }\n </span>\n @if (!s.current) {\n <span class=\"sp-account__item-actions\">\n @if (confirming() === s.id) {\n <hm-button size=\"small\" emphasis=\"minimal\" [disabled]=\"busy()\" (clicked)=\"cancel()\">{{ t.cancel }}</hm-button>\n }\n <hm-button size=\"small\" [emphasis]=\"confirming() === s.id ? 'high' : 'medium'\" intent=\"danger\" [loading]=\"busy() && confirming() === s.id\" [disabled]=\"busy() && confirming() !== s.id\" (clicked)=\"end(s)\">\n {{ confirming() === s.id ? t.confirmEndSession : t.endSession }}\n </hm-button>\n </span>\n }\n </li>\n } @empty {\n <li class=\"sp-account__item sp-account__meta\">{{ t.sessionsEmpty }}</li>\n }\n </ul>\n\n @if (others() > 0) {\n @if (confirming() === 'others') {\n <p class=\"sp-account__hint\">{{ othersConfirm() }}</p>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"high\" intent=\"danger\" icon=\"logout\" [loading]=\"busy()\" (clicked)=\"endOthers()\">{{ t.endOthers }}</hm-button>\n <hm-button emphasis=\"minimal\" [disabled]=\"busy()\" (clicked)=\"cancel()\">{{ t.cancel }}</hm-button>\n </div>\n } @else {\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"medium\" icon=\"logout\" [disabled]=\"busy()\" (clicked)=\"ask('others')\">{{ t.endOthers }}</hm-button>\n </div>\n }\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%;min-width:0}.sp-account__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__icon{display:inline-flex;flex:none;color:var(--hm-muted)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__form,.sp-account__code{display:flex;flex-direction:column;gap:var(--hm-space-xxxs)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:Geist Mono,ui-monospace,monospace;overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"], dependencies: [{ kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2292
|
+
}
|
|
2293
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountSessionsComponent, decorators: [{
|
|
2294
|
+
type: Component,
|
|
2295
|
+
args: [{ selector: 'spine-account-sessions', imports: [AlertComponent, ButtonComponent, IconComponent, TagComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (loading() && !sessions().length) {\n <p class=\"sp-account__hint\" role=\"status\">{{ t.loading }}</p>\n} @else if (loadError()) {\n <hm-alert variant=\"error\">{{ errorText(loadError()) }}</hm-alert>\n @if (loadError() !== 'login_required' && loadError() !== 'invalid_token') {\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" (clicked)=\"load()\">{{ t.retry }}</hm-button>\n </div>\n }\n} @else {\n @if (notice()) {\n <hm-alert variant=\"success\">{{ notice() }}</hm-alert>\n }\n @if (error()) {\n <hm-alert variant=\"error\">{{ error() }}</hm-alert>\n }\n\n <ul class=\"sp-account__list\">\n @for (s of ordered(); track s.id) {\n <li class=\"sp-account__item\">\n <span class=\"sp-account__icon\"><hm-icon [shape]=\"s.device.mobile ? 'mobile' : 'computer'\" /></span>\n <span class=\"sp-account__item-main\">\n <span class=\"sp-account__item-title\">\n <strong>{{ s.device.label }}</strong>\n @if (s.current) {\n <hm-tag [name]=\"t.thisSession\" color=\"success\" />\n } @else if (s.this_device) {\n <hm-tag [name]=\"t.thisBrowser\" color=\"info\" />\n }\n @if (!s.sso_active) {\n <hm-tag [name]=\"t.appsOnly\" color=\"warning\" />\n }\n </span>\n <span class=\"sp-account__meta\">{{ detail(s) }}</span>\n <span class=\"sp-account__meta\">{{ since(s) }}</span>\n @if (s.clients.length) {\n <span class=\"sp-account__meta\">{{ apps(s) }}</span>\n }\n </span>\n @if (!s.current) {\n <span class=\"sp-account__item-actions\">\n @if (confirming() === s.id) {\n <hm-button size=\"small\" emphasis=\"minimal\" [disabled]=\"busy()\" (clicked)=\"cancel()\">{{ t.cancel }}</hm-button>\n }\n <hm-button size=\"small\" [emphasis]=\"confirming() === s.id ? 'high' : 'medium'\" intent=\"danger\" [loading]=\"busy() && confirming() === s.id\" [disabled]=\"busy() && confirming() !== s.id\" (clicked)=\"end(s)\">\n {{ confirming() === s.id ? t.confirmEndSession : t.endSession }}\n </hm-button>\n </span>\n }\n </li>\n } @empty {\n <li class=\"sp-account__item sp-account__meta\">{{ t.sessionsEmpty }}</li>\n }\n </ul>\n\n @if (others() > 0) {\n @if (confirming() === 'others') {\n <p class=\"sp-account__hint\">{{ othersConfirm() }}</p>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"high\" intent=\"danger\" icon=\"logout\" [loading]=\"busy()\" (clicked)=\"endOthers()\">{{ t.endOthers }}</hm-button>\n <hm-button emphasis=\"minimal\" [disabled]=\"busy()\" (clicked)=\"cancel()\">{{ t.cancel }}</hm-button>\n </div>\n } @else {\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"medium\" icon=\"logout\" [disabled]=\"busy()\" (clicked)=\"ask('others')\">{{ t.endOthers }}</hm-button>\n </div>\n }\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%;min-width:0}.sp-account__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__icon{display:inline-flex;flex:none;color:var(--hm-muted)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__form,.sp-account__code{display:flex;flex-direction:column;gap:var(--hm-space-xxxs)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:Geist Mono,ui-monospace,monospace;overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"] }]
|
|
2296
|
+
}], ctorParameters: () => [] });
|
|
2297
|
+
|
|
1441
2298
|
/**
|
|
1442
2299
|
* Tokens de aplicação pessoais (`x-app-token`) do usuário logado — `argos-admin /me/app-tokens`.
|
|
1443
2300
|
* Sempre da aplicação LOGADA: o servidor tira o client do próprio access-token da sessão, então
|
|
@@ -1468,7 +2325,7 @@ class SpineAppTokensService {
|
|
|
1468
2325
|
return firstValueFrom(this.http.post(this.baseUrl, body, { headers: this.headers }));
|
|
1469
2326
|
}
|
|
1470
2327
|
revoke(id) {
|
|
1471
|
-
return firstValueFrom(this.http.delete(`${this.baseUrl}
|
|
2328
|
+
return firstValueFrom(this.http.delete(`${this.baseUrl}${urlPath `/${id}`}`, { headers: this.headers }));
|
|
1472
2329
|
}
|
|
1473
2330
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1474
2331
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, providedIn: 'root' }); }
|
|
@@ -1488,6 +2345,8 @@ class AppTokensComponent {
|
|
|
1488
2345
|
constructor() {
|
|
1489
2346
|
this.text = inject(SPINE_MESSAGES).appTokens;
|
|
1490
2347
|
this.fmt = formatMessage;
|
|
2348
|
+
/** Para `[innerHTML]`: escapa os parâmetros (o nome da aplicação vem do servidor). */
|
|
2349
|
+
this.fmtHtml = formatMessageHtml;
|
|
1491
2350
|
this.locale = inject(SPINE_MESSAGES);
|
|
1492
2351
|
this.service = inject(SpineAppTokensService);
|
|
1493
2352
|
this.toast = inject(HermesToastService);
|
|
@@ -1624,11 +2483,11 @@ class AppTokensComponent {
|
|
|
1624
2483
|
return detail || fallback;
|
|
1625
2484
|
}
|
|
1626
2485
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AppTokensComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1627
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AppTokensComponent, isStandalone: true, selector: "spine-app-tokens", ngImport: i0, template: "@if (!available) {\n <p class=\"sp-tokens__note\">{{ text.unavailable }}</p>\n} @else {\n <!-- Token rec\u00E9m-criado: \u00FAnica vez em que o valor aparece. -->\n @if (created(); as c) {\n <div class=\"sp-tokens__created\" role=\"status\">\n <p class=\"sp-tokens__created-title\">\n <hm-icon shape=\"check-circle\" /> {{ fmt(text.created, { name: c.name }) }}\n </p>\n <code class=\"sp-tokens__secret\">{{ c.token }}</code>\n <p class=\"sp-tokens__note\" [innerHTML]=\"
|
|
2486
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AppTokensComponent, isStandalone: true, selector: "spine-app-tokens", ngImport: i0, template: "@if (!available) {\n <p class=\"sp-tokens__note\">{{ text.unavailable }}</p>\n} @else {\n <!-- Token rec\u00E9m-criado: \u00FAnica vez em que o valor aparece. -->\n @if (created(); as c) {\n <div class=\"sp-tokens__created\" role=\"status\">\n <p class=\"sp-tokens__created-title\">\n <hm-icon shape=\"check-circle\" /> {{ fmt(text.created, { name: c.name }) }}\n </p>\n <code class=\"sp-tokens__secret\">{{ c.token }}</code>\n <p class=\"sp-tokens__note\" [innerHTML]=\"fmtHtml(text.sendHeader, { date: formatDate(c.expiresAt) })\"></p>\n <div class=\"sp-tokens__actions\">\n <hm-button emphasis=\"high\" icon=\"copy\" (clicked)=\"copyCreated()\">{{ text.copyToken }}</hm-button>\n <hm-button emphasis=\"minimal\" (clicked)=\"dismissCreated()\">{{ text.saved }}</hm-button>\n </div>\n </div>\n }\n\n @if (loading()) {\n <p class=\"sp-tokens__note\">{{ text.loading }}</p>\n } @else if (loadError()) {\n <p class=\"sp-tokens__note sp-tokens__note--error\">{{ loadError() }}</p>\n } @else {\n @if (tokens().length) {\n <ul class=\"sp-tokens__list\">\n @for (t of tokens(); track t.id) {\n <li class=\"sp-tokens__item\" [class.sp-tokens__item--expired]=\"t.expired\">\n <span class=\"sp-tokens__item-main\">\n <strong>{{ t.name }}</strong>\n <span class=\"sp-tokens__meta\">{{ t.badge_name }} \u00B7 <code>{{ t.prefix }}</code></span>\n <span class=\"sp-tokens__meta\">\n @if (t.expired) { <span class=\"sp-tokens__expired\">{{ fmt(text.expiredOn, { date: formatDate(t.expires_at) }) }}</span> }\n @else { {{ fmt(text.expiresOn, { date: formatDate(t.expires_at) }) }} }\n \u00B7 {{ fmt(text.lastUsed, { date: formatDate(t.last_used_at) }) }}\n </span>\n </span>\n <hm-button emphasis=\"minimal\" intent=\"danger\" (clicked)=\"revoke(t)\">\n {{ confirmingRevoke() === t.id ? text.confirmRevoke : text.revoke }}\n </hm-button>\n </li>\n }\n </ul>\n } @else if (!creating()) {\n <p class=\"sp-tokens__note\">{{ text.empty }}</p>\n }\n\n @if (creating()) {\n <form class=\"sp-tokens__form\" (submit)=\"$event.preventDefault(); submit()\">\n @if (!badges().length) {\n <p class=\"sp-tokens__note\">{{ text.noBadges }}</p>\n } @else {\n @if (clientName()) {\n <p class=\"sp-tokens__note\" [innerHTML]=\"fmtHtml(text.appOnly, { name: clientName() })\"></p>\n }\n @if (badgeOptions().length > 1) {\n <div class=\"sp-tokens__field\">\n <span class=\"sp-tokens__label\">{{ text.accessLevel }}</span>\n <hm-segmented-control emphasis=\"action\" [label]=\"text.accessLevel\" [options]=\"badgeOptions()\" [value]=\"badgeId()\" (valueChange)=\"badgeId.set($event)\" />\n </div>\n }\n <hm-input [label]=\"text.name\" [placeholder]=\"text.namePlaceholder\" [formControl]=\"nameControl\" [messages]=\"{ required: text.nameRequired, maxlength: text.nameMax }\" />\n <div class=\"sp-tokens__field\">\n <span class=\"sp-tokens__label\">{{ text.validity }}</span>\n <hm-segmented-control emphasis=\"action\" [label]=\"text.validity\" [options]=\"validityOptions()\" [value]=\"validity()\" (valueChange)=\"validity.set($event)\" />\n </div>\n }\n <div class=\"sp-tokens__actions\">\n @if (badges().length) {\n <hm-button type=\"submit\" emphasis=\"high\" icon=\"key\" [disabled]=\"!canSubmit()\" [loading]=\"submitting()\">{{ text.create }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" (clicked)=\"cancelCreate()\">{{ text.cancel }}</hm-button>\n </div>\n </form>\n } @else {\n <div class=\"sp-tokens__actions\">\n <hm-button emphasis=\"medium\" icon=\"plus\" [disabled]=\"atLimit()\" (clicked)=\"startCreate()\">{{ text.newToken }}</hm-button>\n <span class=\"sp-tokens__note\">{{ fmt(text.validCount, { valid: validCount(), max: maxActive() }) }}</span>\n </div>\n @if (atLimit()) {\n <p class=\"sp-tokens__note\">{{ text.limit }}</p>\n }\n }\n }\n}\n", styles: [":host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%}.sp-tokens__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-tokens__note--error{color:var(--hm-danger)}.sp-tokens__note code{font-family:Geist Mono,ui-monospace,monospace}.sp-tokens__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-tokens__item{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-tokens__item+.sp-tokens__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-tokens__item--expired .sp-tokens__item-main strong{color:var(--hm-muted)}.sp-tokens__item-main{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm)}.sp-tokens__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-tokens__meta code{font-family:Geist Mono,ui-monospace,monospace}.sp-tokens__expired{color:var(--hm-danger)}.sp-tokens__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-tokens__form{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-tokens__field{display:flex;flex-direction:column;gap:var(--hm-space-micro)}.sp-tokens__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-tokens__created{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-success);border-radius:var(--hm-radius-control);background:var(--hm-success-bg)}.sp-tokens__created-title{display:flex;align-items:center;gap:var(--hm-space-nano);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-tokens__created-title hm-icon{color:var(--hm-success)}.sp-tokens__secret{display:block;padding:var(--hm-space-nano) var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-family:Geist Mono,ui-monospace,monospace;font-size:var(--hm-font-size-xs);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1628
2487
|
}
|
|
1629
2488
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AppTokensComponent, decorators: [{
|
|
1630
2489
|
type: Component,
|
|
1631
|
-
args: [{ selector: 'spine-app-tokens', imports: [ReactiveFormsModule, ButtonComponent, InputComponent, IconComponent, SegmentedControlComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (!available) {\n <p class=\"sp-tokens__note\">{{ text.unavailable }}</p>\n} @else {\n <!-- Token rec\u00E9m-criado: \u00FAnica vez em que o valor aparece. -->\n @if (created(); as c) {\n <div class=\"sp-tokens__created\" role=\"status\">\n <p class=\"sp-tokens__created-title\">\n <hm-icon shape=\"check-circle\" /> {{ fmt(text.created, { name: c.name }) }}\n </p>\n <code class=\"sp-tokens__secret\">{{ c.token }}</code>\n <p class=\"sp-tokens__note\" [innerHTML]=\"
|
|
2490
|
+
args: [{ selector: 'spine-app-tokens', imports: [ReactiveFormsModule, ButtonComponent, InputComponent, IconComponent, SegmentedControlComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (!available) {\n <p class=\"sp-tokens__note\">{{ text.unavailable }}</p>\n} @else {\n <!-- Token rec\u00E9m-criado: \u00FAnica vez em que o valor aparece. -->\n @if (created(); as c) {\n <div class=\"sp-tokens__created\" role=\"status\">\n <p class=\"sp-tokens__created-title\">\n <hm-icon shape=\"check-circle\" /> {{ fmt(text.created, { name: c.name }) }}\n </p>\n <code class=\"sp-tokens__secret\">{{ c.token }}</code>\n <p class=\"sp-tokens__note\" [innerHTML]=\"fmtHtml(text.sendHeader, { date: formatDate(c.expiresAt) })\"></p>\n <div class=\"sp-tokens__actions\">\n <hm-button emphasis=\"high\" icon=\"copy\" (clicked)=\"copyCreated()\">{{ text.copyToken }}</hm-button>\n <hm-button emphasis=\"minimal\" (clicked)=\"dismissCreated()\">{{ text.saved }}</hm-button>\n </div>\n </div>\n }\n\n @if (loading()) {\n <p class=\"sp-tokens__note\">{{ text.loading }}</p>\n } @else if (loadError()) {\n <p class=\"sp-tokens__note sp-tokens__note--error\">{{ loadError() }}</p>\n } @else {\n @if (tokens().length) {\n <ul class=\"sp-tokens__list\">\n @for (t of tokens(); track t.id) {\n <li class=\"sp-tokens__item\" [class.sp-tokens__item--expired]=\"t.expired\">\n <span class=\"sp-tokens__item-main\">\n <strong>{{ t.name }}</strong>\n <span class=\"sp-tokens__meta\">{{ t.badge_name }} \u00B7 <code>{{ t.prefix }}</code></span>\n <span class=\"sp-tokens__meta\">\n @if (t.expired) { <span class=\"sp-tokens__expired\">{{ fmt(text.expiredOn, { date: formatDate(t.expires_at) }) }}</span> }\n @else { {{ fmt(text.expiresOn, { date: formatDate(t.expires_at) }) }} }\n \u00B7 {{ fmt(text.lastUsed, { date: formatDate(t.last_used_at) }) }}\n </span>\n </span>\n <hm-button emphasis=\"minimal\" intent=\"danger\" (clicked)=\"revoke(t)\">\n {{ confirmingRevoke() === t.id ? text.confirmRevoke : text.revoke }}\n </hm-button>\n </li>\n }\n </ul>\n } @else if (!creating()) {\n <p class=\"sp-tokens__note\">{{ text.empty }}</p>\n }\n\n @if (creating()) {\n <form class=\"sp-tokens__form\" (submit)=\"$event.preventDefault(); submit()\">\n @if (!badges().length) {\n <p class=\"sp-tokens__note\">{{ text.noBadges }}</p>\n } @else {\n @if (clientName()) {\n <p class=\"sp-tokens__note\" [innerHTML]=\"fmtHtml(text.appOnly, { name: clientName() })\"></p>\n }\n @if (badgeOptions().length > 1) {\n <div class=\"sp-tokens__field\">\n <span class=\"sp-tokens__label\">{{ text.accessLevel }}</span>\n <hm-segmented-control emphasis=\"action\" [label]=\"text.accessLevel\" [options]=\"badgeOptions()\" [value]=\"badgeId()\" (valueChange)=\"badgeId.set($event)\" />\n </div>\n }\n <hm-input [label]=\"text.name\" [placeholder]=\"text.namePlaceholder\" [formControl]=\"nameControl\" [messages]=\"{ required: text.nameRequired, maxlength: text.nameMax }\" />\n <div class=\"sp-tokens__field\">\n <span class=\"sp-tokens__label\">{{ text.validity }}</span>\n <hm-segmented-control emphasis=\"action\" [label]=\"text.validity\" [options]=\"validityOptions()\" [value]=\"validity()\" (valueChange)=\"validity.set($event)\" />\n </div>\n }\n <div class=\"sp-tokens__actions\">\n @if (badges().length) {\n <hm-button type=\"submit\" emphasis=\"high\" icon=\"key\" [disabled]=\"!canSubmit()\" [loading]=\"submitting()\">{{ text.create }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" (clicked)=\"cancelCreate()\">{{ text.cancel }}</hm-button>\n </div>\n </form>\n } @else {\n <div class=\"sp-tokens__actions\">\n <hm-button emphasis=\"medium\" icon=\"plus\" [disabled]=\"atLimit()\" (clicked)=\"startCreate()\">{{ text.newToken }}</hm-button>\n <span class=\"sp-tokens__note\">{{ fmt(text.validCount, { valid: validCount(), max: maxActive() }) }}</span>\n </div>\n @if (atLimit()) {\n <p class=\"sp-tokens__note\">{{ text.limit }}</p>\n }\n }\n }\n}\n", styles: [":host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%}.sp-tokens__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-tokens__note--error{color:var(--hm-danger)}.sp-tokens__note code{font-family:Geist Mono,ui-monospace,monospace}.sp-tokens__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-tokens__item{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-tokens__item+.sp-tokens__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-tokens__item--expired .sp-tokens__item-main strong{color:var(--hm-muted)}.sp-tokens__item-main{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm)}.sp-tokens__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-tokens__meta code{font-family:Geist Mono,ui-monospace,monospace}.sp-tokens__expired{color:var(--hm-danger)}.sp-tokens__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-tokens__form{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-tokens__field{display:flex;flex-direction:column;gap:var(--hm-space-micro)}.sp-tokens__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-tokens__created{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-success);border-radius:var(--hm-radius-control);background:var(--hm-success-bg)}.sp-tokens__created-title{display:flex;align-items:center;gap:var(--hm-space-nano);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-tokens__created-title hm-icon{color:var(--hm-success)}.sp-tokens__secret{display:block;padding:var(--hm-space-nano) var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-family:Geist Mono,ui-monospace,monospace;font-size:var(--hm-font-size-xs);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}\n"] }]
|
|
1632
2491
|
}], ctorParameters: () => [] });
|
|
1633
2492
|
|
|
1634
2493
|
/** Dependências que valem mostrar em "Sobre" (as demais são detalhe de build). */
|
|
@@ -1645,9 +2504,9 @@ const KEY_DEPENDENCIES = [
|
|
|
1645
2504
|
];
|
|
1646
2505
|
/**
|
|
1647
2506
|
* Painel de configurações do shell (substitui as telas do antigo módulo "Sistema"): Aparência,
|
|
1648
|
-
* Região, Atalhos, Conta e
|
|
1649
|
-
* `provideSettingsSection`. Tudo vale na hora — cada controle chama
|
|
1650
|
-
* aquela preferência (tema, densidade, menu lateral, fuso).
|
|
2507
|
+
* Região, Atalhos, Conta, Conta e segurança (MFA, códigos de recuperação e sessões do Argos) e Sobre, mais
|
|
2508
|
+
* as seções que o app registrar com `provideSettingsSection`. Tudo vale na hora — cada controle chama
|
|
2509
|
+
* o serviço que já persiste aquela preferência (tema, densidade, menu lateral, fuso).
|
|
1651
2510
|
*
|
|
1652
2511
|
* Aberto por `SpineSettingsService.open()` — menu do perfil, `Ctrl+,` ou `?settings=<seção>`.
|
|
1653
2512
|
*/
|
|
@@ -1667,11 +2526,18 @@ class SettingsPanelComponent {
|
|
|
1667
2526
|
this.shortcutService = inject(ShortcutService);
|
|
1668
2527
|
this.onboarding = inject(OnboardingService);
|
|
1669
2528
|
this.auth = inject(AuthorizationService);
|
|
2529
|
+
this.authConfig = inject(AUTH_CONFIG, { optional: true });
|
|
1670
2530
|
this.toast = inject(HermesToastService);
|
|
1671
2531
|
this.packageInfo = inject(APP_PACKAGE_INFO, { optional: true }) ?? {};
|
|
1672
2532
|
this.config = inject(SPINE_CONFIG, { optional: true });
|
|
1673
2533
|
this.appSections = inject(SPINE_SETTINGS_SECTIONS, { optional: true }) ?? [];
|
|
1674
2534
|
this.appName = this.config?.appName ?? 'Hermes';
|
|
2535
|
+
/**
|
|
2536
|
+
* "Copiar token de acesso" é ferramenta de desenvolvimento: põe o token da sessão (Bearer de tudo que a
|
|
2537
|
+
* pessoa pode fazer) na área de transferência, ao alcance de qualquer app/extensão que leia o clipboard.
|
|
2538
|
+
* Só aparece com `SpineConfig.technicalDocs` (sem configuração, só em `isDevMode()`), como a visão técnica.
|
|
2539
|
+
*/
|
|
2540
|
+
this.showAccessToken = this.config?.technicalDocs ?? isDevMode();
|
|
1675
2541
|
// ── Aparência ───────────────────────────────────────────────────────────────
|
|
1676
2542
|
this.themeOptions = computed(() => [
|
|
1677
2543
|
{ value: 'light', label: this.text.themeLight, icon: 'sun' },
|
|
@@ -1726,6 +2592,11 @@ class SettingsPanelComponent {
|
|
|
1726
2592
|
// ── Atalhos ─────────────────────────────────────────────────────────────────
|
|
1727
2593
|
/** Atalhos registrados (favoritos numa linha só) + os que outra peça dispara — os mesmos do painel "?". */
|
|
1728
2594
|
this.shortcutItems = this.shortcutService.panelItems;
|
|
2595
|
+
this.userStorage = inject(SpineUserStorage);
|
|
2596
|
+
// ── Conta e segurança ───────────────────────────────────────────────────────
|
|
2597
|
+
/** API da conta no Argos (Bearer com o token da sessão), a partir do `AUTH_CONFIG`. Sem ela (sem
|
|
2598
|
+
* `authority`/`realmId`, ou com `CHANGE_ME`) a seção não aparece. */
|
|
2599
|
+
this.accountSecurity = inject(SpineAccountSecurityService).available;
|
|
1729
2600
|
// ── Sobre ───────────────────────────────────────────────────────────────────
|
|
1730
2601
|
this.dependencies = KEY_DEPENDENCIES
|
|
1731
2602
|
.map((d) => ({ ...d, version: this.packageInfo.dependencies?.[d.name] }))
|
|
@@ -1770,6 +2641,8 @@ class SettingsPanelComponent {
|
|
|
1770
2641
|
: formatMessage(this.text.sessionExpiresAt, { time: when });
|
|
1771
2642
|
}
|
|
1772
2643
|
async copyToken() {
|
|
2644
|
+
if (!this.showAccessToken)
|
|
2645
|
+
return;
|
|
1773
2646
|
const token = this.auth.getToken();
|
|
1774
2647
|
if (!token) {
|
|
1775
2648
|
this.toast.warning(this.text.noSessionToken);
|
|
@@ -1779,8 +2652,18 @@ class SettingsPanelComponent {
|
|
|
1779
2652
|
}
|
|
1780
2653
|
logout() {
|
|
1781
2654
|
this.settings.close();
|
|
2655
|
+
this.userStorage.clearHistory();
|
|
1782
2656
|
this.auth.logout();
|
|
1783
2657
|
}
|
|
2658
|
+
/** Login da pessoa (`preferred_username` do token, ou o e-mail), para dizer QUAL conta do Argos. */
|
|
2659
|
+
get accountLogin() {
|
|
2660
|
+
const payload = decodeTokenPayload(this.auth.getToken());
|
|
2661
|
+
const login = payload?.['preferred_username'] ?? payload?.['email'];
|
|
2662
|
+
return typeof login === 'string' ? login : '';
|
|
2663
|
+
}
|
|
2664
|
+
get accountRealm() {
|
|
2665
|
+
return this.authConfig?.realmId ?? '';
|
|
2666
|
+
}
|
|
1784
2667
|
get buildDate() {
|
|
1785
2668
|
const date = this.packageInfo.buildDate;
|
|
1786
2669
|
return date ? new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeStyle: 'short' }).format(new Date(date)) : '';
|
|
@@ -1821,16 +2704,17 @@ class SettingsPanelComponent {
|
|
|
1821
2704
|
this.settings.close();
|
|
1822
2705
|
}
|
|
1823
2706
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1824
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SettingsPanelComponent, isStandalone: true, selector: "spine-settings-panel", ngImport: i0, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"settings.section()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <spine-app-tokens />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [keywords]=\"text.timeZoneKeywords\">\n <span rowDescription class=\"sp-settings__hint\">{{ fmt(text.timeZoneHint, { now: timeZonePreview() }) }}</span>\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n <hm-settings-row [label]=\"text.tour\" [description]=\"text.tourDescription\" [keywords]=\"text.tourKeywords\">\n <hm-button emphasis=\"medium\" icon=\"play\" (clicked)=\"replayTour()\">{{ text.replayTour }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Se\u00E7\u00F5es do app (provideSettingsSection) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (s of appSections; track s.key) {\n <hm-settings-section [key]=\"s.key\" [label]=\"s.label\" [icon]=\"s.icon ?? 'cog'\" [description]=\"s.description ?? ''\" [keywords]=\"s.keywords ?? ''\">\n <ng-container *ngComponentOutlet=\"s.component\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sobre \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.about\" [label]=\"text.about\" icon=\"info-circle\" [keywords]=\"text.aboutKeywords\">\n <hm-settings-row [label]=\"text.application\">\n <span class=\"sp-settings__value\">{{ appName }}@if (packageInfo.name) { <span class=\"sp-settings__muted\"> \u00B7 {{ packageInfo.name }}</span> }</span>\n </hm-settings-row>\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"minimal\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"minimal\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"minimal\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__hint{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text)}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:Geist Mono,ui-monospace,monospace}.sp-settings__links{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-nano)}.sp-settings__deps{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xxxs);padding:var(--hm-space-nano) var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-hover)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: AppTokensComponent, selector: "spine-app-tokens" }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "help", "size", "labelHidden", "clearable", "requiredManual", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SettingsDialogComponent, selector: "hm-settings-dialog", inputs: ["open", "heading", "activeSection", "closeHandler", "actions", "submitLabel", "cancelLabel"], outputs: ["openChange", "activeSectionChange", "closed", "submitted"] }, { kind: "component", type: SettingsRowComponent, selector: "hm-settings-row", inputs: ["label", "description", "keywords", "stacked"] }, { kind: "component", type: SettingsSectionComponent, selector: "hm-settings-section", inputs: ["key", "label", "icon", "description", "keywords", "control", "invalid"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: ShortcutListComponent, selector: "hm-shortcut-list", inputs: ["items", "registered", "searchable", "characterKeysToggle", "headingLevel"] }, { kind: "component", type: ThemePickerComponent, selector: "hm-theme-picker", inputs: ["families", "family", "theme", "showMode"], outputs: ["familyChosen", "themeChosen"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2707
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SettingsPanelComponent, isStandalone: true, selector: "spine-settings-panel", ngImport: i0, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"settings.section()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (showAccessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <spine-app-tokens />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Conta e seguran\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA, c\u00F3digos de recupera\u00E7\u00E3o e sess\u00F5es aqui\n mesmo, sem abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o,\n cadastro em andamento), que morre quando o painel fecha. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.security\" [label]=\"text.security\" icon=\"shield-check\" [description]=\"text.securityDescription\" [keywords]=\"text.securityKeywords\">\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\">\n @if (accountRealm) { <span rowDescription class=\"sp-settings__hint\">{{ fmt(text.signedInRealm, { realm: accountRealm }) }}</span> }\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [keywords]=\"text.timeZoneKeywords\">\n <span rowDescription class=\"sp-settings__hint\">{{ fmt(text.timeZoneHint, { now: timeZonePreview() }) }}</span>\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n <hm-settings-row [label]=\"text.tour\" [description]=\"text.tourDescription\" [keywords]=\"text.tourKeywords\">\n <hm-button emphasis=\"medium\" icon=\"play\" (clicked)=\"replayTour()\">{{ text.replayTour }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Se\u00E7\u00F5es do app (provideSettingsSection) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (s of appSections; track s.key) {\n <hm-settings-section [key]=\"s.key\" [label]=\"s.label\" [icon]=\"s.icon ?? 'cog'\" [description]=\"s.description ?? ''\" [keywords]=\"s.keywords ?? ''\">\n <ng-container *ngComponentOutlet=\"s.component\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sobre \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.about\" [label]=\"text.about\" icon=\"info-circle\" [keywords]=\"text.aboutKeywords\">\n <hm-settings-row [label]=\"text.application\">\n <span class=\"sp-settings__value\">{{ appName }}@if (packageInfo.name) { <span class=\"sp-settings__muted\"> \u00B7 {{ packageInfo.name }}</span> }</span>\n </hm-settings-row>\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"minimal\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"minimal\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"minimal\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__hint{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text)}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:Geist Mono,ui-monospace,monospace}.sp-settings__links{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-nano)}.sp-settings__deps{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xxxs);padding:var(--hm-space-nano) var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-hover)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: AppTokensComponent, selector: "spine-app-tokens" }, { kind: "component", type: AccountMfaComponent, selector: "spine-account-mfa" }, { kind: "component", type: AccountRecoveryCodesComponent, selector: "spine-account-recovery-codes" }, { kind: "component", type: AccountSessionsComponent, selector: "spine-account-sessions" }, { kind: "directive", type: SpineAccountSecurityScopeDirective, selector: "[spineAccountSecurity]" }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "help", "size", "labelHidden", "clearable", "requiredManual", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SettingsDialogComponent, selector: "hm-settings-dialog", inputs: ["open", "heading", "activeSection", "closeHandler", "actions", "submitLabel", "cancelLabel"], outputs: ["openChange", "activeSectionChange", "closed", "submitted"] }, { kind: "component", type: SettingsRowComponent, selector: "hm-settings-row", inputs: ["label", "description", "keywords", "stacked"] }, { kind: "component", type: SettingsSectionComponent, selector: "hm-settings-section", inputs: ["key", "label", "icon", "description", "keywords", "control", "invalid"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: ShortcutListComponent, selector: "hm-shortcut-list", inputs: ["items", "registered", "searchable", "characterKeysToggle", "headingLevel"] }, { kind: "component", type: ThemePickerComponent, selector: "hm-theme-picker", inputs: ["families", "family", "theme", "showMode"], outputs: ["familyChosen", "themeChosen"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1825
2708
|
}
|
|
1826
2709
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, decorators: [{
|
|
1827
2710
|
type: Component,
|
|
1828
2711
|
args: [{ selector: 'spine-settings-panel', imports: [
|
|
1829
2712
|
NgComponentOutlet, AppTokensComponent,
|
|
2713
|
+
AccountMfaComponent, AccountRecoveryCodesComponent, AccountSessionsComponent, SpineAccountSecurityScopeDirective,
|
|
1830
2714
|
AccountAvatarComponent, ButtonComponent, ComboboxComponent, IconComponent,
|
|
1831
2715
|
SegmentedControlComponent, SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent,
|
|
1832
2716
|
SelectComponent, ShortcutListComponent, ThemePickerComponent,
|
|
1833
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"settings.section()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <spine-app-tokens />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [keywords]=\"text.timeZoneKeywords\">\n <span rowDescription class=\"sp-settings__hint\">{{ fmt(text.timeZoneHint, { now: timeZonePreview() }) }}</span>\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n <hm-settings-row [label]=\"text.tour\" [description]=\"text.tourDescription\" [keywords]=\"text.tourKeywords\">\n <hm-button emphasis=\"medium\" icon=\"play\" (clicked)=\"replayTour()\">{{ text.replayTour }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Se\u00E7\u00F5es do app (provideSettingsSection) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (s of appSections; track s.key) {\n <hm-settings-section [key]=\"s.key\" [label]=\"s.label\" [icon]=\"s.icon ?? 'cog'\" [description]=\"s.description ?? ''\" [keywords]=\"s.keywords ?? ''\">\n <ng-container *ngComponentOutlet=\"s.component\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sobre \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.about\" [label]=\"text.about\" icon=\"info-circle\" [keywords]=\"text.aboutKeywords\">\n <hm-settings-row [label]=\"text.application\">\n <span class=\"sp-settings__value\">{{ appName }}@if (packageInfo.name) { <span class=\"sp-settings__muted\"> \u00B7 {{ packageInfo.name }}</span> }</span>\n </hm-settings-row>\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"minimal\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"minimal\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"minimal\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__hint{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text)}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:Geist Mono,ui-monospace,monospace}.sp-settings__links{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-nano)}.sp-settings__deps{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xxxs);padding:var(--hm-space-nano) var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-hover)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"] }]
|
|
2717
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"settings.section()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (showAccessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <spine-app-tokens />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Conta e seguran\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA, c\u00F3digos de recupera\u00E7\u00E3o e sess\u00F5es aqui\n mesmo, sem abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o,\n cadastro em andamento), que morre quando o painel fecha. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.security\" [label]=\"text.security\" icon=\"shield-check\" [description]=\"text.securityDescription\" [keywords]=\"text.securityKeywords\">\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\">\n @if (accountRealm) { <span rowDescription class=\"sp-settings__hint\">{{ fmt(text.signedInRealm, { realm: accountRealm }) }}</span> }\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [keywords]=\"text.timeZoneKeywords\">\n <span rowDescription class=\"sp-settings__hint\">{{ fmt(text.timeZoneHint, { now: timeZonePreview() }) }}</span>\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n <hm-settings-row [label]=\"text.tour\" [description]=\"text.tourDescription\" [keywords]=\"text.tourKeywords\">\n <hm-button emphasis=\"medium\" icon=\"play\" (clicked)=\"replayTour()\">{{ text.replayTour }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Se\u00E7\u00F5es do app (provideSettingsSection) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (s of appSections; track s.key) {\n <hm-settings-section [key]=\"s.key\" [label]=\"s.label\" [icon]=\"s.icon ?? 'cog'\" [description]=\"s.description ?? ''\" [keywords]=\"s.keywords ?? ''\">\n <ng-container *ngComponentOutlet=\"s.component\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sobre \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.about\" [label]=\"text.about\" icon=\"info-circle\" [keywords]=\"text.aboutKeywords\">\n <hm-settings-row [label]=\"text.application\">\n <span class=\"sp-settings__value\">{{ appName }}@if (packageInfo.name) { <span class=\"sp-settings__muted\"> \u00B7 {{ packageInfo.name }}</span> }</span>\n </hm-settings-row>\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"minimal\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"minimal\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"minimal\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__hint{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text)}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:Geist Mono,ui-monospace,monospace}.sp-settings__links{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-nano)}.sp-settings__deps{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xxxs);padding:var(--hm-space-nano) var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-hover)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"] }]
|
|
1834
2718
|
}] });
|
|
1835
2719
|
/** "UTC−03:00" para um fuso IANA. */
|
|
1836
2720
|
function offsetOf(timeZone) {
|
|
@@ -2389,11 +3273,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
2389
3273
|
args: [{ selector: '[appPanelLocalLink]' }]
|
|
2390
3274
|
}], ctorParameters: () => [], propDecorators: { appPanelLocalLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "appPanelLocalLink", required: false }] }], localNavigate: [{ type: i0.Output, args: ["localNavigate"] }] } });
|
|
2391
3275
|
|
|
3276
|
+
/**
|
|
3277
|
+
* Perfil do DOMPurify para o Markdown do spine (documentação de tela, novidades). Mais restrito que o
|
|
3278
|
+
* padrão: o HTML entra no DOM do shell (sem shadow DOM), então
|
|
3279
|
+
* - `<style>` e o atributo `style` ficam de fora (CSS valeria para a página inteira: esconder/redesenhar a
|
|
3280
|
+
* UI, sobrepor um aviso falso com `position: fixed`, exfiltrar por seletor de atributo);
|
|
3281
|
+
* - formulário e controles (`<form>`, `<input>`, `<button>`, `<textarea>`, `<select>`) e `<iframe>` ficam de
|
|
3282
|
+
* fora (tela de login falsa dentro do conteúdo);
|
|
3283
|
+
* - link para outra origem abre em outra aba com `rel="noopener noreferrer"`.
|
|
3284
|
+
*/
|
|
3285
|
+
const SPINE_MARKDOWN_PURIFY_CONFIG = {
|
|
3286
|
+
FORBID_TAGS: ['style', 'form', 'input', 'button', 'textarea', 'select', 'option', 'iframe', 'frame', 'frameset', 'object', 'embed', 'link', 'meta', 'base'],
|
|
3287
|
+
FORBID_ATTR: ['style', 'target', 'formaction', 'action'],
|
|
3288
|
+
ALLOW_DATA_ATTR: false,
|
|
3289
|
+
};
|
|
3290
|
+
let instance;
|
|
3291
|
+
/** Instância própria (o hook de links não vaza para o DOMPurify global que o app use). `null` sem DOM. */
|
|
3292
|
+
function purifier() {
|
|
3293
|
+
if (instance !== undefined)
|
|
3294
|
+
return instance;
|
|
3295
|
+
if (typeof window === 'undefined')
|
|
3296
|
+
return (instance = null);
|
|
3297
|
+
const purify = DOMPurify(window);
|
|
3298
|
+
if (!purify.isSupported)
|
|
3299
|
+
return (instance = null);
|
|
3300
|
+
purify.addHook('afterSanitizeAttributes', (node) => {
|
|
3301
|
+
if (node.nodeName !== 'A')
|
|
3302
|
+
return;
|
|
3303
|
+
const href = node.getAttribute('href');
|
|
3304
|
+
if (!href)
|
|
3305
|
+
return;
|
|
3306
|
+
let external = false;
|
|
3307
|
+
try {
|
|
3308
|
+
external = new URL(href, window.location.href).origin !== window.location.origin;
|
|
3309
|
+
}
|
|
3310
|
+
catch {
|
|
3311
|
+
external = false;
|
|
3312
|
+
}
|
|
3313
|
+
if (external) {
|
|
3314
|
+
node.setAttribute('target', '_blank');
|
|
3315
|
+
node.setAttribute('rel', 'noopener noreferrer');
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
return (instance = purify);
|
|
3319
|
+
}
|
|
3320
|
+
/** HTML limpo pelo perfil do spine, ou `null` quando não há DOM (SSR: melhor não renderizar). */
|
|
3321
|
+
function sanitizeSpineMarkdownHtml(html) {
|
|
3322
|
+
const purify = purifier();
|
|
3323
|
+
return purify ? purify.sanitize(html, SPINE_MARKDOWN_PURIFY_CONFIG) : null;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
2392
3326
|
// Instância própria: não mexe nas opções globais do `marked` que o app possa usar.
|
|
2393
3327
|
const parser = new Marked({ gfm: true, breaks: false, async: false });
|
|
2394
3328
|
/**
|
|
2395
3329
|
* Renderiza Markdown (GFM) como HTML com a tipografia do design system. O texto é tratado como dado:
|
|
2396
|
-
* o HTML gerado passa pelo DOMPurify
|
|
3330
|
+
* o HTML gerado passa pelo DOMPurify, com o perfil restrito do spine (`SPINE_MARKDOWN_PURIFY_CONFIG`: sem
|
|
3331
|
+
* `<style>`/`style`, formulários e `<iframe>`; link externo com `rel="noopener noreferrer"`), antes de chegar ao DOM.
|
|
2397
3332
|
*/
|
|
2398
3333
|
class SpineMarkdownComponent {
|
|
2399
3334
|
constructor() {
|
|
@@ -2403,10 +3338,10 @@ class SpineMarkdownComponent {
|
|
|
2403
3338
|
this.html = computed(() => {
|
|
2404
3339
|
const source = this.content();
|
|
2405
3340
|
// Sem DOM (SSR) o DOMPurify não limpa nada; melhor não renderizar.
|
|
2406
|
-
if (!source
|
|
3341
|
+
if (!source)
|
|
2407
3342
|
return null;
|
|
2408
|
-
const clean =
|
|
2409
|
-
return this.#sanitizer.bypassSecurityTrustHtml(clean);
|
|
3343
|
+
const clean = sanitizeSpineMarkdownHtml(parser.parse(source));
|
|
3344
|
+
return clean === null ? null : this.#sanitizer.bypassSecurityTrustHtml(clean);
|
|
2410
3345
|
}, ...(ngDevMode ? [{ debugName: "html" }] : /* istanbul ignore next */ []));
|
|
2411
3346
|
}
|
|
2412
3347
|
#sanitizer;
|
|
@@ -2706,6 +3641,7 @@ class LayoutComponent {
|
|
|
2706
3641
|
this.preferences = inject(SpinePreferencesService);
|
|
2707
3642
|
this.workspace = inject(SpineWorkspaceNavService);
|
|
2708
3643
|
this.settings = inject(SpineSettingsService);
|
|
3644
|
+
this.userStorage = inject(SpineUserStorage);
|
|
2709
3645
|
this.isNoPermission = this.matchesNoPermission(this.router.url);
|
|
2710
3646
|
this.router.events.pipe(filter((e) => e instanceof NavigationEnd))
|
|
2711
3647
|
.subscribe(e => { this.isNoPermission = this.matchesNoPermission(e.urlAfterRedirects); });
|
|
@@ -2719,7 +3655,10 @@ class LayoutComponent {
|
|
|
2719
3655
|
navigate: (url) => this.router.navigateByUrl(url),
|
|
2720
3656
|
toggleTheme: () => this.theme.setTheme(this.theme.current === 'light' ? 'dark' : 'light'),
|
|
2721
3657
|
toggleFullscreen: () => this.toggleFullscreen(),
|
|
2722
|
-
logout: () =>
|
|
3658
|
+
logout: () => {
|
|
3659
|
+
this.userStorage.clearHistory();
|
|
3660
|
+
this.auth.logout();
|
|
3661
|
+
},
|
|
2723
3662
|
openSettings: () => this.settings.toggle(),
|
|
2724
3663
|
});
|
|
2725
3664
|
// Link direto pra uma seção do painel de configurações: `?settings=aparencia` (ou só
|
|
@@ -2730,9 +3669,12 @@ class LayoutComponent {
|
|
|
2730
3669
|
this.settings.open(params.get('settings') ?? '');
|
|
2731
3670
|
void this.router.navigate([], { queryParams: { settings: null }, queryParamsHandling: 'merge', replaceUrl: true });
|
|
2732
3671
|
}
|
|
2733
|
-
// Onboarding interativo na primeira entrada nesta máquina.
|
|
3672
|
+
// Onboarding interativo na primeira entrada nesta máquina. Vem ANTES das novidades: quando devido,
|
|
3673
|
+
// o tour fica `pending` na hora, e as novidades (que esperam o tour ficar `idle`) já o enxergam.
|
|
3674
|
+
// A ordem inversa também funciona — elas conferem o estado de novo antes de abrir —, mas tour
|
|
3675
|
+
// primeiro é o caminho direto.
|
|
2734
3676
|
this.onboarding.maybeStart();
|
|
2735
|
-
// Novidades do app (provideSpineNews): abrem sozinhas uma vez, depois do tour.
|
|
3677
|
+
// Novidades do app (provideSpineNews): abrem sozinhas uma vez, só depois do tour.
|
|
2736
3678
|
this.news.scheduleOnEntry();
|
|
2737
3679
|
}
|
|
2738
3680
|
onLocalNavigate(panelId, href) {
|
|
@@ -2844,5 +3786,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
2844
3786
|
* Generated bundle index. Do not edit.
|
|
2845
3787
|
*/
|
|
2846
3788
|
|
|
2847
|
-
export { AppLauncherComponent, HeaderComponent, LayoutComponent, OnboardingOverlayComponent, OnboardingService, SPINE_BRAND, SPINE_SETTINGS_KEYS, SPINE_SETTINGS_SECTIONS, SettingsPanelComponent, ShortcutService, ShortcutsCheatsheetComponent, SidebarComponent, SpineAppTokensService, SpineBrandComponent, SpineConnectionBannerComponent, SpineDocsComponent, SpineDocsService, SpineMarkdownComponent, SpineNewsComponent, SpineNewsService, SpineSettingsService, findTourTarget, provideSettingsSection };
|
|
3789
|
+
export { AppLauncherComponent, HeaderComponent, LayoutComponent, OnboardingOverlayComponent, OnboardingService, SPINE_BRAND, SPINE_MARKDOWN_PURIFY_CONFIG, SPINE_SETTINGS_KEYS, SPINE_SETTINGS_SECTIONS, SettingsPanelComponent, ShortcutService, ShortcutsCheatsheetComponent, SidebarComponent, SpineAccountSecurityService, SpineAppTokensService, SpineBrandComponent, SpineConnectionBannerComponent, SpineDocsComponent, SpineDocsService, SpineMarkdownComponent, SpineNewsComponent, SpineNewsService, SpineSettingsService, accountErrorCode, argosAccountApi, findTourTarget, provideSettingsSection, sanitizeSpineMarkdownHtml };
|
|
2848
3790
|
//# sourceMappingURL=trismegisto-spine-layout.mjs.map
|