# Modal System Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Replace existing dialog implementations with a unified modal system providing glassmorphism backdrop, full accessibility, and smooth animations. **Architecture:** Single `ModalManager` class manages all modals with focus trap, keyboard handling, and dynamic backdrop. All 10 dialogs convert to unified HTML structure with new CSS classes. **Tech Stack:** Vanilla JavaScript, CSS custom properties (design tokens), no external dependencies --- ## Task 1: Create ModalManager Class **Files:** - Create: `src/utils/ModalManager.js` **Step 1: Create utils directory and ModalManager class** ```javascript /** * ModalManager - Unified modal system with accessibility support * @version 4.0.0 */ export class ModalManager { #modal; #backdrop; #options; #lastFocusedElement; #focusableElements; #eventListeners; #isOpen; static #openModals = []; constructor(element, options = {}) { this.#modal = typeof element === 'string' ? document.querySelector(element) : element; this.#options = { closeOnBackdrop: true, closeOnEscape: true, focusFirst: true, onOpen: null, onClose: null, ...options }; this.#isOpen = false; this.#eventListeners = []; this.#init(); } #init() { // Ensure modal has required attributes if (!this.#modal.hasAttribute('role')) { this.#modal.setAttribute('role', 'dialog'); } if (!this.#modal.hasAttribute('aria-modal')) { this.#modal.setAttribute('aria-modal', 'true'); } // Find or create backdrop this.#backdrop = this.#modal.querySelector('.modal-backdrop'); // Setup close triggers this.#setupCloseTriggers(); } #setupCloseTriggers() { // Close button const closeBtn = this.#modal.querySelector('.modal-close'); if (closeBtn) { const handler = (e) => { e.preventDefault(); this.close(); }; closeBtn.addEventListener('click', handler); this.#eventListeners.push({ el: closeBtn, type: 'click', handler }); } // Elements with data-close attribute const closeTriggers = this.#modal.querySelectorAll('[data-close]'); closeTriggers.forEach(el => { if (el.classList.contains('modal-backdrop') && !this.#options.closeOnBackdrop) { return; } const handler = (e) => { e.preventDefault(); this.close(); }; el.addEventListener('click', handler); this.#eventListeners.push({ el, type: 'click', handler }); }); } #getFocusableElements() { const selector = [ 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'a[href]', '[tabindex]:not([tabindex="-1"])' ].join(', '); return Array.from(this.#modal.querySelectorAll(selector)) .filter(el => el.offsetParent !== null && !el.classList.contains('modal-backdrop')); } #trapFocus(e) { if (e.key !== 'Tab') return; const focusable = this.#getFocusableElements(); if (focusable.length === 0) return; const firstEl = focusable[0]; const lastEl = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === firstEl) { e.preventDefault(); lastEl.focus(); } } else { if (document.activeElement === lastEl) { e.preventDefault(); firstEl.focus(); } } } #handleKeydown(e) { if (e.key === 'Escape' && this.#options.closeOnEscape) { e.preventDefault(); this.close(); } this.#trapFocus(e); } open() { if (this.#isOpen) return; // Store last focused element this.#lastFocusedElement = document.activeElement; // Track open modals ModalManager.#openModals.push(this); // Show modal this.#modal.classList.add('open'); this.#isOpen = true; // Prevent body scroll document.body.style.overflow = 'hidden'; // Add keyboard listener const keydownHandler = (e) => this.#handleKeydown(e); document.addEventListener('keydown', keydownHandler); this.#eventListeners.push({ el: document, type: 'keydown', handler: keydownHandler }); // Focus first element if (this.#options.focusFirst) { requestAnimationFrame(() => { const focusable = this.#getFocusableElements(); if (focusable.length > 0) { focusable[0].focus(); } }); } // Callback if (this.#options.onOpen) { this.#options.onOpen(this); } // Dispatch custom event this.#modal.dispatchEvent(new CustomEvent('modal:open')); } close() { if (!this.#isOpen) return; // Remove from open modals const index = ModalManager.#openModals.indexOf(this); if (index > -1) { ModalManager.#openModals.splice(index, 1); } // Hide modal this.#modal.classList.remove('open'); this.#isOpen = false; // Restore body scroll if no modals open if (ModalManager.#openModals.length === 0) { document.body.style.overflow = ''; } // Remove keyboard listener const keydownListener = this.#eventListeners.find( l => l.el === document && l.type === 'keydown' ); if (keydownListener) { document.removeEventListener('keydown', keydownListener.handler); this.#eventListeners = this.#eventListeners.filter(l => l !== keydownListener); } // Restore focus if (this.#lastFocusedElement && typeof this.#lastFocusedElement.focus === 'function') { this.#lastFocusedElement.focus(); } // Callback if (this.#options.onClose) { this.#options.onClose(this); } // Dispatch custom event this.#modal.dispatchEvent(new CustomEvent('modal:close')); } isOpen() { return this.#isOpen; } destroy() { // Close if open if (this.#isOpen) { this.close(); } // Remove all event listeners this.#eventListeners.forEach(({ el, type, handler }) => { el.removeEventListener(type, handler); }); this.#eventListeners = []; } } // Export for use in renderer window.ModalManager = ModalManager; ``` **Step 2: Commit ModalManager** ```bash git add src/utils/ModalManager.js git commit -m "feat: add ModalManager class for unified modal system" ``` --- ## Task 2: Create Modal CSS Styles **Files:** - Create: `src/styles/modal.css` **Step 1: Create modal.css with glassmorphism styles** ```css /** * Modal System Styles * Unified modal components with glassmorphism backdrop * @version 4.0.0 */ /* ============================================ * Modal Backdrop - Glassmorphism * ============================================ */ .modal-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); z-index: var(--z-modal, 200); cursor: pointer; } /* ============================================ * Modal Container * ============================================ */ .modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: calc(var(--z-modal, 200) + 1); opacity: 0; visibility: hidden; transition: opacity var(--transition-normal, 200ms cubic-bezier(0.4, 0, 0.2, 1)), visibility var(--transition-normal, 200ms cubic-bezier(0.4, 0, 0.2, 1)); padding: var(--spacing-4, 1rem); } .modal.open { opacity: 1; visibility: visible; } /* ============================================ * Modal Content - With Animation * ============================================ */ .modal-content { background: hsl(var(--background, 0 0% 100%)); border-radius: var(--radius-lg, 0.5rem); box-shadow: var(--shadow-xl, 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)); max-width: 90vw; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; transform: scale(0.95); transition: transform var(--transition-normal, 200ms cubic-bezier(0.4, 0, 0.2, 1)); } .modal.open .modal-content { transform: scale(1); } /* ============================================ * Modal Header * ============================================ */ .modal-header { display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-4, 1rem) var(--spacing-6, 1.5rem); border-bottom: 1px solid hsl(var(--border, 214.3 31.8% 91.4%)); background: hsl(var(--muted, 210 40% 96.1%)); } .modal-header h3 { margin: 0; font-size: var(--text-lg, 1.125rem); font-weight: var(--font-semibold, 600); color: hsl(var(--foreground, 222.2 84% 4.9%)); } /* ============================================ * Modal Close Button * ============================================ */ .modal-close { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: var(--radius, 0.5rem); background: transparent; color: hsl(var(--muted-foreground, 215.4 16.3% 46.9%)); font-size: 24px; line-height: 1; cursor: pointer; transition: background-color var(--transition-fast, 150ms), color var(--transition-fast, 150ms); } .modal-close:hover { background: hsl(var(--accent, 210 40% 96.1%)); color: hsl(var(--foreground, 222.2 84% 4.9%)); } .modal-close:focus-visible { outline: 2px solid hsl(var(--ring, 227 44% 52%)); outline-offset: 2px; } /* ============================================ * Modal Body * ============================================ */ .modal-body { padding: var(--spacing-6, 1.5rem); overflow-y: auto; flex: 1; } /* ============================================ * Modal Footer * ============================================ */ .modal-footer { display: flex; justify-content: flex-end; gap: var(--spacing-3, 0.75rem); padding: var(--spacing-4, 1rem) var(--spacing-6, 1.5rem); border-top: 1px solid hsl(var(--border, 214.3 31.8% 91.4%)); background: hsl(var(--muted, 210 40% 96.1%)); } .modal-footer .btn { min-width: 80px; } /* ============================================ * Form Elements within Modal * ============================================ */ .modal-body .form-row { display: flex; align-items: center; gap: var(--spacing-3, 0.75rem); margin-bottom: var(--spacing-3, 0.75rem); } .modal-body .form-row label { min-width: 100px; font-size: var(--text-sm, 0.875rem); color: hsl(var(--foreground, 222.2 84% 4.9%)); } .modal-body .form-row input, .modal-body .form-row select { flex: 1; padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem); border: 1px solid hsl(var(--input, 214.3 31.8% 91.4%)); border-radius: var(--radius, 0.5rem); font-size: var(--text-sm, 0.875rem); background: hsl(var(--background, 0 0% 100%)); color: hsl(var(--foreground, 222.2 84% 4.9%)); } .modal-body .form-row input:focus, .modal-body .form-row select:focus { outline: 2px solid hsl(var(--ring, 227 44% 52%)); outline-offset: 1px; } .modal-body .export-section { margin-bottom: var(--spacing-4, 1rem); padding-bottom: var(--spacing-4, 1rem); border-bottom: 1px solid hsl(var(--border, 214.3 31.8% 91.4%)); } .modal-body .export-section:last-child { border-bottom: none; margin-bottom: 0; } .modal-body .export-section label { display: block; font-weight: var(--font-medium, 500); margin-bottom: var(--spacing-2, 0.5rem); color: hsl(var(--foreground, 222.2 84% 4.9%)); } .modal-body .checkbox-group label { display: flex; align-items: center; gap: var(--spacing-2, 0.5rem); margin-bottom: var(--spacing-2, 0.5rem); font-weight: var(--font-normal, 400); cursor: pointer; } /* ============================================ * Size Variants * ============================================ */ .modal-content.small { max-width: 400px; } .modal-content.large { max-width: 800px; } .modal-content.full { max-width: 95vw; max-height: 95vh; } /* ============================================ * Dark Mode Support * ============================================ */ .dark .modal-content, [data-theme="dark"] .modal-content { background: hsl(var(--background)); box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3); } .dark .modal-header, .dark .modal-footer, [data-theme="dark"] .modal-header, [data-theme="dark"] .modal-footer { background: hsl(var(--muted)); } /* ============================================ * Accessibility - Reduced Motion * ============================================ */ @media (prefers-reduced-motion: reduce) { .modal, .modal-content { transition: none; } } /* ============================================ * Legacy Support - Hidden class * ============================================ */ .modal.hidden { display: none; } ``` **Step 2: Commit modal.css** ```bash git add src/styles/modal.css git commit -m "feat: add modal CSS with glassmorphism and animations" ``` --- ## Task 3: Include Modal CSS in HTML **Files:** - Modify: `src/index.html` (line ~18) **Step 1: Add modal.css link after tokens.css** Find this line in `src/index.html`: ```html ``` Add after it: ```html ``` **Step 2: Commit** ```bash git add src/index.html git commit -m "feat: include modal.css in index.html" ``` --- ## Task 4: Include ModalManager in HTML **Files:** - Modify: `src/index.html` (near end of body, before renderer.js) **Step 1: Add script tag for ModalManager** Find the script tags near the end of body and add before renderer.js: ```html ``` **Step 2: Commit** ```bash git add src/index.html git commit -m "feat: include ModalManager script in index.html" ``` --- ## Task 5: Convert Find Dialog **Files:** - Modify: `src/index.html` (lines 160-174) - Modify: `src/renderer.js` (find dialog handlers) **Step 1: Update HTML structure** Replace the find-dialog div (lines 160-174) with: ```html