Files
thrillwiki_django_no_react/docs/ux/developer-guidelines.md

11 KiB

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:

<!-- 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:

<!-- 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:

# 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

def park_list(request):
    parks = Park.objects.all()
    return render(request, 'parks/list.html', {'parks': parks})

HTMX-Aware View

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

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:

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:

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

<!-- 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

<!-- 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

<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

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

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

<!-- 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

// 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

<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

<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

/* Use CSS variables */
.custom-element {
    color: hsl(var(--foreground));
    background: hsl(var(--background));
    border-color: hsl(var(--border));
}

2. Use Component Classes

<!-- 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

<!-- 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

<!-- 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

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

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

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

<!-- 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

<div id="content"
     hx-get="/parks/"
     hx-trigger="load"
     hx-swap="innerHTML">
    {% include 'components/skeletons/card_grid_skeleton.html' %}
</div>

3. Lazy Load Content

<!-- Load when visible -->
<div hx-get="/stats/"
     hx-trigger="revealed"
     hx-swap="innerHTML">
    <div class="skeleton h-20"></div>
</div>

4. Debounce Input

<!-- Debounce search input -->
<input hx-get="/search/"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#results">

Security Guidelines

1. CSRF Protection

<!-- 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:

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

<!-- Auto-escaped by Django -->
{{ user_input }}

<!-- Explicitly mark as safe only for trusted content -->
{{ trusted_html|safe }}

4. Use Appropriate Permissions

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