Merge dev into main: take all dev changes
Some checks failed
Repo Health / Access control (push) Successful in 1s
Standards Compliance / Secret Scanning (push) Successful in 3s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 4s
Standards Compliance / Coding Standards Check (push) Failing after 8s
Standards Compliance / Version Consistency Check (push) Successful in 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 2s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 4s
Standards Compliance / Code Duplication Detection (push) Successful in 3s
Standards Compliance / Dead Code Detection (push) Successful in 3s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 3s
CodeQL Security Scanning / Analyze (javascript) (push) Failing after 1m15s
CodeQL Security Scanning / Analyze (actions) (push) Failing after 1m17s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 2s
Standards Compliance / Broken Link Detection (push) Successful in 5s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 5s
Standards Compliance / Unused Dependencies Check (push) Successful in 7s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Enterprise Readiness Check (push) Successful in 4s
Standards Compliance / Repository Health Check (push) Successful in 4s
Repo Health / Release configuration (push) Failing after 4s
Repo Health / Scripts governance (push) Successful in 4s
Standards Compliance / Terraform Configuration Validation (push) Successful in 7s
CodeQL Security Scanning / Security Scan Summary (push) Successful in 1s
Repo Health / Repository health (push) Failing after 4s
Standards Compliance / Compliance Summary (push) Successful in 1s
Auto-Assign Issues & PRs / Assign unassigned issues and PRs (push) Successful in 1s

This commit is contained in:
Jonathan Miller
2026-04-18 17:40:42 -05:00
38 changed files with 2750 additions and 6770 deletions

17
.gitattributes vendored Normal file
View File

@@ -0,0 +1,17 @@
# Force LF line endings for all text files
* text=auto eol=lf
# Explicitly mark binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg text eol=lf
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.zip binary
*.gz binary
*.tar binary

View File

@@ -86,7 +86,7 @@ jobs:
# Check for existing issue with same title prefix # Check for existing issue with same title prefix
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=10" 2>/dev/null \ EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=10" 2>/dev/null \
--jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1) | jq -r ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1)
if [ -n "$EXISTING" ]; then if [ -n "$EXISTING" ]; then
echo " Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY echo " Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY
@@ -164,7 +164,7 @@ jobs:
IFS='|' read -r SUB_TITLE _ _ <<< "$SUB" IFS='|' read -r SUB_TITLE _ _ <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_NUM=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=20" 2>/dev/null \ SUB_NUM=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=20" 2>/dev/null \
--jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1) | jq -r ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1)
if [ -n "$SUB_NUM" ]; then if [ -n "$SUB_NUM" ]; then
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" 2>/dev/null -X PATCH \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" 2>/dev/null -X PATCH \
-f body="$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" | jq -r '.body' 2>/dev/null) -f body="$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" | jq -r '.body' 2>/dev/null)

View File

@@ -53,7 +53,7 @@ permissions:
jobs: jobs:
release: release:
name: Build & Release Pipeline name: Build & Release Pipeline
runs-on: ubuntu-latest runs-on: release
if: >- if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
@@ -64,9 +64,12 @@ jobs:
token: ${{ secrets.GA_TOKEN || github.token }} token: ${{ secrets.GA_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Set authenticated push URL
run: git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Setup MokoStandards tools - name: Setup MokoStandards tools
env: env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }} GA_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: | run: |
git clone --depth 1 --branch version/04 --quiet \ git clone --depth 1 --branch version/04 --quiet \

View File

@@ -67,7 +67,7 @@ jobs:
if [ "$ACTION" = "freeze" ]; then if [ "$ACTION" = "freeze" ]; then
# Check if ruleset already exists # Check if ruleset already exists
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \ EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \
--jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true) | jq -r ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
if [ -n "$EXISTING" ]; then if [ -n "$EXISTING" ]; then
echo "Branch \`${BRANCH}\` is already frozen (ruleset #${EXISTING})" >> $GITHUB_STEP_SUMMARY echo "Branch \`${BRANCH}\` is already frozen (ruleset #${EXISTING})" >> $GITHUB_STEP_SUMMARY
@@ -99,7 +99,7 @@ jobs:
elif [ "$ACTION" = "unfreeze" ]; then elif [ "$ACTION" = "unfreeze" ]; then
# Find and delete the freeze ruleset # Find and delete the freeze ruleset
RULESET_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \ RULESET_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \
--jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true) | jq -r ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
if [ -z "$RULESET_ID" ]; then if [ -z "$RULESET_ID" ]; then
echo "Branch \`${BRANCH}\` is not frozen (no ruleset found)" >> $GITHUB_STEP_SUMMARY echo "Branch \`${BRANCH}\` is not frozen (no ruleset found)" >> $GITHUB_STEP_SUMMARY

View File

@@ -32,7 +32,7 @@ permissions:
jobs: jobs:
deploy: deploy:
name: SFTP Deploy to Dev name: SFTP Deploy to Dev
runs-on: ubuntu-latest runs-on: release
steps: steps:
- name: Checkout repository - name: Checkout repository

View File

@@ -47,7 +47,7 @@ env:
jobs: jobs:
build: build:
name: Build Release Package name: Build Release Package
runs-on: ubuntu-latest runs-on: release
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -139,8 +139,8 @@ jobs:
TOKEN="${{ secrets.GA_TOKEN }}" TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Find and delete existing release by tag # Find and delete existing release by tag (may not exist — ignore 404)
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ RELEASE_ID=$(curl -s -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty') "${API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty')
if [ -n "$RELEASE_ID" ]; then if [ -n "$RELEASE_ID" ]; then
@@ -258,7 +258,7 @@ jobs:
--arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \ --arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \
--arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \ --arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \
--argjson pre "$IS_PRE" \ --argjson pre "$IS_PRE" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}' '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre, draft: false}'
)" | jq -r '.id') )" | jq -r '.id')
# Upload ZIP # Upload ZIP
@@ -367,19 +367,57 @@ jobs:
print(f"Updated {xml_tag} channel: version={version}, sha={sha256[:16]}..., date={date}") print(f"Updated {xml_tag} channel: version={version}, sha={sha256[:16]}..., date={date}")
PYEOF PYEOF
- name: "Commit updates.xml" - name: "Commit updates.xml to current branch and main"
run: | run: |
if git diff --quiet updates.xml 2>/dev/null; then if git diff --quiet updates.xml 2>/dev/null; then
echo "No changes to updates.xml" echo "No changes to updates.xml"
exit 0 exit 0
fi fi
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.meta.outputs.version }}"
CURRENT_BRANCH="${{ github.ref_name }}"
TOKEN="${{ secrets.GA_TOKEN }}"
TOKEN="${{ secrets.GA_TOKEN }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
git add updates.xml git add updates.xml
git commit -m "chore: update ${STABILITY} SHA-256 for ${{ steps.meta.outputs.version }} [skip ci]" \ git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
# Set push URL with GA_TOKEN for authenticated pushes (branch protection requires jmiller)
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
# Push to current branch
git push || true git push || true
# Also update updates.xml on main via Gitea API (git push blocked by branch protection)
if [ "$CURRENT_BRANCH" != "main" ]; then
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# Get current file SHA on main (required for update)
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
if [ -n "$FILE_SHA" ]; then
# Base64-encode the updates.xml content
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/contents/updates.xml" \
-d "$(jq -n \
--arg content "$CONTENT" \
--arg sha "$FILE_SHA" \
--arg msg "chore: update ${STABILITY} channel to ${VERSION} on main [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)" > /dev/null && echo "updates.xml synced to main via API" || echo "WARNING: failed to sync updates.xml to main"
fi
fi
- name: Summary - name: Summary
run: | run: |
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"

View File

@@ -504,7 +504,7 @@ jobs:
DELETED=0 DELETED=0
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" 2>/dev/null \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" 2>/dev/null \
--jq ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do | jq -r ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do
# Lock and close with "not_planned" to mark as cleaned up # Lock and close with "not_planned" to mark as cleaned up
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${num}/lock" 2>/dev/null -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${num}/lock" 2>/dev/null -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true
echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY

View File

@@ -53,7 +53,7 @@ permissions:
jobs: jobs:
update-xml: update-xml:
name: Update updates.xml name: Update updates.xml
runs-on: ubuntu-latest runs-on: release
if: >- if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'

7
.gitignore vendored
View File

@@ -198,5 +198,8 @@ venv/
*.coverage *.coverage
hypothesis/ hypothesis/
src/media/css/theme/dark.custom.css # Custom theme palettes (site-specific, not version controlled)
src/media/css/theme/light.custom.css src/media/css/theme/*.custom.css
src/media/css/theme/*.custom.min.css
src/templates/*.custom.css
templates/*.custom.css

View File

@@ -19,6 +19,48 @@ All notable changes to the MokoCassiopeia Joomla template are documented in this
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [03.10.00] - 2026-04-18 — Bridge Release (MokoCassiopeia → MokoOnyx)
### Important
- **Template Rename** — MokoCassiopeia is being renamed to **MokoOnyx**. This bridge release automatically migrates your template settings, menu assignments, and files to the new name. MokoCassiopeia can be safely uninstalled after this update.
### Added
- **Offline page redesign** — Full-viewport background from Joomla offline_image or header background, glass card overlay, centered logo with glow, login accordion, copyright footer
- **CSS variable click-to-copy** — Text containing `--variable-name` patterns is wrapped in clickable chips that copy to clipboard with toast notification
- **Brand-aside 3-column layout** — Flex columns like top-a with card style
- **mod_stats table layout** — Converted from definition list to semantic table
- **Favicon multi-format support** — Now handles PNG, JPEG, GIF, WebP, BMP (not just PNG)
- **Theme variables** — `--theme-fab-bg`, `--theme-fab-color`, `--theme-fab-btn-bg`, `--theme-fab-border`, `--offline-card-bg`
- **Footer CSS variables** — Added to CSS Variables reference tab
- **Bridge migration script** — `helper/bridge.php` handles automatic MokoCassiopeia → MokoOnyx migration
- **Dedicated release runner** — Release workflows run on isolated `release` label runner
- **Runner fleet** — 3 CI + 1 release runner (12 concurrent jobs)
### Changed
- **Gitea-primary CI/CD** — All workflows use Gitea API, GitHub is backup for stable/RC only
- **Theme switcher** — Larger, bordered, theme-aware colors (off-white on dark, primary on light)
- **Auto switch** — Red when off, green when on
- **A11y toolbar** — Theme-aware colors for dark mode visibility
- **Search button border** — Matches input border (`--input-border-color`)
- **Offline message** — 0=hidden, 1=custom message, 2=system language string
- **Light theme fonts** — Fixed trailing `)` syntax error, normalized quote style to match dark
- **`--accent-color-secondary`** — Unified to `#6fb3ff` across both themes
- **`--alert-color`** — Set to `#000` in light theme
### Removed
- Brand showcase tab (redundant with theme preview)
- Position selectors for a11y/theme FAB (forced to bottom-right)
- Custom theme CSS from git tracking (site-specific, gitignored)
### Fixed
- SHA-256 checksum format — Removed `sha256:` prefix (Joomla expects raw hex)
- Favicon path resolution — Strips `#joomlaImage://` fragment, tries multiple path candidates
- `REQUIRE_SIGNIN_VIEW` — Set to `false` for public release downloads
- Release workflow — Uses Gitea API to update `updates.xml` on main (bypasses branch protection)
- Language loading on offline page — `com_users` and core language files loaded explicitly
---
## [Unreleased] - 2026-04-02 ## [Unreleased] - 2026-04-02
### Added ### Added

View File

@@ -9,11 +9,13 @@
INGROUP: MokoCassiopeia.Documentation INGROUP: MokoCassiopeia.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia
FILE: ./README.md FILE: ./README.md
VERSION: 03.09.14 VERSION: 03.10.07
BRIEF: Documentation for MokoCassiopeia template BRIEF: Documentation for MokoCassiopeia template
--> -->
# MokoCassiopeia Template # MokoCassiopeia → MokoOnyx
> **This template is being renamed to MokoOnyx.** Version 03.10.07 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** **A Modern, Lightweight Joomla Template Based on Cassiopeia**

View File

@@ -1,6 +1,6 @@
{ {
"name": "mokoconsulting/mokocassiopeia", "name": "mokoconsulting/mokocassiopeia",
"description": "MokoCassiopeia \u2014 Joomla site template based on Cassiopeia", "description": "MokoCassiopeia \u00e2\u20ac\u201d Joomla site template based on Cassiopeia",
"type": "joomla-template", "type": "joomla-template",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"authors": [ "authors": [
@@ -10,8 +10,8 @@
} }
], ],
"require": { "require": {
"mokoconsulting-tech/enterprise": "dev-version/04", "php": ">=8.1",
"php": ">=8.1" "ext-zip": "*"
}, },
"require-dev": { "require-dev": {
"mokoconsulting-tech/enterprise": "^4.0" "mokoconsulting-tech/enterprise": "^4.0"

360
src/helper/bridge.php Normal file
View File

@@ -0,0 +1,360 @@
<?php
/**
* 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
*/
/**
* Bridge migration helper — MokoCassiopeia → MokoOnyx
*
* Called from script.php during the v03.10.00 update. Copies the template
* to the new directory name, migrates database records, and sets MokoOnyx
* as the active site template.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Log\Log;
class MokoBridgeMigration
{
private const OLD_NAME = 'mokocassiopeia';
private const NEW_NAME = 'mokoonyx';
private const OLD_DISPLAY = 'MokoCassiopeia';
private const NEW_DISPLAY = 'MokoOnyx';
/**
* Run the full migration.
*
* @return bool True on success, false on failure.
*/
public static function run(): bool
{
$app = Factory::getApplication();
$db = Factory::getDbo();
// 1. Copy template files
if (!self::copyTemplateFiles()) {
$app->enqueueMessage(
'MokoOnyx migration: failed to copy template files. '
. 'You can manually copy templates/mokocassiopeia to templates/mokoonyx.',
'error'
);
return false;
}
// 2. Copy media files
if (!self::copyMediaFiles()) {
$app->enqueueMessage(
'MokoOnyx migration: failed to copy media files. '
. 'You can manually copy media/templates/site/mokocassiopeia to media/templates/site/mokoonyx.',
'warning'
);
}
// 3. Rename internals in the new copy (templateDetails.xml, language files, etc.)
self::renameInternals();
// 4. Register the new template in the database
self::migrateDatabase($db);
// 5. Notify the admin
$app->enqueueMessage(
'<strong>MokoCassiopeia has been renamed to MokoOnyx.</strong><br>'
. 'Your template settings have been migrated automatically. '
. 'MokoOnyx is now your active site template. '
. 'You can safely uninstall MokoCassiopeia from Extensions → Manage.',
'success'
);
self::log('Bridge migration completed successfully.');
return true;
}
/**
* Copy template directory.
*/
private static function copyTemplateFiles(): bool
{
$src = JPATH_ROOT . '/templates/' . self::OLD_NAME;
$dst = JPATH_ROOT . '/templates/' . self::NEW_NAME;
if (is_dir($dst)) {
self::log('MokoOnyx template directory already exists — skipping copy.');
return true;
}
if (!is_dir($src)) {
self::log('Source template directory not found: ' . $src, 'error');
return false;
}
return Folder::copy($src, $dst);
}
/**
* Copy media directory.
*/
private static function copyMediaFiles(): bool
{
$src = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
$dst = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
if (is_dir($dst)) {
self::log('MokoOnyx media directory already exists — skipping copy.');
return true;
}
if (!is_dir($src)) {
self::log('Source media directory not found: ' . $src, 'warning');
return true; // Non-critical
}
return Folder::copy($src, $dst);
}
/**
* Rename internal references in the copied template.
*/
private static function renameInternals(): void
{
$base = JPATH_ROOT . '/templates/' . self::NEW_NAME;
$mediaBase = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
// templateDetails.xml — name, element, update servers, paths
$manifest = $base . '/templateDetails.xml';
if (is_file($manifest)) {
$content = file_get_contents($manifest);
$content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content);
$content = str_replace(self::OLD_NAME, self::NEW_NAME, $content);
// Update the update server URLs to point to MokoOnyx repo
$content = str_replace('MokoCassiopeia', 'MokoOnyx', $content);
file_put_contents($manifest, $content);
self::log('Updated templateDetails.xml for MokoOnyx.');
}
// joomla.asset.json
$assetFile = $base . '/joomla.asset.json';
if (is_file($assetFile)) {
$content = file_get_contents($assetFile);
$content = str_replace(self::OLD_NAME, self::NEW_NAME, $content);
$content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content);
file_put_contents($assetFile, $content);
}
// Language files
$langDirs = [
$base . '/language/en-GB',
$base . '/language/en-US',
];
foreach ($langDirs as $langDir) {
if (!is_dir($langDir)) continue;
foreach (glob($langDir . '/*mokocassiopeia*') as $file) {
$newFile = str_replace(self::OLD_NAME, self::NEW_NAME, $file);
if (is_file($file)) {
$content = file_get_contents($file);
$content = str_replace('MOKOCASSIOPEIA', 'MOKOONYX', $content);
$content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content);
$content = str_replace(self::OLD_NAME, self::NEW_NAME, $content);
file_put_contents($newFile, $content);
if ($newFile !== $file) {
File::delete($file);
}
}
}
}
// script.php — class name
$scriptFile = $base . '/script.php';
if (is_file($scriptFile)) {
$content = file_get_contents($scriptFile);
$content = str_replace('Tpl_MokocassiopeiaInstallerScript', 'Tpl_MokoonyxInstallerScript', $content);
$content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content);
$content = str_replace(self::OLD_NAME, self::NEW_NAME, $content);
// Remove the bridge migration call from the new template's script
$content = preg_replace(
'/\/\/ Bridge migration.*?MokoBridgeMigration::run\(\);/s',
'// Migration complete — this is MokoOnyx',
$content
);
file_put_contents($scriptFile, $content);
}
// Remove bridge helper from the new template (not needed)
$bridgeFile = $base . '/helper/bridge.php';
if (is_file($bridgeFile)) {
File::delete($bridgeFile);
}
self::log('Renamed internal references in MokoOnyx.');
}
/**
* Migrate database records: template_styles, menu assignments.
*/
private static function migrateDatabase(\Joomla\Database\DatabaseInterface $db): void
{
// Get existing MokoCassiopeia styles
$query = $db->getQuery(true)
->select('*')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
->where($db->quoteName('client_id') . ' = 0');
$oldStyles = $db->setQuery($query)->loadObjectList();
if (empty($oldStyles)) {
self::log('No MokoCassiopeia styles found in database.', 'warning');
return;
}
foreach ($oldStyles as $oldStyle) {
// Check if MokoOnyx style already exists
$query = $db->getQuery(true)
->select('COUNT(*)')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
->where($db->quoteName('title') . ' = ' . $db->quote(
str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title)
));
$exists = (int) $db->setQuery($query)->loadResult();
if ($exists > 0) {
continue;
}
// Create new style with same params
$newStyle = clone $oldStyle;
unset($newStyle->id);
$newStyle->template = self::NEW_NAME;
$newStyle->title = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title);
// Update params: replace any mokocassiopeia paths
$params = $newStyle->params;
if (is_string($params)) {
$params = str_replace(self::OLD_NAME, self::NEW_NAME, $params);
$newStyle->params = $params;
}
$db->insertObject('#__template_styles', $newStyle, 'id');
$newId = $newStyle->id;
// Copy menu assignments
$query = $db->getQuery(true)
->select('menuid')
->from('#__template_styles_menus') // Joomla 5 uses this table
->where('template_style_id = ' . (int) $oldStyle->id);
try {
$menuIds = $db->setQuery($query)->loadColumn();
foreach ($menuIds as $menuId) {
$obj = (object) [
'template_style_id' => $newId,
'menuid' => $menuId,
];
$db->insertObject('#__template_styles_menus', $obj);
}
} catch (\Exception $e) {
// Table may not exist in all Joomla versions
}
// If this was the default style, make MokoOnyx the default
if ($oldStyle->home == 1) {
// Set MokoOnyx as default
$query = $db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 1')
->where('id = ' . (int) $newId);
$db->setQuery($query)->execute();
// Unset MokoCassiopeia as default
$query = $db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 0')
->where('id = ' . (int) $oldStyle->id);
$db->setQuery($query)->execute();
self::log('Set MokoOnyx as default site template.');
}
}
// Register the new template in the extensions table
self::registerExtension($db);
self::log('Database migration completed. ' . count($oldStyles) . ' style(s) migrated.');
}
/**
* Register MokoOnyx in the extensions table so Joomla recognizes it.
*/
private static function registerExtension(\Joomla\Database\DatabaseInterface $db): void
{
// Check if already registered
$query = $db->getQuery(true)
->select('extension_id')
->from('#__extensions')
->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
$existing = $db->setQuery($query)->loadResult();
if ($existing) {
self::log('MokoOnyx already registered in extensions table.');
return;
}
// Get the old extension record as a base
$query = $db->getQuery(true)
->select('*')
->from('#__extensions')
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
$oldExt = $db->setQuery($query)->loadObject();
if (!$oldExt) {
self::log('MokoCassiopeia extension record not found.', 'warning');
return;
}
$newExt = clone $oldExt;
unset($newExt->extension_id);
$newExt->element = self::NEW_NAME;
$newExt->name = self::NEW_NAME;
// Update manifest_cache with new name
$cache = json_decode($newExt->manifest_cache, true);
if (is_array($cache)) {
$cache['name'] = self::NEW_DISPLAY;
$newExt->manifest_cache = json_encode($cache);
}
$db->insertObject('#__extensions', $newExt, 'extension_id');
self::log('Registered MokoOnyx in extensions table (ID: ' . $newExt->extension_id . ').');
}
/**
* Log a message.
*/
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_bridge.log.php'],
Log::ALL,
['mokocassiopeia_bridge']
);
Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia_bridge');
}
}

View File

@@ -14,6 +14,8 @@
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Log\Log;
class MokoFaviconHelper class MokoFaviconHelper
{ {
/** /**
@@ -39,7 +41,13 @@ class MokoFaviconHelper
*/ */
public static function generate(string $sourcePath, string $outputDir): bool public static function generate(string $sourcePath, string $outputDir): bool
{ {
if (!is_file($sourcePath) || !extension_loaded('gd')) { 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');
return false; return false;
} }
@@ -55,8 +63,24 @@ class MokoFaviconHelper
return true; return true;
} }
$source = imagecreatefrompng($sourcePath); // Detect image type and load accordingly
$imageInfo = @getimagesize($sourcePath);
if ($imageInfo === false) {
Log::add('Favicon: cannot read image info from ' . $sourcePath, Log::WARNING, 'mokocassiopeia');
return false;
}
$source = match ($imageInfo[2]) {
IMAGETYPE_PNG => @imagecreatefrompng($sourcePath),
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) { if (!$source) {
Log::add('Favicon: unsupported image type (' . ($imageInfo['mime'] ?? 'unknown') . ') at ' . $sourcePath, Log::WARNING, 'mokocassiopeia');
return false; return false;
} }
@@ -162,7 +186,7 @@ class MokoFaviconHelper
*/ */
public static function getHeadTags(string $basePath): string public static function getHeadTags(string $basePath): string
{ {
$basePath = rtrim($basePath, '/'); $basePath = htmlspecialchars(rtrim($basePath, '/'), ENT_QUOTES, 'UTF-8');
return '<link rel="apple-touch-icon" sizes="180x180" href="' . $basePath . '/apple-touch-icon.png">' . "\n" return '<link rel="apple-touch-icon" sizes="180x180" href="' . $basePath . '/apple-touch-icon.png">' . "\n"
. '<link rel="icon" type="image/png" sizes="32x32" href="' . $basePath . '/favicon-32x32.png">' . "\n" . '<link rel="icon" type="image/png" sizes="32x32" href="' . $basePath . '/favicon-32x32.png">' . "\n"

View File

@@ -22,6 +22,9 @@ class MokoMinifyHelper
*/ */
private const CSS_FILES = [ private const CSS_FILES = [
'css/template.css', 'css/template.css',
'css/offline.css',
'css/editor.css',
'css/a11y-high-contrast.css',
'css/theme/light.standard.css', 'css/theme/light.standard.css',
'css/theme/dark.standard.css', 'css/theme/dark.standard.css',
'css/theme/light.custom.css', 'css/theme/light.custom.css',

View File

@@ -26,10 +26,14 @@ $headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'U
<?php if ($module->showtitle) : ?> <?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-stats__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>> <<?php echo $headerTag; ?> class="mod-stats__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?> <?php endif; ?>
<dl class="mod-stats__list"> <table class="mod_stats__table">
<?php foreach ($list as $item) : ?> <tbody>
<dt class="mod-stats__label"><?php echo $item->title; ?></dt> <?php foreach ($list as $item) : ?>
<dd class="mod-stats__data"><?php echo $item->data; ?></dd> <tr>
<?php endforeach; ?> <th class="mod_stats__label" scope="row"><?php echo $item->title; ?></th>
</dl> <td class="mod_stats__data"><?php echo $item->data; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div> </div>

View File

@@ -41,7 +41,7 @@ $params_favicon_source = (string) $this->params->get('favicon_source', '');
$params_theme_enabled = $this->params->get('theme_enabled', 1); $params_theme_enabled = $this->params->get('theme_enabled', 1);
$params_theme_control_type = (string) $this->params->get('theme_control_type', 'radios'); $params_theme_control_type = (string) $this->params->get('theme_control_type', 'radios');
$params_theme_fab_enabled = $this->params->get('theme_fab_enabled', 1); $params_theme_fab_enabled = $this->params->get('theme_fab_enabled', 1);
$params_theme_fab_pos = $this->params->get('theme_fab_pos', 'br'); $params_theme_fab_pos = 'br';
// Accessibility params // Accessibility params
$params_a11y_toolbar = $this->params->get('a11y_toolbar_enabled', 1); $params_a11y_toolbar = $this->params->get('a11y_toolbar_enabled', 1);
@@ -51,7 +51,7 @@ $params_a11y_contrast = $this->params->get('a11y_high_contrast', 1);
$params_a11y_links = $this->params->get('a11y_highlight_links', 1); $params_a11y_links = $this->params->get('a11y_highlight_links', 1);
$params_a11y_font = $this->params->get('a11y_readable_font', 1); $params_a11y_font = $this->params->get('a11y_readable_font', 1);
$params_a11y_animations = $this->params->get('a11y_pause_animations', 1); $params_a11y_animations = $this->params->get('a11y_pause_animations', 1);
$params_a11y_pos = (string) $this->params->get('a11y_toolbar_pos', 'tl'); $params_a11y_pos = 'br';
// Detecting Active Variables // Detecting Active Variables
$option = $input->getCmd('option', ''); $option = $input->getCmd('option', '');
@@ -71,7 +71,27 @@ $templatePath = 'media/templates/site/mokocassiopeia';
$faviconHeadTags = ''; $faviconHeadTags = '';
if ($params_favicon_source) { if ($params_favicon_source) {
require_once __DIR__ . '/helper/favicon.php'; require_once __DIR__ . '/helper/favicon.php';
$faviconSourceAbs = JPATH_ROOT . '/' . ltrim($params_favicon_source, '/'); // Joomla's media field returns paths like:
// 'images/logo.png' (images folder)
// 'media/templates/site/mokocassiopeia/images/logo.png' (template media)
// 'logo.png' (bare filename)
// Strip Joomla's #joomlaImage:// fragment from media field value
$faviconSourceRel = strtok(ltrim($params_favicon_source, '/'), '#');
$faviconSourceAbs = JPATH_ROOT . '/' . $faviconSourceRel;
// Try common prefixes if not found
if (!is_file($faviconSourceAbs)) {
$candidates = [
JPATH_ROOT . '/images/' . $faviconSourceRel,
JPATH_ROOT . '/media/templates/site/' . $this->template . '/' . $faviconSourceRel,
JPATH_ROOT . '/media/templates/site/' . $this->template . '/images/' . basename($faviconSourceRel),
];
foreach ($candidates as $candidate) {
if (is_file($candidate)) {
$faviconSourceAbs = $candidate;
break;
}
}
}
$faviconOutputDir = JPATH_ROOT . '/images/favicons'; $faviconOutputDir = JPATH_ROOT . '/images/favicons';
$faviconUrlBase = Uri::root(true) . '/images/favicons'; $faviconUrlBase = Uri::root(true) . '/images/favicons';
@@ -406,7 +426,7 @@ $wa->useScript('user.js'); // js/user.js
</div> </div>
<?php if ($this->countModules('brand-aside', true)) : ?> <?php if ($this->countModules('brand-aside', true)) : ?>
<div class="container-brand-aside"> <div class="container-brand-aside">
<jdoc:include type="modules" name="brand-aside" style="none" /> <jdoc:include type="modules" name="brand-aside" style="card" />
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@@ -17,7 +17,7 @@
"defgroup": "Joomla.Template.Site", "defgroup": "Joomla.Template.Site",
"ingroup": "MokoCassiopeia.Template.Assets", "ingroup": "MokoCassiopeia.Template.Assets",
"path": "./media/templates/site/mokocassiopeia/joomla.asset.json", "path": "./media/templates/site/mokocassiopeia/joomla.asset.json",
"version": "03.09.14", "version": "03.10.07",
"brief": "Joomla asset registry for MokoCassiopeia" "brief": "Joomla asset registry for MokoCassiopeia"
} }
}, },
@@ -34,6 +34,18 @@
"uri": "media/templates/site/mokocassiopeia/css/template.min.css", "uri": "media/templates/site/mokocassiopeia/css/template.min.css",
"attributes": {"media": "all"} "attributes": {"media": "all"}
}, },
{
"name": "template.offline",
"type": "style",
"uri": "media/templates/site/mokocassiopeia/css/offline.css",
"attributes": {"media": "all"}
},
{
"name": "template.offline.min",
"type": "style",
"uri": "media/templates/site/mokocassiopeia/css/offline.min.css",
"attributes": {"media": "all"}
},
{ {
"name": "template.user", "name": "template.user",
"type": "style", "type": "style",

View File

@@ -259,16 +259,14 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC="<strong>Surfaces &amp; text</strong><br><co
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable" TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable"
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Colour tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>" TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Colour tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>"
TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_LABEL="Footer"
TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_DESC="<strong>Spacing</strong><br><code>--footer-padding-top</code> — Top padding (default: <code>1rem</code>)<br><code>--footer-padding-bottom</code> — Bottom padding (default: <code>80px</code>)<br><code>--footer-grid-padding-y</code> — Grid vertical padding (default: <code>2.5rem</code>)<br><code>--footer-grid-padding-x</code> — Grid horizontal padding (default: <code>0.5em</code>)"
; ===== Theme Preview tab ===== ; ===== Theme Preview tab =====
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colours, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colours, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
; ===== Brand Showcase tab =====
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FIELDSET_LABEL="Brand Showcase"
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_INTRO="<p>Interactive brand and Bootstrap 5 component showcase with colour system gradients. <strong>Hover over any gradient</strong> to sample the exact pixel colour at that point. Use the <strong>Toggle Light / Dark</strong> button to switch themes. This page is also available standalone at <code>templates/mokocassiopeia/templates/brand-showcase.html</code>.</p>"
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FRAME="<iframe src='../templates/mokocassiopeia/templates/brand-showcase.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Brand and Bootstrap showcase with colour sampler'></iframe>"
; ===== Misc ===== ; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:" MOD_BREADCRUMBS_HERE="YOU ARE HERE:"

View File

@@ -259,16 +259,14 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC="<strong>Surfaces &amp; text</strong><br><co
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable" TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable"
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Color tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>" TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Color tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>"
TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_LABEL="Footer"
TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_DESC="<strong>Spacing</strong><br><code>--footer-padding-top</code> — Top padding (default: <code>1rem</code>)<br><code>--footer-padding-bottom</code> — Bottom padding (default: <code>80px</code>)<br><code>--footer-grid-padding-y</code> — Grid vertical padding (default: <code>2.5rem</code>)<br><code>--footer-grid-padding-x</code> — Grid horizontal padding (default: <code>0.5em</code>)"
; ===== Theme Preview tab ===== ; ===== Theme Preview tab =====
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colors, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colors, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
; ===== Brand Showcase tab =====
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FIELDSET_LABEL="Brand Showcase"
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_INTRO="<p>Interactive brand and Bootstrap 5 component showcase with color system gradients. <strong>Hover over any gradient</strong> to sample the exact pixel color at that point. Use the <strong>Toggle Light / Dark</strong> button to switch themes. This page is also available standalone at <code>templates/mokocassiopeia/templates/brand-showcase.html</code>.</p>"
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FRAME="<iframe src='../templates/mokocassiopeia/templates/brand-showcase.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Brand and Bootstrap showcase with color sampler'></iframe>"
; ===== Misc ===== ; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:" MOD_BREADCRUMBS_HERE="YOU ARE HERE:"

258
src/media/css/offline.css Normal file
View File

@@ -0,0 +1,258 @@
/* 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
*/
/* === Offline Page — Full-viewport background with centered overlay card === */
.moko-offline-wrap {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
color: #fff;
font-family: var(--body-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
/* Background: offline_image set inline, or fall back to header background */
background-color: var(--color-primary, #112855);
background-image: var(--header-background-image, none);
background-position: var(--header-background-position, center);
background-attachment: var(--header-background-attachment, fixed);
background-repeat: no-repeat;
background-size: cover;
}
/* Dark theme: overlay to darken the background */
:root[data-bs-theme="dark"] .moko-offline-wrap {
position: relative;
}
:root[data-bs-theme="dark"] .moko-offline-wrap::before {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 0;
}
/* === Centered Card Overlay === */
.moko-offline-card {
width: 100%;
max-width: 720px;
background: var(--offline-card-bg, rgba(0, 0, 0, 0.6));
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: 0.875rem;
padding: 2.5rem 2rem;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
@media (min-width: 768px) {
.moko-offline-card {
padding: 3rem;
}
}
@media (max-width: 575.98px) {
.moko-offline-wrap {
padding: 1rem 0.75rem;
}
.moko-offline-card {
padding: 2rem 1.25rem;
}
}
/* === Logo header area === */
.moko-offline-brand {
display: block;
text-align: center;
text-decoration: none;
color: #fff;
margin-bottom: 1.5rem;
}
.moko-offline-brand:hover {
color: var(--accent-color-primary, #3f8ff0);
}
.moko-offline-brand img {
max-width: 100%;
height: auto;
}
.moko-offline-brand .site-title {
display: block;
font-size: 2rem;
font-weight: 700;
font-family: 'Osaka', var(--body-font-family, sans-serif);
color: var(--accent-color-secondary, #6fb3ff);
}
.moko-offline-brand .brand-tagline {
display: block;
opacity: 0.7;
font-size: 0.9rem;
margin-top: 0.25rem;
}
/* === Offline Message === */
.moko-offline-message {
margin-bottom: 1.5rem;
}
.moko-offline-message h1 {
font-size: 1.5rem;
font-weight: 700;
color: #fff;
margin-bottom: 0.5rem;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.moko-offline-message p {
color: rgba(255, 255, 255, 0.85);
line-height: 1.6;
margin: 0;
}
/* === Offline Module Position === */
.moko-offline-modules {
margin-bottom: 1.5rem;
text-align: left;
}
/* === Copyright Footer === */
.moko-offline-copyright {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.45);
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.moko-offline-copyright a {
color: rgba(255, 255, 255, 0.6);
text-decoration: underline;
}
.moko-offline-copyright a:hover {
color: #fff;
}
/* === Login Accordion (translucent on overlay) === */
.moko-offline-card .accordion {
text-align: left;
}
.moko-offline-card .accordion-item {
background: transparent;
border-color: rgba(255, 255, 255, 0.15);
}
.moko-offline-card .accordion-button {
background: transparent;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
padding: 0.75rem 1rem;
}
.moko-offline-card .accordion-button:not(.collapsed) {
background: rgba(255, 255, 255, 0.05);
color: #fff;
box-shadow: none;
}
.moko-offline-card .accordion-button::after {
filter: invert(1) brightness(2);
}
.moko-offline-card .accordion-body {
background: transparent;
padding: 1rem;
}
/* === Form Controls (glass effect) === */
.moko-offline-card .form-control {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: #fff;
}
.moko-offline-card .form-control::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.moko-offline-card .form-control:focus {
background-color: rgba(255, 255, 255, 0.15);
border-color: var(--accent-color-primary, #3f8ff0);
color: #fff;
box-shadow: 0 0 0 0.25rem rgba(63, 143, 240, 0.25);
}
.moko-offline-card .form-label {
color: rgba(255, 255, 255, 0.8);
font-size: 0.875rem;
}
.moko-offline-card .form-check-label {
color: rgba(255, 255, 255, 0.7);
}
.moko-offline-card .form-check-input {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
.moko-offline-card .form-check-input:checked {
background-color: var(--accent-color-primary, #3f8ff0);
border-color: var(--accent-color-primary, #3f8ff0);
}
/* === Button === */
.moko-offline-card .btn-primary {
background-color: var(--color-primary, #112855);
border-color: rgba(255, 255, 255, 0.15);
color: #fff;
}
.moko-offline-card .btn-primary:hover {
background-color: var(--accent-color-primary, #3f8ff0);
border-color: var(--accent-color-primary, #3f8ff0);
}
/* === Links === */
.moko-offline-card a {
color: var(--accent-color-primary, #3f8ff0);
}
.moko-offline-card a:hover {
color: #fff;
}
/* === Joomla system messages === */
.moko-offline-messages {
width: 100%;
max-width: 720px;
margin-bottom: 1rem;
}
/* === Skip Link === */
.skip-link {
position: absolute;
left: -9999px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
position: static;
width: auto;
height: auto;
padding: 0.5rem 1rem;
}

View File

@@ -14233,6 +14233,28 @@ fieldset>* {
margin-inline-start: auto; margin-inline-start: auto;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1em;
}
.container-brand-aside>* {
flex: 1;
margin: 0.5em 0;
}
@media (max-width: 991.98px) {
.header-brand-wrap {
flex-direction: column;
align-items: stretch;
}
.container-brand-aside {
margin-inline-start: 0;
flex-direction: column;
}
.container-brand-aside>* {
flex: 0 1 auto;
}
} }
.container-header .navbar-brand { .container-header .navbar-brand {
@@ -15771,7 +15793,13 @@ body.wrapper-fluid header>.grid-child {
} }
footer .grid-child>div { footer .grid-child>div {
padding: var(--navbar-padding-y, 1rem) var(--navbar-padding-x, 1rem) 0; padding: calc(var(--navbar-padding-y, 1rem) * 3)
calc(var(--navbar-padding-x, 1rem) * 1)
0;
}
.mod-footer {
border-top: 1px solid var(--border-gray, #b2bfcds);
} }
header .grid-child .navbar-brand { header .grid-child .navbar-brand {
@@ -17064,14 +17092,20 @@ form .form-select {
display: flex; display: flex;
align-items: center; align-items: center;
gap: .5rem; gap: .5rem;
padding: calc(var(--padding-x, 0.25rem) * 2) calc(var(--padding-y, 0.25rem) * 3) calc(var(--padding-x, 0.25rem) * 2) calc(var(--padding-y, 0.25rem) * 8); padding: .5rem .75rem;
border-radius: 999px; border-radius: 999px;
border: none; border: 2px solid var(--theme-fab-border, rgba(255,255,255,.3));
background: var(--muted-color, #6d757e); background: var(--theme-fab-bg, var(--color-primary, #112855));
box-shadow: var(--box-shadow, 0 .5rem 1rem #00000066); color: var(--theme-fab-color, #fff);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
font: inherit; font: inherit;
color: #fff;
font-weight: 600; font-weight: 600;
transition: transform .15s, box-shadow .15s;
}
#mokoThemeFab:hover {
transform: scale(1.05);
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.15);
} }
#mokoThemeFab.pos-br { #mokoThemeFab.pos-br {
@@ -17094,50 +17128,47 @@ form .form-select {
top: 1rem; top: 1rem;
} }
#mokoThemeFab .switch { /* Sun/Moon theme toggle button */
display: inline-flex; .theme-icon-btn {
display: flex;
align-items: center; align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
background: var(--theme-fab-btn-bg, rgba(255,255,255,.15));
color: inherit;
font-size: 1.25rem;
cursor: pointer;
padding: 0;
position: relative; position: relative;
width: 44px;
height: 24px;
background: var(--secondary-color, #e6ebf1bf);
transition: background .2s, border-color .2s;
border-radius: var(--border-radius-xxl, 2rem);
} }
#mokoThemeFab .knob { .theme-icon-btn .fa-sun,
.theme-icon-btn .fa-moon {
position: absolute; position: absolute;
top: 2px; transition: opacity .2s, transform .2s;
left: 2px;
width: 20px;
height: 20px;
border-radius: var(--border-radius-xxl, 2rem);
background: var(--bs-body-bg, #fff);
box-shadow: var(--box-shadow, 0 .5rem 1rem #00000066);
transition: transform .2s ease;
} }
#mokoThemeFab [role="switch"][aria-checked="true"] .knob { /* Light mode: show sun, hide moon */
transform: translateX(20px); .theme-icon-btn.is-light .fa-sun {
opacity: 1;
transform: rotate(0deg);
}
.theme-icon-btn.is-light .fa-moon {
opacity: 0;
transform: rotate(-90deg);
} }
#mokoThemeFab [role="switch"][aria-checked="true"] .switch { /* Dark mode: show moon, hide sun */
background: rgba(var(--secondary-color, #e6ebf1bf), .15); .theme-icon-btn.is-dark .fa-moon {
opacity: 1;
transform: rotate(0deg);
} }
.theme-icon-btn.is-dark .fa-sun {
button#mokoThemeSwitch { opacity: 0;
border: unset; transform: rotate(90deg);
background-color: unset;
}
#mokoThemeFab .label {
user-select: none;
font-size: .875rem;
color: #fff;
}
#mokoThemeFab button {
color: #fff;
} }
/* Auto toggle switch (on/off style) */ /* Auto toggle switch (on/off style) */
@@ -17164,14 +17195,14 @@ button#mokoThemeSwitch {
height: 18px; height: 18px;
border: none; border: none;
border-radius: 999px; border-radius: 999px;
background: var(--secondary-color, #6c757d); background: var(--danger, #c23a31);
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
transition: background .2s; transition: background .2s;
} }
.auto-switch.on { .auto-switch.on {
background: var(--link-color, #3565e5); background: var(--success, #4aa664);
} }
.auto-track { .auto-track {
@@ -17207,6 +17238,15 @@ button#mokoThemeSwitch {
} }
/* Inline a11y toggle inside theme FAB */ /* Inline a11y toggle inside theme FAB */
/* Light mode: darker blue */
:root[data-bs-theme="light"] .a11y-toggle-inline {
--a11y-btn-bg: #1565c0;
}
/* Dark mode: lighter blue */
:root[data-bs-theme="dark"] .a11y-toggle-inline {
--a11y-btn-bg: #42a5f5;
}
.a11y-toggle-inline { .a11y-toggle-inline {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -17214,25 +17254,16 @@ button#mokoThemeSwitch {
width: 28px; width: 28px;
height: 28px; height: 28px;
border-radius: 50%; border-radius: 50%;
border: 1.5px solid currentColor; border: none;
background: transparent; background: var(--a11y-btn-bg, #1976d2);
color: inherit; color: #fff;
font-size: 1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
padding: 0; 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 { .a11y-toggle-inline.active {
opacity: 1; box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--a11y-btn-bg, #1976d2);
background: rgba(255,255,255,.25);
} }
/* Floating a11y panel when inline */ /* Floating a11y panel when inline */
@@ -17361,36 +17392,6 @@ body.site.error-page {
text-decoration: none; text-decoration: none;
} }
#mokoThemeFab .knob {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: var(--border-radius-xxl, 2rem);
background: var(--bs-body-bg, #fff);
box-shadow: var(--box-shadow, 0 .5rem 1rem #00000066);
transition: transform .2s ease;
}
#mokoThemeFab [role="switch"][aria-checked="true"] .knob {
transform: translateX(20px);
}
#mokoThemeFab [role="switch"][aria-checked="true"] .switch {
background: rgba(var(--secondary-color, #e6ebf1bf), .15);
}
button#mokoThemeSwitch {
border: unset;
background-color: unset;
}
#mokoThemeFab .label {
user-select: none;
font-size: .875rem;
}
#mokoThemeFab.debug-outline { #mokoThemeFab.debug-outline {
outline: 2px dashed var(--pink, #ff8fc0); outline: 2px dashed var(--pink, #ff8fc0);
outline-offset: 2px; outline-offset: 2px;
@@ -17499,12 +17500,13 @@ body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
/* Panel */ /* Panel */
.a11y-panel { .a11y-panel {
background: var(--bs-body-bg, #fff); background: var(--body-bg, var(--bs-body-bg, #fff));
border: 1px solid var(--border-color, #dee2e6); border: 1px solid var(--border-color, #dee2e6);
border-radius: var(--border-radius, .375rem); border-radius: var(--border-radius, .375rem);
padding: .75rem; padding: .75rem;
min-width: 200px; min-width: 200px;
box-shadow: var(--box-shadow-lg, 0 1rem 3rem rgba(0,0,0,.175)); box-shadow: var(--box-shadow-lg, 0 1rem 3rem rgba(0,0,0,.175));
color: var(--body-font-color, var(--body-color, #e6ebf1));
} }
.a11y-group { .a11y-group {
@@ -17540,8 +17542,8 @@ body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
height: 34px; height: 34px;
border: 1px solid var(--border-color, #dee2e6); border: 1px solid var(--border-color, #dee2e6);
border-radius: var(--border-radius, .375rem); border-radius: var(--border-radius, .375rem);
background: var(--bs-body-bg, #fff); background: var(--secondary-bg, var(--bs-body-bg, #fff));
color: var(--body-font-color, #444); color: var(--body-font-color, var(--body-color, #e6ebf1));
font-size: .875rem; font-size: .875rem;
cursor: pointer; cursor: pointer;
transition: background .15s, border-color .15s; transition: background .15s, border-color .15s;
@@ -17559,7 +17561,7 @@ body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
font-weight: 600; font-weight: 600;
min-width: 3ch; min-width: 3ch;
text-align: center; text-align: center;
color: var(--body-font-color, #444); color: var(--body-font-color, var(--body-color, #e6ebf1));
} }
.a11y-btn-wide { .a11y-btn-wide {
@@ -18686,7 +18688,7 @@ nav[data-toggle=toc] .nav-link.active+ul{
flex: 0 0 auto; flex: 0 0 auto;
background-color: var(--color-primary, #112855); background-color: var(--color-primary, #112855);
color: var(--mainmenu-nav-link-color, #fff); color: var(--mainmenu-nav-link-color, #fff);
border-color: var(--color-primary, #112855); border: 1px solid var(--input-border-color, #3a4250);
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
border-radius: 0 0.25rem 0.25rem 0; border-radius: 0 0.25rem 0.25rem 0;
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
@@ -18695,7 +18697,7 @@ nav[data-toggle=toc] .nav-link.active+ul{
.mod-finder__search.input-group button:hover, .mod-finder__search.input-group button:hover,
.container-search button[type="submit"]:hover { .container-search button[type="submit"]:hover {
background-color: var(--color-hover, gray); background-color: var(--color-hover, gray);
border-color: var(--color-hover, gray); border-color: var(--input-border-color, #3a4250);
} }
.mod-finder__search.input-group button:focus, .mod-finder__search.input-group button:focus,
@@ -21664,6 +21666,33 @@ nav[data-toggle=toc] .nav-link.active+ul{
color: var(--gray-600, #48525d); color: var(--gray-600, #48525d);
} }
/* === mod_stats === */
.mod_stats__table {
width: 100%;
border-collapse: collapse;
}
.mod_stats__table tr {
border-bottom: 1px solid var(--border-color, #2b323b);
}
.mod_stats__table tr:last-child {
border-bottom: none;
}
.mod_stats__label {
text-align: start;
font-weight: 600;
padding: 0.6rem 1rem 0.6rem 0;
color: var(--body-font-color, #e6ebf1);
}
.mod_stats__data {
text-align: end;
padding: 0.6rem 0;
color: var(--gray-600, #48525d);
}
/* === Mobile Responsive Adjustments === */ /* === Mobile Responsive Adjustments === */
@media (max-width: 575.98px) { @media (max-width: 575.98px) {
.mod-kunena-login__input { .mod-kunena-login__input {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
<svg <svg
width="800" width="800"
height="400" height="400"
viewBox="0 0 800 400"
id="svg2" id="svg2"
version="1.1" version="1.1"
sodipodi:docname="bg.svg" sodipodi:docname="bg.svg"
@@ -93,14 +94,14 @@
style="display:inline;fill:#e5e5e5;fill-opacity:1;stroke:none;stroke-width:2.20303" style="display:inline;fill:#e5e5e5;fill-opacity:1;stroke:none;stroke-width:2.20303"
id="rect4741" id="rect4741"
width="800" width="800"
height="400" height="494"
x="0" x="0"
y="46.331768" /> y="46.331768" />
<rect <rect
style="display:inline;fill:url(#pattern4758);fill-opacity:1;stroke:none;stroke-width:2.20303" style="display:inline;fill:url(#pattern4758);fill-opacity:1;stroke:none;stroke-width:2.20303"
id="rect4737" id="rect4737"
width="800" width="800"
height="400" height="494"
x="0" x="0"
y="46.699127" /> y="46.699127" />
</g> </g>

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -62,30 +62,33 @@
wrap.id = 'mokoThemeFab'; wrap.id = 'mokoThemeFab';
wrap.className = posClassFromBody(); wrap.className = posClassFromBody();
// Light label // Sun/Moon toggle button
var lblL = doc.createElement('span');
lblL.className = 'label';
lblL.textContent = 'Light';
// Switch
var switchWrap = doc.createElement('button'); var switchWrap = doc.createElement('button');
switchWrap.id = 'mokoThemeSwitch'; switchWrap.id = 'mokoThemeSwitch';
switchWrap.type = 'button'; switchWrap.type = 'button';
switchWrap.setAttribute('role', 'switch'); switchWrap.className = 'theme-icon-btn';
switchWrap.setAttribute('aria-label', 'Toggle dark mode'); switchWrap.setAttribute('aria-label', 'Toggle dark mode');
switchWrap.setAttribute('aria-checked', 'false');
var track = doc.createElement('span'); var sunIcon = doc.createElement('i');
track.className = 'switch'; sunIcon.className = 'fa-solid fa-sun';
var knob = doc.createElement('span'); sunIcon.setAttribute('aria-hidden', 'true');
knob.className = 'knob';
track.appendChild(knob);
switchWrap.appendChild(track);
// Dark label var moonIcon = doc.createElement('i');
var lblD = doc.createElement('span'); moonIcon.className = 'fa-solid fa-moon';
lblD.className = 'label'; moonIcon.setAttribute('aria-hidden', 'true');
lblD.textContent = 'Dark';
switchWrap.appendChild(sunIcon);
switchWrap.appendChild(moonIcon);
function updateThemeIcon(theme) {
if (theme === 'dark') {
switchWrap.classList.add('is-dark');
switchWrap.classList.remove('is-light');
} else {
switchWrap.classList.add('is-light');
switchWrap.classList.remove('is-dark');
}
}
// Auto toggle (on/off switch style) // Auto toggle (on/off switch style)
var autoWrap = doc.createElement('div'); var autoWrap = doc.createElement('div');
@@ -127,7 +130,7 @@
var current = (root.getAttribute('data-bs-theme') || 'light').toLowerCase(); var current = (root.getAttribute('data-bs-theme') || 'light').toLowerCase();
var next = current === 'dark' ? 'light' : 'dark'; var next = current === 'dark' ? 'light' : 'dark';
applyTheme(next); applyTheme(next);
switchWrap.setAttribute('aria-checked', next === 'dark' ? 'true' : 'false'); updateThemeIcon(next);
// Turn off auto when manually switching // Turn off auto when manually switching
auto.classList.remove('on'); auto.classList.remove('on');
auto.setAttribute('aria-checked', 'false'); auto.setAttribute('aria-checked', 'false');
@@ -145,7 +148,7 @@
clearStored(); clearStored();
var sys = systemTheme(); var sys = systemTheme();
applyTheme(sys); applyTheme(sys);
switchWrap.setAttribute('aria-checked', sys === 'dark' ? 'true' : 'false'); updateThemeIcon(sys);
} }
}); });
@@ -154,7 +157,7 @@
if (!getStored()) { if (!getStored()) {
var sys = systemTheme(); var sys = systemTheme();
applyTheme(sys); applyTheme(sys);
switchWrap.setAttribute('aria-checked', sys === 'dark' ? 'true' : 'false'); updateThemeIcon(sys);
} }
}; };
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql); if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql);
@@ -162,12 +165,10 @@
// Initial state // Initial state
var initial = getStored() || systemTheme(); var initial = getStored() || systemTheme();
switchWrap.setAttribute('aria-checked', initial === 'dark' ? 'true' : 'false'); updateThemeIcon(initial);
// Mount // Mount
wrap.appendChild(lblL);
wrap.appendChild(switchWrap); wrap.appendChild(switchWrap);
wrap.appendChild(lblD);
wrap.appendChild(autoWrap); wrap.appendChild(autoWrap);
wrap.appendChild(divider); wrap.appendChild(divider);
wrap.appendChild(a11ySlot); wrap.appendChild(a11ySlot);
@@ -292,7 +293,17 @@
toggle.className = "a11y-toggle"; toggle.className = "a11y-toggle";
toggle.setAttribute("aria-label", "Accessibility options"); toggle.setAttribute("aria-label", "Accessibility options");
toggle.setAttribute("aria-expanded", "false"); toggle.setAttribute("aria-expanded", "false");
toggle.appendChild(faIcon("fa-solid fa-universal-access")); var a11yIcon = faIcon("fa-solid fa-universal-access");
// Unicode fallback if FA7 glyph doesn't render (e.g. FA6/FA7 conflict)
setTimeout(function () {
var cs = win.getComputedStyle(a11yIcon, "::before");
if (!cs.content || cs.content === "none" || cs.content === '""' || cs.content === '"" / ""') {
a11yIcon.className = "";
a11yIcon.textContent = "\u267F";
a11yIcon.style.fontSize = "1.1rem";
}
}, 500);
toggle.appendChild(a11yIcon);
// Panel // Panel
var panel = doc.createElement("div"); var panel = doc.createElement("div");
@@ -630,6 +641,154 @@
}); });
} }
// ========================================================================
// CSS VARIABLE CLICK-TO-COPY
// ========================================================================
/**
* Inject toast + variable-chip styles once.
*/
function injectVarCopyStyles() {
if (doc.getElementById("moko-var-copy-styles")) return;
var style = doc.createElement("style");
style.id = "moko-var-copy-styles";
style.textContent =
".moko-var-chip{cursor:pointer;font-family:var(--font-monospace,monospace);font-size:.875em;" +
"background:var(--secondary-bg,#151b22);color:var(--link-color,#8ab4f8);" +
"border:1px solid var(--border-color,#2b323b);border-radius:.25rem;padding:.1em .4em;" +
"transition:background .15s,border-color .15s;white-space:nowrap;display:inline}" +
".moko-var-chip:hover{background:var(--color-primary,#112855);color:#fff;border-color:var(--color-primary,#112855)}" +
".moko-toast{position:fixed;bottom:1.5rem;left:50%;transform:translateX(-50%);z-index:10000;" +
"background:var(--color-primary,#112855);color:#fff;padding:.6rem 1.25rem;" +
"border-radius:.375rem;font-size:.875rem;box-shadow:0 4px 12px rgba(0,0,0,.25);" +
"opacity:0;transition:opacity .2s;pointer-events:none}" +
".moko-toast--show{opacity:1}";
doc.head.appendChild(style);
}
/**
* Show a brief "Copied to clipboard" toast.
* @param {string} text - The variable name that was copied
*/
function showCopyToast(text) {
var existing = doc.querySelector(".moko-toast");
if (existing) existing.remove();
var toast = doc.createElement("div");
toast.className = "moko-toast";
toast.textContent = "Copied to clipboard: " + text;
doc.body.appendChild(toast);
// Trigger reflow then show
void toast.offsetWidth;
toast.classList.add("moko-toast--show");
setTimeout(function () {
toast.classList.remove("moko-toast--show");
setTimeout(function () { toast.remove(); }, 200);
}, 2000);
}
/**
* Copy text to clipboard and show toast.
* @param {string} text
*/
function copyVariable(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
showCopyToast(text);
});
} else {
// Fallback for older browsers using deprecated API
var ta = doc.createElement("textarea");
ta.value = text;
ta.style.cssText = "position:fixed;left:-9999px";
doc.body.appendChild(ta);
ta.select();
try { doc.execCommand("copy"); } catch (e) { /* noop */ }
ta.remove();
showCopyToast(text);
}
}
/**
* Scan text nodes for CSS variable patterns (--variable-name) and wrap
* each match in a clickable chip that copies the variable to clipboard.
*/
function initVarCopy() {
injectVarCopyStyles();
// Pattern: --[a-zA-Z] followed by word/hyphen chars
var varPattern = /--[a-zA-Z][\w-]*/g;
// Elements to skip (inputs, scripts, styles, already-processed, code editors)
var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, TEXTAREA: 1, INPUT: 1, SELECT: 1, NOSCRIPT: 1 };
var walker = doc.createTreeWalker(
doc.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
if (SKIP_TAGS[node.parentNode.tagName]) return NodeFilter.FILTER_REJECT;
if (node.parentNode.classList && node.parentNode.classList.contains("moko-var-chip")) return NodeFilter.FILTER_REJECT;
if (!varPattern.test(node.nodeValue)) return NodeFilter.FILTER_REJECT;
varPattern.lastIndex = 0;
return NodeFilter.FILTER_ACCEPT;
}
}
);
var textNodes = [];
while (walker.nextNode()) textNodes.push(walker.currentNode);
textNodes.forEach(function (node) {
var text = node.nodeValue;
var frag = doc.createDocumentFragment();
var lastIndex = 0;
var match;
varPattern.lastIndex = 0;
while ((match = varPattern.exec(text)) !== null) {
// Text before the match
if (match.index > lastIndex) {
frag.appendChild(doc.createTextNode(text.slice(lastIndex, match.index)));
}
// Clickable chip
var chip = doc.createElement("span");
chip.className = "moko-var-chip";
chip.textContent = match[0];
chip.setAttribute("role", "button");
chip.setAttribute("tabindex", "0");
chip.setAttribute("title", "Click to copy " + match[0]);
chip.addEventListener("click", (function (varName) {
return function (e) {
e.preventDefault();
copyVariable(varName);
};
})(match[0]));
chip.addEventListener("keydown", (function (varName) {
return function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
copyVariable(varName);
}
};
})(match[0]));
frag.appendChild(chip);
lastIndex = match.index + match[0].length;
}
// Remaining text after last match
if (lastIndex < text.length) {
frag.appendChild(doc.createTextNode(text.slice(lastIndex)));
}
node.parentNode.replaceChild(frag, node);
});
}
/** /**
* Run all template JS initializations * Run all template JS initializations
*/ */
@@ -656,6 +815,7 @@
initBackTop(); initBackTop();
initSearchToggle(); initSearchToggle();
initSidebarAccordion(); initSidebarAccordion();
initVarCopy();
} }
if (doc.readyState === "loading") { if (doc.readyState === "loading") {

View File

@@ -1,5 +1,5 @@
<?php <?php
/* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project. This file is part of a Moko Consulting project.
@@ -26,59 +26,90 @@ use Joomla\CMS\Uri\Uri;
$app = Factory::getApplication(); $app = Factory::getApplication();
$doc = Factory::getDocument(); $doc = Factory::getDocument();
$wa = $doc->getWebAssetManager();
$params = $this->params ?: $app->getTemplate(true)->params; $params = $this->params ?: $app->getTemplate(true)->params;
$direction = $this->direction ?: 'ltr'; $direction = $this->direction ?: 'ltr';
// Register the template's asset manifest (not auto-loaded in offline context)
$manifestPath = JPATH_ROOT . '/media/templates/site/' . $this->template . '/joomla.asset.json';
if (is_file($manifestPath)) {
$wa->getRegistry()->addRegistryFile($manifestPath);
}
// Load language files (not auto-loaded in offline context)
$lang = Factory::getLanguage();
$lang->load('tpl_' . $this->template, JPATH_ROOT . '/templates/' . $this->template);
$lang->load('tpl_' . $this->template, JPATH_ROOT);
$lang->load('com_users', JPATH_ROOT);
$lang->load('com_users', JPATH_ROOT . '/components/com_users');
$lang->load('', JPATH_ROOT);
/* ----------------------- /* -----------------------
Load ONLY template.css + theme palettes (with min toggle) Load assets via WebAssetManager (matches index.php pattern)
------------------------ */ ------------------------ */
$useMin = !((int) $params->get('development_mode', 0) === 1); $params_developmentmode = (bool) $params->get('developmentmode', false) || (bool) $app->get('debug', false);
$assetSuffix = $useMin ? '.min' : ''; $suffix = $params_developmentmode ? '' : '.min';
$base = rtrim(Uri::root(true), '/') . '/templates/' . $this->template . '/css/';
$jsBase = rtrim(Uri::root(true), '/') . '/templates/' . $this->template . '/js/';
$doc->addStyleSheet($base . 'template' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-template']); // Core template CSS + offline overlay CSS
$wa->useStyle('template.base' . $suffix);
$wa->useStyle('template.offline' . $suffix);
/* Load theme palettes */ // Osaka font
$doc->addStyleSheet($base . 'theme/light.standard' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-light-standard']); $wa->useStyle('template.font.osaka');
$doc->addStyleSheet($base . 'theme/dark.standard' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-dark-standard']);
/* Load custom palettes only if selected in template configuration AND files exist */ // Font Awesome 7 Free
$wa->useStyle('vendor.fa7free.all' . $suffix);
// Theme palettes
$wa->useStyle('template.light.standard' . $suffix);
$wa->useStyle('template.dark.standard' . $suffix);
// Custom palettes (if selected and files exist)
$params_LightColorName = (string) $params->get('colorLightName', 'standard'); $params_LightColorName = (string) $params->get('colorLightName', 'standard');
$params_DarkColorName = (string) $params->get('colorDarkName', 'standard'); $params_DarkColorName = (string) $params->get('colorDarkName', 'standard');
if ($params_LightColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokocassiopeia/css/theme/light.custom.css')) if ($params_LightColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokocassiopeia/css/theme/light.custom.css'))
{ {
$doc->addStyleSheet($base . 'theme/light.custom' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-light-custom']); $wa->useStyle('template.light.custom' . $suffix);
} }
if ($params_DarkColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokocassiopeia/css/theme/dark.custom.css')) if ($params_DarkColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokocassiopeia/css/theme/dark.custom.css'))
{ {
$doc->addStyleSheet($base . 'theme/dark.custom' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-dark-custom']); $wa->useStyle('template.dark.custom' . $suffix);
} }
/* Load user assets last (after all other styles and scripts) */ // User overrides (loaded last)
$doc->addStyleSheet($base . 'user' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-user']); $wa->useStyle('template.user');
/* Bootstrap CSS/JS for accordion behavior; safe to keep. */ // Accessibility high-contrast stylesheet
HTMLHelper::_('bootstrap.loadCss', true, $doc); $wa->useStyle('template.a11y-high-contrast');
// Template JS (theme switcher, a11y toolbar, var-copy, etc.)
if ($params_developmentmode) {
$wa->useScript('template.js');
} else {
$wa->useScript('template.js.min');
}
$wa->useScript('user.js');
// Bootstrap CSS + JS (accordion, responsive grid, utilities)
try {
$wa->useStyle('bootstrap.css');
} catch (\Exception $e) {
// Fallback: load via HTMLHelper
HTMLHelper::_('bootstrap.loadCss', true, $doc);
}
HTMLHelper::_('bootstrap.framework'); HTMLHelper::_('bootstrap.framework');
/* Load template.js for theme switcher and other functionality */
$doc->addScript($jsBase . 'template' . $assetSuffix . '.js', ['version' => 'auto', 'defer' => true], ['id' => 'moko-template-js']);
/* Load user.js last for custom user scripts */
$doc->addScript($jsBase . 'user' . $assetSuffix . '.js', ['version' => 'auto', 'defer' => true], ['id' => 'moko-user-js']);
/* ----------------------- /* -----------------------
Title + Meta (Include Site Name in Page Titles) Title + Meta
------------------------ */ ------------------------ */
$sitename = (string) $app->get('sitename'); $sitename = (string) $app->get('sitename');
$baseTitle = Text::_('JGLOBAL_OFFLINE') ?: 'Offline'; $baseTitle = Text::_('JGLOBAL_OFFLINE') ?: 'Offline';
$snSetting = (int) $app->get('sitename_pagetitles', 0); // 0=no, 1=before, 2=after $snSetting = (int) $app->get('sitename_pagetitles', 0);
if ($snSetting === 1) { if ($snSetting === 1) {
$doc->setTitle(Text::sprintf('JPAGETITLE', $sitename, $baseTitle)); // Site Name BEFORE $doc->setTitle(Text::sprintf('JPAGETITLE', $sitename, $baseTitle));
} elseif ($snSetting === 2) { } elseif ($snSetting === 2) {
$doc->setTitle(Text::sprintf('JPAGETITLE', $baseTitle, $sitename)); // Site Name AFTER $doc->setTitle(Text::sprintf('JPAGETITLE', $baseTitle, $sitename));
} else { } else {
$doc->setTitle($baseTitle); $doc->setTitle($baseTitle);
} }
@@ -87,11 +118,21 @@ $doc->setMetaData('robots', 'noindex, nofollow');
/* ----------------------- /* -----------------------
Offline content from Global Config Offline content from Global Config
------------------------ */ ------------------------ */
$displayOfflineMessage = (int) $app->get('display_offline_message', 1); // 0|1|2 $displayOfflineMessage = (int) $app->get('display_offline_message', 1);
$offlineMessage = trim((string) $app->get('offline_message', '')); $offlineMessage = trim((string) $app->get('offline_message', ''));
/* ----------------------- /* -----------------------
Brand: logo from params OR siteTitle (matches index.php) Offline image from Joomla Global Config (System > Global Configuration > Site > Offline Image)
Used as the full-viewport background image.
------------------------ */
$offlineImage = trim((string) $app->get('offline_image', ''));
$bgStyle = '';
if ($offlineImage !== '') {
$bgStyle = 'background-image: url(\'' . htmlspecialchars(Uri::root(false) . $offlineImage, ENT_QUOTES, 'UTF-8') . '\');';
}
/* -----------------------
Brand: logo from template params OR siteTitle
------------------------ */ ------------------------ */
$brandHtml = ''; $brandHtml = '';
$logoFile = (string) $params->get('logoFile'); $logoFile = (string) $params->get('logoFile');
@@ -106,9 +147,8 @@ if ($logoFile !== '') {
0 0
); );
} else { } else {
// If no logo file, show the title (defaults to "MokoCassiopeia" if not set)
$siteTitle = $params->get('siteTitle', 'MokoCassiopeia'); $siteTitle = $params->get('siteTitle', 'MokoCassiopeia');
$brandHtml = '<span class="site-title" title="' . $sitename . '">' $brandHtml = '<span class="site-title" title="' . htmlspecialchars($sitename, ENT_QUOTES, 'UTF-8') . '">'
. htmlspecialchars($siteTitle, ENT_COMPAT, 'UTF-8') . htmlspecialchars($siteTitle, ENT_COMPAT, 'UTF-8')
. '</span>'; . '</span>';
} }
@@ -116,8 +156,34 @@ if ($logoFile !== '') {
$brandTagline = (string) ($params->get('brand_tagline') ?: $params->get('siteDescription') ?: ''); $brandTagline = (string) ($params->get('brand_tagline') ?: $params->get('siteDescription') ?: '');
$showTagline = (int) $params->get('show_brand_tagline', 0); $showTagline = (int) $params->get('show_brand_tagline', 0);
// Favicon
$params_favicon_source = (string) $params->get('favicon_source', '');
$faviconHeadTags = '';
if ($params_favicon_source) {
require_once JPATH_ROOT . '/templates/' . $this->template . '/helper/favicon.php';
$faviconSourceAbs = JPATH_ROOT . '/' . ltrim($params_favicon_source, '/');
$faviconOutputDir = JPATH_ROOT . '/images/favicons';
$faviconUrlBase = Uri::root(true) . '/images/favicons';
if (MokoFaviconHelper::generate($faviconSourceAbs, $faviconOutputDir)) {
$faviconHeadTags = MokoFaviconHelper::getHeadTags($faviconUrlBase);
}
}
// Theme params // Theme params
$params_theme_enabled = (int) $params->get('theme_enabled', 1); $params_theme_enabled = (int) $params->get('theme_enabled', 1);
$params_theme_fab_enabled = (int) $params->get('theme_fab_enabled', 1);
$params_theme_fab_pos = 'br';
// Accessibility params
$params_a11y_toolbar = (int) $params->get('a11y_toolbar_enabled', 1);
$params_a11y_resize = (int) $params->get('a11y_text_resize', 1);
$params_a11y_invert = (int) $params->get('a11y_color_inversion', 1);
$params_a11y_contrast = (int) $params->get('a11y_high_contrast', 1);
$params_a11y_links = (int) $params->get('a11y_highlight_links', 1);
$params_a11y_font = (int) $params->get('a11y_readable_font', 1);
$params_a11y_animations = (int) $params->get('a11y_pause_animations', 1);
$params_a11y_pos = 'br';
// Analytics params // Analytics params
$params_googletagmanager = $params->get('googletagmanager', false); $params_googletagmanager = $params->get('googletagmanager', false);
@@ -131,7 +197,7 @@ if (!empty($params_googlesitekey)) {
} }
/* ----------------------- /* -----------------------
Login routes & Users Login routes
------------------------ */ ------------------------ */
$action = Route::_('index.php', true); $action = Route::_('index.php', true);
$return = base64_encode(Uri::base()); $return = base64_encode(Uri::base());
@@ -152,10 +218,12 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
<head> <head>
<jdoc:include type="head" /> <jdoc:include type="head" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<?php if ($faviconHeadTags) : ?>
<?php echo $faviconHeadTags; ?>
<?php endif; ?>
<?php if ($params_theme_enabled) : ?> <?php if ($params_theme_enabled) : ?>
<script> <script>
// Early theme application to avoid FOUC
(function () { (function () {
try { try {
var stored = localStorage.getItem('theme'); var stored = localStorage.getItem('theme');
@@ -168,20 +236,21 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
</script> </script>
<?php endif; ?> <?php endif; ?>
<style>
.moko-offline-wrap { min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; }
.moko-offline-main { display: grid; place-items: center; padding: 2rem 1rem; }
.moko-card { max-width: 720px; width: 100%; }
.moko-brand { display:flex; align-items:center; gap:.75rem; text-decoration:none; }
.moko-brand .brand-tagline { display:block; opacity:.75; font-size:.875rem; line-height:1.2; }
.skip-link { position:absolute; left:-9999px; top:auto; width:1px; height:1px; overflow:hidden; }
.skip-link:focus { position:static; width:auto; height:auto; padding:.5rem 1rem; }
</style>
</head> </head>
<body class="site moko-offline-wrap <?php echo htmlspecialchars($direction, ENT_QUOTES, 'UTF-8'); ?>"> <body class="site moko-offline-wrap"
data-theme-fab-enabled="<?php echo $params_theme_fab_enabled ? '1' : '0'; ?>"
data-theme-fab-pos="<?php echo htmlspecialchars($params_theme_fab_pos, ENT_QUOTES, 'UTF-8'); ?>"
data-a11y-toolbar="<?php echo $params_a11y_toolbar ? '1' : '0'; ?>"
data-a11y-resize="<?php echo $params_a11y_resize ? '1' : '0'; ?>"
data-a11y-invert="<?php echo $params_a11y_invert ? '1' : '0'; ?>"
data-a11y-contrast="<?php echo $params_a11y_contrast ? '1' : '0'; ?>"
data-a11y-links="<?php echo $params_a11y_links ? '1' : '0'; ?>"
data-a11y-font="<?php echo $params_a11y_font ? '1' : '0'; ?>"
data-a11y-animations="<?php echo $params_a11y_animations ? '1' : '0'; ?>"
data-a11y-pos="<?php echo htmlspecialchars($params_a11y_pos, ENT_QUOTES, 'UTF-8'); ?>"
<?php if ($bgStyle) : ?>style="<?php echo $bgStyle; ?>"<?php endif; ?>>
<?php if (!empty($params_googletagmanager) && !empty($params_googletagmanagerid)) : <?php if (!empty($params_googletagmanager) && !empty($params_googletagmanagerid)) :
$gtmID = htmlspecialchars($params_googletagmanagerid, ENT_QUOTES, 'UTF-8'); ?> $gtmID = htmlspecialchars($params_googletagmanagerid, ENT_QUOTES, 'UTF-8'); ?>
<!-- Google Tag Manager -->
<script> <script>
(function(w,d,s,l,i){ (function(w,d,s,l,i){
w[l]=w[l]||[]; w[l]=w[l]||[];
@@ -194,19 +263,14 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
f.parentNode.insertBefore(j,f); f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','<?php echo $gtmID; ?>'); })(window,document,'script','dataLayer','<?php echo $gtmID; ?>');
</script> </script>
<!-- End Google Tag Manager -->
<!-- Google Tag Manager (noscript) -->
<noscript> <noscript>
<iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo $gtmID; ?>" <iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo $gtmID; ?>"
height="0" width="0" style="display:none;visibility:hidden"></iframe> height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript> </noscript>
<!-- End Google Tag Manager (noscript) -->
<?php endif; ?> <?php endif; ?>
<?php if (!empty($params_googleanalytics) && !empty($params_googleanalyticsid)) : <?php if (!empty($params_googleanalytics) && !empty($params_googleanalyticsid)) :
$gaId = htmlspecialchars($params_googleanalyticsid, ENT_QUOTES, 'UTF-8'); ?> $gaId = htmlspecialchars($params_googleanalyticsid, ENT_QUOTES, 'UTF-8'); ?>
<!-- Google Analytics (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo $gaId; ?>"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo $gaId; ?>"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
@@ -223,136 +287,128 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
gtag('config', id, { 'anonymize_ip': true }); gtag('config', id, { 'anonymize_ip': true });
} else if (/^UA-/.test(id)) { } else if (/^UA-/.test(id)) {
gtag('config', id, { 'anonymize_ip': true }); gtag('config', id, { 'anonymize_ip': true });
console.warn('Using a UA- ID. Universal Analytics is sunset; consider migrating to GA4.');
} else { } else {
console.warn('Unrecognized Google Analytics ID format:', id); console.warn('Unrecognized Google Analytics ID format:', id);
} }
})('<?php echo $gaId; ?>'); })('<?php echo $gaId; ?>');
</script> </script>
<!-- End Google Analytics -->
<?php endif; ?> <?php endif; ?>
<a class="skip-link" href="#maincontent"><?php echo Text::_('JSKIP_TO_CONTENT') ?: 'Skip to content'; ?></a> <a class="skip-link" href="#maincontent"><?php echo Text::_('JSKIP_TO_CONTENT') ?: 'Skip to content'; ?></a>
<header class="container-header header py-3"> <!-- Centered overlay card -->
<div class="grid-child container-nav d-flex align-items-center gap-3"> <main id="maincontent">
<div class="moko-offline-card">
<!-- Brand (mutually exclusive image/text) --> <!-- Logo -->
<a class="moko-brand me-auto" href="<?php echo htmlspecialchars(Uri::base(), ENT_QUOTES, 'UTF-8'); ?>" aria-label="<?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?>"> <a class="moko-offline-brand" href="<?php echo htmlspecialchars(Uri::base(), ENT_QUOTES, 'UTF-8'); ?>" aria-label="<?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?>">
<?php echo $brandHtml; ?> <?php echo $brandHtml; ?>
<?php if ($showTagline && $brandTagline): ?> <?php if ($showTagline && $brandTagline): ?>
<small class="brand-tagline"><?php echo htmlspecialchars($brandTagline, ENT_COMPAT, 'UTF-8'); ?></small> <small class="brand-tagline"><?php echo htmlspecialchars($brandTagline, ENT_COMPAT, 'UTF-8'); ?></small>
<?php endif; ?> <?php endif; ?>
</a> </a>
<!-- Header module position: offline-header --> <!-- Offline message: 0=hidden, 1=custom message, 2=system language string -->
<?php if ($this->countModules('offline-header')) : ?> <?php if ($displayOfflineMessage === 1 && $offlineMessage !== '') : ?>
<div class="ms-2"> <div class="moko-offline-message">
<jdoc:include type="modules" name="offline-header" style="none" /> <p><?php echo $offlineMessage; ?></p>
</div>
<?php elseif ($displayOfflineMessage === 2) : ?>
<div class="moko-offline-message">
<p><?php echo Text::_('JOFFLINE_MESSAGE') ?: 'This site is down for maintenance.'; ?></p>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> <!-- Offline module position -->
</header> <?php if ($this->countModules('offline')) : ?>
<div class="moko-offline-modules">
<jdoc:include type="modules" name="offline" style="none" />
</div>
<?php endif; ?>
<main id="maincontent" class="moko-offline-main"> <!-- Login accordion -->
<div class="container"> <div class="accordion" id="offlineAccordion">
<jdoc:include type="message" /> <div class="accordion-item">
<h2 class="accordion-header" id="headingLogin">
<button class="accordion-button collapsed" type="button"
data-bs-toggle="collapse" data-bs-target="#collapseLogin"
aria-expanded="false" aria-controls="collapseLogin">
<?php echo Text::_('JLOGIN'); ?>
</button>
</h2>
<div id="collapseLogin" class="accordion-collapse collapse" aria-labelledby="headingLogin" data-bs-parent="#offlineAccordion">
<div class="accordion-body">
<form action="<?php echo $action; ?>" method="post" class="form-validate">
<fieldset>
<legend class="visually-hidden"><?php echo Text::_('JLOGIN'); ?></legend>
<div class="moko-card card shadow-sm rounded-3 p-4 p-md-5"> <div class="mb-3">
<?php if ($displayOfflineMessage === 1 && $offlineMessage !== '') : ?> <label class="form-label" for="username"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label>
<div class="mb-4"> <input class="form-control" type="text" name="username" id="username" autocomplete="username" required aria-required="true">
<h1 class="h3 mb-2"><?php echo Text::_('JOFFLINE_MESSAGE') ?: 'Site Offline'; ?></h1> </div>
<p class="lead mb-0"><?php echo $offlineMessage; ?></p>
</div>
<?php elseif ($displayOfflineMessage === 2) : ?>
<div class="mb-4">
<h1 class="h3 mb-2"><?php echo Text::_('JOFFLINE_MESSAGE') ?: 'Site Offline'; ?></h1>
<p class="lead mb-0">
<?php echo Text::_('JOFFLINE_MESSAGE_DEFAULT') ?: 'This site is down for maintenance. Please check back soon.'; ?>
</p>
</div>
<?php endif; ?>
<!-- Main offline module position --> <div class="mb-3">
<?php if ($this->countModules('offline')) : ?> <label class="form-label" for="password"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
<section class="mb-4" aria-label="Offline modules"> <input class="form-control" type="password" name="password" id="password" autocomplete="current-password" required aria-required="true">
<jdoc:include type="modules" name="offline" style="none" /> </div>
</section>
<?php endif; ?>
<!-- Login UNDER an accordion (collapsed by default) --> <div class="mb-3">
<div class="accordion" id="offlineAccordion"> <label class="form-label" for="secretkey"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label>
<div class="accordion-item"> <input class="form-control" type="text" name="secretkey" id="secretkey" autocomplete="one-time-code" placeholder="<?php echo Text::_('JGLOBAL_SECRETKEY'); ?>">
<h2 class="accordion-header" id="headingLogin"> </div>
<button class="accordion-button collapsed" type="button"
data-bs-toggle="collapse" data-bs-target="#collapseLogin"
aria-expanded="false" aria-controls="collapseLogin">
<?php echo Text::_('JLOGIN'); ?>
</button>
</h2>
<div id="collapseLogin" class="accordion-collapse collapse" aria-labelledby="headingLogin" data-bs-parent="#offlineAccordion">
<div class="accordion-body">
<form action="<?php echo $action; ?>" method="post" class="form-validate">
<fieldset>
<legend class="visually-hidden"><?php echo Text::_('JLOGIN'); ?></legend>
<div class="mb-3"> <div class="form-check mb-4">
<label class="form-label" for="username"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label> <input class="form-check-input" type="checkbox" name="remember" id="remember">
<input class="form-control" type="text" name="username" id="username" autocomplete="username" required aria-required="true"> <label class="form-check-label" for="remember"><?php echo Text::_('JGLOBAL_REMEMBER_ME'); ?></label>
</div> </div>
<div class="mb-3"> <div class="d-grid">
<label class="form-label" for="password"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label> <button type="submit" class="btn btn-primary"><?php echo Text::_('JLOGIN'); ?></button>
<input class="form-control" type="password" name="password" id="password" autocomplete="current-password" required aria-required="true"> </div>
</div>
<div class="mb-3"> <input type="hidden" name="option" value="com_users">
<label class="form-label" for="secretkey"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label> <input type="hidden" name="task" value="user.login">
<input class="form-control" type="text" name="secretkey" id="secretkey" autocomplete="one-time-code" placeholder="<?php echo Text::_('JGLOBAL_SECRETKEY'); ?>"> <input type="hidden" name="return" value="<?php echo $return; ?>">
</div> <?php echo HTMLHelper::_('form.token'); ?>
</fieldset>
<div class="form-check mb-4"> <nav class="mt-3 small" aria-label="<?php echo Text::_('COM_USERS'); ?>">
<input class="form-check-input" type="checkbox" name="remember" id="remember"> <ul class="list-inline m-0">
<label class="form-check-label" for="remember"><?php echo Text::_('JGLOBAL_REMEMBER_ME'); ?></label> <li class="list-inline-item">
</div> <a href="<?php echo $resetUrl; ?>"><?php echo Text::_('COM_USERS_LOGIN_RESET'); ?></a>
</li>
<div class="d-grid"> <li class="list-inline-item">
<button type="submit" class="btn btn-primary"><?php echo Text::_('JLOGIN'); ?></button> <a href="<?php echo $remindUrl; ?>"><?php echo Text::_('COM_USERS_LOGIN_REMIND'); ?></a>
</div> </li>
<?php if ($allowRegistration) : ?>
<input type="hidden" name="option" value="com_users">
<input type="hidden" name="task" value="user.login">
<input type="hidden" name="return" value="<?php echo $return; ?>">
<?php echo HTMLHelper::_('form.token'); ?>
</fieldset>
<nav class="mt-3 small" aria-label="<?php echo Text::_('COM_USERS'); ?>">
<ul class="list-inline m-0">
<li class="list-inline-item"> <li class="list-inline-item">
<a href="<?php echo $resetUrl; ?>"><?php echo Text::_('COM_USERS_LOGIN_RESET'); ?></a> <a href="<?php echo $registrationUrl; ?>"><?php echo Text::_('COM_USERS_REGISTER'); ?></a>
</li> </li>
<li class="list-inline-item"> <?php endif; ?>
<a href="<?php echo $remindUrl; ?>"><?php echo Text::_('COM_USERS_LOGIN_REMIND'); ?></a> </ul>
</li> </nav>
<?php if ($allowRegistration) : ?> </form>
<li class="list-inline-item">
<a href="<?php echo $registrationUrl; ?>"><?php echo Text::_('COM_USERS_REGISTER'); ?></a>
</li>
<?php endif; ?>
</ul>
</nav>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- /accordion -->
</div> </div>
<!-- Copyright -->
<div class="moko-offline-copyright">
<div>&copy; <?php echo date('Y'); ?> <?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?></div>
<div><?php echo Text::_('MOD_FOOTER_LINE2'); ?></div>
</div>
</div> </div>
</main> </main>
<!-- No footer modules on offline page --> <!-- Offline footer module position -->
<?php if ($this->countModules('offline-footer')) : ?>
<div class="moko-offline-messages mt-3">
<jdoc:include type="modules" name="offline-footer" style="none" />
</div>
<?php endif; ?>
<jdoc:include type="modules" name="debug" style="none" /> <jdoc:include type="modules" name="debug" style="none" />
</body> </body>
</html> </html>

View File

@@ -97,7 +97,7 @@ class Tpl_MokocassiopeiaInstallerScript
*/ */
public function update(InstallerAdapter $parent): bool public function update(InstallerAdapter $parent): bool
{ {
$this->logMessage('MokoCassiopeia template updated.'); $this->logMessage('MokoCassiopeia template updated to v03.10.00 (bridge release).');
// Run CSS variable sync to inject any new variables into user's custom palettes. // Run CSS variable sync to inject any new variables into user's custom palettes.
$synced = $this->syncCustomVariables($parent); $synced = $this->syncCustomVariables($parent);
@@ -113,6 +113,15 @@ class Tpl_MokocassiopeiaInstallerScript
); );
} }
// Bridge migration: MokoCassiopeia → MokoOnyx
$bridgeScript = __DIR__ . '/helper/bridge.php';
if (is_file($bridgeScript)) {
require_once $bridgeScript;
if (class_exists('MokoBridgeMigration')) {
MokoBridgeMigration::run();
}
}
return true; return true;
} }

View File

@@ -34,11 +34,11 @@ final class MokoCssVarSync
*/ */
private const PALETTES = [ private const PALETTES = [
[ [
'starter' => 'templates/light.custom.css', 'starter' => 'media/css/theme/light.standard.css',
'user' => 'media/templates/site/%s/css/theme/light.custom.css', 'user' => 'media/templates/site/%s/css/theme/light.custom.css',
], ],
[ [
'starter' => 'templates/dark.custom.css', 'starter' => 'media/css/theme/dark.standard.css',
'user' => 'media/templates/site/%s/css/theme/dark.custom.css', 'user' => 'media/templates/site/%s/css/theme/dark.custom.css',
], ],
]; ];
@@ -97,28 +97,24 @@ final class MokoCssVarSync
private static function syncFile(string $starterPath, string $userPath): array private static function syncFile(string $starterPath, string $userPath): array
{ {
$starterVars = self::extractVarsWithContext($starterPath); $starterVars = self::extractVarsWithContext($starterPath);
$userVars = self::extractVarNames($userPath); $userVarsMap = self::extractVarsWithContext($userPath);
$userNames = self::extractVarNames($userPath);
// Find missing variables
$missing = []; $missing = [];
foreach ($starterVars as $name => $declaration) { foreach ($starterVars as $name => $declaration) {
if (!isset($userVars[$name])) { if (!isset($userNames[$name])) {
$missing[$name] = $declaration; $missing[$name] = $declaration;
} }
} }
if (empty($missing)) { // Rebuild the entire :root block in starter file order.
return ['added' => [], 'skipped' => []]; // User's custom values are preserved; missing vars get starter defaults.
} $reordered = self::rebuildInStarterOrder($starterPath, $userVarsMap, $missing);
// Group missing variables by their section comment header. // Replace the :root block in the user file with the reordered version.
$sections = self::groupBySection($missing, $starterPath);
// Build the injection block.
$injection = self::buildInjectionBlock($sections);
// Insert before the closing } of the :root rule.
$userCss = file_get_contents($userPath); $userCss = file_get_contents($userPath);
$userCss = self::injectBeforeRootClose($userCss, $injection); $userCss = self::replaceRootBlock($userCss, $reordered);
// Write back (atomic: write to .tmp then rename). // Write back (atomic: write to .tmp then rename).
$tmpPath = $userPath . '.tmp'; $tmpPath = $userPath . '.tmp';
@@ -128,6 +124,104 @@ final class MokoCssVarSync
return ['added' => array_keys($missing), 'skipped' => []]; return ['added' => array_keys($missing), 'skipped' => []];
} }
/**
* Rebuild all variables in the order they appear in the starter file.
* User values are preserved; missing vars use starter defaults.
*
* @param string $starterPath Path to starter file.
* @param array $userVars User's variable name => declaration.
* @param array $missing Missing variable name => starter declaration.
* @return string Complete CSS content for inside :root { }.
*/
private static function rebuildInStarterOrder(string $starterPath, array $userVars, array $missing): string
{
$lines = file($starterPath, FILE_IGNORE_NEW_LINES);
$output = [];
$inRoot = false;
$depth = 0;
foreach ($lines as $line) {
// Track when we enter :root (brace may be on same line)
if (!$inRoot && preg_match('/:root/', $line)) {
$inRoot = true;
// If { is on this same line, don't skip it — just continue processing
if (strpos($line, '{') === false) {
continue;
}
// Fall through to process the rest of this line
}
if (!$inRoot) {
continue;
}
// Track braces (skip lines that are ONLY a brace)
$trimmed = trim($line);
if ($trimmed === '{') {
continue;
}
if ($trimmed === '}') {
break; // End of :root
}
// Section comment headers — always include
if (preg_match('/\/\*\s*=+\s*.+?\s*=+\s*\*\//', $line)) {
$output[] = $line;
continue;
}
// Regular comments — include
if (preg_match('/^\s*\/\*/', $line) || preg_match('/^\s*\*/', $line)) {
$output[] = $line;
continue;
}
// Blank lines — include
if (trim($line) === '') {
$output[] = '';
continue;
}
// Variable declaration
if (preg_match('/^\s*(--[\w-]+)\s*:/', $line, $m)) {
$name = trim($m[1]);
if (isset($userVars[$name])) {
// Use the user's custom value
$output[] = $userVars[$name];
} elseif (isset($missing[$name])) {
// New variable — use starter default
$output[] = $missing[$name];
}
continue;
}
// Other lines (e.g. color-scheme) — include as-is
$output[] = $line;
}
return implode("\n", $output);
}
/**
* Replace the content inside :root { ... } with new content.
*/
private static function replaceRootBlock(string $css, string $newContent): string
{
$rootStart = preg_match('/:root[^{]*\{/', $css, $m, PREG_OFFSET_CAPTURE);
if (!$rootStart) {
return $css;
}
$openBrace = $m[0][1] + strlen($m[0][0]);
$closeBrace = self::findRootClosingBrace($css);
if ($closeBrace === false) {
return $css;
}
return substr($css, 0, $openBrace) . "\n" . $newContent . "\n" . substr($css, $closeBrace);
}
/** /**
* Extract CSS custom property declarations with their full text (name: value). * Extract CSS custom property declarations with their full text (name: value).
* Only extracts from the first :root block. * Only extracts from the first :root block.
@@ -181,29 +275,25 @@ final class MokoCssVarSync
{ {
$lines = file($starterPath, FILE_IGNORE_NEW_LINES); $lines = file($starterPath, FILE_IGNORE_NEW_LINES);
$section = 'Uncategorised'; $section = 'Uncategorised';
$map = []; // variable name => section
// Walk the starter file in order — this preserves the original
// variable ordering so injected variables match the standard theme layout.
$sections = [];
foreach ($lines as $line) { foreach ($lines as $line) {
// Detect section comment headers like /* ===== HERO VARIANTS ===== */ // Detect section comment headers like /* ===== HERO VARIANTS ===== */
if (preg_match('/\/\*\s*=+\s*(.+?)\s*=+\s*\*\//', $line, $m)) { if (preg_match('/\/\*\s*=+\s*(.+?)\s*=+\s*\*\//', $line, $m)) {
$section = trim($m[1]); $section = trim($m[1]);
} }
// Detect variable declaration // Detect variable declaration — only include if it's missing from user file
if (preg_match('/^\s*(--[\w-]+)\s*:/', $line, $m)) { if (preg_match('/^\s*(--[\w-]+)\s*:/', $line, $m)) {
$name = trim($m[1]); $name = trim($m[1]);
if (isset($missing[$name])) { if (isset($missing[$name])) {
$map[$name] = $section; $sections[$section][] = $missing[$name];
} }
} }
} }
// Group by section
$sections = [];
foreach ($missing as $name => $declaration) {
$sec = $map[$name] ?? 'Uncategorised';
$sections[$sec][] = $declaration;
}
return $sections; return $sections;
} }

View File

@@ -39,13 +39,13 @@
</server> </server>
</updateservers> </updateservers>
<name>MokoCassiopeia</name> <name>MokoCassiopeia</name>
<version>03.09.14</version> <version>03.10.07</version>
<scriptfile>script.php</scriptfile> <scriptfile>script.php</scriptfile>
<creationDate>2026-04-14</creationDate> <creationDate>2026-04-15</creationDate>
<author>Jonathan Miller || Moko Consulting</author> <author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright> <copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<description><![CDATA[<p><img src="https://img.shields.io/badge/version-03.09.14-blue.svg?logo=v&amp;logoColor=white" alt="Version 03.09.14" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&amp;logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&amp;logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&amp;logoColor=white" alt="PHP" /></p> <h3>MokoCassiopeia Template Description</h3> <p> <strong>MokoCassiopeia</strong> continues Joomla's tradition of space-themed default templates— building on the legacy of <em>Solarflare</em> (Joomla 1.0), <em>Milkyway</em> (Joomla 1.5), and <em>Protostar</em> (Joomla 3.0). </p> <p> This template is a customized fork of the <strong>Cassiopeia</strong> 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. </p> <h4>Custom Colour Themes</h4> <p> Starter palette files are included with the template. To create a custom colour scheme, copy <code>templates/mokocassiopeia/templates/light.custom.css</code> to <code>media/templates/site/mokocassiopeia/css/theme/light.custom.css</code>, or <code>templates/mokocassiopeia/templates/dark.custom.css</code> to <code>media/templates/site/mokocassiopeia/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoCassiopeia → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS &amp; JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokocassiopeia/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokocassiopeia/js/user.js</code> — loaded on every page for custom JavaScript.</li> </ul> <p> These files are gitignored and will not be overwritten by template updates. </p> <h4>Code Attribution</h4> <p> This template is based on the original <strong>Cassiopeia</strong> template developed by the <a href="https://www.joomla.org" target="_blank" rel="noopener">Joomla! Project</a> and released under the GNU General Public License. </p> <p> Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards. </p> <p> It includes integration with <a href="https://afeld.github.io/bootstrap-toc/" target="_blank" rel="noopener">Bootstrap TOC</a>, an open-source table of contents generator by A. Feld, licensed under the MIT License. </p> <p> All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable. </p>]]></description> <description><![CDATA[<p><img src="https://img.shields.io/badge/version-03.10.07-blue.svg?logo=v&amp;logoColor=white" alt="Version 03.10.07" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&amp;logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&amp;logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&amp;logoColor=white" alt="PHP" /></p> <h3>MokoCassiopeia Template Description</h3> <p> <strong>MokoCassiopeia</strong> continues Joomla's tradition of space-themed default templates— building on the legacy of <em>Solarflare</em> (Joomla 1.0), <em>Milkyway</em> (Joomla 1.5), and <em>Protostar</em> (Joomla 3.0). </p> <p> This template is a customized fork of the <strong>Cassiopeia</strong> 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. </p> <h4>Custom Colour Themes</h4> <p> Starter palette files are included with the template. To create a custom colour scheme, copy <code>templates/mokocassiopeia/templates/light.custom.css</code> to <code>media/templates/site/mokocassiopeia/css/theme/light.custom.css</code>, or <code>templates/mokocassiopeia/templates/dark.custom.css</code> to <code>media/templates/site/mokocassiopeia/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoCassiopeia → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS &amp; JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokocassiopeia/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokocassiopeia/js/user.js</code> — loaded on every page for custom JavaScript.</li> </ul> <p> These files are gitignored and will not be overwritten by template updates. </p> <h4>Code Attribution</h4> <p> This template is based on the original <strong>Cassiopeia</strong> template developed by the <a href="https://www.joomla.org" target="_blank" rel="noopener">Joomla! Project</a> and released under the GNU General Public License. </p> <p> Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards. </p> <p> It includes integration with <a href="https://afeld.github.io/bootstrap-toc/" target="_blank" rel="noopener">Bootstrap TOC</a>, an open-source table of contents generator by A. Feld, licensed under the MIT License. </p> <p> All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable. </p>]]></description>
<inheritable>1</inheritable> <inheritable>1</inheritable>
<files> <files>
<filename>component.php</filename> <filename>component.php</filename>
@@ -295,7 +295,8 @@
<option value="0">JNO</option> <option value="0">JNO</option>
<option value="1">JYES</option> <option value="1">JYES</option>
</field> </field>
<field name="a11y_toolbar_pos" type="list" default="tl" <!-- Position forced to bottom-right in index.php -->
<field name="a11y_toolbar_pos" type="hidden" default="br"
label="TPL_MOKO_A11Y_TOOLBAR_POS" description="TPL_MOKO_A11Y_TOOLBAR_POS_DESC" label="TPL_MOKO_A11Y_TOOLBAR_POS" description="TPL_MOKO_A11Y_TOOLBAR_POS_DESC"
showon="a11y_toolbar_enabled:1"> showon="a11y_toolbar_enabled:1">
<option value="br">Bottom-right</option> <option value="br">Bottom-right</option>
@@ -312,7 +313,8 @@
<option value="0">JNO</option> <option value="0">JNO</option>
<option value="1">JYES</option> <option value="1">JYES</option>
</field> </field>
<field name="theme_fab_pos" type="list" default="br" <!-- Position forced to bottom-right in index.php -->
<field name="theme_fab_pos" type="hidden" default="br"
label="TPL_MOKO_THEME_FAB_POS" description="TPL_MOKO_THEME_FAB_POS_DESC"> label="TPL_MOKO_THEME_FAB_POS" description="TPL_MOKO_THEME_FAB_POS_DESC">
<option value="br">Bottom-right</option> <option value="br">Bottom-right</option>
<option value="bl">Bottom-left</option> <option value="bl">Bottom-left</option>
@@ -365,6 +367,7 @@
<field name="css_vars_offcanvas" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_OFFCANVAS_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_OFFCANVAS_DESC" class="alert alert-light" /> <field name="css_vars_offcanvas" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_OFFCANVAS_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_OFFCANVAS_DESC" class="alert alert-light" />
<field name="css_vars_virtuemart" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC" class="alert alert-light" /> <field name="css_vars_virtuemart" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC" class="alert alert-light" />
<field name="css_vars_gable" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC" class="alert alert-light" /> <field name="css_vars_gable" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC" class="alert alert-light" />
<field name="css_vars_footer" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_DESC" class="alert alert-light" />
</fieldset> </fieldset>
<!-- Theme Preview tab — embedded test sheet --> <!-- Theme Preview tab — embedded test sheet -->
@@ -372,12 +375,6 @@
<field name="theme_preview_intro" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO" /> <field name="theme_preview_intro" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO" />
<field name="theme_preview_frame" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME" /> <field name="theme_preview_frame" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME" />
</fieldset> </fieldset>
<!-- Brand Showcase tab — color system, gradients, interactive sampler -->
<fieldset name="brand_showcase" label="TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FIELDSET_LABEL">
<field name="brand_showcase_intro" type="note" description="TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_INTRO" />
<field name="brand_showcase_frame" type="note" description="TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FRAME" />
</fieldset>
</fields> </fields>
</config> </config>
</extension> </extension>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,327 +0,0 @@
@charset "UTF-8";
/* 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.Templates
PATH: ./templates/dark.custom.css
VERSION: 03.09.03
BRIEF: Custom dark theme color template with Bootstrap button definitions
*/
/* -----------------------------------------------
* CUSTOM DARK THEME TEMPLATE
*
* Copy this file to:
* src/media/css/theme/dark.custom.css
*
* Then register it in src/joomla.asset.json under
* template.dark.custom asset.
* --------------------------------------------- */
:root[data-bs-theme='dark'] {
/* ===== BRAND COLORS (Customize these) ===== */
--color-primary: #3399ff;
--accent-color-primary: #66b3ff;
--accent-color-secondary: #99ccff;
/* ===== LINKS ===== */
--link-color: #6bb3ff;
--link-hover-color: #99ccff;
/* ===== BODY & TYPOGRAPHY ===== */
--body-color: #e9ecef;
--body-bg: #0e1318;
/* ===== BOOTSTRAP STATE COLORS ===== */
--success: #5cb85c;
--info: #5bc0de;
--warning: #ffc107;
--danger: #d9534f;
/* ===== NAVIGATION ===== */
--nav-bg-color: var(--color-primary);
--nav-text-color: #ffffff;
}
/* ===== BOOTSTRAP BUTTON VARIANTS ===== */
.btn-primary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--color-primary);
--btn-border-color: var(--color-primary);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #2680e6;
--btn-hover-border-color: #1f73d9;
--btn-focus-shadow-rgb: 82, 168, 255;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #1f73d9;
--btn-active-border-color: #1a66cc;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--color-primary);
--btn-disabled-border-color: var(--color-primary);
}
.btn-secondary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: #6c757d;
--btn-border-color: #6c757d;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #5c636a;
--btn-hover-border-color: #565e64;
--btn-focus-shadow-rgb: 130, 138, 145;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #565e64;
--btn-active-border-color: #51585e;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: #6c757d;
--btn-disabled-border-color: #6c757d;
}
.btn-success {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--success);
--btn-border-color: var(--success);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #4cae4c;
--btn-hover-border-color: #449d44;
--btn-focus-shadow-rgb: 113, 198, 113;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #449d44;
--btn-active-border-color: #398439;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--success);
--btn-disabled-border-color: var(--success);
}
.btn-info {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--info);
--btn-border-color: var(--info);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #46b8da;
--btn-hover-border-color: #31b0d5;
--btn-focus-shadow-rgb: 116, 204, 233;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #31b0d5;
--btn-active-border-color: #269abc;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--info);
--btn-disabled-border-color: var(--info);
}
.btn-warning {
--btn-color: hsl(0, 0%, 0%);
--btn-bg: var(--warning);
--btn-border-color: var(--warning);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #edb100;
--btn-hover-border-color: #d39e00;
--btn-focus-shadow-rgb: 222, 170, 12;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #d39e00;
--btn-active-border-color: #c69500;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 0%);
--btn-disabled-bg: var(--warning);
--btn-disabled-border-color: var(--warning);
}
.btn-danger {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--danger);
--btn-border-color: var(--danger);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #d43f3a;
--btn-hover-border-color: #c9302c;
--btn-focus-shadow-rgb: 223, 109, 105;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #c9302c;
--btn-active-border-color: #ac2925;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--danger);
--btn-disabled-border-color: var(--danger);
}
.btn-light {
--btn-color: hsl(0, 0%, 0%);
--btn-bg: #e9ecef;
--btn-border-color: #e9ecef;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #d3d6d9;
--btn-hover-border-color: #c8cbcf;
--btn-focus-shadow-rgb: 204, 207, 210;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #c8cbcf;
--btn-active-border-color: #bdc1c5;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 0%);
--btn-disabled-bg: #e9ecef;
--btn-disabled-border-color: #e9ecef;
}
.btn-dark {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: #2c3136;
--btn-border-color: #2c3136;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #1e2124;
--btn-hover-border-color: #191c1f;
--btn-focus-shadow-rgb: 70, 75, 80;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #191c1f;
--btn-active-border-color: #14161a;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: #2c3136;
--btn-disabled-border-color: #2c3136;
}
/* ===== OUTLINE BUTTON VARIANTS ===== */
.btn-outline-primary {
--btn-color: var(--color-primary);
--btn-border-color: var(--color-primary);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--color-primary);
--btn-hover-border-color: var(--color-primary);
--btn-focus-shadow-rgb: 82, 168, 255;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--color-primary);
--btn-active-border-color: var(--color-primary);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--color-primary);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--color-primary);
--gradient: none;
}
.btn-outline-secondary {
--btn-color: #8c959f;
--btn-border-color: #8c959f;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #8c959f;
--btn-hover-border-color: #8c959f;
--btn-focus-shadow-rgb: 150, 158, 167;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #8c959f;
--btn-active-border-color: #8c959f;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #8c959f;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #8c959f;
--gradient: none;
}
.btn-outline-success {
--btn-color: var(--success);
--btn-border-color: var(--success);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--success);
--btn-hover-border-color: var(--success);
--btn-focus-shadow-rgb: 113, 198, 113;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--success);
--btn-active-border-color: var(--success);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--success);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--success);
--gradient: none;
}
.btn-outline-info {
--btn-color: var(--info);
--btn-border-color: var(--info);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--info);
--btn-hover-border-color: var(--info);
--btn-focus-shadow-rgb: 116, 204, 233;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--info);
--btn-active-border-color: var(--info);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--info);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--info);
--gradient: none;
}
.btn-outline-warning {
--btn-color: var(--warning);
--btn-border-color: var(--warning);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--warning);
--btn-hover-border-color: var(--warning);
--btn-focus-shadow-rgb: 222, 170, 12;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--warning);
--btn-active-border-color: var(--warning);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--warning);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--warning);
--gradient: none;
}
.btn-outline-danger {
--btn-color: var(--danger);
--btn-border-color: var(--danger);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--danger);
--btn-hover-border-color: var(--danger);
--btn-focus-shadow-rgb: 223, 109, 105;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--danger);
--btn-active-border-color: var(--danger);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--danger);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--danger);
--gradient: none;
}
.btn-outline-light {
--btn-color: #e9ecef;
--btn-border-color: #e9ecef;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #e9ecef;
--btn-hover-border-color: #e9ecef;
--btn-focus-shadow-rgb: 204, 207, 210;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #e9ecef;
--btn-active-border-color: #e9ecef;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #e9ecef;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #e9ecef;
--gradient: none;
}
.btn-outline-dark {
--btn-color: #4a5057;
--btn-border-color: #4a5057;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #4a5057;
--btn-hover-border-color: #4a5057;
--btn-focus-shadow-rgb: 90, 95, 100;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #4a5057;
--btn-active-border-color: #4a5057;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #4a5057;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #4a5057;
--gradient: none;
}

View File

@@ -1,327 +0,0 @@
@charset "UTF-8";
/* 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.Templates
PATH: ./templates/light.custom.css
VERSION: 03.09.03
BRIEF: Custom light theme color template with Bootstrap button definitions
*/
/* -----------------------------------------------
* CUSTOM LIGHT THEME TEMPLATE
*
* Copy this file to:
* src/media/css/theme/light.custom.css
*
* Then register it in src/joomla.asset.json under
* template.light.custom asset.
* --------------------------------------------- */
:root[data-bs-theme="light"] {
/* ===== BRAND COLORS (Customize these) ===== */
--color-primary: #0066cc;
--accent-color-primary: #3399ff;
--accent-color-secondary: #66b3ff;
/* ===== LINKS ===== */
--link-color: #0066cc;
--link-hover-color: #0052a3;
/* ===== BODY & TYPOGRAPHY ===== */
--body-color: #212529;
--body-bg: #ffffff;
/* ===== BOOTSTRAP STATE COLORS ===== */
--success: #28a745;
--info: #17a2b8;
--warning: #ffc107;
--danger: #dc3545;
/* ===== NAVIGATION ===== */
--nav-bg-color: var(--color-primary);
--nav-text-color: #ffffff;
}
/* ===== BOOTSTRAP BUTTON VARIANTS ===== */
.btn-primary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--color-primary);
--btn-border-color: var(--color-primary);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #0052a3;
--btn-hover-border-color: #004d99;
--btn-focus-shadow-rgb: 38, 128, 217;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #004d99;
--btn-active-border-color: #004788;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--color-primary);
--btn-disabled-border-color: var(--color-primary);
}
.btn-secondary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: #6c757d;
--btn-border-color: #6c757d;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #5c636a;
--btn-hover-border-color: #565e64;
--btn-focus-shadow-rgb: 130, 138, 145;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #565e64;
--btn-active-border-color: #51585e;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: #6c757d;
--btn-disabled-border-color: #6c757d;
}
.btn-success {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--success);
--btn-border-color: var(--success);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #218838;
--btn-hover-border-color: #1e7e34;
--btn-focus-shadow-rgb: 72, 180, 97;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #1e7e34;
--btn-active-border-color: #1c7430;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--success);
--btn-disabled-border-color: var(--success);
}
.btn-info {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--info);
--btn-border-color: var(--info);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #138496;
--btn-hover-border-color: #117a8b;
--btn-focus-shadow-rgb: 58, 176, 195;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #117a8b;
--btn-active-border-color: #10707f;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--info);
--btn-disabled-border-color: var(--info);
}
.btn-warning {
--btn-color: hsl(0, 0%, 0%);
--btn-bg: var(--warning);
--btn-border-color: var(--warning);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #e0a800;
--btn-hover-border-color: #d39e00;
--btn-focus-shadow-rgb: 222, 170, 12;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #d39e00;
--btn-active-border-color: #c69500;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 0%);
--btn-disabled-bg: var(--warning);
--btn-disabled-border-color: var(--warning);
}
.btn-danger {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--danger);
--btn-border-color: var(--danger);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #c82333;
--btn-hover-border-color: #bd2130;
--btn-focus-shadow-rgb: 225, 83, 97;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #bd2130;
--btn-active-border-color: #b21f2d;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--danger);
--btn-disabled-border-color: var(--danger);
}
.btn-light {
--btn-color: hsl(0, 0%, 0%);
--btn-bg: #f8f9fa;
--btn-border-color: #f8f9fa;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #e2e6ea;
--btn-hover-border-color: #dae0e5;
--btn-focus-shadow-rgb: 216, 217, 219;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #dae0e5;
--btn-active-border-color: #d3d9df;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 0%);
--btn-disabled-bg: #f8f9fa;
--btn-disabled-border-color: #f8f9fa;
}
.btn-dark {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: #343a40;
--btn-border-color: #343a40;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #23272b;
--btn-hover-border-color: #1d2124;
--btn-focus-shadow-rgb: 82, 88, 93;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #1d2124;
--btn-active-border-color: #171a1d;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: #343a40;
--btn-disabled-border-color: #343a40;
}
/* ===== OUTLINE BUTTON VARIANTS ===== */
.btn-outline-primary {
--btn-color: var(--color-primary);
--btn-border-color: var(--color-primary);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--color-primary);
--btn-hover-border-color: var(--color-primary);
--btn-focus-shadow-rgb: 38, 128, 217;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--color-primary);
--btn-active-border-color: var(--color-primary);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--color-primary);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--color-primary);
--gradient: none;
}
.btn-outline-secondary {
--btn-color: #6c757d;
--btn-border-color: #6c757d;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #6c757d;
--btn-hover-border-color: #6c757d;
--btn-focus-shadow-rgb: 130, 138, 145;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #6c757d;
--btn-active-border-color: #6c757d;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #6c757d;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #6c757d;
--gradient: none;
}
.btn-outline-success {
--btn-color: var(--success);
--btn-border-color: var(--success);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--success);
--btn-hover-border-color: var(--success);
--btn-focus-shadow-rgb: 72, 180, 97;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--success);
--btn-active-border-color: var(--success);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--success);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--success);
--gradient: none;
}
.btn-outline-info {
--btn-color: var(--info);
--btn-border-color: var(--info);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--info);
--btn-hover-border-color: var(--info);
--btn-focus-shadow-rgb: 58, 176, 195;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--info);
--btn-active-border-color: var(--info);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--info);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--info);
--gradient: none;
}
.btn-outline-warning {
--btn-color: var(--warning);
--btn-border-color: var(--warning);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--warning);
--btn-hover-border-color: var(--warning);
--btn-focus-shadow-rgb: 222, 170, 12;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--warning);
--btn-active-border-color: var(--warning);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--warning);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--warning);
--gradient: none;
}
.btn-outline-danger {
--btn-color: var(--danger);
--btn-border-color: var(--danger);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--danger);
--btn-hover-border-color: var(--danger);
--btn-focus-shadow-rgb: 225, 83, 97;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--danger);
--btn-active-border-color: var(--danger);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--danger);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--danger);
--gradient: none;
}
.btn-outline-light {
--btn-color: #f8f9fa;
--btn-border-color: #f8f9fa;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #f8f9fa;
--btn-hover-border-color: #f8f9fa;
--btn-focus-shadow-rgb: 216, 217, 219;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #f8f9fa;
--btn-active-border-color: #f8f9fa;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #f8f9fa;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #f8f9fa;
--gradient: none;
}
.btn-outline-dark {
--btn-color: #343a40;
--btn-border-color: #343a40;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #343a40;
--btn-hover-border-color: #343a40;
--btn-focus-shadow-rgb: 82, 88, 93;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #343a40;
--btn-active-border-color: #343a40;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #343a40;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #343a40;
--gradient: none;
}

View File

@@ -1,39 +1,39 @@
<!-- <!--
Joomla Extension Update Server XML Joomla Extension Update Server XML
See: https://docs.joomla.org/Deploying_an_Update_Server See: https://docs.joomla.org/Deploying_an_Update_Server
This file is the update server manifest for {{EXTENSION_NAME}}. This file is the update server manifest for {{EXTENSION_NAME}}.
The Joomla installer polls this URL to check for new versions. The Joomla installer polls this URL to check for new versions.
The manifest.xml in this repository must reference this file: The manifest.xml in this repository must reference this file:
<updateservers> <updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}"> <server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/raw/branch/main/update.xml https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/raw/branch/main/update.xml
</server> </server>
<server type="extension" priority="2" name="{{EXTENSION_NAME}}"> <server type="extension" priority="2" name="{{EXTENSION_NAME}}">
https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/update.xml https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/update.xml
</server> </server>
</updateservers> </updateservers>
When a new release is made, run `make release` or the release workflow to When a new release is made, run `make release` or the release workflow to
prepend a new <update> entry to this file automatically. prepend a new <update> entry to this file automatically.
--> -->
<updates> <updates>
<update> <update>
<name>{{EXTENSION_NAME}}</name> <name>{{EXTENSION_NAME}}</name>
<description>MokoCassiopeia — Moko Consulting Joomla extension</description> <description>MokoCassiopeia — Moko Consulting Joomla extension</description>
<element>{{EXTENSION_ELEMENT}}</element> <element>{{EXTENSION_ELEMENT}}</element>
<type>{{EXTENSION_TYPE}}</type> <type>{{EXTENSION_TYPE}}</type>
<version>{{VERSION}}</version> <version>{{VERSION}}</version>
<downloads> <downloads>
<downloadurl type="full" format="zip"> <downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
</downloadurl> </downloadurl>
<downloadurl type="full" format="zip"> <downloadurl type="full" format="zip">
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
</downloadurl> </downloadurl>
</downloads> </downloads>
<targetplatform name="joomla" version="[56].*"/> <targetplatform name="joomla" version="[56].*"/>
<php_minimum>8.1</php_minimum> <php_minimum>8.1</php_minimum>
</update> </update>
</updates> </updates>

View File

@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> <!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 03.10.06 VERSION: 03.10.07
--> -->
<updates> <updates>
@@ -13,11 +13,11 @@
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.06</version> <version>03.10.07</version>
<creationDate>2026-04-18</creationDate> <creationDate>2026-04-18</creationDate>
<infourl title='MokoCassiopeia Dev'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development</infourl> <infourl title='MokoCassiopeia Dev'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.06.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.07.zip</downloadurl>
</downloads> </downloads>
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256> <sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
<tags><tag>development</tag></tags> <tags><tag>development</tag></tags>
@@ -34,11 +34,11 @@
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.06</version> <version>03.10.07</version>
<creationDate>2026-04-18</creationDate> <creationDate>2026-04-18</creationDate>
<infourl title='MokoCassiopeia Alpha'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha</infourl> <infourl title='MokoCassiopeia Alpha'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.06.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.07.zip</downloadurl>
</downloads> </downloads>
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256> <sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
<tags><tag>alpha</tag></tags> <tags><tag>alpha</tag></tags>
@@ -55,11 +55,11 @@
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.06</version> <version>03.10.07</version>
<creationDate>2026-04-18</creationDate> <creationDate>2026-04-18</creationDate>
<infourl title='MokoCassiopeia Beta'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta</infourl> <infourl title='MokoCassiopeia Beta'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.06.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.07.zip</downloadurl>
</downloads> </downloads>
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256> <sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
<tags><tag>beta</tag></tags> <tags><tag>beta</tag></tags>
@@ -76,12 +76,12 @@
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.06</version> <version>03.10.07</version>
<creationDate>2026-04-18</creationDate> <creationDate>2026-04-18</creationDate>
<infourl title='MokoCassiopeia RC'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate</infourl> <infourl title='MokoCassiopeia RC'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.06.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.07.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.06.zip</downloadurl> <downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.07.zip</downloadurl>
</downloads> </downloads>
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256> <sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
<tags><tag>rc</tag></tags> <tags><tag>rc</tag></tags>
@@ -98,12 +98,12 @@
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.06</version> <version>03.10.07</version>
<creationDate>2026-04-18</creationDate> <creationDate>2026-04-18</creationDate>
<infourl title='MokoCassiopeia'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03</infourl> <infourl title='MokoCassiopeia'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.06.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.07.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.06.zip</downloadurl> <downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.07.zip</downloadurl>
</downloads> </downloads>
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256> <sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
<tags><tag>stable</tag></tags> <tags><tag>stable</tag></tags>