From f5cca3487bd7db723cf19220749716374af626c9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:49:25 -0500 Subject: [PATCH 01/21] 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) --- README.md | 7 +- scripts/build-release.sh | 132 -------------------------- scripts/minify.js | 188 ------------------------------------- src/index.php | 5 +- src/media/css/template.css | 17 +++- 5 files changed, 22 insertions(+), 327 deletions(-) delete mode 100755 scripts/build-release.sh delete mode 100644 scripts/minify.js diff --git a/README.md b/README.md index 3bf465f..e26e701 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/scripts/build-release.sh b/scripts/build-release.sh deleted file mode 100755 index 5e8055d..0000000 --- a/scripts/build-release.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env bash -# Copyright (C) 2026 Moko Consulting -# 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 '\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 "" diff --git a/scripts/minify.js b/scripts/minify.js deleted file mode 100644 index b6116e8..0000000 --- a/scripts/minify.js +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env node -/* Copyright (C) 2026 Moko Consulting - * - * 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); -})(); diff --git a/src/index.php b/src/index.php index 4fc14e0..5a5abf0 100644 --- a/src/index.php +++ b/src/index.php @@ -448,7 +448,10 @@ $wa->useScript('user.js'); // js/user.js countModules('search', true)) : ?> -