@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.
Files changed (40) hide show
  1. package/README.md +81 -0
  2. package/ng-package.json +9 -0
  3. package/package.json +40 -0
  4. package/src/lib/atoms/button/button.component.html +21 -0
  5. package/src/lib/atoms/button/button.component.scss +242 -0
  6. package/src/lib/atoms/button/button.component.ts +43 -0
  7. package/src/lib/atoms/checkbox/checkbox.component.scss +131 -0
  8. package/src/lib/atoms/checkbox/checkbox.component.ts +130 -0
  9. package/src/lib/atoms/date-picker/date-picker.component.html +295 -0
  10. package/src/lib/atoms/date-picker/date-picker.component.scss +1002 -0
  11. package/src/lib/atoms/date-picker/date-picker.component.ts +942 -0
  12. package/src/lib/atoms/input/input.component.ts +239 -0
  13. package/src/lib/atoms/loader/loader.component.scss +144 -0
  14. package/src/lib/atoms/loader/loader.component.ts +44 -0
  15. package/src/lib/atoms/radio/radio.component.scss +115 -0
  16. package/src/lib/atoms/radio/radio.component.ts +103 -0
  17. package/src/lib/atoms/textarea/textarea.component.ts +155 -0
  18. package/src/lib/directives/asset-source.directive.ts +64 -0
  19. package/src/lib/directives/icon-source.directive.ts +74 -0
  20. package/src/lib/directives/no-leading-space.directive.ts +18 -0
  21. package/src/lib/directives/only-letters.directive.ts +21 -0
  22. package/src/lib/directives/only-number.directive.ts +15 -0
  23. package/src/lib/directives/rfc.directive.ts +60 -0
  24. package/src/lib/directives/tooltip.directive.ts +211 -0
  25. package/src/lib/models/snackbar.models.ts +16 -0
  26. package/src/lib/molecules/copy-input/copy-input.component.html +29 -0
  27. package/src/lib/molecules/copy-input/copy-input.component.scss +101 -0
  28. package/src/lib/molecules/copy-input/copy-input.component.ts +41 -0
  29. package/src/lib/molecules/multiselect/multiselect.component.scss +298 -0
  30. package/src/lib/molecules/multiselect/multiselect.component.ts +487 -0
  31. package/src/lib/molecules/option/option.component.ts +45 -0
  32. package/src/lib/molecules/phone-input/phone-input.component.scss +78 -0
  33. package/src/lib/molecules/phone-input/phone-input.component.ts +43 -0
  34. package/src/lib/molecules/select/select.component.ts +482 -0
  35. package/src/lib/molecules/snackbar/snackbar.component.scss +201 -0
  36. package/src/lib/molecules/snackbar/snackbar.component.ts +321 -0
  37. package/src/lib/services/snackbar.service.ts +103 -0
  38. package/src/lib/tokens/asset-base-url.token.ts +10 -0
  39. package/src/public-api.ts +38 -0
  40. package/tsconfig.json +31 -0
@@ -0,0 +1,487 @@
1
+ import {
2
+ Component,
3
+ Input,
4
+ forwardRef,
5
+ ViewChild,
6
+ ElementRef,
7
+ HostListener,
8
+ ContentChildren,
9
+ QueryList,
10
+ AfterContentInit,
11
+ OnDestroy,
12
+ Renderer2,
13
+ Inject,
14
+ RendererStyleFlags2
15
+ } from '@angular/core';
16
+ import { CommonModule, DOCUMENT } from '@angular/common';
17
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
18
+ import { OptionComponent } from '../option/option.component';
19
+
20
+ export interface MultiselectOption {
21
+ value: any;
22
+ label: string;
23
+ icon?: string;
24
+ disabled?: boolean;
25
+ }
26
+
27
+ interface DropdownPosition {
28
+ top?: string;
29
+ bottom?: string;
30
+ left: string;
31
+ width: string;
32
+ maxHeight: string;
33
+ }
34
+
35
+ @Component({
36
+ selector: 'app-multiselect',
37
+ standalone: true,
38
+ imports: [CommonModule, ReactiveFormsModule, FormsModule],
39
+ template: `
40
+ <div class="multiselect-wrapper" [ngClass]="customClass">
41
+ <label *ngIf="label" [for]="id">{{ label }}</label>
42
+ <div class="multiselect-container">
43
+ <div
44
+ #trigger
45
+ class="multiselect-header"
46
+ (click)="toggleDropdown($event)"
47
+ [class.disabled]="disabled"
48
+ [class.open]="isOpen"
49
+ [class.error]="hasError">
50
+ <span class="multiselect-placeholder" *ngIf="getSelectedCount() === 0">
51
+ {{ placeholder || 'Selecciona uno o más...' }}
52
+ </span>
53
+ <span class="multiselect-selected" *ngIf="getSelectedCount() > 0">
54
+ {{ getSelectedLabels() }}
55
+ </span>
56
+ <svg class="multiselect-arrow" [class.open]="isOpen" width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
57
+ <path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
58
+ </svg>
59
+ </div>
60
+ </div>
61
+ <div *ngIf="hasError && errorMessage" class="field-error">
62
+ {{ errorMessage }}
63
+ </div>
64
+
65
+ <!-- Hidden container for projected options -->
66
+ <div style="display: none;">
67
+ <ng-content></ng-content>
68
+ </div>
69
+ </div>
70
+ `,
71
+ styleUrls: ['./multiselect.component.scss'],
72
+ providers: [
73
+ {
74
+ provide: NG_VALUE_ACCESSOR,
75
+ useExisting: forwardRef(() => MultiselectComponent),
76
+ multi: true
77
+ }
78
+ ]
79
+ })
80
+ export class MultiselectComponent implements ControlValueAccessor, AfterContentInit, OnDestroy {
81
+ @Input() label = '';
82
+ @Input() id = '';
83
+ @Input() placeholder = '';
84
+ @Input() options: MultiselectOption[] | any[] = [];
85
+ @Input() hasError = false;
86
+ @Input() errorMessage = '';
87
+ @Input() disabled = false;
88
+ @Input() displayKey: string = 'label'; // Property to display as label
89
+ @Input() valueKey: string = 'value'; // Property to use as value
90
+ @Input() customClass: string = ''; // Custom CSS class for styling
91
+ @ViewChild('trigger', { read: ElementRef }) trigger!: ElementRef;
92
+ @ContentChildren(OptionComponent) contentOptions!: QueryList<OptionComponent>;
93
+
94
+ value: any[] = [];
95
+ isOpen = false;
96
+ onChange: any = () => {};
97
+ onTouched: any = () => {};
98
+ private _usingContentOptions = false;
99
+
100
+ private dropdownElement: HTMLElement | null = null;
101
+ private backdropElement: HTMLElement | null = null;
102
+ private scrollHandler: (() => void) | null = null;
103
+ private resizeHandler: (() => void) | null = null;
104
+
105
+ constructor(
106
+ private renderer: Renderer2,
107
+ @Inject(DOCUMENT) private document: Document
108
+ ) {}
109
+
110
+ ngAfterContentInit(): void {
111
+ // Determine if using content projection or input options
112
+ this._usingContentOptions = this.contentOptions && this.contentOptions.length > 0;
113
+
114
+ // Subscribe to changes in content options
115
+ if (this.contentOptions) {
116
+ this.contentOptions.changes.subscribe(() => {
117
+ this._usingContentOptions = this.contentOptions.length > 0;
118
+ });
119
+ }
120
+ }
121
+
122
+ ngOnDestroy(): void {
123
+ this.destroyDropdown();
124
+ }
125
+
126
+ /**
127
+ * Get all options (from content or input)
128
+ */
129
+ getAllOptions(): any[] {
130
+ if (this._usingContentOptions && this.contentOptions) {
131
+ return this.contentOptions.toArray();
132
+ }
133
+ return this.options || [];
134
+ }
135
+
136
+ /**
137
+ * Get display label from option
138
+ */
139
+ getOptionLabel(option: any): string {
140
+ if (option instanceof OptionComponent) {
141
+ return option.viewValue;
142
+ }
143
+ return option?.[this.displayKey] || '';
144
+ }
145
+
146
+ /**
147
+ * Get value from option
148
+ */
149
+ getOptionValue(option: any): any {
150
+ if (option instanceof OptionComponent) {
151
+ return option.value;
152
+ }
153
+ return option?.[this.valueKey];
154
+ }
155
+
156
+ /**
157
+ * Check if option is disabled
158
+ */
159
+ isOptionDisabled(option: any): boolean {
160
+ if (option instanceof OptionComponent) {
161
+ return option.disabled;
162
+ }
163
+ return option?.disabled || false;
164
+ }
165
+
166
+ @HostListener('document:click', ['$event'])
167
+ onDocumentClick(event: MouseEvent): void {
168
+ // Close dropdown if click is outside
169
+ if (this.isOpen && this.dropdownElement && !this.dropdownElement.contains(event.target as Node)) {
170
+ const triggerElement = this.trigger?.nativeElement;
171
+ if (triggerElement && !triggerElement.contains(event.target as Node)) {
172
+ this.closeDropdown();
173
+ }
174
+ }
175
+ }
176
+
177
+ toggleDropdown(event: Event): void {
178
+ event.stopPropagation();
179
+ if (this.disabled) return;
180
+
181
+ if (this.isOpen) {
182
+ this.closeDropdown();
183
+ } else {
184
+ this.openDropdown();
185
+ }
186
+ }
187
+
188
+ private openDropdown(): void {
189
+ if (!this.trigger) return;
190
+
191
+ // Create backdrop
192
+ this.backdropElement = this.renderer.createElement('div');
193
+ this.renderer.addClass(this.backdropElement, 'multiselect-custom-backdrop');
194
+ this.renderer.listen(this.backdropElement, 'click', () => this.closeDropdown());
195
+ this.renderer.appendChild(this.document.body, this.backdropElement);
196
+
197
+ // Create dropdown
198
+ this.dropdownElement = this.renderer.createElement('div');
199
+ this.renderer.addClass(this.dropdownElement, 'multiselect-custom-dropdown');
200
+ this.renderer.addClass(this.dropdownElement, 'multiselect-overlay-pane');
201
+
202
+ // Calculate position
203
+ const position = this.calculatePosition();
204
+
205
+ // Apply positioning styles
206
+ this.renderer.setStyle(this.dropdownElement, 'position', 'fixed');
207
+ this.renderer.setStyle(this.dropdownElement, 'left', position.left);
208
+ this.renderer.setStyle(this.dropdownElement, 'width', position.width);
209
+ this.renderer.setStyle(this.dropdownElement, 'max-height', position.maxHeight);
210
+ this.renderer.setStyle(this.dropdownElement, 'z-index', '100000', RendererStyleFlags2.Important);
211
+
212
+ if (position.top) {
213
+ this.renderer.setStyle(this.dropdownElement, 'top', position.top);
214
+ } else if (position.bottom) {
215
+ this.renderer.setStyle(this.dropdownElement, 'bottom', position.bottom);
216
+ }
217
+
218
+ // Create dropdown content wrapper
219
+ const dropdownContent = this.renderer.createElement('div');
220
+ this.renderer.addClass(dropdownContent, 'multiselect-dropdown');
221
+
222
+ // Add options
223
+ const allOptions = this.getAllOptions();
224
+ allOptions.forEach(option => {
225
+ const optionElement = this.createOptionElement(option);
226
+ this.renderer.appendChild(dropdownContent, optionElement);
227
+ });
228
+
229
+ this.renderer.appendChild(this.dropdownElement, dropdownContent);
230
+ this.renderer.appendChild(this.document.body, this.dropdownElement);
231
+
232
+ this.isOpen = true;
233
+
234
+ // Add scroll and resize listeners to reposition dropdown
235
+ this.addRepositionListeners();
236
+
237
+ // Trigger animation
238
+ setTimeout(() => {
239
+ if (this.dropdownElement) {
240
+ this.renderer.addClass(this.dropdownElement, 'multiselect-dropdown-open');
241
+ }
242
+ }, 10);
243
+ }
244
+
245
+ private calculatePosition(): DropdownPosition {
246
+ const triggerElement = this.trigger.nativeElement;
247
+ const rect = triggerElement.getBoundingClientRect();
248
+
249
+ const spaceBelow = window.innerHeight - rect.bottom;
250
+ const spaceAbove = rect.top;
251
+ const dropdownMaxHeight = 300;
252
+ const offsetY = 4;
253
+
254
+ const shouldOpenAbove = spaceBelow < 200 && spaceAbove > spaceBelow;
255
+
256
+ const position: DropdownPosition = {
257
+ left: `${rect.left}px`,
258
+ width: `${rect.width}px`,
259
+ maxHeight: `${dropdownMaxHeight}px`
260
+ };
261
+
262
+ if (shouldOpenAbove) {
263
+ position.bottom = `${window.innerHeight - rect.top + offsetY}px`;
264
+ } else {
265
+ position.top = `${rect.bottom + offsetY}px`;
266
+ }
267
+
268
+ return position;
269
+ }
270
+
271
+ private addRepositionListeners(): void {
272
+ // Create handlers
273
+ const scrollHandler = () => this.updateDropdownPosition();
274
+ const resizeHandler = () => this.updateDropdownPosition();
275
+
276
+ // Add scroll listener to window with capture phase to catch all scroll events
277
+ window.addEventListener('scroll', scrollHandler, true);
278
+ this.scrollHandler = () => window.removeEventListener('scroll', scrollHandler, true);
279
+
280
+ // Add resize listener
281
+ window.addEventListener('resize', resizeHandler);
282
+ this.resizeHandler = () => window.removeEventListener('resize', resizeHandler);
283
+ }
284
+
285
+ private updateDropdownPosition(): void {
286
+ if (!this.dropdownElement || !this.trigger) return;
287
+
288
+ const position = this.calculatePosition();
289
+
290
+ // Update position styles
291
+ this.renderer.setStyle(this.dropdownElement, 'left', position.left);
292
+ this.renderer.setStyle(this.dropdownElement, 'width', position.width);
293
+
294
+ // Remove old top/bottom styles
295
+ this.renderer.removeStyle(this.dropdownElement, 'top');
296
+ this.renderer.removeStyle(this.dropdownElement, 'bottom');
297
+
298
+ // Apply new top/bottom position
299
+ if (position.top) {
300
+ this.renderer.setStyle(this.dropdownElement, 'top', position.top);
301
+ } else if (position.bottom) {
302
+ this.renderer.setStyle(this.dropdownElement, 'bottom', position.bottom);
303
+ }
304
+ }
305
+
306
+ private createOptionElement(option: any): HTMLElement {
307
+ const labelElement = this.renderer.createElement('label');
308
+ this.renderer.addClass(labelElement, 'multiselect-option');
309
+
310
+ const optionValue = this.getOptionValue(option);
311
+ const optionLabel = this.getOptionLabel(option);
312
+ const optionDisabled = this.isOptionDisabled(option);
313
+
314
+ if (this.isSelected(optionValue)) {
315
+ this.renderer.addClass(labelElement, 'selected');
316
+ }
317
+
318
+ if (optionDisabled) {
319
+ this.renderer.addClass(labelElement, 'disabled');
320
+ }
321
+
322
+ // Stop propagation on label click
323
+ this.renderer.listen(labelElement, 'click', (event: Event) => {
324
+ event.stopPropagation();
325
+ });
326
+
327
+ // Checkbox container
328
+ const checkboxDiv = this.renderer.createElement('div');
329
+ this.renderer.addClass(checkboxDiv, 'option-checkbox');
330
+
331
+ // Hidden native checkbox
332
+ const checkbox = this.renderer.createElement('input');
333
+ this.renderer.setAttribute(checkbox, 'type', 'checkbox');
334
+ this.renderer.setProperty(checkbox, 'checked', this.isSelected(optionValue));
335
+ if (optionDisabled) {
336
+ this.renderer.setProperty(checkbox, 'disabled', true);
337
+ }
338
+
339
+ // Custom checkbox span
340
+ const customCheckbox = this.renderer.createElement('span');
341
+ this.renderer.addClass(customCheckbox, 'checkbox-custom');
342
+ if (this.isSelected(optionValue)) {
343
+ this.renderer.addClass(customCheckbox, 'checked');
344
+ }
345
+
346
+ // Check icon
347
+ if (this.isSelected(optionValue)) {
348
+ const svg = this.createCheckIcon();
349
+ this.renderer.appendChild(customCheckbox, svg);
350
+ }
351
+
352
+ // Listen to change event
353
+ this.renderer.listen(checkbox, 'change', (event: Event) => {
354
+ event.stopPropagation();
355
+
356
+ // Toggle the value
357
+ this.toggleOption(option);
358
+
359
+ // Update visual state
360
+ const isNowSelected = this.isSelected(optionValue);
361
+ this.renderer.setProperty(checkbox, 'checked', isNowSelected);
362
+
363
+ if (isNowSelected) {
364
+ this.renderer.addClass(labelElement, 'selected');
365
+ this.renderer.addClass(customCheckbox, 'checked');
366
+ // Add check icon if not present
367
+ if (!customCheckbox.querySelector('.check-icon')) {
368
+ const svg = this.createCheckIcon();
369
+ this.renderer.appendChild(customCheckbox, svg);
370
+ }
371
+ } else {
372
+ this.renderer.removeClass(labelElement, 'selected');
373
+ this.renderer.removeClass(customCheckbox, 'checked');
374
+ // Remove check icon
375
+ const existingIcon = customCheckbox.querySelector('.check-icon');
376
+ if (existingIcon) {
377
+ this.renderer.removeChild(customCheckbox, existingIcon);
378
+ }
379
+ }
380
+ });
381
+
382
+ this.renderer.appendChild(checkboxDiv, checkbox);
383
+ this.renderer.appendChild(checkboxDiv, customCheckbox);
384
+ this.renderer.appendChild(labelElement, checkboxDiv);
385
+
386
+ // Option text
387
+ const textSpan = this.renderer.createElement('span');
388
+ this.renderer.addClass(textSpan, 'option-text');
389
+ const text = this.renderer.createText(optionLabel);
390
+ this.renderer.appendChild(textSpan, text);
391
+ this.renderer.appendChild(labelElement, textSpan);
392
+
393
+ return labelElement;
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
+ this.onTouched();
420
+ }
421
+
422
+ private destroyDropdown(): void {
423
+ // Remove listeners
424
+ if (this.scrollHandler) {
425
+ this.scrollHandler();
426
+ this.scrollHandler = null;
427
+ }
428
+
429
+ if (this.resizeHandler) {
430
+ this.resizeHandler();
431
+ this.resizeHandler = null;
432
+ }
433
+
434
+ if (this.dropdownElement) {
435
+ this.renderer.removeChild(this.document.body, this.dropdownElement);
436
+ this.dropdownElement = null;
437
+ }
438
+
439
+ if (this.backdropElement) {
440
+ this.renderer.removeChild(this.document.body, this.backdropElement);
441
+ this.backdropElement = null;
442
+ }
443
+ }
444
+
445
+ toggleOption(option: any): void {
446
+ if (this.isOptionDisabled(option)) return;
447
+
448
+ const optionValue = this.getOptionValue(option);
449
+ const index = this.value.indexOf(optionValue);
450
+ if (index > -1) {
451
+ this.value = this.value.filter(v => v !== optionValue);
452
+ } else {
453
+ this.value = [...this.value, optionValue];
454
+ }
455
+ this.onChange(this.value);
456
+ }
457
+
458
+ isSelected(value: any): boolean {
459
+ return this.value.includes(value);
460
+ }
461
+
462
+ getSelectedCount(): number {
463
+ return this.value.length;
464
+ }
465
+
466
+ getSelectedLabels(): string {
467
+ const allOptions = this.getAllOptions();
468
+ const selectedOptions = allOptions.filter(opt => this.isSelected(this.getOptionValue(opt)));
469
+ return selectedOptions.map(opt => this.getOptionLabel(opt)).join(', ');
470
+ }
471
+
472
+ writeValue(value: any): void {
473
+ this.value = Array.isArray(value) ? value : [];
474
+ }
475
+
476
+ registerOnChange(fn: any): void {
477
+ this.onChange = fn;
478
+ }
479
+
480
+ registerOnTouched(fn: any): void {
481
+ this.onTouched = fn;
482
+ }
483
+
484
+ setDisabledState(isDisabled: boolean): void {
485
+ this.disabled = isDisabled;
486
+ }
487
+ }
@@ -0,0 +1,45 @@
1
+ import { Component, Input, HostBinding, ChangeDetectionStrategy, ElementRef } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ @Component({
5
+ selector: 'app-option',
6
+ standalone: true,
7
+ imports: [CommonModule],
8
+ template: `<ng-content></ng-content>`,
9
+ changeDetection: ChangeDetectionStrategy.OnPush,
10
+ host: {
11
+ 'class': 'app-option',
12
+ '[class.disabled]': 'disabled',
13
+ '[class.selected]': 'selected'
14
+ }
15
+ })
16
+ export class OptionComponent {
17
+ @Input() value: any;
18
+ @Input() disabled: boolean = false;
19
+
20
+ @HostBinding('class.selected')
21
+ selected: boolean = false;
22
+
23
+ @HostBinding('class.app-option')
24
+ readonly optionClass = true;
25
+
26
+ // Internal property to store text content
27
+ private _viewValue: string = '';
28
+
29
+ get viewValue(): string {
30
+ return this._viewValue || this.getTextContent();
31
+ }
32
+
33
+ set viewValue(value: string) {
34
+ this._viewValue = value;
35
+ }
36
+
37
+ constructor(private elementRef: ElementRef) {}
38
+
39
+ /**
40
+ * Get the text content from the element
41
+ */
42
+ getTextContent(): string {
43
+ return this.elementRef.nativeElement.textContent?.trim() || '';
44
+ }
45
+ }
@@ -0,0 +1,78 @@
1
+ .svu-phone-input {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ margin-bottom: 6px;
6
+ padding: 0 4px;
7
+
8
+ &__label {
9
+ display: block;
10
+ margin-bottom: 0.5rem;
11
+ font-family: 'Quicksand', sans-serif;
12
+ font-weight: 500;
13
+ font-size: 14px;
14
+ color: #585D61;
15
+ }
16
+
17
+ &__field {
18
+ position: relative;
19
+ display: flex;
20
+ align-items: stretch;
21
+ min-height: 44px;
22
+ height: 44px;
23
+ border: 1px solid #EAECF5;
24
+ box-shadow: 0 1px 2px 0 #A1AFEC66;
25
+ border-radius: 8px;
26
+ transition: all 0.2s ease;
27
+ background: white;
28
+
29
+ &:hover {
30
+ border-color: #FBD399;
31
+ }
32
+
33
+ &:focus-within {
34
+ border-color: #FBD399;
35
+ box-shadow: 0 0 0 4px #FFE8C8, 0 1px 2px 0 #0A0D120D;
36
+ }
37
+ }
38
+
39
+ &__country {
40
+ flex: 0;
41
+ width: 120px;
42
+ display: flex;
43
+ align-items: stretch;
44
+ min-width: 64px;
45
+ }
46
+
47
+ &__number {
48
+ flex: 1;
49
+ min-width: 0;
50
+ display: flex;
51
+ align-items: stretch;
52
+ }
53
+
54
+ // Error state
55
+ &.svu-phone-input--error &__field {
56
+ border-color: #DC3545 !important;
57
+
58
+ &:focus-within {
59
+ box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1) !important;
60
+ }
61
+ }
62
+
63
+ &__error {
64
+ color: #F04438;
65
+ font-size: 0.875rem;
66
+ margin-top: 0.25rem;
67
+ font-family: 'Quicksand', sans-serif;
68
+ }
69
+ }
70
+
71
+ // Responsive adjustments
72
+ @media (max-width: 640px) {
73
+ .svu-phone-input {
74
+ &__country {
75
+ width: 100px;
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,43 @@
1
+ import { Component, Input } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { InputComponent } from '../../atoms/input/input.component';
4
+ import { SelectComponent, SelectOption } from '../select/select.component';
5
+
6
+ export interface PhoneValue {
7
+ countryCode: string;
8
+ number: string;
9
+ }
10
+
11
+ @Component({
12
+ selector: 'app-phone-input',
13
+ standalone: true,
14
+ imports: [CommonModule, InputComponent, SelectComponent],
15
+ template: `
16
+ <div class="svu-phone-input" [class.svu-phone-input--error]="hasError">
17
+ <label *ngIf="label" class="svu-phone-input__label">
18
+ {{ label }}
19
+ </label>
20
+
21
+ <div class="svu-phone-input__field">
22
+ <div class="svu-phone-input__country">
23
+ <ng-content select="[countrySelect]"></ng-content>
24
+ </div>
25
+
26
+ <div class="svu-phone-input__number">
27
+ <ng-content select="[phoneInput]"></ng-content>
28
+ </div>
29
+ </div>
30
+
31
+ <span *ngIf="hasError && errorMessage" class="svu-phone-input__error">
32
+ {{ errorMessage }}
33
+ </span>
34
+ </div>
35
+ `,
36
+ styleUrls: ['./phone-input.component.scss']
37
+ })
38
+ export class PhoneInputComponent {
39
+ @Input() label: string = 'Teléfono';
40
+ @Input() hasError: boolean = false;
41
+ @Input() errorMessage: string = '';
42
+ }
43
+