tailjng 0.1.6 → 0.1.8

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 (175) hide show
  1. package/README.md +27 -5
  2. package/cli/execute/init-app.js +5 -2
  3. package/cli/execute/sync-app.js +14 -2
  4. package/cli/settings/colors-config-utils.js +69 -11
  5. package/cli/settings/icons-config-utils.js +62 -0
  6. package/cli/settings/path-utils.js +32 -2
  7. package/cli/settings/project-utils.js +7 -1
  8. package/cli/templates/app.generator.js +8 -5
  9. package/fesm2022/tailjng.mjs +247 -80
  10. package/fesm2022/tailjng.mjs.map +1 -1
  11. package/lib/services/static/theme.service.d.ts +39 -1
  12. package/lib/utils/theme/theme-variables.util.d.ts +31 -0
  13. package/package.json +1 -1
  14. package/public-api.d.ts +2 -1
  15. package/registry/components.json +41 -18
  16. package/src/colors.safelist.css +2 -2
  17. package/src/lib/components/.config/README.md +11 -0
  18. package/src/lib/components/.config/colors/README.md +40 -0
  19. package/src/lib/components/{colors-config → .config/colors}/colors.config.ts +5 -5
  20. package/src/lib/components/{colors-config → .config/colors}/colors.safelist.css +2 -2
  21. package/src/lib/components/.config/icons/README.md +26 -0
  22. package/src/lib/components/.config/icons/icons.lucide.ts +134 -0
  23. package/src/lib/components/.config/input/README.md +24 -0
  24. package/src/lib/components/.config/input/input.classes.ts +119 -0
  25. package/src/lib/components/alert/alert-dialog/dialog-alert.component.css +177 -6
  26. package/src/lib/components/alert/alert-dialog/dialog-alert.component.html +24 -37
  27. package/src/lib/components/alert/alert-dialog/dialog-alert.component.ts +66 -56
  28. package/src/lib/components/alert/alert-dialog/dialog-alert.types.ts +19 -0
  29. package/src/lib/components/alert/alert-toast/toast-alert.component.css +494 -14
  30. package/src/lib/components/alert/alert-toast/toast-alert.component.html +106 -102
  31. package/src/lib/components/alert/alert-toast/toast-alert.component.ts +485 -128
  32. package/src/lib/components/alert/alert-toast/toast-alert.types.ts +25 -0
  33. package/src/lib/components/badge/badge.component.html +34 -21
  34. package/src/lib/components/badge/badge.component.ts +140 -31
  35. package/src/lib/components/button/button.component.html +16 -10
  36. package/src/lib/components/button/button.component.ts +162 -22
  37. package/src/lib/components/card/card-complete/complete-card.component.html +2 -2
  38. package/src/lib/components/card/card-complete/complete-card.component.ts +26 -16
  39. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.html +2 -2
  40. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +26 -16
  41. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.css +30 -0
  42. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.html +58 -46
  43. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.ts +135 -64
  44. package/src/lib/components/checkbox/checkbox-input/input-checkbox.types.ts +3 -0
  45. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.css +62 -0
  46. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.html +39 -25
  47. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.ts +74 -15
  48. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.types.ts +1 -0
  49. package/src/lib/components/coach-mark/coach-mark.component.html +6 -24
  50. package/src/lib/components/coach-mark/coach-mark.component.scss +1 -7
  51. package/src/lib/components/coach-mark/coach-mark.component.ts +51 -18
  52. package/src/lib/components/coach-mark/coach-mark.directive.ts +133 -78
  53. package/src/lib/components/coach-mark/coach-mark.types.ts +12 -0
  54. package/src/lib/components/dialog/dialog.component.css +1 -1
  55. package/src/lib/components/dialog/dialog.component.html +56 -65
  56. package/src/lib/components/dialog/dialog.component.ts +136 -110
  57. package/src/lib/components/dialog/dialog.types.ts +19 -0
  58. package/src/lib/components/filter/filter-complete/complete-filter.component.html +17 -20
  59. package/src/lib/components/filter/filter-complete/complete-filter.component.scss +25 -0
  60. package/src/lib/components/filter/filter-complete/complete-filter.component.ts +58 -34
  61. package/src/lib/components/filter/filter-complete/complete-filter.types.ts +7 -0
  62. package/src/lib/components/filter/filter-complete/complete-filter.util.ts +16 -0
  63. package/src/lib/components/form/form-container/container-form.component.css +4 -0
  64. package/src/lib/components/form/form-container/container-form.component.html +2 -2
  65. package/src/lib/components/form/form-container/container-form.component.ts +72 -16
  66. package/src/lib/components/form/form-container/container-form.types.ts +42 -0
  67. package/src/lib/components/form/form-container/form-col-span.directive.ts +25 -0
  68. package/src/lib/components/form/form-sidebar/sidebar-form.component.css +45 -0
  69. package/src/lib/components/form/form-sidebar/sidebar-form.component.html +128 -124
  70. package/src/lib/components/form/form-sidebar/sidebar-form.component.ts +114 -34
  71. package/src/lib/components/form/form-sidebar/sidebar-form.types.ts +3 -0
  72. package/src/lib/components/{toggle-radio/toggle-radio.component.css → form/form-validation/validation-form.component.css} +0 -1
  73. package/src/lib/components/form/form-validation/validation-form.component.html +10 -6
  74. package/src/lib/components/form/form-validation/validation-form.component.ts +99 -12
  75. package/src/lib/components/form/form-validation/validation-form.types.ts +33 -0
  76. package/src/lib/components/icon/icon.component.html +8 -5
  77. package/src/lib/components/icon/icon.component.ts +111 -9
  78. package/src/lib/components/input/input/input.component.css +0 -14
  79. package/src/lib/components/input/input/input.component.html +19 -16
  80. package/src/lib/components/input/input/input.component.ts +130 -53
  81. package/src/lib/components/input/input/input.types.ts +8 -0
  82. package/src/lib/components/input/input-file/file-input.component.html +65 -56
  83. package/src/lib/components/input/input-file/file-input.component.ts +276 -173
  84. package/src/lib/components/input/input-file/file-input.types.ts +2 -0
  85. package/src/lib/components/input/input-range/range-input.component.css +67 -0
  86. package/src/lib/components/input/input-range/range-input.component.html +50 -58
  87. package/src/lib/components/input/input-range/range-input.component.ts +148 -60
  88. package/src/lib/components/input/input-range/range-input.types.ts +7 -0
  89. package/src/lib/components/input/input-textarea/textarea-input.component.html +16 -7
  90. package/src/lib/components/input/input-textarea/textarea-input.component.ts +140 -50
  91. package/src/lib/components/input/input-textarea/textarea-input.types.ts +2 -0
  92. package/src/lib/components/label/label.component.html +17 -16
  93. package/src/lib/components/label/label.component.ts +94 -16
  94. package/src/lib/components/label/label.types.ts +2 -0
  95. package/src/lib/components/menu/menu-options-table/menu-options-defaults.ts +34 -0
  96. package/src/lib/components/menu/menu-options-table/options-table-menu.component.html +34 -20
  97. package/src/lib/components/menu/menu-options-table/options-table-menu.component.ts +211 -58
  98. package/src/lib/components/menu/menu-options-table/options-table-menu.types.ts +38 -0
  99. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.html +49 -52
  100. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.ts +112 -24
  101. package/src/lib/components/menu/options-coach-menu/options-coach-menu.types.ts +9 -0
  102. package/src/lib/components/mode-toggle/mode-toggle.component.html +11 -16
  103. package/src/lib/components/mode-toggle/mode-toggle.component.ts +69 -33
  104. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.html +4 -4
  105. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.ts +31 -7
  106. package/src/lib/components/paginator/paginator-complete/complete-paginator.types.ts +12 -0
  107. package/src/lib/components/paginator/paginator-complete/complete-paginator.util.ts +36 -0
  108. package/src/lib/components/progress-bar/progress-bar.component.html +41 -40
  109. package/src/lib/components/progress-bar/progress-bar.component.ts +95 -11
  110. package/src/lib/components/progress-bar/progress-bar.types.ts +2 -0
  111. package/src/lib/components/select/select-dropdown/dropdown-select.component.html +56 -46
  112. package/src/lib/components/select/select-dropdown/dropdown-select.component.ts +450 -509
  113. package/src/lib/components/select/select-dropdown/dropdown-select.types.ts +43 -0
  114. package/src/lib/components/select/select-dropdown/dropdown-select.util.ts +179 -0
  115. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.html +131 -42
  116. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.ts +491 -475
  117. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.types.ts +22 -0
  118. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.util.ts +20 -0
  119. package/src/lib/components/select/select-multi-table/multi-table-select.component.css +4 -0
  120. package/src/lib/components/select/select-multi-table/multi-table-select.component.html +76 -60
  121. package/src/lib/components/select/select-multi-table/multi-table-select.component.ts +250 -313
  122. package/src/lib/components/select/select-multi-table/multi-table-select.types.ts +10 -0
  123. package/src/lib/components/select/select-multi-table/multi-table-select.util.ts +5 -0
  124. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.css +155 -0
  125. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.html +72 -53
  126. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.ts +84 -27
  127. package/src/lib/components/sidebar/sidebar-static/static-sidebar.types.ts +2 -0
  128. package/src/lib/components/table/table-complete/complete-table.component.html +21 -23
  129. package/src/lib/components/table/table-complete/complete-table.component.ts +190 -338
  130. package/src/lib/components/table/table-complete/complete-table.types.ts +28 -0
  131. package/src/lib/components/table/table-complete/complete-table.util.ts +236 -0
  132. package/src/lib/components/table/table-complete/index.ts +2 -0
  133. package/src/lib/components/table/table-crud-complete/complete-crud-table.animations.ts +34 -0
  134. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +87 -142
  135. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.scss +0 -63
  136. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +544 -831
  137. package/src/lib/components/table/table-crud-complete/complete-crud-table.types.ts +57 -0
  138. package/src/lib/components/table/table-crud-complete/complete-crud-table.util.ts +723 -0
  139. package/src/lib/components/table/table-crud-complete/index.ts +3 -0
  140. package/src/lib/components/theme-generator/theme-generator.component.css +21 -0
  141. package/src/lib/components/theme-generator/theme-generator.component.html +141 -116
  142. package/src/lib/components/theme-generator/theme-generator.component.ts +44 -24
  143. package/src/lib/components/toggle-radio/shared/toggle-options.types.ts +8 -0
  144. package/src/lib/components/toggle-radio/shared/toggle-options.util.ts +62 -0
  145. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.css +22 -0
  146. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.html +65 -0
  147. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.ts +192 -0
  148. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.types.ts +1 -0
  149. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.css +34 -0
  150. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.html +47 -0
  151. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.ts +187 -0
  152. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.types.ts +1 -0
  153. package/src/lib/components/tooltip/tooltip.directive.ts +12 -9
  154. package/src/lib/components/tooltip/tooltip.service.ts +331 -133
  155. package/src/lib/components/tooltip/tooltip.types.ts +9 -0
  156. package/src/lib/components/viewer/viewer-image/image-viewer.component.css +14 -4
  157. package/src/lib/components/viewer/viewer-image/image-viewer.component.html +61 -95
  158. package/src/lib/components/viewer/viewer-image/image-viewer.component.ts +182 -177
  159. package/src/lib/components/viewer/viewer-image/image-viewer.types.ts +3 -0
  160. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.css +25 -0
  161. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.html +95 -24
  162. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.ts +168 -15
  163. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.types.ts +1 -0
  164. package/src/styles.css +2 -2
  165. package/lib/services/static/icons.service.d.ts +0 -65
  166. package/src/lib/components/colors-config/README.md +0 -38
  167. package/src/lib/components/form/form-sidebar/sidebar-form.component.scss +0 -0
  168. package/src/lib/components/form/form-validation/validation-form.component.scss +0 -0
  169. package/src/lib/components/menu/menu-options-table/options-table-menu.component.scss +0 -0
  170. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.scss +0 -12
  171. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.scss +0 -0
  172. package/src/lib/components/toggle-radio/toggle-radio.component.html +0 -51
  173. package/src/lib/components/toggle-radio/toggle-radio.component.ts +0 -222
  174. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.scss +0 -0
  175. package/tailjng-0.1.6.tgz +0 -0
@@ -1,27 +1,55 @@
1
- import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
1
+ import { NgClass } from '@angular/common';
2
+ import {
3
+ Component,
4
+ HostListener,
5
+ Input,
6
+ OnChanges,
7
+ SimpleChanges,
8
+ } from '@angular/core';
3
9
  import { SafeUrl } from '@angular/platform-browser';
4
- import { LucideAngularModule } from 'lucide-angular';
5
- import { JIconsService } from 'tailjng';
10
+ import { Icons } from '../../.config/icons/icons.lucide';
6
11
  import { JButtonComponent } from '../../button/button.component';
12
+ import { JIconComponent } from '../../icon/icon.component';
7
13
  import { JTooltipDirective } from '../../tooltip/tooltip.directive';
8
-
14
+ import { ImageObjectFit, ImageViewerControlSize } from './image-viewer.types';
15
+
16
+ export type { ImageObjectFit, ImageViewerControlSize } from './image-viewer.types';
17
+
18
+ const ZOOM_MIN = 0.5;
19
+ const ZOOM_MAX = 3;
20
+ const ZOOM_STEP = 0.1;
21
+
22
+ const CONTROL_BUTTON_SIZES: Record<ImageViewerControlSize, string> = {
23
+ sm: 'w-[28px] h-[28px]',
24
+ md: 'w-[32px] h-[32px]',
25
+ lg: 'w-[38px] h-[38px]',
26
+ };
27
+
28
+ const CONTROL_ICON_SIZES: Record<ImageViewerControlSize, number> = {
29
+ sm: 15,
30
+ md: 18,
31
+ lg: 21,
32
+ };
33
+
34
+ /**
35
+ * Image viewer with zoom, rotate, pan, download, and fullscreen controls.
36
+ *
37
+ * Install: `npx tailjng add viewer-image`
38
+ */
9
39
  @Component({
10
40
  selector: 'JImageViewer',
11
- imports: [CommonModule, LucideAngularModule, JButtonComponent, JTooltipDirective],
41
+ imports: [NgClass, JButtonComponent, JTooltipDirective, JIconComponent],
12
42
  templateUrl: './image-viewer.component.html',
13
- styleUrl: './image-viewer.component.css'
43
+ styleUrl: './image-viewer.component.css',
14
44
  })
15
45
  export class JImageViewerComponent implements OnChanges {
46
+ readonly Icons = Icons;
16
47
 
17
48
  @Input() src: string | SafeUrl | null = null;
18
- @Input() alt: string = 'Imagen';
19
- @Input() emptyMessage: string = 'La imagen no existe';
20
-
21
- @Input() objectFit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down' = 'contain';
22
-
49
+ @Input() alt = 'Imagen';
50
+ @Input() emptyMessage = 'La imagen no existe';
51
+ @Input() objectFit: ImageObjectFit = 'contain';
23
52
  @Input() showControls = true;
24
-
25
53
  @Input() showRotateLeft?: boolean;
26
54
  @Input() showRotateRight?: boolean;
27
55
  @Input() showReset?: boolean;
@@ -29,16 +57,16 @@ export class JImageViewerComponent implements OnChanges {
29
57
  @Input() showFullscreen?: boolean;
30
58
  @Input() showZoomIn?: boolean;
31
59
  @Input() showZoomOut?: boolean;
32
-
33
- @Input() controlSize: 'sm' | 'md' | 'lg' = 'md';
60
+ @Input() controlSize: ImageViewerControlSize = 'md';
61
+ @Input() ngClasses: Record<string, boolean> = {};
34
62
 
35
63
  posX = 0;
36
64
  posY = 0;
37
-
38
65
  zoom = 1;
39
66
  rotate = 0;
40
-
41
67
  isFullscreen = false;
68
+ isDownloading = false;
69
+
42
70
  internalLoading = true;
43
71
  animateTransform = true;
44
72
  hasError = false;
@@ -46,98 +74,159 @@ export class JImageViewerComponent implements OnChanges {
46
74
  private dragging = false;
47
75
  private dragStart = { x: 0, y: 0 };
48
76
 
77
+ /**
78
+ * Whether the image is still loading.
79
+ */
49
80
  get loading(): boolean {
50
81
  return this.internalLoading;
51
82
  }
52
83
 
84
+ /**
85
+ * Whether a source URL is provided.
86
+ */
53
87
  get hasImage(): boolean {
54
88
  return !!this.src;
55
89
  }
56
90
 
57
- constructor(public readonly iconsService: JIconsService) { }
58
-
59
- ngOnChanges(changes: SimpleChanges): void {
60
- if (changes['src'] && changes['src'].currentValue !== changes['src'].previousValue) {
61
- this.reset();
91
+ /**
92
+ * Whether zoom out is at the minimum level.
93
+ */
94
+ get isZoomMin(): boolean {
95
+ return this.zoom <= ZOOM_MIN;
96
+ }
62
97
 
63
- this.hasError = false;
64
- this.internalLoading = !!this.src;
65
- }
98
+ /**
99
+ * Whether zoom in is at the maximum level.
100
+ */
101
+ get isZoomMax(): boolean {
102
+ return this.zoom >= ZOOM_MAX;
66
103
  }
67
104
 
105
+ /**
106
+ * CSS transform applied to the image.
107
+ */
108
+ get imageTransform(): string {
109
+ return `scale(${this.zoom}) rotate(${this.rotate}deg) translate(${this.posX}px, ${this.posY}px)`;
110
+ }
68
111
 
69
112
  /**
70
- * Handles the successful loading of the image.
113
+ * Tailwind size classes for toolbar buttons.
71
114
  */
72
- handleLoad() {
73
- this.internalLoading = false;
74
- this.hasError = false;
115
+ get buttonSizeClass(): string {
116
+ return CONTROL_BUTTON_SIZES[this.controlSize];
75
117
  }
76
118
 
119
+ /**
120
+ * Icon size in px for toolbar buttons.
121
+ */
122
+ get buttonIconSize(): number {
123
+ return CONTROL_ICON_SIZES[this.controlSize];
124
+ }
77
125
 
126
+ /**
127
+ * Shared classes for floating control buttons.
128
+ */
129
+ get controlButtonClasses(): string {
130
+ return `secondary rounded-lg shrink-0 opacity-30 hover:opacity-100 transition-opacity duration-200 ${this.buttonSizeClass}`;
131
+ }
78
132
 
79
133
  /**
80
- * Handles the error loading of the image.
134
+ * Class map for the controls toolbar position.
81
135
  */
82
- handleError() {
83
- this.internalLoading = false;
84
- this.hasError = true;
136
+ get controlsPositionClasses(): Record<string, boolean> {
137
+ return {
138
+ 'top-3 right-3': this.isFullscreen,
139
+ 'top-2 right-2': !this.isFullscreen,
140
+ };
85
141
  }
86
142
 
143
+ /**
144
+ * Class map for the image element.
145
+ */
146
+ get imageClasses(): Record<string, boolean> {
147
+ return {
148
+ 'j-image-viewer__image': true,
149
+ 'invisible': this.loading,
150
+ 'transition-transform': this.animateTransform,
151
+ 'duration-200': this.animateTransform,
152
+ 'ease-in-out': this.animateTransform,
153
+ };
154
+ }
87
155
 
156
+ /**
157
+ * Whether at least one toolbar action is visible.
158
+ */
159
+ get hasAnyControlVisible(): boolean {
160
+ return [
161
+ this.showRotateLeft,
162
+ this.showRotateRight,
163
+ this.showReset,
164
+ this.showZoomOut,
165
+ this.showZoomIn,
166
+ this.showDownload,
167
+ this.showFullscreen,
168
+ ].some((value) => this.isControlVisible(value));
169
+ }
88
170
 
89
171
  /**
90
- * Toggles fullscreen mode for the image viewer.
91
- * @param container
172
+ * Fullscreen toggle icon based on current state.
92
173
  */
93
- toggleFullscreen(container: HTMLElement) {
94
- if (!document.fullscreenElement) {
95
- container.requestFullscreen();
96
- this.isFullscreen = true;
97
- } else {
98
- document.exitFullscreen();
99
- this.isFullscreen = false;
174
+ get fullscreenIcon() {
175
+ return this.isFullscreen ? Icons.Minimize2 : Icons.Scan;
176
+ }
177
+
178
+ ngOnChanges(changes: SimpleChanges): void {
179
+ if (changes['src'] && changes['src'].currentValue !== changes['src'].previousValue) {
180
+ this.resetView();
181
+ this.hasError = false;
182
+ this.internalLoading = !!this.src;
100
183
  }
101
184
  }
102
185
 
186
+ @HostListener('document:fullscreenchange')
187
+ onFullscreenChange(): void {
188
+ this.isFullscreen = !!document.fullscreenElement;
189
+ }
103
190
 
104
191
  /**
105
- * Downloads the currently displayed image.
192
+ * Whether a specific toolbar action should render.
106
193
  */
107
- isDownloading = false;
108
-
109
- private getImageUrl(): string {
110
- if (!this.src) return '';
194
+ isControlVisible(value?: boolean): boolean {
195
+ return value ?? this.showControls;
196
+ }
111
197
 
112
- if (typeof this.src === 'string') {
113
- return this.src;
114
- }
198
+ onImageLoad(): void {
199
+ this.internalLoading = false;
200
+ this.hasError = false;
201
+ }
115
202
 
116
- const anySrc = this.src as any;
203
+ onImageError(): void {
204
+ this.internalLoading = false;
205
+ this.hasError = true;
206
+ }
117
207
 
118
- if (anySrc.changingThisBreaksApplicationSecurity) {
119
- return anySrc.changingThisBreaksApplicationSecurity as string;
208
+ toggleFullscreen(container: HTMLElement): void {
209
+ if (!document.fullscreenElement) {
210
+ container.requestFullscreen();
211
+ this.isFullscreen = true;
212
+ return;
120
213
  }
121
214
 
122
- return this.src.toString();
215
+ document.exitFullscreen();
216
+ this.isFullscreen = false;
123
217
  }
124
218
 
125
-
126
-
127
- /**
128
- * Downloads the currently displayed image, handling both base64 and normal URLs.
129
- * @returns
130
- */
131
- download() {
219
+ download(): void {
132
220
  try {
133
- if (!this.src) return;
221
+ if (!this.src) {
222
+ return;
223
+ }
134
224
 
135
225
  this.isDownloading = true;
136
226
 
137
- const imageUrl = this.getImageUrl();
227
+ const imageUrl = this.resolveImageUrl();
138
228
  const fileName = this.alt?.replace(/\s+/g, '') || 'imagen';
139
229
 
140
- // Si es base64
141
230
  if (imageUrl.startsWith('data:image')) {
142
231
  const link = document.createElement('a');
143
232
  link.href = imageUrl;
@@ -149,7 +238,6 @@ export class JImageViewerComponent implements OnChanges {
149
238
  return;
150
239
  }
151
240
 
152
- // Si es URL normal
153
241
  fetch(imageUrl)
154
242
  .then((response) => {
155
243
  if (!response.ok) {
@@ -175,172 +263,89 @@ export class JImageViewerComponent implements OnChanges {
175
263
  .finally(() => {
176
264
  this.isDownloading = false;
177
265
  });
178
-
179
266
  } catch (err) {
180
267
  this.isDownloading = false;
181
268
  console.error('Error general en download():', err);
182
269
  }
183
270
  }
184
271
 
185
-
186
-
187
-
188
- /**
189
- * Zooms in the image.
190
- */
191
- zoomIn() {
192
- this.zoom = Math.min(this.zoom + 0.1, 3);
272
+ zoomIn(): void {
273
+ this.zoom = Math.min(this.zoom + ZOOM_STEP, ZOOM_MAX);
193
274
  }
194
275
 
195
-
196
-
197
- /**
198
- * Zooms out the image.
199
- */
200
- zoomOut() {
201
- this.zoom = Math.max(this.zoom - 0.1, 0.5);
276
+ zoomOut(): void {
277
+ this.zoom = Math.max(this.zoom - ZOOM_STEP, ZOOM_MIN);
202
278
  }
203
279
 
204
-
205
-
206
- /**
207
- * Rotates the image to the right.
208
- */
209
- rotateRightImg() {
280
+ rotateRight(): void {
210
281
  this.rotate += 90;
211
282
  }
212
283
 
213
-
214
-
215
- /**
216
- * Rotates the image to the left.
217
- */
218
- rotateLeftImg() {
284
+ rotateLeft(): void {
219
285
  this.rotate -= 90;
220
286
  }
221
287
 
222
-
223
-
224
- /**
225
- * Resets the image viewer to its initial state.
226
- */
227
- reset() {
288
+ resetView(): void {
228
289
  this.zoom = 1;
229
290
  this.rotate = 0;
230
291
  this.posX = 0;
231
292
  this.posY = 0;
232
293
  }
233
294
 
295
+ startDrag(event: MouseEvent): void {
296
+ if (this.zoom <= 1) {
297
+ return;
298
+ }
234
299
 
235
-
236
- /**
237
- * Starts dragging the image.
238
- * @param event The mouse event.
239
- * @returns
240
- */
241
- startDrag(event: MouseEvent) {
242
- if (this.zoom <= 1) return;
243
300
  this.dragging = true;
244
301
  this.animateTransform = false;
245
-
246
302
  this.dragStart = {
247
303
  x: event.clientX,
248
- y: event.clientY
304
+ y: event.clientY,
249
305
  };
250
306
 
251
307
  document.addEventListener('mousemove', this.onDrag);
252
308
  document.addEventListener('mouseup', this.endDrag);
253
309
  }
254
310
 
255
-
256
-
257
- /**
258
- * Handles the dragging of the image.
259
- * @param event The mouse event.
260
- * @returns
261
- */
262
- onDrag = (event: MouseEvent) => {
263
- if (!this.dragging) return;
311
+ private onDrag = (event: MouseEvent): void => {
312
+ if (!this.dragging) {
313
+ return;
314
+ }
264
315
 
265
316
  const deltaX = event.clientX - this.dragStart.x;
266
317
  const deltaY = event.clientY - this.dragStart.y;
267
-
268
318
  const angleRad = (this.rotate % 360) * (Math.PI / 180);
269
-
270
319
  const rotatedX = deltaX * Math.cos(angleRad) + deltaY * Math.sin(angleRad);
271
320
  const rotatedY = deltaY * Math.cos(angleRad) - deltaX * Math.sin(angleRad);
272
321
 
273
322
  this.posX += rotatedX;
274
323
  this.posY += rotatedY;
275
-
276
324
  this.dragStart.x = event.clientX;
277
325
  this.dragStart.y = event.clientY;
278
326
  };
279
327
 
280
-
281
- /**
282
- * Ends dragging the image.
283
- */
284
- endDrag = () => {
328
+ private endDrag = (): void => {
285
329
  this.dragging = false;
286
330
  this.animateTransform = true;
287
331
  document.removeEventListener('mousemove', this.onDrag);
288
332
  document.removeEventListener('mouseup', this.endDrag);
289
333
  };
290
334
 
335
+ private resolveImageUrl(): string {
336
+ if (!this.src) {
337
+ return '';
338
+ }
291
339
 
340
+ if (typeof this.src === 'string') {
341
+ return this.src;
342
+ }
292
343
 
293
- /**
294
- * Returns the CSS class for the control buttons based on the selected size.
295
- */
296
- get buttonSizeClass(): string {
297
- const sizes = {
298
- sm: 'w-[28px] h-[28px]',
299
- md: 'w-[32px] h-[32px]',
300
- lg: 'w-[38px] h-[38px]',
301
- };
302
-
303
- return sizes[this.controlSize];
304
- }
305
-
306
-
307
- /**
308
- * Returns the icon size for the control buttons based on the selected size.
309
- */
310
- get buttonIconSize(): number {
311
- const sizes = {
312
- sm: 15,
313
- md: 18,
314
- lg: 21,
315
- };
316
-
317
- return sizes[this.controlSize];
318
- }
319
-
320
-
321
- /**
322
- * Determines if a control button should be visible based on the input value and the general showControls flag.
323
- * @param value The specific control visibility input value.
324
- * @returns True if the control should be visible, false otherwise.
325
- */
326
- isControlVisible(value?: boolean): boolean {
327
- return value ?? this.showControls;
328
- }
329
-
344
+ const safe = this.src as SafeUrl & { changingThisBreaksApplicationSecurity?: string };
345
+ if (safe.changingThisBreaksApplicationSecurity) {
346
+ return safe.changingThisBreaksApplicationSecurity;
347
+ }
330
348
 
331
- /**
332
- * Determines if any control button is visible, used to conditionally display the controls container.
333
- * @returns True if at least one control is visible, false otherwise.
334
- */
335
- get hasAnyControlVisible(): boolean {
336
- return [
337
- this.showRotateLeft,
338
- this.showRotateRight,
339
- this.showReset,
340
- this.showZoomOut,
341
- this.showZoomIn,
342
- this.showDownload,
343
- this.showFullscreen,
344
- ].some(value => this.isControlVisible(value));
349
+ return this.src.toString();
345
350
  }
346
351
  }
@@ -0,0 +1,3 @@
1
+ export type ImageObjectFit = 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
2
+
3
+ export type ImageViewerControlSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,25 @@
1
+ :host {
2
+ display: block;
3
+ width: 100%;
4
+ height: 100%;
5
+ }
6
+
7
+ .j-pdf-viewer__mobile-card {
8
+ border: 1px solid var(--color-border);
9
+ border-radius: 0.75rem;
10
+ background: linear-gradient(
11
+ 180deg,
12
+ color-mix(in srgb, var(--color-muted) 22%, var(--color-background)) 0%,
13
+ var(--color-background) 100%
14
+ );
15
+ }
16
+
17
+ :host-context(.dark) .j-pdf-viewer__mobile-card,
18
+ :host-context(html.dark) .j-pdf-viewer__mobile-card {
19
+ border-color: var(--color-dark-border);
20
+ background: linear-gradient(
21
+ 180deg,
22
+ color-mix(in srgb, var(--color-dark-muted) 30%, var(--color-dark-background)) 0%,
23
+ var(--color-dark-background) 100%
24
+ );
25
+ }
@@ -1,28 +1,99 @@
1
- <div class="flex-1 flex flex-col items-center justify-center">
2
- <object
3
- [data]="safeUrl"
4
- type="application/pdf"
5
- class="w-full h-full min-h-[300px] md:h-[75vh] overflow-auto"
6
- >
7
- <div class="p-6 flex flex-col items-center justify-center text-center gap-3">
8
- <!-- Icono -->
9
- <lucide-icon [name]="iconsService.icons.circleAlert" size="35" class="text-primary"></lucide-icon>
1
+ <div
2
+ class="flex h-full min-h-[18.75rem] w-full flex-col max-md:min-h-56"
3
+ [ngClass]="ngClasses"
4
+ >
5
+ @if (!hasUrl) {
6
+ <div
7
+ class="flex min-h-[18.75rem] flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border p-6 text-center max-md:min-h-56 dark:border-dark-border"
8
+ >
9
+ <JIcon
10
+ [icon]="Icons.CircleAlert"
11
+ [size]="32"
12
+ iconClass="text-muted-foreground"
13
+ [ariaHidden]="true"
14
+ />
15
+ <p
16
+ class="m-0 max-w-80 text-[0.8125rem] leading-[1.45] text-muted-foreground dark:text-dark-muted-foreground"
17
+ >
18
+ {{ emptyMessage }}
19
+ </p>
20
+ </div>
21
+ } @else if (showDesktopEmbed) {
22
+ <iframe
23
+ class="block h-full min-h-[18.75rem] w-full rounded-xl border border-border bg-background max-md:min-h-56 dark:border-dark-border dark:bg-dark-background"
24
+ [src]="safeUrl"
25
+ [title]="documentTitle"
26
+ loading="lazy"
27
+ ></iframe>
28
+ } @else if (useMobileLayout) {
29
+ <div class="flex h-full w-full flex-col gap-0">
30
+ @if (showMobileActions) {
31
+ <div
32
+ class="j-pdf-viewer__mobile-card flex flex-1 flex-col items-center justify-center gap-3.5 p-5 text-center"
33
+ >
34
+ <JIcon
35
+ [icon]="Icons.FileUp"
36
+ [size]="36"
37
+ iconClass="text-primary dark:text-dark-primary"
38
+ [ariaHidden]="true"
39
+ />
40
+ <div class="flex flex-col gap-1.5">
41
+ <h3
42
+ class="m-0 text-[0.9375rem] font-semibold text-foreground dark:text-dark-foreground"
43
+ >
44
+ {{ documentTitle }}
45
+ </h3>
46
+ <p
47
+ class="m-0 max-w-80 text-[0.8125rem] leading-[1.45] text-muted-foreground dark:text-dark-muted-foreground"
48
+ >
49
+ En móvil, abre el PDF en el visor del sistema para navegar y hacer zoom con
50
+ mejor experiencia.
51
+ </p>
52
+ </div>
53
+ <div
54
+ class="flex w-full max-w-64 flex-col flex-wrap items-center justify-center gap-2"
55
+ >
56
+ <JButton
57
+ [text]="'Abrir PDF'"
58
+ [icon]="Icons.Eye"
59
+ (clicked)="openInBrowser()"
60
+ />
61
+ <JButton
62
+ [text]="'Descargar'"
63
+ [icon]="Icons.Download"
64
+ classes="secondary"
65
+ (clicked)="downloadPdf()"
66
+ />
67
+ </div>
68
+ </div>
69
+ }
10
70
 
11
- <!-- Texto -->
12
- <div class="space-y-1">
13
- <h3 class="text-sm font-semibold text-slate-800 dark:text-slate-100">
14
- No se pudo mostrar el PDF
15
- </h3>
16
- <p class="text-xs text-slate-500 dark:text-slate-400 max-w-xs">
17
- Tu navegador no es compatible con la visualización embebida de este documento.
18
- Puedes descargarlo y verlo en tu visor de PDF preferido.
19
- </p>
20
- </div>
71
+ @if (showMobileEmbed) {
72
+ <iframe
73
+ class="block min-h-[60vh] w-full rounded-t-xl border border-border bg-background dark:border-dark-border dark:bg-dark-background"
74
+ [src]="safeUrl"
75
+ [title]="documentTitle"
76
+ loading="lazy"
77
+ ></iframe>
21
78
 
22
- <!-- Botón -->
23
- <a [href]="safeUrl" target="_blank" class="inline-flex items-center justify-center px-4 py-2 rounded-full text-xs font-medium bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 transition-colors">
24
- Descargar archivo
25
- </a>
79
+ <div
80
+ class="flex items-center justify-center gap-2 rounded-b-xl border border-t-0 border-border bg-background px-3 py-2.5 dark:border-dark-border dark:bg-dark-background"
81
+ >
82
+ <JButton
83
+ [text]="'Abrir'"
84
+ [icon]="Icons.Eye"
85
+ size="sm"
86
+ (clicked)="openInBrowser()"
87
+ />
88
+ <JButton
89
+ [text]="'Descargar'"
90
+ [icon]="Icons.Download"
91
+ size="sm"
92
+ classes="secondary"
93
+ (clicked)="downloadPdf()"
94
+ />
95
+ </div>
96
+ }
26
97
  </div>
27
- </object>
98
+ }
28
99
  </div>