From b2ac3dcbee3c44ae9cf3fa2f0caa7b1d754b2225 Mon Sep 17 00:00:00 2001 From: TheFlow Date: Thu, 16 Oct 2025 14:59:58 +1300 Subject: [PATCH] feat: implement responsive mobile language selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mobile UX Improvements: - Replace dropdown with icon-only flags on mobile (< 768px) - Add 44x44px touch targets for better mobile interaction - Add language selector to mobile menu drawer - Desktop keeps full dropdown with language names (≥ 768px) Language Selector Features: - Mobile navbar: Icon-only buttons (🇬🇧 🇩🇪 🇫🇷) - Desktop navbar: Dropdown with full text - Mobile drawer: Full language list with checkmarks - Active state: Blue ring around selected language - Auto-close drawer after language selection Accessibility: - ARIA labels on all buttons - aria-pressed state for current language - Minimum 44x44px touch targets (WCAG AA) - Keyboard navigation support maintained - Screen reader support with role="group" Technical Changes: - language-selector.js: Rewritten with responsive versions - navbar.js: Added mobile-menu-language-selector container - i18n-simple.js: Added languageChanged event dispatch UX Benefits: - Space savings: ~87px saved in mobile navbar - No crowding between language selector and hamburger menu - Flag emojis are universally recognizable - Touch-friendly buttons meet iOS/Android standards 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- public/js/components/language-selector.js | 198 +++++++++++++++++++--- public/js/components/navbar.js | 3 + public/js/i18n-simple.js | 7 +- 3 files changed, 183 insertions(+), 25 deletions(-) diff --git a/public/js/components/language-selector.js b/public/js/components/language-selector.js index 7fda9f1e..33474c80 100644 --- a/public/js/components/language-selector.js +++ b/public/js/components/language-selector.js @@ -1,6 +1,7 @@ /** * Language Selector Component - * Creates a dropdown for switching between supported languages + * Mobile: Icon-only buttons (flags) + * Desktop: Dropdown with full language names */ (function() { @@ -10,24 +11,22 @@ { code: 'fr', name: 'Français', flag: '🇫🇷' }, { code: 'mi', name: 'Te Reo Māori', flag: '🇳🇿', disabled: true, tooltip: 'Coming when system is complete' } ]; - - function createLanguageSelector() { - const container = document.getElementById('language-selector-container'); - if (!container) return; - - const currentLang = (window.I18n && window.I18n.currentLang) || 'en'; - - const selectorHTML = ` -
- - ${supportedLanguages.map(lang => ` -
`; - + } + + /** + * Create mobile icon-only selector + */ + function createMobileSelector(currentLang) { + return ` +
+ ${supportedLanguages.filter(lang => !lang.disabled).map(lang => ` + + `).join('')} +
+ `; + } + + /** + * Create mobile menu language selector (inside drawer) + */ + function createMobileMenuSelector(currentLang) { + const container = document.getElementById('mobile-menu-language-selector'); + if (!container) return; + + const selectorHTML = ` +
+

Language

+
+ ${supportedLanguages.map(lang => ` + + `).join('')} +
+
+ `; + container.innerHTML = selectorHTML; - - // Add change event listener - const selector = document.getElementById('language-selector'); - if (selector && window.I18n) { - selector.addEventListener('change', function(e) { + + // Attach event listeners for mobile menu buttons + document.querySelectorAll('.mobile-menu-lang-btn').forEach(btn => { + if (!btn.disabled) { + btn.addEventListener('click', function(e) { + const selectedLang = this.getAttribute('data-lang'); + if (window.I18n && selectedLang) { + window.I18n.setLanguage(selectedLang); + // Close mobile menu after selection + const mobileMenu = document.getElementById('mobile-menu'); + if (mobileMenu) { + mobileMenu.classList.add('hidden'); + document.body.style.overflow = ''; + } + } + }); + } + }); + } + + /** + * Initialize language selector + */ + function createLanguageSelector() { + const container = document.getElementById('language-selector-container'); + if (!container) return; + + const currentLang = (window.I18n && window.I18n.currentLang) || 'en'; + + // Create both mobile and desktop versions + const combinedHTML = createMobileSelector(currentLang) + createDesktopSelector(currentLang); + container.innerHTML = combinedHTML; + + // Attach event listener for desktop dropdown + const desktopDropdown = document.getElementById('language-selector-dropdown'); + if (desktopDropdown && window.I18n) { + desktopDropdown.addEventListener('change', function(e) { const selectedLang = e.target.value; window.I18n.setLanguage(selectedLang); }); } + + // Attach event listeners for mobile icon buttons + document.querySelectorAll('.language-btn').forEach(btn => { + btn.addEventListener('click', function(e) { + const selectedLang = this.getAttribute('data-lang'); + if (window.I18n && selectedLang) { + window.I18n.setLanguage(selectedLang); + } + }); + }); + + // Initialize mobile menu language selector (if container exists) + createMobileMenuSelector(currentLang); } - + + /** + * Update UI when language changes + */ + function updateLanguageUI(newLang) { + // Update mobile icon buttons + document.querySelectorAll('.language-btn').forEach(btn => { + const lang = btn.getAttribute('data-lang'); + if (lang === newLang) { + btn.classList.add('bg-blue-100', 'ring-2', 'ring-blue-500'); + btn.classList.remove('bg-white', 'border', 'border-gray-300'); + btn.setAttribute('aria-pressed', 'true'); + } else { + btn.classList.remove('bg-blue-100', 'ring-2', 'ring-blue-500'); + btn.classList.add('bg-white', 'border', 'border-gray-300'); + btn.setAttribute('aria-pressed', 'false'); + } + }); + + // Update desktop dropdown + const dropdown = document.getElementById('language-selector-dropdown'); + if (dropdown) { + dropdown.value = newLang; + } + + // Update mobile menu buttons + document.querySelectorAll('.mobile-menu-lang-btn').forEach(btn => { + const lang = btn.getAttribute('data-lang'); + if (lang === newLang) { + btn.classList.add('bg-blue-50', 'text-blue-700'); + btn.classList.remove('text-gray-700', 'hover:bg-gray-50'); + } else { + btn.classList.remove('bg-blue-50', 'text-blue-700'); + btn.classList.add('text-gray-700', 'hover:bg-gray-50'); + } + }); + } + + // Listen for language change events + window.addEventListener('languageChanged', function(e) { + if (e.detail && e.detail.language) { + updateLanguageUI(e.detail.language); + } + }); + // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createLanguageSelector); } else { createLanguageSelector(); } + + // Re-initialize when mobile menu is rendered (if needed) + window.addEventListener('mobileMenuRendered', createLanguageSelector); + })(); diff --git a/public/js/components/navbar.js b/public/js/components/navbar.js index b00e700f..2db5d175 100644 --- a/public/js/components/navbar.js +++ b/public/js/components/navbar.js @@ -71,6 +71,9 @@ class TractatusNavbar {