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:
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>
|
||||
|
||||
Reference in New Issue
Block a user