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... }
+
+ 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
+ });
+ });
+}