cloud-ide-element 1.1.94 → 1.1.96

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