@up_si_vale/sivale-componentes-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.
- package/README.md +81 -0
- package/ng-package.json +9 -0
- package/package.json +40 -0
- package/src/lib/atoms/button/button.component.html +21 -0
- package/src/lib/atoms/button/button.component.scss +242 -0
- package/src/lib/atoms/button/button.component.ts +43 -0
- package/src/lib/atoms/checkbox/checkbox.component.scss +131 -0
- package/src/lib/atoms/checkbox/checkbox.component.ts +130 -0
- package/src/lib/atoms/date-picker/date-picker.component.html +295 -0
- package/src/lib/atoms/date-picker/date-picker.component.scss +1002 -0
- package/src/lib/atoms/date-picker/date-picker.component.ts +942 -0
- package/src/lib/atoms/input/input.component.ts +239 -0
- package/src/lib/atoms/loader/loader.component.scss +144 -0
- package/src/lib/atoms/loader/loader.component.ts +44 -0
- package/src/lib/atoms/radio/radio.component.scss +115 -0
- package/src/lib/atoms/radio/radio.component.ts +103 -0
- package/src/lib/atoms/textarea/textarea.component.ts +155 -0
- package/src/lib/directives/asset-source.directive.ts +64 -0
- package/src/lib/directives/icon-source.directive.ts +74 -0
- package/src/lib/directives/no-leading-space.directive.ts +18 -0
- package/src/lib/directives/only-letters.directive.ts +21 -0
- package/src/lib/directives/only-number.directive.ts +15 -0
- package/src/lib/directives/rfc.directive.ts +60 -0
- package/src/lib/directives/tooltip.directive.ts +211 -0
- package/src/lib/models/snackbar.models.ts +16 -0
- package/src/lib/molecules/copy-input/copy-input.component.html +29 -0
- package/src/lib/molecules/copy-input/copy-input.component.scss +101 -0
- package/src/lib/molecules/copy-input/copy-input.component.ts +41 -0
- package/src/lib/molecules/multiselect/multiselect.component.scss +298 -0
- package/src/lib/molecules/multiselect/multiselect.component.ts +487 -0
- package/src/lib/molecules/option/option.component.ts +45 -0
- package/src/lib/molecules/phone-input/phone-input.component.scss +78 -0
- package/src/lib/molecules/phone-input/phone-input.component.ts +43 -0
- package/src/lib/molecules/select/select.component.ts +482 -0
- package/src/lib/molecules/snackbar/snackbar.component.scss +201 -0
- package/src/lib/molecules/snackbar/snackbar.component.ts +321 -0
- package/src/lib/services/snackbar.service.ts +103 -0
- package/src/lib/tokens/asset-base-url.token.ts +10 -0
- package/src/public-api.ts +38 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Input,
|
|
4
|
+
forwardRef,
|
|
5
|
+
ViewChild,
|
|
6
|
+
ElementRef,
|
|
7
|
+
OnDestroy,
|
|
8
|
+
HostListener,
|
|
9
|
+
Renderer2,
|
|
10
|
+
ContentChildren,
|
|
11
|
+
ContentChild,
|
|
12
|
+
QueryList,
|
|
13
|
+
AfterContentInit,
|
|
14
|
+
RendererStyleFlags2,
|
|
15
|
+
TemplateRef
|
|
16
|
+
} from '@angular/core';
|
|
17
|
+
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
18
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
|
|
19
|
+
import { Inject } from '@angular/core';
|
|
20
|
+
import { OptionComponent } from '../option/option.component';
|
|
21
|
+
|
|
22
|
+
// Directive markers for prefix/suffix
|
|
23
|
+
import { Directive } from '@angular/core';
|
|
24
|
+
|
|
25
|
+
@Directive({
|
|
26
|
+
selector: '[prefix]',
|
|
27
|
+
standalone: true
|
|
28
|
+
})
|
|
29
|
+
export class SelectPrefixDirective {}
|
|
30
|
+
|
|
31
|
+
@Directive({
|
|
32
|
+
selector: '[suffix]',
|
|
33
|
+
standalone: true
|
|
34
|
+
})
|
|
35
|
+
export class SelectSuffixDirective {}
|
|
36
|
+
|
|
37
|
+
export interface SelectOption {
|
|
38
|
+
value: any;
|
|
39
|
+
label: string;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface DropdownPosition {
|
|
44
|
+
top?: string;
|
|
45
|
+
bottom?: string;
|
|
46
|
+
left: string;
|
|
47
|
+
width: string;
|
|
48
|
+
maxHeight: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Component({
|
|
52
|
+
selector: 'app-select',
|
|
53
|
+
standalone: true,
|
|
54
|
+
imports: [CommonModule, ReactiveFormsModule, FormsModule, SelectPrefixDirective, SelectSuffixDirective],
|
|
55
|
+
template: `
|
|
56
|
+
<div class="select-wrapper" [class.select-wrapper--unstyled]="unstyled" [ngClass]="customClass">
|
|
57
|
+
<label *ngIf="label && !unstyled" [for]="id">{{ label }}</label>
|
|
58
|
+
<div class="select-container" [class.select-container--unstyled]="unstyled">
|
|
59
|
+
<div
|
|
60
|
+
#trigger
|
|
61
|
+
class="select-header"
|
|
62
|
+
[attr.tabindex]="disabled ? -1 : 0"
|
|
63
|
+
[class.error]="hasError && !unstyled"
|
|
64
|
+
[class.disabled]="disabled"
|
|
65
|
+
[class.open]="isOpen"
|
|
66
|
+
[class.select-header--unstyled]="unstyled"
|
|
67
|
+
[class.select-header--has-prefix]="hasPrefix"
|
|
68
|
+
(blur)="handleBlur()"
|
|
69
|
+
(click)="!disabled && toggleDropdown($event)">
|
|
70
|
+
<span class="select-prefix" *ngIf="hasPrefix">
|
|
71
|
+
<ng-content select="[prefix]"></ng-content>
|
|
72
|
+
</span>
|
|
73
|
+
<span class="select-placeholder" *ngIf="!getSelectedLabel()">
|
|
74
|
+
{{ placeholder }}
|
|
75
|
+
</span>
|
|
76
|
+
<span class="select-selected" *ngIf="getSelectedLabel()">
|
|
77
|
+
{{ getSelectedLabel() }}
|
|
78
|
+
</span>
|
|
79
|
+
<span class="select-suffix" *ngIf="hasSuffix">
|
|
80
|
+
<ng-content select="[suffix]"></ng-content>
|
|
81
|
+
</span>
|
|
82
|
+
<svg class="select-arrow" [class.open]="isOpen" width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
83
|
+
<path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
84
|
+
</svg>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<div *ngIf="hasError && errorMessage && !unstyled" class="field-error">
|
|
88
|
+
{{ errorMessage }}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Hidden container for projected options -->
|
|
92
|
+
<div style="display: none;">
|
|
93
|
+
<ng-content></ng-content>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
`,
|
|
97
|
+
providers: [
|
|
98
|
+
{
|
|
99
|
+
provide: NG_VALUE_ACCESSOR,
|
|
100
|
+
useExisting: forwardRef(() => SelectComponent),
|
|
101
|
+
multi: true
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
})
|
|
105
|
+
export class SelectComponent implements ControlValueAccessor, OnDestroy, AfterContentInit {
|
|
106
|
+
@Input() label = '';
|
|
107
|
+
@Input() id = '';
|
|
108
|
+
@Input() placeholder = '';
|
|
109
|
+
@Input() options: SelectOption[] | any[] = [];
|
|
110
|
+
@Input() hasError = false;
|
|
111
|
+
@Input() errorMessage = '';
|
|
112
|
+
@Input() disabled = false;
|
|
113
|
+
@Input() unstyled = false;
|
|
114
|
+
@Input() displayKey: string = 'label'; // Property to display as label
|
|
115
|
+
@Input() valueKey: string = 'value'; // Property to use as value
|
|
116
|
+
@Input() customClass: string = ''; // Custom CSS class for styling
|
|
117
|
+
@ViewChild('trigger', { read: ElementRef }) trigger!: ElementRef;
|
|
118
|
+
@ContentChildren(OptionComponent) contentOptions!: QueryList<OptionComponent>;
|
|
119
|
+
@ContentChild(SelectPrefixDirective) prefixDirective!: SelectPrefixDirective;
|
|
120
|
+
@ContentChild(SelectSuffixDirective) suffixDirective!: SelectSuffixDirective;
|
|
121
|
+
|
|
122
|
+
get hasPrefix(): boolean {
|
|
123
|
+
return !!this.prefixDirective;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get hasSuffix(): boolean {
|
|
127
|
+
return !!this.suffixDirective;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
value: any = '';
|
|
131
|
+
isOpen = false;
|
|
132
|
+
onChange: any = () => {};
|
|
133
|
+
onTouched: any = () => {};
|
|
134
|
+
private _usingContentOptions = false;
|
|
135
|
+
|
|
136
|
+
private dropdownElement: HTMLElement | null = null;
|
|
137
|
+
private backdropElement: HTMLElement | null = null;
|
|
138
|
+
private scrollHandler: (() => void) | null = null;
|
|
139
|
+
private resizeHandler: (() => void) | null = null;
|
|
140
|
+
|
|
141
|
+
constructor(
|
|
142
|
+
private renderer: Renderer2,
|
|
143
|
+
@Inject(DOCUMENT) private document: Document
|
|
144
|
+
) {}
|
|
145
|
+
|
|
146
|
+
ngAfterContentInit(): void {
|
|
147
|
+
// Determine if using content projection or input options
|
|
148
|
+
this._usingContentOptions = this.contentOptions && this.contentOptions.length > 0;
|
|
149
|
+
|
|
150
|
+
// Subscribe to changes in content options
|
|
151
|
+
if (this.contentOptions) {
|
|
152
|
+
this.contentOptions.changes.subscribe(() => {
|
|
153
|
+
this._usingContentOptions = this.contentOptions.length > 0;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
ngOnDestroy(): void {
|
|
159
|
+
this.destroyDropdown();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@HostListener('keydown.escape')
|
|
163
|
+
onEsc() {
|
|
164
|
+
this.closeDropdown();
|
|
165
|
+
this.onTouched();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
handleBlur() {
|
|
169
|
+
this.onTouched();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get all options (from content or input)
|
|
174
|
+
*/
|
|
175
|
+
private getAllOptions(): any[] {
|
|
176
|
+
if (this._usingContentOptions && this.contentOptions) {
|
|
177
|
+
return this.contentOptions.toArray();
|
|
178
|
+
}
|
|
179
|
+
return this.options || [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get display label from option
|
|
184
|
+
*/
|
|
185
|
+
getOptionLabel(option: any): string {
|
|
186
|
+
if (option instanceof OptionComponent) {
|
|
187
|
+
return option.viewValue;
|
|
188
|
+
}
|
|
189
|
+
return option?.[this.displayKey] || '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get value from option
|
|
194
|
+
*/
|
|
195
|
+
getOptionValue(option: any): any {
|
|
196
|
+
if (option instanceof OptionComponent) {
|
|
197
|
+
return option.value;
|
|
198
|
+
}
|
|
199
|
+
return option?.[this.valueKey];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check if option is disabled
|
|
204
|
+
*/
|
|
205
|
+
isOptionDisabled(option: any): boolean {
|
|
206
|
+
if (option instanceof OptionComponent) {
|
|
207
|
+
return option.disabled;
|
|
208
|
+
}
|
|
209
|
+
return option?.disabled || false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@HostListener('document:click', ['$event'])
|
|
213
|
+
onDocumentClick(event: MouseEvent): void {
|
|
214
|
+
// Close dropdown if click is outside
|
|
215
|
+
if (this.isOpen && this.dropdownElement && !this.dropdownElement.contains(event.target as Node)) {
|
|
216
|
+
const triggerElement = this.trigger?.nativeElement;
|
|
217
|
+
if (triggerElement && !triggerElement.contains(event.target as Node)) {
|
|
218
|
+
this.closeDropdown();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
toggleDropdown(event: Event): void {
|
|
224
|
+
event.stopPropagation();
|
|
225
|
+
if (this.disabled) return;
|
|
226
|
+
|
|
227
|
+
if (this.isOpen) {
|
|
228
|
+
this.closeDropdown();
|
|
229
|
+
} else {
|
|
230
|
+
this.openDropdown();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private openDropdown(): void {
|
|
235
|
+
if (!this.trigger) return;
|
|
236
|
+
|
|
237
|
+
// Create backdrop
|
|
238
|
+
this.backdropElement = this.renderer.createElement('div');
|
|
239
|
+
this.renderer.addClass(this.backdropElement, 'select-custom-backdrop');
|
|
240
|
+
this.renderer.listen(this.backdropElement, 'click', () => this.closeDropdown());
|
|
241
|
+
this.renderer.appendChild(this.document.body, this.backdropElement);
|
|
242
|
+
|
|
243
|
+
// Create dropdown
|
|
244
|
+
this.dropdownElement = this.renderer.createElement('div');
|
|
245
|
+
this.renderer.addClass(this.dropdownElement, 'select-custom-dropdown');
|
|
246
|
+
this.renderer.addClass(this.dropdownElement, 'select-overlay-pane');
|
|
247
|
+
|
|
248
|
+
// Calculate position
|
|
249
|
+
const position = this.calculatePosition();
|
|
250
|
+
|
|
251
|
+
// Apply positioning styles
|
|
252
|
+
this.renderer.setStyle(this.dropdownElement, 'position', 'fixed');
|
|
253
|
+
this.renderer.setStyle(this.dropdownElement, 'left', position.left);
|
|
254
|
+
this.renderer.setStyle(this.dropdownElement, 'width', position.width);
|
|
255
|
+
this.renderer.setStyle(this.dropdownElement, 'max-height', position.maxHeight);
|
|
256
|
+
this.renderer.setStyle(this.dropdownElement, 'z-index', '100000', RendererStyleFlags2.Important);
|
|
257
|
+
|
|
258
|
+
if (position.top) {
|
|
259
|
+
this.renderer.setStyle(this.dropdownElement, 'top', position.top);
|
|
260
|
+
} else if (position.bottom) {
|
|
261
|
+
this.renderer.setStyle(this.dropdownElement, 'bottom', position.bottom);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Create dropdown content wrapper
|
|
265
|
+
const dropdownContent = this.renderer.createElement('div');
|
|
266
|
+
this.renderer.addClass(dropdownContent, 'select-dropdown');
|
|
267
|
+
|
|
268
|
+
// Add options
|
|
269
|
+
const allOptions = this.getAllOptions();
|
|
270
|
+
allOptions.forEach(option => {
|
|
271
|
+
const optionElement = this.createOptionElement(option);
|
|
272
|
+
this.renderer.appendChild(dropdownContent, optionElement);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
this.renderer.appendChild(this.dropdownElement, dropdownContent);
|
|
276
|
+
this.renderer.appendChild(this.document.body, this.dropdownElement);
|
|
277
|
+
|
|
278
|
+
this.isOpen = true;
|
|
279
|
+
|
|
280
|
+
// Add scroll and resize listeners to reposition dropdown
|
|
281
|
+
this.addRepositionListeners();
|
|
282
|
+
|
|
283
|
+
// Trigger animation
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
if (this.dropdownElement) {
|
|
286
|
+
this.renderer.addClass(this.dropdownElement, 'select-dropdown-open');
|
|
287
|
+
}
|
|
288
|
+
}, 10);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private calculatePosition(): DropdownPosition {
|
|
292
|
+
const triggerElement = this.trigger.nativeElement;
|
|
293
|
+
const rect = triggerElement.getBoundingClientRect();
|
|
294
|
+
|
|
295
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
296
|
+
const spaceAbove = rect.top;
|
|
297
|
+
const dropdownMaxHeight = 300;
|
|
298
|
+
const offsetY = 4;
|
|
299
|
+
|
|
300
|
+
const shouldOpenAbove = spaceBelow < 200 && spaceAbove > spaceBelow;
|
|
301
|
+
|
|
302
|
+
const position: DropdownPosition = {
|
|
303
|
+
left: `${rect.left}px`,
|
|
304
|
+
width: `${rect.width}px`,
|
|
305
|
+
maxHeight: `${dropdownMaxHeight}px`
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (shouldOpenAbove) {
|
|
309
|
+
position.bottom = `${window.innerHeight - rect.top + offsetY}px`;
|
|
310
|
+
} else {
|
|
311
|
+
position.top = `${rect.bottom + offsetY}px`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return position;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private addRepositionListeners(): void {
|
|
318
|
+
// Create handlers
|
|
319
|
+
const scrollHandler = () => this.updateDropdownPosition();
|
|
320
|
+
const resizeHandler = () => this.updateDropdownPosition();
|
|
321
|
+
|
|
322
|
+
// Add scroll listener to window with capture phase to catch all scroll events
|
|
323
|
+
window.addEventListener('scroll', scrollHandler, true);
|
|
324
|
+
this.scrollHandler = () => window.removeEventListener('scroll', scrollHandler, true);
|
|
325
|
+
|
|
326
|
+
// Add resize listener
|
|
327
|
+
window.addEventListener('resize', resizeHandler);
|
|
328
|
+
this.resizeHandler = () => window.removeEventListener('resize', resizeHandler);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private updateDropdownPosition(): void {
|
|
332
|
+
if (!this.dropdownElement || !this.trigger) return;
|
|
333
|
+
|
|
334
|
+
const position = this.calculatePosition();
|
|
335
|
+
|
|
336
|
+
// Update position styles
|
|
337
|
+
this.renderer.setStyle(this.dropdownElement, 'left', position.left);
|
|
338
|
+
this.renderer.setStyle(this.dropdownElement, 'width', position.width);
|
|
339
|
+
|
|
340
|
+
// Remove old top/bottom styles
|
|
341
|
+
this.renderer.removeStyle(this.dropdownElement, 'top');
|
|
342
|
+
this.renderer.removeStyle(this.dropdownElement, 'bottom');
|
|
343
|
+
|
|
344
|
+
// Apply new top/bottom position
|
|
345
|
+
if (position.top) {
|
|
346
|
+
this.renderer.setStyle(this.dropdownElement, 'top', position.top);
|
|
347
|
+
} else if (position.bottom) {
|
|
348
|
+
this.renderer.setStyle(this.dropdownElement, 'bottom', position.bottom);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private createOptionElement(option: any): HTMLElement {
|
|
353
|
+
const optionElement = this.renderer.createElement('div');
|
|
354
|
+
this.renderer.addClass(optionElement, 'select-option');
|
|
355
|
+
|
|
356
|
+
const optionValue = this.getOptionValue(option);
|
|
357
|
+
const optionLabel = this.getOptionLabel(option);
|
|
358
|
+
const optionDisabled = this.isOptionDisabled(option);
|
|
359
|
+
|
|
360
|
+
if (this.isEmptyValue(optionValue)) {
|
|
361
|
+
this.renderer.addClass(optionElement, 'select-option-placeholder');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (this.isSelected(optionValue)) {
|
|
365
|
+
this.renderer.addClass(optionElement, 'selected');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (optionDisabled) {
|
|
369
|
+
this.renderer.addClass(optionElement, 'disabled');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Option text
|
|
373
|
+
const textSpan = this.renderer.createElement('span');
|
|
374
|
+
this.renderer.addClass(textSpan, 'option-text');
|
|
375
|
+
const text = this.renderer.createText(optionLabel);
|
|
376
|
+
this.renderer.appendChild(textSpan, text);
|
|
377
|
+
this.renderer.appendChild(optionElement, textSpan);
|
|
378
|
+
|
|
379
|
+
// Check icon for selected option
|
|
380
|
+
if (this.isSelected(optionValue)) {
|
|
381
|
+
const svg = this.createCheckIcon();
|
|
382
|
+
this.renderer.appendChild(optionElement, svg);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Click handler
|
|
386
|
+
if (!optionDisabled) {
|
|
387
|
+
this.renderer.listen(optionElement, 'click', (event: Event) => {
|
|
388
|
+
event.stopPropagation();
|
|
389
|
+
this.selectOption(option);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return optionElement;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private createCheckIcon(): SVGElement {
|
|
397
|
+
const svg = this.renderer.createElement('svg', 'svg');
|
|
398
|
+
this.renderer.addClass(svg, 'check-icon');
|
|
399
|
+
this.renderer.setAttribute(svg, 'width', '14');
|
|
400
|
+
this.renderer.setAttribute(svg, 'height', '10');
|
|
401
|
+
this.renderer.setAttribute(svg, 'viewBox', '0 0 10 8');
|
|
402
|
+
this.renderer.setAttribute(svg, 'fill', 'none');
|
|
403
|
+
this.renderer.setAttribute(svg, 'xmlns', 'http://www.w3.org/2000/svg');
|
|
404
|
+
|
|
405
|
+
const path = this.renderer.createElement('path', 'svg');
|
|
406
|
+
this.renderer.setAttribute(path, 'd', 'M9 1L3.5 6.5L1 4');
|
|
407
|
+
this.renderer.setAttribute(path, 'stroke', '#F59100');
|
|
408
|
+
this.renderer.setAttribute(path, 'stroke-width', '1.6666');
|
|
409
|
+
this.renderer.setAttribute(path, 'stroke-linecap', 'round');
|
|
410
|
+
this.renderer.setAttribute(path, 'stroke-linejoin', 'round');
|
|
411
|
+
|
|
412
|
+
this.renderer.appendChild(svg, path);
|
|
413
|
+
return svg;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
closeDropdown(): void {
|
|
417
|
+
this.destroyDropdown();
|
|
418
|
+
this.isOpen = false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private destroyDropdown(): void {
|
|
422
|
+
// Remove listeners
|
|
423
|
+
if (this.scrollHandler) {
|
|
424
|
+
this.scrollHandler();
|
|
425
|
+
this.scrollHandler = null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (this.resizeHandler) {
|
|
429
|
+
this.resizeHandler();
|
|
430
|
+
this.resizeHandler = null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (this.dropdownElement) {
|
|
434
|
+
this.renderer.removeChild(this.document.body, this.dropdownElement);
|
|
435
|
+
this.dropdownElement = null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (this.backdropElement) {
|
|
439
|
+
this.renderer.removeChild(this.document.body, this.backdropElement);
|
|
440
|
+
this.backdropElement = null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
selectOption(option: any): void {
|
|
445
|
+
if (this.isOptionDisabled(option)) return;
|
|
446
|
+
|
|
447
|
+
this.value = this.getOptionValue(option);
|
|
448
|
+
this.onChange(this.value);
|
|
449
|
+
this.onTouched();
|
|
450
|
+
this.closeDropdown();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
isSelected(value: any): boolean {
|
|
454
|
+
return this.value === value;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
isEmptyValue(value: any): boolean {
|
|
458
|
+
return value === '' || value === null || value === undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
getSelectedLabel(): string {
|
|
462
|
+
const allOptions = this.getAllOptions();
|
|
463
|
+
const selectedOption = allOptions.find(opt => this.getOptionValue(opt) === this.value);
|
|
464
|
+
return selectedOption ? this.getOptionLabel(selectedOption) : '';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
writeValue(value: any): void {
|
|
468
|
+
this.value = value || '';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
registerOnChange(fn: any): void {
|
|
472
|
+
this.onChange = fn;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
registerOnTouched(fn: any): void {
|
|
476
|
+
this.onTouched = fn;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
setDisabledState(isDisabled: boolean): void {
|
|
480
|
+
this.disabled = isDisabled;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
.snackbar-container {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 999999;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 12px;
|
|
7
|
+
max-width: 644px;
|
|
8
|
+
pointer-events: none;
|
|
9
|
+
|
|
10
|
+
&--top-right {
|
|
11
|
+
top: 24px;
|
|
12
|
+
right: 24px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
&--top-left {
|
|
16
|
+
top: 24px;
|
|
17
|
+
left: 24px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&--top-center {
|
|
21
|
+
top: 24px;
|
|
22
|
+
left: 50%;
|
|
23
|
+
transform: translateX(-50%);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&--bottom-right {
|
|
27
|
+
bottom: 24px;
|
|
28
|
+
right: 24px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&--bottom-left {
|
|
32
|
+
bottom: 24px;
|
|
33
|
+
left: 24px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&--bottom-center {
|
|
37
|
+
bottom: 24px;
|
|
38
|
+
left: 50%;
|
|
39
|
+
transform: translateX(-50%);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media (max-width: 640px) {
|
|
43
|
+
left: 16px !important;
|
|
44
|
+
right: 16px !important;
|
|
45
|
+
max-width: none;
|
|
46
|
+
transform: none !important;
|
|
47
|
+
|
|
48
|
+
&--top-right,
|
|
49
|
+
&--top-left,
|
|
50
|
+
&--top-center {
|
|
51
|
+
top: 16px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&--bottom-right,
|
|
55
|
+
&--bottom-left,
|
|
56
|
+
&--bottom-center {
|
|
57
|
+
bottom: 16px;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.snackbar {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 12px;
|
|
66
|
+
padding: 16px;
|
|
67
|
+
border-radius: 8px;
|
|
68
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
69
|
+
background: #ffffff;
|
|
70
|
+
pointer-events: auto;
|
|
71
|
+
min-width: 300px;
|
|
72
|
+
max-width: 644px;
|
|
73
|
+
animation: slideIn 300ms ease-out;
|
|
74
|
+
|
|
75
|
+
@keyframes slideIn {
|
|
76
|
+
from {
|
|
77
|
+
transform: translateY(-100%);
|
|
78
|
+
opacity: 0;
|
|
79
|
+
}
|
|
80
|
+
to {
|
|
81
|
+
transform: translateY(0);
|
|
82
|
+
opacity: 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@media (max-width: 640px) {
|
|
87
|
+
min-width: auto;
|
|
88
|
+
max-width: none;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&--success {
|
|
92
|
+
background: #f0fdf4;
|
|
93
|
+
border: 1px solid #22c55e;
|
|
94
|
+
|
|
95
|
+
.snackbar__icon {
|
|
96
|
+
color: #22c55e;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&--error {
|
|
101
|
+
background: #FEF3F2;
|
|
102
|
+
border: 1px solid #F04438;
|
|
103
|
+
|
|
104
|
+
.snackbar__icon {
|
|
105
|
+
color: #F04438;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&--warning {
|
|
110
|
+
background: #fffbeb;
|
|
111
|
+
border: 1px solid #f59e0b;
|
|
112
|
+
|
|
113
|
+
.snackbar__icon {
|
|
114
|
+
color: #f59e0b;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&--info {
|
|
119
|
+
background: #eff6ff;
|
|
120
|
+
border: 1px solid #3b82f6;
|
|
121
|
+
|
|
122
|
+
.snackbar__icon {
|
|
123
|
+
color: #3b82f6;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
&__icon {
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
width: 20px;
|
|
133
|
+
height: 20px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&__content {
|
|
137
|
+
flex: 1;
|
|
138
|
+
min-width: 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
&__message {
|
|
142
|
+
margin: 0;
|
|
143
|
+
line-height: 20px;
|
|
144
|
+
word-wrap: break-word;
|
|
145
|
+
font-family: Quicksand, sans-serif;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
font-size: 14px;
|
|
148
|
+
color: #2E353A;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&__action {
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
padding: 6px 12px;
|
|
154
|
+
border: none;
|
|
155
|
+
border-radius: 4px;
|
|
156
|
+
background: transparent;
|
|
157
|
+
color: inherit;
|
|
158
|
+
font-size: 14px;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
transition: background-color 0.2s;
|
|
162
|
+
|
|
163
|
+
&:hover {
|
|
164
|
+
background: rgba(0, 0, 0, 0.05);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&:active {
|
|
168
|
+
background: rgba(0, 0, 0, 0.1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
&__close {
|
|
173
|
+
flex-shrink: 0;
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: center;
|
|
177
|
+
width: 24px;
|
|
178
|
+
height: 24px;
|
|
179
|
+
padding: 0;
|
|
180
|
+
border: none;
|
|
181
|
+
border-radius: 4px;
|
|
182
|
+
background: transparent;
|
|
183
|
+
color: #2E353A;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
transition: all 0.2s;
|
|
186
|
+
|
|
187
|
+
&:hover {
|
|
188
|
+
background: rgba(0, 0, 0, 0.05);
|
|
189
|
+
color: #1f2937;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
&:active {
|
|
193
|
+
background: rgba(0, 0, 0, 0.1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
svg {
|
|
197
|
+
width: 14px;
|
|
198
|
+
height: 14px;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|