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:
2025-11-25 21:30:51 -06:00
parent 196346165f
commit 2419dbeb34
9 changed files with 861 additions and 119 deletions

View File

@@ -0,0 +1,81 @@
/**
* Responsive Modal Navigation
*
* Provides smart navigation that uses modals on desktop (>1024px)
* and full page navigation on mobile (<=1024px) for better UX.
*
* Usage:
* Instead of using hx-get directly on elements, use:
* onclick="navigateResponsive(event, '/page/url', '/modal/url')"
*/
// Breakpoint for mobile vs desktop (matches CSS @media query)
const MOBILE_BREAKPOINT = 1024;
/**
* Check if current viewport is mobile size
*/
function isMobile() {
return window.innerWidth <= MOBILE_BREAKPOINT;
}
/**
* Navigate responsively based on screen size
*
* @param {Event} event - Click event (will be prevented)
* @param {string} pageUrl - Full page URL for mobile
* @param {string} modalUrl - Modal content URL for desktop (HTMX)
*/
function navigateResponsive(event, pageUrl, modalUrl) {
event.preventDefault();
event.stopPropagation();
if (isMobile()) {
// Mobile: Navigate to full page
window.location.href = pageUrl;
} else {
// Desktop: Load modal via HTMX
const target = event.currentTarget;
// Trigger HTMX request programmatically
htmx.ajax('GET', modalUrl, {
target: '#modal-container',
swap: 'innerHTML'
});
}
}
/**
* Setup responsive navigation for NPC items
* Call this after NPCs are loaded
*/
function setupNPCResponsiveNav(sessionId) {
document.querySelectorAll('.npc-item').forEach(item => {
const npcId = item.getAttribute('data-npc-id');
if (!npcId) return;
const pageUrl = `/game/session/${sessionId}/npc/${npcId}`;
const modalUrl = `/game/session/${sessionId}/npc/${npcId}/chat`;
// Remove HTMX attributes and add responsive navigation
item.removeAttribute('hx-get');
item.removeAttribute('hx-target');
item.removeAttribute('hx-swap');
item.style.cursor = 'pointer';
item.onclick = (e) => navigateResponsive(e, pageUrl, modalUrl);
});
}
/**
* Handle window resize to adapt navigation behavior
* Debounced to avoid excessive calls
*/
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
// Re-setup navigation if needed
console.log('Viewport resized:', isMobile() ? 'Mobile' : 'Desktop');
}, 250);
});