**Major Release** — introduces the long-awaited **Dark Mode Toggle**, streamlining accessibility and usability enhancements.

##Added

* **Dark Mode Toggle**

	* Frontend toggle switch included in template.
	* JavaScript handles switching between light/dark modes.
	* Dark mode CSS rules applied across template styles.
	* Automatic persistence of user choice (via localStorage).

* **Header Parameters Update**

	* Added **logo parameter support** in template settings.
	* Updated metadata & copyright header.

* **Expanded TOC (Table of Contents)**

	* Automatic TOC injection when enabled.
	* User selects placement via article > options > layout (`toc-left` or `toc-right`).

##Improved

* Cleaned up `index.php` by removing **skip-to-content** duplicate calls.
* Consolidated JavaScript asset loading (ensuring dark-mode script is loaded correctly from external JS file).
* Streamlined CSS for **toggle switch**, ensuring it inherits Bootstrap/Cassiopeia defaults.
* General accessibility refinements in typography and color contrast.

##Fixed

* Fixed missing **logo param** in header output.
* Corrected stylesheet inconsistencies between Bootstrap 5 helpers and template overrides.
* Patched redundant calls in script includes.
This commit is contained in:
2025-08-30 18:41:15 -05:00
parent d6f84ebb46
commit bee7796645
47 changed files with 2915 additions and 3124 deletions

View File

@@ -1,147 +1,170 @@
/**
* darkmode-toggle.js — Floating theme switch (class-based, CSP-proof)
* @version 2.2.1
* Storage key: "theme" -> "light" | "dark"
* Corner from <body data-theme-fab-pos="br|bl|tr|tl"> (default br)
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/darkmode-toggle.js
* VERSION: 02.00
* BRIEF: JavaScript logic for dark mode toggle functionality in Moko-Cassiopeia
* =========================================================================
*/
(function () {
'use strict';
'use strict';
var STORAGE_KEY = 'theme';
var docEl = document.documentElement;
var mql = window.matchMedia('(prefers-color-scheme: dark)');
var STORAGE_KEY = 'theme';
var docEl = document.documentElement;
var mql = window.matchMedia('(prefers-color-scheme: dark)');
function getStored() { try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; } }
function setStored(v) { try { localStorage.setItem(STORAGE_KEY, v); } catch (e) {} }
function clearStored() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} }
function systemTheme() { return mql.matches ? 'dark' : 'light'; }
function getStored() { try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; } }
function setStored(v) { try { localStorage.setItem(STORAGE_KEY, v); } catch (e) {} }
function clearStored() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} }
function systemTheme() { return mql.matches ? 'dark' : 'light'; }
function applyTheme(theme) {
docEl.setAttribute('data-bs-theme', theme);
docEl.setAttribute('data-aria-theme', theme);
var meta = document.querySelector('meta[name="theme-color"]');
if (meta) {
meta.setAttribute('content', theme === 'dark' ? '#0f1115' : '#ffffff');
}
var sw = document.getElementById('mokoThemeSwitch');
if (sw) {
sw.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
}
}
function applyTheme(theme) {
docEl.setAttribute('data-bs-theme', theme);
docEl.setAttribute('data-aria-theme', theme);
var meta = document.querySelector('meta[name="theme-color"]');
if (meta) {
meta.setAttribute('content', theme === 'dark' ? '#0f1115' : '#ffffff');
}
var sw = document.getElementById('mokoThemeSwitch');
if (sw) {
sw.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
}
}
function initTheme() {
var stored = getStored();
applyTheme(stored ? stored : systemTheme());
}
function initTheme() {
var stored = getStored();
applyTheme(stored ? stored : systemTheme());
}
function posClassFromBody() {
var pos = (document.body.getAttribute('data-theme-fab-pos') || 'br').toLowerCase();
if (!/^(br|bl|tr|tl)$/.test(pos)) pos = 'br';
return 'pos-' + pos;
}
function posClassFromBody() {
var pos = (document.body.getAttribute('data-theme-fab-pos') || 'br').toLowerCase();
if (!/^(br|bl|tr|tl)$/.test(pos)) pos = 'br';
return 'pos-' + pos;
}
function buildToggle() {
if (document.getElementById('mokoThemeFab')) return;
function buildToggle() {
if (document.getElementById('mokoThemeFab')) return;
var wrap = document.createElement('div');
wrap.id = 'mokoThemeFab';
wrap.className = posClassFromBody();
var wrap = document.createElement('div');
wrap.id = 'mokoThemeFab';
wrap.className = posClassFromBody();
// Light label
var lblL = document.createElement('span');
lblL.className = 'label';
lblL.textContent = 'Light';
// Light label
var lblL = document.createElement('span');
lblL.className = 'label';
lblL.textContent = 'Light';
// Switch
var switchWrap = document.createElement('button');
switchWrap.id = 'mokoThemeSwitch';
switchWrap.type = 'button';
switchWrap.setAttribute('role', 'switch');
switchWrap.setAttribute('aria-label', 'Toggle dark mode');
switchWrap.setAttribute('aria-checked', 'false'); // updated after init
// Switch
var switchWrap = document.createElement('button');
switchWrap.id = 'mokoThemeSwitch';
switchWrap.type = 'button';
switchWrap.setAttribute('role', 'switch');
switchWrap.setAttribute('aria-label', 'Toggle dark mode');
switchWrap.setAttribute('aria-checked', 'false'); // updated after init
var track = document.createElement('span');
track.className = 'switch';
var knob = document.createElement('span');
knob.className = 'knob';
track.appendChild(knob);
switchWrap.appendChild(track);
var track = document.createElement('span');
track.className = 'switch';
var knob = document.createElement('span');
knob.className = 'knob';
track.appendChild(knob);
switchWrap.appendChild(track);
// Dark label
var lblD = document.createElement('span');
lblD.className = 'label';
lblD.textContent = 'Dark';
// Dark label
var lblD = document.createElement('span');
lblD.className = 'label';
lblD.textContent = 'Dark';
// Auto button
var auto = document.createElement('button');
auto.id = 'mokoThemeAuto';
auto.type = 'button';
auto.className = 'btn btn-sm btn-link text-decoration-none px-2';
auto.setAttribute('aria-label', 'Follow system theme');
auto.textContent = 'Auto';
// Auto button
var auto = document.createElement('button');
auto.id = 'mokoThemeAuto';
auto.type = 'button';
auto.className = 'btn btn-sm btn-link text-decoration-none px-2';
auto.setAttribute('aria-label', 'Follow system theme');
auto.textContent = 'Auto';
// Behavior
switchWrap.addEventListener('click', function () {
var current = (docEl.getAttribute('data-bs-theme') || 'light').toLowerCase();
var next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
setStored(next);
});
// Behavior
switchWrap.addEventListener('click', function () {
var current = (docEl.getAttribute('data-bs-theme') || 'light').toLowerCase();
var next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
setStored(next);
});
auto.addEventListener('click', function () {
clearStored();
applyTheme(systemTheme());
});
auto.addEventListener('click', function () {
clearStored();
applyTheme(systemTheme());
});
// Respond to OS changes only when not user-forced
var onMql = function () {
if (!getStored()) applyTheme(systemTheme());
};
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql);
else if (typeof mql.addListener === 'function') mql.addListener(onMql);
// Respond to OS changes only when not user-forced
var onMql = function () {
if (!getStored()) applyTheme(systemTheme());
};
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql);
else if (typeof mql.addListener === 'function') mql.addListener(onMql);
// Initial state
var initial = getStored() || systemTheme();
switchWrap.setAttribute('aria-checked', initial === 'dark' ? 'true' : 'false');
// Initial state
var initial = getStored() || systemTheme();
switchWrap.setAttribute('aria-checked', initial === 'dark' ? 'true' : 'false');
// Mount
wrap.appendChild(lblL);
wrap.appendChild(switchWrap);
wrap.appendChild(lblD);
wrap.appendChild(auto);
document.body.appendChild(wrap);
// Mount
wrap.appendChild(lblL);
wrap.appendChild(switchWrap);
wrap.appendChild(lblD);
wrap.appendChild(auto);
document.body.appendChild(wrap);
// Debug helper
window.mokoThemeFabStatus = function () {
var el = document.getElementById('mokoThemeFab');
if (!el) return { mounted: false };
var r = el.getBoundingClientRect();
return {
mounted: true,
rect: { top: r.top, left: r.left, width: r.width, height: r.height },
zIndex: window.getComputedStyle(el).zIndex,
posClass: el.className
};
};
// Debug helper
window.mokoThemeFabStatus = function () {
var el = document.getElementById('mokoThemeFab');
if (!el) return { mounted: false };
var r = el.getBoundingClientRect();
return {
mounted: true,
rect: { top: r.top, left: r.left, width: r.width, height: r.height },
zIndex: window.getComputedStyle(el).zIndex,
posClass: el.className
};
};
// Outline if invisible
setTimeout(function () {
var r = wrap.getBoundingClientRect();
if (r.width < 10 || r.height < 10) {
wrap.classList.add('debug-outline');
console.warn('[moko] Theme FAB mounted but appears too small — check CSS collisions.');
}
}, 50);
}
// Outline if invisible
setTimeout(function () {
var r = wrap.getBoundingClientRect();
if (r.width < 10 || r.height < 10) {
wrap.classList.add('debug-outline');
console.warn('[moko] Theme FAB mounted but appears too small — check CSS collisions.');
}
}, 50);
}
function init() {
initTheme();
buildToggle();
}
function init() {
initTheme();
buildToggle();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -0,0 +1,347 @@
/*
=========================================================================
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see https://www.gnu.org/licenses/ .
=========================================================================
FILE INFORMATION
DEFGROUP: Joomla Template
FILE: media/templates/site/moko-cassiopeia/js/gtm.js
HEADER VERSION: 1.0
VERSION: 2.0
BRIEF: Safe, configurable Google Tag Manager loader for Moko-Cassiopeia.
PATH: media/templates/site/moko-cassiopeia/js/gtm.js
NOTE: Place the <noscript> fallback iframe in your HTML template (index.php). A JS file
cannot provide a true no-JS fallback by definition.
VARIABLES:
- window.MOKO_GTM_ID (string) // Optional global GTM container ID (e.g., "GTM-XXXXXXX")
- window.MOKO_GTM_OPTIONS (object) // Optional global options (see JSDoc below)
- data- attributes on the script tag or <html>/<body>:
data-gtm-id, data-data-layer, data-debug, data-ignore-dnt,
data-env-auth, data-env-preview, data-block-on-dev
=========================================================================
*/
/* global window, document, navigator */
(() => {
"use strict";
/**
* @typedef {Object} MokoGtmOptions
* @property {string} [id] GTM container ID (e.g., "GTM-XXXXXXX")
* @property {string} [dataLayerName] Custom dataLayer name (default: "dataLayer")
* @property {boolean} [debug] Log debug messages to console (default: false)
* @property {boolean} [ignoreDNT] Ignore Do Not Track and always load (default: false)
* @property {boolean} [blockOnDev] Block loading on localhost/*.test/127.0.0.1 (default: true)
* @property {string} [envAuth] GTM Environment auth string (optional)
* @property {string} [envPreview] GTM Environment preview name (optional)
* @property {Record<string,'granted'|'denied'>} [consentDefault]
* Default Consent Mode v2 map. Keys like:
* analytics_storage, ad_storage, ad_user_data, ad_personalization, functionality_storage, security_storage
* (default: {analytics_storage:'granted', functionality_storage:'granted', security_storage:'granted'})
* @property {() => (Record<string, any>|void)} [pageVars]
* Function returning extra page variables to push on init (optional)
*/
const PKG = "moko-gtm";
const PREFIX = `[${PKG}]`;
const WIN = window;
// Public API placeholder (attached to window at the end)
/** @type {{
* init: (opts?: Partial<MokoGtmOptions>) => void,
* setConsent: (updates: Record<string,'granted'|'denied'>) => void,
* push: (...args:any[]) => void,
* isLoaded: () => boolean,
* config: () => Required<MokoGtmOptions>
* }} */
const API = {};
// ---- Utilities ---------------------------------------------------------
const isDevHost = () => {
const h = WIN.location && WIN.location.hostname || "";
return (
h === "localhost" ||
h === "127.0.0.1" ||
h.endsWith(".local") ||
h.endsWith(".test")
);
};
const dntEnabled = () => {
// Different browsers expose DNT differently; treat "1" or "yes" as enabled.
const n = navigator;
const v = (n.doNotTrack || n.msDoNotTrack || (n.navigator && n.navigator.doNotTrack) || "").toString().toLowerCase();
return v === "1" || v === "yes";
};
const getCurrentScript = () => {
// document.currentScript is best; fallback to last <script> whose src ends with /gtm.js
const cs = document.currentScript;
if (cs) return cs;
const scripts = Array.from(document.getElementsByTagName("script"));
return scripts.reverse().find(s => (s.getAttribute("src") || "").includes("/gtm.js")) || null;
};
const getAttr = (el, name) => el ? el.getAttribute(name) : null;
const readDatasetCascade = (name) => {
// Check <script>, <html>, <body>, then <meta name="moko:gtm-<name>">
const script = getCurrentScript();
const html = document.documentElement;
const body = document.body;
const meta = document.querySelector(`meta[name="moko:gtm-${name}"]`);
return (
(script && script.dataset && script.dataset[name]) ||
(html && html.dataset && html.dataset[name]) ||
(body && body.dataset && body.dataset[name]) ||
(meta && meta.getAttribute("content")) ||
null
);
};
const parseBool = (v, fallback = false) => {
if (v == null) return fallback;
const s = String(v).trim().toLowerCase();
if (["1","true","yes","y","on"].includes(s)) return true;
if (["0","false","no","n","off"].includes(s)) return false;
return fallback;
};
const debugLog = (...args) => {
if (STATE.debug) {
try { console.info(PREFIX, ...args); } catch (_) {}
}
};
// ---- Configuration & State --------------------------------------------
/** @type {Required<MokoGtmOptions>} */
const STATE = {
id: "",
dataLayerName: "dataLayer",
debug: false,
ignoreDNT: false,
blockOnDev: true,
envAuth: "",
envPreview: "",
consentDefault: {
analytics_storage: "granted",
functionality_storage: "granted",
security_storage: "granted",
// The following default to "denied" unless the site explicitly opts-in:
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
},
pageVars: () => ({})
};
const mergeOptions = (base, extra = {}) => {
const out = {...base};
for (const k in extra) {
if (!Object.prototype.hasOwnProperty.call(extra, k)) continue;
const v = extra[k];
if (v && typeof v === "object" && !Array.isArray(v)) {
out[k] = {...(out[k] || {}), ...v};
} else if (v !== undefined) {
out[k] = v;
}
}
return out;
};
const detectOptions = () => {
// 1) Global window options
/** @type {Partial<MokoGtmOptions>} */
const globalOpts = (WIN.MOKO_GTM_OPTIONS && typeof WIN.MOKO_GTM_OPTIONS === "object") ? WIN.MOKO_GTM_OPTIONS : {};
// 2) Dataset / meta
const idFromData = readDatasetCascade("id") || WIN.MOKO_GTM_ID || "";
const dlFromData = readDatasetCascade("dataLayer") || "";
const dbgFromData = readDatasetCascade("debug");
const dntFromData = readDatasetCascade("ignoreDnt");
const devFromData = readDatasetCascade("blockOnDev");
const authFromData = readDatasetCascade("envAuth") || "";
const prevFromData = readDatasetCascade("envPreview") || "";
// 3) Combine
/** @type {Partial<MokoGtmOptions>} */
const detected = {
id: idFromData || globalOpts.id || "",
dataLayerName: dlFromData || globalOpts.dataLayerName || undefined,
debug: parseBool(dbgFromData, !!globalOpts.debug),
ignoreDNT: parseBool(dntFromData, !!globalOpts.ignoreDNT),
blockOnDev: parseBool(devFromData, (globalOpts.blockOnDev ?? true)),
envAuth: authFromData || globalOpts.envAuth || "",
envPreview: prevFromData || globalOpts.envPreview || "",
consentDefault: globalOpts.consentDefault || undefined,
pageVars: typeof globalOpts.pageVars === "function" ? globalOpts.pageVars : undefined
};
return detected;
};
// ---- dataLayer / gtag helpers -----------------------------------------
const ensureDataLayer = () => {
const l = STATE.dataLayerName;
WIN[l] = WIN[l] || [];
return WIN[l];
};
/** gtag wrapper backed by dataLayer. */
const gtag = (...args) => {
const dl = ensureDataLayer();
dl.push(arguments.length > 1 ? args : args[0]);
debugLog("gtag push:", args);
};
API.push = (...args) => gtag(...args);
API.setConsent = (updates) => {
gtag("consent", "update", updates || {});
};
API.isLoaded = () => {
const hasScript = !!document.querySelector('script[src*="googletagmanager.com/gtm.js"]');
return hasScript;
};
API.config = () => ({...STATE});
// ---- Loader ------------------------------------------------------------
const buildEnvQuery = () => {
const qp = [];
if (STATE.envAuth) qp.push(`gtm_auth=${encodeURIComponent(STATE.envAuth)}`);
if (STATE.envPreview) qp.push(`gtm_preview=${encodeURIComponent(STATE.envPreview)}`, "gtm_cookies_win=x");
return qp.length ? `&${qp.join("&")}` : "";
};
const injectScript = () => {
if (!STATE.id) {
debugLog("GTM ID missing; aborting load.");
return;
}
if (API.isLoaded()) {
debugLog("GTM already loaded; skipping duplicate injection.");
return;
}
// Standard GTM bootstrap timing event
const dl = ensureDataLayer();
dl.push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
const f = document.getElementsByTagName("script")[0];
const j = document.createElement("script");
j.async = true;
j.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(STATE.id)}${STATE.dataLayerName !== "dataLayer" ? `&l=${encodeURIComponent(STATE.dataLayerName)}` : ""}${buildEnvQuery()}`;
if (f && f.parentNode) {
f.parentNode.insertBefore(j, f);
} else {
(document.head || document.documentElement).appendChild(j);
}
debugLog("Injected GTM script:", j.src);
};
const applyDefaultConsent = () => {
// Consent Mode v2 default
gtag("consent", "default", STATE.consentDefault);
debugLog("Applied default consent:", STATE.consentDefault);
};
const pushInitialVars = () => {
// Minimal page vars; allow site to add more via pageVars()
const vars = {
event: "moko.page_init",
page_title: document.title || "",
page_language: (document.documentElement && document.documentElement.lang) || "",
...(typeof STATE.pageVars === "function" ? (STATE.pageVars() || {}) : {})
};
gtag(vars);
};
const shouldLoad = () => {
if (!STATE.ignoreDNT && dntEnabled()) {
debugLog("DNT is enabled; blocking GTM load (set ignoreDNT=true to override).");
return false;
}
if (STATE.blockOnDev && isDevHost()) {
debugLog("Development host detected; blocking GTM load (set blockOnDev=false to override).");
return false;
}
return true;
};
// ---- Public init -------------------------------------------------------
API.init = (opts = {}) => {
// Merge: defaults <- detected <- passed opts
const detected = detectOptions();
const merged = mergeOptions(STATE, mergeOptions(detected, opts));
// Commit back to STATE
Object.assign(STATE, merged);
debugLog("Config:", STATE);
// Prepare dataLayer/gtag and consent
ensureDataLayer();
applyDefaultConsent();
pushInitialVars();
// Load GTM if allowed
if (shouldLoad()) {
injectScript();
} else {
debugLog("GTM load prevented by configuration or environment.");
}
};
// ---- Auto-init on DOMContentLoaded (safe even if deferred) -------------
const autoInit = () => {
// Only auto-init if we have some ID from globals/datasets.
const detected = detectOptions();
const hasId = !!(detected.id || WIN.MOKO_GTM_ID);
if (hasId) {
API.init(); // use detected/global defaults
} else {
debugLog("No GTM ID detected; awaiting manual init via window.mokoGTM.init({ id: 'GTM-XXXXXXX' }).");
}
};
if (document.readyState === "complete" || document.readyState === "interactive") {
// Defer to ensure <body> exists for any late consumers.
setTimeout(autoInit, 0);
} else {
document.addEventListener("DOMContentLoaded", autoInit, { once: true });
}
// Expose API
WIN.mokoGTM = API;
// Helpful console hint (only if debug true after detection)
try {
const detected = detectOptions();
if (parseBool(detected.debug, false)) {
STATE.debug = true;
debugLog("Ready. You can call window.mokoGTM.init({ id: 'GTM-XXXXXXX' }).");
}
} catch (_) {}
})();

View File

@@ -1,11 +0,0 @@
/**
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/mod_gabble.js
* @copyright (C) 2025 Jonathan Miler || Moko Consulting <https://mokoconsulting.tech>
* @website: https://mokoconsulting.tech
* @email: hello@mokoconsulting.tech
* @phone: +1 (931) 279-6313
* @license GNU General Public License version 2 or later; see LICENSE.txt
* @note This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*/

View File

@@ -1,118 +0,0 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -1,13 +1,30 @@
/**
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu-es5.js
* @copyright (C) 2025 Jonathan Miler || Moko Consulting <https://mokoconsulting.tech>
* @website: https://mokoconsulting.tech
* @email: hello@mokoconsulting.tech
* @phone: +1 (931) 279-6313
* @license GNU General Public License version 2 or later; see LICENSE.txt
* @note This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu-es5.js
* VERSION: 02.00
* BRIEF: ES5-compatible MetisMenu script for Joomla mod_menu in Moko-Cassiopeia
* =========================================================================
*/
(function () {

View File

@@ -1,13 +1,30 @@
/**
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu.js
* @copyright (C) 2025 Jonathan Miler || Moko Consulting <https://mokoconsulting.tech>
* @website: https://mokoconsulting.tech
* @email: hello@mokoconsulting.tech
* @phone: +1 (931) 279-6313
* @license GNU General Public License version 2 or later; see LICENSE.txt
* @note This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu.js
* VERSION: 02.00
* BRIEF: Modern MetisMenu script for Joomla mod_menu in Moko-Cassiopeia
* =========================================================================
*/
document.addEventListener('DOMContentLoaded', () => {

View File

@@ -1,25 +1,14 @@
/**
* template.js — Custom JavaScript for the Moko Cassiopeia Joomla template
*
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/template.js
* @version 2.0
*
* @copyright (C) 2025 Moko Consulting
* @author Jonathan Miller
* @website https://mokoconsulting.tech
* @email hello@mokoconsulting.tech
* @phone +1 (931) 279-6313
*
* SPDX-License-Identifier: GPL-3.0-or-later
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -27,7 +16,15 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/template.js
* VERSION: 02.00
* BRIEF: Core JavaScript utilities and behaviors for Moko-Cassiopeia template
* =========================================================================
*/
(function (win, doc) {

View File

@@ -1,25 +1,14 @@
/**
* theme-init.js — Light/Dark mode initialization for Moko Cassiopeia
*
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/theme-init.js
* @version 2.0
*
* @copyright (C) 2025 Moko Consulting
* @author Jonathan Miller
* @website https://mokoconsulting.tech
* @email hello@mokoconsulting.tech
* @phone +1 (931) 279-6313
*
* SPDX-License-Identifier: GPL-3.0-or-later
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or (at
* your option) any later version.
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -27,7 +16,15 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/theme-init.js
* VERSION: 02.00
* BRIEF: Initialization script for Moko-Cassiopeia theme features and behaviors
* =========================================================================
*/
(function (win, doc) {

View File

@@ -1,25 +1,14 @@
/**
* user.js — User Custom JS File for Moko Cassiopeia
*
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/user.js
* @version 2.0
*
* @copyright (C) 2025 Moko Consulting
* @author Jonathan Miller
* @website https://mokoconsulting.tech
* @email hello@mokoconsulting.tech
* @phone +1 (931) 279-6313
*
* SPDX-License-Identifier: GPL-3.0-or-later
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or (at
* your option) any later version.
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -27,5 +16,13 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/user.js
* VERSION: 02.00
* BRIEF: JavaScript for handling user-specific interactions in Moko-Cassiopeia template
* =========================================================================
*/