From 69cab163822f4afad6194e9e9179f57ffae7f60c Mon Sep 17 00:00:00 2001 From: StarFleetCPTN Date: Sat, 8 Mar 2025 00:03:28 -0800 Subject: [PATCH] Add mobile-first responsive design and PWA support - Implement mobile navigation with bottom nav and hamburger menu - Add mobile-specific meta tags for PWA and iOS compatibility - Enhance CSS for mobile responsiveness and safe area insets - Improve JavaScript for mobile UX with responsive tables and form enhancements - Configure Air.toml for development workflow --- air.toml | 38 ++++++++++++ components/layout.templ | 97 ++++++++++++++++++++++++++++- static/css/app.css | 131 ++++++++++++++++++++++++++++++++++++++++ static/js/app.js | 92 ++++++++++++++++++++++++++++ 4 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 air.toml diff --git a/air.toml b/air.toml new file mode 100644 index 0000000..77b3a6b --- /dev/null +++ b/air.toml @@ -0,0 +1,38 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] +args_bin = [] +bin = "./tmp/main" +cmd = "templ generate && go build -o ./tmp/main ." +delay = 1000 +exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"] +exclude_file = [] +exclude_regex = ["_test.go", "_templ.go"] +exclude_unchanged = false +follow_symlink = false +full_bin = "" +include_dir = [] +include_ext = ["go", "tpl", "tmpl", "templ", "html"] +kill_delay = "0s" +log = "build-errors.log" +send_interrupt = false +stop_on_error = true + +[color] +app = "" +build = "yellow" +main = "magenta" +runner = "green" +watcher = "cyan" + +[log] +time = false + +[misc] +clean_on_exit = false + +[screen] +clear_on_rebuild = false +keep_scroll = true \ No newline at end of file diff --git a/components/layout.templ b/components/layout.templ index 898b1e8..c9d96f3 100644 --- a/components/layout.templ +++ b/components/layout.templ @@ -32,7 +32,10 @@ templ LayoutWithContext(title string, ctx context.Context) { - + + + + @@ -283,6 +286,18 @@ templ LayoutWithContext(title string, ctx context.Context) { + +
+ +
+
+ + +
+ +
} -
+
{ children... }
@@ -326,6 +386,39 @@ templ LayoutWithContext(title string, ctx context.Context) {

+ + if isLoggedIn(ctx) { + + + } } diff --git a/static/css/app.css b/static/css/app.css index 86c003c..5cdb1d7 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -20,6 +20,8 @@ body { line-height: 1.6; color: var(--text-color); background-color: var(--background-color); + /* Prevent content from going under bottom nav on mobile */ + padding-bottom: env(safe-area-inset-bottom); } /* Layout */ @@ -223,8 +225,21 @@ body { border-top: 1px solid #e2e8f0; } +/* Mobile Bottom Navigation */ +.mobile-bottom-nav { + display: none; +} + +.pb-mobile-nav { + padding-bottom: 1rem; +} + /* Responsive */ @media (max-width: 768px) { + .pb-mobile-nav { + padding-bottom: 5rem !important; /* Account for bottom nav */ + } + .grid-cols-2, .grid-cols-3 { grid-template-columns: 1fr; @@ -234,4 +249,120 @@ body { flex-direction: column; align-items: flex-start; } + + /* Adjust cards for mobile */ + .card { + padding: 1rem; + margin-bottom: 0.75rem; + } + + .card-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + /* Table adjustments for mobile */ + .table-responsive { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + /* Adjust forms for mobile */ + .form-label { + font-size: 0.875rem; + } + + .form-input { + font-size: 16px; /* Prevents zoom on iOS */ + padding: 0.625rem; + } + + /* Touch-friendly buttons */ + .button { + padding: 0.625rem 1rem; + min-height: 44px; /* Better touch target */ + } + + /* Mobile spacing */ + .container { + padding: 0.75rem; + } + + .px-4 { + padding-left: 0.75rem; + padding-right: 0.75rem; + } + + .py-4 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + } +} + +/* Small phones */ +@media (max-width: 480px) { + .card-body { + padding: 0.75rem; + } + + .card-title { + font-size: 1.125rem; + } + + .hidden-xs { + display: none; + } + + /* Stack buttons on small screens */ + .button-group { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + /* Ensure safe area at bottom of the page */ + body { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); + } +} + +/* Support for notch phones */ +@supports (padding: max(0px)) { + body, + .container, + .navbar { + padding-left: max(1rem, env(safe-area-inset-left)); + padding-right: max(1rem, env(safe-area-inset-right)); + } + + .mobile-nav-container { + padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); + } +} + +/* Dark mode support for mobile */ +@media (prefers-color-scheme: dark) { + .mobile-bottom-nav { + background-color: #1e293b; + border-top-color: #334155; + } +} + +/* Improve form elements on mobile */ +@media (hover: none) and (pointer: coarse) { + input[type="checkbox"], + input[type="radio"], + button, + .button, + select { + min-height: 44px; + min-width: 44px; + } + + input, select, textarea { + font-size: 16px !important; /* Prevents zoom on focus in iOS */ + } } \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index 3560829..f30ab55 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,6 +1,7 @@ // Theme management for GoMFT application document.addEventListener('DOMContentLoaded', function() { initializeTheme(); + initializeMobileSupport(); }); // Initialize theme based on user preference @@ -242,3 +243,94 @@ style.textContent = ` } `; document.head.appendChild(style); + +// Initialize mobile support features +function initializeMobileSupport() { + // Check if it's a mobile device + const isMobile = window.matchMedia('(max-width: 768px)').matches; + + if (isMobile) { + // Add padding to main content to prevent overlap with bottom nav + const bottomNav = document.querySelector('.mobile-nav-container'); + if (bottomNav) { + const main = document.querySelector('main'); + if (main) { + main.style.paddingBottom = (bottomNav.offsetHeight + 16) + 'px'; + } + } + + // Add active class to current page in bottom nav + highlightCurrentPageInBottomNav(); + + // Make tables scrollable on mobile + makeTablesResponsive(); + + // Improve mobile form experience + enhanceMobileForms(); + } + + // Listen for orientation changes + window.addEventListener('orientationchange', function() { + // Wait for orientation change to complete + setTimeout(function() { + if (window.matchMedia('(max-width: 768px)').matches) { + makeTablesResponsive(); + } + }, 300); + }); +} + +// Highlight current page in mobile bottom navigation +function highlightCurrentPageInBottomNav() { + const currentPath = window.location.pathname; + const bottomNavLinks = document.querySelectorAll('.mobile-nav-container a'); + + bottomNavLinks.forEach(link => { + const href = link.getAttribute('href'); + if (currentPath === href || (href !== '/' && currentPath.startsWith(href))) { + link.classList.add('text-primary-600', 'dark:text-primary-400'); + link.classList.remove('text-secondary-500', 'dark:text-secondary-400'); + } + }); +} + +// Make tables responsive on mobile +function makeTablesResponsive() { + const tables = document.querySelectorAll('table'); + tables.forEach(table => { + if (!table.parentElement.classList.contains('table-responsive')) { + const wrapper = document.createElement('div'); + wrapper.classList.add('table-responsive'); + table.parentNode.insertBefore(wrapper, table); + wrapper.appendChild(table); + } + }); +} + +// Enhance mobile forms +function enhanceMobileForms() { + // Prevent zooming on inputs in iOS + const metaViewport = document.querySelector('meta[name=viewport]'); + if (metaViewport) { + metaViewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'); + } + + // Add 'required' visual indicator to required fields + const requiredInputs = document.querySelectorAll('input[required], select[required], textarea[required]'); + requiredInputs.forEach(input => { + const label = input.previousElementSibling; + if (label && label.tagName === 'LABEL') { + if (!label.innerHTML.includes('*')) { + label.innerHTML += ' *'; + } + } + }); + + // Improve date input on mobile + const dateInputs = document.querySelectorAll('input[type="date"]'); + dateInputs.forEach(input => { + input.addEventListener('focus', function() { + input.click(); // Force native date picker on mobile + }); + }); +}