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
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:
17
.gitattributes
vendored
Normal file
17
.gitattributes
vendored
Normal 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
|
||||
4
.github/workflows/auto-dev-issue.yml
vendored
4
.github/workflows/auto-dev-issue.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
# 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 \
|
||||
--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
|
||||
echo "ℹ️ Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
IFS='|' read -r SUB_TITLE _ _ <<< "$SUB"
|
||||
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 \
|
||||
--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
|
||||
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)
|
||||
|
||||
7
.github/workflows/auto-release.yml
vendored
7
.github/workflows/auto-release.yml
vendored
@@ -53,7 +53,7 @@ permissions:
|
||||
jobs:
|
||||
release:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
||||
|
||||
@@ -64,9 +64,12 @@ jobs:
|
||||
token: ${{ secrets.GA_TOKEN || github.token }}
|
||||
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
|
||||
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 }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch version/04 --quiet \
|
||||
|
||||
4
.github/workflows/branch-freeze.yml
vendored
4
.github/workflows/branch-freeze.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
if [ "$ACTION" = "freeze" ]; then
|
||||
# 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 \
|
||||
--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
|
||||
echo "Branch \`${BRANCH}\` is already frozen (ruleset #${EXISTING})" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
elif [ "$ACTION" = "unfreeze" ]; then
|
||||
# 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 \
|
||||
--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
|
||||
echo "Branch \`${BRANCH}\` is not frozen (no ruleset found)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
2
.github/workflows/deploy-manual.yml
vendored
2
.github/workflows/deploy-manual.yml
vendored
@@ -32,7 +32,7 @@ permissions:
|
||||
jobs:
|
||||
deploy:
|
||||
name: SFTP Deploy to Dev
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: release
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
50
.github/workflows/release.yml
vendored
50
.github/workflows/release.yml
vendored
@@ -47,7 +47,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Build Release Package
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: release
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -139,8 +139,8 @@ jobs:
|
||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Find and delete existing release by tag
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||
# Find and delete existing release by tag (may not exist — ignore 404)
|
||||
RELEASE_ID=$(curl -s -H "Authorization: token ${TOKEN}" \
|
||||
"${API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty')
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
--arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \
|
||||
--arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \
|
||||
--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')
|
||||
|
||||
# Upload ZIP
|
||||
@@ -367,19 +367,57 @@ jobs:
|
||||
print(f"Updated {xml_tag} channel: version={version}, sha={sha256[:16]}..., date={date}")
|
||||
PYEOF
|
||||
|
||||
- name: "Commit updates.xml"
|
||||
- name: "Commit updates.xml to current branch and main"
|
||||
run: |
|
||||
if git diff --quiet updates.xml 2>/dev/null; then
|
||||
echo "No changes to updates.xml"
|
||||
exit 0
|
||||
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.name "gitea-actions[bot]"
|
||||
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>"
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
|
||||
2
.github/workflows/repository-cleanup.yml
vendored
2
.github/workflows/repository-cleanup.yml
vendored
@@ -504,7 +504,7 @@ jobs:
|
||||
|
||||
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 \
|
||||
--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
|
||||
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
|
||||
|
||||
2
.github/workflows/update-server.yml
vendored
2
.github/workflows/update-server.yml
vendored
@@ -53,7 +53,7 @@ permissions:
|
||||
jobs:
|
||||
update-xml:
|
||||
name: Update updates.xml
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
||||
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -198,5 +198,8 @@ venv/
|
||||
*.coverage
|
||||
hypothesis/
|
||||
|
||||
src/media/css/theme/dark.custom.css
|
||||
src/media/css/theme/light.custom.css
|
||||
# Custom theme palettes (site-specific, not version controlled)
|
||||
src/media/css/theme/*.custom.css
|
||||
src/media/css/theme/*.custom.min.css
|
||||
src/templates/*.custom.css
|
||||
templates/*.custom.css
|
||||
|
||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -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/),
|
||||
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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
INGROUP: MokoCassiopeia.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia
|
||||
FILE: ./README.md
|
||||
VERSION: 03.09.14
|
||||
VERSION: 03.10.07
|
||||
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**
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"authors": [
|
||||
@@ -10,8 +10,8 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"mokoconsulting-tech/enterprise": "dev-version/04",
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.1",
|
||||
"ext-zip": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"mokoconsulting-tech/enterprise": "^4.0"
|
||||
|
||||
360
src/helper/bridge.php
Normal file
360
src/helper/bridge.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
class MokoFaviconHelper
|
||||
{
|
||||
/**
|
||||
@@ -39,7 +41,13 @@ class MokoFaviconHelper
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -55,8 +63,24 @@ class MokoFaviconHelper
|
||||
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) {
|
||||
Log::add('Favicon: unsupported image type (' . ($imageInfo['mime'] ?? 'unknown') . ') at ' . $sourcePath, Log::WARNING, 'mokocassiopeia');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -162,7 +186,7 @@ class MokoFaviconHelper
|
||||
*/
|
||||
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"
|
||||
. '<link rel="icon" type="image/png" sizes="32x32" href="' . $basePath . '/favicon-32x32.png">' . "\n"
|
||||
|
||||
@@ -22,6 +22,9 @@ class MokoMinifyHelper
|
||||
*/
|
||||
private const CSS_FILES = [
|
||||
'css/template.css',
|
||||
'css/offline.css',
|
||||
'css/editor.css',
|
||||
'css/a11y-high-contrast.css',
|
||||
'css/theme/light.standard.css',
|
||||
'css/theme/dark.standard.css',
|
||||
'css/theme/light.custom.css',
|
||||
|
||||
@@ -26,10 +26,14 @@ $headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'U
|
||||
<?php if ($module->showtitle) : ?>
|
||||
<<?php echo $headerTag; ?> class="mod-stats__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
|
||||
<?php endif; ?>
|
||||
<dl class="mod-stats__list">
|
||||
<?php foreach ($list as $item) : ?>
|
||||
<dt class="mod-stats__label"><?php echo $item->title; ?></dt>
|
||||
<dd class="mod-stats__data"><?php echo $item->data; ?></dd>
|
||||
<?php endforeach; ?>
|
||||
</dl>
|
||||
<table class="mod_stats__table">
|
||||
<tbody>
|
||||
<?php foreach ($list as $item) : ?>
|
||||
<tr>
|
||||
<th class="mod_stats__label" scope="row"><?php echo $item->title; ?></th>
|
||||
<td class="mod_stats__data"><?php echo $item->data; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ $params_favicon_source = (string) $this->params->get('favicon_source', '');
|
||||
$params_theme_enabled = $this->params->get('theme_enabled', 1);
|
||||
$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_pos = $this->params->get('theme_fab_pos', 'br');
|
||||
$params_theme_fab_pos = 'br';
|
||||
|
||||
// Accessibility params
|
||||
$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_font = $this->params->get('a11y_readable_font', 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
|
||||
$option = $input->getCmd('option', '');
|
||||
@@ -71,7 +71,27 @@ $templatePath = 'media/templates/site/mokocassiopeia';
|
||||
$faviconHeadTags = '';
|
||||
if ($params_favicon_source) {
|
||||
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';
|
||||
$faviconUrlBase = Uri::root(true) . '/images/favicons';
|
||||
|
||||
@@ -406,7 +426,7 @@ $wa->useScript('user.js'); // js/user.js
|
||||
</div>
|
||||
<?php if ($this->countModules('brand-aside', true)) : ?>
|
||||
<div class="container-brand-aside">
|
||||
<jdoc:include type="modules" name="brand-aside" style="none" />
|
||||
<jdoc:include type="modules" name="brand-aside" style="card" />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"defgroup": "Joomla.Template.Site",
|
||||
"ingroup": "MokoCassiopeia.Template.Assets",
|
||||
"path": "./media/templates/site/mokocassiopeia/joomla.asset.json",
|
||||
"version": "03.09.14",
|
||||
"version": "03.10.07",
|
||||
"brief": "Joomla asset registry for MokoCassiopeia"
|
||||
}
|
||||
},
|
||||
@@ -34,6 +34,18 @@
|
||||
"uri": "media/templates/site/mokocassiopeia/css/template.min.css",
|
||||
"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",
|
||||
"type": "style",
|
||||
|
||||
@@ -259,16 +259,14 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC="<strong>Surfaces & text</strong><br><co
|
||||
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_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 =====
|
||||
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_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 =====
|
||||
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
||||
|
||||
|
||||
@@ -259,16 +259,14 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC="<strong>Surfaces & text</strong><br><co
|
||||
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_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 =====
|
||||
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_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 =====
|
||||
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
||||
|
||||
|
||||
258
src/media/css/offline.css
Normal file
258
src/media/css/offline.css
Normal 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;
|
||||
}
|
||||
@@ -14233,6 +14233,28 @@ fieldset>* {
|
||||
margin-inline-start: auto;
|
||||
display: flex;
|
||||
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 {
|
||||
@@ -15771,7 +15793,13 @@ body.wrapper-fluid header>.grid-child {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -17064,14 +17092,20 @@ form .form-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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: none;
|
||||
background: var(--muted-color, #6d757e);
|
||||
box-shadow: var(--box-shadow, 0 .5rem 1rem #00000066);
|
||||
border: 2px solid var(--theme-fab-border, rgba(255,255,255,.3));
|
||||
background: var(--theme-fab-bg, var(--color-primary, #112855));
|
||||
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;
|
||||
color: #fff;
|
||||
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 {
|
||||
@@ -17094,50 +17128,47 @@ form .form-select {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
#mokoThemeFab .switch {
|
||||
display: inline-flex;
|
||||
/* Sun/Moon theme toggle button */
|
||||
.theme-icon-btn {
|
||||
display: flex;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
transition: opacity .2s, transform .2s;
|
||||
}
|
||||
|
||||
#mokoThemeFab [role="switch"][aria-checked="true"] .knob {
|
||||
transform: translateX(20px);
|
||||
/* Light mode: show sun, hide moon */
|
||||
.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 {
|
||||
background: rgba(var(--secondary-color, #e6ebf1bf), .15);
|
||||
/* Dark mode: show moon, hide sun */
|
||||
.theme-icon-btn.is-dark .fa-moon {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
button#mokoThemeSwitch {
|
||||
border: unset;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
#mokoThemeFab .label {
|
||||
user-select: none;
|
||||
font-size: .875rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#mokoThemeFab button {
|
||||
color: #fff;
|
||||
.theme-icon-btn.is-dark .fa-sun {
|
||||
opacity: 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Auto toggle switch (on/off style) */
|
||||
@@ -17164,14 +17195,14 @@ button#mokoThemeSwitch {
|
||||
height: 18px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: var(--secondary-color, #6c757d);
|
||||
background: var(--danger, #c23a31);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: background .2s;
|
||||
}
|
||||
|
||||
.auto-switch.on {
|
||||
background: var(--link-color, #3565e5);
|
||||
background: var(--success, #4aa664);
|
||||
}
|
||||
|
||||
.auto-track {
|
||||
@@ -17207,6 +17238,15 @@ button#mokoThemeSwitch {
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -17214,25 +17254,16 @@ button#mokoThemeSwitch {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid currentColor;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
background: var(--a11y-btn-bg, #1976d2);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: background .2s, color .2s;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.a11y-toggle-inline:hover,
|
||||
.a11y-toggle-inline:focus-visible {
|
||||
opacity: 1;
|
||||
background: rgba(255,255,255,.15);
|
||||
}
|
||||
|
||||
.a11y-toggle-inline.active {
|
||||
opacity: 1;
|
||||
background: rgba(255,255,255,.25);
|
||||
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--a11y-btn-bg, #1976d2);
|
||||
}
|
||||
|
||||
/* Floating a11y panel when inline */
|
||||
@@ -17361,36 +17392,6 @@ body.site.error-page {
|
||||
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 {
|
||||
outline: 2px dashed var(--pink, #ff8fc0);
|
||||
outline-offset: 2px;
|
||||
@@ -17499,12 +17500,13 @@ body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
|
||||
|
||||
/* 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-radius: var(--border-radius, .375rem);
|
||||
padding: .75rem;
|
||||
min-width: 200px;
|
||||
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 {
|
||||
@@ -17540,8 +17542,8 @@ body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
|
||||
height: 34px;
|
||||
border: 1px solid var(--border-color, #dee2e6);
|
||||
border-radius: var(--border-radius, .375rem);
|
||||
background: var(--bs-body-bg, #fff);
|
||||
color: var(--body-font-color, #444);
|
||||
background: var(--secondary-bg, var(--bs-body-bg, #fff));
|
||||
color: var(--body-font-color, var(--body-color, #e6ebf1));
|
||||
font-size: .875rem;
|
||||
cursor: pointer;
|
||||
transition: background .15s, border-color .15s;
|
||||
@@ -17559,7 +17561,7 @@ body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
|
||||
font-weight: 600;
|
||||
min-width: 3ch;
|
||||
text-align: center;
|
||||
color: var(--body-font-color, #444);
|
||||
color: var(--body-font-color, var(--body-color, #e6ebf1));
|
||||
}
|
||||
|
||||
.a11y-btn-wide {
|
||||
@@ -18686,7 +18688,7 @@ nav[data-toggle=toc] .nav-link.active+ul{
|
||||
flex: 0 0 auto;
|
||||
background-color: var(--color-primary, #112855);
|
||||
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;
|
||||
border-radius: 0 0.25rem 0.25rem 0;
|
||||
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,
|
||||
.container-search button[type="submit"]:hover {
|
||||
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,
|
||||
@@ -21664,6 +21666,33 @@ nav[data-toggle=toc] .nav-link.active+ul{
|
||||
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 === */
|
||||
@media (max-width: 575.98px) {
|
||||
.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
@@ -4,6 +4,7 @@
|
||||
<svg
|
||||
width="800"
|
||||
height="400"
|
||||
viewBox="0 0 800 400"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
sodipodi:docname="bg.svg"
|
||||
@@ -93,14 +94,14 @@
|
||||
style="display:inline;fill:#e5e5e5;fill-opacity:1;stroke:none;stroke-width:2.20303"
|
||||
id="rect4741"
|
||||
width="800"
|
||||
height="400"
|
||||
height="494"
|
||||
x="0"
|
||||
y="46.331768" />
|
||||
<rect
|
||||
style="display:inline;fill:url(#pattern4758);fill-opacity:1;stroke:none;stroke-width:2.20303"
|
||||
id="rect4737"
|
||||
width="800"
|
||||
height="400"
|
||||
height="494"
|
||||
x="0"
|
||||
y="46.699127" />
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
@@ -62,30 +62,33 @@
|
||||
wrap.id = 'mokoThemeFab';
|
||||
wrap.className = posClassFromBody();
|
||||
|
||||
// Light label
|
||||
var lblL = doc.createElement('span');
|
||||
lblL.className = 'label';
|
||||
lblL.textContent = 'Light';
|
||||
|
||||
// Switch
|
||||
// Sun/Moon toggle button
|
||||
var switchWrap = doc.createElement('button');
|
||||
switchWrap.id = 'mokoThemeSwitch';
|
||||
switchWrap.type = 'button';
|
||||
switchWrap.setAttribute('role', 'switch');
|
||||
switchWrap.className = 'theme-icon-btn';
|
||||
switchWrap.setAttribute('aria-label', 'Toggle dark mode');
|
||||
switchWrap.setAttribute('aria-checked', 'false');
|
||||
|
||||
var track = doc.createElement('span');
|
||||
track.className = 'switch';
|
||||
var knob = doc.createElement('span');
|
||||
knob.className = 'knob';
|
||||
track.appendChild(knob);
|
||||
switchWrap.appendChild(track);
|
||||
var sunIcon = doc.createElement('i');
|
||||
sunIcon.className = 'fa-solid fa-sun';
|
||||
sunIcon.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Dark label
|
||||
var lblD = doc.createElement('span');
|
||||
lblD.className = 'label';
|
||||
lblD.textContent = 'Dark';
|
||||
var moonIcon = doc.createElement('i');
|
||||
moonIcon.className = 'fa-solid fa-moon';
|
||||
moonIcon.setAttribute('aria-hidden', 'true');
|
||||
|
||||
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)
|
||||
var autoWrap = doc.createElement('div');
|
||||
@@ -127,7 +130,7 @@
|
||||
var current = (root.getAttribute('data-bs-theme') || 'light').toLowerCase();
|
||||
var next = current === 'dark' ? 'light' : 'dark';
|
||||
applyTheme(next);
|
||||
switchWrap.setAttribute('aria-checked', next === 'dark' ? 'true' : 'false');
|
||||
updateThemeIcon(next);
|
||||
// Turn off auto when manually switching
|
||||
auto.classList.remove('on');
|
||||
auto.setAttribute('aria-checked', 'false');
|
||||
@@ -145,7 +148,7 @@
|
||||
clearStored();
|
||||
var sys = systemTheme();
|
||||
applyTheme(sys);
|
||||
switchWrap.setAttribute('aria-checked', sys === 'dark' ? 'true' : 'false');
|
||||
updateThemeIcon(sys);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -154,7 +157,7 @@
|
||||
if (!getStored()) {
|
||||
var sys = systemTheme();
|
||||
applyTheme(sys);
|
||||
switchWrap.setAttribute('aria-checked', sys === 'dark' ? 'true' : 'false');
|
||||
updateThemeIcon(sys);
|
||||
}
|
||||
};
|
||||
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql);
|
||||
@@ -162,12 +165,10 @@
|
||||
|
||||
// Initial state
|
||||
var initial = getStored() || systemTheme();
|
||||
switchWrap.setAttribute('aria-checked', initial === 'dark' ? 'true' : 'false');
|
||||
updateThemeIcon(initial);
|
||||
|
||||
// Mount
|
||||
wrap.appendChild(lblL);
|
||||
wrap.appendChild(switchWrap);
|
||||
wrap.appendChild(lblD);
|
||||
wrap.appendChild(autoWrap);
|
||||
wrap.appendChild(divider);
|
||||
wrap.appendChild(a11ySlot);
|
||||
@@ -292,7 +293,17 @@
|
||||
toggle.className = "a11y-toggle";
|
||||
toggle.setAttribute("aria-label", "Accessibility options");
|
||||
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
|
||||
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
|
||||
*/
|
||||
@@ -656,6 +815,7 @@
|
||||
initBackTop();
|
||||
initSearchToggle();
|
||||
initSidebarAccordion();
|
||||
initVarCopy();
|
||||
}
|
||||
|
||||
if (doc.readyState === "loading") {
|
||||
|
||||
344
src/offline.php
344
src/offline.php
@@ -1,5 +1,5 @@
|
||||
<?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.
|
||||
|
||||
@@ -26,59 +26,90 @@ use Joomla\CMS\Uri\Uri;
|
||||
|
||||
$app = Factory::getApplication();
|
||||
$doc = Factory::getDocument();
|
||||
$wa = $doc->getWebAssetManager();
|
||||
$params = $this->params ?: $app->getTemplate(true)->params;
|
||||
$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);
|
||||
$assetSuffix = $useMin ? '.min' : '';
|
||||
$base = rtrim(Uri::root(true), '/') . '/templates/' . $this->template . '/css/';
|
||||
$jsBase = rtrim(Uri::root(true), '/') . '/templates/' . $this->template . '/js/';
|
||||
$params_developmentmode = (bool) $params->get('developmentmode', false) || (bool) $app->get('debug', false);
|
||||
$suffix = $params_developmentmode ? '' : '.min';
|
||||
|
||||
$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 */
|
||||
$doc->addStyleSheet($base . 'theme/light.standard' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-light-standard']);
|
||||
$doc->addStyleSheet($base . 'theme/dark.standard' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-dark-standard']);
|
||||
// Osaka font
|
||||
$wa->useStyle('template.font.osaka');
|
||||
|
||||
/* 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_DarkColorName = (string) $params->get('colorDarkName', 'standard');
|
||||
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'))
|
||||
{
|
||||
$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) */
|
||||
$doc->addStyleSheet($base . 'user' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-user']);
|
||||
// User overrides (loaded last)
|
||||
$wa->useStyle('template.user');
|
||||
|
||||
/* Bootstrap CSS/JS for accordion behavior; safe to keep. */
|
||||
HTMLHelper::_('bootstrap.loadCss', true, $doc);
|
||||
// Accessibility high-contrast stylesheet
|
||||
$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');
|
||||
|
||||
/* 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');
|
||||
$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) {
|
||||
$doc->setTitle(Text::sprintf('JPAGETITLE', $sitename, $baseTitle)); // Site Name BEFORE
|
||||
$doc->setTitle(Text::sprintf('JPAGETITLE', $sitename, $baseTitle));
|
||||
} elseif ($snSetting === 2) {
|
||||
$doc->setTitle(Text::sprintf('JPAGETITLE', $baseTitle, $sitename)); // Site Name AFTER
|
||||
$doc->setTitle(Text::sprintf('JPAGETITLE', $baseTitle, $sitename));
|
||||
} else {
|
||||
$doc->setTitle($baseTitle);
|
||||
}
|
||||
@@ -87,11 +118,21 @@ $doc->setMetaData('robots', 'noindex, nofollow');
|
||||
/* -----------------------
|
||||
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', ''));
|
||||
|
||||
/* -----------------------
|
||||
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 = '';
|
||||
$logoFile = (string) $params->get('logoFile');
|
||||
@@ -106,9 +147,8 @@ if ($logoFile !== '') {
|
||||
0
|
||||
);
|
||||
} else {
|
||||
// If no logo file, show the title (defaults to "MokoCassiopeia" if not set)
|
||||
$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')
|
||||
. '</span>';
|
||||
}
|
||||
@@ -116,8 +156,34 @@ if ($logoFile !== '') {
|
||||
$brandTagline = (string) ($params->get('brand_tagline') ?: $params->get('siteDescription') ?: '');
|
||||
$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
|
||||
$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
|
||||
$params_googletagmanager = $params->get('googletagmanager', false);
|
||||
@@ -131,7 +197,7 @@ if (!empty($params_googlesitekey)) {
|
||||
}
|
||||
|
||||
/* -----------------------
|
||||
Login routes & Users
|
||||
Login routes
|
||||
------------------------ */
|
||||
$action = Route::_('index.php', true);
|
||||
$return = base64_encode(Uri::base());
|
||||
@@ -152,10 +218,12 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
|
||||
<head>
|
||||
<jdoc:include type="head" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<?php if ($faviconHeadTags) : ?>
|
||||
<?php echo $faviconHeadTags; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($params_theme_enabled) : ?>
|
||||
<script>
|
||||
// Early theme application to avoid FOUC
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem('theme');
|
||||
@@ -168,20 +236,21 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
|
||||
</script>
|
||||
<?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>
|
||||
<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)) :
|
||||
$gtmID = htmlspecialchars($params_googletagmanagerid, ENT_QUOTES, 'UTF-8'); ?>
|
||||
<!-- Google Tag Manager -->
|
||||
<script>
|
||||
(function(w,d,s,l,i){
|
||||
w[l]=w[l]||[];
|
||||
@@ -194,19 +263,14 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
|
||||
f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','<?php echo $gtmID; ?>');
|
||||
</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript>
|
||||
<iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo $gtmID; ?>"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe>
|
||||
</noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($params_googleanalytics) && !empty($params_googleanalyticsid)) :
|
||||
$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>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
@@ -223,136 +287,128 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
|
||||
gtag('config', id, { 'anonymize_ip': true });
|
||||
} else if (/^UA-/.test(id)) {
|
||||
gtag('config', id, { 'anonymize_ip': true });
|
||||
console.warn('Using a UA- ID. Universal Analytics is sunset; consider migrating to GA4.');
|
||||
} else {
|
||||
console.warn('Unrecognized Google Analytics ID format:', id);
|
||||
}
|
||||
})('<?php echo $gaId; ?>');
|
||||
</script>
|
||||
<!-- End Google Analytics -->
|
||||
<?php endif; ?>
|
||||
|
||||
<a class="skip-link" href="#maincontent"><?php echo Text::_('JSKIP_TO_CONTENT') ?: 'Skip to content'; ?></a>
|
||||
|
||||
<header class="container-header header py-3">
|
||||
<div class="grid-child container-nav d-flex align-items-center gap-3">
|
||||
<!-- Centered overlay card -->
|
||||
<main id="maincontent">
|
||||
<div class="moko-offline-card">
|
||||
|
||||
<!-- Brand (mutually exclusive image/text) -->
|
||||
<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'); ?>">
|
||||
<!-- Logo -->
|
||||
<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 if ($showTagline && $brandTagline): ?>
|
||||
<small class="brand-tagline"><?php echo htmlspecialchars($brandTagline, ENT_COMPAT, 'UTF-8'); ?></small>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
|
||||
<!-- Header module position: offline-header -->
|
||||
<?php if ($this->countModules('offline-header')) : ?>
|
||||
<div class="ms-2">
|
||||
<jdoc:include type="modules" name="offline-header" style="none" />
|
||||
<!-- Offline message: 0=hidden, 1=custom message, 2=system language string -->
|
||||
<?php if ($displayOfflineMessage === 1 && $offlineMessage !== '') : ?>
|
||||
<div class="moko-offline-message">
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
<!-- Offline module position -->
|
||||
<?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">
|
||||
<div class="container">
|
||||
<jdoc:include type="message" />
|
||||
<!-- Login accordion -->
|
||||
<div class="accordion" id="offlineAccordion">
|
||||
<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">
|
||||
<?php if ($displayOfflineMessage === 1 && $offlineMessage !== '') : ?>
|
||||
<div class="mb-4">
|
||||
<h1 class="h3 mb-2"><?php echo Text::_('JOFFLINE_MESSAGE') ?: 'Site Offline'; ?></h1>
|
||||
<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; ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="username"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label>
|
||||
<input class="form-control" type="text" name="username" id="username" autocomplete="username" required aria-required="true">
|
||||
</div>
|
||||
|
||||
<!-- Main offline module position -->
|
||||
<?php if ($this->countModules('offline')) : ?>
|
||||
<section class="mb-4" aria-label="Offline modules">
|
||||
<jdoc:include type="modules" name="offline" style="none" />
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="password"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
|
||||
<input class="form-control" type="password" name="password" id="password" autocomplete="current-password" required aria-required="true">
|
||||
</div>
|
||||
|
||||
<!-- Login UNDER an accordion (collapsed by default) -->
|
||||
<div class="accordion" id="offlineAccordion">
|
||||
<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="mb-3">
|
||||
<label class="form-label" for="secretkey"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label>
|
||||
<input class="form-control" type="text" name="secretkey" id="secretkey" autocomplete="one-time-code" placeholder="<?php echo Text::_('JGLOBAL_SECRETKEY'); ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="username"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label>
|
||||
<input class="form-control" type="text" name="username" id="username" autocomplete="username" required aria-required="true">
|
||||
</div>
|
||||
<div class="form-check mb-4">
|
||||
<input class="form-check-input" type="checkbox" name="remember" id="remember">
|
||||
<label class="form-check-label" for="remember"><?php echo Text::_('JGLOBAL_REMEMBER_ME'); ?></label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="password"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
|
||||
<input class="form-control" type="password" name="password" id="password" autocomplete="current-password" required aria-required="true">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary"><?php echo Text::_('JLOGIN'); ?></button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="secretkey"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label>
|
||||
<input class="form-control" type="text" name="secretkey" id="secretkey" autocomplete="one-time-code" placeholder="<?php echo Text::_('JGLOBAL_SECRETKEY'); ?>">
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="form-check mb-4">
|
||||
<input class="form-check-input" type="checkbox" name="remember" id="remember">
|
||||
<label class="form-check-label" for="remember"><?php echo Text::_('JGLOBAL_REMEMBER_ME'); ?></label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary"><?php echo Text::_('JLOGIN'); ?></button>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<nav class="mt-3 small" aria-label="<?php echo Text::_('COM_USERS'); ?>">
|
||||
<ul class="list-inline m-0">
|
||||
<li class="list-inline-item">
|
||||
<a href="<?php echo $resetUrl; ?>"><?php echo Text::_('COM_USERS_LOGIN_RESET'); ?></a>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href="<?php echo $remindUrl; ?>"><?php echo Text::_('COM_USERS_LOGIN_REMIND'); ?></a>
|
||||
</li>
|
||||
<?php if ($allowRegistration) : ?>
|
||||
<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 class="list-inline-item">
|
||||
<a href="<?php echo $remindUrl; ?>"><?php echo Text::_('COM_USERS_LOGIN_REMIND'); ?></a>
|
||||
</li>
|
||||
<?php if ($allowRegistration) : ?>
|
||||
<li class="list-inline-item">
|
||||
<a href="<?php echo $registrationUrl; ?>"><?php echo Text::_('COM_USERS_REGISTER'); ?></a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /accordion -->
|
||||
</div>
|
||||
|
||||
<!-- Copyright -->
|
||||
<div class="moko-offline-copyright">
|
||||
<div>© <?php echo date('Y'); ?> <?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?></div>
|
||||
<div><?php echo Text::_('MOD_FOOTER_LINE2'); ?></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</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" />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -97,7 +97,7 @@ class Tpl_MokocassiopeiaInstallerScript
|
||||
*/
|
||||
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.
|
||||
$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;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ final class MokoCssVarSync
|
||||
*/
|
||||
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',
|
||||
],
|
||||
[
|
||||
'starter' => 'templates/dark.custom.css',
|
||||
'starter' => 'media/css/theme/dark.standard.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
|
||||
{
|
||||
$starterVars = self::extractVarsWithContext($starterPath);
|
||||
$userVars = self::extractVarNames($userPath);
|
||||
$userVarsMap = self::extractVarsWithContext($userPath);
|
||||
$userNames = self::extractVarNames($userPath);
|
||||
|
||||
// Find missing variables
|
||||
$missing = [];
|
||||
foreach ($starterVars as $name => $declaration) {
|
||||
if (!isset($userVars[$name])) {
|
||||
if (!isset($userNames[$name])) {
|
||||
$missing[$name] = $declaration;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($missing)) {
|
||||
return ['added' => [], 'skipped' => []];
|
||||
}
|
||||
// Rebuild the entire :root block in starter file order.
|
||||
// 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.
|
||||
$sections = self::groupBySection($missing, $starterPath);
|
||||
|
||||
// Build the injection block.
|
||||
$injection = self::buildInjectionBlock($sections);
|
||||
|
||||
// Insert before the closing } of the :root rule.
|
||||
// Replace the :root block in the user file with the reordered version.
|
||||
$userCss = file_get_contents($userPath);
|
||||
$userCss = self::injectBeforeRootClose($userCss, $injection);
|
||||
$userCss = self::replaceRootBlock($userCss, $reordered);
|
||||
|
||||
// Write back (atomic: write to .tmp then rename).
|
||||
$tmpPath = $userPath . '.tmp';
|
||||
@@ -128,6 +124,104 @@ final class MokoCssVarSync
|
||||
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).
|
||||
* Only extracts from the first :root block.
|
||||
@@ -181,29 +275,25 @@ final class MokoCssVarSync
|
||||
{
|
||||
$lines = file($starterPath, FILE_IGNORE_NEW_LINES);
|
||||
$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) {
|
||||
// Detect section comment headers like /* ===== HERO VARIANTS ===== */
|
||||
if (preg_match('/\/\*\s*=+\s*(.+?)\s*=+\s*\*\//', $line, $m)) {
|
||||
$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)) {
|
||||
$name = trim($m[1]);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,13 +39,13 @@
|
||||
</server>
|
||||
</updateservers>
|
||||
<name>MokoCassiopeia</name>
|
||||
<version>03.09.14</version>
|
||||
<version>03.10.07</version>
|
||||
<scriptfile>script.php</scriptfile>
|
||||
<creationDate>2026-04-14</creationDate>
|
||||
<creationDate>2026-04-15</creationDate>
|
||||
<author>Jonathan Miller || Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<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&logoColor=white" alt="Version 03.09.14" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&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 & 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&logoColor=white" alt="Version 03.10.07" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&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 & 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>
|
||||
<files>
|
||||
<filename>component.php</filename>
|
||||
@@ -295,7 +295,8 @@
|
||||
<option value="0">JNO</option>
|
||||
<option value="1">JYES</option>
|
||||
</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"
|
||||
showon="a11y_toolbar_enabled:1">
|
||||
<option value="br">Bottom-right</option>
|
||||
@@ -312,7 +313,8 @@
|
||||
<option value="0">JNO</option>
|
||||
<option value="1">JYES</option>
|
||||
</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">
|
||||
<option value="br">Bottom-right</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_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_footer" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_DESC" class="alert alert-light" />
|
||||
</fieldset>
|
||||
|
||||
<!-- 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_frame" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME" />
|
||||
</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>
|
||||
</config>
|
||||
</extension>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
76
update.xml
76
update.xml
@@ -1,39 +1,39 @@
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
See: https://docs.joomla.org/Deploying_an_Update_Server
|
||||
|
||||
This file is the update server manifest for {{EXTENSION_NAME}}.
|
||||
The Joomla installer polls this URL to check for new versions.
|
||||
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/raw/branch/main/update.xml
|
||||
</server>
|
||||
<server type="extension" priority="2" name="{{EXTENSION_NAME}}">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/update.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
When a new release is made, run `make release` or the release workflow to
|
||||
prepend a new <update> entry to this file automatically.
|
||||
-->
|
||||
<updates>
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>MokoCassiopeia — Moko Consulting Joomla extension</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<version>{{VERSION}}</version>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
|
||||
</downloadurl>
|
||||
<downloadurl type="full" format="zip">
|
||||
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
|
||||
</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="[56].*"/>
|
||||
<php_minimum>8.1</php_minimum>
|
||||
</update>
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
See: https://docs.joomla.org/Deploying_an_Update_Server
|
||||
|
||||
This file is the update server manifest for {{EXTENSION_NAME}}.
|
||||
The Joomla installer polls this URL to check for new versions.
|
||||
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/raw/branch/main/update.xml
|
||||
</server>
|
||||
<server type="extension" priority="2" name="{{EXTENSION_NAME}}">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/update.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
When a new release is made, run `make release` or the release workflow to
|
||||
prepend a new <update> entry to this file automatically.
|
||||
-->
|
||||
<updates>
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>MokoCassiopeia — Moko Consulting Joomla extension</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<version>{{VERSION}}</version>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
|
||||
</downloadurl>
|
||||
<downloadurl type="full" format="zip">
|
||||
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
|
||||
</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="[56].*"/>
|
||||
<php_minimum>8.1</php_minimum>
|
||||
</update>
|
||||
</updates>
|
||||
26
updates.xml
26
updates.xml
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: 03.10.06
|
||||
VERSION: 03.10.07
|
||||
-->
|
||||
|
||||
<updates>
|
||||
@@ -13,11 +13,11 @@
|
||||
<element>mokocassiopeia</element>
|
||||
<type>template</type>
|
||||
<client>site</client>
|
||||
<version>03.10.06</version>
|
||||
<version>03.10.07</version>
|
||||
<creationDate>2026-04-18</creationDate>
|
||||
<infourl title='MokoCassiopeia Dev'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development</infourl>
|
||||
<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>
|
||||
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
|
||||
<tags><tag>development</tag></tags>
|
||||
@@ -34,11 +34,11 @@
|
||||
<element>mokocassiopeia</element>
|
||||
<type>template</type>
|
||||
<client>site</client>
|
||||
<version>03.10.06</version>
|
||||
<version>03.10.07</version>
|
||||
<creationDate>2026-04-18</creationDate>
|
||||
<infourl title='MokoCassiopeia Alpha'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha</infourl>
|
||||
<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>
|
||||
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
|
||||
<tags><tag>alpha</tag></tags>
|
||||
@@ -55,11 +55,11 @@
|
||||
<element>mokocassiopeia</element>
|
||||
<type>template</type>
|
||||
<client>site</client>
|
||||
<version>03.10.06</version>
|
||||
<version>03.10.07</version>
|
||||
<creationDate>2026-04-18</creationDate>
|
||||
<infourl title='MokoCassiopeia Beta'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta</infourl>
|
||||
<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>
|
||||
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
|
||||
<tags><tag>beta</tag></tags>
|
||||
@@ -76,12 +76,12 @@
|
||||
<element>mokocassiopeia</element>
|
||||
<type>template</type>
|
||||
<client>site</client>
|
||||
<version>03.10.06</version>
|
||||
<version>03.10.07</version>
|
||||
<creationDate>2026-04-18</creationDate>
|
||||
<infourl title='MokoCassiopeia RC'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate</infourl>
|
||||
<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://github.com/mokoconsulting-tech/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.07.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
|
||||
<tags><tag>rc</tag></tags>
|
||||
@@ -98,12 +98,12 @@
|
||||
<element>mokocassiopeia</element>
|
||||
<type>template</type>
|
||||
<client>site</client>
|
||||
<version>03.10.06</version>
|
||||
<version>03.10.07</version>
|
||||
<creationDate>2026-04-18</creationDate>
|
||||
<infourl title='MokoCassiopeia'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03</infourl>
|
||||
<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://github.com/mokoconsulting-tech/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.07.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>78aebde056dcba369945c851fe1914d5bede9f2cbabb26e87b8d6379d155502d</sha256>
|
||||
<tags><tag>stable</tag></tags>
|
||||
|
||||
Reference in New Issue
Block a user