Files
markdown-converter/docs/plans/2026-03-24-modal-system.md
T

33 KiB

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

/**
 * 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

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

/**
 * 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

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:

    <link rel="stylesheet" href="styles/tokens.css">

Add after it:

    <link rel="stylesheet" href="styles/modal.css">

Step 2: Commit

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:

    <script src="utils/ModalManager.js"></script>

Step 2: Commit

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:

        <!-- Find & Replace Dialog -->
        <div id="find-dialog" class="modal" role="dialog" aria-modal="true" aria-labelledby="find-dialog-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content small">
                <div class="modal-header">
                    <h3 id="find-dialog-title">Find & Replace</h3>
                    <button class="modal-close" id="btn-find-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body">
                    <div class="find-controls">
                        <input type="text" id="find-input" placeholder="Find..." class="input">
                        <input type="text" id="replace-input" placeholder="Replace..." class="input">
                    </div>
                    <div class="find-buttons" style="display: flex; gap: 8px; margin-top: 8px;">
                        <button id="btn-find-prev" class="btn btn-secondary" title="Previous"></button>
                        <button id="btn-find-next" class="btn btn-secondary" title="Next"></button>
                        <button id="btn-replace" class="btn btn-secondary" title="Replace">Replace</button>
                        <button id="btn-replace-all" class="btn btn-secondary" title="Replace All">All</button>
                    </div>
                </div>
                <div class="modal-footer">
                    <span id="find-count" style="flex: 1; color: hsl(var(--muted-foreground));">0 matches</span>
                </div>
            </div>
        </div>

Step 2: Add CSS for find controls in modal.css

Append to src/styles/modal.css:

/* ============================================
 * Find Dialog Specific
 * ============================================ */

.find-controls {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-2, 0.5rem);
}

.find-controls input {
    width: 100%;
}

Step 3: Update renderer.js to use ModalManager

Find the find dialog show/hide code and replace with ModalManager usage.

Add near top of renderer.js (after DOMContentLoaded):

// Initialize ModalManager for find dialog
let findModal = null;

document.addEventListener('DOMContentLoaded', () => {
    findModal = new ModalManager('#find-dialog');
    // ... rest of initialization
});

Replace show/hide calls:

// Old: document.getElementById('find-dialog').classList.remove('hidden');
// New:
findModal.open();

// Old: document.getElementById('find-dialog').classList.add('hidden');
// New:
findModal.close();

Step 4: Commit

git add src/index.html src/renderer.js src/styles/modal.css
git commit -m "feat: convert find dialog to unified modal system"

Task 6: Convert Export Dialog

Files:

  • Modify: src/index.html (lines 176-337)
  • Modify: src/renderer.js (export dialog handlers)

Step 1: Update HTML structure

Replace the export-dialog div with new modal structure. Keep all the internal content, just wrap in modal classes:

        <!-- Export Options Dialog -->
        <div id="export-dialog" class="modal" role="dialog" aria-modal="true" aria-labelledby="export-dialog-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content large">
                <div class="modal-header">
                    <h3 id="export-dialog-title">Export Options</h3>
                    <button class="modal-close" id="export-dialog-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body">
                    <!-- Keep ALL existing content from export-dialog-body here -->
                </div>
                <div class="modal-footer">
                    <button id="export-cancel" class="btn btn-secondary" data-close>Cancel</button>
                    <button id="export-confirm" class="btn btn-primary">Export</button>
                </div>
            </div>
        </div>

Step 2: Update renderer.js

// Initialize export modal
let exportModal = null;

// In DOMContentLoaded:
exportModal = new ModalManager('#export-dialog');

// Replace showExportDialog function:
function showExportDialog(format) {
    console.log('showExportDialog called with format:', format);
    const title = document.getElementById('export-dialog-title');

    title.textContent = `Export as ${format.toUpperCase()}`;
    document.getElementById('export-dialog').setAttribute('data-format', format);

    initializeExportForm(format);
    exportModal.open();
}

// Replace hideExportDialog function:
function hideExportDialog() {
    exportModal.close();
    currentExportFormat = null;
}

Step 3: Remove old event listeners

Remove the manual click and escape key handlers for export-dialog since ModalManager handles them.

Step 4: Commit

git add src/index.html src/renderer.js
git commit -m "feat: convert export dialog to unified modal system"

Task 7: Convert Print Preview Dialog

Files:

  • Modify: src/index.html (lines 339-420)
  • Modify: src/renderer.js

Step 1: Update HTML structure

        <!-- Print Preview Dialog -->
        <div id="print-preview-overlay" class="modal" role="dialog" aria-modal="true" aria-labelledby="print-preview-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content full">
                <div class="modal-header">
                    <h3 id="print-preview-title">Print Preview</h3>
                    <button class="modal-close" id="print-preview-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body print-preview-body">
                    <!-- Keep existing print preview content -->
                </div>
            </div>
        </div>

Step 2: Update renderer.js

let printPreviewModal = null;

// In DOMContentLoaded:
printPreviewModal = new ModalManager('#print-preview-overlay');

function openPrintPreviewDialog() {
    printPreviewModal.open();
    // ... rest of existing logic
}

Step 3: Commit

git add src/index.html src/renderer.js
git commit -m "feat: convert print preview dialog to unified modal system"

Task 8: Convert Table Generator Dialog

Files:

  • Modify: src/index.html (lines 422-471)
  • Modify: src/renderer.js

Step 1: Update HTML structure

        <!-- Table Generator Dialog -->
        <div id="table-generator-dialog" class="modal" role="dialog" aria-modal="true" aria-labelledby="table-dialog-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content">
                <div class="modal-header">
                    <h3 id="table-dialog-title">Table Generator</h3>
                    <button class="modal-close" id="table-dialog-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body">
                    <!-- Keep existing table generator content -->
                </div>
                <div class="modal-footer">
                    <button id="table-dialog-cancel" class="btn btn-secondary" data-close>Cancel</button>
                    <button id="table-dialog-insert" class="btn btn-primary">Insert Table</button>
                </div>
            </div>
        </div>

Step 2: Update renderer.js

let tableModal = null;

// In DOMContentLoaded:
tableModal = new ModalManager('#table-generator-dialog');

Step 3: Commit

git add src/index.html src/renderer.js
git commit -m "feat: convert table generator dialog to unified modal system"

Task 9: Convert ASCII Art Dialog

Files:

  • Modify: src/index.html (lines 473-582)
  • Modify: src/renderer.js

Step 1: Update HTML structure

        <!-- ASCII Art Generator Dialog -->
        <div id="ascii-art-dialog" class="modal" role="dialog" aria-modal="true" aria-labelledby="ascii-dialog-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content large">
                <div class="modal-header">
                    <h3 id="ascii-dialog-title">ASCII Art Generator</h3>
                    <button class="modal-close" id="ascii-dialog-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body">
                    <!-- Keep existing ASCII art content -->
                </div>
                <div class="modal-footer">
                    <button id="ascii-dialog-cancel" class="btn btn-secondary" data-close>Cancel</button>
                    <button id="ascii-dialog-insert" class="btn btn-primary">Insert ASCII Art</button>
                </div>
            </div>
        </div>

Step 2: Update renderer.js

let asciiModal = null;

// In DOMContentLoaded:
asciiModal = new ModalManager('#ascii-art-dialog');

Step 3: Commit

git add src/index.html src/renderer.js
git commit -m "feat: convert ASCII art dialog to unified modal system"

Task 10: Convert Universal Converter Dialog

Files:

  • Modify: src/index.html (lines 584-810)
  • Modify: src/renderer.js

Step 1: Update HTML structure

        <!-- Universal File Converter Dialog -->
        <div id="universal-converter-dialog" class="modal" role="dialog" aria-modal="true" aria-labelledby="converter-dialog-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content large">
                <div class="modal-header">
                    <h3 id="converter-dialog-title">Universal File Converter</h3>
                    <button class="modal-close" id="converter-dialog-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body">
                    <!-- Keep existing converter content -->
                </div>
                <div class="modal-footer">
                    <button id="converter-dialog-cancel" class="btn btn-secondary" data-close>Cancel</button>
                    <button id="converter-dialog-convert" class="btn btn-primary">Convert</button>
                </div>
            </div>
        </div>

Step 2: Update renderer.js

let converterModal = null;

// In DOMContentLoaded:
converterModal = new ModalManager('#universal-converter-dialog');

Step 3: Commit

git add src/index.html src/renderer.js
git commit -m "feat: convert universal converter dialog to unified modal system"

Task 11: Convert Batch Dialog

Files:

  • Modify: src/index.html (lines 812-879)
  • Modify: src/renderer.js

Step 1: Update HTML structure

        <!-- Batch Conversion Dialog -->
        <div id="batch-dialog" class="modal" role="dialog" aria-modal="true" aria-labelledby="batch-dialog-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content large">
                <div class="modal-header">
                    <h3 id="batch-dialog-title">Batch Conversion</h3>
                    <button class="modal-close" id="batch-dialog-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body">
                    <!-- Keep existing batch content -->
                </div>
                <div class="modal-footer">
                    <button id="batch-dialog-cancel" class="btn btn-secondary" data-close>Cancel</button>
                    <button id="batch-dialog-start" class="btn btn-primary">Start Conversion</button>
                </div>
            </div>
        </div>

Step 2: Update renderer.js

let batchModal = null;

// In DOMContentLoaded:
batchModal = new ModalManager('#batch-dialog');

// Replace showBatchDialog:
function showBatchDialog() {
    batchModal.open();
}

// Replace hideBatchDialog:
function hideBatchDialog() {
    batchModal.close();
}

Step 3: Commit

git add src/index.html src/renderer.js
git commit -m "feat: convert batch dialog to unified modal system"

Task 12: Convert PDF Editor Dialog

Files:

  • Modify: src/index.html (lines 881-1274)
  • Modify: src/renderer.js

Step 1: Update HTML structure

        <!-- PDF Editor Dialog -->
        <div id="pdf-editor-dialog" class="modal" role="dialog" aria-modal="true" aria-labelledby="pdf-editor-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content full">
                <div class="modal-header">
                    <h3 id="pdf-editor-title">PDF Editor</h3>
                    <button class="modal-close" id="pdf-editor-dialog-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body pdf-editor-body">
                    <!-- Keep existing PDF editor content -->
                </div>
                <div class="modal-footer">
                    <button id="pdf-editor-cancel" class="btn btn-secondary" data-close>Cancel</button>
                    <button id="pdf-editor-process" class="btn btn-primary">Process PDF</button>
                </div>
            </div>
        </div>

Step 2: Update renderer.js

let pdfEditorModal = null;

// In DOMContentLoaded:
pdfEditorModal = new ModalManager('#pdf-editor-dialog');

Step 3: Commit

git add src/index.html src/renderer.js
git commit -m "feat: convert PDF editor dialog to unified modal system"

Files:

  • Modify: src/index.html (lines 1276-1368)
  • Modify: src/renderer.js

Step 1: Update HTML structure

        <!-- Header & Footer Configuration Dialog -->
        <div id="header-footer-dialog" class="modal" role="dialog" aria-modal="true" aria-labelledby="header-footer-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content">
                <div class="modal-header">
                    <h3 id="header-footer-title">Header & Footer Configuration</h3>
                    <button class="modal-close" id="header-footer-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body">
                    <!-- Keep existing header/footer content -->
                </div>
                <div class="modal-footer">
                    <button id="header-footer-cancel" class="btn btn-secondary" data-close>Cancel</button>
                    <button id="header-footer-save" class="btn btn-primary">Save</button>
                </div>
            </div>
        </div>

Step 2: Update renderer.js

let headerFooterModal = null;

// In DOMContentLoaded:
headerFooterModal = new ModalManager('#header-footer-dialog');

Step 3: Commit

git add src/index.html src/renderer.js
git commit -m "feat: convert header & footer dialog to unified modal system"

Task 14: Convert Field Picker Dialog

Files:

  • Modify: src/index.html (lines 1370-1440)
  • Modify: src/renderer.js

Step 1: Update HTML structure

        <!-- Dynamic Field Picker Dialog -->
        <div id="field-picker-dialog" class="modal" role="dialog" aria-modal="true" aria-labelledby="field-picker-title">
            <div class="modal-backdrop" data-close></div>
            <div class="modal-content small">
                <div class="modal-header">
                    <h3 id="field-picker-title">Insert Field</h3>
                    <button class="modal-close" id="field-picker-close" aria-label="Close">&times;</button>
                </div>
                <div class="modal-body">
                    <!-- Keep existing field picker content -->
                </div>
            </div>
        </div>

Step 2: Update renderer.js

let fieldPickerModal = null;

// In DOMContentLoaded:
fieldPickerModal = new ModalManager('#field-picker-dialog');

Step 3: Commit

git add src/index.html src/renderer.js
git commit -m "feat: convert field picker dialog to unified modal system"

Task 15: Remove Old Dialog CSS

Files:

  • Modify: src/styles.css

Step 1: Remove old .export-dialog, .batch-dialog, .find-dialog CSS

Find and remove these sections from src/styles.css:

  • .find-dialog and related selectors (lines ~1244-1328)
  • .export-dialog and related selectors (lines ~1332-1540)
  • .batch-dialog and related selectors

Step 2: Commit

git add src/styles.css
git commit -m "refactor: remove old dialog CSS in favor of unified modal system"

Task 16: Test All Modals

Step 1: Run the application

npm start

Step 2: Verify each modal

  • Find dialog (Ctrl+F) - opens/closes, focus trap works
  • Export dialog (Ctrl+E or File > Export) - all options visible
  • Print preview (Ctrl+P) - renders correctly
  • Table generator - inserts table
  • ASCII art - generates art
  • Universal converter - file selection works
  • Batch conversion - file list works
  • PDF editor - opens PDF
  • Header & footer - saves settings
  • Field picker - inserts fields

Step 3: Test accessibility

  • Tab cycles within open modal
  • Escape closes modal
  • Click backdrop closes modal
  • Focus returns to trigger element after close

Task 17: Build Windows Release

Files:

  • Run: build command

Step 1: Build release

npm run build:win

Step 2: Verify artifacts

ls -la dist/*.exe

Expected output:

  • dist/MarkdownConverter-Setup-4.0.0.exe
  • dist/MarkdownConverter-4.0.0-x64.exe

Step 3: Final commit with version bump if needed

git add -A
git commit -m "release: v4.0.0 with unified modal system"
git push origin master

Summary

Task Description Files Modified
1 Create ModalManager class src/utils/ModalManager.js
2 Create modal CSS src/styles/modal.css
3 Include modal CSS src/index.html
4 Include ModalManager script src/index.html
5 Convert find dialog src/index.html, src/renderer.js
6 Convert export dialog src/index.html, src/renderer.js
7 Convert print preview src/index.html, src/renderer.js
8 Convert table generator src/index.html, src/renderer.js
9 Convert ASCII art src/index.html, src/renderer.js
10 Convert universal converter src/index.html, src/renderer.js
11 Convert batch dialog src/index.html, src/renderer.js
12 Convert PDF editor src/index.html, src/renderer.js
13 Convert header/footer src/index.html, src/renderer.js
14 Convert field picker src/index.html, src/renderer.js
15 Remove old CSS src/styles.css
16 Test all modals -
17 Build release -