diff --git a/.env.example b/.env.example index efa2617..78cb2d3 100644 --- a/.env.example +++ b/.env.example @@ -65,10 +65,19 @@ # Use a logo URL, or leave it empty and set a Font Awesome icon class. # Logo URLs may be absolute http(s) URLs or root-relative paths such as /assets/logo.png. # OVERLORD_LOGIN_BRAND_NAME=Acme SOC +# OVERLORD_NAV_BRAND_NAME=Acme Console +# OVERLORD_BRAND_ACCENT_COLOR=#14b8a6 # OVERLORD_LOGIN_TITLE=Welcome to Acme Overlord # OVERLORD_LOGIN_SUBTITLE=Sign in with your Acme identity # OVERLORD_LOGIN_LOGO_URL=/assets/acme-logo.png # OVERLORD_LOGIN_LOGO_ALT=Acme logo +# OVERLORD_NAV_LOGO_URL=/assets/acme-nav-logo.png +# OVERLORD_NAV_LOGO_ALT=Acme logo +# OVERLORD_LOGIN_HERO_IMAGE_URL=/assets/acme-login.jpg +# OVERLORD_LOGIN_HERO_IMAGE_ALT=Acme operations center +# OVERLORD_LOGIN_FOOTER_TEXT=Authorized Acme access only +# OVERLORD_LOGIN_SUPPORT_TEXT=Need access help? +# OVERLORD_LOGIN_SUPPORT_URL=https://help.example.com # OVERLORD_LOGIN_ICON_CLASS=fa-solid fa-shield-halved # ============================================ diff --git a/Overlord-Server/public/assets/login.js b/Overlord-Server/public/assets/login.js index 927354b..bd795c9 100644 --- a/Overlord-Server/public/assets/login.js +++ b/Overlord-Server/public/assets/login.js @@ -11,6 +11,9 @@ const loginTitle = document.getElementById("login-title"); const loginSubtitle = document.getElementById("login-subtitle"); const loginIcon = document.getElementById("login-icon"); const loginLogo = document.getElementById("login-logo"); +const loginHeroImage = document.getElementById("login-hero-image"); +const loginBrandFooter = document.getElementById("login-brand-footer"); +const loginSupportLink = document.getElementById("login-support-link"); (() => { const params = new URLSearchParams(window.location.search); @@ -31,6 +34,7 @@ const loginLogo = document.getElementById("login-logo"); if (loginTitle && data.title) loginTitle.textContent = data.title; if (loginSubtitle && data.subtitle) loginSubtitle.textContent = data.subtitle; if (data.productName) document.title = `${data.productName} Login`; + if (data.accentColor) document.documentElement.style.setProperty("--brand-accent", data.accentColor); if (loginIcon && data.iconClass) { loginIcon.className = `${data.iconClass} login-crown`; @@ -48,6 +52,34 @@ const loginLogo = document.getElementById("login-logo"); }; loginLogo.src = data.logoUrl; } + + if (loginHeroImage && data.heroImageUrl) { + loginHeroImage.alt = data.heroImageAlt || ""; + loginHeroImage.onload = () => { + loginHeroImage.style.display = ""; + document.body.classList.add("login-has-hero-image"); + }; + loginHeroImage.onerror = () => { + loginHeroImage.style.display = "none"; + document.body.classList.remove("login-has-hero-image"); + }; + loginHeroImage.src = data.heroImageUrl; + } + + if (loginBrandFooter && data.footerText) { + loginBrandFooter.textContent = data.footerText; + loginBrandFooter.style.display = ""; + } + + if (loginSupportLink && data.supportText && data.supportUrl) { + loginSupportLink.textContent = data.supportText; + loginSupportLink.href = data.supportUrl; + if (!data.supportUrl.startsWith("/")) { + loginSupportLink.target = "_blank"; + loginSupportLink.rel = "noopener noreferrer"; + } + loginSupportLink.style.display = ""; + } } catch {} })(); diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 5278126..7d63f92 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -340,6 +340,27 @@ a { overflow: hidden; } +.login-hero-image { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0.38; + filter: saturate(0.9) contrast(1.05); + pointer-events: none; +} + +.login-body.login-has-hero-image::after { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient(90deg, rgba(5, 7, 15, 0.88), rgba(5, 7, 15, 0.64)), + radial-gradient(75% 75% at 50% 25%, transparent, rgba(5, 7, 15, 0.62)); + pointer-events: none; +} + .login-body::before { content: ""; position: absolute; @@ -984,6 +1005,7 @@ article.card[data-bookmarked="true"] { inset: 0; pointer-events: none; overflow: hidden; + z-index: 1; } .particle { @@ -1031,6 +1053,7 @@ article.card[data-bookmarked="true"] { 0 0 100px rgba(92, 145, 255, 0.1); position: relative; opacity: 0; + z-index: 2; } .login-card::before { @@ -1056,8 +1079,8 @@ article.card[data-bookmarked="true"] { .login-crown { font-size: 48px; - color: #7a5bff; - filter: drop-shadow(0 4px 12px rgba(122, 91, 255, 0.5)); + color: var(--brand-accent, #7a5bff); + filter: drop-shadow(0 4px 12px color-mix(in srgb, var(--brand-accent, #7a5bff) 50%, transparent)); } .login-logo { @@ -1065,7 +1088,7 @@ article.card[data-bookmarked="true"] { height: 56px; object-fit: contain; border-radius: 12px; - filter: drop-shadow(0 4px 12px rgba(122, 91, 255, 0.35)); + filter: drop-shadow(0 4px 12px color-mix(in srgb, var(--brand-accent, #7a5bff) 35%, transparent)); } .login-brand { @@ -1074,7 +1097,7 @@ article.card[data-bookmarked="true"] { font-weight: 700; letter-spacing: 1px; margin-bottom: 12px; - background: linear-gradient(135deg, #7a5bff, #5672ff); + background: linear-gradient(135deg, var(--brand-accent, #7a5bff), #7dd3fc); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -1097,6 +1120,24 @@ article.card[data-bookmarked="true"] { opacity: 0; } +.login-enterprise-footer, +.login-support-link { + display: block; + text-align: center; + font-size: 0.78rem; + color: #94a3b8; + margin-top: 12px; +} + +.login-support-link { + color: #c4b5fd; + text-decoration: none; +} + +.login-support-link:hover { + color: #e0e7ff; +} + .form-group { margin-bottom: 20px; opacity: 0; @@ -4456,6 +4497,33 @@ body.sb-collapsed .sb-link .sb-text { text-decoration: none; } +.nav-brand-link { + min-width: 0; +} + +.nav-brand-logo { + width: 24px; + height: 24px; + object-fit: contain; + border-radius: 6px; + flex: 0 0 auto; + filter: drop-shadow(0 2px 8px color-mix(in srgb, var(--brand-accent, #7a5bff) 28%, transparent)); +} + +.sb-logo .nav-brand-logo, +.sb-mobile-brand .nav-brand-logo { + width: 22px; + height: 22px; +} + +#nav-brand-name, +#nav-mobile-brand-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* ---- Mobile backdrop ---- */ #sb-backdrop { display: none; diff --git a/Overlord-Server/public/assets/nav.js b/Overlord-Server/public/assets/nav.js index f953bc6..002ce4b 100644 --- a/Overlord-Server/public/assets/nav.js +++ b/Overlord-Server/public/assets/nav.js @@ -22,6 +22,7 @@ import { const host = document.getElementById("top-nav"); if (host) { const refs = mountNav(host); + applyBranding(); initCommandPalette(); import("./cert-banner.js").then(({ showCertBannerIfNeeded }) => { runWithoutPageTracking(() => showCertBannerIfNeeded(document.getElementById("sb-mobile-bar") || host)); @@ -246,3 +247,44 @@ if (host) { setupSoftNavigation(host, { onPathChange: applyActivePath }); startPageTracking(); } + +async function applyBranding() { + try { + const res = await fetch("/api/login/branding"); + if (!res.ok) return; + const brand = await res.json(); + if (brand.accentColor) { + document.documentElement.style.setProperty("--brand-accent", brand.accentColor); + } + + const navName = brand.navName || brand.productName || "Overlord"; + for (const el of document.querySelectorAll("#nav-brand-name, #nav-mobile-brand-name")) { + el.textContent = navName; + } + + const iconClass = brand.iconClass || "fa-solid fa-crown"; + for (const icon of document.querySelectorAll("#nav-brand-icon, #nav-mobile-brand-icon")) { + const sidebarIcon = Boolean(icon.closest(".sb-logo")); + icon.className = `${iconClass} header-crown${sidebarIcon ? " sb-icon" : ""}`; + } + + const logoUrl = brand.navLogoUrl || brand.logoUrl || ""; + if (!logoUrl) return; + + for (const logo of document.querySelectorAll("#nav-brand-logo, #nav-mobile-brand-logo")) { + const pairedIcon = logo.id === "nav-mobile-brand-logo" + ? document.getElementById("nav-mobile-brand-icon") + : document.getElementById("nav-brand-icon"); + logo.alt = brand.navLogoAlt || brand.logoAlt || `${navName} logo`; + logo.onload = () => { + logo.style.display = ""; + if (pairedIcon) pairedIcon.style.display = "none"; + }; + logo.onerror = () => { + logo.style.display = "none"; + if (pairedIcon) pairedIcon.style.display = ""; + }; + logo.src = logoUrl; + } + } catch {} +} diff --git a/Overlord-Server/public/assets/nav/template.js b/Overlord-Server/public/assets/nav/template.js index 5ce63dd..a574a07 100644 --- a/Overlord-Server/public/assets/nav/template.js +++ b/Overlord-Server/public/assets/nav/template.js @@ -133,8 +133,10 @@ function mountTopbar(host) { host.innerHTML = `