shirkasoft-ui-components 1.3.0 → 1.3.2

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.
@@ -34,6 +34,10 @@ class FileUploadComponent {
34
34
  this.changeFilesText = input('Cambiar archivos', ...(ngDevMode ? [{ debugName: "changeFilesText" }] : /* istanbul ignore next */ []));
35
35
  /** Enables selecting more than one file at once. */
36
36
  this.multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
37
+ /** Files already stored by the parent (read-only; the component only emits events). */
38
+ this.existingFiles = input([], ...(ngDevMode ? [{ debugName: "existingFiles" }] : /* istanbul ignore next */ []));
39
+ /** How existing files are rendered. */
40
+ this.existingDisplay = input('chips', ...(ngDevMode ? [{ debugName: "existingDisplay" }] : /* istanbul ignore next */ []));
37
41
  /** Emitted with the currently selected files. */
38
42
  this.fileSelected = output();
39
43
  /** Emitted when the last selected file is removed. */
@@ -42,6 +46,10 @@ class FileUploadComponent {
42
46
  this.fileError = output();
43
47
  /** Emitted when a file is near the size limit. */
44
48
  this.fileWarning = output();
49
+ /** Emitted when an existing file is requested for download/view. */
50
+ this.existingFileAction = output();
51
+ /** Emitted when an existing file should be removed (parent deletes it). */
52
+ this.existingFileRemove = output();
45
53
  /** Currently selected files. */
46
54
  this.selectedFiles = signal([], ...(ngDevMode ? [{ debugName: "selectedFiles" }] : /* istanbul ignore next */ []));
47
55
  /** Localized error message displayed to the user. */
@@ -218,6 +226,50 @@ class FileUploadComponent {
218
226
  }
219
227
  this.previewUrls.clear();
220
228
  }
229
+ /** Revokes pending preview URLs when the component is destroyed. */
230
+ ngOnDestroy() {
231
+ this.clearPreviews();
232
+ }
233
+ /** Emits the action (download/view) for an existing file. */
234
+ onExistingAction(file) {
235
+ this.existingFileAction.emit(file);
236
+ }
237
+ /** Emits the removal request for an existing file. */
238
+ onExistingRemove(file) {
239
+ this.existingFileRemove.emit(file);
240
+ }
241
+ /** Whether an existing file is an image (by mime type or extension). */
242
+ isExistingImage(file) {
243
+ const mime = (file.mimeType || '').toLowerCase();
244
+ if (mime.startsWith('image/'))
245
+ return true;
246
+ const ext = this.getExtension(file.name).toLowerCase();
247
+ return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext);
248
+ }
249
+ /** Resolves the extension of a file name. */
250
+ getExtension(name) {
251
+ return name.split('.').pop()?.toLowerCase() || '';
252
+ }
253
+ /** Icon + colored badge shown for non-image files, hinting at the type. */
254
+ getExistingBadge(file) {
255
+ const ext = this.getExtension(file.name);
256
+ switch (ext) {
257
+ case 'pdf':
258
+ return { label: 'PDF', color: 'bg-red-100 text-red-600', icon: 'file-text' };
259
+ case 'doc':
260
+ case 'docx':
261
+ return { label: 'DOC', color: 'bg-blue-100 text-blue-600', icon: 'file-text' };
262
+ case 'xls':
263
+ case 'xlsx':
264
+ return { label: 'XLS', color: 'bg-green-100 text-green-600', icon: 'file-spreadsheet' };
265
+ default:
266
+ return { label: ext.toUpperCase() || 'FILE', color: 'bg-slate-100 text-slate-500', icon: 'file' };
267
+ }
268
+ }
269
+ /** trackBy for existing files. */
270
+ trackExistingFile(file) {
271
+ return file.id ?? file.name;
272
+ }
221
273
  /** Human readable size (B/KB/MB). */
222
274
  formatFileSize(bytes) {
223
275
  if (bytes < 1024)
@@ -227,12 +279,12 @@ class FileUploadComponent {
227
279
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
228
280
  }
229
281
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: FileUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
230
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: FileUploadComponent, isStandalone: true, selector: "shk-file-upload", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, fileUploadText: { classPropertyName: "fileUploadText", publicName: "fileUploadText", isSignal: true, isRequired: false, transformFunction: null }, fileRecommendation: { classPropertyName: "fileRecommendation", publicName: "fileRecommendation", isSignal: true, isRequired: false, transformFunction: null }, changeFilesText: { classPropertyName: "changeFilesText", publicName: "changeFilesText", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileSelected: "fileSelected", fileRemoved: "fileRemoved", fileError: "fileError", fileWarning: "fileWarning" }, ngImport: i0, template: "<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n M\u00E1ximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
282
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: FileUploadComponent, isStandalone: true, selector: "shk-file-upload", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, fileUploadText: { classPropertyName: "fileUploadText", publicName: "fileUploadText", isSignal: true, isRequired: false, transformFunction: null }, fileRecommendation: { classPropertyName: "fileRecommendation", publicName: "fileRecommendation", isSignal: true, isRequired: false, transformFunction: null }, changeFilesText: { classPropertyName: "changeFilesText", publicName: "changeFilesText", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, existingFiles: { classPropertyName: "existingFiles", publicName: "existingFiles", isSignal: true, isRequired: false, transformFunction: null }, existingDisplay: { classPropertyName: "existingDisplay", publicName: "existingDisplay", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileSelected: "fileSelected", fileRemoved: "fileRemoved", fileError: "fileError", fileWarning: "fileWarning", existingFileAction: "existingFileAction", existingFileRemove: "existingFileRemove" }, ngImport: i0, template: "<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n M\u00E1ximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (existingFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of existingFiles(); track trackExistingFile(file)) {\n @if (existingDisplay() === 'tiles') {\n @let badge = getExistingBadge(file);\n <div class=\"relative group w-[80px]\">\n <div\n class=\"w-20 h-20 rounded-lg border _shk-border dark:[border-color:var(--shk-surface-700)] overflow-hidden cursor-pointer relative\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n >\n @if (isExistingImage(file) && file.url) {\n <img [src]=\"file.url\" [alt]=\"file.name\" class=\"w-full h-full object-cover\" />\n } @else {\n <div class=\"w-full h-full flex flex-col items-center justify-center gap-1 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"px-1.5 py-0.5 rounded text-[9px] font-semibold leading-none\" [class]=\"badge.color\">\n {{ badge.label }}\n </span>\n </div>\n }\n <div\n class=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition rounded-lg\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-5 h-5 text-white\"></svg>\n </div>\n </div>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n <span class=\"block text-[10px] _shk-text-surface-400 truncate text-center mt-1 max-w-[80px]\" [title]=\"file.name\">\n {{ file.name }}\n </span>\n </div>\n } @else {\n @let badge = getExistingBadge(file);\n <div class=\"flex items-center gap-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border _shk-border dark:[border-color:var(--shk-surface-200)] rounded-full pl-3 pr-1.5 py-1 text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-4 h-4 _shk-text-surface-400 shrink-0\"></svg>\n <span class=\"max-w-[200px] truncate\" [title]=\"file.name\">{{ file.name }}</span>\n <button\n type=\"button\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-surface-200)] _shk-text-surface-500 hover:[color:var(--shk-primary-600)]\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-error-50)] _shk-text-surface-500 hover:[color:var(--shk-error-600)]\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n </div>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
231
283
  }
232
284
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: FileUploadComponent, decorators: [{
233
285
  type: Component,
234
- args: [{ selector: 'shk-file-upload', standalone: true, imports: [CommonModule, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n M\u00E1ximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>" }]
235
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], fileUploadText: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileUploadText", required: false }] }], fileRecommendation: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileRecommendation", required: false }] }], changeFilesText: [{ type: i0.Input, args: [{ isSignal: true, alias: "changeFilesText", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], fileError: [{ type: i0.Output, args: ["fileError"] }], fileWarning: [{ type: i0.Output, args: ["fileWarning"] }] } });
286
+ args: [{ selector: 'shk-file-upload', standalone: true, imports: [CommonModule, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n M\u00E1ximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (existingFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of existingFiles(); track trackExistingFile(file)) {\n @if (existingDisplay() === 'tiles') {\n @let badge = getExistingBadge(file);\n <div class=\"relative group w-[80px]\">\n <div\n class=\"w-20 h-20 rounded-lg border _shk-border dark:[border-color:var(--shk-surface-700)] overflow-hidden cursor-pointer relative\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n >\n @if (isExistingImage(file) && file.url) {\n <img [src]=\"file.url\" [alt]=\"file.name\" class=\"w-full h-full object-cover\" />\n } @else {\n <div class=\"w-full h-full flex flex-col items-center justify-center gap-1 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"px-1.5 py-0.5 rounded text-[9px] font-semibold leading-none\" [class]=\"badge.color\">\n {{ badge.label }}\n </span>\n </div>\n }\n <div\n class=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition rounded-lg\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-5 h-5 text-white\"></svg>\n </div>\n </div>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n <span class=\"block text-[10px] _shk-text-surface-400 truncate text-center mt-1 max-w-[80px]\" [title]=\"file.name\">\n {{ file.name }}\n </span>\n </div>\n } @else {\n @let badge = getExistingBadge(file);\n <div class=\"flex items-center gap-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border _shk-border dark:[border-color:var(--shk-surface-200)] rounded-full pl-3 pr-1.5 py-1 text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-4 h-4 _shk-text-surface-400 shrink-0\"></svg>\n <span class=\"max-w-[200px] truncate\" [title]=\"file.name\">{{ file.name }}</span>\n <button\n type=\"button\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-surface-200)] _shk-text-surface-500 hover:[color:var(--shk-primary-600)]\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-error-50)] _shk-text-surface-500 hover:[color:var(--shk-error-600)]\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n </div>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>" }]
287
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], fileUploadText: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileUploadText", required: false }] }], fileRecommendation: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileRecommendation", required: false }] }], changeFilesText: [{ type: i0.Input, args: [{ isSignal: true, alias: "changeFilesText", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], existingFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingFiles", required: false }] }], existingDisplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingDisplay", required: false }] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], fileError: [{ type: i0.Output, args: ["fileError"] }], fileWarning: [{ type: i0.Output, args: ["fileWarning"] }], existingFileAction: [{ type: i0.Output, args: ["existingFileAction"] }], existingFileRemove: [{ type: i0.Output, args: ["existingFileRemove"] }] } });
236
288
 
237
289
  /**
238
290
  * Generated bundle index. Do not edit.
@@ -1 +1 @@
1
- {"version":3,"file":"shirkasoft-ui-components-file-upload.mjs","sources":["../../../projects/ui-components/file-upload/file-upload.component.ts","../../../projects/ui-components/file-upload/file-upload.component.html","../../../projects/ui-components/file-upload/shirkasoft-ui-components-file-upload.ts"],"sourcesContent":["import { Component, input, output, signal, computed, ChangeDetectionStrategy, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { TranslocoService } from '@jsverse/transloco';\nimport { LucideDynamicIcon, LucideUpload } from '@lucide/angular';\nimport { getIcon as getIconFromCentral } from 'shirkasoft-ui-components/utils';\n\nexport interface FileUploadError {\n type: 'size' | 'type';\n message: string;\n file: File;\n}\n\n/**\n * Shirkasoft UI Components.\n * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).\n * Belongs to Shirkasoft.\n */\n\n/**\n * Drag & drop file uploader with file type/size validation, image previews,\n * multiple files support and localized error messages.\n */\n@Component({\n selector: 'shk-file-upload',\n standalone: true,\n imports: [CommonModule, LucideDynamicIcon],\n templateUrl: './file-upload.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class FileUploadComponent {\n protected getIcon = (name: any) => getIconFromCentral(name, LucideUpload);\n private transloco = inject(TranslocoService);\n\n /** Main label of the uploader. */\n label = input<string>('Cargar archivo');\n /** Accepted MIME types/extensions (e.g. 'image/*', '.pdf'). */\n accept = input<string>('image/*');\n /** Maximum allowed size per file (bytes). */\n maxFileSize = input<number>(2 * 1024 * 1024);\n /** Maximum number of files (0 = unlimited). */\n maxFiles = input<number>(0);\n /** Text of the file picker button. */\n fileUploadText = input<string>('Seleccionar archivo');\n /** Helper text shown next to the label. */\n fileRecommendation = input<string>('');\n /** Text shown when files are already loaded. */\n changeFilesText = input<string>('Cambiar archivos');\n /** Enables selecting more than one file at once. */\n multiple = input<boolean>(false);\n\n /** Emitted with the currently selected files. */\n fileSelected = output<File[]>();\n /** Emitted when the last selected file is removed. */\n fileRemoved = output<void>();\n /** Emitted when a file fails validation (type/size). */\n fileError = output<FileUploadError>();\n /** Emitted when a file is near the size limit. */\n fileWarning = output<FileUploadError>();\n\n /** Currently selected files. */\n selectedFiles = signal<File[]>([]);\n /** Localized error message displayed to the user. */\n errorMessage = signal<string>('');\n /** Whether the user is dragging files over the zone. */\n isDragOver = signal<boolean>(false);\n\n /** Object URLs used for image previews. */\n private previewUrls = new Map<File, string>();\n\n /** Whether another selection is allowed. */\n canAddMore = computed(() => {\n if (!this.multiple()) return this.selectedFiles().length === 0;\n const max = this.maxFiles();\n if (max <= 0) return true;\n return this.selectedFiles().length < max;\n });\n\n /** True when the maximum number of files has been reached. */\n maxReached = computed(() => {\n const max = this.maxFiles();\n if (max <= 0) return false;\n return this.selectedFiles().length >= max;\n });\n\n /** Whether the file is an image (used for previews). */\n isImageFile(file: File): boolean {\n return file.type.startsWith('image/');\n }\n\n /** Returns (creating on demand) the object URL for a file preview. */\n previewUrl(file: File): string {\n if (!this.previewUrls.has(file)) {\n this.previewUrls.set(file, URL.createObjectURL(file));\n }\n return this.previewUrls.get(file) ?? '';\n }\n\n /** Marks the drop zone as active while dragging over it. */\n onDragOver(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(true);\n }\n\n /** Clears the drag-over state when the drag leaves the zone. */\n onDragLeave(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n }\n\n /** Processes the files dropped on the zone. */\n onDrop(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n\n const files = event.dataTransfer?.files;\n if (files && files.length > 0) {\n this.processFiles(Array.from(files));\n }\n }\n\n /** Processes the files picked from the native file input. */\n onFileChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n const files = input.files;\n if (files && files.length > 0) {\n this.processFiles(Array.from(files));\n }\n input.value = '';\n }\n\n /** Validates and appends the received files, emitting errors/warnings. */\n private processFiles(files: File[]): void {\n this.errorMessage.set('');\n const validFiles: File[] = [];\n\n for (const file of files) {\n const error = this.validateFile(file);\n if (error) {\n this.fileError.emit(error);\n this.errorMessage.set(error.message);\n return;\n }\n\n const warning = this.checkSizeWarning(file);\n if (warning) {\n this.fileWarning.emit(warning);\n }\n\n validFiles.push(file);\n }\n\n const max = this.maxFiles();\n if (max > 0) {\n const currentCount = this.selectedFiles().length;\n const allowed = max - currentCount;\n if (validFiles.length > allowed) {\n const errorMsg = this.transloco.translate('fileUpload.maxFiles', undefined, `Máximo ${max} archivos permitidos`);\n this.errorMessage.set(errorMsg);\n return;\n }\n }\n\n if (this.multiple()) {\n this.selectedFiles.set([...this.selectedFiles(), ...validFiles]);\n } else {\n this.clearPreviews();\n this.selectedFiles.set(validFiles);\n }\n\n this.fileSelected.emit(this.selectedFiles());\n }\n\n /** Validates a file against `accept` and `maxFileSize`. */\n private validateFile(file: File): FileUploadError | null {\n const acceptStr = this.accept();\n if (acceptStr && acceptStr !== '*') {\n const acceptTypes = acceptStr.split(',').map((t) => t.trim());\n const valid = acceptTypes.some((type) => {\n if (type.endsWith('/*')) {\n const category = type.replace('/*', '');\n return file.type.startsWith(category);\n }\n if (type.startsWith('.')) {\n const ext = type.slice(1).toLowerCase();\n return file.name.toLowerCase().endsWith('.' + ext);\n }\n if (type.includes('/')) {\n return file.type === type;\n }\n return false;\n });\n\n if (!valid) {\n return {\n type: 'type',\n message: this.transloco.translate('fileUpload.invalidType', { accept: acceptStr }, `Tipo de archivo no válido. Formatos aceptados: ${acceptStr}`),\n file,\n };\n }\n }\n\n if (this.maxFileSize() > 0 && file.size > this.maxFileSize()) {\n return {\n type: 'size',\n message: this.transloco.translate('fileUpload.fileTooLarge', { size: this.formatFileSize(this.maxFileSize()) }, `El archivo excede el tamaño máximo de ${this.formatFileSize(this.maxFileSize())}`),\n file,\n };\n }\n\n return null;\n }\n\n /** Emits a warning when the file is close to the size limit (>90%). */\n private checkSizeWarning(file: File): FileUploadError | null {\n if (this.maxFileSize() > 0 && file.size > this.maxFileSize() * 0.9) {\n return {\n type: 'size',\n message: this.transloco.translate('fileUpload.sizeWarning', { size: this.formatFileSize(this.maxFileSize()) }, `El archivo está cerca del límite de tamaño (${this.formatFileSize(this.maxFileSize())})`),\n file,\n };\n }\n return null;\n }\n\n /** Removes a file, revoking its preview URL. */\n removeFile(file: File): void {\n const url = this.previewUrls.get(file);\n if (url) {\n URL.revokeObjectURL(url);\n this.previewUrls.delete(file);\n }\n\n this.selectedFiles.update((files) => files.filter((f) => f !== file));\n this.errorMessage.set('');\n\n if (this.selectedFiles().length === 0) {\n this.fileRemoved.emit();\n }\n }\n\n /** Clears all selected files and previews. */\n changeFiles(): void {\n this.clearPreviews();\n this.selectedFiles.set([]);\n this.errorMessage.set('');\n }\n\n /** Revokes every stored preview object URL. */\n private clearPreviews(): void {\n for (const [file, url] of this.previewUrls) {\n URL.revokeObjectURL(url);\n }\n this.previewUrls.clear();\n }\n\n /** Human readable size (B/KB/MB). */\n private formatFileSize(bytes: number): string {\n if (bytes < 1024) return bytes + ' B';\n if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';\n return (bytes / (1024 * 1024)).toFixed(1) + ' MB';\n }\n}","<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n Máximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":["getIconFromCentral"],"mappings":"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"}
1
+ {"version":3,"file":"shirkasoft-ui-components-file-upload.mjs","sources":["../../../projects/ui-components/file-upload/file-upload.component.ts","../../../projects/ui-components/file-upload/file-upload.component.html","../../../projects/ui-components/file-upload/shirkasoft-ui-components-file-upload.ts"],"sourcesContent":["import {\n Component,\n input,\n output,\n signal,\n computed,\n ChangeDetectionStrategy,\n inject,\n OnDestroy,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { TranslocoService } from '@jsverse/transloco';\nimport { LucideDynamicIcon, LucideUpload } from '@lucide/angular';\nimport { getIcon as getIconFromCentral } from 'shirkasoft-ui-components/utils';\n\nexport interface FileUploadError {\n type: 'size' | 'type';\n message: string;\n file: File;\n}\n\nexport interface ExistingFile {\n id?: number | string;\n name: string;\n url?: string;\n mimeType?: string;\n size?: number;\n}\n\n/**\n * Shirkasoft UI Components.\n * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).\n * Belongs to Shirkasoft.\n */\n\n/**\n * Drag & drop file uploader with file type/size validation, image previews,\n * multiple files support and localized error messages.\n */\n@Component({\n selector: 'shk-file-upload',\n standalone: true,\n imports: [CommonModule, LucideDynamicIcon],\n templateUrl: './file-upload.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class FileUploadComponent implements OnDestroy {\n protected getIcon = (name: any) => getIconFromCentral(name, LucideUpload);\n private transloco = inject(TranslocoService);\n\n /** Main label of the uploader. */\n label = input<string>('Cargar archivo');\n /** Accepted MIME types/extensions (e.g. 'image/*', '.pdf'). */\n accept = input<string>('image/*');\n /** Maximum allowed size per file (bytes). */\n maxFileSize = input<number>(2 * 1024 * 1024);\n /** Maximum number of files (0 = unlimited). */\n maxFiles = input<number>(0);\n /** Text of the file picker button. */\n fileUploadText = input<string>('Seleccionar archivo');\n /** Helper text shown next to the label. */\n fileRecommendation = input<string>('');\n /** Text shown when files are already loaded. */\n changeFilesText = input<string>('Cambiar archivos');\n /** Enables selecting more than one file at once. */\n multiple = input<boolean>(false);\n /** Files already stored by the parent (read-only; the component only emits events). */\n existingFiles = input<ExistingFile[]>([]);\n /** How existing files are rendered. */\n existingDisplay = input<'chips' | 'tiles'>('chips');\n\n /** Emitted with the currently selected files. */\n fileSelected = output<File[]>();\n /** Emitted when the last selected file is removed. */\n fileRemoved = output<void>();\n /** Emitted when a file fails validation (type/size). */\n fileError = output<FileUploadError>();\n /** Emitted when a file is near the size limit. */\n fileWarning = output<FileUploadError>();\n /** Emitted when an existing file is requested for download/view. */\n existingFileAction = output<ExistingFile>();\n /** Emitted when an existing file should be removed (parent deletes it). */\n existingFileRemove = output<ExistingFile>();\n\n /** Currently selected files. */\n selectedFiles = signal<File[]>([]);\n /** Localized error message displayed to the user. */\n errorMessage = signal<string>('');\n /** Whether the user is dragging files over the zone. */\n isDragOver = signal<boolean>(false);\n\n /** Object URLs used for image previews. */\n private previewUrls = new Map<File, string>();\n\n /** Whether another selection is allowed. */\n canAddMore = computed(() => {\n if (!this.multiple()) return this.selectedFiles().length === 0;\n const max = this.maxFiles();\n if (max <= 0) return true;\n return this.selectedFiles().length < max;\n });\n\n /** True when the maximum number of files has been reached. */\n maxReached = computed(() => {\n const max = this.maxFiles();\n if (max <= 0) return false;\n return this.selectedFiles().length >= max;\n });\n\n /** Whether the file is an image (used for previews). */\n isImageFile(file: File): boolean {\n return file.type.startsWith('image/');\n }\n\n /** Returns (creating on demand) the object URL for a file preview. */\n previewUrl(file: File): string {\n if (!this.previewUrls.has(file)) {\n this.previewUrls.set(file, URL.createObjectURL(file));\n }\n return this.previewUrls.get(file) ?? '';\n }\n\n /** Marks the drop zone as active while dragging over it. */\n onDragOver(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(true);\n }\n\n /** Clears the drag-over state when the drag leaves the zone. */\n onDragLeave(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n }\n\n /** Processes the files dropped on the zone. */\n onDrop(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n\n const files = event.dataTransfer?.files;\n if (files && files.length > 0) {\n this.processFiles(Array.from(files));\n }\n }\n\n /** Processes the files picked from the native file input. */\n onFileChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n const files = input.files;\n if (files && files.length > 0) {\n this.processFiles(Array.from(files));\n }\n input.value = '';\n }\n\n /** Validates and appends the received files, emitting errors/warnings. */\n private processFiles(files: File[]): void {\n this.errorMessage.set('');\n const validFiles: File[] = [];\n\n for (const file of files) {\n const error = this.validateFile(file);\n if (error) {\n this.fileError.emit(error);\n this.errorMessage.set(error.message);\n return;\n }\n\n const warning = this.checkSizeWarning(file);\n if (warning) {\n this.fileWarning.emit(warning);\n }\n\n validFiles.push(file);\n }\n\n const max = this.maxFiles();\n if (max > 0) {\n const currentCount = this.selectedFiles().length;\n const allowed = max - currentCount;\n if (validFiles.length > allowed) {\n const errorMsg = this.transloco.translate('fileUpload.maxFiles', undefined, `Máximo ${max} archivos permitidos`);\n this.errorMessage.set(errorMsg);\n return;\n }\n }\n\n if (this.multiple()) {\n this.selectedFiles.set([...this.selectedFiles(), ...validFiles]);\n } else {\n this.clearPreviews();\n this.selectedFiles.set(validFiles);\n }\n\n this.fileSelected.emit(this.selectedFiles());\n }\n\n /** Validates a file against `accept` and `maxFileSize`. */\n private validateFile(file: File): FileUploadError | null {\n const acceptStr = this.accept();\n if (acceptStr && acceptStr !== '*') {\n const acceptTypes = acceptStr.split(',').map((t) => t.trim());\n const valid = acceptTypes.some((type) => {\n if (type.endsWith('/*')) {\n const category = type.replace('/*', '');\n return file.type.startsWith(category);\n }\n if (type.startsWith('.')) {\n const ext = type.slice(1).toLowerCase();\n return file.name.toLowerCase().endsWith('.' + ext);\n }\n if (type.includes('/')) {\n return file.type === type;\n }\n return false;\n });\n\n if (!valid) {\n return {\n type: 'type',\n message: this.transloco.translate('fileUpload.invalidType', { accept: acceptStr }, `Tipo de archivo no válido. Formatos aceptados: ${acceptStr}`),\n file,\n };\n }\n }\n\n if (this.maxFileSize() > 0 && file.size > this.maxFileSize()) {\n return {\n type: 'size',\n message: this.transloco.translate('fileUpload.fileTooLarge', { size: this.formatFileSize(this.maxFileSize()) }, `El archivo excede el tamaño máximo de ${this.formatFileSize(this.maxFileSize())}`),\n file,\n };\n }\n\n return null;\n }\n\n /** Emits a warning when the file is close to the size limit (>90%). */\n private checkSizeWarning(file: File): FileUploadError | null {\n if (this.maxFileSize() > 0 && file.size > this.maxFileSize() * 0.9) {\n return {\n type: 'size',\n message: this.transloco.translate('fileUpload.sizeWarning', { size: this.formatFileSize(this.maxFileSize()) }, `El archivo está cerca del límite de tamaño (${this.formatFileSize(this.maxFileSize())})`),\n file,\n };\n }\n return null;\n }\n\n /** Removes a file, revoking its preview URL. */\n removeFile(file: File): void {\n const url = this.previewUrls.get(file);\n if (url) {\n URL.revokeObjectURL(url);\n this.previewUrls.delete(file);\n }\n\n this.selectedFiles.update((files) => files.filter((f) => f !== file));\n this.errorMessage.set('');\n\n if (this.selectedFiles().length === 0) {\n this.fileRemoved.emit();\n }\n }\n\n /** Clears all selected files and previews. */\n changeFiles(): void {\n this.clearPreviews();\n this.selectedFiles.set([]);\n this.errorMessage.set('');\n }\n\n /** Revokes every stored preview object URL. */\n private clearPreviews(): void {\n for (const [file, url] of this.previewUrls) {\n URL.revokeObjectURL(url);\n }\n this.previewUrls.clear();\n }\n\n /** Revokes pending preview URLs when the component is destroyed. */\n ngOnDestroy(): void {\n this.clearPreviews();\n }\n\n /** Emits the action (download/view) for an existing file. */\n onExistingAction(file: ExistingFile): void {\n this.existingFileAction.emit(file);\n }\n\n /** Emits the removal request for an existing file. */\n onExistingRemove(file: ExistingFile): void {\n this.existingFileRemove.emit(file);\n }\n\n /** Whether an existing file is an image (by mime type or extension). */\n isExistingImage(file: ExistingFile): boolean {\n const mime = (file.mimeType || '').toLowerCase();\n if (mime.startsWith('image/')) return true;\n const ext = this.getExtension(file.name).toLowerCase();\n return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext);\n }\n\n /** Resolves the extension of a file name. */\n getExtension(name: string): string {\n return name.split('.').pop()?.toLowerCase() || '';\n }\n\n /** Icon + colored badge shown for non-image files, hinting at the type. */\n getExistingBadge(file: ExistingFile): { label: string; color: string; icon: string } {\n const ext = this.getExtension(file.name);\n switch (ext) {\n case 'pdf':\n return { label: 'PDF', color: 'bg-red-100 text-red-600', icon: 'file-text' };\n case 'doc':\n case 'docx':\n return { label: 'DOC', color: 'bg-blue-100 text-blue-600', icon: 'file-text' };\n case 'xls':\n case 'xlsx':\n return { label: 'XLS', color: 'bg-green-100 text-green-600', icon: 'file-spreadsheet' };\n default:\n return { label: ext.toUpperCase() || 'FILE', color: 'bg-slate-100 text-slate-500', icon: 'file' };\n }\n }\n\n /** trackBy for existing files. */\n trackExistingFile(file: ExistingFile): any {\n return file.id ?? file.name;\n }\n\n /** Human readable size (B/KB/MB). */\n private formatFileSize(bytes: number): string {\n if (bytes < 1024) return bytes + ' B';\n if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';\n return (bytes / (1024 * 1024)).toFixed(1) + ' MB';\n }\n}","<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n Máximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (existingFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of existingFiles(); track trackExistingFile(file)) {\n @if (existingDisplay() === 'tiles') {\n @let badge = getExistingBadge(file);\n <div class=\"relative group w-[80px]\">\n <div\n class=\"w-20 h-20 rounded-lg border _shk-border dark:[border-color:var(--shk-surface-700)] overflow-hidden cursor-pointer relative\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n >\n @if (isExistingImage(file) && file.url) {\n <img [src]=\"file.url\" [alt]=\"file.name\" class=\"w-full h-full object-cover\" />\n } @else {\n <div class=\"w-full h-full flex flex-col items-center justify-center gap-1 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"px-1.5 py-0.5 rounded text-[9px] font-semibold leading-none\" [class]=\"badge.color\">\n {{ badge.label }}\n </span>\n </div>\n }\n <div\n class=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition rounded-lg\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-5 h-5 text-white\"></svg>\n </div>\n </div>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n <span class=\"block text-[10px] _shk-text-surface-400 truncate text-center mt-1 max-w-[80px]\" [title]=\"file.name\">\n {{ file.name }}\n </span>\n </div>\n } @else {\n @let badge = getExistingBadge(file);\n <div class=\"flex items-center gap-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border _shk-border dark:[border-color:var(--shk-surface-200)] rounded-full pl-3 pr-1.5 py-1 text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-4 h-4 _shk-text-surface-400 shrink-0\"></svg>\n <span class=\"max-w-[200px] truncate\" [title]=\"file.name\">{{ file.name }}</span>\n <button\n type=\"button\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-surface-200)] _shk-text-surface-500 hover:[color:var(--shk-primary-600)]\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-error-50)] _shk-text-surface-500 hover:[color:var(--shk-error-600)]\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n </div>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":["getIconFromCentral"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { model, input, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { input, model, inject, DestroyRef, signal, computed, effect, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule } from '@angular/common';
5
- import * as i2 from '@angular/forms';
6
- import { FormsModule } from '@angular/forms';
5
+ import { ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
6
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
7
 
8
8
  /**
9
9
  * Shirkasoft UI Components.
@@ -11,14 +11,22 @@ import { FormsModule } from '@angular/forms';
11
11
  * Belongs to Shirkasoft.
12
12
  */
13
13
  /**
14
- * Numeric input tailored for currency values with a optional percentage mode.
15
- * Uses two-way bindable models: `value` and `isPercentage`.
14
+ * Currency/percentage numeric input with reactive forms support.
15
+ *
16
+ * Binding modes (highest precedence first):
17
+ * - `control`: a FormControl (or use it inside a form with `formControlName`).
18
+ * - `value`/`valueChange` or `[(value)]`: two-way model signals.
19
+ *
20
+ * Text-based input (`type="text"` + `inputmode="decimal"`) with decimal
21
+ * precision control, min/max clamping on blur and thousands formatting.
16
22
  */
17
23
  class PriceInputComponent {
18
24
  constructor() {
19
- /** Numeric value, bindable with [(value)]. */
20
- this.value = model.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
21
- /** Whether the input works as a percentage, bindable with [(isPercentage)]. */
25
+ /** Reactive control. When provided it takes precedence over `value`/`valueChange`. */
26
+ this.control = input(undefined, ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
27
+ /** Numeric value (two-way), used when no `control` is bound. */
28
+ this.value = model(0, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
29
+ /** Whether the input works as a percentage (two-way). */
22
30
  this.isPercentage = model(false, ...(ngDevMode ? [{ debugName: "isPercentage" }] : /* istanbul ignore next */ []));
23
31
  /** Label displayed above the field. */
24
32
  this.label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
@@ -28,33 +36,204 @@ class PriceInputComponent {
28
36
  this.required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
29
37
  /** Disables the input. */
30
38
  this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
31
- /** Minimum allowed value. */
39
+ /** Minimum allowed value (clamped on blur). */
32
40
  this.min = input(0, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
33
- /** Maximum allowed value. */
41
+ /** Maximum allowed value (clamped on blur). */
34
42
  this.max = input(Infinity, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
43
+ /** Maximum number of decimal places (0 = integer). */
44
+ this.decimals = input(2, ...(ngDevMode ? [{ debugName: "decimals" }] : /* istanbul ignore next */ []));
45
+ /** Symbol shown in the mode toggle button when not a percentage. */
46
+ this.currency = input('$', ...(ngDevMode ? [{ debugName: "currency" }] : /* istanbul ignore next */ []));
47
+ /** Placeholder text of the native input. */
48
+ this.placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
49
+ /** Accessible label; falls back to `label`. */
50
+ this.ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
51
+ /** Blocks the minus sign from being typed. */
52
+ this.preventNegative = input(false, ...(ngDevMode ? [{ debugName: "preventNegative" }] : /* istanbul ignore next */ []));
35
53
  /** Shows the error styling on the field. */
36
54
  this.showError = input(false, ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
37
55
  /** Message rendered when the field is in error state. */
38
56
  this.errorMessage = input('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
57
+ this.destroyRef = inject(DestroyRef);
58
+ /** Text currently shown in the input (raw while focused, formatted on blur). */
59
+ this.displayText = signal('', ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
60
+ /** Whether the native input currently has focus. */
61
+ this.focused = signal(false, ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
62
+ /** Disabled state coming from the reactive control. */
63
+ this.controlDisabled = signal(false, ...(ngDevMode ? [{ debugName: "controlDisabled" }] : /* istanbul ignore next */ []));
64
+ /** Prevents re-subscribing to the same form control. */
65
+ this.ctrlSubscribed = false;
66
+ /** Effective disabled state combining the input and the form binding. */
67
+ this.isDisabled = computed(() => this.disabled() || this.controlDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
68
+ /** Callbacks used by the ControlValueAccessor contract. */
69
+ this.onChange = () => { };
70
+ this.onTouched = () => { };
71
+ // Subscribe once to the bound control for change/status tracking.
72
+ effect(() => {
73
+ const ctrl = this.control();
74
+ if (!ctrl || this.ctrlSubscribed)
75
+ return;
76
+ this.ctrlSubscribed = true;
77
+ this.controlDisabled.set(ctrl.disabled);
78
+ ctrl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((v) => {
79
+ if (!this.focused()) {
80
+ this.displayText.set(this.formatNumber(v ?? 0));
81
+ }
82
+ });
83
+ ctrl.statusChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
84
+ this.controlDisabled.set(ctrl.disabled);
85
+ });
86
+ });
87
+ // Reflect the effective value into the display (unless the user is typing).
88
+ effect(() => {
89
+ const ctrl = this.control();
90
+ const v = ctrl ? ctrl.value : this.value();
91
+ if (!this.focused()) {
92
+ this.displayText.set(this.formatNumber(v ?? 0));
93
+ }
94
+ });
39
95
  }
40
96
  /** Toggles between amount and percentage modes (unless disabled). */
41
97
  toggleMode() {
42
- if (this.disabled())
98
+ if (this.isDisabled())
43
99
  return;
44
- this.isPercentage.update(v => !v);
100
+ this.isPercentage.update((v) => !v);
45
101
  }
46
- /** Parses the user input and updates the bound `value`. */
102
+ /** Selects the whole value on focus so the user can type over it. */
103
+ onFocus(event) {
104
+ this.focused.set(true);
105
+ event.target.select();
106
+ }
107
+ /** Clamps, rounds and formats the value when the field loses focus. */
108
+ onBlur() {
109
+ this.focused.set(false);
110
+ this.onTouched();
111
+ let v = parseFloat(this.displayText().replace(/,/g, ''));
112
+ if (isNaN(v))
113
+ v = 0;
114
+ const min = this.min();
115
+ const max = this.max();
116
+ if (isFinite(min) && v < min)
117
+ v = min;
118
+ if (isFinite(max) && v > max)
119
+ v = max;
120
+ v = this.roundTo(v);
121
+ this.setValue(v);
122
+ this.displayText.set(this.formatNumber(v));
123
+ }
124
+ /** Blocks characters that would break the numeric format. */
125
+ onKeydown(event) {
126
+ const allowed = [
127
+ 'Backspace',
128
+ 'Tab',
129
+ 'End',
130
+ 'Home',
131
+ 'ArrowLeft',
132
+ 'ArrowRight',
133
+ 'Delete',
134
+ 'Enter',
135
+ 'Escape',
136
+ ];
137
+ if (allowed.includes(event.key))
138
+ return;
139
+ if (/^\d$/.test(event.key))
140
+ return;
141
+ if (event.key === '.' || event.key === ',') {
142
+ const input = event.target;
143
+ if (input.value.includes('.') || input.value.includes(','))
144
+ event.preventDefault();
145
+ return;
146
+ }
147
+ if (event.key === '-') {
148
+ const input = event.target;
149
+ if (this.preventNegative() || this.min() >= 0) {
150
+ event.preventDefault();
151
+ }
152
+ else if (input.value.includes('-')) {
153
+ event.preventDefault();
154
+ }
155
+ return;
156
+ }
157
+ event.preventDefault();
158
+ }
159
+ /** Sanitizes the raw input (digits + one separator + optional sign) and writes the value once. */
47
160
  onInput(event) {
48
161
  const input = event.target;
49
- this.value.set(parseFloat(input.value) || 0);
162
+ const pos = input.selectionStart ?? input.value.length;
163
+ let cleaned = input.value.replace(/[^0-9.,-]/g, '');
164
+ if (this.preventNegative() || this.min() >= 0)
165
+ cleaned = cleaned.replace(/-/g, '');
166
+ cleaned = cleaned.replace(/,/g, '.');
167
+ const dot = cleaned.indexOf('.');
168
+ if (dot >= 0) {
169
+ const dec = Math.max(0, this.decimals());
170
+ const intPart = cleaned.slice(0, dot);
171
+ const decPart = cleaned.slice(dot + 1, dot + 1 + dec);
172
+ cleaned = intPart + '.' + decPart;
173
+ }
174
+ input.value = cleaned;
175
+ const newPos = Math.min(pos, cleaned.length);
176
+ input.setSelectionRange(newPos, newPos);
177
+ this.displayText.set(cleaned);
178
+ const num = parseFloat(cleaned);
179
+ this.setValue(isNaN(num) ? 0 : num);
180
+ }
181
+ /** Writes the value to the effective binding source. */
182
+ setValue(v) {
183
+ const ctrl = this.control();
184
+ if (ctrl) {
185
+ ctrl.setValue(v, { emitEvent: true });
186
+ }
187
+ else {
188
+ this.value.set(v);
189
+ }
190
+ this.onChange(v);
191
+ }
192
+ roundTo(v) {
193
+ const factor = Math.pow(10, Math.max(0, this.decimals()));
194
+ return Math.round(v * factor) / factor;
195
+ }
196
+ /** Formats a number with thousands separator and up to `decimals` places. */
197
+ formatNumber(v) {
198
+ if (v == null || isNaN(v))
199
+ return '0';
200
+ const dec = Math.max(0, this.decimals());
201
+ return v.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: dec });
202
+ }
203
+ // ── ControlValueAccessor ───────────────────────────────────────────────────
204
+ writeValue(value) {
205
+ if (!this.focused()) {
206
+ this.displayText.set(this.formatNumber(value ?? 0));
207
+ }
208
+ }
209
+ registerOnChange(fn) {
210
+ this.onChange = fn;
211
+ }
212
+ registerOnTouched(fn) {
213
+ this.onTouched = fn;
214
+ }
215
+ setDisabledState(isDisabled) {
216
+ this.controlDisabled.set(isDisabled);
50
217
  }
51
218
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: PriceInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
52
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: PriceInputComponent, isStandalone: true, selector: "shk-price-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, isPercentage: { classPropertyName: "isPercentage", publicName: "isPercentage", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, showError: { classPropertyName: "showError", publicName: "showError", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", isPercentage: "isPercentageChange" }, ngImport: i0, template: "<div>\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n @if (required()) {\n <span class=\"text-red-400\">*</span>\n }\n </div>\n }\n <div class=\"flex\">\n <input\n [id]=\"inputId()\"\n type=\"number\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"value.set($event ?? 0)\"\n (input)=\"onInput($event)\"\n [disabled]=\"disabled()\"\n [min]=\"min()\"\n [max]=\"max()\"\n step=\"0.01\"\n class=\"block w-full px-3 py-2 _shk-text-surface-700 dark:[color:var(--shk-surface-700)] _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-l-lg transition-colors placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-400)] focus:outline-none focus:[background-color:var(--shk-surface-50)] dark:focus:[background-color:var(--shk-surface-100)] focus:ring-1 disabled:[background-color:var(--shk-surface-50)] dark:disabled:[background-color:var(--shk-surface-100)] disabled:cursor-not-allowed sm:text-sm\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': showError(),\n '_shk-border dark:[border-color:var(--shk-surface-200)] focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !showError()\n }\"\n />\n <button\n type=\"button\"\n (click)=\"toggleMode()\"\n [disabled]=\"disabled()\"\n class=\"px-3 py-2 text-sm font-medium border rounded-r-lg transition-colors shrink-0\"\n [ngClass]=\"{\n '_shk-bg-primary-soft _shk-border-primary-200 _shk-text-primary-700 dark:[background-color:var(--shk-primary-900)] dark:[border-color:var(--shk-primary-700)] dark:[color:var(--shk-primary-300)]': isPercentage(),\n '_shk-bg-white _shk-border _shk-text-surface-500 dark:[background-color:var(--shk-surface-100)] dark:[border-color:var(--shk-surface-200)] dark:[color:var(--shk-surface-400)]': !isPercentage(),\n 'cursor-not-allowed opacity-50': disabled()\n }\"\n >\n {{ isPercentage() ? '%' : '$' }}\n </button>\n </div>\n @if (showError()) {\n <div class=\"text-xs _shk-text-red-500 mt-1\">\n {{ errorMessage() }}\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
219
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: PriceInputComponent, isStandalone: true, selector: "shk-price-input", inputs: { control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, isPercentage: { classPropertyName: "isPercentage", publicName: "isPercentage", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, decimals: { classPropertyName: "decimals", publicName: "decimals", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, preventNegative: { classPropertyName: "preventNegative", publicName: "preventNegative", isSignal: true, isRequired: false, transformFunction: null }, showError: { classPropertyName: "showError", publicName: "showError", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", isPercentage: "isPercentageChange" }, providers: [
220
+ {
221
+ provide: NG_VALUE_ACCESSOR,
222
+ useExisting: forwardRef(() => PriceInputComponent),
223
+ multi: true,
224
+ },
225
+ ], ngImport: i0, template: "<div>\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n @if (required()) {\n <span class=\"text-red-400\">*</span>\n }\n </div>\n }\n <div class=\"flex\">\n <input\n [id]=\"inputId()\"\n type=\"text\"\n inputmode=\"decimal\"\n autocomplete=\"off\"\n [value]=\"displayText()\"\n [placeholder]=\"placeholder()\"\n [attr.aria-label]=\"ariaLabel() || label()\"\n [attr.aria-invalid]=\"showError() || null\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n (input)=\"onInput($event)\"\n class=\"block w-full px-3 py-2 _shk-text-surface-700 dark:[color:var(--shk-surface-700)] _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-l-lg transition-colors placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-400)] focus:outline-none focus:[background-color:var(--shk-surface-50)] dark:focus:[background-color:var(--shk-surface-100)] focus:ring-1 disabled:[background-color:var(--shk-surface-50)] dark:disabled:[background-color:var(--shk-surface-100)] disabled:cursor-not-allowed sm:text-sm\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': showError(),\n '_shk-border dark:[border-color:var(--shk-surface-200)] focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !showError()\n }\"\n />\n <button\n type=\"button\"\n (click)=\"toggleMode()\"\n [disabled]=\"isDisabled()\"\n class=\"px-3 py-2 text-sm font-medium border rounded-r-lg transition-colors shrink-0\"\n [ngClass]=\"{\n '_shk-bg-primary-soft _shk-border-primary-200 _shk-text-primary-700 dark:[background-color:var(--shk-primary-900)] dark:[border-color:var(--shk-primary-700)] dark:[color:var(--shk-primary-300)]': isPercentage(),\n '_shk-bg-white _shk-border _shk-text-surface-500 dark:[background-color:var(--shk-surface-100)] dark:[border-color:var(--shk-surface-200)] dark:[color:var(--shk-surface-400)]': !isPercentage(),\n 'cursor-not-allowed opacity-50': isDisabled()\n }\"\n >\n {{ isPercentage() ? '%' : currency() }}\n </button>\n </div>\n @if (showError()) {\n <div class=\"text-xs _shk-text-red-500 mt-1\">\n {{ errorMessage() }}\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
53
226
  }
54
227
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: PriceInputComponent, decorators: [{
55
228
  type: Component,
56
- args: [{ selector: 'shk-price-input', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div>\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n @if (required()) {\n <span class=\"text-red-400\">*</span>\n }\n </div>\n }\n <div class=\"flex\">\n <input\n [id]=\"inputId()\"\n type=\"number\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"value.set($event ?? 0)\"\n (input)=\"onInput($event)\"\n [disabled]=\"disabled()\"\n [min]=\"min()\"\n [max]=\"max()\"\n step=\"0.01\"\n class=\"block w-full px-3 py-2 _shk-text-surface-700 dark:[color:var(--shk-surface-700)] _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-l-lg transition-colors placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-400)] focus:outline-none focus:[background-color:var(--shk-surface-50)] dark:focus:[background-color:var(--shk-surface-100)] focus:ring-1 disabled:[background-color:var(--shk-surface-50)] dark:disabled:[background-color:var(--shk-surface-100)] disabled:cursor-not-allowed sm:text-sm\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': showError(),\n '_shk-border dark:[border-color:var(--shk-surface-200)] focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !showError()\n }\"\n />\n <button\n type=\"button\"\n (click)=\"toggleMode()\"\n [disabled]=\"disabled()\"\n class=\"px-3 py-2 text-sm font-medium border rounded-r-lg transition-colors shrink-0\"\n [ngClass]=\"{\n '_shk-bg-primary-soft _shk-border-primary-200 _shk-text-primary-700 dark:[background-color:var(--shk-primary-900)] dark:[border-color:var(--shk-primary-700)] dark:[color:var(--shk-primary-300)]': isPercentage(),\n '_shk-bg-white _shk-border _shk-text-surface-500 dark:[background-color:var(--shk-surface-100)] dark:[border-color:var(--shk-surface-200)] dark:[color:var(--shk-surface-400)]': !isPercentage(),\n 'cursor-not-allowed opacity-50': disabled()\n }\"\n >\n {{ isPercentage() ? '%' : '$' }}\n </button>\n </div>\n @if (showError()) {\n <div class=\"text-xs _shk-text-red-500 mt-1\">\n {{ errorMessage() }}\n </div>\n }\n</div>\n" }]
57
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], isPercentage: [{ type: i0.Input, args: [{ isSignal: true, alias: "isPercentage", required: false }] }, { type: i0.Output, args: ["isPercentageChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], showError: [{ type: i0.Input, args: [{ isSignal: true, alias: "showError", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }] } });
229
+ args: [{ selector: 'shk-price-input', standalone: true, imports: [CommonModule, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
230
+ {
231
+ provide: NG_VALUE_ACCESSOR,
232
+ useExisting: forwardRef(() => PriceInputComponent),
233
+ multi: true,
234
+ },
235
+ ], template: "<div>\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n @if (required()) {\n <span class=\"text-red-400\">*</span>\n }\n </div>\n }\n <div class=\"flex\">\n <input\n [id]=\"inputId()\"\n type=\"text\"\n inputmode=\"decimal\"\n autocomplete=\"off\"\n [value]=\"displayText()\"\n [placeholder]=\"placeholder()\"\n [attr.aria-label]=\"ariaLabel() || label()\"\n [attr.aria-invalid]=\"showError() || null\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n (input)=\"onInput($event)\"\n class=\"block w-full px-3 py-2 _shk-text-surface-700 dark:[color:var(--shk-surface-700)] _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-l-lg transition-colors placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-400)] focus:outline-none focus:[background-color:var(--shk-surface-50)] dark:focus:[background-color:var(--shk-surface-100)] focus:ring-1 disabled:[background-color:var(--shk-surface-50)] dark:disabled:[background-color:var(--shk-surface-100)] disabled:cursor-not-allowed sm:text-sm\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': showError(),\n '_shk-border dark:[border-color:var(--shk-surface-200)] focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !showError()\n }\"\n />\n <button\n type=\"button\"\n (click)=\"toggleMode()\"\n [disabled]=\"isDisabled()\"\n class=\"px-3 py-2 text-sm font-medium border rounded-r-lg transition-colors shrink-0\"\n [ngClass]=\"{\n '_shk-bg-primary-soft _shk-border-primary-200 _shk-text-primary-700 dark:[background-color:var(--shk-primary-900)] dark:[border-color:var(--shk-primary-700)] dark:[color:var(--shk-primary-300)]': isPercentage(),\n '_shk-bg-white _shk-border _shk-text-surface-500 dark:[background-color:var(--shk-surface-100)] dark:[border-color:var(--shk-surface-200)] dark:[color:var(--shk-surface-400)]': !isPercentage(),\n 'cursor-not-allowed opacity-50': isDisabled()\n }\"\n >\n {{ isPercentage() ? '%' : currency() }}\n </button>\n </div>\n @if (showError()) {\n <div class=\"text-xs _shk-text-red-500 mt-1\">\n {{ errorMessage() }}\n </div>\n }\n</div>\n" }]
236
+ }], ctorParameters: () => [], propDecorators: { control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], isPercentage: [{ type: i0.Input, args: [{ isSignal: true, alias: "isPercentage", required: false }] }, { type: i0.Output, args: ["isPercentageChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], decimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "decimals", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], preventNegative: [{ type: i0.Input, args: [{ isSignal: true, alias: "preventNegative", required: false }] }], showError: [{ type: i0.Input, args: [{ isSignal: true, alias: "showError", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }] } });
58
237
 
59
238
  /**
60
239
  * Generated bundle index. Do not edit.