codexly-ui 0.2.1 → 0.4.0

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.
@@ -4850,6 +4850,46 @@ const UPLOAD_SIZE_MAP = {
4850
4850
  md: { zone: 'py-9 px-8 gap-3', icon: 'lg', title: 'text-sm', hint: 'text-xs' },
4851
4851
  lg: { zone: 'py-12 px-10 gap-4', icon: 'xl', title: 'text-base', hint: 'text-sm' },
4852
4852
  };
4853
+ const FILE_TYPE_BY_MIME = {
4854
+ 'application/pdf': { icon: 'picture_as_pdf', color: 'red' },
4855
+ 'application/msword': { icon: 'description', color: 'blue' },
4856
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': { icon: 'description', color: 'blue' },
4857
+ 'application/vnd.ms-excel': { icon: 'table_chart', color: 'green' },
4858
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { icon: 'table_chart', color: 'green' },
4859
+ 'text/csv': { icon: 'table_chart', color: 'green' },
4860
+ 'application/vnd.ms-powerpoint': { icon: 'slideshow', color: 'orange' },
4861
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': { icon: 'slideshow', color: 'orange' },
4862
+ 'text/plain': { icon: 'article', color: 'slate' },
4863
+ 'application/zip': { icon: 'folder_zip', color: 'amber' },
4864
+ 'application/x-zip-compressed': { icon: 'folder_zip', color: 'amber' },
4865
+ 'application/x-rar-compressed': { icon: 'folder_zip', color: 'amber' },
4866
+ 'application/x-7z-compressed': { icon: 'folder_zip', color: 'amber' },
4867
+ };
4868
+ const FILE_TYPE_BY_EXTENSION = {
4869
+ pdf: { icon: 'picture_as_pdf', color: 'red' },
4870
+ doc: { icon: 'description', color: 'blue' },
4871
+ docx: { icon: 'description', color: 'blue' },
4872
+ xls: { icon: 'table_chart', color: 'green' },
4873
+ xlsx: { icon: 'table_chart', color: 'green' },
4874
+ csv: { icon: 'table_chart', color: 'green' },
4875
+ ppt: { icon: 'slideshow', color: 'orange' },
4876
+ pptx: { icon: 'slideshow', color: 'orange' },
4877
+ txt: { icon: 'article', color: 'slate' },
4878
+ zip: { icon: 'folder_zip', color: 'amber' },
4879
+ rar: { icon: 'folder_zip', color: 'amber' },
4880
+ '7z': { icon: 'folder_zip', color: 'amber' },
4881
+ };
4882
+ const FILE_TYPE_DEFAULT = { icon: 'draft', color: 'slate' };
4883
+ function resolveFileTypeVisual(name, mimeType) {
4884
+ if (mimeType?.startsWith('video/'))
4885
+ return { icon: 'movie', color: 'purple' };
4886
+ if (mimeType?.startsWith('audio/'))
4887
+ return { icon: 'audiotrack', color: 'pink' };
4888
+ if (mimeType && FILE_TYPE_BY_MIME[mimeType])
4889
+ return FILE_TYPE_BY_MIME[mimeType];
4890
+ const ext = name.split('.').pop()?.toLowerCase() ?? '';
4891
+ return FILE_TYPE_BY_EXTENSION[ext] ?? FILE_TYPE_DEFAULT;
4892
+ }
4853
4893
 
4854
4894
  class ClxUploadComponent {
4855
4895
  _themeSvc = inject(ClxThemeService);
@@ -4863,13 +4903,20 @@ class ClxUploadComponent {
4863
4903
  size = input(undefined, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
4864
4904
  _size = computed(() => (this.size() ?? this._themeSvc.config().defaultSize), ...(ngDevMode ? [{ debugName: "_size" }] : /* istanbul ignore next */ []));
4865
4905
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4906
+ /** Already-persisted files (e.g. previously uploaded to the backend). Rendered in the same
4907
+ * preview grid as newly-selected local files. The component never deletes these itself —
4908
+ * removing one only emits `existingFileRemove`, letting the caller decide (e.g. call the API). */
4909
+ existingFiles = input([], ...(ngDevMode ? [{ debugName: "existingFiles" }] : /* istanbul ignore next */ []));
4866
4910
  // ── Outputs ────────────────────────────────────────────────────────────────
4867
4911
  onUploadError = output();
4868
4912
  onUploadSuccess = output();
4913
+ existingFileRemove = output();
4869
4914
  // ── Internal state ─────────────────────────────────────────────────────────
4870
4915
  _files = signal([], ...(ngDevMode ? [{ debugName: "_files" }] : /* istanbul ignore next */ []));
4871
4916
  _isDragging = signal(false, ...(ngDevMode ? [{ debugName: "_isDragging" }] : /* istanbul ignore next */ []));
4872
4917
  _errorKey = signal(null, ...(ngDevMode ? [{ debugName: "_errorKey" }] : /* istanbul ignore next */ []));
4918
+ /** File → object URL cache, so we don't re-create (and leak) a new URL on every re-render. */
4919
+ _objectUrls = new Map();
4873
4920
  // ── CVA ────────────────────────────────────────────────────────────────────
4874
4921
  _ngControl = inject(NgControl, { optional: true, self: true });
4875
4922
  _cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_cvaDisabled" }] : /* istanbul ignore next */ []));
@@ -4886,11 +4933,37 @@ class ClxUploadComponent {
4886
4933
  _disabled = computed(() => this._cvaDisabled() || this.disabled(), ...(ngDevMode ? [{ debugName: "_disabled" }] : /* istanbul ignore next */ []));
4887
4934
  _sizeConfig = computed(() => UPLOAD_SIZE_MAP[this._size()], ...(ngDevMode ? [{ debugName: "_sizeConfig" }] : /* istanbul ignore next */ []));
4888
4935
  _isError = computed(() => this._errorKey() !== null, ...(ngDevMode ? [{ debugName: "_isError" }] : /* istanbul ignore next */ []));
4889
- _fileItemClass = computed(() => {
4890
- const t = resolveColor(this._color());
4891
- return `flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${t.borderLight} ${t.bgSubtle}`;
4892
- }, ...(ngDevMode ? [{ debugName: "_fileItemClass" }] : /* istanbul ignore next */ []));
4893
- _fileIconClass = computed(() => resolveColor(this._color()).textSubtle, ...(ngDevMode ? [{ debugName: "_fileIconClass" }] : /* istanbul ignore next */ []));
4936
+ _thumbClass = computed(() => `relative group aspect-square ${resolveRadius(this._themeSvc.config().borderRadius)} overflow-hidden border border-clx-border bg-clx-surface-2`, ...(ngDevMode ? [{ debugName: "_thumbClass" }] : /* istanbul ignore next */ []));
4937
+ _fileCardClass = computed(() => `relative group aspect-square ${resolveRadius(this._themeSvc.config().borderRadius)} border border-clx-border bg-clx-surface-2 flex flex-col items-center justify-center gap-1 p-2`, ...(ngDevMode ? [{ debugName: "_fileCardClass" }] : /* istanbul ignore next */ []));
4938
+ _previewItems = computed(() => {
4939
+ const existing = this.existingFiles().map(f => ({
4940
+ kind: 'existing',
4941
+ key: `existing:${f.id}`,
4942
+ name: f.name ?? '',
4943
+ isImage: true,
4944
+ thumbUrl: f.url,
4945
+ fileIcon: '',
4946
+ fileColor: '',
4947
+ status: f.status,
4948
+ existing: f,
4949
+ }));
4950
+ const fresh = this._files().map(file => {
4951
+ const isImage = file.type.startsWith('image/');
4952
+ const visual = isImage ? null : resolveFileTypeVisual(file.name, file.type);
4953
+ return {
4954
+ kind: 'new',
4955
+ key: `new:${file.name}:${file.size}:${file.lastModified}`,
4956
+ name: file.name,
4957
+ size: file.size,
4958
+ isImage,
4959
+ thumbUrl: isImage ? this._getObjectUrl(file) : null,
4960
+ fileIcon: visual?.icon ?? '',
4961
+ fileColor: visual?.color ?? '',
4962
+ file,
4963
+ };
4964
+ });
4965
+ return [...existing, ...fresh];
4966
+ }, ...(ngDevMode ? [{ debugName: "_previewItems" }] : /* istanbul ignore next */ []));
4894
4967
  _zoneClass = computed(() => {
4895
4968
  const base = [
4896
4969
  'flex flex-col items-center justify-center text-center',
@@ -4957,15 +5030,13 @@ class ClxUploadComponent {
4957
5030
  // ── CVA implementation ─────────────────────────────────────────────────────
4958
5031
  writeValue(value) {
4959
5032
  this._cvaConnected = true;
4960
- if (!value) {
4961
- this._files.set([]);
4962
- }
4963
- else if (Array.isArray(value)) {
4964
- this._files.set(value);
4965
- }
4966
- else {
4967
- this._files.set([value]);
5033
+ const next = !value ? [] : Array.isArray(value) ? value : [value];
5034
+ const nextSet = new Set(next);
5035
+ for (const file of this._files()) {
5036
+ if (!nextSet.has(file))
5037
+ this._releaseObjectUrl(file);
4968
5038
  }
5039
+ this._files.set(next);
4969
5040
  }
4970
5041
  registerOnChange(fn) {
4971
5042
  this._onChange = fn;
@@ -5004,13 +5075,39 @@ class ClxUploadComponent {
5004
5075
  if (files?.length)
5005
5076
  this._addFiles(files);
5006
5077
  }
5007
- _removeFile(file) {
5078
+ _removeItem(item) {
5079
+ if (item.kind === 'existing') {
5080
+ this.existingFileRemove.emit(item.existing.id);
5081
+ return;
5082
+ }
5083
+ const file = item.file;
5008
5084
  this._files.update(current => current.filter(f => f !== file));
5085
+ this._releaseObjectUrl(file);
5009
5086
  this._errorKey.set(null);
5010
5087
  this._ngControl?.control?.setErrors(null);
5011
5088
  this._emit();
5012
5089
  this._onTouched();
5013
5090
  }
5091
+ _getObjectUrl(file) {
5092
+ let url = this._objectUrls.get(file);
5093
+ if (!url) {
5094
+ url = URL.createObjectURL(file);
5095
+ this._objectUrls.set(file, url);
5096
+ }
5097
+ return url;
5098
+ }
5099
+ _releaseObjectUrl(file) {
5100
+ const url = this._objectUrls.get(file);
5101
+ if (url) {
5102
+ URL.revokeObjectURL(url);
5103
+ this._objectUrls.delete(file);
5104
+ }
5105
+ }
5106
+ ngOnDestroy() {
5107
+ for (const url of this._objectUrls.values())
5108
+ URL.revokeObjectURL(url);
5109
+ this._objectUrls.clear();
5110
+ }
5014
5111
  // ── Helpers ────────────────────────────────────────────────────────────────
5015
5112
  _addFiles(rawFiles) {
5016
5113
  const files = Array.from(rawFiles);
@@ -5083,7 +5180,7 @@ class ClxUploadComponent {
5083
5180
  return `${(bytes / 1_048_576).toFixed(1)} MB`;
5084
5181
  }
5085
5182
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5086
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxUploadComponent, isStandalone: true, selector: "clx-upload", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", 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 }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onUploadError: "onUploadError", onUploadSuccess: "onUploadSuccess" }, host: { classAttribute: "block w-full" }, viewQueries: [{ propertyName: "_fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
5183
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxUploadComponent, isStandalone: true, selector: "clx-upload", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", 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 }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, existingFiles: { classPropertyName: "existingFiles", publicName: "existingFiles", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onUploadError: "onUploadError", onUploadSuccess: "onUploadSuccess", existingFileRemove: "existingFileRemove" }, host: { classAttribute: "block w-full" }, viewQueries: [{ propertyName: "_fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
5087
5184
  <!-- Hidden file input -->
5088
5185
  <input
5089
5186
  #fileInput
@@ -5127,25 +5224,48 @@ class ClxUploadComponent {
5127
5224
  </div>
5128
5225
  </div>
5129
5226
 
5130
- <!-- ── File list ─────────────────────────────────────────────────────────── -->
5131
- @if (_files().length) {
5132
- <div class="mt-3 space-y-2">
5133
- @for (file of _files(); track file.name + file.size) {
5134
- <div [class]="_fileItemClass()">
5135
- <span clx-icon name="description" size="sm" [class]="_fileIconClass() + ' shrink-0'"></span>
5136
- <span class="flex-1 text-sm text-clx-text-label font-medium truncate">{{ file.name }}</span>
5137
- <span class="text-xs text-clx-text-muted shrink-0">{{ _formatSize(file.size) }}</span>
5138
- <button
5139
- clx-button
5140
- variant="ghost"
5141
- [color]="_color()"
5142
- shape="circle"
5143
- size="sm"
5144
- icon="close"
5145
- [iconOnly]="true"
5146
- (click)="_removeFile(file); $event.stopPropagation()">
5147
- </button>
5148
- </div>
5227
+ <!-- ── Preview list (existing + new files, images get a real thumbnail) ───── -->
5228
+ @if (_previewItems().length) {
5229
+ <div class="mt-3 grid grid-cols-3 sm:grid-cols-4 gap-2">
5230
+ @for (item of _previewItems(); track item.key) {
5231
+ @if (item.isImage) {
5232
+ <div [class]="_thumbClass()">
5233
+ <img [src]="item.thumbUrl" [alt]="item.name" loading="lazy" class="w-full h-full object-cover" />
5234
+
5235
+ @if (item.status === 'PENDING' || item.status === 'PROCESSING') {
5236
+ <div class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center gap-1">
5237
+ <span clx-icon name="progress_activity" size="md" class="text-white animate-spin"></span>
5238
+ </div>
5239
+ } @else if (item.status === 'FAILED') {
5240
+ <div class="absolute inset-0 bg-red-500/40 flex items-center justify-center">
5241
+ <span clx-icon name="error" size="md" class="text-white"></span>
5242
+ </div>
5243
+ }
5244
+
5245
+ <button
5246
+ type="button"
5247
+ class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 hover:opacity-100 transition-opacity bg-black/60 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center"
5248
+ [disabled]="_disabled()"
5249
+ (click)="_removeItem(item); $event.stopPropagation()">
5250
+ <span clx-icon name="close" size="xs"></span>
5251
+ </button>
5252
+ </div>
5253
+ } @else {
5254
+ <div [class]="_fileCardClass()">
5255
+ <span clx-icon [name]="item.fileIcon" size="lg" [class]="'text-' + item.fileColor + '-500'"></span>
5256
+ <span class="text-xs text-clx-text-label font-medium text-center truncate w-full px-1">{{ item.name }}</span>
5257
+ @if (item.size !== undefined) {
5258
+ <span class="text-[11px] text-clx-text-muted">{{ _formatSize(item.size) }}</span>
5259
+ }
5260
+ <button
5261
+ type="button"
5262
+ class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 hover:opacity-100 transition-opacity bg-black/60 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center"
5263
+ [disabled]="_disabled()"
5264
+ (click)="_removeItem(item); $event.stopPropagation()">
5265
+ <span clx-icon name="close" size="xs"></span>
5266
+ </button>
5267
+ </div>
5268
+ }
5149
5269
  }
5150
5270
  </div>
5151
5271
  }
@@ -5157,14 +5277,14 @@ class ClxUploadComponent {
5157
5277
  {{ _errorMessage() }}
5158
5278
  </p>
5159
5279
  }
5160
- `, isInline: true, dependencies: [{ kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
5280
+ `, isInline: true, dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
5161
5281
  }
5162
5282
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxUploadComponent, decorators: [{
5163
5283
  type: Component,
5164
5284
  args: [{
5165
5285
  selector: 'clx-upload',
5166
5286
  standalone: true,
5167
- imports: [ClxButtonComponent, ClxIconComponent],
5287
+ imports: [ClxIconComponent],
5168
5288
  template: `
5169
5289
  <!-- Hidden file input -->
5170
5290
  <input
@@ -5209,25 +5329,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
5209
5329
  </div>
5210
5330
  </div>
5211
5331
 
5212
- <!-- ── File list ─────────────────────────────────────────────────────────── -->
5213
- @if (_files().length) {
5214
- <div class="mt-3 space-y-2">
5215
- @for (file of _files(); track file.name + file.size) {
5216
- <div [class]="_fileItemClass()">
5217
- <span clx-icon name="description" size="sm" [class]="_fileIconClass() + ' shrink-0'"></span>
5218
- <span class="flex-1 text-sm text-clx-text-label font-medium truncate">{{ file.name }}</span>
5219
- <span class="text-xs text-clx-text-muted shrink-0">{{ _formatSize(file.size) }}</span>
5220
- <button
5221
- clx-button
5222
- variant="ghost"
5223
- [color]="_color()"
5224
- shape="circle"
5225
- size="sm"
5226
- icon="close"
5227
- [iconOnly]="true"
5228
- (click)="_removeFile(file); $event.stopPropagation()">
5229
- </button>
5230
- </div>
5332
+ <!-- ── Preview list (existing + new files, images get a real thumbnail) ───── -->
5333
+ @if (_previewItems().length) {
5334
+ <div class="mt-3 grid grid-cols-3 sm:grid-cols-4 gap-2">
5335
+ @for (item of _previewItems(); track item.key) {
5336
+ @if (item.isImage) {
5337
+ <div [class]="_thumbClass()">
5338
+ <img [src]="item.thumbUrl" [alt]="item.name" loading="lazy" class="w-full h-full object-cover" />
5339
+
5340
+ @if (item.status === 'PENDING' || item.status === 'PROCESSING') {
5341
+ <div class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center gap-1">
5342
+ <span clx-icon name="progress_activity" size="md" class="text-white animate-spin"></span>
5343
+ </div>
5344
+ } @else if (item.status === 'FAILED') {
5345
+ <div class="absolute inset-0 bg-red-500/40 flex items-center justify-center">
5346
+ <span clx-icon name="error" size="md" class="text-white"></span>
5347
+ </div>
5348
+ }
5349
+
5350
+ <button
5351
+ type="button"
5352
+ class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 hover:opacity-100 transition-opacity bg-black/60 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center"
5353
+ [disabled]="_disabled()"
5354
+ (click)="_removeItem(item); $event.stopPropagation()">
5355
+ <span clx-icon name="close" size="xs"></span>
5356
+ </button>
5357
+ </div>
5358
+ } @else {
5359
+ <div [class]="_fileCardClass()">
5360
+ <span clx-icon [name]="item.fileIcon" size="lg" [class]="'text-' + item.fileColor + '-500'"></span>
5361
+ <span class="text-xs text-clx-text-label font-medium text-center truncate w-full px-1">{{ item.name }}</span>
5362
+ @if (item.size !== undefined) {
5363
+ <span class="text-[11px] text-clx-text-muted">{{ _formatSize(item.size) }}</span>
5364
+ }
5365
+ <button
5366
+ type="button"
5367
+ class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 hover:opacity-100 transition-opacity bg-black/60 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center"
5368
+ [disabled]="_disabled()"
5369
+ (click)="_removeItem(item); $event.stopPropagation()">
5370
+ <span clx-icon name="close" size="xs"></span>
5371
+ </button>
5372
+ </div>
5373
+ }
5231
5374
  }
5232
5375
  </div>
5233
5376
  }
@@ -5246,7 +5389,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
5246
5389
  class: 'block w-full',
5247
5390
  },
5248
5391
  }]
5249
- }], ctorParameters: () => [], propDecorators: { multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], onUploadError: [{ type: i0.Output, args: ["onUploadError"] }], onUploadSuccess: [{ type: i0.Output, args: ["onUploadSuccess"] }], _fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }] } });
5392
+ }], ctorParameters: () => [], propDecorators: { multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], existingFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingFiles", required: false }] }], onUploadError: [{ type: i0.Output, args: ["onUploadError"] }], onUploadSuccess: [{ type: i0.Output, args: ["onUploadSuccess"] }], existingFileRemove: [{ type: i0.Output, args: ["existingFileRemove"] }], _fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }] } });
5250
5393
 
5251
5394
  const COLOR_PICKER_SIZE_MAP = {
5252
5395
  sm: { minH: 'min-h-8', text: 'text-sm', label: 'text-xs font-medium', hint: 'text-xs', px: 'px-2.5', swatchSz: 'w-4 h-4', btnSize: 'xs' },
@@ -11977,6 +12120,10 @@ class ClxTableComponent {
11977
12120
  singleSelect = input(false, ...(ngDevMode ? [{ debugName: "singleSelect" }] : /* istanbul ignore next */ []));
11978
12121
  searchable = input(true, ...(ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []));
11979
12122
  pagination = input(true, ...(ngDevMode ? [{ debugName: "pagination" }] : /* istanbul ignore next */ []));
12123
+ /** When true, `data` is treated as the current page only (already paginated by the caller/backend).
12124
+ * Client-side search/sort/slicing is skipped; `totalItems` drives the pagination control. */
12125
+ serverSide = input(false, ...(ngDevMode ? [{ debugName: "serverSide" }] : /* istanbul ignore next */ []));
12126
+ totalItems = input(undefined, ...(ngDevMode ? [{ debugName: "totalItems" }] : /* istanbul ignore next */ []));
11980
12127
  color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
11981
12128
  _color = computed(() => this.color() ?? this._themeSvc.config().primaryColor, ...(ngDevMode ? [{ debugName: "_color" }] : /* istanbul ignore next */ []));
11982
12129
  emptyIcon = input('table_chart', ...(ngDevMode ? [{ debugName: "emptyIcon" }] : /* istanbul ignore next */ []));
@@ -12025,6 +12172,8 @@ class ClxTableComponent {
12025
12172
  return this._columnDefMap().get(key);
12026
12173
  }
12027
12174
  _filteredData = computed(() => {
12175
+ if (this.serverSide())
12176
+ return this.data();
12028
12177
  let result = [...this.data()];
12029
12178
  const query = this.searchQuery().toLowerCase().trim();
12030
12179
  if (query) {
@@ -12044,12 +12193,12 @@ class ClxTableComponent {
12044
12193
  }, ...(ngDevMode ? [{ debugName: "_filteredData" }] : /* istanbul ignore next */ []));
12045
12194
  _paginatedData = computed(() => {
12046
12195
  const filtered = this._filteredData();
12047
- if (!this.pagination())
12196
+ if (this.serverSide() || !this.pagination())
12048
12197
  return filtered;
12049
12198
  const start = this.pageIndex() * this.pageSize();
12050
12199
  return filtered.slice(start, start + this.pageSize());
12051
12200
  }, ...(ngDevMode ? [{ debugName: "_paginatedData" }] : /* istanbul ignore next */ []));
12052
- _totalItems = computed(() => this._filteredData().length, ...(ngDevMode ? [{ debugName: "_totalItems" }] : /* istanbul ignore next */ []));
12201
+ _totalItems = computed(() => this.serverSide() ? (this.totalItems() ?? this.data().length) : this._filteredData().length, ...(ngDevMode ? [{ debugName: "_totalItems" }] : /* istanbul ignore next */ []));
12053
12202
  _emptyTitle = computed(() => this.emptyTitle() || (this.searchQuery() ? 'Sin resultados' : 'No hay datos'), ...(ngDevMode ? [{ debugName: "_emptyTitle" }] : /* istanbul ignore next */ []));
12054
12203
  _emptyDescription = computed(() => this.emptyDescription() ||
12055
12204
  (this.searchQuery()
@@ -12117,9 +12266,9 @@ class ClxTableComponent {
12117
12266
  this.selectionChange.emit({ selected: this.selectedItems(), allSelected: false });
12118
12267
  }
12119
12268
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
12120
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxTableComponent, isStandalone: true, selector: "clx-table", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, singleSelect: { classPropertyName: "singleSelect", publicName: "singleSelect", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, emptyTitle: { classPropertyName: "emptyTitle", publicName: "emptyTitle", isSignal: true, isRequired: false, transformFunction: null }, emptyDescription: { classPropertyName: "emptyDescription", publicName: "emptyDescription", isSignal: true, isRequired: false, transformFunction: null }, sortKey: { classPropertyName: "sortKey", publicName: "sortKey", isSignal: true, isRequired: false, transformFunction: null }, sortDir: { classPropertyName: "sortDir", publicName: "sortDir", isSignal: true, isRequired: false, transformFunction: null }, pageIndex: { classPropertyName: "pageIndex", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, _currentPage: { classPropertyName: "_currentPage", publicName: "_currentPage", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortKey: "sortKeyChange", sortDir: "sortDirChange", pageIndex: "pageIndexChange", pageSize: "pageSizeChange", _currentPage: "_currentPageChange", searchQuery: "searchQueryChange", selectedItems: "selectedItemsChange", sortChange: "sortChange", pageChange: "pageChange", selectionChange: "selectionChange" }, host: { classAttribute: "block" }, queries: [{ propertyName: "_columnDefs", predicate: ClxColumnDefDirective, isSignal: true }], ngImport: i0, template: `
12269
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxTableComponent, isStandalone: true, selector: "clx-table", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, singleSelect: { classPropertyName: "singleSelect", publicName: "singleSelect", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: false, transformFunction: null }, serverSide: { classPropertyName: "serverSide", publicName: "serverSide", isSignal: true, isRequired: false, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, emptyTitle: { classPropertyName: "emptyTitle", publicName: "emptyTitle", isSignal: true, isRequired: false, transformFunction: null }, emptyDescription: { classPropertyName: "emptyDescription", publicName: "emptyDescription", isSignal: true, isRequired: false, transformFunction: null }, sortKey: { classPropertyName: "sortKey", publicName: "sortKey", isSignal: true, isRequired: false, transformFunction: null }, sortDir: { classPropertyName: "sortDir", publicName: "sortDir", isSignal: true, isRequired: false, transformFunction: null }, pageIndex: { classPropertyName: "pageIndex", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, _currentPage: { classPropertyName: "_currentPage", publicName: "_currentPage", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortKey: "sortKeyChange", sortDir: "sortDirChange", pageIndex: "pageIndexChange", pageSize: "pageSizeChange", _currentPage: "_currentPageChange", searchQuery: "searchQueryChange", selectedItems: "selectedItemsChange", sortChange: "sortChange", pageChange: "pageChange", selectionChange: "selectionChange" }, host: { classAttribute: "block" }, queries: [{ propertyName: "_columnDefs", predicate: ClxColumnDefDirective, isSignal: true }], ngImport: i0, template: `
12121
12270
  <!-- ── Toolbar: búsqueda ──────────────────────────────────────────────── -->
12122
- @if (searchable() && !loading() && _filteredData().length > 0) {
12271
+ @if (searchable() && !serverSide() && !loading() && _filteredData().length > 0) {
12123
12272
  <div class="mb-3 max-w-80 min-w-40">
12124
12273
  <clx-input
12125
12274
  placeholder="Buscar..."
@@ -12273,7 +12422,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
12273
12422
  ],
12274
12423
  template: `
12275
12424
  <!-- ── Toolbar: búsqueda ──────────────────────────────────────────────── -->
12276
- @if (searchable() && !loading() && _filteredData().length > 0) {
12425
+ @if (searchable() && !serverSide() && !loading() && _filteredData().length > 0) {
12277
12426
  <div class="mb-3 max-w-80 min-w-40">
12278
12427
  <clx-input
12279
12428
  placeholder="Buscar..."
@@ -12413,7 +12562,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
12413
12562
  changeDetection: ChangeDetectionStrategy.OnPush,
12414
12563
  host: { class: 'block' },
12415
12564
  }]
12416
- }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], singleSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleSelect", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], emptyIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIcon", required: false }] }], emptyTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyTitle", required: false }] }], emptyDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyDescription", required: false }] }], sortKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortKey", required: false }] }, { type: i0.Output, args: ["sortKeyChange"] }], sortDir: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortDir", required: false }] }, { type: i0.Output, args: ["sortDirChange"] }], pageIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: false }] }, { type: i0.Output, args: ["pageIndexChange"] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }, { type: i0.Output, args: ["pageSizeChange"] }], _currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "_currentPage", required: false }] }, { type: i0.Output, args: ["_currentPageChange"] }], searchQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchQuery", required: false }] }, { type: i0.Output, args: ["searchQueryChange"] }], selectedItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedItems", required: false }] }, { type: i0.Output, args: ["selectedItemsChange"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], _columnDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ClxColumnDefDirective), { isSignal: true }] }] } });
12565
+ }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], singleSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleSelect", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: false }] }], serverSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverSide", required: false }] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], emptyIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIcon", required: false }] }], emptyTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyTitle", required: false }] }], emptyDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyDescription", required: false }] }], sortKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortKey", required: false }] }, { type: i0.Output, args: ["sortKeyChange"] }], sortDir: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortDir", required: false }] }, { type: i0.Output, args: ["sortDirChange"] }], pageIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: false }] }, { type: i0.Output, args: ["pageIndexChange"] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }, { type: i0.Output, args: ["pageSizeChange"] }], _currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "_currentPage", required: false }] }, { type: i0.Output, args: ["_currentPageChange"] }], searchQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchQuery", required: false }] }, { type: i0.Output, args: ["searchQueryChange"] }], selectedItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedItems", required: false }] }, { type: i0.Output, args: ["selectedItemsChange"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], _columnDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ClxColumnDefDirective), { isSignal: true }] }] } });
12417
12566
 
12418
12567
  class ClxPageNotFoundComponent {
12419
12568
  title = input('Página no encontrada', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));