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,225 @@
# Form Templates
This directory contains form-related templates for ThrillWiki.
## Directory Structure
```
forms/
├── partials/ # Individual form components
│ ├── form_field.html # Complete form field
│ ├── field_error.html # Error messages
│ ├── field_success.html # Success indicator
│ └── form_actions.html # Submit/cancel buttons
├── layouts/ # Form layout templates
│ ├── stacked.html # Vertical layout
│ ├── inline.html # Horizontal layout
│ └── grid.html # Multi-column grid
└── README.md # This file
```
## Form Layouts
### Stacked Layout (Default)
Vertical layout with full-width fields:
```django
{% include 'forms/layouts/stacked.html' with form=form %}
{# With options #}
{% include 'forms/layouts/stacked.html' with
form=form
submit_text='Save Changes'
show_cancel=True
cancel_url='/parks/'
%}
```
### Inline Layout
Horizontal layout with labels beside inputs:
```django
{% include 'forms/layouts/inline.html' with form=form %}
{# Custom label width #}
{% include 'forms/layouts/inline.html' with form=form label_width='w-1/4' %}
```
### Grid Layout
Multi-column responsive grid:
```django
{# 2-column grid #}
{% include 'forms/layouts/grid.html' with form=form cols=2 %}
{# 3-column grid #}
{% include 'forms/layouts/grid.html' with form=form cols=3 %}
{# Full-width description field #}
{% include 'forms/layouts/grid.html' with form=form cols=2 full_width='description' %}
```
## Layout Parameters
| Parameter | Description | Default |
|-----------|-------------|---------|
| `form` | Django form object | Required |
| `exclude` | Comma-separated fields to exclude | None |
| `fields` | Comma-separated fields to include | All |
| `show_help` | Show help text | True |
| `show_required` | Show required indicator | True |
| `submit_text` | Submit button text | 'Submit' |
| `submit_class` | Submit button CSS class | 'btn-primary' |
| `show_cancel` | Show cancel button | False |
| `cancel_url` | URL for cancel link | None |
| `cancel_text` | Cancel button text | 'Cancel' |
| `show_actions` | Show action buttons | True |
## Individual Components
### Form Field
Complete field with label, input, help, and errors:
```django
{% include 'forms/partials/form_field.html' with field=form.email %}
{# Custom label #}
{% include 'forms/partials/form_field.html' with field=form.email label='Your Email' %}
{# Without label #}
{% include 'forms/partials/form_field.html' with field=form.search show_label=False %}
{# Inline layout #}
{% include 'forms/partials/form_field.html' with field=form.email layout='inline' %}
{# HTMX validation #}
{% include 'forms/partials/form_field.html' with
field=form.username
hx_validate=True
hx_validate_url='/api/validate/username/'
%}
```
### Field Error
Error message display:
```django
{% include 'forms/partials/field_error.html' with errors=field.errors %}
{# Without icon #}
{% include 'forms/partials/field_error.html' with errors=field.errors show_icon=False %}
{# Different size #}
{% include 'forms/partials/field_error.html' with errors=field.errors size='md' %}
```
### Field Success
Success indicator for validation:
```django
{% include 'forms/partials/field_success.html' with message='Username available' %}
{# Just checkmark #}
{% include 'forms/partials/field_success.html' %}
```
### Form Actions
Submit and cancel buttons:
```django
{% include 'forms/partials/form_actions.html' %}
{# With cancel #}
{% include 'forms/partials/form_actions.html' with show_cancel=True cancel_url='/list/' %}
{# With loading state #}
{% include 'forms/partials/form_actions.html' with show_loading=True %}
{# Left-aligned #}
{% include 'forms/partials/form_actions.html' with align='left' %}
{# Custom icon #}
{% include 'forms/partials/form_actions.html' with submit_icon='fas fa-check' %}
```
## HTMX Integration
### Inline Validation
```django
<form hx-post="/submit/" hx-target="#form-results">
{% for field in form %}
{% include 'forms/partials/form_field.html' with
field=field
hx_validate=True
hx_validate_url='/validate/'
%}
{% endfor %}
{% include 'forms/partials/form_actions.html' with show_loading=True %}
</form>
```
### Validation Endpoint Response
```django
{# Success #}
{% include 'forms/partials/field_success.html' with message='Valid!' %}
{# Error #}
{% include 'forms/partials/field_error.html' with errors=errors %}
```
## Custom Form Rendering
For complete control, use the components directly:
```django
<form method="post" action="{% url 'submit' %}">
{% csrf_token %}
{# Non-field errors #}
{% if form.non_field_errors %}
<div class="mb-4 p-4 bg-red-50 rounded-lg" role="alert">
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
{# Custom field layout #}
<div class="grid grid-cols-2 gap-4">
{% include 'forms/partials/form_field.html' with field=form.first_name %}
{% include 'forms/partials/form_field.html' with field=form.last_name %}
</div>
{% include 'forms/partials/form_field.html' with field=form.email %}
{% include 'forms/partials/form_actions.html' with submit_text='Create Account' %}
</form>
```
## CSS Classes
The form components use these CSS classes (defined in `components.css`):
- `.btn-primary` - Primary action button
- `.btn-secondary` - Secondary action button
- `.form-field` - Field wrapper
- `.field-error` - Error message styling
## Accessibility
All form components include:
- Labels properly associated with inputs (`for`/`id`)
- Required field indicators with screen reader text
- Error messages with `role="alert"`
- Help text linked via `aria-describedby`
- Invalid state with `aria-invalid`

View File

@@ -0,0 +1,106 @@
{% comment %}
Grid Form Layout
================
Renders form fields in a responsive grid layout.
Purpose:
Provides a multi-column grid layout for forms with many fields.
Responsive - adjusts columns based on screen size.
Usage Examples:
2-column grid:
{% include 'forms/layouts/grid.html' with form=form cols=2 %}
3-column grid:
{% include 'forms/layouts/grid.html' with form=form cols=3 %}
With full-width fields:
{% include 'forms/layouts/grid.html' with form=form cols=2 full_width='description,notes' %}
Parameters:
Required:
- form: Django form object
Optional:
- cols: Number of columns (2 or 3, default: 2)
- exclude: Comma-separated field names to exclude
- fields: Comma-separated field names to include
- full_width: Comma-separated field names that span full width
- show_help: Show help text (default: True)
- show_required: Show required indicator (default: True)
- gap: Grid gap class (default: 'gap-4')
- submit_text: Submit button text (default: 'Submit')
- submit_class: Submit button CSS class
- form_class: Additional CSS classes for form wrapper
Dependencies:
- forms/partials/form_field.html
- Tailwind CSS
Accessibility:
- Responsive grid maintains logical order
- Labels properly associated with inputs
{% endcomment %}
{% load common_filters %}
{% with show_help=show_help|default:True show_required=show_required|default:True cols=cols|default:2 gap=gap|default:'gap-4' submit_text=submit_text|default:'Submit' %}
<div class="form-layout-grid {{ form_class }}">
{# Non-field errors #}
{% if form.non_field_errors %}
<div class="mb-4 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
role="alert">
<ul class="text-sm text-red-700 dark:text-red-300 space-y-1">
{% for error in form.non_field_errors %}
<li class="flex items-start gap-2">
<i class="fas fa-exclamation-circle mt-0.5" aria-hidden="true"></i>
{{ error }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{# Grid container #}
<div class="grid {% if cols == 3 %}grid-cols-1 md:grid-cols-2 lg:grid-cols-3{% else %}grid-cols-1 md:grid-cols-2{% endif %} {{ gap }}">
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% elif fields %}
{% if field.name in fields %}
<div class="{% if full_width and field.name in full_width %}{% if cols == 3 %}md:col-span-2 lg:col-span-3{% else %}md:col-span-2{% endif %}{% endif %}">
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
</div>
{% endif %}
{% elif exclude %}
{% if field.name not in exclude %}
<div class="{% if full_width and field.name in full_width %}{% if cols == 3 %}md:col-span-2 lg:col-span-3{% else %}md:col-span-2{% endif %}{% endif %}">
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
</div>
{% endif %}
{% else %}
<div class="{% if full_width and field.name in full_width %}{% if cols == 3 %}md:col-span-2 lg:col-span-3{% else %}md:col-span-2{% endif %}{% endif %}">
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
</div>
{% endif %}
{% endfor %}
</div>
{# Form actions - full width #}
{% if show_actions|default:True %}
<div class="flex items-center justify-end gap-3 mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
{% if show_cancel and cancel_url %}
<a href="{{ cancel_url }}" class="btn-secondary">
{{ cancel_text|default:'Cancel' }}
</a>
{% endif %}
<button type="submit" class="{{ submit_class|default:'btn-primary' }}">
{{ submit_text }}
</button>
</div>
{% endif %}
</div>
{% endwith %}

View File

@@ -0,0 +1,149 @@
{% comment %}
Inline Form Layout
==================
Renders form fields horizontally with labels inline with inputs.
Purpose:
Provides an inline/horizontal form layout where labels appear
to the left of inputs on larger screens.
Usage Examples:
Basic inline form:
{% include 'forms/layouts/inline.html' with form=form %}
Custom label width:
{% include 'forms/layouts/inline.html' with form=form label_width='w-1/4' %}
Parameters:
Required:
- form: Django form object
Optional:
- exclude: Comma-separated field names to exclude
- fields: Comma-separated field names to include
- label_width: Label column width class (default: 'w-1/3')
- show_help: Show help text (default: True)
- show_required: Show required indicator (default: True)
- submit_text: Submit button text (default: 'Submit')
- submit_class: Submit button CSS class (default: 'btn-primary')
- show_cancel: Show cancel button (default: False)
- cancel_url: URL for cancel button
- form_class: Additional CSS classes for form wrapper
Dependencies:
- forms/partials/form_field.html
- Tailwind CSS
Accessibility:
- Labels properly associated with inputs
- Responsive - stacks on mobile
{% endcomment %}
{% load common_filters %}
{% with show_help=show_help|default:True show_required=show_required|default:True label_width=label_width|default:'w-1/3' submit_text=submit_text|default:'Submit' %}
<div class="form-layout-inline {{ form_class }}">
{# Non-field errors #}
{% if form.non_field_errors %}
<div class="mb-4 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
role="alert">
<ul class="text-sm text-red-700 dark:text-red-300 space-y-1">
{% for error in form.non_field_errors %}
<li class="flex items-start gap-2">
<i class="fas fa-exclamation-circle mt-0.5" aria-hidden="true"></i>
{{ error }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{# Form fields - inline layout #}
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% elif fields %}
{% if field.name in fields %}
<div class="mb-4 sm:flex sm:items-start">
<label for="{{ field.id_for_label }}"
class="block mb-1 sm:mb-0 sm:{{ label_width }} sm:pt-2 sm:pr-4 sm:text-right text-sm font-medium text-gray-700 dark:text-gray-300">
{{ field.label }}
{% if show_required and field.field.required %}
<span class="text-red-500" aria-hidden="true">*</span>
{% endif %}
</label>
<div class="flex-1">
{{ field|add_class:'w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white' }}
{% if field.errors %}
{% include 'forms/partials/field_error.html' with errors=field.errors %}
{% endif %}
{% if show_help and field.help_text %}
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>
{% endif %}
</div>
</div>
{% endif %}
{% elif exclude %}
{% if field.name not in exclude %}
<div class="mb-4 sm:flex sm:items-start">
<label for="{{ field.id_for_label }}"
class="block mb-1 sm:mb-0 sm:{{ label_width }} sm:pt-2 sm:pr-4 sm:text-right text-sm font-medium text-gray-700 dark:text-gray-300">
{{ field.label }}
{% if show_required and field.field.required %}
<span class="text-red-500" aria-hidden="true">*</span>
{% endif %}
</label>
<div class="flex-1">
{{ field|add_class:'w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white' }}
{% if field.errors %}
{% include 'forms/partials/field_error.html' with errors=field.errors %}
{% endif %}
{% if show_help and field.help_text %}
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<div class="mb-4 sm:flex sm:items-start">
<label for="{{ field.id_for_label }}"
class="block mb-1 sm:mb-0 sm:{{ label_width }} sm:pt-2 sm:pr-4 sm:text-right text-sm font-medium text-gray-700 dark:text-gray-300">
{{ field.label }}
{% if show_required and field.field.required %}
<span class="text-red-500" aria-hidden="true">*</span>
{% endif %}
</label>
<div class="flex-1">
{{ field|add_class:'w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white' }}
{% if field.errors %}
{% include 'forms/partials/field_error.html' with errors=field.errors %}
{% endif %}
{% if show_help and field.help_text %}
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
{# Form actions - aligned with inputs #}
{% if show_actions|default:True %}
<div class="sm:flex sm:items-center mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
<div class="sm:{{ label_width }}"></div>
<div class="flex-1 flex items-center justify-start gap-3">
<button type="submit" class="{{ submit_class|default:'btn-primary' }}">
{{ submit_text }}
</button>
{% if show_cancel and cancel_url %}
<a href="{{ cancel_url }}" class="btn-secondary">
{{ cancel_text|default:'Cancel' }}
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endwith %}

View File

@@ -0,0 +1,106 @@
{% comment %}
Stacked Form Layout
===================
Renders form fields in a vertical stacked layout (default form layout).
Purpose:
Provides a standard vertical form layout where each field takes
full width with labels above inputs.
Usage Examples:
Basic form:
{% include 'forms/layouts/stacked.html' with form=form %}
With fieldsets:
{% include 'forms/layouts/stacked.html' with form=form show_fieldsets=True %}
Exclude fields:
{% include 'forms/layouts/stacked.html' with form=form exclude='password2' %}
Parameters:
Required:
- form: Django form object
Optional:
- exclude: Comma-separated field names to exclude
- fields: Comma-separated field names to include (if set, only these are shown)
- show_fieldsets: Group fields by fieldset (default: False)
- show_help: Show help text (default: True)
- show_required: Show required indicator (default: True)
- submit_text: Submit button text (default: 'Submit')
- submit_class: Submit button CSS class (default: 'btn-primary')
- show_cancel: Show cancel button (default: False)
- cancel_url: URL for cancel button
- cancel_text: Cancel button text (default: 'Cancel')
- form_class: Additional CSS classes for form wrapper
Dependencies:
- forms/partials/form_field.html
- forms/partials/field_error.html
- Tailwind CSS
Accessibility:
- Uses fieldset/legend for grouped fields
- Labels properly associated with inputs
- Error summary at top for screen readers
{% endcomment %}
{% load common_filters %}
{% with show_help=show_help|default:True show_required=show_required|default:True submit_text=submit_text|default:'Submit' %}
<div class="form-layout-stacked {{ form_class }}">
{# Non-field errors at top #}
{% if form.non_field_errors %}
<div class="mb-4 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
role="alert">
<div class="flex items-start gap-2">
<i class="fas fa-exclamation-circle text-red-500 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Please correct the following errors:</h3>
<ul class="mt-1 text-sm text-red-700 dark:text-red-300 list-disc list-inside">
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}
{# Form fields #}
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% elif fields %}
{# Only show specified fields #}
{% if field.name in fields %}
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
{% endif %}
{% elif exclude %}
{# Exclude specified fields #}
{% if field.name not in exclude %}
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
{% endif %}
{% else %}
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
{% endif %}
{% endfor %}
{# Form actions #}
{% if show_actions|default:True %}
<div class="flex items-center justify-end gap-3 mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
{% if show_cancel and cancel_url %}
<a href="{{ cancel_url }}" class="btn-secondary {{ cancel_class }}">
{{ cancel_text|default:'Cancel' }}
</a>
{% endif %}
<button type="submit" class="{{ submit_class|default:'btn-primary' }}">
{{ submit_text }}
</button>
</div>
{% endif %}
</div>
{% endwith %}

View File

@@ -1,7 +1,57 @@
{% comment %}
Field Error Component
=====================
Displays error messages for a form field with icon and proper accessibility.
Purpose:
Renders error messages in a consistent, accessible format with visual
indicators. Used within form_field.html or standalone.
Usage Examples:
Within form_field.html (automatic):
{% include 'forms/partials/field_error.html' with errors=field.errors %}
Standalone:
{% include 'forms/partials/field_error.html' with errors=form.email.errors %}
Non-field errors:
{% include 'forms/partials/field_error.html' with errors=form.non_field_errors %}
Parameters:
Required:
- errors: List of error messages (typically field.errors)
Optional:
- show_icon: Show error icon (default: True)
- animate: Add entrance animation (default: True)
- size: 'sm', 'md', 'lg' (default: 'sm')
Dependencies:
- Tailwind CSS for styling
- Font Awesome icons
Accessibility:
- Uses role="alert" for immediate screen reader announcement
- aria-live="assertive" for dynamic error display
- Error icon is decorative (aria-hidden)
{% endcomment %}
{% with show_icon=show_icon|default:True animate=animate|default:True size=size|default:'sm' %}
{% if errors %}
<ul class="field-errors">
{% for e in errors %}
<li>{{ e }}</li>
<ul class="{% if size == 'lg' %}text-base{% elif size == 'md' %}text-sm{% else %}text-xs{% endif %} text-red-600 dark:text-red-400 space-y-1 {% if animate %}animate-slide-down{% endif %}"
role="alert"
aria-live="assertive">
{% for error in errors %}
<li class="flex items-start gap-1.5">
{% if show_icon %}
<i class="fas fa-exclamation-circle mt-0.5 flex-shrink-0" aria-hidden="true"></i>
{% endif %}
<span>{{ error }}</span>
</li>
{% endfor %}
</ul>
</ul>
{% endif %}
{% endwith %}

View File

@@ -1 +1,48 @@
<div class="field-success" aria-hidden="true"></div>
{% comment %}
Field Success Component
=======================
Displays success message for a validated form field with icon.
Purpose:
Renders a success indicator when a field passes validation.
Typically used with HTMX inline validation.
Usage Examples:
After successful validation:
{% include 'forms/partials/field_success.html' with message='Username is available' %}
Simple checkmark (no message):
{% include 'forms/partials/field_success.html' %}
Parameters:
Optional:
- message: Success message text (if empty, shows only icon)
- show_icon: Show success icon (default: True)
- animate: Add entrance animation (default: True)
- size: 'sm', 'md', 'lg' (default: 'sm')
Dependencies:
- Tailwind CSS for styling
- Font Awesome icons
Accessibility:
- Uses role="status" for polite screen reader announcement
- aria-live="polite" for non-urgent updates
- Success icon is decorative (aria-hidden)
{% endcomment %}
{% with show_icon=show_icon|default:True animate=animate|default:True size=size|default:'sm' %}
<div class="{% if size == 'lg' %}text-base{% elif size == 'md' %}text-sm{% else %}text-xs{% endif %} text-green-600 dark:text-green-400 flex items-center gap-1.5 {% if animate %}animate-slide-down{% endif %}"
role="status"
aria-live="polite">
{% if show_icon %}
<i class="fas fa-check-circle flex-shrink-0" aria-hidden="true"></i>
{% endif %}
{% if message %}
<span>{{ message }}</span>
{% endif %}
</div>
{% endwith %}

View File

@@ -1,4 +1,135 @@
<div class="form-actions">
<button type="submit" class="btn-primary">Save</button>
<button type="button" class="btn-secondary" hx-trigger="click" hx-swap="none">Cancel</button>
{% comment %}
Form Actions Component
======================
Renders form submit and cancel buttons with consistent styling.
Purpose:
Provides a standardized form actions section with submit button,
optional cancel button, and loading state support.
Usage Examples:
Basic submit:
{% include 'forms/partials/form_actions.html' %}
With cancel:
{% include 'forms/partials/form_actions.html' with show_cancel=True cancel_url='/list/' %}
Custom text:
{% include 'forms/partials/form_actions.html' with submit_text='Save Changes' %}
With loading state:
{% include 'forms/partials/form_actions.html' with show_loading=True loading_id='submit-loading' %}
Right-aligned (default):
{% include 'forms/partials/form_actions.html' %}
Left-aligned:
{% include 'forms/partials/form_actions.html' with align='left' %}
HTMX form:
{% include 'forms/partials/form_actions.html' with hx_disable='true' %}
Parameters:
Optional (submit):
- submit_text: Submit button text (default: 'Save')
- submit_class: CSS class (default: 'btn-primary')
- submit_disabled: Disable submit button (default: False)
- submit_icon: Icon class for submit button (e.g., 'fas fa-check')
Optional (cancel):
- show_cancel: Show cancel button (default: False)
- cancel_url: URL for cancel link
- cancel_text: Cancel button text (default: 'Cancel')
- cancel_class: CSS class (default: 'btn-secondary')
Optional (loading):
- show_loading: Show loading indicator on submit (default: False)
- loading_id: ID for htmx-indicator (default: 'submit-loading')
Optional (layout):
- align: 'left', 'right', 'center', 'between' (default: 'right')
- show_border: Show top border (default: True)
Optional (HTMX):
- hx_disable: Add hx-disable on submit (default: False)
Dependencies:
- Tailwind CSS
- HTMX (optional, for loading states)
- Font Awesome (optional, for icons)
Accessibility:
- Submit button is properly typed
- Loading state announced to screen readers
{% endcomment %}
{% with submit_text=submit_text|default:'Save' cancel_text=cancel_text|default:'Cancel' align=align|default:'right' show_border=show_border|default:True %}
<div class="form-actions flex items-center gap-3 mt-6
{% if show_border %}pt-4 border-t border-gray-200 dark:border-gray-700{% endif %}
{% if align == 'left' %}justify-start
{% elif align == 'center' %}justify-center
{% elif align == 'between' %}justify-between
{% else %}justify-end{% endif %}">
{# Cancel button (left side for 'between' alignment) #}
{% if show_cancel and align == 'between' %}
{% if cancel_url %}
<a href="{{ cancel_url }}"
class="{{ cancel_class|default:'btn-secondary' }}">
{{ cancel_text }}
</a>
{% else %}
<button type="button"
class="{{ cancel_class|default:'btn-secondary' }}"
onclick="history.back()">
{{ cancel_text }}
</button>
{% endif %}
{% endif %}
{# Button group #}
<div class="flex items-center gap-3">
{# Cancel button (for non-between alignment) #}
{% if show_cancel and align != 'between' %}
{% if cancel_url %}
<a href="{{ cancel_url }}"
class="{{ cancel_class|default:'btn-secondary' }}">
{{ cancel_text }}
</a>
{% else %}
<button type="button"
class="{{ cancel_class|default:'btn-secondary' }}"
onclick="history.back()">
{{ cancel_text }}
</button>
{% endif %}
{% endif %}
{# Submit button #}
<button type="submit"
class="{{ submit_class|default:'btn-primary' }} relative"
{% if submit_disabled %}disabled{% endif %}
{% if hx_disable %}hx-disable{% endif %}>
{# Icon (optional) #}
{% if submit_icon %}
<i class="{{ submit_icon }} mr-2" aria-hidden="true"></i>
{% endif %}
{# Text #}
<span>{{ submit_text }}</span>
{# Loading indicator (optional) #}
{% if show_loading %}
<span id="{{ loading_id|default:'submit-loading' }}"
class="htmx-indicator ml-2">
<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>
<span class="sr-only">Submitting...</span>
</span>
{% endif %}
</button>
</div>
</div>
{% endwith %}

View File

@@ -1,5 +1,198 @@
<div class="form-field" data-field-name="{{ field.name }}">
<label for="id_{{ field.name }}">{{ field.label }}</label>
{{ field }}
<div class="field-feedback" aria-live="polite">{% include "forms/partials/field_error.html" %}</div>
{% comment %}
Form Field Component
====================
A comprehensive form field component with label, input, help text, and error handling.
Purpose:
Renders a complete form field with consistent styling, accessibility attributes,
and optional HTMX validation support.
Usage Examples:
Basic field:
{% include 'forms/partials/form_field.html' with field=form.email %}
Field with custom label:
{% include 'forms/partials/form_field.html' with field=form.email label='Email Address' %}
Field without label:
{% include 'forms/partials/form_field.html' with field=form.hidden_field show_label=False %}
Field with HTMX validation:
{% include 'forms/partials/form_field.html' with field=form.username hx_validate=True hx_validate_url='/api/validate-username/' %}
Inline field:
{% include 'forms/partials/form_field.html' with field=form.email layout='inline' %}
With success state:
{% include 'forms/partials/form_field.html' with field=form.email show_success=True %}
Parameters:
Required:
- field: Django form field object
Optional (labels):
- label: Custom label text (default: field.label)
- show_label: Show label (default: True)
- label_class: Additional CSS classes for label
Optional (input):
- input_class: Additional CSS classes for input
- placeholder: Custom placeholder text
- autofocus: Add autofocus attribute (default: False)
- disabled: Disable input (default: False)
- readonly: Make input readonly (default: False)
Optional (help/errors):
- help_text: Custom help text (default: field.help_text)
- show_help: Show help text (default: True)
- show_errors: Show error messages (default: True)
- show_success: Show success indicator when valid (default: False)
Optional (HTMX validation):
- hx_validate: Enable HTMX inline validation (default: False)
- hx_validate_url: URL for validation endpoint
- hx_validate_trigger: Trigger event (default: 'blur changed delay:500ms')
- hx_validate_target: Target for validation response (default: auto-generated)
Optional (layout):
- layout: 'stacked' or 'inline' (default: 'stacked')
- required_indicator: Show required indicator (default: True)
- size: 'sm', 'md', 'lg' for input size (default: 'md')
Dependencies:
- Tailwind CSS for styling
- HTMX (optional, for inline validation)
- Alpine.js (optional, for enhanced interactions)
- common_filters template tags (for add_class filter)
Accessibility:
- Label properly associated with input via for/id
- aria-describedby links help text and errors to input
- aria-invalid set when field has errors
- aria-required set for required fields
- Error messages use role="alert" for screen reader announcement
{% endcomment %}
{% load common_filters %}
{% with show_label=show_label|default:True show_help=show_help|default:True show_errors=show_errors|default:True show_success=show_success|default:False required_indicator=required_indicator|default:True layout=layout|default:'stacked' size=size|default:'md' %}
<div class="form-field {% if layout == 'inline' %}flex items-center gap-4{% else %}mb-4{% endif %}"
data-field-name="{{ field.name }}"
data-field-required="{{ field.field.required|yesno:'true,false' }}"
{% if hx_validate %}x-data="{ valid: null, validating: false, touched: false }"{% endif %}>
{# Label #}
{% if show_label %}
<label for="{{ field.id_for_label }}"
class="{% if layout == 'inline' %}w-1/3 text-right{% else %}block mb-1.5{% endif %} {% if size == 'sm' %}text-xs{% elif size == 'lg' %}text-base{% else %}text-sm{% endif %} font-medium text-foreground {{ label_class }}">
{{ label|default:field.label }}
{% if required_indicator and field.field.required %}
<span class="text-destructive ml-0.5" aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
{% endif %}
</label>
{% endif %}
{# Input wrapper #}
<div class="{% if layout == 'inline' and show_label %}flex-1{% else %}w-full{% endif %} relative">
{# Field input with enhanced attributes #}
{# Base widget classes for sizing, spacing, and transitions #}
{% with base_class='w-full border rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-0 bg-background text-foreground placeholder:text-muted-foreground' %}
{# Size classes #}
{% with size_class=size|default:'md'|yesno:'px-2.5 py-1.5 text-xs,px-3 py-2 text-sm,px-4 py-2.5 text-base' %}
{% if size == 'sm' %}{% with actual_size='px-2.5 py-1.5 text-xs' %}
{# Error-specific classes override border and focus ring colors #}
{% with error_class='border-destructive focus:ring-destructive/30 focus:border-destructive' %}
{# Success-specific classes #}
{% with success_class='border-green-500 focus:ring-green-500/30 focus:border-green-500' %}
{# Normal state classes for border and focus ring #}
{% with normal_class='border-input focus:ring-ring/30 focus:border-ring' %}
{# Render the field with base classes plus state-specific classes #}
{% if field.errors %}
{{ field|add_class:base_class|add_class:actual_size|add_class:error_class|add_class:input_class }}
{% else %}
{{ field|add_class:base_class|add_class:actual_size|add_class:normal_class|add_class:input_class }}
{% endif %}
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
{% elif size == 'lg' %}{% with actual_size='px-4 py-2.5 text-base' %}
{% with error_class='border-destructive focus:ring-destructive/30 focus:border-destructive' %}
{% with success_class='border-green-500 focus:ring-green-500/30 focus:border-green-500' %}
{% with normal_class='border-input focus:ring-ring/30 focus:border-ring' %}
{% if field.errors %}
{{ field|add_class:base_class|add_class:actual_size|add_class:error_class|add_class:input_class }}
{% else %}
{{ field|add_class:base_class|add_class:actual_size|add_class:normal_class|add_class:input_class }}
{% endif %}
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
{% else %}{% with actual_size='px-3 py-2 text-sm' %}
{% with error_class='border-destructive focus:ring-destructive/30 focus:border-destructive' %}
{% with success_class='border-green-500 focus:ring-green-500/30 focus:border-green-500' %}
{% with normal_class='border-input focus:ring-ring/30 focus:border-ring' %}
{% if field.errors %}
{{ field|add_class:base_class|add_class:actual_size|add_class:error_class|add_class:input_class }}
{% else %}
{{ field|add_class:base_class|add_class:actual_size|add_class:normal_class|add_class:input_class }}
{% endif %}
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
{% endif %}
{% endwith %}
{% endwith %}
{# HTMX validation attributes (when enabled) #}
{% if hx_validate and hx_validate_url %}
<script>
(function() {
var el = document.getElementById('{{ field.id_for_label }}');
if (el) {
el.setAttribute('hx-post', '{{ hx_validate_url }}');
el.setAttribute('hx-trigger', '{{ hx_validate_trigger|default:"blur changed delay:500ms" }}');
el.setAttribute('hx-target', '#{{ field.name }}-feedback');
el.setAttribute('hx-swap', 'innerHTML');
el.setAttribute('hx-indicator', '#{{ field.name }}-indicator');
el.setAttribute('hx-include', '[name="{{ field.name }}"]');
if (typeof htmx !== 'undefined') htmx.process(el);
}
})();
</script>
{% endif %}
{# Validation indicator (for HTMX) - positioned inside input #}
{% if hx_validate %}
<div id="{{ field.name }}-indicator" class="htmx-indicator absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
<i class="fas fa-spinner fa-spin text-muted-foreground" aria-hidden="true"></i>
</div>
{% endif %}
{# Success indicator (shown when valid and no errors) #}
{% if show_success and not field.errors and field.value %}
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
<i class="fas fa-check-circle text-green-500" aria-hidden="true"></i>
</div>
{% endif %}
{# Feedback area (errors, success, help) #}
<div id="{{ field.name }}-feedback"
class="field-feedback mt-1.5"
aria-live="polite"
aria-atomic="true">
{# Error messages #}
{% if show_errors and field.errors %}
{% include 'forms/partials/field_error.html' with errors=field.errors %}
{% endif %}
{# Help text #}
{% if show_help and field.help_text %}
<p id="{{ field.name }}-help"
class="{% if size == 'sm' %}text-xs{% elif size == 'lg' %}text-sm{% else %}text-xs{% endif %} text-muted-foreground {% if field.errors %}mt-1{% endif %}">
{{ help_text|default:field.help_text }}
</p>
{% endif %}
</div>
</div>
</div>
{% endwith %}

View File

@@ -0,0 +1,194 @@
{% comment %}
Form Submission Feedback Component
==================================
Displays feedback after form submission with various states.
Purpose:
Provides consistent feedback UI for form submission results including
success confirmations, error summaries, and loading states.
Usage Examples:
Success state:
{% include 'forms/partials/form_submission_feedback.html' with state='success' message='Park saved successfully!' %}
Error state:
{% include 'forms/partials/form_submission_feedback.html' with state='error' message='Please fix the errors below' %}
Loading state:
{% include 'forms/partials/form_submission_feedback.html' with state='loading' message='Saving...' %}
With redirect countdown:
{% include 'forms/partials/form_submission_feedback.html' with state='success' message='Saved!' redirect_url='/parks/' redirect_seconds=3 %}
Parameters:
Required:
- state: 'success', 'error', 'warning', 'loading' (default: 'success')
- message: Main feedback message
Optional:
- title: Optional title text
- show_icon: Show status icon (default: True)
- show_actions: Show action buttons (default: True)
- primary_action_text: Primary button text (default: varies by state)
- primary_action_url: Primary button URL
- secondary_action_text: Secondary button text
- secondary_action_url: Secondary button URL
- redirect_url: URL to redirect to after countdown
- redirect_seconds: Seconds before redirect (default: 3)
- errors: List of error messages (for error state)
- animate: Enable animations (default: True)
Dependencies:
- Tailwind CSS for styling
- Alpine.js for countdown functionality
- Font Awesome icons
Accessibility:
- Uses appropriate ARIA roles based on state
- Announces changes to screen readers
{% endcomment %}
{% with state=state|default:'success' show_icon=show_icon|default:True show_actions=show_actions|default:True animate=animate|default:True redirect_seconds=redirect_seconds|default:3 %}
<div class="form-submission-feedback rounded-lg p-4 {% if animate %}animate-fade-in{% endif %}
{% if state == 'success' %}bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800
{% elif state == 'error' %}bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800
{% elif state == 'warning' %}bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800
{% elif state == 'loading' %}bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800
{% endif %}"
role="{% if state == 'error' %}alert{% else %}status{% endif %}"
aria-live="{% if state == 'error' %}assertive{% else %}polite{% endif %}"
{% if redirect_url %}
x-data="{ countdown: {{ redirect_seconds }} }"
x-init="setInterval(() => { if(countdown > 0) countdown--; else window.location.href = '{{ redirect_url }}'; }, 1000)"
{% endif %}>
<div class="flex {% if title or errors %}flex-col gap-3{% else %}items-center gap-3{% endif %}">
{# Icon #}
{% if show_icon %}
<div class="flex-shrink-0 {% if title or errors %}flex items-center gap-3{% endif %}">
{% if state == 'success' %}
<div class="w-10 h-10 rounded-full bg-green-100 dark:bg-green-800/30 flex items-center justify-center">
<i class="fas fa-check text-green-600 dark:text-green-400 text-lg" aria-hidden="true"></i>
</div>
{% elif state == 'error' %}
<div class="w-10 h-10 rounded-full bg-red-100 dark:bg-red-800/30 flex items-center justify-center">
<i class="fas fa-exclamation-circle text-red-600 dark:text-red-400 text-lg" aria-hidden="true"></i>
</div>
{% elif state == 'warning' %}
<div class="w-10 h-10 rounded-full bg-yellow-100 dark:bg-yellow-800/30 flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-yellow-600 dark:text-yellow-400 text-lg" aria-hidden="true"></i>
</div>
{% elif state == 'loading' %}
<div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-800/30 flex items-center justify-center">
<i class="fas fa-spinner fa-spin text-blue-600 dark:text-blue-400 text-lg" aria-hidden="true"></i>
</div>
{% endif %}
{# Title (if provided) #}
{% if title %}
<h3 class="font-semibold
{% if state == 'success' %}text-green-800 dark:text-green-200
{% elif state == 'error' %}text-red-800 dark:text-red-200
{% elif state == 'warning' %}text-yellow-800 dark:text-yellow-200
{% elif state == 'loading' %}text-blue-800 dark:text-blue-200
{% endif %}">
{{ title }}
</h3>
{% endif %}
</div>
{% endif %}
{# Content #}
<div class="flex-1 min-w-0">
{# Main message #}
<p class="text-sm
{% if state == 'success' %}text-green-700 dark:text-green-300
{% elif state == 'error' %}text-red-700 dark:text-red-300
{% elif state == 'warning' %}text-yellow-700 dark:text-yellow-300
{% elif state == 'loading' %}text-blue-700 dark:text-blue-300
{% endif %}">
{{ message }}
</p>
{# Error list (for error state) #}
{% if state == 'error' and errors %}
<ul class="mt-2 text-sm text-red-600 dark:text-red-400 space-y-1 list-disc list-inside">
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
{# Redirect countdown #}
{% if redirect_url %}
<p class="mt-2 text-xs
{% if state == 'success' %}text-green-600 dark:text-green-400
{% else %}text-muted-foreground
{% endif %}">
Redirecting in <span x-text="countdown"></span> seconds...
<a href="{{ redirect_url }}" class="underline hover:no-underline ml-1">Go now</a>
</p>
{% endif %}
</div>
{# Actions #}
{% if show_actions and state != 'loading' %}
<div class="flex-shrink-0 flex items-center gap-2 {% if title or errors %}mt-2{% endif %}">
{% if state == 'error' %}
{# Error state actions #}
{% if secondary_action_url %}
<a href="{{ secondary_action_url }}"
class="btn btn-sm btn-ghost text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-800/30">
{{ secondary_action_text|default:'Cancel' }}
</a>
{% endif %}
{% if primary_action_url %}
<a href="{{ primary_action_url }}"
class="btn btn-sm bg-red-600 hover:bg-red-700 text-white">
{{ primary_action_text|default:'Try Again' }}
</a>
{% else %}
<button type="submit"
class="btn btn-sm bg-red-600 hover:bg-red-700 text-white">
{{ primary_action_text|default:'Try Again' }}
</button>
{% endif %}
{% elif state == 'success' %}
{# Success state actions #}
{% if secondary_action_url %}
<a href="{{ secondary_action_url }}"
class="btn btn-sm btn-ghost text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800/30">
{{ secondary_action_text|default:'Add Another' }}
</a>
{% endif %}
{% if primary_action_url %}
<a href="{{ primary_action_url }}"
class="btn btn-sm bg-green-600 hover:bg-green-700 text-white">
{{ primary_action_text|default:'View' }}
</a>
{% endif %}
{% elif state == 'warning' %}
{# Warning state actions #}
{% if secondary_action_url %}
<a href="{{ secondary_action_url }}"
class="btn btn-sm btn-ghost">
{{ secondary_action_text|default:'Cancel' }}
</a>
{% endif %}
{% if primary_action_url %}
<a href="{{ primary_action_url }}"
class="btn btn-sm bg-yellow-600 hover:bg-yellow-700 text-white">
{{ primary_action_text|default:'Continue' }}
</a>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endwith %}