Merge dev into main for v03.09.01 release

Merges all dev branch work including:
- Accessibility toolbar (6 toggleable options)
- Complete module overrides with showtitle (24 modules)
- IcoMoon to Font Awesome 7 compatibility layer
- Sidebar accordion (open desktop, collapsed mobile)
- TOC scoped to article body, multi-level heading support
- Bootstrap collapse for mobile menu
- Search module full-width in header
- Blog equal-height cards
- Footer padding and dynamic floating control offsets
- Auto dev mode when Joomla debug enabled
- mod_login count() null fix
- Main menu link color fixes
- Back-to-top FA icon and anchor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 17:40:15 -05:00
175 changed files with 8153 additions and 5891 deletions

18
.github/CODEOWNERS vendored
View File

@@ -8,20 +8,8 @@
# Combined with branch protection (require PR reviews), this prevents
# unauthorized modifications to workflows, configs, and governance files.
# ── Synced workflows (managed by MokoStandards — do not edit manually) ────
/.github/workflows/deploy-dev.yml @jmiller-moko
/.github/workflows/deploy-demo.yml @jmiller-moko
/.github/workflows/deploy-rs.yml @jmiller-moko
/.github/workflows/auto-release.yml @jmiller-moko
/.github/workflows/auto-dev-issue.yml @jmiller-moko
/.github/workflows/auto-assign.yml @jmiller-moko
/.github/workflows/sync-version-on-merge.yml @jmiller-moko
/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko
/.github/workflows/repository-cleanup.yml @jmiller-moko
/.github/workflows/standards-compliance.yml @jmiller-moko
/.github/workflows/codeql-analysis.yml @jmiller-moko
/.github/workflows/repo_health.yml @jmiller-moko
# Custom workflows in .github/workflows/ not listed above are repo-owned.
# ── Workflows (synced from MokoStandards — must not be manually edited) ──
/.github/workflows/ @jmiller-moko
# ── GitHub configuration ─────────────────────────────────────────────────
/.github/ISSUE_TEMPLATE/ @jmiller-moko
@@ -35,7 +23,7 @@
/composer.json @jmiller-moko
/phpstan.neon @jmiller-moko
/Makefile @jmiller-moko
/.ftpignore @jmiller-moko
/.ftp_ignore @jmiller-moko
/.gitignore @jmiller-moko
/.gitattributes @jmiller-moko
/.editorconfig @jmiller-moko

View File

@@ -9,22 +9,14 @@
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/auto-dev-issue.yml.template
# VERSION: 04.05.13
# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow
# VERSION: 04.05.00
# BRIEF: Auto-create tracking issue when a dev/** or rc/** branch is pushed
# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos.
name: Dev/RC Branch Issue
name: Auto Dev Branch Issue
on:
# Auto-create on RC branch creation
create:
# Manual trigger for dev branches
workflow_dispatch:
inputs:
branch:
description: 'Branch name (e.g., dev/my-feature or dev/04.06)'
required: true
type: string
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -38,20 +30,15 @@ jobs:
name: Create version tracking issue
runs-on: ubuntu-latest
if: >-
(github.event_name == 'workflow_dispatch') ||
(github.event.ref_type == 'branch' && startsWith(github.event.ref, 'rc/'))
github.event.ref_type == 'branch' &&
(startsWith(github.event.ref, 'dev/') || startsWith(github.event.ref, 'rc/'))
steps:
- name: Create tracking issue and sub-issues
- name: Create tracking issue
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
# For manual dispatch, use input; for auto, use event ref
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
BRANCH="${{ inputs.branch }}"
else
BRANCH="${{ github.event.ref }}"
fi
BRANCH="${{ github.event.ref }}"
REPO="${{ github.repository }}"
ACTOR="${{ github.actor }}"
NOW=$(date -u '+%Y-%m-%d %H:%M UTC')
@@ -71,122 +58,45 @@ jobs:
TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}"
BODY="## ${BRANCH_TYPE} Branch Created
| Field | Value |
|-------|-------|
| **Branch** | \`${BRANCH}\` |
| **Version** | \`${VERSION}\` |
| **Type** | ${BRANCH_TYPE} |
| **Created by** | @${ACTOR} |
| **Created at** | ${NOW} |
| **Repository** | \`${REPO}\` |
## Checklist
- [ ] Feature development complete
- [ ] Tests passing
- [ ] README.md version bumped to \`${VERSION}\`
- [ ] CHANGELOG.md updated
- [ ] PR created targeting \`main\`
- [ ] Code reviewed and approved
- [ ] Merged to \`main\`
---
*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*"
# Dedent heredoc
BODY=$(echo "$BODY" | sed 's/^ //')
# Check for existing issue with same title prefix
EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=10" \
EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=5" \
--jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1)
if [ -n "$EXISTING" ]; then
echo " Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# ── Define sub-issues for the dev workflow ────────────────────────
if [[ "$BRANCH" == rc/* ]]; then
SUB_ISSUES=(
"RC Testing|Verify all features work on rc branch|type: test,release-candidate"
"Regression Testing|Run full regression suite before merge to main|type: test,release-candidate"
"Version Bump|Bump version in README.md and all headers|type: version,release-candidate"
"Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate"
"Merge to Main|Create PR from rc branch to main|type: release,needs-review"
)
else
SUB_ISSUES=(
"Development|Implement feature/fix on dev branch|type: feature,status: in-progress"
"Unit Testing|Write and pass unit tests|type: test,status: pending"
"Code Review|Request and complete code review|needs-review,status: pending"
"Version Bump|Bump version in README.md and all headers|type: version,status: pending"
"Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending"
"Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending"
"Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending"
)
fi
# ── Create sub-issues first ───────────────────────────────────────
SUB_LIST=""
SUB_NUMBERS=""
for SUB in "${SUB_ISSUES[@]}"; do
IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \
"$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH")
SUB_URL=$(gh issue create \
ISSUE_URL=$(gh issue create \
--repo "$REPO" \
--title "$SUB_FULL_TITLE" \
--body "$SUB_BODY" \
--label "${SUB_LABELS}" \
--title "$TITLE" \
--body "$BODY" \
--label "${LABEL_TYPE},version" \
--assignee "jmiller-moko" 2>&1)
SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$')
if [ -n "$SUB_NUM" ]; then
SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})"
SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}"
fi
sleep 0.3
done
# ── Create parent tracking issue ──────────────────────────────────
PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \
"$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST")
PARENT_URL=$(gh issue create \
--repo "$REPO" \
--title "$TITLE" \
--body "$PARENT_BODY" \
--label "${LABEL_TYPE},version" \
--assignee "jmiller-moko" 2>&1)
PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$')
# ── Link sub-issues back to parent ────────────────────────────────
if [ -n "$PARENT_NUM" ]; then
for SUB in "${SUB_ISSUES[@]}"; do
IFS='|' read -r SUB_TITLE _ _ <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_NUM=$(gh api "repos/${REPO}/issues?state=open&per_page=20" \
--jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1)
if [ -n "$SUB_NUM" ]; then
gh api "repos/${REPO}/issues/${SUB_NUM}" -X PATCH \
-f body="$(gh api "repos/${REPO}/issues/${SUB_NUM}" --jq '.body' 2>/dev/null)
> **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true
fi
sleep 0.2
done
echo "✅ Created tracking issue: ${ISSUE_URL}" >> $GITHUB_STEP_SUMMARY
fi
# ── RC: Create or update draft release ────────────────────────────
if [[ "$BRANCH" == rc/* ]]; then
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
RELEASE_TAG="v${MAJOR}"
DRAFT_EXISTS=$(gh release view "$RELEASE_TAG" --json isDraft -q .isDraft 2>/dev/null || true)
if [ -z "$DRAFT_EXISTS" ]; then
# No release exists — create draft
gh release create "$RELEASE_TAG" \
--title "v${MAJOR} (RC: ${VERSION})" \
--notes "## Release Candidate ${VERSION}\n\nRC branch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
--draft \
--target main 2>/dev/null || true
echo "Draft release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
elif [ "$DRAFT_EXISTS" = "true" ]; then
# Draft exists — update title
gh release edit "$RELEASE_TAG" \
--title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true
echo "Draft release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
# Release exists and is published — set back to draft for RC
gh release edit "$RELEASE_TAG" \
--title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true
echo "Release ${RELEASE_TAG} set to draft for RC" >> $GITHUB_STEP_SUMMARY
fi
fi
# ── Summary ───────────────────────────────────────────────────────
echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY
echo "|------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY
echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY

View File

@@ -6,31 +6,29 @@
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/auto-release.yml.template
# VERSION: 04.05.13
# BRIEF: Joomla build & release — ZIP package, update.xml, SHA-256 checksum
# PATH: /templates/workflows/shared/auto-release.yml.template
# VERSION: 04.05.00
# BRIEF: Unified build & release pipeline — version branch, platform version, badges, tag, release
#
# +========================================================================+
# | BUILD & RELEASE PIPELINE (JOOMLA) |
# +========================================================================+
# | |
# | Triggers on push to main (skips bot commits + [skip ci]): |
# | |
# | Every push: |
# | 1. Read version from README.md |
# | 3. Set platform version (Joomla <version>) |
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
# | 5. Write update.xml (Joomla update server XML) |
# | 6. Create git tag vXX.YY.ZZ |
# | 7a. Patch: update existing GitHub Release for this minor |
# | 8. Build ZIP, upload asset, write SHA-256 to update.xml |
# | |
# | Every version change: archives main -> version/XX.YY branch |
# | Patch 00 = development (no release). First release = patch 01. |
# | First release only (patch == 01): |
# | 7b. Create new GitHub Release |
# | |
# +========================================================================+
# ╔════════════════════════════════════════════════════════════════════════╗
# BUILD & RELEASE PIPELINE
# ╠════════════════════════════════════════════════════════════════════════╣
#
# Triggers on push to main (skips bot commits + [skip ci]):
#
# Every push:
# 1. Read version from README.md
# 3. Set platform version (Dolibarr $this->version, Joomla <version>)║
# 4. Update [VERSION: XX.YY.ZZ] badges in markdown files
# 5. Write update.txt / update.xml
# 6. Create git tag vXX.YY.ZZ
# 7a. Patch: update existing GitHub Release for this minor
#
# ║ Minor releases only (patch == 00):
# 2. Create/update version/XX.YY branch (patches update in-place)
# 7b. Create new GitHub Release
#
# ╚════════════════════════════════════════════════════════════════════════╝
name: Build & Release
@@ -72,13 +70,13 @@ jobs:
cd /tmp/mokostandards
composer install --no-dev --no-interaction --quiet
# -- STEP 1: Read version -----------------------------------------------
# ── STEP 1: Read version ───────────────────────────────────────────
- name: "Step 1: Read version from README.md"
id: version
run: |
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null)
if [ -z "$VERSION" ]; then
echo "No VERSION in README.md — skipping release"
echo "⏭️ No VERSION in README.md — skipping release"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -86,34 +84,24 @@ jobs:
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "branch=version/${MINOR}" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch 00 = development — skipping release)"
echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "✅ Version: $VERSION (minor release — full pipeline)"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "01" ]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (first release — full pipeline)"
else
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch — platform version + badges only)"
fi
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "✅ Version: $VERSION (patch — platform version + badges only)"
fi
- name: Check if already released
if: steps.version.outputs.skip != 'true'
id: check
run: |
TAG="${{ steps.version.outputs.release_tag }}"
TAG="${{ steps.version.outputs.tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
TAG_EXISTS=false
@@ -131,109 +119,102 @@ jobs:
echo "already_released=false" >> "$GITHUB_OUTPUT"
fi
# -- SANITY CHECKS -------------------------------------------------------
- name: "Sanity: Pre-release validation"
# ── SANITY CHECKS ────────────────────────────────────────────────────
- name: "Sanity: Platform-specific validation"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null)
ERRORS=0
echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY
echo "## 🔍 Pre-Release Sanity Checks" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Platform: \`${PLATFORM}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# -- Version drift check (must pass before release) --------
README_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' README.md 2>/dev/null | head -1)
if [ "$README_VER" != "$VERSION" ]; then
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check CHANGELOG version matches
CL_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' CHANGELOG.md 2>/dev/null | head -1)
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
# Check composer.json version if present
if [ -f "composer.json" ]; then
COMP_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' composer.json 2>/dev/null | head -1)
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
fi
# Common checks
if [ ! -f "LICENSE" ]; then
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
echo " Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
echo " LICENSE" >> $GITHUB_STEP_SUMMARY
fi
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
if [ ! -d "src" ]; then
echo "⚠️ No src/ directory" >> $GITHUB_STEP_SUMMARY
else
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
echo "✅ src/ directory" >> $GITHUB_STEP_SUMMARY
fi
# -- Joomla: manifest version drift --------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
XML_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
# Dolibarr-specific checks
if [ "$PLATFORM" = "crm-module" ]; then
MOD_FILE=$(find src htdocs -path "*/core/modules/mod*.class.php" -print -quit 2>/dev/null)
if [ -z "$MOD_FILE" ]; then
echo "❌ No module descriptor (src/core/modules/mod*.class.php)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
echo " Module descriptor: \`${MOD_FILE}\`" >> $GITHUB_STEP_SUMMARY
# Check module number
NUMERO=$(grep -oP '\$this->numero\s*=\s*\K\d+' "$MOD_FILE" 2>/dev/null || echo "0")
if [ "$NUMERO" = "0" ] || [ -z "$NUMERO" ]; then
echo "❌ Module number (\$this->numero) is 0 or not set" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "✅ Module number: ${NUMERO}" >> $GITHUB_STEP_SUMMARY
fi
# Check url_last_version exists
if grep -q 'url_last_version' "$MOD_FILE" 2>/dev/null; then
echo "✅ url_last_version is set" >> $GITHUB_STEP_SUMMARY
else
echo "⚠️ url_last_version not set — update checks won't work" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
# -- Joomla: XML manifest existence --------
if [ -z "$MANIFEST" ]; then
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Joomla-specific checks
if [ "$PLATFORM" = "waas-component" ]; then
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "❌ No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "✅ Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# -- Joomla: extension type check --------
TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
# Check extension type
TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null)
echo " Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
else
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
# Always runs — every version change on main archives to version/XX.YY
- name: "Step 2: Version archive branch"
if: steps.check.outputs.already_released != 'true'
# ── STEP 2: Create or update version/XX.YY branch ──────────────────
- name: "Step 2: Version branch"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
BRANCH="${{ steps.version.outputs.branch }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}"
PATCH="${{ steps.version.outputs.version }}"
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
# Check if branch exists
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
git push origin HEAD:"$BRANCH" --force
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
else
if [ "$IS_MINOR" = "true" ]; then
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
git push origin "$BRANCH" --force
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
echo "🌿 Created branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
git push origin HEAD:"$BRANCH" --force
echo "📝 Updated branch: ${BRANCH} (patch)" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 3: Set platform version ----------------------------------------
# ── STEP 3: Set platform version ───────────────────────────────────
- name: "Step 3: Set platform version"
if: >-
steps.version.outputs.skip != 'true' &&
@@ -243,7 +224,7 @@ jobs:
php /tmp/mokostandards/api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
# -- STEP 4: Update version badges ----------------------------------------
# ── STEP 4: Update version badges ──────────────────────────────────
- name: "Step 4: Update version badges"
if: >-
steps.version.outputs.skip != 'true' &&
@@ -256,100 +237,107 @@ jobs:
fi
done
# -- STEP 5: Write update.xml (Joomla update server) ---------------------
- name: "Step 5: Write update.xml"
# ── STEP 5: Write update files (Dolibarr: update.txt / Joomla: update.xml)
- name: "Step 5: Write update files"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null)
VERSION="${{ steps.version.outputs.version }}"
REPO="${{ github.repository }}"
# -- Parse extension metadata from XML manifest ----------------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "Warning: No Joomla XML manifest found — skipping update.xml" >> $GITHUB_STEP_SUMMARY
exit 0
if [ "$PLATFORM" = "crm-module" ]; then
printf '%s' "$VERSION" > update.txt
echo "📦 update.txt: ${VERSION}" >> $GITHUB_STEP_SUMMARY
fi
EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}")
EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component")
EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "")
EXT_CLIENT=$(grep -oP '<extension[^>]+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
EXT_FOLDER=$(grep -oP '<extension[^>]+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
TARGET_PLATFORM=$(grep -oP '<targetplatform[^/]*/>' "$MANIFEST" 2>/dev/null | head -1 || echo "")
PHP_MINIMUM=$(grep -oP '<php_minimum>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "")
if [ "$PLATFORM" = "waas-component" ]; then
# ── Parse extension metadata from XML manifest ──────────────
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "⚠️ No Joomla XML manifest found — skipping update.xml" >> $GITHUB_STEP_SUMMARY
else
EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}")
EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component")
EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "")
EXT_CLIENT=$(grep -oP '<extension[^>]+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
EXT_FOLDER=$(grep -oP '<extension[^>]+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
TARGET_PLATFORM=$(grep -oP '<targetplatform[^/]*/>' "$MANIFEST" 2>/dev/null | head -1 || echo "")
PHP_MINIMUM=$(grep -oP '<php_minimum>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "")
# Derive element from manifest filename if not in XML
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml)
# Derive element from manifest filename if not in XML
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml)
fi
# Build client tag: plugins and frontend modules need <client>site</client>
CLIENT_TAG=""
if [ -n "$EXT_CLIENT" ]; then
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
CLIENT_TAG="<client>site</client>"
fi
# Build folder tag for plugins (required for Joomla to match the update)
FOLDER_TAG=""
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
fi
# Build targetplatform (fallback to Joomla 5+6 if not in manifest)
if [ -z "$TARGET_PLATFORM" ]; then
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
fi
# Build php_minimum tag
PHP_TAG=""
if [ -n "$PHP_MINIMUM" ]; then
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
fi
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}"
# ── Write update.xml (stable release) ───────────────────────
{
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' '<updates>'
printf '%s\n' ' <update>'
printf '%s\n' " <name>${EXT_NAME}</name>"
printf '%s\n' " <description>${EXT_NAME} update</description>"
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
printf '%s\n' " <type>${EXT_TYPE}</type>"
printf '%s\n' " <version>${VERSION}</version>"
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
printf '%s\n' ' <tags>'
printf '%s\n' ' <tag>stable</tag>'
printf '%s\n' ' </tags>'
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
printf '%s\n' ' <downloads>'
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
printf '%s\n' ' </downloads>'
printf '%s\n' " ${TARGET_PLATFORM}"
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
printf '%s\n' ' </update>'
printf '%s\n' '</updates>'
} > update.xml
echo "📦 update.xml: ${VERSION} (stable) — ${EXT_TYPE}/${EXT_ELEMENT}" >> $GITHUB_STEP_SUMMARY
fi
fi
# Build client tag: plugins and frontend modules need <client>site</client>
CLIENT_TAG=""
if [ -n "$EXT_CLIENT" ]; then
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
CLIENT_TAG="<client>site</client>"
fi
# Build folder tag for plugins (required for Joomla to match the update)
FOLDER_TAG=""
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
fi
# Build targetplatform (fallback to Joomla 5 if not in manifest)
if [ -z "$TARGET_PLATFORM" ]; then
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
fi
# Build php_minimum tag
PHP_TAG=""
if [ -n "$PHP_MINIMUM" ]; then
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
fi
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}"
# -- Write update.xml (stable release) --------------------------
{
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' '<updates>'
printf '%s\n' ' <update>'
printf '%s\n' " <name>${EXT_NAME}</name>"
printf '%s\n' " <description>${EXT_NAME} update</description>"
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
printf '%s\n' " <type>${EXT_TYPE}</type>"
printf '%s\n' " <version>${VERSION}</version>"
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
printf '%s\n' ' <tags>'
printf '%s\n' ' <tag>stable</tag>'
printf '%s\n' ' </tags>'
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
printf '%s\n' ' <downloads>'
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
printf '%s\n' ' </downloads>'
printf '%s\n' " ${TARGET_PLATFORM}"
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
printf '%s\n' ' </update>'
printf '%s\n' '</updates>'
} > update.xml
echo "update.xml: ${VERSION} (stable) — ${EXT_TYPE}/${EXT_ELEMENT}" >> $GITHUB_STEP_SUMMARY
# -- Commit all changes ---------------------------------------------------
# ── Commit all changes ─────────────────────────────────────────────
- name: Commit release changes
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
if git diff --quiet && git diff --cached --quiet; then
echo "No changes to commit"
echo " No changes to commit"
exit 0
fi
VERSION="${{ steps.version.outputs.version }}"
@@ -360,25 +348,18 @@ jobs:
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
# -- STEP 6: Create tag ---------------------------------------------------
# ── STEP 6: Create tag ─────────────────────────────────────────────
- name: "Step 6: Create git tag"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true' &&
steps.version.outputs.is_minor == 'true'
steps.check.outputs.tag_exists != 'true'
run: |
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
# Only create the major release tag if it doesn't exist yet
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
git tag "$RELEASE_TAG"
git push origin "$RELEASE_TAG"
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
fi
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
TAG="${{ steps.version.outputs.tag }}"
git tag "$TAG"
git push origin "$TAG"
echo "🏷️ Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 7: Create or update GitHub Release ------------------------------
# ── STEP 7: Create or update GitHub Release ──────────────────────────
- name: "Step 7: GitHub Release"
if: >-
steps.version.outputs.skip != 'true' &&
@@ -387,129 +368,67 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
TAG="${{ steps.version.outputs.tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
MAJOR="${{ steps.version.outputs.major }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}"
# Derive the minor version base (XX.YY.00)
MINOR_BASE=$(echo "$VERSION" | sed 's/\.[0-9]*$/.00/')
MINOR_TAG="v${MINOR_BASE}"
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
# Check if the major release already exists
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
if [ -z "$EXISTING" ]; then
# First release for this major
gh release create "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
if [ "$IS_MINOR" = "true" ]; then
# Minor release: create new GitHub Release
gh release create "$TAG" \
--title "${VERSION}" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH"
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
echo "🚀 Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
# Append version notes to existing major release
CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true)
{
echo "$CURRENT_NOTES"
echo ""
echo "---"
echo "### ${VERSION}"
echo ""
cat /tmp/release_notes.md
} > /tmp/updated_notes.md
# Patch release: update the existing minor release with new tag
# Find the latest release for this minor version
EXISTING=$(gh release view "$MINOR_TAG" --json tagName -q .tagName 2>/dev/null || true)
if [ -n "$EXISTING" ]; then
# Update existing release body with patch info
CURRENT_NOTES=$(gh release view "$MINOR_TAG" --json body -q .body 2>/dev/null || true)
{
echo "$CURRENT_NOTES"
echo ""
echo "---"
echo "### Patch ${VERSION}"
echo ""
cat /tmp/release_notes.md
} > /tmp/updated_notes.md
gh release edit "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/updated_notes.md
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
# Every patch builds an install-ready ZIP and uploads it to the minor release.
# Result: one Release per minor version with a ZIP for each patch.
- name: "Step 8: Build Joomla package and update checksum"
if: >-
steps.version.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}"
# All ZIPs upload to the major release tag (vXX)
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || {
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
exit 0
}
# Find extension element name from manifest
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
[ -z "$MANIFEST" ] && exit 0
EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
PACKAGE_NAME="${EXT_ELEMENT}-${VERSION}.zip"
# -- Build install-ready ZIP from src/ ----------------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; }
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_NAME}" . -x '*.git*' '*.DS_Store' 'Thumbs.db' '*.log'
cd ..
FILESIZE=$(stat -c%s "/tmp/${PACKAGE_NAME}" 2>/dev/null || stat -f%z "/tmp/${PACKAGE_NAME}" 2>/dev/null || echo "unknown")
# -- Calculate SHA-256 -------------------------------------------
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
# -- Upload ZIP to the minor release tag -------------------------
gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || {
echo "Could not upload with --clobber, retrying..."
gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" 2>/dev/null || true
}
# -- Update update.xml with SHA-256 for latest patch -------------
if [ -f "update.xml" ]; then
if grep -q '<sha256>' update.xml; then
sed -i "s|<sha256>.*</sha256>|<sha256>sha256:${SHA256}</sha256>|" update.xml
gh release edit "$MINOR_TAG" \
--title "${MINOR_BASE} (latest: ${VERSION})" \
--notes-file /tmp/updated_notes.md
echo "📝 Release updated: ${MINOR_BASE} → patch ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
sed -i "s|</downloads>|</downloads>\n <sha256>sha256:${SHA256}</sha256>|" update.xml
# No existing minor release found — create one for this patch
gh release create "$TAG" \
--title "${VERSION}" \
--notes-file /tmp/release_notes.md
echo "🚀 Release created: ${VERSION} (no minor release found)" >> $GITHUB_STEP_SUMMARY
fi
# Also update the download URL to point to this patch's ZIP
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
sed -i "s|<downloadurl[^>]*>[^<]*</downloadurl>|<downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>|" update.xml
git add update.xml
git commit -m "chore(release): SHA-256 + download URL for ${VERSION} [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" || true
git push || true
fi
echo "### Joomla Package" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${PACKAGE_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Size | ${FILESIZE} bytes |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | \`${RELEASE_TAG}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY
# -- Summary --------------------------------------------------------------
# ── Summary ────────────────────────────────────────────────────────
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.version.outputs.version }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "## ⏭️ Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/deploy-demo.yml.template
# VERSION: 04.05.13
# VERSION: 04.05.00
# BRIEF: SFTP deployment workflow for demo server — synced to all governed repos
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos.
# Port is resolved in order: DEMO_FTP_PORT variable → :port suffix in DEMO_FTP_HOST → 22.
@@ -296,12 +296,6 @@ jobs:
HOST="$HOST_RAW"
PORT="$PORT_VAR"
if [ -z "$HOST" ]; then
echo "⏭️ DEMO_FTP_HOST not configured — skipping demo deployment."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Priority 1 — explicit DEMO_FTP_PORT variable
if [ -n "$PORT" ]; then
echo " Using explicit DEMO_FTP_PORT=${PORT}"
@@ -323,7 +317,7 @@ jobs:
echo "SFTP target: ${HOST}:${PORT}"
- name: Build remote path
if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true'
if: steps.source.outputs.skip == 'false'
id: remote
env:
DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }}
@@ -332,9 +326,10 @@ jobs:
BASE="$DEMO_FTP_PATH"
if [ -z "$BASE" ]; then
echo "⏭️ DEMO_FTP_PATH not configured — skipping demo deployment."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
echo " DEMO_FTP_PATH is not set."
echo " Configure it as an org-level variable (Settings → Variables) and"
echo " ensure this repository has been granted access to it."
exit 1
fi
# DEMO_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
@@ -645,7 +640,7 @@ jobs:
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Create or update failure issue
if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true'
if: failure() && steps.remote.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/deploy-dev.yml.template
# VERSION: 04.05.13
# VERSION: 04.05.00
# BRIEF: SFTP deployment workflow for development server — synced to all governed repos
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos.
# Port is resolved in order: DEV_FTP_PORT variable → :port suffix in DEV_FTP_HOST → 22.

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/deploy-rs.yml.template
# VERSION: 04.05.13
# VERSION: 04.05.00
# BRIEF: SFTP deployment workflow for release staging server — synced to all governed repos
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-rs.yml in all governed repos.
# Port is resolved in order: RS_FTP_PORT variable → :port suffix in RS_FTP_HOST → 22.
@@ -296,12 +296,6 @@ jobs:
HOST="$HOST_RAW"
PORT="$PORT_VAR"
if [ -z "$HOST" ]; then
echo "⏭️ RS_FTP_HOST not configured — skipping RS deployment."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Priority 1 — explicit RS_FTP_PORT variable
if [ -n "$PORT" ]; then
echo " Using explicit RS_FTP_PORT=${PORT}"
@@ -323,7 +317,7 @@ jobs:
echo "SFTP target: ${HOST}:${PORT}"
- name: Build remote path
if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true'
if: steps.source.outputs.skip == 'false'
id: remote
env:
RS_FTP_PATH: ${{ vars.RS_FTP_PATH }}
@@ -332,9 +326,10 @@ jobs:
BASE="$RS_FTP_PATH"
if [ -z "$BASE" ]; then
echo "⏭️ RS_FTP_PATH not configured — skipping RS deployment."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
echo " RS_FTP_PATH is not set."
echo " Configure it as an org-level variable (Settings → Variables) and"
echo " ensure this repository has been granted access to it."
exit 1
fi
# RS_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
@@ -572,7 +567,7 @@ jobs:
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Create or update failure issue
if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true'
if: failure() && steps.remote.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Firewall
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template
# VERSION: 04.05.13
# VERSION: 04.05.00
# BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server
# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules.

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Maintenance
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/repository-cleanup.yml.template
# VERSION: 04.05.13
# VERSION: 04.05.00
# BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes
# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos.
# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch.

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template
# VERSION: 04.05.13
# VERSION: 04.05.00
# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers
# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos.
# README.md is the single source of truth for the repository version.

8
.gitignore vendored
View File

@@ -198,9 +198,5 @@ venv/
*.coverage
hypothesis/
# ============================================================
# Cassiopeia custom theme overrides
# ============================================================
/src/media/css/theme/dark.custom.css
/src/media/css/theme/light.custom.css
src/media/css/theme/dark.custom.css
src/media/css/theme/light.custom.css

View File

@@ -12,13 +12,48 @@
BRIEF: Changelog file documenting version history of MokoCassiopeia
-->
# Changelog — MokoCassiopeia (VERSION: 03.08.03)
# Changelog — MokoCassiopeia (VERSION: 03.09.02)
All notable changes to the MokoCassiopeia Joomla template are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] - 2026-04-02
### Added
- **Favicon configuration** — New "Favicon" tab in template config; upload a PNG and all favicon sizes are auto-generated via PHP GD (ICO, Apple Touch Icon 180px, Android Chrome 192/512px, site.webmanifest)
- **Module overrides** — 11 new `default.php` layout overrides for Joomla core modules: `mod_custom`, `mod_articles_latest`, `mod_articles_popular`, `mod_articles_news`, `mod_articles_category`, `mod_breadcrumbs`, `mod_footer`, `mod_login`, `mod_finder`, `mod_tags_popular`, `mod_tags_similar`, `mod_related_items`
- **Module title support** — All module overrides respect `$module->showtitle`, `header_tag`, `header_class`, and `moduleclass_sfx` parameters
- **Module CSS** — BEM-scoped styles for module titles, article lists, tag badges, search forms, login forms, breadcrumbs, and footer content
- **Hero card variables** — Full variable-driven hero system: `--hero-card-bg`, `--hero-card-color`, `--hero-card-overlay`, `--hero-card-border-radius`, `--hero-card-padding-x/y`, `--hero-card-max-width`, plus `--hero-alt-card-*` for secondary variant
- **Hero mobile breakpoint** — Photo background hidden on mobile (≤767.98px), hero card becomes full-bleed (100dvh, no border-radius)
- **CSS fallback values** — 1365 `var()` calls in template.css now include inline fallback values
- **Card border-radius** — `.card` now has `.25rem` fallback on `var(--card-border-radius)`
- **Usage section in README** — Added missing "Usage" section required by MokoStandards
### Changed
- **Button backgrounds** — `--btn-bg: transparent` changed to `var(--body-bg)` in dark and light themes
- **Offcanvas close button** — `.offcanvas-header .btn-close` now gets `background-color` from `--offcanvas-bg`
- **Custom template sync** — Both `dark.custom.css` and `light.custom.css` now contain all variables from their standard counterparts (was missing 223 variables)
- **Overlay layer** — Added `--hero-overlay-bg-position` and `--hero-overlay-bg-size` variables
- **Legacy CSS cleanup** — Removed vendor prefixes (`-webkit-box`, `-ms-flexbox`) from `.overlay` rules, replaced with modern flexbox
### Removed
- **FILE INFORMATION headers** — Stripped DEFGROUP/INGROUP/PATH/VERSION/BRIEF metadata from all PHP, CSS, JS, INI, and HTML files (kept in XML and README per policy)
- **Mobile overrides** — Deleted 26 `mobile.php` layout files and their empty parent directories
- **Joomla-specific gitignore entries** — Removed ~700 lines of Joomla CMS core paths from `.gitignore` (not applicable to a template repository)
### Fixed
- **CI: composer install** — Workflow `standards-compliance.yml` now conditionally runs `composer install` only when `composer.json` exists
- **CI: YAML syntax** — Fixed invalid YAML in `auto-update-sha.yml` caused by multiline commit message in run block
---
## [03.09.02] - 2026-03-26
### Added - Hero Variant System & Block Color System
@@ -37,13 +72,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#### Files Modified
- `src/media/css/template.css` — hero variant rules, block color `:nth-child()` rules, named override rules
- `src/templates/light.custom.css` — hero and block color variables (light mode)
- `src/templates/dark.custom.css` — hero and block color variables (dark mode)
- `docs/CSS_VARIABLES.md` — full variable reference for both systems
- `src/media/css/theme/light.standard.css` — hero and block color variables (light standard)
- `src/media/css/theme/dark.standard.css` — hero and block color variables (dark standard)
- `src/templates/light.custom.css` — hero and block color variables (light custom starter)
- `src/templates/dark.custom.css` — hero and block color variables (dark custom starter)
- `src/templateDetails.xml` — Theme Preview tab, hero/block note fields, scriptfile registration, version bump to 03.09.02
- `src/language/en-GB/tpl_mokocassiopeia.ini` — language strings for new admin fields (British English)
- `src/language/en-US/tpl_mokocassiopeia.ini` — language strings for new admin fields (American English)
- `docs/CSS_VARIABLES.md` — full variable reference for both systems, sync script documentation
- `CHANGELOG.md` — this entry
#### Files Added
- `src/templates/theme-test.html` — Bootstrap-style test page showing all CSS variables and new features
- `src/templates/theme-test.html` — Bootstrap-style test page with branded showcase, CSS variable swatches, hero demos, block color demos, and color test image
- `src/script.php` — Joomla install/update lifecycle script (runs CSS variable sync on upgrade, checks PHP/Joomla minimum versions)
- `src/sync_custom_vars.php` — CLI/library utility that detects missing CSS variables in user custom palettes and injects them
- `src/templates/brand-showcase.html` — Interactive color system gradients with hover pixel sampler, Bootstrap component showcase
#### Variable Audit
- All 20 hero/block variables confirmed present in all 4 theme files (light/dark standard + custom)
- No duplicate variable declarations found across any theme file
- `--gutter-x` references in template.css are self-scoped to grid containers (standard Bootstrap 5 behavior, not a `:root` variable)
---

View File

@@ -146,6 +146,20 @@ The template includes a dark mode toggle. Test it by:
---
## Usage
Once installed and set as the default site template, MokoCassiopeia works out of the box with Joomla's standard content and module system. Key usage points:
- **Template Options** — Configure via **System → Site Templates → MokoCassiopeia** (theme colours, layout, analytics, favicon, drawers)
- **Custom Colour Schemes** — Copy `templates/mokocassiopeia/templates/light.custom.css` or `dark.custom.css` to `media/templates/site/mokocassiopeia/css/theme/` and select "Custom" in the Theme tab
- **Custom CSS/JS** — Create `media/templates/site/mokocassiopeia/css/user.css` or `js/user.js` for site-specific overrides that survive template updates
- **Module Overrides** — The template includes overrides for common Joomla modules with consistent title rendering, Bootstrap 5 styling, and Font Awesome 7 icons
- **Dark Mode** — Enabled by default with a floating toggle button; respects system preference and persists via localStorage
See [Configuration](#-configuration) below for detailed parameter reference.
---
## ⚙️ Configuration
### Global Parameters

View File

@@ -10,7 +10,7 @@
INGROUP: MokoCassiopeia.Documentation
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
FILE: docs/CSS_VARIABLES.md
VERSION: 03.06.03
VERSION: 03.09.02
BRIEF: Complete CSS variable reference for MokoCassiopeia template
-->
@@ -60,6 +60,12 @@ To create custom color schemes:
4. **Note**: Custom files are gitignored and won't be committed to the repository
5. **On upgrade**: When the template is updated, `script.php` automatically runs `sync_custom_vars.php` to detect any new variables added to the starter templates and inject them into your existing custom palette files. Your existing values are never overwritten — only genuinely new variables are added. You can also run this manually:
```bash
php templates/mokocassiopeia/sync_custom_vars.php --dry-run # preview what would be added
php templates/mokocassiopeia/sync_custom_vars.php # apply missing variables
```
---
## Primary Brand Colors
@@ -1440,9 +1446,9 @@ These ensure optimal readability for links within alert boxes.
* Repository: [https://github.com/mokoconsulting-tech/MokoCassiopeia](https://github.com/mokoconsulting-tech/MokoCassiopeia)
* Path: /docs/CSS_VARIABLES.md
* Owner: Moko Consulting
* Version: 03.06.03
* Version: 03.09.02
* Status: Active
* Effective Date: 2026-01-30
* Effective Date: 2026-03-26
* Classification: Public Open Source Documentation
## Revision History

View File

@@ -1,19 +1,9 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
/* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoCassiopeia
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
PATH: ./templates/mokocassiopeia/component.php
VERSION: 03.06.02
BRIEF: Main template index file for MokoCassiopeia rendering site layout
*/
@@ -54,15 +44,6 @@ $sitename = htmlspecialchars($sitenameR, ENT_QUOTES, 'UTF-8');
$menu = $app->getMenu()->getActive();
$pageclass = $menu !== null ? $menu->getParams()->get('pageclass_sfx', '') : '';
// Respect “Site Name in Page Titles” (0:none, 1:before, 2:after)
$mode = (int) $app->get('sitename_pagetitles', 0);
$pageTitle = trim($this->getTitle());
$final = $pageTitle !== ''
? ($mode === 1 ? $sitenameR . ' - ' . $pageTitle
: ($mode === 2 ? $pageTitle . ' - ' . $sitenameR : $pageTitle))
: $sitenameR;
$this->setTitle($final);
// Template/Media path
$templatePath = 'media/templates/site/mokocassiopeia';

View File

@@ -1,19 +1,9 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
/* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoCassiopeia
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
PATH: ./templates/mokocassiopeia/custom.php
VERSION: 03.06.02
BRIEF: MokoCassiopeia with user-defined overrides
*/
function console_log($output, $with_script_tags = true) {

View File

@@ -1,19 +1,9 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
/* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoCassiopeia
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
PATH: ./templates/mokocassiopeia/error.php
VERSION: 03.06.02
BRIEF: Error page template file for MokoCassiopeia
*/
defined('_JEXEC') or die;
@@ -428,7 +418,7 @@ $wa->useScript('user.js'); // js/user.js
<?php if ($this->params->get('backTop') == 1) : ?>
<a href="#top" id="back-top" class="back-to-top-link" aria-label="<?php echo Text::_('TPL_MOKOCASSIOPEIA_BACKTOTOP'); ?>">
<span class="icon-arrow-up icon-fw" aria-hidden="true"></span>
<span class="fa-solid fa-arrow-up" aria-hidden="true"></span>
</a>
<?php endif; ?>

173
src/helper/favicon.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Favicon generator — creates ICO, Apple Touch Icon, and Android icons
* from a single source PNG uploaded via the template config.
*/
defined('_JEXEC') or die;
class MokoFaviconHelper
{
/**
* Sizes to generate: filename => [width, height, format].
* ICO embeds 16×16 and 32×32 internally.
*/
private const SIZES = [
'apple-touch-icon.png' => [180, 180, 'png'],
'favicon-32x32.png' => [32, 32, 'png'],
'favicon-16x16.png' => [16, 16, 'png'],
'android-chrome-192x192.png' => [192, 192, 'png'],
'android-chrome-512x512.png' => [512, 512, 'png'],
];
/**
* Generate all favicon files from a source PNG if they don't already exist
* or if the source has been modified since last generation.
*
* @param string $sourcePath Absolute path to the source PNG.
* @param string $outputDir Absolute path to the output directory.
*
* @return bool True if generation succeeded or files are up to date.
*/
public static function generate(string $sourcePath, string $outputDir): bool
{
if (!is_file($sourcePath) || !extension_loaded('gd')) {
return false;
}
if (!is_dir($outputDir)) {
mkdir($outputDir, 0755, true);
}
$sourceTime = filemtime($sourcePath);
$stampFile = $outputDir . '/.favicon_generated';
// Skip if already up to date
if (is_file($stampFile) && filemtime($stampFile) >= $sourceTime) {
return true;
}
$source = imagecreatefrompng($sourcePath);
if (!$source) {
return false;
}
imagealphablending($source, false);
imagesavealpha($source, true);
$srcW = imagesx($source);
$srcH = imagesy($source);
// Generate PNG sizes
foreach (self::SIZES as $filename => [$w, $h]) {
$resized = imagecreatetruecolor($w, $h);
imagealphablending($resized, false);
imagesavealpha($resized, true);
$transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
imagefill($resized, 0, 0, $transparent);
imagecopyresampled($resized, $source, 0, 0, 0, 0, $w, $h, $srcW, $srcH);
imagepng($resized, $outputDir . '/' . $filename, 9);
imagedestroy($resized);
}
// Generate ICO (contains 16×16 and 32×32)
self::generateIco($source, $srcW, $srcH, $outputDir . '/favicon.ico');
// Generate site.webmanifest
self::generateManifest($outputDir);
imagedestroy($source);
// Write timestamp stamp
file_put_contents($stampFile, date('c'));
return true;
}
/**
* Build a minimal ICO file containing 16×16 and 32×32 PNG entries.
*/
private static function generateIco(\GdImage $source, int $srcW, int $srcH, string $outPath): void
{
$entries = [];
foreach ([16, 32] as $size) {
$resized = imagecreatetruecolor($size, $size);
imagealphablending($resized, false);
imagesavealpha($resized, true);
$transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
imagefill($resized, 0, 0, $transparent);
imagecopyresampled($resized, $source, 0, 0, 0, 0, $size, $size, $srcW, $srcH);
ob_start();
imagepng($resized, null, 9);
$pngData = ob_get_clean();
imagedestroy($resized);
$entries[] = ['size' => $size, 'data' => $pngData];
}
// ICO header: 2 bytes reserved, 2 bytes type (1=ICO), 2 bytes count
$count = count($entries);
$ico = pack('vvv', 0, 1, $count);
// Calculate offset: header (6) + directory entries (16 each)
$offset = 6 + ($count * 16);
$imageData = '';
foreach ($entries as $entry) {
$size = $entry['size'] >= 256 ? 0 : $entry['size'];
$dataLen = strlen($entry['data']);
// ICONDIRENTRY: width, height, colors, reserved, planes, bpp, size, offset
$ico .= pack('CCCCvvVV', $size, $size, 0, 0, 1, 32, $dataLen, $offset);
$imageData .= $entry['data'];
$offset += $dataLen;
}
file_put_contents($outPath, $ico . $imageData);
}
/**
* Write a site.webmanifest for Android/PWA icon discovery.
*/
private static function generateManifest(string $outputDir): void
{
$manifest = [
'icons' => [
['src' => 'android-chrome-192x192.png', 'sizes' => '192x192', 'type' => 'image/png'],
['src' => 'android-chrome-512x512.png', 'sizes' => '512x512', 'type' => 'image/png'],
],
];
file_put_contents(
$outputDir . '/site.webmanifest',
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Return the <link> tags to inject into <head>.
*
* @param string $basePath URL path to the favicon directory (relative to site root).
*
* @return string HTML link tags.
*/
public static function getHeadTags(string $basePath): string
{
$basePath = rtrim($basePath, '/');
return '<link rel="apple-touch-icon" sizes="180x180" href="' . $basePath . '/apple-touch-icon.png">' . "\n"
. '<link rel="icon" type="image/png" sizes="32x32" href="' . $basePath . '/favicon-32x32.png">' . "\n"
. '<link rel="icon" type="image/png" sizes="16x16" href="' . $basePath . '/favicon-16x16.png">' . "\n"
. '<link rel="manifest" href="' . $basePath . '/site.webmanifest">' . "\n"
. '<link rel="shortcut icon" href="' . $basePath . '/favicon.ico">' . "\n";
}
}

162
src/helper/minify.php Normal file
View File

@@ -0,0 +1,162 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* CSS/JS minifier — generates .min files from source when dev mode is off,
* deletes them when dev mode is on.
*/
defined('_JEXEC') or die;
class MokoMinifyHelper
{
/**
* Files to minify: source path relative to template media root.
* The .min variant is derived automatically (template.css → template.min.css).
*/
private const CSS_FILES = [
'css/template.css',
'css/theme/light.standard.css',
'css/theme/dark.standard.css',
'css/theme/light.custom.css',
'css/theme/dark.custom.css',
];
private const JS_FILES = [
'js/template.js',
];
/**
* When dev mode is ON: delete all .min files.
* When dev mode is OFF: regenerate .min files if source is newer.
*
* @param string $mediaRoot Absolute path to the template media directory.
* @param bool $devMode Whether development mode is enabled.
*/
public static function sync(string $mediaRoot, bool $devMode): void
{
$mediaRoot = rtrim($mediaRoot, '/\\');
foreach (self::CSS_FILES as $relPath) {
$source = $mediaRoot . '/' . $relPath;
$min = self::minPath($source);
if ($devMode) {
self::deleteIfExists($min);
} else {
self::buildIfStale($source, $min, 'css');
}
}
foreach (self::JS_FILES as $relPath) {
$source = $mediaRoot . '/' . $relPath;
$min = self::minPath($source);
if ($devMode) {
self::deleteIfExists($min);
} else {
self::buildIfStale($source, $min, 'js');
}
}
}
/**
* Derive the .min path from a source path.
* template.css → template.min.css
*/
private static function minPath(string $path): string
{
$info = pathinfo($path);
return $info['dirname'] . '/' . $info['filename'] . '.min.' . $info['extension'];
}
/**
* Delete a file if it exists.
*/
private static function deleteIfExists(string $path): void
{
if (is_file($path)) {
@unlink($path);
}
}
/**
* Build the minified file if the source is newer or the min file is missing.
*/
private static function buildIfStale(string $source, string $min, string $type): void
{
if (!is_file($source)) {
return;
}
// Skip if min file exists and is newer than source
if (is_file($min) && filemtime($min) >= filemtime($source)) {
return;
}
$content = file_get_contents($source);
if ($content === false) {
return;
}
$minified = ($type === 'css')
? self::minifyCss($content)
: self::minifyJs($content);
file_put_contents($min, $minified);
}
/**
* Minify CSS by stripping comments, excess whitespace, and unnecessary characters.
*/
private static function minifyCss(string $css): string
{
// Remove comments (but keep IE hacks like /*\*/)
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
// Remove whitespace around { } : ; , > + ~
$css = preg_replace('/\s*([{}:;,>+~])\s*/', '$1', $css);
// Remove remaining newlines and tabs
$css = preg_replace('/\s+/', ' ', $css);
// Remove spaces around selectors
$css = str_replace(['{ ', ' {', '; ', ' ;'], ['{', '{', ';', ';'], $css);
// Remove trailing semicolons before closing braces
$css = str_replace(';}', '}', $css);
// Remove leading/trailing whitespace
return trim($css);
}
/**
* Minify JS by stripping single-line comments, multi-line comments,
* and collapsing whitespace. Preserves string literals.
*/
private static function minifyJs(string $js): string
{
// Remove multi-line comments
$js = preg_replace('!/\*.*?\*/!s', '', $js);
// Remove single-line comments (but not URLs like http://)
$js = preg_replace('!(?<=^|[\s;{}()\[\]])//[^\n]*!m', '', $js);
// Collapse whitespace
$js = preg_replace('/\s+/', ' ', $js);
// Remove spaces around operators and punctuation
$js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js);
// Restore necessary spaces (after keywords)
$js = preg_replace('/(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)([^\s;})><=!&|?:,])/', '$1 $2', $js);
return trim($js);
}
}

View File

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

View File

@@ -1,112 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage com_comprofiler
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Community Builder login view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
$return = $this->return ?? '';
$showRegisterLink = $this->showRegisterLink ?? true;
$showLostPasswordLink = $this->showLostPasswordLink ?? true;
?>
<div class="cb-login-responsive cb-component">
<div class="cb-login__container">
<div class="cb-login__header">
<h1 class="cb-login__title">
<?php echo Text::_('COM_COMPROFILER_LOGIN'); ?>
</h1>
</div>
<form action="<?php echo Route::_('index.php?option=com_comprofiler&view=login'); ?>"
method="post"
class="cb-login__form"
aria-label="<?php echo Text::_('COM_COMPROFILER_LOGIN_FORM'); ?>">
<div class="cb-login__field">
<label for="cb-username" class="cb-login__label">
<?php echo Text::_('COM_COMPROFILER_USERNAME'); ?>
<span class="cb-login__required" aria-label="<?php echo Text::_('COM_COMPROFILER_REQUIRED'); ?>">*</span>
</label>
<input type="text"
id="cb-username"
name="username"
class="cb-login__input"
required
aria-required="true"
autocomplete="username"
placeholder="<?php echo Text::_('COM_COMPROFILER_USERNAME'); ?>">
</div>
<div class="cb-login__field">
<label for="cb-password" class="cb-login__label">
<?php echo Text::_('COM_COMPROFILER_PASSWORD'); ?>
<span class="cb-login__required" aria-label="<?php echo Text::_('COM_COMPROFILER_REQUIRED'); ?>">*</span>
</label>
<input type="password"
id="cb-password"
name="passwd"
class="cb-login__input"
required
aria-required="true"
autocomplete="current-password"
placeholder="<?php echo Text::_('COM_COMPROFILER_PASSWORD'); ?>">
</div>
<?php if ($this->showRememberMe ?? true) : ?>
<div class="cb-login__remember">
<div class="form-check">
<input type="checkbox"
id="cb-remember"
name="remember"
class="form-check-input cb-login__remember-checkbox"
value="yes">
<label for="cb-remember" class="form-check-label cb-login__remember-label">
<?php echo Text::_('COM_COMPROFILER_REMEMBER_ME'); ?>
</label>
</div>
</div>
<?php endif; ?>
<div class="cb-login__actions">
<button type="submit" class="cb-login__btn cb-login__btn--submit btn btn-primary">
<span class="icon-lock" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_LOGIN'); ?>
</button>
</div>
<input type="hidden" name="task" value="login">
<input type="hidden" name="return" value="<?php echo htmlspecialchars($return, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo $this->token ?? ''; ?>
</form>
<div class="cb-login__links">
<?php if ($showRegisterLink) : ?>
<div class="cb-login__link">
<a href="<?php echo Route::_('index.php?option=com_comprofiler&view=registers'); ?>" class="cb-login__link-item">
<span class="icon-user-plus" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_REGISTER_NEW_ACCOUNT'); ?>
</a>
</div>
<?php endif; ?>
<?php if ($showLostPasswordLink) : ?>
<div class="cb-login__link">
<a href="<?php echo Route::_('index.php?option=com_comprofiler&view=lostpassword'); ?>" class="cb-login__link-item">
<span class="icon-question" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_FORGOT_PASSWORD'); ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>

View File

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

View File

@@ -1,136 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage com_comprofiler
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Community Builder registration view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
// Get form and fields
$form = $this->form ?? null;
$tabs = $this->tabs ?? null;
?>
<div class="cb-register-responsive cb-component">
<div class="cb-register__header">
<h1 class="cb-register__title">
<?php echo Text::_('COM_COMPROFILER_REGISTER'); ?>
</h1>
<?php if ($this->introduction ?? null) : ?>
<div class="cb-register__intro">
<?php echo $this->introduction; ?>
</div>
<?php endif; ?>
</div>
<?php if ($form) : ?>
<form action="<?php echo $this->action ?? ''; ?>"
method="post"
class="cb-register__form"
enctype="multipart/form-data"
aria-label="<?php echo Text::_('COM_COMPROFILER_REGISTRATION_FORM'); ?>">
<?php if ($tabs) : ?>
<?php foreach ($tabs as $tab) : ?>
<?php if (isset($tab->fields) && !empty($tab->fields)) : ?>
<fieldset class="cb-register__fieldset">
<?php if ($tab->title) : ?>
<legend class="cb-register__legend">
<?php echo htmlspecialchars($tab->title, ENT_QUOTES, 'UTF-8'); ?>
</legend>
<?php endif; ?>
<?php if ($tab->description) : ?>
<div class="cb-register__tab-description">
<?php echo $tab->description; ?>
</div>
<?php endif; ?>
<div class="cb-register__fields">
<?php foreach ($tab->fields as $field) : ?>
<div class="cb-register__field<?php echo $field->required ? ' cb-register__field--required' : ''; ?>">
<label for="<?php echo htmlspecialchars($field->name, ENT_QUOTES, 'UTF-8'); ?>"
class="cb-register__label">
<?php echo htmlspecialchars($field->title, ENT_QUOTES, 'UTF-8'); ?>
<?php if ($field->required) : ?>
<span class="cb-register__required" aria-label="<?php echo Text::_('COM_COMPROFILER_REQUIRED'); ?>">*</span>
<?php endif; ?>
</label>
<?php if ($field->description) : ?>
<div class="cb-register__field-description" id="<?php echo htmlspecialchars($field->name, ENT_QUOTES, 'UTF-8'); ?>-desc">
<?php echo $field->description; ?>
</div>
<?php endif; ?>
<div class="cb-register__input-wrapper">
<?php echo $field->input; ?>
</div>
<?php if (isset($field->error) && $field->error) : ?>
<div class="cb-register__error" role="alert">
<?php echo $field->error; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</fieldset>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($this->showCaptcha ?? false) : ?>
<div class="cb-register__captcha">
<?php echo $this->captcha; ?>
</div>
<?php endif; ?>
<?php if ($this->showTerms ?? false) : ?>
<div class="cb-register__terms">
<div class="form-check">
<input type="checkbox"
id="cb-terms"
name="agreedToTerms"
class="form-check-input cb-register__terms-checkbox"
required
aria-required="true"
aria-describedby="cb-terms-text">
<label for="cb-terms" class="form-check-label cb-register__terms-label" id="cb-terms-text">
<?php echo Text::_('COM_COMPROFILER_AGREE_TO_TERMS'); ?>
<a href="<?php echo $this->termsUrl ?? ''; ?>" target="_blank" rel="noopener noreferrer">
<?php echo Text::_('COM_COMPROFILER_TERMS_CONDITIONS'); ?>
</a>
</label>
</div>
</div>
<?php endif; ?>
<div class="cb-register__actions">
<button type="submit" class="cb-register__btn cb-register__btn--submit btn btn-primary">
<span class="icon-check" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_REGISTER_SUBMIT'); ?>
</button>
<a href="<?php echo $this->loginUrl ?? ''; ?>" class="cb-register__btn cb-register__btn--cancel btn btn-secondary">
<span class="icon-cancel" aria-hidden="true"></span>
<?php echo Text::_('JCANCEL'); ?>
</a>
</div>
<?php echo $this->token ?? ''; ?>
</form>
<?php else : ?>
<div class="alert alert-warning" role="alert">
<?php echo Text::_('COM_COMPROFILER_REGISTRATION_NOT_AVAILABLE'); ?>
</div>
<?php endif; ?>
</div>

View File

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

View File

@@ -1,100 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage com_comprofiler
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Community Builder user profile view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
// Get user object
$user = $this->user ?? null;
$tabs = $this->tabs ?? null;
?>
<div class="cb-profile-responsive cb-component">
<?php if ($user) : ?>
<div class="cb-profile__header">
<?php if ($user->getField('avatar', null, 'html', 'none', 'profile')) : ?>
<div class="cb-profile__avatar">
<?php echo $user->getField('avatar', null, 'html', 'none', 'profile'); ?>
</div>
<?php endif; ?>
<div class="cb-profile__header-info">
<h1 class="cb-profile__name">
<?php echo htmlspecialchars($user->getField('formatname', null, 'html', 'none', 'profile'), ENT_QUOTES, 'UTF-8'); ?>
</h1>
<?php if ($user->getField('onlinestatus', null, 'html', 'none', 'profile')) : ?>
<div class="cb-profile__status">
<?php echo $user->getField('onlinestatus', null, 'html', 'none', 'profile'); ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($tabs) : ?>
<div class="cb-profile__tabs">
<ul class="cb-profile__tabs-nav" role="tablist" aria-label="<?php echo Text::_('COM_COMPROFILER_PROFILE_TABS'); ?>">
<?php foreach ($tabs as $tab) : ?>
<?php if (isset($tab->fields) && !empty($tab->fields)) : ?>
<li class="cb-profile__tab-item" role="presentation">
<a href="#<?php echo htmlspecialchars($tab->id, ENT_QUOTES, 'UTF-8'); ?>"
class="cb-profile__tab-link"
role="tab"
aria-controls="<?php echo htmlspecialchars($tab->id, ENT_QUOTES, 'UTF-8'); ?>"
aria-selected="false">
<?php echo htmlspecialchars($tab->title, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
<div class="cb-profile__tabs-content">
<?php foreach ($tabs as $tab) : ?>
<?php if (isset($tab->fields) && !empty($tab->fields)) : ?>
<div id="<?php echo htmlspecialchars($tab->id, ENT_QUOTES, 'UTF-8'); ?>"
class="cb-profile__tab-pane"
role="tabpanel"
aria-labelledby="<?php echo htmlspecialchars($tab->id, ENT_QUOTES, 'UTF-8'); ?>-tab">
<?php if ($tab->description) : ?>
<div class="cb-profile__tab-description">
<?php echo $tab->description; ?>
</div>
<?php endif; ?>
<div class="cb-profile__fields">
<?php foreach ($tab->fields as $field) : ?>
<?php if ($field->value) : ?>
<div class="cb-profile__field">
<div class="cb-profile__field-label">
<?php echo htmlspecialchars($field->title, ENT_QUOTES, 'UTF-8'); ?>
</div>
<div class="cb-profile__field-value">
<?php echo $field->value; ?>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php else : ?>
<div class="alert alert-warning" role="alert">
<?php echo Text::_('COM_COMPROFILER_USER_NOT_FOUND'); ?>
</div>
<?php endif; ?>
</div>

View File

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

View File

@@ -1,123 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage com_comprofiler
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Community Builder users list view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
// Get users list
$users = $this->users ?? [];
$pagination = $this->pagination ?? null;
$search = $this->search ?? '';
$listid = $this->listid ?? 0;
?>
<div class="cb-userslist-responsive cb-component">
<div class="cb-userslist__header">
<h1 class="cb-userslist__title">
<?php echo Text::_('COM_COMPROFILER_USERLIST'); ?>
</h1>
<?php if ($this->showSearch ?? true) : ?>
<div class="cb-userslist__search">
<form action="<?php echo Route::_('index.php?option=com_comprofiler&view=userslist&listid=' . $listid); ?>"
method="post"
class="cb-userslist__search-form"
role="search"
aria-label="<?php echo Text::_('COM_COMPROFILER_SEARCH_USERS'); ?>">
<div class="cb-userslist__search-wrapper">
<label for="cb-userslist-search" class="visually-hidden">
<?php echo Text::_('COM_COMPROFILER_SEARCH'); ?>
</label>
<input type="search"
id="cb-userslist-search"
name="search"
class="cb-userslist__search-input"
placeholder="<?php echo Text::_('COM_COMPROFILER_SEARCH_USERS'); ?>"
value="<?php echo htmlspecialchars($search, ENT_QUOTES, 'UTF-8'); ?>"
aria-label="<?php echo Text::_('COM_COMPROFILER_SEARCH'); ?>">
<button type="submit"
class="cb-userslist__search-btn btn btn-primary"
aria-label="<?php echo Text::_('COM_COMPROFILER_SEARCH'); ?>">
<span class="icon-search" aria-hidden="true"></span>
<span class="cb-userslist__search-text"><?php echo Text::_('COM_COMPROFILER_SEARCH'); ?></span>
</button>
</div>
</form>
</div>
<?php endif; ?>
</div>
<?php if (!empty($users)) : ?>
<div class="cb-userslist__grid">
<?php foreach ($users as $user) : ?>
<div class="cb-userslist__user-card">
<?php if ($user->getField('avatar', null, 'html', 'none', 'list')) : ?>
<div class="cb-userslist__avatar">
<a href="<?php echo $user->getField('canvas', null, 'html', 'none', 'profile'); ?>"
aria-label="<?php echo Text::sprintf('COM_COMPROFILER_VIEW_PROFILE', htmlspecialchars($user->getField('formatname', null, 'html', 'none', 'list'), ENT_QUOTES, 'UTF-8')); ?>">
<?php echo $user->getField('avatar', null, 'html', 'none', 'list'); ?>
</a>
</div>
<?php endif; ?>
<div class="cb-userslist__user-info">
<h3 class="cb-userslist__username">
<a href="<?php echo $user->getField('canvas', null, 'html', 'none', 'profile'); ?>">
<?php echo htmlspecialchars($user->getField('formatname', null, 'html', 'none', 'list'), ENT_QUOTES, 'UTF-8'); ?>
</a>
</h3>
<?php if ($user->getField('onlinestatus', null, 'html', 'none', 'list')) : ?>
<div class="cb-userslist__status">
<?php echo $user->getField('onlinestatus', null, 'html', 'none', 'list'); ?>
</div>
<?php endif; ?>
<?php if (isset($user->fields) && !empty($user->fields)) : ?>
<div class="cb-userslist__fields">
<?php foreach ($user->fields as $field) : ?>
<?php if ($field->value) : ?>
<div class="cb-userslist__field">
<span class="cb-userslist__field-label"><?php echo htmlspecialchars($field->title, ENT_QUOTES, 'UTF-8'); ?>:</span>
<span class="cb-userslist__field-value"><?php echo $field->value; ?></span>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="cb-userslist__actions">
<a href="<?php echo $user->getField('canvas', null, 'html', 'none', 'profile'); ?>"
class="cb-userslist__btn btn btn-primary btn-sm">
<span class="icon-user" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_VIEW_PROFILE'); ?>
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if ($pagination) : ?>
<div class="cb-userslist__pagination">
<?php echo $pagination->getPagesLinks(); ?>
</div>
<?php endif; ?>
<?php else : ?>
<div class="alert alert-info" role="alert">
<?php echo Text::_('COM_COMPROFILER_NO_USERS_FOUND'); ?>
</div>
<?php endif; ?>
</div>

View File

@@ -1 +1,76 @@
<!DOCTYPE html><title></title>
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -1,17 +1,10 @@
<?php
/**
* @package Joomla.Site
* @subpackage Templates.MokoCassiopeia
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 3 or later; see LICENSE.txt
*
* FILE INFORMATION
* DEFGROUP: Joomla.Template.Site
* INGROUP: MokoCassiopeia
* PATH: ./templates/mokocassiopeia/html/com_content/article/toc-left.php
* VERSION: 03.06.02
* BRIEF: Article layout with table of contents on the left side using Bootstrap TOC
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
@@ -19,6 +12,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
// Load Bootstrap TOC assets
@@ -43,13 +37,13 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<div class="col-lg-3 col-md-4 order-md-1 mb-4">
<div class="sticky-top toc-wrapper" style="top: 20px;">
<nav id="toc" data-toggle="toc" class="toc-container">
<h5 class="toc-title"><?php echo HTMLHelper::_('string.truncate', $this->item->title, 50); ?></h5>
<h5 class="toc-title"><?php echo Text::_('TPL_MOKOCASSIOPEIA_TOC_TITLE'); ?></h5>
</nav>
</div>
</div>
<!-- Article Content -->
<div class="col-lg-9 col-md-8 order-md-2" data-toc-scope>
<div class="col-lg-9 col-md-8 order-md-2">
<meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>" />
<?php if ($this->params->get('show_page_heading')) : ?>
@@ -91,7 +85,7 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<div class="article-content" itemprop="articleBody">
<div class="article-content" itemprop="articleBody" data-toc-scope>
<?php echo $this->item->text; ?>
</div>

View File

@@ -1,17 +1,10 @@
<?php
/**
* @package Joomla.Site
* @subpackage Templates.MokoCassiopeia
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 3 or later; see LICENSE.txt
*
* FILE INFORMATION
* DEFGROUP: Joomla.Template.Site
* INGROUP: MokoCassiopeia
* PATH: ./templates/mokocassiopeia/html/com_content/article/toc-right.php
* VERSION: 03.06.02
* BRIEF: Article layout with table of contents on the right side using Bootstrap TOC
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
@@ -19,6 +12,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
// Load Bootstrap TOC assets
@@ -40,7 +34,7 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<div class="com-content-article item-page<?php echo $this->pageclass_sfx; ?>">
<div class="row">
<!-- Article Content -->
<div class="col-lg-9 col-md-8 order-md-1" data-toc-scope>
<div class="col-lg-9 col-md-8 order-md-1">
<meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>" />
<?php if ($this->params->get('show_page_heading')) : ?>
@@ -82,7 +76,7 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<div class="article-content" itemprop="articleBody">
<div class="article-content" itemprop="articleBody" data-toc-scope>
<?php echo $this->item->text; ?>
</div>
@@ -108,7 +102,7 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<div class="col-lg-3 col-md-4 order-md-2 mb-4">
<div class="sticky-top toc-wrapper" style="top: 20px;">
<nav id="toc" data-toggle="toc" class="toc-container">
<h5 class="toc-title"><?php echo HTMLHelper::_('string.truncate', $this->item->title, 50); ?></h5>
<h5 class="toc-title"><?php echo Text::_('TPL_MOKOCASSIOPEIA_TOC_TITLE'); ?></h5>
</nav>
</div>
</div>

View File

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

View File

@@ -1,167 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM calendar view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\HTML\HTMLHelper;
$events = $this->rows ?? [];
$date = $this->date ?? null;
$year = $this->year ?? date('Y');
$month = $this->month ?? date('m');
?>
<div class="jem-calendar-responsive jem-component">
<div class="jem-calendar__container">
<!-- Calendar Header -->
<div class="jem-calendar__header">
<h1 class="jem-calendar__title">
<?php echo Text::_('COM_JEM_CALENDAR'); ?>
</h1>
</div>
<!-- Calendar Navigation -->
<div class="jem-calendar__navigation">
<a href="<?php echo Route::_('index.php?option=com_jem&view=calendar&year=' . ($month == 1 ? $year - 1 : $year) . '&month=' . ($month == 1 ? 12 : $month - 1)); ?>"
class="jem-calendar__nav-button jem-calendar__nav-prev"
aria-label="<?php echo Text::_('COM_JEM_PREVIOUS_MONTH'); ?>">
<span aria-hidden="true">&#8249;</span>
</a>
<h2 class="jem-calendar__current-month">
<?php echo HTMLHelper::_('date', $year . '-' . $month . '-01', 'F Y'); ?>
</h2>
<a href="<?php echo Route::_('index.php?option=com_jem&view=calendar&year=' . ($month == 12 ? $year + 1 : $year) . '&month=' . ($month == 12 ? 1 : $month + 1)); ?>"
class="jem-calendar__nav-button jem-calendar__nav-next"
aria-label="<?php echo Text::_('COM_JEM_NEXT_MONTH'); ?>">
<span aria-hidden="true">&#8250;</span>
</a>
</div>
<!-- Calendar Grid -->
<div class="jem-calendar__grid">
<!-- Weekday Headers -->
<div class="jem-calendar__weekdays">
<?php
$weekDays = [
Text::_('SUN'),
Text::_('MON'),
Text::_('TUE'),
Text::_('WED'),
Text::_('THU'),
Text::_('FRI'),
Text::_('SAT')
];
foreach ($weekDays as $day) : ?>
<div class="jem-calendar__weekday">
<?php echo $day; ?>
</div>
<?php endforeach; ?>
</div>
<!-- Calendar Days -->
<div class="jem-calendar__days">
<?php
// Generate calendar days
$firstDay = mktime(0, 0, 0, $month, 1, $year);
$daysInMonth = date('t', $firstDay);
$dayOfWeek = date('w', $firstDay);
// Empty cells before first day
for ($i = 0; $i < $dayOfWeek; $i++) : ?>
<div class="jem-calendar__day jem-calendar__day--empty"></div>
<?php endfor;
// Days with events
for ($day = 1; $day <= $daysInMonth; $day++) :
$currentDate = sprintf('%04d-%02d-%02d', $year, $month, $day);
$hasEvents = false;
$dayEvents = [];
// Check for events on this day
if (!empty($events)) {
foreach ($events as $event) {
if (!empty($event->dates) && date('Y-m-d', strtotime($event->dates)) == $currentDate) {
$hasEvents = true;
$dayEvents[] = $event;
}
}
}
$isToday = ($currentDate == date('Y-m-d'));
$classes = 'jem-calendar__day';
if ($hasEvents) {
$classes .= ' jem-calendar__day--has-events';
}
if ($isToday) {
$classes .= ' jem-calendar__day--today';
}
?>
<div class="<?php echo $classes; ?>" data-date="<?php echo $currentDate; ?>">
<div class="jem-calendar__day-number">
<?php echo $day; ?>
</div>
<?php if ($hasEvents) : ?>
<div class="jem-calendar__day-events">
<span class="jem-calendar__event-indicator"
aria-label="<?php echo Text::sprintf('COM_JEM_EVENTS_COUNT', count($dayEvents)); ?>">
<?php echo count($dayEvents); ?>
</span>
</div>
<?php endif; ?>
</div>
<?php endfor; ?>
</div>
</div>
<!-- Events List for Selected/Current Day -->
<?php if (!empty($events)) : ?>
<div class="jem-calendar__events-list">
<h3 class="jem-calendar__events-title">
<?php echo Text::_('COM_JEM_UPCOMING_EVENTS'); ?>
</h3>
<div class="jem-calendar__events">
<?php foreach ($events as $event) : ?>
<div class="jem-calendar__event-item">
<div class="jem-calendar__event-date">
<?php if (!empty($event->dates)) : ?>
<time datetime="<?php echo $this->escape($event->dates); ?>">
<?php echo HTMLHelper::_('date', $event->dates, Text::_('DATE_FORMAT_LC4')); ?>
</time>
<?php endif; ?>
</div>
<h4 class="jem-calendar__event-title">
<?php if (!empty($event->slug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=event&id=' . $event->slug); ?>"
class="jem-calendar__event-link">
<?php echo $this->escape($event->title); ?>
</a>
<?php else : ?>
<?php echo $this->escape($event->title); ?>
<?php endif; ?>
</h4>
<?php if (!empty($event->venue)) : ?>
<div class="jem-calendar__event-venue">
📍 <?php echo $this->escape($event->venue); ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>

View File

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

View File

@@ -1,111 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM categories view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
$categories = $this->categories ?? [];
?>
<div class="jem-categories-responsive jem-component">
<div class="jem-categories__container">
<!-- Categories Header -->
<div class="jem-categories__header">
<h1 class="jem-categories__title">
<?php echo Text::_('COM_JEM_CATEGORIES'); ?>
</h1>
</div>
<?php if (!empty($categories)) : ?>
<div class="jem-categories__list">
<?php foreach ($categories as $category) : ?>
<div class="jem-categories__item">
<div class="jem-categories__item-inner">
<!-- Category Image -->
<?php if (!empty($category->image)) : ?>
<div class="jem-categories__image-wrapper">
<img src="<?php echo $this->escape($category->image); ?>"
alt="<?php echo $this->escape($category->catname); ?>"
class="jem-categories__image"
loading="lazy">
</div>
<?php endif; ?>
<!-- Category Content -->
<div class="jem-categories__content">
<!-- Category Title -->
<h2 class="jem-categories__category-title">
<?php if (!empty($category->slug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=category&id=' . $category->slug); ?>"
class="jem-categories__link">
<?php echo $this->escape($category->catname); ?>
</a>
<?php else : ?>
<?php echo $this->escape($category->catname); ?>
<?php endif; ?>
</h2>
<!-- Category Description -->
<?php if (!empty($category->catdescription)) : ?>
<div class="jem-categories__description">
<?php echo $category->catdescription; ?>
</div>
<?php endif; ?>
<!-- Event Count -->
<?php if (isset($category->eventcount)) : ?>
<div class="jem-categories__meta">
<span class="jem-categories__event-count">
<?php echo Text::sprintf('COM_JEM_EVENTS_COUNT_FULL', (int) $category->eventcount); ?>
</span>
</div>
<?php endif; ?>
<!-- View Category Button -->
<?php if (!empty($category->slug)) : ?>
<div class="jem-categories__actions">
<a href="<?php echo Route::_('index.php?option=com_jem&view=category&id=' . $category->slug); ?>"
class="jem-categories__button btn btn-primary"
aria-label="<?php echo Text::sprintf('COM_JEM_VIEW_CATEGORY', $this->escape($category->catname)); ?>">
<?php echo Text::_('COM_JEM_VIEW_CATEGORY'); ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if (!empty($this->pagination)) : ?>
<div class="jem-categories__pagination">
<?php echo $this->pagination->getPagesLinks(); ?>
</div>
<?php endif; ?>
<?php else : ?>
<div class="jem-categories__empty">
<p class="jem-categories__empty-message">
<?php echo Text::_('COM_JEM_NO_CATEGORIES'); ?>
</p>
</div>
<?php endif; ?>
</div>
</div>

View File

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

View File

@@ -1,212 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM event details view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\HTML\HTMLHelper;
$item = $this->item ?? null;
$params = $this->params ?? null;
if (!$item) {
return;
}
?>
<div class="jem-event-responsive jem-component">
<div class="jem-event__container">
<!-- Event Header -->
<div class="jem-event__header">
<h1 class="jem-event__title">
<?php echo $this->escape($item->title); ?>
</h1>
</div>
<!-- Event Image -->
<?php if (!empty($item->datimage)) : ?>
<div class="jem-event__image-wrapper">
<img src="<?php echo $this->escape($item->datimage); ?>"
alt="<?php echo $this->escape($item->title); ?>"
class="jem-event__image"
loading="lazy">
</div>
<?php endif; ?>
<!-- Event Meta Information -->
<div class="jem-event__meta">
<!-- Date and Time -->
<div class="jem-event__meta-item jem-event__date">
<span class="jem-event__meta-icon" aria-hidden="true">📅</span>
<div class="jem-event__meta-content">
<strong class="jem-event__meta-label">
<?php echo Text::_('COM_JEM_DATE'); ?>:
</strong>
<?php if (!empty($item->dates)) : ?>
<time datetime="<?php echo $this->escape($item->dates); ?>"
class="jem-event__datetime">
<?php echo HTMLHelper::_('date', $item->dates, Text::_('DATE_FORMAT_LC3')); ?>
</time>
<?php endif; ?>
<?php if (!empty($item->enddates) && $item->enddates != $item->dates) : ?>
<span class="jem-event__date-separator"> - </span>
<time datetime="<?php echo $this->escape($item->enddates); ?>"
class="jem-event__datetime">
<?php echo HTMLHelper::_('date', $item->enddates, Text::_('DATE_FORMAT_LC3')); ?>
</time>
<?php endif; ?>
</div>
</div>
<!-- Time -->
<?php if (!empty($item->times)) : ?>
<div class="jem-event__meta-item jem-event__time">
<span class="jem-event__meta-icon" aria-hidden="true">🕐</span>
<div class="jem-event__meta-content">
<strong class="jem-event__meta-label">
<?php echo Text::_('COM_JEM_TIME'); ?>:
</strong>
<span class="jem-event__time-value">
<?php echo $this->escape($item->times); ?>
<?php if (!empty($item->endtimes)) : ?>
- <?php echo $this->escape($item->endtimes); ?>
<?php endif; ?>
</span>
</div>
</div>
<?php endif; ?>
<!-- Venue -->
<?php if (!empty($item->venue)) : ?>
<div class="jem-event__meta-item jem-event__venue">
<span class="jem-event__meta-icon" aria-hidden="true">📍</span>
<div class="jem-event__meta-content">
<strong class="jem-event__meta-label">
<?php echo Text::_('COM_JEM_VENUE'); ?>:
</strong>
<?php if (!empty($item->venueslug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=venue&id=' . $item->venueslug); ?>"
class="jem-event__venue-link">
<?php echo $this->escape($item->venue); ?>
</a>
<?php else : ?>
<span class="jem-event__venue-name">
<?php echo $this->escape($item->venue); ?>
</span>
<?php endif; ?>
<?php if (!empty($item->street) || !empty($item->city)) : ?>
<div class="jem-event__address">
<?php if (!empty($item->street)) : ?>
<span class="jem-event__street">
<?php echo $this->escape($item->street); ?>
</span>
<?php endif; ?>
<?php if (!empty($item->city)) : ?>
<span class="jem-event__city">
<?php echo $this->escape($item->city); ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Categories -->
<?php if (!empty($item->categories)) : ?>
<div class="jem-event__meta-item jem-event__categories">
<span class="jem-event__meta-icon" aria-hidden="true">🏷️</span>
<div class="jem-event__meta-content">
<strong class="jem-event__meta-label">
<?php echo Text::_('COM_JEM_CATEGORIES'); ?>:
</strong>
<div class="jem-event__category-list">
<?php foreach ($item->categories as $category) : ?>
<span class="jem-event__category-badge">
<?php echo $this->escape($category->catname); ?>
</span>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Event Description -->
<?php if (!empty($item->fulltext)) : ?>
<div class="jem-event__description">
<h2 class="jem-event__description-title">
<?php echo Text::_('COM_JEM_DESCRIPTION'); ?>
</h2>
<div class="jem-event__description-content">
<?php echo $item->fulltext; ?>
</div>
</div>
<?php endif; ?>
<!-- Event Registration -->
<?php if (!empty($item->registra) && $item->registra == 1) : ?>
<div class="jem-event__registration">
<h2 class="jem-event__registration-title">
<?php echo Text::_('COM_JEM_REGISTRATION'); ?>
</h2>
<?php if (!empty($item->maxplaces)) : ?>
<p class="jem-event__capacity">
<strong><?php echo Text::_('COM_JEM_MAX_PLACES'); ?>:</strong>
<?php echo (int) $item->maxplaces; ?>
</p>
<?php endif; ?>
<?php if (!empty($item->waitinglist)) : ?>
<p class="jem-event__waitinglist">
<?php echo Text::_('COM_JEM_WAITING_LIST_ENABLED'); ?>
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Event Contact -->
<?php if (!empty($item->contactname)) : ?>
<div class="jem-event__contact">
<h2 class="jem-event__contact-title">
<?php echo Text::_('COM_JEM_CONTACT'); ?>
</h2>
<p class="jem-event__contact-info">
<strong><?php echo Text::_('COM_JEM_NAME'); ?>:</strong>
<?php echo $this->escape($item->contactname); ?>
</p>
<?php if (!empty($item->contactemail)) : ?>
<p class="jem-event__contact-info">
<strong><?php echo Text::_('COM_JEM_EMAIL'); ?>:</strong>
<a href="mailto:<?php echo $this->escape($item->contactemail); ?>"
class="jem-event__contact-link">
<?php echo $this->escape($item->contactemail); ?>
</a>
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Back Button -->
<div class="jem-event__actions">
<a href="<?php echo Route::_('index.php?option=com_jem&view=eventslist'); ?>"
class="jem-event__button btn btn-secondary">
<?php echo Text::_('COM_JEM_BACK_TO_EVENTS'); ?>
</a>
</div>
</div>
</div>

View File

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

View File

@@ -1,147 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM events list view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\HTML\HTMLHelper;
// Load JEM helper if available
if (file_exists(JPATH_SITE . '/components/com_jem/helpers/helper.php')) {
require_once JPATH_SITE . '/components/com_jem/helpers/helper.php';
}
$items = $this->items ?? [];
$params = $this->params ?? null;
?>
<div class="jem-eventslist-responsive jem-component">
<div class="jem-eventslist__container">
<?php if (!empty($this->pageheading)) : ?>
<div class="jem-eventslist__header">
<h1 class="jem-eventslist__title">
<?php echo $this->escape($this->pageheading); ?>
</h1>
</div>
<?php endif; ?>
<?php if (!empty($items)) : ?>
<div class="jem-eventslist__list">
<?php foreach ($items as $item) : ?>
<div class="jem-eventslist__item">
<div class="jem-eventslist__item-inner">
<!-- Event Date -->
<div class="jem-eventslist__date">
<?php if (!empty($item->dates)) : ?>
<time datetime="<?php echo $this->escape($item->dates); ?>"
class="jem-eventslist__datetime">
<?php echo HTMLHelper::_('date', $item->dates, Text::_('DATE_FORMAT_LC4')); ?>
</time>
<?php endif; ?>
<?php if (!empty($item->enddates) && $item->enddates != $item->dates) : ?>
<span class="jem-eventslist__date-separator"> - </span>
<time datetime="<?php echo $this->escape($item->enddates); ?>"
class="jem-eventslist__datetime">
<?php echo HTMLHelper::_('date', $item->enddates, Text::_('DATE_FORMAT_LC4')); ?>
</time>
<?php endif; ?>
</div>
<!-- Event Title -->
<h2 class="jem-eventslist__event-title">
<?php if (!empty($item->slug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=event&id=' . $item->slug); ?>"
class="jem-eventslist__link">
<?php echo $this->escape($item->title); ?>
</a>
<?php else : ?>
<?php echo $this->escape($item->title); ?>
<?php endif; ?>
</h2>
<!-- Event Venue -->
<?php if (!empty($item->venue)) : ?>
<div class="jem-eventslist__venue">
<span class="jem-eventslist__venue-icon" aria-hidden="true">📍</span>
<?php if (!empty($item->venueslug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=venue&id=' . $item->venueslug); ?>"
class="jem-eventslist__venue-link">
<?php echo $this->escape($item->venue); ?>
</a>
<?php else : ?>
<span class="jem-eventslist__venue-name">
<?php echo $this->escape($item->venue); ?>
</span>
<?php endif; ?>
<?php if (!empty($item->city)) : ?>
<span class="jem-eventslist__city">
, <?php echo $this->escape($item->city); ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Event Description -->
<?php if (!empty($item->introtext)) : ?>
<div class="jem-eventslist__description">
<?php echo $item->introtext; ?>
</div>
<?php endif; ?>
<!-- Event Categories -->
<?php if (!empty($item->categories)) : ?>
<div class="jem-eventslist__categories">
<?php foreach ($item->categories as $category) : ?>
<span class="jem-eventslist__category-badge">
<?php echo $this->escape($category->catname); ?>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Read More Button -->
<?php if (!empty($item->slug)) : ?>
<div class="jem-eventslist__actions">
<a href="<?php echo Route::_('index.php?option=com_jem&view=event&id=' . $item->slug); ?>"
class="jem-eventslist__button btn btn-primary"
aria-label="<?php echo Text::sprintf('COM_JEM_READ_MORE_ABOUT', $this->escape($item->title)); ?>">
<?php echo Text::_('COM_JEM_READ_MORE'); ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if (!empty($this->pagination)) : ?>
<div class="jem-eventslist__pagination">
<?php echo $this->pagination->getPagesLinks(); ?>
</div>
<?php endif; ?>
<?php else : ?>
<div class="jem-eventslist__empty">
<p class="jem-eventslist__empty-message">
<?php echo Text::_('COM_JEM_NO_EVENTS'); ?>
</p>
</div>
<?php endif; ?>
</div>
</div>

View File

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

View File

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

View File

@@ -1,188 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM venue view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\HTML\HTMLHelper;
$venue = $this->venue ?? null;
$events = $this->rows ?? [];
if (!$venue) {
return;
}
?>
<div class="jem-venue-responsive jem-component">
<div class="jem-venue__container">
<!-- Venue Header -->
<div class="jem-venue__header">
<h1 class="jem-venue__title">
<?php echo $this->escape($venue->venue); ?>
</h1>
</div>
<!-- Venue Image -->
<?php if (!empty($venue->locimage)) : ?>
<div class="jem-venue__image-wrapper">
<img src="<?php echo $this->escape($venue->locimage); ?>"
alt="<?php echo $this->escape($venue->venue); ?>"
class="jem-venue__image"
loading="lazy">
</div>
<?php endif; ?>
<!-- Venue Information -->
<div class="jem-venue__info">
<!-- Address -->
<?php if (!empty($venue->street) || !empty($venue->city) || !empty($venue->postalCode)) : ?>
<div class="jem-venue__info-item jem-venue__address">
<span class="jem-venue__info-icon" aria-hidden="true">📍</span>
<div class="jem-venue__info-content">
<strong class="jem-venue__info-label">
<?php echo Text::_('COM_JEM_ADDRESS'); ?>:
</strong>
<address class="jem-venue__address-content">
<?php if (!empty($venue->street)) : ?>
<div class="jem-venue__street">
<?php echo $this->escape($venue->street); ?>
</div>
<?php endif; ?>
<?php if (!empty($venue->postalCode) || !empty($venue->city)) : ?>
<div class="jem-venue__city-line">
<?php if (!empty($venue->postalCode)) : ?>
<span class="jem-venue__postal">
<?php echo $this->escape($venue->postalCode); ?>
</span>
<?php endif; ?>
<?php if (!empty($venue->city)) : ?>
<span class="jem-venue__city">
<?php echo $this->escape($venue->city); ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($venue->state)) : ?>
<div class="jem-venue__state">
<?php echo $this->escape($venue->state); ?>
</div>
<?php endif; ?>
<?php if (!empty($venue->country)) : ?>
<div class="jem-venue__country">
<?php echo $this->escape($venue->country); ?>
</div>
<?php endif; ?>
</address>
</div>
</div>
<?php endif; ?>
<!-- Website -->
<?php if (!empty($venue->url)) : ?>
<div class="jem-venue__info-item jem-venue__website">
<span class="jem-venue__info-icon" aria-hidden="true">🌐</span>
<div class="jem-venue__info-content">
<strong class="jem-venue__info-label">
<?php echo Text::_('COM_JEM_WEBSITE'); ?>:
</strong>
<a href="<?php echo $this->escape($venue->url); ?>"
target="_blank"
rel="noopener noreferrer"
class="jem-venue__link">
<?php echo $this->escape($venue->url); ?>
</a>
</div>
</div>
<?php endif; ?>
<!-- Description -->
<?php if (!empty($venue->locdescription)) : ?>
<div class="jem-venue__description">
<h2 class="jem-venue__description-title">
<?php echo Text::_('COM_JEM_DESCRIPTION'); ?>
</h2>
<div class="jem-venue__description-content">
<?php echo $venue->locdescription; ?>
</div>
</div>
<?php endif; ?>
</div>
<!-- Map -->
<?php if (!empty($venue->latitude) && !empty($venue->longitude)) : ?>
<div class="jem-venue__map">
<h2 class="jem-venue__map-title">
<?php echo Text::_('COM_JEM_LOCATION'); ?>
</h2>
<div class="jem-venue__map-container">
<!-- Map would be rendered here by JEM's map functionality -->
<div class="jem-venue__map-placeholder">
<p><?php echo Text::_('COM_JEM_MAP_VIEW'); ?></p>
<p>
<a href="https://www.google.com/maps?q=<?php echo $venue->latitude; ?>,<?php echo $venue->longitude; ?>"
target="_blank"
rel="noopener noreferrer"
class="jem-venue__map-link btn btn-primary">
<?php echo Text::_('COM_JEM_VIEW_ON_MAP'); ?>
</a>
</p>
</div>
</div>
</div>
<?php endif; ?>
<!-- Events at this Venue -->
<?php if (!empty($events)) : ?>
<div class="jem-venue__events">
<h2 class="jem-venue__events-title">
<?php echo Text::_('COM_JEM_EVENTS_AT_VENUE'); ?>
</h2>
<div class="jem-venue__events-list">
<?php foreach ($events as $event) : ?>
<div class="jem-venue__event-item">
<div class="jem-venue__event-date">
<?php if (!empty($event->dates)) : ?>
<time datetime="<?php echo $this->escape($event->dates); ?>">
<?php echo HTMLHelper::_('date', $event->dates, Text::_('DATE_FORMAT_LC4')); ?>
</time>
<?php endif; ?>
</div>
<h3 class="jem-venue__event-title">
<?php if (!empty($event->slug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=event&id=' . $event->slug); ?>"
class="jem-venue__event-link">
<?php echo $this->escape($event->title); ?>
</a>
<?php else : ?>
<?php echo $this->escape($event->title); ?>
<?php endif; ?>
</h3>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Back Button -->
<div class="jem-venue__actions">
<a href="<?php echo Route::_('index.php?option=com_jem&view=eventslist'); ?>"
class="jem-venue__button btn btn-secondary">
<?php echo Text::_('COM_JEM_BACK_TO_EVENTS'); ?>
</a>
</div>
</div>
</div>

View File

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

View File

@@ -1,70 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage com_kunena
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Kunena category list
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$this->document->addStyleDeclaration('
.kunena-category-list-responsive {
width: 100%;
}
.kunena-category-responsive {
background: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
transition: all 0.2s;
}
.kunena-category-responsive:hover {
background: var(--secondary-bg);
border-color: var(--color-primary);
}
@media (max-width: 575.98px) {
.kunena-category-responsive {
padding: 0.75rem;
}
}
');
?>
<div class="kunena-category-list-responsive">
<?php if (!empty($this->categories)) : ?>
<?php foreach ($this->categories as $category) : ?>
<div class="kunena-category-responsive">
<h3 class="kunena-category__title">
<a href="<?php echo $category->getUrl(); ?>">
<?php echo $this->escape($category->name); ?>
</a>
</h3>
<?php if ($category->description) : ?>
<div class="kunena-category__description">
<?php echo $category->displayField('description'); ?>
</div>
<?php endif; ?>
<div class="kunena-category__meta">
<span><?php echo Text::_('COM_KUNENA_TOPICS'); ?>: <?php echo $category->numTopics; ?></span>
<span><?php echo Text::_('COM_KUNENA_POSTS'); ?>: <?php echo $category->numPosts; ?></span>
</div>
</div>
<?php endforeach; ?>
<?php else : ?>
<div class="alert alert-info">
<?php echo Text::_('COM_KUNENA_NO_CATEGORIES'); ?>
</div>
<?php endif; ?>
</div>

View File

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

View File

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

View File

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

View File

@@ -1,141 +0,0 @@
<?php
/**
* @package OS Membership Pro
* @subpackage com_osmembership
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for OS Membership plans list
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$this->document->addStyleDeclaration('
.osmembership-plans-responsive {
display: grid;
gap: 2rem;
grid-template-columns: 1fr;
}
.osmembership-plan-card {
background: var(--body-bg);
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
padding: 2rem;
transition: all 0.3s;
display: flex;
flex-direction: column;
}
.osmembership-plan-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
border-color: var(--color-primary);
}
.osmembership-plan-card--featured {
border-color: var(--color-primary);
position: relative;
}
.osmembership-plan-card--featured::before {
content: "' . Text::_('OSM_POPULAR') . '";
position: absolute;
top: -12px;
right: 20px;
background: var(--color-primary);
color: white;
padding: 0.25rem 1rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 600;
}
@media (min-width: 768px) {
.osmembership-plans-responsive {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 992px) {
.osmembership-plans-responsive {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1200px) {
.osmembership-plans-responsive.osmembership-plans--many {
grid-template-columns: repeat(4, 1fr);
}
}
');
?>
<div class="osmembership-plans-responsive <?php echo count($this->items) > 3 ? 'osmembership-plans--many' : ''; ?>">
<?php foreach ($this->items as $item) : ?>
<div class="osmembership-plan-card <?php echo $item->featured ? 'osmembership-plan-card--featured' : ''; ?>">
<?php if (!empty($item->image)) : ?>
<div class="osmembership-plan__image" style="margin-bottom: 1.5rem;">
<img src="<?php echo $item->image; ?>"
alt="<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>"
style="width: 100%; height: auto; border-radius: var(--border-radius);" />
</div>
<?php endif; ?>
<h2 class="osmembership-plan__title" style="margin: 0 0 1rem 0; font-size: 1.75rem; font-weight: 700;">
<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>
</h2>
<div class="osmembership-plan__pricing" style="margin-bottom: 1.5rem;">
<?php if ($item->price > 0) : ?>
<div style="font-size: 2.5rem; font-weight: 700; color: var(--color-primary); line-height: 1;">
<span style="font-size: 1.5rem; vertical-align: super;"><?php echo $this->config->currency_symbol; ?></span>
<?php echo number_format($item->price, 0); ?>
</div>
<?php if ($item->subscription_length > 0) : ?>
<div style="color: var(--gray-600); margin-top: 0.5rem;">
<?php echo Text::_('OSM_PER') . ' ' . $item->subscription_length . ' ' . Text::_('OSM_' . strtoupper($item->subscription_length_unit)); ?>
</div>
<?php endif; ?>
<?php else : ?>
<div style="font-size: 2.5rem; font-weight: 700; color: var(--success);">
<?php echo Text::_('OSM_FREE'); ?>
</div>
<?php endif; ?>
</div>
<?php if (!empty($item->short_description)) : ?>
<div class="osmembership-plan__description" style="margin-bottom: 1.5rem; color: var(--gray-600); line-height: 1.6;">
<?php echo $item->short_description; ?>
</div>
<?php endif; ?>
<?php if (!empty($item->features)) : ?>
<div class="osmembership-plan__features" style="flex: 1; margin-bottom: 1.5rem;">
<ul style="list-style: none; padding: 0; margin: 0;">
<?php foreach (explode("\n", $item->features) as $feature) : ?>
<?php if (trim($feature)) : ?>
<li style="padding: 0.5rem 0; display: flex; align-items: flex-start; gap: 0.5rem;">
<span class="icon-check" style="color: var(--success); flex-shrink: 0; margin-top: 0.25rem;"></span>
<span><?php echo htmlspecialchars(trim($feature), ENT_COMPAT, 'UTF-8'); ?></span>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="osmembership-plan__actions">
<a href="<?php echo OSMembershipHelperRoute::getRegistrationRoute($item->id); ?>"
class="btn btn-primary"
style="width: 100%; min-height: 48px; display: inline-flex; align-items: center; justify-content: center; text-decoration: none;">
<?php echo Text::_('OSM_SUBSCRIBE_NOW'); ?>
<span class="icon-chevron-right" style="margin-left: 0.5rem;"></span>
</a>
</div>
</div>
<?php endforeach; ?>
</div>

View File

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

View File

@@ -1,82 +0,0 @@
<?php
/**
* @package AcyMailing
* @subpackage mod_acymailing
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_acymailing module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-acymailing mod-acymailing-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if (!empty($formDisplay)) : ?>
<div class="mod-acymailing__form-container">
<?php if ($params->get('intro_text')) : ?>
<div class="mod-acymailing__intro">
<?php echo $params->get('intro_text'); ?>
</div>
<?php endif; ?>
<?php echo $formDisplay; ?>
<?php if ($params->get('outro_text')) : ?>
<div class="mod-acymailing__outro">
<?php echo $params->get('outro_text'); ?>
</div>
<?php endif; ?>
</div>
<?php else : ?>
<div class="mod-acymailing__empty">
<p><?php echo Text::_('MOD_ACYMAILING_NO_FORM'); ?></p>
</div>
<?php endif; ?>
</div>
<style>
/* Override AcyMailing inline styles for mobile responsiveness */
.mod-acymailing-responsive .acymailing_module input[type="email"],
.mod-acymailing-responsive .acymailing_module input[type="text"] {
min-height: 44px !important;
font-size: 1rem !important;
padding: 0.5rem 0.75rem !important;
border-radius: var(--border-radius, 0.375rem) !important;
border: 1px solid var(--input-border-color, #dee2e6) !important;
width: 100% !important;
box-sizing: border-box !important;
}
.mod-acymailing-responsive .acymailing_module button[type="submit"],
.mod-acymailing-responsive .acymailing_module input[type="submit"] {
min-height: 44px !important;
padding: 0.625rem 1rem !important;
font-size: 1rem !important;
border-radius: var(--border-radius, 0.375rem) !important;
cursor: pointer !important;
}
@media (max-width: 575.98px) {
.mod-acymailing-responsive .acymailing_module input[type="email"],
.mod-acymailing-responsive .acymailing_module input[type="text"] {
font-size: 16px !important;
min-height: 48px !important;
padding: 0.75rem 1rem !important;
}
.mod-acymailing-responsive .acymailing_module button[type="submit"],
.mod-acymailing-responsive .acymailing_module input[type="submit"] {
min-height: 48px !important;
width: 100% !important;
}
}
</style>

View File

@@ -0,0 +1,36 @@
<?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
*/
/**
* Default layout override for mod_articles_archive.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-archive<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-archive__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-archive__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-archive__item">
<a href="<?php echo $item->link; ?>"><?php echo $item->text; ?></a>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,44 @@
<?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
*/
/**
* Default layout override for mod_articles_categories.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$showDescription = $params->get('show_description', 0);
$numitems = $params->get('numitems', 0);
?>
<div class="mod-articles-categories<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-categories__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-categories__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-categories__item">
<a href="<?php echo $item->link; ?>"><?php echo $item->title; ?></a>
<?php if ($numitems) : ?>
<span class="mod-articles-categories__count">(<?php echo $item->numitems; ?>)</span>
<?php endif; ?>
<?php if ($showDescription && !empty($item->description)) : ?>
<p class="mod-articles-categories__description"><?php echo $item->description; ?></p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,78 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_category.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
Factory::getApplication()->getLanguage()->load('mod_articles_category', JPATH_SITE);
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-category<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-category__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-category__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-category__item" itemscope itemtype="https://schema.org/Article">
<?php if ($params->get('link_titles') == 1) : ?>
<a class="mod-articles-category__link" href="<?php echo $item->link; ?>" itemprop="url">
<span itemprop="name"><?php echo $item->title; ?></span>
</a>
<?php else : ?>
<span itemprop="name"><?php echo $item->title; ?></span>
<?php endif; ?>
<?php if ($item->displayHits) : ?>
<span class="mod-articles-category__hits">
(<?php echo $item->displayHits; ?>)
</span>
<?php endif; ?>
<?php if ($params->get('show_author', 0)) : ?>
<span class="mod-articles-category__author">
<?php echo $item->displayAuthorName; ?>
</span>
<?php endif; ?>
<?php if ($item->displayDate) : ?>
<time class="mod-articles-category__date" datetime="<?php echo HTMLHelper::_('date', $item->displayDate, 'c'); ?>" itemprop="datePublished">
<?php echo $item->displayDate; ?>
</time>
<?php endif; ?>
<?php if ($params->get('show_introtext', 0)) : ?>
<div class="mod-articles-category__intro" itemprop="description">
<?php echo $item->displayIntrotext; ?>
</div>
<?php endif; ?>
<?php if ($params->get('show_readmore', 0)) : ?>
<a class="mod-articles-category__readmore" href="<?php echo $item->link; ?>" itemprop="url">
<?php echo Text::_('MOD_ARTICLES_CATEGORY_READ_MORE_TITLE'); ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_latest.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-latest<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-latest__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-latest__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-latest__item" itemscope itemtype="https://schema.org/Article">
<a href="<?php echo $item->link; ?>" itemprop="url">
<span itemprop="name"><?php echo $item->title; ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,58 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_news (newsflash).
* Adds showtitle support with card-based layout.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-news newsflash<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-news__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php foreach ($list as $item) : ?>
<div class="mod-articles-news__item" itemscope itemtype="https://schema.org/Article">
<?php if ($params->get('item_title')) : ?>
<h4 class="mod-articles-news__item-title" itemprop="name">
<?php if ($item->link !== '' && $params->get('link_titles')) : ?>
<a href="<?php echo $item->link; ?>" itemprop="url"><?php echo $item->title; ?></a>
<?php else : ?>
<?php echo $item->title; ?>
<?php endif; ?>
</h4>
<?php endif; ?>
<?php if (!empty($item->afterDisplayTitle)) : ?>
<?php echo $item->afterDisplayTitle; ?>
<?php endif; ?>
<?php if ($params->get('show_introtext', 1)) : ?>
<div class="mod-articles-news__intro" itemprop="description">
<?php echo $item->introtext; ?>
</div>
<?php endif; ?>
<?php if (isset($item->readmore) && $item->readmore) : ?>
<a class="mod-articles-news__readmore" href="<?php echo $item->link; ?>" itemprop="url">
<?php echo $item->linkText; ?>
</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_popular.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-popular<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-popular__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-popular__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-popular__item" itemscope itemtype="https://schema.org/Article">
<a href="<?php echo $item->link; ?>" itemprop="url">
<span itemprop="name"><?php echo $item->title; ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,52 @@
<?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
*/
/**
* Default layout override for mod_banners.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-banners<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-banners__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php foreach ($list as $item) : ?>
<div class="mod-banners__item">
<?php $link = $item->params->get('url') ?: ''; ?>
<?php if ($item->type == 1) : ?>
<?php // Image banner ?>
<?php $imageUrl = $item->params->get('imageurl', ''); ?>
<?php $alt = htmlspecialchars($item->name, ENT_COMPAT, 'UTF-8'); ?>
<?php if ($link) : ?>
<a href="<?php echo htmlspecialchars($link, ENT_COMPAT, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
<img src="<?php echo htmlspecialchars($imageUrl, ENT_COMPAT, 'UTF-8'); ?>" alt="<?php echo $alt; ?>" class="mod-banners__image" loading="lazy" />
</a>
<?php else : ?>
<img src="<?php echo htmlspecialchars($imageUrl, ENT_COMPAT, 'UTF-8'); ?>" alt="<?php echo $alt; ?>" class="mod-banners__image" loading="lazy" />
<?php endif; ?>
<?php else : ?>
<?php // Custom HTML banner ?>
<?php echo $item->custombannercode; ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,48 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_breadcrumbs.
* Bootstrap 5 breadcrumb with schema.org BreadcrumbList markup.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
Factory::getApplication()->getLanguage()->load('mod_breadcrumbs', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<nav class="mod-breadcrumbs<?php echo $suffix ? ' ' . $suffix : ''; ?>" aria-label="<?php echo Text::_('MOD_BREADCRUMBS_HERE'); ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-breadcrumbs__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ol class="breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">
<?php foreach ($list as $key => $item) : ?>
<?php
$isLast = ($key === array_key_last($list));
?>
<li class="breadcrumb-item<?php echo $isLast ? ' active' : ''; ?>" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"
<?php echo $isLast ? ' aria-current="page"' : ''; ?>>
<?php if (!$isLast && $item->link) : ?>
<a href="<?php echo $item->link; ?>" itemprop="item">
<span itemprop="name"><?php echo $item->name; ?></span>
</a>
<?php else : ?>
<span itemprop="name"><?php echo $item->name; ?></span>
<?php endif; ?>
<meta itemprop="position" content="<?php echo $key + 1; ?>" />
</li>
<?php endforeach; ?>
</ol>
</nav>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

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

View File

@@ -1,164 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage mod_cblogin
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_cblogin module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
// Ensure module language file is loaded
$lang = Factory::getLanguage();
$lang->load('mod_cblogin', JPATH_SITE);
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-cblogin mod-cblogin-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if ($type === 'logout') : ?>
<div class="mod-cblogin__logout">
<?php if ($params->get('greeting', 1)) : ?>
<div class="mod-cblogin__greeting">
<?php if ($cbUser) : ?>
<div class="mod-cblogin__avatar">
<?php echo $cbUser->getField('avatar', null, 'html', 'none', 'list'); ?>
</div>
<div class="mod-cblogin__user-info">
<span class="mod-cblogin__username">
<?php echo htmlspecialchars($cbUser->getField('formatname', null, 'html', 'none', 'list'), ENT_COMPAT, 'UTF-8'); ?>
</span>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<form action="<?php echo $action; ?>" method="post" class="mod-cblogin__form">
<div class="mod-cblogin__actions">
<?php if ($params->get('profileLink', 1) && $cbUser) : ?>
<a href="<?php echo $cbUser->getField('canvas', null, 'html', 'none', 'profile'); ?>" class="mod-cblogin__btn mod-cblogin__btn--profile btn btn-secondary">
<span class="icon-user" aria-hidden="true"></span>
<?php echo Text::_('MOD_CBLOGIN_PROFILE'); ?>
</a>
<?php endif; ?>
<button type="submit" name="Submit" class="mod-cblogin__btn mod-cblogin__btn--logout btn btn-primary">
<span class="icon-sign-out" aria-hidden="true"></span>
<?php echo Text::_('MOD_CBLOGIN_LOGOUT'); ?>
</button>
</div>
<input type="hidden" name="op2" value="logout" />
<input type="hidden" name="lang" value="<?php echo $lang; ?>" />
<input type="hidden" name="return" value="<?php echo $return; ?>" />
<?php echo $securityToken; ?>
</form>
</div>
<?php else : ?>
<form action="<?php echo $action; ?>" method="post" name="login<?php echo $moduleId; ?>" id="login<?php echo $moduleId; ?>" class="mod-cblogin__form mod-cblogin__form--login">
<?php if ($params->get('pretext')) : ?>
<div class="mod-cblogin__pretext">
<?php echo $params->get('pretext'); ?>
</div>
<?php endif; ?>
<div class="mod-cblogin__fields">
<div class="mod-cblogin__field">
<label for="modloginusername<?php echo $moduleId; ?>" class="mod-cblogin__label">
<?php echo Text::_('MOD_CBLOGIN_USERNAME'); ?>
</label>
<input
id="modloginusername<?php echo $moduleId; ?>"
type="text"
name="username"
class="mod-cblogin__input form-control"
placeholder="<?php echo Text::_('MOD_CBLOGIN_USERNAME'); ?>"
autocomplete="username"
required
/>
</div>
<div class="mod-cblogin__field">
<label for="modloginpass<?php echo $moduleId; ?>" class="mod-cblogin__label">
<?php echo Text::_('MOD_CBLOGIN_PASSWORD'); ?>
</label>
<input
id="modloginpass<?php echo $moduleId; ?>"
type="password"
name="passwd"
class="mod-cblogin__input form-control"
placeholder="<?php echo Text::_('MOD_CBLOGIN_PASSWORD'); ?>"
autocomplete="current-password"
required
/>
</div>
<?php if ($params->get('remember_me', 1)) : ?>
<div class="mod-cblogin__remember">
<input
id="modloginrememberme<?php echo $moduleId; ?>"
type="checkbox"
name="remember"
class="mod-cblogin__checkbox"
value="yes"
/>
<label for="modloginrememberme<?php echo $moduleId; ?>" class="mod-cblogin__remember-label">
<?php echo Text::_('MOD_CBLOGIN_REMEMBER_ME'); ?>
</label>
</div>
<?php endif; ?>
</div>
<div class="mod-cblogin__actions">
<button type="submit" name="Submit" class="mod-cblogin__btn mod-cblogin__btn--submit btn btn-primary">
<span class="icon-sign-in" aria-hidden="true"></span>
<?php echo Text::_('MOD_CBLOGIN_LOGIN'); ?>
</button>
</div>
<div class="mod-cblogin__links">
<?php if ($params->get('lostpassword_link', 1)) : ?>
<a href="<?php echo $lostPasswordLink; ?>" class="mod-cblogin__link">
<?php echo Text::_('MOD_CBLOGIN_FORGOT_PASSWORD'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
<?php if ($params->get('lostusername_link', 1)) : ?>
<a href="<?php echo $lostUsernameLink; ?>" class="mod-cblogin__link">
<?php echo Text::_('MOD_CBLOGIN_FORGOT_USERNAME'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
<?php if ($params->get('registration_link', 1)) : ?>
<a href="<?php echo $registrationLink; ?>" class="mod-cblogin__link">
<?php echo Text::_('MOD_CBLOGIN_REGISTER'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
</div>
<?php if ($params->get('posttext')) : ?>
<div class="mod-cblogin__posttext">
<?php echo $params->get('posttext'); ?>
</div>
<?php endif; ?>
<input type="hidden" name="op2" value="login" />
<input type="hidden" name="lang" value="<?php echo $lang; ?>" />
<input type="hidden" name="return" value="<?php echo $return; ?>" />
<input type="hidden" name="message" value="0" />
<input type="hidden" name="loginfrom" value="loginmodule" />
<?php echo $securityToken; ?>
</form>
<?php endif; ?>
</div>

View File

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

View File

@@ -1,99 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage mod_comprofilerOnline
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_comprofilerOnline module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
// Ensure module language file is loaded
$lang = Factory::getLanguage();
$lang->load('mod_comprofilerOnline', JPATH_SITE);
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-cb-online mod-cb-online-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if (!empty($onlineUsers)) : ?>
<div class="mod-cb-online__stats">
<div class="mod-cb-online__count">
<span class="mod-cb-online__count-number"><?php echo $totalOnline; ?></span>
<span class="mod-cb-online__count-label">
<?php echo $totalOnline == 1 ? Text::_('MOD_CB_ONLINE_USER') : Text::_('MOD_CB_ONLINE_USERS'); ?>
</span>
</div>
<?php if ($params->get('show_guest_count', 1)) : ?>
<div class="mod-cb-online__breakdown">
<span class="mod-cb-online__breakdown-item">
<span class="icon-users" aria-hidden="true"></span>
<?php echo $membersOnline; ?> <?php echo Text::_('MOD_CB_ONLINE_MEMBERS'); ?>
</span>
<span class="mod-cb-online__breakdown-item">
<span class="icon-eye" aria-hidden="true"></span>
<?php echo $guestsOnline; ?> <?php echo Text::_('MOD_CB_ONLINE_GUESTS'); ?>
</span>
</div>
<?php endif; ?>
</div>
<?php if ($params->get('show_user_list', 1) && !empty($onlineUsers)) : ?>
<div class="mod-cb-online__users">
<h<?php echo $params->get('header_level', 3); ?> class="mod-cb-online__heading">
<?php echo Text::_('MOD_CB_ONLINE_WHO_IS_ONLINE'); ?>
</h<?php echo $params->get('header_level', 3); ?>>
<ul class="mod-cb-online__list">
<?php foreach ($onlineUsers as $user) : ?>
<li class="mod-cb-online__user">
<?php if ($params->get('show_avatar', 1) && !empty($user->avatar)) : ?>
<div class="mod-cb-online__avatar">
<?php echo $user->avatar; ?>
</div>
<?php endif; ?>
<div class="mod-cb-online__info">
<?php if ($params->get('link_names', 1) && !empty($user->link)) : ?>
<a href="<?php echo $user->link; ?>" class="mod-cb-online__name">
<?php echo htmlspecialchars($user->name, ENT_COMPAT, 'UTF-8'); ?>
</a>
<?php else : ?>
<span class="mod-cb-online__name">
<?php echo htmlspecialchars($user->name, ENT_COMPAT, 'UTF-8'); ?>
</span>
<?php endif; ?>
<?php if ($params->get('show_status', 1) && !empty($user->status)) : ?>
<span class="mod-cb-online__status">
<?php echo htmlspecialchars($user->status, ENT_COMPAT, 'UTF-8'); ?>
</span>
<?php endif; ?>
</div>
<?php if ($params->get('show_online_icon', 1)) : ?>
<span class="mod-cb-online__indicator" title="<?php echo Text::_('MOD_CB_ONLINE_NOW'); ?>">
<span class="icon-checkmark" aria-hidden="true"></span>
</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php else : ?>
<div class="mod-cb-online__empty">
<p><?php echo Text::_('MOD_CB_ONLINE_NO_USERS'); ?></p>
</div>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,39 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_custom.
* Adds showtitle support and respects all module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Uri\Uri;
$modId = 'mod-custom' . $module->id;
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
if ($params->get('backgroundimage')) {
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $app->getDocument()->getWebAssetManager();
$wa->addInlineStyle(
'#' . $modId . '{background-image: url("' . Uri::root(true) . '/' . HTMLHelper::_('cleanImageURL', $params->get('backgroundimage'))->url . '");}',
['name' => $modId]
);
}
?>
<div class="mod-custom custom<?php echo $suffix ? ' ' . $suffix : ''; ?>" id="<?php echo $modId; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-custom__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php echo $module->content; ?>
</div>

View File

@@ -1,12 +1,14 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_custom
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Template override for mod_custom adding banner-overlay wrapper pattern.
* Based on Cassiopeia's banner layout approach.
*/
@@ -16,8 +18,10 @@ defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Uri\Uri;
$modId = 'mod-custom' . $module->id;
$moduleclass = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$modId = 'mod-custom' . $module->id;
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
if ($params->get('backgroundimage')) {
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
@@ -28,9 +32,11 @@ if ($params->get('backgroundimage')) {
);
}
?>
<div class="mod-custom custom banner-overlay custom-hero<?php echo $moduleclass ? ' ' . $moduleclass : ''; ?>" id="<?php echo $modId; ?>">
<div class="mod-custom custom banner-overlay custom-hero<?php echo $suffix ? ' ' . $suffix : ''; ?>" id="<?php echo $modId; ?>">
<div class="overlay">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-custom__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php echo $module->content; ?>
</div>
</div>

View File

@@ -0,0 +1,88 @@
<?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
*/
/**
* Default layout override for mod_feed.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
if (!$feed) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$rssurl = $params->get('rssurl', '');
$rsstitle = $params->get('rsstitle', 1);
$rssdesc = $params->get('rssrtl', 0) ? ' feed-rtl' : '';
$rssimage = $params->get('rssimage', 1);
$rssitems = $params->get('rssitems', 5);
$rssitemdesc = $params->get('rssitemdesc', 1);
$word_count = $params->get('word_count', 0);
?>
<div class="mod-feed<?php echo $suffix ? ' ' . $suffix : ''; ?><?php echo $rssdesc; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-feed__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php if ($feed->title && $rsstitle) : ?>
<h4 class="mod-feed__feed-title">
<?php if (!empty($rssurl)) : ?>
<a href="<?php echo htmlspecialchars($rssurl, ENT_COMPAT, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
<?php echo $feed->title; ?>
</a>
<?php else : ?>
<?php echo $feed->title; ?>
<?php endif; ?>
</h4>
<?php endif; ?>
<?php if ($feed->description && $rssdesc) : ?>
<p class="mod-feed__description"><?php echo $feed->description; ?></p>
<?php endif; ?>
<?php if ($rssimage && $feed->image) : ?>
<img src="<?php echo $feed->image->uri; ?>" alt="<?php echo $feed->image->title ?? ''; ?>" class="mod-feed__image" />
<?php endif; ?>
<?php if (!empty($feed->items)) : ?>
<ul class="mod-feed__list">
<?php for ($i = 0, $max = min(count($feed->items), $rssitems); $i < $max; $i++) :
$item = $feed->items[$i];
?>
<li class="mod-feed__item">
<?php if (!empty($item->uri)) : ?>
<a href="<?php echo htmlspecialchars($item->uri, ENT_COMPAT, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
<?php echo $item->title; ?>
</a>
<?php else : ?>
<?php echo $item->title; ?>
<?php endif; ?>
<?php if ($rssitemdesc && !empty($item->content)) :
$desc = $item->content;
if ($word_count) {
$words = explode(' ', strip_tags($desc));
if (count($words) > $word_count) {
$desc = implode(' ', array_slice($words, 0, $word_count)) . '&hellip;';
}
}
?>
<p class="mod-feed__item-description"><?php echo $desc; ?></p>
<?php endif; ?>
</li>
<?php endfor; ?>
</ul>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,85 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_finder (Smart Search).
* Bootstrap 5 search form with showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
// Load component language for search labels
$lang = $app->getLanguage();
$lang->load('com_finder', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$showLabel = $params->get('show_label', 1);
$labelClass = (!$showLabel ? 'visually-hidden ' : '') . 'finder';
Text::script('MOD_FINDER_SEARCH_VALUE');
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $app->getDocument()->getWebAssetManager();
$wa->getRegistry()->addExtensionRegistryFile('com_finder');
if ($params->get('show_autosuggest', 1)) {
$wa->usePreset('awesomplete');
$app->getDocument()->addScriptOptions('finder-search', ['url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false)]);
Text::script('COM_FINDER_SEARCH_FORM_LIST_LABEL');
Text::script('JLIB_JS_AJAX_ERROR_OTHER');
Text::script('JLIB_JS_AJAX_ERROR_PARSE');
}
$wa->useScript('com_finder.finder');
?>
<div class="mod-finder<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-finder__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<form class="mod-finder__form js-finder-searchform form-search" action="<?php echo Route::_($route); ?>" method="get" role="search">
<label for="mod-finder-searchword<?php echo $module->id; ?>" class="<?php echo $labelClass; ?>">
<?php echo $params->get('alt_label', Text::_('JSEARCH_FILTER_SUBMIT')); ?>
</label>
<div class="input-group">
<input type="text" name="q" id="mod-finder-searchword<?php echo $module->id; ?>"
class="js-finder-search-query form-control"
value="<?php echo htmlspecialchars($app->getInput()->get('q', '', 'string'), ENT_COMPAT, 'UTF-8'); ?>"
placeholder="<?php echo Text::_('MOD_FINDER_SEARCH_VALUE'); ?>">
<?php if ($params->get('show_button', 0)) : ?>
<button class="btn btn-primary" type="submit">
<span class="fa-solid fa-magnifying-glass" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('JSEARCH_FILTER_SUBMIT'); ?></span>
</button>
<?php endif; ?>
</div>
<?php $show_advanced = $params->get('show_advanced', 0); ?>
<?php if ($show_advanced == 2) : ?>
<a href="<?php echo Route::_($route); ?>" class="mod-finder__advanced-link mt-2 d-inline-block">
<?php echo Text::_('COM_FINDER_ADVANCED_SEARCH'); ?>
</a>
<?php elseif ($show_advanced == 1) : ?>
<div class="mod-finder__advanced js-finder-advanced mt-2">
<?php echo HTMLHelper::_('filter.select', $query, $params); ?>
</div>
<?php endif; ?>
<?php
$finderHelper = $app->bootModule('mod_finder', 'site')->getHelper('FinderHelper');
echo $finderHelper->getHiddenFields($route);
?>
</form>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,29 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_footer.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-footer<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-footer__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<div class="mod-footer__line1"><?php echo $lineone; ?></div>
<div class="mod-footer__line2"><?php echo Text::_('MOD_FOOTER_LINE2'); ?></div>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

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

View File

@@ -1,110 +0,0 @@
<?php
/**
* @package HikaShop
* @subpackage mod_hikashop_cart
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_hikashop_cart module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-hikashop-cart mod-hikashop-cart-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>" id="hikashop_cart_module<?php echo $params->get('id'); ?>">
<?php if (!empty($cart->products)) : ?>
<div class="mod-hikashop-cart__header">
<span class="mod-hikashop-cart__icon" aria-hidden="true">
<span class="icon-basket"></span>
</span>
<div class="mod-hikashop-cart__summary">
<div class="mod-hikashop-cart__count">
<?php echo count($cart->products); ?>
<?php echo count($cart->products) == 1 ? Text::_('ITEM') : Text::_('ITEMS'); ?>
</div>
<?php if (!empty($cart->total)) : ?>
<div class="mod-hikashop-cart__total">
<?php echo $cart->total->price_value_with_tax_formated; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($params->get('show_products', 1)) : ?>
<div class="mod-hikashop-cart__products">
<?php foreach ($cart->products as $product) : ?>
<div class="mod-hikashop-cart__product">
<?php if (!empty($product->images[0]) && $params->get('show_image', 1)) : ?>
<div class="mod-hikashop-cart__product-image">
<img src="<?php echo $product->images[0]->file_path; ?>"
alt="<?php echo htmlspecialchars($product->product_name, ENT_COMPAT, 'UTF-8'); ?>" />
</div>
<?php endif; ?>
<div class="mod-hikashop-cart__product-details">
<div class="mod-hikashop-cart__product-name">
<?php echo htmlspecialchars($product->product_name, ENT_COMPAT, 'UTF-8'); ?>
</div>
<div class="mod-hikashop-cart__product-quantity">
<?php echo Text::_('QUANTITY'); ?>:
<span class="mod-hikashop-cart__quantity-value"><?php echo $product->cart_product_quantity; ?></span>
</div>
<?php if (!empty($product->prices[0])) : ?>
<div class="mod-hikashop-cart__product-price">
<?php echo $product->prices[0]->price_value_with_tax_formated; ?>
</div>
<?php endif; ?>
</div>
<?php if ($params->get('show_delete', 1)) : ?>
<div class="mod-hikashop-cart__product-remove">
<a href="#"
class="mod-hikashop-cart__remove-btn hikashop_cart_product_delete"
data-product-id="<?php echo $product->product_id; ?>"
title="<?php echo Text::_('HIKA_DELETE'); ?>"
aria-label="<?php echo Text::_('HIKA_DELETE') . ' ' . htmlspecialchars($product->product_name, ENT_COMPAT, 'UTF-8'); ?>">
<span class="icon-remove" aria-hidden="true"></span>
</a>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="mod-hikashop-cart__actions">
<?php if ($params->get('show_cart_button', 1)) : ?>
<a href="<?php echo hikashop_completeLink('cart'); ?>"
class="mod-hikashop-cart__btn mod-hikashop-cart__btn--view btn btn-secondary">
<?php echo Text::_('HIKASHOP_CART_VIEW'); ?>
</a>
<?php endif; ?>
<?php if ($params->get('show_checkout_button', 1)) : ?>
<a href="<?php echo hikashop_completeLink('checkout'); ?>"
class="mod-hikashop-cart__btn mod-hikashop-cart__btn--checkout btn btn-primary">
<?php echo Text::_('HIKASHOP_CHECKOUT'); ?>
</a>
<?php endif; ?>
</div>
<?php else : ?>
<div class="mod-hikashop-cart__empty">
<span class="mod-hikashop-cart__empty-icon" aria-hidden="true">
<span class="icon-basket"></span>
</span>
<p class="mod-hikashop-cart__empty-text">
<?php echo Text::_('HIKASHOP_CART_EMPTY'); ?>
</p>
</div>
<?php endif; ?>
</div>

View File

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

View File

@@ -1,112 +0,0 @@
<?php
/**
* @package K2
* @subpackage mod_k2_content
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_k2_content module
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-k2-content mod-k2-content-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if (count($items)) : ?>
<ul class="mod-k2-content__list">
<?php foreach ($items as $key => $item) : ?>
<li class="mod-k2-content__item">
<?php if ($params->get('itemImage') && !empty($item->imageXSmall)) : ?>
<div class="mod-k2-content__image">
<a href="<?php echo $item->link; ?>" title="<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>">
<img src="<?php echo $item->imageXSmall; ?>" alt="<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>" />
</a>
</div>
<?php endif; ?>
<div class="mod-k2-content__content">
<?php if ($params->get('itemTitle')) : ?>
<h<?php echo $params->get('item_heading', 4); ?> class="mod-k2-content__title">
<a href="<?php echo $item->link; ?>">
<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>
</a>
</h<?php echo $params->get('item_heading', 4); ?>>
<?php endif; ?>
<?php if ($params->get('itemAuthor') || $params->get('itemDateCreated') || $params->get('itemCategory') || $params->get('itemHits')) : ?>
<div class="mod-k2-content__meta">
<?php if ($params->get('itemAuthor')) : ?>
<span class="mod-k2-content__author">
<span class="icon-user" aria-hidden="true"></span>
<?php echo $item->author; ?>
</span>
<?php endif; ?>
<?php if ($params->get('itemDateCreated')) : ?>
<span class="mod-k2-content__date">
<span class="icon-calendar" aria-hidden="true"></span>
<time datetime="<?php echo HTMLHelper::_('date', $item->created, 'c'); ?>">
<?php echo HTMLHelper::_('date', $item->created, Text::_('DATE_FORMAT_LC3')); ?>
</time>
</span>
<?php endif; ?>
<?php if ($params->get('itemCategory')) : ?>
<span class="mod-k2-content__category">
<span class="icon-folder" aria-hidden="true"></span>
<a href="<?php echo $item->categoryLink; ?>">
<?php echo $item->categoryname; ?>
</a>
</span>
<?php endif; ?>
<?php if ($params->get('itemHits')) : ?>
<span class="mod-k2-content__hits">
<span class="icon-eye" aria-hidden="true"></span>
<?php echo $item->hits; ?> <?php echo Text::_('MOD_K2_CONTENT_HITS'); ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($params->get('itemIntroText') && !empty($item->introtext)) : ?>
<div class="mod-k2-content__intro">
<?php echo $item->introtext; ?>
</div>
<?php endif; ?>
<?php if ($params->get('itemReadMore')) : ?>
<div class="mod-k2-content__readmore">
<a href="<?php echo $item->link; ?>" class="mod-k2-content__readmore-link btn btn-secondary">
<?php echo Text::_('MOD_K2_CONTENT_READ_MORE'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
</div>
<?php endif; ?>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php if ($params->get('itemCustomLink')) : ?>
<div class="mod-k2-content__custom-link">
<a href="<?php echo $params->get('itemCustomLinkURL'); ?>" class="btn btn-primary">
<?php echo $params->get('itemCustomLinkTitle'); ?>
</a>
</div>
<?php endif; ?>
<?php else : ?>
<div class="mod-k2-content__empty">
<p><?php echo Text::_('MOD_K2_CONTENT_NO_ITEMS'); ?></p>
</div>
<?php endif; ?>
</div>

View File

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

View File

@@ -1,110 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage mod_kunenalatest
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_kunenalatest module
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-kunena-latest mod-kunena-latest-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if (!empty($posts)) : ?>
<ul class="mod-kunena-latest__list">
<?php foreach ($posts as $post) : ?>
<li class="mod-kunena-latest__item">
<?php if ($params->get('sh_userpic', 1) && !empty($post->getAuthor()->getAvatarImage())) : ?>
<div class="mod-kunena-latest__avatar">
<?php echo $post->getAuthor()->getAvatarImage('', 40, 40); ?>
</div>
<?php endif; ?>
<div class="mod-kunena-latest__content">
<?php if ($params->get('sh_topic', 1)) : ?>
<h<?php echo $params->get('header_level', 4); ?> class="mod-kunena-latest__title">
<a href="<?php echo $post->getUrl(); ?>">
<?php echo htmlspecialchars($post->subject, ENT_COMPAT, 'UTF-8'); ?>
</a>
</h<?php echo $params->get('header_level', 4); ?>>
<?php endif; ?>
<div class="mod-kunena-latest__meta">
<?php if ($params->get('sh_username', 1)) : ?>
<span class="mod-kunena-latest__author">
<span class="icon-user" aria-hidden="true"></span>
<a href="<?php echo $post->getAuthor()->getLink(); ?>">
<?php echo $post->getAuthor()->getName(); ?>
</a>
</span>
<?php endif; ?>
<?php if ($params->get('sh_time', 1)) : ?>
<span class="mod-kunena-latest__date">
<span class="icon-clock" aria-hidden="true"></span>
<time datetime="<?php echo HTMLHelper::_('date', $post->time, 'c'); ?>">
<?php echo $post->getTime(); ?>
</time>
</span>
<?php endif; ?>
<?php if ($params->get('sh_category', 1)) : ?>
<span class="mod-kunena-latest__category">
<span class="icon-folder" aria-hidden="true"></span>
<a href="<?php echo $post->getCategory()->getUrl(); ?>">
<?php echo $post->getCategory()->name; ?>
</a>
</span>
<?php endif; ?>
<?php if ($params->get('sh_hits', 0)) : ?>
<span class="mod-kunena-latest__hits">
<span class="icon-eye" aria-hidden="true"></span>
<?php echo $post->getTopic()->hits; ?>
</span>
<?php endif; ?>
<?php if ($params->get('sh_replies', 0)) : ?>
<span class="mod-kunena-latest__replies">
<span class="icon-comments" aria-hidden="true"></span>
<?php echo $post->getTopic()->getReplies(); ?>
</span>
<?php endif; ?>
</div>
<?php if ($params->get('sh_text', 0) && !empty($post->message)) : ?>
<div class="mod-kunena-latest__excerpt">
<?php echo KunenaHtmlParser::parseBBCode($post->message, $params->get('txt_len', 50)); ?>
</div>
<?php endif; ?>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php if ($params->get('more_link', 1)) : ?>
<div class="mod-kunena-latest__more">
<a href="<?php echo KunenaRoute::_('index.php?option=com_kunena'); ?>"
class="mod-kunena-latest__more-link btn btn-secondary">
<?php echo Text::_('MOD_KUNENALATEST_MORE'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
</div>
<?php endif; ?>
<?php else : ?>
<div class="mod-kunena-latest__empty">
<p><?php echo Text::_('MOD_KUNENALATEST_NO_POSTS'); ?></p>
</div>
<?php endif; ?>
</div>

View File

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

View File

@@ -1,187 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage mod_kunenalogin
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_kunenalogin module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-kunena-login mod-kunena-login-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if ($kunena_my->exists()) : ?>
<!-- Logged in state -->
<div class="mod-kunena-login__profile">
<?php if ($params->get('showAvatar', 1)) : ?>
<div class="mod-kunena-login__avatar">
<?php echo $kunena_my->getAvatarImage('', 60, 60); ?>
</div>
<?php endif; ?>
<div class="mod-kunena-login__user-info">
<div class="mod-kunena-login__username">
<a href="<?php echo $kunena_my->getURL(); ?>">
<?php echo $kunena_my->getName(); ?>
</a>
</div>
<?php if ($params->get('showRank', 1) && !empty($kunena_my->getRank())) : ?>
<div class="mod-kunena-login__rank">
<?php echo $kunena_my->getRank(); ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($params->get('showStats', 1)) : ?>
<div class="mod-kunena-login__stats">
<div class="mod-kunena-login__stat">
<span class="mod-kunena-login__stat-label"><?php echo Text::_('MOD_KUNENALOGIN_POSTS'); ?>:</span>
<span class="mod-kunena-login__stat-value"><?php echo $kunena_my->posts; ?></span>
</div>
<?php if ($params->get('showKarma', 0) && isset($kunena_my->karma)) : ?>
<div class="mod-kunena-login__stat">
<span class="mod-kunena-login__stat-label"><?php echo Text::_('MOD_KUNENALOGIN_KARMA'); ?>:</span>
<span class="mod-kunena-login__stat-value"><?php echo $kunena_my->karma; ?></span>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="mod-kunena-login__actions">
<?php if ($params->get('showProfile', 1)) : ?>
<a href="<?php echo $kunena_my->getURL(); ?>" class="mod-kunena-login__btn btn btn-secondary">
<span class="icon-user" aria-hidden="true"></span>
<?php echo Text::_('MOD_KUNENALOGIN_PROFILE'); ?>
</a>
<?php endif; ?>
<?php if ($params->get('showMessages', 1)) : ?>
<a href="<?php echo KunenaRoute::_('index.php?option=com_kunena&view=user&layout=messages'); ?>"
class="mod-kunena-login__btn btn btn-secondary">
<span class="icon-envelope" aria-hidden="true"></span>
<?php echo Text::_('MOD_KUNENALOGIN_PRIVATE_MESSAGES'); ?>
<?php if (!empty($private_messages)) : ?>
<span class="mod-kunena-login__badge"><?php echo $private_messages; ?></span>
<?php endif; ?>
</a>
<?php endif; ?>
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-kunena-login__logout-form">
<button type="submit" class="mod-kunena-login__btn mod-kunena-login__btn--logout btn btn-primary">
<span class="icon-sign-out" aria-hidden="true"></span>
<?php echo Text::_('MOD_KUNENALOGIN_LOGOUT'); ?>
</button>
<input type="hidden" name="option" value="com_users" />
<input type="hidden" name="task" value="user.logout" />
<input type="hidden" name="return" value="<?php echo $return; ?>" />
<?php echo JHtml::_('form.token'); ?>
</form>
</div>
<?php else : ?>
<!-- Login form -->
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-kunena-login__form">
<?php if ($params->get('pretext')) : ?>
<div class="mod-kunena-login__pretext">
<?php echo $params->get('pretext'); ?>
</div>
<?php endif; ?>
<div class="mod-kunena-login__fields">
<div class="mod-kunena-login__field">
<label for="kunena-login-username-<?php echo $module->id; ?>" class="mod-kunena-login__label">
<?php echo Text::_('MOD_KUNENALOGIN_USERNAME'); ?>
</label>
<input
id="kunena-login-username-<?php echo $module->id; ?>"
type="text"
name="username"
class="mod-kunena-login__input form-control"
placeholder="<?php echo Text::_('MOD_KUNENALOGIN_USERNAME'); ?>"
autocomplete="username"
required
/>
</div>
<div class="mod-kunena-login__field">
<label for="kunena-login-password-<?php echo $module->id; ?>" class="mod-kunena-login__label">
<?php echo Text::_('MOD_KUNENALOGIN_PASSWORD'); ?>
</label>
<input
id="kunena-login-password-<?php echo $module->id; ?>"
type="password"
name="password"
class="mod-kunena-login__input form-control"
placeholder="<?php echo Text::_('MOD_KUNENALOGIN_PASSWORD'); ?>"
autocomplete="current-password"
required
/>
</div>
<?php if ($params->get('showRememberMe', 1)) : ?>
<div class="mod-kunena-login__remember">
<input
id="kunena-login-remember-<?php echo $module->id; ?>"
type="checkbox"
name="remember"
class="mod-kunena-login__checkbox"
value="yes"
/>
<label for="kunena-login-remember-<?php echo $module->id; ?>" class="mod-kunena-login__remember-label">
<?php echo Text::_('MOD_KUNENALOGIN_REMEMBER_ME'); ?>
</label>
</div>
<?php endif; ?>
</div>
<div class="mod-kunena-login__actions">
<button type="submit" class="mod-kunena-login__btn mod-kunena-login__btn--submit btn btn-primary">
<span class="icon-sign-in" aria-hidden="true"></span>
<?php echo Text::_('MOD_KUNENALOGIN_LOGIN'); ?>
</button>
</div>
<div class="mod-kunena-login__links">
<?php if ($params->get('showRegister', 1) && $usersConfig->get('allowUserRegistration')) : ?>
<a href="<?php echo Route::_('index.php?option=com_users&view=registration'); ?>"
class="mod-kunena-login__link">
<?php echo Text::_('MOD_KUNENALOGIN_REGISTER'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
<?php if ($params->get('showForgot', 1)) : ?>
<a href="<?php echo Route::_('index.php?option=com_users&view=reset'); ?>"
class="mod-kunena-login__link">
<?php echo Text::_('MOD_KUNENALOGIN_FORGOT_PASSWORD'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
</div>
<?php if ($params->get('posttext')) : ?>
<div class="mod-kunena-login__posttext">
<?php echo $params->get('posttext'); ?>
</div>
<?php endif; ?>
<input type="hidden" name="option" value="com_users" />
<input type="hidden" name="task" value="user.login" />
<input type="hidden" name="return" value="<?php echo $return; ?>" />
<?php echo JHtml::_('form.token'); ?>
</form>
<?php endif; ?>
</div>

View File

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

View File

@@ -1,74 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage mod_kunenasearch
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_kunenasearch module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-kunena-search mod-kunena-search-responsive ' . $moduleclass_sfx;
$button_pos = $params->get('button_pos', 'right');
$button_text = $params->get('button_text', '');
?>
<div class="<?php echo $wrapperClass; ?>">
<form action="<?php echo KunenaRoute::_('index.php?option=com_kunena&view=search'); ?>"
method="post"
class="mod-kunena-search__form mod-kunena-search__form--button-<?php echo $button_pos; ?>">
<?php if ($button_pos === 'top' || $button_pos === 'left') : ?>
<div class="mod-kunena-search__button-wrapper mod-kunena-search__button-wrapper--<?php echo $button_pos; ?>">
<button type="submit" class="mod-kunena-search__button btn btn-primary">
<?php if ($button_text) : ?>
<?php echo htmlspecialchars($button_text, ENT_COMPAT, 'UTF-8'); ?>
<?php else : ?>
<span class="icon-search" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('MOD_KUNENASEARCH_SEARCH'); ?></span>
<?php endif; ?>
</button>
</div>
<?php endif; ?>
<div class="mod-kunena-search__input-wrapper">
<label for="mod-kunena-search-<?php echo $module->id; ?>" class="visually-hidden">
<?php echo Text::_('MOD_KUNENASEARCH_SEARCH_FORUM'); ?>
</label>
<input
type="search"
name="q"
id="mod-kunena-search-<?php echo $module->id; ?>"
class="mod-kunena-search__input form-control"
placeholder="<?php echo Text::_('MOD_KUNENASEARCH_SEARCH_FORUM'); ?>"
aria-label="<?php echo Text::_('MOD_KUNENASEARCH_SEARCH_FORUM'); ?>"
value="<?php echo htmlspecialchars($params->get('default_value', ''), ENT_COMPAT, 'UTF-8'); ?>"
/>
</div>
<?php if ($button_pos === 'bottom' || $button_pos === 'right') : ?>
<div class="mod-kunena-search__button-wrapper mod-kunena-search__button-wrapper--<?php echo $button_pos; ?>">
<button type="submit" class="mod-kunena-search__button btn btn-primary">
<?php if ($button_text) : ?>
<?php echo htmlspecialchars($button_text, ENT_COMPAT, 'UTF-8'); ?>
<?php else : ?>
<span class="icon-search" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('MOD_KUNENASEARCH_SEARCH'); ?></span>
<?php endif; ?>
</button>
</div>
<?php endif; ?>
<input type="hidden" name="task" value="results" />
<input type="hidden" name="option" value="com_kunena" />
</form>
</div>

View File

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

View File

@@ -1,100 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage mod_kunenastats
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_kunenastats module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-kunena-stats mod-kunena-stats-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<div class="mod-kunena-stats__container">
<?php if ($params->get('sh_latestMemberCount', 1)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-users" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->memberCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_MEMBERS'); ?></div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_latestMember', 1) && !empty($kunena_stats->latestMember)) : ?>
<div class="mod-kunena-stats__stat mod-kunena-stats__stat--latest-member">
<div class="mod-kunena-stats__icon">
<span class="icon-user-plus" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_LATEST_MEMBER'); ?></div>
<div class="mod-kunena-stats__value mod-kunena-stats__value--link">
<a href="<?php echo $kunena_stats->latestMember->getURL(); ?>">
<?php echo $kunena_stats->latestMember->getName(); ?>
</a>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_messageCount', 1)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-comments" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->messageCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_MESSAGES'); ?></div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_topicCount', 1)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-folder-open" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->topicCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_TOPICS'); ?></div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_todayTopicCount', 0)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-calendar-check" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->todayTopicCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_TODAY_TOPICS'); ?></div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_yesterdayTopicCount', 0)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-calendar" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->yesterdayTopicCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_YESTERDAY_TOPICS'); ?></div>
</div>
</div>
<?php endif; ?>
</div>
</div>

View File

@@ -0,0 +1,63 @@
<?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
*/
/**
* Default layout override for mod_languages.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-languages<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-languages__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-languages__list">
<?php foreach ($list as $language) : ?>
<?php $isActive = $language->active ? ' active' : ''; ?>
<li class="mod-languages__item<?php echo $isActive; ?>" dir="<?php echo $language->rtl ? 'rtl' : 'ltr'; ?>">
<?php if ($language->active) : ?>
<span class="mod-languages__link mod-languages__link--active" lang="<?php echo $language->sef; ?>">
<?php else : ?>
<a class="mod-languages__link" href="<?php echo htmlspecialchars($language->link, ENT_COMPAT, 'UTF-8'); ?>" lang="<?php echo $language->sef; ?>">
<?php endif; ?>
<?php if ($params->get('image', 1)) : ?>
<?php if ($language->image) : ?>
<?php echo HTMLHelper::_('image', 'mod_languages/' . $language->image . '.gif', '', null, true); ?>
<?php else : ?>
<span class="mod-languages__badge badge bg-secondary"><?php echo strtoupper($language->sef); ?></span>
<?php endif; ?>
<?php endif; ?>
<?php if ($params->get('show_name', 1)) : ?>
<?php echo $language->title_native; ?>
<?php endif; ?>
<?php if ($language->active) : ?>
</span>
<?php else : ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,126 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_login.
* Bootstrap 5 login form with showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
Factory::getApplication()->getLanguage()->load('mod_login', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-login<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-login__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php if ($type === 'logout') : ?>
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-login__form mod-login__form--logout">
<?php if ($params->get('greeting', 1)) : ?>
<div class="mod-login__greeting">
<?php if (!empty($user->name)) : ?>
<span class="mod-login__name"><?php echo Text::sprintf('MOD_LOGIN_HINAME', htmlspecialchars($user->name, ENT_COMPAT, 'UTF-8')); ?></span>
<?php else : ?>
<span class="mod-login__name"><?php echo Text::sprintf('MOD_LOGIN_HINAME', htmlspecialchars($user->username, ENT_COMPAT, 'UTF-8')); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="mod-login__submit">
<button type="submit" name="Submit" class="btn btn-primary w-100"><?php echo Text::_('JLOGOUT'); ?></button>
</div>
<input type="hidden" name="option" value="com_users">
<input type="hidden" name="task" value="user.logout">
<input type="hidden" name="return" value="<?php echo $return; ?>">
<?php echo HTMLHelper::_('form.token'); ?>
</form>
<?php else : ?>
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-login__form mod-login__form--login">
<?php if ($params->get('pretext')) : ?>
<div class="mod-login__pretext"><?php echo $params->get('pretext'); ?></div>
<?php endif; ?>
<div class="mod-login__field mb-3">
<label for="modlgn-username-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-user" aria-hidden="true"></i></span>
<input id="modlgn-username-<?php echo $module->id; ?>" type="text" name="username" class="form-control" autocomplete="username" placeholder="<?php echo Text::_('JGLOBAL_USERNAME'); ?>">
</div>
</div>
<div class="mod-login__field mb-3">
<label for="modlgn-passwd-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-lock" aria-hidden="true"></i></span>
<input id="modlgn-passwd-<?php echo $module->id; ?>" type="password" name="password" class="form-control" autocomplete="current-password" placeholder="<?php echo Text::_('JGLOBAL_PASSWORD'); ?>">
</div>
</div>
<?php if (!empty($twofactormethods) && count($twofactormethods) > 1) : ?>
<div class="mod-login__field mb-3">
<label for="modlgn-secretkey-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-shield-halved" aria-hidden="true"></i></span>
<input id="modlgn-secretkey-<?php echo $module->id; ?>" type="text" name="secretkey" class="form-control" autocomplete="one-time-code" placeholder="<?php echo Text::_('JGLOBAL_SECRETKEY'); ?>">
</div>
</div>
<?php endif; ?>
<?php if ($params->get('remember', 1)) : ?>
<div class="mod-login__remember form-check mb-3">
<input id="modlgn-remember-<?php echo $module->id; ?>" type="checkbox" name="remember" class="form-check-input" value="yes">
<label for="modlgn-remember-<?php echo $module->id; ?>" class="form-check-label"><?php echo Text::_('JGLOBAL_REMEMBER_ME'); ?></label>
</div>
<?php endif; ?>
<div class="mod-login__submit mb-3">
<button type="submit" name="Submit" class="btn btn-primary w-100"><?php echo Text::_('JLOGIN'); ?></button>
</div>
<?php $usersConfig = \Joomla\CMS\Component\ComponentHelper::getParams('com_users'); ?>
<ul class="mod-login__options list-unstyled small">
<?php if ($usersConfig->get('allowUserRegistration')) : ?>
<li>
<a href="<?php echo Route::_('index.php?option=com_users&view=registration'); ?>">
<?php echo Text::_('MOD_LOGIN_REGISTER'); ?>
</a>
</li>
<?php endif; ?>
<li>
<a href="<?php echo Route::_('index.php?option=com_users&view=remind'); ?>">
<?php echo Text::_('MOD_LOGIN_FORGOT_YOUR_USERNAME'); ?>
</a>
</li>
<li>
<a href="<?php echo Route::_('index.php?option=com_users&view=reset'); ?>">
<?php echo Text::_('MOD_LOGIN_FORGOT_YOUR_PASSWORD'); ?>
</a>
</li>
</ul>
<input type="hidden" name="option" value="com_users">
<input type="hidden" name="task" value="user.login">
<input type="hidden" name="return" value="<?php echo $return; ?>">
<?php echo HTMLHelper::_('form.token'); ?>
<?php if ($params->get('posttext')) : ?>
<div class="mod-login__posttext"><?php echo $params->get('posttext'); ?></div>
<?php endif; ?>
</form>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,95 @@
<?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
*/
/**
* Default layout override for mod_menu.
* Simple list menu with showtitle support, suitable for sidebars and footers.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Helper\ModuleHelper;
$id = '';
if ($tagId = $params->get('tag_id', '')) {
$id = ' id="' . $tagId . '"';
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<nav class="mod-menu<?php echo $suffix ? ' ' . $suffix : ''; ?>"<?php echo $id; ?> aria-label="<?php echo htmlspecialchars($module->title, ENT_COMPAT, 'UTF-8'); ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-menu__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-menu__list nav flex-column">
<?php foreach ($list as $i => &$item) :
$itemParams = $item->getParams();
$class = 'nav-item mod-menu__item item-' . $item->id;
if ($item->id == $default_id) {
$class .= ' default';
}
if ($item->id == $active_id || ($item->type === 'alias' && $itemParams->get('aliasoptions') == $active_id)) {
$class .= ' current';
}
if (in_array($item->id, $path)) {
$class .= ' active';
} elseif ($item->type === 'alias') {
$aliasToId = $itemParams->get('aliasoptions');
if (count($path) > 0 && $aliasToId == $path[count($path) - 1]) {
$class .= ' active';
} elseif (in_array($aliasToId, $path)) {
$class .= ' alias-parent-active';
}
}
if ($item->type === 'separator') {
$class .= ' divider';
}
if ($item->deeper) {
$class .= ' deeper';
}
if ($item->parent) {
$class .= ' parent';
}
echo '<li class="' . $class . '">';
switch ($item->type) :
case 'separator':
case 'component':
case 'heading':
case 'url':
require ModuleHelper::getLayoutPath('mod_menu', 'default_' . $item->type);
break;
default:
require ModuleHelper::getLayoutPath('mod_menu', 'default_url');
break;
endswitch;
if ($item->deeper) {
echo '<ul class="mod-menu__sub nav flex-column ms-3">';
} elseif ($item->shallower) {
echo '</li>';
echo str_repeat('</ul></li>', $item->level_diff);
} else {
echo '</li>';
}
endforeach;
?></ul>
</nav>

View File

@@ -1,9 +1,76 @@
<!DOCTYPE html>
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -1,11 +1,13 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_menu
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - Mobile responsive collapsible dropdown menu override
* Bootstrap 5 responsive navbar with hamburger menu
*/
@@ -29,7 +31,7 @@ $moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COM
<div class="container-fluid">
<!-- Hamburger toggle button for mobile -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainMenuCollapse" aria-controls="mainMenuCollapse" aria-expanded="false" aria-label="Toggle Main Menu">
<span class="navbar-toggler-icon"></span>
<span class="fa-solid fa-bars" aria-hidden="true"></span>
</button>
<!-- Collapsible menu content -->

View File

@@ -1,11 +1,13 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_menu
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - Component item layout
*/

View File

@@ -1,11 +1,13 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_menu
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - Heading item layout
*/

View File

@@ -1,11 +1,13 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_menu
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - Separator item layout
*/

View File

@@ -1,11 +1,13 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_menu
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - URL item layout
*/

Some files were not shown because too many files have changed in this diff Show More