Exemplos de Recursos de Desktop Web TypeScript

Exemplos de recursos de desktop Web TypeScript incluindo caixas de diálogo de arquivo, caixas de mensagem e bandeja do sistema

💻 Caixa de Diálogo de Arquivo typescript

🟢 simple ⭐⭐⭐

Abrir caixas de diálogo de salvar e abrir usando File System Access API e entrada de arquivo tradicional

⏱️ 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 };

💻 Caixa de Mensagem typescript

🟢 simple ⭐⭐⭐

Exibir alertas, confirmações e diálogos modais personalizados para interação do usuário

⏱️ 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 };

💻 Bandeja do Sistema typescript

🟡 intermediate ⭐⭐⭐

Usar Notification API para funcionalidades tipo bandeja do sistema com badges e permissões

⏱️ 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 };