@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,239 @@
1
+ import { Component, Input, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
4
+ import { IconSourceDirective } from '../../directives/icon-source.directive';
5
+ import { NoLeadingSpaceDirective } from '../../directives/no-leading-space.directive';
6
+
7
+ type InputFormat = 'alphanumeric' | 'currency' | 'numeric';
8
+
9
+ @Component({
10
+ selector: 'app-input',
11
+ standalone: true,
12
+ imports: [CommonModule, ReactiveFormsModule, FormsModule, IconSourceDirective, NoLeadingSpaceDirective],
13
+ changeDetection: ChangeDetectionStrategy.OnPush,
14
+ template: `
15
+ <div class="input-wrapper" [class.input-wrapper--unstyled]="unstyled">
16
+ <label *ngIf="label && !unstyled" [for]="id">{{ label }}</label>
17
+ <div class="input-container" [class.error]="hasError && !unstyled" [class.input-container--unstyled]="unstyled">
18
+ <div class="input-prefix-wrapper">
19
+ <ng-content select="[prefix]"></ng-content>
20
+ </div>
21
+ <input
22
+ noLeadingSpace
23
+ [type]="type"
24
+ [id]="id"
25
+ [placeholder]="placeholder"
26
+ [attr.maxlength]="format === 'currency' ? null : maxLength"
27
+ [disabled]="disabled"
28
+ [readonly]="readonly"
29
+ class="input-field"
30
+ [class.error]="hasError && !unstyled"
31
+ [class.input-field--unstyled]="unstyled"
32
+ [value]="formattedValue"
33
+ (blur)="onBlur()"
34
+ (input)="onInputChange($event)">
35
+ <div class="input-suffix-wrapper">
36
+ <ng-content select="[suffix]"></ng-content>
37
+ </div>
38
+ </div>
39
+ <div *ngIf="hasError && errorMessage && !unstyled" class="field-error">
40
+ {{ errorMessage }}
41
+ </div>
42
+ </div>
43
+ `,
44
+ styles: [`
45
+ /* Solución para el sombreado azul del autocompletado del navegador */
46
+ input:-webkit-autofill,
47
+ input:-webkit-autofill:hover,
48
+ input:-webkit-autofill:focus,
49
+ input:-webkit-autofill:active {
50
+ -webkit-box-shadow: 0 0 0 30px white inset !important;
51
+ box-shadow: 0 0 0 30px white inset !important;
52
+ -webkit-text-fill-color: #2E353A !important;
53
+ transition: background-color 5000s ease-in-out 0s;
54
+ }
55
+
56
+ /* Wrappers para prefix y suffix con soporte completo de eventos */
57
+ .input-prefix-wrapper,
58
+ .input-suffix-wrapper {
59
+ flex-shrink: 0;
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 0.5rem;
63
+ font-family: 'Quicksand', sans-serif;
64
+ font-size: 14px;
65
+ font-weight: 500;
66
+ color: #969A9C;
67
+ position: relative;
68
+ z-index: 2;
69
+ pointer-events: auto !important;
70
+ }
71
+
72
+ .input-prefix-wrapper {
73
+ margin-right: 0.5rem;
74
+
75
+ &:empty {
76
+ display: none;
77
+ margin-right: 0;
78
+ }
79
+ }
80
+
81
+ .input-suffix-wrapper {
82
+ margin-left: 0.5rem;
83
+
84
+ &:empty {
85
+ display: none;
86
+ margin-left: 0;
87
+ }
88
+ }
89
+
90
+ /* Asegurar que todos los elementos dentro de prefix/suffix sean clickeables */
91
+ .input-prefix-wrapper *,
92
+ .input-suffix-wrapper * {
93
+ pointer-events: auto !important;
94
+ }
95
+
96
+ /* El input no debe cubrir los wrappers */
97
+ .input-field {
98
+ position: relative;
99
+ z-index: 1;
100
+ }
101
+ `],
102
+ providers: [
103
+ {
104
+ provide: NG_VALUE_ACCESSOR,
105
+ useExisting: forwardRef(() => InputComponent),
106
+ multi: true
107
+ }
108
+ ]
109
+ })
110
+ export class InputComponent implements ControlValueAccessor {
111
+ @Input() label = '';
112
+ @Input() id = '';
113
+ @Input() placeholder = '';
114
+ @Input() type: 'text' | 'email' | 'tel' | 'password' | 'number' = 'text';
115
+ @Input() maxLength?: number | null;
116
+ @Input() hasError = false;
117
+ @Input() errorMessage = '';
118
+ @Input() disabled = false;
119
+ @Input() readonly = false;
120
+ @Input() format: InputFormat = 'alphanumeric';
121
+ @Input() unstyled = false; // Para usar dentro de componentes compuestos
122
+
123
+ value: any = '';
124
+ formattedValue = '';
125
+ onChange: any = () => {};
126
+ onTouched: any = () => {};
127
+
128
+ constructor(private cdr: ChangeDetectorRef) {}
129
+
130
+ writeValue(value: any): void {
131
+ let processedValue = value || '';
132
+
133
+ // For currency format, enforce maxLength on the raw value (without commas)
134
+ if (this.format === 'currency' && this.maxLength) {
135
+ const rawValue = String(processedValue).replace(/[^0-9.]/g, '');
136
+ if (rawValue.length > this.maxLength) {
137
+ processedValue = rawValue.slice(0, this.maxLength);
138
+ }
139
+ }
140
+
141
+ // For number inputs, enforce maxLength on initial value
142
+ if (this.type === 'number' && this.maxLength && String(processedValue).length > this.maxLength) {
143
+ processedValue = String(processedValue).slice(0, this.maxLength);
144
+ }
145
+
146
+ this.value = processedValue;
147
+ this.formattedValue = this.applyFormat(this.value);
148
+ this.cdr.markForCheck();
149
+ }
150
+
151
+ registerOnChange(fn: any): void {
152
+ this.onChange = fn;
153
+ }
154
+
155
+ registerOnTouched(fn: any): void {
156
+ this.onTouched = fn;
157
+ }
158
+
159
+ setDisabledState(isDisabled: boolean): void {
160
+ this.disabled = isDisabled;
161
+ this.cdr.markForCheck();
162
+ }
163
+
164
+ onInputChange(event: Event): void {
165
+ const target = event.target as HTMLInputElement;
166
+ let inputValue = target.value;
167
+
168
+ // Parse the value first (removes formatting like commas)
169
+ let rawValue = this.parseValue(inputValue);
170
+
171
+ // For currency format, enforce maxLength on the raw value (without commas)
172
+ if (this.format === 'currency' && this.maxLength && String(rawValue).length > this.maxLength) {
173
+ rawValue = String(rawValue).slice(0, this.maxLength);
174
+ }
175
+
176
+ // For number inputs or numeric format, maxLength doesn't work natively, so we handle it manually
177
+ if ((this.type === 'number' || this.format === 'numeric') && this.maxLength) {
178
+ // Check the length of the raw value (without formatting)
179
+ const rawString = String(rawValue);
180
+ if (rawString.length > this.maxLength) {
181
+ rawValue = rawString.slice(0, this.maxLength);
182
+ // Update the input value to reflect the truncation
183
+ target.value = rawValue;
184
+ }
185
+ }
186
+
187
+ this.value = rawValue;
188
+ this.formattedValue = this.applyFormat(rawValue);
189
+
190
+ // Update the input display (for formatted inputs like currency)
191
+ if (this.format === 'currency') {
192
+ target.value = this.formattedValue;
193
+ }
194
+
195
+ this.onChange(rawValue);
196
+ }
197
+
198
+ onBlur(): void {
199
+ this.onTouched();
200
+ }
201
+
202
+ private parseValue(value: string): any {
203
+ switch (this.format) {
204
+ case 'currency':
205
+ // Remove all non-numeric characters except decimal point
206
+ return value.replace(/[^0-9.]/g, '');
207
+ case 'numeric':
208
+ // Remove all non-numeric characters (only digits allowed)
209
+ return value.replace(/[^0-9]/g, '');
210
+ case 'alphanumeric':
211
+ default:
212
+ return value;
213
+ }
214
+ }
215
+
216
+ private applyFormat(value: any): string {
217
+ if (!value && value !== 0) return '';
218
+
219
+ switch (this.format) {
220
+ case 'currency':
221
+ return this.formatCurrency(value);
222
+ case 'alphanumeric':
223
+ default:
224
+ return String(value);
225
+ }
226
+ }
227
+
228
+ private formatCurrency(value: any): string {
229
+ const numValue = typeof value === 'string' ? parseFloat(value.replace(/[^0-9.]/g, '')) : value;
230
+
231
+ if (isNaN(numValue)) return '';
232
+
233
+ // Format with thousands separator
234
+ return numValue.toLocaleString('es-MX', {
235
+ minimumFractionDigits: 0,
236
+ maximumFractionDigits: 2
237
+ });
238
+ }
239
+ }
@@ -0,0 +1,144 @@
1
+ .svu-loader-overlay {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ width: 100vw;
6
+ height: 100vh;
7
+ background: #F8F9FC;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 99999;
12
+ animation: fadeIn 0.3s ease-in-out;
13
+ }
14
+
15
+ @keyframes fadeIn {
16
+ from {
17
+ opacity: 0;
18
+ }
19
+ to {
20
+ opacity: 1;
21
+ }
22
+ }
23
+
24
+ .svu-loader-content {
25
+ display: flex;
26
+ flex-direction: column;
27
+ align-items: center;
28
+ gap: 2rem;
29
+ min-height: 500px;
30
+ justify-content: center;
31
+ }
32
+
33
+ .svu-loader-logo {
34
+ margin-bottom: 2rem;
35
+
36
+ img {
37
+ height: 60px;
38
+ width: auto;
39
+ }
40
+ }
41
+
42
+ .svu-loader-image {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ animation: float 3s ease-in-out infinite;
47
+
48
+ img {
49
+ max-width: 300px;
50
+ height: auto;
51
+ }
52
+ }
53
+
54
+ @keyframes float {
55
+ 0%, 100% {
56
+ transform: translateY(0px);
57
+ }
58
+ 50% {
59
+ transform: translateY(-20px);
60
+ }
61
+ }
62
+
63
+ .svu-loader-dots {
64
+ display: flex;
65
+ gap: 12px;
66
+ align-items: center;
67
+ justify-content: center;
68
+ padding: 1rem 0;
69
+ height: 80px;
70
+ }
71
+
72
+ .svu-loader-dot {
73
+ width: 20px;
74
+ height: 20px;
75
+ border-radius: 50%;
76
+ background: #F59100;
77
+ position: relative;
78
+ transform-origin: center;
79
+
80
+ &:nth-child(1) {
81
+ animation: bounce 1.4s ease-in-out infinite;
82
+ }
83
+
84
+ &:nth-child(2) {
85
+ animation: bounce 1.4s ease-in-out infinite;
86
+ animation-delay: 0.2s;
87
+ }
88
+
89
+ &:nth-child(3) {
90
+ animation: bounce 1.4s ease-in-out infinite;
91
+ animation-delay: 0.4s;
92
+ }
93
+ }
94
+
95
+ @keyframes bounce {
96
+ 0%, 80%, 100% {
97
+ transform: scale(0.6);
98
+ opacity: 0.5;
99
+ }
100
+ 40% {
101
+ transform: scale(1);
102
+ opacity: 1;
103
+ }
104
+ }
105
+
106
+ .svu-loader-message {
107
+ font-family: 'DM Sans', sans-serif;
108
+ color: #2E353A;
109
+ text-align: center;
110
+ font-weight: 900;
111
+ font-size: 30px;
112
+
113
+ ::ng-deep strong,
114
+ ::ng-deep b {
115
+ color: #F26D00;
116
+ font-weight: 900;
117
+ }
118
+ }
119
+
120
+ // Responsive adjustments
121
+ @media (max-width: 768px) {
122
+ .svu-loader-logo img {
123
+ height: 48px;
124
+ }
125
+
126
+ .svu-loader-image img {
127
+ max-width: 200px;
128
+ }
129
+
130
+ .svu-loader-message {
131
+ font-size: 20px;
132
+ padding: 0 1rem;
133
+ }
134
+
135
+ .svu-loader-dots {
136
+ height: 50px;
137
+ gap: 5px;
138
+ }
139
+
140
+ .svu-loader-dot {
141
+ width: 12px;
142
+ height: 12px;
143
+ }
144
+ }
@@ -0,0 +1,44 @@
1
+ import { Component, Input } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { AssetSourceDirective } from '../../directives/asset-source.directive';
4
+
5
+ @Component({
6
+ selector: 'app-loader',
7
+ standalone: true,
8
+ imports: [CommonModule, AssetSourceDirective],
9
+ template: `
10
+ <div class="svu-loader-overlay" *ngIf="show">
11
+ <div class="svu-loader-content">
12
+ <!-- Logo -->
13
+ <div class="svu-loader-logo" *ngIf="logo">
14
+ <img [src]="logo" assetSource="mf" alt="Logo">
15
+ </div>
16
+
17
+ <!-- Image/Icon -->
18
+ <div class="svu-loader-image" *ngIf="image">
19
+ <img [src]="image" assetSource="mf" [alt]="imageAlt">
20
+ </div>
21
+
22
+ <!-- Loading Dots Animation -->
23
+ <div class="svu-loader-dots">
24
+ <span class="svu-loader-dot"></span>
25
+ <span class="svu-loader-dot"></span>
26
+ <span class="svu-loader-dot"></span>
27
+ </div>
28
+
29
+ <!-- Message -->
30
+ <div class="svu-loader-message" *ngIf="message">
31
+ <span [innerHTML]="message"></span>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ `,
36
+ styleUrls: ['./loader.component.scss']
37
+ })
38
+ export class LoaderComponent {
39
+ @Input() show: boolean = false;
40
+ @Input() message?: string;
41
+ @Input() logo?: string;
42
+ @Input() image?: string;
43
+ @Input() imageAlt: string = 'Loading';
44
+ }
@@ -0,0 +1,115 @@
1
+ :host {
2
+ display: block;
3
+ outline: none !important;
4
+
5
+ &:focus,
6
+ &:focus-visible,
7
+ &:focus-within {
8
+ outline: none !important;
9
+ box-shadow: none !important;
10
+ }
11
+ }
12
+
13
+ .radio-wrapper {
14
+ cursor: pointer;
15
+ outline: none !important;
16
+ box-shadow: none !important;
17
+
18
+ &:focus,
19
+ &:focus-visible,
20
+ &:focus-within {
21
+ outline: none !important;
22
+ box-shadow: none !important;
23
+ }
24
+
25
+ &.disabled {
26
+ opacity: 0.6;
27
+ cursor: not-allowed;
28
+ pointer-events: none;
29
+ }
30
+
31
+ &--selected {
32
+ .radio-custom {
33
+ border-color: #F59100;
34
+ }
35
+ }
36
+ }
37
+
38
+ .radio-container {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 12px;
42
+ padding: 0;
43
+ background: transparent;
44
+ border: none;
45
+ outline: none !important;
46
+ box-shadow: none !important;
47
+
48
+ &:focus,
49
+ &:focus-visible,
50
+ &:focus-within {
51
+ outline: none !important;
52
+ box-shadow: none !important;
53
+ }
54
+
55
+ &:hover .radio-custom:not(.checked) {
56
+ border-color: #F59100;
57
+ }
58
+ }
59
+
60
+ .radio-input {
61
+ position: absolute;
62
+ opacity: 0;
63
+ pointer-events: none;
64
+ width: 0;
65
+ height: 0;
66
+ margin: 0;
67
+ padding: 0;
68
+ border: none !important;
69
+ outline: none !important;
70
+ box-shadow: none !important;
71
+
72
+ &:focus,
73
+ &:focus-visible,
74
+ &:checked {
75
+ outline: none !important;
76
+ box-shadow: none !important;
77
+ border: none !important;
78
+ }
79
+ }
80
+
81
+ .radio-custom {
82
+ width: 20px;
83
+ height: 20px;
84
+ min-width: 20px;
85
+ border: 2px solid #D5D7DA;
86
+ border-radius: 50%;
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ transition: border-color 0.2s ease;
91
+ background: white;
92
+ flex-shrink: 0;
93
+ outline: none !important;
94
+ box-shadow: none !important;
95
+
96
+ &.checked {
97
+ border-color: #F59100;
98
+ }
99
+
100
+ .radio-dot {
101
+ width: 10px;
102
+ height: 10px;
103
+ border-radius: 50%;
104
+ background: #F59100;
105
+ }
106
+ }
107
+
108
+ .radio-text {
109
+ font-family: 'Quicksand', sans-serif;
110
+ font-size: 16px;
111
+ font-weight: 500;
112
+ line-height: 24px;
113
+ color: #585D61;
114
+ user-select: none;
115
+ }
@@ -0,0 +1,103 @@
1
+ import { Component, Input, forwardRef, ChangeDetectorRef, Output, EventEmitter } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
4
+
5
+ @Component({
6
+ selector: 'app-radio',
7
+ standalone: true,
8
+ imports: [CommonModule, ReactiveFormsModule, FormsModule],
9
+ template: `
10
+ <div
11
+ class="radio-wrapper"
12
+ [class.disabled]="disabled"
13
+ [class.radio-wrapper--selected]="isSelected"
14
+ (click)="onContainerClick()"
15
+ >
16
+ <div class="radio-container">
17
+ <input
18
+ type="radio"
19
+ [id]="id"
20
+ [name]="name"
21
+ [value]="radioValue"
22
+ [disabled]="disabled"
23
+ [checked]="isSelected"
24
+ (blur)="onTouched()"
25
+ (change)="onRadioChange($event)"
26
+ class="radio-input"
27
+ />
28
+ <span class="radio-custom" [class.checked]="isSelected">
29
+ <span class="radio-dot" *ngIf="isSelected"></span>
30
+ </span>
31
+ <span class="radio-text">
32
+ <ng-content></ng-content>
33
+ <span *ngIf="label">{{ label }}</span>
34
+ </span>
35
+ </div>
36
+ </div>
37
+ `,
38
+ styleUrls: ['./radio.component.scss'],
39
+ providers: [
40
+ {
41
+ provide: NG_VALUE_ACCESSOR,
42
+ useExisting: forwardRef(() => RadioComponent),
43
+ multi: true
44
+ }
45
+ ]
46
+ })
47
+ export class RadioComponent implements ControlValueAccessor {
48
+ @Input() label = '';
49
+ @Input() id = '';
50
+ @Input() name = '';
51
+ @Input() radioValue: any = '';
52
+ @Input() disabled = false;
53
+
54
+ @Output() changed = new EventEmitter<any>();
55
+
56
+ value: any = null;
57
+ onChange: any = () => {};
58
+ onTouched: any = () => {};
59
+
60
+ constructor(private cdr: ChangeDetectorRef) {}
61
+
62
+ get isSelected(): boolean {
63
+ return this.value === this.radioValue;
64
+ }
65
+
66
+ onContainerClick(): void {
67
+ if (this.disabled) return;
68
+
69
+ this.value = this.radioValue;
70
+ this.onChange(this.value);
71
+ this.changed.emit(this.value);
72
+ this.cdr.markForCheck();
73
+ }
74
+
75
+ onRadioChange(event: Event): void {
76
+ event.stopPropagation();
77
+ const target = event.target as HTMLInputElement;
78
+ if (target.checked) {
79
+ this.value = this.radioValue;
80
+ this.onChange(this.value);
81
+ this.changed.emit(this.value);
82
+ this.cdr.markForCheck();
83
+ }
84
+ }
85
+
86
+ writeValue(value: any): void {
87
+ this.value = value;
88
+ this.cdr.markForCheck();
89
+ }
90
+
91
+ registerOnChange(fn: any): void {
92
+ this.onChange = fn;
93
+ }
94
+
95
+ registerOnTouched(fn: any): void {
96
+ this.onTouched = fn;
97
+ }
98
+
99
+ setDisabledState(isDisabled: boolean): void {
100
+ this.disabled = isDisabled;
101
+ this.cdr.markForCheck();
102
+ }
103
+ }