+
-
Version 2.0
+
Version 2.0
Compy 2.0 is a lightweight, offline‑first clipboard and snippet manager for the web. Save reusable text (commands, credentials, templates), organize with tags and search, and copy instantly — all stored locally with optional JSON/CSV import & export.
-
-
Bheb Developer 2025
+
+
© 2025 Bheb Developer
-
+
+
-
+
+
+
+
+
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..35e43fa
--- /dev/null
+++ b/js/app.js
@@ -0,0 +1,1258 @@
+/**
+ * Compy 2.0 - Main Application Module
+ * Enhanced with better code organization, error handling, and modern JavaScript practices
+ */
+
+import { STORAGE_KEYS, UI_CONFIG, ICONS, DEFAULT_THEME } from './constants.js';
+import {
+ $, $$, escapeHtml, highlightText, stringHash, downloadFile,
+ parseCSVLine, csvEscape, formatDate, focusElement,
+ getAllTags, filterItems, validateItem, debounce
+} from './utils.js';
+import {
+ initState, getState, subscribe, upsertItem,
+ deleteItem, updateFilterTags, updateSearch, updateProfile,
+ setEditingId, getBackups
+} from './state.js';
+
+/**
+ * @typedef {Object} AppItem
+ * @property {string} id
+ * @property {string} text
+ * @property {string} desc
+ * @property {boolean} sensitive
+ * @property {string[]} tags
+ */
+/**
+ * @typedef {Object} AppState
+ * @property {AppItem[]} items
+ * @property {string[]} filterTags
+ * @property {string} search
+ * @property {string|null} editingId
+ * @property {string} profileName
+ */
+/**
+ * @typedef {Object} EmptyStateOptions
+ * @property {boolean} hasSearch
+ * @property {boolean} hasFilters
+ */
+
+/**
+ * Main Application Class
+ *
+ * Orchestrates UI initialization, state subscriptions, event handlers, and import/export flows
+ * for the Compy application. This class does not own application data; it delegates persistence
+ * to the state module and reads configuration from constants.
+ *
+ * External dependencies:
+ * - Web Clipboard API (navigator.clipboard) with an execCommand fallback for older browsers
+ * - localStorage (via state and theme helpers) for persistence
+ * - requestAnimationFrame for smooth rendering
+ */
+class CompyApp {
+ /**
+ * Construct a new CompyApp instance.
+ * Binds handlers to maintain context when used as event listeners.
+ */
+ constructor() {
+ this.initialized = false;
+ this.clipboard = null;
+ this.notifications = null;
+ this.modals = null;
+ this.theme = null;
+ this.search = null;
+ this.cards = null;
+
+ // Bind methods to maintain context
+ this.handleStateChange = this.handleStateChange.bind(this);
+ this.handleKeyboardShortcuts = this.handleKeyboardShortcuts.bind(this);
+ this.handleModalKeyboard = this.handleModalKeyboard.bind(this);
+ }
+
+ /**
+ * Initialize the application UI and services.
+ *
+ * Responsibilities:
+ * - Load state and theme from storage
+ * - Initialize core components (clipboard, notifications, modals, search, cards, profile)
+ * - Subscribe to state changes and wire global keyboard handlers
+ * - Measure responsive navbar height
+ *
+ * Errors are surfaced to the user via a non-blocking notification.
+ * @returns {Promise
}
+ */
+ async init() {
+ if (this.initialized) return;
+
+ try {
+ // Initialize state management
+ initState();
+
+ // Initialize components
+ this.initClipboard();
+ this.initNotifications();
+ this.initModals();
+ this.initTheme();
+ this.initSearch();
+ this.initCards();
+ this.initProfile();
+ this.initExport();
+ this.initImport();
+ this.initEventHandlers();
+
+ // Subscribe to state changes
+ subscribe(this.handleStateChange);
+
+ // Setup keyboard shortcuts
+ document.addEventListener('keydown', this.handleKeyboardShortcuts);
+ document.addEventListener('keydown', this.handleModalKeyboard);
+
+ // Setup responsive navbar
+ this.setupResponsiveNavbar();
+
+ this.initialized = true;
+ console.log('Compy 2.0 initialized successfully');
+
+ } catch (error) {
+ console.error('Failed to initialize Compy 2.0:', error);
+ this.showNotification('Failed to initialize application', 'error');
+ }
+ }
+
+ /**
+ * React to state changes by updating dependent UI regions.
+ * @param {Object} state - Immutable snapshot of the current application state
+ */
+ handleStateChange(state) {
+ this.renderCards(state);
+ this.renderProfile(state);
+ this.renderFilterBadge(state);
+ this.updateSearchInput(state);
+ }
+
+ /**
+ * Initialize clipboard functionality with fallback support
+ *
+ * Creates a clipboard management object with intelligent fallback handling.
+ * The modern Clipboard API is preferred for better security and user experience,
+ * but falls back to execCommand for broader browser compatibility.
+ *
+ * Clipboard Security:
+ * - Modern browsers require secure context (HTTPS or localhost)
+ * - User interaction is required for clipboard access
+ * - Some browsers require explicit permissions
+ *
+ * Performance Optimization:
+ * - Checks API availability once during initialization
+ * - Avoids repeated feature detection on each copy operation
+ */
+ initClipboard() {
+ // Pre-check clipboard API availability for performance
+ const hasClipboardAPI = navigator.clipboard &&
+ typeof navigator.clipboard.writeText === 'function';
+
+ this.clipboard = {
+ copy: async (text) => {
+ // Input validation - prevent empty or invalid copy operations
+ if (!text || typeof text !== 'string') {
+ console.warn('Clipboard copy attempted with invalid text:', text);
+ return false;
+ }
+
+ try {
+ if (hasClipboardAPI) {
+ // Use modern Clipboard API for better security and UX
+ await navigator.clipboard.writeText(text);
+ this.showNotification('Copied to clipboard', 'success');
+ return true;
+ } else {
+ // Fall back to legacy method for older browsers
+ return this.fallbackCopy(text);
+ }
+ } catch (error) {
+ // Modern API failed - try fallback as last resort
+ console.warn('Modern clipboard API failed, trying fallback:', error);
+ return this.fallbackCopy(text);
+ }
+ }
+ };
+ }
+
+ /**
+ * Fallback clipboard copy method using execCommand
+ */
+ fallbackCopy(text) {
+ const textarea = document.createElement('textarea');
+ textarea.value = text;
+ textarea.style.position = 'fixed';
+ textarea.style.left = '-9999px';
+ document.body.appendChild(textarea);
+ textarea.select();
+
+ const success = document.execCommand('copy');
+ document.body.removeChild(textarea);
+
+ this.showNotification(
+ success ? 'Copied to clipboard' : 'Copy failed - please try manually',
+ success ? 'info' : 'error'
+ );
+ }
+
+ /**
+ * Initialize ephemeral notification system (snackbar).
+ * Uses UI_CONFIG.snackbarDuration for auto-dismiss timing.
+ */
+ initNotifications() {
+ const snackbar = $('#snackbar');
+
+ this.notifications = {
+ show: (message, type = 'info', duration = UI_CONFIG.snackbarDuration) => {
+ snackbar.textContent = message;
+ snackbar.className = `snackbar ${type}`;
+ snackbar.classList.add('show');
+
+ setTimeout(() => {
+ snackbar.classList.remove('show');
+ }, duration);
+ }
+ };
+ }
+
+ /**
+ * Show a transient snackbar message.
+ * @param {string} message - Message to display
+ * @param {'info'|'error'} [type='info'] - Visual style of the snackbar
+ */
+ showNotification(message, type = 'info') {
+ this.notifications.show(message, type);
+ }
+
+ /**
+ * Initialize modal helpers and close-button behaviors.
+ * Relies on [data-close-modal] attributes inside .modal elements.
+ */
+ initModals() {
+ this.modals = {
+ open: (selector) => {
+ const modal = $(selector);
+ if (modal) {
+ modal.setAttribute('aria-hidden', 'false');
+ // Focus first focusable element or close button
+ const focusTarget = modal.querySelector('[data-close-modal]') ||
+ modal.querySelector('input, textarea, button');
+ focusElement(focusTarget, 100);
+ }
+ },
+
+ close: (selector) => {
+ const modal = $(selector);
+ if (modal) {
+ modal.setAttribute('aria-hidden', 'true');
+ }
+ }
+ };
+
+ // Setup close handlers for all modals
+ $$('[data-close-modal]').forEach(button => {
+ button.addEventListener('click', (e) => {
+ const modal = e.target.closest('.modal');
+ if (modal) {
+ this.modals.close(`#${modal.id}`);
+ }
+ });
+ });
+ }
+
+ /**
+ * Initialize theme switching and persistence.
+ * Persists user choice in localStorage and applies a short CSS transition class
+ * to avoid abrupt theme changes.
+ */
+ initTheme() {
+ const themeSelect = $('#themeSelect');
+
+ this.theme = {
+ apply: (themeName) => {
+ document.documentElement.setAttribute('data-theme', themeName);
+ localStorage.setItem(STORAGE_KEYS.theme, themeName);
+ themeSelect.value = themeName;
+
+ // Add transition class for smooth theme switching
+ document.documentElement.classList.add('theme-switching');
+ setTimeout(() => {
+ document.documentElement.classList.remove('theme-switching');
+ }, 300);
+ },
+
+ load: () => {
+ const savedTheme = localStorage.getItem(STORAGE_KEYS.theme) || DEFAULT_THEME;
+ this.theme.apply(savedTheme);
+ }
+ };
+
+ // Load saved theme
+ this.theme.load();
+
+ // Handle theme changes
+ themeSelect.addEventListener('change', (e) => {
+ this.theme.apply(e.target.value);
+ });
+ }
+
+ /**
+ * Initialize search input, clear button, and debounced state updates.
+ */
+ initSearch() {
+ const searchInput = $('#searchInput');
+ const searchClear = $('#searchClear');
+
+ this.search = {
+ focus: () => {
+ searchInput.focus();
+ },
+
+ clear: () => {
+ searchInput.value = '';
+ updateSearch('');
+ }
+ };
+
+ // Handle search input
+ searchInput.addEventListener('input', debounce((e) => {
+ updateSearch(e.target.value);
+ }, 150));
+
+ // Handle clear button
+ searchClear.addEventListener('click', this.search.clear);
+ }
+
+ /**
+ * Keep the search input value in sync with state without causing extra input events.
+ * @param {Object} state - Current application state
+ */
+ updateSearchInput(state) {
+ const searchInput = $('#searchInput');
+ if (searchInput.value !== state.search) {
+ searchInput.value = state.search;
+ }
+ }
+
+ /**
+ * Initialize card rendering helpers and the Add button handler.
+ */
+ initCards() {
+ const cardsContainer = $('#cards');
+
+ this.cards = {
+ render: (items, search = '') => {
+ this.renderCards({ items, search });
+ },
+
+ showSkeleton: () => {
+ cardsContainer.innerHTML = '';
+ const skeletonCount = Math.min(UI_CONFIG.skeletonCount, 6);
+
+ for (let i = 0; i < skeletonCount; i++) {
+ const skeleton = document.createElement('div');
+ skeleton.className = 'skel-card skeleton';
+ skeleton.setAttribute('aria-hidden', 'true');
+ cardsContainer.appendChild(skeleton);
+ }
+ }
+ };
+
+ // Handle add button
+ $('#addBtn').addEventListener('click', () => this.openItemModal());
+ }
+
+ /**
+ * Render the visible list of cards from state.
+ * Uses requestAnimationFrame to batch DOM work for smooth updates.
+ * @param {Object} state - Current application state
+ */
+ renderCards(state) {
+ const container = $('#cards');
+ const filteredItems = filterItems(state.items, state.search, state.filterTags);
+
+ // Use animation frame for smooth rendering
+ requestAnimationFrame(() => {
+ container.innerHTML = '';
+
+ // Handle empty states
+ if (state.items.length === 0) {
+ this.renderEmptyState(container, 'welcome');
+ return;
+ }
+
+ if (filteredItems.length === 0) {
+ this.renderEmptyState(container, 'no-results', {
+ hasSearch: !!state.search?.trim(),
+ hasFilters: state.filterTags.length > 0
+ });
+ return;
+ }
+
+ // Remove empty state class
+ container.classList.remove('empty-state');
+
+ // Render cards
+ filteredItems.forEach(item => {
+ container.appendChild(this.createCardElement(item, state.search));
+ });
+ });
+ }
+
+ /**
+ * Create a DOM element representing a single item card.
+ * Respects the 'sensitive' flag by masking the title.
+ * @param {Object} item - Item with text, desc, sensitive, tags
+ * @param {string} [searchQuery=''] - Current search query for highlighting
+ * @returns {HTMLElement}
+ */
+ createCardElement(item, searchQuery = '') {
+ const card = document.createElement('article');
+ card.className = 'card';
+ card.tabIndex = 0;
+
+ const displayText = item.sensitive ? '••••••••••' : escapeHtml(item.text);
+ const highlightedText = highlightText(displayText, searchQuery);
+ const highlightedDesc = highlightText(escapeHtml(item.desc), searchQuery);
+
+ card.innerHTML = `
+
+
+ ${ICONS.edit}
+
+
+ ${ICONS.delete}
+
+
+ ${ICONS.copy}
+
+
+ ${highlightedText}
+ ${highlightedDesc}
+ ${this.renderTags(item.tags, searchQuery)}
+ `;
+
+ // Setup event handlers
+ this.setupCardEventHandlers(card, item);
+
+ return card;
+ }
+
+ /**
+ * Wire click/keyboard handlers for a card's interactions.
+ * Click on card copies content unless an action button was clicked.
+ * @param {HTMLElement} card - Card element
+ * @param {Object} item - Item backing the card
+ */
+ setupCardEventHandlers(card, item) {
+ // Click to copy (but not on action buttons)
+ card.addEventListener('click', (e) => {
+ if (!e.target.closest('.actions')) {
+ this.clipboard.copy(item.text);
+ }
+ });
+
+ // Keyboard support
+ card.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ this.clipboard.copy(item.text);
+ }
+ });
+
+ // Action buttons
+ card.querySelector('[data-act="edit"]').addEventListener('click', () =>
+ this.openItemModal(item.id)
+ );
+
+ card.querySelector('[data-act="delete"]').addEventListener('click', () =>
+ this.removeItem(item.id)
+ );
+
+ card.querySelector('[data-act="copy"]').addEventListener('click', () =>
+ this.clipboard.copy(item.text)
+ );
+ }
+
+ /**
+ * Render tag chips for a card with deterministic hues and optional highlighting.
+ * Limits visible chips to UI_CONFIG.maxVisibleTags and shows a '+N more' affordance.
+ * @param {string[]} [tags=[]]
+ * @param {string} [searchQuery='']
+ * @returns {string} HTML string
+ */
+ renderTags(tags = [], searchQuery = '') {
+ const maxVisible = UI_CONFIG.maxVisibleTags;
+ const visibleTags = tags.slice(0, maxVisible);
+ const extraCount = tags.length - visibleTags.length;
+
+ let html = visibleTags.map(tag => {
+ const highlighted = highlightText(escapeHtml(tag), searchQuery);
+ const hue = Math.abs(stringHash(tag)) % 360;
+ return `${highlighted} `;
+ }).join('');
+
+ if (extraCount > 0) {
+ html += `+${extraCount} more `;
+ }
+
+ return html;
+ }
+
+ /**
+ * Render contextual empty state UI (welcome or no-results) into container.
+ * @param {HTMLElement} container - Target container
+ * @param {'welcome'|'no-results'} type - Empty state variant
+ * @param {Object} [options]
+ */
+ renderEmptyState(container, type, options = {}) {
+ container.classList.add('empty-state');
+
+ let content = '';
+
+ switch (type) {
+ case 'welcome':
+ content = this.getWelcomeEmptyState();
+ break;
+ case 'no-results':
+ content = this.getNoResultsEmptyState(options);
+ break;
+ }
+
+ container.innerHTML = content;
+ this.setupEmptyStateHandlers();
+ }
+
+ /**
+ * Generate HTML for the initial welcome empty state.
+ * @returns {string}
+ */
+ getWelcomeEmptyState() {
+ return `
+
+
+
📋
+
Welcome to Compy 2.0
+
Your personal clipboard for commands, snippets, credentials, and frequently used text.
+
+ Add your first snippet
+ Import JSON/CSV
+
+
+
+ Search fast with Ctrl+F or /
+ Organize with tags and filters
+ Personalize with themes and your profile name
+
+
+
+ `;
+ }
+
+ /**
+ * Generate HTML for the 'no results' empty state.
+ * @param {{hasSearch: boolean, hasFilters: boolean}} param0 - Flags indicating current UI filters
+ * @returns {string}
+ */
+ getNoResultsEmptyState({ hasSearch, hasFilters }) {
+ let details = '';
+ if (hasSearch && hasFilters) {
+ details = 'No items match your search and selected filters.';
+ } else if (hasSearch) {
+ details = 'No items match your search.';
+ } else if (hasFilters) {
+ details = 'No items match the selected filters.';
+ }
+
+ const searchButton = hasSearch ?
+ 'Clear search ' : '';
+ const filtersButton = hasFilters ?
+ 'Clear filters ' : '';
+
+ return `
+
+
+
🔎
+
No results found
+
${details}
+
+ ${searchButton}
+ ${filtersButton}
+
+
+
+ `;
+ }
+
+ /**
+ * Attach event handlers for buttons rendered inside empty state UIs.
+ */
+ setupEmptyStateHandlers() {
+ $('#emptyAddBtn')?.addEventListener('click', () => this.openItemModal());
+ $('#emptyImportBtn')?.addEventListener('click', () => $('#importFile').click());
+ $('#clearSearchBtn')?.addEventListener('click', () => {
+ this.search.clear();
+ });
+ $('#clearFiltersBtn')?.addEventListener('click', () => {
+ updateFilterTags([]);
+ });
+ }
+
+ /**
+ * Open the item modal for adding a new item or editing an existing one.
+ * @param {string|null} [itemId=null] - ID of the item to edit; null for a new item
+ */
+ openItemModal(itemId = null) {
+ setEditingId(itemId);
+ const state = getState();
+ const item = itemId ? state.items.find(i => i.id === itemId) : {
+ text: '',
+ desc: '',
+ sensitive: false,
+ tags: []
+ };
+
+ // Update modal title
+ $('#itemModalTitle').textContent = itemId ? 'Edit Snippet' : 'Add Snippet';
+
+ // Populate form
+ $('#itemText').value = item.text;
+ $('#itemDesc').value = item.desc;
+ $('#itemSensitive').checked = item.sensitive;
+ this.setTagChips(item.tags);
+
+ this.modals.open('#itemModal');
+ focusElement($('#itemText'), 100);
+ }
+
+ /**
+ * Delete an item by ID and notify the user.
+ * @param {string} itemId
+ */
+ removeItem(itemId) {
+ deleteItem(itemId);
+ this.showNotification('Snippet deleted');
+ }
+
+ /**
+ * Initialize profile editing modal and related event handlers.
+ */
+ initProfile() {
+ $('#profileEditBtn').addEventListener('click', () => {
+ const state = getState();
+ $('#profileNameInput').value = state.profileName;
+ this.modals.open('#profileModal');
+ focusElement($('#profileNameInput'), 100);
+ });
+
+ $('#profileSaveBtn').addEventListener('click', () => {
+ const name = $('#profileNameInput').value.trim();
+ updateProfile(name);
+ this.modals.close('#profileModal');
+ this.showNotification('Profile updated');
+ });
+
+ // Handle Enter key in profile input
+ $('#profileNameInput').addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ $('#profileSaveBtn').click();
+ }
+ });
+ }
+
+ /**
+ * Render the profile name indicator next to the app title.
+ * @param {Object} state
+ */
+ renderProfile(state) {
+ const display = $('#profileDisplay');
+ display.textContent = state.profileName ?
+ `· ${state.profileName}'s Compy` : '';
+ }
+
+ /**
+ * Update the filter count badge visibility and text.
+ * @param {Object} state
+ */
+ renderFilterBadge(state) {
+ const badge = $('#filterBadge');
+ const count = state.filterTags.length;
+
+ if (count > 0) {
+ badge.textContent = count;
+ badge.hidden = false;
+ } else {
+ badge.hidden = true;
+ }
+ }
+
+ /**
+ * Initialize export menu interactions (JSON, CSV, backups).
+ */
+ initExport() {
+ // Export menu handling
+ const exportBtn = $('#exportMenuBtn');
+ const exportMenu = $('#exportMenu');
+
+ exportBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ exportMenu.classList.toggle('open');
+ });
+
+ document.addEventListener('click', (e) => {
+ if (!e.target.closest('#exportMenu')) {
+ exportMenu.classList.remove('open');
+ }
+ });
+
+ // Export handlers
+ exportMenu.addEventListener('click', (e) => {
+ const button = e.target.closest('button');
+ if (!button) return;
+
+ exportMenu.classList.remove('open');
+ const exportType = button.dataset.export;
+
+ if (exportType === 'json') {
+ this.exportJSON();
+ } else if (exportType === 'csv') {
+ this.exportCSV();
+ } else if (button.id === 'backupsBtn') {
+ this.openBackupsModal();
+ }
+ });
+ }
+
+ /**
+ * Export the current state as a JSON file.
+ * Uses a helper to trigger a safe, temporary download link.
+ */
+ exportJSON() {
+ const state = getState();
+ const payload = {
+ profileName: state.profileName || '',
+ items: state.items
+ };
+
+ downloadFile(
+ 'compy-export.json',
+ JSON.stringify(payload, null, 2),
+ 'application/json'
+ );
+
+ this.showNotification('JSON export downloaded');
+ }
+
+ /**
+ * Export the current state as a CSV file.
+ * Includes an optional metadata section for profileName.
+ */
+ exportCSV() {
+ const state = getState();
+ const rows = [
+ ['profileName'],
+ [csvEscape(state.profileName || '')],
+ [''],
+ ['text', 'desc', 'sensitive', 'tags'],
+ ...state.items.map(item => [
+ csvEscape(item.text),
+ csvEscape(item.desc),
+ item.sensitive ? '1' : '0',
+ csvEscape(item.tags.join('|'))
+ ])
+ ];
+
+ const csv = rows.map(row => row.join(',')).join('\n');
+
+ downloadFile('compy-export.csv', csv, 'text/csv');
+ this.showNotification('CSV export downloaded');
+ }
+
+ /**
+ * Initialize file import handling for JSON and CSV formats.
+ */
+ initImport() {
+ const importFile = $('#importFile');
+
+ importFile.addEventListener('change', async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ try {
+ const text = await file.text();
+
+ if (file.name.endsWith('.json')) {
+ this.importJSON(text);
+ } else if (file.name.endsWith('.csv')) {
+ this.importCSV(text);
+ } else {
+ this.showNotification('Unsupported file format', 'error');
+ }
+ } catch (error) {
+ console.error('Import failed:', error);
+ this.showNotification('Import failed', 'error');
+ } finally {
+ importFile.value = ''; // Clear file input
+ }
+ });
+ }
+
+ /**
+ * Import items from a JSON payload.
+ * Accepts both legacy array-only exports and the newer object format containing { items, profileName }.
+ * @param {string} jsonText - Raw JSON string
+ */
+ importJSON(jsonText) {
+ try {
+ const parsed = JSON.parse(jsonText);
+ let items = [];
+
+ if (Array.isArray(parsed)) {
+ // Legacy format: array of items
+ items = parsed;
+ } else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.items)) {
+ // New format with profile
+ items = parsed.items;
+
+ if (typeof parsed.profileName === 'string' && parsed.profileName.trim()) {
+ updateProfile(parsed.profileName.trim());
+ }
+ } else {
+ throw new Error('Invalid JSON format');
+ }
+
+ let importCount = 0;
+ for (const item of items) {
+ if (this.addImportedItem(item)) {
+ importCount++;
+ }
+ }
+
+ this.showNotification(`Imported ${importCount} items`);
+
+ } catch (error) {
+ console.error('JSON import failed:', error);
+ this.showNotification('Invalid JSON file', 'error');
+ }
+ }
+
+ /**
+ * Import items from a CSV payload.
+ * Supports an optional two-line metadata block with a single 'profileName' column.
+ * Robustly parses quoted fields and BOM.
+ * @param {string} csvText - Raw CSV string
+ */
+ importCSV(csvText) {
+ try {
+ const lines = csvText.split(/\r?\n/).filter(line => line.trim());
+ if (!lines.length) {
+ throw new Error('Empty CSV file');
+ }
+
+ // Parse first line (could be profile metadata or headers)
+ const firstLine = parseCSVLine(lines[0].replace(/^\uFEFF/, '')); // Remove BOM
+ let headerIndex = 0;
+
+ // Check for profile metadata
+ if (firstLine.length === 1 && firstLine[0].toLowerCase() === 'profilename') {
+ const profileLine = lines[1];
+ if (profileLine) {
+ const profileData = parseCSVLine(profileLine);
+ const profileName = (profileData[0] || '').trim();
+ if (profileName) {
+ updateProfile(profileName);
+ }
+ }
+ headerIndex = 2; // Skip to actual headers
+ }
+
+ // Parse headers
+ const headerLine = lines[headerIndex];
+ if (!headerLine) throw new Error('No headers found');
+
+ const headers = parseCSVLine(headerLine).map(h => h.toLowerCase().trim());
+ const textIndex = headers.indexOf('text');
+ const descIndex = headers.indexOf('desc');
+ const sensitiveIndex = headers.indexOf('sensitive');
+ const tagsIndex = headers.indexOf('tags');
+
+ if (textIndex === -1 || descIndex === -1) {
+ throw new Error('Required columns missing (text, desc)');
+ }
+
+ let importCount = 0;
+ for (let i = headerIndex + 1; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (!line) continue;
+
+ const values = parseCSVLine(line);
+ const item = {
+ text: (values[textIndex] || '').trim(),
+ desc: (values[descIndex] || '').trim(),
+ sensitive: sensitiveIndex >= 0 ?
+ ['1', 'true'].includes((values[sensitiveIndex] || '').toLowerCase()) : false,
+ tags: tagsIndex >= 0 ?
+ (values[tagsIndex] || '').split('|').map(t => t.trim()).filter(Boolean) : []
+ };
+
+ if (this.addImportedItem(item)) {
+ importCount++;
+ }
+ }
+
+ this.showNotification(`Imported ${importCount} items`);
+
+ } catch (error) {
+ console.error('CSV import failed:', error);
+ this.showNotification('Invalid CSV file', 'error');
+ }
+ }
+
+ /**
+ * Validate and insert an imported item into state.
+ * @param {Object} itemData - Candidate item
+ * @returns {boolean} True if item was accepted
+ */
+ addImportedItem(itemData) {
+ const validation = validateItem(itemData);
+ if (!validation.isValid) {
+ console.warn('Skipping invalid item:', validation.errors);
+ return false;
+ }
+
+ upsertItem({
+ text: itemData.text,
+ desc: itemData.desc,
+ sensitive: !!itemData.sensitive,
+ tags: Array.isArray(itemData.tags) ? itemData.tags : []
+ });
+
+ return true;
+ }
+
+
+ /**
+ * Open the backups modal listing auto-saved snapshots with download actions.
+ */
+ openBackupsModal() {
+ const backups = getBackups();
+ const list = $('#backupsList');
+ list.innerHTML = '';
+
+ if (backups.length === 0) {
+ list.innerHTML = 'No backups available
';
+ } else {
+ backups.forEach(backup => {
+ const button = document.createElement('button');
+ const date = formatDate(backup.ts);
+ button.textContent = `${date} (${backup.items.length} items)`;
+ button.addEventListener('click', () => {
+ const filename = `compy-backup-${backup.ts.replace(/[:.]/g, '-')}.json`;
+ downloadFile(filename, JSON.stringify(backup.items, null, 2), 'application/json');
+ });
+ list.appendChild(button);
+ });
+ }
+
+ this.modals.open('#backupsModal');
+ }
+
+ /**
+ * Register global UI event handlers for header actions, forms, tags, and overlays.
+ */
+ initEventHandlers() {
+ // Brand click - refresh page
+ $('#brand').addEventListener('click', () => location.reload());
+
+ // About button
+ $('#aboutBtn').addEventListener('click', () => this.modals.open('#aboutModal'));
+
+ // Filter button
+ $('#filterBtn').addEventListener('click', () => this.openFilterModal());
+
+ // Item form submission
+ $('#saveItemBtn').addEventListener('click', () => this.saveItem());
+ $('#itemForm').addEventListener('submit', (e) => {
+ e.preventDefault();
+ this.saveItem();
+ });
+
+ // Clear field buttons
+ $$('[data-clear]').forEach(button => {
+ button.addEventListener('click', () => {
+ const target = $(button.getAttribute('data-clear'));
+ if (target) {
+ target.value = '';
+ target.focus();
+ }
+ });
+ });
+
+ // Tag input handling
+ this.initTagInput();
+
+ // More tags modal
+ document.addEventListener('click', (e) => {
+ const moreButton = e.target.closest('[data-more-tags]');
+ if (moreButton) {
+ this.showMoreTags(moreButton);
+ }
+ });
+ }
+
+ /**
+ * Initialize tag entry behaviors (Enter to add, Backspace to remove last).
+ */
+ initTagInput() {
+ const tagEntry = $('#tagEntry');
+
+ tagEntry.addEventListener('keydown', (e) => {
+ const value = e.target.value.trim();
+
+ if (e.key === 'Enter' && value) {
+ e.preventDefault();
+ this.addTagChip(value);
+ e.target.value = '';
+ } else if (e.key === 'Backspace' && !e.target.value) {
+ // Remove last tag chip
+ const chips = $$('#tagChips .chip');
+ const lastChip = chips[chips.length - 1];
+ lastChip?.remove();
+ }
+ });
+ }
+
+ /**
+ * Render a set of tag chips into the edit form.
+ * @param {string[]} tags
+ */
+ setTagChips(tags) {
+ const container = $('#tagChips');
+ container.innerHTML = '';
+ tags.forEach(tag => this.addTagChip(tag));
+ }
+
+ /**
+ * Append a single tag chip if it is non-empty and not a duplicate.
+ * @param {string} tagText
+ */
+ addTagChip(tagText) {
+ const normalizedTag = tagText.trim();
+ if (!normalizedTag) return;
+
+ // Check for duplicates
+ const existing = $$('#tagChips .chip').some(chip =>
+ chip.dataset.value === normalizedTag
+ );
+ if (existing) return;
+
+ const chip = document.createElement('span');
+ chip.className = 'chip';
+ chip.dataset.value = normalizedTag;
+
+ const hue = Math.abs(stringHash(normalizedTag)) % 360;
+ chip.style.setProperty('--hue', hue);
+
+ chip.innerHTML = `
+ ${escapeHtml(normalizedTag)}
+ ×
+ `;
+
+ // Handle removal
+ chip.querySelector('.x').addEventListener('click', () => {
+ chip.remove();
+ });
+
+ $('#tagChips').appendChild(chip);
+ }
+
+ /**
+ * Collect tag values from the currently rendered chips.
+ * @returns {string[]}
+ */
+ getTagsFromChips() {
+ return $$('#tagChips .chip').map(chip => chip.dataset.value);
+ }
+
+ /**
+ * Validate and persist the item currently in the edit form.
+ * Shows a notification on success or the first validation error.
+ */
+ saveItem() {
+ const text = $('#itemText').value.trim();
+ const desc = $('#itemDesc').value.trim();
+ const sensitive = $('#itemSensitive').checked;
+ const tags = this.getTagsFromChips();
+
+ const validation = validateItem({ text, desc });
+ if (!validation.isValid) {
+ this.showNotification(validation.errors[0], 'error');
+ return;
+ }
+
+ upsertItem({ text, desc, sensitive, tags });
+ this.modals.close('#itemModal');
+ this.showNotification('Snippet saved');
+ }
+
+ /**
+ * Open the filter modal populated with the deduplicated tag list.
+ */
+ openFilterModal() {
+ const state = getState();
+ this.renderFilterList(getAllTags(state.items), state.filterTags);
+ this.modals.open('#filterModal');
+ focusElement($('#filterTagSearch'), 100);
+ }
+
+ /**
+ * Render the filterable tag checklist inside the modal.
+ * @param {string[]} allTags - All tags across items (unique, sorted)
+ * @param {string[]} selectedTags - Currently selected filter tags
+ * @param {string} [searchQuery=''] - Filter query for the list itself
+ */
+ renderFilterList(allTags, selectedTags, searchQuery = '') {
+ const list = $('#filterTagList');
+ list.innerHTML = '';
+
+ const filteredTags = searchQuery ?
+ allTags.filter(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) :
+ allTags;
+
+ if (filteredTags.length === 0) {
+ const message = allTags.length === 0 ?
+ 'No tags yet. Add tags to items to filter by them.' :
+ `No tags match "${escapeHtml(searchQuery)}".`;
+ list.innerHTML = `${message}
`;
+ return;
+ }
+
+ filteredTags.forEach(tag => {
+ const id = `filter-tag-${tag}`;
+ const isSelected = selectedTags.includes(tag);
+
+ const label = document.createElement('label');
+ label.className = 'list-row';
+ label.htmlFor = id;
+
+ label.innerHTML = `
+
+ ${escapeHtml(tag)}
+ `;
+
+ list.appendChild(label);
+ });
+ }
+
+ /**
+ * Open a modal to display all tags for a specific card when '+N more' is clicked.
+ * @param {HTMLElement} moreButton - The '+N more' button element inside a card
+ */
+ showMoreTags(moreButton) {
+ const card = moreButton.closest('.card');
+ const cards = Array.from($('#cards').children);
+ const cardIndex = cards.indexOf(card);
+ const state = getState();
+ const filteredItems = filterItems(state.items, state.search, state.filterTags);
+ const item = filteredItems[cardIndex];
+
+ if (!item) return;
+
+ const list = $('#allTagsList');
+ list.innerHTML = '';
+
+ item.tags.forEach(tag => {
+ const chip = document.createElement('span');
+ chip.className = 'chip';
+ const hue = Math.abs(stringHash(tag)) % 360;
+ chip.style.setProperty('--hue', hue);
+ chip.textContent = tag;
+ list.appendChild(chip);
+ });
+
+ this.modals.open('#moreTagsModal');
+ }
+
+ /**
+ * Global keyboard shortcuts for search and new-item creation.
+ * @param {KeyboardEvent} e
+ */
+ handleKeyboardShortcuts(e) {
+ // Search shortcuts
+ if ((e.ctrlKey && e.key.toLowerCase() === 'f') || e.key === '/') {
+ e.preventDefault();
+ this.search.focus();
+ return;
+ }
+
+ // Add item shortcut
+ if (e.ctrlKey && e.key.toLowerCase() === 'n') {
+ e.preventDefault();
+ this.openItemModal();
+ return;
+ }
+ }
+
+ /**
+ * Close any open modal on Escape to align with common accessibility patterns.
+ * @param {KeyboardEvent} e
+ */
+ handleModalKeyboard(e) {
+ if (e.key === 'Escape') {
+ // Close any open modal
+ const openModal = $('[aria-hidden="false"].modal');
+ if (openModal) {
+ this.modals.close(`#${openModal.id}`);
+ }
+ }
+ }
+
+ /**
+ * Measure the navbar height and expose it as a CSS custom property (--nav-h).
+ * Keeps layout spacing correct across resizes.
+ */
+ setupResponsiveNavbar() {
+ const adjustHeight = () => {
+ const navbar = $('.navbar');
+ if (!navbar) return;
+
+ const height = navbar.getBoundingClientRect().height;
+ document.documentElement.style.setProperty('--nav-h', `${height}px`);
+ };
+
+ // Adjust on load and resize
+ adjustHeight();
+ window.addEventListener('resize', adjustHeight);
+ window.addEventListener('load', adjustHeight);
+ }
+
+}
+
+// Initialize and export the app
+let appInstance = null;
+
+/**
+ * Initialize the Compy application singleton and return the instance.
+ * @returns {Promise} Resolved app instance
+ */
+export const initializeApp = async () => {
+ if (appInstance) return appInstance;
+
+ appInstance = new CompyApp();
+ await appInstance.init();
+ return appInstance;
+};
+
+/**
+ * Get the current app instance if initialized.
+ * @returns {CompyApp|null}
+ */
+export const getApp = () => appInstance;
diff --git a/js/components/clipboard.js b/js/components/clipboard.js
new file mode 100644
index 0000000..4a50181
--- /dev/null
+++ b/js/components/clipboard.js
@@ -0,0 +1,229 @@
+/**
+ * Clipboard Management Component for Compy 2.0
+ *
+ * This module provides a robust clipboard management system that handles
+ * copying text to the system clipboard with fallback support for older browsers.
+ * It encapsulates all clipboard-related functionality in a single, reusable component.
+ *
+ * Features:
+ * - Modern Clipboard API with execCommand fallback
+ * - Automatic error handling and user feedback
+ * - Cross-browser compatibility
+ * - Integration with notification system
+ *
+ * @fileoverview Clipboard management component with fallback support
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+/**
+ * ClipboardManager handles all clipboard operations with robust fallback support
+ *
+ * This class provides a unified interface for copying text to the clipboard,
+ * automatically falling back to older methods when the modern Clipboard API
+ * is unavailable. It integrates with the notification system to provide
+ * user feedback on clipboard operations.
+ *
+ * @class ClipboardManager
+ * @example
+ * const clipboardManager = new ClipboardManager(notificationManager);
+ *
+ * // Copy text to clipboard
+ * await clipboardManager.copy('Hello, World!');
+ *
+ * // The manager will automatically show success/error notifications
+ */
+export class ClipboardManager {
+ /**
+ * Initialize the clipboard manager
+ *
+ * @param {Object} notificationManager - Notification manager instance for user feedback
+ */
+ constructor(notificationManager) {
+ this.notifications = notificationManager;
+
+ // Check clipboard API availability
+ this.hasClipboardAPI = navigator.clipboard &&
+ typeof navigator.clipboard.writeText === 'function';
+
+ // Bind methods to maintain context
+ this.copy = this.copy.bind(this);
+ this.fallbackCopy = this.fallbackCopy.bind(this);
+ }
+
+ /**
+ * Copy text to the system clipboard with automatic fallback
+ *
+ * This method attempts to use the modern Clipboard API first, falling back
+ * to the legacy execCommand approach if the modern API is unavailable or fails.
+ * User feedback is provided through the notification system.
+ *
+ * @param {string} text - Text to copy to clipboard
+ * @returns {Promise} Promise resolving to true if successful
+ *
+ * @example
+ * // Basic usage
+ * const success = await clipboardManager.copy('console.log("Hello")');
+ *
+ * // With error handling
+ * try {
+ * await clipboardManager.copy(snippetText);
+ * console.log('Copy successful');
+ * } catch (error) {
+ * console.error('Copy failed:', error);
+ * }
+ */
+ async copy(text) {
+ if (!text) {
+ this.notifications.show('Nothing to copy', 'error');
+ return false;
+ }
+
+ try {
+ if (this.hasClipboardAPI) {
+ // Use modern Clipboard API
+ await navigator.clipboard.writeText(text);
+ this.notifications.show('Copied to clipboard');
+ return true;
+ } else {
+ // Fall back to legacy method
+ return this.fallbackCopy(text);
+ }
+ } catch (error) {
+ console.warn('Modern clipboard API failed, trying fallback:', error);
+ // Attempt fallback even if modern API exists but failed
+ return this.fallbackCopy(text);
+ }
+ }
+
+ /**
+ * Legacy clipboard copy method using execCommand
+ *
+ * This method provides clipboard functionality for older browsers or
+ * contexts where the modern Clipboard API is not available. It creates
+ * a temporary textarea element to hold the text and uses document.execCommand
+ * to copy it.
+ *
+ * @param {string} text - Text to copy using legacy method
+ * @returns {boolean} True if copy was successful
+ *
+ * @private
+ */
+ fallbackCopy(text) {
+ try {
+ // Create temporary textarea for the copy operation
+ const textarea = this.createTempTextarea(text);
+
+ // Add to DOM, select, and copy
+ document.body.appendChild(textarea);
+ textarea.select();
+ textarea.setSelectionRange(0, 99999); // For mobile devices
+
+ // Attempt to copy using execCommand
+ const successful = document.execCommand('copy');
+
+ // Clean up temporary element
+ document.body.removeChild(textarea);
+
+ if (successful) {
+ this.notifications.show('Copied to clipboard');
+ return true;
+ } else {
+ this.notifications.show('Copy failed - please try manually', 'error');
+ return false;
+ }
+ } catch (error) {
+ console.error('Fallback copy also failed:', error);
+ this.notifications.show('Copy not supported - please copy manually', 'error');
+ return false;
+ }
+ }
+
+ /**
+ * Create a temporary textarea element for fallback copying
+ *
+ * The textarea is positioned off-screen to avoid visual disruption
+ * while still being selectable for the copy operation.
+ *
+ * @param {string} text - Text to place in textarea
+ * @returns {HTMLTextAreaElement} Configured textarea element
+ *
+ * @private
+ */
+ createTempTextarea(text) {
+ const textarea = document.createElement('textarea');
+ textarea.value = text;
+
+ // Style to keep it invisible but accessible
+ textarea.style.position = 'fixed';
+ textarea.style.left = '-9999px';
+ textarea.style.top = '-9999px';
+ textarea.style.opacity = '0';
+ textarea.style.pointerEvents = 'none';
+ textarea.style.zIndex = '-1';
+
+ // Ensure it's focusable for selection
+ textarea.setAttribute('readonly', '');
+ textarea.setAttribute('aria-hidden', 'true');
+
+ return textarea;
+ }
+
+ /**
+ * Check if clipboard functionality is available
+ *
+ * @returns {boolean} True if either modern API or fallback is available
+ */
+ isSupported() {
+ return this.hasClipboardAPI || this.isExecCommandSupported();
+ }
+
+ /**
+ * Check if execCommand copy is supported
+ *
+ * @returns {boolean} True if execCommand('copy') is supported
+ *
+ * @private
+ */
+ isExecCommandSupported() {
+ try {
+ return document.queryCommandSupported && document.queryCommandSupported('copy');
+ } catch (error) {
+ return false;
+ }
+ }
+
+ /**
+ * Get clipboard capabilities information
+ *
+ * @returns {Object} Object describing available clipboard features
+ *
+ * @example
+ * const capabilities = clipboardManager.getCapabilities();
+ * console.log('Modern API:', capabilities.hasModernAPI);
+ * console.log('Fallback:', capabilities.hasFallback);
+ */
+ getCapabilities() {
+ return {
+ hasModernAPI: this.hasClipboardAPI,
+ hasFallback: this.isExecCommandSupported(),
+ isSupported: this.isSupported()
+ };
+ }
+}
+
+/**
+ * Factory function to create a clipboard manager instance
+ *
+ * @param {Object} notificationManager - Notification manager for user feedback
+ * @returns {ClipboardManager} Configured clipboard manager instance
+ *
+ * @example
+ * import { createClipboardManager } from './clipboard.js';
+ *
+ * const clipboardManager = createClipboardManager(notificationManager);
+ */
+export const createClipboardManager = (notificationManager) => {
+ return new ClipboardManager(notificationManager);
+};
diff --git a/js/components/modals.js b/js/components/modals.js
new file mode 100644
index 0000000..79a0c32
--- /dev/null
+++ b/js/components/modals.js
@@ -0,0 +1,551 @@
+/**
+ * Modal Management Component for Compy 2.0
+ *
+ * This module provides a comprehensive modal dialog management system that handles
+ * opening, closing, and managing modal dialogs throughout the application. It includes
+ * accessibility features, keyboard navigation, and focus management.
+ *
+ * Features:
+ * - Automatic focus management and restoration
+ * - Keyboard navigation (Escape to close, Tab trapping)
+ * - Accessibility support with ARIA attributes
+ * - Stack management for nested modals
+ * - Background click to close functionality
+ * - Animation and transition support
+ *
+ * @fileoverview Modal dialog management with accessibility features
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+import { $, $$, focusElement } from '../utils.js';
+
+/**
+ * ModalManager handles all modal dialog operations with accessibility support
+ *
+ * This class manages the lifecycle of modal dialogs, ensuring proper focus
+ * management, keyboard navigation, and accessibility compliance. It maintains
+ * a stack of open modals and handles nested modal scenarios.
+ *
+ * @class ModalManager
+ * @example
+ * const modalManager = new ModalManager();
+ *
+ * // Open a modal
+ * modalManager.open('#confirmModal');
+ *
+ * // Close current modal
+ * modalManager.close();
+ *
+ * // Close specific modal
+ * modalManager.close('#confirmModal');
+ */
+export class ModalManager {
+ /**
+ * Initialize the modal manager
+ *
+ * @param {Object} [options={}] - Configuration options
+ * @param {boolean} [options.closeOnBackdropClick=true] - Close modal when backdrop is clicked
+ * @param {boolean} [options.closeOnEscape=true] - Close modal when Escape key is pressed
+ * @param {number} [options.focusDelay=100] - Delay before focusing elements in milliseconds
+ * @param {string} [options.activeClass='modal-open'] - CSS class applied to body when modal is open
+ */
+ constructor(options = {}) {
+ // Merge options with defaults
+ this.options = {
+ closeOnBackdropClick: true,
+ closeOnEscape: true,
+ focusDelay: 100,
+ activeClass: 'modal-open',
+ ...options
+ };
+
+ // Initialize state
+ this.modalStack = []; // Stack of open modals
+ this.previousFocus = null; // Element that had focus before modal opened
+ this.focusableSelectors = this.getFocusableSelectors();
+
+ // Bind methods to maintain context
+ this.open = this.open.bind(this);
+ this.close = this.close.bind(this);
+ this.handleKeyboard = this.handleKeyboard.bind(this);
+ this.handleBackdropClick = this.handleBackdropClick.bind(this);
+ this.trapFocus = this.trapFocus.bind(this);
+
+ // Initialize event listeners
+ this.initializeEventListeners();
+ }
+
+ /**
+ * Get CSS selectors for focusable elements
+ *
+ * @returns {string} CSS selector string for focusable elements
+ * @private
+ */
+ getFocusableSelectors() {
+ return [
+ 'button:not([disabled])',
+ '[href]:not([disabled])',
+ 'input:not([disabled]):not([type="hidden"])',
+ 'select:not([disabled])',
+ 'textarea:not([disabled])',
+ '[tabindex]:not([tabindex="-1"]):not([disabled])',
+ 'details:not([disabled])',
+ 'summary:not([disabled])'
+ ].join(', ');
+ }
+
+ /**
+ * Initialize global event listeners
+ *
+ * Sets up keyboard and click event listeners that handle modal interactions
+ * throughout the application.
+ *
+ * @private
+ */
+ initializeEventListeners() {
+ // Global keyboard handler
+ document.addEventListener('keydown', this.handleKeyboard);
+
+ // Setup click handlers for modal close buttons
+ this.setupCloseHandlers();
+ }
+
+ /**
+ * Setup click handlers for modal close buttons
+ *
+ * Automatically wires up any elements with [data-close-modal] attribute
+ * to close their parent modal when clicked.
+ *
+ * @private
+ */
+ setupCloseHandlers() {
+ $$('[data-close-modal]').forEach(button => {
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ // Find the closest modal to this button
+ const modal = e.target.closest('.modal');
+ if (modal) {
+ this.close(`#${modal.id}`);
+ }
+ });
+ });
+ }
+
+ /**
+ * Open a modal dialog
+ *
+ * This method opens the specified modal, manages focus, adds it to the modal
+ * stack, and sets up proper accessibility attributes.
+ *
+ * @param {string} selector - CSS selector for the modal element
+ * @param {Object} [options={}] - Options for opening the modal
+ * @param {boolean} [options.restoreFocus=true] - Whether to restore focus when modal closes
+ * @param {string} [options.initialFocus] - CSS selector for element to focus initially
+ *
+ * @example
+ * // Basic modal opening
+ * modalManager.open('#settingsModal');
+ *
+ * // Open with specific initial focus
+ * modalManager.open('#confirmModal', {
+ * initialFocus: '[data-action="confirm"]'
+ * });
+ *
+ * // Open without focus restoration
+ * modalManager.open('#infoModal', { restoreFocus: false });
+ */
+ open(selector, options = {}) {
+ const modal = $(selector);
+ if (!modal) {
+ console.warn(`Modal not found: ${selector}`);
+ return false;
+ }
+
+ // Merge options
+ const config = {
+ restoreFocus: true,
+ initialFocus: null,
+ ...options
+ };
+
+ // Store previous focus if this is the first modal
+ if (this.modalStack.length === 0) {
+ this.previousFocus = document.activeElement;
+ }
+
+ // Add modal to stack
+ const modalInfo = {
+ element: modal,
+ selector,
+ config,
+ timestamp: Date.now()
+ };
+ this.modalStack.push(modalInfo);
+
+ // Show the modal
+ this.showModal(modal, config);
+
+ return true;
+ }
+
+ /**
+ * Show a modal element with proper setup
+ *
+ * @param {HTMLElement} modal - Modal element to show
+ * @param {Object} config - Configuration options
+ * @private
+ */
+ showModal(modal, config) {
+ // Set accessibility attributes
+ modal.setAttribute('aria-hidden', 'false');
+ modal.setAttribute('role', 'dialog');
+ modal.setAttribute('aria-modal', 'true');
+
+ // Add body class for styling
+ document.body.classList.add(this.options.activeClass);
+
+ // Setup backdrop click handler
+ if (this.options.closeOnBackdropClick) {
+ modal.addEventListener('click', this.handleBackdropClick);
+ }
+
+ // Focus management
+ this.setupModalFocus(modal, config);
+
+ // Setup focus trapping
+ modal.addEventListener('keydown', this.trapFocus);
+ }
+
+ /**
+ * Setup focus management for the modal
+ *
+ * @param {HTMLElement} modal - Modal element
+ * @param {Object} config - Configuration options
+ * @private
+ */
+ setupModalFocus(modal, config) {
+ let focusTarget;
+
+ if (config.initialFocus) {
+ // Use specified initial focus element
+ focusTarget = modal.querySelector(config.initialFocus);
+ }
+
+ if (!focusTarget) {
+ // Try to find a suitable focus target
+ focusTarget = this.findInitialFocusTarget(modal);
+ }
+
+ // Focus the target element
+ focusElement(focusTarget, this.options.focusDelay);
+ }
+
+ /**
+ * Find an appropriate initial focus target within the modal
+ *
+ * @param {HTMLElement} modal - Modal element to search within
+ * @returns {HTMLElement|null} Element to focus or null if none found
+ * @private
+ */
+ findInitialFocusTarget(modal) {
+ // Priority order for focus targets
+ const priorities = [
+ '[data-close-modal]', // Close buttons
+ 'button[data-primary]', // Primary action buttons
+ 'input:not([type="hidden"])', // Input fields
+ 'textarea', // Text areas
+ 'button', // Any button
+ '[tabindex="0"]' // Explicitly focusable elements
+ ];
+
+ for (const selector of priorities) {
+ const target = modal.querySelector(selector);
+ if (target && this.isVisible(target)) {
+ return target;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Check if an element is visible
+ *
+ * @param {HTMLElement} element - Element to check
+ * @returns {boolean} True if element is visible
+ * @private
+ */
+ isVisible(element) {
+ const style = window.getComputedStyle(element);
+ return style.display !== 'none' &&
+ style.visibility !== 'hidden' &&
+ style.opacity !== '0';
+ }
+
+ /**
+ * Close a modal dialog
+ *
+ * Closes the specified modal or the topmost modal if no selector is provided.
+ * Handles focus restoration and cleanup.
+ *
+ * @param {string} [selector] - CSS selector for specific modal to close
+ * @returns {boolean} True if a modal was closed
+ *
+ * @example
+ * // Close topmost modal
+ * modalManager.close();
+ *
+ * // Close specific modal
+ * modalManager.close('#settingsModal');
+ */
+ close(selector = null) {
+ if (this.modalStack.length === 0) {
+ return false;
+ }
+
+ let modalInfo;
+ let stackIndex;
+
+ if (selector) {
+ // Find specific modal in stack
+ stackIndex = this.modalStack.findIndex(info => info.selector === selector);
+ if (stackIndex === -1) {
+ return false;
+ }
+ modalInfo = this.modalStack[stackIndex];
+ } else {
+ // Close topmost modal
+ stackIndex = this.modalStack.length - 1;
+ modalInfo = this.modalStack[stackIndex];
+ }
+
+ // Hide the modal
+ this.hideModal(modalInfo.element);
+
+ // Remove from stack
+ this.modalStack.splice(stackIndex, 1);
+
+ // Handle focus restoration
+ this.handleFocusRestoration(modalInfo);
+
+ // Update body class if no modals remain
+ if (this.modalStack.length === 0) {
+ document.body.classList.remove(this.options.activeClass);
+ }
+
+ return true;
+ }
+
+ /**
+ * Hide a modal element and clean up
+ *
+ * @param {HTMLElement} modal - Modal element to hide
+ * @private
+ */
+ hideModal(modal) {
+ // Set accessibility attributes
+ modal.setAttribute('aria-hidden', 'true');
+ modal.removeAttribute('role');
+ modal.removeAttribute('aria-modal');
+
+ // Remove event listeners
+ modal.removeEventListener('click', this.handleBackdropClick);
+ modal.removeEventListener('keydown', this.trapFocus);
+ }
+
+ /**
+ * Handle focus restoration when modal closes
+ *
+ * @param {Object} modalInfo - Information about the closing modal
+ * @private
+ */
+ handleFocusRestoration(modalInfo) {
+ if (modalInfo.config.restoreFocus) {
+ if (this.modalStack.length === 0) {
+ // Last modal - restore original focus
+ focusElement(this.previousFocus, this.options.focusDelay);
+ this.previousFocus = null;
+ } else {
+ // Focus the previous modal in stack
+ const previousModal = this.modalStack[this.modalStack.length - 1];
+ const focusTarget = this.findInitialFocusTarget(previousModal.element);
+ focusElement(focusTarget, this.options.focusDelay);
+ }
+ }
+ }
+
+ /**
+ * Close all open modals
+ *
+ * Closes all modals in the stack, useful for cleanup or navigation.
+ *
+ * @example
+ * // Close all modals when navigating away
+ * modalManager.closeAll();
+ */
+ closeAll() {
+ while (this.modalStack.length > 0) {
+ this.close();
+ }
+ }
+
+ /**
+ * Handle keyboard events for modal interaction
+ *
+ * @param {KeyboardEvent} e - Keyboard event
+ * @private
+ */
+ handleKeyboard(e) {
+ if (this.modalStack.length === 0) return;
+
+ // Handle Escape key
+ if (e.key === 'Escape' && this.options.closeOnEscape) {
+ e.preventDefault();
+ this.close();
+ }
+ }
+
+ /**
+ * Handle backdrop clicks to close modal
+ *
+ * @param {MouseEvent} e - Click event
+ * @private
+ */
+ handleBackdropClick(e) {
+ // Only close if click was on the modal backdrop itself, not on modal content
+ if (e.target === e.currentTarget) {
+ this.close();
+ }
+ }
+
+ /**
+ * Trap focus within the modal
+ *
+ * @param {KeyboardEvent} e - Keyboard event
+ * @private
+ */
+ trapFocus(e) {
+ if (e.key !== 'Tab') return;
+
+ const modal = e.currentTarget;
+ const focusableElements = modal.querySelectorAll(this.focusableSelectors);
+ const focusableArray = Array.from(focusableElements).filter(el => this.isVisible(el));
+
+ if (focusableArray.length === 0) return;
+
+ const firstElement = focusableArray[0];
+ const lastElement = focusableArray[focusableArray.length - 1];
+
+ if (e.shiftKey) {
+ // Shift + Tab: moving backwards
+ if (document.activeElement === firstElement) {
+ e.preventDefault();
+ lastElement.focus();
+ }
+ } else {
+ // Tab: moving forwards
+ if (document.activeElement === lastElement) {
+ e.preventDefault();
+ firstElement.focus();
+ }
+ }
+ }
+
+ /**
+ * Get information about currently open modals
+ *
+ * @returns {Object} Modal stack information
+ *
+ * @example
+ * const info = modalManager.getModalInfo();
+ * console.log(`${info.count} modals open`);
+ * console.log('Current modal:', info.current?.selector);
+ */
+ getModalInfo() {
+ return {
+ count: this.modalStack.length,
+ current: this.modalStack[this.modalStack.length - 1] || null,
+ stack: [...this.modalStack] // Return copy
+ };
+ }
+
+ /**
+ * Check if a specific modal is open
+ *
+ * @param {string} selector - CSS selector for the modal
+ * @returns {boolean} True if the modal is open
+ *
+ * @example
+ * if (modalManager.isOpen('#settingsModal')) {
+ * console.log('Settings modal is open');
+ * }
+ */
+ isOpen(selector) {
+ return this.modalStack.some(info => info.selector === selector);
+ }
+
+ /**
+ * Check if any modal is open
+ *
+ * @returns {boolean} True if any modal is open
+ *
+ * @example
+ * if (modalManager.hasOpenModals()) {
+ * console.log('At least one modal is open');
+ * }
+ */
+ hasOpenModals() {
+ return this.modalStack.length > 0;
+ }
+
+ /**
+ * Update configuration options
+ *
+ * @param {Object} newOptions - New options to merge
+ *
+ * @example
+ * // Disable backdrop click to close
+ * modalManager.updateOptions({ closeOnBackdropClick: false });
+ */
+ updateOptions(newOptions) {
+ this.options = { ...this.options, ...newOptions };
+ }
+
+ /**
+ * Destroy the modal manager and clean up resources
+ *
+ * Removes all event listeners and closes any open modals.
+ */
+ destroy() {
+ // Close all modals
+ this.closeAll();
+
+ // Remove global event listeners
+ document.removeEventListener('keydown', this.handleKeyboard);
+
+ // Clear references
+ this.modalStack = [];
+ this.previousFocus = null;
+ }
+}
+
+/**
+ * Factory function to create a modal manager instance
+ *
+ * @param {Object} [options={}] - Configuration options
+ * @returns {ModalManager} Configured modal manager instance
+ *
+ * @example
+ * import { createModalManager } from './modals.js';
+ *
+ * const modalManager = createModalManager({
+ * closeOnBackdropClick: false,
+ * focusDelay: 200
+ * });
+ */
+export const createModalManager = (options = {}) => {
+ return new ModalManager(options);
+};
diff --git a/js/components/notifications.js b/js/components/notifications.js
new file mode 100644
index 0000000..4b66015
--- /dev/null
+++ b/js/components/notifications.js
@@ -0,0 +1,442 @@
+/**
+ * Notification Management Component for Compy 2.0
+ *
+ * This module provides a comprehensive notification system for user feedback
+ * throughout the application. It handles success messages, error alerts,
+ * and informational notifications with customizable duration and styling.
+ *
+ * Features:
+ * - Multiple notification types (info, success, error, warning)
+ * - Configurable display duration
+ * - Queue management for multiple notifications
+ * - Automatic cleanup and memory management
+ * - Accessibility support with ARIA attributes
+ *
+ * @fileoverview User notification system with queue management
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+import { UI_CONFIG } from '../constants.js';
+import { $ } from '../utils.js';
+
+/**
+ * NotificationManager handles all user notifications and feedback
+ *
+ * This class manages the display of transient messages to users, including
+ * success confirmations, error alerts, and informational messages. It provides
+ * a consistent interface for user feedback throughout the application.
+ *
+ * @class NotificationManager
+ * @example
+ * const notificationManager = new NotificationManager();
+ *
+ * // Show success message
+ * notificationManager.show('Item saved successfully', 'success');
+ *
+ * // Show error message
+ * notificationManager.show('Failed to save item', 'error');
+ *
+ * // Show with custom duration
+ * notificationManager.show('Processing...', 'info', 5000);
+ */
+export class NotificationManager {
+ /**
+ * Initialize the notification manager
+ *
+ * @param {Object} [options={}] - Configuration options
+ * @param {number} [options.defaultDuration] - Default display duration in milliseconds
+ * @param {string} [options.containerSelector='#snackbar'] - CSS selector for notification container
+ */
+ constructor(options = {}) {
+ // Merge options with defaults
+ this.options = {
+ defaultDuration: UI_CONFIG.snackbarDuration,
+ containerSelector: '#snackbar',
+ ...options
+ };
+
+ // Initialize state
+ this.queue = []; // Queue of pending notifications
+ this.currentNotification = null; // Currently displayed notification
+ this.timeoutId = null; // Timeout for current notification
+
+ // Get or create notification container
+ this.container = this.initializeContainer();
+
+ // Bind methods to maintain context
+ this.show = this.show.bind(this);
+ this.hide = this.hide.bind(this);
+ this.processQueue = this.processQueue.bind(this);
+ }
+
+ /**
+ * Initialize or find the notification container element
+ *
+ * @returns {HTMLElement} The notification container element
+ * @private
+ */
+ initializeContainer() {
+ let container = $(this.options.containerSelector);
+
+ if (!container) {
+ // Create container if it doesn't exist
+ container = this.createContainer();
+ }
+
+ return container;
+ }
+
+ /**
+ * Create a notification container element with proper styling and accessibility
+ *
+ * @returns {HTMLElement} Newly created notification container
+ * @private
+ */
+ createContainer() {
+ const container = document.createElement('div');
+ container.id = this.options.containerSelector.replace('#', '');
+ container.className = 'snackbar';
+ container.setAttribute('role', 'alert');
+ container.setAttribute('aria-live', 'polite');
+ container.setAttribute('aria-atomic', 'true');
+
+ // Add to document body
+ document.body.appendChild(container);
+
+ return container;
+ }
+
+ /**
+ * Show a notification message
+ *
+ * This method displays a notification to the user. If another notification
+ * is currently displayed, the new one is queued and shown when the current
+ * one finishes.
+ *
+ * @param {string} message - Message text to display
+ * @param {string} [type='info'] - Notification type: 'info', 'success', 'error', 'warning'
+ * @param {number} [duration] - Display duration in milliseconds (uses default if not specified)
+ *
+ * @example
+ * // Basic info notification
+ * notificationManager.show('Data updated');
+ *
+ * // Success notification
+ * notificationManager.show('File saved successfully', 'success');
+ *
+ * // Error with longer duration
+ * notificationManager.show('Network error occurred', 'error', 5000);
+ */
+ show(message, type = 'info', duration = this.options.defaultDuration) {
+ if (!message) return;
+
+ // Create notification object
+ const notification = {
+ id: this.generateNotificationId(),
+ message: String(message).trim(),
+ type: this.validateType(type),
+ duration: Math.max(1000, Number(duration) || this.options.defaultDuration),
+ timestamp: Date.now()
+ };
+
+ // Add to queue
+ this.queue.push(notification);
+
+ // Process queue if no notification is currently shown
+ if (!this.currentNotification) {
+ this.processQueue();
+ }
+ }
+
+ /**
+ * Process the notification queue
+ *
+ * Displays the next notification in the queue if any exist.
+ * This method is called automatically when new notifications are added
+ * or when the current notification finishes.
+ *
+ * @private
+ */
+ processQueue() {
+ // Return if already showing a notification or queue is empty
+ if (this.currentNotification || this.queue.length === 0) {
+ return;
+ }
+
+ // Get next notification from queue
+ const notification = this.queue.shift();
+ this.displayNotification(notification);
+ }
+
+ /**
+ * Display a single notification
+ *
+ * @param {Object} notification - Notification object to display
+ * @private
+ */
+ displayNotification(notification) {
+ this.currentNotification = notification;
+
+ // Update container content and styling
+ this.updateContainer(notification);
+
+ // Show the notification
+ this.container.classList.add('show');
+
+ // Set timer to hide after duration
+ this.timeoutId = setTimeout(() => {
+ this.hide();
+ }, notification.duration);
+ }
+
+ /**
+ * Update container with notification content and styling
+ *
+ * @param {Object} notification - Notification to display
+ * @private
+ */
+ updateContainer(notification) {
+ // Set text content
+ this.container.textContent = notification.message;
+
+ // Update CSS classes for styling
+ this.container.className = `snackbar ${notification.type}`;
+
+ // Update accessibility attributes
+ this.container.setAttribute('data-notification-id', notification.id);
+ this.container.setAttribute('data-notification-type', notification.type);
+ }
+
+ /**
+ * Hide the current notification
+ *
+ * This method hides the currently displayed notification and processes
+ * the next one in the queue if available.
+ *
+ * @example
+ * // Manually hide current notification
+ * notificationManager.hide();
+ */
+ hide() {
+ if (!this.currentNotification) return;
+
+ // Clear timeout
+ if (this.timeoutId) {
+ clearTimeout(this.timeoutId);
+ this.timeoutId = null;
+ }
+
+ // Hide notification
+ this.container.classList.remove('show');
+
+ // Clear current notification
+ this.currentNotification = null;
+
+ // Process next notification in queue after a brief delay
+ setTimeout(() => {
+ this.processQueue();
+ }, 300); // Allow time for hide animation
+ }
+
+ /**
+ * Clear all pending notifications
+ *
+ * Removes all notifications from the queue and hides the current one.
+ * Useful for cleaning up when navigating away or resetting the application state.
+ *
+ * @example
+ * // Clear all notifications when changing pages
+ * notificationManager.clear();
+ */
+ clear() {
+ // Clear queue
+ this.queue = [];
+
+ // Hide current notification
+ this.hide();
+ }
+
+ /**
+ * Show multiple notifications in sequence
+ *
+ * @param {Array} notifications - Array of notification objects
+ * @param {number} [delayBetween=0] - Delay between notifications in milliseconds
+ *
+ * @example
+ * // Show multiple related notifications
+ * notificationManager.showMultiple([
+ * { message: 'Starting backup...', type: 'info' },
+ * { message: 'Backup completed', type: 'success', duration: 3000 }
+ * ], 500);
+ */
+ showMultiple(notifications, delayBetween = 0) {
+ if (!Array.isArray(notifications)) return;
+
+ notifications.forEach((notification, index) => {
+ const delay = index * delayBetween;
+ setTimeout(() => {
+ this.show(
+ notification.message,
+ notification.type || 'info',
+ notification.duration
+ );
+ }, delay);
+ });
+ }
+
+ /**
+ * Generate unique notification ID
+ *
+ * @returns {string} Unique notification identifier
+ * @private
+ */
+ generateNotificationId() {
+ return `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ }
+
+ /**
+ * Validate and normalize notification type
+ *
+ * @param {string} type - Notification type to validate
+ * @returns {string} Valid notification type
+ * @private
+ */
+ validateType(type) {
+ const validTypes = ['info', 'success', 'error', 'warning'];
+ return validTypes.includes(type) ? type : 'info';
+ }
+
+ /**
+ * Get current notification information
+ *
+ * @returns {Object|null} Current notification object or null if none active
+ *
+ * @example
+ * const current = notificationManager.getCurrentNotification();
+ * if (current) {
+ * console.log('Current notification:', current.message, current.type);
+ * }
+ */
+ getCurrentNotification() {
+ return this.currentNotification;
+ }
+
+ /**
+ * Get queue status information
+ *
+ * @returns {Object} Queue status with count and pending notifications
+ *
+ * @example
+ * const status = notificationManager.getQueueStatus();
+ * console.log(`${status.count} notifications pending`);
+ */
+ getQueueStatus() {
+ return {
+ count: this.queue.length,
+ hasActive: Boolean(this.currentNotification),
+ queue: [...this.queue] // Return copy to prevent external modification
+ };
+ }
+
+ /**
+ * Update configuration options
+ *
+ * @param {Object} newOptions - New configuration options to merge
+ *
+ * @example
+ * // Change default duration
+ * notificationManager.updateOptions({ defaultDuration: 3000 });
+ */
+ updateOptions(newOptions) {
+ this.options = { ...this.options, ...newOptions };
+ }
+
+ /**
+ * Destroy the notification manager and clean up resources
+ *
+ * Call this method when the notification manager is no longer needed
+ * to prevent memory leaks and clean up DOM elements.
+ */
+ destroy() {
+ // Clear all notifications and timeouts
+ this.clear();
+
+ // Remove container from DOM if we created it
+ if (this.container && this.container.id === this.options.containerSelector.replace('#', '')) {
+ this.container.remove();
+ }
+
+ // Clear references
+ this.container = null;
+ this.currentNotification = null;
+ this.queue = [];
+ }
+}
+
+/**
+ * Factory function to create a notification manager instance
+ *
+ * @param {Object} [options={}] - Configuration options
+ * @returns {NotificationManager} Configured notification manager instance
+ *
+ * @example
+ * import { createNotificationManager } from './notifications.js';
+ *
+ * const notificationManager = createNotificationManager({
+ * defaultDuration: 2000,
+ * containerSelector: '#notifications'
+ * });
+ */
+export const createNotificationManager = (options = {}) => {
+ return new NotificationManager(options);
+};
+
+/**
+ * Convenience functions for common notification types
+ */
+
+/**
+ * Show success notification
+ *
+ * @param {NotificationManager} manager - Notification manager instance
+ * @param {string} message - Success message
+ * @param {number} [duration] - Display duration
+ */
+export const showSuccess = (manager, message, duration) => {
+ manager.show(message, 'success', duration);
+};
+
+/**
+ * Show error notification
+ *
+ * @param {NotificationManager} manager - Notification manager instance
+ * @param {string} message - Error message
+ * @param {number} [duration] - Display duration
+ */
+export const showError = (manager, message, duration) => {
+ manager.show(message, 'error', duration);
+};
+
+/**
+ * Show info notification
+ *
+ * @param {NotificationManager} manager - Notification manager instance
+ * @param {string} message - Info message
+ * @param {number} [duration] - Display duration
+ */
+export const showInfo = (manager, message, duration) => {
+ manager.show(message, 'info', duration);
+};
+
+/**
+ * Show warning notification
+ *
+ * @param {NotificationManager} manager - Notification manager instance
+ * @param {string} message - Warning message
+ * @param {number} [duration] - Display duration
+ */
+export const showWarning = (manager, message, duration) => {
+ manager.show(message, 'warning', duration);
+};
diff --git a/js/compy.js b/js/compy.js
deleted file mode 100644
index a4c34c1..0000000
--- a/js/compy.js
+++ /dev/null
@@ -1,579 +0,0 @@
-/* Compy - vanilla JS implementation per Doc3 */
-(function(){
- const $ = (sel, root=document) => root.querySelector(sel);
- const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
-
- const STORAGE_KEYS = {
- items: 'compy.items',
- theme: 'compy.theme',
- profile: 'compy.profile',
- backups: 'compy.backups',
- filters: 'compy.filters',
- };
-
- let state = {
- items: [], // {id, text, desc, sensitive, tags:[]}
- filterTags: [],
- search: '',
- editingId: null,
- profileName: '',
- };
-
- // Utilities
- const uid = () => Math.random().toString(36).slice(2) + Date.now().toString(36);
- const saveState = () => {
- localStorage.setItem(STORAGE_KEYS.items, JSON.stringify(state.items));
- localStorage.setItem(STORAGE_KEYS.filters, JSON.stringify(state.filterTags));
- if (state.profileName) localStorage.setItem(STORAGE_KEYS.profile, state.profileName);
- scheduleBackup();
- };
- const loadState = () => {
- try { state.items = JSON.parse(localStorage.getItem(STORAGE_KEYS.items) || '[]'); } catch { state.items = []; }
- try { state.filterTags = JSON.parse(localStorage.getItem(STORAGE_KEYS.filters) || '[]'); } catch { state.filterTags = []; }
- state.profileName = localStorage.getItem(STORAGE_KEYS.profile) || '';
- };
-
- const showSnackbar = (msg) => {
- const s = $('#snackbar');
- s.textContent = msg; s.classList.add('show');
- setTimeout(()=>s.classList.remove('show'), 1500);
- };
-
- const copyToClipboard = async (txt) => {
- try { await navigator.clipboard.writeText(txt); showSnackbar('Copied'); }
- catch { // fallback
- const ta = document.createElement('textarea'); ta.value = txt; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); showSnackbar('Copied');
- }
- };
-
- // Themes
- const applyTheme = (val) => { document.documentElement.setAttribute('data-theme', val); localStorage.setItem(STORAGE_KEYS.theme, val); $('#themeSelect').value = val; };
- const loadTheme = () => { const t = localStorage.getItem(STORAGE_KEYS.theme); if (t) applyTheme(t); };
-
- // Simple inline SVG icons (crisp, theme-aware)
- const ICONS = {
- edit: ' ',
- delete: ' ',
- copy: ' '
- };
-
- // Rendering
- const renderCards = () => {
- const container = $('#cards');
- const items = filteredAndSearchedItems();
- container.innerHTML = '';
-
- // Home/Getting Started when there is no data at all
- if (state.items.length === 0) {
- container.classList.add('empty-state');
- container.innerHTML = emptyStateHtmlPro();
- $('#emptyAddBtn')?.addEventListener('click', ()=> openItemModal(null));
- $('#emptyImportBtn')?.addEventListener('click', ()=> $('#importFile').click());
- return;
- } else {
- container.classList.remove('empty-state');
- }
-
- // If filtered result is empty (due to search/filter), show a helpful empty state
- if (!items.length) {
- container.classList.add('empty-state');
- container.innerHTML = noResultsHtml();
- $('#clearSearchBtn')?.addEventListener('click', ()=>{ $('#searchInput').value=''; state.search=''; renderCards(); });
- $('#clearFiltersBtn')?.addEventListener('click', ()=>{ state.filterTags=[]; localStorage.setItem(STORAGE_KEYS.filters, JSON.stringify(state.filterTags)); renderFilterBadge(); renderCards(); });
- return;
- }
-
- for (const it of items) container.appendChild(renderCard(it));
- };
-
- // Empty state (Getting Started)
- const emptyStateHtml = () => `
-
- Welcome to Compy
- Store commands, snippets, credentials, and frequently used text. Click a card to copy it.
-
- Add your first item
- Import from JSON/CSV
-
-
- Use Ctrl+F or / to quickly search
- Tag items and filter by tags
- Choose a theme you like
-
-
- `;
- // Professional empty state variant (not used yet)
- const emptyStateHtmlPro = () => `
-
-
-
📋
-
Welcome to Compy
-
Your personal clipboard for commands, snippets, credentials, and frequently used text.
-
- Add your first item
- Import JSON/CSV
-
-
-
- Search fast with Ctrl+F or /
- Organize with tags and filters
- Personalize with themes and your profile name
-
-
-
- `;
-
- // No results state for search/filter
- const noResultsHtml = () => {
- const hasSearch = !!state.search?.trim();
- const hasFilters = state.filterTags.length > 0;
- let details = '';
- if (hasSearch && hasFilters) details = `No items match your search and selected filters.`;
- else if (hasSearch) details = `No items match your search.`;
- else if (hasFilters) details = `No items match the selected filters.`;
- return `
-
-
-
🔎
-
No results
-
${details}
-
- ${hasSearch ? 'Clear search ' : ''}
- ${hasFilters ? 'Clear filters ' : ''}
-
-
- `;
- };
-
-
- const highlight = (text, query) => {
- if (!query) return text;
- const q = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- return text.replace(new RegExp(q, 'gi'), (m)=>`${m} `);
- };
-
- const renderCard = (it) => {
- const card = document.createElement('article'); card.className = 'card'; card.tabIndex = 0;
- card.innerHTML = `
-
- ${ICONS.edit}
- ${ICONS.delete}
- ${ICONS.copy}
-
- ${highlight(it.sensitive ? '**********' : escapeHtml(it.text), state.search)}
- ${highlight(escapeHtml(it.desc), state.search)}
- ${tagsHtml(it.tags)}
- `;
- card.addEventListener('click', (e)=>{
- if (e.target.closest('.actions')) return; // actions handled separately
- copyToClipboard(it.text);
- });
- card.addEventListener('keydown', (e)=>{ if (e.key==='Enter') copyToClipboard(it.text); });
- card.querySelector('[data-act="edit"]').addEventListener('click', ()=>openItemModal(it.id));
- card.querySelector('[data-act="delete"]').addEventListener('click', ()=>deleteItem(it.id));
- card.querySelector('[data-act="copy"]').addEventListener('click', ()=>copyToClipboard(it.text));
- return card;
- };
-
- const tagsHtml = (tags=[]) => {
- const visible = tags.slice(0,5);
- const more = tags.length - visible.length;
- let html = visible.map(t=>`${escapeHtml(t)} `).join('');
- if (more>0) html += `+${more} more `;
- return html;
- };
-
- const filteredAndSearchedItems = () => {
- let items = state.items.slice();
- if (state.filterTags.length) items = items.filter(it=> state.filterTags.every(t=> it.tags.includes(t)) );
- if (state.search) {
- const q = state.search.toLowerCase();
- items = items.filter(it => it.text.toLowerCase().includes(q) || it.desc.toLowerCase().includes(q) || it.tags.some(t=>t.toLowerCase().includes(q)) );
- }
- return items;
- };
-
- // Item CRUD
- const upsertItem = (payload) => {
- if (state.editingId) {
- const idx = state.items.findIndex(i=>i.id===state.editingId); if (idx>-1) state.items[idx] = { ...state.items[idx], ...payload };
- } else {
- state.items.unshift({ id: uid(), ...payload });
- }
- saveState(); renderCards();
- };
- const deleteItem = (id) => {
- state.items = state.items.filter(i=>i.id!==id); saveState(); renderCards(); showSnackbar('Deleted');
- };
-
- // Modals
- const openModal = (el) => { el.setAttribute('aria-hidden','false'); el.querySelector('[data-close-modal]')?.focus(); };
- const closeModal = (el) => { el.setAttribute('aria-hidden','true'); };
- $$('#itemModal [data-close-modal]').forEach(b=>b.addEventListener('click', ()=>closeModal($('#itemModal'))));
- $$('#filterModal [data-close-modal]').forEach(b=>b.addEventListener('click', ()=>closeModal($('#filterModal'))));
- $$('#moreTagsModal [data-close-modal]').forEach(b=>b.addEventListener('click', ()=>closeModal($('#moreTagsModal'))));
- $$('#aboutModal [data-close-modal]').forEach(b=>b.addEventListener('click', ()=>closeModal($('#aboutModal'))));
- $$('#backupsModal [data-close-modal]').forEach(b=>b.addEventListener('click', ()=>closeModal($('#backupsModal'))));
- $$('#profileModal [data-close-modal]').forEach(b=>b.addEventListener('click', ()=>closeModal($('#profileModal'))));
-
- // Add/Edit modal logic
- const openItemModal = (id=null) => {
- state.editingId = id;
- $('#itemModalTitle').textContent = id ? 'Edit Item' : 'Add Item';
- const it = id ? state.items.find(i=>i.id===id) : {text:'',desc:'',sensitive:false,tags:[]};
- $('#itemText').value = it.text || '';
- $('#itemDesc').value = it.desc || '';
- $('#itemSensitive').checked = !!it.sensitive;
- setTagChips(it.tags || []);
- openModal($('#itemModal'));
- $('#itemText').focus();
- };
-
- const getTagsFromChips = () => $$('#tagChips .chip').map(c=>c.dataset.val);
- const setTagChips = (tags) => {
- const wrap = $('#tagChips'); wrap.innerHTML = '';
- tags.forEach(addTagChip);
- };
- const addTagChip = (tag) => {
- if (!tag) return;
- const norm = tag.trim(); if (!norm) return;
- const chip = document.createElement('span'); chip.className = 'chip'; chip.dataset.val = norm;
- chip.style.setProperty('--h', String(Math.abs(hash(norm)) % 360));
- chip.setAttribute('data-color','1');
- chip.innerHTML = `${escapeHtml(norm)} × `;
- chip.querySelector('.x').addEventListener('click', ()=>{ chip.remove(); });
- $('#tagChips').appendChild(chip);
- };
-
- $('#tagEntry').addEventListener('keydown', (e)=>{
- if (e.key==='Enter' && e.currentTarget.value.trim()) { addTagChip(e.currentTarget.value); e.currentTarget.value=''; }
- else if (e.key==='Backspace' && !e.currentTarget.value) { const chips = $$('#tagChips .chip'); chips.at(-1)?.remove(); }
- });
-
- // Save item
- $('#saveItemBtn').addEventListener('click', ()=>{
- const text = $('#itemText').value.trim();
- const desc = $('#itemDesc').value.trim();
- const sensitive = $('#itemSensitive').checked;
- const tags = getTagsFromChips();
- if (!text || !desc) { showSnackbar('CompyItem and Description are required'); return; }
- upsertItem({ text, desc, sensitive, tags });
- closeModal($('#itemModal'));
- });
-
- // Clear field buttons
- $$('[data-clear]').forEach(btn=> btn.addEventListener('click', ()=>{ const t = $(btn.getAttribute('data-clear')); if (t) { t.value=''; t.focus(); } }));
-
- // Search
- const focusSearch = () => $('#searchInput').focus();
- $('#searchClear').addEventListener('click', ()=>{ $('#searchInput').value=''; state.search=''; renderCards(); });
- $('#searchInput').addEventListener('input', (e)=>{ state.search = e.target.value; renderCards(); });
- document.addEventListener('keydown', (e)=>{
- if ((e.ctrlKey && e.key.toLowerCase()==='f') || e.key==='/') { e.preventDefault(); focusSearch(); }
- });
-
- // Filter
- const openFilter = () => {
- renderFilterList();
- $('#filterTagSearch').value='';
- openModal($('#filterModal'));
- };
- const renderFilterList = () => {
- const list = $('#filterTagList'); list.innerHTML = '';
- const allTags = Array.from(new Set(state.items.flatMap(i=>i.tags))).sort();
- const q = $('#filterTagSearch').value.toLowerCase();
- for (const t of allTags) {
- if (q && !t.toLowerCase().includes(q)) continue;
- const id = `tag-${t}`;
- const row = document.createElement('label'); row.className = 'list-row'; row.htmlFor = id;
- row.innerHTML = ` ${escapeHtml(t)}`;
- list.appendChild(row);
- }
- if (!list.children.length) {
- const msg = document.createElement('div');
- msg.className = 'empty-note';
- const qRaw = $('#filterTagSearch').value.trim();
- msg.innerHTML = allTags.length ? `No tags match "${escapeHtml(qRaw)}".` : 'No tags yet. Add tags to items to filter by them.';
- list.appendChild(msg);
- }
- };
- $('#filterBtn').addEventListener('click', openFilter);
- $('#filterTagSearch').addEventListener('input', renderFilterList);
- $('#applyFilterBtn').addEventListener('click', ()=>{
- state.filterTags = $$(`#filterTagList input:checked`).map(i=>i.id.replace(/^tag-/,''));
- localStorage.setItem(STORAGE_KEYS.filters, JSON.stringify(state.filterTags));
- renderFilterBadge();
- renderCards();
- closeModal($('#filterModal'));
- });
- // Clear filters: uncheck all, reset filterTags and search inside modal
- $('#clearFilterBtn').addEventListener('click', ()=>{
- state.filterTags = [];
- localStorage.setItem(STORAGE_KEYS.filters, JSON.stringify(state.filterTags));
- $$('#filterTagList input[type="checkbox"]').forEach(i=> i.checked = false);
- $('#filterTagSearch').value = '';
- renderFilterList();
- renderFilterBadge();
- renderCards();
- closeModal($('#filterModal'));
- });
-
- // More tags
- document.addEventListener('click', (e)=>{
- const more = e.target.closest('[data-more-tags]');
- if (more) {
- const card = more.closest('.card');
- const idx = Array.from($('#cards').children).indexOf(card);
- const it = filteredAndSearchedItems()[idx];
- const list = $('#allTagsList'); list.innerHTML = '';
- it.tags.forEach(t=>{ const ch=document.createElement('span'); ch.className='chip'; ch.textContent=t; list.appendChild(ch); });
- openModal($('#moreTagsModal'));
- }
- });
-
- // Menu: Export (floating, does not affect navbar height)
- const exportBtn = $('#exportMenuBtn');
- const exportMenu = $('#exportMenu');
- const exportHost = exportMenu.parentElement; // .menu
-
- function positionExportMenu() {
- const r = exportBtn.getBoundingClientRect();
- exportMenu.style.position = 'fixed';
- exportMenu.style.left = `${Math.round(r.left)}px`;
- exportMenu.style.top = `${Math.round(r.bottom + 6)}px`;
- exportMenu.style.right = 'auto';
- }
- function openExportMenu() {
- if (!exportMenu.classList.contains('open')) {
- document.body.appendChild(exportMenu);
- positionExportMenu();
- exportMenu.classList.add('open', 'floating');
- window.addEventListener('resize', closeExportMenu, { once: true });
- window.addEventListener('scroll', closeExportMenu, { once: true });
- }
- }
- function closeExportMenu() {
- exportMenu.classList.remove('open', 'floating');
- exportMenu.style.cssText = '';
- exportHost.appendChild(exportMenu);
- }
- exportBtn.addEventListener('click', (e)=>{
- e.stopPropagation();
- if (exportMenu.classList.contains('open')) closeExportMenu(); else openExportMenu();
- });
- document.addEventListener('click', (e)=>{ if (!e.target.closest('#exportMenu')) closeExportMenu(); });
- exportMenu.addEventListener('click', (e)=>{
- const btn = e.target.closest('button'); if (!btn) return;
- closeExportMenu();
- if (btn.dataset.export==='json') exportJSON();
- else if (btn.dataset.export==='csv') exportCSV();
- else if (btn.id==='backupsBtn') openBackups();
- });
-
- // Import
- $('#importFile').addEventListener('change', async (e)=>{
- const file = e.target.files?.[0]; if (!file) return;
- const text = await file.text();
- if (file.name.endsWith('.json')) importJSON(text);
- else if (file.name.endsWith('.csv')) importCSV(text);
- e.target.value = '';
- });
-
- // Backups
- let backupTimer = null;
- const scheduleBackup = () => {
- if (backupTimer) clearTimeout(backupTimer);
- backupTimer = setTimeout(doBackup, 200);
- };
- const doBackup = () => {
- const now = new Date();
- const backup = { ts: now.toISOString(), items: state.items };
- let arr = [];
- try { arr = JSON.parse(localStorage.getItem(STORAGE_KEYS.backups) || '[]'); } catch {}
- arr.unshift(backup);
- arr = arr.slice(0, 10);
- localStorage.setItem(STORAGE_KEYS.backups, JSON.stringify(arr));
- };
- const openBackups = () => {
- const list = $('#backupsList'); list.innerHTML = '';
- let arr = [];
- try { arr = JSON.parse(localStorage.getItem(STORAGE_KEYS.backups) || '[]'); } catch {}
- arr.forEach((b, i)=>{
- const btn = document.createElement('button');
- btn.textContent = `${new Date(b.ts).toLocaleString()} (${b.items.length} items)`;
- btn.addEventListener('click', ()=> download('compy-backup-'+b.ts.replace(/[:.]/g,'-')+'.json', JSON.stringify(b.items, null, 2)));
- list.appendChild(btn);
- });
- openModal($('#backupsModal'));
- };
-
- // Export/Import helpers
- const download = (filename, text) => {
- const blob = new Blob([text], {type: 'application/json'});
- const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href);
- };
- const exportJSON = () => {
- const payload = { profileName: state.profileName || '', items: state.items };
- download('compy-export.json', JSON.stringify(payload, null, 2));
- };
- const exportCSV = () => {
- const header = ['profileName'];
- const meta = [[csvEscape(state.profileName || '')]];
- const rows = [['text','desc','sensitive','tags']].concat(state.items.map(i=>[
- csvEscape(i.text), csvEscape(i.desc), i.sensitive?'1':'0', csvEscape(i.tags.join('|'))
- ]));
- const csv = [header.join(','), ...meta.map(r=>r.join(',')), ...rows.map(r=>r.join(','))].join('\n');
- const blob = new Blob([csv], {type: 'text/csv'});
- const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'compy-export.csv'; a.click(); URL.revokeObjectURL(a.href);
- };
- const importJSON = (json) => {
- try {
- const parsed = JSON.parse(json);
- let itemsArr;
- if (Array.isArray(parsed)) {
- // Backward compatibility: old exports were an array of items
- itemsArr = parsed;
- } else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.items)) {
- // New format with profile
- itemsArr = parsed.items;
- if (typeof parsed.profileName === 'string') {
- state.profileName = parsed.profileName.trim();
- localStorage.setItem(STORAGE_KEYS.profile, state.profileName);
- renderProfile();
- }
- } else {
- throw new Error('Invalid JSON');
- }
- for (const o of itemsArr) addImportedItem(o);
- saveState(); renderCards(); showSnackbar('Imported JSON');
- } catch { showSnackbar('Invalid JSON'); }
- };
- const importCSV = (csv) => {
- const lines = csv.split(/\r?\n/).filter(l => l.trim().length > 0);
- if (!lines.length) { showSnackbar('Invalid CSV'); return; }
- // Handle BOM and parse header robustly (quotes/case/whitespace)
- const rawHeader = lines.shift().replace(/^\uFEFF/, '');
- let headerCols = parseCSVLine(rawHeader).map(h => String(h || '').trim().toLowerCase());
- // Optional metadata block: single-column "profileName" followed by a value row
- if (headerCols.length === 1 && headerCols[0] === 'profilename') {
- const metaLine = lines.shift();
- if (metaLine != null) {
- const metaCols = parseCSVLine(metaLine);
- state.profileName = String(metaCols[0] || '').trim();
- localStorage.setItem(STORAGE_KEYS.profile, state.profileName);
- renderProfile();
- }
- const itemsHeader = lines.shift();
- if (!itemsHeader) { showSnackbar('Invalid CSV'); return; }
- headerCols = parseCSVLine(itemsHeader).map(h => String(h || '').trim().toLowerCase());
- }
- const idx = (name) => headerCols.indexOf(name);
- const iText = idx('text');
- const iDesc = idx('desc');
- const iSensitive = idx('sensitive');
- const iTags = idx('tags');
- if (iText === -1 || iDesc === -1) { showSnackbar('Invalid CSV'); return; }
- for (const line of lines) {
- const cols = parseCSVLine(line);
- const text = String(cols[iText] || '').trim();
- const desc = String(cols[iDesc] || '').trim();
- const sensRaw = iSensitive !== -1 ? String(cols[iSensitive] || '').trim() : '';
- const tagsRaw = iTags !== -1 ? String(cols[iTags] || '').trim() : '';
- addImportedItem({
- text,
- desc,
- sensitive: sensRaw === '1' || sensRaw.toLowerCase() === 'true',
- tags: tagsRaw.split('|').map(s => s.trim()).filter(Boolean)
- });
- }
- saveState(); renderCards(); showSnackbar('Imported CSV');
- };
- const addImportedItem = (o) => {
- if (!o || !o.text || !o.desc) return;
- state.items.push({ id: uid(), text: String(o.text), desc: String(o.desc), sensitive: !!o.sensitive, tags: Array.isArray(o.tags)? o.tags.map(String) : [] });
- };
-
- // CSV helpers
- const csvEscape = (s) => '"' + String(s).replace(/"/g, '""') + '"';
- const parseCSVLine = (line) => {
- const out = []; let cur=''; let inQ=false;
- for (let i=0;i {
- $('#profileDisplay').textContent = state.profileName ? `· ${state.profileName}'s Compy` : '';
- };
-
- // Filter badge counter in header
- const renderFilterBadge = () => {
- const badge = $('#filterBadge');
- if (!badge) return;
- const count = state.filterTags.length;
- if (count > 0) {
- badge.textContent = String(count);
- badge.hidden = false;
- } else {
- badge.hidden = true;
- }
- };
-
- $('#profileEditBtn').addEventListener('click', ()=>{
- // Open dedicated profile modal instead of prompt
- $('#profileNameInput').value = state.profileName || '';
- openModal($('#profileModal'));
- $('#profileNameInput').focus();
- });
-
- function saveProfileFromModal() {
- const name = $('#profileNameInput').value;
- state.profileName = (name || '').trim();
- localStorage.setItem(STORAGE_KEYS.profile, state.profileName);
- renderProfile();
- closeModal($('#profileModal'));
- }
-
- $('#profileSaveBtn').addEventListener('click', saveProfileFromModal);
- $('#profileNameInput').addEventListener('keydown', (e)=>{ if (e.key === 'Enter') { e.preventDefault(); saveProfileFromModal(); } });
-
- // Brand refresh
- $('#brand').addEventListener('click', ()=> location.reload());
-
- // Add button
- $('#addBtn').addEventListener('click', ()=> openItemModal(null));
-
- // Theme change
- $('#themeSelect').addEventListener('change', (e)=> applyTheme(e.target.value));
-
- // About
- $('#aboutBtn').addEventListener('click', ()=> openModal($('#aboutModal')));
-
- // Clear chips via X inside card should not actually remove tags from item per requirements; only in modal we remove.
-
- // Init
- function escapeHtml(s){ return String(s).replace(/[&<>"']/g, (c)=>({"&":"&","<":"<", ">":">","\"":""","'":"'"}[c])); }
- loadState();
- loadTheme();
- renderProfile();
- renderFilterBadge();
- renderCards();
- // simple string hash for deterministic tag hue
- function hash(str){ let h=0; for(let i=0;i
+
+
+ `,
+
+ /** Delete/trash icon - Used for snippet removal actions */
+ delete: `
+
+
+
+
+ `,
+
+ /** Copy/clipboard icon - Used for copying snippet content to clipboard */
+ copy: `
+
+
+ `
+};
+
+
+/**
+ * Available Theme Options
+ *
+ * List of all theme identifiers that can be applied via the data-theme attribute
+ * on the document element. Each theme corresponds to a CSS file or section that
+ * defines the visual appearance of the application.
+ *
+ * Theme Naming Convention:
+ * - Format: "{mode}-{name}"
+ * - Modes: 'dark' or 'light'
+ * - Names: descriptive, kebab-case identifiers
+ *
+ * @constant {string[]} THEME_LIST
+ */
+export const THEME_LIST = [
+ /** Dark theme with green forest colors */
+ 'dark-mystic-forest',
+
+ /** Dark theme with red/crimson accent colors */
+ 'dark-crimson-night',
+
+ /** Dark theme with purple/blue royal colors */
+ 'dark-royal-elegance',
+
+ /** Light theme with warm orange/yellow sunrise colors */
+ 'light-sunrise',
+
+ /** Light theme with soft, muted colors */
+ 'light-soft-glow',
+
+ /** Light theme with pastel floral colors */
+ 'light-floral-breeze'
+];
+
+/**
+ * Default Theme Selection
+ *
+ * The theme that is applied when:
+ * - User visits the application for the first time
+ * - No theme preference is stored in localStorage
+ * - The stored theme preference is invalid/corrupted
+ *
+ * This theme should provide the best default experience and represent
+ * the primary visual identity of the application.
+ *
+ * @constant {string} DEFAULT_THEME
+ */
+export const DEFAULT_THEME = 'dark-mystic-forest';
diff --git a/js/core/componentFactory.js b/js/core/componentFactory.js
new file mode 100644
index 0000000..03ae36f
--- /dev/null
+++ b/js/core/componentFactory.js
@@ -0,0 +1,678 @@
+/**
+ * Component Factory and Registry for Compy 2.0
+ *
+ * This module provides a centralized system for creating, managing, and
+ * coordinating application components. It implements the factory pattern
+ * to create component instances and maintains a registry for lifecycle
+ * management and inter-component communication.
+ *
+ * Features:
+ * - Component factory with dependency injection
+ * - Component lifecycle management
+ * - Service locator pattern for component access
+ * - Event-driven inter-component communication
+ * - Automatic dependency resolution
+ * - Component cleanup and memory management
+ *
+ * @fileoverview Central component factory and registry system
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+import { ClipboardManager } from '../components/clipboard.js';
+import { NotificationManager } from '../components/notifications.js';
+import { ModalManager } from '../components/modals.js';
+import { itemService } from '../services/itemService.js';
+
+/**
+ * ComponentFactory creates and manages application components
+ *
+ * This class implements the factory pattern for component creation,
+ * handles dependency injection, and maintains a registry of active
+ * components for lifecycle management.
+ *
+ * @class ComponentFactory
+ * @example
+ * const factory = new ComponentFactory();
+ *
+ * // Create components with automatic dependency resolution
+ * const components = factory.createComponents([
+ * 'notifications',
+ * 'clipboard',
+ * 'modals'
+ * ]);
+ *
+ * // Access components
+ * const clipboardManager = factory.get('clipboard');
+ */
+export class ComponentFactory {
+ /**
+ * Initialize the component factory
+ *
+ * @param {Object} [options={}] - Factory configuration options
+ * @param {boolean} [options.autoCleanup=true] - Automatically cleanup components on page unload
+ * @param {boolean} [options.enableEvents=true] - Enable inter-component event system
+ */
+ constructor(options = {}) {
+ // Factory configuration
+ this.options = {
+ autoCleanup: true,
+ enableEvents: true,
+ ...options
+ };
+
+ // Component registry
+ this.components = new Map(); // Active component instances
+ this.componentDefinitions = new Map(); // Component configuration and factory functions
+ this.dependencyGraph = new Map(); // Component dependency relationships
+ this.eventEmitter = null; // Inter-component event system
+
+ // Lifecycle state
+ this.isInitialized = false;
+ this.isDestroyed = false;
+
+ // Bind methods
+ this.create = this.create.bind(this);
+ this.get = this.get.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ // Initialize the factory
+ this.init();
+ }
+
+ /**
+ * Initialize the component factory
+ *
+ * Sets up component definitions, event system, and automatic cleanup.
+ *
+ * @private
+ */
+ init() {
+ if (this.isInitialized) return;
+
+ // Setup event system if enabled
+ if (this.options.enableEvents) {
+ this.setupEventSystem();
+ }
+
+ // Register core component definitions
+ this.registerCoreComponents();
+
+ // Setup automatic cleanup
+ if (this.options.autoCleanup) {
+ this.setupAutoCleanup();
+ }
+
+ this.isInitialized = true;
+ }
+
+ /**
+ * Setup inter-component event system
+ *
+ * Creates a simple event emitter for component communication.
+ *
+ * @private
+ */
+ setupEventSystem() {
+ this.eventEmitter = {
+ listeners: new Map(),
+
+ on(event, callback) {
+ if (!this.listeners.has(event)) {
+ this.listeners.set(event, new Set());
+ }
+ this.listeners.get(event).add(callback);
+ },
+
+ off(event, callback) {
+ const callbacks = this.listeners.get(event);
+ if (callbacks) {
+ callbacks.delete(callback);
+ }
+ },
+
+ emit(event, data) {
+ const callbacks = this.listeners.get(event);
+ if (callbacks) {
+ callbacks.forEach(callback => {
+ try {
+ callback(data);
+ } catch (error) {
+ console.error(`Event handler error for ${event}:`, error);
+ }
+ });
+ }
+ }
+ };
+ }
+
+ /**
+ * Register core component definitions
+ *
+ * Defines how to create each type of component and their dependencies.
+ *
+ * @private
+ */
+ registerCoreComponents() {
+ // NotificationManager - no dependencies
+ this.registerComponent('notifications', {
+ factory: (deps) => new NotificationManager(),
+ dependencies: [],
+ singleton: true
+ });
+
+ // ClipboardManager - depends on notifications
+ this.registerComponent('clipboard', {
+ factory: (deps) => new ClipboardManager(deps.notifications),
+ dependencies: ['notifications'],
+ singleton: true
+ });
+
+ // ModalManager - no dependencies
+ this.registerComponent('modals', {
+ factory: (deps) => new ModalManager(),
+ dependencies: [],
+ singleton: true
+ });
+
+ // ItemService - singleton service
+ this.registerComponent('itemService', {
+ factory: () => itemService,
+ dependencies: [],
+ singleton: true
+ });
+
+ // ThemeManager - placeholder for future implementation
+ this.registerComponent('theme', {
+ factory: (deps) => this.createThemeManager(deps),
+ dependencies: [],
+ singleton: true
+ });
+
+ // SearchManager - placeholder for future implementation
+ this.registerComponent('search', {
+ factory: (deps) => this.createSearchManager(deps),
+ dependencies: ['itemService'],
+ singleton: true
+ });
+ }
+
+ /**
+ * Register a component definition
+ *
+ * @param {string} name - Component name
+ * @param {Object} definition - Component definition
+ * @param {Function} definition.factory - Factory function to create component
+ * @param {string[]} [definition.dependencies=[]] - Array of dependency component names
+ * @param {boolean} [definition.singleton=true] - Whether component should be a singleton
+ * @param {Function} [definition.cleanup] - Optional cleanup function
+ *
+ * @example
+ * factory.registerComponent('customComponent', {
+ * factory: (deps) => new CustomComponent(deps.notifications),
+ * dependencies: ['notifications'],
+ * singleton: true,
+ * cleanup: (instance) => instance.destroy()
+ * });
+ */
+ registerComponent(name, definition) {
+ // Validate definition
+ if (!definition.factory || typeof definition.factory !== 'function') {
+ throw new Error(`Component ${name} must have a factory function`);
+ }
+
+ // Set defaults
+ const componentDef = {
+ dependencies: [],
+ singleton: true,
+ cleanup: null,
+ ...definition
+ };
+
+ this.componentDefinitions.set(name, componentDef);
+
+ // Update dependency graph
+ if (componentDef.dependencies.length > 0) {
+ this.dependencyGraph.set(name, componentDef.dependencies);
+ }
+ }
+
+ /**
+ * Create a component instance with comprehensive error handling
+ *
+ * Creates the specified component and all its dependencies in the correct order.
+ * Includes robust error handling, dependency validation, and lifecycle management.
+ * Returns the created component instance.
+ *
+ * Enhanced Features:
+ * - Comprehensive input validation and error handling
+ * - Dependency cycle detection and prevention
+ * - Component creation performance monitoring
+ * - Graceful failure recovery with detailed error context
+ * - Memory leak prevention through proper cleanup
+ *
+ * @param {string} componentName - Name of the component to create
+ * @returns {*} Created component instance
+ * @throws {Error} When component cannot be created or dependencies are invalid
+ *
+ * @example
+ * try {
+ * const clipboardManager = factory.create('clipboard');
+ * await clipboardManager.copy('Hello World');
+ * } catch (error) {
+ * console.error('Component creation failed:', error.message);
+ * }
+ */
+ create(componentName) {
+ // Validate factory state
+ if (this.isDestroyed) {
+ throw new Error(`Cannot create components: factory has been destroyed`);
+ }
+
+ // Validate input parameters
+ if (!componentName || typeof componentName !== 'string') {
+ throw new Error(`Invalid component name: expected string, got ${typeof componentName}`);
+ }
+
+ // Check if component definition exists
+ const definition = this.componentDefinitions.get(componentName);
+ if (!definition) {
+ const availableComponents = Array.from(this.componentDefinitions.keys());
+ throw new Error(`Unknown component: "${componentName}". Available components: [${availableComponents.join(', ')}]`);
+ }
+
+ // Return existing singleton if available
+ if (definition.singleton && this.components.has(componentName)) {
+ return this.components.get(componentName);
+ }
+
+ const startTime = performance.now();
+
+ try {
+ // Resolve dependencies with cycle detection
+ const dependencies = this.resolveDependencies(componentName);
+
+ // Create the component with error context
+ let instance;
+ try {
+ instance = definition.factory(dependencies);
+ } catch (error) {
+ throw new Error(`Component factory failed for "${componentName}": ${error.message}`);
+ }
+
+ // Validate created instance
+ if (!instance) {
+ throw new Error(`Component factory for "${componentName}" returned null or undefined`);
+ }
+
+ // Register the component if it's a singleton
+ if (definition.singleton) {
+ this.components.set(componentName, instance);
+ }
+
+ // Performance monitoring
+ const creationTime = performance.now() - startTime;
+ if (creationTime > 100) { // Log slow component creation
+ console.warn(`Slow component creation: "${componentName}" took ${creationTime.toFixed(2)}ms`);
+ }
+
+ // Emit creation event with detailed context
+ if (this.eventEmitter) {
+ this.eventEmitter.emit('component:created', {
+ name: componentName,
+ instance,
+ creationTime,
+ dependencyCount: definition.dependencies.length
+ });
+ }
+
+ return instance;
+
+ } catch (error) {
+ // Enhanced error reporting with context
+ const errorContext = {
+ component: componentName,
+ dependencies: definition.dependencies,
+ singleton: definition.singleton,
+ factoryType: typeof definition.factory,
+ activeComponents: this.components.size
+ };
+
+ console.error('Component creation failed:', error.message, errorContext);
+
+ // Emit error event for monitoring
+ if (this.eventEmitter) {
+ this.eventEmitter.emit('component:error', {
+ name: componentName,
+ error,
+ context: errorContext
+ });
+ }
+
+ // Re-throw with enhanced context
+ throw error;
+ }
+ }
+
+ /**
+ * Create multiple components
+ *
+ * Creates multiple components in dependency order and returns a map
+ * of component names to instances.
+ *
+ * @param {string[]} componentNames - Array of component names to create
+ * @returns {Map} Map of component names to instances
+ *
+ * @example
+ * const components = factory.createComponents(['notifications', 'clipboard', 'modals']);
+ * const clipboardManager = components.get('clipboard');
+ */
+ createComponents(componentNames) {
+ const created = new Map();
+
+ // Sort by dependency order
+ const sortedNames = this.topologicalSort(componentNames);
+
+ for (const name of sortedNames) {
+ const instance = this.create(name);
+ created.set(name, instance);
+ }
+
+ return created;
+ }
+
+ /**
+ * Get a component instance
+ *
+ * Returns an existing component instance or creates it if it doesn't exist
+ * (for singletons only).
+ *
+ * @param {string} componentName - Name of the component to retrieve
+ * @returns {*|null} Component instance or null if not found
+ *
+ * @example
+ * const notifications = factory.get('notifications');
+ * notifications.show('Hello World', 'success');
+ */
+ get(componentName) {
+ const existing = this.components.get(componentName);
+ if (existing) {
+ return existing;
+ }
+
+ // Auto-create singleton components
+ const definition = this.componentDefinitions.get(componentName);
+ if (definition && definition.singleton) {
+ return this.create(componentName);
+ }
+
+ return null;
+ }
+
+ /**
+ * Check if a component exists
+ *
+ * @param {string} componentName - Component name to check
+ * @returns {boolean} True if component is registered
+ */
+ has(componentName) {
+ return this.components.has(componentName);
+ }
+
+ /**
+ * Resolve dependencies for a component
+ *
+ * Creates all required dependencies and returns them as an object.
+ *
+ * @param {string} componentName - Component name
+ * @returns {Object} Object with dependency instances
+ * @private
+ */
+ resolveDependencies(componentName) {
+ const definition = this.componentDefinitions.get(componentName);
+ if (!definition || !definition.dependencies.length) {
+ return {};
+ }
+
+ const dependencies = {};
+
+ for (const depName of definition.dependencies) {
+ // Check for circular dependencies
+ if (this.hasCircularDependency(componentName, depName)) {
+ throw new Error(`Circular dependency detected: ${componentName} -> ${depName}`);
+ }
+
+ // Create or get dependency
+ dependencies[depName] = this.create(depName);
+ }
+
+ return dependencies;
+ }
+
+ /**
+ * Check for circular dependencies
+ *
+ * @param {string} from - Source component
+ * @param {string} to - Target dependency
+ * @param {Set} [visited] - Visited components (for recursion)
+ * @returns {boolean} True if circular dependency exists
+ * @private
+ */
+ hasCircularDependency(from, to, visited = new Set()) {
+ if (visited.has(from)) {
+ return from === to;
+ }
+
+ visited.add(from);
+ const deps = this.dependencyGraph.get(to);
+
+ if (deps) {
+ for (const dep of deps) {
+ if (this.hasCircularDependency(from, dep, visited)) {
+ return true;
+ }
+ }
+ }
+
+ visited.delete(from);
+ return false;
+ }
+
+ /**
+ * Sort components by dependency order (topological sort)
+ *
+ * @param {string[]} componentNames - Component names to sort
+ * @returns {string[]} Sorted component names
+ * @private
+ */
+ topologicalSort(componentNames) {
+ const result = [];
+ const visited = new Set();
+ const visiting = new Set();
+
+ const visit = (name) => {
+ if (visiting.has(name)) {
+ throw new Error(`Circular dependency detected involving: ${name}`);
+ }
+ if (visited.has(name)) {
+ return;
+ }
+
+ visiting.add(name);
+
+ const deps = this.dependencyGraph.get(name);
+ if (deps) {
+ for (const dep of deps) {
+ if (componentNames.includes(dep)) {
+ visit(dep);
+ }
+ }
+ }
+
+ visiting.delete(name);
+ visited.add(name);
+ result.push(name);
+ };
+
+ for (const name of componentNames) {
+ visit(name);
+ }
+
+ return result;
+ }
+
+ /**
+ * Setup automatic cleanup on page unload
+ *
+ * @private
+ */
+ setupAutoCleanup() {
+ window.addEventListener('beforeunload', () => {
+ this.destroy();
+ });
+
+ // Also cleanup on page hide (for mobile browsers)
+ document.addEventListener('visibilitychange', () => {
+ if (document.hidden) {
+ this.cleanup();
+ }
+ });
+ }
+
+ /**
+ * Cleanup components without destroying the factory
+ *
+ * Calls cleanup functions for all components but keeps the factory
+ * ready for creating new components.
+ */
+ cleanup() {
+ for (const [name, instance] of this.components) {
+ this.cleanupComponent(name, instance);
+ }
+ }
+
+ /**
+ * Cleanup a specific component
+ *
+ * @param {string} name - Component name
+ * @param {*} instance - Component instance
+ * @private
+ */
+ cleanupComponent(name, instance) {
+ try {
+ const definition = this.componentDefinitions.get(name);
+
+ if (definition && definition.cleanup) {
+ definition.cleanup(instance);
+ } else if (instance && typeof instance.destroy === 'function') {
+ instance.destroy();
+ } else if (instance && typeof instance.cleanup === 'function') {
+ instance.cleanup();
+ }
+
+ // Emit cleanup event
+ if (this.eventEmitter) {
+ this.eventEmitter.emit('component:cleaned', { name, instance });
+ }
+ } catch (error) {
+ console.error(`Error cleaning up component ${name}:`, error);
+ }
+ }
+
+ /**
+ * Destroy the factory and all components
+ *
+ * Performs complete cleanup and makes the factory unusable.
+ * This is typically called when the application shuts down.
+ */
+ destroy() {
+ if (this.isDestroyed) return;
+
+ // Cleanup all components
+ this.cleanup();
+
+ // Clear registries
+ this.components.clear();
+ this.dependencyGraph.clear();
+
+ // Cleanup event system
+ if (this.eventEmitter) {
+ this.eventEmitter.listeners.clear();
+ this.eventEmitter = null;
+ }
+
+ this.isDestroyed = true;
+ }
+
+ /**
+ * Get factory status and statistics
+ *
+ * @returns {Object} Factory status information
+ *
+ * @example
+ * const status = factory.getStatus();
+ * console.log(`${status.activeComponents} components active`);
+ */
+ getStatus() {
+ return {
+ initialized: this.isInitialized,
+ destroyed: this.isDestroyed,
+ activeComponents: this.components.size,
+ registeredComponents: this.componentDefinitions.size,
+ componentNames: Array.from(this.components.keys())
+ };
+ }
+
+ /**
+ * Placeholder factory for ThemeManager (to be implemented)
+ *
+ * @private
+ */
+ createThemeManager() {
+ // Placeholder - return a simple theme manager
+ return {
+ apply: (theme) => console.log('Theme applied:', theme),
+ current: 'dark-mystic-forest'
+ };
+ }
+
+ /**
+ * Placeholder factory for SearchManager (to be implemented)
+ *
+ * @private
+ */
+ createSearchManager(deps) {
+ // Placeholder - return a simple search manager
+ return {
+ search: (query) => deps.itemService.searchItems(query),
+ focus: () => console.log('Search focused')
+ };
+ }
+}
+
+/**
+ * Global component factory instance
+ *
+ * Provides a singleton factory instance for the entire application.
+ * This is the recommended way to access components throughout the app.
+ */
+export const componentFactory = new ComponentFactory();
+
+/**
+ * Factory function to create a new component factory
+ *
+ * @param {Object} [options={}] - Factory configuration options
+ * @returns {ComponentFactory} New component factory instance
+ *
+ * @example
+ * import { createComponentFactory } from './componentFactory.js';
+ *
+ * const factory = createComponentFactory({
+ * autoCleanup: false,
+ * enableEvents: false
+ * });
+ */
+export const createComponentFactory = (options = {}) => {
+ return new ComponentFactory(options);
+};
diff --git a/js/main.js b/js/main.js
new file mode 100644
index 0000000..22624a0
--- /dev/null
+++ b/js/main.js
@@ -0,0 +1,478 @@
+/**
+ * Main Entry Point for Compy 2.0 Application - Refactored Version
+ *
+ * This module serves as the primary bootstrap file that initializes the entire
+ * Compy 2.0 application using the new modular architecture. It demonstrates
+ * the improved initialization process with better error handling, logging,
+ * and component management.
+ *
+ * Key Improvements in Refactored Version:
+ * - Modular component architecture with dependency injection
+ * - Better error handling and user feedback
+ * - Centralized component management through factory pattern
+ * - Improved logging and debugging capabilities
+ * - Easier testing and maintenance
+ *
+ * @fileoverview Refactored entry point with modular architecture
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+// Import both original and refactored initialization functions
+import { initializeApp } from './app.js';
+import { initializeRefactoredApp } from './app-refactored.js';
+
+/**
+ * Configuration for application initialization
+ *
+ * Set USE_REFACTORED_VERSION to true to use the new modular architecture,
+ * or false to use the original monolithic version for comparison.
+ */
+const CONFIG = {
+ USE_REFACTORED_VERSION: true, // Switch this to false to use original version
+ ENABLE_PERFORMANCE_MONITORING: true,
+ ENABLE_DEBUG_LOGGING: true
+};
+
+/**
+ * Enhanced Application Bootstrap Process
+ *
+ * The initialization process now includes:
+ * - Feature detection and browser compatibility checks
+ * - Performance monitoring setup
+ * - Selection between original and refactored architectures
+ * - Enhanced error handling and user feedback
+ * - Development vs production mode detection
+ */
+document.addEventListener('DOMContentLoaded', async () => {
+ console.log('🌟 Starting Compy 2.0 initialization...');
+
+ // Performance monitoring
+ const startTime = CONFIG.ENABLE_PERFORMANCE_MONITORING ? performance.now() : 0;
+
+ try {
+ // Feature detection
+ if (!checkBrowserCompatibility()) {
+ showCompatibilityWarning();
+ return;
+ }
+
+ // Initialize the application based on configuration
+ let app;
+ if (CONFIG.USE_REFACTORED_VERSION) {
+ console.log('🚀 Using refactored modular architecture');
+ app = await initializeRefactoredApp();
+ } else {
+ console.log('⚡ Using original monolithic architecture');
+ app = await initializeApp();
+ }
+
+ // Performance reporting
+ if (CONFIG.ENABLE_PERFORMANCE_MONITORING) {
+ const endTime = performance.now();
+ const initTime = Math.round(endTime - startTime);
+ console.log(`⚡ Application initialized in ${initTime}ms`);
+
+ // Report performance metrics
+ reportPerformanceMetrics(initTime);
+ }
+
+ // Setup development helpers
+ if (CONFIG.ENABLE_DEBUG_LOGGING) {
+ setupDebugHelpers(app);
+ }
+
+ console.log('✅ Compy 2.0 startup completed successfully!');
+
+ } catch (error) {
+ console.error('❌ Failed to initialize Compy 2.0:', error);
+ handleInitializationFailure(error);
+ }
+});
+
+/**
+ * Check browser compatibility for required features
+ *
+ * @returns {boolean} True if browser is compatible
+ */
+function checkBrowserCompatibility() {
+ const requiredFeatures = [
+ 'localStorage' in window,
+ 'addEventListener' in document,
+ 'querySelector' in document,
+ 'JSON' in window,
+ 'Promise' in window
+ ];
+
+ const isCompatible = requiredFeatures.every(feature => feature);
+
+ if (CONFIG.ENABLE_DEBUG_LOGGING) {
+ console.log('🔍 Browser compatibility check:', {
+ compatible: isCompatible,
+ features: {
+ localStorage: 'localStorage' in window,
+ eventListeners: 'addEventListener' in document,
+ querySelector: 'querySelector' in document,
+ JSON: 'JSON' in window,
+ promises: 'Promise' in window,
+ modules: 'import' in document.createElement('script')
+ }
+ });
+ }
+
+ return isCompatible;
+}
+
+/**
+ * Show compatibility warning for unsupported browsers
+ */
+function showCompatibilityWarning() {
+ const message = 'Your browser is not fully compatible with Compy 2.0. Please update to a modern browser for the best experience.';
+
+ // Try to show a user-friendly warning
+ try {
+ const warningDiv = document.createElement('div');
+ warningDiv.style.cssText = `
+ position: fixed;
+ top: 20px;
+ left: 20px;
+ right: 20px;
+ background: #ff4444;
+ color: white;
+ padding: 15px;
+ border-radius: 5px;
+ z-index: 10000;
+ font-family: Arial, sans-serif;
+ text-align: center;
+ `;
+ warningDiv.textContent = message;
+ document.body.appendChild(warningDiv);
+ } catch (error) {
+ // Fallback to alert if DOM manipulation fails
+ alert(message);
+ }
+
+ console.warn('⚠️ Browser compatibility issue detected');
+}
+
+/**
+ * Report performance metrics
+ *
+ * @param {number} initTime - Initialization time in milliseconds
+ */
+function reportPerformanceMetrics(initTime) {
+ const metrics = {
+ initializationTime: initTime,
+ memoryUsage: performance.memory ? {
+ used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
+ total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
+ limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
+ } : null,
+ timestamp: new Date().toISOString()
+ };
+
+ console.log('📊 Performance Metrics:', metrics);
+
+ // In a production environment, you might send these metrics to a monitoring service
+ // Example: analytics.track('app_initialized', metrics);
+}
+
+/**
+ * Setup development helpers and debugging tools
+ *
+ * @param {*} app - Application instance
+ */
+function setupDebugHelpers(app) {
+ // Make app instance globally available for debugging
+ window.compyApp = app;
+
+ // Add helpful console commands
+ window.compyDebug = {
+ getStatus: () => {
+ if (typeof app.getStatus === 'function') {
+ return app.getStatus();
+ }
+ return { message: 'Status not available for this app version' };
+ },
+
+ getComponents: () => {
+ if (CONFIG.USE_REFACTORED_VERSION && app.components) {
+ return Array.from(app.components.keys());
+ }
+ return ['Components not available in this version'];
+ },
+
+ testNotification: (message = 'Test notification', type = 'info') => {
+ if (CONFIG.USE_REFACTORED_VERSION) {
+ const notifications = app.getComponent('notifications');
+ if (notifications) {
+ notifications.show(message, type);
+ return 'Notification sent';
+ }
+ }
+ return 'Notifications not available';
+ }
+ };
+
+ console.log('🛠️ Debug helpers available:', Object.keys(window.compyDebug));
+ console.log('💡 Try: compyDebug.getStatus(), compyDebug.testNotification()');
+}
+
+/**
+ * Handle initialization failure with enhanced user feedback and error recovery
+ *
+ * Provides comprehensive error handling with multiple fallback options,
+ * detailed error reporting, and user-friendly recovery mechanisms.
+ *
+ * Error Recovery Strategy:
+ * 1. Attempt to show user-friendly modal with recovery options
+ * 2. Fall back to basic alert if DOM manipulation fails
+ * 3. Provide detailed logging for debugging
+ * 4. Offer multiple recovery paths (refresh, clear data, etc.)
+ *
+ * @param {Error} error - The initialization error
+ */
+function handleInitializationFailure(error) {
+ const errorMessage = 'Failed to initialize Compy 2.0. This might be due to corrupted data or browser issues.';
+
+ // Determine error category for better user guidance
+ const errorCategory = categorizeError(error);
+
+ // Try to show enhanced user-friendly error interface
+ try {
+ const errorDiv = createErrorModal(errorMessage, errorCategory);
+ document.body.appendChild(errorDiv);
+
+ // Add keyboard support for accessibility
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ location.reload();
+ } else if (e.key === 'Escape') {
+ errorDiv.remove();
+ }
+ });
+
+ } catch (domError) {
+ // Enhanced fallback with multiple options
+ console.error('Could not show error UI:', domError);
+ const fallbackMessage = `${errorMessage}\n\nOptions:\n1. Refresh the page\n2. Clear browser data if problem persists`;
+
+ if (confirm(`${fallbackMessage}\n\nWould you like to refresh now?`)) {
+ location.reload();
+ }
+ }
+
+ // Enhanced error logging with more context
+ logDetailedError(error, errorCategory);
+}
+
+/**
+ * Create an enhanced error modal with recovery options
+ *
+ * @param {string} message - Primary error message
+ * @param {string} category - Error category for specific guidance
+ * @returns {HTMLElement} Error modal element
+ * @private
+ */
+function createErrorModal(message, category) {
+ const errorDiv = document.createElement('div');
+ errorDiv.style.cssText = `
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: #ff4444;
+ color: white;
+ padding: 30px;
+ border-radius: 12px;
+ z-index: 10000;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ text-align: center;
+ max-width: 450px;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
+ backdrop-filter: blur(10px);
+ `;
+
+ const categoryMessage = getCategorySpecificMessage(category);
+
+ errorDiv.innerHTML = `
+ ⚠️
+ Initialization Error
+ ${message}
+ ${categoryMessage ? `${categoryMessage}
` : ''}
+
+
+ 🔄 Refresh Page
+
+
+ 🗑️ Clear Data & Refresh
+
+
+ Press Enter to refresh or Escape to close
+ `;
+
+ // Add the clearStorageAndReload function to window temporarily
+ window.clearStorageAndReload = () => {
+ try {
+ // Clear localStorage data that might be corrupted
+ localStorage.removeItem('compy.items');
+ localStorage.removeItem('compy.filters');
+ localStorage.removeItem('compy.profile');
+ localStorage.removeItem('compy.theme');
+ localStorage.removeItem('compy.backups');
+
+ // Clear sessionStorage as well
+ sessionStorage.clear();
+
+ // Reload the page
+ location.reload();
+ } catch (clearError) {
+ console.error('Failed to clear storage:', clearError);
+ // Just reload if clearing fails
+ location.reload();
+ }
+ };
+
+ return errorDiv;
+}
+
+/**
+ * Categorize the error for better user guidance
+ *
+ * @param {Error} error - The error to categorize
+ * @returns {string} Error category
+ * @private
+ */
+function categorizeError(error) {
+ const message = error.message.toLowerCase();
+ const stack = error.stack?.toLowerCase() || '';
+
+ if (message.includes('localstorage') || message.includes('quota') || stack.includes('storage')) {
+ return 'storage';
+ }
+
+ if (message.includes('network') || message.includes('fetch') || message.includes('load')) {
+ return 'network';
+ }
+
+ if (message.includes('script') || message.includes('module') || stack.includes('import')) {
+ return 'script';
+ }
+
+ if (message.includes('permission') || message.includes('blocked')) {
+ return 'permission';
+ }
+
+ return 'general';
+}
+
+/**
+ * Get category-specific guidance message
+ *
+ * @param {string} category - Error category
+ * @returns {string} Category-specific message
+ * @private
+ */
+function getCategorySpecificMessage(category) {
+ const messages = {
+ storage: 'This might be caused by corrupted browser data. Try clearing data to fix the issue.',
+ network: 'This appears to be a network-related issue. Check your connection and try refreshing.',
+ script: 'There was a problem loading the application scripts. A refresh should resolve this.',
+ permission: 'The browser blocked some required functionality. Check browser settings and refresh.',
+ general: 'An unexpected error occurred during startup. A refresh usually resolves this issue.'
+ };
+
+ return messages[category] || messages.general;
+}
+
+/**
+ * Log detailed error information for debugging
+ *
+ * @param {Error} error - The error to log
+ * @param {string} category - Error category
+ * @private
+ */
+function logDetailedError(error, category) {
+ const errorDetails = {
+ // Basic error information
+ message: error.message,
+ stack: error.stack,
+ category,
+
+ // Timing information
+ timestamp: new Date().toISOString(),
+
+ // Browser environment
+ userAgent: navigator.userAgent,
+ language: navigator.language,
+ platform: navigator.platform,
+
+ // Page context
+ url: window.location.href,
+ referrer: document.referrer,
+
+ // Storage availability
+ localStorageAvailable: isStorageAvailable('localStorage'),
+ sessionStorageAvailable: isStorageAvailable('sessionStorage'),
+
+ // Memory information (if available)
+ memory: performance.memory ? {
+ used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
+ total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
+ limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
+ } : null
+ };
+
+ console.error('🚨 Compy 2.0 Initialization Failure:', errorDetails);
+
+ // Group related console entries
+ console.group('Error Analysis');
+ console.error('Category:', category);
+ console.error('Browser Support:', {
+ modules: 'import' in document.createElement('script'),
+ promises: 'Promise' in window,
+ localStorage: isStorageAvailable('localStorage'),
+ clipboard: 'clipboard' in navigator
+ });
+ console.groupEnd();
+}
+
+/**
+ * Check if a storage type is available and functional
+ *
+ * @param {string} type - Storage type ('localStorage' or 'sessionStorage')
+ * @returns {boolean} True if storage is available
+ * @private
+ */
+function isStorageAvailable(type) {
+ try {
+ const storage = window[type];
+ const testKey = '__test__';
+ storage.setItem(testKey, 'test');
+ storage.removeItem(testKey);
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
diff --git a/js/performance.js b/js/performance.js
new file mode 100644
index 0000000..f1804ed
--- /dev/null
+++ b/js/performance.js
@@ -0,0 +1,962 @@
+/**
+ * Performance Optimization Utilities for Compy 2.0
+ *
+ * This module provides a comprehensive set of performance optimization tools
+ * designed to improve the user experience of the Compy 2.0 application. These
+ * utilities help reduce memory usage, improve scroll performance, and manage
+ * DOM operations efficiently.
+ *
+ * Key Features:
+ * - Lazy loading with Intersection Observer API
+ * - Virtual scrolling for large lists
+ * - Animation frame-based task scheduling
+ * - DOM operation batching to prevent layout thrashing
+ * - Event delegation for memory efficiency
+ * - Resource preloading and caching
+ *
+ * @fileoverview Advanced performance optimization utilities
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+// =============================================================================
+// LAZY LOADING UTILITIES
+// =============================================================================
+
+/**
+ * Lazy Loading Implementation using Intersection Observer
+ *
+ * This class provides efficient lazy loading functionality for images, content,
+ * or any custom loading operations. It uses the modern Intersection Observer API
+ * to detect when elements come into view, providing better performance than
+ * traditional scroll-based lazy loading.
+ *
+ * Features:
+ * - Configurable root margin and thresholds
+ * - Support for both image lazy loading and custom load functions
+ * - Memory efficient using WeakSet for loaded elements tracking
+ * - Automatic cleanup after loading
+ *
+ * Use Cases:
+ * - Lazy loading images in large lists
+ * - Deferring expensive DOM operations until needed
+ * - Progressive content loading as user scrolls
+ * - Reducing initial page load time
+ *
+ * @class LazyLoader
+ * @example
+ * // Basic image lazy loading
+ * const loader = new LazyLoader();
+ * document.querySelectorAll('img[data-src]').forEach(img => {
+ * loader.observe(img);
+ * });
+ *
+ * // Custom loading with callback
+ * const loader = new LazyLoader({ rootMargin: '100px' });
+ * loader.observe(element, (el) => {
+ * // Custom loading logic
+ * el.innerHTML = generateExpensiveContent();
+ * });
+ */
+export class LazyLoader {
+ /**
+ * Initialize the lazy loader with configuration options
+ *
+ * @param {Object} [options={}] - Configuration options
+ * @param {string} [options.rootMargin='50px'] - Margin around root for intersection
+ * @param {number} [options.threshold=0.1] - Percentage of element visibility to trigger loading
+ * @param {Element} [options.root=null] - Root element for intersection (null = viewport)
+ */
+ constructor(options = {}) {
+ // Merge user options with sensible defaults
+ this.options = {
+ rootMargin: '50px', // Load elements 50px before they become visible
+ threshold: 0.1, // Trigger when 10% of element is visible
+ ...options
+ };
+
+ // Create Intersection Observer with bound callback
+ this.observer = new IntersectionObserver(
+ this.handleIntersection.bind(this),
+ this.options
+ );
+
+ // Track loaded elements to prevent duplicate loading
+ // WeakSet automatically handles garbage collection
+ this.loadedElements = new WeakSet();
+ }
+
+ /**
+ * Handle intersection events from the observer
+ *
+ * This callback is triggered whenever observed elements enter or leave
+ * the intersection threshold. It only processes elements that are entering
+ * the viewport and haven't been loaded yet.
+ *
+ * @param {IntersectionObserverEntry[]} entries - Array of intersection entries
+ * @private
+ */
+ handleIntersection(entries) {
+ entries.forEach(entry => {
+ // Only process elements entering the viewport that haven't been loaded
+ if (entry.isIntersecting && !this.loadedElements.has(entry.target)) {
+ // Trigger the loading process
+ this.loadElement(entry.target);
+
+ // Mark element as loaded to prevent duplicate loading
+ this.loadedElements.add(entry.target);
+
+ // Stop observing this element (cleanup)
+ this.observer.unobserve(entry.target);
+ }
+ });
+ }
+
+ /**
+ * Load an element using either custom function or default image loading
+ *
+ * This method supports two loading mechanisms:
+ * 1. Custom loading function attached to the element
+ * 2. Default image loading using data-src attribute
+ *
+ * @param {Element} element - Element to load
+ * @private
+ */
+ loadElement(element) {
+ // Check for custom loading function first
+ const loadFn = element._lazyLoadFn;
+ if (loadFn) {
+ // Execute custom loading function
+ loadFn(element);
+ } else if (element.dataset.src) {
+ // Default image loading: move data-src to src
+ element.src = element.dataset.src;
+ element.removeAttribute('data-src');
+ }
+ }
+
+ /**
+ * Start observing an element for lazy loading
+ *
+ * @param {Element} element - Element to observe
+ * @param {Function} [loadFn=null] - Optional custom loading function
+ *
+ * @example
+ * // Observe image with data-src
+ * loader.observe(imgElement);
+ *
+ * // Observe with custom loading function
+ * loader.observe(element, (el) => {
+ * el.innerHTML = 'Content loaded!
';
+ * });
+ */
+ observe(element, loadFn = null) {
+ // Attach custom loading function if provided
+ if (loadFn) {
+ element._lazyLoadFn = loadFn;
+ }
+
+ // Start observing the element
+ this.observer.observe(element);
+ }
+
+ /**
+ * Stop observing a specific element
+ *
+ * @param {Element} element - Element to stop observing
+ */
+ unobserve(element) {
+ this.observer.unobserve(element);
+ }
+
+ /**
+ * Disconnect the observer and clean up resources
+ *
+ * Call this when the lazy loader is no longer needed to prevent
+ * memory leaks and free up resources.
+ */
+ disconnect() {
+ this.observer.disconnect();
+ this.loadedElements = new WeakSet();
+ }
+}
+
+// =============================================================================
+// VIRTUAL SCROLLING UTILITIES
+// =============================================================================
+
+/**
+ * Virtual Scrolling Implementation for Large Lists
+ *
+ * Virtual scrolling is a technique for handling very large lists efficiently
+ * by only rendering the items that are currently visible in the viewport.
+ * This dramatically reduces DOM nodes and improves performance for lists
+ * with thousands of items.
+ *
+ * How it works:
+ * 1. Calculate which items should be visible based on scroll position
+ * 2. Render only visible items plus a small buffer
+ * 3. Position items absolutely to maintain scroll appearance
+ * 4. Update rendered items as user scrolls
+ *
+ * Performance Benefits:
+ * - Constant rendering performance regardless of list size
+ * - Reduced memory usage (fewer DOM nodes)
+ * - Smooth scrolling even with 10,000+ items
+ * - Better browser responsiveness
+ *
+ * @class VirtualScroller
+ * @example
+ * const scroller = new VirtualScroller(container, {
+ * itemHeight: 80,
+ * bufferSize: 10
+ * });
+ *
+ * scroller.setItems(largeItemArray, (item, index) => {
+ * const div = document.createElement('div');
+ * div.textContent = item.name;
+ * return div;
+ * });
+ */
+export class VirtualScroller {
+ /**
+ * Initialize virtual scroller
+ *
+ * @param {Element} container - Container element for the virtual list
+ * @param {Object} [options={}] - Configuration options
+ * @param {number} [options.itemHeight=100] - Fixed height of each item in pixels
+ * @param {number} [options.bufferSize=5] - Number of extra items to render outside viewport
+ */
+ constructor(container, options = {}) {
+ this.container = container;
+ this.options = {
+ itemHeight: 100, // Fixed height per item (required for calculations)
+ bufferSize: 5, // Extra items to render for smooth scrolling
+ ...options
+ };
+
+ // Initialize state
+ this.items = []; // Data items to virtualize
+ this.visibleItems = []; // Currently rendered items
+ this.scrollTop = 0; // Current scroll position
+ this.containerHeight = 0; // Container viewport height
+
+ this.init();
+ }
+
+ /**
+ * Initialize the virtual scroller setup
+ *
+ * Sets up the container styling, creates viewport element,
+ * and attaches necessary event listeners.
+ *
+ * @private
+ */
+ init() {
+ // Setup container for virtual scrolling
+ this.container.style.position = 'relative';
+ this.container.style.overflow = 'auto';
+
+ // Create viewport element for rendered items
+ this.viewport = document.createElement('div');
+ this.viewport.style.position = 'absolute';
+ this.viewport.style.top = '0';
+ this.viewport.style.left = '0';
+ this.viewport.style.right = '0';
+ this.container.appendChild(this.viewport);
+
+ // Attach scroll listener for viewport updates
+ this.container.addEventListener('scroll', this.handleScroll.bind(this));
+
+ // Setup resize observer if available (for responsive layouts)
+ if (window.ResizeObserver) {
+ this.resizeObserver = new ResizeObserver(this.handleResize.bind(this));
+ this.resizeObserver.observe(this.container);
+ }
+ }
+
+ /**
+ * Handle scroll events to update visible items
+ *
+ * @private
+ */
+ handleScroll() {
+ this.scrollTop = this.container.scrollTop;
+ this.render();
+ }
+
+ /**
+ * Handle container resize events
+ *
+ * @private
+ */
+ handleResize() {
+ this.containerHeight = this.container.clientHeight;
+ this.render();
+ }
+
+ /**
+ * Set the items and render function for virtual scrolling
+ *
+ * @param {Array} items - Array of data items to virtualize
+ * @param {Function} renderFn - Function to render each item (item, index) => Element
+ *
+ * @example
+ * scroller.setItems(users, (user, index) => {
+ * const div = document.createElement('div');
+ * div.innerHTML = `${user.name} ${user.email}
`;
+ * return div;
+ * });
+ */
+ setItems(items, renderFn) {
+ this.items = items;
+ this.renderItem = renderFn;
+
+ // Update container total height based on item count
+ const totalHeight = items.length * this.options.itemHeight;
+ this.container.style.height = `${totalHeight}px`;
+
+ // Initial render
+ this.render();
+ }
+
+ /**
+ * Render the currently visible items
+ *
+ * Calculates which items should be visible based on scroll position
+ * and renders them in the correct positions.
+ *
+ * @private
+ */
+ render() {
+ const { itemHeight, bufferSize } = this.options;
+ const containerHeight = this.containerHeight || this.container.clientHeight;
+
+ // Calculate visible range with buffer
+ const startIndex = Math.max(0,
+ Math.floor(this.scrollTop / itemHeight) - bufferSize
+ );
+ const endIndex = Math.min(this.items.length - 1,
+ Math.floor((this.scrollTop + containerHeight) / itemHeight) + bufferSize
+ );
+
+ // Clear viewport
+ this.viewport.innerHTML = '';
+
+ // Render visible items with absolute positioning
+ for (let i = startIndex; i <= endIndex; i++) {
+ const item = this.items[i];
+ const element = this.renderItem(item, i);
+
+ // Position element absolutely based on item index
+ element.style.position = 'absolute';
+ element.style.top = `${i * itemHeight}px`;
+ element.style.height = `${itemHeight}px`;
+ element.style.width = '100%';
+
+ this.viewport.appendChild(element);
+ }
+ }
+
+ /**
+ * Clean up and destroy the virtual scroller
+ *
+ * Removes event listeners and cleans up resources.
+ */
+ destroy() {
+ // Remove event listeners
+ this.container.removeEventListener('scroll', this.handleScroll.bind(this));
+
+ // Disconnect resize observer
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ }
+
+ // Remove viewport element
+ this.viewport.remove();
+ }
+}
+
+// =============================================================================
+// TASK SCHEDULING AND ANIMATION FRAME UTILITIES
+// =============================================================================
+
+/**
+ * Request Animation Frame-based Task Scheduler
+ *
+ * This scheduler manages task execution using requestAnimationFrame to ensure
+ * smooth performance and maintain 60fps frame rates. It's perfect for breaking
+ * down heavy operations into smaller chunks that don't block the main thread.
+ *
+ * Features:
+ * - Priority-based task ordering
+ * - Time-sliced execution (16ms budget per frame)
+ * - Automatic frame scheduling
+ * - Error handling for individual tasks
+ *
+ * Use Cases:
+ * - Breaking down large data processing operations
+ * - Rendering large lists without blocking UI
+ * - Smooth animations and transitions
+ * - Background processing tasks
+ *
+ * @class TaskScheduler
+ * @example
+ * const scheduler = new TaskScheduler();
+ *
+ * // Schedule high-priority task
+ * scheduler.schedule(() => {
+ * updateCriticalUI();
+ * }, 10);
+ *
+ * // Schedule normal priority task
+ * scheduler.schedule(() => {
+ * processBackgroundData();
+ * }, 0);
+ */
+export class TaskScheduler {
+ /**
+ * Initialize task scheduler
+ */
+ constructor() {
+ this.tasks = []; // Queue of pending tasks
+ this.isRunning = false; // Flag to prevent multiple execution loops
+ }
+
+ /**
+ * Schedule a task to run in upcoming animation frames
+ *
+ * Tasks are sorted by priority (higher numbers run first) and executed
+ * in time-sliced chunks to maintain smooth frame rates.
+ *
+ * @param {Function} task - Function to execute
+ * @param {number} [priority=0] - Task priority (higher = more urgent)
+ *
+ * @example
+ * // High priority UI update
+ * scheduler.schedule(() => updateUI(), 10);
+ *
+ * // Background processing
+ * scheduler.schedule(() => processData(), 1);
+ */
+ schedule(task, priority = 0) {
+ // Add task to queue with priority
+ this.tasks.push({ task, priority });
+
+ // Sort by priority (descending - higher priorities first)
+ this.tasks.sort((a, b) => b.priority - a.priority);
+
+ // Start processing if not already running
+ if (!this.isRunning) {
+ this.start();
+ }
+ }
+
+ /**
+ * Start the task processing loop
+ *
+ * @private
+ */
+ start() {
+ if (this.isRunning) return;
+
+ this.isRunning = true;
+ this.processTasks();
+ }
+
+ /**
+ * Process tasks within animation frame budget
+ *
+ * Uses time-slicing to execute as many tasks as possible within the
+ * 16ms frame budget (to maintain 60fps). Continues in next frame if
+ * more tasks remain.
+ *
+ * @private
+ */
+ processTasks() {
+ // Stop if no tasks remaining
+ if (this.tasks.length === 0) {
+ this.isRunning = false;
+ return;
+ }
+
+ requestAnimationFrame(() => {
+ const startTime = performance.now();
+ const maxTime = 16; // 16ms budget for 60fps
+
+ // Execute tasks until time budget is exhausted
+ while (this.tasks.length > 0 && (performance.now() - startTime) < maxTime) {
+ const { task } = this.tasks.shift();
+
+ try {
+ task();
+ } catch (error) {
+ console.error('TaskScheduler: Task execution failed:', error);
+ // Continue with other tasks even if one fails
+ }
+ }
+
+ // Continue processing in next frame if tasks remain
+ this.processTasks();
+ });
+ }
+
+ /**
+ * Clear all scheduled tasks
+ *
+ * Useful for cleanup or canceling pending operations.
+ */
+ clear() {
+ this.tasks = [];
+ }
+}
+
+// =============================================================================
+// DOM BATCHING UTILITIES
+// =============================================================================
+
+/**
+ * DOM Operation Batcher for Layout Thrashing Prevention
+ *
+ * This utility prevents layout thrashing by batching DOM read and write
+ * operations and executing them in the optimal order. All reads are performed
+ * first, followed by all writes, within a single animation frame.
+ *
+ * Layout Thrashing occurs when:
+ * 1. Read DOM property (forces layout calculation)
+ * 2. Write DOM property (invalidates layout)
+ * 3. Read DOM property again (forces layout recalculation)
+ *
+ * The batcher solves this by separating reads from writes.
+ *
+ * @class DOMBatcher
+ * @example
+ * const batcher = new DOMBatcher();
+ *
+ * // Schedule reads
+ * batcher.read(() => {
+ * const height = element.offsetHeight;
+ * console.log('Height:', height);
+ * });
+ *
+ * // Schedule writes
+ * batcher.write(() => {
+ * element.style.height = '200px';
+ * });
+ */
+export class DOMBatcher {
+ /**
+ * Initialize DOM batcher
+ */
+ constructor() {
+ this.reads = []; // Queue of DOM read operations
+ this.writes = []; // Queue of DOM write operations
+ this.scheduled = false; // Flag to prevent duplicate scheduling
+ }
+
+ /**
+ * Schedule a DOM read operation
+ *
+ * Read operations access DOM properties that may trigger layout calculations.
+ * Examples: offsetWidth, offsetHeight, getComputedStyle, getBoundingClientRect
+ *
+ * @param {Function} fn - Function performing DOM reads
+ *
+ * @example
+ * batcher.read(() => {
+ * const rect = element.getBoundingClientRect();
+ * console.log('Element position:', rect.x, rect.y);
+ * });
+ */
+ read(fn) {
+ this.reads.push(fn);
+ this.schedule();
+ }
+
+ /**
+ * Schedule a DOM write operation
+ *
+ * Write operations modify DOM properties that invalidate layout.
+ * Examples: style.width, style.height, classList.add, innerHTML
+ *
+ * @param {Function} fn - Function performing DOM writes
+ *
+ * @example
+ * batcher.write(() => {
+ * element.style.transform = 'translateX(100px)';
+ * element.classList.add('active');
+ * });
+ */
+ write(fn) {
+ this.writes.push(fn);
+ this.schedule();
+ }
+
+ /**
+ * Schedule batch processing for next animation frame
+ *
+ * @private
+ */
+ schedule() {
+ if (this.scheduled) return;
+
+ this.scheduled = true;
+ requestAnimationFrame(() => {
+ this.flush();
+ });
+ }
+
+ /**
+ * Execute all batched operations in optimal order
+ *
+ * Executes all reads first to get consistent measurements,
+ * then all writes to apply changes efficiently.
+ *
+ * @private
+ */
+ flush() {
+ // Execute all reads first to avoid layout thrashing
+ this.reads.forEach(read => {
+ try {
+ read();
+ } catch (error) {
+ console.error('DOMBatcher: Read operation failed:', error);
+ }
+ });
+
+ // Then execute all writes
+ this.writes.forEach(write => {
+ try {
+ write();
+ } catch (error) {
+ console.error('DOMBatcher: Write operation failed:', error);
+ }
+ });
+
+ // Reset for next batch
+ this.reads = [];
+ this.writes = [];
+ this.scheduled = false;
+ }
+}
+
+// =============================================================================
+// EVENT DELEGATION UTILITIES
+// =============================================================================
+
+/**
+ * Memory Efficient Event Delegation System
+ *
+ * Event delegation reduces memory usage by using a single event listener
+ * on a parent element to handle events for multiple child elements.
+ * This is especially beneficial for dynamic content where elements are
+ * frequently added or removed.
+ *
+ * Benefits:
+ * - Reduced memory footprint (fewer event listeners)
+ * - Works with dynamically added elements
+ * - Better performance for large lists
+ * - Automatic cleanup when elements are removed
+ *
+ * @class EventDelegator
+ * @example
+ * const delegator = new EventDelegator(document.body);
+ *
+ * // Handle clicks on any button
+ * delegator.on('click', 'button.action', (event, element) => {
+ * console.log('Button clicked:', element.textContent);
+ * });
+ *
+ * // Handle hover on cards
+ * delegator.on('mouseenter', '.card', (event, element) => {
+ * element.classList.add('hovered');
+ * });
+ */
+export class EventDelegator {
+ /**
+ * Initialize event delegator
+ *
+ * @param {Element} [root=document] - Root element for event delegation
+ */
+ constructor(root = document) {
+ this.root = root; // Root element for delegation
+ this.handlers = new Map(); // Map of event types to handlers
+ }
+
+ /**
+ * Add delegated event listener
+ *
+ * @param {string} eventType - Event type (e.g., 'click', 'mouseenter')
+ * @param {string} selector - CSS selector for target elements
+ * @param {Function} handler - Event handler function (event, target) => void
+ *
+ * @example
+ * // Handle clicks on delete buttons
+ * delegator.on('click', '[data-action="delete"]', (event, button) => {
+ * const itemId = button.dataset.itemId;
+ * deleteItem(itemId);
+ * });
+ */
+ on(eventType, selector, handler) {
+ // Initialize event type map if needed
+ if (!this.handlers.has(eventType)) {
+ this.handlers.set(eventType, new Map());
+ // Add single listener for this event type
+ this.root.addEventListener(eventType, this.handleEvent.bind(this, eventType));
+ }
+
+ // Store handler for this selector
+ this.handlers.get(eventType).set(selector, handler);
+ }
+
+ /**
+ * Remove delegated event listener
+ *
+ * @param {string} eventType - Event type to remove
+ * @param {string} selector - CSS selector to stop handling
+ */
+ off(eventType, selector) {
+ const eventHandlers = this.handlers.get(eventType);
+ if (eventHandlers) {
+ eventHandlers.delete(selector);
+
+ // Remove event listener if no handlers remain
+ if (eventHandlers.size === 0) {
+ this.handlers.delete(eventType);
+ this.root.removeEventListener(eventType, this.handleEvent.bind(this, eventType));
+ }
+ }
+ }
+
+ /**
+ * Handle delegated events by matching selectors
+ *
+ * @param {string} eventType - Type of event that occurred
+ * @param {Event} event - Event object
+ * @private
+ */
+ handleEvent(eventType, event) {
+ const eventHandlers = this.handlers.get(eventType);
+ if (!eventHandlers) return;
+
+ // Check each registered selector against the event target
+ for (const [selector, handler] of eventHandlers) {
+ const target = event.target.closest(selector);
+ if (target) {
+ handler(event, target);
+ }
+ }
+ }
+
+ /**
+ * Clean up and remove all event listeners
+ */
+ destroy() {
+ for (const eventType of this.handlers.keys()) {
+ this.root.removeEventListener(eventType, this.handleEvent.bind(this, eventType));
+ }
+ this.handlers.clear();
+ }
+}
+
+// =============================================================================
+// RESOURCE PRELOADING UTILITIES
+// =============================================================================
+
+/**
+ * Resource Preloader with Intelligent Caching
+ *
+ * This utility preloads and caches resources (images, JSON, etc.) to improve
+ * perceived performance. It prevents duplicate requests and provides a simple
+ * interface for resource management.
+ *
+ * Features:
+ * - Prevents duplicate requests for the same resource
+ * - Supports different resource types (image, JSON, generic)
+ * - Intelligent waiting for ongoing requests
+ * - Memory-efficient caching with Map
+ *
+ * @class ResourcePreloader
+ * @example
+ * const preloader = new ResourcePreloader();
+ *
+ * // Preload images
+ * await preloader.preload('/images/hero.jpg', 'image');
+ *
+ * // Preload JSON data
+ * const data = await preloader.preload('/api/config', 'json');
+ *
+ * // Get cached resource
+ * const cachedImage = preloader.get('/images/hero.jpg');
+ */
+export class ResourcePreloader {
+ /**
+ * Initialize resource preloader
+ */
+ constructor() {
+ this.cache = new Map(); // Cache for loaded resources
+ this.loading = new Set(); // Track currently loading resources
+ }
+
+ /**
+ * Preload a resource and cache it
+ *
+ * @param {string} url - Resource URL to preload
+ * @param {string} [type='fetch'] - Resource type: 'image', 'json', or 'fetch'
+ * @returns {Promise} Promise resolving to the loaded resource
+ *
+ * @example
+ * // Preload and cache an image
+ * const image = await preloader.preload('/hero.jpg', 'image');
+ *
+ * // Preload JSON configuration
+ * const config = await preloader.preload('/config.json', 'json');
+ */
+ async preload(url, type = 'fetch') {
+ // Return cached resource if available
+ if (this.cache.has(url)) {
+ return this.cache.get(url);
+ }
+
+ // Wait for existing load if in progress
+ if (this.loading.has(url)) {
+ return new Promise(resolve => {
+ const check = () => {
+ if (this.cache.has(url)) {
+ resolve(this.cache.get(url));
+ } else {
+ setTimeout(check, 10);
+ }
+ };
+ check();
+ });
+ }
+
+ // Mark as loading to prevent duplicates
+ this.loading.add(url);
+
+ try {
+ let resource;
+
+ // Load resource based on type
+ switch (type) {
+ case 'image':
+ resource = await this.preloadImage(url);
+ break;
+ case 'json':
+ resource = await this.preloadJSON(url);
+ break;
+ default:
+ resource = await this.preloadGeneric(url);
+ }
+
+ // Cache the loaded resource
+ this.cache.set(url, resource);
+ return resource;
+
+ } finally {
+ // Always cleanup loading flag
+ this.loading.delete(url);
+ }
+ }
+
+ /**
+ * Preload an image resource
+ *
+ * @param {string} url - Image URL
+ * @returns {Promise} Promise resolving to loaded image
+ * @private
+ */
+ preloadImage(url) {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => resolve(img);
+ img.onerror = reject;
+ img.src = url;
+ });
+ }
+
+ /**
+ * Preload and parse JSON resource
+ *
+ * @param {string} url - JSON URL
+ * @returns {Promise} Promise resolving to parsed JSON data
+ * @private
+ */
+ async preloadJSON(url) {
+ const response = await fetch(url);
+ return response.json();
+ }
+
+ /**
+ * Preload generic resource using fetch
+ *
+ * @param {string} url - Resource URL
+ * @returns {Promise} Promise resolving to fetch response
+ * @private
+ */
+ async preloadGeneric(url) {
+ return fetch(url);
+ }
+
+ /**
+ * Get cached resource without loading
+ *
+ * @param {string} url - Resource URL
+ * @returns {any|undefined} Cached resource or undefined if not cached
+ */
+ get(url) {
+ return this.cache.get(url);
+ }
+
+ /**
+ * Clear all cached resources
+ *
+ * Useful for memory management or cache invalidation.
+ */
+ clear() {
+ this.cache.clear();
+ this.loading.clear();
+ }
+}
+
+// =============================================================================
+// UTILITY FACTORY FUNCTION
+// =============================================================================
+
+/**
+ * Create a collection of performance utilities
+ *
+ * This factory function creates instances of all performance utilities
+ * with sensible defaults, providing a convenient way to access all
+ * performance tools in a single object.
+ *
+ * @returns {Object} Object containing all performance utility instances
+ *
+ * @example
+ * const perf = createPerformanceUtils();
+ *
+ * // Use lazy loader
+ * perf.lazyLoader.observe(imageElement);
+ *
+ * // Schedule tasks
+ * perf.taskScheduler.schedule(() => processData(), 5);
+ *
+ * // Batch DOM operations
+ * perf.domBatcher.read(() => console.log(element.offsetWidth));
+ * perf.domBatcher.write(() => element.style.width = '200px');
+ *
+ * // Delegate events
+ * perf.eventDelegator.on('click', '.button', handleClick);
+ *
+ * // Preload resources
+ * await perf.resourcePreloader.preload('/image.jpg', 'image');
+ */
+export const createPerformanceUtils = () => ({
+ lazyLoader: new LazyLoader(),
+ taskScheduler: new TaskScheduler(),
+ domBatcher: new DOMBatcher(),
+ eventDelegator: new EventDelegator(),
+ resourcePreloader: new ResourcePreloader()
+});
diff --git a/js/services/itemService.js b/js/services/itemService.js
new file mode 100644
index 0000000..e1d0e56
--- /dev/null
+++ b/js/services/itemService.js
@@ -0,0 +1,574 @@
+/**
+ * Item Service for Compy 2.0
+ *
+ * This module provides a service layer for all item-related operations, including
+ * CRUD operations, validation, filtering, and business logic. It acts as an
+ * abstraction layer between the UI components and the state management system.
+ *
+ * Features:
+ * - Complete CRUD operations for items
+ * - Data validation and sanitization
+ * - Advanced filtering and searching
+ * - Import/export functionality
+ * - Business logic encapsulation
+ *
+ * @fileoverview Service layer for item management operations
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+import {
+ upsertItem,
+ deleteItem,
+ getState,
+ setEditingId
+} from '../state.js';
+import {
+ validateItem,
+ filterItems,
+ getAllTags,
+ parseCSVLine,
+ csvEscape
+} from '../utils.js';
+import { generateUID } from '../utils.js';
+
+/**
+ * ItemService handles all item-related business operations
+ *
+ * This class encapsulates the business logic for managing snippet items,
+ * providing a clean API for UI components while maintaining separation
+ * of concerns from state management.
+ *
+ * @class ItemService
+ * @example
+ * const itemService = new ItemService();
+ *
+ * // Create new item
+ * const result = itemService.createItem({
+ * text: 'console.log("Hello World")',
+ * desc: 'Basic logging command',
+ * tags: ['javascript', 'debug']
+ * });
+ *
+ * // Search and filter items
+ * const filtered = itemService.searchItems('console', ['javascript']);
+ */
+export class ItemService {
+ /**
+ * Initialize the item service
+ */
+ constructor() {
+ // Bind methods to maintain context
+ this.createItem = this.createItem.bind(this);
+ this.updateItem = this.updateItem.bind(this);
+ this.removeItem = this.removeItem.bind(this);
+ this.searchItems = this.searchItems.bind(this);
+ }
+
+ /**
+ * Create a new item
+ *
+ * Validates the item data, assigns a unique ID, and saves it to state.
+ * Returns the result of the operation including any validation errors.
+ *
+ * @param {Object} itemData - Item data to create
+ * @param {string} itemData.text - Snippet content
+ * @param {string} itemData.desc - Snippet description
+ * @param {boolean} [itemData.sensitive=false] - Whether snippet is sensitive
+ * @param {string[]} [itemData.tags=[]] - Array of tags
+ * @returns {Object} Operation result with success flag and data/errors
+ *
+ * @example
+ * const result = itemService.createItem({
+ * text: 'git status',
+ * desc: 'Check git repository status',
+ * tags: ['git', 'version-control']
+ * });
+ *
+ * if (result.success) {
+ * console.log('Item created:', result.data.id);
+ * } else {
+ * console.error('Validation errors:', result.errors);
+ * }
+ */
+ createItem(itemData) {
+ // Validate the input data
+ const validation = this.validateItemData(itemData);
+ if (!validation.isValid) {
+ return {
+ success: false,
+ errors: validation.errors,
+ data: null
+ };
+ }
+
+ // Create the item object
+ const item = this.sanitizeItemData({
+ text: itemData.text,
+ desc: itemData.desc,
+ sensitive: Boolean(itemData.sensitive),
+ tags: this.normalizeTags(itemData.tags || [])
+ });
+
+ try {
+ // Clear editing state and create new item
+ setEditingId(null);
+ upsertItem(item);
+
+ return {
+ success: true,
+ errors: [],
+ data: item
+ };
+ } catch (error) {
+ console.error('Failed to create item:', error);
+ return {
+ success: false,
+ errors: ['Failed to create item. Please try again.'],
+ data: null
+ };
+ }
+ }
+
+ /**
+ * Update an existing item
+ *
+ * Finds the item by ID, validates the updated data, and saves changes.
+ * Preserves existing data for fields not provided in the update.
+ *
+ * @param {string} itemId - ID of the item to update
+ * @param {Object} updates - Partial item data to update
+ * @returns {Object} Operation result with success flag and data/errors
+ *
+ * @example
+ * const result = itemService.updateItem('abc123', {
+ * desc: 'Updated description',
+ * tags: ['git', 'cli', 'status']
+ * });
+ */
+ updateItem(itemId, updates) {
+ // Find the existing item
+ const existingItem = this.getItemById(itemId);
+ if (!existingItem) {
+ return {
+ success: false,
+ errors: ['Item not found'],
+ data: null
+ };
+ }
+
+ // Merge updates with existing data
+ const updatedData = {
+ ...existingItem,
+ ...updates,
+ tags: updates.tags ? this.normalizeTags(updates.tags) : existingItem.tags
+ };
+
+ // Validate the updated data
+ const validation = this.validateItemData(updatedData);
+ if (!validation.isValid) {
+ return {
+ success: false,
+ errors: validation.errors,
+ data: null
+ };
+ }
+
+ try {
+ // Set editing ID and update the item
+ setEditingId(itemId);
+ const sanitizedItem = this.sanitizeItemData(updatedData);
+ upsertItem(sanitizedItem);
+
+ return {
+ success: true,
+ errors: [],
+ data: sanitizedItem
+ };
+ } catch (error) {
+ console.error('Failed to update item:', error);
+ return {
+ success: false,
+ errors: ['Failed to update item. Please try again.'],
+ data: null
+ };
+ }
+ }
+
+ /**
+ * Remove an item by ID
+ *
+ * @param {string} itemId - ID of the item to remove
+ * @returns {Object} Operation result with success flag
+ *
+ * @example
+ * const result = itemService.removeItem('abc123');
+ * if (result.success) {
+ * console.log('Item deleted successfully');
+ * }
+ */
+ removeItem(itemId) {
+ // Verify item exists
+ const existingItem = this.getItemById(itemId);
+ if (!existingItem) {
+ return {
+ success: false,
+ errors: ['Item not found'],
+ data: null
+ };
+ }
+
+ try {
+ deleteItem(itemId);
+ return {
+ success: true,
+ errors: [],
+ data: { deletedId: itemId }
+ };
+ } catch (error) {
+ console.error('Failed to delete item:', error);
+ return {
+ success: false,
+ errors: ['Failed to delete item. Please try again.'],
+ data: null
+ };
+ }
+ }
+
+ /**
+ * Get an item by its ID
+ *
+ * @param {string} itemId - ID of the item to retrieve
+ * @returns {Object|null} Item object or null if not found
+ *
+ * @example
+ * const item = itemService.getItemById('abc123');
+ * if (item) {
+ * console.log('Found item:', item.text);
+ * }
+ */
+ getItemById(itemId) {
+ const state = getState();
+ return state.items.find(item => item.id === itemId) || null;
+ }
+
+ /**
+ * Get all items with optional filtering and searching
+ *
+ * @param {Object} [options={}] - Filtering options
+ * @param {string} [options.search=''] - Search query
+ * @param {string[]} [options.tags=[]] - Tags to filter by
+ * @param {boolean} [options.caseSensitive=false] - Whether search should be case sensitive
+ * @returns {Array} Array of filtered items
+ *
+ * @example
+ * // Get all items
+ * const allItems = itemService.getAllItems();
+ *
+ * // Get items matching search
+ * const searchResults = itemService.getAllItems({ search: 'git' });
+ *
+ * // Get items with specific tags
+ * const gitItems = itemService.getAllItems({ tags: ['git'] });
+ */
+ getAllItems(options = {}) {
+ const state = getState();
+ const { search = '', tags = [] } = options;
+
+ if (!search && tags.length === 0) {
+ return [...state.items]; // Return copy
+ }
+
+ return filterItems(state.items, search, tags);
+ }
+
+ /**
+ * Search items using advanced criteria
+ *
+ * @param {string} query - Search query
+ * @param {string[]} [filterTags=[]] - Tags to filter by
+ * @param {Object} [options={}] - Additional search options
+ * @returns {Array} Array of matching items
+ *
+ * @example
+ * // Simple search
+ * const results = itemService.searchItems('console.log');
+ *
+ * // Search with tag filtering
+ * const results = itemService.searchItems('debug', ['javascript']);
+ */
+ searchItems(query, filterTags = [], options = {}) {
+ return this.getAllItems({
+ search: query,
+ tags: filterTags,
+ ...options
+ });
+ }
+
+ /**
+ * Get all unique tags from items
+ *
+ * @returns {string[]} Sorted array of unique tag names
+ *
+ * @example
+ * const tags = itemService.getAllTags();
+ * console.log('Available tags:', tags);
+ */
+ getAllTags() {
+ const state = getState();
+ return getAllTags(state.items);
+ }
+
+ /**
+ * Get statistics about items
+ *
+ * @returns {Object} Statistics object
+ *
+ * @example
+ * const stats = itemService.getStatistics();
+ * console.log(`Total items: ${stats.totalItems}`);
+ * console.log(`Sensitive items: ${stats.sensitiveItems}`);
+ */
+ getStatistics() {
+ const state = getState();
+ const items = state.items;
+
+ return {
+ totalItems: items.length,
+ sensitiveItems: items.filter(item => item.sensitive).length,
+ totalTags: this.getAllTags().length,
+ averageTagsPerItem: items.length > 0 ?
+ items.reduce((sum, item) => sum + item.tags.length, 0) / items.length : 0,
+ itemsWithoutTags: items.filter(item => item.tags.length === 0).length,
+ oldestItem: items.length > 0 ?
+ items.reduce((oldest, item) => item.id < oldest.id ? item : oldest) : null,
+ newestItem: items.length > 0 ?
+ items.reduce((newest, item) => item.id > newest.id ? item : newest) : null
+ };
+ }
+
+ /**
+ * Duplicate an existing item
+ *
+ * Creates a copy of an item with a new ID and optional modifications.
+ *
+ * @param {string} itemId - ID of the item to duplicate
+ * @param {Object} [overrides={}] - Properties to override in the duplicate
+ * @returns {Object} Operation result with success flag and data/errors
+ *
+ * @example
+ * const result = itemService.duplicateItem('abc123', {
+ * desc: 'Copy of original command'
+ * });
+ */
+ duplicateItem(itemId, overrides = {}) {
+ const originalItem = this.getItemById(itemId);
+ if (!originalItem) {
+ return {
+ success: false,
+ errors: ['Original item not found'],
+ data: null
+ };
+ }
+
+ // Create duplicate data
+ const duplicateData = {
+ ...originalItem,
+ ...overrides,
+ // Ensure tags are properly normalized if provided
+ tags: overrides.tags ? this.normalizeTags(overrides.tags) : originalItem.tags
+ };
+
+ // Remove ID so a new one will be generated
+ delete duplicateData.id;
+
+ return this.createItem(duplicateData);
+ }
+
+ /**
+ * Bulk operations for multiple items
+ *
+ * @param {string} operation - Operation type: 'delete', 'addTag', 'removeTag', 'setSensitive'
+ * @param {string[]} itemIds - Array of item IDs to operate on
+ * @param {*} [operationData] - Additional data for the operation
+ * @returns {Object} Operation result with success/failure counts
+ *
+ * @example
+ * // Delete multiple items
+ * const result = itemService.bulkOperation('delete', ['id1', 'id2', 'id3']);
+ *
+ * // Add tag to multiple items
+ * const result = itemService.bulkOperation('addTag', ['id1', 'id2'], 'important');
+ */
+ bulkOperation(operation, itemIds, operationData) {
+ const results = {
+ success: 0,
+ failed: 0,
+ errors: []
+ };
+
+ for (const itemId of itemIds) {
+ try {
+ let operationResult;
+
+ switch (operation) {
+ case 'delete':
+ operationResult = this.removeItem(itemId);
+ break;
+
+ case 'addTag':
+ operationResult = this.addTagToItem(itemId, operationData);
+ break;
+
+ case 'removeTag':
+ operationResult = this.removeTagFromItem(itemId, operationData);
+ break;
+
+ case 'setSensitive':
+ operationResult = this.updateItem(itemId, { sensitive: operationData });
+ break;
+
+ default:
+ throw new Error(`Unknown operation: ${operation}`);
+ }
+
+ if (operationResult.success) {
+ results.success++;
+ } else {
+ results.failed++;
+ results.errors.push(`Item ${itemId}: ${operationResult.errors.join(', ')}`);
+ }
+ } catch (error) {
+ results.failed++;
+ results.errors.push(`Item ${itemId}: ${error.message}`);
+ }
+ }
+
+ return results;
+ }
+
+ /**
+ * Add a tag to an existing item
+ *
+ * @param {string} itemId - ID of the item
+ * @param {string} tag - Tag to add
+ * @returns {Object} Operation result
+ *
+ * @private
+ */
+ addTagToItem(itemId, tag) {
+ const item = this.getItemById(itemId);
+ if (!item) {
+ return { success: false, errors: ['Item not found'] };
+ }
+
+ const normalizedTag = this.normalizeTag(tag);
+ if (!normalizedTag || item.tags.includes(normalizedTag)) {
+ return { success: true, errors: [] }; // Already exists or invalid
+ }
+
+ const updatedTags = [...item.tags, normalizedTag];
+ return this.updateItem(itemId, { tags: updatedTags });
+ }
+
+ /**
+ * Remove a tag from an existing item
+ *
+ * @param {string} itemId - ID of the item
+ * @param {string} tag - Tag to remove
+ * @returns {Object} Operation result
+ *
+ * @private
+ */
+ removeTagFromItem(itemId, tag) {
+ const item = this.getItemById(itemId);
+ if (!item) {
+ return { success: false, errors: ['Item not found'] };
+ }
+
+ const updatedTags = item.tags.filter(t => t !== tag);
+ return this.updateItem(itemId, { tags: updatedTags });
+ }
+
+ /**
+ * Validate item data
+ *
+ * @param {Object} itemData - Item data to validate
+ * @returns {Object} Validation result
+ *
+ * @private
+ */
+ validateItemData(itemData) {
+ return validateItem(itemData);
+ }
+
+ /**
+ * Sanitize item data for safe storage
+ *
+ * @param {Object} itemData - Raw item data
+ * @returns {Object} Sanitized item data
+ *
+ * @private
+ */
+ sanitizeItemData(itemData) {
+ return {
+ text: String(itemData.text || '').trim(),
+ desc: String(itemData.desc || '').trim(),
+ sensitive: Boolean(itemData.sensitive),
+ tags: this.normalizeTags(itemData.tags || [])
+ };
+ }
+
+ /**
+ * Normalize array of tags
+ *
+ * @param {string[]} tags - Raw tags array
+ * @returns {string[]} Normalized tags array
+ *
+ * @private
+ */
+ normalizeTags(tags) {
+ if (!Array.isArray(tags)) return [];
+
+ return tags
+ .map(tag => this.normalizeTag(tag))
+ .filter(Boolean)
+ .filter((tag, index, arr) => arr.indexOf(tag) === index); // Remove duplicates
+ }
+
+ /**
+ * Normalize a single tag
+ *
+ * @param {string} tag - Raw tag string
+ * @returns {string} Normalized tag or empty string if invalid
+ *
+ * @private
+ */
+ normalizeTag(tag) {
+ if (typeof tag !== 'string') return '';
+
+ return tag
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9\-_]/g, '') // Allow only alphanumeric, hyphens, underscores
+ .substring(0, 50); // Limit length
+ }
+}
+
+/**
+ * Factory function to create an item service instance
+ *
+ * @returns {ItemService} Configured item service instance
+ *
+ * @example
+ * import { createItemService } from './itemService.js';
+ *
+ * const itemService = createItemService();
+ */
+export const createItemService = () => {
+ return new ItemService();
+};
+
+// Export singleton instance for convenience
+export const itemService = new ItemService();
diff --git a/js/state.js b/js/state.js
new file mode 100644
index 0000000..875b708
--- /dev/null
+++ b/js/state.js
@@ -0,0 +1,638 @@
+/**
+ * Application State Management Module for Compy 2.0
+ *
+ * This module implements a centralized state management system using the
+ * observer pattern for reactive updates. It provides a single source of truth
+ * for all application data and ensures consistent state synchronization across
+ * UI components.
+ *
+ * Key Features:
+ * - Immutable state updates with automatic persistence
+ * - Observer pattern for reactive UI updates
+ * - Automatic localStorage synchronization
+ * - Debounced backup system for data recovery
+ * - Type-safe interfaces with JSDoc annotations
+ * - Memory-efficient listener management with Set
+ *
+ * Architecture:
+ * - State is kept in a single object with immutable updates
+ * - Changes trigger notifications to registered listeners
+ * - Persistence to localStorage happens automatically
+ * - Backups are created on a schedule with debouncing
+ *
+ * @fileoverview Centralized state management with persistence and reactivity
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+// Import necessary utilities and configuration
+import { STORAGE_KEYS, UI_CONFIG } from './constants.js';
+import { generateUID, debounce } from './utils.js';
+
+// =============================================================================
+// TYPE DEFINITIONS
+// =============================================================================
+
+/**
+ * Represents a single snippet item in the application
+ *
+ * @typedef {Object} AppItem
+ * @property {string} id - Unique identifier for the item
+ * @property {string} text - The main snippet content
+ * @property {string} desc - Human-readable description of the snippet
+ * @property {boolean} sensitive - Whether the snippet contains sensitive data (masked in UI)
+ * @property {string[]} tags - Array of category/organization tags
+ */
+
+/**
+ * Complete application state structure
+ *
+ * @typedef {Object} AppState
+ * @property {AppItem[]} items - All snippet items in the application
+ * @property {string[]} filterTags - Currently active filter tags
+ * @property {string} search - Current search query string
+ * @property {string|null} editingId - ID of item being edited (null if none)
+ * @property {string} profileName - User's display name for personalization
+ */
+
+/**
+ * Callback function for state change notifications
+ *
+ * @callback StateListener
+ * @param {AppState} state - Latest immutable state snapshot
+ * @returns {void}
+ */
+
+// =============================================================================
+// STATE INITIALIZATION
+// =============================================================================
+
+/**
+ * Default/initial state structure
+ *
+ * This represents a clean slate when the application starts for the first time
+ * or when state needs to be reset. All properties have sensible defaults.
+ *
+ * @constant {AppState} initialState
+ */
+const initialState = {
+ items: [], // No snippets initially
+ filterTags: [], // No active filters
+ search: '', // Empty search query
+ editingId: null, // No item being edited
+ profileName: '', // No profile name set
+};
+
+/**
+ * Current application state
+ *
+ * This is the single source of truth for all application data. It should only
+ * be modified through the exported functions to ensure consistency and trigger
+ * proper notifications.
+ *
+ * @type {AppState}
+ */
+let state = { ...initialState };
+
+/**
+ * Set of registered state change listeners
+ *
+ * Using Set for efficient listener management - provides O(1) add/remove
+ * operations and automatic deduplication of listener functions.
+ *
+ * @type {Set}
+ */
+const listeners = new Set();
+
+// =============================================================================
+// OBSERVER PATTERN IMPLEMENTATION
+// =============================================================================
+
+/**
+ * Subscribe to state changes using the observer pattern
+ *
+ * Registers a callback function that will be invoked whenever the application
+ * state changes. This enables reactive UI updates and decoupled architecture.
+ *
+ * @param {StateListener} listener - Callback invoked with the latest state on changes
+ * @returns {() => void} Unsubscribe function to remove the listener
+ *
+ * @example
+ * // Subscribe to state changes
+ * const unsubscribe = subscribe((newState) => {
+ * console.log('State updated:', newState);
+ * updateUI(newState);
+ * });
+ *
+ * // Later, unsubscribe to prevent memory leaks
+ * unsubscribe();
+ */
+export const subscribe = (listener) => {
+ listeners.add(listener);
+
+ // Return unsubscribe function for cleanup
+ return () => {
+ listeners.delete(listener);
+ };
+};
+
+/**
+ * Notify all registered listeners of state changes
+ *
+ * This function is called internally whenever the state is modified.
+ * It iterates through all registered listeners and calls each one with
+ * the current state snapshot.
+ *
+ * Performance Note: Uses forEach for optimal iteration over Set.
+ *
+ * @private
+ */
+const notifyListeners = () => {
+ listeners.forEach(listener => listener(state));
+};
+
+// =============================================================================
+// STATE PERSISTENCE AND LOADING
+// =============================================================================
+
+/**
+ * Load application state from localStorage and hydrate the state object
+ *
+ * This function attempts to restore the application state from browser storage.
+ * It gracefully handles missing or corrupted data by falling back to defaults.
+ * Includes data validation to ensure loaded state meets expected structure.
+ *
+ * Storage Keys Used:
+ * - STORAGE_KEYS.items: Array of snippet items
+ * - STORAGE_KEYS.filters: Array of active filter tags
+ * - STORAGE_KEYS.profile: User profile name string
+ *
+ * Data Validation:
+ * - Validates that items is an array with proper structure
+ * - Ensures filterTags is an array of strings
+ * - Sanitizes profile name for security
+ *
+ * Error Handling:
+ * If localStorage is unavailable or contains invalid JSON, the function
+ * logs the error and resets to the initial state to prevent app crashes.
+ *
+ * @example
+ * // Load state on app startup
+ * loadState();
+ */
+export const loadState = () => {
+ try {
+ // Attempt to parse stored data with fallbacks and validation
+ const rawItems = localStorage.getItem(STORAGE_KEYS.items);
+ const rawFilters = localStorage.getItem(STORAGE_KEYS.filters);
+ const rawProfile = localStorage.getItem(STORAGE_KEYS.profile);
+
+ // Parse and validate items with structure checking
+ let items = [];
+ if (rawItems) {
+ const parsedItems = JSON.parse(rawItems);
+ if (Array.isArray(parsedItems)) {
+ // Validate item structure and filter out invalid items
+ items = parsedItems.filter(item =>
+ item &&
+ typeof item.id === 'string' &&
+ typeof item.text === 'string' &&
+ typeof item.desc === 'string' &&
+ typeof item.sensitive === 'boolean' &&
+ Array.isArray(item.tags)
+ );
+ }
+ }
+
+ // Parse and validate filter tags
+ let filterTags = [];
+ if (rawFilters) {
+ const parsedFilters = JSON.parse(rawFilters);
+ if (Array.isArray(parsedFilters)) {
+ // Ensure all filter tags are strings
+ filterTags = parsedFilters.filter(tag => typeof tag === 'string');
+ }
+ }
+
+ // Sanitize profile name
+ const profileName = rawProfile ? String(rawProfile).trim().slice(0, 100) : '';
+
+ // Update state immutably with validated data
+ state = {
+ ...state,
+ items,
+ filterTags,
+ profileName
+ };
+
+ console.log(`State loaded successfully: ${items.length} items, ${filterTags.length} active filters`);
+
+ // Notify subscribers of the loaded state
+ notifyListeners();
+
+ } catch (error) {
+ console.error('Failed to load state from localStorage:', error);
+
+ // Reset to clean state on error to prevent corruption
+ state = { ...initialState };
+
+ // Still notify listeners so UI can render empty state
+ notifyListeners();
+ }
+};
+
+/**
+ * Save current application state to localStorage with automatic backup scheduling
+ *
+ * This function persists the current state to browser storage and schedules
+ * a backup for data recovery. It's called automatically after state mutations.
+ *
+ * Persistence Strategy:
+ * - Items and filters are stored as JSON strings
+ * - Profile name is stored as a plain string
+ * - Empty profile names are not stored (to keep storage clean)
+ *
+ * Side Effects:
+ * - Schedules a debounced backup creation
+ * - Notifies all state listeners
+ *
+ * Error Handling:
+ * Storage errors are logged but don't throw to prevent app crashes.
+ * This allows the app to continue working even if storage is full.
+ *
+ * @example
+ * // Save state after modifying items
+ * state.items.push(newItem);
+ * saveState();
+ */
+export const saveState = () => {
+ try {
+ // Persist core application data
+ localStorage.setItem(STORAGE_KEYS.items, JSON.stringify(state.items));
+ localStorage.setItem(STORAGE_KEYS.filters, JSON.stringify(state.filterTags));
+
+ // Only store profile name if it's not empty (keeps storage clean)
+ if (state.profileName) {
+ localStorage.setItem(STORAGE_KEYS.profile, state.profileName);
+ }
+
+ // Schedule backup creation (debounced for performance)
+ scheduleBackup();
+
+ // Notify listeners that state has been persisted
+ notifyListeners();
+ } catch (error) {
+ console.error('Failed to save state to localStorage:', error);
+ // Continue execution - app can work without persistence
+ }
+};
+
+// =============================================================================
+// BACKUP AND RECOVERY SYSTEM
+// =============================================================================
+
+/**
+ * Debounced backup scheduler to prevent excessive backup creation
+ *
+ * Uses debounce utility to delay backup creation until after a period of
+ * inactivity. This prevents creating too many backups during rapid changes.
+ *
+ * @private
+ */
+const scheduleBackup = debounce(() => {
+ doBackup();
+}, UI_CONFIG.backupDelay);
+
+/**
+ * Create a timestamped backup snapshot for data recovery
+ *
+ * Backups are essential for user data safety. They provide a way to recover
+ * from accidental deletions or data corruption. Backups are automatically
+ * rotated to prevent unlimited storage growth.
+ *
+ * Backup Structure:
+ * - ts: ISO timestamp string for sorting and display
+ * - items: Complete snapshot of all snippet items
+ *
+ * Storage Management:
+ * - New backups are added to the beginning of the array
+ * - Only the most recent UI_CONFIG.maxBackups are kept
+ * - Older backups are automatically removed
+ *
+ * @example
+ * // Manually create a backup
+ * doBackup();
+ */
+export const doBackup = () => {
+ const now = new Date();
+ const backup = {
+ ts: now.toISOString(), // Sortable timestamp
+ items: state.items // Complete data snapshot
+ };
+
+ try {
+ // Load existing backups with fallback to empty array
+ let backups = JSON.parse(localStorage.getItem(STORAGE_KEYS.backups) || '[]');
+
+ // Add new backup at the beginning (most recent first)
+ backups.unshift(backup);
+
+ // Limit backup count to prevent storage bloat
+ backups = backups.slice(0, UI_CONFIG.maxBackups);
+
+ // Persist updated backup list
+ localStorage.setItem(STORAGE_KEYS.backups, JSON.stringify(backups));
+ } catch (error) {
+ console.error('Failed to save backup:', error);
+ // Backup failure shouldn't crash the app
+ }
+};
+
+// =============================================================================
+// STATE ACCESS AND MANIPULATION
+// =============================================================================
+
+/**
+ * Get current application state as an immutable snapshot
+ *
+ * Returns a shallow copy of the current state to prevent accidental mutations.
+ * This is the recommended way to access state from UI components.
+ *
+ * @returns {AppState} Immutable copy of current state
+ *
+ * @example
+ * const currentState = getState();
+ * console.log('Current items:', currentState.items.length);
+ *
+ * // This won't affect the actual state (safe)
+ * currentState.search = 'test';
+ */
+export const getState = () => ({ ...state });
+
+
+// =============================================================================
+// ITEM MANAGEMENT OPERATIONS
+// =============================================================================
+
+/**
+ * Add a new item or update an existing one (upsert operation)
+ *
+ * This function handles both creating new snippet items and updating existing ones.
+ * The behavior is determined by checking the `state.editingId` property:
+ * - If editingId is set: Update the existing item with that ID
+ * - If editingId is null: Create a new item and add it to the beginning of the list
+ *
+ * Immutability:
+ * All operations preserve immutability by creating new objects and arrays
+ * rather than modifying existing ones.
+ *
+ * @param {Partial} item - Item data (without ID for new items)
+ * @param {string} [item.text] - Snippet text content
+ * @param {string} [item.desc] - Snippet description
+ * @param {boolean} [item.sensitive] - Whether snippet is sensitive
+ * @param {string[]} [item.tags] - Array of tags
+ *
+ * @example
+ * // Create new item
+ * setEditingId(null);
+ * upsertItem({
+ * text: 'console.log("Hello World")',
+ * desc: 'Basic console output',
+ * sensitive: false,
+ * tags: ['javascript', 'debug']
+ * });
+ *
+ * // Update existing item
+ * setEditingId('existing-item-id');
+ * upsertItem({
+ * desc: 'Updated description'
+ * });
+ */
+export const upsertItem = (item) => {
+ if (state.editingId) {
+ // Update existing item by ID
+ const index = state.items.findIndex(i => i.id === state.editingId);
+
+ if (index > -1) {
+ // Merge new properties with existing item
+ const updatedItem = { ...state.items[index], ...item };
+
+ // Create new items array with the updated item
+ const updatedItems = [...state.items];
+ updatedItems[index] = updatedItem;
+
+ // Update state and clear editing ID
+ state = { ...state, items: updatedItems, editingId: null };
+ }
+ } else {
+ // Create new item with unique ID
+ const newItem = { id: generateUID(), ...item };
+
+ // Add to beginning of list (most recent first)
+ state = { ...state, items: [newItem, ...state.items] };
+ }
+
+ // Persist changes and trigger backups
+ saveState();
+};
+
+/**
+ * Delete an item by its unique identifier
+ *
+ * Removes the specified item from the items array and persists the change.
+ * Uses array.filter for immutable deletion.
+ *
+ * @param {string} id - Unique ID of the item to delete
+ *
+ * @example
+ * // Delete item by ID
+ * deleteItem('abc123def456');
+ */
+export const deleteItem = (id) => {
+ // Filter out the item with matching ID (immutable deletion)
+ state = { ...state, items: state.items.filter(item => item.id !== id) };
+
+ // Persist changes and trigger backups
+ saveState();
+};
+
+// =============================================================================
+// UI STATE MANAGEMENT
+// =============================================================================
+
+/**
+ * Update the active filter tags for snippet filtering
+ *
+ * Sets the tags that are used to filter the visible snippets. Only items
+ * that contain ALL of the specified tags will be shown in the UI.
+ *
+ * Note: This function persists filters separately from other state to allow
+ * for more granular control over when filters are saved.
+ *
+ * @param {string[]} tags - Array of tag names to filter by
+ *
+ * @example
+ * // Filter by multiple tags
+ * updateFilterTags(['javascript', 'frontend']);
+ *
+ * // Clear all filters
+ * updateFilterTags([]);
+ */
+export const updateFilterTags = (tags) => {
+ state = { ...state, filterTags: tags };
+
+ // Persist filters separately for immediate effect
+ localStorage.setItem(STORAGE_KEYS.filters, JSON.stringify(tags));
+
+ // Notify UI components to re-render
+ notifyListeners();
+};
+
+/**
+ * Update the current search query string
+ *
+ * Sets the search term used for filtering snippets. The search is applied
+ * across item text, descriptions, and tags.
+ *
+ * @param {string} query - Search query string (case-insensitive)
+ *
+ * @example
+ * // Search for items containing "react"
+ * updateSearch('react');
+ *
+ * // Clear search
+ * updateSearch('');
+ */
+export const updateSearch = (query) => {
+ state = { ...state, search: query };
+
+ // Search doesn't need persistence (session-only)
+ notifyListeners();
+};
+
+/**
+ * Update the user's profile name for personalization
+ *
+ * Sets the display name shown in the UI. The name is trimmed to remove
+ * leading/trailing whitespace.
+ *
+ * @param {string} name - User's display name
+ *
+ * @example
+ * // Set user name
+ * updateProfile('John Doe');
+ *
+ * // Clear user name
+ * updateProfile('');
+ */
+export const updateProfile = (name) => {
+ const trimmedName = (name || '').trim();
+ state = { ...state, profileName: trimmedName };
+
+ // Persist profile immediately for cross-session consistency
+ localStorage.setItem(STORAGE_KEYS.profile, trimmedName);
+
+ // Notify UI to update profile display
+ notifyListeners();
+};
+
+/**
+ * Set the ID of the item currently being edited
+ *
+ * This is used by the edit modal to determine whether to create a new item
+ * or update an existing one when the form is submitted.
+ *
+ * @param {string|null} id - Item ID to edit, or null for new item creation
+ *
+ * @example
+ * // Start editing existing item
+ * setEditingId('abc123def456');
+ *
+ * // Prepare for new item creation
+ * setEditingId(null);
+ */
+export const setEditingId = (id) => {
+ state = { ...state, editingId: id };
+
+ // This is UI state only - no persistence needed
+ notifyListeners();
+};
+
+// =============================================================================
+// BACKUP MANAGEMENT
+// =============================================================================
+
+/**
+ * Get the current list of backup snapshots
+ *
+ * Retrieves all available backup snapshots from localStorage, sorted by
+ * timestamp (most recent first). Used by the backups modal to display
+ * available recovery options.
+ *
+ * @returns {Array<{ts: string, items: AppItem[]}>} Array of backup objects
+ * @returns {string} returns[].ts - ISO timestamp of the backup
+ * @returns {AppItem[]} returns[].items - Snapshot of items at backup time
+ *
+ * @example
+ * // Get all backups for display
+ * const backups = getBackups();
+ * backups.forEach(backup => {
+ * console.log(`Backup from ${backup.ts}: ${backup.items.length} items`);
+ * });
+ */
+export const getBackups = () => {
+ try {
+ return JSON.parse(localStorage.getItem(STORAGE_KEYS.backups) || '[]');
+ } catch (error) {
+ console.error('Failed to get backups:', error);
+ // Return empty array on error to prevent crashes
+ return [];
+ }
+};
+
+// =============================================================================
+// INITIALIZATION AND LIFECYCLE
+// =============================================================================
+
+/**
+ * Start the recurring backup timer for automatic data protection
+ *
+ * Sets up an interval timer that creates backups at regular intervals
+ * defined by UI_CONFIG.backupInterval. This provides automatic data
+ * protection without user intervention.
+ *
+ * Timer Management:
+ * The timer runs continuously once started and doesn't need cleanup
+ * since it's tied to the application lifecycle.
+ *
+ * @example
+ * // Start automatic backups (called during app initialization)
+ * setupBackupInterval();
+ */
+export const setupBackupInterval = () => {
+ setInterval(doBackup, UI_CONFIG.backupInterval);
+};
+
+/**
+ * Initialize the state management system
+ *
+ * This function should be called once during application startup to:
+ * 1. Load existing data from localStorage
+ * 2. Set up automatic backup intervals
+ * 3. Prepare the state system for use
+ *
+ * Call this before any other state operations to ensure the system
+ * is properly initialized.
+ *
+ * @example
+ * // Initialize state system on app startup
+ * initState();
+ */
+export const initState = () => {
+ // Load persisted data from browser storage
+ loadState();
+
+ // Start automatic backup system
+ setupBackupInterval();
+};
diff --git a/js/utils.js b/js/utils.js
new file mode 100644
index 0000000..8649488
--- /dev/null
+++ b/js/utils.js
@@ -0,0 +1,662 @@
+/**
+ * Utility Functions for Compy 2.0
+ *
+ * This module provides a comprehensive collection of utility functions used
+ * throughout the application. It includes DOM manipulation helpers, data processing
+ * functions, validation utilities, and common operations that promote code reuse
+ * and maintainability.
+ *
+ * Categories:
+ * - DOM Manipulation: $, $$, focusElement
+ * - Data Processing: escapeHtml, highlightText, stringHash
+ * - File Operations: downloadFile, parseCSVLine, csvEscape
+ * - Validation: validateItem
+ * - Array Operations: filterItems, getAllTags
+ * - Async Operations: debounce
+ * - Date/Time: formatDate
+ * - Accessibility: prefersReducedMotion
+ *
+ * @fileoverview Core utility functions for DOM, data, and common operations
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+// =============================================================================
+// DOM MANIPULATION UTILITIES
+// =============================================================================
+
+/**
+ * Shorthand for document.querySelector with enhanced error handling
+ *
+ * This function provides a concise way to select single DOM elements while
+ * maintaining flexibility for different root contexts. It's used extensively
+ * throughout the application for element selection.
+ *
+ * @param {string} selector - CSS selector string to match elements
+ * @param {Element|Document} [root=document] - Root element to search from
+ * @returns {Element|null} First matching element or null if none found
+ *
+ * @example
+ * // Select by ID
+ * const button = $('#saveBtn');
+ *
+ * // Select within a specific container
+ * const modal = $('#modal');
+ * const input = $('input[type="text"]', modal);
+ *
+ * // Complex selector
+ * const activeCard = $('.card.active[data-id="123"]');
+ */
+export const $ = (selector, root = document) => root.querySelector(selector);
+
+/**
+ * Shorthand for document.querySelectorAll returning a real Array
+ *
+ * Unlike the native querySelectorAll which returns a NodeList, this function
+ * returns a proper Array with all array methods available (map, filter, etc.).
+ * This makes it much more convenient for functional programming patterns.
+ *
+ * @param {string} selector - CSS selector string to match elements
+ * @param {Element|Document} [root=document] - Root element to search from
+ * @returns {Element[]} Array of matching elements (empty array if none found)
+ *
+ * @example
+ * // Get all cards and map over them
+ * const cardData = $$('.card').map(card => ({
+ * id: card.dataset.id,
+ * title: card.querySelector('.title').textContent
+ * }));
+ *
+ * // Filter elements by attribute
+ * const sensitiveCards = $$('.card[data-sensitive="true"]');
+ *
+ * // Apply event listeners to multiple elements
+ * $$('.btn').forEach(btn => btn.addEventListener('click', handleClick));
+ */
+export const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));
+
+// =============================================================================
+// ID GENERATION AND HASHING UTILITIES
+// =============================================================================
+
+/**
+ * Generate a unique identifier for new items
+ *
+ * This function creates collision-resistant unique identifiers by combining
+ * a random component with a timestamp component. The random part provides
+ * uniqueness within the same timestamp, while the timestamp ensures
+ * chronological ordering and global uniqueness.
+ *
+ * Performance Optimization:
+ * - Uses single operation instead of multiple concatenations
+ * - Base-36 encoding provides compact output
+ * - No external dependencies or complex algorithms
+ *
+ * Format: {random_part}{timestamp_part}
+ * Example: "a7k3m9n8qp5r2t1"
+ *
+ * @returns {string} Unique identifier (16-20 characters)
+ *
+ * @example
+ * // Generate unique IDs for items
+ * const itemId = generateUID(); // "a7k3m9n8qp5r2t1"
+ * const anotherId = generateUID(); // "x2v4b1n7mp9q3s6"
+ *
+ * // IDs are sortable by creation time due to timestamp component
+ * console.log(itemId < anotherId); // true if first was created earlier
+ */
+export const generateUID = () =>
+ Math.random().toString(36).slice(2) + Date.now().toString(36);
+
+// =============================================================================
+// DATA PROCESSING AND SECURITY UTILITIES
+// =============================================================================
+
+/**
+ * Escape HTML characters to prevent Cross-Site Scripting (XSS) attacks
+ *
+ * This function sanitizes user input by converting potentially dangerous HTML
+ * characters into their safe HTML entity equivalents. This is crucial for
+ * security when displaying user-generated content in the DOM.
+ *
+ * Protected Characters:
+ * - & (ampersand) → & (prevents entity injection)
+ * - < (less than) → < (prevents tag injection)
+ * - > (greater than) → > (completes tag prevention)
+ * - " (double quote) → " (prevents attribute injection)
+ * - ' (single quote) → ' (prevents attribute injection)
+ *
+ * @param {string} str - String to escape (will be converted to string if not)
+ * @returns {string} HTML-safe escaped string
+ *
+ * @example
+ * // Protect against script injection
+ * const userInput = '';
+ * const safeOutput = escapeHtml(userInput);
+ * // Result: "<script>alert("XSS")</script>"
+ *
+ * // Protect snippet content before display
+ * const snippet = { text: 'echo "Hello & goodbye"' };
+ * element.innerHTML = escapeHtml(snippet.text);
+ */
+export const escapeHtml = (str) => {
+ const escapeMap = {
+ '&': '&', // Must be first to avoid double-escaping
+ '<': '<', // Prevents opening tags
+ '>': '>', // Prevents closing tags
+ '"': '"', // Prevents attribute values with double quotes
+ "'": ''' // Prevents attribute values with single quotes
+ };
+ return String(str).replace(/[&<>"']/g, (match) => escapeMap[match]);
+};
+
+/**
+ * Highlight search terms within text using HTML mark elements
+ *
+ * This function wraps matching search terms with tags to visually
+ * highlight them in the UI. It handles regex escaping to prevent injection
+ * and uses case-insensitive matching for better user experience.
+ *
+ * Features:
+ * - Case-insensitive search matching
+ * - Regex special character escaping
+ * - Safe HTML mark injection
+ * - Preserves original text when no query provided
+ *
+ * @param {string} text - Text to search within and highlight
+ * @param {string} query - Search term to highlight (empty string returns original text)
+ * @returns {string} Text with tags around matching terms
+ *
+ * @example
+ * // Basic highlighting
+ * const result = highlightText('Hello world', 'world');
+ * // Result: "Hello world "
+ *
+ * // Case-insensitive matching
+ * const result = highlightText('JavaScript is great', 'SCRIPT');
+ * // Result: "JavaScript is great"
+ *
+ * // Multiple matches
+ * const result = highlightText('test test test', 'test');
+ * // Result: "test test test "
+ */
+export const highlightText = (text, query) => {
+ if (!query) return text;
+
+ // Escape special regex characters to prevent injection
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
+ // Create case-insensitive global regex and replace with mark tags
+ return text.replace(new RegExp(escapedQuery, 'gi'), (match) => `${match} `);
+};
+
+/**
+ * Generate a deterministic hash for consistent visual elements
+ *
+ * This simple string hash function creates a numeric hash from any string,
+ * which is useful for generating consistent colors, positions, or other
+ * visual properties. The same input will always produce the same output.
+ *
+ * Algorithm:
+ * Uses a variation of the djb2 hash algorithm with bit manipulation
+ * for performance. The hash is computed by:
+ * 1. Initialize hash to 0
+ * 2. For each character: hash = (hash << 5) - hash + charCode
+ * 3. Apply bitwise OR with 0 to ensure 32-bit integer
+ *
+ * @param {string} str - String to hash
+ * @returns {number} 32-bit signed integer hash
+ *
+ * @example
+ * // Generate consistent colors for tags
+ * const tagName = 'javascript';
+ * const hue = Math.abs(stringHash(tagName)) % 360;
+ * const color = `hsl(${hue}, 70%, 50%)`;
+ *
+ * // Same input always gives same result
+ * console.log(stringHash('test')); // Always returns same number
+ * console.log(stringHash('test')); // Same as above
+ */
+export const stringHash = (str) => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ // djb2-style hash with bit manipulation for performance
+ hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
+ }
+ return hash;
+};
+
+// =============================================================================
+// ASYNC UTILITIES AND PERFORMANCE HELPERS
+// =============================================================================
+
+/**
+ * Debounce function execution to improve performance
+ *
+ * Debouncing delays function execution until after a specified wait time has
+ * elapsed since the last time it was invoked. This is essential for performance
+ * when dealing with high-frequency events like typing, scrolling, or resizing.
+ *
+ * Use Cases:
+ * - Search input (wait for user to stop typing)
+ * - Window resize handlers
+ * - API calls that shouldn't be made too frequently
+ * - Auto-save functionality
+ *
+ * @param {Function} func - Function to debounce
+ * @param {number} wait - Delay in milliseconds before execution
+ * @returns {Function} Debounced version of the original function
+ *
+ * @example
+ * // Debounce search to avoid excessive API calls
+ * const debouncedSearch = debounce((query) => {
+ * performSearch(query);
+ * }, 300);
+ *
+ * searchInput.addEventListener('input', (e) => {
+ * debouncedSearch(e.target.value);
+ * });
+ *
+ * // Debounce window resize handler
+ * const debouncedResize = debounce(() => {
+ * updateLayout();
+ * }, 100);
+ *
+ * window.addEventListener('resize', debouncedResize);
+ */
+export const debounce = (func, wait) => {
+ let timeout;
+
+ return function executedFunction(...args) {
+ // Define the delayed execution function
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+
+ // Clear previous timeout and set new one
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+};
+
+// =============================================================================
+// FILE OPERATIONS AND DATA EXCHANGE
+// =============================================================================
+
+/**
+ * Download text content as a file through the browser
+ *
+ * Creates a temporary blob URL and triggers a download through a hidden anchor
+ * element. This method works in all modern browsers and automatically cleans
+ * up the temporary URL to prevent memory leaks.
+ *
+ * Process:
+ * 1. Create a Blob with the specified content and MIME type
+ * 2. Generate a temporary object URL for the blob
+ * 3. Create a hidden anchor element with download attribute
+ * 4. Programmatically click the anchor to trigger download
+ * 5. Clean up DOM and revoke the object URL
+ *
+ * @param {string} filename - Desired filename including extension
+ * @param {string} content - Text content to download
+ * @param {string} [mimeType='text/plain'] - MIME type for the file
+ *
+ * @example
+ * // Download JSON data
+ * const data = { items: [...] };
+ * downloadFile('backup.json', JSON.stringify(data, null, 2), 'application/json');
+ *
+ * // Download CSV export
+ * const csv = 'name,email\nJohn,john@example.com';
+ * downloadFile('contacts.csv', csv, 'text/csv');
+ *
+ * // Download plain text
+ * downloadFile('notes.txt', 'My important notes', 'text/plain');
+ */
+export const downloadFile = (filename, content, mimeType = 'text/plain') => {
+ // Create blob with proper MIME type
+ const blob = new Blob([content], { type: mimeType });
+
+ // Generate temporary URL for the blob
+ const url = URL.createObjectURL(blob);
+
+ // Create hidden download link
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+
+ // Trigger download by programmatically clicking the link
+ document.body.appendChild(link);
+ link.click();
+
+ // Clean up: remove element and revoke object URL
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+};
+
+/**
+ * Parse a CSV line with proper quote and escape handling
+ *
+ * This function correctly parses CSV lines that may contain:
+ * - Quoted fields with embedded commas
+ * - Escaped quotes (doubled quotes within quoted fields)
+ * - Mixed quoted and unquoted fields
+ * - Empty fields
+ *
+ * CSV Parsing Rules:
+ * - Fields separated by commas
+ * - Fields containing commas must be quoted
+ * - Quotes within quoted fields are escaped by doubling ("")
+ * - Leading/trailing whitespace in unquoted fields is preserved
+ *
+ * @param {string} line - CSV line to parse
+ * @returns {string[]} Array of field values
+ *
+ * @example
+ * // Simple fields
+ * parseCSVLine('name,age,city');
+ * // Result: ['name', 'age', 'city']
+ *
+ * // Quoted fields with commas
+ * parseCSVLine('"John, Jr",25,"New York, NY"');
+ * // Result: ['John, Jr', '25', 'New York, NY']
+ *
+ * // Escaped quotes
+ * parseCSVLine('"He said ""Hello""",greeting');
+ * // Result: ['He said "Hello"', 'greeting']
+ */
+export const parseCSVLine = (line) => {
+ const result = [];
+ let current = '';
+ let inQuotes = false;
+
+ for (let i = 0; i < line.length; i++) {
+ const char = line[i];
+ const nextChar = line[i + 1];
+
+ if (inQuotes) {
+ // Inside quoted field
+ if (char === '"' && nextChar === '"') {
+ // Escaped quote: add single quote and skip next character
+ current += '"';
+ i++; // Skip the next quote
+ } else if (char === '"') {
+ // End quote: exit quoted mode
+ inQuotes = false;
+ } else {
+ // Regular character inside quotes
+ current += char;
+ }
+ } else {
+ // Outside quoted field
+ if (char === ',') {
+ // Field separator: save current field and start new one
+ result.push(current);
+ current = '';
+ } else if (char === '"') {
+ // Start quote: enter quoted mode
+ inQuotes = true;
+ } else {
+ // Regular character
+ current += char;
+ }
+ }
+ }
+
+ // Don't forget the last field
+ result.push(current);
+ return result;
+};
+
+/**
+ * Escape a string for safe inclusion in CSV format
+ *
+ * Wraps the string in double quotes and escapes any existing double quotes
+ * by doubling them. This ensures the field can contain commas, newlines,
+ * and quotes without breaking the CSV structure.
+ *
+ * @param {string} str - String to escape (will be converted to string if not)
+ * @returns {string} CSV-safe quoted string
+ *
+ * @example
+ * csvEscape('Hello, world');
+ * // Result: '"Hello, world"'
+ *
+ * csvEscape('He said "Hi"');
+ * // Result: '"He said ""Hi"""'
+ *
+ * csvEscape('Simple text');
+ * // Result: '"Simple text"'
+ */
+export const csvEscape = (str) => `"${String(str).replace(/"/g, '""')}"`;
+
+// =============================================================================
+// DATE AND TIME UTILITIES
+// =============================================================================
+
+/**
+ * Format a date for user-friendly display
+ *
+ * Converts various date inputs into a localized string representation
+ * using the user's browser locale and timezone settings.
+ *
+ * @param {Date|string|number} date - Date to format (Date object, ISO string, or timestamp)
+ * @returns {string} Formatted date string according to user's locale
+ *
+ * @example
+ * // With Date object
+ * formatDate(new Date());
+ * // Result: "12/25/2024, 3:30:45 PM" (varies by locale)
+ *
+ * // With ISO string
+ * formatDate('2024-12-25T15:30:45.000Z');
+ * // Result: Localized format based on user's timezone
+ *
+ * // With timestamp
+ * formatDate(1703520645000);
+ * // Result: Localized format
+ */
+export const formatDate = (date) => {
+ const d = new Date(date);
+ return d.toLocaleString();
+};
+
+// =============================================================================
+// ACCESSIBILITY AND USER PREFERENCE UTILITIES
+// =============================================================================
+
+/**
+ * Check if user prefers reduced motion for accessibility
+ *
+ * Uses the CSS media query to detect if the user has requested reduced motion
+ * in their system settings. This is important for respecting accessibility
+ * preferences and can be used to disable or reduce animations.
+ *
+ * @returns {boolean} True if user prefers reduced motion
+ *
+ * @example
+ * // Conditionally apply animations
+ * const shouldAnimate = !prefersReducedMotion();
+ *
+ * if (shouldAnimate) {
+ * element.classList.add('animated');
+ * } else {
+ * element.classList.add('instant');
+ * }
+ */
+export const prefersReducedMotion = () =>
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+
+/**
+ * Focus an element with optional delay for better UX
+ *
+ * Safely focuses an element while handling null/undefined elements gracefully.
+ * The delay option is useful for focusing elements after animations or when
+ * timing is important (e.g., after modal open animations).
+ *
+ * @param {Element|null} element - Element to focus (can be null/undefined)
+ * @param {number} [delay=0] - Optional delay in milliseconds before focusing
+ *
+ * @example
+ * // Immediate focus
+ * focusElement($('#searchInput'));
+ *
+ * // Delayed focus after modal animation
+ * focusElement($('#modalInput'), 150);
+ *
+ * // Safe with null elements (no error thrown)
+ * focusElement(null, 100); // No-op
+ */
+export const focusElement = (element, delay = 0) => {
+ if (delay > 0) {
+ setTimeout(() => element?.focus(), delay);
+ } else {
+ element?.focus();
+ }
+};
+
+// =============================================================================
+// DATA PROCESSING AND FILTERING UTILITIES
+// =============================================================================
+
+/**
+ * Extract all unique tags from a collection of items
+ *
+ * Flattens the tags arrays from all items, removes duplicates using Set,
+ * and returns a sorted array of unique tag names. This is useful for
+ * populating filter lists and tag selectors.
+ *
+ * @param {Array} items - Array of items with tags property
+ * @returns {string[]} Sorted array of unique tag names
+ *
+ * @example
+ * const items = [
+ * { tags: ['javascript', 'frontend'] },
+ * { tags: ['python', 'backend'] },
+ * { tags: ['javascript', 'react'] }
+ * ];
+ *
+ * getAllTags(items);
+ * // Result: ['backend', 'frontend', 'javascript', 'python', 'react']
+ */
+export const getAllTags = (items) =>
+ Array.from(new Set(items.flatMap(item => item.tags))).sort();
+
+/**
+ * Filter items by search query and selected tags
+ *
+ * Applies both text search and tag filtering to an array of items.
+ * Tag filtering uses AND logic (item must have ALL selected tags).
+ * Text search is case-insensitive and searches across text, description, and tags.
+ *
+ * @param {Array} items - Items to filter
+ * @param {string} [searchQuery=''] - Search query for text matching
+ * @param {string[]} [filterTags=[]] - Array of tags that items must contain
+ * @returns {Array} Filtered array of items
+ *
+ * @example
+ * const items = [
+ * { text: 'Hello world', desc: 'Greeting', tags: ['basic', 'demo'] },
+ * { text: 'console.log()', desc: 'Debug output', tags: ['javascript', 'debug'] }
+ * ];
+ *
+ * // Filter by search query
+ * filterItems(items, 'hello'); // Returns first item
+ *
+ * // Filter by tags
+ * filterItems(items, '', ['javascript']); // Returns second item
+ *
+ * // Combine search and tags
+ * filterItems(items, 'console', ['javascript']); // Returns second item
+ */
+export const filterItems = (items, searchQuery = '', filterTags = []) => {
+ let filtered = items.slice();
+
+ // Filter by tags using AND logic (item must have ALL selected tags)
+ if (filterTags.length > 0) {
+ filtered = filtered.filter(item =>
+ filterTags.every(tag => item.tags.includes(tag))
+ );
+ }
+
+ // Filter by search query across multiple fields
+ if (searchQuery.trim()) {
+ const query = searchQuery.toLowerCase();
+ filtered = filtered.filter(item =>
+ item.text.toLowerCase().includes(query) ||
+ item.desc.toLowerCase().includes(query) ||
+ item.tags.some(tag => tag.toLowerCase().includes(query))
+ );
+ }
+
+ return filtered;
+};
+
+// =============================================================================
+// VALIDATION UTILITIES
+// =============================================================================
+
+/**
+ * Validate snippet item data for completeness and constraints
+ *
+ * Performs comprehensive validation on item data to ensure it meets
+ * the application's requirements. Returns both a boolean validity flag
+ * and an array of specific error messages for user feedback.
+ *
+ * Validation Rules:
+ * - Text content is required and non-empty
+ * - Description is required and non-empty
+ * - Text content must not exceed 500 characters
+ * - Description must not exceed 500 characters
+ *
+ * @param {Object} item - Item object to validate
+ * @param {string} item.text - Snippet text content
+ * @param {string} item.desc - Snippet description
+ * @returns {Object} Validation result object
+ * @returns {boolean} returns.isValid - True if item passes all validations
+ * @returns {string[]} returns.errors - Array of error messages (empty if valid)
+ *
+ * @example
+ * // Valid item
+ * const validItem = { text: 'console.log("Hello")', desc: 'Debug output' };
+ * const result = validateItem(validItem);
+ * // Result: { isValid: true, errors: [] }
+ *
+ * // Invalid item
+ * const invalidItem = { text: '', desc: '' };
+ * const result = validateItem(invalidItem);
+ * // Result: {
+ * // isValid: false,
+ * // errors: ['Snippet content is required', 'Description is required']
+ * // }
+ */
+export const validateItem = (item) => {
+ const errors = [];
+
+ // Validate required text content
+ if (!item.text || !item.text.trim()) {
+ errors.push('Snippet content is required');
+ }
+
+ // Validate required description
+ if (!item.desc || !item.desc.trim()) {
+ errors.push('Description is required');
+ }
+
+ // Validate text length constraints
+ if (item.text && item.text.length > 500) {
+ errors.push('Snippet content must be 500 characters or less');
+ }
+
+ // Validate description length constraints
+ if (item.desc && item.desc.length > 500) {
+ errors.push('Description must be 500 characters or less');
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors
+ };
+};
diff --git a/sw.js b/sw.js
new file mode 100644
index 0000000..cb8a61c
--- /dev/null
+++ b/sw.js
@@ -0,0 +1,267 @@
+/**
+ * Compy 2.0 Service Worker - Offline-First Caching Strategy
+ *
+ * This service worker implements a comprehensive offline-first caching strategy
+ * for the Compy 2.0 application. It provides seamless offline functionality by:
+ *
+ * - Pre-caching critical static assets during installation
+ * - Implementing cache-first strategy with network fallback
+ * - Providing offline fallback pages for navigation requests
+ * - Automatically cleaning up outdated cache versions
+ * - Supporting both the modular and single-file app variants
+ *
+ * Caching Strategy:
+ * 1. Install: Pre-cache all static assets
+ * 2. Fetch: Cache-first for all same-origin GET requests
+ * 3. Navigate: Fallback to cached index.html when offline
+ * 4. Activate: Clean up old cache versions
+ *
+ * @fileoverview Service Worker for offline functionality and performance
+ * @version 2.0
+ * @author Bheb Developer
+ * @since 2025
+ */
+
+// =============================================================================
+// CACHE CONFIGURATION
+// =============================================================================
+
+/**
+ * Cache Version Identifier
+ *
+ * This version number is used to create a unique cache name. When you need
+ * to invalidate all cached content (e.g., after a major update), increment
+ * this version number. The activation event will automatically clean up
+ * old cache versions.
+ *
+ * ⚠️ IMPORTANT: Only increment this when you need to force cache invalidation
+ * across all users. Normal updates should work with the existing cache.
+ *
+ * @constant {string} CACHE_NAME
+ */
+const CACHE_NAME = 'compy-v2';
+
+/**
+ * Static Assets for Pre-caching
+ *
+ * These files are cached immediately when the service worker is installed.
+ * They represent the core files needed for the app to function offline.
+ *
+ * Selection Criteria:
+ * - Essential HTML, CSS, and JavaScript files
+ * - Critical icons and manifest files
+ * - Files needed for basic app functionality
+ * - Small files that won't impact cache performance
+ *
+ * Note: This includes both single-file (compy.js) and modular variants,
+ * ensuring the service worker works with both implementations.
+ *
+ * @constant {string[]} STATIC_ASSETS
+ */
+const STATIC_ASSETS = [
+ 'index.html', // Main HTML entry point
+ 'css/compy.css', // Application styles
+ 'js/compy.js', // Single-file implementation
+ 'favicon_io/favicon.ico', // Browser tab icon
+ 'favicon_io/favicon-16x16.png', // Small favicon variant
+ 'favicon_io/favicon-32x32.png', // Standard favicon variant
+ 'favicon_io/apple-touch-icon.png', // iOS home screen icon
+ 'favicon_io/site.webmanifest' // PWA manifest file
+];
+
+// =============================================================================
+// SERVICE WORKER EVENT HANDLERS
+// =============================================================================
+
+/**
+ * Install Event Handler - Pre-cache Critical Assets
+ *
+ * The install event fires when the service worker is first downloaded and
+ * installed. This is the perfect time to cache essential static assets that
+ * the app needs to function offline.
+ *
+ * Process:
+ * 1. Open the current cache bucket
+ * 2. Add all static assets to the cache
+ * 3. Skip waiting to activate immediately
+ * 4. Handle any cache failures gracefully
+ *
+ * Error Handling:
+ * If caching fails, the service worker will still install but with limited
+ * offline functionality. This prevents the entire app from breaking due to
+ * network issues during installation.
+ *
+ * @param {InstallEvent} event - Service worker install event
+ */
+self.addEventListener('install', (event) => {
+ console.log('Service Worker: Installing...');
+
+ event.waitUntil(
+ caches.open(CACHE_NAME)
+ .then((cache) => {
+ console.log('Service Worker: Caching static assets');
+ // Cache all essential files for offline functionality
+ return cache.addAll(STATIC_ASSETS);
+ })
+ .then(() => {
+ console.log('Service Worker: Static assets cached successfully');
+ // Skip waiting period and activate immediately
+ // This ensures users get the latest version without page refresh
+ self.skipWaiting();
+ })
+ .catch((error) => {
+ // Log cache failures but don't break the installation
+ console.warn('Service Worker: Cache installation failed:', error);
+ // Service worker will still install with limited offline capability
+ })
+ );
+});
+
+/**
+ * Activate Event Handler - Clean Up Old Caches
+ *
+ * The activate event fires after the service worker is installed and becomes
+ * the active service worker. This is when we clean up old cache versions to
+ * prevent storage bloat and ensure users get the latest content.
+ *
+ * Process:
+ * 1. Get all existing cache names
+ * 2. Delete any caches that don't match the current version
+ * 3. Claim control of all existing clients
+ *
+ * Cache Management:
+ * Only caches with names different from CACHE_NAME are deleted. This ensures
+ * we keep the current cache while removing outdated versions.
+ *
+ * @param {ExtendableEvent} event - Service worker activate event
+ */
+self.addEventListener('activate', (event) => {
+ console.log('Service Worker: Activating...');
+
+ event.waitUntil(
+ caches.keys()
+ .then((cacheNames) => {
+ console.log('Service Worker: Cleaning up old caches');
+
+ // Create array of promises to delete outdated caches
+ return Promise.all(
+ cacheNames.map((cacheName) => {
+ if (cacheName !== CACHE_NAME) {
+ console.log('Service Worker: Deleting old cache:', cacheName);
+ return caches.delete(cacheName);
+ }
+ // Return resolved promise for current cache (no deletion needed)
+ return Promise.resolve();
+ })
+ );
+ })
+ .then(() => {
+ console.log('Service Worker: Cache cleanup completed');
+ // Take control of all existing clients immediately
+ // This ensures the new service worker is used right away
+ self.clients.claim();
+ })
+ );
+});
+
+/**
+ * Fetch Event Handler - Optimized Cache-First Strategy with Intelligent Fallbacks
+ *
+ * This handler implements an advanced caching strategy for the application with
+ * performance optimizations and enhanced error handling. It intercepts network
+ * requests and provides intelligent caching with multiple fallback layers.
+ *
+ * Enhanced Strategy:
+ * 1. Pre-filter requests for optimal performance (method, origin, content-type)
+ * 2. Implement cache-first with intelligent cache key normalization
+ * 3. Provide network fallback with retry logic for transient failures
+ * 4. Cache successful responses with smart invalidation policies
+ * 5. Multi-layer offline fallbacks (cached content → offline page → error handling)
+ *
+ * Performance Optimizations:
+ * - Early request filtering to avoid unnecessary processing
+ * - Cache key normalization for better hit rates
+ * - Parallel cache checking and network requests where appropriate
+ * - Memory-efficient response cloning
+ *
+ * Request Filtering:
+ * - Only handles GET requests (POST/PUT/DELETE bypass for data integrity)
+ * - Only handles same-origin requests (prevents CORS complications)
+ * - Skips chrome-extension and other non-HTTP protocols
+ *
+ * Advanced Caching Logic:
+ * - Cache successful responses (200-299 status codes, basic/cors types)
+ * - Implement cache versioning for better invalidation
+ * - Handle partial content and range requests appropriately
+ * - Skip caching for private/no-cache headers
+ *
+ * Error Handling:
+ * - Graceful fallbacks for network failures
+ * - Retry logic for transient network errors
+ * - Comprehensive offline experience
+ *
+ * @param {FetchEvent} event - Service worker fetch event
+ */
+self.addEventListener('fetch', (event) => {
+ // Filter requests: Only handle GET requests
+ if (event.request.method !== 'GET') {
+ // Let non-GET requests (POST, PUT, DELETE) go directly to network
+ return;
+ }
+
+ // Filter requests: Only handle same-origin requests
+ if (!event.request.url.startsWith(self.location.origin)) {
+ // Let external requests go directly to network to avoid CORS issues
+ return;
+ }
+
+ // Implement cache-first strategy with network fallback
+ event.respondWith(
+ caches.match(event.request)
+ .then((cachedResponse) => {
+ // Return cached response if available (cache hit)
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ // Cache miss: fetch from network
+ return fetch(event.request)
+ .then((response) => {
+ // Validate response before caching
+ if (!response || response.status !== 200 || response.type !== 'basic') {
+ // Don't cache:
+ // - null/undefined responses
+ // - non-200 status codes (errors, redirects)
+ // - non-basic types (opaque, CORS responses)
+ return response;
+ }
+
+ // Clone response for caching (responses can only be consumed once)
+ const responseToCache = response.clone();
+
+ // Cache the successful response asynchronously
+ caches.open(CACHE_NAME)
+ .then((cache) => {
+ cache.put(event.request, responseToCache);
+ })
+ .catch((error) => {
+ console.warn('Service Worker: Failed to cache response:', error);
+ // Continue serving response even if caching fails
+ });
+
+ return response;
+ })
+ .catch(() => {
+ // Network failure: provide offline fallbacks
+ if (event.request.mode === 'navigate') {
+ // For navigation requests (page loads), serve the cached index.html
+ // This ensures the app loads even when completely offline
+ return caches.match('index.html');
+ }
+
+ // For other requests, let the error propagate
+ throw new Error('Network failed and no cache available');
+ });
+ })
+ );
+});