@trismegisto/spine 2.0.1 → 2.1.1

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