tailjng 0.0.13 → 0.0.15
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/cli/component-manager.js +45 -0
- package/cli/dependency-manager.js +52 -0
- package/cli/file-operations.js +88 -0
- package/cli/index.js +51 -0
- package/cli/settings/colors.js +17 -0
- package/cli/settings/components-list.js +87 -0
- package/cli/settings/header-generator.js +42 -0
- package/cli/settings/path-utils.js +50 -0
- package/cli/settings/prompt-utils.js +37 -0
- package/cli/settings/tailwind-check.js +21 -0
- package/fesm2022/tailjng.mjs +903 -25
- package/fesm2022/tailjng.mjs.map +1 -1
- package/lib/config/tailjng-config.token.d.ts +3 -0
- package/lib/interfaces/alert/dialog-alert.interface.d.ts +52 -0
- package/lib/interfaces/alert/toast-alert.interface.d.ts +52 -0
- package/lib/interfaces/config.interface.d.ts +5 -0
- package/lib/interfaces/crud/api-response.d.ts +29 -0
- package/lib/interfaces/crud/crud.interface.d.ts +103 -0
- package/lib/services/alert/dialog-alert.service.d.ts +24 -0
- package/lib/services/alert/toast-alert.service.d.ts +26 -0
- package/lib/services/crud/converter-crud.service.d.ts +41 -0
- package/lib/services/crud/generic-crud.service.d.ts +81 -0
- package/lib/services/http/error-handler-http.service.d.ts +26 -0
- package/lib/services/http/params-http.service.d.ts +13 -0
- package/lib/services/static/icons.service.d.ts +31 -0
- package/lib/services/transformer/calendar.service.d.ts +71 -0
- package/package.json +5 -3
- package/public-api.d.ts +10 -3
- package/src/lib/components/alert/dialog-alert/dialog-alert.component.css +0 -0
- package/src/lib/components/alert/dialog-alert/dialog-alert.component.html +72 -0
- package/src/lib/components/alert/dialog-alert/dialog-alert.component.ts +66 -0
- package/src/lib/components/alert/toast-alert/toast-alert.component.css +5 -0
- package/src/lib/components/alert/toast-alert/toast-alert.component.html +76 -0
- package/src/lib/components/alert/toast-alert/toast-alert.component.ts +87 -0
- package/src/lib/components/button/button.component.css +0 -0
- package/src/lib/components/button/button.component.html +36 -0
- package/src/lib/components/button/button.component.ts +95 -0
- package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.css +0 -0
- package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.html +23 -0
- package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.ts +44 -0
- package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.css +0 -0
- package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.html +26 -0
- package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.ts +29 -0
- package/src/lib/components/color/colors.service.ts +109 -0
- package/src/lib/components/dialog/dialog.component.css +8 -0
- package/src/lib/components/dialog/dialog.component.html +57 -0
- package/src/lib/components/dialog/dialog.component.ts +179 -0
- package/src/lib/components/image/viewer-image/viewer-image.component.css +4 -0
- package/src/lib/components/image/viewer-image/viewer-image.component.html +75 -0
- package/src/lib/components/image/viewer-image/viewer-image.component.ts +131 -0
- package/src/lib/components/input/file-input/file-input.component.css +0 -0
- package/src/lib/components/input/file-input/file-input.component.html +49 -0
- package/src/lib/components/input/file-input/file-input.component.ts +218 -0
- package/src/lib/components/input/input/input.component.css +0 -0
- package/src/lib/components/input/input/input.component.html +24 -0
- package/src/lib/components/input/input/input.component.ts +78 -0
- package/src/lib/components/input/range-input/range-input.component.css +0 -0
- package/src/lib/components/input/range-input/range-input.component.html +64 -0
- package/src/lib/components/input/range-input/range-input.component.ts +78 -0
- package/src/lib/components/input/textarea-input/textarea-input.component.css +0 -0
- package/src/lib/components/input/textarea-input/textarea-input.component.html +21 -0
- package/src/lib/components/input/textarea-input/textarea-input.component.ts +75 -0
- package/src/lib/components/label/label.component.html +1 -1
- package/src/lib/components/label/label.component.ts +1 -1
- package/src/lib/components/mode-toggle/mode-toggle.component.css +0 -0
- package/src/lib/components/mode-toggle/mode-toggle.component.html +8 -0
- package/src/lib/components/mode-toggle/mode-toggle.component.ts +61 -0
- package/src/lib/components/progress-bar/progress-bar.component.css +0 -0
- package/src/lib/components/progress-bar/progress-bar.component.html +22 -0
- package/src/lib/components/progress-bar/progress-bar.component.ts +20 -0
- package/src/lib/components/select/dropdown/dropdown.component.css +0 -0
- package/src/lib/components/select/dropdown/dropdown.component.html +95 -0
- package/src/lib/components/select/dropdown/dropdown.component.ts +562 -0
- package/src/lib/components/select/multi-dropdown/multi-dropdown.component.css +0 -0
- package/src/lib/components/select/multi-dropdown/multi-dropdown.component.html +87 -0
- package/src/lib/components/select/multi-dropdown/multi-dropdown.component.ts +315 -0
- package/src/lib/components/select/multi-table/multi-table.component.css +0 -0
- package/src/lib/components/select/multi-table/multi-table.component.html +83 -0
- package/src/lib/components/select/multi-table/multi-table.component.ts +230 -0
- package/src/lib/components/toggle-radio/toggle-radio.component.css +0 -0
- package/src/lib/components/toggle-radio/toggle-radio.component.html +51 -0
- package/src/lib/components/toggle-radio/toggle-radio.component.ts +203 -0
- package/src/styles.css +126 -0
- package/cli/tailjng.js +0 -105
- package/lib/services/icons.service.d.ts +0 -9
- package/lib/tailjng.component.d.ts +0 -5
- package/lib/tailjng.service.d.ts +0 -6
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div class="flex flex-col items-center justify-center gap-2">
|
|
2
|
+
@if (title) {
|
|
3
|
+
<span class="text-[8px] opacity-80">{{title}}</span>
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
@if (!isLoading) {
|
|
7
|
+
<div onKeyPress
|
|
8
|
+
class="relative inline-block min-w-[40px] w-[40px] h-[20px] cursor-pointer"
|
|
9
|
+
(click)="toggleSwitch(isChecked)"
|
|
10
|
+
>
|
|
11
|
+
<!-- Slider background -->
|
|
12
|
+
<div class="absolute inset-0 rounded-full transition-all duration-300"
|
|
13
|
+
[ngClass]="isChecked ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-500'">
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- Circle -->
|
|
17
|
+
<div
|
|
18
|
+
class="absolute top-[2px] left-[2px] bg-white rounded-full w-[16px] h-[16px] shadow-md transition-all duration-300"
|
|
19
|
+
[class]="classes"
|
|
20
|
+
[ngStyle]="{'transform': isChecked ? 'translateX(20px)' : 'translateX(0)'}">
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
} @else {
|
|
24
|
+
<lucide-icon [name]="iconsService.icons.loading" size="20" class="min-w-[40px] w-[40px] text-dark-primary dark:text-white animate-spin"></lucide-icon>
|
|
25
|
+
}
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Component, Input } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { LucideAngularModule } from 'lucide-angular';
|
|
4
|
+
import { JIconsService } from 'tailjng';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'JSwitchCheckbox',
|
|
8
|
+
imports: [CommonModule, LucideAngularModule],
|
|
9
|
+
templateUrl: './switch-checkbox.component.html',
|
|
10
|
+
styleUrl: './switch-checkbox.component.css'
|
|
11
|
+
})
|
|
12
|
+
export class JSwitchCheckboxComponent {
|
|
13
|
+
|
|
14
|
+
@Input() title!: string;
|
|
15
|
+
|
|
16
|
+
@Input() disabled?: boolean;
|
|
17
|
+
@Input() classes: string = '';
|
|
18
|
+
@Input() isLoading?: boolean;
|
|
19
|
+
|
|
20
|
+
@Input() isChecked: boolean = false;
|
|
21
|
+
|
|
22
|
+
// Funciones
|
|
23
|
+
@Input() toggleSwitch: (isChecked: boolean) => void = () => { };
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
public readonly iconsService: JIconsService
|
|
27
|
+
) { }
|
|
28
|
+
|
|
29
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Injectable({
|
|
4
|
+
providedIn: 'root',
|
|
5
|
+
})
|
|
6
|
+
export class JColorsService {
|
|
7
|
+
|
|
8
|
+
// Variants
|
|
9
|
+
variants: { [key: string]: string } = {
|
|
10
|
+
primary: 'bg-primary dark:bg-dark-primary text-white dark:text-white hover:bg-dark-primary dark:hover:bg-dark-primary/60 dark:hover:text-white',
|
|
11
|
+
primary_secondary: 'bg-none text-dark-border dark:text-border border-dark-border dark:border-border hover:bg-dark-border/10 dark:hover:bg-border/10 shadow-md',
|
|
12
|
+
secondary: 'bg-background dark:bg-dark-background text-black dark:text-white hover:bg-accent dark:hover:bg-dark-accent/50',
|
|
13
|
+
success: 'bg-green-500 hover:bg-green-600 text-white border border-green-500 dark:border-green-600 shadow-md',
|
|
14
|
+
success_secondary: 'bg-none text-green-500 border-green-500 dark:border-green-600 hover:bg-green-500/10 dark:hover:bg-green-600/10 shadow-md',
|
|
15
|
+
info: 'bg-blue-500 hover:bg-blue-600 text-white border border-blue-500 dark:border-blue-600 shadow-md',
|
|
16
|
+
info_secondary: 'bg-none text-blue-500 border-blue-500 dark:border-blue-600 hover:bg-blue-500/10 dark:hover:bg-blue-600/10 shadow-md',
|
|
17
|
+
warning: 'bg-yellow-600 hover:bg-yellow-700 text-white border border-yellow-600 dark:border-yellow-700 shadow-md',
|
|
18
|
+
warning_secondary: 'bg-none text-yellow-600 border-yellow-600 dark:border-yellow-700 hover:bg-yellow-600/10 dark:hover:bg-yellow-700/10 shadow-md',
|
|
19
|
+
question: 'bg-purple-500 hover:bg-purple-600 text-white border border-purple-500 dark:border-purple-600 shadow-md',
|
|
20
|
+
question_secondary: 'bg-none text-purple-500 border-purple-500 dark:border-purple-600 hover:bg-purple-500/10 dark:hover:bg-purple-600/10 shadow-md',
|
|
21
|
+
error: 'bg-red-500 hover:bg-red-600 text-white border border-red-500 dark:border-red-600 shadow-md',
|
|
22
|
+
error_secondary: 'bg-none text-red-500 border-red-500 dark:border-red-600 hover:bg-red-500/10 dark:hover:bg-red-600/10 shadow-md',
|
|
23
|
+
loading: 'bg-gray-500 hover:bg-gray-600 text-white border border-gray-500 dark:border-gray-600 shadow-md',
|
|
24
|
+
loading_secondary: 'bg-none text-gray-500 border-gray-500 dark:border-gray-600 hover:bg-gray-500/10 dark:hover:bg-gray-600/10 shadow-md',
|
|
25
|
+
|
|
26
|
+
orange: 'bg-orange-500 hover:bg-orange-600 text-white border border-orange-500 dark:border-orange-600 shadow-md',
|
|
27
|
+
orange_secondary: 'bg-none text-orange-500 border-orange-500 dark:border-orange-600 hover:bg-orange-500/10 dark:hover:bg-orange-600/10 shadow-md',
|
|
28
|
+
cyan: 'bg-cyan-500 hover:bg-cyan-600 text-white border border-cyan-500 dark:border-cyan-600 shadow-md',
|
|
29
|
+
cyan_secondary: 'bg-none text-cyan-500 border-cyan-500 dark:border-cyan-600 hover:bg-cyan-500/10 dark:hover:bg-cyan-600/10 shadow-md',
|
|
30
|
+
purple: 'bg-purple-500 hover:bg-purple-600 text-white border border-purple-500 dark:border-purple-600 shadow-md',
|
|
31
|
+
purple_secondary: 'bg-none text-purple-500 border-purple-500 dark:border-purple-600 hover:bg-purple-500/10 dark:hover:bg-purple-600/10 shadow-md',
|
|
32
|
+
teal: 'bg-teal-500 hover:bg-teal-600 text-white border border-teal-500 dark:border-teal-600 shadow-md',
|
|
33
|
+
teal_secondary: 'bg-none text-teal-500 border-teal-500 dark:border-teal-600 hover:bg-teal-500/10 dark:hover:bg-teal-600/10 shadow-md',
|
|
34
|
+
pink: 'bg-pink-500 hover:bg-pink-600 text-white border border-pink-500 dark:border-pink-600 shadow-md',
|
|
35
|
+
pink_secondary: 'bg-none text-pink-500 border-pink-500 dark:border-pink-600 hover:bg-pink-500/10 dark:hover:bg-pink-600/10 shadow-md',
|
|
36
|
+
green: 'bg-green-500 hover:bg-green-600 text-white border border-green-500 dark:border-green-600 shadow-md',
|
|
37
|
+
green_secondary: 'bg-none text-green-500 border-green-500 dark:border-green-600 hover:bg-green-500/10 dark:hover:bg-green-600/10 shadow-md',
|
|
38
|
+
|
|
39
|
+
default: ' text-black dark:text-white shadow-md',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Alerts
|
|
43
|
+
getAlertClass(type: string, monocromatic: boolean) {
|
|
44
|
+
if (!monocromatic) {
|
|
45
|
+
switch (type) {
|
|
46
|
+
case 'success': return 'border-green-500 bg-green-50 dark:bg-[#15241f]';
|
|
47
|
+
case 'error': return 'border-red-500 bg-red-50 dark:bg-[#21181c]';
|
|
48
|
+
case 'warning': return 'border-yellow-500 bg-yellow-50 dark:bg-[#1f1c1a]';
|
|
49
|
+
case 'info': return 'border-blue-500 bg-blue-50 dark:bg-[#1a1a24]';
|
|
50
|
+
case 'question': return 'border-purple-500 bg-purple-50 dark:bg-[#241732]';
|
|
51
|
+
case 'loading': return 'border-gray-500 bg-gray-50 dark:bg-[#15181e]';
|
|
52
|
+
default: return 'border-gray-500';
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
return 'bg-white dark:bg-foreground border-border dark:border-dark-border';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Icons
|
|
60
|
+
getIconClass(type: string, monocromatic: boolean) {
|
|
61
|
+
if (!monocromatic) {
|
|
62
|
+
switch (type) {
|
|
63
|
+
case 'success': return 'text-green-500';
|
|
64
|
+
case 'error': return 'text-red-500';
|
|
65
|
+
case 'warning': return 'text-yellow-500';
|
|
66
|
+
case 'info': return 'text-blue-500';
|
|
67
|
+
case 'question': return 'text-purple-500';
|
|
68
|
+
case 'loading': return 'text-gray-500';
|
|
69
|
+
default: return 'text-primary';
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
return 'text-primary';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Buttons primary
|
|
77
|
+
getButtonClass(type: string, monocromatic: boolean): { [key: string]: boolean } {
|
|
78
|
+
if (!monocromatic) {
|
|
79
|
+
switch (type) {
|
|
80
|
+
case 'success': return { 'success': true };
|
|
81
|
+
case 'error': return { 'error': true };
|
|
82
|
+
case 'warning': return { 'warning': true };
|
|
83
|
+
case 'info': return { 'info': true };
|
|
84
|
+
case 'question': return { 'question': true };
|
|
85
|
+
case 'loading': return { 'loading': true };
|
|
86
|
+
default: return { 'primary': true };
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
return { 'primary': true };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Buttons secondary
|
|
94
|
+
getButtonSecondaryClass(type: string, monocromatic: boolean): { [key: string]: boolean } {
|
|
95
|
+
if (!monocromatic) {
|
|
96
|
+
switch (type) {
|
|
97
|
+
case 'success': return { 'success_secondary': true };
|
|
98
|
+
case 'error': return { 'error_secondary': true };
|
|
99
|
+
case 'warning': return { 'warning_secondary': true };
|
|
100
|
+
case 'info': return { 'info_secondary': true };
|
|
101
|
+
case 'question': return { 'question_secondary': true };
|
|
102
|
+
case 'loading': return { 'loading_secondary': true };
|
|
103
|
+
default: return { 'secondary': true };
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
return { 'secondary': true };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
@if (openModal) {
|
|
2
|
+
<!-- Overlay -->
|
|
3
|
+
@if (overlay) {
|
|
4
|
+
<div class="fixed inset-0 z-[999] bg-black/50"></div>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
<!-- Modal -->
|
|
8
|
+
<div class="fixed inset-0 z-[1000] flex pointer-events-none" [ngClass]="getPositionClass()">
|
|
9
|
+
|
|
10
|
+
<div @modalTransition
|
|
11
|
+
class="pointer-events-auto bg-white dark:bg-foreground rounded-[12px] shadow-lg border-2 border-border dark:border-dark-border"
|
|
12
|
+
[ngStyle]="getOffsetStyles()"
|
|
13
|
+
data-draggable-dialog>
|
|
14
|
+
|
|
15
|
+
<!-- Header draggable -->
|
|
16
|
+
@if (draggable) {
|
|
17
|
+
<div class="flex p-1 pl-4 pr-4 justify-between items-center bg-primary dark:bg-dark-primary border-b border-border dark:border-dark-border rounded-[10px] font-semibold text-2sm cursor-move select-none"
|
|
18
|
+
(mousedown)="$event.stopPropagation(); startDrag($event)">
|
|
19
|
+
<h3 class="text-[1em] font-semibold text-white leading-none">{{ title }}</h3>
|
|
20
|
+
|
|
21
|
+
<button type="button" (click)="$event.stopPropagation(); onClose()"
|
|
22
|
+
class="p-2 rounded-full border border-border dark:border-dark-border text-white hover:bg-dark-background focus:outline-none cursor-pointer">
|
|
23
|
+
<lucide-icon [name]="iconsService.icons.close" size="16"></lucide-icon>
|
|
24
|
+
</button>
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
<!-- Header normal -->
|
|
30
|
+
@if (!draggable) {
|
|
31
|
+
<div class="flex p-1 pl-4 pr-4 justify-between items-center bg-primary dark:bg-dark-primary border-b border-border dark:border-dark-border rounded-[10px] font-semibold text-2sm cursor-normal select-none">
|
|
32
|
+
<h3 class="text-[1em] font-semibold text-white leading-none">{{ title }}</h3>
|
|
33
|
+
<button type="button" (click)="onClose()"
|
|
34
|
+
class="p-2 rounded-full border border-border dark:border-dark-border text-white hover:bg-dark-background focus:outline-none cursor-pointer">
|
|
35
|
+
<lucide-icon [name]="iconsService.icons.close" size="16"></lucide-icon>
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
<!-- Content -->
|
|
41
|
+
<div class="m-2"
|
|
42
|
+
[ngClass]="{ 'jdialog-full': isFullScreen() }"
|
|
43
|
+
[ngStyle]="{
|
|
44
|
+
width: getModalWidth(),
|
|
45
|
+
height: getModalHeight(),
|
|
46
|
+
'min-width': !isFullScreen() ? '200px' : null,
|
|
47
|
+
'min-height': !isFullScreen() ? '40px' : null
|
|
48
|
+
}">
|
|
49
|
+
@if (dialogTemplate) {
|
|
50
|
+
<ng-container [ngTemplateOutlet]="dialogTemplate"></ng-container>
|
|
51
|
+
}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
}
|
|
57
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Component, Input, TemplateRef, CUSTOM_ELEMENTS_SCHEMA, Output, EventEmitter, HostListener, OnChanges, SimpleChanges } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { trigger, transition, style, animate } from '@angular/animations';
|
|
4
|
+
import { LucideAngularModule } from 'lucide-angular';
|
|
5
|
+
import { JIconsService } from 'tailjng';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'JDialog',
|
|
9
|
+
imports: [LucideAngularModule, CommonModule],
|
|
10
|
+
templateUrl: './dialog.component.html',
|
|
11
|
+
styleUrl: './dialog.component.css',
|
|
12
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
13
|
+
animations: [
|
|
14
|
+
trigger('modalTransition', [
|
|
15
|
+
transition(':enter', [
|
|
16
|
+
style({ transform: 'translateY(1rem)', opacity: 0 }),
|
|
17
|
+
animate('300ms ease-out', style({ transform: 'translateY(0)', opacity: 1 }))
|
|
18
|
+
]),
|
|
19
|
+
transition(':leave', [
|
|
20
|
+
animate('150ms ease-in', style({ transform: 'translateY(1rem)', opacity: 0 }))
|
|
21
|
+
])
|
|
22
|
+
])
|
|
23
|
+
]
|
|
24
|
+
})
|
|
25
|
+
export class JDialogComponent implements OnChanges {
|
|
26
|
+
|
|
27
|
+
@Input() position:
|
|
28
|
+
| 'center'
|
|
29
|
+
| 'leftCenter'
|
|
30
|
+
| 'rightCenter'
|
|
31
|
+
| 'topCenter'
|
|
32
|
+
| 'bottomCenter'
|
|
33
|
+
| 'leftTop'
|
|
34
|
+
| 'leftBottom'
|
|
35
|
+
| 'rightTop'
|
|
36
|
+
| 'rightBottom' = 'center';
|
|
37
|
+
|
|
38
|
+
@Input() offset: { top?: number, bottom?: number, left?: number, right?: number } = {};
|
|
39
|
+
|
|
40
|
+
@Input() openModal = false;
|
|
41
|
+
@Output() closeModal = new EventEmitter<void>();
|
|
42
|
+
|
|
43
|
+
@Input() title = 'Dialog Title';
|
|
44
|
+
@Input() dialogTemplate!: TemplateRef<any>;
|
|
45
|
+
|
|
46
|
+
@Input() width: number | 'auto' | 'full' = 500;
|
|
47
|
+
@Input() height: number | 'auto' | 'full' = 300;
|
|
48
|
+
|
|
49
|
+
@Input() overlay: boolean = true;
|
|
50
|
+
@Input() draggable: boolean = false;
|
|
51
|
+
|
|
52
|
+
private isDragging = false;
|
|
53
|
+
private hasMoved = false;
|
|
54
|
+
private dragOffset = { x: 0, y: 0 };
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
public readonly iconsService: JIconsService
|
|
58
|
+
) { }
|
|
59
|
+
|
|
60
|
+
ngOnChanges(changes: SimpleChanges) {
|
|
61
|
+
if (changes['openModal']?.currentValue === true) {
|
|
62
|
+
this.hasMoved = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onOpen() {
|
|
67
|
+
this.hasMoved = false;
|
|
68
|
+
this.openModal = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
onClose() {
|
|
72
|
+
this.closeModal.emit();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getModalWidth(): string {
|
|
76
|
+
if (this.width === 'auto') return 'auto';
|
|
77
|
+
if (typeof this.width === 'number') return `${this.width}px`;
|
|
78
|
+
if (this.width === 'full') return '90vw';
|
|
79
|
+
return `${this.width || 100}px`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getModalHeight(): string {
|
|
83
|
+
if (this.height === 'auto') return 'auto';
|
|
84
|
+
if (typeof this.height === 'number') return `${this.height}px`;
|
|
85
|
+
if (this.height === 'full') return '90vh';
|
|
86
|
+
return `${this.height || 40}px`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
isFullScreen(): boolean {
|
|
91
|
+
return this.width === 'full' || this.height === 'full';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@HostListener('document:keydown.escape', ['$event'])
|
|
95
|
+
handleEscape(event: KeyboardEvent) {
|
|
96
|
+
if (this.openModal) {
|
|
97
|
+
this.onClose();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getPositionClass(): string {
|
|
102
|
+
switch (this.position) {
|
|
103
|
+
case 'leftCenter': return 'justify-start items-center';
|
|
104
|
+
case 'rightCenter': return 'justify-end items-center';
|
|
105
|
+
case 'topCenter': return 'justify-center items-start';
|
|
106
|
+
case 'bottomCenter': return 'justify-center items-end';
|
|
107
|
+
case 'leftTop': return 'justify-start items-start';
|
|
108
|
+
case 'leftBottom': return 'justify-start items-end';
|
|
109
|
+
case 'rightTop': return 'justify-end items-start';
|
|
110
|
+
case 'rightBottom': return 'justify-end items-end';
|
|
111
|
+
case 'center':
|
|
112
|
+
default: return 'justify-center items-center';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getOffsetStyles(): { [key: string]: string } {
|
|
117
|
+
return {
|
|
118
|
+
marginTop: this.offset.top !== undefined ? `${this.offset.top}px` : '',
|
|
119
|
+
marginBottom: this.offset.bottom !== undefined ? `${this.offset.bottom}px` : '',
|
|
120
|
+
marginLeft: this.offset.left !== undefined ? `${this.offset.left}px` : '',
|
|
121
|
+
marginRight: this.offset.right !== undefined ? `${this.offset.right}px` : '',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
startDrag(event: MouseEvent) {
|
|
126
|
+
if (!this.draggable) return;
|
|
127
|
+
|
|
128
|
+
this.isDragging = true;
|
|
129
|
+
|
|
130
|
+
const dialogElement = (event.currentTarget as HTMLElement).closest('[data-draggable-dialog]') as HTMLElement;
|
|
131
|
+
if (!dialogElement) return;
|
|
132
|
+
|
|
133
|
+
const rect = dialogElement.getBoundingClientRect();
|
|
134
|
+
this.dragOffset = {
|
|
135
|
+
x: event.clientX - rect.left,
|
|
136
|
+
y: event.clientY - rect.top
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Only set position if it hasn't been moved before (respects initial offset)
|
|
140
|
+
if (!this.hasMoved) {
|
|
141
|
+
const computedTop = this.offset.top ?? rect.top;
|
|
142
|
+
const computedLeft = rect.left;
|
|
143
|
+
|
|
144
|
+
dialogElement.style.position = 'fixed';
|
|
145
|
+
dialogElement.style.margin = '0';
|
|
146
|
+
dialogElement.style.transform = 'none';
|
|
147
|
+
dialogElement.style.zIndex = '1001';
|
|
148
|
+
dialogElement.style.top = `${computedTop}px`;
|
|
149
|
+
dialogElement.style.left = `${computedLeft}px`;
|
|
150
|
+
|
|
151
|
+
this.hasMoved = true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const mouseMoveHandler = (moveEvent: MouseEvent) => {
|
|
155
|
+
if (!this.isDragging) return;
|
|
156
|
+
|
|
157
|
+
const newLeft = moveEvent.clientX - this.dragOffset.x;
|
|
158
|
+
const newTop = moveEvent.clientY - this.dragOffset.y;
|
|
159
|
+
|
|
160
|
+
const maxLeft = window.innerWidth - dialogElement.offsetWidth;
|
|
161
|
+
const maxTop = window.innerHeight - dialogElement.offsetHeight;
|
|
162
|
+
|
|
163
|
+
dialogElement.style.left = `${Math.min(Math.max(newLeft, 0), maxLeft)}px`;
|
|
164
|
+
dialogElement.style.top = `${Math.min(Math.max(newTop, 0), maxTop)}px`;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const mouseUpHandler = () => {
|
|
168
|
+
this.isDragging = false;
|
|
169
|
+
document.removeEventListener('mousemove', mouseMoveHandler);
|
|
170
|
+
document.removeEventListener('mouseup', mouseUpHandler);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
document.addEventListener('mousemove', mouseMoveHandler);
|
|
174
|
+
document.addEventListener('mouseup', mouseUpHandler);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<div class="relative w-full h-full" #container>
|
|
2
|
+
|
|
3
|
+
<!-- Control buttons -->
|
|
4
|
+
<div class="absolute flex gap-1 z-2" [ngClass]="{ 'top-3 right-3': isFullscreen, 'top-0 right-0': !isFullscreen }">
|
|
5
|
+
<JButton (clicked)="rotateLeftImg()" classes="secondary w-[35px] h-[35px]">
|
|
6
|
+
<lucide-icon [name]="iconsService.icons.rotateLeft" size="20" />
|
|
7
|
+
</JButton>
|
|
8
|
+
|
|
9
|
+
<JButton (clicked)="rotateRightImg()" classes="secondary w-[35px] h-[35px]">
|
|
10
|
+
<lucide-icon [name]="iconsService.icons.rotateRight" size="20" />
|
|
11
|
+
</JButton>
|
|
12
|
+
|
|
13
|
+
<JButton (clicked)="toggleFullscreen(container)" classes="secondary w-[35px] h-[35px]">
|
|
14
|
+
@if (isFullscreen) {
|
|
15
|
+
<lucide-icon [name]="iconsService.icons.exitFullscreen" size="20" />
|
|
16
|
+
} @else {
|
|
17
|
+
<lucide-icon [name]="iconsService.icons.fullscreen" size="20" />
|
|
18
|
+
}
|
|
19
|
+
</JButton>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="absolute flex gap-1 z-2" [ngClass]="{ 'top-13 right-3': isFullscreen, 'top-10 right-0': !isFullscreen }">
|
|
23
|
+
<JButton (clicked)="reset()" classes="secondary w-[35px] h-[35px]">
|
|
24
|
+
<lucide-icon [name]="iconsService.icons.reset" size="20" />
|
|
25
|
+
</JButton>
|
|
26
|
+
|
|
27
|
+
<JButton [disabled]="zoom === 3" (clicked)="zoomIn()" classes="secondary w-[35px] h-[35px]">
|
|
28
|
+
<lucide-icon [name]="iconsService.icons.zoomIn" size="20" />
|
|
29
|
+
</JButton>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="absolute flex gap-1 z-2" [ngClass]="{ 'top-23 right-3': isFullscreen, 'top-20 right-0': !isFullscreen }">
|
|
33
|
+
<JButton [disabled]="zoom === 0.5" (clicked)="zoomOut()" classes="secondary w-[35px] h-[35px]">
|
|
34
|
+
<lucide-icon [name]="iconsService.icons.zoomOut" size="20" />
|
|
35
|
+
</JButton>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Container image -->
|
|
39
|
+
<div class="flex justify-center items-center w-full h-full select-none" [class.fullscreen]="isFullscreen">
|
|
40
|
+
<div class="relative w-full h-full overflow-hidden">
|
|
41
|
+
@if (!hasError) {
|
|
42
|
+
<img
|
|
43
|
+
[src]="src"
|
|
44
|
+
[alt]="alt"
|
|
45
|
+
(load)="handleLoad()"
|
|
46
|
+
(error)="handleError()"
|
|
47
|
+
(mousedown)="startDrag($event)"
|
|
48
|
+
[style.transform]="'scale(' + zoom + ') rotate(' + rotate + 'deg) translate(' + posX + 'px,' + posY + 'px)'"
|
|
49
|
+
[style.objectFit]="objectFit"
|
|
50
|
+
[style.cursor]="zoom > 1 ? 'grab' : 'default'"
|
|
51
|
+
[class.invisible]="loading"
|
|
52
|
+
[class.transition-transform]="animateTransform"
|
|
53
|
+
[class.duration-200]="animateTransform"
|
|
54
|
+
[class.ease-in-out]="animateTransform"
|
|
55
|
+
class="w-full h-full object-contain pointer-events-auto select-none"
|
|
56
|
+
/>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@if (loading && !hasError) {
|
|
60
|
+
<div class="absolute flex flex-col gap-3 inset-0 items-center justify-center bg-white/70 dark:bg-black/40">
|
|
61
|
+
<lucide-icon [name]="iconsService.icons.loading" size="30" class="text-primary animate-spin"></lucide-icon>
|
|
62
|
+
<span class="text-sm text-gray-500">Cargando imagen...</span>
|
|
63
|
+
</div>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@if (hasError) {
|
|
67
|
+
<div class="absolute flex flex-col gap-3 inset-0 items-center justify-center">
|
|
68
|
+
<lucide-icon [name]="iconsService.icons.imageOff" size="70" class="text-red-500" />
|
|
69
|
+
<span class="text-sm text-red-500">No se pudo cargar la imagen</span>
|
|
70
|
+
</div>
|
|
71
|
+
}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { SafeUrl } from '@angular/platform-browser';
|
|
4
|
+
import { LucideAngularModule } from 'lucide-angular';
|
|
5
|
+
import { JIconsService } from 'tailjng';
|
|
6
|
+
import { JButtonComponent } from '../../button/button.component';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'JViewerImage',
|
|
10
|
+
imports: [CommonModule, LucideAngularModule, JButtonComponent],
|
|
11
|
+
templateUrl: './viewer-image.component.html',
|
|
12
|
+
styleUrl: './viewer-image.component.css'
|
|
13
|
+
})
|
|
14
|
+
export class JViewerImageComponent implements OnChanges {
|
|
15
|
+
|
|
16
|
+
@Input() src!: string | SafeUrl;
|
|
17
|
+
@Input() alt: string = 'Imagen';
|
|
18
|
+
|
|
19
|
+
@Input() width?: number;
|
|
20
|
+
@Input() height?: number;
|
|
21
|
+
|
|
22
|
+
@Input() objectFit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down' = 'contain';
|
|
23
|
+
|
|
24
|
+
posX = 0;
|
|
25
|
+
posY = 0;
|
|
26
|
+
|
|
27
|
+
zoom = 1;
|
|
28
|
+
rotate = 0;
|
|
29
|
+
|
|
30
|
+
isFullscreen = false;
|
|
31
|
+
internalLoading = true;
|
|
32
|
+
animateTransform = true;
|
|
33
|
+
hasError = false;
|
|
34
|
+
|
|
35
|
+
private dragging = false;
|
|
36
|
+
private dragStart = { x: 0, y: 0 };
|
|
37
|
+
|
|
38
|
+
constructor(public readonly iconsService: JIconsService) { }
|
|
39
|
+
|
|
40
|
+
ngOnChanges(changes: SimpleChanges): void {
|
|
41
|
+
if (changes['src'] && changes['src'].currentValue !== changes['src'].previousValue) {
|
|
42
|
+
this.internalLoading = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
handleLoad() {
|
|
47
|
+
this.internalLoading = false;
|
|
48
|
+
this.hasError = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
handleError() {
|
|
52
|
+
this.internalLoading = false;
|
|
53
|
+
this.hasError = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get loading(): boolean {
|
|
57
|
+
return this.internalLoading;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toggleFullscreen(container: HTMLElement) {
|
|
61
|
+
if (!document.fullscreenElement) {
|
|
62
|
+
container.requestFullscreen();
|
|
63
|
+
this.isFullscreen = true;
|
|
64
|
+
} else {
|
|
65
|
+
document.exitFullscreen();
|
|
66
|
+
this.isFullscreen = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
zoomIn() {
|
|
71
|
+
this.zoom = Math.min(this.zoom + 0.1, 3);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
zoomOut() {
|
|
75
|
+
this.zoom = Math.max(this.zoom - 0.1, 0.5);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
rotateRightImg() {
|
|
79
|
+
this.rotate += 90;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
rotateLeftImg() {
|
|
83
|
+
this.rotate -= 90;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
reset() {
|
|
87
|
+
this.zoom = 1;
|
|
88
|
+
this.rotate = 0;
|
|
89
|
+
this.posX = 0;
|
|
90
|
+
this.posY = 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
startDrag(event: MouseEvent) {
|
|
94
|
+
if (this.zoom <= 1) return;
|
|
95
|
+
this.dragging = true;
|
|
96
|
+
this.animateTransform = false;
|
|
97
|
+
|
|
98
|
+
this.dragStart = {
|
|
99
|
+
x: event.clientX,
|
|
100
|
+
y: event.clientY
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
document.addEventListener('mousemove', this.onDrag);
|
|
104
|
+
document.addEventListener('mouseup', this.endDrag);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onDrag = (event: MouseEvent) => {
|
|
108
|
+
if (!this.dragging) return;
|
|
109
|
+
|
|
110
|
+
const deltaX = event.clientX - this.dragStart.x;
|
|
111
|
+
const deltaY = event.clientY - this.dragStart.y;
|
|
112
|
+
|
|
113
|
+
const angleRad = (this.rotate % 360) * (Math.PI / 180);
|
|
114
|
+
|
|
115
|
+
const rotatedX = deltaX * Math.cos(angleRad) + deltaY * Math.sin(angleRad);
|
|
116
|
+
const rotatedY = deltaY * Math.cos(angleRad) - deltaX * Math.sin(angleRad);
|
|
117
|
+
|
|
118
|
+
this.posX += rotatedX;
|
|
119
|
+
this.posY += rotatedY;
|
|
120
|
+
|
|
121
|
+
this.dragStart.x = event.clientX;
|
|
122
|
+
this.dragStart.y = event.clientY;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
endDrag = () => {
|
|
126
|
+
this.dragging = false;
|
|
127
|
+
this.animateTransform = true;
|
|
128
|
+
document.removeEventListener('mousemove', this.onDrag);
|
|
129
|
+
document.removeEventListener('mouseup', this.endDrag);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<div class="relative w-full select-none">
|
|
2
|
+
<input
|
|
3
|
+
#fileInput
|
|
4
|
+
type="file"
|
|
5
|
+
[id]="id"
|
|
6
|
+
[name]="name ?? ''"
|
|
7
|
+
[accept]="accept"
|
|
8
|
+
[multiple]="multiple"
|
|
9
|
+
(change)="onFileSelected($event)"
|
|
10
|
+
[required]="required"
|
|
11
|
+
[disabled]="disabled"
|
|
12
|
+
class="hidden"
|
|
13
|
+
(blur)="onTouched()"
|
|
14
|
+
/>
|
|
15
|
+
|
|
16
|
+
<label
|
|
17
|
+
[for]="id"
|
|
18
|
+
class="input block text-sm dark:text-gray-400 w-full h-[40px] pr-1 bg-background dark:bg-dark-background border border-border dark:border-dark-border flex items-center justify-between text-black dark:text-white rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary transition duration-200 cursor-pointer">
|
|
19
|
+
|
|
20
|
+
<div class="truncate" [ngClass]="{ 'opacity-50' : !innerValue?.name }">
|
|
21
|
+
@if (innerValue?.name) {
|
|
22
|
+
<span>{{ innerValue?.name }}</span>
|
|
23
|
+
} @else {
|
|
24
|
+
<span class="flex items-center gap-2">
|
|
25
|
+
<lucide-icon [name]="iconsService.icons.upload" [size]="15" />
|
|
26
|
+
Seleccionar archivo...
|
|
27
|
+
</span>
|
|
28
|
+
}
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
@if (value && clearButton) {
|
|
32
|
+
<button type="button" class="text-gray-400 text-gray-400 hover:text-gray-500 pr-1 mr-1 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer" (click)="clearFile()">
|
|
33
|
+
<lucide-icon [name]="iconsService.icons.close" class="w-4 h-4" />
|
|
34
|
+
</button>
|
|
35
|
+
}
|
|
36
|
+
</label>
|
|
37
|
+
|
|
38
|
+
@if (previewUrl && showImage) {
|
|
39
|
+
<div class="mt-2 w-full align-center justify-center flex">
|
|
40
|
+
<img [src]="previewUrl" alt="Vista previa"
|
|
41
|
+
[ngStyle]="{
|
|
42
|
+
width: widthImgFile ? widthImgFile + 'px' : '100%',
|
|
43
|
+
height: heightImgFile ? heightImgFile + 'px' : 'auto'
|
|
44
|
+
}"
|
|
45
|
+
class="rounded border border-border dark:border-dark-border shadow-sm object-cover"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
}
|
|
49
|
+
</div>
|