mobile: off-canvas sidebar, responsive grids/charts/top-bar
- Sidebar becomes slide-in overlay under 768px with backdrop - Hamburger button in top-bar for mobile nav toggle - Grids stack single-column, charts shrink to 280px - Tables scroll horizontally with touch momentum - Status text hidden on mobile (dot stays visible) - Card headers/actions stack vertically - Period selectors scroll horizontally also: image size validation clamp in handleImageGenerations
This commit is contained in:
@@ -850,6 +850,31 @@ func (s *Server) handleImageGenerations(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ponytail: per-model valid size sets. Add new models here.
|
||||||
|
if req.Size != nil {
|
||||||
|
validSizes := map[string][]string{
|
||||||
|
"gpt-image": {"1024x1024", "1024x1536", "1536x1024", "auto"},
|
||||||
|
"dall-e-3": {"1024x1024", "1024x1792", "1792x1024", "auto"},
|
||||||
|
"dall-e-2": {"256x256", "512x512", "1024x1024", "auto"},
|
||||||
|
}
|
||||||
|
for prefix, sizes := range validSizes {
|
||||||
|
if strings.HasPrefix(req.Model, prefix) {
|
||||||
|
valid := false
|
||||||
|
for _, s := range sizes {
|
||||||
|
if *req.Size == s {
|
||||||
|
valid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
*req.Size = "1024x1024"
|
||||||
|
log.Printf("[WARN] Unsupported size for %s, clamped to 1024x1024", req.Model)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
provider, ok := s.providers[providerName]
|
provider, ok := s.providers[providerName]
|
||||||
if !ok {
|
if !ok {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Provider %s not enabled or supported", providerName)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Provider %s not enabled or supported", providerName)})
|
||||||
|
|||||||
+143
-1
@@ -1395,8 +1395,150 @@ body {
|
|||||||
border: 1px solid var(--bg2);
|
border: 1px solid var(--bg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings: Warning Card */
|
/* Warning Card */
|
||||||
.warning-card {
|
.warning-card {
|
||||||
border: 1px dashed var(--warning);
|
border: 1px dashed var(--warning);
|
||||||
background: rgba(215, 153, 33, 0.08);
|
background: rgba(215, 153, 33, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== MOBILE BAREBONES ==================== */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
width: 280px;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
.sidebar.mobile-visible {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
.sidebar.collapsed {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
.sidebar.collapsed .menu-item span,
|
||||||
|
.sidebar.collapsed .menu-title,
|
||||||
|
.sidebar.collapsed .user-details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.sidebar.collapsed .menu-item {
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0.75rem var(--spacing-lg);
|
||||||
|
}
|
||||||
|
.sidebar.collapsed .menu-item i {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.sidebar.collapsed .sidebar-footer {
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
z-index: 1999;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.sidebar-backdrop.visible {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg1);
|
||||||
|
border: 1px solid var(--bg3);
|
||||||
|
color: var(--fg1);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.mobile-menu-btn:active {
|
||||||
|
background: var(--bg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.grid-2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.grid-3 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-body {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
.top-bar {
|
||||||
|
padding: 0 var(--spacing-md);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.top-bar .page-title h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.top-bar-actions {
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator .status-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitoring-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
padding: 2rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-selector {
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.mobile-menu-btn { display: inline-flex; }
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.mobile-menu-btn { display: none; }
|
||||||
|
.sidebar-backdrop { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ponytail: single-breakpoint responsive layer, no card-based table fallback
|
||||||
|
add table→card reflow when tables get too wide to scroll horizontally */
|
||||||
|
|||||||
+9
-3
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>GopherGate - Admin Dashboard</title>
|
<title>GopherGate - Admin Dashboard</title>
|
||||||
<link rel="stylesheet" href="/css/dashboard.css?v=11">
|
<link rel="stylesheet" href="/css/dashboard.css?v=12">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<span>GopherGate</span>
|
<span>GopherGate</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||||
<i class="fas fa-bars"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,11 +135,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Mobile sidebar backdrop -->
|
||||||
|
<div class="sidebar-backdrop" id="sidebar-backdrop"></div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<header class="top-bar">
|
<header class="top-bar">
|
||||||
|
<button class="mobile-menu-btn" id="mobile-menu-btn">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
<h2 id="current-page-title">Overview</h2>
|
<h2 id="page-title">Overview</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-bar-actions">
|
<div class="top-bar-actions">
|
||||||
<div id="connection-status" class="status-indicator">
|
<div id="connection-status" class="status-indicator">
|
||||||
|
|||||||
+57
-5
@@ -60,23 +60,75 @@ class Dashboard {
|
|||||||
const toggleBtn = document.getElementById('sidebar-toggle');
|
const toggleBtn = document.getElementById('sidebar-toggle');
|
||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector('.sidebar');
|
||||||
const logoutBtn = document.getElementById('logout-btn');
|
const logoutBtn = document.getElementById('logout-btn');
|
||||||
|
const backdrop = document.getElementById('sidebar-backdrop');
|
||||||
|
const mobileBtn = document.getElementById('mobile-menu-btn');
|
||||||
|
|
||||||
|
const isMobile = () => window.innerWidth < 768;
|
||||||
|
|
||||||
|
const closeMobileNav = () => {
|
||||||
|
if (sidebar) sidebar.classList.remove('mobile-visible');
|
||||||
|
if (backdrop) backdrop.classList.remove('visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMobileNav = () => {
|
||||||
|
if (!sidebar || !backdrop) return;
|
||||||
|
const opening = !sidebar.classList.contains('mobile-visible');
|
||||||
|
sidebar.classList.toggle('mobile-visible', opening);
|
||||||
|
backdrop.classList.toggle('visible', opening);
|
||||||
|
document.body.style.overflow = opening && isMobile() ? 'hidden' : '';
|
||||||
|
};
|
||||||
|
|
||||||
if (toggleBtn && sidebar) {
|
if (toggleBtn && sidebar) {
|
||||||
toggleBtn.onclick = () => {
|
toggleBtn.onclick = () => {
|
||||||
sidebar.classList.toggle('collapsed');
|
if (isMobile()) {
|
||||||
localStorage.setItem('sidebar_collapsed', sidebar.classList.contains('collapsed'));
|
toggleMobileNav();
|
||||||
|
} else {
|
||||||
|
sidebar.classList.toggle('collapsed');
|
||||||
|
localStorage.setItem('sidebar_collapsed', sidebar.classList.contains('collapsed'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (localStorage.getItem('sidebar_collapsed') === 'true') {
|
if (!isMobile() && localStorage.getItem('sidebar_collapsed') === 'true') {
|
||||||
sidebar.classList.add('collapsed');
|
sidebar.classList.add('collapsed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mobileBtn) {
|
||||||
|
mobileBtn.onclick = toggleMobileNav;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.onclick = closeMobileNav;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close sidebar on page navigation (mobile)
|
||||||
|
const menuItems = document.querySelectorAll('.menu-item');
|
||||||
|
menuItems.forEach(item => {
|
||||||
|
const origClick = item.onclick;
|
||||||
|
item.onclick = (e) => {
|
||||||
|
if (isMobile()) closeMobileNav();
|
||||||
|
if (origClick) origClick(e);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (logoutBtn) {
|
if (logoutBtn) {
|
||||||
logoutBtn.onclick = () => {
|
logoutBtn.onclick = () => {
|
||||||
|
if (isMobile()) closeMobileNav();
|
||||||
window.authManager.logout();
|
window.authManager.logout();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle resize
|
||||||
|
let resizeTimer;
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(() => {
|
||||||
|
if (!isMobile()) {
|
||||||
|
closeMobileNav();
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupRefresh() {
|
setupRefresh() {
|
||||||
|
|||||||
Reference in New Issue
Block a user