@ramonbsales/noah-angular 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2188 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, Component, EventEmitter, forwardRef, Output, Input, inject, HostListener, InjectionToken, Optional, Inject } from '@angular/core';
3
+ import * as i1 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import * as i3 from '@angular/forms';
6
+ import { ReactiveFormsModule, NG_VALUE_ACCESSOR, NgControl, FormBuilder, Validators } from '@angular/forms';
7
+ import * as i1$1 from '@angular/router';
8
+ import { RouterModule, Router } from '@angular/router';
9
+ import { Subject, BehaviorSubject, Observable, from, throwError } from 'rxjs';
10
+ import { map, takeUntil, switchMap, catchError } from 'rxjs/operators';
11
+
12
+ class SharedComponentsService {
13
+ constructor() { }
14
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsService, providedIn: 'root' });
16
+ }
17
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsService, decorators: [{
18
+ type: Injectable,
19
+ args: [{
20
+ providedIn: 'root'
21
+ }]
22
+ }], ctorParameters: () => [] });
23
+
24
+ class SharedComponentsComponent {
25
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
26
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: SharedComponentsComponent, isStandalone: true, selector: "lib-shared-components", ngImport: i0, template: `
27
+ <p>
28
+ shared-components works!
29
+ </p>
30
+ `, isInline: true, styles: [""] });
31
+ }
32
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SharedComponentsComponent, decorators: [{
33
+ type: Component,
34
+ args: [{ selector: 'lib-shared-components', imports: [], template: `
35
+ <p>
36
+ shared-components works!
37
+ </p>
38
+ ` }]
39
+ }] });
40
+
41
+ class DropdownComponent {
42
+ label = 'Nome do campo';
43
+ placeholder = 'Selecione uma opção';
44
+ options = [];
45
+ disabled = false;
46
+ required = false;
47
+ errorMessage = 'Campo obrigatório';
48
+ selectedOption = null;
49
+ hideLabel = false;
50
+ valueChange = new EventEmitter();
51
+ internalValue = null;
52
+ isOpen = false;
53
+ // ControlValueAccessor implementation
54
+ onChange = (value) => { };
55
+ onTouched = () => { };
56
+ constructor() { }
57
+ ngOnInit() {
58
+ // Se selectedOption foi passado como Input, use-o como valor inicial
59
+ if (this.selectedOption !== null && this.selectedOption !== undefined) {
60
+ this.internalValue = this.selectedOption;
61
+ }
62
+ }
63
+ ngOnChanges(changes) {
64
+ // Detecta mudanças no selectedOption Input
65
+ if (changes['selectedOption'] && !changes['selectedOption'].firstChange) {
66
+ const newValue = changes['selectedOption'].currentValue;
67
+ if (newValue !== this.internalValue) {
68
+ this.internalValue = newValue;
69
+ }
70
+ }
71
+ }
72
+ selectOption(option) {
73
+ this.internalValue = option.value;
74
+ this.isOpen = false;
75
+ this.onChange(option.value);
76
+ this.onTouched();
77
+ this.valueChange.emit(option.value);
78
+ }
79
+ toggleDropdown() {
80
+ if (!this.disabled) {
81
+ this.isOpen = !this.isOpen;
82
+ if (this.isOpen) {
83
+ this.onTouched();
84
+ }
85
+ }
86
+ }
87
+ getSelectedLabel() {
88
+ if (this.internalValue !== null && this.internalValue !== undefined) {
89
+ const option = this.options.find(opt => opt.value === this.internalValue);
90
+ return option ? option.label : '';
91
+ }
92
+ return this.placeholder;
93
+ }
94
+ hasValue() {
95
+ return this.internalValue !== null && this.internalValue !== undefined;
96
+ }
97
+ // ControlValueAccessor methods
98
+ writeValue(value) {
99
+ this.internalValue = value;
100
+ this.valueChange.emit(value);
101
+ }
102
+ registerOnChange(fn) {
103
+ this.onChange = fn;
104
+ }
105
+ registerOnTouched(fn) {
106
+ this.onTouched = fn;
107
+ }
108
+ setDisabledState(isDisabled) {
109
+ this.disabled = isDisabled;
110
+ }
111
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
112
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: DropdownComponent, isStandalone: true, selector: "lib-dropdown", inputs: { label: "label", placeholder: "placeholder", options: "options", disabled: "disabled", required: "required", errorMessage: "errorMessage", selectedOption: "selectedOption", hideLabel: "hideLabel" }, outputs: { valueChange: "valueChange" }, providers: [
113
+ {
114
+ provide: NG_VALUE_ACCESSOR,
115
+ useExisting: forwardRef(() => DropdownComponent),
116
+ multi: true
117
+ }
118
+ ], usesOnChanges: true, ngImport: i0, template: "<label *ngIf=\"!hideLabel\" for=\"\">{{label}}</label>\n<div class=\"dropdown\" tabindex=\"0\" (blur)=\"isOpen = false\">\n <button class=\"btn btn-light w-100 text-start d-flex justify-content-between align-items-center\" type=\"button\"\n (click)=\"toggleDropdown()\" [attr.aria-expanded]=\"isOpen\" [disabled]=\"disabled\">\n <span class=\"select-option-text\" *ngIf=\"hasValue()\">{{ getSelectedLabel() }}</span>\n <span class=\"placeholder-text\" *ngIf=\"!hasValue()\">{{ getSelectedLabel() }}</span>\n <span class=\"material-icons\">\n unfold_more\n </span>\n </button>\n <ul class=\"dropdown-menu w-100 show\" *ngIf=\"isOpen\">\n <li *ngFor=\"let option of options\">\n <a class=\"dropdown-item\" (click)=\"selectOption(option)\">{{ option.label }}</a>\n </li>\n </ul>\n</div>", styles: ["label{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem;line-height:2.5}.dropdown{font-family:DM Sans,sans-serif}.btn{font-family:DM Sans,sans-serif;font-weight:500;font-size:.875rem;border:1px solid #dee2e6;background-color:#fff}.btn .select-option-text{color:#343a40}.btn .placeholder-text{color:#6c757d}.btn .material-icons{font-size:.875rem}.dropdown-item{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }] });
119
+ }
120
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DropdownComponent, decorators: [{
121
+ type: Component,
122
+ args: [{ selector: 'lib-dropdown', imports: [CommonModule, ReactiveFormsModule], providers: [
123
+ {
124
+ provide: NG_VALUE_ACCESSOR,
125
+ useExisting: forwardRef(() => DropdownComponent),
126
+ multi: true
127
+ }
128
+ ], template: "<label *ngIf=\"!hideLabel\" for=\"\">{{label}}</label>\n<div class=\"dropdown\" tabindex=\"0\" (blur)=\"isOpen = false\">\n <button class=\"btn btn-light w-100 text-start d-flex justify-content-between align-items-center\" type=\"button\"\n (click)=\"toggleDropdown()\" [attr.aria-expanded]=\"isOpen\" [disabled]=\"disabled\">\n <span class=\"select-option-text\" *ngIf=\"hasValue()\">{{ getSelectedLabel() }}</span>\n <span class=\"placeholder-text\" *ngIf=\"!hasValue()\">{{ getSelectedLabel() }}</span>\n <span class=\"material-icons\">\n unfold_more\n </span>\n </button>\n <ul class=\"dropdown-menu w-100 show\" *ngIf=\"isOpen\">\n <li *ngFor=\"let option of options\">\n <a class=\"dropdown-item\" (click)=\"selectOption(option)\">{{ option.label }}</a>\n </li>\n </ul>\n</div>", styles: ["label{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem;line-height:2.5}.dropdown{font-family:DM Sans,sans-serif}.btn{font-family:DM Sans,sans-serif;font-weight:500;font-size:.875rem;border:1px solid #dee2e6;background-color:#fff}.btn .select-option-text{color:#343a40}.btn .placeholder-text{color:#6c757d}.btn .material-icons{font-size:.875rem}.dropdown-item{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;cursor:pointer}\n"] }]
129
+ }], ctorParameters: () => [], propDecorators: { label: [{
130
+ type: Input
131
+ }], placeholder: [{
132
+ type: Input
133
+ }], options: [{
134
+ type: Input
135
+ }], disabled: [{
136
+ type: Input
137
+ }], required: [{
138
+ type: Input
139
+ }], errorMessage: [{
140
+ type: Input
141
+ }], selectedOption: [{
142
+ type: Input
143
+ }], hideLabel: [{
144
+ type: Input
145
+ }], valueChange: [{
146
+ type: Output
147
+ }] } });
148
+
149
+ class InputComponent {
150
+ injector;
151
+ label = 'Nome do campo';
152
+ placeholder = 'Digite aqui...';
153
+ type = 'text';
154
+ customErrorMessages = {};
155
+ // Detects if the field is required based on FormControl validator
156
+ get isRequired() {
157
+ if (this.control?.control?.validator) {
158
+ const validator = this.control.control.validator({});
159
+ return !!(validator && validator['required']);
160
+ }
161
+ return false;
162
+ }
163
+ // Detects if the field is disabled
164
+ get isDisabled() {
165
+ return (this.control?.control?.disabled ?? false);
166
+ }
167
+ value = '';
168
+ isFocused = false;
169
+ control;
170
+ // ControlValueAccessor implementation
171
+ onChange = (value) => { };
172
+ onTouched = () => { };
173
+ // Mapeamento padrão de mensagens de erro
174
+ defaultErrorMessages = {
175
+ required: 'Campo obrigatório',
176
+ email: 'Email inválido',
177
+ minlength: 'Muito curto',
178
+ maxlength: 'Muito longo',
179
+ min: 'Valor muito baixo',
180
+ max: 'Valor muito alto',
181
+ pattern: 'Formato inválido'
182
+ };
183
+ constructor(injector) {
184
+ this.injector = injector;
185
+ }
186
+ ngOnInit() {
187
+ // Injeta o NgControl para acessar os erros do FormControl
188
+ this.control = this.injector.get(NgControl, null) || undefined;
189
+ }
190
+ onFocus() {
191
+ this.isFocused = true;
192
+ this.onTouched();
193
+ }
194
+ onBlur() {
195
+ this.isFocused = false;
196
+ }
197
+ onInput(event) {
198
+ const target = event.target;
199
+ let newValue = target.value;
200
+ // Conversão de tipos
201
+ if (this.type === 'number') {
202
+ newValue = newValue === '' ? null : Number(newValue);
203
+ }
204
+ this.value = newValue;
205
+ this.onChange(newValue);
206
+ }
207
+ // Verifica se deve mostrar erro
208
+ get showError() {
209
+ return !!(this.control?.invalid && (this.control?.dirty || this.control?.touched));
210
+ }
211
+ get showRequiredAsterisk() {
212
+ return !!this.isRequired && !!this.control?.invalid;
213
+ }
214
+ // Obtém a mensagem de erro do FormControl
215
+ get errorMessage() {
216
+ if (!this.control?.errors) {
217
+ return '';
218
+ }
219
+ const errors = this.control.errors;
220
+ // Primeiro verifica se há mensagem customizada
221
+ for (const errorKey in errors) {
222
+ if (this.customErrorMessages[errorKey]) {
223
+ const errorValue = errors[errorKey];
224
+ return this.interpolateErrorMessage(this.customErrorMessages[errorKey], errorValue);
225
+ }
226
+ }
227
+ // Depois verifica mensagens padrão
228
+ for (const errorKey in errors) {
229
+ if (this.defaultErrorMessages[errorKey]) {
230
+ const errorValue = errors[errorKey];
231
+ return this.interpolateErrorMessage(this.defaultErrorMessages[errorKey], errorValue);
232
+ }
233
+ }
234
+ // Retorna erro genérico se não encontrar mensagem específica
235
+ return 'Campo inválido';
236
+ }
237
+ // Interpola valores dinâmicos na mensagem de erro
238
+ interpolateErrorMessage(message, errorValue) {
239
+ if (typeof errorValue === 'object') {
240
+ // Para erros como minlength, maxlength, min, max
241
+ if (errorValue.requiredLength) {
242
+ message = message.replace('{{requiredLength}}', errorValue.requiredLength);
243
+ message = message.replace('{{actualLength}}', errorValue.actualLength);
244
+ }
245
+ if (errorValue.min !== undefined) {
246
+ message = message.replace('{{min}}', errorValue.min);
247
+ message = message.replace('{{actual}}', errorValue.actual);
248
+ }
249
+ if (errorValue.max !== undefined) {
250
+ message = message.replace('{{max}}', errorValue.max);
251
+ message = message.replace('{{actual}}', errorValue.actual);
252
+ }
253
+ }
254
+ return message;
255
+ }
256
+ validateField() {
257
+ // Removido - agora usa a validação do FormControl
258
+ }
259
+ hasValue() {
260
+ return this.value !== null && this.value !== undefined && this.value !== '';
261
+ }
262
+ // ControlValueAccessor methods
263
+ writeValue(value) {
264
+ this.value = value;
265
+ }
266
+ registerOnChange(fn) {
267
+ this.onChange = fn;
268
+ }
269
+ registerOnTouched(fn) {
270
+ this.onTouched = fn;
271
+ }
272
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: InputComponent, deps: [{ token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component });
273
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: InputComponent, isStandalone: true, selector: "lib-input", inputs: { label: "label", placeholder: "placeholder", type: "type", customErrorMessages: "customErrorMessages" }, providers: [
274
+ {
275
+ provide: NG_VALUE_ACCESSOR,
276
+ useExisting: forwardRef(() => InputComponent),
277
+ multi: true
278
+ }
279
+ ], ngImport: i0, template: "<div class=\"form-group\">\n\n <label>{{ label }}\n <span class=\"required-asterisk\" *ngIf=\"showRequiredAsterisk\">*</span>\n </label>\n\n <div class=\"input-container\" [class.focused]=\"isFocused\" [class.has-value]=\"hasValue()\" [class.disabled]=\"isDisabled\"\n [class.error]=\"showError\">\n\n <input [type]=\"type\" [placeholder]=\"placeholder\" [value]=\"value || ''\" [disabled]=\"isDisabled\"\n [required]=\"isRequired\" (input)=\"onInput($event)\" (focus)=\"onFocus()\" (blur)=\"onBlur()\"\n [ngClass]=\"{'error': showError}\" />\n </div>\n\n <small class=\"form-text error\" *ngIf=\"showError\">\n {{ errorMessage }}\n </small>\n\n</div>", styles: ["label{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem;line-height:2.5}.required-asterisk{color:#dc3545;margin-left:.25rem}.input-container{font-family:DM Sans,sans-serif}input{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;border:1px solid #dee2e6;border-radius:.25rem;display:block;width:100%;padding:.375rem .75rem;line-height:1.5;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-clip:padding-box;outline:none}input::placeholder{color:#6c757d}input:focus{border-color:#b2b9be;box-shadow:none}input:focus.error{border-color:#dc3545}small.error{color:#dc3545}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }] });
280
+ }
281
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: InputComponent, decorators: [{
282
+ type: Component,
283
+ args: [{ selector: 'lib-input', imports: [CommonModule, ReactiveFormsModule], providers: [
284
+ {
285
+ provide: NG_VALUE_ACCESSOR,
286
+ useExisting: forwardRef(() => InputComponent),
287
+ multi: true
288
+ }
289
+ ], template: "<div class=\"form-group\">\n\n <label>{{ label }}\n <span class=\"required-asterisk\" *ngIf=\"showRequiredAsterisk\">*</span>\n </label>\n\n <div class=\"input-container\" [class.focused]=\"isFocused\" [class.has-value]=\"hasValue()\" [class.disabled]=\"isDisabled\"\n [class.error]=\"showError\">\n\n <input [type]=\"type\" [placeholder]=\"placeholder\" [value]=\"value || ''\" [disabled]=\"isDisabled\"\n [required]=\"isRequired\" (input)=\"onInput($event)\" (focus)=\"onFocus()\" (blur)=\"onBlur()\"\n [ngClass]=\"{'error': showError}\" />\n </div>\n\n <small class=\"form-text error\" *ngIf=\"showError\">\n {{ errorMessage }}\n </small>\n\n</div>", styles: ["label{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem;line-height:2.5}.required-asterisk{color:#dc3545;margin-left:.25rem}.input-container{font-family:DM Sans,sans-serif}input{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;border:1px solid #dee2e6;border-radius:.25rem;display:block;width:100%;padding:.375rem .75rem;line-height:1.5;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-clip:padding-box;outline:none}input::placeholder{color:#6c757d}input:focus{border-color:#b2b9be;box-shadow:none}input:focus.error{border-color:#dc3545}small.error{color:#dc3545}\n"] }]
290
+ }], ctorParameters: () => [{ type: i0.Injector }], propDecorators: { label: [{
291
+ type: Input
292
+ }], placeholder: [{
293
+ type: Input
294
+ }], type: [{
295
+ type: Input
296
+ }], customErrorMessages: [{
297
+ type: Input
298
+ }] } });
299
+
300
+ class ButtonComponent {
301
+ title = 'Salvar informação';
302
+ backgroundColor = '#007bff';
303
+ color = '#fff';
304
+ borderColor = '#007bff';
305
+ minWidth = '8rem';
306
+ disabled = false;
307
+ loading = false;
308
+ loadingText = 'Carregando...';
309
+ onClickEvent = new EventEmitter();
310
+ onClick() {
311
+ if (!this.loading && !this.disabled) {
312
+ this.onClickEvent.emit();
313
+ }
314
+ }
315
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
316
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: ButtonComponent, isStandalone: true, selector: "lib-button", inputs: { title: "title", backgroundColor: "backgroundColor", color: "color", borderColor: "borderColor", minWidth: "minWidth", disabled: "disabled", loading: "loading", loadingText: "loadingText" }, outputs: { onClickEvent: "onClickEvent" }, ngImport: i0, template: "<button type=\"button\" class=\"btn\" [style.background-color]=\"backgroundColor\" [style.color]=\"color\"\n [style.border-color]=\"borderColor\" [style.min-width]=\"minWidth\" [disabled]=\"disabled || loading\"\n (click)=\"onClick()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-2\" role=\"status\" aria-hidden=\"true\"></span>\n <span *ngIf=\"!loading\">{{title}}</span>\n <span *ngIf=\"loading\">{{loadingText}}</span>\n</button>", styles: ["span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
317
+ }
318
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ButtonComponent, decorators: [{
319
+ type: Component,
320
+ args: [{ selector: 'lib-button', imports: [CommonModule], template: "<button type=\"button\" class=\"btn\" [style.background-color]=\"backgroundColor\" [style.color]=\"color\"\n [style.border-color]=\"borderColor\" [style.min-width]=\"minWidth\" [disabled]=\"disabled || loading\"\n (click)=\"onClick()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-2\" role=\"status\" aria-hidden=\"true\"></span>\n <span *ngIf=\"!loading\">{{title}}</span>\n <span *ngIf=\"loading\">{{loadingText}}</span>\n</button>", styles: ["span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"] }]
321
+ }], propDecorators: { title: [{
322
+ type: Input
323
+ }], backgroundColor: [{
324
+ type: Input
325
+ }], color: [{
326
+ type: Input
327
+ }], borderColor: [{
328
+ type: Input
329
+ }], minWidth: [{
330
+ type: Input
331
+ }], disabled: [{
332
+ type: Input
333
+ }], loading: [{
334
+ type: Input
335
+ }], loadingText: [{
336
+ type: Input
337
+ }], onClickEvent: [{
338
+ type: Output
339
+ }] } });
340
+
341
+ class CheckboxComponent {
342
+ /** Array de opções: [{ label: string, value: any }] */
343
+ options = [];
344
+ /** Se true, permite múltipla seleção */
345
+ multiple = false;
346
+ /** FormControl do Reactive Forms */
347
+ control;
348
+ /** Emite valor selecionado ao pai (opcional) */
349
+ selectedChange = new EventEmitter();
350
+ get disabled() {
351
+ return !!this.control?.disabled;
352
+ }
353
+ get selected() {
354
+ return this.control?.value;
355
+ }
356
+ isChecked(value) {
357
+ if (this.multiple) {
358
+ return Array.isArray(this.selected) && this.selected.includes(value);
359
+ }
360
+ return this.selected === value;
361
+ }
362
+ onOptionChange(event, value) {
363
+ if (this.disabled)
364
+ return;
365
+ if (this.multiple) {
366
+ const checked = event.target.checked;
367
+ let newSelected = Array.isArray(this.selected) ? [...this.selected] : [];
368
+ if (checked) {
369
+ if (!newSelected.includes(value))
370
+ newSelected.push(value);
371
+ }
372
+ else {
373
+ newSelected = newSelected.filter(v => v !== value);
374
+ }
375
+ this.control?.setValue(newSelected);
376
+ this.control?.markAsDirty();
377
+ this.selectedChange.emit(newSelected);
378
+ }
379
+ else {
380
+ this.control?.setValue(value);
381
+ this.control?.markAsDirty();
382
+ this.selectedChange.emit(value);
383
+ }
384
+ }
385
+ get showError() {
386
+ return !!this.control && this.control.invalid && (this.control.dirty || this.control.touched);
387
+ }
388
+ get errorMessage() {
389
+ if (!this.control || !this.control.errors)
390
+ return null;
391
+ if (this.control.errors['required'])
392
+ return 'Seleção obrigatória.';
393
+ // Adicione outras mensagens customizadas conforme necessário
394
+ return 'Valor inválido.';
395
+ }
396
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: CheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
397
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: CheckboxComponent, isStandalone: true, selector: "lib-checkbox", inputs: { options: "options", multiple: "multiple", control: "control" }, outputs: { selectedChange: "selectedChange" }, ngImport: i0, template: "<div *ngFor=\"let option of options; let i = index\"\n style=\"display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;\">\n\n <input class=\"form-check-input\" [type]=\"multiple ? 'checkbox' : 'radio'\"\n [name]=\"multiple ? 'checkbox-group' : 'radio-group'\" [value]=\"option.value\" [checked]=\"isChecked(option.value)\"\n [disabled]=\"disabled\" (change)=\"onOptionChange($event, option.value)\" />\n <span>{{ option.label }}</span>\n</div>\n<div *ngIf=\"showError\" class=\"text-danger small mt-1\">\n {{ errorMessage }}\n</div>", styles: ["input{cursor:pointer;margin-top:0}.form-check-input:checked{background-color:#414141;border-color:#414141}.form-check-input:focus{border-color:#b2b9be;outline:0;box-shadow:0 0 0 .25rem #04112440}span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }] });
398
+ }
399
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: CheckboxComponent, decorators: [{
400
+ type: Component,
401
+ args: [{ selector: 'lib-checkbox', imports: [CommonModule, ReactiveFormsModule], template: "<div *ngFor=\"let option of options; let i = index\"\n style=\"display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;\">\n\n <input class=\"form-check-input\" [type]=\"multiple ? 'checkbox' : 'radio'\"\n [name]=\"multiple ? 'checkbox-group' : 'radio-group'\" [value]=\"option.value\" [checked]=\"isChecked(option.value)\"\n [disabled]=\"disabled\" (change)=\"onOptionChange($event, option.value)\" />\n <span>{{ option.label }}</span>\n</div>\n<div *ngIf=\"showError\" class=\"text-danger small mt-1\">\n {{ errorMessage }}\n</div>", styles: ["input{cursor:pointer;margin-top:0}.form-check-input:checked{background-color:#414141;border-color:#414141}.form-check-input:focus{border-color:#b2b9be;outline:0;box-shadow:0 0 0 .25rem #04112440}span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"] }]
402
+ }], propDecorators: { options: [{
403
+ type: Input
404
+ }], multiple: [{
405
+ type: Input
406
+ }], control: [{
407
+ type: Input
408
+ }], selectedChange: [{
409
+ type: Output
410
+ }] } });
411
+
412
+ class ToggleComponent {
413
+ title = '';
414
+ enableTitle = false;
415
+ value = false;
416
+ onChange = (value) => { };
417
+ onTouched = () => { };
418
+ writeValue(value) {
419
+ this.value = value;
420
+ }
421
+ registerOnChange(fn) {
422
+ this.onChange = fn;
423
+ }
424
+ registerOnTouched(fn) {
425
+ this.onTouched = fn;
426
+ }
427
+ toggle() {
428
+ this.value = !this.value;
429
+ this.onChange(this.value);
430
+ this.onTouched();
431
+ }
432
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
433
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: ToggleComponent, isStandalone: true, selector: "lib-toggle", inputs: { title: "title", enableTitle: "enableTitle" }, providers: [
434
+ {
435
+ provide: NG_VALUE_ACCESSOR,
436
+ useExisting: forwardRef(() => ToggleComponent),
437
+ multi: true
438
+ }
439
+ ], ngImport: i0, template: "<div class=\"form-check form-switch centered-toggle\">\n <div>\n <input class=\"form-check-input\" type=\"checkbox\" [checked]=\"value\" (change)=\"toggle()\" />\n </div>\n <span *ngIf=\"enableTitle\">{{ title }}</span>\n</div>", styles: [".centered-toggle{display:flex;align-items:anchor-center}input{cursor:pointer;margin-top:0}.form-check-input:checked{background-color:#414141;border-color:#414141}.form-check-input:focus{border-color:#b2b9be;outline:0;box-shadow:0 0 0 .25rem #04112440}span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
440
+ }
441
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: ToggleComponent, decorators: [{
442
+ type: Component,
443
+ args: [{ selector: 'lib-toggle', imports: [CommonModule], providers: [
444
+ {
445
+ provide: NG_VALUE_ACCESSOR,
446
+ useExisting: forwardRef(() => ToggleComponent),
447
+ multi: true
448
+ }
449
+ ], template: "<div class=\"form-check form-switch centered-toggle\">\n <div>\n <input class=\"form-check-input\" type=\"checkbox\" [checked]=\"value\" (change)=\"toggle()\" />\n </div>\n <span *ngIf=\"enableTitle\">{{ title }}</span>\n</div>", styles: [".centered-toggle{display:flex;align-items:anchor-center}input{cursor:pointer;margin-top:0}.form-check-input:checked{background-color:#414141;border-color:#414141}.form-check-input:focus{border-color:#b2b9be;outline:0;box-shadow:0 0 0 .25rem #04112440}span{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}\n"] }]
450
+ }], propDecorators: { title: [{
451
+ type: Input
452
+ }], enableTitle: [{
453
+ type: Input
454
+ }] } });
455
+
456
+ class BreadcrumbComponent {
457
+ items = [];
458
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: BreadcrumbComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
459
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: BreadcrumbComponent, isStandalone: true, selector: "lib-breadcrumb", inputs: { items: "items" }, ngImport: i0, template: "<nav class=\"breadcrumb\">\n <ol class=\"breadcrumb-list\">\n <li class=\"breadcrumb-item\" *ngFor=\"let item of items; let last = last\">\n <a *ngIf=\"!last\" [routerLink]=\"item.url\">{{ item.label }}</a>\n <span *ngIf=\"last\">{{ item.label }}</span>\n </li>\n </ol>\n</nav>", styles: [".breadcrumb{font-family:DM Sans,sans-serif;background:none;padding:.5rem 0;margin-bottom:1rem}.breadcrumb-list{display:flex;flex-wrap:wrap;list-style:none;padding:0;margin:0}.breadcrumb-item{display:flex;align-items:center;font-size:.875rem;color:#495057;font-weight:500}.breadcrumb-item a{color:#414141;text-decoration:none;padding:.25rem .5rem;border-radius:4px;transition:background .2s,color .2s}.breadcrumb-item a:hover{background:#dee2e6;color:#343a40;text-decoration:underline}.breadcrumb-separator{margin:0 .25rem;color:#b2b9be;font-size:1rem}.breadcrumb-item:last-child{color:#6c757d;font-weight:400}\n"], dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
460
+ }
461
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: BreadcrumbComponent, decorators: [{
462
+ type: Component,
463
+ args: [{ selector: 'lib-breadcrumb', imports: [RouterModule, CommonModule], template: "<nav class=\"breadcrumb\">\n <ol class=\"breadcrumb-list\">\n <li class=\"breadcrumb-item\" *ngFor=\"let item of items; let last = last\">\n <a *ngIf=\"!last\" [routerLink]=\"item.url\">{{ item.label }}</a>\n <span *ngIf=\"last\">{{ item.label }}</span>\n </li>\n </ol>\n</nav>", styles: [".breadcrumb{font-family:DM Sans,sans-serif;background:none;padding:.5rem 0;margin-bottom:1rem}.breadcrumb-list{display:flex;flex-wrap:wrap;list-style:none;padding:0;margin:0}.breadcrumb-item{display:flex;align-items:center;font-size:.875rem;color:#495057;font-weight:500}.breadcrumb-item a{color:#414141;text-decoration:none;padding:.25rem .5rem;border-radius:4px;transition:background .2s,color .2s}.breadcrumb-item a:hover{background:#dee2e6;color:#343a40;text-decoration:underline}.breadcrumb-separator{margin:0 .25rem;color:#b2b9be;font-size:1rem}.breadcrumb-item:last-child{color:#6c757d;font-weight:400}\n"] }]
464
+ }], propDecorators: { items: [{
465
+ type: Input
466
+ }] } });
467
+
468
+ class TableComponent {
469
+ columns = [];
470
+ data = [];
471
+ columnTypes = {};
472
+ page = 1;
473
+ pageSize = 10;
474
+ totalItems = 0;
475
+ pageSizeOptions = [
476
+ { value: 5, label: '5' },
477
+ { value: 10, label: '10' },
478
+ { value: 20, label: '20' },
479
+ { value: 50, label: '50' }
480
+ ];
481
+ pageChange = new EventEmitter();
482
+ pageSizeChange = new EventEmitter();
483
+ get hasActions() {
484
+ return this.columns.some(c => !!c.actions && c.actions.length > 0);
485
+ }
486
+ get actionsList() {
487
+ const col = this.columns.find((c) => !!c.actions && c.actions.length > 0);
488
+ return col?.actions || [];
489
+ }
490
+ onPageSizeChange(newSize) {
491
+ this.pageSizeChange.emit(newSize);
492
+ }
493
+ // Paginação
494
+ get totalPages() {
495
+ return Math.max(1, Math.ceil(this.totalItems / this.pageSize));
496
+ }
497
+ get currentPage() {
498
+ return this.page;
499
+ }
500
+ goToPage(page) {
501
+ if (page < 1 || page > this.totalPages || page === this.currentPage)
502
+ return;
503
+ this.pageChange.emit(page);
504
+ }
505
+ getPages() {
506
+ const pages = [];
507
+ const total = this.totalPages;
508
+ const current = this.currentPage;
509
+ if (total <= 8) {
510
+ for (let i = 1; i <= total; i++)
511
+ pages.push(i);
512
+ }
513
+ else {
514
+ if (current <= 4) {
515
+ for (let i = 1; i <= 5; i++)
516
+ pages.push(i);
517
+ pages.push('...');
518
+ pages.push(total - 1, total);
519
+ }
520
+ else if (current >= total - 3) {
521
+ pages.push(1, 2);
522
+ pages.push('...');
523
+ for (let i = total - 4; i <= total; i++)
524
+ pages.push(i);
525
+ }
526
+ else {
527
+ pages.push(1, 2);
528
+ pages.push('...');
529
+ for (let i = current - 1; i <= current + 1; i++)
530
+ pages.push(i);
531
+ pages.push('...');
532
+ pages.push(total - 1, total);
533
+ }
534
+ }
535
+ return pages;
536
+ }
537
+ toNumber(val) {
538
+ return typeof val === 'number' ? val : parseInt(val, 10);
539
+ }
540
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
541
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: TableComponent, isStandalone: true, selector: "lib-table", inputs: { columns: "columns", data: "data", columnTypes: "columnTypes", page: "page", pageSize: "pageSize", totalItems: "totalItems", pageSizeOptions: "pageSizeOptions" }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<table class=\"table\">\n <thead class=\"table-light\">\n <tr>\n <ng-container *ngFor=\"let col of columns\">\n <th [style.width]=\"col.width || null\">\n {{ col.title }}\n </th>\n </ng-container>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let row of data\">\n <ng-container *ngFor=\"let col of columns\">\n <td class=\"align-middle\">\n <ng-container *ngIf=\"!col.actions\" [ngSwitch]=\"columnTypes[col.prop]\">\n <ng-container *ngSwitchCase=\"'date'\">{{ row[col.prop] | date:'dd/MM/yyyy' }}</ng-container>\n <ng-container *ngSwitchCase=\"'time'\">{{ row[col.prop] | date:'HH:mm' }}</ng-container>\n <ng-container *ngSwitchCase=\"'tag'\">\n <span class=\"badge rounded-pill\" [ngClass]=\"row[col.prop + 'Class'] || 'text-bg-dark'\">{{\n row[col.prop] }}</span>\n </ng-container>\n <ng-container *ngSwitchDefault>{{ row[col.prop] }}</ng-container>\n </ng-container>\n <ng-container *ngIf=\"col.actions\">\n <div class=\"dropdown\">\n <button class=\"btn btn-light btn-sm\" type=\"button\" data-bs-toggle=\"dropdown\"\n aria-expanded=\"false\">\n <i class=\"material-icons align-middle\">more_horiz</i>\n </button>\n <ul class=\"dropdown-menu\">\n <li *ngFor=\"let action of col.actions\">\n <a class=\"dropdown-item\" href=\"#\"\n (click)=\"action.callback(row); $event.preventDefault()\">\n <i *ngIf=\"action.icon\" class=\"material-icons align-middle me-2\">{{ action.icon\n }}</i>\n {{ action.title }}\n </a>\n </li>\n </ul>\n </div>\n </ng-container>\n </td>\n </ng-container>\n </tr>\n </tbody>\n</table>\n\n<!-- Paginator -->\n<nav aria-label=\"Tabela pagina\u00E7\u00E3o\" class=\"d-flex justify-content-between mt-3\">\n <lib-dropdown [options]=\"pageSizeOptions\" placeholder=\"Tamanho da p\u00E1gina\" [selectedOption]=\"pageSize\"\n [hideLabel]=\"true\" (valueChange)=\"onPageSizeChange($event)\"></lib-dropdown>\n\n <ul *ngIf=\"totalPages > 1\" class=\"pagination pagination-sm\">\n <li class=\"page-item\" [class.disabled]=\"currentPage === 1\">\n <button class=\"page-link\" (click)=\"goToPage(currentPage - 1)\" [disabled]=\"currentPage === 1\">\n <i class=\"material-icons align-middle\">chevron_left</i>\n </button>\n </li>\n <ng-container *ngFor=\"let page of getPages()\">\n <li class=\"page-item\" *ngIf=\"page !== '...'\" [class.active]=\"page === currentPage\">\n <button class=\"page-link\" (click)=\"goToPage(toNumber(page))\" [disabled]=\"page === currentPage\">{{ page\n }}</button>\n </li>\n <li class=\"page-item disabled\" *ngIf=\"page === '...'\"><span class=\"page-link\">...</span></li>\n </ng-container>\n <li class=\"page-item\" [class.disabled]=\"currentPage === totalPages\">\n <button class=\"page-link\" (click)=\"goToPage(currentPage + 1)\" [disabled]=\"currentPage === totalPages\">\n <i class=\"material-icons align-middle\">chevron_right</i>\n </button>\n </li>\n </ul>\n</nav>", styles: ["thead{border-top:1px solid #c6c7c8}thead th{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem}tbody th,tbody td,tbody .dropdown-menu{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}.pagination i{font-size:20px}.pagination .page-item .page-link{color:#414141;background-color:#fff}.pagination .page-item.active .page-link{color:#fff;background-color:#414141;border-color:#414141}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "pipe", type: i1.DatePipe, name: "date" }, { kind: "component", type: DropdownComponent, selector: "lib-dropdown", inputs: ["label", "placeholder", "options", "disabled", "required", "errorMessage", "selectedOption", "hideLabel"], outputs: ["valueChange"] }] });
542
+ }
543
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: TableComponent, decorators: [{
544
+ type: Component,
545
+ args: [{ selector: 'lib-table', imports: [CommonModule, DropdownComponent], template: "<table class=\"table\">\n <thead class=\"table-light\">\n <tr>\n <ng-container *ngFor=\"let col of columns\">\n <th [style.width]=\"col.width || null\">\n {{ col.title }}\n </th>\n </ng-container>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let row of data\">\n <ng-container *ngFor=\"let col of columns\">\n <td class=\"align-middle\">\n <ng-container *ngIf=\"!col.actions\" [ngSwitch]=\"columnTypes[col.prop]\">\n <ng-container *ngSwitchCase=\"'date'\">{{ row[col.prop] | date:'dd/MM/yyyy' }}</ng-container>\n <ng-container *ngSwitchCase=\"'time'\">{{ row[col.prop] | date:'HH:mm' }}</ng-container>\n <ng-container *ngSwitchCase=\"'tag'\">\n <span class=\"badge rounded-pill\" [ngClass]=\"row[col.prop + 'Class'] || 'text-bg-dark'\">{{\n row[col.prop] }}</span>\n </ng-container>\n <ng-container *ngSwitchDefault>{{ row[col.prop] }}</ng-container>\n </ng-container>\n <ng-container *ngIf=\"col.actions\">\n <div class=\"dropdown\">\n <button class=\"btn btn-light btn-sm\" type=\"button\" data-bs-toggle=\"dropdown\"\n aria-expanded=\"false\">\n <i class=\"material-icons align-middle\">more_horiz</i>\n </button>\n <ul class=\"dropdown-menu\">\n <li *ngFor=\"let action of col.actions\">\n <a class=\"dropdown-item\" href=\"#\"\n (click)=\"action.callback(row); $event.preventDefault()\">\n <i *ngIf=\"action.icon\" class=\"material-icons align-middle me-2\">{{ action.icon\n }}</i>\n {{ action.title }}\n </a>\n </li>\n </ul>\n </div>\n </ng-container>\n </td>\n </ng-container>\n </tr>\n </tbody>\n</table>\n\n<!-- Paginator -->\n<nav aria-label=\"Tabela pagina\u00E7\u00E3o\" class=\"d-flex justify-content-between mt-3\">\n <lib-dropdown [options]=\"pageSizeOptions\" placeholder=\"Tamanho da p\u00E1gina\" [selectedOption]=\"pageSize\"\n [hideLabel]=\"true\" (valueChange)=\"onPageSizeChange($event)\"></lib-dropdown>\n\n <ul *ngIf=\"totalPages > 1\" class=\"pagination pagination-sm\">\n <li class=\"page-item\" [class.disabled]=\"currentPage === 1\">\n <button class=\"page-link\" (click)=\"goToPage(currentPage - 1)\" [disabled]=\"currentPage === 1\">\n <i class=\"material-icons align-middle\">chevron_left</i>\n </button>\n </li>\n <ng-container *ngFor=\"let page of getPages()\">\n <li class=\"page-item\" *ngIf=\"page !== '...'\" [class.active]=\"page === currentPage\">\n <button class=\"page-link\" (click)=\"goToPage(toNumber(page))\" [disabled]=\"page === currentPage\">{{ page\n }}</button>\n </li>\n <li class=\"page-item disabled\" *ngIf=\"page === '...'\"><span class=\"page-link\">...</span></li>\n </ng-container>\n <li class=\"page-item\" [class.disabled]=\"currentPage === totalPages\">\n <button class=\"page-link\" (click)=\"goToPage(currentPage + 1)\" [disabled]=\"currentPage === totalPages\">\n <i class=\"material-icons align-middle\">chevron_right</i>\n </button>\n </li>\n </ul>\n</nav>", styles: ["thead{border-top:1px solid #c6c7c8}thead th{font-family:DM Sans,sans-serif;font-weight:600;font-size:.875rem}tbody th,tbody td,tbody .dropdown-menu{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem}.pagination i{font-size:20px}.pagination .page-item .page-link{color:#414141;background-color:#fff}.pagination .page-item.active .page-link{color:#fff;background-color:#414141;border-color:#414141}\n"] }]
546
+ }], propDecorators: { columns: [{
547
+ type: Input
548
+ }], data: [{
549
+ type: Input
550
+ }], columnTypes: [{
551
+ type: Input
552
+ }], page: [{
553
+ type: Input
554
+ }], pageSize: [{
555
+ type: Input
556
+ }], totalItems: [{
557
+ type: Input
558
+ }], pageSizeOptions: [{
559
+ type: Input
560
+ }], pageChange: [{
561
+ type: Output
562
+ }], pageSizeChange: [{
563
+ type: Output
564
+ }] } });
565
+
566
+ class SidebarComponent {
567
+ destroy$ = new Subject();
568
+ router = inject(Router);
569
+ // Configuração e dados
570
+ config = {};
571
+ menuItems = [];
572
+ isCollapsed = false;
573
+ // Eventos
574
+ itemClick = new EventEmitter();
575
+ toggle = new EventEmitter();
576
+ overlayToggle = new EventEmitter();
577
+ logoClick = new EventEmitter();
578
+ // Estado interno
579
+ state$ = new BehaviorSubject({
580
+ isCollapsed: false,
581
+ isMobile: false,
582
+ isOverlayOpen: false,
583
+ activeItemId: undefined
584
+ });
585
+ // Configuração padrão
586
+ defaultConfig = {
587
+ width: '280px',
588
+ collapsedWidth: '64px',
589
+ position: 'left',
590
+ collapsible: true,
591
+ autoCollapse: true,
592
+ overlay: true,
593
+ logo: {
594
+ src: '',
595
+ alt: 'Logo',
596
+ route: '/'
597
+ },
598
+ theme: 'light',
599
+ customCssClass: ''
600
+ };
601
+ // Getters para o estado
602
+ get currentState() {
603
+ return this.state$.value;
604
+ }
605
+ get finalConfig() {
606
+ return { ...this.defaultConfig, ...this.config };
607
+ }
608
+ get sidebarWidth() {
609
+ return this.currentState.isCollapsed ? this.finalConfig.collapsedWidth : this.finalConfig.width;
610
+ }
611
+ ngOnInit() {
612
+ this.initializeState();
613
+ this.detectActiveRoute();
614
+ }
615
+ ngOnDestroy() {
616
+ this.destroy$.next();
617
+ this.destroy$.complete();
618
+ }
619
+ /**
620
+ * Detecta mudanças de tela
621
+ */
622
+ onResize(event) {
623
+ this.checkMobileState();
624
+ }
625
+ /**
626
+ * Inicializa o estado do sidebar
627
+ */
628
+ initializeState() {
629
+ this.checkMobileState();
630
+ // Configura estado inicial baseado no input
631
+ this.updateState({
632
+ isCollapsed: this.isCollapsed
633
+ });
634
+ // Auto-collapse em mobile se configurado
635
+ if (this.finalConfig.autoCollapse && this.currentState.isMobile) {
636
+ this.updateState({ isCollapsed: true });
637
+ }
638
+ }
639
+ /**
640
+ * Verifica se está em modo mobile
641
+ */
642
+ checkMobileState() {
643
+ const isMobile = window.innerWidth < 768; // Breakpoint md
644
+ this.updateState({ isMobile });
645
+ if (isMobile && this.finalConfig.autoCollapse) {
646
+ this.updateState({ isCollapsed: true });
647
+ }
648
+ }
649
+ /**
650
+ * Detecta rota ativa
651
+ */
652
+ detectActiveRoute() {
653
+ const currentUrl = this.router.url;
654
+ const activeItem = this.findActiveItem(this.menuItems, currentUrl);
655
+ if (activeItem) {
656
+ this.updateState({ activeItemId: activeItem.id });
657
+ }
658
+ }
659
+ /**
660
+ * Encontra item ativo baseado na URL
661
+ */
662
+ findActiveItem(items, url) {
663
+ for (const item of items) {
664
+ if (item.route && url.startsWith(item.route)) {
665
+ return item;
666
+ }
667
+ if (item.children) {
668
+ const childItem = this.findActiveItem(item.children, url);
669
+ if (childItem)
670
+ return childItem;
671
+ }
672
+ }
673
+ return null;
674
+ }
675
+ /**
676
+ * Atualiza o estado
677
+ */
678
+ updateState(newState) {
679
+ const current = this.currentState;
680
+ this.state$.next({ ...current, ...newState });
681
+ }
682
+ /**
683
+ * Toggle do sidebar
684
+ */
685
+ toggleSidebar() {
686
+ const newCollapsedState = !this.currentState.isCollapsed;
687
+ this.updateState({ isCollapsed: newCollapsedState });
688
+ this.toggle.emit(!newCollapsedState);
689
+ }
690
+ /**
691
+ * Toggle do overlay (mobile)
692
+ */
693
+ toggleOverlay() {
694
+ const newOverlayState = !this.currentState.isOverlayOpen;
695
+ this.updateState({ isOverlayOpen: newOverlayState });
696
+ this.overlayToggle.emit(newOverlayState);
697
+ }
698
+ /**
699
+ * Clique no item do menu
700
+ */
701
+ onItemClick(item, event) {
702
+ if (item.disabled) {
703
+ event.preventDefault();
704
+ return;
705
+ }
706
+ // Se tem filhos, toggle expansão
707
+ if (item.children && item.children.length > 0) {
708
+ event.preventDefault();
709
+ this.toggleItemExpansion(item);
710
+ return;
711
+ }
712
+ // Ação personalizada
713
+ if (item.action) {
714
+ event.preventDefault();
715
+ item.action();
716
+ }
717
+ // Navegação
718
+ if (item.route) {
719
+ this.updateState({ activeItemId: item.id });
720
+ this.router.navigate([item.route]);
721
+ // Fecha overlay em mobile após navegação
722
+ if (this.currentState.isMobile && this.currentState.isOverlayOpen) {
723
+ this.updateState({ isOverlayOpen: false });
724
+ }
725
+ }
726
+ // Emite evento
727
+ this.itemClick.emit(item);
728
+ }
729
+ /**
730
+ * Toggle expansão de item com filhos
731
+ */
732
+ toggleItemExpansion(item) {
733
+ item.isExpanded = !item.isExpanded;
734
+ }
735
+ /**
736
+ * Clique no logo
737
+ */
738
+ onLogoClick() {
739
+ if (this.finalConfig.logo.route) {
740
+ this.router.navigate([this.finalConfig.logo.route]);
741
+ }
742
+ this.logoClick.emit();
743
+ }
744
+ /**
745
+ * Fecha overlay ao clicar fora (mobile)
746
+ */
747
+ closeOverlay() {
748
+ if (this.currentState.isMobile && this.currentState.isOverlayOpen) {
749
+ this.updateState({ isOverlayOpen: false });
750
+ }
751
+ }
752
+ /**
753
+ * Verifica se item está ativo
754
+ */
755
+ isItemActive(item) {
756
+ return this.currentState.activeItemId === item.id;
757
+ }
758
+ /**
759
+ * Verifica se item tem filhos ativos
760
+ */
761
+ hasActiveChild(item) {
762
+ if (!item.children)
763
+ return false;
764
+ return item.children.some(child => this.isItemActive(child) || this.hasActiveChild(child));
765
+ }
766
+ /**
767
+ * TrackBy function para performance
768
+ */
769
+ trackByItemId(index, item) {
770
+ return item.id;
771
+ }
772
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
773
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: SidebarComponent, isStandalone: true, selector: "lib-sidebar", inputs: { config: "config", menuItems: "menuItems", isCollapsed: "isCollapsed" }, outputs: { itemClick: "itemClick", toggle: "toggle", overlayToggle: "overlayToggle", logoClick: "logoClick" }, host: { listeners: { "window:resize": "onResize($event)" } }, ngImport: i0, template: "<!-- Overlay para mobile -->\n<div *ngIf=\"currentState.isMobile && currentState.isOverlayOpen\" class=\"sidebar-overlay\" (click)=\"closeOverlay()\">\n</div>\n\n<!-- Sidebar principal -->\n<aside class=\"sidebar\" [class.collapsed]=\"currentState.isCollapsed\" [class.mobile]=\"currentState.isMobile\"\n [class.overlay-open]=\"currentState.isOverlayOpen\" [class.theme-light]=\"finalConfig.theme === 'light'\"\n [class.theme-dark]=\"finalConfig.theme === 'dark'\" [class.position-left]=\"finalConfig.position === 'left'\"\n [class.position-right]=\"finalConfig.position === 'right'\" [ngClass]=\"finalConfig.customCssClass\"\n [style.width]=\"sidebarWidth\">\n\n <!-- Header com logo -->\n <div class=\"sidebar-header\" *ngIf=\"finalConfig.logo.src\">\n <button class=\"logo-container\" (click)=\"onLogoClick()\" [attr.aria-label]=\"finalConfig.logo.alt\">\n <img [src]=\"finalConfig.logo.src\" [alt]=\"finalConfig.logo.alt\" class=\"logo-image\">\n\n <span class=\"logo-text\" *ngIf=\"!currentState.isCollapsed\">\n {{ finalConfig.logo.alt }}\n </span>\n </button>\n\n <!-- Bot\u00E3o de toggle (se habilitado) -->\n <button *ngIf=\"finalConfig.collapsible && !currentState.isMobile\" class=\"toggle-btn\" (click)=\"toggleSidebar()\"\n [attr.aria-label]=\"currentState.isCollapsed ? 'Expandir menu' : 'Recolher menu'\">\n <i class=\"toggle-icon fa\" [class.fa-angle-left]=\"!currentState.isCollapsed\"\n [class.fa-angle-right]=\"currentState.isCollapsed\">\n </i>\n </button>\n </div>\n\n <!-- Navega\u00E7\u00E3o principal -->\n <nav class=\"sidebar-nav\">\n <ul class=\"nav-list\">\n <ng-container *ngFor=\"let item of menuItems; trackBy: trackByItemId\">\n\n <!-- Divisor -->\n <li *ngIf=\"item.divider\" class=\"nav-divider\" role=\"separator\"></li>\n\n <!-- Item do menu -->\n <li class=\"nav-item\" [class.active]=\"isItemActive(item)\"\n [class.has-children]=\"item.children && item.children.length > 0\" [class.expanded]=\"item.isExpanded\"\n [class.disabled]=\"item.disabled\" [class.has-active-child]=\"hasActiveChild(item)\">\n\n <a class=\"nav-link\" [routerLink]=\"item.route\" [class.no-route]=\"!item.route\"\n (click)=\"onItemClick(item, $event)\" [attr.aria-label]=\"item.label\"\n [attr.aria-expanded]=\"item.children ? item.isExpanded : null\" [attr.tabindex]=\"item.disabled ? -1 : 0\">\n\n <!-- \u00CDcone -->\n <i class=\"nav-icon\" [ngClass]=\"item.icon\" [attr.aria-hidden]=\"true\">\n </i>\n\n <!-- Label -->\n <span class=\"nav-label\" *ngIf=\"!currentState.isCollapsed\">\n {{ item.label }}\n </span>\n\n <!-- Badge -->\n <span class=\"nav-badge\" *ngIf=\"item.badge && !currentState.isCollapsed\">\n {{ item.badge }}\n </span>\n\n <!-- Seta para submenu -->\n <i class=\"nav-arrow fa\" *ngIf=\"item.children && item.children.length > 0 && !currentState.isCollapsed\"\n [class.fa-chevron-down]=\"item.isExpanded\" [class.fa-chevron-right]=\"!item.isExpanded\"\n [attr.aria-hidden]=\"true\">\n </i>\n </a>\n\n <!-- Submenu -->\n <ul class=\"nav-submenu\"\n *ngIf=\"item.children && item.children.length > 0 && item.isExpanded && !currentState.isCollapsed\">\n <li class=\"nav-subitem\" *ngFor=\"let subItem of item.children; trackBy: trackByItemId\"\n [class.active]=\"isItemActive(subItem)\" [class.disabled]=\"subItem.disabled\">\n\n <a class=\"nav-sublink\" [routerLink]=\"subItem.route\" [class.no-route]=\"!subItem.route\"\n (click)=\"onItemClick(subItem, $event)\" [attr.aria-label]=\"subItem.label\"\n [attr.tabindex]=\"subItem.disabled ? -1 : 0\">\n\n <!-- \u00CDcone do subitem -->\n <i class=\"nav-subicon\" [ngClass]=\"subItem.icon\" [attr.aria-hidden]=\"true\">\n </i>\n\n <!-- Label do subitem -->\n <span class=\"nav-sublabel\">\n {{ subItem.label }}\n </span>\n\n <!-- Badge do subitem -->\n <span class=\"nav-subbadge\" *ngIf=\"subItem.badge\">\n {{ subItem.badge }}\n </span>\n </a>\n </li>\n </ul>\n </li>\n </ng-container>\n </ul>\n </nav>\n\n <!-- Footer opcional -->\n <div class=\"sidebar-footer\" *ngIf=\"!currentState.isCollapsed\">\n <ng-content select=\"[slot=footer]\"></ng-content>\n </div>\n</aside>\n\n<!-- Bot\u00E3o mobile para abrir sidebar -->\n<button *ngIf=\"currentState.isMobile && !currentState.isOverlayOpen\" class=\"mobile-toggle-btn\" (click)=\"toggleOverlay()\"\n [attr.aria-label]=\"'Abrir menu'\">\n <i class=\"fa fa-bars\" [attr.aria-hidden]=\"true\"></i>\n</button>", styles: [":host{--sidebar-bg: #ffffff;--sidebar-text: #343a40;--sidebar-text-secondary: #495057;--sidebar-border: #dee2e6;--sidebar-hover: #f8f9fa;--sidebar-active: #007bff;--sidebar-active-bg: rgba(0, 123, 255, .1);--sidebar-transition: .3s cubic-bezier(.4, 0, .2, 1);--sidebar-shadow: 0 2px 12px rgba(0, 0, 0, .08)}:host.theme-dark{--sidebar-bg: #343a40;--sidebar-text: #ffffff;--sidebar-text-secondary: #b2b9be;--sidebar-border: #495057;--sidebar-hover: rgba(255, 255, 255, .1);--sidebar-shadow: 0 2px 12px rgba(0, 0, 0, .25)}.sidebar-overlay{position:fixed;inset:0;background:#00000080;z-index:998;backdrop-filter:blur(2px)}.sidebar{position:fixed;top:0;bottom:0;left:0;width:280px;background:var(--sidebar-bg);border-right:1px solid var(--sidebar-border);box-shadow:var(--sidebar-shadow);transition:var(--sidebar-transition);z-index:999;display:flex;flex-direction:column;overflow:visible}.sidebar.position-right{left:auto;right:0;border-left:1px solid var(--sidebar-border);border-right:none}.sidebar.collapsed{width:64px}.sidebar.collapsed .sidebar-header .logo-text,.sidebar.collapsed .nav-label,.sidebar.collapsed .nav-badge,.sidebar.collapsed .nav-arrow,.sidebar.collapsed .nav-submenu{opacity:0;pointer-events:none}.sidebar.collapsed .nav-link{justify-content:center;padding-left:.5rem;padding-right:.5rem;gap:0}.sidebar.collapsed .nav-icon{margin:0 auto}.sidebar.collapsed .sidebar-header{padding:1rem;justify-content:center!important}.sidebar.collapsed .logo-container{width:100%;display:flex;justify-content:center;align-items:center;gap:0;padding:0;margin:0}.sidebar.collapsed .logo-image{margin:0 auto;display:block;width:32px;height:32px}.sidebar.collapsed .toggle-btn{right:-18px;width:36px;height:36px}.sidebar.mobile{transform:translate(-100%)}.sidebar.mobile.position-right{transform:translate(100%)}.sidebar.mobile.overlay-open{transform:translate(0)}.sidebar-header{padding:1.5rem;border-bottom:1px solid var(--sidebar-border);display:flex;align-items:center;justify-content:space-between;min-height:80px;position:relative}.logo-container{display:flex;align-items:center;gap:1rem;background:none;border:none;padding:0;cursor:pointer;color:inherit;text-decoration:none;transition:var(--sidebar-transition)}.logo-container:hover{opacity:.8}.logo-image{width:32px;height:32px;object-fit:contain;flex-shrink:0}.logo-text{font-family:DM Sans,sans-serif;font-size:1.125rem;font-weight:600;color:var(--sidebar-text);transition:var(--sidebar-transition);white-space:nowrap}.toggle-btn{background:none;border:none;width:32px;height:32px;border-radius:.5rem;display:flex;align-items:center;background:var(--sidebar-bg);border:1px solid rgba(0,0,0,.06);box-shadow:0 6px 14px #0614231f;backdrop-filter:blur(4px);transition:transform .18s var(--sidebar-transition),box-shadow .18s var(--sidebar-transition);position:absolute;top:50%;transform:translateY(-50%);right:-16px;z-index:1000}.toggle-btn:hover{transform:translateY(-50%) translateY(-2px);box-shadow:0 10px 20px #06142329}.toggle-btn:hover{background:var(--sidebar-hover);color:var(--sidebar-text)}.toggle-btn .toggle-icon{font-size:1rem}.sidebar-nav{flex:1;overflow-y:auto;overflow-x:hidden;padding:1rem 0}.sidebar-nav::-webkit-scrollbar{width:4px}.sidebar-nav::-webkit-scrollbar-track{background:transparent}.sidebar-nav::-webkit-scrollbar-thumb{background:var(--sidebar-border);border-radius:2px}.sidebar-nav::-webkit-scrollbar-thumb:hover{background:var(--sidebar-text-secondary)}.nav-list{list-style:none;margin:0;padding:0}.nav-divider{height:1px;background:var(--sidebar-border);margin:1rem 0}.nav-item{position:relative;margin:0 1rem .25rem}.nav-item.active>.nav-link{background:var(--sidebar-active-bg);color:var(--sidebar-active)}.nav-item.active>.nav-link .nav-icon{color:var(--sidebar-active)}.nav-item.has-active-child>.nav-link{color:var(--sidebar-active)}.nav-item.has-active-child>.nav-link .nav-icon{color:var(--sidebar-active)}.nav-item.disabled{opacity:.5;pointer-events:none}.nav-link{display:flex;align-items:center;gap:1rem;padding:1rem;border-radius:.5rem;color:var(--sidebar-text);text-decoration:none;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:500;line-height:1.5;transition:var(--sidebar-transition);cursor:pointer;position:relative}.nav-link:hover:not(.active){background:var(--sidebar-hover)}.nav-link.no-route{cursor:pointer}.nav-icon{font-size:1.125rem;color:var(--sidebar-text-secondary);flex-shrink:0;width:20px;text-align:center;transition:var(--sidebar-transition)}.nav-label{flex:1;white-space:nowrap;transition:var(--sidebar-transition)}.nav-badge{background:var(--sidebar-active);color:#fff;font-size:.75rem;font-weight:600;padding:2px 6px;border-radius:10px;min-width:18px;text-align:center;transition:var(--sidebar-transition)}.nav-arrow{font-size:.875rem;color:var(--sidebar-text-secondary);transition:var(--sidebar-transition);margin-left:auto}.nav-submenu{list-style:none;margin:.25rem 0 0;padding:0;border-left:2px solid var(--sidebar-border);margin-left:calc(20px + 1rem);transition:var(--sidebar-transition)}.nav-subitem{margin:0 0 .25rem}.nav-subitem.active>.nav-sublink{color:var(--sidebar-active);background:var(--sidebar-active-bg)}.nav-subitem.active>.nav-sublink .nav-subicon{color:var(--sidebar-active)}.nav-subitem.disabled{opacity:.5;pointer-events:none}.nav-subicon{font-size:.875rem;width:16px;text-align:center;flex-shrink:0;transition:var(--sidebar-transition)}.nav-sublabel{flex:1;white-space:nowrap}.nav-subbadge{background:var(--sidebar-active);color:#fff;font-size:10px;font-weight:600;padding:1px 4px;border-radius:8px;min-width:14px;text-align:center}.sidebar-footer{padding:1rem;border-top:1px solid var(--sidebar-border);margin-top:auto}.mobile-toggle-btn{position:fixed;top:1.5rem;left:1.5rem;width:48px;height:48px;background:var(--sidebar-active);color:#fff;border:none;border-radius:.75rem;display:flex;align-items:center;justify-content:center;font-size:1.125rem;box-shadow:var(--sidebar-shadow);z-index:997;cursor:pointer;transition:var(--sidebar-transition)}.mobile-toggle-btn:hover{transform:scale(1.05)}.mobile-toggle-btn:active{transform:scale(.95)}@media (max-width: 768px){.sidebar{width:280px!important}.sidebar.position-right{transform:translate(100%)}.sidebar.position-right.overlay-open{transform:translate(0)}}@keyframes slideInLeft{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.sidebar.mobile.overlay-open{animation:slideInLeft .3s ease-out}.sidebar.mobile.overlay-open.position-right{animation:slideInRight .3s ease-out}.nav-link:focus,.nav-sublink:focus,.toggle-btn:focus,.logo-container:focus,.mobile-toggle-btn:focus{outline:2px solid var(--sidebar-active);outline-offset:2px}.collapsed .nav-submenu{display:none!important}.has-children.collapsed .nav-arrow{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] });
774
+ }
775
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SidebarComponent, decorators: [{
776
+ type: Component,
777
+ args: [{ selector: 'lib-sidebar', imports: [CommonModule, RouterModule], template: "<!-- Overlay para mobile -->\n<div *ngIf=\"currentState.isMobile && currentState.isOverlayOpen\" class=\"sidebar-overlay\" (click)=\"closeOverlay()\">\n</div>\n\n<!-- Sidebar principal -->\n<aside class=\"sidebar\" [class.collapsed]=\"currentState.isCollapsed\" [class.mobile]=\"currentState.isMobile\"\n [class.overlay-open]=\"currentState.isOverlayOpen\" [class.theme-light]=\"finalConfig.theme === 'light'\"\n [class.theme-dark]=\"finalConfig.theme === 'dark'\" [class.position-left]=\"finalConfig.position === 'left'\"\n [class.position-right]=\"finalConfig.position === 'right'\" [ngClass]=\"finalConfig.customCssClass\"\n [style.width]=\"sidebarWidth\">\n\n <!-- Header com logo -->\n <div class=\"sidebar-header\" *ngIf=\"finalConfig.logo.src\">\n <button class=\"logo-container\" (click)=\"onLogoClick()\" [attr.aria-label]=\"finalConfig.logo.alt\">\n <img [src]=\"finalConfig.logo.src\" [alt]=\"finalConfig.logo.alt\" class=\"logo-image\">\n\n <span class=\"logo-text\" *ngIf=\"!currentState.isCollapsed\">\n {{ finalConfig.logo.alt }}\n </span>\n </button>\n\n <!-- Bot\u00E3o de toggle (se habilitado) -->\n <button *ngIf=\"finalConfig.collapsible && !currentState.isMobile\" class=\"toggle-btn\" (click)=\"toggleSidebar()\"\n [attr.aria-label]=\"currentState.isCollapsed ? 'Expandir menu' : 'Recolher menu'\">\n <i class=\"toggle-icon fa\" [class.fa-angle-left]=\"!currentState.isCollapsed\"\n [class.fa-angle-right]=\"currentState.isCollapsed\">\n </i>\n </button>\n </div>\n\n <!-- Navega\u00E7\u00E3o principal -->\n <nav class=\"sidebar-nav\">\n <ul class=\"nav-list\">\n <ng-container *ngFor=\"let item of menuItems; trackBy: trackByItemId\">\n\n <!-- Divisor -->\n <li *ngIf=\"item.divider\" class=\"nav-divider\" role=\"separator\"></li>\n\n <!-- Item do menu -->\n <li class=\"nav-item\" [class.active]=\"isItemActive(item)\"\n [class.has-children]=\"item.children && item.children.length > 0\" [class.expanded]=\"item.isExpanded\"\n [class.disabled]=\"item.disabled\" [class.has-active-child]=\"hasActiveChild(item)\">\n\n <a class=\"nav-link\" [routerLink]=\"item.route\" [class.no-route]=\"!item.route\"\n (click)=\"onItemClick(item, $event)\" [attr.aria-label]=\"item.label\"\n [attr.aria-expanded]=\"item.children ? item.isExpanded : null\" [attr.tabindex]=\"item.disabled ? -1 : 0\">\n\n <!-- \u00CDcone -->\n <i class=\"nav-icon\" [ngClass]=\"item.icon\" [attr.aria-hidden]=\"true\">\n </i>\n\n <!-- Label -->\n <span class=\"nav-label\" *ngIf=\"!currentState.isCollapsed\">\n {{ item.label }}\n </span>\n\n <!-- Badge -->\n <span class=\"nav-badge\" *ngIf=\"item.badge && !currentState.isCollapsed\">\n {{ item.badge }}\n </span>\n\n <!-- Seta para submenu -->\n <i class=\"nav-arrow fa\" *ngIf=\"item.children && item.children.length > 0 && !currentState.isCollapsed\"\n [class.fa-chevron-down]=\"item.isExpanded\" [class.fa-chevron-right]=\"!item.isExpanded\"\n [attr.aria-hidden]=\"true\">\n </i>\n </a>\n\n <!-- Submenu -->\n <ul class=\"nav-submenu\"\n *ngIf=\"item.children && item.children.length > 0 && item.isExpanded && !currentState.isCollapsed\">\n <li class=\"nav-subitem\" *ngFor=\"let subItem of item.children; trackBy: trackByItemId\"\n [class.active]=\"isItemActive(subItem)\" [class.disabled]=\"subItem.disabled\">\n\n <a class=\"nav-sublink\" [routerLink]=\"subItem.route\" [class.no-route]=\"!subItem.route\"\n (click)=\"onItemClick(subItem, $event)\" [attr.aria-label]=\"subItem.label\"\n [attr.tabindex]=\"subItem.disabled ? -1 : 0\">\n\n <!-- \u00CDcone do subitem -->\n <i class=\"nav-subicon\" [ngClass]=\"subItem.icon\" [attr.aria-hidden]=\"true\">\n </i>\n\n <!-- Label do subitem -->\n <span class=\"nav-sublabel\">\n {{ subItem.label }}\n </span>\n\n <!-- Badge do subitem -->\n <span class=\"nav-subbadge\" *ngIf=\"subItem.badge\">\n {{ subItem.badge }}\n </span>\n </a>\n </li>\n </ul>\n </li>\n </ng-container>\n </ul>\n </nav>\n\n <!-- Footer opcional -->\n <div class=\"sidebar-footer\" *ngIf=\"!currentState.isCollapsed\">\n <ng-content select=\"[slot=footer]\"></ng-content>\n </div>\n</aside>\n\n<!-- Bot\u00E3o mobile para abrir sidebar -->\n<button *ngIf=\"currentState.isMobile && !currentState.isOverlayOpen\" class=\"mobile-toggle-btn\" (click)=\"toggleOverlay()\"\n [attr.aria-label]=\"'Abrir menu'\">\n <i class=\"fa fa-bars\" [attr.aria-hidden]=\"true\"></i>\n</button>", styles: [":host{--sidebar-bg: #ffffff;--sidebar-text: #343a40;--sidebar-text-secondary: #495057;--sidebar-border: #dee2e6;--sidebar-hover: #f8f9fa;--sidebar-active: #007bff;--sidebar-active-bg: rgba(0, 123, 255, .1);--sidebar-transition: .3s cubic-bezier(.4, 0, .2, 1);--sidebar-shadow: 0 2px 12px rgba(0, 0, 0, .08)}:host.theme-dark{--sidebar-bg: #343a40;--sidebar-text: #ffffff;--sidebar-text-secondary: #b2b9be;--sidebar-border: #495057;--sidebar-hover: rgba(255, 255, 255, .1);--sidebar-shadow: 0 2px 12px rgba(0, 0, 0, .25)}.sidebar-overlay{position:fixed;inset:0;background:#00000080;z-index:998;backdrop-filter:blur(2px)}.sidebar{position:fixed;top:0;bottom:0;left:0;width:280px;background:var(--sidebar-bg);border-right:1px solid var(--sidebar-border);box-shadow:var(--sidebar-shadow);transition:var(--sidebar-transition);z-index:999;display:flex;flex-direction:column;overflow:visible}.sidebar.position-right{left:auto;right:0;border-left:1px solid var(--sidebar-border);border-right:none}.sidebar.collapsed{width:64px}.sidebar.collapsed .sidebar-header .logo-text,.sidebar.collapsed .nav-label,.sidebar.collapsed .nav-badge,.sidebar.collapsed .nav-arrow,.sidebar.collapsed .nav-submenu{opacity:0;pointer-events:none}.sidebar.collapsed .nav-link{justify-content:center;padding-left:.5rem;padding-right:.5rem;gap:0}.sidebar.collapsed .nav-icon{margin:0 auto}.sidebar.collapsed .sidebar-header{padding:1rem;justify-content:center!important}.sidebar.collapsed .logo-container{width:100%;display:flex;justify-content:center;align-items:center;gap:0;padding:0;margin:0}.sidebar.collapsed .logo-image{margin:0 auto;display:block;width:32px;height:32px}.sidebar.collapsed .toggle-btn{right:-18px;width:36px;height:36px}.sidebar.mobile{transform:translate(-100%)}.sidebar.mobile.position-right{transform:translate(100%)}.sidebar.mobile.overlay-open{transform:translate(0)}.sidebar-header{padding:1.5rem;border-bottom:1px solid var(--sidebar-border);display:flex;align-items:center;justify-content:space-between;min-height:80px;position:relative}.logo-container{display:flex;align-items:center;gap:1rem;background:none;border:none;padding:0;cursor:pointer;color:inherit;text-decoration:none;transition:var(--sidebar-transition)}.logo-container:hover{opacity:.8}.logo-image{width:32px;height:32px;object-fit:contain;flex-shrink:0}.logo-text{font-family:DM Sans,sans-serif;font-size:1.125rem;font-weight:600;color:var(--sidebar-text);transition:var(--sidebar-transition);white-space:nowrap}.toggle-btn{background:none;border:none;width:32px;height:32px;border-radius:.5rem;display:flex;align-items:center;background:var(--sidebar-bg);border:1px solid rgba(0,0,0,.06);box-shadow:0 6px 14px #0614231f;backdrop-filter:blur(4px);transition:transform .18s var(--sidebar-transition),box-shadow .18s var(--sidebar-transition);position:absolute;top:50%;transform:translateY(-50%);right:-16px;z-index:1000}.toggle-btn:hover{transform:translateY(-50%) translateY(-2px);box-shadow:0 10px 20px #06142329}.toggle-btn:hover{background:var(--sidebar-hover);color:var(--sidebar-text)}.toggle-btn .toggle-icon{font-size:1rem}.sidebar-nav{flex:1;overflow-y:auto;overflow-x:hidden;padding:1rem 0}.sidebar-nav::-webkit-scrollbar{width:4px}.sidebar-nav::-webkit-scrollbar-track{background:transparent}.sidebar-nav::-webkit-scrollbar-thumb{background:var(--sidebar-border);border-radius:2px}.sidebar-nav::-webkit-scrollbar-thumb:hover{background:var(--sidebar-text-secondary)}.nav-list{list-style:none;margin:0;padding:0}.nav-divider{height:1px;background:var(--sidebar-border);margin:1rem 0}.nav-item{position:relative;margin:0 1rem .25rem}.nav-item.active>.nav-link{background:var(--sidebar-active-bg);color:var(--sidebar-active)}.nav-item.active>.nav-link .nav-icon{color:var(--sidebar-active)}.nav-item.has-active-child>.nav-link{color:var(--sidebar-active)}.nav-item.has-active-child>.nav-link .nav-icon{color:var(--sidebar-active)}.nav-item.disabled{opacity:.5;pointer-events:none}.nav-link{display:flex;align-items:center;gap:1rem;padding:1rem;border-radius:.5rem;color:var(--sidebar-text);text-decoration:none;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:500;line-height:1.5;transition:var(--sidebar-transition);cursor:pointer;position:relative}.nav-link:hover:not(.active){background:var(--sidebar-hover)}.nav-link.no-route{cursor:pointer}.nav-icon{font-size:1.125rem;color:var(--sidebar-text-secondary);flex-shrink:0;width:20px;text-align:center;transition:var(--sidebar-transition)}.nav-label{flex:1;white-space:nowrap;transition:var(--sidebar-transition)}.nav-badge{background:var(--sidebar-active);color:#fff;font-size:.75rem;font-weight:600;padding:2px 6px;border-radius:10px;min-width:18px;text-align:center;transition:var(--sidebar-transition)}.nav-arrow{font-size:.875rem;color:var(--sidebar-text-secondary);transition:var(--sidebar-transition);margin-left:auto}.nav-submenu{list-style:none;margin:.25rem 0 0;padding:0;border-left:2px solid var(--sidebar-border);margin-left:calc(20px + 1rem);transition:var(--sidebar-transition)}.nav-subitem{margin:0 0 .25rem}.nav-subitem.active>.nav-sublink{color:var(--sidebar-active);background:var(--sidebar-active-bg)}.nav-subitem.active>.nav-sublink .nav-subicon{color:var(--sidebar-active)}.nav-subitem.disabled{opacity:.5;pointer-events:none}.nav-subicon{font-size:.875rem;width:16px;text-align:center;flex-shrink:0;transition:var(--sidebar-transition)}.nav-sublabel{flex:1;white-space:nowrap}.nav-subbadge{background:var(--sidebar-active);color:#fff;font-size:10px;font-weight:600;padding:1px 4px;border-radius:8px;min-width:14px;text-align:center}.sidebar-footer{padding:1rem;border-top:1px solid var(--sidebar-border);margin-top:auto}.mobile-toggle-btn{position:fixed;top:1.5rem;left:1.5rem;width:48px;height:48px;background:var(--sidebar-active);color:#fff;border:none;border-radius:.75rem;display:flex;align-items:center;justify-content:center;font-size:1.125rem;box-shadow:var(--sidebar-shadow);z-index:997;cursor:pointer;transition:var(--sidebar-transition)}.mobile-toggle-btn:hover{transform:scale(1.05)}.mobile-toggle-btn:active{transform:scale(.95)}@media (max-width: 768px){.sidebar{width:280px!important}.sidebar.position-right{transform:translate(100%)}.sidebar.position-right.overlay-open{transform:translate(0)}}@keyframes slideInLeft{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.sidebar.mobile.overlay-open{animation:slideInLeft .3s ease-out}.sidebar.mobile.overlay-open.position-right{animation:slideInRight .3s ease-out}.nav-link:focus,.nav-sublink:focus,.toggle-btn:focus,.logo-container:focus,.mobile-toggle-btn:focus{outline:2px solid var(--sidebar-active);outline-offset:2px}.collapsed .nav-submenu{display:none!important}.has-children.collapsed .nav-arrow{display:none}\n"] }]
778
+ }], propDecorators: { config: [{
779
+ type: Input
780
+ }], menuItems: [{
781
+ type: Input
782
+ }], isCollapsed: [{
783
+ type: Input
784
+ }], itemClick: [{
785
+ type: Output
786
+ }], toggle: [{
787
+ type: Output
788
+ }], overlayToggle: [{
789
+ type: Output
790
+ }], logoClick: [{
791
+ type: Output
792
+ }], onResize: [{
793
+ type: HostListener,
794
+ args: ['window:resize', ['$event']]
795
+ }] } });
796
+
797
+ /**
798
+ * Tipos e interfaces para autenticação
799
+ */
800
+ /**
801
+ * Estados do processo de login
802
+ */
803
+ var LoginStatus;
804
+ (function (LoginStatus) {
805
+ LoginStatus["IDLE"] = "IDLE";
806
+ LoginStatus["LOADING"] = "LOADING";
807
+ LoginStatus["SUCCESS"] = "SUCCESS";
808
+ LoginStatus["ERROR"] = "ERROR";
809
+ })(LoginStatus || (LoginStatus = {}));
810
+
811
+ class LocalStorageService {
812
+ storageAvailable = new BehaviorSubject(this.isStorageAvailable());
813
+ // Observable para monitorar disponibilidade do storage
814
+ get storageAvailable$() {
815
+ return this.storageAvailable.asObservable();
816
+ }
817
+ /**
818
+ * Salva item no storage com validação básica
819
+ */
820
+ async setItem(key, value, storageType = 'localStorage', options) {
821
+ if (!this.isStorageAvailable(storageType)) {
822
+ console.warn(`${storageType} não está disponível`);
823
+ return false;
824
+ }
825
+ if (!this.validateKey(key)) {
826
+ console.error('Chave inválida fornecida');
827
+ return false;
828
+ }
829
+ try {
830
+ const storage = this.getStorage(storageType);
831
+ let serializedValue = this.safeJsonStringify(value);
832
+ if (!serializedValue) {
833
+ console.error('Falha na serialização segura dos dados');
834
+ return false;
835
+ }
836
+ // Aplicar criptografia REAL se solicitada
837
+ if (options?.encrypt) {
838
+ serializedValue = await this.encryptData(serializedValue);
839
+ if (!serializedValue) {
840
+ console.error('Falha na criptografia dos dados');
841
+ return false;
842
+ }
843
+ }
844
+ // Validação de tamanho (localStorage tem limite ~5MB)
845
+ if (serializedValue.length > 5000000) {
846
+ console.error('Dados muito grandes para o storage');
847
+ return false;
848
+ }
849
+ storage.setItem(this.sanitizeKey(key), serializedValue);
850
+ return true;
851
+ }
852
+ catch (error) {
853
+ console.error(`Erro ao salvar no ${storageType}:`, error);
854
+ return false;
855
+ }
856
+ }
857
+ /**
858
+ * Recupera item do storage com tipo seguro
859
+ */
860
+ async getItem(key, storageType = 'localStorage', options) {
861
+ if (!this.isStorageAvailable(storageType)) {
862
+ return null;
863
+ }
864
+ try {
865
+ const storage = this.getStorage(storageType);
866
+ let item = storage.getItem(this.sanitizeKey(key));
867
+ if (!item)
868
+ return null;
869
+ // Aplicar descriptografia REAL se necessário
870
+ if (options?.encrypt && item) {
871
+ item = await this.decryptData(item);
872
+ if (!item) {
873
+ console.error('Falha ao descriptografar item');
874
+ return null;
875
+ }
876
+ }
877
+ return item ? this.safeJsonParse(item) : null;
878
+ }
879
+ catch (error) {
880
+ console.error(`Erro ao recuperar do ${storageType}:`, error);
881
+ return null;
882
+ }
883
+ }
884
+ /**
885
+ * Remove item específico
886
+ */
887
+ removeItem(key, storageType = 'localStorage') {
888
+ if (!this.isStorageAvailable(storageType)) {
889
+ return false;
890
+ }
891
+ try {
892
+ const storage = this.getStorage(storageType);
893
+ storage.removeItem(this.sanitizeKey(key));
894
+ return true;
895
+ }
896
+ catch (error) {
897
+ console.error(`Erro ao remover do ${storageType}:`, error);
898
+ return false;
899
+ }
900
+ }
901
+ /**
902
+ * Limpa todo o storage
903
+ */
904
+ clear(storageType = 'localStorage') {
905
+ if (!this.isStorageAvailable(storageType)) {
906
+ return false;
907
+ }
908
+ try {
909
+ const storage = this.getStorage(storageType);
910
+ storage.clear();
911
+ return true;
912
+ }
913
+ catch (error) {
914
+ console.error(`Erro ao limpar ${storageType}:`, error);
915
+ return false;
916
+ }
917
+ }
918
+ /**
919
+ * Verifica se item existe
920
+ */
921
+ hasItem(key, storageType = 'localStorage') {
922
+ if (!this.isStorageAvailable(storageType)) {
923
+ return false;
924
+ }
925
+ const storage = this.getStorage(storageType);
926
+ return storage.getItem(this.sanitizeKey(key)) !== null;
927
+ }
928
+ /**
929
+ * Salva item com expiração
930
+ */
931
+ async setItemWithExpiry(key, value, expiryInMinutes, storageType = 'localStorage') {
932
+ const now = new Date();
933
+ const item = {
934
+ value: value,
935
+ expiry: now.getTime() + (expiryInMinutes * 60 * 1000)
936
+ };
937
+ return await this.setItem(key, item, storageType);
938
+ }
939
+ /**
940
+ * Recupera item com verificação de expiração
941
+ */
942
+ async getItemWithExpiry(key, storageType = 'localStorage') {
943
+ const itemStr = await this.getItem(key, storageType);
944
+ if (!itemStr) {
945
+ return null;
946
+ }
947
+ const now = new Date();
948
+ if (now.getTime() > itemStr.expiry) {
949
+ this.removeItem(key, storageType);
950
+ return null;
951
+ }
952
+ return itemStr.value;
953
+ }
954
+ /**
955
+ * Obtém todas as chaves do storage
956
+ */
957
+ getAllKeys(storageType = 'localStorage') {
958
+ if (!this.isStorageAvailable(storageType)) {
959
+ return [];
960
+ }
961
+ const storage = this.getStorage(storageType);
962
+ const keys = [];
963
+ for (let i = 0; i < storage.length; i++) {
964
+ const key = storage.key(i);
965
+ if (key)
966
+ keys.push(key);
967
+ }
968
+ return keys;
969
+ }
970
+ /**
971
+ * Obtém tamanho ocupado do storage em bytes (aproximado)
972
+ */
973
+ getStorageSize(storageType = 'localStorage') {
974
+ if (!this.isStorageAvailable(storageType)) {
975
+ return 0;
976
+ }
977
+ const storage = this.getStorage(storageType);
978
+ let total = 0;
979
+ for (let key in storage) {
980
+ if (storage.hasOwnProperty(key)) {
981
+ total += storage[key].length + key.length;
982
+ }
983
+ }
984
+ return total;
985
+ }
986
+ /**
987
+ * Monitor de eventos de storage
988
+ */
989
+ monitorStorageEvents() {
990
+ return new Observable(observer => {
991
+ const handler = (event) => {
992
+ // Validação básica de segurança
993
+ if (this.validateStorageEvent(event)) {
994
+ observer.next(event);
995
+ }
996
+ };
997
+ window.addEventListener('storage', handler);
998
+ return () => {
999
+ window.removeEventListener('storage', handler);
1000
+ };
1001
+ });
1002
+ }
1003
+ /**
1004
+ * Limpa itens expirados do storage
1005
+ */
1006
+ async cleanExpiredItems(storageType = 'localStorage') {
1007
+ if (!this.isStorageAvailable(storageType)) {
1008
+ return 0;
1009
+ }
1010
+ const keys = this.getAllKeys(storageType);
1011
+ let cleanedCount = 0;
1012
+ for (const key of keys) {
1013
+ try {
1014
+ const item = await this.getItem(key, storageType);
1015
+ if (item && item.expiry) {
1016
+ const now = new Date().getTime();
1017
+ if (now > item.expiry) {
1018
+ this.removeItem(key, storageType);
1019
+ cleanedCount++;
1020
+ }
1021
+ }
1022
+ }
1023
+ catch (error) {
1024
+ // Ignora erros de parsing - pode não ser um item com expiração
1025
+ }
1026
+ }
1027
+ return cleanedCount;
1028
+ }
1029
+ /**
1030
+ * Backup de todos os dados do storage
1031
+ */
1032
+ backup(storageType = 'localStorage') {
1033
+ if (!this.isStorageAvailable(storageType)) {
1034
+ return {};
1035
+ }
1036
+ const storage = this.getStorage(storageType);
1037
+ const backup = {};
1038
+ for (let i = 0; i < storage.length; i++) {
1039
+ const key = storage.key(i);
1040
+ if (key) {
1041
+ backup[key] = storage.getItem(key);
1042
+ }
1043
+ }
1044
+ return backup;
1045
+ }
1046
+ /**
1047
+ * Restaura backup para o storage
1048
+ */
1049
+ restore(backup, storageType = 'localStorage') {
1050
+ if (!this.isStorageAvailable(storageType)) {
1051
+ return false;
1052
+ }
1053
+ // Validação rigorosa do backup
1054
+ if (!backup || typeof backup !== 'object' || Array.isArray(backup)) {
1055
+ console.error('Backup inválido fornecido');
1056
+ return false;
1057
+ }
1058
+ try {
1059
+ const storage = this.getStorage(storageType);
1060
+ const keys = Object.keys(backup);
1061
+ // Limita número de itens para evitar DoS
1062
+ if (keys.length > 100) {
1063
+ console.error('Backup muito grande - máximo 100 itens');
1064
+ return false;
1065
+ }
1066
+ for (const key of keys) {
1067
+ // Validação rigorosa de cada chave
1068
+ if (!this.validateKey(key)) {
1069
+ console.warn(`Chave inválida ignorada no backup: ${key}`);
1070
+ continue;
1071
+ }
1072
+ const value = backup[key];
1073
+ // Validação do valor
1074
+ if (typeof value !== 'string') {
1075
+ console.warn(`Valor inválido ignorado para chave: ${key}`);
1076
+ continue;
1077
+ }
1078
+ // Validação de tamanho
1079
+ if (value.length > 50000) { // 50KB por item
1080
+ console.warn(`Valor muito grande ignorado para chave: ${key}`);
1081
+ continue;
1082
+ }
1083
+ storage.setItem(key, value);
1084
+ }
1085
+ return true;
1086
+ }
1087
+ catch (error) {
1088
+ console.error('Erro ao restaurar backup:', error);
1089
+ return false;
1090
+ }
1091
+ }
1092
+ /**
1093
+ * MÉTODOS PRIVADOS SIMPLIFICADOS
1094
+ */
1095
+ isStorageAvailable(storageType = 'localStorage') {
1096
+ try {
1097
+ const storage = this.getStorage(storageType);
1098
+ const test = '__storage_test__';
1099
+ storage.setItem(test, test);
1100
+ storage.removeItem(test);
1101
+ return true;
1102
+ }
1103
+ catch {
1104
+ return false;
1105
+ }
1106
+ }
1107
+ getStorage(storageType) {
1108
+ return storageType === 'localStorage' ? localStorage : sessionStorage;
1109
+ }
1110
+ sanitizeKey(key) {
1111
+ if (!key || typeof key !== 'string') {
1112
+ return '';
1113
+ }
1114
+ return key
1115
+ .replace(/[^a-zA-Z0-9_.-]/g, '') // Remove caracteres especiais
1116
+ .substring(0, 50); // Limita tamanho
1117
+ }
1118
+ validateKey(key) {
1119
+ if (!key || typeof key !== 'string') {
1120
+ return false;
1121
+ }
1122
+ // Bloqueia chaves perigosas para prototype pollution
1123
+ const dangerousKeys = ['__proto__', 'constructor', 'prototype', 'toString', 'valueOf'];
1124
+ if (dangerousKeys.includes(key)) {
1125
+ return false;
1126
+ }
1127
+ // Validação de tamanho e caracteres
1128
+ if (key.length > 30 || key.length === 0) {
1129
+ return false;
1130
+ }
1131
+ // Apenas alfanumérico e underscore
1132
+ if (!/^[a-zA-Z0-9_]+$/.test(key)) {
1133
+ return false;
1134
+ }
1135
+ return true;
1136
+ }
1137
+ safeJsonStringify(obj) {
1138
+ try {
1139
+ // Verifica se não é função ou undefined
1140
+ if (typeof obj === 'function' || obj === undefined) {
1141
+ return null;
1142
+ }
1143
+ // Limita profundidade para evitar referências circulares
1144
+ const seen = new WeakSet();
1145
+ const replacer = (key, value) => {
1146
+ // Bloqueia chaves perigosas
1147
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
1148
+ return undefined;
1149
+ }
1150
+ // Evita referências circulares
1151
+ if (typeof value === 'object' && value !== null) {
1152
+ if (seen.has(value)) {
1153
+ return '[Circular Reference]';
1154
+ }
1155
+ seen.add(value);
1156
+ }
1157
+ return value;
1158
+ };
1159
+ return JSON.stringify(obj, replacer);
1160
+ }
1161
+ catch (error) {
1162
+ console.error('Erro na serialização segura:', error);
1163
+ return null;
1164
+ }
1165
+ }
1166
+ safeJsonParse(text) {
1167
+ try {
1168
+ return JSON.parse(text, (key, value) => {
1169
+ // Bloqueia prototype pollution
1170
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
1171
+ return undefined;
1172
+ }
1173
+ return value;
1174
+ });
1175
+ }
1176
+ catch (error) {
1177
+ console.error('Erro no parsing seguro:', error);
1178
+ return null;
1179
+ }
1180
+ }
1181
+ async encryptData(text) {
1182
+ try {
1183
+ // Gera chave simples baseada no contexto da aplicação
1184
+ const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(window.location.hostname + '_secure_key'), 'PBKDF2', false, ['deriveKey']);
1185
+ // Gera salt fixo baseado no hostname (para poder descriptografar)
1186
+ const salt = new TextEncoder().encode(window.location.hostname.padEnd(16, '0').substring(0, 16));
1187
+ const key = await crypto.subtle.deriveKey({
1188
+ name: 'PBKDF2',
1189
+ salt: salt,
1190
+ iterations: 10000,
1191
+ hash: 'SHA-256'
1192
+ }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
1193
+ const iv = crypto.getRandomValues(new Uint8Array(12));
1194
+ const encoded = new TextEncoder().encode(text);
1195
+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, encoded);
1196
+ // Combina IV + dados criptografados em base64
1197
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
1198
+ combined.set(iv);
1199
+ combined.set(new Uint8Array(encrypted), iv.length);
1200
+ return btoa(String.fromCharCode(...combined));
1201
+ }
1202
+ catch (error) {
1203
+ console.error('Erro na criptografia:', error);
1204
+ return null;
1205
+ }
1206
+ }
1207
+ async decryptData(encryptedText) {
1208
+ try {
1209
+ // Mesmo processo para gerar a chave
1210
+ const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(window.location.hostname + '_secure_key'), 'PBKDF2', false, ['deriveKey']);
1211
+ const salt = new TextEncoder().encode(window.location.hostname.padEnd(16, '0').substring(0, 16));
1212
+ const key = await crypto.subtle.deriveKey({
1213
+ name: 'PBKDF2',
1214
+ salt: salt,
1215
+ iterations: 10000,
1216
+ hash: 'SHA-256'
1217
+ }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
1218
+ // Decodifica base64 e separa IV dos dados
1219
+ const combined = new Uint8Array(atob(encryptedText).split('').map(c => c.charCodeAt(0)));
1220
+ const iv = combined.slice(0, 12);
1221
+ const encrypted = combined.slice(12);
1222
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, encrypted);
1223
+ return new TextDecoder().decode(decrypted);
1224
+ }
1225
+ catch (error) {
1226
+ console.error('Erro na descriptografia:', error);
1227
+ return null;
1228
+ }
1229
+ }
1230
+ validateStorageEvent(event) {
1231
+ // Validação rigorosa de origem
1232
+ if (!event.url || event.url.trim() === '') {
1233
+ return false;
1234
+ }
1235
+ try {
1236
+ const eventOrigin = new URL(event.url).origin;
1237
+ const currentOrigin = window.location.origin;
1238
+ if (eventOrigin !== currentOrigin) {
1239
+ return false;
1240
+ }
1241
+ }
1242
+ catch {
1243
+ return false;
1244
+ }
1245
+ // Validação rigorosa de chave
1246
+ if (event.key && !this.validateKey(event.key)) {
1247
+ return false;
1248
+ }
1249
+ // Verificação adicional de integridade temporal
1250
+ const now = Date.now();
1251
+ if (!this.lastEventTime) {
1252
+ this.lastEventTime = now;
1253
+ return true;
1254
+ }
1255
+ // Rate limiting básico
1256
+ if (now - this.lastEventTime < 50) { // Máximo 20 eventos/segundo
1257
+ return false;
1258
+ }
1259
+ this.lastEventTime = now;
1260
+ return true;
1261
+ }
1262
+ lastEventTime = 0;
1263
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LocalStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1264
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LocalStorageService, providedIn: 'root' });
1265
+ }
1266
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LocalStorageService, decorators: [{
1267
+ type: Injectable,
1268
+ args: [{
1269
+ providedIn: 'root'
1270
+ }]
1271
+ }] });
1272
+
1273
+ class AuthStorageService {
1274
+ storage;
1275
+ TOKEN_KEY = 'access_token';
1276
+ USER_DATA_KEY = 'user_data';
1277
+ // Observable para monitorar estado de autenticação
1278
+ isAuthenticated$ = new BehaviorSubject(false);
1279
+ get authState$() {
1280
+ return this.isAuthenticated$.asObservable();
1281
+ }
1282
+ constructor(storage) {
1283
+ this.storage = storage;
1284
+ // Inicializa o estado de autenticação de forma assíncrona
1285
+ this.initAuthState();
1286
+ }
1287
+ async initAuthState() {
1288
+ const hasToken = await this.hasToken();
1289
+ this.isAuthenticated$.next(hasToken);
1290
+ }
1291
+ /**
1292
+ * Salva Access Token COM CRIPTOGRAFIA (backend fará todas as validações)
1293
+ */
1294
+ async setToken(token, expiryInMinutes = 60) {
1295
+ if (!token || token.trim().length === 0) {
1296
+ return false;
1297
+ }
1298
+ // 🔐 SEMPRE criptografar tokens para máxima segurança
1299
+ // Criamos estrutura com expiração manualmente para usar criptografia
1300
+ const now = new Date();
1301
+ const item = {
1302
+ value: token,
1303
+ expiry: now.getTime() + (expiryInMinutes * 60 * 1000)
1304
+ };
1305
+ const success = await this.storage.setItem(this.TOKEN_KEY, item, 'sessionStorage', { encrypt: true });
1306
+ if (success) {
1307
+ this.isAuthenticated$.next(true);
1308
+ }
1309
+ return success;
1310
+ }
1311
+ /**
1312
+ * Recupera Access Token COM DESCRIPTOGRAFIA e validação de expiração
1313
+ */
1314
+ async getToken() {
1315
+ try {
1316
+ // 🔓 SEMPRE descriptografar tokens
1317
+ const item = await this.storage.getItem(this.TOKEN_KEY, 'sessionStorage', { encrypt: true });
1318
+ if (!item || !item.value) {
1319
+ this.isAuthenticated$.next(false);
1320
+ return null;
1321
+ }
1322
+ // Verificar expiração
1323
+ const now = Date.now();
1324
+ if (item.expiry && now > item.expiry) {
1325
+ // Token expirado - remover
1326
+ this.storage.removeItem(this.TOKEN_KEY, 'sessionStorage');
1327
+ this.isAuthenticated$.next(false);
1328
+ return null;
1329
+ }
1330
+ return item.value;
1331
+ }
1332
+ catch (error) {
1333
+ console.warn('Erro ao recuperar token criptografado:', error);
1334
+ this.isAuthenticated$.next(false);
1335
+ return null;
1336
+ }
1337
+ }
1338
+ // REMOVIDO: Refresh Token methods - ficam no servidor
1339
+ // O backend gerencia refresh automaticamente via interceptors
1340
+ /**
1341
+ * Salva dados do usuário COM CRIPTOGRAFIA e validação básica
1342
+ */
1343
+ async setUserData(userData) {
1344
+ if (!userData || typeof userData !== 'object') {
1345
+ return false;
1346
+ }
1347
+ // Validação básica de tamanho
1348
+ try {
1349
+ const serialized = JSON.stringify(userData);
1350
+ if (serialized.length > 100000) { // 100KB limite
1351
+ return false;
1352
+ }
1353
+ }
1354
+ catch {
1355
+ return false;
1356
+ }
1357
+ // 🔐 SEMPRE criptografar dados do usuário também
1358
+ return await this.storage.setItem(this.USER_DATA_KEY, userData, 'sessionStorage', { encrypt: true });
1359
+ }
1360
+ /**
1361
+ * Recupera dados do usuário COM DESCRIPTOGRAFIA
1362
+ */
1363
+ async getUserData() {
1364
+ try {
1365
+ // 🔓 SEMPRE descriptografar dados do usuário
1366
+ return await this.storage.getItem(this.USER_DATA_KEY, 'sessionStorage', { encrypt: true });
1367
+ }
1368
+ catch (error) {
1369
+ console.warn('Erro ao recuperar dados do usuário criptografados:', error);
1370
+ return null;
1371
+ }
1372
+ }
1373
+ /**
1374
+ * Verifica apenas se possui token (backend validará se é válido)
1375
+ */
1376
+ async hasToken() {
1377
+ const token = await this.getToken();
1378
+ return !!token && token.length > 0;
1379
+ }
1380
+ /**
1381
+ * Verifica se está "aparentemente" autenticado (token existe)
1382
+ */
1383
+ async isAuthenticated() {
1384
+ return await this.hasToken();
1385
+ }
1386
+ /**
1387
+ * Limpa APENAS dados do cliente (não afeta refresh token no servidor)
1388
+ */
1389
+ clearAuthData() {
1390
+ this.storage.removeItem(this.TOKEN_KEY, 'sessionStorage');
1391
+ this.storage.removeItem(this.USER_DATA_KEY, 'sessionStorage');
1392
+ this.isAuthenticated$.next(false);
1393
+ }
1394
+ /**
1395
+ * Decodifica payload do JWT (SEM validação - apenas para UI)
1396
+ * IMPORTANTE: Use apenas para exibição, nunca para lógica de segurança
1397
+ *
1398
+ * ⚠️ ATENÇÃO: Agora requer token como parâmetro ou uso assíncrono
1399
+ */
1400
+ decodeTokenPayload(token) {
1401
+ try {
1402
+ // Se não foi passado token, não podemos usar getTokenSync (descontinuado)
1403
+ if (!token) {
1404
+ console.warn('⚠️ decodeTokenPayload(): Passe o token como parâmetro ou use ' +
1405
+ 'await getToken() primeiro, pois tokens são criptografados.');
1406
+ return null;
1407
+ }
1408
+ const jwt = token;
1409
+ if (!jwt || typeof jwt !== 'string')
1410
+ return null;
1411
+ // Validação básica do formato JWT
1412
+ const parts = jwt.split('.');
1413
+ if (parts.length !== 3)
1414
+ return null;
1415
+ // Validação base64url
1416
+ const base64UrlPattern = /^[A-Za-z0-9_-]+$/;
1417
+ if (!base64UrlPattern.test(parts[1]))
1418
+ return null;
1419
+ // Validação de tamanho razoável
1420
+ if (parts[1].length > 10000)
1421
+ return null;
1422
+ const payload = parts[1];
1423
+ const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
1424
+ // Parse seguro com proteção contra prototype pollution
1425
+ return JSON.parse(decoded, (key, value) => {
1426
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
1427
+ return undefined;
1428
+ }
1429
+ return value;
1430
+ });
1431
+ }
1432
+ catch (error) {
1433
+ console.warn('Erro ao decodificar token para UI:', error);
1434
+ return null;
1435
+ }
1436
+ }
1437
+ /**
1438
+ * Versão síncrona DESATUALIZADA (tokens agora são criptografados)
1439
+ * ⚠️ AVISO: Esta versão não funciona mais com tokens criptografados
1440
+ * Use getToken() async para acessar tokens de forma segura
1441
+ */
1442
+ getTokenSync() {
1443
+ console.warn('⚠️ getTokenSync() DESCONTINUADO: Tokens agora são criptografados. ' +
1444
+ 'Use getToken() async para descriptografia segura.');
1445
+ // Não é possível descriptografar sincronamente
1446
+ // A criptografia AES-GCM requer operações assíncronas
1447
+ return null;
1448
+ }
1449
+ /**
1450
+ * Verifica se token está próximo de expirar (para UI)
1451
+ * ⚠️ ATENÇÃO: Agora é assíncrono devido à criptografia
1452
+ */
1453
+ async isTokenExpiringSoon(minutesThreshold = 5) {
1454
+ try {
1455
+ const token = await this.getToken();
1456
+ if (!token)
1457
+ return true;
1458
+ const tokenData = this.decodeTokenPayload(token);
1459
+ if (!tokenData || !tokenData.exp)
1460
+ return true;
1461
+ const now = Math.floor(Date.now() / 1000);
1462
+ const timeUntilExpiry = tokenData.exp - now;
1463
+ return timeUntilExpiry < (minutesThreshold * 60);
1464
+ }
1465
+ catch (error) {
1466
+ console.warn('Erro ao verificar expiração do token:', error);
1467
+ return true; // Em caso de erro, assumir que está expirando
1468
+ }
1469
+ }
1470
+ /**
1471
+ * Método utilitário: Obtém e decodifica token em uma operação
1472
+ * Ideal para componentes que precisam dos dados do token para UI
1473
+ */
1474
+ async getDecodedTokenData() {
1475
+ try {
1476
+ const token = await this.getToken();
1477
+ if (!token)
1478
+ return null;
1479
+ return this.decodeTokenPayload(token);
1480
+ }
1481
+ catch (error) {
1482
+ console.warn('Erro ao obter dados decodificados do token:', error);
1483
+ return null;
1484
+ }
1485
+ }
1486
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthStorageService, deps: [{ token: LocalStorageService }], target: i0.ɵɵFactoryTarget.Injectable });
1487
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthStorageService, providedIn: 'root' });
1488
+ }
1489
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthStorageService, decorators: [{
1490
+ type: Injectable,
1491
+ args: [{
1492
+ providedIn: 'root'
1493
+ }]
1494
+ }], ctorParameters: () => [{ type: LocalStorageService }] });
1495
+
1496
+ /**
1497
+ * Token de injeção para o AuthProvider
1498
+ */
1499
+ const AUTH_PROVIDER = new InjectionToken('AuthProvider');
1500
+ class LoginService {
1501
+ authStorage;
1502
+ authProvider;
1503
+ DEFAULT_OPTIONS = {
1504
+ // Configurações visuais
1505
+ title: 'Login',
1506
+ subtitle: 'Entre na sua conta',
1507
+ logoUrl: '',
1508
+ backgroundUrl: '',
1509
+ footerText: '',
1510
+ // Funcionalidades
1511
+ redirectUrl: '/dashboard',
1512
+ showForgotPassword: true,
1513
+ showRegisterLink: true,
1514
+ passwordMinLength: 6,
1515
+ // Links personalizados
1516
+ links: {
1517
+ forgotPassword: '/forgot-password',
1518
+ register: '/register',
1519
+ termsOfService: '/terms',
1520
+ privacyPolicy: '/privacy'
1521
+ },
1522
+ // Personalização visual
1523
+ theme: 'light',
1524
+ primaryColor: '#3b82f6'
1525
+ };
1526
+ // Estado reativo do login
1527
+ loginState$ = new BehaviorSubject({
1528
+ status: LoginStatus.IDLE,
1529
+ user: null,
1530
+ isAuthenticated: false,
1531
+ errors: {},
1532
+ lastAttempt: null
1533
+ });
1534
+ // Configurações atuais
1535
+ currentOptions = { ...this.DEFAULT_OPTIONS };
1536
+ constructor(authStorage, authProvider) {
1537
+ this.authStorage = authStorage;
1538
+ this.authProvider = authProvider;
1539
+ this.initializeService();
1540
+ }
1541
+ /**
1542
+ * Observable do estado completo do login
1543
+ */
1544
+ get state$() {
1545
+ return this.loginState$.asObservable();
1546
+ }
1547
+ /**
1548
+ * Observable apenas do status de loading
1549
+ */
1550
+ get isLoading$() {
1551
+ return this.loginState$.pipe(map(state => state.status === LoginStatus.LOADING));
1552
+ }
1553
+ /**
1554
+ * Observable apenas do status de autenticação
1555
+ */
1556
+ get isAuthenticated$() {
1557
+ return this.loginState$.pipe(map(state => state.isAuthenticated));
1558
+ }
1559
+ /**
1560
+ * Observable apenas dos erros
1561
+ */
1562
+ get errors$() {
1563
+ return this.loginState$.pipe(map(state => state.errors));
1564
+ }
1565
+ /**
1566
+ * Observable do usuário atual
1567
+ */
1568
+ get currentUser$() {
1569
+ return this.loginState$.pipe(map(state => state.user));
1570
+ }
1571
+ /**
1572
+ * Configura opções do serviço de login
1573
+ */
1574
+ configure(options) {
1575
+ this.currentOptions = { ...this.currentOptions, ...options };
1576
+ }
1577
+ /**
1578
+ * Realiza login com as credenciais fornecidas
1579
+ */
1580
+ async login(credentials) {
1581
+ // Valida credenciais básicas
1582
+ const validationErrors = this.validateCredentials(credentials);
1583
+ if (Object.keys(validationErrors).length > 0) {
1584
+ this.updateState({
1585
+ status: LoginStatus.ERROR,
1586
+ errors: validationErrors
1587
+ });
1588
+ return { success: false, message: 'Dados inválidos', errors: Object.values(validationErrors).flat() };
1589
+ }
1590
+ // Inicia processo de login
1591
+ this.updateState({
1592
+ status: LoginStatus.LOADING,
1593
+ errors: {}
1594
+ });
1595
+ try {
1596
+ let response;
1597
+ // if (this.authProvider) {
1598
+ // // Usa provider customizado da aplicação
1599
+ // response = await this.authProvider.login(credentials);
1600
+ // } else {
1601
+ // // Simula resposta (para desenvolvimento/demo)
1602
+ // response = await this.simulateLogin(credentials);
1603
+ // }
1604
+ response = await this.simulateLogin(credentials);
1605
+ if (response.success && response.accessToken && response.user) {
1606
+ // Login bem-sucedido
1607
+ await this.handleSuccessfulLogin(response);
1608
+ this.updateState({
1609
+ status: LoginStatus.SUCCESS,
1610
+ user: response.user,
1611
+ isAuthenticated: true,
1612
+ errors: {}
1613
+ });
1614
+ // Login bem-sucedido
1615
+ return response;
1616
+ }
1617
+ else {
1618
+ // Login falhou
1619
+ await this.handleFailedLogin();
1620
+ this.updateState({
1621
+ status: LoginStatus.ERROR,
1622
+ errors: { general: [response.message || 'Credenciais inválidas'] }
1623
+ });
1624
+ return response;
1625
+ }
1626
+ }
1627
+ catch (error) {
1628
+ console.error('Erro durante login:', error);
1629
+ await this.handleFailedLogin();
1630
+ const errorMessage = error instanceof Error ? error.message : 'Erro interno do servidor';
1631
+ this.updateState({
1632
+ status: LoginStatus.ERROR,
1633
+ errors: { general: [errorMessage] }
1634
+ });
1635
+ return { success: false, message: errorMessage };
1636
+ }
1637
+ }
1638
+ /**
1639
+ * Realiza logout
1640
+ */
1641
+ async logout() {
1642
+ this.updateState({ status: LoginStatus.LOADING });
1643
+ try {
1644
+ let response = { success: true };
1645
+ if (this.authProvider?.logout) {
1646
+ // Notifica o backend
1647
+ response = await this.authProvider.logout();
1648
+ }
1649
+ // Limpa dados locais independente da resposta do backend
1650
+ this.authStorage.clearAuthData();
1651
+ this.updateState({
1652
+ status: LoginStatus.IDLE,
1653
+ user: null,
1654
+ isAuthenticated: false,
1655
+ errors: {},
1656
+ lastAttempt: null
1657
+ });
1658
+ return response;
1659
+ }
1660
+ catch (error) {
1661
+ console.error('Erro durante logout:', error);
1662
+ // Mesmo com erro, limpa dados locais
1663
+ this.authStorage.clearAuthData();
1664
+ this.updateState({
1665
+ status: LoginStatus.IDLE,
1666
+ user: null,
1667
+ isAuthenticated: false,
1668
+ errors: {}
1669
+ });
1670
+ return { success: true }; // Sempre considera logout como sucesso localmente
1671
+ }
1672
+ }
1673
+ /**
1674
+ * Verifica se o usuário atual está autenticado
1675
+ */
1676
+ async checkAuthenticationStatus() {
1677
+ try {
1678
+ const hasToken = await this.authStorage.hasToken();
1679
+ if (!hasToken) {
1680
+ this.updateState({
1681
+ status: LoginStatus.IDLE,
1682
+ isAuthenticated: false,
1683
+ user: null
1684
+ });
1685
+ return false;
1686
+ }
1687
+ // Carrega dados do usuário se autenticado
1688
+ const userData = await this.authStorage.getUserData();
1689
+ if (userData) {
1690
+ this.updateState({
1691
+ status: LoginStatus.SUCCESS,
1692
+ isAuthenticated: true,
1693
+ user: userData
1694
+ });
1695
+ return true;
1696
+ }
1697
+ return false;
1698
+ }
1699
+ catch (error) {
1700
+ console.error('Erro ao verificar autenticação:', error);
1701
+ return false;
1702
+ }
1703
+ }
1704
+ /**
1705
+ * Obtém informações do usuário atual
1706
+ */
1707
+ async getCurrentUser() {
1708
+ return await this.authStorage.getUserData();
1709
+ }
1710
+ /**
1711
+ * Verifica se token está expirando em breve
1712
+ * ⚠️ ATENÇÃO: Agora é assíncrono devido à criptografia
1713
+ */
1714
+ async isTokenExpiringSoon(minutesThreshold = 5) {
1715
+ return await this.authStorage.isTokenExpiringSoon(minutesThreshold);
1716
+ }
1717
+ /**
1718
+ * Limpa erros do estado atual
1719
+ */
1720
+ clearErrors() {
1721
+ this.updateState({ errors: {} });
1722
+ }
1723
+ /**
1724
+ * Reseta estado do login
1725
+ */
1726
+ async resetLoginState() {
1727
+ this.updateState({
1728
+ status: LoginStatus.IDLE,
1729
+ errors: {}
1730
+ });
1731
+ }
1732
+ // ========== MÉTODOS PRIVADOS ==========
1733
+ /**
1734
+ * Inicializa o serviço
1735
+ */
1736
+ async initializeService() {
1737
+ // Inicialização simples sem controle de tentativas
1738
+ // Verifica autenticação atual
1739
+ await this.checkAuthenticationStatus();
1740
+ // Monitora mudanças de autenticação
1741
+ this.authStorage.authState$.subscribe(isAuthenticated => {
1742
+ if (!isAuthenticated && this.loginState$.value.isAuthenticated) {
1743
+ // Usuário perdeu autenticação
1744
+ this.updateState({
1745
+ status: LoginStatus.IDLE,
1746
+ user: null,
1747
+ isAuthenticated: false
1748
+ });
1749
+ }
1750
+ });
1751
+ }
1752
+ /**
1753
+ * Atualiza estado reativo
1754
+ */
1755
+ updateState(updates) {
1756
+ const currentState = this.loginState$.value;
1757
+ this.loginState$.next({
1758
+ ...currentState,
1759
+ ...updates,
1760
+ lastAttempt: updates.status === LoginStatus.LOADING ? new Date() : currentState.lastAttempt
1761
+ });
1762
+ }
1763
+ /**
1764
+ * Valida credenciais do formulário
1765
+ */
1766
+ validateCredentials(credentials) {
1767
+ const errors = {};
1768
+ // Validação de email
1769
+ if (!credentials.email) {
1770
+ errors.email = ['Email é obrigatório'];
1771
+ }
1772
+ else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(credentials.email)) {
1773
+ errors.email = ['Email deve ter um formato válido'];
1774
+ }
1775
+ // Validação de senha
1776
+ if (!credentials.password) {
1777
+ errors.password = ['Senha é obrigatória'];
1778
+ }
1779
+ else if (credentials.password.length < 3) {
1780
+ errors.password = ['Senha deve ter pelo menos 3 caracteres'];
1781
+ }
1782
+ return errors;
1783
+ }
1784
+ /**
1785
+ * Trata login bem-sucedido
1786
+ */
1787
+ async handleSuccessfulLogin(response) {
1788
+ if (!response.accessToken || !response.user) {
1789
+ throw new Error('Resposta de login inválida');
1790
+ }
1791
+ // Salva token
1792
+ const expiryMinutes = response.expiresIn || 60; // Default 1 hora
1793
+ await this.authStorage.setToken(response.accessToken, expiryMinutes);
1794
+ // Salva dados do usuário
1795
+ await this.authStorage.setUserData(response.user);
1796
+ // Credenciais salvas com sucesso
1797
+ }
1798
+ /**
1799
+ * Trata falha de login
1800
+ */
1801
+ async handleFailedLogin() {
1802
+ // Registra falha de login
1803
+ console.warn('Tentativa de login falhou');
1804
+ }
1805
+ /**
1806
+ * Simula resposta de login para desenvolvimento/demo
1807
+ */
1808
+ async simulateLogin(credentials) {
1809
+ // Simula delay da rede
1810
+ await new Promise(resolve => setTimeout(resolve, 1000));
1811
+ // Credenciais de demo
1812
+ const demoUsers = [
1813
+ {
1814
+ email: 'admin@demo.com',
1815
+ password: 'admin',
1816
+ user: {
1817
+ id: '1',
1818
+ nome: 'Administrador',
1819
+ email: 'admin@demo.com',
1820
+ perfil: 'ADMIN',
1821
+ permissoes: ['TODAS'],
1822
+ avatar: 'https://via.placeholder.com/100'
1823
+ }
1824
+ },
1825
+ {
1826
+ email: 'professor@demo.com',
1827
+ password: 'professor',
1828
+ user: {
1829
+ id: '2',
1830
+ nome: 'Professor Demo',
1831
+ email: 'professor@demo.com',
1832
+ perfil: 'PROFESSOR',
1833
+ permissoes: ['CRIAR_PROVA', 'VER_RESULTADOS'],
1834
+ avatar: 'https://via.placeholder.com/100'
1835
+ }
1836
+ },
1837
+ {
1838
+ email: 'aluno@demo.com',
1839
+ password: 'aluno',
1840
+ user: {
1841
+ id: '3',
1842
+ nome: 'Aluno Demo',
1843
+ email: 'aluno@demo.com',
1844
+ perfil: 'ALUNO',
1845
+ permissoes: ['RESPONDER_PROVA'],
1846
+ avatar: 'https://via.placeholder.com/100'
1847
+ }
1848
+ }
1849
+ ];
1850
+ const validUser = demoUsers.find(user => user.email === credentials.email && user.password === credentials.password);
1851
+ if (validUser) {
1852
+ // Gera token fake
1853
+ const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
1854
+ const payload = btoa(JSON.stringify({
1855
+ sub: validUser.user.id,
1856
+ email: validUser.user.email,
1857
+ name: validUser.user.nome,
1858
+ roles: validUser.user.permissoes,
1859
+ iat: Math.floor(Date.now() / 1000),
1860
+ exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hora
1861
+ }));
1862
+ const fakeToken = `${header}.${payload}.fake-signature`;
1863
+ return {
1864
+ success: true,
1865
+ accessToken: fakeToken,
1866
+ user: validUser.user,
1867
+ expiresIn: 60,
1868
+ message: 'Login realizado com sucesso'
1869
+ };
1870
+ }
1871
+ return {
1872
+ success: false,
1873
+ message: 'Email ou senha incorretos'
1874
+ };
1875
+ }
1876
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginService, deps: [{ token: AuthStorageService }, { token: AUTH_PROVIDER, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
1877
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginService, providedIn: 'root' });
1878
+ }
1879
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginService, decorators: [{
1880
+ type: Injectable,
1881
+ args: [{
1882
+ providedIn: 'root'
1883
+ }]
1884
+ }], ctorParameters: () => [{ type: AuthStorageService }, { type: undefined, decorators: [{
1885
+ type: Optional
1886
+ }, {
1887
+ type: Inject,
1888
+ args: [AUTH_PROVIDER]
1889
+ }] }] });
1890
+
1891
+ class LoginComponent {
1892
+ loginService;
1893
+ destroy$ = new Subject();
1894
+ fb = inject(FormBuilder);
1895
+ // Configurações do componente
1896
+ options = {};
1897
+ showHeader = true;
1898
+ showFooter = true;
1899
+ logoUrl;
1900
+ backgroundImageUrl;
1901
+ customCssClass;
1902
+ // Eventos
1903
+ loginSuccess = new EventEmitter();
1904
+ loginError = new EventEmitter();
1905
+ formChange = new EventEmitter();
1906
+ // Estado do componente
1907
+ loginForm;
1908
+ loginState$;
1909
+ isLoading$;
1910
+ errors$;
1911
+ // Estados locais
1912
+ showPassword = false;
1913
+ LoginStatus = LoginStatus; // Para usar no template
1914
+ constructor(loginService) {
1915
+ this.loginService = loginService;
1916
+ this.createForm();
1917
+ this.initializeObservables();
1918
+ }
1919
+ /**
1920
+ * Inicializa os observables
1921
+ */
1922
+ initializeObservables() {
1923
+ this.loginState$ = this.loginService.state$;
1924
+ this.isLoading$ = this.loginService.isLoading$;
1925
+ this.errors$ = this.loginService.errors$;
1926
+ }
1927
+ ngOnInit() {
1928
+ // Configura opções do serviço
1929
+ if (this.options) {
1930
+ this.loginService.configure(this.options);
1931
+ }
1932
+ // Monitora mudanças do estado
1933
+ this.loginState$
1934
+ .pipe(takeUntil(this.destroy$))
1935
+ .subscribe(state => this.handleStateChange(state));
1936
+ // Monitora mudanças do formulário
1937
+ this.loginForm.valueChanges
1938
+ .pipe(takeUntil(this.destroy$))
1939
+ .subscribe(value => {
1940
+ this.formChange.emit(value);
1941
+ });
1942
+ // Limpa erros quando usuário começa a digitar
1943
+ this.loginForm.get('email')?.valueChanges
1944
+ .pipe(takeUntil(this.destroy$))
1945
+ .subscribe(() => this.loginService.clearErrors());
1946
+ this.loginForm.get('password')?.valueChanges
1947
+ .pipe(takeUntil(this.destroy$))
1948
+ .subscribe(() => this.loginService.clearErrors());
1949
+ }
1950
+ ngOnDestroy() {
1951
+ this.destroy$.next();
1952
+ this.destroy$.complete();
1953
+ }
1954
+ /**
1955
+ * Cria o formulário reativo
1956
+ */
1957
+ createForm() {
1958
+ this.loginForm = this.fb.group({
1959
+ email: ['', [
1960
+ Validators.required,
1961
+ Validators.email,
1962
+ Validators.maxLength(255)
1963
+ ]],
1964
+ password: ['', [
1965
+ Validators.required,
1966
+ Validators.minLength(3),
1967
+ Validators.maxLength(100)
1968
+ ]],
1969
+ rememberMe: [false]
1970
+ });
1971
+ }
1972
+ /**
1973
+ * Manipula mudanças de estado do login
1974
+ */
1975
+ handleStateChange(state) {
1976
+ if (state.status === LoginStatus.SUCCESS && state.user) {
1977
+ this.loginSuccess.emit({
1978
+ success: true,
1979
+ user: state.user,
1980
+ accessToken: '', // Não expor token no evento
1981
+ message: 'Login realizado com sucesso'
1982
+ });
1983
+ }
1984
+ if (state.status === LoginStatus.ERROR && state.errors.general) {
1985
+ this.loginError.emit(state.errors.general[0]);
1986
+ }
1987
+ }
1988
+ /**
1989
+ * Submete o formulário de login
1990
+ */
1991
+ async onSubmit() {
1992
+ if (this.loginForm.invalid) {
1993
+ this.markFormGroupTouched();
1994
+ return;
1995
+ }
1996
+ const credentials = this.loginForm.value;
1997
+ try {
1998
+ const response = await this.loginService.login(credentials);
1999
+ if (!response.success) {
2000
+ // Erro já tratado pelo serviço via observable
2001
+ console.warn('Login falhou:', response.message);
2002
+ }
2003
+ }
2004
+ catch (error) {
2005
+ console.error('Erro no componente de login:', error);
2006
+ }
2007
+ }
2008
+ /**
2009
+ * Alterna visibilidade da senha
2010
+ */
2011
+ togglePasswordVisibility() {
2012
+ this.showPassword = !this.showPassword;
2013
+ }
2014
+ /**
2015
+ * Marca todos os campos como tocados para mostrar erros
2016
+ */
2017
+ markFormGroupTouched() {
2018
+ Object.keys(this.loginForm.controls).forEach(key => {
2019
+ const control = this.loginForm.get(key);
2020
+ control?.markAsTouched();
2021
+ });
2022
+ }
2023
+ /**
2024
+ * Verifica se campo tem erro específico
2025
+ */
2026
+ hasFieldError(fieldName, errorType) {
2027
+ const field = this.loginForm.get(fieldName);
2028
+ return !!(field?.hasError(errorType) && (field?.dirty || field?.touched));
2029
+ }
2030
+ /**
2031
+ * Obtém mensagem de erro do campo
2032
+ */
2033
+ getFieldErrorMessage(fieldName) {
2034
+ const field = this.loginForm.get(fieldName);
2035
+ if (!field || !field.errors || (!field.dirty && !field.touched)) {
2036
+ return '';
2037
+ }
2038
+ const errors = field.errors;
2039
+ if (errors['required']) {
2040
+ return `${this.getFieldLabel(fieldName)} é obrigatório`;
2041
+ }
2042
+ if (errors['email']) {
2043
+ return 'Email deve ter um formato válido';
2044
+ }
2045
+ if (errors['minlength']) {
2046
+ return `${this.getFieldLabel(fieldName)} deve ter pelo menos ${errors['minlength'].requiredLength} caracteres`;
2047
+ }
2048
+ if (errors['maxlength']) {
2049
+ return `${this.getFieldLabel(fieldName)} deve ter no máximo ${errors['maxlength'].requiredLength} caracteres`;
2050
+ }
2051
+ return 'Campo inválido';
2052
+ }
2053
+ /**
2054
+ * Obtém label do campo
2055
+ */
2056
+ getFieldLabel(fieldName) {
2057
+ const labels = {
2058
+ email: 'Email',
2059
+ password: 'Senha'
2060
+ };
2061
+ return labels[fieldName] || fieldName;
2062
+ }
2063
+ /**
2064
+ * Reseta o formulário
2065
+ */
2066
+ resetForm() {
2067
+ this.loginForm.reset();
2068
+ this.showPassword = false;
2069
+ this.loginService.clearErrors();
2070
+ }
2071
+ /**
2072
+ * Tenta fazer login com credenciais de demonstração
2073
+ */
2074
+ async loginWithDemo(userType) {
2075
+ const demoCredentials = {
2076
+ admin: {
2077
+ email: 'admin@demo.com',
2078
+ password: 'admin'
2079
+ },
2080
+ professor: {
2081
+ email: 'professor@demo.com',
2082
+ password: 'professor'
2083
+ },
2084
+ aluno: {
2085
+ email: 'aluno@demo.com',
2086
+ password: 'aluno'
2087
+ }
2088
+ };
2089
+ const credentials = demoCredentials[userType];
2090
+ if (credentials) {
2091
+ // Preenche formulário
2092
+ this.loginForm.patchValue(credentials);
2093
+ // Executa login
2094
+ await this.onSubmit();
2095
+ }
2096
+ }
2097
+ /**
2098
+ * Manipula esqueci minha senha
2099
+ */
2100
+ onForgotPassword() {
2101
+ // Implementação específica da aplicação
2102
+ console.log('Forgot password clicked');
2103
+ }
2104
+ /**
2105
+ * Manipula criação de conta
2106
+ */
2107
+ onCreateAccount() {
2108
+ // Implementação específica da aplicação
2109
+ console.log('Create account clicked');
2110
+ }
2111
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginComponent, deps: [{ token: LoginService }], target: i0.ɵɵFactoryTarget.Component });
2112
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.15", type: LoginComponent, isStandalone: true, selector: "lib-login", inputs: { options: "options", showHeader: "showHeader", showFooter: "showFooter", logoUrl: "logoUrl", backgroundImageUrl: "backgroundImageUrl", customCssClass: "customCssClass" }, outputs: { loginSuccess: "loginSuccess", loginError: "loginError", formChange: "formChange" }, ngImport: i0, template: "<!-- Container principal do login -->\n<div class=\"login-container\" [ngClass]=\"customCssClass\"\n [style.background-image]=\"options.backgroundUrl ? 'url(' + options.backgroundUrl + ')' : null\">\n\n <!-- Card de login -->\n <div class=\"login-card\">\n\n <!-- Header -->\n <div class=\"login-header\">\n <img *ngIf=\"options.logoUrl\" [src]=\"options.logoUrl\" alt=\"Logo\" class=\"login-logo\">\n\n <h1 class=\"login-title\">{{ options.title || 'Entrar' }}</h1>\n <p class=\"login-subtitle\">{{ options.subtitle || 'Acesse sua conta' }}</p>\n </div>\n\n <!-- Indicador de loading -->\n <div *ngIf=\"isLoading$ | async\" class=\"loading-overlay\">\n <div class=\"loading-spinner\"></div>\n <span class=\"loading-text\">Entrando...</span>\n </div>\n\n <!-- Estado de bloqueio removido conforme solicita\u00E7\u00E3o -->\n\n <!-- Formul\u00E1rio de login -->\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"onSubmit()\" class=\"login-form\"\n [class.disabled]=\"(isLoading$ | async)\">\n\n <!-- Campo Email -->\n <div class=\"form-group\">\n <label for=\"email\" class=\"form-label\">\n Email <span class=\"required\">*</span>\n </label>\n\n <div class=\"input-wrapper\">\n <input id=\"email\" type=\"email\" formControlName=\"email\" class=\"form-input\"\n [class.error]=\"hasFieldError('email', 'required') || hasFieldError('email', 'email')\"\n placeholder=\"Digite seu email\" autocomplete=\"email\" autocapitalize=\"off\">\n\n <i class=\"input-icon icon-email\"></i>\n </div>\n\n <!-- Erro do campo email -->\n <div *ngIf=\"getFieldErrorMessage('email')\" class=\"field-error\">\n {{ getFieldErrorMessage('email') }}\n </div>\n </div>\n\n <!-- Campo Senha -->\n <div class=\"form-group\">\n <label for=\"password\" class=\"form-label\">\n Senha <span class=\"required\">*</span>\n </label>\n\n <div class=\"input-wrapper\">\n <input id=\"password\" [type]=\"showPassword ? 'text' : 'password'\" formControlName=\"password\"\n class=\"form-input\"\n [class.error]=\"hasFieldError('password', 'required') || hasFieldError('password', 'minlength')\"\n placeholder=\"Digite sua senha\" autocomplete=\"current-password\">\n\n <button type=\"button\" class=\"password-toggle\" (click)=\"togglePasswordVisibility()\"\n [attr.aria-label]=\"showPassword ? 'Ocultar senha' : 'Mostrar senha'\">\n <i [class]=\"showPassword ? 'icon-eye-off' : 'icon-eye'\"></i>\n </button>\n </div>\n\n <!-- Erro do campo senha -->\n <div *ngIf=\"getFieldErrorMessage('password')\" class=\"field-error\">\n {{ getFieldErrorMessage('password') }}\n </div>\n </div>\n\n <!-- Checkbox removido conforme solicita\u00E7\u00E3o -->\n\n <!-- Erros gerais -->\n <div *ngIf=\"(errors$ | async)?.general\" class=\"alert alert-error\">\n <i class=\"icon-error\"></i>\n <div>\n <div *ngFor=\"let error of (errors$ | async)?.general\">\n {{ error }}\n </div>\n </div>\n </div>\n\n <!-- Bot\u00E3o de submit -->\n <button type=\"submit\" class=\"login-button\" [disabled]=\"loginForm.invalid || (isLoading$ | async)\">\n\n <span *ngIf=\"!(isLoading$ | async)\">Entrar</span>\n <span *ngIf=\"isLoading$ | async\" class=\"loading-content\">\n <i class=\"icon-spinner spin\"></i>\n Entrando...\n </span>\n </button>\n\n <!-- Contador de tentativas removido conforme solicita\u00E7\u00E3o -->\n </form>\n\n <!-- Links adicionais -->\n <div class=\"login-links\">\n <a href=\"#\" (click)=\"onForgotPassword(); $event.preventDefault()\" class=\"link\">\n Esqueci minha senha\n </a>\n\n <span class=\"separator\">\u2022</span>\n\n <a href=\"#\" (click)=\"onCreateAccount(); $event.preventDefault()\" class=\"link\">\n Criar conta\n </a>\n </div>\n\n <!-- Se\u00E7\u00E3o de demonstra\u00E7\u00E3o removida conforme solicita\u00E7\u00E3o -->\n\n <!-- Footer -->\n <div *ngIf=\"options.footerText\" class=\"login-footer\">\n <p class=\"footer-text\">\n {{ options.footerText }}\n </p>\n </div>\n </div>\n</div>", styles: ["@charset \"UTF-8\";:host{--login-primary-color: #007bff;--login-primary-hover: #0056b3;--login-secondary-color: #6c757d;--login-background: #f8f9fa;--login-card-background: #ffffff;--login-border-color: #dee2e6;--login-text-color: #343a40;--login-error-color: #dc3545;--login-success-color: #28a745;--login-warning-color: #ffc107;--login-shadow: 0 10px 25px rgba(0, 0, 0, .1);--login-border-radius: .75rem;--login-transition: all .3s ease}.login-container{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:1rem;background:var(--login-background);background-size:cover;background-position:center;background-repeat:no-repeat;position:relative;font-family:DM Sans,sans-serif}.login-container:before{content:\"\";position:absolute;inset:0;background:#0000004d;z-index:1;opacity:0;transition:opacity .3s ease}.login-container[style*=background-image]:before{opacity:1}.login-card{background:var(--login-card-background);border-radius:var(--login-border-radius);box-shadow:var(--login-shadow);padding:2rem;width:100%;max-width:420px;position:relative;border:1px solid var(--login-border-color);z-index:2}@media (max-width: 480px){.login-card{padding:1.5rem;margin:.5rem}}.login-header{text-align:center;margin-bottom:2rem}.login-header .login-logo{max-width:80px;height:auto;margin-bottom:1rem}.login-header .login-title{font-family:DM Sans,sans-serif;font-size:1.75rem;font-weight:700;color:var(--login-text-color);margin:0 0 .5rem;line-height:1.2}.login-header .login-subtitle{font-size:.875rem;color:var(--login-secondary-color);margin:0;font-weight:400}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:var(--login-border-radius);z-index:10}.loading-overlay .loading-spinner{width:40px;height:40px;border:3px solid var(--login-border-color);border-top:3px solid var(--login-primary-color);border-radius:50%;animation:spin 1s linear infinite;margin-bottom:1rem}.loading-overlay .loading-text{color:var(--login-secondary-color);font-size:.9rem;font-weight:500}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-form.disabled{pointer-events:none;opacity:.7}.form-group{margin-bottom:1.5rem}.form-label{display:block;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:600;color:var(--login-text-color);margin-bottom:.5rem;line-height:2.5}.form-label .required{color:var(--login-error-color);margin-left:.25rem}.input-wrapper{position:relative;display:flex;align-items:center}.form-input{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;border:1px solid var(--login-border-color);border-radius:.25rem;display:block;width:100%;padding:.375rem 2.75rem .375rem .75rem;line-height:1.5;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;background-clip:padding-box;color:var(--login-text-color);transition:var(--login-transition);outline:none}.form-input::placeholder{color:#6c757d}.form-input:focus{border-color:#b2b9be;box-shadow:none}.form-input:focus.error{border-color:var(--login-error-color)}.form-input.error{border-color:var(--login-error-color)}.form-input:disabled{background:#f8f9fa;color:#6c757d;cursor:not-allowed}.input-icon{position:absolute;right:1rem;color:var(--login-secondary-color);font-size:1.1rem;pointer-events:none}.password-toggle{position:absolute;right:.75rem;background:none;border:none;padding:.25rem;cursor:pointer;color:var(--login-secondary-color);transition:var(--login-transition);border-radius:4px}.password-toggle:hover{color:var(--login-primary-color);background:#3b82f61a}.password-toggle:focus{outline:none;box-shadow:0 0 0 2px #3b82f64d}.field-error{margin-top:.5rem;font-size:.75rem;color:var(--login-error-color);font-family:DM Sans,sans-serif}.field-error:before{content:\"\\26a0 \";font-size:.75rem}.alert{padding:1rem;border-radius:8px;margin-bottom:1rem;display:flex;align-items:flex-start;gap:.75rem;font-size:.9rem}.alert.alert-error{background:#fef2f2;border:1px solid #fecaca;color:#991b1b}.alert.alert-error i{color:var(--login-error-color);font-size:1.1rem;margin-top:.1rem}.alert.alert-warning{background:#fffbeb;border:1px solid #fed7aa;color:#92400e}.alert.alert-warning i{color:var(--login-warning-color);font-size:1.1rem;margin-top:.1rem}.lockout-message{margin-bottom:1.5rem}.lockout-message strong{display:block;margin-bottom:.5rem}.lockout-message p{margin:.25rem 0}.login-button{width:100%;padding:.375rem .75rem;background:var(--login-primary-color);color:#fff;border:none;border-radius:.25rem;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:600;cursor:pointer;transition:var(--login-transition);position:relative;display:flex;align-items:center;justify-content:center;gap:.5rem;line-height:1.5}.login-button:hover:not(:disabled){background:var(--login-primary-hover)}.login-button:focus{outline:none;box-shadow:0 0 0 .2rem #007bff40}.login-button:disabled{background:#6c757d;cursor:not-allowed}.login-button .loading-content{display:flex;align-items:center;gap:.5rem}.login-button .icon-spinner{animation:spin 1s linear infinite}.login-links{margin-top:1.5rem;text-align:center;display:flex;align-items:center;justify-content:center;gap:.75rem;flex-wrap:wrap}.login-links .link{color:var(--login-primary-color);text-decoration:none;font-size:.875rem;font-weight:500;transition:var(--login-transition)}.login-links .link:hover{color:var(--login-primary-hover);text-decoration:underline}.login-links .link:focus{outline:2px solid var(--login-primary-color);outline-offset:2px;border-radius:4px}.login-links .separator{color:var(--login-secondary-color);font-size:.875rem}.login-footer{margin-top:2rem;padding-top:1.5rem;border-top:1px solid var(--login-border-color);text-align:center}.login-footer .footer-text{font-family:DM Sans,sans-serif;font-size:.75rem;color:var(--login-secondary-color);margin:0;font-weight:400}@media (max-width: 640px){.login-container{padding:.5rem}.login-card{padding:1.5rem}.login-header .login-title{font-size:1.5rem}.demo-buttons{grid-template-columns:1fr}.login-links{flex-direction:column;gap:.5rem}.login-links .separator{display:none}}@media (max-width: 480px){.form-input{padding:.75rem;font-size:.9rem}.login-button{padding:.8rem;font-size:.9rem}}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.login-card{animation:fadeIn .5s ease-out}@media (prefers-color-scheme: dark){:host{--login-background: #0f172a;--login-card-background: #1e293b;--login-border-color: #334155;--login-text-color: #f1f5f9;--login-secondary-color: #94a3b8}}@media (prefers-contrast: high){:host{--login-border-color: #000000;--login-text-color: #000000;--login-background: #ffffff}.form-input:focus{border-width:3px}}@media (prefers-reduced-motion: reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@media print{.login-container{min-height:auto;background:#fff}.demo-section,.loading-overlay{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }] });
2113
+ }
2114
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: LoginComponent, decorators: [{
2115
+ type: Component,
2116
+ args: [{ selector: 'lib-login', standalone: true, imports: [CommonModule, ReactiveFormsModule], template: "<!-- Container principal do login -->\n<div class=\"login-container\" [ngClass]=\"customCssClass\"\n [style.background-image]=\"options.backgroundUrl ? 'url(' + options.backgroundUrl + ')' : null\">\n\n <!-- Card de login -->\n <div class=\"login-card\">\n\n <!-- Header -->\n <div class=\"login-header\">\n <img *ngIf=\"options.logoUrl\" [src]=\"options.logoUrl\" alt=\"Logo\" class=\"login-logo\">\n\n <h1 class=\"login-title\">{{ options.title || 'Entrar' }}</h1>\n <p class=\"login-subtitle\">{{ options.subtitle || 'Acesse sua conta' }}</p>\n </div>\n\n <!-- Indicador de loading -->\n <div *ngIf=\"isLoading$ | async\" class=\"loading-overlay\">\n <div class=\"loading-spinner\"></div>\n <span class=\"loading-text\">Entrando...</span>\n </div>\n\n <!-- Estado de bloqueio removido conforme solicita\u00E7\u00E3o -->\n\n <!-- Formul\u00E1rio de login -->\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"onSubmit()\" class=\"login-form\"\n [class.disabled]=\"(isLoading$ | async)\">\n\n <!-- Campo Email -->\n <div class=\"form-group\">\n <label for=\"email\" class=\"form-label\">\n Email <span class=\"required\">*</span>\n </label>\n\n <div class=\"input-wrapper\">\n <input id=\"email\" type=\"email\" formControlName=\"email\" class=\"form-input\"\n [class.error]=\"hasFieldError('email', 'required') || hasFieldError('email', 'email')\"\n placeholder=\"Digite seu email\" autocomplete=\"email\" autocapitalize=\"off\">\n\n <i class=\"input-icon icon-email\"></i>\n </div>\n\n <!-- Erro do campo email -->\n <div *ngIf=\"getFieldErrorMessage('email')\" class=\"field-error\">\n {{ getFieldErrorMessage('email') }}\n </div>\n </div>\n\n <!-- Campo Senha -->\n <div class=\"form-group\">\n <label for=\"password\" class=\"form-label\">\n Senha <span class=\"required\">*</span>\n </label>\n\n <div class=\"input-wrapper\">\n <input id=\"password\" [type]=\"showPassword ? 'text' : 'password'\" formControlName=\"password\"\n class=\"form-input\"\n [class.error]=\"hasFieldError('password', 'required') || hasFieldError('password', 'minlength')\"\n placeholder=\"Digite sua senha\" autocomplete=\"current-password\">\n\n <button type=\"button\" class=\"password-toggle\" (click)=\"togglePasswordVisibility()\"\n [attr.aria-label]=\"showPassword ? 'Ocultar senha' : 'Mostrar senha'\">\n <i [class]=\"showPassword ? 'icon-eye-off' : 'icon-eye'\"></i>\n </button>\n </div>\n\n <!-- Erro do campo senha -->\n <div *ngIf=\"getFieldErrorMessage('password')\" class=\"field-error\">\n {{ getFieldErrorMessage('password') }}\n </div>\n </div>\n\n <!-- Checkbox removido conforme solicita\u00E7\u00E3o -->\n\n <!-- Erros gerais -->\n <div *ngIf=\"(errors$ | async)?.general\" class=\"alert alert-error\">\n <i class=\"icon-error\"></i>\n <div>\n <div *ngFor=\"let error of (errors$ | async)?.general\">\n {{ error }}\n </div>\n </div>\n </div>\n\n <!-- Bot\u00E3o de submit -->\n <button type=\"submit\" class=\"login-button\" [disabled]=\"loginForm.invalid || (isLoading$ | async)\">\n\n <span *ngIf=\"!(isLoading$ | async)\">Entrar</span>\n <span *ngIf=\"isLoading$ | async\" class=\"loading-content\">\n <i class=\"icon-spinner spin\"></i>\n Entrando...\n </span>\n </button>\n\n <!-- Contador de tentativas removido conforme solicita\u00E7\u00E3o -->\n </form>\n\n <!-- Links adicionais -->\n <div class=\"login-links\">\n <a href=\"#\" (click)=\"onForgotPassword(); $event.preventDefault()\" class=\"link\">\n Esqueci minha senha\n </a>\n\n <span class=\"separator\">\u2022</span>\n\n <a href=\"#\" (click)=\"onCreateAccount(); $event.preventDefault()\" class=\"link\">\n Criar conta\n </a>\n </div>\n\n <!-- Se\u00E7\u00E3o de demonstra\u00E7\u00E3o removida conforme solicita\u00E7\u00E3o -->\n\n <!-- Footer -->\n <div *ngIf=\"options.footerText\" class=\"login-footer\">\n <p class=\"footer-text\">\n {{ options.footerText }}\n </p>\n </div>\n </div>\n</div>", styles: ["@charset \"UTF-8\";:host{--login-primary-color: #007bff;--login-primary-hover: #0056b3;--login-secondary-color: #6c757d;--login-background: #f8f9fa;--login-card-background: #ffffff;--login-border-color: #dee2e6;--login-text-color: #343a40;--login-error-color: #dc3545;--login-success-color: #28a745;--login-warning-color: #ffc107;--login-shadow: 0 10px 25px rgba(0, 0, 0, .1);--login-border-radius: .75rem;--login-transition: all .3s ease}.login-container{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:1rem;background:var(--login-background);background-size:cover;background-position:center;background-repeat:no-repeat;position:relative;font-family:DM Sans,sans-serif}.login-container:before{content:\"\";position:absolute;inset:0;background:#0000004d;z-index:1;opacity:0;transition:opacity .3s ease}.login-container[style*=background-image]:before{opacity:1}.login-card{background:var(--login-card-background);border-radius:var(--login-border-radius);box-shadow:var(--login-shadow);padding:2rem;width:100%;max-width:420px;position:relative;border:1px solid var(--login-border-color);z-index:2}@media (max-width: 480px){.login-card{padding:1.5rem;margin:.5rem}}.login-header{text-align:center;margin-bottom:2rem}.login-header .login-logo{max-width:80px;height:auto;margin-bottom:1rem}.login-header .login-title{font-family:DM Sans,sans-serif;font-size:1.75rem;font-weight:700;color:var(--login-text-color);margin:0 0 .5rem;line-height:1.2}.login-header .login-subtitle{font-size:.875rem;color:var(--login-secondary-color);margin:0;font-weight:400}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:var(--login-border-radius);z-index:10}.loading-overlay .loading-spinner{width:40px;height:40px;border:3px solid var(--login-border-color);border-top:3px solid var(--login-primary-color);border-radius:50%;animation:spin 1s linear infinite;margin-bottom:1rem}.loading-overlay .loading-text{color:var(--login-secondary-color);font-size:.9rem;font-weight:500}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-form.disabled{pointer-events:none;opacity:.7}.form-group{margin-bottom:1.5rem}.form-label{display:block;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:600;color:var(--login-text-color);margin-bottom:.5rem;line-height:2.5}.form-label .required{color:var(--login-error-color);margin-left:.25rem}.input-wrapper{position:relative;display:flex;align-items:center}.form-input{font-family:DM Sans,sans-serif;font-weight:400;font-size:.875rem;border:1px solid var(--login-border-color);border-radius:.25rem;display:block;width:100%;padding:.375rem 2.75rem .375rem .75rem;line-height:1.5;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;background-clip:padding-box;color:var(--login-text-color);transition:var(--login-transition);outline:none}.form-input::placeholder{color:#6c757d}.form-input:focus{border-color:#b2b9be;box-shadow:none}.form-input:focus.error{border-color:var(--login-error-color)}.form-input.error{border-color:var(--login-error-color)}.form-input:disabled{background:#f8f9fa;color:#6c757d;cursor:not-allowed}.input-icon{position:absolute;right:1rem;color:var(--login-secondary-color);font-size:1.1rem;pointer-events:none}.password-toggle{position:absolute;right:.75rem;background:none;border:none;padding:.25rem;cursor:pointer;color:var(--login-secondary-color);transition:var(--login-transition);border-radius:4px}.password-toggle:hover{color:var(--login-primary-color);background:#3b82f61a}.password-toggle:focus{outline:none;box-shadow:0 0 0 2px #3b82f64d}.field-error{margin-top:.5rem;font-size:.75rem;color:var(--login-error-color);font-family:DM Sans,sans-serif}.field-error:before{content:\"\\26a0 \";font-size:.75rem}.alert{padding:1rem;border-radius:8px;margin-bottom:1rem;display:flex;align-items:flex-start;gap:.75rem;font-size:.9rem}.alert.alert-error{background:#fef2f2;border:1px solid #fecaca;color:#991b1b}.alert.alert-error i{color:var(--login-error-color);font-size:1.1rem;margin-top:.1rem}.alert.alert-warning{background:#fffbeb;border:1px solid #fed7aa;color:#92400e}.alert.alert-warning i{color:var(--login-warning-color);font-size:1.1rem;margin-top:.1rem}.lockout-message{margin-bottom:1.5rem}.lockout-message strong{display:block;margin-bottom:.5rem}.lockout-message p{margin:.25rem 0}.login-button{width:100%;padding:.375rem .75rem;background:var(--login-primary-color);color:#fff;border:none;border-radius:.25rem;font-family:DM Sans,sans-serif;font-size:.875rem;font-weight:600;cursor:pointer;transition:var(--login-transition);position:relative;display:flex;align-items:center;justify-content:center;gap:.5rem;line-height:1.5}.login-button:hover:not(:disabled){background:var(--login-primary-hover)}.login-button:focus{outline:none;box-shadow:0 0 0 .2rem #007bff40}.login-button:disabled{background:#6c757d;cursor:not-allowed}.login-button .loading-content{display:flex;align-items:center;gap:.5rem}.login-button .icon-spinner{animation:spin 1s linear infinite}.login-links{margin-top:1.5rem;text-align:center;display:flex;align-items:center;justify-content:center;gap:.75rem;flex-wrap:wrap}.login-links .link{color:var(--login-primary-color);text-decoration:none;font-size:.875rem;font-weight:500;transition:var(--login-transition)}.login-links .link:hover{color:var(--login-primary-hover);text-decoration:underline}.login-links .link:focus{outline:2px solid var(--login-primary-color);outline-offset:2px;border-radius:4px}.login-links .separator{color:var(--login-secondary-color);font-size:.875rem}.login-footer{margin-top:2rem;padding-top:1.5rem;border-top:1px solid var(--login-border-color);text-align:center}.login-footer .footer-text{font-family:DM Sans,sans-serif;font-size:.75rem;color:var(--login-secondary-color);margin:0;font-weight:400}@media (max-width: 640px){.login-container{padding:.5rem}.login-card{padding:1.5rem}.login-header .login-title{font-size:1.5rem}.demo-buttons{grid-template-columns:1fr}.login-links{flex-direction:column;gap:.5rem}.login-links .separator{display:none}}@media (max-width: 480px){.form-input{padding:.75rem;font-size:.9rem}.login-button{padding:.8rem;font-size:.9rem}}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.login-card{animation:fadeIn .5s ease-out}@media (prefers-color-scheme: dark){:host{--login-background: #0f172a;--login-card-background: #1e293b;--login-border-color: #334155;--login-text-color: #f1f5f9;--login-secondary-color: #94a3b8}}@media (prefers-contrast: high){:host{--login-border-color: #000000;--login-text-color: #000000;--login-background: #ffffff}.form-input:focus{border-width:3px}}@media (prefers-reduced-motion: reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@media print{.login-container{min-height:auto;background:#fff}.demo-section,.loading-overlay{display:none}}\n"] }]
2117
+ }], ctorParameters: () => [{ type: LoginService }], propDecorators: { options: [{
2118
+ type: Input
2119
+ }], showHeader: [{
2120
+ type: Input
2121
+ }], showFooter: [{
2122
+ type: Input
2123
+ }], logoUrl: [{
2124
+ type: Input
2125
+ }], backgroundImageUrl: [{
2126
+ type: Input
2127
+ }], customCssClass: [{
2128
+ type: Input
2129
+ }], loginSuccess: [{
2130
+ type: Output
2131
+ }], loginError: [{
2132
+ type: Output
2133
+ }], formChange: [{
2134
+ type: Output
2135
+ }] } });
2136
+
2137
+ /**
2138
+ * Interceptor HTTP para autenticação automática
2139
+ * Adiciona o Access Token nas requisições e trata erros 401
2140
+ */
2141
+ class AuthInterceptor {
2142
+ authStorage;
2143
+ constructor(authStorage) {
2144
+ this.authStorage = authStorage;
2145
+ }
2146
+ intercept(req, next) {
2147
+ // Converte para Observable para trabalhar com async
2148
+ return from(this.authStorage.getToken()).pipe(switchMap(token => {
2149
+ // Clona a requisição e adiciona o token se disponível
2150
+ let authReq = req;
2151
+ if (token) {
2152
+ authReq = req.clone({
2153
+ setHeaders: {
2154
+ Authorization: `Bearer ${token}`
2155
+ }
2156
+ });
2157
+ }
2158
+ // Processa a requisição
2159
+ return next.handle(authReq).pipe(catchError((error) => {
2160
+ // Se receber 401, limpa os dados de autenticação
2161
+ // O backend já fez o refresh automaticamente, se falhou é porque não tem mais sessão válida
2162
+ if (error.status === 401) {
2163
+ this.authStorage.clearAuthData();
2164
+ }
2165
+ // Re-propaga o erro para ser tratado pelos componentes
2166
+ return throwError(() => error);
2167
+ }));
2168
+ }));
2169
+ }
2170
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthInterceptor, deps: [{ token: AuthStorageService }], target: i0.ɵɵFactoryTarget.Injectable });
2171
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthInterceptor });
2172
+ }
2173
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: AuthInterceptor, decorators: [{
2174
+ type: Injectable
2175
+ }], ctorParameters: () => [{ type: AuthStorageService }] });
2176
+
2177
+ // Export dos interceptors para uso público
2178
+
2179
+ /*
2180
+ * Public API Surface of shared-components
2181
+ */
2182
+
2183
+ /**
2184
+ * Generated bundle index. Do not edit.
2185
+ */
2186
+
2187
+ export { AUTH_PROVIDER, AuthInterceptor, AuthStorageService, BreadcrumbComponent, ButtonComponent, CheckboxComponent, DropdownComponent, InputComponent, LocalStorageService, LoginComponent, LoginService, LoginStatus, SharedComponentsComponent, SharedComponentsService, SidebarComponent, TableComponent, ToggleComponent };
2188
+ //# sourceMappingURL=ramonbsales-noah-angular.mjs.map