@trismegisto/spine 2.1.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -19
- package/fesm2022/trismegisto-spine-audit.mjs +264 -10
- package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-BAKdkVdz.mjs +84 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-BAKdkVdz.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-CAXMP5r8.mjs +87 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-CAXMP5r8.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-audit.technical-C901aIIQ.mjs → trismegisto-spine-auto-service-audit.technical-B75d4mh2.mjs} +6 -5
- package/fesm2022/trismegisto-spine-auto-service-audit.technical-B75d4mh2.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-audit.usage-GIJ2RwuH.mjs → trismegisto-spine-auto-service-audit.usage-IdXMruv5.mjs} +9 -8
- package/fesm2022/trismegisto-spine-auto-service-audit.usage-IdXMruv5.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-CCiz-q8o.mjs → trismegisto-spine-auto-service-auto-service-audit.component-BIL8tH1I.mjs} +4 -4
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-CCiz-q8o.mjs.map → trismegisto-spine-auto-service-auto-service-audit.component-BIL8tH1I.mjs.map} +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client-picker.component-Z0wCANF6.mjs → trismegisto-spine-auto-service-auto-service-client-picker.component-CBt_0Uli.mjs} +3 -3
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client-picker.component-Z0wCANF6.mjs.map → trismegisto-spine-auto-service-auto-service-client-picker.component-CBt_0Uli.mjs.map} +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-C7SPavrX.mjs → trismegisto-spine-auto-service-auto-service-client.component-DAxhZC-a.mjs} +123 -27
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-DAxhZC-a.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-CoJYTtBU.mjs +87 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-CoJYTtBU.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-UVMGF7-B.mjs +544 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-UVMGF7-B.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-login-attempts.component-Boi-kG0G.mjs +37 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-login-attempts.component-Boi-kG0G.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DmLafUkX.mjs +943 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DmLafUkX.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-client.technical-BHWhpyWv.mjs → trismegisto-spine-auto-service-client.technical-DP5G8i3o.mjs} +24 -3
- package/fesm2022/trismegisto-spine-auto-service-client.technical-DP5G8i3o.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-client.usage-CM2WB2e2.mjs → trismegisto-spine-auto-service-client.usage-CcJANb20.mjs} +15 -6
- package/fesm2022/trismegisto-spine-auto-service-client.usage-CcJANb20.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-BlktCM70.mjs +65 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-BlktCM70.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Cz5sCKgn.mjs +81 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Cz5sCKgn.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-home.component-vl3BPFnk.mjs → trismegisto-spine-auto-service-home.component-5_aji1hf.mjs} +5 -4
- package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-login-attempts.technical-CasKIIws.mjs +37 -0
- package/fesm2022/trismegisto-spine-auto-service-login-attempts.technical-CasKIIws.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-login-attempts.usage-BMgLNu7W.mjs +60 -0
- package/fesm2022/trismegisto-spine-auto-service-login-attempts.usage-BMgLNu7W.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service.mjs +49 -29
- package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-layout.mjs +1850 -217
- package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-shell.mjs +28 -8
- package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
- package/fesm2022/trismegisto-spine.mjs +1163 -87
- package/fesm2022/trismegisto-spine.mjs.map +1 -1
- package/package.json +6 -6
- package/types/trismegisto-spine-audit.d.ts +180 -5
- package/types/trismegisto-spine-audit.d.ts.map +1 -1
- package/types/trismegisto-spine-auto-service.d.ts +100 -7
- package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
- package/types/trismegisto-spine-layout.d.ts +885 -22
- package/types/trismegisto-spine-layout.d.ts.map +1 -1
- package/types/trismegisto-spine-shell.d.ts +11 -2
- package/types/trismegisto-spine-shell.d.ts.map +1 -1
- package/types/trismegisto-spine.d.ts +496 -34
- package/types/trismegisto-spine.d.ts.map +1 -1
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-ClMKB9cK.mjs +0 -48
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-ClMKB9cK.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-Cf5SG-oQ.mjs +0 -55
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-Cf5SG-oQ.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-audit.technical-C901aIIQ.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-audit.usage-GIJ2RwuH.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-C7SPavrX.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-6Bk46MOf.mjs +0 -39
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-6Bk46MOf.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-CqIVe2LU.mjs +0 -357
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-CqIVe2LU.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-BLN49F4a.mjs +0 -502
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-BLN49F4a.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-client.technical-BHWhpyWv.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-client.usage-CM2WB2e2.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-jQlwaQPn.mjs +0 -39
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-jQlwaQPn.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Bh7ig2zZ.mjs +0 -54
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Bh7ig2zZ.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-home.component-vl3BPFnk.mjs.map +0 -1
|
@@ -1,22 +1,22 @@
|
|
|
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,
|
|
2
|
+
import { inject, ChangeDetectorRef, ChangeDetectionStrategy, Component, InjectionToken, input, signal, computed, Optional, Inject, Injectable, DestroyRef, Injector, afterNextRender, ViewChild, effect, HostListener, DOCUMENT, PLATFORM_ID, untracked, NgZone, isDevMode, Directive, viewChild, 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,
|
|
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, HermesThemeService, HERMES_THEME_CONFIG, HermesSidebarService, HermesToastService, AlertComponent, SegmentedControlComponent, TagComponent, CodeInputComponent, RadioComponent, CheckboxComponent, CodeViewerComponent, SelectComponent, HERMES_SIDEBAR_CONFIG, HermesI18nService, ComboboxComponent, SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent, ThemePickerComponent, ToggletipComponent, BreadcrumbComponent, TooltipDirective, SkeletonComponent, WorkspaceComponent, ToastStackComponent } from '@trismegisto/angular';
|
|
7
7
|
import { ModalComponent as ModalComponent$1 } from '@trismegisto/spine/modal';
|
|
8
|
-
import { NotificationComponent } from '@trismegisto/spine/notification';
|
|
8
|
+
import { NotificationService, AlertType, 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, SpineUserStorage, SPINE_USER_STORAGE_KEYS, spineRoutesOf, routerIdOf, SpinePreferencesService,
|
|
11
|
+
import { SPINE_MESSAGES, SPINE_CONFIG, ThemeService, resolveSpineLogo, SPINE_NEWS, SpineUserStorage, SPINE_USER_STORAGE_KEYS, spineRoutesOf, routerIdOf, SpinePreferencesService, SpineQuickAccessService, DensityService, ThemePresetService, SPINE_IDLE_OPTIONS, SPINE_ADMIN_API, APP_PACKAGE_INFO } from '@trismegisto/spine';
|
|
12
12
|
import * as i2 from '@angular/common';
|
|
13
|
-
import { CommonModule, NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
|
|
13
|
+
import { CommonModule, NgTemplateOutlet, NgComponentOutlet, isPlatformBrowser } 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 {
|
|
17
|
+
import { AuthorizationService, ViewService, RealmAppsService, AUTH_CONFIG } from '@orfeu/angular/core';
|
|
18
18
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
19
|
-
import { formatMessage,
|
|
19
|
+
import { formatMessage, prefersReducedMotion, motionPreferenceOf, urlPath, pluralMessage, formatMessageHtml, HERMES_FONTS, HERMES_TEXT_SCALES, weekdayNames, FALLBACK_FAMILY } from '@trismegisto/primitives';
|
|
20
20
|
import { resolveEffectiveRouteChain, ComponentDistributorService, componentRouterId, LoggerService } from '@trismegisto/spine/routing';
|
|
21
21
|
import { renderIconSvg } from '@trismegisto/icons';
|
|
22
22
|
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
|
@@ -474,15 +474,120 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
474
474
|
args: [{ providedIn: 'root' }]
|
|
475
475
|
}], ctorParameters: () => [] });
|
|
476
476
|
|
|
477
|
+
/** Tempo do aviso de falha na tela (segundos). */
|
|
478
|
+
const LOGOUT_FAILED_SECONDS = 8;
|
|
479
|
+
/**
|
|
480
|
+
* Saída da sessão pelo shell: todo botão/atalho de sair do spine passa por aqui (menu do perfil, painel de
|
|
481
|
+
* configurações, `Mod+Shift+L`, tela `/blocked`).
|
|
482
|
+
*
|
|
483
|
+
* - Assina `logoutFailed$` do `@orfeu/angular` (1.3.x) uma vez só: quando o logout no Argos falha (a lib repete uma
|
|
484
|
+
* vez e mantém a sessão), mostra o aviso de erro `session.logoutFailed` (`hm-alert` de erro, `role="alert"`). Vale
|
|
485
|
+
* também para um `logout()` chamado direto pelo app.
|
|
486
|
+
* - `logout()` aguarda o resultado: só apaga os históricos do usuário (`SpineUserStorage.clearHistory()`) quando a
|
|
487
|
+
* saída deu certo. Com falha, nada muda: a pessoa continua na tela, com a sessão e os recentes.
|
|
488
|
+
* - `pending` fica `true` enquanto a saída está em andamento e sempre volta a `false` (os botões não travam).
|
|
489
|
+
*
|
|
490
|
+
* Com `@orfeu/angular` 1.0.x (sem `logoutFailed$`, `logout()` sem retorno) a saída conta como concluída, como antes.
|
|
491
|
+
*/
|
|
492
|
+
class SpineLogoutService {
|
|
493
|
+
constructor() {
|
|
494
|
+
this.auth = inject(AuthorizationService);
|
|
495
|
+
this.userStorage = inject(SpineUserStorage);
|
|
496
|
+
this.notifications = inject(NotificationService);
|
|
497
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
498
|
+
this.inFlight = null;
|
|
499
|
+
/** Saída em andamento (o botão mostra "saindo…" / fica desabilitado). */
|
|
500
|
+
this.pending = signal(false, ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
|
|
501
|
+
const failed$ = this.auth.logoutFailed$;
|
|
502
|
+
this.failureStream = typeof failed$?.subscribe === 'function';
|
|
503
|
+
if (!this.failureStream)
|
|
504
|
+
return;
|
|
505
|
+
const sub = failed$.subscribe(() => this.notifyFailure());
|
|
506
|
+
inject(DestroyRef).onDestroy(() => sub.unsubscribe());
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Sai da sessão. Resolve `true` quando saiu (o `@orfeu/angular` já navega para o `/logoff`) e `false` quando o
|
|
510
|
+
* Argos não confirmou: a sessão continua e o aviso de erro aparece. Nunca rejeita. Cliques repetidos durante a
|
|
511
|
+
* saída reaproveitam a mesma tentativa.
|
|
512
|
+
*/
|
|
513
|
+
logout() {
|
|
514
|
+
this.inFlight ??= this.run().finally(() => {
|
|
515
|
+
this.inFlight = null;
|
|
516
|
+
this.pending.set(false);
|
|
517
|
+
});
|
|
518
|
+
return this.inFlight;
|
|
519
|
+
}
|
|
520
|
+
async run() {
|
|
521
|
+
this.pending.set(true);
|
|
522
|
+
// Guarda de quem é a sessão antes de sair: depois do logout o token some, e a limpeza precisa do usuário.
|
|
523
|
+
this.userStorage.userId();
|
|
524
|
+
let ok;
|
|
525
|
+
let rejected = false;
|
|
526
|
+
try {
|
|
527
|
+
// 1.3.x: Promise<boolean>; 1.0.x: void (a saída local sempre acontecia).
|
|
528
|
+
ok = (await this.auth.logout()) !== false;
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
ok = false;
|
|
532
|
+
rejected = true;
|
|
533
|
+
}
|
|
534
|
+
if (ok) {
|
|
535
|
+
this.userStorage.clearHistory();
|
|
536
|
+
}
|
|
537
|
+
else if (rejected || !this.failureStream) {
|
|
538
|
+
// Sem `logoutFailed$` para avisar (ou a chamada nem chegou a resolver): o próprio resultado avisa.
|
|
539
|
+
this.notifyFailure();
|
|
540
|
+
}
|
|
541
|
+
return ok;
|
|
542
|
+
}
|
|
543
|
+
notifyFailure() {
|
|
544
|
+
this.notifications.addNotification({
|
|
545
|
+
timer: LOGOUT_FAILED_SECONDS,
|
|
546
|
+
hermesAlertType: AlertType.DANGER,
|
|
547
|
+
message: this.messages.session.logoutFailed,
|
|
548
|
+
hermesAlertClosable: true,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineLogoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
552
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineLogoutService, providedIn: 'root' }); }
|
|
553
|
+
}
|
|
554
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineLogoutService, decorators: [{
|
|
555
|
+
type: Injectable,
|
|
556
|
+
args: [{ providedIn: 'root' }]
|
|
557
|
+
}], ctorParameters: () => [] });
|
|
558
|
+
|
|
477
559
|
/** Seções que o próprio spine já traz. `key` é também o valor de `?settings=` na URL. */
|
|
478
560
|
const SPINE_SETTINGS_KEYS = {
|
|
479
561
|
appearance: 'aparencia',
|
|
562
|
+
/** Tamanho do texto e animações. */
|
|
563
|
+
accessibility: 'acessibilidade',
|
|
480
564
|
region: 'regiao',
|
|
481
565
|
shortcuts: 'atalhos',
|
|
566
|
+
/** Novidades e tour guiado. */
|
|
567
|
+
help: 'ajuda',
|
|
568
|
+
/** Tipos de aviso do app e aviso do navegador (`SpineFeatures.notices`). */
|
|
569
|
+
notices: 'notificacoes',
|
|
570
|
+
/** Listas guardadas neste navegador (recentes, buscas, favoritos) e restaurar preferências. */
|
|
571
|
+
data: 'dados',
|
|
482
572
|
account: 'conta',
|
|
573
|
+
/** Verificação em duas etapas e códigos de recuperação (API da conta do Argos). */
|
|
574
|
+
mfa: 'mfa',
|
|
575
|
+
/** Sessão atual, token de acesso, tokens de aplicação e sessões e dispositivos. */
|
|
576
|
+
sessions: 'sessoes',
|
|
577
|
+
/** @deprecated A seção "Conta e segurança" virou "MFA" e "Sessões": `seguranca` abre a de MFA (ver
|
|
578
|
+
* `SPINE_SETTINGS_ALIASES`). Use `mfa` ou `sessions`. */
|
|
483
579
|
security: 'seguranca',
|
|
484
580
|
about: 'sobre',
|
|
485
581
|
};
|
|
582
|
+
/** Chaves antigas de `?settings=` que continuam abrindo a seção que as substituiu (links já publicados). */
|
|
583
|
+
const SPINE_SETTINGS_ALIASES = {
|
|
584
|
+
[SPINE_SETTINGS_KEYS.security]: SPINE_SETTINGS_KEYS.mfa,
|
|
585
|
+
};
|
|
586
|
+
/** Chave de seção que vale hoje: troca uma chave antiga pela nova; as demais passam iguais. */
|
|
587
|
+
function resolveSettingsSection(section) {
|
|
588
|
+
// hasOwn: `?settings=constructor` não pode devolver o que vem do protótipo.
|
|
589
|
+
return Object.hasOwn(SPINE_SETTINGS_ALIASES, section) ? SPINE_SETTINGS_ALIASES[section] : section;
|
|
590
|
+
}
|
|
486
591
|
const SPINE_SETTINGS_SECTIONS = new InjectionToken('SPINE_SETTINGS_SECTIONS');
|
|
487
592
|
/** Registra uma seção do app no painel de configurações (aparece depois das do spine, antes de
|
|
488
593
|
* "Sobre"). Pode ser chamado várias vezes, uma por seção. */
|
|
@@ -496,8 +601,9 @@ class SpineSettingsService {
|
|
|
496
601
|
/** Seção pedida na abertura (vazio = a primeira). */
|
|
497
602
|
this.section = signal('', ...(ngDevMode ? [{ debugName: "section" }] : /* istanbul ignore next */ []));
|
|
498
603
|
}
|
|
604
|
+
/** `section` aceita também as chaves antigas (`seguranca` abre "MFA"). */
|
|
499
605
|
open(section = '') {
|
|
500
|
-
this.section.set(section);
|
|
606
|
+
this.section.set(resolveSettingsSection(section));
|
|
501
607
|
this.isOpen.set(true);
|
|
502
608
|
}
|
|
503
609
|
close() {
|
|
@@ -698,6 +804,7 @@ class HeaderComponent {
|
|
|
698
804
|
this.settings = inject(SpineSettingsService);
|
|
699
805
|
this.news = inject(SpineNewsService);
|
|
700
806
|
this.userStorage = inject(SpineUserStorage);
|
|
807
|
+
this.logoutService = inject(SpineLogoutService);
|
|
701
808
|
this.quickQuery = '';
|
|
702
809
|
this.quickOpen = false;
|
|
703
810
|
this.quickResults = [];
|
|
@@ -708,6 +815,10 @@ class HeaderComponent {
|
|
|
708
815
|
/** Verdadeiro quando o painel está exibindo o histórico (campo vazio). */
|
|
709
816
|
this.showingRecent = false;
|
|
710
817
|
this.appName = config?.appName ?? 'Hermes';
|
|
818
|
+
// Lista apagada fora daqui (logout, Configurações › Dados do navegador) ou outra conta: relê, para a
|
|
819
|
+
// próxima busca não ressuscitar o que foi apagado.
|
|
820
|
+
const stop = this.userStorage.onChange(() => (this.recentSearches = this.loadRecent()));
|
|
821
|
+
inject(DestroyRef).onDestroy(stop);
|
|
711
822
|
}
|
|
712
823
|
async ngOnInit() {
|
|
713
824
|
this.updateView(this.viewService.currentView);
|
|
@@ -776,9 +887,9 @@ class HeaderComponent {
|
|
|
776
887
|
async switchBadge(badgeTypeId) {
|
|
777
888
|
await this.authService.switchBadge(badgeTypeId);
|
|
778
889
|
}
|
|
890
|
+
/** Sai pelo `SpineLogoutService`: com falha no Argos a pessoa fica aqui, com aviso, e o histórico fica. */
|
|
779
891
|
logout() {
|
|
780
|
-
this.
|
|
781
|
-
this.authService.logout();
|
|
892
|
+
return this.logoutService.logout();
|
|
782
893
|
}
|
|
783
894
|
/** Reabre o tour guiado de onboarding. */
|
|
784
895
|
startTour() { this.onboarding.restart(); }
|
|
@@ -805,7 +916,8 @@ class HeaderComponent {
|
|
|
805
916
|
// Só quando o app registrou alguma novidade vigente (provideSpineNews).
|
|
806
917
|
if (this.news.hasAny())
|
|
807
918
|
items.push({ id: 'news', label: this.text.news, icon: 'star' });
|
|
808
|
-
|
|
919
|
+
// Desabilitado só enquanto a saída está em andamento; volta sozinho se ela falhar.
|
|
920
|
+
items.push({ id: 'logout', label: this.text.logout, icon: 'logout', danger: true, separatorAbove: true, disabled: this.logoutService.pending() });
|
|
809
921
|
return items;
|
|
810
922
|
}
|
|
811
923
|
onProfileMenuChosen(id) {
|
|
@@ -831,7 +943,7 @@ class HeaderComponent {
|
|
|
831
943
|
this.settings.open(SPINE_SETTINGS_KEYS.appearance);
|
|
832
944
|
return;
|
|
833
945
|
case 'logout':
|
|
834
|
-
this.logout();
|
|
946
|
+
void this.logout();
|
|
835
947
|
return;
|
|
836
948
|
}
|
|
837
949
|
}
|
|
@@ -1289,7 +1401,8 @@ class OnboardingOverlayComponent {
|
|
|
1289
1401
|
const rect = target.getBoundingClientRect();
|
|
1290
1402
|
if (rect.top >= 0 && rect.bottom <= window.innerHeight)
|
|
1291
1403
|
return;
|
|
1292
|
-
|
|
1404
|
+
// Escolha do app (Configurações › Acessibilidade) ou, sem ela, o sistema.
|
|
1405
|
+
const calm = prefersReducedMotion();
|
|
1293
1406
|
target.scrollIntoView({ block: 'center', inline: 'nearest', behavior: calm ? 'auto' : 'smooth' });
|
|
1294
1407
|
}
|
|
1295
1408
|
/** Largura que o CSS dá ao balão (medida quando já existe) e a altura informada. */
|
|
@@ -1396,10 +1509,14 @@ function buildNewsIllustration(illustration, palette) {
|
|
|
1396
1509
|
const y = round(CY + 104 * Math.sin(angle));
|
|
1397
1510
|
parts.push(`<g transform="translate(${x} ${y})"><g class="sat" style="animation-delay:${-(i * 1.3).toFixed(1)}s">`, `<circle r="28" fill="${c.badge}" stroke="${c.border}" stroke-width="2"/>`, icon(sat.icon, -14, -14, 28, palette.icon), `<text y="48" text-anchor="middle" font-size="15" fill="${c.label}">${xml(sat.label)}</text>`, `</g></g>`);
|
|
1398
1511
|
});
|
|
1399
|
-
|
|
1512
|
+
// A imagem é um data URI: o `data-motion` do app (Configurações › Acessibilidade) não chega lá dentro.
|
|
1513
|
+
// Com escolha no app, ela decide aqui, ao gerar; sem escolha, a media query da própria imagem segue o sistema.
|
|
1514
|
+
const motion = typeof document === 'undefined' ? 'system' : motionPreferenceOf(document.documentElement);
|
|
1515
|
+
const style = orbiting && motion !== 'reduce'
|
|
1400
1516
|
? '<style>.sat{animation:drift 5.6s ease-in-out infinite alternate}' +
|
|
1401
1517
|
'@keyframes drift{from{transform:translateY(-5px)}to{transform:translateY(5px)}}' +
|
|
1402
|
-
'@media (prefers-reduced-motion:reduce){.sat{animation:none}}
|
|
1518
|
+
(motion === 'full' ? '' : '@media (prefers-reduced-motion:reduce){.sat{animation:none}}') +
|
|
1519
|
+
'</style>'
|
|
1403
1520
|
: '';
|
|
1404
1521
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${WIDTH} ${HEIGHT}" width="${WIDTH}" height="${HEIGHT}" ` +
|
|
1405
1522
|
`font-family="${xml(palette.font)}">${style}${parts.join('')}</svg>`;
|
|
@@ -1565,6 +1682,32 @@ class SpineAccountSecurityService {
|
|
|
1565
1682
|
endOtherSessions() {
|
|
1566
1683
|
return this.send('DELETE', '/sessions/others');
|
|
1567
1684
|
}
|
|
1685
|
+
/** Entradas e tentativas desta conta nos últimos 90 dias, da mais recente. `result` filtra só sucessos ou falhas. */
|
|
1686
|
+
activity(params = {}) {
|
|
1687
|
+
const query = new URLSearchParams();
|
|
1688
|
+
if (params.limit)
|
|
1689
|
+
query.set('limit', String(params.limit));
|
|
1690
|
+
if (params.offset)
|
|
1691
|
+
query.set('offset', String(params.offset));
|
|
1692
|
+
if (params.result)
|
|
1693
|
+
query.set('result', params.result);
|
|
1694
|
+
const qs = query.toString();
|
|
1695
|
+
return this.send('GET', `/activity${qs ? `?${qs}` : ''}`);
|
|
1696
|
+
}
|
|
1697
|
+
badges() {
|
|
1698
|
+
return this.send('GET', '/badges');
|
|
1699
|
+
}
|
|
1700
|
+
/** Crachá com que o login entra nesta aplicação (`null` volta ao padrão: o de menor nível). */
|
|
1701
|
+
setDefaultBadge(badgeId) {
|
|
1702
|
+
return this.send('PUT', '/badges/default', { badge_id: badgeId });
|
|
1703
|
+
}
|
|
1704
|
+
preferences() {
|
|
1705
|
+
return this.send('GET', '/preferences');
|
|
1706
|
+
}
|
|
1707
|
+
/** Substitui o documento de preferências desta aplicação na conta (até 16 KB). */
|
|
1708
|
+
savePreferences(preferences) {
|
|
1709
|
+
return this.send('PUT', '/preferences', { preferences });
|
|
1710
|
+
}
|
|
1568
1711
|
async send(method, path, body) {
|
|
1569
1712
|
try {
|
|
1570
1713
|
return await this.request(method, path, body);
|
|
@@ -1597,6 +1740,778 @@ function accountErrorCode(err) {
|
|
|
1597
1740
|
return typeof code === 'string' ? code : err.status === 0 ? 'network' : '';
|
|
1598
1741
|
}
|
|
1599
1742
|
|
|
1743
|
+
/** Versão do documento guardado na conta (muda só se o formato deixar de ser compatível). */
|
|
1744
|
+
const DOC_VERSION = 1;
|
|
1745
|
+
/** Espera depois da última mudança antes de gravar. */
|
|
1746
|
+
const SPINE_PREFERENCES_SAVE_DELAY_MS = 1500;
|
|
1747
|
+
/** Favoritos guardados na conta (o servidor aceita até 16 KB no documento inteiro). */
|
|
1748
|
+
const MAX_FAVORITES = 40;
|
|
1749
|
+
/** Quanto o login espera a conta responder antes de seguir com o que está no navegador. */
|
|
1750
|
+
const READY_TIMEOUT_MS = 2000;
|
|
1751
|
+
/**
|
|
1752
|
+
* Preferências na conta (`SpineFeatures.accountPreferences`): espelha no Argos (`GET/PUT {api}/preferences`, por
|
|
1753
|
+
* usuário e aplicação) o que o painel de configurações guarda no navegador — tema, cor, densidade, fonte, menu
|
|
1754
|
+
* lateral, idioma, fuso, relógio, primeiro dia da semana, tamanho do texto, animações, atalhos de tecla única,
|
|
1755
|
+
* inatividade, tela inicial, tipos de aviso e favoritos. As telas Aparência, Região etc. não mudam: o
|
|
1756
|
+
* armazenamento local continua sendo a fonte de cada tela, e este serviço só sincroniza.
|
|
1757
|
+
*
|
|
1758
|
+
* - No início da sessão (`start()`, chamado pelo layout e pelo `/callback`), lê a conta e aplica o que estiver lá
|
|
1759
|
+
* (pelos mesmos `set*` das telas); favoritos são somados (os da conta primeiro). Conta vazia recebe o que está no
|
|
1760
|
+
* navegador.
|
|
1761
|
+
* - Depois, cada mudança é gravada na conta, agrupada (`SPINE_PREFERENCES_SAVE_DELAY_MS`).
|
|
1762
|
+
* - Sem a API da conta, com o recurso desligado ou com um Argos que não tem o endpoint (404/400), fica só no
|
|
1763
|
+
* navegador (`status() === 'local'`), sem erro na tela.
|
|
1764
|
+
*/
|
|
1765
|
+
class SpineAccountPreferencesService {
|
|
1766
|
+
constructor() {
|
|
1767
|
+
this.browser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
1768
|
+
this.api = inject(SpineAccountSecurityService);
|
|
1769
|
+
this.preferences = inject(SpinePreferencesService);
|
|
1770
|
+
this.quickAccess = inject(SpineQuickAccessService);
|
|
1771
|
+
this.hermesTheme = inject(HermesThemeService);
|
|
1772
|
+
this.themeConfig = inject(HERMES_THEME_CONFIG);
|
|
1773
|
+
this.theme = inject(ThemeService);
|
|
1774
|
+
this.density = inject(DensityService);
|
|
1775
|
+
this.presets = inject(ThemePresetService);
|
|
1776
|
+
this.sidebar = inject(HermesSidebarService);
|
|
1777
|
+
/** Recurso ligado e API da conta disponível. */
|
|
1778
|
+
this.enabled = !!inject(SPINE_CONFIG, { optional: true })?.features?.accountPreferences && this.api.available;
|
|
1779
|
+
this.status = signal(this.enabled ? 'idle' : 'local', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
1780
|
+
/** Hora da última leitura/gravação bem-sucedida. */
|
|
1781
|
+
this.syncedAt = signal(null, ...(ngDevMode ? [{ debugName: "syncedAt" }] : /* istanbul ignore next */ []));
|
|
1782
|
+
/** O documento que o navegador tem agora. */
|
|
1783
|
+
this.snapshot = computed(() => {
|
|
1784
|
+
const p = this.preferences;
|
|
1785
|
+
return {
|
|
1786
|
+
v: DOC_VERSION,
|
|
1787
|
+
themeMode: this.hermesTheme.mode(),
|
|
1788
|
+
family: this.hermesTheme.family(),
|
|
1789
|
+
density: this.hermesTheme.density(),
|
|
1790
|
+
font: this.hermesTheme.font(),
|
|
1791
|
+
sidebar: this.sidebar.mode(),
|
|
1792
|
+
language: p.language(),
|
|
1793
|
+
timeZone: p.timeZone(),
|
|
1794
|
+
textScale: p.textScale(),
|
|
1795
|
+
motion: p.motion(),
|
|
1796
|
+
hourCycle: p.hourCycle(),
|
|
1797
|
+
weekStart: p.weekStart(),
|
|
1798
|
+
characterKeys: p.characterKeys(),
|
|
1799
|
+
idleMinutes: p.idleMinutes(),
|
|
1800
|
+
startPage: p.startPage(),
|
|
1801
|
+
notices: { ...p.noticeTypes() },
|
|
1802
|
+
browserNotices: p.browserNotices(),
|
|
1803
|
+
favorites: this.quickAccess.favorites().slice(0, MAX_FAVORITES).map((f) => ({ ...f })),
|
|
1804
|
+
};
|
|
1805
|
+
}, ...(ngDevMode ? [{ debugName: "snapshot" }] : /* istanbul ignore next */ []));
|
|
1806
|
+
this.loaded = false;
|
|
1807
|
+
this.lastSaved = '';
|
|
1808
|
+
this.timer = null;
|
|
1809
|
+
this.readyPromise = null;
|
|
1810
|
+
if (!this.enabled || !this.browser)
|
|
1811
|
+
return;
|
|
1812
|
+
effect(() => {
|
|
1813
|
+
const json = stable(this.snapshot());
|
|
1814
|
+
if (!this.loaded || json === this.lastSaved)
|
|
1815
|
+
return;
|
|
1816
|
+
untracked(() => this.schedule());
|
|
1817
|
+
});
|
|
1818
|
+
inject(DestroyRef).onDestroy(() => this.clearTimer());
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Lê a conta e aplica (uma vez por sessão do app; chamadas seguintes devolvem a mesma promessa). Nunca rejeita.
|
|
1822
|
+
*/
|
|
1823
|
+
start() {
|
|
1824
|
+
if (!this.enabled || !this.browser)
|
|
1825
|
+
return Promise.resolve();
|
|
1826
|
+
this.readyPromise ??= this.load();
|
|
1827
|
+
return this.readyPromise;
|
|
1828
|
+
}
|
|
1829
|
+
/** Como `start()`, mas sem esperar mais que `timeoutMs` (o login não fica parado se o Argos demorar). */
|
|
1830
|
+
ready(timeoutMs = READY_TIMEOUT_MS) {
|
|
1831
|
+
return Promise.race([this.start(), new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
|
|
1832
|
+
}
|
|
1833
|
+
/** Grava agora o que estiver pendente ("Sincronizar agora"); relê a conta se ela ainda não respondeu. */
|
|
1834
|
+
async syncNow() {
|
|
1835
|
+
if (!this.enabled)
|
|
1836
|
+
return;
|
|
1837
|
+
if (!this.loaded) {
|
|
1838
|
+
this.readyPromise = this.load();
|
|
1839
|
+
await this.readyPromise;
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
this.clearTimer();
|
|
1843
|
+
await this.save();
|
|
1844
|
+
}
|
|
1845
|
+
async load() {
|
|
1846
|
+
this.status.set('loading');
|
|
1847
|
+
try {
|
|
1848
|
+
const { preferences } = await this.api.preferences();
|
|
1849
|
+
const remote = isDoc(preferences) ? preferences : null;
|
|
1850
|
+
if (remote)
|
|
1851
|
+
this.apply(remote);
|
|
1852
|
+
this.loaded = true;
|
|
1853
|
+
this.syncedAt.set(new Date());
|
|
1854
|
+
if (remote) {
|
|
1855
|
+
this.lastSaved = stable(remote);
|
|
1856
|
+
// A conta pode ter menos do que o navegador (favoritos somados, chave nova): grava a diferença.
|
|
1857
|
+
if (stable(this.snapshot()) !== this.lastSaved)
|
|
1858
|
+
this.schedule();
|
|
1859
|
+
else
|
|
1860
|
+
this.status.set('synced');
|
|
1861
|
+
}
|
|
1862
|
+
else {
|
|
1863
|
+
await this.save();
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
catch (err) {
|
|
1867
|
+
this.failed(err);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
/** Aplica o documento da conta pelos mesmos caminhos das telas (cada `set*` também grava no navegador). */
|
|
1871
|
+
apply(doc) {
|
|
1872
|
+
const p = this.preferences;
|
|
1873
|
+
if (doc.themeMode && doc.themeMode !== this.hermesTheme.mode())
|
|
1874
|
+
this.theme.setMode(doc.themeMode);
|
|
1875
|
+
if (doc.family && doc.family !== this.hermesTheme.family()) {
|
|
1876
|
+
const family = this.presets.families.find((f) => f.id === (this.themeConfig.aliases?.[doc.family] ?? doc.family));
|
|
1877
|
+
if (family)
|
|
1878
|
+
this.presets.setPreset(family);
|
|
1879
|
+
}
|
|
1880
|
+
if (doc.density && doc.density !== this.hermesTheme.density())
|
|
1881
|
+
this.density.setDensity(doc.density);
|
|
1882
|
+
if (doc.font && doc.font !== this.hermesTheme.font()) {
|
|
1883
|
+
const fonts = this.hermesTheme.fonts ?? [];
|
|
1884
|
+
if (!fonts.length || fonts.some((f) => f.id === doc.font))
|
|
1885
|
+
this.hermesTheme.setFont(doc.font);
|
|
1886
|
+
}
|
|
1887
|
+
if (doc.sidebar && doc.sidebar !== this.sidebar.mode())
|
|
1888
|
+
this.sidebar.setMode(doc.sidebar);
|
|
1889
|
+
if (typeof doc.language === 'string' && doc.language !== p.language())
|
|
1890
|
+
p.setLanguage(doc.language);
|
|
1891
|
+
if (typeof doc.timeZone === 'string' && doc.timeZone !== p.timeZone())
|
|
1892
|
+
p.setTimeZone(doc.timeZone);
|
|
1893
|
+
if (typeof doc.textScale === 'number' && doc.textScale !== p.textScale())
|
|
1894
|
+
p.setTextScale(doc.textScale);
|
|
1895
|
+
if (doc.motion && doc.motion !== p.motion())
|
|
1896
|
+
p.setMotion(doc.motion);
|
|
1897
|
+
if (doc.hourCycle !== undefined && doc.hourCycle !== p.hourCycle())
|
|
1898
|
+
p.setHourCycle(doc.hourCycle);
|
|
1899
|
+
if (doc.weekStart !== undefined && doc.weekStart !== p.weekStart())
|
|
1900
|
+
p.setWeekStart(doc.weekStart);
|
|
1901
|
+
if (typeof doc.characterKeys === 'boolean' && doc.characterKeys !== p.characterKeys())
|
|
1902
|
+
p.setCharacterKeys(doc.characterKeys);
|
|
1903
|
+
if (typeof doc.idleMinutes === 'number' && doc.idleMinutes !== p.idleMinutes())
|
|
1904
|
+
p.setIdleMinutes(doc.idleMinutes);
|
|
1905
|
+
if (typeof doc.startPage === 'string' && doc.startPage !== p.startPage())
|
|
1906
|
+
p.setStartPage(doc.startPage);
|
|
1907
|
+
if (doc.notices && typeof doc.notices === 'object')
|
|
1908
|
+
p.setNoticeTypes({ ...p.noticeTypes(), ...doc.notices });
|
|
1909
|
+
// O aviso do navegador depende da permissão DESTE navegador: a conta só desliga, nunca liga sozinha.
|
|
1910
|
+
if (doc.browserNotices === false && p.browserNotices())
|
|
1911
|
+
p.setBrowserNotices(false);
|
|
1912
|
+
if (Array.isArray(doc.favorites)) {
|
|
1913
|
+
const remote = doc.favorites;
|
|
1914
|
+
const ids = new Set(remote.map((f) => f?.id));
|
|
1915
|
+
const localOnly = this.quickAccess.favorites().filter((f) => !ids.has(f.id));
|
|
1916
|
+
this.quickAccess.setFavorites([...remote, ...localOnly]);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
schedule() {
|
|
1920
|
+
this.clearTimer();
|
|
1921
|
+
this.status.set('saving');
|
|
1922
|
+
this.timer = setTimeout(() => void this.save(), SPINE_PREFERENCES_SAVE_DELAY_MS);
|
|
1923
|
+
}
|
|
1924
|
+
async save() {
|
|
1925
|
+
this.timer = null;
|
|
1926
|
+
const doc = this.snapshot();
|
|
1927
|
+
const json = stable(doc);
|
|
1928
|
+
this.status.set('saving');
|
|
1929
|
+
try {
|
|
1930
|
+
await this.api.savePreferences(doc);
|
|
1931
|
+
this.lastSaved = json;
|
|
1932
|
+
this.syncedAt.set(new Date());
|
|
1933
|
+
this.status.set('synced');
|
|
1934
|
+
}
|
|
1935
|
+
catch (err) {
|
|
1936
|
+
this.failed(err);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
failed(err) {
|
|
1940
|
+
// Argos sem o endpoint (versão anterior) ou sem client no token: fica só no navegador, sem insistir.
|
|
1941
|
+
if (err instanceof HttpErrorResponse && (err.status === 404 || err.status === 400 || err.status === 405)) {
|
|
1942
|
+
this.status.set('local');
|
|
1943
|
+
this.loaded = false;
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
this.status.set('error');
|
|
1947
|
+
}
|
|
1948
|
+
clearTimer() {
|
|
1949
|
+
if (this.timer)
|
|
1950
|
+
clearTimeout(this.timer);
|
|
1951
|
+
this.timer = null;
|
|
1952
|
+
}
|
|
1953
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountPreferencesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1954
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountPreferencesService, providedIn: 'root' }); }
|
|
1955
|
+
}
|
|
1956
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAccountPreferencesService, decorators: [{
|
|
1957
|
+
type: Injectable,
|
|
1958
|
+
args: [{ providedIn: 'root' }]
|
|
1959
|
+
}], ctorParameters: () => [] });
|
|
1960
|
+
/** JSON com as chaves dos objetos em ordem (a conta pode devolver outra ordem). */
|
|
1961
|
+
function stable(value) {
|
|
1962
|
+
return JSON.stringify(value, (_key, v) => v && typeof v === 'object' && !Array.isArray(v)
|
|
1963
|
+
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b)))
|
|
1964
|
+
: v);
|
|
1965
|
+
}
|
|
1966
|
+
function isDoc(value) {
|
|
1967
|
+
return !!value && typeof value === 'object' && !Array.isArray(value) && typeof value.v === 'number';
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
const positive = (value) => {
|
|
1971
|
+
const n = Number(value);
|
|
1972
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n) : 0;
|
|
1973
|
+
};
|
|
1974
|
+
/** Opções e limites de inatividade do app. Sem o recurso, `enabled` é falso e não há opções. */
|
|
1975
|
+
function idleLimits(features) {
|
|
1976
|
+
const config = features?.idleTimeout;
|
|
1977
|
+
if (!config)
|
|
1978
|
+
return { enabled: false, min: 0, max: 0, options: [] };
|
|
1979
|
+
const min = typeof config === 'object' ? positive(config.min) : 0;
|
|
1980
|
+
let max = typeof config === 'object' ? positive(config.max) : 0;
|
|
1981
|
+
if (max && min && max < min)
|
|
1982
|
+
max = min;
|
|
1983
|
+
const options = SPINE_IDLE_OPTIONS.filter((m) => (m === 0 ? !max : m >= min && (!max || m <= max)));
|
|
1984
|
+
// Um limite fora das opções padrão (ex.: máximo de 10 min) entra como opção própria.
|
|
1985
|
+
const extra = [min, max].filter((m) => m > 0 && !options.includes(m));
|
|
1986
|
+
return { enabled: true, min, max, options: [...options, ...extra].sort((a, b) => a - b) };
|
|
1987
|
+
}
|
|
1988
|
+
/** Minutos que valem de fato: a escolha da pessoa dentro dos limites do app (0 = não encerra). */
|
|
1989
|
+
function effectiveIdleMinutes(chosen, limits) {
|
|
1990
|
+
if (!limits.enabled)
|
|
1991
|
+
return 0;
|
|
1992
|
+
let minutes = chosen > 0 ? chosen : 0;
|
|
1993
|
+
if (limits.max && (minutes === 0 || minutes > limits.max))
|
|
1994
|
+
minutes = limits.max;
|
|
1995
|
+
if (limits.min && minutes > 0 && minutes < limits.min)
|
|
1996
|
+
minutes = limits.min;
|
|
1997
|
+
return minutes;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/** Segundos de aviso antes de encerrar. */
|
|
2001
|
+
const SPINE_IDLE_WARNING_SECONDS = 60;
|
|
2002
|
+
/** Intervalo entre gravações da última atividade no `localStorage` (as outras abas leem de lá). */
|
|
2003
|
+
const WRITE_EVERY_MS = 5_000;
|
|
2004
|
+
const ACTIVITY_EVENTS = ['pointerdown', 'pointermove', 'keydown', 'wheel', 'touchstart', 'scroll'];
|
|
2005
|
+
/**
|
|
2006
|
+
* Encerrar por inatividade (`SpineFeatures.idleTimeout`): conta o tempo sem uso (ponteiro, teclado, rolagem) em
|
|
2007
|
+
* TODAS as abas do app — a última atividade vai para o `localStorage` (`hermes_<clientId>_last_activity`) e cada aba
|
|
2008
|
+
* confere a mais recente —, abre o aviso com contagem regressiva `SPINE_IDLE_WARNING_SECONDS` antes do fim e, se
|
|
2009
|
+
* ninguém responder, sai pelo `SpineLogoutService` (o mesmo "Sair" do menu). Com o aviso aberto, só "Continuar
|
|
2010
|
+
* conectado" (ou atividade em outra aba) mantém a sessão: mexer o ponteiro sobre o aviso não conta.
|
|
2011
|
+
*
|
|
2012
|
+
* Os minutos vêm do `SpinePreferencesService.idleMinutes`, limitados pelo app (`effectiveIdleMinutes`). Ligado pelo
|
|
2013
|
+
* `LayoutComponent` (só existe com sessão); não faz nada fora do navegador.
|
|
2014
|
+
*/
|
|
2015
|
+
class SpineIdleService {
|
|
2016
|
+
constructor() {
|
|
2017
|
+
this.browser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
2018
|
+
this.zone = inject(NgZone);
|
|
2019
|
+
this.preferences = inject(SpinePreferencesService);
|
|
2020
|
+
this.logoutService = inject(SpineLogoutService);
|
|
2021
|
+
this.config = inject(SPINE_CONFIG, { optional: true });
|
|
2022
|
+
this.storageKey = `hermes_${this.config?.clientId || 'app'}_last_activity`;
|
|
2023
|
+
this.limits = idleLimits(this.config?.features);
|
|
2024
|
+
/** Minutos que valem agora (0 = não encerra). */
|
|
2025
|
+
this.minutes = computed(() => effectiveIdleMinutes(this.preferences.idleMinutes(), this.limits), ...(ngDevMode ? [{ debugName: "minutes" }] : /* istanbul ignore next */ []));
|
|
2026
|
+
/** Aviso aberto. */
|
|
2027
|
+
this.warning = signal(false, ...(ngDevMode ? [{ debugName: "warning" }] : /* istanbul ignore next */ []));
|
|
2028
|
+
/** Segundos que faltam, enquanto o aviso está aberto. */
|
|
2029
|
+
this.secondsLeft = signal(SPINE_IDLE_WARNING_SECONDS, ...(ngDevMode ? [{ debugName: "secondsLeft" }] : /* istanbul ignore next */ []));
|
|
2030
|
+
this.lastActivity = Date.now();
|
|
2031
|
+
this.lastWrite = 0;
|
|
2032
|
+
this.timer = null;
|
|
2033
|
+
this.expiring = false;
|
|
2034
|
+
/** Ligado por `start()` (o layout, que só existe com sessão). */
|
|
2035
|
+
this.active = signal(false, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
2036
|
+
this.onActivity = () => {
|
|
2037
|
+
if (this.warning())
|
|
2038
|
+
return;
|
|
2039
|
+
this.touch();
|
|
2040
|
+
};
|
|
2041
|
+
if (!this.browser)
|
|
2042
|
+
return;
|
|
2043
|
+
effect(() => {
|
|
2044
|
+
if (this.active() && this.minutes() > 0)
|
|
2045
|
+
this.run();
|
|
2046
|
+
else
|
|
2047
|
+
this.stop();
|
|
2048
|
+
});
|
|
2049
|
+
inject(DestroyRef).onDestroy(() => this.stop());
|
|
2050
|
+
}
|
|
2051
|
+
/** Liga a contagem (idempotente). Para sozinha quando a escolha vira "Nunca". */
|
|
2052
|
+
start() {
|
|
2053
|
+
this.active.set(true);
|
|
2054
|
+
}
|
|
2055
|
+
/** "Continuar conectado": fecha o aviso e recomeça a contagem. */
|
|
2056
|
+
stay() {
|
|
2057
|
+
this.warning.set(false);
|
|
2058
|
+
this.touch(true);
|
|
2059
|
+
}
|
|
2060
|
+
/** "Sair agora". */
|
|
2061
|
+
logoutNow() {
|
|
2062
|
+
void this.expire();
|
|
2063
|
+
}
|
|
2064
|
+
/** Conta uma atividade agora (e a grava para as outras abas no máximo a cada 5 s, ou já, com `force`). */
|
|
2065
|
+
touch(force = false) {
|
|
2066
|
+
const now = Date.now();
|
|
2067
|
+
this.lastActivity = now;
|
|
2068
|
+
if (!force && now - this.lastWrite < WRITE_EVERY_MS)
|
|
2069
|
+
return;
|
|
2070
|
+
this.lastWrite = now;
|
|
2071
|
+
try {
|
|
2072
|
+
localStorage.setItem(this.storageKey, String(now));
|
|
2073
|
+
}
|
|
2074
|
+
catch { /* storage indisponível */ }
|
|
2075
|
+
}
|
|
2076
|
+
/** Confere o tempo sem uso: abre o aviso, atualiza a contagem ou encerra. Exposto para os testes. */
|
|
2077
|
+
check(now = Date.now()) {
|
|
2078
|
+
const minutes = this.minutes();
|
|
2079
|
+
if (!minutes || this.expiring)
|
|
2080
|
+
return;
|
|
2081
|
+
const idle = now - Math.max(this.lastActivity, this.storedActivity());
|
|
2082
|
+
const total = minutes * 60_000;
|
|
2083
|
+
const warnAt = Math.max(0, total - SPINE_IDLE_WARNING_SECONDS * 1000);
|
|
2084
|
+
if (idle >= total) {
|
|
2085
|
+
void this.expire();
|
|
2086
|
+
}
|
|
2087
|
+
else if (idle >= warnAt) {
|
|
2088
|
+
this.zone.run(() => {
|
|
2089
|
+
this.secondsLeft.set(Math.max(1, Math.ceil((total - idle) / 1000)));
|
|
2090
|
+
this.warning.set(true);
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
else if (this.warning()) {
|
|
2094
|
+
// Atividade em outra aba: o aviso some sozinho.
|
|
2095
|
+
this.zone.run(() => this.warning.set(false));
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
storedActivity() {
|
|
2099
|
+
try {
|
|
2100
|
+
const value = Number(localStorage.getItem(this.storageKey));
|
|
2101
|
+
return Number.isFinite(value) ? value : 0;
|
|
2102
|
+
}
|
|
2103
|
+
catch {
|
|
2104
|
+
return 0;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
run() {
|
|
2108
|
+
if (this.timer)
|
|
2109
|
+
return;
|
|
2110
|
+
this.touch(true);
|
|
2111
|
+
this.zone.runOutsideAngular(() => {
|
|
2112
|
+
for (const name of ACTIVITY_EVENTS)
|
|
2113
|
+
document.addEventListener(name, this.onActivity, { passive: true, capture: true });
|
|
2114
|
+
this.timer = setInterval(() => this.check(), 1000);
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
stop() {
|
|
2118
|
+
if (this.timer)
|
|
2119
|
+
clearInterval(this.timer);
|
|
2120
|
+
this.timer = null;
|
|
2121
|
+
if (this.browser)
|
|
2122
|
+
for (const name of ACTIVITY_EVENTS)
|
|
2123
|
+
document.removeEventListener(name, this.onActivity, { capture: true });
|
|
2124
|
+
this.warning.set(false);
|
|
2125
|
+
}
|
|
2126
|
+
async expire() {
|
|
2127
|
+
if (this.expiring)
|
|
2128
|
+
return;
|
|
2129
|
+
this.expiring = true;
|
|
2130
|
+
this.zone.run(() => this.warning.set(false));
|
|
2131
|
+
try {
|
|
2132
|
+
const ok = await this.logoutService.logout();
|
|
2133
|
+
// Saída recusada pelo Argos: a sessão continua (o aviso de falha já apareceu); recomeça a contagem.
|
|
2134
|
+
if (!ok)
|
|
2135
|
+
this.touch(true);
|
|
2136
|
+
}
|
|
2137
|
+
finally {
|
|
2138
|
+
this.expiring = false;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineIdleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2142
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineIdleService, providedIn: 'root' }); }
|
|
2143
|
+
}
|
|
2144
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineIdleService, decorators: [{
|
|
2145
|
+
type: Injectable,
|
|
2146
|
+
args: [{ providedIn: 'root' }]
|
|
2147
|
+
}], ctorParameters: () => [] });
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* Avisos do app (`SpineFeatures.notices`): o app declara os tipos que emite (`{ id, label }`) e chama
|
|
2151
|
+
* `notify(tipo, aviso)` de onde já sabe do acontecimento (subscription, polling). Cada tipo pode ser desligado em
|
|
2152
|
+
* Configurações › Notificações; tipo desligado não aparece. O aviso ligado:
|
|
2153
|
+
*
|
|
2154
|
+
* - aparece na tela (`HermesToastService`, com "Abrir" quando tem `url`);
|
|
2155
|
+
* - com a aba em segundo plano, o aviso do navegador ligado e a permissão concedida, vira também um aviso do sistema
|
|
2156
|
+
* (Notification API); clicar nele traz a aba para a frente e abre a `url`.
|
|
2157
|
+
*
|
|
2158
|
+
* Tipo não declarado em `SpineFeatures.notices.types` aparece sempre (em desenvolvimento, com um aviso no console).
|
|
2159
|
+
*/
|
|
2160
|
+
class SpineNoticesService {
|
|
2161
|
+
constructor() {
|
|
2162
|
+
this.browser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
2163
|
+
this.zone = inject(NgZone);
|
|
2164
|
+
this.injector = inject(Injector);
|
|
2165
|
+
this.toast = inject(HermesToastService);
|
|
2166
|
+
this.preferences = inject(SpinePreferencesService);
|
|
2167
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
2168
|
+
this.config = inject(SPINE_CONFIG, { optional: true })?.features?.notices;
|
|
2169
|
+
this.warned = new Set();
|
|
2170
|
+
/** Recurso ligado pelo app. */
|
|
2171
|
+
this.enabled = !!this.config;
|
|
2172
|
+
/** Tipos declarados pelo app. */
|
|
2173
|
+
this.types = this.config?.types ?? [];
|
|
2174
|
+
/** Permissão atual do navegador. */
|
|
2175
|
+
this.permission = signal(this.readPermission(), ...(ngDevMode ? [{ debugName: "permission" }] : /* istanbul ignore next */ []));
|
|
2176
|
+
/** Aviso do navegador valendo de fato: ligado nas configurações e permitido pelo navegador. */
|
|
2177
|
+
this.browserActive = computed(() => this.preferences.browserNotices() && this.permission() === 'granted', ...(ngDevMode ? [{ debugName: "browserActive" }] : /* istanbul ignore next */ []));
|
|
2178
|
+
}
|
|
2179
|
+
/** O tipo está ligado (escolha da pessoa ou, sem escolha, o `defaultEnabled` dele). */
|
|
2180
|
+
isEnabled(typeId) {
|
|
2181
|
+
const chosen = this.preferences.noticeTypes()[typeId];
|
|
2182
|
+
if (typeof chosen === 'boolean')
|
|
2183
|
+
return chosen;
|
|
2184
|
+
return this.types.find((t) => t.id === typeId)?.defaultEnabled ?? true;
|
|
2185
|
+
}
|
|
2186
|
+
setEnabled(typeId, enabled) {
|
|
2187
|
+
this.preferences.setNoticeType(typeId, enabled);
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Liga o aviso do navegador: pede a permissão quando ainda não foi pedida. Devolve se ficou ligado (permissão
|
|
2191
|
+
* negada ou navegador sem suporte: continua desligado).
|
|
2192
|
+
*/
|
|
2193
|
+
async enableBrowser() {
|
|
2194
|
+
if (this.readPermission() === 'unsupported') {
|
|
2195
|
+
this.permission.set('unsupported');
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
let permission = Notification.permission;
|
|
2199
|
+
if (permission === 'default') {
|
|
2200
|
+
try {
|
|
2201
|
+
permission = await Notification.requestPermission();
|
|
2202
|
+
}
|
|
2203
|
+
catch {
|
|
2204
|
+
permission = 'denied';
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
this.permission.set(permission);
|
|
2208
|
+
const granted = permission === 'granted';
|
|
2209
|
+
this.preferences.setBrowserNotices(granted);
|
|
2210
|
+
return granted;
|
|
2211
|
+
}
|
|
2212
|
+
disableBrowser() {
|
|
2213
|
+
this.preferences.setBrowserNotices(false);
|
|
2214
|
+
}
|
|
2215
|
+
/** Relê a permissão (a pessoa pode ter mudado nas configurações do navegador). */
|
|
2216
|
+
refreshPermission() {
|
|
2217
|
+
this.permission.set(this.readPermission());
|
|
2218
|
+
}
|
|
2219
|
+
/** Mostra o aviso, se o tipo estiver ligado. Devolve se mostrou. */
|
|
2220
|
+
notify(typeId, notice) {
|
|
2221
|
+
if (this.types.length && !this.types.some((t) => t.id === typeId) && isDevMode() && !this.warned.has(typeId)) {
|
|
2222
|
+
this.warned.add(typeId);
|
|
2223
|
+
console.warn(`[hermes-spine] aviso de tipo não declarado em SpineFeatures.notices.types: ${typeId}`);
|
|
2224
|
+
}
|
|
2225
|
+
if (!this.isEnabled(typeId))
|
|
2226
|
+
return false;
|
|
2227
|
+
const url = notice.url && notice.url.startsWith('/') && !notice.url.startsWith('//') ? notice.url : undefined;
|
|
2228
|
+
const message = notice.body ? `${notice.title} — ${notice.body}` : notice.title;
|
|
2229
|
+
this.toast.push(notice.intent ?? 'info', message, url
|
|
2230
|
+
? { action: { label: this.messages.accountFeatures.noticeOpen, run: () => this.open(url) } }
|
|
2231
|
+
: undefined);
|
|
2232
|
+
if (this.browser && document.visibilityState === 'hidden' && this.preferences.browserNotices())
|
|
2233
|
+
this.system(typeId, notice, url);
|
|
2234
|
+
return true;
|
|
2235
|
+
}
|
|
2236
|
+
system(typeId, notice, url) {
|
|
2237
|
+
this.refreshPermission();
|
|
2238
|
+
if (this.permission() !== 'granted')
|
|
2239
|
+
return;
|
|
2240
|
+
try {
|
|
2241
|
+
const shown = new Notification(notice.title, { body: notice.body, tag: url ? `${typeId}:${url}` : typeId });
|
|
2242
|
+
shown.onclick = () => {
|
|
2243
|
+
window.focus();
|
|
2244
|
+
shown.close();
|
|
2245
|
+
if (url)
|
|
2246
|
+
this.open(url);
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
catch {
|
|
2250
|
+
// Alguns navegadores (Chrome no Android) só aceitam aviso por service worker: fica só o da tela.
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
open(url) {
|
|
2254
|
+
this.zone.run(() => void this.injector.get(Router).navigateByUrl(url));
|
|
2255
|
+
}
|
|
2256
|
+
readPermission() {
|
|
2257
|
+
if (!this.browser || typeof Notification === 'undefined')
|
|
2258
|
+
return 'unsupported';
|
|
2259
|
+
return Notification.permission;
|
|
2260
|
+
}
|
|
2261
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineNoticesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2262
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineNoticesService, providedIn: 'root' }); }
|
|
2263
|
+
}
|
|
2264
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineNoticesService, decorators: [{
|
|
2265
|
+
type: Injectable,
|
|
2266
|
+
args: [{ providedIn: 'root' }]
|
|
2267
|
+
}] });
|
|
2268
|
+
|
|
2269
|
+
/** Rotas do shell que nunca viram "última tela". */
|
|
2270
|
+
const IGNORED = /^\/(callback|logoff|loggedout|login|no-permission|blocked)(\/|$)/;
|
|
2271
|
+
/** Caminho interno seguro (`/x`, nunca `//host` nem `http:`). */
|
|
2272
|
+
function isInternalPath(url) {
|
|
2273
|
+
return typeof url === 'string' && url.startsWith('/') && !url.startsWith('//') && !url.includes('\\');
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Tela inicial (`SpineFeatures.startPage`): para onde o app vai depois do login quando a pessoa não pediu uma URL
|
|
2277
|
+
* (`/callback` sem URL de retorno, ou com `/` ou `/home`) — o início do app, um favorito ou a última tela visitada.
|
|
2278
|
+
*
|
|
2279
|
+
* A última tela fica no `SpineUserStorage` (`lastPage`, por usuário) e só é gravada enquanto a escolha for
|
|
2280
|
+
* "Última tela visitada" (`track()`, ligado pelo `LayoutComponent`); ao contrário dos recentes, ela fica no logout.
|
|
2281
|
+
*/
|
|
2282
|
+
class SpineStartPageService {
|
|
2283
|
+
constructor() {
|
|
2284
|
+
this.browser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
2285
|
+
this.injector = inject(Injector);
|
|
2286
|
+
this.destroyRef = inject(DestroyRef);
|
|
2287
|
+
this.preferences = inject(SpinePreferencesService);
|
|
2288
|
+
this.storage = inject(SpineUserStorage);
|
|
2289
|
+
this.enabled = !!inject(SPINE_CONFIG, { optional: true })?.features?.startPage;
|
|
2290
|
+
this.tracking = false;
|
|
2291
|
+
}
|
|
2292
|
+
/** Grava a URL de cada navegação enquanto a escolha for "Última tela visitada" (idempotente). */
|
|
2293
|
+
track() {
|
|
2294
|
+
if (!this.enabled || !this.browser || this.tracking)
|
|
2295
|
+
return;
|
|
2296
|
+
this.tracking = true;
|
|
2297
|
+
const router = this.injector.get(Router);
|
|
2298
|
+
const sub = router.events
|
|
2299
|
+
.pipe(filter((e) => e instanceof NavigationEnd))
|
|
2300
|
+
.subscribe((e) => this.remember(e.urlAfterRedirects));
|
|
2301
|
+
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
2302
|
+
}
|
|
2303
|
+
/** Guarda `url` como última tela (só com a escolha "Última tela visitada"). */
|
|
2304
|
+
remember(url) {
|
|
2305
|
+
if (!this.enabled || this.preferences.startPage() !== 'last' || !isInternalPath(url) || IGNORED.test(url))
|
|
2306
|
+
return;
|
|
2307
|
+
try {
|
|
2308
|
+
localStorage.setItem(this.storage.key(SPINE_USER_STORAGE_KEYS.lastPage), url);
|
|
2309
|
+
}
|
|
2310
|
+
catch { /* storage indisponível */ }
|
|
2311
|
+
}
|
|
2312
|
+
/** Destino da tela inicial escolhida, ou `null` (início do app, recurso desligado ou favorito que sumiu). */
|
|
2313
|
+
target() {
|
|
2314
|
+
if (!this.enabled || !this.browser)
|
|
2315
|
+
return null;
|
|
2316
|
+
const choice = this.preferences.startPage();
|
|
2317
|
+
if (choice === 'last') {
|
|
2318
|
+
let url = null;
|
|
2319
|
+
try {
|
|
2320
|
+
url = localStorage.getItem(this.storage.key(SPINE_USER_STORAGE_KEYS.lastPage));
|
|
2321
|
+
}
|
|
2322
|
+
catch {
|
|
2323
|
+
url = null;
|
|
2324
|
+
}
|
|
2325
|
+
return isInternalPath(url) && !IGNORED.test(url) ? url : null;
|
|
2326
|
+
}
|
|
2327
|
+
if (choice.startsWith('fav:')) {
|
|
2328
|
+
const id = choice.slice(4);
|
|
2329
|
+
const favorite = this.injector.get(SpineQuickAccessService).favorites().find((f) => f.id === id);
|
|
2330
|
+
return favorite && isInternalPath(favorite.redirect) ? favorite.redirect : null;
|
|
2331
|
+
}
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
/** `true` quando o login não pediu uma URL própria (aí a tela inicial manda). */
|
|
2335
|
+
static isDefaultReturn(returnUrl) {
|
|
2336
|
+
return !returnUrl || returnUrl === '/' || returnUrl === '/home';
|
|
2337
|
+
}
|
|
2338
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineStartPageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2339
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineStartPageService, providedIn: 'root' }); }
|
|
2340
|
+
}
|
|
2341
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineStartPageService, decorators: [{
|
|
2342
|
+
type: Injectable,
|
|
2343
|
+
args: [{ providedIn: 'root' }]
|
|
2344
|
+
}] });
|
|
2345
|
+
|
|
2346
|
+
/** Itens por página. */
|
|
2347
|
+
const PAGE = 10;
|
|
2348
|
+
/**
|
|
2349
|
+
* Atividade da conta (MFA › "Atividade da conta", `SpineFeatures.accountActivity`): as entradas e as tentativas que
|
|
2350
|
+
* falharam desta conta nos últimos 90 dias (`GET {api}/activity` do Argos) — quando, de onde (IP e navegador), em qual
|
|
2351
|
+
* aplicativo, por qual método e, na falha, o motivo. "Só falhas" filtra; "Carregar mais" traz a página seguinte.
|
|
2352
|
+
*/
|
|
2353
|
+
class AccountActivityComponent {
|
|
2354
|
+
constructor() {
|
|
2355
|
+
this.api = inject(SpineAccountSecurityService);
|
|
2356
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
2357
|
+
this.t = this.messages.accountFeatures;
|
|
2358
|
+
this.attempts = this.messages.loginAttempts;
|
|
2359
|
+
this.preferences = inject(SpinePreferencesService);
|
|
2360
|
+
this.filter = signal('', ...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
|
|
2361
|
+
this.entries = signal([], ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
|
|
2362
|
+
this.total = signal(0, ...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
|
|
2363
|
+
this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2364
|
+
this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
|
|
2365
|
+
this.filterOptions = computed(() => [
|
|
2366
|
+
{ value: '', label: this.t.activityAll },
|
|
2367
|
+
{ value: 'failure', label: this.t.activityFailures },
|
|
2368
|
+
], ...(ngDevMode ? [{ debugName: "filterOptions" }] : /* istanbul ignore next */ []));
|
|
2369
|
+
this.showing = computed(() => formatMessage(this.t.activityShowing, { shown: this.entries().length, total: this.total() }), ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
|
|
2370
|
+
if (this.api.available)
|
|
2371
|
+
void this.load(0);
|
|
2372
|
+
else
|
|
2373
|
+
this.loading.set(false);
|
|
2374
|
+
}
|
|
2375
|
+
setFilter(value) {
|
|
2376
|
+
this.filter.set(value === 'failure' ? 'failure' : '');
|
|
2377
|
+
this.reload();
|
|
2378
|
+
}
|
|
2379
|
+
reload() {
|
|
2380
|
+
this.entries.set([]);
|
|
2381
|
+
void this.load(0);
|
|
2382
|
+
}
|
|
2383
|
+
more() {
|
|
2384
|
+
void this.load(this.entries().length);
|
|
2385
|
+
}
|
|
2386
|
+
async load(offset) {
|
|
2387
|
+
this.loading.set(true);
|
|
2388
|
+
try {
|
|
2389
|
+
const page = await this.api.activity({ limit: PAGE, offset, result: this.filter() || undefined });
|
|
2390
|
+
const attempts = page?.attempts ?? [];
|
|
2391
|
+
this.entries.update((list) => (offset ? [...list, ...attempts] : attempts));
|
|
2392
|
+
this.total.set(page?.total ?? attempts.length);
|
|
2393
|
+
this.loadError.set('');
|
|
2394
|
+
}
|
|
2395
|
+
catch (err) {
|
|
2396
|
+
this.loadError.set(accountErrorCode(err) || 'generic');
|
|
2397
|
+
}
|
|
2398
|
+
finally {
|
|
2399
|
+
this.loading.set(false);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
errorText() {
|
|
2403
|
+
const errors = this.messages.accountSecurity.errors;
|
|
2404
|
+
const code = this.loadError();
|
|
2405
|
+
return code === 'generic' || !errors[code] ? this.t.activityLoadError : errors[code];
|
|
2406
|
+
}
|
|
2407
|
+
resultLabel(a) {
|
|
2408
|
+
return this.attempts.results[a.result] ?? a.result;
|
|
2409
|
+
}
|
|
2410
|
+
resultColor(a) {
|
|
2411
|
+
return a.result === 'success' ? 'success' : a.result === 'failure' ? 'danger' : 'neutral';
|
|
2412
|
+
}
|
|
2413
|
+
method(a) {
|
|
2414
|
+
return this.attempts.methods[a.method] ?? a.method;
|
|
2415
|
+
}
|
|
2416
|
+
reason(a) {
|
|
2417
|
+
return this.attempts.reasons[a.reason ?? ''] ?? a.reason ?? '';
|
|
2418
|
+
}
|
|
2419
|
+
app(a) {
|
|
2420
|
+
return formatMessage(this.t.activityApp, { app: a.client?.name || this.t.unknownApp });
|
|
2421
|
+
}
|
|
2422
|
+
detail(a) {
|
|
2423
|
+
const date = new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeStyle: 'short', timeZone: this.preferences.effectiveTimeZone() })
|
|
2424
|
+
.format(new Date(a.at));
|
|
2425
|
+
return formatMessage(this.t.activityDetail, { date, ip: a.ip || this.t.unknownIp });
|
|
2426
|
+
}
|
|
2427
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountActivityComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2428
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AccountActivityComponent, isStandalone: true, selector: "spine-account-activity", ngImport: i0, template: `
|
|
2429
|
+
<div class="sp-account__toolbar">
|
|
2430
|
+
<hm-segmented-control size="small" emphasis="action" [label]="t.activityFilter" [options]="filterOptions()" [value]="filter()" (valueChange)="setFilter($event)" />
|
|
2431
|
+
</div>
|
|
2432
|
+
@if (loading() && !entries().length) {
|
|
2433
|
+
<p class="sp-account__note" role="status">{{ messages.accountSecurity.loading }}</p>
|
|
2434
|
+
} @else if (loadError()) {
|
|
2435
|
+
<hm-alert variant="error">{{ errorText() }}</hm-alert>
|
|
2436
|
+
<div class="sp-account__actions">
|
|
2437
|
+
<hm-button size="small" emphasis="medium" icon="refresh" (clicked)="reload()">{{ messages.accountSecurity.retry }}</hm-button>
|
|
2438
|
+
</div>
|
|
2439
|
+
} @else {
|
|
2440
|
+
<div class="sp-account__list">
|
|
2441
|
+
<ul class="sp-account__items">
|
|
2442
|
+
@for (a of entries(); track a.id) {
|
|
2443
|
+
<li class="sp-account__item">
|
|
2444
|
+
<span class="sp-account__icon" [class.sp-account__icon--accent]="a.result === 'success'"><hm-icon [shape]="a.device.mobile ? 'mobile' : 'computer'" /></span>
|
|
2445
|
+
<span class="sp-account__item-main">
|
|
2446
|
+
<span class="sp-account__item-title">
|
|
2447
|
+
<strong>{{ a.device.label }}</strong>
|
|
2448
|
+
<hm-tag [name]="resultLabel(a)" [color]="resultColor(a)" />
|
|
2449
|
+
</span>
|
|
2450
|
+
<span class="sp-account__meta">{{ detail(a) }}</span>
|
|
2451
|
+
<span class="sp-account__meta">{{ method(a) }}@if (a.reason) { · {{ reason(a) }} }@if (a.client) { · {{ app(a) }} }</span>
|
|
2452
|
+
</span>
|
|
2453
|
+
</li>
|
|
2454
|
+
} @empty {
|
|
2455
|
+
<li class="sp-account__item sp-account__item--empty">{{ t.activityEmpty }}</li>
|
|
2456
|
+
}
|
|
2457
|
+
</ul>
|
|
2458
|
+
@if (entries().length) {
|
|
2459
|
+
<div class="sp-account__list-footer">
|
|
2460
|
+
<span class="sp-account__note">{{ showing() }}</span>
|
|
2461
|
+
@if (entries().length < total()) {
|
|
2462
|
+
<hm-button size="small" emphasis="medium" [loading]="loading()" (clicked)="more()">{{ t.activityMore }}</hm-button>
|
|
2463
|
+
}
|
|
2464
|
+
</div>
|
|
2465
|
+
}
|
|
2466
|
+
</div>
|
|
2467
|
+
}
|
|
2468
|
+
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\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: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2469
|
+
}
|
|
2470
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountActivityComponent, decorators: [{
|
|
2471
|
+
type: Component,
|
|
2472
|
+
args: [{ selector: 'spine-account-activity', imports: [AlertComponent, ButtonComponent, IconComponent, SegmentedControlComponent, TagComponent], template: `
|
|
2473
|
+
<div class="sp-account__toolbar">
|
|
2474
|
+
<hm-segmented-control size="small" emphasis="action" [label]="t.activityFilter" [options]="filterOptions()" [value]="filter()" (valueChange)="setFilter($event)" />
|
|
2475
|
+
</div>
|
|
2476
|
+
@if (loading() && !entries().length) {
|
|
2477
|
+
<p class="sp-account__note" role="status">{{ messages.accountSecurity.loading }}</p>
|
|
2478
|
+
} @else if (loadError()) {
|
|
2479
|
+
<hm-alert variant="error">{{ errorText() }}</hm-alert>
|
|
2480
|
+
<div class="sp-account__actions">
|
|
2481
|
+
<hm-button size="small" emphasis="medium" icon="refresh" (clicked)="reload()">{{ messages.accountSecurity.retry }}</hm-button>
|
|
2482
|
+
</div>
|
|
2483
|
+
} @else {
|
|
2484
|
+
<div class="sp-account__list">
|
|
2485
|
+
<ul class="sp-account__items">
|
|
2486
|
+
@for (a of entries(); track a.id) {
|
|
2487
|
+
<li class="sp-account__item">
|
|
2488
|
+
<span class="sp-account__icon" [class.sp-account__icon--accent]="a.result === 'success'"><hm-icon [shape]="a.device.mobile ? 'mobile' : 'computer'" /></span>
|
|
2489
|
+
<span class="sp-account__item-main">
|
|
2490
|
+
<span class="sp-account__item-title">
|
|
2491
|
+
<strong>{{ a.device.label }}</strong>
|
|
2492
|
+
<hm-tag [name]="resultLabel(a)" [color]="resultColor(a)" />
|
|
2493
|
+
</span>
|
|
2494
|
+
<span class="sp-account__meta">{{ detail(a) }}</span>
|
|
2495
|
+
<span class="sp-account__meta">{{ method(a) }}@if (a.reason) { · {{ reason(a) }} }@if (a.client) { · {{ app(a) }} }</span>
|
|
2496
|
+
</span>
|
|
2497
|
+
</li>
|
|
2498
|
+
} @empty {
|
|
2499
|
+
<li class="sp-account__item sp-account__item--empty">{{ t.activityEmpty }}</li>
|
|
2500
|
+
}
|
|
2501
|
+
</ul>
|
|
2502
|
+
@if (entries().length) {
|
|
2503
|
+
<div class="sp-account__list-footer">
|
|
2504
|
+
<span class="sp-account__note">{{ showing() }}</span>
|
|
2505
|
+
@if (entries().length < total()) {
|
|
2506
|
+
<hm-button size="small" emphasis="medium" [loading]="loading()" (clicked)="more()">{{ t.activityMore }}</hm-button>
|
|
2507
|
+
}
|
|
2508
|
+
</div>
|
|
2509
|
+
}
|
|
2510
|
+
</div>
|
|
2511
|
+
}
|
|
2512
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"] }]
|
|
2513
|
+
}], ctorParameters: () => [] });
|
|
2514
|
+
|
|
1600
2515
|
/** Métodos que o painel trata (o Argos só tem fluxo para estes; o SMS aparece como indisponível). */
|
|
1601
2516
|
const SUPPORTED = ['totp', 'email', 'sms'];
|
|
1602
2517
|
const nameOf = (m) => m?.method_name?.toLowerCase() ?? '';
|
|
@@ -1604,7 +2519,7 @@ const isTotp = (m) => nameOf(m) === 'totp';
|
|
|
1604
2519
|
const isEmail = (m) => nameOf(m) === 'email';
|
|
1605
2520
|
const isSms = (m) => nameOf(m) === 'sms';
|
|
1606
2521
|
/**
|
|
1607
|
-
* Estado da seção "
|
|
2522
|
+
* Estado da seção "MFA" do painel de configurações: visão geral do Argos e o fluxo de uma
|
|
1608
2523
|
* alteração (reautenticação → cadastro/remoção/códigos). Mesmo roteiro da tela "Segurança da conta" do
|
|
1609
2524
|
* Argos: toda alteração pede antes a prova de identidade recente (senha ou um fator ativo, 5 minutos no
|
|
1610
2525
|
* servidor); a recusa `reauthentication_required` no meio volta para a prova e a ação segue depois dela.
|
|
@@ -1820,7 +2735,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1820
2735
|
type: Injectable
|
|
1821
2736
|
}], ctorParameters: () => [] });
|
|
1822
2737
|
/**
|
|
1823
|
-
* Escopo do estado da conta: no `hm-settings-section` da
|
|
2738
|
+
* Escopo do estado da conta: no `hm-settings-section` da seção "MFA", para as linhas da seção
|
|
1824
2739
|
* (métodos e códigos de recuperação) dividirem o mesmo estado e ele morrer junto com o painel.
|
|
1825
2740
|
*/
|
|
1826
2741
|
class SpineAccountSecurityScopeDirective {
|
|
@@ -1835,7 +2750,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1835
2750
|
}]
|
|
1836
2751
|
}] });
|
|
1837
2752
|
|
|
1838
|
-
let sequence$
|
|
2753
|
+
let sequence$4 = 0;
|
|
1839
2754
|
/**
|
|
1840
2755
|
* Prova de identidade antes de uma alteração na conta (senha, código do autenticador, código enviado ao
|
|
1841
2756
|
* e-mail cadastrado ou código de recuperação). Vale 5 minutos no Argos, presa à sessão do login; depois
|
|
@@ -1845,8 +2760,9 @@ class AccountReauthComponent {
|
|
|
1845
2760
|
constructor() {
|
|
1846
2761
|
this.state = inject(SpineAccountSecurityState);
|
|
1847
2762
|
this.t = inject(SPINE_MESSAGES).accountSecurity;
|
|
1848
|
-
this.titleId = `sp-account-reauth-${++sequence$
|
|
2763
|
+
this.titleId = `sp-account-reauth-${++sequence$4}`;
|
|
1849
2764
|
this.codeLabelId = `${this.titleId}-code`;
|
|
2765
|
+
this.hintId = `${this.titleId}-hint`;
|
|
1850
2766
|
this.password = new FormControl('', { nonNullable: true, validators: [Validators.required] });
|
|
1851
2767
|
this.recoveryCode = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.minLength(10)] });
|
|
1852
2768
|
/** 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. */
|
|
@@ -1893,9 +2809,14 @@ class AccountReauthComponent {
|
|
|
1893
2809
|
}
|
|
1894
2810
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountReauthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1895
2811
|
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
|
-
<
|
|
1898
|
-
|
|
2812
|
+
<section class="sp-account__panel" [attr.aria-labelledby]="titleId" [attr.aria-describedby]="hintId">
|
|
2813
|
+
<div class="sp-account__panel-head">
|
|
2814
|
+
<hm-icon shape="shield-check" />
|
|
2815
|
+
<div class="sp-account__panel-text">
|
|
2816
|
+
<h4 class="sp-account__title" [id]="titleId">{{ t.reauthTitle }}</h4>
|
|
2817
|
+
<p class="sp-account__note" [id]="hintId">{{ t.reauthHint }}</p>
|
|
2818
|
+
</div>
|
|
2819
|
+
</div>
|
|
1899
2820
|
|
|
1900
2821
|
@if (choices().length > 1) {
|
|
1901
2822
|
<hm-radio layout="inline" [label]="t.reauthHow" [options]="choices()" [value]="state.reauthMode()" (valueChange)="choose($event)" />
|
|
@@ -1907,7 +2828,7 @@ class AccountReauthComponent {
|
|
|
1907
2828
|
<hm-input type="password" autocomplete="current-password" [label]="t.currentPassword" [formControl]="password" [messages]="{ required: t.passwordRequired }" />
|
|
1908
2829
|
<div class="sp-account__actions">
|
|
1909
2830
|
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
1910
|
-
<hm-button emphasis="
|
|
2831
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1911
2832
|
</div>
|
|
1912
2833
|
</form>
|
|
1913
2834
|
}
|
|
@@ -1917,14 +2838,14 @@ class AccountReauthComponent {
|
|
|
1917
2838
|
[messages]="{ required: t.recoveryCodeRequired, minlength: t.recoveryCodeLength }" />
|
|
1918
2839
|
<div class="sp-account__actions">
|
|
1919
2840
|
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
1920
|
-
<hm-button emphasis="
|
|
2841
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1921
2842
|
</div>
|
|
1922
2843
|
</form>
|
|
1923
2844
|
}
|
|
1924
2845
|
@case ('email-request') {
|
|
1925
2846
|
<div class="sp-account__actions">
|
|
1926
2847
|
<hm-button emphasis="high" icon="mail" [loading]="state.busy()" (clicked)="state.sendReauthCode()">{{ t.sendEmailCode }}</hm-button>
|
|
1927
|
-
<hm-button emphasis="
|
|
2848
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1928
2849
|
</div>
|
|
1929
2850
|
}
|
|
1930
2851
|
@default {
|
|
@@ -1933,19 +2854,24 @@ class AccountReauthComponent {
|
|
|
1933
2854
|
<hm-code-input (completed)="submitCode($event)" />
|
|
1934
2855
|
</div>
|
|
1935
2856
|
<div class="sp-account__actions">
|
|
1936
|
-
<hm-button emphasis="
|
|
2857
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1937
2858
|
</div>
|
|
1938
2859
|
}
|
|
1939
2860
|
}
|
|
1940
2861
|
</section>
|
|
1941
|
-
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-
|
|
2862
|
+
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\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: 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: RadioComponent, selector: "hm-radio", inputs: ["value", "label", "options", "appearance", "layout", "hint", "help"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1942
2863
|
}
|
|
1943
2864
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountReauthComponent, decorators: [{
|
|
1944
2865
|
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
|
-
<
|
|
1948
|
-
|
|
2866
|
+
args: [{ selector: 'spine-account-reauth', imports: [ReactiveFormsModule, ButtonComponent, CodeInputComponent, IconComponent, InputComponent, RadioComponent], template: `
|
|
2867
|
+
<section class="sp-account__panel" [attr.aria-labelledby]="titleId" [attr.aria-describedby]="hintId">
|
|
2868
|
+
<div class="sp-account__panel-head">
|
|
2869
|
+
<hm-icon shape="shield-check" />
|
|
2870
|
+
<div class="sp-account__panel-text">
|
|
2871
|
+
<h4 class="sp-account__title" [id]="titleId">{{ t.reauthTitle }}</h4>
|
|
2872
|
+
<p class="sp-account__note" [id]="hintId">{{ t.reauthHint }}</p>
|
|
2873
|
+
</div>
|
|
2874
|
+
</div>
|
|
1949
2875
|
|
|
1950
2876
|
@if (choices().length > 1) {
|
|
1951
2877
|
<hm-radio layout="inline" [label]="t.reauthHow" [options]="choices()" [value]="state.reauthMode()" (valueChange)="choose($event)" />
|
|
@@ -1957,7 +2883,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1957
2883
|
<hm-input type="password" autocomplete="current-password" [label]="t.currentPassword" [formControl]="password" [messages]="{ required: t.passwordRequired }" />
|
|
1958
2884
|
<div class="sp-account__actions">
|
|
1959
2885
|
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
1960
|
-
<hm-button emphasis="
|
|
2886
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1961
2887
|
</div>
|
|
1962
2888
|
</form>
|
|
1963
2889
|
}
|
|
@@ -1967,14 +2893,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1967
2893
|
[messages]="{ required: t.recoveryCodeRequired, minlength: t.recoveryCodeLength }" />
|
|
1968
2894
|
<div class="sp-account__actions">
|
|
1969
2895
|
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
1970
|
-
<hm-button emphasis="
|
|
2896
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1971
2897
|
</div>
|
|
1972
2898
|
</form>
|
|
1973
2899
|
}
|
|
1974
2900
|
@case ('email-request') {
|
|
1975
2901
|
<div class="sp-account__actions">
|
|
1976
2902
|
<hm-button emphasis="high" icon="mail" [loading]="state.busy()" (clicked)="state.sendReauthCode()">{{ t.sendEmailCode }}</hm-button>
|
|
1977
|
-
<hm-button emphasis="
|
|
2903
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1978
2904
|
</div>
|
|
1979
2905
|
}
|
|
1980
2906
|
@default {
|
|
@@ -1983,79 +2909,480 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1983
2909
|
<hm-code-input (completed)="submitCode($event)" />
|
|
1984
2910
|
</div>
|
|
1985
2911
|
<div class="sp-account__actions">
|
|
1986
|
-
<hm-button emphasis="
|
|
2912
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
2913
|
+
</div>
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
</section>
|
|
2917
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"] }]
|
|
2918
|
+
}] });
|
|
2919
|
+
|
|
2920
|
+
let sequence$3 = 0;
|
|
2921
|
+
/**
|
|
2922
|
+
* Verificação em duas etapas da conta no Argos, dentro do painel de configurações: os métodos (ativo ou não
|
|
2923
|
+
* cadastrado; o SMS, sem fluxo no Argos, só aparece se já estiver ativo) e as ações — adicionar (TOTP com QR Code
|
|
2924
|
+
* e confirmação pelo código; e-mail com código no endereço informado), trocar o aparelho do autenticador e remover. Cada ação passa antes pela
|
|
2925
|
+
* reautenticação (`spine-account-reauth`).
|
|
2926
|
+
*/
|
|
2927
|
+
class AccountMfaComponent {
|
|
2928
|
+
constructor() {
|
|
2929
|
+
this.state = inject(SpineAccountSecurityState);
|
|
2930
|
+
this.t = inject(SPINE_MESSAGES).accountSecurity;
|
|
2931
|
+
this.fmt = formatMessage;
|
|
2932
|
+
this.isTotp = isTotp;
|
|
2933
|
+
this.isSms = isSms;
|
|
2934
|
+
this.ids = `sp-account-mfa-${++sequence$3}`;
|
|
2935
|
+
this.email = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] });
|
|
2936
|
+
/** Métodos da lista: o SMS não cadastrado fica de fora (não há como cadastrar daqui). */
|
|
2937
|
+
this.shown = computed(() => this.state.methods().filter((m) => !isSms(m) || m.active), ...(ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []));
|
|
2938
|
+
}
|
|
2939
|
+
label(method) {
|
|
2940
|
+
if (!method)
|
|
2941
|
+
return '';
|
|
2942
|
+
if (isTotp(method))
|
|
2943
|
+
return this.t.methodTotp;
|
|
2944
|
+
if (isEmail(method))
|
|
2945
|
+
return this.t.methodEmail;
|
|
2946
|
+
if (isSms(method))
|
|
2947
|
+
return this.t.methodSms;
|
|
2948
|
+
return method.method_name;
|
|
2949
|
+
}
|
|
2950
|
+
icon(method) {
|
|
2951
|
+
return isTotp(method) ? 'authenticator' : isEmail(method) ? 'mail' : 'sms';
|
|
2952
|
+
}
|
|
2953
|
+
status(method) {
|
|
2954
|
+
if (!method.active)
|
|
2955
|
+
return this.t.notRegistered;
|
|
2956
|
+
return method.secret ? formatMessage(this.t.activeWithDetail, { detail: method.secret }) : this.t.active;
|
|
2957
|
+
}
|
|
2958
|
+
/** Método da ação em andamento (para o título "Remover ...?"). */
|
|
2959
|
+
actionMethod() {
|
|
2960
|
+
const action = this.state.action();
|
|
2961
|
+
return action && action.kind !== 'codes' ? action.method : null;
|
|
2962
|
+
}
|
|
2963
|
+
errorText(code) {
|
|
2964
|
+
return this.t.errors[code] ?? this.t.errors.generic;
|
|
2965
|
+
}
|
|
2966
|
+
startAdd(method) {
|
|
2967
|
+
this.email.reset('');
|
|
2968
|
+
this.state.start({ kind: 'add', method });
|
|
2969
|
+
}
|
|
2970
|
+
sendEmail() {
|
|
2971
|
+
this.email.markAsTouched();
|
|
2972
|
+
if (this.email.invalid)
|
|
2973
|
+
return;
|
|
2974
|
+
void this.state.sendEnrollEmail(this.email.value.trim());
|
|
2975
|
+
}
|
|
2976
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountMfaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2977
|
+
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__note\" 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 size=\"small\" 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 sp-account__panel--danger\" [attr.aria-labelledby]=\"ids + '-remove'\">\n <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"trash\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-remove'\">{{ fmt(t.removeTitle, { method: label(actionMethod()) }) }}</h4>\n <p class=\"sp-account__note\">\n {{ t.removeHint }}\n @if (state.activeMethods().length <= 1) { {{ t.removeLastHint }} }\n </p>\n </div>\n </div>\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=\"medium\" [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 <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"authenticator\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-totp'\">{{ t.totpTitle }}</h4>\n @if (state.totpSetup(); as setup) {\n <p class=\"sp-account__note\">{{ t.manualKey }} <code class=\"sp-account__secret\">{{ setup.secret }}</code></p>\n }\n </div>\n </div>\n @if (state.totpSetup()?.qrcode; as qrcode) {\n <div class=\"sp-account__qr\"><img [src]=\"qrcode\" [alt]=\"t.qrAlt\" /></div>\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=\"medium\" [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 <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"mail\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email'\">{{ t.emailTitle }}</h4>\n </div>\n </div>\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=\"medium\" [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 <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"mail\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email-code'\">{{ t.emailCodeTitle }}</h4>\n <p class=\"sp-account__note\" [id]=\"ids + '-email-code-hint'\">{{ fmt(t.emailCodeHint, { email: state.enrollEmail() }) }}</p>\n </div>\n </div>\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=\"medium\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n }\n } @else {\n <div class=\"sp-account__list\">\n <ul class=\"sp-account__items\">\n @for (m of shown(); track m.method_id) {\n <li class=\"sp-account__item\">\n <span class=\"sp-account__icon\" [class.sp-account__icon--accent]=\"m.active\"><hm-icon [shape]=\"icon(m)\" /></span>\n <span class=\"sp-account__item-main\">\n <span class=\"sp-account__item-title\"><strong>{{ label(m) }}</strong></span>\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=\"medium\" 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 @if (data.policy.required) {\n <div class=\"sp-account__list-footer\">\n <span class=\"sp-account__note\">{{ t.requiredHint }}</span>\n </div>\n }\n </div>\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\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 }); }
|
|
2978
|
+
}
|
|
2979
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountMfaComponent, decorators: [{
|
|
2980
|
+
type: Component,
|
|
2981
|
+
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__note\" 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 size=\"small\" 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 sp-account__panel--danger\" [attr.aria-labelledby]=\"ids + '-remove'\">\n <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"trash\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-remove'\">{{ fmt(t.removeTitle, { method: label(actionMethod()) }) }}</h4>\n <p class=\"sp-account__note\">\n {{ t.removeHint }}\n @if (state.activeMethods().length <= 1) { {{ t.removeLastHint }} }\n </p>\n </div>\n </div>\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=\"medium\" [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 <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"authenticator\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-totp'\">{{ t.totpTitle }}</h4>\n @if (state.totpSetup(); as setup) {\n <p class=\"sp-account__note\">{{ t.manualKey }} <code class=\"sp-account__secret\">{{ setup.secret }}</code></p>\n }\n </div>\n </div>\n @if (state.totpSetup()?.qrcode; as qrcode) {\n <div class=\"sp-account__qr\"><img [src]=\"qrcode\" [alt]=\"t.qrAlt\" /></div>\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=\"medium\" [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 <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"mail\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email'\">{{ t.emailTitle }}</h4>\n </div>\n </div>\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=\"medium\" [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 <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"mail\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email-code'\">{{ t.emailCodeTitle }}</h4>\n <p class=\"sp-account__note\" [id]=\"ids + '-email-code-hint'\">{{ fmt(t.emailCodeHint, { email: state.enrollEmail() }) }}</p>\n </div>\n </div>\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=\"medium\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n }\n } @else {\n <div class=\"sp-account__list\">\n <ul class=\"sp-account__items\">\n @for (m of shown(); track m.method_id) {\n <li class=\"sp-account__item\">\n <span class=\"sp-account__icon\" [class.sp-account__icon--accent]=\"m.active\"><hm-icon [shape]=\"icon(m)\" /></span>\n <span class=\"sp-account__item-main\">\n <span class=\"sp-account__item-title\"><strong>{{ label(m) }}</strong></span>\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=\"medium\" 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 @if (data.policy.required) {\n <div class=\"sp-account__list-footer\">\n <span class=\"sp-account__note\">{{ t.requiredHint }}</span>\n </div>\n }\n </div>\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"] }]
|
|
2982
|
+
}] });
|
|
2983
|
+
|
|
2984
|
+
/**
|
|
2985
|
+
* Tokens de aplicação pessoais (`x-app-token`) do usuário logado — `argos-admin /me/app-tokens`.
|
|
2986
|
+
* Sempre da aplicação LOGADA: o servidor tira o client do próprio access-token da sessão, então
|
|
2987
|
+
* aqui não existe escolha de aplicação.
|
|
2988
|
+
* Autentica com o access-token da sessão (o servidor recusa um app-token aqui: token não cria
|
|
2989
|
+
* token). `available` é falso quando o app não tem a URL do argos-admin (`SPINE_ADMIN_API`).
|
|
2990
|
+
*/
|
|
2991
|
+
class SpineAppTokensService {
|
|
2992
|
+
constructor() {
|
|
2993
|
+
this.http = inject(HttpClient);
|
|
2994
|
+
this.auth = inject(AuthorizationService);
|
|
2995
|
+
this.adminApi = inject(SPINE_ADMIN_API, { optional: true });
|
|
2996
|
+
this.available = !!this.adminApi;
|
|
2997
|
+
/** Sobe a cada token criado por aqui (o "Conectar um agente de IA" cria um; a lista de tokens relê). */
|
|
2998
|
+
this.changes = signal(0, ...(ngDevMode ? [{ debugName: "changes" }] : /* istanbul ignore next */ []));
|
|
2999
|
+
}
|
|
3000
|
+
get baseUrl() {
|
|
3001
|
+
return `${this.adminApi}/me/app-tokens`;
|
|
3002
|
+
}
|
|
3003
|
+
get headers() {
|
|
3004
|
+
return new HttpHeaders({ 'x-access-token': this.auth.getToken() ?? '' });
|
|
3005
|
+
}
|
|
3006
|
+
list() {
|
|
3007
|
+
return firstValueFrom(this.http.get(this.baseUrl, { headers: this.headers }));
|
|
3008
|
+
}
|
|
3009
|
+
options() {
|
|
3010
|
+
return firstValueFrom(this.http.get(`${this.baseUrl}/options`, { headers: this.headers }));
|
|
3011
|
+
}
|
|
3012
|
+
async create(body) {
|
|
3013
|
+
const created = await firstValueFrom(this.http.post(this.baseUrl, body, { headers: this.headers }));
|
|
3014
|
+
this.changes.update((n) => n + 1);
|
|
3015
|
+
return created;
|
|
3016
|
+
}
|
|
3017
|
+
revoke(id) {
|
|
3018
|
+
return firstValueFrom(this.http.delete(`${this.baseUrl}${urlPath `/${id}`}`, { headers: this.headers }));
|
|
3019
|
+
}
|
|
3020
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3021
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, providedIn: 'root' }); }
|
|
3022
|
+
}
|
|
3023
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, decorators: [{
|
|
3024
|
+
type: Injectable,
|
|
3025
|
+
args: [{ providedIn: 'root' }]
|
|
3026
|
+
}] });
|
|
3027
|
+
|
|
3028
|
+
/** Monta o `.mcp.json` do Claude Code: uma entrada por servidor, todas com o mesmo token. Com `write`, cada servidor
|
|
3029
|
+
* que tem `writeScopes` pede esses escopos; os demais (e todos sem `write`) vão com `readScopes`, ou sem o header
|
|
3030
|
+
* (o MCP já é só leitura sem ele). URL relativa é resolvida contra `origin`. */
|
|
3031
|
+
function buildMcpConfig(servers, token, write, origin) {
|
|
3032
|
+
const mcpServers = {};
|
|
3033
|
+
for (const server of servers) {
|
|
3034
|
+
const headers = { 'x-app-token': token };
|
|
3035
|
+
const scopes = write && server.writeScopes ? server.writeScopes : server.readScopes;
|
|
3036
|
+
if (scopes)
|
|
3037
|
+
headers['x-mcp-scopes'] = scopes;
|
|
3038
|
+
mcpServers[server.name] = { type: 'http', url: new URL(server.url, origin).href, headers };
|
|
3039
|
+
}
|
|
3040
|
+
return JSON.stringify({ mcpServers }, null, 2);
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* Sessões › "Conectar um agente de IA" (`SpineFeatures.aiAgent`): em um passo, cria um token de aplicação pessoal
|
|
3044
|
+
* (`x-app-token`, a mesma API de "Tokens de aplicação") e monta a configuração MCP pronta para o Claude Code, com os
|
|
3045
|
+
* servidores que o app declarou e os escopos do acesso escolhido (só leitura ou leitura e escrita). O token aparece
|
|
3046
|
+
* uma única vez, dentro da configuração; nunca vai para o console nem para o armazenamento do navegador, e some ao
|
|
3047
|
+
* concluir ou fechar o painel.
|
|
3048
|
+
*/
|
|
3049
|
+
class AiAgentComponent {
|
|
3050
|
+
constructor() {
|
|
3051
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
3052
|
+
this.t = this.messages.accountFeatures;
|
|
3053
|
+
this.tokens = this.messages.appTokens;
|
|
3054
|
+
this.fmt = formatMessage;
|
|
3055
|
+
this.service = inject(SpineAppTokensService);
|
|
3056
|
+
this.toast = inject(HermesToastService);
|
|
3057
|
+
this.preferences = inject(SpinePreferencesService);
|
|
3058
|
+
this.available = this.service.available;
|
|
3059
|
+
this.servers = inject(SPINE_CONFIG, { optional: true })?.features?.aiAgent?.servers ?? [];
|
|
3060
|
+
this.open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
3061
|
+
this.starting = signal(false, ...(ngDevMode ? [{ debugName: "starting" }] : /* istanbul ignore next */ []));
|
|
3062
|
+
this.submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : /* istanbul ignore next */ []));
|
|
3063
|
+
this.selected = signal(new Set(this.servers.map((s) => s.name)), ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
3064
|
+
this.access = signal('read', ...(ngDevMode ? [{ debugName: "access" }] : /* istanbul ignore next */ []));
|
|
3065
|
+
this.badges = signal([], ...(ngDevMode ? [{ debugName: "badges" }] : /* istanbul ignore next */ []));
|
|
3066
|
+
this.badgeId = signal('', ...(ngDevMode ? [{ debugName: "badgeId" }] : /* istanbul ignore next */ []));
|
|
3067
|
+
this.validity = signal('90', ...(ngDevMode ? [{ debugName: "validity" }] : /* istanbul ignore next */ []));
|
|
3068
|
+
/** Configuração gerada (com o token), mostrada uma única vez. */
|
|
3069
|
+
this.result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
|
|
3070
|
+
this.canWrite = computed(() => this.servers.some((s) => s.writeScopes && this.selected().has(s.name)), ...(ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []));
|
|
3071
|
+
this.noBadges = computed(() => !this.badges().length, ...(ngDevMode ? [{ debugName: "noBadges" }] : /* istanbul ignore next */ []));
|
|
3072
|
+
this.canSubmit = computed(() => !!this.selected().size && !!this.badgeId() && !this.submitting(), ...(ngDevMode ? [{ debugName: "canSubmit" }] : /* istanbul ignore next */ []));
|
|
3073
|
+
this.accessOptions = computed(() => [
|
|
3074
|
+
{ value: 'read', label: this.t.aiAgentReadOnly },
|
|
3075
|
+
{ value: 'write', label: this.t.aiAgentReadWrite },
|
|
3076
|
+
], ...(ngDevMode ? [{ debugName: "accessOptions" }] : /* istanbul ignore next */ []));
|
|
3077
|
+
this.badgeOptions = computed(() => this.badges().map((b) => ({ value: b.id, label: b.name })), ...(ngDevMode ? [{ debugName: "badgeOptions" }] : /* istanbul ignore next */ []));
|
|
3078
|
+
this.validityOptions = computed(() => [
|
|
3079
|
+
{ value: '30', label: this.tokens.days30 },
|
|
3080
|
+
{ value: '90', label: this.tokens.days90 },
|
|
3081
|
+
{ value: '180', label: this.tokens.months6 },
|
|
3082
|
+
{ value: '365', label: this.tokens.year1 },
|
|
3083
|
+
], ...(ngDevMode ? [{ debugName: "validityOptions" }] : /* istanbul ignore next */ []));
|
|
3084
|
+
}
|
|
3085
|
+
serverLabel(server) {
|
|
3086
|
+
const label = server.label ?? server.name;
|
|
3087
|
+
return server.writeScopes ? label : formatMessage(this.t.aiAgentReadOnlyServer, { name: label });
|
|
3088
|
+
}
|
|
3089
|
+
toggleServer(name, checked) {
|
|
3090
|
+
const next = new Set(this.selected());
|
|
3091
|
+
if (checked)
|
|
3092
|
+
next.add(name);
|
|
3093
|
+
else
|
|
3094
|
+
next.delete(name);
|
|
3095
|
+
this.selected.set(next);
|
|
3096
|
+
}
|
|
3097
|
+
async start() {
|
|
3098
|
+
this.starting.set(true);
|
|
3099
|
+
try {
|
|
3100
|
+
const { badges } = await this.service.options();
|
|
3101
|
+
this.badges.set(badges);
|
|
3102
|
+
this.badgeId.set(badges[0]?.id ?? '');
|
|
3103
|
+
this.access.set('read');
|
|
3104
|
+
this.validity.set('90');
|
|
3105
|
+
this.open.set(true);
|
|
3106
|
+
}
|
|
3107
|
+
catch (err) {
|
|
3108
|
+
this.toast.error(errorDetail(err) || this.tokens.badgesError);
|
|
3109
|
+
}
|
|
3110
|
+
finally {
|
|
3111
|
+
this.starting.set(false);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
cancel() {
|
|
3115
|
+
this.open.set(false);
|
|
3116
|
+
}
|
|
3117
|
+
async generate() {
|
|
3118
|
+
if (!this.canSubmit())
|
|
3119
|
+
return;
|
|
3120
|
+
this.submitting.set(true);
|
|
3121
|
+
try {
|
|
3122
|
+
const date = new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeZone: this.preferences.effectiveTimeZone() }).format(new Date());
|
|
3123
|
+
const created = await this.service.create({
|
|
3124
|
+
badge_id: this.badgeId(),
|
|
3125
|
+
name: formatMessage(this.t.aiAgentTokenName, { date }).slice(0, 80),
|
|
3126
|
+
validity_days: Number(this.validity()),
|
|
3127
|
+
});
|
|
3128
|
+
const servers = this.servers.filter((s) => this.selected().has(s.name));
|
|
3129
|
+
const write = this.access() === 'write' && this.canWrite();
|
|
3130
|
+
this.result.set({
|
|
3131
|
+
config: buildMcpConfig(servers, created.token, write, location.origin),
|
|
3132
|
+
expires: new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeZone: this.preferences.effectiveTimeZone() })
|
|
3133
|
+
.format(new Date(created.expires_at)),
|
|
3134
|
+
});
|
|
3135
|
+
this.open.set(false);
|
|
3136
|
+
}
|
|
3137
|
+
catch (err) {
|
|
3138
|
+
this.toast.error(errorDetail(err) || this.t.aiAgentCreateError);
|
|
3139
|
+
}
|
|
3140
|
+
finally {
|
|
3141
|
+
this.submitting.set(false);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
async copy() {
|
|
3145
|
+
const config = this.result()?.config;
|
|
3146
|
+
if (!config)
|
|
3147
|
+
return;
|
|
3148
|
+
try {
|
|
3149
|
+
await navigator.clipboard.writeText(config);
|
|
3150
|
+
this.toast.success(this.t.aiAgentCopied);
|
|
3151
|
+
}
|
|
3152
|
+
catch {
|
|
3153
|
+
this.toast.error(this.tokens.copyFailed);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
done() {
|
|
3157
|
+
this.result.set(null);
|
|
3158
|
+
}
|
|
3159
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AiAgentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3160
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AiAgentComponent, isStandalone: true, selector: "spine-ai-agent", ngImport: i0, template: `
|
|
3161
|
+
@if (!available) {
|
|
3162
|
+
<p class="sp-account__note">{{ t.aiAgentUnavailable }}</p>
|
|
3163
|
+
} @else if (result(); as r) {
|
|
3164
|
+
<div class="sp-account__panel sp-account__panel--success" role="status">
|
|
3165
|
+
<div class="sp-account__panel-head">
|
|
3166
|
+
<hm-icon shape="check-circle" />
|
|
3167
|
+
<div class="sp-account__panel-text">
|
|
3168
|
+
<p class="sp-account__title">{{ t.aiAgentResult }}</p>
|
|
3169
|
+
<p class="sp-account__note">{{ fmt(t.aiAgentResultHint, { date: r.expires }) }}</p>
|
|
3170
|
+
</div>
|
|
3171
|
+
</div>
|
|
3172
|
+
<hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.aiAgentResult" />
|
|
3173
|
+
<div class="sp-account__actions">
|
|
3174
|
+
<hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.aiAgentCopy }}</hm-button>
|
|
3175
|
+
<hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.aiAgentDone }}</hm-button>
|
|
3176
|
+
</div>
|
|
3177
|
+
</div>
|
|
3178
|
+
} @else if (open()) {
|
|
3179
|
+
<div class="sp-account__panel">
|
|
3180
|
+
<div class="sp-account__form">
|
|
3181
|
+
@if (servers.length > 1) {
|
|
3182
|
+
<fieldset class="sp-account__choices">
|
|
3183
|
+
<legend class="sp-account__label">{{ t.aiAgentServers }}</legend>
|
|
3184
|
+
@for (s of servers; track s.name) {
|
|
3185
|
+
<hm-checkbox [label]="serverLabel(s)" [checked]="selected().has(s.name)" (checkedChange)="toggleServer(s.name, $event)" />
|
|
3186
|
+
}
|
|
3187
|
+
</fieldset>
|
|
3188
|
+
}
|
|
3189
|
+
@if (canWrite()) {
|
|
3190
|
+
<div class="sp-account__field">
|
|
3191
|
+
<span class="sp-account__label">{{ t.aiAgentAccess }}</span>
|
|
3192
|
+
<hm-segmented-control size="small" emphasis="action" [label]="t.aiAgentAccess" [options]="accessOptions()" [value]="access()" (valueChange)="access.set($event === 'write' ? 'write' : 'read')" />
|
|
3193
|
+
@if (access() === 'write') { <p class="sp-account__note">{{ t.aiAgentWriteHint }}</p> }
|
|
3194
|
+
</div>
|
|
3195
|
+
}
|
|
3196
|
+
@if (badgeOptions().length > 1) {
|
|
3197
|
+
<div class="sp-account__field">
|
|
3198
|
+
<span class="sp-account__label">{{ tokens.accessLevel }}</span>
|
|
3199
|
+
<hm-segmented-control size="small" emphasis="action" [label]="tokens.accessLevel" [options]="badgeOptions()" [value]="badgeId()" (valueChange)="badgeId.set($event)" />
|
|
3200
|
+
</div>
|
|
3201
|
+
}
|
|
3202
|
+
<div class="sp-account__field">
|
|
3203
|
+
<span class="sp-account__label">{{ tokens.validity }}</span>
|
|
3204
|
+
<hm-segmented-control size="small" emphasis="action" [label]="tokens.validity" [options]="validityOptions()" [value]="validity()" (valueChange)="validity.set($event)" />
|
|
3205
|
+
</div>
|
|
3206
|
+
@if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.aiAgentSelectServer }}</p> }
|
|
3207
|
+
@if (noBadges()) { <p class="sp-account__note sp-account__note--error">{{ tokens.noBadges }}</p> }
|
|
3208
|
+
</div>
|
|
3209
|
+
<div class="sp-account__actions">
|
|
3210
|
+
<hm-button size="small" emphasis="medium" [disabled]="submitting()" (clicked)="cancel()">{{ tokens.cancel }}</hm-button>
|
|
3211
|
+
<hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.aiAgentGenerate }}</hm-button>
|
|
3212
|
+
</div>
|
|
3213
|
+
</div>
|
|
3214
|
+
} @else {
|
|
3215
|
+
<div class="sp-account__actions">
|
|
3216
|
+
<hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.aiAgentStart }}</hm-button>
|
|
3217
|
+
</div>
|
|
3218
|
+
}
|
|
3219
|
+
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"], dependencies: [{ 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: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: CodeViewerComponent, selector: "hm-code-viewer", inputs: ["code", "language", "filename", "label", "showLineNumbers", "firstLineNumber", "highlightLines", "wrap", "tabSize", "copyable", "header", "virtualize", "virtualizeThreshold", "revealInvisibles"], outputs: ["copied", "tokenized"] }, { 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 }); }
|
|
3220
|
+
}
|
|
3221
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AiAgentComponent, decorators: [{
|
|
3222
|
+
type: Component,
|
|
3223
|
+
args: [{ selector: 'spine-ai-agent', imports: [ButtonComponent, CheckboxComponent, CodeViewerComponent, IconComponent, SegmentedControlComponent], template: `
|
|
3224
|
+
@if (!available) {
|
|
3225
|
+
<p class="sp-account__note">{{ t.aiAgentUnavailable }}</p>
|
|
3226
|
+
} @else if (result(); as r) {
|
|
3227
|
+
<div class="sp-account__panel sp-account__panel--success" role="status">
|
|
3228
|
+
<div class="sp-account__panel-head">
|
|
3229
|
+
<hm-icon shape="check-circle" />
|
|
3230
|
+
<div class="sp-account__panel-text">
|
|
3231
|
+
<p class="sp-account__title">{{ t.aiAgentResult }}</p>
|
|
3232
|
+
<p class="sp-account__note">{{ fmt(t.aiAgentResultHint, { date: r.expires }) }}</p>
|
|
3233
|
+
</div>
|
|
3234
|
+
</div>
|
|
3235
|
+
<hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.aiAgentResult" />
|
|
3236
|
+
<div class="sp-account__actions">
|
|
3237
|
+
<hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.aiAgentCopy }}</hm-button>
|
|
3238
|
+
<hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.aiAgentDone }}</hm-button>
|
|
3239
|
+
</div>
|
|
3240
|
+
</div>
|
|
3241
|
+
} @else if (open()) {
|
|
3242
|
+
<div class="sp-account__panel">
|
|
3243
|
+
<div class="sp-account__form">
|
|
3244
|
+
@if (servers.length > 1) {
|
|
3245
|
+
<fieldset class="sp-account__choices">
|
|
3246
|
+
<legend class="sp-account__label">{{ t.aiAgentServers }}</legend>
|
|
3247
|
+
@for (s of servers; track s.name) {
|
|
3248
|
+
<hm-checkbox [label]="serverLabel(s)" [checked]="selected().has(s.name)" (checkedChange)="toggleServer(s.name, $event)" />
|
|
3249
|
+
}
|
|
3250
|
+
</fieldset>
|
|
3251
|
+
}
|
|
3252
|
+
@if (canWrite()) {
|
|
3253
|
+
<div class="sp-account__field">
|
|
3254
|
+
<span class="sp-account__label">{{ t.aiAgentAccess }}</span>
|
|
3255
|
+
<hm-segmented-control size="small" emphasis="action" [label]="t.aiAgentAccess" [options]="accessOptions()" [value]="access()" (valueChange)="access.set($event === 'write' ? 'write' : 'read')" />
|
|
3256
|
+
@if (access() === 'write') { <p class="sp-account__note">{{ t.aiAgentWriteHint }}</p> }
|
|
3257
|
+
</div>
|
|
3258
|
+
}
|
|
3259
|
+
@if (badgeOptions().length > 1) {
|
|
3260
|
+
<div class="sp-account__field">
|
|
3261
|
+
<span class="sp-account__label">{{ tokens.accessLevel }}</span>
|
|
3262
|
+
<hm-segmented-control size="small" emphasis="action" [label]="tokens.accessLevel" [options]="badgeOptions()" [value]="badgeId()" (valueChange)="badgeId.set($event)" />
|
|
3263
|
+
</div>
|
|
3264
|
+
}
|
|
3265
|
+
<div class="sp-account__field">
|
|
3266
|
+
<span class="sp-account__label">{{ tokens.validity }}</span>
|
|
3267
|
+
<hm-segmented-control size="small" emphasis="action" [label]="tokens.validity" [options]="validityOptions()" [value]="validity()" (valueChange)="validity.set($event)" />
|
|
1987
3268
|
</div>
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
3269
|
+
@if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.aiAgentSelectServer }}</p> }
|
|
3270
|
+
@if (noBadges()) { <p class="sp-account__note sp-account__note--error">{{ tokens.noBadges }}</p> }
|
|
3271
|
+
</div>
|
|
3272
|
+
<div class="sp-account__actions">
|
|
3273
|
+
<hm-button size="small" emphasis="medium" [disabled]="submitting()" (clicked)="cancel()">{{ tokens.cancel }}</hm-button>
|
|
3274
|
+
<hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.aiAgentGenerate }}</hm-button>
|
|
3275
|
+
</div>
|
|
3276
|
+
</div>
|
|
3277
|
+
} @else {
|
|
3278
|
+
<div class="sp-account__actions">
|
|
3279
|
+
<hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.aiAgentStart }}</hm-button>
|
|
3280
|
+
</div>
|
|
3281
|
+
}
|
|
3282
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"] }]
|
|
1992
3283
|
}] });
|
|
3284
|
+
function errorDetail(err) {
|
|
3285
|
+
return err instanceof HttpErrorResponse ? (err.error?.detail ?? '') : '';
|
|
3286
|
+
}
|
|
1993
3287
|
|
|
1994
|
-
let sequence$2 = 0;
|
|
1995
3288
|
/**
|
|
1996
|
-
*
|
|
1997
|
-
*
|
|
1998
|
-
*
|
|
1999
|
-
*
|
|
3289
|
+
* Crachá padrão (Conta › "Crachá padrão", `SpineFeatures.defaultBadge`): o nível de acesso com que o login entra
|
|
3290
|
+
* nesta aplicação, guardado no Argos por usuário e aplicação (`GET/PUT {api}/badges`) — vale em qualquer navegador.
|
|
3291
|
+
* "Automático" volta à regra do Argos (o crachá de menor nível). A troca no menu do perfil continua valendo só para a
|
|
3292
|
+
* sessão.
|
|
2000
3293
|
*/
|
|
2001
|
-
class
|
|
3294
|
+
class DefaultBadgeComponent {
|
|
2002
3295
|
constructor() {
|
|
2003
|
-
this.
|
|
2004
|
-
this.
|
|
2005
|
-
this.
|
|
2006
|
-
this.
|
|
2007
|
-
this.
|
|
2008
|
-
this.
|
|
2009
|
-
this.
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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;
|
|
3296
|
+
this.api = inject(SpineAccountSecurityService);
|
|
3297
|
+
this.toast = inject(HermesToastService);
|
|
3298
|
+
this.t = inject(SPINE_MESSAGES).accountFeatures;
|
|
3299
|
+
this.badges = signal([], ...(ngDevMode ? [{ debugName: "badges" }] : /* istanbul ignore next */ []));
|
|
3300
|
+
this.value = signal('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
3301
|
+
this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
3302
|
+
this.saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
|
|
3303
|
+
this.loadError = signal(false, ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
|
|
3304
|
+
this.select = viewChild(SelectComponent, ...(ngDevMode ? [{ debugName: "select" }] : /* istanbul ignore next */ []));
|
|
3305
|
+
void this.load();
|
|
2039
3306
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
3307
|
+
async load() {
|
|
3308
|
+
try {
|
|
3309
|
+
const result = await this.api.badges();
|
|
3310
|
+
this.badges.set(result.badges ?? []);
|
|
3311
|
+
this.value.set(result.default_badge_id ?? '');
|
|
3312
|
+
}
|
|
3313
|
+
catch {
|
|
3314
|
+
this.loadError.set(true);
|
|
3315
|
+
}
|
|
3316
|
+
finally {
|
|
3317
|
+
this.loading.set(false);
|
|
3318
|
+
}
|
|
2043
3319
|
}
|
|
2044
|
-
|
|
2045
|
-
this.
|
|
2046
|
-
if (
|
|
3320
|
+
async save(value) {
|
|
3321
|
+
const previous = this.value();
|
|
3322
|
+
if (value === previous)
|
|
2047
3323
|
return;
|
|
2048
|
-
|
|
3324
|
+
this.value.set(value);
|
|
3325
|
+
this.saving.set(true);
|
|
3326
|
+
try {
|
|
3327
|
+
const result = await this.api.setDefaultBadge(value || null);
|
|
3328
|
+
this.value.set(result.default_badge_id ?? '');
|
|
3329
|
+
this.toast.success(this.t.defaultBadgeSaved);
|
|
3330
|
+
}
|
|
3331
|
+
catch {
|
|
3332
|
+
this.value.set(previous);
|
|
3333
|
+
// O <select> já mostra a escolha recusada: volta ao valor salvo.
|
|
3334
|
+
this.select()?.value.set(previous);
|
|
3335
|
+
this.toast.error(this.t.defaultBadgeError);
|
|
3336
|
+
}
|
|
3337
|
+
finally {
|
|
3338
|
+
this.saving.set(false);
|
|
3339
|
+
}
|
|
2049
3340
|
}
|
|
2050
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type:
|
|
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 }); }
|
|
3341
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DefaultBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3342
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: DefaultBadgeComponent, isStandalone: true, selector: "spine-default-badge", viewQueries: [{ propertyName: "select", first: true, predicate: SelectComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
3343
|
+
@if (loadError()) {
|
|
3344
|
+
<hm-alert variant="error">{{ t.defaultBadgeLoadError }}</hm-alert>
|
|
3345
|
+
} @else {
|
|
3346
|
+
<hm-select
|
|
3347
|
+
class="sp-settings__badge"
|
|
3348
|
+
[label]="t.defaultBadge"
|
|
3349
|
+
[labelHidden]="true"
|
|
3350
|
+
[value]="value()"
|
|
3351
|
+
[disabled]="loading() || saving()"
|
|
3352
|
+
(valueChange)="save($event ?? '')"
|
|
3353
|
+
>
|
|
3354
|
+
<option value="">{{ t.defaultBadgeAuto }}</option>
|
|
3355
|
+
@for (b of badges(); track b.id) {
|
|
3356
|
+
<option [value]="b.id">{{ b.name }}</option>
|
|
3357
|
+
}
|
|
3358
|
+
</hm-select>
|
|
3359
|
+
}
|
|
3360
|
+
`, isInline: true, styles: [":host{display:block;min-width:0}.sp-settings__badge{min-inline-size:14rem;max-inline-size:100%}\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: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2052
3361
|
}
|
|
2053
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type:
|
|
3362
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DefaultBadgeComponent, decorators: [{
|
|
2054
3363
|
type: Component,
|
|
2055
|
-
args: [{ selector: 'spine-
|
|
2056
|
-
|
|
3364
|
+
args: [{ selector: 'spine-default-badge', imports: [AlertComponent, SelectComponent], template: `
|
|
3365
|
+
@if (loadError()) {
|
|
3366
|
+
<hm-alert variant="error">{{ t.defaultBadgeLoadError }}</hm-alert>
|
|
3367
|
+
} @else {
|
|
3368
|
+
<hm-select
|
|
3369
|
+
class="sp-settings__badge"
|
|
3370
|
+
[label]="t.defaultBadge"
|
|
3371
|
+
[labelHidden]="true"
|
|
3372
|
+
[value]="value()"
|
|
3373
|
+
[disabled]="loading() || saving()"
|
|
3374
|
+
(valueChange)="save($event ?? '')"
|
|
3375
|
+
>
|
|
3376
|
+
<option value="">{{ t.defaultBadgeAuto }}</option>
|
|
3377
|
+
@for (b of badges(); track b.id) {
|
|
3378
|
+
<option [value]="b.id">{{ b.name }}</option>
|
|
3379
|
+
}
|
|
3380
|
+
</hm-select>
|
|
3381
|
+
}
|
|
3382
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;min-width:0}.sp-settings__badge{min-inline-size:14rem;max-inline-size:100%}\n"] }]
|
|
3383
|
+
}], ctorParameters: () => [], propDecorators: { select: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SelectComponent), { isSignal: true }] }] } });
|
|
2057
3384
|
|
|
2058
|
-
let sequence$
|
|
3385
|
+
let sequence$2 = 0;
|
|
2059
3386
|
/**
|
|
2060
3387
|
* Códigos de recuperação da conta no Argos: quantos restam e "gerar novos" (com reautenticação). O lote
|
|
2061
3388
|
* novo aparece UMA vez (o Argos só guarda o hash), com copiar e baixar em .txt, até a pessoa dizer que
|
|
@@ -2067,7 +3394,7 @@ class AccountRecoveryCodesComponent {
|
|
|
2067
3394
|
this.messages = inject(SPINE_MESSAGES);
|
|
2068
3395
|
this.t = this.messages.accountSecurity;
|
|
2069
3396
|
this.toast = inject(HermesToastService);
|
|
2070
|
-
this.titleId = `sp-account-codes-${++sequence$
|
|
3397
|
+
this.titleId = `sp-account-codes-${++sequence$2}`;
|
|
2071
3398
|
this.remaining = computed(() => pluralMessage(this.t.codesRemaining, this.state.remainingCodes(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "remaining" }] : /* istanbul ignore next */ []));
|
|
2072
3399
|
}
|
|
2073
3400
|
errorText(code) {
|
|
@@ -2110,35 +3437,52 @@ class AccountRecoveryCodesComponent {
|
|
|
2110
3437
|
<spine-account-reauth />
|
|
2111
3438
|
} @else if (state.owner() === 'codes' && state.step() === 'codes' && state.issuedCodes(); as codes) {
|
|
2112
3439
|
<section class="sp-account__panel sp-account__panel--success" [attr.aria-labelledby]="titleId">
|
|
2113
|
-
<
|
|
2114
|
-
|
|
3440
|
+
<div class="sp-account__panel-head">
|
|
3441
|
+
<hm-icon shape="key" />
|
|
3442
|
+
<div class="sp-account__panel-text">
|
|
3443
|
+
<h4 class="sp-account__title" [id]="titleId">{{ t.codesTitle }}</h4>
|
|
3444
|
+
<p class="sp-account__note">{{ t.codesShownOnce }}</p>
|
|
3445
|
+
</div>
|
|
3446
|
+
</div>
|
|
2115
3447
|
<ol class="sp-account__codes">
|
|
2116
3448
|
@for (code of codes; track code) {
|
|
2117
3449
|
<li><code>{{ code }}</code></li>
|
|
2118
3450
|
}
|
|
2119
3451
|
</ol>
|
|
2120
3452
|
<div class="sp-account__actions">
|
|
3453
|
+
<hm-button emphasis="high" (clicked)="state.codesSaved()">{{ t.codesSaved }}</hm-button>
|
|
2121
3454
|
<hm-button emphasis="medium" icon="copy" (clicked)="copy(codes)">{{ t.copyCodes }}</hm-button>
|
|
2122
3455
|
<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
3456
|
</div>
|
|
2125
3457
|
</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
3458
|
} @else {
|
|
2134
|
-
<
|
|
3459
|
+
<ul class="sp-account__list">
|
|
3460
|
+
<li class="sp-account__item">
|
|
3461
|
+
<span class="sp-account__icon" [class.sp-account__icon--accent]="state.remainingCodes() > 0"><hm-icon shape="key" /></span>
|
|
3462
|
+
<span class="sp-account__item-main">
|
|
3463
|
+
@if (state.activeMethods().length) {
|
|
3464
|
+
<span class="sp-account__item-title"><strong>{{ remaining() }}</strong></span>
|
|
3465
|
+
<span class="sp-account__meta">{{ t.codesHint }}</span>
|
|
3466
|
+
} @else {
|
|
3467
|
+
<span class="sp-account__meta">{{ t.codesNeedMethod }}</span>
|
|
3468
|
+
}
|
|
3469
|
+
</span>
|
|
3470
|
+
@if (state.activeMethods().length) {
|
|
3471
|
+
<span class="sp-account__item-actions">
|
|
3472
|
+
<hm-button size="small" emphasis="medium" [disabled]="state.busy() || state.step() !== 'idle'" (clicked)="state.start({ kind: 'codes' })">
|
|
3473
|
+
{{ state.remainingCodes() ? t.regenerateCodes : t.generateCodes }}
|
|
3474
|
+
</hm-button>
|
|
3475
|
+
</span>
|
|
3476
|
+
}
|
|
3477
|
+
</li>
|
|
3478
|
+
</ul>
|
|
2135
3479
|
}
|
|
2136
3480
|
}
|
|
2137
|
-
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-
|
|
3481
|
+
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\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: AccountReauthComponent, selector: "spine-account-reauth" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2138
3482
|
}
|
|
2139
3483
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountRecoveryCodesComponent, decorators: [{
|
|
2140
3484
|
type: Component,
|
|
2141
|
-
args: [{ selector: 'spine-account-recovery-codes', imports: [AlertComponent, ButtonComponent, AccountReauthComponent], template: `
|
|
3485
|
+
args: [{ selector: 'spine-account-recovery-codes', imports: [AlertComponent, ButtonComponent, IconComponent, AccountReauthComponent], template: `
|
|
2142
3486
|
@if (state.overview()) {
|
|
2143
3487
|
@if (state.owner() === 'codes') {
|
|
2144
3488
|
@if (state.notice(); as notice) {
|
|
@@ -2153,31 +3497,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
2153
3497
|
<spine-account-reauth />
|
|
2154
3498
|
} @else if (state.owner() === 'codes' && state.step() === 'codes' && state.issuedCodes(); as codes) {
|
|
2155
3499
|
<section class="sp-account__panel sp-account__panel--success" [attr.aria-labelledby]="titleId">
|
|
2156
|
-
<
|
|
2157
|
-
|
|
3500
|
+
<div class="sp-account__panel-head">
|
|
3501
|
+
<hm-icon shape="key" />
|
|
3502
|
+
<div class="sp-account__panel-text">
|
|
3503
|
+
<h4 class="sp-account__title" [id]="titleId">{{ t.codesTitle }}</h4>
|
|
3504
|
+
<p class="sp-account__note">{{ t.codesShownOnce }}</p>
|
|
3505
|
+
</div>
|
|
3506
|
+
</div>
|
|
2158
3507
|
<ol class="sp-account__codes">
|
|
2159
3508
|
@for (code of codes; track code) {
|
|
2160
3509
|
<li><code>{{ code }}</code></li>
|
|
2161
3510
|
}
|
|
2162
3511
|
</ol>
|
|
2163
3512
|
<div class="sp-account__actions">
|
|
3513
|
+
<hm-button emphasis="high" (clicked)="state.codesSaved()">{{ t.codesSaved }}</hm-button>
|
|
2164
3514
|
<hm-button emphasis="medium" icon="copy" (clicked)="copy(codes)">{{ t.copyCodes }}</hm-button>
|
|
2165
3515
|
<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
3516
|
</div>
|
|
2168
3517
|
</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
3518
|
} @else {
|
|
2177
|
-
<
|
|
3519
|
+
<ul class="sp-account__list">
|
|
3520
|
+
<li class="sp-account__item">
|
|
3521
|
+
<span class="sp-account__icon" [class.sp-account__icon--accent]="state.remainingCodes() > 0"><hm-icon shape="key" /></span>
|
|
3522
|
+
<span class="sp-account__item-main">
|
|
3523
|
+
@if (state.activeMethods().length) {
|
|
3524
|
+
<span class="sp-account__item-title"><strong>{{ remaining() }}</strong></span>
|
|
3525
|
+
<span class="sp-account__meta">{{ t.codesHint }}</span>
|
|
3526
|
+
} @else {
|
|
3527
|
+
<span class="sp-account__meta">{{ t.codesNeedMethod }}</span>
|
|
3528
|
+
}
|
|
3529
|
+
</span>
|
|
3530
|
+
@if (state.activeMethods().length) {
|
|
3531
|
+
<span class="sp-account__item-actions">
|
|
3532
|
+
<hm-button size="small" emphasis="medium" [disabled]="state.busy() || state.step() !== 'idle'" (clicked)="state.start({ kind: 'codes' })">
|
|
3533
|
+
{{ state.remainingCodes() ? t.regenerateCodes : t.generateCodes }}
|
|
3534
|
+
</hm-button>
|
|
3535
|
+
</span>
|
|
3536
|
+
}
|
|
3537
|
+
</li>
|
|
3538
|
+
</ul>
|
|
2178
3539
|
}
|
|
2179
3540
|
}
|
|
2180
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-
|
|
3541
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"] }]
|
|
2181
3542
|
}] });
|
|
2182
3543
|
|
|
2183
3544
|
/**
|
|
@@ -2288,53 +3649,14 @@ class AccountSessionsComponent {
|
|
|
2288
3649
|
.format(new Date(value));
|
|
2289
3650
|
}
|
|
2290
3651
|
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-
|
|
3652
|
+
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__note\" 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 size=\"small\" 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 <div class=\"sp-account__list\">\n <ul class=\"sp-account__items\">\n @for (s of ordered(); track s.id) {\n <li class=\"sp-account__item\">\n <span class=\"sp-account__icon\" [class.sp-account__icon--accent]=\"s.current\"><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) }}@if (s.clients.length) { \u00B7 {{ apps(s) }} }</span>\n </span>\n @if (!s.current) {\n <span class=\"sp-account__item-actions\">\n @if (confirming() === s.id) {\n <hm-button size=\"small\" emphasis=\"medium\" [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__item--empty\">{{ t.sessionsEmpty }}</li>\n }\n </ul>\n\n @if (others() > 0) {\n <div class=\"sp-account__list-footer\">\n @if (confirming() === 'others') {\n <span class=\"sp-account__note\">{{ othersConfirm() }}</span>\n <span class=\"sp-account__actions\">\n <hm-button size=\"small\" emphasis=\"medium\" [disabled]=\"busy()\" (clicked)=\"cancel()\">{{ t.cancel }}</hm-button>\n <hm-button size=\"small\" emphasis=\"high\" intent=\"danger\" icon=\"logout\" [loading]=\"busy()\" (clicked)=\"endOthers()\">{{ t.endOthers }}</hm-button>\n </span>\n } @else {\n <hm-button size=\"small\" emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [disabled]=\"busy()\" (clicked)=\"ask('others')\">{{ t.endOthers }}</hm-button>\n }\n </div>\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\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
3653
|
}
|
|
2293
3654
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountSessionsComponent, decorators: [{
|
|
2294
3655
|
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-
|
|
3656
|
+
args: [{ selector: 'spine-account-sessions', imports: [AlertComponent, ButtonComponent, IconComponent, TagComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (loading() && !sessions().length) {\n <p class=\"sp-account__note\" 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 size=\"small\" 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 <div class=\"sp-account__list\">\n <ul class=\"sp-account__items\">\n @for (s of ordered(); track s.id) {\n <li class=\"sp-account__item\">\n <span class=\"sp-account__icon\" [class.sp-account__icon--accent]=\"s.current\"><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) }}@if (s.clients.length) { \u00B7 {{ apps(s) }} }</span>\n </span>\n @if (!s.current) {\n <span class=\"sp-account__item-actions\">\n @if (confirming() === s.id) {\n <hm-button size=\"small\" emphasis=\"medium\" [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__item--empty\">{{ t.sessionsEmpty }}</li>\n }\n </ul>\n\n @if (others() > 0) {\n <div class=\"sp-account__list-footer\">\n @if (confirming() === 'others') {\n <span class=\"sp-account__note\">{{ othersConfirm() }}</span>\n <span class=\"sp-account__actions\">\n <hm-button size=\"small\" emphasis=\"medium\" [disabled]=\"busy()\" (clicked)=\"cancel()\">{{ t.cancel }}</hm-button>\n <hm-button size=\"small\" emphasis=\"high\" intent=\"danger\" icon=\"logout\" [loading]=\"busy()\" (clicked)=\"endOthers()\">{{ t.endOthers }}</hm-button>\n </span>\n } @else {\n <hm-button size=\"small\" emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [disabled]=\"busy()\" (clicked)=\"ask('others')\">{{ t.endOthers }}</hm-button>\n }\n </div>\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"] }]
|
|
2296
3657
|
}], ctorParameters: () => [] });
|
|
2297
3658
|
|
|
2298
|
-
|
|
2299
|
-
* Tokens de aplicação pessoais (`x-app-token`) do usuário logado — `argos-admin /me/app-tokens`.
|
|
2300
|
-
* Sempre da aplicação LOGADA: o servidor tira o client do próprio access-token da sessão, então
|
|
2301
|
-
* aqui não existe escolha de aplicação.
|
|
2302
|
-
* Autentica com o access-token da sessão (o servidor recusa um app-token aqui: token não cria
|
|
2303
|
-
* token). `available` é falso quando o app não tem a URL do argos-admin (`SPINE_ADMIN_API`).
|
|
2304
|
-
*/
|
|
2305
|
-
class SpineAppTokensService {
|
|
2306
|
-
constructor() {
|
|
2307
|
-
this.http = inject(HttpClient);
|
|
2308
|
-
this.auth = inject(AuthorizationService);
|
|
2309
|
-
this.adminApi = inject(SPINE_ADMIN_API, { optional: true });
|
|
2310
|
-
this.available = !!this.adminApi;
|
|
2311
|
-
}
|
|
2312
|
-
get baseUrl() {
|
|
2313
|
-
return `${this.adminApi}/me/app-tokens`;
|
|
2314
|
-
}
|
|
2315
|
-
get headers() {
|
|
2316
|
-
return new HttpHeaders({ 'x-access-token': this.auth.getToken() ?? '' });
|
|
2317
|
-
}
|
|
2318
|
-
list() {
|
|
2319
|
-
return firstValueFrom(this.http.get(this.baseUrl, { headers: this.headers }));
|
|
2320
|
-
}
|
|
2321
|
-
options() {
|
|
2322
|
-
return firstValueFrom(this.http.get(`${this.baseUrl}/options`, { headers: this.headers }));
|
|
2323
|
-
}
|
|
2324
|
-
create(body) {
|
|
2325
|
-
return firstValueFrom(this.http.post(this.baseUrl, body, { headers: this.headers }));
|
|
2326
|
-
}
|
|
2327
|
-
revoke(id) {
|
|
2328
|
-
return firstValueFrom(this.http.delete(`${this.baseUrl}${urlPath `/${id}`}`, { headers: this.headers }));
|
|
2329
|
-
}
|
|
2330
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2331
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, providedIn: 'root' }); }
|
|
2332
|
-
}
|
|
2333
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, decorators: [{
|
|
2334
|
-
type: Injectable,
|
|
2335
|
-
args: [{ providedIn: 'root' }]
|
|
2336
|
-
}] });
|
|
2337
|
-
|
|
3659
|
+
let sequence$1 = 0;
|
|
2338
3660
|
/** Validades oferecidas (dias). O servidor recusa acima de 365 — o CHECK no banco também. */
|
|
2339
3661
|
/**
|
|
2340
3662
|
* Tokens de aplicação do usuário (seção Conta do painel de configurações): lista, criação e
|
|
@@ -2352,6 +3674,7 @@ class AppTokensComponent {
|
|
|
2352
3674
|
this.toast = inject(HermesToastService);
|
|
2353
3675
|
this.preferences = inject(SpinePreferencesService);
|
|
2354
3676
|
this.available = this.service.available;
|
|
3677
|
+
this.ids = `sp-tokens-${++sequence$1}`;
|
|
2355
3678
|
this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2356
3679
|
this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
|
|
2357
3680
|
this.tokens = signal([], ...(ngDevMode ? [{ debugName: "tokens" }] : /* istanbul ignore next */ []));
|
|
@@ -2383,6 +3706,16 @@ class AppTokensComponent {
|
|
|
2383
3706
|
void this.load();
|
|
2384
3707
|
else
|
|
2385
3708
|
this.loading.set(false);
|
|
3709
|
+
// Token criado em outro lugar do painel ("Conectar um agente de IA"): relê a lista. O que esta lista cria já relê.
|
|
3710
|
+
let seen = this.service.changes();
|
|
3711
|
+
effect(() => {
|
|
3712
|
+
const changes = this.service.changes();
|
|
3713
|
+
if (changes === seen)
|
|
3714
|
+
return;
|
|
3715
|
+
seen = changes;
|
|
3716
|
+
if (!untracked(this.submitting))
|
|
3717
|
+
void untracked(() => this.load());
|
|
3718
|
+
});
|
|
2386
3719
|
}
|
|
2387
3720
|
async load() {
|
|
2388
3721
|
this.loading.set(true);
|
|
@@ -2472,6 +3805,9 @@ class AppTokensComponent {
|
|
|
2472
3805
|
this.toast.error(this.errorText(err, this.text.revokeError));
|
|
2473
3806
|
}
|
|
2474
3807
|
}
|
|
3808
|
+
cancelRevoke() {
|
|
3809
|
+
this.confirmingRevoke.set(null);
|
|
3810
|
+
}
|
|
2475
3811
|
formatDate(value) {
|
|
2476
3812
|
if (!value)
|
|
2477
3813
|
return this.text.never;
|
|
@@ -2483,11 +3819,11 @@ class AppTokensComponent {
|
|
|
2483
3819
|
return detail || fallback;
|
|
2484
3820
|
}
|
|
2485
3821
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AppTokensComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
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 }); }
|
|
3822
|
+
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-account__note\">{{ text.unavailable }}</p>\n} @else {\n <!-- Token rec\u00E9m-criado: \u00FAnica vez em que o valor aparece. -->\n @if (created(); as c) {\n <section class=\"sp-account__panel sp-account__panel--success\" role=\"status\" [attr.aria-labelledby]=\"ids + '-created'\">\n <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"check-circle\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-created'\">{{ fmt(text.created, { name: c.name }) }}</h4>\n <p class=\"sp-account__note\" [innerHTML]=\"fmtHtml(text.sendHeader, { date: formatDate(c.expiresAt) })\"></p>\n </div>\n </div>\n <code class=\"sp-account__secret sp-account__secret--block\">{{ c.token }}</code>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"high\" icon=\"copy\" (clicked)=\"copyCreated()\">{{ text.copyToken }}</hm-button>\n <hm-button emphasis=\"medium\" (clicked)=\"dismissCreated()\">{{ text.saved }}</hm-button>\n </div>\n </section>\n }\n\n @if (loading()) {\n <p class=\"sp-account__note\" role=\"status\">{{ text.loading }}</p>\n } @else if (loadError()) {\n <p class=\"sp-account__note sp-account__note--error\">{{ loadError() }}</p>\n } @else {\n <div class=\"sp-account__list\">\n <ul class=\"sp-account__items\">\n @for (t of tokens(); track t.id) {\n <li class=\"sp-account__item\" [class.sp-account__item--muted]=\"t.expired\">\n <span class=\"sp-account__icon\" [class.sp-account__icon--accent]=\"!t.expired\"><hm-icon shape=\"key\" /></span>\n <span class=\"sp-account__item-main\">\n <span class=\"sp-account__item-title\"><strong>{{ t.name }}</strong></span>\n <span class=\"sp-account__meta\">{{ t.badge_name }} \u00B7 <code>{{ t.prefix }}</code></span>\n <span class=\"sp-account__meta\">\n @if (t.expired) { <span class=\"sp-account__meta--danger\">{{ 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 <span class=\"sp-account__item-actions\">\n @if (confirmingRevoke() === t.id) {\n <hm-button size=\"small\" emphasis=\"medium\" (clicked)=\"cancelRevoke()\">{{ text.cancel }}</hm-button>\n }\n <hm-button size=\"small\" [emphasis]=\"confirmingRevoke() === t.id ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"revoke(t)\">\n {{ confirmingRevoke() === t.id ? text.confirmRevoke : text.revoke }}\n </hm-button>\n </span>\n </li>\n } @empty {\n <li class=\"sp-account__item sp-account__item--empty\">{{ text.empty }}</li>\n }\n </ul>\n\n @if (!creating()) {\n <div class=\"sp-account__list-footer\">\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"plus\" [disabled]=\"atLimit()\" (clicked)=\"startCreate()\">{{ text.newToken }}</hm-button>\n <span class=\"sp-account__note\">{{ atLimit() ? text.limit : fmt(text.validCount, { valid: validCount(), max: maxActive() }) }}</span>\n </div>\n }\n </div>\n\n @if (creating()) {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-new'\">\n <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"key\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-new'\">{{ text.newToken }}</h4>\n @if (clientName()) {\n <p class=\"sp-account__note\" [innerHTML]=\"fmtHtml(text.appOnly, { name: clientName() })\"></p>\n }\n </div>\n </div>\n <form class=\"sp-account__form\" (submit)=\"$event.preventDefault(); submit()\">\n @if (!badges().length) {\n <p class=\"sp-account__note\">{{ text.noBadges }}</p>\n } @else {\n @if (badgeOptions().length > 1) {\n <div class=\"sp-account__field\">\n <span class=\"sp-account__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-account__field\">\n <span class=\"sp-account__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-account__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=\"medium\" (clicked)=\"cancelCreate()\">{{ text.cancel }}</hm-button>\n </div>\n </form>\n </section>\n }\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\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 }); }
|
|
2487
3823
|
}
|
|
2488
3824
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AppTokensComponent, decorators: [{
|
|
2489
3825
|
type: Component,
|
|
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"] }]
|
|
3826
|
+
args: [{ selector: 'spine-app-tokens', imports: [ReactiveFormsModule, ButtonComponent, InputComponent, IconComponent, SegmentedControlComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (!available) {\n <p class=\"sp-account__note\">{{ text.unavailable }}</p>\n} @else {\n <!-- Token rec\u00E9m-criado: \u00FAnica vez em que o valor aparece. -->\n @if (created(); as c) {\n <section class=\"sp-account__panel sp-account__panel--success\" role=\"status\" [attr.aria-labelledby]=\"ids + '-created'\">\n <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"check-circle\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-created'\">{{ fmt(text.created, { name: c.name }) }}</h4>\n <p class=\"sp-account__note\" [innerHTML]=\"fmtHtml(text.sendHeader, { date: formatDate(c.expiresAt) })\"></p>\n </div>\n </div>\n <code class=\"sp-account__secret sp-account__secret--block\">{{ c.token }}</code>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"high\" icon=\"copy\" (clicked)=\"copyCreated()\">{{ text.copyToken }}</hm-button>\n <hm-button emphasis=\"medium\" (clicked)=\"dismissCreated()\">{{ text.saved }}</hm-button>\n </div>\n </section>\n }\n\n @if (loading()) {\n <p class=\"sp-account__note\" role=\"status\">{{ text.loading }}</p>\n } @else if (loadError()) {\n <p class=\"sp-account__note sp-account__note--error\">{{ loadError() }}</p>\n } @else {\n <div class=\"sp-account__list\">\n <ul class=\"sp-account__items\">\n @for (t of tokens(); track t.id) {\n <li class=\"sp-account__item\" [class.sp-account__item--muted]=\"t.expired\">\n <span class=\"sp-account__icon\" [class.sp-account__icon--accent]=\"!t.expired\"><hm-icon shape=\"key\" /></span>\n <span class=\"sp-account__item-main\">\n <span class=\"sp-account__item-title\"><strong>{{ t.name }}</strong></span>\n <span class=\"sp-account__meta\">{{ t.badge_name }} \u00B7 <code>{{ t.prefix }}</code></span>\n <span class=\"sp-account__meta\">\n @if (t.expired) { <span class=\"sp-account__meta--danger\">{{ 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 <span class=\"sp-account__item-actions\">\n @if (confirmingRevoke() === t.id) {\n <hm-button size=\"small\" emphasis=\"medium\" (clicked)=\"cancelRevoke()\">{{ text.cancel }}</hm-button>\n }\n <hm-button size=\"small\" [emphasis]=\"confirmingRevoke() === t.id ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"revoke(t)\">\n {{ confirmingRevoke() === t.id ? text.confirmRevoke : text.revoke }}\n </hm-button>\n </span>\n </li>\n } @empty {\n <li class=\"sp-account__item sp-account__item--empty\">{{ text.empty }}</li>\n }\n </ul>\n\n @if (!creating()) {\n <div class=\"sp-account__list-footer\">\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"plus\" [disabled]=\"atLimit()\" (clicked)=\"startCreate()\">{{ text.newToken }}</hm-button>\n <span class=\"sp-account__note\">{{ atLimit() ? text.limit : fmt(text.validCount, { valid: validCount(), max: maxActive() }) }}</span>\n </div>\n }\n </div>\n\n @if (creating()) {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-new'\">\n <div class=\"sp-account__panel-head\">\n <hm-icon shape=\"key\" />\n <div class=\"sp-account__panel-text\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-new'\">{{ text.newToken }}</h4>\n @if (clientName()) {\n <p class=\"sp-account__note\" [innerHTML]=\"fmtHtml(text.appOnly, { name: clientName() })\"></p>\n }\n </div>\n </div>\n <form class=\"sp-account__form\" (submit)=\"$event.preventDefault(); submit()\">\n @if (!badges().length) {\n <p class=\"sp-account__note\">{{ text.noBadges }}</p>\n } @else {\n @if (badgeOptions().length > 1) {\n <div class=\"sp-account__field\">\n <span class=\"sp-account__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-account__field\">\n <span class=\"sp-account__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-account__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=\"medium\" (clicked)=\"cancelCreate()\">{{ text.cancel }}</hm-button>\n </div>\n </form>\n </section>\n }\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;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__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.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:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.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:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"] }]
|
|
2491
3827
|
}], ctorParameters: () => [] });
|
|
2492
3828
|
|
|
2493
3829
|
/** Dependências que valem mostrar em "Sobre" (as demais são detalhe de build). */
|
|
@@ -2502,23 +3838,78 @@ const KEY_DEPENDENCIES = [
|
|
|
2502
3838
|
{ name: 'rxjs', label: 'RxJS' },
|
|
2503
3839
|
// check-strings: on
|
|
2504
3840
|
];
|
|
3841
|
+
/** Lista → chave do `SpineUserStorage` (por usuário). */
|
|
3842
|
+
const BROWSER_LISTS = {
|
|
3843
|
+
recents: SPINE_USER_STORAGE_KEYS.recents,
|
|
3844
|
+
searches: SPINE_USER_STORAGE_KEYS.recentSearches,
|
|
3845
|
+
favorites: SPINE_USER_STORAGE_KEYS.favorites,
|
|
3846
|
+
};
|
|
2505
3847
|
/**
|
|
2506
|
-
* Painel de configurações do shell (substitui as telas do antigo módulo "Sistema"):
|
|
2507
|
-
*
|
|
2508
|
-
*
|
|
2509
|
-
*
|
|
3848
|
+
* Painel de configurações do shell (substitui as telas do antigo módulo "Sistema"): Conta (perfil, nível,
|
|
3849
|
+
* sessão, sair), MFA (verificação em duas etapas e códigos de recuperação do Argos), Sessões (token de
|
|
3850
|
+
* acesso, tokens de aplicação e sessões e dispositivos do Argos), Aparência, Acessibilidade (tamanho do texto,
|
|
3851
|
+
* animações), Região (idioma, fuso, formato de hora, primeiro dia da semana), Atalhos, Dados do navegador
|
|
3852
|
+
* (limpar listas, restaurar preferências), Ajuda (novidades, tour) e Sobre, mais as seções que o app registrar
|
|
3853
|
+
* com `provideSettingsSection`. Tudo vale na hora — cada controle chama o serviço que já persiste aquela
|
|
3854
|
+
* preferência (tema, densidade, menu lateral, fuso, tamanho do texto…).
|
|
2510
3855
|
*
|
|
2511
3856
|
* Aberto por `SpineSettingsService.open()` — menu do perfil, `Ctrl+,` ou `?settings=<seção>`.
|
|
2512
3857
|
*/
|
|
2513
3858
|
class SettingsPanelComponent {
|
|
3859
|
+
setThemeMode(mode) {
|
|
3860
|
+
this.theme.setMode(mode);
|
|
3861
|
+
}
|
|
3862
|
+
setFamily(id) {
|
|
3863
|
+
const family = this.families.find((f) => f.id === id);
|
|
3864
|
+
if (family)
|
|
3865
|
+
this.presets.setPreset(family);
|
|
3866
|
+
}
|
|
3867
|
+
setDensity(value) {
|
|
3868
|
+
this.density.setDensity(value);
|
|
3869
|
+
}
|
|
3870
|
+
setFont(value) {
|
|
3871
|
+
this.hermesTheme.setFont(value);
|
|
3872
|
+
}
|
|
3873
|
+
setSidebarMode(value) {
|
|
3874
|
+
this.sidebar.setMode(value);
|
|
3875
|
+
}
|
|
3876
|
+
setTextSize(value) {
|
|
3877
|
+
this.preferences.setTextScale(Number(value));
|
|
3878
|
+
}
|
|
3879
|
+
setMotion(value) {
|
|
3880
|
+
this.preferences.setMotion(value);
|
|
3881
|
+
}
|
|
3882
|
+
setHourCycle(value) {
|
|
3883
|
+
this.preferences.setHourCycle(value);
|
|
3884
|
+
}
|
|
3885
|
+
setWeekStart(value) {
|
|
3886
|
+
this.preferences.setWeekStart(value === '' ? null : Number(value));
|
|
3887
|
+
}
|
|
3888
|
+
/** Fecha o painel antes: novidades e tour ocupam a tela. */
|
|
3889
|
+
showNews() {
|
|
3890
|
+
this.settings.close();
|
|
3891
|
+
this.news.showAll();
|
|
3892
|
+
}
|
|
3893
|
+
replayTour() {
|
|
3894
|
+
this.settings.close();
|
|
3895
|
+
this.onboarding.restart();
|
|
3896
|
+
}
|
|
2514
3897
|
constructor() {
|
|
2515
3898
|
this.settings = inject(SpineSettingsService);
|
|
2516
3899
|
this.messages = inject(SPINE_MESSAGES);
|
|
2517
3900
|
this.text = this.messages.settings;
|
|
2518
3901
|
this.fmt = formatMessage;
|
|
2519
3902
|
this.keys = SPINE_SETTINGS_KEYS;
|
|
3903
|
+
/** Seção aberta, com a chave antiga já trocada pela nova (`seguranca` → `mfa`). */
|
|
3904
|
+
this.activeSection = computed(() => resolveSettingsSection(this.settings.section()), ...(ngDevMode ? [{ debugName: "activeSection" }] : /* istanbul ignore next */ []));
|
|
2520
3905
|
this.theme = inject(ThemeService);
|
|
2521
3906
|
this.hermesTheme = inject(HermesThemeService);
|
|
3907
|
+
this.themeConfig = inject(HERMES_THEME_CONFIG);
|
|
3908
|
+
this.sidebarConfig = inject(HERMES_SIDEBAR_CONFIG);
|
|
3909
|
+
this.i18n = inject(HermesI18nService);
|
|
3910
|
+
this.news = inject(SpineNewsService);
|
|
3911
|
+
this.quickAccess = inject(SpineQuickAccessService);
|
|
3912
|
+
this.userStorage = inject(SpineUserStorage);
|
|
2522
3913
|
this.presets = inject(ThemePresetService);
|
|
2523
3914
|
this.density = inject(DensityService);
|
|
2524
3915
|
this.sidebar = inject(HermesSidebarService);
|
|
@@ -2532,6 +3923,9 @@ class SettingsPanelComponent {
|
|
|
2532
3923
|
this.config = inject(SPINE_CONFIG, { optional: true });
|
|
2533
3924
|
this.appSections = inject(SPINE_SETTINGS_SECTIONS, { optional: true }) ?? [];
|
|
2534
3925
|
this.appName = this.config?.appName ?? 'Hermes';
|
|
3926
|
+
/** Recursos opcionais ligados pelo app (`SpineConfig.features`). */
|
|
3927
|
+
this.features = this.config?.features ?? {};
|
|
3928
|
+
this.featureText = this.messages.accountFeatures;
|
|
2535
3929
|
/**
|
|
2536
3930
|
* "Copiar token de acesso" é ferramenta de desenvolvimento: põe o token da sessão (Bearer de tudo que a
|
|
2537
3931
|
* pessoa pode fazer) na área de transferência, ao alcance de qualquer app/extensão que leia o clipboard.
|
|
@@ -2561,6 +3955,18 @@ class SettingsPanelComponent {
|
|
|
2561
3955
|
this.densityValue = computed(() => this.hermesTheme.density(), ...(ngDevMode ? [{ debugName: "densityValue" }] : /* istanbul ignore next */ []));
|
|
2562
3956
|
this.fontValue = computed(() => this.hermesTheme.font(), ...(ngDevMode ? [{ debugName: "fontValue" }] : /* istanbul ignore next */ []));
|
|
2563
3957
|
this.families = this.presets.families;
|
|
3958
|
+
// ── Acessibilidade ──────────────────────────────────────────────────────────
|
|
3959
|
+
/** 100%, 110%, 125%, 150% — no formato de porcentagem do idioma. */
|
|
3960
|
+
this.textSizeOptions = computed(() => {
|
|
3961
|
+
const percent = new Intl.NumberFormat(this.i18n.formatLocale(), { style: 'percent', maximumFractionDigits: 0 });
|
|
3962
|
+
return HERMES_TEXT_SCALES.map((scale) => ({ value: String(scale), label: percent.format(scale) }));
|
|
3963
|
+
}, ...(ngDevMode ? [{ debugName: "textSizeOptions" }] : /* istanbul ignore next */ []));
|
|
3964
|
+
this.textSizeValue = computed(() => String(this.preferences.textScale()), ...(ngDevMode ? [{ debugName: "textSizeValue" }] : /* istanbul ignore next */ []));
|
|
3965
|
+
this.motionOptions = computed(() => [
|
|
3966
|
+
{ value: 'system', label: this.text.motionSystem },
|
|
3967
|
+
{ value: 'reduce', label: this.text.motionReduce },
|
|
3968
|
+
{ value: 'full', label: this.text.motionFull },
|
|
3969
|
+
], ...(ngDevMode ? [{ debugName: "motionOptions" }] : /* istanbul ignore next */ []));
|
|
2564
3970
|
// ── Região ──────────────────────────────────────────────────────────────────
|
|
2565
3971
|
this.languageOptions = computed(() => {
|
|
2566
3972
|
const appLanguage = this.preferences.languages.find((l) => l.value === this.preferences.appLanguage)?.label ?? this.preferences.appLanguage;
|
|
@@ -2582,46 +3988,183 @@ class SettingsPanelComponent {
|
|
|
2582
3988
|
this.timeZonePreview = computed(() => {
|
|
2583
3989
|
const zone = this.preferences.effectiveTimeZone();
|
|
2584
3990
|
try {
|
|
2585
|
-
return new Intl.DateTimeFormat(this.
|
|
3991
|
+
return new Intl.DateTimeFormat(this.i18n.formatLocale(), { dateStyle: 'short', timeStyle: 'short', timeZone: zone }).format(new Date())
|
|
2586
3992
|
+ ` (${offsetOf(zone)})`;
|
|
2587
3993
|
}
|
|
2588
3994
|
catch {
|
|
2589
3995
|
return zone;
|
|
2590
3996
|
}
|
|
2591
3997
|
}, ...(ngDevMode ? [{ debugName: "timeZonePreview" }] : /* istanbul ignore next */ []));
|
|
3998
|
+
this.hourCycleOptions = computed(() => [
|
|
3999
|
+
{ value: '', label: this.text.hourCycleAuto },
|
|
4000
|
+
{ value: 'h23', label: this.text.hourCycle24 },
|
|
4001
|
+
{ value: 'h12', label: this.text.hourCycle12 },
|
|
4002
|
+
], ...(ngDevMode ? [{ debugName: "hourCycleOptions" }] : /* istanbul ignore next */ []));
|
|
4003
|
+
/** Hora agora, no relógio escolhido — prévia do formato. */
|
|
4004
|
+
this.hourCyclePreview = computed(() => {
|
|
4005
|
+
try {
|
|
4006
|
+
return new Intl.DateTimeFormat(this.i18n.formatLocale(), { timeStyle: 'short', timeZone: this.preferences.effectiveTimeZone() }).format(new Date());
|
|
4007
|
+
}
|
|
4008
|
+
catch {
|
|
4009
|
+
return '';
|
|
4010
|
+
}
|
|
4011
|
+
}, ...(ngDevMode ? [{ debugName: "hourCyclePreview" }] : /* istanbul ignore next */ []));
|
|
4012
|
+
/** Automático, domingo e segunda — os nomes dos dias saem do `Intl`, no idioma. */
|
|
4013
|
+
this.weekStartOptions = computed(() => {
|
|
4014
|
+
const names = weekdayNames(this.i18n.locale(), 'long', 0);
|
|
4015
|
+
return [
|
|
4016
|
+
{ value: '', label: this.text.weekStartAuto },
|
|
4017
|
+
{ value: '0', label: names[0] },
|
|
4018
|
+
{ value: '1', label: names[1] },
|
|
4019
|
+
];
|
|
4020
|
+
}, ...(ngDevMode ? [{ debugName: "weekStartOptions" }] : /* istanbul ignore next */ []));
|
|
4021
|
+
this.weekStartValue = computed(() => {
|
|
4022
|
+
const day = this.preferences.weekStart();
|
|
4023
|
+
return day === null ? '' : String(day);
|
|
4024
|
+
}, ...(ngDevMode ? [{ debugName: "weekStartValue" }] : /* istanbul ignore next */ []));
|
|
2592
4025
|
// ── Atalhos ─────────────────────────────────────────────────────────────────
|
|
2593
4026
|
/** Atalhos registrados (favoritos numa linha só) + os que outra peça dispara — os mesmos do painel "?". */
|
|
2594
4027
|
this.shortcutItems = this.shortcutService.panelItems;
|
|
2595
|
-
|
|
2596
|
-
|
|
4028
|
+
// ── Ajuda ───────────────────────────────────────────────────────────────────
|
|
4029
|
+
/** "Ver novidades" só quando o app tem alguma vigente (`provideSpineNews`). */
|
|
4030
|
+
this.hasNews = this.news.hasAny;
|
|
4031
|
+
// ── Dados do navegador ──────────────────────────────────────────────────────
|
|
4032
|
+
/** Relida quando o painel abre e depois de limpar (as buscas recentes não têm sinal próprio). */
|
|
4033
|
+
this.dataVersion = signal(0, ...(ngDevMode ? [{ debugName: "dataVersion" }] : /* istanbul ignore next */ []));
|
|
4034
|
+
this.counts = computed(() => {
|
|
4035
|
+
this.dataVersion();
|
|
4036
|
+
this.settings.isOpen();
|
|
4037
|
+
return {
|
|
4038
|
+
recents: this.quickAccess.recents().length,
|
|
4039
|
+
searches: this.userStorage.count(BROWSER_LISTS.searches),
|
|
4040
|
+
favorites: this.quickAccess.favorites().length,
|
|
4041
|
+
};
|
|
4042
|
+
}, ...(ngDevMode ? [{ debugName: "counts" }] : /* istanbul ignore next */ []));
|
|
4043
|
+
/** As três listas, com a contagem já no texto da linha. */
|
|
4044
|
+
this.browserLists = computed(() => {
|
|
4045
|
+
const counts = this.counts();
|
|
4046
|
+
const locale = this.i18n.formatLocale();
|
|
4047
|
+
const row = (id, label, keywords, count) => ({ id, label, keywords, count, empty: counts[id] === 0 });
|
|
4048
|
+
return [
|
|
4049
|
+
row('recents', this.text.recentPages, this.text.recentPagesKeywords, pluralMessage(this.text.pagesCount, counts.recents, locale)),
|
|
4050
|
+
row('searches', this.text.recentSearches, this.text.recentSearchesKeywords, pluralMessage(this.text.searchesCount, counts.searches, locale)),
|
|
4051
|
+
row('favorites', this.text.favorites, this.text.favoritesKeywords, pluralMessage(this.text.pagesCount, counts.favorites, locale)),
|
|
4052
|
+
];
|
|
4053
|
+
}, ...(ngDevMode ? [{ debugName: "browserLists" }] : /* istanbul ignore next */ []));
|
|
4054
|
+
/** Ação destrutiva esperando confirmação (uma por vez, como nas listas do painel). */
|
|
4055
|
+
this.confirming = signal(null, ...(ngDevMode ? [{ debugName: "confirming" }] : /* istanbul ignore next */ []));
|
|
4056
|
+
this.logoutService = inject(SpineLogoutService);
|
|
4057
|
+
/** "Saindo…" no botão; volta a `false` também quando a saída falha. */
|
|
4058
|
+
this.logoutPending = this.logoutService.pending;
|
|
4059
|
+
// ── MFA ─────────────────────────────────────────────────────────────────────
|
|
2597
4060
|
/** 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
|
|
4061
|
+
* `authority`/`realmId`, ou com `CHANGE_ME`) a seção MFA e a linha "Sessões e dispositivos" não aparecem. */
|
|
2599
4062
|
this.accountSecurity = inject(SpineAccountSecurityService).available;
|
|
4063
|
+
// ── Sessões ─────────────────────────────────────────────────────────────────
|
|
4064
|
+
/** Linhas da seção Sessões, cada uma com a própria condição. "Tokens de aplicação" sempre aparece (sem
|
|
4065
|
+
* `SPINE_ADMIN_API` os tokens explicam que não estão disponíveis); o token de acesso só com
|
|
4066
|
+
* `technicalDocs`; sessões e dispositivos só com a API da conta. A "Sessão" (validade) fica em Conta. */
|
|
4067
|
+
this.sessionsItems = {
|
|
4068
|
+
accessToken: this.showAccessToken,
|
|
4069
|
+
appTokens: true,
|
|
4070
|
+
aiAgent: !!this.config?.features?.aiAgent?.servers?.length,
|
|
4071
|
+
devices: this.accountSecurity,
|
|
4072
|
+
};
|
|
4073
|
+
/** Sem nenhuma linha visível a seção não aparece. */
|
|
4074
|
+
this.hasSessionsSection = Object.values(this.sessionsItems).some(Boolean);
|
|
4075
|
+
// ── Recursos opcionais (SpineConfig.features) ──────────────────────────────
|
|
4076
|
+
this.idle = inject(SpineIdleService);
|
|
4077
|
+
this.startPageService = inject(SpineStartPageService);
|
|
4078
|
+
this.router = inject(Router);
|
|
4079
|
+
this.notices = inject(SpineNoticesService);
|
|
4080
|
+
this.accountPreferences = inject(SpineAccountPreferencesService);
|
|
4081
|
+
/** Conta › "Encerrar por inatividade": opções dentro dos limites do app. */
|
|
4082
|
+
this.idleOptions = computed(() => this.idle.limits.options.map((m) => ({ value: String(m), label: m ? formatMessage(this.featureText.idleOption, { minutes: m }) : this.featureText.idleNever })), ...(ngDevMode ? [{ debugName: "idleOptions" }] : /* istanbul ignore next */ []));
|
|
4083
|
+
this.idleValue = computed(() => String(this.idle.minutes()), ...(ngDevMode ? [{ debugName: "idleValue" }] : /* istanbul ignore next */ []));
|
|
4084
|
+
this.idleHint = this.idle.limits.max ? formatMessage(this.featureText.idleLimitHint, { minutes: this.idle.limits.max }) : '';
|
|
4085
|
+
/** Conta › "Tela inicial": início, última tela e cada favorito. */
|
|
4086
|
+
this.startPageOptions = computed(() => [
|
|
4087
|
+
{ value: 'home', label: this.featureText.startHome },
|
|
4088
|
+
{ value: 'last', label: this.featureText.startLast },
|
|
4089
|
+
...this.quickAccess.favorites().map((f) => ({ value: `fav:${f.id}`, label: formatMessage(this.featureText.startFavorite, { name: f.displayName || f.redirect }) })),
|
|
4090
|
+
], ...(ngDevMode ? [{ debugName: "startPageOptions" }] : /* istanbul ignore next */ []));
|
|
4091
|
+
/** Favorito escolhido que deixou de ser favorito: mostra "Início" (é para onde o login vai). */
|
|
4092
|
+
this.startPageValue = computed(() => {
|
|
4093
|
+
const value = this.preferences.startPage();
|
|
4094
|
+
return this.startPageOptions().some((o) => o.value === value) ? value : 'home';
|
|
4095
|
+
}, ...(ngDevMode ? [{ debugName: "startPageValue" }] : /* istanbul ignore next */ []));
|
|
4096
|
+
this.quickAccessHasFavorites = computed(() => this.quickAccess.favorites().length > 0, ...(ngDevMode ? [{ debugName: "quickAccessHasFavorites" }] : /* istanbul ignore next */ []));
|
|
4097
|
+
/** "Preferências na conta": situação da sincronização. */
|
|
4098
|
+
this.syncStatus = computed(() => {
|
|
4099
|
+
const status = this.accountPreferences.status();
|
|
4100
|
+
const at = this.accountPreferences.syncedAt();
|
|
4101
|
+
if (status === 'local')
|
|
4102
|
+
return this.featureText.syncLocal;
|
|
4103
|
+
if (status === 'error')
|
|
4104
|
+
return this.featureText.syncError;
|
|
4105
|
+
if (status === 'saving' || status === 'loading')
|
|
4106
|
+
return this.featureText.syncSaving;
|
|
4107
|
+
if (!at)
|
|
4108
|
+
return '';
|
|
4109
|
+
const time = new Intl.DateTimeFormat(this.i18n.formatLocale(), { timeStyle: 'short', timeZone: this.preferences.effectiveTimeZone() }).format(at);
|
|
4110
|
+
return formatMessage(this.featureText.syncSynced, { time });
|
|
4111
|
+
}, ...(ngDevMode ? [{ debugName: "syncStatus" }] : /* istanbul ignore next */ []));
|
|
2600
4112
|
// ── Sobre ───────────────────────────────────────────────────────────────────
|
|
2601
4113
|
this.dependencies = KEY_DEPENDENCIES
|
|
2602
4114
|
.map((d) => ({ ...d, version: this.packageInfo.dependencies?.[d.name] }))
|
|
2603
4115
|
.filter((d) => !!d.version);
|
|
4116
|
+
// Painel fechado por qualquer caminho: a confirmação pendente não reaparece na próxima abertura.
|
|
4117
|
+
effect(() => {
|
|
4118
|
+
if (!this.settings.isOpen())
|
|
4119
|
+
this.confirming.set(null);
|
|
4120
|
+
});
|
|
2604
4121
|
}
|
|
2605
|
-
|
|
2606
|
-
this.
|
|
2607
|
-
}
|
|
2608
|
-
setFamily(id) {
|
|
2609
|
-
const family = this.families.find((f) => f.id === id);
|
|
2610
|
-
if (family)
|
|
2611
|
-
this.presets.setPreset(family);
|
|
2612
|
-
}
|
|
2613
|
-
setDensity(value) {
|
|
2614
|
-
this.density.setDensity(value);
|
|
2615
|
-
}
|
|
2616
|
-
setFont(value) {
|
|
2617
|
-
this.hermesTheme.setFont(value);
|
|
4122
|
+
cancelConfirm() {
|
|
4123
|
+
this.confirming.set(null);
|
|
2618
4124
|
}
|
|
2619
|
-
|
|
2620
|
-
|
|
4125
|
+
/** 1º clique pede confirmação; o 2º apaga a lista do usuário atual e avisa. */
|
|
4126
|
+
clearList(list) {
|
|
4127
|
+
if (this.confirming() !== list) {
|
|
4128
|
+
this.confirming.set(list);
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4131
|
+
this.confirming.set(null);
|
|
4132
|
+
this.userStorage.clear(BROWSER_LISTS[list]);
|
|
4133
|
+
this.dataVersion.update((v) => v + 1);
|
|
4134
|
+
const done = { recents: this.text.recentPagesCleared, searches: this.text.recentSearchesCleared, favorites: this.text.favoritesCleared };
|
|
4135
|
+
this.toast.success(done[list]);
|
|
2621
4136
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
4137
|
+
/**
|
|
4138
|
+
* Restaurar preferências (com confirmação): tema (sistema), cor principal (a padrão do app), densidade
|
|
4139
|
+
* (confortável), fonte (a padrão do app), menu lateral (o modo padrão do app), tamanho do texto (100%),
|
|
4140
|
+
* animações (sistema), formato de hora e primeiro dia da semana (automáticos). Idioma, fuso horário e
|
|
4141
|
+
* atalhos de tecla única ficam: mudam como a pessoa lê e digita, e não são "aparência".
|
|
4142
|
+
*/
|
|
4143
|
+
restorePreferences() {
|
|
4144
|
+
if (this.confirming() !== 'preferences') {
|
|
4145
|
+
this.confirming.set('preferences');
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
this.confirming.set(null);
|
|
4149
|
+
const config = this.themeConfig;
|
|
4150
|
+
this.theme.setMode('system');
|
|
4151
|
+
const families = this.families;
|
|
4152
|
+
const knownFamily = (id) => !!id && families.some((f) => f.id === (config.aliases?.[id] ?? id));
|
|
4153
|
+
const family = [config.defaultFamily, FALLBACK_FAMILY, families[0]?.id].find(knownFamily);
|
|
4154
|
+
if (family)
|
|
4155
|
+
this.hermesTheme.setFamily(family);
|
|
4156
|
+
this.density.setDensity('comfortable');
|
|
4157
|
+
const fonts = this.hermesTheme.fonts ?? HERMES_FONTS;
|
|
4158
|
+
const knownFont = (id) => !!id && fonts.some((f) => f.id === (config.fontAliases?.[id] ?? id));
|
|
4159
|
+
const font = [config.defaultFont, 'geist', fonts[0]?.id].find(knownFont);
|
|
4160
|
+
if (font)
|
|
4161
|
+
this.hermesTheme.setFont(font);
|
|
4162
|
+
this.sidebar.setMode(this.sidebarConfig.defaultMode ?? 'auto');
|
|
4163
|
+
this.preferences.setTextScale(1);
|
|
4164
|
+
this.preferences.setMotion('system');
|
|
4165
|
+
this.preferences.setHourCycle('');
|
|
4166
|
+
this.preferences.setWeekStart(null);
|
|
4167
|
+
this.toast.success(this.text.preferencesRestored);
|
|
2625
4168
|
}
|
|
2626
4169
|
// ── Conta ───────────────────────────────────────────────────────────────────
|
|
2627
4170
|
get profile() { return this.auth.profile; }
|
|
@@ -2633,7 +4176,7 @@ class SettingsPanelComponent {
|
|
|
2633
4176
|
return this.text.sessionUnknown;
|
|
2634
4177
|
const at = new Date(exp * 1000);
|
|
2635
4178
|
const minutes = Math.round((at.getTime() - Date.now()) / 60000);
|
|
2636
|
-
const when = new Intl.DateTimeFormat(this.
|
|
4179
|
+
const when = new Intl.DateTimeFormat(this.i18n.formatLocale(), { timeStyle: 'short', timeZone: this.preferences.effectiveTimeZone() }).format(at);
|
|
2637
4180
|
if (minutes <= 0)
|
|
2638
4181
|
return formatMessage(this.text.sessionExpired, { time: when });
|
|
2639
4182
|
return minutes < 60
|
|
@@ -2650,10 +4193,10 @@ class SettingsPanelComponent {
|
|
|
2650
4193
|
}
|
|
2651
4194
|
await this.copy(token, this.text.tokenCopied);
|
|
2652
4195
|
}
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
this.
|
|
2656
|
-
|
|
4196
|
+
/** O painel fica aberto até a saída dar certo: com falha, a pessoa continua aqui, com o aviso de erro. */
|
|
4197
|
+
async logout() {
|
|
4198
|
+
if (await this.logoutService.logout())
|
|
4199
|
+
this.settings.close();
|
|
2657
4200
|
}
|
|
2658
4201
|
/** Login da pessoa (`preferred_username` do token, ou o e-mail), para dizer QUAL conta do Argos. */
|
|
2659
4202
|
get accountLogin() {
|
|
@@ -2661,12 +4204,40 @@ class SettingsPanelComponent {
|
|
|
2661
4204
|
const login = payload?.['preferred_username'] ?? payload?.['email'];
|
|
2662
4205
|
return typeof login === 'string' ? login : '';
|
|
2663
4206
|
}
|
|
4207
|
+
/** Realm pelo nome (`master`, `Minha Empresa`); um id (UUID) não diz nada a quem lê e fica de fora. */
|
|
2664
4208
|
get accountRealm() {
|
|
2665
|
-
|
|
4209
|
+
const realm = this.authConfig?.realmId?.trim() ?? '';
|
|
4210
|
+
return UUID.test(realm) ? '' : realm;
|
|
4211
|
+
}
|
|
4212
|
+
setIdle(value) {
|
|
4213
|
+
this.preferences.setIdleMinutes(Number(value));
|
|
4214
|
+
}
|
|
4215
|
+
setStartPage(value) {
|
|
4216
|
+
this.preferences.setStartPage(value);
|
|
4217
|
+
// "Última tela visitada" começa a valer já com a tela de agora.
|
|
4218
|
+
if (value === 'last')
|
|
4219
|
+
this.startPageService.remember(this.router.url);
|
|
4220
|
+
}
|
|
4221
|
+
/** Conta › "Crachá padrão": só com mais de um crachá nesta aplicação (claim `badges` do token). */
|
|
4222
|
+
get showDefaultBadge() {
|
|
4223
|
+
return !!this.features.defaultBadge && this.accountSecurity && (this.auth.badges?.length ?? 0) > 1;
|
|
4224
|
+
}
|
|
4225
|
+
/** Notificações: aviso do navegador ligado (escolha + permissão). */
|
|
4226
|
+
async setBrowserNotices(enabled) {
|
|
4227
|
+
if (!enabled) {
|
|
4228
|
+
this.notices.disableBrowser();
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
const ok = await this.notices.enableBrowser();
|
|
4232
|
+
if (!ok)
|
|
4233
|
+
this.toast.warning(this.notices.permission() === 'unsupported' ? this.featureText.browserNoticesUnsupported : this.featureText.browserNoticesDenied);
|
|
4234
|
+
}
|
|
4235
|
+
syncNow() {
|
|
4236
|
+
void this.accountPreferences.syncNow();
|
|
2666
4237
|
}
|
|
2667
4238
|
get buildDate() {
|
|
2668
4239
|
const date = this.packageInfo.buildDate;
|
|
2669
|
-
return date ? new Intl.DateTimeFormat(this.
|
|
4240
|
+
return date ? new Intl.DateTimeFormat(this.i18n.formatLocale(), { dateStyle: 'short', timeStyle: 'short' }).format(new Date(date)) : '';
|
|
2670
4241
|
}
|
|
2671
4242
|
/** Texto pronto pra colar num chamado: app, versão, ambiente, navegador, tela, fuso e página. */
|
|
2672
4243
|
async copySupportInfo() {
|
|
@@ -2704,18 +4275,19 @@ class SettingsPanelComponent {
|
|
|
2704
4275
|
this.settings.close();
|
|
2705
4276
|
}
|
|
2706
4277
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
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 }); }
|
|
4278
|
+
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]=\"activeSection()\"\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 <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\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 <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\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 @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.aiAgent). -->\n @if (sessionsItems.aiAgent) {\n <hm-settings-row [label]=\"featureText.aiAgent\" [description]=\"featureText.aiAgentDescription\" [keywords]=\"featureText.aiAgentKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.aiAgent })\" [text]=\"featureText.aiAgentHelp\" />\n <spine-ai-agent />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\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 class=\"sp-settings__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-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\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 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($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\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\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\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\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 <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\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 class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 <!-- Novidades (s\u00F3 com alguma vigente) e tour: os dois fecham o painel, que ocupa a tela. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\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=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" 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__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.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;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.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: AiAgentComponent, selector: "spine-ai-agent" }, { kind: "component", type: AccountActivityComponent, selector: "spine-account-activity" }, { kind: "component", type: DefaultBadgeComponent, selector: "spine-default-badge" }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { 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"] }, { kind: "component", type: ToggletipComponent, selector: "hm-toggletip", inputs: ["label", "text", "side", "intent", "icon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2708
4279
|
}
|
|
2709
4280
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, decorators: [{
|
|
2710
4281
|
type: Component,
|
|
2711
4282
|
args: [{ selector: 'spine-settings-panel', imports: [
|
|
2712
|
-
NgComponentOutlet, AppTokensComponent,
|
|
4283
|
+
NgComponentOutlet, AppTokensComponent, AiAgentComponent, AccountActivityComponent, DefaultBadgeComponent, CheckboxComponent,
|
|
2713
4284
|
AccountMfaComponent, AccountRecoveryCodesComponent, AccountSessionsComponent, SpineAccountSecurityScopeDirective,
|
|
2714
|
-
AccountAvatarComponent, ButtonComponent, ComboboxComponent,
|
|
4285
|
+
AccountAvatarComponent, ButtonComponent, ComboboxComponent,
|
|
2715
4286
|
SegmentedControlComponent, SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent,
|
|
2716
|
-
SelectComponent, ShortcutListComponent, ThemePickerComponent,
|
|
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"] }]
|
|
2718
|
-
}] });
|
|
4287
|
+
SelectComponent, ShortcutListComponent, ThemePickerComponent, ToggletipComponent,
|
|
4288
|
+
], 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]=\"activeSection()\"\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 <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\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 <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\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 @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.aiAgent). -->\n @if (sessionsItems.aiAgent) {\n <hm-settings-row [label]=\"featureText.aiAgent\" [description]=\"featureText.aiAgentDescription\" [keywords]=\"featureText.aiAgentKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.aiAgent })\" [text]=\"featureText.aiAgentHelp\" />\n <spine-ai-agent />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\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 class=\"sp-settings__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-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\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 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($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\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\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\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\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 <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\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 class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 <!-- Novidades (s\u00F3 com alguma vigente) e tour: os dois fecham o painel, que ocupa a tela. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\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=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" 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__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.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;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.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"] }]
|
|
4289
|
+
}], ctorParameters: () => [] });
|
|
4290
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2719
4291
|
/** "UTC−03:00" para um fuso IANA. */
|
|
2720
4292
|
function offsetOf(timeZone) {
|
|
2721
4293
|
try {
|
|
@@ -3466,7 +5038,7 @@ class SpineDocsComponent {
|
|
|
3466
5038
|
</div>
|
|
3467
5039
|
</hm-modal>
|
|
3468
5040
|
}
|
|
3469
|
-
`, isInline: true, styles: [".layout{display:grid;grid-template-columns:minmax(12rem,16rem) minmax(0,1fr);gap:var(--hm-space-lg);block-size:min(70vh,48rem);min-block-size:0}.index,.reader{min-block-size:0;overflow-y:auto;overscroll-behavior:contain}.index{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding-inline-end:var(--hm-space-md);border-inline-end:var(--hm-border-hairline) solid var(--hm-divider)}.filter{box-sizing:border-box;inline-size:100%;min-block-size:var(--hm-control-height);margin-block-end:var(--hm-space-sm);padding-inline:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm)}.filter::placeholder{color:var(--hm-placeholder)}.filter:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-hairline)}.group-title{margin:var(--hm-space-sm) 0 0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);text-transform:uppercase;letter-spacing:.04em}.entries{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.entry{inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer;transition:background-color var(--hm-duration-quick) var(--hm-ease)}.entry:hover{background:var(--hm-hover)}.entry:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:calc(var(--hm-border-thin) * -1)}.entry--active{background:var(--hm-accent-subtle);color:var(--hm-accent-strong);font-weight:var(--hm-font-weight-semibold)}.reader{display:flex;flex-direction:column;gap:var(--hm-space-md);align-items:stretch}.reader-title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.reading{animation:spine-docs-enter var(--hm-duration-standard) var(--hm-ease-decelerate)}.note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}@keyframes spine-docs-enter{0%{opacity:0}to{opacity:1}}@media(max-width:767px){.layout{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,2fr) minmax(0,3fr)}.index{padding-inline-end:0;padding-block-end:var(--hm-space-md);border-inline-end:0;border-block-end:var(--hm-border-hairline) solid var(--hm-divider)}}@media(prefers-reduced-motion:reduce){.reading{animation:none}.entry{transition:none}}@media(forced-colors:active){.entry--active{border-color:Highlight}}\n"], dependencies: [{ kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: SkeletonComponent, selector: "hm-skeleton", inputs: ["shape", "count", "label"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SpineMarkdownComponent, selector: "spine-markdown", inputs: ["content"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5041
|
+
`, isInline: true, styles: [".layout{display:grid;grid-template-columns:minmax(12rem,16rem) minmax(0,1fr);gap:var(--hm-space-lg);block-size:min(70vh,48rem);min-block-size:0}.index,.reader{min-block-size:0;overflow-y:auto;overscroll-behavior:contain}.index{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding-inline-end:var(--hm-space-md);border-inline-end:var(--hm-border-hairline) solid var(--hm-divider)}.filter{box-sizing:border-box;inline-size:100%;min-block-size:var(--hm-control-height);margin-block-end:var(--hm-space-sm);padding-inline:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm)}.filter::placeholder{color:var(--hm-placeholder)}.filter:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-hairline)}.group-title{margin:var(--hm-space-sm) 0 0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);text-transform:uppercase;letter-spacing:.04em}.entries{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.entry{inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer;transition:background-color var(--hm-duration-quick) var(--hm-ease)}.entry:hover{background:var(--hm-hover)}.entry:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:calc(var(--hm-border-thin) * -1)}.entry--active{background:var(--hm-accent-subtle);color:var(--hm-accent-strong);font-weight:var(--hm-font-weight-semibold)}.reader{display:flex;flex-direction:column;gap:var(--hm-space-md);align-items:stretch}.reader-title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.reading{animation:spine-docs-enter var(--hm-duration-standard) var(--hm-ease-decelerate)}.note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}@keyframes spine-docs-enter{0%{opacity:0}to{opacity:1}}@media(max-width:767px){.layout{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,2fr) minmax(0,3fr)}.index{padding-inline-end:0;padding-block-end:var(--hm-space-md);border-inline-end:0;border-block-end:var(--hm-border-hairline) solid var(--hm-divider)}}@media(prefers-reduced-motion:reduce){.reading{animation:none}.entry{transition:none}}:host-context([data-motion=\"reduce\"]) .reading{animation:none}:host-context([data-motion=\"reduce\"]) .entry{transition:none}@media(forced-colors:active){.entry--active{border-color:Highlight}}\n"], dependencies: [{ kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: SkeletonComponent, selector: "hm-skeleton", inputs: ["shape", "count", "label"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SpineMarkdownComponent, selector: "spine-markdown", inputs: ["content"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3470
5042
|
}
|
|
3471
5043
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineDocsComponent, decorators: [{
|
|
3472
5044
|
type: Component,
|
|
@@ -3535,7 +5107,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3535
5107
|
</div>
|
|
3536
5108
|
</hm-modal>
|
|
3537
5109
|
}
|
|
3538
|
-
`, styles: [".layout{display:grid;grid-template-columns:minmax(12rem,16rem) minmax(0,1fr);gap:var(--hm-space-lg);block-size:min(70vh,48rem);min-block-size:0}.index,.reader{min-block-size:0;overflow-y:auto;overscroll-behavior:contain}.index{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding-inline-end:var(--hm-space-md);border-inline-end:var(--hm-border-hairline) solid var(--hm-divider)}.filter{box-sizing:border-box;inline-size:100%;min-block-size:var(--hm-control-height);margin-block-end:var(--hm-space-sm);padding-inline:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm)}.filter::placeholder{color:var(--hm-placeholder)}.filter:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-hairline)}.group-title{margin:var(--hm-space-sm) 0 0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);text-transform:uppercase;letter-spacing:.04em}.entries{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.entry{inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer;transition:background-color var(--hm-duration-quick) var(--hm-ease)}.entry:hover{background:var(--hm-hover)}.entry:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:calc(var(--hm-border-thin) * -1)}.entry--active{background:var(--hm-accent-subtle);color:var(--hm-accent-strong);font-weight:var(--hm-font-weight-semibold)}.reader{display:flex;flex-direction:column;gap:var(--hm-space-md);align-items:stretch}.reader-title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.reading{animation:spine-docs-enter var(--hm-duration-standard) var(--hm-ease-decelerate)}.note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}@keyframes spine-docs-enter{0%{opacity:0}to{opacity:1}}@media(max-width:767px){.layout{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,2fr) minmax(0,3fr)}.index{padding-inline-end:0;padding-block-end:var(--hm-space-md);border-inline-end:0;border-block-end:var(--hm-border-hairline) solid var(--hm-divider)}}@media(prefers-reduced-motion:reduce){.reading{animation:none}.entry{transition:none}}@media(forced-colors:active){.entry--active{border-color:Highlight}}\n"] }]
|
|
5110
|
+
`, styles: [".layout{display:grid;grid-template-columns:minmax(12rem,16rem) minmax(0,1fr);gap:var(--hm-space-lg);block-size:min(70vh,48rem);min-block-size:0}.index,.reader{min-block-size:0;overflow-y:auto;overscroll-behavior:contain}.index{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding-inline-end:var(--hm-space-md);border-inline-end:var(--hm-border-hairline) solid var(--hm-divider)}.filter{box-sizing:border-box;inline-size:100%;min-block-size:var(--hm-control-height);margin-block-end:var(--hm-space-sm);padding-inline:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm)}.filter::placeholder{color:var(--hm-placeholder)}.filter:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-hairline)}.group-title{margin:var(--hm-space-sm) 0 0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);text-transform:uppercase;letter-spacing:.04em}.entries{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.entry{inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer;transition:background-color var(--hm-duration-quick) var(--hm-ease)}.entry:hover{background:var(--hm-hover)}.entry:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:calc(var(--hm-border-thin) * -1)}.entry--active{background:var(--hm-accent-subtle);color:var(--hm-accent-strong);font-weight:var(--hm-font-weight-semibold)}.reader{display:flex;flex-direction:column;gap:var(--hm-space-md);align-items:stretch}.reader-title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-lg);font-weight:var(--hm-font-weight-semibold)}.reading{animation:spine-docs-enter var(--hm-duration-standard) var(--hm-ease-decelerate)}.note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}@keyframes spine-docs-enter{0%{opacity:0}to{opacity:1}}@media(max-width:767px){.layout{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,2fr) minmax(0,3fr)}.index{padding-inline-end:0;padding-block-end:var(--hm-space-md);border-inline-end:0;border-block-end:var(--hm-border-hairline) solid var(--hm-divider)}}@media(prefers-reduced-motion:reduce){.reading{animation:none}.entry{transition:none}}:host-context([data-motion=\"reduce\"]) .reading{animation:none}:host-context([data-motion=\"reduce\"]) .entry{transition:none}@media(forced-colors:active){.entry--active{border-color:Highlight}}\n"] }]
|
|
3539
5111
|
}] });
|
|
3540
5112
|
|
|
3541
5113
|
/**
|
|
@@ -3611,6 +5183,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3611
5183
|
`, styles: [":host{display:block;flex:none}\n"] }]
|
|
3612
5184
|
}] });
|
|
3613
5185
|
|
|
5186
|
+
/**
|
|
5187
|
+
* Aviso de "Encerrar por inatividade" (`SpineIdleService`): contagem regressiva, "Continuar conectado" (padrão, e o
|
|
5188
|
+
* que fechar pelo X/Esc faz) e "Sair agora". Montado uma vez no `spine-layout`.
|
|
5189
|
+
*/
|
|
5190
|
+
class SpineIdleWarningComponent {
|
|
5191
|
+
constructor() {
|
|
5192
|
+
this.idle = inject(SpineIdleService);
|
|
5193
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
5194
|
+
this.text = this.messages.accountFeatures;
|
|
5195
|
+
this.countdown = computed(() => pluralMessage(this.text.idleWarningText, this.idle.secondsLeft(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "countdown" }] : /* istanbul ignore next */ []));
|
|
5196
|
+
}
|
|
5197
|
+
onOpenChange(open) {
|
|
5198
|
+
if (!open)
|
|
5199
|
+
this.idle.stay();
|
|
5200
|
+
}
|
|
5201
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineIdleWarningComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5202
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineIdleWarningComponent, isStandalone: true, selector: "spine-idle-warning", ngImport: i0, template: `
|
|
5203
|
+
@if (idle.warning()) {
|
|
5204
|
+
<hm-modal [open]="true" [heading]="text.idleWarningHeading" size="small" (openChange)="onOpenChange($event)">
|
|
5205
|
+
<p class="sp-idle__text" role="timer">{{ countdown() }}</p>
|
|
5206
|
+
<div footer class="sp-idle__actions">
|
|
5207
|
+
<hm-button emphasis="medium" icon="logout" (clicked)="idle.logoutNow()">{{ text.idleLogout }}</hm-button>
|
|
5208
|
+
<hm-button emphasis="high" (clicked)="idle.stay()">{{ text.idleStay }}</hm-button>
|
|
5209
|
+
</div>
|
|
5210
|
+
</hm-modal>
|
|
5211
|
+
}
|
|
5212
|
+
`, isInline: true, styles: [".sp-idle__text{margin:0}.sp-idle__actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-2)}\n"], dependencies: [{ 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: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5213
|
+
}
|
|
5214
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineIdleWarningComponent, decorators: [{
|
|
5215
|
+
type: Component,
|
|
5216
|
+
args: [{ selector: 'spine-idle-warning', imports: [ButtonComponent, ModalComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
5217
|
+
@if (idle.warning()) {
|
|
5218
|
+
<hm-modal [open]="true" [heading]="text.idleWarningHeading" size="small" (openChange)="onOpenChange($event)">
|
|
5219
|
+
<p class="sp-idle__text" role="timer">{{ countdown() }}</p>
|
|
5220
|
+
<div footer class="sp-idle__actions">
|
|
5221
|
+
<hm-button emphasis="medium" icon="logout" (clicked)="idle.logoutNow()">{{ text.idleLogout }}</hm-button>
|
|
5222
|
+
<hm-button emphasis="high" (clicked)="idle.stay()">{{ text.idleStay }}</hm-button>
|
|
5223
|
+
</div>
|
|
5224
|
+
</hm-modal>
|
|
5225
|
+
}
|
|
5226
|
+
`, styles: [".sp-idle__text{margin:0}.sp-idle__actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-2)}\n"] }]
|
|
5227
|
+
}] });
|
|
5228
|
+
|
|
3614
5229
|
/**
|
|
3615
5230
|
* Shell do spine — MESMA estrutura do `docs/website`'s `app.component.html`/`.scss` (decisão de
|
|
3616
5231
|
* 2026-09-28, ver `hefesto-migration-plan.md`): `.shell` com altura de viewport de verdade
|
|
@@ -3620,7 +5235,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3620
5235
|
* (`hm-sidebar` fixa + `.main` com `hm-workspace`). **Sem barra de breadcrumb própria** — isso
|
|
3621
5236
|
* era resíduo do spine antigo (`spine-navigation`), removido: o docs não tem uma, a navegação
|
|
3622
5237
|
* inteira mora na sidebar. Entre o header e o corpo fica o `spine-connection-banner` (aviso de conexão
|
|
3623
|
-
* do `@orfeu/angular`, vazio quando `online`).
|
|
5238
|
+
* do `@orfeu/angular`, vazio quando `online`). Os avisos ficam no `spine-notification` (`NotificationService`) e
|
|
5239
|
+
* no `hm-toast-stack` (`HermesToastService`), os dois montados aqui: o app não monta nenhum.
|
|
3624
5240
|
*
|
|
3625
5241
|
* O `<router-outlet>` que sobra aqui é só pra `/no-permission` (a única rota do shell com
|
|
3626
5242
|
* `component` de verdade — o resto da árvore do backend nunca passa pelo `Router` pra renderizar,
|
|
@@ -3630,10 +5246,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3630
5246
|
* corressem na mesma volta de detecção de mudanças.
|
|
3631
5247
|
*/
|
|
3632
5248
|
class LayoutComponent {
|
|
3633
|
-
constructor(router, theme,
|
|
5249
|
+
constructor(router, theme, shortcuts, onboarding, news) {
|
|
3634
5250
|
this.router = router;
|
|
3635
5251
|
this.theme = theme;
|
|
3636
|
-
this.auth = auth;
|
|
3637
5252
|
this.shortcuts = shortcuts;
|
|
3638
5253
|
this.onboarding = onboarding;
|
|
3639
5254
|
this.news = news;
|
|
@@ -3641,7 +5256,12 @@ class LayoutComponent {
|
|
|
3641
5256
|
this.preferences = inject(SpinePreferencesService);
|
|
3642
5257
|
this.workspace = inject(SpineWorkspaceNavService);
|
|
3643
5258
|
this.settings = inject(SpineSettingsService);
|
|
3644
|
-
|
|
5259
|
+
/** Também liga, uma vez, o aviso de falha do logout (`logoutFailed$` do `@orfeu/angular`). */
|
|
5260
|
+
this.logoutService = inject(SpineLogoutService);
|
|
5261
|
+
/** Recursos opcionais (`SpineConfig.features`): cada serviço não faz nada com o seu recurso desligado. */
|
|
5262
|
+
this.idle = inject(SpineIdleService);
|
|
5263
|
+
this.startPage = inject(SpineStartPageService);
|
|
5264
|
+
this.accountPreferences = inject(SpineAccountPreferencesService);
|
|
3645
5265
|
this.isNoPermission = this.matchesNoPermission(this.router.url);
|
|
3646
5266
|
this.router.events.pipe(filter((e) => e instanceof NavigationEnd))
|
|
3647
5267
|
.subscribe(e => { this.isNoPermission = this.matchesNoPermission(e.urlAfterRedirects); });
|
|
@@ -3650,15 +5270,16 @@ class LayoutComponent {
|
|
|
3650
5270
|
return url.split(/[?#]/)[0] === '/no-permission';
|
|
3651
5271
|
}
|
|
3652
5272
|
ngOnInit() {
|
|
5273
|
+
// Preferências na conta (lê e aplica uma vez por sessão do app), inatividade e a "última tela visitada".
|
|
5274
|
+
void this.accountPreferences.start();
|
|
5275
|
+
this.idle.start();
|
|
5276
|
+
this.startPage.track();
|
|
3653
5277
|
// Liga os atalhos de teclado globais às ações reais do sistema.
|
|
3654
5278
|
this.shortcuts.setActions({
|
|
3655
5279
|
navigate: (url) => this.router.navigateByUrl(url),
|
|
3656
5280
|
toggleTheme: () => this.theme.setTheme(this.theme.current === 'light' ? 'dark' : 'light'),
|
|
3657
5281
|
toggleFullscreen: () => this.toggleFullscreen(),
|
|
3658
|
-
logout: () =>
|
|
3659
|
-
this.userStorage.clearHistory();
|
|
3660
|
-
this.auth.logout();
|
|
3661
|
-
},
|
|
5282
|
+
logout: () => void this.logoutService.logout(),
|
|
3662
5283
|
openSettings: () => this.settings.toggle(),
|
|
3663
5284
|
});
|
|
3664
5285
|
// Link direto pra uma seção do painel de configurações: `?settings=aparencia` (ou só
|
|
@@ -3688,7 +5309,7 @@ class LayoutComponent {
|
|
|
3688
5309
|
document.exitFullscreen?.();
|
|
3689
5310
|
}
|
|
3690
5311
|
}
|
|
3691
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LayoutComponent, deps: [{ token: i3.Router }, { token: i2$1.ThemeService }, { token:
|
|
5312
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LayoutComponent, deps: [{ token: i3.Router }, { token: i2$1.ThemeService }, { token: ShortcutService }, { token: OnboardingService }, { token: SpineNewsService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3692
5313
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: LayoutComponent, isStandalone: true, selector: "spine-layout", ngImport: i0, template: `
|
|
3693
5314
|
<div class="shell">
|
|
3694
5315
|
<spine-header></spine-header>
|
|
@@ -3717,6 +5338,9 @@ class LayoutComponent {
|
|
|
3717
5338
|
</div>
|
|
3718
5339
|
</div>
|
|
3719
5340
|
<spine-notification></spine-notification>
|
|
5341
|
+
<!-- Avisos do HermesToastService (configurações, conta e segurança, auto-serviço…). Se o app também montar
|
|
5342
|
+
uma, só uma desenha (o serviço escolhe a pilha ativa): nunca em dobro. -->
|
|
5343
|
+
<hm-toast-stack />
|
|
3720
5344
|
<spine-side-panel />
|
|
3721
5345
|
<spine-modal />
|
|
3722
5346
|
<spine-shortcuts-cheatsheet></spine-shortcuts-cheatsheet>
|
|
@@ -3724,7 +5348,9 @@ class LayoutComponent {
|
|
|
3724
5348
|
<spine-docs />
|
|
3725
5349
|
<spine-news />
|
|
3726
5350
|
<spine-onboarding-overlay></spine-onboarding-overlay>
|
|
3727
|
-
|
|
5351
|
+
<!-- Aviso de "Encerrar por inatividade" (SpineFeatures.idleTimeout): só desenha quando abre. -->
|
|
5352
|
+
<spine-idle-warning />
|
|
5353
|
+
`, isInline: true, styles: [":host{display:block}.shell{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--hm-surface);color:var(--hm-text)}.body{position:relative;display:flex;flex:1;min-height:0;overflow:hidden}.main{display:flex;flex:1;min-width:0;flex-direction:column}.workspace-area{flex:1;min-height:0;display:flex;flex-direction:column;--hm-tab-strip-bg: var(--hm-surface);--hm-tab-active-bg: var(--hm-surface-raised)}hm-workspace{flex:1;min-height:0}.panel-content{display:flex;flex-direction:column;height:100%;min-height:0}\n"], dependencies: [{ kind: "component", type: SpineNewsComponent, selector: "spine-news" }, { kind: "component", type: SpineDocsComponent, selector: "spine-docs" }, { kind: "component", type: HeaderComponent, selector: "spine-header" }, { kind: "component", type: SidebarComponent, selector: "spine-sidebar" }, { kind: "component", type: SpineConnectionBannerComponent, selector: "spine-connection-banner" }, { kind: "component", type: SpinePagePanelComponent, selector: "spine-page-panel", inputs: ["url", "isFocusedPanel"] }, { kind: "directive", type: PanelLocalLinkDirective, selector: "[appPanelLocalLink]", inputs: ["appPanelLocalLink"], outputs: ["localNavigate"] }, { kind: "component", type: WorkspaceComponent, selector: "hm-workspace", inputs: ["initialTabs", "initialActiveId", "keepAlive", "label", "providedStore"], outputs: ["stateChange", "newTabRequested"] }, { kind: "component", type: ToastStackComponent, selector: "hm-toast-stack" }, { kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: ModalComponent$1, selector: "spine-modal" }, { kind: "component", type: NotificationComponent, selector: "spine-notification" }, { kind: "component", type: SidePanelComponent, selector: "spine-side-panel" }, { kind: "component", type: OnboardingOverlayComponent, selector: "spine-onboarding-overlay" }, { kind: "component", type: ShortcutsCheatsheetComponent, selector: "spine-shortcuts-cheatsheet" }, { kind: "component", type: SettingsPanelComponent, selector: "spine-settings-panel" }, { kind: "component", type: SpineIdleWarningComponent, selector: "spine-idle-warning" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3728
5354
|
}
|
|
3729
5355
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LayoutComponent, decorators: [{
|
|
3730
5356
|
type: Component,
|
|
@@ -3737,6 +5363,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3737
5363
|
SpinePagePanelComponent,
|
|
3738
5364
|
PanelLocalLinkDirective,
|
|
3739
5365
|
WorkspaceComponent,
|
|
5366
|
+
ToastStackComponent,
|
|
3740
5367
|
RouterOutlet,
|
|
3741
5368
|
ModalComponent$1,
|
|
3742
5369
|
NotificationComponent,
|
|
@@ -3744,6 +5371,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3744
5371
|
OnboardingOverlayComponent,
|
|
3745
5372
|
ShortcutsCheatsheetComponent,
|
|
3746
5373
|
SettingsPanelComponent,
|
|
5374
|
+
SpineIdleWarningComponent,
|
|
3747
5375
|
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
3748
5376
|
<div class="shell">
|
|
3749
5377
|
<spine-header></spine-header>
|
|
@@ -3772,6 +5400,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3772
5400
|
</div>
|
|
3773
5401
|
</div>
|
|
3774
5402
|
<spine-notification></spine-notification>
|
|
5403
|
+
<!-- Avisos do HermesToastService (configurações, conta e segurança, auto-serviço…). Se o app também montar
|
|
5404
|
+
uma, só uma desenha (o serviço escolhe a pilha ativa): nunca em dobro. -->
|
|
5405
|
+
<hm-toast-stack />
|
|
3775
5406
|
<spine-side-panel />
|
|
3776
5407
|
<spine-modal />
|
|
3777
5408
|
<spine-shortcuts-cheatsheet></spine-shortcuts-cheatsheet>
|
|
@@ -3779,12 +5410,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3779
5410
|
<spine-docs />
|
|
3780
5411
|
<spine-news />
|
|
3781
5412
|
<spine-onboarding-overlay></spine-onboarding-overlay>
|
|
5413
|
+
<!-- Aviso de "Encerrar por inatividade" (SpineFeatures.idleTimeout): só desenha quando abre. -->
|
|
5414
|
+
<spine-idle-warning />
|
|
3782
5415
|
`, styles: [":host{display:block}.shell{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--hm-surface);color:var(--hm-text)}.body{position:relative;display:flex;flex:1;min-height:0;overflow:hidden}.main{display:flex;flex:1;min-width:0;flex-direction:column}.workspace-area{flex:1;min-height:0;display:flex;flex-direction:column;--hm-tab-strip-bg: var(--hm-surface);--hm-tab-active-bg: var(--hm-surface-raised)}hm-workspace{flex:1;min-height:0}.panel-content{display:flex;flex-direction:column;height:100%;min-height:0}\n"] }]
|
|
3783
|
-
}], ctorParameters: () => [{ type: i3.Router }, { type: i2$1.ThemeService }, { type:
|
|
5416
|
+
}], ctorParameters: () => [{ type: i3.Router }, { type: i2$1.ThemeService }, { type: ShortcutService }, { type: OnboardingService }, { type: SpineNewsService }] });
|
|
3784
5417
|
|
|
3785
5418
|
/**
|
|
3786
5419
|
* Generated bundle index. Do not edit.
|
|
3787
5420
|
*/
|
|
3788
5421
|
|
|
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 };
|
|
5422
|
+
export { AiAgentComponent, AppLauncherComponent, HeaderComponent, LayoutComponent, OnboardingOverlayComponent, OnboardingService, SPINE_BRAND, SPINE_IDLE_WARNING_SECONDS, SPINE_MARKDOWN_PURIFY_CONFIG, SPINE_PREFERENCES_SAVE_DELAY_MS, SPINE_SETTINGS_ALIASES, SPINE_SETTINGS_KEYS, SPINE_SETTINGS_SECTIONS, SettingsPanelComponent, ShortcutService, ShortcutsCheatsheetComponent, SidebarComponent, SpineAccountPreferencesService, SpineAccountSecurityService, SpineAppTokensService, SpineBrandComponent, SpineConnectionBannerComponent, SpineDocsComponent, SpineDocsService, SpineIdleService, SpineIdleWarningComponent, SpineLogoutService, SpineMarkdownComponent, SpineNewsComponent, SpineNewsService, SpineNoticesService, SpineSettingsService, SpineStartPageService, accountErrorCode, argosAccountApi, buildMcpConfig, effectiveIdleMinutes, findTourTarget, idleLimits, isInternalPath, provideSettingsSection, resolveSettingsSection, sanitizeSpineMarkdownHtml, stable };
|
|
3790
5423
|
//# sourceMappingURL=trismegisto-spine-layout.mjs.map
|