Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX

This commit is contained in:
pacnpal
2025-12-22 16:56:27 -05:00
parent 2e35f8c5d9
commit ae31e889d7
144 changed files with 25792 additions and 4440 deletions

View File

@@ -0,0 +1,191 @@
# HTMX Components
This directory contains HTMX-related template components for loading states, error handling, and success feedback.
## Loading State Guidelines
### When to Use Each Type
| Scenario | Component | Example |
|----------|-----------|---------|
| Initial page load, full content replacement | Skeleton screens | Parks list loading |
| Button actions, form submissions | Loading indicator (inline) | Submit button spinner |
| Partial content updates | Loading indicator (block) | Search results loading |
| Container replacement | Loading indicator (overlay) | Modal content loading |
### Components
#### loading_indicator.html
Standardized loading indicator for HTMX requests with three modes:
**Inline Mode** - For buttons and links:
```django
<button hx-post="/api/action" hx-indicator="#btn-loading">
Save
{% include 'htmx/components/loading_indicator.html' with id='btn-loading' mode='inline' %}
</button>
```
**Block Mode** (default) - For content areas:
```django
<div hx-get="/api/list" hx-indicator="#list-loading">
<!-- Content -->
</div>
{% include 'htmx/components/loading_indicator.html' with id='list-loading' %}
```
**Overlay Mode** - For containers:
```django
<div class="relative" hx-get="/api/data" hx-indicator="#overlay-loading">
<!-- Content to cover -->
{% include 'htmx/components/loading_indicator.html' with id='overlay-loading' mode='overlay' %}
</div>
```
#### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `id` | str | - | ID for hx-indicator targeting |
| `message` | str | "Loading..." | Loading text to display |
| `mode` | str | "block" | 'inline', 'block', or 'overlay' |
| `size` | str | "md" | 'sm', 'md', or 'lg' |
| `spinner` | str | "border" | 'spin' (fa-spinner) or 'border' (CSS) |
## Skeleton Screens
Located in `components/skeletons/`, these provide content-aware loading placeholders:
### Available Skeletons
| Component | Use Case |
|-----------|----------|
| `list_skeleton.html` | List views, search results |
| `card_grid_skeleton.html` | Card-based grid layouts |
| `detail_skeleton.html` | Detail/show pages |
| `form_skeleton.html` | Form loading states |
| `table_skeleton.html` | Data tables |
### Usage with HTMX
```django
{# Initial content shows skeleton, replaced by HTMX #}
<div id="parks-list"
hx-get="/parks/"
hx-trigger="load"
hx-swap="innerHTML">
{% include 'components/skeletons/card_grid_skeleton.html' with cards=6 %}
</div>
```
### With hx-indicator
```django
<div id="results">
<!-- Results here -->
</div>
{# Skeleton shown during loading #}
<div id="results-skeleton" class="htmx-indicator">
{% include 'components/skeletons/list_skeleton.html' %}
</div>
```
## Error Handling
### error_message.html
Displays error messages with consistent styling:
```django
{% include 'htmx/components/error_message.html' with
message="Unable to load data"
show_retry=True
retry_url="/api/data"
%}
```
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `message` | str | - | Error message text |
| `code` | str | - | HTTP status code |
| `show_retry` | bool | False | Show retry button |
| `retry_url` | str | - | URL for retry action |
| `show_report` | bool | False | Show "Report Issue" link |
## Success Feedback
### success_toast.html
Triggers a toast notification via HTMX response:
```django
{% include 'htmx/components/success_toast.html' with
message="Park saved successfully"
type="success"
%}
```
### Using HX-Trigger Header
From views, trigger toasts via response headers:
```python
from django.http import HttpResponse
def save_park(request):
# ... save logic ...
response = HttpResponse()
response['HX-Trigger'] = json.dumps({
'showToast': {
'type': 'success',
'message': 'Park saved successfully'
}
})
return response
```
## HTMX Configuration
The base template configures HTMX with:
- 30-second timeout
- Global view transitions enabled
- Template fragments enabled
- Comprehensive error handling
### Swap Strategies
| Strategy | Use Case |
|----------|----------|
| `innerHTML` | Replace content inside container (lists, search results) |
| `outerHTML` | Replace entire element (status badges, individual items) |
| `beforeend` | Append items (infinite scroll) |
| `afterbegin` | Prepend items (new items at top) |
### Target Naming Conventions
- `#object-type-id` - For specific objects (e.g., `#park-123`)
- `#section-name` - For page sections (e.g., `#results`, `#filters`)
- `#modal-container` - For modals
- `this` - For self-replacement
### Custom Events
- `{model}-status-changed` - Status updates (e.g., `park-status-changed`)
- `auth-changed` - Authentication state changes
- `{model}-created` - New item created
- `{model}-updated` - Item updated
- `{model}-deleted` - Item deleted
## Best Practices
1. **Always specify hx-indicator** for user feedback
2. **Use skeleton screens** for initial page loads and full content replacement
3. **Use inline indicators** for button actions
4. **Use overlay indicators** for modal content loading
5. **Add aria attributes** for accessibility (role="status", aria-busy)
6. **Clean up loading states** after request completion (automatic with HTMX)

View File

@@ -1,33 +1,125 @@
{% comment %}
Loading Indicator Component
HTMX Loading Indicator Component
================================
Displays a loading spinner for HTMX requests.
A standardized loading indicator for HTMX requests.
Optional context:
- size: 'sm', 'md', or 'lg' (defaults to 'md')
- inline: Whether to render inline (defaults to false)
- message: Loading message text (defaults to 'Loading...')
- id: Optional ID for the indicator element
Purpose:
Provides consistent loading feedback during HTMX requests.
Can be used inline, block, or as an overlay.
Usage Examples:
Inline spinner (in button/link):
<button hx-get="/api/data" hx-indicator="#btn-loading">
Load Data
{% include 'htmx/components/loading_indicator.html' with id='btn-loading' inline=True size='sm' %}
</button>
Block indicator (below content):
<div hx-get="/api/list" hx-indicator="#list-loading">
Content here
</div>
{% include 'htmx/components/loading_indicator.html' with id='list-loading' message='Loading items...' %}
Overlay indicator (covers container):
<div class="relative" hx-get="/api/data" hx-indicator="#overlay-loading">
Content to cover
{% include 'htmx/components/loading_indicator.html' with id='overlay-loading' mode='overlay' %}
</div>
Custom spinner:
{% include 'htmx/components/loading_indicator.html' with id='custom-loading' spinner='border' %}
Parameters:
Optional:
- id: Unique identifier for the indicator (used with hx-indicator)
- message: Loading text to display (default: 'Loading...')
- mode: 'inline', 'block', or 'overlay' (default: 'block')
- inline: Shortcut for mode='inline' (backwards compatible)
- size: 'sm', 'md', 'lg' (default: 'md')
- spinner: 'spin' (fa-spinner) or 'border' (CSS animation) (default: 'border')
Dependencies:
- HTMX for .htmx-indicator class behavior
- Tailwind CSS for styling
- Font Awesome icons (for 'spin' spinner)
Accessibility:
- Uses role="status" and aria-live="polite" for screen readers
- aria-hidden while not loading (HTMX handles visibility)
- Loading message announced to screen readers
{% endcomment %}
{% if inline %}
<!-- Inline Loading Indicator -->
<span class="htmx-indicator inline-flex items-center gap-2 {% if size == 'sm' %}text-sm{% elif size == 'lg' %}text-lg{% endif %}"
{% if id %}id="{{ id }}"{% endif %}
aria-hidden="true">
<i class="fas fa-spinner fa-spin text-blue-500"></i>
{% if message %}<span class="text-gray-500 dark:text-gray-400">{{ message }}</span>{% endif %}
</span>
{% else %}
<!-- Block Loading Indicator -->
<div class="htmx-indicator flex items-center justify-center p-4 {% if size == 'sm' %}p-2{% elif size == 'lg' %}p-6{% endif %}"
{# Support both 'inline' param and 'mode' param #}
{% with actual_mode=mode|default:inline|yesno:'inline,block' %}
{% if actual_mode == 'overlay' %}
{# ============================================
Overlay Mode - Covers parent element
Parent must have position: relative
============================================ #}
<div class="htmx-indicator absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm z-10 rounded-lg"
{% if id %}id="{{ id }}"{% endif %}
aria-hidden="true">
role="status"
aria-live="polite">
<div class="flex flex-col items-center gap-3">
{# Spinner #}
{% if spinner == 'spin' %}
<i class="fas fa-spinner fa-spin {% if size == 'sm' %}text-xl{% elif size == 'lg' %}text-5xl{% else %}text-3xl{% endif %} text-blue-500"
aria-hidden="true"></i>
{% else %}
<div class="{% if size == 'sm' %}w-6 h-6 border-3{% elif size == 'lg' %}w-12 h-12 border-4{% else %}w-8 h-8 border-4{% endif %} border-blue-500 rounded-full animate-spin border-t-transparent"
aria-hidden="true"></div>
{% endif %}
{# Message #}
<span class="{% if size == 'sm' %}text-sm{% elif size == 'lg' %}text-lg{% else %}text-base{% endif %} font-medium text-gray-700 dark:text-gray-300">
{{ message|default:"Loading..." }}
</span>
</div>
</div>
{% elif actual_mode == 'inline' or inline %}
{# ============================================
Inline Mode - For use within buttons/links
============================================ #}
<span class="htmx-indicator inline-flex items-center gap-2"
{% if id %}id="{{ id }}"{% endif %}
role="status"
aria-live="polite">
{% if spinner == 'spin' or spinner == '' %}
<i class="fas fa-spinner fa-spin {% if size == 'sm' %}text-sm{% elif size == 'lg' %}text-lg{% endif %} text-blue-500"
aria-hidden="true"></i>
{% else %}
<div class="{% if size == 'sm' %}w-4 h-4 border-2{% elif size == 'lg' %}w-6 h-6 border-3{% else %}w-5 h-5 border-2{% endif %} border-current rounded-full animate-spin border-t-transparent"
aria-hidden="true"></div>
{% endif %}
{% if message %}<span class="text-gray-500 dark:text-gray-400">{{ message }}</span>{% endif %}
{% if not message %}<span class="sr-only">Loading...</span>{% endif %}
</span>
{% else %}
{# ============================================
Block Mode (default) - Centered block
============================================ #}
<div class="htmx-indicator flex items-center justify-center {% if size == 'sm' %}p-2{% elif size == 'lg' %}p-6{% else %}p-4{% endif %}"
{% if id %}id="{{ id }}"{% endif %}
role="status"
aria-live="polite">
<div class="flex items-center gap-3">
<div class="{% if size == 'sm' %}w-5 h-5{% elif size == 'lg' %}w-10 h-10{% else %}w-8 h-8{% endif %} border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
{# Spinner #}
{% if spinner == 'spin' %}
<i class="fas fa-spinner fa-spin {% if size == 'sm' %}text-lg{% elif size == 'lg' %}text-3xl{% else %}text-2xl{% endif %} text-blue-500"
aria-hidden="true"></i>
{% else %}
<div class="{% if size == 'sm' %}w-5 h-5 border-3{% elif size == 'lg' %}w-10 h-10 border-4{% else %}w-8 h-8 border-4{% endif %} border-blue-500 rounded-full animate-spin border-t-transparent"
aria-hidden="true"></div>
{% endif %}
{# Message #}
<span class="{% if size == 'sm' %}text-sm{% elif size == 'lg' %}text-lg{% else %}text-base{% endif %} text-gray-600 dark:text-gray-300">
{{ message|default:"Loading..." }}
</span>
</div>
</div>
{% endif %}
{% endwith %}

View File

@@ -1,9 +0,0 @@
<nav class="htmx-pagination" role="navigation" aria-label="Pagination">
{% if page_obj.has_previous %}
<button hx-get="{{ request.path }}?page={{ page_obj.previous_page_number }}" hx-swap="#results">Previous</button>
{% endif %}
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<button hx-get="{{ request.path }}?page={{ page_obj.next_page_number }}" hx-swap="#results">Next</button>
{% endif %}
</nav>