@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,27 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../utils/cn';
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva('inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium', {
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
primary: 'border-transparent bg-primary text-primary-foreground',
|
|
9
|
+
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
10
|
+
outline: 'border-border text-foreground',
|
|
11
|
+
destructive: 'border-transparent bg-destructive text-destructive-foreground',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type UiBadgeVariant = NonNullable<VariantProps<typeof badgeVariants>['variant']>;
|
|
17
|
+
|
|
18
|
+
@Component({
|
|
19
|
+
selector: 'ui-badge',
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
template: `<span [class]="classes()"><ng-content /></span>`,
|
|
22
|
+
host: { class: 'inline-flex' },
|
|
23
|
+
})
|
|
24
|
+
export class UiBadgeComponent {
|
|
25
|
+
readonly variant = input<UiBadgeVariant>('primary');
|
|
26
|
+
readonly classes = computed(() => cn(badgeVariants({ variant: this.variant() })));
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export interface UiBreadcrumbItem {
|
|
4
|
+
label: string;
|
|
5
|
+
href: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'ui-breadcrumb',
|
|
10
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
11
|
+
template: `
|
|
12
|
+
<nav aria-label="Miga de pan">
|
|
13
|
+
<ol class="flex flex-wrap items-center gap-2 text-sm">
|
|
14
|
+
@for (item of items(); track item.label; let last = $last) {
|
|
15
|
+
<li class="flex items-center gap-2">
|
|
16
|
+
@if (item.href.length === 0) {
|
|
17
|
+
<span class="text-foreground">{{ item.label }}</span>
|
|
18
|
+
} @else {
|
|
19
|
+
<a class="text-muted-foreground hover:text-foreground" [href]="item.href">{{ item.label }}</a>
|
|
20
|
+
}
|
|
21
|
+
@if (!last) {
|
|
22
|
+
<span class="text-muted-foreground">/</span>
|
|
23
|
+
}
|
|
24
|
+
</li>
|
|
25
|
+
}
|
|
26
|
+
</ol>
|
|
27
|
+
</nav>
|
|
28
|
+
`,
|
|
29
|
+
})
|
|
30
|
+
export class UiBreadcrumbComponent {
|
|
31
|
+
readonly items = input.required<readonly UiBreadcrumbItem[]>();
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../utils/cn';
|
|
4
|
+
|
|
5
|
+
export const buttonVariants = cva(
|
|
6
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-ui text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
11
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
12
|
+
outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground',
|
|
13
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
14
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
15
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
sm: 'h-8 px-3',
|
|
19
|
+
md: 'h-10 px-4',
|
|
20
|
+
lg: 'h-11 px-6',
|
|
21
|
+
icon: 'size-10',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export type UiButtonVariant = NonNullable<VariantProps<typeof buttonVariants>['variant']>;
|
|
28
|
+
export type UiButtonSize = NonNullable<VariantProps<typeof buttonVariants>['size']>;
|
|
29
|
+
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'ui-button',
|
|
32
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
33
|
+
template: `
|
|
34
|
+
<button [type]="type()" [disabled]="disabled()" [class]="classes()" (click)="pressed.emit($event)">
|
|
35
|
+
<ng-content />
|
|
36
|
+
</button>
|
|
37
|
+
`,
|
|
38
|
+
host: { class: 'inline-flex' },
|
|
39
|
+
})
|
|
40
|
+
export class UiButtonComponent {
|
|
41
|
+
readonly variant = input<UiButtonVariant>('primary');
|
|
42
|
+
readonly size = input<UiButtonSize>('md');
|
|
43
|
+
readonly type = input<'button' | 'submit' | 'reset'>('button');
|
|
44
|
+
readonly disabled = input(false);
|
|
45
|
+
readonly buttonClass = input('');
|
|
46
|
+
readonly pressed = output<MouseEvent>();
|
|
47
|
+
readonly classes = computed(() =>
|
|
48
|
+
cn(buttonVariants({ variant: this.variant(), size: this.size() }), this.buttonClass()),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input, model } from '@angular/core';
|
|
2
|
+
import { UiButtonComponent } from '../button/button.component';
|
|
3
|
+
|
|
4
|
+
export interface UiCarouselItem {
|
|
5
|
+
src: string;
|
|
6
|
+
alt: string;
|
|
7
|
+
title: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'ui-carousel',
|
|
12
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
13
|
+
imports: [UiButtonComponent],
|
|
14
|
+
template: `
|
|
15
|
+
<div class="overflow-hidden rounded-ui border border-border bg-card">
|
|
16
|
+
@if (current(); as item) {
|
|
17
|
+
<img class="h-64 w-full object-cover" [src]="item.src" [alt]="item.alt" />
|
|
18
|
+
<p class="px-4 py-3 text-sm font-medium">{{ item.title }}</p>
|
|
19
|
+
}
|
|
20
|
+
<div class="flex items-center justify-between border-t border-border p-2">
|
|
21
|
+
<ui-button variant="outline" size="sm" (pressed)="previous()">Anterior</ui-button>
|
|
22
|
+
<span class="text-xs text-muted-foreground">{{ positionLabel() }}</span>
|
|
23
|
+
<ui-button variant="outline" size="sm" (pressed)="next()">Siguiente</ui-button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
`,
|
|
27
|
+
})
|
|
28
|
+
export class UiCarouselComponent {
|
|
29
|
+
readonly items = input.required<readonly UiCarouselItem[]>();
|
|
30
|
+
readonly index = model(0);
|
|
31
|
+
readonly current = computed(() => {
|
|
32
|
+
const items = this.items();
|
|
33
|
+
if (items.length === 0) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const item = items[this.index()];
|
|
37
|
+
if (item === undefined) {
|
|
38
|
+
throw new Error('ui-carousel index está fuera de rango.');
|
|
39
|
+
}
|
|
40
|
+
return item;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
positionLabel(): string {
|
|
44
|
+
const total = this.items().length;
|
|
45
|
+
if (total === 0) {
|
|
46
|
+
return '0 / 0';
|
|
47
|
+
}
|
|
48
|
+
return `${this.index() + 1} / ${total}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
previous(): void {
|
|
52
|
+
const total = this.items().length;
|
|
53
|
+
if (total === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const nextIndex = this.index() - 1;
|
|
57
|
+
if (nextIndex < 0) {
|
|
58
|
+
this.index.set(total - 1);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.index.set(nextIndex);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
next(): void {
|
|
65
|
+
const total = this.items().length;
|
|
66
|
+
if (total === 0) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const nextIndex = this.index() + 1;
|
|
70
|
+
if (nextIndex >= total) {
|
|
71
|
+
this.index.set(0);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.index.set(nextIndex);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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 { uiLabelClass } from '../utils/classes';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'ui-checkbox',
|
|
15
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
|
+
template: `
|
|
17
|
+
<label class="inline-flex items-center gap-2 text-sm text-foreground">
|
|
18
|
+
<input
|
|
19
|
+
class="size-4 rounded border-input accent-primary"
|
|
20
|
+
type="checkbox"
|
|
21
|
+
[id]="resolvedId()"
|
|
22
|
+
[checked]="value()"
|
|
23
|
+
[disabled]="isDisabled()"
|
|
24
|
+
(change)="onToggle($event)"
|
|
25
|
+
(blur)="markTouched()"
|
|
26
|
+
/>
|
|
27
|
+
@if (label().length > 0) {
|
|
28
|
+
<span [class]="labelClass">{{ label() }}</span>
|
|
29
|
+
}
|
|
30
|
+
<ng-content />
|
|
31
|
+
</label>
|
|
32
|
+
`,
|
|
33
|
+
providers: [
|
|
34
|
+
{
|
|
35
|
+
provide: NG_VALUE_ACCESSOR,
|
|
36
|
+
useExisting: forwardRef(() => UiCheckboxComponent),
|
|
37
|
+
multi: true,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
export class UiCheckboxComponent implements ControlValueAccessor {
|
|
42
|
+
readonly value = model(false);
|
|
43
|
+
readonly label = input('');
|
|
44
|
+
readonly inputId = input('');
|
|
45
|
+
readonly disabled = input(false);
|
|
46
|
+
private readonly formDisabled = signal(false);
|
|
47
|
+
private readonly generatedId = crypto.randomUUID();
|
|
48
|
+
private onChange: (value: boolean) => void = () => undefined;
|
|
49
|
+
private onTouched: () => void = () => undefined;
|
|
50
|
+
readonly labelClass = uiLabelClass;
|
|
51
|
+
readonly isDisabled = computed(() => this.disabled() || this.formDisabled());
|
|
52
|
+
readonly resolvedId = computed(() => {
|
|
53
|
+
const provided = this.inputId();
|
|
54
|
+
if (provided.length > 0) {
|
|
55
|
+
return provided;
|
|
56
|
+
}
|
|
57
|
+
return this.generatedId;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
onToggle(event: Event): void {
|
|
61
|
+
const target = event.target;
|
|
62
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
63
|
+
throw new Error('ui-checkbox esperaba un HTMLInputElement.');
|
|
64
|
+
}
|
|
65
|
+
this.value.set(target.checked);
|
|
66
|
+
this.onChange(target.checked);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
markTouched(): void {
|
|
70
|
+
this.onTouched();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
writeValue(value: boolean | null): void {
|
|
74
|
+
if (value === null) {
|
|
75
|
+
this.value.set(false);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.value.set(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
registerOnChange(fn: (value: boolean) => void): void {
|
|
82
|
+
this.onChange = fn;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
registerOnTouched(fn: () => void): void {
|
|
86
|
+
this.onTouched = fn;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setDisabledState(isDisabled: boolean): void {
|
|
90
|
+
this.formDisabled.set(isDisabled);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiCheckboxComponent } from './checkbox.component';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'ui-chip',
|
|
5
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6
|
+
template: `
|
|
7
|
+
<span class="inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1 text-sm text-secondary-foreground">
|
|
8
|
+
{{ label() }}
|
|
9
|
+
@if (removable()) {
|
|
10
|
+
<button type="button" class="text-base leading-none" aria-label="Quitar" (click)="removed.emit()">×</button>
|
|
11
|
+
}
|
|
12
|
+
</span>
|
|
13
|
+
`,
|
|
14
|
+
host: { class: 'inline-flex' },
|
|
15
|
+
})
|
|
16
|
+
export class UiChipComponent {
|
|
17
|
+
readonly label = input.required<string>();
|
|
18
|
+
readonly removable = input(false);
|
|
19
|
+
readonly removed = output<void>();
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiChipComponent } from './chip.component';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input, model, output } from '@angular/core';
|
|
2
|
+
import { UiButtonComponent } from '../button/button.component';
|
|
3
|
+
import { UiDialogComponent } from '../dialog/dialog.component';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'ui-confirm-dialog',
|
|
7
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
8
|
+
imports: [UiDialogComponent, UiButtonComponent],
|
|
9
|
+
template: `
|
|
10
|
+
<ui-dialog [open]="open()" (openChange)="open.set($event)" [closable]="false">
|
|
11
|
+
<h2 class="text-lg font-semibold">{{ title() }}</h2>
|
|
12
|
+
<p class="text-sm text-muted-foreground">{{ message() }}</p>
|
|
13
|
+
<div class="flex justify-end gap-2">
|
|
14
|
+
<ui-button variant="outline" (pressed)="cancel()">{{ cancelLabel() }}</ui-button>
|
|
15
|
+
<ui-button variant="destructive" (pressed)="confirm()">{{ confirmLabel() }}</ui-button>
|
|
16
|
+
</div>
|
|
17
|
+
</ui-dialog>
|
|
18
|
+
`,
|
|
19
|
+
})
|
|
20
|
+
export class UiConfirmDialogComponent {
|
|
21
|
+
readonly open = model(false);
|
|
22
|
+
readonly title = input.required<string>();
|
|
23
|
+
readonly message = input.required<string>();
|
|
24
|
+
readonly confirmLabel = input('Confirmar');
|
|
25
|
+
readonly cancelLabel = input('Cancelar');
|
|
26
|
+
readonly confirmed = output<void>();
|
|
27
|
+
readonly cancelled = output<void>();
|
|
28
|
+
|
|
29
|
+
confirm(): void {
|
|
30
|
+
this.open.set(false);
|
|
31
|
+
this.confirmed.emit();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
cancel(): void {
|
|
35
|
+
this.open.set(false);
|
|
36
|
+
this.cancelled.emit();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiConfirmDialogComponent } from './confirm-dialog.component';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input, model, output } from '@angular/core';
|
|
2
|
+
import { UiButtonComponent } from '../button/button.component';
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'ui-confirm-popup',
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
7
|
+
imports: [UiButtonComponent],
|
|
8
|
+
template: `
|
|
9
|
+
<div class="relative inline-flex">
|
|
10
|
+
<div (click)="open.set(!open())">
|
|
11
|
+
<ng-content />
|
|
12
|
+
</div>
|
|
13
|
+
@if (open()) {
|
|
14
|
+
<div class="absolute top-full left-0 z-50 mt-2 w-64 rounded-ui border border-border bg-card p-3 text-card-foreground shadow-md">
|
|
15
|
+
<p class="text-sm">{{ message() }}</p>
|
|
16
|
+
<div class="mt-3 flex justify-end gap-2">
|
|
17
|
+
<ui-button variant="outline" size="sm" (pressed)="cancel()">{{ cancelLabel() }}</ui-button>
|
|
18
|
+
<ui-button size="sm" (pressed)="confirm()">{{ confirmLabel() }}</ui-button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
}
|
|
22
|
+
</div>
|
|
23
|
+
`,
|
|
24
|
+
})
|
|
25
|
+
export class UiConfirmPopupComponent {
|
|
26
|
+
readonly open = model(false);
|
|
27
|
+
readonly message = input.required<string>();
|
|
28
|
+
readonly confirmLabel = input('Confirmar');
|
|
29
|
+
readonly cancelLabel = input('Cancelar');
|
|
30
|
+
readonly confirmed = output<void>();
|
|
31
|
+
readonly cancelled = output<void>();
|
|
32
|
+
|
|
33
|
+
confirm(): void {
|
|
34
|
+
this.open.set(false);
|
|
35
|
+
this.confirmed.emit();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cancel(): void {
|
|
39
|
+
this.open.set(false);
|
|
40
|
+
this.cancelled.emit();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiConfirmPopupComponent } from './confirm-popup.component';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, HostListener, input, output, signal } from '@angular/core';
|
|
2
|
+
import { UiMenuComponent } from '../menu/menu.component';
|
|
3
|
+
import { type UiMenuItem } from '../utils/models';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'ui-context-menu',
|
|
7
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
8
|
+
imports: [UiMenuComponent],
|
|
9
|
+
template: `
|
|
10
|
+
<div (contextmenu)="onContextMenu($event)">
|
|
11
|
+
<ng-content />
|
|
12
|
+
@if (open()) {
|
|
13
|
+
<div class="fixed z-50" [style.left.px]="x()" [style.top.px]="y()">
|
|
14
|
+
<ui-menu [items]="items()" (selected)="choose($event)" />
|
|
15
|
+
</div>
|
|
16
|
+
}
|
|
17
|
+
</div>
|
|
18
|
+
`,
|
|
19
|
+
})
|
|
20
|
+
export class UiContextMenuComponent {
|
|
21
|
+
readonly items = input.required<readonly UiMenuItem[]>();
|
|
22
|
+
readonly selected = output<UiMenuItem>();
|
|
23
|
+
readonly open = signal(false);
|
|
24
|
+
readonly x = signal(0);
|
|
25
|
+
readonly y = signal(0);
|
|
26
|
+
|
|
27
|
+
onContextMenu(event: MouseEvent): void {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
this.x.set(event.clientX);
|
|
30
|
+
this.y.set(event.clientY);
|
|
31
|
+
this.open.set(true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
choose(item: UiMenuItem): void {
|
|
35
|
+
this.selected.emit(item);
|
|
36
|
+
this.open.set(false);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@HostListener('document:click')
|
|
40
|
+
onDocumentClick(): void {
|
|
41
|
+
this.open.set(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiContextMenuComponent } from './context-menu.component';
|
|
@@ -0,0 +1,205 @@
|
|
|
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 { cn } from '../utils/cn';
|
|
12
|
+
import { uiFieldClass, uiLabelClass } from '../utils/classes';
|
|
13
|
+
|
|
14
|
+
interface UiCalendarDay {
|
|
15
|
+
iso: string;
|
|
16
|
+
day: number;
|
|
17
|
+
inMonth: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'ui-datepicker',
|
|
22
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
23
|
+
template: `
|
|
24
|
+
<div class="relative flex w-full flex-col gap-1.5">
|
|
25
|
+
@if (label().length > 0) {
|
|
26
|
+
<label [class]="labelClass" [attr.for]="resolvedId()">{{ label() }}</label>
|
|
27
|
+
}
|
|
28
|
+
<input
|
|
29
|
+
[id]="resolvedId()"
|
|
30
|
+
[class]="fieldClass"
|
|
31
|
+
[value]="value()"
|
|
32
|
+
placeholder="AAAA-MM-DD"
|
|
33
|
+
[disabled]="isDisabled()"
|
|
34
|
+
(focus)="open.set(true)"
|
|
35
|
+
(input)="onInput($event)"
|
|
36
|
+
(blur)="markTouched()"
|
|
37
|
+
/>
|
|
38
|
+
@if (open()) {
|
|
39
|
+
<div class="absolute top-full z-50 mt-1 w-72 rounded-ui border border-border bg-card p-3 text-card-foreground shadow-md">
|
|
40
|
+
<div class="mb-3 flex items-center justify-between">
|
|
41
|
+
<button type="button" class="rounded-ui px-2 py-1 text-sm hover:bg-accent" (mousedown)="shiftMonth($event, -1)">‹</button>
|
|
42
|
+
<span class="text-sm font-medium capitalize">{{ monthTitle() }}</span>
|
|
43
|
+
<button type="button" class="rounded-ui px-2 py-1 text-sm hover:bg-accent" (mousedown)="shiftMonth($event, 1)">›</button>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="grid grid-cols-7 gap-1 text-center text-xs text-muted-foreground">
|
|
46
|
+
@for (weekday of weekdays; track weekday) {
|
|
47
|
+
<span>{{ weekday }}</span>
|
|
48
|
+
}
|
|
49
|
+
</div>
|
|
50
|
+
<div class="mt-1 grid grid-cols-7 gap-1">
|
|
51
|
+
@for (day of days(); track day.iso) {
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
[class]="dayClass(day)"
|
|
55
|
+
(mousedown)="selectDay($event, day.iso)"
|
|
56
|
+
>
|
|
57
|
+
{{ day.day }}
|
|
58
|
+
</button>
|
|
59
|
+
}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
}
|
|
63
|
+
</div>
|
|
64
|
+
`,
|
|
65
|
+
providers: [
|
|
66
|
+
{
|
|
67
|
+
provide: NG_VALUE_ACCESSOR,
|
|
68
|
+
useExisting: forwardRef(() => UiDatepickerComponent),
|
|
69
|
+
multi: true,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
export class UiDatepickerComponent implements ControlValueAccessor {
|
|
74
|
+
readonly value = model('');
|
|
75
|
+
readonly label = input('');
|
|
76
|
+
readonly inputId = input('');
|
|
77
|
+
readonly disabled = input(false);
|
|
78
|
+
readonly open = signal(false);
|
|
79
|
+
readonly visibleMonth = signal(this.startOfMonth(new Date()));
|
|
80
|
+
readonly weekdays = ['Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa', 'Do'] as const;
|
|
81
|
+
private readonly formDisabled = signal(false);
|
|
82
|
+
private readonly generatedId = crypto.randomUUID();
|
|
83
|
+
private onChange: (value: string) => void = () => undefined;
|
|
84
|
+
private onTouched: () => void = () => undefined;
|
|
85
|
+
readonly fieldClass = uiFieldClass;
|
|
86
|
+
readonly labelClass = uiLabelClass;
|
|
87
|
+
readonly isDisabled = computed(() => this.disabled() || this.formDisabled());
|
|
88
|
+
readonly resolvedId = computed(() => {
|
|
89
|
+
const provided = this.inputId();
|
|
90
|
+
if (provided.length > 0) {
|
|
91
|
+
return provided;
|
|
92
|
+
}
|
|
93
|
+
return this.generatedId;
|
|
94
|
+
});
|
|
95
|
+
readonly monthTitle = computed(() =>
|
|
96
|
+
new Intl.DateTimeFormat('es', { month: 'long', year: 'numeric' }).format(this.visibleMonth()),
|
|
97
|
+
);
|
|
98
|
+
readonly days = computed(() => {
|
|
99
|
+
const month = this.visibleMonth();
|
|
100
|
+
const mondayOffset = (month.getDay() + 6) % 7;
|
|
101
|
+
const cursor = new Date(month);
|
|
102
|
+
cursor.setDate(month.getDate() - mondayOffset);
|
|
103
|
+
const days: UiCalendarDay[] = [];
|
|
104
|
+
for (let index = 0; index < 42; index += 1) {
|
|
105
|
+
const date = new Date(cursor);
|
|
106
|
+
date.setDate(cursor.getDate() + index);
|
|
107
|
+
days.push({
|
|
108
|
+
iso: this.toIso(date),
|
|
109
|
+
day: date.getDate(),
|
|
110
|
+
inMonth: date.getMonth() === month.getMonth(),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return days;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
dayClass(day: UiCalendarDay): string {
|
|
117
|
+
return cn('h-8 rounded-ui text-sm hover:bg-accent', {
|
|
118
|
+
'text-foreground': day.inMonth,
|
|
119
|
+
'text-muted-foreground': !day.inMonth,
|
|
120
|
+
'bg-primary text-primary-foreground': day.iso === this.value(),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
onInput(event: Event): void {
|
|
125
|
+
const target = event.target;
|
|
126
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
127
|
+
throw new Error('ui-datepicker esperaba un HTMLInputElement.');
|
|
128
|
+
}
|
|
129
|
+
this.commit(target.value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
selectDay(event: MouseEvent, iso: string): void {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
this.commit(iso);
|
|
135
|
+
this.open.set(false);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
shiftMonth(event: MouseEvent, amount: number): void {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
const current = this.visibleMonth();
|
|
141
|
+
this.visibleMonth.set(new Date(current.getFullYear(), current.getMonth() + amount, 1));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
markTouched(): void {
|
|
145
|
+
this.onTouched();
|
|
146
|
+
this.open.set(false);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
writeValue(value: string | null): void {
|
|
150
|
+
if (value === null || value.length === 0) {
|
|
151
|
+
this.value.set('');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.value.set(value);
|
|
155
|
+
this.visibleMonth.set(this.startOfMonth(this.parseIso(value)));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
registerOnChange(fn: (value: string) => void): void {
|
|
159
|
+
this.onChange = fn;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
registerOnTouched(fn: () => void): void {
|
|
163
|
+
this.onTouched = fn;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setDisabledState(isDisabled: boolean): void {
|
|
167
|
+
this.formDisabled.set(isDisabled);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private commit(value: string): void {
|
|
171
|
+
if (value.length === 0) {
|
|
172
|
+
this.value.set('');
|
|
173
|
+
this.onChange('');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.parseIso(value);
|
|
177
|
+
this.value.set(value);
|
|
178
|
+
this.onChange(value);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private startOfMonth(date: Date): Date {
|
|
182
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private toIso(date: Date): string {
|
|
186
|
+
const year = String(date.getFullYear());
|
|
187
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
188
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
189
|
+
return `${year}-${month}-${day}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private parseIso(value: string): Date {
|
|
193
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
194
|
+
if (match === null) {
|
|
195
|
+
throw new Error('ui-datepicker esperaba una fecha YYYY-MM-DD.');
|
|
196
|
+
}
|
|
197
|
+
const yearText = match[1];
|
|
198
|
+
const monthText = match[2];
|
|
199
|
+
const dayText = match[3];
|
|
200
|
+
if (yearText === undefined || monthText === undefined || dayText === undefined) {
|
|
201
|
+
throw new Error('ui-datepicker esperaba una fecha YYYY-MM-DD.');
|
|
202
|
+
}
|
|
203
|
+
return new Date(Number(yearText), Number(monthText) - 1, Number(dayText));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiDatepickerComponent } from './datepicker.component';
|