Exemples de Fonctionnalités de Bureau Web TypeScript

Exemples de fonctionnalités de bureau Web TypeScript incluant les boîtes de dialogue de fichiers, les boîtes de messages et la barre d'état du système

💻 Boîte de Dialogue Fichier typescript

🟢 simple ⭐⭐⭐

Ouvrir les boîtes de dialogue d'enregistrement et d'ouverture en utilisant File System Access API et l'entrée de fichier traditionnelle

⏱️ 20 min 🏷️ typescript, web, desktop features
Prerequisites: Basic TypeScript, File System Access API
// Web TypeScript File Dialog Examples
// Using File System Access API and File Input for desktop-like file dialogs

// 1. File Dialog Manager
class FileDialogManager {
  // Check if File System Access API is supported
  isFileSystemAccessAPISupported(): boolean {
    return 'showOpenFilePicker' in window;
  }

  // Open file picker (modern API)
  async openFile(options: {
    multiple?: boolean;
    types?: Array<{ description: string; accept: Record<string, string[]> }>;
  } = {}): Promise<File[]> {
    if (!this.isFileSystemAccessAPISupported()) {
      return this.openFileLegacy(options);
    }

    const opts: OpenFilePickerOptions = {
      multiple: options.multiple || false
    };

    if (options.types) {
      opts.types = options.types;
    }

    try {
      const handles = await (window as any).showOpenFilePicker(opts);

      const files: File[] = [];
      for (const handle of handles) {
        const file = await handle.getFile();
        files.push(file);
      }

      return files;
    } catch (error) {
      // User cancelled
      if ((error as Error).name === 'AbortError') {
        return [];
      }
      throw error;
    }
  }

  // Save file picker (modern API)
  async saveFile(options: {
    suggestedName?: string;
    types?: Array<{ description: string; accept: Record<string, string[]> }>;
  } = {}): Promise<FileSystemFileHandle | null> {
    if (!this.isFileSystemAccessAPISupported()) {
      return this.saveFileLegacy(options);
    }

    const opts: SaveFilePickerOptions = {};

    if (options.suggestedName) {
      opts.suggestedName = options.suggestedName;
    }

    if (options.types) {
      opts.types = options.types;
    }

    try {
      const handle = await (window as any).showSaveFilePicker(opts);
      return handle;
    } catch (error) {
      // User cancelled
      if ((error as Error).name === 'AbortError') {
        return null;
      }
      throw error;
    }
  }

  // Open directory picker
  async openDirectory(): Promise<FileSystemDirectoryHandle | null> {
    if (!this.isFileSystemAccessAPISupported()) {
      throw new Error('Directory picker not supported in this browser');
    }

    try {
      const handle = await (window as any).showDirectoryPicker();
      return handle;
    } catch (error) {
      // User cancelled
      if ((error as Error).name === 'AbortError') {
        return null;
      }
      throw error;
    }
  }

  // Legacy file open (input element)
  private openFileLegacy(options: {
    multiple?: boolean;
    types?: Array<{ description: string; accept: Record<string, string[]> }>;
  }): Promise<File[]> {
    return new Promise((resolve, reject) => {
      const input = document.createElement('input');
      input.type = 'file';
      input.multiple = options.multiple || false;

      if (options.types && options.types.length > 0) {
        const accept = options.types.flatMap(t => Object.keys(t.accept).flatMap(ext => t.accept[ext]));
        input.accept = accept.join(',');
      }

      input.onchange = (e) => {
        const files = (e.target as HTMLInputElement).files;
        if (files && files.length > 0) {
          resolve(Array.from(files));
        } else {
          resolve([]);
        }
      };

      input.oncancel = () => {
        resolve([]);
      };

      input.onerror = () => {
        reject(new Error('File selection failed'));
      };

      input.click();
    });
  }

  // Legacy file save (download)
  private async saveFileLegacy(options: {
    suggestedName?: string;
  }): Promise<FileSystemFileHandle | null> {
    // For legacy, we'll create a download trigger
    // Note: Can't return FileSystemFileHandle in legacy mode
    return null;
  }
}

// 2. File Type Presets
class FileTypePresets {
  // Get common file type presets
  static getPresets(): Record<string, Array<{ description: string; accept: Record<string, string[]> }>> {
    return {
      images: [
        {
          description: 'Image Files',
          accept: {
            'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']
          }
        }
      ],
      text: [
        {
          description: 'Text Files',
          accept: {
            'text/plain': ['.txt'],
            'text/html': ['.html', '.htm'],
            'text/css': ['.css'],
            'text/javascript': ['.js']
          }
        }
      ],
      json: [
        {
          description: 'JSON Files',
          accept: {
            'application/json': ['.json']
          }
        }
      ],
      documents: [
        {
          description: 'Documents',
          accept: {
            'application/pdf': ['.pdf'],
            'application/msword': ['.doc'],
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx']
          }
        }
      ],
      all: []
    };
  }

  // Get preset by name
  static getPreset(name: string): Array<{ description: string; accept: Record<string, string[]> }> {
    return this.getPresets()[name] || [];
  }
}

// 3. File Content Manager
class FileContentManager {
  // Read file content
  async readFile(file: File): Promise<string> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = () => {
        resolve(reader.result as string);
      };

      reader.onerror = () => {
        reject(new Error('Failed to read file'));
      };

      reader.readAsText(file);
    });
  }

  // Read file as binary
  async readFileAsBinary(file: File): Promise<ArrayBuffer> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = () => {
        resolve(reader.result as ArrayBuffer);
      };

      reader.onerror = () => {
        reject(new Error('Failed to read file'));
      };

      reader.readAsArrayBuffer(file);
    });
  }

  // Write content to file handle
  async writeFile(handle: FileSystemFileHandle, content: string): Promise<void> {
    const writable = await handle.createWritable();
    await writable.write(content);
    await writable.close();
  }

  // Write binary to file handle
  async writeBinaryFile(handle: FileSystemFileHandle, content: ArrayBuffer): Promise<void> {
    const writable = await handle.createWritable();
    await writable.write(content);
    await writable.close();
  }

  // Append to file
  async appendToFile(handle: FileSystemFileHandle, content: string): Promise<void> {
    const writable = await handle.createWritable({ keepExistingData: true });
    const existing = await this.readFile(await handle.getFile());
    await writable.write(existing + content);
    await writable.close();
  }
}

// 4. File Dialog Builder
class FileDialogBuilder {
  private options: {
    multiple?: boolean;
    suggestedName?: string;
    types?: Array<{ description: string; accept: Record<string, string[]> }>;
  } = {};

  // Set multiple selection
  allowMultiple(allow: boolean = true): this {
    this.options.multiple = allow;
    return this;
  }

  // Set suggested name
  setSuggestedName(name: string): this {
    this.options.suggestedName = name;
    return this;
  }

  // Add file type filter
  addTypeFilter(description: string, mimeType: string, extensions: string[]): this {
    if (!this.options.types) {
      this.options.types = [];
    }

    this.options.types.push({
      description,
      accept: { [mimeType]: extensions }
    });

    return this;
  }

  // Add preset type filter
  addPresetFilter(presetName: string): this {
    const preset = FileTypePresets.getPreset(presetName);

    if (preset.length > 0) {
      if (!this.options.types) {
        this.options.types = [];
      }

      this.options.types.push(...preset);
    }

    return this;
  }

  // Build and open dialog
  async open(): Promise<File[]> {
    const manager = new FileDialogManager();
    return manager.openFile(this.options);
  }

  // Build and save dialog
  async save(): Promise<FileSystemFileHandle | null> {
    const manager = new FileDialogManager();
    return manager.saveFile(this.options);
  }
}

// 5. Recent Files Manager
class RecentFilesManager {
  private storageKey: string = 'recentFiles';
  private maxFiles: number = 10;

  // Add file to recent list
  async addFile(file: File): Promise<void> {
    const recent = await getRecentFiles();

    const fileData = {
      name: file.name,
      size: file.size,
      type: file.type,
      lastModified: file.lastModified,
      timestamp: Date.now()
    };

    // Remove if already exists
    const filtered = recent.filter(f => f.name !== fileData.name);

    // Add to front
    filtered.unshift(fileData);

    // Keep only max files
    const trimmed = filtered.slice(0, this.maxFiles);

    localStorage.setItem(this.storageKey, JSON.stringify(trimmed));
  }

  // Get recent files
  async getRecentFiles(): Promise<Array<{
    name: string;
    size: number;
    type: string;
    lastModified: number;
    timestamp: number;
  }>> {
    const stored = localStorage.getItem(this.storageKey);

    if (!stored) {
      return [];
    }

    return JSON.parse(stored);
  }

  // Clear recent files
  clearRecentFiles(): void {
    localStorage.removeItem(this.storageKey);
  }

  // Remove specific file
  async removeFile(fileName: string): Promise<void> {
    const recent = await this.getRecentFiles();
    const filtered = recent.filter(f => f.name !== fileName);

    localStorage.setItem(this.storageKey, JSON.stringify(filtered));
  }
}

// 6. File Drop Zone Handler
class FileDropZoneHandler {
  private element: HTMLElement;
  private onDrop?: (files: File[]) => void;
  private onDragEnter?: () => void;
  private onDragLeave?: () => void;

  constructor(element: HTMLElement) {
    this.element = element;
  }

  // Set drop handler
  setDropHandler(handler: (files: File[]) => void): this {
    this.onDrop = handler;
    return this;
  }

  // Set drag enter handler
  setDragEnterHandler(handler: () => void): this {
    this.onDragEnter = handler;
    return this;
  }

  // Set drag leave handler
  setDragLeaveHandler(handler: () => void): this {
    this.onDragLeave = handler;
    return this;
  }

  // Enable drop zone
  enable(): void {
    this.element.addEventListener('dragover', this.handleDragOver);
    this.element.addEventListener('dragenter', this.handleDragEnter);
    this.element.addEventListener('dragleave', this.handleDragLeave);
    this.element.addEventListener('drop', this.handleDrop);
  }

  // Disable drop zone
  disable(): void {
    this.element.removeEventListener('dragover', this.handleDragOver);
    this.element.removeEventListener('dragenter', this.handleDragEnter);
    this.element.removeEventListener('dragleave', this.handleDragLeave);
    this.element.removeEventListener('drop', this.handleDrop);
  }

  private handleDragOver = (e: DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    if (e.dataTransfer) {
      e.dataTransfer.dropEffect = 'copy';
    }
  };

  private handleDragEnter = (e: DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    this.element.classList.add('drag-over');
    if (this.onDragEnter) {
      this.onDragEnter();
    }
  };

  private handleDragLeave = (e: DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    this.element.classList.remove('drag-over');
    if (this.onDragLeave) {
      this.onDragLeave();
    }
  };

  private handleDrop = (e: DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    this.element.classList.remove('drag-over');

    if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
      const files = Array.from(e.dataTransfer.files);

      if (this.onDrop) {
        this.onDrop(files);
      }
    }
  };
}

// Usage Examples
async function demonstrateFileDialog() {
  console.log('=== Web TypeScript File Dialog Examples ===\n');

  const dialogManager = new FileDialogManager();
  const contentManager = new FileContentManager();
  const recentManager = new RecentFilesManager();

  // 1. Check support
  console.log('--- 1. API Support ---');
  console.log(`File System Access API: ${dialogManager.isFileSystemAccessAPISupported() ? 'Supported' : 'Not Supported'}`);

  // 2. Open single file
  console.log('\n--- 2. Open Single File ---');
  const singleFile = await dialogManager.openFile({
    types: FileTypePresets.getPreset('text')
  });
  console.log(`Selected ${singleFile.length} file(s)`);

  // 3. Open multiple files
  console.log('\n--- 3. Open Multiple Files ---');
  const multipleFiles = await dialogManager.openFile({
    multiple: true,
    types: FileTypePresets.getPreset('images')
  });
  console.log(`Selected ${multipleFiles.length} file(s)`);

  // 4. Save file
  console.log('\n--- 4. Save File ---');
  const savedHandle = await dialogManager.saveFile({
    suggestedName: 'document.txt',
    types: FileTypePresets.getPreset('text')
  });

  if (savedHandle) {
    await contentManager.writeFile(savedHandle, 'Hello, World!');
    console.log('File saved successfully');
  }

  // 5. File dialog builder
  console.log('\n--- 5. File Dialog Builder ---');
  const builder = new FileDialogBuilder()
    .allowMultiple()
    .addPresetFilter('images');

  const files = await builder.open();
  console.log(`Builder selected ${files.length} file(s)`);

  // 6. Recent files
  console.log('\n--- 6. Recent Files ---');
  if (files.length > 0) {
    await recentManager.addFile(files[0]);
  }

  const recentFiles = await recentManager.getRecentFiles();
  console.log(`Recent files: ${recentFiles.map(f => f.name).join(', ')}`);

  // 7. Drop zone
  console.log('\n--- 7. Drop Zone ---');
  const dropZone = document.createElement('div');
  dropZone.style.width = '400px';
  dropZone.style.height = '200px';
  dropZone.style.border = '2px dashed #ccc';
  dropZone.style.display = 'flex';
  dropZone.style.alignItems = 'center';
  dropZone.style.justifyContent = 'center';
  dropZone.textContent = 'Drop files here';

  const dropHandler = new FileDropZoneHandler(dropZone)
    .setDropHandler((files) => {
      console.log(`Dropped ${files.length} file(s)`);
      files.forEach(f => console.log(`  - ${f.name}`));
    })
    .setDragEnterHandler(() => {
      dropZone.style.borderColor = '#4CAF50';
      dropZone.style.backgroundColor = '#f0f0f0';
    })
    .setDragLeaveHandler(() => {
      dropZone.style.borderColor = '#ccc';
      dropZone.style.backgroundColor = 'transparent';
    });

  dropHandler.enable();
  console.log('Drop zone created (would be attached to DOM in real usage)');

  console.log('\n=== All File Dialog Examples Completed ===');
}

// Helper function for RecentFilesManager
async function getRecentFiles() {
  return new Promise<Array<any>>((resolve) => {
    const stored = localStorage.getItem('recentFiles');
    resolve(stored ? JSON.parse(stored) : []);
  });
}

// Export functions
export { FileDialogManager, FileTypePresets, FileContentManager, FileDialogBuilder, RecentFilesManager, FileDropZoneHandler };
export { demonstrateFileDialog };

💻 Boîte de Message typescript

🟢 simple ⭐⭐⭐

Afficher des alertes, confirmations et boîtes de dialogue modales personnalisées pour l'interaction utilisateur

⏱️ 20 min 🏷️ typescript, web, desktop features
Prerequisites: Basic TypeScript, DOM manipulation
// Web TypeScript Message Box Examples
// Custom dialogs and modal boxes for desktop-like messaging

// 1. Message Box Types
type MessageBoxType = 'info' | 'warning' | 'error' | 'success' | 'question';
type MessageBoxIcon = 'info' | 'warning' | 'error' | 'success' | 'question';

// 2. Message Box Options
interface MessageBoxOptions {
  title?: string;
  message: string;
  type?: MessageBoxType;
  buttons?: Array<{ text: string; value?: any; primary?: boolean }>;
  defaultButton?: number;
  width?: string;
  closable?: boolean;
  backdrop?: boolean;
}

// 3. Message Box Result
interface MessageBoxResult {
  closed: boolean;
  buttonValue?: any;
}

// 4. Message Box Manager
class MessageBoxManager {
  private overlay: HTMLDivElement | null = null;
  private activeMessageBox: HTMLElement | null = null;

  // Show alert message
  async alert(message: string, title: string = 'Alert'): Promise<void> {
    await this.show({
      title,
      message,
      type: 'info',
      buttons: [{ text: 'OK', value: 'ok', primary: true }]
    });
  }

  // Show confirm dialog
  async confirm(message: string, title: string = 'Confirm'): Promise<boolean> {
    const result = await this.show({
      title,
      message,
      type: 'question',
      buttons: [
        { text: 'Yes', value: true, primary: true },
        { text: 'No', value: false }
      ]
    });

    return result.buttonValue === true;
  }

  // Show prompt dialog
  async prompt(message: string, defaultValue: string = '', title: string = 'Prompt'): Promise<string | null> {
    const result = await this.show({
      title,
      message,
      type: 'question',
      buttons: [
        { text: 'OK', value: 'ok', primary: true },
        { text: 'Cancel', value: 'cancel' }
      ]
    });

    if (result.buttonValue === 'ok') {
      const input = this.activeMessageBox?.querySelector('input') as HTMLInputElement;
      return input ? input.value : defaultValue;
    }

    return null;
  }

  // Show custom message box
  async show(options: MessageBoxOptions): Promise<MessageBoxResult> {
    return new Promise((resolve) => {
      // Create overlay
      this.createOverlay();

      // Create message box
      const messageBox = this.createMessageBox(options);

      // Add to DOM
      document.body.appendChild(messageBox);
      this.activeMessageBox = messageBox;

      // Focus
      setTimeout(() => {
        const primaryButton = messageBox.querySelector('.btn-primary') as HTMLButtonElement;
        if (primaryButton) {
          primaryButton.focus();
        }
      }, 100);

      // Setup handlers
      const closeHandler = (buttonValue?: any) => {
        this.close(messageBox);
        resolve({ closed: true, buttonValue });
      };

      // Add button click handlers
      const buttons = messageBox.querySelectorAll('button');
      buttons.forEach((button, index) => {
        button.addEventListener('click', () => {
          const value = options.buttons?.[index].value;
          closeHandler(value);
        });
      });

      // Handle backdrop click
      if (options.backdrop !== false) {
        this.overlay?.addEventListener('click', () => {
          if (options.closable !== false) {
            closeHandler();
          }
        });
      }

      // Handle ESC key
      const escapeHandler = (e: KeyboardEvent) => {
        if (e.key === 'Escape' && options.closable !== false) {
          closeHandler();
          document.removeEventListener('keydown', escapeHandler);
        }
      };

      document.addEventListener('keydown', escapeHandler);
    });
  }

  // Create overlay
  private createOverlay(): void {
    if (!this.overlay) {
      this.overlay = document.createElement('div');
      this.overlay.className = 'message-box-overlay';
      this.overlay.style.cssText = `
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 10000;
      `;
      document.body.appendChild(this.overlay);
    }
  }

  // Create message box
  private createMessageBox(options: MessageBoxOptions): HTMLElement {
    const box = document.createElement('div');
    box.className = 'message-box';

    const type = options.type || 'info';

    box.style.cssText = `
      background: white;
      border-radius: 8px;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
      min-width: 300px;
      max-width: ${options.width || '500px'};
      max-height: 80vh;
      overflow: auto;
      display: flex;
      flex-direction: column;
    `;

    // Header
    const header = this.createHeader(options.title || this.getDefaultTitle(type), type, options.closable);
    box.appendChild(header);

    // Content
    const content = this.createContent(options.message, type);
    box.appendChild(content);

    // Footer (buttons)
    const footer = this.createFooter(options.buttons || [{ text: 'OK', value: 'ok', primary: true }]);
    box.appendChild(footer);

    return box;
  }

  // Create header
  private createHeader(title: string, type: MessageBoxType, closable?: boolean): HTMLElement {
    const header = document.createElement('div');
    header.style.cssText = `
      padding: 16px 20px;
      border-bottom: 1px solid #e0e0e0;
      display: flex;
      align-items: center;
      gap: 12px;
    `;

    // Icon
    const icon = document.createElement('div');
    icon.textContent = this.getIconForType(type);
    icon.style.cssText = 'font-size: 24px;';
    header.appendChild(icon);

    // Title
    const titleElement = document.createElement('div');
    titleElement.textContent = title;
    titleElement.style.cssText = `
      flex: 1;
      font-size: 18px;
      font-weight: 600;
      color: #333;
    `;
    header.appendChild(titleElement);

    // Close button
    if (closable !== false) {
      const closeButton = document.createElement('button');
      closeButton.innerHTML = '×';
      closeButton.style.cssText = `
        background: none;
        border: none;
        font-size: 24px;
        cursor: pointer;
        padding: 0;
        width: 32px;
        height: 32px;
        display: flex;
        align-items: center;
        justify-content: center;
        color: #666;
      `;
      closeButton.onclick = () => {
        this.close(header.parentElement as HTMLElement);
      };
      header.appendChild(closeButton);
    }

    return header;
  }

  // Create content
  private createContent(message: string, type: MessageBoxType): HTMLElement {
    const content = document.createElement('div');
    content.style.cssText = `
      padding: 20px;
      color: #666;
      line-height: 1.5;
    `;

    if (type === 'question') {
      // Add input for prompt
      const input = document.createElement('input');
      input.type = 'text';
      input.style.cssText = `
        width: 100%;
        padding: 8px 12px;
        border: 1px solid #ddd;
        border-radius: 4px;
        font-size: 14px;
        margin-top: 12px;
      `;
      content.appendChild(document.createTextNode(message));
      content.appendChild(document.createElement('br'));
      content.appendChild(input);
    } else {
      content.textContent = message;
    }

    return content;
  }

  // Create footer
  private createFooter(buttons: Array<{ text: string; value?: any; primary?: boolean }>): HTMLElement {
    const footer = document.createElement('div');
    footer.style.cssText = `
      padding: 12px 20px;
      border-top: 1px solid #e0e0e0;
      display: flex;
      justify-content: flex-end;
      gap: 8px;
    `;

    buttons.forEach(buttonConfig => {
      const button = document.createElement('button');
      button.textContent = buttonConfig.text;
      button.className = buttonConfig.primary ? 'btn-primary' : '';

      button.style.cssText = `
        padding: 8px 16px;
        border: 1px solid #ddd;
        border-radius: 4px;
        background: ${buttonConfig.primary ? '#2196F3' : 'white'};
        color: ${buttonConfig.primary ? 'white' : '#666'};
        font-size: 14px;
        cursor: pointer;
        transition: all 0.2s;
      `;

      button.addEventListener('mouseenter', () => {
        button.style.background = buttonConfig.primary ? '#1976D2' : '#f5f5f5';
      });

      button.addEventListener('mouseleave', () => {
        button.style.background = buttonConfig.primary ? '#2196F3' : 'white';
      });

      footer.appendChild(button);
    });

    return footer;
  }

  // Get icon for type
  private getIconForType(type: MessageBoxType): string {
    const icons: Record<MessageBoxIcon, string> = {
      info: 'ℹ️',
      warning: '⚠️',
      error: '❌',
      success: '✅',
      question: '❓'
    };

    return icons[type as MessageBoxIcon] || 'ℹ️';
  }

  // Get default title
  private getDefaultTitle(type: MessageBoxType): string {
    const titles: Record<MessageBoxType, string> = {
      info: 'Information',
      warning: 'Warning',
      error: 'Error',
      success: 'Success',
      question: 'Question'
    };

    return titles[type];
  }

  // Close message box
  private close(messageBox: HTMLElement): void {
    messageBox.remove();

    if (this.overlay && document.querySelectorAll('.message-box').length === 0) {
      this.overlay.remove();
      this.overlay = null;
    }

    this.activeMessageBox = null;
  }
}

// 5. Toast Manager
class ToastManager {
  private container: HTMLElement | null = null;

  // Show toast notification
  show(
    message: string,
    type: 'info' | 'success' | 'warning' | 'error' = 'info',
    duration: number = 3000
  ): void {
    this.ensureContainer();

    const toast = this.createToast(message, type);
    this.container!.appendChild(toast);

    // Auto dismiss
    setTimeout(() => {
      this.dismiss(toast);
    }, duration);
  }

  // Show success toast
  success(message: string, duration?: number): void {
    this.show(message, 'success', duration);
  }

  // Show error toast
  error(message: string, duration?: number): void {
    this.show(message, 'error', duration);
  }

  // Show warning toast
  warning(message: string, duration?: number): void {
    this.show(message, 'warning', duration);
  }

  // Ensure container exists
  private ensureContainer(): void {
    if (!this.container) {
      this.container = document.createElement('div');
      this.container.className = 'toast-container';
      this.container.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 10001;
        display: flex;
        flex-direction: column;
        gap: 10px;
      `;
      document.body.appendChild(this.container);
    }
  }

  // Create toast element
  private createToast(message: string, type: string): HTMLElement {
    const toast = document.createElement('div');
    toast.className = `toast toast-${type}`;

    const colors: Record<string, string> = {
      info: '#2196F3',
      success: '#4CAF50',
      warning: '#FF9800',
      error: '#F44336'
    };

    toast.style.cssText = `
      background: ${colors[type] || colors.info};
      color: white;
      padding: 12px 16px;
      border-radius: 4px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
      min-width: 250px;
      max-width: 400px;
      animation: slideIn 0.3s ease-out;
      display: flex;
      align-items: center;
      gap: 8px;
    `;

    const icon = document.createElement('span');
    icon.textContent = this.getToastIcon(type);
    toast.appendChild(icon);

    const text = document.createElement('span');
    text.textContent = message;
    toast.appendChild(text);

    return toast;
  }

  // Get toast icon
  private getToastIcon(type: string): string {
    const icons: Record<string, string> = {
      info: 'ℹ️',
      success: '✅',
      warning: '⚠️',
      error: '❌'
    };

    return icons[type] || 'ℹ️';
  }

  // Dismiss toast
  private dismiss(toast: HTMLElement): void {
    toast.style.animation = 'slideOut 0.3s ease-in';

    setTimeout(() => {
      toast.remove();

      if (this.container && this.container.children.length === 0) {
        this.container.remove();
        this.container = null;
      }
    }, 300);
  }
}

// 6. Progress Dialog
class ProgressDialog {
  private messageBox: MessageBoxManager;
  private currentElement: HTMLElement | null = null;

  constructor() {
    this.messageBox = new MessageBoxManager();
  }

  // Show progress
  async show(options: {
    title: string;
    message: string;
    progress: number;
  }): Promise<void> {
    // Create custom progress dialog
    const overlay = document.createElement('div');
    overlay.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.5);
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 10000;
    `;

    const dialog = document.createElement('div');
    dialog.style.cssText = `
      background: white;
      border-radius: 8px;
      padding: 24px;
      min-width: 300px;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
    `;

    dialog.innerHTML = `
      <h3 style="margin: 0 0 16px 0; font-size: 18px;">${options.title}</h3>
      <p style="margin: 0 0 16px 0; color: #666;">${options.message}</p>
      <div style="
        width: 100%;
        height: 8px;
        background: #e0e0e0;
        border-radius: 4px;
        overflow: hidden;
      ">
        <div style="
          width: ${options.progress}%;
          height: 100%;
          background: #2196F3;
          transition: width 0.3s;
        "></div>
      </div>
      <p style="margin: 8px 0 0 0; text-align: center; color: #999; font-size: 14px;">
        ${Math.round(options.progress)}%
      </p>
    `;

    overlay.appendChild(dialog);
    document.body.appendChild(overlay);
    this.currentElement = overlay;
  }

  // Update progress
  update(progress: number): void {
    if (this.currentElement) {
      const progressBar = this.currentElement.querySelector('div > div') as HTMLDivElement;
      const progressText = this.currentElement.querySelector('p:last-child') as HTMLParagraphElement;

      if (progressBar) {
        progressBar.style.width = `${progress}%`;
      }

      if (progressText) {
        progressText.textContent = `${Math.round(progress)}%`;
      }
    }
  }

  // Close progress dialog
  close(): void {
    if (this.currentElement) {
      this.currentElement.remove();
      this.currentElement = null;
    }
  }
}

// Usage Examples
async function demonstrateMessageBox() {
  console.log('=== Web TypeScript Message Box Examples ===\n');

  const messageBox = new MessageBoxManager();
  const toast = new ToastManager();
  const progressDialog = new ProgressDialog();

  // 1. Alert
  console.log('--- 1. Alert ---');
  await messageBox.alert('This is an alert message!', 'Alert');

  // 2. Confirm
  console.log('\n--- 2. Confirm ---');
  const confirmed = await messageBox.confirm('Do you want to continue?', 'Confirm');
  console.log(`Confirmed: ${confirmed}`);

  // 3. Prompt
  console.log('\n--- 3. Prompt ---');
  const name = await messageBox.prompt('Please enter your name:', '', 'Input Required');
  console.log(`Name: ${name}`);

  // 4. Custom message box
  console.log('\n--- 4. Custom Message Box ---');
  const customResult = await messageBox.show({
    title: 'Custom Dialog',
    message: 'This is a custom message box with multiple buttons.',
    type: 'question',
    buttons: [
      { text: 'Option A', value: 'a', primary: false },
      { text: 'Option B', value: 'b', primary: true },
      { text: 'Option C', value: 'c', primary: false }
    ]
  });
  console.log(`Selected: ${customResult.buttonValue}`);

  // 5. Toast notifications
  console.log('\n--- 5. Toast Notifications ---');
  toast.show('This is an info toast');
  toast.success('Operation completed successfully!');
  toast.warning('This is a warning message');
  toast.error('An error occurred!');

  // 6. Progress dialog
  console.log('\n--- 6. Progress Dialog ---');
  await progressDialog.show({
    title: 'Processing',
    message: 'Please wait while we process...',
    progress: 0
  });

  for (let i = 0; i <= 100; i += 10) {
    progressDialog.update(i);
    await new Promise(resolve => setTimeout(resolve, 200));
  }

  progressDialog.close();

  console.log('\n=== All Message Box Examples Completed ===');
}

// Export functions
export { MessageBoxManager, ToastManager, ProgressDialog };
export { demonstrateMessageBox };
export type { MessageBoxOptions, MessageBoxResult, MessageBoxType };

💻 Barre d'État du Système typescript

🟡 intermediate ⭐⭐⭐

Utiliser Notification API pour des fonctionnalités de type barre d'état avec badges et permissions

⏱️ 25 min 🏷️ typescript, web, desktop features
Prerequisites: Intermediate TypeScript, Notification API
// Web TypeScript System Tray Examples
// Using Notification API and favicon badges for system tray-like features

// 1. Notification Manager
class NotificationManager {
  private permission: NotificationPermission = 'default';

  // Initialize notification manager
  async initialize(): Promise<void> {
    if (!this.isSupported()) {
      console.warn('Notifications not supported');
      return;
    }

    this.permission = Notification.permission;

    if (this.permission === 'default') {
      this.permission = await Notification.requestPermission();
    }
  }

  // Check if notifications are supported
  isSupported(): boolean {
    return 'Notification' in window;
  }

  // Check if permission is granted
  isGranted(): boolean {
    return this.permission === 'granted';
  }

  // Request permission
  async requestPermission(): Promise<NotificationPermission> {
    if (!this.isSupported()) {
      return 'denied';
    }

    this.permission = await Notification.requestPermission();
    return this.permission;
  }

  // Show notification
  show(options: {
    title: string;
    body: string;
    icon?: string;
    image?: string;
    badge?: string;
    tag?: string;
    data?: any;
    actions?: Array<{ action: string; title: string; icon?: string }>;
    vibrate?: number[];
    sound?: string;
    requireInteraction?: boolean;
  }): Notification | null {
    if (!this.isGranted()) {
      console.warn('Notification permission not granted');
      return null;
    }

    const notification = new Notification(options.title, {
      body: options.body,
      icon: options.icon,
      image: options.image,
      badge: options.badge,
      tag: options.tag,
      data: options.data,
      actions: options.actions,
      vibrate: options.vibrate,
      requireInteraction: options.requireInteraction
    });

    return notification;
  }

  // Show simple notification
  notify(title: string, body: string): Notification | null {
    return this.show({ title, body });
  }

  // Show success notification
  success(message: string, title: string = 'Success'): Notification | null {
    return this.show({
      title,
      body: message,
      icon: this.getIcon('success')
    });
  }

  // Show error notification
  error(message: string, title: string = 'Error'): Notification | null {
    return this.show({
      title,
      body: message,
      icon: this.getIcon('error'),
      vibrate: [200, 100, 200]
    });
  }

  // Show warning notification
  warning(message: string, title: string = 'Warning'): Notification | null {
    return this.show({
      title,
      body: message,
      icon: this.getIcon('warning')
    });
  }

  // Show info notification
  info(message: string, title: string = 'Information'): Notification | null {
    return this.show({
      title,
      body: message,
      icon: this.getIcon('info')
    });
  }

  // Show progress notification
  async showProgress(
    title: string,
    progress: number,
    options: {
      body?: string;
      tag?: string;
    } = {}
  ): Promise<Notification | null> {
    const notification = this.show({
      title,
      body: options.body || `${Math.round(progress)}% complete`,
      tag: options.tag || 'progress',
      requireInteraction: false,
      data: { progress }
    });

    return notification;
  }

  // Close notification by tag
  closeByTag(tag: string): void {
    // Note: Notification.close() only works on the notification instance
    // This is a placeholder for a tracking system
  }

  // Get icon URL
  private getIcon(type: 'success' | 'error' | 'warning' | 'info'): string {
    // In a real app, these would be actual icon URLs
    const icons: Record<string, string> = {
      success: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%234CAF50"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>',
      error: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23F44336"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',
      warning: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FF9800"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>',
      info: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%232196F3"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>'
    };

    return icons[type];
  }
}

// 2. Favicon Badge Manager
class FaviconBadgeManager {
  private originalFavicon: string = '';
  private canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;

  constructor() {
    this.canvas = document.createElement('canvas');
    this.canvas.width = 32;
    this.canvas.height = 32;
    this.ctx = this.canvas.getContext('2d')!;

    // Store original favicon
    const link = document.querySelector('link[rel="icon"]') as HTMLLinkElement;
    if (link) {
      this.originalFavicon = link.href;
    }
  }

  // Set badge number
  setBadge(count: number, color: string = '#F44336'): void {
    // Clear canvas
    this.ctx.clearRect(0, 0, 32, 32);

    // Draw original favicon (simplified - would need to load actual favicon)
    this.ctx.fillStyle = '#2196F3';
    this.ctx.fillRect(0, 0, 32, 32);

    // Draw badge circle
    if (count > 0) {
      this.ctx.fillStyle = color;
      this.ctx.beginPath();
      this.ctx.arc(24, 24, 10, 0, Math.PI * 2);
      this.ctx.fill();

      // Draw count text
      this.ctx.fillStyle = 'white';
      this.ctx.font = 'bold 12px Arial';
      this.ctx.textAlign = 'center';
      this.ctx.textBaseline = 'middle';

      const displayCount = count > 99 ? '99+' : count.toString();
      this.ctx.fillText(displayCount, 24, 24);
    }

    // Update favicon
    const dataURL = this.canvas.toDataURL();
    this.updateFavicon(dataURL);
  }

  // Set badge text
  setBadgeText(text: string, color: string = '#F44336'): void {
    this.ctx.clearRect(0, 0, 32, 32);

    // Draw original favicon background
    this.ctx.fillStyle = '#2196F3';
    this.ctx.fillRect(0, 0, 32, 32);

    if (text) {
      // Draw badge circle
      this.ctx.fillStyle = color;
      this.ctx.beginPath();
      this.ctx.arc(24, 24, 10, 0, Math.PI * 2);
      this.ctx.fill();

      // Draw text
      this.ctx.fillStyle = 'white';
      this.ctx.font = 'bold 10px Arial';
      this.ctx.textAlign = 'center';
      this.ctx.textBaseline = 'middle';
      this.ctx.fillText(text.substring(0, 3), 24, 24);
    }

    const dataURL = this.canvas.toDataURL();
    this.updateFavicon(dataURL);
  }

  // Set badge dot
  setBadgeDot(color: string = '#F44336'): void {
    this.ctx.clearRect(0, 0, 32, 32);

    // Draw original favicon background
    this.ctx.fillStyle = '#2196F3';
    this.ctx.fillRect(0, 0, 32, 32);

    // Draw dot
    this.ctx.fillStyle = color;
    this.ctx.beginPath();
    this.ctx.arc(26, 26, 6, 0, Math.PI * 2);
    this.ctx.fill();

    const dataURL = this.canvas.toDataURL();
    this.updateFavicon(dataURL);
  }

  // Clear badge
  clearBadge(): void {
    if (this.originalFavicon) {
      this.updateFavicon(this.originalFavicon);
    }
  }

  // Update favicon
  private updateFavicon(href: string): void {
    let link = document.querySelector('link[rel="icon"]') as HTMLLinkElement;

    if (!link) {
      link = document.createElement('link');
      link.rel = 'icon';
      document.head.appendChild(link);
    }

    link.href = href;
  }
}

// 3. Notification Scheduler
class NotificationScheduler {
  private notifications: Map<string, NodeJS.Timeout> = new Map();

  // Schedule notification
  schedule(
    id: string,
    delay: number,
    options: {
      title: string;
      body: string;
      icon?: string;
    }
  ): void {
    // Clear existing if any
    this.cancel(id);

    // Schedule new notification
    const timeout = setTimeout(() => {
      const manager = new NotificationManager();
      manager.show(options);
      this.notifications.delete(id);
    }, delay);

    this.notifications.set(id, timeout);
  }

  // Schedule at specific time
  scheduleAt(
    id: string,
    date: Date,
    options: {
      title: string;
      body: string;
      icon?: string;
    }
  ): void {
    const delay = date.getTime() - Date.now();

    if (delay > 0) {
      this.schedule(id, delay, options);
    }
  }

  // Cancel scheduled notification
  cancel(id: string): void {
    const timeout = this.notifications.get(id);

    if (timeout) {
      clearTimeout(timeout);
      this.notifications.delete(id);
    }
  }

  // Cancel all scheduled notifications
  cancelAll(): void {
    for (const [id, timeout] of this.notifications) {
      clearTimeout(timeout);
    }

    this.notifications.clear();
  }
}

// 4. Notification Categories
class NotificationCategories {
  private manager: NotificationManager;

  constructor() {
    this.manager = new NotificationManager();
  }

  // New message notification
  newMessage(from: string, message: string): Notification | null {
    return this.manager.show({
      title: `New message from ${from}`,
      body: message,
      icon: this.getIcon('message'),
      tag: 'message',
      data: { type: 'message', from }
    });
  }

  // Download complete notification
  downloadComplete(fileName: string): Notification | null {
    return this.manager.show({
      title: 'Download Complete',
      body: `${fileName} has been downloaded`,
      icon: this.getIcon('download'),
      tag: 'download',
      actions: [
        { action: 'open', title: 'Open' },
        { action: 'dismiss', title: 'Dismiss' }
      ]
    });
  }

  // System update notification
  systemUpdate(version: string): Notification | null {
    return this.manager.show({
      title: 'System Update Available',
      body: `Version ${version} is ready to install`,
      icon: this.getIcon('update'),
      tag: 'system-update',
      requireInteraction: true,
      actions: [
        { action: 'install', title: 'Install Now' },
        { action: 'later', title: 'Later' }
      ]
    });
  }

  // Reminder notification
  reminder(title: string, message: string): Notification | null {
    return this.manager.show({
      title,
      body: message,
      icon: this.getIcon('reminder'),
      tag: 'reminder',
      vibrate: [200, 100, 200]
    });
  }

  private getIcon(type: string): string {
    // Placeholder for actual icon URLs
    return 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%232196F3"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/></svg>';
  }
}

// 5. Notification History
class NotificationHistory {
  private storageKey: string = 'notificationHistory';
  private maxEntries: number = 50;

  // Add to history
  add(notification: {
    title: string;
    body: string;
    timestamp: number;
    type?: string;
  }): void {
    const history = this.getHistory();
    history.unshift(notification);

    // Keep only max entries
    const trimmed = history.slice(0, this.maxEntries);

    localStorage.setItem(this.storageKey, JSON.stringify(trimmed));
  }

  // Get history
  getHistory(): Array<{
    title: string;
    body: string;
    timestamp: number;
    type?: string;
  }> {
    const stored = localStorage.getItem(this.storageKey);

    if (!stored) {
      return [];
    }

    return JSON.parse(stored);
  }

  // Clear history
  clearHistory(): void {
    localStorage.removeItem(this.storageKey);
  }

  // Get by type
  getByType(type: string): Array<{
    title: string;
    body: string;
    timestamp: number;
    type?: string;
  }> {
    const history = this.getHistory();
    return history.filter(n => n.type === type);
  }
}

// 6. Desktop Notification Manager (Combined)
class DesktopNotificationManager {
  private notificationManager: NotificationManager;
  private badgeManager: FaviconBadgeManager;
  private scheduler: NotificationScheduler;
  private categories: NotificationCategories;
  private history: NotificationHistory;

  constructor() {
    this.notificationManager = new NotificationManager();
    this.badgeManager = new FaviconBadgeManager();
    this.scheduler = new NotificationScheduler();
    this.categories = new NotificationCategories();
    this.history = new NotificationHistory();
  }

  // Initialize all systems
  async initialize(): Promise<void> {
    await this.notificationManager.initialize();
  }

  // Show notification with badge
  notifyWithBadge(
    title: string,
    body: string,
    badgeCount?: number
  ): Notification | null {
    const notification = this.notificationManager.show({ title, body });

    if (badgeCount !== undefined) {
      this.badgeManager.setBadge(badgeCount);
    }

    // Add to history
    this.history.add({
      title,
      body,
      timestamp: Date.now(),
      type: 'notification'
    });

    return notification;
  }

  // Update badge
  updateBadge(count: number, color?: string): void {
    if (count === 0) {
      this.badgeManager.clearBadge();
    } else {
      this.badgeManager.setBadge(count, color);
    }
  }

  // Get notification manager
  getNotificationManager(): NotificationManager {
    return this.notificationManager;
  }

  // Get badge manager
  getBadgeManager(): FaviconBadgeManager {
    return this.badgeManager;
  }

  // Get scheduler
  getScheduler(): NotificationScheduler {
    return this.scheduler;
  }

  // Get categories
  getCategories(): NotificationCategories {
    return this.categories;
  }

  // Get history
  getHistory(): NotificationHistory {
    return this.history;
  }
}

// Usage Examples
async function demonstrateSystemTray() {
  console.log('=== Web TypeScript System Tray Examples ===\n');

  const manager = new DesktopNotificationManager();

  // 1. Initialize
  console.log('--- 1. Initialize ---');
  await manager.initialize();
  console.log(`Notifications supported: ${manager.getNotificationManager().isSupported()}`);
  console.log(`Permission granted: ${manager.getNotificationManager().isGranted()}`);

  // 2. Basic notifications
  console.log('\n--- 2. Basic Notifications ---');
  manager.getNotificationManager().info('This is an info notification');
  await new Promise(resolve => setTimeout(resolve, 1000));

  manager.getNotificationManager().success('Operation completed successfully!');
  await new Promise(resolve => setTimeout(resolve, 1000));

  manager.getNotificationManager().warning('This is a warning message');
  await new Promise(resolve => setTimeout(resolve, 1000));

  manager.getNotificationManager().error('An error has occurred!');

  // 3. Badge notifications
  console.log('\n--- 3. Badge Notifications ---');
  manager.updateBadge(5);
  manager.notifyWithBadge('New messages', 'You have 5 unread messages');
  await new Promise(resolve => setTimeout(resolve, 2000));

  manager.updateBadge(1);
  manager.notifyWithBadge('New message', 'You received a new message', 1);
  await new Promise(resolve => setTimeout(resolve, 2000));

  manager.updateBadge(0);

  // 4. Scheduled notifications
  console.log('\n--- 4. Scheduled Notifications ---');
  manager.getScheduler().schedule('test', 3000, {
    title: 'Scheduled Notification',
    body: 'This notification was scheduled 3 seconds ago'
  });
  console.log('Notification scheduled in 3 seconds...');

  // 5. Category notifications
  console.log('\n--- 5. Category Notifications ---');
  await new Promise(resolve => setTimeout(resolve, 4000));

  manager.getCategories().newMessage('Alice', 'Hey, how are you?');
  await new Promise(resolve => setTimeout(resolve, 1000));

  manager.getCategories().downloadComplete('document.pdf');
  await new Promise(resolve => setTimeout(resolve, 1000));

  manager.getCategories().reminder('Meeting', 'Team meeting in 15 minutes');

  // 6. History
  console.log('\n--- 6. Notification History ---');
  const history = manager.getHistory().getHistory();
  console.log(`Total notifications: ${history.length}`);
  history.slice(0, 3).forEach(n => {
    console.log(`- ${n.title}: ${n.body}`);
  });

  console.log('\n=== All System Tray Examples Completed ===');
}

// Export functions
export { NotificationManager, FaviconBadgeManager, NotificationScheduler, NotificationCategories, NotificationHistory, DesktopNotificationManager };
export { demonstrateSystemTray };