@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
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # @up_si_vale/sivale-componentes-angular
2
+
3
+ Librería de componentes Angular **standalone** (atoms + molecules) reutilizables por los microfrontends de Sivale.
4
+
5
+ ## Instalación
6
+
7
+ ```bash
8
+ npm install @up_si_vale/sivale-componentes-angular
9
+ ```
10
+
11
+ Esta librería **no incluye estilos**. El consumidor debe importar globalmente el design system:
12
+
13
+ ```scss
14
+ // styles.scss del consumidor
15
+ @use '@up_si_vale/sivale-design-styles/scss';
16
+ ```
17
+
18
+ ## Configuración de assets (opcional)
19
+
20
+ Las directivas `AssetSourceDirective` e `IconSourceDirective` necesitan saber dónde están los íconos/imágenes. Provee el token `ASSET_BASE_URL` en el bootstrap de tu app:
21
+
22
+ ```ts
23
+ import { ApplicationConfig } from '@angular/core';
24
+ import { ASSET_BASE_URL } from '@up_si_vale/sivale-componentes-angular';
25
+
26
+ export const appConfig: ApplicationConfig = {
27
+ providers: [
28
+ { provide: ASSET_BASE_URL, useValue: environment.mfAssetsUrl }
29
+ ]
30
+ };
31
+ ```
32
+
33
+ ## Componentes
34
+
35
+ ### Atoms
36
+ - `ButtonComponent` — `<app-button>`
37
+ - `InputComponent` — `<app-input>` (CVA, `format: alphanumeric | currency | numeric`)
38
+ - `TextareaComponent` — `<app-textarea>` (CVA)
39
+ - `CheckboxComponent` — `<app-checkbox>` (CVA)
40
+ - `RadioComponent` — `<app-radio>` (CVA)
41
+ - `DatePickerComponent` — `<app-date-picker>` (CVA)
42
+ - `LoaderComponent` — `<app-loader>`
43
+
44
+ ### Molecules
45
+ - `SelectComponent` — `<app-select>` (CVA + content projection con `<app-option>`)
46
+ - `MultiselectComponent` — `<app-multiselect>` (CVA)
47
+ - `OptionComponent` — `<app-option>`
48
+ - `PhoneInputComponent` — `<app-phone-input>` (composición)
49
+ - `CopyInputComponent` — `<copy-input>`
50
+ - `SnackbarComponent` — `<app-snackbar>` (consumir vía `SnackbarService`)
51
+
52
+ ### Directives
53
+ - `[onlyNumber]`, `[onlyLetters]`, `[rfcValidator]`, `[noLeadingSpace]`, `[appTooltip]`, `[assetSource]`, `i[iconName]`
54
+
55
+ ### Services
56
+ - `SnackbarService` — `success() / error() / warning() / info() / show() / dismiss() / dismissAll()`
57
+
58
+ ## Uso (ejemplo)
59
+
60
+ ```ts
61
+ import { Component } from '@angular/core';
62
+ import { InputComponent, SelectComponent, OptionComponent } from '@up_si_vale/sivale-componentes-angular';
63
+
64
+ @Component({
65
+ standalone: true,
66
+ imports: [InputComponent, SelectComponent, OptionComponent],
67
+ template: `
68
+ <app-input label="Nombre" [(ngModel)]="nombre"></app-input>
69
+ <app-select label="País" [(ngModel)]="pais">
70
+ <app-option value="mx">México</app-option>
71
+ </app-select>
72
+ `
73
+ })
74
+ export class DemoComponent { nombre = ''; pais = ''; }
75
+ ```
76
+
77
+ ## Build
78
+
79
+ ```bash
80
+ npm run build # genera dist/ con ng-packagr (FESM, ESM, types)
81
+ ```
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "./dist",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts",
6
+ "styleIncludePaths": []
7
+ },
8
+ "assets": []
9
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@up_si_vale/sivale-componentes-angular",
3
+ "version": "1.0.0",
4
+ "description": "Librería de componentes Angular standalone (atoms + molecules) para los frontends de Sivale.",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "scripts": {
9
+ "build": "ng-packagr -p ng-package.json",
10
+ "build:watch": "ng-packagr -p ng-package.json --watch",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "peerDependencies": {
14
+ "@angular/common": "^17.3.0",
15
+ "@angular/core": "^17.3.0",
16
+ "@angular/forms": "^17.3.0",
17
+ "@up_si_vale/sivale-design-styles": "^1.0.15",
18
+ "rxjs": "^7.8.0"
19
+ },
20
+ "dependencies": {
21
+ "tslib": "^2.3.0"
22
+ },
23
+ "devDependencies": {
24
+ "@angular/common": "^17.3.0",
25
+ "@angular/compiler": "^17.3.0",
26
+ "@angular/compiler-cli": "^17.3.0",
27
+ "@angular/core": "^17.3.0",
28
+ "@angular/forms": "^17.3.0",
29
+ "ng-packagr": "^17.3.0",
30
+ "rxjs": "^7.8.0",
31
+ "tslib": "^2.3.0",
32
+ "typescript": "~5.4.2",
33
+ "zone.js": "~0.14.3"
34
+ },
35
+ "sideEffects": false,
36
+ "main": "index.js",
37
+ "keywords": [],
38
+ "author": "",
39
+ "license": "ISC"
40
+ }
@@ -0,0 +1,21 @@
1
+ <button
2
+ [type]="type"
3
+ [class]="buttonClasses"
4
+ [disabled]="disabled || loading"
5
+ [id]="id"
6
+ [attr.aria-label]="ariaLabel"
7
+ [attr.aria-busy]="loading"
8
+ (click)="handleClick($event)">
9
+
10
+ <!-- Loading spinner -->
11
+ <span *ngIf="loading" class="svu-btn__spinner">
12
+ <svg class="svu-spinner" viewBox="0 0 24 24">
13
+ <circle class="svu-spinner__circle" cx="12" cy="12" r="10" fill="none" stroke-width="3"></circle>
14
+ </svg>
15
+ </span>
16
+
17
+ <!-- Content -->
18
+ <span [class.svu-btn__content--hidden]="loading" class="svu-btn__content">
19
+ <ng-content></ng-content>
20
+ </span>
21
+ </button>
@@ -0,0 +1,242 @@
1
+ .svu-btn {
2
+ position: relative;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: 0.5rem;
7
+ font-family: 'Quicksand', sans-serif;
8
+ font-weight: 600;
9
+ border: none;
10
+ border-radius: 8px;
11
+ cursor: pointer;
12
+ transition: all 0.2s ease-in-out;
13
+ outline: none !important;
14
+ text-decoration: none;
15
+ white-space: nowrap;
16
+ width: 100%;
17
+
18
+ &:focus-visible {
19
+ outline: 2px solid transparent;
20
+ outline-offset: 2px;
21
+ }
22
+
23
+ // Sizes
24
+ &--small {
25
+ padding: 0.5rem 1rem;
26
+ font-size: 0.875rem;
27
+ line-height: 1.25rem;
28
+ height: inherit;
29
+ }
30
+
31
+ &--medium {
32
+ padding: 0.75rem 1.5rem;
33
+ font-size: 1rem;
34
+ line-height: 1.5rem;
35
+ }
36
+
37
+ &--large {
38
+ padding: 1rem 2rem;
39
+ font-size: 1.125rem;
40
+ line-height: 1.75rem;
41
+ }
42
+
43
+ // Full width
44
+ &--full-width {
45
+ width: 100%;
46
+ }
47
+
48
+ // Primary variant
49
+ &--primary {
50
+ background: #364B9F;
51
+ color: white;
52
+
53
+ &:hover:not(:disabled):not(.btn--loading) {
54
+ background: #2A3B7F;
55
+ transform: translateY(-1px);
56
+ box-shadow: 0 4px 12px rgba(54, 75, 159, 0.3);
57
+ }
58
+
59
+ &:active:not(:disabled):not(.btn--loading) {
60
+ transform: translateY(0);
61
+ background: #1F2D5F;
62
+ }
63
+
64
+ &:focus-visible {
65
+ box-shadow: 0 0 0 3px rgba(54, 75, 159, 0.3);
66
+ }
67
+ }
68
+
69
+ // Secondary variant
70
+ &--secondary {
71
+ background: #F3F4F6;
72
+ color: #374151;
73
+
74
+ &:hover:not(:disabled):not(.btn--loading) {
75
+ background: #E5E7EB;
76
+ transform: translateY(-1px);
77
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
78
+ }
79
+
80
+ &:active:not(:disabled):not(.btn--loading) {
81
+ transform: translateY(0);
82
+ }
83
+
84
+ &:focus-visible {
85
+ box-shadow: 0 0 0 3px rgba(55, 65, 81, 0.2);
86
+ }
87
+ }
88
+
89
+ // Outline variant
90
+ &--outline {
91
+ background: transparent;
92
+ color: #364B9F;
93
+ border: 1px solid #364B9F;
94
+
95
+ &:hover:not(:disabled):not(.btn--loading) {
96
+ background: rgba(54, 75, 159, 0.05);
97
+ border-color: #2A3B7F;
98
+ color: #2A3B7F;
99
+ transform: translateY(-1px);
100
+ }
101
+
102
+ &:active:not(:disabled):not(.btn--loading) {
103
+ transform: translateY(0);
104
+ background: rgba(54, 75, 159, 0.1);
105
+ }
106
+
107
+ &:focus-visible {
108
+ box-shadow: 0 0 0 3px rgba(54, 75, 159, 0.2);
109
+ }
110
+ }
111
+
112
+ // Ghost variant
113
+ &--ghost {
114
+ background: transparent;
115
+ color: #364B9F;
116
+
117
+ &:hover:not(:disabled):not(.btn--loading) {
118
+ background: rgba(54, 75, 159, 0.1);
119
+ transform: translateY(-1px);
120
+ }
121
+
122
+ &:active:not(:disabled):not(.btn--loading) {
123
+ transform: translateY(0);
124
+ background: rgba(54, 75, 159, 0.15);
125
+ }
126
+
127
+ &:focus-visible {
128
+ box-shadow: 0 0 0 3px rgba(54, 75, 159, 0.2);
129
+ }
130
+ }
131
+
132
+ // Danger variant
133
+ &--danger {
134
+ background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
135
+ color: white;
136
+
137
+ &:hover:not(:disabled):not(.btn--loading) {
138
+ background: linear-gradient(135deg, #DC2626 0%, #B91C1C 100%);
139
+ transform: translateY(-1px);
140
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
141
+ }
142
+
143
+ &:active:not(:disabled):not(.btn--loading) {
144
+ transform: translateY(0);
145
+ }
146
+
147
+ &:focus-visible {
148
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.3);
149
+ }
150
+ }
151
+
152
+ // Success variant
153
+ &--success {
154
+ background: linear-gradient(135deg, #10B981 0%, #059669 100%);
155
+ color: white;
156
+
157
+ &:hover:not(:disabled):not(.btn--loading) {
158
+ background: linear-gradient(135deg, #059669 0%, #047857 100%);
159
+ transform: translateY(-1px);
160
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
161
+ }
162
+
163
+ &:active:not(:disabled):not(.btn--loading) {
164
+ transform: translateY(0);
165
+ }
166
+
167
+ &:focus-visible {
168
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.3);
169
+ }
170
+ }
171
+
172
+ // Disabled state
173
+ &:disabled,
174
+ &--disabled {
175
+ opacity: 0.5;
176
+ cursor: not-allowed;
177
+ transform: none !important;
178
+ box-shadow: none !important;
179
+ }
180
+
181
+ // Loading state
182
+ &--loading {
183
+ cursor: wait;
184
+ pointer-events: none;
185
+ }
186
+
187
+ &__spinner {
188
+ position: absolute;
189
+ left: 50%;
190
+ top: 50%;
191
+ transform: translate(-50%, -50%);
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: center;
195
+ }
196
+
197
+ &__content {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 0.5rem;
201
+
202
+ &--hidden {
203
+ visibility: hidden;
204
+ }
205
+ }
206
+ }
207
+
208
+ // Spinner animation
209
+ .svu-spinner {
210
+ width: 1.25rem;
211
+ height: 1.25rem;
212
+ animation: svu-spin 0.8s linear infinite;
213
+
214
+ &__circle {
215
+ stroke: currentColor;
216
+ stroke-dasharray: 50;
217
+ stroke-dashoffset: 25;
218
+ stroke-linecap: round;
219
+ animation: svu-spinner-dash 1.5s ease-in-out infinite;
220
+ }
221
+ }
222
+
223
+ @keyframes svu-spin {
224
+ to {
225
+ transform: rotate(360deg);
226
+ }
227
+ }
228
+
229
+ @keyframes svu-spinner-dash {
230
+ 0% {
231
+ stroke-dasharray: 1, 150;
232
+ stroke-dashoffset: 0;
233
+ }
234
+ 50% {
235
+ stroke-dasharray: 90, 150;
236
+ stroke-dashoffset: -35;
237
+ }
238
+ 100% {
239
+ stroke-dasharray: 90, 150;
240
+ stroke-dashoffset: -124;
241
+ }
242
+ }
@@ -0,0 +1,43 @@
1
+ import { Component, Input, Output, EventEmitter } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success';
5
+ export type ButtonSize = 'small' | 'medium' | 'large';
6
+ export type ButtonType = 'button' | 'submit' | 'reset';
7
+
8
+ @Component({
9
+ selector: 'app-button',
10
+ standalone: true,
11
+ imports: [CommonModule],
12
+ templateUrl: './button.component.html',
13
+ styleUrls: ['./button.component.scss']
14
+ })
15
+ export class ButtonComponent {
16
+ @Input() variant: ButtonVariant = 'primary';
17
+ @Input() size: ButtonSize = 'medium';
18
+ @Input() type: ButtonType = 'button';
19
+ @Input() disabled: boolean = false;
20
+ @Input() loading: boolean = false;
21
+ @Input() fullWidth: boolean = false;
22
+ @Input() id?: string;
23
+ @Input() ariaLabel?: string;
24
+
25
+ @Output() onClick = new EventEmitter<MouseEvent>();
26
+
27
+ get buttonClasses(): string {
28
+ return [
29
+ 'svu-btn',
30
+ `svu-btn--${this.variant}`,
31
+ `svu-btn--${this.size}`,
32
+ this.fullWidth ? 'svu-btn--full-width' : '',
33
+ this.loading ? 'svu-btn--loading' : '',
34
+ this.disabled ? 'svu-btn--disabled' : ''
35
+ ].filter(Boolean).join(' ');
36
+ }
37
+
38
+ handleClick(event: MouseEvent): void {
39
+ if (!this.disabled && !this.loading) {
40
+ this.onClick.emit(event);
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,131 @@
1
+ .checkbox-wrapper {
2
+ width: 100%;
3
+ padding: 0 4px;
4
+
5
+ &.disabled {
6
+ opacity: 0.6;
7
+ cursor: not-allowed;
8
+ }
9
+ }
10
+
11
+ .checkbox-container {
12
+ display: flex;
13
+ align-items: flex-start;
14
+ gap: 0.75rem;
15
+ }
16
+
17
+ .checkbox-label-input {
18
+ cursor: pointer;
19
+ position: relative;
20
+ display: flex;
21
+ align-items: center;
22
+
23
+ input[type="checkbox"] {
24
+ position: absolute;
25
+ opacity: 0;
26
+ cursor: pointer;
27
+ width: 22px;
28
+ height: 22px;
29
+ margin: 0;
30
+ z-index: 2;
31
+ top: 2px;
32
+ left: 0;
33
+ }
34
+
35
+ .checkbox-custom {
36
+ width: 22px;
37
+ height: 22px;
38
+ min-width: 22px;
39
+ border: 2.5px solid #D5D7D8;
40
+ border-radius: 6px;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
45
+ background: white;
46
+ margin-top: 2px;
47
+ position: relative;
48
+
49
+ &::after {
50
+ content: '';
51
+ position: absolute;
52
+ width: 100%;
53
+ height: 100%;
54
+ border-radius: 4px;
55
+ background: #F59100;
56
+ opacity: 0;
57
+ transform: scale(0.5);
58
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
59
+ }
60
+
61
+ &.checked, &.indeterminate {
62
+ background: white;
63
+ border-color: #F59100;
64
+ border-width: 2.5px;
65
+ box-shadow: 0 3px 8px rgba(242, 109, 0, 0.25), 0 1px 3px rgba(242, 109, 0, 0.15);
66
+ transform: scale(1.05);
67
+
68
+ &::after {
69
+ opacity: 0;
70
+ }
71
+ }
72
+
73
+ .check-icon {
74
+ position: relative;
75
+ z-index: 1;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ animation: checkPop 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
80
+ }
81
+ }
82
+
83
+ &:hover .checkbox-custom:not(.checked) {
84
+ border-color: #F26D00;
85
+ transform: scale(1.05);
86
+ }
87
+ }
88
+
89
+ .checkbox-text {
90
+ flex: 1;
91
+ font-family: 'Quicksand', sans-serif;
92
+ font-size: 14px;
93
+ font-weight: 500;
94
+ line-height: 1.7;
95
+ user-select: none;
96
+ cursor: pointer;
97
+
98
+ ::ng-deep a {
99
+ color: #364B9F;
100
+ font-family: 'Quicksand', sans-serif;
101
+ font-size: 14px;
102
+ font-weight: 500;
103
+ line-height: 1.7;
104
+ cursor: pointer;
105
+ }
106
+ }
107
+
108
+ .checkbox-wrapper.disabled .checkbox-text {
109
+ cursor: not-allowed;
110
+ }
111
+
112
+ @keyframes checkPop {
113
+ 0% {
114
+ transform: scale(0);
115
+ opacity: 0;
116
+ }
117
+ 50% {
118
+ transform: scale(1.2);
119
+ }
120
+ 100% {
121
+ transform: scale(1);
122
+ opacity: 1;
123
+ }
124
+ }
125
+
126
+ .field-error {
127
+ color: #F04438;
128
+ font-size: 0.875rem;
129
+ margin-top: 0.25rem;
130
+ font-family: 'Quicksand', sans-serif;
131
+ }
@@ -0,0 +1,130 @@
1
+ import { Component, Input, forwardRef, ChangeDetectorRef, Output, EventEmitter, SimpleChanges, ElementRef } 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-checkbox',
7
+ standalone: true,
8
+ imports: [CommonModule, ReactiveFormsModule, FormsModule],
9
+ template: `
10
+ <div class="checkbox-wrapper" [class.disabled]="disabled">
11
+ <div class="checkbox-container">
12
+ <label [for]="id" class="checkbox-label-input">
13
+ <input
14
+ type="checkbox"
15
+ [id]="id"
16
+ [disabled]="disabled"
17
+ [checked]="value"
18
+ (blur)="onTouched()"
19
+ (change)="onCheckboxChange($event)">
20
+ <span class="checkbox-custom" [class.checked]="value" [class.indeterminate]="indeterminate">
21
+ <!-- Paloma -->
22
+ <svg *ngIf="value && !indeterminate" class="check-icon" width="14" height="10" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
23
+ <path d="M9 1L3.5 6.5L1 4" stroke="#F59100" stroke-width="1.6666" stroke-linecap="round" stroke-linejoin="round"/>
24
+ </svg>
25
+ <!-- Guion -->
26
+ <svg *ngIf="indeterminate && !value" class="minus-icon" width="12" height="2" viewBox="0 0 12 2" fill="none" xmlns="http://www.w3.org/2000/svg">
27
+ <rect width="12" height="2" rx="1" fill="#F59100"/>
28
+ </svg>
29
+ </span>
30
+ </label>
31
+ <span class="checkbox-text" (click)="onTextClick($event)">
32
+ <ng-content></ng-content>
33
+ <span *ngIf="label" [innerHTML]="label"></span>
34
+ </span>
35
+ </div>
36
+ <div *ngIf="hasError && errorMessage" class="field-error">
37
+ {{ errorMessage }}
38
+ </div>
39
+ </div>
40
+ `,
41
+ styleUrls: ['./checkbox.component.scss'],
42
+ providers: [
43
+ {
44
+ provide: NG_VALUE_ACCESSOR,
45
+ useExisting: forwardRef(() => CheckboxComponent),
46
+ multi: true
47
+ }
48
+ ]
49
+ })
50
+ export class CheckboxComponent implements ControlValueAccessor {
51
+ @Input() label = '';
52
+ @Input() id = '';
53
+ @Input() hasError = false;
54
+ @Input() errorMessage = '';
55
+ @Input() disabled = false;
56
+ @Input() indeterminate = false;
57
+ @Input()
58
+ set checked(value: boolean) {
59
+ this.value = !!value;
60
+ this.cdr.markForCheck();
61
+ }
62
+ get checked(): boolean {
63
+ return this.value;
64
+ }
65
+
66
+ @Output() changed = new EventEmitter<boolean>();
67
+
68
+ value = false;
69
+ onChange: any = () => {};
70
+ onTouched: any = () => {};
71
+
72
+ constructor(private cdr: ChangeDetectorRef, private elRef: ElementRef) {}
73
+
74
+ ngAfterViewInit() {
75
+ this.updateNativeIndeterminate();
76
+ }
77
+
78
+ ngOnChanges(changes: SimpleChanges): void {
79
+ if (changes['indeterminate']) {
80
+ this.updateNativeIndeterminate();
81
+ }
82
+ }
83
+
84
+ private updateNativeIndeterminate() {
85
+ const input = this.elRef.nativeElement.querySelector('input[type="checkbox"]') as HTMLInputElement;
86
+ if (input) input.indeterminate = this.indeterminate;
87
+ }
88
+
89
+ onCheckboxChange(event: Event): void {
90
+ const target = event.target as HTMLInputElement;
91
+ this.value = target.checked;
92
+ this.onChange(this.value);
93
+ this.changed.emit(this.value); // ← EMITE EVENTO MANUAL PARA USO EXTERNO
94
+ this.cdr.markForCheck();
95
+ }
96
+
97
+ onTextClick(event: Event): void {
98
+ const target = event.target as HTMLElement;
99
+ if (target.tagName === 'A' || target.closest('a')) {
100
+ return;
101
+ }
102
+
103
+ if (this.disabled) {
104
+ return;
105
+ }
106
+
107
+ this.value = !this.value;
108
+ this.onChange(this.value);
109
+ this.changed.emit(this.value);
110
+ this.cdr.markForCheck();
111
+ }
112
+
113
+ writeValue(value: any): void {
114
+ this.value = !!value;
115
+ this.cdr.markForCheck();
116
+ }
117
+
118
+ registerOnChange(fn: any): void {
119
+ this.onChange = fn;
120
+ }
121
+
122
+ registerOnTouched(fn: any): void {
123
+ this.onTouched = fn;
124
+ }
125
+
126
+ setDisabledState(isDisabled: boolean): void {
127
+ this.disabled = isDisabled;
128
+ this.cdr.markForCheck();
129
+ }
130
+ }