cloud-ide-element 1.1.94 → 1.1.95

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.
@@ -4319,11 +4319,13 @@ class CideEleFileInputComponent {
4319
4319
  groupId = signal(null, ...(ngDevMode ? [{ debugName: "groupId" }] : [])); // Group ID for multiple file uploads
4320
4320
  isMultipleUploadMode = signal(false, ...(ngDevMode ? [{ debugName: "isMultipleUploadMode" }] : [])); // Flag to track if we're in multiple upload mode
4321
4321
  hasEverUploaded = signal(false, ...(ngDevMode ? [{ debugName: "hasEverUploaded" }] : [])); // Track if this component has ever uploaded files
4322
+ failedFile = signal(null, ...(ngDevMode ? [{ debugName: "failedFile" }] : [])); // Store failed file for retry
4322
4323
  // Computed signals for better relationships
4323
4324
  hasFiles = computed(() => this.files() !== null && this.files().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
4324
4325
  canUpload = computed(() => this.hasFiles() && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canUpload" }] : []));
4325
4326
  isInErrorState = computed(() => this.uploadStatus() === 'error', ...(ngDevMode ? [{ debugName: "isInErrorState" }] : []));
4326
4327
  isInSuccessState = computed(() => this.uploadStatus() === 'success', ...(ngDevMode ? [{ debugName: "isInSuccessState" }] : []));
4328
+ canRetry = computed(() => this.isInErrorState() && this.failedFile() !== null && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canRetry" }] : []));
4327
4329
  // Optimized computed values - only calculate when needed
4328
4330
  totalFileSize = computed(() => {
4329
4331
  const files = this.files();
@@ -4465,10 +4467,27 @@ class CideEleFileInputComponent {
4465
4467
  this.fileNames.set([]);
4466
4468
  this.clearPreviews();
4467
4469
  this.uploadStatus.set('idle');
4470
+ this.failedFile.set(null); // Clear failed file when clearing files
4468
4471
  console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
4469
4472
  this.onChange(null);
4470
4473
  this.fileChange.emit(null);
4471
4474
  }
4475
+ /**
4476
+ * Retry uploading the failed file
4477
+ */
4478
+ retryUpload() {
4479
+ const failedFile = this.failedFile();
4480
+ if (!failedFile) {
4481
+ console.warn('⚠️ [FileInput] No failed file to retry');
4482
+ return;
4483
+ }
4484
+ console.log('🔄 [FileInput] Retrying upload for file:', failedFile.name);
4485
+ // Clear the failed file reference (will be set again if retry fails)
4486
+ this.failedFile.set(null);
4487
+ // Reset upload status and retry
4488
+ this.uploadStatus.set('idle');
4489
+ this.uploadFile(failedFile);
4490
+ }
4472
4491
  uploadFile(file) {
4473
4492
  console.log('🚨 [FileInput] uploadFile called for:', file.name, '- SINGLE UPLOAD INITIATED');
4474
4493
  // Angular 20: Use PendingTasks for better loading state management
@@ -4547,6 +4566,8 @@ class CideEleFileInputComponent {
4547
4566
  const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
4548
4567
  if (uploadedId) {
4549
4568
  console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
4569
+ // Clear failed file on successful upload
4570
+ this.failedFile.set(null);
4550
4571
  // Set the uploaded ID as the form control value
4551
4572
  this.onChange(uploadedId);
4552
4573
  console.log('📝 [FileInput] Form control value set to uploaded ID:', uploadedId);
@@ -4571,6 +4592,9 @@ class CideEleFileInputComponent {
4571
4592
  // Angular 20: Complete the pending task even on error
4572
4593
  // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
4573
4594
  // console.log('❌ [FileInput] Pending task completed for failed upload');
4595
+ // Store the failed file for retry
4596
+ this.failedFile.set(file);
4597
+ console.log('💾 [FileInput] Failed file stored for retry:', file.name);
4574
4598
  // Set upload status to 'error' and remove upload validation error
4575
4599
  this.uploadStatus.set('error');
4576
4600
  console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
@@ -5293,7 +5317,7 @@ class CideEleFileInputComponent {
5293
5317
  useExisting: CideEleFileInputComponent,
5294
5318
  multi: true
5295
5319
  }
5296
- ], usesOnChanges: true, ngImport: i0, template: "<div class=\"tw-flex tw-flex-col tw-gap-2\">\r\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\r\n @if (labelSignal() && !isPreviewBoxMode()) {\r\n <label class=\"tw-block tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-1.5 tw-leading-5\" [attr.for]=\"'cide-file-input-' + id()\">\r\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"tw-text-red-500 dark:tw-text-red-400\"> *</span>}\r\n </label>\r\n }\r\n \r\n <!-- Preview Box Mode -->\r\n @if (isPreviewBoxMode()) {\r\n <div class=\"tw-relative\">\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n [attr.id]=\"'cide-file-input-' + id()\"\r\n [attr.accept]=\"acceptSignal()\"\r\n [attr.multiple]=\"multipleSignal() ? true : null\"\r\n [disabled]=\"disabledSignal()\"\r\n (change)=\"onFileSelected($event)\"\r\n class=\"tw-hidden\"\r\n />\r\n \r\n <!-- Preview Box -->\r\n <div \r\n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-relative tw-overflow-hidden hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\r\n [class]=\"getPreviewBoxClasses()\"\r\n [style.width]=\"previewWidthSignal()\"\r\n [style.height]=\"previewHeightSignal()\"\r\n (click)=\"triggerFileSelect()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\r\n \r\n <!-- No Image State -->\r\n @if (!hasImages()) {\r\n <div class=\"tw-flex tw-flex-col tw-items-center tw-justify-center tw-h-full tw-p-4\">\r\n <div class=\"tw-mb-2\">\r\n <cide-ele-icon class=\"tw-text-gray-400 dark:tw-text-gray-500\" size=\"lg\">{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\r\n </div>\r\n <div class=\"tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center\">\r\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Image Preview State -->\r\n @if (hasImages()) {\r\n <div class=\"tw-relative tw-w-full tw-h-full\">\r\n <img \r\n [src]=\"previewUrls()[0]\" \r\n [alt]=\"fileNames()[0] || 'Preview image'\"\r\n class=\"tw-w-full tw-h-full tw-object-cover tw-rounded-lg\">\r\n <div class=\"tw-absolute tw-inset-0 tw-bg-black tw-bg-opacity-0 hover:tw-bg-opacity-30 tw-transition-all tw-duration-200 tw-flex tw-items-center tw-justify-center tw-rounded-lg\">\r\n <div class=\"tw-text-white tw-text-sm tw-opacity-0 hover:tw-opacity-100 tw-transition-opacity tw-duration-200\">Click to change</div>\r\n </div>\r\n @if (!disabledSignal()) {\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-top-2 tw-right-2 tw-w-6 tw-h-6 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-sm tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\r\n (click)=\"clearFiles(); $event.stopPropagation()\"\r\n title=\"Remove image\">\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n \r\n <!-- File name display for preview box mode -->\r\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\r\n <div class=\"tw-mt-2 tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center tw-truncate\">\r\n {{ fileNames()[0] }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Standard Mode -->\r\n @if (!isPreviewBoxMode()) {\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n [attr.id]=\"'cide-file-input-' + id()\"\r\n [attr.accept]=\"acceptSignal()\"\r\n [attr.multiple]=\"multipleSignal() ? true : null\"\r\n [disabled]=\"disabledSignal()\"\r\n (change)=\"onFileSelected($event)\"\r\n class=\"tw-hidden\"\r\n />\r\n \r\n <!-- Modern Drag and Drop Zone -->\r\n <div \r\n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-min-h-[60px] hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\r\n [class]=\"getDragDropZoneClasses()\"\r\n (click)=\"triggerFileSelect()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\">\r\n \r\n <div class=\"tw-flex tw-items-center tw-justify-between tw-p-3 tw-gap-3\">\r\n <!-- Icon and Text -->\r\n <div class=\"tw-flex tw-items-center tw-gap-2.5 tw-flex-1 tw-min-w-0\">\r\n <cide-ele-icon class=\"tw-flex-shrink-0 tw-transition-colors tw-duration-200\" \r\n [class]=\"getIconClasses()\" \r\n size=\"sm\">\r\n {{ isDragOver() ? 'file_download' : (hasFiles() ? 'check_circle' : 'cloud_upload') }}\r\n </cide-ele-icon>\r\n \r\n <div class=\"tw-flex tw-flex-col tw-gap-0.5 tw-min-w-0\">\r\n @if (isDragOver()) {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-blue-700 dark:tw-text-blue-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">Drop files here</span>\r\n } @else if (hasFiles()) {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-emerald-700 dark:tw-text-emerald-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\r\n @if (multipleSignal() && fileNames().length > 1) {\r\n {{ fileNames().length }} files selected\r\n } @else {\r\n {{ fileNames()[0] }}\r\n }\r\n </span>\r\n @if (totalFileSize() > 0) {\r\n <span class=\"tw-text-xs tw-text-emerald-600 dark:tw-text-emerald-400\">{{ fileSizeInMB() }} MB</span>\r\n }\r\n } @else {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\r\n {{ multipleSignal() ? 'Choose files or drag here' : 'Choose file or drag here' }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n \r\n <!-- Action Buttons -->\r\n <div class=\"tw-flex tw-gap-1 tw-flex-shrink-0\">\r\n @if (hasFiles()) {\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-w-6 tw-h-6 tw-border-none tw-rounded tw-bg-transparent tw-cursor-pointer tw-transition-all tw-duration-200 tw-text-red-600 hover:tw-bg-red-50 dark:hover:tw-bg-red-900/20 hover:tw-text-red-700\" \r\n (click)=\"clearFiles(); $event.stopPropagation()\"\r\n title=\"Clear files\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Image Preview Section (only for standard mode) -->\r\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\r\n <div class=\"tw-mt-3\">\r\n <div class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-2\">Preview:</div>\r\n <div class=\"tw-flex tw-flex-wrap tw-gap-3\">\r\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\r\n <div \r\n class=\"tw-relative tw-border tw-border-gray-200 dark:tw-border-gray-600 tw-rounded-lg tw-overflow-hidden tw-bg-white dark:tw-bg-gray-800\"\r\n [style.width]=\"previewWidthSignal()\"\r\n [style.height]=\"previewHeightSignal()\">\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-top-1 tw-right-1 tw-w-5 tw-h-5 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-xs tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer tw-z-10\"\r\n (click)=\"removePreview(i)\"\r\n title=\"Remove image\">\r\n \u00D7\r\n </button>\r\n <img \r\n [src]=\"previewUrl\" \r\n [alt]=\"fileNames()[i] || 'Preview image'\"\r\n class=\"tw-w-full tw-h-full tw-object-cover\"\r\n loading=\"lazy\">\r\n <div class=\"tw-absolute tw-bottom-0 tw-left-0 tw-right-0 tw-bg-black tw-bg-opacity-75 tw-text-white tw-text-xs tw-p-1 tw-truncate\">{{ fileNames()[i] }}</div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Upload Status and Show Files Button (only for multiple file inputs) -->\r\n @if (multiple && showFloatingUploaderSignal() && (getUploadCount() > 0 || hasActiveUploads() || hasEverUploaded())) {\r\n <div class=\"tw-flex tw-items-center tw-justify-between tw-py-1.5 tw-gap-2\">\r\n <div class=\"tw-flex tw-items-center tw-gap-2\">\r\n <cide-ele-icon class=\"tw-text-blue-600 dark:tw-text-blue-400\" size=\"sm\">cloud_upload</cide-ele-icon>\r\n <span class=\"tw-text-sm tw-text-gray-700 dark:tw-text-gray-300\">\r\n @if (hasActiveUploads()) {\r\n {{ getActiveUploadCount() }} uploading\r\n } @else if (getUploadCount() > 0) {\r\n {{ getUploadCount() }} completed\r\n } @else if (hasEverUploaded()) {\r\n View uploads\r\n }\r\n </span>\r\n </div>\r\n <button \r\n type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-w-8 tw-h-8 tw-rounded-md tw-bg-gray-100 dark:tw-bg-gray-700 hover:tw-bg-gray-200 dark:hover:tw-bg-gray-600 tw-text-gray-600 dark:tw-text-gray-300 tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\r\n (click)=\"showFloatingUploaderDialog()\"\r\n title=\"View upload progress and history\">\r\n <cide-ele-icon size=\"sm\">visibility</cide-ele-icon>\r\n </button>\r\n </div>\r\n }\r\n \r\n @if (errorTextSignal()) {\r\n <div class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400 tw-mt-1\">{{ errorTextSignal() }}</div>\r\n }\r\n @if (helperTextSignal() && !errorTextSignal()) {\r\n <div class=\"tw-text-sm tw-text-gray-500 dark:tw-text-gray-400 tw-mt-1\">{{ helperTextSignal() }}</div>\r\n }\r\n</div> ", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
5320
+ ], usesOnChanges: true, ngImport: i0, template: "<div class=\"tw-flex tw-flex-col tw-gap-2\">\r\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\r\n @if (labelSignal() && !isPreviewBoxMode()) {\r\n <label class=\"tw-block tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-1.5 tw-leading-5\" [attr.for]=\"'cide-file-input-' + id()\">\r\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"tw-text-red-500 dark:tw-text-red-400\"> *</span>}\r\n </label>\r\n }\r\n \r\n <!-- Preview Box Mode -->\r\n @if (isPreviewBoxMode()) {\r\n <div class=\"tw-relative\">\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n [attr.id]=\"'cide-file-input-' + id()\"\r\n [attr.accept]=\"acceptSignal()\"\r\n [attr.multiple]=\"multipleSignal() ? true : null\"\r\n [disabled]=\"disabledSignal()\"\r\n (change)=\"onFileSelected($event)\"\r\n class=\"tw-hidden\"\r\n />\r\n \r\n <!-- Preview Box -->\r\n <div \r\n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-relative tw-overflow-hidden hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\r\n [class]=\"getPreviewBoxClasses()\"\r\n [style.width]=\"previewWidthSignal()\"\r\n [style.height]=\"previewHeightSignal()\"\r\n (click)=\"triggerFileSelect()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\r\n \r\n <!-- No Image State -->\r\n @if (!hasImages()) {\r\n <div class=\"tw-flex tw-flex-col tw-items-center tw-justify-center tw-h-full tw-p-4\">\r\n <div class=\"tw-mb-2\">\r\n <cide-ele-icon class=\"tw-text-gray-400 dark:tw-text-gray-500\" size=\"lg\">{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\r\n </div>\r\n <div class=\"tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center\">\r\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Image Preview State -->\r\n @if (hasImages()) {\r\n <div class=\"tw-relative tw-w-full tw-h-full\">\r\n <img \r\n [src]=\"previewUrls()[0]\" \r\n [alt]=\"fileNames()[0] || 'Preview image'\"\r\n class=\"tw-w-full tw-h-full tw-object-cover tw-rounded-lg\">\r\n <div class=\"tw-absolute tw-inset-0 tw-bg-black tw-bg-opacity-0 hover:tw-bg-opacity-30 tw-transition-all tw-duration-200 tw-flex tw-items-center tw-justify-center tw-rounded-lg\">\r\n <div class=\"tw-text-white tw-text-sm tw-opacity-0 hover:tw-opacity-100 tw-transition-opacity tw-duration-200\">Click to change</div>\r\n </div>\r\n @if (!disabledSignal()) {\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-top-2 tw-right-2 tw-w-6 tw-h-6 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-sm tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\r\n (click)=\"clearFiles(); $event.stopPropagation()\"\r\n title=\"Remove image\">\r\n \u00D7\r\n </button>\r\n }\r\n @if (isInErrorState() && failedFile()) {\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-bottom-2 tw-left-1/2 tw-transform tw--translate-x-1/2 tw-px-3 tw-py-1.5 tw-text-xs tw-font-medium tw-border-none tw-rounded tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-cursor-pointer tw-transition-all tw-duration-200 disabled:tw-opacity-50 disabled:tw-cursor-not-allowed tw-shadow-lg tw-z-10\"\r\n (click)=\"retryUpload(); $event.stopPropagation()\"\r\n [disabled]=\"isUploading() || disabledSignal()\"\r\n title=\"Retry upload\">\r\n <cide-ele-icon size=\"xs\" class=\"tw-mr-1\">refresh</cide-ele-icon>\r\n Retry Upload\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n \r\n <!-- File name display for preview box mode -->\r\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\r\n <div class=\"tw-mt-2 tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center tw-truncate\">\r\n {{ fileNames()[0] }}\r\n </div>\r\n }\r\n <!-- Error message and retry button for preview box mode -->\r\n @if (isInErrorState() && failedFile() && !hasImages()) {\r\n <div class=\"tw-mt-2 tw-flex tw-flex-col tw-items-center tw-gap-2\">\r\n <span class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400\">Upload failed. Please retry.</span>\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-gap-1 tw-px-3 tw-py-1.5 tw-text-xs tw-font-medium tw-border-none tw-rounded tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-cursor-pointer tw-transition-all tw-duration-200 disabled:tw-opacity-50 disabled:tw-cursor-not-allowed\" \r\n (click)=\"retryUpload()\"\r\n [disabled]=\"isUploading() || disabledSignal()\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n Retry Upload\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Standard Mode -->\r\n @if (!isPreviewBoxMode()) {\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n [attr.id]=\"'cide-file-input-' + id()\"\r\n [attr.accept]=\"acceptSignal()\"\r\n [attr.multiple]=\"multipleSignal() ? true : null\"\r\n [disabled]=\"disabledSignal()\"\r\n (change)=\"onFileSelected($event)\"\r\n class=\"tw-hidden\"\r\n />\r\n \r\n <!-- Modern Drag and Drop Zone -->\r\n <div \r\n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-min-h-[60px] hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\r\n [class]=\"getDragDropZoneClasses()\"\r\n (click)=\"triggerFileSelect()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\">\r\n \r\n <div class=\"tw-flex tw-items-center tw-justify-between tw-p-3 tw-gap-3\">\r\n <!-- Icon and Text -->\r\n <div class=\"tw-flex tw-items-center tw-gap-2.5 tw-flex-1 tw-min-w-0\">\r\n <cide-ele-icon class=\"tw-flex-shrink-0 tw-transition-colors tw-duration-200\" \r\n [class]=\"getIconClasses()\" \r\n size=\"sm\">\r\n {{ isDragOver() ? 'file_download' : (hasFiles() ? 'check_circle' : 'cloud_upload') }}\r\n </cide-ele-icon>\r\n \r\n <div class=\"tw-flex tw-flex-col tw-gap-0.5 tw-min-w-0\">\r\n @if (isDragOver()) {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-blue-700 dark:tw-text-blue-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">Drop files here</span>\r\n } @else if (hasFiles()) {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-emerald-700 dark:tw-text-emerald-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\r\n @if (multipleSignal() && fileNames().length > 1) {\r\n {{ fileNames().length }} files selected\r\n } @else {\r\n {{ fileNames()[0] }}\r\n }\r\n </span>\r\n @if (totalFileSize() > 0) {\r\n <span class=\"tw-text-xs tw-text-emerald-600 dark:tw-text-emerald-400\">{{ fileSizeInMB() }} MB</span>\r\n }\r\n } @else {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\r\n {{ multipleSignal() ? 'Choose files or drag here' : 'Choose file or drag here' }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n \r\n <!-- Action Buttons -->\r\n <div class=\"tw-flex tw-gap-1 tw-flex-shrink-0\">\r\n @if (isInErrorState() && failedFile()) {\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-px-2 tw-py-1 tw-text-xs tw-font-medium tw-border-none tw-rounded tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-cursor-pointer tw-transition-all tw-duration-200 disabled:tw-opacity-50 disabled:tw-cursor-not-allowed\" \r\n (click)=\"retryUpload(); $event.stopPropagation()\"\r\n [disabled]=\"isUploading() || disabledSignal()\"\r\n title=\"Retry upload\">\r\n <cide-ele-icon size=\"xs\" class=\"tw-mr-1\">refresh</cide-ele-icon>\r\n Retry\r\n </button>\r\n }\r\n @if (hasFiles()) {\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-w-6 tw-h-6 tw-border-none tw-rounded tw-bg-transparent tw-cursor-pointer tw-transition-all tw-duration-200 tw-text-red-600 hover:tw-bg-red-50 dark:hover:tw-bg-red-900/20 hover:tw-text-red-700\" \r\n (click)=\"clearFiles(); $event.stopPropagation()\"\r\n title=\"Clear files\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Image Preview Section (only for standard mode) -->\r\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\r\n <div class=\"tw-mt-3\">\r\n <div class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-2\">Preview:</div>\r\n <div class=\"tw-flex tw-flex-wrap tw-gap-3\">\r\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\r\n <div \r\n class=\"tw-relative tw-border tw-border-gray-200 dark:tw-border-gray-600 tw-rounded-lg tw-overflow-hidden tw-bg-white dark:tw-bg-gray-800\"\r\n [style.width]=\"previewWidthSignal()\"\r\n [style.height]=\"previewHeightSignal()\">\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-top-1 tw-right-1 tw-w-5 tw-h-5 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-xs tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer tw-z-10\"\r\n (click)=\"removePreview(i)\"\r\n title=\"Remove image\">\r\n \u00D7\r\n </button>\r\n <img \r\n [src]=\"previewUrl\" \r\n [alt]=\"fileNames()[i] || 'Preview image'\"\r\n class=\"tw-w-full tw-h-full tw-object-cover\"\r\n loading=\"lazy\">\r\n <div class=\"tw-absolute tw-bottom-0 tw-left-0 tw-right-0 tw-bg-black tw-bg-opacity-75 tw-text-white tw-text-xs tw-p-1 tw-truncate\">{{ fileNames()[i] }}</div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Upload Status and Show Files Button (only for multiple file inputs) -->\r\n @if (multiple && showFloatingUploaderSignal() && (getUploadCount() > 0 || hasActiveUploads() || hasEverUploaded())) {\r\n <div class=\"tw-flex tw-items-center tw-justify-between tw-py-1.5 tw-gap-2\">\r\n <div class=\"tw-flex tw-items-center tw-gap-2\">\r\n <cide-ele-icon class=\"tw-text-blue-600 dark:tw-text-blue-400\" size=\"sm\">cloud_upload</cide-ele-icon>\r\n <span class=\"tw-text-sm tw-text-gray-700 dark:tw-text-gray-300\">\r\n @if (hasActiveUploads()) {\r\n {{ getActiveUploadCount() }} uploading\r\n } @else if (getUploadCount() > 0) {\r\n {{ getUploadCount() }} completed\r\n } @else if (hasEverUploaded()) {\r\n View uploads\r\n }\r\n </span>\r\n </div>\r\n <button \r\n type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-w-8 tw-h-8 tw-rounded-md tw-bg-gray-100 dark:tw-bg-gray-700 hover:tw-bg-gray-200 dark:hover:tw-bg-gray-600 tw-text-gray-600 dark:tw-text-gray-300 tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\r\n (click)=\"showFloatingUploaderDialog()\"\r\n title=\"View upload progress and history\">\r\n <cide-ele-icon size=\"sm\">visibility</cide-ele-icon>\r\n </button>\r\n </div>\r\n }\r\n \r\n @if (errorTextSignal()) {\r\n <div class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400 tw-mt-1\">{{ errorTextSignal() }}</div>\r\n }\r\n @if (isInErrorState() && failedFile() && !errorTextSignal()) {\r\n <div class=\"tw-flex tw-items-center tw-gap-2 tw-mt-1\">\r\n <span class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400\">Upload failed. Please retry.</span>\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-gap-1 tw-px-2 tw-py-1 tw-text-xs tw-font-medium tw-border-none tw-rounded tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-cursor-pointer tw-transition-all tw-duration-200 disabled:tw-opacity-50 disabled:tw-cursor-not-allowed\" \r\n (click)=\"retryUpload()\"\r\n [disabled]=\"isUploading() || disabledSignal()\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n Retry Upload\r\n </button>\r\n </div>\r\n }\r\n @if (helperTextSignal() && !errorTextSignal() && !isInErrorState()) {\r\n <div class=\"tw-text-sm tw-text-gray-500 dark:tw-text-gray-400 tw-mt-1\">{{ helperTextSignal() }}</div>\r\n }\r\n</div> ", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
5297
5321
  }
5298
5322
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
5299
5323
  type: Component,
@@ -5308,7 +5332,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
5308
5332
  useExisting: CideEleFileInputComponent,
5309
5333
  multi: true
5310
5334
  }
5311
- ], template: "<div class=\"tw-flex tw-flex-col tw-gap-2\">\r\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\r\n @if (labelSignal() && !isPreviewBoxMode()) {\r\n <label class=\"tw-block tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-1.5 tw-leading-5\" [attr.for]=\"'cide-file-input-' + id()\">\r\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"tw-text-red-500 dark:tw-text-red-400\"> *</span>}\r\n </label>\r\n }\r\n \r\n <!-- Preview Box Mode -->\r\n @if (isPreviewBoxMode()) {\r\n <div class=\"tw-relative\">\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n [attr.id]=\"'cide-file-input-' + id()\"\r\n [attr.accept]=\"acceptSignal()\"\r\n [attr.multiple]=\"multipleSignal() ? true : null\"\r\n [disabled]=\"disabledSignal()\"\r\n (change)=\"onFileSelected($event)\"\r\n class=\"tw-hidden\"\r\n />\r\n \r\n <!-- Preview Box -->\r\n <div \r\n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-relative tw-overflow-hidden hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\r\n [class]=\"getPreviewBoxClasses()\"\r\n [style.width]=\"previewWidthSignal()\"\r\n [style.height]=\"previewHeightSignal()\"\r\n (click)=\"triggerFileSelect()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\r\n \r\n <!-- No Image State -->\r\n @if (!hasImages()) {\r\n <div class=\"tw-flex tw-flex-col tw-items-center tw-justify-center tw-h-full tw-p-4\">\r\n <div class=\"tw-mb-2\">\r\n <cide-ele-icon class=\"tw-text-gray-400 dark:tw-text-gray-500\" size=\"lg\">{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\r\n </div>\r\n <div class=\"tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center\">\r\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Image Preview State -->\r\n @if (hasImages()) {\r\n <div class=\"tw-relative tw-w-full tw-h-full\">\r\n <img \r\n [src]=\"previewUrls()[0]\" \r\n [alt]=\"fileNames()[0] || 'Preview image'\"\r\n class=\"tw-w-full tw-h-full tw-object-cover tw-rounded-lg\">\r\n <div class=\"tw-absolute tw-inset-0 tw-bg-black tw-bg-opacity-0 hover:tw-bg-opacity-30 tw-transition-all tw-duration-200 tw-flex tw-items-center tw-justify-center tw-rounded-lg\">\r\n <div class=\"tw-text-white tw-text-sm tw-opacity-0 hover:tw-opacity-100 tw-transition-opacity tw-duration-200\">Click to change</div>\r\n </div>\r\n @if (!disabledSignal()) {\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-top-2 tw-right-2 tw-w-6 tw-h-6 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-sm tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\r\n (click)=\"clearFiles(); $event.stopPropagation()\"\r\n title=\"Remove image\">\r\n \u00D7\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n \r\n <!-- File name display for preview box mode -->\r\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\r\n <div class=\"tw-mt-2 tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center tw-truncate\">\r\n {{ fileNames()[0] }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Standard Mode -->\r\n @if (!isPreviewBoxMode()) {\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n [attr.id]=\"'cide-file-input-' + id()\"\r\n [attr.accept]=\"acceptSignal()\"\r\n [attr.multiple]=\"multipleSignal() ? true : null\"\r\n [disabled]=\"disabledSignal()\"\r\n (change)=\"onFileSelected($event)\"\r\n class=\"tw-hidden\"\r\n />\r\n \r\n <!-- Modern Drag and Drop Zone -->\r\n <div \r\n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-min-h-[60px] hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\r\n [class]=\"getDragDropZoneClasses()\"\r\n (click)=\"triggerFileSelect()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\">\r\n \r\n <div class=\"tw-flex tw-items-center tw-justify-between tw-p-3 tw-gap-3\">\r\n <!-- Icon and Text -->\r\n <div class=\"tw-flex tw-items-center tw-gap-2.5 tw-flex-1 tw-min-w-0\">\r\n <cide-ele-icon class=\"tw-flex-shrink-0 tw-transition-colors tw-duration-200\" \r\n [class]=\"getIconClasses()\" \r\n size=\"sm\">\r\n {{ isDragOver() ? 'file_download' : (hasFiles() ? 'check_circle' : 'cloud_upload') }}\r\n </cide-ele-icon>\r\n \r\n <div class=\"tw-flex tw-flex-col tw-gap-0.5 tw-min-w-0\">\r\n @if (isDragOver()) {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-blue-700 dark:tw-text-blue-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">Drop files here</span>\r\n } @else if (hasFiles()) {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-emerald-700 dark:tw-text-emerald-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\r\n @if (multipleSignal() && fileNames().length > 1) {\r\n {{ fileNames().length }} files selected\r\n } @else {\r\n {{ fileNames()[0] }}\r\n }\r\n </span>\r\n @if (totalFileSize() > 0) {\r\n <span class=\"tw-text-xs tw-text-emerald-600 dark:tw-text-emerald-400\">{{ fileSizeInMB() }} MB</span>\r\n }\r\n } @else {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\r\n {{ multipleSignal() ? 'Choose files or drag here' : 'Choose file or drag here' }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n \r\n <!-- Action Buttons -->\r\n <div class=\"tw-flex tw-gap-1 tw-flex-shrink-0\">\r\n @if (hasFiles()) {\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-w-6 tw-h-6 tw-border-none tw-rounded tw-bg-transparent tw-cursor-pointer tw-transition-all tw-duration-200 tw-text-red-600 hover:tw-bg-red-50 dark:hover:tw-bg-red-900/20 hover:tw-text-red-700\" \r\n (click)=\"clearFiles(); $event.stopPropagation()\"\r\n title=\"Clear files\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Image Preview Section (only for standard mode) -->\r\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\r\n <div class=\"tw-mt-3\">\r\n <div class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-2\">Preview:</div>\r\n <div class=\"tw-flex tw-flex-wrap tw-gap-3\">\r\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\r\n <div \r\n class=\"tw-relative tw-border tw-border-gray-200 dark:tw-border-gray-600 tw-rounded-lg tw-overflow-hidden tw-bg-white dark:tw-bg-gray-800\"\r\n [style.width]=\"previewWidthSignal()\"\r\n [style.height]=\"previewHeightSignal()\">\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-top-1 tw-right-1 tw-w-5 tw-h-5 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-xs tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer tw-z-10\"\r\n (click)=\"removePreview(i)\"\r\n title=\"Remove image\">\r\n \u00D7\r\n </button>\r\n <img \r\n [src]=\"previewUrl\" \r\n [alt]=\"fileNames()[i] || 'Preview image'\"\r\n class=\"tw-w-full tw-h-full tw-object-cover\"\r\n loading=\"lazy\">\r\n <div class=\"tw-absolute tw-bottom-0 tw-left-0 tw-right-0 tw-bg-black tw-bg-opacity-75 tw-text-white tw-text-xs tw-p-1 tw-truncate\">{{ fileNames()[i] }}</div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Upload Status and Show Files Button (only for multiple file inputs) -->\r\n @if (multiple && showFloatingUploaderSignal() && (getUploadCount() > 0 || hasActiveUploads() || hasEverUploaded())) {\r\n <div class=\"tw-flex tw-items-center tw-justify-between tw-py-1.5 tw-gap-2\">\r\n <div class=\"tw-flex tw-items-center tw-gap-2\">\r\n <cide-ele-icon class=\"tw-text-blue-600 dark:tw-text-blue-400\" size=\"sm\">cloud_upload</cide-ele-icon>\r\n <span class=\"tw-text-sm tw-text-gray-700 dark:tw-text-gray-300\">\r\n @if (hasActiveUploads()) {\r\n {{ getActiveUploadCount() }} uploading\r\n } @else if (getUploadCount() > 0) {\r\n {{ getUploadCount() }} completed\r\n } @else if (hasEverUploaded()) {\r\n View uploads\r\n }\r\n </span>\r\n </div>\r\n <button \r\n type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-w-8 tw-h-8 tw-rounded-md tw-bg-gray-100 dark:tw-bg-gray-700 hover:tw-bg-gray-200 dark:hover:tw-bg-gray-600 tw-text-gray-600 dark:tw-text-gray-300 tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\r\n (click)=\"showFloatingUploaderDialog()\"\r\n title=\"View upload progress and history\">\r\n <cide-ele-icon size=\"sm\">visibility</cide-ele-icon>\r\n </button>\r\n </div>\r\n }\r\n \r\n @if (errorTextSignal()) {\r\n <div class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400 tw-mt-1\">{{ errorTextSignal() }}</div>\r\n }\r\n @if (helperTextSignal() && !errorTextSignal()) {\r\n <div class=\"tw-text-sm tw-text-gray-500 dark:tw-text-gray-400 tw-mt-1\">{{ helperTextSignal() }}</div>\r\n }\r\n</div> " }]
5335
+ ], template: "<div class=\"tw-flex tw-flex-col tw-gap-2\">\r\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\r\n @if (labelSignal() && !isPreviewBoxMode()) {\r\n <label class=\"tw-block tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-1.5 tw-leading-5\" [attr.for]=\"'cide-file-input-' + id()\">\r\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"tw-text-red-500 dark:tw-text-red-400\"> *</span>}\r\n </label>\r\n }\r\n \r\n <!-- Preview Box Mode -->\r\n @if (isPreviewBoxMode()) {\r\n <div class=\"tw-relative\">\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n [attr.id]=\"'cide-file-input-' + id()\"\r\n [attr.accept]=\"acceptSignal()\"\r\n [attr.multiple]=\"multipleSignal() ? true : null\"\r\n [disabled]=\"disabledSignal()\"\r\n (change)=\"onFileSelected($event)\"\r\n class=\"tw-hidden\"\r\n />\r\n \r\n <!-- Preview Box -->\r\n <div \r\n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-relative tw-overflow-hidden hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\r\n [class]=\"getPreviewBoxClasses()\"\r\n [style.width]=\"previewWidthSignal()\"\r\n [style.height]=\"previewHeightSignal()\"\r\n (click)=\"triggerFileSelect()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\r\n \r\n <!-- No Image State -->\r\n @if (!hasImages()) {\r\n <div class=\"tw-flex tw-flex-col tw-items-center tw-justify-center tw-h-full tw-p-4\">\r\n <div class=\"tw-mb-2\">\r\n <cide-ele-icon class=\"tw-text-gray-400 dark:tw-text-gray-500\" size=\"lg\">{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\r\n </div>\r\n <div class=\"tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center\">\r\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Image Preview State -->\r\n @if (hasImages()) {\r\n <div class=\"tw-relative tw-w-full tw-h-full\">\r\n <img \r\n [src]=\"previewUrls()[0]\" \r\n [alt]=\"fileNames()[0] || 'Preview image'\"\r\n class=\"tw-w-full tw-h-full tw-object-cover tw-rounded-lg\">\r\n <div class=\"tw-absolute tw-inset-0 tw-bg-black tw-bg-opacity-0 hover:tw-bg-opacity-30 tw-transition-all tw-duration-200 tw-flex tw-items-center tw-justify-center tw-rounded-lg\">\r\n <div class=\"tw-text-white tw-text-sm tw-opacity-0 hover:tw-opacity-100 tw-transition-opacity tw-duration-200\">Click to change</div>\r\n </div>\r\n @if (!disabledSignal()) {\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-top-2 tw-right-2 tw-w-6 tw-h-6 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-sm tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\r\n (click)=\"clearFiles(); $event.stopPropagation()\"\r\n title=\"Remove image\">\r\n \u00D7\r\n </button>\r\n }\r\n @if (isInErrorState() && failedFile()) {\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-bottom-2 tw-left-1/2 tw-transform tw--translate-x-1/2 tw-px-3 tw-py-1.5 tw-text-xs tw-font-medium tw-border-none tw-rounded tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-cursor-pointer tw-transition-all tw-duration-200 disabled:tw-opacity-50 disabled:tw-cursor-not-allowed tw-shadow-lg tw-z-10\"\r\n (click)=\"retryUpload(); $event.stopPropagation()\"\r\n [disabled]=\"isUploading() || disabledSignal()\"\r\n title=\"Retry upload\">\r\n <cide-ele-icon size=\"xs\" class=\"tw-mr-1\">refresh</cide-ele-icon>\r\n Retry Upload\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n \r\n <!-- File name display for preview box mode -->\r\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\r\n <div class=\"tw-mt-2 tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center tw-truncate\">\r\n {{ fileNames()[0] }}\r\n </div>\r\n }\r\n <!-- Error message and retry button for preview box mode -->\r\n @if (isInErrorState() && failedFile() && !hasImages()) {\r\n <div class=\"tw-mt-2 tw-flex tw-flex-col tw-items-center tw-gap-2\">\r\n <span class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400\">Upload failed. Please retry.</span>\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-gap-1 tw-px-3 tw-py-1.5 tw-text-xs tw-font-medium tw-border-none tw-rounded tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-cursor-pointer tw-transition-all tw-duration-200 disabled:tw-opacity-50 disabled:tw-cursor-not-allowed\" \r\n (click)=\"retryUpload()\"\r\n [disabled]=\"isUploading() || disabledSignal()\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n Retry Upload\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Standard Mode -->\r\n @if (!isPreviewBoxMode()) {\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n [attr.id]=\"'cide-file-input-' + id()\"\r\n [attr.accept]=\"acceptSignal()\"\r\n [attr.multiple]=\"multipleSignal() ? true : null\"\r\n [disabled]=\"disabledSignal()\"\r\n (change)=\"onFileSelected($event)\"\r\n class=\"tw-hidden\"\r\n />\r\n \r\n <!-- Modern Drag and Drop Zone -->\r\n <div \r\n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-min-h-[60px] hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\r\n [class]=\"getDragDropZoneClasses()\"\r\n (click)=\"triggerFileSelect()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\">\r\n \r\n <div class=\"tw-flex tw-items-center tw-justify-between tw-p-3 tw-gap-3\">\r\n <!-- Icon and Text -->\r\n <div class=\"tw-flex tw-items-center tw-gap-2.5 tw-flex-1 tw-min-w-0\">\r\n <cide-ele-icon class=\"tw-flex-shrink-0 tw-transition-colors tw-duration-200\" \r\n [class]=\"getIconClasses()\" \r\n size=\"sm\">\r\n {{ isDragOver() ? 'file_download' : (hasFiles() ? 'check_circle' : 'cloud_upload') }}\r\n </cide-ele-icon>\r\n \r\n <div class=\"tw-flex tw-flex-col tw-gap-0.5 tw-min-w-0\">\r\n @if (isDragOver()) {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-blue-700 dark:tw-text-blue-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">Drop files here</span>\r\n } @else if (hasFiles()) {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-emerald-700 dark:tw-text-emerald-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\r\n @if (multipleSignal() && fileNames().length > 1) {\r\n {{ fileNames().length }} files selected\r\n } @else {\r\n {{ fileNames()[0] }}\r\n }\r\n </span>\r\n @if (totalFileSize() > 0) {\r\n <span class=\"tw-text-xs tw-text-emerald-600 dark:tw-text-emerald-400\">{{ fileSizeInMB() }} MB</span>\r\n }\r\n } @else {\r\n <span class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\r\n {{ multipleSignal() ? 'Choose files or drag here' : 'Choose file or drag here' }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n \r\n <!-- Action Buttons -->\r\n <div class=\"tw-flex tw-gap-1 tw-flex-shrink-0\">\r\n @if (isInErrorState() && failedFile()) {\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-px-2 tw-py-1 tw-text-xs tw-font-medium tw-border-none tw-rounded tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-cursor-pointer tw-transition-all tw-duration-200 disabled:tw-opacity-50 disabled:tw-cursor-not-allowed\" \r\n (click)=\"retryUpload(); $event.stopPropagation()\"\r\n [disabled]=\"isUploading() || disabledSignal()\"\r\n title=\"Retry upload\">\r\n <cide-ele-icon size=\"xs\" class=\"tw-mr-1\">refresh</cide-ele-icon>\r\n Retry\r\n </button>\r\n }\r\n @if (hasFiles()) {\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-w-6 tw-h-6 tw-border-none tw-rounded tw-bg-transparent tw-cursor-pointer tw-transition-all tw-duration-200 tw-text-red-600 hover:tw-bg-red-50 dark:hover:tw-bg-red-900/20 hover:tw-text-red-700\" \r\n (click)=\"clearFiles(); $event.stopPropagation()\"\r\n title=\"Clear files\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Image Preview Section (only for standard mode) -->\r\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\r\n <div class=\"tw-mt-3\">\r\n <div class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-2\">Preview:</div>\r\n <div class=\"tw-flex tw-flex-wrap tw-gap-3\">\r\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\r\n <div \r\n class=\"tw-relative tw-border tw-border-gray-200 dark:tw-border-gray-600 tw-rounded-lg tw-overflow-hidden tw-bg-white dark:tw-bg-gray-800\"\r\n [style.width]=\"previewWidthSignal()\"\r\n [style.height]=\"previewHeightSignal()\">\r\n <button \r\n type=\"button\" \r\n class=\"tw-absolute tw-top-1 tw-right-1 tw-w-5 tw-h-5 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-xs tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer tw-z-10\"\r\n (click)=\"removePreview(i)\"\r\n title=\"Remove image\">\r\n \u00D7\r\n </button>\r\n <img \r\n [src]=\"previewUrl\" \r\n [alt]=\"fileNames()[i] || 'Preview image'\"\r\n class=\"tw-w-full tw-h-full tw-object-cover\"\r\n loading=\"lazy\">\r\n <div class=\"tw-absolute tw-bottom-0 tw-left-0 tw-right-0 tw-bg-black tw-bg-opacity-75 tw-text-white tw-text-xs tw-p-1 tw-truncate\">{{ fileNames()[i] }}</div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Upload Status and Show Files Button (only for multiple file inputs) -->\r\n @if (multiple && showFloatingUploaderSignal() && (getUploadCount() > 0 || hasActiveUploads() || hasEverUploaded())) {\r\n <div class=\"tw-flex tw-items-center tw-justify-between tw-py-1.5 tw-gap-2\">\r\n <div class=\"tw-flex tw-items-center tw-gap-2\">\r\n <cide-ele-icon class=\"tw-text-blue-600 dark:tw-text-blue-400\" size=\"sm\">cloud_upload</cide-ele-icon>\r\n <span class=\"tw-text-sm tw-text-gray-700 dark:tw-text-gray-300\">\r\n @if (hasActiveUploads()) {\r\n {{ getActiveUploadCount() }} uploading\r\n } @else if (getUploadCount() > 0) {\r\n {{ getUploadCount() }} completed\r\n } @else if (hasEverUploaded()) {\r\n View uploads\r\n }\r\n </span>\r\n </div>\r\n <button \r\n type=\"button\" \r\n class=\"tw-flex tw-items-center tw-justify-center tw-w-8 tw-h-8 tw-rounded-md tw-bg-gray-100 dark:tw-bg-gray-700 hover:tw-bg-gray-200 dark:hover:tw-bg-gray-600 tw-text-gray-600 dark:tw-text-gray-300 tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\r\n (click)=\"showFloatingUploaderDialog()\"\r\n title=\"View upload progress and history\">\r\n <cide-ele-icon size=\"sm\">visibility</cide-ele-icon>\r\n </button>\r\n </div>\r\n }\r\n \r\n @if (errorTextSignal()) {\r\n <div class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400 tw-mt-1\">{{ errorTextSignal() }}</div>\r\n }\r\n @if (isInErrorState() && failedFile() && !errorTextSignal()) {\r\n <div class=\"tw-flex tw-items-center tw-gap-2 tw-mt-1\">\r\n <span class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400\">Upload failed. Please retry.</span>\r\n <button type=\"button\" \r\n class=\"tw-flex tw-items-center tw-gap-1 tw-px-2 tw-py-1 tw-text-xs tw-font-medium tw-border-none tw-rounded tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-cursor-pointer tw-transition-all tw-duration-200 disabled:tw-opacity-50 disabled:tw-cursor-not-allowed\" \r\n (click)=\"retryUpload()\"\r\n [disabled]=\"isUploading() || disabledSignal()\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n Retry Upload\r\n </button>\r\n </div>\r\n }\r\n @if (helperTextSignal() && !errorTextSignal() && !isInErrorState()) {\r\n <div class=\"tw-text-sm tw-text-gray-500 dark:tw-text-gray-400 tw-mt-1\">{{ helperTextSignal() }}</div>\r\n }\r\n</div> " }]
5312
5336
  }], ctorParameters: () => [], propDecorators: { label: [{
5313
5337
  type: Input
5314
5338
  }], accept: [{
@@ -7380,1230 +7404,1287 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
7380
7404
  }] } });
7381
7405
 
7382
7406
  /**
7383
- * Notification API Service
7384
- * HTTP client for notification API endpoints
7407
+ * CIDE Element Library - Theme Service
7408
+ * Independent theme management for the element library
7409
+ * Can be used standalone without parent application theme dependencies
7385
7410
  */
7386
- class NotificationApiService {
7387
- http = inject(HttpClient);
7388
- apiUrl = cidePath.join([
7389
- hostManagerRoutesUrl.cideSuiteHost,
7390
- notificationRoutesUrl.module
7391
- ]);
7411
+ class CideThemeService {
7412
+ platformId = inject(PLATFORM_ID);
7413
+ isBrowser;
7414
+ currentTheme$ = new BehaviorSubject('light');
7415
+ effectiveTheme$ = new BehaviorSubject('light');
7416
+ config = {
7417
+ theme: 'light',
7418
+ targetElement: null,
7419
+ persistPreference: true,
7420
+ storageKey: 'cide-theme',
7421
+ useDataAttribute: true,
7422
+ useClassName: true
7423
+ };
7424
+ constructor() {
7425
+ this.isBrowser = isPlatformBrowser(this.platformId);
7426
+ if (this.isBrowser) {
7427
+ this.config.targetElement = document.documentElement;
7428
+ }
7429
+ }
7392
7430
  /**
7393
- * Get user notifications
7431
+ * Initialize the theme service with configuration
7432
+ * This should be called once in the application, typically in main.ts or app config
7394
7433
  */
7395
- getNotifications(params) {
7396
- let httpParams = new HttpParams();
7397
- if (params) {
7398
- Object.keys(params).forEach(key => {
7399
- const value = params[key];
7400
- if (value !== undefined && value !== null) {
7401
- httpParams = httpParams.set(key, value.toString());
7402
- }
7403
- });
7434
+ initialize(config) {
7435
+ if (!this.isBrowser) {
7436
+ return;
7437
+ }
7438
+ // Merge configuration
7439
+ if (config) {
7440
+ this.config = {
7441
+ ...this.config,
7442
+ ...config,
7443
+ targetElement: config.targetElement || document.documentElement
7444
+ };
7445
+ }
7446
+ // Load saved theme preference
7447
+ const savedTheme = this.loadThemePreference();
7448
+ const initialTheme = savedTheme || this.config.theme;
7449
+ // Apply initial theme
7450
+ this.setTheme(initialTheme);
7451
+ // Listen for system theme changes if theme is set to 'auto'
7452
+ if (initialTheme === 'auto') {
7453
+ this.listenToSystemTheme();
7404
7454
  }
7405
- // Debug: Log the URL being used
7406
- console.log('[NotificationApi] Request URL:', this.apiUrl);
7407
- console.log('[NotificationApi] Full URL with params:', `${this.apiUrl}?${httpParams.toString()}`);
7408
- return this.http.get(this.apiUrl, { params: httpParams });
7409
7455
  }
7410
7456
  /**
7411
- * Get notification by ID
7457
+ * Set the current theme
7412
7458
  */
7413
- getNotificationById(id) {
7414
- return this.http.get(`${this.apiUrl}/${id}`);
7459
+ setTheme(theme) {
7460
+ if (!this.isBrowser) {
7461
+ return;
7462
+ }
7463
+ this.currentTheme$.next(theme);
7464
+ // Save preference if persistence is enabled
7465
+ if (this.config.persistPreference) {
7466
+ this.saveThemePreference(theme);
7467
+ }
7468
+ // Determine effective theme
7469
+ let effectiveTheme;
7470
+ if (theme === 'auto') {
7471
+ effectiveTheme = this.getSystemTheme();
7472
+ this.listenToSystemTheme();
7473
+ }
7474
+ else {
7475
+ effectiveTheme = theme;
7476
+ }
7477
+ this.effectiveTheme$.next(effectiveTheme);
7478
+ this.applyTheme(effectiveTheme);
7415
7479
  }
7416
7480
  /**
7417
- * Get unread count
7481
+ * Get the current theme preference
7418
7482
  */
7419
- getUnreadCount() {
7420
- return this.http.get(`${this.apiUrl}/${notificationRoutesUrl.unreadCount}`);
7483
+ getCurrentTheme() {
7484
+ return this.currentTheme$.value;
7421
7485
  }
7422
7486
  /**
7423
- * Create notification
7487
+ * Get the effective theme (actual theme being displayed)
7424
7488
  */
7425
- createNotification(payload) {
7426
- return this.http.post(this.apiUrl, payload);
7489
+ getEffectiveTheme() {
7490
+ return this.effectiveTheme$.value;
7427
7491
  }
7428
7492
  /**
7429
- * Mark notification as read
7493
+ * Observable for current theme changes
7430
7494
  */
7431
- markAsRead(id) {
7432
- const url = `${this.apiUrl}/${id}/read`;
7433
- console.log('[NotificationApi] Mark as read URL:', url);
7434
- return this.http.put(url, {});
7495
+ getCurrentTheme$() {
7496
+ return this.currentTheme$.asObservable();
7435
7497
  }
7436
7498
  /**
7437
- * Mark all notifications as read
7499
+ * Observable for effective theme changes
7438
7500
  */
7439
- markAllAsRead() {
7440
- const url = `${this.apiUrl}/read-all`;
7441
- console.log('[NotificationApi] Mark all as read URL:', url);
7442
- return this.http.put(url, {});
7501
+ getEffectiveTheme$() {
7502
+ return this.effectiveTheme$.asObservable();
7443
7503
  }
7444
7504
  /**
7445
- * Archive notification
7505
+ * Toggle between light and dark themes
7446
7506
  */
7447
- archiveNotification(id) {
7448
- return this.http.put(`${this.apiUrl}/${id}/archive`, {});
7507
+ toggleTheme() {
7508
+ const currentEffective = this.effectiveTheme$.value;
7509
+ const newTheme = currentEffective === 'light' ? 'dark' : 'light';
7510
+ this.setTheme(newTheme);
7449
7511
  }
7450
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: NotificationApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7451
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: NotificationApiService, providedIn: 'root' });
7452
- }
7453
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: NotificationApiService, decorators: [{
7454
- type: Injectable,
7455
- args: [{
7456
- providedIn: 'root'
7457
- }]
7458
- }] });
7459
-
7460
- /**
7461
- * WebSocket Notification Service
7462
- * Socket.IO client for real-time notifications
7463
- */
7464
- class WebSocketNotificationService {
7465
- socket = null;
7466
- notificationSubject = new Subject();
7467
- connectionStatusSubject = new BehaviorSubject({
7468
- connected: false
7469
- });
7470
- // Signals for reactive state
7471
- notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : []));
7472
- unreadNotifications = signal([], ...(ngDevMode ? [{ debugName: "unreadNotifications" }] : []));
7473
- // Public observables
7474
- notification$ = this.notificationSubject.asObservable();
7475
- connectionStatus$ = this.connectionStatusSubject.asObservable();
7476
- // Computed signals
7477
- allNotifications = computed(() => this.notifications(), ...(ngDevMode ? [{ debugName: "allNotifications" }] : []));
7478
- unreadNotificationsCount = computed(() => this.unreadNotifications().length, ...(ngDevMode ? [{ debugName: "unreadNotificationsCount" }] : []));
7479
- isConnected = computed(() => this.connectionStatusSubject.value.connected, ...(ngDevMode ? [{ debugName: "isConnected" }] : []));
7480
7512
  /**
7481
- * Connect to Socket.IO server
7513
+ * Check if dark mode is currently active
7482
7514
  */
7483
- connect(token, userId) {
7484
- if (this.socket?.connected) {
7485
- console.log('Socket already connected');
7486
- return;
7487
- }
7488
- // Resolve actual API URL from placeholder
7489
- // The placeholder is replaced by host interceptor for HTTP, but WebSocket needs direct resolution
7490
- // Based on host-manager.json: "__cloud_ide_suite_layout__" -> "http://localhost:3001"
7491
- const placeholder = hostManagerRoutesUrl.cideSuiteHost;
7492
- let apiUrl = 'http://localhost:3001'; // Default from host-manager.json
7493
- // Try to resolve from host-manager.json via fetch (same as HTTP interceptor)
7494
- if (typeof window !== 'undefined') {
7495
- // Try to fetch host-manager.json to get the actual URL
7496
- fetch('/routes/host-manager.json')
7497
- .then(res => res.json())
7498
- .then((hostManagerRoutes) => {
7499
- const hostManagerRoutesEntry = hostManagerRoutes?.hosts?.find((hostRoute) => placeholder?.includes(hostRoute?.url));
7500
- if (hostManagerRoutesEntry?.replace) {
7501
- apiUrl = hostManagerRoutesEntry.replace;
7502
- }
7503
- // Remove /api suffix if present (Socket.IO connects to base server)
7504
- apiUrl = apiUrl.replace(/\/api\/?$/, '');
7505
- this.connectWithUrl(apiUrl, token, userId);
7506
- })
7507
- .catch(() => {
7508
- // Fallback if fetch fails - use default or try localStorage
7509
- this.resolveUrlFallback(token, userId);
7510
- });
7511
- }
7512
- else {
7513
- // Server-side or fallback
7514
- this.resolveUrlFallback(token, userId);
7515
- }
7515
+ isDarkMode() {
7516
+ return this.effectiveTheme$.value === 'dark';
7516
7517
  }
7517
7518
  /**
7518
- * Fallback URL resolution
7519
+ * Apply theme to target element
7519
7520
  */
7520
- resolveUrlFallback(token, userId) {
7521
- let apiUrl = 'http://localhost:3001'; // Default from host-manager.json
7522
- // Try to get from current window location
7523
- if (typeof window !== 'undefined') {
7524
- const currentOrigin = window.location.origin;
7525
- // Check if we're on localhost (development)
7526
- if (currentOrigin.includes('localhost') || currentOrigin.includes('127.0.0.1')) {
7527
- // Use localhost:3001 for development (from host-manager.json)
7528
- apiUrl = 'http://localhost:3001';
7521
+ applyTheme(theme) {
7522
+ if (!this.isBrowser || !this.config.targetElement) {
7523
+ return;
7524
+ }
7525
+ const element = this.config.targetElement;
7526
+ // Apply data-theme attribute
7527
+ if (this.config.useDataAttribute) {
7528
+ element.setAttribute('data-theme', theme);
7529
+ }
7530
+ // Apply class name
7531
+ if (this.config.useClassName) {
7532
+ if (theme === 'dark') {
7533
+ element.classList.add('dark-mode');
7534
+ element.classList.remove('light-mode');
7529
7535
  }
7530
7536
  else {
7531
- // In production, use the same origin (assuming API is on same domain)
7532
- apiUrl = currentOrigin || 'http://localhost:3001';
7537
+ element.classList.add('light-mode');
7538
+ element.classList.remove('dark-mode');
7533
7539
  }
7534
- // Try to get from localStorage (if set by app initialization)
7535
- const storedApiUrl = localStorage.getItem('api_base_url');
7536
- if (storedApiUrl) {
7537
- apiUrl = storedApiUrl.replace(/\/api\/?$/, '');
7540
+ }
7541
+ // Also apply to body for better compatibility
7542
+ if (element === document.documentElement && document.body) {
7543
+ if (this.config.useDataAttribute) {
7544
+ document.body.setAttribute('data-theme', theme);
7545
+ }
7546
+ if (this.config.useClassName) {
7547
+ if (theme === 'dark') {
7548
+ document.body.classList.add('dark-mode');
7549
+ document.body.classList.remove('light-mode');
7550
+ }
7551
+ else {
7552
+ document.body.classList.add('light-mode');
7553
+ document.body.classList.remove('dark-mode');
7554
+ }
7538
7555
  }
7539
7556
  }
7540
- this.connectWithUrl(apiUrl, token, userId);
7541
7557
  }
7542
7558
  /**
7543
- * Connect with resolved URL
7559
+ * Get system theme preference
7544
7560
  */
7545
- connectWithUrl(apiUrl, token, userId) {
7546
- console.log('[WebSocket] Connecting to:', apiUrl);
7547
- this.socket = io(apiUrl, {
7548
- path: '/notifications-socket',
7549
- transports: ['websocket', 'polling'],
7550
- auth: {
7551
- token: token
7552
- },
7553
- reconnection: true,
7554
- reconnectionDelay: 1000,
7555
- reconnectionAttempts: 5,
7556
- reconnectionDelayMax: 5000
7557
- });
7558
- this.setupEventHandlers(userId);
7561
+ getSystemTheme() {
7562
+ if (!this.isBrowser) {
7563
+ return 'light';
7564
+ }
7565
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
7566
+ return 'dark';
7567
+ }
7568
+ return 'light';
7559
7569
  }
7560
7570
  /**
7561
- * Setup Socket.IO event handlers
7571
+ * Listen to system theme changes
7562
7572
  */
7563
- setupEventHandlers(userId) {
7564
- if (!this.socket)
7573
+ listenToSystemTheme() {
7574
+ if (!this.isBrowser || !window.matchMedia) {
7565
7575
  return;
7566
- // Connection established
7567
- this.socket.on('connect', () => {
7568
- console.log('Socket.IO connected:', this.socket?.id);
7569
- this.connectionStatusSubject.next({
7570
- connected: true,
7571
- socketId: this.socket?.id,
7572
- userId: userId,
7573
- timestamp: new Date()
7574
- });
7575
- });
7576
- // Connection confirmation
7577
- this.socket.on('connected', (data) => {
7578
- console.log('Notification service connected:', data);
7579
- this.connectionStatusSubject.next({
7580
- connected: true,
7581
- socketId: data.socketId,
7582
- userId: data.userId,
7583
- timestamp: data.timestamp
7584
- });
7585
- });
7586
- // Receive notification
7587
- this.socket.on('notification', (notification) => {
7588
- console.log('Received notification:', notification);
7589
- // Add to notifications list
7590
- this.notifications.update(notifications => [notification, ...notifications]);
7591
- // Add to unread list
7592
- this.unreadNotifications.update(unread => [notification, ...unread]);
7593
- // Emit to subscribers
7594
- this.notificationSubject.next(notification);
7595
- // Acknowledge receipt
7596
- this.acknowledgeNotification(notification.id);
7597
- });
7598
- // Disconnect
7599
- this.socket.on('disconnect', (reason) => {
7600
- console.log('Socket.IO disconnected:', reason);
7601
- this.connectionStatusSubject.next({
7602
- connected: false,
7603
- timestamp: new Date()
7604
- });
7605
- });
7606
- // Connection error
7607
- this.socket.on('connect_error', (error) => {
7608
- console.error('Socket.IO connection error:', error);
7609
- this.connectionStatusSubject.next({
7610
- connected: false,
7611
- timestamp: new Date()
7612
- });
7613
- });
7614
- }
7615
- /**
7616
- * Acknowledge notification receipt
7617
- */
7618
- acknowledgeNotification(notificationId) {
7619
- if (this.socket?.connected) {
7620
- this.socket.emit('notification:ack', { notificationId });
7621
7576
  }
7622
- }
7623
- /**
7624
- * Mark notification as read
7625
- */
7626
- markAsRead(notificationId) {
7627
- if (this.socket?.connected) {
7628
- this.socket.emit('notification:read', { notificationId });
7629
- // Remove from unread list
7630
- this.unreadNotifications.update(unread => unread.filter(n => n.id !== notificationId));
7577
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
7578
+ const handler = (e) => {
7579
+ if (this.currentTheme$.value === 'auto') {
7580
+ const effectiveTheme = e.matches ? 'dark' : 'light';
7581
+ this.effectiveTheme$.next(effectiveTheme);
7582
+ this.applyTheme(effectiveTheme);
7583
+ }
7584
+ };
7585
+ // Modern browsers
7586
+ if (mediaQuery.addEventListener) {
7587
+ mediaQuery.addEventListener('change', handler);
7588
+ }
7589
+ else if (mediaQuery.addListener) {
7590
+ // Legacy browsers
7591
+ mediaQuery.addListener(handler);
7631
7592
  }
7632
7593
  }
7633
7594
  /**
7634
- * Mark all as read
7635
- */
7636
- markAllAsRead() {
7637
- const unreadIds = this.unreadNotifications().map(n => n.id);
7638
- unreadIds.forEach(id => this.markAsRead(id));
7639
- }
7640
- /**
7641
- * Disconnect from server
7595
+ * Save theme preference to localStorage
7642
7596
  */
7643
- disconnect() {
7644
- if (this.socket) {
7645
- this.socket.disconnect();
7646
- this.socket = null;
7647
- this.connectionStatusSubject.next({
7648
- connected: false,
7649
- timestamp: new Date()
7650
- });
7597
+ saveThemePreference(theme) {
7598
+ if (!this.isBrowser) {
7599
+ return;
7600
+ }
7601
+ try {
7602
+ // Save in new format: 'light', 'dark', or 'auto'
7603
+ localStorage.setItem(this.config.storageKey, theme);
7604
+ // Also update old format for backward compatibility
7605
+ // This allows old code that reads 'true'/'false' to still work
7606
+ if (theme === 'dark') {
7607
+ localStorage.setItem(this.config.storageKey, 'dark');
7608
+ }
7609
+ else if (theme === 'light') {
7610
+ localStorage.setItem(this.config.storageKey, 'light');
7611
+ }
7612
+ }
7613
+ catch (error) {
7614
+ console.warn('Failed to save theme preference to localStorage:', error);
7651
7615
  }
7652
7616
  }
7653
7617
  /**
7654
- * Clear all notifications
7618
+ * Load theme preference from localStorage
7655
7619
  */
7656
- clearNotifications() {
7657
- this.notifications.set([]);
7658
- this.unreadNotifications.set([]);
7620
+ loadThemePreference() {
7621
+ if (!this.isBrowser) {
7622
+ return null;
7623
+ }
7624
+ try {
7625
+ const saved = localStorage.getItem(this.config.storageKey);
7626
+ // Handle new format: 'light', 'dark', 'auto'
7627
+ if (saved && ['light', 'dark', 'auto'].includes(saved)) {
7628
+ return saved;
7629
+ }
7630
+ // Handle legacy format: 'true' (dark mode) or 'false' (light mode)
7631
+ if (saved === 'true') {
7632
+ // Migrate to new format
7633
+ localStorage.setItem(this.config.storageKey, 'dark');
7634
+ return 'dark';
7635
+ }
7636
+ else if (saved === 'false') {
7637
+ // Migrate to new format
7638
+ localStorage.setItem(this.config.storageKey, 'light');
7639
+ return 'light';
7640
+ }
7641
+ }
7642
+ catch (error) {
7643
+ console.warn('Failed to load theme preference from localStorage:', error);
7644
+ }
7645
+ return null;
7659
7646
  }
7660
7647
  /**
7661
- * Remove notification from list
7648
+ * Clear saved theme preference
7662
7649
  */
7663
- removeNotification(notificationId) {
7664
- this.notifications.update(notifications => notifications.filter(n => n.id !== notificationId));
7665
- this.unreadNotifications.update(unread => unread.filter(n => n.id !== notificationId));
7666
- }
7667
- ngOnDestroy() {
7668
- this.disconnect();
7669
- this.notificationSubject.complete();
7670
- this.connectionStatusSubject.complete();
7650
+ clearThemePreference() {
7651
+ if (!this.isBrowser) {
7652
+ return;
7653
+ }
7654
+ try {
7655
+ localStorage.removeItem(this.config.storageKey);
7656
+ }
7657
+ catch (error) {
7658
+ console.warn('Failed to clear theme preference from localStorage:', error);
7659
+ }
7671
7660
  }
7672
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: WebSocketNotificationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7673
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: WebSocketNotificationService, providedIn: 'root' });
7661
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7662
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideThemeService, providedIn: 'root' });
7674
7663
  }
7675
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: WebSocketNotificationService, decorators: [{
7664
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideThemeService, decorators: [{
7676
7665
  type: Injectable,
7677
7666
  args: [{
7678
7667
  providedIn: 'root'
7679
7668
  }]
7680
- }] });
7669
+ }], ctorParameters: () => [] });
7681
7670
 
7682
- class CideCoreFileManagerService {
7683
- http;
7684
- apiUrl = `${coreRoutesUrl?.fileManager}`;
7685
- fileListSubject = new BehaviorSubject([]);
7686
- fileList$ = this.fileListSubject.asObservable();
7687
- constructor(http) {
7688
- this.http = http;
7689
- console.log('CideCoreFileManagerService initialized - using real API');
7671
+ /**
7672
+ * Apple-inspired dark/light mode toggle component
7673
+ *
7674
+ * Features:
7675
+ * - Smooth animated toggle switch
7676
+ * - Apple-style design with rounded corners and shadows
7677
+ * - Works on pages without header/sidebar (dynamic website, login pages, auth project)
7678
+ * - Automatically syncs with system theme preference
7679
+ * - Persists user preference to localStorage
7680
+ */
7681
+ class CideEleThemeToggleComponent {
7682
+ themeService = inject(CideThemeService);
7683
+ destroyRef = inject(DestroyRef);
7684
+ // Current theme state
7685
+ isDarkMode = signal(false, ...(ngDevMode ? [{ debugName: "isDarkMode" }] : []));
7686
+ isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
7687
+ // Computed values
7688
+ togglePosition = computed(() => this.isDarkMode() ? 'translate-x-5' : 'translate-x-0', ...(ngDevMode ? [{ debugName: "togglePosition" }] : []));
7689
+ toggleBgColor = computed(() => this.isDarkMode()
7690
+ ? 'tw-bg-gradient-to-r tw-from-blue-500 tw-to-indigo-600'
7691
+ : 'tw-bg-gray-200 dark:tw-bg-gray-700', ...(ngDevMode ? [{ debugName: "toggleBgColor" }] : []));
7692
+ ngOnInit() {
7693
+ // Subscribe to theme changes
7694
+ this.themeService.getEffectiveTheme$()
7695
+ .pipe(takeUntilDestroyed(this.destroyRef))
7696
+ .subscribe(theme => {
7697
+ this.isDarkMode.set(theme === 'dark');
7698
+ });
7699
+ // Set initial state
7700
+ this.isDarkMode.set(this.themeService.isDarkMode());
7690
7701
  }
7691
- /**
7692
- * Get file list from API
7693
- */
7694
- getFileList(body) {
7695
- const query = generateStringFromObject(body);
7696
- return this.http?.get(cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, query]))
7697
- .pipe(tap((response) => {
7698
- if (response?.success) {
7699
- this.fileListSubject.next(response?.data || []);
7700
- }
7701
- }), catchError$1(error => {
7702
- console.error('CideCoreFileManagerService API error:', error);
7703
- return this.handleError(error);
7704
- }));
7702
+ ngOnDestroy() {
7703
+ // Cleanup handled by takeUntilDestroyed
7705
7704
  }
7706
7705
  /**
7707
- * Get file list using mock data (deprecated - use getFileList instead)
7706
+ * Toggle between light and dark mode
7708
7707
  */
7709
- getFileListWithMockData(_body) {
7710
- console.warn('getFileListWithMockData is deprecated. Use getFileList instead.');
7711
- return this.getFileList(_body);
7708
+ toggleTheme() {
7709
+ if (this.isAnimating()) {
7710
+ return; // Prevent rapid clicking
7711
+ }
7712
+ this.isAnimating.set(true);
7713
+ // Toggle theme using the service
7714
+ this.themeService.toggleTheme();
7715
+ // Reset animation flag after transition
7716
+ setTimeout(() => {
7717
+ this.isAnimating.set(false);
7718
+ }, 300);
7712
7719
  }
7720
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleThemeToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7721
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.7", type: CideEleThemeToggleComponent, isStandalone: true, selector: "cide-ele-theme-toggle", ngImport: i0, template: "<div class=\"cide-theme-toggle-wrapper\">\r\n <button\r\n type=\"button\"\r\n class=\"cide-theme-toggle-button\"\r\n [class.is-dark]=\"isDarkMode()\"\r\n [class.is-animating]=\"isAnimating()\"\r\n (click)=\"toggleTheme()\"\r\n [attr.aria-label]=\"isDarkMode() ? 'Switch to light mode' : 'Switch to dark mode'\"\r\n [attr.aria-pressed]=\"isDarkMode()\">\r\n \r\n <!-- Toggle Track -->\r\n <span class=\"cide-theme-toggle-track\" [class]=\"toggleBgColor()\">\r\n <!-- Sun Icon (Light Mode) -->\r\n <span class=\"cide-theme-toggle-icon sun-icon\" [class.hidden]=\"isDarkMode()\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <circle cx=\"12\" cy=\"12\" r=\"4\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"currentColor\"/>\r\n <path d=\"M12 2V4M12 20V22M4 12H2M22 12H20M19.07 4.93L17.66 6.34M6.34 17.66L4.93 19.07M19.07 19.07L17.66 17.66M6.34 6.34L4.93 4.93\" \r\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </span>\r\n \r\n <!-- Moon Icon (Dark Mode) -->\r\n <span class=\"cide-theme-toggle-icon moon-icon\" [class.hidden]=\"!isDarkMode()\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" \r\n stroke=\"currentColor\" stroke-width=\"2\" fill=\"currentColor\"/>\r\n </svg>\r\n </span>\r\n </span>\r\n \r\n <!-- Toggle Thumb (Slider) -->\r\n <span class=\"cide-theme-toggle-thumb\" [style.transform]=\"togglePosition()\">\r\n </span>\r\n </button>\r\n</div>\r\n\r\n", styles: [".cide-theme-toggle-wrapper{display:inline-flex;align-items:center;justify-content:center}.cide-theme-toggle-button{position:relative;width:52px;height:32px;padding:0;border:none;background:transparent;cursor:pointer;outline:none;transition:all .3s cubic-bezier(.4,0,.2,1);user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.cide-theme-toggle-button:focus-visible{outline:2px solid rgba(59,130,246,.5);outline-offset:2px;border-radius:20px}.cide-theme-toggle-track{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:16px;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:space-between;padding:0 4px;box-shadow:inset 0 2px 4px #0000001a,0 1px 2px #0000000d}.is-dark .cide-theme-toggle-track{box-shadow:inset 0 2px 4px #0003,0 1px 2px #0000001a}.cide-theme-toggle-icon{display:flex;align-items:center;justify-content:center;width:20px;height:20px;color:#fff;transition:all .3s cubic-bezier(.4,0,.2,1);opacity:1}.cide-theme-toggle-icon.hidden{opacity:0;width:0;transform:scale(0)}.cide-theme-toggle-icon svg{width:16px;height:16px;filter:drop-shadow(0 1px 2px rgba(0,0,0,.2))}.cide-theme-toggle-icon.sun-icon{color:#fbbf24}.cide-theme-toggle-icon.moon-icon{color:#e0e7ff}.cide-theme-toggle-thumb{position:absolute;top:2px;left:2px;width:28px;height:28px;background:#fff;border-radius:50%;box-shadow:0 2px 4px #0003,0 1px 2px #0000001a;transition:transform .3s cubic-bezier(.4,0,.2,1);z-index:1;background:linear-gradient(180deg,#fff,#f9fafb)}.is-dark .cide-theme-toggle-thumb{background:linear-gradient(180deg,#f3f4f6,#e5e7eb);box-shadow:0 2px 4px #0000004d,0 1px 2px #00000026}.is-animating .cide-theme-toggle-thumb{transition:transform .3s cubic-bezier(.4,0,.2,1)}.cide-theme-toggle-button:hover .cide-theme-toggle-thumb{box-shadow:0 3px 6px #00000040,0 2px 4px #00000026}.is-dark .cide-theme-toggle-button:hover .cide-theme-toggle-thumb{box-shadow:0 3px 6px #0006,0 2px 4px #0003}.cide-theme-toggle-button:hover .cide-theme-toggle-track{box-shadow:inset 0 2px 4px #00000026,0 2px 4px #0000001a}.is-dark .cide-theme-toggle-button:hover .cide-theme-toggle-track{box-shadow:inset 0 2px 4px #00000040,0 2px 4px #00000026}.cide-theme-toggle-button:active .cide-theme-toggle-thumb{transform:scale(.95)}.cide-theme-toggle-button:disabled{opacity:.5;cursor:not-allowed}.cide-theme-toggle-button:disabled:hover .cide-theme-toggle-thumb,.cide-theme-toggle-button:disabled:hover .cide-theme-toggle-track{box-shadow:none}@media (prefers-color-scheme: dark){.cide-theme-toggle-thumb{background:linear-gradient(180deg,#f3f4f6,#e5e7eb)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
7722
+ }
7723
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleThemeToggleComponent, decorators: [{
7724
+ type: Component,
7725
+ args: [{ selector: 'cide-ele-theme-toggle', standalone: true, imports: [CommonModule], template: "<div class=\"cide-theme-toggle-wrapper\">\r\n <button\r\n type=\"button\"\r\n class=\"cide-theme-toggle-button\"\r\n [class.is-dark]=\"isDarkMode()\"\r\n [class.is-animating]=\"isAnimating()\"\r\n (click)=\"toggleTheme()\"\r\n [attr.aria-label]=\"isDarkMode() ? 'Switch to light mode' : 'Switch to dark mode'\"\r\n [attr.aria-pressed]=\"isDarkMode()\">\r\n \r\n <!-- Toggle Track -->\r\n <span class=\"cide-theme-toggle-track\" [class]=\"toggleBgColor()\">\r\n <!-- Sun Icon (Light Mode) -->\r\n <span class=\"cide-theme-toggle-icon sun-icon\" [class.hidden]=\"isDarkMode()\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <circle cx=\"12\" cy=\"12\" r=\"4\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"currentColor\"/>\r\n <path d=\"M12 2V4M12 20V22M4 12H2M22 12H20M19.07 4.93L17.66 6.34M6.34 17.66L4.93 19.07M19.07 19.07L17.66 17.66M6.34 6.34L4.93 4.93\" \r\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </span>\r\n \r\n <!-- Moon Icon (Dark Mode) -->\r\n <span class=\"cide-theme-toggle-icon moon-icon\" [class.hidden]=\"!isDarkMode()\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" \r\n stroke=\"currentColor\" stroke-width=\"2\" fill=\"currentColor\"/>\r\n </svg>\r\n </span>\r\n </span>\r\n \r\n <!-- Toggle Thumb (Slider) -->\r\n <span class=\"cide-theme-toggle-thumb\" [style.transform]=\"togglePosition()\">\r\n </span>\r\n </button>\r\n</div>\r\n\r\n", styles: [".cide-theme-toggle-wrapper{display:inline-flex;align-items:center;justify-content:center}.cide-theme-toggle-button{position:relative;width:52px;height:32px;padding:0;border:none;background:transparent;cursor:pointer;outline:none;transition:all .3s cubic-bezier(.4,0,.2,1);user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.cide-theme-toggle-button:focus-visible{outline:2px solid rgba(59,130,246,.5);outline-offset:2px;border-radius:20px}.cide-theme-toggle-track{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:16px;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:space-between;padding:0 4px;box-shadow:inset 0 2px 4px #0000001a,0 1px 2px #0000000d}.is-dark .cide-theme-toggle-track{box-shadow:inset 0 2px 4px #0003,0 1px 2px #0000001a}.cide-theme-toggle-icon{display:flex;align-items:center;justify-content:center;width:20px;height:20px;color:#fff;transition:all .3s cubic-bezier(.4,0,.2,1);opacity:1}.cide-theme-toggle-icon.hidden{opacity:0;width:0;transform:scale(0)}.cide-theme-toggle-icon svg{width:16px;height:16px;filter:drop-shadow(0 1px 2px rgba(0,0,0,.2))}.cide-theme-toggle-icon.sun-icon{color:#fbbf24}.cide-theme-toggle-icon.moon-icon{color:#e0e7ff}.cide-theme-toggle-thumb{position:absolute;top:2px;left:2px;width:28px;height:28px;background:#fff;border-radius:50%;box-shadow:0 2px 4px #0003,0 1px 2px #0000001a;transition:transform .3s cubic-bezier(.4,0,.2,1);z-index:1;background:linear-gradient(180deg,#fff,#f9fafb)}.is-dark .cide-theme-toggle-thumb{background:linear-gradient(180deg,#f3f4f6,#e5e7eb);box-shadow:0 2px 4px #0000004d,0 1px 2px #00000026}.is-animating .cide-theme-toggle-thumb{transition:transform .3s cubic-bezier(.4,0,.2,1)}.cide-theme-toggle-button:hover .cide-theme-toggle-thumb{box-shadow:0 3px 6px #00000040,0 2px 4px #00000026}.is-dark .cide-theme-toggle-button:hover .cide-theme-toggle-thumb{box-shadow:0 3px 6px #0006,0 2px 4px #0003}.cide-theme-toggle-button:hover .cide-theme-toggle-track{box-shadow:inset 0 2px 4px #00000026,0 2px 4px #0000001a}.is-dark .cide-theme-toggle-button:hover .cide-theme-toggle-track{box-shadow:inset 0 2px 4px #00000040,0 2px 4px #00000026}.cide-theme-toggle-button:active .cide-theme-toggle-thumb{transform:scale(.95)}.cide-theme-toggle-button:disabled{opacity:.5;cursor:not-allowed}.cide-theme-toggle-button:disabled:hover .cide-theme-toggle-thumb,.cide-theme-toggle-button:disabled:hover .cide-theme-toggle-track{box-shadow:none}@media (prefers-color-scheme: dark){.cide-theme-toggle-thumb{background:linear-gradient(180deg,#f3f4f6,#e5e7eb)}}\n"] }]
7726
+ }] });
7727
+
7728
+ /**
7729
+ * Notification API Service
7730
+ * HTTP client for notification API endpoints
7731
+ */
7732
+ class NotificationApiService {
7733
+ http = inject(HttpClient);
7734
+ apiUrl = cidePath.join([
7735
+ hostManagerRoutesUrl.cideSuiteHost,
7736
+ notificationRoutesUrl.module
7737
+ ]);
7713
7738
  /**
7714
- * Get file list from cache (if available)
7739
+ * Get user notifications
7715
7740
  */
7716
- getFileListFromCache() {
7717
- return this.fileListSubject.value;
7741
+ getNotifications(params) {
7742
+ let httpParams = new HttpParams();
7743
+ if (params) {
7744
+ Object.keys(params).forEach(key => {
7745
+ const value = params[key];
7746
+ if (value !== undefined && value !== null) {
7747
+ httpParams = httpParams.set(key, value.toString());
7748
+ }
7749
+ });
7750
+ }
7751
+ // Debug: Log the URL being used
7752
+ console.log('[NotificationApi] Request URL:', this.apiUrl);
7753
+ console.log('[NotificationApi] Full URL with params:', `${this.apiUrl}?${httpParams.toString()}`);
7754
+ return this.http.get(this.apiUrl, { params: httpParams });
7718
7755
  }
7719
7756
  /**
7720
- * Upload file with progress tracking
7757
+ * Get notification by ID
7721
7758
  */
7722
- uploadFile(request) {
7723
- const formData = new FormData();
7724
- formData.append('file', request.file);
7725
- formData.append('altText', request.altText || '');
7726
- formData.append('tags', JSON.stringify(request.tags || []));
7727
- formData.append('permissions', JSON.stringify(request.permissions || []));
7728
- formData.append('userId', request.userId || '');
7729
- const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, 'upload']);
7730
- const req = new HttpRequest('POST', url, formData, {
7731
- reportProgress: true
7732
- });
7733
- return this.http.request(req).pipe(catchError$1(this.handleError));
7759
+ getNotificationById(id) {
7760
+ return this.http.get(`${this.apiUrl}/${id}`);
7734
7761
  }
7735
7762
  /**
7736
- * Upload file with progress tracking (mock version - deprecated)
7763
+ * Get unread count
7737
7764
  */
7738
- uploadFileWithMockData(request) {
7739
- console.warn('uploadFileWithMockData is deprecated. Use uploadFile instead.');
7740
- return this.uploadFile(request);
7765
+ getUnreadCount() {
7766
+ return this.http.get(`${this.apiUrl}/${notificationRoutesUrl.unreadCount}`);
7741
7767
  }
7742
7768
  /**
7743
- * Update file metadata
7769
+ * Create notification
7744
7770
  */
7745
- updateFile(request) {
7746
- console.log('Updating file:', request);
7747
- const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager]);
7748
- return this.http.post(url, request)
7749
- .pipe(tap((response) => {
7750
- if (response.success) {
7751
- this.refreshFileList();
7752
- }
7753
- }), catchError$1(this.handleError));
7771
+ createNotification(payload) {
7772
+ return this.http.post(this.apiUrl, payload);
7754
7773
  }
7755
7774
  /**
7756
- * Delete file
7775
+ * Mark notification as read
7757
7776
  */
7758
- deleteFile(id) {
7759
- const payload = { cyfm_id: id };
7760
- const query = generateStringFromObject(payload);
7761
- const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, query]);
7762
- return this.http.delete(url)
7763
- .pipe(tap((response) => {
7764
- if (response.success) {
7765
- this.refreshFileList();
7766
- }
7767
- }), catchError$1(this.handleError));
7777
+ markAsRead(id) {
7778
+ const url = `${this.apiUrl}/${id}/read`;
7779
+ console.log('[NotificationApi] Mark as read URL:', url);
7780
+ return this.http.put(url, {});
7768
7781
  }
7769
7782
  /**
7770
- * Delete multiple files
7783
+ * Mark all notifications as read
7771
7784
  */
7772
- deleteMultipleFiles(ids) {
7773
- console.log('Deleting multiple files:', ids);
7774
- const payload = { ids };
7775
- const query = generateStringFromObject(payload);
7776
- const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, query]);
7777
- return this.http.delete(url)
7778
- .pipe(tap((response) => {
7779
- if (response.success) {
7780
- this.refreshFileList();
7781
- }
7782
- }), catchError$1(this.handleError));
7785
+ markAllAsRead() {
7786
+ const url = `${this.apiUrl}/read-all`;
7787
+ console.log('[NotificationApi] Mark all as read URL:', url);
7788
+ return this.http.put(url, {});
7783
7789
  }
7784
7790
  /**
7785
- * Toggle file active status
7791
+ * Archive notification
7786
7792
  */
7787
- toggleFileStatus(id) {
7788
- console.log('Toggling file status:', id);
7789
- const payload = { id };
7790
- const query = generateStringFromObject(payload);
7791
- const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, query]);
7792
- return this.http.put(url, {})
7793
- .pipe(tap((response) => {
7794
- if (response.success) {
7795
- this.refreshFileList();
7796
- }
7797
- }), catchError$1(this.handleError));
7793
+ archiveNotification(id) {
7794
+ return this.http.put(`${this.apiUrl}/${id}/archive`, {});
7798
7795
  }
7796
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: NotificationApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7797
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: NotificationApiService, providedIn: 'root' });
7798
+ }
7799
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: NotificationApiService, decorators: [{
7800
+ type: Injectable,
7801
+ args: [{
7802
+ providedIn: 'root'
7803
+ }]
7804
+ }] });
7805
+
7806
+ /**
7807
+ * WebSocket Notification Service
7808
+ * Socket.IO client for real-time notifications
7809
+ */
7810
+ class WebSocketNotificationService {
7811
+ socket = null;
7812
+ notificationSubject = new Subject();
7813
+ connectionStatusSubject = new BehaviorSubject({
7814
+ connected: false
7815
+ });
7816
+ // Signals for reactive state
7817
+ notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : []));
7818
+ unreadNotifications = signal([], ...(ngDevMode ? [{ debugName: "unreadNotifications" }] : []));
7819
+ // Public observables
7820
+ notification$ = this.notificationSubject.asObservable();
7821
+ connectionStatus$ = this.connectionStatusSubject.asObservable();
7822
+ // Computed signals
7823
+ allNotifications = computed(() => this.notifications(), ...(ngDevMode ? [{ debugName: "allNotifications" }] : []));
7824
+ unreadNotificationsCount = computed(() => this.unreadNotifications().length, ...(ngDevMode ? [{ debugName: "unreadNotificationsCount" }] : []));
7825
+ isConnected = computed(() => this.connectionStatusSubject.value.connected, ...(ngDevMode ? [{ debugName: "isConnected" }] : []));
7799
7826
  /**
7800
- * Get file by ID
7827
+ * Connect to Socket.IO server
7801
7828
  */
7802
- getFileById(id) {
7803
- console.log('Getting file by ID:', id);
7804
- const payload = { cyfm_id: id };
7805
- const query = generateStringFromObject(payload);
7806
- const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, 'byId', query]);
7807
- return this.http.get(url)
7808
- .pipe(catchError$1(this.handleError));
7829
+ connect(token, userId) {
7830
+ if (this.socket?.connected) {
7831
+ console.log('Socket already connected');
7832
+ return;
7833
+ }
7834
+ // Resolve actual API URL from placeholder
7835
+ // The placeholder is replaced by host interceptor for HTTP, but WebSocket needs direct resolution
7836
+ // Based on host-manager.json: "__cloud_ide_suite_layout__" -> "http://localhost:3001"
7837
+ const placeholder = hostManagerRoutesUrl.cideSuiteHost;
7838
+ let apiUrl = 'http://localhost:3001'; // Default from host-manager.json
7839
+ // Try to resolve from host-manager.json via fetch (same as HTTP interceptor)
7840
+ if (typeof window !== 'undefined') {
7841
+ // Try to fetch host-manager.json to get the actual URL
7842
+ fetch('/routes/host-manager.json')
7843
+ .then(res => res.json())
7844
+ .then((hostManagerRoutes) => {
7845
+ const hostManagerRoutesEntry = hostManagerRoutes?.hosts?.find((hostRoute) => placeholder?.includes(hostRoute?.url));
7846
+ if (hostManagerRoutesEntry?.replace) {
7847
+ apiUrl = hostManagerRoutesEntry.replace;
7848
+ }
7849
+ // Remove /api suffix if present (Socket.IO connects to base server)
7850
+ apiUrl = apiUrl.replace(/\/api\/?$/, '');
7851
+ this.connectWithUrl(apiUrl, token, userId);
7852
+ })
7853
+ .catch(() => {
7854
+ // Fallback if fetch fails - use default or try localStorage
7855
+ this.resolveUrlFallback(token, userId);
7856
+ });
7857
+ }
7858
+ else {
7859
+ // Server-side or fallback
7860
+ this.resolveUrlFallback(token, userId);
7861
+ }
7809
7862
  }
7810
7863
  /**
7811
- * Find file by ID
7864
+ * Fallback URL resolution
7812
7865
  */
7813
- findFileById(id, items = this.fileListSubject.value) {
7814
- for (const item of items) {
7815
- if (item._id === id) {
7816
- return item;
7866
+ resolveUrlFallback(token, userId) {
7867
+ let apiUrl = 'http://localhost:3001'; // Default from host-manager.json
7868
+ // Try to get from current window location
7869
+ if (typeof window !== 'undefined') {
7870
+ const currentOrigin = window.location.origin;
7871
+ // Check if we're on localhost (development)
7872
+ if (currentOrigin.includes('localhost') || currentOrigin.includes('127.0.0.1')) {
7873
+ // Use localhost:3001 for development (from host-manager.json)
7874
+ apiUrl = 'http://localhost:3001';
7875
+ }
7876
+ else {
7877
+ // In production, use the same origin (assuming API is on same domain)
7878
+ apiUrl = currentOrigin || 'http://localhost:3001';
7879
+ }
7880
+ // Try to get from localStorage (if set by app initialization)
7881
+ const storedApiUrl = localStorage.getItem('api_base_url');
7882
+ if (storedApiUrl) {
7883
+ apiUrl = storedApiUrl.replace(/\/api\/?$/, '');
7817
7884
  }
7818
7885
  }
7819
- return null;
7820
- }
7821
- /**
7822
- * Get file size in human readable format
7823
- */
7824
- getFileSizeDisplay(bytes) {
7825
- if (bytes === 0)
7826
- return '0 Bytes';
7827
- const k = 1024;
7828
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
7829
- const i = Math.floor(Math.log(bytes) / Math.log(k));
7830
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
7886
+ this.connectWithUrl(apiUrl, token, userId);
7831
7887
  }
7832
7888
  /**
7833
- * Get file type icon
7889
+ * Connect with resolved URL
7834
7890
  */
7835
- getFileTypeIcon(fileType) {
7836
- const typeMap = {
7837
- 'image/jpeg': 'image',
7838
- 'image/png': 'image',
7839
- 'image/gif': 'image',
7840
- 'image/svg+xml': 'image',
7841
- 'application/pdf': 'picture_as_pdf',
7842
- 'text/plain': 'description',
7843
- 'text/html': 'code',
7844
- 'application/json': 'code',
7845
- 'application/javascript': 'code',
7846
- 'text/css': 'code',
7847
- 'application/zip': 'folder_zip',
7848
- 'application/x-zip-compressed': 'folder_zip',
7849
- 'application/msword': 'description',
7850
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'description',
7851
- 'application/vnd.ms-excel': 'table_chart',
7852
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'table_chart',
7853
- 'application/vnd.ms-powerpoint': 'slideshow',
7854
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'slideshow'
7855
- };
7856
- return typeMap[fileType] || 'insert_drive_file';
7891
+ connectWithUrl(apiUrl, token, userId) {
7892
+ console.log('[WebSocket] Connecting to:', apiUrl);
7893
+ this.socket = io(apiUrl, {
7894
+ path: '/notifications-socket',
7895
+ transports: ['websocket', 'polling'],
7896
+ auth: {
7897
+ token: token
7898
+ },
7899
+ reconnection: true,
7900
+ reconnectionDelay: 1000,
7901
+ reconnectionAttempts: 5,
7902
+ reconnectionDelayMax: 5000
7903
+ });
7904
+ this.setupEventHandlers(userId);
7857
7905
  }
7858
7906
  /**
7859
- * Refresh file list from server
7907
+ * Setup Socket.IO event handlers
7860
7908
  */
7861
- refreshFileList() {
7862
- const defaultBody = {
7863
- pageIndex: 1,
7864
- pageSize: 10
7865
- };
7866
- this.getFileList(defaultBody).subscribe({
7867
- next: () => {
7868
- console.log('File list refreshed successfully');
7869
- },
7870
- error: (error) => {
7871
- console.error('Error refreshing file list:', error);
7872
- }
7909
+ setupEventHandlers(userId) {
7910
+ if (!this.socket)
7911
+ return;
7912
+ // Connection established
7913
+ this.socket.on('connect', () => {
7914
+ console.log('Socket.IO connected:', this.socket?.id);
7915
+ this.connectionStatusSubject.next({
7916
+ connected: true,
7917
+ socketId: this.socket?.id,
7918
+ userId: userId,
7919
+ timestamp: new Date()
7920
+ });
7921
+ });
7922
+ // Connection confirmation
7923
+ this.socket.on('connected', (data) => {
7924
+ console.log('Notification service connected:', data);
7925
+ this.connectionStatusSubject.next({
7926
+ connected: true,
7927
+ socketId: data.socketId,
7928
+ userId: data.userId,
7929
+ timestamp: data.timestamp
7930
+ });
7931
+ });
7932
+ // Receive notification
7933
+ this.socket.on('notification', (notification) => {
7934
+ console.log('Received notification:', notification);
7935
+ // Add to notifications list
7936
+ this.notifications.update(notifications => [notification, ...notifications]);
7937
+ // Add to unread list
7938
+ this.unreadNotifications.update(unread => [notification, ...unread]);
7939
+ // Emit to subscribers
7940
+ this.notificationSubject.next(notification);
7941
+ // Acknowledge receipt
7942
+ this.acknowledgeNotification(notification.id);
7943
+ });
7944
+ // Disconnect
7945
+ this.socket.on('disconnect', (reason) => {
7946
+ console.log('Socket.IO disconnected:', reason);
7947
+ this.connectionStatusSubject.next({
7948
+ connected: false,
7949
+ timestamp: new Date()
7950
+ });
7951
+ });
7952
+ // Connection error
7953
+ this.socket.on('connect_error', (error) => {
7954
+ console.error('Socket.IO connection error:', error);
7955
+ this.connectionStatusSubject.next({
7956
+ connected: false,
7957
+ timestamp: new Date()
7958
+ });
7873
7959
  });
7874
7960
  }
7875
7961
  /**
7876
- * Handle errors
7962
+ * Acknowledge notification receipt
7877
7963
  */
7878
- handleError(error) {
7879
- let errorMessage = 'An error occurred';
7880
- if (error instanceof Error) {
7881
- errorMessage = error.message;
7882
- }
7883
- else if (typeof error === 'string') {
7884
- errorMessage = error;
7964
+ acknowledgeNotification(notificationId) {
7965
+ if (this.socket?.connected) {
7966
+ this.socket.emit('notification:ack', { notificationId });
7885
7967
  }
7886
- console.error('CideCoreFileManagerService Error:', errorMessage);
7887
- return throwError(() => new Error(errorMessage));
7888
- }
7889
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideCoreFileManagerService, deps: [{ token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
7890
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideCoreFileManagerService, providedIn: 'root' });
7891
- }
7892
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideCoreFileManagerService, decorators: [{
7893
- type: Injectable,
7894
- args: [{
7895
- providedIn: 'root'
7896
- }]
7897
- }], ctorParameters: () => [{ type: i1$1.HttpClient }] });
7898
-
7899
- class DropdownManagerService {
7900
- activeDropdowns = signal(new Map(), ...(ngDevMode ? [{ debugName: "activeDropdowns" }] : []));
7901
- documentClickListener;
7902
- escapeKeyListener;
7903
- constructor() {
7904
- this.initializeGlobalListeners();
7905
- }
7906
- initializeGlobalListeners() {
7907
- // Global document click listener
7908
- this.documentClickListener = (event) => {
7909
- this.handleDocumentClick(event);
7910
- };
7911
- // Global escape key listener
7912
- this.escapeKeyListener = (event) => {
7913
- if (event.key === 'Escape') {
7914
- this.closeAllDropdowns();
7915
- }
7916
- };
7917
- // Add listeners
7918
- document.addEventListener('click', this.documentClickListener, true);
7919
- document.addEventListener('keydown', this.escapeKeyListener, true);
7920
- }
7921
- registerDropdown(dropdown) {
7922
- // Close any existing dropdowns before opening a new one
7923
- this.closeAllDropdowns();
7924
- this.activeDropdowns.update(map => {
7925
- const newMap = new Map(map);
7926
- newMap.set(dropdown.id, dropdown);
7927
- return newMap;
7928
- });
7929
7968
  }
7930
- unregisterDropdown(id) {
7931
- this.activeDropdowns.update(map => {
7932
- const newMap = new Map(map);
7933
- newMap.delete(id);
7934
- return newMap;
7935
- });
7969
+ /**
7970
+ * Mark notification as read
7971
+ */
7972
+ markAsRead(notificationId) {
7973
+ if (this.socket?.connected) {
7974
+ this.socket.emit('notification:read', { notificationId });
7975
+ // Remove from unread list
7976
+ this.unreadNotifications.update(unread => unread.filter(n => n.id !== notificationId));
7977
+ }
7936
7978
  }
7937
- closeAllDropdowns() {
7938
- const dropdowns = this.activeDropdowns();
7939
- dropdowns.forEach(dropdown => {
7940
- dropdown.close();
7941
- });
7942
- this.activeDropdowns.set(new Map());
7979
+ /**
7980
+ * Mark all as read
7981
+ */
7982
+ markAllAsRead() {
7983
+ const unreadIds = this.unreadNotifications().map(n => n.id);
7984
+ unreadIds.forEach(id => this.markAsRead(id));
7943
7985
  }
7944
- closeDropdown(id) {
7945
- const dropdowns = this.activeDropdowns();
7946
- const dropdown = dropdowns.get(id);
7947
- if (dropdown) {
7948
- dropdown.close();
7949
- this.unregisterDropdown(id);
7986
+ /**
7987
+ * Disconnect from server
7988
+ */
7989
+ disconnect() {
7990
+ if (this.socket) {
7991
+ this.socket.disconnect();
7992
+ this.socket = null;
7993
+ this.connectionStatusSubject.next({
7994
+ connected: false,
7995
+ timestamp: new Date()
7996
+ });
7950
7997
  }
7951
7998
  }
7952
- isDropdownOpen(id) {
7953
- return this.activeDropdowns().has(id);
7999
+ /**
8000
+ * Clear all notifications
8001
+ */
8002
+ clearNotifications() {
8003
+ this.notifications.set([]);
8004
+ this.unreadNotifications.set([]);
7954
8005
  }
7955
- handleDocumentClick(event) {
7956
- const dropdowns = this.activeDropdowns();
7957
- if (dropdowns.size === 0) {
7958
- return;
7959
- }
7960
- // Check if click is inside any open dropdown
7961
- const target = event.target;
7962
- let isClickInsideDropdown = false;
7963
- dropdowns.forEach(dropdown => {
7964
- if (dropdown.element && dropdown.element.contains(target)) {
7965
- isClickInsideDropdown = true;
7966
- }
7967
- });
7968
- // If click is outside all dropdowns, close them
7969
- if (!isClickInsideDropdown) {
7970
- this.closeAllDropdowns();
7971
- }
8006
+ /**
8007
+ * Remove notification from list
8008
+ */
8009
+ removeNotification(notificationId) {
8010
+ this.notifications.update(notifications => notifications.filter(n => n.id !== notificationId));
8011
+ this.unreadNotifications.update(unread => unread.filter(n => n.id !== notificationId));
7972
8012
  }
7973
8013
  ngOnDestroy() {
7974
- // Clean up global listeners
7975
- if (this.documentClickListener) {
7976
- document.removeEventListener('click', this.documentClickListener, true);
7977
- }
7978
- if (this.escapeKeyListener) {
7979
- document.removeEventListener('keydown', this.escapeKeyListener, true);
7980
- }
8014
+ this.disconnect();
8015
+ this.notificationSubject.complete();
8016
+ this.connectionStatusSubject.complete();
7981
8017
  }
7982
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: DropdownManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7983
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: DropdownManagerService, providedIn: 'root' });
8018
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: WebSocketNotificationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8019
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: WebSocketNotificationService, providedIn: 'root' });
7984
8020
  }
7985
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: DropdownManagerService, decorators: [{
8021
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: WebSocketNotificationService, decorators: [{
7986
8022
  type: Injectable,
7987
8023
  args: [{
7988
8024
  providedIn: 'root'
7989
8025
  }]
7990
- }], ctorParameters: () => [] });
8026
+ }] });
7991
8027
 
7992
- class KeyboardShortcutService {
7993
- shortcuts = new Map();
7994
- overrides = new Map();
7995
- keydownListener;
7996
- constructor() {
7997
- this.setupGlobalListener();
7998
- }
7999
- ngOnDestroy() {
8000
- this.removeGlobalListener();
8028
+ class CideCoreFileManagerService {
8029
+ http;
8030
+ apiUrl = `${coreRoutesUrl?.fileManager}`;
8031
+ fileListSubject = new BehaviorSubject([]);
8032
+ fileList$ = this.fileListSubject.asObservable();
8033
+ constructor(http) {
8034
+ this.http = http;
8035
+ console.log('CideCoreFileManagerService initialized - using real API');
8001
8036
  }
8002
8037
  /**
8003
- * Register a new keyboard shortcut
8038
+ * Get file list from API
8004
8039
  */
8005
- register(shortcut) {
8006
- this.shortcuts.set(shortcut.id, shortcut);
8007
- console.log(`⌨️ [KeyboardShortcut] Registered shortcut: ${shortcut.id} (${this.getKeyDescription(shortcut)})`);
8040
+ getFileList(body) {
8041
+ const query = generateStringFromObject(body);
8042
+ return this.http?.get(cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, query]))
8043
+ .pipe(tap((response) => {
8044
+ if (response?.success) {
8045
+ this.fileListSubject.next(response?.data || []);
8046
+ }
8047
+ }), catchError$1(error => {
8048
+ console.error('CideCoreFileManagerService API error:', error);
8049
+ return this.handleError(error);
8050
+ }));
8008
8051
  }
8009
8052
  /**
8010
- * Override an existing shortcut with new key combination
8053
+ * Get file list using mock data (deprecated - use getFileList instead)
8011
8054
  */
8012
- override(shortcutId, newKey, options) {
8013
- const originalShortcut = this.shortcuts.get(shortcutId);
8014
- if (!originalShortcut) {
8015
- console.warn(`⚠️ [KeyboardShortcut] Cannot override shortcut '${shortcutId}' - not found`);
8016
- return;
8017
- }
8018
- const override = {
8019
- shortcutId,
8020
- newKey,
8021
- newCtrlKey: options?.ctrlKey,
8022
- newAltKey: options?.altKey,
8023
- newShiftKey: options?.shiftKey,
8024
- newMetaKey: options?.metaKey
8025
- };
8026
- this.overrides.set(shortcutId, override);
8027
- console.log(`🔄 [KeyboardShortcut] Override registered for '${shortcutId}': ${this.getKeyDescription({ ...originalShortcut, ...options, key: newKey })}`);
8055
+ getFileListWithMockData(_body) {
8056
+ console.warn('getFileListWithMockData is deprecated. Use getFileList instead.');
8057
+ return this.getFileList(_body);
8028
8058
  }
8029
8059
  /**
8030
- * Remove a shortcut
8031
- */
8032
- unregister(shortcutId) {
8033
- this.shortcuts.delete(shortcutId);
8034
- this.overrides.delete(shortcutId);
8035
- console.log(`🗑️ [KeyboardShortcut] Removed shortcut: ${shortcutId}`);
8036
- }
8037
- /**
8038
- * Remove override for a shortcut (restore original key combination)
8060
+ * Get file list from cache (if available)
8039
8061
  */
8040
- removeOverride(shortcutId) {
8041
- this.overrides.delete(shortcutId);
8042
- console.log(`🔄 [KeyboardShortcut] Override removed for: ${shortcutId}`);
8062
+ getFileListFromCache() {
8063
+ return this.fileListSubject.value;
8043
8064
  }
8044
8065
  /**
8045
- * Get all registered shortcuts
8066
+ * Upload file with progress tracking
8046
8067
  */
8047
- getAllShortcuts() {
8048
- return Array.from(this.shortcuts.values());
8068
+ uploadFile(request) {
8069
+ const formData = new FormData();
8070
+ formData.append('file', request.file);
8071
+ formData.append('altText', request.altText || '');
8072
+ formData.append('tags', JSON.stringify(request.tags || []));
8073
+ formData.append('permissions', JSON.stringify(request.permissions || []));
8074
+ formData.append('userId', request.userId || '');
8075
+ const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, 'upload']);
8076
+ const req = new HttpRequest('POST', url, formData, {
8077
+ reportProgress: true
8078
+ });
8079
+ return this.http.request(req).pipe(catchError$1(this.handleError));
8049
8080
  }
8050
8081
  /**
8051
- * Get shortcuts by category or filter
8082
+ * Upload file with progress tracking (mock version - deprecated)
8052
8083
  */
8053
- getShortcuts(filter) {
8054
- const shortcuts = this.getAllShortcuts();
8055
- return filter ? shortcuts.filter(filter) : shortcuts;
8084
+ uploadFileWithMockData(request) {
8085
+ console.warn('uploadFileWithMockData is deprecated. Use uploadFile instead.');
8086
+ return this.uploadFile(request);
8056
8087
  }
8057
8088
  /**
8058
- * Check if a shortcut is registered
8089
+ * Update file metadata
8059
8090
  */
8060
- hasShortcut(shortcutId) {
8061
- return this.shortcuts.has(shortcutId);
8091
+ updateFile(request) {
8092
+ console.log('Updating file:', request);
8093
+ const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager]);
8094
+ return this.http.post(url, request)
8095
+ .pipe(tap((response) => {
8096
+ if (response.success) {
8097
+ this.refreshFileList();
8098
+ }
8099
+ }), catchError$1(this.handleError));
8062
8100
  }
8063
8101
  /**
8064
- * Get shortcut information
8102
+ * Delete file
8065
8103
  */
8066
- getShortcut(shortcutId) {
8067
- return this.shortcuts.get(shortcutId);
8104
+ deleteFile(id) {
8105
+ const payload = { cyfm_id: id };
8106
+ const query = generateStringFromObject(payload);
8107
+ const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, query]);
8108
+ return this.http.delete(url)
8109
+ .pipe(tap((response) => {
8110
+ if (response.success) {
8111
+ this.refreshFileList();
8112
+ }
8113
+ }), catchError$1(this.handleError));
8068
8114
  }
8069
8115
  /**
8070
- * Clear all shortcuts
8116
+ * Delete multiple files
8071
8117
  */
8072
- clearAll() {
8073
- this.shortcuts.clear();
8074
- this.overrides.clear();
8075
- console.log(`🧹 [KeyboardShortcut] All shortcuts cleared`);
8118
+ deleteMultipleFiles(ids) {
8119
+ console.log('Deleting multiple files:', ids);
8120
+ const payload = { ids };
8121
+ const query = generateStringFromObject(payload);
8122
+ const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, query]);
8123
+ return this.http.delete(url)
8124
+ .pipe(tap((response) => {
8125
+ if (response.success) {
8126
+ this.refreshFileList();
8127
+ }
8128
+ }), catchError$1(this.handleError));
8076
8129
  }
8077
8130
  /**
8078
- * Set up global keyboard listener
8131
+ * Toggle file active status
8079
8132
  */
8080
- setupGlobalListener() {
8081
- this.keydownListener = (event) => {
8082
- this.handleKeydown(event);
8083
- };
8084
- document.addEventListener('keydown', this.keydownListener);
8085
- console.log(`🎧 [KeyboardShortcut] Global keyboard listener attached`);
8133
+ toggleFileStatus(id) {
8134
+ console.log('Toggling file status:', id);
8135
+ const payload = { id };
8136
+ const query = generateStringFromObject(payload);
8137
+ const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, query]);
8138
+ return this.http.put(url, {})
8139
+ .pipe(tap((response) => {
8140
+ if (response.success) {
8141
+ this.refreshFileList();
8142
+ }
8143
+ }), catchError$1(this.handleError));
8086
8144
  }
8087
8145
  /**
8088
- * Remove global keyboard listener
8146
+ * Get file by ID
8089
8147
  */
8090
- removeGlobalListener() {
8091
- if (this.keydownListener) {
8092
- document.removeEventListener('keydown', this.keydownListener);
8093
- this.keydownListener = undefined;
8094
- console.log(`🎧 [KeyboardShortcut] Global keyboard listener removed`);
8095
- }
8148
+ getFileById(id) {
8149
+ console.log('Getting file by ID:', id);
8150
+ const payload = { cyfm_id: id };
8151
+ const query = generateStringFromObject(payload);
8152
+ const url = cidePath$1?.join([hostManagerRoutesUrl$1?.cideSuiteHost, coreRoutesUrl?.module, coreRoutesUrl?.fileManager, 'byId', query]);
8153
+ return this.http.get(url)
8154
+ .pipe(catchError$1(this.handleError));
8096
8155
  }
8097
8156
  /**
8098
- * Handle keydown events
8157
+ * Find file by ID
8099
8158
  */
8100
- handleKeydown(event) {
8101
- for (const [shortcutId, shortcut] of this.shortcuts) {
8102
- if (this.matchesShortcut(event, shortcut)) {
8103
- // Check for override
8104
- const override = this.overrides.get(shortcutId);
8105
- if (override && !this.matchesOverride(event, override)) {
8106
- continue; // Skip if override doesn't match
8107
- }
8108
- if (shortcut.preventDefault !== false) {
8109
- event.preventDefault();
8110
- }
8111
- if (shortcut.stopPropagation) {
8112
- event.stopPropagation();
8113
- }
8114
- console.log(`⌨️ [KeyboardShortcut] Executing shortcut: ${shortcutId}`);
8115
- shortcut.action();
8116
- break; // Only execute one shortcut per keydown
8159
+ findFileById(id, items = this.fileListSubject.value) {
8160
+ for (const item of items) {
8161
+ if (item._id === id) {
8162
+ return item;
8117
8163
  }
8118
8164
  }
8165
+ return null;
8119
8166
  }
8120
8167
  /**
8121
- * Check if event matches shortcut
8168
+ * Get file size in human readable format
8122
8169
  */
8123
- matchesShortcut(event, shortcut) {
8124
- return event.key === shortcut.key &&
8125
- (shortcut.ctrlKey === undefined || event.ctrlKey === shortcut.ctrlKey) &&
8126
- (shortcut.altKey === undefined || event.altKey === shortcut.altKey) &&
8127
- (shortcut.shiftKey === undefined || event.shiftKey === shortcut.shiftKey) &&
8128
- (shortcut.metaKey === undefined || event.metaKey === shortcut.metaKey);
8170
+ getFileSizeDisplay(bytes) {
8171
+ if (bytes === 0)
8172
+ return '0 Bytes';
8173
+ const k = 1024;
8174
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
8175
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
8176
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
8129
8177
  }
8130
8178
  /**
8131
- * Check if event matches override
8179
+ * Get file type icon
8132
8180
  */
8133
- matchesOverride(event, override) {
8134
- return event.key === override.newKey &&
8135
- (override.newCtrlKey === undefined || event.ctrlKey === override.newCtrlKey) &&
8136
- (override.newAltKey === undefined || event.altKey === override.newAltKey) &&
8137
- (override.newShiftKey === undefined || event.shiftKey === override.newShiftKey) &&
8138
- (override.newMetaKey === undefined || event.metaKey === override.newMetaKey);
8181
+ getFileTypeIcon(fileType) {
8182
+ const typeMap = {
8183
+ 'image/jpeg': 'image',
8184
+ 'image/png': 'image',
8185
+ 'image/gif': 'image',
8186
+ 'image/svg+xml': 'image',
8187
+ 'application/pdf': 'picture_as_pdf',
8188
+ 'text/plain': 'description',
8189
+ 'text/html': 'code',
8190
+ 'application/json': 'code',
8191
+ 'application/javascript': 'code',
8192
+ 'text/css': 'code',
8193
+ 'application/zip': 'folder_zip',
8194
+ 'application/x-zip-compressed': 'folder_zip',
8195
+ 'application/msword': 'description',
8196
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'description',
8197
+ 'application/vnd.ms-excel': 'table_chart',
8198
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'table_chart',
8199
+ 'application/vnd.ms-powerpoint': 'slideshow',
8200
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'slideshow'
8201
+ };
8202
+ return typeMap[fileType] || 'insert_drive_file';
8139
8203
  }
8140
8204
  /**
8141
- * Get human-readable key description
8205
+ * Refresh file list from server
8142
8206
  */
8143
- getKeyDescription(shortcut) {
8144
- const modifiers = [];
8145
- if (shortcut.ctrlKey)
8146
- modifiers.push('Ctrl');
8147
- if (shortcut.altKey)
8148
- modifiers.push('Alt');
8149
- if (shortcut.shiftKey)
8150
- modifiers.push('Shift');
8151
- if (shortcut.metaKey)
8152
- modifiers.push('Meta');
8153
- return [...modifiers, shortcut.key].join(' + ');
8207
+ refreshFileList() {
8208
+ const defaultBody = {
8209
+ pageIndex: 1,
8210
+ pageSize: 10
8211
+ };
8212
+ this.getFileList(defaultBody).subscribe({
8213
+ next: () => {
8214
+ console.log('File list refreshed successfully');
8215
+ },
8216
+ error: (error) => {
8217
+ console.error('Error refreshing file list:', error);
8218
+ }
8219
+ });
8154
8220
  }
8155
8221
  /**
8156
- * Get key description for a shortcut ID
8222
+ * Handle errors
8157
8223
  */
8158
- getKeyDescriptionForShortcut(shortcutId) {
8159
- const shortcut = this.shortcuts.get(shortcutId);
8160
- if (!shortcut)
8161
- return 'Not found';
8162
- const override = this.overrides.get(shortcutId);
8163
- if (override) {
8164
- return this.getKeyDescription({
8165
- key: override.newKey,
8166
- ctrlKey: override.newCtrlKey,
8167
- altKey: override.newAltKey,
8168
- shiftKey: override.newShiftKey,
8169
- metaKey: override.newMetaKey
8170
- });
8224
+ handleError(error) {
8225
+ let errorMessage = 'An error occurred';
8226
+ if (error instanceof Error) {
8227
+ errorMessage = error.message;
8171
8228
  }
8172
- return this.getKeyDescription(shortcut);
8229
+ else if (typeof error === 'string') {
8230
+ errorMessage = error;
8231
+ }
8232
+ console.error('CideCoreFileManagerService Error:', errorMessage);
8233
+ return throwError(() => new Error(errorMessage));
8173
8234
  }
8174
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: KeyboardShortcutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8175
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: KeyboardShortcutService, providedIn: 'root' });
8235
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideCoreFileManagerService, deps: [{ token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
8236
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideCoreFileManagerService, providedIn: 'root' });
8176
8237
  }
8177
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: KeyboardShortcutService, decorators: [{
8238
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideCoreFileManagerService, decorators: [{
8178
8239
  type: Injectable,
8179
8240
  args: [{
8180
8241
  providedIn: 'root'
8181
8242
  }]
8182
- }], ctorParameters: () => [] });
8243
+ }], ctorParameters: () => [{ type: i1$1.HttpClient }] });
8183
8244
 
8184
- class ConfirmationService {
8185
- // Modern signal-based state management
8186
- confirmationQueue = signal([], ...(ngDevMode ? [{ debugName: "confirmationQueue" }] : []));
8187
- currentRequest = signal(null, ...(ngDevMode ? [{ debugName: "currentRequest" }] : []));
8188
- // Modern computed signals with proper typing
8189
- hasActiveConfirmation = computed(() => this.currentRequest() !== null, ...(ngDevMode ? [{ debugName: "hasActiveConfirmation" }] : []));
8190
- queueLength = computed(() => this.confirmationQueue().length, ...(ngDevMode ? [{ debugName: "queueLength" }] : []));
8245
+ class DropdownManagerService {
8246
+ activeDropdowns = signal(new Map(), ...(ngDevMode ? [{ debugName: "activeDropdowns" }] : []));
8247
+ documentClickListener;
8248
+ escapeKeyListener;
8191
8249
  constructor() {
8192
- // Modern service initialization
8193
- }
8194
- ngOnDestroy() {
8195
- // Cleanup if needed
8196
- }
8197
- /**
8198
- * Ask for confirmation with a simple yes/no dialog
8199
- */
8200
- ask(options) {
8201
- console.log('🔔 ConfirmationService.ask called with options:', options);
8202
- return new Promise((resolve, reject) => {
8203
- const request = {
8204
- id: this.generateId(),
8205
- title: options.title,
8206
- message: options.message,
8207
- type: options.type || 'warning',
8208
- icon: options.icon || this.getDefaultIcon(options.type || 'warning'),
8209
- confirmText: options.confirmText || 'Confirm',
8210
- cancelText: options.cancelText || 'Cancel',
8211
- resolve: (value) => resolve(value),
8212
- reject
8213
- };
8214
- console.log('🔽 Ask for confirmation with a simple yes/no dialog', request);
8215
- this.addToQueue(request);
8216
- });
8250
+ this.initializeGlobalListeners();
8217
8251
  }
8218
- /**
8219
- * Ask for confirmation with custom template and return custom data
8220
- */
8221
- askWithTemplate(options) {
8222
- return new Promise((resolve, reject) => {
8223
- const request = {
8224
- id: this.generateId(),
8225
- title: options.title,
8226
- message: options.message,
8227
- type: options.type || 'info',
8228
- icon: options.icon || this.getDefaultIcon(options.type || 'info'),
8229
- confirmText: options.confirmText || 'Confirm',
8230
- cancelText: options.cancelText || 'Cancel',
8231
- customTemplate: options.customTemplate,
8232
- customData: options.customData,
8233
- resolve: (value) => resolve(value),
8234
- reject
8235
- };
8236
- // Cast to unknown for queue compatibility
8237
- this.addToQueue(request);
8238
- });
8252
+ initializeGlobalListeners() {
8253
+ // Global document click listener
8254
+ this.documentClickListener = (event) => {
8255
+ this.handleDocumentClick(event);
8256
+ };
8257
+ // Global escape key listener
8258
+ this.escapeKeyListener = (event) => {
8259
+ if (event.key === 'Escape') {
8260
+ this.closeAllDropdowns();
8261
+ }
8262
+ };
8263
+ // Add listeners
8264
+ document.addEventListener('click', this.documentClickListener, true);
8265
+ document.addEventListener('keydown', this.escapeKeyListener, true);
8239
8266
  }
8240
- /**
8241
- * Quick confirmation for dangerous actions
8242
- */
8243
- confirmDelete(itemName) {
8244
- return this.ask({
8245
- title: 'Confirm Delete',
8246
- message: itemName ? `Are you sure you want to delete "${itemName}"? This action cannot be undone.` : 'Are you sure you want to delete this item? This action cannot be undone.',
8247
- type: 'danger',
8248
- icon: 'delete_forever',
8249
- confirmText: 'Delete',
8250
- cancelText: 'Cancel'
8267
+ registerDropdown(dropdown) {
8268
+ // Close any existing dropdowns before opening a new one
8269
+ this.closeAllDropdowns();
8270
+ this.activeDropdowns.update(map => {
8271
+ const newMap = new Map(map);
8272
+ newMap.set(dropdown.id, dropdown);
8273
+ return newMap;
8251
8274
  });
8252
8275
  }
8253
- /**
8254
- * Quick confirmation for permanent actions
8255
- */
8256
- confirmPermanentAction(action, itemName) {
8257
- return this.ask({
8258
- title: `Confirm ${action}`,
8259
- message: itemName ? `Are you sure you want to ${action.toLowerCase()} "${itemName}"? This action cannot be undone.` : `Are you sure you want to ${action.toLowerCase()}? This action cannot be undone.`,
8260
- type: 'danger',
8261
- icon: 'warning',
8262
- confirmText: action,
8263
- cancelText: 'Cancel'
8276
+ unregisterDropdown(id) {
8277
+ this.activeDropdowns.update(map => {
8278
+ const newMap = new Map(map);
8279
+ newMap.delete(id);
8280
+ return newMap;
8264
8281
  });
8265
8282
  }
8266
- /**
8267
- * Quick confirmation for safe actions
8268
- */
8269
- confirmSafeAction(action, itemName) {
8270
- return this.ask({
8271
- title: `Confirm ${action}`,
8272
- message: itemName ? `Are you sure you want to ${action.toLowerCase()} "${itemName}"?` : `Are you sure you want to ${action.toLowerCase()}?`,
8273
- type: 'info',
8274
- icon: 'info',
8275
- confirmText: action,
8276
- cancelText: 'Cancel'
8283
+ closeAllDropdowns() {
8284
+ const dropdowns = this.activeDropdowns();
8285
+ dropdowns.forEach(dropdown => {
8286
+ dropdown.close();
8277
8287
  });
8288
+ this.activeDropdowns.set(new Map());
8278
8289
  }
8279
- // Modern private methods with proper typing
8280
- addToQueue(request) {
8281
- this.confirmationQueue.update(queue => [...queue, request]);
8282
- console.log('🔔 Added request to queue:', request);
8283
- this.processQueue();
8284
- }
8285
- processQueue() {
8286
- console.log('🔔 Processing queue. Current request:', this.currentRequest(), 'Queue length:', this.confirmationQueue().length);
8287
- if (this.currentRequest() || this.confirmationQueue().length === 0) {
8288
- return;
8290
+ closeDropdown(id) {
8291
+ const dropdowns = this.activeDropdowns();
8292
+ const dropdown = dropdowns.get(id);
8293
+ if (dropdown) {
8294
+ dropdown.close();
8295
+ this.unregisterDropdown(id);
8289
8296
  }
8290
- const nextRequest = this.confirmationQueue()[0];
8291
- console.log('🔔 Setting current request:', nextRequest);
8292
- this.currentRequest.set(nextRequest);
8293
8297
  }
8294
- // Modern public methods with proper typing
8295
- confirmCurrentRequest(data) {
8296
- const request = this.currentRequest();
8297
- if (!request)
8298
- return;
8299
- // Always resolve with true for confirmation, unless custom data is explicitly provided
8300
- const resolvedValue = data !== undefined ? data : true;
8301
- console.log('🔔 Confirming request with value:', resolvedValue);
8302
- request.resolve(resolvedValue);
8303
- this.removeCurrentRequest();
8298
+ isDropdownOpen(id) {
8299
+ return this.activeDropdowns().has(id);
8304
8300
  }
8305
- cancelCurrentRequest() {
8306
- const request = this.currentRequest();
8307
- if (!request)
8301
+ handleDocumentClick(event) {
8302
+ const dropdowns = this.activeDropdowns();
8303
+ if (dropdowns.size === 0) {
8308
8304
  return;
8309
- request.reject(new Error('User cancelled'));
8310
- this.removeCurrentRequest();
8311
- }
8312
- removeCurrentRequest() {
8313
- this.confirmationQueue.update(queue => queue.slice(1));
8314
- this.currentRequest.set(null);
8315
- // Process next request if any
8316
- setTimeout(() => this.processQueue(), 100);
8317
- }
8318
- generateId() {
8319
- return `confirmation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
8320
- }
8321
- // Modern private utility method with proper typing
8322
- getDefaultIcon(type) {
8323
- const iconMap = {
8324
- danger: 'error',
8325
- warning: 'warning',
8326
- info: 'info',
8327
- success: 'check_circle'
8328
- };
8329
- return iconMap[type];
8330
- }
8331
- // Modern public getters with proper typing
8332
- getCurrentRequest() {
8333
- return this.currentRequest;
8305
+ }
8306
+ // Check if click is inside any open dropdown
8307
+ const target = event.target;
8308
+ let isClickInsideDropdown = false;
8309
+ dropdowns.forEach(dropdown => {
8310
+ if (dropdown.element && dropdown.element.contains(target)) {
8311
+ isClickInsideDropdown = true;
8312
+ }
8313
+ });
8314
+ // If click is outside all dropdowns, close them
8315
+ if (!isClickInsideDropdown) {
8316
+ this.closeAllDropdowns();
8317
+ }
8334
8318
  }
8335
- getHasActiveConfirmation() {
8336
- return this.hasActiveConfirmation;
8319
+ ngOnDestroy() {
8320
+ // Clean up global listeners
8321
+ if (this.documentClickListener) {
8322
+ document.removeEventListener('click', this.documentClickListener, true);
8323
+ }
8324
+ if (this.escapeKeyListener) {
8325
+ document.removeEventListener('keydown', this.escapeKeyListener, true);
8326
+ }
8337
8327
  }
8338
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: ConfirmationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8339
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: ConfirmationService, providedIn: 'root' });
8328
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: DropdownManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8329
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: DropdownManagerService, providedIn: 'root' });
8340
8330
  }
8341
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: ConfirmationService, decorators: [{
8331
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: DropdownManagerService, decorators: [{
8342
8332
  type: Injectable,
8343
8333
  args: [{
8344
8334
  providedIn: 'root'
8345
8335
  }]
8346
8336
  }], ctorParameters: () => [] });
8347
8337
 
8348
- /**
8349
- * CIDE Element Library - Theme Service
8350
- * Independent theme management for the element library
8351
- * Can be used standalone without parent application theme dependencies
8352
- */
8353
- class CideThemeService {
8354
- platformId = inject(PLATFORM_ID);
8355
- isBrowser;
8356
- currentTheme$ = new BehaviorSubject('light');
8357
- effectiveTheme$ = new BehaviorSubject('light');
8358
- config = {
8359
- theme: 'light',
8360
- targetElement: null,
8361
- persistPreference: true,
8362
- storageKey: 'cide-theme',
8363
- useDataAttribute: true,
8364
- useClassName: true
8365
- };
8338
+ class KeyboardShortcutService {
8339
+ shortcuts = new Map();
8340
+ overrides = new Map();
8341
+ keydownListener;
8366
8342
  constructor() {
8367
- this.isBrowser = isPlatformBrowser(this.platformId);
8368
- if (this.isBrowser) {
8369
- this.config.targetElement = document.documentElement;
8370
- }
8343
+ this.setupGlobalListener();
8344
+ }
8345
+ ngOnDestroy() {
8346
+ this.removeGlobalListener();
8371
8347
  }
8372
8348
  /**
8373
- * Initialize the theme service with configuration
8374
- * This should be called once in the application, typically in main.ts or app config
8349
+ * Register a new keyboard shortcut
8375
8350
  */
8376
- initialize(config) {
8377
- if (!this.isBrowser) {
8351
+ register(shortcut) {
8352
+ this.shortcuts.set(shortcut.id, shortcut);
8353
+ console.log(`⌨️ [KeyboardShortcut] Registered shortcut: ${shortcut.id} (${this.getKeyDescription(shortcut)})`);
8354
+ }
8355
+ /**
8356
+ * Override an existing shortcut with new key combination
8357
+ */
8358
+ override(shortcutId, newKey, options) {
8359
+ const originalShortcut = this.shortcuts.get(shortcutId);
8360
+ if (!originalShortcut) {
8361
+ console.warn(`⚠️ [KeyboardShortcut] Cannot override shortcut '${shortcutId}' - not found`);
8378
8362
  return;
8379
8363
  }
8380
- // Merge configuration
8381
- if (config) {
8382
- this.config = {
8383
- ...this.config,
8384
- ...config,
8385
- targetElement: config.targetElement || document.documentElement
8386
- };
8387
- }
8388
- // Load saved theme preference
8389
- const savedTheme = this.loadThemePreference();
8390
- const initialTheme = savedTheme || this.config.theme;
8391
- // Apply initial theme
8392
- this.setTheme(initialTheme);
8393
- // Listen for system theme changes if theme is set to 'auto'
8394
- if (initialTheme === 'auto') {
8395
- this.listenToSystemTheme();
8396
- }
8364
+ const override = {
8365
+ shortcutId,
8366
+ newKey,
8367
+ newCtrlKey: options?.ctrlKey,
8368
+ newAltKey: options?.altKey,
8369
+ newShiftKey: options?.shiftKey,
8370
+ newMetaKey: options?.metaKey
8371
+ };
8372
+ this.overrides.set(shortcutId, override);
8373
+ console.log(`🔄 [KeyboardShortcut] Override registered for '${shortcutId}': ${this.getKeyDescription({ ...originalShortcut, ...options, key: newKey })}`);
8397
8374
  }
8398
8375
  /**
8399
- * Set the current theme
8376
+ * Remove a shortcut
8400
8377
  */
8401
- setTheme(theme) {
8402
- if (!this.isBrowser) {
8403
- return;
8404
- }
8405
- this.currentTheme$.next(theme);
8406
- // Save preference if persistence is enabled
8407
- if (this.config.persistPreference) {
8408
- this.saveThemePreference(theme);
8409
- }
8410
- // Determine effective theme
8411
- let effectiveTheme;
8412
- if (theme === 'auto') {
8413
- effectiveTheme = this.getSystemTheme();
8414
- this.listenToSystemTheme();
8415
- }
8416
- else {
8417
- effectiveTheme = theme;
8418
- }
8419
- this.effectiveTheme$.next(effectiveTheme);
8420
- this.applyTheme(effectiveTheme);
8378
+ unregister(shortcutId) {
8379
+ this.shortcuts.delete(shortcutId);
8380
+ this.overrides.delete(shortcutId);
8381
+ console.log(`🗑️ [KeyboardShortcut] Removed shortcut: ${shortcutId}`);
8421
8382
  }
8422
8383
  /**
8423
- * Get the current theme preference
8384
+ * Remove override for a shortcut (restore original key combination)
8424
8385
  */
8425
- getCurrentTheme() {
8426
- return this.currentTheme$.value;
8386
+ removeOverride(shortcutId) {
8387
+ this.overrides.delete(shortcutId);
8388
+ console.log(`🔄 [KeyboardShortcut] Override removed for: ${shortcutId}`);
8427
8389
  }
8428
8390
  /**
8429
- * Get the effective theme (actual theme being displayed)
8391
+ * Get all registered shortcuts
8430
8392
  */
8431
- getEffectiveTheme() {
8432
- return this.effectiveTheme$.value;
8393
+ getAllShortcuts() {
8394
+ return Array.from(this.shortcuts.values());
8433
8395
  }
8434
8396
  /**
8435
- * Observable for current theme changes
8397
+ * Get shortcuts by category or filter
8436
8398
  */
8437
- getCurrentTheme$() {
8438
- return this.currentTheme$.asObservable();
8399
+ getShortcuts(filter) {
8400
+ const shortcuts = this.getAllShortcuts();
8401
+ return filter ? shortcuts.filter(filter) : shortcuts;
8439
8402
  }
8440
8403
  /**
8441
- * Observable for effective theme changes
8404
+ * Check if a shortcut is registered
8442
8405
  */
8443
- getEffectiveTheme$() {
8444
- return this.effectiveTheme$.asObservable();
8406
+ hasShortcut(shortcutId) {
8407
+ return this.shortcuts.has(shortcutId);
8445
8408
  }
8446
8409
  /**
8447
- * Toggle between light and dark themes
8410
+ * Get shortcut information
8448
8411
  */
8449
- toggleTheme() {
8450
- const currentEffective = this.effectiveTheme$.value;
8451
- const newTheme = currentEffective === 'light' ? 'dark' : 'light';
8452
- this.setTheme(newTheme);
8412
+ getShortcut(shortcutId) {
8413
+ return this.shortcuts.get(shortcutId);
8453
8414
  }
8454
8415
  /**
8455
- * Check if dark mode is currently active
8416
+ * Clear all shortcuts
8456
8417
  */
8457
- isDarkMode() {
8458
- return this.effectiveTheme$.value === 'dark';
8418
+ clearAll() {
8419
+ this.shortcuts.clear();
8420
+ this.overrides.clear();
8421
+ console.log(`🧹 [KeyboardShortcut] All shortcuts cleared`);
8459
8422
  }
8460
8423
  /**
8461
- * Apply theme to target element
8424
+ * Set up global keyboard listener
8462
8425
  */
8463
- applyTheme(theme) {
8464
- if (!this.isBrowser || !this.config.targetElement) {
8465
- return;
8466
- }
8467
- const element = this.config.targetElement;
8468
- // Apply data-theme attribute
8469
- if (this.config.useDataAttribute) {
8470
- element.setAttribute('data-theme', theme);
8471
- }
8472
- // Apply class name
8473
- if (this.config.useClassName) {
8474
- if (theme === 'dark') {
8475
- element.classList.add('dark-mode');
8476
- element.classList.remove('light-mode');
8477
- }
8478
- else {
8479
- element.classList.add('light-mode');
8480
- element.classList.remove('dark-mode');
8481
- }
8426
+ setupGlobalListener() {
8427
+ this.keydownListener = (event) => {
8428
+ this.handleKeydown(event);
8429
+ };
8430
+ document.addEventListener('keydown', this.keydownListener);
8431
+ console.log(`🎧 [KeyboardShortcut] Global keyboard listener attached`);
8432
+ }
8433
+ /**
8434
+ * Remove global keyboard listener
8435
+ */
8436
+ removeGlobalListener() {
8437
+ if (this.keydownListener) {
8438
+ document.removeEventListener('keydown', this.keydownListener);
8439
+ this.keydownListener = undefined;
8440
+ console.log(`🎧 [KeyboardShortcut] Global keyboard listener removed`);
8482
8441
  }
8483
- // Also apply to body for better compatibility
8484
- if (element === document.documentElement && document.body) {
8485
- if (this.config.useDataAttribute) {
8486
- document.body.setAttribute('data-theme', theme);
8487
- }
8488
- if (this.config.useClassName) {
8489
- if (theme === 'dark') {
8490
- document.body.classList.add('dark-mode');
8491
- document.body.classList.remove('light-mode');
8442
+ }
8443
+ /**
8444
+ * Handle keydown events
8445
+ */
8446
+ handleKeydown(event) {
8447
+ for (const [shortcutId, shortcut] of this.shortcuts) {
8448
+ if (this.matchesShortcut(event, shortcut)) {
8449
+ // Check for override
8450
+ const override = this.overrides.get(shortcutId);
8451
+ if (override && !this.matchesOverride(event, override)) {
8452
+ continue; // Skip if override doesn't match
8492
8453
  }
8493
- else {
8494
- document.body.classList.add('light-mode');
8495
- document.body.classList.remove('dark-mode');
8454
+ if (shortcut.preventDefault !== false) {
8455
+ event.preventDefault();
8456
+ }
8457
+ if (shortcut.stopPropagation) {
8458
+ event.stopPropagation();
8496
8459
  }
8460
+ console.log(`⌨️ [KeyboardShortcut] Executing shortcut: ${shortcutId}`);
8461
+ shortcut.action();
8462
+ break; // Only execute one shortcut per keydown
8497
8463
  }
8498
8464
  }
8499
8465
  }
8500
8466
  /**
8501
- * Get system theme preference
8467
+ * Check if event matches shortcut
8502
8468
  */
8503
- getSystemTheme() {
8504
- if (!this.isBrowser) {
8505
- return 'light';
8506
- }
8507
- if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
8508
- return 'dark';
8509
- }
8510
- return 'light';
8469
+ matchesShortcut(event, shortcut) {
8470
+ return event.key === shortcut.key &&
8471
+ (shortcut.ctrlKey === undefined || event.ctrlKey === shortcut.ctrlKey) &&
8472
+ (shortcut.altKey === undefined || event.altKey === shortcut.altKey) &&
8473
+ (shortcut.shiftKey === undefined || event.shiftKey === shortcut.shiftKey) &&
8474
+ (shortcut.metaKey === undefined || event.metaKey === shortcut.metaKey);
8511
8475
  }
8512
8476
  /**
8513
- * Listen to system theme changes
8477
+ * Check if event matches override
8514
8478
  */
8515
- listenToSystemTheme() {
8516
- if (!this.isBrowser || !window.matchMedia) {
8517
- return;
8518
- }
8519
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
8520
- const handler = (e) => {
8521
- if (this.currentTheme$.value === 'auto') {
8522
- const effectiveTheme = e.matches ? 'dark' : 'light';
8523
- this.effectiveTheme$.next(effectiveTheme);
8524
- this.applyTheme(effectiveTheme);
8525
- }
8526
- };
8527
- // Modern browsers
8528
- if (mediaQuery.addEventListener) {
8529
- mediaQuery.addEventListener('change', handler);
8530
- }
8531
- else if (mediaQuery.addListener) {
8532
- // Legacy browsers
8533
- mediaQuery.addListener(handler);
8534
- }
8479
+ matchesOverride(event, override) {
8480
+ return event.key === override.newKey &&
8481
+ (override.newCtrlKey === undefined || event.ctrlKey === override.newCtrlKey) &&
8482
+ (override.newAltKey === undefined || event.altKey === override.newAltKey) &&
8483
+ (override.newShiftKey === undefined || event.shiftKey === override.newShiftKey) &&
8484
+ (override.newMetaKey === undefined || event.metaKey === override.newMetaKey);
8535
8485
  }
8536
8486
  /**
8537
- * Save theme preference to localStorage
8487
+ * Get human-readable key description
8538
8488
  */
8539
- saveThemePreference(theme) {
8540
- if (!this.isBrowser) {
8541
- return;
8542
- }
8543
- try {
8544
- // Save in new format: 'light', 'dark', or 'auto'
8545
- localStorage.setItem(this.config.storageKey, theme);
8546
- // Also update old format for backward compatibility
8547
- // This allows old code that reads 'true'/'false' to still work
8548
- if (theme === 'dark') {
8549
- localStorage.setItem(this.config.storageKey, 'dark');
8550
- }
8551
- else if (theme === 'light') {
8552
- localStorage.setItem(this.config.storageKey, 'light');
8553
- }
8554
- }
8555
- catch (error) {
8556
- console.warn('Failed to save theme preference to localStorage:', error);
8557
- }
8489
+ getKeyDescription(shortcut) {
8490
+ const modifiers = [];
8491
+ if (shortcut.ctrlKey)
8492
+ modifiers.push('Ctrl');
8493
+ if (shortcut.altKey)
8494
+ modifiers.push('Alt');
8495
+ if (shortcut.shiftKey)
8496
+ modifiers.push('Shift');
8497
+ if (shortcut.metaKey)
8498
+ modifiers.push('Meta');
8499
+ return [...modifiers, shortcut.key].join(' + ');
8558
8500
  }
8559
8501
  /**
8560
- * Load theme preference from localStorage
8502
+ * Get key description for a shortcut ID
8561
8503
  */
8562
- loadThemePreference() {
8563
- if (!this.isBrowser) {
8564
- return null;
8565
- }
8566
- try {
8567
- const saved = localStorage.getItem(this.config.storageKey);
8568
- // Handle new format: 'light', 'dark', 'auto'
8569
- if (saved && ['light', 'dark', 'auto'].includes(saved)) {
8570
- return saved;
8571
- }
8572
- // Handle legacy format: 'true' (dark mode) or 'false' (light mode)
8573
- if (saved === 'true') {
8574
- // Migrate to new format
8575
- localStorage.setItem(this.config.storageKey, 'dark');
8576
- return 'dark';
8577
- }
8578
- else if (saved === 'false') {
8579
- // Migrate to new format
8580
- localStorage.setItem(this.config.storageKey, 'light');
8581
- return 'light';
8582
- }
8583
- }
8584
- catch (error) {
8585
- console.warn('Failed to load theme preference from localStorage:', error);
8504
+ getKeyDescriptionForShortcut(shortcutId) {
8505
+ const shortcut = this.shortcuts.get(shortcutId);
8506
+ if (!shortcut)
8507
+ return 'Not found';
8508
+ const override = this.overrides.get(shortcutId);
8509
+ if (override) {
8510
+ return this.getKeyDescription({
8511
+ key: override.newKey,
8512
+ ctrlKey: override.newCtrlKey,
8513
+ altKey: override.newAltKey,
8514
+ shiftKey: override.newShiftKey,
8515
+ metaKey: override.newMetaKey
8516
+ });
8586
8517
  }
8587
- return null;
8518
+ return this.getKeyDescription(shortcut);
8519
+ }
8520
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: KeyboardShortcutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8521
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: KeyboardShortcutService, providedIn: 'root' });
8522
+ }
8523
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: KeyboardShortcutService, decorators: [{
8524
+ type: Injectable,
8525
+ args: [{
8526
+ providedIn: 'root'
8527
+ }]
8528
+ }], ctorParameters: () => [] });
8529
+
8530
+ class ConfirmationService {
8531
+ // Modern signal-based state management
8532
+ confirmationQueue = signal([], ...(ngDevMode ? [{ debugName: "confirmationQueue" }] : []));
8533
+ currentRequest = signal(null, ...(ngDevMode ? [{ debugName: "currentRequest" }] : []));
8534
+ // Modern computed signals with proper typing
8535
+ hasActiveConfirmation = computed(() => this.currentRequest() !== null, ...(ngDevMode ? [{ debugName: "hasActiveConfirmation" }] : []));
8536
+ queueLength = computed(() => this.confirmationQueue().length, ...(ngDevMode ? [{ debugName: "queueLength" }] : []));
8537
+ constructor() {
8538
+ // Modern service initialization
8539
+ }
8540
+ ngOnDestroy() {
8541
+ // Cleanup if needed
8588
8542
  }
8589
8543
  /**
8590
- * Clear saved theme preference
8544
+ * Ask for confirmation with a simple yes/no dialog
8591
8545
  */
8592
- clearThemePreference() {
8593
- if (!this.isBrowser) {
8546
+ ask(options) {
8547
+ console.log('🔔 ConfirmationService.ask called with options:', options);
8548
+ return new Promise((resolve, reject) => {
8549
+ const request = {
8550
+ id: this.generateId(),
8551
+ title: options.title,
8552
+ message: options.message,
8553
+ type: options.type || 'warning',
8554
+ icon: options.icon || this.getDefaultIcon(options.type || 'warning'),
8555
+ confirmText: options.confirmText || 'Confirm',
8556
+ cancelText: options.cancelText || 'Cancel',
8557
+ resolve: (value) => resolve(value),
8558
+ reject
8559
+ };
8560
+ console.log('🔽 Ask for confirmation with a simple yes/no dialog', request);
8561
+ this.addToQueue(request);
8562
+ });
8563
+ }
8564
+ /**
8565
+ * Ask for confirmation with custom template and return custom data
8566
+ */
8567
+ askWithTemplate(options) {
8568
+ return new Promise((resolve, reject) => {
8569
+ const request = {
8570
+ id: this.generateId(),
8571
+ title: options.title,
8572
+ message: options.message,
8573
+ type: options.type || 'info',
8574
+ icon: options.icon || this.getDefaultIcon(options.type || 'info'),
8575
+ confirmText: options.confirmText || 'Confirm',
8576
+ cancelText: options.cancelText || 'Cancel',
8577
+ customTemplate: options.customTemplate,
8578
+ customData: options.customData,
8579
+ resolve: (value) => resolve(value),
8580
+ reject
8581
+ };
8582
+ // Cast to unknown for queue compatibility
8583
+ this.addToQueue(request);
8584
+ });
8585
+ }
8586
+ /**
8587
+ * Quick confirmation for dangerous actions
8588
+ */
8589
+ confirmDelete(itemName) {
8590
+ return this.ask({
8591
+ title: 'Confirm Delete',
8592
+ message: itemName ? `Are you sure you want to delete "${itemName}"? This action cannot be undone.` : 'Are you sure you want to delete this item? This action cannot be undone.',
8593
+ type: 'danger',
8594
+ icon: 'delete_forever',
8595
+ confirmText: 'Delete',
8596
+ cancelText: 'Cancel'
8597
+ });
8598
+ }
8599
+ /**
8600
+ * Quick confirmation for permanent actions
8601
+ */
8602
+ confirmPermanentAction(action, itemName) {
8603
+ return this.ask({
8604
+ title: `Confirm ${action}`,
8605
+ message: itemName ? `Are you sure you want to ${action.toLowerCase()} "${itemName}"? This action cannot be undone.` : `Are you sure you want to ${action.toLowerCase()}? This action cannot be undone.`,
8606
+ type: 'danger',
8607
+ icon: 'warning',
8608
+ confirmText: action,
8609
+ cancelText: 'Cancel'
8610
+ });
8611
+ }
8612
+ /**
8613
+ * Quick confirmation for safe actions
8614
+ */
8615
+ confirmSafeAction(action, itemName) {
8616
+ return this.ask({
8617
+ title: `Confirm ${action}`,
8618
+ message: itemName ? `Are you sure you want to ${action.toLowerCase()} "${itemName}"?` : `Are you sure you want to ${action.toLowerCase()}?`,
8619
+ type: 'info',
8620
+ icon: 'info',
8621
+ confirmText: action,
8622
+ cancelText: 'Cancel'
8623
+ });
8624
+ }
8625
+ // Modern private methods with proper typing
8626
+ addToQueue(request) {
8627
+ this.confirmationQueue.update(queue => [...queue, request]);
8628
+ console.log('🔔 Added request to queue:', request);
8629
+ this.processQueue();
8630
+ }
8631
+ processQueue() {
8632
+ console.log('🔔 Processing queue. Current request:', this.currentRequest(), 'Queue length:', this.confirmationQueue().length);
8633
+ if (this.currentRequest() || this.confirmationQueue().length === 0) {
8594
8634
  return;
8595
8635
  }
8596
- try {
8597
- localStorage.removeItem(this.config.storageKey);
8598
- }
8599
- catch (error) {
8600
- console.warn('Failed to clear theme preference from localStorage:', error);
8601
- }
8636
+ const nextRequest = this.confirmationQueue()[0];
8637
+ console.log('🔔 Setting current request:', nextRequest);
8638
+ this.currentRequest.set(nextRequest);
8602
8639
  }
8603
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8604
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideThemeService, providedIn: 'root' });
8640
+ // Modern public methods with proper typing
8641
+ confirmCurrentRequest(data) {
8642
+ const request = this.currentRequest();
8643
+ if (!request)
8644
+ return;
8645
+ // Always resolve with true for confirmation, unless custom data is explicitly provided
8646
+ const resolvedValue = data !== undefined ? data : true;
8647
+ console.log('🔔 Confirming request with value:', resolvedValue);
8648
+ request.resolve(resolvedValue);
8649
+ this.removeCurrentRequest();
8650
+ }
8651
+ cancelCurrentRequest() {
8652
+ const request = this.currentRequest();
8653
+ if (!request)
8654
+ return;
8655
+ request.reject(new Error('User cancelled'));
8656
+ this.removeCurrentRequest();
8657
+ }
8658
+ removeCurrentRequest() {
8659
+ this.confirmationQueue.update(queue => queue.slice(1));
8660
+ this.currentRequest.set(null);
8661
+ // Process next request if any
8662
+ setTimeout(() => this.processQueue(), 100);
8663
+ }
8664
+ generateId() {
8665
+ return `confirmation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
8666
+ }
8667
+ // Modern private utility method with proper typing
8668
+ getDefaultIcon(type) {
8669
+ const iconMap = {
8670
+ danger: 'error',
8671
+ warning: 'warning',
8672
+ info: 'info',
8673
+ success: 'check_circle'
8674
+ };
8675
+ return iconMap[type];
8676
+ }
8677
+ // Modern public getters with proper typing
8678
+ getCurrentRequest() {
8679
+ return this.currentRequest;
8680
+ }
8681
+ getHasActiveConfirmation() {
8682
+ return this.hasActiveConfirmation;
8683
+ }
8684
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: ConfirmationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8685
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: ConfirmationService, providedIn: 'root' });
8605
8686
  }
8606
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideThemeService, decorators: [{
8687
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: ConfirmationService, decorators: [{
8607
8688
  type: Injectable,
8608
8689
  args: [{
8609
8690
  providedIn: 'root'
@@ -14249,5 +14330,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
14249
14330
  * Generated bundle index. Do not edit.
14250
14331
  */
14251
14332
 
14252
- export { CapitalizePipe, CideCoreFileManagerService, CideEleBreadcrumbComponent, CideEleButtonComponent, CideEleConfirmationModalComponent, CideEleDataGridComponent, CideEleDropdownComponent, CideEleFileImageDirective, CideEleFileInputComponent, CideEleFileManagerService, CideEleFloatingContainerComponent, CideEleFloatingContainerDynamicDirective, CideEleFloatingContainerManagerComponent, CideEleFloatingContainerService, CideEleFloatingFeaturesService, CideEleFloatingFileUploaderComponent, CideEleFloatingFileUploaderService, CideEleGlobalNotificationsComponent, CideEleJsonEditorComponent, CideEleResizerDirective, CideEleSkeletonLoaderComponent, CideEleTabComponent, CideEleToastNotificationComponent, CideElementsService, CideFormFieldErrorComponent, CideIconComponent, CideInputComponent, CideSelectComponent, CideSelectOptionComponent, CideSpinnerComponent, CideTextareaComponent, CideThemeService, ConfirmationService, CoreFileManagerInsertUpdatePayload, DEFAULT_GRID_CONFIG, DropdownManagerService, ExportService, ICoreCyfmSave, KeyboardShortcutService, MFileManager, NotificationApiService, NotificationService, PortalService, TooltipDirective, WebSocketNotificationService, cidePath, hostManagerRoutesUrl, notificationRoutesUrl };
14333
+ export { CapitalizePipe, CideCoreFileManagerService, CideEleBreadcrumbComponent, CideEleButtonComponent, CideEleConfirmationModalComponent, CideEleDataGridComponent, CideEleDropdownComponent, CideEleFileImageDirective, CideEleFileInputComponent, CideEleFileManagerService, CideEleFloatingContainerComponent, CideEleFloatingContainerDynamicDirective, CideEleFloatingContainerManagerComponent, CideEleFloatingContainerService, CideEleFloatingFeaturesService, CideEleFloatingFileUploaderComponent, CideEleFloatingFileUploaderService, CideEleGlobalNotificationsComponent, CideEleJsonEditorComponent, CideEleResizerDirective, CideEleSkeletonLoaderComponent, CideEleTabComponent, CideEleThemeToggleComponent, CideEleToastNotificationComponent, CideElementsService, CideFormFieldErrorComponent, CideIconComponent, CideInputComponent, CideSelectComponent, CideSelectOptionComponent, CideSpinnerComponent, CideTextareaComponent, CideThemeService, ConfirmationService, CoreFileManagerInsertUpdatePayload, DEFAULT_GRID_CONFIG, DropdownManagerService, ExportService, ICoreCyfmSave, KeyboardShortcutService, MFileManager, NotificationApiService, NotificationService, PortalService, TooltipDirective, WebSocketNotificationService, cidePath, hostManagerRoutesUrl, notificationRoutesUrl };
14253
14334
  //# sourceMappingURL=cloud-ide-element.mjs.map