@trismegisto/spine 2.1.2 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -17
- 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-dKM4fG42.mjs +89 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-dKM4fG42.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-VMrCG5o3.mjs +91 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-VMrCG5o3.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-BNtNWfiu.mjs +37 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-CCiz-q8o.mjs.map → trismegisto-spine-auto-service-auto-service-audit.component-BNtNWfiu.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-DdtDk_xl.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-DdtDk_xl.mjs.map} +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-C7SPavrX.mjs → trismegisto-spine-auto-service-auto-service-client.component-DXl1y6Sx.mjs} +123 -27
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-DXl1y6Sx.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-B1tXgMvi.mjs +88 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-B1tXgMvi.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-C4iQ2GPX.mjs +599 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-C4iQ2GPX.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-login-attempts.component-B87vibNB.mjs +37 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-login-attempts.component-B87vibNB.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DV88Uger.mjs +941 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DV88Uger.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-D05838rU.mjs +69 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-D05838rU.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-ChGMgYU3.mjs +80 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-ChGMgYU3.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 +1698 -156
- package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-shell.mjs +17 -1
- package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
- package/fesm2022/trismegisto-spine.mjs +1175 -85
- 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 +833 -13
- package/types/trismegisto-spine-layout.d.ts.map +1 -1
- package/types/trismegisto-spine-shell.d.ts +8 -0
- package/types/trismegisto-spine-shell.d.ts.map +1 -1
- package/types/trismegisto-spine.d.ts +506 -32
- 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-audit.component-CCiz-q8o.mjs +0 -37
- 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, DestroyRef, 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
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
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';
|
|
@@ -559,12 +559,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
559
559
|
/** Seções que o próprio spine já traz. `key` é também o valor de `?settings=` na URL. */
|
|
560
560
|
const SPINE_SETTINGS_KEYS = {
|
|
561
561
|
appearance: 'aparencia',
|
|
562
|
+
/** Tamanho do texto e animações. */
|
|
563
|
+
accessibility: 'acessibilidade',
|
|
562
564
|
region: 'regiao',
|
|
563
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',
|
|
564
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`. */
|
|
565
579
|
security: 'seguranca',
|
|
566
580
|
about: 'sobre',
|
|
567
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
|
+
}
|
|
568
591
|
const SPINE_SETTINGS_SECTIONS = new InjectionToken('SPINE_SETTINGS_SECTIONS');
|
|
569
592
|
/** Registra uma seção do app no painel de configurações (aparece depois das do spine, antes de
|
|
570
593
|
* "Sobre"). Pode ser chamado várias vezes, uma por seção. */
|
|
@@ -578,8 +601,9 @@ class SpineSettingsService {
|
|
|
578
601
|
/** Seção pedida na abertura (vazio = a primeira). */
|
|
579
602
|
this.section = signal('', ...(ngDevMode ? [{ debugName: "section" }] : /* istanbul ignore next */ []));
|
|
580
603
|
}
|
|
604
|
+
/** `section` aceita também as chaves antigas (`seguranca` abre "MFA"). */
|
|
581
605
|
open(section = '') {
|
|
582
|
-
this.section.set(section);
|
|
606
|
+
this.section.set(resolveSettingsSection(section));
|
|
583
607
|
this.isOpen.set(true);
|
|
584
608
|
}
|
|
585
609
|
close() {
|
|
@@ -791,6 +815,10 @@ class HeaderComponent {
|
|
|
791
815
|
/** Verdadeiro quando o painel está exibindo o histórico (campo vazio). */
|
|
792
816
|
this.showingRecent = false;
|
|
793
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);
|
|
794
822
|
}
|
|
795
823
|
async ngOnInit() {
|
|
796
824
|
this.updateView(this.viewService.currentView);
|
|
@@ -1373,7 +1401,8 @@ class OnboardingOverlayComponent {
|
|
|
1373
1401
|
const rect = target.getBoundingClientRect();
|
|
1374
1402
|
if (rect.top >= 0 && rect.bottom <= window.innerHeight)
|
|
1375
1403
|
return;
|
|
1376
|
-
|
|
1404
|
+
// Escolha do app (Configurações › Acessibilidade) ou, sem ela, o sistema.
|
|
1405
|
+
const calm = prefersReducedMotion();
|
|
1377
1406
|
target.scrollIntoView({ block: 'center', inline: 'nearest', behavior: calm ? 'auto' : 'smooth' });
|
|
1378
1407
|
}
|
|
1379
1408
|
/** Largura que o CSS dá ao balão (medida quando já existe) e a altura informada. */
|
|
@@ -1480,10 +1509,14 @@ function buildNewsIllustration(illustration, palette) {
|
|
|
1480
1509
|
const y = round(CY + 104 * Math.sin(angle));
|
|
1481
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>`);
|
|
1482
1511
|
});
|
|
1483
|
-
|
|
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'
|
|
1484
1516
|
? '<style>.sat{animation:drift 5.6s ease-in-out infinite alternate}' +
|
|
1485
1517
|
'@keyframes drift{from{transform:translateY(-5px)}to{transform:translateY(5px)}}' +
|
|
1486
|
-
'@media (prefers-reduced-motion:reduce){.sat{animation:none}}
|
|
1518
|
+
(motion === 'full' ? '' : '@media (prefers-reduced-motion:reduce){.sat{animation:none}}') +
|
|
1519
|
+
'</style>'
|
|
1487
1520
|
: '';
|
|
1488
1521
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${WIDTH} ${HEIGHT}" width="${WIDTH}" height="${HEIGHT}" ` +
|
|
1489
1522
|
`font-family="${xml(palette.font)}">${style}${parts.join('')}</svg>`;
|
|
@@ -1649,6 +1682,32 @@ class SpineAccountSecurityService {
|
|
|
1649
1682
|
endOtherSessions() {
|
|
1650
1683
|
return this.send('DELETE', '/sessions/others');
|
|
1651
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
|
+
}
|
|
1652
1711
|
async send(method, path, body) {
|
|
1653
1712
|
try {
|
|
1654
1713
|
return await this.request(method, path, body);
|
|
@@ -1681,6 +1740,778 @@ function accountErrorCode(err) {
|
|
|
1681
1740
|
return typeof code === 'string' ? code : err.status === 0 ? 'network' : '';
|
|
1682
1741
|
}
|
|
1683
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
|
+
|
|
1684
2515
|
/** Métodos que o painel trata (o Argos só tem fluxo para estes; o SMS aparece como indisponível). */
|
|
1685
2516
|
const SUPPORTED = ['totp', 'email', 'sms'];
|
|
1686
2517
|
const nameOf = (m) => m?.method_name?.toLowerCase() ?? '';
|
|
@@ -1688,7 +2519,7 @@ const isTotp = (m) => nameOf(m) === 'totp';
|
|
|
1688
2519
|
const isEmail = (m) => nameOf(m) === 'email';
|
|
1689
2520
|
const isSms = (m) => nameOf(m) === 'sms';
|
|
1690
2521
|
/**
|
|
1691
|
-
* 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
|
|
1692
2523
|
* alteração (reautenticação → cadastro/remoção/códigos). Mesmo roteiro da tela "Segurança da conta" do
|
|
1693
2524
|
* Argos: toda alteração pede antes a prova de identidade recente (senha ou um fator ativo, 5 minutos no
|
|
1694
2525
|
* servidor); a recusa `reauthentication_required` no meio volta para a prova e a ação segue depois dela.
|
|
@@ -1904,7 +2735,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1904
2735
|
type: Injectable
|
|
1905
2736
|
}], ctorParameters: () => [] });
|
|
1906
2737
|
/**
|
|
1907
|
-
* 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
|
|
1908
2739
|
* (métodos e códigos de recuperação) dividirem o mesmo estado e ele morrer junto com o painel.
|
|
1909
2740
|
*/
|
|
1910
2741
|
class SpineAccountSecurityScopeDirective {
|
|
@@ -1919,7 +2750,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1919
2750
|
}]
|
|
1920
2751
|
}] });
|
|
1921
2752
|
|
|
1922
|
-
let sequence$
|
|
2753
|
+
let sequence$4 = 0;
|
|
1923
2754
|
/**
|
|
1924
2755
|
* Prova de identidade antes de uma alteração na conta (senha, código do autenticador, código enviado ao
|
|
1925
2756
|
* e-mail cadastrado ou código de recuperação). Vale 5 minutos no Argos, presa à sessão do login; depois
|
|
@@ -1929,8 +2760,9 @@ class AccountReauthComponent {
|
|
|
1929
2760
|
constructor() {
|
|
1930
2761
|
this.state = inject(SpineAccountSecurityState);
|
|
1931
2762
|
this.t = inject(SPINE_MESSAGES).accountSecurity;
|
|
1932
|
-
this.titleId = `sp-account-reauth-${++sequence$
|
|
2763
|
+
this.titleId = `sp-account-reauth-${++sequence$4}`;
|
|
1933
2764
|
this.codeLabelId = `${this.titleId}-code`;
|
|
2765
|
+
this.hintId = `${this.titleId}-hint`;
|
|
1934
2766
|
this.password = new FormControl('', { nonNullable: true, validators: [Validators.required] });
|
|
1935
2767
|
this.recoveryCode = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.minLength(10)] });
|
|
1936
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. */
|
|
@@ -1977,9 +2809,14 @@ class AccountReauthComponent {
|
|
|
1977
2809
|
}
|
|
1978
2810
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountReauthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1979
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: `
|
|
1980
|
-
<section class="sp-account__panel" [attr.aria-labelledby]="titleId">
|
|
1981
|
-
<
|
|
1982
|
-
|
|
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>
|
|
1983
2820
|
|
|
1984
2821
|
@if (choices().length > 1) {
|
|
1985
2822
|
<hm-radio layout="inline" [label]="t.reauthHow" [options]="choices()" [value]="state.reauthMode()" (valueChange)="choose($event)" />
|
|
@@ -1991,7 +2828,7 @@ class AccountReauthComponent {
|
|
|
1991
2828
|
<hm-input type="password" autocomplete="current-password" [label]="t.currentPassword" [formControl]="password" [messages]="{ required: t.passwordRequired }" />
|
|
1992
2829
|
<div class="sp-account__actions">
|
|
1993
2830
|
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
1994
|
-
<hm-button emphasis="
|
|
2831
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
1995
2832
|
</div>
|
|
1996
2833
|
</form>
|
|
1997
2834
|
}
|
|
@@ -2001,14 +2838,14 @@ class AccountReauthComponent {
|
|
|
2001
2838
|
[messages]="{ required: t.recoveryCodeRequired, minlength: t.recoveryCodeLength }" />
|
|
2002
2839
|
<div class="sp-account__actions">
|
|
2003
2840
|
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
2004
|
-
<hm-button emphasis="
|
|
2841
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
2005
2842
|
</div>
|
|
2006
2843
|
</form>
|
|
2007
2844
|
}
|
|
2008
2845
|
@case ('email-request') {
|
|
2009
2846
|
<div class="sp-account__actions">
|
|
2010
2847
|
<hm-button emphasis="high" icon="mail" [loading]="state.busy()" (clicked)="state.sendReauthCode()">{{ t.sendEmailCode }}</hm-button>
|
|
2011
|
-
<hm-button emphasis="
|
|
2848
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
2012
2849
|
</div>
|
|
2013
2850
|
}
|
|
2014
2851
|
@default {
|
|
@@ -2017,19 +2854,24 @@ class AccountReauthComponent {
|
|
|
2017
2854
|
<hm-code-input (completed)="submitCode($event)" />
|
|
2018
2855
|
</div>
|
|
2019
2856
|
<div class="sp-account__actions">
|
|
2020
|
-
<hm-button emphasis="
|
|
2857
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
2021
2858
|
</div>
|
|
2022
2859
|
}
|
|
2023
2860
|
}
|
|
2024
2861
|
</section>
|
|
2025
|
-
`, 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 }); }
|
|
2026
2863
|
}
|
|
2027
2864
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountReauthComponent, decorators: [{
|
|
2028
2865
|
type: Component,
|
|
2029
|
-
args: [{ selector: 'spine-account-reauth', imports: [ReactiveFormsModule, ButtonComponent, CodeInputComponent, InputComponent, RadioComponent], template: `
|
|
2030
|
-
<section class="sp-account__panel" [attr.aria-labelledby]="titleId">
|
|
2031
|
-
<
|
|
2032
|
-
|
|
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>
|
|
2033
2875
|
|
|
2034
2876
|
@if (choices().length > 1) {
|
|
2035
2877
|
<hm-radio layout="inline" [label]="t.reauthHow" [options]="choices()" [value]="state.reauthMode()" (valueChange)="choose($event)" />
|
|
@@ -2041,7 +2883,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
2041
2883
|
<hm-input type="password" autocomplete="current-password" [label]="t.currentPassword" [formControl]="password" [messages]="{ required: t.passwordRequired }" />
|
|
2042
2884
|
<div class="sp-account__actions">
|
|
2043
2885
|
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
2044
|
-
<hm-button emphasis="
|
|
2886
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
2045
2887
|
</div>
|
|
2046
2888
|
</form>
|
|
2047
2889
|
}
|
|
@@ -2051,14 +2893,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
2051
2893
|
[messages]="{ required: t.recoveryCodeRequired, minlength: t.recoveryCodeLength }" />
|
|
2052
2894
|
<div class="sp-account__actions">
|
|
2053
2895
|
<hm-button type="submit" emphasis="high" [loading]="state.busy()">{{ t.confirm }}</hm-button>
|
|
2054
|
-
<hm-button emphasis="
|
|
2896
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
2055
2897
|
</div>
|
|
2056
2898
|
</form>
|
|
2057
2899
|
}
|
|
2058
2900
|
@case ('email-request') {
|
|
2059
2901
|
<div class="sp-account__actions">
|
|
2060
2902
|
<hm-button emphasis="high" icon="mail" [loading]="state.busy()" (clicked)="state.sendReauthCode()">{{ t.sendEmailCode }}</hm-button>
|
|
2061
|
-
<hm-button emphasis="
|
|
2903
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
2062
2904
|
</div>
|
|
2063
2905
|
}
|
|
2064
2906
|
@default {
|
|
@@ -2067,19 +2909,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
2067
2909
|
<hm-code-input (completed)="submitCode($event)" />
|
|
2068
2910
|
</div>
|
|
2069
2911
|
<div class="sp-account__actions">
|
|
2070
|
-
<hm-button emphasis="
|
|
2912
|
+
<hm-button emphasis="medium" [disabled]="state.busy()" (clicked)="state.cancel()">{{ t.cancel }}</hm-button>
|
|
2071
2913
|
</div>
|
|
2072
2914
|
}
|
|
2073
2915
|
}
|
|
2074
2916
|
</section>
|
|
2075
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-
|
|
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"] }]
|
|
2076
2918
|
}] });
|
|
2077
2919
|
|
|
2078
|
-
let sequence$
|
|
2920
|
+
let sequence$3 = 0;
|
|
2079
2921
|
/**
|
|
2080
|
-
* Verificação em duas etapas da conta no Argos, dentro do painel de configurações: os métodos (ativo
|
|
2081
|
-
*
|
|
2082
|
-
* com código no endereço informado), trocar o aparelho do autenticador e remover. Cada ação passa antes pela
|
|
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
|
|
2083
2925
|
* reautenticação (`spine-account-reauth`).
|
|
2084
2926
|
*/
|
|
2085
2927
|
class AccountMfaComponent {
|
|
@@ -2089,8 +2931,10 @@ class AccountMfaComponent {
|
|
|
2089
2931
|
this.fmt = formatMessage;
|
|
2090
2932
|
this.isTotp = isTotp;
|
|
2091
2933
|
this.isSms = isSms;
|
|
2092
|
-
this.ids = `sp-account-mfa-${++sequence$
|
|
2934
|
+
this.ids = `sp-account-mfa-${++sequence$3}`;
|
|
2093
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 */ []));
|
|
2094
2938
|
}
|
|
2095
2939
|
label(method) {
|
|
2096
2940
|
if (!method)
|
|
@@ -2107,8 +2951,6 @@ class AccountMfaComponent {
|
|
|
2107
2951
|
return isTotp(method) ? 'authenticator' : isEmail(method) ? 'mail' : 'sms';
|
|
2108
2952
|
}
|
|
2109
2953
|
status(method) {
|
|
2110
|
-
if (isSms(method))
|
|
2111
|
-
return this.t.unavailable;
|
|
2112
2954
|
if (!method.active)
|
|
2113
2955
|
return this.t.notRegistered;
|
|
2114
2956
|
return method.secret ? formatMessage(this.t.activeWithDetail, { detail: method.secret }) : this.t.active;
|
|
@@ -2132,26 +2974,427 @@ class AccountMfaComponent {
|
|
|
2132
2974
|
void this.state.sendEnrollEmail(this.email.value.trim());
|
|
2133
2975
|
}
|
|
2134
2976
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountMfaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2135
|
-
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 }); }
|
|
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 }); }
|
|
2136
2978
|
}
|
|
2137
2979
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountMfaComponent, decorators: [{
|
|
2138
2980
|
type: Component,
|
|
2139
|
-
args: [{ selector: 'spine-account-mfa', imports: [ReactiveFormsModule, AlertComponent, ButtonComponent, CodeInputComponent, IconComponent, InputComponent, AccountReauthComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (state.loading() && !state.overview()) {\n <p class=\"sp-account__hint\" role=\"status\">{{ t.loading }}</p>\n} @else if (state.loadError()) {\n <hm-alert variant=\"error\">{{ errorText(state.loadError()) }}</hm-alert>\n @if (state.loadError() !== 'login_required' && state.loadError() !== 'invalid_token') {\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" (clicked)=\"state.load()\">{{ t.retry }}</hm-button>\n </div>\n }\n} @else if (state.overview(); as data) {\n @if (state.owner() === 'methods') {\n @if (state.notice(); as notice) {\n <hm-alert variant=\"success\">{{ t.notices[notice] }}</hm-alert>\n }\n @if (state.error(); as error) {\n <hm-alert variant=\"error\">{{ errorText(error) }}</hm-alert>\n }\n }\n\n @if (state.owner() === 'methods' && state.step() !== 'idle') {\n @switch (state.step()) {\n @case ('reauth') {\n <spine-account-reauth />\n }\n\n @case ('confirm-remove') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-remove'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-remove'\">{{ fmt(t.removeTitle, { method: label(actionMethod()) }) }}</h4>\n <p class=\"sp-account__hint\">\n {{ t.removeHint }}\n @if (state.activeMethods().length <= 1) { {{ t.removeLastHint }} }\n </p>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"high\" intent=\"danger\" [loading]=\"state.busy()\" (clicked)=\"state.confirmRemove()\">{{ t.remove }}</hm-button>\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n\n @case ('enroll-totp') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-totp'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-totp'\">{{ t.totpTitle }}</h4>\n @if (state.totpSetup(); as setup) {\n @if (setup.qrcode) {\n <div class=\"sp-account__qr\"><img [src]=\"setup.qrcode\" [alt]=\"t.qrAlt\" /></div>\n }\n <p class=\"sp-account__hint\">{{ t.manualKey }} <code class=\"sp-account__secret\">{{ setup.secret }}</code></p>\n }\n <div class=\"sp-account__code\" role=\"group\" [attr.aria-labelledby]=\"ids + '-totp-code'\">\n <span class=\"sp-account__label\" [id]=\"ids + '-totp-code'\">{{ t.appCode }}</span>\n <hm-code-input (completed)=\"state.confirmEnroll($event)\" />\n </div>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n\n @case ('enroll-email') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-email'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email'\">{{ t.emailTitle }}</h4>\n <form class=\"sp-account__form\" (submit)=\"$event.preventDefault(); sendEmail()\">\n <hm-input type=\"email\" autocomplete=\"email\" [label]=\"t.email\" [placeholder]=\"t.emailPlaceholder\" [formControl]=\"email\"\n [messages]=\"{ required: t.emailRequired, email: t.emailInvalid }\" />\n <div class=\"sp-account__actions\">\n <hm-button type=\"submit\" emphasis=\"high\" icon=\"mail\" [loading]=\"state.busy()\">{{ t.sendCode }}</hm-button>\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </form>\n </section>\n }\n\n @case ('enroll-email-code') {\n <section class=\"sp-account__panel\" [attr.aria-labelledby]=\"ids + '-email-code'\">\n <h4 class=\"sp-account__title\" [id]=\"ids + '-email-code'\">{{ t.emailCodeTitle }}</h4>\n <p class=\"sp-account__hint\" [id]=\"ids + '-email-code-hint'\">{{ fmt(t.emailCodeHint, { email: state.enrollEmail() }) }}</p>\n <div class=\"sp-account__code\" role=\"group\" [attr.aria-labelledby]=\"ids + '-email-code-label'\" [attr.aria-describedby]=\"ids + '-email-code-hint'\">\n <span class=\"sp-account__label\" [id]=\"ids + '-email-code-label'\">{{ t.sixDigitCode }}</span>\n <hm-code-input (completed)=\"state.confirmEnroll($event)\" />\n </div>\n <div class=\"sp-account__actions\">\n <hm-button emphasis=\"minimal\" [disabled]=\"state.busy()\" (clicked)=\"state.cancel()\">{{ t.cancel }}</hm-button>\n </div>\n </section>\n }\n }\n } @else {\n @if (data.policy.required) {\n <p class=\"sp-account__hint\">{{ t.requiredHint }}</p>\n }\n <ul class=\"sp-account__list\">\n @for (m of state.methods(); track m.method_id) {\n <li class=\"sp-account__item\">\n <span class=\"sp-account__icon\"><hm-icon [shape]=\"icon(m)\" /></span>\n <span class=\"sp-account__item-main\">\n <strong>{{ label(m) }}</strong>\n <span class=\"sp-account__meta\">{{ status(m) }}</span>\n </span>\n @if (!isSms(m)) {\n <span class=\"sp-account__item-actions\">\n @if (!m.active) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"plus\" [disabled]=\"state.busy() || state.step() !== 'idle'\" (clicked)=\"startAdd(m)\">{{ t.add }}</hm-button>\n } @else {\n @if (isTotp(m)) {\n <hm-button size=\"small\" emphasis=\"medium\" [disabled]=\"state.busy() || state.step() !== 'idle'\" (clicked)=\"state.start({ kind: 'replace', method: m })\">{{ t.replaceDevice }}</hm-button>\n }\n <hm-button size=\"small\" emphasis=\"minimal\" intent=\"danger\" [disabled]=\"state.busy() || state.step() !== 'idle' || !state.canRemove(m)\" (clicked)=\"state.start({ kind: 'remove', method: m })\">{{ t.remove }}</hm-button>\n }\n </span>\n }\n </li>\n }\n </ul>\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);width:100%;min-width:0}.sp-account__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__list{display:flex;flex-direction:column;margin:0;padding:0;list-style:none;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control)}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-xxxs) var(--hm-space-xs)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__icon{display:inline-flex;flex:none;color:var(--hm-muted)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;padding:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__form,.sp-account__code{display:flex;flex-direction:column;gap:var(--hm-space-xxxs)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:600}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:Geist Mono,ui-monospace,monospace;overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"] }]
|
|
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"] }]
|
|
2140
2982
|
}] });
|
|
2141
2983
|
|
|
2142
|
-
let sequence$1 = 0;
|
|
2143
2984
|
/**
|
|
2144
|
-
*
|
|
2145
|
-
*
|
|
2146
|
-
*
|
|
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`).
|
|
2147
2990
|
*/
|
|
2148
|
-
class
|
|
2991
|
+
class SpineAppTokensService {
|
|
2149
2992
|
constructor() {
|
|
2150
|
-
this.
|
|
2151
|
-
this.
|
|
2152
|
-
this.
|
|
2153
|
-
this.
|
|
2154
|
-
|
|
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)" />
|
|
3268
|
+
</div>
|
|
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"] }]
|
|
3283
|
+
}] });
|
|
3284
|
+
function errorDetail(err) {
|
|
3285
|
+
return err instanceof HttpErrorResponse ? (err.error?.detail ?? '') : '';
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
/**
|
|
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.
|
|
3293
|
+
*/
|
|
3294
|
+
class DefaultBadgeComponent {
|
|
3295
|
+
constructor() {
|
|
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();
|
|
3306
|
+
}
|
|
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
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
async save(value) {
|
|
3321
|
+
const previous = this.value();
|
|
3322
|
+
if (value === previous)
|
|
3323
|
+
return;
|
|
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
|
+
}
|
|
3340
|
+
}
|
|
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 }); }
|
|
3361
|
+
}
|
|
3362
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DefaultBadgeComponent, decorators: [{
|
|
3363
|
+
type: Component,
|
|
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 }] }] } });
|
|
3384
|
+
|
|
3385
|
+
let sequence$2 = 0;
|
|
3386
|
+
/**
|
|
3387
|
+
* Códigos de recuperação da conta no Argos: quantos restam e "gerar novos" (com reautenticação). O lote
|
|
3388
|
+
* novo aparece UMA vez (o Argos só guarda o hash), com copiar e baixar em .txt, até a pessoa dizer que
|
|
3389
|
+
* guardou — e some quando o painel fecha.
|
|
3390
|
+
*/
|
|
3391
|
+
class AccountRecoveryCodesComponent {
|
|
3392
|
+
constructor() {
|
|
3393
|
+
this.state = inject(SpineAccountSecurityState);
|
|
3394
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
3395
|
+
this.t = this.messages.accountSecurity;
|
|
3396
|
+
this.toast = inject(HermesToastService);
|
|
3397
|
+
this.titleId = `sp-account-codes-${++sequence$2}`;
|
|
2155
3398
|
this.remaining = computed(() => pluralMessage(this.t.codesRemaining, this.state.remainingCodes(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "remaining" }] : /* istanbul ignore next */ []));
|
|
2156
3399
|
}
|
|
2157
3400
|
errorText(code) {
|
|
@@ -2194,35 +3437,52 @@ class AccountRecoveryCodesComponent {
|
|
|
2194
3437
|
<spine-account-reauth />
|
|
2195
3438
|
} @else if (state.owner() === 'codes' && state.step() === 'codes' && state.issuedCodes(); as codes) {
|
|
2196
3439
|
<section class="sp-account__panel sp-account__panel--success" [attr.aria-labelledby]="titleId">
|
|
2197
|
-
<
|
|
2198
|
-
|
|
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>
|
|
2199
3447
|
<ol class="sp-account__codes">
|
|
2200
3448
|
@for (code of codes; track code) {
|
|
2201
3449
|
<li><code>{{ code }}</code></li>
|
|
2202
3450
|
}
|
|
2203
3451
|
</ol>
|
|
2204
3452
|
<div class="sp-account__actions">
|
|
3453
|
+
<hm-button emphasis="high" (clicked)="state.codesSaved()">{{ t.codesSaved }}</hm-button>
|
|
2205
3454
|
<hm-button emphasis="medium" icon="copy" (clicked)="copy(codes)">{{ t.copyCodes }}</hm-button>
|
|
2206
3455
|
<hm-button emphasis="medium" icon="download" (clicked)="download(codes)">{{ t.downloadCodes }}</hm-button>
|
|
2207
|
-
<hm-button emphasis="high" (clicked)="state.codesSaved()">{{ t.codesSaved }}</hm-button>
|
|
2208
3456
|
</div>
|
|
2209
3457
|
</section>
|
|
2210
|
-
} @else if (state.activeMethods().length) {
|
|
2211
|
-
<p class="sp-account__hint">{{ remaining() }} {{ t.codesHint }}</p>
|
|
2212
|
-
<div class="sp-account__actions">
|
|
2213
|
-
<hm-button emphasis="medium" icon="key" [disabled]="state.busy() || state.step() !== 'idle'" (clicked)="state.start({ kind: 'codes' })">
|
|
2214
|
-
{{ state.remainingCodes() ? t.regenerateCodes : t.generateCodes }}
|
|
2215
|
-
</hm-button>
|
|
2216
|
-
</div>
|
|
2217
3458
|
} @else {
|
|
2218
|
-
<
|
|
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>
|
|
2219
3479
|
}
|
|
2220
3480
|
}
|
|
2221
|
-
`, 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 }); }
|
|
2222
3482
|
}
|
|
2223
3483
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountRecoveryCodesComponent, decorators: [{
|
|
2224
3484
|
type: Component,
|
|
2225
|
-
args: [{ selector: 'spine-account-recovery-codes', imports: [AlertComponent, ButtonComponent, AccountReauthComponent], template: `
|
|
3485
|
+
args: [{ selector: 'spine-account-recovery-codes', imports: [AlertComponent, ButtonComponent, IconComponent, AccountReauthComponent], template: `
|
|
2226
3486
|
@if (state.overview()) {
|
|
2227
3487
|
@if (state.owner() === 'codes') {
|
|
2228
3488
|
@if (state.notice(); as notice) {
|
|
@@ -2237,31 +3497,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
2237
3497
|
<spine-account-reauth />
|
|
2238
3498
|
} @else if (state.owner() === 'codes' && state.step() === 'codes' && state.issuedCodes(); as codes) {
|
|
2239
3499
|
<section class="sp-account__panel sp-account__panel--success" [attr.aria-labelledby]="titleId">
|
|
2240
|
-
<
|
|
2241
|
-
|
|
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>
|
|
2242
3507
|
<ol class="sp-account__codes">
|
|
2243
3508
|
@for (code of codes; track code) {
|
|
2244
3509
|
<li><code>{{ code }}</code></li>
|
|
2245
3510
|
}
|
|
2246
3511
|
</ol>
|
|
2247
3512
|
<div class="sp-account__actions">
|
|
3513
|
+
<hm-button emphasis="high" (clicked)="state.codesSaved()">{{ t.codesSaved }}</hm-button>
|
|
2248
3514
|
<hm-button emphasis="medium" icon="copy" (clicked)="copy(codes)">{{ t.copyCodes }}</hm-button>
|
|
2249
3515
|
<hm-button emphasis="medium" icon="download" (clicked)="download(codes)">{{ t.downloadCodes }}</hm-button>
|
|
2250
|
-
<hm-button emphasis="high" (clicked)="state.codesSaved()">{{ t.codesSaved }}</hm-button>
|
|
2251
3516
|
</div>
|
|
2252
3517
|
</section>
|
|
2253
|
-
} @else if (state.activeMethods().length) {
|
|
2254
|
-
<p class="sp-account__hint">{{ remaining() }} {{ t.codesHint }}</p>
|
|
2255
|
-
<div class="sp-account__actions">
|
|
2256
|
-
<hm-button emphasis="medium" icon="key" [disabled]="state.busy() || state.step() !== 'idle'" (clicked)="state.start({ kind: 'codes' })">
|
|
2257
|
-
{{ state.remainingCodes() ? t.regenerateCodes : t.generateCodes }}
|
|
2258
|
-
</hm-button>
|
|
2259
|
-
</div>
|
|
2260
3518
|
} @else {
|
|
2261
|
-
<
|
|
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>
|
|
2262
3539
|
}
|
|
2263
3540
|
}
|
|
2264
|
-
`, 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"] }]
|
|
2265
3542
|
}] });
|
|
2266
3543
|
|
|
2267
3544
|
/**
|
|
@@ -2372,53 +3649,14 @@ class AccountSessionsComponent {
|
|
|
2372
3649
|
.format(new Date(value));
|
|
2373
3650
|
}
|
|
2374
3651
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountSessionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2375
|
-
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 }); }
|
|
2376
3653
|
}
|
|
2377
3654
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AccountSessionsComponent, decorators: [{
|
|
2378
3655
|
type: Component,
|
|
2379
|
-
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"] }]
|
|
2380
3657
|
}], ctorParameters: () => [] });
|
|
2381
3658
|
|
|
2382
|
-
|
|
2383
|
-
* Tokens de aplicação pessoais (`x-app-token`) do usuário logado — `argos-admin /me/app-tokens`.
|
|
2384
|
-
* Sempre da aplicação LOGADA: o servidor tira o client do próprio access-token da sessão, então
|
|
2385
|
-
* aqui não existe escolha de aplicação.
|
|
2386
|
-
* Autentica com o access-token da sessão (o servidor recusa um app-token aqui: token não cria
|
|
2387
|
-
* token). `available` é falso quando o app não tem a URL do argos-admin (`SPINE_ADMIN_API`).
|
|
2388
|
-
*/
|
|
2389
|
-
class SpineAppTokensService {
|
|
2390
|
-
constructor() {
|
|
2391
|
-
this.http = inject(HttpClient);
|
|
2392
|
-
this.auth = inject(AuthorizationService);
|
|
2393
|
-
this.adminApi = inject(SPINE_ADMIN_API, { optional: true });
|
|
2394
|
-
this.available = !!this.adminApi;
|
|
2395
|
-
}
|
|
2396
|
-
get baseUrl() {
|
|
2397
|
-
return `${this.adminApi}/me/app-tokens`;
|
|
2398
|
-
}
|
|
2399
|
-
get headers() {
|
|
2400
|
-
return new HttpHeaders({ 'x-access-token': this.auth.getToken() ?? '' });
|
|
2401
|
-
}
|
|
2402
|
-
list() {
|
|
2403
|
-
return firstValueFrom(this.http.get(this.baseUrl, { headers: this.headers }));
|
|
2404
|
-
}
|
|
2405
|
-
options() {
|
|
2406
|
-
return firstValueFrom(this.http.get(`${this.baseUrl}/options`, { headers: this.headers }));
|
|
2407
|
-
}
|
|
2408
|
-
create(body) {
|
|
2409
|
-
return firstValueFrom(this.http.post(this.baseUrl, body, { headers: this.headers }));
|
|
2410
|
-
}
|
|
2411
|
-
revoke(id) {
|
|
2412
|
-
return firstValueFrom(this.http.delete(`${this.baseUrl}${urlPath `/${id}`}`, { headers: this.headers }));
|
|
2413
|
-
}
|
|
2414
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2415
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, providedIn: 'root' }); }
|
|
2416
|
-
}
|
|
2417
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAppTokensService, decorators: [{
|
|
2418
|
-
type: Injectable,
|
|
2419
|
-
args: [{ providedIn: 'root' }]
|
|
2420
|
-
}] });
|
|
2421
|
-
|
|
3659
|
+
let sequence$1 = 0;
|
|
2422
3660
|
/** Validades oferecidas (dias). O servidor recusa acima de 365 — o CHECK no banco também. */
|
|
2423
3661
|
/**
|
|
2424
3662
|
* Tokens de aplicação do usuário (seção Conta do painel de configurações): lista, criação e
|
|
@@ -2436,6 +3674,7 @@ class AppTokensComponent {
|
|
|
2436
3674
|
this.toast = inject(HermesToastService);
|
|
2437
3675
|
this.preferences = inject(SpinePreferencesService);
|
|
2438
3676
|
this.available = this.service.available;
|
|
3677
|
+
this.ids = `sp-tokens-${++sequence$1}`;
|
|
2439
3678
|
this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2440
3679
|
this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
|
|
2441
3680
|
this.tokens = signal([], ...(ngDevMode ? [{ debugName: "tokens" }] : /* istanbul ignore next */ []));
|
|
@@ -2467,6 +3706,16 @@ class AppTokensComponent {
|
|
|
2467
3706
|
void this.load();
|
|
2468
3707
|
else
|
|
2469
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
|
+
});
|
|
2470
3719
|
}
|
|
2471
3720
|
async load() {
|
|
2472
3721
|
this.loading.set(true);
|
|
@@ -2556,6 +3805,9 @@ class AppTokensComponent {
|
|
|
2556
3805
|
this.toast.error(this.errorText(err, this.text.revokeError));
|
|
2557
3806
|
}
|
|
2558
3807
|
}
|
|
3808
|
+
cancelRevoke() {
|
|
3809
|
+
this.confirmingRevoke.set(null);
|
|
3810
|
+
}
|
|
2559
3811
|
formatDate(value) {
|
|
2560
3812
|
if (!value)
|
|
2561
3813
|
return this.text.never;
|
|
@@ -2567,11 +3819,11 @@ class AppTokensComponent {
|
|
|
2567
3819
|
return detail || fallback;
|
|
2568
3820
|
}
|
|
2569
3821
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AppTokensComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2570
|
-
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 }); }
|
|
2571
3823
|
}
|
|
2572
3824
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AppTokensComponent, decorators: [{
|
|
2573
3825
|
type: Component,
|
|
2574
|
-
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"] }]
|
|
2575
3827
|
}], ctorParameters: () => [] });
|
|
2576
3828
|
|
|
2577
3829
|
/** Dependências que valem mostrar em "Sobre" (as demais são detalhe de build). */
|
|
@@ -2586,23 +3838,78 @@ const KEY_DEPENDENCIES = [
|
|
|
2586
3838
|
{ name: 'rxjs', label: 'RxJS' },
|
|
2587
3839
|
// check-strings: on
|
|
2588
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
|
+
};
|
|
2589
3847
|
/**
|
|
2590
|
-
* Painel de configurações do shell (substitui as telas do antigo módulo "Sistema"):
|
|
2591
|
-
*
|
|
2592
|
-
*
|
|
2593
|
-
*
|
|
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…).
|
|
2594
3855
|
*
|
|
2595
3856
|
* Aberto por `SpineSettingsService.open()` — menu do perfil, `Ctrl+,` ou `?settings=<seção>`.
|
|
2596
3857
|
*/
|
|
2597
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
|
+
}
|
|
2598
3897
|
constructor() {
|
|
2599
3898
|
this.settings = inject(SpineSettingsService);
|
|
2600
3899
|
this.messages = inject(SPINE_MESSAGES);
|
|
2601
3900
|
this.text = this.messages.settings;
|
|
2602
3901
|
this.fmt = formatMessage;
|
|
2603
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 */ []));
|
|
2604
3905
|
this.theme = inject(ThemeService);
|
|
2605
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);
|
|
2606
3913
|
this.presets = inject(ThemePresetService);
|
|
2607
3914
|
this.density = inject(DensityService);
|
|
2608
3915
|
this.sidebar = inject(HermesSidebarService);
|
|
@@ -2616,6 +3923,9 @@ class SettingsPanelComponent {
|
|
|
2616
3923
|
this.config = inject(SPINE_CONFIG, { optional: true });
|
|
2617
3924
|
this.appSections = inject(SPINE_SETTINGS_SECTIONS, { optional: true }) ?? [];
|
|
2618
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;
|
|
2619
3929
|
/**
|
|
2620
3930
|
* "Copiar token de acesso" é ferramenta de desenvolvimento: põe o token da sessão (Bearer de tudo que a
|
|
2621
3931
|
* pessoa pode fazer) na área de transferência, ao alcance de qualquer app/extensão que leia o clipboard.
|
|
@@ -2645,6 +3955,18 @@ class SettingsPanelComponent {
|
|
|
2645
3955
|
this.densityValue = computed(() => this.hermesTheme.density(), ...(ngDevMode ? [{ debugName: "densityValue" }] : /* istanbul ignore next */ []));
|
|
2646
3956
|
this.fontValue = computed(() => this.hermesTheme.font(), ...(ngDevMode ? [{ debugName: "fontValue" }] : /* istanbul ignore next */ []));
|
|
2647
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 */ []));
|
|
2648
3970
|
// ── Região ──────────────────────────────────────────────────────────────────
|
|
2649
3971
|
this.languageOptions = computed(() => {
|
|
2650
3972
|
const appLanguage = this.preferences.languages.find((l) => l.value === this.preferences.appLanguage)?.label ?? this.preferences.appLanguage;
|
|
@@ -2666,48 +3988,183 @@ class SettingsPanelComponent {
|
|
|
2666
3988
|
this.timeZonePreview = computed(() => {
|
|
2667
3989
|
const zone = this.preferences.effectiveTimeZone();
|
|
2668
3990
|
try {
|
|
2669
|
-
return new Intl.DateTimeFormat(this.
|
|
3991
|
+
return new Intl.DateTimeFormat(this.i18n.formatLocale(), { dateStyle: 'short', timeStyle: 'short', timeZone: zone }).format(new Date())
|
|
2670
3992
|
+ ` (${offsetOf(zone)})`;
|
|
2671
3993
|
}
|
|
2672
3994
|
catch {
|
|
2673
3995
|
return zone;
|
|
2674
3996
|
}
|
|
2675
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 */ []));
|
|
2676
4025
|
// ── Atalhos ─────────────────────────────────────────────────────────────────
|
|
2677
4026
|
/** Atalhos registrados (favoritos numa linha só) + os que outra peça dispara — os mesmos do painel "?". */
|
|
2678
4027
|
this.shortcutItems = this.shortcutService.panelItems;
|
|
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 */ []));
|
|
2679
4056
|
this.logoutService = inject(SpineLogoutService);
|
|
2680
4057
|
/** "Saindo…" no botão; volta a `false` também quando a saída falha. */
|
|
2681
4058
|
this.logoutPending = this.logoutService.pending;
|
|
2682
|
-
// ──
|
|
4059
|
+
// ── MFA ─────────────────────────────────────────────────────────────────────
|
|
2683
4060
|
/** API da conta no Argos (Bearer com o token da sessão), a partir do `AUTH_CONFIG`. Sem ela (sem
|
|
2684
|
-
* `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. */
|
|
2685
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 */ []));
|
|
2686
4112
|
// ── Sobre ───────────────────────────────────────────────────────────────────
|
|
2687
4113
|
this.dependencies = KEY_DEPENDENCIES
|
|
2688
4114
|
.map((d) => ({ ...d, version: this.packageInfo.dependencies?.[d.name] }))
|
|
2689
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
|
+
});
|
|
2690
4121
|
}
|
|
2691
|
-
|
|
2692
|
-
this.
|
|
2693
|
-
}
|
|
2694
|
-
setFamily(id) {
|
|
2695
|
-
const family = this.families.find((f) => f.id === id);
|
|
2696
|
-
if (family)
|
|
2697
|
-
this.presets.setPreset(family);
|
|
2698
|
-
}
|
|
2699
|
-
setDensity(value) {
|
|
2700
|
-
this.density.setDensity(value);
|
|
2701
|
-
}
|
|
2702
|
-
setFont(value) {
|
|
2703
|
-
this.hermesTheme.setFont(value);
|
|
4122
|
+
cancelConfirm() {
|
|
4123
|
+
this.confirming.set(null);
|
|
2704
4124
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
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]);
|
|
2707
4136
|
}
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
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);
|
|
2711
4168
|
}
|
|
2712
4169
|
// ── Conta ───────────────────────────────────────────────────────────────────
|
|
2713
4170
|
get profile() { return this.auth.profile; }
|
|
@@ -2719,7 +4176,7 @@ class SettingsPanelComponent {
|
|
|
2719
4176
|
return this.text.sessionUnknown;
|
|
2720
4177
|
const at = new Date(exp * 1000);
|
|
2721
4178
|
const minutes = Math.round((at.getTime() - Date.now()) / 60000);
|
|
2722
|
-
const when = new Intl.DateTimeFormat(this.
|
|
4179
|
+
const when = new Intl.DateTimeFormat(this.i18n.formatLocale(), { timeStyle: 'short', timeZone: this.preferences.effectiveTimeZone() }).format(at);
|
|
2723
4180
|
if (minutes <= 0)
|
|
2724
4181
|
return formatMessage(this.text.sessionExpired, { time: when });
|
|
2725
4182
|
return minutes < 60
|
|
@@ -2747,12 +4204,40 @@ class SettingsPanelComponent {
|
|
|
2747
4204
|
const login = payload?.['preferred_username'] ?? payload?.['email'];
|
|
2748
4205
|
return typeof login === 'string' ? login : '';
|
|
2749
4206
|
}
|
|
4207
|
+
/** Realm pelo nome (`master`, `Minha Empresa`); um id (UUID) não diz nada a quem lê e fica de fora. */
|
|
2750
4208
|
get accountRealm() {
|
|
2751
|
-
|
|
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();
|
|
2752
4237
|
}
|
|
2753
4238
|
get buildDate() {
|
|
2754
4239
|
const date = this.packageInfo.buildDate;
|
|
2755
|
-
return date ? new Intl.DateTimeFormat(this.
|
|
4240
|
+
return date ? new Intl.DateTimeFormat(this.i18n.formatLocale(), { dateStyle: 'short', timeStyle: 'short' }).format(new Date(date)) : '';
|
|
2756
4241
|
}
|
|
2757
4242
|
/** Texto pronto pra colar num chamado: app, versão, ambiente, navegador, tela, fuso e página. */
|
|
2758
4243
|
async copySupportInfo() {
|
|
@@ -2790,18 +4275,19 @@ class SettingsPanelComponent {
|
|
|
2790
4275
|
this.settings.close();
|
|
2791
4276
|
}
|
|
2792
4277
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2793
|
-
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\" [loading]=\"logoutPending()\" (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 }); }
|
|
2794
4279
|
}
|
|
2795
4280
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, decorators: [{
|
|
2796
4281
|
type: Component,
|
|
2797
4282
|
args: [{ selector: 'spine-settings-panel', imports: [
|
|
2798
|
-
NgComponentOutlet, AppTokensComponent,
|
|
4283
|
+
NgComponentOutlet, AppTokensComponent, AiAgentComponent, AccountActivityComponent, DefaultBadgeComponent, CheckboxComponent,
|
|
2799
4284
|
AccountMfaComponent, AccountRecoveryCodesComponent, AccountSessionsComponent, SpineAccountSecurityScopeDirective,
|
|
2800
|
-
AccountAvatarComponent, ButtonComponent, ComboboxComponent,
|
|
4285
|
+
AccountAvatarComponent, ButtonComponent, ComboboxComponent,
|
|
2801
4286
|
SegmentedControlComponent, SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent,
|
|
2802
|
-
SelectComponent, ShortcutListComponent, ThemePickerComponent,
|
|
2803
|
-
], 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\" [loading]=\"logoutPending()\" (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"] }]
|
|
2804
|
-
}] });
|
|
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;
|
|
2805
4291
|
/** "UTC−03:00" para um fuso IANA. */
|
|
2806
4292
|
function offsetOf(timeZone) {
|
|
2807
4293
|
try {
|
|
@@ -3552,7 +5038,7 @@ class SpineDocsComponent {
|
|
|
3552
5038
|
</div>
|
|
3553
5039
|
</hm-modal>
|
|
3554
5040
|
}
|
|
3555
|
-
`, 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 }); }
|
|
3556
5042
|
}
|
|
3557
5043
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineDocsComponent, decorators: [{
|
|
3558
5044
|
type: Component,
|
|
@@ -3621,7 +5107,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3621
5107
|
</div>
|
|
3622
5108
|
</hm-modal>
|
|
3623
5109
|
}
|
|
3624
|
-
`, 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"] }]
|
|
3625
5111
|
}] });
|
|
3626
5112
|
|
|
3627
5113
|
/**
|
|
@@ -3697,6 +5183,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3697
5183
|
`, styles: [":host{display:block;flex:none}\n"] }]
|
|
3698
5184
|
}] });
|
|
3699
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
|
+
|
|
3700
5229
|
/**
|
|
3701
5230
|
* Shell do spine — MESMA estrutura do `docs/website`'s `app.component.html`/`.scss` (decisão de
|
|
3702
5231
|
* 2026-09-28, ver `hefesto-migration-plan.md`): `.shell` com altura de viewport de verdade
|
|
@@ -3729,6 +5258,10 @@ class LayoutComponent {
|
|
|
3729
5258
|
this.settings = inject(SpineSettingsService);
|
|
3730
5259
|
/** Também liga, uma vez, o aviso de falha do logout (`logoutFailed$` do `@orfeu/angular`). */
|
|
3731
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);
|
|
3732
5265
|
this.isNoPermission = this.matchesNoPermission(this.router.url);
|
|
3733
5266
|
this.router.events.pipe(filter((e) => e instanceof NavigationEnd))
|
|
3734
5267
|
.subscribe(e => { this.isNoPermission = this.matchesNoPermission(e.urlAfterRedirects); });
|
|
@@ -3737,6 +5270,10 @@ class LayoutComponent {
|
|
|
3737
5270
|
return url.split(/[?#]/)[0] === '/no-permission';
|
|
3738
5271
|
}
|
|
3739
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();
|
|
3740
5277
|
// Liga os atalhos de teclado globais às ações reais do sistema.
|
|
3741
5278
|
this.shortcuts.setActions({
|
|
3742
5279
|
navigate: (url) => this.router.navigateByUrl(url),
|
|
@@ -3811,7 +5348,9 @@ class LayoutComponent {
|
|
|
3811
5348
|
<spine-docs />
|
|
3812
5349
|
<spine-news />
|
|
3813
5350
|
<spine-onboarding-overlay></spine-onboarding-overlay>
|
|
3814
|
-
|
|
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 }); }
|
|
3815
5354
|
}
|
|
3816
5355
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LayoutComponent, decorators: [{
|
|
3817
5356
|
type: Component,
|
|
@@ -3832,6 +5371,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3832
5371
|
OnboardingOverlayComponent,
|
|
3833
5372
|
ShortcutsCheatsheetComponent,
|
|
3834
5373
|
SettingsPanelComponent,
|
|
5374
|
+
SpineIdleWarningComponent,
|
|
3835
5375
|
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
3836
5376
|
<div class="shell">
|
|
3837
5377
|
<spine-header></spine-header>
|
|
@@ -3870,6 +5410,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3870
5410
|
<spine-docs />
|
|
3871
5411
|
<spine-news />
|
|
3872
5412
|
<spine-onboarding-overlay></spine-onboarding-overlay>
|
|
5413
|
+
<!-- Aviso de "Encerrar por inatividade" (SpineFeatures.idleTimeout): só desenha quando abre. -->
|
|
5414
|
+
<spine-idle-warning />
|
|
3873
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"] }]
|
|
3874
5416
|
}], ctorParameters: () => [{ type: i3.Router }, { type: i2$1.ThemeService }, { type: ShortcutService }, { type: OnboardingService }, { type: SpineNewsService }] });
|
|
3875
5417
|
|
|
@@ -3877,5 +5419,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3877
5419
|
* Generated bundle index. Do not edit.
|
|
3878
5420
|
*/
|
|
3879
5421
|
|
|
3880
|
-
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, SpineLogoutService, 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 };
|
|
3881
5423
|
//# sourceMappingURL=trismegisto-spine-layout.mjs.map
|