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.
Files changed (87) hide show
  1. package/cli/component-manager.js +45 -0
  2. package/cli/dependency-manager.js +52 -0
  3. package/cli/file-operations.js +88 -0
  4. package/cli/index.js +51 -0
  5. package/cli/settings/colors.js +17 -0
  6. package/cli/settings/components-list.js +87 -0
  7. package/cli/settings/header-generator.js +42 -0
  8. package/cli/settings/path-utils.js +50 -0
  9. package/cli/settings/prompt-utils.js +37 -0
  10. package/cli/settings/tailwind-check.js +21 -0
  11. package/fesm2022/tailjng.mjs +903 -25
  12. package/fesm2022/tailjng.mjs.map +1 -1
  13. package/lib/config/tailjng-config.token.d.ts +3 -0
  14. package/lib/interfaces/alert/dialog-alert.interface.d.ts +52 -0
  15. package/lib/interfaces/alert/toast-alert.interface.d.ts +52 -0
  16. package/lib/interfaces/config.interface.d.ts +5 -0
  17. package/lib/interfaces/crud/api-response.d.ts +29 -0
  18. package/lib/interfaces/crud/crud.interface.d.ts +103 -0
  19. package/lib/services/alert/dialog-alert.service.d.ts +24 -0
  20. package/lib/services/alert/toast-alert.service.d.ts +26 -0
  21. package/lib/services/crud/converter-crud.service.d.ts +41 -0
  22. package/lib/services/crud/generic-crud.service.d.ts +81 -0
  23. package/lib/services/http/error-handler-http.service.d.ts +26 -0
  24. package/lib/services/http/params-http.service.d.ts +13 -0
  25. package/lib/services/static/icons.service.d.ts +31 -0
  26. package/lib/services/transformer/calendar.service.d.ts +71 -0
  27. package/package.json +5 -3
  28. package/public-api.d.ts +10 -3
  29. package/src/lib/components/alert/dialog-alert/dialog-alert.component.css +0 -0
  30. package/src/lib/components/alert/dialog-alert/dialog-alert.component.html +72 -0
  31. package/src/lib/components/alert/dialog-alert/dialog-alert.component.ts +66 -0
  32. package/src/lib/components/alert/toast-alert/toast-alert.component.css +5 -0
  33. package/src/lib/components/alert/toast-alert/toast-alert.component.html +76 -0
  34. package/src/lib/components/alert/toast-alert/toast-alert.component.ts +87 -0
  35. package/src/lib/components/button/button.component.css +0 -0
  36. package/src/lib/components/button/button.component.html +36 -0
  37. package/src/lib/components/button/button.component.ts +95 -0
  38. package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.css +0 -0
  39. package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.html +23 -0
  40. package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.ts +44 -0
  41. package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.css +0 -0
  42. package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.html +26 -0
  43. package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.ts +29 -0
  44. package/src/lib/components/color/colors.service.ts +109 -0
  45. package/src/lib/components/dialog/dialog.component.css +8 -0
  46. package/src/lib/components/dialog/dialog.component.html +57 -0
  47. package/src/lib/components/dialog/dialog.component.ts +179 -0
  48. package/src/lib/components/image/viewer-image/viewer-image.component.css +4 -0
  49. package/src/lib/components/image/viewer-image/viewer-image.component.html +75 -0
  50. package/src/lib/components/image/viewer-image/viewer-image.component.ts +131 -0
  51. package/src/lib/components/input/file-input/file-input.component.css +0 -0
  52. package/src/lib/components/input/file-input/file-input.component.html +49 -0
  53. package/src/lib/components/input/file-input/file-input.component.ts +218 -0
  54. package/src/lib/components/input/input/input.component.css +0 -0
  55. package/src/lib/components/input/input/input.component.html +24 -0
  56. package/src/lib/components/input/input/input.component.ts +78 -0
  57. package/src/lib/components/input/range-input/range-input.component.css +0 -0
  58. package/src/lib/components/input/range-input/range-input.component.html +64 -0
  59. package/src/lib/components/input/range-input/range-input.component.ts +78 -0
  60. package/src/lib/components/input/textarea-input/textarea-input.component.css +0 -0
  61. package/src/lib/components/input/textarea-input/textarea-input.component.html +21 -0
  62. package/src/lib/components/input/textarea-input/textarea-input.component.ts +75 -0
  63. package/src/lib/components/label/label.component.html +1 -1
  64. package/src/lib/components/label/label.component.ts +1 -1
  65. package/src/lib/components/mode-toggle/mode-toggle.component.css +0 -0
  66. package/src/lib/components/mode-toggle/mode-toggle.component.html +8 -0
  67. package/src/lib/components/mode-toggle/mode-toggle.component.ts +61 -0
  68. package/src/lib/components/progress-bar/progress-bar.component.css +0 -0
  69. package/src/lib/components/progress-bar/progress-bar.component.html +22 -0
  70. package/src/lib/components/progress-bar/progress-bar.component.ts +20 -0
  71. package/src/lib/components/select/dropdown/dropdown.component.css +0 -0
  72. package/src/lib/components/select/dropdown/dropdown.component.html +95 -0
  73. package/src/lib/components/select/dropdown/dropdown.component.ts +562 -0
  74. package/src/lib/components/select/multi-dropdown/multi-dropdown.component.css +0 -0
  75. package/src/lib/components/select/multi-dropdown/multi-dropdown.component.html +87 -0
  76. package/src/lib/components/select/multi-dropdown/multi-dropdown.component.ts +315 -0
  77. package/src/lib/components/select/multi-table/multi-table.component.css +0 -0
  78. package/src/lib/components/select/multi-table/multi-table.component.html +83 -0
  79. package/src/lib/components/select/multi-table/multi-table.component.ts +230 -0
  80. package/src/lib/components/toggle-radio/toggle-radio.component.css +0 -0
  81. package/src/lib/components/toggle-radio/toggle-radio.component.html +51 -0
  82. package/src/lib/components/toggle-radio/toggle-radio.component.ts +203 -0
  83. package/src/styles.css +126 -0
  84. package/cli/tailjng.js +0 -105
  85. package/lib/services/icons.service.d.ts +0 -9
  86. package/lib/tailjng.component.d.ts +0 -5
  87. package/lib/tailjng.service.d.ts +0 -6
@@ -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,8 @@
1
+ :host ::ng-deep .jdialog-full {
2
+ width: 100vw;
3
+ height: 100vh;
4
+ max-width: 100vw;
5
+ max-height: 100vh;
6
+ margin: 0 !important;
7
+ border-radius: 0;
8
+ }
@@ -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,4 @@
1
+ img {
2
+ -webkit-user-drag: none; /* Safari y Chrome */
3
+ user-select: none; /* Evita selección de texto */
4
+ }
@@ -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
+ }
@@ -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>