Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 63 additions & 71 deletions internal/http/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@
:root {
--bg-elevated: var(--surface);
--bg-card: var(--surface);
--bg-glass: color-mix(in srgb, var(--surface) 85%, transparent);
--border: var(--line);
--border-hover: var(--line-strong);
--border-focus: var(--accent);
Expand Down Expand Up @@ -259,77 +258,86 @@ hgroup { margin-bottom: 1.2em; }
hgroup h1 { margin-bottom: 0.15em; }
hgroup p { color: var(--text-muted); margin: 0; font-size: 0.95em; }

/* Sticky Glass Navbar */
/* Shell: sticky header (56px) + main nav (bottom tab bar < 900px, tab row under the header >= 900px) */
.top-nav {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
gap: 1.5em;
padding: 0.75em 1.75em;
border-bottom: 1px solid var(--border);
background: var(--bg-glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
transition: background-color 0.2s, border-color 0.2s;
}
.top-nav .logo {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
font-size: 1.1em;
color: var(--text);
letter-spacing: -0.02em;
background: var(--surface);
border-bottom: 1px solid var(--line);
}
.top-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; height: 56px; padding: 0 16px; }
.top-actions { display: flex; align-items: center; gap: 8px; }
.top-actions form { margin: 0; }
.top-nav .logo { display: flex; align-items: center; gap: 8px; font-weight: 800; font-size: 17px; color: var(--ink); letter-spacing: -0.02em; }
.top-nav .logo:hover { color: var(--accent); }
.top-nav .logo-icon {
width: 24px;
height: 24px;
border-radius: 6px;
width: 28px;
height: 28px;
border-radius: 8px;
background: linear-gradient(135deg, var(--accent-fill), var(--chart));
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--on-accent);
font-size: 0.8em;
font-size: 14px;
font-weight: 800;
box-shadow: var(--shadow);
}
.top-nav .logo:hover { color: var(--accent); }
.top-nav .nav-links { display: flex; gap: 0.35em; margin-left: auto; align-items: center; }
.top-nav .nav-links a {
padding: 0.4em 0.8em;
border-radius: 6px;
font-size: 0.9em;
font-weight: 500;
color: var(--text-muted);
transition: all 0.15s ease;
.main-nav {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
height: calc(56px + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
background: var(--surface);
border-top: 1px solid var(--line);
}
.top-nav .nav-links a:hover { color: var(--text); background: var(--accent-light); }
.top-nav .nav-links a.active { color: var(--accent); background: var(--accent-light); font-weight: 600; }

/* Theme toggle button */

/* Mobile Menu Toggle */
.mobile-nav-toggle {
display: none;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.4em;
color: var(--text);
cursor: pointer;
.main-nav a {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
border-top: 2px solid transparent;
margin-top: -1px;
font-size: 11px;
font-weight: 600;
color: var(--muted);
}
.main-nav a:hover { color: var(--ink); }
.main-nav a[aria-current=page] { color: var(--accent); border-top-color: var(--accent); }
.main-nav .icon { width: 20px; height: 20px; }
body:not(.login-page) { padding-bottom: calc(56px + env(safe-area-inset-bottom)); }
@media (min-width: 900px) {
body:not(.login-page) { padding-bottom: 0; }
.top-bar, .main-nav { max-width: 1100px; margin: 0 auto; }
.top-bar { padding: 0 24px; }
.main-nav {
position: static;
display: flex;
gap: 4px;
height: 44px;
padding: 0 24px;
background: none;
border-top: 0;
}
.main-nav a { flex-direction: row; gap: 8px; border-top: 0; border-bottom: 2px solid transparent; margin: 0; padding: 0 12px; font-size: 14px; }
.main-nav a[aria-current=page] { border-bottom-color: var(--accent); }
.main-nav .icon { width: 16px; height: 16px; }
}

/* Container */
.container { max-width: 1040px; margin: 0 auto; padding: 1.75em 1.5em; }
.container { max-width: 1100px; margin: 0 auto; padding: 16px; }
@media (min-width: 900px) { .container { padding: 24px; } }

/* Cards & Elevation */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
border-radius: 16px;
padding: 1.25em 1.4em;
margin-bottom: 1em;
box-shadow: var(--shadow-sm);
Expand Down Expand Up @@ -486,7 +494,7 @@ input, textarea, select {
font-family: inherit;
font-size: 0.92em;
padding: 0.55em 0.8em;
border-radius: 6px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
Expand Down Expand Up @@ -514,6 +522,8 @@ legend {
}
small { font-size: 0.82em; color: var(--text-dim); display: block; margin-top: 0.35em; }
.grid label { margin-bottom: 0; }
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 700px) { .kpi-grid { grid-template-columns: repeat(4, 1fr); } }

/* Status Badges with Pulse Effects */
@keyframes pulse-ring {
Expand Down Expand Up @@ -722,26 +732,8 @@ pre {

/* Responsive Media Queries */
@media (max-width: 680px) {
.container { padding: 1em; }
.grid { grid-template-columns: 1fr; }
.grid:not(.kpi-grid) { grid-template-columns: 1fr; }
.grid-2 { grid-template-columns: 1fr; }
.top-nav { padding: 0.6em 1em; }
.mobile-nav-toggle { display: block; }
.top-nav .nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
background: var(--bg-elevated);
border-bottom: 1px solid var(--border);
padding: 0.75em 1em;
gap: 0.4em;
box-shadow: var(--shadow-lg);
}
.top-nav .nav-links.open { display: flex; }
.top-nav .nav-links a { width: 100%; padding: 0.6em 0.8em; }
.stats-gauges { grid-template-columns: 1fr 1fr; }
.btn-group { flex-direction: column; align-items: stretch; }
.btn-group-left, .btn-group-right { width: 100%; }
Expand Down Expand Up @@ -807,4 +799,4 @@ pre {
}
.login-card input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.login-card .btn-primary { width: 100%; text-align: center; justify-content: center; }
.login-page .theme-btn { position: fixed; top: 1.2em; right: 1.2em; }
.login-page .theme-btn { position: fixed; top: 12px; right: 12px; }
15 changes: 15 additions & 0 deletions internal/http/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,20 @@ func GetDevMock(r *http.Request) bool {
// assetVersion busts the browser cache of /static/app.css on upgrade; set by NewRouter.
var assetVersion string

// navSection maps the request path to the active main-nav item.
func navSection(r *http.Request) string {
p := "/" + strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, strings.TrimSuffix(web.GetBasePath(r), "/")), "/")
switch {
case strings.HasPrefix(p, "/servers"):
return "servers"
case strings.HasPrefix(p, "/apps"):
return "apps"
case strings.HasPrefix(p, "/settings"), strings.HasPrefix(p, "/about"):
return "settings"
}
return "dashboard"
}

type RenderFunc func(w http.ResponseWriter, r *http.Request, status int, name string, data interface{})

func Render(w http.ResponseWriter, r *http.Request, status int, name string, data interface{}) {
Expand All @@ -258,6 +272,7 @@ func Render(w http.ResponseWriter, r *http.Request, status int, name string, dat
m["BasePath"] = web.GetBasePath(r)
m["DevMock"] = GetDevMock(r)
m["AssetVersion"] = assetVersion
m["Nav"] = navSection(r)
}

var buf bytes.Buffer
Expand Down
2 changes: 1 addition & 1 deletion internal/http/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h1>Dashboard</h1>
</hgroup>

{{if .Stats}}
<div class="grid" style="margin-bottom: 1.5em;">
<div class="grid kpi-grid" style="margin-bottom: 1.5em;">
<div class="card stat">
<h2>{{.Stats.TotalServers}}</h2>
<small>Servers</small>
Expand Down
54 changes: 14 additions & 40 deletions internal/http/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,52 +58,26 @@

{{define "nav"}}
{{if .DevMock}}<div class="dev-mock-bar">⚠ Dev Mock Mode — All SSH operations are simulated</div>{{end}}
<nav class="top-nav">
<a href="." class="logo">
<span class="logo-icon">D</span>
Dockify
</a>
<button class="mobile-nav-toggle" onclick="toggleMobileNav()" aria-label="Toggle navigation">{{template "ui_icon" "menu"}}</button>
<div class="nav-links" id="nav-links">
<a href="." id="nav-dashboard">Dashboard</a>
<a href="servers" id="nav-servers">Servers</a>
<a href="apps" id="nav-apps">Apps</a>
<a href="settings" id="nav-settings">Settings</a>
<a href="about" id="nav-about">About</a>
<button class="btn btn-secondary btn-icon theme-btn" onclick="dockifyCycleTheme()" id="theme-toggle" title="Theme" aria-label="Theme"><span data-theme-icon="light" hidden>{{template "ui_icon" "sun"}}</span><span data-theme-icon="dark" hidden>{{template "ui_icon" "moon"}}</span><span data-theme-icon="system">{{template "ui_icon" "monitor"}}</span></button>
<header class="top-nav">
<div class="top-bar">
<a href="." class="logo"><span class="logo-icon">D</span>Dockify</a>
<div class="top-actions">
<button class="btn btn-secondary btn-icon theme-btn" onclick="dockifyCycleTheme()" id="theme-toggle" title="Theme" aria-label="Theme"><span data-theme-icon="light" hidden>{{template "ui_icon" "sun"}}</span><span data-theme-icon="dark" hidden>{{template "ui_icon" "moon"}}</span><span data-theme-icon="system">{{template "ui_icon" "monitor"}}</span></button>
<form method="POST" action="logout"><button type="submit" class="btn btn-secondary btn-icon" aria-label="Sign out" title="Sign out">{{template "ui_icon" "logout"}}</button></form>
</div>
</div>
</nav>
<nav class="main-nav" aria-label="Main">
<a href="."{{if eq .Nav "dashboard"}} aria-current="page"{{end}}>{{template "ui_icon" "layout-dashboard"}}<span>Dashboard</span></a>
<a href="servers"{{if eq .Nav "servers"}} aria-current="page"{{end}}>{{template "ui_icon" "server"}}<span>Servers</span></a>
<a href="apps"{{if eq .Nav "apps"}} aria-current="page"{{end}}>{{template "ui_icon" "box"}}<span>Apps</span></a>
<a href="settings"{{if eq .Nav "settings"}} aria-current="page"{{end}}>{{template "ui_icon" "settings"}}<span>Settings</span></a>
</nav>
</header>
{{end}}

{{define "scripts"}}
<script>
(function() {
// Highlight active nav link based on current path
var path = window.location.pathname;
var links = {
'servers': 'nav-servers',
'apps': 'nav-apps',
'settings': 'nav-settings',
'about': 'nav-about'
};
var matched = false;
for (var k in links) {
if (path.indexOf('/' + k) !== -1 || path === k) {
var el = document.getElementById(links[k]);
if (el) { el.classList.add('active'); matched = true; }
break;
}
}
if (!matched) {
var dash = document.getElementById('nav-dashboard');
if (dash) dash.classList.add('active');
}
})();

function toggleMobileNav() {
var nav = document.getElementById('nav-links');
if (nav) nav.classList.toggle('open');
}

document.querySelectorAll('time[datetime]').forEach(function(el){
var d = new Date(el.getAttribute('datetime'));
Expand Down
1 change: 1 addition & 0 deletions internal/http/templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ <h3>Import</h3>
</p>
<a href="import" class="btn btn-secondary">Go to Import Page</a>
</div>
<p><a href="about" class="btn btn-link">About Dockify</a></p>
</div>
{{template "ui_confirm" (dict "ID" "roll-confirm" "Title" "Roll webhook secret?" "Body" "The old secret stops working immediately. Update your CI secrets right after." "Label" "Roll secret")}}
<script>
Expand Down
2 changes: 1 addition & 1 deletion internal/http/templates/ui_icon.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{define "ui_icon"}}<svg class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{{if eq . "sun"}}<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>{{else if eq . "moon"}}<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>{{else if eq . "monitor"}}<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>{{else if eq . "logout"}}<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5M21 12H9"/>{{else if eq . "menu"}}<path d="M3 6h18M3 12h18M3 18h18"/>{{else if eq . "x"}}<path d="M18 6L6 18M6 6l12 12"/>{{else if eq . "check"}}<path d="M20 6L9 17l-5-5"/>{{else if eq . "alert"}}<path d="M10.3 3.9L1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/>{{else if eq . "refresh"}}<path d="M21 12a9 9 0 0 1-15.5 6.2L3 16"/><path d="M3 21v-5h5"/><path d="M3 12a9 9 0 0 1 15.5-6.2L21 8"/><path d="M21 3v5h-5"/>{{else if eq . "plus"}}<path d="M12 5v14M5 12h14"/>{{else if eq . "trash"}}<path d="M3 6h18"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>{{else if eq . "external"}}<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6M10 14L21 3"/>{{else if eq . "chevron-down"}}<path d="M6 9l6 6 6-6"/>{{else if eq . "chevron-right"}}<path d="M9 6l6 6-6 6"/>{{else if eq . "search"}}<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.3-4.3"/>{{end}}</svg>{{end}}
{{define "ui_icon"}}<svg class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{{if eq . "sun"}}<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>{{else if eq . "moon"}}<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>{{else if eq . "monitor"}}<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>{{else if eq . "logout"}}<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5M21 12H9"/>{{else if eq . "menu"}}<path d="M3 6h18M3 12h18M3 18h18"/>{{else if eq . "x"}}<path d="M18 6L6 18M6 6l12 12"/>{{else if eq . "check"}}<path d="M20 6L9 17l-5-5"/>{{else if eq . "alert"}}<path d="M10.3 3.9L1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/>{{else if eq . "refresh"}}<path d="M21 12a9 9 0 0 1-15.5 6.2L3 16"/><path d="M3 21v-5h5"/><path d="M3 12a9 9 0 0 1 15.5-6.2L21 8"/><path d="M21 3v5h-5"/>{{else if eq . "plus"}}<path d="M12 5v14M5 12h14"/>{{else if eq . "trash"}}<path d="M3 6h18"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>{{else if eq . "external"}}<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6M10 14L21 3"/>{{else if eq . "chevron-down"}}<path d="M6 9l6 6 6-6"/>{{else if eq . "chevron-right"}}<path d="M9 6l6 6-6 6"/>{{else if eq . "search"}}<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.3-4.3"/>{{else if eq . "layout-dashboard"}}<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>{{else if eq . "server"}}<rect width="20" height="8" x="2" y="2" rx="2" ry="2"/><rect width="20" height="8" x="2" y="14" rx="2" ry="2"/><path d="M6 6h.01M6 18h.01"/>{{else if eq . "box"}}<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="M3.3 7L12 12l8.7-5M12 22V12"/>{{else if eq . "settings"}}<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>{{end}}</svg>{{end}}
Loading