Search toggle icon on mobile, 2-col desktop; update README badges
Search position: - Desktop: 2 columns (16.667%), menu fills the rest - Mobile: collapses to a magnifying glass icon button (like the hamburger) that expands the search form via Bootstrap collapse README: - Version moved to badge (03.09.04) - Joomla badge updated to 5.x | 6.x - PHP badge updated to 8.1+ - Removed inline VERSION from title Remove obsolete build-release.sh and minify.js scripts (replaced by CI workflow and PHP helper). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,13 +13,14 @@
|
|||||||
BRIEF: Documentation for MokoCassiopeia template
|
BRIEF: Documentation for MokoCassiopeia template
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# README - MokoCassiopeia (VERSION: 03.09.03)
|
# MokoCassiopeia
|
||||||
|
|
||||||
**A Modern, Lightweight Joomla Template Based on Cassiopeia**
|
**A Modern, Lightweight Joomla Template Based on Cassiopeia**
|
||||||
|
|
||||||
|
[](https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://www.joomla.org)
|
[](https://www.joomla.org)
|
||||||
[](https://www.php.net)
|
[](https://www.php.net)
|
||||||
|
|
||||||
MokoCassiopeia is a modern, lightweight enhancement layer built on top of Joomla's Cassiopeia template. It adds **Font Awesome 7**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, advanced **Dark Mode** theming, and optional integrations for **Google Tag Manager** and **Google Analytics (GA4)**—all while maintaining minimal core template overrides for maximum upgrade compatibility.
|
MokoCassiopeia is a modern, lightweight enhancement layer built on top of Joomla's Cassiopeia template. It adds **Font Awesome 7**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, advanced **Dark Mode** theming, and optional integrations for **Google Tag Manager** and **Google Analytics (GA4)**—all while maintaining minimal core template overrides for maximum upgrade compatibility.
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Build.Scripts
|
|
||||||
# INGROUP: MokoCassiopeia.Build
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
|
||||||
# PATH: /scripts/build-release.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Build release package for MokoCassiopeia template
|
|
||||||
# USAGE: ./scripts/build-release.sh [version]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Script directory
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
# Functions
|
|
||||||
log_info() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if version is provided
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
# Try to extract version from templateDetails.xml
|
|
||||||
if [ -f "${PROJECT_ROOT}/src/templateDetails.xml" ]; then
|
|
||||||
VERSION=$(grep -oP '<version>\K[^<]+' "${PROJECT_ROOT}/src/templateDetails.xml" | head -1)
|
|
||||||
log_info "Detected version: ${VERSION}"
|
|
||||||
else
|
|
||||||
log_error "Please provide version as argument: ./build-release.sh 03.08.03"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
VERSION="$1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Building MokoCassiopeia release package"
|
|
||||||
log_info "Version: ${VERSION}"
|
|
||||||
|
|
||||||
# Change to project root
|
|
||||||
cd "${PROJECT_ROOT}"
|
|
||||||
|
|
||||||
# Create build directory
|
|
||||||
BUILD_DIR="${PROJECT_ROOT}/build"
|
|
||||||
PACKAGE_DIR="${BUILD_DIR}/package"
|
|
||||||
rm -rf "${BUILD_DIR}"
|
|
||||||
mkdir -p "${PACKAGE_DIR}"
|
|
||||||
|
|
||||||
log_info "Creating package structure..."
|
|
||||||
|
|
||||||
# Copy template files from src (excluding media directory)
|
|
||||||
if [ -d "src" ]; then
|
|
||||||
rsync -av --exclude='.git*' --exclude='media' src/ "${PACKAGE_DIR}/"
|
|
||||||
else
|
|
||||||
log_error "src directory not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy media files from src/media
|
|
||||||
if [ -d "src/media" ]; then
|
|
||||||
mkdir -p "${PACKAGE_DIR}/media"
|
|
||||||
rsync -av --exclude='.git*' src/media/ "${PACKAGE_DIR}/media/"
|
|
||||||
else
|
|
||||||
log_warning "src/media directory not found, skipping media files"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Package structure created"
|
|
||||||
|
|
||||||
# Create ZIP package
|
|
||||||
cd "${PACKAGE_DIR}"
|
|
||||||
ZIP_NAME="mokocassiopeia-src-${VERSION}.zip"
|
|
||||||
log_info "Creating ZIP package: ${ZIP_NAME}"
|
|
||||||
|
|
||||||
zip -r "../${ZIP_NAME}" . -q
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
log_error "Failed to create ZIP package"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "${BUILD_DIR}"
|
|
||||||
log_success "Created: ${ZIP_NAME}"
|
|
||||||
|
|
||||||
# Generate checksums
|
|
||||||
log_info "Generating checksums..."
|
|
||||||
sha256sum "${ZIP_NAME}" > "${ZIP_NAME}.sha256"
|
|
||||||
md5sum "${ZIP_NAME}" > "${ZIP_NAME}.md5"
|
|
||||||
|
|
||||||
# Extract just the hash
|
|
||||||
SHA256_HASH=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
log_success "SHA-256: ${SHA256_HASH}"
|
|
||||||
log_success "MD5: $(md5sum "${ZIP_NAME}" | cut -d' ' -f1)"
|
|
||||||
|
|
||||||
# Show file info
|
|
||||||
FILE_SIZE=$(du -h "${ZIP_NAME}" | cut -f1)
|
|
||||||
log_info "Package size: ${FILE_SIZE}"
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
echo ""
|
|
||||||
log_success "Build completed successfully!"
|
|
||||||
echo ""
|
|
||||||
echo "Package: ${BUILD_DIR}/${ZIP_NAME}"
|
|
||||||
echo "SHA-256: ${BUILD_DIR}/${ZIP_NAME}.sha256"
|
|
||||||
echo "MD5: ${BUILD_DIR}/${ZIP_NAME}.md5"
|
|
||||||
echo ""
|
|
||||||
echo "Next steps:"
|
|
||||||
echo "1. Test the package installation in Joomla"
|
|
||||||
echo "2. Create a GitHub release with this package"
|
|
||||||
echo "3. Update updates.xml with the new version and SHA-256 hash"
|
|
||||||
echo ""
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
*
|
|
||||||
* This file is part of a Moko Consulting project.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* # FILE INFORMATION
|
|
||||||
* DEFGROUP: Joomla.Template.Site
|
|
||||||
* INGROUP: MokoCassiopeia
|
|
||||||
* REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
|
||||||
* PATH: ./scripts/minify.js
|
|
||||||
* VERSION: 03.09.03
|
|
||||||
* BRIEF: Generates .min.css and .min.js files from the Joomla asset manifest
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const CleanCSS = require('clean-css');
|
|
||||||
const { minify: terserMinify } = require('terser');
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Config
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const ROOT = path.resolve(__dirname, '..');
|
|
||||||
const SRC_MEDIA = path.join(ROOT, 'src', 'media');
|
|
||||||
const ASSET_JSON = path.join(ROOT, 'src', 'joomla.asset.json');
|
|
||||||
|
|
||||||
// URI prefix used in the manifest — maps to SRC_MEDIA on disk.
|
|
||||||
// e.g. "media/templates/site/mokocassiopeia/css/template.css"
|
|
||||||
const URI_PREFIX = 'media/templates/site/mokocassiopeia/';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a manifest URI to an absolute disk path under src/media/.
|
|
||||||
*
|
|
||||||
* @param {string} uri e.g. "media/templates/site/mokocassiopeia/css/foo.css"
|
|
||||||
* @returns {string|null}
|
|
||||||
*/
|
|
||||||
function uriToPath(uri) {
|
|
||||||
if (!uri.startsWith(URI_PREFIX)) return null;
|
|
||||||
return path.join(SRC_MEDIA, uri.slice(URI_PREFIX.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if the filename looks like an already-minified file or belongs
|
|
||||||
* to a vendor bundle we don't own.
|
|
||||||
*/
|
|
||||||
function isVendorOrUserFile(filePath) {
|
|
||||||
const rel = filePath.replace(SRC_MEDIA + path.sep, '');
|
|
||||||
return rel.startsWith('vendor' + path.sep)
|
|
||||||
|| path.basename(filePath).startsWith('user.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Pair detection
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the asset manifest and return an array of { src, dest, type } pairs
|
|
||||||
* where dest is a minified version of src that doesn't already exist or is
|
|
||||||
* older than src.
|
|
||||||
*
|
|
||||||
* Pairing logic: for every non-.min asset, check whether the manifest also
|
|
||||||
* contains a corresponding .min asset. If so, that's our pair.
|
|
||||||
*/
|
|
||||||
function detectPairs(assets) {
|
|
||||||
// Build a lookup of all URIs in the manifest.
|
|
||||||
const uriSet = new Set(assets.map(a => a.uri));
|
|
||||||
|
|
||||||
const pairs = [];
|
|
||||||
|
|
||||||
for (const asset of assets) {
|
|
||||||
const { uri, type } = asset;
|
|
||||||
if (type !== 'style' && type !== 'script') continue;
|
|
||||||
|
|
||||||
// Skip already-minified entries.
|
|
||||||
if (/\.min\.(css|js)$/.test(uri)) continue;
|
|
||||||
|
|
||||||
// Derive the expected .min URI.
|
|
||||||
const minUri = uri.replace(/\.(css|js)$/, '.min.$1');
|
|
||||||
if (!uriSet.has(minUri)) continue;
|
|
||||||
|
|
||||||
const srcPath = uriToPath(uri);
|
|
||||||
const destPath = uriToPath(minUri);
|
|
||||||
if (!srcPath || !destPath) continue;
|
|
||||||
|
|
||||||
if (isVendorOrUserFile(srcPath)) continue;
|
|
||||||
|
|
||||||
if (!fs.existsSync(srcPath)) {
|
|
||||||
console.warn(` [skip] source missing: ${srcPath}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
pairs.push({ src: srcPath, dest: destPath, type });
|
|
||||||
}
|
|
||||||
|
|
||||||
return pairs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Minifiers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function minifyCSS(srcPath, destPath) {
|
|
||||||
const source = fs.readFileSync(srcPath, 'utf8');
|
|
||||||
const result = new CleanCSS({ level: 2, returnPromise: true });
|
|
||||||
const output = await result.minify(source);
|
|
||||||
|
|
||||||
if (output.errors && output.errors.length) {
|
|
||||||
throw new Error(output.errors.join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
||||||
fs.writeFileSync(destPath, output.styles, 'utf8');
|
|
||||||
|
|
||||||
const srcSize = Buffer.byteLength(source, 'utf8');
|
|
||||||
const destSize = Buffer.byteLength(output.styles, 'utf8');
|
|
||||||
const saving = (100 - (destSize / srcSize * 100)).toFixed(1);
|
|
||||||
|
|
||||||
return { srcSize, destSize, saving };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function minifyJS(srcPath, destPath) {
|
|
||||||
const source = fs.readFileSync(srcPath, 'utf8');
|
|
||||||
const result = await terserMinify(source, {
|
|
||||||
compress: { drop_console: false },
|
|
||||||
mangle: true,
|
|
||||||
format: { comments: false }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.code) throw new Error('terser returned no output');
|
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
||||||
fs.writeFileSync(destPath, result.code, 'utf8');
|
|
||||||
|
|
||||||
const srcSize = Buffer.byteLength(source, 'utf8');
|
|
||||||
const destSize = Buffer.byteLength(result.code, 'utf8');
|
|
||||||
const saving = (100 - (destSize / srcSize * 100)).toFixed(1);
|
|
||||||
|
|
||||||
return { srcSize, destSize, saving };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Main
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const manifest = JSON.parse(fs.readFileSync(ASSET_JSON, 'utf8'));
|
|
||||||
const pairs = detectPairs(manifest.assets);
|
|
||||||
|
|
||||||
if (pairs.length === 0) {
|
|
||||||
console.log('No pairs found — nothing to minify.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nMinifying ${pairs.length} file(s)...\n`);
|
|
||||||
|
|
||||||
let ok = 0, fail = 0;
|
|
||||||
|
|
||||||
for (const { src, dest, type } of pairs) {
|
|
||||||
const label = path.relative(ROOT, src);
|
|
||||||
process.stdout.write(` ${label} ... `);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stats = type === 'style'
|
|
||||||
? await minifyCSS(src, dest)
|
|
||||||
: await minifyJS(src, dest);
|
|
||||||
|
|
||||||
const kb = n => (n / 1024).toFixed(1) + ' kB';
|
|
||||||
console.log(`${kb(stats.srcSize)} → ${kb(stats.destSize)} (${stats.saving}% saved)`);
|
|
||||||
ok++;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`FAILED\n ${err.message}`);
|
|
||||||
fail++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nDone. ${ok} succeeded, ${fail} failed.\n`);
|
|
||||||
if (fail > 0) process.exit(1);
|
|
||||||
})();
|
|
||||||
@@ -448,7 +448,10 @@ $wa->useScript('user.js'); // js/user.js
|
|||||||
</nav>
|
</nav>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($this->countModules('search', true)) : ?>
|
<?php if ($this->countModules('search', true)) : ?>
|
||||||
<div class="container-search">
|
<button class="search-toggler d-lg-none" type="button" data-bs-toggle="collapse" data-bs-target="#headerSearchCollapse" aria-controls="headerSearchCollapse" aria-expanded="false" aria-label="<?php echo Text::_('JSEARCH_FILTER_SUBMIT'); ?>">
|
||||||
|
<span class="fa-solid fa-magnifying-glass" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div class="container-search collapse d-lg-block" id="headerSearchCollapse">
|
||||||
<jdoc:include type="modules" name="search" style="none" />
|
<jdoc:include type="modules" name="search" style="none" />
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -14477,13 +14477,20 @@ li.current a {
|
|||||||
min-width: 12rem;
|
min-width: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-header .navbar-toggler {
|
.container-header .navbar-toggler,
|
||||||
|
.container-header .search-toggler {
|
||||||
color: var(--mainmenu-nav-link-color, #fff);
|
color: var(--mainmenu-nav-link-color, #fff);
|
||||||
border-color: var(--mainmenu-nav-link-color, #fff);
|
border-color: var(--mainmenu-nav-link-color, #fff);
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container-header .search-toggler {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.container-header .container-search {
|
.container-header .container-search {
|
||||||
margin-top: 0.75em;
|
margin-top: 0.75em;
|
||||||
}
|
}
|
||||||
@@ -18665,6 +18672,10 @@ nav[data-toggle=toc] .nav-link.active+ul{
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container-header .container-search.collapse:not(.show) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mod-finder__search.input-group {
|
.mod-finder__search.input-group {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
@@ -18683,8 +18694,8 @@ nav[data-toggle=toc] .nav-link.active+ul{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container-header .container-search {
|
.container-header .container-search {
|
||||||
flex: 0 0 25%;
|
flex: 0 0 16.667%;
|
||||||
max-width: 25%;
|
max-width: 16.667%;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user