tailjng 0.1.6 → 0.1.7

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 (176) hide show
  1. package/README.md +12 -4
  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 +43 -8
  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 +2 -2
  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 +38 -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 +244 -2
  26. package/src/lib/components/alert/alert-dialog/dialog-alert.component.html +25 -38
  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 +630 -12
  30. package/src/lib/components/alert/alert-toast/toast-alert.component.html +103 -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 +97 -0
  42. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.html +54 -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 +112 -0
  46. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.html +28 -25
  47. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.ts +67 -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 +4 -22
  50. package/src/lib/components/coach-mark/coach-mark.component.scss +1 -1
  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 +103 -1
  55. package/src/lib/components/dialog/dialog.component.html +46 -66
  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 +16 -19
  59. package/src/lib/components/filter/filter-complete/complete-filter.component.scss +35 -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 +276 -0
  69. package/src/lib/components/form/form-sidebar/sidebar-form.component.html +117 -125
  70. package/src/lib/components/form/form-sidebar/sidebar-form.component.ts +109 -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.html +19 -16
  79. package/src/lib/components/input/input/input.component.ts +130 -53
  80. package/src/lib/components/input/input/input.types.ts +8 -0
  81. package/src/lib/components/input/input-file/file-input.component.html +65 -56
  82. package/src/lib/components/input/input-file/file-input.component.ts +276 -173
  83. package/src/lib/components/input/input-file/file-input.types.ts +2 -0
  84. package/src/lib/components/input/input-range/range-input.component.css +67 -0
  85. package/src/lib/components/input/input-range/range-input.component.html +50 -58
  86. package/src/lib/components/input/input-range/range-input.component.ts +148 -60
  87. package/src/lib/components/input/input-range/range-input.types.ts +7 -0
  88. package/src/lib/components/input/input-textarea/textarea-input.component.html +16 -7
  89. package/src/lib/components/input/input-textarea/textarea-input.component.ts +140 -50
  90. package/src/lib/components/input/input-textarea/textarea-input.types.ts +2 -0
  91. package/src/lib/components/label/label.component.html +17 -16
  92. package/src/lib/components/label/label.component.ts +70 -16
  93. package/src/lib/components/label/label.types.ts +2 -0
  94. package/src/lib/components/menu/menu-options-table/menu-options-defaults.ts +34 -0
  95. package/src/lib/components/menu/menu-options-table/options-table-menu.component.html +34 -20
  96. package/src/lib/components/menu/menu-options-table/options-table-menu.component.ts +211 -58
  97. package/src/lib/components/menu/menu-options-table/options-table-menu.types.ts +38 -0
  98. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.html +49 -52
  99. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.ts +112 -24
  100. package/src/lib/components/menu/options-coach-menu/options-coach-menu.types.ts +9 -0
  101. package/src/lib/components/mode-toggle/mode-toggle.component.html +11 -16
  102. package/src/lib/components/mode-toggle/mode-toggle.component.ts +69 -33
  103. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.html +4 -4
  104. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.ts +31 -7
  105. package/src/lib/components/paginator/paginator-complete/complete-paginator.types.ts +12 -0
  106. package/src/lib/components/paginator/paginator-complete/complete-paginator.util.ts +36 -0
  107. package/src/lib/components/progress-bar/progress-bar.component.css +11 -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.css +6 -0
  112. package/src/lib/components/select/select-dropdown/dropdown-select.component.html +54 -44
  113. package/src/lib/components/select/select-dropdown/dropdown-select.component.ts +450 -509
  114. package/src/lib/components/select/select-dropdown/dropdown-select.types.ts +43 -0
  115. package/src/lib/components/select/select-dropdown/dropdown-select.util.ts +179 -0
  116. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.css +6 -0
  117. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.html +131 -42
  118. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.ts +491 -475
  119. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.types.ts +22 -0
  120. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.util.ts +20 -0
  121. package/src/lib/components/select/select-multi-table/multi-table-select.component.css +10 -0
  122. package/src/lib/components/select/select-multi-table/multi-table-select.component.html +76 -60
  123. package/src/lib/components/select/select-multi-table/multi-table-select.component.ts +250 -313
  124. package/src/lib/components/select/select-multi-table/multi-table-select.types.ts +10 -0
  125. package/src/lib/components/select/select-multi-table/multi-table-select.util.ts +5 -0
  126. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.css +212 -0
  127. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.html +62 -53
  128. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.ts +84 -27
  129. package/src/lib/components/sidebar/sidebar-static/static-sidebar.types.ts +2 -0
  130. package/src/lib/components/table/table-complete/complete-table.component.html +15 -17
  131. package/src/lib/components/table/table-complete/complete-table.component.ts +190 -338
  132. package/src/lib/components/table/table-complete/complete-table.types.ts +28 -0
  133. package/src/lib/components/table/table-complete/complete-table.util.ts +236 -0
  134. package/src/lib/components/table/table-complete/index.ts +2 -0
  135. package/src/lib/components/table/table-crud-complete/complete-crud-table.animations.ts +34 -0
  136. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +73 -128
  137. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +542 -829
  138. package/src/lib/components/table/table-crud-complete/complete-crud-table.types.ts +57 -0
  139. package/src/lib/components/table/table-crud-complete/complete-crud-table.util.ts +723 -0
  140. package/src/lib/components/table/table-crud-complete/index.ts +3 -0
  141. package/src/lib/components/theme-generator/theme-generator.component.css +21 -0
  142. package/src/lib/components/theme-generator/theme-generator.component.html +146 -116
  143. package/src/lib/components/theme-generator/theme-generator.component.ts +44 -24
  144. package/src/lib/components/toggle-radio/shared/toggle-options.types.ts +8 -0
  145. package/src/lib/components/toggle-radio/shared/toggle-options.util.ts +44 -0
  146. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.css +135 -0
  147. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.html +52 -0
  148. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.ts +198 -0
  149. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.types.ts +1 -0
  150. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.css +108 -0
  151. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.html +37 -0
  152. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.ts +193 -0
  153. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.types.ts +1 -0
  154. package/src/lib/components/tooltip/tooltip.directive.ts +12 -9
  155. package/src/lib/components/tooltip/tooltip.service.ts +331 -133
  156. package/src/lib/components/tooltip/tooltip.types.ts +9 -0
  157. package/src/lib/components/viewer/viewer-image/image-viewer.component.css +90 -4
  158. package/src/lib/components/viewer/viewer-image/image-viewer.component.html +52 -103
  159. package/src/lib/components/viewer/viewer-image/image-viewer.component.ts +182 -177
  160. package/src/lib/components/viewer/viewer-image/image-viewer.types.ts +3 -0
  161. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.css +177 -0
  162. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.html +74 -24
  163. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.ts +168 -15
  164. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.types.ts +1 -0
  165. package/src/styles.css +2 -2
  166. package/lib/services/static/icons.service.d.ts +0 -65
  167. package/src/lib/components/colors-config/README.md +0 -38
  168. package/src/lib/components/form/form-sidebar/sidebar-form.component.scss +0 -0
  169. package/src/lib/components/form/form-validation/validation-form.component.scss +0 -0
  170. package/src/lib/components/menu/menu-options-table/options-table-menu.component.scss +0 -0
  171. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.scss +0 -12
  172. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.scss +0 -0
  173. package/src/lib/components/toggle-radio/toggle-radio.component.html +0 -51
  174. package/src/lib/components/toggle-radio/toggle-radio.component.ts +0 -222
  175. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.scss +0 -0
  176. package/tailjng-0.1.6.tgz +0 -0
@@ -1,130 +1,249 @@
1
- import { Component, Input, forwardRef, ViewChild, ElementRef, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
2
- import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule, ReactiveFormsModule } from '@angular/forms';
3
- import { NgClass, CommonModule } from '@angular/common';
4
- import { LucideAngularModule } from 'lucide-angular';
5
- import { JAlertToastService, JIconsService } from 'tailjng';
6
-
1
+ import { NgClass, NgStyle } from '@angular/common';
2
+ import {
3
+ Component,
4
+ ElementRef,
5
+ Input,
6
+ OnDestroy,
7
+ OnInit,
8
+ ViewChild,
9
+ forwardRef,
10
+ } from '@angular/core';
11
+ import {
12
+ ControlValueAccessor,
13
+ FormsModule,
14
+ NG_VALUE_ACCESSOR,
15
+ ReactiveFormsModule,
16
+ } from '@angular/forms';
17
+ import { JAlertToastService } from 'tailjng';
18
+ import { Icons } from '../../.config/icons/icons.lucide';
19
+ import {
20
+ INPUT_DISABLED_OVERLAY_CLASSES,
21
+ INPUT_FILE_LABEL_LAYOUT_CLASSES,
22
+ INPUT_FOCUS_WITHIN_CLASSES,
23
+ INPUT_PADDING_X_SINGLE,
24
+ INPUT_SINGLE_LINE_HEIGHT_CLASSES,
25
+ INPUT_SURFACE_CLASSES,
26
+ inputActionButtonClasses,
27
+ mergeInputClasses,
28
+ } from '../../.config/input/input.classes';
29
+ import { JIconComponent } from '../../icon/icon.component';
30
+ import { FileInputValue } from './file-input.types';
31
+
32
+ export type { FileInputValue } from './file-input.types';
33
+
34
+ /**
35
+ * File input with image preview, clipboard paste, and optional clear button.
36
+ *
37
+ * Install: `npx tailjng add input-file`
38
+ */
7
39
  @Component({
8
40
  selector: 'JFileInput',
9
- imports: [FormsModule, ReactiveFormsModule, NgClass, LucideAngularModule, CommonModule],
41
+ imports: [FormsModule, ReactiveFormsModule, NgClass, NgStyle, JIconComponent],
10
42
  templateUrl: './file-input.component.html',
11
43
  styleUrl: './file-input.component.css',
12
44
  providers: [
13
45
  {
14
46
  provide: NG_VALUE_ACCESSOR,
15
47
  useExisting: forwardRef(() => JFileInputComponent),
16
- multi: true
17
- }
18
- ]
48
+ multi: true,
49
+ },
50
+ ],
19
51
  })
20
- export class JFileInputComponent implements ControlValueAccessor, OnChanges, OnDestroy {
52
+ export class JFileInputComponent implements ControlValueAccessor, OnInit, OnDestroy {
53
+ // Project Icons registry — template usage: Icons.Upload, Icons.X, Icons.Clipboard
54
+ readonly Icons = Icons;
21
55
 
22
56
  @Input() id?: string;
23
57
  @Input() name?: string;
24
- @Input() accept: string = '';
25
- @Input() multiple: boolean = false;
26
- @Input() isDisabled: boolean = false;
27
- @Input() required: boolean = false;
28
- @Input() clearButton: boolean = false;
29
- @Input() showImage: boolean = false;
30
- @Input() widthImgFile: number = 0;
31
- @Input() heightImgFile: number = 0;
32
58
 
33
- @ViewChild('fileInput') fileInputRef!: ElementRef<HTMLInputElement>;
59
+ // Native accept attribute (e.g. "image/*", ".pdf")
60
+ @Input() accept = '';
34
61
 
35
- private _innerValue: any = null;
36
- private _previewUrl: string | null = null;
37
- private _fileInputCleared = false;
62
+ // Allow selecting multiple files
63
+ @Input() multiple = false;
38
64
 
39
- private readonly _uniqueId: string;
65
+ @Input() isDisabled = false;
66
+ @Input() required = false;
40
67
 
41
- // Public Getters
42
- get innerValue(): any {
43
- return this._innerValue;
44
- }
68
+ // Show clear (X) button when a file is selected
69
+ @Input() clearButton = false;
45
70
 
46
- get previewUrl(): string | null {
47
- return this._previewUrl;
48
- }
71
+ // Show image preview below the field when value is an image
72
+ @Input() showImage = false;
49
73
 
50
- get fileInputCleared(): boolean {
51
- return this._fileInputCleared;
52
- }
74
+ // Preview dimensions in px; 0 = width 100% / height auto
75
+ @Input() widthImgFile = 0;
76
+ @Input() heightImgFile = 0;
53
77
 
54
- get value(): any {
55
- return this._innerValue;
56
- }
78
+ // Placeholder when no file is selected
79
+ @Input() placeholder = 'Select file...';
57
80
 
58
- // ControlValueAccessor callbacks
59
- private _onChange: (value: any) => void = () => { };
60
- private _onTouched: () => void = () => { };
81
+ // Extra Tailwind classes on the visible label
82
+ @Input() classes = '';
61
83
 
62
- constructor(
63
- public readonly iconsService: JIconsService,
64
- private readonly alertToastService: JAlertToastService,
65
- ) {
66
- // ID unique
67
- this._uniqueId = this.generateUniqueId();
84
+ // Extra conditional classes on the visible label
85
+ @Input() ngClasses: Record<string, boolean> = {};
86
+
87
+ @ViewChild('fileInput') fileInputRef!: ElementRef<HTMLInputElement>;
88
+
89
+ private innerValue: FileInputValue = null;
90
+ private previewUrl: string | null = null;
91
+ private fileInputCleared = false;
92
+ private readonly uniqueId: string;
93
+
94
+ private onChange: (value: FileInputValue) => void = () => {};
95
+ private onTouched: () => void = () => {};
96
+
97
+ constructor(private readonly alertToastService: JAlertToastService) {
98
+ this.uniqueId = this.generateUniqueId();
68
99
  }
69
100
 
70
- ngOnDestroy(): void {
71
- if (this._previewUrl && this._previewUrl.startsWith('blob:')) {
72
- URL.revokeObjectURL(this._previewUrl);
101
+ ngOnInit(): void {
102
+ if (!this.id) {
103
+ this.id = this.uniqueId;
73
104
  }
74
105
  }
75
106
 
76
- private generateUniqueId(): string {
77
- return 'file-input-' + Math.random().toString(36).substr(2, 9) + '-' + Date.now();
107
+ ngOnDestroy(): void {
108
+ this.revokePreviewUrl();
78
109
  }
79
110
 
111
+ /**
112
+ * Whether a file (or files) is currently selected.
113
+ * @returns `true` when `innerValue` is set.
114
+ */
115
+ get hasValue(): boolean {
116
+ if (this.innerValue == null) {
117
+ return false;
118
+ }
119
+ if (Array.isArray(this.innerValue)) {
120
+ return this.innerValue.length > 0;
121
+ }
122
+ return true;
123
+ }
80
124
 
125
+ /**
126
+ * Whether the clear button should be visible.
127
+ * @returns `true` when `clearButton` is enabled and a file is selected.
128
+ */
129
+ get showClearButton(): boolean {
130
+ return this.clearButton && this.hasValue;
131
+ }
81
132
 
82
133
  /**
83
- * Writes a value to the component.
134
+ * Blob URL for the current image preview.
135
+ * @returns Preview URL or `null`.
84
136
  */
85
- writeValue(val: any): void {
86
- this._innerValue = val;
137
+ get previewImageUrl(): string | null {
138
+ return this.previewUrl;
139
+ }
87
140
 
88
- // Clean previous preview if exists
89
- if (this._previewUrl && this._previewUrl.startsWith('blob:')) {
90
- URL.revokeObjectURL(this._previewUrl);
141
+ /**
142
+ * Whether the image preview block should render.
143
+ * @returns `true` when preview URL exists and `showImage` is enabled.
144
+ */
145
+ get showPreview(): boolean {
146
+ return !!this.previewUrl && this.showImage;
147
+ }
148
+
149
+ /**
150
+ * Label text for the selected file(s).
151
+ * @returns File name, count summary, or `null` when empty.
152
+ */
153
+ get displayFileName(): string | null {
154
+ if (!this.hasValue || this.innerValue == null) {
155
+ return null;
91
156
  }
92
157
 
93
- // If the value is null or empty, clear the preview
94
- if (!val) {
95
- this._previewUrl = null;
96
- if (this.fileInputRef?.nativeElement) {
97
- this.fileInputRef.nativeElement.value = "";
98
- this._fileInputCleared = true;
158
+ if (Array.isArray(this.innerValue)) {
159
+ if (this.innerValue.length === 1) {
160
+ return this.innerValue[0].name;
99
161
  }
100
- } else if (val instanceof File && val.type?.startsWith("image")) {
101
- this.createPreview(val);
162
+ return `${this.innerValue.length} files selected`;
102
163
  }
164
+
165
+ return this.innerValue.name;
103
166
  }
104
167
 
168
+ /**
169
+ * Inline styles for the preview image.
170
+ * @returns Width/height based on `widthImgFile` and `heightImgFile`.
171
+ */
172
+ get previewStyles(): Record<string, string> {
173
+ return {
174
+ width: this.widthImgFile ? `${this.widthImgFile}px` : '100%',
175
+ height: this.heightImgFile ? `${this.heightImgFile}px` : 'auto',
176
+ };
177
+ }
105
178
 
179
+ /**
180
+ * Class map for the visible file picker label.
181
+ * @returns Base styles merged with `ngClasses` and disabled state.
182
+ */
183
+ get computedClasses(): Record<string, boolean> {
184
+ return mergeInputClasses(
185
+ INPUT_SURFACE_CLASSES,
186
+ INPUT_SINGLE_LINE_HEIGHT_CLASSES,
187
+ INPUT_FOCUS_WITHIN_CLASSES,
188
+ INPUT_PADDING_X_SINGLE,
189
+ INPUT_FILE_LABEL_LAYOUT_CLASSES,
190
+ this.ngClasses,
191
+ this.isDisabled
192
+ ? mergeInputClasses(INPUT_DISABLED_OVERLAY_CLASSES, { 'pointer-events-none': true })
193
+ : {},
194
+ );
195
+ }
106
196
 
107
197
  /**
108
- * Registers a callback function that is called when the value changes.
198
+ * Class map for icon action buttons (clear, clipboard).
199
+ * @returns Theme-aware hover utilities.
109
200
  */
110
- registerOnChange(fn: (value: any) => void): void {
111
- this._onChange = fn;
201
+ get actionButtonClasses(): Record<string, boolean> {
202
+ return inputActionButtonClasses(this.isDisabled);
112
203
  }
113
204
 
205
+ /**
206
+ * Writes the value from Angular forms into the view.
207
+ * @param value File, array of files, or null.
208
+ */
209
+ writeValue(value: FileInputValue): void {
210
+ this.innerValue = value;
211
+ this.revokePreviewUrl();
212
+
213
+ if (!value) {
214
+ this.previewUrl = null;
215
+ if (this.fileInputRef?.nativeElement) {
216
+ this.fileInputRef.nativeElement.value = '';
217
+ this.fileInputCleared = true;
218
+ }
219
+ return;
220
+ }
114
221
 
222
+ const file = Array.isArray(value) ? value[0] : value;
223
+ if (file?.type?.startsWith('image/')) {
224
+ this.createPreview(file);
225
+ }
226
+ }
115
227
 
228
+ /**
229
+ * Registers the change callback for Angular forms.
230
+ * @param fn Callback invoked when the value changes.
231
+ */
232
+ registerOnChange(fn: (value: FileInputValue) => void): void {
233
+ this.onChange = fn;
234
+ }
116
235
 
117
236
  /**
118
- * Registers a callback function that is called when the input is touched.
237
+ * Registers the touched callback for Angular forms.
238
+ * @param fn Callback invoked when the control is touched.
119
239
  */
120
240
  registerOnTouched(fn: () => void): void {
121
- this._onTouched = fn;
241
+ this.onTouched = fn;
122
242
  }
123
243
 
124
-
125
-
126
244
  /**
127
- * Sets the isDisabled state of the component.
245
+ * Syncs disabled state from Angular forms.
246
+ * @param isDisabled Whether the control is disabled.
128
247
  */
129
248
  setDisabledState(isDisabled: boolean): void {
130
249
  this.isDisabled = isDisabled;
@@ -133,122 +252,102 @@ export class JFileInputComponent implements ControlValueAccessor, OnChanges, OnD
133
252
  }
134
253
  }
135
254
 
136
-
137
-
138
255
  /**
139
- * Handles changes to the component's inputs.
140
- */
141
- ngOnChanges(changes: SimpleChanges): void {
142
- // Si no hay ID proporcionado, usar el único generado
143
- if (!this.id) {
144
- this.id = this._uniqueId;
145
- }
146
- }
147
-
148
-
149
-
150
- /**
151
- * Handles the file selection event.
256
+ * Handles native file selection.
257
+ * @param event Change event from the hidden file input.
152
258
  */
153
259
  onFileSelected(event: Event): void {
154
260
  const input = event.target as HTMLInputElement;
155
261
  const files = input.files;
156
262
 
157
- if (files && files.length > 0) {
158
- const value = this.multiple ? Array.from(files) : files[0];
159
- this._innerValue = value;
160
- this._onChange(value);
161
- this._onTouched();
162
- this._fileInputCleared = false;
163
-
164
- if (!this.multiple && value instanceof File && value.type?.startsWith("image")) {
165
- this.createPreview(value);
166
- } else {
167
- if (this._previewUrl && this._previewUrl.startsWith('blob:')) {
168
- URL.revokeObjectURL(this._previewUrl);
169
- }
170
- this._previewUrl = null;
171
- }
263
+ if (!files?.length) {
264
+ return;
172
265
  }
173
- }
174
-
175
266
 
267
+ const value: FileInputValue = this.multiple ? Array.from(files) : files[0];
268
+ this.innerValue = value;
269
+ this.onChange(value);
270
+ this.onTouched();
271
+ this.fileInputCleared = false;
272
+
273
+ const previewFile = Array.isArray(value) ? value[0] : value;
274
+ if (!this.multiple && previewFile?.type?.startsWith('image/')) {
275
+ this.createPreview(previewFile);
276
+ } else {
277
+ this.revokePreviewUrl();
278
+ this.previewUrl = null;
279
+ }
280
+ }
176
281
 
177
282
  /**
178
- * Creates a preview URL for an image file
283
+ * Marks the control as touched on blur.
179
284
  */
180
- private createPreview(file: File): void {
181
- if (this._previewUrl && this._previewUrl.startsWith('blob:')) {
182
- URL.revokeObjectURL(this._previewUrl);
183
- }
184
-
185
- this._previewUrl = URL.createObjectURL(file);
285
+ onBlur(): void {
286
+ this.onTouched();
186
287
  }
187
288
 
188
-
189
-
190
289
  /**
191
- * Clears the file input and resets the preview URL.
290
+ * Clears the selected file(s) and resets the native input.
291
+ * @param event Click event to avoid triggering the file dialog.
192
292
  */
193
- clearFile(): void {
194
- if (this._previewUrl && this._previewUrl.startsWith('blob:')) {
195
- URL.revokeObjectURL(this._previewUrl);
196
- }
197
-
198
- this._innerValue = null;
199
- this._previewUrl = null;
200
- this._onChange(this.multiple ? [] : null);
201
- this._onTouched();
202
-
203
- // Reset the file input field
204
- if (this.fileInputRef?.nativeElement) {
205
- this.fileInputRef.nativeElement.value = "";
206
- this._fileInputCleared = true;
207
- }
293
+ onClearClick(event: MouseEvent): void {
294
+ event.preventDefault();
295
+ event.stopPropagation();
296
+ this.clearFile();
208
297
  }
209
298
 
210
-
211
-
212
299
  /**
213
- * Method called when input is touched (for template)
300
+ * Pastes an image from the clipboard into the field.
301
+ * @param event Click event — stopped so the label does not open the file dialog.
214
302
  */
215
- onTouched(): void {
216
- this._onTouched();
303
+ onClipboardClick(event: MouseEvent): void {
304
+ event.preventDefault();
305
+ event.stopPropagation();
306
+ void this.getImageFromClipboard();
217
307
  }
218
308
 
219
-
220
-
221
309
  /**
222
- * Resets the file input to its initial state.
310
+ * Clears the selected file(s) and notifies Angular forms.
223
311
  */
312
+ clearFile(): void {
313
+ this.revokePreviewUrl();
314
+ this.innerValue = null;
315
+ this.previewUrl = null;
316
+ this.onChange(this.multiple ? [] : null);
317
+ this.onTouched();
318
+
319
+ if (this.fileInputRef?.nativeElement) {
320
+ this.fileInputRef.nativeElement.value = '';
321
+ this.fileInputCleared = true;
322
+ }
323
+ }
324
+
325
+ /** Alias for {@link clearFile}. */
224
326
  resetFileInput(): void {
225
327
  this.clearFile();
226
328
  }
227
329
 
228
-
229
-
230
330
  /**
231
- * Obtains an image from the clipboard and sets it as the input value.
232
- * @returns
331
+ * Reads an image from the clipboard and sets it as the field value.
233
332
  */
234
333
  async getImageFromClipboard(): Promise<void> {
235
- try {
334
+ if (this.isDisabled) {
335
+ return;
336
+ }
236
337
 
237
- // Verify support
338
+ try {
238
339
  if (!('clipboard' in navigator) || typeof navigator.clipboard.read !== 'function') {
239
340
  this.alertToastService.AlertToast({
240
341
  type: 'warning',
241
342
  title: 'No soportado',
242
343
  description: 'Tu navegador no soporta leer imágenes del portapapeles.',
243
- })
344
+ });
244
345
  return;
245
346
  }
246
347
 
247
348
  const clipboardItems = await navigator.clipboard.read();
248
-
249
- // Search for the first item that has an image type
250
- const imageItem = clipboardItems.find(item =>
251
- item.types.some(t => t.startsWith('image/'))
349
+ const imageItem = clipboardItems.find((item) =>
350
+ item.types.some((type) => type.startsWith('image/')),
252
351
  );
253
352
 
254
353
  if (!imageItem) {
@@ -256,42 +355,30 @@ export class JFileInputComponent implements ControlValueAccessor, OnChanges, OnD
256
355
  type: 'info',
257
356
  title: 'Imagen no encontrada',
258
357
  description: 'No se encontró una imagen en el portapapeles.',
259
- })
358
+ });
260
359
  return;
261
360
  }
262
361
 
263
- // Get the first image type available
264
- const imageType = imageItem.types.find(t => t.startsWith('image/')) || 'image/png';
362
+ const imageType = imageItem.types.find((type) => type.startsWith('image/')) ?? 'image/png';
265
363
  const blob = await imageItem.getType(imageType);
266
-
267
- // Create a File from the Blob so the form has a real file
268
364
  const extension = imageType.split('/')[1] || 'png';
269
365
  const fileFromClipboard = new File(
270
366
  [blob],
271
367
  `clipboard-${Date.now()}.${extension}`,
272
- { type: imageType }
368
+ { type: imageType },
273
369
  );
274
370
 
275
- // Update the internal value just like in onFileSelected
276
- const value = this.multiple ? [fileFromClipboard] : fileFromClipboard;
277
- this._innerValue = value;
278
- this._fileInputCleared = false;
279
-
280
- // Notify the reactive form
281
- this._onChange(value);
282
- this._onTouched();
283
-
284
- // Update the preview reusing existing logic
285
- if (!this.multiple && fileFromClipboard.type?.startsWith('image')) {
286
- if (this._previewUrl && this._previewUrl.startsWith('blob:')) {
287
- URL.revokeObjectURL(this._previewUrl);
288
- }
289
- this._previewUrl = URL.createObjectURL(fileFromClipboard);
290
- // this.showImage = true;
291
- }
371
+ const value: FileInputValue = this.multiple ? [fileFromClipboard] : fileFromClipboard;
372
+ this.innerValue = value;
373
+ this.fileInputCleared = false;
374
+ this.onChange(value);
375
+ this.onTouched();
292
376
 
377
+ if (!this.multiple && fileFromClipboard.type.startsWith('image/')) {
378
+ this.createPreview(fileFromClipboard);
379
+ }
293
380
  } catch (err) {
294
- console.error('Error al obtener la imagen del portapapeles:', err);
381
+ console.error('Error reading image from clipboard:', err);
295
382
  this.alertToastService.AlertToast({
296
383
  type: 'error',
297
384
  title: 'Error',
@@ -300,5 +387,21 @@ export class JFileInputComponent implements ControlValueAccessor, OnChanges, OnD
300
387
  }
301
388
  }
302
389
 
390
+ /** Creates an object URL preview for an image file. */
391
+ private createPreview(file: File): void {
392
+ this.revokePreviewUrl();
393
+ this.previewUrl = URL.createObjectURL(file);
394
+ }
395
+
396
+ /** Revokes a blob preview URL when present. */
397
+ private revokePreviewUrl(): void {
398
+ if (this.previewUrl?.startsWith('blob:')) {
399
+ URL.revokeObjectURL(this.previewUrl);
400
+ }
401
+ }
303
402
 
403
+ /** Generates a stable fallback id when `[id]` is omitted. */
404
+ private generateUniqueId(): string {
405
+ return `file-input-${Math.random().toString(36).slice(2, 11)}-${Date.now()}`;
406
+ }
304
407
  }
@@ -0,0 +1,2 @@
1
+ /** Value held by {@link JFileInputComponent} (single or multiple files). */
2
+ export type FileInputValue = File | File[] | null;
@@ -0,0 +1,67 @@
1
+ .j-range-input {
2
+ width: 100%;
3
+ height: 1.25rem;
4
+ appearance: none;
5
+ cursor: pointer;
6
+ border-radius: 0.5rem;
7
+ border: 1px solid var(--color-border);
8
+ background-color: var(--color-background);
9
+ transition: box-shadow 200ms;
10
+ }
11
+
12
+ .dark .j-range-input {
13
+ border-color: var(--color-dark-border);
14
+ background-color: var(--color-dark-background);
15
+ }
16
+
17
+ .j-range-input:focus {
18
+ outline: none;
19
+ box-shadow: 0 0 0 2px var(--color-primary);
20
+ }
21
+
22
+ .dark .j-range-input:focus {
23
+ box-shadow: 0 0 0 2px var(--color-dark-primary);
24
+ }
25
+
26
+ .j-range-input:disabled {
27
+ cursor: not-allowed;
28
+ opacity: 0.5;
29
+ }
30
+
31
+ .j-range-input::-webkit-slider-thumb {
32
+ appearance: none;
33
+ height: 2rem;
34
+ width: 2rem;
35
+ border-radius: 9999px;
36
+ cursor: pointer;
37
+ background-color: var(--color-dark-primary);
38
+ }
39
+
40
+ .dark .j-range-input::-webkit-slider-thumb {
41
+ background-color: var(--color-primary);
42
+ }
43
+
44
+ .j-range-input::-webkit-slider-thumb:hover {
45
+ background-color: var(--color-primary);
46
+ }
47
+
48
+ .dark .j-range-input::-webkit-slider-thumb:hover {
49
+ background-color: var(--color-dark-primary);
50
+ }
51
+
52
+ .j-range-input::-moz-range-thumb {
53
+ height: 1rem;
54
+ width: 1rem;
55
+ border: none;
56
+ border-radius: 9999px;
57
+ cursor: pointer;
58
+ background-color: var(--color-primary);
59
+ }
60
+
61
+ .j-range-input::-moz-range-thumb:hover {
62
+ background-color: var(--color-primary);
63
+ }
64
+
65
+ .dark .j-range-input::-moz-range-thumb:hover {
66
+ background-color: var(--color-dark-primary);
67
+ }