diff --git a/Makefile b/Makefile
index ec94cc9..dc8ca18 100644
--- a/Makefile
+++ b/Makefile
@@ -1,58 +1,32 @@
-# Makefile for Joomla Extensions
+# Makefile for MokoCassiopeia Joomla Template
# Copyright (C) 2026 Moko Consulting
# SPDX-License-Identifier: GPL-3.0-or-later
#
-# This is a reference Makefile for building Joomla extensions.
-# Copy this to your repository root as "Makefile" and customize as needed.
-#
-# Supports: Modules, Plugins, Components, Packages, Templates
+# Build and validation powered by MokoStandards Enterprise API
+# Install: composer install
# ==============================================================================
-# CONFIGURATION - Customize these for your extension
+# CONFIGURATION
# ==============================================================================
-# Extension Configuration
-EXTENSION_NAME := mokoexample
-EXTENSION_TYPE := module
-# Options: module, plugin, component, package, template
-EXTENSION_VERSION := 1.0.0
+EXTENSION_NAME := mokocassiopeia
+EXTENSION_TYPE := template
+EXTENSION_VERSION := $(shell grep -oP 'VERSION:\s*\K[0-9.]+' README.md 2>/dev/null || echo "0.0.0")
-# Module Configuration (for modules only)
-MODULE_TYPE := site
-# Options: site, admin
-
-# Plugin Configuration (for plugins only)
-PLUGIN_GROUP := system
-# Options: system, content, user, authentication, etc.
-
-# Directories
-SRC_DIR := .
+SRC_DIR := src
BUILD_DIR := build
-DIST_DIR := dist
-DOCS_DIR := docs
+DIST_DIR := dist
-# Joomla Installation (for local testing - customize paths)
-JOOMLA_ROOT := /var/www/html/joomla
-JOOMLA_VERSION := 4
-
-# Tools
-PHP := php
+PHP := php
COMPOSER := composer
-NPM := npm
-PHPCS := vendor/bin/phpcs
-PHPCBF := vendor/bin/phpcbf
-PHPUNIT := vendor/bin/phpunit
-ZIP := zip
+MOKO := vendor/bin/moko
-# Coding Standards
-PHPCS_STANDARD := Joomla
-
-# Colors for output
-COLOR_RESET := \033[0m
-COLOR_GREEN := \033[32m
+# Colors
+COLOR_RESET := \033[0m
+COLOR_GREEN := \033[32m
COLOR_YELLOW := \033[33m
-COLOR_BLUE := \033[34m
-COLOR_RED := \033[31m
+COLOR_BLUE := \033[34m
+COLOR_RED := \033[31m
# ==============================================================================
# TARGETS
@@ -61,92 +35,85 @@ COLOR_RED := \033[31m
.PHONY: help
help: ## Show this help message
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
- @echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)"
+ @echo "$(COLOR_BLUE)║ MokoCassiopeia Template Build ║$(COLOR_RESET)"
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
@echo ""
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
+ @echo "Powered by: MokoStandards Enterprise API"
@echo ""
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
@echo ""
- @echo "$(COLOR_YELLOW)Quick Start:$(COLOR_RESET)"
- @echo " 1. make install-deps # Install dependencies"
- @echo " 2. make build # Build extension package"
- @echo " 3. make test # Run tests"
- @echo ""
+
+# ── Dependencies ──────────────────────────────────────────────────────────────
.PHONY: install-deps
-install-deps: ## Install all dependencies (Composer + npm)
+install-deps: ## Install Composer dependencies (includes MokoStandards API)
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
- @if [ -f "composer.json" ]; then \
- $(COMPOSER) install; \
- echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
- fi
- @if [ -f "package.json" ]; then \
- $(NPM) install; \
- echo "$(COLOR_GREEN)✓ npm dependencies installed$(COLOR_RESET)"; \
- fi
+ @$(COMPOSER) install
+ @echo "$(COLOR_GREEN)✓ Dependencies installed$(COLOR_RESET)"
.PHONY: update-deps
-update-deps: ## Update all dependencies
+update-deps: ## Update Composer dependencies
@echo "$(COLOR_BLUE)Updating dependencies...$(COLOR_RESET)"
- @if [ -f "composer.json" ]; then \
- $(COMPOSER) update; \
- echo "$(COLOR_GREEN)✓ Composer dependencies updated$(COLOR_RESET)"; \
- fi
- @if [ -f "package.json" ]; then \
- $(NPM) update; \
- echo "$(COLOR_GREEN)✓ npm dependencies updated$(COLOR_RESET)"; \
+ @$(COMPOSER) update
+ @echo "$(COLOR_GREEN)✓ Dependencies updated$(COLOR_RESET)"
+
+# ── Validation (MokoStandards API) ────────────────────────────────────────────
+
+.PHONY: check-moko
+check-moko:
+ @if [ ! -f "$(MOKO)" ]; then \
+ echo "$(COLOR_RED)✗ MokoStandards CLI not found. Run: make install-deps$(COLOR_RESET)"; \
+ exit 1; \
fi
.PHONY: lint
-lint: ## Run PHP linter (syntax check)
- @echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
- @find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \
- -exec $(PHP) -l {} \; | grep -v "No syntax errors" || true
- @echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
+lint: check-moko ## PHP syntax check via MokoStandards
+ @echo "$(COLOR_BLUE)Running PHP syntax check...$(COLOR_RESET)"
+ @$(PHP) $(MOKO) check:syntax -- --path .
+ @echo "$(COLOR_GREEN)✓ PHP syntax OK$(COLOR_RESET)"
-.PHONY: phpcs
-phpcs: ## Run PHP CodeSniffer (Joomla standards)
- @echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
- @if [ -f "$(PHPCS)" ]; then \
- $(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \
- else \
- echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \
- fi
+.PHONY: check-joomla
+check-joomla: check-moko ## Validate Joomla manifest via MokoStandards
+ @echo "$(COLOR_BLUE)Validating Joomla manifest...$(COLOR_RESET)"
+ @$(PHP) $(MOKO) check:joomla -- --path .
+ @echo "$(COLOR_GREEN)✓ Joomla manifest valid$(COLOR_RESET)"
-.PHONY: phpcbf
-phpcbf: ## Fix coding standards automatically
- @echo "$(COLOR_BLUE)Running PHP Code Beautifier...$(COLOR_RESET)"
- @if [ -f "$(PHPCBF)" ]; then \
- $(PHPCBF) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \
- echo "$(COLOR_GREEN)✓ Code formatting applied$(COLOR_RESET)"; \
- else \
- echo "$(COLOR_YELLOW)⚠ PHP Code Beautifier not installed. Run: make install-deps$(COLOR_RESET)"; \
- fi
+.PHONY: check-version
+check-version: check-moko ## Verify version consistency across files
+ @echo "$(COLOR_BLUE)Checking version consistency...$(COLOR_RESET)"
+ @$(PHP) $(MOKO) check:version -- --path .
+ @echo "$(COLOR_GREEN)✓ Versions consistent$(COLOR_RESET)"
+
+.PHONY: check-headers
+check-headers: check-moko ## Check license headers on source files
+ @echo "$(COLOR_BLUE)Checking license headers...$(COLOR_RESET)"
+ @$(PHP) $(MOKO) check:headers -- --path .
+ @echo "$(COLOR_GREEN)✓ Headers OK$(COLOR_RESET)"
+
+.PHONY: check-secrets
+check-secrets: check-moko ## Scan for leaked credentials
+ @echo "$(COLOR_BLUE)Scanning for secrets...$(COLOR_RESET)"
+ @$(PHP) $(MOKO) check:secrets -- --path .
+ @echo "$(COLOR_GREEN)✓ No secrets found$(COLOR_RESET)"
+
+.PHONY: check-xml
+check-xml: check-moko ## Validate XML files are well-formed
+ @echo "$(COLOR_BLUE)Checking XML files...$(COLOR_RESET)"
+ @$(PHP) $(MOKO) check:xml -- --path .
+ @echo "$(COLOR_GREEN)✓ XML well-formed$(COLOR_RESET)"
.PHONY: validate
-validate: lint phpcs ## Run all validation checks
+validate: lint check-joomla check-version check-xml check-headers check-secrets ## Run all MokoStandards validation checks
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
-.PHONY: test
-test: ## Run PHPUnit tests
- @echo "$(COLOR_BLUE)Running tests...$(COLOR_RESET)"
- @if [ -f "$(PHPUNIT)" ] && [ -f "phpunit.xml" ]; then \
- $(PHPUNIT); \
- else \
- echo "$(COLOR_YELLOW)⚠ PHPUnit not configured$(COLOR_RESET)"; \
- fi
+.PHONY: health
+health: check-moko ## Full repository health check via MokoStandards
+ @echo "$(COLOR_BLUE)Running full health check...$(COLOR_RESET)"
+ @$(PHP) $(MOKO) health -- --path .
-.PHONY: test-coverage
-test-coverage: ## Run tests with coverage report
- @echo "$(COLOR_BLUE)Running tests with coverage...$(COLOR_RESET)"
- @if [ -f "$(PHPUNIT)" ] && [ -f "phpunit.xml" ]; then \
- $(PHPUNIT) --coverage-html $(BUILD_DIR)/coverage; \
- echo "$(COLOR_GREEN)✓ Coverage report: $(BUILD_DIR)/coverage/index.html$(COLOR_RESET)"; \
- else \
- echo "$(COLOR_YELLOW)⚠ PHPUnit not configured$(COLOR_RESET)"; \
- fi
+# ── Build ─────────────────────────────────────────────────────────────────────
.PHONY: clean
clean: ## Clean build artifacts
@@ -155,179 +122,78 @@ clean: ## Clean build artifacts
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
.PHONY: build
-build: clean validate ## Build extension package
- @echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
- @mkdir -p $(DIST_DIR) $(BUILD_DIR)
-
- # Determine package prefix based on extension type
- @case "$(EXTENSION_TYPE)" in \
- module) \
- PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
- BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
- ;; \
- plugin) \
- PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
- BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
- ;; \
- component) \
- PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
- BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
- ;; \
- package) \
- PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
- BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
- ;; \
- template) \
- PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
- BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
- ;; \
- *) \
- echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
+build: clean ## Build template installable ZIP from src/
+ @echo "$(COLOR_BLUE)Building $(EXTENSION_NAME) v$(EXTENSION_VERSION)...$(COLOR_RESET)"
+ @mkdir -p $(BUILD_DIR)/package $(DIST_DIR)
+ @cp -r $(SRC_DIR)/* $(BUILD_DIR)/package/
+ @cd $(BUILD_DIR)/package && \
+ if command -v zip >/dev/null 2>&1; then \
+ zip -r "../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" .; \
+ elif command -v pwsh >/dev/null 2>&1; then \
+ pwsh -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip' -Force"; \
+ elif command -v powershell >/dev/null 2>&1; then \
+ powershell -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip' -Force"; \
+ else \
+ echo "$(COLOR_RED)✗ No zip tool found (zip, pwsh, powershell)$(COLOR_RESET)"; \
exit 1; \
- ;; \
- esac; \
- \
- mkdir -p "$$BUILD_TARGET"; \
- \
- echo "Building $$PACKAGE_PREFIX..."; \
- \
- rsync -av --progress \
- --exclude='$(BUILD_DIR)' \
- --exclude='$(DIST_DIR)' \
- --exclude='.git*' \
- --exclude='vendor/' \
- --exclude='node_modules/' \
- --exclude='tests/' \
- --exclude='Makefile' \
- --exclude='composer.json' \
- --exclude='composer.lock' \
- --exclude='package.json' \
- --exclude='package-lock.json' \
- --exclude='phpunit.xml' \
- --exclude='*.md' \
- --exclude='.editorconfig' \
- . "$$BUILD_TARGET/"; \
- \
- cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
- \
- echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
+ fi
+ @echo "$(COLOR_GREEN)✓ Package: $(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
-.PHONY: package
-package: build ## Alias for build
- @echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)"
-
-.PHONY: install-local
-install-local: build ## Install to local Joomla (upload via admin)
- @echo "$(COLOR_BLUE)Package ready for installation$(COLOR_RESET)"
- @case "$(EXTENSION_TYPE)" in \
- module) PACKAGE="mod_$(EXTENSION_NAME)";; \
- plugin) PACKAGE="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)";; \
- component) PACKAGE="com_$(EXTENSION_NAME)";; \
- package) PACKAGE="pkg_$(EXTENSION_NAME)";; \
- template) PACKAGE="tpl_$(EXTENSION_NAME)";; \
- esac; \
- echo "$(COLOR_YELLOW)Upload $(DIST_DIR)/$${PACKAGE}-$(EXTENSION_VERSION).zip via Joomla Administrator$(COLOR_RESET)"; \
- echo "Admin URL: $(JOOMLA_ROOT) → Extensions → Install"
-
-.PHONY: dev-install
-dev-install: ## Create symlink for development (Joomla 4+)
- @echo "$(COLOR_BLUE)Creating development symlink...$(COLOR_RESET)"
- @if [ ! -d "$(JOOMLA_ROOT)" ]; then \
- echo "$(COLOR_RED)✗ Joomla root not found at $(JOOMLA_ROOT)$(COLOR_RESET)"; \
- echo "Update JOOMLA_ROOT in Makefile"; \
- exit 1; \
- fi
-
- @case "$(EXTENSION_TYPE)" in \
- module) \
- if [ "$(MODULE_TYPE)" = "admin" ]; then \
- TARGET="$(JOOMLA_ROOT)/administrator/modules/mod_$(EXTENSION_NAME)"; \
- else \
- TARGET="$(JOOMLA_ROOT)/modules/mod_$(EXTENSION_NAME)"; \
- fi; \
- ;; \
- plugin) \
- TARGET="$(JOOMLA_ROOT)/plugins/$(PLUGIN_GROUP)/$(EXTENSION_NAME)"; \
- ;; \
- component) \
- echo "$(COLOR_YELLOW)⚠ Components require complex symlink setup$(COLOR_RESET)"; \
- echo "Manual setup recommended for component development"; \
+.PHONY: build-beta
+build-beta: clean ## Build beta release ZIP
+ @echo "$(COLOR_BLUE)Building $(EXTENSION_NAME) v$(EXTENSION_VERSION)-beta...$(COLOR_RESET)"
+ @mkdir -p $(BUILD_DIR)/package $(DIST_DIR)
+ @cp -r $(SRC_DIR)/* $(BUILD_DIR)/package/
+ @cd $(BUILD_DIR)/package && \
+ if command -v zip >/dev/null 2>&1; then \
+ zip -r "../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip" .; \
+ elif command -v pwsh >/dev/null 2>&1; then \
+ pwsh -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip' -Force"; \
+ elif command -v powershell >/dev/null 2>&1; then \
+ powershell -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip' -Force"; \
+ else \
+ echo "$(COLOR_RED)✗ No zip tool found$(COLOR_RESET)"; \
exit 1; \
- ;; \
- *) \
- echo "$(COLOR_RED)✗ dev-install not supported for $(EXTENSION_TYPE)$(COLOR_RESET)"; \
- exit 1; \
- ;; \
- esac; \
- \
- rm -rf "$$TARGET"; \
- ln -s "$(PWD)" "$$TARGET"; \
- echo "$(COLOR_GREEN)✓ Development symlink created at $$TARGET$(COLOR_RESET)"
+ fi
+ @echo "$(COLOR_GREEN)✓ Package: $(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip$(COLOR_RESET)"
-.PHONY: watch
-watch: ## Watch for changes and rebuild
- @echo "$(COLOR_BLUE)Watching for changes...$(COLOR_RESET)"
- @echo "$(COLOR_YELLOW)Press Ctrl+C to stop$(COLOR_RESET)"
- @while true; do \
- inotifywait -r -e modify,create,delete --exclude '($(BUILD_DIR)|$(DIST_DIR)|vendor|node_modules)' . 2>/dev/null || \
- (echo "$(COLOR_YELLOW)⚠ inotifywait not installed. Install: apt-get install inotify-tools$(COLOR_RESET)" && sleep 5); \
- make build; \
+.PHONY: checksum
+checksum: ## Generate SHA-256 checksums for dist packages
+ @echo "$(COLOR_BLUE)Generating checksums...$(COLOR_RESET)"
+ @for f in $(DIST_DIR)/*.zip; do \
+ sha256sum "$$f" | tee "$${f}.sha256"; \
done
+ @echo "$(COLOR_GREEN)✓ Checksums generated$(COLOR_RESET)"
+
+# ── Release ───────────────────────────────────────────────────────────────────
+
+.PHONY: release
+release: validate build checksum ## Full release pipeline (validate + build + checksum)
+ @echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
+ @echo ""
+ @echo "$(COLOR_BLUE)Next steps:$(COLOR_RESET)"
+ @echo " 1. Tag: git tag $(EXTENSION_VERSION)"
+ @echo " 2. Push: git push origin --tags"
+ @echo " 3. Create Gitea release and attach $(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip"
+ @echo ""
+
+# ── Info ──────────────────────────────────────────────────────────────────────
.PHONY: version
-version: ## Display version information
+version: ## Display version and extension info
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
@echo " Name: $(EXTENSION_NAME)"
@echo " Type: $(EXTENSION_TYPE)"
@echo " Version: $(EXTENSION_VERSION)"
- @if [ "$(EXTENSION_TYPE)" = "module" ]; then \
- echo " Module: $(MODULE_TYPE)"; \
- fi
- @if [ "$(EXTENSION_TYPE)" = "plugin" ]; then \
- echo " Group: $(PLUGIN_GROUP)"; \
- fi
-
-.PHONY: docs
-docs: ## Generate documentation
- @echo "$(COLOR_BLUE)Generating documentation...$(COLOR_RESET)"
- @mkdir -p $(DOCS_DIR)
- @echo "$(COLOR_YELLOW)⚠ Documentation generation not configured$(COLOR_RESET)"
- @echo "Consider adding phpDocumentor or similar"
-
-.PHONY: release
-release: validate test build ## Create a release (validate + test + build)
- @echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
- @echo ""
- @echo "$(COLOR_BLUE)Release Checklist:$(COLOR_RESET)"
- @echo " [ ] Update CHANGELOG.md"
- @echo " [ ] Update version in XML manifest"
- @echo " [ ] Test installation in clean Joomla"
- @echo " [ ] Tag release in git: git tag v$(EXTENSION_VERSION)"
- @echo " [ ] Push tags: git push --tags"
- @echo " [ ] Create GitHub release"
- @echo ""
- @case "$(EXTENSION_TYPE)" in \
- module) PACKAGE="mod_$(EXTENSION_NAME)";; \
- plugin) PACKAGE="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)";; \
- component) PACKAGE="com_$(EXTENSION_NAME)";; \
- package) PACKAGE="pkg_$(EXTENSION_NAME)";; \
- template) PACKAGE="tpl_$(EXTENSION_NAME)";; \
- esac; \
- echo "$(COLOR_GREEN)Package: $(DIST_DIR)/$${PACKAGE}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
.PHONY: security-check
-security-check: ## Run security checks on dependencies
+security-check: ## Run Composer security audit
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
- @if [ -f "composer.json" ]; then \
- $(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
- fi
- @if [ -f "package.json" ]; then \
- $(NPM) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
- fi
+ @$(COMPOSER) audit
+ @echo "$(COLOR_GREEN)✓ Security check complete$(COLOR_RESET)"
.PHONY: all
-all: install-deps validate test build ## Run complete build pipeline
+all: install-deps validate build checksum ## Full pipeline: deps → validate → build → checksum
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
-# Default target
.DEFAULT_GOAL := help
diff --git a/README.md b/README.md
index f120c8e..4b0816d 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
INGROUP: MokoCassiopeia.Documentation
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
FILE: ./README.md
- VERSION: 03.09.13
+ VERSION: 03.09.14
BRIEF: Documentation for MokoCassiopeia template
-->
diff --git a/composer.json b/composer.json
index 8b13789..5f32b61 100644
--- a/composer.json
+++ b/composer.json
@@ -1 +1,29 @@
-
+{
+ "name": "mokoconsulting/mokocassiopeia",
+ "description": "MokoCassiopeia — Joomla site template based on Cassiopeia",
+ "type": "joomla-template",
+ "license": "GPL-3.0-or-later",
+ "authors": [
+ {
+ "name": "Jonathan Miller",
+ "email": "hello@mokoconsulting.tech"
+ }
+ ],
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "mokoconsulting-tech/enterprise": "^4.0"
+ },
+ "config": {
+ "sort-packages": true,
+ "optimize-autoloader": true,
+ "preferred-install": "dist"
+ },
+ "repositories": [
+ {
+ "type": "composer",
+ "url": "https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer"
+ }
+ ]
+}
diff --git a/src/joomla.asset.json b/src/joomla.asset.json
index ff60ae1..737a35c 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.09.02",
+ "version": "03.09.14",
"brief": "Joomla asset registry for MokoCassiopeia"
}
},
@@ -124,6 +124,12 @@
"uri": "media/templates/site/mokocassiopeia/css/theme/dark.custom.min.css",
"attributes": {"media": "all"}
},
+ {
+ "name": "template.a11y-high-contrast",
+ "type": "style",
+ "uri": "media/templates/site/mokocassiopeia/css/a11y-high-contrast.css",
+ "attributes": {"media": "all"}
+ },
{
"name": "template.js",
"type": "script",
diff --git a/src/media/css/a11y-high-contrast.css b/src/media/css/a11y-high-contrast.css
new file mode 100644
index 0000000..10606fa
--- /dev/null
+++ b/src/media/css/a11y-high-contrast.css
@@ -0,0 +1,227 @@
+@charset "UTF-8";
+/* 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.Accessibility
+ * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia
+ * PATH: ./media/css/a11y-high-contrast.css
+ * VERSION: 03.09.14
+ * BRIEF: High-contrast stylesheet for accessibility toolbar
+ */
+
+/* ===================================================================
+ * HIGH CONTRAST MODE
+ * Applied when .a11y-high-contrast is on .
+ * Overrides theme variables to maximise contrast ratios (WCAG AAA).
+ * =================================================================== */
+
+/* ── Light mode high contrast ─────────────────────────────────────── */
+:root[data-bs-theme="light"].a11y-high-contrast {
+ --body-color: #000;
+ --body-color-rgb: 0, 0, 0;
+ --body-bg: #fff;
+ --body-bg-rgb: 255, 255, 255;
+ --heading-color: #000;
+ --emphasis-color: #000;
+ --secondary-color: #000000bf;
+ --tertiary-color: #00000080;
+ --muted-color: #333;
+
+ /* Links — strong blue on white */
+ --color-link: #0000ee;
+ --link-color: #0000ee;
+ --link-color-rgb: 0, 0, 238;
+ --color-hover: #cc0000;
+ --link-hover-color: #cc0000;
+ --link-hover-color-rgb: 204, 0, 0;
+
+ /* Borders — visible on all backgrounds */
+ --border-color: #000;
+ --border-color-soft: #333;
+
+ /* Backgrounds */
+ --secondary-bg: #e0e0e0;
+ --secondary-bg-rgb: 224, 224, 224;
+ --tertiary-bg: #f0f0f0;
+ --tertiary-bg-rgb: 240, 240, 240;
+
+ /* Navigation */
+ --nav-bg-color: #000;
+ --nav-text-color: #fff;
+ --mainmenu-nav-link-color: #fff;
+
+ /* Buttons */
+ --btn-color: #fff;
+ --btn-bg: #000;
+ --btn-border-color: #000;
+ --btn-hover-color: #000;
+ --btn-hover-bg: #ffff00;
+ --btn-hover-border-color: #000;
+ --btn-active-color: #000;
+ --btn-active-bg: #ffff00;
+ --btn-active-border-color: #000;
+
+ /* Forms */
+ --input-color: #000;
+ --input-bg: #fff;
+ --input-border-color: #000;
+ --input-focus-color: #000;
+ --input-focus-bg: #ffffcc;
+ --input-focus-border-color: #0000ee;
+ --input-placeholder-color: #555;
+
+ /* Cards */
+ --card-border-color: #000;
+ --card-bg: #fff;
+ --card-cap-bg: #e0e0e0;
+
+ /* Tables */
+ --table-color: #000;
+ --table-bg: #fff;
+ --table-border-color: #000;
+ --table-striped-bg: #f0f0f0;
+ --table-hover-bg: #ffff99;
+
+ /* Alerts */
+ --alert-border-width: 2px;
+
+ /* Code */
+ --code-color: #000;
+ --code-bg-color: #ffffcc;
+
+ /* Selection */
+ --selection-bg: #0000ee;
+ --selection-ink: #fff;
+
+ /* Focus indicator — always visible */
+ --focus-ring-color: #0000ee;
+ --focus-ring-width: 3px;
+}
+
+/* ── Dark mode high contrast ──────────────────────────────────────── */
+:root[data-bs-theme="dark"].a11y-high-contrast {
+ --body-color: #fff;
+ --body-color-rgb: 255, 255, 255;
+ --body-bg: #000;
+ --body-bg-rgb: 0, 0, 0;
+ --heading-color: #fff;
+ --emphasis-color: #fff;
+ --secondary-color: #ffffffbf;
+ --tertiary-color: #ffffff80;
+ --muted-color: #ccc;
+
+ /* Links — bright yellow on black */
+ --color-link: #ffff00;
+ --link-color: #ffff00;
+ --link-color-rgb: 255, 255, 0;
+ --color-hover: #00ffff;
+ --link-hover-color: #00ffff;
+ --link-hover-color-rgb: 0, 255, 255;
+
+ /* Borders */
+ --border-color: #fff;
+ --border-color-soft: #ccc;
+
+ /* Backgrounds */
+ --secondary-bg: #1a1a1a;
+ --secondary-bg-rgb: 26, 26, 26;
+ --tertiary-bg: #111;
+ --tertiary-bg-rgb: 17, 17, 17;
+
+ /* Navigation */
+ --nav-bg-color: #000;
+ --nav-text-color: #fff;
+ --mainmenu-nav-link-color: #ffff00;
+
+ /* Buttons */
+ --btn-color: #000;
+ --btn-bg: #ffff00;
+ --btn-border-color: #ffff00;
+ --btn-hover-color: #000;
+ --btn-hover-bg: #00ffff;
+ --btn-hover-border-color: #00ffff;
+ --btn-active-color: #000;
+ --btn-active-bg: #00ffff;
+ --btn-active-border-color: #00ffff;
+
+ /* Forms */
+ --input-color: #fff;
+ --input-bg: #000;
+ --input-border-color: #fff;
+ --input-focus-color: #fff;
+ --input-focus-bg: #1a1a1a;
+ --input-focus-border-color: #ffff00;
+ --input-placeholder-color: #aaa;
+
+ /* Cards */
+ --card-border-color: #fff;
+ --card-bg: #000;
+ --card-cap-bg: #1a1a1a;
+
+ /* Tables */
+ --table-color: #fff;
+ --table-bg: #000;
+ --table-border-color: #fff;
+ --table-striped-bg: #111;
+ --table-hover-bg: #333;
+
+ /* Alerts */
+ --alert-border-width: 2px;
+
+ /* Code */
+ --code-color: #00ff00;
+ --code-bg-color: #1a1a1a;
+
+ /* Selection */
+ --selection-bg: #ffff00;
+ --selection-ink: #000;
+
+ /* Focus indicator */
+ --focus-ring-color: #ffff00;
+ --focus-ring-width: 3px;
+}
+
+/* ── Shared high-contrast overrides (both modes) ──────────────────── */
+.a11y-high-contrast * {
+ border-color: var(--border-color) !important;
+}
+
+.a11y-high-contrast *:focus-visible {
+ outline: var(--focus-ring-width, 3px) solid var(--focus-ring-color, #0000ee) !important;
+ outline-offset: 2px !important;
+}
+
+.a11y-high-contrast img {
+ outline: 2px solid var(--border-color);
+}
+
+.a11y-high-contrast a {
+ text-decoration: underline !important;
+ text-decoration-thickness: 2px !important;
+}
+
+.a11y-high-contrast button,
+.a11y-high-contrast .btn,
+.a11y-high-contrast input,
+.a11y-high-contrast select,
+.a11y-high-contrast textarea {
+ border-width: 2px !important;
+ border-style: solid !important;
+}
+
+.a11y-high-contrast .badge,
+.a11y-high-contrast .alert {
+ border: 2px solid var(--border-color) !important;
+}
+
+/* Ensure disabled states are still distinguishable */
+.a11y-high-contrast [disabled],
+.a11y-high-contrast .disabled {
+ opacity: .5 !important;
+ text-decoration: line-through !important;
+}
diff --git a/src/media/css/template.css b/src/media/css/template.css
index 2251e3f..5b82fc3 100644
--- a/src/media/css/template.css
+++ b/src/media/css/template.css
@@ -13780,14 +13780,6 @@ meter {
margin-left: auto;
}
-.page-link {
- color: var(--color-link, white);
-}
-
-.page-link:active {
- color: var(--color-link, white);
-}
-
.pager .pagination {
-webkit-box-pack: center;
-ms-flex-pack: center;
@@ -13987,10 +13979,9 @@ meter {
}
.footer {
- padding-top: 1rem;
- color: var(--body-bg, #e6ebf1);
+ color: var(--mainmenu-nav-link-color, #fff);
background-color: var(--nav-bg-color);
- padding-bottom: 80px;
+ padding-bottom: var(--footer-padding-bottom, 80px);
}
.footer .grid-child {
@@ -14002,7 +13993,6 @@ meter {
-ms-flex-direction: column;
flex-direction: column;
width: 100%;
- padding: 2.5rem 0.5em;
}
.footer a {
@@ -14248,8 +14238,8 @@ fieldset>* {
.container-header .navbar-brand {
position: relative;
display: inline-block;
- padding-top: 0.3125rem;
- padding-bottom: 0.3125rem;
+ padding-top: var(--navbar-brand-padding-y, 0.3125rem);
+ padding-bottom: var(--navbar-brand-padding-y, 0.3125rem);
font-size: 2rem;
color: var(--nav-text-color, gray);
-webkit-margin-end: auto;
@@ -15781,7 +15771,7 @@ body.wrapper-fluid header>.grid-child {
}
footer .grid-child>div {
- padding: 1rem 0 0;
+ padding: var(--navbar-padding-y, 1rem) var(--navbar-padding-x, 1rem) 0;
}
header .grid-child .navbar-brand {
@@ -17080,8 +17070,8 @@ form .form-select {
background: var(--muted-color, #6d757e);
box-shadow: var(--box-shadow, 0 .5rem 1rem #00000066);
font: inherit;
- color: var(--body-bg, #0e1318);
- font-weight: 600px;
+ color: #fff;
+ font-weight: 600;
}
#mokoThemeFab.pos-br {
@@ -17143,10 +17133,112 @@ button#mokoThemeSwitch {
#mokoThemeFab .label {
user-select: none;
font-size: .875rem;
+ color: #fff;
}
#mokoThemeFab button {
- color: var(--body-bg, #0e1318);
+ color: #fff;
+}
+
+/* Auto toggle switch (on/off style) */
+.auto-toggle-wrap {
+ display: flex;
+ align-items: center;
+ gap: .35rem;
+}
+
+.auto-label {
+ font-size: .75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: .04em;
+ opacity: .7;
+ user-select: none;
+}
+
+.auto-switch {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ width: 32px;
+ height: 18px;
+ border: none;
+ border-radius: 999px;
+ background: var(--secondary-color, #6c757d);
+ cursor: pointer;
+ padding: 0;
+ transition: background .2s;
+}
+
+.auto-switch.on {
+ background: var(--link-color, #3565e5);
+}
+
+.auto-track {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+.auto-knob {
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 1px 3px rgba(0,0,0,.3);
+ transition: transform .2s ease;
+}
+
+.auto-switch.on .auto-knob {
+ transform: translateX(14px);
+}
+
+/* FAB divider between theme controls and a11y */
+.fab-divider {
+ display: block;
+ width: 1px;
+ height: 24px;
+ background: currentColor;
+ opacity: .25;
+ margin: 0 .15rem;
+}
+
+/* Inline a11y toggle inside theme FAB */
+.a11y-toggle-inline {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: 1.5px solid currentColor;
+ background: transparent;
+ color: inherit;
+ font-size: 1rem;
+ cursor: pointer;
+ padding: 0;
+ transition: background .2s, color .2s;
+ opacity: .8;
+}
+
+.a11y-toggle-inline:hover,
+.a11y-toggle-inline:focus-visible {
+ opacity: 1;
+ background: rgba(255,255,255,.15);
+}
+
+.a11y-toggle-inline.active {
+ opacity: 1;
+ background: rgba(255,255,255,.25);
+}
+
+/* Floating a11y panel when inline */
+.a11y-toolbar-floating {
+ position: fixed;
+ z-index: 1202;
}
body.site.error-page {
@@ -17372,11 +17464,9 @@ html.a11y-pause-animations *::after {
right: 2.5rem;
}
-/* When theme FAB is present, sit a11y toolbar to its left */
+/* When theme FAB is present, a11y toggle is inline — hide standalone toolbar positioning */
body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
- right: auto;
- left: 2.5rem;
- bottom: 1rem;
+ position: static;
}
/* Toggle button */
diff --git a/src/media/css/theme/dark.custom.css b/src/media/css/theme/dark.custom.css
index 50f9782..b04fd7e 100644
--- a/src/media/css/theme/dark.custom.css
+++ b/src/media/css/theme/dark.custom.css
@@ -817,6 +817,12 @@ color-scheme: dark;
--table-active-color: var(--body-color);
--table-active-bg: rgba(var(--white-rgb), 0.1);
+/* ===== FOOTER ===== */
+--footer-padding-top: 1rem;
+--footer-padding-bottom: 80px;
+--footer-grid-padding-y: 2.5rem;
+--footer-grid-padding-x: 0.5em;
+
/* ===== BACKDROP ===== */
--backdrop-zindex: 1040;
--backdrop-bg: hsl(0, 0%, 0%);
diff --git a/src/media/css/theme/dark.standard.css b/src/media/css/theme/dark.standard.css
index 97515a7..8f2a82d 100644
--- a/src/media/css/theme/dark.standard.css
+++ b/src/media/css/theme/dark.standard.css
@@ -817,6 +817,12 @@ color-scheme: dark;
--table-active-color: var(--body-color);
--table-active-bg: rgba(var(--white-rgb), 0.1);
+/* ===== FOOTER ===== */
+--footer-padding-top: 1rem;
+--footer-padding-bottom: 80px;
+--footer-grid-padding-y: 2.5rem;
+--footer-grid-padding-x: 0.5em;
+
/* ===== BACKDROP ===== */
--backdrop-zindex: 1040;
--backdrop-bg: hsl(0, 0%, 0%);
diff --git a/src/media/css/theme/light.custom.css b/src/media/css/theme/light.custom.css
index 53dc8c4..a99b9db 100644
--- a/src/media/css/theme/light.custom.css
+++ b/src/media/css/theme/light.custom.css
@@ -816,6 +816,12 @@ color-scheme: light;
--table-active-color: var(--body-color);
--table-active-bg: rgba(var(--black-rgb), 0.075);
+/* ===== FOOTER ===== */
+--footer-padding-top: 1rem;
+--footer-padding-bottom: 80px;
+--footer-grid-padding-y: 2.5rem;
+--footer-grid-padding-x: 0.5em;
+
/* ===== BACKDROP ===== */
--backdrop-zindex: 1040;
--backdrop-bg: hsl(0, 0%, 0%);
diff --git a/src/media/css/theme/light.standard.css b/src/media/css/theme/light.standard.css
index 3ca6e62..07a74c2 100644
--- a/src/media/css/theme/light.standard.css
+++ b/src/media/css/theme/light.standard.css
@@ -816,6 +816,12 @@ color-scheme: light;
--table-active-color: var(--body-color);
--table-active-bg: rgba(var(--black-rgb), 0.075);
+/* ===== FOOTER ===== */
+--footer-padding-top: 1rem;
+--footer-padding-bottom: 80px;
+--footer-grid-padding-y: 2.5rem;
+--footer-grid-padding-x: 0.5em;
+
/* ===== BACKDROP ===== */
--backdrop-zindex: 1040;
--backdrop-bg: hsl(0, 0%, 0%);
diff --git a/src/media/js/template.js b/src/media/js/template.js
index b1d85d1..f1fb442 100644
--- a/src/media/js/template.js
+++ b/src/media/js/template.js
@@ -87,13 +87,40 @@
lblD.className = 'label';
lblD.textContent = 'Dark';
- // Auto button
+ // Auto toggle (on/off switch style)
+ var autoWrap = doc.createElement('div');
+ autoWrap.className = 'auto-toggle-wrap';
+
+ var autoLabel = doc.createElement('span');
+ autoLabel.className = 'auto-label';
+ autoLabel.textContent = 'Auto';
+
var auto = doc.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.className = 'auto-switch';
+ auto.setAttribute('role', 'switch');
+ auto.setAttribute('aria-label', 'Automatic theme (follow system)');
+ auto.setAttribute('aria-checked', getStored() ? 'false' : 'true');
+
+ var autoTrack = doc.createElement('span');
+ autoTrack.className = 'auto-track';
+ var autoKnob = doc.createElement('span');
+ autoKnob.className = 'auto-knob';
+ autoTrack.appendChild(autoKnob);
+ auto.appendChild(autoTrack);
+ if (!getStored()) auto.classList.add('on');
+
+ autoWrap.appendChild(autoLabel);
+ autoWrap.appendChild(auto);
+
+ // Divider before a11y slot
+ var divider = doc.createElement('span');
+ divider.className = 'fab-divider';
+
+ // A11y slot — buildA11yToolbar will inject its toggle here
+ var a11ySlot = doc.createElement('span');
+ a11ySlot.id = 'mokoA11ySlot';
// Behavior
switchWrap.addEventListener('click', function () {
@@ -101,6 +128,9 @@
var next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
switchWrap.setAttribute('aria-checked', next === 'dark' ? 'true' : 'false');
+ // Turn off auto when manually switching
+ auto.classList.remove('on');
+ auto.setAttribute('aria-checked', 'false');
// Update meta theme color
var meta = doc.querySelector('meta[name="theme-color"]');
if (meta) {
@@ -109,10 +139,14 @@
});
auto.addEventListener('click', function () {
- clearStored();
- var sys = systemTheme();
- applyTheme(sys);
- switchWrap.setAttribute('aria-checked', sys === 'dark' ? 'true' : 'false');
+ var isAuto = auto.classList.toggle('on');
+ auto.setAttribute('aria-checked', isAuto ? 'true' : 'false');
+ if (isAuto) {
+ clearStored();
+ var sys = systemTheme();
+ applyTheme(sys);
+ switchWrap.setAttribute('aria-checked', sys === 'dark' ? 'true' : 'false');
+ }
});
// Respond to OS changes only when not user-forced
@@ -134,7 +168,9 @@
wrap.appendChild(lblL);
wrap.appendChild(switchWrap);
wrap.appendChild(lblD);
- wrap.appendChild(auto);
+ wrap.appendChild(autoWrap);
+ wrap.appendChild(divider);
+ wrap.appendChild(a11ySlot);
doc.body.appendChild(wrap);
// Debug helper
@@ -193,6 +229,18 @@
function applyContrast(on) {
root.classList.toggle("a11y-high-contrast", on);
+ // Lazy-load the high-contrast stylesheet
+ var hcId = "mokoA11yHcSheet";
+ var existing = doc.getElementById(hcId);
+ if (on && !existing) {
+ var link = doc.createElement("link");
+ link.id = hcId;
+ link.rel = "stylesheet";
+ link.href = (doc.querySelector('link[href*="mokocassiopeia/css/template"]') || {}).href
+ ? (doc.querySelector('link[href*="mokocassiopeia/css/template"]').href.replace(/\/css\/template[^/]*$/, "/css/a11y-high-contrast.css"))
+ : "media/templates/site/mokocassiopeia/css/a11y-high-contrast.css";
+ doc.head.appendChild(link);
+ }
}
function applyLinks(on) {
@@ -209,10 +257,10 @@
/** Create a Font Awesome icon element (safe DOM, no innerHTML). */
function faIcon(classes) {
- var span = doc.createElement("span");
- span.className = classes;
- span.setAttribute("aria-hidden", "true");
- return span;
+ var i = doc.createElement("i");
+ i.className = classes;
+ i.setAttribute("aria-hidden", "true");
+ return i;
}
function buildA11yToolbar() {
@@ -359,23 +407,6 @@
addSwitchOption(showFont, "font", "fa-solid fa-font", "Readable font", applyFont);
addSwitchOption(showAnimations, "paused", "fa-solid fa-pause", "Pause animations", applyPaused);
- // Toggle panel open/close
- toggle.addEventListener("click", function () {
- var isOpen = !panel.hidden;
- panel.hidden = isOpen;
- toggle.setAttribute("aria-expanded", isOpen ? "false" : "true");
- toggle.classList.toggle("active", !isOpen);
- });
-
- // Close on outside click
- doc.addEventListener("click", function (e) {
- if (!toolbar.contains(e.target) && !panel.hidden) {
- panel.hidden = true;
- toggle.setAttribute("aria-expanded", "false");
- toggle.classList.remove("active");
- }
- });
-
// Apply saved preferences on load
if (prefs.fontStep !== defaultStep) applyFontSize(prefs.fontStep);
if (prefs.inverted) applyInversion(true);
@@ -384,9 +415,55 @@
if (prefs.font) applyFont(true);
if (prefs.paused) applyPaused(true);
- toolbar.appendChild(toggle);
- toolbar.appendChild(panel);
- body.appendChild(toolbar);
+ // If theme FAB is present, mount a11y toggle inside it; otherwise standalone
+ var fabSlot = doc.getElementById("mokoA11ySlot");
+ if (fabSlot) {
+ toggle.className = "a11y-toggle a11y-toggle-inline";
+ fabSlot.appendChild(toggle);
+ toolbar.className = "a11y-toolbar-floating";
+ toolbar.appendChild(panel);
+ body.appendChild(toolbar);
+ // Position panel near the FAB
+ toggle.addEventListener("click", function () {
+ var isOpen = !panel.hidden;
+ panel.hidden = isOpen;
+ toggle.setAttribute("aria-expanded", isOpen ? "false" : "true");
+ toggle.classList.toggle("active", !isOpen);
+ if (!isOpen) {
+ var rect = toggle.getBoundingClientRect();
+ toolbar.style.position = "fixed";
+ toolbar.style.bottom = (win.innerHeight - rect.top + 8) + "px";
+ toolbar.style.right = (win.innerWidth - rect.right) + "px";
+ toolbar.style.zIndex = "1202";
+ }
+ });
+ // Close on outside click for inline mode
+ doc.addEventListener("click", function (e) {
+ if (!toggle.contains(e.target) && !toolbar.contains(e.target) && !panel.hidden) {
+ panel.hidden = true;
+ toggle.setAttribute("aria-expanded", "false");
+ toggle.classList.remove("active");
+ }
+ });
+ } else {
+ // Standalone mode — toggle and close handlers
+ toggle.addEventListener("click", function () {
+ var isOpen = !panel.hidden;
+ panel.hidden = isOpen;
+ toggle.setAttribute("aria-expanded", isOpen ? "false" : "true");
+ toggle.classList.toggle("active", !isOpen);
+ });
+ doc.addEventListener("click", function (e) {
+ if (!toolbar.contains(e.target) && !panel.hidden) {
+ panel.hidden = true;
+ toggle.setAttribute("aria-expanded", "false");
+ toggle.classList.remove("active");
+ }
+ });
+ toolbar.appendChild(toggle);
+ toolbar.appendChild(panel);
+ body.appendChild(toolbar);
+ }
}
// ========================================================================
diff --git a/src/templateDetails.xml b/src/templateDetails.xml
index fd87629..f928b11 100644
--- a/src/templateDetails.xml
+++ b/src/templateDetails.xml
@@ -31,18 +31,21 @@
-->
-
- https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/updates.xml
-
+
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/raw/branch/main/updates.xml
+
+
+ https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/updates.xml
+
MokoCassiopeia
- 03.09.12
+ 03.09.14
script.php
- 2026-03-26
+ 2026-04-14
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
diff --git a/src/templates/dark.custom.css b/src/templates/dark.custom.css
index f3fdbf3..689bfda 100644
--- a/src/templates/dark.custom.css
+++ b/src/templates/dark.custom.css
@@ -813,6 +813,12 @@ color-scheme: dark;
--table-active-color: var(--body-color);
--table-active-bg: rgba(var(--white-rgb), 0.1);
+/* ===== FOOTER ===== */
+--footer-padding-top: 1rem;
+--footer-padding-bottom: 80px;
+--footer-grid-padding-y: 2.5rem;
+--footer-grid-padding-x: 0.5em;
+
/* ===== BACKDROP ===== */
--backdrop-zindex: 1040;
--backdrop-bg: hsl(0, 0%, 0%);
diff --git a/src/templates/light.custom.css b/src/templates/light.custom.css
index 9170f61..d4b6d60 100644
--- a/src/templates/light.custom.css
+++ b/src/templates/light.custom.css
@@ -812,6 +812,12 @@ color-scheme: light;
--table-active-color: var(--body-color);
--table-active-bg: rgba(var(--black-rgb), 0.075);
+/* ===== FOOTER ===== */
+--footer-padding-top: 1rem;
+--footer-padding-bottom: 80px;
+--footer-grid-padding-y: 2.5rem;
+--footer-grid-padding-x: 0.5em;
+
/* ===== BACKDROP ===== */
--backdrop-zindex: 1040;
--backdrop-bg: hsl(0, 0%, 0%);
diff --git a/updates.xml b/updates.xml
index f5eb54d..2609b3b 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,7 +1,7 @@
@@ -13,17 +13,19 @@
mokocassiopeia
template
site
- 03.09.12
- 2026-04-08
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/development
+ 03.09.14
+ 2026-04-14
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.12-dev.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.14-dev.zip
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.14-dev.zip
- c2660acdf7389244462485f7ab4c286e9f851366a148acc16739a184576f7932
+ 4cbe4fc379182ef17580396e7d12ce4ce95a90017ef364b922bdc2d04b0b3d97
development
Moko Consulting
https://mokoconsulting.tech
-
+
+ 8.1
@@ -33,17 +35,19 @@
mokocassiopeia
template
site
- 03.09.12
- 2026-04-08
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/alpha
+ 03.09.14
+ 2026-04-14
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.09.12-alpha.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.09.14-alpha.zip
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.09.14-alpha.zip
c2660acdf7389244462485f7ab4c286e9f851366a148acc16739a184576f7932
alpha
Moko Consulting
https://mokoconsulting.tech
-
+
+ 8.1
@@ -53,17 +57,19 @@
mokocassiopeia
template
site
- 03.09.12
- 2026-04-08
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/beta
+ 03.09.14
+ 2026-04-14
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.09.12-beta.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.09.14-beta.zip
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.09.14-beta.zip
- c2660acdf7389244462485f7ab4c286e9f851366a148acc16739a184576f7932
+ 4cbe4fc379182ef17580396e7d12ce4ce95a90017ef364b922bdc2d04b0b3d97
beta
Moko Consulting
https://mokoconsulting.tech
-
+
+ 8.1
@@ -73,17 +79,19 @@
mokocassiopeia
template
site
- 03.09.12
- 2026-04-08
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/release-candidate
+ 03.09.14
+ 2026-04-14
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-03.09.12-rc.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-03.09.14-rc.zip
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-03.09.14-rc.zip
c2660acdf7389244462485f7ab4c286e9f851366a148acc16739a184576f7932
rc
Moko Consulting
https://mokoconsulting.tech
-
+
+ 8.1
@@ -93,17 +101,19 @@
mokocassiopeia
template
site
- 03.09.12
- 2026-04-08
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03
+ 03.09.14
+ 2026-04-14
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.12.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.14.zip
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.14.zip
c2660acdf7389244462485f7ab4c286e9f851366a148acc16739a184576f7932
stable
Moko Consulting
https://mokoconsulting.tech
-
+
+ 8.1