diff --git a/docs/plans/2026-03-24-modal-system-design.md b/docs/plans/2026-03-24-modal-system-design.md new file mode 100644 index 0000000..5fb12b6 --- /dev/null +++ b/docs/plans/2026-03-24-modal-system-design.md @@ -0,0 +1,202 @@ +# Modal System Design + +**Date:** 2026-03-24 +**Version:** 4.0.0 +**Status:** Approved + +## Overview + +Replace the existing dialog implementations with a unified modal system that provides: +- Glassmorphism backdrop matching app aesthetic +- Full accessibility (ARIA, focus trap, keyboard navigation) +- Smooth fade + scale animations +- Consistent API via `ModalManager` class + +## Decisions Made + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Architecture | Unified `ModalManager` class | Cleaner, consistent behavior across all modals | +| Backdrop style | Glassmorphism | Matches existing app design language | +| Focus management | Focus first interactive element | Standard, predictable behavior | +| Animation | Fade + scale (95% → 100%) | Modern, subtle effect | +| Implementation | Custom (not native ``) | Full control, no polyfill concerns | + +## Architecture + +### File Structure + +``` +src/ +├── utils/ +│ └── ModalManager.js # Core modal logic (~150 lines) +├── styles/ +│ └── modal.css # Unified modal styles (~200 lines) +└── index.html # Updated dialog markup +``` + +### ModalManager Class + +```javascript +class ModalManager { + constructor(element, options = {}) + open() // Show modal with animation + close() // Hide modal with animation + destroy() // Cleanup event listeners + on(event, callback) // Event subscription + + // Internal + #createBackdrop() // Create glassmorphism backdrop + #trapFocus() // Manage focus within modal + #handleKeydown(e) // Escape key handler + #getFocusableElements() // Query focusable children +} +``` + +### Events + +- `open` — Fired after open animation completes +- `close` — Fired after close animation completes + +## CSS Design + +### Variables (from tokens.css) + +```css +--z-modal: 200; +--transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1); +--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1); +--radius-lg: 0.5rem; +``` + +### Backdrop + +```css +.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); +} +``` + +### Modal Container + +```css +.modal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: calc(var(--z-modal) + 1); + opacity: 0; + visibility: hidden; + transition: opacity var(--transition-normal), + visibility var(--transition-normal); +} + +.modal.open { + opacity: 1; + visibility: visible; +} +``` + +### Modal Content (with animation) + +```css +.modal-content { + background: hsl(var(--background)); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + max-width: 90vw; + max-height: 90vh; + overflow: hidden; + transform: scale(0.95); + transition: transform var(--transition-normal); +} + +.modal.open .modal-content { + transform: scale(1); +} +``` + +## HTML Structure + +All dialogs convert to unified structure: + +```html + +``` + +### Key Attributes + +- `role="dialog"` — Screen reader identification +- `aria-modal="true"` — Prevents screen reader from accessing background +- `aria-labelledby` — References the dialog title +- `data-close` — Click handler for closing (backdrop, cancel buttons) + +## Accessibility Features + +1. **Focus trap** — Tab cycles within modal only +2. **Focus first element** — Auto-focuses first input/button on open +3. **Escape key** — Closes modal +4. **Click outside** — Clicking backdrop closes modal +5. **Focus restoration** — Returns focus to trigger element on close +6. **ARIA attributes** — Proper screen reader support + +## Dialogs to Migrate + +| Dialog ID | Current Class | Complexity | +|-----------|--------------|------------| +| `find-dialog` | `.find-dialog` | Simple | +| `export-dialog` | `.export-dialog` | Complex (many sections) | +| `print-preview-overlay` | `.export-dialog` | Medium | +| `table-generator-dialog` | `.export-dialog` | Simple | +| `ascii-art-dialog` | `.export-dialog` | Medium | +| `universal-converter-dialog` | `.export-dialog` | Complex | +| `batch-dialog` | `.batch-dialog` | Complex | +| `pdf-editor-dialog` | `.export-dialog` | Complex | +| `header-footer-dialog` | `.export-dialog` | Medium | +| `field-picker-dialog` | `.export-dialog` | Simple | + +## Migration Steps + +1. Create `src/utils/ModalManager.js` +2. Create `src/styles/modal.css` +3. Update `index.html` to include new stylesheet +4. Convert each dialog HTML to new structure +5. Initialize `ModalManager` instances in `renderer.js` +6. Remove old CSS from `styles.css` +7. Test all dialogs + +## Out of Scope + +- Modal nesting (stacked modals) — can be added later if needed +- Animated backdrop (currently static blur) +- Modal size variants (small/large/fullscreen) — can use inline styles