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:
44
public_web/templates/game/npc_chat_page.html
Normal file
44
public_web/templates/game/npc_chat_page.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ npc.name }} - Code of Conquest{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<!-- Play screen styles for NPC chat -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/play.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="npc-chat-page">
|
||||
{# Page Header with Back Button #}
|
||||
<div class="npc-chat-header">
|
||||
<a href="{{ url_for('game.play_session', session_id=session_id) }}" class="npc-chat-back-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Back
|
||||
</a>
|
||||
<h1 class="npc-chat-title">{{ npc.name }}</h1>
|
||||
</div>
|
||||
|
||||
{# Include shared NPC chat content #}
|
||||
{% include 'game/partials/npc_chat_content.html' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Clear chat input after submission
|
||||
document.body.addEventListener('htmx:afterSwap', function(e) {
|
||||
if (e.target.closest('.chat-history')) {
|
||||
const form = document.querySelector('.chat-input-form');
|
||||
if (form) {
|
||||
const input = form.querySelector('.chat-input');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
120
public_web/templates/game/partials/npc_chat_content.html
Normal file
120
public_web/templates/game/partials/npc_chat_content.html
Normal file
@@ -0,0 +1,120 @@
|
||||
{#
|
||||
NPC Chat Content (Shared Partial)
|
||||
Used by both modal and dedicated page views
|
||||
Displays NPC profile, conversation interface, and message history
|
||||
#}
|
||||
<div class="npc-chat-container">
|
||||
{# Left Column: NPC Profile #}
|
||||
<div class="npc-profile">
|
||||
{# NPC Portrait #}
|
||||
<div class="npc-portrait">
|
||||
{% if npc.image_url %}
|
||||
<img src="{{ npc.image_url }}" alt="{{ npc.name }}">
|
||||
{% else %}
|
||||
<div class="npc-portrait-placeholder">
|
||||
{{ npc.name[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# NPC Info #}
|
||||
<div class="npc-profile-info">
|
||||
<div class="npc-profile-role">{{ npc.role }}</div>
|
||||
{% if npc.appearance %}
|
||||
<div class="npc-profile-appearance">{{ npc.appearance }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# NPC Tags #}
|
||||
{% if npc.tags %}
|
||||
<div class="npc-profile-tags">
|
||||
{% for tag in npc.tags %}
|
||||
<span class="npc-profile-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Relationship Meter #}
|
||||
<div class="npc-relationship">
|
||||
<div class="relationship-header">
|
||||
<span class="relationship-label">Relationship</span>
|
||||
<span class="relationship-value">{{ relationship_level|default(50) }}/100</span>
|
||||
</div>
|
||||
<div class="relationship-bar">
|
||||
<div class="relationship-fill" style="width: {{ relationship_level|default(50) }}%"></div>
|
||||
</div>
|
||||
<div class="relationship-status">
|
||||
{% set level = relationship_level|default(50) %}
|
||||
{% if level >= 80 %}
|
||||
<span class="status-friendly">Trusted Ally</span>
|
||||
{% elif level >= 60 %}
|
||||
<span class="status-friendly">Friendly</span>
|
||||
{% elif level >= 40 %}
|
||||
<span class="status-neutral">Neutral</span>
|
||||
{% elif level >= 20 %}
|
||||
<span class="status-unfriendly">Wary</span>
|
||||
{% else %}
|
||||
<span class="status-unfriendly">Hostile</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Interaction Stats #}
|
||||
<div class="npc-interaction-stats">
|
||||
<div class="interaction-stat">
|
||||
<span class="stat-label">Conversations</span>
|
||||
<span class="stat-value">{{ interaction_count|default(0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Center Column: Conversation #}
|
||||
<div class="npc-conversation">
|
||||
{# Conversation History #}
|
||||
<div class="chat-history" id="chat-history-{{ npc.npc_id }}">
|
||||
{% if conversation_history %}
|
||||
{% for msg in conversation_history %}
|
||||
<div class="chat-message chat-message--{{ msg.speaker }}">
|
||||
<strong>{{ msg.speaker_name }}:</strong> {{ msg.text }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="chat-empty-state">
|
||||
Start a conversation with {{ npc.name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Chat Input #}
|
||||
<form class="chat-input-form"
|
||||
hx-post="{{ url_for('game.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-target="#chat-history-{{ npc.npc_id }}"
|
||||
hx-swap="beforeend">
|
||||
<input type="text"
|
||||
name="player_response"
|
||||
class="chat-input"
|
||||
placeholder="Say something..."
|
||||
autocomplete="off"
|
||||
autofocus>
|
||||
<button type="submit" class="chat-send-btn">Send</button>
|
||||
<button type="button"
|
||||
class="chat-greet-btn"
|
||||
hx-post="{{ url_for('game.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-vals='{"topic": "greeting"}'
|
||||
hx-target="#chat-history-{{ npc.npc_id }}"
|
||||
hx-swap="beforeend">
|
||||
Greet
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Right Column: Message History Sidebar #}
|
||||
<aside class="npc-history-panel htmx-indicator"
|
||||
id="npc-history-{{ npc.npc_id }}"
|
||||
hx-get="{{ url_for('game.npc_chat_history', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-trigger="load, newMessage from:body"
|
||||
hx-swap="innerHTML">
|
||||
{# History loaded via HTMX #}
|
||||
<div class="history-loading">Loading history...</div>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
{#
|
||||
NPC Chat Modal (Expanded)
|
||||
Shows NPC profile with portrait, relationship meter, and conversation interface
|
||||
Uses shared content partial for consistency with dedicated page view
|
||||
#}
|
||||
<div class="modal-overlay" onclick="if(event.target === this) closeModal()">
|
||||
<div class="modal-content modal-content--xl">
|
||||
@@ -10,122 +11,9 @@ Shows NPC profile with portrait, relationship meter, and conversation interface
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
|
||||
{# Modal Body - Three Column Layout #}
|
||||
{# Modal Body - Uses shared content partial #}
|
||||
<div class="modal-body npc-modal-body npc-modal-body--three-col">
|
||||
{# Left Column: NPC Profile #}
|
||||
<div class="npc-profile">
|
||||
{# NPC Portrait #}
|
||||
<div class="npc-portrait">
|
||||
{% if npc.image_url %}
|
||||
<img src="{{ npc.image_url }}" alt="{{ npc.name }}">
|
||||
{% else %}
|
||||
<div class="npc-portrait-placeholder">
|
||||
{{ npc.name[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# NPC Info #}
|
||||
<div class="npc-profile-info">
|
||||
<div class="npc-profile-role">{{ npc.role }}</div>
|
||||
{% if npc.appearance %}
|
||||
<div class="npc-profile-appearance">{{ npc.appearance }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# NPC Tags #}
|
||||
{% if npc.tags %}
|
||||
<div class="npc-profile-tags">
|
||||
{% for tag in npc.tags %}
|
||||
<span class="npc-profile-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Relationship Meter #}
|
||||
<div class="npc-relationship">
|
||||
<div class="relationship-header">
|
||||
<span class="relationship-label">Relationship</span>
|
||||
<span class="relationship-value">{{ relationship_level|default(50) }}/100</span>
|
||||
</div>
|
||||
<div class="relationship-bar">
|
||||
<div class="relationship-fill" style="width: {{ relationship_level|default(50) }}%"></div>
|
||||
</div>
|
||||
<div class="relationship-status">
|
||||
{% set level = relationship_level|default(50) %}
|
||||
{% if level >= 80 %}
|
||||
<span class="status-friendly">Trusted Ally</span>
|
||||
{% elif level >= 60 %}
|
||||
<span class="status-friendly">Friendly</span>
|
||||
{% elif level >= 40 %}
|
||||
<span class="status-neutral">Neutral</span>
|
||||
{% elif level >= 20 %}
|
||||
<span class="status-unfriendly">Wary</span>
|
||||
{% else %}
|
||||
<span class="status-unfriendly">Hostile</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Interaction Stats #}
|
||||
<div class="npc-interaction-stats">
|
||||
<div class="interaction-stat">
|
||||
<span class="stat-label">Conversations</span>
|
||||
<span class="stat-value">{{ interaction_count|default(0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Right Column: Conversation #}
|
||||
<div class="npc-conversation">
|
||||
{# Conversation History #}
|
||||
<div class="chat-history" id="chat-history-{{ npc.npc_id }}">
|
||||
{% if conversation_history %}
|
||||
{% for msg in conversation_history %}
|
||||
<div class="chat-message chat-message--{{ msg.speaker }}">
|
||||
<strong>{{ msg.speaker_name }}:</strong> {{ msg.text }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="chat-empty-state">
|
||||
Start a conversation with {{ npc.name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Chat Input #}
|
||||
<form class="chat-input-form"
|
||||
hx-post="{{ url_for('game.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-target="#chat-history-{{ npc.npc_id }}"
|
||||
hx-swap="beforeend">
|
||||
<input type="text"
|
||||
name="player_response"
|
||||
class="chat-input"
|
||||
placeholder="Say something..."
|
||||
autocomplete="off"
|
||||
autofocus>
|
||||
<button type="submit" class="chat-send-btn">Send</button>
|
||||
<button type="button"
|
||||
class="chat-greet-btn"
|
||||
hx-post="{{ url_for('game.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-vals='{"topic": "greeting"}'
|
||||
hx-target="#chat-history-{{ npc.npc_id }}"
|
||||
hx-swap="beforeend">
|
||||
Greet
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Right Column: Message History Sidebar #}
|
||||
<aside class="npc-history-panel"
|
||||
id="npc-history-{{ npc.npc_id }}"
|
||||
hx-get="{{ url_for('game.npc_chat_history', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-trigger="load, newMessage from:body"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator=".history-loading">
|
||||
{# History loaded via HTMX #}
|
||||
<div class="loading-state-small history-loading">Loading history...</div>
|
||||
</aside>
|
||||
{% include 'game/partials/npc_chat_content.html' %}
|
||||
</div>
|
||||
|
||||
{# Modal Footer #}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{#
|
||||
NPCs Accordion Content
|
||||
Shows NPCs at current location with click to chat
|
||||
Uses responsive navigation: modals on desktop, full pages on mobile
|
||||
#}
|
||||
{% if npcs %}
|
||||
<div class="npc-list">
|
||||
{% for npc in npcs %}
|
||||
<div class="npc-item"
|
||||
hx-get="{{ url_for('game.npc_chat_modal', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML">
|
||||
data-npc-id="{{ npc.npc_id }}"
|
||||
onclick="navigateResponsive(event, '{{ url_for('game.npc_chat_page', session_id=session_id, npc_id=npc.npc_id) }}', '{{ url_for('game.npc_chat_modal', session_id=session_id, npc_id=npc.npc_id) }}')"
|
||||
style="cursor: pointer;">
|
||||
<div class="npc-name">{{ npc.name }}</div>
|
||||
<div class="npc-role">{{ npc.role }}</div>
|
||||
<div class="npc-appearance">{{ npc.appearance }}</div>
|
||||
|
||||
@@ -149,4 +149,7 @@ document.addEventListener('keydown', function(e) {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Responsive Modal Navigation -->
|
||||
<script src="{{ url_for('static', filename='js/responsive-modals.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user