@seniorsistemas/angular-components 19.2.0 → 19.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dynamic-form/dynamic-form/dynamic-form.directive.d.ts +3 -0
  2. package/dynamic-form/dynamic-form/form-field/configurations/fields/field.d.ts +7 -0
  3. package/dynamic-form/public-api.d.ts +2 -3
  4. package/esm2022/dynamic-form/dynamic-form/dynamic-form.directive.mjs +17 -1
  5. package/esm2022/dynamic-form/dynamic-form/form-field/configurations/fields/field.mjs +1 -1
  6. package/esm2022/dynamic-form/public-api.mjs +3 -4
  7. package/esm2022/lib/locale/fallback.mjs +10 -2
  8. package/esm2022/spotlight/lib/spotlight/spotlight-overlay/spotlight-overlay.component.mjs +460 -0
  9. package/esm2022/spotlight/lib/spotlight/spotlight-step.directive.mjs +50 -0
  10. package/esm2022/spotlight/lib/spotlight/spotlight-tour.service.mjs +251 -0
  11. package/esm2022/spotlight/lib/spotlight/spotlight.component.mjs +193 -0
  12. package/esm2022/spotlight/lib/spotlight/types/spotlight-position.mjs +2 -0
  13. package/esm2022/spotlight/lib/spotlight/types/spotlight-step.mjs +2 -0
  14. package/esm2022/spotlight/lib/spotlight/types/spotlight-stop-event.mjs +2 -0
  15. package/esm2022/spotlight/public-api.mjs +4 -0
  16. package/esm2022/spotlight/seniorsistemas-angular-components-spotlight.mjs +5 -0
  17. package/esm2022/table/lib/table/table-column/table-columns.component.mjs +3 -3
  18. package/fesm2022/seniorsistemas-angular-components-dynamic-form.mjs +16 -0
  19. package/fesm2022/seniorsistemas-angular-components-dynamic-form.mjs.map +1 -1
  20. package/fesm2022/seniorsistemas-angular-components-spotlight.mjs +947 -0
  21. package/fesm2022/seniorsistemas-angular-components-spotlight.mjs.map +1 -0
  22. package/fesm2022/seniorsistemas-angular-components-table.mjs +2 -2
  23. package/fesm2022/seniorsistemas-angular-components-table.mjs.map +1 -1
  24. package/fesm2022/seniorsistemas-angular-components.mjs +9 -1
  25. package/fesm2022/seniorsistemas-angular-components.mjs.map +1 -1
  26. package/package.json +7 -1
  27. package/spotlight/README.md +311 -0
  28. package/spotlight/index.d.ts +5 -0
  29. package/spotlight/lib/spotlight/spotlight-overlay/spotlight-overlay.component.d.ts +70 -0
  30. package/spotlight/lib/spotlight/spotlight-step.directive.d.ts +28 -0
  31. package/spotlight/lib/spotlight/spotlight-tour.service.d.ts +146 -0
  32. package/spotlight/lib/spotlight/spotlight.component.d.ts +82 -0
  33. package/spotlight/lib/spotlight/types/spotlight-position.d.ts +1 -0
  34. package/spotlight/lib/spotlight/types/spotlight-step.d.ts +21 -0
  35. package/spotlight/lib/spotlight/types/spotlight-stop-event.d.ts +13 -0
  36. package/spotlight/public-api.d.ts +6 -0
@@ -0,0 +1,947 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, output, signal, TemplateRef, Component, inject, DestroyRef, NgZone, EnvironmentInjector, computed, Injectable, ElementRef, effect, Directive } from '@angular/core';
3
+ import { Overlay } from '@angular/cdk/overlay';
4
+ import { ComponentPortal } from '@angular/cdk/portal';
5
+ import { combineLatest, fromEvent, debounceTime, Subject } from 'rxjs';
6
+ import * as i1 from '@angular/common';
7
+ import { CommonModule, NgStyle } from '@angular/common';
8
+ import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
+ import { FocusTrapFactory } from '@angular/cdk/a11y';
10
+ import * as i3 from '@ngx-translate/core';
11
+ import { TranslateModule } from '@ngx-translate/core';
12
+ import * as i2 from '@seniorsistemas/angular-components/button';
13
+ import { ButtonModule } from '@seniorsistemas/angular-components/button';
14
+ import { CheckboxComponent } from '@seniorsistemas/angular-components/checkbox';
15
+
16
+ /**
17
+ * Componente de popover do Spotlight.
18
+ *
19
+ * Exibe o card de destaque com título, mensagem, seta direcional, contador de passos,
20
+ * botões de navegação e, em modo simples (passo único), o checkbox "não mostrar novamente".
21
+ *
22
+ * Este componente é instanciado programaticamente pelo `SpotlightOverlayComponent`
23
+ * via CDK Overlay. Normalmente não é usado diretamente no template.
24
+ */
25
+ class SpotlightComponent {
26
+ /** Título do popover. Aceita string ou `TemplateRef` para conteúdo dinâmico. */
27
+ title = input('');
28
+ /** Mensagem principal do popover. Aceita string ou `TemplateRef` para conteúdo dinâmico. */
29
+ message = input();
30
+ /** Template opcional para área de mídia/conteúdo customizado acima da mensagem. */
31
+ content = input(null);
32
+ /** Posição do popover em relação ao elemento alvo. @default 'bottom-center' */
33
+ position = input('bottom-center');
34
+ /** Número do passo atual (1-based) exibido no contador. @default 1 */
35
+ currentStep = input(1);
36
+ /** Total de passos do tour, usado no contador e para controle de botões. @default 1 */
37
+ totalSteps = input(1);
38
+ /**
39
+ * Exibe o checkbox "não mostrar novamente" (apenas em passo único).
40
+ * Passe `false` para suprimir o checkbox quando não for necessário persistir a preferência.
41
+ * @default true
42
+ */
43
+ showDoNotShowAgain = input(true);
44
+ /** Lista de botões de ação adicionais exibidos na área de rodapé. */
45
+ actions = input([]);
46
+ /**
47
+ * Deslocamento em pixels para posicionar a seta manualmente no eixo principal.
48
+ * Quando `null`, o alinhamento é controlado pela `position`. @default null
49
+ */
50
+ arrowOffsetPx = input(null);
51
+ /** Emitido ao clicar no botão de fechar (X). */
52
+ closed = output();
53
+ /** Emitido ao clicar em "Próximo". */
54
+ next = output();
55
+ /** Emitido ao clicar em "Voltar". */
56
+ previous = output();
57
+ /** Emitido ao clicar em "Entendido" (modo simples, passo único). */
58
+ understand = output();
59
+ /** Estado atual do checkbox "não mostrar novamente". */
60
+ isDoNotShowAgainChecked = signal(false);
61
+ static instanceCount = 0;
62
+ /** @internal */
63
+ instanceId = `spotlight-${++SpotlightComponent.instanceCount}`;
64
+ /** @internal */
65
+ titleId = `${this.instanceId}-title`;
66
+ /** @internal */
67
+ descId = `${this.instanceId}-desc`;
68
+ get titleTemplate() {
69
+ const v = this.title();
70
+ return v instanceof TemplateRef ? v : null;
71
+ }
72
+ get titleString() {
73
+ const v = this.title();
74
+ return v instanceof TemplateRef ? '' : (v ?? '');
75
+ }
76
+ get messageTemplate() {
77
+ const v = this.message();
78
+ return v instanceof TemplateRef ? v : null;
79
+ }
80
+ get messageString() {
81
+ const v = this.message();
82
+ return v instanceof TemplateRef ? '' : (v ?? '');
83
+ }
84
+ get arrowPosition() {
85
+ const [direction] = this.position().split('-');
86
+ const [, alignment] = this.position().split('-');
87
+ const oppositeDirection = this.getOppositeDirection(direction);
88
+ return `${oppositeDirection}-${alignment}`;
89
+ }
90
+ get isArrowTop() {
91
+ return this.arrowPosition.startsWith('top-');
92
+ }
93
+ get isArrowBottom() {
94
+ return this.arrowPosition.startsWith('bottom-');
95
+ }
96
+ get isArrowLeft() {
97
+ return this.arrowPosition.startsWith('left-');
98
+ }
99
+ get isArrowRight() {
100
+ return this.arrowPosition.startsWith('right-');
101
+ }
102
+ get arrowRotation() {
103
+ const direction = this.arrowPosition.split('-')[0];
104
+ switch (direction) {
105
+ case 'top':
106
+ return '';
107
+ case 'bottom':
108
+ return 'rotate-180';
109
+ case 'left':
110
+ return '-rotate-90';
111
+ case 'right':
112
+ return 'rotate-90';
113
+ default:
114
+ return '';
115
+ }
116
+ }
117
+ get arrowAlignmentClass() {
118
+ const offset = this.arrowOffsetPx();
119
+ if (offset !== null) {
120
+ return 'justify-start';
121
+ }
122
+ const alignment = this.arrowPosition.split('-')[1];
123
+ if (this.isArrowTop || this.isArrowBottom) {
124
+ switch (alignment) {
125
+ case 'start':
126
+ return 'justify-start';
127
+ case 'center':
128
+ return 'justify-center';
129
+ case 'end':
130
+ return 'justify-end';
131
+ default:
132
+ return '';
133
+ }
134
+ }
135
+ else {
136
+ switch (alignment) {
137
+ case 'start':
138
+ return 'items-start justify-start';
139
+ case 'center':
140
+ return 'items-center justify-center';
141
+ case 'end':
142
+ return 'items-end justify-end';
143
+ default:
144
+ return '';
145
+ }
146
+ }
147
+ }
148
+ get arrowPaddingStyle() {
149
+ const offset = this.arrowOffsetPx();
150
+ if (offset === null) {
151
+ return {};
152
+ }
153
+ if (this.isArrowTop || this.isArrowBottom) {
154
+ return { 'padding-left': `${offset}px`, 'padding-right': '0' };
155
+ }
156
+ return { 'padding-top': `${offset}px`, 'padding-bottom': '0' };
157
+ }
158
+ /** Emite o evento `closed`. Chamado pelo botão de fechar no template. */
159
+ onClose() {
160
+ this.closed.emit();
161
+ }
162
+ /** Emite o evento `next`. Chamado pelo botão "Próximo" no template. */
163
+ onNext() {
164
+ this.next.emit();
165
+ }
166
+ /** Emite o evento `previous`. Chamado pelo botão "Voltar" no template. */
167
+ onPrevious() {
168
+ this.previous.emit();
169
+ }
170
+ /** Emite o evento `understand`. Chamado pelo botão "Entendido" no template. */
171
+ onUnderstand() {
172
+ this.understand.emit();
173
+ }
174
+ /** Alterna o estado de `isDoNotShowAgainChecked`. */
175
+ toggleDoNotShowAgain() {
176
+ this.isDoNotShowAgainChecked.update((v) => !v);
177
+ }
178
+ getOppositeDirection(direction) {
179
+ switch (direction) {
180
+ case 'top':
181
+ return 'bottom';
182
+ case 'bottom':
183
+ return 'top';
184
+ case 'left':
185
+ return 'right';
186
+ case 'right':
187
+ return 'left';
188
+ default:
189
+ return direction;
190
+ }
191
+ }
192
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpotlightComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
193
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SpotlightComponent, isStandalone: true, selector: "s-spotlight", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, currentStep: { classPropertyName: "currentStep", publicName: "currentStep", isSignal: true, isRequired: false, transformFunction: null }, totalSteps: { classPropertyName: "totalSteps", publicName: "totalSteps", isSignal: true, isRequired: false, transformFunction: null }, showDoNotShowAgain: { classPropertyName: "showDoNotShowAgain", publicName: "showDoNotShowAgain", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, arrowOffsetPx: { classPropertyName: "arrowOffsetPx", publicName: "arrowOffsetPx", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", next: "next", previous: "previous", understand: "understand" }, ngImport: i0, template: "<div\n [attr.role]=\"totalSteps() > 1 ? 'dialog' : 'tooltip'\"\n [attr.aria-modal]=\"totalSteps() > 1 ? 'true' : null\"\n [attr.aria-labelledby]=\"titleId\"\n [attr.aria-describedby]=\"descId\"\n class=\"flex max-w-[342px]\"\n [ngClass]=\"isArrowLeft || isArrowRight ? 'flex-row' : 'flex-col'\"\n>\n @if (isArrowTop) {\n <ng-container *ngTemplateOutlet=\"arrowTopTemplate\"></ng-container>\n }\n\n @if (isArrowLeft) {\n <ng-container *ngTemplateOutlet=\"arrowLeftTemplate\"></ng-container>\n }\n\n <div\n class=\"relative flex w-full flex-col overflow-hidden rounded-[4px] bg-grayscale-0\"\n style=\"\n box-shadow:\n 0 3px 5px rgba(0, 0, 0, 0.2),\n 0 1px 18px rgba(0, 0, 0, 0.12),\n 0 6px 10px rgba(0, 0, 0, 0.14);\n \"\n >\n <!-- Regi\u00E3o aria-live para anuncia\u00E7\u00E3o de mudan\u00E7a de passo para leitores de tela -->\n <span\n class=\"sr-only\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n @if (totalSteps() > 1) {\n {{\n 'platform.angular_components.step_progress'\n | translate: { current: currentStep(), total: totalSteps() }\n }}\n }\n </span>\n <div class=\"flex h-[40px] w-full items-center justify-between p-[12px]\">\n <div class=\"flex min-w-0 flex-1 items-center gap-[8px]\">\n <h2\n [id]=\"titleId\"\n class=\"flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap font-open-sans text-[14px] font-bold leading-[1.5] text-grayscale-90\"\n >\n @if (titleTemplate) {\n <ng-container *ngTemplateOutlet=\"titleTemplate\"></ng-container>\n } @else {\n {{ titleString }}\n }\n </h2>\n </div>\n <button\n (click)=\"onClose()\"\n class=\"flex h-[16px] w-[16px] flex-none items-center justify-center text-grayscale-90 transition-colors hover:text-grayscale-100\"\n [attr.aria-label]=\"'platform.angular_components.close' | translate\"\n >\n <i class=\"fas fa-times\"></i>\n </button>\n </div>\n\n @if (content()) {\n <ng-container *ngTemplateOutlet=\"content()\"></ng-container>\n }\n\n <div class=\"flex w-full flex-col items-start gap-[20px] bg-grayscale-0 p-[12px]\">\n <div\n [id]=\"descId\"\n class=\"w-full whitespace-pre-wrap font-open-sans text-[14px] font-normal leading-[1.5] text-grayscale-90\"\n >\n @if (messageTemplate) {\n <ng-container *ngTemplateOutlet=\"messageTemplate\"></ng-container>\n } @else {\n {{ messageString }}\n }\n </div>\n\n <div class=\"flex w-full flex-wrap gap-3\">\n @if (totalSteps() > 1) {\n <p class=\"font-open-sans text-[14px] font-normal leading-[1.5] text-grayscale-90\">\n {{\n 'platform.angular_components.step_counter'\n | translate: { current: currentStep(), total: totalSteps() }\n }}\n </p>\n }\n\n @if (totalSteps() === 1 && showDoNotShowAgain()) {\n <s-checkbox\n [checked]=\"isDoNotShowAgainChecked()\"\n (checkedChange)=\"toggleDoNotShowAgain()\"\n [label]=\"'platform.angular_components.dont_show_again' | translate\"\n ></s-checkbox>\n }\n\n <div\n class=\"flex flex-wrap items-start justify-end gap-[8px]\"\n [ngClass]=\"totalSteps() > 1 ? 'w-full' : ''\"\n >\n @for (action of actions(); track action.label) {\n <s-button\n [label]=\"action.label\"\n (clicked)=\"action.handler()\"\n priority=\"default\"\n size=\"small\"\n class=\"flex-none\"\n ></s-button>\n }\n\n @if (totalSteps() > 1 && currentStep() > 1) {\n <s-button\n [label]=\"'platform.angular_components.back' | translate\"\n (clicked)=\"onPrevious()\"\n priority=\"default\"\n size=\"small\"\n class=\"flex-none\"\n ></s-button>\n }\n\n <s-button\n [label]=\"\n totalSteps() <= 1\n ? ('platform.angular_components.understood' | translate)\n : currentStep() < totalSteps()\n ? ('platform.angular_components.next' | translate)\n : ('platform.angular_components.complete' | translate)\n \"\n (clicked)=\"totalSteps() > 1 ? onNext() : onUnderstand()\"\n priority=\"secondary\"\n size=\"small\"\n class=\"flex-none\"\n ></s-button>\n </div>\n </div>\n </div>\n </div>\n\n @if (isArrowBottom) {\n <ng-container *ngTemplateOutlet=\"arrowBottomTemplate\"></ng-container>\n }\n\n @if (isArrowRight) {\n <ng-container *ngTemplateOutlet=\"arrowRightTemplate\"></ng-container>\n }\n</div>\n\n<!-- Arrow Templates -->\n<ng-template #arrowTopTemplate>\n <div\n class=\"relative z-10 flex h-[8px] w-full px-[12px] py-0\"\n [ngClass]=\"arrowAlignmentClass\"\n [ngStyle]=\"arrowPaddingStyle\"\n >\n <svg\n width=\"16\"\n height=\"8\"\n viewBox=\"0 0 16 8\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-grayscale-0\"\n [ngClass]=\"arrowRotation\"\n >\n <path\n d=\"M0 8L8 0L16 8\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n</ng-template>\n\n<ng-template #arrowBottomTemplate>\n <div\n class=\"relative z-10 flex h-[8px] w-full px-[12px] py-0\"\n [ngClass]=\"arrowAlignmentClass\"\n [ngStyle]=\"arrowPaddingStyle\"\n >\n <svg\n width=\"16\"\n height=\"8\"\n viewBox=\"0 0 16 8\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-grayscale-0\"\n [ngClass]=\"arrowRotation\"\n >\n <path\n d=\"M0 8L8 0L16 8\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n</ng-template>\n\n<ng-template #arrowLeftTemplate>\n <div\n class=\"relative z-10 flex w-[8px] flex-col items-center self-stretch px-0\"\n [ngClass]=\"arrowAlignmentClass\"\n [ngStyle]=\"arrowPaddingStyle\"\n >\n <svg\n width=\"16\"\n height=\"8\"\n viewBox=\"0 0 16 8\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-grayscale-0\"\n [ngClass]=\"arrowRotation\"\n >\n <path\n d=\"M0 8L8 0L16 8\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n</ng-template>\n\n<ng-template #arrowRightTemplate>\n <div\n class=\"relative z-10 flex w-[8px] flex-col items-center self-stretch px-0\"\n [ngClass]=\"arrowAlignmentClass\"\n [ngStyle]=\"arrowPaddingStyle\"\n >\n <svg\n width=\"16\"\n height=\"8\"\n viewBox=\"0 0 16 8\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-grayscale-0\"\n [ngClass]=\"arrowRotation\"\n >\n <path\n d=\"M0 8L8 0L16 8\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n</ng-template>\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "s-button", inputs: ["id", "label", "tooltip", "tooltipPosition", "iconClass", "rightIconClass", "caret", "styleClass", "baseZIndex", "disabled", "auxiliary", "type", "priority", "menuOptions", "size", "slide", "animation", "badge", "iconColor", "menuAriaLabel"], outputs: ["clicked"] }, { kind: "component", type: CheckboxComponent, selector: "s-checkbox", inputs: ["disabled", "checked", "indeterminate", "label", "ariaLabel", "ariaLabelledBy"], outputs: ["disabledChange", "checkedChange"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }] });
194
+ }
195
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpotlightComponent, decorators: [{
196
+ type: Component,
197
+ args: [{ selector: 's-spotlight', standalone: true, imports: [CommonModule, ButtonModule, CheckboxComponent, TranslateModule], template: "<div\n [attr.role]=\"totalSteps() > 1 ? 'dialog' : 'tooltip'\"\n [attr.aria-modal]=\"totalSteps() > 1 ? 'true' : null\"\n [attr.aria-labelledby]=\"titleId\"\n [attr.aria-describedby]=\"descId\"\n class=\"flex max-w-[342px]\"\n [ngClass]=\"isArrowLeft || isArrowRight ? 'flex-row' : 'flex-col'\"\n>\n @if (isArrowTop) {\n <ng-container *ngTemplateOutlet=\"arrowTopTemplate\"></ng-container>\n }\n\n @if (isArrowLeft) {\n <ng-container *ngTemplateOutlet=\"arrowLeftTemplate\"></ng-container>\n }\n\n <div\n class=\"relative flex w-full flex-col overflow-hidden rounded-[4px] bg-grayscale-0\"\n style=\"\n box-shadow:\n 0 3px 5px rgba(0, 0, 0, 0.2),\n 0 1px 18px rgba(0, 0, 0, 0.12),\n 0 6px 10px rgba(0, 0, 0, 0.14);\n \"\n >\n <!-- Regi\u00E3o aria-live para anuncia\u00E7\u00E3o de mudan\u00E7a de passo para leitores de tela -->\n <span\n class=\"sr-only\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n @if (totalSteps() > 1) {\n {{\n 'platform.angular_components.step_progress'\n | translate: { current: currentStep(), total: totalSteps() }\n }}\n }\n </span>\n <div class=\"flex h-[40px] w-full items-center justify-between p-[12px]\">\n <div class=\"flex min-w-0 flex-1 items-center gap-[8px]\">\n <h2\n [id]=\"titleId\"\n class=\"flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap font-open-sans text-[14px] font-bold leading-[1.5] text-grayscale-90\"\n >\n @if (titleTemplate) {\n <ng-container *ngTemplateOutlet=\"titleTemplate\"></ng-container>\n } @else {\n {{ titleString }}\n }\n </h2>\n </div>\n <button\n (click)=\"onClose()\"\n class=\"flex h-[16px] w-[16px] flex-none items-center justify-center text-grayscale-90 transition-colors hover:text-grayscale-100\"\n [attr.aria-label]=\"'platform.angular_components.close' | translate\"\n >\n <i class=\"fas fa-times\"></i>\n </button>\n </div>\n\n @if (content()) {\n <ng-container *ngTemplateOutlet=\"content()\"></ng-container>\n }\n\n <div class=\"flex w-full flex-col items-start gap-[20px] bg-grayscale-0 p-[12px]\">\n <div\n [id]=\"descId\"\n class=\"w-full whitespace-pre-wrap font-open-sans text-[14px] font-normal leading-[1.5] text-grayscale-90\"\n >\n @if (messageTemplate) {\n <ng-container *ngTemplateOutlet=\"messageTemplate\"></ng-container>\n } @else {\n {{ messageString }}\n }\n </div>\n\n <div class=\"flex w-full flex-wrap gap-3\">\n @if (totalSteps() > 1) {\n <p class=\"font-open-sans text-[14px] font-normal leading-[1.5] text-grayscale-90\">\n {{\n 'platform.angular_components.step_counter'\n | translate: { current: currentStep(), total: totalSteps() }\n }}\n </p>\n }\n\n @if (totalSteps() === 1 && showDoNotShowAgain()) {\n <s-checkbox\n [checked]=\"isDoNotShowAgainChecked()\"\n (checkedChange)=\"toggleDoNotShowAgain()\"\n [label]=\"'platform.angular_components.dont_show_again' | translate\"\n ></s-checkbox>\n }\n\n <div\n class=\"flex flex-wrap items-start justify-end gap-[8px]\"\n [ngClass]=\"totalSteps() > 1 ? 'w-full' : ''\"\n >\n @for (action of actions(); track action.label) {\n <s-button\n [label]=\"action.label\"\n (clicked)=\"action.handler()\"\n priority=\"default\"\n size=\"small\"\n class=\"flex-none\"\n ></s-button>\n }\n\n @if (totalSteps() > 1 && currentStep() > 1) {\n <s-button\n [label]=\"'platform.angular_components.back' | translate\"\n (clicked)=\"onPrevious()\"\n priority=\"default\"\n size=\"small\"\n class=\"flex-none\"\n ></s-button>\n }\n\n <s-button\n [label]=\"\n totalSteps() <= 1\n ? ('platform.angular_components.understood' | translate)\n : currentStep() < totalSteps()\n ? ('platform.angular_components.next' | translate)\n : ('platform.angular_components.complete' | translate)\n \"\n (clicked)=\"totalSteps() > 1 ? onNext() : onUnderstand()\"\n priority=\"secondary\"\n size=\"small\"\n class=\"flex-none\"\n ></s-button>\n </div>\n </div>\n </div>\n </div>\n\n @if (isArrowBottom) {\n <ng-container *ngTemplateOutlet=\"arrowBottomTemplate\"></ng-container>\n }\n\n @if (isArrowRight) {\n <ng-container *ngTemplateOutlet=\"arrowRightTemplate\"></ng-container>\n }\n</div>\n\n<!-- Arrow Templates -->\n<ng-template #arrowTopTemplate>\n <div\n class=\"relative z-10 flex h-[8px] w-full px-[12px] py-0\"\n [ngClass]=\"arrowAlignmentClass\"\n [ngStyle]=\"arrowPaddingStyle\"\n >\n <svg\n width=\"16\"\n height=\"8\"\n viewBox=\"0 0 16 8\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-grayscale-0\"\n [ngClass]=\"arrowRotation\"\n >\n <path\n d=\"M0 8L8 0L16 8\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n</ng-template>\n\n<ng-template #arrowBottomTemplate>\n <div\n class=\"relative z-10 flex h-[8px] w-full px-[12px] py-0\"\n [ngClass]=\"arrowAlignmentClass\"\n [ngStyle]=\"arrowPaddingStyle\"\n >\n <svg\n width=\"16\"\n height=\"8\"\n viewBox=\"0 0 16 8\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-grayscale-0\"\n [ngClass]=\"arrowRotation\"\n >\n <path\n d=\"M0 8L8 0L16 8\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n</ng-template>\n\n<ng-template #arrowLeftTemplate>\n <div\n class=\"relative z-10 flex w-[8px] flex-col items-center self-stretch px-0\"\n [ngClass]=\"arrowAlignmentClass\"\n [ngStyle]=\"arrowPaddingStyle\"\n >\n <svg\n width=\"16\"\n height=\"8\"\n viewBox=\"0 0 16 8\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-grayscale-0\"\n [ngClass]=\"arrowRotation\"\n >\n <path\n d=\"M0 8L8 0L16 8\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n</ng-template>\n\n<ng-template #arrowRightTemplate>\n <div\n class=\"relative z-10 flex w-[8px] flex-col items-center self-stretch px-0\"\n [ngClass]=\"arrowAlignmentClass\"\n [ngStyle]=\"arrowPaddingStyle\"\n >\n <svg\n width=\"16\"\n height=\"8\"\n viewBox=\"0 0 16 8\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-grayscale-0\"\n [ngClass]=\"arrowRotation\"\n >\n <path\n d=\"M0 8L8 0L16 8\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n</ng-template>\n\n" }]
198
+ }] });
199
+
200
+ const POPOVER_WIDTH = 342;
201
+ const SPOTLIGHT_PADDING = 8;
202
+ const ARROW_HALF_SIZE = 8;
203
+ const ARROW_OFFSET_MIN = 4;
204
+ const ARROW_OFFSET_MAX = POPOVER_WIDTH - 4 - 16;
205
+ const MASK_BG = 'rgba(0,0,0,0.5)';
206
+ const MASK_TRANSITION = 'all 0.2s ease';
207
+ const OVERLAY_Z = '10000';
208
+ const POPOVER_Z = '10001';
209
+ function toConnectedPositions(position) {
210
+ const [direction, alignment] = position.split('-');
211
+ const p = SPOTLIGHT_PADDING + 2;
212
+ const hAlign = alignment;
213
+ const vAlignMap = { start: 'top', center: 'center', end: 'bottom' };
214
+ const vAlign = vAlignMap[alignment] ?? 'center';
215
+ let primary;
216
+ let opposite;
217
+ switch (direction) {
218
+ case 'top':
219
+ primary = { originX: hAlign, originY: 'top', overlayX: hAlign, overlayY: 'bottom', offsetY: -p };
220
+ opposite = { originX: hAlign, originY: 'bottom', overlayX: hAlign, overlayY: 'top', offsetY: p };
221
+ break;
222
+ case 'right':
223
+ primary = { originX: 'end', originY: vAlign, overlayX: 'start', overlayY: vAlign, offsetX: p };
224
+ opposite = { originX: 'start', originY: vAlign, overlayX: 'end', overlayY: vAlign, offsetX: -p };
225
+ break;
226
+ case 'left':
227
+ primary = { originX: 'start', originY: vAlign, overlayX: 'end', overlayY: vAlign, offsetX: -p };
228
+ opposite = { originX: 'end', originY: vAlign, overlayX: 'start', overlayY: vAlign, offsetX: p };
229
+ break;
230
+ default: // 'bottom'
231
+ primary = { originX: hAlign, originY: 'bottom', overlayX: hAlign, overlayY: 'top', offsetY: p };
232
+ opposite = { originX: hAlign, originY: 'top', overlayX: hAlign, overlayY: 'bottom', offsetY: -p };
233
+ }
234
+ return [
235
+ primary,
236
+ opposite,
237
+ { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: p },
238
+ { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -p },
239
+ { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: p },
240
+ { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center', offsetX: -p },
241
+ ];
242
+ }
243
+ class SpotlightOverlayComponent {
244
+ tourService = inject(SpotlightTourService);
245
+ destroyRef = inject(DestroyRef);
246
+ zone = inject(NgZone);
247
+ cdkOverlay = inject(Overlay);
248
+ environmentInjector = inject(EnvironmentInjector);
249
+ focusTrapFactory = inject(FocusTrapFactory);
250
+ /** @internal */
251
+ isActive = this.tourService.isActive;
252
+ /** @internal */
253
+ currentStep = this.tourService.currentStep;
254
+ /** @internal */
255
+ currentIndex = this.tourService.currentIndex;
256
+ /** @internal */
257
+ totalSteps = this.tourService.totalSteps;
258
+ /** @internal*/
259
+ targetRect = signal(null);
260
+ /** @internal */
261
+ topMaskStyle = computed(() => {
262
+ const rect = this.targetRect();
263
+ const base = {
264
+ position: 'fixed',
265
+ 'z-index': OVERLAY_Z,
266
+ background: MASK_BG,
267
+ transition: MASK_TRANSITION,
268
+ cursor: 'default',
269
+ };
270
+ if (!rect) {
271
+ return { ...base, top: '0', left: '0', right: '0', bottom: '0' };
272
+ }
273
+ const top = Math.max(0, rect.top - SPOTLIGHT_PADDING);
274
+ return { ...base, top: '0', left: '0', right: '0', height: `${top}px` };
275
+ });
276
+ /** @internal */
277
+ leftMaskStyle = computed(() => {
278
+ const rect = this.targetRect();
279
+ if (!rect) {
280
+ return {};
281
+ }
282
+ const p = SPOTLIGHT_PADDING;
283
+ const top = Math.max(0, rect.top - p);
284
+ const bottom = Math.min(window.innerHeight, rect.bottom + p);
285
+ const left = Math.max(0, rect.left - p);
286
+ return {
287
+ position: 'fixed',
288
+ 'z-index': OVERLAY_Z,
289
+ background: MASK_BG,
290
+ transition: MASK_TRANSITION,
291
+ cursor: 'default',
292
+ top: `${top}px`,
293
+ left: '0',
294
+ width: `${left}px`,
295
+ height: `${bottom - top}px`,
296
+ };
297
+ });
298
+ /** @internal */
299
+ rightMaskStyle = computed(() => {
300
+ const rect = this.targetRect();
301
+ if (!rect) {
302
+ return {};
303
+ }
304
+ const p = SPOTLIGHT_PADDING;
305
+ const top = Math.max(0, rect.top - p);
306
+ const bottom = Math.min(window.innerHeight, rect.bottom + p);
307
+ const right = Math.min(window.innerWidth, rect.right + p);
308
+ return {
309
+ position: 'fixed',
310
+ 'z-index': OVERLAY_Z,
311
+ background: MASK_BG,
312
+ transition: MASK_TRANSITION,
313
+ cursor: 'default',
314
+ top: `${top}px`,
315
+ left: `${right}px`,
316
+ right: '0',
317
+ height: `${bottom - top}px`,
318
+ };
319
+ });
320
+ /** @internal */
321
+ bottomMaskStyle = computed(() => {
322
+ const rect = this.targetRect();
323
+ if (!rect) {
324
+ return {};
325
+ }
326
+ const p = SPOTLIGHT_PADDING;
327
+ const bottom = Math.min(window.innerHeight, rect.bottom + p);
328
+ return {
329
+ position: 'fixed',
330
+ 'z-index': OVERLAY_Z,
331
+ background: MASK_BG,
332
+ transition: MASK_TRANSITION,
333
+ cursor: 'default',
334
+ top: `${bottom}px`,
335
+ left: '0',
336
+ right: '0',
337
+ bottom: '0',
338
+ };
339
+ });
340
+ /** @internal */
341
+ transparentBlockerStyle = computed(() => ({
342
+ position: 'fixed',
343
+ top: '0',
344
+ left: '0',
345
+ right: '0',
346
+ bottom: '0',
347
+ 'z-index': OVERLAY_Z,
348
+ cursor: 'default',
349
+ }));
350
+ resizeObserver = null;
351
+ popoverOverlayRef = null;
352
+ popoverComponentRef = null;
353
+ popoverSubs = [];
354
+ focusTrap = null;
355
+ previouslyFocusedElement = null;
356
+ constructor() {
357
+ combineLatest([
358
+ toObservable(this.currentStep),
359
+ toObservable(this.isActive),
360
+ toObservable(this.tourService.registrationVersion),
361
+ ])
362
+ .pipe(takeUntilDestroyed(this.destroyRef))
363
+ .subscribe(([step, active]) => {
364
+ this.disconnectResizeObserver();
365
+ if (!step || !active) {
366
+ this.targetRect.set(null);
367
+ this.destroyPopoverOverlay();
368
+ return;
369
+ }
370
+ const el = this.tourService.getElement(step.stepId);
371
+ if (el?.nativeElement) {
372
+ el.nativeElement.scrollIntoView({ block: 'nearest' });
373
+ const target = el.nativeElement;
374
+ this.zone.runOutsideAngular(() => {
375
+ requestAnimationFrame(() => {
376
+ this.zone.run(() => {
377
+ if (!this.isActive()) {
378
+ return;
379
+ }
380
+ this.updateTargetRect(target);
381
+ this.observeElement(target);
382
+ this.updatePopoverOverlay(target, step);
383
+ });
384
+ });
385
+ });
386
+ }
387
+ else {
388
+ this.targetRect.set(null);
389
+ this.updatePopoverOverlay(null, step);
390
+ }
391
+ });
392
+ }
393
+ ngOnInit() {
394
+ this.zone.runOutsideAngular(() => {
395
+ const scrollOpts = { capture: true, passive: true };
396
+ fromEvent(document, 'scroll', scrollOpts)
397
+ .pipe(debounceTime(16), takeUntilDestroyed(this.destroyRef))
398
+ .subscribe(() => this.zone.run(() => {
399
+ this.refreshRect();
400
+ this.popoverOverlayRef?.updatePosition();
401
+ this.scheduleSync();
402
+ }));
403
+ fromEvent(window, 'resize', { passive: true })
404
+ .pipe(debounceTime(16), takeUntilDestroyed(this.destroyRef))
405
+ .subscribe(() => this.zone.run(() => {
406
+ this.refreshRect();
407
+ this.popoverOverlayRef?.updatePosition();
408
+ this.scheduleSync();
409
+ }));
410
+ fromEvent(document, 'keydown')
411
+ .pipe(takeUntilDestroyed(this.destroyRef))
412
+ .subscribe((event) => {
413
+ if (event.key === 'Escape') {
414
+ this.zone.run(() => this.onDismiss());
415
+ }
416
+ });
417
+ });
418
+ }
419
+ ngOnDestroy() {
420
+ this.destroyPopoverOverlay();
421
+ }
422
+ /** @internal */
423
+ onNext() {
424
+ this.tourService.next();
425
+ }
426
+ /** @internal */
427
+ onPrevious() {
428
+ this.tourService.previous();
429
+ }
430
+ /** @internal */
431
+ onClose() {
432
+ this.tourService.stop();
433
+ }
434
+ /** @internal */
435
+ onDismiss() {
436
+ const step = this.currentStep();
437
+ if (!step || step.dismissible === false) {
438
+ return;
439
+ }
440
+ this.tourService.stop({
441
+ reason: this.totalSteps() === 1 ? 'dismissed' : 'interrupted',
442
+ doNotShowAgain: false,
443
+ });
444
+ }
445
+ updatePopoverOverlay(targetEl, step) {
446
+ const strategy = targetEl
447
+ ? this.cdkOverlay
448
+ .position()
449
+ .flexibleConnectedTo(targetEl)
450
+ .withPositions(toConnectedPositions(step.position ?? 'bottom-center'))
451
+ .withFlexibleDimensions(false)
452
+ .withPush(true)
453
+ .withViewportMargin(8)
454
+ : this.cdkOverlay.position().global().centerHorizontally().centerVertically();
455
+ if (!this.popoverOverlayRef) {
456
+ this.popoverOverlayRef = this.cdkOverlay.create({
457
+ hasBackdrop: false,
458
+ scrollStrategy: targetEl
459
+ ? this.cdkOverlay.scrollStrategies.reposition()
460
+ : this.cdkOverlay.scrollStrategies.noop(),
461
+ positionStrategy: strategy,
462
+ width: POPOVER_WIDTH,
463
+ });
464
+ this.setPopoverZIndex();
465
+ this.attachPopoverPortal(step);
466
+ }
467
+ else {
468
+ this.popoverOverlayRef.updatePositionStrategy(strategy);
469
+ this.popoverOverlayRef.updatePosition();
470
+ this.updatePopoverInputs(step);
471
+ if (targetEl) {
472
+ this.scheduleSync();
473
+ }
474
+ this.scheduleFocusTrapRefocus();
475
+ }
476
+ }
477
+ setPopoverZIndex() {
478
+ if (!this.popoverOverlayRef) {
479
+ return;
480
+ }
481
+ this.popoverOverlayRef.overlayElement.style.zIndex = POPOVER_Z;
482
+ const host = this.popoverOverlayRef.hostElement;
483
+ if (host !== this.popoverOverlayRef.overlayElement) {
484
+ host.style.zIndex = POPOVER_Z;
485
+ }
486
+ }
487
+ attachPopoverPortal(step) {
488
+ this.previouslyFocusedElement = document.activeElement;
489
+ const portal = new ComponentPortal(SpotlightComponent, null, this.environmentInjector);
490
+ this.popoverComponentRef = this.popoverOverlayRef.attach(portal);
491
+ this.updatePopoverInputs(step);
492
+ this.scheduleSync();
493
+ if (this.totalSteps() > 1) {
494
+ this.activateTourMode();
495
+ }
496
+ this.popoverSubs.push(this.popoverComponentRef.instance.next.subscribe(() => {
497
+ const doNotShowAgain = this.popoverComponentRef?.instance.isDoNotShowAgainChecked() ?? false;
498
+ this.tourService.next(doNotShowAgain);
499
+ }), this.popoverComponentRef.instance.previous.subscribe(() => this.tourService.previous()), this.popoverComponentRef.instance.closed.subscribe(() => this.tourService.stop({
500
+ reason: this.totalSteps() === 1 ? 'dismissed' : 'interrupted',
501
+ doNotShowAgain: false,
502
+ })), this.popoverComponentRef.instance.understand.subscribe(() => {
503
+ const doNotShowAgain = this.popoverComponentRef?.instance.isDoNotShowAgainChecked() ?? false;
504
+ this.tourService.stop({ reason: 'completed', doNotShowAgain });
505
+ }));
506
+ }
507
+ updatePopoverInputs(step) {
508
+ const ref = this.popoverComponentRef;
509
+ if (!ref) {
510
+ return;
511
+ }
512
+ ref.setInput('title', step.title);
513
+ ref.setInput('message', step.message);
514
+ ref.setInput('position', step.position ?? 'bottom-center');
515
+ ref.setInput('currentStep', this.currentIndex() + 1);
516
+ ref.setInput('totalSteps', this.totalSteps());
517
+ ref.setInput('content', step.content ?? null);
518
+ ref.setInput('actions', step.actions ?? []);
519
+ ref.setInput('showDoNotShowAgain', step.showDoNotShowAgain ?? true);
520
+ ref.setInput('arrowOffsetPx', null);
521
+ }
522
+ scheduleSync() {
523
+ this.zone.runOutsideAngular(() => {
524
+ requestAnimationFrame(() => this.zone.run(() => this.syncDirectionAndArrow()));
525
+ });
526
+ }
527
+ activateTourMode() {
528
+ document.body.style.overflow = 'hidden';
529
+ this.zone.runOutsideAngular(() => {
530
+ requestAnimationFrame(() => {
531
+ this.zone.run(() => {
532
+ const paneEl = this.popoverOverlayRef?.overlayElement;
533
+ if (paneEl) {
534
+ this.focusTrap = this.focusTrapFactory.create(paneEl);
535
+ this.focusTrap.focusInitialElementWhenReady();
536
+ }
537
+ });
538
+ });
539
+ });
540
+ }
541
+ /**
542
+ * After a step change, moves focus back to the first tabbable element inside the
543
+ * popover so the user does not lose keyboard position.
544
+ * No-op when there is no active focus trap (simple spotlight mode).
545
+ */
546
+ scheduleFocusTrapRefocus() {
547
+ if (!this.focusTrap)
548
+ return;
549
+ this.zone.runOutsideAngular(() => {
550
+ requestAnimationFrame(() => {
551
+ this.zone.run(() => this.focusTrap?.focusFirstTabbableElement());
552
+ });
553
+ });
554
+ }
555
+ syncDirectionAndArrow() {
556
+ const ref = this.popoverComponentRef;
557
+ const paneEl = this.popoverOverlayRef?.overlayElement;
558
+ const rect = this.targetRect();
559
+ if (!ref || !paneEl || !rect) {
560
+ ref?.setInput('arrowOffsetPx', null);
561
+ return;
562
+ }
563
+ const pane = paneEl.getBoundingClientRect();
564
+ const tol = SPOTLIGHT_PADDING + 4;
565
+ let dir;
566
+ if (pane.top >= rect.bottom - tol) {
567
+ dir = 'bottom';
568
+ }
569
+ else if (pane.bottom <= rect.top + tol) {
570
+ dir = 'top';
571
+ }
572
+ else if (pane.left >= rect.right - tol) {
573
+ dir = 'right';
574
+ }
575
+ else if (pane.right <= rect.left + tol) {
576
+ dir = 'left';
577
+ }
578
+ else {
579
+ const dy = Math.abs(pane.top + pane.height / 2 - (rect.top + rect.height / 2));
580
+ const dx = Math.abs(pane.left + pane.width / 2 - (rect.left + rect.width / 2));
581
+ dir =
582
+ dy >= dx
583
+ ? pane.top + pane.height / 2 < rect.top + rect.height / 2
584
+ ? 'top'
585
+ : 'bottom'
586
+ : pane.left + pane.width / 2 < rect.left + rect.width / 2
587
+ ? 'left'
588
+ : 'right';
589
+ }
590
+ ref.setInput('position', `${dir}-center`);
591
+ const tgtCX = rect.left + rect.width / 2;
592
+ const tgtCY = rect.top + rect.height / 2;
593
+ const offset = dir === 'top' || dir === 'bottom'
594
+ ? tgtCX - pane.left - ARROW_HALF_SIZE
595
+ : tgtCY - pane.top - ARROW_HALF_SIZE;
596
+ ref.setInput('arrowOffsetPx', Math.max(ARROW_OFFSET_MIN, Math.min(ARROW_OFFSET_MAX, offset)));
597
+ }
598
+ destroyPopoverOverlay() {
599
+ this.popoverSubs.forEach((s) => s.unsubscribe());
600
+ this.popoverSubs = [];
601
+ this.popoverComponentRef = null;
602
+ if (this.focusTrap) {
603
+ this.focusTrap.destroy();
604
+ this.focusTrap = null;
605
+ }
606
+ document.body.style.overflow = '';
607
+ if (this.previouslyFocusedElement) {
608
+ this.previouslyFocusedElement.focus();
609
+ this.previouslyFocusedElement = null;
610
+ }
611
+ if (this.popoverOverlayRef) {
612
+ this.popoverOverlayRef.detach();
613
+ this.popoverOverlayRef.dispose();
614
+ this.popoverOverlayRef = null;
615
+ }
616
+ }
617
+ updateTargetRect(el) {
618
+ this.targetRect.set(el.getBoundingClientRect());
619
+ }
620
+ refreshRect() {
621
+ const step = this.currentStep();
622
+ if (!step || !this.isActive()) {
623
+ return;
624
+ }
625
+ const el = this.tourService.getElement(step.stepId);
626
+ if (el?.nativeElement) {
627
+ this.updateTargetRect(el.nativeElement);
628
+ }
629
+ }
630
+ observeElement(el) {
631
+ this.resizeObserver = new ResizeObserver(() => {
632
+ this.zone.run(() => this.updateTargetRect(el));
633
+ });
634
+ this.resizeObserver.observe(el);
635
+ }
636
+ disconnectResizeObserver() {
637
+ if (this.resizeObserver) {
638
+ this.resizeObserver.disconnect();
639
+ this.resizeObserver = null;
640
+ }
641
+ }
642
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpotlightOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
643
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SpotlightOverlayComponent, isStandalone: true, selector: "s-spotlight-overlay", ngImport: i0, template: "@if (isActive() && currentStep(); as step) {\n @if (step.showBackdrop !== false) {\n <div\n [ngStyle]=\"topMaskStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n\n @if (targetRect()) {\n <div\n [ngStyle]=\"leftMaskStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n <div\n [ngStyle]=\"rightMaskStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n <div\n [ngStyle]=\"bottomMaskStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n }\n }\n\n @if (step.showBackdrop === false) {\n @if (step.dismissible === false) {\n <div [ngStyle]=\"transparentBlockerStyle()\"></div>\n } @else {\n <div\n [ngStyle]=\"transparentBlockerStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n }\n }\n}\n\n", dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
644
+ }
645
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpotlightOverlayComponent, decorators: [{
646
+ type: Component,
647
+ args: [{ selector: 's-spotlight-overlay', standalone: true, imports: [NgStyle], template: "@if (isActive() && currentStep(); as step) {\n @if (step.showBackdrop !== false) {\n <div\n [ngStyle]=\"topMaskStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n\n @if (targetRect()) {\n <div\n [ngStyle]=\"leftMaskStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n <div\n [ngStyle]=\"rightMaskStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n <div\n [ngStyle]=\"bottomMaskStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n }\n }\n\n @if (step.showBackdrop === false) {\n @if (step.dismissible === false) {\n <div [ngStyle]=\"transparentBlockerStyle()\"></div>\n } @else {\n <div\n [ngStyle]=\"transparentBlockerStyle()\"\n (click)=\"onDismiss()\"\n ></div>\n }\n }\n}\n\n" }]
648
+ }], ctorParameters: () => [] });
649
+
650
+ /**
651
+ * Serviço central do Spotlight Tour.
652
+ *
653
+ * Gerencia o estado do tour (passos, índice atual, ativo/inativo), a navegação entre
654
+ * passos e o registro de elementos do DOM associados a cada `stepId`.
655
+ *
656
+ * Use `start()` para iniciar um tour com múltiplos passos ou `spotlight()` para
657
+ * exibir um destaque simples em um único elemento.
658
+ *
659
+ * @example
660
+ * ```typescript
661
+ * // Tour com múltiplos passos
662
+ * this.tourService.start([
663
+ * { stepId: 'passo-1', title: 'Título', message: 'Mensagem' },
664
+ * { stepId: 'passo-2', title: 'Título', message: 'Mensagem' },
665
+ * ]);
666
+ *
667
+ * // Destaque simples
668
+ * this.tourService.spotlight('meu-elemento', { title: 'Dica', message: 'Detalhes' });
669
+ * ```
670
+ */
671
+ class SpotlightTourService {
672
+ overlay = inject(Overlay);
673
+ environmentInjector = inject(EnvironmentInjector);
674
+ overlayRef = null;
675
+ _steps = signal([]);
676
+ _currentIndex = signal(0);
677
+ _isActive = signal(false);
678
+ _registrationVersion = signal(0);
679
+ _registeredElements = new Map();
680
+ _pendingDoNotShowAgain = false;
681
+ _stopped$ = new Subject();
682
+ _stepChanged$ = new Subject();
683
+ /** Signal somente-leitura que indica se o tour está ativo no momento. */
684
+ isActive = this._isActive.asReadonly();
685
+ /** Signal somente-leitura com o índice (0-based) do passo exibido atualmente. */
686
+ currentIndex = this._currentIndex.asReadonly();
687
+ /** Signal computado com o total de passos do tour em andamento. */
688
+ totalSteps = computed(() => this._steps().length);
689
+ /** Signal computado com os dados do passo atualmente exibido. */
690
+ currentStep = computed(() => this._steps()[this._currentIndex()]);
691
+ /**
692
+ * Observable emitido quando o tour é encerrado, seja por conclusão, interrupção
693
+ * ou descarte. Carrega um `SpotlightStopEvent` com o motivo e o estado final.
694
+ */
695
+ stopped$ = this._stopped$.asObservable();
696
+ /**
697
+ * Observable emitido sempre que o passo exibido muda, incluindo o início do tour.
698
+ * Carrega um `SpotlightStepChangedEvent` com o `stepId` e o índice do novo passo.
699
+ */
700
+ stepChanged$ = this._stepChanged$.asObservable();
701
+ /**
702
+ * Signal somente-leitura incrementado a cada chamada de `registerElement` ou
703
+ * `unregisterElement`. Permite que outros componentes reajam a mudanças no
704
+ * registro de elementos sem polling.
705
+ */
706
+ registrationVersion = this._registrationVersion.asReadonly();
707
+ /**
708
+ * Inicia um tour com a lista de passos fornecida.
709
+ *
710
+ * O tour começa pelo primeiro passo da lista. Emite `stepChanged$` para o passo inicial.
711
+ * Se `steps` for um array vazio, o método não faz nada.
712
+ * Se já houver um tour ativo, ele é encerrado (emitindo `stopped$` com `reason: 'interrupted'`)
713
+ * antes de o novo tour começar.
714
+ *
715
+ * @param steps Lista de passos a percorrer em ordem.
716
+ */
717
+ start(steps) {
718
+ if (steps.length === 0)
719
+ return;
720
+ if (this._isActive()) {
721
+ this.stop();
722
+ }
723
+ this._steps.set(steps);
724
+ this._currentIndex.set(0);
725
+ this._isActive.set(true);
726
+ this.attachOverlay();
727
+ this._stepChanged$.next({ stepId: steps[0].stepId, stepIndex: 0, totalSteps: steps.length });
728
+ }
729
+ /**
730
+ * Exibe um destaque simples (passo único) no elemento identificado por `stepId`.
731
+ *
732
+ * Equivalente a chamar `start()` com um array de um único passo.
733
+ * O checkbox "Não mostrar novamente" é exibido por padrão (`showDoNotShowAgain: true`);
734
+ * passe `showDoNotShowAgain: false` em `config` para suprimi-lo.
735
+ *
736
+ * @param stepId ID do elemento registrado via `sSpotlightStep` ou `registerElement`.
737
+ * @param config Configuração visual e comportamental do destaque.
738
+ */
739
+ spotlight(stepId, config) {
740
+ const step = { stepId, ...config };
741
+ this.start([step]);
742
+ }
743
+ /**
744
+ * Avança para o próximo passo do tour.
745
+ *
746
+ * Se o passo atual definir `beforeNext`, aguarda sua resolução antes de avançar.
747
+ * No último passo, encerra o tour com `reason: 'completed'`.
748
+ *
749
+ * @param doNotShowAgain Quando `true`, o evento `stopped$` será emitido com
750
+ * `doNotShowAgain: true` ao concluir o tour. Relevante apenas no último passo.
751
+ */
752
+ next(doNotShowAgain = false) {
753
+ const current = this.currentStep();
754
+ this._pendingDoNotShowAgain = doNotShowAgain;
755
+ if (current?.beforeNext) {
756
+ const result = current.beforeNext();
757
+ if (result instanceof Promise) {
758
+ result.then(() => this.advance()).catch(() => { });
759
+ }
760
+ else {
761
+ this.advance();
762
+ }
763
+ }
764
+ else {
765
+ this.advance();
766
+ }
767
+ }
768
+ /**
769
+ * Retorna ao passo anterior do tour.
770
+ *
771
+ * Se o passo atual definir `beforePrevious`, aguarda sua resolução antes de voltar.
772
+ * Não faz nada se o tour estiver no primeiro passo.
773
+ */
774
+ previous() {
775
+ if (this._currentIndex() <= 0)
776
+ return;
777
+ const current = this.currentStep();
778
+ if (current?.beforePrevious) {
779
+ const result = current.beforePrevious();
780
+ if (result instanceof Promise) {
781
+ result.then(() => this.retreat()).catch(() => { });
782
+ }
783
+ else {
784
+ this.retreat();
785
+ }
786
+ }
787
+ else {
788
+ this.retreat();
789
+ }
790
+ }
791
+ /**
792
+ * Encerra o tour imediatamente e emite o evento `stopped$`.
793
+ *
794
+ * @param event Dados parciais do evento de parada. Valores não fornecidos são
795
+ * preenchidos com os defaults: `reason: 'interrupted'`, `doNotShowAgain: false`
796
+ * e o `stepId`/`stepIndex`/`totalSteps` do momento atual.
797
+ */
798
+ stop(event) {
799
+ const steps = this._steps();
800
+ const index = this._currentIndex();
801
+ const fullEvent = {
802
+ reason: 'interrupted',
803
+ doNotShowAgain: false,
804
+ stepId: steps[index]?.stepId ?? '',
805
+ stepIndex: index,
806
+ totalSteps: steps.length,
807
+ ...event,
808
+ };
809
+ this._isActive.set(false);
810
+ this._steps.set([]);
811
+ this._currentIndex.set(0);
812
+ this._pendingDoNotShowAgain = false;
813
+ this._stopped$.next(fullEvent);
814
+ this.detachOverlay();
815
+ }
816
+ /**
817
+ * Registra um elemento do DOM como alvo do passo identificado por `stepId`.
818
+ *
819
+ * Normalmente chamado pela diretiva `sSpotlightStep`. Use diretamente apenas
820
+ * quando precisar registrar elementos criados programaticamente.
821
+ *
822
+ * @param stepId ID único do passo ao qual o elemento pertence.
823
+ * @param el Referência ao elemento do DOM.
824
+ */
825
+ registerElement(stepId, el) {
826
+ this._registeredElements.set(stepId, el);
827
+ this._registrationVersion.update((v) => v + 1);
828
+ }
829
+ /**
830
+ * Remove o registro do elemento associado a `stepId`.
831
+ *
832
+ * Normalmente chamado pela diretiva `sSpotlightStep` no `ngOnDestroy`.
833
+ *
834
+ * @param stepId ID do passo cujo elemento deve ser removido.
835
+ */
836
+ unregisterElement(stepId) {
837
+ this._registeredElements.delete(stepId);
838
+ this._registrationVersion.update((v) => v + 1);
839
+ }
840
+ /**
841
+ * Retorna o `ElementRef` registrado para o `stepId` informado,
842
+ * ou `undefined` se nenhum elemento estiver registrado.
843
+ *
844
+ * @param stepId ID do passo a buscar.
845
+ */
846
+ getElement(stepId) {
847
+ return this._registeredElements.get(stepId);
848
+ }
849
+ advance() {
850
+ const steps = this._steps();
851
+ const currentIndex = this._currentIndex();
852
+ if (currentIndex < steps.length - 1) {
853
+ const newIndex = currentIndex + 1;
854
+ this._pendingDoNotShowAgain = false;
855
+ this._currentIndex.set(newIndex);
856
+ this._stepChanged$.next({ stepId: steps[newIndex].stepId, stepIndex: newIndex, totalSteps: steps.length });
857
+ }
858
+ else {
859
+ this.stop({ reason: 'completed', doNotShowAgain: this._pendingDoNotShowAgain });
860
+ }
861
+ }
862
+ retreat() {
863
+ const steps = this._steps();
864
+ const newIndex = this._currentIndex() - 1;
865
+ this._currentIndex.set(newIndex);
866
+ this._stepChanged$.next({ stepId: steps[newIndex].stepId, stepIndex: newIndex, totalSteps: steps.length });
867
+ }
868
+ attachOverlay() {
869
+ if (this.overlayRef) {
870
+ return;
871
+ }
872
+ this.overlayRef = this.overlay.create({
873
+ hasBackdrop: false,
874
+ scrollStrategy: this.overlay.scrollStrategies.noop(),
875
+ positionStrategy: this.overlay.position().global(),
876
+ });
877
+ const portal = new ComponentPortal(SpotlightOverlayComponent, null, this.environmentInjector);
878
+ this.overlayRef.attach(portal);
879
+ }
880
+ detachOverlay() {
881
+ if (this.overlayRef) {
882
+ this.overlayRef.detach();
883
+ this.overlayRef.dispose();
884
+ this.overlayRef = null;
885
+ }
886
+ }
887
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpotlightTourService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
888
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpotlightTourService, providedIn: 'root' });
889
+ }
890
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpotlightTourService, decorators: [{
891
+ type: Injectable,
892
+ args: [{ providedIn: 'root' }]
893
+ }] });
894
+
895
+ /**
896
+ * Diretiva que registra um elemento do DOM como alvo de um passo do Spotlight Tour.
897
+ *
898
+ * Aplique a diretiva em qualquer elemento e forneça um ID único que corresponda
899
+ * ao `stepId` configurado no `SpotlightTourService`. O elemento será automaticamente
900
+ * registrado ao ser criado e atualizado caso o ID mude dinamicamente.
901
+ *
902
+ * @example
903
+ * ```html
904
+ * <button [sSpotlightStep]="'meu-botao'">Clique aqui</button>
905
+ * ```
906
+ */
907
+ class SpotlightStepDirective {
908
+ /**
909
+ * ID único do passo do tour ao qual este elemento está associado.
910
+ * Deve corresponder ao `stepId` definido em `SpotlightStep`.
911
+ */
912
+ sSpotlightStep = input.required();
913
+ el = inject(ElementRef);
914
+ tourService = inject(SpotlightTourService);
915
+ currentId = null;
916
+ constructor() {
917
+ effect(() => {
918
+ const newId = this.sSpotlightStep();
919
+ if (this.currentId !== null && this.currentId !== newId) {
920
+ this.tourService.unregisterElement(this.currentId);
921
+ }
922
+ this.currentId = newId;
923
+ this.tourService.registerElement(newId, this.el);
924
+ }, { allowSignalWrites: true });
925
+ }
926
+ ngOnDestroy() {
927
+ if (this.currentId !== null) {
928
+ this.tourService.unregisterElement(this.currentId);
929
+ }
930
+ }
931
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpotlightStepDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
932
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.14", type: SpotlightStepDirective, isStandalone: true, selector: "[sSpotlightStep]", inputs: { sSpotlightStep: { classPropertyName: "sSpotlightStep", publicName: "sSpotlightStep", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
933
+ }
934
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpotlightStepDirective, decorators: [{
935
+ type: Directive,
936
+ args: [{
937
+ selector: '[sSpotlightStep]',
938
+ standalone: true,
939
+ }]
940
+ }], ctorParameters: () => [] });
941
+
942
+ /**
943
+ * Generated bundle index. Do not edit.
944
+ */
945
+
946
+ export { SpotlightComponent, SpotlightStepDirective, SpotlightTourService };
947
+ //# sourceMappingURL=seniorsistemas-angular-components-spotlight.mjs.map