app.js

63.04 KB
20/10/2025 09:08
JS
app.js
/**
 * Sticky Notes App - Offline Web Application
 * A complete sticky notes application with local storage persistence
 */

// Global namespace for the app
const StickyApp = (function() {
  'use strict';

  // Utility functions
  const Utils = {
    /**
     * Generate a UUID v4
     * @returns {string} UUID
     */
    generateUUID: function() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = Math.random() * 16 | 0;
        const v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    },

    /**
     * Debounce function to limit how often a function can be called
     * @param {Function} func - Function to debounce
     * @param {number} wait - Wait time in milliseconds
     * @returns {Function} Debounced function
     */
    debounce: function(func, wait) {
      let timeout;
      return function(...args) {
        const context = this;
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(context, args), wait);
      };
    },

    /**
     * Sanitize HTML to prevent XSS
     * @param {string} str - String to sanitize
     * @returns {string} Sanitized string
     */
    sanitizeHTML: function(str) {
      const temp = document.createElement('div');
      temp.textContent = str;
      return temp.innerHTML;
    },

    /**
     * Extract tags from text content
     * @param {string} content - Text content
     * @returns {Array} Array of tags
     */
    extractTags: function(content) {
      const tagRegex = /#(\w+)/g;
      const tags = [];
      let match;

      while ((match = tagRegex.exec(content)) !== null) {
        tags.push(match[1]);
      }

      return [...new Set(tags)]; // Remove duplicates
    },

    /**
     * Format date for display
     * @param {Date|string} date - Date to format
     * @returns {string} Formatted date
     */
    formatDate: function(date) {
      const d = new Date(date);
      return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
    },

    /**
     * Show notification
     * @param {string} message - Message to display
     * @param {string} type - Type of notification (success, error, info)
     */
    showNotification: function(message, type = 'info') {
      const container = document.getElementById('notification-container');
      const notification = document.createElement('div');
      notification.className = `notification ${type}`;
      notification.textContent = message;

      container.appendChild(notification);

      // Trigger animation
      setTimeout(() => {
        notification.classList.add('show');
      }, 10);

      // Remove after 3 seconds
      setTimeout(() => {
        notification.classList.remove('show');
        setTimeout(() => {
          container.removeChild(notification);
        }, 300);
      }, 3000);
    }
  };

  /**
   * Note model class
   */
  class Note {
    constructor(data = {}) {
      this.id = data.id || Utils.generateUUID();
      this.content = data.content || '';
      this.x = data.x || 20 + Math.random() * 200;
      this.y = data.y || 20 + Math.random() * 200;
      this.width = data.width || 250;
      this.height = data.height || 250;
      this.color = data.color || '#fff9a8';
      this.zIndex = data.zIndex || 10;
      this.createdAt = data.createdAt || new Date().toISOString();
      this.updatedAt = data.updatedAt || new Date().toISOString();
      this.pinned = data.pinned || false;
      this.tags = data.tags || [];
      this.reminder = data.reminder || null;
      this.category = data.category || 'Uncategorized';
      this.isMarkdown = data.isMarkdown !== undefined ? data.isMarkdown : false;
      this.kanbanStatus = data.kanbanStatus || 'todo';
    }

    /**
     * Update note content and extract tags
     * @param {string} content - New content
     */
    updateContent(content) {
      this.content = content;
      this.tags = Utils.extractTags(content);
      this.updatedAt = new Date().toISOString();
    }

    /**
     * Convert note to JSON
     * @returns {Object} Note data
     */
    toJSON() {
      return {
        id: this.id,
        content: this.content,
        x: this.x,
        y: this.y,
        width: this.width,
        height: this.height,
        color: this.color,
        zIndex: this.zIndex,
        createdAt: this.createdAt,
        updatedAt: this.updatedAt,
        pinned: this.pinned,
        tags: this.tags,
        reminder: this.reminder,
        category: this.category,
        isMarkdown: this.isMarkdown,
        kanbanStatus: this.kanbanStatus
      };
    }

    /**
     * Create note from JSON
     * @param {Object} data - Note data
     * @returns {Note} Note instance
     */
    static fromJSON(data) {
      return new Note(data);
    }
  }

  /**
   * Storage manager class for handling localStorage
   */
  class StorageManager {
    constructor(key = 'stickyNotesApp') {
      this.key = key;
      this.maxRecycleBinSize = 10;
    }

    /**
     * Save all data to localStorage
     * @param {Object} data - Data to save
     */
    save(data) {
      try {
        localStorage.setItem(this.key, JSON.stringify(data));
        return true;
      } catch (error) {
        if (error.name === 'QuotaExceededError') {
          Utils.showNotification('Storage quota exceeded. Please export and delete some notes.', 'error');
        } else {
          Utils.showNotification('Failed to save data.', 'error');
        }
        return false;
      }
    }

    /**
     * Load all data from localStorage
     * @returns {Object} Loaded data
     */
    load() {
      try {
        const data = localStorage.getItem(this.key);
        return data ? JSON.parse(data) : this.getEmptyData();
      } catch (error) {
        Utils.showNotification('Failed to load data.', 'error');
        return this.getEmptyData();
      }
    }

    /**
     * Get empty data structure
     * @returns {Object} Empty data structure
     */
    getEmptyData() {
      return {
        notes: [],
        settings: {
          theme: 'light',
          lastZIndex: 10,
          nextId: 1
        },
        recycleBin: []
      };
    }

    /**
     * Export data as JSON string
     * @returns {string} JSON string
     */
    export() {
      const data = this.load();
      return JSON.stringify(data, null, 2);
    }

    /**
     * Import data from JSON string
     * @param {string} jsonString - JSON string to import
     * @param {string} mode - Import mode ('replace' or 'merge')
     * @returns {boolean} Success status
     */
    import(jsonString, mode = 'merge') {
      try {
        const importedData = JSON.parse(jsonString);

        // Validate data structure
        if (!importedData.notes || !Array.isArray(importedData.notes)) {
          throw new Error('Invalid data format');
        }

        const currentData = this.load();

        if (mode === 'replace') {
          currentData.notes = importedData.notes;
        } else {
          // Merge notes, avoiding duplicates by ID
          const existingIds = new Set(currentData.notes.map(note => note.id));
          const newNotes = importedData.notes.filter(note => !existingIds.has(note.id));
          currentData.notes.push(...newNotes);
        }

        // Update settings if available
        if (importedData.settings) {
          currentData.settings = {...currentData.settings, ...importedData.settings};
        }

        return this.save(currentData);
      } catch (error) {
        Utils.showNotification('Failed to import data: ' + error.message, 'error');
        return false;
      }
    }

    /**
     * Add note to recycle bin
     * @param {Note} note - Note to add to recycle bin
     */
    addToRecycleBin(note) {
      const data = this.load();
      data.recycleBin.unshift(note.toJSON());

      // Limit recycle bin size
      if (data.recycleBin.length > this.maxRecycleBinSize) {
        data.recycleBin = data.recycleBin.slice(0, this.maxRecycleBinSize);
      }

      this.save(data);
    }

    /**
     * Restore note from recycle bin
     * @param {string} noteId - ID of note to restore
     * @returns {Note|null} Restored note or null
     */
    restoreFromRecycleBin(noteId) {
      const data = this.load();
      const noteIndex = data.recycleBin.findIndex(note => note.id === noteId);

      if (noteIndex !== -1) {
        const note = data.recycleBin.splice(noteIndex, 1)[0];
        data.notes.push(note);
        this.save(data);
        return Note.fromJSON(note);
      }

      return null;
    }

    /**
     * Empty recycle bin
     */
    emptyRecycleBin() {
      const data = this.load();
      data.recycleBin = [];
      this.save(data);
    }
  }

  /**
   * UI Manager class for handling DOM operations
   */
  class UIManager {
    constructor(boardElement) {
      this.board = boardElement;
      this.notes = new Map(); // Map of note ID to DOM element
      this.selectedNoteId = null;
      this.isDragging = false;
      this.isResizing = false;
      this.dragOffset = {x: 0, y: 0};
      this.activeColorPicker = null;
      this.activeMenu = null;
      this.setupEventListeners();
    }

    /**
     * Setup global event listeners
     */
    setupEventListeners() {
      // Close color pickers and menus when clicking outside
      document.addEventListener('click', (e) => {
        if (this.activeColorPicker && !this.activeColorPicker.contains(e.target)) {
          this.activeColorPicker.remove();
          this.activeColorPicker = null;
        }

        if (this.activeMenu && !this.activeMenu.contains(e.target)) {
          this.activeMenu.remove();
          this.activeMenu = null;
        }
      });

      // Handle window resize
      window.addEventListener('resize', () => {
        this.adjustNotesPosition();
      });
    }

    /**
     * Render a note
     * @param {Note} note - Note to render
     * @returns {HTMLElement} Note DOM element
     */
    renderNote(note) {
      // Check if note already exists
      if (this.notes.has(note.id)) {
        return this.updateNote(note);
      }

      // Create note element
      const noteEl = document.createElement('div');
      noteEl.className = 'note';
      noteEl.id = `note-${note.id}`;
      noteEl.style.left = `${note.x}px`;
      noteEl.style.top = `${note.y}px`;
      noteEl.style.width = `${note.width}px`;
      noteEl.style.height = `${note.height}px`;
      noteEl.style.backgroundColor = note.color;
      noteEl.style.zIndex = note.zIndex;

      if (note.pinned) {
        noteEl.classList.add('pinned');
      }

      // Reminder indicator
      if (note.reminder && new Date(note.reminder) > new Date()) {
        const reminderIndicator = document.createElement('div');
        reminderIndicator.className = 'reminder-indicator';
        reminderIndicator.innerHTML = '<span class="material-symbols-outlined">alarm</span>' + new Date(note.reminder).toLocaleDateString();
        noteEl.appendChild(reminderIndicator);
      }

      // Create note header
      const header = document.createElement('div');
      header.className = 'note-header';

      // Drag handle
      const dragHandle = document.createElement('div');
      dragHandle.className = 'drag-handle';
      dragHandle.innerHTML = '<span class="material-symbols-outlined" aria-hidden="true">drag_handle</span>';
      dragHandle.setAttribute('aria-label', 'Drag to move');

      // Note actions
      const actions = document.createElement('div');
      actions.className = 'note-actions';

      // Pin button
      const pinBtn = document.createElement('button');
      pinBtn.className = 'note-action pin-btn';
      pinBtn.innerHTML = note.pinned ? '<span class="material-symbols-outlined">push_pin</span>' : '<span class="material-symbols-outlined">push_pin</span>';
      if (note.pinned) pinBtn.classList.add('active');
      pinBtn.setAttribute('aria-label', note.pinned ? 'Unpin note' : 'Pin note');

      // Markdown toggle button
      const markdownBtn = document.createElement('button');
      markdownBtn.className = 'note-action markdown-btn';
      markdownBtn.innerHTML = '<span class="material-symbols-outlined">code</span>';
      if (note.isMarkdown) markdownBtn.classList.add('active');
      markdownBtn.setAttribute('aria-label', 'Toggle markdown');

      // Color picker button
      const colorBtn = document.createElement('button');
      colorBtn.className = 'note-action color-btn';
      colorBtn.innerHTML = '<span class="material-symbols-outlined" aria-hidden="true">palette</span>';
      colorBtn.setAttribute('aria-label', 'Change color');

      // Menu button
      const menuBtn = document.createElement('button');
      menuBtn.className = 'note-action menu-btn';
      menuBtn.innerHTML = '<span class="material-symbols-outlined" aria-hidden="true">more_vert</span>';
      menuBtn.setAttribute('aria-label', 'Menu');

      actions.appendChild(pinBtn);
      actions.appendChild(markdownBtn);
      actions.appendChild(colorBtn);
      actions.appendChild(menuBtn);

      header.appendChild(dragHandle);
      header.appendChild(actions);

      // Rich text toolbar
      const toolbar = document.createElement('div');
      toolbar.className = 'rich-text-toolbar';
      toolbar.style.display = 'none';

      const toolbarButtons = [
        {cmd: 'bold', icon: 'B', title: 'Bold'},
        {cmd: 'italic', icon: 'I', title: 'Italic'},
        {cmd: 'underline', icon: 'U', title: 'Underline'},
        {cmd: 'strikeThrough', icon: 'S', title: 'Strikethrough'},
        {cmd: 'insertUnorderedList', icon: '•', title: 'Bullet List'},
        {cmd: 'insertOrderedList', icon: '1.', title: 'Numbered List'}
      ];

      toolbarButtons.forEach(btn => {
        const button = document.createElement('button');
        button.className = 'toolbar-btn';
        button.textContent = btn.icon;
        button.title = btn.title;
        button.setAttribute('data-command', btn.cmd);
        toolbar.appendChild(button);
      });

      // Create note content
      const content = document.createElement('div');
      content.className = 'note-content';
      content.contentEditable = true;

      if (note.isMarkdown && note.content) {
        content.innerHTML = marked.parse(note.content);
        content.classList.add('preview-mode');
        content.contentEditable = false;
      } else {
        content.textContent = note.content;
      }

      content.setAttribute('role', 'textbox');
      content.setAttribute('aria-label', 'Note content');

      // Create note footer
      const footer = document.createElement('div');
      footer.className = 'note-footer';

      const timestamp = document.createElement('div');
      timestamp.className = 'note-timestamp';
      timestamp.textContent = Utils.formatDate(note.updatedAt);

      const tags = document.createElement('div');
      tags.className = 'note-tags';

      // Category badge
      if (note.category && note.category !== 'Uncategorized') {
        const categoryBadge = document.createElement('span');
        categoryBadge.className = 'category-badge';
        categoryBadge.textContent = note.category;
        tags.appendChild(categoryBadge);
      }

      note.tags.forEach(tag => {
        const tagEl = document.createElement('span');
        tagEl.className = 'tag';
        tagEl.textContent = `#${tag}`;
        tags.appendChild(tagEl);
      });

      footer.appendChild(timestamp);
      footer.appendChild(tags);

      // Create resize handle
      const resizeHandle = document.createElement('div');
      resizeHandle.className = 'resize-handle';

      // Assemble note
      noteEl.appendChild(header);
      noteEl.appendChild(toolbar);
      noteEl.appendChild(content);
      noteEl.appendChild(footer);
      noteEl.appendChild(resizeHandle);

      // Add event listeners
      this.setupNoteEventListeners(noteEl, note);

      // Add to board and track
      this.board.appendChild(noteEl);
      this.notes.set(note.id, noteEl);

      return noteEl;
    }

    /**
     * Update an existing note
     * @param {Note} note - Note to update
     * @returns {HTMLElement} Updated note DOM element
     */
    updateNote(note) {
      const noteEl = this.notes.get(note.id);
      if (!noteEl) return null;

      // Update position and size
      noteEl.style.left = `${note.x}px`;
      noteEl.style.top = `${note.y}px`;
      noteEl.style.width = `${note.width}px`;
      noteEl.style.height = `${note.height}px`;
      noteEl.style.backgroundColor = note.color;
      noteEl.style.zIndex = note.zIndex;

      // Update pinned state
      if (note.pinned) {
        noteEl.classList.add('pinned');
      } else {
        noteEl.classList.remove('pinned');
      }

      // Update content
      const contentEl = noteEl.querySelector('.note-content');
      contentEl.textContent = note.content;

      // Update tags
      const tagsEl = noteEl.querySelector('.note-tags');
      tagsEl.innerHTML = '';
      note.tags.forEach(tag => {
        const tagEl = document.createElement('span');
        tagEl.className = 'tag';
        tagEl.textContent = `#${tag}`;
        tagsEl.appendChild(tagEl);
      });

      // Update timestamp
      const timestampEl = noteEl.querySelector('.note-timestamp');
      timestampEl.textContent = Utils.formatDate(note.updatedAt);

      // Update pin button
      const pinBtn = noteEl.querySelector('.pin-btn');
      pinBtn.innerHTML = '<span class="material-symbols-outlined">push_pin</span>';
      if (note.pinned) pinBtn.classList.add('active'); else pinBtn.classList.remove('active');
      pinBtn.setAttribute('aria-label', note.pinned ? 'Unpin note' : 'Pin note');

      return noteEl;
    }

    /**
     * Setup event listeners for a note
     * @param {HTMLElement} noteEl - Note DOM element
     * @param {Note} note - Note model
     */
    setupNoteEventListeners(noteEl, note) {
      const header = noteEl.querySelector('.note-header');
      const content = noteEl.querySelector('.note-content');
      const toolbar = noteEl.querySelector('.rich-text-toolbar');
      const pinBtn = noteEl.querySelector('.pin-btn');
      const markdownBtn = noteEl.querySelector('.markdown-btn');
      const colorBtn = noteEl.querySelector('.color-btn');
      const menuBtn = noteEl.querySelector('.menu-btn');
      const resizeHandle = noteEl.querySelector('.resize-handle');

      // Note selection
      noteEl.addEventListener('mousedown', (e) => {
        if (e.target === noteEl || e.target === header || e.target === header.querySelector('.drag-handle')) {
          this.selectNote(note.id);
        }
      });

      // Drag functionality
      header.addEventListener('pointerdown', (e) => {
        // If the pointerdown originated from any action button (pin, color, menu, etc.)
        // don't start dragging. Use closest() to handle clicks on inner elements
        // (icons/text nodes) inside the button rather than relying on strict
        // reference equality which can fail for nested targets.
        if (e.target.closest && e.target.closest('.note-action')) return;

        this.isDragging = true;
        this.dragOffset.x = e.clientX - note.x;
        this.dragOffset.y = e.clientY - note.y;

        const handlePointerMove = (e) => {
          if (!this.isDragging) return;

          const newX = e.clientX - this.dragOffset.x;
          const newY = e.clientY - this.dragOffset.y;

          // Keep note within viewport
          const maxX = window.innerWidth - note.width;
          const maxY = window.innerHeight - note.height - 80; // Account for header

          note.x = Math.max(0, Math.min(newX, maxX));
          note.y = Math.max(0, Math.min(newY, maxY));

          noteEl.style.left = `${note.x}px`;
          noteEl.style.top = `${note.y}px`;
        };

        const handlePointerUp = () => {
          this.isDragging = false;
          document.removeEventListener('pointermove', handlePointerMove);
          document.removeEventListener('pointerup', handlePointerUp);

          // Trigger save
          this.onNoteChange(note);
        };

        document.addEventListener('pointermove', handlePointerMove);
        document.addEventListener('pointerup', handlePointerUp);
      });

      // Resize functionality
      resizeHandle.addEventListener('pointerdown', (e) => {
        this.isResizing = true;

        const startX = e.clientX;
        const startY = e.clientY;
        const startWidth = note.width;
        const startHeight = note.height;

        const handlePointerMove = (e) => {
          if (!this.isResizing) return;

          const newWidth = startWidth + (e.clientX - startX);
          const newHeight = startHeight + (e.clientY - startY);

          // Minimum size
          note.width = Math.max(150, newWidth);
          note.height = Math.max(150, newHeight);

          noteEl.style.width = `${note.width}px`;
          noteEl.style.height = `${note.height}px`;
        };

        const handlePointerUp = () => {
          this.isResizing = false;
          document.removeEventListener('pointermove', handlePointerMove);
          document.removeEventListener('pointerup', handlePointerUp);

          // Trigger save
          this.onNoteChange(note);
        };

        document.addEventListener('pointermove', handlePointerMove);
        document.addEventListener('pointerup', handlePointerUp);
      });

      // Content change
      content.addEventListener('input', Utils.debounce(() => {
        if (note.isMarkdown) {
          note.updateContent(content.textContent);
        } else {
          note.updateContent(content.textContent);
        }
        this.updateNote(note);
        this.onNoteChange(note);
      }, 500));

      // Show/hide toolbar on focus
      content.addEventListener('focus', () => {
        if (!note.isMarkdown) {
          toolbar.style.display = 'flex';
        }
      });

      content.addEventListener('blur', () => {
        setTimeout(() => {
          if (!toolbar.contains(document.activeElement)) {
            toolbar.style.display = 'none';
          }
        }, 200);
      });

      // Toolbar buttons
      toolbar.querySelectorAll('.toolbar-btn').forEach(btn => {
        btn.addEventListener('mousedown', (e) => {
          e.preventDefault();
          const command = btn.getAttribute('data-command');
          document.execCommand(command, false, null);
          content.focus();
        });
      });

      // Pin toggle
      pinBtn.addEventListener('click', () => {
        note.pinned = !note.pinned;
        this.updateNote(note);
        this.onNoteChange(note);
      });

      // Markdown toggle
      markdownBtn.addEventListener('click', () => {
        note.isMarkdown = !note.isMarkdown;

        if (note.isMarkdown) {
          // Switch to markdown preview mode
          const plainText = content.textContent;
          note.content = plainText;
          content.innerHTML = marked.parse(plainText);
          content.classList.add('preview-mode');
          content.contentEditable = false;
          toolbar.style.display = 'none';
          markdownBtn.classList.add('active');
        } else {
          // Switch back to edit mode
          const plainText = note.content;
          content.textContent = plainText;
          content.classList.remove('preview-mode');
          content.contentEditable = true;
          markdownBtn.classList.remove('active');
        }

        this.onNoteChange(note);
      });

      // Color picker (render as floating element attached to body to avoid clipping)
      colorBtn.addEventListener('click', (e) => {
        e.stopPropagation();

        // Close existing color picker
        if (this.activeColorPicker) {
          this.activeColorPicker.remove();
          this.activeColorPicker = null;
        }

        // Create color picker and append to body
        const colorPicker = this.createColorPicker(note);
        document.body.appendChild(colorPicker);
        this.positionFloatingElement(colorPicker, colorBtn);
        colorPicker.style.display = 'flex';
        this.activeColorPicker = colorPicker;

        // Prevent the header drag from starting when clicking inside the picker
        colorPicker.addEventListener('pointerdown', (ev) => ev.stopPropagation());
      });

      // Menu (render as floating element attached to body to avoid clipping)
      menuBtn.addEventListener('click', (e) => {
        e.stopPropagation();

        // Close existing menu
        if (this.activeMenu) {
          this.activeMenu.remove();
          this.activeMenu = null;
        }

        // Create menu and append to body
        const menu = this.createNoteMenu(note);
        document.body.appendChild(menu);
        this.positionFloatingElement(menu, menuBtn);
        menu.style.display = 'block';
        this.activeMenu = menu;

        // Prevent the header drag from starting when clicking inside the menu
        menu.addEventListener('pointerdown', (ev) => ev.stopPropagation());
      });
    }

    /**
     * Create color picker for a note
     * @param {Note} note - Note to create color picker for
     * @returns {HTMLElement} Color picker element
     */
    createColorPicker(note) {
      const colorPicker = document.createElement('div');
      colorPicker.className = 'color-picker';

      const colors = ['#fff9a8', '#ffebcc', '#d4f0f0', '#ffd6d6', '#e6ccff', '#ccffcc'];

      colors.forEach(color => {
        const colorOption = document.createElement('div');
        colorOption.className = 'color-option';
        colorOption.style.backgroundColor = color;
        colorOption.setAttribute('aria-label', `Select color ${color}`);

        colorOption.addEventListener('click', () => {
          note.color = color;
          this.updateNote(note);
          this.onNoteChange(note);
          colorPicker.style.display = 'none';
          this.activeColorPicker = null;
        });

        colorPicker.appendChild(colorOption);
      });

      return colorPicker;
    }

    /**
     * Position a floating element (appended to body) next to a reference element
     * and keep it inside the viewport.
     * @param {HTMLElement} floatingEl - Element appended to body
     * @param {HTMLElement} referenceEl - Element to position next to
     */
    positionFloatingElement(floatingEl, referenceEl) {
      const refRect = referenceEl.getBoundingClientRect();
      const floatRect = floatingEl.getBoundingClientRect();

      // Default position: below and aligned to right of reference
      let top = refRect.bottom + window.scrollY + 4; // small gap
      let left = refRect.right + window.scrollX - floatRect.width;

      // If it goes off the right edge, align to the left of the reference
      if (left + floatRect.width > window.innerWidth + window.scrollX) {
        left = refRect.left + window.scrollX;
      }

      // If it goes off the bottom edge, position above reference
      if (top + floatRect.height > window.innerHeight + window.scrollY) {
        top = refRect.top + window.scrollY - floatRect.height - 4;
      }

      // Clamp to viewport
      top = Math.max(window.scrollY + 4, Math.min(top, window.scrollY + window.innerHeight - floatRect.height - 4));
      left = Math.max(window.scrollX + 4, Math.min(left, window.scrollX + window.innerWidth - floatRect.width - 4));

      floatingEl.style.position = 'absolute';
      floatingEl.style.top = `${top}px`;
      floatingEl.style.left = `${left}px`;
      floatingEl.style.zIndex = 2000;
    }

    /**
     * Create menu for a note
     * @param {Note} note - Note to create menu for
     * @returns {HTMLElement} Menu element
     */
    createNoteMenu(note) {
      const menu = document.createElement('div');
      menu.className = 'note-menu';

      // Set reminder option
      const reminderOption = document.createElement('div');
      reminderOption.className = 'menu-item';
      reminderOption.textContent = 'Set Reminder';
      reminderOption.addEventListener('click', () => {
        this.onSetReminder(note);
        menu.style.display = 'none';
        this.activeMenu = null;
      });

      // Set category option
      const categoryOption = document.createElement('div');
      categoryOption.className = 'menu-item';
      categoryOption.textContent = 'Set Category';
      categoryOption.addEventListener('click', () => {
        this.onSetCategory(note);
        menu.style.display = 'none';
        this.activeMenu = null;
      });

      // Duplicate option
      const duplicateOption = document.createElement('div');
      duplicateOption.className = 'menu-item';
      duplicateOption.textContent = 'Duplicate';
      duplicateOption.addEventListener('click', () => {
        this.onNoteDuplicate(note);
        menu.style.display = 'none';
        this.activeMenu = null;
      });

      // Delete option
      const deleteOption = document.createElement('div');
      deleteOption.className = 'menu-item';
      deleteOption.textContent = 'Delete';
      deleteOption.addEventListener('click', () => {
        this.onNoteDelete(note);
        menu.style.display = 'none';
        this.activeMenu = null;
      });

      // Export option
      const exportOption = document.createElement('div');
      exportOption.className = 'menu-item';
      exportOption.textContent = 'Export';
      exportOption.addEventListener('click', () => {
        this.onNoteExport(note);
        menu.style.display = 'none';
        this.activeMenu = null;
      });

      menu.appendChild(reminderOption);
      menu.appendChild(categoryOption);
      menu.appendChild(duplicateOption);
      menu.appendChild(deleteOption);
      menu.appendChild(exportOption);

      return menu;
    }

    /**
     * Select a note
     * @param {string} noteId - ID of note to select
     */
    selectNote(noteId) {
      // Deselect previous note
      if (this.selectedNoteId) {
        const prevNoteEl = this.notes.get(this.selectedNoteId);
        if (prevNoteEl) {
          prevNoteEl.classList.remove('selected');
        }
      }

      // Select new note
      this.selectedNoteId = noteId;
      const noteEl = this.notes.get(noteId);
      if (noteEl) {
        noteEl.classList.add('selected');
        this.onNoteSelect(noteId);
      }
    }

    /**
     * Remove a note from the DOM
     * @param {string} noteId - ID of note to remove
     */
    removeNote(noteId) {
      const noteEl = this.notes.get(noteId);
      if (noteEl) {
        this.board.removeChild(noteEl);
        this.notes.delete(noteId);

        if (this.selectedNoteId === noteId) {
          this.selectedNoteId = null;
        }
      }
    }

    /**
     * Clear all notes from the DOM
     */
    clearAllNotes() {
      this.notes.forEach(noteEl => {
        this.board.removeChild(noteEl);
      });
      this.notes.clear();
      this.selectedNoteId = null;
    }

    /**
     * Filter notes based on search query
     * @param {string} query - Search query
     */
    filterNotes(query) {
      const lowerQuery = query.toLowerCase();

      this.notes.forEach((noteEl, noteId) => {
        const note = this.onGetNote(noteId);
        if (!note) return;

        const matchesContent = note.content.toLowerCase().includes(lowerQuery);
        const matchesTags = note.tags.some(tag => tag.toLowerCase().includes(lowerQuery));

        if (matchesContent || matchesTags) {
          noteEl.style.display = 'flex';
        } else {
          noteEl.style.display = 'none';
        }
      });
    }

    /**
     * Show all notes
     */
    showAllNotes() {
      this.notes.forEach(noteEl => {
        noteEl.style.display = 'flex';
      });
    }

    /**
     * Adjust notes position to fit within viewport
     */
    adjustNotesPosition() {
      this.notes.forEach((noteEl, noteId) => {
        const note = this.onGetNote(noteId);
        if (!note) return;

        const maxX = window.innerWidth - note.width;
        const maxY = window.innerHeight - note.height - 80; // Account for header

        if (note.x > maxX) {
          note.x = maxX;
          noteEl.style.left = `${note.x}px`;
        }

        if (note.y > maxY) {
          note.y = maxY;
          noteEl.style.top = `${note.y}px`;
        }
      });
    }

    /**
     * Set theme
     * @param {string} theme - Theme name ('light' or 'dark')
     */
    setTheme(theme) {
      document.documentElement.setAttribute('data-theme', theme);
    }

    /**
     * Show recycle bin modal
     * @param {Array} deletedNotes - Array of deleted notes
     */
    showRecycleBin(deletedNotes) {
      const modal = document.getElementById('recycle-bin-modal');
      const content = document.getElementById('recycle-bin-content');

      content.innerHTML = '';

      if (deletedNotes.length === 0) {
        content.innerHTML = '<div class="empty-state show"><div class="empty-state-icon">🗑️</div><p>Recycle bin is empty</p></div>';
      } else {
        deletedNotes.forEach(noteData => {
          const note = Note.fromJSON(noteData);
          const noteEl = document.createElement('div');
          noteEl.className = 'deleted-note';

          const noteContent = document.createElement('div');
          noteContent.className = 'deleted-note-content';
          noteContent.textContent = note.content.substring(0, 100) + (note.content.length > 100 ? '...' : '');

          const noteActions = document.createElement('div');
          noteActions.className = 'deleted-note-actions';

          const restoreBtn = document.createElement('button');
          restoreBtn.className = 'btn btn-primary';
          restoreBtn.textContent = 'Restore';
          restoreBtn.addEventListener('click', () => {
            this.onRestoreNote(note.id);
          });

          noteActions.appendChild(restoreBtn);
          noteEl.appendChild(noteContent);
          noteEl.appendChild(noteActions);
          content.appendChild(noteEl);
        });
      }

      modal.classList.add('active');
      modal.setAttribute('aria-hidden', 'false');
    }

    /**
     * Hide recycle bin modal
     */
    hideRecycleBin() {
      const modal = document.getElementById('recycle-bin-modal');
      modal.classList.remove('active');
      modal.setAttribute('aria-hidden', 'true');
    }

    /**
     * Show help modal
     */
    showHelp() {
      const modal = document.getElementById('help-modal');
      modal.classList.add('active');
      modal.setAttribute('aria-hidden', 'false');
    }

    /**
     * Hide help modal
     */
    hideHelp() {
      const modal = document.getElementById('help-modal');
      modal.classList.remove('active');
      modal.setAttribute('aria-hidden', 'true');
    }

    /**
     * Callback for when a note changes
     * @param {Note} note - Changed note
     */
    onNoteChange(note) {
      // This will be overridden by the AppController
    }

    /**
     * Callback for when a note is selected
     * @param {string} noteId - Selected note ID
     */
    onNoteSelect(noteId) {
      // This will be overridden by the AppController
    }

    /**
     * Callback for when a note is deleted
     * @param {Note} note - Deleted note
     */
    onNoteDelete(note) {
      // This will be overridden by the AppController
    }

    /**
     * Callback for when a note is duplicated
     * @param {Note} note - Note to duplicate
     */
    onNoteDuplicate(note) {
      // This will be overridden by the AppController
    }

    /**
     * Callback for when a note is exported
     * @param {Note} note - Note to export
     */
    onNoteExport(note) {
      // This will be overridden by the AppController
    }

    /**
     * Callback for when a note is restored
     * @param {string} noteId - ID of note to restore
     */
    onRestoreNote(noteId) {
      // This will be overridden by the AppController
    }

    /**
     * Callback for getting a note by ID
     * @param {string} noteId - ID of note to get
     * @returns {Note|null} Note or null
     */
    onGetNote(noteId) {
      // This will be overridden by the AppController
      return null;
    }

    /**
     * Callback for setting reminder
     * @param {Note} note - Note to set reminder for
     */
    onSetReminder(note) {
      // This will be overridden by the AppController
    }

    /**
     * Callback for setting category
     * @param {Note} note - Note to set category for
     */
    onSetCategory(note) {
      // This will be overridden by the AppController
    }

    /**
     * Render Kanban board
     * @param {Array} notes - Array of notes
     */
    renderKanban(notes) {
      const todoColumn = document.getElementById('kanban-todo');
      const inProgressColumn = document.getElementById('kanban-in-progress');
      const doneColumn = document.getElementById('kanban-done');

      // Clear columns
      todoColumn.innerHTML = '';
      inProgressColumn.innerHTML = '';
      doneColumn.innerHTML = '';

      // Count notes in each column
      let todoCount = 0;
      let inProgressCount = 0;
      let doneCount = 0;

      notes.forEach(note => {
        const card = this.createKanbanCard(note);

        if (note.kanbanStatus === 'todo') {
          todoColumn.appendChild(card);
          todoCount++;
        } else if (note.kanbanStatus === 'in-progress') {
          inProgressColumn.appendChild(card);
          inProgressCount++;
        } else if (note.kanbanStatus === 'done') {
          doneColumn.appendChild(card);
          doneCount++;
        }
      });

      // Update counts
      document.querySelector('[data-status="todo"] .kanban-count').textContent = todoCount;
      document.querySelector('[data-status="in-progress"] .kanban-count').textContent = inProgressCount;
      document.querySelector('[data-status="done"] .kanban-count').textContent = doneCount;
    }

    /**
     * Create Kanban card
     * @param {Note} note - Note to create card for
     * @returns {HTMLElement} Kanban card element
     */
    createKanbanCard(note) {
      const card = document.createElement('div');
      card.className = 'kanban-card';
      card.setAttribute('data-note-id', note.id);
      card.style.borderLeftColor = note.color;
      card.draggable = true;

      const header = document.createElement('div');
      header.className = 'kanban-card-header';

      if (note.category && note.category !== 'Uncategorized') {
        const categoryBadge = document.createElement('span');
        categoryBadge.className = 'category-badge';
        categoryBadge.textContent = note.category;
        header.appendChild(categoryBadge);
      }

      const content = document.createElement('div');
      content.className = 'kanban-card-content';

      if (note.isMarkdown) {
        content.innerHTML = marked.parse(note.content.substring(0, 200));
      } else {
        content.textContent = note.content.substring(0, 200) + (note.content.length > 200 ? '...' : '');
      }

      const footer = document.createElement('div');
      footer.className = 'kanban-card-footer';

      const timestamp = document.createElement('span');
      timestamp.textContent = Utils.formatDate(note.updatedAt);

      const tags = document.createElement('div');
      tags.className = 'note-tags';
      note.tags.slice(0, 2).forEach(tag => {
        const tagEl = document.createElement('span');
        tagEl.className = 'tag';
        tagEl.textContent = `#${tag}`;
        tags.appendChild(tagEl);
      });

      footer.appendChild(timestamp);
      footer.appendChild(tags);

      card.appendChild(header);
      card.appendChild(content);
      card.appendChild(footer);

      // Drag events
      card.addEventListener('dragstart', (e) => {
        card.classList.add('dragging');
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/plain', note.id);
      });

      card.addEventListener('dragend', () => {
        card.classList.remove('dragging');
      });

      // Click to edit
      card.addEventListener('click', () => {
        this.onKanbanCardClick(note.id);
      });

      return card;
    }

    /**
     * Callback for Kanban card click
     * @param {string} noteId - ID of note clicked
     */
    onKanbanCardClick(noteId) {
      // This will be overridden by the AppController
    }
  }

  /**
   * App Controller class to tie everything together
   */
  class AppController {
    constructor() {
      this.storage = new StorageManager();
      this.ui = new UIManager(document.getElementById('board'));
      this.notes = new Map(); // Map of note ID to Note model
      this.settings = {};
      this.recycleBin = [];
      this.lastZIndex = 10;
      this.currentView = 'sticky'; // 'sticky' or 'kanban'
      this.categories = new Set(['Uncategorized']);
      this.templates = this.getTemplates();

      this.setupUICallbacks();
      this.setupGlobalEventListeners();
      this.loadData();
      this.updateCategoryFilter();
      this.checkReminders();
    }

    /**
     * Setup UI callbacks
     */
    setupUICallbacks() {
      // Note change callback
      this.ui.onNoteChange = (note) => {
        this.saveNote(note);
      };

      // Note select callback
      this.ui.onNoteSelect = (noteId) => {
        this.bringNoteToFront(noteId);
      };

      // Note delete callback
      this.ui.onNoteDelete = (note) => {
        this.deleteNote(note.id);
      };

      // Note duplicate callback
      this.ui.onNoteDuplicate = (note) => {
        this.duplicateNote(note.id);
      };

      // Note export callback
      this.ui.onNoteExport = (note) => {
        this.exportSingleNote(note);
      };

      // Note restore callback
      this.ui.onRestoreNote = (noteId) => {
        this.restoreNote(noteId);
      };

      // Get note callback
      this.ui.onGetNote = (noteId) => {
        return this.notes.get(noteId);
      };

      // Set reminder callback
      this.ui.onSetReminder = (note) => {
        this.setReminder(note);
      };

      // Set category callback
      this.ui.onSetCategory = (note) => {
        this.setCategory(note);
      };

      // Kanban card click callback
      this.ui.onKanbanCardClick = (noteId) => {
        this.switchToStickyView();
        setTimeout(() => {
          this.ui.selectNote(noteId);
          const noteEl = this.ui.notes.get(noteId);
          if (noteEl) {
            noteEl.scrollIntoView({behavior: 'smooth', block: 'center'});
          }
        }, 100);
      };
    }

    /**
     * Setup global event listeners
     */
    setupGlobalEventListeners() {
      // New note button
      document.getElementById('new-note-btn').addEventListener('click', () => {
        this.createNote();
      });

      // Search input
      document.getElementById('search-input').addEventListener('input', (e) => {
        const query = e.target.value.trim();
        if (query) {
          this.ui.filterNotes(query);
        } else {
          this.ui.showAllNotes();
        }
      });

      // Export button
      document.getElementById('export-btn').addEventListener('click', () => {
        this.exportNotes();
      });

      // Import button
      document.getElementById('import-btn').addEventListener('click', () => {
        document.getElementById('import-file').click();
      });

      // Import file input
      document.getElementById('import-file').addEventListener('change', (e) => {
        const file = e.target.files[0];
        if (file) {
          const reader = new FileReader();
          reader.onload = (event) => {
            const json = event.target.result;
            this.importNotes(json);
          };
          reader.readAsText(file);
        }
        // Reset input
        e.target.value = '';
      });

      // Theme toggle
      document.getElementById('theme-toggle').addEventListener('click', () => {
        this.toggleTheme();
      });

      // Recycle bin button
      document.getElementById('recycle-bin-btn').addEventListener('click', () => {
        this.ui.showRecycleBin(this.recycleBin);
      });

      // Close recycle bin
      document.getElementById('close-recycle-bin').addEventListener('click', () => {
        this.ui.hideRecycleBin();
      });

      // Empty recycle bin
      document.getElementById('empty-recycle-bin').addEventListener('click', () => {
        this.emptyRecycleBin();
      });

      // Help button
      document.getElementById('help-btn').addEventListener('click', () => {
        this.ui.showHelp();
      });

      // Close help
      document.getElementById('close-help').addEventListener('click', () => {
        this.ui.hideHelp();
      });

      // Template button
      document.getElementById('template-btn').addEventListener('click', () => {
        this.showTemplateModal();
      });

      // Close template modal
      document.getElementById('close-template').addEventListener('click', () => {
        this.hideTemplateModal();
      });

      // Template selection
      document.querySelectorAll('.template-card').forEach(card => {
        card.addEventListener('click', () => {
          const templateType = card.getAttribute('data-template');
          this.createNoteFromTemplate(templateType);
          this.hideTemplateModal();
        });
      });

      // View toggle
      document.getElementById('view-toggle').addEventListener('click', () => {
        this.toggleView();
      });

      // Category filter
      document.getElementById('category-filter').addEventListener('change', (e) => {
        const category = e.target.value;
        this.filterByCategory(category);
      });

      // Kanban drag and drop
      document.querySelectorAll('.kanban-content').forEach(column => {
        column.addEventListener('dragover', (e) => {
          e.preventDefault();
          e.dataTransfer.dropEffect = 'move';
        });

        column.addEventListener('drop', (e) => {
          e.preventDefault();
          const noteId = e.dataTransfer.getData('text/plain');
          const newStatus = column.parentElement.getAttribute('data-status');
          this.updateKanbanStatus(noteId, newStatus);
        });
      });

      // Keyboard shortcuts
      document.addEventListener('keydown', (e) => {
        // N - new note
        if (e.key === 'n' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
          // Only create new note if not typing in a note
          const activeElement = document.activeElement;
          if (!activeElement || !activeElement.classList.contains('note-content')) {
            e.preventDefault();
            this.createNote();
          }
        }

        // Ctrl/Cmd + S - export
        if ((e.ctrlKey || e.metaKey) && e.key === 's') {
          e.preventDefault();
          this.exportNotes();
        }

        // Delete - delete selected note
        if (e.key === 'Delete' && this.ui.selectedNoteId) {
          this.deleteNote(this.ui.selectedNoteId);
        }

        // Ctrl/Cmd + Z - undo last delete
        if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
          e.preventDefault();
          this.undoLastDelete();
        }

        // Esc - close modals
        if (e.key === 'Escape') {
          this.ui.hideRecycleBin();
          this.ui.hideHelp();
        }
      });

      // Storage events for cross-tab sync
      window.addEventListener('storage', (e) => {
        if (e.key === this.storage.key) {
          this.loadData();
        }
      });
    }

    /**
     * Load data from storage
     */
    loadData() {
      const data = this.storage.load();

      // Load settings
      this.settings = data.settings || {
        theme: 'light',
        lastZIndex: 10,
        nextId: 1
      };

      this.lastZIndex = this.settings.lastZIndex || 10;

      // Apply theme
      this.ui.setTheme(this.settings.theme || 'light');

      // Update theme button
      const themeBtn = document.getElementById('theme-toggle');
      themeBtn.innerHTML = this.settings.theme === 'dark' ? '<span class="material-symbols-outlined">light_mode</span>' : '<span class="material-symbols-outlined">dark_mode</span>';

      const emptyState = document.getElementById('empty-state');

      // Load notes
      this.notes.clear();
      this.ui.clearAllNotes();
      this.categories.clear();
      this.categories.add('Uncategorized');

      if (data.notes && data.notes.length > 0) {
        data.notes.forEach(noteData => {
          const note = Note.fromJSON(noteData);
          this.notes.set(note.id, note);

          // Collect categories
          if (note.category) {
            this.categories.add(note.category);
          }

          if (this.currentView === 'sticky') {
            this.ui.renderNote(note);
          }
        });
        emptyState.style.display = 'none';
      } else {
        emptyState.style.display = 'flex';
      }

      // Render Kanban if in Kanban view
      if (this.currentView === 'kanban') {
        this.ui.renderKanban(Array.from(this.notes.values()));
      }

      // Load recycle bin
      this.recycleBin = data.recycleBin || [];

      // Update category filter
      this.updateCategoryFilter();
    }

    /**
     * Save data to storage
     */
    saveData() {
      const notes = Array.from(this.notes.values()).map(note => note.toJSON());

      const data = {
        notes: notes,
        settings: {
          ...this.settings,
          lastZIndex: this.lastZIndex
        },
        recycleBin: this.recycleBin
      };

      this.storage.save(data);
    }

    /**
     * Save a note
     * @param {Note} note - Note to save
     */
    saveNote(note) {
      this.notes.set(note.id, note);
      this.saveData();
    }

    /**
     * Create a new note
     * @param {number} x - X position
     * @param {number} y - Y position
     * @returns {Note} Created note
     */
    createNote(x = null, y = null) {
      const note = new Note({
        x: x || 20 + Math.random() * 200,
        y: y || 20 + Math.random() * 200,
        zIndex: ++this.lastZIndex
      });

      this.notes.set(note.id, note);
      this.ui.renderNote(note);
      this.saveData();

      // Focus on the new note
      setTimeout(() => {
        const noteEl = this.ui.notes.get(note.id);
        if (noteEl) {
          const contentEl = noteEl.querySelector('.note-content');
          if (contentEl) {
            contentEl.focus();
          }
        }
      }, 100);

      return note;
    }

    /**
     * Delete a note
     * @param {string} noteId - ID of note to delete
     */
    deleteNote(noteId) {
      const note = this.notes.get(noteId);
      if (!note) return;

      // Add to recycle bin
      this.storage.addToRecycleBin(note);
      this.recycleBin = this.storage.load().recycleBin;

      // Remove from UI and model
      this.ui.removeNote(noteId);
      this.notes.delete(noteId);
      this.saveData();

      Utils.showNotification('Note deleted. You can restore it from the recycle bin.', 'success');
    }

    /**
     * Duplicate a note
     * @param {string} noteId - ID of note to duplicate
     * @returns {Note|null} Duplicated note or null
     */
    duplicateNote(noteId) {
      const originalNote = this.notes.get(noteId);
      if (!originalNote) return null;

      const duplicatedNote = new Note({
        content: originalNote.content,
        x: originalNote.x + 20,
        y: originalNote.y + 20,
        width: originalNote.width,
        height: originalNote.height,
        color: originalNote.color,
        zIndex: ++this.lastZIndex,
        pinned: originalNote.pinned,
        reminder: originalNote.reminder
      });

      this.notes.set(duplicatedNote.id, duplicatedNote);
      this.ui.renderNote(duplicatedNote);
      this.saveData();

      Utils.showNotification('Note duplicated.', 'success');
      return duplicatedNote;
    }

    /**
     * Export a single note
     * @param {Note} note - Note to export
     */
    exportSingleNote(note) {
      const data = {
        notes: [note.toJSON()],
        settings: this.settings,
        recycleBin: []
      };

      const json = JSON.stringify(data, null, 2);
      const blob = new Blob([json], {type: 'application/json'});
      const url = URL.createObjectURL(blob);

      const a = document.createElement('a');
      a.href = url;
      a.download = `sticky-note-${note.id}.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);

      URL.revokeObjectURL(url);
      Utils.showNotification('Note exported.', 'success');
    }

    /**
     * Export all notes
     */
    exportNotes() {
      const json = this.storage.export();
      const blob = new Blob([json], {type: 'application/json'});
      const url = URL.createObjectURL(blob);

      const a = document.createElement('a');
      a.href = url;
      a.download = `sticky-notes-${new Date().toISOString().split('T')[0]}.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);

      URL.revokeObjectURL(url);
      Utils.showNotification('Notes exported.', 'success');
    }

    /**
     * Import notes
     * @param {string} json - JSON string to import
     */
    importNotes(json) {
      // Ask user for import mode
      const mode = confirm('Replace all notes or merge with existing notes?\n\nOK = Replace, Cancel = Merge') ? 'replace' : 'merge';

      if (this.storage.import(json, mode)) {
        this.loadData();
        Utils.showNotification(`Notes imported (${mode} mode).`, 'success');
      }
    }

    /**
     * Restore a note from recycle bin
     * @param {string} noteId - ID of note to restore
     */
    restoreNote(noteId) {
      const note = this.storage.restoreFromRecycleBin(noteId);
      if (note) {
        this.notes.set(note.id, note);
        this.ui.renderNote(note);
        this.saveData();
        this.loadData(); // Refresh recycle bin

        Utils.showNotification('Note restored.', 'success');
      }
    }

    /**
     * Empty recycle bin
     */
    emptyRecycleBin() {
      if (confirm('Are you sure you want to empty the recycle bin? This action cannot be undone.')) {
        this.storage.emptyRecycleBin();
        this.recycleBin = [];
        this.ui.showRecycleBin([]);
        Utils.showNotification('Recycle bin emptied.', 'success');
      }
    }

    /**
     * Undo last delete
     */
    undoLastDelete() {
      if (this.recycleBin.length > 0) {
        const lastDeleted = this.recycleBin[0];
        this.restoreNote(lastDeleted.id);
      } else {
        Utils.showNotification('Nothing to undo.', 'info');
      }
    }

    /**
     * Bring a note to front
     * @param {string} noteId - ID of note to bring to front
     */
    bringNoteToFront(noteId) {
      const note = this.notes.get(noteId);
      if (!note) return;

      note.zIndex = ++this.lastZIndex;
      this.ui.updateNote(note);
      this.saveData();
    }

    /**
     * Toggle theme
     */
    toggleTheme() {
      const currentTheme = this.settings.theme || 'light';
      const newTheme = currentTheme === 'light' ? 'dark' : 'light';

      this.settings.theme = newTheme;
      this.ui.setTheme(newTheme);
      this.saveData();

      // Update theme button
      const themeBtn = document.getElementById('theme-toggle');
      themeBtn.innerHTML = newTheme === 'dark' ? '<span class="material-symbols-outlined">light_mode</span>' : '<span class="material-symbols-outlined">dark_mode</span>';
    }



    /**
     * Get templates
     * @returns {Object} Templates object
     */
    getTemplates() {
      return {
        todo: {
          content: '# To-Do List\n\n- [ ] Task 1\n- [ ] Task 2\n- [ ] Task 3\n\n## Priority\n- [ ] High priority task\n\n## Notes\nAdd any additional notes here...',
          category: 'Tasks',
          color: '#d4f0f0',
          isMarkdown: true
        },
        meeting: {
          content: '# Meeting Notes\n\n**Date:** ' + new Date().toLocaleDateString() + '\n**Attendees:** \n\n## Agenda\n1. \n2. \n3. \n\n## Discussion\n\n## Action Items\n- [ ] \n- [ ] ',
          category: 'Meetings',
          color: '#ffebcc',
          isMarkdown: true
        },
        idea: {
          content: '# 💡 Idea\n\n## Concept\n\n## Why?\n\n## Next Steps\n- \n- ',
          category: 'Ideas',
          color: '#fff9a8',
          isMarkdown: true
        },
        goals: {
          content: '# 🎯 Goals\n\n## Short-term\n- [ ] \n- [ ] \n\n## Long-term\n- [ ] \n- [ ] \n\n## Progress\n',
          category: 'Goals',
          color: '#e6ccff',
          isMarkdown: true
        },
        shopping: {
          content: '# 🛒 Shopping List\n\n## Groceries\n- [ ] \n- [ ] \n\n## Other\n- [ ] \n- [ ] ',
          category: 'Shopping',
          color: '#ccffcc',
          isMarkdown: true
        },
        blank: {
          content: '',
          category: 'Uncategorized',
          color: '#fff9a8',
          isMarkdown: false
        }
      };
    }

    /**
     * Create note from template
     * @param {string} templateType - Type of template
     */
    createNoteFromTemplate(templateType) {
      const template = this.templates[templateType];
      if (!template) return;

      const note = new Note({
        content: template.content,
        color: template.color,
        category: template.category,
        isMarkdown: template.isMarkdown,
        zIndex: ++this.lastZIndex
      });

      this.notes.set(note.id, note);
      this.categories.add(template.category);

      if (this.currentView === 'sticky') {
        this.ui.renderNote(note);
      }

      this.saveData();
      this.updateCategoryFilter();

      Utils.showNotification('Note created from template!', 'success');
    }

    /**
     * Show template modal
     */
    showTemplateModal() {
      const modal = document.getElementById('template-modal');
      modal.classList.add('active');
      modal.setAttribute('aria-hidden', 'false');
    }

    /**
     * Hide template modal
     */
    hideTemplateModal() {
      const modal = document.getElementById('template-modal');
      modal.classList.remove('active');
      modal.setAttribute('aria-hidden', 'true');
    }

    /**
     * Set reminder for a note
     * @param {Note} note - Note to set reminder for
     */
    setReminder(note) {
      const dateStr = prompt('Set reminder date and time (YYYY-MM-DD HH:MM):',
        new Date(Date.now() + 86400000).toISOString().slice(0, 16).replace('T', ' '));

      if (dateStr) {
        try {
          const reminderDate = new Date(dateStr);
          if (reminderDate > new Date()) {
            note.reminder = reminderDate.toISOString();
            this.ui.updateNote(note);
            this.saveData();
            Utils.showNotification('Reminder set!', 'success');
          } else {
            Utils.showNotification('Please set a future date.', 'error');
          }
        } catch (e) {
          Utils.showNotification('Invalid date format.', 'error');
        }
      }
    }

    /**
     * Set category for a note
     * @param {Note} note - Note to set category for
     */
    setCategory(note) {
      const categoriesList = Array.from(this.categories).join(', ');
      const category = prompt(`Set category for this note:\n\nExisting categories: ${categoriesList}`, note.category || 'Uncategorized');

      if (category !== null && category.trim()) {
        note.category = category.trim();
        this.categories.add(category.trim());
        this.ui.updateNote(note);
        this.saveData();
        this.updateCategoryFilter();
        Utils.showNotification('Category updated!', 'success');
      }
    }

    /**
     * Update category filter dropdown
     */
    updateCategoryFilter() {
      const select = document.getElementById('category-filter');
      const currentValue = select.value;

      select.innerHTML = '<option value="">All Categories</option>';

      Array.from(this.categories).sort().forEach(category => {
        const option = document.createElement('option');
        option.value = category;
        option.textContent = category;
        select.appendChild(option);
      });

      select.value = currentValue;
    }

    /**
     * Filter notes by category
     * @param {string} category - Category to filter by
     */
    filterByCategory(category) {
      if (!category) {
        this.ui.showAllNotes();
        return;
      }

      this.ui.notes.forEach((noteEl, noteId) => {
        const note = this.notes.get(noteId);
        if (note && note.category === category) {
          noteEl.style.display = 'flex';
        } else {
          noteEl.style.display = 'none';
        }
      });
    }

    /**
     * Toggle between sticky and kanban view
     */
    toggleView() {
      const stickyBoard = document.getElementById('board');
      const kanbanBoard = document.getElementById('kanban-board');
      const viewToggleBtn = document.getElementById('view-toggle');

      if (this.currentView === 'sticky') {
        // Switch to Kanban
        this.currentView = 'kanban';
        stickyBoard.style.display = 'none';
        kanbanBoard.style.display = 'flex';
        viewToggleBtn.innerHTML = '<span class="material-symbols-outlined">dashboard</span> Sticky';
        this.ui.renderKanban(Array.from(this.notes.values()));
      } else {
        // Switch to Sticky
        this.switchToStickyView();
      }
    }

    /**
     * Switch to sticky view
     */
    switchToStickyView() {
      const stickyBoard = document.getElementById('board');
      const kanbanBoard = document.getElementById('kanban-board');
      const viewToggleBtn = document.getElementById('view-toggle');

      this.currentView = 'sticky';
      stickyBoard.style.display = 'block';
      kanbanBoard.style.display = 'none';
      viewToggleBtn.innerHTML = '<span class="material-symbols-outlined">view_kanban</span> Kanban';

      // Re-render all notes
      this.ui.clearAllNotes();
      this.notes.forEach(note => {
        this.ui.renderNote(note);
      });
    }

    /**
     * Update Kanban status for a note
     * @param {string} noteId - ID of note
     * @param {string} newStatus - New status
     */
    updateKanbanStatus(noteId, newStatus) {
      const note = this.notes.get(noteId);
      if (note) {
        note.kanbanStatus = newStatus;
        this.saveData();
        this.ui.renderKanban(Array.from(this.notes.values()));
        Utils.showNotification('Status updated!', 'success');
      }
    }

    /**
     * Check reminders and show notifications
     */
    checkReminders() {
      setInterval(() => {
        const now = new Date();
        this.notes.forEach(note => {
          if (note.reminder) {
            const reminderDate = new Date(note.reminder);
            const diff = reminderDate - now;

            // Show notification 5 minutes before
            if (diff > 0 && diff < 300000 && !note._reminderShown) {
              if ('Notification' in window && Notification.permission === 'granted') {
                new Notification('Reminder: Sticky Note', {
                  body: note.content.substring(0, 100),
                  icon: 'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📝</text></svg>'
                });
              }
              Utils.showNotification('Reminder: ' + note.content.substring(0, 50), 'info');
              note._reminderShown = true;
            }
          }
        });
      }, 60000); // Check every minute

      // Request notification permission
      if ('Notification' in window && Notification.permission === 'default') {
        Notification.requestPermission();
      }
    }
  }

  // Initialize app when DOM is ready
  document.addEventListener('DOMContentLoaded', () => {
    const app = new AppController();

    // Register service worker if available
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('sw.js')
        .then(registration => {
          console.log('Service Worker registered with scope:', registration.scope);
        })
        .catch(error => {
          console.log('Service Worker registration failed:', error);
        });
    }
  });

  // Return public API
  return {
    Note,
    StorageManager,
    UIManager,
    AppController,
    Utils
  };
})();