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,336 @@
# Design System Migration Guide
This guide helps migrate from the legacy frontend system to the unified design system.
## Overview
The ThrillWiki frontend was consolidated from two parallel systems:
- **Legacy System** (`/backend/templates/`, `/backend/static/css/`) - HSL-based variables, Font Awesome icons
- **Modern System** (`/templates/`, `/static/css/`) - RGB hex-based design tokens, SVG icons
The unified system uses `design-tokens.css` as the single source of truth.
## CSS Migration
### Color Variables
Replace HSL-based variables with design tokens:
```css
/* Before (legacy) */
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
/* After (design tokens) */
background-color: var(--color-primary);
color: var(--color-primary-foreground);
```
### Common Replacements
| Legacy | Design Token |
|--------|--------------|
| `hsl(var(--primary))` | `var(--color-primary)` |
| `hsl(var(--secondary))` | `var(--color-secondary)` |
| `hsl(var(--background))` | `var(--color-background)` |
| `hsl(var(--foreground))` | `var(--color-foreground)` |
| `hsl(var(--muted))` | `var(--color-muted)` |
| `hsl(var(--accent))` | `var(--color-accent)` |
| `hsl(var(--destructive))` | `var(--color-destructive)` |
| `hsl(var(--border))` | `var(--color-border)` |
| `hsl(var(--card))` | `var(--color-card)` |
### Font Variables
```css
/* Before */
font-family: var(--font-sans);
/* After */
font-family: var(--font-family-sans);
```
### Shadow Variables
```css
/* Before */
box-shadow: var(--shadow);
/* After */
box-shadow: var(--shadow-base);
```
## Icon Migration
Replace Font Awesome icons with the SVG icon component:
```html
<!-- Before (Font Awesome) -->
<i class="fa fa-search"></i>
<i class="fas fa-user"></i>
<i class="far fa-heart"></i>
<!-- After (SVG Icon Component) -->
{% include "components/ui/icon.html" with name="search" %}
{% include "components/ui/icon.html" with name="user" %}
{% include "components/ui/icon.html" with name="heart" %}
```
### Icon Name Mapping
| Font Awesome | SVG Icon |
|--------------|----------|
| `fa-search` | `search` |
| `fa-user` | `user` |
| `fa-users` | `users` |
| `fa-cog` | `settings` |
| `fa-heart` | `heart` |
| `fa-star` | `star` |
| `fa-home` | `home` |
| `fa-edit` | `edit` |
| `fa-trash` | `trash` |
| `fa-copy` | `copy` |
| `fa-external-link` | `external-link` |
| `fa-chevron-down` | `chevron-down` |
| `fa-chevron-up` | `chevron-up` |
| `fa-chevron-left` | `chevron-left` |
| `fa-chevron-right` | `chevron-right` |
| `fa-check` | `check` |
| `fa-times` | `close` |
| `fa-plus` | `plus` |
| `fa-minus` | `minus` |
| `fa-bars` | `menu` |
### Icon Sizes
```html
<!-- Before -->
<i class="fa fa-search fa-sm"></i>
<i class="fa fa-search fa-lg"></i>
<i class="fa fa-search fa-2x"></i>
<!-- After -->
{% include "components/ui/icon.html" with name="search" size="sm" %}
{% include "components/ui/icon.html" with name="search" size="lg" %}
{% include "components/ui/icon.html" with name="search" size="xl" %}
```
## Button Migration
### Basic Buttons
```html
<!-- Before -->
<button class="btn btn-primary">Click Me</button>
<button class="btn btn-secondary">Click Me</button>
<button class="btn btn-danger">Delete</button>
<button class="btn btn-outline">Outline</button>
<!-- After -->
{% include "components/ui/button.html" with text="Click Me" variant="default" %}
{% include "components/ui/button.html" with text="Click Me" variant="secondary" %}
{% include "components/ui/button.html" with text="Delete" variant="destructive" %}
{% include "components/ui/button.html" with text="Outline" variant="outline" %}
```
### Buttons with Icons
```html
<!-- Before -->
<button class="btn btn-primary">
<i class="fa fa-search mr-2"></i>
Search
</button>
<!-- After -->
{% include "components/ui/button.html" with text="Search" icon="search" %}
```
### Link Buttons
```html
<!-- Before -->
<a href="/url" class="btn btn-primary">Go</a>
<!-- After -->
{% include "components/ui/button.html" with text="Go" href="/url" %}
```
## Form Input Migration
```html
<!-- Before -->
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" class="form-control" placeholder="Enter email">
<small class="form-text text-muted">We'll never share your email.</small>
</div>
<!-- After -->
{% include "components/ui/input.html" with
name="email"
label="Email"
type="email"
placeholder="Enter email"
hint="We'll never share your email."
%}
```
## Card Migration
```html
<!-- Before -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Title</h3>
</div>
<div class="card-body">
<p>Content here</p>
</div>
<div class="card-footer">
<button class="btn">Action</button>
</div>
</div>
<!-- After -->
{% include "components/ui/card.html" with
title="Title"
body_content="<p>Content here</p>"
footer_content="<button class='btn'>Action</button>"
%}
```
## Modal Migration
```html
<!-- Before -->
<div class="modal" id="myModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Title</h5>
<button class="close">&times;</button>
</div>
<div class="modal-body">Content</div>
<div class="modal-footer">
<button class="btn">Close</button>
</div>
</div>
</div>
</div>
<!-- After -->
{% include "components/ui/dialog.html" with
id="myModal"
title="Title"
content="Content"
footer="<button class='btn'>Close</button>"
%}
```
## Alpine.js Migration
### Store Definitions
Move inline store definitions to `stores/index.js`:
```javascript
// Before (inline in template)
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('theme', {
isDark: false,
toggle() { this.isDark = !this.isDark; }
});
});
</script>
// After (use centralized store)
// Store is already defined in stores/index.js
// Access via $store.theme.toggle()
```
### Component Definitions
Move inline component definitions to `alpine-components.js`:
```javascript
// Before (inline in template)
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open">Content</div>
</div>
// After (use registered component)
<div x-data="dropdown()">
<button @click="toggle()">Toggle</button>
<div x-show="open">Content</div>
</div>
```
## Alert/Toast Migration
```html
<!-- Before -->
<div class="alert alert-success">
<strong>Success!</strong> Your action was completed.
</div>
<!-- After -->
<div class="alert alert-success" role="alert">
<div class="alert-title">Success!</div>
<div class="alert-description">Your action was completed.</div>
</div>
<!-- Or use toast store for dynamic notifications -->
<script>
Alpine.store('toast').success('Your action was completed.');
</script>
```
## Responsive Classes Migration
```html
<!-- Before -->
<div class="d-none d-md-block">Hidden on mobile</div>
<div class="d-md-none">Only on mobile</div>
<div class="row">
<div class="col-12 col-md-6">...</div>
</div>
<!-- After -->
<div class="hidden-mobile">Hidden on mobile</div>
<div class="show-mobile">Only on mobile</div>
<div class="grid-responsive-2">
<div>...</div>
<div>...</div>
</div>
```
## Checklist
Use this checklist when migrating a template:
- [ ] Replace HSL color variables with design tokens
- [ ] Replace Font Awesome icons with SVG icon component
- [ ] Update button markup to use button component
- [ ] Update form inputs to use input component
- [ ] Update cards to use card component
- [ ] Update modals to use dialog component
- [ ] Remove inline Alpine.js store definitions
- [ ] Update responsive classes
- [ ] Test dark mode appearance
- [ ] Test focus states for accessibility
- [ ] Test on mobile viewport
## Testing Migration
After migrating, visit `/design-system-test/` to compare your migrated components against the reference implementation.
## Getting Help
If you encounter issues during migration:
1. Check the design system README for component documentation
2. Review `design-tokens.css` for available tokens
3. Inspect the design system test page for examples

View File

@@ -0,0 +1,405 @@
# ThrillWiki Design System
A unified design system for the ThrillWiki application, providing consistent styling, components, and utilities across the entire frontend.
## Quick Start
### Directory Structure
```
backend/
├── static/
│ ├── css/
│ │ └── design-tokens.css # Primary design tokens (colors, typography, spacing)
│ └── js/
│ ├── alpine-components.js # Alpine.js components
│ └── stores/index.js # Alpine.js stores
└── templates/
├── base/
│ └── base.html # Base template (extend this)
└── components/
└── ui/
├── button.html # Button component
├── card.html # Card component
├── dialog.html # Modal/dialog component
├── icon.html # SVG icon component
└── input.html # Form input component
```
### CSS Loading Order
CSS files must be loaded in this specific order:
```html
<link href="{% static 'css/design-tokens.css' %}" rel="stylesheet">
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet">
<link href="{% static 'css/components.css' %}" rel="stylesheet">
```
### JavaScript Loading Order
```html
<!-- Alpine.js stores (loaded before Alpine) -->
<script src="{% static 'js/stores/index.js' %}"></script>
<!-- Alpine.js plugins -->
<script defer src="https://unpkg.com/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://unpkg.com/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine.js core -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine.js components (after Alpine loads) -->
<script src="{% static 'js/alpine-components.js' %}"></script>
```
## Design Tokens
Design tokens are CSS custom properties that define the visual language of the application.
### Color System
#### Color Scales
Each color has a scale from 50-950:
```css
--color-primary-50 /* Lightest */
--color-primary-100
--color-primary-200
--color-primary-300
--color-primary-400
--color-primary-500 /* Base */
--color-primary-600
--color-primary-700
--color-primary-800
--color-primary-900
--color-primary-950 /* Darkest */
```
Available color scales:
- `primary` - Blue (brand color)
- `secondary` - Slate (neutral)
- `accent` - Red (highlights)
- `success` - Green
- `warning` - Amber
- `error` - Red
- `info` - Sky blue
#### Semantic Colors
These adapt automatically for dark mode:
```css
--color-background /* Page background */
--color-foreground /* Primary text */
--color-muted /* Muted backgrounds */
--color-muted-foreground /* Secondary text */
--color-border /* Border color */
--color-card /* Card background */
--color-card-foreground /* Card text */
--color-primary /* Primary action color */
--color-primary-foreground /* Text on primary */
--color-destructive /* Destructive actions */
```
### Typography
```css
/* Font Families */
--font-family-sans /* Inter - body text */
--font-family-serif /* Playfair Display - headings */
--font-family-mono /* JetBrains Mono - code */
/* Font Sizes */
--font-size-xs /* 12px */
--font-size-sm /* 14px */
--font-size-base /* 16px */
--font-size-lg /* 18px */
--font-size-xl /* 20px */
--font-size-2xl /* 24px */
--font-size-3xl /* 30px */
--font-size-4xl /* 36px */
```
### Spacing Scale
```css
--spacing-1 /* 4px */
--spacing-2 /* 8px */
--spacing-3 /* 12px */
--spacing-4 /* 16px */
--spacing-6 /* 24px */
--spacing-8 /* 32px */
--spacing-12 /* 48px */
--spacing-16 /* 64px */
```
### Shadows
```css
--shadow-sm /* Subtle shadow */
--shadow-base /* Default shadow */
--shadow-md /* Medium shadow */
--shadow-lg /* Large shadow */
--shadow-xl /* Extra large shadow */
--shadow-2xl /* Maximum shadow */
```
### Border Radius
```css
--radius-sm /* 2px */
--radius-base /* 4px */
--radius-md /* 6px */
--radius-lg /* 8px */
--radius-xl /* 12px */
--radius-full /* 9999px (pill) */
```
### Breakpoints
```css
--breakpoint-sm /* 640px - Mobile landscape */
--breakpoint-md /* 768px - Tablets */
--breakpoint-lg /* 1024px - Desktops */
--breakpoint-xl /* 1280px - Large screens */
--breakpoint-2xl /* 1536px - Extra large */
```
## Components
### Button Component
```django
{% include "components/ui/button.html" with
text="Button Text"
variant="default" {# default|secondary|destructive|outline|ghost|link #}
size="default" {# default|sm|lg|icon #}
icon="search" {# Optional: icon name #}
disabled=False
type="button" {# button|submit|reset #}
href="" {# If provided, renders as <a> #}
hx_get="/url" {# HTMX attributes #}
hx_post="/url"
hx_target="#element"
hx_swap="innerHTML"
x_on_click="handler" {# Alpine.js attributes #}
%}
```
### Card Component
```django
{% include "components/ui/card.html" with
title="Card Title"
description="Optional description"
body_content="<p>Card content here</p>"
footer_content="<button>Action</button>"
class="additional-classes"
%}
```
### Input Component
```django
{% include "components/ui/input.html" with
name="field_name"
label="Field Label"
type="text" {# text|email|password|number|textarea #}
placeholder="Placeholder text"
value=""
required=False
disabled=False
error="Error message"
hint="Helper text"
%}
```
### Dialog/Modal Component
```django
{% include "components/ui/dialog.html" with
id="modal-id"
title="Dialog Title"
description="Optional description"
content="<p>Dialog content</p>"
footer="<button>Action</button>"
size="default" {# sm|default|lg|xl|full #}
closable=True
open=True
%}
```
### Icon Component
```django
{% include "components/ui/icon.html" with
name="search" {# Icon name from library #}
size="md" {# xs|sm|md|lg|xl #}
class="text-primary" {# Additional classes #}
%}
```
Available icons: search, menu, close, chevron-up, chevron-down, chevron-left, chevron-right, arrow-up, arrow-down, arrow-left, arrow-right, user, users, settings, cog, heart, heart-filled, star, star-filled, home, edit, trash, copy, external-link, download, upload, check, check-circle, x-circle, info, warning, error, plus, minus, filter, sort, calendar, clock, map-pin, phone, mail, globe, link, image, camera, play, pause, volume, bell, bookmark, share, refresh, eye, eye-off, lock, unlock, sun, moon, loader
## Utility Classes
### Responsive Utilities
```css
/* Visibility */
.hidden-mobile /* Hidden on mobile */
.hidden-sm /* Hidden on sm breakpoint */
.show-mobile /* Only visible on mobile */
.show-lg /* Only visible on lg breakpoint */
/* Grid */
.grid-responsive-2 /* 1 col mobile, 2 cols sm+ */
.grid-responsive-3 /* 1 col mobile, 2 cols sm+, 3 cols lg+ */
.grid-responsive-4 /* 1 col mobile, 2 cols sm+, 4 cols lg+ */
.grid-auto-fit /* Auto-fit grid with 300px min */
/* Flex */
.stack-to-row /* Column on mobile, row on sm+ */
.stack-to-row-lg /* Column on mobile/tablet, row on lg+ */
/* Spacing */
.py-responsive /* 16px mobile, 24px sm, 32px lg */
.px-responsive /* Same for horizontal */
.gap-responsive /* Responsive gap */
```
### Container Classes
```css
.container /* max-width: 1280px */
.container-sm /* max-width: 640px */
.container-md /* max-width: 768px */
.container-lg /* max-width: 1024px */
.container-xl /* max-width: 1280px */
.container-2xl /* max-width: 1536px */
```
### Accessibility Utilities
```css
.sr-only /* Visually hidden, screen reader accessible */
.sr-only-focusable /* sr-only that becomes visible on focus */
.focus-ring /* Focus ring on :focus-visible */
.touch-target /* Minimum 44x44px touch target */
.skip-link /* Skip to content link */
```
## Alpine.js Stores
### Theme Store
```javascript
// Access
Alpine.store('theme')
// Properties
$store.theme.isDark // Current theme state
$store.theme.systemTheme // System preference
// Methods
$store.theme.toggle() // Toggle theme
$store.theme.set('dark') // Set specific theme
```
### Toast Store
```javascript
// Show notifications
Alpine.store('toast').show('Message', 'success')
Alpine.store('toast').success('Success message')
Alpine.store('toast').error('Error message')
Alpine.store('toast').warning('Warning message')
Alpine.store('toast').info('Info message')
```
### Auth Store
```javascript
// Access current user
$store.auth.user // User object or null
$store.auth.isLoggedIn // Boolean
$store.auth.hasPermission('permission_name')
```
### UI Store
```javascript
// Modal management
Alpine.store('ui').openModal('modal-id')
Alpine.store('ui').closeModal('modal-id')
Alpine.store('ui').isModalOpen('modal-id')
// Sidebar
Alpine.store('ui').toggleSidebar()
$store.ui.isSidebarOpen
```
## Dark Mode
Dark mode is automatically supported through CSS custom properties. Toggle with:
```html
<button @click="$store.theme.toggle()">
Toggle Theme
</button>
```
Or use the theme toggle component in the navbar.
## Accessibility
### Focus Management
All interactive elements have visible focus indicators using `:focus-visible`. Custom focus rings can be added with `.focus-ring`.
### Reduced Motion
Animations are automatically disabled for users who prefer reduced motion:
```css
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; }
}
```
### Screen Reader Support
Use `.sr-only` for screen reader text:
```html
<button>
<svg>...</svg>
<span class="sr-only">Close menu</span>
</button>
```
### Touch Targets
Ensure interactive elements meet minimum touch target size (44x44px):
```html
<button class="touch-target">...</button>
```
## Testing
Visit `/design-system-test/` (development only) to see all components rendered with their various states and options.
## Migration from Legacy System
If migrating from the legacy system:
1. Replace HSL color variables with design token references
2. Replace Font Awesome icons with `{% include "components/ui/icon.html" %}`
3. Update button classes from `.btn-primary` to the button component
4. Replace inline Alpine.js stores with centralized stores from `stores/index.js`
5. Update responsive classes to use new utility classes
See `MIGRATION.md` for detailed migration steps.

165
docs/ux/README.md Normal file
View File

@@ -0,0 +1,165 @@
# ThrillWiki UX Documentation
This directory contains comprehensive UX documentation for the ThrillWiki Django application.
## Overview
ThrillWiki uses a modern stack combining:
- **Django Templates** for server-side rendering
- **HTMX** for seamless AJAX interactions
- **Alpine.js** for client-side state management
- **Tailwind CSS** for styling
## Documentation Index
### Core Patterns
- [Interaction Patterns](./interaction-patterns.md) - Alpine.js stores, HTMX patterns, event handling
- [HTMX Conventions](./htmx-conventions.md) - Standardized HTMX usage patterns
- [CTA Guidelines](./cta-guidelines.md) - Button placement and action patterns
### Components
- [Component Library](./component-library.md) - Catalog of all reusable components
- [Navigation Components](../backend/templates/components/navigation/README.md)
- [Form Components](../backend/templates/forms/README.md)
- [HTMX Components](../backend/templates/htmx/components/README.md)
### Developer Resources
- [Developer Guidelines](./developer-guidelines.md) - Best practices for UX development
- [Migration Guide](./migration-guide.md) - Updating existing templates
## Quick Start
### Using Breadcrumbs
```python
# In your view
from apps.core.utils.breadcrumbs import build_breadcrumb
def park_detail(request, slug):
park = get_object_or_404(Park, slug=slug)
request.breadcrumbs = [
build_breadcrumb('Home', '/', icon='fas fa-home'),
build_breadcrumb('Parks', reverse('parks:list')),
build_breadcrumb(park.name, is_current=True),
]
return render(request, 'parks/detail.html', {'park': park})
```
### Using Toast Notifications
```javascript
// From JavaScript
Alpine.store('toast').success('Item saved successfully!');
Alpine.store('toast').error('Something went wrong');
// With action
Alpine.store('toast').success('Item deleted', 5000, {
action: { label: 'Undo', onClick: () => undoDelete() }
});
```
```python
# From Django view
from apps.core.htmx_utils import htmx_success
def save_item(request):
# ... save logic ...
return htmx_success('Item saved successfully!')
```
### Using Skeleton Screens
```django
{# Show skeleton while loading #}
<div id="parks-list"
hx-get="/parks/"
hx-trigger="load"
hx-swap="innerHTML">
{% include 'components/skeletons/card_grid_skeleton.html' with cards=6 %}
</div>
```
### Using Page Headers
```django
{% include 'components/layout/page_header.html' with
title='Parks'
subtitle='Browse theme parks worldwide'
icon='fas fa-map-marker-alt'
%}
```
### Using Modals
```django
<div x-data="{ showModal: false }">
<button @click="showModal = true" class="btn btn-primary">Open Modal</button>
{% include 'components/modals/modal_base.html' with
modal_id='my-modal'
title='Modal Title'
show_var='showModal'
%}
</div>
```
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ Django Templates │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Pages │ │ Partials │ │ Components │ │
│ │ │ │ │ │ │ │
│ │ - parks/ │ │ - _list.html│ │ - navigation/ │ │
│ │ - rides/ │ │ - _form.html│ │ - forms/ │ │
│ │ - auth/ │ │ - _card.html│ │ - modals/ │ │
│ └─────────────┘ └─────────────┘ │ - skeletons/ │ │
│ │ - ui/ │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Client-Side Layer │
│ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │ HTMX │ │ Alpine.js │ │
│ │ │ │ │ │
│ │ - Partial updates │ │ - UI state management │ │
│ │ - Form validation │ │ - Toast notifications │ │
│ │ - Infinite scroll │ │ - Search components │ │
│ │ - Modal loading │ │ - Theme switching │ │
│ └─────────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Key Concepts
### 1. Progressive Enhancement
All interactions work without JavaScript, with HTMX and Alpine.js enhancing the experience.
### 2. Consistent Feedback
Every user action provides immediate feedback:
- Loading states during HTMX requests
- Toast notifications for success/error
- Inline validation for forms
### 3. Accessibility First
All components include:
- Proper ARIA attributes
- Keyboard navigation
- Screen reader support
- Focus management
### 4. Responsive Design
Components adapt to screen size:
- Mobile-first approach
- Stacked layouts on small screens
- Collapsed navigation on mobile

View File

@@ -0,0 +1,453 @@
# ThrillWiki Component Library
This document catalogs all reusable UI components available in ThrillWiki.
## Layout Components
### Page Header
Standardized header for pages with optional actions and breadcrumbs.
```django
{% include 'components/layout/page_header.html' with
title='Parks'
subtitle='Browse theme parks worldwide'
icon='fas fa-map-marker-alt'
primary_action_url='/parks/create/'
primary_action_text='Add Park'
primary_action_icon='fas fa-plus'
%}
```
**Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `title` | string | Page title (required) |
| `subtitle` | string | Optional subtitle |
| `icon` | string | Icon class for title |
| `primary_action_url` | string | URL for primary action button |
| `primary_action_text` | string | Text for primary action |
| `primary_action_icon` | string | Icon for primary action |
| `secondary_action_url` | string | URL for secondary action |
| `secondary_action_text` | string | Text for secondary action |
### Action Bar
Container for action buttons with consistent layout.
```django
{% include 'components/ui/action_bar.html' with
align='between'
primary_action_text='Save'
secondary_action_text='Preview'
tertiary_action_text='Cancel'
tertiary_action_url='/back/'
%}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `align` | string | 'right' | 'left', 'right', 'center', 'between' |
| `mobile_stack` | boolean | true | Stack vertically on mobile |
| `show_border` | boolean | false | Show top border |
| `primary_action_*` | various | - | Primary button configuration |
| `secondary_action_*` | various | - | Secondary button configuration |
| `tertiary_action_*` | various | - | Tertiary button configuration |
## Navigation Components
### Breadcrumbs
Hierarchical navigation with Schema.org support.
```django
{% include 'components/navigation/breadcrumbs.html' %}
```
Set breadcrumbs in your view:
```python
from apps.core.utils import build_breadcrumb
def park_detail(request, slug):
park = get_object_or_404(Park, slug=slug)
request.breadcrumbs = [
build_breadcrumb('Home', '/', icon='fas fa-home'),
build_breadcrumb('Parks', reverse('parks:list')),
build_breadcrumb(park.name, is_current=True),
]
return render(request, 'parks/detail.html', {'park': park})
```
## Modal Components
### Modal Base
Base modal component with customizable content.
```django
{% include 'components/modals/modal_base.html' with
modal_id='edit-modal'
show_var='showEditModal'
title='Edit Park'
subtitle='Update park information'
icon='fas fa-edit'
size='lg'
animation='scale'
%}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `modal_id` | string | required | Unique identifier |
| `show_var` | string | required | Alpine.js show variable |
| `title` | string | - | Modal title |
| `subtitle` | string | - | Modal subtitle |
| `icon` | string | - | Title icon class |
| `size` | string | 'md' | 'sm', 'md', 'lg', 'xl', 'full' |
| `animation` | string | 'scale' | 'scale', 'slide-up', 'fade' |
| `close_on_backdrop` | boolean | true | Close when clicking backdrop |
| `close_on_escape` | boolean | true | Close on Escape key |
| `show_close_button` | boolean | true | Show X button |
| `prevent_scroll` | boolean | true | Prevent body scroll |
### Confirmation Modal
Pre-styled confirmation dialog.
```django
{% include 'components/modals/modal_confirm.html' with
modal_id='delete-confirm'
show_var='showDeleteModal'
title='Delete Park'
message='Are you sure? This cannot be undone.'
confirm_text='Delete'
confirm_variant='destructive'
confirm_hx_delete='/parks/123/'
%}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `modal_id` | string | required | Unique identifier |
| `show_var` | string | required | Alpine.js show variable |
| `title` | string | required | Modal title |
| `message` | string | required | Confirmation message |
| `icon` | string | auto | Icon class |
| `icon_variant` | string | 'warning' | 'destructive', 'warning', 'info', 'success' |
| `confirm_text` | string | 'Confirm' | Confirm button text |
| `confirm_variant` | string | 'primary' | 'destructive', 'primary', 'warning' |
| `cancel_text` | string | 'Cancel' | Cancel button text |
| `confirm_url` | string | - | URL for confirm (makes it a link) |
| `confirm_hx_post` | string | - | HTMX POST URL |
| `confirm_hx_delete` | string | - | HTMX DELETE URL |
| `on_confirm` | string | - | Alpine.js expression |
## Skeleton Components
### List Skeleton
```django
{% include 'components/skeletons/list_skeleton.html' with rows=5 %}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `rows` | number | 5 | Number of skeleton rows |
| `show_avatar` | boolean | false | Show avatar placeholder |
| `show_meta` | boolean | true | Show metadata line |
| `show_action` | boolean | true | Show action button placeholder |
### Card Grid Skeleton
```django
{% include 'components/skeletons/card_grid_skeleton.html' with cards=6 cols=3 %}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `cards` | number | 6 | Number of skeleton cards |
| `cols` | number | 3 | Grid columns (sm screens) |
| `show_image` | boolean | true | Show image placeholder |
| `show_badge` | boolean | false | Show badge placeholder |
### Detail Skeleton
```django
{% include 'components/skeletons/detail_skeleton.html' %}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `show_image` | boolean | true | Show hero image placeholder |
| `show_sidebar` | boolean | true | Show sidebar section |
| `content_lines` | number | 8 | Number of content lines |
### Table Skeleton
```django
{% include 'components/skeletons/table_skeleton.html' with rows=10 columns=5 %}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `rows` | number | 5 | Number of table rows |
| `columns` | number | 4 | Number of columns |
| `show_checkbox` | boolean | false | Show checkbox column |
| `show_actions` | boolean | true | Show actions column |
### Form Skeleton
```django
{% include 'components/skeletons/form_skeleton.html' with fields=4 %}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `fields` | number | 4 | Number of form fields |
| `show_textarea` | boolean | true | Include textarea field |
| `columns` | number | 1 | Form columns (1 or 2) |
## Form Components
### Form Field
Standardized form field with label, input, and validation.
```django
{% include 'forms/partials/form_field.html' with
field=form.name
help_text='Enter the full park name'
size='lg'
%}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `field` | Field | required | Django form field |
| `label` | string | field.label | Override label |
| `help_text` | string | field.help_text | Help text |
| `size` | string | 'md' | 'sm', 'md', 'lg' |
| `show_label` | boolean | true | Show label |
| `inline` | boolean | false | Inline layout |
### Form Actions
Form submission buttons.
```django
{% include 'forms/partials/form_actions.html' with
submit_text='Save Park'
cancel_url='/parks/'
%}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `submit_text` | string | 'Submit' | Submit button text |
| `submit_icon` | string | - | Submit button icon |
| `cancel_url` | string | - | Cancel button URL |
| `cancel_text` | string | 'Cancel' | Cancel button text |
| `show_cancel` | boolean | true | Show cancel button |
### Field Error
Display field validation errors.
```django
{% include 'forms/partials/field_error.html' with errors=field.errors %}
```
### Field Success
Display field validation success.
```django
{% include 'forms/partials/field_success.html' with message='Available' %}
```
## UI Components
### Button
```html
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-outline">Outline</button>
<button class="btn btn-ghost">Ghost</button>
<button class="btn btn-link">Link</button>
<button class="btn btn-destructive">Destructive</button>
```
**Sizes:**
```html
<button class="btn btn-primary btn-sm">Small</button>
<button class="btn btn-primary">Default</button>
<button class="btn btn-primary btn-lg">Large</button>
```
### Card
```django
{% include 'components/ui/card.html' with
title='Card Title'
subtitle='Card subtitle'
%}
<p>Card content goes here.</p>
{% endinclude %}
```
### Input
```django
{% include 'components/ui/input.html' with
name='email'
type='email'
placeholder='Enter email'
required=True
%}
```
### Status Badge
```django
{% include 'components/status_badge.html' with
status='published'
label='Published'
%}
```
**Status variants:**
- `published` / `active` / `approved` - Green
- `draft` / `pending` - Yellow
- `rejected` / `archived` - Red
- `review` - Blue
### Icon
```django
{% include 'components/ui/icon.html' with
icon='fas fa-star'
size='lg'
color='primary'
%}
```
### Dialog (Alert Dialog)
```django
{% include 'components/ui/dialog.html' with
dialog_id='alert-dialog'
show_var='showAlert'
title='Alert'
message='This is an alert message.'
confirm_text='OK'
%}
```
## Toast Notifications
### From JavaScript
```javascript
// Success toast
Alpine.store('toast').success('Saved successfully!');
// Error toast
Alpine.store('toast').error('Something went wrong');
// Warning toast
Alpine.store('toast').warning('Session expiring soon');
// Info toast
Alpine.store('toast').info('New features available');
// With custom duration (ms)
Alpine.store('toast').success('Quick message', 2000);
// With action button
Alpine.store('toast').success('Item deleted', 5000, {
action: { label: 'Undo', onClick: () => undoAction() }
});
```
### From Django Views
```python
from apps.core.htmx_utils import htmx_success, htmx_error
# Success toast
return htmx_success('Park saved successfully!')
# Error toast
return htmx_error('Validation failed', status=422)
# With action
return htmx_success('Item deleted', action={
'label': 'Undo',
'onClick': 'undoDelete()'
})
```
## Loading Components
### Loading Indicator
```django
{% include 'htmx/components/loading_indicator.html' with
id='my-loading'
text='Loading...'
size='md'
%}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `id` | string | - | Element ID for hx-indicator |
| `text` | string | 'Loading...' | Loading text |
| `size` | string | 'md' | 'sm', 'md', 'lg' |
| `type` | string | 'spinner' | 'spinner', 'dots', 'pulse' |
## Pagination
### Standard Pagination
```django
{% include 'components/pagination.html' with
page_obj=page_obj
show_first_last=True
%}
```
### HTMX Pagination
```django
{% include 'components/pagination.html' with
page_obj=page_obj
hx_target='#items-list'
hx_swap='innerHTML'
hx_push_url='true'
%}
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page_obj` | Page | required | Django paginator page |
| `show_first_last` | boolean | true | Show first/last buttons |
| `show_page_numbers` | boolean | true | Show page numbers |
| `max_pages` | number | 5 | Max visible page numbers |
| `hx_target` | string | - | HTMX target selector |
| `hx_swap` | string | - | HTMX swap method |
| `hx_push_url` | boolean | false | Push URL on navigation |

423
docs/ux/cta-guidelines.md Normal file
View File

@@ -0,0 +1,423 @@
# Call-to-Action (CTA) Guidelines
This document outlines the standardized patterns for buttons, links, and action elements in ThrillWiki.
## Button Hierarchy
### Primary Actions
Use for the main action on a page or in a section:
```html
<button class="btn btn-primary">Save Park</button>
<a href="/create/" class="btn btn-primary">Create New</a>
```
**When to use:**
- Form submissions
- Main page actions
- Completing a flow
### Secondary Actions
Use for important but non-primary actions:
```html
<button class="btn btn-outline">Preview</button>
<button class="btn btn-secondary">Save Draft</button>
```
**When to use:**
- Alternative actions
- Non-destructive secondary options
- Cancel with importance
### Tertiary Actions
Use for low-emphasis actions:
```html
<button class="btn btn-ghost">Cancel</button>
<a href="/back/" class="btn btn-link">Go Back</a>
```
**When to use:**
- Cancel buttons
- Navigation links
- Dismissive actions
### Destructive Actions
Use for actions that delete or remove data:
```html
<button class="btn btn-destructive">Delete Park</button>
```
**When to use:**
- Delete operations
- Remove actions
- Destructive confirmations
## Button Sizes
```html
<!-- Small - for compact UIs -->
<button class="btn btn-primary btn-sm">Small</button>
<!-- Default -->
<button class="btn btn-primary">Default</button>
<!-- Large - for prominent CTAs -->
<button class="btn btn-primary btn-lg">Large</button>
```
## Button States
### Loading State
```html
<button class="btn btn-primary" disabled>
<i class="fas fa-spinner fa-spin mr-2"></i>
Saving...
</button>
```
### Disabled State
```html
<button class="btn btn-primary" disabled>
Submit
</button>
```
### With Icon
```html
<button class="btn btn-primary">
<i class="fas fa-plus mr-2"></i>
Add Park
</button>
<button class="btn btn-outline">
Download
<i class="fas fa-download ml-2"></i>
</button>
```
## Action Bar Component
Use the action bar for consistent button placement:
```django
{% include 'components/ui/action_bar.html' with
primary_action_text='Save Changes'
primary_action_icon='fas fa-save'
secondary_action_text='Preview'
tertiary_action_text='Cancel'
tertiary_action_url='/parks/'
%}
```
### Alignment Options
```django
{# Right-aligned (default) #}
{% include 'components/ui/action_bar.html' with align='right' ... %}
{# Left-aligned #}
{% include 'components/ui/action_bar.html' with align='left' ... %}
{# Space-between (cancel left, actions right) #}
{% include 'components/ui/action_bar.html' with align='between' ... %}
{# Center-aligned #}
{% include 'components/ui/action_bar.html' with align='center' ... %}
```
## Placement Guidelines
### Page Headers
Primary CTA in page header, aligned right:
```django
{% include 'components/layout/page_header.html' with
title='Parks'
primary_action_url='/parks/create/'
primary_action_text='Add Park'
primary_action_icon='fas fa-plus'
%}
```
### Card Actions
Actions in card footer, right-aligned:
```html
<div class="card">
<div class="card-body">
<!-- Content -->
</div>
<div class="card-footer flex justify-end gap-2">
<button class="btn btn-ghost btn-sm">Cancel</button>
<button class="btn btn-primary btn-sm">Save</button>
</div>
</div>
```
### Form Actions
Form buttons at bottom, with cancel on left or right based on flow:
```html
<!-- Standard form (cancel left, submit right) -->
<div class="flex justify-between mt-6">
<a href="/cancel/" class="btn btn-ghost">Cancel</a>
<button type="submit" class="btn btn-primary">Save</button>
</div>
<!-- Simple form (all right-aligned) -->
<div class="flex justify-end gap-3 mt-6">
<button type="button" class="btn btn-ghost">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
```
### Modal Actions
Actions in modal footer, right-aligned:
```html
<div class="modal-footer">
<button @click="showModal = false" class="btn btn-ghost">
Cancel
</button>
<button class="btn btn-primary">
Confirm
</button>
</div>
```
### Inline Actions
For table rows or list items:
```html
<div class="flex items-center gap-2">
<button class="btn btn-ghost btn-sm">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-ghost btn-sm text-destructive">
<i class="fas fa-trash"></i>
</button>
</div>
```
## Link vs Button
### Use Links For:
- Navigation to other pages
- Opening in new tabs
- Bookmarkable destinations
```html
<a href="/parks/123/" class="btn btn-primary">View Park</a>
```
### Use Buttons For:
- Form submissions
- JavaScript actions
- State changes
- Toggles
```html
<button type="submit" class="btn btn-primary">Save</button>
<button @click="showModal = true" class="btn btn-outline">Open Modal</button>
```
## Icon Guidelines
### Icon Position
- **Left icon**: For actions (Add, Create, Save)
- **Right icon**: For navigation (Next, Go, External link)
```html
<!-- Action with left icon -->
<button class="btn btn-primary">
<i class="fas fa-plus mr-2"></i>
Add Park
</button>
<!-- Navigation with right icon -->
<a href="/next/" class="btn btn-outline">
Next Step
<i class="fas fa-arrow-right ml-2"></i>
</a>
```
### Common Icon Mappings
| Action | Icon |
|--------|------|
| Create/Add | `fa-plus` |
| Edit | `fa-edit` or `fa-pen` |
| Delete | `fa-trash` |
| Save | `fa-save` |
| Cancel | `fa-times` |
| Search | `fa-search` |
| Filter | `fa-filter` |
| Download | `fa-download` |
| Upload | `fa-upload` |
| Settings | `fa-cog` |
| Back | `fa-arrow-left` |
| Next | `fa-arrow-right` |
| External Link | `fa-external-link-alt` |
## Confirmation Patterns
### Inline Confirmation (Low Risk)
```html
<button hx-delete="/items/123/"
hx-confirm="Delete this item?">
Delete
</button>
```
### Modal Confirmation (High Risk)
```django
<button @click="showDeleteModal = true" class="btn btn-destructive">
Delete Park
</button>
{% include 'components/modals/modal_confirm.html' with
modal_id='delete-confirm'
show_var='showDeleteModal'
title='Delete Park'
message='This will permanently delete the park and all associated data. This action cannot be undone.'
confirm_text='Delete'
confirm_variant='destructive'
confirm_hx_delete='/parks/123/'
%}
```
## Responsive Behavior
### Mobile Stacking
Buttons stack vertically on mobile:
```html
<div class="flex flex-col sm:flex-row gap-3">
<button class="btn btn-ghost order-2 sm:order-1">Cancel</button>
<button class="btn btn-primary order-1 sm:order-2">Save</button>
</div>
```
### Icon-Only on Mobile
```html
<button class="btn btn-outline">
<i class="fas fa-filter"></i>
<span class="hidden sm:inline ml-2">Filter</span>
</button>
```
### Full-Width on Mobile
```html
<button class="btn btn-primary w-full sm:w-auto">
Save Changes
</button>
```
## Accessibility
### Button Labels
Always provide accessible labels:
```html
<!-- Icon-only buttons need aria-label -->
<button class="btn btn-ghost" aria-label="Edit park">
<i class="fas fa-edit" aria-hidden="true"></i>
</button>
<!-- Or use visually hidden text -->
<button class="btn btn-ghost">
<i class="fas fa-edit" aria-hidden="true"></i>
<span class="sr-only">Edit park</span>
</button>
```
### Focus States
All buttons must have visible focus states:
```css
.btn:focus {
outline: none;
ring: 2px;
ring-offset: 2px;
ring-color: var(--ring);
}
```
### Keyboard Navigation
Ensure buttons are keyboard accessible:
```html
<!-- Use native button elements -->
<button class="btn btn-primary">Click Me</button>
<!-- If using div/span, add role and tabindex -->
<div role="button" tabindex="0" class="btn btn-primary"
@keydown.enter="handleClick"
@keydown.space.prevent="handleClick">
Click Me
</div>
```
## HTMX Integration
### Loading Indicators
Show loading state during requests:
```html
<button hx-post="/save/"
hx-indicator="this"
class="btn btn-primary">
<span class="htmx-indicator hidden">
<i class="fas fa-spinner fa-spin mr-2"></i>
</span>
Save
</button>
```
### Disable During Request
```html
<button hx-post="/save/"
hx-disabled-elt="this"
class="btn btn-primary">
Save
</button>
```
### Success Feedback
```python
# In view
from apps.core.htmx_utils import htmx_success
def save_item(request):
# ... save logic ...
return htmx_success('Item saved successfully!')
```

View File

@@ -0,0 +1,510 @@
# Developer Guidelines
Best practices for UX development in ThrillWiki.
## General Principles
### 1. Progressive Enhancement
All features should work without JavaScript, with HTMX and Alpine.js enhancing the experience:
```html
<!-- Form works with or without JS -->
<form method="post" action="/parks/create/"
hx-post="/parks/create/"
hx-target="#form-container">
{% csrf_token %}
<!-- Form fields -->
<button type="submit">Save</button>
</form>
```
### 2. Consistent Feedback
Every user action should provide immediate feedback:
- **Loading states**: Show progress during requests
- **Success messages**: Confirm completed actions
- **Error messages**: Explain what went wrong
- **Validation**: Inline feedback on form fields
### 3. Accessibility First
All components must be accessible:
- Proper ARIA attributes
- Keyboard navigation
- Screen reader support
- Focus management
- Sufficient color contrast
### 4. Mobile-First Design
Design for mobile first, then enhance for larger screens:
```html
<!-- Mobile: stacked, Desktop: side-by-side -->
<div class="flex flex-col md:flex-row gap-4">
<div class="w-full md:w-1/2">Left</div>
<div class="w-full md:w-1/2">Right</div>
</div>
```
## File Organization
### Template Structure
```
templates/
├── base/
│ └── base.html # Base template
├── components/
│ ├── layout/ # Layout components
│ ├── modals/ # Modal components
│ ├── navigation/ # Navigation components
│ ├── skeletons/ # Skeleton screens
│ └── ui/ # UI primitives
├── forms/
│ ├── layouts/ # Form layouts
│ └── partials/ # Form field components
├── htmx/
│ └── components/ # HTMX-specific components
└── [app_name]/
├── list.html # Full page templates
├── list_partial.html # HTMX partials
└── _card.html # Reusable fragments
```
### Naming Conventions
| Type | Convention | Example |
|------|------------|---------|
| Full page | `name.html` | `list.html` |
| HTMX partial | `name_partial.html` | `list_partial.html` |
| Fragment | `_name.html` | `_card.html` |
| Component | `name.html` in components/ | `button.html` |
## Django Integration
### Context Processors
Use context processors for global data:
```python
# apps/core/context_processors.py
def breadcrumbs(request):
return {'breadcrumbs': getattr(request, 'breadcrumbs', [])}
def page_meta(request):
return {'page_meta': getattr(request, 'page_meta', {})}
```
### View Patterns
#### Standard View
```python
def park_list(request):
parks = Park.objects.all()
return render(request, 'parks/list.html', {'parks': parks})
```
#### HTMX-Aware View
```python
from apps.core.htmx_utils import is_htmx_request
def park_list(request):
parks = Park.objects.all()
template = 'parks/list_partial.html' if is_htmx_request(request) else 'parks/list.html'
return render(request, template, {'parks': parks})
```
#### Using Decorator
```python
from apps.core.htmx_utils import htmx_partial
@htmx_partial('parks/list.html')
def park_list(request):
parks = Park.objects.all()
return ({'parks': parks},)
```
### Breadcrumbs
Set breadcrumbs in views:
```python
from apps.core.utils import BreadcrumbBuilder
def park_detail(request, slug):
park = get_object_or_404(Park, slug=slug)
request.breadcrumbs = (
BreadcrumbBuilder()
.add_home()
.add('Parks', reverse('parks:list'))
.add_current(park.name)
.build()
)
return render(request, 'parks/detail.html', {'park': park})
```
### Page Meta
Set meta information in views:
```python
from apps.core.utils import build_meta_context
def park_detail(request, slug):
park = get_object_or_404(Park, slug=slug)
request.page_meta = build_meta_context(
title=park.name,
description=park.description,
instance=park,
request=request,
)
return render(request, 'parks/detail.html', {'park': park})
```
## HTMX Best Practices
### 1. Use Appropriate HTTP Methods
```html
<!-- GET for read operations -->
<button hx-get="/parks/">Load Parks</button>
<!-- POST for create -->
<form hx-post="/parks/create/">...</form>
<!-- PUT/PATCH for update -->
<form hx-put="/parks/123/edit/">...</form>
<!-- DELETE for remove -->
<button hx-delete="/parks/123/">Delete</button>
```
### 2. Target Specific Elements
```html
<!-- Good: Specific target -->
<button hx-get="/parks/" hx-target="#parks-list">Load</button>
<!-- Avoid: Replacing large sections unnecessarily -->
<button hx-get="/parks/" hx-target="#main-content">Load</button>
```
### 3. Use Loading Indicators
```html
<button hx-get="/parks/"
hx-target="#parks-list"
hx-indicator="#loading">
Load Parks
</button>
<div id="loading" class="htmx-indicator">
{% include 'htmx/components/loading_indicator.html' %}
</div>
```
### 4. Handle Errors Gracefully
```python
def create_park(request):
form = ParkForm(request.POST)
if not form.is_valid():
# Return form with errors, status 422
return render(
request,
'parks/_form.html',
{'form': form},
status=422
)
park = form.save()
return htmx_success(f'{park.name} created!')
```
### 5. Use Response Headers
```python
from apps.core.htmx_utils import (
htmx_success,
htmx_error,
htmx_redirect,
htmx_modal_close,
)
# Success with toast
return htmx_success('Park saved!')
# Error with toast
return htmx_error('Validation failed', status=422)
# Redirect
return htmx_redirect('/parks/')
# Close modal and refresh
return htmx_modal_close(
message='Saved!',
refresh_target='#parks-list'
)
```
## Alpine.js Best Practices
### 1. Keep Components Small
```html
<!-- Good: Focused component -->
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open">Content</div>
</div>
<!-- Avoid: Large monolithic components -->
<div x-data="{
open: false,
items: [],
selectedItem: null,
loading: false,
// ... many more properties
}">
```
### 2. Use Stores for Global State
```javascript
// stores/index.js
Alpine.store('toast', {
messages: [],
success(message, duration = 5000) { /* ... */ },
error(message, duration = 0) { /* ... */ },
});
// Usage anywhere
Alpine.store('toast').success('Saved!');
```
### 3. Initialize from Server Data
```html
<div x-data="{ items: {{ items_json|safe }} }">
<template x-for="item in items">
<div x-text="item.name"></div>
</template>
</div>
```
### 4. Use Magic Properties
```html
<div x-data="{ message: '' }">
<!-- $refs for DOM access -->
<input x-ref="input" x-model="message">
<button @click="$refs.input.focus()">Focus</button>
<!-- $watch for reactive updates -->
<div x-init="$watch('message', value => console.log(value))">
<!-- $nextTick for DOM updates -->
<button @click="items.push('new'); $nextTick(() => scrollToBottom())">
Add
</button>
</div>
```
## CSS/Tailwind Best Practices
### 1. Use Design Tokens
```css
/* Use CSS variables */
.custom-element {
color: hsl(var(--foreground));
background: hsl(var(--background));
border-color: hsl(var(--border));
}
```
### 2. Use Component Classes
```html
<!-- Good: Semantic classes -->
<button class="btn btn-primary btn-lg">Save</button>
<!-- Avoid: Long utility chains for common patterns -->
<button class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium">
Save
</button>
```
### 3. Responsive Design
```html
<!-- Mobile-first -->
<div class="p-4 md:p-6 lg:p-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Items -->
</div>
</div>
```
### 4. Dark Mode Support
```html
<!-- Automatic dark mode via CSS variables -->
<div class="bg-background text-foreground">
Content adapts to theme
</div>
<!-- Explicit dark mode variants -->
<div class="bg-white dark:bg-gray-800">
Explicit dark override
</div>
```
## Testing Guidelines
### 1. Test User Flows
```python
def test_create_park_flow(self):
# Navigate to create page
response = self.client.get('/parks/create/')
self.assertEqual(response.status_code, 200)
# Submit form
response = self.client.post('/parks/create/', {
'name': 'Test Park',
'location': 'Test Location',
})
# Verify redirect or success
self.assertEqual(response.status_code, 302)
self.assertTrue(Park.objects.filter(name='Test Park').exists())
```
### 2. Test HTMX Responses
```python
def test_htmx_park_list(self):
response = self.client.get('/parks/', HTTP_HX_REQUEST='true')
# Should return partial template
self.assertTemplateUsed(response, 'parks/list_partial.html')
# Should not include base template chrome
self.assertNotContains(response, '<html')
```
### 3. Test Error States
```python
def test_form_validation_errors(self):
response = self.client.post('/parks/create/', {
'name': '', # Required field
})
self.assertEqual(response.status_code, 422)
self.assertContains(response, 'This field is required')
```
## Performance Guidelines
### 1. Minimize DOM Updates
```html
<!-- Good: Update specific element -->
<button hx-get="/count/" hx-target="#count-badge">Refresh</button>
<!-- Avoid: Replacing entire page section -->
<button hx-get="/dashboard/" hx-target="#main">Refresh</button>
```
### 2. Use Skeleton Screens
```html
<div id="content"
hx-get="/parks/"
hx-trigger="load"
hx-swap="innerHTML">
{% include 'components/skeletons/card_grid_skeleton.html' %}
</div>
```
### 3. Lazy Load Content
```html
<!-- Load when visible -->
<div hx-get="/stats/"
hx-trigger="revealed"
hx-swap="innerHTML">
<div class="skeleton h-20"></div>
</div>
```
### 4. Debounce Input
```html
<!-- Debounce search input -->
<input hx-get="/search/"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
```
## Security Guidelines
### 1. CSRF Protection
```html
<!-- Include token in body -->
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<!-- Or in forms -->
<form hx-post="/create/">
{% csrf_token %}
</form>
```
### 2. Validate on Server
Always validate data server-side, even with client-side validation:
```python
def create_park(request):
form = ParkForm(request.POST)
if not form.is_valid():
return htmx_error('Invalid data', status=422)
# Process valid data
```
### 3. Escape User Content
```django
<!-- Auto-escaped by Django -->
{{ user_input }}
<!-- Explicitly mark as safe only for trusted content -->
{{ trusted_html|safe }}
```
### 4. Use Appropriate Permissions
```python
from django.contrib.auth.decorators import login_required, permission_required
@login_required
@permission_required('parks.add_park')
def create_park(request):
# Only authenticated users with permission
pass
```

528
docs/ux/htmx-conventions.md Normal file
View File

@@ -0,0 +1,528 @@
# HTMX Conventions
This document outlines the standardized HTMX patterns and conventions used in ThrillWiki.
## Request Attributes
### hx-get / hx-post / hx-put / hx-delete
Use the appropriate HTTP method for the action:
```html
<!-- Read operations -->
<button hx-get="/parks/">Load Parks</button>
<!-- Create operations -->
<form hx-post="/parks/create/">...</form>
<!-- Update operations -->
<form hx-put="/parks/{{ park.id }}/edit/">...</form>
<!-- Delete operations -->
<button hx-delete="/parks/{{ park.id }}/">Delete</button>
```
### hx-target
Specify where the response should be inserted:
```html
<!-- Target by ID -->
<button hx-get="/content/" hx-target="#content-area">Load</button>
<!-- Target closest ancestor -->
<button hx-delete="/item/" hx-target="closest .item-row">Delete</button>
<!-- Target this element -->
<div hx-get="/refresh/" hx-target="this">Content</div>
```
### hx-swap
Control how content is inserted:
```html
<!-- Replace inner HTML (default) -->
<div hx-get="/content/" hx-swap="innerHTML">...</div>
<!-- Replace entire element -->
<div hx-get="/content/" hx-swap="outerHTML">...</div>
<!-- Insert before/after -->
<div hx-get="/content/" hx-swap="beforeend">...</div>
<div hx-get="/content/" hx-swap="afterbegin">...</div>
<!-- Delete element -->
<button hx-delete="/item/" hx-swap="delete">Remove</button>
<!-- No swap (for side effects only) -->
<button hx-post="/action/" hx-swap="none">Action</button>
```
### hx-trigger
Define when requests are made:
```html
<!-- On click (default for buttons) -->
<button hx-get="/content/" hx-trigger="click">Load</button>
<!-- On page load -->
<div hx-get="/content/" hx-trigger="load">...</div>
<!-- When element becomes visible -->
<div hx-get="/more/" hx-trigger="revealed">Load More</div>
<!-- On form input with debounce -->
<input hx-get="/search/" hx-trigger="keyup changed delay:300ms">
<!-- On blur for validation -->
<input hx-post="/validate/" hx-trigger="blur changed">
<!-- Custom event -->
<div hx-get="/refresh/" hx-trigger="refresh from:body">...</div>
```
## Response Headers
### Server-Side Response Helpers
Use the standardized HTMX utility functions in views:
```python
from apps.core.htmx_utils import (
htmx_success,
htmx_error,
htmx_redirect,
htmx_modal_close,
htmx_refresh,
)
def create_park(request):
# On success
return htmx_success('Park created successfully!')
# On error
return htmx_error('Validation failed', status=422)
# Redirect
return htmx_redirect('/parks/')
# Close modal with refresh
return htmx_modal_close(
message='Park saved!',
refresh_target='#parks-list'
)
```
### HX-Trigger
Trigger client-side events from the server:
```python
# Single event
response['HX-Trigger'] = 'parkCreated'
# Event with data
response['HX-Trigger'] = json.dumps({
'showToast': {
'type': 'success',
'message': 'Park created!'
}
})
# Multiple events
response['HX-Trigger'] = json.dumps({
'closeModal': True,
'refreshList': {'target': '#parks-list'},
'showToast': {'type': 'success', 'message': 'Done!'}
})
```
### HX-Redirect
Perform client-side navigation:
```python
response['HX-Redirect'] = '/parks/'
```
### HX-Refresh
Trigger a full page refresh:
```python
response['HX-Refresh'] = 'true'
```
### HX-Retarget / HX-Reswap
Override target and swap method:
```python
response['HX-Retarget'] = '#different-target'
response['HX-Reswap'] = 'outerHTML'
```
## Partial Templates
### Convention
For each full template, create a corresponding partial:
```
templates/
parks/
list.html # Full page
list_partial.html # Just the content (for HTMX)
detail.html
detail_partial.html
```
### Usage with Decorator
```python
from apps.core.htmx_utils import htmx_partial
@htmx_partial('parks/list.html')
def park_list(request):
parks = Park.objects.all()
return ({'parks': parks},) # Returns context tuple
```
### Manual Detection
```python
from apps.core.htmx_utils import is_htmx_request
def park_list(request):
parks = Park.objects.all()
if is_htmx_request(request):
return render(request, 'parks/list_partial.html', {'parks': parks})
return render(request, 'parks/list.html', {'parks': parks})
```
## Loading States
### Indicator Attribute
```html
<button hx-get="/content/"
hx-indicator="#loading-spinner">
Load Content
</button>
<div id="loading-spinner" class="htmx-indicator">
<i class="fas fa-spinner fa-spin"></i>
</div>
```
### CSS Classes
HTMX automatically adds classes during requests:
```css
/* Added to element making request */
.htmx-request {
opacity: 0.5;
pointer-events: none;
}
/* Added to indicator elements */
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline-block;
}
```
### Skeleton Screens
Use skeleton components for better UX:
```html
<div id="parks-list"
hx-get="/parks/"
hx-trigger="load"
hx-swap="innerHTML">
{% include 'components/skeletons/card_grid_skeleton.html' with cards=6 %}
</div>
```
## Form Handling
### Standard Form Pattern
```html
<form id="park-form"
hx-post="{% url 'parks:create' %}"
hx-target="#park-form"
hx-swap="outerHTML"
hx-indicator="#form-loading">
{% csrf_token %}
{% include 'forms/partials/form_field.html' with field=form.name %}
{% include 'forms/partials/form_field.html' with field=form.location %}
<div id="form-loading" class="htmx-indicator">
Saving...
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>
```
### Inline Validation
```html
<input type="text"
name="name"
hx-post="{% url 'parks:validate_name' %}"
hx-trigger="blur changed delay:500ms"
hx-target="#name-feedback"
hx-swap="innerHTML">
<div id="name-feedback"></div>
```
### Server Response
```python
from apps.core.htmx_utils import htmx_validation_response
def validate_name(request):
name = request.POST.get('name', '')
if not name:
return htmx_validation_response('name', errors=['Name is required'])
if Park.objects.filter(name=name).exists():
return htmx_validation_response('name', errors=['Name already exists'])
return htmx_validation_response('name', success_message='Name is available')
```
## Pagination
### Load More Pattern
```html
<div id="parks-list">
{% for park in parks %}
{% include 'parks/_card.html' %}
{% endfor %}
</div>
{% if page_obj.has_next %}
<button hx-get="?page={{ page_obj.next_page_number }}"
hx-target="#parks-list"
hx-swap="beforeend"
hx-select="#parks-list > *"
class="btn btn-outline">
Load More
</button>
{% endif %}
```
### Infinite Scroll
```html
{% if page_obj.has_next %}
<div hx-get="?page={{ page_obj.next_page_number }}"
hx-trigger="revealed"
hx-target="this"
hx-swap="outerHTML"
hx-select="oob-swap">
<div class="htmx-indicator text-center py-4">
<i class="fas fa-spinner fa-spin"></i> Loading...
</div>
</div>
{% endif %}
```
### Traditional Pagination
```html
{% include 'components/pagination.html' with
page_obj=page_obj
hx_target='#parks-list'
hx_swap='innerHTML'
hx_push_url='true'
%}
```
## Modal Integration
### Opening Modal with Content
```html
<button hx-get="{% url 'parks:edit' park.id %}"
hx-target="#modal-content"
hx-swap="innerHTML"
@click="showEditModal = true">
Edit Park
</button>
<div x-show="showEditModal" @close-modal.window="showEditModal = false">
<div id="modal-content">
<!-- Content loaded here -->
</div>
</div>
```
### Closing Modal from Server
```python
def save_park(request, pk):
park = get_object_or_404(Park, pk=pk)
form = ParkForm(request.POST, instance=park)
if form.is_valid():
form.save()
return htmx_modal_close(
message='Park updated successfully!',
refresh_target='#park-detail'
)
return render(request, 'parks/_edit_form.html', {'form': form})
```
## Out-of-Band Updates
### Multiple Element Updates
```html
<!-- Main response -->
<div id="main-content">
Updated content here
</div>
<!-- Out-of-band updates -->
<div id="notification-count" hx-swap-oob="true">
<span class="badge">5</span>
</div>
<div id="sidebar-status" hx-swap-oob="true">
<span class="status">Online</span>
</div>
```
### Server-Side OOB
```python
def update_item(request, pk):
item = get_object_or_404(Item, pk=pk)
# ... update logic ...
# Render multiple fragments
main_html = render_to_string('items/_detail.html', {'item': item})
count_html = render_to_string('items/_count.html', {'count': Item.objects.count()})
return HttpResponse(
main_html +
f'<div id="item-count" hx-swap-oob="true">{count_html}</div>'
)
```
## Error Handling
### HTTP Status Codes
HTMX handles different status codes:
- **2xx**: Normal swap
- **204**: No swap (success, no content)
- **4xx**: Swap (show validation errors)
- **5xx**: Trigger error event
### Client-Side Error Handling
```javascript
document.body.addEventListener('htmx:responseError', (event) => {
const status = event.detail.xhr.status;
if (status === 401) {
Alpine.store('toast').error('Please log in to continue');
window.location.href = '/login/';
} else if (status >= 500) {
Alpine.store('toast').error('Server error. Please try again.');
}
});
```
### Validation Errors
Return form HTML with errors on 422:
```python
def create_park(request):
form = ParkForm(request.POST)
if not form.is_valid():
return render(
request,
'parks/_form.html',
{'form': form},
status=422
)
park = form.save()
return htmx_success(f'{park.name} created!')
```
## Best Practices
### 1. Use Semantic URLs
```html
<!-- Good -->
<button hx-delete="/parks/123/">Delete</button>
<!-- Avoid -->
<button hx-post="/parks/123/delete/">Delete</button>
```
### 2. Include CSRF Token
```html
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
```
### 3. Use hx-confirm for Destructive Actions
```html
<button hx-delete="/parks/123/"
hx-confirm="Are you sure you want to delete this park?">
Delete
</button>
```
### 4. Preserve Scroll Position
```html
<div hx-get="/parks/"
hx-swap="innerHTML show:top">
```
### 5. Push URL for Navigation
```html
<a hx-get="/parks/123/"
hx-target="#main"
hx-push-url="true">
View Park
</a>
```
### 6. Handle Browser Back Button
```javascript
window.addEventListener('popstate', (event) => {
if (event.state && event.state.htmx) {
htmx.ajax('GET', window.location.href, '#main');
}
});
```

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');
});
```

437
docs/ux/migration-guide.md Normal file
View File

@@ -0,0 +1,437 @@
# Migration Guide
This guide helps migrate existing templates to use the new standardized UX patterns.
## Overview
The ThrillWiki UX standardization introduces:
1. **Breadcrumb system** with Schema.org support
2. **Skeleton screens** for loading states
3. **Standardized form validation** with HTMX
4. **Toast notifications** with action support
5. **Page headers** with consistent layout
6. **Action bars** for button placement
7. **Enhanced modals** with focus trapping
8. **HTMX response utilities** for views
## Step-by-Step Migration
### Step 1: Update Base Template Usage
Ensure your templates extend the base template correctly:
```django
{% extends 'base/base.html' %}
{% block title %}Your Page Title{% endblock %}
{% block content %}
<!-- Your content -->
{% endblock %}
```
### Step 2: Add Breadcrumbs
#### In Your View
```python
from apps.core.utils import BreadcrumbBuilder
def your_view(request):
request.breadcrumbs = (
BreadcrumbBuilder()
.add_home()
.add('Section', '/section/')
.add_current('Current Page')
.build()
)
return render(request, 'your_template.html')
```
#### In Your Template
The breadcrumbs are automatically rendered if you're using the base template.
If you need to place them elsewhere:
```django
{% include 'components/navigation/breadcrumbs.html' %}
```
### Step 3: Replace Page Headers
#### Before
```html
<div class="page-header">
<h1>Parks</h1>
<a href="/parks/create/" class="btn">Add Park</a>
</div>
```
#### After
```django
{% include 'components/layout/page_header.html' with
title='Parks'
subtitle='Browse theme parks worldwide'
primary_action_url='/parks/create/'
primary_action_text='Add Park'
primary_action_icon='fas fa-plus'
%}
```
### Step 4: Update Form Fields
#### Before
```html
<div class="form-group">
<label for="name">Name</label>
{{ form.name }}
{% if form.name.errors %}
<span class="error">{{ form.name.errors.0 }}</span>
{% endif %}
</div>
```
#### After
```django
{% include 'forms/partials/form_field.html' with field=form.name %}
```
For multiple fields with consistent styling:
```django
{% for field in form %}
{% include 'forms/partials/form_field.html' with field=field %}
{% endfor %}
```
### Step 5: Update Form Actions
#### Before
```html
<div class="form-buttons">
<a href="/cancel/">Cancel</a>
<button type="submit">Save</button>
</div>
```
#### After
```django
{% include 'forms/partials/form_actions.html' with
submit_text='Save Park'
cancel_url='/parks/'
%}
```
Or use the action bar for more options:
```django
{% include 'components/ui/action_bar.html' with
align='between'
primary_action_text='Save'
tertiary_action_text='Cancel'
tertiary_action_url='/parks/'
%}
```
### Step 6: Add Loading States
#### For Page Sections
```django
<div id="parks-list"
hx-get="{% url 'parks:list' %}"
hx-trigger="load"
hx-swap="innerHTML">
{% include 'components/skeletons/card_grid_skeleton.html' with cards=6 %}
</div>
```
#### For Forms
```django
<form hx-post="{% url 'parks:create' %}"
hx-target="#form-container"
hx-indicator="#form-loading">
{% csrf_token %}
<!-- Form fields -->
<div id="form-loading" class="htmx-indicator">
{% include 'htmx/components/loading_indicator.html' with text='Saving...' %}
</div>
<button type="submit">Save</button>
</form>
```
### Step 7: Update Toast Messages
#### Before (Django Messages)
```python
from django.contrib import messages
def your_view(request):
messages.success(request, 'Park saved!')
return redirect('parks:list')
```
#### After (HTMX Response)
```python
from apps.core.htmx_utils import htmx_success
def your_view(request):
# For HTMX requests
if request.headers.get('HX-Request'):
return htmx_success('Park saved!')
# For regular requests, still use messages
messages.success(request, 'Park saved!')
return redirect('parks:list')
```
### Step 8: Update Modals
#### Before
```html
<div class="modal" id="edit-modal">
<div class="modal-header">
<h2>Edit Park</h2>
<button onclick="closeModal()">×</button>
</div>
<div class="modal-body">
<!-- Content -->
</div>
</div>
```
#### After
```django
<div x-data="{ showEditModal: false }">
<button @click="showEditModal = true">Edit</button>
{% include 'components/modals/modal_base.html' with
modal_id='edit-modal'
show_var='showEditModal'
title='Edit Park'
size='lg'
%}
</div>
```
For confirmation dialogs:
```django
{% include 'components/modals/modal_confirm.html' with
modal_id='delete-confirm'
show_var='showDeleteModal'
title='Delete Park'
message='Are you sure? This cannot be undone.'
confirm_text='Delete'
confirm_variant='destructive'
confirm_hx_delete='/parks/123/'
%}
```
### Step 9: Update HTMX Views
#### Before
```python
from django.http import HttpResponse
import json
def create_park(request):
park = Park.objects.create(**form.cleaned_data)
response = HttpResponse('')
response['HX-Trigger'] = json.dumps({
'showMessage': {'message': 'Park created!'}
})
return response
```
#### After
```python
from apps.core.htmx_utils import htmx_success, htmx_error, htmx_modal_close
def create_park(request):
form = ParkForm(request.POST)
if not form.is_valid():
return htmx_error('Validation failed', status=422)
park = form.save()
# Simple success
return htmx_success(f'{park.name} created!')
# Or close modal and refresh list
return htmx_modal_close(
message=f'{park.name} created!',
refresh_target='#parks-list'
)
```
### Step 10: Add Page Meta
#### In Your View
```python
from apps.core.utils import build_meta_context
def park_detail(request, slug):
park = get_object_or_404(Park, slug=slug)
request.page_meta = build_meta_context(
title=park.name,
description=park.description,
instance=park,
request=request,
)
return render(request, 'parks/detail.html', {'park': park})
```
The meta tags are automatically rendered in the base template.
## Component Replacements
| Old Pattern | New Component |
|-------------|---------------|
| Custom page header | `components/layout/page_header.html` |
| Form field div | `forms/partials/form_field.html` |
| Form buttons | `forms/partials/form_actions.html` or `components/ui/action_bar.html` |
| Loading spinner | `htmx/components/loading_indicator.html` |
| Placeholder content | `components/skeletons/*.html` |
| Custom modal | `components/modals/modal_base.html` |
| Confirm dialog | `components/modals/modal_confirm.html` |
| Custom breadcrumbs | `components/navigation/breadcrumbs.html` |
| Status labels | `components/status_badge.html` |
## View Helper Replacements
| Old Pattern | New Helper |
|-------------|------------|
| `HttpResponse + HX-Trigger` | `htmx_success()` / `htmx_error()` |
| `HttpResponse + HX-Redirect` | `htmx_redirect()` |
| Custom modal close | `htmx_modal_close()` |
| Inline validation | `htmx_validation_response()` |
| Check HX-Request header | `is_htmx_request()` |
## Common Issues
### Issue: Breadcrumbs Not Showing
1. Ensure context processor is added to settings:
```python
TEMPLATES = [{
'OPTIONS': {
'context_processors': [
# ...
'apps.core.context_processors.breadcrumbs',
],
},
}]
```
2. Ensure you set `request.breadcrumbs` in your view.
### Issue: Toast Not Appearing
1. Ensure toast container is in base template:
```django
{% include 'components/ui/toast-container.html' %}
```
2. Ensure Alpine.js is loaded and toast store is initialized.
3. For HTMX responses, use the helper functions:
```python
return htmx_success('Message here')
```
### Issue: Modal Not Closing
1. Ensure you're using the correct `show_var`:
```django
<div x-data="{ showModal: false }">
{% include 'components/modals/modal_base.html' with show_var='showModal' %}
</div>
```
2. For HTMX responses, use:
```python
return htmx_modal_close(message='Done!')
```
### Issue: Form Validation Not Working
1. Ensure HTMX attributes are correct:
```html
<form hx-post="/create/"
hx-target="#form-container"
hx-swap="outerHTML">
```
2. Return proper status code on errors:
```python
if not form.is_valid():
return render(request, 'form.html', {'form': form}, status=422)
```
### Issue: Skeleton Not Matching Content
Adjust skeleton parameters to match your content:
```django
{# For a 3-column grid with 9 cards #}
{% include 'components/skeletons/card_grid_skeleton.html' with cards=9 cols=3 %}
{# For a table with checkboxes #}
{% include 'components/skeletons/table_skeleton.html' with rows=10 columns=5 show_checkbox=True %}
```
## Testing Migrations
After migrating a template:
1. **Visual Check**: Ensure layout matches design
2. **Functionality**: Test all interactive elements
3. **Accessibility**: Test keyboard navigation and screen readers
4. **Responsive**: Check on mobile, tablet, and desktop
5. **Loading States**: Verify skeletons and indicators work
6. **Error States**: Test form validation and error messages
7. **Success States**: Verify toast notifications appear
## Rollback
If issues arise, components are designed to be backwards compatible.
You can temporarily revert to old patterns while debugging:
```django
{% comment %}
{% include 'components/layout/page_header.html' with title='Parks' %}
{% endcomment %}
<div class="page-header">
<h1>Parks</h1>
</div>
```