Release 01.00.18 — minify pipeline, header color var, hero centering

This commit was merged in pull request #2.
This commit is contained in:
2026-04-23 20:01:26 +00:00
parent e3349bc043
commit 1cf063e08e
48 changed files with 2537 additions and 281 deletions

View File

@@ -0,0 +1,532 @@
# 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: Gitea.Workflow
# INGROUP: MokoOnyx.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
# PATH: /.gitea/workflows/release.yml
# VERSION: 01.00.00
# BRIEF: Joomla release — build ZIP, publish to Gitea, mirror to GitHub
name: Create Release
on:
push:
tags:
- '[0-9][0-9].[0-9][0-9].[0-9][0-9]'
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., 01.00.00)'
required: true
type: string
prerelease:
description: 'Mark as pre-release'
required: false
type: boolean
default: false
stability:
description: 'Stability tag (development, alpha, beta, rc, stable)'
required: false
type: string
default: 'development'
permissions:
contents: write
env:
GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting
GITEA_REPO: MokoOnyx
EXT_ELEMENT: mokoonyx
jobs:
build:
name: Build Release Package
runs-on: release
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PHP
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
php -v
composer --version
- name: Get version and stability
id: meta
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
STABILITY="${{ inputs.stability }}"
PRERELEASE="${{ inputs.prerelease }}"
else
VERSION=${GITHUB_REF#refs/tags/}
STABILITY="stable"
PRERELEASE="false"
fi
# Derive suffix and tag from stability
case "$STABILITY" in
development) SUFFIX="-dev"; TAG_NAME="development" ;;
alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;;
beta) SUFFIX="-beta"; TAG_NAME="beta" ;;
rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;;
stable) SUFFIX=""; TAG_NAME="stable" ;;
*) SUFFIX="-dev"; TAG_NAME="development" ;;
esac
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "Building: ${ZIP_NAME} (${STABILITY})"
- name: Auto-bump patch version
id: bump
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
INPUT_VERSION: ${{ steps.meta.outputs.version }}
INPUT_STABILITY: ${{ steps.meta.outputs.stability }}
INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }}
run: |
BRANCH="${{ github.ref_name }}"
GITEA_API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# Read current version from README.md
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
if [ -z "$CURRENT" ]; then
echo "No VERSION in README.md — using input version"
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
exit 0
fi
# Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1)
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1)))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
echo "Bumping: ${CURRENT} → ${NEW_VERSION}"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md
# Update templateDetails.xml / manifest
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
sed -i "s|<version>${CURRENT}</version>|<version>${NEW_VERSION}</version>|" "$MANIFEST"
fi
# Update only the matching stability channel in updates.xml
if [ -f "updates.xml" ]; then
export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY"
python3 << 'PYEOF'
import re, os
old = os.environ["PY_OLD"]
new = os.environ["PY_NEW"]
stability = os.environ["PY_STABILITY"]
with open("updates.xml") as f:
content = f.read()
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(stability) + r"</tag>.*?</update>)"
match = re.search(pattern, content, re.DOTALL)
if match:
block = match.group(1)
updated = block.replace(old, new)
content = content.replace(block, updated)
with open("updates.xml", "w") as f:
f.write(content)
print(f"Updated {stability} channel: {old} -> {new}")
PYEOF
fi
# Commit bump to current branch
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
# For stable releases from dev: merge dev → main via Gitea API
if [ "$INPUT_STABILITY" = "stable" ] && [ "$BRANCH" != "main" ]; then
echo "Merging ${BRANCH} → main via Gitea API..."
curl -sf -X POST -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/merges" \
-d "$(jq -n \
--arg base "main" \
--arg head "${BRANCH}" \
--arg msg "chore(release): merge ${BRANCH} for stable ${NEW_VERSION} [skip ci]" \
'{base: $base, head: $head, merge_message_field: $msg}'
)" > /dev/null 2>&1 || echo "Merge API call failed — may need manual merge"
fi
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
- name: Install dependencies
env:
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-dev --optimize-autoloader --no-interaction
fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Minify CSS and JS
run: |
npm ci --ignore-scripts
node scripts/minify.js
- name: Create package
run: |
mkdir -p build/package
rsync -av \
--exclude='sftp-config*' \
--exclude='.ftpignore' \
--exclude='*.ppk' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='.env*' \
--exclude='*.local' \
src/ build/package/
- name: Build ZIP
id: zip
run: |
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
cd build/package
zip -r "../${ZIP_NAME}" .
cd ..
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
SIZE=$(stat -c%s "${ZIP_NAME}")
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
echo "size=${SIZE}" >> "$GITHUB_OUTPUT"
echo "SHA-256: ${SHA256}"
echo "Size: ${SIZE} bytes"
# ── Gitea Release (PRIMARY) ──────────────────────────────────────
- name: "Gitea: Delete existing release"
run: |
TAG="${{ steps.meta.outputs.tag_name }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# 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
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/releases/${RELEASE_ID}" || true
echo "Deleted existing release id=${RELEASE_ID}"
fi
# Delete existing tag
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" 2>/dev/null || true
- name: "Gitea: Create release"
id: gitea_release
run: |
TAG="${{ steps.meta.outputs.tag_name }}"
VERSION="${{ steps.bump.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
PRERELEASE="${{ steps.meta.outputs.prerelease }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Build release body
BODY="## ${EXT_ELEMENT} ${VERSION} (${STABILITY})
### SHA-256
\`${SHA256}\`"
# Extract changelog if available
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md)
if [ -n "$NOTES" ]; then
BODY="## ${EXT_ELEMENT} ${VERSION} (${STABILITY})
${NOTES}
### SHA-256
\`${SHA256}\`"
fi
fi
IS_PRE="true"
if [ "$STABILITY" = "stable" ]; then
IS_PRE="false"
fi
RESULT=$(curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "${{ github.ref_name }}" \
--arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^}" \
--arg body "$BODY" \
--argjson pre "$IS_PRE" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}'
)")
RELEASE_ID=$(echo "$RESULT" | jq -r '.id')
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
echo "Gitea release created: id=${RELEASE_ID}, tag=${TAG}"
- name: "Gitea: Upload ZIP"
run: |
RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
curl -sf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@build/${ZIP_NAME}"
echo "Uploaded ${ZIP_NAME} to Gitea release ${RELEASE_ID}"
# ── GitHub Mirror (BACKUP) ───────────────────────────────────────
- name: "GitHub: Mirror release (stable/rc only)"
if: ${{ steps.meta.outputs.stability == 'stable' || steps.meta.outputs.stability == 'rc' }}
continue-on-error: true
run: |
TAG="${{ steps.meta.outputs.tag_name }}"
VERSION="${{ steps.bump.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TOKEN="${{ secrets.GH_TOKEN }}"
GH_REPO="mokoconsulting-tech/${GITEA_REPO}"
GH_API="https://api.github.com/repos/${GH_REPO}"
IS_PRE="true"
[ "$STABILITY" = "stable" ] && IS_PRE="false"
# Delete existing release by tag
EXISTING=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${GH_API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty')
if [ -n "$EXISTING" ]; then
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${GH_API}/releases/${EXISTING}" || true
fi
# Delete tag
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${GH_API}/git/refs/tags/${TAG}" 2>/dev/null || true
# Create release
RELEASE_ID=$(curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${GH_API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "${{ github.sha }}" \
--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, draft: false}'
)" | jq -r '.id')
# Upload ZIP
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ]; then
curl -sf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"https://uploads.github.com/repos/${GH_REPO}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@build/${ZIP_NAME}"
echo "GitHub mirror: uploaded ${ZIP_NAME}"
fi
# ── Update updates.xml ──────────────────────────────────────────
- name: "Update updates.xml for this channel"
run: |
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.bump.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag_name }}"
DATE=$(date +%Y-%m-%d)
if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then
echo "No updates.xml or no SHA — skipping"
exit 0
fi
export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
python3 << 'PYEOF'
import re, os
stability = os.environ["PY_STABILITY"]
version = os.environ["PY_VERSION"]
sha256 = os.environ["PY_SHA256"]
zip_name = os.environ["PY_ZIP_NAME"]
tag = os.environ["PY_TAG"]
date = os.environ["PY_DATE"]
gitea_org = os.environ["PY_GITEA_ORG"]
gitea_repo = os.environ["PY_GITEA_REPO"]
# Map stability to the <tag> value in updates.xml
tag_map = {
"development": "development",
"alpha": "alpha",
"beta": "beta",
"rc": "rc",
"stable": "stable",
}
xml_tag = tag_map.get(stability, "development")
with open("updates.xml", "r") as f:
content = f.read()
# Build regex to find the specific <update> block for this stability tag
# Use negative lookahead to avoid matching across multiple <update> blocks
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
match = re.search(block_pattern, content, re.DOTALL)
if not match:
print(f"No <update> block found for <tag>{xml_tag}</tag>")
exit(0)
block = match.group(1)
original_block = block
# Update version
block = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
# Update creation date
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
# Update SHA-256
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", block)
# Update Gitea download URL
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
block = re.sub(
r"(<downloadurl[^>]*>)https://git\.mokoconsulting\.tech/[^<]*(</downloadurl>)",
rf"\g<1>{gitea_url}\g<2>",
block
)
# Update GitHub download URL only for RC and stable (others are Gitea-only)
if stability in ("rc", "stable"):
gh_url = f"https://github.com/mokoconsulting-tech/{gitea_repo}/releases/download/{tag}/{zip_name}"
block = re.sub(
r"(<downloadurl[^>]*>)https://github\.com/[^<]*(</downloadurl>)",
rf"\g<1>{gh_url}\g<2>",
block
)
else:
# Remove any GitHub download URL for dev/alpha/beta
block = re.sub(
r"\n\s*<downloadurl[^>]*>https://github\.com/[^<]*</downloadurl>",
"",
block
)
content = content.replace(original_block, block)
with open("updates.xml", "w") as f:
f.write(content)
print(f"Updated {xml_tag} channel: version={version}, sha={sha256[:16]}..., date={date}")
PYEOF
- 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.bump.outputs.version }}"
CURRENT_BRANCH="${{ github.ref_name }}"
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 ${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
# Sync updates.xml to main via direct API
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
if [ -n "$FILE_SHA" ]; then
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: sync updates.xml ${STABILITY} ${VERSION} [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)" > /dev/null 2>&1 \
&& echo "updates.xml synced to main" \
|| echo "WARNING: failed to sync updates.xml to main"
fi
- name: Summary
run: |
VERSION="${{ steps.bump.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TAG="${{ steps.meta.outputs.tag_name }}"
echo "### Release Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY

219
.github/CLAUDE.md vendored
View File

@@ -6,60 +6,31 @@ This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: MokoStandards.Templates.GitHub DEFGROUP: MokoOnyx.GitHub
INGROUP: MokoStandards.Templates INGROUP: MokoOnyx
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
PATH: /templates/github/copilot-instructions.joomla.md.template PATH: /.github/CLAUDE.md
VERSION: XX.YY.ZZ VERSION: 01.00.05
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories BRIEF: Claude Code custom instructions for MokoOnyx template
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
Tokens replaced at sync time: MokoOnyx, https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx, {{EXTENSION_NAME}},
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
--> -->
> [!IMPORTANT] # MokoOnyx — Claude Code Instructions
> **🔧 AI Self-Update Required on First Use**
>
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
> this repository — or this is a brand-new repo.
>
> **On your very first interaction in this repository, before answering any other question:**
>
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
> 2. If they do, locate the values as described in the table below and replace every occurrence
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
> through the closing `> ---` line) in both files.
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
> values — all set."*
>
> | Placeholder | Where to find the value |
> |---|---|
> | `MokoOnyx` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
> | `https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx` | Full GitHub URL, e.g. `https://git.mokoconsulting.tech/MokoConsulting/<repo-name>` |
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
>
> ---
# MokoOnyx — GitHub Copilot Custom Instructions
## What This Repo Is ## What This Repo Is
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync. This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
Repository URL: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx Repository URL: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
Extension name: **{{EXTENSION_NAME}}** Extension name: **MokoOnyx**
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`) Extension type: **template** (`mokoonyx`)
Platform: **Joomla 4.x / MokoWaaS** Platform: **Joomla 5.x / 6.x / MokoWaaS**
Successor to: **MokoCassiopeia** (renamed in v01.00.00)
--- ---
## Primary Language ## Primary Language
**PHP** (≥ 7.4) is the primary language for this Joomla extension. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`. **PHP** (≥ 8.1) is the primary language for this Joomla template. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
--- ---
@@ -77,7 +48,7 @@ Every new file needs a copyright header as its first content.
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoOnyx.{{EXTENSION_TYPE}} * DEFGROUP: MokoOnyx.Template
* INGROUP: MokoOnyx * INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /path/to/file.php * PATH: /path/to/file.php
@@ -115,139 +86,93 @@ BRIEF: One-line description
**`README.md` is the single source of truth for the repository version.** **`README.md` is the single source of truth for the repository version.**
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03``01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. - **Patch version is auto-bumped by the release workflow** — `release.yml` reads the current version from `README.md`, increments the patch (`XX.YY.ZZ``XX.YY.(ZZ+1)`), updates `README.md`, `templateDetails.xml`, and the matching channel in `updates.xml`, commits, pushes, then builds the ZIP. Manual bumping is no longer required.
- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references. - The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references.
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`). - Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.00.05`).
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only. - Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
### Joomla Version Alignment ### Joomla Version Alignment
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically. The version in `README.md` **must always match** the `<version>` tag in `templateDetails.xml` and the matching channel entry in `updates.xml`. The release workflow updates all three automatically.
### Multi-Channel updates.xml
`updates.xml` contains separate `<update>` blocks per stability channel (development, alpha, beta, rc, stable). Each release workflow only modifies its own channel using targeted Python regex replacement — other channels are preserved untouched. Joomla filters by the user's "Minimum Stability" setting.
```xml ```xml
<!-- In manifest.xml — must match README.md version -->
<version>01.02.04</version>
<!-- In updates.xml — prepend a new <update> block for every release.
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
in the XML attribute value. Joomla's update server treats the value as a
regular expression, so \. matches a literal dot. -->
<updates> <updates>
<update> <!-- 1. DEVELOPMENT --> <update>...<tag>development</tag>...</update>
<name>{{EXTENSION_NAME}}</name> <!-- 2. ALPHA --> <update>...<tag>alpha</tag>...</update>
<version>01.02.04</version> <!-- 3. BETA --> <update>...<tag>beta</tag>...</update>
<downloads> <!-- 4. RC --> <update>...<tag>rc</tag>...</update>
<downloadurl type="full" format="zip"> <!-- 5. STABLE --> <update>...<tag>stable</tag>...</update>
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="4\.[0-9]+" />
</update>
<!-- … older entries preserved below … -->
</updates> </updates>
``` ```
**Key rules:**
- SHA-256 must be raw hex (no `sha256:` prefix)
- Version format must be `XX.YY.ZZ`, not tag names like `v01`
- Download URLs must point to Gitea (not GitHub) for all pre-release channels
- **Always push updates.xml to main** — Joomla sites read from main, not dev
--- ---
## Joomla Extension Structure ## MokoCassiopeia Migration (v01.x only)
MokoOnyx v01.x includes a migration system for MokoCassiopeia users:
- **`helper/migrate.php`** — runs on first page load via `index.php` bootstrap
- Creates `.migrated` marker file so it only runs once
- Copies template style params from MokoCassiopeia → MokoOnyx
- Creates matching styles, copies user files, redirects update server
- **Joomla 6 does NOT call `<scriptfile>` for templates** — that's why migration runs from `index.php` instead of `script.php`
- The migration script will be **removed in v02.00.00** (see ROADMAP.md)
---
## Template Structure
``` ```
MokoOnyx/ MokoOnyx/
├── manifest.xml # Joomla installer manifest (root — required) ├── src/
├── updates.xml # Update server manifest (root — required, see below) │ ├── templateDetails.xml # Joomla installer manifest
├── site/ # Frontend (site) code ├── script.php # Install/update script (limited in Joomla 6 for templates)
│ ├── controller.php │ ├── index.php # Main template file (includes migration bootstrap)
│ ├── controllers/ │ ├── helper/
│ ├── models/ │ ├── migrate.php # MokoCassiopeia → MokoOnyx migration (v01.x)
└── views/ │ ├── favicon.php # Favicon generator
├── admin/ # Backend (admin) code └── minify.php # Asset minification
│ ├── controller.php │ ├── language/ # Language INI files (en-GB, en-US)
│ ├── controllers/ │ ├── media/ # CSS, JS, images, vendor assets
── models/ ── templates/ # Custom CSS palette starters, theme test
│ ├── views/ ├── updates.xml # Update server manifest (root — required)
│ └── sql/ ├── .gitea/workflows/ # Gitea Actions workflows
├── language/ # Language INI files ├── docs/ # Documentation
├── media/ # CSS, JS, images (deployed to /media/{{EXTENSION_ELEMENT}}/)
├── docs/ # Technical documentation
├── tests/ # Test suite
├── .github/
│ ├── workflows/
│ ├── copilot-instructions.md # This file
│ └── CLAUDE.md
├── README.md # Version source of truth ├── README.md # Version source of truth
├── CHANGELOG.md ├── CHANGELOG.md
── CONTRIBUTING.md ── LICENSE
├── LICENSE # GPL-3.0-or-later
└── Makefile # Build automation
``` ```
--- ---
## updates.xml — Required in Repo Root ## Gitea Actions — Token Usage
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension. Every workflow must use **`secrets.GA_TOKEN`** (the org-level Personal Access Token).
The `manifest.xml` must reference it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/main/updates.xml
</server>
</updateservers>
```
**Rules:**
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
---
## manifest.xml Rules
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
- Must include `<files folder="site">` and `<administration>` sections.
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
---
## GitHub Actions — Token Usage
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
```yaml ```yaml
# ✅ Correct # ✅ Correct
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GA_TOKEN }}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
``` ```
```yaml ```yaml
# ❌ Wrong — never use these in workflows # ❌ Wrong
token: ${{ github.token }} token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
``` ```
--- Note: Workflows are in `.gitea/workflows/` (not `.github/workflows/`).
## MokoStandards Reference
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). Authoritative policies:
| Document | Purpose |
|----------|---------|
| [file-header-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
| [coding-style-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
| [branching-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
| [changelog-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
--- ---
@@ -260,7 +185,7 @@ This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/M
| PHP variable | `$snake_case` | `$item_id` | | PHP variable | `$snake_case` | `$item_id` |
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` | | PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
| PHP class file | `PascalCase.php` | `ItemModel.php` | | PHP class file | `PascalCase.php` | `ItemModel.php` |
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` | | YAML workflow | `kebab-case.yml` | `release.yml` |
| Markdown doc | `kebab-case.md` | `installation-guide.md` | | Markdown doc | `kebab-case.md` | `installation-guide.md` |
--- ---
@@ -286,11 +211,11 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
| Change type | Documentation to update | | Change type | Documentation to update |
|-------------|------------------------| |-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry | | New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version | | New or changed templateDetails.xml | Release workflow auto-bumps version across README.md, templateDetails.xml, and updates.xml |
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version | | New release | Trigger `release.yml` — auto-bumps patch, builds ZIP, updates matching channel in `updates.xml` |
| New or changed workflow | `docs/workflows/<workflow-name>.md` | | New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | | Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it | | **Every release** | **Patch auto-bumped** by `release.yml` — no manual version bump needed |
--- ---
@@ -300,5 +225,7 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
- Never skip the FILE INFORMATION block on a new file - Never skip the FILE INFORMATION block on a new file
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files - Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate - Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN` - Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GA_TOKEN`
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync - Never let `templateDetails.xml` version, `updates.xml` version, and `README.md` version go out of sync
- Always push `updates.xml` to main after updating on dev (Joomla reads from main)
- Always update documentation when changing MokoStandards-API or MokoStandards repos

View File

@@ -103,6 +103,16 @@ jobs:
composer install --no-dev --optimize-autoloader --no-interaction composer install --no-dev --optimize-autoloader --no-interaction
fi fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Minify CSS and JS
run: |
npm ci --ignore-scripts
node scripts/minify.js
- name: Create package - name: Create package
run: | run: |
mkdir -p build/package mkdir -p build/package

8
.gitignore vendored
View File

@@ -119,6 +119,14 @@ site/
*.js.map *.js.map
*.tsbuildinfo *.tsbuildinfo
# ============================================================
# Minified assets (generated at release time by scripts/minify.js)
# Vendor pre-minified files are excluded from this rule.
# ============================================================
src/media/css/*.min.css
src/media/css/theme/*.min.css
src/media/js/*.min.js
# ============================================================ # ============================================================
# CI / test artifacts # CI / test artifacts
# ============================================================ # ============================================================

View File

@@ -9,17 +9,17 @@
INGROUP: MokoOnyx.Documentation INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: ./README.md FILE: ./README.md
VERSION: 01.00.00 VERSION: 01.00.18
BRIEF: Documentation for MokoOnyx template BRIEF: Documentation for MokoOnyx template
--> -->
# MokoOnyx → MokoOnyx # MokoOnyx
> **This template is being renamed to MokoOnyx.** Version 01.00.00 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoOnyx can be safely uninstalled. > **MokoOnyx** is the successor to MokoCassiopeia. On install, it automatically migrates your settings, content references, and custom files. After installing, MokoCassiopeia can be safely uninstalled.
**A Modern, Lightweight Joomla Template Based on Cassiopeia** **A Modern, Lightweight Joomla Template Based on Cassiopeia**
[![Version](https://img.shields.io/badge/version-03.09.07-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/v03) [![Version](https://img.shields.io/gitea/v/release/MokoConsulting/MokoOnyx?gitea_url=https%3A%2F%2Fgit.mokoconsulting.tech&logo=gitea&logoColor=white&label=version)](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable)
[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE)
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org) [![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net) [![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net)
@@ -53,8 +53,8 @@ MokoOnyx is a modern, lightweight enhancement layer built on top of Joomla's Cas
- **Built on Cassiopeia**: Extends Joomla's default template with minimal overrides - **Built on Cassiopeia**: Extends Joomla's default template with minimal overrides
- **Font Awesome 7**: Fully integrated into Joomla's asset manager with 2,000+ icons - **Font Awesome 7**: Fully integrated into Joomla's asset manager with 2,000+ icons
- **Bootstrap 5**: Extended utility classes and responsive grid system - **Bootstrap 5**: Extended utility classes and responsive grid system
- **No Template Overrides**: Clean installation that inherits all Cassiopeia defaults - **Template Overrides**: Includes overrides for all core Joomla modules, Community Builder, and DPCalendar with consistent title rendering and Bootstrap 5 styling
- **Upgrade-Friendly**: Minimal modifications ensure smooth Joomla updates - **Upgrade-Friendly**: Minimal core modifications ensure smooth Joomla updates
### Advanced Theming ### Advanced Theming
@@ -90,8 +90,8 @@ MokoOnyx is a modern, lightweight enhancement layer built on top of Joomla's Cas
## 📋 Requirements ## 📋 Requirements
- **Joomla**: 4.4.x or 5.x - **Joomla**: 5.x or 6.x
- **PHP**: 8.0 or higher - **PHP**: 8.1 or higher
- **Database**: MySQL 5.7+ / MariaDB 10.2+ / PostgreSQL 11+ - **Database**: MySQL 5.7+ / MariaDB 10.2+ / PostgreSQL 11+
- **Browser Support**: Modern browsers (Chrome, Firefox, Safari, Edge) - **Browser Support**: Modern browsers (Chrome, Firefox, Safari, Edge)
@@ -360,11 +360,7 @@ See the [CHANGELOG.md](./CHANGELOG.md) for detailed version history.
### Recent Releases ### Recent Releases
- **[03.06.03]** (2026-01-30) - README updates and TOC color variable improvements See [Gitea Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases) for all versions.
- **[03.06.02]** (2026-01-30) - Complete rebrand to MokoOnyx, removed all overrides
- **[03.06.00]** (2026-01-28) - Version standardization
- **[03.05.01]** (2026-01-09) - Security and compliance improvements
- **[02.00.00]** (2025-08-30) - Dark mode toggle and improved theming
--- ---
@@ -373,8 +369,7 @@ See the [CHANGELOG.md](./CHANGELOG.md) for detailed version history.
### Getting Help ### Getting Help
- **Documentation**: Check this README and [docs folder](./docs/) - **Documentation**: Check this README and [docs folder](./docs/)
- **Issues**: Report bugs via [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues) - **Issues**: Report bugs via [Gitea Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Discussions**: Ask questions in [GitHub Discussions](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/discussions)
- **Roadmap**: View planned features in [Roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap) - **Roadmap**: View planned features in [Roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap)
### Reporting Bugs ### Reporting Bugs
@@ -439,7 +434,7 @@ MokoOnyx includes the following third-party libraries to provide enhanced functi
- Responsive design (collapses on mobile) - Responsive design (collapses on mobile)
- Smooth scrolling to sections - Smooth scrolling to sections
- Automatic unique ID generation for headings - Automatic unique ID generation for headings
- **Customizations**: CSS adapted to use Cassiopeia CSS variables for theme compatibility - **Customizations**: CSS adapted to use MokoOnyx CSS variables for theme compatibility
### Font Awesome 7 Free ### Font Awesome 7 Free
@@ -467,7 +462,7 @@ All third-party libraries are:
- ✅ Registered in Joomla's Web Asset Manager (`joomla.asset.json`) - ✅ Registered in Joomla's Web Asset Manager (`joomla.asset.json`)
- ✅ Loaded on-demand to optimize performance - ✅ Loaded on-demand to optimize performance
- ✅ Versioned and documented for maintenance - ✅ Versioned and documented for maintenance
- ✅ Compatible with Joomla 4.4.x and 5.x - ✅ Compatible with Joomla 5.x and 6.x
--- ---
@@ -489,9 +484,9 @@ All third-party libraries and assets remain the property of their respective aut
## 🔗 Links ## 🔗 Links
- **Repository**: [GitHub](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) - **Repository**: [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues) - **Issue Tracker**: [Gitea Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Discussions**: [GitHub Discussions](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/discussions) - **GitHub Mirror**: [GitHub](https://github.com/mokoconsulting-tech/MokoOnyx)
- **Roadmap**: [Full Roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap) - **Roadmap**: [Full Roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap)
- **Moko Consulting**: [Website](https://mokoconsulting.tech) - **Moko Consulting**: [Website](https://mokoconsulting.tech)
@@ -509,11 +504,8 @@ All third-party libraries and assets remain the property of their respective aut
| Date | Version | Change Summary | Author | | Date | Version | Change Summary | Author |
| ---------- | -------- | ------------------------------------------------------------------------- | ------------------------------- | | ---------- | -------- | ------------------------------------------------------------------------- | ------------------------------- |
| 2026-01-30 | 03.06.03 | Updated README title, fixed custom color variables instructions, improved TOC color scheme integration | Copilot Agent | | 2026-04-22 | 01.00.15 | Updated README: dynamic version badge, corrected requirements, fixed links | Claude Code |
| 2026-01-30 | 03.06.02 | Regenerated README with comprehensive documentation and updated structure | Copilot Agent | | 2026-04-19 | 01.00.00 | Initial MokoOnyx release — renamed from MokoCassiopeia with auto-migration | Moko Consulting |
| 2026-01-30 | 03.06.02 | Complete rebrand to MokoOnyx, removed overrides | Copilot Agent |
| 2026-01-05 | 03.00.00 | Initial publication of template documentation and feature overview | Moko Consulting |
| 2026-01-05 | 03.00.00 | Fixed malformed markdown table formatting in revision history | Jonathan Miller (@jmiller-moko) |
--- ---

View File

@@ -22,7 +22,7 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Template.Site DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: docs/ROADMAP.md FILE: docs/ROADMAP.md
VERSION: 03.09.03 VERSION: 03.09.03
BRIEF: Version-specific roadmap for MokoOnyx template BRIEF: Version-specific roadmap for MokoOnyx template
@@ -144,6 +144,25 @@ This document provides a comprehensive, version-specific roadmap for the MokoOny
The following versions represent our planned annual major releases, each building upon the previous version's foundation. The following versions represent our planned annual major releases, each building upon the previous version's foundation.
#### v02.00.00 (Q3 2026) - Clean Slate
**Status**: Planned
**Target Release**: Q3 2026
**Breaking Changes**:
- **Remove MokoCassiopeia migration script** — the `helper/migrate.php` bootstrap in `index.php` and the `.migrated` marker file will be removed. All MokoCassiopeia users must migrate before upgrading to v02.
- **Remove MokoCassiopeia references** — all `str_replace(mokocassiopeia, mokoonyx)` logic, old name constants, and legacy compatibility code will be cleaned out.
- **Remove `script.php` bridge logic** — the `postflight()` migration code for MokoCassiopeia will be removed.
**New Features**:
- Clean codebase with no migration overhead
- Performance improvements from removing first-load migration check
- Fresh start for MokoOnyx-native development
**Migration Notice**:
Users still running MokoCassiopeia must install MokoOnyx v01.x first to migrate their settings before upgrading to v02. The v01.x line will remain available for download.
---
#### v04.00.00 (Q4 2027) - Enhanced Accessibility & Performance #### v04.00.00 (Q4 2027) - Enhanced Accessibility & Performance
**Status**: Planned **Status**: Planned
**Target Release**: December 2027 **Target Release**: December 2027
@@ -860,8 +879,8 @@ MokoOnyx aims to be the **most developer-friendly, user-customizable, and standa
### Official Links ### Official Links
- **Full Roadmap**: [https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap) - **Full Roadmap**: [https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap)
- **Repository**: [https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia) - **Repository**: [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia/issues) - **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Changelog**: [CHANGELOG.md](../CHANGELOG.md) - **Changelog**: [CHANGELOG.md](../CHANGELOG.md)
### Community ### Community
@@ -882,7 +901,7 @@ MokoOnyx aims to be the **most developer-friendly, user-customizable, and standa
Have ideas for future features? We welcome community input! Have ideas for future features? We welcome community input!
**How to Suggest Features**: **How to Suggest Features**:
1. Check the [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia/issues) for existing requests 1. Check the [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues) for existing requests
2. Open a new issue with the `enhancement` label 2. Open a new issue with the `enhancement` label
3. Provide clear use cases and benefits 3. Provide clear use cases and benefits
4. Engage in community discussion 4. Engage in community discussion

87
scripts/minify.js Normal file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env node
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoOnyx.Build
* INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /scripts/minify.js
* VERSION: 01.00.00
* BRIEF: Minify project CSS and JS assets (excludes vendor/)
*/
const fs = require('fs');
const path = require('path');
const CleanCSS = require('clean-css');
const { minify: terserMinify } = require('terser');
const SRC = path.resolve(__dirname, '..', 'src', 'media');
// Project-owned files only — vendor assets ship pre-minified
const CSS_FILES = [
'css/editor.css',
'css/template.css',
'css/theme/dark.standard.css',
'css/theme/light.standard.css',
];
const JS_FILES = [
'js/gtm.js',
'js/template.js',
];
async function minifyCSS(relPath) {
const src = path.join(SRC, relPath);
const dest = src.replace(/\.css$/, '.min.css');
const input = fs.readFileSync(src, 'utf8');
const result = new CleanCSS({ level: 1 }).minify(input);
if (result.errors.length) {
console.error(` FAIL ${relPath}:`, result.errors);
return false;
}
fs.writeFileSync(dest, result.styles);
const ratio = ((1 - result.styles.length / input.length) * 100).toFixed(0);
console.log(` ${relPath} → .min.css (${ratio}% smaller)`);
return true;
}
async function minifyJS(relPath) {
const src = path.join(SRC, relPath);
const dest = src.replace(/\.js$/, '.min.js');
const input = fs.readFileSync(src, 'utf8');
const result = await terserMinify(input, { compress: true, mangle: true });
if (result.error) {
console.error(` FAIL ${relPath}:`, result.error);
return false;
}
fs.writeFileSync(dest, result.code);
const ratio = ((1 - result.code.length / input.length) * 100).toFixed(0);
console.log(` ${relPath} → .min.js (${ratio}% smaller)`);
return true;
}
async function main() {
console.log('Minifying project assets...\n');
let ok = true;
for (const f of CSS_FILES) {
if (!await minifyCSS(f)) ok = false;
}
for (const f of JS_FILES) {
if (!await minifyJS(f)) ok = false;
}
console.log(ok ? '\nDone.' : '\nCompleted with errors.');
process.exit(ok ? 0 : 1);
}
main();

View File

@@ -48,7 +48,8 @@ class MokoFaviconHelper
$sourceTime = filemtime($sourcePath); $sourceTime = filemtime($sourcePath);
$stampFile = $outputDir . '/.favicon_generated'; $stampFile = $outputDir . '/.favicon_generated';
if (is_file($stampFile) && filemtime($stampFile) >= $sourceTime) { $manifestFile = $outputDir . '/site.webmanifest';
if (is_file($stampFile) && filemtime($stampFile) >= $sourceTime && is_file($manifestFile)) {
return true; return true;
} }

297
src/helper/migrate.php Normal file
View File

@@ -0,0 +1,297 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* One-time migration from MokoCassiopeia → MokoOnyx.
* Called from index.php on first page load. Creates a .migrated
* marker file so it only runs once.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
(function () {
$markerFile = __DIR__ . '/../.migrated';
// Already migrated
if (file_exists($markerFile)) {
return;
}
$db = Factory::getDbo();
$app = Factory::getApplication();
$oldName = 'mokocassiopeia';
$newName = 'mokoonyx';
$oldDisplay = 'MokoCassiopeia';
$newDisplay = 'MokoOnyx';
// Init logger
Log::addLogger(
['text_file' => 'mokoonyx_migrate.log.php'],
Log::ALL,
['mokoonyx_migrate']
);
$log = function (string $msg, int $level = Log::INFO) {
Log::add($msg, $level, 'mokoonyx_migrate');
};
$log('=== MokoOnyx migration started (index.php bootstrap) ===');
// Check if MokoCassiopeia has styles to migrate
$query = $db->getQuery(true)
->select('*')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote($oldName))
->where($db->quoteName('client_id') . ' = 0');
$oldStyles = $db->setQuery($query)->loadObjectList();
if (empty($oldStyles)) {
$log('No MokoCassiopeia styles found — fresh install, nothing to migrate.');
@file_put_contents($markerFile, date('Y-m-d H:i:s') . ' fresh install');
return;
}
$log('Found ' . count($oldStyles) . ' MokoCassiopeia style(s) to migrate.');
// Get the default MokoOnyx style (created by Joomla installer)
$query = $db->getQuery(true)
->select('id')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote($newName))
->where($db->quoteName('client_id') . ' = 0')
->order($db->quoteName('id') . ' ASC');
$defaultOnyxId = (int) $db->setQuery($query, 0, 1)->loadResult();
$isFirst = true;
foreach ($oldStyles as $old) {
$newTitle = str_replace($oldDisplay, $newDisplay, $old->title);
$newTitle = str_replace($oldName, $newName, $newTitle);
$newParams = is_string($old->params)
? str_replace($oldName, $newName, $old->params)
: $old->params;
if ($isFirst && $defaultOnyxId) {
// Apply params to the installer-created default MokoOnyx style
$update = $db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
->set($db->quoteName('params') . ' = ' . $db->quote($newParams))
->where('id = ' . $defaultOnyxId);
$db->setQuery($update)->execute();
// Set as default if MokoCassiopeia was default
if ($old->home == 1) {
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 1')
->where('id = ' . $defaultOnyxId)
)->execute();
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 0')
->where('id = ' . (int) $old->id)
)->execute();
$log('Set MokoOnyx as default site template.');
}
$log("Applied params to default MokoOnyx style: {$newTitle}");
$isFirst = false;
continue;
}
// Additional styles: check if already exists
$check = $db->getQuery(true)
->select('COUNT(*)')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote($newName))
->where($db->quoteName('title') . ' = ' . $db->quote($newTitle));
if ((int) $db->setQuery($check)->loadResult() > 0) {
$log("Style '{$newTitle}' already exists — skipping.");
continue;
}
// Create new MokoOnyx style copy
$new = clone $old;
unset($new->id);
$new->template = $newName;
$new->title = $newTitle;
$new->params = $newParams;
$new->home = 0;
try {
$db->insertObject('#__template_styles', $new, 'id');
$log("Created MokoOnyx style: {$newTitle}");
} catch (\Throwable $e) {
$log("Failed to create style '{$newTitle}': " . $e->getMessage(), Log::WARNING);
}
}
// Copy user files from MokoCassiopeia media
$oldMedia = JPATH_ROOT . '/media/templates/site/' . $oldName;
$newMedia = JPATH_ROOT . '/media/templates/site/' . $newName;
if (is_dir($oldMedia) && is_dir($newMedia)) {
$userFiles = [
'css/theme/light.custom.css',
'css/theme/dark.custom.css',
'css/theme/light.custom.min.css',
'css/theme/dark.custom.min.css',
'css/user.css',
'css/user.min.css',
'js/user.js',
'js/user.min.js',
];
$copied = 0;
foreach ($userFiles as $rel) {
$src = $oldMedia . '/' . $rel;
$dst = $newMedia . '/' . $rel;
if (is_file($src) && !is_file($dst)) {
$dir = dirname($dst);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
copy($src, $dst);
$copied++;
}
}
if ($copied > 0) {
$log("Copied {$copied} user file(s) from MokoCassiopeia.");
}
}
// Replace MokoCassiopeia references in article content
try {
$contentTables = [
['table' => '#__content', 'columns' => ['introtext', 'fulltext']],
];
$totalReplaced = 0;
foreach ($contentTables as $spec) {
foreach ($spec['columns'] as $col) {
$query = $db->getQuery(true)
->update($spec['table'])
->set(
$db->quoteName($col) . ' = REPLACE(REPLACE('
. $db->quoteName($col) . ', '
. $db->quote($oldDisplay) . ', '
. $db->quote($newDisplay) . '), '
. $db->quote($oldName) . ', '
. $db->quote($newName) . ')'
)
->where(
'(' . $db->quoteName($col) . ' LIKE ' . $db->quote('%' . $oldDisplay . '%')
. ' OR ' . $db->quoteName($col) . ' LIKE ' . $db->quote('%' . $oldName . '%') . ')'
);
$db->setQuery($query)->execute();
$totalReplaced += $db->getAffectedRows();
}
}
if ($totalReplaced > 0) {
$log("Replaced MokoCassiopeia references in {$totalReplaced} content row(s).");
}
} catch (\Throwable $e) {
$log('Content replacement failed: ' . $e->getMessage(), Log::WARNING);
}
// Replace MokoCassiopeia references in custom HTML modules
try {
$query = $db->getQuery(true)
->update('#__modules')
->set(
$db->quoteName('content') . ' = REPLACE(REPLACE('
. $db->quoteName('content') . ', '
. $db->quote($oldDisplay) . ', '
. $db->quote($newDisplay) . '), '
. $db->quote($oldName) . ', '
. $db->quote($newName) . ')'
)
->where(
'(' . $db->quoteName('content') . ' LIKE ' . $db->quote('%' . $oldDisplay . '%')
. ' OR ' . $db->quoteName('content') . ' LIKE ' . $db->quote('%' . $oldName . '%') . ')'
);
$db->setQuery($query)->execute();
$modulesUpdated = $db->getAffectedRows();
if ($modulesUpdated > 0) {
$log("Replaced MokoCassiopeia references in {$modulesUpdated} module(s).");
}
} catch (\Throwable $e) {
$log('Module content replacement failed: ' . $e->getMessage(), Log::WARNING);
}
// Update the update server
try {
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
$query = $db->getQuery(true)
->update('#__update_sites')
->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
->set($db->quoteName('name') . ' = ' . $db->quote($newDisplay))
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
$db->setQuery($query)->execute();
$n = $db->getAffectedRows();
if ($n > 0) {
$log("Redirected {$n} update site(s) to MokoOnyx.");
}
} catch (\Throwable $e) {
$log('Update server redirect failed: ' . $e->getMessage(), Log::WARNING);
}
// Unlock MokoCassiopeia (allow uninstall) + lock MokoOnyx (prevent accidental uninstall)
try {
$query = $db->getQuery(true)
->update('#__extensions')
->set($db->quoteName('locked') . ' = 0')
->set($db->quoteName('protected') . ' = 0')
->where($db->quoteName('element') . ' = ' . $db->quote($oldName))
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
$db->setQuery($query)->execute();
if ($db->getAffectedRows() > 0) {
$log('Unlocked MokoCassiopeia (can be uninstalled).');
}
} catch (\Throwable $e) {
$log('Failed to unlock MokoCassiopeia: ' . $e->getMessage(), Log::WARNING);
}
try {
$query = $db->getQuery(true)
->update('#__extensions')
->set($db->quoteName('locked') . ' = 1')
->where($db->quoteName('element') . ' = ' . $db->quote($newName))
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
$db->setQuery($query)->execute();
if ($db->getAffectedRows() > 0) {
$log('Locked MokoOnyx (protected from uninstall).');
}
} catch (\Throwable $e) {
$log('Failed to lock MokoOnyx: ' . $e->getMessage(), Log::WARNING);
}
// Write marker file
@file_put_contents($markerFile, date('Y-m-d H:i:s') . " migrated {$oldName}{$newName}");
$log('=== Migration completed ===');
// Enqueue message for admin
if ($app->isClient('administrator')) {
$app->enqueueMessage(
'<strong>MokoOnyx has imported your MokoCassiopeia settings.</strong><br>'
. 'You can safely uninstall MokoCassiopeia from Extensions &rarr; Manage.',
'success'
);
}
})();

View File

@@ -0,0 +1,48 @@
<?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
*
* FILE INFORMATION
* DEFGROUP: MokoOnyx.Layout
* INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /html/layouts/joomla/module/card.php
* VERSION: 01.00.07
* BRIEF: Custom card module chrome — renders module titles for all modules
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$module = $displayData['module'];
$params = $displayData['params'];
$attribs = $displayData['attribs'];
$moduleTag = htmlspecialchars($params->get('module_tag', 'div'), ENT_QUOTES, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_QUOTES, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_QUOTES, 'UTF-8');
$bootstrapSize = (int) $params->get('bootstrap_size', 0);
$moduleClass = htmlspecialchars($attribs['moduleclass_sfx'] ?? '', ENT_QUOTES, 'UTF-8');
$moduleId = 'module-' . $module->id;
if ($module->content === '' && $module->content === null) {
return;
}
$cardClass = 'card' . ($moduleClass ? ' ' . $moduleClass : '');
?>
<<?php echo $moduleTag; ?> id="<?php echo $moduleId; ?>" class="<?php echo $cardClass; ?>">
<?php if ((bool) $module->showtitle) : ?>
<div class="card-header">
<<?php echo $headerTag; ?> class="card-title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
</div>
<?php endif; ?>
<div class="card-body">
<?php echo $module->content; ?>
</div>
</<?php echo $moduleTag; ?>>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

View File

@@ -0,0 +1,194 @@
<?php
/**
* Community Builder (TM)
* @version $Id: $
* @package CommunityBuilder
* @copyright (C) 2004-2025 www.joomlapolis.com / Lightning MultiCom SA - and its licensors, all rights reserved
* @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU/GPL version 2
*/
use CBLib\Application\Application;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
if ( ! ( defined( '_VALID_CB' ) || defined( '_JEXEC' ) || defined( '_VALID_MOS' ) ) ) { die( 'Direct Access to this location is not allowed.' ); }
HTMLHelper::_( 'behavior.keepalive' );
?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'beforeForm' ); ?>
<form action="<?php echo $_CB_framework->viewUrl( 'login', true, null, 'html', $secureForm ); ?>" method="post" id="login-form" class="cbLoginForm">
<input type="hidden" name="option" value="com_comprofiler" />
<input type="hidden" name="view" value="login" />
<input type="hidden" name="op2" value="login" />
<input type="hidden" name="return" value="B:<?php echo $loginReturnUrl; ?>" />
<input type="hidden" name="message" value="<?php echo (int) $params->get( 'login_message', 0 ); ?>" />
<input type="hidden" name="loginfrom" value="<?php echo htmlspecialchars( ( defined( '_UE_LOGIN_FROM' ) ? _UE_LOGIN_FROM : 'loginmodule' ) ); ?>" />
<?php echo Application::Session()->getFormTokenInput(); ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'start' ); ?>
<?php if ( $preLogintText ) { ?>
<div class="pretext <?php echo htmlspecialchars( $templateClass ); ?>">
<p><?php echo $preLogintText; ?></p>
</div>
<?php } ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'almostStart' ); ?>
<?php if ( $loginMethod != 4 ) { ?>
<fieldset class="userdata">
<p id="form-login-username">
<?php if ( in_array( $showUsernameLabel, array( 1, 2, 3, 5 ) ) ) { ?>
<?php if ( in_array( $showUsernameLabel, array( 2, 5 ) ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleUsernameIcon fa fa-user" title="<?php echo htmlspecialchars( $userNameText ); ?>"></span>
</span>
<?php } else { ?>
<label for="modlgn-username">
<?php if ( $showUsernameLabel == 3 ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleUsernameIcon fa fa-user" title="<?php echo htmlspecialchars( $userNameText ); ?>"></span>
</span>
<?php } ?>
<?php if ( in_array( $showUsernameLabel, array( 1, 3 ) ) ) { ?>
<?php echo htmlspecialchars( $userNameText ); ?>
<?php } ?>
</label>
<?php } ?>
<?php } ?>
<input id="modlgn-username" type="text" name="username" class="<?php echo ( $styleUsername ? htmlspecialchars( $styleUsername ) : 'inputbox' ); ?>" size="<?php echo $usernameInputLength; ?>"<?php echo ( in_array( $showUsernameLabel, array( 4, 5 ) ) ? ' placeholder="' . htmlspecialchars( $userNameText ) . '"' : null ); ?> />
</p>
<p id="form-login-password">
<?php if ( in_array( $showPasswordLabel, array( 1, 2, 3, 5 ) ) ) { ?>
<?php if ( in_array( $showPasswordLabel, array( 2, 5 ) ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModulePasswordIcon fa fa-lock" title="<?php echo htmlspecialchars( CBTxt::T( 'Password' ) ); ?>"></span>
</span>
<?php } else { ?>
<label for="modlgn-passwd">
<?php if ( $showPasswordLabel == 3 ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModulePasswordIcon fa fa-lock" title="<?php echo htmlspecialchars( CBTxt::T( 'Password' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( in_array( $showPasswordLabel, array( 1, 3 ) ) ) { ?>
<?php echo htmlspecialchars( CBTxt::T( 'Password' ) ); ?>
<?php } ?>
</label>
<?php } ?>
<?php } ?>
<input id="modlgn-passwd" type="password" name="passwd" class="<?php echo ( $stylePassword ? htmlspecialchars( $stylePassword ) : 'inputbox' ); ?>" size="<?php echo $passwordInputLength; ?>"<?php echo ( in_array( $showPasswordLabel, array( 4, 5 ) ) ? ' placeholder="' . htmlspecialchars( CBTxt::T( 'Password' ) ) . '"' : null ); ?> />
</p>
<?php if ( count( $twoFactorMethods ) > 1 ) { ?>
<p id="form-login-secretkey">
<?php if ( in_array( $showSecretKeyLabel, array( 1, 2, 3, 5 ) ) ) { ?>
<?php if ( in_array( $showSecretKeyLabel, array( 2, 5 ) ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleSecretKeyIcon fa fa-star" title="<?php echo htmlspecialchars( CBTxt::T( 'Secret Key' ) ); ?>"></span>
</span>
<?php } else { ?>
<label for="modlgn-secretkey">
<?php if ( $showSecretKeyLabel == 3 ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleSecretKeyIcon fa fa-star" title="<?php echo htmlspecialchars( CBTxt::T( 'Secret Key' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( in_array( $showSecretKeyLabel, array( 1, 3 ) ) ) { ?>
<?php echo htmlspecialchars( CBTxt::T( 'Secret Key' ) ); ?>
<?php } ?>
</label>
<?php } ?>
<?php } ?>
<input id="modlgn-secretkey" autocomplete="one-time-code" type="text" name="secretkey" tabindex="0" class="<?php echo ( $styleSecretKey ? htmlspecialchars( $styleSecretKey ) : 'inputbox' ); ?>" size="<?php echo $secretKeyInputLength; ?>"<?php echo ( in_array( $showSecretKeyLabel, array( 4, 5 ) ) ? ' placeholder="' . htmlspecialchars( CBTxt::T( 'Secret Key' ) ) . '"' : null ); ?> />
</p>
<?php } ?>
<?php if ( in_array( $showRememberMe, array( 1, 3 ) ) ) { ?>
<p id="form-login-remember">
<label for="modlgn-remember"><?php echo htmlspecialchars( CBTxt::T( 'Remember Me' ) ); ?></label>
<input id="modlgn-remember" type="checkbox" name="remember" class="inputbox" value="yes"<?php echo ( $showRememberMe == 3 ? ' checked="checked"' : null ); ?> />
</p>
<?php } elseif ( $showRememberMe == 2 ) { ?>
<input id="modlgn-remember" type="hidden" name="remember" class="inputbox" value="yes" />
<?php } ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'beforeButton', 'p' ); ?>
<button type="submit" name="Submit" class="<?php echo ( $styleLogin ? htmlspecialchars( $styleLogin ) : 'button' ); ?>"<?php echo $buttonStyle; ?>>
<?php if ( in_array( $showButton, array( 1, 2, 3 ) ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleLoginIcon fa fa-sign-in" title="<?php echo htmlspecialchars( CBTxt::T( 'Log in' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( in_array( $showButton, array( 0, 1, 4 ) ) ) { ?>
<?php echo htmlspecialchars( CBTxt::T( 'Log in' ) ); ?>
<?php } ?>
</button>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'afterButton', 'p' ); ?>
</fieldset>
<?php } else { ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'beforeButton', 'p' ); ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'afterButton', 'p' ); ?>
<?php } ?>
<?php if ( $showForgotLogin || $showRegister ) { ?>
<ul id="form-login-links">
<?php if ( $showForgotLogin ) { ?>
<?php if ( ! Application::Config()->getBool( 'forgotlogin_type', true ) ) { ?>
<li id="form-login-forgot-password">
<a href="<?php echo cbSef( 'index.php?option=com_users&view=reset' ); ?>"<?php echo ( $styleForgotLogin ? ' class="' . htmlspecialchars( $styleForgotLogin ) . '"' : null ); ?>>
<?php if ( in_array( $showForgotLogin, array( 2, 3 ) ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleForgotLoginIcon fa fa-unlock-alt" title="<?php echo htmlspecialchars( Text::_( 'MOD_LOGIN_FORGOT_YOUR_PASSWORD' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( in_array( $showForgotLogin, array( 1, 3 ) ) ) { ?>
<?php echo Text::_( 'MOD_LOGIN_FORGOT_YOUR_PASSWORD' ); ?>
<?php } ?>
</a>
</li>
<li id="form-login-forgot-username">
<a href="<?php echo cbSef( 'index.php?option=com_users&view=remind' ); ?>"<?php echo ( $styleForgotLogin ? ' class="' . htmlspecialchars( $styleForgotLogin ) . '"' : null ); ?>>
<?php if ( in_array( $showForgotLogin, array( 2, 3 ) ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleForgotLoginIcon fa fa-unlock-alt" title="<?php echo htmlspecialchars( Text::_( 'MOD_LOGIN_FORGOT_YOUR_USERNAME' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( in_array( $showForgotLogin, array( 1, 3 ) ) ) { ?>
<?php echo Text::_( 'MOD_LOGIN_FORGOT_YOUR_USERNAME' ); ?>
<?php } ?>
</a>
</li>
<?php } else { ?>
<li id="form-login-forgot">
<a href="<?php echo $_CB_framework->viewUrl( 'lostpassword', true, null, 'html', $secureForm ); ?>"<?php echo ( $styleForgotLogin ? ' class="' . htmlspecialchars( $styleForgotLogin ) . '"' : null ); ?>>
<?php if ( in_array( $showForgotLogin, array( 2, 3 ) ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleForgotLoginIcon fa fa-unlock-alt" title="<?php echo htmlspecialchars( CBTxt::T( 'Forgot Login?' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( in_array( $showForgotLogin, array( 1, 3 ) ) ) { ?>
<?php echo CBTxt::T( 'Forgot Login?' ); ?>
<?php } ?>
</a>
</li>
<?php } ?>
<?php } ?>
<?php if ( $showRegister ) { ?>
<li id="form-login-register">
<a href="<?php echo $_CB_framework->viewUrl( 'registers', true, null, 'html', $secureForm ); ?>"<?php echo ( $styleRegister ? ' class="' . htmlspecialchars( $styleRegister ) . '"' : null ); ?>>
<?php if ( in_array( $params->get( 'show_newaccount', 1 ), array( 2, 3 ) ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleRegisterIcon fa fa-edit" title="<?php echo htmlspecialchars( CBTxt::T( 'UE_REGISTER', 'Sign up' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( in_array( $params->get( 'show_newaccount', 1 ), array( 1, 3 ) ) ) { ?>
<?php echo CBTxt::T( 'UE_REGISTER', 'Sign up' ); ?>
<?php } ?>
</a>
</li>
<?php } ?>
</ul>
<?php } ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'almostEnd' ); ?>
<?php if ( $postLoginText ) { ?>
<div class="posttext <?php echo htmlspecialchars( $templateClass ); ?>">
<p><?php echo $postLoginText; ?></p>
</div>
<?php } ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'end' ); ?>
</form>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'afterForm' ); ?>

View File

@@ -0,0 +1,127 @@
<?php
/**
* Community Builder (TM)
* @version $Id: $
* @package CommunityBuilder
* @copyright (C) 2004-2025 www.joomlapolis.com / Lightning MultiCom SA - and its licensors, all rights reserved
* @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU/GPL version 2
*/
use CBLib\Application\Application;
use Joomla\CMS\HTML\HTMLHelper;
if ( ! ( defined( '_VALID_CB' ) || defined( '_JEXEC' ) || defined( '_VALID_MOS' ) ) ) { die( 'Direct Access to this location is not allowed.' ); }
HTMLHelper::_( 'behavior.keepalive' );
?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'beforeForm' ); ?>
<form action="<?php echo $_CB_framework->viewUrl( 'logout', true, null, 'html', $secureForm ); ?>" method="post" id="login-form" class="cbLogoutForm">
<input type="hidden" name="option" value="com_comprofiler" />
<input type="hidden" name="view" value="logout" />
<input type="hidden" name="op2" value="logout" />
<input type="hidden" name="return" value="B:<?php echo $logoutReturnUrl; ?>" />
<input type="hidden" name="message" value="<?php echo (int) $params->get( 'logout_message', 0 ); ?>" />
<?php echo Application::Session()->getFormTokenInput(); ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'start' ); ?>
<?php if ( $preLogoutText ) { ?>
<div class="pretext <?php echo htmlspecialchars( $templateClass ); ?>">
<p><?php echo $preLogoutText; ?></p>
</div>
<?php } ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'almostStart' ); ?>
<?php if ( (int) $params->get( 'greeting', 1 ) ) { ?>
<div class="login-greeting <?php echo htmlspecialchars( $templateClass ); ?>">
<p><?php echo $greetingText; ?></p>
</div>
<?php } ?>
<?php if ( (int) $params->get( 'show_avatar', 1 ) ) { ?>
<div class="login-avatar <?php echo htmlspecialchars( $templateClass ); ?>">
<p><?php echo $cbUser->getField( 'avatar', null, 'html', 'none', 'list', 0, true ); ?></p>
</div>
<?php } ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'beforeButton', 'p' ); ?>
<div class="logout-button">
<button type="submit" name="Submit" class="<?php echo ( $styleLogout ? htmlspecialchars( $styleLogout ) : 'button' ); ?>"<?php echo $buttonStyle; ?>>
<?php if ( in_array( $showButton, array( 1, 2, 3 ) ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleLogoutIcon fa fa-sign-out" title="<?php echo htmlspecialchars( CBTxt::T( 'Log out' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( in_array( $showButton, array( 0, 1, 4 ) ) ) { ?>
<?php echo htmlspecialchars( CBTxt::T( 'Log out' ) ); ?>
<?php } ?>
</button>
</div>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'afterButton', 'p' ); ?>
<?php if ( $profileViewText || $profileEditText || $showPrivateMessages || $showConnectionRequests ) { ?>
<p>
<ul class="logout-links">
<?php if ( $showPrivateMessages ) { ?>
<li class="logout-private-messages">
<a href="<?php echo $privateMessageURL; ?>"<?php echo ( $stylePrivateMsgs ? ' class="' . htmlspecialchars( $stylePrivateMsgs ) . '"' : null ); ?>>
<?php if ( $params->get( 'show_pms_icon', 0 ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModulePMIcon fa fa-envelope" title="<?php echo htmlspecialchars( CBTxt::T( 'Private Messages' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( $newMessageCount ) { ?>
<?php echo ( $newMessageCount == 1 ? CBTxt::T( 'YOU_HAVE_COUNT_NEW_PRIVATE_MESSAGE', 'You have [count] new private message.', array( '[count]' => $newMessageCount ) ) : CBTxt::T( 'YOU_HAVE_COUNT_NEW_PRIVATE_MESSAGES', 'You have [count] new private messages.', array( '[count]' => $newMessageCount ) ) ); ?>
<?php } else { ?>
<?php echo CBTxt::T( 'You have no new private messages.' ); ?>
<?php } ?>
</a>
</li>
<?php } ?>
<?php if ( $showConnectionRequests ) { ?>
<li class="logout-connection-requests">
<a href="<?php echo $_CB_framework->viewUrl( 'manageconnections' ); ?>"<?php echo ( $styleConnRequests ? ' class="' . htmlspecialchars( $styleConnRequests ) . '"' : null ); ?>>
<?php if ( $params->get( 'show_connection_notifications_icon', 0 ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleConnectionsIcon fa fa-users" title="<?php echo htmlspecialchars( CBTxt::T( 'Connections' ) ); ?>"></span>
</span>
<?php } ?>
<?php if ( $newConnectionRequests ) { ?>
<?php echo ( $newConnectionRequests == 1 ? CBTxt::T( 'YOU_HAVE_COUNT_NEW_CONNECTION_REQUEST', 'You have [count] new connection request.', array( '[count]' => $newConnectionRequests ) ) : CBTxt::T( 'YOU_HAVE_COUNT_NEW_CONNECTION_REQUESTS', 'You have [count] new connection requests.', array( '[count]' => $newConnectionRequests ) ) ); ?>
<?php } else { ?>
<?php echo CBTxt::T( 'You have no new connection requests.' ); ?>
<?php } ?>
</a>
</li>
<?php } ?>
<?php if ( $profileViewText ) { ?>
<li class="logout-profile">
<a href="<?php echo $_CB_framework->userProfileUrl(); ?>"<?php echo ( $styleProfile ? ' class="' . htmlspecialchars( $styleProfile ) . '"' : null ); ?>>
<?php if ( $params->get( 'icon_show_profile', 0 ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleProfileViewIcon fa fa-user" title="<?php echo htmlspecialchars( $profileViewText ); ?>"></span>
</span>
<?php } ?>
<?php echo $profileViewText; ?>
</a>
</li>
<?php } ?>
<?php if ( $profileEditText ) { ?>
<li class="logout-profile-edit">
<a href="<?php echo $_CB_framework->userProfileEditUrl(); ?>"<?php echo ( $styleProfileEdit ? ' class="' . htmlspecialchars( $styleProfileEdit ) . '"' : null ); ?>>
<?php if ( $params->get( 'icon_edit_profile', 0 ) ) { ?>
<span class="<?php echo htmlspecialchars( $templateClass ); ?>">
<span class="cbModuleProfileEditIcon fa fa-pencil" title="<?php echo htmlspecialchars( $profileEditText ); ?>"></span>
</span>
<?php } ?>
<?php echo $profileEditText; ?>
</a>
</li>
<?php } ?>
</ul>
</p>
<?php } ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'almostEnd' ); ?>
<?php if ( $postLogoutText ) { ?>
<div class="posttext <?php echo htmlspecialchars( $templateClass ); ?>">
<p><?php echo $postLogoutText; ?></p>
</div>
<?php } ?>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'end' ); ?>
</form>
<?php echo modCBLoginHelper::getPlugins( $params, $type, 'afterForm' ); ?>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

View File

@@ -0,0 +1,66 @@
<?php
/**
* Community Builder (TM)
* @version $Id: $
* @package CommunityBuilder
* @copyright (C) 2004-2025 www.joomlapolis.com / Lightning MultiCom SA - and its licensors, all rights reserved
* @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU/GPL version 2
*/
if ( ! ( defined( '_VALID_CB' ) || defined( '_JEXEC' ) || defined( '_VALID_MOS' ) ) ) { die( 'Direct Access to this location is not allowed.' ); }
?>
<?php echo modCBModeratorHelper::getPlugins( $params, 'start' ); ?>
<?php if ( $preText ) { ?>
<div class="pretext">
<p><?php echo $preText; ?></p>
</div>
<?php } ?>
<?php echo modCBModeratorHelper::getPlugins( $params, 'almostStart' ); ?>
<?php if ( modCBModeratorHelper::getPlugins( $params, 'beforeLinks' ) || $showBanned || $showImageApproval || $showUserReports || $showUnbanRequests || $showUserApproval || $showPrivateMessages || $showConnectionRequests || modCBModeratorHelper::getPlugins( $params, 'afterLinks' ) ) { ?>
<ul class="m-0 unstyled list-unstyled cbModeratorLinks">
<?php echo modCBModeratorHelper::getPlugins( $params, 'beforeLinks', 'li' ); ?>
<?php if ( $showBanned ) { ?>
<li class="cbModeratorLink cbModeratorLinkBanned">
<a href="<?php echo $_CB_framework->userProfileUrl(); ?>"><?php echo ( $bannedStatus == 1 ? CBTxt::T( 'Profile Banned' ) : CBTxt::T( 'Unban Request Pending' ) ); ?></a>
</li>
<?php } ?>
<?php if ( $showImageApproval ) { ?>
<li class="cbModeratorLink cbModeratorLinkImageApproval">
<a href="<?php echo $_CB_framework->viewUrl( 'moderateimages' ); ?>"><?php echo ( $imageApprovalCount == 1 ? CBTxt::T( 'COUNT_IMAGE_APPROVAL', '[count] Image Approval', array( '[count]' => $imageApprovalCount ) ) : CBTxt::T( 'COUNT_IMAGE_APPROVALS', '[count] Image Approvals', array( '[count]' => $imageApprovalCount ) ) ); ?></a>
</li>
<?php } ?>
<?php if ( $showUserReports ) { ?>
<li class="cbModeratorLink cbModeratorLinkUserReports">
<a href="<?php echo $_CB_framework->viewUrl( 'moderatereports' ); ?>"><?php echo ( $userReportsCount == 1 ? CBTxt::T( 'COUNT_PROFILE_REPORT', '[count] Profile Report', array( '[count]' => $userReportsCount ) ) : CBTxt::T( 'COUNT_PROFILE_REPORTS', '[count] Profile Reports', array( '[count]' => $userReportsCount ) ) ); ?></a>
</li>
<?php } ?>
<?php if ( $showUnbanRequests ) { ?>
<li class="cbModeratorLink cbModeratorLinkUnbanRequests">
<a href="<?php echo $_CB_framework->viewUrl( 'moderatebans' ); ?>"><?php echo ( $unbanRequestCount == 1 ? CBTxt::T( 'COUNT_UNBAN_REQUEST', '[count] Unban Request', array( '[count]' => $unbanRequestCount ) ) : CBTxt::T( 'COUNT_UNBAN_REQUESTS', '[count] Unban Requests', array( '[count]' => $unbanRequestCount ) ) ); ?></a>
</li>
<?php } ?>
<?php if ( $showUserApproval ) { ?>
<li class="cbModeratorLink cbModeratorLinkUserApproval">
<a href="<?php echo $_CB_framework->viewUrl( 'pendingapprovaluser' ); ?>"><?php echo ( $userApprovalCount == 1 ? CBTxt::T( 'COUNT_USER_APPROVAL', '[count] User Approval', array( '[count]' => $userApprovalCount ) ) : CBTxt::T( 'COUNT_USER_APPROVALS', '[count] User Approvals', array( '[count]' => $userApprovalCount ) ) ); ?></a>
</li>
<?php } ?>
<?php if ( $showPrivateMessages ) { ?>
<li class="cbModeratorLink cbModeratorLinkPrivateMessages">
<a href="<?php echo $privateMessageURL; ?>"><?php echo ( $newMessageCount == 1 ? CBTxt::T( 'COUNT_PRIVATE_MESSAGE', '[count] Private Message', array( '[count]' => $newMessageCount ) ) : CBTxt::T( 'COUNT_PRIVATE_MESSAGES', '[count] Private Messages', array( '[count]' => $newMessageCount ) ) ); ?></a>
</li>
<?php } ?>
<?php if ( $showConnectionRequests ) { ?>
<li class="cbModeratorLink cbModeratorLinkConnectionRequests">
<a href="<?php echo $_CB_framework->viewUrl( 'manageconnections' ); ?>"><?php echo ( $newConnectionRequests == 1 ? CBTxt::T( 'COUNT_CONNECTION_REQUEST', '[count] Connection Request', array( '[count]' => $newConnectionRequests ) ) : CBTxt::T( 'COUNT_CONNECTION_REQUESTS', '[count] Connection Requests', array( '[count]' => $newConnectionRequests ) ) ); ?></a>
</li>
<?php } ?>
<?php echo modCBModeratorHelper::getPlugins( $params, 'afterLinks', 'li' ); ?>
</ul>
<?php } ?>
<?php echo modCBModeratorHelper::getPlugins( $params, 'almostEnd' ); ?>
<?php if ( $postText ) { ?>
<div class="posttext">
<p><?php echo $postText; ?></p>
</div>
<?php } ?>
<?php echo modCBModeratorHelper::getPlugins( $params, 'end' ); ?>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

View File

@@ -0,0 +1,42 @@
<?php
/**
* Community Builder (TM)
* @version $Id: $
* @package CommunityBuilder
* @copyright (C) 2004-2025 www.joomlapolis.com / Lightning MultiCom SA - and its licensors, all rights reserved
* @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU/GPL version 2
*/
if ( ! ( defined( '_VALID_CB' ) || defined( '_JEXEC' ) || defined( '_VALID_MOS' ) ) ) { die( 'Direct Access to this location is not allowed.' ); }
?>
<?php echo modCBOnlineHelper::getPlugins( $params, 'start' ); ?>
<?php if ( $preText ) { ?>
<div class="pretext">
<p><?php echo $preText; ?></p>
</div>
<?php } ?>
<?php echo modCBOnlineHelper::getPlugins( $params, 'beforeUsers' ); ?>
<?php if ( modCBOnlineHelper::getPlugins( $params, 'beforeLinks' ) || $cbUsers || modCBOnlineHelper::getPlugins( $params, 'afterUsers' ) ) { ?>
<ul class="m-0 unstyled list-unstyled cbOnlineUsers">
<?php echo modCBOnlineHelper::getPlugins( $params, 'beforeLinks' ); ?>
<?php foreach ( $cbUsers as $cbUser ) { ?>
<li class="cbOnlineUser">
<?php
if ( $params->get( 'usertext' ) ) {
echo $cbUser->replaceUserVars( $params->get( 'usertext' ) );
} else {
echo $cbUser->getField( 'formatname', null, 'html', 'none', 'list', 0, true );
}
?>
</li>
<?php } ?>
<?php echo modCBOnlineHelper::getPlugins( $params, 'afterUsers' ); ?>
</ul>
<?php } ?>
<?php echo modCBOnlineHelper::getPlugins( $params, 'almostEnd' ); ?>
<?php if ( $postText ) { ?>
<div class="posttext">
<p><?php echo $postText; ?></p>
</div>
<?php } ?>
<?php echo modCBOnlineHelper::getPlugins( $params, 'end' ); ?>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

View File

@@ -0,0 +1,122 @@
<?php
/**
* @package DPCalendar
* @copyright Copyright (C) 2014 Digital Peak GmbH. <https://www.digital-peak.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
\defined('_JEXEC') or die();
use Joomla\CMS\HTML\HTMLHelper;
if (!$events) {
return;
}
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_YEARS');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_MONTHS');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_WEEKS');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_DAYS');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_HOURS');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_MINUTES');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_SECONDS');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_YEAR');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_MONTH');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_WEEK');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_DAY');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_HOUR');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_MINUTE');
$translator->translateJS('MOD_DPCALENDAR_COUNTER_LABEL_SECOND');
$translator->translateJS('COM_DPCALENDAR_CLOSE');
$document->loadStyleFile('default.css', 'mod_dpcalendar_counter');
$document->loadScriptFile('default.js', 'mod_dpcalendar_counter');
$document->addStyle($params->get('custom_css', ''));
?>
<div class="mod-dpcalendar-counter mod-dpcalendar-counter-<?php echo $module->id; ?>">
<div class="mod-dpcalendar-counter__custom-text">
<?php echo HTMLHelper::_('content.prepare', $translator->translate($params->get('textbefore', ''))); ?>
</div>
<div class="mod-dpcalendar-counter__events">
<?php foreach ($events as $event) { ?>
<div class="mod-dpcalendar-counter__event"
data-date="<?php echo $dateHelper->getDate($event->start_date, $event->all_day)->format('Y-m-d H:i:s'); ?>"
data-modal="<?php echo $params->get('show_as_popup'); ?>"
data-counting="<?php echo !$params->get('disable_counting'); ?>">
<div class="mod-dpcalendar-counter__upcoming">
<div class="mod-dpcalendar-counter__intro-text">
<?php echo $translator->translate('MOD_DPCALENDAR_COUNTER_SOON_OUTPUT'); ?>
</div>
<?php if ($params->get('show_field_year', 1)) { ?>
<span class="mod-dpcalendar-counter__year dp-counter-block">
<span class="dp-counter-block__number"></span>
<span class="dp-counter-block__content"></span>
</span>
<?php } ?>
<?php if ($params->get('show_field_month', 1)) { ?>
<span class="mod-dpcalendar-counter__month dp-counter-block">
<span class="dp-counter-block__number"></span>
<span class="dp-counter-block__content"></span>
</span>
<?php } ?>
<?php if ($params->get('show_field_week', 1)) { ?>
<span class="mod-dpcalendar-counter__week dp-counter-block">
<span class="dp-counter-block__number"></span>
<span class="dp-counter-block__content"></span>
</span>
<?php } ?>
<?php if ($params->get('show_field_day', 1)) { ?>
<span class="mod-dpcalendar-counter__day dp-counter-block">
<span class="dp-counter-block__number"></span>
<span class="dp-counter-block__content"></span>
</span>
<?php } ?>
<?php if ($params->get('show_field_hour', 1)) { ?>
<span class="mod-dpcalendar-counter__hour dp-counter-block">
<span class="dp-counter-block__number"></span>
<span class="dp-counter-block__content"></span>
</span>
<?php } ?>
<span class="mod-dpcalendar-counter__minute dp-counter-block">
<span class="dp-counter-block__number"></span>
<span class="dp-counter-block__content"></span>
</span>
<span class="mod-dpcalendar-counter__second dp-counter-block">
<span class="dp-counter-block__number"></span>
<span class="dp-counter-block__content"></span>
</span>
</div>
<div class="mod-dpcalendar-counter__ongoing">
<div class="mod-dpcalendar-counter__intro-text">
<?php echo $translator->translate('MOD_DPCALENDAR_COUNTER_ONGOING_OUTPUT'); ?>
</div>
<a href="<?php echo $router->getEventRoute($event->id, $event->catid); ?>" class="mod-dpcalendar-counter__link dp-link">
<?php echo $event->title; ?>
</a>
<?php if ($event->images->image_intro) { ?>
<div class="mod-dpcalendar-upcoming-counter__image">
<figure class="dp-figure">
<a href="<?php echo $router->getEventRoute($event->id, $event->catid); ?>" class="mod-dpcalendar-counter__link dp-link">
<img class="dp-image" src="<?php echo $event->images->image_intro; ?>"
alt="<?php echo $event->images->image_intro_alt; ?>"
loading="lazy" <?php echo $event->images->image_intro_dimensions; ?>>
</a>
<?php if ($event->images->image_intro_caption) { ?>
<figcaption class="dp-figure__caption"><?php echo $event->images->image_intro_caption; ?></figcaption>
<?php } ?>
</figure>
</div>
<?php } ?>
<?php if ($event->truncatedDescription) { ?>
<div class="mod-dpcalendar-counter__description">
<?php echo $event->truncatedDescription; ?>
</div>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
<div class="mod-dpcalendar-counter__custom-text">
<?php echo HTMLHelper::_('content.prepare', $translator->translate($params->get('textafter', ''))); ?>
</div>
</div>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

View File

@@ -0,0 +1,37 @@
<?php
/**
* @package DPCalendar
* @copyright Copyright (C) 2014 Digital Peak GmbH. <https://www.digital-peak.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
\defined('_JEXEC') or die();
use Joomla\CMS\HTML\HTMLHelper;
$document->loadStyleFile('default.css', 'mod_dpcalendar_map');
$document->loadScriptFile('views/map/default.js');
$document->addStyle($params->get('custom_css', ''));
$layoutHelper->renderLayout('block.map', $displayData);
$translator->translateJS('COM_DPCALENDAR_FIELD_CONFIG_EVENT_LABEL_NO_EVENT_TEXT');
?>
<div class="mod-dpcalendar-map mod-dpcalendar-map-<?php echo $module->id; ?> dp-search-map"
data-popup="<?php echo $params->get('show_as_popup'); ?>">
<div class="mod-dpcalendar-map__custom-text">
<?php echo HTMLHelper::_('content.prepare', $translator->translate($params->get('textbefore', ''))); ?>
</div>
<?php echo $layoutHelper->renderLayout('block.loader', $displayData); ?>
<?php echo $layoutHelper->renderLayout('block.filter', $displayData); ?>
<div class="mod-dpcalendar-map__map dp-map"
style="width: <?php echo $params->get('width', '100%'); ?>; height: <?php echo $params->get('height', '300px'); ?>"
data-zoom="<?php echo $params->get('zoom', 4); ?>"
data-latitude="<?php echo $params->get('lat', 47); ?>"
data-longitude="<?php echo $params->get('long', 4); ?>"
data-ask-consent="<?php echo $params->get('map_ask_consent'); ?>">
</div>
<div class="mod-dpcalendar-map__custom-text">
<?php echo HTMLHelper::_('content.prepare', $translator->translate($params->get('textafter', ''))); ?>
</div>
</div>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

View File

@@ -0,0 +1,204 @@
<?php
/**
* @package DPCalendar
* @copyright Copyright (C) 2018 Digital Peak GmbH. <https://www.digital-peak.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
\defined('_JEXEC') or die();
use DigitalPeak\Component\DPCalendar\Administrator\Helper\DPCalendarHelper;
use DigitalPeak\Component\DPCalendar\Administrator\Router\Router;;
use Joomla\Utilities\ArrayHelper;
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_ALL_DAY');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_MONTH');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_WEEK');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_DAY');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_LIST');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_UNTIL');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_PAST');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_TODAY');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_TOMORROW');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_THIS_WEEK');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_NEXT_WEEK');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_THIS_MONTH');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_NEXT_MONTH');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_FUTURE');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_WEEK');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_VIEW_TEXTS_MORE');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_TOOLBAR_NEXT');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_TOOLBAR_PREVIOUS');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_TOOLBAR_TODAY');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_SHOW_DATEPICKER');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_TOOLBAR_PRINT');
$translator->translateJS('COM_DPCALENDAR_VIEW_CALENDAR_TOOLBAR_ADD');
$translator->translateJS('JCANCEL');
$translator->translateJS('COM_DPCALENDAR_CLOSE');
$translator->translateJS('COM_DPCALENDAR_PREVIOUS');
$translator->translateJS('COM_DPCALENDAR_NEXT');
$translator->translateJS('COM_DPCALENDAR_FIELD_CAPACITY_UNLIMITED');
$document->addScriptOptions('calendar.names', $dateHelper->getNames());
$document->addScriptOptions('timezone', $dateHelper->getDate()->getTimezone()->getName());
$document->addScriptOptions('itemid', $app->getInput()->getInt('Itemid', 0));
// The options which will be passed to the js library
$options = [];
$options['requestUrlRoot'] = 'view=events&limit=0&format=raw&module_id=' . $module->id . '&Itemid=' . $app->getInput()->getInt('Itemid', 0);
$options['calendarIds'] = [implode(',', $ids)];
// Set the default view
$options['initialView'] = $params->get('default_view', 'month');
// Some general calendar options
$options['weekNumbers'] = (bool)$params->get('week_numbers');
$options['weekends'] = (bool)$params->get('weekend', 1);
$options['fixedWeekCount'] = (bool)$params->get('fixed_week_count', 1);
$bd = $params->get('business_hours_days', []);
if ($bd && !(is_countable($bd) ? count($bd) : 0 === 1 && !$bd[0])) {
$options['businessHours'] = [
'startTime' => $params->get('business_hours_start', ''),
'endTime' => $params->get('business_hours_end', ''),
'daysOfWeek' => $params->get('business_hours_days', [])
];
}
$options['firstDay'] = (int)$params->get('weekstart', 1);
$options['hiddenDays'] = ArrayHelper::toInteger($params->get('hidden_days', []));
$options['scrollTime'] = $params->get('first_hour', 6) === 'now' ? 'now' : $params->get('first_hour', 6) . ':00:00';
$options['weekNumberCalculation'] = 'ISO';
$options['displayEventEnd'] = true;
$options['navLinks'] = true;
$max = $params->get('max_time', 24);
if (is_numeric($max)) {
$max .= ':00:00';
}
$options['slotMaxTime'] = $max;
$min = $params->get('min_time', 0);
if (is_numeric($min)) {
$min .= ':00:00';
}
$options['slotMinTime'] = $min;
$options['nowIndicator'] = (bool)$params->get('current_time_indicator', 1);
$options['displayEventTime'] = $params->get('compact_events', 2) != 2;
if ($params->get('event_limit', '') != '-1') {
$options['dayMaxEventRows'] = $params->get('event_limit', '') == '' ? 2 : $params->get('event_limit', '') + 1;
}
// Set the height
if ($params->get('calendar_height', 0) > 0) {
$options['contentHeight'] = (int)$params->get('calendar_height', 0);
} else {
$options['height'] = 'auto';
}
$options['slotEventOverlap'] = (bool)$params->get('overlap_events', 1);
// Set up the header
$options['headerToolbar'] = ['left' => [], 'center' => [], 'right' => []];
if ($params->get('header_show_navigation', 1)) {
$options['headerToolbar']['left'][] = 'prev';
$options['headerToolbar']['left'][] = 'next';
}
if ($params->get('header_show_today', 0)) {
$options['headerToolbar']['left'][] = 'today';
}
if ($params->get('header_show_datepicker', 0)) {
$options['headerToolbar']['left'][] = 'datepicker';
}
if ($params->get('header_show_create', 1) && DPCalendarHelper::canCreateEvent()) {
$options['headerToolbar']['left'][] = 'add';
}
if ($params->get('header_show_title', 1)) {
$options['headerToolbar']['center'][] = 'title';
}
if ($params->get('header_show_month', 1)) {
$options['headerToolbar']['right'][] = 'month';
}
if ($params->get('header_show_week', 1)) {
$options['headerToolbar']['right'][] = 'week';
}
if ($params->get('header_show_day', 1)) {
$options['headerToolbar']['right'][] = 'day';
} else {
$options['navLinks'] = false;
}
if ($params->get('header_show_list', 1)) {
$options['headerToolbar']['right'][] = 'list';
}
$options['headerToolbar']['left'] = implode(',', $options['headerToolbar']['left']);
$options['headerToolbar']['center'] = implode(',', $options['headerToolbar']['center']);
$options['headerToolbar']['right'] = implode(',', $options['headerToolbar']['right']);
$resourceViews = $params->get('calendar_resource_views');
if (!DPCalendarHelper::isFree() && $resourceViews && $resources) {
$options['resources'] = $resources;
$options['resourceViews'] = $resourceViews;
$options['datesAboveResources'] = true;
}
// Set up the views
$options['views'] = [];
$options['views']['month'] = [
'titleFormat' => $dateHelper->convertPHPDateToJS($params->get('titleformat_month', 'F Y')),
'eventTimeFormat' => $dateHelper->convertPHPDateToJS($params->get('timeformat_month', 'H:i')),
'dayHeaderFormat' => $dateHelper->convertPHPDateToJS($params->get('columnformat_month', 'D'))
];
$options['views']['week'] = [
'titleFormat' => $dateHelper->convertPHPDateToJS($params->get('titleformat_week', 'M j Y')),
'eventTimeFormat' => $dateHelper->convertPHPDateToJS($params->get('timeformat_week', 'H:i')),
'dayHeaderFormat' => $dateHelper->convertPHPDateToJS($params->get('columnformat_week', 'D n/j')),
'slotDuration' => $dateHelper->minutesToDuration((int)$params->get('agenda_slot_minutes', 30)),
'slotLabelInterval' => $dateHelper->minutesToDuration((int)$params->get('agenda_slot_minutes', 30)),
'slotLabelFormat' => $dateHelper->convertPHPDateToJS($params->get('axisformat_week', 'H:i'))
];
$options['views']['day'] = [
'titleFormat' => $dateHelper->convertPHPDateToJS($params->get('titleformat_day', 'F j Y')),
'eventTimeFormat' => $dateHelper->convertPHPDateToJS($params->get('timeformat_day', 'H:i')),
'dayHeaderFormat' => $dateHelper->convertPHPDateToJS($params->get('columnformat_day', 'l')),
'slotDuration' => $dateHelper->minutesToDuration((int)$params->get('agenda_slot_minutes', 30)),
'slotLabelInterval' => $dateHelper->minutesToDuration((int)$params->get('agenda_slot_minutes', 30)),
'slotLabelFormat' => $dateHelper->convertPHPDateToJS($params->get('axisformat_day', 'H:i'))
];
$options['views']['list'] = [
'titleFormat' => $dateHelper->convertPHPDateToJS($params->get('titleformat_list', 'M j Y')),
'eventTimeFormat' => $dateHelper->convertPHPDateToJS($params->get('timeformat_list', 'H:i')),
'dayHeaderFormat' => $dateHelper->convertPHPDateToJS($params->get('columnformat_list', 'D')),
'listDayFormat' => $dateHelper->convertPHPDateToJS($params->get('dayformat_list', 'l')),
'listDaySideFormat' => $dateHelper->convertPHPDateToJS($params->get('dateformat_list', 'F j, Y')),
'duration' => ['days' => (int)$params->get('list_range', 30)],
'noEventsContent' => $translator->translate('COM_DPCALENDAR_ERROR_EVENT_NOT_FOUND', true)
];
// Some DPCalendar specific options
$options['show_event_as_popup'] = $params->get('show_event_as_popup');
$options['popupWidth'] = $params->get('popup_width');
$options['popupHeight'] = $params->get('popup_height');
$options['show_event_tooltip'] = $params->get('show_event_tooltip', 1);
$options['show_map'] = $params->get('show_map', 1);
$options['event_create_form'] = (int)$params->get('event_create_form', 1);
$options['screen_size_list_view'] = $params->get('screen_size_list_view', 500);
$options['use_hash'] = false;
if (DPCalendarHelper::canCreateEvent()) {
$router = new Router();
$input = $app->getInput();
$returnPage = $input->getInt('Itemid', 0) ? 'index.php?Itemid=' . $input->getInt('Itemid', 0) : null;
$options['event_create_url'] = $router->getEventFormRoute('0', $returnPage, null, false);
}
// Set the actual date
$now = DPCalendarHelper::getDate($params->get('start_date'));
$options['year'] = $now->format('Y', true);
$options['month'] = $now->format('m', true);
$options['date'] = $now->format('d', true);
$document->addScriptOptions('module.mini.' . $module->id . '.options', $options);

View File

@@ -0,0 +1,50 @@
<?php
/**
* @package DPCalendar
* @copyright Copyright (C) 2014 Digital Peak GmbH. <https://www.digital-peak.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
\defined('_JEXEC') or die();
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\HTML\HTMLHelper;
if ($params->get('show_map', 1)) {
$layoutHelper->renderLayout('block.map', $displayData);
}
$document->loadScriptFile('views/calendar/default.js');
$document->loadScriptFile('views/calendar/default.js');
$document->loadStyleFile('default.css', 'mod_dpcalendar_mini');
$document->addStyle($params->get('custom_css', ''));
require ModuleHelper::getLayoutPath('mod_dpcalendar_mini', '_scripts');
$compact = $params->get('compact_events', 2) == 1 ? 'compact' : 'expanded';
?>
<div class="mod-dpcalendar-mini mod-dpcalendar-mini_<?php echo $compact; ?> mod-dpcalendar-mini-<?php echo $module->id; ?>">
<div class="mod-dpcalendar-mini__loader">
<?php echo $layoutHelper->renderLayout('block.loader', $displayData); ?>
</div>
<div class="mod-dpcalendar-mini__custom-text">
<?php echo HTMLHelper::_('content.prepare', $translator->translate($params->get('textbefore', ''))); ?>
</div>
<div class="mod-dpcalendar-mini__calendar dp-calendar"
data-options="DPCalendar.module.mini.<?php echo $module->id; ?>.options"></div>
<?php require ModuleHelper::getLayoutPath('mod_dpcalendar_mini', 'default_map'); ?>
<?php require ModuleHelper::getLayoutPath('mod_dpcalendar_mini', 'default_quickadd'); ?>
<?php require ModuleHelper::getLayoutPath('mod_dpcalendar_mini', 'default_icons'); ?>
<div class="dp-filter">
<form class="dp-form">
<?php foreach ($ids as $id) { ?>
<input type="hidden" name="filter[calendars][]" value="<?php echo $id; ?>">
<?php } ?>
<input type="hidden" name="list[start-date]">
<input type="hidden" name="list[end-date]">
</form>
</div>
<div class="mod-dpcalendar-mini__custom-text">
<?php echo HTMLHelper::_('content.prepare', $translator->translate($params->get('textafter', ''))); ?>
</div>
</div>

View File

@@ -0,0 +1,22 @@
<?php
/**
* @package DPCalendar
* @copyright Copyright (C) 2020 Digital Peak GmbH. <https://www.digital-peak.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
\defined('_JEXEC') or die();
use DigitalPeak\Component\DPCalendar\Administrator\HTML\Block\Icon;
?>
<div class="mod-dpcalendar-mini__icons">
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::DELETE]); ?>
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::EDIT]); ?>
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::PLUS]); ?>
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::PRINTING]); ?>
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::CALENDAR]); ?>
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::NEXT]); ?>
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::BACK]); ?>
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::USERS]); ?>
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::MONEY]); ?>
</div>

View File

@@ -0,0 +1,20 @@
<?php
/**
* @package DPCalendar
* @copyright Copyright (C) 2019 Digital Peak GmbH. <https://www.digital-peak.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
\defined('_JEXEC') or die();
if (!$params->get('show_map', 0)) {
return;
}
?>
<div class="mod-dpcalendar-mini__map dp-map"
style="width: <?php echo $params->get('map_width', '100%'); ?>; height: <?php echo $params->get('map_height', '350px'); ?>"
data-zoom="<?php echo $params->get('map_zoom', 4); ?>"
data-latitude="<?php echo $params->get('map_lat', 47); ?>"
data-longitude="<?php echo $params->get('map_long', 4); ?>"
data-ask-consent="<?php echo $params->get('map_ask_consent'); ?>">
</div>

View File

@@ -0,0 +1,47 @@
<?php
/**
* @package DPCalendar
* @copyright Copyright (C) 2023 Digital Peak GmbH. <https://www.digital-peak.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
\defined('_JEXEC') or die();
use DigitalPeak\Component\DPCalendar\Administrator\Helper\DPCalendarHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\HTML\HTMLHelper;
if (!DPCalendarHelper::canCreateEvent()) {
return;
}
?>
<div class="mod-dpcalendar-mini__quickadd dp-quickadd">
<form action="<?php echo $router->getEventFormRoute(0, Uri::getInstance()->toString()); ?>" method="post" class="dp-form form-validate">
<?php echo $quickaddForm->renderField('start_date'); ?>
<?php echo $quickaddForm->renderField('end_date'); ?>
<?php echo $quickaddForm->renderField('title'); ?>
<?php echo $quickaddForm->renderField('catid'); ?>
<?php echo $quickaddForm->renderField('color'); ?>
<input type="hidden" name="task" class="dp-input dp-input-hidden">
<input type="hidden" name="urlhash" class="dp-input dp-input-hidden">
<input type="hidden" name="jform[capacity]" value="0" class="dp-input dp-input-hidden">
<?php if ($params->get('event_create_form', 1) == '1' || $params->get('event_create_form', 1) == '3') { ?>
<input type="hidden" name="jform[all_day]" value="0" class="dp-input dp-input-hidden">
<?php } ?>
<input type="hidden" name="layout" value="edit" class="dp-input dp-input-hidden">
<input type="hidden" name="jform[location_ids][]" class="dp-input dp-input-hidden">
<input type="hidden" name="jform[rooms][]" class="dp-input dp-input-hidden">
<?php echo HTMLHelper::_('form.token'); ?>
<div class="dp-quickadd__buttons">
<button type="button" class="dp-button dp-quickadd__button-submit">
<?php echo $translator->translate('JSAVE'); ?>
</button>
<button type="button" class="dp-button dp-quickadd__button-edit">
<?php echo $translator->translate('JACTION_EDIT'); ?>
</button>
<button type="button" class="dp-button dp-quickadd__button-cancel">
<?php echo $translator->translate('JCANCEL'); ?>
</button>
</div>
</form>
</div>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

View File

@@ -0,0 +1,22 @@
<?php
/**
* @package DPCalendar
* @copyright Copyright (C) 2018 Digital Peak GmbH. <https://www.digital-peak.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
\defined('_JEXEC') or die();
// Load the required modal JS libraries
if ($params->get('show_as_popup') || $params->get('show_map')) {
$document->loadScriptFile('default.js', 'mod_dpcalendar_upcoming');
$translator->translateJS('COM_DPCALENDAR_CLOSE');
}
if ($params->get('show_map')) {
$layoutHelper->renderLayout('block.map', $displayData);
}
// Load the stylesheet
$document->loadStyleFile(str_replace('_:', '', (string)$params->get('layout', 'default')) . '.css', 'mod_dpcalendar_upcoming');
$document->addStyle($params->get('custom_css', ''));

View File

@@ -0,0 +1,172 @@
<?php
/**
* @package DPCalendar
* @copyright Copyright (C) 2014 Digital Peak GmbH. <https://www.digital-peak.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
\defined('_JEXEC') or die();
use DigitalPeak\Component\DPCalendar\Administrator\Helper\Booking;
use DigitalPeak\Component\DPCalendar\Administrator\Helper\DPCalendarHelper;
use DigitalPeak\Component\DPCalendar\Administrator\HTML\Block\Icon;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\HTML\HTMLHelper;
if (!$events) {
echo $translator->translate($params->get('no_events_text', 'MOD_DPCALENDAR_UPCOMING_NO_EVENT_TEXT'));
return;
}
require ModuleHelper::getLayoutPath('mod_dpcalendar_upcoming', '_scripts');
?>
<div class="mod-dpcalendar-upcoming mod-dpcalendar-upcoming-default mod-dpcalendar-upcoming-<?php echo $module->id; ?> dp-locations"
data-popup="<?php echo $params->get('show_as_popup', 0); ?>">
<div class="mod-dpcalendar-upcoming-default__custom-text">
<?php echo HTMLHelper::_('content.prepare', $translator->translate($params->get('textbefore', ''))); ?>
</div>
<div class="mod-dpcalendar-upcoming-default__events">
<?php foreach ($groupedEvents as $groupHeading => $events) { ?>
<?php if ($groupHeading) { ?>
<div class="mod-dpcalendar-upcoming-default__group">
<p class="mod-dpcalendar-upcoming-default__heading dp-group-heading"><?php echo $groupHeading; ?></p>
<?php } ?>
<?php foreach ($events as $event) { ?>
<?php $displayData['event'] = $event; ?>
<?php $startDate = $dateHelper->getDate($event->start_date, $event->all_day); ?>
<div class="mod-dpcalendar-upcoming-default__event dp-event dp-event_<?php echo $event->ongoing_start_date ? ($event->ongoing_end_date ? 'started' : 'finished') : 'future'; ?>">
<?php echo $layoutHelper->renderLayout('block.flatcalendar', ['date' => $startDate, 'color' => $event->color]); ?>
<div class="mod-dpcalendar-upcoming-default__information">
<?php if ($event->state == 3) { ?>
<span class="dp-event_canceled">[<?php echo $translator->translate('MOD_DPCALENDAR_UPCOMING_CANCELED'); ?>]</span>
<?php } ?>
<a href="<?php echo $event->realUrl; ?>" class="dp-event-url dp-link"><?php echo $event->title; ?></a>
<?php if ($params->get('show_display_events') && $event->displayEvent->afterDisplayTitle) { ?>
<div class="dp-event-display-after-title"><?php echo $event->displayEvent->afterDisplayTitle; ?></div>
<?php } ?>
<?php if (($params->get('show_location') || $params->get('show_map')) && isset($event->locations) && $event->locations) { ?>
<div class="mod-dpcalendar-upcoming-default__location">
<?php if ($params->get('show_location')) { ?>
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::LOCATION]); ?>
<?php } ?>
<?php foreach ($event->locations as $location) { ?>
<div class="dp-location<?php echo $params->get('show_location') ? '' : ' dp-location_hidden'; ?>">
<div class="dp-location__details"
data-latitude="<?php echo $location->latitude; ?>"
data-longitude="<?php echo $location->longitude; ?>"
data-title="<?php echo $location->title; ?>"
data-color="<?php echo $event->color; ?>"></div>
<?php if ($params->get('show_location')) { ?>
<a href="<?php echo $router->getLocationRoute($location); ?>" class="dp-location__url dp-link">
<span class="dp-location__title"><?php echo $location->title; ?></span>
<?php if (!empty($event->roomTitles[$location->id])) { ?>
<span class="dp-location__rooms">[<?php echo implode(', ', $event->roomTitles[$location->id]); ?>]</span>
<?php } ?>
</a>
<?php } ?>
<div class="dp-location__description">
<?php echo $layoutHelper->renderLayout('event.tooltip', $displayData); ?>
</div>
</div>
<?php } ?>
</div>
<?php } ?>
<div class="mod-dpcalendar-upcoming-default__date">
<?php echo $layoutHelper->renderLayout(
'block.icon',
['icon' => Icon::CLOCK, 'title' => $translator->translate('MOD_DPCALENDAR_UPCOMING_DATE')]
); ?>
<?php echo $dateHelper->getDateStringFromEvent($event, $params->get('date_format'), $params->get('time_format')); ?>
</div>
<?php if ($event->rrule) { ?>
<div class="mod-dpcalendar-upcoming-default__rrule">
<?php echo $layoutHelper->renderLayout(
'block.icon',
['icon' => Icon::RECURRING, 'title' => $translator->translate('MOD_DPCALENDAR_UPCOMING_SERIES')]
); ?>
<?php echo nl2br((string) $dateHelper->transformRRuleToString($event->rrule, $event->start_date, $event->exdates)); ?>
</div>
<?php } ?>
<?php if ($params->get('show_price') && $event->prices) { ?>
<?php foreach ($event->prices as $price) { ?>
<?php $discounted = Booking::getPriceWithDiscount($price->value, $event); ?>
<div class="mod-dpcalendar-upcoming-default__price dp-event-price">
<?php echo $layoutHelper->renderLayout(
'block.icon',
[
'icon' => Icon::MONEY,
'title' => $translator->translate('MOD_DPCALENDAR_UPCOMING_PRICES')
]
); ?>
<span class="dp-event-price__label">
<?php echo $price->label ?: $translator->translate('MOD_DPCALENDAR_UPCOMING_PRICES'); ?>
</span>
<span class="dp-event-price__regular<?php echo $discounted != $price->value ? ' dp-event-price__regular_has-discount' : ''; ?>">
<?php echo $price->value === '' ? '' : DPCalendarHelper::renderPrice($price->value); ?>
</span>
<?php if ($discounted != $price->value) { ?>
<span class="dp-event-price__discount"><?php echo DPCalendarHelper::renderPrice($discounted); ?></span>
<?php } ?>
<span class="dp-event-price__description">
<?php echo $price->description; ?>
</span>
</div>
<?php } ?>
<?php } ?>
</div>
<?php if ($params->get('show_image', 1) && $event->images->image_intro) { ?>
<div class="mod-dpcalendar-upcoming-default__image">
<figure class="dp-figure">
<a href="<?php echo $event->realUrl; ?>" class="dp-event-url dp-link">
<img class="dp-image" src="<?php echo $event->images->image_intro; ?>"
aria-label="<?php echo $event->images->image_intro_alt; ?>"
alt="<?php echo $event->images->image_intro_alt; ?>"
loading="lazy" <?php echo $event->images->image_intro_dimensions; ?>>
</a>
<?php if ($event->images->image_intro_caption) { ?>
<figcaption class="dp-figure__caption"><?php echo $event->images->image_intro_caption; ?></figcaption>
<?php } ?>
</figure>
</div>
<?php } ?>
<?php if ($params->get('show_booking', 1) && Booking::openForBooking($event)) { ?>
<a href="<?php echo $router->getBookingFormRouteFromEvent($event, $return, true, $moduleParams->get('default_menu_item', 0)); ?>"
class="dp-link dp-link_cta">
<?php echo $layoutHelper->renderLayout('block.icon', ['icon' => Icon::BOOK]); ?>
<span class="dp-link__text">
<?php echo $translator->translate('MOD_DPCALENDAR_UPCOMING_BOOK'); ?>
</span>
</a>
<?php } ?>
<?php if ($params->get('show_display_events') && $event->displayEvent->beforeDisplayContent) { ?>
<div class="dp-event-display-before-content"><?php echo $event->displayEvent->beforeDisplayContent; ?></div>
<?php } ?>
<div class="mod-dpcalendar-upcoming-default__description">
<?php echo $event->truncatedDescription; ?>
</div>
<?php if ($params->get('show_display_events') && $event->displayEvent->afterDisplayContent) { ?>
<div class="dp-event-display-after-content"><?php echo $event->displayEvent->afterDisplayContent; ?></div>
<?php } ?>
<?php $displayData['event'] = $event; ?>
<?php echo $layoutHelper->renderLayout('schema.event', $displayData); ?>
</div>
<?php } ?>
<?php if ($groupHeading) { ?>
</div>
<?php } ?>
<?php } ?>
</div>
<?php if ($params->get('show_map')) { ?>
<div class="mod-dpcalendar-upcoming-default__map dp-map"
style="width: <?php echo $params->get('map_width', '100%'); ?>; height: <?php echo $params->get('map_height', '350px'); ?>"
data-zoom="<?php echo $params->get('map_zoom', 4); ?>"
data-latitude="<?php echo $params->get('map_lat', 47); ?>"
data-longitude="<?php echo $params->get('map_long', 4); ?>"
data-ask-consent="<?php echo $params->get('map_ask_consent'); ?>">
</div>
<?php } ?>
<div class="mod-dpcalendar-upcoming-default__custom-text">
<?php echo HTMLHelper::_('content.prepare', $translator->translate($params->get('textafter', ''))); ?>
</div>
</div>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

View File

@@ -9,6 +9,11 @@
defined('_JEXEC') or die; defined('_JEXEC') or die;
// One-time migration from MokoCassiopeia (runs once, creates .migrated marker)
if (!file_exists(__DIR__ . '/.migrated')) {
require_once __DIR__ . '/helper/migrate.php';
}
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
@@ -92,8 +97,8 @@ if ($params_favicon_source) {
} }
} }
} }
$faviconOutputDir = JPATH_ROOT . '/images/favicons'; $faviconOutputDir = JPATH_ROOT . '/media/templates/site/' . $this->template . '/images/favicons';
$faviconUrlBase = Uri::root(true) . '/images/favicons'; $faviconUrlBase = Uri::root(true) . '/media/templates/site/' . $this->template . '/images/favicons';
if (MokoFaviconHelper::generate($faviconSourceAbs, $faviconOutputDir)) { if (MokoFaviconHelper::generate($faviconSourceAbs, $faviconOutputDir)) {
$faviconHeadTags = MokoFaviconHelper::getHeadTags($faviconUrlBase); $faviconHeadTags = MokoFaviconHelper::getHeadTags($faviconUrlBase);

View File

@@ -176,7 +176,7 @@ TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_LABEL="Block Colour System (top-a / top-b / b
TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand colour palette for modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions. Colours assigned by <code>:nth-child()</code> order — no classes needed.<br><br><strong>Slot palette</strong><br><code>--block-color-1</code> / <code>--block-text-1</code> — 1st module<br><code>--block-color-2</code> / <code>--block-text-2</code> — 2nd module<br><code>--block-color-3</code> / <code>--block-text-3</code> — 3rd module<br><code>--block-color-4</code> / <code>--block-text-4</code> — 4th module<br><br><strong>Named overrides</strong> (add an ID to the module HTML to bypass slot colour)<br><code>--block-highlight-bg</code> / <code>--block-highlight-text</code> — for <code>#block-highlight</code><br><code>--block-cta-bg</code> / <code>--block-cta-text</code> — for <code>#block-cta</code><br><code>--block-alert-bg</code> / <code>--block-alert-text</code> — for <code>#block-alert</code><br><br><strong>Priority:</strong> Named ID &gt; Slot colour. No <code>!important</code> needed — specificity handles it." TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand colour palette for modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions. Colours assigned by <code>:nth-child()</code> order — no classes needed.<br><br><strong>Slot palette</strong><br><code>--block-color-1</code> / <code>--block-text-1</code> — 1st module<br><code>--block-color-2</code> / <code>--block-text-2</code> — 2nd module<br><code>--block-color-3</code> / <code>--block-text-3</code> — 3rd module<br><code>--block-color-4</code> / <code>--block-text-4</code> — 4th module<br><br><strong>Named overrides</strong> (add an ID to the module HTML to bypass slot colour)<br><code>--block-highlight-bg</code> / <code>--block-highlight-text</code> — for <code>#block-highlight</code><br><code>--block-cta-bg</code> / <code>--block-cta-text</code> — for <code>#block-cta</code><br><code>--block-alert-bg</code> / <code>--block-alert-text</code> — for <code>#block-alert</code><br><br><strong>Priority:</strong> Named ID &gt; Slot colour. No <code>!important</code> needed — specificity handles it."
TPL_MOKOONYX_CSS_VARS_HEADER_LABEL="Header Background" TPL_MOKOONYX_CSS_VARS_HEADER_LABEL="Header Background"
TPL_MOKOONYX_CSS_VARS_HEADER_DESC="Controls the background of the topbar/header area.<br><code>--header-background-image</code> — CSS <code>background-image</code> value (default: built-in SVG pattern)<br><code>--header-background-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--header-background-repeat</code> — e.g. <code>repeat</code>, <code>no-repeat</code><br><code>--header-background-size</code> — e.g. <code>auto</code>, <code>cover</code>, <code>contain</code>" TPL_MOKOONYX_CSS_VARS_HEADER_DESC="Controls the background of the topbar/header area.<br><code>--header-background-color</code> — fallback colour when no image is set (default: <code>#adadad</code> light / <code>#1a1f2b</code> dark). Set <code>--header-background-image: none;</code> to use a solid colour.<br><code>--header-background-image</code> — CSS <code>background-image</code> value (default: built-in SVG pattern)<br><code>--header-background-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--header-background-repeat</code> — e.g. <code>repeat</code>, <code>no-repeat</code><br><code>--header-background-size</code> — e.g. <code>auto</code>, <code>cover</code>, <code>contain</code>"
TPL_MOKOONYX_CSS_VARS_CONTAINERS_LABEL="Container Backgrounds" TPL_MOKOONYX_CSS_VARS_CONTAINERS_LABEL="Container Backgrounds"
TPL_MOKOONYX_CSS_VARS_CONTAINERS_DESC="Each layout container has its own background variables. Replace <code>{pos}</code> with: <code>below-topbar</code>, <code>top-a</code>, <code>top-b</code>, <code>sidebar</code>, <code>bottom-a</code>, or <code>bottom-b</code>.<br><br><code>--container-{pos}-bg-image</code> — Background image (default: <code>none</code>)<br><code>--container-{pos}-bg-color</code> — Background colour (default: <code>transparent</code>)<br><code>--container-{pos}-bg-position</code> — Background position<br><code>--container-{pos}-bg-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--container-{pos}-bg-repeat</code> — Repeat behaviour<br><code>--container-{pos}-bg-size</code> — e.g. <code>cover</code>, <code>auto</code><br><code>--container-{pos}-border</code> — Border shorthand<br><code>--container-{pos}-border-radius</code> — Border radius<br><br>Also: <code>--container-toc-bg</code> / <code>--container-toc-color</code> for the TOC sidebar." TPL_MOKOONYX_CSS_VARS_CONTAINERS_DESC="Each layout container has its own background variables. Replace <code>{pos}</code> with: <code>below-topbar</code>, <code>top-a</code>, <code>top-b</code>, <code>sidebar</code>, <code>bottom-a</code>, or <code>bottom-b</code>.<br><br><code>--container-{pos}-bg-image</code> — Background image (default: <code>none</code>)<br><code>--container-{pos}-bg-color</code> — Background colour (default: <code>transparent</code>)<br><code>--container-{pos}-bg-position</code> — Background position<br><code>--container-{pos}-bg-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--container-{pos}-bg-repeat</code> — Repeat behaviour<br><code>--container-{pos}-bg-size</code> — e.g. <code>cover</code>, <code>auto</code><br><code>--container-{pos}-border</code> — Border shorthand<br><code>--container-{pos}-border-radius</code> — Border radius<br><br>Also: <code>--container-toc-bg</code> / <code>--container-toc-color</code> for the TOC sidebar."
@@ -267,6 +267,13 @@ TPL_MOKOONYX_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
TPL_MOKOONYX_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/mokoonyx/templates/theme-test.html</code>.</p>" TPL_MOKOONYX_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/mokoonyx/templates/theme-test.html</code>.</p>"
TPL_MOKOONYX_THEME_PREVIEW_FRAME="<iframe src='../templates/mokoonyx/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>" TPL_MOKOONYX_THEME_PREVIEW_FRAME="<iframe src='../templates/mokoonyx/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
; ===== Migration =====
TPL_MOKOONYX_MIGRATION_FIELDSET_LABEL="Migration"
TPL_MOKOONYX_MIGRATION_NOTE_LABEL="MokoCassiopeia Migration"
TPL_MOKOONYX_MIGRATION_NOTE_DESC="MokoOnyx automatically imports settings from MokoCassiopeia on first page load. If you need to re-run the migration, delete the file <code>templates/mokoonyx/.migrated</code> and visit any frontend page. Check <code>administrator/logs/mokoonyx_migrate.log.php</code> to confirm."
TPL_MOKOONYX_MIGRATION_RUN_LABEL="Re-run Migration"
TPL_MOKOONYX_MIGRATION_RUN_DESC="<strong>To re-run the migration:</strong> Delete <code>templates/mokoonyx/.migrated</code> via FTP or file manager, then visit any page on your site. The migration will run again automatically.<br><br><strong>To uninstall MokoCassiopeia:</strong> Go to <a href='index.php?option=com_installer&amp;view=manage&amp;filter[search]=mokocassiopeia'>Extensions → Manage</a>, find MokoCassiopeia, and click Uninstall."
; ===== Misc ===== ; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:" MOD_BREADCRUMBS_HERE="YOU ARE HERE:"

View File

@@ -28,4 +28,4 @@ TPL_MOKOONYX_POSITION_TOP_B="Top-b"
TPL_MOKOONYX_POSITION_TOPBAR="Top Bar" TPL_MOKOONYX_POSITION_TOPBAR="Top Bar"
TPL_MOKOONYX_POSITION_DRAWER_LEFT="Drawer-Left" TPL_MOKOONYX_POSITION_DRAWER_LEFT="Drawer-Left"
TPL_MOKOONYX_POSITION_DRAWER_RIGHT="Drawer-Right" TPL_MOKOONYX_POSITION_DRAWER_RIGHT="Drawer-Right"
TPL_MOKOONYX_XML_DESCRIPTION="<h3>MokoOnyx Template Description</h3> <p> <strong>MokoOnyx</strong> continues Joomlas 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/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code>, or <code>templates/mokoonyx/templates/dark.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoOnyx → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS &amp; JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokoonyx/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>" TPL_MOKOONYX_XML_DESCRIPTION="<p><img src=\"https://img.shields.io/gitea/v/release/MokoConsulting/MokoOnyx?gitea_url=https%3A%2F%2Fgit.mokoconsulting.tech&logo=gitea&logoColor=white&label=version\" alt=\"Version\" /> <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>MokoOnyx Template Description</h3> <p> <strong>MokoOnyx</strong> continues Joomlas 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/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code>, or <code>templates/mokoonyx/templates/dark.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoOnyx → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS &amp; JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokoonyx/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>"

View File

@@ -176,7 +176,7 @@ TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_LABEL="Block Color System (top-a / top-b / bo
TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand color palette for modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions. Colors assigned by <code>:nth-child()</code> order — no classes needed.<br><br><strong>Slot palette</strong><br><code>--block-color-1</code> / <code>--block-text-1</code> — 1st module<br><code>--block-color-2</code> / <code>--block-text-2</code> — 2nd module<br><code>--block-color-3</code> / <code>--block-text-3</code> — 3rd module<br><code>--block-color-4</code> / <code>--block-text-4</code> — 4th module<br><br><strong>Named overrides</strong> (add an ID to the module HTML to bypass slot color)<br><code>--block-highlight-bg</code> / <code>--block-highlight-text</code> — for <code>#block-highlight</code><br><code>--block-cta-bg</code> / <code>--block-cta-text</code> — for <code>#block-cta</code><br><code>--block-alert-bg</code> / <code>--block-alert-text</code> — for <code>#block-alert</code><br><br><strong>Priority:</strong> Named ID &gt; Slot color. No <code>!important</code> needed — specificity handles it." TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand color palette for modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions. Colors assigned by <code>:nth-child()</code> order — no classes needed.<br><br><strong>Slot palette</strong><br><code>--block-color-1</code> / <code>--block-text-1</code> — 1st module<br><code>--block-color-2</code> / <code>--block-text-2</code> — 2nd module<br><code>--block-color-3</code> / <code>--block-text-3</code> — 3rd module<br><code>--block-color-4</code> / <code>--block-text-4</code> — 4th module<br><br><strong>Named overrides</strong> (add an ID to the module HTML to bypass slot color)<br><code>--block-highlight-bg</code> / <code>--block-highlight-text</code> — for <code>#block-highlight</code><br><code>--block-cta-bg</code> / <code>--block-cta-text</code> — for <code>#block-cta</code><br><code>--block-alert-bg</code> / <code>--block-alert-text</code> — for <code>#block-alert</code><br><br><strong>Priority:</strong> Named ID &gt; Slot color. No <code>!important</code> needed — specificity handles it."
TPL_MOKOONYX_CSS_VARS_HEADER_LABEL="Header Background" TPL_MOKOONYX_CSS_VARS_HEADER_LABEL="Header Background"
TPL_MOKOONYX_CSS_VARS_HEADER_DESC="Controls the background of the topbar/header area.<br><code>--header-background-image</code> — CSS <code>background-image</code> value (default: built-in SVG pattern)<br><code>--header-background-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--header-background-repeat</code> — e.g. <code>repeat</code>, <code>no-repeat</code><br><code>--header-background-size</code> — e.g. <code>auto</code>, <code>cover</code>, <code>contain</code>" TPL_MOKOONYX_CSS_VARS_HEADER_DESC="Controls the background of the topbar/header area.<br><code>--header-background-color</code> — fallback color when no image is set (default: <code>#adadad</code> light / <code>#1a1f2b</code> dark). Set <code>--header-background-image: none;</code> to use a solid color.<br><code>--header-background-image</code> — CSS <code>background-image</code> value (default: built-in SVG pattern)<br><code>--header-background-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--header-background-repeat</code> — e.g. <code>repeat</code>, <code>no-repeat</code><br><code>--header-background-size</code> — e.g. <code>auto</code>, <code>cover</code>, <code>contain</code>"
TPL_MOKOONYX_CSS_VARS_CONTAINERS_LABEL="Container Backgrounds" TPL_MOKOONYX_CSS_VARS_CONTAINERS_LABEL="Container Backgrounds"
TPL_MOKOONYX_CSS_VARS_CONTAINERS_DESC="Each layout container has its own background variables. Replace <code>{pos}</code> with: <code>below-topbar</code>, <code>top-a</code>, <code>top-b</code>, <code>sidebar</code>, <code>bottom-a</code>, or <code>bottom-b</code>.<br><br><code>--container-{pos}-bg-image</code> — Background image (default: <code>none</code>)<br><code>--container-{pos}-bg-color</code> — Background color (default: <code>transparent</code>)<br><code>--container-{pos}-bg-position</code> — Background position<br><code>--container-{pos}-bg-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--container-{pos}-bg-repeat</code> — Repeat behavior<br><code>--container-{pos}-bg-size</code> — e.g. <code>cover</code>, <code>auto</code><br><code>--container-{pos}-border</code> — Border shorthand<br><code>--container-{pos}-border-radius</code> — Border radius<br><br>Also: <code>--container-toc-bg</code> / <code>--container-toc-color</code> for the TOC sidebar." TPL_MOKOONYX_CSS_VARS_CONTAINERS_DESC="Each layout container has its own background variables. Replace <code>{pos}</code> with: <code>below-topbar</code>, <code>top-a</code>, <code>top-b</code>, <code>sidebar</code>, <code>bottom-a</code>, or <code>bottom-b</code>.<br><br><code>--container-{pos}-bg-image</code> — Background image (default: <code>none</code>)<br><code>--container-{pos}-bg-color</code> — Background color (default: <code>transparent</code>)<br><code>--container-{pos}-bg-position</code> — Background position<br><code>--container-{pos}-bg-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--container-{pos}-bg-repeat</code> — Repeat behavior<br><code>--container-{pos}-bg-size</code> — e.g. <code>cover</code>, <code>auto</code><br><code>--container-{pos}-border</code> — Border shorthand<br><code>--container-{pos}-border-radius</code> — Border radius<br><br>Also: <code>--container-toc-bg</code> / <code>--container-toc-color</code> for the TOC sidebar."
@@ -267,6 +267,13 @@ TPL_MOKOONYX_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
TPL_MOKOONYX_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/mokoonyx/templates/theme-test.html</code>.</p>" TPL_MOKOONYX_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/mokoonyx/templates/theme-test.html</code>.</p>"
TPL_MOKOONYX_THEME_PREVIEW_FRAME="<iframe src='../templates/mokoonyx/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>" TPL_MOKOONYX_THEME_PREVIEW_FRAME="<iframe src='../templates/mokoonyx/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
; ===== Migration =====
TPL_MOKOONYX_MIGRATION_FIELDSET_LABEL="Migration"
TPL_MOKOONYX_MIGRATION_NOTE_LABEL="MokoCassiopeia Migration"
TPL_MOKOONYX_MIGRATION_NOTE_DESC="MokoOnyx automatically imports settings from MokoCassiopeia on first page load. If you need to re-run the migration, delete the file <code>templates/mokoonyx/.migrated</code> and visit any frontend page. Check <code>administrator/logs/mokoonyx_migrate.log.php</code> to confirm."
TPL_MOKOONYX_MIGRATION_RUN_LABEL="Re-run Migration"
TPL_MOKOONYX_MIGRATION_RUN_DESC="<strong>To re-run the migration:</strong> Delete <code>templates/mokoonyx/.migrated</code> via FTP or file manager, then visit any page on your site. The migration will run again automatically.<br><br><strong>To uninstall MokoCassiopeia:</strong> Go to <a href='index.php?option=com_installer&amp;view=manage&amp;filter[search]=mokocassiopeia'>Extensions → Manage</a>, find MokoCassiopeia, and click Uninstall."
; ===== Misc ===== ; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:" MOD_BREADCRUMBS_HERE="YOU ARE HERE:"

View File

@@ -1 +0,0 @@
@charset "UTF-8";body{font-size:1rem;font-weight:400;line-height:1.5;color:#22262a;background-color:#fff}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:700;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}h2{font-size:calc(1.325rem + .9vw)}h3{font-size:calc(1.3rem + .6vw)}h4{font-size:calc(1.275rem + .3vw)}h5{font-size:1.25rem}h6{font-size:1rem}a{text-decoration:none}a:link{color:#224faa}a:hover{color:#424077}p{margin-top:0;margin-bottom:1rem}hr#system-readmore{color:red;border:1px dashed red}span[lang]{padding:2px;border:1px dashed #bbb}span[lang]:after{font-size:smaller;color:red;vertical-align:super;content:attr(lang)}

View File

@@ -14204,10 +14204,11 @@ fieldset>* {
.container-header { .container-header {
z-index: 100; z-index: 100;
background: var(--header-background-image, url('../../../../../../media/templates/site/mokoonyx/images/bg.svg')); background-color: var(--header-background-color, #adadad);
background-image: var(--header-background-image, url('../../../../../../media/templates/site/mokoonyx/images/bg.svg'));
background-size: var(--header-background-size, auto); background-size: var(--header-background-size, auto);
box-shadow: 0 5px 5px hsla(0, 0%, 0%, 0.03) inset;
background-repeat: var(--header-background-repeat, repeat); background-repeat: var(--header-background-repeat, repeat);
box-shadow: 0 5px 5px hsla(0, 0%, 0%, 0.03) inset;
} }
/* Sticky header: override z-index to stay above all content */ /* Sticky header: override z-index to stay above all content */
@@ -15005,6 +15006,10 @@ iframe {
margin-bottom: 0; margin-bottom: 0;
} }
#maincontent {
margin-bottom: 0.75rem;
}
.container-component, .container-component,
.sidebar-left, .sidebar-left,
.sidebar-right { .sidebar-right {

File diff suppressed because one or more lines are too long

View File

@@ -210,6 +210,7 @@ color-scheme: dark;
/* ===== HEADER BACKGROUND ===== */ /* ===== HEADER BACKGROUND ===== */
--header-background-color: #1a1f2b;
--header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg'); --header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg');
--header-background-attachment: fixed; --header-background-attachment: fixed;
--header-background-repeat: repeat; --header-background-repeat: repeat;

File diff suppressed because one or more lines are too long

View File

@@ -209,6 +209,7 @@ color-scheme: light;
/* ===== HEADER BACKGROUND ===== */ /* ===== HEADER BACKGROUND ===== */
--header-background-color: #adadad;
--header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg'); --header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg');
--header-background-attachment: fixed; --header-background-attachment: fixed;
--header-background-repeat: repeat; --header-background-repeat: repeat;

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
(()=>{"use strict";const e=window,t={},n=e=>{const t=(()=>{const e=document.currentScript;return e||(Array.from(document.getElementsByTagName("script")).reverse().find(e=>(e.getAttribute("src")||"").includes("/gtm.js"))||null)})(),n=document.documentElement,o=document.body,a=document.querySelector(`meta[name="moko:gtm-${e}"]`);return t&&t.dataset&&t.dataset[e]||n&&n.dataset&&n.dataset[e]||o&&o.dataset&&o.dataset[e]||a&&a.getAttribute("content")||null},o=(e,t=!1)=>{if(null==e)return t;const n=String(e).trim().toLowerCase();return!!["1","true","yes","y","on"].includes(n)||!["0","false","no","n","off"].includes(n)&&t},a=(...e)=>{if(r.debug)try{console.info("[moko-gtm]",...e)}catch(e){}},r={id:"",dataLayerName:"dataLayer",debug:!1,ignoreDNT:!1,blockOnDev:!0,envAuth:"",envPreview:"",consentDefault:{analytics_storage:"granted",functionality_storage:"granted",security_storage:"granted",ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied"},pageVars:()=>({})},d=(e,t={})=>{const n={...e};for(const e in t){if(!Object.prototype.hasOwnProperty.call(t,e))continue;const o=t[e];o&&"object"==typeof o&&!Array.isArray(o)?n[e]={...n[e]||{},...o}:void 0!==o&&(n[e]=o)}return n},i=()=>{const t=e.MOKO_GTM_OPTIONS&&"object"==typeof e.MOKO_GTM_OPTIONS?e.MOKO_GTM_OPTIONS:{},a=n("id")||e.MOKO_GTM_ID||"",r=n("dataLayer")||"",d=n("debug"),i=n("ignoreDnt"),c=n("blockOnDev"),s=n("envAuth")||"",u=n("envPreview")||"";return{id:a||t.id||"",dataLayerName:r||t.dataLayerName||void 0,debug:o(d,!!t.debug),ignoreDNT:o(i,!!t.ignoreDNT),blockOnDev:o(c,t.blockOnDev??!0),envAuth:s||t.envAuth||"",envPreview:u||t.envPreview||"",consentDefault:t.consentDefault||void 0,pageVars:"function"==typeof t.pageVars?t.pageVars:void 0}},c=()=>{const t=r.dataLayerName;return e[t]=e[t]||[],e[t]},s=(...e)=>{c().push(arguments.length>1?e:e[0]),a("gtag push:",e)};t.push=(...e)=>s(...e),t.setConsent=e=>{s("consent","update",e||{})},t.isLoaded=()=>!!document.querySelector('script[src*="googletagmanager.com/gtm.js"]'),t.config=()=>({...r});const u=()=>{if(!r.id)return void a("GTM ID missing; aborting load.");if(t.isLoaded())return void a("GTM already loaded; skipping duplicate injection.");c().push({"gtm.start":(new Date).getTime(),event:"gtm.js"});const e=document.getElementsByTagName("script")[0],n=document.createElement("script");n.async=!0,n.src=`https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(r.id)}${"dataLayer"!==r.dataLayerName?`&l=${encodeURIComponent(r.dataLayerName)}`:""}${(()=>{const e=[];return r.envAuth&&e.push(`gtm_auth=${encodeURIComponent(r.envAuth)}`),r.envPreview&&e.push(`gtm_preview=${encodeURIComponent(r.envPreview)}`,"gtm_cookies_win=x"),e.length?`&${e.join("&")}`:""})()}`,e&&e.parentNode?e.parentNode.insertBefore(n,e):(document.head||document.documentElement).appendChild(n),a("Injected GTM script:",n.src)},g=()=>!r.ignoreDNT&&(()=>{const e=navigator,t=(e.doNotTrack||e.msDoNotTrack||e.navigator&&e.navigator.doNotTrack||"").toString().toLowerCase();return"1"===t||"yes"===t})()?(a("DNT is enabled; blocking GTM load (set ignoreDNT=true to override)."),!1):!r.blockOnDev||!(()=>{const t=e.location&&e.location.hostname||"";return"localhost"===t||"127.0.0.1"===t||t.endsWith(".local")||t.endsWith(".test")})()||(a("Development host detected; blocking GTM load (set blockOnDev=false to override)."),!1);t.init=(e={})=>{const t=i(),n=d(r,d(t,e));Object.assign(r,n),a("Config:",r),c(),s("consent","default",r.consentDefault),a("Applied default consent:",r.consentDefault),(()=>{const e={event:"moko.page_init",page_title:document.title||"",page_language:document.documentElement&&document.documentElement.lang||"",..."function"==typeof r.pageVars&&r.pageVars()||{}};s(e)})(),g()?u():a("GTM load prevented by configuration or environment.")};const m=()=>{!(!i().id&&!e.MOKO_GTM_ID)?t.init():a("No GTM ID detected; awaiting manual init via window.mokoGTM.init({ id: 'GTM-XXXXXXX' }).")};"complete"===document.readyState||"interactive"===document.readyState?setTimeout(m,0):document.addEventListener("DOMContentLoaded",m,{once:!0}),e.mokoGTM=t;try{const e=i();o(e.debug,!1)&&(r.debug=!0,a("Ready. You can call window.mokoGTM.init({ id: 'GTM-XXXXXXX' })."))}catch(e){}})();

View File

@@ -1 +0,0 @@
!function(e,t){"use strict";var a="theme",n=e.matchMedia("(prefers-color-scheme: dark)"),r=t.documentElement;function o(e){r.setAttribute("data-bs-theme",e),r.setAttribute("data-aria-theme",e);try{localStorage.setItem(a,e)}catch(e){}}function d(){try{localStorage.removeItem(a)}catch(e){}}function i(){return n.matches?"dark":"light"}function c(){try{return localStorage.getItem(a)}catch(e){return null}}function l(){if(!t.getElementById("mokoThemeFab")){var a,l=t.createElement("div");l.id="mokoThemeFab",l.className=(a=(t.body.getAttribute("data-theme-fab-pos")||"br").toLowerCase(),/^(br|bl|tr|tl)$/.test(a)||(a="br"),"pos-"+a);var s=t.createElement("span");s.className="label",s.textContent="Light";var u=t.createElement("button");u.id="mokoThemeSwitch",u.type="button",u.setAttribute("role","switch"),u.setAttribute("aria-label","Toggle dark mode"),u.setAttribute("aria-checked","false");var m=t.createElement("span");m.className="switch";var h=t.createElement("span");h.className="knob",m.appendChild(h),u.appendChild(m);var f=t.createElement("span");f.className="label",f.textContent="Dark";var b=t.createElement("button");b.id="mokoThemeAuto",b.type="button",b.className="btn btn-sm btn-link text-decoration-none px-2",b.setAttribute("aria-label","Follow system theme"),b.textContent="Auto",u.addEventListener("click",function(){var e="dark"===(r.getAttribute("data-bs-theme")||"light").toLowerCase()?"light":"dark";o(e),u.setAttribute("aria-checked","dark"===e?"true":"false");var a=t.querySelector('meta[name="theme-color"]');a&&a.setAttribute("content","dark"===e?"#0f1115":"#ffffff")}),b.addEventListener("click",function(){d();var e=i();o(e),u.setAttribute("aria-checked","dark"===e?"true":"false")});var g=function(){if(!c()){var e=i();o(e),u.setAttribute("aria-checked","dark"===e?"true":"false")}};"function"==typeof n.addEventListener?n.addEventListener("change",g):"function"==typeof n.addListener&&n.addListener(g);var p=c()||i();u.setAttribute("aria-checked","dark"===p?"true":"false"),l.appendChild(s),l.appendChild(u),l.appendChild(f),l.appendChild(b),t.body.appendChild(l),e.mokoThemeFabStatus=function(){var a=t.getElementById("mokoThemeFab");if(!a)return{mounted:!1};var n=a.getBoundingClientRect();return{mounted:!0,rect:{top:n.top,left:n.left,width:n.width,height:n.height},zIndex:e.getComputedStyle(a).zIndex,posClass:a.className}},setTimeout(function(){var e=l.getBoundingClientRect();(e.width<10||e.height<10)&&(l.classList.add("debug-outline"),console.warn("[moko] Theme FAB mounted but appears too small — check CSS collisions."))},50)}}function s(){e.scrollY>50?t.body.classList.add("scrolled"):t.body.classList.remove("scrolled")}function u(){var a=t.getElementById("back-top");a&&a.addEventListener("click",function(t){t.preventDefault(),e.scrollTo({top:0,behavior:"smooth"})})}function m(){!function(){var e=c()||i();o(e);var a=function(){c()||o(i())};"function"==typeof n.addEventListener?n.addEventListener("change",a):"function"==typeof n.addListener&&n.addListener(a);var r=t.getElementById("themeSwitch"),l=t.getElementById("themeAuto");r&&(r.checked="dark"===e,r.addEventListener("change",function(){o(r.checked?"dark":"light")})),l&&l.addEventListener("click",function(){d(),o(i())})}(),"1"===t.body.getAttribute("data-theme-fab-enabled")&&l(),s(),e.addEventListener("scroll",s),t.querySelector(".drawer-toggle-left")||t.querySelector(".drawer-toggle-right"),u()}"loading"===t.readyState?t.addEventListener("DOMContentLoaded",m):m()}(window,document);

View File

@@ -1,6 +1,6 @@
<?php <?php
/** /**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> * Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* *
* This file is part of a Moko Consulting project. * This file is part of a Moko Consulting project.
* *
@@ -12,45 +12,33 @@
* Joomla calls the methods in this class automatically during template * Joomla calls the methods in this class automatically during template
* install, update, and uninstall via the <scriptfile> element in * install, update, and uninstall via the <scriptfile> element in
* templateDetails.xml. * templateDetails.xml.
* Joomla 5 and 6 compatible — uses the InstallerScriptInterface when *
* available, falls back to the legacy class-based approach otherwise. * On first install, detects MokoCassiopeia and migrates template styles,
* parameters, menu assignments, and user files automatically.
*/ */
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Installer\InstallerScriptInterface;
use Joomla\CMS\Log\Log; use Joomla\CMS\Log\Log;
class Tpl_MokoonyxInstallerScript class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
{ {
/**
* Minimum PHP version required by this template.
*/
private const MIN_PHP = '8.1.0'; private const MIN_PHP = '8.1.0';
/**
* Minimum Joomla version required by this template.
*/
private const MIN_JOOMLA = '4.4.0'; private const MIN_JOOMLA = '4.4.0';
/** private const OLD_NAME = 'mokocassiopeia';
* Called before install/update/uninstall. private const NEW_NAME = 'mokoonyx';
* private const OLD_DISPLAY = 'MokoCassiopeia';
* @param string $type install, update, discover_install, or uninstall. private const NEW_DISPLAY = 'MokoOnyx';
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool True to proceed, false to abort.
*/
public function preflight(string $type, InstallerAdapter $parent): bool public function preflight(string $type, InstallerAdapter $parent): bool
{ {
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) { if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
Factory::getApplication()->enqueueMessage( Factory::getApplication()->enqueueMessage(
sprintf( sprintf('MokoOnyx requires PHP %s or later. You are running PHP %s.', self::MIN_PHP, PHP_VERSION),
'MokoOnyx requires PHP %s or later. You are running PHP %s.',
self::MIN_PHP,
PHP_VERSION
),
'error' 'error'
); );
return false; return false;
@@ -58,11 +46,7 @@ class Tpl_MokoonyxInstallerScript
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) { if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
Factory::getApplication()->enqueueMessage( Factory::getApplication()->enqueueMessage(
sprintf( sprintf('MokoOnyx requires Joomla %s or later. You are running Joomla %s.', self::MIN_JOOMLA, JVERSION),
'MokoOnyx requires Joomla %s or later. You are running Joomla %s.',
self::MIN_JOOMLA,
JVERSION
),
'error' 'error'
); );
return false; return false;
@@ -71,37 +55,17 @@ class Tpl_MokoonyxInstallerScript
return true; return true;
} }
/**
* Called after a successful install.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function install(InstallerAdapter $parent): bool public function install(InstallerAdapter $parent): bool
{ {
$this->logMessage('MokoOnyx template installed.'); $this->logMessage('MokoOnyx template installed.');
return true; return true;
} }
/**
* Called after a successful update.
*
* This is where the CSS variable sync runs — it detects variables that
* were added in the new version and injects them into the user's custom
* palette files without overwriting existing values.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function update(InstallerAdapter $parent): bool public function update(InstallerAdapter $parent): bool
{ {
$this->logMessage('MokoOnyx template updated.'); $this->logMessage('MokoOnyx template updated.');
// Run CSS variable sync to inject any new variables into user's custom palettes.
$synced = $this->syncCustomVariables($parent); $synced = $this->syncCustomVariables($parent);
if ($synced > 0) { if ($synced > 0) {
Factory::getApplication()->enqueueMessage( Factory::getApplication()->enqueueMessage(
sprintf( sprintf(
@@ -116,47 +80,272 @@ class Tpl_MokoonyxInstallerScript
return true; return true;
} }
/**
* Called after a successful uninstall.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function uninstall(InstallerAdapter $parent): bool public function uninstall(InstallerAdapter $parent): bool
{ {
$this->logMessage('MokoOnyx template uninstalled.'); $this->logMessage('MokoOnyx template uninstalled.');
return true; return true;
} }
/**
* Called after install/update completes (regardless of type).
*
* @param string $type install, update, or discover_install.
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function postflight(string $type, InstallerAdapter $parent): bool public function postflight(string $type, InstallerAdapter $parent): bool
{ {
// On install or update: migrate from MokoCassiopeia if it exists
if ($type === 'install' || $type === 'update') {
$this->migrateFromCassiopeia();
$this->replaceCassiopeiaReferences();
$this->clearFaviconStamp();
}
return true; return true;
} }
/** /**
* Run the CSS variable sync utility. * Replace MokoCassiopeia references in article content and module content.
*
* Loads sync_custom_vars.php from the template directory and calls
* MokoCssVarSync::run() to detect and inject missing variables.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return int Number of variables added across all files.
*/ */
private function replaceCassiopeiaReferences(): void
{
$db = Factory::getDbo();
// Replace in article content (introtext + fulltext)
foreach (['introtext', 'fulltext'] as $col) {
try {
$query = $db->getQuery(true)
->update('#__content')
->set(
$db->quoteName($col) . ' = REPLACE(REPLACE('
. $db->quoteName($col) . ', '
. $db->quote(self::OLD_DISPLAY) . ', '
. $db->quote(self::NEW_DISPLAY) . '), '
. $db->quote(self::OLD_NAME) . ', '
. $db->quote(self::NEW_NAME) . ')'
)
->where(
'(' . $db->quoteName($col) . ' LIKE ' . $db->quote('%' . self::OLD_DISPLAY . '%')
. ' OR ' . $db->quoteName($col) . ' LIKE ' . $db->quote('%' . self::OLD_NAME . '%') . ')'
);
$db->setQuery($query)->execute();
$n = $db->getAffectedRows();
if ($n > 0) {
$this->logMessage("Replaced MokoCassiopeia in {$n} content row(s) ({$col}).");
}
} catch (\Throwable $e) {
$this->logMessage('Content replacement failed (' . $col . '): ' . $e->getMessage(), 'warning');
}
}
// Replace in module content (custom HTML modules etc.)
try {
$query = $db->getQuery(true)
->update('#__modules')
->set(
$db->quoteName('content') . ' = REPLACE(REPLACE('
. $db->quoteName('content') . ', '
. $db->quote(self::OLD_DISPLAY) . ', '
. $db->quote(self::NEW_DISPLAY) . '), '
. $db->quote(self::OLD_NAME) . ', '
. $db->quote(self::NEW_NAME) . ')'
)
->where(
'(' . $db->quoteName('content') . ' LIKE ' . $db->quote('%' . self::OLD_DISPLAY . '%')
. ' OR ' . $db->quoteName('content') . ' LIKE ' . $db->quote('%' . self::OLD_NAME . '%') . ')'
);
$db->setQuery($query)->execute();
$n = $db->getAffectedRows();
if ($n > 0) {
$this->logMessage("Replaced MokoCassiopeia in {$n} module(s).");
}
} catch (\Throwable $e) {
$this->logMessage('Module replacement failed: ' . $e->getMessage(), 'warning');
}
}
/**
* Delete the favicon stamp file so favicons and site.webmanifest
* are regenerated on the next page load after install/update.
* Also removes the old /images/favicons/ location.
*/
private function clearFaviconStamp(): void
{
// Clear new location stamp
$stampFile = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME . '/images/favicons/.favicon_generated';
if (is_file($stampFile)) {
@unlink($stampFile);
$this->logMessage('Cleared favicon stamp — will regenerate on next page load.');
}
// Remove old /images/favicons/ directory from previous versions
$oldDir = JPATH_ROOT . '/images/favicons';
if (is_dir($oldDir)) {
$files = glob($oldDir . '/*');
if ($files) {
foreach ($files as $file) {
@unlink($file);
}
}
@unlink($oldDir . '/.favicon_generated');
@rmdir($oldDir);
$this->logMessage('Removed old favicon directory: images/favicons/');
}
// Remove any favicon files left in the site root
$rootFavicons = ['favicon.ico', 'favicon.png', 'apple-touch-icon.png', 'site.webmanifest'];
foreach ($rootFavicons as $file) {
$path = JPATH_ROOT . '/' . $file;
if (is_file($path)) {
@unlink($path);
$this->logMessage('Removed root favicon: ' . $file);
}
}
}
/**
* Detect MokoCassiopeia and create matching MokoOnyx styles with the same params.
* Creates a MokoOnyx style copy for each MokoCassiopeia style.
*/
private function migrateFromCassiopeia(): void
{
$db = Factory::getDbo();
// Get all 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)) {
$this->logMessage('No MokoCassiopeia styles found — fresh install.');
return;
}
$this->logMessage('MokoCassiopeia detected — creating ' . count($oldStyles) . ' matching MokoOnyx style(s).');
// Get the installer-created default MokoOnyx style (to apply params to it)
$query = $db->getQuery(true)
->select('id')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
->where($db->quoteName('client_id') . ' = 0')
->order($db->quoteName('id') . ' ASC');
$defaultOnyxId = (int) $db->setQuery($query, 0, 1)->loadResult();
$firstStyle = true;
foreach ($oldStyles as $oldStyle) {
$newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title);
$newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle);
$params = is_string($oldStyle->params)
? str_replace(self::OLD_NAME, self::NEW_NAME, $oldStyle->params)
: $oldStyle->params;
if ($firstStyle && $defaultOnyxId) {
// Update the installer-created default style with the first MokoCassiopeia style's params
$update = $db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('params') . ' = ' . $db->quote($params))
->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
->where('id = ' . $defaultOnyxId);
$db->setQuery($update)->execute();
// Set as default if MokoCassiopeia was default
if ($oldStyle->home == 1) {
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 1')
->where('id = ' . $defaultOnyxId)
)->execute();
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 0')
->where('id = ' . (int) $oldStyle->id)
)->execute();
$this->logMessage('Set MokoOnyx as default site template.');
}
$this->logMessage("Updated default MokoOnyx style with params: {$newTitle}");
$firstStyle = false;
continue;
}
// For additional styles: create new MokoOnyx style copies
$newStyle = clone $oldStyle;
unset($newStyle->id);
$newStyle->template = self::NEW_NAME;
$newStyle->title = $newTitle;
$newStyle->home = 0;
$newStyle->params = $params;
try {
$db->insertObject('#__template_styles', $newStyle, 'id');
$this->logMessage("Created MokoOnyx style: {$newTitle}");
} catch (\Throwable $e) {
$this->logMessage("Failed to create style {$newTitle}: " . $e->getMessage(), 'warning');
}
}
// 2. Copy user files (custom themes, user.css, user.js)
$this->copyUserFiles();
// 3. Notify admin
Factory::getApplication()->enqueueMessage(
'<strong>MokoOnyx has been installed as a replacement for MokoCassiopeia.</strong><br>'
. 'Your template settings and custom files have been migrated automatically. '
. 'MokoOnyx is now your active site template. '
. 'You can safely uninstall MokoCassiopeia from Extensions &rarr; Manage.',
'success'
);
}
/**
* Copy user-specific files from MokoCassiopeia media to MokoOnyx media.
*/
private function copyUserFiles(): void
{
$oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
$newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
if (!is_dir($oldMedia) || !is_dir($newMedia)) {
return;
}
$userFiles = [
'css/theme/light.custom.css',
'css/theme/dark.custom.css',
'css/theme/light.custom.min.css',
'css/theme/dark.custom.min.css',
'css/user.css',
'css/user.min.css',
'js/user.js',
'js/user.min.js',
];
$copied = 0;
foreach ($userFiles as $relPath) {
$src = $oldMedia . '/' . $relPath;
$dst = $newMedia . '/' . $relPath;
if (is_file($src) && !is_file($dst)) {
$dstDir = dirname($dst);
if (!is_dir($dstDir)) {
mkdir($dstDir, 0755, true);
}
copy($src, $dst);
$copied++;
}
}
if ($copied > 0) {
$this->logMessage("Copied {$copied} user file(s) from MokoCassiopeia.");
}
}
private function syncCustomVariables(InstallerAdapter $parent): int private function syncCustomVariables(InstallerAdapter $parent): int
{ {
$templateDir = $parent->getParent()->getPath('source'); $templateDir = $parent->getParent()->getPath('source');
// The sync script lives alongside this script in the template root.
$syncScript = $templateDir . '/sync_custom_vars.php'; $syncScript = $templateDir . '/sync_custom_vars.php';
if (!is_file($syncScript)) { if (!is_file($syncScript)) {
@@ -172,20 +361,13 @@ class Tpl_MokoonyxInstallerScript
} }
try { try {
$joomlaRoot = JPATH_ROOT; $results = MokoCssVarSync::run(JPATH_ROOT);
$results = MokoCssVarSync::run($joomlaRoot);
$totalAdded = 0; $totalAdded = 0;
foreach ($results as $filePath => $result) { foreach ($results as $filePath => $result) {
$totalAdded += count($result['added']); $totalAdded += count($result['added']);
if (!empty($result['added'])) { if (!empty($result['added'])) {
$this->logMessage( $this->logMessage(sprintf('CSS sync: added %d variable(s) to %s', count($result['added']), basename($filePath)));
sprintf(
'CSS sync: added %d variable(s) to %s',
count($result['added']),
basename($filePath)
)
);
} }
} }
@@ -196,12 +378,6 @@ class Tpl_MokoonyxInstallerScript
} }
} }
/**
* Log a message to Joomla's log system.
*
* @param string $message The log message.
* @param string $priority Log priority (info, warning, error).
*/
private function logMessage(string $message, string $priority = 'info'): void private function logMessage(string $message, string $priority = 'info'): void
{ {
$priorities = [ $priorities = [

View File

@@ -39,13 +39,13 @@
</server> </server>
</updateservers> </updateservers>
<name>MokoOnyx</name> <name>MokoOnyx</name>
<version>01.00.00</version> <version>01.00.18</version>
<scriptfile>script.php</scriptfile> <scriptfile>script.php</scriptfile>
<creationDate>2026-04-15</creationDate> <creationDate>2026-04-15</creationDate>
<author>Jonathan Miller || Moko Consulting</author> <author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright> <copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<description><![CDATA[<p><img src="https://img.shields.io/badge/version-01.00.00-blue.svg?logo=v&amp;logoColor=white" alt="Version 01.00.00" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&amp;logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&amp;logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&amp;logoColor=white" alt="PHP" /></p> <h3>MokoOnyx Template Description</h3> <p> <strong>MokoOnyx</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/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code>, or <code>templates/mokoonyx/templates/dark.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoOnyx → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS &amp; JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokoonyx/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[<div style="padding:1rem;border:2px solid #059669;border-radius:8px;background:#ecfdf5;margin-bottom:1rem;"> <h3 style="color:#065f46;margin:0 0 .5rem;">Migrating from MokoCassiopeia?</h3> <p style="margin:0 0 .75rem;">MokoOnyx automatically imports your MokoCassiopeia settings on first use. To trigger the migration:</p> <ol style="margin:0 0 .75rem 1.5rem;"> <li>Install MokoOnyx via <strong>System → Install → Extensions</strong></li> <li>Go to <strong>System → Site Templates</strong> and set <strong>MokoOnyx</strong> as your default template</li> <li><strong>Visit any page on your site</strong> — the migration runs automatically on first page load</li> <li>Check <strong>administrator/logs/mokoonyx_migrate.log.php</strong> to confirm migration completed</li> <li>Uninstall MokoCassiopeia from <strong>Extensions → Manage</strong></li> </ol> <p style="margin:0;font-size:.9em;color:#065f46;"> <strong>What gets migrated:</strong> template style params, custom colour palettes (light.custom.css, dark.custom.css), user.css, user.js, and update server URL. To re-run the migration, delete <code>templates/mokoonyx/.migrated</code> and reload any page. </p></div> <p><img src="https://img.shields.io/gitea/v/release/MokoConsulting/MokoOnyx?gitea_url=https%3A%2F%2Fgit.mokoconsulting.tech&amp;logo=gitea&amp;logoColor=white&amp;label=version" alt="Version" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&amp;logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&amp;logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&amp;logoColor=white" alt="PHP" /></p> <h3>MokoOnyx</h3> <p> <strong>MokoOnyx</strong> (formerly MokoCassiopeia) is a modern, lightweight enhancement layer built on Joomla's Cassiopeia template. It adds Font Awesome 7, Bootstrap 5 helpers, automatic Table of Contents, advanced Dark Mode theming, and optional Google Tag Manager / GA4 integration — all with minimal core overrides for maximum upgrade compatibility. </p> <h4>Custom Colour Themes</h4> <p> Copy <code>templates/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code> (or dark equivalent), customise the CSS variables, then select "Custom" in the Theme tab. </p> <h4>Custom CSS &amp; JavaScript</h4> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — custom CSS overrides</li> <li><code>media/templates/site/mokoonyx/js/user.js</code> — custom JavaScript</li> </ul> <p>These files survive template updates.</p>]]></description>
<inheritable>1</inheritable> <inheritable>1</inheritable>
<files> <files>
<filename>component.php</filename> <filename>component.php</filename>
@@ -108,6 +108,10 @@
<fields name="params"> <fields name="params">
<!-- Advanced tab (non-theme/system options only) --> <!-- Advanced tab (non-theme/system options only) -->
<fieldset name="migration" label="TPL_MOKOONYX_MIGRATION_FIELDSET_LABEL">
<field name="migration_note" type="note" label="TPL_MOKOONYX_MIGRATION_NOTE_LABEL" description="TPL_MOKOONYX_MIGRATION_NOTE_DESC" />
<field name="migration_run" type="note" label="TPL_MOKOONYX_MIGRATION_RUN_LABEL" description="TPL_MOKOONYX_MIGRATION_RUN_DESC" />
</fieldset>
<fieldset name="advanced"> <fieldset name="advanced">
<field name="developmentmode" type="radio" label="TPL_MOKOONYX_DEVELOPMENTMODE_LABEL" description="TPL_MOKOONYX_DEVELOPMENTMODE_DESC" default="0" layout="joomla.form.field.radio.switcher" filter="boolean"> <field name="developmentmode" type="radio" label="TPL_MOKOONYX_DEVELOPMENTMODE_LABEL" description="TPL_MOKOONYX_DEVELOPMENTMODE_DESC" default="0" layout="joomla.form.field.radio.switcher" filter="boolean">
<option value="0">JNO</option> <option value="0">JNO</option>

View File

@@ -210,6 +210,7 @@ color-scheme: dark;
/* ===== HEADER BACKGROUND ===== */ /* ===== HEADER BACKGROUND ===== */
--header-background-color: #1a1f2b;
--header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg'); --header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg');
--header-background-attachment: fixed; --header-background-attachment: fixed;
--header-background-repeat: repeat; --header-background-repeat: repeat;

View File

@@ -209,6 +209,7 @@ color-scheme: light;
/* ===== HEADER BACKGROUND ===== */ /* ===== HEADER BACKGROUND ===== */
--header-background-color: #adadad;
--header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg'); --header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg');
--header-background-attachment: fixed; --header-background-attachment: fixed;
--header-background-repeat: repeat; --header-background-repeat: repeat;