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>
This commit is contained in:
380
public_web/docs/RESPONSIVE_MODALS.md
Normal file
380
public_web/docs/RESPONSIVE_MODALS.md
Normal file
@@ -0,0 +1,380 @@
|
||||
# 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`
|
||||
|
||||
```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`
|
||||
|
||||
```python
|
||||
@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`
|
||||
|
||||
```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`
|
||||
|
||||
```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()">×</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`
|
||||
|
||||
```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`
|
||||
|
||||
```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`
|
||||
|
||||
```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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
if (isMobile()) {
|
||||
// Mobile-specific behavior
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Breakpoint
|
||||
|
||||
The mobile/desktop breakpoint is **1024px** to match the CSS media query:
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- **HTMX_PATTERNS.md** - HTMX integration patterns
|
||||
- **TEMPLATES.md** - Template structure and conventions
|
||||
- **TESTING.md** - Manual testing procedures
|
||||
Reference in New Issue
Block a user