The login/register page and the auth modal had drifted into a parallel system: floating-input fields, a sign-page-card shell and a bespoke jio-auth-modal wrapper that none of the theme's own sign CSS targeted. The reference card rules (.sign, .sign-img, .sign-content, .line-form-input, .scale-placeholder, .social_loginbar, .title-h-left, #u_sign .modal-dialog, .qrcode-signin-container) have been in main.css all along, so the fix is to render the reference DOM instead of re-styling a second one. auth-content.php now emits one .sign.jio-panel.blur-bg.relative card with a .tab-content of #tab-sign-in / #tab-sign-up / #tab-resetpassword / #tab-qrcode-signin / #tab-bind panes. Each pane opens with a .title-h-left.fa-2x heading plus a .muted-color.px12 link, fields are .relative.line-form.mb10 > .line-form-input + .scale-placeholder + .line-form-line, the verification trigger is the .yztx.abs-right captchsubmit button, and submits are .but.radius.jb-blue|jb-green.padding-lg.btn-block. Social entries come from the shared render_social_buttons() separator + .social_loginbar, and the OAuth bind pane uses the oauth-data-box avatar card. The modal shell is #u_sign.modal > .modal-dialog > .sign-content > .sign-img.absolute.hide-sm + .sign, and the page shell is the reference .container.sign-page > .row.sign-row.gutters-5 > .col-md-6(.col-md-offset-6/ .col-md-offset-3).sign-col with a .fixed.lazyload background and a blurred fixed footer. auth.js drives .in instead of .is-open, toggles the new password/signin-mode/invite-gate controls and no longer touches the retired markup. The page also drops two footer-hook removals that targeted functions which no longer exist, and opts out of the fixed-nav offset. The sign page's custom theme setting now actually applies: Router answers jio_forced_theme_mode, jioData.themeForced wins over the stored cookie in theme.js, theme-toggle positions are suppressed, and the page marks html/body dark or white accordingly. sign.css shrinks from ~114 lines of bespoke component styling to a thin positioning layer, and the retired classes are gone rather than kept. Verification: php -l on every changed PHP file, node --check on both JS files, node --test across widget-system, navigation and front-submit (873 passing), and tools/audit-psr4-paths.php / audit-admin-contracts.php both at 0 errors. assets/js/runtime/main.min.js was rebuilt because theme.js gained the forced theme branch.
249 lines
8.4 KiB
JavaScript
249 lines
8.4 KiB
JavaScript
/**
|
|
* Jio Theme Toggle — dark/light mode switching with transitions
|
|
*/
|
|
(function () {
|
|
"use strict";
|
|
|
|
var THEME_KEY = "jio-theme";
|
|
var THEME_COOKIE_KEY = "theme_mode";
|
|
|
|
function normalizeStoredTheme(value) {
|
|
if (value === "dark" || value === "dark-theme") return "dark";
|
|
if (value === "light" || value === "white-theme") return "light";
|
|
return null;
|
|
}
|
|
|
|
function toCookieTheme(value) {
|
|
return value === "dark" ? "dark-theme" : "white-theme";
|
|
}
|
|
|
|
function getCookie(name) {
|
|
var parts = document.cookie ? document.cookie.split("; ") : [];
|
|
for (var i = 0; i < parts.length; i++) {
|
|
var pair = parts[i].split("=");
|
|
if (decodeURIComponent(pair[0]) === name) {
|
|
return decodeURIComponent(pair.slice(1).join("="));
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function setCookie(name, value, days) {
|
|
var maxAge = days ? "; max-age=" + (days * 24 * 60 * 60) : "";
|
|
var secure = window.location && window.location.protocol === "https:" ? "; Secure" : "";
|
|
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + maxAge + "; path=/; SameSite=Lax" + secure;
|
|
}
|
|
|
|
function getStoredTheme() {
|
|
var localTheme = null;
|
|
try {
|
|
localTheme = normalizeStoredTheme(localStorage.getItem(THEME_KEY));
|
|
} catch (e) {
|
|
localTheme = null;
|
|
}
|
|
|
|
var theme = normalizeStoredTheme(getCookie(THEME_COOKIE_KEY)) || localTheme;
|
|
if (theme) {
|
|
setStoredTheme(theme);
|
|
}
|
|
return theme;
|
|
}
|
|
|
|
function setStoredTheme(value) {
|
|
var theme = normalizeStoredTheme(value);
|
|
if (!theme) return;
|
|
try {
|
|
localStorage.setItem(THEME_KEY, theme);
|
|
} catch (e) {}
|
|
setCookie(THEME_COOKIE_KEY, toCookieTheme(theme), 365);
|
|
}
|
|
|
|
function getTimeAutoTheme() {
|
|
var hour = (new Date()).getHours();
|
|
return hour >= 20 || hour < 8 ? "dark" : "light";
|
|
}
|
|
|
|
function resolveConfiguredTheme(mode) {
|
|
var normalized = normalizeStoredTheme(mode);
|
|
if (normalized) return normalized;
|
|
if (mode === "time-auto" || mode === "auto") {
|
|
return getTimeAutoTheme();
|
|
}
|
|
return "light";
|
|
}
|
|
|
|
function syncBrandLogos(isDark) {
|
|
document.querySelectorAll("[data-logo-light]").forEach(function (img) {
|
|
var lightSrc = img.getAttribute("data-logo-light") || "";
|
|
var darkSrc = img.getAttribute("data-logo-dark") || "";
|
|
if (!lightSrc) return;
|
|
img.setAttribute("src", isDark && darkSrc ? darkSrc : lightSrc);
|
|
});
|
|
}
|
|
|
|
function syncThemeColorMeta(isDark) {
|
|
var meta = document.querySelector('meta[name="theme-color"]');
|
|
if (!meta) {
|
|
meta = document.createElement("meta");
|
|
meta.setAttribute("name", "theme-color");
|
|
document.head.appendChild(meta);
|
|
}
|
|
meta.setAttribute("content", isDark ? "#2F3135" : "#FDFCFE");
|
|
}
|
|
|
|
function getThemeImageAttr(img, name) {
|
|
return img.getAttribute(name) || img.getAttribute("data-" + name) || "";
|
|
}
|
|
|
|
function setThemeImageSource(img, src) {
|
|
if (!src || img.getAttribute("src") === src) return;
|
|
img.setAttribute("src", src);
|
|
}
|
|
|
|
function syncThemeImages(isDark) {
|
|
document.querySelectorAll("img[switch-src], img[data-switch-src]").forEach(function (img) {
|
|
var darkSrc = getThemeImageAttr(img, "switch-src");
|
|
if (!img.dataset.jioThemeLightSrc) {
|
|
img.dataset.jioThemeLightSrc = img.getAttribute("src") || "";
|
|
}
|
|
setThemeImageSource(img, isDark && darkSrc ? darkSrc : img.dataset.jioThemeLightSrc);
|
|
});
|
|
|
|
document.querySelectorAll("img[white-src], img[dark-src], img[data-white-src], img[data-dark-src]").forEach(function (img) {
|
|
var lightSrc = getThemeImageAttr(img, "white-src") || img.dataset.jioThemeLightSrc || img.getAttribute("src") || "";
|
|
var darkSrc = getThemeImageAttr(img, "dark-src");
|
|
if (!img.dataset.jioThemeLightSrc && lightSrc) {
|
|
img.dataset.jioThemeLightSrc = lightSrc;
|
|
}
|
|
setThemeImageSource(img, isDark && darkSrc ? darkSrc : lightSrc);
|
|
});
|
|
}
|
|
|
|
function syncCodeHighlightTheme(isDark) {
|
|
var settings = (window.jioData && window.jioData.codeHighlight) || {};
|
|
var theme = isDark ? (settings.darkTheme || "monokai") : (settings.lightTheme || "enlighter");
|
|
document.querySelectorAll(".enlighter-default").forEach(function (el) {
|
|
el.className = el.className.replace(/\benlighter-t-[\w-]+\b/g, "").replace(/\s+/g, " ").trim();
|
|
el.classList.add("enlighter-t-" + theme);
|
|
});
|
|
}
|
|
|
|
function dispatchThemeChange(isDark) {
|
|
try {
|
|
document.dispatchEvent(new CustomEvent("jio:theme-change", {
|
|
detail: {
|
|
theme: isDark ? "dark" : "light",
|
|
isDark: isDark
|
|
}
|
|
}));
|
|
} catch (e) {}
|
|
}
|
|
|
|
function applyTheme(mode) {
|
|
var isDark = mode === "dark";
|
|
document.documentElement.classList.toggle("dark", isDark);
|
|
document.documentElement.setAttribute("data-theme", isDark ? "dark" : "light");
|
|
document.body.classList.toggle("theme-dark", isDark);
|
|
document.body.classList.toggle("dark-theme", isDark);
|
|
document.body.classList.toggle("white-theme", !isDark);
|
|
document.body.setAttribute("data-theme", isDark ? "dark" : "light");
|
|
syncBrandLogos(isDark);
|
|
syncThemeColorMeta(isDark);
|
|
syncThemeImages(isDark);
|
|
syncCodeHighlightTheme(isDark);
|
|
dispatchThemeChange(isDark);
|
|
// Glyph visibility (sun / moon slide) is CSS-driven off .toggle-theme's
|
|
// ::before / ::after and the .dark-theme body class — no JS here.
|
|
document.querySelectorAll(".toggle-theme").forEach(function (btn) {
|
|
btn.setAttribute("aria-pressed", isDark ? "true" : "false");
|
|
});
|
|
}
|
|
|
|
var themeInitialized = false;
|
|
|
|
function initTheme() {
|
|
if (themeInitialized) return;
|
|
themeInitialized = true;
|
|
|
|
var forced = window.jioData && window.jioData.themeForced;
|
|
var stored = getStoredTheme();
|
|
var configuredMode = (window.jioData && window.jioData.themeMode) || "light";
|
|
if (forced === "dark" || forced === "light") {
|
|
// A surface can pin the theme (the sign page's custom theme mode); a
|
|
// stored preference must not override it.
|
|
applyTheme(forced);
|
|
} else if (stored === "dark" || stored === "light") {
|
|
applyTheme(stored);
|
|
} else {
|
|
applyTheme(resolveConfiguredTheme(configuredMode));
|
|
}
|
|
|
|
function prefersReducedMotion() {
|
|
return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
}
|
|
|
|
function getThemeTransitionPoint(event) {
|
|
if (event && typeof event.clientX === "number" && typeof event.clientY === "number") {
|
|
return { x: event.clientX, y: event.clientY };
|
|
}
|
|
return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
|
}
|
|
|
|
function runThemeTransition(next, event) {
|
|
if (!document.startViewTransition || prefersReducedMotion()) {
|
|
applyTheme(next);
|
|
return;
|
|
}
|
|
|
|
var point = getThemeTransitionPoint(event);
|
|
var endRadius = Math.hypot(
|
|
Math.max(point.x, window.innerWidth - point.x),
|
|
Math.max(point.y, window.innerHeight - point.y)
|
|
);
|
|
var clipPath = [
|
|
"circle(0px at " + point.x + "px " + point.y + "px)",
|
|
"circle(" + endRadius + "px at " + point.x + "px " + point.y + "px)"
|
|
];
|
|
var enteringDark = next === "dark";
|
|
|
|
try {
|
|
var transition = document.startViewTransition(function () {
|
|
applyTheme(next);
|
|
});
|
|
|
|
transition.ready.then(function () {
|
|
try {
|
|
document.documentElement.animate(
|
|
{ clipPath: enteringDark ? clipPath : clipPath.slice().reverse() },
|
|
{
|
|
duration: 500,
|
|
easing: enteringDark ? "ease-in" : "ease-out",
|
|
fill: "both",
|
|
pseudoElement: enteringDark ? "::view-transition-new(root)" : "::view-transition-old(root)"
|
|
}
|
|
);
|
|
} catch (e) {}
|
|
}).catch(function () {});
|
|
} catch (e) {
|
|
applyTheme(next);
|
|
}
|
|
}
|
|
|
|
// Delegated so every current and future .toggle-theme anchor works — the
|
|
// header, the mobile drawer and the floating rail all share one binding.
|
|
document.addEventListener("click", function (event) {
|
|
var btn = event.target && event.target.closest ? event.target.closest(".toggle-theme") : null;
|
|
if (!btn) return;
|
|
var next = document.body.classList.contains("theme-dark") ? "light" : "dark";
|
|
setStoredTheme(next);
|
|
runThemeTransition(next, event);
|
|
});
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initTheme);
|
|
} else {
|
|
initTheme();
|
|
}
|
|
})();
|