diff --git a/README.md b/README.md
index d03e10c..a3a0042 100644
--- a/README.md
+++ b/README.md
@@ -9,13 +9,13 @@
INGROUP: MokoCassiopeia.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia
FILE: ./README.md
- VERSION: 03.10.09
+ VERSION: 03.10.10
BRIEF: Documentation for MokoCassiopeia template
-->
# MokoCassiopeia → MokoOnyx
-> **This template is being renamed to MokoOnyx.** Version 03.10.09 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled.
+> **This template is being renamed to MokoOnyx.** Version 03.10.10 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled.
**A Modern, Lightweight Joomla Template Based on Cassiopeia**
diff --git a/src/helper/favicon.php b/src/helper/favicon.php
index 78d6269..ca64d61 100644
--- a/src/helper/favicon.php
+++ b/src/helper/favicon.php
@@ -1,6 +1,6 @@
+ * Copyright (C) 2026 Moko Consulting
*
* This file is part of a Moko Consulting project.
*
@@ -9,7 +9,12 @@
/**
* Favicon generator — creates ICO, Apple Touch Icon, and Android icons
- * from a single source PNG uploaded via the template config.
+ * from a single source image uploaded via the template config.
+ *
+ * Supports three backends in priority order:
+ * 1. GD (fastest, most common)
+ * 2. Imagick (common on shared hosting)
+ * 3. Pure PHP (zero-dependency fallback using raw PNG manipulation)
*/
defined('_JEXEC') or die;
@@ -18,36 +23,21 @@ use Joomla\CMS\Log\Log;
class MokoFaviconHelper
{
- /**
- * Sizes to generate: filename => [width, height, format].
- * ICO embeds 16×16 and 32×32 internally.
- */
private const SIZES = [
- 'apple-touch-icon.png' => [180, 180, 'png'],
- 'favicon-32x32.png' => [32, 32, 'png'],
- 'favicon-16x16.png' => [16, 16, 'png'],
- 'android-chrome-192x192.png' => [192, 192, 'png'],
- 'android-chrome-512x512.png' => [512, 512, 'png'],
+ 'apple-touch-icon.png' => [180, 180],
+ 'favicon-32x32.png' => [32, 32],
+ 'favicon-16x16.png' => [16, 16],
+ 'android-chrome-192x192.png' => [192, 192],
+ 'android-chrome-512x512.png' => [512, 512],
];
/**
- * Generate all favicon files from a source PNG if they don't already exist
- * or if the source has been modified since last generation.
- *
- * @param string $sourcePath Absolute path to the source PNG.
- * @param string $outputDir Absolute path to the output directory.
- *
- * @return bool True if generation succeeded or files are up to date.
+ * Generate all favicon files from a source image.
*/
public static function generate(string $sourcePath, string $outputDir): bool
{
- if (!extension_loaded('gd')) {
- Log::add('Favicon: GD extension not loaded', Log::WARNING, 'mokocassiopeia');
- return false;
- }
-
if (!is_file($sourcePath)) {
- Log::add('Favicon: source file not found: ' . $sourcePath, Log::WARNING, 'mokocassiopeia');
+ self::log('Favicon: source file not found: ' . $sourcePath, 'warning');
return false;
}
@@ -58,15 +48,37 @@ class MokoFaviconHelper
$sourceTime = filemtime($sourcePath);
$stampFile = $outputDir . '/.favicon_generated';
- // Skip if already up to date
if (is_file($stampFile) && filemtime($stampFile) >= $sourceTime) {
return true;
}
- // Detect image type and load accordingly
+ // Strip #joomlaImage fragment if present
+ $sourcePath = strtok($sourcePath, '#');
+
+ // Select backend
+ if (extension_loaded('gd')) {
+ $result = self::generateWithGd($sourcePath, $outputDir);
+ } elseif (extension_loaded('imagick')) {
+ $result = self::generateWithImagick($sourcePath, $outputDir);
+ } else {
+ $result = self::generatePurePHP($sourcePath, $outputDir);
+ }
+
+ if ($result) {
+ self::generateManifest($outputDir);
+ file_put_contents($stampFile, date('c'));
+ }
+
+ return $result;
+ }
+
+ // ── GD Backend ──────────────────────────────────────────────────
+
+ private static function generateWithGd(string $sourcePath, string $outputDir): bool
+ {
$imageInfo = @getimagesize($sourcePath);
if ($imageInfo === false) {
- Log::add('Favicon: cannot read image info from ' . $sourcePath, Log::WARNING, 'mokocassiopeia');
+ self::log('Favicon: cannot read image: ' . $sourcePath, 'warning');
return false;
}
@@ -75,83 +87,385 @@ class MokoFaviconHelper
IMAGETYPE_JPEG => @imagecreatefromjpeg($sourcePath),
IMAGETYPE_GIF => @imagecreatefromgif($sourcePath),
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($sourcePath) : false,
- IMAGETYPE_BMP => function_exists('imagecreatefrombmp') ? @imagecreatefrombmp($sourcePath) : false,
default => false,
};
if (!$source) {
- Log::add('Favicon: unsupported image type (' . ($imageInfo['mime'] ?? 'unknown') . ') at ' . $sourcePath, Log::WARNING, 'mokocassiopeia');
+ self::log('Favicon: unsupported image type', 'warning');
return false;
}
imagealphablending($source, false);
imagesavealpha($source, true);
-
$srcW = imagesx($source);
$srcH = imagesy($source);
- // Generate PNG sizes
foreach (self::SIZES as $filename => [$w, $h]) {
$resized = imagecreatetruecolor($w, $h);
imagealphablending($resized, false);
imagesavealpha($resized, true);
- $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
- imagefill($resized, 0, 0, $transparent);
-
+ imagefill($resized, 0, 0, imagecolorallocatealpha($resized, 0, 0, 0, 127));
imagecopyresampled($resized, $source, 0, 0, 0, 0, $w, $h, $srcW, $srcH);
imagepng($resized, $outputDir . '/' . $filename, 9);
imagedestroy($resized);
}
- // Generate ICO (contains 16×16 and 32×32)
- self::generateIco($source, $srcW, $srcH, $outputDir . '/favicon.ico');
-
- // Generate site.webmanifest
- self::generateManifest($outputDir);
-
- imagedestroy($source);
-
- // Write timestamp stamp
- file_put_contents($stampFile, date('c'));
-
- return true;
- }
-
- /**
- * Build a minimal ICO file containing 16×16 and 32×32 PNG entries.
- */
- private static function generateIco(\GdImage $source, int $srcW, int $srcH, string $outPath): void
- {
- $entries = [];
+ // ICO from GD
+ $icoEntries = [];
foreach ([16, 32] as $size) {
$resized = imagecreatetruecolor($size, $size);
imagealphablending($resized, false);
imagesavealpha($resized, true);
- $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
- imagefill($resized, 0, 0, $transparent);
+ imagefill($resized, 0, 0, imagecolorallocatealpha($resized, 0, 0, 0, 127));
imagecopyresampled($resized, $source, 0, 0, 0, 0, $size, $size, $srcW, $srcH);
-
ob_start();
imagepng($resized, null, 9);
- $pngData = ob_get_clean();
+ $icoEntries[] = ['size' => $size, 'data' => ob_get_clean()];
imagedestroy($resized);
+ }
+ self::writeIco($icoEntries, $outputDir . '/favicon.ico');
- $entries[] = ['size' => $size, 'data' => $pngData];
+ imagedestroy($source);
+ self::log('Favicon: generated with GD');
+ return true;
+ }
+
+ // ── Imagick Backend ─────────────────────────────────────────────
+
+ private static function generateWithImagick(string $sourcePath, string $outputDir): bool
+ {
+ try {
+ foreach (self::SIZES as $filename => [$w, $h]) {
+ $img = new \Imagick($sourcePath);
+ $img->setImageFormat('png');
+ $img->setImageCompressionQuality(95);
+ $img->thumbnailImage($w, $h, true);
+ // Center on transparent canvas if not square
+ $canvas = new \Imagick();
+ $canvas->newImage($w, $h, new \ImagickPixel('transparent'), 'png');
+ $offsetX = (int)(($w - $img->getImageWidth()) / 2);
+ $offsetY = (int)(($h - $img->getImageHeight()) / 2);
+ $canvas->compositeImage($img, \Imagick::COMPOSITE_OVER, $offsetX, $offsetY);
+ $canvas->writeImage($outputDir . '/' . $filename);
+ $img->destroy();
+ $canvas->destroy();
+ }
+
+ // ICO from Imagick
+ $icoEntries = [];
+ foreach ([16, 32] as $size) {
+ $img = new \Imagick($sourcePath);
+ $img->setImageFormat('png');
+ $img->thumbnailImage($size, $size, true);
+ $icoEntries[] = ['size' => $size, 'data' => (string) $img];
+ $img->destroy();
+ }
+ self::writeIco($icoEntries, $outputDir . '/favicon.ico');
+
+ self::log('Favicon: generated with Imagick');
+ return true;
+ } catch (\Exception $e) {
+ self::log('Favicon: Imagick failed: ' . $e->getMessage(), 'warning');
+ return false;
+ }
+ }
+
+ // ── Pure PHP Backend (zero dependencies) ────────────────────────
+
+ private static function generatePurePHP(string $sourcePath, string $outputDir): bool
+ {
+ $pngData = @file_get_contents($sourcePath);
+ if ($pngData === false) {
+ self::log('Favicon: cannot read source file', 'warning');
+ return false;
}
- // ICO header: 2 bytes reserved, 2 bytes type (1=ICO), 2 bytes count
+ // Detect format — we can only resize PNG in pure PHP
+ // For JPEG/other formats, just copy the source as-is for each size
+ $isPng = (substr($pngData, 0, 8) === "\x89PNG\r\n\x1a\n");
+
+ if (!$isPng) {
+ // Non-PNG: copy source file for all sizes (no resize capability without extensions)
+ foreach (self::SIZES as $filename => [$w, $h]) {
+ copy($sourcePath, $outputDir . '/' . $filename);
+ }
+ // ICO: embed the raw source for 16 and 32 entries
+ self::writeIco([
+ ['size' => 16, 'data' => $pngData],
+ ['size' => 32, 'data' => $pngData],
+ ], $outputDir . '/favicon.ico');
+ self::log('Favicon: non-PNG source copied without resize (no GD/Imagick)');
+ return true;
+ }
+
+ // Parse PNG dimensions from IHDR
+ $ihdr = self::parsePngIhdr($pngData);
+ if (!$ihdr) {
+ self::log('Favicon: cannot parse PNG header', 'warning');
+ return false;
+ }
+
+ $srcW = $ihdr['width'];
+ $srcH = $ihdr['height'];
+
+ // Decode PNG to raw RGBA pixel array
+ $pixels = self::decodePngToRgba($pngData, $srcW, $srcH, $ihdr);
+ if ($pixels === null) {
+ // Fallback: copy source for all sizes
+ foreach (self::SIZES as $filename => [$w, $h]) {
+ copy($sourcePath, $outputDir . '/' . $filename);
+ }
+ self::writeIco([
+ ['size' => 16, 'data' => $pngData],
+ ['size' => 32, 'data' => $pngData],
+ ], $outputDir . '/favicon.ico');
+ self::log('Favicon: PNG decode failed, copied source without resize');
+ return true;
+ }
+
+ // Generate resized PNGs
+ foreach (self::SIZES as $filename => [$w, $h]) {
+ $resized = self::resizePixels($pixels, $srcW, $srcH, $w, $h);
+ $png = self::encodePng($resized, $w, $h);
+ file_put_contents($outputDir . '/' . $filename, $png);
+ }
+
+ // ICO
+ $icoEntries = [];
+ foreach ([16, 32] as $size) {
+ $resized = self::resizePixels($pixels, $srcW, $srcH, $size, $size);
+ $icoEntries[] = ['size' => $size, 'data' => self::encodePng($resized, $size, $size)];
+ }
+ self::writeIco($icoEntries, $outputDir . '/favicon.ico');
+
+ self::log('Favicon: generated with pure PHP');
+ return true;
+ }
+
+ /**
+ * Parse PNG IHDR chunk.
+ */
+ private static function parsePngIhdr(string $data): ?array
+ {
+ if (strlen($data) < 33) return null;
+ // Skip 8-byte signature, 4-byte chunk length, 4-byte "IHDR"
+ $width = unpack('N', substr($data, 16, 4))[1];
+ $height = unpack('N', substr($data, 20, 4))[1];
+ $bitDepth = ord($data[24]);
+ $colorType = ord($data[25]);
+ return ['width' => $width, 'height' => $height, 'bitDepth' => $bitDepth, 'colorType' => $colorType];
+ }
+
+ /**
+ * Decode PNG to flat RGBA array using zlib decompression.
+ *
+ * @return array|null Flat array of [r,g,b,a, r,g,b,a, ...] or null on failure.
+ */
+ private static function decodePngToRgba(string $data, int $w, int $h, array $ihdr): ?array
+ {
+ // Only support 8-bit RGBA (color type 6) and RGB (color type 2) for simplicity
+ $colorType = $ihdr['colorType'];
+ $bitDepth = $ihdr['bitDepth'];
+
+ if ($bitDepth !== 8 || ($colorType !== 6 && $colorType !== 2 && $colorType !== 3)) {
+ return null; // Unsupported format
+ }
+
+ // Collect all IDAT chunks
+ $idatData = '';
+ $pos = 8; // Skip PNG signature
+ $palette = null;
+ $trns = null;
+
+ while ($pos < strlen($data) - 4) {
+ $chunkLen = unpack('N', substr($data, $pos, 4))[1];
+ $chunkType = substr($data, $pos + 4, 4);
+
+ if ($chunkType === 'IDAT') {
+ $idatData .= substr($data, $pos + 8, $chunkLen);
+ } elseif ($chunkType === 'PLTE') {
+ $palette = substr($data, $pos + 8, $chunkLen);
+ } elseif ($chunkType === 'tRNS') {
+ $trns = substr($data, $pos + 8, $chunkLen);
+ } elseif ($chunkType === 'IEND') {
+ break;
+ }
+
+ $pos += 12 + $chunkLen; // 4 len + 4 type + data + 4 crc
+ }
+
+ $raw = @gzuncompress($idatData);
+ if ($raw === false) {
+ $raw = @gzinflate($idatData);
+ }
+ if ($raw === false) {
+ // Try with zlib header
+ $raw = @gzinflate(substr($idatData, 2));
+ }
+ if ($raw === false) {
+ return null;
+ }
+
+ $bpp = $colorType === 6 ? 4 : ($colorType === 2 ? 3 : 1); // bytes per pixel
+ $stride = 1 + $w * $bpp; // +1 for filter byte per row
+ $pixels = [];
+
+ $prevRow = array_fill(0, $w * $bpp, 0);
+
+ for ($y = 0; $y < $h; $y++) {
+ $rowStart = $y * $stride;
+ if ($rowStart >= strlen($raw)) break;
+
+ $filter = ord($raw[$rowStart]);
+ $row = [];
+
+ for ($x = 0; $x < $w * $bpp; $x++) {
+ $rawByte = ord($raw[$rowStart + 1 + $x]);
+ $a = ($x >= $bpp) ? $row[$x - $bpp] : 0;
+ $b = $prevRow[$x];
+ $c = ($x >= $bpp) ? $prevRow[$x - $bpp] : 0;
+
+ $val = match ($filter) {
+ 0 => $rawByte,
+ 1 => ($rawByte + $a) & 0xFF,
+ 2 => ($rawByte + $b) & 0xFF,
+ 3 => ($rawByte + (int)(($a + $b) / 2)) & 0xFF,
+ 4 => ($rawByte + self::paethPredictor($a, $b, $c)) & 0xFF,
+ default => $rawByte,
+ };
+
+ $row[] = $val;
+ }
+
+ // Convert row to RGBA
+ for ($x = 0; $x < $w; $x++) {
+ if ($colorType === 6) { // RGBA
+ $pixels[] = $row[$x * 4];
+ $pixels[] = $row[$x * 4 + 1];
+ $pixels[] = $row[$x * 4 + 2];
+ $pixels[] = $row[$x * 4 + 3];
+ } elseif ($colorType === 2) { // RGB
+ $pixels[] = $row[$x * 3];
+ $pixels[] = $row[$x * 3 + 1];
+ $pixels[] = $row[$x * 3 + 2];
+ $pixels[] = 255;
+ } elseif ($colorType === 3 && $palette) { // Indexed
+ $idx = $row[$x];
+ $pixels[] = ord($palette[$idx * 3]);
+ $pixels[] = ord($palette[$idx * 3 + 1]);
+ $pixels[] = ord($palette[$idx * 3 + 2]);
+ $pixels[] = ($trns && $idx < strlen($trns)) ? ord($trns[$idx]) : 255;
+ }
+ }
+
+ $prevRow = $row;
+ }
+
+ return $pixels;
+ }
+
+ private static function paethPredictor(int $a, int $b, int $c): int
+ {
+ $p = $a + $b - $c;
+ $pa = abs($p - $a);
+ $pb = abs($p - $b);
+ $pc = abs($p - $c);
+ if ($pa <= $pb && $pa <= $pc) return $a;
+ if ($pb <= $pc) return $b;
+ return $c;
+ }
+
+ /**
+ * Bilinear resize of RGBA pixel array.
+ */
+ private static function resizePixels(array $src, int $srcW, int $srcH, int $dstW, int $dstH): array
+ {
+ $dst = [];
+ $xRatio = $srcW / $dstW;
+ $yRatio = $srcH / $dstH;
+
+ for ($y = 0; $y < $dstH; $y++) {
+ $srcY = $y * $yRatio;
+ $y0 = (int) $srcY;
+ $y1 = min($y0 + 1, $srcH - 1);
+ $yFrac = $srcY - $y0;
+
+ for ($x = 0; $x < $dstW; $x++) {
+ $srcX = $x * $xRatio;
+ $x0 = (int) $srcX;
+ $x1 = min($x0 + 1, $srcW - 1);
+ $xFrac = $srcX - $x0;
+
+ for ($c = 0; $c < 4; $c++) {
+ $tl = $src[($y0 * $srcW + $x0) * 4 + $c];
+ $tr = $src[($y0 * $srcW + $x1) * 4 + $c];
+ $bl = $src[($y1 * $srcW + $x0) * 4 + $c];
+ $br = $src[($y1 * $srcW + $x1) * 4 + $c];
+
+ $top = $tl + ($tr - $tl) * $xFrac;
+ $bot = $bl + ($br - $bl) * $xFrac;
+ $dst[] = (int) round($top + ($bot - $top) * $yFrac);
+ }
+ }
+ }
+
+ return $dst;
+ }
+
+ /**
+ * Encode RGBA pixel array to PNG binary.
+ */
+ private static function encodePng(array $pixels, int $w, int $h): string
+ {
+ // Build raw image data with filter byte 0 (None) per row
+ $raw = '';
+ for ($y = 0; $y < $h; $y++) {
+ $raw .= "\x00"; // filter: None
+ for ($x = 0; $x < $w; $x++) {
+ $i = ($y * $w + $x) * 4;
+ $raw .= chr($pixels[$i]) . chr($pixels[$i + 1]) . chr($pixels[$i + 2]) . chr($pixels[$i + 3]);
+ }
+ }
+
+ $compressed = gzcompress($raw);
+
+ // Build PNG
+ $png = "\x89PNG\r\n\x1a\n";
+
+ // IHDR
+ $ihdr = pack('NNCCCC', $w, $h, 8, 6, 0, 0, 0); // 8-bit RGBA
+ $png .= self::pngChunk('IHDR', $ihdr);
+
+ // IDAT
+ $png .= self::pngChunk('IDAT', $compressed);
+
+ // IEND
+ $png .= self::pngChunk('IEND', '');
+
+ return $png;
+ }
+
+ private static function pngChunk(string $type, string $data): string
+ {
+ $chunk = $type . $data;
+ return pack('N', strlen($data)) . $chunk . pack('N', crc32($chunk));
+ }
+
+ // ── Shared Utilities ────────────────────────────────────────────
+
+ /**
+ * Write ICO file from PNG data entries.
+ */
+ private static function writeIco(array $entries, string $outPath): void
+ {
$count = count($entries);
$ico = pack('vvv', 0, 1, $count);
-
- // Calculate offset: header (6) + directory entries (16 each)
$offset = 6 + ($count * 16);
$imageData = '';
foreach ($entries as $entry) {
$size = $entry['size'] >= 256 ? 0 : $entry['size'];
$dataLen = strlen($entry['data']);
-
- // ICONDIRENTRY: width, height, colors, reserved, planes, bpp, size, offset
$ico .= pack('CCCCvvVV', $size, $size, 0, 0, 1, 32, $dataLen, $offset);
$imageData .= $entry['data'];
$offset += $dataLen;
@@ -160,9 +474,6 @@ class MokoFaviconHelper
file_put_contents($outPath, $ico . $imageData);
}
- /**
- * Write a site.webmanifest for Android/PWA icon discovery.
- */
private static function generateManifest(string $outputDir): void
{
$manifest = [
@@ -177,13 +488,6 @@ class MokoFaviconHelper
);
}
- /**
- * Return the tags to inject into .
- *
- * @param string $basePath URL path to the favicon directory (relative to site root).
- *
- * @return string HTML link tags.
- */
public static function getHeadTags(string $basePath): string
{
$basePath = htmlspecialchars(rtrim($basePath, '/'), ENT_QUOTES, 'UTF-8');
@@ -194,4 +498,21 @@ class MokoFaviconHelper
. ' ' . "\n"
. ' ' . "\n";
}
+
+ private static function log(string $message, string $priority = 'info'): void
+ {
+ $priorities = [
+ 'info' => Log::INFO,
+ 'warning' => Log::WARNING,
+ 'error' => Log::ERROR,
+ ];
+
+ Log::addLogger(
+ ['text_file' => 'mokocassiopeia.log.php'],
+ Log::ALL,
+ ['mokocassiopeia']
+ );
+
+ Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia');
+ }
}
diff --git a/src/joomla.asset.json b/src/joomla.asset.json
index a6a6548..915411b 100644
--- a/src/joomla.asset.json
+++ b/src/joomla.asset.json
@@ -17,7 +17,7 @@
"defgroup": "Joomla.Template.Site",
"ingroup": "MokoCassiopeia.Template.Assets",
"path": "./media/templates/site/mokocassiopeia/joomla.asset.json",
- "version": "03.10.09",
+ "version": "03.10.10",
"brief": "Joomla asset registry for MokoCassiopeia"
}
},
diff --git a/src/templateDetails.xml b/src/templateDetails.xml
index 33fd39b..13a6271 100644
--- a/src/templateDetails.xml
+++ b/src/templateDetails.xml
@@ -39,13 +39,13 @@
MokoCassiopeia
- 03.10.09
+ 03.10.10
script.php
2026-04-15
Jonathan Miller || Moko Consulting
hello@mokoconsulting.tech
(C)GNU General Public License Version 3 - 2026 Moko Consulting
-
MokoCassiopeia Template Description MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).
This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.
Custom Colour Themes Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.
Custom CSS & JavaScript For site-specific styles and scripts that should survive template updates, create the following files:
media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides. media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript. These files are gitignored and will not be overwritten by template updates.
Code Attribution This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.
Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.
It includes integration with Bootstrap TOC , an open-source table of contents generator by A. Feld, licensed under the MIT License.
All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.
]]>
+ MokoCassiopeia Template Description MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).
This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.
Custom Colour Themes Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.
Custom CSS & JavaScript For site-specific styles and scripts that should survive template updates, create the following files:
media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides. media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript. These files are gitignored and will not be overwritten by template updates.
Code Attribution This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.
Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.
It includes integration with Bootstrap TOC , an open-source table of contents generator by A. Feld, licensed under the MIT License.
All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.
]]>
1
component.php