mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 23:11:09 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
162 lines
7.1 KiB
HTML
162 lines
7.1 KiB
HTML
{# Inner modal template - do not use directly, use modal_base.html instead #}
|
|
{# Enhanced with animations, focus trap, and loading states #}
|
|
{% comment %}
|
|
ACCESSIBILITY FEATURES:
|
|
- Focus trap: Tab/Shift+Tab cycles through focusable elements within modal
|
|
- Home/End keys: Jump to first/last focusable element
|
|
- Escape key: Close modal (configurable via close_on_escape)
|
|
- aria-modal="true": Indicates modal dialog semantics
|
|
- aria-labelledby: References modal title for screen readers
|
|
- aria-describedby: References modal body and subtitle for description
|
|
- Automatic focus: First focusable element receives focus on open
|
|
- Focus restoration: Focus returns to trigger element on close (handled by parent)
|
|
{% endcomment %}
|
|
|
|
{% with animation=animation|default:'scale' loading=loading|default:False %}
|
|
|
|
<div id="{{ modal_id }}"
|
|
x-show="{{ show_var }}"
|
|
x-cloak
|
|
{% if close_on_escape %}@keydown.escape.window="{{ show_var }} = false"{% endif %}
|
|
x-init="
|
|
$watch('{{ show_var }}', value => {
|
|
{% if prevent_scroll %}document.body.style.overflow = value ? 'hidden' : '';{% endif %}
|
|
if (value) {
|
|
$nextTick(() => {
|
|
const firstFocusable = $el.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');
|
|
if (firstFocusable) firstFocusable.focus();
|
|
});
|
|
}
|
|
});
|
|
"
|
|
@keydown.tab.prevent="
|
|
const focusables = $el.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');
|
|
const first = focusables[0];
|
|
const last = focusables[focusables.length - 1];
|
|
if ($event.shiftKey && document.activeElement === first) {
|
|
last.focus();
|
|
} else if (!$event.shiftKey && document.activeElement === last) {
|
|
first.focus();
|
|
} else {
|
|
return true;
|
|
}
|
|
"
|
|
@keydown.home.prevent="
|
|
const focusables = $el.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');
|
|
if (focusables[0]) focusables[0].focus();
|
|
"
|
|
@keydown.end.prevent="
|
|
const focusables = $el.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');
|
|
if (focusables[focusables.length - 1]) focusables[focusables.length - 1].focus();
|
|
"
|
|
class="fixed inset-0 z-[60] flex items-center justify-center p-4"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
{% if title %}aria-labelledby="{{ modal_id }}-title"{% endif %}
|
|
{% if subtitle %}aria-describedby="{{ modal_id }}-subtitle {{ modal_id }}-body"{% else %}aria-describedby="{{ modal_id }}-body"{% endif %}>
|
|
|
|
{# Backdrop #}
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"
|
|
x-show="{{ show_var }}"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
{% if close_on_backdrop %}@click="{{ show_var }} = false"{% endif %}
|
|
aria-hidden="true">
|
|
</div>
|
|
|
|
{# Modal Content #}
|
|
<div class="relative w-full {{ size_class }} bg-background rounded-xl shadow-2xl overflow-hidden border border-border"
|
|
x-show="{{ show_var }}"
|
|
{% if animation == 'slide-up' %}
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 translate-y-8"
|
|
x-transition:enter-end="opacity-100 translate-y-0"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 translate-y-0"
|
|
x-transition:leave-end="opacity-0 translate-y-8"
|
|
{% elif animation == 'fade' %}
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
{% else %}
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
{% endif %}
|
|
@click.stop>
|
|
|
|
{# Loading Overlay #}
|
|
{% if loading %}
|
|
<div x-show="loading"
|
|
class="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
|
<div class="flex flex-col items-center gap-3">
|
|
<div class="w-8 h-8 border-4 border-primary rounded-full animate-spin border-t-transparent"></div>
|
|
<span class="text-sm text-muted-foreground">Loading...</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Header #}
|
|
{% if title or show_close_button %}
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
{% block modal_header %}
|
|
{% if title %}
|
|
<div class="flex items-center gap-3">
|
|
{% if icon %}
|
|
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
<i class="{{ icon }} text-primary" aria-hidden="true"></i>
|
|
</div>
|
|
{% endif %}
|
|
<div>
|
|
<h3 id="{{ modal_id }}-title" class="text-lg font-semibold text-foreground">
|
|
{{ title }}
|
|
</h3>
|
|
{% if subtitle %}
|
|
<p id="{{ modal_id }}-subtitle" class="text-sm text-muted-foreground">{{ subtitle }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div></div>
|
|
{% endif %}
|
|
{% endblock modal_header %}
|
|
|
|
{% if show_close_button %}
|
|
<button type="button"
|
|
@click="{{ show_var }} = false"
|
|
class="p-2 -mr-2 text-muted-foreground hover:text-foreground rounded-lg hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 transition-colors"
|
|
aria-label="Close {{ title|default:'modal' }}">
|
|
<i class="fas fa-times text-lg" aria-hidden="true"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Body #}
|
|
<div id="{{ modal_id }}-body" class="px-6 py-4 overflow-y-auto max-h-[70vh]">
|
|
{% block modal_body %}{% endblock modal_body %}
|
|
</div>
|
|
|
|
{# Footer (optional) #}
|
|
{% block modal_footer_wrapper %}
|
|
{% if block.modal_footer %}
|
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border bg-muted/30">
|
|
{% block modal_footer %}{% endblock modal_footer %}
|
|
</div>
|
|
{% endif %}
|
|
{% endblock modal_footer_wrapper %}
|
|
</div>
|
|
</div>
|
|
|
|
{% endwith %}
|