feat: unified THE FORGE // SECTION headers and merged proxy routes
Build / build (push) Successful in 10m37s
Build / build (push) Successful in 10m37s
UI consistency
- ForgeHero now supports backHref, mono kicker, stats snippet, staggered
entrance animation, and a registration-tick divider
- Every route now opens with the same "THE FORGE // SECTION" eyebrow: projects,
sites, stacks, proxies, events, dns, deploy, settings, stale containers,
site/project detail + env/volumes/browse, new site wizard
- Stacks list/detail/new moved to the shared hero and brand-anchor eyebrow
- Toolbars migrated from bespoke buttons to the shared .forge-btn utilities
- Sidebar footline adds a live UTC "forge clock" and a vim-style g-prefix
quick-nav hint (g d/p/s/k/x/r/e/c jumps to each section)
Proxies page
- Server-side: merge static site proxy routes with instance routes and sort
by domain (internal/api/proxies.go, internal/store/static_sites.go)
- ProxyRoute gains a Source field ("instance" | "static_site")
- Frontend adds source filter tabs and per-source labels/badges
This commit is contained in:
@@ -44,6 +44,43 @@
|
||||
let hintsExpanded = $state(false);
|
||||
let proxyHintsExpanded = $state(false);
|
||||
|
||||
// Live UTC forge clock (refreshes every second). A small thing, but it makes
|
||||
// the sidebar feel alive and reinforces the "control room" aesthetic.
|
||||
let nowUtc = $state('');
|
||||
let clockTimer: ReturnType<typeof setInterval> | null = null;
|
||||
function tickClock() {
|
||||
const d = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
nowUtc = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
||||
}
|
||||
|
||||
// Keyboard quick-nav: "g" then a letter jumps to a section (vim-style).
|
||||
// g+d → dashboard, g+p → projects, g+s → sites, g+k → stacks, g+x → deploy,
|
||||
// g+r → proxies, g+e → events, g+c → settings
|
||||
let gPressedAt = 0;
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
// Ignore when typing in inputs/textareas/contenteditable.
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return;
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
|
||||
if (e.key === 'g') {
|
||||
gPressedAt = Date.now();
|
||||
return;
|
||||
}
|
||||
if (Date.now() - gPressedAt > 1200) return;
|
||||
const map: Record<string, string> = {
|
||||
d: '/', p: '/projects', s: '/sites', k: '/stacks',
|
||||
x: '/deploy', r: '/proxies', e: '/events', c: '/settings'
|
||||
};
|
||||
const dest = map[e.key.toLowerCase()];
|
||||
if (dest) {
|
||||
e.preventDefault();
|
||||
gPressedAt = 0;
|
||||
goto(dest);
|
||||
}
|
||||
}
|
||||
|
||||
const dockerConnected = $derived(dockerHealth?.connected ?? false);
|
||||
const proxyConnected = $derived(proxyHealth?.connected ?? true);
|
||||
const proxyProviderName = $derived(proxyHealth?.provider ?? '');
|
||||
@@ -80,6 +117,9 @@
|
||||
goto('/', { replaceState: true });
|
||||
}
|
||||
}
|
||||
tickClock();
|
||||
clockTimer = setInterval(tickClock, 1000);
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
// Start health polling when authenticated.
|
||||
@@ -106,6 +146,8 @@
|
||||
|
||||
onDestroy(() => {
|
||||
if (healthInterval) clearInterval(healthInterval);
|
||||
if (clockTimer) clearInterval(clockTimer);
|
||||
if (typeof window !== 'undefined') window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -255,7 +297,18 @@
|
||||
<IconLogout size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-[var(--text-tertiary)]">{$t('app.name')} {$t('app.version')}</p>
|
||||
<div class="forge-footline">
|
||||
<span class="forge-footline-version">{$t('app.name')} {$t('app.version')}</span>
|
||||
<span class="forge-footline-clock" title="UTC">
|
||||
<span class="clock-dot"></span>
|
||||
<span class="clock-time">{nowUtc || '--:--:--'}</span>
|
||||
<span class="clock-suffix">UTC</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="forge-nav-hint" title="Press 'g' then a letter to jump between sections">
|
||||
<kbd>g</kbd><span class="arr">→</span><kbd>d</kbd><kbd>p</kbd><kbd>s</kbd><kbd>k</kbd>
|
||||
<span class="hint-label">quick-nav</span>
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -336,6 +389,82 @@
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
/* ── Sidebar footline (version + live UTC clock) ───────────── */
|
||||
.forge-footline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding-top: 0.1rem;
|
||||
}
|
||||
.forge-footline-version {
|
||||
font-family: var(--forge-mono, 'JetBrains Mono', monospace);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
.forge-footline-clock {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-family: var(--forge-mono, 'JetBrains Mono', monospace);
|
||||
font-size: 0.66rem;
|
||||
color: var(--text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.clock-dot {
|
||||
width: 5px; height: 5px; border-radius: 50%;
|
||||
background: var(--color-brand-500);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-brand-500) 20%, transparent);
|
||||
animation: forge-breathe 2.4s ease-in-out infinite;
|
||||
}
|
||||
.clock-suffix {
|
||||
font-size: 0.56rem;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ── Keyboard quick-nav hint ───────────────────────────────── */
|
||||
.forge-nav-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
margin: 0.55rem 0 0;
|
||||
padding: 0;
|
||||
font-family: var(--forge-mono, 'JetBrains Mono', monospace);
|
||||
font-size: 0.58rem;
|
||||
color: var(--text-tertiary);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.forge-nav-hint kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
padding: 0 3px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 3px;
|
||||
background: var(--surface-card);
|
||||
color: var(--text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 0.6rem;
|
||||
line-height: 1;
|
||||
box-shadow: 0 1px 0 var(--border-primary);
|
||||
}
|
||||
.forge-nav-hint .arr {
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.5;
|
||||
font-size: 0.55rem;
|
||||
padding: 0 0.15rem;
|
||||
}
|
||||
.forge-nav-hint .hint-label {
|
||||
margin-left: auto;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Apply dot-grid backdrop to main content */
|
||||
:global(main) {
|
||||
position: relative;
|
||||
|
||||
Reference in New Issue
Block a user