Files
Code_of_Conquest/public_web/docs/RESPONSIVE_MODALS.md
Phillip Tarrant 2419dbeb34 feat(web): implement responsive modal pattern for mobile-friendly NPC chat
- Add hybrid modal/page navigation based on screen size (1024px breakpoint)
- Desktop (>1024px): Uses modal overlays for quick interactions
- Mobile (≤1024px): Navigates to dedicated full pages for better UX
- Extract shared NPC chat content into reusable partial template
- Add responsive navigation JavaScript (responsive-modals.js)
- Create dedicated NPC chat page route with back button navigation
- Add mobile-optimized CSS with sticky header and chat input
- Fix HTMX indicator errors by using htmx-indicator class pattern
- Document responsive modal pattern for future features

Addresses mobile UX issues: cramped space, nested scrolling, keyboard conflicts,
and lack of native back button support in modals.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 21:30:51 -06:00

9.4 KiB

Responsive Modal Pattern

Overview

This pattern provides an optimal UX for modal-like content by adapting to screen size:

  • Desktop (>1024px): Displays content in modal overlays
  • Mobile (≤1024px): Navigates to dedicated full pages

This addresses common mobile modal UX issues: cramped space, nested scrolling, keyboard conflicts, and lack of native back button support.


When to Use This Pattern

Use this pattern when:

  • Content requires significant interaction (forms, chat, complex data)
  • Mobile users need better scroll/keyboard handling
  • The interaction benefits from full-screen on mobile
  • Desktop users benefit from keeping context visible

Don't use for:

  • Simple confirmations/alerts (use standard modals)
  • Very brief interactions (dropdowns, tooltips)
  • Content that must overlay the game state

Implementation Steps

1. Create Shared Content Partial

Extract the actual content into a reusable partial template that both the modal and page can use.

File: templates/game/partials/[feature]_content.html

{# Shared content partial #}
<div class="feature-container">
    <!-- Your content here -->
    <!-- This will be used by both modal and page -->
</div>

2. Create Two Routes

Create both a modal route and a page route in your view:

File: app/views/game_views.py

@game_bp.route('/session/<session_id>/feature/<feature_id>')
@require_auth
def feature_page(session_id: str, feature_id: str):
    """
    Dedicated page view (mobile-friendly).
    Used on mobile devices for better UX.
    """
    # Fetch data
    data = get_feature_data(session_id, feature_id)

    return render_template(
        'game/feature_page.html',
        session_id=session_id,
        feature=data
    )


@game_bp.route('/session/<session_id>/feature/<feature_id>/modal')
@require_auth
def feature_modal(session_id: str, feature_id: str):
    """
    Modal view (desktop overlay).
    Used on desktop for quick interactions.
    """
    # Fetch same data
    data = get_feature_data(session_id, feature_id)

    return render_template(
        'game/partials/feature_modal.html',
        session_id=session_id,
        feature=data
    )

3. Create Page Template

Create a dedicated page template with header, back button, and shared content.

File: templates/game/feature_page.html

{% extends "base.html" %}

{% block title %}{{ feature.name }} - Code of Conquest{% endblock %}

{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/play.css') }}">
{% endblock %}

{% block content %}
<div class="feature-page">
    {# Page Header with Back Button #}
    <div class="feature-header">
        <a href="{{ url_for('game.play_session', session_id=session_id) }}"
           class="feature-back-btn">
            <svg><!-- Back arrow icon --></svg>
            Back
        </a>
        <h1 class="feature-title">{{ feature.name }}</h1>
    </div>

    {# Include shared content #}
    {% include 'game/partials/feature_content.html' %}
</div>
{% endblock %}

{% block scripts %}
<script>
// Add any feature-specific JavaScript here
</script>
{% endblock %}

4. Update Modal Template

Update your modal template to use the shared content partial.

File: templates/game/partials/feature_modal.html

{# Modal wrapper #}
<div class="modal-overlay" onclick="if(event.target === this) closeModal()">
    <div class="modal-content modal-content--xl">
        {# Modal Header #}
        <div class="modal-header">
            <h3 class="modal-title">{{ feature.name }}</h3>
            <button class="modal-close" onclick="closeModal()">&times;</button>
        </div>

        {# Modal Body - Uses shared content #}
        <div class="modal-body">
            {% include 'game/partials/feature_content.html' %}
        </div>

        {# Modal Footer (optional) #}
        <div class="modal-footer">
            <button class="btn btn--secondary" onclick="closeModal()">
                Close
            </button>
        </div>
    </div>
</div>

5. Add Responsive Navigation

Update the trigger element to use responsive navigation JavaScript.

File: templates/game/partials/sidebar_features.html

<div class="feature-item"
     data-feature-id="{{ feature.id }}"
     onclick="navigateResponsive(
         event,
         '{{ url_for('game.feature_page', session_id=session_id, feature_id=feature.id) }}',
         '{{ url_for('game.feature_modal', session_id=session_id, feature_id=feature.id) }}'
     )"
     style="cursor: pointer;">
    <div class="feature-name">{{ feature.name }}</div>
</div>

6. Add Mobile-Friendly CSS

Add CSS for the dedicated page with mobile optimizations.

File: static/css/play.css

/* Feature Page */
.feature-page {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--bg-secondary);
}

/* Page Header with Back Button */
.feature-header {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1rem;
    background: var(--bg-primary);
    border-bottom: 2px solid var(--play-border);
    position: sticky;
    top: 0;
    z-index: 100;
}

.feature-back-btn {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    background: var(--bg-input);
    color: var(--text-primary);
    text-decoration: none;
    border-radius: 4px;
    border: 1px solid var(--play-border);
    transition: all 0.2s ease;
}

.feature-back-btn:hover {
    background: var(--bg-tertiary);
    border-color: var(--accent-gold);
    color: var(--accent-gold);
}

.feature-title {
    flex: 1;
    margin: 0;
    font-family: var(--font-heading);
    font-size: 1.5rem;
    color: var(--accent-gold);
}

/* Responsive layout */
@media (min-width: 1025px) {
    .feature-page .feature-container {
        max-width: 1400px;
        margin: 0 auto;
        padding: 2rem;
    }
}

@media (max-width: 1024px) {
    .feature-page .feature-container {
        padding: 1rem;
        flex: 1;
    }
}

7. Include JavaScript

Ensure the responsive modals JavaScript is included in your play page template.

File: templates/game/play.html

{% block scripts %}
<!-- Existing scripts -->
<script>
// Your existing JavaScript
</script>

<!-- Responsive Modal Navigation -->
<script src="{{ url_for('static', filename='js/responsive-modals.js') }}"></script>
{% endblock %}

JavaScript API

The responsive navigation is handled by responsive-modals.js:

Functions

navigateResponsive(event, pageUrl, modalUrl)

Navigates responsively based on screen size.

Parameters:

  • event (Event): Click event (will be prevented)
  • pageUrl (string): Full page URL for mobile navigation
  • modalUrl (string): Modal content URL for desktop HTMX loading

Example:

navigateResponsive(
    event,
    '/game/session/123/npc/456',      // Page route
    '/game/session/123/npc/456/chat'  // Modal route
)

isMobile()

Returns true if viewport is ≤1024px.

Example:

if (isMobile()) {
    // Mobile-specific behavior
}

Breakpoint

The mobile/desktop breakpoint is 1024px to match the CSS media query:

const MOBILE_BREAKPOINT = 1024;

This ensures consistent behavior between JavaScript navigation and CSS responsive layouts.


Example: NPC Chat

See the NPC chat implementation for a complete working example:

Routes:

  • Page: /game/session/<session_id>/npc/<npc_id> (public_web/app/views/game_views.py:695)
  • Modal: /game/session/<session_id>/npc/<npc_id>/chat (public_web/app/views/game_views.py:746)

Templates:

  • Shared content: templates/game/partials/npc_chat_content.html
  • Page: templates/game/npc_chat_page.html
  • Modal: templates/game/partials/npc_chat_modal.html

Trigger:

  • templates/game/partials/sidebar_npcs.html (line 11)

CSS:

  • Page styles: static/css/play.css (lines 1809-1956)

Testing Checklist

When implementing this pattern, test:

  • Desktop (>1024px): Modal opens correctly
  • Desktop: Modal closes with X button, overlay click, and Escape key
  • Desktop: Game screen remains visible behind modal
  • Mobile (≤1024px): Navigates to dedicated page
  • Mobile: Back button returns to game
  • Mobile: Content scrolls naturally
  • Mobile: Keyboard doesn't break layout
  • Both: Shared content renders identically
  • Both: HTMX interactions work correctly
  • Resize: Behavior adapts when crossing breakpoint

Best Practices

  1. Always share content: Use a partial template to avoid duplication
  2. Keep routes parallel: Use consistent URL patterns (/feature and /feature/modal)
  3. Test both views: Ensure feature parity between modal and page
  4. Mobile-first CSS: Design for mobile, enhance for desktop
  5. Consistent breakpoint: Always use 1024px to match the JavaScript
  6. Document examples: Link to working implementations in this doc

Future Improvements

Potential enhancements to this pattern:

  • Add transition animations for modal open/close
  • Support deep linking to modals on desktop
  • Add browser back button handling for desktop modals
  • Support customizable breakpoints per feature
  • Add analytics tracking for modal vs page usage

  • HTMX_PATTERNS.md - HTMX integration patterns
  • TEMPLATES.md - Template structure and conventions
  • TESTING.md - Manual testing procedures