{# Modal Component Library #} {# Usage: {{ import '_modals.html' }} then call modal macros #} {# Modal Container Macro Parameters: - id (str): Modal HTML id attribute (optional) - title (str): Modal title (shows in header) - size (str): 'sm' (480px), 'md' (620px), 'lg' (720px), 'xl' (960px) (default: 'md') - position (str): 'center', 'top' (default: 'center') - scrollable (bool): Allow content scrolling (default: True) - classes (str): Additional CSS classes for modal container - content_classes (str): Additional CSS classes for modal content box - show_close (bool): Show close button in header (default: True) - backdrop_click_close (bool): Close on backdrop click (default: True) - role (str): ARIA role (default: 'dialog') - aria_labelledby (str): ARIA labelledby id (default: auto-generated from title) Content Blocks: - header: Optional custom header content (overrides default title) - body: Main modal content (required) - footer: Optional footer with action buttons Examples: {% call modal(title='Edit Card', size='md') %} {% block body %}
{% endblock %} {% block footer %} {{ button('Cancel', variant='secondary', onclick='closeModal()') }} {{ button('Save', type='submit') }} {% endblock %} {% endcall %} #} {% macro modal(id='', title='', size='md', position='center', scrollable=True, classes='', content_classes='', show_close=True, backdrop_click_close=True, role='dialog', aria_labelledby='') %} {%- set modal_id = id if id else 'modal-' ~ title|lower|replace(' ', '-') -%} {%- set title_id = aria_labelledby if aria_labelledby else modal_id + '-title' -%} {%- set size_class = 'modal-' + size -%} {%- set position_class = 'modal-' + position -%} {%- set scrollable_class = 'modal-scrollable' if scrollable else '' -%} {%- set all_classes = ['modal', size_class, position_class, scrollable_class, classes]|select|join(' ') -%}Are you sure you want to delete this deck?
', footer_buttons=[ {'text': 'Cancel', 'variant': 'secondary', 'onclick': 'closeModal()'}, {'text': 'Delete', 'variant': 'danger', 'onclick': 'deleteDeck()'} ] ) }} #} {% macro simple_modal(title='', content='', footer_buttons=[], id='', size='md', position='center', scrollable=True, classes='', show_close=True) %} {%- set modal_id = id if id else 'modal-' ~ title|lower|replace(' ', '-') -%} {%- set title_id = modal_id + '-title' -%} {%- set size_class = 'modal-' + size -%} {%- set position_class = 'modal-' + position -%} {%- set scrollable_class = 'modal-scrollable' if scrollable else '' -%} {%- set all_classes = ['modal', size_class, position_class, scrollable_class, classes]|select|join(' ') -%}' + message + '
', footer_buttons=[ {'text': cancel_text, 'variant': 'secondary', 'onclick': on_cancel if on_cancel else "this.closest('.modal').remove()"}, {'text': confirm_text, 'variant': confirm_variant, 'onclick': on_confirm} ], size='sm', classes='modal-confirm ' + classes ) }} {% endmacro %} {# Form Modal Macro Parameters: Similar to modal() plus: - form_action (str): Form action URL (hx-post or action) - form_method (str): 'post', 'get' (default: 'post') - use_htmx (bool): Use HTMX for form submission (default: True) - hx_target (str): HTMX target selector (default: 'closest .modal') - hx_swap (str): HTMX swap strategy (default: 'outerHTML') - submit_text (str): Submit button text (default: 'Submit') - cancel_text (str): Cancel button text (default: 'Cancel') - form_attrs (str): Additional form attributes Content Blocks: - body: Form fields (required) Examples: {% call form_modal( title='Add Card', form_action='/build/add-card', submit_text='Add', hx_target='#deck-list' ) %} {% block body %} {% endblock %} {% endcall %} #} {% macro form_modal(title='', form_action='', form_method='post', use_htmx=True, hx_target='closest .modal', hx_swap='outerHTML', submit_text='Submit', cancel_text='Cancel', size='md', classes='', form_attrs='') %} {%- set modal_id = 'modal-form-' ~ title|lower|replace(' ', '-') -%} {%- set title_id = modal_id + '-title' -%} {%- set size_class = 'modal-' + size -%} {%- set all_classes = ['modal', 'modal-form', size_class, classes]|select|join(' ') -%}' + message + '
', footer_buttons=[ {'text': ok_text, 'variant': 'primary', 'onclick': on_ok if on_ok else "this.closest('.modal').remove()"} ], size='sm', classes='modal-alert ' + type_class + ' ' + classes, show_close=False ) }} {% endmacro %} {# CSS Classes Reference #} {# Modal Sizes: - .modal-sm (480px max-width) - .modal-md (620px max-width, default) - .modal-lg (720px max-width) - .modal-xl (960px max-width) Modal Position: - .modal-center (vertically centered, default) - .modal-top (aligned to top with padding) Modal Modifiers: - .modal-scrollable (allows body scrolling) - .modal-form (form-specific styling) - .modal-confirm (confirmation dialog styling) - .modal-alert (alert dialog styling) - .modal-alert-info (blue theme) - .modal-alert-success (green theme) - .modal-alert-warning (yellow theme) - .modal-alert-error (red theme) Modal Structure: - .modal (outer container, fixed positioning) - .modal-backdrop (backdrop overlay) - .modal-content (content box) - .modal-header (header with title and close button) - .modal-title (h2 title) - .modal-body (main content area) - .modal-footer (action buttons) #} {# JavaScript Helper Functions #} {# These functions should be included in a global JavaScript file or inline script: // Open modal by ID function openModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; } } // Close modal by ID or element function closeModal(modalOrId) { const modal = typeof modalOrId === 'string' ? document.getElementById(modalOrId) : modalOrId; if (modal) { modal.remove(); // Check if any other modals are open if (!document.querySelector('.modal')) { document.body.style.overflow = ''; } } } // Close all modals function closeAllModals() { document.querySelectorAll('.modal').forEach(modal => modal.remove()); document.body.style.overflow = ''; } #}