@trismegisto/spine 3.3.5 → 3.3.7

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 (72) hide show
  1. package/README.md +4 -2
  2. package/fesm2022/trismegisto-spine-audit.mjs +20 -2
  3. package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
  4. package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs → trismegisto-spine-auto-service-auto-service-audit.component-CszXNvSs.mjs} +2 -2
  5. package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs.map → trismegisto-spine-auto-service-auto-service-audit.component-CszXNvSs.mjs.map} +1 -1
  6. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs → trismegisto-spine-auto-service-auto-service-client-picker.component-CknHYd_P.mjs} +7 -3
  7. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-CknHYd_P.mjs.map +1 -0
  8. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs → trismegisto-spine-auto-service-auto-service-client.component-Dg3mJrpz.mjs} +24 -5
  9. package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-Dg3mJrpz.mjs.map +1 -0
  10. package/fesm2022/{trismegisto-spine-auto-service-auto-service-delete-CV_t9vxm.mjs → trismegisto-spine-auto-service-auto-service-delete-BWY5G-xe.mjs} +2 -2
  11. package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-BWY5G-xe.mjs.map +1 -0
  12. package/fesm2022/{trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs → trismegisto-spine-auto-service-auto-service-groups.component-DQqzfq0M.mjs} +21 -5
  13. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-DQqzfq0M.mjs.map +1 -0
  14. package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs → trismegisto-spine-auto-service-auto-service-login-attempts.component-CjxKYv-y.mjs} +2 -2
  15. package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs.map → trismegisto-spine-auto-service-auto-service-login-attempts.component-CjxKYv-y.mjs.map} +1 -1
  16. package/fesm2022/{trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs → trismegisto-spine-auto-service-auto-service-news-page.component-Cbaszx4F.mjs} +21 -5
  17. package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-Cbaszx4F.mjs.map +1 -0
  18. package/fesm2022/{trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs → trismegisto-spine-auto-service-auto-service-ratings-page.component-CJRIDWXi.mjs} +22 -4
  19. package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-CJRIDWXi.mjs.map +1 -0
  20. package/fesm2022/{trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs → trismegisto-spine-auto-service-auto-service-suggestions-page.component-Bu8YX554.mjs} +46 -4
  21. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-Bu8YX554.mjs.map +1 -0
  22. package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs → trismegisto-spine-auto-service-auto-service.component-BLkLHp6J.mjs} +23 -5
  23. package/fesm2022/trismegisto-spine-auto-service-auto-service.component-BLkLHp6J.mjs.map +1 -0
  24. package/fesm2022/{trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs → trismegisto-spine-auto-service-home.component-BYMiTZp6.mjs} +6 -3
  25. package/fesm2022/trismegisto-spine-auto-service-home.component-BYMiTZp6.mjs.map +1 -0
  26. package/fesm2022/trismegisto-spine-auto-service.mjs +10 -10
  27. package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
  28. package/fesm2022/trismegisto-spine-layout-ai-agent-chat.component-LlxtxNA5.mjs +294 -0
  29. package/fesm2022/trismegisto-spine-layout-ai-agent-chat.component-LlxtxNA5.mjs.map +1 -0
  30. package/fesm2022/trismegisto-spine-layout-ai-agent-transcript-BNcgbrV_.mjs +438 -0
  31. package/fesm2022/trismegisto-spine-layout-ai-agent-transcript-BNcgbrV_.mjs.map +1 -0
  32. package/fesm2022/{trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs → trismegisto-spine-layout-suggestions-page.component-BEGFNlvw.mjs} +18 -4
  33. package/fesm2022/trismegisto-spine-layout-suggestions-page.component-BEGFNlvw.mjs.map +1 -0
  34. package/fesm2022/{trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs → trismegisto-spine-layout-trismegisto-spine-layout-D3vetDC_.mjs} +1075 -57
  35. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-D3vetDC_.mjs.map +1 -0
  36. package/fesm2022/trismegisto-spine-layout.mjs +1 -1
  37. package/fesm2022/trismegisto-spine-modal.mjs +4 -1
  38. package/fesm2022/trismegisto-spine-modal.mjs.map +1 -1
  39. package/fesm2022/trismegisto-spine-notification.mjs +1 -1
  40. package/fesm2022/trismegisto-spine-notification.mjs.map +1 -1
  41. package/fesm2022/trismegisto-spine-routing.mjs +2 -2
  42. package/fesm2022/trismegisto-spine-routing.mjs.map +1 -1
  43. package/fesm2022/trismegisto-spine-shell.mjs +2 -1
  44. package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
  45. package/fesm2022/trismegisto-spine-side-panel.mjs +4 -1
  46. package/fesm2022/trismegisto-spine-side-panel.mjs.map +1 -1
  47. package/fesm2022/trismegisto-spine.mjs +218 -69
  48. package/fesm2022/trismegisto-spine.mjs.map +1 -1
  49. package/package.json +6 -6
  50. package/types/trismegisto-spine-audit.d.ts +11 -4
  51. package/types/trismegisto-spine-audit.d.ts.map +1 -1
  52. package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
  53. package/types/trismegisto-spine-layout.d.ts +124 -105
  54. package/types/trismegisto-spine-layout.d.ts.map +1 -1
  55. package/types/trismegisto-spine-modal.d.ts +2 -1
  56. package/types/trismegisto-spine-modal.d.ts.map +1 -1
  57. package/types/trismegisto-spine-shell.d.ts.map +1 -1
  58. package/types/trismegisto-spine-side-panel.d.ts +2 -1
  59. package/types/trismegisto-spine-side-panel.d.ts.map +1 -1
  60. package/types/trismegisto-spine.d.ts +110 -28
  61. package/types/trismegisto-spine.d.ts.map +1 -1
  62. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs.map +0 -1
  63. package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs.map +0 -1
  64. package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-CV_t9vxm.mjs.map +0 -1
  65. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs.map +0 -1
  66. package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs.map +0 -1
  67. package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs.map +0 -1
  68. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs.map +0 -1
  69. package/fesm2022/trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs.map +0 -1
  70. package/fesm2022/trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs.map +0 -1
  71. package/fesm2022/trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs.map +0 -1
  72. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs.map +0 -1
@@ -1,28 +1,84 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ChangeDetectorRef, ChangeDetectionStrategy, Component, InjectionToken, input, signal, computed, Injectable, PLATFORM_ID, effect, untracked, DestroyRef, Injector, model, ElementRef, output, Optional, Inject, viewChild, NgZone, isDevMode, Directive, makeEnvironmentProviders, provideEnvironmentInitializer, TemplateRef, ViewContainerRef, afterNextRender, ViewEncapsulation } from '@angular/core';
2
+ import { inject, ChangeDetectorRef, ChangeDetectionStrategy, Component, InjectionToken, input, signal, computed, Injectable, PLATFORM_ID, effect, untracked, DestroyRef, Injector, model, ElementRef, output, Optional, Inject, viewChild, NgZone, isDevMode, Directive, makeEnvironmentProviders, provideEnvironmentInitializer, TemplateRef, ViewContainerRef, afterNextRender, ViewEncapsulation, DOCUMENT } from '@angular/core';
3
3
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
4
4
  import * as i3 from '@angular/router';
5
5
  import { RouterLink, Router, NavigationEnd, NavigationSkipped, RouterOutlet } from '@angular/router';
6
6
  import { firstValueFrom, filter, Subject, take, Subscription, debounceTime, distinctUntilChanged } from 'rxjs';
7
- import { MenuButtonComponent, HermesThemeService, HERMES_THEME_CONFIG, HermesSidebarService, HermesToastService, IconComponent, ButtonComponent, SkeletonComponent, TextareaComponent, FloatingWindowComponent, IconButtonComponent, AccountAvatarComponent, BadgeComponent, PopoverComponent, PopoverTriggerDirective, HermesShortcutService, HeaderComponent as HeaderComponent$1, CommandPaletteComponent, panelIdsInOrder, createPersistedWorkspaceStore, HERMES_WORKSPACE_PERSISTENCE_CONFIG, findPanelTabs, canSplitPanel, cyclicPanelNeighbor, ContextMenuDirective, SidebarComponent as SidebarComponent$1, SidebarNodeComponent, HermesI18nService, SegmentedControlComponent, ThemePickerComponent, AlertComponent, BreadcrumbComponent, DrawerComponent, ModalComponent, PaginatorComponent, RichTextComponent, SearchFieldComponent, ShortcutListComponent, TagComponent, CodeInputComponent, InputComponent, RadioComponent, CheckboxComponent, CodeViewerComponent, SelectComponent, AvatarGroupComponent, HERMES_SIDEBAR_CONFIG, ComboboxComponent, SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent, ToggletipComponent, TooltipDirective, LoadingSquareComponent, registerUnsavedChanges, HermesUnsavedChangesService, WorkspaceComponent, ToastStackComponent, SkipLinkComponent, MessageScrollerComponent, BubbleComponent, MessageComponent, MessageDividerComponent } from '@trismegisto/angular';
8
- import { HermesSidebarService as HermesSidebarService$1 } from '@trismegisto/angular/sidebar';
7
+ import { SkipLinkComponent } from '@trismegisto/angular/skip-link';
8
+ import { HermesToastService, ToastStackComponent } from '@trismegisto/angular/toast';
9
+ import { WorkspaceComponent } from '@trismegisto/angular/workspace';
10
+ import { HermesSidebarService, SidebarComponent as SidebarComponent$1, SidebarNodeComponent, HERMES_SIDEBAR_CONFIG } from '@trismegisto/angular/sidebar';
9
11
  import { ModalComponent as ModalComponent$1 } from '@trismegisto/spine/modal';
10
12
  import { SidePanelComponent } from '@trismegisto/spine/side-panel';
11
13
  import * as i2$1 from '@trismegisto/spine';
12
- import { SPINE_MESSAGES, SPINE_CONFIG, ThemeService, resolveSpineLogo, SpinePreferencesService, SpineQuickAccessService, DensityService, ThemePresetService, SPINE_ADMIN_API, SpineUserStorage, SPINE_USER_STORAGE_KEYS, spineRoutesOf, routerIdOf, SPINE_IDLE_OPTIONS, APP_PACKAGE_INFO, registerSpineBuiltinPage } from '@trismegisto/spine';
14
+ import { SPINE_MESSAGES, SPINE_CONFIG, ThemeService, resolveSpineLogo, SpinePreferencesService, SpineQuickAccessService, DensityService, ThemePresetService, SPINE_ADMIN_API, resolveSpineImage, SPINE_DEFAULT_AGENT_AVATAR, SPINE_DEFAULT_AGENT_IMAGE, SpineUserStorage, SPINE_USER_STORAGE_KEYS, spineRoutesOf, routerIdOf, SPINE_IDLE_OPTIONS, APP_PACKAGE_INFO, registerSpineBuiltinPage } from '@trismegisto/spine';
13
15
  import * as i2 from '@angular/common';
14
16
  import { CommonModule, NgTemplateOutlet, NgComponentOutlet, isPlatformBrowser, Location } from '@angular/common';
17
+ import { AccountAvatarComponent } from '@trismegisto/angular/account-avatar';
18
+ import { CommandPaletteComponent } from '@trismegisto/angular/command-palette';
19
+ import { HeaderComponent as HeaderComponent$1 } from '@trismegisto/angular/header';
20
+ import { IconComponent } from '@trismegisto/angular/icon';
21
+ import { IconButtonComponent } from '@trismegisto/angular/icon-button';
22
+ import { MenuButtonComponent } from '@trismegisto/angular/menu-button';
15
23
  import * as i1 from '@orfeu/angular/core';
16
24
  import { AuthorizationService, AUTH_CONFIG, ViewService, RealmAppsService } from '@orfeu/angular/core';
17
25
  import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
18
- import { urlPath, formatMessage, pluralMessage, detectShortcutPlatform, tabUrl, HERMES_FONTS, HERMES_TEXT_SCALES, formatShortcut, formatMessageHtml, weekdayNames, FALLBACK_FAMILY, findPanelTabs as findPanelTabs$1, isMessageContinuation } from '@trismegisto/primitives';
26
+ import { HermesThemeService, HERMES_THEME_CONFIG } from '@trismegisto/angular/theme';
27
+ import { formatMessage, pluralMessage } from '@trismegisto/primitives/i18n';
28
+ import { urlPath, formatMessageHtml } from '@trismegisto/primitives/html';
29
+ import { FloatingWindowComponent } from '@trismegisto/angular/floating-window';
30
+ import { ButtonComponent } from '@trismegisto/angular/button';
31
+ import { SkeletonComponent } from '@trismegisto/angular/skeleton';
32
+ import { TextareaComponent } from '@trismegisto/angular/textarea';
33
+ import { BadgeComponent } from '@trismegisto/angular/badge';
34
+ import { PopoverComponent, PopoverTriggerDirective } from '@trismegisto/angular/popover';
19
35
  import { NotificationService, AlertType } from '@trismegisto/spine/notification';
36
+ import { HermesShortcutService, ShortcutListComponent } from '@trismegisto/angular/shortcut-list';
37
+ import { detectShortcutPlatform, formatShortcut } from '@trismegisto/primitives/shortcuts';
38
+ import { ContextMenuDirective } from '@trismegisto/angular/menu';
39
+ import { createPersistedWorkspaceStore, HERMES_WORKSPACE_PERSISTENCE_CONFIG } from '@trismegisto/angular/workspace-persistence';
40
+ import { panelIdsInOrder, findPanelTabs, canSplitPanel, cyclicPanelNeighbor } from '@trismegisto/primitives/workspace';
41
+ import { tabUrl } from '@trismegisto/primitives/tab-bar';
20
42
  import { resolveEffectiveRouteChain, ComponentDistributorService, componentRouterId, LoggerService } from '@trismegisto/spine/routing';
43
+ import { HermesI18nService } from '@trismegisto/angular/i18n';
44
+ import { SegmentedControlComponent } from '@trismegisto/angular/segmented-control';
45
+ import { ThemePickerComponent } from '@trismegisto/angular/theme-picker';
46
+ import { HERMES_FONTS, FALLBACK_FAMILY } from '@trismegisto/primitives/theme';
47
+ import { HERMES_TEXT_SCALES } from '@trismegisto/primitives/zoom';
48
+ import { AlertComponent } from '@trismegisto/angular/alert';
49
+ import { BreadcrumbComponent } from '@trismegisto/angular/breadcrumb';
50
+ import { DrawerComponent } from '@trismegisto/angular/drawer';
51
+ import { ModalComponent } from '@trismegisto/angular/modal';
52
+ import { PaginatorComponent } from '@trismegisto/angular/paginator';
53
+ import { RichTextComponent } from '@trismegisto/angular/rich-text';
54
+ import { SearchFieldComponent } from '@trismegisto/angular/search-field';
55
+ import { CheckboxComponent } from '@trismegisto/angular/checkbox';
56
+ import { ComboboxComponent } from '@trismegisto/angular/combobox';
57
+ import { SelectComponent } from '@trismegisto/angular/select';
58
+ import { SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent } from '@trismegisto/angular/settings';
59
+ import { ToggletipComponent } from '@trismegisto/angular/toggletip';
60
+ import { weekdayNames } from '@trismegisto/primitives/date';
61
+ import { TagComponent } from '@trismegisto/angular/tag';
21
62
  import * as i1$1 from '@angular/forms';
22
63
  import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
64
+ import { CodeInputComponent } from '@trismegisto/angular/code-input';
65
+ import { InputComponent } from '@trismegisto/angular/input';
66
+ import { RadioComponent } from '@trismegisto/angular/radio';
67
+ import { CodeViewerComponent } from '@trismegisto/angular/code-viewer';
68
+ import { AvatarGroupComponent } from '@trismegisto/angular/avatar-group';
23
69
  import { Title, DomSanitizer } from '@angular/platform-browser';
70
+ import { LoadingSquareComponent } from '@trismegisto/angular/loading-square';
71
+ import { TooltipDirective } from '@trismegisto/angular/tooltip';
24
72
  import { Marked } from 'marked';
25
73
  import DOMPurify from 'dompurify';
74
+ import { FabComponent } from '@trismegisto/angular/fab';
75
+ import { PromptSuggestionsComponent } from '@trismegisto/angular/prompt-suggestions';
76
+ import { registerUnsavedChanges, HermesUnsavedChangesService } from '@trismegisto/angular/unsaved-changes';
77
+ import { BubbleComponent } from '@trismegisto/angular/bubble';
78
+ import { MessageComponent } from '@trismegisto/angular/message';
79
+ import { MessageDividerComponent } from '@trismegisto/angular/message-divider';
80
+ import { MessageScrollerComponent } from '@trismegisto/angular/message-scroller';
81
+ import { isMessageContinuation } from '@trismegisto/primitives/conversation';
26
82
 
27
83
  /**
28
84
  * Seletor de aplicações do realm: um `hm-menu-button` próprio, só com o ícone de grade, que abre a lista das
@@ -1856,6 +1912,553 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
1856
1912
  `, styles: [":host{display:contents}.spine-inbox{position:relative;display:inline-flex}.spine-inbox__badge{position:absolute;inset-block-start:0;inset-inline-end:0;pointer-events:none;transform:translate(25%,-15%)}.spine-inbox__list{display:flex;flex-direction:column;inline-size:min(21rem,calc(100vw - 4rem));max-block-size:min(28rem,70dvh);margin:0;padding:0;overflow-x:hidden;overflow-y:auto;list-style:none}.spine-inbox__item{display:flex;gap:var(--hm-space-sm);inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.spine-inbox__item:hover{background:var(--hm-hover)}.spine-inbox__item:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:calc(var(--hm-border-thin) * -1)}.spine-inbox__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:2rem;block-size:2rem;border-radius:50%;background:var(--hm-accent-subtle);color:var(--hm-accent)}.spine-inbox__text{display:flex;flex:1;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.spine-inbox__source{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.spine-inbox__title{overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.spine-inbox__body{display:-webkit-box;overflow:hidden;color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.spine-inbox__meta{display:flex;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.spine-inbox__empty{margin:0;padding-block:var(--hm-space-sm);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}\n"] }]
1857
1913
  }], ctorParameters: () => [] });
1858
1914
 
1915
+ const TOKEN_HEADER = 'x-access-token';
1916
+ const WS_PROTOCOL = 'graphql-transport-ws';
1917
+ const SUBSCRIPTION_ID = '1';
1918
+ // Nenhuma operação leva o agente: o gateway o descobre pelo client do token.
1919
+ const EVENTS_QUERY = `subscription ($conversationId: ID!) {
1920
+ agentEvents(conversationId: $conversationId) { type text tool title ok detail message }
1921
+ }`;
1922
+ const SEND_MUTATION = `mutation ($input: SendAgentMessageInput!) {
1923
+ sendAgentMessage(input: $input) { status message }
1924
+ }`;
1925
+ const RATE_MUTATION = `mutation ($input: RateAgentMessageInput!) {
1926
+ rateAgentMessage(input: $input) { status message }
1927
+ }`;
1928
+ const CONVERSATION_QUERY = `query ($conversationId: ID!) {
1929
+ agentConversation(conversationId: $conversationId) { status data { role text steps { tool title ok detail } } }
1930
+ }`;
1931
+ const AGENT_QUERY = `query { agent { status data { suggestions } } }`;
1932
+ /**
1933
+ * Assina os eventos de uma conversa e devolve a função que encerra a assinatura. Usa o WebSocket do navegador, com o
1934
+ * protocolo do graphql-ws: o shell também atende apps sem GraphQL, que não têm o pacote `graphql-ws` instalado.
1935
+ * `onClosed` avisa que a conexão caiu sem que quem assinou tenha pedido.
1936
+ */
1937
+ function subscribeAgentEvents(url, token, variables, onEvent, onClosed) {
1938
+ const socket = new WebSocket(url, WS_PROTOCOL);
1939
+ let stopped = false;
1940
+ const send = (message) => socket.send(JSON.stringify(message));
1941
+ socket.onopen = () => send({ type: 'connection_init', payload: { [TOKEN_HEADER]: token, headers: { [TOKEN_HEADER]: token } } });
1942
+ socket.onmessage = ({ data }) => {
1943
+ const message = JSON.parse(String(data));
1944
+ if (message.type === 'connection_ack') {
1945
+ send({ id: SUBSCRIPTION_ID, type: 'subscribe', payload: { query: EVENTS_QUERY, variables } });
1946
+ }
1947
+ else if (message.type === 'ping') {
1948
+ send({ type: 'pong' });
1949
+ }
1950
+ else if (message.type === 'next' && message.payload?.data?.agentEvents) {
1951
+ onEvent(message.payload.data.agentEvents);
1952
+ }
1953
+ else if (message.type === 'next' || message.type === 'error' || message.type === 'complete') {
1954
+ // Recusa do gateway (agente de outro client, credencial vencida) ou fim inesperado: não há mais o que esperar.
1955
+ stop();
1956
+ onClosed();
1957
+ }
1958
+ };
1959
+ socket.onclose = () => {
1960
+ if (!stopped) {
1961
+ stopped = true;
1962
+ onClosed();
1963
+ }
1964
+ };
1965
+ function stop() {
1966
+ stopped = true;
1967
+ if (socket.readyState === WebSocket.OPEN)
1968
+ send({ id: SUBSCRIPTION_ID, type: 'complete' });
1969
+ socket.close();
1970
+ }
1971
+ return stop;
1972
+ }
1973
+ /** Envia a mensagem. Falha de rede sobe como exceção; recusa do backend volta no envelope `{ status, message }`. */
1974
+ async function sendAgentMessage(url, token, variables, text) {
1975
+ const response = await fetch(url, {
1976
+ method: 'POST',
1977
+ headers: { 'content-type': 'application/json', [TOKEN_HEADER]: token },
1978
+ body: JSON.stringify({
1979
+ query: SEND_MUTATION,
1980
+ variables: { input: { conversationId: variables.conversationId, text } },
1981
+ }),
1982
+ });
1983
+ const body = await response.json();
1984
+ return body?.data?.sendAgentMessage ?? { status: response.status >= 400 ? response.status : 500, message: '' };
1985
+ }
1986
+ /** Avalia uma resposta, com uma correção opcional. `messageIndex` é a posição dela na conversa guardada. */
1987
+ async function rateAgentMessage(url, token, variables, messageIndex, rating, correction) {
1988
+ const response = await fetch(url, {
1989
+ method: 'POST',
1990
+ headers: { 'content-type': 'application/json', [TOKEN_HEADER]: token },
1991
+ body: JSON.stringify({
1992
+ query: RATE_MUTATION,
1993
+ variables: { input: { conversationId: variables.conversationId, messageIndex, rating, correction } },
1994
+ }),
1995
+ });
1996
+ const body = await response.json();
1997
+ return body?.data?.rateAgentMessage ?? { status: response.status >= 400 ? response.status : 500, message: '' };
1998
+ }
1999
+ /** Perguntas prontas que o agente oferece. */
2000
+ async function fetchAgentSuggestions(url, token) {
2001
+ const response = await fetch(url, {
2002
+ method: 'POST',
2003
+ headers: { 'content-type': 'application/json', [TOKEN_HEADER]: token },
2004
+ body: JSON.stringify({ query: AGENT_QUERY }),
2005
+ });
2006
+ const body = await response.json();
2007
+ return body?.data?.agent?.data?.suggestions ?? [];
2008
+ }
2009
+ /** As perguntas e respostas de uma conversa que já existe; vazia se ela expirou. */
2010
+ async function fetchAgentConversation(url, token, variables) {
2011
+ const response = await fetch(url, {
2012
+ method: 'POST',
2013
+ headers: { 'content-type': 'application/json', [TOKEN_HEADER]: token },
2014
+ body: JSON.stringify({ query: CONVERSATION_QUERY, variables }),
2015
+ });
2016
+ const body = await response.json();
2017
+ return body?.data?.agentConversation?.data ?? [];
2018
+ }
2019
+
2020
+ // Teto para o agente dar sinal de vida; acima do limite de uma execução no backend.
2021
+ const SILENCE_LIMIT_MS = 200_000;
2022
+ const STORAGE_PREFIX = 'spine-ai-agent:conversation:';
2023
+ const PLACEMENT_KEY = 'spine-ai-agent:placement';
2024
+ // O gateway fecha a conexão quando o token vence, e a rede também cai: a resposta continua no backend. Reconecta com o token atual.
2025
+ const RECONNECT_ATTEMPTS = 5;
2026
+ const RECONNECT_DELAY_MS = 1_000;
2027
+ const RECOVERY_POLL_MS = 5_000;
2028
+ function readPlacement() {
2029
+ try {
2030
+ return JSON.parse(readStored(PLACEMENT_KEY) ?? '{}');
2031
+ }
2032
+ catch {
2033
+ return {};
2034
+ }
2035
+ }
2036
+ function readStored(key) {
2037
+ try {
2038
+ return localStorage.getItem(key);
2039
+ }
2040
+ catch {
2041
+ return null;
2042
+ }
2043
+ }
2044
+ function writeStored(key, value) {
2045
+ try {
2046
+ localStorage.setItem(key, value);
2047
+ }
2048
+ catch {
2049
+ // Navegador sem armazenamento: a conversa só não volta depois de recarregar.
2050
+ }
2051
+ }
2052
+ /**
2053
+ * Conversa com o agente do app (`SpineFeatures.aiAgent`). Fica no serviço para sobreviver a fechar e abrir o painel. O id
2054
+ * da conversa fica no navegador, só ele: as mensagens são do backend e voltam de lá ao recarregar a tela, e a chave lá
2055
+ * leva o usuário do token, então quem entrar depois nesse navegador não vê a conversa. A cada mensagem abre uma assinatura, espera o `ready` do gateway,
2056
+ * envia e acompanha até `done` ou `error`.
2057
+ *
2058
+ * Se a conexão cair depois do envio, o backend segue trabalhando: reconecta com o token atual, sem reenviar, e busca a resposta salva, porque os
2059
+ * eventos do intervalo se perderam e a resposta pode até já ter terminado.
2060
+ */
2061
+ class SpineAiAgentService {
2062
+ constructor() {
2063
+ this.spine = inject(SPINE_CONFIG, { optional: true });
2064
+ this.config = this.spine?.features?.aiAgent ?? null;
2065
+ this.auth = inject(AuthorizationService);
2066
+ this.text = inject(SPINE_MESSAGES).aiAgentChat;
2067
+ this.enabled = !!this.config;
2068
+ this.title = this.config?.title || this.text.title;
2069
+ this.theme = inject(ThemeService);
2070
+ this.avatarSources = resolveSpineImage(this.config?.avatar, SPINE_DEFAULT_AGENT_AVATAR);
2071
+ this.thinkingSources = resolveSpineImage(this.config?.avatarThinking, null);
2072
+ this.imageSources = resolveSpineImage(this.config?.image, SPINE_DEFAULT_AGENT_IMAGE);
2073
+ /** Arquivos que não carregaram: saem, em vez de virar imagem quebrada na tela. */
2074
+ this.failedImages = signal(new Set(), ...(ngDevMode ? [{ debugName: "failedImages" }] : /* istanbul ignore next */ []));
2075
+ /** O rosto do agente, para o tema atual. */
2076
+ this.avatar = computed(() => this.pickImage(this.avatarSources), ...(ngDevMode ? [{ debugName: "avatar" }] : /* istanbul ignore next */ []));
2077
+ /** A figura do agente, para o tema atual. */
2078
+ this.image = computed(() => this.pickImage(this.imageSources), ...(ngDevMode ? [{ debugName: "image" }] : /* istanbul ignore next */ []));
2079
+ this.greetingTitle = this.config?.greeting?.title || this.text.greetingTitle;
2080
+ this.greetingText = this.config?.greeting?.text || this.text.greetingText;
2081
+ this.entries = signal([], ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
2082
+ this.running = signal(false, ...(ngDevMode ? [{ debugName: "running" }] : /* istanbul ignore next */ []));
2083
+ /** Perguntas prontas que o agente oferece, mostradas na conversa vazia. */
2084
+ this.suggestions = signal([], ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
2085
+ /** O rosto de agora, que muda enquanto o agente trabalha. */
2086
+ this.avatarNow = computed(() => (this.running() ? this.pickImage(this.thinkingSources) : null) ?? this.avatar(), ...(ngDevMode ? [{ debugName: "avatarNow" }] : /* istanbul ignore next */ []));
2087
+ this.panelOpen = signal(false, ...(ngDevMode ? [{ debugName: "panelOpen" }] : /* istanbul ignore next */ []));
2088
+ /** O balão saiu da tela e virou um ícone no cabeçalho. */
2089
+ this.docked = signal(readPlacement().docked === true, ...(ngDevMode ? [{ debugName: "docked" }] : /* istanbul ignore next */ []));
2090
+ this.position = signal(readPlacement().position ?? null, ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
2091
+ /** A pessoa pediu o painel agora: a conversa recebe o foco. Ao recarregar a tela, não. */
2092
+ this.focusOnOpen = false;
2093
+ // Por app: vários apps podem morar na mesma origem, e o localStorage é dela.
2094
+ this.storageKey = `${STORAGE_PREFIX}${this.spine?.clientId ?? ''}`;
2095
+ this.storedConversation = readStored(this.storageKey);
2096
+ this.conversationId = this.storedConversation ?? this.newConversationId();
2097
+ this.restored = !this.storedConversation;
2098
+ this.nextId = 0;
2099
+ this.stop = null;
2100
+ this.silence = null;
2101
+ this.reconnectTimer = null;
2102
+ this.recoveryTimer = null;
2103
+ /** A mensagem já foi enviada: daqui em diante, a conexão pode cair sem perder a resposta. */
2104
+ this.messageSent = false;
2105
+ this.reconnects = 0;
2106
+ this.resumed = false;
2107
+ /** Mensagens que o backend já tinha guardado antes desta pergunta, para reconhecer a resposta nova. */
2108
+ this.savedBefore = 0;
2109
+ this.suggestionsLoaded = false;
2110
+ }
2111
+ /** Como no logo do header: no escuro vale `dark`, e se ele falhar volta para `light`. */
2112
+ pickImage(sources) {
2113
+ if (!this.config || !sources)
2114
+ return null;
2115
+ const failed = this.failedImages();
2116
+ if (this.theme.current === 'dark' && sources.dark && !failed.has(sources.dark))
2117
+ return sources.dark;
2118
+ return failed.has(sources.light) ? null : sources.light;
2119
+ }
2120
+ imageFailed(src) {
2121
+ if (!this.failedImages().has(src))
2122
+ this.failedImages.update((failed) => new Set(failed).add(src));
2123
+ }
2124
+ togglePanel() {
2125
+ this.focusOnOpen = !this.panelOpen();
2126
+ this.panelOpen.set(this.focusOnOpen);
2127
+ }
2128
+ dock() {
2129
+ this.docked.set(true);
2130
+ this.savePlacement();
2131
+ }
2132
+ undock() {
2133
+ this.docked.set(false);
2134
+ this.savePlacement();
2135
+ }
2136
+ savePlacement() {
2137
+ writeStored(PLACEMENT_KEY, JSON.stringify({ docked: this.docked(), position: this.position() }));
2138
+ }
2139
+ /** Abre o painel e pergunta, para quem escolheu uma sugestão no convite. */
2140
+ askFromGreeting(question) {
2141
+ this.focusOnOpen = true;
2142
+ this.panelOpen.set(true);
2143
+ this.restored = true;
2144
+ this.send(question);
2145
+ }
2146
+ /** Busca as sugestões uma vez. Sem elas o chat funciona igual, então a falha não aparece para a pessoa. */
2147
+ loadSuggestions() {
2148
+ const token = this.auth.getToken();
2149
+ if (!this.config || !token || this.suggestionsLoaded)
2150
+ return;
2151
+ this.suggestionsLoaded = true;
2152
+ fetchAgentSuggestions(this.config.http, token).then((suggestions) => this.suggestions.set(suggestions), () => { this.suggestionsLoaded = false; });
2153
+ }
2154
+ /** Traz de volta a conversa guardada, uma vez. Se falhar, ou se ela expirou, o chat só começa vazio. */
2155
+ restoreConversation() {
2156
+ const token = this.auth.getToken();
2157
+ if (!this.config || !token || this.restored)
2158
+ return;
2159
+ this.restored = true;
2160
+ const conversationId = this.conversationId;
2161
+ fetchAgentConversation(this.config.http, token, { conversationId }).then((messages) => {
2162
+ // A pessoa pode ter começado a escrever antes de a resposta chegar: o que está na tela vence.
2163
+ if (this.entries().length || conversationId !== this.conversationId)
2164
+ return;
2165
+ for (const message of messages) {
2166
+ const id = this.append(message.role, message.text);
2167
+ const steps = this.storedSteps(message.steps);
2168
+ if (steps.length)
2169
+ this.update(id, (entry) => ({ ...entry, steps }));
2170
+ }
2171
+ }, () => { this.restored = false; });
2172
+ }
2173
+ /** Começa outra conversa; a anterior expira sozinha no backend. */
2174
+ reset() {
2175
+ this.finish();
2176
+ this.entries.set([]);
2177
+ this.restored = true;
2178
+ this.conversationId = this.newConversationId();
2179
+ }
2180
+ /** Avalia uma resposta. A posição conta só o que o backend guardou: resposta que falhou não entra no histórico dele. */
2181
+ async rate(entryId, rating, correction = '') {
2182
+ const token = this.auth.getToken();
2183
+ const saved = this.entries().filter((entry) => !entry.error);
2184
+ const index = saved.findIndex((entry) => entry.id === entryId);
2185
+ if (!this.config || !token || index < 1)
2186
+ return false;
2187
+ const variables = { conversationId: this.conversationId };
2188
+ try {
2189
+ const { status } = await rateAgentMessage(this.config.http, token, variables, index, rating, correction);
2190
+ if (status >= 400)
2191
+ return false;
2192
+ }
2193
+ catch {
2194
+ return false;
2195
+ }
2196
+ this.update(entryId, (entry) => ({ ...entry, rating }));
2197
+ return true;
2198
+ }
2199
+ /**
2200
+ * Baixa a conversa da tela em PDF, gerado aqui no navegador: nada sai para o backend. O gerador vem só no clique, para
2201
+ * não pesar em quem nunca baixa. Devolve `false` se não houver o que baixar ou se a geração falhar.
2202
+ */
2203
+ async downloadTranscript() {
2204
+ const entries = this.entries().filter((entry) => entry.text.trim() || entry.steps.length);
2205
+ if (!entries.length)
2206
+ return false;
2207
+ try {
2208
+ const { buildTranscriptPdf, transcriptFileName } = await import('./trismegisto-spine-layout-ai-agent-transcript-BNcgbrV_.mjs');
2209
+ const now = new Date();
2210
+ const person = this.auth.profile?.name;
2211
+ const pdf = buildTranscriptPdf({
2212
+ title: this.title,
2213
+ subtitle: [this.spine?.appName, person, now.toLocaleString()].filter(Boolean).join(' · '),
2214
+ personLabel: person || this.text.you,
2215
+ agentLabel: this.title,
2216
+ queriesLabel: this.text.transcriptQueries,
2217
+ failedLabel: this.text.transcriptStepFailed,
2218
+ pageLabel: this.text.transcriptPage,
2219
+ entries: entries.map((entry) => ({
2220
+ role: entry.role,
2221
+ text: entry.text,
2222
+ error: entry.error,
2223
+ steps: entry.steps.map((step) => ({ title: step.title, detail: step.detail, failed: step.status === 'error' })),
2224
+ })),
2225
+ });
2226
+ const url = URL.createObjectURL(new Blob([pdf.slice().buffer], { type: 'application/pdf' }));
2227
+ const link = document.createElement('a');
2228
+ link.href = url;
2229
+ link.download = transcriptFileName(this.title, this.text.transcriptFile, now);
2230
+ link.click();
2231
+ // O navegador só lê o endereço depois do clique: revogar na hora cancelaria o download em alguns deles.
2232
+ setTimeout(() => URL.revokeObjectURL(url), 10_000);
2233
+ return true;
2234
+ }
2235
+ catch {
2236
+ return false;
2237
+ }
2238
+ }
2239
+ /** Repete a última pergunta depois de um erro. Resposta que falhou não entra no histórico do backend. */
2240
+ retry() {
2241
+ const entries = this.entries();
2242
+ const question = entries[entries.length - 2];
2243
+ if (this.running() || !entries[entries.length - 1]?.error || question?.role !== 'person')
2244
+ return;
2245
+ this.entries.set(entries.slice(0, -2));
2246
+ this.send(question.text);
2247
+ }
2248
+ send(raw) {
2249
+ const text = raw.trim();
2250
+ const token = this.auth.getToken();
2251
+ if (!this.config || !text || this.running())
2252
+ return;
2253
+ this.savedBefore = this.entries().filter((entry) => !entry.error).length;
2254
+ this.append('person', text);
2255
+ const answerId = this.append('agent', '');
2256
+ if (!token) {
2257
+ this.fail(answerId, this.text.sendError);
2258
+ return;
2259
+ }
2260
+ this.running.set(true);
2261
+ this.listen(answerId, { conversationId: this.conversationId }, text);
2262
+ }
2263
+ listen(answerId, variables, question) {
2264
+ const token = this.auth.getToken();
2265
+ if (!this.config || !token) {
2266
+ this.fail(answerId, this.text.connectionError);
2267
+ return;
2268
+ }
2269
+ const http = this.config.http;
2270
+ this.watchSilence(answerId);
2271
+ this.stop = subscribeAgentEvents(this.config.ws, token, variables, (event) => {
2272
+ this.watchSilence(answerId);
2273
+ if (event.type === 'ready') {
2274
+ this.onReady(answerId, variables, question, http, token);
2275
+ }
2276
+ else if (event.type === 'done' && this.resumed) {
2277
+ void this.loadAnswer(answerId, variables, question).finally(() => this.finish());
2278
+ }
2279
+ else {
2280
+ this.apply(answerId, event);
2281
+ }
2282
+ }, () => this.onDisconnected(answerId, variables, question));
2283
+ }
2284
+ onReady(answerId, variables, question, http, token) {
2285
+ if (this.messageSent) {
2286
+ this.reconnects = 0;
2287
+ this.resumed = true;
2288
+ void this.pollAnswer(answerId, variables, question);
2289
+ return;
2290
+ }
2291
+ this.messageSent = true;
2292
+ sendAgentMessage(http, token, variables, question).then(({ status, message }) => { if (status >= 400)
2293
+ this.fail(answerId, message || this.text.sendError); }, () => this.fail(answerId, this.text.sendError));
2294
+ }
2295
+ onDisconnected(answerId, variables, question) {
2296
+ if (!this.running())
2297
+ return;
2298
+ // Antes do envio, ou depois de esgotar as tentativas, ninguém está trabalhando nesta resposta ou não há como acompanhar.
2299
+ if (!this.messageSent || this.reconnects >= RECONNECT_ATTEMPTS) {
2300
+ this.fail(answerId, this.text.connectionError);
2301
+ return;
2302
+ }
2303
+ this.reconnects += 1;
2304
+ this.stop = null;
2305
+ this.reconnectTimer = setTimeout(() => this.listen(answerId, variables, question), RECONNECT_DELAY_MS * this.reconnects);
2306
+ }
2307
+ async pollAnswer(answerId, variables, question) {
2308
+ if (await this.loadAnswer(answerId, variables, question)) {
2309
+ this.finish();
2310
+ }
2311
+ else if (this.running()) {
2312
+ this.recoveryTimer = setTimeout(() => void this.pollAnswer(answerId, variables, question), RECOVERY_POLL_MS);
2313
+ }
2314
+ }
2315
+ /** Traz a resposta salva, se ela já terminou. Serve também para repor o que se perdeu enquanto a conexão estava fora. */
2316
+ async loadAnswer(answerId, variables, question) {
2317
+ const token = this.auth.getToken();
2318
+ if (!this.config || !token)
2319
+ return false;
2320
+ try {
2321
+ const messages = await fetchAgentConversation(this.config.http, token, variables);
2322
+ const answer = messages[messages.length - 1];
2323
+ const asked = messages[messages.length - 2];
2324
+ if (answer?.role !== 'agent' || asked?.text !== question || messages.length <= this.savedBefore)
2325
+ return false;
2326
+ const steps = this.storedSteps(answer.steps);
2327
+ this.update(answerId, (entry) => ({ ...entry, text: answer.text, steps: steps.length ? steps : entry.steps }));
2328
+ return true;
2329
+ }
2330
+ catch {
2331
+ return false;
2332
+ }
2333
+ }
2334
+ storedSteps(steps) {
2335
+ return (steps ?? []).map((step) => ({
2336
+ id: ++this.nextId, tool: step.tool, title: step.title, status: step.ok ? 'done' : 'error', detail: step.detail ?? undefined,
2337
+ }));
2338
+ }
2339
+ newConversationId() {
2340
+ const id = crypto.randomUUID();
2341
+ writeStored(this.storageKey, id);
2342
+ return id;
2343
+ }
2344
+ apply(answerId, event) {
2345
+ switch (event.type) {
2346
+ case 'text_delta':
2347
+ this.update(answerId, (entry) => ({ ...entry, text: entry.text + (event.text ?? '') }));
2348
+ break;
2349
+ case 'text_replace':
2350
+ this.update(answerId, (entry) => ({ ...entry, text: event.text ?? '' }));
2351
+ break;
2352
+ case 'tool_started': {
2353
+ const tool = event.tool ?? '';
2354
+ const step = { id: ++this.nextId, tool, title: event.title || tool, status: 'running' };
2355
+ this.update(answerId, (entry) => ({ ...entry, steps: [...entry.steps, step] }));
2356
+ break;
2357
+ }
2358
+ case 'tool_finished':
2359
+ this.finishStep(answerId, event.tool ?? '', event.ok === false ? 'error' : 'done', event.detail ?? undefined);
2360
+ break;
2361
+ case 'done':
2362
+ this.finish();
2363
+ break;
2364
+ case 'error':
2365
+ this.fail(answerId, event.message || this.text.sendError);
2366
+ break;
2367
+ }
2368
+ }
2369
+ /** A mesma ferramenta pode rodar mais de uma vez: encerra a chamada mais antiga que ainda está em andamento. */
2370
+ finishStep(answerId, tool, status, detail) {
2371
+ this.update(answerId, (entry) => {
2372
+ const index = entry.steps.findIndex((step) => step.tool === tool && step.status === 'running');
2373
+ return { ...entry, steps: entry.steps.map((step, i) => (i === index ? { ...step, status, detail } : step)) };
2374
+ });
2375
+ }
2376
+ fail(answerId, message) {
2377
+ this.update(answerId, (entry) => ({
2378
+ ...entry,
2379
+ text: message,
2380
+ error: true,
2381
+ steps: entry.steps.map((step) => (step.status === 'running' ? { ...step, status: 'error' } : step)),
2382
+ }));
2383
+ this.finish();
2384
+ }
2385
+ finish() {
2386
+ if (this.silence)
2387
+ clearTimeout(this.silence);
2388
+ if (this.reconnectTimer)
2389
+ clearTimeout(this.reconnectTimer);
2390
+ if (this.recoveryTimer)
2391
+ clearTimeout(this.recoveryTimer);
2392
+ this.silence = this.reconnectTimer = this.recoveryTimer = null;
2393
+ this.messageSent = this.resumed = false;
2394
+ this.reconnects = 0;
2395
+ this.running.set(false);
2396
+ const stop = this.stop;
2397
+ this.stop = null;
2398
+ stop?.();
2399
+ }
2400
+ watchSilence(answerId) {
2401
+ if (this.silence)
2402
+ clearTimeout(this.silence);
2403
+ this.silence = setTimeout(() => this.fail(answerId, this.text.connectionError), SILENCE_LIMIT_MS);
2404
+ }
2405
+ append(role, text) {
2406
+ const id = ++this.nextId;
2407
+ this.entries.update((entries) => [...entries, { id, role, text, steps: [], error: false }]);
2408
+ return id;
2409
+ }
2410
+ update(id, change) {
2411
+ this.entries.update((entries) => entries.map((entry) => (entry.id === id ? change(entry) : entry)));
2412
+ }
2413
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2414
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentService, providedIn: 'root' }); }
2415
+ }
2416
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentService, decorators: [{
2417
+ type: Injectable,
2418
+ args: [{ providedIn: 'root' }]
2419
+ }] });
2420
+
2421
+ const SPINE_AGENT_DOCK_CLASS = 'spine-agent-dock';
2422
+ /** O agente no cabeçalho, ao lado do sino: só aparece depois que a pessoa tira o balão da tela. */
2423
+ class SpineAiAgentDockButtonComponent {
2424
+ constructor() {
2425
+ this.agent = inject(SpineAiAgentService);
2426
+ this.text = inject(SPINE_MESSAGES).aiAgentChat;
2427
+ this.dockClass = SPINE_AGENT_DOCK_CLASS;
2428
+ }
2429
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentDockButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2430
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineAiAgentDockButtonComponent, isStandalone: true, selector: "spine-ai-agent-dock-button", ngImport: i0, template: `
2431
+ @if (agent.enabled && agent.docked()) {
2432
+ <hm-icon-button
2433
+ [class]="dockClass"
2434
+ emphasis="subtle"
2435
+ icon="chat-bubble"
2436
+ [label]="agent.panelOpen() ? text.close : agent.title"
2437
+ (clicked)="agent.togglePanel()"
2438
+ />
2439
+ }
2440
+ `, isInline: true, dependencies: [{ kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2441
+ }
2442
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentDockButtonComponent, decorators: [{
2443
+ type: Component,
2444
+ args: [{
2445
+ selector: 'spine-ai-agent-dock-button',
2446
+ imports: [IconButtonComponent],
2447
+ changeDetection: ChangeDetectionStrategy.OnPush,
2448
+ template: `
2449
+ @if (agent.enabled && agent.docked()) {
2450
+ <hm-icon-button
2451
+ [class]="dockClass"
2452
+ emphasis="subtle"
2453
+ icon="chat-bubble"
2454
+ [label]="agent.panelOpen() ? text.close : agent.title"
2455
+ (clicked)="agent.togglePanel()"
2456
+ />
2457
+ }
2458
+ `,
2459
+ }]
2460
+ }] });
2461
+
1859
2462
  /** Tempo do aviso de falha na tela (segundos). */
1860
2463
  const LOGOUT_FAILED_SECONDS = 8;
1861
2464
  /**
@@ -2415,7 +3018,7 @@ class HeaderComponent {
2415
3018
  catch { /* storage cheio/indisponível */ }
2416
3019
  }
2417
3020
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HeaderComponent, deps: [{ token: i1.AuthorizationService }, { token: i2$1.ThemeService }, { token: i2$1.ThemePresetService }, { token: i2$1.DensityService }, { token: i1.ViewService }, { token: i3.Router }, { token: ShortcutService }, { token: SPINE_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
2418
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: HeaderComponent, isStandalone: true, selector: "spine-header", ngImport: i0, template: "<hm-header [class.header-blocked]=\"headerBlocked\" (searchRequested)=\"onSearchRequested()\">\n <div *ngIf=\"headerBlocked\" class=\"header-overlay\"></div>\n\n <spine-app-launcher></spine-app-launcher>\n\n <!-- Marca: logo do app (SpineConfig.logo, padr\u00E3o assets/images/logo.svg) + SpineConfig.appName,\n levando \u00E0 primeira rota naveg\u00E1vel. Ver SpineBrandComponent. -->\n <spine-brand [homeRoute]=\"homeRoute\" [label]=\"text.home\" />\n\n <div actions class=\"h-right\">\n <!-- central de notifica\u00E7\u00F5es: o sino, com as novidades das fontes registradas (sem fonte, n\u00E3o aparece) -->\n <spine-inbox-button />\n\n <!-- tema claro/escuro -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setTheme(themeService.current == 'light' ? 'dark' : 'light')\"\n [icon]=\"themeService.current == 'light' ? 'moon' : 'sun'\"\n [label]=\"themeService.current == 'light' ? text.themeDark : text.themeLight\">\n </hm-icon-button>\n\n <!-- densidade -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setDensity()\"\n [icon]=\"isCompactDensity ? 'density-comfortable' : 'density-compact'\"\n [label]=\"isCompactDensity ? text.densityComfortable : text.densityCompact\">\n </hm-icon-button>\n\n <!-- Avaliar o {app} (SpineFeatures.ratings): a estrela abre o di\u00E1logo e fica cheia depois de avaliar. Toma o\n lugar do bot\u00E3o de tela cheia (o atalho de teclado continua); sem avalia\u00E7\u00E3o, a tela cheia volta aqui. -->\n @if (showFullscreenButton) {\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"toggleFullscreen()\"\n [icon]=\"isFullscreen ? 'shrink' : 'resize'\"\n [label]=\"isFullscreen ? text.exitFullscreen : text.fullscreen\">\n </hm-icon-button>\n } @else {\n <spine-rate-app-button />\n }\n\n <span class=\"h-divider\" aria-hidden=\"true\"></span>\n\n <!-- perfil: crach\u00E1, ajuda, tema e sair ficam dentro do menu -->\n <hm-menu-button class=\"h-profile\" [label]=\"profile.name ?? text.user\" align=\"end\" [items]=\"profileMenuItems\" (chosen)=\"onProfileMenuChosen($event)\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\"></hm-account-avatar>\n <span class=\"h-profile-info\">\n <span class=\"h-profile-name\">{{ profile.name ?? text.user }}</span>\n <span class=\"h-profile-role\" *ngIf=\"badge\">{{ badge?.name }}</span>\n </span>\n </hm-menu-button>\n </div>\n</hm-header>\n\n<!-- Busca global: paleta ancorada logo abaixo do cabe\u00E7alho. Campo vazio = favoritos, abertos h\u00E1 pouco e\n m\u00F3dulos; com texto, busca em todas as telas. Setas/Enter navegam, Esc fecha. -->\n<hm-command-palette\n [(open)]=\"quickOpen\"\n [commands]=\"searchCommands\"\n [browseGroups]=\"searchBrowse\"\n [groupIcons]=\"searchIcons\"\n [label]=\"text.search\"\n [placeholder]=\"text.searchPlaceholder\"\n/>\n\n<!-- A janela de avalia\u00E7\u00E3o (flutuante, canto inferior direito): \"Avaliar o {app}\" da estrela do cabe\u00E7alho e de\n Configura\u00E7\u00F5es \u203A Sobre, e \"Avaliar esta tela\" da barra da p\u00E1gina (SpineFeatures.ratings). S\u00F3 desenha com o recurso\n ligado e aberta. -->\n<spine-rating-window />\n", styles: ["@charset \"UTF-8\";:host{display:contents}.header-overlay{position:absolute;inset:0;z-index:9999;cursor:not-allowed}.h-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:40px;min-width:40px;padding:0 10px;border:none;background:transparent;cursor:pointer;border-radius:9px;text-decoration:none;color:var(--hm-navbar-text);outline:none}.h-btn:focus-visible,.h-btn:hover{background:var(--hm-navbar-hover)}.h-btn.active{background:var(--hm-navbar-hover)}.h-right{display:flex;align-items:center;gap:var(--hm-header-action-gap)}.h-divider{width:1px;height:24px;margin:0 4px;background:var(--hm-navbar-hover)}.h-profile{display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 4px 0 2px}.h-profile hm-account-avatar{background:color-mix(in srgb,var(--hm-navbar-text) 18%,transparent);color:var(--hm-navbar-text)}.h-profile-info{display:flex;flex-direction:column;min-width:0;max-width:140px;line-height:1.2;text-align:left}.h-profile-name{font-size:var(--hm-font-size-label);font-weight:var(--hm-font-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.h-profile-role{color:color-mix(in srgb,var(--hm-navbar-text) 65%,transparent);font-size:var(--hm-font-size-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: HeaderComponent$1, selector: "hm-header", inputs: ["searchPlaceholder", "searchLabel"], outputs: ["searchRequested"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: MenuButtonComponent, selector: "hm-menu-button", inputs: ["label", "icon", "items", "showArrow", "align", "side", "size", "shape", "layer", "closeOnCheck", "disabled"], outputs: ["chosen", "checkedChange"] }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: CommandPaletteComponent, selector: "hm-command-palette", inputs: ["open", "commands", "shortcut", "label", "placeholder", "recentLimit", "recentIds", "closeOnRun", "browseGroups", "groupIcons"], outputs: ["openChange", "recentIdsChange", "executed"] }, { kind: "component", type: AppLauncherComponent, selector: "spine-app-launcher" }, { kind: "component", type: SpineBrandComponent, selector: "spine-brand", inputs: ["homeRoute", "label"] }, { kind: "component", type: SpineRatingWindowComponent, selector: "spine-rating-window" }, { kind: "component", type: SpineRateAppButtonComponent, selector: "spine-rate-app-button" }, { kind: "component", type: SpineInboxButtonComponent, selector: "spine-inbox-button" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3021
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: HeaderComponent, isStandalone: true, selector: "spine-header", ngImport: i0, template: "<hm-header [class.header-blocked]=\"headerBlocked\" (searchRequested)=\"onSearchRequested()\">\n <div *ngIf=\"headerBlocked\" class=\"header-overlay\"></div>\n\n <spine-app-launcher></spine-app-launcher>\n\n <!-- Marca: logo do app (SpineConfig.logo, padr\u00E3o assets/images/logo.svg) + SpineConfig.appName,\n levando \u00E0 primeira rota naveg\u00E1vel. Ver SpineBrandComponent. -->\n <spine-brand [homeRoute]=\"homeRoute\" [label]=\"text.home\" />\n\n <div actions class=\"h-right\">\n <!-- central de notifica\u00E7\u00F5es: o sino, com as novidades das fontes registradas (sem fonte, n\u00E3o aparece) -->\n <spine-inbox-button />\n\n <!-- o agente de IA, depois que a pessoa tira o bal\u00E3o da tela -->\n <spine-ai-agent-dock-button />\n\n <!-- tema claro/escuro -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setTheme(themeService.current == 'light' ? 'dark' : 'light')\"\n [icon]=\"themeService.current == 'light' ? 'moon' : 'sun'\"\n [label]=\"themeService.current == 'light' ? text.themeDark : text.themeLight\">\n </hm-icon-button>\n\n <!-- densidade -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setDensity()\"\n [icon]=\"isCompactDensity ? 'density-comfortable' : 'density-compact'\"\n [label]=\"isCompactDensity ? text.densityComfortable : text.densityCompact\">\n </hm-icon-button>\n\n <!-- Avaliar o {app} (SpineFeatures.ratings): a estrela abre o di\u00E1logo e fica cheia depois de avaliar. Toma o\n lugar do bot\u00E3o de tela cheia (o atalho de teclado continua); sem avalia\u00E7\u00E3o, a tela cheia volta aqui. -->\n @if (showFullscreenButton) {\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"toggleFullscreen()\"\n [icon]=\"isFullscreen ? 'shrink' : 'resize'\"\n [label]=\"isFullscreen ? text.exitFullscreen : text.fullscreen\">\n </hm-icon-button>\n } @else {\n <spine-rate-app-button />\n }\n\n <span class=\"h-divider\" aria-hidden=\"true\"></span>\n\n <!-- perfil: crach\u00E1, ajuda, tema e sair ficam dentro do menu -->\n <hm-menu-button class=\"h-profile\" [label]=\"profile.name ?? text.user\" align=\"end\" [items]=\"profileMenuItems\" (chosen)=\"onProfileMenuChosen($event)\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\"></hm-account-avatar>\n <span class=\"h-profile-info\">\n <span class=\"h-profile-name\">{{ profile.name ?? text.user }}</span>\n <span class=\"h-profile-role\" *ngIf=\"badge\">{{ badge?.name }}</span>\n </span>\n </hm-menu-button>\n </div>\n</hm-header>\n\n<!-- Busca global: paleta ancorada logo abaixo do cabe\u00E7alho. Campo vazio = favoritos, abertos h\u00E1 pouco e\n m\u00F3dulos; com texto, busca em todas as telas. Setas/Enter navegam, Esc fecha. -->\n<hm-command-palette\n [(open)]=\"quickOpen\"\n [commands]=\"searchCommands\"\n [browseGroups]=\"searchBrowse\"\n [groupIcons]=\"searchIcons\"\n [label]=\"text.search\"\n [placeholder]=\"text.searchPlaceholder\"\n/>\n\n<!-- A janela de avalia\u00E7\u00E3o (flutuante, canto inferior direito): \"Avaliar o {app}\" da estrela do cabe\u00E7alho e de\n Configura\u00E7\u00F5es \u203A Sobre, e \"Avaliar esta tela\" da barra da p\u00E1gina (SpineFeatures.ratings). S\u00F3 desenha com o recurso\n ligado e aberta. -->\n<spine-rating-window />\n", styles: ["@charset \"UTF-8\";:host{display:contents}.header-overlay{position:absolute;inset:0;z-index:9999;cursor:not-allowed}.h-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:40px;min-width:40px;padding:0 10px;border:none;background:transparent;cursor:pointer;border-radius:9px;text-decoration:none;color:var(--hm-navbar-text);outline:none}.h-btn:focus-visible,.h-btn:hover{background:var(--hm-navbar-hover)}.h-btn.active{background:var(--hm-navbar-hover)}.h-right{display:flex;align-items:center;gap:var(--hm-header-action-gap)}.h-divider{width:1px;height:24px;margin:0 4px;background:var(--hm-navbar-hover)}.h-profile{display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 4px 0 2px}.h-profile hm-account-avatar{background:color-mix(in srgb,var(--hm-navbar-text) 18%,transparent);color:var(--hm-navbar-text)}.h-profile-info{display:flex;flex-direction:column;min-width:0;max-width:140px;line-height:1.2;text-align:left}.h-profile-name{font-size:var(--hm-font-size-label);font-weight:var(--hm-font-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.h-profile-role{color:color-mix(in srgb,var(--hm-navbar-text) 65%,transparent);font-size:var(--hm-font-size-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: HeaderComponent$1, selector: "hm-header", inputs: ["searchPlaceholder", "searchLabel"], outputs: ["searchRequested"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: MenuButtonComponent, selector: "hm-menu-button", inputs: ["label", "icon", "items", "showArrow", "align", "side", "size", "shape", "layer", "closeOnCheck", "disabled"], outputs: ["chosen", "checkedChange"] }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: CommandPaletteComponent, selector: "hm-command-palette", inputs: ["open", "commands", "shortcut", "label", "placeholder", "recentLimit", "recentIds", "closeOnRun", "browseGroups", "groupIcons"], outputs: ["openChange", "recentIdsChange", "executed"] }, { kind: "component", type: AppLauncherComponent, selector: "spine-app-launcher" }, { kind: "component", type: SpineBrandComponent, selector: "spine-brand", inputs: ["homeRoute", "label"] }, { kind: "component", type: SpineRatingWindowComponent, selector: "spine-rating-window" }, { kind: "component", type: SpineRateAppButtonComponent, selector: "spine-rate-app-button" }, { kind: "component", type: SpineInboxButtonComponent, selector: "spine-inbox-button" }, { kind: "component", type: SpineAiAgentDockButtonComponent, selector: "spine-ai-agent-dock-button" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2419
3022
  }
2420
3023
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HeaderComponent, decorators: [{
2421
3024
  type: Component,
@@ -2423,7 +3026,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
2423
3026
  CommonModule,
2424
3027
  HeaderComponent$1, IconButtonComponent, IconComponent, MenuButtonComponent, AccountAvatarComponent, CommandPaletteComponent,
2425
3028
  AppLauncherComponent, SpineBrandComponent, SpineRatingWindowComponent, SpineRateAppButtonComponent, SpineInboxButtonComponent,
2426
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<hm-header [class.header-blocked]=\"headerBlocked\" (searchRequested)=\"onSearchRequested()\">\n <div *ngIf=\"headerBlocked\" class=\"header-overlay\"></div>\n\n <spine-app-launcher></spine-app-launcher>\n\n <!-- Marca: logo do app (SpineConfig.logo, padr\u00E3o assets/images/logo.svg) + SpineConfig.appName,\n levando \u00E0 primeira rota naveg\u00E1vel. Ver SpineBrandComponent. -->\n <spine-brand [homeRoute]=\"homeRoute\" [label]=\"text.home\" />\n\n <div actions class=\"h-right\">\n <!-- central de notifica\u00E7\u00F5es: o sino, com as novidades das fontes registradas (sem fonte, n\u00E3o aparece) -->\n <spine-inbox-button />\n\n <!-- tema claro/escuro -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setTheme(themeService.current == 'light' ? 'dark' : 'light')\"\n [icon]=\"themeService.current == 'light' ? 'moon' : 'sun'\"\n [label]=\"themeService.current == 'light' ? text.themeDark : text.themeLight\">\n </hm-icon-button>\n\n <!-- densidade -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setDensity()\"\n [icon]=\"isCompactDensity ? 'density-comfortable' : 'density-compact'\"\n [label]=\"isCompactDensity ? text.densityComfortable : text.densityCompact\">\n </hm-icon-button>\n\n <!-- Avaliar o {app} (SpineFeatures.ratings): a estrela abre o di\u00E1logo e fica cheia depois de avaliar. Toma o\n lugar do bot\u00E3o de tela cheia (o atalho de teclado continua); sem avalia\u00E7\u00E3o, a tela cheia volta aqui. -->\n @if (showFullscreenButton) {\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"toggleFullscreen()\"\n [icon]=\"isFullscreen ? 'shrink' : 'resize'\"\n [label]=\"isFullscreen ? text.exitFullscreen : text.fullscreen\">\n </hm-icon-button>\n } @else {\n <spine-rate-app-button />\n }\n\n <span class=\"h-divider\" aria-hidden=\"true\"></span>\n\n <!-- perfil: crach\u00E1, ajuda, tema e sair ficam dentro do menu -->\n <hm-menu-button class=\"h-profile\" [label]=\"profile.name ?? text.user\" align=\"end\" [items]=\"profileMenuItems\" (chosen)=\"onProfileMenuChosen($event)\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\"></hm-account-avatar>\n <span class=\"h-profile-info\">\n <span class=\"h-profile-name\">{{ profile.name ?? text.user }}</span>\n <span class=\"h-profile-role\" *ngIf=\"badge\">{{ badge?.name }}</span>\n </span>\n </hm-menu-button>\n </div>\n</hm-header>\n\n<!-- Busca global: paleta ancorada logo abaixo do cabe\u00E7alho. Campo vazio = favoritos, abertos h\u00E1 pouco e\n m\u00F3dulos; com texto, busca em todas as telas. Setas/Enter navegam, Esc fecha. -->\n<hm-command-palette\n [(open)]=\"quickOpen\"\n [commands]=\"searchCommands\"\n [browseGroups]=\"searchBrowse\"\n [groupIcons]=\"searchIcons\"\n [label]=\"text.search\"\n [placeholder]=\"text.searchPlaceholder\"\n/>\n\n<!-- A janela de avalia\u00E7\u00E3o (flutuante, canto inferior direito): \"Avaliar o {app}\" da estrela do cabe\u00E7alho e de\n Configura\u00E7\u00F5es \u203A Sobre, e \"Avaliar esta tela\" da barra da p\u00E1gina (SpineFeatures.ratings). S\u00F3 desenha com o recurso\n ligado e aberta. -->\n<spine-rating-window />\n", styles: ["@charset \"UTF-8\";:host{display:contents}.header-overlay{position:absolute;inset:0;z-index:9999;cursor:not-allowed}.h-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:40px;min-width:40px;padding:0 10px;border:none;background:transparent;cursor:pointer;border-radius:9px;text-decoration:none;color:var(--hm-navbar-text);outline:none}.h-btn:focus-visible,.h-btn:hover{background:var(--hm-navbar-hover)}.h-btn.active{background:var(--hm-navbar-hover)}.h-right{display:flex;align-items:center;gap:var(--hm-header-action-gap)}.h-divider{width:1px;height:24px;margin:0 4px;background:var(--hm-navbar-hover)}.h-profile{display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 4px 0 2px}.h-profile hm-account-avatar{background:color-mix(in srgb,var(--hm-navbar-text) 18%,transparent);color:var(--hm-navbar-text)}.h-profile-info{display:flex;flex-direction:column;min-width:0;max-width:140px;line-height:1.2;text-align:left}.h-profile-name{font-size:var(--hm-font-size-label);font-weight:var(--hm-font-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.h-profile-role{color:color-mix(in srgb,var(--hm-navbar-text) 65%,transparent);font-size:var(--hm-font-size-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
3029
+ SpineAiAgentDockButtonComponent,
3030
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<hm-header [class.header-blocked]=\"headerBlocked\" (searchRequested)=\"onSearchRequested()\">\n <div *ngIf=\"headerBlocked\" class=\"header-overlay\"></div>\n\n <spine-app-launcher></spine-app-launcher>\n\n <!-- Marca: logo do app (SpineConfig.logo, padr\u00E3o assets/images/logo.svg) + SpineConfig.appName,\n levando \u00E0 primeira rota naveg\u00E1vel. Ver SpineBrandComponent. -->\n <spine-brand [homeRoute]=\"homeRoute\" [label]=\"text.home\" />\n\n <div actions class=\"h-right\">\n <!-- central de notifica\u00E7\u00F5es: o sino, com as novidades das fontes registradas (sem fonte, n\u00E3o aparece) -->\n <spine-inbox-button />\n\n <!-- o agente de IA, depois que a pessoa tira o bal\u00E3o da tela -->\n <spine-ai-agent-dock-button />\n\n <!-- tema claro/escuro -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setTheme(themeService.current == 'light' ? 'dark' : 'light')\"\n [icon]=\"themeService.current == 'light' ? 'moon' : 'sun'\"\n [label]=\"themeService.current == 'light' ? text.themeDark : text.themeLight\">\n </hm-icon-button>\n\n <!-- densidade -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setDensity()\"\n [icon]=\"isCompactDensity ? 'density-comfortable' : 'density-compact'\"\n [label]=\"isCompactDensity ? text.densityComfortable : text.densityCompact\">\n </hm-icon-button>\n\n <!-- Avaliar o {app} (SpineFeatures.ratings): a estrela abre o di\u00E1logo e fica cheia depois de avaliar. Toma o\n lugar do bot\u00E3o de tela cheia (o atalho de teclado continua); sem avalia\u00E7\u00E3o, a tela cheia volta aqui. -->\n @if (showFullscreenButton) {\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"toggleFullscreen()\"\n [icon]=\"isFullscreen ? 'shrink' : 'resize'\"\n [label]=\"isFullscreen ? text.exitFullscreen : text.fullscreen\">\n </hm-icon-button>\n } @else {\n <spine-rate-app-button />\n }\n\n <span class=\"h-divider\" aria-hidden=\"true\"></span>\n\n <!-- perfil: crach\u00E1, ajuda, tema e sair ficam dentro do menu -->\n <hm-menu-button class=\"h-profile\" [label]=\"profile.name ?? text.user\" align=\"end\" [items]=\"profileMenuItems\" (chosen)=\"onProfileMenuChosen($event)\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\"></hm-account-avatar>\n <span class=\"h-profile-info\">\n <span class=\"h-profile-name\">{{ profile.name ?? text.user }}</span>\n <span class=\"h-profile-role\" *ngIf=\"badge\">{{ badge?.name }}</span>\n </span>\n </hm-menu-button>\n </div>\n</hm-header>\n\n<!-- Busca global: paleta ancorada logo abaixo do cabe\u00E7alho. Campo vazio = favoritos, abertos h\u00E1 pouco e\n m\u00F3dulos; com texto, busca em todas as telas. Setas/Enter navegam, Esc fecha. -->\n<hm-command-palette\n [(open)]=\"quickOpen\"\n [commands]=\"searchCommands\"\n [browseGroups]=\"searchBrowse\"\n [groupIcons]=\"searchIcons\"\n [label]=\"text.search\"\n [placeholder]=\"text.searchPlaceholder\"\n/>\n\n<!-- A janela de avalia\u00E7\u00E3o (flutuante, canto inferior direito): \"Avaliar o {app}\" da estrela do cabe\u00E7alho e de\n Configura\u00E7\u00F5es \u203A Sobre, e \"Avaliar esta tela\" da barra da p\u00E1gina (SpineFeatures.ratings). S\u00F3 desenha com o recurso\n ligado e aberta. -->\n<spine-rating-window />\n", styles: ["@charset \"UTF-8\";:host{display:contents}.header-overlay{position:absolute;inset:0;z-index:9999;cursor:not-allowed}.h-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:40px;min-width:40px;padding:0 10px;border:none;background:transparent;cursor:pointer;border-radius:9px;text-decoration:none;color:var(--hm-navbar-text);outline:none}.h-btn:focus-visible,.h-btn:hover{background:var(--hm-navbar-hover)}.h-btn.active{background:var(--hm-navbar-hover)}.h-right{display:flex;align-items:center;gap:var(--hm-header-action-gap)}.h-divider{width:1px;height:24px;margin:0 4px;background:var(--hm-navbar-hover)}.h-profile{display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 4px 0 2px}.h-profile hm-account-avatar{background:color-mix(in srgb,var(--hm-navbar-text) 18%,transparent);color:var(--hm-navbar-text)}.h-profile-info{display:flex;flex-direction:column;min-width:0;max-width:140px;line-height:1.2;text-align:left}.h-profile-name{font-size:var(--hm-font-size-label);font-weight:var(--hm-font-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.h-profile-role{color:color-mix(in srgb,var(--hm-navbar-text) 65%,transparent);font-size:var(--hm-font-size-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
2427
3031
  }], ctorParameters: () => [{ type: i1.AuthorizationService }, { type: i2$1.ThemeService }, { type: i2$1.ThemePresetService }, { type: i2$1.DensityService }, { type: i1.ViewService }, { type: i3.Router }, { type: ShortcutService }, { type: undefined, decorators: [{
2428
3032
  type: Optional
2429
3033
  }, {
@@ -4690,13 +5294,13 @@ function buildMcpConfig(servers, token, write, origin) {
4690
5294
  return JSON.stringify({ mcpServers }, null, 2);
4691
5295
  }
4692
5296
  /**
4693
- * Sessões › "Conectar um agente de IA" (`SpineFeatures.aiAgent`): em um passo, cria um token de aplicação pessoal
5297
+ * Sessões › "Conectar um agente de IA" (`SpineFeatures.mcpConnect`): em um passo, cria um token de aplicação pessoal
4694
5298
  * (`x-app-token`, a mesma API de "Tokens de aplicação") e monta a configuração MCP pronta para o Claude Code, com os
4695
5299
  * servidores que o app declarou e os escopos do acesso escolhido (só leitura ou leitura e escrita). O token aparece
4696
5300
  * uma única vez, dentro da configuração; nunca vai para o console nem para o armazenamento do navegador, e some ao
4697
5301
  * concluir ou fechar o painel.
4698
5302
  */
4699
- class AiAgentComponent {
5303
+ class McpConnectComponent {
4700
5304
  constructor() {
4701
5305
  this.messages = inject(SPINE_MESSAGES);
4702
5306
  this.t = this.messages.accountFeatures;
@@ -4706,7 +5310,7 @@ class AiAgentComponent {
4706
5310
  this.toast = inject(HermesToastService);
4707
5311
  this.preferences = inject(SpinePreferencesService);
4708
5312
  this.available = this.service.available;
4709
- this.servers = inject(SPINE_CONFIG, { optional: true })?.features?.aiAgent?.servers ?? [];
5313
+ this.servers = inject(SPINE_CONFIG, { optional: true })?.features?.mcpConnect?.servers ?? [];
4710
5314
  this.open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
4711
5315
  this.starting = signal(false, ...(ngDevMode ? [{ debugName: "starting" }] : /* istanbul ignore next */ []));
4712
5316
  this.submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : /* istanbul ignore next */ []));
@@ -4721,8 +5325,8 @@ class AiAgentComponent {
4721
5325
  this.noBadges = computed(() => !this.badges().length, ...(ngDevMode ? [{ debugName: "noBadges" }] : /* istanbul ignore next */ []));
4722
5326
  this.canSubmit = computed(() => !!this.selected().size && !!this.badgeId() && !this.submitting(), ...(ngDevMode ? [{ debugName: "canSubmit" }] : /* istanbul ignore next */ []));
4723
5327
  this.accessOptions = computed(() => [
4724
- { value: 'read', label: this.t.aiAgentReadOnly },
4725
- { value: 'write', label: this.t.aiAgentReadWrite },
5328
+ { value: 'read', label: this.t.mcpConnectReadOnly },
5329
+ { value: 'write', label: this.t.mcpConnectReadWrite },
4726
5330
  ], ...(ngDevMode ? [{ debugName: "accessOptions" }] : /* istanbul ignore next */ []));
4727
5331
  this.badgeOptions = computed(() => this.badges().map((b) => ({ value: b.id, label: b.name })), ...(ngDevMode ? [{ debugName: "badgeOptions" }] : /* istanbul ignore next */ []));
4728
5332
  this.validityOptions = computed(() => [
@@ -4734,7 +5338,7 @@ class AiAgentComponent {
4734
5338
  }
4735
5339
  serverLabel(server) {
4736
5340
  const label = server.label ?? server.name;
4737
- return server.writeScopes ? label : formatMessage(this.t.aiAgentReadOnlyServer, { name: label });
5341
+ return server.writeScopes ? label : formatMessage(this.t.mcpConnectReadOnlyServer, { name: label });
4738
5342
  }
4739
5343
  toggleServer(name, checked) {
4740
5344
  const next = new Set(this.selected());
@@ -4772,7 +5376,7 @@ class AiAgentComponent {
4772
5376
  const date = new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeZone: this.preferences.effectiveTimeZone() }).format(new Date());
4773
5377
  const created = await this.service.create({
4774
5378
  badge_id: this.badgeId(),
4775
- name: formatMessage(this.t.aiAgentTokenName, { date }).slice(0, 80),
5379
+ name: formatMessage(this.t.mcpConnectTokenName, { date }).slice(0, 80),
4776
5380
  validity_days: Number(this.validity()),
4777
5381
  });
4778
5382
  const servers = this.servers.filter((s) => this.selected().has(s.name));
@@ -4785,7 +5389,7 @@ class AiAgentComponent {
4785
5389
  this.open.set(false);
4786
5390
  }
4787
5391
  catch (err) {
4788
- this.toast.error(errorDetail(err) || this.t.aiAgentCreateError);
5392
+ this.toast.error(errorDetail(err) || this.t.mcpConnectCreateError);
4789
5393
  }
4790
5394
  finally {
4791
5395
  this.submitting.set(false);
@@ -4797,7 +5401,7 @@ class AiAgentComponent {
4797
5401
  return;
4798
5402
  try {
4799
5403
  await navigator.clipboard.writeText(config);
4800
- this.toast.success(this.t.aiAgentCopied);
5404
+ this.toast.success(this.t.mcpConnectCopied);
4801
5405
  }
4802
5406
  catch {
4803
5407
  this.toast.error(this.tokens.copyFailed);
@@ -4806,23 +5410,23 @@ class AiAgentComponent {
4806
5410
  done() {
4807
5411
  this.result.set(null);
4808
5412
  }
4809
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AiAgentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4810
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AiAgentComponent, isStandalone: true, selector: "spine-ai-agent", ngImport: i0, template: `
5413
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: McpConnectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5414
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: McpConnectComponent, isStandalone: true, selector: "spine-mcp-connect", ngImport: i0, template: `
4811
5415
  @if (!available) {
4812
- <p class="sp-account__note">{{ t.aiAgentUnavailable }}</p>
5416
+ <p class="sp-account__note">{{ t.mcpConnectUnavailable }}</p>
4813
5417
  } @else if (result(); as r) {
4814
5418
  <div class="sp-account__panel sp-account__panel--success" role="status">
4815
5419
  <div class="sp-account__panel-head">
4816
5420
  <hm-icon shape="check-circle" />
4817
5421
  <div class="sp-account__panel-text">
4818
- <p class="sp-account__title">{{ t.aiAgentResult }}</p>
4819
- <p class="sp-account__note">{{ fmt(t.aiAgentResultHint, { date: r.expires }) }}</p>
5422
+ <p class="sp-account__title">{{ t.mcpConnectResult }}</p>
5423
+ <p class="sp-account__note">{{ fmt(t.mcpConnectResultHint, { date: r.expires }) }}</p>
4820
5424
  </div>
4821
5425
  </div>
4822
- <hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.aiAgentResult" />
5426
+ <hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.mcpConnectResult" />
4823
5427
  <div class="sp-account__actions">
4824
- <hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.aiAgentCopy }}</hm-button>
4825
- <hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.aiAgentDone }}</hm-button>
5428
+ <hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.mcpConnectCopy }}</hm-button>
5429
+ <hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.mcpConnectDone }}</hm-button>
4826
5430
  </div>
4827
5431
  </div>
4828
5432
  } @else if (open()) {
@@ -4830,7 +5434,7 @@ class AiAgentComponent {
4830
5434
  <div class="sp-account__form">
4831
5435
  @if (servers.length > 1) {
4832
5436
  <fieldset class="sp-account__choices">
4833
- <legend class="sp-account__label">{{ t.aiAgentServers }}</legend>
5437
+ <legend class="sp-account__label">{{ t.mcpConnectServers }}</legend>
4834
5438
  @for (s of servers; track s.name) {
4835
5439
  <hm-checkbox [label]="serverLabel(s)" [checked]="selected().has(s.name)" (checkedChange)="toggleServer(s.name, $event)" />
4836
5440
  }
@@ -4838,9 +5442,9 @@ class AiAgentComponent {
4838
5442
  }
4839
5443
  @if (canWrite()) {
4840
5444
  <div class="sp-account__field">
4841
- <span class="sp-account__label">{{ t.aiAgentAccess }}</span>
4842
- <hm-segmented-control size="small" emphasis="action" [label]="t.aiAgentAccess" [options]="accessOptions()" [value]="access()" (valueChange)="access.set($event === 'write' ? 'write' : 'read')" />
4843
- @if (access() === 'write') { <p class="sp-account__note">{{ t.aiAgentWriteHint }}</p> }
5445
+ <span class="sp-account__label">{{ t.mcpConnectAccess }}</span>
5446
+ <hm-segmented-control size="small" emphasis="action" [label]="t.mcpConnectAccess" [options]="accessOptions()" [value]="access()" (valueChange)="access.set($event === 'write' ? 'write' : 'read')" />
5447
+ @if (access() === 'write') { <p class="sp-account__note">{{ t.mcpConnectWriteHint }}</p> }
4844
5448
  </div>
4845
5449
  }
4846
5450
  @if (badgeOptions().length > 1) {
@@ -4853,39 +5457,39 @@ class AiAgentComponent {
4853
5457
  <span class="sp-account__label">{{ tokens.validity }}</span>
4854
5458
  <hm-segmented-control size="small" emphasis="action" [label]="tokens.validity" [options]="validityOptions()" [value]="validity()" (valueChange)="validity.set($event)" />
4855
5459
  </div>
4856
- @if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.aiAgentSelectServer }}</p> }
5460
+ @if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.mcpConnectSelectServer }}</p> }
4857
5461
  @if (noBadges()) { <p class="sp-account__note sp-account__note--error">{{ tokens.noBadges }}</p> }
4858
5462
  </div>
4859
5463
  <div class="sp-account__actions">
4860
5464
  <hm-button size="small" emphasis="medium" [disabled]="submitting()" (clicked)="cancel()">{{ tokens.cancel }}</hm-button>
4861
- <hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.aiAgentGenerate }}</hm-button>
5465
+ <hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.mcpConnectGenerate }}</hm-button>
4862
5466
  </div>
4863
5467
  </div>
4864
5468
  } @else {
4865
5469
  <div class="sp-account__actions">
4866
- <hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.aiAgentStart }}</hm-button>
5470
+ <hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.mcpConnectStart }}</hm-button>
4867
5471
  </div>
4868
5472
  }
4869
5473
  `, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: CodeViewerComponent, selector: "hm-code-viewer", inputs: ["code", "language", "filename", "label", "showLineNumbers", "firstLineNumber", "highlightLines", "wrap", "tabSize", "copyable", "header", "virtualize", "virtualizeThreshold", "revealInvisibles"], outputs: ["copied", "tokenized"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4870
5474
  }
4871
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AiAgentComponent, decorators: [{
5475
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: McpConnectComponent, decorators: [{
4872
5476
  type: Component,
4873
- args: [{ selector: 'spine-ai-agent', imports: [ButtonComponent, CheckboxComponent, CodeViewerComponent, IconComponent, SegmentedControlComponent], template: `
5477
+ args: [{ selector: 'spine-mcp-connect', imports: [ButtonComponent, CheckboxComponent, CodeViewerComponent, IconComponent, SegmentedControlComponent], template: `
4874
5478
  @if (!available) {
4875
- <p class="sp-account__note">{{ t.aiAgentUnavailable }}</p>
5479
+ <p class="sp-account__note">{{ t.mcpConnectUnavailable }}</p>
4876
5480
  } @else if (result(); as r) {
4877
5481
  <div class="sp-account__panel sp-account__panel--success" role="status">
4878
5482
  <div class="sp-account__panel-head">
4879
5483
  <hm-icon shape="check-circle" />
4880
5484
  <div class="sp-account__panel-text">
4881
- <p class="sp-account__title">{{ t.aiAgentResult }}</p>
4882
- <p class="sp-account__note">{{ fmt(t.aiAgentResultHint, { date: r.expires }) }}</p>
5485
+ <p class="sp-account__title">{{ t.mcpConnectResult }}</p>
5486
+ <p class="sp-account__note">{{ fmt(t.mcpConnectResultHint, { date: r.expires }) }}</p>
4883
5487
  </div>
4884
5488
  </div>
4885
- <hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.aiAgentResult" />
5489
+ <hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.mcpConnectResult" />
4886
5490
  <div class="sp-account__actions">
4887
- <hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.aiAgentCopy }}</hm-button>
4888
- <hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.aiAgentDone }}</hm-button>
5491
+ <hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.mcpConnectCopy }}</hm-button>
5492
+ <hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.mcpConnectDone }}</hm-button>
4889
5493
  </div>
4890
5494
  </div>
4891
5495
  } @else if (open()) {
@@ -4893,7 +5497,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
4893
5497
  <div class="sp-account__form">
4894
5498
  @if (servers.length > 1) {
4895
5499
  <fieldset class="sp-account__choices">
4896
- <legend class="sp-account__label">{{ t.aiAgentServers }}</legend>
5500
+ <legend class="sp-account__label">{{ t.mcpConnectServers }}</legend>
4897
5501
  @for (s of servers; track s.name) {
4898
5502
  <hm-checkbox [label]="serverLabel(s)" [checked]="selected().has(s.name)" (checkedChange)="toggleServer(s.name, $event)" />
4899
5503
  }
@@ -4901,9 +5505,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
4901
5505
  }
4902
5506
  @if (canWrite()) {
4903
5507
  <div class="sp-account__field">
4904
- <span class="sp-account__label">{{ t.aiAgentAccess }}</span>
4905
- <hm-segmented-control size="small" emphasis="action" [label]="t.aiAgentAccess" [options]="accessOptions()" [value]="access()" (valueChange)="access.set($event === 'write' ? 'write' : 'read')" />
4906
- @if (access() === 'write') { <p class="sp-account__note">{{ t.aiAgentWriteHint }}</p> }
5508
+ <span class="sp-account__label">{{ t.mcpConnectAccess }}</span>
5509
+ <hm-segmented-control size="small" emphasis="action" [label]="t.mcpConnectAccess" [options]="accessOptions()" [value]="access()" (valueChange)="access.set($event === 'write' ? 'write' : 'read')" />
5510
+ @if (access() === 'write') { <p class="sp-account__note">{{ t.mcpConnectWriteHint }}</p> }
4907
5511
  </div>
4908
5512
  }
4909
5513
  @if (badgeOptions().length > 1) {
@@ -4916,17 +5520,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
4916
5520
  <span class="sp-account__label">{{ tokens.validity }}</span>
4917
5521
  <hm-segmented-control size="small" emphasis="action" [label]="tokens.validity" [options]="validityOptions()" [value]="validity()" (valueChange)="validity.set($event)" />
4918
5522
  </div>
4919
- @if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.aiAgentSelectServer }}</p> }
5523
+ @if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.mcpConnectSelectServer }}</p> }
4920
5524
  @if (noBadges()) { <p class="sp-account__note sp-account__note--error">{{ tokens.noBadges }}</p> }
4921
5525
  </div>
4922
5526
  <div class="sp-account__actions">
4923
5527
  <hm-button size="small" emphasis="medium" [disabled]="submitting()" (clicked)="cancel()">{{ tokens.cancel }}</hm-button>
4924
- <hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.aiAgentGenerate }}</hm-button>
5528
+ <hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.mcpConnectGenerate }}</hm-button>
4925
5529
  </div>
4926
5530
  </div>
4927
5531
  } @else {
4928
5532
  <div class="sp-account__actions">
4929
- <hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.aiAgentStart }}</hm-button>
5533
+ <hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.mcpConnectStart }}</hm-button>
4930
5534
  </div>
4931
5535
  }
4932
5536
  `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"] }]
@@ -6288,7 +6892,7 @@ class SettingsPanelComponent {
6288
6892
  this.sessionsItems = {
6289
6893
  accessToken: this.showAccessToken,
6290
6894
  appTokens: true,
6291
- aiAgent: !!this.config?.features?.aiAgent?.servers?.length,
6895
+ mcpConnect: !!this.config?.features?.mcpConnect?.servers?.length,
6292
6896
  devices: this.accountSecurity,
6293
6897
  };
6294
6898
  /** Sem nenhuma linha visível a seção não aparece. */
@@ -6508,17 +7112,17 @@ class SettingsPanelComponent {
6508
7112
  this.settings.close();
6509
7113
  }
6510
7114
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6511
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SettingsPanelComponent, isStandalone: true, selector: "spine-settings-panel", ngImport: i0, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"activeSection()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.aiAgent). -->\n @if (sessionsItems.aiAgent) {\n <hm-settings-row [label]=\"featureText.aiAgent\" [description]=\"featureText.aiAgentDescription\" [keywords]=\"featureText.aiAgentKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.aiAgent })\" [text]=\"featureText.aiAgentHelp\" />\n <spine-ai-agent />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Grupos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os grupos desta aplica\u00E7\u00E3o de que a pessoa \u00E9 membro (argos-admin /me/groups, relidos a cada abertura): a lista,\n e escolhendo um, a tela de membros dele. Sem grupo nenhum, a se\u00E7\u00E3o n\u00E3o aparece. -->\n @if (myGroups().length) {\n <hm-settings-section [key]=\"keys.groups\" [label]=\"text.groups\" icon=\"employee-group\" [keywords]=\"text.groupsKeywords\"\n [class.sp-settings__section--group-open]=\"groupOpen()\" [description]=\"groupOpen() ? '' : text.groupsDescription\">\n <spine-my-groups [groups]=\"myGroups()\" (openChange)=\"groupOpen.set($event)\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n class=\"sp-settings__picker\"\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n\n <!-- Dividir abas: com ela desligada (o padr\u00E3o) a \u00E1rea de abas fica num painel s\u00F3 (SpineWorkspaceNavService). -->\n <hm-settings-row [label]=\"text.splitTabs\" [description]=\"text.splitTabsDescription\" [keywords]=\"text.splitTabsKeywords\">\n <hm-checkbox [label]=\"text.splitTabs\" [labelHidden]=\"true\" [checked]=\"preferences.splitTabs()\" (checkedChange)=\"preferences.setSplitTabs($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Novidades (s\u00F3 com alguma vigente), configura\u00E7\u00E3o inicial e o essencial: fecham o painel antes de abrir. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\n @if (setupEnabled) {\n <hm-settings-row [label]=\"text.setup\" [description]=\"text.setupDescription\" [keywords]=\"text.setupKeywords\">\n <hm-button emphasis=\"medium\" icon=\"color-palette\" (clicked)=\"redoSetup()\">{{ text.redoSetup }}</hm-button>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.essentials\" [description]=\"text.essentialsDescription\" [keywords]=\"text.essentialsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"help\" (clicked)=\"showEssentials()\">{{ text.showEssentials }}</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 <!-- M\u00E9dia da ferramenta (SpineFeatures.ratings) e \"Avaliar\", que abre o di\u00E1logo da estrela do cabe\u00E7alho. -->\n @if (ratingsEnabled) {\n <hm-settings-row [label]=\"ratingText.aboutRating\" [keywords]=\"ratingText.aboutRatingKeywords\">\n <spine-app-rating-summary />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__section--group-open ::ng-deep .hm-settings-section__heading{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: AppTokensComponent, selector: "spine-app-tokens" }, { kind: "component", type: MyGroupsComponent, selector: "spine-my-groups", inputs: ["groups"], outputs: ["openChange"] }, { kind: "component", type: AiAgentComponent, selector: "spine-ai-agent" }, { kind: "component", type: AccountActivityComponent, selector: "spine-account-activity" }, { kind: "component", type: DefaultBadgeComponent, selector: "spine-default-badge" }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: AccountMfaComponent, selector: "spine-account-mfa" }, { kind: "component", type: AccountRecoveryCodesComponent, selector: "spine-account-recovery-codes" }, { kind: "component", type: AccountSessionsComponent, selector: "spine-account-sessions" }, { kind: "directive", type: SpineAccountSecurityScopeDirective, selector: "[spineAccountSecurity]" }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "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", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SettingsDialogComponent, selector: "hm-settings-dialog", inputs: ["open", "heading", "activeSection", "closeHandler", "actions", "submitLabel", "cancelLabel"], outputs: ["openChange", "activeSectionChange", "closed", "submitted"] }, { kind: "component", type: SettingsRowComponent, selector: "hm-settings-row", inputs: ["label", "description", "keywords", "stacked"] }, { kind: "component", type: SettingsSectionComponent, selector: "hm-settings-section", inputs: ["key", "label", "icon", "description", "keywords", "control", "invalid"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: ShortcutListComponent, selector: "hm-shortcut-list", inputs: ["items", "registered", "searchable", "characterKeysToggle", "headingLevel"] }, { kind: "component", type: ThemePickerComponent, selector: "hm-theme-picker", inputs: ["families", "family", "theme", "showMode"], outputs: ["familyChosen", "themeChosen"] }, { kind: "component", type: ToggletipComponent, selector: "hm-toggletip", inputs: ["label", "text", "side", "intent", "icon"] }, { kind: "component", type: SpineAppRatingSummaryComponent, selector: "spine-app-rating-summary" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
7115
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SettingsPanelComponent, isStandalone: true, selector: "spine-settings-panel", ngImport: i0, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"activeSection()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.mcpConnect). -->\n @if (sessionsItems.mcpConnect) {\n <hm-settings-row [label]=\"featureText.mcpConnect\" [description]=\"featureText.mcpConnectDescription\" [keywords]=\"featureText.mcpConnectKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.mcpConnect })\" [text]=\"featureText.mcpConnectHelp\" />\n <spine-mcp-connect />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Grupos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os grupos desta aplica\u00E7\u00E3o de que a pessoa \u00E9 membro (argos-admin /me/groups, relidos a cada abertura): a lista,\n e escolhendo um, a tela de membros dele. Sem grupo nenhum, a se\u00E7\u00E3o n\u00E3o aparece. -->\n @if (myGroups().length) {\n <hm-settings-section [key]=\"keys.groups\" [label]=\"text.groups\" icon=\"employee-group\" [keywords]=\"text.groupsKeywords\"\n [class.sp-settings__section--group-open]=\"groupOpen()\" [description]=\"groupOpen() ? '' : text.groupsDescription\">\n <spine-my-groups [groups]=\"myGroups()\" (openChange)=\"groupOpen.set($event)\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n class=\"sp-settings__picker\"\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n\n <!-- Dividir abas: com ela desligada (o padr\u00E3o) a \u00E1rea de abas fica num painel s\u00F3 (SpineWorkspaceNavService). -->\n <hm-settings-row [label]=\"text.splitTabs\" [description]=\"text.splitTabsDescription\" [keywords]=\"text.splitTabsKeywords\">\n <hm-checkbox [label]=\"text.splitTabs\" [labelHidden]=\"true\" [checked]=\"preferences.splitTabs()\" (checkedChange)=\"preferences.setSplitTabs($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Novidades (s\u00F3 com alguma vigente), configura\u00E7\u00E3o inicial e o essencial: fecham o painel antes de abrir. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\n @if (setupEnabled) {\n <hm-settings-row [label]=\"text.setup\" [description]=\"text.setupDescription\" [keywords]=\"text.setupKeywords\">\n <hm-button emphasis=\"medium\" icon=\"color-palette\" (clicked)=\"redoSetup()\">{{ text.redoSetup }}</hm-button>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.essentials\" [description]=\"text.essentialsDescription\" [keywords]=\"text.essentialsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"help\" (clicked)=\"showEssentials()\">{{ text.showEssentials }}</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 <!-- M\u00E9dia da ferramenta (SpineFeatures.ratings) e \"Avaliar\", que abre o di\u00E1logo da estrela do cabe\u00E7alho. -->\n @if (ratingsEnabled) {\n <hm-settings-row [label]=\"ratingText.aboutRating\" [keywords]=\"ratingText.aboutRatingKeywords\">\n <spine-app-rating-summary />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__section--group-open ::ng-deep .hm-settings-section__heading{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: AppTokensComponent, selector: "spine-app-tokens" }, { kind: "component", type: MyGroupsComponent, selector: "spine-my-groups", inputs: ["groups"], outputs: ["openChange"] }, { kind: "component", type: McpConnectComponent, selector: "spine-mcp-connect" }, { kind: "component", type: AccountActivityComponent, selector: "spine-account-activity" }, { kind: "component", type: DefaultBadgeComponent, selector: "spine-default-badge" }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: AccountMfaComponent, selector: "spine-account-mfa" }, { kind: "component", type: AccountRecoveryCodesComponent, selector: "spine-account-recovery-codes" }, { kind: "component", type: AccountSessionsComponent, selector: "spine-account-sessions" }, { kind: "directive", type: SpineAccountSecurityScopeDirective, selector: "[spineAccountSecurity]" }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "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", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SettingsDialogComponent, selector: "hm-settings-dialog", inputs: ["open", "heading", "activeSection", "closeHandler", "actions", "submitLabel", "cancelLabel"], outputs: ["openChange", "activeSectionChange", "closed", "submitted"] }, { kind: "component", type: SettingsRowComponent, selector: "hm-settings-row", inputs: ["label", "description", "keywords", "stacked"] }, { kind: "component", type: SettingsSectionComponent, selector: "hm-settings-section", inputs: ["key", "label", "icon", "description", "keywords", "control", "invalid"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: ShortcutListComponent, selector: "hm-shortcut-list", inputs: ["items", "registered", "searchable", "characterKeysToggle", "headingLevel"] }, { kind: "component", type: ThemePickerComponent, selector: "hm-theme-picker", inputs: ["families", "family", "theme", "showMode"], outputs: ["familyChosen", "themeChosen"] }, { kind: "component", type: ToggletipComponent, selector: "hm-toggletip", inputs: ["label", "text", "side", "intent", "icon"] }, { kind: "component", type: SpineAppRatingSummaryComponent, selector: "spine-app-rating-summary" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6512
7116
  }
6513
7117
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, decorators: [{
6514
7118
  type: Component,
6515
7119
  args: [{ selector: 'spine-settings-panel', imports: [
6516
- NgComponentOutlet, AppTokensComponent, MyGroupsComponent, AiAgentComponent, AccountActivityComponent, DefaultBadgeComponent, CheckboxComponent,
7120
+ NgComponentOutlet, AppTokensComponent, MyGroupsComponent, McpConnectComponent, AccountActivityComponent, DefaultBadgeComponent, CheckboxComponent,
6517
7121
  AccountMfaComponent, AccountRecoveryCodesComponent, AccountSessionsComponent, SpineAccountSecurityScopeDirective,
6518
7122
  AccountAvatarComponent, ButtonComponent, ComboboxComponent,
6519
7123
  SegmentedControlComponent, SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent,
6520
7124
  SelectComponent, ShortcutListComponent, ThemePickerComponent, ToggletipComponent, SpineAppRatingSummaryComponent,
6521
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"activeSection()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.aiAgent). -->\n @if (sessionsItems.aiAgent) {\n <hm-settings-row [label]=\"featureText.aiAgent\" [description]=\"featureText.aiAgentDescription\" [keywords]=\"featureText.aiAgentKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.aiAgent })\" [text]=\"featureText.aiAgentHelp\" />\n <spine-ai-agent />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Grupos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os grupos desta aplica\u00E7\u00E3o de que a pessoa \u00E9 membro (argos-admin /me/groups, relidos a cada abertura): a lista,\n e escolhendo um, a tela de membros dele. Sem grupo nenhum, a se\u00E7\u00E3o n\u00E3o aparece. -->\n @if (myGroups().length) {\n <hm-settings-section [key]=\"keys.groups\" [label]=\"text.groups\" icon=\"employee-group\" [keywords]=\"text.groupsKeywords\"\n [class.sp-settings__section--group-open]=\"groupOpen()\" [description]=\"groupOpen() ? '' : text.groupsDescription\">\n <spine-my-groups [groups]=\"myGroups()\" (openChange)=\"groupOpen.set($event)\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n class=\"sp-settings__picker\"\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n\n <!-- Dividir abas: com ela desligada (o padr\u00E3o) a \u00E1rea de abas fica num painel s\u00F3 (SpineWorkspaceNavService). -->\n <hm-settings-row [label]=\"text.splitTabs\" [description]=\"text.splitTabsDescription\" [keywords]=\"text.splitTabsKeywords\">\n <hm-checkbox [label]=\"text.splitTabs\" [labelHidden]=\"true\" [checked]=\"preferences.splitTabs()\" (checkedChange)=\"preferences.setSplitTabs($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Novidades (s\u00F3 com alguma vigente), configura\u00E7\u00E3o inicial e o essencial: fecham o painel antes de abrir. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\n @if (setupEnabled) {\n <hm-settings-row [label]=\"text.setup\" [description]=\"text.setupDescription\" [keywords]=\"text.setupKeywords\">\n <hm-button emphasis=\"medium\" icon=\"color-palette\" (clicked)=\"redoSetup()\">{{ text.redoSetup }}</hm-button>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.essentials\" [description]=\"text.essentialsDescription\" [keywords]=\"text.essentialsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"help\" (clicked)=\"showEssentials()\">{{ text.showEssentials }}</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 <!-- M\u00E9dia da ferramenta (SpineFeatures.ratings) e \"Avaliar\", que abre o di\u00E1logo da estrela do cabe\u00E7alho. -->\n @if (ratingsEnabled) {\n <hm-settings-row [label]=\"ratingText.aboutRating\" [keywords]=\"ratingText.aboutRatingKeywords\">\n <spine-app-rating-summary />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__section--group-open ::ng-deep .hm-settings-section__heading{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"] }]
7125
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"activeSection()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.mcpConnect). -->\n @if (sessionsItems.mcpConnect) {\n <hm-settings-row [label]=\"featureText.mcpConnect\" [description]=\"featureText.mcpConnectDescription\" [keywords]=\"featureText.mcpConnectKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.mcpConnect })\" [text]=\"featureText.mcpConnectHelp\" />\n <spine-mcp-connect />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Grupos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os grupos desta aplica\u00E7\u00E3o de que a pessoa \u00E9 membro (argos-admin /me/groups, relidos a cada abertura): a lista,\n e escolhendo um, a tela de membros dele. Sem grupo nenhum, a se\u00E7\u00E3o n\u00E3o aparece. -->\n @if (myGroups().length) {\n <hm-settings-section [key]=\"keys.groups\" [label]=\"text.groups\" icon=\"employee-group\" [keywords]=\"text.groupsKeywords\"\n [class.sp-settings__section--group-open]=\"groupOpen()\" [description]=\"groupOpen() ? '' : text.groupsDescription\">\n <spine-my-groups [groups]=\"myGroups()\" (openChange)=\"groupOpen.set($event)\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n class=\"sp-settings__picker\"\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n\n <!-- Dividir abas: com ela desligada (o padr\u00E3o) a \u00E1rea de abas fica num painel s\u00F3 (SpineWorkspaceNavService). -->\n <hm-settings-row [label]=\"text.splitTabs\" [description]=\"text.splitTabsDescription\" [keywords]=\"text.splitTabsKeywords\">\n <hm-checkbox [label]=\"text.splitTabs\" [labelHidden]=\"true\" [checked]=\"preferences.splitTabs()\" (checkedChange)=\"preferences.setSplitTabs($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Novidades (s\u00F3 com alguma vigente), configura\u00E7\u00E3o inicial e o essencial: fecham o painel antes de abrir. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\n @if (setupEnabled) {\n <hm-settings-row [label]=\"text.setup\" [description]=\"text.setupDescription\" [keywords]=\"text.setupKeywords\">\n <hm-button emphasis=\"medium\" icon=\"color-palette\" (clicked)=\"redoSetup()\">{{ text.redoSetup }}</hm-button>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.essentials\" [description]=\"text.essentialsDescription\" [keywords]=\"text.essentialsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"help\" (clicked)=\"showEssentials()\">{{ text.showEssentials }}</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 <!-- M\u00E9dia da ferramenta (SpineFeatures.ratings) e \"Avaliar\", que abre o di\u00E1logo da estrela do cabe\u00E7alho. -->\n @if (ratingsEnabled) {\n <hm-settings-row [label]=\"ratingText.aboutRating\" [keywords]=\"ratingText.aboutRatingKeywords\">\n <spine-app-rating-summary />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__section--group-open ::ng-deep .hm-settings-section__heading{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"] }]
6522
7126
  }], ctorParameters: () => [] });
6523
7127
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6524
7128
  /** "UTC−03:00" para um fuso IANA. */
@@ -6996,7 +7600,7 @@ function registerPage() {
6996
7600
  route_redirect: SPINE_SUGGESTIONS_PAGE_PATH,
6997
7601
  description: text.description,
6998
7602
  },
6999
- load: () => import('./trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs').then((m) => m.SpineSuggestionsPageComponent),
7603
+ load: () => import('./trismegisto-spine-layout-suggestions-page.component-BEGFNlvw.mjs').then((m) => m.SpineSuggestionsPageComponent),
7000
7604
  });
7001
7605
  });
7002
7606
  }
@@ -8101,6 +8705,415 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
8101
8705
  `, styles: [":host{display:block;flex:none}\n"] }]
8102
8706
  }] });
8103
8707
 
8708
+ const PANEL_ID = 'spine-ai-agent-panel';
8709
+ const TITLE_ID = 'spine-ai-agent-title';
8710
+ const PANEL_STATE_KEY = 'spine-ai-agent:panel';
8711
+ // Em px, medidas do ponteiro: até aqui é um clique, e o balão não encosta na borda da tela.
8712
+ const DRAG_THRESHOLD = 4;
8713
+ const DRAG_MARGIN = 8;
8714
+ const GREETING_KEY = 'spine-ai-agent:greeting-closed';
8715
+ const GREETING_MS = 15_000;
8716
+ const GREETING_SUGGESTIONS = 3;
8717
+ function greetingClosed() {
8718
+ try {
8719
+ return sessionStorage.getItem(GREETING_KEY) === '1';
8720
+ }
8721
+ catch {
8722
+ return false;
8723
+ }
8724
+ }
8725
+ function readPanelState() {
8726
+ try {
8727
+ return JSON.parse(sessionStorage.getItem(PANEL_STATE_KEY) ?? '{}');
8728
+ }
8729
+ catch {
8730
+ return {};
8731
+ }
8732
+ }
8733
+ /**
8734
+ * O agente de IA do app: um balão flutuante, que abre a conversa num painel ao lado dele. O painel não é modal, para a
8735
+ * pessoa continuar vendo e usando a tela enquanto pergunta. Esc fecha e devolve o foco a quem abriu, e o painel pode ser
8736
+ * expandido para respostas longas. Aberto e tamanho voltam como estavam ao recarregar a aba. A tela da conversa fica em chunk
8737
+ * próprio, carregado na primeira abertura, e continua montada depois disso.
8738
+ *
8739
+ * O balão pode ser arrastado e, pelo X dele, sair da tela: vira um ícone no cabeçalho (`spine-ai-agent-dock-button`), e o
8740
+ * painel passa a abrir logo abaixo do cabeçalho. Onde ele está vale neste navegador. Ao lado do balão aparece um convite com
8741
+ * o que dá para perguntar, uma vez por aba, que sai sozinho ou quando a pessoa o fecha. Sem `SpineFeatures.aiAgent`, não
8742
+ * desenha nada.
8743
+ */
8744
+ class SpineAiAgentLauncherComponent {
8745
+ constructor() {
8746
+ this.agent = inject(SpineAiAgentService);
8747
+ this.text = inject(SPINE_MESSAGES).aiAgentChat;
8748
+ this.toast = inject(HermesToastService);
8749
+ this.panelId = PANEL_ID;
8750
+ this.titleId = TITLE_ID;
8751
+ /** Tamanho do painel: continua como a pessoa deixou ao fechar e abrir. */
8752
+ this.expanded = signal(readPanelState().expanded === true, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
8753
+ this.chat = signal(null, ...(ngDevMode ? [{ debugName: "chat" }] : /* istanbul ignore next */ []));
8754
+ this.dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
8755
+ /** Com o balão na metade de cima da tela, o painel abre para baixo, onde há espaço. */
8756
+ this.below = computed(() => !this.agent.docked() && (this.agent.position()?.bottom ?? 0) > window.innerHeight / 2, ...(ngDevMode ? [{ debugName: "below" }] : /* istanbul ignore next */ []));
8757
+ /** O balão é um círculo: o rosto cabe nele, e uma imagem de corpo inteiro cortaria onde não deve. */
8758
+ this.fabImage = computed(() => this.agent.avatarNow() ?? this.agent.image(), ...(ngDevMode ? [{ debugName: "fabImage" }] : /* istanbul ignore next */ []));
8759
+ /** Com o balão na metade inicial da tela, o convite fica depois dele. */
8760
+ this.greetingAfter = computed(() => (this.agent.position()?.end ?? 0) > window.innerWidth / 2, ...(ngDevMode ? [{ debugName: "greetingAfter" }] : /* istanbul ignore next */ []));
8761
+ this.greetingOpen = signal(!greetingClosed(), ...(ngDevMode ? [{ debugName: "greetingOpen" }] : /* istanbul ignore next */ []));
8762
+ this.greetingVisible = computed(() => this.greetingOpen() && !this.agent.panelOpen() && !this.agent.docked() && !this.dragging(), ...(ngDevMode ? [{ debugName: "greetingVisible" }] : /* istanbul ignore next */ []));
8763
+ this.greetingSuggestions = computed(() => this.agent.suggestions().slice(0, GREETING_SUGGESTIONS), ...(ngDevMode ? [{ debugName: "greetingSuggestions" }] : /* istanbul ignore next */ []));
8764
+ this.host = inject(ElementRef);
8765
+ this.injector = inject(Injector);
8766
+ this.document = inject(DOCUMENT);
8767
+ /** O clique que encerra um arraste não abre o painel. */
8768
+ this.dragged = false;
8769
+ this.stopDrag = null;
8770
+ this.greetingTimer = null;
8771
+ inject(DestroyRef).onDestroy(() => {
8772
+ this.stopDrag?.();
8773
+ this.keepGreeting();
8774
+ });
8775
+ effect(() => {
8776
+ const state = JSON.stringify({ open: this.agent.panelOpen(), expanded: this.expanded() });
8777
+ try {
8778
+ sessionStorage.setItem(PANEL_STATE_KEY, state);
8779
+ }
8780
+ catch {
8781
+ // Navegador sem armazenamento: o painel só não volta aberto.
8782
+ }
8783
+ });
8784
+ // O painel também abre pelo ícone do cabeçalho, que está em outro componente.
8785
+ effect(() => {
8786
+ if (this.agent.panelOpen())
8787
+ untracked(() => void this.prepare());
8788
+ });
8789
+ }
8790
+ ngOnInit() {
8791
+ // Sem roubar o foco: a pessoa acabou de recarregar a tela, não pediu o chat agora.
8792
+ if (this.agent.enabled && readPanelState().open === true)
8793
+ this.agent.panelOpen.set(true);
8794
+ if (this.agent.enabled && this.greetingVisible()) {
8795
+ this.agent.loadSuggestions();
8796
+ this.greetingTimer = setTimeout(() => this.closeGreeting(), GREETING_MS);
8797
+ }
8798
+ }
8799
+ toggle() {
8800
+ if (!this.dragged)
8801
+ this.agent.togglePanel();
8802
+ }
8803
+ async prepare() {
8804
+ const focus = this.agent.focusOnOpen;
8805
+ this.agent.focusOnOpen = false;
8806
+ if (!this.chat()) {
8807
+ const { SpineAiAgentChatComponent } = await import('./trismegisto-spine-layout-ai-agent-chat.component-LlxtxNA5.mjs');
8808
+ this.chat.set(SpineAiAgentChatComponent);
8809
+ }
8810
+ this.agent.loadSuggestions();
8811
+ this.agent.restoreConversation();
8812
+ if (focus)
8813
+ afterNextRender(() => this.host.nativeElement.querySelector('textarea')?.focus(), { injector: this.injector });
8814
+ }
8815
+ /** Quem está lendo ou usando o convite decide quando ele sai. */
8816
+ keepGreeting() {
8817
+ if (this.greetingTimer)
8818
+ clearTimeout(this.greetingTimer);
8819
+ this.greetingTimer = null;
8820
+ }
8821
+ closeGreeting() {
8822
+ this.keepGreeting();
8823
+ this.greetingOpen.set(false);
8824
+ try {
8825
+ sessionStorage.setItem(GREETING_KEY, '1');
8826
+ }
8827
+ catch {
8828
+ // Navegador sem armazenamento: o convite só volta ao recarregar.
8829
+ }
8830
+ }
8831
+ askFromGreeting(question) {
8832
+ this.closeGreeting();
8833
+ this.agent.askFromGreeting(question);
8834
+ }
8835
+ close() {
8836
+ this.agent.panelOpen.set(false);
8837
+ this.focusEntry();
8838
+ }
8839
+ dock() {
8840
+ this.agent.dock();
8841
+ afterNextRender(() => this.focusEntry(), { injector: this.injector });
8842
+ }
8843
+ async downloadTranscript() {
8844
+ if (!(await this.agent.downloadTranscript()))
8845
+ this.toast.error(this.text.transcriptDownloadError);
8846
+ }
8847
+ undock() {
8848
+ this.agent.undock();
8849
+ afterNextRender(() => this.focusEntry(), { injector: this.injector });
8850
+ }
8851
+ /** O foco volta a quem abre o painel: o balão ou, com ele fora da tela, o ícone do cabeçalho. */
8852
+ focusEntry() {
8853
+ const entry = this.agent.docked()
8854
+ ? this.document.querySelector(`.${SPINE_AGENT_DOCK_CLASS} button`)
8855
+ : this.host.nativeElement.querySelector('.hm-fab__button');
8856
+ entry?.focus();
8857
+ }
8858
+ /** Só passa a ser arraste depois de `DRAG_THRESHOLD`; antes disso, soltar é o clique normal do balão. */
8859
+ startDrag(event) {
8860
+ if (event.button !== 0)
8861
+ return;
8862
+ this.dragged = false;
8863
+ const box = event.currentTarget.getBoundingClientRect();
8864
+ const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
8865
+ const { clientX: startX, clientY: startY } = event;
8866
+ const view = this.document.defaultView;
8867
+ const move = (moved) => {
8868
+ const dx = moved.clientX - startX;
8869
+ const dy = moved.clientY - startY;
8870
+ if (!this.dragged && Math.hypot(dx, dy) < DRAG_THRESHOLD)
8871
+ return;
8872
+ this.dragged = true;
8873
+ this.dragging.set(true);
8874
+ const left = Math.min(Math.max(box.left + dx, DRAG_MARGIN), view.innerWidth - box.width - DRAG_MARGIN);
8875
+ const top = Math.min(Math.max(box.top + dy, DRAG_MARGIN), view.innerHeight - box.height - DRAG_MARGIN);
8876
+ this.agent.position.set({ end: rtl ? left : view.innerWidth - left - box.width, bottom: view.innerHeight - top - box.height });
8877
+ };
8878
+ const stop = () => {
8879
+ this.stopDrag = null;
8880
+ this.document.removeEventListener('pointermove', move);
8881
+ this.document.removeEventListener('pointerup', stop);
8882
+ this.document.removeEventListener('pointercancel', stop);
8883
+ this.dragging.set(false);
8884
+ if (this.dragged)
8885
+ this.agent.savePlacement();
8886
+ };
8887
+ this.document.addEventListener('pointermove', move);
8888
+ this.document.addEventListener('pointerup', stop);
8889
+ this.document.addEventListener('pointercancel', stop);
8890
+ this.stopDrag = stop;
8891
+ }
8892
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentLauncherComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
8893
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineAiAgentLauncherComponent, isStandalone: true, selector: "spine-ai-agent-launcher", host: { properties: { "class.spine-agent--docked": "agent.docked()", "class.spine-agent--below": "below()", "style.--spine-agent-end": "agent.position() ? agent.position()!.end + 'px' : null", "style.--spine-agent-bottom": "agent.position() ? agent.position()!.bottom + 'px' : null" } }, ngImport: i0, template: `
8894
+ @if (agent.enabled) {
8895
+ @if (chat(); as component) {
8896
+ <section
8897
+ class="spine-agent-panel"
8898
+ role="dialog"
8899
+ aria-modal="false"
8900
+ [attr.id]="panelId"
8901
+ [attr.aria-labelledby]="titleId"
8902
+ [class.spine-agent-panel--open]="agent.panelOpen()"
8903
+ [class.spine-agent-panel--expanded]="expanded()"
8904
+ [hidden]="!agent.panelOpen()"
8905
+ (keydown.escape)="close()"
8906
+ >
8907
+ <header class="spine-agent-panel__header">
8908
+ @if (agent.avatarNow(); as avatar) {
8909
+ <img class="spine-agent-panel__avatar" [src]="avatar" alt="" (error)="agent.imageFailed(avatar)" />
8910
+ }
8911
+ <h2 class="spine-agent-panel__title" [attr.id]="titleId">{{ agent.title }}</h2>
8912
+ <hm-icon-button
8913
+ emphasis="subtle"
8914
+ size="small"
8915
+ icon="plus"
8916
+ [label]="text.newConversation"
8917
+ [disabled]="agent.running() || !agent.entries().length"
8918
+ (clicked)="agent.reset()"
8919
+ />
8920
+ <hm-icon-button
8921
+ emphasis="subtle"
8922
+ size="small"
8923
+ icon="download"
8924
+ [label]="text.transcriptDownload"
8925
+ [disabled]="agent.running() || !agent.entries().length"
8926
+ (clicked)="downloadTranscript()"
8927
+ />
8928
+ <hm-icon-button
8929
+ emphasis="subtle"
8930
+ size="small"
8931
+ [icon]="expanded() ? 'window-restore' : 'window-max'"
8932
+ [label]="expanded() ? text.collapse : text.expand"
8933
+ (clicked)="expanded.set(!expanded())"
8934
+ />
8935
+ @if (agent.docked()) {
8936
+ <hm-icon-button emphasis="subtle" size="small" icon="unpin" [label]="text.undock" (clicked)="undock()" />
8937
+ }
8938
+ <hm-icon-button emphasis="subtle" size="small" icon="times" [label]="text.close" (clicked)="close()" />
8939
+ </header>
8940
+ <div class="spine-agent-panel__body">
8941
+ <ng-container *ngComponentOutlet="component" />
8942
+ </div>
8943
+ </section>
8944
+ }
8945
+ @if (!agent.docked()) {
8946
+ <div class="spine-agent-fab" [class.spine-agent-fab--dragging]="dragging()" (pointerdown)="startDrag($event)">
8947
+ @if (greetingVisible()) {
8948
+ <section
8949
+ class="spine-agent-greeting"
8950
+ [class.spine-agent-greeting--after]="greetingAfter()"
8951
+ [class.spine-agent-greeting--below]="below()"
8952
+ [class.spine-agent-greeting--figure]="agent.image()"
8953
+ [attr.aria-label]="agent.title"
8954
+ (pointerdown)="$event.stopPropagation()"
8955
+ (mouseenter)="keepGreeting()"
8956
+ (focusin)="keepGreeting()"
8957
+ >
8958
+ @if (agent.image(); as figure) {
8959
+ <img class="spine-agent-greeting__figure" [src]="figure" alt="" (error)="agent.imageFailed(figure)" />
8960
+ }
8961
+ <div class="spine-agent-greeting__content">
8962
+ <div class="spine-agent-greeting__head">
8963
+ <p class="spine-agent-greeting__title">{{ agent.greetingTitle }}</p>
8964
+ <hm-icon-button emphasis="subtle" size="small" icon="times" [label]="text.greetingClose" (clicked)="closeGreeting()" />
8965
+ </div>
8966
+ <p class="spine-agent-greeting__text">{{ agent.greetingText }}</p>
8967
+ @if (greetingSuggestions().length) {
8968
+ <hm-prompt-suggestions [suggestions]="greetingSuggestions()" (chosen)="askFromGreeting($event)" />
8969
+ }
8970
+ </div>
8971
+ </section>
8972
+ }
8973
+ <hm-fab
8974
+ [fixed]="false"
8975
+ [label]="agent.panelOpen() ? text.close : agent.title"
8976
+ [icon]="agent.panelOpen() ? 'times' : fabImage() ? '' : 'chat-bubble'"
8977
+ [expanded]="agent.panelOpen()"
8978
+ [controls]="panelId"
8979
+ avoid=""
8980
+ (triggered)="toggle()"
8981
+ />
8982
+ @if (!agent.panelOpen() && fabImage(); as image) {
8983
+ <img class="spine-agent-fab__image" [src]="image" alt="" (error)="agent.imageFailed(image)" />
8984
+ }
8985
+ @if (!agent.panelOpen()) {
8986
+ <hm-icon-button
8987
+ class="spine-agent-fab__dock"
8988
+ emphasis="outlined"
8989
+ size="small"
8990
+ icon="times"
8991
+ [label]="text.dock"
8992
+ (pointerdown)="$event.stopPropagation()"
8993
+ (clicked)="dock()"
8994
+ />
8995
+ }
8996
+ </div>
8997
+ }
8998
+ }
8999
+ `, isInline: true, styles: [":host{--spine-agent-fab-size: calc(var(--hm-control-height-large) * 1.75);--spine-agent-end-now: var(--spine-agent-end, var(--hm-space-lg));--spine-agent-bottom-now: var(--spine-agent-bottom, var(--hm-space-lg));--spine-agent-panel-inline: min(calc(var(--hm-space-3xl) * 11), calc(100vw - 2 * var(--hm-space-md)));--spine-agent-offset: calc(var(--spine-agent-bottom-now) + var(--spine-agent-fab-size) + var(--hm-space-sm));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-header-height) - var(--hm-space-xs))}:host(.spine-agent--below){--spine-agent-offset: calc(100dvh - var(--spine-agent-bottom-now) + var(--hm-space-sm));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-space-md))}:host(.spine-agent--docked){--spine-agent-offset: calc(var(--hm-header-height) + var(--hm-space-xs));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-space-md))}.spine-agent-fab{position:fixed;inset-inline-end:min(var(--spine-agent-end-now),calc(100vw - var(--spine-agent-fab-size) - var(--hm-space-xs)));inset-block-end:calc(min(var(--spine-agent-bottom-now),calc(100dvh - var(--spine-agent-fab-size) - var(--hm-space-xs))) + env(safe-area-inset-bottom,0px));z-index:var(--hm-z-fixed);inline-size:var(--spine-agent-fab-size);block-size:var(--spine-agent-fab-size);cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.spine-agent-fab--dragging{cursor:grabbing}.spine-agent-fab__image{position:absolute;inset:0;inline-size:100%;block-size:100%;object-fit:cover;border-radius:var(--hm-radius-pill);pointer-events:none}.spine-agent-greeting{position:absolute;inset-block-end:0;inset-inline-end:calc(100% + var(--hm-space-sm));display:flex;align-items:flex-end;gap:var(--hm-space-sm);box-sizing:border-box;inline-size:min(calc(var(--hm-space-3xl) * 7),calc(100vw - var(--spine-agent-fab-size) - 2 * var(--hm-space-lg)));overflow:hidden;padding:var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-md);background:var(--hm-surface-raised);color:var(--hm-text);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:calc(var(--hm-radius) * 2);box-shadow:var(--hm-shadow-overlay);cursor:auto;touch-action:auto;-webkit-user-select:text;user-select:text;animation:spine-agent-panel-in var(--hm-duration-quick) var(--hm-ease-decelerate)}.spine-agent-greeting--figure{inline-size:min(calc(var(--hm-space-3xl) * 11),calc(100vw - var(--spine-agent-fab-size) - 2 * var(--hm-space-lg)))}.spine-agent-greeting__figure{flex:none;inline-size:calc(var(--hm-space-3xl) * 3.5);margin-block-end:calc(var(--hm-space-sm) * -1);object-fit:contain;object-position:bottom}.spine-agent-greeting__content{display:flex;flex:1;flex-direction:column;align-self:stretch;gap:var(--hm-space-xs);min-inline-size:0}.spine-agent-greeting--after{inset-inline:calc(100% + var(--hm-space-sm)) auto}.spine-agent-greeting--below{inset-block:0 auto}.spine-agent-greeting__head{display:flex;align-items:center;gap:var(--hm-space-xs)}.spine-agent-greeting__title{flex:1;min-inline-size:0;margin:0;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.spine-agent-greeting__text{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.spine-agent-fab--dragging hm-fab{pointer-events:none}.spine-agent-fab__dock{--hm-radius-control: var(--hm-radius-pill);position:absolute;inset-block-start:calc(var(--hm-space-xs) * -1);inset-inline-end:calc(var(--hm-space-xs) * -1);background:var(--hm-surface-raised);border-radius:var(--hm-radius-pill);box-shadow:var(--hm-shadow-overlay);opacity:0;transition:opacity var(--hm-duration-quick) var(--hm-ease)}.spine-agent-fab:hover .spine-agent-fab__dock,.spine-agent-fab:focus-within .spine-agent-fab__dock{opacity:1}@media(hover:none){.spine-agent-fab__dock{opacity:1}}.spine-agent-panel{position:fixed;inset-inline-end:clamp(var(--hm-space-md),var(--spine-agent-end-now),calc(100vw - var(--spine-agent-panel-inline) - var(--hm-space-md)));inset-block-end:calc(var(--spine-agent-offset) + env(safe-area-inset-bottom,0px));z-index:var(--hm-z-fixed);display:flex;flex-direction:column;inline-size:var(--spine-agent-panel-inline);block-size:min(calc(var(--hm-space-3xl) * 16),var(--spine-agent-room));overflow:hidden;background:var(--hm-surface-raised);color:var(--hm-text);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:calc(var(--hm-radius) * 2);box-shadow:var(--hm-shadow-overlay)}.spine-agent-panel[hidden]{display:none}:host(.spine-agent--below) .spine-agent-panel,:host(.spine-agent--docked) .spine-agent-panel{inset-block-start:var(--spine-agent-offset);inset-block-end:auto}:host(.spine-agent--docked) .spine-agent-panel{inset-inline-end:var(--hm-space-md)}.spine-agent-panel--expanded{--spine-agent-panel-inline: min(calc(var(--hm-space-3xl) * 24), calc(100vw - 2 * var(--hm-space-md)));block-size:var(--spine-agent-room)}.spine-agent-panel--open{transform-origin:bottom right;animation:spine-agent-panel-in var(--hm-duration-quick) var(--hm-ease-decelerate)}:host(.spine-agent--below) .spine-agent-panel--open,:host(.spine-agent--docked) .spine-agent-panel--open{transform-origin:top right}@keyframes spine-agent-panel-in{0%{opacity:0;transform:translateY(var(--hm-space-sm)) scale(.97)}}@media(prefers-reduced-motion:reduce){.spine-agent-panel--open,.spine-agent-greeting{animation:none}.spine-agent-fab__dock{transition:none}}.spine-agent-panel__header{display:flex;align-items:center;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-md);border-block-end:var(--hm-border-hairline) solid var(--hm-divider)}.spine-agent-panel__avatar{flex:none;inline-size:var(--hm-control-height);block-size:var(--hm-control-height);object-fit:cover;border-radius:var(--hm-radius-pill)}.spine-agent-panel__title{flex:1;min-inline-size:0;margin:0;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.spine-agent-panel__body{flex:1;min-block-size:0;padding:var(--hm-space-md)}@media(max-width:600px){:host{--spine-agent-panel-inline: calc(100vw - 2 * var(--hm-space-xs))}.spine-agent-greeting__figure{display:none}.spine-agent-panel,:host(.spine-agent--docked) .spine-agent-panel{inset-inline-end:var(--hm-space-xs)}}\n"], dependencies: [{ kind: "component", type: FabComponent, selector: "hm-fab", inputs: ["label", "icon", "extended", "position", "fixed", "size", "disabled", "highlight", "actions", "hideOnScroll", "avoid", "expanded", "controls"], outputs: ["triggered", "chosen"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: PromptSuggestionsComponent, selector: "hm-prompt-suggestions", inputs: ["suggestions", "label", "disabled"], outputs: ["chosen"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
9000
+ }
9001
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentLauncherComponent, decorators: [{
9002
+ type: Component,
9003
+ args: [{ selector: 'spine-ai-agent-launcher', imports: [FabComponent, IconButtonComponent, NgComponentOutlet, PromptSuggestionsComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
9004
+ '[class.spine-agent--docked]': 'agent.docked()',
9005
+ '[class.spine-agent--below]': 'below()',
9006
+ '[style.--spine-agent-end]': "agent.position() ? agent.position()!.end + 'px' : null",
9007
+ '[style.--spine-agent-bottom]': "agent.position() ? agent.position()!.bottom + 'px' : null",
9008
+ }, template: `
9009
+ @if (agent.enabled) {
9010
+ @if (chat(); as component) {
9011
+ <section
9012
+ class="spine-agent-panel"
9013
+ role="dialog"
9014
+ aria-modal="false"
9015
+ [attr.id]="panelId"
9016
+ [attr.aria-labelledby]="titleId"
9017
+ [class.spine-agent-panel--open]="agent.panelOpen()"
9018
+ [class.spine-agent-panel--expanded]="expanded()"
9019
+ [hidden]="!agent.panelOpen()"
9020
+ (keydown.escape)="close()"
9021
+ >
9022
+ <header class="spine-agent-panel__header">
9023
+ @if (agent.avatarNow(); as avatar) {
9024
+ <img class="spine-agent-panel__avatar" [src]="avatar" alt="" (error)="agent.imageFailed(avatar)" />
9025
+ }
9026
+ <h2 class="spine-agent-panel__title" [attr.id]="titleId">{{ agent.title }}</h2>
9027
+ <hm-icon-button
9028
+ emphasis="subtle"
9029
+ size="small"
9030
+ icon="plus"
9031
+ [label]="text.newConversation"
9032
+ [disabled]="agent.running() || !agent.entries().length"
9033
+ (clicked)="agent.reset()"
9034
+ />
9035
+ <hm-icon-button
9036
+ emphasis="subtle"
9037
+ size="small"
9038
+ icon="download"
9039
+ [label]="text.transcriptDownload"
9040
+ [disabled]="agent.running() || !agent.entries().length"
9041
+ (clicked)="downloadTranscript()"
9042
+ />
9043
+ <hm-icon-button
9044
+ emphasis="subtle"
9045
+ size="small"
9046
+ [icon]="expanded() ? 'window-restore' : 'window-max'"
9047
+ [label]="expanded() ? text.collapse : text.expand"
9048
+ (clicked)="expanded.set(!expanded())"
9049
+ />
9050
+ @if (agent.docked()) {
9051
+ <hm-icon-button emphasis="subtle" size="small" icon="unpin" [label]="text.undock" (clicked)="undock()" />
9052
+ }
9053
+ <hm-icon-button emphasis="subtle" size="small" icon="times" [label]="text.close" (clicked)="close()" />
9054
+ </header>
9055
+ <div class="spine-agent-panel__body">
9056
+ <ng-container *ngComponentOutlet="component" />
9057
+ </div>
9058
+ </section>
9059
+ }
9060
+ @if (!agent.docked()) {
9061
+ <div class="spine-agent-fab" [class.spine-agent-fab--dragging]="dragging()" (pointerdown)="startDrag($event)">
9062
+ @if (greetingVisible()) {
9063
+ <section
9064
+ class="spine-agent-greeting"
9065
+ [class.spine-agent-greeting--after]="greetingAfter()"
9066
+ [class.spine-agent-greeting--below]="below()"
9067
+ [class.spine-agent-greeting--figure]="agent.image()"
9068
+ [attr.aria-label]="agent.title"
9069
+ (pointerdown)="$event.stopPropagation()"
9070
+ (mouseenter)="keepGreeting()"
9071
+ (focusin)="keepGreeting()"
9072
+ >
9073
+ @if (agent.image(); as figure) {
9074
+ <img class="spine-agent-greeting__figure" [src]="figure" alt="" (error)="agent.imageFailed(figure)" />
9075
+ }
9076
+ <div class="spine-agent-greeting__content">
9077
+ <div class="spine-agent-greeting__head">
9078
+ <p class="spine-agent-greeting__title">{{ agent.greetingTitle }}</p>
9079
+ <hm-icon-button emphasis="subtle" size="small" icon="times" [label]="text.greetingClose" (clicked)="closeGreeting()" />
9080
+ </div>
9081
+ <p class="spine-agent-greeting__text">{{ agent.greetingText }}</p>
9082
+ @if (greetingSuggestions().length) {
9083
+ <hm-prompt-suggestions [suggestions]="greetingSuggestions()" (chosen)="askFromGreeting($event)" />
9084
+ }
9085
+ </div>
9086
+ </section>
9087
+ }
9088
+ <hm-fab
9089
+ [fixed]="false"
9090
+ [label]="agent.panelOpen() ? text.close : agent.title"
9091
+ [icon]="agent.panelOpen() ? 'times' : fabImage() ? '' : 'chat-bubble'"
9092
+ [expanded]="agent.panelOpen()"
9093
+ [controls]="panelId"
9094
+ avoid=""
9095
+ (triggered)="toggle()"
9096
+ />
9097
+ @if (!agent.panelOpen() && fabImage(); as image) {
9098
+ <img class="spine-agent-fab__image" [src]="image" alt="" (error)="agent.imageFailed(image)" />
9099
+ }
9100
+ @if (!agent.panelOpen()) {
9101
+ <hm-icon-button
9102
+ class="spine-agent-fab__dock"
9103
+ emphasis="outlined"
9104
+ size="small"
9105
+ icon="times"
9106
+ [label]="text.dock"
9107
+ (pointerdown)="$event.stopPropagation()"
9108
+ (clicked)="dock()"
9109
+ />
9110
+ }
9111
+ </div>
9112
+ }
9113
+ }
9114
+ `, styles: [":host{--spine-agent-fab-size: calc(var(--hm-control-height-large) * 1.75);--spine-agent-end-now: var(--spine-agent-end, var(--hm-space-lg));--spine-agent-bottom-now: var(--spine-agent-bottom, var(--hm-space-lg));--spine-agent-panel-inline: min(calc(var(--hm-space-3xl) * 11), calc(100vw - 2 * var(--hm-space-md)));--spine-agent-offset: calc(var(--spine-agent-bottom-now) + var(--spine-agent-fab-size) + var(--hm-space-sm));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-header-height) - var(--hm-space-xs))}:host(.spine-agent--below){--spine-agent-offset: calc(100dvh - var(--spine-agent-bottom-now) + var(--hm-space-sm));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-space-md))}:host(.spine-agent--docked){--spine-agent-offset: calc(var(--hm-header-height) + var(--hm-space-xs));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-space-md))}.spine-agent-fab{position:fixed;inset-inline-end:min(var(--spine-agent-end-now),calc(100vw - var(--spine-agent-fab-size) - var(--hm-space-xs)));inset-block-end:calc(min(var(--spine-agent-bottom-now),calc(100dvh - var(--spine-agent-fab-size) - var(--hm-space-xs))) + env(safe-area-inset-bottom,0px));z-index:var(--hm-z-fixed);inline-size:var(--spine-agent-fab-size);block-size:var(--spine-agent-fab-size);cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.spine-agent-fab--dragging{cursor:grabbing}.spine-agent-fab__image{position:absolute;inset:0;inline-size:100%;block-size:100%;object-fit:cover;border-radius:var(--hm-radius-pill);pointer-events:none}.spine-agent-greeting{position:absolute;inset-block-end:0;inset-inline-end:calc(100% + var(--hm-space-sm));display:flex;align-items:flex-end;gap:var(--hm-space-sm);box-sizing:border-box;inline-size:min(calc(var(--hm-space-3xl) * 7),calc(100vw - var(--spine-agent-fab-size) - 2 * var(--hm-space-lg)));overflow:hidden;padding:var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-md);background:var(--hm-surface-raised);color:var(--hm-text);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:calc(var(--hm-radius) * 2);box-shadow:var(--hm-shadow-overlay);cursor:auto;touch-action:auto;-webkit-user-select:text;user-select:text;animation:spine-agent-panel-in var(--hm-duration-quick) var(--hm-ease-decelerate)}.spine-agent-greeting--figure{inline-size:min(calc(var(--hm-space-3xl) * 11),calc(100vw - var(--spine-agent-fab-size) - 2 * var(--hm-space-lg)))}.spine-agent-greeting__figure{flex:none;inline-size:calc(var(--hm-space-3xl) * 3.5);margin-block-end:calc(var(--hm-space-sm) * -1);object-fit:contain;object-position:bottom}.spine-agent-greeting__content{display:flex;flex:1;flex-direction:column;align-self:stretch;gap:var(--hm-space-xs);min-inline-size:0}.spine-agent-greeting--after{inset-inline:calc(100% + var(--hm-space-sm)) auto}.spine-agent-greeting--below{inset-block:0 auto}.spine-agent-greeting__head{display:flex;align-items:center;gap:var(--hm-space-xs)}.spine-agent-greeting__title{flex:1;min-inline-size:0;margin:0;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.spine-agent-greeting__text{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.spine-agent-fab--dragging hm-fab{pointer-events:none}.spine-agent-fab__dock{--hm-radius-control: var(--hm-radius-pill);position:absolute;inset-block-start:calc(var(--hm-space-xs) * -1);inset-inline-end:calc(var(--hm-space-xs) * -1);background:var(--hm-surface-raised);border-radius:var(--hm-radius-pill);box-shadow:var(--hm-shadow-overlay);opacity:0;transition:opacity var(--hm-duration-quick) var(--hm-ease)}.spine-agent-fab:hover .spine-agent-fab__dock,.spine-agent-fab:focus-within .spine-agent-fab__dock{opacity:1}@media(hover:none){.spine-agent-fab__dock{opacity:1}}.spine-agent-panel{position:fixed;inset-inline-end:clamp(var(--hm-space-md),var(--spine-agent-end-now),calc(100vw - var(--spine-agent-panel-inline) - var(--hm-space-md)));inset-block-end:calc(var(--spine-agent-offset) + env(safe-area-inset-bottom,0px));z-index:var(--hm-z-fixed);display:flex;flex-direction:column;inline-size:var(--spine-agent-panel-inline);block-size:min(calc(var(--hm-space-3xl) * 16),var(--spine-agent-room));overflow:hidden;background:var(--hm-surface-raised);color:var(--hm-text);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:calc(var(--hm-radius) * 2);box-shadow:var(--hm-shadow-overlay)}.spine-agent-panel[hidden]{display:none}:host(.spine-agent--below) .spine-agent-panel,:host(.spine-agent--docked) .spine-agent-panel{inset-block-start:var(--spine-agent-offset);inset-block-end:auto}:host(.spine-agent--docked) .spine-agent-panel{inset-inline-end:var(--hm-space-md)}.spine-agent-panel--expanded{--spine-agent-panel-inline: min(calc(var(--hm-space-3xl) * 24), calc(100vw - 2 * var(--hm-space-md)));block-size:var(--spine-agent-room)}.spine-agent-panel--open{transform-origin:bottom right;animation:spine-agent-panel-in var(--hm-duration-quick) var(--hm-ease-decelerate)}:host(.spine-agent--below) .spine-agent-panel--open,:host(.spine-agent--docked) .spine-agent-panel--open{transform-origin:top right}@keyframes spine-agent-panel-in{0%{opacity:0;transform:translateY(var(--hm-space-sm)) scale(.97)}}@media(prefers-reduced-motion:reduce){.spine-agent-panel--open,.spine-agent-greeting{animation:none}.spine-agent-fab__dock{transition:none}}.spine-agent-panel__header{display:flex;align-items:center;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-md);border-block-end:var(--hm-border-hairline) solid var(--hm-divider)}.spine-agent-panel__avatar{flex:none;inline-size:var(--hm-control-height);block-size:var(--hm-control-height);object-fit:cover;border-radius:var(--hm-radius-pill)}.spine-agent-panel__title{flex:1;min-inline-size:0;margin:0;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.spine-agent-panel__body{flex:1;min-block-size:0;padding:var(--hm-space-md)}@media(max-width:600px){:host{--spine-agent-panel-inline: calc(100vw - 2 * var(--hm-space-xs))}.spine-agent-greeting__figure{display:none}.spine-agent-panel,:host(.spine-agent--docked) .spine-agent-panel{inset-inline-end:var(--hm-space-xs)}}\n"] }]
9115
+ }], ctorParameters: () => [] });
9116
+
8104
9117
  /**
8105
9118
  * Aviso de "Encerrar por inatividade" (`SpineIdleService`): contagem regressiva, "Continuar conectado" (padrão, e o
8106
9119
  * que fechar pelo X/Esc faz) e "Sair agora". Montado uma vez no `spine-layout`.
@@ -8185,11 +9198,11 @@ class SpineUnsavedChangesService {
8185
9198
  canReplaceActive(panelId) {
8186
9199
  if (!this.enabled)
8187
9200
  return true;
8188
- const active = findPanelTabs$1(this.nav.store.getState().root, panelId)?.activeId;
9201
+ const active = findPanelTabs(this.nav.store.getState().root, panelId)?.activeId;
8189
9202
  return active && this.unsaved.hasUnsavedChanges(active) ? this.unsaved.confirmDiscard() : true;
8190
9203
  }
8191
9204
  guard(store) {
8192
- const tabsOf = (panelId) => findPanelTabs$1(store.getState().root, panelId)?.tabs.map((t) => t.id) ?? [];
9205
+ const tabsOf = (panelId) => findPanelTabs(store.getState().root, panelId)?.tabs.map((t) => t.id) ?? [];
8193
9206
  // Sem alteração em nenhuma das abas, fecha na hora (mesmo comportamento de antes); com alteração, pergunta.
8194
9207
  const closing = (ids, close) => {
8195
9208
  if (!ids.some((id) => this.unsaved.hasUnsavedChanges(id))) {
@@ -8247,7 +9260,7 @@ class LayoutComponent {
8247
9260
  this.preferences = inject(SpinePreferencesService);
8248
9261
  this.workspace = inject(SpineWorkspaceNavService);
8249
9262
  this.settings = inject(SpineSettingsService);
8250
- this.sidebar = inject(HermesSidebarService$1);
9263
+ this.sidebar = inject(HermesSidebarService);
8251
9264
  /** Também liga, uma vez, o aviso de falha do logout (`logoutFailed$` do `@orfeu/angular`). */
8252
9265
  this.logoutNotice = inject(SpineLogoutService);
8253
9266
  /** Recursos opcionais (`SpineConfig.features`): cada serviço não faz nada com o seu recurso desligado. */
@@ -8351,6 +9364,8 @@ class LayoutComponent {
8351
9364
  <!-- Todos os avisos (respostas do servidor, avisos do app, configurações, auto-serviço…). Se o app também montar
8352
9365
  uma, só uma desenha (o serviço escolhe a pilha ativa): nunca em dobro. -->
8353
9366
  <hm-toast-stack />
9367
+ <!-- Agente de IA do app (SpineFeatures.aiAgent): botão flutuante no canto, que abre a conversa. -->
9368
+ <spine-ai-agent-launcher />
8354
9369
  <spine-side-panel />
8355
9370
  <spine-modal />
8356
9371
  <spine-shortcuts-cheatsheet></spine-shortcuts-cheatsheet>
@@ -8363,7 +9378,7 @@ class LayoutComponent {
8363
9378
  }
8364
9379
  <!-- Aviso de "Encerrar por inatividade" (SpineFeatures.idleTimeout): só desenha quando abre. -->
8365
9380
  <spine-idle-warning />
8366
- `, isInline: true, styles: [":host{display:block}.shell{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--hm-surface);color:var(--hm-text)}.body{position:relative;display:flex;flex:1;min-height:0;overflow:hidden}.main{display:flex;flex:1;min-width:0;flex-direction:column}.workspace-area{flex:1;min-height:0;display:flex;flex-direction:column;--hm-tab-strip-bg: var(--hm-shell-tab-strip-bg);--hm-tab-active-bg: var(--hm-surface-raised)}hm-workspace{flex:1;min-height:0}.panel-content{display:flex;flex-direction:column;height:100%;min-height:0}\n"], dependencies: [{ kind: "component", type: SpineNewsComponent, selector: "spine-news" }, { kind: "component", type: SpineDocsComponent, selector: "spine-docs" }, { kind: "component", type: HeaderComponent, selector: "spine-header" }, { kind: "component", type: SidebarComponent, selector: "spine-sidebar" }, { kind: "component", type: SpineConnectionBannerComponent, selector: "spine-connection-banner" }, { kind: "component", type: SpinePagePanelComponent, selector: "spine-page-panel", inputs: ["url", "isFocusedPanel", "tabId", "panelId"] }, { kind: "directive", type: PanelLocalLinkDirective, selector: "[appPanelLocalLink]", inputs: ["appPanelLocalLink"], outputs: ["localNavigate"] }, { kind: "component", type: WorkspaceComponent, selector: "hm-workspace", inputs: ["initialTabs", "initialActiveId", "keepAlive", "split", "maxColumns", "maxRows", "label", "providedStore"], outputs: ["stateChange", "newTabRequested"] }, { kind: "component", type: ToastStackComponent, selector: "hm-toast-stack", inputs: ["position", "expand", "visibleToasts"] }, { kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: ModalComponent$1, selector: "spine-modal" }, { kind: "component", type: SidePanelComponent, selector: "spine-side-panel" }, { kind: "component", type: SpineInitialSetupComponent, selector: "spine-initial-setup" }, { kind: "component", type: ShortcutsCheatsheetComponent, selector: "spine-shortcuts-cheatsheet" }, { kind: "component", type: SettingsPanelComponent, selector: "spine-settings-panel" }, { kind: "component", type: SpineIdleWarningComponent, selector: "spine-idle-warning" }, { kind: "component", type: SkipLinkComponent, selector: "hm-skip-link", inputs: ["target", "label", "texts"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
9381
+ `, isInline: true, styles: [":host{display:block}.shell{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--hm-surface);color:var(--hm-text)}.body{position:relative;display:flex;flex:1;min-height:0;overflow:hidden}.main{display:flex;flex:1;min-width:0;flex-direction:column}.workspace-area{flex:1;min-height:0;display:flex;flex-direction:column;--hm-tab-strip-bg: var(--hm-shell-tab-strip-bg);--hm-tab-active-bg: var(--hm-surface-raised)}hm-workspace{flex:1;min-height:0}.panel-content{display:flex;flex-direction:column;height:100%;min-height:0}\n"], dependencies: [{ kind: "component", type: SpineNewsComponent, selector: "spine-news" }, { kind: "component", type: SpineDocsComponent, selector: "spine-docs" }, { kind: "component", type: HeaderComponent, selector: "spine-header" }, { kind: "component", type: SidebarComponent, selector: "spine-sidebar" }, { kind: "component", type: SpineConnectionBannerComponent, selector: "spine-connection-banner" }, { kind: "component", type: SpinePagePanelComponent, selector: "spine-page-panel", inputs: ["url", "isFocusedPanel", "tabId", "panelId"] }, { kind: "directive", type: PanelLocalLinkDirective, selector: "[appPanelLocalLink]", inputs: ["appPanelLocalLink"], outputs: ["localNavigate"] }, { kind: "component", type: WorkspaceComponent, selector: "hm-workspace", inputs: ["initialTabs", "initialActiveId", "keepAlive", "split", "maxColumns", "maxRows", "label", "providedStore"], outputs: ["stateChange", "newTabRequested"] }, { kind: "component", type: ToastStackComponent, selector: "hm-toast-stack", inputs: ["position", "expand", "visibleToasts"] }, { kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: ModalComponent$1, selector: "spine-modal" }, { kind: "component", type: SidePanelComponent, selector: "spine-side-panel" }, { kind: "component", type: SpineInitialSetupComponent, selector: "spine-initial-setup" }, { kind: "component", type: ShortcutsCheatsheetComponent, selector: "spine-shortcuts-cheatsheet" }, { kind: "component", type: SettingsPanelComponent, selector: "spine-settings-panel" }, { kind: "component", type: SpineIdleWarningComponent, selector: "spine-idle-warning" }, { kind: "component", type: SkipLinkComponent, selector: "hm-skip-link", inputs: ["target", "label", "texts"] }, { kind: "component", type: SpineAiAgentLauncherComponent, selector: "spine-ai-agent-launcher" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
8367
9382
  }
8368
9383
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LayoutComponent, decorators: [{
8369
9384
  type: Component,
@@ -8385,6 +9400,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
8385
9400
  SettingsPanelComponent,
8386
9401
  SpineIdleWarningComponent,
8387
9402
  SkipLinkComponent,
9403
+ SpineAiAgentLauncherComponent,
8388
9404
  ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
8389
9405
  <!-- "Pular para o conteúdo" (SpineFeatures.skipLink): primeira parada do Tab. -->
8390
9406
  @if (skipLink) {
@@ -8421,6 +9437,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
8421
9437
  <!-- Todos os avisos (respostas do servidor, avisos do app, configurações, auto-serviço…). Se o app também montar
8422
9438
  uma, só uma desenha (o serviço escolhe a pilha ativa): nunca em dobro. -->
8423
9439
  <hm-toast-stack />
9440
+ <!-- Agente de IA do app (SpineFeatures.aiAgent): botão flutuante no canto, que abre a conversa. -->
9441
+ <spine-ai-agent-launcher />
8424
9442
  <spine-side-panel />
8425
9443
  <spine-modal />
8426
9444
  <spine-shortcuts-cheatsheet></spine-shortcuts-cheatsheet>
@@ -8760,5 +9778,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
8760
9778
  * Generated bundle index. Do not edit.
8761
9779
  */
8762
9780
 
8763
- export { SpineConnectionBannerComponent as $, AiAgentComponent as A, SPINE_INBOX_SOURCES as B, SPINE_MARKDOWN_PURIFY_CONFIG as C, SPINE_PREFERENCES_SAVE_DELAY_MS as D, SPINE_RATING_COMMENT_MAX as E, SPINE_RATING_SCORE_MAX as F, SPINE_SETTINGS_ALIASES as G, HeaderComponent as H, SPINE_SETTINGS_KEYS as I, SPINE_SETTINGS_SECTIONS as J, SPINE_SUGGESTIONS_PAGE_ID as K, LayoutComponent as L, SPINE_SUGGESTION_LIMITS as M, NEW_SCREEN_VALUE as N, SPINE_SUGGESTION_PUBLIC_SORTS as O, SPINE_SUGGESTION_PUBLIC_STATUSES as P, SPINE_SUGGESTION_STATUS_ICONS as Q, SPINE_WORKSPACE_LIMITS as R, SpineSuggestionsService as S, SettingsPanelComponent as T, ShortcutService as U, ShortcutsCheatsheetComponent as V, SidebarComponent as W, SpineAccountPreferencesService as X, SpineAccountSecurityService as Y, SpineAppTokensService as Z, SpineBrandComponent as _, SpineInboxService as a, SpineDocsComponent as a0, SpineDocsService as a1, SpineIdleService as a2, SpineIdleWarningComponent as a3, SpineInboxButtonComponent as a4, SpineInitialSetupComponent as a5, SpineInitialSetupService as a6, SpineLogoutService as a7, SpineMarkdownComponent as a8, SpineMyGroupsService as a9, spinePathOf as aA, spineScoreBand as aB, spineStarFills as aC, spineSuggestionsEnabled as aD, spineTagColor as aE, stable as aF, SpineNewsComponent as aa, SpineNewsService as ab, SpineNoticesService as ac, SpinePageActionsDirective as ad, SpinePageActionsSlot as ae, SpineRatingStarsComponent as af, SpineRatingsService as ag, SpineSettingsService as ah, SpineStartPageService as ai, SpineUnsavedChangesService as aj, SpineWorkspaceNavService as ak, accountErrorCode as al, argosAccountApi as am, buildMcpConfig as an, effectiveIdleMinutes as ao, idleLimits as ap, isInternalPath as aq, provideSettingsSection as ar, provideSpineInboxSource as as, provideSpineSuggestionsPage as at, ratingTargetKey as au, registerSpineUnsavedChanges as av, resolveSettingsSection as aw, sanitizeSpineMarkdownHtml as ax, spineNormalizeUrl as ay, spineParamsOf as az, SpineSuggestionChatComponent as b, SPINE_SUGGESTION_STATUS_COLORS as c, SPINE_SUGGESTION_KIND_COLORS as d, SPINE_SUGGESTION_KIND_ICONS as e, SPINE_SUGGESTION_WAITING_COLORS as f, SPINE_SUGGESTION_VISIBLE_SORTS as g, SPINE_SUGGESTION_REASONS as h, SPINE_SUGGESTION_STATUSES as i, SPINE_SUGGESTION_KINDS as j, SPINE_SUGGESTION_WAITING as k, SPINE_TAB as l, SPINE_SUGGESTION_PAGE_PARAM as m, SPINE_SUGGESTION_TAB_PARAM as n, SPINE_SUGGESTION_TAB_CONVERSATION as o, screenName as p, screenModule as q, SPINE_SUGGESTIONS_PAGE_PATH as r, suggestionScreens as s, SpineSuggestionsDialogComponent as t, SpineTagRowComponent as u, AppLauncherComponent as v, SPINE_BRAND as w, SPINE_GLOBAL_PREFERENCE_KEYS as x, SPINE_IDLE_WARNING_SECONDS as y, SPINE_INBOX_POLL_MS as z };
8764
- //# sourceMappingURL=trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs.map
9781
+ export { SpineAppTokensService as $, AppLauncherComponent as A, SPINE_INBOX_POLL_MS as B, SPINE_INBOX_SOURCES as C, SPINE_MARKDOWN_PURIFY_CONFIG as D, SPINE_PREFERENCES_SAVE_DELAY_MS as E, SPINE_RATING_COMMENT_MAX as F, SPINE_RATING_SCORE_MAX as G, HeaderComponent as H, SPINE_SETTINGS_ALIASES as I, SPINE_SETTINGS_KEYS as J, SPINE_SETTINGS_SECTIONS as K, LayoutComponent as L, McpConnectComponent as M, NEW_SCREEN_VALUE as N, SPINE_SUGGESTIONS_PAGE_ID as O, SPINE_SUGGESTION_LIMITS as P, SPINE_SUGGESTION_PUBLIC_SORTS as Q, SPINE_SUGGESTION_PUBLIC_STATUSES as R, SpineSuggestionsService as S, SPINE_SUGGESTION_STATUS_ICONS as T, SPINE_WORKSPACE_LIMITS as U, SettingsPanelComponent as V, ShortcutService as W, ShortcutsCheatsheetComponent as X, SidebarComponent as Y, SpineAccountPreferencesService as Z, SpineAccountSecurityService as _, SpineInboxService as a, SpineBrandComponent as a0, SpineConnectionBannerComponent as a1, SpineDocsComponent as a2, SpineDocsService as a3, SpineIdleService as a4, SpineIdleWarningComponent as a5, SpineInboxButtonComponent as a6, SpineInitialSetupComponent as a7, SpineInitialSetupService as a8, SpineLogoutService as a9, spineParamsOf as aA, spinePathOf as aB, spineScoreBand as aC, spineStarFills as aD, spineSuggestionsEnabled as aE, spineTagColor as aF, stable as aG, SpineMyGroupsService as aa, SpineNewsComponent as ab, SpineNewsService as ac, SpineNoticesService as ad, SpinePageActionsDirective as ae, SpinePageActionsSlot as af, SpineRatingStarsComponent as ag, SpineRatingsService as ah, SpineSettingsService as ai, SpineStartPageService as aj, SpineUnsavedChangesService as ak, SpineWorkspaceNavService as al, accountErrorCode as am, argosAccountApi as an, buildMcpConfig as ao, effectiveIdleMinutes as ap, idleLimits as aq, isInternalPath as ar, provideSettingsSection as as, provideSpineInboxSource as at, provideSpineSuggestionsPage as au, ratingTargetKey as av, registerSpineUnsavedChanges as aw, resolveSettingsSection as ax, sanitizeSpineMarkdownHtml as ay, spineNormalizeUrl as az, SpineSuggestionChatComponent as b, SPINE_SUGGESTION_STATUS_COLORS as c, SPINE_SUGGESTION_KIND_COLORS as d, SPINE_SUGGESTION_KIND_ICONS as e, SPINE_SUGGESTION_WAITING_COLORS as f, SPINE_SUGGESTION_VISIBLE_SORTS as g, SPINE_SUGGESTION_REASONS as h, SPINE_SUGGESTION_STATUSES as i, SPINE_SUGGESTION_KINDS as j, SPINE_SUGGESTION_WAITING as k, SPINE_TAB as l, SPINE_SUGGESTION_PAGE_PARAM as m, SPINE_SUGGESTION_TAB_PARAM as n, SPINE_SUGGESTION_TAB_CONVERSATION as o, screenName as p, screenModule as q, SPINE_SUGGESTIONS_PAGE_PATH as r, suggestionScreens as s, SpineSuggestionsDialogComponent as t, SpineTagRowComponent as u, SpineAiAgentService as v, SpineMarkdownComponent as w, SPINE_BRAND as x, SPINE_GLOBAL_PREFERENCE_KEYS as y, SPINE_IDLE_WARNING_SECONDS as z };
9782
+ //# sourceMappingURL=trismegisto-spine-layout-trismegisto-spine-layout-D3vetDC_.mjs.map