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

Merged
jmiller merged 62 commits from dev into main 2026-04-23 20:01:27 +00:00
49 changed files with 2558 additions and 302 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

221
.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
# FILE INFORMATION
DEFGROUP: MokoStandards.Templates.GitHub
INGROUP: MokoStandards.Templates
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
PATH: /templates/github/copilot-instructions.joomla.md.template
VERSION: XX.YY.ZZ
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
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}}
DEFGROUP: MokoOnyx.GitHub
INGROUP: MokoOnyx
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
PATH: /.github/CLAUDE.md
VERSION: 01.00.05
BRIEF: Claude Code custom instructions for MokoOnyx template
-->
> [!IMPORTANT]
> **🔧 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
# MokoOnyx — Claude Code Instructions
## 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.
Repository URL: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
Extension name: **{{EXTENSION_NAME}}**
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
Platform: **Joomla 4.x / MokoWaaS**
Extension name: **MokoOnyx**
Extension type: **template** (`mokoonyx`)
Platform: **Joomla 5.x / 6.x / MokoWaaS**
Successor to: **MokoCassiopeia** (renamed in v01.00.00)
---
## 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
*
* FILE INFORMATION
* DEFGROUP: MokoOnyx.{{EXTENSION_TYPE}}
* DEFGROUP: MokoOnyx.Template
* INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* 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.**
- **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.
- 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.
### 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
<!-- 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>
<update>
<name>{{EXTENSION_NAME}}</name>
<version>01.02.04</version>
<downloads>
<downloadurl type="full" format="zip">
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 … -->
<!-- 1. DEVELOPMENT --> <update>...<tag>development</tag>...</update>
<!-- 2. ALPHA --> <update>...<tag>alpha</tag>...</update>
<!-- 3. BETA --> <update>...<tag>beta</tag>...</update>
<!-- 4. RC --> <update>...<tag>rc</tag>...</update>
<!-- 5. STABLE --> <update>...<tag>stable</tag>...</update>
</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/
├── manifest.xml # Joomla installer manifest (root — required)
├── updates.xml # Update server manifest (root — required, see below)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
└── views/
├── admin/ # Backend (admin) code
│ ├── controller.php
│ ├── controllers/
── models/
│ ├── views/
│ └── sql/
├── language/ # Language INI files
├── 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
├── src/
│ ├── templateDetails.xml # Joomla installer manifest
├── script.php # Install/update script (limited in Joomla 6 for templates)
│ ├── index.php # Main template file (includes migration bootstrap)
│ ├── helper/
│ ├── migrate.php # MokoCassiopeia → MokoOnyx migration (v01.x)
│ ├── favicon.php # Favicon generator
└── minify.php # Asset minification
│ ├── language/ # Language INI files (en-GB, en-US)
│ ├── media/ # CSS, JS, images, vendor assets
── templates/ # Custom CSS palette starters, theme test
├── updates.xml # Update server manifest (root — required)
├── .gitea/workflows/ # Gitea Actions workflows
├── docs/ # Documentation
├── README.md # Version source of truth
├── CHANGELOG.md
── CONTRIBUTING.md
├── LICENSE # GPL-3.0-or-later
└── Makefile # Build automation
── LICENSE
```
---
## 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.
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).
Every workflow must use **`secrets.GA_TOKEN`** (the org-level Personal Access Token).
```yaml
# ✅ Correct
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
token: ${{ secrets.GA_TOKEN }}
```
```yaml
# ❌ Wrong — never use these in workflows
# ❌ Wrong
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
```
---
## 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 |
Note: Workflows are in `.gitea/workflows/` (not `.github/workflows/`).
---
@@ -260,7 +185,7 @@ This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/M
| PHP variable | `$snake_case` | `$item_id` |
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
| 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` |
---
@@ -286,11 +211,11 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
| Change type | Documentation to update |
|-------------|------------------------|
| 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 release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed templateDetails.xml | Release workflow auto-bumps version across README.md, templateDetails.xml, and updates.xml |
| 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` |
| 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 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 use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GA_TOKEN`
- 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
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

8
.gitignore vendored
View File

@@ -119,6 +119,14 @@ site/
*.js.map
*.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
# ============================================================

View File

@@ -9,17 +9,17 @@
INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: ./README.md
VERSION: 01.00.00
VERSION: 01.00.18
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**
[![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)
[![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)
@@ -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
- **Font Awesome 7**: Fully integrated into Joomla's asset manager with 2,000+ icons
- **Bootstrap 5**: Extended utility classes and responsive grid system
- **No Template Overrides**: Clean installation that inherits all Cassiopeia defaults
- **Upgrade-Friendly**: Minimal modifications ensure smooth Joomla updates
- **Template Overrides**: Includes overrides for all core Joomla modules, Community Builder, and DPCalendar with consistent title rendering and Bootstrap 5 styling
- **Upgrade-Friendly**: Minimal core modifications ensure smooth Joomla updates
### Advanced Theming
@@ -90,8 +90,8 @@ MokoOnyx is a modern, lightweight enhancement layer built on top of Joomla's Cas
## 📋 Requirements
- **Joomla**: 4.4.x or 5.x
- **PHP**: 8.0 or higher
- **Joomla**: 5.x or 6.x
- **PHP**: 8.1 or higher
- **Database**: MySQL 5.7+ / MariaDB 10.2+ / PostgreSQL 11+
- **Browser Support**: Modern browsers (Chrome, Firefox, Safari, Edge)
@@ -360,11 +360,7 @@ See the [CHANGELOG.md](./CHANGELOG.md) for detailed version history.
### Recent Releases
- **[03.06.03]** (2026-01-30) - README updates and TOC color variable improvements
- **[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
See [Gitea Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases) for all versions.
---
@@ -373,8 +369,7 @@ See the [CHANGELOG.md](./CHANGELOG.md) for detailed version history.
### Getting Help
- **Documentation**: Check this README and [docs folder](./docs/)
- **Issues**: Report bugs via [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Discussions**: Ask questions in [GitHub Discussions](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/discussions)
- **Issues**: Report bugs via [Gitea Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Roadmap**: View planned features in [Roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap)
### Reporting Bugs
@@ -439,7 +434,7 @@ MokoOnyx includes the following third-party libraries to provide enhanced functi
- Responsive design (collapses on mobile)
- Smooth scrolling to sections
- 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
@@ -467,7 +462,7 @@ All third-party libraries are:
- ✅ Registered in Joomla's Web Asset Manager (`joomla.asset.json`)
- ✅ Loaded on-demand to optimize performance
- ✅ 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
- **Repository**: [GitHub](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Discussions**: [GitHub Discussions](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/discussions)
- **Repository**: [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
- **Issue Tracker**: [Gitea Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **GitHub Mirror**: [GitHub](https://github.com/mokoconsulting-tech/MokoOnyx)
- **Roadmap**: [Full Roadmap](https://mokoconsulting.tech/support/joomla-cms/mokoonyx-roadmap)
- **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 |
| ---------- | -------- | ------------------------------------------------------------------------- | ------------------------------- |
| 2026-01-30 | 03.06.03 | Updated README title, fixed custom color variables instructions, improved TOC color scheme integration | Copilot Agent |
| 2026-01-30 | 03.06.02 | Regenerated README with comprehensive documentation and updated structure | Copilot Agent |
| 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) |
| 2026-04-22 | 01.00.15 | Updated README: dynamic version badge, corrected requirements, fixed links | Claude Code |
| 2026-04-19 | 01.00.00 | Initial MokoOnyx release — renamed from MokoCassiopeia with auto-migration | Moko Consulting |
---

View File

@@ -22,7 +22,7 @@
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: docs/ROADMAP.md
VERSION: 03.09.03
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.
#### 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
**Status**: Planned
**Target Release**: December 2027
@@ -860,8 +879,8 @@ MokoOnyx aims to be the **most developer-friendly, user-customizable, and standa
### Official Links
- **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)
- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia/issues)
- **Repository**: [https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)
- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/issues)
- **Changelog**: [CHANGELOG.md](../CHANGELOG.md)
### 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!
**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
3. Provide clear use cases and benefits
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);
$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;
}

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;
// 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\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
@@ -92,8 +97,8 @@ if ($params_favicon_source) {
}
}
}
$faviconOutputDir = JPATH_ROOT . '/images/favicons';
$faviconUrlBase = Uri::root(true) . '/images/favicons';
$faviconOutputDir = JPATH_ROOT . '/media/templates/site/' . $this->template . '/images/favicons';
$faviconUrlBase = Uri::root(true) . '/media/templates/site/' . $this->template . '/images/favicons';
if (MokoFaviconHelper::generate($faviconSourceAbs, $faviconOutputDir)) {
$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_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_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_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 =====
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_DRAWER_LEFT="Drawer-Left"
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_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_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_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 =====
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

@@ -14193,10 +14193,11 @@ fieldset>* {
.container-header {
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);
box-shadow: 0 5px 5px hsla(0, 0%, 0%, 0.03) inset;
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 */
@@ -14994,6 +14995,10 @@ iframe {
margin-bottom: 0;
}
#maincontent {
margin-bottom: 0.75rem;
}
.container-component,
.sidebar-left,
.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-color: #1a1f2b;
--header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg');
--header-background-attachment: fixed;
--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-color: #adadad;
--header-background-image: url('../../../../../../media/templates/site/mokoonyx/images/bg.svg');
--header-background-attachment: fixed;
--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
/**
* 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.
*
@@ -12,45 +12,33 @@
* Joomla calls the methods in this class automatically during template
* install, update, and uninstall via the <scriptfile> element in
* 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;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Installer\InstallerScriptInterface;
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';
/**
* Minimum Joomla version required by this template.
*/
private const MIN_JOOMLA = '4.4.0';
/**
* Called before install/update/uninstall.
*
* @param string $type install, update, discover_install, or uninstall.
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool True to proceed, false to abort.
*/
private const OLD_NAME = 'mokocassiopeia';
private const NEW_NAME = 'mokoonyx';
private const OLD_DISPLAY = 'MokoCassiopeia';
private const NEW_DISPLAY = 'MokoOnyx';
public function preflight(string $type, InstallerAdapter $parent): bool
{
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
Factory::getApplication()->enqueueMessage(
sprintf(
'MokoOnyx requires PHP %s or later. You are running PHP %s.',
self::MIN_PHP,
PHP_VERSION
),
sprintf('MokoOnyx requires PHP %s or later. You are running PHP %s.', self::MIN_PHP, PHP_VERSION),
'error'
);
return false;
@@ -58,11 +46,7 @@ class Tpl_MokoonyxInstallerScript
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
Factory::getApplication()->enqueueMessage(
sprintf(
'MokoOnyx requires Joomla %s or later. You are running Joomla %s.',
self::MIN_JOOMLA,
JVERSION
),
sprintf('MokoOnyx requires Joomla %s or later. You are running Joomla %s.', self::MIN_JOOMLA, JVERSION),
'error'
);
return false;
@@ -71,37 +55,17 @@ class Tpl_MokoonyxInstallerScript
return true;
}
/**
* Called after a successful install.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function install(InstallerAdapter $parent): bool
{
$this->logMessage('MokoOnyx template installed.');
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
{
$this->logMessage('MokoOnyx template updated.');
// Run CSS variable sync to inject any new variables into user's custom palettes.
$synced = $this->syncCustomVariables($parent);
if ($synced > 0) {
Factory::getApplication()->enqueueMessage(
sprintf(
@@ -116,47 +80,272 @@ class Tpl_MokoonyxInstallerScript
return true;
}
/**
* Called after a successful uninstall.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function uninstall(InstallerAdapter $parent): bool
{
$this->logMessage('MokoOnyx template uninstalled.');
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
{
// On install or update: migrate from MokoCassiopeia if it exists
if ($type === 'install' || $type === 'update') {
$this->migrateFromCassiopeia();
$this->replaceCassiopeiaReferences();
$this->clearFaviconStamp();
}
return true;
}
/**
* Run the CSS variable sync utility.
*
* 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.
* Replace MokoCassiopeia references in article content and module content.
*/
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
{
$templateDir = $parent->getParent()->getPath('source');
// The sync script lives alongside this script in the template root.
$syncScript = $templateDir . '/sync_custom_vars.php';
if (!is_file($syncScript)) {
@@ -172,20 +361,13 @@ class Tpl_MokoonyxInstallerScript
}
try {
$joomlaRoot = JPATH_ROOT;
$results = MokoCssVarSync::run($joomlaRoot);
$results = MokoCssVarSync::run(JPATH_ROOT);
$totalAdded = 0;
foreach ($results as $filePath => $result) {
$totalAdded += count($result['added']);
if (!empty($result['added'])) {
$this->logMessage(
sprintf(
'CSS sync: added %d variable(s) to %s',
count($result['added']),
basename($filePath)
)
);
$this->logMessage(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
{
$priorities = [

View File

@@ -39,13 +39,13 @@
</server>
</updateservers>
<name>MokoOnyx</name>
<version>01.00.00</version>
<version>01.00.18</version>
<scriptfile>script.php</scriptfile>
<creationDate>2026-04-15</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<description><![CDATA[<p><img src="https://img.shields.io/badge/version-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>
<files>
<filename>component.php</filename>
@@ -108,6 +108,10 @@
<fields name="params">
<!-- 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">
<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>

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 01.00.00
VERSION: 01.00.07
-->
<updates>
@@ -13,13 +13,13 @@
<element>mokoonyx</element>
<type>template</type>
<client>site</client>
<version>01.00.00</version>
<creationDate>2026-04-19</creationDate>
<version>01.00.18</version>
<creationDate>2026-04-23</creationDate>
<infourl title='MokoOnyx Dev'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-01.00.00-dev.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-01.00.18-dev.zip</downloadurl>
</downloads>
<sha256></sha256>
<sha256>bcfc0af3cb19bcfeb65d180f657107e905b2566505e6ce81d0e23956ba7784d1</sha256>
<tags><tag>development</tag></tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -34,11 +34,11 @@
<element>mokoonyx</element>
<type>template</type>
<client>site</client>
<version>01.00.00</version>
<creationDate>2026-04-19</creationDate>
<version>01.00.07</version>
<creationDate>2026-04-21</creationDate>
<infourl title='MokoOnyx Alpha'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/alpha</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/alpha/mokoonyx-01.00.00-alpha.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/alpha/mokoonyx-01.00.07-alpha.zip</downloadurl>
</downloads>
<sha256></sha256>
<tags><tag>alpha</tag></tags>
@@ -55,11 +55,11 @@
<element>mokoonyx</element>
<type>template</type>
<client>site</client>
<version>01.00.00</version>
<creationDate>2026-04-19</creationDate>
<version>01.00.07</version>
<creationDate>2026-04-21</creationDate>
<infourl title='MokoOnyx Beta'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/beta</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/beta/mokoonyx-01.00.00-beta.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/beta/mokoonyx-01.00.07-beta.zip</downloadurl>
</downloads>
<sha256></sha256>
<tags><tag>beta</tag></tags>
@@ -76,12 +76,12 @@
<element>mokoonyx</element>
<type>template</type>
<client>site</client>
<version>01.00.00</version>
<creationDate>2026-04-19</creationDate>
<version>01.00.07</version>
<creationDate>2026-04-21</creationDate>
<infourl title='MokoOnyx RC'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/release-candidate</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.00-rc.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.00-rc.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.07-rc.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.07-rc.zip</downloadurl>
</downloads>
<sha256></sha256>
<tags><tag>rc</tag></tags>
@@ -98,14 +98,14 @@
<element>mokoonyx</element>
<type>template</type>
<client>site</client>
<version>01.00.00</version>
<creationDate>2026-04-19</creationDate>
<infourl title='MokoOnyx'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/v01</infourl>
<version>01.00.17</version>
<creationDate>2026-04-22</creationDate>
<infourl title='MokoOnyx'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/mokoonyx-01.00.17.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoOnyx/releases/download/stable/mokoonyx-01.00.17.zip</downloadurl>
</downloads>
<sha256></sha256>
<sha256>f4811005774b99e54513cd2cae052b815ef085e2a04f3a91cd5c0bc9cff7ffc2</sha256>
<tags><tag>stable</tag></tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>