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,375 @@
# ThrillWiki Interaction Patterns
This document describes the standardized interaction patterns used throughout ThrillWiki.
## Alpine.js Stores
ThrillWiki uses Alpine.js stores for global client-side state management.
### Toast Store
The toast store manages notification messages displayed to users.
```javascript
// Show a success toast
Alpine.store('toast').success('Park saved successfully!');
// Show an error toast
Alpine.store('toast').error('Failed to save park');
// Show a warning toast
Alpine.store('toast').warning('Your session will expire soon');
// Show an info toast
Alpine.store('toast').info('New features are available');
// Toast with custom duration (in ms)
Alpine.store('toast').success('Quick message', 2000);
// Toast with action button
Alpine.store('toast').success('Item deleted', 5000, {
action: {
label: 'Undo',
onClick: () => undoDelete()
}
});
// Persistent toast (duration = 0)
Alpine.store('toast').error('Connection lost', 0);
```
### Theme Store
The theme store manages dark/light mode preferences.
```javascript
// Get current theme
const isDark = Alpine.store('theme').isDark;
// Toggle theme
Alpine.store('theme').toggle();
// Set specific theme
Alpine.store('theme').set('dark');
Alpine.store('theme').set('light');
Alpine.store('theme').set('system');
```
### Auth Store
The auth store tracks user authentication state.
```javascript
// Check if user is authenticated
if (Alpine.store('auth').isAuthenticated) {
// Show authenticated content
}
// Get current user
const user = Alpine.store('auth').user;
```
## HTMX Patterns
### Standard Request Pattern
```html
<button hx-post="/api/items/create/"
hx-target="#items-list"
hx-swap="innerHTML"
hx-indicator="#loading">
Create Item
</button>
```
### Form Submission
```html
<form hx-post="/items/create/"
hx-target="#form-container"
hx-swap="outerHTML"
hx-indicator=".form-loading">
{% csrf_token %}
<!-- Form fields -->
<button type="submit">Save</button>
</form>
```
### Inline Validation
```html
<input type="email"
name="email"
hx-post="/validate/email/"
hx-trigger="blur changed delay:500ms"
hx-target="next .field-feedback"
hx-swap="innerHTML">
<div class="field-feedback"></div>
```
### Infinite Scroll
```html
<div id="items-container">
{% for item in items %}
{% include 'items/_card.html' %}
{% endfor %}
{% if has_next %}
<div hx-get="?page={{ next_page }}"
hx-trigger="revealed"
hx-swap="outerHTML"
hx-indicator="#scroll-loading">
<div id="scroll-loading" class="htmx-indicator">
Loading more...
</div>
</div>
{% endif %}
</div>
```
### Modal Loading
```html
<button hx-get="/items/{{ item.id }}/edit/"
hx-target="#modal-container"
hx-swap="innerHTML"
@click="showModal = true">
Edit
</button>
<div id="modal-container" x-show="showModal">
<!-- Modal content loaded here -->
</div>
```
## Event Handling
### HTMX Events
```javascript
// Before request
document.body.addEventListener('htmx:beforeRequest', (event) => {
// Show loading state
});
// After successful swap
document.body.addEventListener('htmx:afterSwap', (event) => {
// Initialize new content
});
// Handle errors
document.body.addEventListener('htmx:responseError', (event) => {
Alpine.store('toast').error('Request failed');
});
```
### Custom Events
```javascript
// Trigger from server via HX-Trigger header
document.body.addEventListener('showToast', (event) => {
const { type, message, duration } = event.detail;
Alpine.store('toast')[type](message, duration);
});
// Close modal event
document.body.addEventListener('closeModal', () => {
Alpine.store('modal').close();
});
// Refresh section event
document.body.addEventListener('refreshSection', (event) => {
const { target, url } = event.detail;
htmx.ajax('GET', url, target);
});
```
## Loading States
### Button Loading
```html
<button type="submit"
x-data="{ loading: false }"
@click="loading = true"
:disabled="loading"
:class="{ 'opacity-50 cursor-not-allowed': loading }">
<span x-show="!loading">Save</span>
<span x-show="loading" class="flex items-center gap-2">
<i class="fas fa-spinner fa-spin"></i>
Saving...
</span>
</button>
```
### Section Loading
```html
<div id="content"
hx-get="/content/"
hx-trigger="load"
hx-indicator="#content-loading">
<div id="content-loading" class="htmx-indicator">
{% include 'components/skeletons/list_skeleton.html' %}
</div>
</div>
```
### Global Loading Bar
The application includes a global loading bar that appears during HTMX requests:
```css
.htmx-request .loading-bar {
display: block;
}
```
## Focus Management
### Modal Focus Trap
Modals automatically trap focus within their bounds:
```html
{% include 'components/modals/modal_base.html' with
modal_id='my-modal'
show_var='showModal'
title='Modal Title'
%}
```
The modal component handles:
- Focus moves to first focusable element on open
- Tab cycles through modal elements only
- Escape key closes modal
- Focus returns to trigger element on close
### Form Focus
After validation errors, focus moves to the first invalid field:
```javascript
document.body.addEventListener('htmx:afterSwap', (event) => {
const firstError = event.target.querySelector('.field-error input');
if (firstError) {
firstError.focus();
}
});
```
## Keyboard Navigation
### Standard Shortcuts
| Key | Action |
|-----|--------|
| `Escape` | Close modal/dropdown |
| `Enter` | Submit form / Activate button |
| `Tab` | Move to next focusable element |
| `Shift+Tab` | Move to previous focusable element |
| `Arrow Up/Down` | Navigate dropdown options |
### Custom Shortcuts
```javascript
// Global keyboard shortcuts
document.addEventListener('keydown', (event) => {
// Ctrl/Cmd + K: Open search
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault();
Alpine.store('search').open();
}
});
```
## Animation Patterns
### Entry Animations
```html
<div x-show="visible"
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">
Content
</div>
```
### Slide Animations
```html
<div x-show="visible"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
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-4">
Content
</div>
```
### Stagger Animations
```html
<template x-for="(item, index) in items" :key="item.id">
<div x-show="visible"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
:style="{ transitionDelay: `${index * 50}ms` }">
<!-- Item content -->
</div>
</template>
```
## Error Handling
### Graceful Degradation
All interactions work without JavaScript:
```html
<!-- Works with or without JS -->
<form method="post" action="/items/create/"
hx-post="/items/create/"
hx-target="#form-container">
<!-- Form content -->
</form>
```
### Error Recovery
```javascript
// Retry failed requests
document.body.addEventListener('htmx:responseError', (event) => {
const { xhr, target } = event.detail;
if (xhr.status >= 500) {
Alpine.store('toast').error('Server error. Please try again.', 0, {
action: {
label: 'Retry',
onClick: () => htmx.trigger(target, 'htmx:load')
}
});
}
});
```
### Offline Handling
```javascript
window.addEventListener('offline', () => {
Alpine.store('toast').warning('You are offline', 0);
});
window.addEventListener('online', () => {
Alpine.store('toast').success('Connection restored');
});
```