@vekron/vekron-ui 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/bin/vekron-ui.js +362 -0
- package/package.json +45 -3
- package/src/accordion/accordion.component.ts +64 -0
- package/src/accordion/public-api.ts +1 -0
- package/src/autocomplete/autocomplete.component.ts +144 -0
- package/src/autocomplete/public-api.ts +1 -0
- package/src/avatar/avatar.component.ts +23 -0
- package/src/avatar/public-api.ts +1 -0
- package/src/badge/badge.component.ts +27 -0
- package/src/badge/public-api.ts +2 -0
- package/src/breadcrumb/breadcrumb.component.ts +32 -0
- package/src/breadcrumb/public-api.ts +2 -0
- package/src/button/button.component.ts +50 -0
- package/src/button/public-api.ts +2 -0
- package/src/carousel/carousel.component.ts +76 -0
- package/src/carousel/public-api.ts +2 -0
- package/src/checkbox/checkbox.component.ts +92 -0
- package/src/checkbox/public-api.ts +1 -0
- package/src/chip/chip.component.ts +20 -0
- package/src/chip/public-api.ts +1 -0
- package/src/confirm-dialog/confirm-dialog.component.ts +38 -0
- package/src/confirm-dialog/public-api.ts +1 -0
- package/src/confirm-popup/confirm-popup.component.ts +42 -0
- package/src/confirm-popup/public-api.ts +1 -0
- package/src/context-menu/context-menu.component.ts +43 -0
- package/src/context-menu/public-api.ts +1 -0
- package/src/datepicker/datepicker.component.ts +205 -0
- package/src/datepicker/public-api.ts +1 -0
- package/src/dialog/dialog.component.ts +77 -0
- package/src/dialog/public-api.ts +1 -0
- package/src/divider/divider.component.ts +17 -0
- package/src/divider/public-api.ts +1 -0
- package/src/file-upload/file-upload.component.ts +110 -0
- package/src/file-upload/public-api.ts +1 -0
- package/src/input-number/input-number.component.ts +123 -0
- package/src/input-number/public-api.ts +1 -0
- package/src/input-otp/input-otp.component.ts +129 -0
- package/src/input-otp/public-api.ts +1 -0
- package/src/input-password/input-password.component.ts +127 -0
- package/src/input-password/public-api.ts +1 -0
- package/src/input-text/input-text.component.ts +106 -0
- package/src/input-text/public-api.ts +1 -0
- package/src/menu/menu.component.ts +35 -0
- package/src/menu/public-api.ts +1 -0
- package/src/message/message.component.ts +17 -0
- package/src/message/public-api.ts +1 -0
- package/src/paginator/paginator.component.ts +62 -0
- package/src/paginator/public-api.ts +1 -0
- package/src/pdf-viewer/pdf-viewer.component.ts +16 -0
- package/src/pdf-viewer/public-api.ts +1 -0
- package/src/pick-list/pick-list.component.ts +68 -0
- package/src/pick-list/public-api.ts +1 -0
- package/src/popover/popover.component.ts +22 -0
- package/src/popover/public-api.ts +1 -0
- package/src/progress-bar/progress-bar.component.ts +27 -0
- package/src/progress-bar/public-api.ts +1 -0
- package/src/progress-spinner/progress-spinner.component.ts +27 -0
- package/src/progress-spinner/public-api.ts +1 -0
- package/src/public-api.ts +46 -0
- package/src/radio-button/public-api.ts +1 -0
- package/src/radio-button/radio-button.component.ts +98 -0
- package/src/registry.json +184 -0
- package/src/scroll-top/public-api.ts +1 -0
- package/src/scroll-top/scroll-top.component.ts +36 -0
- package/src/select/public-api.ts +1 -0
- package/src/select/select.component.ts +116 -0
- package/src/select-button/public-api.ts +1 -0
- package/src/select-button/select-button.component.ts +70 -0
- package/src/skeleton/public-api.ts +1 -0
- package/src/skeleton/skeleton.component.ts +11 -0
- package/src/split-button/public-api.ts +1 -0
- package/src/split-button/split-button.component.ts +39 -0
- package/src/stepper/public-api.ts +2 -0
- package/src/stepper/stepper.component.ts +45 -0
- package/src/styles.css +63 -0
- package/src/table/public-api.ts +2 -0
- package/src/table/table.component.ts +47 -0
- package/src/tabs/public-api.ts +1 -0
- package/src/tabs/tabs.component.ts +76 -0
- package/src/tag/public-api.ts +1 -0
- package/src/tag/tag.component.ts +22 -0
- package/src/textarea/public-api.ts +1 -0
- package/src/textarea/textarea.component.ts +106 -0
- package/src/toast/public-api.ts +2 -0
- package/src/toast/toast.component.ts +60 -0
- package/src/toggle-button/public-api.ts +1 -0
- package/src/toggle-button/toggle-button.component.ts +80 -0
- package/src/toggle-switch/public-api.ts +1 -0
- package/src/toggle-switch/toggle-switch.component.ts +96 -0
- package/src/toolbar/public-api.ts +1 -0
- package/src/toolbar/toolbar.component.ts +17 -0
- package/src/tooltip/public-api.ts +1 -0
- package/src/tooltip/tooltip.component.ts +30 -0
- package/src/tree/public-api.ts +2 -0
- package/src/tree/tree.component.ts +64 -0
- package/src/utils/classes.ts +19 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/models.ts +13 -0
- package/src/utils/public-api.ts +3 -0
- package/README.md +0 -3
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
effect,
|
|
6
|
+
input,
|
|
7
|
+
model,
|
|
8
|
+
viewChild,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { UiButtonComponent } from '../button/button.component';
|
|
11
|
+
|
|
12
|
+
@Component({
|
|
13
|
+
selector: 'ui-dialog',
|
|
14
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
|
+
imports: [UiButtonComponent],
|
|
16
|
+
template: `
|
|
17
|
+
<dialog
|
|
18
|
+
#dialog
|
|
19
|
+
class="w-full max-w-lg rounded-ui border border-border bg-card p-6 text-card-foreground shadow-lg backdrop:bg-black/40"
|
|
20
|
+
(click)="onBackdrop($event)"
|
|
21
|
+
(close)="handleClose()"
|
|
22
|
+
>
|
|
23
|
+
<div class="flex flex-col gap-4">
|
|
24
|
+
@if (closable()) {
|
|
25
|
+
<div class="flex justify-end">
|
|
26
|
+
<ui-button variant="ghost" size="icon" aria-label="Cerrar" (pressed)="close()">×</ui-button>
|
|
27
|
+
</div>
|
|
28
|
+
}
|
|
29
|
+
<ng-content />
|
|
30
|
+
</div>
|
|
31
|
+
</dialog>
|
|
32
|
+
`,
|
|
33
|
+
})
|
|
34
|
+
export class UiDialogComponent {
|
|
35
|
+
readonly open = model(false);
|
|
36
|
+
readonly closable = input(true);
|
|
37
|
+
private readonly dialogElement = viewChild<ElementRef<HTMLDialogElement>>('dialog');
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
effect(() => {
|
|
41
|
+
const dialog = this.dialogElement();
|
|
42
|
+
if (dialog === undefined) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const element = dialog.nativeElement;
|
|
46
|
+
if (this.open()) {
|
|
47
|
+
if (element.open) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
element.showModal();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!element.open) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
element.close();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
close(): void {
|
|
61
|
+
this.open.set(false);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onBackdrop(event: MouseEvent): void {
|
|
65
|
+
if (event.target !== event.currentTarget) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.close();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
handleClose(): void {
|
|
72
|
+
if (!this.open()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.open.set(false);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiDialogComponent } from './dialog.component';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'ui-divider',
|
|
5
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6
|
+
template: `<div role="separator" [attr.aria-orientation]="orientation()" [class]="lineClass()"></div>`,
|
|
7
|
+
})
|
|
8
|
+
export class UiDividerComponent {
|
|
9
|
+
readonly orientation = input<'horizontal' | 'vertical'>('horizontal');
|
|
10
|
+
|
|
11
|
+
lineClass(): string {
|
|
12
|
+
if (this.orientation() === 'vertical') {
|
|
13
|
+
return 'mx-2 h-full min-h-4 w-px bg-border';
|
|
14
|
+
}
|
|
15
|
+
return 'my-2 h-px w-full bg-border';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiDividerComponent } from './divider.component';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
forwardRef,
|
|
6
|
+
input,
|
|
7
|
+
model,
|
|
8
|
+
viewChild,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
11
|
+
import { UiButtonComponent } from '../button/button.component';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'ui-file-upload',
|
|
15
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
|
+
imports: [UiButtonComponent],
|
|
17
|
+
template: `
|
|
18
|
+
<div
|
|
19
|
+
class="flex w-full flex-col gap-3 rounded-ui border border-dashed border-border p-6"
|
|
20
|
+
(dragover)="onDragOver($event)"
|
|
21
|
+
(drop)="onDrop($event)"
|
|
22
|
+
>
|
|
23
|
+
<p class="text-sm text-muted-foreground">Arrastra archivos o elige desde el equipo.</p>
|
|
24
|
+
<ui-button variant="outline" (pressed)="openPicker()">Elegir archivos</ui-button>
|
|
25
|
+
<input
|
|
26
|
+
#picker
|
|
27
|
+
class="hidden"
|
|
28
|
+
type="file"
|
|
29
|
+
[accept]="accept()"
|
|
30
|
+
[multiple]="multiple()"
|
|
31
|
+
(change)="onFiles($event)"
|
|
32
|
+
/>
|
|
33
|
+
<ul class="flex flex-col gap-1 text-sm">
|
|
34
|
+
@for (file of files(); track file.name) {
|
|
35
|
+
<li>{{ file.name }}</li>
|
|
36
|
+
}
|
|
37
|
+
</ul>
|
|
38
|
+
</div>
|
|
39
|
+
`,
|
|
40
|
+
providers: [
|
|
41
|
+
{
|
|
42
|
+
provide: NG_VALUE_ACCESSOR,
|
|
43
|
+
useExisting: forwardRef(() => UiFileUploadComponent),
|
|
44
|
+
multi: true,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
})
|
|
48
|
+
export class UiFileUploadComponent implements ControlValueAccessor {
|
|
49
|
+
readonly files = model<readonly File[]>([]);
|
|
50
|
+
readonly multiple = input(false);
|
|
51
|
+
readonly accept = input('');
|
|
52
|
+
private readonly picker = viewChild<ElementRef<HTMLInputElement>>('picker');
|
|
53
|
+
private onChange: (value: readonly File[]) => void = () => undefined;
|
|
54
|
+
private onTouched: () => void = () => undefined;
|
|
55
|
+
|
|
56
|
+
openPicker(): void {
|
|
57
|
+
const picker = this.picker();
|
|
58
|
+
if (picker === undefined) {
|
|
59
|
+
throw new Error('ui-file-upload no encontró el input de archivos.');
|
|
60
|
+
}
|
|
61
|
+
picker.nativeElement.click();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onDragOver(event: DragEvent): void {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
onDrop(event: DragEvent): void {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
const transfer = event.dataTransfer;
|
|
71
|
+
if (transfer === null) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.commit(transfer.files);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onFiles(event: Event): void {
|
|
78
|
+
const target = event.target;
|
|
79
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
80
|
+
throw new Error('ui-file-upload esperaba un HTMLInputElement.');
|
|
81
|
+
}
|
|
82
|
+
this.commit(target.files);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
writeValue(value: readonly File[] | null): void {
|
|
86
|
+
if (value === null) {
|
|
87
|
+
this.files.set([]);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.files.set(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
registerOnChange(fn: (value: readonly File[]) => void): void {
|
|
94
|
+
this.onChange = fn;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
registerOnTouched(fn: () => void): void {
|
|
98
|
+
this.onTouched = fn;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private commit(list: FileList | null): void {
|
|
102
|
+
if (list === null) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const files = Array.from(list);
|
|
106
|
+
this.files.set(files);
|
|
107
|
+
this.onChange(files);
|
|
108
|
+
this.onTouched();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiFileUploadComponent } from './file-upload.component';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
forwardRef,
|
|
6
|
+
input,
|
|
7
|
+
model,
|
|
8
|
+
signal,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
11
|
+
import { uiFieldClass, uiLabelClass } from '../utils/classes';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'ui-input-number',
|
|
15
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
|
+
template: `
|
|
17
|
+
<div class="flex w-full flex-col gap-1.5">
|
|
18
|
+
@if (label().length > 0) {
|
|
19
|
+
<label [class]="labelClass" [attr.for]="resolvedId()">{{ label() }}</label>
|
|
20
|
+
}
|
|
21
|
+
<input
|
|
22
|
+
[id]="resolvedId()"
|
|
23
|
+
type="number"
|
|
24
|
+
[attr.name]="resolvedName()"
|
|
25
|
+
[placeholder]="placeholder()"
|
|
26
|
+
[disabled]="isDisabled()"
|
|
27
|
+
[readOnly]="readOnly()"
|
|
28
|
+
[attr.min]="min()"
|
|
29
|
+
[attr.max]="max()"
|
|
30
|
+
[attr.step]="step()"
|
|
31
|
+
[value]="displayValue()"
|
|
32
|
+
[class]="fieldClass"
|
|
33
|
+
(input)="onInput($event)"
|
|
34
|
+
(blur)="markTouched()"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
`,
|
|
38
|
+
providers: [
|
|
39
|
+
{
|
|
40
|
+
provide: NG_VALUE_ACCESSOR,
|
|
41
|
+
useExisting: forwardRef(() => UiInputNumberComponent),
|
|
42
|
+
multi: true,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
})
|
|
46
|
+
export class UiInputNumberComponent implements ControlValueAccessor {
|
|
47
|
+
readonly value = model<number | null>(null);
|
|
48
|
+
readonly label = input('');
|
|
49
|
+
readonly placeholder = input('');
|
|
50
|
+
readonly name = input('');
|
|
51
|
+
readonly inputId = input('');
|
|
52
|
+
readonly min = input<number | null>(null);
|
|
53
|
+
readonly max = input<number | null>(null);
|
|
54
|
+
readonly step = input(1);
|
|
55
|
+
readonly readOnly = input(false);
|
|
56
|
+
readonly disabled = input(false);
|
|
57
|
+
private readonly formDisabled = signal(false);
|
|
58
|
+
private readonly generatedId = crypto.randomUUID();
|
|
59
|
+
private onChange: (value: number | null) => void = () => undefined;
|
|
60
|
+
private onTouched: () => void = () => undefined;
|
|
61
|
+
readonly fieldClass = uiFieldClass;
|
|
62
|
+
readonly labelClass = uiLabelClass;
|
|
63
|
+
readonly isDisabled = computed(() => this.disabled() || this.formDisabled());
|
|
64
|
+
readonly resolvedId = computed(() => {
|
|
65
|
+
const provided = this.inputId();
|
|
66
|
+
if (provided.length > 0) {
|
|
67
|
+
return provided;
|
|
68
|
+
}
|
|
69
|
+
return this.generatedId;
|
|
70
|
+
});
|
|
71
|
+
readonly resolvedName = computed(() => {
|
|
72
|
+
const name = this.name();
|
|
73
|
+
if (name.length === 0) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return name;
|
|
77
|
+
});
|
|
78
|
+
readonly displayValue = computed(() => {
|
|
79
|
+
const current = this.value();
|
|
80
|
+
if (current === null) {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
return String(current);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
onInput(event: Event): void {
|
|
87
|
+
const target = event.target;
|
|
88
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
89
|
+
throw new Error('ui-input-number esperaba un HTMLInputElement.');
|
|
90
|
+
}
|
|
91
|
+
if (target.value.length === 0) {
|
|
92
|
+
this.value.set(null);
|
|
93
|
+
this.onChange(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const parsed = Number(target.value);
|
|
97
|
+
if (Number.isNaN(parsed)) {
|
|
98
|
+
throw new Error('ui-input-number recibió un valor que no es numérico.');
|
|
99
|
+
}
|
|
100
|
+
this.value.set(parsed);
|
|
101
|
+
this.onChange(parsed);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
markTouched(): void {
|
|
105
|
+
this.onTouched();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
writeValue(value: number | null): void {
|
|
109
|
+
this.value.set(value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
registerOnChange(fn: (value: number | null) => void): void {
|
|
113
|
+
this.onChange = fn;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
registerOnTouched(fn: () => void): void {
|
|
117
|
+
this.onTouched = fn;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setDisabledState(isDisabled: boolean): void {
|
|
121
|
+
this.formDisabled.set(isDisabled);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiInputNumberComponent } from './input-number.component';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
ElementRef,
|
|
6
|
+
forwardRef,
|
|
7
|
+
input,
|
|
8
|
+
model,
|
|
9
|
+
signal,
|
|
10
|
+
viewChildren,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
13
|
+
import { uiLabelClass } from '../utils/classes';
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'ui-input-otp',
|
|
17
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18
|
+
template: `
|
|
19
|
+
<div class="flex w-full flex-col gap-1.5">
|
|
20
|
+
@if (label().length > 0) {
|
|
21
|
+
<label [class]="labelClass">{{ label() }}</label>
|
|
22
|
+
}
|
|
23
|
+
<div class="flex gap-2">
|
|
24
|
+
@for (index of slots(); track index) {
|
|
25
|
+
<input
|
|
26
|
+
#slot
|
|
27
|
+
class="h-12 w-10 rounded-ui border border-input bg-background text-center text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
28
|
+
inputmode="numeric"
|
|
29
|
+
maxlength="1"
|
|
30
|
+
[value]="charAt(index)"
|
|
31
|
+
[disabled]="isDisabled()"
|
|
32
|
+
(input)="onSlot($event, index)"
|
|
33
|
+
(keydown)="onKeyDown($event, index)"
|
|
34
|
+
(blur)="markTouched()"
|
|
35
|
+
/>
|
|
36
|
+
}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
`,
|
|
40
|
+
providers: [
|
|
41
|
+
{
|
|
42
|
+
provide: NG_VALUE_ACCESSOR,
|
|
43
|
+
useExisting: forwardRef(() => UiInputOtpComponent),
|
|
44
|
+
multi: true,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
})
|
|
48
|
+
export class UiInputOtpComponent implements ControlValueAccessor {
|
|
49
|
+
readonly value = model('');
|
|
50
|
+
readonly label = input('');
|
|
51
|
+
readonly length = input(6);
|
|
52
|
+
readonly disabled = input(false);
|
|
53
|
+
private readonly formDisabled = signal(false);
|
|
54
|
+
private readonly slotsRef = viewChildren<ElementRef<HTMLInputElement>>('slot');
|
|
55
|
+
private onChange: (value: string) => void = () => undefined;
|
|
56
|
+
private onTouched: () => void = () => undefined;
|
|
57
|
+
readonly labelClass = uiLabelClass;
|
|
58
|
+
readonly isDisabled = computed(() => this.disabled() || this.formDisabled());
|
|
59
|
+
readonly slots = computed(() => {
|
|
60
|
+
const length = this.length();
|
|
61
|
+
if (length < 1) {
|
|
62
|
+
throw new Error('ui-input-otp length debe ser mayor a 0.');
|
|
63
|
+
}
|
|
64
|
+
return Array.from({ length }, (_, index) => index);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
charAt(index: number): string {
|
|
68
|
+
const char = this.value()[index];
|
|
69
|
+
if (char === undefined) {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
return char;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onSlot(event: Event, index: number): void {
|
|
76
|
+
const target = event.target;
|
|
77
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
78
|
+
throw new Error('ui-input-otp esperaba un HTMLInputElement.');
|
|
79
|
+
}
|
|
80
|
+
const chars = this.value().split('');
|
|
81
|
+
chars[index] = target.value.slice(-1);
|
|
82
|
+
const next = chars.join('');
|
|
83
|
+
this.value.set(next);
|
|
84
|
+
this.onChange(next);
|
|
85
|
+
const following = this.slotsRef()[index + 1];
|
|
86
|
+
if (following === undefined) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
following.nativeElement.focus();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onKeyDown(event: KeyboardEvent, index: number): void {
|
|
93
|
+
if (event.key !== 'Backspace') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (this.charAt(index).length > 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const previous = this.slotsRef()[index - 1];
|
|
100
|
+
if (previous === undefined) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
previous.nativeElement.focus();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
markTouched(): void {
|
|
107
|
+
this.onTouched();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
writeValue(value: string | null): void {
|
|
111
|
+
if (value === null) {
|
|
112
|
+
this.value.set('');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.value.set(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
registerOnChange(fn: (value: string) => void): void {
|
|
119
|
+
this.onChange = fn;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
registerOnTouched(fn: () => void): void {
|
|
123
|
+
this.onTouched = fn;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setDisabledState(isDisabled: boolean): void {
|
|
127
|
+
this.formDisabled.set(isDisabled);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiInputOtpComponent } from './input-otp.component';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
forwardRef,
|
|
6
|
+
input,
|
|
7
|
+
model,
|
|
8
|
+
signal,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
11
|
+
import { uiFieldClass, uiLabelClass } from '../utils/classes';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'ui-input-password',
|
|
15
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
|
+
template: `
|
|
17
|
+
<div class="flex w-full flex-col gap-1.5">
|
|
18
|
+
@if (label().length > 0) {
|
|
19
|
+
<label [class]="labelClass" [attr.for]="resolvedId()">{{ label() }}</label>
|
|
20
|
+
}
|
|
21
|
+
<div class="relative">
|
|
22
|
+
<input
|
|
23
|
+
[id]="resolvedId()"
|
|
24
|
+
[attr.name]="resolvedName()"
|
|
25
|
+
[type]="fieldType()"
|
|
26
|
+
[placeholder]="placeholder()"
|
|
27
|
+
[disabled]="isDisabled()"
|
|
28
|
+
[value]="value()"
|
|
29
|
+
[class]="fieldClass + ' pr-20'"
|
|
30
|
+
(input)="onInput($event)"
|
|
31
|
+
(blur)="markTouched()"
|
|
32
|
+
/>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="absolute top-1/2 right-2 -translate-y-1/2 text-xs text-muted-foreground"
|
|
36
|
+
(click)="visible.set(!visible())"
|
|
37
|
+
>
|
|
38
|
+
{{ visibilityLabel() }}
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
`,
|
|
43
|
+
providers: [
|
|
44
|
+
{
|
|
45
|
+
provide: NG_VALUE_ACCESSOR,
|
|
46
|
+
useExisting: forwardRef(() => UiInputPasswordComponent),
|
|
47
|
+
multi: true,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
})
|
|
51
|
+
export class UiInputPasswordComponent implements ControlValueAccessor {
|
|
52
|
+
readonly value = model('');
|
|
53
|
+
readonly label = input('');
|
|
54
|
+
readonly placeholder = input('');
|
|
55
|
+
readonly name = input('');
|
|
56
|
+
readonly inputId = input('');
|
|
57
|
+
readonly disabled = input(false);
|
|
58
|
+
readonly visible = signal(false);
|
|
59
|
+
private readonly formDisabled = signal(false);
|
|
60
|
+
private readonly generatedId = crypto.randomUUID();
|
|
61
|
+
private onChange: (value: string) => void = () => undefined;
|
|
62
|
+
private onTouched: () => void = () => undefined;
|
|
63
|
+
readonly fieldClass = uiFieldClass;
|
|
64
|
+
readonly labelClass = uiLabelClass;
|
|
65
|
+
readonly isDisabled = computed(() => this.disabled() || this.formDisabled());
|
|
66
|
+
readonly resolvedId = computed(() => {
|
|
67
|
+
const provided = this.inputId();
|
|
68
|
+
if (provided.length > 0) {
|
|
69
|
+
return provided;
|
|
70
|
+
}
|
|
71
|
+
return this.generatedId;
|
|
72
|
+
});
|
|
73
|
+
readonly resolvedName = computed(() => {
|
|
74
|
+
const name = this.name();
|
|
75
|
+
if (name.length === 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return name;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
fieldType(): 'password' | 'text' {
|
|
82
|
+
if (this.visible()) {
|
|
83
|
+
return 'text';
|
|
84
|
+
}
|
|
85
|
+
return 'password';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
visibilityLabel(): string {
|
|
89
|
+
if (this.visible()) {
|
|
90
|
+
return 'Ocultar';
|
|
91
|
+
}
|
|
92
|
+
return 'Mostrar';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onInput(event: Event): void {
|
|
96
|
+
const target = event.target;
|
|
97
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
98
|
+
throw new Error('ui-input-password esperaba un HTMLInputElement.');
|
|
99
|
+
}
|
|
100
|
+
this.value.set(target.value);
|
|
101
|
+
this.onChange(target.value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
markTouched(): void {
|
|
105
|
+
this.onTouched();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
writeValue(value: string | null): void {
|
|
109
|
+
if (value === null) {
|
|
110
|
+
this.value.set('');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.value.set(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
registerOnChange(fn: (value: string) => void): void {
|
|
117
|
+
this.onChange = fn;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
registerOnTouched(fn: () => void): void {
|
|
121
|
+
this.onTouched = fn;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
setDisabledState(isDisabled: boolean): void {
|
|
125
|
+
this.formDisabled.set(isDisabled);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiInputPasswordComponent } from './input-password.component';
|