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:
2026-04-07 18:49:25 -05:00
parent 516d852ea8
commit f5cca3487b
5 changed files with 22 additions and 327 deletions

View File

@@ -13,13 +13,14 @@
BRIEF: Documentation for MokoCassiopeia template
-->
# README - MokoCassiopeia (VERSION: 03.09.03)
# MokoCassiopeia
**A Modern, Lightweight Joomla Template Based on Cassiopeia**
[![Version](https://img.shields.io/badge/version-03.09.04-green.svg)](https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Joomla](https://img.shields.io/badge/Joomla-4.4.x%20%7C%205.x-blue.svg)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.0%2B-blue.svg)](https://www.php.net)
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-blue.svg)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-blue.svg)](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.

View File

@@ -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 ""

View File

@@ -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);
})();

View File

@@ -448,7 +448,10 @@ $wa->useScript('user.js'); // js/user.js
</nav>
<?php endif; ?>
<?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" />
</div>
<?php endif; ?>

View File

@@ -14477,13 +14477,20 @@ li.current a {
min-width: 12rem;
}
.container-header .navbar-toggler {
.container-header .navbar-toggler,
.container-header .search-toggler {
color: var(--mainmenu-nav-link-color, #fff);
border-color: var(--mainmenu-nav-link-color, #fff);
font-size: 1.25rem;
cursor: pointer;
}
.container-header .search-toggler {
background: none;
border: none;
padding: 0.25rem 0.5rem;
}
.container-header .container-search {
margin-top: 0.75em;
}
@@ -18665,6 +18672,10 @@ nav[data-toggle=toc] .nav-link.active+ul{
margin-top: 0.5rem;
}
.container-header .container-search.collapse:not(.show) {
display: none;
}
.mod-finder__search.input-group {
max-width: 100%;
}
@@ -18683,8 +18694,8 @@ nav[data-toggle=toc] .nav-link.active+ul{
}
.container-header .container-search {
flex: 0 0 25%;
max-width: 25%;
flex: 0 0 16.667%;
max-width: 16.667%;
margin-top: 0;
}
}